from collections import Counter
from dataclasses import dataclass, field
from typing import Any, Iterator, Self
import chess
import shakmaty_python_binding
from chipiron.environments.chess_env.board.board_modification import (
BoardModification,
BoardModificationP,
BoardModificationRust,
)
from chipiron.environments.chess_env.move import moveUci
from chipiron.environments.chess_env.move.imove import moveKey
from .iboard import (
IBoard,
LegalMoveKeyGeneratorP,
boardKey,
boardKeyWithoutCounters,
compute_key,
)
from .utils import FenPlusHistory, fen
[docs]class LegalMoveKeyGeneratorRust(LegalMoveKeyGeneratorP):
# whether to sort the legal_moves by their respective uci for easy comparison of various implementations
sort_legal_moves: bool
generated_moves: list[shakmaty_python_binding.MyMove] | None
all_generated_keys: list[moveKey] | None
chess_rust_binding: shakmaty_python_binding.MyChess
def __init__(
self,
sort_legal_moves: bool,
chess_rust_binding: shakmaty_python_binding.MyChess,
generated_moves: list[shakmaty_python_binding.MyMove] | None = None,
):
self.chess_rust_binding = chess_rust_binding
self.generated_moves = generated_moves
if generated_moves is not None:
self.number_moves = len(generated_moves)
self.it: Iterator[int] = iter(range(self.number_moves))
self.all_generated_keys = list(range(self.number_moves))
if sort_legal_moves:
def f(i: int) -> moveUci:
assert self.generated_moves is not None
return self.generated_moves[i].uci()
self.all_generated_keys = sorted(
list(range(self.number_moves)),
# key=lambda i: self.generated_moves[i].uci()
key=f,
)
else:
self.all_generated_keys = list(range(self.number_moves))
else:
self.all_generated_keys = None
self.sort_legal_moves = sort_legal_moves
@property
def fen(self) -> fen:
return self.chess_rust_binding.fen()
[docs] def reset(self, generated_moves: list[shakmaty_python_binding.MyMove]) -> None:
self.generated_moves = generated_moves
self.number_moves = len(generated_moves)
self.it = iter(range(self.number_moves))
self.all_generated_keys = list(range(self.number_moves))
[docs] def copy_with_reset(
self, generated_moves: list[shakmaty_python_binding.MyMove] | None = None
) -> "LegalMoveKeyGeneratorRust":
legal_move_copy = LegalMoveKeyGeneratorRust(
chess_rust_binding=self.chess_rust_binding,
generated_moves=generated_moves,
sort_legal_moves=self.sort_legal_moves,
)
return legal_move_copy
[docs] def set_legal_moves(
self, generated_moves: list[shakmaty_python_binding.MyMove]
) -> None:
self.generated_moves = generated_moves
self.number_moves = len(generated_moves)
def __iter__(self) -> Iterator[moveKey]:
if self.generated_moves is None:
self.generated_moves = self.chess_rust_binding.legal_moves()
if self.sort_legal_moves:
assert self.generated_moves is not None
def f(i: int) -> moveUci:
assert self.generated_moves is not None
return self.generated_moves[i].uci()
self.it = iter(
sorted(
list(range(self.number_moves)),
# key=lambda i: self.generated_moves[i].uci()
key=f,
)
)
else:
self.it = iter(range(self.number_moves))
return self
def __next__(self) -> moveKey:
return self.it.__next__()
[docs] def copy(
self, copied_chess_rust_binding: shakmaty_python_binding.MyChess | None = None
) -> "LegalMoveKeyGeneratorRust":
if copied_chess_rust_binding is None:
copied_chess_rust_binding_ = self.chess_rust_binding
else:
copied_chess_rust_binding_ = copied_chess_rust_binding
legal_move_copy = LegalMoveKeyGeneratorRust(
chess_rust_binding=copied_chess_rust_binding_,
generated_moves=(
self.generated_moves.copy()
if self.generated_moves is not None
else None
),
sort_legal_moves=self.sort_legal_moves,
)
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 = legal_move_copy.all_generated_keys
return legal_move_copy
[docs] def get_all(self) -> list[moveKey]:
if self.generated_moves is None:
self.generated_moves = self.chess_rust_binding.legal_moves()
self.number_moves = len(self.generated_moves)
self.all_generated_keys = None
if self.all_generated_keys is None:
if self.sort_legal_moves:
def f(i: int) -> moveUci:
assert self.generated_moves is not None
return self.generated_moves[i].uci()
s = sorted(
list(range(self.number_moves)),
# key=lambda i: self.generated_moves[i].uci()
key=f,
)
return s
else:
return list(range(self.number_moves))
else:
return self.all_generated_keys
[docs] def more_than_one_move(self) -> bool:
if self.generated_moves is None:
self.generated_moves = self.chess_rust_binding.legal_moves()
self.number_moves = len(self.generated_moves)
self.all_generated_keys = None
return len(self.generated_moves) > 0
# todo implement rewind (and a test for it)
[docs]@dataclass
class RustyBoardChi(IBoard):
"""
Rusty Board Chipiron
object that describes the current board. it wraps the chess Board from the chess package so it can have more in it
but im not sure its really necessary.i keep it for potential usefulness
This is the Rust version for speedy execution
It is based on the binding library shakmaty_python_binding to use the rust library shakmaty
"""
# the shakmaty implementation of the board that we wrap here
chess_: shakmaty_python_binding.MyChess
compute_board_modification: bool
# to count the number of occurrence of each board to be able to compute
# three-fold repetition as shakmaty does not do it atm
rep_to_count: Counter[boardKeyWithoutCounters]
fast_representation_: boardKey
# storing the info here for fast access as it seems calls to rust bingings can be costy
pawns_: int
kings_: int
queens_: int
rooks_: int
bishops_: int
knights_: int
white_: int
black_: int
turn_: bool
ep_square_: int | None
promoted_: int
castling_rights_: int
legal_moves_: LegalMoveKeyGeneratorRust
# the move history is kept here because shakmaty_python_binding.MyChess does not have a move stack at the moment
move_stack: list[moveUci] = field(default_factory=list)
def __post_init__(self) -> None:
self.rep_to_count[self.fast_representation_without_counters] = 1
[docs] def __str__(self) -> str:
"""
Returns a string representation of the board.
Returns:
str: A string representation of the board.
"""
return self.fen
[docs] def play_min_2(self, move: shakmaty_python_binding.MyMove) -> None:
# _str, ply, turn, is_game_over = self.chess_.play_and_return(move)
(
self.castling_rights_,
self.pawns_,
self.knights_,
self.bishops_,
self.rooks_,
self.queens_,
self.kings_,
self.white_,
self.black_,
turn_int,
ep_square_int,
self.promoted_,
) = self.chess_.play_and_return_o(move)
self.turn_ = bool(turn_int)
if ep_square_int == -1:
self.ep_square_ = None
else:
self.ep_square_ = ep_square_int
[docs] def play_min_3(self, move: shakmaty_python_binding.MyMove) -> BoardModificationRust:
# _str, ply, turn, is_game_over = self.chess_.play_and_return(move)
(
(
self.castling_rights_,
self.pawns_,
self.knights_,
self.bishops_,
self.rooks_,
self.queens_,
self.kings_,
self.white_,
self.black_,
turn_int,
ep_square_int,
self.promoted_,
),
appearances,
removals,
) = self.chess_.play_and_return_modifications(move)
self.turn_ = bool(turn_int)
if ep_square_int == -1:
self.ep_square_ = None
else:
self.ep_square_ = ep_square_int
board_modifications: BoardModificationRust = self.convert(appearances, removals)
# board_modifications: BoardModification = BoardModificationRust(appearances_=appearances,removals_=removals)
return board_modifications
[docs] def convert(
self,
appearances: set[tuple[int, int, int]],
removals: set[tuple[int, int, int]],
) -> BoardModificationRust:
board_modifications: BoardModificationRust = BoardModificationRust(
appearances_=appearances, removals_=removals
)
# board_modifications: BoardModification = BoardModification(
# appearances={PieceInSquare(square=a[0],piece=a[1],color=bool(a[2])) for a in appearances},
# removals={PieceInSquare(square=r[0],piece=r[1],color=bool(r[2])) for r in removals}
# )
return board_modifications
[docs] def play_move(
self, move: shakmaty_python_binding.MyMove
) -> 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.board.is_legal(move)
#
board_modifications: BoardModificationRust | None = None
if self.compute_board_modification:
if True:
board_modifications = self.play_min_3(move)
# else:
# previous_pawns = self.pawns_
# previous_kings = self.kings_
# previous_queens = self.queens_
# previous_rooks = self.rooks_
# previous_bishops = self.bishops_
# previous_knights = self.knights_
# previous_occupied_white = self.white_
# previous_occupied_black = self.black_
#
# self.play_min_2(move)
#
# new_pawns = self.pawns_
# new_kings = self.kings_
# new_queens = self.queens_
# new_rooks = self.rooks_
# new_bishops = self.bishops_
# new_knights = self.knights_
# new_occupied_white = self.white_
# new_occupied_black = self.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.play_min_2(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.pawns_,
knights=self.knights_,
bishops=self.bishops_,
rooks=self.rooks_,
queens=self.queens_,
kings=self.kings_,
turn=self.turn_,
castling_rights=self.castling_rights_,
ep_square=self.ep_square_,
white=self.white_,
black=self.black_,
promoted=self.promoted_,
fullmove_number=self.chess_.fullmove_number(),
halfmove_clock=self.chess_.halfmove_clock(),
)
self.fast_representation_ = fast_representation
self.rep_to_count.update([self.fast_representation_without_counters])
self.move_stack.append(move.uci())
# self.turn_ = not self.turn_
return board_modifications
[docs] def play_move_uci(self, move_uci: moveUci) -> BoardModificationP | None:
chess_move: shakmaty_python_binding.MyMove = shakmaty_python_binding.MyMove(
uci=move_uci, my_chess=self.chess_
)
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:
assert self.legal_moves_.generated_moves is not None
my_move: shakmaty_python_binding.MyMove = self.legal_moves_.generated_moves[
move
]
return self.play_move(move=my_move)
[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
"""
ply: int = self.chess_.ply()
return ply
@property
def turn(self) -> chess.Color:
"""
Get the current turn color.
Returns:
chess.Color: The color of the current turn.
"""
# return bool(self.chess_.turn())
return self.turn_
[docs] def is_game_over(self) -> bool:
"""
Check if the game is over.
Returns:
bool: True if the game is over, False otherwise.
"""
claim_draw: bool = True if len(self.move_stack) >= 5 else False
three_fold_repetition: bool = (
max(self.rep_to_count.values()) > 2 if claim_draw else False
)
# todo check the move stack : check for repetition as the rust version not do it
# todo remove this hasatrribute at some point
if hasattr(self, "is_game_over_"):
return three_fold_repetition or self.is_game_over_
else:
return three_fold_repetition or self.chess_.is_game_over()
[docs] def copy(self, stack: bool, deep_copy_legal_moves: bool = True) -> Self:
"""
Create a copy of the current board.
Args:
stack (bool): Whether to copy the move stack as well.
Returns:
RustyBoardChi: A new instance of the BoardChi class with the copied board.
"""
chess_copy: shakmaty_python_binding.MyChess = self.chess_.copy()
move_stack_ = self.move_stack.copy() if stack else []
legal_moves_copy: LegalMoveKeyGeneratorRust
if deep_copy_legal_moves:
legal_moves_copy = self.legal_moves_.copy(
copied_chess_rust_binding=chess_copy
)
else:
legal_moves_copy = self.legal_moves_
legal_moves_copy.chess_rust_binding = chess_copy
return type(self)(
chess_=chess_copy,
move_stack=move_stack_,
compute_board_modification=self.compute_board_modification,
rep_to_count=self.rep_to_count.copy(),
fast_representation_=self.fast_representation_,
pawns_=self.pawns_,
knights_=self.knights_,
kings_=self.kings_,
rooks_=self.rooks_,
queens_=self.queens_,
bishops_=self.bishops_,
black_=self.black_,
white_=self.white_,
turn_=self.turn_,
ep_square_=self.ep_square_,
promoted_=self.promoted_,
castling_rights_=self.castling_rights_,
legal_moves_=legal_moves_copy,
)
@property
def legal_moves(self) -> LegalMoveKeyGeneratorRust:
# todo minimize this call and understand when the role of the variable all legal move generated
return self.legal_moves_
[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_.number_of_pieces_on_the_board()
@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_.fen()
[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.
"""
piece_or_none = self.chess_.piece_at(square)
piece: chess.Piece | None
if piece_or_none is None:
piece = None
else:
piece = chess.Piece(piece_type=piece_or_none[1], color=piece_or_none[0])
return piece
[docs] def piece_map(self) -> dict[chess.Square, tuple[int, bool]]:
dict_raw = self.chess_.piece_map()
return dict_raw
[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_.has_kingside_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 kingside castling rights, False otherwise.
"""
return self.chess_.has_queenside_castling_rights(color)
[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_.fen())
[docs] def tell_result(self) -> None: ...
@property
def move_history_stack(self) -> list[moveUci]:
return self.move_stack
[docs] def dump(self, f: Any) -> None: ...
[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.
"""
return self.chess_.is_attacked(a_color)
@property
def pawns(self) -> chess.Bitboard:
return self.chess_.pawns()
@property
def knights(self) -> chess.Bitboard:
return self.knights_
# return self.chess_.knights()
@property
def bishops(self) -> chess.Bitboard:
return self.bishops_
# return self.chess_.bishops()
@property
def rooks(self) -> chess.Bitboard:
return self.rooks_
# return self.chess_.rooks()
@property
def queens(self) -> chess.Bitboard:
return self.queens_
# return self.chess_.queens()
@property
def kings(self) -> chess.Bitboard:
return self.kings_
# return self.chess_.kings()
@property
def white(self) -> chess.Bitboard:
return self.white_
# return self.chess_.white()
@property
def black(self) -> chess.Bitboard:
return self.black_
# return self.chess_.black()
@property
def occupied(self) -> chess.Bitboard:
return self.chess_.occupied()
[docs] def result(self, claim_draw: bool = False) -> str:
claim_draw_: bool = True if len(self.move_stack) >= 5 and claim_draw else False
three_fold_repetition: bool = (
max(self.rep_to_count.values()) > 2 if claim_draw_ else False
)
if three_fold_repetition:
return "1/2-1/2"
else:
return self.chess_.result()
@property
def castling_rights(self) -> chess.Bitboard:
# return self.chess_.castling_rights()
return self.castling_rights_
[docs] def termination(self) -> None:
return None
[docs] def occupied_color(self, color: chess.Color) -> chess.Bitboard:
if color == chess.WHITE:
return self.chess_.white()
else:
return self.chess_.black()
@property
def halfmove_clock(self) -> int:
return self.chess_.halfmove_clock()
@property
def promoted(self) -> chess.Bitboard:
return self.promoted_
@property
def fullmove_number(self) -> int:
return self.chess_.fullmove_number()
@property
def ep_square(self) -> int | None:
return self.ep_square_
[docs] def is_zeroing(self, move: moveKey) -> bool:
assert self.legal_moves_.generated_moves is not None
chess_move: shakmaty_python_binding.MyMove = self.legal_moves_.generated_moves[
move
]
return chess_move.is_zeroing()
[docs] def into_fen_plus_history(self) -> FenPlusHistory:
return FenPlusHistory(
current_fen=self.fen, historical_moves=self.move_history_stack
)