From cf1de479c39434f6863c6c3e1ccabebc8dad1a0b Mon Sep 17 00:00:00 2001 From: simoncreates Date: Thu, 26 Feb 2026 21:05:08 +0100 Subject: [PATCH] added castling + rough testing --- app/chess_sim/test.py | 132 +++++++++++++++++++++++++++++++++++------- 1 file changed, 110 insertions(+), 22 deletions(-) diff --git a/app/chess_sim/test.py b/app/chess_sim/test.py index 5d4b88d..185b2d7 100644 --- a/app/chess_sim/test.py +++ b/app/chess_sim/test.py @@ -67,8 +67,8 @@ class Piece: class MoveType(Enum): NORMAL = auto() - CASTLING_KINGSIDE = auto() #todo: implement - CASTLING_QUEENSIDE = auto() #todo: implement + CASTLING_KINGSIDE = auto() + CASTLING_QUEENSIDE = auto() EN_PASSANT = auto() #todo: implement PROMOTION = auto() #todo: implement @@ -76,7 +76,7 @@ class MoveType(Enum): class BoardMove: m_from: BoardPos m_to: BoardPos - move_type: MoveType = MoveType.NORMAL #todo: implement and handle + move_type: MoveType = MoveType.NORMAL promotion_piece: Optional[PieceType] = None #todo: implement and handle def __str__(self) -> str: @@ -137,24 +137,54 @@ class ChessBoard: # attempt to make move for color # returns true on success def make_move(self, move: BoardMove, color: Color) -> bool: - sr, sc = move.m_from.p - tr, tc = move.m_to.p - src_field = self.fields[sr][sc] + captured = None + row_height = 7 if color == Color.WHITE else 0 # validate move using legal moves for the player color legal = self.moves_basic(color) - if not any(m.m_from.p == move.m_from.p and m.m_to.p == move.m_to.p for m in legal): + if not any(m.m_from.p == move.m_from.p and m.m_to.p == move.m_to.p and m.move_type == move.move_type and m.promotion_piece == move.promotion_piece for m in legal): return False + if move.move_type == MoveType.NORMAL: + # perform move and record history for unmaking + captured = self.move_piece(move.m_from, move.m_to).piece + + #castling is hardcoded + elif move.move_type == MoveType.CASTLING_KINGSIDE: + king_src = BoardPos((row_height, 4)) + king_dest = BoardPos((row_height, 6)) + ks_rook_src = BoardPos((row_height, 7)) + ks_rook_dest = BoardPos((row_height, 5)) + self.move_piece(king_src, king_dest) + self.move_piece(ks_rook_src, ks_rook_dest) + + elif move.move_type == MoveType.CASTLING_QUEENSIDE: + king_src = BoardPos((row_height, 4)) + king_dest = BoardPos((row_height, 2)) + ks_rook_src = BoardPos((row_height, 0)) + ks_rook_dest = BoardPos((row_height, 3)) + self.move_piece(king_src, king_dest) + self.move_piece(ks_rook_src, ks_rook_dest) - # perform move and record history for unmaking - captured = self.fields[tr][tc].piece - self.fields[tr][tc].piece = src_field.piece - src_field.piece = None self.move_history.append((move, captured)) self.num_moves += 1 return True + + # moves one piece to another location + # returns the dest field + def move_piece(self, src_pos: BoardPos, dest_pos: BoardPos) -> BoardField: + src_field = self.fields[src_pos.x][src_pos.y] + dest_field = self.fields[dest_pos.x][dest_pos.y] + moving = src_field.piece + captured = BoardField(dest_field.piece) + # move + dest_field.piece = moving + src_field.piece = None + return captured + + def get_field(self, pos: BoardPos) -> Optional[Piece]: + return self.fields[pos.x][pos.y].piece # returns false if there is no move to unmake def unmake_move(self) -> bool: if not self.move_history or len(self.move_history) == 0: @@ -280,6 +310,9 @@ class ChessBoard: row_height = 7 if color == Color.WHITE else 0 expected_king_pos = BoardPos((row_height, 4)) + king_lhs_dest = BoardPos((row_height, 2)) + king_rhs_dest = BoardPos((row_height, 6)) + # if king moved if self.has_piece_moved(expected_king_pos): return moves @@ -287,6 +320,9 @@ class ChessBoard: expected_lhs_rook_pos = BoardPos((row_height, 0)) expected_rhs_rook_pos = BoardPos((row_height, 7)) + lhs_rook_dest = BoardPos((row_height, 3)) + rhs_rook_dest = BoardPos((row_height, 6)) + # squares that must be empty between rook and king lhs_pieces_inbetween = [BoardPos((row_height, i)) for i in range(1, 4)] # cols 1,2,3 rhs_pieces_inbetween = [BoardPos((row_height, i)) for i in range(5, 7)] # cols 5,6 @@ -298,14 +334,14 @@ class ChessBoard: if (not self.has_piece_moved(expected_lhs_rook_pos) and self.are_pieces_none(lhs_pieces_inbetween) and not self.are_fields_attacked(lhs_fields_attacked, color.opposite)): - # todo: allow castling here - pass + moves.append(BoardMove(expected_king_pos, king_lhs_dest, MoveType.CASTLING_QUEENSIDE)) + moves.append(BoardMove(expected_lhs_rook_pos, lhs_rook_dest, MoveType.CASTLING_QUEENSIDE)) if (not self.has_piece_moved(expected_rhs_rook_pos) and self.are_pieces_none(rhs_pieces_inbetween) and not self.are_fields_attacked(rhs_fields_attacked, color.opposite)): - # todo: allow castling here - pass + moves.append(BoardMove(expected_king_pos, king_rhs_dest, MoveType.CASTLING_KINGSIDE)) + moves.append(BoardMove(expected_rhs_rook_pos, rhs_rook_dest, MoveType.CASTLING_QUEENSIDE)) return moves def has_piece_moved(self, pos: BoardPos) -> bool: @@ -432,7 +468,7 @@ class ChessBoard: if move.m_to != king_pos: us_moves_rule_compliant.append(move) - return us_moves_wo_check + return us_moves_rule_compliant def __str__(self) -> str: @@ -470,7 +506,15 @@ def play_random_game(board: Optional[ChessBoard] = None, max_moves: int = 400, v print(f"no legal moves for {current.name} after {moves_played} moves") return board, moves_played - move = random.choice(legal) + # prefer castling for debugging + castling_moves = [ + m for m in legal + if m.move_type in (MoveType.CASTLING_KINGSIDE, MoveType.CASTLING_QUEENSIDE) + ] + if castling_moves: + move = castling_moves[0] + else: + move = random.choice(legal) ok = board.make_move(move, current) # debug assert if not ok: @@ -478,7 +522,9 @@ def play_random_game(board: Optional[ChessBoard] = None, max_moves: int = 400, v if verbose: print(f"make_move returned False for move {move} by {current}") return board, moves_played - + if move.move_type == MoveType.CASTLING_KINGSIDE or move.move_type == MoveType.CASTLING_QUEENSIDE: + print("hell yeah we castled") + raise ValueError("casling") moves_played += 1 if verbose: print(f"{moves_played:03d}: {current.name} played {move}\nboard:\n{board}") @@ -500,13 +546,53 @@ def play_random_game(board: Optional[ChessBoard] = None, max_moves: int = 400, v def run_random_games(n: int = 100, max_moves: int = 400, verbose: bool = False): for i in range(n): - final_board, moves = play_random_game(None, max_moves=max_moves, verbose=verbose and (i < 3)) - if verbose and (i < 3): + final_board, moves = play_random_game(None, max_moves=max_moves, verbose=verbose) + if verbose: print(f"game {i+1}: moves={moves}\nfinal_board:\n{final_board}") return -# used only for testing purposesa +def test_castling_kingside_both_sides(): + board = ChessBoard.init_default() + + seq = [ + (BoardMove(BoardPos((7, 6)), BoardPos((5, 5))), Color.WHITE), + (BoardMove(BoardPos((0, 1)), BoardPos((2, 2))), Color.BLACK), + # clear white pawn allows bishop moving + (BoardMove(BoardPos((6, 6)), BoardPos((5, 6))), Color.WHITE), + # move bishop + (BoardMove(BoardPos((0, 6)), BoardPos((2, 5))), Color.BLACK), + (BoardMove(BoardPos((7, 5)), BoardPos((5, 7))), Color.WHITE), + # clear black pawn for bishop + (BoardMove(BoardPos((1, 4)), BoardPos((2, 4))), Color.BLACK), + # moving black bishop + (BoardMove(BoardPos((0, 5)), BoardPos((1, 4))), Color.BLACK), + + + # white castle kingside + (BoardMove(BoardPos((7, 4)), BoardPos((7, 6)), MoveType.CASTLING_KINGSIDE), Color.WHITE), + # black castle kingside + (BoardMove(BoardPos((0, 4)), BoardPos((0, 6)), MoveType.CASTLING_KINGSIDE), Color.BLACK), + ] + + for move, color in seq: + ok = board.make_move(move, color) + assert ok, f"move {move} by {color} failed unexpectedly on board:\n{board}" + + # validate castle + wk = board.pos_of_king(Color.WHITE) + assert wk == BoardPos((7, 6)), f"white king expected at (7,6), found {wk}" + wf = board.fields[7][5].piece + assert wf is not None and wf.type == PieceType.ROOK and wf.color == Color.WHITE, "white rook not on f1 after castling" + + bk = board.pos_of_king(Color.BLACK) + assert bk == BoardPos((0, 6)), f"black king expected at (0,6), found {bk}" + bf = board.fields[0][5].piece + assert bf is not None and bf.type == PieceType.ROOK and bf.color == Color.BLACK, "black rook not on f8 after castling" + + print("test_castling_kingside_both_sides: passed") + +# used only for testing purposes def main(): default_brd = ChessBoard.init_default() default_brd.generate_moves @@ -539,7 +625,9 @@ def main(): expected_move_count = 20 # 16 pawn moves and 4 knight if len(mvs) != expected_move_count: raise AssertionError(f"Initial move count for white mismatch: expected {expected_move_count}, got {len(mvs)}") - play_random_game(verbose=True) + + test_castling_kingside_both_sides() + run_random_games(verbose=False) print(f"all tests passed")