summaryrefslogtreecommitdiff
path: root/engine
diff options
context:
space:
mode:
Diffstat (limited to 'engine')
-rwxr-xr-x[-rw-r--r--]engine/.gitignore0
-rwxr-xr-x[-rw-r--r--]engine/Cargo.toml3
-rwxr-xr-xengine/src/c_abi.rs403
-rwxr-xr-x[-rw-r--r--]engine/src/engine.rs548
-rwxr-xr-x[-rw-r--r--]engine/src/eval.rs331
-rwxr-xr-xengine/src/info.rs27
-rwxr-xr-x[-rw-r--r--]engine/src/lazysort.rs174
-rwxr-xr-x[-rw-r--r--]engine/src/lib.rs38
-rwxr-xr-x[-rw-r--r--]engine/src/main.rs141
-rwxr-xr-x[-rw-r--r--]engine/src/search.rs530
-rwxr-xr-x[-rw-r--r--]engine/src/tablebase.rs372
-rwxr-xr-x[-rw-r--r--]engine/src/transposition_table.rs354
12 files changed, 1710 insertions, 1211 deletions
diff --git a/engine/.gitignore b/engine/.gitignore
index 96ef6c0..96ef6c0 100644..100755
--- a/engine/.gitignore
+++ b/engine/.gitignore
diff --git a/engine/Cargo.toml b/engine/Cargo.toml
index 3e17c08..745e7a5 100644..100755
--- a/engine/Cargo.toml
+++ b/engine/Cargo.toml
@@ -7,6 +7,9 @@ publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+[lib]
+crate_type = ["staticlib", "lib"]
+
[dependencies]
model = {path = "../model"}
byteorder = "1"
diff --git a/engine/src/c_abi.rs b/engine/src/c_abi.rs
new file mode 100755
index 0000000..6a7f35f
--- /dev/null
+++ b/engine/src/c_abi.rs
@@ -0,0 +1,403 @@
+use core::ffi::{c_int, c_ulonglong};
+use std::ffi::CString;
+use std::num::{NonZeroU8, NonZeroUsize};
+use std::sync::atomic::AtomicBool;
+use std::time::Duration;
+
+use model::{CheckersBitBoard, Move, MoveDirection, PieceColor};
+
+use crate::{
+ ActualLimit, Clock, Engine, EvalInfo, Evaluation, EvaluationSettings, Frontend, SearchLimit,
+};
+
+#[repr(C)]
+struct CFrontend {
+ debug_fn: extern "C" fn(*const u8),
+ info_fn: extern "C" fn(*const EvalInfo),
+ bestmove_fn: extern "C" fn(*const Move),
+}
+
+impl Frontend for CFrontend {
+ fn debug(&self, msg: &str) {
+ (self.debug_fn)(msg.as_bytes().as_ptr())
+ }
+
+ fn info(&self, info: EvalInfo) {
+ (self.info_fn)(&info)
+ }
+
+ fn report_best_move(&self, best_move: Move) {
+ (self.bestmove_fn)(&best_move)
+ }
+}
+
+#[repr(C)]
+struct EvalResult {
+ evaluation: Box<Evaluation>,
+ best_move: Option<Box<Move>>,
+}
+
+#[no_mangle]
+extern "C" fn ampere_new_engine(hash_size: c_ulonglong, frontend: &CFrontend) -> Box<Engine<'_>> {
+ Box::new(Engine::new(hash_size as usize, frontend))
+}
+
+#[no_mangle]
+extern "C" fn ampere_set_debug(engine: &Engine<'_>, debug: bool) {
+ engine.set_debug(debug)
+}
+
+#[no_mangle]
+extern "C" fn ampere_islegal(engine: &Engine<'_>, ampere_move: &Move) -> bool {
+ engine.is_legal_move(*ampere_move)
+}
+
+#[no_mangle]
+extern "C" fn ampere_current_position(engine: &Engine<'_>) -> Box<CheckersBitBoard> {
+ Box::new(engine.current_position())
+}
+
+#[no_mangle]
+extern "C" fn ampere_reset_position(engine: &Engine<'_>) {
+ engine.reset_position();
+}
+
+#[no_mangle]
+extern "C" fn ampere_set_position(engine: &Engine<'_>, board: &CheckersBitBoard) {
+ engine.set_position(*board);
+}
+
+#[no_mangle]
+extern "C" fn ampere_play_move(engine: &Engine<'_>, ampere_move: &Move) -> bool {
+ engine.apply_move(*ampere_move).is_some()
+}
+
+#[no_mangle]
+extern "C" fn ampere_evaluate(
+ engine: &'static Engine<'_>,
+ cancel: Option<&AtomicBool>,
+ nodes: c_int,
+ depth: c_int,
+ time: Option<&Clock>,
+) -> EvalResult {
+ let limits = if nodes == 0 && depth == 0 && time.is_none() {
+ SearchLimit::Auto
+ } else {
+ let time = time.cloned().unwrap_or(Clock::Unlimited);
+
+ SearchLimit::Limited(ActualLimit {
+ nodes: NonZeroUsize::new(nodes as usize),
+ depth: NonZeroU8::new(depth as u8),
+ time: Some(time.recommended_time(engine.current_position().turn)),
+ })
+ };
+
+ let (eval, best) = engine.evaluate(
+ cancel,
+ EvaluationSettings {
+ restrict_moves: None,
+ ponder: false,
+ clock: Clock::Unlimited,
+ search_until: limits,
+ },
+ );
+
+ let evaluation = Box::new(eval);
+ let best_move = best.map(Box::new);
+
+ EvalResult {
+ evaluation,
+ best_move,
+ }
+}
+
+#[no_mangle]
+extern "C" fn ampere_starteval_limited(
+ engine: &'static Engine<'_>,
+ ponder: bool,
+ nodes: c_int,
+ depth: c_int,
+ time: c_int,
+) {
+ let limits = if nodes == 0 && depth == 0 && time == 0 {
+ SearchLimit::Auto
+ } else {
+ let time = if time == 0 {
+ None
+ } else {
+ Some(Duration::from_millis(time as u64))
+ };
+
+ SearchLimit::Limited(ActualLimit {
+ nodes: NonZeroUsize::new(nodes as usize),
+ depth: NonZeroU8::new(depth as u8),
+ time,
+ })
+ };
+
+ engine.start_evaluation(EvaluationSettings {
+ restrict_moves: None,
+ ponder,
+ clock: Clock::Unlimited,
+ search_until: limits,
+ })
+}
+
+#[no_mangle]
+extern "C" fn ampere_starteval_unlimited(engine: &'static Engine<'_>, ponder: bool) {
+ engine.start_evaluation(EvaluationSettings {
+ restrict_moves: None,
+ ponder,
+ clock: Clock::Unlimited,
+ search_until: SearchLimit::Infinite,
+ })
+}
+
+#[no_mangle]
+extern "C" fn ampere_stopeval(engine: &Engine<'_>) -> bool {
+ engine.stop_evaluation().is_some()
+}
+
+#[no_mangle]
+extern "C" fn ampere_destroy_engine(engine: Box<Engine<'_>>) {
+ drop(engine)
+}
+
+#[no_mangle]
+extern "C" fn ampere_evalinfo_nodes(info: &EvalInfo) -> c_ulonglong {
+ info.nodes_searched as c_ulonglong
+}
+
+#[no_mangle]
+extern "C" fn ampere_evalinfo_evaluation(info: &EvalInfo) -> *const Evaluation {
+ &info.evaluation
+}
+
+#[no_mangle]
+extern "C" fn ampere_evalinfo_bestmove(info: &EvalInfo) -> Option<&Move> {
+ info.current_best_move.as_ref()
+}
+
+#[no_mangle]
+extern "C" fn ampere_evalinfo_depth(info: &EvalInfo) -> c_int {
+ info.current_depth as c_int
+}
+
+#[no_mangle]
+extern "C" fn ampere_evalinfo_nodespersec(info: &EvalInfo) -> c_ulonglong {
+ info.nodes_per_second() as c_ulonglong
+}
+
+#[no_mangle]
+extern "C" fn ampere_evalinfo_elapsed(info: &EvalInfo) -> c_ulonglong {
+ info.elapsed_milliseconds() as c_ulonglong
+}
+
+#[no_mangle]
+extern "C" fn ampere_board_starting_position() -> Box<CheckersBitBoard> {
+ Box::new(CheckersBitBoard::starting_position())
+}
+
+#[no_mangle]
+extern "C" fn ampere_board_new(
+ pieces: u32,
+ color: u32,
+ kings: u32,
+ turn: PieceColor,
+) -> Box<CheckersBitBoard> {
+ Box::new(CheckersBitBoard::new(pieces, color, kings, turn))
+}
+
+#[no_mangle]
+extern "C" fn ampere_clock_unlimited() -> Box<Clock> {
+ Box::new(Clock::Unlimited)
+}
+
+#[no_mangle]
+extern "C" fn ampere_clock_timepermove(millis: c_int) -> Box<Clock> {
+ Box::new(Clock::TimePerMove(Duration::from_millis(millis as u64)))
+}
+
+#[no_mangle]
+extern "C" fn ampere_clock_incremental(
+ white_time: c_int,
+ black_time: c_int,
+ white_inc: c_int,
+ black_inc: c_int,
+ moves_to_time_control: c_int,
+ time_control: c_int,
+) -> Box<Clock> {
+ let moves_until_next_time_control = if time_control == 0 {
+ None
+ } else {
+ Some((
+ moves_to_time_control as u32,
+ Duration::from_millis(time_control as u64),
+ ))
+ };
+
+ Box::new(Clock::Incremental {
+ white_time_remaining: Duration::from_millis(white_time as u64),
+ black_time_remaining: Duration::from_millis(black_time as u64),
+ white_increment: Duration::from_millis(white_inc as u64),
+ black_increment: Duration::from_millis(black_inc as u64),
+ moves_until_next_time_control,
+ })
+}
+
+#[no_mangle]
+extern "C" fn ampere_clock_destroy(clock: Box<Clock>) {
+ drop(clock)
+}
+
+#[no_mangle]
+extern "C" fn ampere_board_clone(board: &CheckersBitBoard) -> Box<CheckersBitBoard> {
+ Box::new(*board)
+}
+
+#[no_mangle]
+extern "C" fn ampere_board_equal(a: &CheckersBitBoard, b: &CheckersBitBoard) -> bool {
+ *a == *b
+}
+
+#[no_mangle]
+extern "C" fn ampere_board_hash(board: &CheckersBitBoard) -> u64 {
+ board.hash_code()
+}
+
+#[no_mangle]
+extern "C" fn ampere_board_pieces(board: &mut CheckersBitBoard) -> &mut u32 {
+ &mut board.pieces
+}
+
+#[no_mangle]
+extern "C" fn ampere_board_colors(board: &mut CheckersBitBoard) -> &mut u32 {
+ &mut board.color
+}
+
+#[no_mangle]
+extern "C" fn ampere_board_kings(board: &mut CheckersBitBoard) -> &mut u32 {
+ &mut board.kings
+}
+
+#[no_mangle]
+extern "C" fn ampere_board_turn(board: &mut CheckersBitBoard) -> &mut PieceColor {
+ &mut board.turn
+}
+
+#[no_mangle]
+extern "C" fn ampere_board_has_piece_at(board: &CheckersBitBoard, square: c_int) -> bool {
+ board.piece_at(square as usize)
+}
+
+#[no_mangle]
+unsafe extern "C" fn ampere_board_color_at(board: &CheckersBitBoard, square: c_int) -> PieceColor {
+ board.color_at_unchecked(square as usize)
+}
+
+#[no_mangle]
+unsafe extern "C" fn ampere_board_king_at(board: &CheckersBitBoard, square: c_int) -> bool {
+ board.king_at_unchecked(square as usize)
+}
+
+#[no_mangle]
+unsafe extern "C" fn ampere_board_move_piece(
+ board: &mut CheckersBitBoard,
+ start: c_int,
+ dest: c_int,
+) {
+ *board = board.move_piece_to_unchecked(start as usize, dest as usize);
+}
+
+#[no_mangle]
+extern "C" fn ampere_board_clear_piece(board: &mut CheckersBitBoard, square: c_int) {
+ *board = board.clear_piece(square as usize);
+}
+
+#[no_mangle]
+extern "C" fn ampere_board_destroy(board: Box<CheckersBitBoard>) {
+ drop(board)
+}
+
+#[no_mangle]
+extern "C" fn ampere_eval_is_force_win(evaluation: &Evaluation) -> bool {
+ evaluation.is_force_win()
+}
+
+#[no_mangle]
+extern "C" fn ampere_eval_is_force_loss(evaluation: &Evaluation) -> bool {
+ evaluation.is_force_loss()
+}
+
+#[no_mangle]
+extern "C" fn ampere_eval_is_force_seq(evaluation: &Evaluation) -> bool {
+ evaluation.is_force_sequence()
+}
+
+#[no_mangle]
+unsafe extern "C" fn ampere_eval_forceseq_len(evaluation: &Evaluation) -> c_int {
+ evaluation.force_sequence_length().unwrap_unchecked() as c_int
+}
+
+#[no_mangle]
+unsafe extern "C" fn ampere_eval_tofloat(evaluation: &Evaluation) -> f32 {
+ evaluation.to_f32_unchecked()
+}
+
+#[no_mangle]
+extern "C" fn ampere_eval_destroy(evaluation: Box<Evaluation>) {
+ drop(evaluation)
+}
+
+#[no_mangle]
+extern "C" fn ampere_move_new(start: c_int, direction: MoveDirection, jump: bool) -> Box<Move> {
+ Box::new(Move::new(start as usize, direction, jump))
+}
+
+#[no_mangle]
+extern "C" fn ampere_move_clone(ampere_move: &Move) -> Box<Move> {
+ Box::new(*ampere_move)
+}
+
+#[no_mangle]
+extern "C" fn ampere_move_equal(a: &Move, b: &Move) -> bool {
+ *a == *b
+}
+
+#[no_mangle]
+unsafe extern "C" fn ampere_move_string(m: &Move, buffer: *mut u8) {
+ let buffer = std::slice::from_raw_parts_mut(buffer, 6);
+ let string = CString::new(m.to_string().as_bytes()).unwrap_unchecked();
+ let bytes = string.as_bytes_with_nul();
+ buffer[..bytes.len()].copy_from_slice(bytes)
+}
+
+#[no_mangle]
+extern "C" fn ampere_move_start(ampere_move: &Move) -> c_int {
+ ampere_move.start() as c_int
+}
+
+#[no_mangle]
+extern "C" fn ampere_move_direction(ampere_move: &Move) -> MoveDirection {
+ ampere_move.direction()
+}
+
+#[no_mangle]
+extern "C" fn ampere_move_is_jump(ampere_move: &Move) -> bool {
+ ampere_move.is_jump()
+}
+
+#[no_mangle]
+unsafe extern "C" fn ampere_move_jump_position(ampere_move: &Move) -> c_int {
+ ampere_move.jump_position() as c_int
+}
+
+#[no_mangle]
+extern "C" fn ampere_move_end(ampere_move: &Move) -> c_int {
+ ampere_move.end_position() as c_int
+}
+
+#[no_mangle]
+extern "C" fn ampere_move_destroy(ampere_move: Box<Move>) {
+ drop(ampere_move)
+}
diff --git a/engine/src/engine.rs b/engine/src/engine.rs
index 6402f21..479e0ef 100644..100755
--- a/engine/src/engine.rs
+++ b/engine/src/engine.rs
@@ -1,273 +1,275 @@
-use std::num::{NonZeroU8, NonZeroUsize};
-use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
-use std::sync::Arc;
-use std::thread::JoinHandle;
-use std::time::Duration;
-
-use model::{CheckersBitBoard, Move, PieceColor, PossibleMoves};
-use parking_lot::Mutex;
-
-use crate::eval::Evaluation;
-use crate::search::search;
-use crate::{TranspositionTable, TranspositionTableRef};
-
-pub const ENGINE_NAME: &str = "Ampere";
-pub const ENGINE_AUTHOR: &str = "Mica White";
-pub const ENGINE_ABOUT: &str = "Ampere Checkers Bot v1.0\nCopyright Mica White";
-
-type EvalThread = JoinHandle<(Evaluation, Option<Move>)>;
-
-pub struct Engine<'a> {
- position: Mutex<CheckersBitBoard>,
- transposition_table: TranspositionTable,
-
- debug: AtomicBool,
- frontend: &'a dyn Frontend,
-
- current_thread: Mutex<Option<EvalThread>>,
- current_task: Mutex<Option<Arc<EvaluationTask<'a>>>>,
- pondering_task: Mutex<Option<Arc<EvaluationTask<'a>>>>,
-}
-
-pub struct EvaluationTask<'a> {
- pub position: CheckersBitBoard,
- pub transposition_table: TranspositionTableRef<'a>,
- pub allowed_moves: Option<Arc<[Move]>>,
- pub limits: ActualLimit,
- pub ponder: bool,
- pub cancel_flag: AtomicBool,
- pub end_ponder_flag: AtomicBool,
-
- pub nodes_explored: AtomicUsize,
-}
-
-#[derive(Debug, Default, Clone)]
-pub struct EvaluationSettings {
- pub restrict_moves: Option<Arc<[Move]>>,
- pub ponder: bool,
- pub clock: Clock,
- pub search_until: SearchLimit,
-}
-
-impl EvaluationSettings {
- fn get_limits(&self, this_color: PieceColor) -> ActualLimit {
- match &self.search_until {
- SearchLimit::Infinite => ActualLimit::default(),
- SearchLimit::Limited(limit) => *limit,
- SearchLimit::Auto => ActualLimit {
- nodes: None,
- depth: NonZeroU8::new(30),
- time: Some(self.clock.recommended_time(this_color)),
- },
- }
- }
-}
-
-#[derive(Debug, Clone)]
-pub enum Clock {
- Unlimited,
- TimePerMove(Duration),
- Standard {
- white_time_remaining: Duration,
- black_time_remaining: Duration,
- white_increment: Duration,
- black_increment: Duration,
- moves_until_next_time_control: Option<(u32, Duration)>,
- },
-}
-
-impl Clock {
- fn recommended_time(&self, this_color: PieceColor) -> Duration {
- match self {
- Self::Unlimited => Duration::from_secs(60 * 5), // 5 minutes
- Self::TimePerMove(time) => *time,
- Self::Standard {
- white_time_remaining,
- black_time_remaining,
- white_increment,
- black_increment,
- moves_until_next_time_control,
- } => {
- let my_time = match this_color {
- PieceColor::Dark => black_time_remaining,
- PieceColor::Light => white_time_remaining,
- };
- let my_increment = match this_color {
- PieceColor::Dark => black_increment,
- PieceColor::Light => white_increment,
- };
-
- // TODO this could certainly be better
- let moves_to_go = moves_until_next_time_control.map(|m| m.0).unwrap_or(50);
-
- (my_time.checked_div(moves_to_go).unwrap_or(*my_time) + *my_increment).div_f32(1.25)
- }
- }
- }
-}
-
-impl Default for Clock {
- fn default() -> Self {
- Self::TimePerMove(Duration::from_secs(60 * 5))
- }
-}
-
-#[derive(Debug, Default, Clone)]
-pub enum SearchLimit {
- #[default]
- Auto,
- Infinite,
- Limited(ActualLimit),
-}
-
-#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
-#[repr(C)]
-pub struct ActualLimit {
- pub nodes: Option<NonZeroUsize>,
- pub depth: Option<NonZeroU8>,
- pub time: Option<Duration>,
-}
-
-pub trait Frontend: Sync {
- fn debug(&self, msg: &str);
-
- fn report_best_move(&self, best_move: Move);
-}
-
-impl<'a> Engine<'a> {
- pub fn new(transposition_table_size: usize, frontend: &'a dyn Frontend) -> Self {
- Self {
- position: Mutex::new(CheckersBitBoard::starting_position()),
- transposition_table: TranspositionTable::new(transposition_table_size),
-
- debug: AtomicBool::new(false),
- frontend,
-
- current_thread: Mutex::new(None),
- current_task: Mutex::new(None),
- pondering_task: Mutex::new(None),
- }
- }
-
- pub fn set_debug(&self, debug: bool) {
- self.debug.store(debug, Ordering::Release);
- }
-
- pub fn is_legal_move(&self, checker_move: Move) -> bool {
- let position = self.position.lock();
- PossibleMoves::moves(*position).contains(checker_move)
- }
-
- pub fn current_position(&self) -> CheckersBitBoard {
- *self.position.lock()
- }
-
- pub fn reset_position(&self) {
- self.set_position(CheckersBitBoard::starting_position())
- }
-
- pub fn set_position(&self, position: CheckersBitBoard) {
- let mut position_ptr = self.position.lock();
- *position_ptr = position;
- }
-
- pub fn apply_move(&self, checker_move: Move) -> Option<()> {
- unsafe {
- if self.is_legal_move(checker_move) {
- let mut position = self.position.lock();
- *position = checker_move.apply_to(*position);
- Some(())
- } else {
- None
- }
- }
- }
-
- pub fn evaluate(
- &self,
- cancel: Option<&AtomicBool>,
- settings: EvaluationSettings,
- ) -> (Evaluation, Option<Move>) {
- // finish the pondering thread
- let mut pondering_task = self.pondering_task.lock();
- if let Some(task) = pondering_task.take() {
- task.end_ponder_flag.store(true, Ordering::Release);
- }
-
- let position = *self.position.lock();
- let transposition_table = self.transposition_table.get_ref();
- let limits = settings.get_limits(position.turn());
- let allowed_moves = settings.restrict_moves;
- let cancel_flag = AtomicBool::new(false);
- let end_ponder_flag = AtomicBool::new(false);
-
- let nodes_explored = AtomicUsize::new(0);
-
- let task = EvaluationTask {
- position,
- transposition_table,
- allowed_moves,
- limits,
- ponder: false,
- cancel_flag,
- end_ponder_flag,
-
- nodes_explored,
- };
-
- search(Arc::new(task), self.frontend, cancel)
- }
-
- pub fn start_evaluation(&'static self, settings: EvaluationSettings) {
- // finish the pondering thread
- let mut pondering_task = self.pondering_task.lock();
- if let Some(task) = pondering_task.take() {
- task.end_ponder_flag.store(true, Ordering::Release);
- }
-
- let position = *self.position.lock();
- let transposition_table = self.transposition_table.get_ref();
- let limits = settings.get_limits(position.turn());
- let allowed_moves = settings.restrict_moves;
- let ponder = settings.ponder;
- let cancel_flag = AtomicBool::new(false);
- let end_ponder_flag = AtomicBool::new(false);
-
- let nodes_explored = AtomicUsize::new(0);
-
- let task = EvaluationTask {
- position,
- transposition_table,
- allowed_moves,
- limits,
- ponder,
- cancel_flag,
- end_ponder_flag,
-
- nodes_explored,
- };
-
- let task = Arc::new(task);
- let task_ref = task.clone();
- let mut task_ptr = self.current_task.lock();
- *task_ptr = Some(task);
-
- if ponder {
- let mut pondering_task = self.pondering_task.lock();
- *pondering_task = Some(task_ref.clone());
- }
-
- let thread = std::thread::spawn(move || search(task_ref, self.frontend, None));
- let mut thread_ptr = self.current_thread.lock();
- *thread_ptr = Some(thread);
- }
-
- pub fn stop_evaluation(&self) -> Option<()> {
- let current_task = self.current_task.lock().take()?;
- current_task.cancel_flag.store(true, Ordering::Release);
-
- let _ = self.current_thread.lock().take()?.join();
-
- Some(())
- }
-}
+use std::num::{NonZeroU8, NonZeroUsize};
+use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
+use std::sync::Arc;
+use std::thread::JoinHandle;
+use std::time::Duration;
+
+use model::{CheckersBitBoard, Move, PieceColor, PossibleMoves};
+use parking_lot::Mutex;
+
+use crate::eval::Evaluation;
+use crate::search::search;
+use crate::{EvalInfo, TranspositionTable, TranspositionTableRef};
+
+pub const ENGINE_NAME: &str = "Ampere";
+pub const ENGINE_AUTHOR: &str = "Mica White";
+pub const ENGINE_ABOUT: &str = "Ampere Checkers Bot v1.0\nCopyright Mica White";
+
+type EvalThread = JoinHandle<(Evaluation, Option<Move>)>;
+
+pub struct Engine<'a> {
+ position: Mutex<CheckersBitBoard>,
+ transposition_table: TranspositionTable,
+
+ debug: AtomicBool,
+ frontend: &'a dyn Frontend,
+
+ current_thread: Mutex<Option<EvalThread>>,
+ current_task: Mutex<Option<Arc<EvaluationTask<'a>>>>,
+ pondering_task: Mutex<Option<Arc<EvaluationTask<'a>>>>,
+}
+
+pub struct EvaluationTask<'a> {
+ pub position: CheckersBitBoard,
+ pub transposition_table: TranspositionTableRef<'a>,
+ pub allowed_moves: Option<Arc<[Move]>>,
+ pub limits: ActualLimit,
+ pub ponder: bool,
+ pub cancel_flag: AtomicBool,
+ pub end_ponder_flag: AtomicBool,
+
+ pub nodes_explored: AtomicUsize,
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct EvaluationSettings {
+ pub restrict_moves: Option<Arc<[Move]>>,
+ pub ponder: bool,
+ pub clock: Clock,
+ pub search_until: SearchLimit,
+}
+
+impl EvaluationSettings {
+ fn get_limits(&self, this_color: PieceColor) -> ActualLimit {
+ match &self.search_until {
+ SearchLimit::Infinite => ActualLimit::default(),
+ SearchLimit::Limited(limit) => *limit,
+ SearchLimit::Auto => ActualLimit {
+ nodes: None,
+ depth: NonZeroU8::new(30),
+ time: Some(self.clock.recommended_time(this_color)),
+ },
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub enum Clock {
+ Unlimited,
+ TimePerMove(Duration),
+ Incremental {
+ white_time_remaining: Duration,
+ black_time_remaining: Duration,
+ white_increment: Duration,
+ black_increment: Duration,
+ moves_until_next_time_control: Option<(u32, Duration)>,
+ },
+}
+
+impl Clock {
+ pub(crate) fn recommended_time(&self, this_color: PieceColor) -> Duration {
+ match self {
+ Self::Unlimited => Duration::from_secs(60 * 5), // 5 minutes
+ Self::TimePerMove(time) => time.div_f32(2.0),
+ Self::Incremental {
+ white_time_remaining,
+ black_time_remaining,
+ white_increment,
+ black_increment,
+ moves_until_next_time_control,
+ } => {
+ let my_time = match this_color {
+ PieceColor::Dark => black_time_remaining,
+ PieceColor::Light => white_time_remaining,
+ };
+ let my_increment = match this_color {
+ PieceColor::Dark => black_increment,
+ PieceColor::Light => white_increment,
+ };
+
+ // TODO this could certainly be better
+ let moves_to_go = moves_until_next_time_control.map(|m| m.0).unwrap_or(50);
+
+ my_time.checked_div(moves_to_go * 2).unwrap_or(*my_time) + *my_increment
+ }
+ }
+ }
+}
+
+impl Default for Clock {
+ fn default() -> Self {
+ Self::TimePerMove(Duration::from_secs(60 * 5))
+ }
+}
+
+#[derive(Debug, Default, Clone)]
+pub enum SearchLimit {
+ #[default]
+ Auto,
+ Infinite,
+ Limited(ActualLimit),
+}
+
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
+#[repr(C)]
+pub struct ActualLimit {
+ pub nodes: Option<NonZeroUsize>,
+ pub depth: Option<NonZeroU8>,
+ pub time: Option<Duration>,
+}
+
+pub trait Frontend: Sync {
+ fn debug(&self, msg: &str);
+
+ fn info(&self, info: EvalInfo);
+
+ fn report_best_move(&self, best_move: Move);
+}
+
+impl<'a> Engine<'a> {
+ pub fn new(transposition_table_size: usize, frontend: &'a dyn Frontend) -> Self {
+ Self {
+ position: Mutex::new(CheckersBitBoard::starting_position()),
+ transposition_table: TranspositionTable::new(transposition_table_size),
+
+ debug: AtomicBool::new(false),
+ frontend,
+
+ current_thread: Mutex::new(None),
+ current_task: Mutex::new(None),
+ pondering_task: Mutex::new(None),
+ }
+ }
+
+ pub fn set_debug(&self, debug: bool) {
+ self.debug.store(debug, Ordering::Release);
+ }
+
+ pub fn is_legal_move(&self, checker_move: Move) -> bool {
+ let position = self.position.lock();
+ PossibleMoves::moves(*position).contains(checker_move)
+ }
+
+ pub fn current_position(&self) -> CheckersBitBoard {
+ *self.position.lock()
+ }
+
+ pub fn reset_position(&self) {
+ self.set_position(CheckersBitBoard::starting_position())
+ }
+
+ pub fn set_position(&self, position: CheckersBitBoard) {
+ let mut position_ptr = self.position.lock();
+ *position_ptr = position;
+ }
+
+ pub fn apply_move(&self, checker_move: Move) -> Option<()> {
+ unsafe {
+ if self.is_legal_move(checker_move) {
+ let mut position = self.position.lock();
+ *position = checker_move.apply_to(*position);
+ Some(())
+ } else {
+ None
+ }
+ }
+ }
+
+ pub fn evaluate(
+ &self,
+ cancel: Option<&AtomicBool>,
+ settings: EvaluationSettings,
+ ) -> (Evaluation, Option<Move>) {
+ // finish the pondering thread
+ let mut pondering_task = self.pondering_task.lock();
+ if let Some(task) = pondering_task.take() {
+ task.end_ponder_flag.store(true, Ordering::Release);
+ }
+
+ let position = *self.position.lock();
+ let transposition_table = self.transposition_table.get_ref();
+ let limits = settings.get_limits(position.turn());
+ let allowed_moves = settings.restrict_moves;
+ let cancel_flag = AtomicBool::new(false);
+ let end_ponder_flag = AtomicBool::new(false);
+
+ let nodes_explored = AtomicUsize::new(0);
+
+ let task = EvaluationTask {
+ position,
+ transposition_table,
+ allowed_moves,
+ limits,
+ ponder: false,
+ cancel_flag,
+ end_ponder_flag,
+
+ nodes_explored,
+ };
+
+ search(Arc::new(task), self.frontend, cancel)
+ }
+
+ pub fn start_evaluation(&'static self, settings: EvaluationSettings) {
+ // finish the pondering thread
+ let mut pondering_task = self.pondering_task.lock();
+ if let Some(task) = pondering_task.take() {
+ task.end_ponder_flag.store(true, Ordering::Release);
+ }
+
+ let position = *self.position.lock();
+ let transposition_table = self.transposition_table.get_ref();
+ let limits = settings.get_limits(position.turn());
+ let allowed_moves = settings.restrict_moves;
+ let ponder = settings.ponder;
+ let cancel_flag = AtomicBool::new(false);
+ let end_ponder_flag = AtomicBool::new(false);
+
+ let nodes_explored = AtomicUsize::new(0);
+
+ let task = EvaluationTask {
+ position,
+ transposition_table,
+ allowed_moves,
+ limits,
+ ponder,
+ cancel_flag,
+ end_ponder_flag,
+
+ nodes_explored,
+ };
+
+ let task = Arc::new(task);
+ let task_ref = task.clone();
+ let mut task_ptr = self.current_task.lock();
+ *task_ptr = Some(task);
+
+ if ponder {
+ let mut pondering_task = self.pondering_task.lock();
+ *pondering_task = Some(task_ref.clone());
+ }
+
+ let thread = std::thread::spawn(move || search(task_ref, self.frontend, None));
+ let mut thread_ptr = self.current_thread.lock();
+ *thread_ptr = Some(thread);
+ }
+
+ pub fn stop_evaluation(&self) -> Option<()> {
+ let current_task = self.current_task.lock().take()?;
+ current_task.cancel_flag.store(true, Ordering::Release);
+
+ let _ = self.current_thread.lock().take()?.join();
+
+ Some(())
+ }
+}
diff --git a/engine/src/eval.rs b/engine/src/eval.rs
index 94849ce..a666913 100644..100755
--- a/engine/src/eval.rs
+++ b/engine/src/eval.rs
@@ -1,160 +1,171 @@
-use std::fmt::{self, Display};
-use std::ops::Neg;
-
-use model::CheckersBitBoard;
-
-const KING_WORTH: u32 = 2;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub struct Evaluation(i16);
-
-impl Display for Evaluation {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- if self.is_force_win() {
- write!(f, "+M{}", self.force_sequence_length().unwrap())
- } else if self.is_force_loss() {
- write!(f, "-M{}", self.force_sequence_length().unwrap())
- } else {
- write!(f, "{:+}", self.to_f32().unwrap())
- }
- }
-}
-
-impl Neg for Evaluation {
- type Output = Self;
-
- fn neg(self) -> Self::Output {
- Self(-self.0)
- }
-}
-
-impl Evaluation {
- pub(crate) const NULL_MAX: Self = Self(i16::MAX);
- pub(crate) const NULL_MIN: Self = Self(i16::MIN + 1);
-
- pub const WIN: Self = Self(i16::MAX - 1);
- pub const DRAW: Self = Self(0);
- pub const LOSS: Self = Self(i16::MIN + 2);
-
- // last fourteen bits set to 1
- const FORCE_WIN_THRESHOLD: i16 = 0x3FFF;
-
- pub fn new(eval: f32) -> Self {
- if eval >= 1.0 {
- return Self::WIN;
- } else if eval <= -1.0 {
- return Self::LOSS;
- }
-
- Self((eval * 16384.0) as i16)
- }
-
- pub fn to_f32(self) -> Option<f32> {
- if self.is_force_sequence() {
- return None;
- }
-
- Some(self.0 as f32 / 16384.0)
- }
-
- pub fn is_force_win(self) -> bool {
- self.0 > Self::FORCE_WIN_THRESHOLD
- }
-
- pub fn is_force_loss(self) -> bool {
- self.0 < -Self::FORCE_WIN_THRESHOLD
- }
-
- pub fn is_force_sequence(self) -> bool {
- self.is_force_win() || self.is_force_loss()
- }
-
- pub fn force_sequence_length(self) -> Option<u8> {
- if self == Self::NULL_MAX || self == Self::NULL_MIN {
- return None;
- }
-
- if self.is_force_win() {
- Some((Self::WIN.0 - self.0) as u8)
- } else if self.is_force_loss() {
- Some((self.0 - Self::LOSS.0) as u8)
- } else {
- None
- }
- }
-
- pub fn increment(self) -> Self {
- if self.is_force_win() {
- Self(self.0 - 1)
- } else if self.is_force_loss() {
- Self(self.0 + 1)
- } else {
- self
- }
- }
-
- pub fn add_f32(self, rhs: f32) -> Self {
- let Some(eval) = self.to_f32() else {
- return self;
- };
-
- Self::new(eval + rhs)
- }
-}
-
-pub fn eval_position(board: CheckersBitBoard) -> Evaluation {
- let light_pieces = board.pieces_bits() & !board.color_bits();
- let dark_pieces = board.pieces_bits() & board.color_bits();
-
- let light_peasants = light_pieces & !board.king_bits();
- let dark_peasants = dark_pieces & !board.king_bits();
-
- let light_kings = light_pieces & board.king_bits();
- let dark_kings = dark_pieces & board.king_bits();
-
- // if we assume the black player doesn't exist, how good is this for white?
- let light_eval =
- (light_peasants.count_ones() as f32) + ((light_kings.count_ones() * KING_WORTH) as f32);
- let dark_eval =
- (dark_peasants.count_ones() as f32) + ((dark_kings.count_ones() * KING_WORTH) as f32);
-
- // avoiding a divide by zero error
- if dark_eval + light_eval != 0.0 {
- Evaluation::new((dark_eval - light_eval) / (dark_eval + light_eval))
- } else {
- Evaluation::DRAW
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn zero_eval() {
- let draw = Evaluation::new(0.0);
- assert_eq!(draw, Evaluation::DRAW);
- assert_eq!(draw.to_f32(), Some(0.0));
- assert_eq!(draw.to_string(), "+0");
- }
-
- #[test]
- fn comparisons() {
- assert!(Evaluation::NULL_MAX > Evaluation::WIN);
- assert!(Evaluation::WIN > Evaluation::new(0.5));
- assert!(Evaluation::new(0.5) > Evaluation::DRAW);
- assert!(Evaluation::DRAW > Evaluation::new(-0.5));
- assert!(Evaluation::new(-0.5) > Evaluation::LOSS);
- assert!(Evaluation::LOSS > Evaluation::NULL_MIN);
- }
-
- #[test]
- fn negations() {
- assert_eq!(-Evaluation::NULL_MAX, Evaluation::NULL_MIN);
- assert_eq!(-Evaluation::NULL_MIN, Evaluation::NULL_MAX);
- assert_eq!(-Evaluation::WIN, Evaluation::LOSS);
- assert_eq!(-Evaluation::LOSS, Evaluation::WIN);
- assert_eq!(-Evaluation::DRAW, Evaluation::DRAW);
- assert_eq!(-Evaluation::new(0.5), Evaluation::new(-0.5));
- }
-}
+use std::fmt::{self, Display};
+use std::ops::Neg;
+
+use model::CheckersBitBoard;
+
+const KING_WORTH: u32 = 2;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Evaluation(i16);
+
+impl Display for Evaluation {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ if self.is_force_win() {
+ write!(f, "+W{}", self.force_sequence_length().unwrap())
+ } else if self.is_force_loss() {
+ write!(f, "-W{}", self.force_sequence_length().unwrap())
+ } else {
+ write!(f, "{:+}", self.to_f32().unwrap())
+ }
+ }
+}
+
+impl Neg for Evaluation {
+ type Output = Self;
+
+ fn neg(self) -> Self::Output {
+ Self(-self.0)
+ }
+}
+
+impl Evaluation {
+ pub(crate) const NULL_MAX: Self = Self(i16::MAX);
+ pub(crate) const NULL_MIN: Self = Self(i16::MIN + 1);
+
+ pub const WIN: Self = Self(i16::MAX - 1);
+ pub const DRAW: Self = Self(0);
+ pub const LOSS: Self = Self(i16::MIN + 2);
+
+ // last fourteen bits set to 1
+ const FORCE_WIN_THRESHOLD: i16 = 0x3FFF;
+ // divisor for converting to a float
+ const MAX_FLOAT: f32 = 16384.0;
+
+ pub fn new(eval: f32) -> Self {
+ if eval >= 1.0 {
+ return Self::WIN;
+ } else if eval <= -1.0 {
+ return Self::LOSS;
+ }
+
+ Self((eval * 16384.0) as i16)
+ }
+
+ pub fn to_f32(self) -> Option<f32> {
+ if self.is_force_sequence() {
+ return None;
+ }
+
+ Some(self.0 as f32 / Self::MAX_FLOAT)
+ }
+
+ /// Converts to an `f32` without checking to see if the game if a force
+ /// sequence.
+ ///
+ /// # Safety
+ /// Results in undefined behavior if the evaluation is a force sequence
+ pub unsafe fn to_f32_unchecked(self) -> f32 {
+ self.0 as f32 / Self::MAX_FLOAT
+ }
+
+ pub fn is_force_win(self) -> bool {
+ self.0 > Self::FORCE_WIN_THRESHOLD
+ }
+
+ pub fn is_force_loss(self) -> bool {
+ self.0 < -Self::FORCE_WIN_THRESHOLD
+ }
+
+ pub fn is_force_sequence(self) -> bool {
+ self.is_force_win() || self.is_force_loss()
+ }
+
+ pub fn force_sequence_length(self) -> Option<u8> {
+ if self == Self::NULL_MAX || self == Self::NULL_MIN {
+ return None;
+ }
+
+ if self.is_force_win() {
+ Some((Self::WIN.0 - self.0) as u8)
+ } else if self.is_force_loss() {
+ Some((self.0 - Self::LOSS.0) as u8)
+ } else {
+ None
+ }
+ }
+
+ pub fn increment(self) -> Self {
+ if self.is_force_win() {
+ Self(self.0 - 1)
+ } else if self.is_force_loss() {
+ Self(self.0 + 1)
+ } else {
+ self
+ }
+ }
+
+ pub fn add_f32(self, rhs: f32) -> Self {
+ let Some(eval) = self.to_f32() else {
+ return self;
+ };
+
+ Self::new(eval + rhs)
+ }
+}
+
+pub fn eval_position(board: CheckersBitBoard) -> Evaluation {
+ let light_pieces = board.pieces_bits() & !board.color_bits();
+ let dark_pieces = board.pieces_bits() & board.color_bits();
+
+ let light_peasants = light_pieces & !board.king_bits();
+ let dark_peasants = dark_pieces & !board.king_bits();
+
+ let light_kings = light_pieces & board.king_bits();
+ let dark_kings = dark_pieces & board.king_bits();
+
+ // if we assume the black player doesn't exist, how good is this for white?
+ let light_eval =
+ (light_peasants.count_ones() as f32) + ((light_kings.count_ones() * KING_WORTH) as f32);
+ let dark_eval =
+ (dark_peasants.count_ones() as f32) + ((dark_kings.count_ones() * KING_WORTH) as f32);
+
+ // avoiding a divide by zero error
+ if dark_eval + light_eval != 0.0 {
+ Evaluation::new((dark_eval - light_eval) / (dark_eval + light_eval))
+ } else {
+ Evaluation::DRAW
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn zero_eval() {
+ let draw = Evaluation::new(0.0);
+ assert_eq!(draw, Evaluation::DRAW);
+ assert_eq!(draw.to_f32(), Some(0.0));
+ assert_eq!(draw.to_string(), "+0");
+ }
+
+ #[test]
+ fn comparisons() {
+ assert!(Evaluation::NULL_MAX > Evaluation::WIN);
+ assert!(Evaluation::WIN > Evaluation::new(0.5));
+ assert!(Evaluation::new(0.5) > Evaluation::DRAW);
+ assert!(Evaluation::DRAW > Evaluation::new(-0.5));
+ assert!(Evaluation::new(-0.5) > Evaluation::LOSS);
+ assert!(Evaluation::LOSS > Evaluation::NULL_MIN);
+ }
+
+ #[test]
+ fn negations() {
+ assert_eq!(-Evaluation::NULL_MAX, Evaluation::NULL_MIN);
+ assert_eq!(-Evaluation::NULL_MIN, Evaluation::NULL_MAX);
+ assert_eq!(-Evaluation::WIN, Evaluation::LOSS);
+ assert_eq!(-Evaluation::LOSS, Evaluation::WIN);
+ assert_eq!(-Evaluation::DRAW, Evaluation::DRAW);
+ assert_eq!(-Evaluation::new(0.5), Evaluation::new(-0.5));
+ }
+}
diff --git a/engine/src/info.rs b/engine/src/info.rs
new file mode 100755
index 0000000..4588941
--- /dev/null
+++ b/engine/src/info.rs
@@ -0,0 +1,27 @@
+use std::marker::PhantomData;
+use std::time::Instant;
+
+use model::Move;
+
+use crate::Evaluation;
+
+#[derive(Debug, Clone, Copy)]
+pub struct EvalInfo {
+ pub start_time: Instant,
+ pub nodes_searched: usize,
+ pub evaluation: Evaluation,
+ pub current_best_move: Option<Move>,
+ pub current_depth: u8,
+ pub(crate) _unused: PhantomData<()>,
+}
+
+impl EvalInfo {
+ pub fn nodes_per_second(&self) -> usize {
+ let elapsed = self.start_time.elapsed().as_secs_f64();
+ (self.nodes_searched as f64 / elapsed) as usize
+ }
+
+ pub fn elapsed_milliseconds(self) -> u32 {
+ self.start_time.elapsed().as_millis() as u32
+ }
+}
diff --git a/engine/src/lazysort.rs b/engine/src/lazysort.rs
index f028778..9d54fe5 100644..100755
--- a/engine/src/lazysort.rs
+++ b/engine/src/lazysort.rs
@@ -1,87 +1,87 @@
-use arrayvec::ArrayVec;
-
-pub struct LazySort<T: Clone, F: Fn(&T) -> R, R: Ord, const CAPACITY: usize> {
- collection: ArrayVec<T, CAPACITY>,
- sorted: usize,
- sort_by: F,
-}
-
-pub struct LazySortIter<T: Clone, F: Fn(&T) -> R, R: Ord, const CAPACITY: usize> {
- sorter: LazySort<T, F, R, CAPACITY>,
- index: usize,
-}
-
-impl<T: Clone, F: Fn(&T) -> R, R: Ord, const CAPACITY: usize> LazySort<T, F, R, CAPACITY> {
- pub fn new(collection: impl IntoIterator<Item = T>, sort_by: F) -> Self {
- Self {
- collection: collection.into_iter().collect(),
- sort_by,
- sorted: 0,
- }
- }
-
- pub fn is_empty(&self) -> bool {
- self.collection.is_empty()
- }
-}
-
-impl<T: Clone, F: Fn(&T) -> R, R: Ord, const CAPACITY: usize> LazySort<T, F, R, CAPACITY> {
- fn sort(&mut self, index: usize) {
- let mut min: Option<R> = None;
- let mut min_index = None;
- for i in index..self.collection.len() {
- if let Some(min) = &mut min {
- let res = (self.sort_by)(&self.collection[i]);
- if res < *min {
- *min = res;
- min_index = Some(i);
- }
- }
- }
-
- if let Some(min_index) = min_index {
- self.collection.swap(index, min_index);
- }
- }
-
- fn sort_between(&mut self, start: usize, end: usize) {
- for i in start..=end {
- self.sort(i);
- }
- }
-
- pub fn get(&mut self, index: usize) -> Option<&T> {
- if index >= self.sorted {
- self.sort_between(self.sorted, index);
- self.sorted = index;
- }
-
- self.collection.get(index)
- }
-}
-
-impl<T: Copy, F: Fn(&T) -> R, R: Ord, const CAPACITY: usize> IntoIterator
- for LazySort<T, F, R, CAPACITY>
-{
- type IntoIter = LazySortIter<T, F, R, CAPACITY>;
- type Item = T;
-
- fn into_iter(self) -> Self::IntoIter {
- LazySortIter {
- sorter: self,
- index: 0,
- }
- }
-}
-
-impl<T: Copy, F: Fn(&T) -> R, R: Ord, const CAPACITY: usize> Iterator
- for LazySortIter<T, F, R, CAPACITY>
-{
- type Item = T;
-
- fn next(&mut self) -> Option<Self::Item> {
- let r = self.sorter.get(self.index);
- self.index += 1;
- r.cloned()
- }
-}
+use arrayvec::ArrayVec;
+
+pub struct LazySort<T: Clone, F: Fn(&T) -> R, R: Ord, const CAPACITY: usize> {
+ collection: ArrayVec<T, CAPACITY>,
+ sorted: usize,
+ sort_by: F,
+}
+
+pub struct LazySortIter<T: Clone, F: Fn(&T) -> R, R: Ord, const CAPACITY: usize> {
+ sorter: LazySort<T, F, R, CAPACITY>,
+ index: usize,
+}
+
+impl<T: Clone, F: Fn(&T) -> R, R: Ord, const CAPACITY: usize> LazySort<T, F, R, CAPACITY> {
+ pub fn new(collection: impl IntoIterator<Item = T>, sort_by: F) -> Self {
+ Self {
+ collection: collection.into_iter().collect(),
+ sort_by,
+ sorted: 0,
+ }
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.collection.is_empty()
+ }
+}
+
+impl<T: Clone, F: Fn(&T) -> R, R: Ord, const CAPACITY: usize> LazySort<T, F, R, CAPACITY> {
+ fn sort(&mut self, index: usize) {
+ let mut min: Option<R> = None;
+ let mut min_index = None;
+ for i in index..self.collection.len() {
+ if let Some(min) = &mut min {
+ let res = (self.sort_by)(&self.collection[i]);
+ if res < *min {
+ *min = res;
+ min_index = Some(i);
+ }
+ }
+ }
+
+ if let Some(min_index) = min_index {
+ self.collection.swap(index, min_index);
+ }
+ }
+
+ fn sort_between(&mut self, start: usize, end: usize) {
+ for i in start..=end {
+ self.sort(i);
+ }
+ }
+
+ pub fn get(&mut self, index: usize) -> Option<&T> {
+ if index >= self.sorted {
+ self.sort_between(self.sorted, index);
+ self.sorted = index;
+ }
+
+ self.collection.get(index)
+ }
+}
+
+impl<T: Copy, F: Fn(&T) -> R, R: Ord, const CAPACITY: usize> IntoIterator
+ for LazySort<T, F, R, CAPACITY>
+{
+ type IntoIter = LazySortIter<T, F, R, CAPACITY>;
+ type Item = T;
+
+ fn into_iter(self) -> Self::IntoIter {
+ LazySortIter {
+ sorter: self,
+ index: 0,
+ }
+ }
+}
+
+impl<T: Copy, F: Fn(&T) -> R, R: Ord, const CAPACITY: usize> Iterator
+ for LazySortIter<T, F, R, CAPACITY>
+{
+ type Item = T;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let r = self.sorter.get(self.index);
+ self.index += 1;
+ r.cloned()
+ }
+}
diff --git a/engine/src/lib.rs b/engine/src/lib.rs
index d87c225..7c5bd7f 100644..100755
--- a/engine/src/lib.rs
+++ b/engine/src/lib.rs
@@ -1,18 +1,20 @@
-#![feature(new_uninit)]
-#![feature(maybe_uninit_uninit_array)]
-#![feature(maybe_uninit_slice)]
-
-pub use engine::{
- ActualLimit, Clock, Engine, EvaluationSettings, Frontend, SearchLimit, ENGINE_ABOUT,
- ENGINE_AUTHOR, ENGINE_NAME,
-};
-pub use eval::Evaluation;
-pub use model::{CheckersBitBoard, Move, MoveDirection, Piece, PieceColor, PossibleMoves};
-pub use transposition_table::{TranspositionTable, TranspositionTableRef};
-
-pub mod c_abi;
-mod engine;
-mod eval;
-mod lazysort;
-mod search;
-mod transposition_table;
+#![feature(new_uninit)]
+#![feature(maybe_uninit_uninit_array)]
+#![feature(maybe_uninit_slice)]
+
+pub use engine::{
+ ActualLimit, Clock, Engine, EvaluationSettings, Frontend, SearchLimit, ENGINE_ABOUT,
+ ENGINE_AUTHOR, ENGINE_NAME,
+};
+pub use eval::Evaluation;
+pub use info::EvalInfo;
+pub use model::{CheckersBitBoard, Move, MoveDirection, Piece, PieceColor, PossibleMoves};
+pub use transposition_table::{TranspositionTable, TranspositionTableRef};
+
+mod c_abi;
+mod engine;
+mod eval;
+mod info;
+mod lazysort;
+mod search;
+mod transposition_table;
diff --git a/engine/src/main.rs b/engine/src/main.rs
index d4bcc48..187ff89 100644..100755
--- a/engine/src/main.rs
+++ b/engine/src/main.rs
@@ -1,58 +1,83 @@
-use std::num::NonZeroU8;
-
-use engine::{ActualLimit, Engine, EvaluationSettings, Frontend};
-use mimalloc::MiMalloc;
-use model::CheckersBitBoard;
-
-#[global_allocator]
-static ALLOCATOR: MiMalloc = MiMalloc;
-
-const DEPTH: u8 = 19;
-
-struct BasicFrontend;
-
-impl Frontend for BasicFrontend {
- fn debug(&self, msg: &str) {
- println!("{msg}");
- }
-
- fn report_best_move(&self, best_move: model::Move) {
- println!("{best_move}");
- }
-}
-
-fn main() {
- let engine = Box::leak(Box::new(Engine::new(1_000_000, &BasicFrontend)));
- let (_, best) = engine.evaluate(
- None,
- EvaluationSettings {
- restrict_moves: None,
- ponder: false,
- clock: engine::Clock::Unlimited,
- search_until: engine::SearchLimit::Limited(ActualLimit {
- nodes: None,
- depth: Some(NonZeroU8::new(DEPTH).unwrap()),
- time: None,
- }),
- },
- );
- engine.set_position(CheckersBitBoard::new(
- 4294967295,
- 2206409603,
- 3005432691,
- model::PieceColor::Light,
- ));
- engine.evaluate(
- None,
- EvaluationSettings {
- restrict_moves: None,
- ponder: false,
- clock: engine::Clock::Unlimited,
- search_until: engine::SearchLimit::Limited(ActualLimit {
- nodes: None,
- depth: Some(NonZeroU8::new(DEPTH).unwrap()),
- time: None,
- }),
- },
- );
-}
+use std::{num::NonZeroU8, time::Instant};
+
+use engine::{ActualLimit, Engine, EvalInfo, EvaluationSettings, Frontend};
+use mimalloc::MiMalloc;
+use model::CheckersBitBoard;
+
+#[global_allocator]
+static ALLOCATOR: MiMalloc = MiMalloc;
+
+const DEPTH: u8 = 19;
+
+struct BasicFrontend;
+
+impl Frontend for BasicFrontend {
+ fn debug(&self, msg: &str) {
+ println!("{msg}");
+ }
+
+ fn info(&self, _info: EvalInfo) {}
+
+ fn report_best_move(&self, best_move: model::Move) {
+ println!("{best_move}");
+ }
+}
+
+fn main() {
+ let engine = Box::leak(Box::new(Engine::new(1_000_000, &BasicFrontend)));
+ let start = Instant::now();
+ engine.evaluate(
+ None,
+ EvaluationSettings {
+ restrict_moves: None,
+ ponder: false,
+ clock: engine::Clock::Unlimited,
+ search_until: engine::SearchLimit::Limited(ActualLimit {
+ nodes: None,
+ depth: Some(NonZeroU8::new(DEPTH).unwrap()),
+ time: None,
+ }),
+ },
+ );
+ println!("{} ms", start.elapsed().as_millis());
+ engine.set_position(CheckersBitBoard::new(
+ 4294967295,
+ 2206409603,
+ 3005432691,
+ model::PieceColor::Light,
+ ));
+ engine.evaluate(
+ None,
+ EvaluationSettings {
+ restrict_moves: None,
+ ponder: false,
+ clock: engine::Clock::Unlimited,
+ search_until: engine::SearchLimit::Limited(ActualLimit {
+ nodes: None,
+ depth: Some(NonZeroU8::new(DEPTH).unwrap()),
+ time: None,
+ }),
+ },
+ );
+ // TODO test FEN W:W19,20,21,24,25,26,27,28,29,30,32:B1,2,4,6,7,8,9,11,12,15,17,18
+ println!("test");
+ engine.set_position(CheckersBitBoard::new(
+ 3615436253,
+ 75309505,
+ 0,
+ model::PieceColor::Light,
+ ));
+ engine.evaluate(
+ None,
+ EvaluationSettings {
+ restrict_moves: None,
+ ponder: false,
+ clock: engine::Clock::Unlimited,
+ search_until: engine::SearchLimit::Limited(ActualLimit {
+ nodes: None,
+ depth: Some(NonZeroU8::new(DEPTH).unwrap()),
+ time: None,
+ }),
+ },
+ );
+}
diff --git a/engine/src/search.rs b/engine/src/search.rs
index 4326ac6..fd8162a 100644..100755
--- a/engine/src/search.rs
+++ b/engine/src/search.rs
@@ -1,252 +1,278 @@
-use std::num::NonZeroU8;
-use std::sync::{atomic::AtomicBool, Arc};
-use std::time::Instant;
-
-use model::{CheckersBitBoard, Move, PieceColor, PossibleMoves};
-
-use crate::engine::EvaluationTask;
-use crate::Frontend;
-use crate::{
- eval::{eval_position, Evaluation},
- lazysort::LazySort,
- TranspositionTableRef,
-};
-
-unsafe fn sort_moves(
- a: &Move,
- board: CheckersBitBoard,
- table: TranspositionTableRef,
-) -> Evaluation {
- table
- .get_any_depth(a.apply_to(board))
- .unwrap_or(Evaluation::DRAW)
-}
-
-pub fn negamax(
- depth: u8,
- mut alpha: Evaluation,
- beta: Evaluation,
- board: CheckersBitBoard,
- allowed_moves: Option<Arc<[Move]>>,
- cancel_flag: &AtomicBool,
- task: &EvaluationTask,
-) -> (Evaluation, Option<Move>) {
- task.nodes_explored
- .fetch_add(1, std::sync::atomic::Ordering::Release);
-
- if depth < 1 {
- if board.turn() == PieceColor::Dark {
- (eval_position(board), None)
- } else {
- (-eval_position(board), None)
- }
- } else {
- let table = task.transposition_table;
- if let Some((entry, best_move)) = table.get(board, depth) {
- return (entry, Some(best_move));
- }
-
- let turn = board.turn();
- let mut best_eval = Evaluation::NULL_MIN;
- let mut best_move = None;
-
- let sort_fn = |m: &Move| unsafe { sort_moves(m, board, table) };
- let sorter: LazySort<Move, _, Evaluation, { PossibleMoves::MAX_POSSIBLE_MOVES }> =
- if let Some(moves) = allowed_moves {
- LazySort::new(moves.iter().cloned(), sort_fn)
- } else {
- let moves = PossibleMoves::moves(board);
- LazySort::new(moves, sort_fn)
- };
-
- if sorter.is_empty() {
- return (Evaluation::LOSS, None);
- }
-
- for current_move in sorter.into_iter() {
- if cancel_flag.load(std::sync::atomic::Ordering::Acquire) {
- return (best_eval, best_move);
- }
-
- let board = unsafe { current_move.apply_to(board) };
- let current_eval = if board.turn() == turn {
- negamax(depth - 1, alpha, beta, board, None, cancel_flag, task)
- .0
- .increment()
- } else {
- -negamax(depth - 1, -beta, -alpha, board, None, cancel_flag, task)
- .0
- .increment()
- };
-
- if best_eval < current_eval {
- best_eval = current_eval;
- best_move = Some(current_move);
- }
-
- if alpha < best_eval {
- alpha = best_eval;
- }
-
- if alpha >= beta {
- return (best_eval, best_move);
- }
- }
-
- // safety: we already checked that the list isn't empty, so there must
- // be at least one move here
- let best_move = unsafe { best_move.unwrap_unchecked() };
- // safety: in the case of a zero depth, a different branch is taken
- let depth = unsafe { NonZeroU8::new_unchecked(depth) };
- table.insert(board, best_eval, best_move, depth);
-
- (best_eval, Some(best_move))
- }
-}
-
-pub fn search(
- task: Arc<EvaluationTask>,
- frontend: &dyn Frontend,
- cancel: Option<&AtomicBool>,
-) -> (Evaluation, Option<Move>) {
- let board = task.position;
- let cancel_flag = cancel.unwrap_or(&task.cancel_flag);
-
- let allowed_moves = task.allowed_moves.clone();
- let limits = task.limits;
- let max_depth = limits.depth;
- let max_nodes = limits.nodes;
- let max_time = limits.time.map(|d| Instant::now() + d.div_f32(2.0));
-
- let mut alpha = Evaluation::NULL_MIN;
- let mut beta = Evaluation::NULL_MAX;
- let mut depth = 0;
- let mut eval = Evaluation::DRAW;
- let mut best_move = None;
- loop {
- // don't leave search is no good moves have been found
- if best_move.is_some() {
- if let Some(max_depth) = max_depth {
- if depth > max_depth.get() {
- break;
- }
- }
-
- if let Some(max_time) = max_time {
- if Instant::now() > max_time {
- break;
- }
- }
-
- if let Some(max_nodes) = max_nodes {
- if task
- .nodes_explored
- .load(std::sync::atomic::Ordering::Acquire)
- > max_nodes.get()
- {
- break;
- }
- }
- }
-
- let em = negamax(
- depth,
- alpha,
- beta,
- board,
- allowed_moves.clone(),
- cancel_flag,
- &task,
- );
-
- // prevent incomplete search from overwriting evaluation
- if best_move.is_some() && cancel_flag.load(std::sync::atomic::Ordering::Acquire) {
- break;
- }
-
- eval = em.0;
- best_move = em.1;
-
- while (eval <= alpha) || (eval >= beta) {
- let em = negamax(
- depth,
- alpha,
- beta,
- board,
- allowed_moves.clone(),
- cancel_flag,
- &task,
- );
-
- // prevent incomplete search from overwriting evaluation
- if best_move.is_some() && cancel_flag.load(std::sync::atomic::Ordering::Acquire) {
- break;
- }
-
- eval = em.0;
- best_move = em.1;
-
- if eval <= alpha {
- alpha = Evaluation::NULL_MIN;
- } else if eval >= beta {
- beta = Evaluation::NULL_MAX;
- }
- }
-
- if alpha.is_force_loss() {
- alpha = Evaluation::NULL_MIN;
- } else {
- alpha = eval.add_f32(-0.125);
- }
-
- if beta.is_force_win() {
- beta = Evaluation::NULL_MAX;
- } else {
- beta = eval.add_f32(0.125);
- }
-
- if eval.is_force_sequence() {
- // we don't need to search any deeper
- return (eval, best_move);
- }
-
- depth += 1;
- }
-
- // ponder
- if let Some(best_move) = best_move {
- // If the best move has not been found yet, then no move will be
- // reported. This should be very rare. This technically is not allowed
- // by the UCI specification, but if someone stops it this quickly, they
- // probably didn't care about the best move anyway.
- frontend.report_best_move(best_move);
-
- if task.ponder {
- let board = unsafe { best_move.apply_to(board) };
-
- let mut depth = 0;
- loop {
- if task
- .end_ponder_flag
- .load(std::sync::atomic::Ordering::Acquire)
- {
- break;
- }
-
- negamax(
- depth,
- Evaluation::NULL_MIN,
- Evaluation::NULL_MAX,
- board,
- None,
- &task.end_ponder_flag,
- &task,
- );
-
- depth += 1;
- }
- }
- }
-
- (eval, best_move)
-}
+use std::marker::PhantomData;
+use std::num::NonZeroU8;
+use std::sync::{atomic::AtomicBool, Arc};
+use std::time::Instant;
+
+use model::{CheckersBitBoard, Move, PieceColor, PossibleMoves};
+
+use crate::engine::EvaluationTask;
+use crate::{
+ eval::{eval_position, Evaluation},
+ lazysort::LazySort,
+ TranspositionTableRef,
+};
+use crate::{EvalInfo, Frontend};
+
+unsafe fn sort_moves(
+ a: &Move,
+ board: CheckersBitBoard,
+ table: TranspositionTableRef,
+) -> Evaluation {
+ table
+ .get_any_depth(a.apply_to(board))
+ .unwrap_or(Evaluation::DRAW)
+}
+
+pub fn negamax(
+ depth: u8,
+ mut alpha: Evaluation,
+ beta: Evaluation,
+ board: CheckersBitBoard,
+ allowed_moves: Option<Arc<[Move]>>,
+ cancel_flag: &AtomicBool,
+ task: &EvaluationTask,
+) -> (Evaluation, Option<Move>) {
+ task.nodes_explored
+ .fetch_add(1, std::sync::atomic::Ordering::Release);
+
+ if depth < 1 {
+ if board.turn() == PieceColor::Dark {
+ (eval_position(board), None)
+ } else {
+ (-eval_position(board), None)
+ }
+ } else {
+ let table = task.transposition_table;
+ if let Some((entry, best_move)) = table.get(board, depth) {
+ return (entry, Some(best_move));
+ }
+
+ let turn = board.turn();
+ let mut best_eval = Evaluation::NULL_MIN;
+ let mut best_move = None;
+
+ let sort_fn = |m: &Move| unsafe { sort_moves(m, board, table) };
+ let sorter: LazySort<Move, _, Evaluation, { PossibleMoves::MAX_POSSIBLE_MOVES }> =
+ if let Some(moves) = allowed_moves {
+ LazySort::new(moves.iter().cloned(), sort_fn)
+ } else {
+ let moves = PossibleMoves::moves(board);
+ LazySort::new(moves, sort_fn)
+ };
+
+ if sorter.is_empty() {
+ return (Evaluation::LOSS, None);
+ }
+
+ for current_move in sorter.into_iter() {
+ if cancel_flag.load(std::sync::atomic::Ordering::Acquire) {
+ return (best_eval, best_move);
+ }
+
+ let board = unsafe { current_move.apply_to(board) };
+ let current_eval = if board.turn() == turn {
+ negamax(depth - 1, alpha, beta, board, None, cancel_flag, task)
+ .0
+ .increment()
+ } else {
+ -negamax(depth - 1, -beta, -alpha, board, None, cancel_flag, task)
+ .0
+ .increment()
+ };
+
+ if best_eval < current_eval {
+ best_eval = current_eval;
+ best_move = Some(current_move);
+ }
+
+ if alpha < best_eval {
+ alpha = best_eval;
+ }
+
+ if alpha >= beta {
+ return (best_eval, best_move);
+ }
+ }
+
+ // safety: we already checked that the list isn't empty, so there must
+ // be at least one move here
+ let best_move = unsafe { best_move.unwrap_unchecked() };
+ // safety: in the case of a zero depth, a different branch is taken
+ let depth = unsafe { NonZeroU8::new_unchecked(depth) };
+ table.insert(board, best_eval, best_move, depth);
+
+ (best_eval, Some(best_move))
+ }
+}
+
+pub fn search(
+ task: Arc<EvaluationTask>,
+ frontend: &dyn Frontend,
+ cancel: Option<&AtomicBool>,
+) -> (Evaluation, Option<Move>) {
+ let board = task.position;
+ let cancel_flag = cancel.unwrap_or(&task.cancel_flag);
+
+ let allowed_moves = task.allowed_moves.clone();
+ let limits = task.limits;
+ let max_depth = limits.depth;
+ let max_nodes = limits.nodes;
+ let start_time = Instant::now();
+ let max_time = limits.time.map(|d| start_time + d);
+
+ let mut alpha = Evaluation::NULL_MIN;
+ let mut beta = Evaluation::NULL_MAX;
+ let mut depth = 0;
+ let mut eval = Evaluation::DRAW;
+ let mut best_move = None;
+ loop {
+ // don't leave search is no good moves have been found
+ if best_move.is_some() {
+ if let Some(max_depth) = max_depth {
+ if depth > max_depth.get() {
+ break;
+ }
+ }
+
+ if let Some(max_time) = max_time {
+ if Instant::now() > max_time {
+ break;
+ }
+ }
+
+ if let Some(max_nodes) = max_nodes {
+ if task
+ .nodes_explored
+ .load(std::sync::atomic::Ordering::Acquire)
+ > max_nodes.get()
+ {
+ break;
+ }
+ }
+ } else {
+ // we don't need to do this every time
+ let mut possible_moves = PossibleMoves::moves(board).into_iter();
+ let (_, max_size) = possible_moves.size_hint();
+ if max_size == Some(1) {
+ // don't spend too much time thinking about a single possible move
+ eval = task
+ .transposition_table
+ .get_any_depth(board)
+ .unwrap_or_else(|| eval_position(board));
+ best_move = possible_moves.next();
+ break;
+ }
+ }
+
+ let em = negamax(
+ depth,
+ alpha,
+ beta,
+ board,
+ allowed_moves.clone(),
+ cancel_flag,
+ &task,
+ );
+
+ // prevent incomplete search from overwriting evaluation
+ if best_move.is_some() && cancel_flag.load(std::sync::atomic::Ordering::Acquire) {
+ break;
+ }
+
+ eval = em.0;
+ best_move = em.1;
+
+ while (eval <= alpha) || (eval >= beta) {
+ let em = negamax(
+ depth,
+ alpha,
+ beta,
+ board,
+ allowed_moves.clone(),
+ cancel_flag,
+ &task,
+ );
+
+ // prevent incomplete search from overwriting evaluation
+ if best_move.is_some() && cancel_flag.load(std::sync::atomic::Ordering::Acquire) {
+ break;
+ }
+
+ eval = em.0;
+ best_move = em.1;
+
+ if eval <= alpha {
+ alpha = Evaluation::NULL_MIN;
+ } else if eval >= beta {
+ beta = Evaluation::NULL_MAX;
+ }
+ }
+
+ if alpha.is_force_loss() {
+ alpha = Evaluation::NULL_MIN;
+ } else {
+ alpha = eval.add_f32(-0.125);
+ }
+
+ if beta.is_force_win() {
+ beta = Evaluation::NULL_MAX;
+ } else {
+ beta = eval.add_f32(0.125);
+ }
+
+ if eval.is_force_sequence() {
+ // we don't need to search any deeper
+ return (eval, best_move);
+ }
+
+ frontend.info(EvalInfo {
+ start_time,
+ nodes_searched: task
+ .nodes_explored
+ .load(std::sync::atomic::Ordering::Relaxed),
+ evaluation: eval,
+ current_best_move: best_move,
+ current_depth: depth,
+ _unused: PhantomData,
+ });
+
+ depth += 1;
+ }
+
+ // ponder
+ if let Some(best_move) = best_move {
+ // If the best move has not been found yet, then no move will be
+ // reported. This should be very rare. This technically is not allowed
+ // by the UCI specification, but if someone stops it this quickly, they
+ // probably didn't care about the best move anyway.
+ frontend.report_best_move(best_move);
+
+ if task.ponder {
+ let board = unsafe { best_move.apply_to(board) };
+
+ let mut depth = 0;
+ loop {
+ if task
+ .end_ponder_flag
+ .load(std::sync::atomic::Ordering::Acquire)
+ {
+ break;
+ }
+
+ negamax(
+ depth,
+ Evaluation::NULL_MIN,
+ Evaluation::NULL_MAX,
+ board,
+ None,
+ &task.end_ponder_flag,
+ &task,
+ );
+
+ depth += 1;
+ }
+ }
+ }
+
+ (eval, best_move)
+}
diff --git a/engine/src/tablebase.rs b/engine/src/tablebase.rs
index 87bf404..b56bea4 100644..100755
--- a/engine/src/tablebase.rs
+++ b/engine/src/tablebase.rs
@@ -1,186 +1,186 @@
-use std::{io, string::FromUtf8Error};
-
-use byteorder::{BigEndian, ReadBytesExt};
-use model::{CheckersBitBoard, PieceColor};
-use thiserror::Error;
-
-const MAGIC: u32 = u32::from_be_bytes(*b".amp");
-const SUPPORTED_VERSION: u16 = 0;
-const MAX_TABLE_LENGTH: u64 = 5_000_000_000;
-
-#[derive(Debug, Clone, PartialEq)]
-pub struct Tablebase {
- header: FileHeader,
- entries: Box<[Option<TablebaseEntry>]>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-struct FileHeader {
- /// The version of Ampere Tablebase Format being used
- version: u16,
- /// The magic number multiplied by board hash values
- magic_factor: u64,
- /// The number of entries in the tablebase
- entries_count: u64,
- /// The length of the table needed in-memory
- table_length: u64,
- /// The type of game the tablebase is for
- game_type: GameType,
- /// The name of the tablebase
- tablebase_name: Box<str>,
- /// The tablebase author
- author_name: Box<str>,
- /// The Unix timestamp indicating when the tablebase was created
- publication_time: u64,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-struct GameType {
- /// The type of game being played
- game_type: Game,
- /// The color that makes the first move
- start_color: PieceColor,
- /// The width of the board
- board_width: u8,
- /// The height of the board
- board_height: u8,
- /// The move notation
- notation: MoveNotation,
- /// True if the bottom-left square is a playing square
- invert_flag: bool,
-}
-
-#[repr(u8)]
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-enum Game {
- EnglishDraughts = 21,
-}
-
-#[repr(u8)]
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-enum MoveNotation {
- /// Standard Chess Notation, like e5
- Standard = 0,
- /// Alpha-numeric square representation, like e7-e5
- Alpha = 1,
- /// Numeric square representation, like 11-12
- Numeric = 2,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq)]
-struct TablebaseEntry {
- board: CheckersBitBoard,
- evaluation: f32,
- depth: u8,
-}
-
-#[derive(Debug, Error)]
-enum TablebaseFileError {
- #[error("Invalid tablebase: the magic header field was incorrect")]
- MagicError,
- #[error("This version of the tablebase format is unsupported. Only {SUPPORTED_VERSION} is supported")]
- UnsupportedVersion(u16),
- #[error("The table is too large. The length of the table is {} entries, but the max is only {}", .found, .max)]
- TableTooLarge { found: u64, max: u64 },
- #[error("The game type for this tablebase is unsupported. Only standard American Checkers is supported")]
- UnsupportedGameType(u8),
- #[error("A string was not valid UTF-8: {}", .0)]
- InvalidString(#[from] FromUtf8Error),
- #[error(transparent)]
- IoError(#[from] io::Error),
-}
-
-fn read_header(reader: &mut impl ReadBytesExt) -> Result<FileHeader, TablebaseFileError> {
- // magic is used to verify that the file is valid
- let magic = reader.read_u32::<BigEndian>()?;
- if magic != MAGIC {
- return Err(TablebaseFileError::MagicError);
- }
-
- read_reserved_bytes::<2>(reader)?;
-
- let version = reader.read_u16::<BigEndian>()?;
- if version != SUPPORTED_VERSION {
- return Err(TablebaseFileError::UnsupportedVersion(version));
- }
-
- let magic_factor = reader.read_u64::<BigEndian>()?;
- let entries_count = reader.read_u64::<BigEndian>()?;
- let table_length = reader.read_u64::<BigEndian>()?;
-
- if table_length > MAX_TABLE_LENGTH {
- return Err(TablebaseFileError::TableTooLarge {
- found: table_length,
- max: MAX_TABLE_LENGTH,
- });
- }
-
- let game_type = read_game_type(reader)?;
- let publication_time = reader.read_u64::<BigEndian>()?;
- let tablebase_name_len = reader.read_u8()?;
- let author_name_len = reader.read_u8()?;
- let _ = read_reserved_bytes::<14>(reader);
-
- let tablebase_name = read_string(reader, tablebase_name_len)?;
- let author_name = read_string(reader, author_name_len)?;
-
- Ok(FileHeader {
- version,
- magic_factor,
- entries_count,
- table_length,
- game_type,
- publication_time,
- tablebase_name,
- author_name,
- })
-}
-
-fn read_reserved_bytes<const NUM_BYTES: usize>(reader: &mut impl ReadBytesExt) -> io::Result<()> {
- reader.read_exact([0; NUM_BYTES].as_mut_slice())?;
- Ok(())
-}
-
-#[derive(Debug, Error)]
-enum ReadStringError {
- #[error(transparent)]
- InvalidUtf8(#[from] FromUtf8Error),
- #[error(transparent)]
- IoError(#[from] io::Error),
-}
-
-fn read_string(reader: &mut impl ReadBytesExt, len: u8) -> Result<Box<str>, TablebaseFileError> {
- let mut buffer = vec![0; len as usize];
- reader.read_exact(&mut buffer)?;
- Ok(String::from_utf8(buffer)?.into_boxed_str())
-}
-
-fn read_game_type(reader: &mut impl ReadBytesExt) -> Result<GameType, TablebaseFileError> {
- read_reserved_bytes::<1>(reader)?;
- let game_type = reader.read_u8()?;
- let start_color = reader.read_u8()?;
- let board_width = reader.read_u8()?;
- let board_height = reader.read_u8()?;
- let invert_flag = reader.read_u8()?;
- let notation = reader.read_u8()?;
- read_reserved_bytes::<1>(reader)?;
-
- if game_type != 21
- || start_color != 1
- || board_width != 8
- || board_height != 8
- || invert_flag != 1
- || notation != 2
- {
- Err(TablebaseFileError::UnsupportedGameType(game_type))
- } else {
- Ok(GameType {
- game_type: Game::EnglishDraughts,
- start_color: PieceColor::Dark,
- board_width: 8,
- board_height: 8,
- notation: MoveNotation::Numeric,
- invert_flag: true,
- })
- }
-}
+use std::{io, string::FromUtf8Error};
+
+use byteorder::{BigEndian, ReadBytesExt};
+use model::{CheckersBitBoard, PieceColor};
+use thiserror::Error;
+
+const MAGIC: u32 = u32::from_be_bytes(*b".amp");
+const SUPPORTED_VERSION: u16 = 0;
+const MAX_TABLE_LENGTH: u64 = 5_000_000_000;
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct Tablebase {
+ header: FileHeader,
+ entries: Box<[Option<TablebaseEntry>]>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+struct FileHeader {
+ /// The version of Ampere Tablebase Format being used
+ version: u16,
+ /// The magic number multiplied by board hash values
+ magic_factor: u64,
+ /// The number of entries in the tablebase
+ entries_count: u64,
+ /// The length of the table needed in-memory
+ table_length: u64,
+ /// The type of game the tablebase is for
+ game_type: GameType,
+ /// The name of the tablebase
+ tablebase_name: Box<str>,
+ /// The tablebase author
+ author_name: Box<str>,
+ /// The Unix timestamp indicating when the tablebase was created
+ publication_time: u64,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+struct GameType {
+ /// The type of game being played
+ game_type: Game,
+ /// The color that makes the first move
+ start_color: PieceColor,
+ /// The width of the board
+ board_width: u8,
+ /// The height of the board
+ board_height: u8,
+ /// The move notation
+ notation: MoveNotation,
+ /// True if the bottom-left square is a playing square
+ invert_flag: bool,
+}
+
+#[repr(u8)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum Game {
+ EnglishDraughts = 21,
+}
+
+#[repr(u8)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum MoveNotation {
+ /// Standard Chess Notation, like e5
+ Standard = 0,
+ /// Alpha-numeric square representation, like e7-e5
+ Alpha = 1,
+ /// Numeric square representation, like 11-12
+ Numeric = 2,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+struct TablebaseEntry {
+ board: CheckersBitBoard,
+ evaluation: f32,
+ depth: u8,
+}
+
+#[derive(Debug, Error)]
+enum TablebaseFileError {
+ #[error("Invalid tablebase: the magic header field was incorrect")]
+ MagicError,
+ #[error("This version of the tablebase format is unsupported. Only {SUPPORTED_VERSION} is supported")]
+ UnsupportedVersion(u16),
+ #[error("The table is too large. The length of the table is {} entries, but the max is only {}", .found, .max)]
+ TableTooLarge { found: u64, max: u64 },
+ #[error("The game type for this tablebase is unsupported. Only standard American Checkers is supported")]
+ UnsupportedGameType(u8),
+ #[error("A string was not valid UTF-8: {}", .0)]
+ InvalidString(#[from] FromUtf8Error),
+ #[error(transparent)]
+ IoError(#[from] io::Error),
+}
+
+fn read_header(reader: &mut impl ReadBytesExt) -> Result<FileHeader, TablebaseFileError> {
+ // magic is used to verify that the file is valid
+ let magic = reader.read_u32::<BigEndian>()?;
+ if magic != MAGIC {
+ return Err(TablebaseFileError::MagicError);
+ }
+
+ read_reserved_bytes::<2>(reader)?;
+
+ let version = reader.read_u16::<BigEndian>()?;
+ if version != SUPPORTED_VERSION {
+ return Err(TablebaseFileError::UnsupportedVersion(version));
+ }
+
+ let magic_factor = reader.read_u64::<BigEndian>()?;
+ let entries_count = reader.read_u64::<BigEndian>()?;
+ let table_length = reader.read_u64::<BigEndian>()?;
+
+ if table_length > MAX_TABLE_LENGTH {
+ return Err(TablebaseFileError::TableTooLarge {
+ found: table_length,
+ max: MAX_TABLE_LENGTH,
+ });
+ }
+
+ let game_type = read_game_type(reader)?;
+ let publication_time = reader.read_u64::<BigEndian>()?;
+ let tablebase_name_len = reader.read_u8()?;
+ let author_name_len = reader.read_u8()?;
+ let _ = read_reserved_bytes::<14>(reader);
+
+ let tablebase_name = read_string(reader, tablebase_name_len)?;
+ let author_name = read_string(reader, author_name_len)?;
+
+ Ok(FileHeader {
+ version,
+ magic_factor,
+ entries_count,
+ table_length,
+ game_type,
+ publication_time,
+ tablebase_name,
+ author_name,
+ })
+}
+
+fn read_reserved_bytes<const NUM_BYTES: usize>(reader: &mut impl ReadBytesExt) -> io::Result<()> {
+ reader.read_exact([0; NUM_BYTES].as_mut_slice())?;
+ Ok(())
+}
+
+#[derive(Debug, Error)]
+enum ReadStringError {
+ #[error(transparent)]
+ InvalidUtf8(#[from] FromUtf8Error),
+ #[error(transparent)]
+ IoError(#[from] io::Error),
+}
+
+fn read_string(reader: &mut impl ReadBytesExt, len: u8) -> Result<Box<str>, TablebaseFileError> {
+ let mut buffer = vec![0; len as usize];
+ reader.read_exact(&mut buffer)?;
+ Ok(String::from_utf8(buffer)?.into_boxed_str())
+}
+
+fn read_game_type(reader: &mut impl ReadBytesExt) -> Result<GameType, TablebaseFileError> {
+ read_reserved_bytes::<1>(reader)?;
+ let game_type = reader.read_u8()?;
+ let start_color = reader.read_u8()?;
+ let board_width = reader.read_u8()?;
+ let board_height = reader.read_u8()?;
+ let invert_flag = reader.read_u8()?;
+ let notation = reader.read_u8()?;
+ read_reserved_bytes::<1>(reader)?;
+
+ if game_type != 21
+ || start_color != 1
+ || board_width != 8
+ || board_height != 8
+ || invert_flag != 1
+ || notation != 2
+ {
+ Err(TablebaseFileError::UnsupportedGameType(game_type))
+ } else {
+ Ok(GameType {
+ game_type: Game::EnglishDraughts,
+ start_color: PieceColor::Dark,
+ board_width: 8,
+ board_height: 8,
+ notation: MoveNotation::Numeric,
+ invert_flag: true,
+ })
+ }
+}
diff --git a/engine/src/transposition_table.rs b/engine/src/transposition_table.rs
index 290ba68..e3cd59a 100644..100755
--- a/engine/src/transposition_table.rs
+++ b/engine/src/transposition_table.rs
@@ -1,177 +1,177 @@
-use crate::{eval::Evaluation, CheckersBitBoard};
-use model::Move;
-use parking_lot::RwLock;
-use std::num::NonZeroU8;
-
-#[derive(Copy, Clone, Debug)]
-struct TranspositionTableEntry {
- board: CheckersBitBoard,
- eval: Evaluation,
- best_move: Move,
- depth: NonZeroU8,
-}
-
-impl TranspositionTableEntry {
- const fn new(
- board: CheckersBitBoard,
- eval: Evaluation,
- best_move: Move,
- depth: NonZeroU8,
- ) -> Self {
- Self {
- board,
- eval,
- best_move,
- depth,
- }
- }
-}
-
-pub struct TranspositionTable {
- replace_table: Box<[RwLock<Option<TranspositionTableEntry>>]>,
- depth_table: Box<[RwLock<Option<TranspositionTableEntry>>]>,
-}
-
-#[derive(Copy, Clone, Debug)]
-pub struct TranspositionTableRef<'a> {
- replace_table: &'a [RwLock<Option<TranspositionTableEntry>>],
- depth_table: &'a [RwLock<Option<TranspositionTableEntry>>],
-}
-
-impl<'a> TranspositionTableRef<'a> {
- pub fn get(self, board: CheckersBitBoard, depth: u8) -> Option<(Evaluation, Move)> {
- let table_len = self.replace_table.as_ref().len();
-
- // try the replace table
- let entry = unsafe {
- self.replace_table
- .as_ref()
- .get_unchecked(board.hash_code() as usize % table_len)
- .read()
- };
- if let Some(entry) = *entry {
- if entry.board == board && entry.depth.get() >= depth {
- return Some((entry.eval, entry.best_move));
- }
- }
-
- // try the depth table
- let entry = unsafe {
- self.depth_table
- .as_ref()
- .get_unchecked(board.hash_code() as usize % table_len)
- .read()
- };
- match *entry {
- Some(entry) => {
- if entry.board == board {
- if entry.depth.get() >= depth {
- Some((entry.eval, entry.best_move))
- } else {
- None
- }
- } else {
- None
- }
- }
- None => None,
- }
- }
-
- pub fn get_any_depth(self, board: CheckersBitBoard) -> Option<Evaluation> {
- let table_len = self.replace_table.as_ref().len();
-
- // try the depth table
- let entry = unsafe {
- self.depth_table
- .as_ref()
- .get_unchecked(board.hash_code() as usize % table_len)
- .read()
- };
- if let Some(entry) = *entry {
- if entry.board == board {
- return Some(entry.eval);
- }
- }
-
- // try the replace table
- let entry = unsafe {
- self.replace_table
- .as_ref()
- .get_unchecked(board.hash_code() as usize % table_len)
- .read()
- };
- match *entry {
- Some(entry) => {
- if entry.board == board {
- Some(entry.eval)
- } else {
- None
- }
- }
- None => None,
- }
- }
-
- pub fn insert(
- &self,
- board: CheckersBitBoard,
- eval: Evaluation,
- best_move: Move,
- depth: NonZeroU8,
- ) {
- let table_len = self.replace_table.as_ref().len();
-
- // insert to the replace table
- let mut entry = unsafe {
- self.replace_table
- .get_unchecked(board.hash_code() as usize % table_len)
- .write()
- };
- *entry = Some(TranspositionTableEntry::new(board, eval, best_move, depth));
-
- // insert to the depth table, only if the new depth is higher
- let mut entry = unsafe {
- self.depth_table
- .get_unchecked(board.hash_code() as usize % table_len)
- .write()
- };
- match *entry {
- Some(entry_val) => {
- if depth >= entry_val.depth {
- *entry = Some(TranspositionTableEntry::new(board, eval, best_move, depth));
- }
- }
- None => *entry = Some(TranspositionTableEntry::new(board, eval, best_move, depth)),
- }
- }
-}
-
-impl TranspositionTable {
- pub fn new(table_size: usize) -> Self {
- let table_size =
- table_size / 2 / std::mem::size_of::<RwLock<Option<TranspositionTableEntry>>>();
- let mut replace_table = Box::new_uninit_slice(table_size);
- let mut depth_table = Box::new_uninit_slice(table_size);
-
- for entry in replace_table.iter_mut() {
- entry.write(RwLock::new(None));
- }
-
- for entry in depth_table.iter_mut() {
- entry.write(RwLock::new(None));
- }
-
- Self {
- replace_table: unsafe { replace_table.assume_init() },
- depth_table: unsafe { depth_table.assume_init() },
- }
- }
-
- pub fn get_ref(&self) -> TranspositionTableRef {
- TranspositionTableRef {
- replace_table: &self.replace_table,
- depth_table: &self.depth_table,
- }
- }
-}
+use crate::{eval::Evaluation, CheckersBitBoard};
+use model::Move;
+use parking_lot::RwLock;
+use std::num::NonZeroU8;
+
+#[derive(Copy, Clone, Debug)]
+struct TranspositionTableEntry {
+ board: CheckersBitBoard,
+ eval: Evaluation,
+ best_move: Move,
+ depth: NonZeroU8,
+}
+
+impl TranspositionTableEntry {
+ const fn new(
+ board: CheckersBitBoard,
+ eval: Evaluation,
+ best_move: Move,
+ depth: NonZeroU8,
+ ) -> Self {
+ Self {
+ board,
+ eval,
+ best_move,
+ depth,
+ }
+ }
+}
+
+pub struct TranspositionTable {
+ replace_table: Box<[RwLock<Option<TranspositionTableEntry>>]>,
+ depth_table: Box<[RwLock<Option<TranspositionTableEntry>>]>,
+}
+
+#[derive(Copy, Clone, Debug)]
+pub struct TranspositionTableRef<'a> {
+ replace_table: &'a [RwLock<Option<TranspositionTableEntry>>],
+ depth_table: &'a [RwLock<Option<TranspositionTableEntry>>],
+}
+
+impl<'a> TranspositionTableRef<'a> {
+ pub fn get(self, board: CheckersBitBoard, depth: u8) -> Option<(Evaluation, Move)> {
+ let table_len = self.replace_table.as_ref().len();
+
+ // try the replace table
+ let entry = unsafe {
+ self.replace_table
+ .as_ref()
+ .get_unchecked(board.hash_code() as usize % table_len)
+ .read()
+ };
+ if let Some(entry) = *entry {
+ if entry.board == board && entry.depth.get() >= depth {
+ return Some((entry.eval, entry.best_move));
+ }
+ }
+
+ // try the depth table
+ let entry = unsafe {
+ self.depth_table
+ .as_ref()
+ .get_unchecked(board.hash_code() as usize % table_len)
+ .read()
+ };
+ match *entry {
+ Some(entry) => {
+ if entry.board == board {
+ if entry.depth.get() >= depth {
+ Some((entry.eval, entry.best_move))
+ } else {
+ None
+ }
+ } else {
+ None
+ }
+ }
+ None => None,
+ }
+ }
+
+ pub fn get_any_depth(self, board: CheckersBitBoard) -> Option<Evaluation> {
+ let table_len = self.replace_table.as_ref().len();
+
+ // try the depth table
+ let entry = unsafe {
+ self.depth_table
+ .as_ref()
+ .get_unchecked(board.hash_code() as usize % table_len)
+ .read()
+ };
+ if let Some(entry) = *entry {
+ if entry.board == board {
+ return Some(entry.eval);
+ }
+ }
+
+ // try the replace table
+ let entry = unsafe {
+ self.replace_table
+ .as_ref()
+ .get_unchecked(board.hash_code() as usize % table_len)
+ .read()
+ };
+ match *entry {
+ Some(entry) => {
+ if entry.board == board {
+ Some(entry.eval)
+ } else {
+ None
+ }
+ }
+ None => None,
+ }
+ }
+
+ pub fn insert(
+ &self,
+ board: CheckersBitBoard,
+ eval: Evaluation,
+ best_move: Move,
+ depth: NonZeroU8,
+ ) {
+ let table_len = self.replace_table.as_ref().len();
+
+ // insert to the replace table
+ let mut entry = unsafe {
+ self.replace_table
+ .get_unchecked(board.hash_code() as usize % table_len)
+ .write()
+ };
+ *entry = Some(TranspositionTableEntry::new(board, eval, best_move, depth));
+
+ // insert to the depth table, only if the new depth is higher
+ let mut entry = unsafe {
+ self.depth_table
+ .get_unchecked(board.hash_code() as usize % table_len)
+ .write()
+ };
+ match *entry {
+ Some(entry_val) => {
+ if depth >= entry_val.depth {
+ *entry = Some(TranspositionTableEntry::new(board, eval, best_move, depth));
+ }
+ }
+ None => *entry = Some(TranspositionTableEntry::new(board, eval, best_move, depth)),
+ }
+ }
+}
+
+impl TranspositionTable {
+ pub fn new(table_size: usize) -> Self {
+ let table_size =
+ table_size / 2 / std::mem::size_of::<RwLock<Option<TranspositionTableEntry>>>();
+ let mut replace_table = Box::new_uninit_slice(table_size);
+ let mut depth_table = Box::new_uninit_slice(table_size);
+
+ for entry in replace_table.iter_mut() {
+ entry.write(RwLock::new(None));
+ }
+
+ for entry in depth_table.iter_mut() {
+ entry.write(RwLock::new(None));
+ }
+
+ Self {
+ replace_table: unsafe { replace_table.assume_init() },
+ depth_table: unsafe { depth_table.assume_init() },
+ }
+ }
+
+ pub fn get_ref(&self) -> TranspositionTableRef {
+ TranspositionTableRef {
+ replace_table: &self.replace_table,
+ depth_table: &self.depth_table,
+ }
+ }
+}