Skip to content

Commit

Permalink
Edited the README, made the piece's player property writable, fixed a…
Browse files Browse the repository at this point in the history
… bug with cancelling the turn changed event, split the checkmate result to checkmate and no kings left, and added a duck chess variant.
  • Loading branch information
WolfDWyc committed May 19, 2023
1 parent 31277ef commit 611dff2
Show file tree
Hide file tree
Showing 27 changed files with 264 additions and 189 deletions.
24 changes: 14 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ Play: [https://chessmaker.azurewebsites.net](https://chessmaker.azurewebsites.ne
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.

ChessMaker isn't tied to any GUI, but comes with a thin, [pywebio](https://pywebio.readthedocs.io/en/latest/), multiplayer web interface.

It was inspired by [r/AnarchyChess](https://www.reddit.com/r/AnarchyChess/) - and the packaged optional rules are almost all inspired by that subreddit.

These rules are:
ChessMaker isn't tied to any GUI, but comes with a thin, [pywebio](https://pywebio.readthedocs.io/en/latest/), 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
Expand All @@ -37,16 +40,17 @@ These rules are:
* 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 doesn't support users, matchmaking, ratings, cheating detection, etc.
The frontend is very simple and 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,
* 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.


-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.
1 change: 1 addition & 0 deletions chessmaker/chess/base/board.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ def _on_after_move(self, _: AfterMoveEvent):
before_turn_change_event = BeforeTurnChangeEvent(self, next_player)
self.publish(before_turn_change_event)
if before_turn_change_event.cancelled:
self.turn_iterator = itertools.chain([next_player], self.turn_iterator)
return
self.current_player = before_turn_change_event.next_player
self.publish(AfterTurnChangeEvent(self, self.current_player))
Expand Down
6 changes: 1 addition & 5 deletions chessmaker/chess/base/piece.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class BeforeCapturedEvent(AfterCapturedEvent):
class Piece(Cloneable, EventPublisher):
def __init__(self, player: Player):
super().__init__()
self._player = player
self.player = player
self._board: Board = None

def __repr__(self):
Expand Down Expand Up @@ -92,10 +92,6 @@ def move(self, move_option: MoveOption):
def on_join_board(self):
pass

@property
def player(self):
return self._player

@property
def position(self):
return self.board._get_piece_position(self)
Expand Down
16 changes: 11 additions & 5 deletions chessmaker/chess/game_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,17 @@
from chessmaker.chess.base.player import Player
from chessmaker.chess.base.square import Square
from chessmaker.chess.pieces.bishop import Bishop
from chessmaker.chess.pieces.duck import Duck
from chessmaker.chess.pieces.king import King
from chessmaker.chess.pieces.knight import Knight
from chessmaker.chess.pieces.knook.knookable_knight import KnookableKnight
from chessmaker.chess.pieces.knook.knookable_rook import KnookableRook
from chessmaker.chess.pieces.pawn import Pawn
from chessmaker.chess.pieces.queen import Queen
from chessmaker.chess.pieces.rook import Rook
from chessmaker.chess.results import stalemate, Repetition, NoCapturesOrPawnMoves, checkmate
from chessmaker.chess.results import stalemate, Repetition, NoCapturesOrPawnMoves, checkmate, no_kings
from chessmaker.chess.rules import ForcedEnPassant, KnightBoosting, OmnipotentF6Pawn, SiberianSwipe, IlVaticano, \
BetaDecay, KingCantMoveToC2, LaBastarda
BetaDecay, KingCantMoveToC2, LaBastarda, DuckChess


class A:
Expand All @@ -39,13 +40,15 @@ def create_game(
vertical_castling: bool = False,
double_check_to_win: bool = False,
capture_all_pieces_to_win: bool = False,
duck_chess: bool = False,
):
_knight = Knight
_rook = Rook
castling_directions = ((1, 0), (-1, 0))
if vertical_castling:
castling_directions = tuple(list(castling_directions) + [(0, 1), (0, -1)])
_king = lambda player: King(player, attackable=capture_all_pieces_to_win, castling_directions=castling_directions)
attackable = capture_all_pieces_to_win or duck_chess
_king = lambda player: King(player, attackable=attackable, castling_directions=castling_directions)
if knooks:
_knight = KnookableKnight
_rook = KnookableRook
Expand Down Expand Up @@ -100,6 +103,7 @@ def _piece_row() -> list[Callable[[Player], Piece]]:
(omnipotent_f6_pawn, OmnipotentF6Pawn(pawn=_pawn)),
(la_bastarda, LaBastarda(pawn=_pawn)),
(beta_decay, BetaDecay([_rook, Bishop, _pawn])),
(duck_chess, DuckChess()),
]:
if enabled:
rules.append(rule)
Expand All @@ -108,7 +112,9 @@ def _piece_row() -> list[Callable[[Player], Piece]]:
if capture_all_pieces_to_win:
result_functions.insert(0, results.capture_all_pieces_to_win)
else:
result_functions.insert(0, checkmate)
if not duck_chess:
result_functions.insert(0, checkmate)
result_functions.insert(0, no_kings)

if double_check_to_win:
result_functions.insert(0, results.double_check_to_win)
Expand All @@ -129,7 +135,7 @@ def __call__(self, board: Board):
[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(7) + [Square(Duck(white) if duck_chess else None)],
_empty_line(8),
_empty_line(8),
[Square(_pawn(white)) for _ in range(8)],
Expand Down
34 changes: 34 additions & 0 deletions chessmaker/chess/pieces/duck.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from typing import Iterable

from chessmaker.chess.base import Player
from chessmaker.chess.base.move_option import MoveOption
from chessmaker.chess.base.piece import Piece, BeforeGetMoveOptionsEvent
from chessmaker.events import EventPriority


class Duck(Piece):
def __init__(self, player: Player, movable: bool = False):
super().__init__(player)
self.movable = movable

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

def on_join_board(self):
self.board.subscribe(BeforeGetMoveOptionsEvent, self._on_before_get_move_options, EventPriority.HIGH)

def _on_before_get_move_options(self, event: BeforeGetMoveOptionsEvent):
if self.movable:
if event.piece is not self:
event.set_move_options([])

def _get_move_options(self) -> Iterable[MoveOption]:
if self.movable:
for square in self.board:
if square.piece is None and square.position != self.position:
yield MoveOption(square.position)

def clone(self):
return Duck(self.player, movable=self.movable)
1 change: 1 addition & 0 deletions chessmaker/chess/results/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .standard_result import StandardResult
from .no_kings import no_kings
from .checkmate import checkmate
from .stalemate import stalemate
from .repetition import Repetition
Expand Down
14 changes: 4 additions & 10 deletions chessmaker/chess/results/checkmate.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,8 @@


def checkmate(board: Board) -> str | None:
is_stalemate = stalemate(board)
if is_stalemate:
current_player = board.current_player
player_pieces = list(board.get_player_pieces(current_player))

kings = [piece for piece in player_pieces if isinstance(piece, King)]
if not kings or all(king.is_attacked() for king in kings):
return f"Checkmate - {current_player.name} loses"


current_player = board.current_player
kings = [piece for piece in board.get_player_pieces(current_player) if isinstance(piece, King)]

if stalemate(board) and all(king.is_attacked() for king in kings):
return f"Checkmate - {current_player.name} loses"
8 changes: 8 additions & 0 deletions chessmaker/chess/results/no_kings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from chessmaker.chess.base import Board
from chessmaker.chess.pieces import King


def no_kings(board: Board) -> str | None:
for player in board.players:
if not [piece for piece in board.get_player_pieces(player) if isinstance(piece, King)]:
return f"No kings left - {player.name} loses"
2 changes: 1 addition & 1 deletion chessmaker/chess/results/repetition.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ def __call__(self, board: Board) -> str | None:

self.positions[position_hash] = self.positions.get(position_hash, 0) + 1
if self.positions[position_hash] >= self.needed_repetitions:
return "Repetition - Draw"
return "Repetition - Draw"
2 changes: 2 additions & 0 deletions chessmaker/chess/results/standard_result.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from chessmaker.chess.base.board import Board
from chessmaker.chess.results import no_kings
from chessmaker.chess.results.checkmate import checkmate
from chessmaker.chess.results.no_captures_or_pawn_moves import NoCapturesOrPawnMoves
from chessmaker.chess.results.repetition import Repetition
Expand All @@ -8,6 +9,7 @@
class StandardResult:
def __init__(self):
self.results = [
no_kings,
checkmate,
stalemate,
Repetition(),
Expand Down
3 changes: 2 additions & 1 deletion chessmaker/chess/rules/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
from .omnipotent_f6_pawn import OmnipotentF6Pawn
from .siberian_swipe import SiberianSwipe
from .king_cant_move_to_c2 import KingCantMoveToC2
from .la_bastarda import LaBastarda
from .la_bastarda import LaBastarda
from .duck_chess import DuckChess
25 changes: 25 additions & 0 deletions chessmaker/chess/rules/duck_chess.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from itertools import chain

from chessmaker.chess.base import Board, as_rule, BeforeTurnChangeEvent
from chessmaker.chess.pieces.duck import Duck
from chessmaker.events import EventPriority


def on_before_turn_change(event: BeforeTurnChangeEvent):
current_player = event.board.current_player
if current_player != event.next_player:
duck: Duck = [piece for piece in event.board.get_pieces() if isinstance(piece, Duck)][0]
if duck.movable:
duck.movable = False
duck.player = event.next_player
else:
event.board.turn_iterator = chain([event.next_player], event.board.turn_iterator)
event.set_next_player(current_player)
duck.movable = True


def duck_chess(board: Board):
board.subscribe(BeforeTurnChangeEvent, on_before_turn_change, EventPriority.HIGH)


DuckChess = as_rule(duck_chess)
11 changes: 7 additions & 4 deletions chessmaker/clients/pywebio_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ def main():
])
shared_position = shared_positions[get_query("position_id")]
new_game(lambda **_: Game(shared_position.board.clone(), deepcopy(shared_position.get_result)),
shared_position.options, form_result["mode"], piece_urls)
shared_position.options, form_result["mode"], piece_urls)
return

form_result = input_group("New Game", [
Expand Down Expand Up @@ -447,7 +447,10 @@ def main():
create_game,
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"],
piece_urls=PIECE_URLS | {"Knook": ["https://i.imgur.com/UiWcdEb.png", "https://i.imgur.com/g7xTVts.png"]}
,remote_access=True, debug=True
"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"]
}
)
Loading

0 comments on commit 611dff2

Please sign in to comment.