From 813134ad68ec9e55f30c0faf770977572f8fd1db Mon Sep 17 00:00:00 2001 From: simoncreates Date: Tue, 3 Mar 2026 20:06:50 +0100 Subject: [PATCH] beginning of transition --- app/chess_sim/game_board.py | 108 ++++++++++++++++++++++++++++++++++-- app/chess_sim/test.py | 27 +++++++++ app/sockets/socket.py | 27 +++++---- 3 files changed, 146 insertions(+), 16 deletions(-) diff --git a/app/chess_sim/game_board.py b/app/chess_sim/game_board.py index 0a1321d..d44bda6 100644 --- a/app/chess_sim/game_board.py +++ b/app/chess_sim/game_board.py @@ -1,11 +1,13 @@ from enum import Enum, auto from dataclasses import dataclass from typing import Iterator, Optional, List, Tuple -import random - +import copy FILES = "abcdefgh" RANKS = "12345678" + + + class PieceType(Enum): PAWN = auto() KNIGHT = auto() @@ -26,6 +28,19 @@ class Color(Enum): else: return Color.WHITE +class Outcome(Enum): + DRAW = auto() + WHITE_WIN = auto() + BLACK_WIN = auto() + NOT_FINISHED = auto() + + @classmethod + def from_color(cls, color: Color) -> "Outcome": + if color == Color.WHITE: + return Outcome.WHITE_WIN + else: + return Outcome.BLACK_WIN + @dataclass class BoardPos: @@ -99,6 +114,87 @@ class ChessBoard: fields: List[List[BoardField]] num_moves: int move_history: List[Tuple['BoardMove', Optional[Piece]]] + initial_board: List[List[BoardField]] + + @property + def turn(self) -> "Color": + len_mv_history = len(self.move_history) + if len_mv_history > 0: + + first_moved_piece_color = self.initial_move_color + if len_mv_history % 2 == 0: + return first_moved_piece_color + else: + return first_moved_piece_color.opposite + else: + return Color.WHITE + + @property + def initial_move_color(self) -> Color: + first_move = self.move_history[0] + from_pos = first_move[0].m_from + # look up what color the piece had that moved first + # (odd solution but i think its quite fine) + first_moved_field = self.initial_board[from_pos.x][from_pos.y] + first_moved_piece_color = Color.WHITE + if first_moved_field.piece is not None: + first_moved_piece_color = first_moved_field.piece.color + return first_moved_piece_color + + def is_checkmate(self) -> bool: + cur_turn_color = self.turn + return len(self.generate_moves(cur_turn_color)) == 0 and self.is_field_attacked(self.pos_of_king(cur_turn_color), cur_turn_color.opposite) + + def is_stalemate(self) -> bool: + cur_turn_color = self.turn + return len(self.generate_moves(cur_turn_color)) == 0 + + def is_seventyfive_moves(self) -> bool: + return len(self.move_history) >= 75 + + def is_fivefold_repetition(self) -> bool: + return self.highest_repetiton_amount() >= 5 + + def outcome(self) -> Outcome: + if self.is_checkmate(): + return Outcome.from_color(self.turn) + if self.is_stalemate() or self.is_seventyfive_moves or self.is_fivefold_repetition: + return Outcome.DRAW + return Outcome.NOT_FINISHED + + def highest_repetiton_amount(self) -> int: + if self.move_history: + starting_color = self.initial_move_color + else: + starting_color = Color.WHITE + + dummy = ChessBoard( + copy.deepcopy(self.initial_board), + 0, + [], + copy.deepcopy(self.initial_board), + ) + + color = starting_color + states: list[str] = [] + + states.append(str(dummy)) + + for mv, _ in self.move_history: + dummy.make_move(mv, color) + color = color.opposite + states.append(str(dummy)) + + counts: dict[str, int] = {} + max_count = 0 + + for state in states: + counts[state] = counts.get(state, 0) + 1 + if counts[state] > max_count: + max_count = counts[state] + + return max_count + def place(self, row: int, col: int, piece_type: PieceType, color: Color): self.fields[row][col].piece = Piece(piece_type, color) @@ -106,8 +202,9 @@ class ChessBoard: # initialize default starting position @classmethod def init_default(cls) -> "ChessBoard": + empty_board = [[BoardField() for _ in range(8)] for _ in range(8)] brd = cls( - [[BoardField() for _ in range(8)] for _ in range(8)], 0, [] + empty_board, 0, [], empty_board ) # place pawns @@ -135,6 +232,9 @@ class ChessBoard: for col, piece_type in enumerate(back_rank): brd.place(7, col, piece_type, Color.WHITE) + #set the inital board for later ref + brd.initial_board = brd.fields + return brd # attempt to make move for color @@ -317,7 +417,7 @@ class ChessBoard: color.opposite, ): # check if the last move moved the pawn there from the starting square - last_move = self.move_history[move_history_len][0] + last_move = self.move_history[-1][0] # final move, not past-the-end if last_move.m_from == BoardPos((tr, tc + en_passant_dir)) and last_move.m_to == BoardPos((tr, tc - en_passant_dir)): moves.append(BoardMove(pos, BoardPos((tr, tc)))) diff --git a/app/chess_sim/test.py b/app/chess_sim/test.py index 8ac6188..0d963d0 100644 --- a/app/chess_sim/test.py +++ b/app/chess_sim/test.py @@ -65,6 +65,7 @@ def run_random_games(n: int = 20, max_moves: int = 200, verbose: bool = False): def test_castling_kingside_both_sides(): board = ChessBoard.init_default() + # rewrite with boardmove from string seq = [ (BoardMove(BoardPos((7, 6)), BoardPos((5, 5))), Color.WHITE), (BoardMove(BoardPos((0, 1)), BoardPos((2, 2))), Color.BLACK), @@ -139,6 +140,32 @@ def main(): test_castling_kingside_both_sides() run_random_games(verbose=False) + board = ChessBoard.init_default() + # helper to create boardmove from two-square string + def bm(s): + return BoardMove( + ChessBoard.algebraic_to_pos(s[0:2]), + ChessBoard.algebraic_to_pos(s[2:4]) + ) + + # perform four cycles of the knight shuffle + # Nf3-Nf6-Ng1-Ng8 and returns to the starting position. + seq = [] + for _ in range(4): + seq.extend([ + (bm("g1f3"), Color.WHITE), + (bm("g8f6"), Color.BLACK), + (bm("f3g1"), Color.WHITE), + (bm("f6g8"), Color.BLACK), + ]) + for move, color in seq: + ok = board.make_move(move, color) + assert ok, "repetition test move failed" + + rep = board.highest_repetiton_amount() + assert rep >= 5, f"expected at least 5 repetitions, got {rep}" + assert board.is_fivefold_repetition(), "board should report fivefold repetition" + print(f"all tests passed") diff --git a/app/sockets/socket.py b/app/sockets/socket.py index beeb62a..c0f9732 100644 --- a/app/sockets/socket.py +++ b/app/sockets/socket.py @@ -5,7 +5,8 @@ from dataclasses import dataclass, field from threading import Lock from typing import Optional -import chess #todo: replace with own chess logic implementation to remove dependency +import chess +from chess_sim.game_board import ChessBoard, Color, Outcome from flask_login import current_user from flask import request from flask_socketio import emit, join_room, leave_room @@ -23,7 +24,7 @@ class GameRoom: p2_sid: Optional[str] = None p2_name: Optional[str] = None ready: dict[str, bool] = field(default_factory=dict) - board: chess.Board = field(default_factory=chess.Board) + board: ChessBoard = ChessBoard.init_default() color_by_sid: dict[str, str] = field(default_factory=dict) initial_ms: int = 600000 increment_ms: int = 0 @@ -81,14 +82,14 @@ def _sid_for_color(room: GameRoom, color: str) -> Optional[str]: def _turn_sid(room: GameRoom) -> Optional[str]: - return _sid_for_color(room, "w" if room.board.turn == chess.WHITE else "b") + return _sid_for_color(room, "w" if room.board.turn == Color.WHITE else "b") def _clock_payload(room: GameRoom) -> dict: return { "white_time_left_ms": max(0, room.white_ms), "black_time_left_ms": max(0, room.black_ms), - "turn": "w" if room.board.turn == chess.WHITE else "b", + "turn": "w" if room.board.turn == Color.WHITE else "b", } @@ -101,7 +102,7 @@ def _apply_elapsed(room: GameRoom) -> Optional[str]: if elapsed_ms <= 0: return None - active_color = "w" if room.board.turn == chess.WHITE else "b" + active_color = "w" if room.board.turn == Color.WHITE else "b" if active_color == "w": room.white_ms = max(0, room.white_ms - elapsed_ms) else: @@ -125,13 +126,14 @@ def _cleanup_room(code: str) -> None: code_by_sid.pop(room.p2_sid, None) -def _game_over_reason(board: chess.Board) -> str: +def _game_over_reason(board: ChessBoard) -> str: if board.is_checkmate(): return "checkmate" if board.is_stalemate(): return "stalemate" - if board.is_insufficient_material(): - return "insufficient material" + # todo introduce game over reason insufficient material + # if board.is_insufficient_material(): + # return "insufficient material" if board.is_seventyfive_moves(): return "75-move rule" if board.is_fivefold_repetition(): @@ -143,10 +145,11 @@ def _emit_game_over(room: GameRoom, reason: str) -> None: room.completed = True room.game_active = False - outcome = room.board.outcome(claim_draw=True) + outcome = room.board.outcome() p1_result = "draw" p2_result = "draw" + if outcome and outcome.winner is not None: winner_color = "w" if outcome.winner == chess.WHITE else "b" if room.color_by_sid.get(room.p1_sid) == winner_color: @@ -181,7 +184,7 @@ def _start_game_if_ready(room: GameRoom) -> None: if not room.ready.get(room.p1_sid) or not room.ready.get(room.p2_sid): return - room.board = chess.Board() + room.board = ChessBoard.init_default() if room.p1_pref == "w": p1_color = "w" elif room.p1_pref == "b": @@ -202,14 +205,14 @@ def _start_game_if_ready(room: GameRoom) -> None: p1_payload = { "play_as": p1_color, "time_left_ms": room.white_ms if p1_color == "w" else room.black_ms, - "fen": room.board.fen(), + "fen": room.board.to_fen(), "opponent": room.p2_name, **_clock_payload(room), } p2_payload = { "play_as": p2_color, "time_left_ms": room.white_ms if p2_color == "w" else room.black_ms, - "fen": room.board.fen(), + "fen": room.board.to_fen(), "opponent": room.p1_name, **_clock_payload(room), }