added castling + rough testing
This commit is contained in:
+110
-22
@@ -67,8 +67,8 @@ class Piece:
|
|||||||
|
|
||||||
class MoveType(Enum):
|
class MoveType(Enum):
|
||||||
NORMAL = auto()
|
NORMAL = auto()
|
||||||
CASTLING_KINGSIDE = auto() #todo: implement
|
CASTLING_KINGSIDE = auto()
|
||||||
CASTLING_QUEENSIDE = auto() #todo: implement
|
CASTLING_QUEENSIDE = auto()
|
||||||
EN_PASSANT = auto() #todo: implement
|
EN_PASSANT = auto() #todo: implement
|
||||||
PROMOTION = auto() #todo: implement
|
PROMOTION = auto() #todo: implement
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ class MoveType(Enum):
|
|||||||
class BoardMove:
|
class BoardMove:
|
||||||
m_from: BoardPos
|
m_from: BoardPos
|
||||||
m_to: BoardPos
|
m_to: BoardPos
|
||||||
move_type: MoveType = MoveType.NORMAL #todo: implement and handle
|
move_type: MoveType = MoveType.NORMAL
|
||||||
promotion_piece: Optional[PieceType] = None #todo: implement and handle
|
promotion_piece: Optional[PieceType] = None #todo: implement and handle
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@@ -137,24 +137,54 @@ class ChessBoard:
|
|||||||
# attempt to make move for color
|
# attempt to make move for color
|
||||||
# returns true on success
|
# returns true on success
|
||||||
def make_move(self, move: BoardMove, color: Color) -> bool:
|
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]
|
captured = None
|
||||||
|
row_height = 7 if color == Color.WHITE else 0
|
||||||
|
|
||||||
# validate move using legal moves for the player color
|
# validate move using legal moves for the player color
|
||||||
legal = self.moves_basic(color)
|
legal = self.moves_basic(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):
|
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
|
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)
|
||||||
|
|
||||||
# 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.move_history.append((move, captured))
|
||||||
self.num_moves += 1
|
self.num_moves += 1
|
||||||
return True
|
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]:
|
||||||
|
return self.fields[pos.x][pos.y].piece
|
||||||
# returns false if there is no move to unmake
|
# returns false if there is no move to unmake
|
||||||
def unmake_move(self) -> bool:
|
def unmake_move(self) -> bool:
|
||||||
if not self.move_history or len(self.move_history) == 0:
|
if not self.move_history or len(self.move_history) == 0:
|
||||||
@@ -280,6 +310,9 @@ class ChessBoard:
|
|||||||
row_height = 7 if color == Color.WHITE else 0
|
row_height = 7 if color == Color.WHITE else 0
|
||||||
expected_king_pos = BoardPos((row_height, 4))
|
expected_king_pos = BoardPos((row_height, 4))
|
||||||
|
|
||||||
|
king_lhs_dest = BoardPos((row_height, 2))
|
||||||
|
king_rhs_dest = BoardPos((row_height, 6))
|
||||||
|
|
||||||
# if king moved
|
# if king moved
|
||||||
if self.has_piece_moved(expected_king_pos):
|
if self.has_piece_moved(expected_king_pos):
|
||||||
return moves
|
return moves
|
||||||
@@ -287,6 +320,9 @@ class ChessBoard:
|
|||||||
expected_lhs_rook_pos = BoardPos((row_height, 0))
|
expected_lhs_rook_pos = BoardPos((row_height, 0))
|
||||||
expected_rhs_rook_pos = BoardPos((row_height, 7))
|
expected_rhs_rook_pos = BoardPos((row_height, 7))
|
||||||
|
|
||||||
|
lhs_rook_dest = BoardPos((row_height, 3))
|
||||||
|
rhs_rook_dest = BoardPos((row_height, 6))
|
||||||
|
|
||||||
# squares that must be empty between rook and king
|
# 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
|
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
|
rhs_pieces_inbetween = [BoardPos((row_height, i)) for i in range(5, 7)] # cols 5,6
|
||||||
@@ -298,14 +334,14 @@ class ChessBoard:
|
|||||||
if (not self.has_piece_moved(expected_lhs_rook_pos)
|
if (not self.has_piece_moved(expected_lhs_rook_pos)
|
||||||
and self.are_pieces_none(lhs_pieces_inbetween)
|
and self.are_pieces_none(lhs_pieces_inbetween)
|
||||||
and not self.are_fields_attacked(lhs_fields_attacked, color.opposite)):
|
and not self.are_fields_attacked(lhs_fields_attacked, color.opposite)):
|
||||||
# todo: allow castling here
|
moves.append(BoardMove(expected_king_pos, king_lhs_dest, MoveType.CASTLING_QUEENSIDE))
|
||||||
pass
|
moves.append(BoardMove(expected_lhs_rook_pos, lhs_rook_dest, MoveType.CASTLING_QUEENSIDE))
|
||||||
|
|
||||||
if (not self.has_piece_moved(expected_rhs_rook_pos)
|
if (not self.has_piece_moved(expected_rhs_rook_pos)
|
||||||
and self.are_pieces_none(rhs_pieces_inbetween)
|
and self.are_pieces_none(rhs_pieces_inbetween)
|
||||||
and not self.are_fields_attacked(rhs_fields_attacked, color.opposite)):
|
and not self.are_fields_attacked(rhs_fields_attacked, color.opposite)):
|
||||||
# todo: allow castling here
|
moves.append(BoardMove(expected_king_pos, king_rhs_dest, MoveType.CASTLING_KINGSIDE))
|
||||||
pass
|
moves.append(BoardMove(expected_rhs_rook_pos, rhs_rook_dest, MoveType.CASTLING_QUEENSIDE))
|
||||||
return moves
|
return moves
|
||||||
|
|
||||||
def has_piece_moved(self, pos: BoardPos) -> bool:
|
def has_piece_moved(self, pos: BoardPos) -> bool:
|
||||||
@@ -432,7 +468,7 @@ class ChessBoard:
|
|||||||
if move.m_to != king_pos:
|
if move.m_to != king_pos:
|
||||||
us_moves_rule_compliant.append(move)
|
us_moves_rule_compliant.append(move)
|
||||||
|
|
||||||
return us_moves_wo_check
|
return us_moves_rule_compliant
|
||||||
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@@ -470,7 +506,15 @@ def play_random_game(board: Optional[ChessBoard] = None, max_moves: int = 400, v
|
|||||||
print(f"no legal moves for {current.name} after {moves_played} moves")
|
print(f"no legal moves for {current.name} after {moves_played} moves")
|
||||||
return board, moves_played
|
return board, moves_played
|
||||||
|
|
||||||
move = random.choice(legal)
|
# 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)
|
ok = board.make_move(move, current)
|
||||||
# debug assert
|
# debug assert
|
||||||
if not ok:
|
if not ok:
|
||||||
@@ -478,7 +522,9 @@ def play_random_game(board: Optional[ChessBoard] = None, max_moves: int = 400, v
|
|||||||
if verbose:
|
if verbose:
|
||||||
print(f"make_move returned False for move {move} by {current}")
|
print(f"make_move returned False for move {move} by {current}")
|
||||||
return board, moves_played
|
return board, moves_played
|
||||||
|
if move.move_type == MoveType.CASTLING_KINGSIDE or move.move_type == MoveType.CASTLING_QUEENSIDE:
|
||||||
|
print("hell yeah we castled")
|
||||||
|
raise ValueError("casling")
|
||||||
moves_played += 1
|
moves_played += 1
|
||||||
if verbose:
|
if verbose:
|
||||||
print(f"{moves_played:03d}: {current.name} played {move}\nboard:\n{board}")
|
print(f"{moves_played:03d}: {current.name} played {move}\nboard:\n{board}")
|
||||||
@@ -500,13 +546,53 @@ def play_random_game(board: Optional[ChessBoard] = None, max_moves: int = 400, v
|
|||||||
|
|
||||||
def run_random_games(n: int = 100, max_moves: int = 400, verbose: bool = False):
|
def run_random_games(n: int = 100, max_moves: int = 400, verbose: bool = False):
|
||||||
for i in range(n):
|
for i in range(n):
|
||||||
final_board, moves = play_random_game(None, max_moves=max_moves, verbose=verbose and (i < 3))
|
final_board, moves = play_random_game(None, max_moves=max_moves, verbose=verbose)
|
||||||
if verbose and (i < 3):
|
if verbose:
|
||||||
print(f"game {i+1}: moves={moves}\nfinal_board:\n{final_board}")
|
print(f"game {i+1}: moves={moves}\nfinal_board:\n{final_board}")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
# used only for testing purposesa
|
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():
|
def main():
|
||||||
default_brd = ChessBoard.init_default()
|
default_brd = ChessBoard.init_default()
|
||||||
default_brd.generate_moves
|
default_brd.generate_moves
|
||||||
@@ -539,7 +625,9 @@ def main():
|
|||||||
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_castling_kingside_both_sides()
|
||||||
|
run_random_games(verbose=False)
|
||||||
|
|
||||||
print(f"all tests passed")
|
print(f"all tests passed")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user