diff --git a/app/chess_sim/test.py b/app/chess_sim/test.py index d1a964d..5c4f3a2 100644 --- a/app/chess_sim/test.py +++ b/app/chess_sim/test.py @@ -1,7 +1,7 @@ from enum import Enum, auto from dataclasses import dataclass from typing import Optional, List, Tuple - +import random class PieceType(Enum): PAWN = auto() @@ -30,8 +30,9 @@ class Piece: type: PieceType color: Color + # uppercase = White, lowercase = Black def char(self) -> str: - """Single-character representation. Uppercase = White, lowercase = Black.""" + mapping = { PieceType.PAWN: "p", PieceType.KNIGHT: "n", @@ -70,6 +71,8 @@ class BoardField: @dataclass class ChessBoard: fields: List[List[BoardField]] + num_moves: int + move_history: List[Tuple['BoardMove', Optional[Piece]]] def place(self, row: int, col: int, piece_type: PieceType, color: Color): self.fields[row][col].piece = Piece(piece_type, color) @@ -78,7 +81,7 @@ class ChessBoard: @classmethod def init_default(cls) -> "ChessBoard": brd = cls( - [[BoardField() for _ in range(8)] for _ in range(8)] + [[BoardField() for _ in range(8)] for _ in range(8)], 0, [] ) # place pawns @@ -108,6 +111,43 @@ class ChessBoard: return brd + # 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] + + # validate move using legal moves for the player color + legal = self.moves_basic_checked(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): + return False + + # 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 + + # 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: + return False + move, captured = self.move_history.pop() + sr, sc = move.m_from.p + tr, tc = move.m_to.p + + moving_piece = self.fields[tr][tc].piece + + # restore + self.fields[sr][sc].piece = moving_piece + self.fields[tr][tc].piece = captured + self.num_moves = max(0, self.num_moves - 1) + return True + def moves_unchecked(self, piece: Piece, pos: BoardPos) -> List[List[Tuple[int, int]]]: row, column = pos.p rays: List[List[Tuple[int, int]]] = [] @@ -221,7 +261,8 @@ class ChessBoard: def __str__(self) -> str: """ - example string repr of starting pos + example string repr of starting pos: + rnbqkbnr pppppppp ........ @@ -237,36 +278,60 @@ class ChessBoard: lines.append("".join(row_chars)) return "\n".join(lines) - @classmethod - def from_string(cls, s: str) -> "ChessBoard": - lines = s.strip().split("\n") - brd = cls([[BoardField() for _ in range(8)] for _ in range(8)]) +def play_random_game(board: Optional[ChessBoard] = None, max_moves: int = 400, verbose: bool = False) -> Tuple[ ChessBoard, int]: + if board is None: + board = ChessBoard.init_default() - char_to_piece = { - 'p': PieceType.PAWN, - 'n': PieceType.KNIGHT, - 'b': PieceType.BISHOP, - 'r': PieceType.ROOK, - 'q': PieceType.QUEEN, - 'k': PieceType.KING, - } + current = Color.WHITE # white moves first + moves_played = 0 - for r, line in enumerate(lines): - for c, ch in enumerate(line): - if ch == '.': - continue - lower = ch.lower() - if lower not in char_to_piece: - raise ValueError(f"Invalid piece character '{ch}' at ({r},{c})") - piece_type = char_to_piece[lower] - color = Color.WHITE if ch.isupper() else Color.BLACK - brd.place(r, c, piece_type, color) + while moves_played < max_moves: + legal = board.moves_basic_checked(current) + if not legal: + # no legal moves + winner = Color.BLACK if current == Color.WHITE else Color.WHITE + if verbose: + print(f"no legal moves for {current.name} after {moves_played} moves") + return board, moves_played - return brd + move = random.choice(legal) + ok = board.make_move(move, current) + # debug assert + if not ok: + # if an unexpected failure happens, break and treat as draw + if verbose: + print(f"make_move returned False for move {move} by {current}") + return board, moves_played + + moves_played += 1 + if verbose: + print(f"{moves_played:03d}: {current.name} played {move}\nboard:\n{board}") + + # check whether the captured piece was a king + last_move, captured = board.move_history[-1] + if captured is not None and captured.type == PieceType.KING: + if verbose: + print(f"king captured by {current.name} on move {moves_played}.") + return board, moves_played + + # switch side + current = Color.BLACK if current == Color.WHITE else Color.WHITE + + if verbose: + print(f"reached move limit ({max_moves})") + return board, moves_played -# used only for testing purposes +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): + print(f"game {i+1}: moves={moves}\nfinal_board:\n{final_board}") + return + + +# used only for testing purposesa def main(): default_brd = ChessBoard.init_default() @@ -291,24 +356,19 @@ def main(): mvs = default_brd.moves_basic_checked(Color.BLACK) expected_move_count = 20 # 16 pawn moves and 4 knight if len(mvs) != expected_move_count: - raise AssertionError(f"Initial move count for black mismatch: expected {expected_move_count}, got {len(mvs)}") + raise AssertionError(f"initial move count for black mismatch: expected {expected_move_count}, got {len(mvs)}") mvs = default_brd.moves_basic_checked(Color.WHITE) 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)}") - - - # test loading - loaded_brd = ChessBoard.from_string(expected_start) - if str(loaded_brd) != expected_start: - raise AssertionError( - "board loaded from string does not match original representation" - ) + play_random_game(verbose=True) print(f"all tests passed") if __name__ == '__main__': main() + +