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 @@ + + + + + + + + + + + + + + + + + + ChessMaker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ +

404 - Not found

+ +
+
+ + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/api-reference/board/index.html b/api-reference/board/index.html new file mode 100644 index 0000000..7d44ca5 --- /dev/null +++ b/api-reference/board/index.html @@ -0,0 +1,1436 @@ + + + + + + + + + + + + + + + + + + + + + + + + Board - ChessMaker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

+

chessmaker.chess.base.board

+

[view_source]

+

+

AfterNewPieceEvent

+
@dataclass(frozen=True)
+class AfterNewPieceEvent(Event)
+
+

[view_source]

+

+

piece: Piece

+

[view_source]

+

+

AfterRemoveSquareEvent

+
@dataclass(frozen=True)
+class AfterRemoveSquareEvent(Event)
+
+

[view_source]

+

+

position: Position

+

[view_source]

+

+

square: Square

+

[view_source]

+

+

BeforeRemoveSquareEvent

+
@dataclass(frozen=True)
+class BeforeRemoveSquareEvent(AfterRemoveSquareEvent)
+
+

[view_source]

+

+

AfterAddSquareEvent

+
@dataclass(frozen=True)
+class AfterAddSquareEvent(Event)
+
+

[view_source]

+

+

position: Position

+

[view_source]

+

+

square: Square

+

[view_source]

+

+

BeforeAddSquareEvent

+
@dataclass(frozen=True)
+class BeforeAddSquareEvent(AfterAddSquareEvent)
+
+

[view_source]

+

+

set_square

+
def set_square(square: Square)
+
+

[view_source]

+

+

BeforeTurnChangeEvent

+
@dataclass(frozen=True)
+class BeforeTurnChangeEvent(CancellableEvent)
+
+

[view_source]

+

+

board: "Board"

+

[view_source]

+

+

next_player: Player

+

[view_source]

+

+

set_next_player

+
def set_next_player(next_player: Player)
+
+

[view_source]

+

+

AfterTurnChangeEvent

+
@dataclass(frozen=True)
+class AfterTurnChangeEvent(Event)
+
+

[view_source]

+

+

board: "Board"

+

[view_source]

+

+

player: Player

+

[view_source]

+

+

Board

+
@event_publisher(*SQUARE_EVENT_TYPES, *PIECE_EVENT_TYPES, BeforeAddSquareEvent, AfterAddSquareEvent,
+                 BeforeRemoveSquareEvent, AfterRemoveSquareEvent, BeforeTurnChangeEvent, AfterTurnChangeEvent,
+                 AfterNewPieceEvent)
+class Board(Cloneable,  EventPublisher)
+
+

[view_source]

+

+

__init__

+
def __init__(squares: list[list[Square | None]], players: list[Player], turn_iterator: Iterator[Player], rules: list[Rule] = None)
+
+

[view_source]

+

+

__getitem__

+
def __getitem__(position: Position) -> Square | None
+
+

[view_source]

+

+

__setitem__

+
def __setitem__(position: Position, square: Square | None)
+
+

[view_source]

+

+

__iter__

+
def __iter__() -> Iterable[Square]
+
+

[view_source]

+

+

get_pieces

+
def get_pieces() -> Iterable[Piece]
+
+

[view_source]

+

+

get_player_pieces

+
def get_player_pieces(player: Player) -> Iterable[Piece]
+
+

[view_source]

+

+

clone

+
def clone()
+
+

[view_source]

+ + + + + + +
+
+ + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/api-reference/events/index.html b/api-reference/events/index.html new file mode 100644 index 0000000..ba77963 --- /dev/null +++ b/api-reference/events/index.html @@ -0,0 +1,1032 @@ + + + + + + + + + + + + + + + + + + + + + + + + Events - ChessMaker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

+

chessmaker.events.event

+

[view_source]

+

+

Event

+
@dataclass(frozen=True)
+class Event()
+
+

[view_source]

+

+

CancellableEvent

+
@dataclass(frozen=True)
+class CancellableEvent(Event)
+
+

[view_source]

+

+

cancelled: bool

+

[view_source]

+

+

set_cancelled

+
def set_cancelled(cancelled: bool)
+
+

[view_source]

+

+

chessmaker.events.event_priority

+

[view_source]

+

+

EventPriority

+
class EventPriority(int,  Enum)
+
+

[view_source]

+

+

VERY_LOW

+

[view_source]

+

+

LOW

+

[view_source]

+

+

NORMAL

+

[view_source]

+

+

HIGH

+

[view_source]

+

+

VERY_HIGH

+

[view_source]

+

+

chessmaker.events.event_publisher

+

[view_source]

+

+

EventPublisher

+
class EventPublisher()
+
+

[view_source]

+

+

__init__

+
def __init__(event_types: tuple[Type[Event], ...] = None)
+
+

[view_source]

+

+

subscribe

+
def subscribe(event_type: Type[TEvent], callback: Callable[[TEvent], None], priority: int = EventPriority.NORMAL)
+
+

[view_source]

+

+

unsubscribe

+
def unsubscribe(event_type: Type[TEvent], callback: Callable[[TEvent], None])
+
+

[view_source]

+

+

subscribe_to_all

+
def subscribe_to_all(callback: Callable[[Event], None], priority: int = EventPriority.NORMAL)
+
+

[view_source]

+

+

unsubscribe_from_all

+
def unsubscribe_from_all(callback: Callable[[Event], None])
+
+

[view_source]

+

+

publish

+
def publish(event: Event)
+
+

[view_source]

+

+

propagate

+
def propagate(publisher: 'EventPublisher', event_type: Type[Event])
+
+

[view_source]

+

For all events publisher publishes of type event_type, publish them to self

+

+

propagate_all

+
def propagate_all(publisher: 'EventPublisher')
+
+

[view_source]

+

For all events publisher publishes, publish them to self

+

+

event_publisher

+
def event_publisher(*event_types: Type[Event]) -> Callable[[Type[T]], Type[T] | Type[EventPublisher]]
+
+

[view_source]

+ + + + + + +
+
+ + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/api-reference/game/index.html b/api-reference/game/index.html new file mode 100644 index 0000000..e3c7599 --- /dev/null +++ b/api-reference/game/index.html @@ -0,0 +1,983 @@ + + + + + + + + + + + + + + + + + + + + + + + + Game - ChessMaker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ +
+ + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/api-reference/move-option/index.html b/api-reference/move-option/index.html new file mode 100644 index 0000000..28238dc --- /dev/null +++ b/api-reference/move-option/index.html @@ -0,0 +1,949 @@ + + + + + + + + + + + + + + + + + + + + + + + + MoveOption - ChessMaker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ +
+ + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/api-reference/piece-utils/index.html b/api-reference/piece-utils/index.html new file mode 100644 index 0000000..d1ef498 --- /dev/null +++ b/api-reference/piece-utils/index.html @@ -0,0 +1,1018 @@ + + + + + + + + + + + + + + + + + + + + + + + + Piece Utilities - ChessMaker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

+

chessmaker.chess.piece_utils

+

[view_source]

+

+

is_in_board

+
def is_in_board(board: Board, position: Position) -> bool
+
+

[view_source]

+

+

iterate_until_blocked

+
def iterate_until_blocked(piece: Piece, direction: tuple[int, int]) -> Iterable[Position]
+
+

[view_source]

+

+

get_diagonals_until_blocked

+
def get_diagonals_until_blocked(piece: Piece) -> Iterable[Position]
+
+

[view_source]

+

+

get_horizontal_until_blocked

+
def get_horizontal_until_blocked(piece: Piece) -> Iterable[Position]
+
+

[view_source]

+

+

get_vertical_until_blocked

+
def get_vertical_until_blocked(piece: Piece) -> Iterable[Position]
+
+

[view_source]

+

+

get_straight_until_blocked

+
def get_straight_until_blocked(piece: Piece) -> Iterable[Position]
+
+

[view_source]

+

+

filter_uncapturable_positions

+
def filter_uncapturable_positions(piece: Piece, positions: Iterable[Position]) -> Iterable[Position]
+
+

[view_source]

+

+

positions_to_move_options

+
def positions_to_move_options(board: Board, positions: Iterable[Position]) -> Iterable[MoveOption]
+
+

[view_source]

+ + + + + + +
+
+ + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/api-reference/piece/index.html b/api-reference/piece/index.html new file mode 100644 index 0000000..0e37c42 --- /dev/null +++ b/api-reference/piece/index.html @@ -0,0 +1,1404 @@ + + + + + + + + + + + + + + + + + + + + + + + + Piece - ChessMaker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

+

chessmaker.chess.base.piece

+

[view_source]

+

+

AfterGetMoveOptionsEvent

+
@dataclass(frozen=True)
+class AfterGetMoveOptionsEvent(Event)
+
+

[view_source]

+

+

piece: "Piece"

+

[view_source]

+

+

move_options: Iterable[MoveOption]

+

[view_source]

+

+

BeforeGetMoveOptionsEvent

+
class BeforeGetMoveOptionsEvent(AfterGetMoveOptionsEvent)
+
+

[view_source]

+

+

set_move_options

+
def set_move_options(move_options: Iterable[MoveOption])
+
+

[view_source]

+

+

AfterMoveEvent

+
@dataclass(frozen=True)
+class AfterMoveEvent(Event)
+
+

[view_source]

+

+

piece: "Piece"

+

[view_source]

+

+

move_option: MoveOption

+

[view_source]

+

+

BeforeMoveEvent

+
class BeforeMoveEvent(AfterMoveEvent,  CancellableEvent)
+
+

[view_source]

+

+

set_move_option

+
def set_move_option(move_option: MoveOption)
+
+

[view_source]

+

+

AfterCapturedEvent

+
@dataclass(frozen=True)
+class AfterCapturedEvent(Event)
+
+

[view_source]

+

+

captured_piece: "Piece"

+

[view_source]

+

+

BeforeCapturedEvent

+
class BeforeCapturedEvent(AfterCapturedEvent)
+
+

[view_source]

+

+

PIECE_EVENT_TYPES

+

[view_source]

+

+

Piece

+
@event_publisher(*PIECE_EVENT_TYPES)
+class Piece(Cloneable,  EventPublisher)
+
+

[view_source]

+

+

__init__

+
def __init__(player: Player)
+
+

[view_source]

+

+

__repr__

+
def __repr__()
+
+

[view_source]

+

+

get_move_options

+
def get_move_options() -> Iterable[MoveOption]
+
+

[view_source]

+

+

move

+
def move(move_option: MoveOption)
+
+

[view_source]

+

+

on_join_board

+
def on_join_board()
+
+

[view_source]

+

+

position

+
@property
+def position()
+
+

[view_source]

+

+

board

+
@property
+def board()
+
+

[view_source]

+

+

name

+
@classmethod
+@property
+@abstractmethod
+def name(cls)
+
+

[view_source]

+

+

clone

+
@abstractmethod
+def clone()
+
+

[view_source]

+ + + + + + +
+
+ + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/api-reference/player/index.html b/api-reference/player/index.html new file mode 100644 index 0000000..13b4e3a --- /dev/null +++ b/api-reference/player/index.html @@ -0,0 +1,934 @@ + + + + + + + + + + + + + + + + + + + + + + + + Player - ChessMaker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ +
+ + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/api-reference/position/index.html b/api-reference/position/index.html new file mode 100644 index 0000000..7dc136f --- /dev/null +++ b/api-reference/position/index.html @@ -0,0 +1,969 @@ + + + + + + + + + + + + + + + + + + + + + + + + Position - ChessMaker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ +
+ + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/api-reference/rule/index.html b/api-reference/rule/index.html new file mode 100644 index 0000000..2ee92b9 --- /dev/null +++ b/api-reference/rule/index.html @@ -0,0 +1,939 @@ + + + + + + + + + + + + + + + + + + + + + + Rule - ChessMaker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ +
+ + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/api-reference/square/index.html b/api-reference/square/index.html new file mode 100644 index 0000000..8ff99a9 --- /dev/null +++ b/api-reference/square/index.html @@ -0,0 +1,1246 @@ + + + + + + + + + + + + + + + + + + + + + + + + Square - ChessMaker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

+

chessmaker.chess.base.square

+

[view_source]

+

+

AfterAddPieceEvent

+
@dataclass(frozen=True)
+class AfterAddPieceEvent(Event)
+
+

[view_source]

+

+

square: "Square"

+

[view_source]

+

+

piece: "Piece"

+

[view_source]

+

+

BeforeAddPieceEvent

+
class BeforeAddPieceEvent(AfterAddPieceEvent)
+
+

[view_source]

+

+

set_piece

+
def set_piece(piece: "Piece")
+
+

[view_source]

+

+

BeforeRemovePieceEvent

+
@dataclass(frozen=True)
+class BeforeRemovePieceEvent(Event)
+
+

[view_source]

+

+

square: "Square"

+

[view_source]

+

+

piece: "Piece"

+

[view_source]

+

+

AfterRemovePieceEvent

+
class AfterRemovePieceEvent(BeforeRemovePieceEvent)
+
+

[view_source]

+

+

SQUARE_EVENT_TYPES

+

[view_source]

+

+

Square

+
@event_publisher(*SQUARE_EVENT_TYPES)
+class Square(Cloneable,  EventPublisher)
+
+

[view_source]

+

+

__init__

+
def __init__(piece: Optional["Piece"] = None)
+
+

[view_source]

+

+

piece

+
@property
+def piece() -> "Piece"
+
+

[view_source]

+

+

position

+
@property
+def position()
+
+

[view_source]

+

+

board

+
@property
+def board()
+
+

[view_source]

+

+

piece

+
@piece.setter
+def piece(piece: "Piece")
+
+

[view_source]

+

+

clone

+
def clone()
+
+

[view_source]

+ + + + + + +
+
+ + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/assets/gifs/forced_en_passant.gif b/assets/gifs/forced_en_passant.gif new file mode 100644 index 0000000..4dcf334 Binary files /dev/null and b/assets/gifs/forced_en_passant.gif differ diff --git a/assets/gifs/knook.gif b/assets/gifs/knook.gif new file mode 100644 index 0000000..2cd9cff Binary files /dev/null and b/assets/gifs/knook.gif differ diff --git a/assets/gifs/omnipotent_f6_pawn.gif b/assets/gifs/omnipotent_f6_pawn.gif new file mode 100644 index 0000000..3b61b1e Binary files /dev/null and b/assets/gifs/omnipotent_f6_pawn.gif differ diff --git a/assets/gifs/siberian_swipe.gif b/assets/gifs/siberian_swipe.gif new file mode 100644 index 0000000..19fe870 Binary files /dev/null and b/assets/gifs/siberian_swipe.gif differ diff --git a/assets/images/_logo.png b/assets/images/_logo.png new file mode 100644 index 0000000..6e7eb50 Binary files /dev/null and b/assets/images/_logo.png differ diff --git a/assets/images/_simple-logo.png b/assets/images/_simple-logo.png new file mode 100644 index 0000000..1fb7df3 Binary files /dev/null and b/assets/images/_simple-logo.png differ diff --git a/assets/images/basic_game.png b/assets/images/basic_game.png new file mode 100644 index 0000000..a2cfdce Binary files /dev/null and b/assets/images/basic_game.png differ diff --git a/assets/images/custom_5x5_game.png b/assets/images/custom_5x5_game.png new file mode 100644 index 0000000..ae3c5ed Binary files /dev/null and b/assets/images/custom_5x5_game.png differ diff --git a/assets/images/favicon.png b/assets/images/favicon.png new file mode 100644 index 0000000..1cf13b9 Binary files /dev/null and b/assets/images/favicon.png differ diff --git a/assets/images/logo-cropped.png b/assets/images/logo-cropped.png new file mode 100644 index 0000000..8c881f6 Binary files /dev/null and b/assets/images/logo-cropped.png differ diff --git a/assets/images/logo.png b/assets/images/logo.png new file mode 100644 index 0000000..44865ca Binary files /dev/null and b/assets/images/logo.png differ diff --git a/assets/images/simple-logo-cropped-swapped.png b/assets/images/simple-logo-cropped-swapped.png new file mode 100644 index 0000000..806c160 Binary files /dev/null and b/assets/images/simple-logo-cropped-swapped.png differ diff --git a/assets/images/simple-logo-cropped.png b/assets/images/simple-logo-cropped.png new file mode 100644 index 0000000..004b423 Binary files /dev/null and b/assets/images/simple-logo-cropped.png differ diff --git a/assets/images/simple-logo.png b/assets/images/simple-logo.png new file mode 100644 index 0000000..ef57516 Binary files /dev/null and b/assets/images/simple-logo.png differ diff --git a/assets/javascripts/bundle.220ee61c.min.js b/assets/javascripts/bundle.220ee61c.min.js new file mode 100644 index 0000000..116072a --- /dev/null +++ b/assets/javascripts/bundle.220ee61c.min.js @@ -0,0 +1,29 @@ +"use strict";(()=>{var Ci=Object.create;var gr=Object.defineProperty;var Ri=Object.getOwnPropertyDescriptor;var ki=Object.getOwnPropertyNames,Ht=Object.getOwnPropertySymbols,Hi=Object.getPrototypeOf,yr=Object.prototype.hasOwnProperty,nn=Object.prototype.propertyIsEnumerable;var rn=(e,t,r)=>t in e?gr(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,P=(e,t)=>{for(var r in t||(t={}))yr.call(t,r)&&rn(e,r,t[r]);if(Ht)for(var r of Ht(t))nn.call(t,r)&&rn(e,r,t[r]);return e};var on=(e,t)=>{var r={};for(var n in e)yr.call(e,n)&&t.indexOf(n)<0&&(r[n]=e[n]);if(e!=null&&Ht)for(var n of Ht(e))t.indexOf(n)<0&&nn.call(e,n)&&(r[n]=e[n]);return r};var Pt=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Pi=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of ki(t))!yr.call(e,o)&&o!==r&&gr(e,o,{get:()=>t[o],enumerable:!(n=Ri(t,o))||n.enumerable});return e};var yt=(e,t,r)=>(r=e!=null?Ci(Hi(e)):{},Pi(t||!e||!e.__esModule?gr(r,"default",{value:e,enumerable:!0}):r,e));var sn=Pt((xr,an)=>{(function(e,t){typeof xr=="object"&&typeof an!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(xr,function(){"use strict";function e(r){var n=!0,o=!1,i=null,s={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function a(O){return!!(O&&O!==document&&O.nodeName!=="HTML"&&O.nodeName!=="BODY"&&"classList"in O&&"contains"in O.classList)}function f(O){var Qe=O.type,De=O.tagName;return!!(De==="INPUT"&&s[Qe]&&!O.readOnly||De==="TEXTAREA"&&!O.readOnly||O.isContentEditable)}function c(O){O.classList.contains("focus-visible")||(O.classList.add("focus-visible"),O.setAttribute("data-focus-visible-added",""))}function u(O){O.hasAttribute("data-focus-visible-added")&&(O.classList.remove("focus-visible"),O.removeAttribute("data-focus-visible-added"))}function p(O){O.metaKey||O.altKey||O.ctrlKey||(a(r.activeElement)&&c(r.activeElement),n=!0)}function m(O){n=!1}function d(O){a(O.target)&&(n||f(O.target))&&c(O.target)}function h(O){a(O.target)&&(O.target.classList.contains("focus-visible")||O.target.hasAttribute("data-focus-visible-added"))&&(o=!0,window.clearTimeout(i),i=window.setTimeout(function(){o=!1},100),u(O.target))}function v(O){document.visibilityState==="hidden"&&(o&&(n=!0),Y())}function Y(){document.addEventListener("mousemove",N),document.addEventListener("mousedown",N),document.addEventListener("mouseup",N),document.addEventListener("pointermove",N),document.addEventListener("pointerdown",N),document.addEventListener("pointerup",N),document.addEventListener("touchmove",N),document.addEventListener("touchstart",N),document.addEventListener("touchend",N)}function B(){document.removeEventListener("mousemove",N),document.removeEventListener("mousedown",N),document.removeEventListener("mouseup",N),document.removeEventListener("pointermove",N),document.removeEventListener("pointerdown",N),document.removeEventListener("pointerup",N),document.removeEventListener("touchmove",N),document.removeEventListener("touchstart",N),document.removeEventListener("touchend",N)}function N(O){O.target.nodeName&&O.target.nodeName.toLowerCase()==="html"||(n=!1,B())}document.addEventListener("keydown",p,!0),document.addEventListener("mousedown",m,!0),document.addEventListener("pointerdown",m,!0),document.addEventListener("touchstart",m,!0),document.addEventListener("visibilitychange",v,!0),Y(),r.addEventListener("focus",d,!0),r.addEventListener("blur",h,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)})});var cn=Pt(Er=>{(function(e){var t=function(){try{return!!Symbol.iterator}catch(c){return!1}},r=t(),n=function(c){var u={next:function(){var p=c.shift();return{done:p===void 0,value:p}}};return r&&(u[Symbol.iterator]=function(){return u}),u},o=function(c){return encodeURIComponent(c).replace(/%20/g,"+")},i=function(c){return decodeURIComponent(String(c).replace(/\+/g," "))},s=function(){var c=function(p){Object.defineProperty(this,"_entries",{writable:!0,value:{}});var m=typeof p;if(m!=="undefined")if(m==="string")p!==""&&this._fromString(p);else if(p instanceof c){var d=this;p.forEach(function(B,N){d.append(N,B)})}else if(p!==null&&m==="object")if(Object.prototype.toString.call(p)==="[object Array]")for(var h=0;hd[0]?1:0}),c._entries&&(c._entries={});for(var p=0;p1?i(d[1]):"")}})})(typeof global!="undefined"?global:typeof window!="undefined"?window:typeof self!="undefined"?self:Er);(function(e){var t=function(){try{var o=new e.URL("b","http://a");return o.pathname="c d",o.href==="http://a/c%20d"&&o.searchParams}catch(i){return!1}},r=function(){var o=e.URL,i=function(f,c){typeof f!="string"&&(f=String(f)),c&&typeof c!="string"&&(c=String(c));var u=document,p;if(c&&(e.location===void 0||c!==e.location.href)){c=c.toLowerCase(),u=document.implementation.createHTMLDocument(""),p=u.createElement("base"),p.href=c,u.head.appendChild(p);try{if(p.href.indexOf(c)!==0)throw new Error(p.href)}catch(O){throw new Error("URL unable to set base "+c+" due to "+O)}}var m=u.createElement("a");m.href=f,p&&(u.body.appendChild(m),m.href=m.href);var d=u.createElement("input");if(d.type="url",d.value=f,m.protocol===":"||!/:/.test(m.href)||!d.checkValidity()&&!c)throw new TypeError("Invalid URL");Object.defineProperty(this,"_anchorElement",{value:m});var h=new e.URLSearchParams(this.search),v=!0,Y=!0,B=this;["append","delete","set"].forEach(function(O){var Qe=h[O];h[O]=function(){Qe.apply(h,arguments),v&&(Y=!1,B.search=h.toString(),Y=!0)}}),Object.defineProperty(this,"searchParams",{value:h,enumerable:!0});var N=void 0;Object.defineProperty(this,"_updateSearchParams",{enumerable:!1,configurable:!1,writable:!1,value:function(){this.search!==N&&(N=this.search,Y&&(v=!1,this.searchParams._fromString(this.search),v=!0))}})},s=i.prototype,a=function(f){Object.defineProperty(s,f,{get:function(){return this._anchorElement[f]},set:function(c){this._anchorElement[f]=c},enumerable:!0})};["hash","host","hostname","port","protocol"].forEach(function(f){a(f)}),Object.defineProperty(s,"search",{get:function(){return this._anchorElement.search},set:function(f){this._anchorElement.search=f,this._updateSearchParams()},enumerable:!0}),Object.defineProperties(s,{toString:{get:function(){var f=this;return function(){return f.href}}},href:{get:function(){return this._anchorElement.href.replace(/\?$/,"")},set:function(f){this._anchorElement.href=f,this._updateSearchParams()},enumerable:!0},pathname:{get:function(){return this._anchorElement.pathname.replace(/(^\/?)/,"/")},set:function(f){this._anchorElement.pathname=f},enumerable:!0},origin:{get:function(){var f={"http:":80,"https:":443,"ftp:":21}[this._anchorElement.protocol],c=this._anchorElement.port!=f&&this._anchorElement.port!=="";return this._anchorElement.protocol+"//"+this._anchorElement.hostname+(c?":"+this._anchorElement.port:"")},enumerable:!0},password:{get:function(){return""},set:function(f){},enumerable:!0},username:{get:function(){return""},set:function(f){},enumerable:!0}}),i.createObjectURL=function(f){return o.createObjectURL.apply(o,arguments)},i.revokeObjectURL=function(f){return o.revokeObjectURL.apply(o,arguments)},e.URL=i};if(t()||r(),e.location!==void 0&&!("origin"in e.location)){var n=function(){return e.location.protocol+"//"+e.location.hostname+(e.location.port?":"+e.location.port:"")};try{Object.defineProperty(e.location,"origin",{get:n,enumerable:!0})}catch(o){setInterval(function(){e.location.origin=n()},100)}}})(typeof global!="undefined"?global:typeof window!="undefined"?window:typeof self!="undefined"?self:Er)});var qr=Pt((Mt,Nr)=>{/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */(function(t,r){typeof Mt=="object"&&typeof Nr=="object"?Nr.exports=r():typeof define=="function"&&define.amd?define([],r):typeof Mt=="object"?Mt.ClipboardJS=r():t.ClipboardJS=r()})(Mt,function(){return function(){var e={686:function(n,o,i){"use strict";i.d(o,{default:function(){return Ai}});var s=i(279),a=i.n(s),f=i(370),c=i.n(f),u=i(817),p=i.n(u);function m(j){try{return document.execCommand(j)}catch(T){return!1}}var d=function(T){var E=p()(T);return m("cut"),E},h=d;function v(j){var T=document.documentElement.getAttribute("dir")==="rtl",E=document.createElement("textarea");E.style.fontSize="12pt",E.style.border="0",E.style.padding="0",E.style.margin="0",E.style.position="absolute",E.style[T?"right":"left"]="-9999px";var H=window.pageYOffset||document.documentElement.scrollTop;return E.style.top="".concat(H,"px"),E.setAttribute("readonly",""),E.value=j,E}var Y=function(T,E){var H=v(T);E.container.appendChild(H);var I=p()(H);return m("copy"),H.remove(),I},B=function(T){var E=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},H="";return typeof T=="string"?H=Y(T,E):T instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(T==null?void 0:T.type)?H=Y(T.value,E):(H=p()(T),m("copy")),H},N=B;function O(j){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?O=function(E){return typeof E}:O=function(E){return E&&typeof Symbol=="function"&&E.constructor===Symbol&&E!==Symbol.prototype?"symbol":typeof E},O(j)}var Qe=function(){var T=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},E=T.action,H=E===void 0?"copy":E,I=T.container,q=T.target,Me=T.text;if(H!=="copy"&&H!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(q!==void 0)if(q&&O(q)==="object"&&q.nodeType===1){if(H==="copy"&&q.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(H==="cut"&&(q.hasAttribute("readonly")||q.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(Me)return N(Me,{container:I});if(q)return H==="cut"?h(q):N(q,{container:I})},De=Qe;function $e(j){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?$e=function(E){return typeof E}:$e=function(E){return E&&typeof Symbol=="function"&&E.constructor===Symbol&&E!==Symbol.prototype?"symbol":typeof E},$e(j)}function Ei(j,T){if(!(j instanceof T))throw new TypeError("Cannot call a class as a function")}function tn(j,T){for(var E=0;E0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof I.action=="function"?I.action:this.defaultAction,this.target=typeof I.target=="function"?I.target:this.defaultTarget,this.text=typeof I.text=="function"?I.text:this.defaultText,this.container=$e(I.container)==="object"?I.container:document.body}},{key:"listenClick",value:function(I){var q=this;this.listener=c()(I,"click",function(Me){return q.onClick(Me)})}},{key:"onClick",value:function(I){var q=I.delegateTarget||I.currentTarget,Me=this.action(q)||"copy",kt=De({action:Me,container:this.container,target:this.target(q),text:this.text(q)});this.emit(kt?"success":"error",{action:Me,text:kt,trigger:q,clearSelection:function(){q&&q.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(I){return vr("action",I)}},{key:"defaultTarget",value:function(I){var q=vr("target",I);if(q)return document.querySelector(q)}},{key:"defaultText",value:function(I){return vr("text",I)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(I){var q=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return N(I,q)}},{key:"cut",value:function(I){return h(I)}},{key:"isSupported",value:function(){var I=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],q=typeof I=="string"?[I]:I,Me=!!document.queryCommandSupported;return q.forEach(function(kt){Me=Me&&!!document.queryCommandSupported(kt)}),Me}}]),E}(a()),Ai=Li},828:function(n){var o=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function s(a,f){for(;a&&a.nodeType!==o;){if(typeof a.matches=="function"&&a.matches(f))return a;a=a.parentNode}}n.exports=s},438:function(n,o,i){var s=i(828);function a(u,p,m,d,h){var v=c.apply(this,arguments);return u.addEventListener(m,v,h),{destroy:function(){u.removeEventListener(m,v,h)}}}function f(u,p,m,d,h){return typeof u.addEventListener=="function"?a.apply(null,arguments):typeof m=="function"?a.bind(null,document).apply(null,arguments):(typeof u=="string"&&(u=document.querySelectorAll(u)),Array.prototype.map.call(u,function(v){return a(v,p,m,d,h)}))}function c(u,p,m,d){return function(h){h.delegateTarget=s(h.target,p),h.delegateTarget&&d.call(u,h)}}n.exports=f},879:function(n,o){o.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},o.nodeList=function(i){var s=Object.prototype.toString.call(i);return i!==void 0&&(s==="[object NodeList]"||s==="[object HTMLCollection]")&&"length"in i&&(i.length===0||o.node(i[0]))},o.string=function(i){return typeof i=="string"||i instanceof String},o.fn=function(i){var s=Object.prototype.toString.call(i);return s==="[object Function]"}},370:function(n,o,i){var s=i(879),a=i(438);function f(m,d,h){if(!m&&!d&&!h)throw new Error("Missing required arguments");if(!s.string(d))throw new TypeError("Second argument must be a String");if(!s.fn(h))throw new TypeError("Third argument must be a Function");if(s.node(m))return c(m,d,h);if(s.nodeList(m))return u(m,d,h);if(s.string(m))return p(m,d,h);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function c(m,d,h){return m.addEventListener(d,h),{destroy:function(){m.removeEventListener(d,h)}}}function u(m,d,h){return Array.prototype.forEach.call(m,function(v){v.addEventListener(d,h)}),{destroy:function(){Array.prototype.forEach.call(m,function(v){v.removeEventListener(d,h)})}}}function p(m,d,h){return a(document.body,m,d,h)}n.exports=f},817:function(n){function o(i){var s;if(i.nodeName==="SELECT")i.focus(),s=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var a=i.hasAttribute("readonly");a||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),a||i.removeAttribute("readonly"),s=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var f=window.getSelection(),c=document.createRange();c.selectNodeContents(i),f.removeAllRanges(),f.addRange(c),s=f.toString()}return s}n.exports=o},279:function(n){function o(){}o.prototype={on:function(i,s,a){var f=this.e||(this.e={});return(f[i]||(f[i]=[])).push({fn:s,ctx:a}),this},once:function(i,s,a){var f=this;function c(){f.off(i,c),s.apply(a,arguments)}return c._=s,this.on(i,c,a)},emit:function(i){var s=[].slice.call(arguments,1),a=((this.e||(this.e={}))[i]||[]).slice(),f=0,c=a.length;for(f;f{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var rs=/["'&<>]/;Yo.exports=ns;function ns(e){var t=""+e,r=rs.exec(t);if(!r)return t;var n,o="",i=0,s=0;for(i=r.index;i0&&i[i.length-1])&&(c[0]===6||c[0]===2)){r=0;continue}if(c[0]===3&&(!i||c[1]>i[0]&&c[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function W(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var n=r.call(e),o,i=[],s;try{for(;(t===void 0||t-- >0)&&!(o=n.next()).done;)i.push(o.value)}catch(a){s={error:a}}finally{try{o&&!o.done&&(r=n.return)&&r.call(n)}finally{if(s)throw s.error}}return i}function D(e,t,r){if(r||arguments.length===2)for(var n=0,o=t.length,i;n1||a(m,d)})})}function a(m,d){try{f(n[m](d))}catch(h){p(i[0][3],h)}}function f(m){m.value instanceof et?Promise.resolve(m.value.v).then(c,u):p(i[0][2],m)}function c(m){a("next",m)}function u(m){a("throw",m)}function p(m,d){m(d),i.shift(),i.length&&a(i[0][0],i[0][1])}}function pn(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof Ee=="function"?Ee(e):e[Symbol.iterator](),r={},n("next"),n("throw"),n("return"),r[Symbol.asyncIterator]=function(){return this},r);function n(i){r[i]=e[i]&&function(s){return new Promise(function(a,f){s=e[i](s),o(a,f,s.done,s.value)})}}function o(i,s,a,f){Promise.resolve(f).then(function(c){i({value:c,done:a})},s)}}function C(e){return typeof e=="function"}function at(e){var t=function(n){Error.call(n),n.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var It=at(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: +`+r.map(function(n,o){return o+1+") "+n.toString()}).join(` + `):"",this.name="UnsubscriptionError",this.errors=r}});function Ve(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var Ie=function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,n,o,i;if(!this.closed){this.closed=!0;var s=this._parentage;if(s)if(this._parentage=null,Array.isArray(s))try{for(var a=Ee(s),f=a.next();!f.done;f=a.next()){var c=f.value;c.remove(this)}}catch(v){t={error:v}}finally{try{f&&!f.done&&(r=a.return)&&r.call(a)}finally{if(t)throw t.error}}else s.remove(this);var u=this.initialTeardown;if(C(u))try{u()}catch(v){i=v instanceof It?v.errors:[v]}var p=this._finalizers;if(p){this._finalizers=null;try{for(var m=Ee(p),d=m.next();!d.done;d=m.next()){var h=d.value;try{ln(h)}catch(v){i=i!=null?i:[],v instanceof It?i=D(D([],W(i)),W(v.errors)):i.push(v)}}}catch(v){n={error:v}}finally{try{d&&!d.done&&(o=m.return)&&o.call(m)}finally{if(n)throw n.error}}}if(i)throw new It(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)ln(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&Ve(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&Ve(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=function(){var t=new e;return t.closed=!0,t}(),e}();var Sr=Ie.EMPTY;function jt(e){return e instanceof Ie||e&&"closed"in e&&C(e.remove)&&C(e.add)&&C(e.unsubscribe)}function ln(e){C(e)?e():e.unsubscribe()}var Le={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var st={setTimeout:function(e,t){for(var r=[],n=2;n0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var n=this,o=this,i=o.hasError,s=o.isStopped,a=o.observers;return i||s?Sr:(this.currentObservers=null,a.push(r),new Ie(function(){n.currentObservers=null,Ve(a,r)}))},t.prototype._checkFinalizedStatuses=function(r){var n=this,o=n.hasError,i=n.thrownError,s=n.isStopped;o?r.error(i):s&&r.complete()},t.prototype.asObservable=function(){var r=new F;return r.source=this,r},t.create=function(r,n){return new xn(r,n)},t}(F);var xn=function(e){ie(t,e);function t(r,n){var o=e.call(this)||this;return o.destination=r,o.source=n,o}return t.prototype.next=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.next)===null||o===void 0||o.call(n,r)},t.prototype.error=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.error)===null||o===void 0||o.call(n,r)},t.prototype.complete=function(){var r,n;(n=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||n===void 0||n.call(r)},t.prototype._subscribe=function(r){var n,o;return(o=(n=this.source)===null||n===void 0?void 0:n.subscribe(r))!==null&&o!==void 0?o:Sr},t}(x);var Et={now:function(){return(Et.delegate||Date).now()},delegate:void 0};var wt=function(e){ie(t,e);function t(r,n,o){r===void 0&&(r=1/0),n===void 0&&(n=1/0),o===void 0&&(o=Et);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=n,i._timestampProvider=o,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=n===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,n),i}return t.prototype.next=function(r){var n=this,o=n.isStopped,i=n._buffer,s=n._infiniteTimeWindow,a=n._timestampProvider,f=n._windowTime;o||(i.push(r),!s&&i.push(a.now()+f)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var n=this._innerSubscribe(r),o=this,i=o._infiniteTimeWindow,s=o._buffer,a=s.slice(),f=0;f0?e.prototype.requestAsyncId.call(this,r,n,o):(r.actions.push(this),r._scheduled||(r._scheduled=ut.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,n,o){var i;if(o===void 0&&(o=0),o!=null?o>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,n,o);var s=r.actions;n!=null&&((i=s[s.length-1])===null||i===void 0?void 0:i.id)!==n&&(ut.cancelAnimationFrame(n),r._scheduled=void 0)},t}(Wt);var Sn=function(e){ie(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var n=this._scheduled;this._scheduled=void 0;var o=this.actions,i;r=r||o.shift();do if(i=r.execute(r.state,r.delay))break;while((r=o[0])&&r.id===n&&o.shift());if(this._active=!1,i){for(;(r=o[0])&&r.id===n&&o.shift();)r.unsubscribe();throw i}},t}(Dt);var Oe=new Sn(wn);var M=new F(function(e){return e.complete()});function Vt(e){return e&&C(e.schedule)}function Cr(e){return e[e.length-1]}function Ye(e){return C(Cr(e))?e.pop():void 0}function Te(e){return Vt(Cr(e))?e.pop():void 0}function zt(e,t){return typeof Cr(e)=="number"?e.pop():t}var pt=function(e){return e&&typeof e.length=="number"&&typeof e!="function"};function Nt(e){return C(e==null?void 0:e.then)}function qt(e){return C(e[ft])}function Kt(e){return Symbol.asyncIterator&&C(e==null?void 0:e[Symbol.asyncIterator])}function Qt(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function zi(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var Yt=zi();function Gt(e){return C(e==null?void 0:e[Yt])}function Bt(e){return un(this,arguments,function(){var r,n,o,i;return $t(this,function(s){switch(s.label){case 0:r=e.getReader(),s.label=1;case 1:s.trys.push([1,,9,10]),s.label=2;case 2:return[4,et(r.read())];case 3:return n=s.sent(),o=n.value,i=n.done,i?[4,et(void 0)]:[3,5];case 4:return[2,s.sent()];case 5:return[4,et(o)];case 6:return[4,s.sent()];case 7:return s.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function Jt(e){return C(e==null?void 0:e.getReader)}function U(e){if(e instanceof F)return e;if(e!=null){if(qt(e))return Ni(e);if(pt(e))return qi(e);if(Nt(e))return Ki(e);if(Kt(e))return On(e);if(Gt(e))return Qi(e);if(Jt(e))return Yi(e)}throw Qt(e)}function Ni(e){return new F(function(t){var r=e[ft]();if(C(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function qi(e){return new F(function(t){for(var r=0;r=2;return function(n){return n.pipe(e?A(function(o,i){return e(o,i,n)}):de,ge(1),r?He(t):Dn(function(){return new Zt}))}}function Vn(){for(var e=[],t=0;t=2,!0))}function pe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new x}:t,n=e.resetOnError,o=n===void 0?!0:n,i=e.resetOnComplete,s=i===void 0?!0:i,a=e.resetOnRefCountZero,f=a===void 0?!0:a;return function(c){var u,p,m,d=0,h=!1,v=!1,Y=function(){p==null||p.unsubscribe(),p=void 0},B=function(){Y(),u=m=void 0,h=v=!1},N=function(){var O=u;B(),O==null||O.unsubscribe()};return y(function(O,Qe){d++,!v&&!h&&Y();var De=m=m!=null?m:r();Qe.add(function(){d--,d===0&&!v&&!h&&(p=$r(N,f))}),De.subscribe(Qe),!u&&d>0&&(u=new rt({next:function($e){return De.next($e)},error:function($e){v=!0,Y(),p=$r(B,o,$e),De.error($e)},complete:function(){h=!0,Y(),p=$r(B,s),De.complete()}}),U(O).subscribe(u))})(c)}}function $r(e,t){for(var r=[],n=2;ne.next(document)),e}function K(e,t=document){return Array.from(t.querySelectorAll(e))}function z(e,t=document){let r=ce(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function ce(e,t=document){return t.querySelector(e)||void 0}function _e(){return document.activeElement instanceof HTMLElement&&document.activeElement||void 0}function tr(e){return L(b(document.body,"focusin"),b(document.body,"focusout")).pipe(ke(1),l(()=>{let t=_e();return typeof t!="undefined"?e.contains(t):!1}),V(e===_e()),J())}function Xe(e){return{x:e.offsetLeft,y:e.offsetTop}}function Kn(e){return L(b(window,"load"),b(window,"resize")).pipe(Ce(0,Oe),l(()=>Xe(e)),V(Xe(e)))}function rr(e){return{x:e.scrollLeft,y:e.scrollTop}}function dt(e){return L(b(e,"scroll"),b(window,"resize")).pipe(Ce(0,Oe),l(()=>rr(e)),V(rr(e)))}var Yn=function(){if(typeof Map!="undefined")return Map;function e(t,r){var n=-1;return t.some(function(o,i){return o[0]===r?(n=i,!0):!1}),n}return function(){function t(){this.__entries__=[]}return Object.defineProperty(t.prototype,"size",{get:function(){return this.__entries__.length},enumerable:!0,configurable:!0}),t.prototype.get=function(r){var n=e(this.__entries__,r),o=this.__entries__[n];return o&&o[1]},t.prototype.set=function(r,n){var o=e(this.__entries__,r);~o?this.__entries__[o][1]=n:this.__entries__.push([r,n])},t.prototype.delete=function(r){var n=this.__entries__,o=e(n,r);~o&&n.splice(o,1)},t.prototype.has=function(r){return!!~e(this.__entries__,r)},t.prototype.clear=function(){this.__entries__.splice(0)},t.prototype.forEach=function(r,n){n===void 0&&(n=null);for(var o=0,i=this.__entries__;o0},e.prototype.connect_=function(){!Wr||this.connected_||(document.addEventListener("transitionend",this.onTransitionEnd_),window.addEventListener("resize",this.refresh),va?(this.mutationsObserver_=new MutationObserver(this.refresh),this.mutationsObserver_.observe(document,{attributes:!0,childList:!0,characterData:!0,subtree:!0})):(document.addEventListener("DOMSubtreeModified",this.refresh),this.mutationEventsAdded_=!0),this.connected_=!0)},e.prototype.disconnect_=function(){!Wr||!this.connected_||(document.removeEventListener("transitionend",this.onTransitionEnd_),window.removeEventListener("resize",this.refresh),this.mutationsObserver_&&this.mutationsObserver_.disconnect(),this.mutationEventsAdded_&&document.removeEventListener("DOMSubtreeModified",this.refresh),this.mutationsObserver_=null,this.mutationEventsAdded_=!1,this.connected_=!1)},e.prototype.onTransitionEnd_=function(t){var r=t.propertyName,n=r===void 0?"":r,o=ba.some(function(i){return!!~n.indexOf(i)});o&&this.refresh()},e.getInstance=function(){return this.instance_||(this.instance_=new e),this.instance_},e.instance_=null,e}(),Gn=function(e,t){for(var r=0,n=Object.keys(t);r0},e}(),Jn=typeof WeakMap!="undefined"?new WeakMap:new Yn,Xn=function(){function e(t){if(!(this instanceof e))throw new TypeError("Cannot call a class as a function.");if(!arguments.length)throw new TypeError("1 argument required, but only 0 present.");var r=ga.getInstance(),n=new La(t,r,this);Jn.set(this,n)}return e}();["observe","unobserve","disconnect"].forEach(function(e){Xn.prototype[e]=function(){var t;return(t=Jn.get(this))[e].apply(t,arguments)}});var Aa=function(){return typeof nr.ResizeObserver!="undefined"?nr.ResizeObserver:Xn}(),Zn=Aa;var eo=new x,Ca=$(()=>k(new Zn(e=>{for(let t of e)eo.next(t)}))).pipe(g(e=>L(ze,k(e)).pipe(R(()=>e.disconnect()))),X(1));function he(e){return{width:e.offsetWidth,height:e.offsetHeight}}function ye(e){return Ca.pipe(S(t=>t.observe(e)),g(t=>eo.pipe(A(({target:r})=>r===e),R(()=>t.unobserve(e)),l(()=>he(e)))),V(he(e)))}function bt(e){return{width:e.scrollWidth,height:e.scrollHeight}}function ar(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}var to=new x,Ra=$(()=>k(new IntersectionObserver(e=>{for(let t of e)to.next(t)},{threshold:0}))).pipe(g(e=>L(ze,k(e)).pipe(R(()=>e.disconnect()))),X(1));function sr(e){return Ra.pipe(S(t=>t.observe(e)),g(t=>to.pipe(A(({target:r})=>r===e),R(()=>t.unobserve(e)),l(({isIntersecting:r})=>r))))}function ro(e,t=16){return dt(e).pipe(l(({y:r})=>{let n=he(e),o=bt(e);return r>=o.height-n.height-t}),J())}var cr={drawer:z("[data-md-toggle=drawer]"),search:z("[data-md-toggle=search]")};function no(e){return cr[e].checked}function Ke(e,t){cr[e].checked!==t&&cr[e].click()}function Ue(e){let t=cr[e];return b(t,"change").pipe(l(()=>t.checked),V(t.checked))}function ka(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function Ha(){return L(b(window,"compositionstart").pipe(l(()=>!0)),b(window,"compositionend").pipe(l(()=>!1))).pipe(V(!1))}function oo(){let e=b(window,"keydown").pipe(A(t=>!(t.metaKey||t.ctrlKey)),l(t=>({mode:no("search")?"search":"global",type:t.key,claim(){t.preventDefault(),t.stopPropagation()}})),A(({mode:t,type:r})=>{if(t==="global"){let n=_e();if(typeof n!="undefined")return!ka(n,r)}return!0}),pe());return Ha().pipe(g(t=>t?M:e))}function le(){return new URL(location.href)}function ot(e){location.href=e.href}function io(){return new x}function ao(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)ao(e,r)}function _(e,t,...r){let n=document.createElement(e);if(t)for(let o of Object.keys(t))typeof t[o]!="undefined"&&(typeof t[o]!="boolean"?n.setAttribute(o,t[o]):n.setAttribute(o,""));for(let o of r)ao(n,o);return n}function fr(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function so(){return location.hash.substring(1)}function Dr(e){let t=_("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function Pa(e){return L(b(window,"hashchange"),e).pipe(l(so),V(so()),A(t=>t.length>0),X(1))}function co(e){return Pa(e).pipe(l(t=>ce(`[id="${t}"]`)),A(t=>typeof t!="undefined"))}function Vr(e){let t=matchMedia(e);return er(r=>t.addListener(()=>r(t.matches))).pipe(V(t.matches))}function fo(){let e=matchMedia("print");return L(b(window,"beforeprint").pipe(l(()=>!0)),b(window,"afterprint").pipe(l(()=>!1))).pipe(V(e.matches))}function zr(e,t){return e.pipe(g(r=>r?t():M))}function ur(e,t={credentials:"same-origin"}){return ue(fetch(`${e}`,t)).pipe(fe(()=>M),g(r=>r.status!==200?Ot(()=>new Error(r.statusText)):k(r)))}function We(e,t){return ur(e,t).pipe(g(r=>r.json()),X(1))}function uo(e,t){let r=new DOMParser;return ur(e,t).pipe(g(n=>n.text()),l(n=>r.parseFromString(n,"text/xml")),X(1))}function pr(e){let t=_("script",{src:e});return $(()=>(document.head.appendChild(t),L(b(t,"load"),b(t,"error").pipe(g(()=>Ot(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(l(()=>{}),R(()=>document.head.removeChild(t)),ge(1))))}function po(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function lo(){return L(b(window,"scroll",{passive:!0}),b(window,"resize",{passive:!0})).pipe(l(po),V(po()))}function mo(){return{width:innerWidth,height:innerHeight}}function ho(){return b(window,"resize",{passive:!0}).pipe(l(mo),V(mo()))}function bo(){return G([lo(),ho()]).pipe(l(([e,t])=>({offset:e,size:t})),X(1))}function lr(e,{viewport$:t,header$:r}){let n=t.pipe(ee("size")),o=G([n,r]).pipe(l(()=>Xe(e)));return G([r,t,o]).pipe(l(([{height:i},{offset:s,size:a},{x:f,y:c}])=>({offset:{x:s.x-f,y:s.y-c+i},size:a})))}(()=>{function e(n,o){parent.postMessage(n,o||"*")}function t(...n){return n.reduce((o,i)=>o.then(()=>new Promise(s=>{let a=document.createElement("script");a.src=i,a.onload=s,document.body.appendChild(a)})),Promise.resolve())}var r=class extends EventTarget{constructor(n){super(),this.url=n,this.m=i=>{i.source===this.w&&(this.dispatchEvent(new MessageEvent("message",{data:i.data})),this.onmessage&&this.onmessage(i))},this.e=(i,s,a,f,c)=>{if(s===`${this.url}`){let u=new ErrorEvent("error",{message:i,filename:s,lineno:a,colno:f,error:c});this.dispatchEvent(u),this.onerror&&this.onerror(u)}};let o=document.createElement("iframe");o.hidden=!0,document.body.appendChild(this.iframe=o),this.w.document.open(),this.w.document.write(` + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Getting started - Creating a custom game

+

Installation

+
$ pip install chessmaker
+
+

Usage

+

Using the provided game factory

+

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)
+    )
+
+
    +
  1. +

    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.

    +
  2. +
  3. +

    In order to use custom pieces, you need to provide the URLs of the images as a tuple with a URL +for each player.

    +
  4. +
  5. +

    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).

    +
  6. +
+

Creating a standard game factory

+

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:

+

Basic game

+

Creating a custom game factory

+

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:

+

Custom game

+ + + + + + +
+
+ + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/guide/concepts/base-game/index.html b/guide/concepts/base-game/index.html new file mode 100644 index 0000000..9b949fd --- /dev/null +++ b/guide/concepts/base-game/index.html @@ -0,0 +1,1225 @@ + + + + + + + + + + + + + + + + + + + + + + + + Base Game - ChessMaker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

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.

+
game = Game(board, get_result)
+game.subscribe(AfterGameEndEvent, lambda event: print(event.result))
+
+

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.

+
player0 = Player()
+player1 = Player("white")
+player2 = Player("my_player2")
+
+

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],
+)
+
+ + + + + + +
+
+ + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/guide/concepts/cloneables/index.html b/guide/concepts/cloneables/index.html new file mode 100644 index 0000000..46561a3 --- /dev/null +++ b/guide/concepts/cloneables/index.html @@ -0,0 +1,1124 @@ + + + + + + + + + + + + + + + + + + + + + + + + Cloneables - ChessMaker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Cloneables

+

Introduction

+

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.

+
+Info +

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.

+
+

What is Cloneable?

+
    +
  • The board
  • +
  • Squares
  • +
  • Pieces
  • +
  • Rules
  • +
+

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.

+

Pieces

+

Stateless pieces

+

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):
+    def clone(self):
+        return CoolPiece(self.player)
+
+

Stateful pieces

+

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)
+
+

Inheriting from other pieces

+

If you're inheriting from another piece, you should reimplement the clone method. +

class CoolerPiece(CoolPiece):
+    def clone(self):
+        return CoolerPiece(self.player)
+

+

Rules

+

For rules, it's about the same - but a bit easier.

+

Stateless rules

+

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)
+
+

Stateful rules

+

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())
+
+

Why not just deepcopy?

+

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.

+ + + + + + +
+
+ + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/guide/concepts/events/index.html b/guide/concepts/events/index.html new file mode 100644 index 0000000..9e762e6 --- /dev/null +++ b/guide/concepts/events/index.html @@ -0,0 +1,1157 @@ + + + + + + + + + + + + + + + + + + + + + + + + Events - ChessMaker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Events

+

Introduction

+

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.

+

Events

+

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)
+
+

Cancellable Events

+

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.

+
+

Subscribing to events

+

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.

+
+

Event Priorities

+

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)
+
+

Subscribing to all events of an instance

+

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)
+
+

Unsubscribing from events

+

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)
+
+

Publishing events

+

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))
+
+

Propagating events

+

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.

+
+ + + + + + +
+
+ + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/guide/first-steps/index.html b/guide/first-steps/index.html new file mode 100644 index 0000000..c88a1d0 --- /dev/null +++ b/guide/first-steps/index.html @@ -0,0 +1,840 @@ + + + + + + + + + + + + + + + + + + + + + + + + First steps - ChessMaker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

First steps

+

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.

+ + + + + + +
+
+ + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/guide/tutorials/capture-all-pieces-to-win/index.html b/guide/tutorials/capture-all-pieces-to-win/index.html new file mode 100644 index 0000000..84e3a15 --- /dev/null +++ b/guide/tutorials/capture-all-pieces-to-win/index.html @@ -0,0 +1,1011 @@ + + + + + + + + + + + + + + + + + + + + + + + + Capture All Pieces to Win - Changing the result function - ChessMaker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Capture All Pieces to Win

+

Introduction

+

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.

+

Creating the result function

+
capture_all_pieces_to_win.py
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)
+
+
    +
  1. We check if the player has any pieces left.
  2. +
  3. If no players have lost, the game is not over - we return None.
  4. +
  5. If only one player has no pieces, we return a string saying that they have lost. +We specifically check for 1 player to support a custom rule that could allow a piece to destroy +itself - causing both players to have no pieces left.
  6. +
  7. If both players have no pieces, we return a string saying that the game is a draw.
  8. +
+

Then, when we create a new game, we can pass this function to the result_function argument:

+
game = Game(
+    board=...,
+    get_result=capture_all_pieces_to_win,
+)
+
+

And that's it! We can now play a game where the only winning condition is to capture all pieces.

+

Adding other result functions

+

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.

+
+

Removing checks

+

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,
+)
+
+
+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):
+    def on_join_board(self, board: Board):
+        pass
+

+
+ + + + + + +
+
+ + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/guide/tutorials/forced-en-passant/index.html b/guide/tutorials/forced-en-passant/index.html new file mode 100644 index 0000000..30891a6 --- /dev/null +++ b/guide/tutorials/forced-en-passant/index.html @@ -0,0 +1,1074 @@ + + + + + + + + + + + + + + + + + + + + + + + + Forced En Passant - Removing move options - ChessMaker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Forced En Passant

+

Introduction

+

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.

+

Structuring the Rule

+

After a turn changed:

+
    +
  • +

    For all

    +
      +
    • Pawns of a player.
    • +
    +
  • +
  • +

    We need to

    +
      +
    • Check if the pawn can en passant.
    • +
    +
  • +
+

And if any of the pawns can:

+
    +
  • +

    For all

    +
      +
    • Pieces of the same player.
    • +
    +
  • +
  • +

    We need to

    +
      +
    • Remove all moves that aren't en passant.
    • +
    +
  • +
+
forced_en_passant.py
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())
+
+

Implementing the Rule

+

Here too, use the annotations to help you understand what's going on.

+

Checking if any pawn can en passant

+
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)
+
+
    +
  1. For each player.
  2. +
  3. For each pawn of the player.
  4. +
  5. If any of the pawn's move options is an en passant.
  6. +
  7. If any of the player's pawns can en passant, we don't need to check the other pawns.
  8. +
+

Removing all other move options

+
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)
+
+
    +
  1. If the player can en passant.
  2. +
  3. Keep only the move options that are en passant.
  4. +
  5. Set the move options to the new list.
  6. +
+

Finishing Up

+

Now that we've implemented our rule, we can add it to the board:

+
board = Board(
+    ...
+    rules=[ForcedEnPassant()]
+)
+
+

And that's it! We've implemented a rule that makes en passant forced when it's possible. +Let's see it in action:

+

Forced En Passant

+ + + + + + +
+
+ + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/guide/tutorials/knook/index.html b/guide/tutorials/knook/index.html new file mode 100644 index 0000000..5ac66c3 --- /dev/null +++ b/guide/tutorials/knook/index.html @@ -0,0 +1,1303 @@ + + + + + + + + + + + + + + + + + + + + + + + + Knook - Adding a new piece - ChessMaker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Knook

+

Introduction

+

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 Itself

+

Implementing the Piece

+

The Knook can move like a knight and a rook. +We can ue functions in piece_utils to help us implement this.

+
knook.py
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)
+
+
    +
  1. We could have also used the knight's MOVE_OFFSETS constant.
  2. +
  3. The name of the piece is used for display purposes, and it's a class property.
  4. +
  5. The _get_move_options method is called when the piece is asked for its move options. + It returns an iterable of MoveOption objects.
  6. +
  7. We get all of a Knight's move options, filter out the ones that are out of the board, + and filter out the ones that are blocked by a piece of the same player.
  8. +
  9. We add all of a Rook's move options, and filter out the ones that are blocked by +a piece of the same player.
  10. +
  11. We return the move options as a list of MoveOption objects. The positions_to_move_options + function is a helper function adds the captures argument if the position is occupied by + a piece.
  12. +
+

Making it displayable

+

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.

+
main.py
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.

+

Implementing merging

+

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.

+

Knookable

+

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:
+    pass
+
+

Getting the merge move options

+

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)
+
+
    +
  1. We only want to merge with pieces of the same player.
  2. +
  3. We only want to merge with pieces that are Knookable, and not the same type as the piece + (e.g. we can't merge a knight with another knight).
  4. +
  5. We return a move option with the knook extra argument set to True, + so that we can later easily know that this move option is for merging.
  6. +
+

Performing the merge

+

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))
+
+
    +
  1. Because of how we implemented this, we're not able to provide both the + rook and the knight in the event, so we just provide the piece that + initiated the merge. This isn't a problem, because this is our own event.
  2. +
  3. We create a BeforeMergeToKnookEvent event that will allow subscribers to + change the Knook object that will be created.
  4. +
  5. We use the Events _set method to change the knook attribute, + which can't be changed regularly (because we want the rest of the event to be immutable).
  6. +
  7. We're subscribing to the 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.
  8. +
  9. We check if the move option has the knook extra argument set to True.
  10. +
  11. We create the BeforeMergeToKnookEvent event in a separate variable, + so that we can still access it after we publish it.
  12. +
  13. We get the knook object from the event, so that subscribers can change it.
  14. +
  15. Finally, we change the piece on the new position to a knook.
  16. +
+

The new pieces

+

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)
+
+
    +
  1. While it's clear that we need to inherit from 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.
  2. +
  3. We subscribe to the 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.
  4. +
  5. If we didn't know the knight's MOVE_OFFSETS constant, we would just create + our own.
  6. +
  7. We get the positions the knight can move to, without filtering positions + where there's a piece of the same player, and instead filter them + (and convert to move options) using the get_merge_move_options function.
  8. +
  9. We add the move options from the 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.
  10. +
+

Finishing up

+

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:

+

Knook

+ + + + + + +
+
+ + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/guide/tutorials/omnipotent-f6-pawn/index.html b/guide/tutorials/omnipotent-f6-pawn/index.html new file mode 100644 index 0000000..bb18431 --- /dev/null +++ b/guide/tutorials/omnipotent-f6-pawn/index.html @@ -0,0 +1,1110 @@ + + + + + + + + + + + + + + + + + + + + + + + + Omnipotent F6 Pawn - Adding custom moves - ChessMaker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Omnipotent F6 Pawn

+

Introduction

+

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.

+

Designing the Rule

+

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).

+

Structuring the Rule

+

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

    +
      +
    • Pieces in the board
    • +
    +
  • +
  • +

    We need to

    +
      +
    • Add a move option to the piece
    • +
    • Before the piece moves, alter the move option to summon a pawn instead.
    • +
    +
  • +
+
omnipotent_f6_pawn.py
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)
+
+
    +
  1. Notice that while chess notation numbers positions from the bottom left, ChessMaker + numbers them from the top left. This is because the squares are a 2D array - and + the first index is the row, and the second is the column.
  2. +
  3. The reason we're not using the 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.
  4. +
+

Implementing the Rule

+

Again, use the annotations to help you understand what's going on.

+

Adding the Move Option

+
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)
+
+
    +
  1. We only want to add the move option if the F6 square is occupied by an enemy piece.
  2. +
  3. We create a move option that moves to the F6 square, and has an extra + attribute that we'll use later to know that we need to summon a pawn.
  4. +
  5. Because move options are an iterable and not a list, we need to use the + chain function to add the move option to the existing move options.
  6. +
+

Summoning the Pawn

+
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)
+
+
    +
  1. We save the captured piece so we can publish the AfterCaptureEvent later.
  2. +
  3. We only publish a BeforeCapturedEvent and not a BeforeMoveEvent event because + the BeforeMoveEvent event has already been published (we're a subscriber to it).
  4. +
  5. We create a new pawn and place it on the F6 square.
  6. +
  7. We publish the move event for the pawn - and not for the piece that made the move. + This is because that piece isn't actually the one moving, as mentioned before.
  8. +
  9. We cancel the actual move - because we don't want the piece to move + (And because we don't want 2 move events to be published).
  10. +
+

Finishing Up

+

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.

+

Omnipotent F6 Pawn

+ + + + + + +
+
+ + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/guide/tutorials/siberian-swipe/index.html b/guide/tutorials/siberian-swipe/index.html new file mode 100644 index 0000000..ac23fdb --- /dev/null +++ b/guide/tutorials/siberian-swipe/index.html @@ -0,0 +1,1062 @@ + + + + + + + + + + + + + + + + + + + + + + + + Siberian Swipe - Adding move options - ChessMaker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Siberian Swipe

+

Introduction

+

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.

+

Defining the Rule

+

When a rook hasn't moved yet, it can skip any pieces on it's file +in order to capture a distant enemy rook.

+

Structuring the Rule

+

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

    +
      +
    • Current rooks
    • +
    • Rooks that will be added in the future
    • +
    +
  • +
  • +

    We need to

    +
      +
    • Add a capture move option.
    • +
    +
  • +
+
siberian_swipe.py
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)
+
+
    +
  1. When adding move options, it's generally a good idea to add them with a high priority. +So that rules which remove options will be able to run after this rule and remove it if needed.
  2. +
  3. We'll implement this later.
  4. +
  5. Because our rule is stateless, we don't have to do anything special when cloning it.
  6. +
+

Implementing the Rule

+

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.

+
siberian_swipe.py
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)
+
+
    +
  1. As mentioned earlier, it is recommended to use the event's board and not the one +passed to us in on_join_board.
  2. +
  3. If the rook has moved, it can't do a siberian swipe.
  4. +
  5. We check both up and down.
  6. +
  7. We search for rooks in that direction - ignoring all pieces that might block us, +and use piece_utils.is_in_board to check if we reached the edge of the board (or a hole).
  8. +
  9. We want our move to be able to skip over all pieces, so we ignore all pieces that aren't enemy rooks.
  10. +
  11. If there is, we create a move option that captures it. Notice that +we add 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.
  12. +
  13. Because the move options are an iterable, we can't just append to them. +Instead, we use itertools.chain to create a new iterable that contains both +the old move options, and the new move option we created.
  14. +
+

Finishing Up

+

Now that we've implemented our rule, we can add it to the board:

+
board = Board(
+    ...
+    rules=[SiberianSwipe()]
+)
+
+

And that's it! We've implemented a rule that adds a new move option to rooks. +Let' see it in action:

+

Siberian Swipe

+ + + + + + +
+
+ + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..1b516e5 --- /dev/null +++ b/index.html @@ -0,0 +1,922 @@ + + + + + + + + + + + + + + + + + + + + + + ChessMaker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

ChessMaker

+

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

+
+

What is 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:

+
    +
  • Chess960
  • +
  • Knooks
  • +
  • Forced En Passant
  • +
  • Knight Boosting
  • +
  • Siberian Swipe
  • +
  • Il Vaticano
  • +
  • Beta Decay
  • +
  • La Bastarda
  • +
  • Omnipotent F6 Pawn
  • +
  • King Cant Move to C2
  • +
  • Vertical Castling
  • +
  • Double Check to Win
  • +
  • Capture All Pieces to Win
  • +
  • Duck Chess
  • +
+

Contributions of new variants or anything else you'd like to see in the project are welcome!

+

What ChessMaker isn't

+
    +
  • A complete chess server - It currently doesn't support users, matchmaking, ratings, cheating detection, +and is very thin. The frontend is very simple and currently not the focus of the project.
  • +
  • A chess engine - The design choices are not optimized for speed, and it doesn't provide any analysis or AI.
  • +
  • A compliant or standard chess implementation - It doesn't support UCI or existing chess GUIs, +because it allows rules that wouldn't be possible with those.
  • +
+
+

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.

+
+ + + + + + +
+
+ + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/packaged-variants/index.html b/packaged-variants/index.html new file mode 100644 index 0000000..c96d2a1 --- /dev/null +++ b/packaged-variants/index.html @@ -0,0 +1,902 @@ + + + + + + + + + + + + + + + + + + + + + + + + Packaged Variants - ChessMaker + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Packaged Variants

+

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.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariantDescription
Chess960Chess960, as described here.
KnooksRooks 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 PassantEn 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 BoostingWhen a pawn is promoted to a knight, the player gets an extra turn to move that knight.
Omnipotent F6 PawnWhen there is an enemy piece on F6, a pawn can be summoned there.
Siberian SwipeWhen a Rook hasn't moved yet, it can jump over all pieces in a file to capture an enemy opponent's rook.
Il VaticanoWhen 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 DecayWhen 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 BastardaWhen 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 C2Kings cannot move to C2.
Vertical CastlingCastlling can also be done vertically.
Double Check To WinIn addition to checkmating, a player can win by giving check with 2 pieces or more.
Capture All Pieces To WinInstead of checkmating, a player can win by capturing all pieces of the opponent. Kings can be under attack and can be taken.
Duck ChessThere 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.
+ + + + + + +
+
+ + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/search/search_index.json b/search/search_index.json new file mode 100644 index 0000000..ecd082c --- /dev/null +++ b/search/search_index.json @@ -0,0 +1 @@ +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"ChessMaker","text":"

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:

  • Chess960
  • Knooks
  • Forced En Passant
  • Knight Boosting
  • Siberian Swipe
  • Il Vaticano
  • Beta Decay
  • La Bastarda
  • Omnipotent F6 Pawn
  • King Cant Move to C2
  • Vertical Castling
  • Double Check to Win
  • Capture All Pieces to Win
  • Duck Chess

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":"
  • A complete chess server - It currently doesn't support users, matchmaking, ratings, cheating detection, and is very thin. The frontend is very simple and currently not the focus of the project.
  • A chess engine - The design choices are not optimized for speed, and it doesn't provide any analysis or AI.
  • A compliant or standard chess implementation - It doesn't support UCI or existing chess GUIs, because it allows rules that wouldn't be possible with those.

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
  1. 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.

  2. In order to use custom pieces, you need to provide the URLs of the images as a tuple with a URL for each player.

  3. 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).

"},{"location":"getting-started/#creating-a-standard-game-factory","title":"Creating a standard game factory","text":"

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.

"},{"location":"guide/concepts/base-game/#square","title":"Square","text":"

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.

"},{"location":"guide/concepts/base-game/#move-option","title":"Move Option","text":"

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.

Info

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.

"},{"location":"guide/concepts/cloneables/#what-is-cloneable","title":"What is Cloneable?","text":"
  • The board
  • Squares
  • Pieces
  • Rules

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.

"},{"location":"guide/concepts/events/","title":"Events","text":""},{"location":"guide/concepts/events/#introduction","title":"Introduction","text":"

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.

"},{"location":"guide/concepts/events/#event-priorities","title":"Event Priorities","text":"

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.

"},{"location":"guide/tutorials/capture-all-pieces-to-win/#creating-the-result-function","title":"Creating the result function","text":"capture_all_pieces_to_win.py
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
  1. We check if the player has any pieces left.
  2. If no players have lost, the game is not over - we return None.
  3. If only one player has no pieces, we return a string saying that they have lost. We specifically check for 1 player to support a custom rule that could allow a piece to destroy itself - causing both players to have no pieces left.
  4. If both players have no pieces, we return a string saying that the game is a draw.

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

    • Pawns of a player.
  • We need to

    • Check if the pawn can en passant.

And if any of the pawns can:

  • For all

    • Pieces of the same player.
  • We need to

    • Remove all moves that aren't en passant.
forced_en_passant.py
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
  1. For each player.
  2. For each pawn of the player.
  3. If any of the pawn's move options is an en passant.
  4. If any of the player's pawns can en passant, we don't need to check the other pawns.
"},{"location":"guide/tutorials/forced-en-passant/#removing-all-other-move-options","title":"Removing all other move options","text":"
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
  1. If the player can en passant.
  2. Keep only the move options that are en passant.
  3. Set the move options to the new list.
"},{"location":"guide/tutorials/forced-en-passant/#finishing-up","title":"Finishing Up","text":"

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.

knook.py
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
  1. We could have also used the knight's MOVE_OFFSETS constant.
  2. The name of the piece is used for display purposes, and it's a class property.
  3. The _get_move_options method is called when the piece is asked for its move options. It returns an iterable of MoveOption objects.
  4. We get all of a Knight's move options, filter out the ones that are out of the board, and filter out the ones that are blocked by a piece of the same player.
  5. We add all of a Rook's move options, and filter out the ones that are blocked by a piece of the same player.
  6. We return the move options as a list of MoveOption objects. The positions_to_move_options function is a helper function adds the captures argument if the position is occupied by a piece.
"},{"location":"guide/tutorials/knook/#making-it-displayable","title":"Making it displayable","text":"

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.

main.py
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
  1. We only want to merge with pieces of the same player.
  2. We only want to merge with pieces that are Knookable, and not the same type as the piece (e.g. we can't merge a knight with another knight).
  3. We return a move option with the knook extra argument set to True, so that we can later easily know that this move option is for merging.
"},{"location":"guide/tutorials/knook/#performing-the-merge","title":"Performing the merge","text":"

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
  1. Because of how we implemented this, we're not able to provide both the rook and the knight in the event, so we just provide the piece that initiated the merge. This isn't a problem, because this is our own event.
  2. We create a BeforeMergeToKnookEvent event that will allow subscribers to change the Knook object that will be created.
  3. We use the Events _set method to change the knook attribute, which can't be changed regularly (because we want the rest of the event to be immutable).
  4. We're subscribing to the 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.
  5. We check if the move option has the knook extra argument set to True.
  6. We create the BeforeMergeToKnookEvent event in a separate variable, so that we can still access it after we publish it.
  7. We get the knook object from the event, so that subscribers can change it.
  8. Finally, we change the piece on the new position to a knook.
"},{"location":"guide/tutorials/knook/#the-new-pieces","title":"The new pieces","text":"

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
  1. While it's clear that we need to inherit from 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.
  2. We subscribe to the 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.
  3. If we didn't know the knight's MOVE_OFFSETS constant, we would just create our own.
  4. We get the positions the knight can move to, without filtering positions where there's a piece of the same player, and instead filter them (and convert to move options) using the get_merge_move_options function.
  5. We add the move options from the 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.
"},{"location":"guide/tutorials/knook/#finishing-up","title":"Finishing up","text":"

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).

"},{"location":"guide/tutorials/omnipotent-f6-pawn/#structuring-the-rule","title":"Structuring the Rule","text":"

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

    • Pieces in the board
  • We need to

    • Add a move option to the piece
    • Before the piece moves, alter the move option to summon a pawn instead.
omnipotent_f6_pawn.py
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
  1. Notice that while chess notation numbers positions from the bottom left, ChessMaker numbers them from the top left. This is because the squares are a 2D array - and the first index is the row, and the second is the column.
  2. The reason we're not using the 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.
"},{"location":"guide/tutorials/omnipotent-f6-pawn/#implementing-the-rule","title":"Implementing the Rule","text":"

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
  1. We only want to add the move option if the F6 square is occupied by an enemy piece.
  2. We create a move option that moves to the F6 square, and has an extra attribute that we'll use later to know that we need to summon a pawn.
  3. Because move options are an iterable and not a list, we need to use the chain function to add the move option to the existing move options.
"},{"location":"guide/tutorials/omnipotent-f6-pawn/#summoning-the-pawn","title":"Summoning the Pawn","text":"
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
  1. We save the captured piece so we can publish the AfterCaptureEvent later.
  2. We only publish a BeforeCapturedEvent and not a BeforeMoveEvent event because the BeforeMoveEvent event has already been published (we're a subscriber to it).
  3. We create a new pawn and place it on the F6 square.
  4. We publish the move event for the pawn - and not for the piece that made the move. This is because that piece isn't actually the one moving, as mentioned before.
  5. We cancel the actual move - because we don't want the piece to move (And because we don't want 2 move events to be published).
"},{"location":"guide/tutorials/omnipotent-f6-pawn/#finishing-up","title":"Finishing Up","text":"

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

    • Current rooks
    • Rooks that will be added in the future
  • We need to

    • Add a capture move option.
siberian_swipe.py
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
  1. When adding move options, it's generally a good idea to add them with a high priority. So that rules which remove options will be able to run after this rule and remove it if needed.
  2. We'll implement this later.
  3. Because our rule is stateless, we don't have to do anything special when cloning it.
"},{"location":"guide/tutorials/siberian-swipe/#implementing-the-rule","title":"Implementing the Rule","text":"

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.

siberian_swipe.py
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
  1. As mentioned earlier, it is recommended to use the event's board and not the one passed to us in on_join_board.
  2. If the rook has moved, it can't do a siberian swipe.
  3. We check both up and down.
  4. We search for rooks in that direction - ignoring all pieces that might block us, and use piece_utils.is_in_board to check if we reached the edge of the board (or a hole).
  5. We want our move to be able to skip over all pieces, so we ignore all pieces that aren't enemy rooks.
  6. If there is, we create a move option that captures it. Notice that we add 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.
  7. Because the move options are an iterable, we can't just append to them. Instead, we use itertools.chain to create a new iterable that contains both the old move options, and the new move option we created.
"},{"location":"guide/tutorials/siberian-swipe/#finishing-up","title":"Finishing Up","text":"

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 @@ + + + + https://wolfdwyc.github.io/ChessMaker/ + 2023-07-21 + daily + + + https://wolfdwyc.github.io/ChessMaker/getting-started/ + 2023-07-21 + daily + + + https://wolfdwyc.github.io/ChessMaker/packaged-variants/ + 2023-07-21 + daily + + + https://wolfdwyc.github.io/ChessMaker/api-reference/board/ + 2023-07-21 + daily + + + https://wolfdwyc.github.io/ChessMaker/api-reference/events/ + 2023-07-21 + daily + + + https://wolfdwyc.github.io/ChessMaker/api-reference/game/ + 2023-07-21 + daily + + + https://wolfdwyc.github.io/ChessMaker/api-reference/move-option/ + 2023-07-21 + daily + + + https://wolfdwyc.github.io/ChessMaker/api-reference/piece-utils/ + 2023-07-21 + daily + + + https://wolfdwyc.github.io/ChessMaker/api-reference/piece/ + 2023-07-21 + daily + + + https://wolfdwyc.github.io/ChessMaker/api-reference/player/ + 2023-07-21 + daily + + + https://wolfdwyc.github.io/ChessMaker/api-reference/position/ + 2023-07-21 + daily + + + https://wolfdwyc.github.io/ChessMaker/api-reference/rule/ + 2023-07-21 + daily + + + https://wolfdwyc.github.io/ChessMaker/api-reference/square/ + 2023-07-21 + daily + + + https://wolfdwyc.github.io/ChessMaker/guide/first-steps/ + 2023-07-21 + daily + + + https://wolfdwyc.github.io/ChessMaker/guide/concepts/base-game/ + 2023-07-21 + daily + + + https://wolfdwyc.github.io/ChessMaker/guide/concepts/cloneables/ + 2023-07-21 + daily + + + https://wolfdwyc.github.io/ChessMaker/guide/concepts/events/ + 2023-07-21 + daily + + + https://wolfdwyc.github.io/ChessMaker/guide/tutorials/capture-all-pieces-to-win/ + 2023-07-21 + daily + + + https://wolfdwyc.github.io/ChessMaker/guide/tutorials/forced-en-passant/ + 2023-07-21 + daily + + + https://wolfdwyc.github.io/ChessMaker/guide/tutorials/knook/ + 2023-07-21 + daily + + + https://wolfdwyc.github.io/ChessMaker/guide/tutorials/omnipotent-f6-pawn/ + 2023-07-21 + daily + + + https://wolfdwyc.github.io/ChessMaker/guide/tutorials/siberian-swipe/ + 2023-07-21 + daily + + \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz new file mode 100644 index 0000000..748b5e8 Binary files /dev/null and b/sitemap.xml.gz differ