176 lines
6.2 KiB
Python
176 lines
6.2 KiB
Python
from enum import Enum, auto
|
|
from dataclasses import dataclass
|
|
from typing import Iterator, Optional, List, Tuple
|
|
import random
|
|
from game_board import ChessBoard, MoveType, Color, PieceType, BoardMove, BoardPos
|
|
|
|
|
|
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()
|
|
|
|
current = Color.WHITE # white moves first
|
|
moves_played = 0
|
|
|
|
while moves_played < max_moves:
|
|
legal = board.generate_moves(current)
|
|
if not legal:
|
|
# no legal moves
|
|
if verbose:
|
|
print(f"no legal moves for {current.name} after {moves_played} moves")
|
|
return board, moves_played
|
|
|
|
# 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:
|
|
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
|
|
_, 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}.")
|
|
raise RuntimeWarning("this should not be possible")
|
|
return board, moves_played
|
|
|
|
current = current.opposite
|
|
|
|
if verbose:
|
|
print(f"reached move limit ({max_moves})")
|
|
return board, moves_played
|
|
|
|
|
|
def run_random_games(n: int = 20, max_moves: int = 200, verbose: bool = False):
|
|
for i in range(n):
|
|
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
|
|
|
|
|
|
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),
|
|
# 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
|
|
|
|
# standardized expected string for initial position
|
|
expected_start = (
|
|
"rnbqkbnr\n"
|
|
"pppppppp\n"
|
|
"........\n"
|
|
"........\n"
|
|
"........\n"
|
|
"........\n"
|
|
"PPPPPPPP\n"
|
|
"RNBQKBNR"
|
|
)
|
|
|
|
# test string representation of board
|
|
actual_start = str(default_brd)
|
|
if actual_start != expected_start:
|
|
raise AssertionError(f"initial board mismatch:\nexpected:\n{expected_start}\n\nactual:\n{actual_start}")
|
|
|
|
# test num moves
|
|
mvs = default_brd.moves_unfiltered(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)}")
|
|
|
|
|
|
mvs = default_brd.moves_unfiltered(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_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")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
|
|
|