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(Debug, Clone)]
pub struct EngineProperties {
    _use_mate_distance_pruning: bool,
    _clear_table_after_each_search: bool,
    _use_lmr: bool,
}

impl EngineProperties {
    #[inline]
    pub fn use_mate_distance_pruning(&self) -> bool {
        self._use_mate_distance_pruning && !DISABLE_ALL_PRUNINGS
    }

    #[inline]
    pub fn set_using_mate_distance_pruning(&mut self, value: bool) {
        self._use_mate_distance_pruning = value;
    }

    #[inline]
    pub fn clear_table_after_each_search(&self) -> bool {
        self._clear_table_after_each_search || DISABLE_ALL_PRUNINGS
    }

    #[inline]
    pub fn set_clearing_table_after_each_search(&mut self, value: bool) {
        self._clear_table_after_each_search = value;
    }

    #[inline]
    pub fn use_lmr(&self) -> bool {
        self._use_lmr && !DISABLE_ALL_PRUNINGS
    }

    #[inline]
    pub fn set_using_lmr(&mut self, value: bool) {
        self._use_lmr = value;
    }
}

impl Default for EngineProperties {
    fn default() -> Self {
        Self {
            _use_mate_distance_pruning: true,
            _clear_table_after_each_search: true,
            _use_lmr: true,
        }
    }
}

#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct CustomEngine<T: SearchControl<Searcher<P>>, P: PositionEvaluation> {
    board: Board,
    transposition_table: Arc<TranspositionTable>,
    evaluator: P,
    last_score: Option<Score>,
    controller: T,
    num_threads: NonZeroUsize,
    num_nodes_searched: Arc<AtomicUsize>,
    selective_depth: Arc<AtomicUsize>,
    #[cfg_attr(feature = "serde", serde(skip))]
    optional_io_reader: Option<IoReader>,
    stop_command: Arc<AtomicBool>,
    terminate: Arc<AtomicBool>,
    properties: EngineProperties,
    #[cfg_attr(feature = "serde", serde(skip))]
    opening_book: Option<Arc<dyn PolyglotBook>>,
}

impl<T: SearchControl<Searcher<P>>, P: PositionEvaluation> CustomEngine<T, P> {
    pub fn new(
        board: Board,
        transposition_table: TranspositionTable,
        controller: T,
        evaluator: P,
    ) -> Self {
        Self {
            board,
            transposition_table: transposition_table.into(),
            evaluator,
            last_score: None,
            controller,
            num_threads: TIMECAT_DEFAULTS.num_threads,
            num_nodes_searched: AtomicUsize::new(0).into(),
            selective_depth: AtomicUsize::new(0).into(),
            optional_io_reader: None,
            stop_command: AtomicBool::new(false).into(),
            terminate: AtomicBool::new(false).into(),
            properties: EngineProperties::default(),
            opening_book: TIMECAT_DEFAULTS
                .inbuilt_book_bytes
                .and_then(|bytes| PolyglotBookHashMap::try_from(bytes).ok())
                .map(|book| Arc::new(book) as Arc<dyn PolyglotBook>),
        }
    }

    #[inline]
    fn get_transposition_table(&self) -> &TranspositionTable {
        &self.transposition_table
    }

    #[inline]
    pub fn get_search_controller(&self) -> &(impl SearchControl<Searcher<P>> + use<T, P>) {
        &self.controller
    }

    #[inline]
    pub fn get_search_controller_mut(
        &mut self,
    ) -> &mut (impl SearchControl<Searcher<P>> + use<T, P>) {
        &mut self.controller
    }

    #[inline]
    pub fn get_properties(&self) -> &EngineProperties {
        &self.properties
    }

    #[inline]
    pub fn get_properties_mut(&mut self) -> &mut EngineProperties {
        &mut self.properties
    }

    #[inline]
    fn get_num_nodes_searched(&self) -> usize {
        self.num_nodes_searched.load(MEMORY_ORDERING)
    }

    #[inline]
    pub fn get_selective_depth(&self) -> Ply {
        self.selective_depth.load(MEMORY_ORDERING)
    }

    #[inline]
    pub fn get_num_threads(&self) -> usize {
        self.num_threads.get()
    }

    #[inline]
    pub fn get_move_overhead(&self) -> Duration {
        self.controller.get_move_overhead()
    }

    #[inline]
    pub fn get_optional_io_reader(&self) -> Option<IoReader> {
        self.optional_io_reader.clone()
    }

    pub fn reset_variables(&mut self) {
        self.num_nodes_searched.store(0, MEMORY_ORDERING);
        self.selective_depth.store(0, MEMORY_ORDERING);
        self.controller.reset_variables();
        self.evaluator.reset_variables();
        if self.properties.clear_table_after_each_search() {
            self.transposition_table.clear();
        }
        self.transposition_table.reset_variables();
        self.set_stop_command(false);
        self.set_termination(false);
    }

    #[inline]
    pub fn generate_searcher(&self, id: usize) -> Searcher<P> {
        Searcher::new(
            id,
            self.last_score,
            self.board.clone(),
            self.evaluator.clone(),
            self.transposition_table.clone(),
            self.num_nodes_searched.clone(),
            self.selective_depth.clone(),
            self.stop_command.clone(),
            self.properties.clone(),
        )
    }

    #[inline]
    pub fn get_stop_command(&self) -> bool {
        self.stop_command.load(MEMORY_ORDERING)
    }

    #[inline]
    pub fn set_stop_command(&self, b: bool) {
        self.stop_command.store(b, MEMORY_ORDERING);
    }

    fn update_stop_command(
        stop_command: Arc<AtomicBool>,
        io_reader: IoReader,
        terminate: Arc<AtomicBool>,
    ) {
        while !stop_command.load(MEMORY_ORDERING) {
            match io_reader
                .read_line_once()
                .unwrap_or_default()
                .to_lowercase()
                .trim()
            {
                "stop" => stop_command.store(true, MEMORY_ORDERING),
                "quit" | "exit" => {
                    stop_command.store(true, MEMORY_ORDERING);
                    terminate.store(true, MEMORY_ORDERING);
                }
                _ => {}
            }
        }
    }
}

impl<T: SearchControl<Searcher<P>>, P: PositionEvaluation> ChessEngine for CustomEngine<T, P> {
    type IoReader = IoReader;

    #[inline]
    fn get_board(&self) -> &Board {
        &self.board
    }

    #[inline]
    fn get_board_mut(&mut self) -> &mut Board {
        &mut self.board
    }

    fn set_fen(&mut self, fen: &str) -> Result<()> {
        self.get_board_mut().set_fen(fen)?;
        self.reset_variables();
        Ok(())
    }

    fn set_transposition_table_size(&self, size: CacheTableSize) {
        self.transposition_table.set_size(size);
        if GLOBAL_TIMECAT_STATE.is_in_debug_mode() {
            self.transposition_table.print_info();
        }
    }

    #[inline]
    fn set_num_threads(&mut self, num_threads: NonZeroUsize) {
        self.num_threads = num_threads;
    }

    #[inline]
    fn set_move_overhead(&mut self, duration: Duration) {
        self.controller.set_move_overhead(duration);
    }

    #[inline]
    fn get_opening_book(&self) -> Option<&dyn PolyglotBook> {
        self.opening_book.as_deref()
    }

    #[inline]
    fn set_opening_book<B: PolyglotBook + 'static>(&mut self, book: Option<Arc<B>>) {
        self.opening_book = book.map(|b| b as Arc<dyn PolyglotBook>);
    }

    #[inline]
    fn terminate(&self) -> bool {
        self.terminate.load(MEMORY_ORDERING)
    }

    #[inline]
    fn set_termination(&self, b: bool) {
        self.terminate.store(b, MEMORY_ORDERING);
    }

    fn clear_hash(&mut self) {
        self.get_transposition_table().clear();
        self.evaluator.clear();
    }

    fn print_info(&self) {
        print_engine_version();
        println_wasm!();
        self.transposition_table.print_info();
        self.evaluator.print_info();
    }

    #[inline]
    fn set_optional_io_reader(&mut self, optional_io_reader: Self::IoReader) {
        self.optional_io_reader = Some(optional_io_reader);
    }

    #[inline]
    fn evaluate_current_position(&mut self) -> Score {
        self.evaluator.evaluate(&self.board)
    }

    #[inline]
    fn evaluate_current_position_flipped(&mut self) -> Score {
        self.evaluator.evaluate_flipped(&self.board)
    }

    fn search(&mut self, config: &SearchConfig, verbose: bool) -> SearchInfo {
        if let Some(WeightedMove { move_, weight }) = self.get_opening_book_weighted_move() {
            return SearchInfoBuilder::new(self.board.get_position().clone(), vec![move_])
                .set_score(weight as Score)
                .build();
        }
        self.reset_variables();
        let mut join_handles = vec![];
        for id in 1..self.num_threads.get() {
            let mut threaded_searcher = self.generate_searcher(id);
            let controller = self.controller.clone();
            let join_handle = thread::spawn(move || {
                threaded_searcher.search(
                    const { &SearchConfig::new_infinite() },
                    controller,
                    false,
                );
            });
            join_handles.push(join_handle);
        }
        if let Some(io_reader) = self.optional_io_reader.as_ref() {
            let stop_command = self.stop_command.clone();
            let reader = io_reader.clone();
            let terminate = self.terminate.clone();
            join_handles.push(thread::spawn(move || {
                Self::update_stop_command(stop_command, reader, terminate);
            }));
        }
        let mut main_thread_searcher = self.generate_searcher(0);
        main_thread_searcher.search(config, self.controller.clone(), verbose);
        self.set_stop_command(true);
        for join_handle in join_handles {
            join_handle.join().unwrap();
        }
        let mut search_info = main_thread_searcher.get_search_info();
        if search_info.get_pv().is_empty() && self.board.status() == BoardStatus::Ongoing {
            search_info.set_pv(vec![
                self.board
                    .get_single_legal_move(BitBoard::ALL, BitBoard::ALL)
                    .unwrap(),
            ]);
        }
        self.last_score = search_info.get_score();
        search_info
    }
}

impl<T: SearchControl<Searcher<P>> + Default, P: PositionEvaluation + Default> CustomEngine<T, P> {
    #[inline]
    pub fn from_board(board: Board) -> Self {
        Self::new(
            board,
            TranspositionTable::default(),
            T::default(),
            P::default(),
        )
    }

    #[inline]
    pub fn from_fen(fen: &str) -> Result<Self> {
        Ok(Self::from_board(Board::from_fen(fen)?))
    }
}

impl<T: SearchControl<Searcher<P>>, P: PositionEvaluation> Clone for CustomEngine<T, P> {
    fn clone(&self) -> Self {
        Self {
            board: self.board.clone(),
            transposition_table: self.transposition_table.as_ref().clone().into(),
            evaluator: self.evaluator.clone(),
            controller: self.controller.clone(),
            num_nodes_searched: AtomicUsize::new(self.num_nodes_searched.load(MEMORY_ORDERING))
                .into(),
            selective_depth: AtomicUsize::new(self.selective_depth.load(MEMORY_ORDERING)).into(),
            optional_io_reader: self.optional_io_reader.clone(),
            stop_command: AtomicBool::new(self.stop_command.load(MEMORY_ORDERING)).into(),
            terminate: AtomicBool::new(self.terminate.load(MEMORY_ORDERING)).into(),
            properties: self.properties.clone(),
            opening_book: self.opening_book.clone(),
            ..*self
        }
    }
}

impl<T: SearchControl<Searcher<P>> + Default, P: PositionEvaluation + Default> Default
    for CustomEngine<T, P>
{
    fn default() -> Self {
        Self::new(
            Board::default(),
            TranspositionTable::default(),
            T::default(),
            P::default(),
        )
    }
}