improved testing

This commit is contained in:
simoncreates
2026-02-24 19:22:13 +01:00
parent ff3e99382e
commit 83119a2443
+97 -37
View File
@@ -1,7 +1,7 @@
from enum import Enum, auto from enum import Enum, auto
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional, List, Tuple from typing import Optional, List, Tuple
import random
class PieceType(Enum): class PieceType(Enum):
PAWN = auto() PAWN = auto()
@@ -30,8 +30,9 @@ class Piece:
type: PieceType type: PieceType
color: Color color: Color
# uppercase = White, lowercase = Black
def char(self) -> str: def char(self) -> str:
"""Single-character representation. Uppercase = White, lowercase = Black."""
mapping = { mapping = {
PieceType.PAWN: "p", PieceType.PAWN: "p",
PieceType.KNIGHT: "n", PieceType.KNIGHT: "n",
@@ -70,6 +71,8 @@ class BoardField:
@dataclass @dataclass
class ChessBoard: class ChessBoard:
fields: List[List[BoardField]] 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): def place(self, row: int, col: int, piece_type: PieceType, color: Color):
self.fields[row][col].piece = Piece(piece_type, color) self.fields[row][col].piece = Piece(piece_type, color)
@@ -78,7 +81,7 @@ class ChessBoard:
@classmethod @classmethod
def init_default(cls) -> "ChessBoard": def init_default(cls) -> "ChessBoard":
brd = cls( brd = cls(
[[BoardField() for _ in range(8)] for _ in range(8)] [[BoardField() for _ in range(8)] for _ in range(8)], 0, []
) )
# place pawns # place pawns
@@ -108,6 +111,43 @@ class ChessBoard:
return brd 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]]]: def moves_unchecked(self, piece: Piece, pos: BoardPos) -> List[List[Tuple[int, int]]]:
row, column = pos.p row, column = pos.p
rays: List[List[Tuple[int, int]]] = [] rays: List[List[Tuple[int, int]]] = []
@@ -221,7 +261,8 @@ class ChessBoard:
def __str__(self) -> str: def __str__(self) -> str:
""" """
example string repr of starting pos example string repr of starting pos:
rnbqkbnr rnbqkbnr
pppppppp pppppppp
........ ........
@@ -237,36 +278,60 @@ class ChessBoard:
lines.append("".join(row_chars)) lines.append("".join(row_chars))
return "\n".join(lines) 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 = { current = Color.WHITE # white moves first
'p': PieceType.PAWN, moves_played = 0
'n': PieceType.KNIGHT,
'b': PieceType.BISHOP,
'r': PieceType.ROOK,
'q': PieceType.QUEEN,
'k': PieceType.KING,
}
for r, line in enumerate(lines): while moves_played < max_moves:
for c, ch in enumerate(line): legal = board.moves_basic_checked(current)
if ch == '.': if not legal:
continue # no legal moves
lower = ch.lower() winner = Color.BLACK if current == Color.WHITE else Color.WHITE
if lower not in char_to_piece: if verbose:
raise ValueError(f"Invalid piece character '{ch}' at ({r},{c})") print(f"no legal moves for {current.name} after {moves_played} moves")
piece_type = char_to_piece[lower] return board, moves_played
color = Color.WHITE if ch.isupper() else Color.BLACK
brd.place(r, c, piece_type, color)
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(): def main():
default_brd = ChessBoard.init_default() default_brd = ChessBoard.init_default()
@@ -291,24 +356,19 @@ def main():
mvs = default_brd.moves_basic_checked(Color.BLACK) mvs = default_brd.moves_basic_checked(Color.BLACK)
expected_move_count = 20 # 16 pawn moves and 4 knight expected_move_count = 20 # 16 pawn moves and 4 knight
if len(mvs) != expected_move_count: 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) mvs = default_brd.moves_basic_checked(Color.WHITE)
expected_move_count = 20 # 16 pawn moves and 4 knight expected_move_count = 20 # 16 pawn moves and 4 knight
if len(mvs) != expected_move_count: if len(mvs) != expected_move_count:
raise AssertionError(f"Initial move count for white mismatch: expected {expected_move_count}, got {len(mvs)}") raise AssertionError(f"Initial move count for white mismatch: expected {expected_move_count}, got {len(mvs)}")
play_random_game(verbose=True)
# 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"
)
print(f"all tests passed") print(f"all tests passed")
if __name__ == '__main__': if __name__ == '__main__':
main() main()