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::*;

trait ToUnsigned {
    type Unsigned;
    fn to_unsigned(self) -> Self::Unsigned;
}

macro_rules! to_unsigned {
    ($from:ty, $to:ty) => {
        impl ToUnsigned for $from {
            type Unsigned = $to;

            fn to_unsigned(self) -> Self::Unsigned {
                self as Self::Unsigned
            }
        }
    };
}

to_unsigned!(i8, u8);
to_unsigned!(i16, u16);
to_unsigned!(i32, u32);
to_unsigned!(i64, u64);
to_unsigned!(i128, u128);
to_unsigned!(isize, usize);

#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Copy, Debug, PartialOrd, PartialEq, Eq, Default)]
pub enum EntryFlagHash {
    #[default]
    Exact,
    Alpha,
    Beta,
}

#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Copy, Debug, PartialOrd, PartialEq)]
struct TranspositionTableData {
    depth: Depth,
    score: Score,
    flag: EntryFlagHash,
}

impl Default for TranspositionTableData {
    fn default() -> Self {
        Self {
            depth: -1,
            score: Default::default(),
            flag: Default::default(),
        }
    }
}

#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct TranspositionTableEntry {
    optional_data: Option<TranspositionTableData>,
    best_move: Option<Move>,
}

impl TranspositionTableEntry {
    fn new(optional_data: Option<TranspositionTableData>, best_move: Option<Move>) -> Self {
        Self {
            optional_data,
            best_move,
        }
    }
}

#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[repr(transparent)]
#[derive(Debug, Clone)]
pub struct TranspositionTable {
    table: CacheTable<TranspositionTableEntry>,
}

impl TranspositionTable {
    #[inline]
    pub fn print_info(&self) {
        print_cache_table_info("Hash Table", self.table.len(), self.table.get_size());
    }

    #[inline]
    fn generate_new_table(cache_table_size: CacheTableSize) -> CacheTable<TranspositionTableEntry> {
        CacheTable::new(cache_table_size)
    }

    pub fn new(cache_table_size: CacheTableSize) -> Self {
        Self {
            table: Self::generate_new_table(cache_table_size),
        }
    }

    pub fn read(
        &self,
        key: u64,
        depth: Depth,
        ply: Ply,
    ) -> (Option<(Score, EntryFlagHash)>, Option<Move>) {
        let tt_entry = match self.table.get(key) {
            Some(entry) => entry,
            None => return (None, None),
        };
        let best_move = tt_entry.best_move;
        if tt_entry.optional_data.is_none() {
            return (None, best_move);
        }
        let data = tt_entry.optional_data.unwrap();
        if data.depth < depth {
            return (None, best_move);
        }
        let mut score = data.score;
        if is_checkmate(score) {
            score -= if score.is_positive() {
                ply as Score
            } else {
                -(ply as Score)
            };
        }
        (Some((score, data.flag)), best_move)
    }

    #[inline]
    pub fn read_best_move(&self, key: u64) -> Option<Move> {
        self.table.get(key)?.best_move
    }

    pub fn write(
        &self,
        key: u64,
        depth: Depth,
        ply: Ply,
        mut score: Score,
        flag: EntryFlagHash,
        best_move: Option<Move>,
    ) {
        // TODO: Logic Wrong Here?
        let save_score = !is_checkmate(score);
        if save_score && is_checkmate(score) {
            let mate_distance = CHECKMATE_SCORE
                .abs_diff(score.abs())
                .abs_diff(ply as <Score as ToUnsigned>::Unsigned)
                as Score;
            let mate_score = CHECKMATE_SCORE - mate_distance;
            score = if score.is_positive() {
                mate_score
            } else {
                -mate_score
            };
        }
        let old_optional_entry = self.table.get(key);
        self.table.add(
            key,
            TranspositionTableEntry::new(
                save_score.then(|| {
                    old_optional_entry
                        .and_then(|tt_entry| tt_entry.optional_data)
                        .filter(|data| data.depth > depth)
                        .unwrap_or(TranspositionTableData { depth, score, flag })
                }),
                best_move.or_else(|| old_optional_entry.and_then(|tt_entry| tt_entry.best_move)),
            ),
        );
    }

    #[inline]
    pub fn clear_best_moves(&self) {
        self.table
            .get_table()
            .write()
            .unwrap()
            .iter_mut()
            .flatten()
            .for_each(|entry| {
                entry.get_entry_mut().best_move = None;
            });
    }
}

impl Default for TranspositionTable {
    fn default() -> Self {
        Self::new(TIMECAT_DEFAULTS.t_table_size)
    }
}

impl Deref for TranspositionTable {
    type Target = CacheTable<TranspositionTableEntry>;

    fn deref(&self) -> &Self::Target {
        &self.table
    }
}