improved testing
This commit is contained in:
+97
-37
@@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user