Skip to content

Commit

Permalink
Rework event system, fix bugs in omnipotent F6 pawn and in double che…
Browse files Browse the repository at this point in the history
…ck, make movements in the pywebio UI show before the result gets determined, and fix the invite URL showing for the invited player.
  • Loading branch information
WolfDWyc committed Apr 16, 2023
1 parent c9884fe commit 8e9275e
Show file tree
Hide file tree
Showing 28 changed files with 353 additions and 281 deletions.
2 changes: 1 addition & 1 deletion chessmaker/chess/base/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from .board import Board, BeforeTurnChangeEvent, AfterTurnChangeEvent, AfterNewPieceEvent, BeforeAddSquareEvent, \
BeforeRemoveSquareEvent, AfterAddSquareEvent, AfterRemoveSquareEvent
from .game import Game, AfterGameEndEvent
from .piece import Piece, PieceEventTypes, AfterMoveEvent, BeforeMoveEvent, AfterGetMoveOptionsEvent, \
from .piece import Piece, PIECE_EVENT_TYPES, AfterMoveEvent, BeforeMoveEvent, AfterGetMoveOptionsEvent, \
BeforeGetMoveOptionsEvent, AfterCapturedEvent, BeforeCapturedEvent
from .player import Player
from .position import Position
Expand Down
35 changes: 20 additions & 15 deletions chessmaker/chess/base/board.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,43 @@
from dataclasses import dataclass
from typing import Iterable, Iterator

from chessmaker.chess.base.piece import Piece, PieceEventTypes, AfterMoveEvent
from chessmaker.cloneable import Cloneable
from chessmaker.chess.base.piece import Piece, PIECE_EVENT_TYPES, AfterMoveEvent
from chessmaker.chess.base.player import Player
from chessmaker.chess.base.position import Position
from chessmaker.chess.base.rule import Rule
from chessmaker.chess.base.square import Square, BeforeAddPieceEvent, AfterRemovePieceEvent, AfterAddPieceEvent, \
BeforeRemovePieceEvent
from chessmaker.cloneable import Cloneable
from chessmaker.events import EventPublisher, Event, CancellableEvent
from chessmaker.chess.base.square import Square, SQUARE_EVENT_TYPES, AfterAddPieceEvent
from chessmaker.events import event_publisher, Event, CancellableEvent, EventPublisher, EventPriority


@dataclass(frozen=True)
class AfterNewPieceEvent(Event):
piece: Piece


@dataclass(frozen=True)
class AfterRemoveSquareEvent(Event):
position: Position
square: Square


@dataclass(frozen=True)
class BeforeRemoveSquareEvent(AfterRemoveSquareEvent):
pass


@dataclass(frozen=True)
class AfterAddSquareEvent(Event):
position: Position
square: Square


@dataclass(frozen=True)
class BeforeAddSquareEvent(AfterAddSquareEvent):
def set_square(self, square: Square):
self._set("square", square)


@dataclass(frozen=True)
class BeforeTurnChangeEvent(CancellableEvent):
board: "Board"
Expand All @@ -44,27 +48,30 @@ class BeforeTurnChangeEvent(CancellableEvent):
def set_next_player(self, next_player: Player):
self._set("next_player", next_player)


@dataclass(frozen=True)
class AfterTurnChangeEvent(Event):
board: "Board"
player: Player

class Board(EventPublisher[BeforeAddPieceEvent | AfterAddPieceEvent | BeforeRemovePieceEvent | AfterRemovePieceEvent
| PieceEventTypes | AfterNewPieceEvent | BeforeAddSquareEvent | AfterAddSquareEvent
| BeforeRemoveSquareEvent | AfterRemoveSquareEvent | BeforeTurnChangeEvent
| AfterTurnChangeEvent], Cloneable):

@event_publisher(*SQUARE_EVENT_TYPES, *PIECE_EVENT_TYPES, BeforeAddSquareEvent, AfterAddSquareEvent,
BeforeRemoveSquareEvent, AfterRemoveSquareEvent, BeforeTurnChangeEvent, AfterTurnChangeEvent,
AfterNewPieceEvent)
class Board(Cloneable, EventPublisher):
def __init__(
self,
squares: list[list[Square | None]],
players: list[Player],
turn_iterator: Iterator[Player],
rules: list[Rule] = None
):
):
super().__init__()
# Use the max length for each dimension to determine the size of the board
self.size = (max(len(row) for row in squares), len(squares))
self._squares = squares
self._squares_to_positions = {square: Position(x, y) for y, row in enumerate(squares) for x, square in enumerate(row) if square is not None}
self._squares_to_positions = {square: Position(x, y) for y, row in enumerate(squares) for x, square in
enumerate(row) if square is not None}
self.players = players
self.turn_iterator = turn_iterator
self.current_player = next(self.turn_iterator)
Expand All @@ -81,12 +88,11 @@ def __init__(
self._on_after_add_piece(AfterAddPieceEvent(square, square.piece))

self.subscribe(AfterAddPieceEvent, self._on_after_add_piece)
self.subscribe(AfterMoveEvent, self._on_after_move)
self.subscribe(AfterMoveEvent, self._on_after_move, EventPriority.VERY_LOW)

for rule in self.rules:
rule.on_join_board(self)


# TODO: Implement this in a way that detects actual new pieces and not moved pieces
def _on_after_add_piece(self, event: AfterAddPieceEvent):
piece: Piece = event.piece
Expand All @@ -111,7 +117,7 @@ def __getitem__(self, position: Position) -> Square | None:
def __setitem__(self, position: Position, square: Square | None):
if square == self._squares[position.y][position.x]:
warnings.warn("Setting a square to the same square doesn't have any effect, did you mean to to change "
"the piece on the square?", RuntimeWarning)
"the piece on the square?", RuntimeWarning)
return

old_square = self._squares[position.y][position.x]
Expand Down Expand Up @@ -169,4 +175,3 @@ def clone(self):
turn_iterators[1],
[rule.clone() for rule in self.rules]
)

10 changes: 5 additions & 5 deletions chessmaker/chess/base/game.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
from dataclasses import dataclass
from dataclasses import dataclass
from typing import Callable

from chessmaker.chess.base.board import Board, AfterTurnChangeEvent
from chessmaker.events import Event, EventPublisher
from chessmaker.events import Event, event_publisher, EventPublisher


@dataclass(frozen=True)
class AfterGameEndEvent(Event):
game: "Game"
result: str

class Game(EventPublisher[AfterGameEndEvent]):

@event_publisher(AfterGameEndEvent)
class Game(EventPublisher):
def __init__(
self,
board: Board,
get_result: Callable[[Board], str | None],
):
):
super().__init__()
self.board: Board = board
self._get_result = get_result
self.result = None

self.board.subscribe(AfterTurnChangeEvent, self._on_after_turn_change)


def _on_after_turn_change(self, _: AfterTurnChangeEvent):
self.result = self._get_result(self.board)
if self.result is not None:
Expand Down
20 changes: 15 additions & 5 deletions chessmaker/chess/base/piece.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
from dataclasses import dataclass
from typing import TYPE_CHECKING, Iterable

from chessmaker.chess.base.square import AfterAddPieceEvent, AfterRemovePieceEvent
from chessmaker.chess.base.move_option import MoveOption
from chessmaker.chess.base.player import Player
from chessmaker.cloneable import Cloneable
from chessmaker.events import EventPublisher, Event, CancellableEvent
from chessmaker.events import event_publisher, Event, CancellableEvent, EventPublisher

if TYPE_CHECKING:
from chessmaker.chess.base.board import Board
Expand Down Expand Up @@ -42,27 +43,32 @@ class BeforeCapturedEvent(AfterCapturedEvent):
pass


PieceEventTypes = AfterGetMoveOptionsEvent | BeforeGetMoveOptionsEvent | AfterMoveEvent | BeforeMoveEvent | \
AfterCapturedEvent | BeforeCapturedEvent
PIECE_EVENT_TYPES = (BeforeGetMoveOptionsEvent, AfterGetMoveOptionsEvent, BeforeMoveEvent, AfterMoveEvent,
BeforeCapturedEvent, AfterCapturedEvent)


class Piece(EventPublisher[PieceEventTypes], Cloneable):
@event_publisher(*PIECE_EVENT_TYPES)
class Piece(Cloneable, EventPublisher):
def __init__(self, player: Player):
super().__init__()
self._player = player
self._board: Board = None
self._move_options = None

def __repr__(self):
return f"{self.__class__.__name__} ({self.player})"

def get_move_options(self) -> Iterable[MoveOption]:
if False:
return self._move_options
move_options = self._get_move_options()

before_get_move_options_event = BeforeGetMoveOptionsEvent(self, move_options)
self.publish(before_get_move_options_event)
move_options = before_get_move_options_event.move_options
self.publish(AfterGetMoveOptionsEvent(self, move_options))

self._move_options = move_options
return move_options

def move(self, move_option: MoveOption):
Expand All @@ -89,7 +95,11 @@ def move(self, move_option: MoveOption):
self.publish(AfterMoveEvent(self, move_option))

def on_join_board(self):
pass
self.board.subscribe(AfterAddPieceEvent, self._on_after_change_piece)
self.board.subscribe(AfterRemovePieceEvent, self._on_after_change_piece)

def _on_after_change_piece(self, _: Event):
self._move_options = None

@property
def player(self):
Expand Down
28 changes: 18 additions & 10 deletions chessmaker/chess/base/square.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,48 @@
import warnings
from dataclasses import dataclass
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Optional

from chessmaker.chess.base.piece import Piece
from chessmaker.cloneable import Cloneable
from chessmaker.events import Event, EventPublisher
from chessmaker.events import Event, event_publisher, EventPublisher

if TYPE_CHECKING:
from chessmaker.chess.base.piece import Piece
from chessmaker.chess.base.board import Board


@dataclass(frozen=True)
class AfterAddPieceEvent(Event):
square: "Square"
piece: Piece
piece: "Piece"


class BeforeAddPieceEvent(AfterAddPieceEvent):
def set_piece(self, piece: Piece):
def set_piece(self, piece: "Piece"):
self._set("piece", piece)


@dataclass(frozen=True)
class BeforeRemovePieceEvent(Event):
square: "Square"
piece: Piece
piece: "Piece"


class AfterRemovePieceEvent(BeforeRemovePieceEvent):
pass


class Square(EventPublisher[BeforeAddPieceEvent | AfterAddPieceEvent | BeforeRemovePieceEvent | AfterRemovePieceEvent], Cloneable):
def __init__(self, piece: Piece | None = None):
SQUARE_EVENT_TYPES = (BeforeAddPieceEvent, AfterAddPieceEvent, BeforeRemovePieceEvent, AfterRemovePieceEvent)


@event_publisher(*SQUARE_EVENT_TYPES)
class Square(Cloneable, EventPublisher):
def __init__(self, piece: Optional["Piece"] = None):
super().__init__()
self._piece = piece
self._board: Board = None

@property
def piece(self) -> Piece:
def piece(self) -> "Piece":
return self._piece

@property
Expand All @@ -48,7 +56,7 @@ def board(self):
return self._board

@piece.setter
def piece(self, piece: Piece):
def piece(self, piece: "Piece"):
old_piece = self._piece

if piece is old_piece:
Expand Down
14 changes: 8 additions & 6 deletions chessmaker/chess/pieces/king.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,22 @@

from chessmaker.chess.base.board import AfterNewPieceEvent
from chessmaker.chess.base.move_option import MoveOption
from chessmaker.chess.base.piece import Piece, PieceEventTypes, BeforeMoveEvent, BeforeGetMoveOptionsEvent
from chessmaker.chess.base.piece import Piece, BeforeMoveEvent, BeforeGetMoveOptionsEvent
from chessmaker.chess.base.player import Player
from chessmaker.chess.base.position import Position
from chessmaker.chess.base.square import Square
from chessmaker.chess.piece_utils import filter_uncapturable_positions, is_in_board, iterate_until_blocked, \
positions_to_move_options
from chessmaker.chess.pieces.rook import Rook
from chessmaker.events import Event, EventPublisher, EventPriority
from chessmaker.events import Event, event_publisher, EventPriority


@dataclass(frozen=True)
class AfterCastleEvent(Event):
king: "King"
rook: Rook


@dataclass(frozen=True)
class BeforeCastleEvent(AfterCastleEvent):
king_destination: Square
Expand All @@ -30,7 +31,9 @@ def set_king_destination(self, king_destination: Square):
def set_rook_destination(self, rook_destination: Square):
self._set("rook_destination", rook_destination)

class King(Piece, EventPublisher[PieceEventTypes | AfterCastleEvent | BeforeCastleEvent]):

@event_publisher(AfterCastleEvent, BeforeCastleEvent)
class King(Piece):
def __init__(
self,
player: Player,
Expand All @@ -44,14 +47,13 @@ def __init__(
self._castling_directions = castling_directions
self.subscribe(BeforeMoveEvent, self._on_before_move, EventPriority.VERY_HIGH)



@classmethod
@property
def name(cls):
return "King"

def on_join_board(self):
super().on_join_board()
if not self._attackable:
for piece in self.board.get_pieces():
if piece.player == self.player:
Expand All @@ -74,7 +76,6 @@ def is_attacked(self) -> bool:
return True
return False


def _is_attacked_after_move(self, piece: Piece, move_option: MoveOption) -> bool:
board_clone = self.board.clone()
self_clone = board_clone[self.position].piece
Expand Down Expand Up @@ -134,6 +135,7 @@ def _get_move_options(self) -> Iterable[MoveOption]:
move_options.append(MoveOption(line[1], extra=dict(castle=line[-1])))

return move_options

def _on_before_move(self, event: BeforeMoveEvent):
if "castle" in event.move_option.extra:
move_option = event.move_option
Expand Down
14 changes: 8 additions & 6 deletions chessmaker/chess/pieces/knook/knookable_knight.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
from functools import partial
from itertools import chain

from chessmaker.chess.base.piece import AfterMoveEvent, PieceEventTypes
from chessmaker.chess.base.piece import AfterMoveEvent
from chessmaker.chess.pieces import knight
from chessmaker.chess.pieces.knight import Knight
from chessmaker.chess.pieces.knook import merge_to_knook
from chessmaker.chess.pieces.knook.knookable import Knookable
from chessmaker.chess.piece_utils import is_in_board
from chessmaker.events import EventPriority, EventPublisher
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


class KnookableKnight(Knight, EventPublisher[PieceEventTypes | merge_to_knook.MergeToKnookEventTypes], Knookable):
@event_publisher(*MERGE_TO_KNOOK_EVENT_TYPES)
class KnookableKnight(Knight, Knookable):
def __init__(self, player):
super().__init__(player)
self.subscribe(AfterMoveEvent, merge_to_knook.on_after_move, EventPriority.VERY_HIGH)
self.subscribe(AfterMoveEvent, merge_after_move, EventPriority.VERY_HIGH)

def _get_move_options(self):
positions = [self.position.offset(*offset) for offset in knight.MOVE_OFFSETS]
positions = list(filter(partial(is_in_board, self.board), positions))
merge_move_options = merge_to_knook.get_merge_move_options(self, positions)
merge_move_options = get_merge_move_options(self, positions)

return chain(super()._get_move_options(), merge_move_options)

Expand Down
Loading

0 comments on commit 8e9275e

Please sign in to comment.