timecat 1.52.0

A NNUE-based chess engine that implements the Negamax algorithm and can be integrated into any project as a library. It features move generation, advanced position evaluation through NNUE, and move searching capabilities.
Documentation
use super::*;

#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug)]
pub struct ChessPositionBuilder {
    #[cfg_attr(feature = "serde", serde(with = "serde_handler"))]
    pieces: [Option<Piece>; 64],
    turn: Color,
    castle_rights: [CastleRights; 2],
    ep_file: Option<File>,
    halfmove_clock: u8,
    fullmove_number: NumMoves,
}

impl ChessPositionBuilder {
    /// Returns empty board builder with white to move
    pub fn new() -> Self {
        Self {
            pieces: [None; 64],
            turn: White,
            castle_rights: [CastleRights::None, CastleRights::None],
            ep_file: None,
            halfmove_clock: 0,
            fullmove_number: 1,
        }
    }

    pub fn setup(
        pieces: impl IntoIterator<Item = (Piece, Square)>,
        turn: Color,
        white_castle_rights: CastleRights,
        black_castle_rights: CastleRights,
        ep_file: Option<File>,
        halfmove_clock: u8,
        fullmove_number: u16,
    ) -> Self {
        let mut result = Self {
            pieces: [None; 64],
            turn,
            castle_rights: std::array::from_fn(|index| {
                if index == White.to_index() {
                    white_castle_rights
                } else {
                    black_castle_rights
                }
            }),
            ep_file,
            halfmove_clock,
            fullmove_number,
        };

        for (piece, square) in pieces {
            *get_item_unchecked_mut!(result.pieces, square.to_index()) = Some(piece);
        }

        result
    }

    #[inline]
    pub fn get_turn(&self) -> Color {
        self.turn
    }

    #[inline]
    pub fn get_castle_rights(&self, color: Color) -> CastleRights {
        *get_item_unchecked!(self.castle_rights, color.to_index())
    }

    #[inline]
    pub fn get_en_passant(&self) -> Option<Square> {
        self.ep_file
            .map(|f| Square::from_rank_and_file((!self.get_turn()).to_third_rank(), f))
    }

    #[inline]
    pub fn get_halfmove_clock(&self) -> u8 {
        self.halfmove_clock
    }

    #[inline]
    pub fn get_fullmove_number(&self) -> NumMoves {
        self.fullmove_number
    }

    pub fn set_turn(&mut self, color: Color) -> &mut Self {
        self.turn = color;
        self
    }

    pub fn castle_rights(&mut self, color: Color, castle_rights: CastleRights) -> &mut Self {
        *get_item_unchecked_mut!(self.castle_rights, color.to_index()) = castle_rights;
        self
    }

    pub fn add_piece(&mut self, square: Square, piece: Piece) -> &mut Self {
        self[square] = Some(piece);
        self
    }

    pub fn clear_square(&mut self, square: Square) -> &mut Self {
        self[square] = None;
        self
    }

    pub fn ep_file(&mut self, file: Option<File>) -> &mut Self {
        self.ep_file = file;
        self
    }

    pub fn halfmove_clock(&mut self, halfmove_clock: u8) -> &mut Self {
        self.halfmove_clock = halfmove_clock;
        self
    }

    pub fn fullmove_number(&mut self, fullmove_number: NumMoves) -> &mut Self {
        self.fullmove_number = fullmove_number;
        self
    }
}

impl Index<Square> for ChessPositionBuilder {
    type Output = Option<Piece>;

    fn index(&self, index: Square) -> &Self::Output {
        get_item_unchecked!(self.pieces, index.to_index())
    }
}

impl IndexMut<Square> for ChessPositionBuilder {
    fn index_mut(&mut self, index: Square) -> &mut Self::Output {
        get_item_unchecked_mut!(self.pieces, index.to_index())
    }
}

impl fmt::Display for ChessPositionBuilder {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let mut count = 0;
        for &rank in ALL_RANKS.iter().rev() {
            for file in ALL_FILES {
                let square = Square::from_rank_and_file(rank, file).to_index();

                if self.pieces[square].is_some() && count != 0 {
                    write!(f, "{}", count)?;
                    count = 0;
                }

                if let Some(piece) = self.pieces[square] {
                    write!(f, "{piece}")?;
                } else {
                    count += 1;
                }
            }

            if count != 0 {
                write!(f, "{}", count)?;
            }

            if rank != Rank::First {
                write!(f, "/")?;
            }
            count = 0;
        }

        write!(f, " ")?;

        if self.turn == White {
            write!(f, "w ")?;
        } else {
            write!(f, "b ")?;
        }

        write!(
            f,
            "{}",
            get_item_unchecked!(self.castle_rights, White.to_index()).to_string(White)
        )?;
        write!(
            f,
            "{}",
            get_item_unchecked!(self.castle_rights, Black.to_index()).to_string(Black)
        )?;
        if self.castle_rights[0] == CastleRights::None
            && self.castle_rights[1] == CastleRights::None
        {
            write!(f, "-")?;
        }

        write!(f, " ")?;
        if let Some(square) = self.get_en_passant() {
            write!(f, "{}", square)?;
        } else {
            write!(f, "-")?;
        }

        write!(f, " {} {}", self.halfmove_clock, self.fullmove_number)
    }
}

impl Default for ChessPositionBuilder {
    fn default() -> Self {
        Self::from_str(STARTING_POSITION_FEN).unwrap()
    }
}

impl FromStr for ChessPositionBuilder {
    type Err = TimecatError;

    fn from_str(value: &str) -> Result<Self> {
        let mut cur_rank = Rank::Eighth;
        let mut cur_file = File::A;
        let mut position_builder = Self::new();

        let tokens: Vec<&str> = value.split(' ').collect();
        if tokens.len() < 4 {
            return Err(TimecatError::BadFen {
                fen: value.to_string().into(),
            });
        }

        let pieces = tokens[0];
        let side = tokens[1];
        let castles = tokens[2];
        let ep = tokens[3];
        let halfmove_clock = tokens.get(4).and_then(|s| s.parse().ok()).unwrap_or(0);
        let fullmove_number = tokens.get(5).and_then(|s| s.parse().ok()).unwrap_or(1);

        for x in pieces.chars() {
            match x {
                '/' => {
                    cur_rank = cur_rank.wrapping_down();
                    cur_file = File::A;
                }
                '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' => {
                    cur_file = unsafe {
                        File::from_index((cur_file.to_index() + (x as usize) - ('0' as usize)) & 7)
                    };
                }
                'r' => {
                    position_builder[Square::from_rank_and_file(cur_rank, cur_file)] =
                        Some(BlackRook);
                    cur_file = cur_file.wrapping_right();
                }
                'R' => {
                    position_builder[Square::from_rank_and_file(cur_rank, cur_file)] =
                        Some(WhiteRook);
                    cur_file = cur_file.wrapping_right();
                }
                'n' => {
                    position_builder[Square::from_rank_and_file(cur_rank, cur_file)] =
                        Some(BlackKnight);
                    cur_file = cur_file.wrapping_right();
                }
                'N' => {
                    position_builder[Square::from_rank_and_file(cur_rank, cur_file)] =
                        Some(WhiteKnight);
                    cur_file = cur_file.wrapping_right();
                }
                'b' => {
                    position_builder[Square::from_rank_and_file(cur_rank, cur_file)] =
                        Some(BlackBishop);
                    cur_file = cur_file.wrapping_right();
                }
                'B' => {
                    position_builder[Square::from_rank_and_file(cur_rank, cur_file)] =
                        Some(WhiteBishop);
                    cur_file = cur_file.wrapping_right();
                }
                'p' => {
                    position_builder[Square::from_rank_and_file(cur_rank, cur_file)] =
                        Some(BlackPawn);
                    cur_file = cur_file.wrapping_right();
                }
                'P' => {
                    position_builder[Square::from_rank_and_file(cur_rank, cur_file)] =
                        Some(WhitePawn);
                    cur_file = cur_file.wrapping_right();
                }
                'q' => {
                    position_builder[Square::from_rank_and_file(cur_rank, cur_file)] =
                        Some(BlackQueen);
                    cur_file = cur_file.wrapping_right();
                }
                'Q' => {
                    position_builder[Square::from_rank_and_file(cur_rank, cur_file)] =
                        Some(WhiteQueen);
                    cur_file = cur_file.wrapping_right();
                }
                'k' => {
                    position_builder[Square::from_rank_and_file(cur_rank, cur_file)] =
                        Some(BlackKing);
                    cur_file = cur_file.wrapping_right();
                }
                'K' => {
                    position_builder[Square::from_rank_and_file(cur_rank, cur_file)] =
                        Some(WhiteKing);
                    cur_file = cur_file.wrapping_right();
                }
                _ => {
                    return Err(TimecatError::BadFen {
                        fen: value.to_string().into(),
                    });
                }
            }
        }
        match side {
            "w" | "W" => _ = position_builder.set_turn(White),
            "b" | "B" => _ = position_builder.set_turn(Black),
            _ => {
                return Err(TimecatError::BadFen {
                    fen: value.to_string().into(),
                });
            }
        }

        if castles.contains('K') && castles.contains('Q') {
            position_builder.castle_rights[White.to_index()] = CastleRights::Both;
        } else if castles.contains('K') {
            position_builder.castle_rights[White.to_index()] = CastleRights::KingSide;
        } else if castles.contains('Q') {
            position_builder.castle_rights[White.to_index()] = CastleRights::QueenSide;
        } else {
            position_builder.castle_rights[White.to_index()] = CastleRights::None;
        }

        if castles.contains('k') && castles.contains('q') {
            position_builder.castle_rights[Black.to_index()] = CastleRights::Both;
        } else if castles.contains('k') {
            position_builder.castle_rights[Black.to_index()] = CastleRights::KingSide;
        } else if castles.contains('q') {
            position_builder.castle_rights[Black.to_index()] = CastleRights::QueenSide;
        } else {
            position_builder.castle_rights[Black.to_index()] = CastleRights::None;
        }

        if let Ok(square) = Square::from_str(ep) {
            position_builder.ep_file(Some(square.get_file()));
        }

        position_builder
            .halfmove_clock(halfmove_clock)
            .fullmove_number(fullmove_number);

        Ok(position_builder)
    }
}

impl From<&ChessPosition> for ChessPositionBuilder {
    fn from(board: &ChessPosition) -> Self {
        Self::setup(
            board.iter(),
            board.turn(),
            board.castle_rights(White),
            board.castle_rights(Black),
            board.ep_square().map(|square| square.get_file()),
            board.get_halfmove_clock(),
            board.get_fullmove_number(),
        )
    }
}

impl From<ChessPosition> for ChessPositionBuilder {
    fn from(board: ChessPosition) -> Self {
        (&board).into()
    }
}