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 IntoSpin {
    fn into_spin(self) -> Spin;
}

macro_rules! impl_into_spin {
    ($type_:ty) => {
        impl IntoSpin for $type_ {
            fn into_spin(self) -> Spin {
                self as Spin
            }
        }
    };

    ($type_:ty, $func:ident) => {
        impl IntoSpin for $type_ {
            fn into_spin(self) -> Spin {
                self.$func() as Spin
            }
        }
    };
}

impl_into_spin!(usize);
impl_into_spin!(CacheTableSize, unwrap);
impl_into_spin!(Duration, as_millis);

#[derive(Clone, Debug)]
enum UCIOptionType<T: ChessEngine> {
    Button {
        function: fn(&mut T) -> Result<()>,
    },
    Check {
        default: bool,
        function: fn(&mut T, bool) -> Result<()>,
    },
    String {
        default: Cow<'static, str>,
        function: fn(&mut T, &str) -> Result<()>,
    },
    Spin {
        default: Spin,
        min: Spin,
        max: Spin,
        function: fn(&mut T, Spin) -> Result<()>,
    },
    Combo {
        default: Cow<'static, str>,
        options: Vec<Cow<'static, str>>,
        function: fn(&mut T, &str) -> Result<()>,
    },
}

#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Copy, PartialEq, Eq)]
struct SpinValue<T: Clone + Copy + IntoSpin> {
    default: T,
    min: T,
    max: T,
}

impl<T: Clone + Copy + IntoSpin> SpinValue<T> {
    #[inline]
    pub const fn new(default: T, min: T, max: T) -> Self {
        Self { default, min, max }
    }

    #[inline]
    pub const fn get_default(self) -> T {
        self.default
    }

    #[inline]
    pub const fn get_min(self) -> T {
        self.min
    }

    #[inline]
    pub const fn get_max(self) -> T {
        self.max
    }
}

#[derive(Clone, Debug)]
pub struct UCIOption<T: ChessEngine> {
    name: Cow<'static, str>,
    sorted_aliases: Vec<Cow<'static, str>>,
    option_type: UCIOptionType<T>,
}

impl<T: ChessEngine> UCIOption<T> {
    fn new(name: &str, option_type: UCIOptionType<T>) -> Self {
        Self {
            name: name.trim().to_string().into(),
            sorted_aliases: vec![],
            option_type,
        }
    }

    fn alias(mut self, name: &str) -> Self {
        self.sorted_aliases.push(name.trim().to_lowercase().into());
        self.sorted_aliases.sort_unstable();
        self
    }

    fn new_spin<U: Clone + Copy + IntoSpin>(
        name: &str,
        values: SpinValue<U>,
        function: fn(&mut T, Spin) -> Result<()>,
    ) -> Self {
        Self::new(
            name,
            UCIOptionType::Spin {
                default: values.get_default().into_spin(),
                min: values.get_min().into_spin(),
                max: values.get_max().into_spin(),
                function,
            },
        )
    }

    fn new_check(name: &str, default: bool, function: fn(&mut T, bool) -> Result<()>) -> Self {
        Self::new(name, UCIOptionType::Check { default, function })
    }

    fn new_button(name: &str, function: fn(&mut T) -> Result<()>) -> Self {
        Self::new(name, UCIOptionType::Button { function })
    }

    fn new_string(
        name: &str,
        default: Cow<'static, str>,
        function: fn(&mut T, &str) -> Result<()>,
    ) -> Self {
        Self::new(name, UCIOptionType::String { default, function })
    }

    fn set_option(&self, engine: &mut T, value_string: Cow<'static, str>) -> Result<()> {
        match self.option_type {
            UCIOptionType::Check { function, .. } => {
                function(engine, value_string.parse()?)?;
            }
            UCIOptionType::Spin {
                min, max, function, ..
            } => {
                let value = value_string.parse()?;
                if value < min || value > max {
                    return Err(TimecatError::InvalidSpinValue {
                        name: self.name.clone(),
                        value,
                        min,
                        max,
                    });
                }
                function(engine, value)?;
            }
            UCIOptionType::Combo { function, .. } => {
                function(engine, &value_string)?;
            }
            UCIOptionType::Button { function } => {
                function(engine)?;
            }
            UCIOptionType::String { function, .. } => {
                function(engine, &value_string)?;
            }
        }
        Ok(())
    }
}

impl<T: ChessEngine> fmt::Display for UCIOption<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let s = match &self.option_type {
            UCIOptionType::Check { default, .. } => {
                format!(
                    "{} {} {} {}",
                    "option name".colorize(INFO_MESSAGE_STYLE),
                    self.name,
                    "type check default".colorize(INFO_MESSAGE_STYLE),
                    default,
                )
            }
            UCIOptionType::Spin {
                default, min, max, ..
            } => {
                format!(
                    "{} {} {} {} {} {} {} {}",
                    "option name".colorize(INFO_MESSAGE_STYLE),
                    self.name,
                    "type spin default".colorize(INFO_MESSAGE_STYLE),
                    default,
                    "min".colorize(INFO_MESSAGE_STYLE),
                    min,
                    "max".colorize(INFO_MESSAGE_STYLE),
                    max,
                )
            }
            UCIOptionType::Combo {
                default, options, ..
            } => {
                format!(
                    "{} {} {} {} {}",
                    "option name".colorize(INFO_MESSAGE_STYLE),
                    self.name,
                    "type combo default".colorize(INFO_MESSAGE_STYLE),
                    default,
                    options.iter().map(|s| format!("var {s}")).join(" "),
                )
            }
            UCIOptionType::Button { .. } => {
                format!(
                    "{} {} {}",
                    "option name".colorize(INFO_MESSAGE_STYLE),
                    self.name,
                    "type button".colorize(INFO_MESSAGE_STYLE),
                )
            }
            UCIOptionType::String { default, .. } => {
                format!(
                    "{} {} {} {}",
                    "option name".colorize(INFO_MESSAGE_STYLE),
                    self.name,
                    "type string default".colorize(INFO_MESSAGE_STYLE),
                    default,
                )
            }
        };
        write!(f, "{s}")
    }
}

pub struct UCIStateManager<T: ChessEngine> {
    options: Vec<UCIOption<T>>,
}

impl<T: ChessEngine> UCIStateManager<T> {
    pub const fn dummy() -> Self {
        Self {
            options: Vec::new(),
        }
    }

    fn new() -> Self {
        Self {
            options: get_uci_state_manager(),
        }
    }

    pub fn get_option(&self, command_name: &str) -> Option<&UCIOption<T>> {
        self.options.iter().find(
            |UCIOption {
                 name,
                 sorted_aliases,
                 ..
             }| {
                name.eq_ignore_ascii_case(command_name)
                    || sorted_aliases.binary_search(&command_name.into()).is_ok()
            },
        )
    }

    pub fn get_all_options(&self) -> &[UCIOption<T>] {
        &self.options
    }

    pub fn run_command(&self, engine: &mut T, user_input: &str) -> Result<()> {
        let binding = Parser::sanitize_string(user_input);
        let commands = binding.split_whitespace().collect_vec();
        if commands
            .first()
            .ok_or(TimecatError::UnknownCommand)?
            .to_lowercase()
            != "setoption"
        {
            return Err(TimecatError::UnknownCommand);
        }
        if commands
            .get(1)
            .ok_or(TimecatError::UnknownCommand)?
            .to_lowercase()
            != "name"
        {
            return Err(TimecatError::UnknownCommand);
        }
        let command_name = commands
            .iter()
            .skip(2)
            .take_while(|&&c| c != "value")
            .join(" ")
            .to_lowercase();
        let value_string = commands
            .iter()
            .skip_while(|&&s| s != "value")
            .skip(1)
            .join(" ");

        self.get_option(&command_name)
            .ok_or(TimecatError::UnknownCommand)?
            .set_option(engine, value_string.into())
    }
}

impl<T: ChessEngine> Default for UCIStateManager<T> {
    fn default() -> Self {
        Self::new()
    }
}

fn get_uci_state_manager<T: ChessEngine>() -> Vec<UCIOption<T>> {
    // SetHashSize(u64),
    // SetMultiPV(u8),
    // SetUCIElo(u16),
    // SetEngineMode(EngineMode),
    // SetColor,
    // SetConsoleMode,
    // SetUciAnalyzeMode,
    // SetUCIChess960,
    // SetUCIOpponent,
    // SetUCIShowCurrLine,
    // SetUCIShowRefutations

    let options: Vec<UCIOption<T>> = vec![
        UCIOption::new_spin(
            "Threads",
            SpinValue::new(TIMECAT_DEFAULTS.num_threads.get(), 1, 1024),
            |engine: &mut T, value| {
                let num_threads = unsafe { NonZeroUsize::new_unchecked(value as usize) };
                engine.set_num_threads(num_threads);
                print_uci_info("Number of threads is set to", num_threads);
                Ok(())
            },
        )
        .alias("Thread"),
        UCIOption::new_spin(
            "Hash",
            SpinValue::new(
                TIMECAT_DEFAULTS.t_table_size,
                const { CacheTableSize::Exact(1) },
                const { CacheTableSize::Exact(1 << 25) },
            ),
            {
                |engine, value| {
                    let size = CacheTableSize::Exact(value as usize);
                    engine.set_transposition_table_size(size);
                    print_uci_info(
                        "Transposition table is set to size to",
                        size.to_memory_size_in_mb::<TranspositionTableEntry>(),
                    );
                    Ok(())
                }
            },
        ),
        UCIOption::new_button("Clear Hash", |engine| {
            engine.clear_hash();
            print_uci_info::<&str>("All hash tables are cleared!", None);
            Ok(())
        }),
        UCIOption::new_spin(
            "Move Overhead",
            SpinValue::new(
                TIMECAT_DEFAULTS.move_overhead,
                Duration::ZERO,
                const { Duration::from_secs(60) },
            ),
            |engine, value| {
                let duration = Duration::from_millis(value as u64);
                engine.set_move_overhead(duration);
                print_uci_info("Move Overhead is set to", duration.stringify());
                Ok(())
            },
        ),
        UCIOption::new_string(
            "BookFile",
            TIMECAT_DEFAULTS.book_path.unwrap_or("None").into(),
            |engine, book_path| {
                engine.set_opening_book(Some(Arc::new(PolyglotBookReader::from_str(book_path)?)));
                print_uci_info("BookFile is set to", book_path.to_string());
                Ok(())
            },
        ),
        // UCIOption::new_check(
        //     "OwnBook",
        //     TIMECAT_DEFAULTS.use_own_book,
        //     |engine, b| {
        //         use_own_book.store(b, MEMORY_ORDERING);
        //         print_uci_info("Own Book Usage is set to", b);
        //     },
        // ),
        // UCIOption::new_check(
        //     "UCI_Chess960",
        //     TIMECAT_DEFAULTS.chess960_mode,
        //     |engine, b| {
        //         chess960_mode.store(b, MEMORY_ORDERING);
        //         print_uci_info("Chess 960 mode is set to", b);
        //     },
        // ),
    ];
    options
}