This repository has been archived on 2026-03-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
cau-praktikum/app/chess_sim/test.py
T
2026-02-26 22:14:50 +01:00

785 lines
29 KiB
Python

from enum import Enum, auto
from dataclasses import dataclass
from typing import Iterator, Optional, List, Tuple
import random
FILES = "abcdefgh"
RANKS = "12345678"
class PieceType(Enum):
PAWN = auto()
KNIGHT = auto()
BISHOP = auto()
ROOK = auto()
QUEEN = auto()
KING = auto()
class Color(Enum):
WHITE = auto()
BLACK = auto()
@property
def opposite(self) -> "Color":
if self == Color.WHITE:
return Color.BLACK
else:
return Color.WHITE
@dataclass
class BoardPos:
p: Tuple[int, int]
def __str__(self) -> str:
return f"({self.p[0]},{self.p[1]})"
@property
def x (self) -> int:
return self.p[0]
@property
def y (self) -> int:
return self.p[1]
@dataclass
class Piece:
type: PieceType
color: Color
# uppercase = White, lowercase = Black
def char(self) -> str:
mapping = {
PieceType.PAWN: "p",
PieceType.KNIGHT: "n",
PieceType.BISHOP: "b",
PieceType.ROOK: "r",
PieceType.QUEEN: "q",
PieceType.KING: "k",
}
ch = mapping[self.type]
return ch.upper() if self.color == Color.WHITE else ch
def __str__(self) -> str:
return self.char()
def __repr__(self) -> str:
return f"Piece({self.type.name},{self.color.name})"
class MoveType(Enum):
NORMAL = auto()
CASTLING_KINGSIDE = auto()
CASTLING_QUEENSIDE = auto()
EN_PASSANT = auto()
PROMOTION = auto() #todo: implement
@dataclass
class BoardMove:
m_from: BoardPos
m_to: BoardPos
move_type: MoveType = MoveType.NORMAL
promotion_piece: Optional[PieceType] = None #todo: implement and handle
def __str__(self) -> str:
return f"{self.m_from}->{self.m_to}"
@dataclass
class BoardField:
piece: Optional[Piece] = None
def __str__(self) -> str:
return str(self.piece) if self.piece is not None else "."
@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)
# initialize default starting position
@classmethod
def init_default(cls) -> "ChessBoard":
brd = cls(
[[BoardField() for _ in range(8)] for _ in range(8)], 0, []
)
# place pawns
for col in range(8):
brd.place(1, col, PieceType.PAWN, Color.BLACK)
brd.place(6, col, PieceType.PAWN, Color.WHITE)
# back rank order
back_rank = [
PieceType.ROOK,
PieceType.KNIGHT,
PieceType.BISHOP,
PieceType.QUEEN,
PieceType.KING,
PieceType.BISHOP,
PieceType.KNIGHT,
PieceType.ROOK,
]
# place black back rank
for col, piece_type in enumerate(back_rank):
brd.place(0, col, piece_type, Color.BLACK)
# place white back rank
for col, piece_type in enumerate(back_rank):
brd.place(7, col, piece_type, Color.WHITE)
return brd
# attempt to make move for color
# returns true on success
def make_move(self, move: BoardMove, color: Color) -> bool:
captured = None
row_height = 7 if color == Color.WHITE else 0
# validate move using legal moves for the player color
legal = self.moves_unfiltered(color)
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)
elif move.move_type == MoveType.EN_PASSANT:
# direction the enemys pawn moved in
en_passant_dir = 1 if color == Color.WHITE else -1
self.move_piece(move.m_from, move.m_to)
affected_pawn_pos = BoardPos((move.m_to.x - en_passant_dir, move.m_to.y))
captured = self.get_field(affected_pawn_pos)
self.fields[affected_pawn_pos.x][affected_pawn_pos.y].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]:
if not self.on_board(pos.x, pos.y):
return None
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:
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
# creates all theoretically possible moves which can be performed by the piece at a position
def moves_unchecked(self, piece: Piece, pos: BoardPos) -> List[List[Tuple[int, int]]]:
row, column = pos.p
rays: List[List[Tuple[int, int]]] = []
if piece.type == PieceType.KING:
king_offsets = [(1, 1), (1, 0), (0, 1), (-1, -1), (-1, 0), (0, -1), (1, -1), (-1, 1)]
rays.append([(row + dr, column + dc) for dr, dc in king_offsets])
elif piece.type == PieceType.KNIGHT:
knight_offsets = [
(2, 1), (2, -1), (-2, 1), (-2, -1),
(1, 2), (1, -2), (-1, 2), (-1, -2)
]
rays.append([(row + dr, column + dc) for dr, dc in knight_offsets])
elif piece.type == PieceType.PAWN:
# correct forward direction and start row depending on color
if piece.color == Color.WHITE:
forward = -1
start_row = 6
else:
forward = 1
start_row = 1
forward_ray: List[Tuple[int, int]] = []
forward_ray.append((row + forward, column))
# if the pawn is at the start row, include the double-step
if row == start_row:
forward_ray.append((row + 2 * forward, column))
rays.append(forward_ray)
# capture moves
captures = [(row + forward, column - 1), (row + forward, column + 1)]
rays.extend([[(rr, cc)] for rr, cc in captures])
elif piece.type == PieceType.BISHOP:
directions = [(1, 1), (1, -1), (-1, 1), (-1, -1)]
for dr, dc in directions:
ray = []
for k in range(1, 8):
ray.append((row + k * dr, column + k * dc))
rays.append(ray)
elif piece.type == PieceType.ROOK:
directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
for dr, dc in directions:
ray = []
for k in range(1, 8):
ray.append((row + k * dr, column + k * dc))
rays.append(ray)
elif piece.type == PieceType.QUEEN:
directions = [
(1, 0), (-1, 0), (0, 1), (0, -1),
(1, 1), (1, -1), (-1, 1), (-1, -1)
]
for dr, dc in directions:
ray = []
for k in range(1, 8):
ray.append((row + k * dr, column + k * dc))
rays.append(ray)
return rays
def on_board(self, rr: int, cc: int) -> bool:
return 0 <= rr < 8 and 0 <= cc < 8
# takes the color of the player whos possible moves will be returned
def moves_unfiltered(self, color: Color) -> List[BoardMove]:
moves: List[BoardMove] = []
for pos, piece in self.iter_pieces():
if piece.color != color:
continue
rays = self.moves_unchecked(piece, pos)
for ray in rays:
# detect pawn capture ray and dont allow movement in that ray without capturing
is_pawn_capture_ray = (
piece.type == PieceType.PAWN
and len(ray) == 1
and ray[0][1] != pos.y
and abs(ray[0][0] - pos.x) == 1
)
# non sliding rays get treated individually
non_sliding = piece.type in (PieceType.KNIGHT, PieceType.KING) or is_pawn_capture_ray
for (tr, tc) in ray:
if not self.on_board(tr, tc):
if non_sliding:
continue
break
target_field = self.fields[tr][tc]
if target_field.piece is None:
# empty target
if is_pawn_capture_ray:
move_history_len = len(self.move_history)
# pawn can only move diagonally into an empty square during en passant, check for that
en_passant_dir = 1 if color == Color.WHITE else -1
if move_history_len > 0:
# check if there is a pawn in the correct position
if self.get_field(BoardPos((tr, tc - en_passant_dir))) == Piece(
PieceType.PAWN,
color.opposite,
):
# check if the last move moved the pawn there from the starting square
last_move = self.move_history[move_history_len][0]
if last_move.m_from == BoardPos((tr, tc + en_passant_dir)) and last_move.m_to == BoardPos((tr, tc - en_passant_dir)):
moves.append(BoardMove(pos, BoardPos((tr, tc))))
break
moves.append(BoardMove(pos, BoardPos((tr, tc))))
continue
else:
# occupied
if target_field.piece.color == piece.color:
break
else:
# opponent piece
moves.append(BoardMove(pos, BoardPos((tr, tc))))
break
# manually adding castling
row_height = 7 if color == Color.WHITE else 0
expected_king_pos = BoardPos((row_height, 4))
if self.has_piece_moved(expected_king_pos):
return moves
expected_lhs_rook_pos = BoardPos((row_height, 0))
expected_rhs_rook_pos = BoardPos((row_height, 7))
# 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
# squares that must not be attacked by the opponent
lhs_fields_attacked = [BoardPos((row_height, i)) for i in (4, 3, 2)]
rhs_fields_attacked = [BoardPos((row_height, i)) for i in (4, 5, 6)]
# queenside
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)):
king_lhs_dest = BoardPos((row_height, 2))
moves.append(BoardMove(expected_king_pos, king_lhs_dest, MoveType.CASTLING_QUEENSIDE))
# kingside
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)):
king_rhs_dest = BoardPos((row_height, 6))
moves.append(BoardMove(expected_king_pos, king_rhs_dest, MoveType.CASTLING_KINGSIDE))
return moves
def has_piece_moved(self, pos: BoardPos) -> bool:
for move in self.move_history:
if move[0].m_from == pos:
return True
elif move[0].m_to == pos:
return True
return False
def are_pieces_none(self, positions: List[BoardPos]):
are_gone = True
for pos in positions:
if not self.is_piece_none(pos):
are_gone = False
return are_gone
def is_piece_none(self, pos: BoardPos) -> bool:
piece = self.fields[pos.x][pos.y].piece
if piece is None:
return True
else:
return False
def is_piece_of_type(self, pos: BoardPos, piece_type: PieceType) -> bool:
piece = self.fields[pos.x][pos.y].piece
if piece is None:
return False
return piece.type == piece_type
def iter_pieces(self) -> Iterator[Tuple[BoardPos, Piece]]:
for row_idx, row in enumerate(self.fields):
for col_idx, field in enumerate(row):
if field.piece is not None:
yield BoardPos((row_idx, col_idx)), field.piece
def pos_of_king(self, color: Color) -> BoardPos:
for pos, piece in self.iter_pieces():
if piece.type == PieceType.KING and piece.color == color:
return pos
raise ValueError("player has no king")
# returns true if one field is actively being attacked
def are_fields_attacked(self, fields: List[BoardPos], attacker_color: Color) -> bool:
for field in fields:
if self.is_field_attacked(field, attacker_color):
return True
return False
# checks if the field is being actively attacked by any of the pieces of the attacker color
# this "complicated" implementation was necessary to not rely on recursive calls
def is_field_attacked(self, pos: BoardPos, attacker_color: Color) -> bool:
# iterate opponent pieces and check whether they attack
for attacker_pos, attacker in self.iter_pieces():
if attacker.color != attacker_color:
continue
rays = self.moves_unchecked(attacker, attacker_pos)
if attacker.type == PieceType.PAWN:
# pawn attacks are the pawn capture rays only
forward = -1 if attacker.color == Color.WHITE else 1
for dc in (-1, 1):
rr = attacker_pos.x + forward
cc = attacker_pos.y + dc
if not self.on_board(rr, cc):
continue
if BoardPos((rr, cc)) == pos:
return True
continue
if attacker.type in (PieceType.KNIGHT, PieceType.KING):
# non-sliding
for (rr, cc) in rays[0]:
if not self.on_board(rr, cc):
continue
if BoardPos((rr, cc)) == pos:
return True
continue
# sliding pieces
for ray in rays:
for (rr, cc) in ray:
if not self.on_board(rr, cc):
break
if BoardPos((rr, cc)) == pos:
return True
# stop at first occupied square (blocker)
if self.fields[rr][cc].piece is not None:
break
return False
# if a player does not have any moves, it has lost
def generate_moves(self, color: Color) -> List[BoardMove]:
# only moves after which the king is not in check are allowed
# todo: the current method of checking what move will resolve the check is based on bruteforcing
# maybe there is a better way of doing it, but for now this should suffice
us_moves_wo_check = []
for move in self.moves_unfiltered(color):
# do a move and then check, if the king is still in check
# if it isnt, add the move to the possibles ones
if not self.make_move(move, color):
raise ValueError(f"self.moves_basic created a move, which cannot be done (hopefully unreachable). move: {move}")
all_basic_enemy_moves = self.moves_unfiltered(color.opposite)
king_pos = self.pos_of_king(color)
king_in_check = False
for mv in all_basic_enemy_moves:
if mv.m_to == king_pos:
king_in_check = True
if not king_in_check:
us_moves_wo_check.append(move)
if not self.unmake_move():
raise ValueError("failed to unmake move, shouldnt be an issue here")
# check if our move lands directly on the enemies king, if it does, its illigal
us_moves_rule_compliant = []
king_pos = self.pos_of_king(color.opposite)
for move in us_moves_wo_check:
if move.m_to != king_pos:
us_moves_rule_compliant.append(move)
return us_moves_rule_compliant
def __str__(self) -> str:
"""
example string repr of starting pos:
rnbqkbnr
pppppppp
........
........
........
........
PPPPPPPP
RNBQKBNR
"""
lines: List[str] = []
for r in range(8):
row_chars = [str(self.fields[r][c]) for c in range(8)]
lines.append("".join(row_chars))
return "\n".join(lines)
# https://en.wikipedia.org/wiki/Algebraic_notation_(chess)
@staticmethod
def pos_to_algebraic(pos: BoardPos) -> str:
file = FILES[pos.y]
rank = RANKS[7 - pos.x]
return file + rank
@staticmethod
def algebraic_to_pos(s: str) -> BoardPos:
if len(s) != 2:
raise ValueError("invalid algebraic square")
file_char = s[0].lower()
rank_char = s[1]
if file_char not in FILES or rank_char not in RANKS:
raise ValueError("invalid algebraic square")
col = FILES.index(file_char)
row = 7 - RANKS.index(rank_char)
return BoardPos((row, col))
# https://www.chess.com/de/terms/forsyth-edwards-notation-fen
def to_fen(self) -> str:
# piece placement
ranks: List[str] = []
for r in range(8):
empty = 0
row_str = ""
for c in range(8):
p = self.fields[r][c].piece
if p is None:
empty += 1
else:
if empty > 0:
row_str += str(empty)
empty = 0
row_str += p.char()
if empty > 0:
row_str += str(empty)
ranks.append(row_str)
placement = "/".join(ranks)
active = 'w' if (self.num_moves % 2) == 0 else 'b'
# castling availability
castling = ''
# white K/Q
if (self.fields[7][4].piece is not None and self.fields[7][4].piece.type == PieceType.KING and self.fields[7][4].piece.color == Color.WHITE
and self.fields[7][7].piece is not None and self.fields[7][7].piece.type == PieceType.ROOK and self.fields[7][7].piece.color == Color.WHITE
and not self.has_piece_moved(BoardPos((7,4))) and not self.has_piece_moved(BoardPos((7,7)))):
castling += 'K'
if (self.fields[7][4].piece is not None and self.fields[7][4].piece.type == PieceType.KING and self.fields[7][4].piece.color == Color.WHITE
and self.fields[7][0].piece is not None and self.fields[7][0].piece.type == PieceType.ROOK and self.fields[7][0].piece.color == Color.WHITE
and not self.has_piece_moved(BoardPos((7,4))) and not self.has_piece_moved(BoardPos((7,0)))):
castling += 'Q'
# black k/q
if (self.fields[0][4].piece is not None and self.fields[0][4].piece.type == PieceType.KING and self.fields[0][4].piece.color == Color.BLACK
and self.fields[0][7].piece is not None and self.fields[0][7].piece.type == PieceType.ROOK and self.fields[0][7].piece.color == Color.BLACK
and not self.has_piece_moved(BoardPos((0,4))) and not self.has_piece_moved(BoardPos((0,7)))):
castling += 'k'
if (self.fields[0][4].piece is not None and self.fields[0][4].piece.type == PieceType.KING and self.fields[0][4].piece.color == Color.BLACK
and self.fields[0][0].piece is not None and self.fields[0][0].piece.type == PieceType.ROOK and self.fields[0][0].piece.color == Color.BLACK
and not self.has_piece_moved(BoardPos((0,4))) and not self.has_piece_moved(BoardPos((0,0)))):
castling += 'q'
if castling == '':
castling = '-'
# en passant target square: if last move was a pawn double-step, set the square behind it
ep = '-'
if len(self.move_history) > 0:
last_move = self.move_history[-1][0]
moved_piece = self.get_field(last_move.m_to)
if moved_piece is not None and moved_piece.type == PieceType.PAWN and abs(last_move.m_from.x - last_move.m_to.x) == 2:
passed_row = (last_move.m_from.x + last_move.m_to.x) // 2
passed_col = last_move.m_from.y
ep = self.pos_to_algebraic(BoardPos((passed_row, passed_col)))
# todo half moves not yet tracked
halfmove = 0
# fullmove number
fullmove = (self.num_moves // 2) + 1
return f"{placement} {active} {castling} {ep} {halfmove} {fullmove}"
# https://backscattering.de/chess/uci/
def make_move_uci(self, uci: str, color: Color) -> bool:
if uci == '0000':
return False
if len(uci) < 4:
return False
src = self.algebraic_to_pos(uci[0:2])
dst = self.algebraic_to_pos(uci[2:4])
promo_piece = None
if len(uci) == 5:
pc = uci[4].lower()
mapping = {'q': PieceType.QUEEN, 'r': PieceType.ROOK, 'b': PieceType.BISHOP, 'n': PieceType.KNIGHT}
promo_piece = mapping.get(pc, None)
moving = self.get_field(src)
if moving is None or moving.color != color:
return False
# detect castling by king moving two files
move_type = MoveType.NORMAL
if moving.type == PieceType.KING and abs(src.y - dst.y) == 2:
move_type = MoveType.CASTLING_KINGSIDE if dst.y > src.y else MoveType.CASTLING_QUEENSIDE
else:
# en passant detection: pawn moves diagonally to empty square
if moving.type == PieceType.PAWN and src.y != dst.y and self.get_field(dst) is None:
if len(self.move_history) > 0:
# make sure the last move was a pawn move for this en passant to be possible
last = self.move_history[-1][0]
if last.m_from.x - last.m_to.x == 2 and last.m_to.x == src.x and last.m_to.y == dst.y:
move_type = MoveType.EN_PASSANT
# promotion
if promo_piece is not None:
move_type = MoveType.PROMOTION
bm = BoardMove(src, dst, move_type, promotion_piece=promo_piece)
return self.make_move(bm, color)
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()
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)
print(f"all tests passed")
if __name__ == '__main__':
main()