Base Game¶
Introduction¶
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.
Game¶
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.
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
Board¶
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)
Player¶
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.
Position¶
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.
Square¶
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)
Piece¶
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.
Move Option¶
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)
Rule¶
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],
)