Source code for chipiron.environments.chess_env.board.board_chi

"""
Module that contains the BoardChi class that wraps the chess.Board class from the chess package
"""

import typing
from typing import Iterator

import chess
import chess.polyglot
from chess import Outcome, _BoardState

from chipiron.environments.chess_env.board.board_modification import (
    BoardModification,
    BoardModificationP,
    PieceInSquare,
    compute_modifications,
)
from chipiron.environments.chess_env.move import moveUci
from chipiron.environments.chess_env.move.imove import moveKey
from chipiron.utils.logger import chipiron_logger

from .iboard import IBoard, LegalMoveKeyGeneratorP, boardKey, compute_key
from .utils import FenPlusHistory, fen

# todo check if we need this here
COLORS = [WHITE, BLACK] = [True, False]


[docs]class LegalMoveKeyGenerator(LegalMoveKeyGeneratorP): generated_moves: dict[moveKey, chess.Move] all_generated_keys: list[moveKey] | None # whether to sort the legal_moves by their respective uci for easy comparison of various implementations sort_legal_moves: bool = False chess_board: chess.Board def __init__(self, chess_board: chess.Board, sort_legal_moves: bool) -> None: self.chess_board = chess_board self.it: Iterator[chess.Move] = self.chess_board.generate_legal_moves() self.generated_moves = {} self.sort_legal_moves = sort_legal_moves self.count = 0 self.all_generated_keys = None @property def fen(self) -> fen: return self.chess_board.fen() def __str__(self) -> str: the_string: str = "Legals Moves: " ucis: list[moveUci] = [ chess_move.uci() for move_key, chess_move in self.generated_moves.items() ] keys: list[moveKey] = [ move_key for move_key, chess_move in self.generated_moves.items() ] the_string = the_string + f"Generated ucis {ucis}" the_string = the_string + f" and generated ucis {keys}" return the_string def __iter__(self) -> Iterator[moveKey]: self.it = self.chess_board.generate_legal_moves() self.count = 0 return self def __next__(self) -> moveKey: new_move: chess.Move = self.it.__next__() # move_key_ = new_move.uci() move_key_ = self.count self.generated_moves[move_key_] = new_move self.count += 1 return move_key_
[docs] def copy( self, copied_chess_board: chess.Board | None = None ) -> "LegalMoveKeyGenerator": if copied_chess_board is None: copied_chess_board_ = self.chess_board else: copied_chess_board_ = copied_chess_board legal_move_copy = LegalMoveKeyGenerator( chess_board=copied_chess_board_, sort_legal_moves=self.sort_legal_moves ) legal_move_copy.generated_moves = self.generated_moves.copy() if self.all_generated_keys is not None: legal_move_copy.all_generated_keys = self.all_generated_keys.copy() else: legal_move_copy.all_generated_keys = None legal_move_copy.count = self.count return legal_move_copy
[docs] def reset(self) -> None: self.it = self.chess_board.generate_legal_moves() self.generated_moves = {} self.count = 0 self.all_generated_keys = None
[docs] def copy_with_reset(self) -> "LegalMoveKeyGenerator": legal_move_copy = LegalMoveKeyGenerator( chess_board=self.chess_board, sort_legal_moves=self.sort_legal_moves ) return legal_move_copy
[docs] def get_all(self) -> list[moveKey]: if self.all_generated_keys is None: list_keys: list[moveKey] if self.sort_legal_moves: list_keys = sorted( list(self), key=lambda i: self.generated_moves[i].uci() ) else: list_keys = list(self) self.all_generated_keys = list_keys return list_keys else: return self.all_generated_keys
[docs] def more_than_one_move(self) -> bool: # assume legal_moves not empty iter_move: Iterator[chess.Move] = self.chess_board.generate_legal_moves() # remove the first element next(iter_move) return any(iter_move)
[docs]class BoardChi(IBoard): """ Board Chipiron object that describes the current board. it wraps the chess Board from the chess package so it can have more in it """ chess_board: chess.Board compute_board_modification: bool legal_moves_: LegalMoveKeyGenerator fast_representation_: boardKey # whether to sort the legal_moves by their respective uci for easy comparison of various implementations sort_legal_moves: bool def __init__( self, chess_board: chess.Board, compute_board_modification: bool, fast_representation_: boardKey, legal_moves_: LegalMoveKeyGenerator, ) -> None: """ Initializes a new instance of the BoardChi class. Args: board: The chess.Board object to wrap. """ self.chess_board = chess_board self.compute_board_modification = compute_board_modification self.fast_representation_ = fast_representation_ self.legal_moves_ = legal_moves_
[docs] def play_mon(self, move: chess.Move) -> None: self.chess_board.push(move)
[docs] def play_move( self, move: chess.Move, use_compute_modification_function: bool = False ) -> BoardModificationP | None: """ Plays a move on the board and returns the board modification. Args: move: The move to play. Returns: The board modification resulting from the move or None. """ # todo: illegal moves seem accepted, do we care? if we dont write it in the doc # assert self.chess_board.is_legal(move) # board_modifications: BoardModificationP | None = None if self.compute_board_modification: if not use_compute_modification_function: board_modifications = self.push_and_return_modification(move) # type: ignore else: previous_pawns = self.chess_board.pawns previous_kings = self.chess_board.kings previous_queens = self.chess_board.queens previous_rooks = self.chess_board.rooks previous_bishops = self.chess_board.bishops previous_knights = self.chess_board.knights previous_occupied_white = self.chess_board.occupied_co[chess.WHITE] previous_occupied_black = self.chess_board.occupied_co[chess.BLACK] self.play_mon(move) # self.play_mon(move) new_pawns = self.chess_board.pawns new_kings = self.chess_board.kings new_queens = self.chess_board.queens new_rooks = self.chess_board.rooks new_bishops = self.chess_board.bishops new_knights = self.chess_board.knights new_occupied_white = self.chess_board.occupied_co[chess.WHITE] new_occupied_black = self.chess_board.occupied_co[chess.BLACK] board_modifications = compute_modifications( previous_bishops=previous_bishops, previous_pawns=previous_pawns, previous_kings=previous_kings, previous_knights=previous_knights, previous_queens=previous_queens, previous_occupied_white=previous_occupied_white, previous_rooks=previous_rooks, previous_occupied_black=previous_occupied_black, new_kings=new_kings, new_bishops=new_bishops, new_pawns=new_pawns, new_queens=new_queens, new_rooks=new_rooks, new_knights=new_knights, new_occupied_black=new_occupied_black, new_occupied_white=new_occupied_white, ) else: self.chess_board.push(move) # update after move self.legal_moves_ = ( self.legal_moves_.copy_with_reset() ) # the legals moves needs to be recomputed as the board has changed fast_representation: boardKey = compute_key( pawns=self.chess_board.pawns, knights=self.chess_board.knights, bishops=self.chess_board.bishops, rooks=self.chess_board.rooks, queens=self.chess_board.queens, kings=self.chess_board.kings, turn=self.chess_board.turn, castling_rights=self.chess_board.castling_rights, ep_square=self.chess_board.ep_square, white=self.chess_board.occupied_co[chess.WHITE], black=self.chess_board.occupied_co[chess.BLACK], promoted=self.chess_board.promoted, fullmove_number=self.chess_board.fullmove_number, halfmove_clock=self.chess_board.halfmove_clock, ) self.fast_representation_ = fast_representation return board_modifications
[docs] def play_move_uci(self, move_uci: moveUci) -> BoardModificationP | None: chess_move: chess.Move = chess.Move.from_uci(uci=move_uci) return self.play_move(move=chess_move)
# todo look like this function might move to iboard when the dust settle
[docs] def play_move_key(self, move: moveKey) -> BoardModificationP | None: # chess_move: chess.Move = chess.Move.from_uci(uci=move) # if True: # if self.legal_moves_ is not None and move in self.legal_moves_.generated_moves: chess_move: chess.Move = self.legal_moves_.generated_moves[move] board_modification = self.play_move(move=chess_move) return board_modification
[docs] def rewind_one_move(self) -> None: """ Rewinds the board state to the previous move. """ if self.ply() > 0: self.chess_board.pop() else: chipiron_logger.warning( f"Cannot rewind more as self.halfmove_clock equals {self.ply()}" )
[docs] @typing.no_type_check def push_and_return_modification( self, move: chess.Move ) -> BoardModification | None: """ Mostly reuse the push function of the chess library but records the modifications to the bitboard so that we can do the same with other parallel representations such as tensor in pytorch Args: move: The move to push. Returns: The board modification resulting from the move, or None if the move is a null move. """ board_modifications = BoardModification() # Push move and remember board state move = self.chess_board._to_chess960(move) board_state = _BoardState(self.chess_board) self.chess_board.castling_rights = ( self.chess_board.clean_castling_rights() ) # Before pushing stack self.chess_board.move_stack.append( self.chess_board._from_chess960( self.chess_board.chess960, move.from_square, move.to_square, move.promotion, move.drop, ) ) self.chess_board._stack.append(board_state) # Reset en passant square. ep_square = self.chess_board.ep_square self.chess_board.ep_square = None # Increment move counters. self.chess_board.halfmove_clock += 1 if self.chess_board.turn == BLACK: self.chess_board.fullmove_number += 1 # On a null move, simply swap turns and reset the en passant square. if not move: self.chess_board.turn = not self.chess_board.turn return # Drops. if move.drop: self.chess_board._set_piece_at( move.to_square, move.drop, self.chess_board.turn ) self.chess_board.turn = not self.turn return # Zero the half-move clock. if self.chess_board.is_zeroing(move): self.chess_board.halfmove_clock = 0 from_bb = chess.BB_SQUARES[move.from_square] to_bb = chess.BB_SQUARES[move.to_square] promoted = bool(self.chess_board.promoted & from_bb) piece_type = self.chess_board._remove_piece_at(move.from_square) # start added piece_in_square: PieceInSquare = PieceInSquare( square=move.from_square, piece=piece_type, color=self.chess_board.turn ) board_modifications.add_removal(removal=piece_in_square) # end added assert ( piece_type is not None ), f"push() expects move to be pseudo-legal, but got {move} in {self.chess_board.board_fen()}" capture_square = move.to_square captured_piece_type = self.chess_board.piece_type_at(capture_square) # start added castling = ( piece_type == chess.KING and self.chess_board.occupied_co[self.turn] & to_bb ) if captured_piece_type is not None and not castling: captured_piece_in_square: PieceInSquare = PieceInSquare( square=capture_square, piece=captured_piece_type, color=not self.chess_board.turn, ) board_modifications.add_removal(removal=captured_piece_in_square) # end added # Update castling rights. self.chess_board.castling_rights &= ~to_bb & ~from_bb if piece_type == chess.KING and not promoted: if self.turn == WHITE: self.chess_board.castling_rights &= ~chess.BB_RANK_1 else: self.chess_board.castling_rights &= ~chess.BB_RANK_8 elif ( captured_piece_type == chess.KING and not self.chess_board.promoted & to_bb ): if self.turn == WHITE and chess.square_rank(move.to_square) == 7: self.chess_board.castling_rights &= ~chess.BB_RANK_8 elif self.turn == BLACK and chess.square_rank(move.to_square) == 0: self.chess_board.castling_rights &= ~chess.BB_RANK_1 # Handle special pawn moves. if piece_type == chess.PAWN: diff = move.to_square - move.from_square if diff == 16 and chess.square_rank(move.from_square) == 1: self.chess_board.ep_square = move.from_square + 8 elif diff == -16 and chess.square_rank(move.from_square) == 6: self.chess_board.ep_square = move.from_square - 8 elif ( move.to_square == ep_square and abs(diff) in [7, 9] and not captured_piece_type ): # Remove pawns captured en passant. down = -8 if self.turn == WHITE else 8 capture_square = ep_square + down captured_piece_type = self.chess_board._remove_piece_at(capture_square) # start added pawn_captured_piece_in_square: PieceInSquare = PieceInSquare( square=capture_square, piece=captured_piece_type, color=not self.chess_board.turn, ) board_modifications.add_removal(removal=pawn_captured_piece_in_square) # end added # Promotion. if move.promotion: promoted = True piece_type = move.promotion # Castling. castling = ( piece_type == chess.KING and self.chess_board.occupied_co[self.turn] & to_bb ) if castling: a_side = chess.square_file(move.to_square) < chess.square_file( move.from_square ) self.chess_board._remove_piece_at(move.from_square) self.chess_board._remove_piece_at(move.to_square) remove_rook_in_square: PieceInSquare = PieceInSquare( square=move.to_square, piece=chess.ROOK, color=self.chess_board.turn ) board_modifications.add_removal(removal=remove_rook_in_square) # start added if a_side: king_square = ( chess.C1 if self.chess_board.turn == chess.WHITE else chess.C8 ) rook_square = chess.D1 if self.chess_board.turn == WHITE else chess.D8 self.chess_board._set_piece_at( chess.C1 if self.turn == WHITE else chess.C8, chess.KING, self.turn ) self.chess_board._set_piece_at( chess.D1 if self.turn == WHITE else chess.D8, chess.ROOK, self.turn ) else: king_square = ( chess.G1 if self.chess_board.turn == chess.WHITE else chess.G8 ) rook_square = chess.F1 if self.chess_board.turn == WHITE else chess.F8 self.chess_board._set_piece_at( chess.G1 if self.turn == WHITE else chess.G8, chess.KING, self.turn ) self.chess_board._set_piece_at( chess.F1 if self.turn == WHITE else chess.F8, chess.ROOK, self.turn ) # start added king_in_square: PieceInSquare = PieceInSquare( square=king_square, piece=chess.KING, color=self.chess_board.turn ) board_modifications.add_appearance(appearance=king_in_square) rook_in_square: PieceInSquare = PieceInSquare( square=rook_square, piece=chess.ROOK, color=self.chess_board.turn ) board_modifications.add_appearance(appearance=rook_in_square) # end added # Put the piece on the target square. if not castling: was_promoted = bool(self.chess_board.promoted & to_bb) self.chess_board._set_piece_at( move.to_square, piece_type, self.chess_board.turn, promoted ) if captured_piece_type: self.chess_board._push_capture( move, capture_square, captured_piece_type, was_promoted ) promote_piece_in_square: PieceInSquare = PieceInSquare( square=move.to_square, piece=piece_type, color=self.chess_board.turn ) board_modifications.add_appearance(appearance=promote_piece_in_square) # Swap turn. self.chess_board.turn = not self.chess_board.turn return board_modifications
[docs] def compute_key_old(self) -> str: """ Computes and returns a unique key representing the current state of the chess board. The key is computed by concatenating various attributes of the board, including the positions of pawns, knights, bishops, rooks, queens, and kings, as well as the current turn, castling rights, en passant square, halfmove clock, occupied squares for each color, promoted pieces, and the fullmove number. Returns: str: A unique key representing the current state of the chess board. """ string = ( str(self.chess_board.pawns) + str(self.chess_board.knights) + str(self.chess_board.bishops) + str(self.chess_board.rooks) + str(self.chess_board.queens) + str(self.chess_board.kings) + str(self.chess_board.turn) + str(self.chess_board.castling_rights) + str(self.chess_board.ep_square) + str(self.chess_board.halfmove_clock) + str(self.chess_board.occupied_co[WHITE]) + str(self.chess_board.occupied_co[BLACK]) + str(self.chess_board.promoted) + str(self.chess_board.fullmove_number) ) return string
[docs] def print_chess_board(self) -> str: """ Prints the current state of the chess board. This method prints the current state of the chess board, including the position of all the pieces. It also prints the FEN (Forsyth–Edwards Notation) representation of the board. Returns: None """ return str(self.chess_board.fen)
[docs] def number_of_pieces_on_the_board(self) -> int: """ Returns the number of pieces currently on the board. Returns: int: The number of pieces on the board. """ return self.chess_board.occupied.bit_count()
[docs] def is_attacked(self, a_color: chess.Color) -> bool: """Check if any piece of the color `a_color` is attacked. Args: a_color (chess.Color): The color of the pieces to check. Returns: bool: True if any piece of the specified color is attacked, False otherwise. """ all_squares_of_color = chess.SquareSet() for piece_type in [1, 2, 3, 4, 5, 6]: new_squares = self.chess_board.pieces(piece_type=piece_type, color=a_color) all_squares_of_color = all_squares_of_color.union(new_squares) all_attackers = chess.SquareSet() for square in all_squares_of_color: new_attackers = self.chess_board.attackers(not a_color, square) all_attackers = all_attackers.union(new_attackers) return bool(all_attackers)
[docs] def is_game_over(self) -> bool: """ Check if the game is over. Returns: bool: True if the game is over, False otherwise. """ # assume that player claim draw otherwise the opponent might be overoptimistic # in winning position where draw by repetition occur claim_draw: bool = True if len(self.chess_board.move_stack) >= 4 else False is_game_over: bool = self.chess_board.is_game_over(claim_draw=claim_draw) return is_game_over
[docs] def ply(self) -> int: """ Returns the number of half-moves (plies) that have been played on the board. :return: The number of half-moves played on the board. :rtype: int """ return self.chess_board.ply()
@property def turn(self) -> chess.Color: """ Get the current turn color. Returns: chess.Color: The color of the current turn. """ return self.chess_board.turn @property def fen(self) -> fen: """ Returns the Forsyth-Edwards Notation (FEN) representation of the chess board. :return: The FEN string representing the current state of the board. """ return self.chess_board.fen() @property def legal_moves(self) -> LegalMoveKeyGenerator: """ Returns a generator that yields all the legal moves for the current board state. Returns: chess.LegalMoveGenerator: A generator that yields legal moves. """ # return self.chess_board.legal_moves return self.legal_moves_
[docs] def piece_at(self, square: chess.Square) -> chess.Piece | None: """ Returns the piece at the specified square on the chess board. Args: square (chess.Square): The square on the chess board. Returns: chess.Piece | None: The piece at the specified square, or None if there is no piece. """ return self.chess_board.piece_at(square)
[docs] def piece_map( self, mask: chess.Bitboard = chess.BB_ALL ) -> dict[chess.Square, tuple[int, bool]]: result = {} for square in chess.scan_reversed(self.chess_board.occupied & mask): piece_type: int | None = self.chess_board.piece_type_at(square) assert piece_type is not None mask = chess.BB_SQUARES[square] color = bool(self.chess_board.occupied_co[WHITE] & mask) result[square] = (piece_type, color) return result
[docs] def has_castling_rights(self, color: chess.Color) -> bool: """ Check if the specified color has castling rights. Args: color (chess.Color): The color to check for castling rights. Returns: bool: True if the color has castling rights, False otherwise. """ return self.chess_board.has_castling_rights(color)
[docs] def has_queenside_castling_rights(self, color: chess.Color) -> bool: """ Check if the specified color has queenside castling rights. Args: color (chess.Color): The color to check for queenside castling rights. Returns: bool: True if the specified color has queenside castling rights, False otherwise. """ return self.chess_board.has_queenside_castling_rights(color)
[docs] def has_kingside_castling_rights(self, color: chess.Color) -> bool: """ Check if the specified color has kingside castling rights. Args: color (chess.Color): The color to check for kingside castling rights. Returns: bool: True if the specified color has kingside castling rights, False otherwise. """ return self.chess_board.has_kingside_castling_rights(color)
[docs] def copy(self, stack: bool, deep_copy_legal_moves: bool = True) -> "BoardChi": """ Create a copy of the current board. Args: stack (bool): Whether to copy the move stack as well. Returns: BoardChi: A new instance of the BoardChi class with the copied board. """ chess_board_copy: chess.Board = self.chess_board.copy(stack=stack) legal_moves_copy: LegalMoveKeyGenerator if deep_copy_legal_moves: # deep_copy legal_moves_copy = self.legal_moves_.copy( copied_chess_board=chess_board_copy ) else: # faster as move generated are not deep copied but tricky (should not be modified later!) legal_moves_copy = self.legal_moves_ legal_moves_copy.chess_board = chess_board_copy return BoardChi( chess_board=chess_board_copy, compute_board_modification=self.compute_board_modification, fast_representation_=self.fast_representation_, legal_moves_=legal_moves_copy, )
[docs] def __str__(self) -> str: """ Returns a string representation of the board. Returns: str: A string representation of the board. """ return self.chess_board.__str__()
[docs] def tell_result(self) -> None: if self.chess_board.is_fivefold_repetition(): ("is_fivefold_repetition") if self.chess_board.is_seventyfive_moves(): chipiron_logger.info("is seventy five moves") if self.chess_board.is_insufficient_material(): chipiron_logger.info("is_insufficient_material") if self.chess_board.is_stalemate(): chipiron_logger.info("is_stalemate") if self.chess_board.is_checkmate(): chipiron_logger.info("is_checkmate") chipiron_logger.info(self.chess_board.result())
[docs] def result(self, claim_draw: bool = False) -> str: return self.chess_board.result(claim_draw=claim_draw)
@property def move_history_stack(self) -> list[moveUci]: return [move.uci() for move in self.chess_board.move_stack] @property def pawns(self) -> chess.Bitboard: return self.chess_board.pawns @property def knights(self) -> chess.Bitboard: return self.chess_board.knights @property def bishops(self) -> chess.Bitboard: return self.chess_board.bishops @property def rooks(self) -> chess.Bitboard: return self.chess_board.rooks @property def queens(self) -> chess.Bitboard: return self.chess_board.queens @property def kings(self) -> chess.Bitboard: return self.chess_board.kings @property def white(self) -> chess.Bitboard: return self.chess_board.occupied_co[chess.WHITE] @property def black(self) -> chess.Bitboard: return self.chess_board.occupied_co[chess.BLACK] @property def castling_rights(self) -> chess.Bitboard: return self.chess_board.castling_rights @property def occupied(self) -> chess.Bitboard: return self.chess_board.occupied
[docs] def occupied_color(self, color: chess.Color) -> chess.Bitboard: return self.chess_board.occupied_co[color]
[docs] def termination(self) -> chess.Termination: outcome: Outcome | None = self.chess_board.outcome(claim_draw=True) assert outcome is not None return outcome.termination
@property def promoted(self) -> chess.Bitboard: return self.chess_board.promoted @property def fullmove_number(self) -> int: return self.chess_board.fullmove_number @property def halfmove_clock(self) -> int: return self.chess_board.halfmove_clock @property def ep_square(self) -> int | None: return self.chess_board.ep_square
[docs] def is_zeroing(self, move: moveKey) -> bool: chess_move: chess.Move = self.legal_moves_.generated_moves[move] return self.chess_board.is_zeroing(chess_move)
[docs] def into_fen_plus_history(self) -> FenPlusHistory: return FenPlusHistory( current_fen=self.fen, historical_moves=self.move_history_stack, historical_boards=self.chess_board._stack, )