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

import typing
from typing import Iterator, Self

import chess
from chess import Outcome, _BoardState  # pyright: ignore[reportPrivateUsage]

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]


class LegalMoveKeyGenerator(LegalMoveKeyGeneratorP):
    """Generates legal move keys for a chess board."""

    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:
        """Initializes the LegalMoveKeyGenerator.

        Args:
            chess_board (chess.Board): The chess board to generate legal moves for.
            sort_legal_moves (bool): Whether to sort the legal moves.
        """
        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

    def get_uci_from_move_key(self, move_key: moveKey) -> moveUci:
        """Returns the UCI string corresponding to the given move key.
        Args:
            move_key (moveKey): The move key to convert to UCI.
        Returns:
            moveUci: The UCI string corresponding to the given move key."""
        chess_move: chess.Move = self.generated_moves[move_key]
        return chess_move.uci()

    @property
    def fen(self) -> fen:
        """Returns the FEN string of the current chess board."""
        return self.chess_board.fen()

    def __str__(self) -> str:
        """Returns a string representation of the legal move key generator.

        Returns:
            str: A string representation of the legal move key generator.
        """
        the_string: str = "Legals Moves: "
        ucis: list[moveUci] = [
            chess_move.uci() for chess_move in self.generated_moves.values()
        ]
        keys: list[moveKey] = [move_key for move_key in self.generated_moves.keys()]

        the_string = the_string + f"Generated ucis {ucis}"
        the_string = the_string + f" and generated keys {keys}"

        return the_string

    def __iter__(self) -> Iterator[moveKey]:
        """Returns an iterator over the legal move keys."""
        self.it = self.chess_board.generate_legal_moves()
        self.count = 0
        return self

    def __next__(self) -> moveKey:
        """Returns the next legal move key."""
        new_move: chess.Move = self.it.__next__()
        move_key_ = self.count
        self.generated_moves[move_key_] = new_move
        self.count += 1
        return move_key_

    def copy(
        self, copied_chess_board: chess.Board | None = None
    ) -> "LegalMoveKeyGenerator":
        """Returns a copy of the 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

    def reset(self) -> None:
        """Resets the iterator to the beginning of the legal moves."""
        self.it = self.chess_board.generate_legal_moves()
        self.generated_moves = {}
        self.count = 0
        self.all_generated_keys = None

    def copy_with_reset(self) -> Self:
        """Returns a copy of the LegalMoveKeyGenerator with the iterator reset."""
        legal_move_copy = type(self)(
            chess_board=self.chess_board, sort_legal_moves=self.sort_legal_moves
        )
        return legal_move_copy

    def get_all(self) -> list[moveKey]:
        """Returns a list of all generated move keys."""
        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

    def more_than_one_move(self) -> bool:
        """Checks if there is more than one legal move available.

        Returns:
            bool: True if there is more than one legal move, False otherwise.
        """
        # 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)


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_

    def play_mon(self, move: chess.Move) -> None:
        """Plays a move on the board.

        Args:
            move (chess.Move): The move to play.
        """
        self.chess_board.push(move)

    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)
            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)

                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

    def play_move_uci(self, move_uci: moveUci) -> BoardModificationP | None:
        """Plays a move in UCI format.

        Args:
            move_uci (moveUci): The UCI string representation of the move.

        Returns:
            BoardModificationP | None: The board modification resulting from the move, or None if the move is invalid.
        """

        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
    def play_move_key(self, move: moveKey) -> BoardModificationP | None:
        """Plays a move using its key.

        Args:
            move (moveKey): The key of the move to play.

        Returns:
            BoardModificationP | None: The board modification resulting from the move, or None if the move is invalid.
        """
        chess_move: chess.Move = self.legal_moves_.generated_moves[move]
        board_modification = self.play_move(move=chess_move)
        return board_modification

    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(
                "Cannot rewind more as self.halfmove_clock equals %d", self.ply()
            )

    @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

    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

    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)

    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()

    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)

    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

    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) -> LegalMoveKeyGeneratorP:
        """
        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.legal_moves_

    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)

    def piece_map(
        self, mask: chess.Bitboard = chess.BB_ALL
    ) -> dict[chess.Square, tuple[int, bool]]:
        """Get a mapping of squares to their piece type and color.

        Args:
            mask (chess.Bitboard, optional): A bitboard mask to filter the squares. Defaults to chess.BB_ALL.

        Returns:
            dict[chess.Square, tuple[int, bool]]: A dictionary mapping squares to their piece type and color.
        """
        result: dict[chess.Square, tuple[int, bool]] = {}
        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

    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)

    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)

    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)

    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,
        )

    def __str__(self) -> str:
        """
        Returns a string representation of the board.

        Returns:
            str: A string representation of the board.
        """
        return self.chess_board.__str__()

    def tell_result(self) -> None:
        if self.chess_board.is_fivefold_repetition():
            chipiron_logger.info("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())

    def result(self, claim_draw: bool = False) -> str:
        """Get the result of the game.

        Args:
            claim_draw (bool, optional): Whether to claim a draw. Defaults to False.

        Returns:
            str: The result of the game.
        """
        return self.chess_board.result(claim_draw=claim_draw)

    @property
    def move_history_stack(self) -> list[moveUci]:
        """Get the history of moves made in the game.

        Returns:
            list[moveUci]: A list of UCI strings representing the moves made.
        """
        return [move.uci() for move in self.chess_board.move_stack]

    @property
    def pawns(self) -> chess.Bitboard:
        """Get the pawns on the board.

        Returns:
            chess.Bitboard: A bitboard representing the positions of the pawns.
        """
        return self.chess_board.pawns

    @property
    def knights(self) -> chess.Bitboard:
        """Get the knights on the board.

        Returns:
            chess.Bitboard: A bitboard representing the positions of the knights.
        """
        return self.chess_board.knights

    @property
    def bishops(self) -> chess.Bitboard:
        """Get the bishops on the board.

        Returns:
            chess.Bitboard: A bitboard representing the positions of the bishops.
        """
        return self.chess_board.bishops

    @property
    def rooks(self) -> chess.Bitboard:
        """Get the rooks on the board.

        Returns:
            chess.Bitboard: A bitboard representing the positions of the rooks.
        """
        return self.chess_board.rooks

    @property
    def queens(self) -> chess.Bitboard:
        """Get the queens on the board.

        Returns:
            chess.Bitboard: A bitboard representing the positions of the queens.
        """
        return self.chess_board.queens

    @property
    def kings(self) -> chess.Bitboard:
        """Get the kings on the board.

        Returns:
            chess.Bitboard: A bitboard representing the positions of the kings.
        """
        return self.chess_board.kings

    @property
    def white(self) -> chess.Bitboard:
        """Get the white pieces on the board.

        Returns:
            chess.Bitboard: A bitboard representing the positions of the white pieces.
        """
        return self.chess_board.occupied_co[chess.WHITE]

    @property
    def black(self) -> chess.Bitboard:
        """Get the black pieces on the board.

        Returns:
            chess.Bitboard: A bitboard representing the positions of the black pieces.
        """
        return self.chess_board.occupied_co[chess.BLACK]

    @property
    def castling_rights(self) -> chess.Bitboard:
        """Get the castling rights for both sides.

        Returns:
            chess.Bitboard: A bitboard representing the castling rights.
        """
        return self.chess_board.castling_rights

    @property
    def occupied(self) -> chess.Bitboard:
        """Get the occupied squares on the board.

        Returns:
            chess.Bitboard: A bitboard representing the occupied squares.
        """
        return self.chess_board.occupied

    def occupied_color(self, color: chess.Color) -> chess.Bitboard:
        """Get the occupied squares of a specific color.

        Args:
            color (chess.Color): The color to get the occupied squares for.

        Returns:
            chess.Bitboard: A bitboard representing the occupied squares of the specified color.
        """
        return self.chess_board.occupied_co[color]

    def termination(self) -> chess.Termination:
        """Get the termination status of the game.

        Returns:
            chess.Termination: The termination status of the game.
        """
        outcome: Outcome | None = self.chess_board.outcome(claim_draw=True)
        assert outcome is not None
        return outcome.termination

    @property
    def promoted(self) -> chess.Bitboard:
        """Get the promoted pieces on the board.

        Returns:
            chess.Bitboard: A bitboard representing the positions of the promoted pieces.
        """
        return self.chess_board.promoted

    @property
    def fullmove_number(self) -> int:
        """Get the fullmove number of the game.

        Returns:
            int: The fullmove number of the game.
        """
        return self.chess_board.fullmove_number

    @property
    def halfmove_clock(self) -> int:
        """Get the halfmove clock of the game.

        Returns:
            int: The halfmove clock of the game.
        """
        return self.chess_board.halfmove_clock

    @property
    def ep_square(self) -> int | None:
        """Get the en passant square of the game.

        Returns:
            int | None: The en passant square of the game, or None if not applicable.
        """
        return self.chess_board.ep_square

    def is_zeroing(self, move: moveKey) -> bool:
        """Check if a move is a zeroing move (i.e., checks if the given move is a capture or pawn move.

        Args:
            move (moveKey): The move to check.

        Returns:
            bool: True if the move is a zeroing move, False otherwise.
        """
        chess_move: chess.Move = self.legal_moves_.generated_moves[move]
        return self.chess_board.is_zeroing(chess_move)

    def into_fen_plus_history(self) -> FenPlusHistory:
        """Get the FEN string and history of the game.

        Returns:
            FenPlusHistory: The FEN string and history of the game.
        """
        return FenPlusHistory(
            current_fen=self.fen,
            historical_moves=self.move_history_stack,
            historical_boards=self.chess_board._stack,  # pyright: ignore[reportPrivateUsage]
        )
