diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/404.html b/404.html new file mode 100644 index 0000000..ada7d69 --- /dev/null +++ b/404.html @@ -0,0 +1,762 @@ + + + +
+ + + + + + + + + + + + + +Piece
¶Position
¶Square
¶Position
¶Square
¶"Board"
¶Player
¶"Board"
¶Player
¶@event_publisher(*SQUARE_EVENT_TYPES, *PIECE_EVENT_TYPES, BeforeAddSquareEvent, AfterAddSquareEvent,
+ BeforeRemoveSquareEvent, AfterRemoveSquareEvent, BeforeTurnChangeEvent, AfterTurnChangeEvent,
+ AfterNewPieceEvent)
+class Board(Cloneable, EventPublisher)
+
def __init__(squares: list[list[Square | None]], players: list[Player], turn_iterator: Iterator[Player], rules: list[Rule] = None)
+
bool
¶def subscribe(event_type: Type[TEvent], callback: Callable[[TEvent], None], priority: int = EventPriority.NORMAL)
+
For all events publisher publishes of type event_type, publish them to self
+ +For all events publisher publishes, publish them to self
+ +def filter_uncapturable_positions(piece: Piece, positions: Iterable[Position]) -> Iterable[Position]
+
"Piece"
¶Iterable[MoveOption]
¶"Piece"
¶MoveOption
¶"Piece"
¶"Square"
¶"Piece"
¶"Square"
¶"Piece"
¶This is what runs on the online example:
+from chessmaker.chess import create_game
+from chessmaker.clients import start_pywebio_chess_server, PIECE_URLS
+
+if __name__ == "__main__":
+ start_pywebio_chess_server(
+ create_game, # (1)
+ supported_options=["chess960", "knooks", "forced_en_passant", "knight_boosting", "omnipotent_f6_pawn",
+ "siberian_swipe", "il_vaticano", "beta_decay", "la_bastarda", "king_cant_move_to_c2",
+ "vertical_castling", "double_check_to_win", "capture_all_pieces_to_win", "duck_chess"],
+ piece_urls=PIECE_URLS |
+ {
+ "Knook": ["https://i.imgur.com/UiWcdEb.png", "https://i.imgur.com/g7xTVts.png"],
+ "Duck": ["https://i.imgur.com/ZZ2WSUq.png", "https://i.imgur.com/ZZ2WSUq.png"]
+ } # (2)
+ ,remote_access=True # (3)
+ )
+
The game_factory argument is a function that creates a new game instance. +The factory function should accept a list of boolean keyword arguments, which are specified in the supported_options argument. +Accepting options are useful if you want to host a server that supports multiple variants of chess, but most of the time you know what variant you want to play, +so it's not needed.
+In order to use custom pieces, you need to provide the URLs of the images as a tuple with a URL +for each player.
+The remote_access argument puts the server on the internet, so you can play with your friends! +It uses pywebio's remote access feature (which internally uses http://localhost.run).
+Now, let's make our own game factory. +This one won't support any custom rules - just the standard chess rules.
+from itertools import cycle
+
+from chessmaker.chess.base import Board
+from chessmaker.chess.base import Game
+from chessmaker.chess.base import Player
+from chessmaker.chess.base import Square
+from chessmaker.chess.pieces import Bishop
+from chessmaker.chess.pieces import King
+from chessmaker.chess.pieces import Knight
+from chessmaker.chess.pieces import Pawn
+from chessmaker.chess.pieces import Queen
+from chessmaker.chess.pieces import Rook
+from chessmaker.chess.results import no_kings, checkmate, stalemate, Repetition, NoCapturesOrPawnMoves
+from chessmaker.clients import start_pywebio_chess_server
+
+
+def _empty_line(length: int) -> list[Square]:
+ return [Square() for _ in range(length)]
+
+def get_result(board: Board) -> str:
+ for result_function in [no_kings, checkmate, stalemate, Repetition(3), NoCapturesOrPawnMoves(50)]:
+ result = result_function(board)
+ if result:
+ return result
+
+piece_row = [Rook, Knight, Bishop, Queen, King, Bishop, Knight, Rook]
+
+def create_game(**_) -> Game:
+ white = Player("white")
+ black = Player("black")
+ turn_iterator = cycle([white, black])
+
+ def _pawn(player: Player):
+ if player == white:
+ return Pawn(white, Pawn.Direction.UP, promotions=[Bishop, Rook, Queen, Knight])
+ elif player == black:
+ return Pawn(black, Pawn.Direction.DOWN, promotions=[Bishop, Rook, Queen, Knight])
+
+ game = Game(
+ board=Board(
+ squares=[
+ [Square(piece_row[i](black)) for i in range(8)],
+ [Square(_pawn(black)) for _ in range(8)],
+ _empty_line(8),
+ _empty_line(8),
+ _empty_line(8),
+ _empty_line(8),
+ [Square(_pawn(white)) for _ in range(8)],
+ [Square(piece_row[i](white)) for i in range(8)],
+ ],
+ players=[white, black],
+ turn_iterator=turn_iterator,
+ ),
+ get_result=get_result,
+ )
+
+ return game
+
+
+if __name__ == "__main__":
+ start_pywebio_chess_server(create_game, debug=True)
+
We aren't going to get into the details of how things work behind the scenes yet, but let's break down what's going on here.
+Our game object is created with 2 arguments. Let's start with the simpler one.
+The get_result argument is the function that will be called to determine the result of the game.
+We create a result function that checks for all the standard end conditions: checkmate, stalemate, repetition, and the 50 move rule.
+For simplicity, we could have also imported StandardResult
for the same effect.
The board argument is the main object we'll be working with, and it is created with 3 arguments. +Again, let's do the simpler arguments first.
+The players argument is a list of the players in the game. In most cases, we'll have 2 players - but it's possible to have more. +The names of the players are currently only used for display purposes.
+Because ChessMaker supports altering the player's turns, we can't just use the player list to determine who's turn it is. +The turn_iterator argument is a generator that will be called to get the next player in the turn order.
+The square argument is a 2D list of Square
objects.
+Squares can also be None
, if we want to make non-rectangular boards or have a board with holes in it.
Each square accepts an optional piece argument, which is the piece that will be placed on the square. +A piece always needs to accept a player argument.
+The first and last ranks are pretty simple. We just create a list of the pieces we want to use, and create a square for each one. +This is because none of the pieces on those ranks need any extra arguments.
+For our pawns, we need to specify the direction they can move in - and what can they promote to. +We can do this using the direction and promotions arguments.
+The result is a complete chess game, with all the standard rules:
+ +Now that we've seen how to create a basic game factory, let's look at how to create a custom one. +In this example, we'll create a 5x5 board, pawns on the corners, kings in the middle of the edge ranks, bishops between the pawns and the kings - and a hole in the middle of the board.
+from itertools import cycle
+
+from chessmaker.chess.base import Board
+from chessmaker.chess.base import Game
+from chessmaker.chess.base import Player
+from chessmaker.chess.base import Square
+from chessmaker.chess.pieces import Bishop
+from chessmaker.chess.pieces import King
+from chessmaker.chess.pieces import Pawn
+from chessmaker.chess.results import StandardResult
+from chessmaker.clients import start_pywebio_chess_server
+
+def _empty_line(length: int) -> list[Square]:
+ return [Square() for _ in range(length)]
+
+
+def create_custom_game(*_):
+ white = Player("white")
+ black = Player("black")
+ turn_iterator = cycle([white, black])
+
+ def _pawn(player: Player):
+ if player == white:
+ return Pawn(white, Pawn.Direction.UP, promotions=[Bishop])
+ elif player == black:
+ return Pawn(black, Pawn.Direction.DOWN, promotions=[Bishop])
+
+ game = Game(
+ board=Board(
+ squares=[
+ [Square(piece(black)) for piece in [_pawn, Bishop, King, Bishop, _down_pawn]],
+ _empty_line(5),
+ _empty_line(2) + [None] + _empty_line(2),
+ _empty_line(5),
+ [Square(piece(white)) for piece in [_pawn, Bishop, King, Bishop, _up_pawn]],
+ ],
+ players=[white, black],
+ turn_iterator=turn_iterator,
+ ),
+ get_result=StandardResult(),
+ )
+
+ return game
+
+
+if __name__ == "__main__":
+ start_pywebio_chess_server(create_custom_game, debug=True)
+
And the result:
+ + + + + + + +Now that we've covered the more general concepts, we can start looking at the game itself.
+This section contains an overview of each concept, and tries to highlight useful methods, +but it's not a complete reference - for that, you should look at the API Reference.
+The game class is a container for the board and the result function.
+It doesn't do much except for having an AfterGameEndEvent
event that is published when the game ends.
game = Game(board, get_result)
+game.subscribe(AfterGameEndEvent, lambda event: print(event.result))
+
The result function is a function that is called after every turn - it takes a board and returns either None or a string. +There is no structure to the string - and it's used to tell the client what the result of the game is, +but if the string returned is not None, the game will end.
+For a result function to have state (e.g. 50 move rule) it should be wrapped in a class that has a __call__
method.
class GetDumbResult:
+ def __init__(self):
+ self.move_count = 0
+
+ def __call__(self, board: Board) -> Optional[str]:
+ self.move_count += 1
+ if self.move_count > 100:
+ return "Draw - I'm bored"
+ return None
+
The board is the main container for all squares. +It also contains the players and the turn iterator.
+It's important to understand is that even though the board contains a turn iterator, +it (or the Game itself) doesn't actually manage a game loop - it leaves that to any client.
+A board contains a 2D list of squares - these squares can be None (e.g. holes)
+to create non-rectangular boards. When a square in the board changes (Not to confuse with when a piece changes)
+the board can publish BeforeRemoveSquareEvent
, AfterRemoveSquareEvent
, BeforeAddSquareEvent
and AfterAddSquareEvent
.
The board also contains a list of players, and a turn iterator.
+The turn iterator is a generator that will be called to get the next player in the turn order.
+When this happens, the board publishes a BeforeChangeTurnEvent
and AfterChangeTurnEvent
.
The board propagates all events from the squares and pieces it contains,
+which is very useful for subscribing to all of them at once.
+and also publishes AfterNewPieceEvent
when a new piece is added to the board.
It also contains a lot of utility methods for getting squares, pieces and players.
+board = Board(squares, players, turn_iterator)
+
+# Get a square
+square = board[Position(0, 0)]
+piece = square.piece
+
+for square in board:
+ print(square.position)
+
+for y in board.size[1]:
+ for x in board.size[0]:
+ print(Position(x, y))
+
+for player_piece in board.get_player_pieces(piece.player):
+ print(player_piece)
+
The player class is a simple container that is used to identify the owner of a piece. +The name chosen is arbitrary - and doesn't have to be unique.
+ +A position is a named tuple that contains the x and y coordinates of a square or piece.
+Position(0, 0)
is at the top left of the board.
position = Position(0, 0)
+print(position)
+print(position.x, position.y)
+print(position[0], position[1])
+
+print(position.offset(1, 1))
+
Info
+While both pieces and squares have a position
attribute, it doesn't need to be changed manually.
+instead the board knows where each piece and square is, and the position
attribute
+simply asks the board for its position.
A square is a container for a piece. When setting a square's piece,
+it can publish BeforeRemovePieceEvent
, AfterRemovePieceEvent
, BeforeAddPieceEvent
and AfterAddPieceEvent
.
The square has an (auto-updating) position
attribute, and a piece
attribute.
board = ...
+square = board[Position(0, 0)]
+print(square.position, square.piece)
+
+square.subscribe(AfterAddPieceEvent, lambda event: print(event.piece))
+
+square.piece = Pawn(player0)
+
The piece is the main class in the base game that is meant to be extended. +The piece is an abstract class, and must be extended to be used.
+A piece has an (immutable) player
attribute, and an (auto-updating) position
attribute.
+It also has a name
class attribute, which is used for display purposes.
The piece also has a board
attribute, which is set when the piece is added to a board.
+Because the piece is created before it's added to the board, trying to access it when it's created will result in an
+error saying Piece is not on the board yet
. To perform startup logic, the piece can implement an
+on_join_board
method, which will be called when the piece is added to the board.
Each piece has to implement a _get_move_options
method, which returns an iterable of what moves the piece can make.
+Then, when the piece is asked for its move options, it will call the _get_move_options
method and publish
+BeforeGetMoveOptionsEvent
and AfterGetMoveOptionsEvent
events.
Then, a move option is selected by the user, and the piece is asked to make the move using .move()
- which
+will publish BeforeMoveEvent
, AfterMoveEvent
, BeforeCapturedEvent
and AfterCapturedEvent
events.
For a piece to implement moves that are more complex than just moving and capturing,
+it should subscribe to its own BeforeMoveEvent
and AfterMoveEvent
events, and implement the logic there.
A MoveOption is used to describe a move that a piece can make.
+A move option has to specify the position
it will move to with the position
attribute,
+and all positions it will capture with the captures
attribute.
In addition, for special moves (e.g. castling, en passant) a move option can have an extra
attribute,
+which is a dict. Ideally, this dict shouldn't contain complex objects like pieces or other dicts, but instead
+positions or other simple objects.
class CoolPiece(Piece):
+ """
+ A piece that can move one square diagonally (down and right).
+ """
+
+ @classmethod
+ @property
+ def name(cls):
+ return "CoolPiece"
+
+ def _get_move_options(self) -> Iterable[MoveOption]:
+ move_position = self.position.offset(1, 1)
+
+ if not is_in_board(self.board, move_position):
+ return
+
+ if (self.board[move_position].piece is not None
+ and self.board[move_position].piece.player == self.player):
+ return
+
+ yield MoveOption(self.position, captures=[move_position])
+
+
+class CoolerPiece(CoolPiece):
+ """
+ A piece that can move one square diagonally (down and right) and turn into a Queen when capturing another cool piece.
+ """
+ def __init__(self):
+ super().__init__()
+ # When listening to yourself, it's a good practice to use a high priority,
+ # to emulate being the default behavior of the piece.
+ self.subscribe(AfterMoveEvent, self._on_after_move, EventPriority.VERY_HIGH)
+
+ @classmethod
+ @property
+ def name(cls):
+ return "CoolerPiece"
+
+ def _get_move_options(self) -> Iterable[MoveOption]:
+ move_options = super()._get_move_options()
+ for move_option in move_options:
+ if isinstance(self.board[move_option.position].piece, CoolPiece):
+ move_option.extra = dict(turn_into_queen=True)
+ yield move_option
+
+ def _on_after_move(self, event: AfterMoveEvent):
+ if event.move_option.extra.get("turn_into_queen"):
+ # To make this extendible, it's a good practice to send Before and After events for this "promotion".
+ self.board[event.move_option.position].piece = Queen(self.player)
+
A rule is a class that can be used to add custom logic to the game. +It is also an abstract class, and must be extended to be used.
+Similarly to pieces, rules also have an on_join_board
method - only that this one is required to implement,
+and gets the board as an argument. It should contain only startup logic (e.g. subscribing to events), and the
+board passed shouldn't be kept in state - instead, callbacks should use the board from the event
+(This is again related to cloneables, and will be explained in the next section).
An as_rule
method is provided to turn a function into a rule, which is useful for stateless rules.
def _on_after_move(event: AfterMoveEvent):
+ if isinstance(event.piece, King):
+ event.board.turn_iterator = chain(
+ [event.board.current_player],
+ event.board.turn_iterator,
+ )
+
+def extra_turn_if_moved_king(board: Board):
+ board.subscribe(AfterMoveEvent, _on_after_move, EventPriority.HIGH)
+
+ExtraTurnIfMovedKing = as_rule(extra_turn_if_moved_king)
+
+
+class ExtraTurnIfMovedKingFirst(Rule):
+
+ def __init__(self):
+ self.any_king_moved = False
+
+
+ def _on_after_move(event: AfterMoveEvent):
+ if not self.any_king_moved and isinstance(event.piece, King):
+ event.board.turn_iterator = chain(
+ [event.board.current_player],
+ event.board.turn_iterator,
+ )
+ self.any_king_moved = True
+
+
+ def on_join_board(self, board: Board):
+ board.subscribe(BeforeMoveEvent, self._on_before_move, EventPriority.HIGH)
+
+
+board = Board(
+ ...,
+ rules=[ExtraTurnIfMovedKing, ExtraTurnIfMovedKingFirst],
+)
+
While ChessMaker isn't dependent on any specific chess rule or piece, +there some concepts that are mainly used by one piece in the standard game.
+One of these is that everything in the board (e.g. everything besides the Game object)
+has a clone
method, which returns a copy of the object.
In the standard game, this is only used for the King implementation (Though it could be useful for +other custom rules too) - so while most your rules and pieces won't have to use the clone method, +they all have to implement it.
+Because a game doesn't have to include a king, other pieces aren't aware of the concepts of
+check and checkmate. This means the king subscribes to BeforeGetMoveOptionsEvent
events
+of all pieces, and checks if any of those moves would make it attacked by simulating
+the move in a cloned board. Simulation is necessary because of custom rules.
+For example - a custom piece could theoretically define that if the King
+moved near it - it would turn into a queen. This means just looking at the move options
+and not actually moving the piece would cause incorrect results.
The board and squares don't really interest us, since they aren't meant to be extended. +So let's focus on pieces and rules.
+Even stateless pieces have to implement the clone method - while this could be implemented +by the base piece class, making all pieces do it makes it harder to forget when +implementing a stateful piece. The is how it would look:
+ +The simplest example for this is the Rook. +While the Rook is a fairly simple piece, it has to know if it has moved or not. +This is because it can only castle if it hasn't moved yet.
+class Rook(Piece):
+ def __init__(self, player, moved=False):
+ super().__init__(player)
+ self._moved = moved
+
+ # Some rook logic...
+
+ def clone(self):
+ return Rook(self.player, self._moved)
+
If you're inheriting from another piece, you should reimplement the clone method. +
+For rules, it's about the same - but a bit easier.
+If your rule doesn't have any state, it should be a function that uses
+the as_rule
helper function anyway - so you don't have to implement the clone method.
def my_simple_rule(board: Board):
+ # Some rule logic...
+
+MySimpleRule = as_rule(my_simple_rule)
+
If your rule has state, you should implement the clone method.
+class ExtraTurnOnFirstKingMove(Rule):
+
+ def __init__(self, player_king_moved: dict[Player, bool] = None):
+ if player_king_moved is None:
+ player_king_moved = defaultdict(bool)
+ self.player_king_moved: dict[Player, bool] = player_king_moved
+
+
+ def _on_after_move(event: AfterMoveEvent):
+ if not self.player_king_moved[event.piece.player] and isinstance(event.piece, King):
+ event.board.turn_iterator = chain(
+ [event.board.current_player],
+ event.board.turn_iterator,
+ )
+ self.player_king_moved[event.piece.player] = True
+
+
+ def on_join_board(self, board: Board):
+ board.subscribe(AfterMoveEvent, self._on_after_move, EventPriority.HIGH)
+
+
+ def clone(self):
+ return ExtraTurnOnFirstKingMove(self.player_king_moved.copy())
+
If you're wondering why ChessMaker doesn't just use copy.deepcopy
- it's because
+deepcopy would copy all attributes, including event subscriptions - which is not
+what we want.
ChessMaker uses a custom event system to allow altering and extending the game logic. +This allows you to add custom logic to the game without having to modify the engine code.
+The event system is inspired by Spigot's Event API.
+The event system defines a base Event
dataclass that all event types inherit from.
+All attributes of the event are immutable by default, and the event exposes
+one function called _set, which allows event types to make a specific attribute mutable.
Generally, things that happen expose a before and after event, +with some only exposing an after event. A common pattern is for after events +to be completely immutable, and for before events to have mutable attributes.
+from dataclasses import dataclass
+from chessmaker.events import Event
+from chessmaker.chess.base.move_option import MoveOption
+
+# An immutable event
+@dataclass(frozen=True)
+class AfterMoveEvent(Event):
+ piece: "Piece"
+ move_option: MoveOption
+
+# A mutable event
+class BeforeMoveEvent(AfterMoveEvent):
+ def set_move_option(self, move_option: MoveOption):
+ self._set("move_option", move_option)
+
Events can also inherit from the CancellableEvent
class,
+which adds a cancelled
attribute and a set_cancelled
function to the event.
from chessmaker.events import CancellableEvent
+
+class BeforeMoveEvent(AfterMoveEvent, CancellableEvent):
+ def set_move_option(self, move_option: MoveOption):
+ self._set("move_option", move_option)
+
Note
+Most of the time, you're going to be subscribing to existing events, +but if you are creating a new event, you should remember events are just dataclasses - and don't actually +implement logic like cancelling or mutating. +It is the publisher's responsibility to use the mutated event in the correct way.
+To subscribe to events, you need to subscribe to a publisher with the event type and callback function. +Events are subscribed to on a per-instance basis - when you subscribe to a Pawn moving, +it will only subscribe to that specific pawn - not all pawns.
+import random
+
+board: Board = ...
+
+def on_after_turn_change(event: BeforeTurnChangeEvent):
+ if random.random() < 0.5:
+ event.set_cancelled(True)
+ else:
+ event.set_next_player(event.board.players[1])
+
+
+
+board.subscribe(BeforeTurnChangeEvent, on_after_turn_change)
+
Tip
+In you event's callback function, you should use the arguments from the event,
+rather than using ones from your outer scope (For example, board
in the above example).
+This is related to Cloneables, and will be explained later.
Events can be subscribed to with a priority, which determines the order in which they are called - +a higher priority means the event is called earlier.
+For most use cases, the default priority of 0
is fine,
+but if you need to ensure your event is called before or after another event,
+you can either use the EventPriority
enum to specify a priority, or use an integer for more fine-grained control.
from chessmaker.events import EventPriority
+
+board.subscribe(BeforeTurnChangeEvent, on_after_turn_change)
+board.subscribe(BeforeTurnChangeEvent, on_after_turn_change, priority=EventPriority.VERY_LOW)
+board.subscribe(BeforeTurnChangeEvent, on_after_turn_change, 2000)
+
You can also subscribe to all events of an instance by using the subscribe_all
function.
def on_any_event(_: Event):
+ print("Something happened to the board!")
+
+board.subscribe_all(on_any_event)
+
To unsubscribe from events, you need to call the unsubscribe
function with the same arguments you used to subscribe.
+Similarly, you can use unsubscribe_all
to unsubscribe from all events of an instance.
board.unsubscribe(BeforeTurnChangeEvent, on_after_turn_change)
+board.unsubscribe_all(on_any_event)
+
If you're adding new code, and want to make that code extendible - it is recommended to publish events.
+For an instance to publish events, it needs to use the @event_publisher
decorator,
+and specify the event types it publishes.
If it inherits from another publisher, you need use the same decorator +to specify the additional event types it publishes, +If it doesn't publish any additional events, you don't have to use the decorator at all.
+For typing and completion purposes, a publisher should also inherit from EventPublisher
.
+(If it doesn't inherit from another publisher).
from chessmaker.events import EventPublisher, event_publisher
+
+@event_publisher(BeforePrintEvent, AfterPrintEvent)
+class MyPrinter(EventPublisher):
+
+ def print_number(self):
+ number = str(random.randint(0, 100))
+ before_print_event = BeforePrintEvent(self, number)
+ self.publish(before_print_event)
+ if not before_print_event.cancelled:
+ print(before_print_event.number)
+ self.publish(AfterPrintEvent(self, number))
+
Sometimes, you may want to publish events from a publisher to another one. +You can do this either to all event types, or to a specific one.
+@event_publisher(BeforePrintEvent, AfterPrintEvent)
+class MyPrinterManager(EventPublisher):
+
+ def __init__(self, my_printer: MyPrinter):
+ self.my_printer = my_printer
+ self.my_printer.propagate_all(self)
+ self.my_printer.propagate(BeforePrintEvent, self)
+
Now, every time the printer publishes an event, the manager will also publish it. +Currently, you can not unpropagate events.
+Info
+The main use of this in the game is the board propagating all events of its pieces and squares to itself. +This means that instead of subscribing to a specific piece move, you can subscribe to all pieces moving by subscribing to the board.
+Chess maker isn't designed around any specific chess rule or piece. +This means any variant you make is a first-class citizen and should work +just as fine as the standard game.
+This section covers the base concepts you'll have to learn in order to create these +variants.
+However - you will probably not need to know all concepts in order to create a simple rule. +It's recommended to skim through the concepts section, and then head to the tutorial that +interests you.
+ + + + + + +Out of all custom rules we'll cover, this is probably the simplest one.
+We're going to create a new result function - capture_all_pieces_to_win
,
+it accepts a board and returns a descriptive string if the game is over -
+otherwise it returns None
.
from chessmaker.chess.base import Board
+
+
+def capture_all_pieces_to_win(board: Board) -> str | None:
+ lost_players = []
+ for player in board.players:
+ if len(list(board.get_player_pieces(player))) == 0: # (1)
+ lost_players.append(player)
+
+ if len(lost_players) == 0: # (2)
+ return None
+
+ if len(lost_players) == 1: # (3)
+ return f"All pieces captured - {lost_players[0].name} loses"
+
+ return "Multiple players have no pieces - Draw" # (4)
+
None
.Then, when we create a new game, we can pass this function to the result_function
argument:
And that's it! We can now play a game where the only winning condition is to capture all pieces.
+Even though we don't want to be able to win by checkmate in this variant, +we might still want to have stalemate, repetition and other result functions.
+To do this, we can change our result function to a class, and add in other results:
+from chessmaker.chess.results import stalemate, Repetition, NoCapturesOrPawnMoves
+
+class CaptureAllPiecesToWin:
+ def __init__(self):
+ self.result_functions = [capture_all_pieces_to_win, stalemate, Repetition(), NoCapturesOrPawnMoves()]
+
+ def __call__(self, board: Board):
+ for result_function in self.result_functions:
+ result = result_function(board)
+ if result:
+ return result
+
Note
+Results that hold state (like repitition or compuond results like ours) should always be classes +(and not functions), so they can be copied.
+For a game mode like this, starting with a king is not required +(though it's still possible to do so).
+However, if we would want to start with a king that can't be checked, +we would have to change some more things when initializing the board.
+Thankfully, the default King implementation supports an attackable
argument
+(which defaults to False), so we can just set it to true:
_king = lambda player: King(player, attackable=True)
+
+game = Game(
+ board=Board(
+ squares=[
+ [Square(_king(black)), ...],
+ ...
+ ],
+ ...
+ )
+ get_result=capture_all_pieces_to_win,
+)
+
attackable
argument?In this case, it was convenient that the King class had an attackable
argument
+(for another purpose), but how would we implement this if it didn't? Because a custom
+king implementation is a lot of work, we could just inherit from the default King class.
+It would require looking a bit at the source code - but we would quickly see the
+startup logic for the King's check handling is done in on_join_board
, so we would just override it:
+
This rule is pretty simple to define, and it works similarly to how the king works. +When a certain condition is met, we force all the player's pieces to make only specific moves.
+In the king's case, the condition is that the king can't be attacked. +And in our case, the condition is that the player can en passant.
+After a turn changed:
+For all
+We need to
+And if any of the pawns can:
+For all
+We need to
+from collections import defaultdict
+
+from chessmaker.chess.base.board import Board
+from chessmaker.chess.base.game import AfterTurnChangeEvent
+from chessmaker.chess.base.piece import BeforeGetMoveOptionsEvent
+from chessmaker.chess.base.player import Player
+from chessmaker.chess.base.rule import Rule
+from chessmaker.events import EventPriority
+
+class ForcedEnPassant(Rule):
+ def __init__(self, can_en_passant: dict[Player, bool] = None):
+ if can_en_passant is None:
+ can_en_passant = defaultdict(lambda: False)
+ self.can_en_passant: dict[Player, bool] = can_en_passant
+
+ def on_join_board(self, board: Board):
+ board.subscribe(BeforeGetMoveOptionsEvent, self.on_before_get_move_options, EventPriority.LOW)
+ board.subscribe(AfterTurnChangeEvent, self.on_turn_change)
+
+ def on_turn_change(self, event: AfterTurnChangeEvent):
+ pass
+
+ def on_before_get_move_options(self, event: BeforeGetMoveOptionsEvent):
+ pass
+
+ def clone(self):
+ return ForcedEnPassant(can_en_passant=self.can_en_passant.copy())
+
Here too, use the annotations to help you understand what's going on.
+from chessmaker.chess.pieces import Pawn
+
+ def on_turn_change(self, event: AfterTurnChangeEvent):
+ for player in event.board.players: # (1)
+ self.can_en_passant[player] = False
+
+ for piece in event.board.get_player_pieces(player):
+
+ if isinstance(piece, Pawn): # (2)
+ move_options = piece.get_move_options()
+
+ if any(move_option.extra.get("en_passant") for move_option in move_options): # (3)
+ self.can_en_passant[player] = True
+ break # (4)
+
from chessmaker.chess.base.piece import BeforeGetMoveOptionsEvent
+
+ def on_before_get_move_options(self, event: BeforeGetMoveOptionsEvent):
+ if self.can_en_passant[event.piece.player]: # (1)
+ move_options = []
+ for move_option in event.move_options:
+ if move_option.extra.get("en_passant"): # (2)
+ move_options.append(move_option)
+ event.set_move_options(move_options) # (3)
+
Now that we've implemented our rule, we can add it to the board:
+ +And that's it! We've implemented a rule that makes en passant forced when it's possible. +Let's see it in action:
+ + + + + + + +A Knook is a new piece that can move both like a knight and a rook. +It is created by merging a knight and a rook of the same player.
+While this rule could be implemented with 1 new piece and a 1 new rule, +We're going to implement it with 3 new pieces and no rules - +to demonstrate the flexibility of creating new variants.
+The first thing we need to do is create the Knook piece. +Then, we'll add a way for the knight and the rook to be merged into one.
+Tip
+If you're just looking for how to create a new piece, don't be +scared by the length of this tutorial. Most of it is just for the merging.
+The Knook can move like a knight and a rook.
+We can ue functions in piece_utils
to help us implement this.
from functools import partial
+from typing import Iterable
+
+from chessmaker.chess.base.move_option import MoveOption
+from chessmaker.chess.base.piece import Piece
+from chessmaker.chess.piece_utils import filter_uncapturable_positions, is_in_board, \
+ get_straight_until_blocked, positions_to_move_options
+
+MOVE_OFFSETS = [(1, 2), (2, 1), (2, -1), (1, -2), (-1, -2), (-2, -1), (-2, 1), (-1, 2)] # (1)
+
+class Knook(Piece):
+ @classmethod
+ @property
+ def name(cls):
+ return "Knook" # (2)
+
+ def _get_move_options(self) -> Iterable[MoveOption]: # (3)
+ positions = [self.position.offset(*offset) for offset in MOVE_OFFSETS]
+ positions = filter(partial(is_in_board, self.board), positions)
+ positions = filter_uncapturable_positions(self, positions) # (4)
+
+ positions += filter_uncapturable_positions(self,
+ get_straight_until_blocked(self)
+ ) # (5)
+
+ return positions_to_move_options(self.board, positions) # (6)
+
+ def clone(self):
+ return Knook(self.player)
+
MOVE_OFFSETS
constant._get_move_options
method is called when the piece is asked for its move options.
+ It returns an iterable of MoveOption
objects.MoveOption
objects. The positions_to_move_options
+ function is a helper function adds the captures
argument if the position is occupied by
+ a piece.The UI is independent of the game logic. And theoretically, you could use any UI you want. +However, since ChessMaker is packaged with a UI, this tutorial will also show how to add +the Knook to it.
+The start_pywebio_chess_server
function accepts an optional PIECE_URLS argument.
+The argument is a dictionary where the keys are the names of the pieces, and the values
+are tuples of URLs, with as many amount of players you want to support.
The pywebio_ui
also exposes a PIECE_URLS
constant, which is a dictionary of the default
+pieces. We can use it to create a new dictionary with the Knook.
from chessmaker.clients.pywebio_ui import start_pywebio_chess_server, PIECE_URLS
+
+if __name__ == "__main__":
+ start_pywebio_chess_server(
+ create_game,
+ piece_urls=PIECE_URLS | {"Knook": ("https://i.imgur.com/UiWcdEb.png", "https://i.imgur.com/g7xTVts.png")}
+ )
+
And that's it for the new piece! If we didn't want to have the piece created by merging, +this would be very simple. However, we have some more work to do.
+Now that we have the Knook, we need to implement a way to create it by +merging a knight and a rook.
+As mentioned before, this is possible to do by creating a new rule,
+but for the sake of this tutorial, we'll implement it with 2 new pieces.
+We'll create a KnookableKnight
and a KnookableRook
.
Because both the new knight and the new rook need to have similar logic (yet not identical), +we'll create a few helper functions that will be used by both pieces.
+First, we'll define an empty interface called Knookable
that will let
+a mergeable piece know that it can be merge with another piece.
Then, we'll create a helper function that will return move options that +are available for merging.
+The idea is that a piece will provide where it can move to, +and the merge move options will return the MoveOptions that are +occupied by a piece that it can be merged with it, +along with extra information about the merge in the move option, +so that the merge can be done later.
+from typing import Iterable
+
+from chessmaker.chess.base.move_option import MoveOption
+from chessmaker.chess.base.piece import Piece
+from chessmaker.chess.base.position import Position
+from chessmaker.chess.pieces.knook.knookable import Knookable
+
+
+def get_merge_move_options(piece: Piece, positions: Iterable[Position]) -> Iterable[MoveOption]:
+ for position in positions:
+ position_piece = piece.board[position].piece
+
+ if position_piece is not None and position_piece.player == piece.player: # (1)
+ if isinstance(position_piece, Knookable) and not isinstance(position_piece, type(piece)): # (2)
+ yield MoveOption(position, extra=dict(knook=True)) # (3)
+
knook
extra argument set to True
,
+ so that we can later easily know that this move option is for merging.We'll create another helper function that will perform the merge,
+given an AfterMoveEvent
event - and both of our new pieces will
+subscribe to it with that function.
To make our rule extendible, we'll also publish events when a merge +occurs - but because these are new events that are not part of the core game, +it's up to us how and what to publish.
+from dataclasses import dataclass
+
+from chessmaker.chess.base.piece import Piece, AfterMoveEvent
+from chessmaker.chess.pieces.knook.knook import Knook
+from chessmaker.events import Event
+
+@dataclass(frozen=True)
+class AfterMergeToKnookEvent(Event):
+ piece: Piece # (1)
+ knook: Knook
+
+
+class BeforeMergeToKnookEvent(AfterMergeToKnookEvent):
+ def set_knook(self, knook: Knook): # (2)
+ self._set("knook", knook) # (3)
+
+def on_after_move(event: AfterMoveEvent): # (4)
+ if event.move_option.extra.get("knook"): # (5)
+ piece = event.piece
+ before_merge_to_knook_event = BeforeMergeToKnookEvent(
+ piece,
+ Knook(event.piece.player)
+ ) # (6)
+ event.piece.publish(before_merge_to_knook_event)
+
+ knook = before_merge_to_knook_event.knook # (7)
+ piece.board[event.move_option.position].piece = knook # (8)
+
+ piece.publish(AfterMergeToKnookEvent(piece, knook))
+
BeforeMergeToKnookEvent
event that will allow subscribers to
+ change the Knook
object that will be created.Event
s _set
method to change the knook
attribute,
+ which can't be changed regularly (because we want the rest of the event to be immutable).AfterMoveEvent
event - meaning at the time
+ this function is called, the initiating piece has moved to the piece it
+ wants to merge with - which is now not on the board anymore. Now we just
+ have to change the piece on the new position to a Knook
.knook
extra argument set to True
.BeforeMergeToKnookEvent
event in a separate variable,
+ so that we can still access it after we publish it.knook
object from the event, so that subscribers can change it.A tricky part here is that it's very tempting to think we can just
+pass the Knight and Rook's move options to the get_merge_move_options
-
+but in fact, those move options already filtered out positions where
+there's a piece of the same player, so we'll have to re-create the move options
+partially.
We'll still want to inherit from the Knight and Rook, so that pieces
+which check for the type of the piece (using isinstance
) will still work.
The annotations here will only be for the KnookableKnight
class,
+since it's about the same for both.
from functools import partial
+from itertools import chain
+from typing import Iterable
+
+from chessmaker.chess.base.move_option import MoveOption
+from chessmaker.chess.base.piece import AfterMoveEvent
+from chessmaker.chess.base.player import Player
+from chessmaker.chess.pieces import knight
+from chessmaker.chess.pieces.knight import Knight
+from chessmaker.chess.pieces.knook.knookable import Knookable
+from chessmaker.chess.piece_utils import is_in_board, get_straight_until_blocked
+from chessmaker.chess.pieces.rook import Rook
+from chessmaker.chess.pieces.knook.merge_to_knook import get_merge_move_options, merge_after_move, \
+ MERGE_TO_KNOOK_EVENT_TYPES
+from chessmaker.events import EventPriority, event_publisher
+
+
+@event_publisher(*MERGE_TO_KNOOK_EVENT_TYPES) # (1)
+class KnookableKnight(Knight, Knookable):
+ def __init__(self, player):
+ super().__init__(player)
+ self.subscribe(AfterMoveEvent, merge_after_move, EventPriority.VERY_HIGH) # (2)
+
+ def _get_move_options(self):
+ positions = [self.position.offset(*offset) for offset in knight.MOVE_OFFSETS] # (3)
+ positions = list(filter(partial(is_in_board, self.board), positions))
+ merge_move_options = get_merge_move_options(self, positions) # (4)
+
+ return chain(super()._get_move_options(), merge_move_options) # (5)
+
+ def clone(self):
+ return KnookableKnight(self.player)
+
+
+
+@event_publisher(*MERGE_TO_KNOOK_EVENT_TYPES)
+class KnookableRook(Rook, Knookable):
+ def __init__(self, player: Player, moved: bool = False):
+ super().__init__(player, moved)
+ self.subscribe(AfterMoveEvent, merge_after_move, EventPriority.VERY_HIGH)
+
+ def _get_move_options(self) -> Iterable[MoveOption]:
+ positions = list(get_straight_until_blocked(self))
+ merge_move_options = get_merge_move_options(self, positions)
+
+ return chain(super()._get_move_options(), merge_move_options)
+
+ def clone(self):
+ return KnookableRook(self.player, self.moved)
+
Knight
and Knookable
,
+ we also want to inherit from EventPublisher
- because we want to specify
+ we're publishing more events than the base Piece
class. This isn't
+ necessary, but it's good practice.AfterMoveEvent
with the helper function we created
+ earlier. It's a good practice to set the priority to VERY_HIGH
when subscribing
+ to your own events, because you want all other subscribers to have the changes
+ you make.MOVE_OFFSETS
constant, we would just create
+ our own.get_merge_move_options
function.get_merge_move_options
function to the
+ move options from the Knight
class. We could have also just created
+ the Knight's move options, since we already did some of the work needed for it.Now that we have both our new pieces, we're almost done! +We just need to create a board that uses our Knookable pieces. +It's also important to remember to use it in other references to Knight and Rook +in the board creation, such as in promotion - otherwise the promotion +will create an unmergeable piece.
+board = Board(
+ squares=[
+ [KnookableRook(black), KnookableKnight(black), ...],
+ [Pawn(black, Pawn.Direction.DOWN, promotions=[KnookableKnight, KnookableRook, ...])],
+ ...
+ ],
+ ...
+)
+
And that's it! We now have a fully functional chess game with a new piece. +Let's see it in action:
+ + + + + + + +For this rule, we're going to implement something quite special.
+The Omnipotent F6 Pawn rule is another rule that originated from r/AnarchyChess. +Without getting into it's history, the rule makes it so that if the enemy has a piece +on the F6 square, then a pawn can be summoned there instead.
+Creating a rule like this is tricky. It challenges some assumptions that we've made +like that each move belongs to a piece. Luckily, ChessMaker's flexibility allows us +to implement this rule without breaking any of the assumptions.
+What we're going to do is make it so each piece has an extra move option that +summons a pawn to the F6 square - and doesn't affect the piece itself.
+We'll do this by using the BeforeMoveEvent.set_cancelled()
method.
+We'll cancel the move, and instead summon a pawn to the F6 square
+(and publish events for it as if it was a regular move).
So, we want to add a move option to all pieces, and use BeforeMoveEvent
+once any piece has moved with that move option to cancel the move and summon a pawn instead.
For all
+We need to
+from typing import Callable
+
+from chessmaker.chess.base.board import Board
+from chessmaker.chess.base.piece import BeforeGetMoveOptionsEvent, BeforeMoveEvent
+from chessmaker.chess.base.player import Player
+from chessmaker.chess.base.position import Position
+from chessmaker.chess.base.rule import Rule
+from chessmaker.chess.pieces import Pawn
+from chessmaker.events import EventPriority
+
+F6 = Position(5, 2) # (1)
+
+
+class OmnipotentF6Pawn(Rule):
+ def __init__(self, pawn: Callable[[Player], Pawn]):
+ self._pawn = pawn # (2)
+
+ def on_join_board(self, board: Board):
+ board.subscribe(BeforeGetMoveOptionsEvent, self.on_before_get_move_options, EventPriority.HIGH)
+ board.subscribe(BeforeMoveEvent, self.on_before_move)
+
+ def on_before_get_move_options(self, event: BeforeGetMoveOptionsEvent):
+ pass
+
+ def on_before_move(self, event: BeforeMoveEvent):
+ pass
+
+
+ def clone(self):
+ return OmnipotentF6Pawn(pawn=self._pawn)
+
Pawn
class directly is that because it
+ accepts more arguments that we don't have - like the pawn's direction
+ and what it can promote to.Again, use the annotations to help you understand what's going on.
+from itertools import chain
+from chessmaker.chess.base.move_option import MoveOption
+
+
+def on_before_get_move_options(self, event: BeforeGetMoveOptionsEvent):
+ f6_piece = event.piece.board[F6].piece
+
+ if f6_piece is not None and f6_piece.player != event.piece.player: # (1)
+ move_option = MoveOption(F6, extra=dict(summon_pawn=True), captures={F6}) # (2)
+ event.set_move_options(chain(event.move_options, [move_option])) # (3)
+
chain
function to add the move option to the existing move options.from chessmaker.chess.base.piece import AfterMoveEvent, BeforeCapturedEvent AfterCapturedEvent
+
+
+def on_before_move(self, event: BeforeMoveEvent):
+ if event.move_option.extra.get("summon_pawn"):
+ captured_piece = event.piece.board[F6].piece # (1)
+ captured_piece.publish(BeforeCapturedEvent(captured_piece)) # (2)
+
+ pawn = self._pawn(event.piece.player)
+ event.piece.board[F6].piece = pawn # (3)
+
+ captured_piece.publish(AfterCapturedEvent(captured_piece))
+ pawn.publish(AfterMoveEvent(pawn, event.move_option)) # (4)
+
+ event.set_cancelled(True) # (5)
+
AfterCaptureEvent
later.BeforeCapturedEvent
and not a BeforeMoveEvent
event because
+ the BeforeMoveEvent
event has already been published (we're a subscriber to it).Again, all that's left is to add the rule to the board. Though this time it requires +a bit more work:
+def _pawn(player: Player):
+ if player == white:
+ return Pawn(white, Pawn.Direction.UP, promotions=[Bishop, Rook, Queen, Knight])
+ elif player == black:
+ return Pawn(black, Pawn.Direction.DOWN, promotions=[Bishop, Rook, Queen, Knight])
+
+board = Board(
+ ...
+ rules=[OmnipotentF6Pawn(pawn=_pawn)]
+)
+
And that's it! This was probably the most complicated rule we've made so far, +and it shows how we can do almost anything with ChessMaker.
+ + + + + + + +Siberian Swipe is a rule that originated from r/AnarchyChess. +With rules like this, there's no official place we can find out how it works, +so we have to define it ourselves.
+When a rook hasn't moved yet, it can skip any pieces on it's file +in order to capture a distant enemy rook.
+Now that we know what the rule does, we can start building our rule.
+Because our rule only applies to Rooks - we only need to subscribe to
+rook events. And because we only need to add to where the rook can move to,
+and not how the movement works (We're just capturing a piece - not doing
+anything special like summoning a piece or moving a piece to a different
+location) - we only need to subscribe to the BeforeGetMoveOptions
event.
In addition, if rooks are added in the middle of the game (For example by a promotion), +the rule also needs to apply to them. So we need to do 3 things:
+For all
+We need to
+from chessmaker.chess.base.rule import Rule
+from chessmaker.chess.base.board import AfterNewPieceEvent, Board
+from chessmaker.chess.base.piece import Piece, BeforeGetMoveOptionsEvent
+from chessmaker.chess.pieces.rook import Rook
+from chessmaker.events import EventPriority
+
+class SiberianSwipe(Rule):
+ def on_join_board(self, board: Board):
+ for piece in board.get_pieces():
+ self.subscribe_to_piece(piece)
+ board.subscribe(AfterNewPieceEvent, self.on_new_piece)
+
+ def subscribe_to_piece(self, piece: Piece):
+ if isinstance(piece, Rook):
+ piece.subscribe(BeforeGetMoveOptionsEvent, self.on_before_get_move_options, EventPriority.HIGH) # (1)
+
+ def on_new_piece(self, event: AfterNewPieceEvent):
+ self.subscribe_to_piece(event.piece)
+
+ def on_before_get_move_options(self, event: BeforeGetMoveOptionsEvent):
+ pass # (2)
+
+ def clone(self):
+ return SiberianSwipe() # (3)
+
Now all that's left to do is implement on_before_get_move_options
.
+Use the annotations to help you understand what's going on.
from itertools import chain
+
+from chessmaker.chess.base.move_option import MoveOption
+from chessmaker.chess.base.piece import BeforeGetMoveOptionsEvent
+from chessmaker.chess.piece_utils import is_in_board
+from chessmaker.chess.pieces.rook import Rook
+
+ def on_before_get_move_options(self, event: BeforeGetMoveOptionsEvent):
+ move_options = event.move_options
+ rook: Rook = event.piece
+ board = event.piece.board # (1)
+ player = event.piece.player
+ position = event.piece.position
+ new_move_options = []
+
+ if rook.moved: # (2)
+ return
+
+ for direction in [(0, 1), (0, -1)]: # (3)
+ enemy_position = position.offset(*direction)
+
+ while is_in_board(board, enemy_position): # (4)
+ enemy_piece = board[enemy_position].piece
+
+ if isinstance(enemy_piece, Rook) and enemy_piece.player != player: # (5)
+ move_option = MoveOption(
+ enemy_position,
+ captures={enemy_position},
+ extra=dict(siberian_swipe=True) # (6)
+ )
+ new_move_options.append(move_option)
+
+ enemy_position = enemy_position.offset(*direction)
+
+ event.set_move_options(chain(move_options, new_move_options)) # (7)
+
on_join_board
.piece_utils.is_in_board
to check if we reached the edge of the board (or a hole).extra=dict(siberian_swipe=True)
to the move option. For our use case,
+this is only used for display purposes, but if we wanted to modify how the move works
+using the BeforeMoveEvent
and AfterMoveEvent
events, we could use this to check
+if the move is a siberian swipe.Now that we've implemented our rule, we can add it to the board:
+ +And that's it! We've implemented a rule that adds a new move option to rooks. +Let' see it in action:
+ + + + + + + +An easily extendible chess implementation designed +to support any custom rule or feature.
++Play: https://chessmaker.azurewebsites.net +
+Documentation: https://wolfdwyc.github.io/ChessMaker
+Source Code: https://github.com/WolfDWyc/ChessMaker
+ChessMaker is a Python (3.11+) chess implementation that can be extended to support any custom rule or feature. +It allows you to build almost any variant you can think of easily and quickly.
+It was inspired by r/AnarchyChess - and the packaged optional rules are almost all inspired by that subreddit.
+ChessMaker isn't tied to any GUI, but comes with a thin, pywebio, multiplayer web interface. +The web interface supports choosing from the packaged rules, singleplayer (vs Yourself), and multiplayer +(vs a friend or random opponent). It also supports saving and loading games - which can be shared with others +and be used as puzzles.
+The packaged rules are:
+Contributions of new variants or anything else you'd like to see in the project are welcome!
+Note
+While ChessMaker isn't a chess server, one could be built on top of it, and development of alternative clients to it is welcomed and encouraged. +If this project gets enough interest, a more complete server might be added.
+This is a list of variants that are packaged in the main repository +and are available in the online version of the game.
+You can enable any combination of these variants in the game by checking +the appropriate boxes in the "Options" section of the "New Game" screen.
+Variant | +Description | +
---|---|
Chess960 | +Chess960, as described here. | +
Knooks | +Rooks and knights of the same player can be moved to the same position and merge into a Knook: a piece that moves both like a rook and a knight (but cannot castle). | +
Forced En Passant | +En passant capture is mandatory if available. If a player is checked and can en passant, they lose. A player can move into a position that will put their king under attack if the next player must en passant instead of taking the king. | +
Knight Boosting | +When a pawn is promoted to a knight, the player gets an extra turn to move that knight. | +
Omnipotent F6 Pawn | +When there is an enemy piece on F6, a pawn can be summoned there. | +
Siberian Swipe | +When a Rook hasn't moved yet, it can jump over all pieces in a file to capture an enemy opponent's rook. | +
Il Vaticano | +When 2 bishops of the same player have a 2-square gap between them, and that gap is filled by 2 enemy pieces, the bishops can switch places, capturing both enemy pieces. | +
Beta Decay | +When a queen has an empty, straight, line of 3 or more squares with a board edge, hole, or a piece of the same player - it can split to a rook, bishop, and pawn - in that order. | +
La Bastarda | +When a queen is moved to an enemy king, the king can move away directly opposite from the queen, summoning a pawn of the same player as the queen between them. | +
King Cant Move to C2 | +Kings cannot move to C2. | +
Vertical Castling | +Castlling can also be done vertically. | +
Double Check To Win | +In addition to checkmating, a player can win by giving check with 2 pieces or more. | +
Capture All Pieces To Win | +Instead of checkmating, a player can win by capturing all pieces of the opponent. Kings can be under attack and can be taken. | +
Duck Chess | +There is a Duck on the board which can not capture or be captured, but can move to any position. After playing a turn, the player has to move duck to a different square. There are no checks, and you lose by getting your King captured. | +
An easily extendible chess implementation designed to support any custom rule or feature.
Play: https://chessmaker.azurewebsites.net
Documentation: https://wolfdwyc.github.io/ChessMaker
Source Code: https://github.com/WolfDWyc/ChessMaker
"},{"location":"#what-is-chessmaker","title":"What is ChessMaker?","text":"ChessMaker is a Python (3.11+) chess implementation that can be extended to support any custom rule or feature. It allows you to build almost any variant you can think of easily and quickly.
It was inspired by r/AnarchyChess - and the packaged optional rules are almost all inspired by that subreddit.
ChessMaker isn't tied to any GUI, but comes with a thin, pywebio, multiplayer web interface. The web interface supports choosing from the packaged rules, singleplayer (vs Yourself), and multiplayer (vs a friend or random opponent). It also supports saving and loading games - which can be shared with others and be used as puzzles.
The packaged rules are:
Contributions of new variants or anything else you'd like to see in the project are welcome!
"},{"location":"#what-chessmaker-isnt","title":"What ChessMaker isn't","text":"Note
While ChessMaker isn't a chess server, one could be built on top of it, and development of alternative clients to it is welcomed and encouraged. If this project gets enough interest, a more complete server might be added.
"},{"location":"getting-started/","title":"Getting started - Creating a custom game","text":""},{"location":"getting-started/#installation","title":"Installation","text":"$ pip install chessmaker\n
"},{"location":"getting-started/#usage","title":"Usage","text":""},{"location":"getting-started/#using-the-provided-game-factory","title":"Using the provided game factory","text":"This is what runs on the online example:
from chessmaker.chess import create_game\nfrom chessmaker.clients import start_pywebio_chess_server, PIECE_URLS\nif __name__ == \"__main__\":\nstart_pywebio_chess_server(\ncreate_game, # (1)\nsupported_options=[\"chess960\", \"knooks\", \"forced_en_passant\", \"knight_boosting\", \"omnipotent_f6_pawn\",\n\"siberian_swipe\", \"il_vaticano\", \"beta_decay\", \"la_bastarda\", \"king_cant_move_to_c2\",\n\"vertical_castling\", \"double_check_to_win\", \"capture_all_pieces_to_win\", \"duck_chess\"],\npiece_urls=PIECE_URLS |\n{\n\"Knook\": [\"https://i.imgur.com/UiWcdEb.png\", \"https://i.imgur.com/g7xTVts.png\"],\n\"Duck\": [\"https://i.imgur.com/ZZ2WSUq.png\", \"https://i.imgur.com/ZZ2WSUq.png\"]\n} # (2)\n,remote_access=True # (3)\n)\n
The game_factory argument is a function that creates a new game instance. The factory function should accept a list of boolean keyword arguments, which are specified in the supported_options argument. Accepting options are useful if you want to host a server that supports multiple variants of chess, but most of the time you know what variant you want to play, so it's not needed.
In order to use custom pieces, you need to provide the URLs of the images as a tuple with a URL for each player.
The remote_access argument puts the server on the internet, so you can play with your friends! It uses pywebio's remote access feature (which internally uses http://localhost.run).
Now, let's make our own game factory. This one won't support any custom rules - just the standard chess rules.
from itertools import cycle\nfrom chessmaker.chess.base import Board\nfrom chessmaker.chess.base import Game\nfrom chessmaker.chess.base import Player\nfrom chessmaker.chess.base import Square\nfrom chessmaker.chess.pieces import Bishop\nfrom chessmaker.chess.pieces import King\nfrom chessmaker.chess.pieces import Knight\nfrom chessmaker.chess.pieces import Pawn\nfrom chessmaker.chess.pieces import Queen\nfrom chessmaker.chess.pieces import Rook\nfrom chessmaker.chess.results import no_kings, checkmate, stalemate, Repetition, NoCapturesOrPawnMoves\nfrom chessmaker.clients import start_pywebio_chess_server\ndef _empty_line(length: int) -> list[Square]:\nreturn [Square() for _ in range(length)]\ndef get_result(board: Board) -> str:\nfor result_function in [no_kings, checkmate, stalemate, Repetition(3), NoCapturesOrPawnMoves(50)]:\nresult = result_function(board)\nif result:\nreturn result\npiece_row = [Rook, Knight, Bishop, Queen, King, Bishop, Knight, Rook]\ndef create_game(**_) -> Game:\nwhite = Player(\"white\")\nblack = Player(\"black\")\nturn_iterator = cycle([white, black])\ndef _pawn(player: Player):\nif player == white:\nreturn Pawn(white, Pawn.Direction.UP, promotions=[Bishop, Rook, Queen, Knight])\nelif player == black:\nreturn Pawn(black, Pawn.Direction.DOWN, promotions=[Bishop, Rook, Queen, Knight])\ngame = Game(\nboard=Board(\nsquares=[\n[Square(piece_row[i](black)) for i in range(8)],\n[Square(_pawn(black)) for _ in range(8)],\n_empty_line(8),\n_empty_line(8),\n_empty_line(8),\n_empty_line(8),\n[Square(_pawn(white)) for _ in range(8)],\n[Square(piece_row[i](white)) for i in range(8)],\n],\nplayers=[white, black],\nturn_iterator=turn_iterator,\n),\nget_result=get_result,\n)\nreturn game\nif __name__ == \"__main__\":\nstart_pywebio_chess_server(create_game, debug=True)\n
We aren't going to get into the details of how things work behind the scenes yet, but let's break down what's going on here.
Our game object is created with 2 arguments. Let's start with the simpler one.
The get_result argument is the function that will be called to determine the result of the game. We create a result function that checks for all the standard end conditions: checkmate, stalemate, repetition, and the 50 move rule. For simplicity, we could have also imported StandardResult
for the same effect.
The board argument is the main object we'll be working with, and it is created with 3 arguments. Again, let's do the simpler arguments first.
The players argument is a list of the players in the game. In most cases, we'll have 2 players - but it's possible to have more. The names of the players are currently only used for display purposes.
Because ChessMaker supports altering the player's turns, we can't just use the player list to determine who's turn it is. The turn_iterator argument is a generator that will be called to get the next player in the turn order.
The square argument is a 2D list of Square
objects. Squares can also be None
, if we want to make non-rectangular boards or have a board with holes in it.
Each square accepts an optional piece argument, which is the piece that will be placed on the square. A piece always needs to accept a player argument.
The first and last ranks are pretty simple. We just create a list of the pieces we want to use, and create a square for each one. This is because none of the pieces on those ranks need any extra arguments.
For our pawns, we need to specify the direction they can move in - and what can they promote to. We can do this using the direction and promotions arguments.
The result is a complete chess game, with all the standard rules:
"},{"location":"getting-started/#creating-a-custom-game-factory","title":"Creating a custom game factory","text":"Now that we've seen how to create a basic game factory, let's look at how to create a custom one. In this example, we'll create a 5x5 board, pawns on the corners, kings in the middle of the edge ranks, bishops between the pawns and the kings - and a hole in the middle of the board.
from itertools import cycle\nfrom chessmaker.chess.base import Board\nfrom chessmaker.chess.base import Game\nfrom chessmaker.chess.base import Player\nfrom chessmaker.chess.base import Square\nfrom chessmaker.chess.pieces import Bishop\nfrom chessmaker.chess.pieces import King\nfrom chessmaker.chess.pieces import Pawn\nfrom chessmaker.chess.results import StandardResult\nfrom chessmaker.clients import start_pywebio_chess_server\ndef _empty_line(length: int) -> list[Square]:\nreturn [Square() for _ in range(length)]\ndef create_custom_game(*_):\nwhite = Player(\"white\")\nblack = Player(\"black\")\nturn_iterator = cycle([white, black])\ndef _pawn(player: Player):\nif player == white:\nreturn Pawn(white, Pawn.Direction.UP, promotions=[Bishop])\nelif player == black:\nreturn Pawn(black, Pawn.Direction.DOWN, promotions=[Bishop])\ngame = Game(\nboard=Board(\nsquares=[\n[Square(piece(black)) for piece in [_pawn, Bishop, King, Bishop, _down_pawn]],\n_empty_line(5),\n_empty_line(2) + [None] + _empty_line(2),\n_empty_line(5),\n[Square(piece(white)) for piece in [_pawn, Bishop, King, Bishop, _up_pawn]],\n],\nplayers=[white, black],\nturn_iterator=turn_iterator,\n),\nget_result=StandardResult(),\n)\nreturn game\nif __name__ == \"__main__\":\nstart_pywebio_chess_server(create_custom_game, debug=True)\n
And the result:
"},{"location":"packaged-variants/","title":"Packaged Variants","text":"This is a list of variants that are packaged in the main repository and are available in the online version of the game.
You can enable any combination of these variants in the game by checking the appropriate boxes in the \"Options\" section of the \"New Game\" screen.
Variant Description Chess960 Chess960, as described here. Knooks Rooks and knights of the same player can be moved to the same position and merge into a Knook: a piece that moves both like a rook and a knight (but cannot castle). Forced En Passant En passant capture is mandatory if available. If a player is checked and can en passant, they lose. A player can move into a position that will put their king under attack if the next player must en passant instead of taking the king. Knight Boosting When a pawn is promoted to a knight, the player gets an extra turn to move that knight. Omnipotent F6 Pawn When there is an enemy piece on F6, a pawn can be summoned there. Siberian Swipe When a Rook hasn't moved yet, it can jump over all pieces in a file to capture an enemy opponent's rook. Il Vaticano When 2 bishops of the same player have a 2-square gap between them, and that gap is filled by 2 enemy pieces, the bishops can switch places, capturing both enemy pieces. Beta Decay When a queen has an empty, straight, line of 3 or more squares with a board edge, hole, or a piece of the same player - it can split to a rook, bishop, and pawn - in that order. La Bastarda When a queen is moved to an enemy king, the king can move away directly opposite from the queen, summoning a pawn of the same player as the queen between them. King Cant Move to C2 Kings cannot move to C2. Vertical Castling Castlling can also be done vertically. Double Check To Win In addition to checkmating, a player can win by giving check with 2 pieces or more. Capture All Pieces To Win Instead of checkmating, a player can win by capturing all pieces of the opponent. Kings can be under attack and can be taken. Duck Chess There is a Duck on the board which can not capture or be captured, but can move to any position. After playing a turn, the player has to move duck to a different square. There are no checks, and you lose by getting your King captured."},{"location":"api-reference/board/","title":"Board","text":""},{"location":"api-reference/board/#chessmakerchessbaseboard","title":"chessmaker.chess.base.board","text":"[view_source]
"},{"location":"api-reference/board/#afternewpieceevent","title":"AfterNewPieceEvent","text":"@dataclass(frozen=True)\nclass AfterNewPieceEvent(Event)\n
[view_source]
"},{"location":"api-reference/board/#piece-piece","title":"piece:Piece
","text":"[view_source]
"},{"location":"api-reference/board/#afterremovesquareevent","title":"AfterRemoveSquareEvent","text":"@dataclass(frozen=True)\nclass AfterRemoveSquareEvent(Event)\n
[view_source]
"},{"location":"api-reference/board/#position-position","title":"position:Position
","text":"[view_source]
"},{"location":"api-reference/board/#square-square","title":"square:Square
","text":"[view_source]
"},{"location":"api-reference/board/#beforeremovesquareevent","title":"BeforeRemoveSquareEvent","text":"@dataclass(frozen=True)\nclass BeforeRemoveSquareEvent(AfterRemoveSquareEvent)\n
[view_source]
"},{"location":"api-reference/board/#afteraddsquareevent","title":"AfterAddSquareEvent","text":"@dataclass(frozen=True)\nclass AfterAddSquareEvent(Event)\n
[view_source]
"},{"location":"api-reference/board/#position-position_1","title":"position:Position
","text":"[view_source]
"},{"location":"api-reference/board/#square-square_1","title":"square:Square
","text":"[view_source]
"},{"location":"api-reference/board/#beforeaddsquareevent","title":"BeforeAddSquareEvent","text":"@dataclass(frozen=True)\nclass BeforeAddSquareEvent(AfterAddSquareEvent)\n
[view_source]
"},{"location":"api-reference/board/#set_square","title":"set_square","text":"def set_square(square: Square)\n
[view_source]
"},{"location":"api-reference/board/#beforeturnchangeevent","title":"BeforeTurnChangeEvent","text":"@dataclass(frozen=True)\nclass BeforeTurnChangeEvent(CancellableEvent)\n
[view_source]
"},{"location":"api-reference/board/#board-board","title":"board:\"Board\"
","text":"[view_source]
"},{"location":"api-reference/board/#next_player-player","title":"next_player:Player
","text":"[view_source]
"},{"location":"api-reference/board/#set_next_player","title":"set_next_player","text":"def set_next_player(next_player: Player)\n
[view_source]
"},{"location":"api-reference/board/#afterturnchangeevent","title":"AfterTurnChangeEvent","text":"@dataclass(frozen=True)\nclass AfterTurnChangeEvent(Event)\n
[view_source]
"},{"location":"api-reference/board/#board-board_1","title":"board:\"Board\"
","text":"[view_source]
"},{"location":"api-reference/board/#player-player","title":"player:Player
","text":"[view_source]
"},{"location":"api-reference/board/#board","title":"Board","text":"@event_publisher(*SQUARE_EVENT_TYPES, *PIECE_EVENT_TYPES, BeforeAddSquareEvent, AfterAddSquareEvent,\nBeforeRemoveSquareEvent, AfterRemoveSquareEvent, BeforeTurnChangeEvent, AfterTurnChangeEvent,\nAfterNewPieceEvent)\nclass Board(Cloneable, EventPublisher)\n
[view_source]
"},{"location":"api-reference/board/#__init__","title":"__init__","text":"def __init__(squares: list[list[Square | None]], players: list[Player], turn_iterator: Iterator[Player], rules: list[Rule] = None)\n
[view_source]
"},{"location":"api-reference/board/#__getitem__","title":"__getitem__","text":"def __getitem__(position: Position) -> Square | None\n
[view_source]
"},{"location":"api-reference/board/#__setitem__","title":"__setitem__","text":"def __setitem__(position: Position, square: Square | None)\n
[view_source]
"},{"location":"api-reference/board/#__iter__","title":"__iter__","text":"def __iter__() -> Iterable[Square]\n
[view_source]
"},{"location":"api-reference/board/#get_pieces","title":"get_pieces","text":"def get_pieces() -> Iterable[Piece]\n
[view_source]
"},{"location":"api-reference/board/#get_player_pieces","title":"get_player_pieces","text":"def get_player_pieces(player: Player) -> Iterable[Piece]\n
[view_source]
"},{"location":"api-reference/board/#clone","title":"clone","text":"def clone()\n
[view_source]
"},{"location":"api-reference/events/","title":"Events","text":""},{"location":"api-reference/events/#chessmakereventsevent","title":"chessmaker.events.event","text":"[view_source]
"},{"location":"api-reference/events/#event","title":"Event","text":"@dataclass(frozen=True)\nclass Event()\n
[view_source]
"},{"location":"api-reference/events/#cancellableevent","title":"CancellableEvent","text":"@dataclass(frozen=True)\nclass CancellableEvent(Event)\n
[view_source]
"},{"location":"api-reference/events/#cancelled-bool","title":"cancelled:bool
","text":"[view_source]
"},{"location":"api-reference/events/#set_cancelled","title":"set_cancelled","text":"def set_cancelled(cancelled: bool)\n
[view_source]
"},{"location":"api-reference/events/#chessmakereventsevent_priority","title":"chessmaker.events.event_priority","text":"[view_source]
"},{"location":"api-reference/events/#eventpriority","title":"EventPriority","text":"class EventPriority(int, Enum)\n
[view_source]
"},{"location":"api-reference/events/#very_low","title":"VERY_LOW","text":"[view_source]
"},{"location":"api-reference/events/#low","title":"LOW","text":"[view_source]
"},{"location":"api-reference/events/#normal","title":"NORMAL","text":"[view_source]
"},{"location":"api-reference/events/#high","title":"HIGH","text":"[view_source]
"},{"location":"api-reference/events/#very_high","title":"VERY_HIGH","text":"[view_source]
"},{"location":"api-reference/events/#chessmakereventsevent_publisher","title":"chessmaker.events.event_publisher","text":"[view_source]
"},{"location":"api-reference/events/#eventpublisher","title":"EventPublisher","text":"class EventPublisher()\n
[view_source]
"},{"location":"api-reference/events/#__init__","title":"__init__","text":"def __init__(event_types: tuple[Type[Event], ...] = None)\n
[view_source]
"},{"location":"api-reference/events/#subscribe","title":"subscribe","text":"def subscribe(event_type: Type[TEvent], callback: Callable[[TEvent], None], priority: int = EventPriority.NORMAL)\n
[view_source]
"},{"location":"api-reference/events/#unsubscribe","title":"unsubscribe","text":"def unsubscribe(event_type: Type[TEvent], callback: Callable[[TEvent], None])\n
[view_source]
"},{"location":"api-reference/events/#subscribe_to_all","title":"subscribe_to_all","text":"def subscribe_to_all(callback: Callable[[Event], None], priority: int = EventPriority.NORMAL)\n
[view_source]
"},{"location":"api-reference/events/#unsubscribe_from_all","title":"unsubscribe_from_all","text":"def unsubscribe_from_all(callback: Callable[[Event], None])\n
[view_source]
"},{"location":"api-reference/events/#publish","title":"publish","text":"def publish(event: Event)\n
[view_source]
"},{"location":"api-reference/events/#propagate","title":"propagate","text":"def propagate(publisher: 'EventPublisher', event_type: Type[Event])\n
[view_source]
For all events publisher publishes of type event_type, publish them to self
"},{"location":"api-reference/events/#propagate_all","title":"propagate_all","text":"def propagate_all(publisher: 'EventPublisher')\n
[view_source]
For all events publisher publishes, publish them to self
"},{"location":"api-reference/events/#event_publisher","title":"event_publisher","text":"def event_publisher(*event_types: Type[Event]) -> Callable[[Type[T]], Type[T] | Type[EventPublisher]]\n
[view_source]
"},{"location":"api-reference/game/","title":"Game","text":""},{"location":"api-reference/game/#chessmakerchessbasegame","title":"chessmaker.chess.base.game","text":"[view_source]
"},{"location":"api-reference/game/#aftergameendevent","title":"AfterGameEndEvent","text":"@dataclass(frozen=True)\nclass AfterGameEndEvent(Event)\n
[view_source]
"},{"location":"api-reference/game/#game-game","title":"game:\"Game\"
","text":"[view_source]
"},{"location":"api-reference/game/#result-str","title":"result:str
","text":"[view_source]
"},{"location":"api-reference/game/#game","title":"Game","text":"@event_publisher(AfterGameEndEvent)\nclass Game(EventPublisher)\n
[view_source]
"},{"location":"api-reference/game/#__init__","title":"__init__","text":"def __init__(board: Board, get_result: Callable[[Board], str | None])\n
[view_source]
"},{"location":"api-reference/move-option/","title":"MoveOption","text":""},{"location":"api-reference/move-option/#chessmakerchessbasemove_option","title":"chessmaker.chess.base.move_option","text":"[view_source]
"},{"location":"api-reference/move-option/#moveoption","title":"MoveOption","text":"@dataclass\nclass MoveOption()\n
[view_source]
"},{"location":"api-reference/move-option/#position-position","title":"position:Position
","text":"[view_source]
"},{"location":"api-reference/move-option/#captures-setposition","title":"captures:set[Position]
","text":"[view_source]
"},{"location":"api-reference/move-option/#extra-dictstr-any","title":"extra:dict[str, any]
","text":"[view_source]
"},{"location":"api-reference/piece-utils/","title":"Piece Utilities","text":""},{"location":"api-reference/piece-utils/#chessmakerchesspiece_utils","title":"chessmaker.chess.piece_utils","text":"[view_source]
"},{"location":"api-reference/piece-utils/#is_in_board","title":"is_in_board","text":"def is_in_board(board: Board, position: Position) -> bool\n
[view_source]
"},{"location":"api-reference/piece-utils/#iterate_until_blocked","title":"iterate_until_blocked","text":"def iterate_until_blocked(piece: Piece, direction: tuple[int, int]) -> Iterable[Position]\n
[view_source]
"},{"location":"api-reference/piece-utils/#get_diagonals_until_blocked","title":"get_diagonals_until_blocked","text":"def get_diagonals_until_blocked(piece: Piece) -> Iterable[Position]\n
[view_source]
"},{"location":"api-reference/piece-utils/#get_horizontal_until_blocked","title":"get_horizontal_until_blocked","text":"def get_horizontal_until_blocked(piece: Piece) -> Iterable[Position]\n
[view_source]
"},{"location":"api-reference/piece-utils/#get_vertical_until_blocked","title":"get_vertical_until_blocked","text":"def get_vertical_until_blocked(piece: Piece) -> Iterable[Position]\n
[view_source]
"},{"location":"api-reference/piece-utils/#get_straight_until_blocked","title":"get_straight_until_blocked","text":"def get_straight_until_blocked(piece: Piece) -> Iterable[Position]\n
[view_source]
"},{"location":"api-reference/piece-utils/#filter_uncapturable_positions","title":"filter_uncapturable_positions","text":"def filter_uncapturable_positions(piece: Piece, positions: Iterable[Position]) -> Iterable[Position]\n
[view_source]
"},{"location":"api-reference/piece-utils/#positions_to_move_options","title":"positions_to_move_options","text":"def positions_to_move_options(board: Board, positions: Iterable[Position]) -> Iterable[MoveOption]\n
[view_source]
"},{"location":"api-reference/piece/","title":"Piece","text":""},{"location":"api-reference/piece/#chessmakerchessbasepiece","title":"chessmaker.chess.base.piece","text":"[view_source]
"},{"location":"api-reference/piece/#aftergetmoveoptionsevent","title":"AfterGetMoveOptionsEvent","text":"@dataclass(frozen=True)\nclass AfterGetMoveOptionsEvent(Event)\n
[view_source]
"},{"location":"api-reference/piece/#piece-piece","title":"piece:\"Piece\"
","text":"[view_source]
"},{"location":"api-reference/piece/#move_options-iterablemoveoption","title":"move_options:Iterable[MoveOption]
","text":"[view_source]
"},{"location":"api-reference/piece/#beforegetmoveoptionsevent","title":"BeforeGetMoveOptionsEvent","text":"class BeforeGetMoveOptionsEvent(AfterGetMoveOptionsEvent)\n
[view_source]
"},{"location":"api-reference/piece/#set_move_options","title":"set_move_options","text":"def set_move_options(move_options: Iterable[MoveOption])\n
[view_source]
"},{"location":"api-reference/piece/#aftermoveevent","title":"AfterMoveEvent","text":"@dataclass(frozen=True)\nclass AfterMoveEvent(Event)\n
[view_source]
"},{"location":"api-reference/piece/#piece-piece_1","title":"piece:\"Piece\"
","text":"[view_source]
"},{"location":"api-reference/piece/#move_option-moveoption","title":"move_option:MoveOption
","text":"[view_source]
"},{"location":"api-reference/piece/#beforemoveevent","title":"BeforeMoveEvent","text":"class BeforeMoveEvent(AfterMoveEvent, CancellableEvent)\n
[view_source]
"},{"location":"api-reference/piece/#set_move_option","title":"set_move_option","text":"def set_move_option(move_option: MoveOption)\n
[view_source]
"},{"location":"api-reference/piece/#aftercapturedevent","title":"AfterCapturedEvent","text":"@dataclass(frozen=True)\nclass AfterCapturedEvent(Event)\n
[view_source]
"},{"location":"api-reference/piece/#captured_piece-piece","title":"captured_piece:\"Piece\"
","text":"[view_source]
"},{"location":"api-reference/piece/#beforecapturedevent","title":"BeforeCapturedEvent","text":"class BeforeCapturedEvent(AfterCapturedEvent)\n
[view_source]
"},{"location":"api-reference/piece/#piece_event_types","title":"PIECE_EVENT_TYPES","text":"[view_source]
"},{"location":"api-reference/piece/#piece","title":"Piece","text":"@event_publisher(*PIECE_EVENT_TYPES)\nclass Piece(Cloneable, EventPublisher)\n
[view_source]
"},{"location":"api-reference/piece/#__init__","title":"__init__","text":"def __init__(player: Player)\n
[view_source]
"},{"location":"api-reference/piece/#__repr__","title":"__repr__","text":"def __repr__()\n
[view_source]
"},{"location":"api-reference/piece/#get_move_options","title":"get_move_options","text":"def get_move_options() -> Iterable[MoveOption]\n
[view_source]
"},{"location":"api-reference/piece/#move","title":"move","text":"def move(move_option: MoveOption)\n
[view_source]
"},{"location":"api-reference/piece/#on_join_board","title":"on_join_board","text":"def on_join_board()\n
[view_source]
"},{"location":"api-reference/piece/#position","title":"position","text":"@property\ndef position()\n
[view_source]
"},{"location":"api-reference/piece/#board","title":"board","text":"@property\ndef board()\n
[view_source]
"},{"location":"api-reference/piece/#name","title":"name","text":"@classmethod\n@property\n@abstractmethod\ndef name(cls)\n
[view_source]
"},{"location":"api-reference/piece/#clone","title":"clone","text":"@abstractmethod\ndef clone()\n
[view_source]
"},{"location":"api-reference/player/","title":"Player","text":""},{"location":"api-reference/player/#chessmakerchessbaseplayer","title":"chessmaker.chess.base.player","text":"[view_source]
"},{"location":"api-reference/player/#player","title":"Player","text":"@dataclass(frozen=True)\nclass Player()\n
[view_source]
"},{"location":"api-reference/player/#name-str","title":"name:str
","text":"[view_source]
"},{"location":"api-reference/player/#__repr__","title":"__repr__","text":"def __repr__()\n
[view_source]
"},{"location":"api-reference/position/","title":"Position","text":""},{"location":"api-reference/position/#chessmakerchessbaseposition","title":"chessmaker.chess.base.position","text":"[view_source]
"},{"location":"api-reference/position/#position","title":"Position","text":"class Position(NamedTuple)\n
[view_source]
"},{"location":"api-reference/position/#x-int","title":"x:int
","text":"[view_source]
"},{"location":"api-reference/position/#y-int","title":"y:int
","text":"[view_source]
"},{"location":"api-reference/position/#__str__","title":"__str__","text":"def __str__()\n
[view_source]
"},{"location":"api-reference/position/#offset","title":"offset","text":"def offset(x: int, y: int)\n
[view_source]
"},{"location":"api-reference/rule/","title":"Rule","text":""},{"location":"api-reference/rule/#chessmakerchessbaserule","title":"chessmaker.chess.base.rule","text":"[view_source]
"},{"location":"api-reference/rule/#rule","title":"Rule","text":"class Rule(Cloneable)\n
[view_source]
"},{"location":"api-reference/rule/#on_join_board","title":"on_join_board","text":"@abstractmethod\ndef on_join_board(board: \"Board\")\n
[view_source]
"},{"location":"api-reference/rule/#clone","title":"clone","text":"@abstractmethod\ndef clone()\n
[view_source]
"},{"location":"api-reference/rule/#as_rule","title":"as_rule","text":"def as_rule(rule_func: Callable[[\"Board\"], None]) -> Type[Rule]\n
[view_source]
"},{"location":"api-reference/square/","title":"Square","text":""},{"location":"api-reference/square/#chessmakerchessbasesquare","title":"chessmaker.chess.base.square","text":"[view_source]
"},{"location":"api-reference/square/#afteraddpieceevent","title":"AfterAddPieceEvent","text":"@dataclass(frozen=True)\nclass AfterAddPieceEvent(Event)\n
[view_source]
"},{"location":"api-reference/square/#square-square","title":"square:\"Square\"
","text":"[view_source]
"},{"location":"api-reference/square/#piece-piece","title":"piece:\"Piece\"
","text":"[view_source]
"},{"location":"api-reference/square/#beforeaddpieceevent","title":"BeforeAddPieceEvent","text":"class BeforeAddPieceEvent(AfterAddPieceEvent)\n
[view_source]
"},{"location":"api-reference/square/#set_piece","title":"set_piece","text":"def set_piece(piece: \"Piece\")\n
[view_source]
"},{"location":"api-reference/square/#beforeremovepieceevent","title":"BeforeRemovePieceEvent","text":"@dataclass(frozen=True)\nclass BeforeRemovePieceEvent(Event)\n
[view_source]
"},{"location":"api-reference/square/#square-square_1","title":"square:\"Square\"
","text":"[view_source]
"},{"location":"api-reference/square/#piece-piece_1","title":"piece:\"Piece\"
","text":"[view_source]
"},{"location":"api-reference/square/#afterremovepieceevent","title":"AfterRemovePieceEvent","text":"class AfterRemovePieceEvent(BeforeRemovePieceEvent)\n
[view_source]
"},{"location":"api-reference/square/#square_event_types","title":"SQUARE_EVENT_TYPES","text":"[view_source]
"},{"location":"api-reference/square/#square","title":"Square","text":"@event_publisher(*SQUARE_EVENT_TYPES)\nclass Square(Cloneable, EventPublisher)\n
[view_source]
"},{"location":"api-reference/square/#__init__","title":"__init__","text":"def __init__(piece: Optional[\"Piece\"] = None)\n
[view_source]
"},{"location":"api-reference/square/#piece","title":"piece","text":"@property\ndef piece() -> \"Piece\"\n
[view_source]
"},{"location":"api-reference/square/#position","title":"position","text":"@property\ndef position()\n
[view_source]
"},{"location":"api-reference/square/#board","title":"board","text":"@property\ndef board()\n
[view_source]
"},{"location":"api-reference/square/#piece_1","title":"piece","text":"@piece.setter\ndef piece(piece: \"Piece\")\n
[view_source]
"},{"location":"api-reference/square/#clone","title":"clone","text":"def clone()\n
[view_source]
"},{"location":"guide/first-steps/","title":"First steps","text":"Chess maker isn't designed around any specific chess rule or piece. This means any variant you make is a first-class citizen and should work just as fine as the standard game.
This section covers the base concepts you'll have to learn in order to create these variants.
However - you will probably not need to know all concepts in order to create a simple rule. It's recommended to skim through the concepts section, and then head to the tutorial that interests you.
"},{"location":"guide/concepts/base-game/","title":"Base Game","text":""},{"location":"guide/concepts/base-game/#introduction","title":"Introduction","text":"Now that we've covered the more general concepts, we can start looking at the game itself.
This section contains an overview of each concept, and tries to highlight useful methods, but it's not a complete reference - for that, you should look at the API Reference.
"},{"location":"guide/concepts/base-game/#game","title":"Game","text":"The game class is a container for the board and the result function. It doesn't do much except for having an AfterGameEndEvent
event that is published when the game ends.
game = Game(board, get_result)\ngame.subscribe(AfterGameEndEvent, lambda event: print(event.result))\n
"},{"location":"guide/concepts/base-game/#result","title":"Result","text":"The result function is a function that is called after every turn - it takes a board and returns either None or a string. There is no structure to the string - and it's used to tell the client what the result of the game is, but if the string returned is not None, the game will end.
For a result function to have state (e.g. 50 move rule) it should be wrapped in a class that has a __call__
method.
class GetDumbResult:\ndef __init__(self):\nself.move_count = 0\ndef __call__(self, board: Board) -> Optional[str]:\nself.move_count += 1\nif self.move_count > 100:\nreturn \"Draw - I'm bored\"\nreturn None\n
"},{"location":"guide/concepts/base-game/#board","title":"Board","text":"The board is the main container for all squares. It also contains the players and the turn iterator.
It's important to understand is that even though the board contains a turn iterator, it (or the Game itself) doesn't actually manage a game loop - it leaves that to any client.
A board contains a 2D list of squares - these squares can be None (e.g. holes) to create non-rectangular boards. When a square in the board changes (Not to confuse with when a piece changes) the board can publish BeforeRemoveSquareEvent
, AfterRemoveSquareEvent
, BeforeAddSquareEvent
and AfterAddSquareEvent
.
The board also contains a list of players, and a turn iterator. The turn iterator is a generator that will be called to get the next player in the turn order. When this happens, the board publishes a BeforeChangeTurnEvent
and AfterChangeTurnEvent
.
The board propagates all events from the squares and pieces it contains, which is very useful for subscribing to all of them at once. and also publishes AfterNewPieceEvent
when a new piece is added to the board.
It also contains a lot of utility methods for getting squares, pieces and players.
board = Board(squares, players, turn_iterator)\n# Get a square\nsquare = board[Position(0, 0)]\npiece = square.piece\nfor square in board:\nprint(square.position)\nfor y in board.size[1]:\nfor x in board.size[0]:\nprint(Position(x, y))\nfor player_piece in board.get_player_pieces(piece.player):\nprint(player_piece)\n
"},{"location":"guide/concepts/base-game/#player","title":"Player","text":"The player class is a simple container that is used to identify the owner of a piece. The name chosen is arbitrary - and doesn't have to be unique.
player0 = Player()\nplayer1 = Player(\"white\")\nplayer2 = Player(\"my_player2\")\n
"},{"location":"guide/concepts/base-game/#position","title":"Position","text":"A position is a named tuple that contains the x and y coordinates of a square or piece. Position(0, 0)
is at the top left of the board.
position = Position(0, 0)\nprint(position)\nprint(position.x, position.y)\nprint(position[0], position[1])\n\nprint(position.offset(1, 1))\n
Info
While both pieces and squares have a position
attribute, it doesn't need to be changed manually. instead the board knows where each piece and square is, and the position
attribute simply asks the board for its position.
A square is a container for a piece. When setting a square's piece, it can publish BeforeRemovePieceEvent
, AfterRemovePieceEvent
, BeforeAddPieceEvent
and AfterAddPieceEvent
.
The square has an (auto-updating) position
attribute, and a piece
attribute.
board = ...\nsquare = board[Position(0, 0)]\nprint(square.position, square.piece)\nsquare.subscribe(AfterAddPieceEvent, lambda event: print(event.piece))\nsquare.piece = Pawn(player0)\n
"},{"location":"guide/concepts/base-game/#piece","title":"Piece","text":"The piece is the main class in the base game that is meant to be extended. The piece is an abstract class, and must be extended to be used.
A piece has an (immutable) player
attribute, and an (auto-updating) position
attribute. It also has a name
class attribute, which is used for display purposes.
The piece also has a board
attribute, which is set when the piece is added to a board. Because the piece is created before it's added to the board, trying to access it when it's created will result in an error saying Piece is not on the board yet
. To perform startup logic, the piece can implement an on_join_board
method, which will be called when the piece is added to the board.
Each piece has to implement a _get_move_options
method, which returns an iterable of what moves the piece can make. Then, when the piece is asked for its move options, it will call the _get_move_options
method and publish BeforeGetMoveOptionsEvent
and AfterGetMoveOptionsEvent
events.
Then, a move option is selected by the user, and the piece is asked to make the move using .move()
- which will publish BeforeMoveEvent
, AfterMoveEvent
, BeforeCapturedEvent
and AfterCapturedEvent
events.
For a piece to implement moves that are more complex than just moving and capturing, it should subscribe to its own BeforeMoveEvent
and AfterMoveEvent
events, and implement the logic there.
A MoveOption is used to describe a move that a piece can make. A move option has to specify the position
it will move to with the position
attribute, and all positions it will capture with the captures
attribute.
In addition, for special moves (e.g. castling, en passant) a move option can have an extra
attribute, which is a dict. Ideally, this dict shouldn't contain complex objects like pieces or other dicts, but instead positions or other simple objects.
class CoolPiece(Piece):\n\"\"\"\n A piece that can move one square diagonally (down and right).\n \"\"\"\n@classmethod\n@property\ndef name(cls):\nreturn \"CoolPiece\"\ndef _get_move_options(self) -> Iterable[MoveOption]:\nmove_position = self.position.offset(1, 1)\nif not is_in_board(self.board, move_position):\nreturn\nif (self.board[move_position].piece is not None\nand self.board[move_position].piece.player == self.player):\nreturn\nyield MoveOption(self.position, captures=[move_position])\nclass CoolerPiece(CoolPiece):\n\"\"\"\n A piece that can move one square diagonally (down and right) and turn into a Queen when capturing another cool piece.\n \"\"\"\ndef __init__(self):\nsuper().__init__()\n# When listening to yourself, it's a good practice to use a high priority,\n# to emulate being the default behavior of the piece.\nself.subscribe(AfterMoveEvent, self._on_after_move, EventPriority.VERY_HIGH)\n@classmethod\n@property\ndef name(cls):\nreturn \"CoolerPiece\"\ndef _get_move_options(self) -> Iterable[MoveOption]:\nmove_options = super()._get_move_options()\nfor move_option in move_options: \nif isinstance(self.board[move_option.position].piece, CoolPiece):\nmove_option.extra = dict(turn_into_queen=True)\nyield move_option\ndef _on_after_move(self, event: AfterMoveEvent):\nif event.move_option.extra.get(\"turn_into_queen\"):\n# To make this extendible, it's a good practice to send Before and After events for this \"promotion\".\nself.board[event.move_option.position].piece = Queen(self.player)\n
"},{"location":"guide/concepts/base-game/#rule","title":"Rule","text":"A rule is a class that can be used to add custom logic to the game. It is also an abstract class, and must be extended to be used.
Similarly to pieces, rules also have an on_join_board
method - only that this one is required to implement, and gets the board as an argument. It should contain only startup logic (e.g. subscribing to events), and the board passed shouldn't be kept in state - instead, callbacks should use the board from the event (This is again related to cloneables, and will be explained in the next section).
An as_rule
method is provided to turn a function into a rule, which is useful for stateless rules.
def _on_after_move(event: AfterMoveEvent):\nif isinstance(event.piece, King):\nevent.board.turn_iterator = chain(\n[event.board.current_player],\nevent.board.turn_iterator,\n)\ndef extra_turn_if_moved_king(board: Board):\nboard.subscribe(AfterMoveEvent, _on_after_move, EventPriority.HIGH)\nExtraTurnIfMovedKing = as_rule(extra_turn_if_moved_king)\nclass ExtraTurnIfMovedKingFirst(Rule):\ndef __init__(self):\nself.any_king_moved = False\ndef _on_after_move(event: AfterMoveEvent):\nif not self.any_king_moved and isinstance(event.piece, King):\nevent.board.turn_iterator = chain(\n[event.board.current_player],\nevent.board.turn_iterator,\n)\nself.any_king_moved = True\ndef on_join_board(self, board: Board):\nboard.subscribe(BeforeMoveEvent, self._on_before_move, EventPriority.HIGH)\nboard = Board(\n...,\nrules=[ExtraTurnIfMovedKing, ExtraTurnIfMovedKingFirst],\n)\n
"},{"location":"guide/concepts/cloneables/","title":"Cloneables","text":""},{"location":"guide/concepts/cloneables/#introduction","title":"Introduction","text":"While ChessMaker isn't dependent on any specific chess rule or piece, there some concepts that are mainly used by one piece in the standard game.
One of these is that everything in the board (e.g. everything besides the Game object) has a clone
method, which returns a copy of the object.
In the standard game, this is only used for the King implementation (Though it could be useful for other custom rules too) - so while most your rules and pieces won't have to use the clone method, they all have to implement it.
InfoBecause a game doesn't have to include a king, other pieces aren't aware of the concepts of check and checkmate. This means the king subscribes to BeforeGetMoveOptionsEvent
events of all pieces, and checks if any of those moves would make it attacked by simulating the move in a cloned board. Simulation is necessary because of custom rules. For example - a custom piece could theoretically define that if the King moved near it - it would turn into a queen. This means just looking at the move options and not actually moving the piece would cause incorrect results.
The board and squares don't really interest us, since they aren't meant to be extended. So let's focus on pieces and rules.
"},{"location":"guide/concepts/cloneables/#pieces","title":"Pieces","text":""},{"location":"guide/concepts/cloneables/#stateless-pieces","title":"Stateless pieces","text":"Even stateless pieces have to implement the clone method - while this could be implemented by the base piece class, making all pieces do it makes it harder to forget when implementing a stateful piece. The is how it would look:
class CoolPiece(Piece):\ndef clone(self):\nreturn CoolPiece(self.player)\n
"},{"location":"guide/concepts/cloneables/#stateful-pieces","title":"Stateful pieces","text":"The simplest example for this is the Rook. While the Rook is a fairly simple piece, it has to know if it has moved or not. This is because it can only castle if it hasn't moved yet.
class Rook(Piece):\ndef __init__(self, player, moved=False):\nsuper().__init__(player)\nself._moved = moved\n# Some rook logic...\ndef clone(self):\nreturn Rook(self.player, self._moved)\n
"},{"location":"guide/concepts/cloneables/#inheriting-from-other-pieces","title":"Inheriting from other pieces","text":"If you're inheriting from another piece, you should reimplement the clone method.
class CoolerPiece(CoolPiece):\ndef clone(self):\nreturn CoolerPiece(self.player)\n
"},{"location":"guide/concepts/cloneables/#rules","title":"Rules","text":"For rules, it's about the same - but a bit easier.
"},{"location":"guide/concepts/cloneables/#stateless-rules","title":"Stateless rules","text":"If your rule doesn't have any state, it should be a function that uses the as_rule
helper function anyway - so you don't have to implement the clone method.
def my_simple_rule(board: Board):\n# Some rule logic...\nMySimpleRule = as_rule(my_simple_rule)\n
"},{"location":"guide/concepts/cloneables/#stateful-rules","title":"Stateful rules","text":"If your rule has state, you should implement the clone method.
class ExtraTurnOnFirstKingMove(Rule):\ndef __init__(self, player_king_moved: dict[Player, bool] = None):\nif player_king_moved is None:\nplayer_king_moved = defaultdict(bool)\nself.player_king_moved: dict[Player, bool] = player_king_moved\ndef _on_after_move(event: AfterMoveEvent):\nif not self.player_king_moved[event.piece.player] and isinstance(event.piece, King):\nevent.board.turn_iterator = chain(\n[event.board.current_player],\nevent.board.turn_iterator,\n)\nself.player_king_moved[event.piece.player] = True\ndef on_join_board(self, board: Board):\nboard.subscribe(AfterMoveEvent, self._on_after_move, EventPriority.HIGH)\ndef clone(self):\nreturn ExtraTurnOnFirstKingMove(self.player_king_moved.copy())\n
"},{"location":"guide/concepts/cloneables/#why-not-just-deepcopy","title":"Why not just deepcopy?","text":"If you're wondering why ChessMaker doesn't just use copy.deepcopy
- it's because deepcopy would copy all attributes, including event subscriptions - which is not what we want.
ChessMaker uses a custom event system to allow altering and extending the game logic. This allows you to add custom logic to the game without having to modify the engine code.
The event system is inspired by Spigot's Event API.
"},{"location":"guide/concepts/events/#events_1","title":"Events","text":"The event system defines a base Event
dataclass that all event types inherit from. All attributes of the event are immutable by default, and the event exposes one function called _set, which allows event types to make a specific attribute mutable.
Generally, things that happen expose a before and after event, with some only exposing an after event. A common pattern is for after events to be completely immutable, and for before events to have mutable attributes.
from dataclasses import dataclass\nfrom chessmaker.events import Event\nfrom chessmaker.chess.base.move_option import MoveOption\n# An immutable event\n@dataclass(frozen=True)\nclass AfterMoveEvent(Event):\npiece: \"Piece\"\nmove_option: MoveOption\n# A mutable event\nclass BeforeMoveEvent(AfterMoveEvent):\ndef set_move_option(self, move_option: MoveOption):\nself._set(\"move_option\", move_option)\n
"},{"location":"guide/concepts/events/#cancellable-events","title":"Cancellable Events","text":"Events can also inherit from the CancellableEvent
class, which adds a cancelled
attribute and a set_cancelled
function to the event.
from chessmaker.events import CancellableEvent\nclass BeforeMoveEvent(AfterMoveEvent, CancellableEvent):\ndef set_move_option(self, move_option: MoveOption):\nself._set(\"move_option\", move_option)\n
Note
Most of the time, you're going to be subscribing to existing events, but if you are creating a new event, you should remember events are just dataclasses - and don't actually implement logic like cancelling or mutating. It is the publisher's responsibility to use the mutated event in the correct way.
"},{"location":"guide/concepts/events/#subscribing-to-events","title":"Subscribing to events","text":"To subscribe to events, you need to subscribe to a publisher with the event type and callback function. Events are subscribed to on a per-instance basis - when you subscribe to a Pawn moving, it will only subscribe to that specific pawn - not all pawns.
import random\nboard: Board = ...\ndef on_after_turn_change(event: BeforeTurnChangeEvent):\nif random.random() < 0.5:\nevent.set_cancelled(True)\nelse:\nevent.set_next_player(event.board.players[1])\nboard.subscribe(BeforeTurnChangeEvent, on_after_turn_change)\n
Tip
In you event's callback function, you should use the arguments from the event, rather than using ones from your outer scope (For example, board
in the above example). This is related to Cloneables, and will be explained later.
Events can be subscribed to with a priority, which determines the order in which they are called - a higher priority means the event is called earlier.
For most use cases, the default priority of 0
is fine, but if you need to ensure your event is called before or after another event, you can either use the EventPriority
enum to specify a priority, or use an integer for more fine-grained control.
from chessmaker.events import EventPriority\nboard.subscribe(BeforeTurnChangeEvent, on_after_turn_change)\nboard.subscribe(BeforeTurnChangeEvent, on_after_turn_change, priority=EventPriority.VERY_LOW)\nboard.subscribe(BeforeTurnChangeEvent, on_after_turn_change, 2000)\n
"},{"location":"guide/concepts/events/#subscribing-to-all-events-of-an-instance","title":"Subscribing to all events of an instance","text":"You can also subscribe to all events of an instance by using the subscribe_all
function.
def on_any_event(_: Event):\nprint(\"Something happened to the board!\")\nboard.subscribe_all(on_any_event)\n
"},{"location":"guide/concepts/events/#unsubscribing-from-events","title":"Unsubscribing from events","text":"To unsubscribe from events, you need to call the unsubscribe
function with the same arguments you used to subscribe. Similarly, you can use unsubscribe_all
to unsubscribe from all events of an instance.
board.unsubscribe(BeforeTurnChangeEvent, on_after_turn_change)\nboard.unsubscribe_all(on_any_event)\n
"},{"location":"guide/concepts/events/#publishing-events","title":"Publishing events","text":"If you're adding new code, and want to make that code extendible - it is recommended to publish events. For an instance to publish events, it needs to use the @event_publisher
decorator, and specify the event types it publishes.
If it inherits from another publisher, you need use the same decorator to specify the additional event types it publishes, If it doesn't publish any additional events, you don't have to use the decorator at all.
For typing and completion purposes, a publisher should also inherit from EventPublisher
. (If it doesn't inherit from another publisher).
from chessmaker.events import EventPublisher, event_publisher\n@event_publisher(BeforePrintEvent, AfterPrintEvent)\nclass MyPrinter(EventPublisher):\ndef print_number(self):\nnumber = str(random.randint(0, 100))\nbefore_print_event = BeforePrintEvent(self, number)\nself.publish(before_print_event)\nif not before_print_event.cancelled:\nprint(before_print_event.number)\nself.publish(AfterPrintEvent(self, number))\n
"},{"location":"guide/concepts/events/#propagating-events","title":"Propagating events","text":"Sometimes, you may want to publish events from a publisher to another one. You can do this either to all event types, or to a specific one.
@event_publisher(BeforePrintEvent, AfterPrintEvent)\nclass MyPrinterManager(EventPublisher):\ndef __init__(self, my_printer: MyPrinter):\nself.my_printer = my_printer\nself.my_printer.propagate_all(self)\nself.my_printer.propagate(BeforePrintEvent, self)\n
Now, every time the printer publishes an event, the manager will also publish it. Currently, you can not unpropagate events.
Info
The main use of this in the game is the board propagating all events of its pieces and squares to itself. This means that instead of subscribing to a specific piece move, you can subscribe to all pieces moving by subscribing to the board.
"},{"location":"guide/tutorials/capture-all-pieces-to-win/","title":"Capture All Pieces to Win","text":""},{"location":"guide/tutorials/capture-all-pieces-to-win/#introduction","title":"Introduction","text":"Out of all custom rules we'll cover, this is probably the simplest one.
We're going to create a new result function - capture_all_pieces_to_win
, it accepts a board and returns a descriptive string if the game is over - otherwise it returns None
.
from chessmaker.chess.base import Board\ndef capture_all_pieces_to_win(board: Board) -> str | None: \nlost_players = []\nfor player in board.players: \nif len(list(board.get_player_pieces(player))) == 0: # (1)\nlost_players.append(player)\nif len(lost_players) == 0: # (2)\nreturn None\nif len(lost_players) == 1: # (3)\nreturn f\"All pieces captured - {lost_players[0].name} loses\"\nreturn \"Multiple players have no pieces - Draw\" # (4)\n
None
.Then, when we create a new game, we can pass this function to the result_function
argument:
game = Game(\nboard=...,\nget_result=capture_all_pieces_to_win,\n)\n
And that's it! We can now play a game where the only winning condition is to capture all pieces.
"},{"location":"guide/tutorials/capture-all-pieces-to-win/#adding-other-result-functions","title":"Adding other result functions","text":"Even though we don't want to be able to win by checkmate in this variant, we might still want to have stalemate, repetition and other result functions.
To do this, we can change our result function to a class, and add in other results:
from chessmaker.chess.results import stalemate, Repetition, NoCapturesOrPawnMoves\nclass CaptureAllPiecesToWin:\ndef __init__(self):\nself.result_functions = [capture_all_pieces_to_win, stalemate, Repetition(), NoCapturesOrPawnMoves()]\ndef __call__(self, board: Board):\nfor result_function in self.result_functions:\nresult = result_function(board)\nif result:\nreturn result\n
Note
Results that hold state (like repitition or compuond results like ours) should always be classes (and not functions), so they can be copied.
"},{"location":"guide/tutorials/capture-all-pieces-to-win/#removing-checks","title":"Removing checks","text":"For a game mode like this, starting with a king is not required (though it's still possible to do so).
However, if we would want to start with a king that can't be checked, we would have to change some more things when initializing the board.
Thankfully, the default King implementation supports an attackable
argument (which defaults to False), so we can just set it to true:
_king = lambda player: King(player, attackable=True)\ngame = Game(\nboard=Board(\nsquares=[\n[Square(_king(black)), ...],\n...\n], \n...\n)\nget_result=capture_all_pieces_to_win,\n)\n
What if the king didn't have an attackable
argument? In this case, it was convenient that the King class had an attackable
argument (for another purpose), but how would we implement this if it didn't? Because a custom king implementation is a lot of work, we could just inherit from the default King class. It would require looking a bit at the source code - but we would quickly see the startup logic for the King's check handling is done in on_join_board
, so we would just override it:
def AttackableKing(King):\ndef on_join_board(self, board: Board):\npass\n
"},{"location":"guide/tutorials/forced-en-passant/","title":"Forced En Passant","text":""},{"location":"guide/tutorials/forced-en-passant/#introduction","title":"Introduction","text":"This rule is pretty simple to define, and it works similarly to how the king works. When a certain condition is met, we force all the player's pieces to make only specific moves.
In the king's case, the condition is that the king can't be attacked. And in our case, the condition is that the player can en passant.
"},{"location":"guide/tutorials/forced-en-passant/#structuring-the-rule","title":"Structuring the Rule","text":"After a turn changed:
For all
We need to
And if any of the pawns can:
For all
We need to
from collections import defaultdict\nfrom chessmaker.chess.base.board import Board\nfrom chessmaker.chess.base.game import AfterTurnChangeEvent\nfrom chessmaker.chess.base.piece import BeforeGetMoveOptionsEvent\nfrom chessmaker.chess.base.player import Player\nfrom chessmaker.chess.base.rule import Rule\nfrom chessmaker.events import EventPriority\nclass ForcedEnPassant(Rule):\ndef __init__(self, can_en_passant: dict[Player, bool] = None):\nif can_en_passant is None:\ncan_en_passant = defaultdict(lambda: False)\nself.can_en_passant: dict[Player, bool] = can_en_passant\ndef on_join_board(self, board: Board):\nboard.subscribe(BeforeGetMoveOptionsEvent, self.on_before_get_move_options, EventPriority.LOW)\nboard.subscribe(AfterTurnChangeEvent, self.on_turn_change)\ndef on_turn_change(self, event: AfterTurnChangeEvent):\npass\ndef on_before_get_move_options(self, event: BeforeGetMoveOptionsEvent):\npass\ndef clone(self):\nreturn ForcedEnPassant(can_en_passant=self.can_en_passant.copy())\n
"},{"location":"guide/tutorials/forced-en-passant/#implementing-the-rule","title":"Implementing the Rule","text":"Here too, use the annotations to help you understand what's going on.
"},{"location":"guide/tutorials/forced-en-passant/#checking-if-any-pawn-can-en-passant","title":"Checking if any pawn can en passant","text":"from chessmaker.chess.pieces import Pawn\ndef on_turn_change(self, event: AfterTurnChangeEvent):\nfor player in event.board.players: # (1)\nself.can_en_passant[player] = False\nfor piece in event.board.get_player_pieces(player): \nif isinstance(piece, Pawn): # (2)\nmove_options = piece.get_move_options()\nif any(move_option.extra.get(\"en_passant\") for move_option in move_options): # (3)\nself.can_en_passant[player] = True\nbreak # (4)\n
from chessmaker.chess.base.piece import BeforeGetMoveOptionsEvent\ndef on_before_get_move_options(self, event: BeforeGetMoveOptionsEvent):\nif self.can_en_passant[event.piece.player]: # (1)\nmove_options = []\nfor move_option in event.move_options: \nif move_option.extra.get(\"en_passant\"): # (2)\nmove_options.append(move_option)\nevent.set_move_options(move_options) # (3)\n
Now that we've implemented our rule, we can add it to the board:
board = Board(\n...\nrules=[ForcedEnPassant()]\n)\n
And that's it! We've implemented a rule that makes en passant forced when it's possible. Let's see it in action:
"},{"location":"guide/tutorials/knook/","title":"Knook","text":""},{"location":"guide/tutorials/knook/#introduction","title":"Introduction","text":"A Knook is a new piece that can move both like a knight and a rook. It is created by merging a knight and a rook of the same player.
While this rule could be implemented with 1 new piece and a 1 new rule, We're going to implement it with 3 new pieces and no rules - to demonstrate the flexibility of creating new variants.
The first thing we need to do is create the Knook piece. Then, we'll add a way for the knight and the rook to be merged into one.
Tip
If you're just looking for how to create a new piece, don't be scared by the length of this tutorial. Most of it is just for the merging.
"},{"location":"guide/tutorials/knook/#the-knook-itself","title":"The Knook Itself","text":""},{"location":"guide/tutorials/knook/#implementing-the-piece","title":"Implementing the Piece","text":"The Knook can move like a knight and a rook. We can ue functions in piece_utils
to help us implement this.
from functools import partial\nfrom typing import Iterable\nfrom chessmaker.chess.base.move_option import MoveOption\nfrom chessmaker.chess.base.piece import Piece\nfrom chessmaker.chess.piece_utils import filter_uncapturable_positions, is_in_board, \\\n get_straight_until_blocked, positions_to_move_options\nMOVE_OFFSETS = [(1, 2), (2, 1), (2, -1), (1, -2), (-1, -2), (-2, -1), (-2, 1), (-1, 2)] # (1)\nclass Knook(Piece):\n@classmethod\n@property\ndef name(cls):\nreturn \"Knook\" # (2)\ndef _get_move_options(self) -> Iterable[MoveOption]: # (3)\npositions = [self.position.offset(*offset) for offset in MOVE_OFFSETS] \npositions = filter(partial(is_in_board, self.board), positions)\npositions = filter_uncapturable_positions(self, positions) # (4)\npositions += filter_uncapturable_positions(self,\nget_straight_until_blocked(self)\n) # (5)\nreturn positions_to_move_options(self.board, positions) # (6)\ndef clone(self):\nreturn Knook(self.player)\n
MOVE_OFFSETS
constant._get_move_options
method is called when the piece is asked for its move options. It returns an iterable of MoveOption
objects.MoveOption
objects. The positions_to_move_options
function is a helper function adds the captures
argument if the position is occupied by a piece.The UI is independent of the game logic. And theoretically, you could use any UI you want. However, since ChessMaker is packaged with a UI, this tutorial will also show how to add the Knook to it.
The start_pywebio_chess_server
function accepts an optional PIECE_URLS argument. The argument is a dictionary where the keys are the names of the pieces, and the values are tuples of URLs, with as many amount of players you want to support.
The pywebio_ui
also exposes a PIECE_URLS
constant, which is a dictionary of the default pieces. We can use it to create a new dictionary with the Knook.
from chessmaker.clients.pywebio_ui import start_pywebio_chess_server, PIECE_URLS\nif __name__ == \"__main__\":\nstart_pywebio_chess_server(\ncreate_game,\npiece_urls=PIECE_URLS | {\"Knook\": (\"https://i.imgur.com/UiWcdEb.png\", \"https://i.imgur.com/g7xTVts.png\")}\n)\n
And that's it for the new piece! If we didn't want to have the piece created by merging, this would be very simple. However, we have some more work to do.
"},{"location":"guide/tutorials/knook/#implementing-merging","title":"Implementing merging","text":"Now that we have the Knook, we need to implement a way to create it by merging a knight and a rook.
As mentioned before, this is possible to do by creating a new rule, but for the sake of this tutorial, we'll implement it with 2 new pieces. We'll create a KnookableKnight
and a KnookableRook
.
Because both the new knight and the new rook need to have similar logic (yet not identical), we'll create a few helper functions that will be used by both pieces.
"},{"location":"guide/tutorials/knook/#knookable","title":"Knookable","text":"First, we'll define an empty interface called Knookable
that will let a mergeable piece know that it can be merge with another piece.
class Knookable:\npass\n
"},{"location":"guide/tutorials/knook/#getting-the-merge-move-options","title":"Getting the merge move options","text":"Then, we'll create a helper function that will return move options that are available for merging.
The idea is that a piece will provide where it can move to, and the merge move options will return the MoveOptions that are occupied by a piece that it can be merged with it, along with extra information about the merge in the move option, so that the merge can be done later.
from typing import Iterable\nfrom chessmaker.chess.base.move_option import MoveOption\nfrom chessmaker.chess.base.piece import Piece\nfrom chessmaker.chess.base.position import Position\nfrom chessmaker.chess.pieces.knook.knookable import Knookable\ndef get_merge_move_options(piece: Piece, positions: Iterable[Position]) -> Iterable[MoveOption]:\nfor position in positions:\nposition_piece = piece.board[position].piece\nif position_piece is not None and position_piece.player == piece.player: # (1)\nif isinstance(position_piece, Knookable) and not isinstance(position_piece, type(piece)): # (2)\nyield MoveOption(position, extra=dict(knook=True)) # (3)\n
knook
extra argument set to True
, so that we can later easily know that this move option is for merging.We'll create another helper function that will perform the merge, given an AfterMoveEvent
event - and both of our new pieces will subscribe to it with that function.
To make our rule extendible, we'll also publish events when a merge occurs - but because these are new events that are not part of the core game, it's up to us how and what to publish.
from dataclasses import dataclass\nfrom chessmaker.chess.base.piece import Piece, AfterMoveEvent\nfrom chessmaker.chess.pieces.knook.knook import Knook\nfrom chessmaker.events import Event\n@dataclass(frozen=True)\nclass AfterMergeToKnookEvent(Event):\npiece: Piece # (1)\nknook: Knook\nclass BeforeMergeToKnookEvent(AfterMergeToKnookEvent):\ndef set_knook(self, knook: Knook): # (2)\nself._set(\"knook\", knook) # (3)\ndef on_after_move(event: AfterMoveEvent): # (4)\nif event.move_option.extra.get(\"knook\"): # (5)\npiece = event.piece\nbefore_merge_to_knook_event = BeforeMergeToKnookEvent(\npiece,\nKnook(event.piece.player)\n) # (6)\nevent.piece.publish(before_merge_to_knook_event)\nknook = before_merge_to_knook_event.knook # (7)\npiece.board[event.move_option.position].piece = knook # (8)\npiece.publish(AfterMergeToKnookEvent(piece, knook))\n
BeforeMergeToKnookEvent
event that will allow subscribers to change the Knook
object that will be created.Event
s _set
method to change the knook
attribute, which can't be changed regularly (because we want the rest of the event to be immutable).AfterMoveEvent
event - meaning at the time this function is called, the initiating piece has moved to the piece it wants to merge with - which is now not on the board anymore. Now we just have to change the piece on the new position to a Knook
.knook
extra argument set to True
.BeforeMergeToKnookEvent
event in a separate variable, so that we can still access it after we publish it.knook
object from the event, so that subscribers can change it.A tricky part here is that it's very tempting to think we can just pass the Knight and Rook's move options to the get_merge_move_options
- but in fact, those move options already filtered out positions where there's a piece of the same player, so we'll have to re-create the move options partially.
We'll still want to inherit from the Knight and Rook, so that pieces which check for the type of the piece (using isinstance
) will still work.
The annotations here will only be for the KnookableKnight
class, since it's about the same for both.
from functools import partial\nfrom itertools import chain\nfrom typing import Iterable\nfrom chessmaker.chess.base.move_option import MoveOption\nfrom chessmaker.chess.base.piece import AfterMoveEvent\nfrom chessmaker.chess.base.player import Player\nfrom chessmaker.chess.pieces import knight\nfrom chessmaker.chess.pieces.knight import Knight\nfrom chessmaker.chess.pieces.knook.knookable import Knookable\nfrom chessmaker.chess.piece_utils import is_in_board, get_straight_until_blocked\nfrom chessmaker.chess.pieces.rook import Rook\nfrom chessmaker.chess.pieces.knook.merge_to_knook import get_merge_move_options, merge_after_move, \\\n MERGE_TO_KNOOK_EVENT_TYPES\nfrom chessmaker.events import EventPriority, event_publisher\n@event_publisher(*MERGE_TO_KNOOK_EVENT_TYPES) # (1)\nclass KnookableKnight(Knight, Knookable):\ndef __init__(self, player):\nsuper().__init__(player)\nself.subscribe(AfterMoveEvent, merge_after_move, EventPriority.VERY_HIGH) # (2)\ndef _get_move_options(self):\npositions = [self.position.offset(*offset) for offset in knight.MOVE_OFFSETS] # (3)\npositions = list(filter(partial(is_in_board, self.board), positions))\nmerge_move_options = get_merge_move_options(self, positions) # (4)\nreturn chain(super()._get_move_options(), merge_move_options) # (5)\ndef clone(self):\nreturn KnookableKnight(self.player)\n@event_publisher(*MERGE_TO_KNOOK_EVENT_TYPES)\nclass KnookableRook(Rook, Knookable):\ndef __init__(self, player: Player, moved: bool = False):\nsuper().__init__(player, moved)\nself.subscribe(AfterMoveEvent, merge_after_move, EventPriority.VERY_HIGH)\ndef _get_move_options(self) -> Iterable[MoveOption]:\npositions = list(get_straight_until_blocked(self))\nmerge_move_options = get_merge_move_options(self, positions)\nreturn chain(super()._get_move_options(), merge_move_options)\ndef clone(self):\nreturn KnookableRook(self.player, self.moved)\n
Knight
and Knookable
, we also want to inherit from EventPublisher
- because we want to specify we're publishing more events than the base Piece
class. This isn't necessary, but it's good practice.AfterMoveEvent
with the helper function we created earlier. It's a good practice to set the priority to VERY_HIGH
when subscribing to your own events, because you want all other subscribers to have the changes you make.MOVE_OFFSETS
constant, we would just create our own.get_merge_move_options
function.get_merge_move_options
function to the move options from the Knight
class. We could have also just created the Knight's move options, since we already did some of the work needed for it.Now that we have both our new pieces, we're almost done! We just need to create a board that uses our Knookable pieces. It's also important to remember to use it in other references to Knight and Rook in the board creation, such as in promotion - otherwise the promotion will create an unmergeable piece.
board = Board(\nsquares=[\n[KnookableRook(black), KnookableKnight(black), ...],\n[Pawn(black, Pawn.Direction.DOWN, promotions=[KnookableKnight, KnookableRook, ...])],\n...\n],\n...\n)\n
And that's it! We now have a fully functional chess game with a new piece. Let's see it in action:
"},{"location":"guide/tutorials/omnipotent-f6-pawn/","title":"Omnipotent F6 Pawn","text":""},{"location":"guide/tutorials/omnipotent-f6-pawn/#introduction","title":"Introduction","text":"For this rule, we're going to implement something quite special.
The Omnipotent F6 Pawn rule is another rule that originated from r/AnarchyChess. Without getting into it's history, the rule makes it so that if the enemy has a piece on the F6 square, then a pawn can be summoned there instead.
"},{"location":"guide/tutorials/omnipotent-f6-pawn/#designing-the-rule","title":"Designing the Rule","text":"Creating a rule like this is tricky. It challenges some assumptions that we've made like that each move belongs to a piece. Luckily, ChessMaker's flexibility allows us to implement this rule without breaking any of the assumptions.
What we're going to do is make it so each piece has an extra move option that summons a pawn to the F6 square - and doesn't affect the piece itself.
We'll do this by using the BeforeMoveEvent.set_cancelled()
method. We'll cancel the move, and instead summon a pawn to the F6 square (and publish events for it as if it was a regular move).
So, we want to add a move option to all pieces, and use BeforeMoveEvent
once any piece has moved with that move option to cancel the move and summon a pawn instead.
For all
We need to
from typing import Callable\nfrom chessmaker.chess.base.board import Board\nfrom chessmaker.chess.base.piece import BeforeGetMoveOptionsEvent, BeforeMoveEvent\nfrom chessmaker.chess.base.player import Player\nfrom chessmaker.chess.base.position import Position\nfrom chessmaker.chess.base.rule import Rule\nfrom chessmaker.chess.pieces import Pawn\nfrom chessmaker.events import EventPriority\nF6 = Position(5, 2) # (1)\nclass OmnipotentF6Pawn(Rule):\ndef __init__(self, pawn: Callable[[Player], Pawn]):\nself._pawn = pawn # (2)\ndef on_join_board(self, board: Board):\nboard.subscribe(BeforeGetMoveOptionsEvent, self.on_before_get_move_options, EventPriority.HIGH)\nboard.subscribe(BeforeMoveEvent, self.on_before_move)\ndef on_before_get_move_options(self, event: BeforeGetMoveOptionsEvent):\npass\ndef on_before_move(self, event: BeforeMoveEvent):\npass\ndef clone(self):\nreturn OmnipotentF6Pawn(pawn=self._pawn)\n
Pawn
class directly is that because it accepts more arguments that we don't have - like the pawn's direction and what it can promote to.Again, use the annotations to help you understand what's going on.
"},{"location":"guide/tutorials/omnipotent-f6-pawn/#adding-the-move-option","title":"Adding the Move Option","text":"from itertools import chain\nfrom chessmaker.chess.base.move_option import MoveOption\ndef on_before_get_move_options(self, event: BeforeGetMoveOptionsEvent):\nf6_piece = event.piece.board[F6].piece\nif f6_piece is not None and f6_piece.player != event.piece.player: # (1)\nmove_option = MoveOption(F6, extra=dict(summon_pawn=True), captures={F6}) # (2)\nevent.set_move_options(chain(event.move_options, [move_option])) # (3)\n
chain
function to add the move option to the existing move options.from chessmaker.chess.base.piece import AfterMoveEvent, BeforeCapturedEvent AfterCapturedEvent\ndef on_before_move(self, event: BeforeMoveEvent):\nif event.move_option.extra.get(\"summon_pawn\"):\ncaptured_piece = event.piece.board[F6].piece # (1)\ncaptured_piece.publish(BeforeCapturedEvent(captured_piece)) # (2)\npawn = self._pawn(event.piece.player)\nevent.piece.board[F6].piece = pawn # (3)\ncaptured_piece.publish(AfterCapturedEvent(captured_piece))\npawn.publish(AfterMoveEvent(pawn, event.move_option)) # (4)\nevent.set_cancelled(True) # (5)\n
AfterCaptureEvent
later.BeforeCapturedEvent
and not a BeforeMoveEvent
event because the BeforeMoveEvent
event has already been published (we're a subscriber to it).Again, all that's left is to add the rule to the board. Though this time it requires a bit more work:
def _pawn(player: Player):\nif player == white:\nreturn Pawn(white, Pawn.Direction.UP, promotions=[Bishop, Rook, Queen, Knight])\nelif player == black:\nreturn Pawn(black, Pawn.Direction.DOWN, promotions=[Bishop, Rook, Queen, Knight])\nboard = Board(\n...\nrules=[OmnipotentF6Pawn(pawn=_pawn)]\n)\n
And that's it! This was probably the most complicated rule we've made so far, and it shows how we can do almost anything with ChessMaker.
"},{"location":"guide/tutorials/siberian-swipe/","title":"Siberian Swipe","text":""},{"location":"guide/tutorials/siberian-swipe/#introduction","title":"Introduction","text":"Siberian Swipe is a rule that originated from r/AnarchyChess. With rules like this, there's no official place we can find out how it works, so we have to define it ourselves.
"},{"location":"guide/tutorials/siberian-swipe/#defining-the-rule","title":"Defining the Rule","text":"When a rook hasn't moved yet, it can skip any pieces on it's file in order to capture a distant enemy rook.
"},{"location":"guide/tutorials/siberian-swipe/#structuring-the-rule","title":"Structuring the Rule","text":"Now that we know what the rule does, we can start building our rule. Because our rule only applies to Rooks - we only need to subscribe to rook events. And because we only need to add to where the rook can move to, and not how the movement works (We're just capturing a piece - not doing anything special like summoning a piece or moving a piece to a different location) - we only need to subscribe to the BeforeGetMoveOptions
event.
In addition, if rooks are added in the middle of the game (For example by a promotion), the rule also needs to apply to them. So we need to do 3 things:
For all
We need to
from chessmaker.chess.base.rule import Rule\nfrom chessmaker.chess.base.board import AfterNewPieceEvent, Board\nfrom chessmaker.chess.base.piece import Piece, BeforeGetMoveOptionsEvent\nfrom chessmaker.chess.pieces.rook import Rook\nfrom chessmaker.events import EventPriority\nclass SiberianSwipe(Rule):\ndef on_join_board(self, board: Board):\nfor piece in board.get_pieces():\nself.subscribe_to_piece(piece)\nboard.subscribe(AfterNewPieceEvent, self.on_new_piece)\ndef subscribe_to_piece(self, piece: Piece):\nif isinstance(piece, Rook):\npiece.subscribe(BeforeGetMoveOptionsEvent, self.on_before_get_move_options, EventPriority.HIGH) # (1)\ndef on_new_piece(self, event: AfterNewPieceEvent):\nself.subscribe_to_piece(event.piece)\ndef on_before_get_move_options(self, event: BeforeGetMoveOptionsEvent):\npass # (2)\ndef clone(self):\nreturn SiberianSwipe() # (3)\n
Now all that's left to do is implement on_before_get_move_options
. Use the annotations to help you understand what's going on.
from itertools import chain\nfrom chessmaker.chess.base.move_option import MoveOption\nfrom chessmaker.chess.base.piece import BeforeGetMoveOptionsEvent\nfrom chessmaker.chess.piece_utils import is_in_board\nfrom chessmaker.chess.pieces.rook import Rook\ndef on_before_get_move_options(self, event: BeforeGetMoveOptionsEvent):\nmove_options = event.move_options\nrook: Rook = event.piece\nboard = event.piece.board # (1)\nplayer = event.piece.player\nposition = event.piece.position\nnew_move_options = []\nif rook.moved: # (2)\nreturn\nfor direction in [(0, 1), (0, -1)]: # (3)\nenemy_position = position.offset(*direction)\nwhile is_in_board(board, enemy_position): # (4)\nenemy_piece = board[enemy_position].piece\nif isinstance(enemy_piece, Rook) and enemy_piece.player != player: # (5)\nmove_option = MoveOption(\nenemy_position,\ncaptures={enemy_position},\nextra=dict(siberian_swipe=True) # (6)\n)\nnew_move_options.append(move_option)\nenemy_position = enemy_position.offset(*direction)\nevent.set_move_options(chain(move_options, new_move_options)) # (7)\n
on_join_board
.piece_utils.is_in_board
to check if we reached the edge of the board (or a hole).extra=dict(siberian_swipe=True)
to the move option. For our use case, this is only used for display purposes, but if we wanted to modify how the move works using the BeforeMoveEvent
and AfterMoveEvent
events, we could use this to check if the move is a siberian swipe.Now that we've implemented our rule, we can add it to the board:
board = Board(\n...\nrules=[SiberianSwipe()]\n)\n
And that's it! We've implemented a rule that adds a new move option to rooks. Let' see it in action:
"}]} \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 0000000..add4c63 --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,113 @@ + +