From fdb2804883deb31e3aeb15bbe588dcc9b7b76bd0 Mon Sep 17 00:00:00 2001 From: Mica White Date: Mon, 8 Dec 2025 19:56:48 -0500 Subject: Stuff --- .gitignore | 0 .idea/.gitignore | 0 .idea/checkers.iml | 1 + .idea/compilerexplorer.settings.xml | 0 .idea/modules.xml | 0 .idea/vcs.xml | 0 .vscode/settings.json | 5 + Cargo.toml | 1 - ampere.h | 66 + config.toml | 0 default.profdata | Bin 0 -> 512 bytes default.profraw | Bin 0 -> 697720 bytes engine/.gitignore | 0 engine/Cargo.toml | 3 + engine/src/c_abi.rs | 403 ++++++ engine/src/engine.rs | 548 +++---- engine/src/eval.rs | 331 ++--- engine/src/info.rs | 27 + engine/src/lazysort.rs | 174 +-- engine/src/lib.rs | 38 +- engine/src/main.rs | 141 +- engine/src/search.rs | 530 +++---- engine/src/tablebase.rs | 372 ++--- engine/src/transposition_table.rs | 354 ++--- model/Cargo.toml | 36 +- model/benches/bitboard.rs | 182 +-- model/proptest-regressions/board/tests.txt | 0 model/src/board.rs | 1346 ++++++++--------- model/src/board/tests.rs | 1114 +++++++------- model/src/color.rs | 0 model/src/coordinates.rs | 304 ++-- model/src/lib.rs | 26 +- model/src/moves.rs | 590 ++++---- model/src/piece.rs | 42 +- model/src/possible_moves.rs | 2156 ++++++++++++++-------------- pdn/Cargo.toml | 0 pdn/src/grammar.rs | 886 ++++++------ pdn/src/lib.rs | 0 pdn/src/tokens.rs | 568 ++++---- rustfmt.toml | 0 40 files changed, 5407 insertions(+), 4837 deletions(-) mode change 100644 => 100755 .gitignore mode change 100644 => 100755 .idea/.gitignore mode change 100644 => 100755 .idea/checkers.iml mode change 100644 => 100755 .idea/compilerexplorer.settings.xml mode change 100644 => 100755 .idea/modules.xml mode change 100644 => 100755 .idea/vcs.xml create mode 100755 .vscode/settings.json mode change 100644 => 100755 Cargo.toml create mode 100755 ampere.h mode change 100644 => 100755 config.toml create mode 100755 default.profdata create mode 100755 default.profraw mode change 100644 => 100755 engine/.gitignore mode change 100644 => 100755 engine/Cargo.toml create mode 100755 engine/src/c_abi.rs mode change 100644 => 100755 engine/src/engine.rs mode change 100644 => 100755 engine/src/eval.rs create mode 100755 engine/src/info.rs mode change 100644 => 100755 engine/src/lazysort.rs mode change 100644 => 100755 engine/src/lib.rs mode change 100644 => 100755 engine/src/main.rs mode change 100644 => 100755 engine/src/search.rs mode change 100644 => 100755 engine/src/tablebase.rs mode change 100644 => 100755 engine/src/transposition_table.rs mode change 100644 => 100755 model/Cargo.toml mode change 100644 => 100755 model/benches/bitboard.rs mode change 100644 => 100755 model/proptest-regressions/board/tests.txt mode change 100644 => 100755 model/src/board.rs mode change 100644 => 100755 model/src/board/tests.rs mode change 100644 => 100755 model/src/color.rs mode change 100644 => 100755 model/src/coordinates.rs mode change 100644 => 100755 model/src/lib.rs mode change 100644 => 100755 model/src/moves.rs mode change 100644 => 100755 model/src/piece.rs mode change 100644 => 100755 model/src/possible_moves.rs mode change 100644 => 100755 pdn/Cargo.toml mode change 100644 => 100755 pdn/src/grammar.rs mode change 100644 => 100755 pdn/src/lib.rs mode change 100644 => 100755 pdn/src/tokens.rs mode change 100644 => 100755 rustfmt.toml diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/.idea/.gitignore b/.idea/.gitignore old mode 100644 new mode 100755 diff --git a/.idea/checkers.iml b/.idea/checkers.iml old mode 100644 new mode 100755 index 0adb4e5..8ba0383 --- a/.idea/checkers.iml +++ b/.idea/checkers.iml @@ -7,6 +7,7 @@ + diff --git a/.idea/compilerexplorer.settings.xml b/.idea/compilerexplorer.settings.xml old mode 100644 new mode 100755 diff --git a/.idea/modules.xml b/.idea/modules.xml old mode 100644 new mode 100755 diff --git a/.idea/vcs.xml b/.idea/vcs.xml old mode 100644 new mode 100755 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100755 index 0000000..d563d57 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "files.associations": { + "enginedefs.h": "c" + } +} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml old mode 100644 new mode 100755 index 4c88f9c..a160adc --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,6 @@ members = [ ] [profile.dev] -opt-level = 3 [profile.release] lto = "fat" diff --git a/ampere.h b/ampere.h new file mode 100755 index 0000000..eed0159 --- /dev/null +++ b/ampere.h @@ -0,0 +1,66 @@ +#ifndef AMPERE +#define AMPERE + +#include +#include + +typedef void *engine_t; +typedef void *board_t; +typedef void *move_t; + +enum color { + LIGHT = 0, + DARK = 1 +}; + +enum direction { + FORWARD_LEFT = 0, + FORWARD_RIGHT = 1, + BACKWARD_LEFT = 2, + BACKWARD_RIGHT = 3, +}; + +struct frontend { + void (*debug)(char *message); + void (*report_bestmove)(move_t move); +}; + +extern engine_t ampere_new_engine(long long int hash_size, struct frontend *frontend); +extern engine_t ampere_set_debug(engine_t engine, bool debug); +extern bool ampere_islegal(engine_t engine, move_t move); +extern void ampere_reset_position(engine_t engine); +extern void ampere_set_position(engine_t engine, board_t board); +extern void ampere_play_move(engine_t engine, move_t move); +extern move_t ampere_evaluate(engine_t engine, bool *cancel, int nodes, int depth, int time); +extern void ampere_starteval_limited(engine_t engine, bool ponder, int nodes, int depth, int time); +extern void ampere_starteval_unlimited(engine_t engine, bool ponder); +extern void ampere_stopeval(engine_t engine); +extern void ampere_destroy_engine(engine_t engine); + +extern board_t ampere_board_starting_position(); +extern board_t ampere_board_new(uint32_t pieces, uint32_t color, uint32_t kings, enum color turn); +extern board_t ampere_board_clone(board_t board); +extern bool ampere_board_equal(board_t a, board_t b); +extern uint64_t ampere_board_hash(board_t board); +extern uint32_t *ampere_board_pieces(board_t board); +extern uint32_t *ampere_board_colors(board_t board); +extern uint32_t *ampere_board_kings(board_t board); +extern enum color *ampere_board_turn(board_t board); +extern bool ampere_board_has_piece_at(board_t board, int square); +extern enum color ampere_board_color_at(board_t board, int square); +extern bool ampere_board_king_at(board_t board, int square); +extern void ampere_board_move_piece(board_t board, int start, int dest); +extern void ampere_board_clear_piece(board_t board, int square); +extern void ampere_board_destroy(board_t board); + +extern move_t ampere_move_new(int start, enum direction direction, bool jump); +extern move_t ampere_move_clone(move_t move); +extern bool ampere_move_equal(move_t a, move_t b); +extern int ampere_move_start(move_t move); +extern enum direction ampere_move_direction(move_t move); +extern bool ampere_move_is_jump(move_t move); +extern int ampere_move_jump_position(move_t move); +extern int ampere_move_end(move_t move); +extern void ampere_move_destroy(move_t move); + +#endif diff --git a/config.toml b/config.toml old mode 100644 new mode 100755 diff --git a/default.profdata b/default.profdata new file mode 100755 index 0000000..dbfbafa Binary files /dev/null and b/default.profdata differ diff --git a/default.profraw b/default.profraw new file mode 100755 index 0000000..8edf8e8 Binary files /dev/null and b/default.profraw differ diff --git a/engine/.gitignore b/engine/.gitignore old mode 100644 new mode 100755 diff --git a/engine/Cargo.toml b/engine/Cargo.toml old mode 100644 new mode 100755 index 3e17c08..745e7a5 --- 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, + best_move: Option>, +} + +#[no_mangle] +extern "C" fn ampere_new_engine(hash_size: c_ulonglong, frontend: &CFrontend) -> Box> { + 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 { + 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>) { + 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 { + Box::new(CheckersBitBoard::starting_position()) +} + +#[no_mangle] +extern "C" fn ampere_board_new( + pieces: u32, + color: u32, + kings: u32, + turn: PieceColor, +) -> Box { + Box::new(CheckersBitBoard::new(pieces, color, kings, turn)) +} + +#[no_mangle] +extern "C" fn ampere_clock_unlimited() -> Box { + Box::new(Clock::Unlimited) +} + +#[no_mangle] +extern "C" fn ampere_clock_timepermove(millis: c_int) -> Box { + 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 { + 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) { + drop(clock) +} + +#[no_mangle] +extern "C" fn ampere_board_clone(board: &CheckersBitBoard) -> Box { + 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) { + 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) { + drop(evaluation) +} + +#[no_mangle] +extern "C" fn ampere_move_new(start: c_int, direction: MoveDirection, jump: bool) -> Box { + Box::new(Move::new(start as usize, direction, jump)) +} + +#[no_mangle] +extern "C" fn ampere_move_clone(ampere_move: &Move) -> Box { + 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) { + drop(ampere_move) +} diff --git a/engine/src/engine.rs b/engine/src/engine.rs old mode 100644 new mode 100755 index 6402f21..479e0ef --- 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)>; - -pub struct Engine<'a> { - position: Mutex, - transposition_table: TranspositionTable, - - debug: AtomicBool, - frontend: &'a dyn Frontend, - - current_thread: Mutex>, - current_task: Mutex>>>, - pondering_task: Mutex>>>, -} - -pub struct EvaluationTask<'a> { - pub position: CheckersBitBoard, - pub transposition_table: TranspositionTableRef<'a>, - pub allowed_moves: Option>, - 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>, - 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, - pub depth: Option, - pub time: Option, -} - -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) { - // 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)>; + +pub struct Engine<'a> { + position: Mutex, + transposition_table: TranspositionTable, + + debug: AtomicBool, + frontend: &'a dyn Frontend, + + current_thread: Mutex>, + current_task: Mutex>>>, + pondering_task: Mutex>>>, +} + +pub struct EvaluationTask<'a> { + pub position: CheckersBitBoard, + pub transposition_table: TranspositionTableRef<'a>, + pub allowed_moves: Option>, + 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>, + 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, + pub depth: Option, + pub time: Option, +} + +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) { + // 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 old mode 100644 new mode 100755 index 94849ce..a666913 --- 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 { - 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 { - 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 { + 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 { + 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, + 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 old mode 100644 new mode 100755 index f028778..9d54fe5 --- a/engine/src/lazysort.rs +++ b/engine/src/lazysort.rs @@ -1,87 +1,87 @@ -use arrayvec::ArrayVec; - -pub struct LazySort R, R: Ord, const CAPACITY: usize> { - collection: ArrayVec, - sorted: usize, - sort_by: F, -} - -pub struct LazySortIter R, R: Ord, const CAPACITY: usize> { - sorter: LazySort, - index: usize, -} - -impl R, R: Ord, const CAPACITY: usize> LazySort { - pub fn new(collection: impl IntoIterator, 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 R, R: Ord, const CAPACITY: usize> LazySort { - fn sort(&mut self, index: usize) { - let mut min: Option = 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 R, R: Ord, const CAPACITY: usize> IntoIterator - for LazySort -{ - type IntoIter = LazySortIter; - type Item = T; - - fn into_iter(self) -> Self::IntoIter { - LazySortIter { - sorter: self, - index: 0, - } - } -} - -impl R, R: Ord, const CAPACITY: usize> Iterator - for LazySortIter -{ - type Item = T; - - fn next(&mut self) -> Option { - let r = self.sorter.get(self.index); - self.index += 1; - r.cloned() - } -} +use arrayvec::ArrayVec; + +pub struct LazySort R, R: Ord, const CAPACITY: usize> { + collection: ArrayVec, + sorted: usize, + sort_by: F, +} + +pub struct LazySortIter R, R: Ord, const CAPACITY: usize> { + sorter: LazySort, + index: usize, +} + +impl R, R: Ord, const CAPACITY: usize> LazySort { + pub fn new(collection: impl IntoIterator, 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 R, R: Ord, const CAPACITY: usize> LazySort { + fn sort(&mut self, index: usize) { + let mut min: Option = 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 R, R: Ord, const CAPACITY: usize> IntoIterator + for LazySort +{ + type IntoIter = LazySortIter; + type Item = T; + + fn into_iter(self) -> Self::IntoIter { + LazySortIter { + sorter: self, + index: 0, + } + } +} + +impl R, R: Ord, const CAPACITY: usize> Iterator + for LazySortIter +{ + type Item = T; + + fn next(&mut self) -> Option { + let r = self.sorter.get(self.index); + self.index += 1; + r.cloned() + } +} diff --git a/engine/src/lib.rs b/engine/src/lib.rs old mode 100644 new mode 100755 index d87c225..7c5bd7f --- 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 old mode 100644 new mode 100755 index d4bcc48..187ff89 --- 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 old mode 100644 new mode 100755 index 4326ac6..fd8162a --- 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>, - cancel_flag: &AtomicBool, - task: &EvaluationTask, -) -> (Evaluation, Option) { - 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 = - 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, - frontend: &dyn Frontend, - cancel: Option<&AtomicBool>, -) -> (Evaluation, Option) { - 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>, + cancel_flag: &AtomicBool, + task: &EvaluationTask, +) -> (Evaluation, Option) { + 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 = + 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, + frontend: &dyn Frontend, + cancel: Option<&AtomicBool>, +) -> (Evaluation, Option) { + 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 old mode 100644 new mode 100755 index 87bf404..b56bea4 --- 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]>, -} - -#[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, - /// The tablebase author - author_name: Box, - /// 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 { - // magic is used to verify that the file is valid - let magic = reader.read_u32::()?; - if magic != MAGIC { - return Err(TablebaseFileError::MagicError); - } - - read_reserved_bytes::<2>(reader)?; - - let version = reader.read_u16::()?; - if version != SUPPORTED_VERSION { - return Err(TablebaseFileError::UnsupportedVersion(version)); - } - - let magic_factor = reader.read_u64::()?; - let entries_count = reader.read_u64::()?; - let table_length = reader.read_u64::()?; - - 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::()?; - 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(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, 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 { - 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]>, +} + +#[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, + /// The tablebase author + author_name: Box, + /// 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 { + // magic is used to verify that the file is valid + let magic = reader.read_u32::()?; + if magic != MAGIC { + return Err(TablebaseFileError::MagicError); + } + + read_reserved_bytes::<2>(reader)?; + + let version = reader.read_u16::()?; + if version != SUPPORTED_VERSION { + return Err(TablebaseFileError::UnsupportedVersion(version)); + } + + let magic_factor = reader.read_u64::()?; + let entries_count = reader.read_u64::()?; + let table_length = reader.read_u64::()?; + + 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::()?; + 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(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, 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 { + 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 old mode 100644 new mode 100755 index 290ba68..e3cd59a --- 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>]>, - depth_table: Box<[RwLock>]>, -} - -#[derive(Copy, Clone, Debug)] -pub struct TranspositionTableRef<'a> { - replace_table: &'a [RwLock>], - depth_table: &'a [RwLock>], -} - -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 { - 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::>>(); - 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>]>, + depth_table: Box<[RwLock>]>, +} + +#[derive(Copy, Clone, Debug)] +pub struct TranspositionTableRef<'a> { + replace_table: &'a [RwLock>], + depth_table: &'a [RwLock>], +} + +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 { + 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::>>(); + 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, + } + } +} diff --git a/model/Cargo.toml b/model/Cargo.toml old mode 100644 new mode 100755 index e732e99..40e8cb8 --- a/model/Cargo.toml +++ b/model/Cargo.toml @@ -1,19 +1,19 @@ -[package] -name = "model" -version = "0.1.0" -authors = ["Mica White "] -edition = "2021" -publish = false - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -serde = { version = "1", optional = true, features = ["derive"] } - -[dev-dependencies] -proptest = "1" -criterion = "0.3" - -[[bench]] -name = "bitboard" +[package] +name = "model" +version = "0.1.0" +authors = ["Mica White "] +edition = "2021" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { version = "1", optional = true, features = ["derive"] } + +[dev-dependencies] +proptest = "1" +criterion = "0.3" + +[[bench]] +name = "bitboard" harness = false \ No newline at end of file diff --git a/model/benches/bitboard.rs b/model/benches/bitboard.rs old mode 100644 new mode 100755 index 18d1a84..db70d65 --- a/model/benches/bitboard.rs +++ b/model/benches/bitboard.rs @@ -1,91 +1,91 @@ -use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use model::CheckersBitBoard; -use std::collections::hash_map::DefaultHasher; -use std::hash::Hash; - -fn clone(c: &mut Criterion) { - let board = CheckersBitBoard::starting_position(); - c.bench_function("clone", |b| b.iter(|| black_box(board))); -} - -fn hash(c: &mut Criterion) { - let board = CheckersBitBoard::starting_position(); - let mut hasher = DefaultHasher::new(); - c.bench_function("hash", |b| b.iter(|| board.hash(black_box(&mut hasher)))); -} - -fn default(c: &mut Criterion) { - c.bench_function("default", |b| { - b.iter(|| black_box(CheckersBitBoard::default())) - }); -} - -fn eq(c: &mut Criterion) { - let board1 = CheckersBitBoard::default(); - let board2 = CheckersBitBoard::default(); - c.bench_function("equal", |b| { - b.iter(|| black_box(board1) == black_box(board2)) - }); -} - -fn default_const(c: &mut Criterion) { - c.bench_function("default (const)", |b| { - b.iter(|| black_box(CheckersBitBoard::starting_position())) - }); -} - -fn piece_at(c: &mut Criterion) { - let board = CheckersBitBoard::starting_position(); - c.bench_function("piece", |b| b.iter(|| board.piece_at(black_box(0)))); -} - -fn color_at_unchecked(c: &mut Criterion) { - let board = CheckersBitBoard::starting_position(); - c.bench_function("color (unsafe)", |b| { - b.iter(|| unsafe { board.color_at_unchecked(black_box(1)) }) - }); -} - -fn king_at_unchecked(c: &mut Criterion) { - let board = CheckersBitBoard::starting_position(); - c.bench_function("king (unsafe)", |b| { - b.iter(|| unsafe { board.king_at_unchecked(black_box(2)) }) - }); -} - -fn color_at(c: &mut Criterion) { - let board = CheckersBitBoard::starting_position(); - c.bench_function("color (safe - filled)", |b| { - b.iter(|| board.color_at(black_box(3))) - }); - - c.bench_function("color (safe - empty)", |b| { - b.iter(|| board.color_at(black_box(2))) - }); -} - -fn king_at(c: &mut Criterion) { - let board = CheckersBitBoard::starting_position(); - c.bench_function("king (safe - filled)", |b| { - b.iter(|| board.king_at(black_box(4))) - }); - - c.bench_function("king (safe - empty)", |b| { - b.iter(|| board.king_at(black_box(9))) - }); -} - -criterion_group!( - bitboard, - clone, - hash, - eq, - default, - default_const, - piece_at, - color_at_unchecked, - king_at_unchecked, - color_at, - king_at, -); -criterion_main!(bitboard); +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use model::CheckersBitBoard; +use std::collections::hash_map::DefaultHasher; +use std::hash::Hash; + +fn clone(c: &mut Criterion) { + let board = CheckersBitBoard::starting_position(); + c.bench_function("clone", |b| b.iter(|| black_box(board))); +} + +fn hash(c: &mut Criterion) { + let board = CheckersBitBoard::starting_position(); + let mut hasher = DefaultHasher::new(); + c.bench_function("hash", |b| b.iter(|| board.hash(black_box(&mut hasher)))); +} + +fn default(c: &mut Criterion) { + c.bench_function("default", |b| { + b.iter(|| black_box(CheckersBitBoard::default())) + }); +} + +fn eq(c: &mut Criterion) { + let board1 = CheckersBitBoard::default(); + let board2 = CheckersBitBoard::default(); + c.bench_function("equal", |b| { + b.iter(|| black_box(board1) == black_box(board2)) + }); +} + +fn default_const(c: &mut Criterion) { + c.bench_function("default (const)", |b| { + b.iter(|| black_box(CheckersBitBoard::starting_position())) + }); +} + +fn piece_at(c: &mut Criterion) { + let board = CheckersBitBoard::starting_position(); + c.bench_function("piece", |b| b.iter(|| board.piece_at(black_box(0)))); +} + +fn color_at_unchecked(c: &mut Criterion) { + let board = CheckersBitBoard::starting_position(); + c.bench_function("color (unsafe)", |b| { + b.iter(|| unsafe { board.color_at_unchecked(black_box(1)) }) + }); +} + +fn king_at_unchecked(c: &mut Criterion) { + let board = CheckersBitBoard::starting_position(); + c.bench_function("king (unsafe)", |b| { + b.iter(|| unsafe { board.king_at_unchecked(black_box(2)) }) + }); +} + +fn color_at(c: &mut Criterion) { + let board = CheckersBitBoard::starting_position(); + c.bench_function("color (safe - filled)", |b| { + b.iter(|| board.color_at(black_box(3))) + }); + + c.bench_function("color (safe - empty)", |b| { + b.iter(|| board.color_at(black_box(2))) + }); +} + +fn king_at(c: &mut Criterion) { + let board = CheckersBitBoard::starting_position(); + c.bench_function("king (safe - filled)", |b| { + b.iter(|| board.king_at(black_box(4))) + }); + + c.bench_function("king (safe - empty)", |b| { + b.iter(|| board.king_at(black_box(9))) + }); +} + +criterion_group!( + bitboard, + clone, + hash, + eq, + default, + default_const, + piece_at, + color_at_unchecked, + king_at_unchecked, + color_at, + king_at, +); +criterion_main!(bitboard); diff --git a/model/proptest-regressions/board/tests.txt b/model/proptest-regressions/board/tests.txt old mode 100644 new mode 100755 diff --git a/model/src/board.rs b/model/src/board.rs old mode 100644 new mode 100755 index b722fd6..c6d6551 --- a/model/src/board.rs +++ b/model/src/board.rs @@ -1,673 +1,673 @@ -use crate::possible_moves::PossibleMoves; -use crate::{Piece, PieceColor, SquareCoordinate}; -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; -use std::hash::{Hash, Hasher}; - -#[cfg(test)] -mod tests; - -/// A checker board, -/// organized in the following structure: -/// ```txt -/// 11 05 31 25 -/// 10 04 30 24 -/// 03 29 23 17 -/// 02 28 22 16 -/// 27 21 15 09 -/// 26 20 14 08 -/// 19 13 07 01 -/// 18 12 06 00 -/// ``` -#[derive(Copy, Clone, Debug, Eq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct CheckersBitBoard { - /// If the space contains a piece, it's a 1 - pub pieces: u32, - /// If the piece is black, 1, otherwise 0 - pub color: u32, - /// 1 if the piece is a king - pub kings: u32, - /// The player who has the next turn - pub turn: PieceColor, - /// The player with the previous turn - pub previous_turn: PieceColor, - /// Where the most recent move was to - pub previous_move_to: u8, -} - -impl Default for CheckersBitBoard { - /// Returns the starting position - fn default() -> Self { - Self::starting_position() - } -} - -impl PartialEq for CheckersBitBoard { - fn eq(&self, other: &Self) -> bool { - self.pieces == other.pieces - && self.pieces & self.color == other.pieces & other.color - && self.pieces & self.kings == other.pieces & other.kings - && self.turn == other.turn - } -} - -impl Hash for CheckersBitBoard { - /// Hashes with only the pieces part, to ensure correctness and efficiency - fn hash(&self, hasher: &mut H) { - self.hash_code().hash(hasher) - } -} - -impl CheckersBitBoard { - /// Creates a new Checkers BitBoard - /// - /// # Arguments - /// - /// * `pieces` - Each bit is 1 if the corresponding space contains a piece - /// * `color` - For each space with a piece, the value is 1 if it's dark, and 0 otherwise. - /// Bits for spaces without colors are undefined - /// * `kings` - For each space with a piece, the value is 1 if it's a king, and 0 otherwise. - /// Bits for spaces without colors are undefined - /// - /// # Example - /// - /// ``` - /// // This is the starting position - /// use model::{CheckersBitBoard, PieceColor}; - /// let board = CheckersBitBoard::new(0b11011111101111100111100111100111, - /// 0b00111100001100001100001111001111, - /// 0, - /// PieceColor::Dark); - /// ``` - #[must_use] - pub const fn new(pieces: u32, color: u32, kings: u32, turn: PieceColor) -> Self { - Self { - pieces, - color, - kings, - turn, - previous_turn: turn.flip(), - // this field is only used if previous_turn == turn - previous_move_to: 0, - } - } - - /// Creates a board at the starting position - #[must_use] - pub const fn starting_position() -> Self { - const STARTING_BITBOARD: CheckersBitBoard = CheckersBitBoard::new( - 0b11100111100111100111110111111011, - 0b00001100001111001111001111000011, - 0, - PieceColor::Dark, - ); - STARTING_BITBOARD - } - - #[must_use] - pub const fn hash_code(self) -> u64 { - (((self.color & self.pieces) as u64) << 32) | (((!self.color & self.pieces) as u64) << 32) - } - - /// Gets the bits that represent where pieces are on the board - #[must_use] - pub const fn pieces_bits(self) -> u32 { - self.pieces - } - - /// Gets the bits that represents the color of each piece on the board - /// - /// # Safety - /// - /// This is inherently unsafe, because this also returns the bits of empty squares - #[must_use] - pub const fn color_bits(self) -> u32 { - self.color - } - - /// Gets the bits that represents the status of each piece on the board - /// - /// # Safety - /// - /// This is inherently unsafe, because this also returns the bits of empty squares - #[must_use] - pub const fn king_bits(self) -> u32 { - self.kings - } - - /// The player whose turn it is - #[must_use] - pub const fn turn(self) -> PieceColor { - self.turn - } - - /// Gets the piece at a given row column coordinate - /// - /// # Arguments - /// - /// * `row` - The row. The a file is row 0 - /// * `col` - The column. The first rank is column 0 - #[must_use] - // TODO test - pub fn get_at_row_col(self, row: usize, col: usize) -> Option { - if row > 32 || col > 32 { - None - } else { - let value = SquareCoordinate::new(row as u8, col as u8).to_ampere_value(); - if let Some(value) = value { - if self.piece_at(value) { - Some(Piece::new( - unsafe { self.king_at_unchecked(value) }, - unsafe { self.color_at_unchecked(value) }, - )) - } else { - None - } - } else { - None - } - } - } - - /// Checks if there's a piece at the given space value - /// - /// # Arguments - /// - /// * `value` - The value of the space to check - /// - /// # Example - /// - /// ``` - /// use model::CheckersBitBoard; - /// let board = CheckersBitBoard::default(); - /// match board.piece_at(0) { - /// true => println!("There's a piece in the bottom right"), - /// false => println!("The bottom right is empty") - /// } - /// ``` - /// - /// # Panics - /// - /// Panics if `value` is greater than or equal to 32 - #[must_use] - pub const fn piece_at(self, value: usize) -> bool { - ((self.pieces >> value) & 1) == 1 - } - - /// Checks the color at the piece in the given location, - /// without checking if there's a piece there - /// - /// # Arguments - /// - /// * `value` - The value of the space to check - /// - /// # Example - /// - /// ``` - /// use model::CheckersBitBoard; - /// use model::PieceColor; - /// let board = CheckersBitBoard::default(); - /// if board.piece_at(0) { - /// match unsafe {board.color_at_unchecked(0)} { - /// PieceColor::Dark => println!("The piece in the bottom right is dark colored"), - /// PieceColor::Light => println!("The piece in the bottom right is light colored") - /// } - /// } - /// ``` - /// - /// # Panics - /// - /// Panics if `value` is greater than or equal to 32 - /// - /// # Safety - /// - /// Checking the color at a square that is empty results in undefined behavior - #[must_use] - pub const unsafe fn color_at_unchecked(self, value: usize) -> PieceColor { - if ((self.color >> value) & 1) != 0 { - PieceColor::Dark - } else { - PieceColor::Light - } - } - - /// Checks the color at the piece in the given location. - /// Returns `None` if there isn't a piece there - /// - /// # Arguments - /// - /// * `value` - The value of the space to check - /// - /// # Example - /// - /// ``` - /// use model::CheckersBitBoard; - /// use model::PieceColor; - /// let board = CheckersBitBoard::default(); - /// if let Some(color) = board.color_at(0) { - /// match color { - /// PieceColor::Dark => println!("The piece in the bottom right is dark colored"), - /// PieceColor::Light => println!("The piece in the bottom left is light colored") - /// } - /// } - /// ``` - /// - /// # Panics - /// - /// Panics if `value` is greater than or equal to 32 - #[must_use] - pub const fn color_at(self, value: usize) -> Option { - if self.piece_at(value) { - // safety: if this block runs, then it's already confirmed a piece exists here - Some(unsafe { self.color_at_unchecked(value) }) - } else { - None - } - } - - /// Checks if the given location has a king, without checking if there's a piece there - /// - /// # Arguments - /// - /// * `value` - The value of the space to check - /// - /// # Example - /// - /// ``` - /// use model::CheckersBitBoard; - /// let board = CheckersBitBoard::default(); - /// if board.piece_at(0) { - /// match unsafe {board.king_at_unchecked(0)} { - /// true => println!("The piece in the bottom right is a king"), - /// false => println!("The piece in the bottom right is a peasant") - /// } - /// } - /// ``` - /// - /// # Panics - /// - /// Panics if `value` is greater than or equal to 32 - /// - /// # Safety - /// - /// Checking a square that is empty results in undefined behavior - #[must_use] - pub const unsafe fn king_at_unchecked(self, value: usize) -> bool { - ((self.kings >> value) & 1) == 1 - } - - /// Checks if the piece in the given location is a king. - /// Returns `None` if there isn't a piece there - /// - /// # Arguments - /// - /// * `value` - The value of the space to check - /// - /// # Example - /// - /// ``` - /// use model::CheckersBitBoard; - /// let board = CheckersBitBoard::default(); - /// if let Some(status) = board.king_at(0) { - /// match status { - /// true => println!("The piece in the bottom right is a king"), - /// false => println!("The piece in the bottom right is a peasant") - /// } - /// } - /// ``` - /// - /// # Panics - /// - /// Panics if `value` is greater than or equal to 32 - #[must_use] - pub const fn king_at(self, value: usize) -> Option { - if self.piece_at(value) { - // safety: if this block runs, then it's already confirmed a piece exists here - Some(unsafe { self.king_at_unchecked(value) }) - } else { - None - } - } - - /// Change whose turn it is, without modifying the board - #[must_use] - // TODO test - pub const fn flip_turn(self) -> Self { - CheckersBitBoard::new(self.pieces, self.color, self.kings, self.turn.flip()) - } - - /// Change whose turn it was previously to the current player - pub const fn set_previous_turn(self, dest: usize) -> Self { - CheckersBitBoard { - pieces: self.pieces, - color: self.color, - kings: self.kings, - turn: self.turn, - previous_turn: self.turn, - previous_move_to: dest as u8, - } - } - - /// Moves a piece from `start` to `dest`. The original location will be empty. - /// This does not mutate the original board. - /// If a piece already exists at `dest`, it will be overwritten. - /// - /// # Arguments - /// - /// * `start` - The original location of the piece - /// * `dest` - The new location - /// - /// # Panics - /// - /// Panics if `start` or `dest` is greater than or equal to 32 - /// - /// # Safety - /// - /// Results in undefined behavior if `start` does not contain a piece - // TODO rip out so we don't need to check for both black and white promotion - #[must_use] - pub const unsafe fn move_piece_to_unchecked(self, start: usize, dest: usize) -> Self { - // Clears the bit at the starting value - // Sets the bit at the destination value - let pieces = (self.pieces & !(1 << start)) | (1 << dest); - - // Clears the bit at the destination value - // Sets the value at the destination to the value of the start - let color = (self.color & !(1 << dest)) | (((self.color >> start) & 1) << dest); - - // The squares where certain pieces should be promoted - const DARK_PROMOTION_MASK: u32 = 0b10000010000000000000100000100000; - const LIGHT_PROMOTION_MASK: u32 = 0b1000001000001000001; - - // Clears the bit at the destination value - // Sets the value at the destination to the value of the start - // Promotes if the end of the board was reached - let kings = (self.kings & !(1 << dest)) - | (((self.kings >> start) & 1) << dest) - | (color & DARK_PROMOTION_MASK) - | (!color & LIGHT_PROMOTION_MASK); - - let turn = self.turn.flip(); - - CheckersBitBoard::new(pieces, color, kings, turn) - } - - /// Moves a piece from `value` to `(value + amount) % 32`. The original location will be empty. - /// This does not mutate the original board - /// - /// # Arguments - /// - /// * `value` - The original location of the piece - /// * `amount` - The amount to shift the location by - /// - /// # Panics - /// - /// Panics if `value` is greater than or equal to 32, - /// or `value + amount` is greater than `usize::MAX` - /// - /// # Safety - /// - /// This results in undefined behavior if `value` does not contain a piece - #[must_use] - const unsafe fn move_piece_forward_unchecked(self, value: usize, amount: usize) -> Self { - self.move_piece_to_unchecked(value, (value + amount) & 31) - } - - /// Moves a piece from `value` to `(value - amount) % 32`. The original location will be empty. - /// This does not mutate the original board. - /// If a piece already exists there, then it will be overwritten - /// - /// # Arguments - /// - /// * `value` - The original location of the piece - /// * `amount` - The amount to shift the location by - /// - /// # Panics - /// - /// Panics if `value` is greater than or equal to 32 - /// - /// # Safety - /// - /// This results in undefined behavior if `value` does not contain a piece - #[must_use] - const unsafe fn move_piece_backward_unchecked(self, value: usize, amount: usize) -> Self { - self.move_piece_to_unchecked(value, value.wrapping_sub(amount) & 31) - } - - /// Tries to move the piece forward and to the left, without checking if it's a legal move. - /// If a piece already exists there, then it will be overwritten - /// - /// # Arguments - /// - /// * `value` - The original location of the piece - /// - /// # Panics - /// - /// Panics if `value` is greater than or equal to 32 - /// - /// # Safety - /// - /// Moving from the left side of the board results in undefined behavior. - /// Moving from the top of the board results in undefined behavior. - /// A `value` which doesn't contain a piece results in undefined behavior. - #[must_use] - pub const unsafe fn move_piece_forward_left_unchecked(self, value: usize) -> Self { - self.move_piece_forward_unchecked(value, 7) - } - - /// Tries to move the piece forward and to the right, without checking if it's a legal move. - /// If a piece already exists there, then it will be overwritten - /// - /// # Arguments - /// - /// * `value` - The original location of the piece - /// - /// # Panics - /// - /// Panics if `value` is greater than or equal to 32 - /// - /// # Safety - /// - /// Moving from the right side of the board results in undefined behavior. - /// Moving from the top of the board results in undefined behavior. - /// A `value` which doesn't contain a piece results in undefined behavior. - #[must_use] - pub const unsafe fn move_piece_forward_right_unchecked(self, value: usize) -> Self { - self.move_piece_forward_unchecked(value, 1) - } - - /// Tries to move the piece backward and to the left, without checking if it's a legal move. - /// If a piece already exists there, then it will be overwritten - /// - /// # Arguments - /// - /// * `value` - The original location of the piece - /// - /// # Panics - /// - /// Panics if `value` is greater than or equal to 32 - /// - /// # Safety - /// - /// Moving from the left side of the board results in undefined behavior. - /// Moving from the bottom of the board results in undefined behavior. - /// A `value` which doesn't contain a piece results in undefined behavior. - #[must_use] - pub const unsafe fn move_piece_backward_left_unchecked(self, value: usize) -> Self { - self.move_piece_backward_unchecked(value, 1) - } - - /// Tries to move the piece backward and to the right, without checking if it's a legal move. - /// If a piece already exists there, then it will be overwritten - /// - /// # Arguments - /// - /// * `value` - The original location of the piece - /// - /// # Panics - /// - /// Panics if `value` is greater than or equal to 32 - /// - /// # Safety - /// - /// Moving from the right side of the board results in undefined behavior. - /// Moving from the bottom of the board results in undefined behavior. - /// A `value` which doesn't contain a piece results in undefined behavior. - #[must_use] - pub const unsafe fn move_piece_backward_right_unchecked(self, value: usize) -> Self { - self.move_piece_backward_unchecked(value, 7) - } - - /// Clears a space on the board. If the space is empty, then this function does nothing. - /// - /// # Arguments - /// - /// * `value` - The value of the space to clear - /// - /// # Panics - /// - /// Panics if `value` is greater than or equal to 32 - #[must_use] - pub const fn clear_piece(self, value: usize) -> Self { - let pieces = self.pieces & !(1 << value); - CheckersBitBoard::new(pieces, self.color, self.kings, self.turn) - } - - /// Tries to jump the piece forward and to the left, without checking if it's a legal move. - /// If a piece already exists there, then it will be overwritten. - /// The space the piece jumps over is cleared - /// - /// # Arguments - /// - /// * `value` - The original location of the piece - /// - /// # Panics - /// - /// Panics if `value` is greater than or equal to 32 - /// - /// # Safety - /// - /// Moving from the left side of the board results in undefined behavior. - /// Moving from the top of the board results in undefined behavior - #[must_use] - // TODO test the edge cases of the below if statement - pub const unsafe fn jump_piece_forward_left_unchecked(self, value: usize) -> Self { - let is_king = self.king_at_unchecked(value); - let board = self - .move_piece_forward_unchecked(value, 14) - .clear_piece((value + 7) & 31); - - const KING_MASK: u32 = 0b00100000100000100000000000001000; - if (is_king || (((1 << value) & KING_MASK) == 0)) - && PossibleMoves::has_jumps_at(board.flip_turn(), (value + 14) & 31) - { - board.flip_turn().set_previous_turn((value + 14) & 31) - } else { - board - } - } - - /// Tries to move the piece forward and to the right, without checking if it's a legal move. - /// If a piece already exists there, then it will be overwritten - /// The space the piece jumps over is cleared - /// - /// # Arguments - /// - /// * `value` - The original location of the piece - /// - /// # Panics - /// - /// Panics if `value` is greater than or equal to 32 - /// - /// # Safety - /// - /// Moving from the right side of the board results in undefined behavior. - /// Moving from the top of the board results in undefined behavior - #[must_use] - pub const unsafe fn jump_piece_forward_right_unchecked(self, value: usize) -> Self { - let is_king = self.king_at_unchecked(value); - let board = self - .move_piece_forward_unchecked(value, 2) - .clear_piece((value + 1) & 31); - - const KING_MASK: u32 = 0b00100000100000100000000000001000; - if (is_king || (((1 << value) & KING_MASK) == 0)) - && PossibleMoves::has_jumps_at(board.flip_turn(), (value + 2) & 31) - { - board.flip_turn().set_previous_turn((value + 2) & 31) - } else { - board - } - } - - /// Tries to move the piece backward and to the left, without checking if it's a legal move. - /// If a piece already exists there, then it will be overwritten - /// The space the piece jumps over is cleared - /// - /// # Arguments - /// - /// * `value` - The original location of the piece - /// - /// # Panics - /// - /// Panics if `value` is greater than or equal to 32 - /// - /// # Safety - /// - /// Moving from the left side of the board results in undefined behavior. - /// Moving from the bottom of the board results in undefined behavior - #[must_use] - pub const unsafe fn jump_piece_backward_left_unchecked(self, value: usize) -> Self { - let is_king = self.king_at_unchecked(value); - let board = self - .move_piece_backward_unchecked(value, 2) - .clear_piece(value.wrapping_sub(1) & 31); - - const KING_MASK: u32 = 0b00000100000100000100000100000000; - if (is_king || (((1 << value) & KING_MASK) == 0)) - && PossibleMoves::has_jumps_at(board.flip_turn(), value.wrapping_sub(2) & 31) - { - board - .flip_turn() - .set_previous_turn((value.wrapping_sub(2)) & 31) - } else { - board - } - } - - /// Tries to move the piece backward and to the right, without checking if it's a legal move. - /// If a piece already exists there, then it will be overwritten - /// The space the piece jumps over is cleared - /// - /// # Arguments - /// - /// * `value` - The original location of the piece - /// - /// # Panics - /// - /// Panics if `value` is greater than or equal to 32 - /// - /// # Safety - /// - /// Moving from the right side of the board results in undefined behavior. - /// Moving from the bottom of the board results in undefined behavior - #[must_use] - pub const unsafe fn jump_piece_backward_right_unchecked(self, value: usize) -> Self { - let is_king = self.king_at_unchecked(value); - let board = self - .move_piece_backward_unchecked(value, 14) - .clear_piece(value.wrapping_sub(7) & 31); - - const KING_MASK: u32 = 0b00000100000100000100000100000000; - if (is_king || (((1 << value) & KING_MASK) == 0)) - && PossibleMoves::has_jumps_at(board.flip_turn(), value.wrapping_sub(14) & 31) - { - board - .flip_turn() - .set_previous_turn((value.wrapping_sub(14)) & 31) - } else { - board - } - } -} +use crate::possible_moves::PossibleMoves; +use crate::{Piece, PieceColor, SquareCoordinate}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use std::hash::{Hash, Hasher}; + +#[cfg(test)] +mod tests; + +/// A checker board, +/// organized in the following structure: +/// ```txt +/// 11 05 31 25 +/// 10 04 30 24 +/// 03 29 23 17 +/// 02 28 22 16 +/// 27 21 15 09 +/// 26 20 14 08 +/// 19 13 07 01 +/// 18 12 06 00 +/// ``` +#[derive(Copy, Clone, Debug, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct CheckersBitBoard { + /// If the space contains a piece, it's a 1 + pub pieces: u32, + /// If the piece is black, 1, otherwise 0 + pub color: u32, + /// 1 if the piece is a king + pub kings: u32, + /// The player who has the next turn + pub turn: PieceColor, + /// The player with the previous turn + pub previous_turn: PieceColor, + /// Where the most recent move was to + pub previous_move_to: u8, +} + +impl Default for CheckersBitBoard { + /// Returns the starting position + fn default() -> Self { + Self::starting_position() + } +} + +impl PartialEq for CheckersBitBoard { + fn eq(&self, other: &Self) -> bool { + self.pieces == other.pieces + && self.pieces & self.color == other.pieces & other.color + && self.pieces & self.kings == other.pieces & other.kings + && self.turn == other.turn + } +} + +impl Hash for CheckersBitBoard { + /// Hashes with only the pieces part, to ensure correctness and efficiency + fn hash(&self, hasher: &mut H) { + self.hash_code().hash(hasher) + } +} + +impl CheckersBitBoard { + /// Creates a new Checkers BitBoard + /// + /// # Arguments + /// + /// * `pieces` - Each bit is 1 if the corresponding space contains a piece + /// * `color` - For each space with a piece, the value is 1 if it's dark, and 0 otherwise. + /// Bits for spaces without colors are undefined + /// * `kings` - For each space with a piece, the value is 1 if it's a king, and 0 otherwise. + /// Bits for spaces without colors are undefined + /// + /// # Example + /// + /// ``` + /// // This is the starting position + /// use model::{CheckersBitBoard, PieceColor}; + /// let board = CheckersBitBoard::new(0b11011111101111100111100111100111, + /// 0b00111100001100001100001111001111, + /// 0, + /// PieceColor::Dark); + /// ``` + #[must_use] + pub const fn new(pieces: u32, color: u32, kings: u32, turn: PieceColor) -> Self { + Self { + pieces, + color, + kings, + turn, + previous_turn: turn.flip(), + // this field is only used if previous_turn == turn + previous_move_to: 0, + } + } + + /// Creates a board at the starting position + #[must_use] + pub const fn starting_position() -> Self { + const STARTING_BITBOARD: CheckersBitBoard = CheckersBitBoard::new( + 0b11100111100111100111110111111011, + 0b00001100001111001111001111000011, + 0, + PieceColor::Dark, + ); + STARTING_BITBOARD + } + + #[must_use] + pub const fn hash_code(self) -> u64 { + (((self.color & self.pieces) as u64) << 32) | (((!self.color & self.pieces) as u64) << 32) + } + + /// Gets the bits that represent where pieces are on the board + #[must_use] + pub const fn pieces_bits(self) -> u32 { + self.pieces + } + + /// Gets the bits that represents the color of each piece on the board + /// + /// # Safety + /// + /// This is inherently unsafe, because this also returns the bits of empty squares + #[must_use] + pub const fn color_bits(self) -> u32 { + self.color + } + + /// Gets the bits that represents the status of each piece on the board + /// + /// # Safety + /// + /// This is inherently unsafe, because this also returns the bits of empty squares + #[must_use] + pub const fn king_bits(self) -> u32 { + self.kings + } + + /// The player whose turn it is + #[must_use] + pub const fn turn(self) -> PieceColor { + self.turn + } + + /// Gets the piece at a given row column coordinate + /// + /// # Arguments + /// + /// * `row` - The row. The a file is row 0 + /// * `col` - The column. The first rank is column 0 + #[must_use] + // TODO test + pub fn get_at_row_col(self, row: usize, col: usize) -> Option { + if row > 32 || col > 32 { + None + } else { + let value = SquareCoordinate::new(row as u8, col as u8).to_ampere_value(); + if let Some(value) = value { + if self.piece_at(value) { + Some(Piece::new( + unsafe { self.king_at_unchecked(value) }, + unsafe { self.color_at_unchecked(value) }, + )) + } else { + None + } + } else { + None + } + } + } + + /// Checks if there's a piece at the given space value + /// + /// # Arguments + /// + /// * `value` - The value of the space to check + /// + /// # Example + /// + /// ``` + /// use model::CheckersBitBoard; + /// let board = CheckersBitBoard::default(); + /// match board.piece_at(0) { + /// true => println!("There's a piece in the bottom right"), + /// false => println!("The bottom right is empty") + /// } + /// ``` + /// + /// # Panics + /// + /// Panics if `value` is greater than or equal to 32 + #[must_use] + pub const fn piece_at(self, value: usize) -> bool { + ((self.pieces >> value) & 1) == 1 + } + + /// Checks the color at the piece in the given location, + /// without checking if there's a piece there + /// + /// # Arguments + /// + /// * `value` - The value of the space to check + /// + /// # Example + /// + /// ``` + /// use model::CheckersBitBoard; + /// use model::PieceColor; + /// let board = CheckersBitBoard::default(); + /// if board.piece_at(0) { + /// match unsafe {board.color_at_unchecked(0)} { + /// PieceColor::Dark => println!("The piece in the bottom right is dark colored"), + /// PieceColor::Light => println!("The piece in the bottom right is light colored") + /// } + /// } + /// ``` + /// + /// # Panics + /// + /// Panics if `value` is greater than or equal to 32 + /// + /// # Safety + /// + /// Checking the color at a square that is empty results in undefined behavior + #[must_use] + pub const unsafe fn color_at_unchecked(self, value: usize) -> PieceColor { + if ((self.color >> value) & 1) != 0 { + PieceColor::Dark + } else { + PieceColor::Light + } + } + + /// Checks the color at the piece in the given location. + /// Returns `None` if there isn't a piece there + /// + /// # Arguments + /// + /// * `value` - The value of the space to check + /// + /// # Example + /// + /// ``` + /// use model::CheckersBitBoard; + /// use model::PieceColor; + /// let board = CheckersBitBoard::default(); + /// if let Some(color) = board.color_at(0) { + /// match color { + /// PieceColor::Dark => println!("The piece in the bottom right is dark colored"), + /// PieceColor::Light => println!("The piece in the bottom left is light colored") + /// } + /// } + /// ``` + /// + /// # Panics + /// + /// Panics if `value` is greater than or equal to 32 + #[must_use] + pub const fn color_at(self, value: usize) -> Option { + if self.piece_at(value) { + // safety: if this block runs, then it's already confirmed a piece exists here + Some(unsafe { self.color_at_unchecked(value) }) + } else { + None + } + } + + /// Checks if the given location has a king, without checking if there's a piece there + /// + /// # Arguments + /// + /// * `value` - The value of the space to check + /// + /// # Example + /// + /// ``` + /// use model::CheckersBitBoard; + /// let board = CheckersBitBoard::default(); + /// if board.piece_at(0) { + /// match unsafe {board.king_at_unchecked(0)} { + /// true => println!("The piece in the bottom right is a king"), + /// false => println!("The piece in the bottom right is a peasant") + /// } + /// } + /// ``` + /// + /// # Panics + /// + /// Panics if `value` is greater than or equal to 32 + /// + /// # Safety + /// + /// Checking a square that is empty results in undefined behavior + #[must_use] + pub const unsafe fn king_at_unchecked(self, value: usize) -> bool { + ((self.kings >> value) & 1) == 1 + } + + /// Checks if the piece in the given location is a king. + /// Returns `None` if there isn't a piece there + /// + /// # Arguments + /// + /// * `value` - The value of the space to check + /// + /// # Example + /// + /// ``` + /// use model::CheckersBitBoard; + /// let board = CheckersBitBoard::default(); + /// if let Some(status) = board.king_at(0) { + /// match status { + /// true => println!("The piece in the bottom right is a king"), + /// false => println!("The piece in the bottom right is a peasant") + /// } + /// } + /// ``` + /// + /// # Panics + /// + /// Panics if `value` is greater than or equal to 32 + #[must_use] + pub const fn king_at(self, value: usize) -> Option { + if self.piece_at(value) { + // safety: if this block runs, then it's already confirmed a piece exists here + Some(unsafe { self.king_at_unchecked(value) }) + } else { + None + } + } + + /// Change whose turn it is, without modifying the board + #[must_use] + // TODO test + pub const fn flip_turn(self) -> Self { + CheckersBitBoard::new(self.pieces, self.color, self.kings, self.turn.flip()) + } + + /// Change whose turn it was previously to the current player + pub const fn set_previous_turn(self, dest: usize) -> Self { + CheckersBitBoard { + pieces: self.pieces, + color: self.color, + kings: self.kings, + turn: self.turn, + previous_turn: self.turn, + previous_move_to: dest as u8, + } + } + + /// Moves a piece from `start` to `dest`. The original location will be empty. + /// This does not mutate the original board. + /// If a piece already exists at `dest`, it will be overwritten. + /// + /// # Arguments + /// + /// * `start` - The original location of the piece + /// * `dest` - The new location + /// + /// # Panics + /// + /// Panics if `start` or `dest` is greater than or equal to 32 + /// + /// # Safety + /// + /// Results in undefined behavior if `start` does not contain a piece + // TODO rip out so we don't need to check for both black and white promotion + #[must_use] + pub const unsafe fn move_piece_to_unchecked(self, start: usize, dest: usize) -> Self { + // Clears the bit at the starting value + // Sets the bit at the destination value + let pieces = (self.pieces & !(1 << start)) | (1 << dest); + + // Clears the bit at the destination value + // Sets the value at the destination to the value of the start + let color = (self.color & !(1 << dest)) | (((self.color >> start) & 1) << dest); + + // The squares where certain pieces should be promoted + const DARK_PROMOTION_MASK: u32 = 0b10000010000000000000100000100000; + const LIGHT_PROMOTION_MASK: u32 = 0b1000001000001000001; + + // Clears the bit at the destination value + // Sets the value at the destination to the value of the start + // Promotes if the end of the board was reached + let kings = (self.kings & !(1 << dest)) + | (((self.kings >> start) & 1) << dest) + | (color & DARK_PROMOTION_MASK) + | (!color & LIGHT_PROMOTION_MASK); + + let turn = self.turn.flip(); + + CheckersBitBoard::new(pieces, color, kings, turn) + } + + /// Moves a piece from `value` to `(value + amount) % 32`. The original location will be empty. + /// This does not mutate the original board + /// + /// # Arguments + /// + /// * `value` - The original location of the piece + /// * `amount` - The amount to shift the location by + /// + /// # Panics + /// + /// Panics if `value` is greater than or equal to 32, + /// or `value + amount` is greater than `usize::MAX` + /// + /// # Safety + /// + /// This results in undefined behavior if `value` does not contain a piece + #[must_use] + const unsafe fn move_piece_forward_unchecked(self, value: usize, amount: usize) -> Self { + self.move_piece_to_unchecked(value, (value + amount) & 31) + } + + /// Moves a piece from `value` to `(value - amount) % 32`. The original location will be empty. + /// This does not mutate the original board. + /// If a piece already exists there, then it will be overwritten + /// + /// # Arguments + /// + /// * `value` - The original location of the piece + /// * `amount` - The amount to shift the location by + /// + /// # Panics + /// + /// Panics if `value` is greater than or equal to 32 + /// + /// # Safety + /// + /// This results in undefined behavior if `value` does not contain a piece + #[must_use] + const unsafe fn move_piece_backward_unchecked(self, value: usize, amount: usize) -> Self { + self.move_piece_to_unchecked(value, value.wrapping_sub(amount) & 31) + } + + /// Tries to move the piece forward and to the left, without checking if it's a legal move. + /// If a piece already exists there, then it will be overwritten + /// + /// # Arguments + /// + /// * `value` - The original location of the piece + /// + /// # Panics + /// + /// Panics if `value` is greater than or equal to 32 + /// + /// # Safety + /// + /// Moving from the left side of the board results in undefined behavior. + /// Moving from the top of the board results in undefined behavior. + /// A `value` which doesn't contain a piece results in undefined behavior. + #[must_use] + pub const unsafe fn move_piece_forward_left_unchecked(self, value: usize) -> Self { + self.move_piece_forward_unchecked(value, 7) + } + + /// Tries to move the piece forward and to the right, without checking if it's a legal move. + /// If a piece already exists there, then it will be overwritten + /// + /// # Arguments + /// + /// * `value` - The original location of the piece + /// + /// # Panics + /// + /// Panics if `value` is greater than or equal to 32 + /// + /// # Safety + /// + /// Moving from the right side of the board results in undefined behavior. + /// Moving from the top of the board results in undefined behavior. + /// A `value` which doesn't contain a piece results in undefined behavior. + #[must_use] + pub const unsafe fn move_piece_forward_right_unchecked(self, value: usize) -> Self { + self.move_piece_forward_unchecked(value, 1) + } + + /// Tries to move the piece backward and to the left, without checking if it's a legal move. + /// If a piece already exists there, then it will be overwritten + /// + /// # Arguments + /// + /// * `value` - The original location of the piece + /// + /// # Panics + /// + /// Panics if `value` is greater than or equal to 32 + /// + /// # Safety + /// + /// Moving from the left side of the board results in undefined behavior. + /// Moving from the bottom of the board results in undefined behavior. + /// A `value` which doesn't contain a piece results in undefined behavior. + #[must_use] + pub const unsafe fn move_piece_backward_left_unchecked(self, value: usize) -> Self { + self.move_piece_backward_unchecked(value, 1) + } + + /// Tries to move the piece backward and to the right, without checking if it's a legal move. + /// If a piece already exists there, then it will be overwritten + /// + /// # Arguments + /// + /// * `value` - The original location of the piece + /// + /// # Panics + /// + /// Panics if `value` is greater than or equal to 32 + /// + /// # Safety + /// + /// Moving from the right side of the board results in undefined behavior. + /// Moving from the bottom of the board results in undefined behavior. + /// A `value` which doesn't contain a piece results in undefined behavior. + #[must_use] + pub const unsafe fn move_piece_backward_right_unchecked(self, value: usize) -> Self { + self.move_piece_backward_unchecked(value, 7) + } + + /// Clears a space on the board. If the space is empty, then this function does nothing. + /// + /// # Arguments + /// + /// * `value` - The value of the space to clear + /// + /// # Panics + /// + /// Panics if `value` is greater than or equal to 32 + #[must_use] + pub const fn clear_piece(self, value: usize) -> Self { + let pieces = self.pieces & !(1 << value); + CheckersBitBoard::new(pieces, self.color, self.kings, self.turn) + } + + /// Tries to jump the piece forward and to the left, without checking if it's a legal move. + /// If a piece already exists there, then it will be overwritten. + /// The space the piece jumps over is cleared + /// + /// # Arguments + /// + /// * `value` - The original location of the piece + /// + /// # Panics + /// + /// Panics if `value` is greater than or equal to 32 + /// + /// # Safety + /// + /// Moving from the left side of the board results in undefined behavior. + /// Moving from the top of the board results in undefined behavior + #[must_use] + // TODO test the edge cases of the below if statement + pub const unsafe fn jump_piece_forward_left_unchecked(self, value: usize) -> Self { + let is_king = self.king_at_unchecked(value); + let board = self + .move_piece_forward_unchecked(value, 14) + .clear_piece((value + 7) & 31); + + const KING_MASK: u32 = 0b00100000100000100000000000001000; + if (is_king || (((1 << value) & KING_MASK) == 0)) + && PossibleMoves::has_jumps_at(board.flip_turn(), (value + 14) & 31) + { + board.flip_turn().set_previous_turn((value + 14) & 31) + } else { + board + } + } + + /// Tries to move the piece forward and to the right, without checking if it's a legal move. + /// If a piece already exists there, then it will be overwritten + /// The space the piece jumps over is cleared + /// + /// # Arguments + /// + /// * `value` - The original location of the piece + /// + /// # Panics + /// + /// Panics if `value` is greater than or equal to 32 + /// + /// # Safety + /// + /// Moving from the right side of the board results in undefined behavior. + /// Moving from the top of the board results in undefined behavior + #[must_use] + pub const unsafe fn jump_piece_forward_right_unchecked(self, value: usize) -> Self { + let is_king = self.king_at_unchecked(value); + let board = self + .move_piece_forward_unchecked(value, 2) + .clear_piece((value + 1) & 31); + + const KING_MASK: u32 = 0b00100000100000100000000000001000; + if (is_king || (((1 << value) & KING_MASK) == 0)) + && PossibleMoves::has_jumps_at(board.flip_turn(), (value + 2) & 31) + { + board.flip_turn().set_previous_turn((value + 2) & 31) + } else { + board + } + } + + /// Tries to move the piece backward and to the left, without checking if it's a legal move. + /// If a piece already exists there, then it will be overwritten + /// The space the piece jumps over is cleared + /// + /// # Arguments + /// + /// * `value` - The original location of the piece + /// + /// # Panics + /// + /// Panics if `value` is greater than or equal to 32 + /// + /// # Safety + /// + /// Moving from the left side of the board results in undefined behavior. + /// Moving from the bottom of the board results in undefined behavior + #[must_use] + pub const unsafe fn jump_piece_backward_left_unchecked(self, value: usize) -> Self { + let is_king = self.king_at_unchecked(value); + let board = self + .move_piece_backward_unchecked(value, 2) + .clear_piece(value.wrapping_sub(1) & 31); + + const KING_MASK: u32 = 0b00000100000100000100000100000000; + if (is_king || (((1 << value) & KING_MASK) == 0)) + && PossibleMoves::has_jumps_at(board.flip_turn(), value.wrapping_sub(2) & 31) + { + board + .flip_turn() + .set_previous_turn((value.wrapping_sub(2)) & 31) + } else { + board + } + } + + /// Tries to move the piece backward and to the right, without checking if it's a legal move. + /// If a piece already exists there, then it will be overwritten + /// The space the piece jumps over is cleared + /// + /// # Arguments + /// + /// * `value` - The original location of the piece + /// + /// # Panics + /// + /// Panics if `value` is greater than or equal to 32 + /// + /// # Safety + /// + /// Moving from the right side of the board results in undefined behavior. + /// Moving from the bottom of the board results in undefined behavior + #[must_use] + pub const unsafe fn jump_piece_backward_right_unchecked(self, value: usize) -> Self { + let is_king = self.king_at_unchecked(value); + let board = self + .move_piece_backward_unchecked(value, 14) + .clear_piece(value.wrapping_sub(7) & 31); + + const KING_MASK: u32 = 0b00000100000100000100000100000000; + if (is_king || (((1 << value) & KING_MASK) == 0)) + && PossibleMoves::has_jumps_at(board.flip_turn(), value.wrapping_sub(14) & 31) + { + board + .flip_turn() + .set_previous_turn((value.wrapping_sub(14)) & 31) + } else { + board + } + } +} diff --git a/model/src/board/tests.rs b/model/src/board/tests.rs old mode 100644 new mode 100755 index 8c119dc..9c356ae --- a/model/src/board/tests.rs +++ b/model/src/board/tests.rs @@ -1,557 +1,557 @@ -use std::collections::hash_map::DefaultHasher; - -use proptest::prelude::*; - -use super::*; - -proptest! { - #[test] - fn test_bitboard_new(p in 0u32..=u32::MAX, c in 0u32..=u32::MAX, k in 0u32..=u32::MAX) { - let board = CheckersBitBoard::new(p, c, k, PieceColor::Dark); - assert_eq!(p, board.pieces); - assert_eq!(c, board.color); - assert_eq!(k, board.kings); - } - - #[test] - fn test_bits_fns(p in 0u32..=u32::MAX, c in 0u32..=u32::MAX, k in 0u32..=u32::MAX) { - let board = CheckersBitBoard::new(p, c, k, PieceColor::Dark); - assert_eq!(p, board.pieces_bits()); - assert_eq!(c, board.color_bits()); - assert_eq!(k, board.king_bits()); - } - - #[test] - fn test_bitboard_hash(pieces in 0u32..=u32::MAX, color in 0u32..=u32::MAX, kings in 0u32..=u32::MAX, c in 0u32..=u32::MAX, k in 0u32..=u32::MAX) { - let board1 = CheckersBitBoard::new(pieces, color, kings, PieceColor::Dark); - let board2 = CheckersBitBoard::new(pieces, color, kings, PieceColor::Dark); - let mut hasher1 = DefaultHasher::new(); - let mut hasher2 = DefaultHasher::new(); - board1.hash(&mut hasher1); - board2.hash(&mut hasher2); - assert_eq!(hasher1.finish(), hasher2.finish()); - } - - #[test] - fn test_bitboard_eq_identical(pieces in 0u32..=u32::MAX, color in 0u32..u32::MAX, kings in 0u32..=u32::MAX) { - let board1 = CheckersBitBoard::new(pieces, color, kings, PieceColor::Dark); - let board2 = CheckersBitBoard::new(pieces, color, kings, PieceColor::Dark); - assert_eq!(board1, board2); - } - - #[test] - fn test_bitboard_eq_empty(c1 in 0u32..u32::MAX, k1 in 0u32..=u32::MAX, c2 in 0u32..u32::MAX, k2 in 0u32..=u32::MAX) { - let board1 = CheckersBitBoard::new(0, c1, k1, PieceColor::Dark); - let board2 = CheckersBitBoard::new(0, c2, k2, PieceColor::Dark); - assert_eq!(board1, board2); - } - - #[test] - fn test_piece_at(p in 0u32..=u32::MAX, c in 0u32..=u32::MAX, k in 0u32..=u32::MAX, v in 0usize..32) { - let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); - - // just test for no crash - let _ = board.piece_at(v); - } - - #[test] - fn test_color_at_unchecked(p in 0u32..=u32::MAX, c in 0u32..=u32::MAX, k in 0u32..=u32::MAX, v in 0usize..32) { - let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); - - // just test for no crash - unsafe {let _ = board.color_at_unchecked(v);} - } - - #[test] - fn test_king_at_unchecked(p in 0u32..=u32::MAX, c in 0u32..=u32::MAX, k in 0u32..=u32::MAX, v in 0usize..32) { - let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); - unsafe {let _ = board.king_at_unchecked(v);} - } - - #[test] - fn test_color_at(p in 0u32..=u32::MAX, c in 0u32..=u32::MAX, k in 0u32..=u32::MAX, v in 0usize..32) { - let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); - - // just testing for no crash - let _ = board.color_at(v); - } - - #[test] - fn test_king_at(p in 0u32..=u32::MAX, c in 0u32..=u32::MAX, k in 0u32..=u32::MAX, v in 0usize..32) { - let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); - - // just testing for no crash - let _ = board.king_at(v); - } - - #[test] - fn test_move_piece_to(p in 0u32..=u32::MAX, c in 0u32..=u32::MAX, k in 0u32..=u32::MAX, s in 0usize..32, e in 0usize..32) { - let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); - let _ = unsafe {board.move_piece_to_unchecked(s, e)}; - } - - #[test] - fn test_move_forward(p in 0..u32::MAX, c in 0..u32::MAX, k in 0..u32::MAX, v in 0usize..32, a in 0usize..usize::MAX) { - if a <= usize::MAX - v { // so there's no overflow - let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); - let _ = unsafe {board.move_piece_forward_unchecked(v, a)}; - } - } - - #[test] - fn test_move_backward(p in 0..u32::MAX, c in 0..u32::MAX, k in 0..u32::MAX, v in 0usize..32, a in 0usize..usize::MAX) { - let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); - let _ = unsafe {board.move_piece_backward_unchecked(v, a)}; - } - - #[test] - fn test_move_forward_left(p in 0..u32::MAX, c in 0..u32::MAX, k in 0..u32::MAX) { - let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); - if board.piece_at(0) { - let board2 = unsafe {board.move_piece_forward_left_unchecked(0)}; - assert_eq!(board2.color_at(7), board.color_at(0)); - assert_eq!(board2.king_at(7), board.king_at(0)); - } - } - - #[test] - fn test_move_forward_right(p in 0..u32::MAX, c in 0..u32::MAX, k in 0..u32::MAX) { - let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); - if board.piece_at(18) { - let board2 = unsafe {board.move_piece_forward_right_unchecked(18)}; - assert_eq!(board2.color_at(19), board.color_at(18)); - assert_eq!(board2.king_at(19), board.king_at(18)); - } - } - - #[test] - fn test_move_backward_left(p in 0..u32::MAX, c in 0..u32::MAX, k in 0..u32::MAX) { - let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); - if board.piece_at(25) { - let board2 = unsafe {board.move_piece_backward_left_unchecked(25)}; - assert_eq!(board2.color_at(24), board.color_at(25)); - assert_eq!(board2.king_at(24), board.king_at(25)); - } - } - - #[test] - fn test_move_backward_right(p in 0..u32::MAX, c in 0..u32::MAX, k in 0..u32::MAX) { - let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); - if board.piece_at(11) { - let board2 = unsafe {board.move_piece_backward_right_unchecked(11)}; - assert_eq!(board2.color_at(4), board.color_at(11)); - assert_eq!(board2.king_at(4), board.king_at(11)); - } - } - - #[test] - fn test_clear_piece(p in 0..u32::MAX, c in 0..u32::MAX, k in 0..u32::MAX, v in 0usize..32) { - let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); - let board = board.clear_piece(v); - assert!(!board.piece_at(v)); - } - - #[test] - fn test_jump_forward_left(p in 0..u32::MAX, c in 0..u32::MAX, k in 0..u32::MAX) { - let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); - unsafe { - if board.piece_at(0) && board.piece_at(7) && !board.piece_at(14) && board.color_at_unchecked(0) != board.color_at_unchecked(7) { - let board2 = board.jump_piece_forward_left_unchecked(0); - assert!(!board2.piece_at(0)); - assert!(!board2.piece_at(7)); - assert!(board2.piece_at(14)); - assert_eq!(board2.color_at_unchecked(14), board.color_at_unchecked(0)); - assert_eq!(board2.king_at_unchecked(14), board.king_at_unchecked(0)); - } - } - } - - #[test] - fn test_jump_forward_right(p in 0..u32::MAX, c in 0..u32::MAX, k in 0..u32::MAX) { - let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); - unsafe { - if board.piece_at(18) && board.piece_at(19) && !board.piece_at(20) && board.color_at_unchecked(18) != board.color_at_unchecked(19) { - let board2 = board.jump_piece_forward_right_unchecked(18); - assert!(!board2.piece_at(18)); - assert!(!board2.piece_at(19)); - assert!(board2.piece_at(20)); - assert_eq!(board2.color_at_unchecked(20), board.color_at_unchecked(18)); - assert_eq!(board2.king_at_unchecked(20), board.king_at_unchecked(18)); - } - } - } - - #[test] - fn test_jump_backward_left(p in 0..u32::MAX, c in 0..u32::MAX, k in 0..u32::MAX) { - let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); - unsafe { - if board.piece_at(25) && board.piece_at(24) && !board.piece_at(23) && board.color_at_unchecked(25) != board.color_at_unchecked(24) { - let board2 = board.jump_piece_backward_left_unchecked(25); - assert!(!board2.piece_at(25)); - assert!(!board2.piece_at(24)); - assert!(board2.piece_at(23)); - assert_eq!(board2.color_at_unchecked(23), board.color_at_unchecked(25)); - assert_eq!(board2.king_at_unchecked(23), board.king_at_unchecked(25)); - } - } - } - - #[test] - fn test_jump_backward_right(p in 0..u32::MAX, c in 0..u32::MAX, k in 0..u32::MAX) { - let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); - unsafe { - if board.piece_at(11) && board.piece_at(4) && !board.piece_at(29) && board.color_at_unchecked(11) != board.color_at_unchecked(4) { - let board2 = board.jump_piece_backward_right_unchecked(11); - assert!(!board2.piece_at(11)); - assert!(!board2.piece_at(4)); - assert!(board2.piece_at(29)); - assert_eq!(board2.color_at_unchecked(29), board.color_at_unchecked(11)); - assert_eq!(board2.king_at_unchecked(29), board.king_at_unchecked(11)); - } - } - } -} - -#[test] -fn test_piece_at_empty_board() { - let board = CheckersBitBoard::new(0, 0, 0, PieceColor::Dark); - - // There should be no piece in any space - for i in 0..32 { - assert!(!board.piece_at(i)) - } -} - -#[test] -fn test_piece_at_space_zero() { - let board = CheckersBitBoard::new(1, 0, 0, PieceColor::Dark); - assert!(board.piece_at(0)); // There should be a piece in space 0 - - // There should be no piece in any other square - for i in 1..32 { - assert!(!board.piece_at(i)) - } -} - -#[test] -fn test_color_at_unchecked_all_light() { - let board = CheckersBitBoard::new(0, 0, 0, PieceColor::Dark); - - // All squares should be light - for i in 0..32 { - assert_eq!(unsafe { board.color_at_unchecked(i) }, PieceColor::Light) - } -} - -#[test] -fn test_color_at_unchecked_all_dark() { - let board = CheckersBitBoard::new(0, u32::MAX, 0, PieceColor::Dark); - - // All squares should be dark - for i in 0..32 { - assert_eq!(unsafe { board.color_at_unchecked(i) }, PieceColor::Dark) - } -} - -#[test] -fn test_king_at_unchecked_all_kings() { - let board = CheckersBitBoard::new(0, 0, u32::MAX, PieceColor::Dark); - - // All squares should be kings - for i in 0..32 { - assert!(unsafe { board.king_at_unchecked(i) }) - } -} - -#[test] -fn test_king_at_unchecked_one_king() { - let board = CheckersBitBoard::new(0, 0, 1, PieceColor::Dark); - - assert!(unsafe { board.king_at_unchecked(0) }); - - // All other squares should be peasants - for i in 1..32 { - assert!(!unsafe { board.king_at_unchecked(i) }) - } -} - -#[test] -fn test_default_bitboard() { - let board = CheckersBitBoard::default(); - let exemptions = [2, 28, 22, 16, 27, 21, 15, 9]; - let black = [18, 12, 6, 0, 19, 13, 7, 1, 26, 20, 14, 8]; - - for i in 0..32 { - if !exemptions.contains(&i) { - assert!(board.piece_at(i)); - assert!(!unsafe { board.king_at_unchecked(i) }); - - if black.contains(&i) { - assert_eq!(unsafe { board.color_at_unchecked(i) }, PieceColor::Dark) - } else { - assert_eq!(unsafe { board.color_at_unchecked(i) }, PieceColor::Light) - } - } else { - assert!(!board.piece_at(i)) - } - } -} - -#[test] -fn test_bitboard_eq_default() { - let board1 = CheckersBitBoard::new( - 0b11100111100111100111110111111011, - 0b11110011110000110000110000111100, - 0, - PieceColor::Dark, - ); - let board2 = CheckersBitBoard::new( - 0b11100111100111100111110111111011, - 0b11110011110000110000110000111100, - 0, - PieceColor::Dark, - ); - assert_eq!(board1, board2); -} - -#[test] -fn test_bitboard_neq_color() { - let board1 = CheckersBitBoard::new( - 0b11100111100111100111110111111011, - 0b11110011110000110000110000111100, - 0, - PieceColor::Dark, - ); - let board2 = CheckersBitBoard::new( - 0b11100111100111100111110111111011, - 465413646, - 0, - PieceColor::Dark, - ); - assert_ne!(board1, board2); -} - -#[test] -fn test_bitboard_neq_kings() { - let board1 = CheckersBitBoard::new( - 0b11100111100111100111110111111011, - 0b11110011110000110000110000111100, - 0, - PieceColor::Dark, - ); - let board2 = CheckersBitBoard::new( - 0b11100111100111100111110111111011, - 0b11110011110000110000110000111100, - 465413646, - PieceColor::Dark, - ); - assert_ne!(board1, board2); -} - -#[test] -fn test_color_at_empty() { - let board = CheckersBitBoard::new(0, 0, 0, PieceColor::Dark); - - for i in 0..32 { - assert_eq!(board.color_at(i), None) - } -} - -#[test] -fn test_color_at_specified_empty_colors() { - let board = CheckersBitBoard::new(0, 0b01, 0, PieceColor::Dark); - - for i in 0..32 { - assert_eq!(board.color_at(i), None) - } -} - -#[test] -fn test_color_at_some_colors() { - let board = CheckersBitBoard::new(3, 0b01, 0, PieceColor::Dark); - - assert_eq!(board.color_at(0), Some(PieceColor::Dark)); - assert_eq!(board.color_at(1), Some(PieceColor::Light)); - - for i in 2..32 { - assert_eq!(board.color_at(i), None) - } -} - -#[test] -fn test_king_at_empty() { - let board = CheckersBitBoard::new(0, 0, 0, PieceColor::Dark); - - for i in 0..32 { - assert_eq!(board.king_at(i), None) - } -} - -#[test] -fn test_king_at_specified_empty_colors() { - let board = CheckersBitBoard::new(0, 0, 0b01, PieceColor::Dark); - - for i in 0..32 { - assert_eq!(board.king_at(i), None) - } -} - -#[test] -fn test_king_at_some_colors() { - let board = CheckersBitBoard::new(3, 0, 0b01, PieceColor::Dark); - - assert_eq!(board.king_at(0), Some(true)); - assert_eq!(board.king_at(1), Some(false)); - - for i in 2..32 { - assert_eq!(board.king_at(i), None) - } -} - -#[test] -fn test_move_piece_to_default_board() { - let board = CheckersBitBoard::default(); - let board = unsafe { board.move_piece_to_unchecked(0, 5) }; - assert!(!board.piece_at(0)); - assert!(board.piece_at(5)); - assert_eq!(board.color_at(5).unwrap(), PieceColor::Dark); - assert!(board.king_at(5).unwrap()); - assert_eq!(board.turn, PieceColor::Light); -} - -#[test] -fn test_move_piece_forward_standard() { - let board = CheckersBitBoard::default(); - let board = unsafe { board.move_piece_forward_unchecked(14, 2) }; // go to 16 - assert!(!board.piece_at(14)); - assert!(board.piece_at(16)); - assert_eq!(board.color_at(16).unwrap(), PieceColor::Dark); - assert!(!board.king_at(16).unwrap()); - assert_eq!(board.turn, PieceColor::Light); -} - -#[test] -fn test_move_piece_forward_wrap() { - let board = CheckersBitBoard::default(); - let board = unsafe { board.move_piece_forward_unchecked(26, 8) }; // go to 9 - assert!(!board.piece_at(26)); - assert!(board.piece_at(2)); - assert_eq!(board.color_at(2).unwrap(), PieceColor::Dark); - assert!(!board.king_at(2).unwrap()); - assert_eq!(board.turn, PieceColor::Light); -} - -#[test] -fn test_move_piece_forward_left_to_king() { - let board = CheckersBitBoard::new(0b10000, 0b10000, 0, PieceColor::Dark); - let board = unsafe { board.move_piece_forward_left_unchecked(4) }; - assert!(board.piece_at(11)); - assert!(board.king_at(11).unwrap()); -} - -#[test] -fn test_move_piece_backward_left_to_king() { - let board = CheckersBitBoard::new(0b10, 0, 0, PieceColor::Dark); - let board = unsafe { board.move_piece_backward_left_unchecked(1) }; - assert!(board.piece_at(0)); - assert!(board.king_at(0).unwrap()); -} - -#[test] -fn test_move_piece_backward_standard() { - let board = CheckersBitBoard::default().flip_turn(); - let board = unsafe { board.move_piece_backward_unchecked(29, 14) }; // go to 15 - assert!(!board.piece_at(29)); - assert!(board.piece_at(15)); - assert_eq!(board.color_at(15).unwrap(), PieceColor::Light); - assert!(!board.king_at(15).unwrap()); - assert_eq!(board.turn, PieceColor::Dark); - assert_eq!(board.previous_turn, PieceColor::Light); -} - -#[test] -fn test_move_piece_backward_wrap() { - let board = CheckersBitBoard::default(); - let board = unsafe { board.move_piece_backward_unchecked(0, 4) }; // go to 28 - assert!(!board.piece_at(0)); - assert!(board.piece_at(28)); - assert_eq!(board.color_at(28).unwrap(), PieceColor::Dark); - assert!(!board.king_at(28).unwrap()); - assert_eq!(board.turn, PieceColor::Light); - assert_eq!(board.previous_turn, PieceColor::Dark); -} - -#[test] -// the specific tests have special values, and are different from the property tests -fn test_jump_forward_left_specific() { - let board = CheckersBitBoard::new(0b10000001, 1, 0, PieceColor::Dark); - - let board2 = unsafe { board.jump_piece_forward_left_unchecked(0) }; - assert!(!board2.piece_at(0)); - assert!(!board2.piece_at(7)); - assert!(board2.piece_at(14)); - assert_eq!(board2.color_at(14).unwrap(), board.color_at(0).unwrap()); - assert_eq!(board2.king_at(14).unwrap(), board.king_at(0).unwrap()); - assert_eq!(board2.turn, PieceColor::Light); -} - -#[test] -fn test_jump_forward_right_specific() { - let board = CheckersBitBoard::new( - 0b11000000000000000000, - 0b10000000000000000000, - 0, - PieceColor::Dark, - ); - - let board2 = unsafe { board.jump_piece_forward_right_unchecked(18) }; - assert!(!board2.piece_at(18)); - assert!(!board2.piece_at(19)); - assert!(board2.piece_at(20)); - assert_eq!(board2.color_at(20).unwrap(), board.color_at(18).unwrap()); - assert_eq!(board2.king_at(20).unwrap(), board.king_at(18).unwrap()); - assert_eq!(board2.turn, PieceColor::Light); -} - -#[test] -fn test_jump_backward_left_specific() { - let board = CheckersBitBoard::new( - 0b110000000000000000000000000, - 0b100000000000000000000000000, - 0, - PieceColor::Dark, - ); - - let board2 = unsafe { board.jump_piece_backward_left_unchecked(25) }; - assert!(!board2.piece_at(25)); - assert!(!board2.piece_at(24)); - assert!(board2.piece_at(23)); - assert_eq!(board2.color_at(23).unwrap(), board.color_at(25).unwrap()); - assert_eq!(board2.king_at(23).unwrap(), board.king_at(25).unwrap()); - assert_eq!(board2.turn, PieceColor::Light); -} - -#[test] -fn test_jump_backward_right_specific() { - let board = CheckersBitBoard::new(0b100000010000, 0b10000, 0, PieceColor::Dark); - - let board2 = unsafe { board.jump_piece_backward_right_unchecked(11) }; - assert!(!board2.piece_at(11)); - assert!(!board2.piece_at(4)); - assert!(board2.piece_at(29)); - assert_eq!(board2.color_at(29).unwrap(), board.color_at(11).unwrap()); - assert_eq!(board2.king_at(29).unwrap(), board.king_at(11).unwrap()); - assert_eq!(board2.turn, PieceColor::Light); -} - -#[test] -fn test_send() { - fn assert_send() {} - assert_send::(); -} - -#[test] -fn test_sync() { - fn assert_sync() {} - assert_sync::(); -} +use std::collections::hash_map::DefaultHasher; + +use proptest::prelude::*; + +use super::*; + +proptest! { + #[test] + fn test_bitboard_new(p in 0u32..=u32::MAX, c in 0u32..=u32::MAX, k in 0u32..=u32::MAX) { + let board = CheckersBitBoard::new(p, c, k, PieceColor::Dark); + assert_eq!(p, board.pieces); + assert_eq!(c, board.color); + assert_eq!(k, board.kings); + } + + #[test] + fn test_bits_fns(p in 0u32..=u32::MAX, c in 0u32..=u32::MAX, k in 0u32..=u32::MAX) { + let board = CheckersBitBoard::new(p, c, k, PieceColor::Dark); + assert_eq!(p, board.pieces_bits()); + assert_eq!(c, board.color_bits()); + assert_eq!(k, board.king_bits()); + } + + #[test] + fn test_bitboard_hash(pieces in 0u32..=u32::MAX, color in 0u32..=u32::MAX, kings in 0u32..=u32::MAX, c in 0u32..=u32::MAX, k in 0u32..=u32::MAX) { + let board1 = CheckersBitBoard::new(pieces, color, kings, PieceColor::Dark); + let board2 = CheckersBitBoard::new(pieces, color, kings, PieceColor::Dark); + let mut hasher1 = DefaultHasher::new(); + let mut hasher2 = DefaultHasher::new(); + board1.hash(&mut hasher1); + board2.hash(&mut hasher2); + assert_eq!(hasher1.finish(), hasher2.finish()); + } + + #[test] + fn test_bitboard_eq_identical(pieces in 0u32..=u32::MAX, color in 0u32..u32::MAX, kings in 0u32..=u32::MAX) { + let board1 = CheckersBitBoard::new(pieces, color, kings, PieceColor::Dark); + let board2 = CheckersBitBoard::new(pieces, color, kings, PieceColor::Dark); + assert_eq!(board1, board2); + } + + #[test] + fn test_bitboard_eq_empty(c1 in 0u32..u32::MAX, k1 in 0u32..=u32::MAX, c2 in 0u32..u32::MAX, k2 in 0u32..=u32::MAX) { + let board1 = CheckersBitBoard::new(0, c1, k1, PieceColor::Dark); + let board2 = CheckersBitBoard::new(0, c2, k2, PieceColor::Dark); + assert_eq!(board1, board2); + } + + #[test] + fn test_piece_at(p in 0u32..=u32::MAX, c in 0u32..=u32::MAX, k in 0u32..=u32::MAX, v in 0usize..32) { + let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); + + // just test for no crash + let _ = board.piece_at(v); + } + + #[test] + fn test_color_at_unchecked(p in 0u32..=u32::MAX, c in 0u32..=u32::MAX, k in 0u32..=u32::MAX, v in 0usize..32) { + let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); + + // just test for no crash + unsafe {let _ = board.color_at_unchecked(v);} + } + + #[test] + fn test_king_at_unchecked(p in 0u32..=u32::MAX, c in 0u32..=u32::MAX, k in 0u32..=u32::MAX, v in 0usize..32) { + let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); + unsafe {let _ = board.king_at_unchecked(v);} + } + + #[test] + fn test_color_at(p in 0u32..=u32::MAX, c in 0u32..=u32::MAX, k in 0u32..=u32::MAX, v in 0usize..32) { + let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); + + // just testing for no crash + let _ = board.color_at(v); + } + + #[test] + fn test_king_at(p in 0u32..=u32::MAX, c in 0u32..=u32::MAX, k in 0u32..=u32::MAX, v in 0usize..32) { + let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); + + // just testing for no crash + let _ = board.king_at(v); + } + + #[test] + fn test_move_piece_to(p in 0u32..=u32::MAX, c in 0u32..=u32::MAX, k in 0u32..=u32::MAX, s in 0usize..32, e in 0usize..32) { + let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); + let _ = unsafe {board.move_piece_to_unchecked(s, e)}; + } + + #[test] + fn test_move_forward(p in 0..u32::MAX, c in 0..u32::MAX, k in 0..u32::MAX, v in 0usize..32, a in 0usize..usize::MAX) { + if a <= usize::MAX - v { // so there's no overflow + let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); + let _ = unsafe {board.move_piece_forward_unchecked(v, a)}; + } + } + + #[test] + fn test_move_backward(p in 0..u32::MAX, c in 0..u32::MAX, k in 0..u32::MAX, v in 0usize..32, a in 0usize..usize::MAX) { + let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); + let _ = unsafe {board.move_piece_backward_unchecked(v, a)}; + } + + #[test] + fn test_move_forward_left(p in 0..u32::MAX, c in 0..u32::MAX, k in 0..u32::MAX) { + let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); + if board.piece_at(0) { + let board2 = unsafe {board.move_piece_forward_left_unchecked(0)}; + assert_eq!(board2.color_at(7), board.color_at(0)); + assert_eq!(board2.king_at(7), board.king_at(0)); + } + } + + #[test] + fn test_move_forward_right(p in 0..u32::MAX, c in 0..u32::MAX, k in 0..u32::MAX) { + let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); + if board.piece_at(18) { + let board2 = unsafe {board.move_piece_forward_right_unchecked(18)}; + assert_eq!(board2.color_at(19), board.color_at(18)); + assert_eq!(board2.king_at(19), board.king_at(18)); + } + } + + #[test] + fn test_move_backward_left(p in 0..u32::MAX, c in 0..u32::MAX, k in 0..u32::MAX) { + let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); + if board.piece_at(25) { + let board2 = unsafe {board.move_piece_backward_left_unchecked(25)}; + assert_eq!(board2.color_at(24), board.color_at(25)); + assert_eq!(board2.king_at(24), board.king_at(25)); + } + } + + #[test] + fn test_move_backward_right(p in 0..u32::MAX, c in 0..u32::MAX, k in 0..u32::MAX) { + let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); + if board.piece_at(11) { + let board2 = unsafe {board.move_piece_backward_right_unchecked(11)}; + assert_eq!(board2.color_at(4), board.color_at(11)); + assert_eq!(board2.king_at(4), board.king_at(11)); + } + } + + #[test] + fn test_clear_piece(p in 0..u32::MAX, c in 0..u32::MAX, k in 0..u32::MAX, v in 0usize..32) { + let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); + let board = board.clear_piece(v); + assert!(!board.piece_at(v)); + } + + #[test] + fn test_jump_forward_left(p in 0..u32::MAX, c in 0..u32::MAX, k in 0..u32::MAX) { + let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); + unsafe { + if board.piece_at(0) && board.piece_at(7) && !board.piece_at(14) && board.color_at_unchecked(0) != board.color_at_unchecked(7) { + let board2 = board.jump_piece_forward_left_unchecked(0); + assert!(!board2.piece_at(0)); + assert!(!board2.piece_at(7)); + assert!(board2.piece_at(14)); + assert_eq!(board2.color_at_unchecked(14), board.color_at_unchecked(0)); + assert_eq!(board2.king_at_unchecked(14), board.king_at_unchecked(0)); + } + } + } + + #[test] + fn test_jump_forward_right(p in 0..u32::MAX, c in 0..u32::MAX, k in 0..u32::MAX) { + let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); + unsafe { + if board.piece_at(18) && board.piece_at(19) && !board.piece_at(20) && board.color_at_unchecked(18) != board.color_at_unchecked(19) { + let board2 = board.jump_piece_forward_right_unchecked(18); + assert!(!board2.piece_at(18)); + assert!(!board2.piece_at(19)); + assert!(board2.piece_at(20)); + assert_eq!(board2.color_at_unchecked(20), board.color_at_unchecked(18)); + assert_eq!(board2.king_at_unchecked(20), board.king_at_unchecked(18)); + } + } + } + + #[test] + fn test_jump_backward_left(p in 0..u32::MAX, c in 0..u32::MAX, k in 0..u32::MAX) { + let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); + unsafe { + if board.piece_at(25) && board.piece_at(24) && !board.piece_at(23) && board.color_at_unchecked(25) != board.color_at_unchecked(24) { + let board2 = board.jump_piece_backward_left_unchecked(25); + assert!(!board2.piece_at(25)); + assert!(!board2.piece_at(24)); + assert!(board2.piece_at(23)); + assert_eq!(board2.color_at_unchecked(23), board.color_at_unchecked(25)); + assert_eq!(board2.king_at_unchecked(23), board.king_at_unchecked(25)); + } + } + } + + #[test] + fn test_jump_backward_right(p in 0..u32::MAX, c in 0..u32::MAX, k in 0..u32::MAX) { + let board = CheckersBitBoard ::new(p, c, k, PieceColor::Dark); + unsafe { + if board.piece_at(11) && board.piece_at(4) && !board.piece_at(29) && board.color_at_unchecked(11) != board.color_at_unchecked(4) { + let board2 = board.jump_piece_backward_right_unchecked(11); + assert!(!board2.piece_at(11)); + assert!(!board2.piece_at(4)); + assert!(board2.piece_at(29)); + assert_eq!(board2.color_at_unchecked(29), board.color_at_unchecked(11)); + assert_eq!(board2.king_at_unchecked(29), board.king_at_unchecked(11)); + } + } + } +} + +#[test] +fn test_piece_at_empty_board() { + let board = CheckersBitBoard::new(0, 0, 0, PieceColor::Dark); + + // There should be no piece in any space + for i in 0..32 { + assert!(!board.piece_at(i)) + } +} + +#[test] +fn test_piece_at_space_zero() { + let board = CheckersBitBoard::new(1, 0, 0, PieceColor::Dark); + assert!(board.piece_at(0)); // There should be a piece in space 0 + + // There should be no piece in any other square + for i in 1..32 { + assert!(!board.piece_at(i)) + } +} + +#[test] +fn test_color_at_unchecked_all_light() { + let board = CheckersBitBoard::new(0, 0, 0, PieceColor::Dark); + + // All squares should be light + for i in 0..32 { + assert_eq!(unsafe { board.color_at_unchecked(i) }, PieceColor::Light) + } +} + +#[test] +fn test_color_at_unchecked_all_dark() { + let board = CheckersBitBoard::new(0, u32::MAX, 0, PieceColor::Dark); + + // All squares should be dark + for i in 0..32 { + assert_eq!(unsafe { board.color_at_unchecked(i) }, PieceColor::Dark) + } +} + +#[test] +fn test_king_at_unchecked_all_kings() { + let board = CheckersBitBoard::new(0, 0, u32::MAX, PieceColor::Dark); + + // All squares should be kings + for i in 0..32 { + assert!(unsafe { board.king_at_unchecked(i) }) + } +} + +#[test] +fn test_king_at_unchecked_one_king() { + let board = CheckersBitBoard::new(0, 0, 1, PieceColor::Dark); + + assert!(unsafe { board.king_at_unchecked(0) }); + + // All other squares should be peasants + for i in 1..32 { + assert!(!unsafe { board.king_at_unchecked(i) }) + } +} + +#[test] +fn test_default_bitboard() { + let board = CheckersBitBoard::default(); + let exemptions = [2, 28, 22, 16, 27, 21, 15, 9]; + let black = [18, 12, 6, 0, 19, 13, 7, 1, 26, 20, 14, 8]; + + for i in 0..32 { + if !exemptions.contains(&i) { + assert!(board.piece_at(i)); + assert!(!unsafe { board.king_at_unchecked(i) }); + + if black.contains(&i) { + assert_eq!(unsafe { board.color_at_unchecked(i) }, PieceColor::Dark) + } else { + assert_eq!(unsafe { board.color_at_unchecked(i) }, PieceColor::Light) + } + } else { + assert!(!board.piece_at(i)) + } + } +} + +#[test] +fn test_bitboard_eq_default() { + let board1 = CheckersBitBoard::new( + 0b11100111100111100111110111111011, + 0b11110011110000110000110000111100, + 0, + PieceColor::Dark, + ); + let board2 = CheckersBitBoard::new( + 0b11100111100111100111110111111011, + 0b11110011110000110000110000111100, + 0, + PieceColor::Dark, + ); + assert_eq!(board1, board2); +} + +#[test] +fn test_bitboard_neq_color() { + let board1 = CheckersBitBoard::new( + 0b11100111100111100111110111111011, + 0b11110011110000110000110000111100, + 0, + PieceColor::Dark, + ); + let board2 = CheckersBitBoard::new( + 0b11100111100111100111110111111011, + 465413646, + 0, + PieceColor::Dark, + ); + assert_ne!(board1, board2); +} + +#[test] +fn test_bitboard_neq_kings() { + let board1 = CheckersBitBoard::new( + 0b11100111100111100111110111111011, + 0b11110011110000110000110000111100, + 0, + PieceColor::Dark, + ); + let board2 = CheckersBitBoard::new( + 0b11100111100111100111110111111011, + 0b11110011110000110000110000111100, + 465413646, + PieceColor::Dark, + ); + assert_ne!(board1, board2); +} + +#[test] +fn test_color_at_empty() { + let board = CheckersBitBoard::new(0, 0, 0, PieceColor::Dark); + + for i in 0..32 { + assert_eq!(board.color_at(i), None) + } +} + +#[test] +fn test_color_at_specified_empty_colors() { + let board = CheckersBitBoard::new(0, 0b01, 0, PieceColor::Dark); + + for i in 0..32 { + assert_eq!(board.color_at(i), None) + } +} + +#[test] +fn test_color_at_some_colors() { + let board = CheckersBitBoard::new(3, 0b01, 0, PieceColor::Dark); + + assert_eq!(board.color_at(0), Some(PieceColor::Dark)); + assert_eq!(board.color_at(1), Some(PieceColor::Light)); + + for i in 2..32 { + assert_eq!(board.color_at(i), None) + } +} + +#[test] +fn test_king_at_empty() { + let board = CheckersBitBoard::new(0, 0, 0, PieceColor::Dark); + + for i in 0..32 { + assert_eq!(board.king_at(i), None) + } +} + +#[test] +fn test_king_at_specified_empty_colors() { + let board = CheckersBitBoard::new(0, 0, 0b01, PieceColor::Dark); + + for i in 0..32 { + assert_eq!(board.king_at(i), None) + } +} + +#[test] +fn test_king_at_some_colors() { + let board = CheckersBitBoard::new(3, 0, 0b01, PieceColor::Dark); + + assert_eq!(board.king_at(0), Some(true)); + assert_eq!(board.king_at(1), Some(false)); + + for i in 2..32 { + assert_eq!(board.king_at(i), None) + } +} + +#[test] +fn test_move_piece_to_default_board() { + let board = CheckersBitBoard::default(); + let board = unsafe { board.move_piece_to_unchecked(0, 5) }; + assert!(!board.piece_at(0)); + assert!(board.piece_at(5)); + assert_eq!(board.color_at(5).unwrap(), PieceColor::Dark); + assert!(board.king_at(5).unwrap()); + assert_eq!(board.turn, PieceColor::Light); +} + +#[test] +fn test_move_piece_forward_standard() { + let board = CheckersBitBoard::default(); + let board = unsafe { board.move_piece_forward_unchecked(14, 2) }; // go to 16 + assert!(!board.piece_at(14)); + assert!(board.piece_at(16)); + assert_eq!(board.color_at(16).unwrap(), PieceColor::Dark); + assert!(!board.king_at(16).unwrap()); + assert_eq!(board.turn, PieceColor::Light); +} + +#[test] +fn test_move_piece_forward_wrap() { + let board = CheckersBitBoard::default(); + let board = unsafe { board.move_piece_forward_unchecked(26, 8) }; // go to 9 + assert!(!board.piece_at(26)); + assert!(board.piece_at(2)); + assert_eq!(board.color_at(2).unwrap(), PieceColor::Dark); + assert!(!board.king_at(2).unwrap()); + assert_eq!(board.turn, PieceColor::Light); +} + +#[test] +fn test_move_piece_forward_left_to_king() { + let board = CheckersBitBoard::new(0b10000, 0b10000, 0, PieceColor::Dark); + let board = unsafe { board.move_piece_forward_left_unchecked(4) }; + assert!(board.piece_at(11)); + assert!(board.king_at(11).unwrap()); +} + +#[test] +fn test_move_piece_backward_left_to_king() { + let board = CheckersBitBoard::new(0b10, 0, 0, PieceColor::Dark); + let board = unsafe { board.move_piece_backward_left_unchecked(1) }; + assert!(board.piece_at(0)); + assert!(board.king_at(0).unwrap()); +} + +#[test] +fn test_move_piece_backward_standard() { + let board = CheckersBitBoard::default().flip_turn(); + let board = unsafe { board.move_piece_backward_unchecked(29, 14) }; // go to 15 + assert!(!board.piece_at(29)); + assert!(board.piece_at(15)); + assert_eq!(board.color_at(15).unwrap(), PieceColor::Light); + assert!(!board.king_at(15).unwrap()); + assert_eq!(board.turn, PieceColor::Dark); + assert_eq!(board.previous_turn, PieceColor::Light); +} + +#[test] +fn test_move_piece_backward_wrap() { + let board = CheckersBitBoard::default(); + let board = unsafe { board.move_piece_backward_unchecked(0, 4) }; // go to 28 + assert!(!board.piece_at(0)); + assert!(board.piece_at(28)); + assert_eq!(board.color_at(28).unwrap(), PieceColor::Dark); + assert!(!board.king_at(28).unwrap()); + assert_eq!(board.turn, PieceColor::Light); + assert_eq!(board.previous_turn, PieceColor::Dark); +} + +#[test] +// the specific tests have special values, and are different from the property tests +fn test_jump_forward_left_specific() { + let board = CheckersBitBoard::new(0b10000001, 1, 0, PieceColor::Dark); + + let board2 = unsafe { board.jump_piece_forward_left_unchecked(0) }; + assert!(!board2.piece_at(0)); + assert!(!board2.piece_at(7)); + assert!(board2.piece_at(14)); + assert_eq!(board2.color_at(14).unwrap(), board.color_at(0).unwrap()); + assert_eq!(board2.king_at(14).unwrap(), board.king_at(0).unwrap()); + assert_eq!(board2.turn, PieceColor::Light); +} + +#[test] +fn test_jump_forward_right_specific() { + let board = CheckersBitBoard::new( + 0b11000000000000000000, + 0b10000000000000000000, + 0, + PieceColor::Dark, + ); + + let board2 = unsafe { board.jump_piece_forward_right_unchecked(18) }; + assert!(!board2.piece_at(18)); + assert!(!board2.piece_at(19)); + assert!(board2.piece_at(20)); + assert_eq!(board2.color_at(20).unwrap(), board.color_at(18).unwrap()); + assert_eq!(board2.king_at(20).unwrap(), board.king_at(18).unwrap()); + assert_eq!(board2.turn, PieceColor::Light); +} + +#[test] +fn test_jump_backward_left_specific() { + let board = CheckersBitBoard::new( + 0b110000000000000000000000000, + 0b100000000000000000000000000, + 0, + PieceColor::Dark, + ); + + let board2 = unsafe { board.jump_piece_backward_left_unchecked(25) }; + assert!(!board2.piece_at(25)); + assert!(!board2.piece_at(24)); + assert!(board2.piece_at(23)); + assert_eq!(board2.color_at(23).unwrap(), board.color_at(25).unwrap()); + assert_eq!(board2.king_at(23).unwrap(), board.king_at(25).unwrap()); + assert_eq!(board2.turn, PieceColor::Light); +} + +#[test] +fn test_jump_backward_right_specific() { + let board = CheckersBitBoard::new(0b100000010000, 0b10000, 0, PieceColor::Dark); + + let board2 = unsafe { board.jump_piece_backward_right_unchecked(11) }; + assert!(!board2.piece_at(11)); + assert!(!board2.piece_at(4)); + assert!(board2.piece_at(29)); + assert_eq!(board2.color_at(29).unwrap(), board.color_at(11).unwrap()); + assert_eq!(board2.king_at(29).unwrap(), board.king_at(11).unwrap()); + assert_eq!(board2.turn, PieceColor::Light); +} + +#[test] +fn test_send() { + fn assert_send() {} + assert_send::(); +} + +#[test] +fn test_sync() { + fn assert_sync() {} + assert_sync::(); +} diff --git a/model/src/color.rs b/model/src/color.rs old mode 100644 new mode 100755 diff --git a/model/src/coordinates.rs b/model/src/coordinates.rs old mode 100644 new mode 100755 index 0f45322..d16f900 --- a/model/src/coordinates.rs +++ b/model/src/coordinates.rs @@ -1,152 +1,152 @@ -use std::fmt::{Display, Formatter}; - -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] -pub struct SquareCoordinate { - rank: u8, - file: u8, -} - -impl SquareCoordinate { - pub fn new(rank: u8, file: u8) -> Self { - if rank > 8 { - panic!("A Square cannot have a rank greater than 8. Got {}", rank) - } else if file > 8 { - panic!("A Square cannot have a file greater than 8. Got {}", file) - } else { - Self { rank, file } - } - } - - pub fn from_normal_value(value: usize) -> Self { - static VALUE_COORDINATE_MAP: [SquareCoordinate; 32] = [ - SquareCoordinate { rank: 0, file: 6 }, - SquareCoordinate { rank: 0, file: 4 }, - SquareCoordinate { rank: 0, file: 2 }, - SquareCoordinate { rank: 0, file: 0 }, - SquareCoordinate { rank: 1, file: 7 }, - SquareCoordinate { rank: 1, file: 5 }, - SquareCoordinate { rank: 1, file: 3 }, - SquareCoordinate { rank: 1, file: 1 }, - SquareCoordinate { rank: 2, file: 6 }, - SquareCoordinate { rank: 2, file: 4 }, - SquareCoordinate { rank: 2, file: 2 }, - SquareCoordinate { rank: 2, file: 0 }, - SquareCoordinate { rank: 3, file: 7 }, - SquareCoordinate { rank: 3, file: 5 }, - SquareCoordinate { rank: 3, file: 3 }, - SquareCoordinate { rank: 3, file: 1 }, - SquareCoordinate { rank: 4, file: 6 }, - SquareCoordinate { rank: 4, file: 4 }, - SquareCoordinate { rank: 4, file: 2 }, - SquareCoordinate { rank: 4, file: 0 }, - SquareCoordinate { rank: 5, file: 7 }, - SquareCoordinate { rank: 5, file: 5 }, - SquareCoordinate { rank: 5, file: 3 }, - SquareCoordinate { rank: 5, file: 1 }, - SquareCoordinate { rank: 6, file: 6 }, - SquareCoordinate { rank: 6, file: 4 }, - SquareCoordinate { rank: 6, file: 2 }, - SquareCoordinate { rank: 6, file: 0 }, - SquareCoordinate { rank: 7, file: 7 }, - SquareCoordinate { rank: 7, file: 5 }, - SquareCoordinate { rank: 7, file: 3 }, - SquareCoordinate { rank: 7, file: 1 }, - ]; - - VALUE_COORDINATE_MAP[value] - } - - pub fn from_ampere_value(value: usize) -> Self { - static VALUE_COORDINATE_MAP: [SquareCoordinate; 32] = [ - SquareCoordinate { rank: 0, file: 6 }, - SquareCoordinate { rank: 1, file: 7 }, - SquareCoordinate { rank: 4, file: 0 }, - SquareCoordinate { rank: 5, file: 1 }, - SquareCoordinate { rank: 6, file: 2 }, - SquareCoordinate { rank: 7, file: 3 }, - SquareCoordinate { rank: 0, file: 4 }, - SquareCoordinate { rank: 1, file: 5 }, - SquareCoordinate { rank: 2, file: 6 }, - SquareCoordinate { rank: 3, file: 7 }, - SquareCoordinate { rank: 6, file: 0 }, - SquareCoordinate { rank: 7, file: 1 }, - SquareCoordinate { rank: 0, file: 2 }, - SquareCoordinate { rank: 1, file: 3 }, - SquareCoordinate { rank: 2, file: 4 }, - SquareCoordinate { rank: 3, file: 5 }, - SquareCoordinate { rank: 4, file: 6 }, - SquareCoordinate { rank: 5, file: 7 }, - SquareCoordinate { rank: 0, file: 0 }, - SquareCoordinate { rank: 1, file: 1 }, - SquareCoordinate { rank: 2, file: 2 }, - SquareCoordinate { rank: 3, file: 3 }, - SquareCoordinate { rank: 4, file: 4 }, - SquareCoordinate { rank: 5, file: 5 }, - SquareCoordinate { rank: 6, file: 6 }, - SquareCoordinate { rank: 7, file: 7 }, - SquareCoordinate { rank: 2, file: 0 }, - SquareCoordinate { rank: 3, file: 1 }, - SquareCoordinate { rank: 4, file: 2 }, - SquareCoordinate { rank: 5, file: 3 }, - SquareCoordinate { rank: 6, file: 4 }, - SquareCoordinate { rank: 7, file: 5 }, - ]; - - VALUE_COORDINATE_MAP[value] - } - - pub fn rank(self) -> u8 { - self.rank - } - - pub fn file(self) -> u8 { - self.file - } - - pub fn to_ampere_value(self) -> Option { - if self.rank % 2 == 0 { - if self.file % 2 == 0 { - Some(((18 - ((self.file / 2) * 6)) + ((self.rank / 2) * 8)) as usize % 32) - } else { - None - } - } else if self.file % 2 == 1 { - let column_value = match self.file { - 1 => 19, - 3 => 13, - 5 => 7, - 7 => 1, - _ => unreachable!(), - }; - let row_value = match self.rank { - 1 => 0, - 3 => 8, - 5 => 16, - 7 => 24, - _ => unreachable!(), - }; - Some((column_value + row_value) % 32) - } else { - None - } - } - - pub fn to_normal_value(self) -> Option { - if self.rank % 2 == 0 { - Some(self.rank as usize * 4 + self.file as usize % 4) - } else { - Some(self.rank as usize * 4 + self.file as usize % 4 + 1) - } - } -} - -impl Display for SquareCoordinate { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}{}", - char::from_u32((self.file + b'a') as u32).unwrap(), - self.rank + 1 - ) - } -} +use std::fmt::{Display, Formatter}; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct SquareCoordinate { + rank: u8, + file: u8, +} + +impl SquareCoordinate { + pub fn new(rank: u8, file: u8) -> Self { + if rank > 8 { + panic!("A Square cannot have a rank greater than 8. Got {}", rank) + } else if file > 8 { + panic!("A Square cannot have a file greater than 8. Got {}", file) + } else { + Self { rank, file } + } + } + + pub fn from_normal_value(value: usize) -> Self { + static VALUE_COORDINATE_MAP: [SquareCoordinate; 32] = [ + SquareCoordinate { rank: 0, file: 6 }, + SquareCoordinate { rank: 0, file: 4 }, + SquareCoordinate { rank: 0, file: 2 }, + SquareCoordinate { rank: 0, file: 0 }, + SquareCoordinate { rank: 1, file: 7 }, + SquareCoordinate { rank: 1, file: 5 }, + SquareCoordinate { rank: 1, file: 3 }, + SquareCoordinate { rank: 1, file: 1 }, + SquareCoordinate { rank: 2, file: 6 }, + SquareCoordinate { rank: 2, file: 4 }, + SquareCoordinate { rank: 2, file: 2 }, + SquareCoordinate { rank: 2, file: 0 }, + SquareCoordinate { rank: 3, file: 7 }, + SquareCoordinate { rank: 3, file: 5 }, + SquareCoordinate { rank: 3, file: 3 }, + SquareCoordinate { rank: 3, file: 1 }, + SquareCoordinate { rank: 4, file: 6 }, + SquareCoordinate { rank: 4, file: 4 }, + SquareCoordinate { rank: 4, file: 2 }, + SquareCoordinate { rank: 4, file: 0 }, + SquareCoordinate { rank: 5, file: 7 }, + SquareCoordinate { rank: 5, file: 5 }, + SquareCoordinate { rank: 5, file: 3 }, + SquareCoordinate { rank: 5, file: 1 }, + SquareCoordinate { rank: 6, file: 6 }, + SquareCoordinate { rank: 6, file: 4 }, + SquareCoordinate { rank: 6, file: 2 }, + SquareCoordinate { rank: 6, file: 0 }, + SquareCoordinate { rank: 7, file: 7 }, + SquareCoordinate { rank: 7, file: 5 }, + SquareCoordinate { rank: 7, file: 3 }, + SquareCoordinate { rank: 7, file: 1 }, + ]; + + VALUE_COORDINATE_MAP[value] + } + + pub fn from_ampere_value(value: usize) -> Self { + static VALUE_COORDINATE_MAP: [SquareCoordinate; 32] = [ + SquareCoordinate { rank: 0, file: 6 }, + SquareCoordinate { rank: 1, file: 7 }, + SquareCoordinate { rank: 4, file: 0 }, + SquareCoordinate { rank: 5, file: 1 }, + SquareCoordinate { rank: 6, file: 2 }, + SquareCoordinate { rank: 7, file: 3 }, + SquareCoordinate { rank: 0, file: 4 }, + SquareCoordinate { rank: 1, file: 5 }, + SquareCoordinate { rank: 2, file: 6 }, + SquareCoordinate { rank: 3, file: 7 }, + SquareCoordinate { rank: 6, file: 0 }, + SquareCoordinate { rank: 7, file: 1 }, + SquareCoordinate { rank: 0, file: 2 }, + SquareCoordinate { rank: 1, file: 3 }, + SquareCoordinate { rank: 2, file: 4 }, + SquareCoordinate { rank: 3, file: 5 }, + SquareCoordinate { rank: 4, file: 6 }, + SquareCoordinate { rank: 5, file: 7 }, + SquareCoordinate { rank: 0, file: 0 }, + SquareCoordinate { rank: 1, file: 1 }, + SquareCoordinate { rank: 2, file: 2 }, + SquareCoordinate { rank: 3, file: 3 }, + SquareCoordinate { rank: 4, file: 4 }, + SquareCoordinate { rank: 5, file: 5 }, + SquareCoordinate { rank: 6, file: 6 }, + SquareCoordinate { rank: 7, file: 7 }, + SquareCoordinate { rank: 2, file: 0 }, + SquareCoordinate { rank: 3, file: 1 }, + SquareCoordinate { rank: 4, file: 2 }, + SquareCoordinate { rank: 5, file: 3 }, + SquareCoordinate { rank: 6, file: 4 }, + SquareCoordinate { rank: 7, file: 5 }, + ]; + + VALUE_COORDINATE_MAP[value] + } + + pub fn rank(self) -> u8 { + self.rank + } + + pub fn file(self) -> u8 { + self.file + } + + pub fn to_ampere_value(self) -> Option { + if self.rank % 2 == 0 { + if self.file % 2 == 0 { + Some(((18 - ((self.file / 2) * 6)) + ((self.rank / 2) * 8)) as usize % 32) + } else { + None + } + } else if self.file % 2 == 1 { + let column_value = match self.file { + 1 => 19, + 3 => 13, + 5 => 7, + 7 => 1, + _ => unreachable!(), + }; + let row_value = match self.rank { + 1 => 0, + 3 => 8, + 5 => 16, + 7 => 24, + _ => unreachable!(), + }; + Some((column_value + row_value) % 32) + } else { + None + } + } + + pub fn to_normal_value(self) -> Option { + if self.rank % 2 == 0 { + Some(self.rank as usize * 4 + self.file as usize % 4) + } else { + Some(self.rank as usize * 4 + self.file as usize % 4 + 1) + } + } +} + +impl Display for SquareCoordinate { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}{}", + char::from_u32((self.file + b'a') as u32).unwrap(), + self.rank + 1 + ) + } +} diff --git a/model/src/lib.rs b/model/src/lib.rs old mode 100644 new mode 100755 index b3d8007..76a7419 --- a/model/src/lib.rs +++ b/model/src/lib.rs @@ -1,13 +1,13 @@ -mod board; -mod color; -mod coordinates; -mod moves; -mod piece; -mod possible_moves; - -pub use board::CheckersBitBoard; -pub use color::PieceColor; -pub use coordinates::SquareCoordinate; -pub use moves::{Move, MoveDirection}; -pub use piece::Piece; -pub use possible_moves::PossibleMoves; +mod board; +mod color; +mod coordinates; +mod moves; +mod piece; +mod possible_moves; + +pub use board::CheckersBitBoard; +pub use color::PieceColor; +pub use coordinates::SquareCoordinate; +pub use moves::{Move, MoveDirection}; +pub use piece::Piece; +pub use possible_moves::PossibleMoves; diff --git a/model/src/moves.rs b/model/src/moves.rs old mode 100644 new mode 100755 index c840e8f..c6dd060 --- a/model/src/moves.rs +++ b/model/src/moves.rs @@ -1,295 +1,295 @@ -use crate::{CheckersBitBoard, SquareCoordinate}; -use std::fmt::{Display, Formatter}; - -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] -#[repr(C)] -pub enum MoveDirection { - ForwardLeft = 0, - ForwardRight = 1, - BackwardLeft = 2, - BackwardRight = 3, -} - -/// A checkers move -// This is stored as a single byte. The first five bits represent the starting -// position, the next two bits represent the direction, and the last bit -// represents whether or not the move is a jump. -// -// starting position direction jump -// |--------------------|--------|----| -// 5 2 1 -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] -pub struct Move(u8); - -impl Move { - /// Create a new move - /// - /// # Arguments - /// - /// * `start` - The location of the piece that should move - /// * `direction` - The direction the piece should move in - /// * `jump` - Whether or not the piece should jump - pub const fn new(start: usize, direction: MoveDirection, jump: bool) -> Self { - Self(((start as u8) << 3) | ((direction as u8) << 1) | jump as u8) - } - - /// The stating position of the move - pub const fn start(self) -> u32 { - ((self.0 >> 3) & 0b11111) as u32 - } - - /// The direction the move goes in - pub const fn direction(self) -> MoveDirection { - match (self.0 >> 1) & 0b11 { - 0 => MoveDirection::ForwardLeft, - 1 => MoveDirection::ForwardRight, - 2 => MoveDirection::BackwardLeft, - 3 => MoveDirection::BackwardRight, - _ => unreachable!(), - } - } - - /// Returns `true` if the move is a jump - pub const fn is_jump(self) -> bool { - (self.0 & 1) == 1 - } - - /// Calculates the value of the end position of the move - pub const fn end_position(self) -> usize { - let dest = match self.is_jump() { - false => match self.direction() { - MoveDirection::ForwardLeft => (self.start() + 7) % 32, - MoveDirection::ForwardRight => (self.start() + 1) % 32, - MoveDirection::BackwardLeft => self.start().wrapping_sub(1) % 32, - MoveDirection::BackwardRight => self.start().wrapping_sub(7) % 32, - }, - true => match self.direction() { - MoveDirection::ForwardLeft => (self.start() + 14) % 32, - MoveDirection::ForwardRight => (self.start() + 2) % 32, - MoveDirection::BackwardLeft => self.start().wrapping_sub(2) % 32, - MoveDirection::BackwardRight => self.start().wrapping_sub(14) % 32, - }, - }; - dest as usize - } - - /// Calculates the value of the position that was jumped over - /// - /// # Safety - /// - /// The result of this function is undefined if the move isn't a jump - pub const unsafe fn jump_position(self) -> usize { - let pos = match self.direction() { - MoveDirection::ForwardLeft => (self.start() + 7) % 32, - MoveDirection::ForwardRight => (self.start() + 1) % 32, - MoveDirection::BackwardLeft => self.start().wrapping_sub(1) % 32, - MoveDirection::BackwardRight => self.start().wrapping_sub(7) % 32, - }; - pos as usize - } - - /// Apply the move to a board. This does not mutate the original board, - /// but instead returns a new one. - /// - /// # Arguments - /// - /// * `board` - The board to apply the move to - /// - /// # Panics - /// - /// Panics if the starting position of this move is greater than or equal to 32 - /// - /// # Safety - /// - /// Applying an illegal move to the board is undefined behavior. - /// This functions results in undefined behavior if: - /// * The piece moves in a direction which would move it outside of the board - /// * The starting position of this move doesn't contain a piece - /// * The end position already contains a piece - /// * A jump occurs where jumps are not allowed - /// * A move is not a jump even though jumps are available - pub const unsafe fn apply_to(self, board: CheckersBitBoard) -> CheckersBitBoard { - match self.is_jump() { - false => match self.direction() { - MoveDirection::ForwardLeft => { - board.move_piece_forward_left_unchecked(self.start() as usize) - } - MoveDirection::ForwardRight => { - board.move_piece_forward_right_unchecked(self.start() as usize) - } - MoveDirection::BackwardLeft => { - board.move_piece_backward_left_unchecked(self.start() as usize) - } - MoveDirection::BackwardRight => { - board.move_piece_backward_right_unchecked(self.start() as usize) - } - }, - true => match self.direction() { - MoveDirection::ForwardLeft => { - board.jump_piece_forward_left_unchecked(self.start() as usize) - } - MoveDirection::ForwardRight => { - board.jump_piece_forward_right_unchecked(self.start() as usize) - } - MoveDirection::BackwardLeft => { - board.jump_piece_backward_left_unchecked(self.start() as usize) - } - MoveDirection::BackwardRight => { - board.jump_piece_backward_right_unchecked(self.start() as usize) - } - }, - } - } -} - -impl Display for Move { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let Some(start) = - SquareCoordinate::from_ampere_value(self.start() as usize).to_normal_value() - else { - return Err(std::fmt::Error); - }; - - let separator = if self.is_jump() { "x" } else { "-" }; - - let Some(end) = SquareCoordinate::from_ampere_value(self.end_position()).to_normal_value() - else { - return Err(std::fmt::Error); - }; - - write!(f, "{start}{separator}{end}") - } -} - -#[cfg(test)] -mod tests { - - use super::*; - use proptest::prelude::*; - - proptest! { - #[test] - fn new(start in 0usize..32, jump in proptest::bool::ANY) { - let direction = MoveDirection::ForwardLeft; - let move_test = Move::new(start, direction, jump); - assert_eq!(move_test.start() as usize, start); - assert_eq!(move_test.direction(), direction); - assert_eq!(move_test.is_jump(), jump); - - let direction = MoveDirection::ForwardRight; - let move_test = Move::new(start, direction, jump); - assert_eq!(move_test.start() as usize, start); - assert_eq!(move_test.direction(), direction); - assert_eq!(move_test.is_jump(), jump); - - let direction = MoveDirection::BackwardLeft; - let move_test = Move::new(start, direction, jump); - assert_eq!(move_test.start() as usize, start); - assert_eq!(move_test.direction(), direction); - assert_eq!(move_test.is_jump(), jump); - - let direction = MoveDirection::BackwardRight; - let move_test = Move::new(start, direction, jump); - assert_eq!(move_test.start() as usize, start); - assert_eq!(move_test.direction(), direction); - assert_eq!(move_test.is_jump(), jump); - } - - #[test] - fn start(start in 0usize..32, jump in proptest::bool::ANY) { - let direction = MoveDirection::ForwardLeft; - let move_test = Move::new(start, direction, jump); - assert_eq!(move_test.start(), start as u32); - } - - #[test] - fn direction(start in 0usize..32, jump in proptest::bool::ANY) { - let direction = MoveDirection::ForwardLeft; - let move_test = Move::new(start, direction, jump); - assert_eq!(move_test.direction(), direction); - - let direction = MoveDirection::ForwardRight; - let move_test = Move::new(start, direction, jump); - assert_eq!(move_test.direction(), direction); - - let direction = MoveDirection::BackwardLeft; - let move_test = Move::new(start, direction, jump); - assert_eq!(move_test.direction(), direction); - - let direction = MoveDirection::BackwardRight; - let move_test = Move::new(start, direction, jump); - assert_eq!(move_test.direction(), direction); - } - - #[test] - fn is_jump(start in 0usize..32, jump in proptest::bool::ANY) { - let direction = MoveDirection::ForwardLeft; - let move_test = Move::new(start, direction, jump); - assert_eq!(move_test.is_jump(), jump); - } - } - - #[test] - fn end_position_forward_left_slide() { - let direction = MoveDirection::ForwardLeft; - let start = 8; - let move_test = Move::new(start, direction, false); - assert_eq!(move_test.end_position(), 15); - } - - #[test] - fn end_position_forward_right_slide() { - let direction = MoveDirection::ForwardRight; - let start = 26; - let move_test = Move::new(start, direction, false); - assert_eq!(move_test.end_position(), 27); - } - - #[test] - fn end_position_backward_right_slide() { - let direction = MoveDirection::BackwardRight; - let start = 2; - let move_test = Move::new(start, direction, false); - assert_eq!(move_test.end_position(), 27); - } - - #[test] - fn end_position_backward_left_slide() { - let direction = MoveDirection::BackwardLeft; - let start = 16; - let move_test = Move::new(start, direction, false); - assert_eq!(move_test.end_position(), 15); - } - - #[test] - fn end_position_forward_left_jump() { - let direction = MoveDirection::ForwardLeft; - let start = 8; - let move_test = Move::new(start, direction, true); - assert_eq!(move_test.end_position(), 22); - } - - #[test] - fn end_position_forward_right_jump() { - let direction = MoveDirection::ForwardRight; - let start = 26; - let move_test = Move::new(start, direction, true); - assert_eq!(move_test.end_position(), 28); - } - - #[test] - fn end_position_backward_right_jump() { - let direction = MoveDirection::BackwardRight; - let start = 2; - let move_test = Move::new(start, direction, true); - assert_eq!(move_test.end_position(), 20); - } - - #[test] - fn end_position_backward_left_jump() { - let direction = MoveDirection::BackwardLeft; - let start = 16; - let move_test = Move::new(start, direction, true); - assert_eq!(move_test.end_position(), 14); - } -} +use crate::{CheckersBitBoard, SquareCoordinate}; +use std::fmt::{Display, Formatter}; + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +#[repr(C)] +pub enum MoveDirection { + ForwardLeft = 0, + ForwardRight = 1, + BackwardLeft = 2, + BackwardRight = 3, +} + +/// A checkers move +// This is stored as a single byte. The first five bits represent the starting +// position, the next two bits represent the direction, and the last bit +// represents whether or not the move is a jump. +// +// starting position direction jump +// |--------------------|--------|----| +// 5 2 1 +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct Move(u8); + +impl Move { + /// Create a new move + /// + /// # Arguments + /// + /// * `start` - The location of the piece that should move + /// * `direction` - The direction the piece should move in + /// * `jump` - Whether or not the piece should jump + pub const fn new(start: usize, direction: MoveDirection, jump: bool) -> Self { + Self(((start as u8) << 3) | ((direction as u8) << 1) | jump as u8) + } + + /// The stating position of the move + pub const fn start(self) -> u32 { + ((self.0 >> 3) & 0b11111) as u32 + } + + /// The direction the move goes in + pub const fn direction(self) -> MoveDirection { + match (self.0 >> 1) & 0b11 { + 0 => MoveDirection::ForwardLeft, + 1 => MoveDirection::ForwardRight, + 2 => MoveDirection::BackwardLeft, + 3 => MoveDirection::BackwardRight, + _ => unreachable!(), + } + } + + /// Returns `true` if the move is a jump + pub const fn is_jump(self) -> bool { + (self.0 & 1) == 1 + } + + /// Calculates the value of the end position of the move + pub const fn end_position(self) -> usize { + let dest = match self.is_jump() { + false => match self.direction() { + MoveDirection::ForwardLeft => (self.start() + 7) % 32, + MoveDirection::ForwardRight => (self.start() + 1) % 32, + MoveDirection::BackwardLeft => self.start().wrapping_sub(1) % 32, + MoveDirection::BackwardRight => self.start().wrapping_sub(7) % 32, + }, + true => match self.direction() { + MoveDirection::ForwardLeft => (self.start() + 14) % 32, + MoveDirection::ForwardRight => (self.start() + 2) % 32, + MoveDirection::BackwardLeft => self.start().wrapping_sub(2) % 32, + MoveDirection::BackwardRight => self.start().wrapping_sub(14) % 32, + }, + }; + dest as usize + } + + /// Calculates the value of the position that was jumped over + /// + /// # Safety + /// + /// The result of this function is undefined if the move isn't a jump + pub const unsafe fn jump_position(self) -> usize { + let pos = match self.direction() { + MoveDirection::ForwardLeft => (self.start() + 7) % 32, + MoveDirection::ForwardRight => (self.start() + 1) % 32, + MoveDirection::BackwardLeft => self.start().wrapping_sub(1) % 32, + MoveDirection::BackwardRight => self.start().wrapping_sub(7) % 32, + }; + pos as usize + } + + /// Apply the move to a board. This does not mutate the original board, + /// but instead returns a new one. + /// + /// # Arguments + /// + /// * `board` - The board to apply the move to + /// + /// # Panics + /// + /// Panics if the starting position of this move is greater than or equal to 32 + /// + /// # Safety + /// + /// Applying an illegal move to the board is undefined behavior. + /// This functions results in undefined behavior if: + /// * The piece moves in a direction which would move it outside of the board + /// * The starting position of this move doesn't contain a piece + /// * The end position already contains a piece + /// * A jump occurs where jumps are not allowed + /// * A move is not a jump even though jumps are available + pub const unsafe fn apply_to(self, board: CheckersBitBoard) -> CheckersBitBoard { + match self.is_jump() { + false => match self.direction() { + MoveDirection::ForwardLeft => { + board.move_piece_forward_left_unchecked(self.start() as usize) + } + MoveDirection::ForwardRight => { + board.move_piece_forward_right_unchecked(self.start() as usize) + } + MoveDirection::BackwardLeft => { + board.move_piece_backward_left_unchecked(self.start() as usize) + } + MoveDirection::BackwardRight => { + board.move_piece_backward_right_unchecked(self.start() as usize) + } + }, + true => match self.direction() { + MoveDirection::ForwardLeft => { + board.jump_piece_forward_left_unchecked(self.start() as usize) + } + MoveDirection::ForwardRight => { + board.jump_piece_forward_right_unchecked(self.start() as usize) + } + MoveDirection::BackwardLeft => { + board.jump_piece_backward_left_unchecked(self.start() as usize) + } + MoveDirection::BackwardRight => { + board.jump_piece_backward_right_unchecked(self.start() as usize) + } + }, + } + } +} + +impl Display for Move { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let Some(start) = + SquareCoordinate::from_ampere_value(self.start() as usize).to_normal_value() + else { + return Err(std::fmt::Error); + }; + + let separator = if self.is_jump() { "x" } else { "-" }; + + let Some(end) = SquareCoordinate::from_ampere_value(self.end_position()).to_normal_value() + else { + return Err(std::fmt::Error); + }; + + write!(f, "{start}{separator}{end}") + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use proptest::prelude::*; + + proptest! { + #[test] + fn new(start in 0usize..32, jump in proptest::bool::ANY) { + let direction = MoveDirection::ForwardLeft; + let move_test = Move::new(start, direction, jump); + assert_eq!(move_test.start() as usize, start); + assert_eq!(move_test.direction(), direction); + assert_eq!(move_test.is_jump(), jump); + + let direction = MoveDirection::ForwardRight; + let move_test = Move::new(start, direction, jump); + assert_eq!(move_test.start() as usize, start); + assert_eq!(move_test.direction(), direction); + assert_eq!(move_test.is_jump(), jump); + + let direction = MoveDirection::BackwardLeft; + let move_test = Move::new(start, direction, jump); + assert_eq!(move_test.start() as usize, start); + assert_eq!(move_test.direction(), direction); + assert_eq!(move_test.is_jump(), jump); + + let direction = MoveDirection::BackwardRight; + let move_test = Move::new(start, direction, jump); + assert_eq!(move_test.start() as usize, start); + assert_eq!(move_test.direction(), direction); + assert_eq!(move_test.is_jump(), jump); + } + + #[test] + fn start(start in 0usize..32, jump in proptest::bool::ANY) { + let direction = MoveDirection::ForwardLeft; + let move_test = Move::new(start, direction, jump); + assert_eq!(move_test.start(), start as u32); + } + + #[test] + fn direction(start in 0usize..32, jump in proptest::bool::ANY) { + let direction = MoveDirection::ForwardLeft; + let move_test = Move::new(start, direction, jump); + assert_eq!(move_test.direction(), direction); + + let direction = MoveDirection::ForwardRight; + let move_test = Move::new(start, direction, jump); + assert_eq!(move_test.direction(), direction); + + let direction = MoveDirection::BackwardLeft; + let move_test = Move::new(start, direction, jump); + assert_eq!(move_test.direction(), direction); + + let direction = MoveDirection::BackwardRight; + let move_test = Move::new(start, direction, jump); + assert_eq!(move_test.direction(), direction); + } + + #[test] + fn is_jump(start in 0usize..32, jump in proptest::bool::ANY) { + let direction = MoveDirection::ForwardLeft; + let move_test = Move::new(start, direction, jump); + assert_eq!(move_test.is_jump(), jump); + } + } + + #[test] + fn end_position_forward_left_slide() { + let direction = MoveDirection::ForwardLeft; + let start = 8; + let move_test = Move::new(start, direction, false); + assert_eq!(move_test.end_position(), 15); + } + + #[test] + fn end_position_forward_right_slide() { + let direction = MoveDirection::ForwardRight; + let start = 26; + let move_test = Move::new(start, direction, false); + assert_eq!(move_test.end_position(), 27); + } + + #[test] + fn end_position_backward_right_slide() { + let direction = MoveDirection::BackwardRight; + let start = 2; + let move_test = Move::new(start, direction, false); + assert_eq!(move_test.end_position(), 27); + } + + #[test] + fn end_position_backward_left_slide() { + let direction = MoveDirection::BackwardLeft; + let start = 16; + let move_test = Move::new(start, direction, false); + assert_eq!(move_test.end_position(), 15); + } + + #[test] + fn end_position_forward_left_jump() { + let direction = MoveDirection::ForwardLeft; + let start = 8; + let move_test = Move::new(start, direction, true); + assert_eq!(move_test.end_position(), 22); + } + + #[test] + fn end_position_forward_right_jump() { + let direction = MoveDirection::ForwardRight; + let start = 26; + let move_test = Move::new(start, direction, true); + assert_eq!(move_test.end_position(), 28); + } + + #[test] + fn end_position_backward_right_jump() { + let direction = MoveDirection::BackwardRight; + let start = 2; + let move_test = Move::new(start, direction, true); + assert_eq!(move_test.end_position(), 20); + } + + #[test] + fn end_position_backward_left_jump() { + let direction = MoveDirection::BackwardLeft; + let start = 16; + let move_test = Move::new(start, direction, true); + assert_eq!(move_test.end_position(), 14); + } +} diff --git a/model/src/piece.rs b/model/src/piece.rs old mode 100644 new mode 100755 index f36e0a4..860142d --- a/model/src/piece.rs +++ b/model/src/piece.rs @@ -1,21 +1,21 @@ -use crate::PieceColor; - -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] -pub struct Piece { - king: bool, - color: PieceColor, -} - -impl Piece { - pub(crate) const fn new(king: bool, color: PieceColor) -> Self { - Self { king, color } - } - - pub const fn is_king(self) -> bool { - self.king - } - - pub const fn color(self) -> PieceColor { - self.color - } -} +use crate::PieceColor; + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct Piece { + king: bool, + color: PieceColor, +} + +impl Piece { + pub(crate) const fn new(king: bool, color: PieceColor) -> Self { + Self { king, color } + } + + pub const fn is_king(self) -> bool { + self.king + } + + pub const fn color(self) -> PieceColor { + self.color + } +} diff --git a/model/src/possible_moves.rs b/model/src/possible_moves.rs old mode 100644 new mode 100755 index ef05048..40d9df7 --- a/model/src/possible_moves.rs +++ b/model/src/possible_moves.rs @@ -1,1078 +1,1078 @@ -use crate::moves::{Move, MoveDirection}; -use crate::{CheckersBitBoard, PieceColor}; - -use std::mem::MaybeUninit; - -// The maximum number of available moves in any given position -pub const POSSIBLE_MOVES_ITER_SIZE: usize = 50; - -/// A struct containing the possible moves in a particular checkers position -#[derive(Copy, Clone, Debug)] -pub struct PossibleMoves { - forward_left_movers: u32, - forward_right_movers: u32, - backward_left_movers: u32, - backward_right_movers: u32, -} - -/// An iterator of possible checkers moves for a particular position -pub struct PossibleMovesIter { - /// A pointer to an array of possibly uninitialized checkers moves - moves: [MaybeUninit; POSSIBLE_MOVES_ITER_SIZE], - - /// The current index into the moves array - index: usize, - - // The number of initialized moves in the array - length: usize, -} - -impl PossibleMovesIter { - fn add_slide_forward_left(&mut self, possible_moves: PossibleMoves) { - if (possible_moves.forward_left_movers >> SQUARE) & 1 != 0 { - debug_assert!(self.length < POSSIBLE_MOVES_ITER_SIZE); - let ptr = unsafe { self.moves.as_mut().get_unchecked_mut(self.length) }; - *ptr = MaybeUninit::new(Move::new(SQUARE, MoveDirection::ForwardLeft, false)); - self.length += 1; - } - } - - fn add_slide_forward_right(&mut self, possible_moves: PossibleMoves) { - if (possible_moves.forward_right_movers >> SQUARE) & 1 != 0 { - debug_assert!(self.length < POSSIBLE_MOVES_ITER_SIZE); - let ptr = unsafe { self.moves.as_mut().get_unchecked_mut(self.length) }; - *ptr = MaybeUninit::new(Move::new(SQUARE, MoveDirection::ForwardRight, false)); - self.length += 1; - } - } - - fn add_slide_backward_left(&mut self, possible_moves: PossibleMoves) { - if (possible_moves.backward_left_movers >> SQUARE) & 1 != 0 { - debug_assert!(self.length < POSSIBLE_MOVES_ITER_SIZE); - let ptr = unsafe { self.moves.as_mut().get_unchecked_mut(self.length) }; - *ptr = MaybeUninit::new(Move::new(SQUARE, MoveDirection::BackwardLeft, false)); - self.length += 1; - } - } - - fn add_slide_backward_right(&mut self, possible_moves: PossibleMoves) { - if (possible_moves.backward_right_movers >> SQUARE) & 1 != 0 { - debug_assert!(self.length < POSSIBLE_MOVES_ITER_SIZE); - let ptr = unsafe { self.moves.as_mut().get_unchecked_mut(self.length) }; - *ptr = MaybeUninit::new(Move::new(SQUARE, MoveDirection::BackwardRight, false)); - self.length += 1; - } - } - - fn add_jump_forward_left(&mut self, possible_moves: PossibleMoves) { - if (possible_moves.forward_left_movers >> SQUARE) & 1 != 0 { - debug_assert!(self.length < POSSIBLE_MOVES_ITER_SIZE); - let ptr = unsafe { self.moves.as_mut().get_unchecked_mut(self.length) }; - *ptr = MaybeUninit::new(Move::new(SQUARE, MoveDirection::ForwardLeft, true)); - self.length += 1; - } - } - - fn add_jump_forward_right(&mut self, possible_moves: PossibleMoves) { - if (possible_moves.forward_right_movers >> SQUARE) & 1 != 0 { - debug_assert!(self.length < POSSIBLE_MOVES_ITER_SIZE); - let ptr = unsafe { self.moves.as_mut().get_unchecked_mut(self.length) }; - *ptr = MaybeUninit::new(Move::new(SQUARE, MoveDirection::ForwardRight, true)); - self.length += 1; - } - } - - fn add_jump_backward_left(&mut self, possible_moves: PossibleMoves) { - if (possible_moves.backward_left_movers >> SQUARE) & 1 != 0 { - debug_assert!(self.length < POSSIBLE_MOVES_ITER_SIZE); - let ptr = unsafe { self.moves.as_mut().get_unchecked_mut(self.length) }; - *ptr = MaybeUninit::new(Move::new(SQUARE, MoveDirection::BackwardLeft, true)); - self.length += 1; - } - } - - fn add_jump_backward_right(&mut self, possible_moves: PossibleMoves) { - if (possible_moves.backward_right_movers >> SQUARE) & 1 != 0 { - debug_assert!(self.length < POSSIBLE_MOVES_ITER_SIZE); - let ptr = unsafe { self.moves.as_mut().get_unchecked_mut(self.length) }; - *ptr = MaybeUninit::new(Move::new(SQUARE, MoveDirection::BackwardRight, true)); - self.length += 1; - } - } -} - -unsafe impl Send for PossibleMovesIter {} - -impl Iterator for PossibleMovesIter { - type Item = Move; - - fn next(&mut self) -> Option { - if self.length > self.index { - debug_assert!(self.index < POSSIBLE_MOVES_ITER_SIZE); - let next_move = unsafe { self.moves.as_ref().get_unchecked(self.index).assume_init() }; - self.index += 1; - Some(next_move) - } else { - None - } - } - - // TODO test - fn size_hint(&self) -> (usize, Option) { - let remaining = self.length - self.index; - (remaining, Some(remaining)) - } - - // TODO test - fn count(self) -> usize - where - Self: Sized, - { - self.length - self.index - } - - // TODO test - fn last(self) -> Option - where - Self: Sized, - { - debug_assert!(self.length <= POSSIBLE_MOVES_ITER_SIZE); - if self.length == 0 { - None - } else { - Some(unsafe { - self.moves - .as_ref() - .get_unchecked(self.length - 1) - .assume_init() - }) - } - } - - // TODO test - fn nth(&mut self, n: usize) -> Option { - if self.length == 0 || self.length - self.index < n { - None - } else { - self.index += n; - let current_move = - unsafe { self.moves.as_ref().get_unchecked(self.index).assume_init() }; - self.index += 1; - Some(current_move) - } - } -} - -impl IntoIterator for PossibleMoves { - type Item = Move; - type IntoIter = PossibleMovesIter; - - // TODO test - fn into_iter(self) -> Self::IntoIter { - let moves = [MaybeUninit::uninit(); POSSIBLE_MOVES_ITER_SIZE]; - let mut iter = PossibleMovesIter { - moves, - index: 0, - length: 0, - }; - - if self.can_jump() { - iter.add_jump_forward_left::<0>(self); - iter.add_jump_forward_left::<1>(self); - iter.add_jump_forward_left::<6>(self); - iter.add_jump_forward_left::<7>(self); - iter.add_jump_forward_left::<8>(self); - iter.add_jump_forward_left::<9>(self); - iter.add_jump_forward_left::<12>(self); - iter.add_jump_forward_left::<13>(self); - iter.add_jump_forward_left::<14>(self); - iter.add_jump_forward_left::<15>(self); - iter.add_jump_forward_left::<16>(self); - iter.add_jump_forward_left::<17>(self); - iter.add_jump_forward_left::<20>(self); - iter.add_jump_forward_left::<21>(self); - iter.add_jump_forward_left::<22>(self); - iter.add_jump_forward_left::<23>(self); - iter.add_jump_forward_left::<28>(self); - iter.add_jump_forward_left::<29>(self); - - iter.add_jump_forward_right::<2>(self); - iter.add_jump_forward_right::<3>(self); - iter.add_jump_forward_right::<6>(self); - iter.add_jump_forward_right::<7>(self); - iter.add_jump_forward_right::<12>(self); - iter.add_jump_forward_right::<13>(self); - iter.add_jump_forward_right::<14>(self); - iter.add_jump_forward_right::<15>(self); - iter.add_jump_forward_right::<18>(self); - iter.add_jump_forward_right::<19>(self); - iter.add_jump_forward_right::<20>(self); - iter.add_jump_forward_right::<21>(self); - iter.add_jump_forward_right::<22>(self); - iter.add_jump_forward_right::<23>(self); - iter.add_jump_forward_right::<26>(self); - iter.add_jump_forward_right::<27>(self); - iter.add_jump_forward_right::<28>(self); - iter.add_jump_forward_right::<29>(self); - - iter.add_jump_backward_left::<4>(self); - iter.add_jump_backward_left::<5>(self); - iter.add_jump_backward_left::<8>(self); - iter.add_jump_backward_left::<9>(self); - iter.add_jump_backward_left::<14>(self); - iter.add_jump_backward_left::<15>(self); - iter.add_jump_backward_left::<16>(self); - iter.add_jump_backward_left::<17>(self); - iter.add_jump_backward_left::<20>(self); - iter.add_jump_backward_left::<21>(self); - iter.add_jump_backward_left::<22>(self); - iter.add_jump_backward_left::<23>(self); - iter.add_jump_backward_left::<24>(self); - iter.add_jump_backward_left::<25>(self); - iter.add_jump_backward_left::<28>(self); - iter.add_jump_backward_left::<29>(self); - iter.add_jump_backward_left::<30>(self); - iter.add_jump_backward_left::<31>(self); - - iter.add_jump_backward_right::<2>(self); - iter.add_jump_backward_right::<3>(self); - iter.add_jump_backward_right::<4>(self); - iter.add_jump_backward_right::<5>(self); - iter.add_jump_backward_right::<10>(self); - iter.add_jump_backward_right::<11>(self); - iter.add_jump_backward_right::<14>(self); - iter.add_jump_backward_right::<15>(self); - iter.add_jump_backward_right::<20>(self); - iter.add_jump_backward_right::<21>(self); - iter.add_jump_backward_right::<22>(self); - iter.add_jump_backward_right::<23>(self); - iter.add_jump_backward_right::<26>(self); - iter.add_jump_backward_right::<27>(self); - iter.add_jump_backward_right::<28>(self); - iter.add_jump_backward_right::<29>(self); - iter.add_jump_backward_right::<30>(self); - iter.add_jump_backward_right::<31>(self); - } else { - iter.add_slide_forward_left::<0>(self); - iter.add_slide_forward_left::<1>(self); - iter.add_slide_forward_left::<3>(self); - iter.add_slide_forward_left::<4>(self); - iter.add_slide_forward_left::<6>(self); - iter.add_slide_forward_left::<7>(self); - iter.add_slide_forward_left::<8>(self); - iter.add_slide_forward_left::<9>(self); - iter.add_slide_forward_left::<12>(self); - iter.add_slide_forward_left::<13>(self); - iter.add_slide_forward_left::<14>(self); - iter.add_slide_forward_left::<15>(self); - iter.add_slide_forward_left::<16>(self); - iter.add_slide_forward_left::<17>(self); - iter.add_slide_forward_left::<19>(self); - iter.add_slide_forward_left::<20>(self); - iter.add_slide_forward_left::<21>(self); - iter.add_slide_forward_left::<22>(self); - iter.add_slide_forward_left::<23>(self); - iter.add_slide_forward_left::<24>(self); - iter.add_slide_forward_left::<27>(self); - iter.add_slide_forward_left::<28>(self); - iter.add_slide_forward_left::<29>(self); - iter.add_slide_forward_left::<30>(self); - - iter.add_slide_forward_right::<0>(self); - iter.add_slide_forward_right::<2>(self); - iter.add_slide_forward_right::<3>(self); - iter.add_slide_forward_right::<4>(self); - iter.add_slide_forward_right::<6>(self); - iter.add_slide_forward_right::<7>(self); - iter.add_slide_forward_right::<8>(self); - iter.add_slide_forward_right::<10>(self); - iter.add_slide_forward_right::<12>(self); - iter.add_slide_forward_right::<13>(self); - iter.add_slide_forward_right::<14>(self); - iter.add_slide_forward_right::<15>(self); - iter.add_slide_forward_right::<16>(self); - iter.add_slide_forward_right::<18>(self); - iter.add_slide_forward_right::<19>(self); - iter.add_slide_forward_right::<20>(self); - iter.add_slide_forward_right::<21>(self); - iter.add_slide_forward_right::<22>(self); - iter.add_slide_forward_right::<23>(self); - iter.add_slide_forward_right::<24>(self); - iter.add_slide_forward_right::<26>(self); - iter.add_slide_forward_right::<27>(self); - iter.add_slide_forward_right::<28>(self); - iter.add_slide_forward_right::<29>(self); - iter.add_slide_forward_right::<30>(self); - - iter.add_slide_backward_left::<1>(self); - iter.add_slide_backward_left::<3>(self); - iter.add_slide_backward_left::<4>(self); - iter.add_slide_backward_left::<5>(self); - iter.add_slide_backward_left::<7>(self); - iter.add_slide_backward_left::<8>(self); - iter.add_slide_backward_left::<9>(self); - iter.add_slide_backward_left::<11>(self); - iter.add_slide_backward_left::<13>(self); - iter.add_slide_backward_left::<14>(self); - iter.add_slide_backward_left::<15>(self); - iter.add_slide_backward_left::<16>(self); - iter.add_slide_backward_left::<17>(self); - iter.add_slide_backward_left::<19>(self); - iter.add_slide_backward_left::<20>(self); - iter.add_slide_backward_left::<21>(self); - iter.add_slide_backward_left::<22>(self); - iter.add_slide_backward_left::<23>(self); - iter.add_slide_backward_left::<24>(self); - iter.add_slide_backward_left::<25>(self); - iter.add_slide_backward_left::<27>(self); - iter.add_slide_backward_left::<28>(self); - iter.add_slide_backward_left::<29>(self); - iter.add_slide_backward_left::<30>(self); - iter.add_slide_backward_left::<31>(self); - - iter.add_slide_backward_right::<2>(self); - iter.add_slide_backward_right::<3>(self); - iter.add_slide_backward_right::<4>(self); - iter.add_slide_backward_right::<5>(self); - iter.add_slide_backward_right::<7>(self); - iter.add_slide_backward_right::<8>(self); - iter.add_slide_backward_right::<10>(self); - iter.add_slide_backward_right::<11>(self); - iter.add_slide_backward_right::<13>(self); - iter.add_slide_backward_right::<14>(self); - iter.add_slide_backward_right::<15>(self); - iter.add_slide_backward_right::<16>(self); - iter.add_slide_backward_right::<19>(self); - iter.add_slide_backward_right::<20>(self); - iter.add_slide_backward_right::<21>(self); - iter.add_slide_backward_right::<22>(self); - iter.add_slide_backward_right::<23>(self); - iter.add_slide_backward_right::<24>(self); - iter.add_slide_backward_right::<26>(self); - iter.add_slide_backward_right::<27>(self); - iter.add_slide_backward_right::<28>(self); - iter.add_slide_backward_right::<29>(self); - iter.add_slide_backward_right::<30>(self); - iter.add_slide_backward_right::<31>(self); - } - - iter - } -} - -impl PossibleMoves { - // TODO test - - /// The highest possible number of valid moves - pub const MAX_POSSIBLE_MOVES: usize = POSSIBLE_MOVES_ITER_SIZE; - - const fn slides_dark(board: CheckersBitBoard) -> Self { - const FORWARD_LEFT_MASK: u32 = 0b01111001111110111111001111011011; - const FORWARD_RIGHT_MASK: u32 = 0b01111101111111011111010111011101; - const BACKWARD_LEFT_MASK: u32 = 0b11111011111110111110101110111010; - const BACKWARD_RIGHT_MASK: u32 = 0b11111001111110011110110110111100; - - let not_occupied = !board.pieces_bits(); - let friendly_pieces = board.pieces_bits() & board.color_bits(); - let friendly_kings = friendly_pieces & board.king_bits(); - - let forward_left_movers = - not_occupied.rotate_right(7) & friendly_pieces & FORWARD_LEFT_MASK; - let forward_right_movers = - not_occupied.rotate_right(1) & friendly_pieces & FORWARD_RIGHT_MASK; - let backward_left_movers; - let backward_right_movers; - - if friendly_kings > 0 { - backward_left_movers = - not_occupied.rotate_left(1) & friendly_kings & BACKWARD_LEFT_MASK; - backward_right_movers = - not_occupied.rotate_left(7) & friendly_kings & BACKWARD_RIGHT_MASK; - } else { - backward_left_movers = 0; - backward_right_movers = 0; - } - - Self { - forward_left_movers, - forward_right_movers, - backward_left_movers, - backward_right_movers, - } - } - - const fn slides_light(board: CheckersBitBoard) -> Self { - const FORWARD_LEFT_MASK: u32 = 0b01111001111110111111001111011011; - const FORWARD_RIGHT_MASK: u32 = 0b01111101111111011111010111011101; - const BACKWARD_LEFT_MASK: u32 = 0b11111011111110111110101110111010; - const BACKWARD_RIGHT_MASK: u32 = 0b11111001111110011110110110111100; - - let not_occupied = !board.pieces_bits(); - let friendly_pieces = board.pieces_bits() & !board.color_bits(); - let friendly_kings = friendly_pieces & board.king_bits(); - - let backward_left_movers = - not_occupied.rotate_left(1) & friendly_pieces & BACKWARD_LEFT_MASK; - let backward_right_movers = - not_occupied.rotate_left(7) & friendly_pieces & BACKWARD_RIGHT_MASK; - let forward_left_movers; - let forward_right_movers; - - if friendly_kings > 0 { - forward_left_movers = not_occupied.rotate_right(7) & friendly_kings & FORWARD_LEFT_MASK; - forward_right_movers = - not_occupied.rotate_right(1) & friendly_kings & FORWARD_RIGHT_MASK; - } else { - forward_left_movers = 0; - forward_right_movers = 0; - } - - Self { - forward_left_movers, - forward_right_movers, - backward_left_movers, - backward_right_movers, - } - } - - const fn jumps_dark(board: CheckersBitBoard) -> Self { - const FORWARD_LEFT_MASK: u32 = 0b00110000111100111111001111000011; - const FORWARD_RIGHT_MASK: u32 = 0b00111100111111001111000011001100; - const BACKWARD_LEFT_MASK: u32 = 0b11110011111100111100001100110000; - const BACKWARD_RIGHT_MASK: u32 = 0b11111100111100001100110000111100; - - let not_occupied = !board.pieces_bits(); - let enemy_pieces = board.pieces_bits() & !board.color_bits(); - let friendly_pieces = board.pieces_bits() & board.color_bits(); - let friendly_kings = friendly_pieces & board.king_bits(); - - let forward_left_movers = not_occupied.rotate_right(14) - & enemy_pieces.rotate_right(7) - & friendly_pieces - & FORWARD_LEFT_MASK; - let forward_right_movers = not_occupied.rotate_right(2) - & enemy_pieces.rotate_right(1) - & friendly_pieces - & FORWARD_RIGHT_MASK; - let backward_left_movers; - let backward_right_movers; - - if friendly_kings > 0 { - backward_left_movers = not_occupied.rotate_left(2) - & enemy_pieces.rotate_left(1) - & friendly_kings & BACKWARD_LEFT_MASK; - backward_right_movers = not_occupied.rotate_left(14) - & enemy_pieces.rotate_left(7) - & friendly_kings & BACKWARD_RIGHT_MASK; - } else { - backward_left_movers = 0; - backward_right_movers = 0; - } - - let can_jump = if forward_left_movers != 0 - || forward_right_movers != 0 - || backward_left_movers != 0 - || backward_right_movers != 0 - { - 2 - } else { - 0 - }; - - Self { - forward_left_movers, - forward_right_movers, - backward_left_movers, - backward_right_movers: backward_right_movers | can_jump, - } - } - - const fn jumps_light(board: CheckersBitBoard) -> Self { - const FORWARD_LEFT_MASK: u32 = 0b00110000111100111111001111000011; - const FORWARD_RIGHT_MASK: u32 = 0b00111100111111001111000011001100; - const BACKWARD_LEFT_MASK: u32 = 0b11110011111100111100001100110000; - const BACKWARD_RIGHT_MASK: u32 = 0b11111100111100001100110000111100; - - let not_occupied = !board.pieces_bits(); - let enemy_pieces = board.pieces_bits() & board.color_bits(); - let friendly_pieces = board.pieces_bits() & !board.color_bits(); - let friendly_kings = friendly_pieces & board.king_bits(); - - let backward_left_movers = not_occupied.rotate_left(2) - & enemy_pieces.rotate_left(1) - & friendly_pieces - & BACKWARD_LEFT_MASK; - let backward_right_movers = not_occupied.rotate_left(14) - & enemy_pieces.rotate_left(7) - & friendly_pieces - & BACKWARD_RIGHT_MASK; - let forward_left_movers; - let forward_right_movers; - - if friendly_kings > 0 { - forward_left_movers = not_occupied.rotate_right(14) - & enemy_pieces.rotate_right(7) - & friendly_kings & FORWARD_LEFT_MASK; - forward_right_movers = not_occupied.rotate_right(2) - & enemy_pieces.rotate_right(1) - & friendly_kings & FORWARD_RIGHT_MASK; - } else { - forward_left_movers = 0; - forward_right_movers = 0; - } - - let can_jump = if forward_left_movers != 0 - || forward_right_movers != 0 - || backward_left_movers != 0 - || backward_right_movers != 0 - { - 2 - } else { - 0 - }; - - Self { - forward_left_movers, - forward_right_movers, - backward_left_movers, - backward_right_movers: backward_right_movers | can_jump, - } - } - - const fn has_jumps_dark(board: CheckersBitBoard) -> bool { - const FORWARD_LEFT_MASK: u32 = 0b00110000111100111111001111000011; - const FORWARD_RIGHT_MASK: u32 = 0b00111100111111001111000011001100; - const BACKWARD_LEFT_MASK: u32 = 0b11110011111100111100001100110000; - const BACKWARD_RIGHT_MASK: u32 = 0b11111100111100001100110000111100; - - let not_occupied = !board.pieces_bits(); - let enemy_pieces = board.pieces_bits() & !board.color_bits(); - let friendly_pieces = board.pieces_bits() & board.color_bits(); - - let forward_left_spaces = - not_occupied.rotate_right(14) & enemy_pieces.rotate_right(7) & FORWARD_LEFT_MASK; - let forward_right_spaces = - not_occupied.rotate_right(2) & enemy_pieces.rotate_right(1) & FORWARD_RIGHT_MASK; - - let forward_spaces = forward_left_spaces | forward_right_spaces; - - if board.king_bits() > 0 { - let backward_left_spaces = - not_occupied.rotate_left(2) & enemy_pieces.rotate_left(1) & BACKWARD_LEFT_MASK; - let backward_right_spaces = - not_occupied.rotate_left(14) & enemy_pieces.rotate_left(7) & BACKWARD_RIGHT_MASK; - let backward_spaces = backward_left_spaces | backward_right_spaces; - - let backward_spaces = board.king_bits() & backward_spaces; - friendly_pieces & (forward_spaces | backward_spaces) != 0 - } else { - friendly_pieces & forward_spaces != 0 - } - } - - const fn has_jumps_light(board: CheckersBitBoard) -> bool { - const FORWARD_LEFT_MASK: u32 = 0b00110000111100111111001111000011; - const FORWARD_RIGHT_MASK: u32 = 0b00111100111111001111000011001100; - const BACKWARD_LEFT_MASK: u32 = 0b11110011111100111100001100110000; - const BACKWARD_RIGHT_MASK: u32 = 0b11111100111100001100110000111100; - - let not_occupied = !board.pieces_bits(); - let enemy_pieces = board.pieces_bits() & board.color_bits(); - let friendly_pieces = board.pieces_bits() & !board.color_bits(); - - let backward_left_spaces = - not_occupied.rotate_left(2) & enemy_pieces.rotate_left(1) & BACKWARD_LEFT_MASK; - let backward_right_spaces = - not_occupied.rotate_left(14) & enemy_pieces.rotate_left(7) & BACKWARD_RIGHT_MASK; - - let backward_spaces = backward_left_spaces | backward_right_spaces; - - if board.king_bits() > 0 { - let forward_left_spaces = - not_occupied.rotate_right(14) & enemy_pieces.rotate_right(7) & FORWARD_LEFT_MASK; - let forward_right_spaces = - not_occupied.rotate_right(2) & enemy_pieces.rotate_right(1) & FORWARD_RIGHT_MASK; - let forward_spaces = forward_left_spaces | forward_right_spaces; - - let forward_spaces = board.king_bits() & forward_spaces; - friendly_pieces & (forward_spaces | backward_spaces) != 0 - } else { - friendly_pieces & backward_spaces != 0 - } - } - - #[inline(always)] - // TODO optimize - pub const fn has_jumps(board: CheckersBitBoard) -> bool { - match board.turn() { - PieceColor::Light => Self::has_jumps_light(board), - PieceColor::Dark => Self::has_jumps_dark(board), - } - } - - const fn has_jumps_at_dark(board: CheckersBitBoard, value: usize) -> bool { - const FORWARD_LEFT_MASK: u32 = 0b00110000111100111111001111000011; - const FORWARD_RIGHT_MASK: u32 = 0b00111100111111001111000011001100; - const BACKWARD_LEFT_MASK: u32 = 0b11110011111100111100001100110000; - const BACKWARD_RIGHT_MASK: u32 = 0b11111100111100001100110000111100; - - let not_occupied = !board.pieces_bits(); - let enemy_pieces = board.pieces_bits() & !board.color_bits(); - let friendly_pieces = board.pieces_bits() & board.color_bits(); - - let forward_left_spaces = - not_occupied.rotate_right(14) & enemy_pieces.rotate_right(7) & FORWARD_LEFT_MASK; - let forward_right_spaces = - not_occupied.rotate_right(2) & enemy_pieces.rotate_right(1) & FORWARD_RIGHT_MASK; - - let forward_spaces = forward_left_spaces | forward_right_spaces; - - if board.king_bits() > 0 { - let backward_left_spaces = - not_occupied.rotate_left(2) & enemy_pieces.rotate_left(1) & BACKWARD_LEFT_MASK; - let backward_right_spaces = - not_occupied.rotate_left(14) & enemy_pieces.rotate_left(7) & BACKWARD_RIGHT_MASK; - let backward_spaces = backward_left_spaces | backward_right_spaces; - - let backward_spaces = board.king_bits() & backward_spaces; - ((friendly_pieces & (forward_spaces | backward_spaces)) >> value) & 1 != 0 - } else { - ((friendly_pieces & forward_spaces) >> value) & 1 != 0 - } - } - - const fn has_jumps_at_light(board: CheckersBitBoard, value: usize) -> bool { - const FORWARD_LEFT_MASK: u32 = 0b00110000111100111111001111000011; - const FORWARD_RIGHT_MASK: u32 = 0b00111100111111001111000011001100; - const BACKWARD_LEFT_MASK: u32 = 0b11110011111100111100001100110000; - const BACKWARD_RIGHT_MASK: u32 = 0b11111100111100001100110000111100; - - let not_occupied = !board.pieces_bits(); - let enemy_pieces = board.pieces_bits() & board.color_bits(); - let friendly_pieces = board.pieces_bits() & !board.color_bits(); - - let backward_left_spaces = - not_occupied.rotate_left(2) & enemy_pieces.rotate_left(1) & BACKWARD_LEFT_MASK; - let backward_right_spaces = - not_occupied.rotate_left(14) & enemy_pieces.rotate_left(7) & BACKWARD_RIGHT_MASK; - - let backward_spaces = backward_left_spaces | backward_right_spaces; - - if board.king_bits() > 0 { - let forward_left_spaces = - not_occupied.rotate_right(14) & enemy_pieces.rotate_right(7) & FORWARD_LEFT_MASK; - let forward_right_spaces = - not_occupied.rotate_right(2) & enemy_pieces.rotate_right(1) & FORWARD_RIGHT_MASK; - let forward_spaces = forward_left_spaces | forward_right_spaces; - - let forward_spaces = board.king_bits() & forward_spaces; - ((friendly_pieces & (forward_spaces | backward_spaces)) >> value) & 1 != 0 - } else { - ((friendly_pieces & backward_spaces) >> value) & 1 != 0 - } - } - - #[inline(always)] - // TODO optimize - pub const fn has_jumps_at(board: CheckersBitBoard, value: usize) -> bool { - match board.turn() { - PieceColor::Light => Self::has_jumps_at_light(board, value), - PieceColor::Dark => Self::has_jumps_at_dark(board, value), - } - } - - const fn light_moves(board: CheckersBitBoard) -> Self { - let jumps = Self::jumps_light(board); - if jumps.is_empty() { - Self::slides_light(board) - } else { - jumps - } - } - - const fn dark_moves(board: CheckersBitBoard) -> Self { - let jumps = Self::jumps_dark(board); - if jumps.is_empty() { - Self::slides_dark(board) - } else { - jumps - } - } - - const fn filter_to_square(self, square: u8) -> Self { - let mask = 1 << square; - Self { - forward_left_movers: self.forward_left_movers & mask, - forward_right_movers: self.forward_right_movers & mask, - backward_left_movers: self.backward_left_movers & mask, - backward_right_movers: self.backward_right_movers & (mask | 2), - } - } - - pub fn moves(board: CheckersBitBoard) -> Self { - let moves = match board.turn() { - PieceColor::Dark => Self::dark_moves(board), - PieceColor::Light => Self::light_moves(board), - }; - - if board.turn == board.previous_turn { - moves.filter_to_square(board.previous_move_to) - } else { - moves - } - } - - /// Returns true if no moves are possible - pub const fn is_empty(self) -> bool { - (self.backward_left_movers - | (self.forward_left_movers) - | self.forward_right_movers - | self.backward_right_movers & 4294967293) - == 0 - } - - /// Returns true if the piece can jump - pub const fn can_jump(self) -> bool { - (self.backward_right_movers & 2) != 0 - } - - /// Returns true if the given move is possible - pub const fn contains(self, checker_move: Move) -> bool { - if checker_move.is_jump() != self.can_jump() { - return false; - } - - let bits = match checker_move.direction() { - MoveDirection::ForwardLeft => self.forward_left_movers, - MoveDirection::ForwardRight => self.forward_right_movers, - MoveDirection::BackwardLeft => self.backward_left_movers, - MoveDirection::BackwardRight => self.backward_right_movers, - }; - - (bits >> checker_move.start()) & 1 == 1 - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn setup_empty_iter() -> PossibleMovesIter { - let moves = [MaybeUninit::uninit(); POSSIBLE_MOVES_ITER_SIZE]; - PossibleMovesIter { - moves, - index: 0, - length: 0, - } - } - - fn setup_add_move_to_iter_invalid() -> (PossibleMovesIter, PossibleMoves) { - let moves = PossibleMoves { - forward_left_movers: 0, - forward_right_movers: 0, - backward_left_movers: 0, - backward_right_movers: 0, - }; - let iter = setup_empty_iter(); - - (iter, moves) - } - - fn setup_add_move_to_iter_valid() -> (PossibleMovesIter, PossibleMoves) { - let moves = PossibleMoves { - forward_left_movers: u32::MAX, - forward_right_movers: u32::MAX, - backward_left_movers: u32::MAX, - backward_right_movers: u32::MAX, - }; - let iter = setup_empty_iter(); - - (iter, moves) - } - - #[test] - fn same() { - let start = CheckersBitBoard::new( - 0b11100111100111100111110111111011, - 0b00001100001111001111001111000011, - 0, - PieceColor::Dark, - ); - let flip = CheckersBitBoard::new( - 0b11100111100111100111110111111011, - 0b11110011110000110000110000111100, - 0, - PieceColor::Light, - ); - - assert_eq!( - PossibleMoves::has_jumps(start), - PossibleMoves::has_jumps(flip) - ) - } - - #[test] - fn iter_next() { - let test_move1 = Move::new(8, MoveDirection::ForwardLeft, false); - let test_move2 = Move::new(26, MoveDirection::ForwardRight, true); - let mut iter = setup_empty_iter(); - iter.length = 2; - - let ptr = iter.moves.as_mut().get_mut(0).unwrap(); - *ptr = MaybeUninit::new(test_move1); - - let ptr = iter.moves.as_mut().get_mut(1).unwrap(); - *ptr = MaybeUninit::new(test_move2); - - let recieved_move = iter.next(); - assert!(recieved_move.is_some()); - assert_eq!(recieved_move.unwrap(), test_move1); - - let recieved_move = iter.next(); - assert!(recieved_move.is_some()); - assert_eq!(recieved_move.unwrap(), test_move2); - - let recieved_move = iter.next(); - assert!(recieved_move.is_none()); - } - - #[test] - fn add_slide_forward_left_to_iter_invalid() { - const START: usize = 8; - let (mut iter, moves) = setup_add_move_to_iter_invalid(); - iter.add_slide_forward_left::(moves); - - assert_eq!(iter.index, 0); - assert_eq!(iter.length, 0); - } - - #[test] - fn add_slide_forward_left_to_iter_valid() { - const START: usize = 8; - let (mut iter, moves) = setup_add_move_to_iter_valid(); - iter.add_slide_forward_left::(moves); - - assert_eq!(iter.index, 0); - assert_eq!(iter.length, 1); - - let new_move = iter.next().unwrap(); - assert_eq!(new_move.start(), START as u32); - assert_eq!(new_move.direction(), MoveDirection::ForwardLeft); - assert!(!new_move.is_jump()); - } - - #[test] - fn add_slide_forward_right_to_iter_invalid() { - const START: usize = 26; - let (mut iter, moves) = setup_add_move_to_iter_invalid(); - iter.add_slide_forward_right::(moves); - - assert_eq!(iter.index, 0); - assert_eq!(iter.length, 0); - } - - #[test] - fn add_slide_forward_right_to_iter_valid() { - const START: usize = 26; - let (mut iter, moves) = setup_add_move_to_iter_valid(); - iter.add_slide_forward_right::(moves); - - assert_eq!(iter.index, 0); - assert_eq!(iter.length, 1); - - let new_move = iter.next().unwrap(); - assert_eq!(new_move.start(), START as u32); - assert_eq!(new_move.direction(), MoveDirection::ForwardRight); - assert!(!new_move.is_jump()); - } - - #[test] - fn add_slide_backward_left_to_iter_invalid() { - const START: usize = 17; - let (mut iter, moves) = setup_add_move_to_iter_invalid(); - iter.add_slide_backward_left::(moves); - - assert_eq!(iter.index, 0); - assert_eq!(iter.length, 0); - } - - #[test] - fn add_slide_backward_left_to_iter_valid() { - const START: usize = 17; - let (mut iter, moves) = setup_add_move_to_iter_valid(); - iter.add_slide_backward_left::(moves); - - assert_eq!(iter.index, 0); - assert_eq!(iter.length, 1); - - let new_move = iter.next().unwrap(); - assert_eq!(new_move.start(), START as u32); - assert_eq!(new_move.direction(), MoveDirection::BackwardLeft); - assert!(!new_move.is_jump()); - } - - #[test] - fn add_slide_backward_right_to_iter_invalid() { - const START: usize = 3; - let (mut iter, moves) = setup_add_move_to_iter_invalid(); - iter.add_slide_backward_right::(moves); - - assert_eq!(iter.index, 0); - assert_eq!(iter.length, 0); - } - - #[test] - fn add_slide_backward_right_to_iter_valid() { - const START: usize = 3; - let (mut iter, moves) = setup_add_move_to_iter_valid(); - iter.add_slide_backward_right::(moves); - - assert_eq!(iter.index, 0); - assert_eq!(iter.length, 1); - - let new_move = iter.next().unwrap(); - assert_eq!(new_move.start(), START as u32); - assert_eq!(new_move.direction(), MoveDirection::BackwardRight); - assert!(!new_move.is_jump()); - } - - #[test] - fn add_jump_forward_left_to_iter_invalid() { - const START: usize = 8; - let (mut iter, moves) = setup_add_move_to_iter_invalid(); - iter.add_jump_forward_left::(moves); - - assert_eq!(iter.index, 0); - assert_eq!(iter.length, 0); - } - - #[test] - fn add_jump_forward_left_to_iter_valid() { - const START: usize = 8; - let (mut iter, moves) = setup_add_move_to_iter_valid(); - iter.add_jump_forward_left::(moves); - - assert_eq!(iter.index, 0); - assert_eq!(iter.length, 1); - - let new_move = iter.next().unwrap(); - assert_eq!(new_move.start(), START as u32); - assert_eq!(new_move.direction(), MoveDirection::ForwardLeft); - assert!(new_move.is_jump()); - } - - #[test] - fn add_jump_forward_right_to_iter_invalid() { - const START: usize = 26; - let (mut iter, moves) = setup_add_move_to_iter_invalid(); - iter.add_jump_forward_right::(moves); - - assert_eq!(iter.index, 0); - assert_eq!(iter.length, 0); - } - - #[test] - fn add_jump_forward_right_to_iter_valid() { - const START: usize = 26; - let (mut iter, moves) = setup_add_move_to_iter_valid(); - iter.add_jump_forward_right::(moves); - - assert_eq!(iter.index, 0); - assert_eq!(iter.length, 1); - - let new_move = iter.next().unwrap(); - assert_eq!(new_move.start(), START as u32); - assert_eq!(new_move.direction(), MoveDirection::ForwardRight); - assert!(new_move.is_jump()); - } - - #[test] - fn add_jump_backward_left_to_iter_invalid() { - const START: usize = 17; - let (mut iter, moves) = setup_add_move_to_iter_invalid(); - iter.add_jump_backward_left::(moves); - - assert_eq!(iter.index, 0); - assert_eq!(iter.length, 0); - } - - #[test] - fn add_jump_backward_left_to_iter_valid() { - const START: usize = 17; - let (mut iter, moves) = setup_add_move_to_iter_valid(); - iter.add_jump_backward_left::(moves); - - assert_eq!(iter.index, 0); - assert_eq!(iter.length, 1); - - let new_move = iter.next().unwrap(); - assert_eq!(new_move.start(), START as u32); - assert_eq!(new_move.direction(), MoveDirection::BackwardLeft); - assert!(new_move.is_jump()); - } - - #[test] - fn add_jump_backward_right_to_iter_invalid() { - const START: usize = 3; - let (mut iter, moves) = setup_add_move_to_iter_invalid(); - iter.add_jump_backward_right::(moves); - - assert_eq!(iter.index, 0); - assert_eq!(iter.length, 0); - } - - #[test] - fn add_jump_backward_right_to_iter_valid() { - const START: usize = 3; - let (mut iter, moves) = setup_add_move_to_iter_valid(); - iter.add_jump_backward_right::(moves); - - assert_eq!(iter.index, 0); - assert_eq!(iter.length, 1); - - let new_move = iter.next().unwrap(); - assert_eq!(new_move.start(), START as u32); - assert_eq!(new_move.direction(), MoveDirection::BackwardRight); - assert!(new_move.is_jump()); - } - - #[test] - fn cant_jump_in_position_2_without_26() { - // This bug was bizarre, but it's caused by a white piece being in the - //second bit while there is no piece in the 26th bit. If you don't - // apply the bit mask for collision detection, then all of the light - // player moves become jumps. - let board = CheckersBitBoard::new(16908890, 401395713, 50332352, PieceColor::Light); - let possible_moves = PossibleMoves::moves(board); - assert!(!possible_moves.can_jump()) - } - - #[test] - fn not_has_jump_at_14_when_has_jump_at_20() { - // This bug was caused by me forgetting to `& 1` to the end of the - // `has_jump_at` functions. After playing a jump with one piece, I was - // able to continue jumping with completely different pieces - let board = CheckersBitBoard::new( - 0b11100111001111001111110111111011, - 0b00001100001111001111001111000011, - 0, - PieceColor::Dark, - ); - let possible_moves = PossibleMoves::moves(board); - assert!(!possible_moves.can_jump()) - } - - #[test] - fn test_send() { - fn assert_send() {} - assert_send::(); - assert_send::(); - } - - #[test] - fn test_sync() { - fn assert_sync() {} - assert_sync::(); - assert_sync::(); - } -} +use crate::moves::{Move, MoveDirection}; +use crate::{CheckersBitBoard, PieceColor}; + +use std::mem::MaybeUninit; + +// The maximum number of available moves in any given position +pub const POSSIBLE_MOVES_ITER_SIZE: usize = 50; + +/// A struct containing the possible moves in a particular checkers position +#[derive(Copy, Clone, Debug)] +pub struct PossibleMoves { + forward_left_movers: u32, + forward_right_movers: u32, + backward_left_movers: u32, + backward_right_movers: u32, +} + +/// An iterator of possible checkers moves for a particular position +pub struct PossibleMovesIter { + /// A pointer to an array of possibly uninitialized checkers moves + moves: [MaybeUninit; POSSIBLE_MOVES_ITER_SIZE], + + /// The current index into the moves array + index: usize, + + // The number of initialized moves in the array + length: usize, +} + +impl PossibleMovesIter { + fn add_slide_forward_left(&mut self, possible_moves: PossibleMoves) { + if (possible_moves.forward_left_movers >> SQUARE) & 1 != 0 { + debug_assert!(self.length < POSSIBLE_MOVES_ITER_SIZE); + let ptr = unsafe { self.moves.as_mut().get_unchecked_mut(self.length) }; + *ptr = MaybeUninit::new(Move::new(SQUARE, MoveDirection::ForwardLeft, false)); + self.length += 1; + } + } + + fn add_slide_forward_right(&mut self, possible_moves: PossibleMoves) { + if (possible_moves.forward_right_movers >> SQUARE) & 1 != 0 { + debug_assert!(self.length < POSSIBLE_MOVES_ITER_SIZE); + let ptr = unsafe { self.moves.as_mut().get_unchecked_mut(self.length) }; + *ptr = MaybeUninit::new(Move::new(SQUARE, MoveDirection::ForwardRight, false)); + self.length += 1; + } + } + + fn add_slide_backward_left(&mut self, possible_moves: PossibleMoves) { + if (possible_moves.backward_left_movers >> SQUARE) & 1 != 0 { + debug_assert!(self.length < POSSIBLE_MOVES_ITER_SIZE); + let ptr = unsafe { self.moves.as_mut().get_unchecked_mut(self.length) }; + *ptr = MaybeUninit::new(Move::new(SQUARE, MoveDirection::BackwardLeft, false)); + self.length += 1; + } + } + + fn add_slide_backward_right(&mut self, possible_moves: PossibleMoves) { + if (possible_moves.backward_right_movers >> SQUARE) & 1 != 0 { + debug_assert!(self.length < POSSIBLE_MOVES_ITER_SIZE); + let ptr = unsafe { self.moves.as_mut().get_unchecked_mut(self.length) }; + *ptr = MaybeUninit::new(Move::new(SQUARE, MoveDirection::BackwardRight, false)); + self.length += 1; + } + } + + fn add_jump_forward_left(&mut self, possible_moves: PossibleMoves) { + if (possible_moves.forward_left_movers >> SQUARE) & 1 != 0 { + debug_assert!(self.length < POSSIBLE_MOVES_ITER_SIZE); + let ptr = unsafe { self.moves.as_mut().get_unchecked_mut(self.length) }; + *ptr = MaybeUninit::new(Move::new(SQUARE, MoveDirection::ForwardLeft, true)); + self.length += 1; + } + } + + fn add_jump_forward_right(&mut self, possible_moves: PossibleMoves) { + if (possible_moves.forward_right_movers >> SQUARE) & 1 != 0 { + debug_assert!(self.length < POSSIBLE_MOVES_ITER_SIZE); + let ptr = unsafe { self.moves.as_mut().get_unchecked_mut(self.length) }; + *ptr = MaybeUninit::new(Move::new(SQUARE, MoveDirection::ForwardRight, true)); + self.length += 1; + } + } + + fn add_jump_backward_left(&mut self, possible_moves: PossibleMoves) { + if (possible_moves.backward_left_movers >> SQUARE) & 1 != 0 { + debug_assert!(self.length < POSSIBLE_MOVES_ITER_SIZE); + let ptr = unsafe { self.moves.as_mut().get_unchecked_mut(self.length) }; + *ptr = MaybeUninit::new(Move::new(SQUARE, MoveDirection::BackwardLeft, true)); + self.length += 1; + } + } + + fn add_jump_backward_right(&mut self, possible_moves: PossibleMoves) { + if (possible_moves.backward_right_movers >> SQUARE) & 1 != 0 { + debug_assert!(self.length < POSSIBLE_MOVES_ITER_SIZE); + let ptr = unsafe { self.moves.as_mut().get_unchecked_mut(self.length) }; + *ptr = MaybeUninit::new(Move::new(SQUARE, MoveDirection::BackwardRight, true)); + self.length += 1; + } + } +} + +unsafe impl Send for PossibleMovesIter {} + +impl Iterator for PossibleMovesIter { + type Item = Move; + + fn next(&mut self) -> Option { + if self.length > self.index { + debug_assert!(self.index < POSSIBLE_MOVES_ITER_SIZE); + let next_move = unsafe { self.moves.as_ref().get_unchecked(self.index).assume_init() }; + self.index += 1; + Some(next_move) + } else { + None + } + } + + // TODO test + fn size_hint(&self) -> (usize, Option) { + let remaining = self.length - self.index; + (remaining, Some(remaining)) + } + + // TODO test + fn count(self) -> usize + where + Self: Sized, + { + self.length - self.index + } + + // TODO test + fn last(self) -> Option + where + Self: Sized, + { + debug_assert!(self.length <= POSSIBLE_MOVES_ITER_SIZE); + if self.length == 0 { + None + } else { + Some(unsafe { + self.moves + .as_ref() + .get_unchecked(self.length - 1) + .assume_init() + }) + } + } + + // TODO test + fn nth(&mut self, n: usize) -> Option { + if self.length == 0 || self.length - self.index < n { + None + } else { + self.index += n; + let current_move = + unsafe { self.moves.as_ref().get_unchecked(self.index).assume_init() }; + self.index += 1; + Some(current_move) + } + } +} + +impl IntoIterator for PossibleMoves { + type Item = Move; + type IntoIter = PossibleMovesIter; + + // TODO test + fn into_iter(self) -> Self::IntoIter { + let moves = [MaybeUninit::uninit(); POSSIBLE_MOVES_ITER_SIZE]; + let mut iter = PossibleMovesIter { + moves, + index: 0, + length: 0, + }; + + if self.can_jump() { + iter.add_jump_forward_left::<0>(self); + iter.add_jump_forward_left::<1>(self); + iter.add_jump_forward_left::<6>(self); + iter.add_jump_forward_left::<7>(self); + iter.add_jump_forward_left::<8>(self); + iter.add_jump_forward_left::<9>(self); + iter.add_jump_forward_left::<12>(self); + iter.add_jump_forward_left::<13>(self); + iter.add_jump_forward_left::<14>(self); + iter.add_jump_forward_left::<15>(self); + iter.add_jump_forward_left::<16>(self); + iter.add_jump_forward_left::<17>(self); + iter.add_jump_forward_left::<20>(self); + iter.add_jump_forward_left::<21>(self); + iter.add_jump_forward_left::<22>(self); + iter.add_jump_forward_left::<23>(self); + iter.add_jump_forward_left::<28>(self); + iter.add_jump_forward_left::<29>(self); + + iter.add_jump_forward_right::<2>(self); + iter.add_jump_forward_right::<3>(self); + iter.add_jump_forward_right::<6>(self); + iter.add_jump_forward_right::<7>(self); + iter.add_jump_forward_right::<12>(self); + iter.add_jump_forward_right::<13>(self); + iter.add_jump_forward_right::<14>(self); + iter.add_jump_forward_right::<15>(self); + iter.add_jump_forward_right::<18>(self); + iter.add_jump_forward_right::<19>(self); + iter.add_jump_forward_right::<20>(self); + iter.add_jump_forward_right::<21>(self); + iter.add_jump_forward_right::<22>(self); + iter.add_jump_forward_right::<23>(self); + iter.add_jump_forward_right::<26>(self); + iter.add_jump_forward_right::<27>(self); + iter.add_jump_forward_right::<28>(self); + iter.add_jump_forward_right::<29>(self); + + iter.add_jump_backward_left::<4>(self); + iter.add_jump_backward_left::<5>(self); + iter.add_jump_backward_left::<8>(self); + iter.add_jump_backward_left::<9>(self); + iter.add_jump_backward_left::<14>(self); + iter.add_jump_backward_left::<15>(self); + iter.add_jump_backward_left::<16>(self); + iter.add_jump_backward_left::<17>(self); + iter.add_jump_backward_left::<20>(self); + iter.add_jump_backward_left::<21>(self); + iter.add_jump_backward_left::<22>(self); + iter.add_jump_backward_left::<23>(self); + iter.add_jump_backward_left::<24>(self); + iter.add_jump_backward_left::<25>(self); + iter.add_jump_backward_left::<28>(self); + iter.add_jump_backward_left::<29>(self); + iter.add_jump_backward_left::<30>(self); + iter.add_jump_backward_left::<31>(self); + + iter.add_jump_backward_right::<2>(self); + iter.add_jump_backward_right::<3>(self); + iter.add_jump_backward_right::<4>(self); + iter.add_jump_backward_right::<5>(self); + iter.add_jump_backward_right::<10>(self); + iter.add_jump_backward_right::<11>(self); + iter.add_jump_backward_right::<14>(self); + iter.add_jump_backward_right::<15>(self); + iter.add_jump_backward_right::<20>(self); + iter.add_jump_backward_right::<21>(self); + iter.add_jump_backward_right::<22>(self); + iter.add_jump_backward_right::<23>(self); + iter.add_jump_backward_right::<26>(self); + iter.add_jump_backward_right::<27>(self); + iter.add_jump_backward_right::<28>(self); + iter.add_jump_backward_right::<29>(self); + iter.add_jump_backward_right::<30>(self); + iter.add_jump_backward_right::<31>(self); + } else { + iter.add_slide_forward_left::<0>(self); + iter.add_slide_forward_left::<1>(self); + iter.add_slide_forward_left::<3>(self); + iter.add_slide_forward_left::<4>(self); + iter.add_slide_forward_left::<6>(self); + iter.add_slide_forward_left::<7>(self); + iter.add_slide_forward_left::<8>(self); + iter.add_slide_forward_left::<9>(self); + iter.add_slide_forward_left::<12>(self); + iter.add_slide_forward_left::<13>(self); + iter.add_slide_forward_left::<14>(self); + iter.add_slide_forward_left::<15>(self); + iter.add_slide_forward_left::<16>(self); + iter.add_slide_forward_left::<17>(self); + iter.add_slide_forward_left::<19>(self); + iter.add_slide_forward_left::<20>(self); + iter.add_slide_forward_left::<21>(self); + iter.add_slide_forward_left::<22>(self); + iter.add_slide_forward_left::<23>(self); + iter.add_slide_forward_left::<24>(self); + iter.add_slide_forward_left::<27>(self); + iter.add_slide_forward_left::<28>(self); + iter.add_slide_forward_left::<29>(self); + iter.add_slide_forward_left::<30>(self); + + iter.add_slide_forward_right::<0>(self); + iter.add_slide_forward_right::<2>(self); + iter.add_slide_forward_right::<3>(self); + iter.add_slide_forward_right::<4>(self); + iter.add_slide_forward_right::<6>(self); + iter.add_slide_forward_right::<7>(self); + iter.add_slide_forward_right::<8>(self); + iter.add_slide_forward_right::<10>(self); + iter.add_slide_forward_right::<12>(self); + iter.add_slide_forward_right::<13>(self); + iter.add_slide_forward_right::<14>(self); + iter.add_slide_forward_right::<15>(self); + iter.add_slide_forward_right::<16>(self); + iter.add_slide_forward_right::<18>(self); + iter.add_slide_forward_right::<19>(self); + iter.add_slide_forward_right::<20>(self); + iter.add_slide_forward_right::<21>(self); + iter.add_slide_forward_right::<22>(self); + iter.add_slide_forward_right::<23>(self); + iter.add_slide_forward_right::<24>(self); + iter.add_slide_forward_right::<26>(self); + iter.add_slide_forward_right::<27>(self); + iter.add_slide_forward_right::<28>(self); + iter.add_slide_forward_right::<29>(self); + iter.add_slide_forward_right::<30>(self); + + iter.add_slide_backward_left::<1>(self); + iter.add_slide_backward_left::<3>(self); + iter.add_slide_backward_left::<4>(self); + iter.add_slide_backward_left::<5>(self); + iter.add_slide_backward_left::<7>(self); + iter.add_slide_backward_left::<8>(self); + iter.add_slide_backward_left::<9>(self); + iter.add_slide_backward_left::<11>(self); + iter.add_slide_backward_left::<13>(self); + iter.add_slide_backward_left::<14>(self); + iter.add_slide_backward_left::<15>(self); + iter.add_slide_backward_left::<16>(self); + iter.add_slide_backward_left::<17>(self); + iter.add_slide_backward_left::<19>(self); + iter.add_slide_backward_left::<20>(self); + iter.add_slide_backward_left::<21>(self); + iter.add_slide_backward_left::<22>(self); + iter.add_slide_backward_left::<23>(self); + iter.add_slide_backward_left::<24>(self); + iter.add_slide_backward_left::<25>(self); + iter.add_slide_backward_left::<27>(self); + iter.add_slide_backward_left::<28>(self); + iter.add_slide_backward_left::<29>(self); + iter.add_slide_backward_left::<30>(self); + iter.add_slide_backward_left::<31>(self); + + iter.add_slide_backward_right::<2>(self); + iter.add_slide_backward_right::<3>(self); + iter.add_slide_backward_right::<4>(self); + iter.add_slide_backward_right::<5>(self); + iter.add_slide_backward_right::<7>(self); + iter.add_slide_backward_right::<8>(self); + iter.add_slide_backward_right::<10>(self); + iter.add_slide_backward_right::<11>(self); + iter.add_slide_backward_right::<13>(self); + iter.add_slide_backward_right::<14>(self); + iter.add_slide_backward_right::<15>(self); + iter.add_slide_backward_right::<16>(self); + iter.add_slide_backward_right::<19>(self); + iter.add_slide_backward_right::<20>(self); + iter.add_slide_backward_right::<21>(self); + iter.add_slide_backward_right::<22>(self); + iter.add_slide_backward_right::<23>(self); + iter.add_slide_backward_right::<24>(self); + iter.add_slide_backward_right::<26>(self); + iter.add_slide_backward_right::<27>(self); + iter.add_slide_backward_right::<28>(self); + iter.add_slide_backward_right::<29>(self); + iter.add_slide_backward_right::<30>(self); + iter.add_slide_backward_right::<31>(self); + } + + iter + } +} + +impl PossibleMoves { + // TODO test + + /// The highest possible number of valid moves + pub const MAX_POSSIBLE_MOVES: usize = POSSIBLE_MOVES_ITER_SIZE; + + const fn slides_dark(board: CheckersBitBoard) -> Self { + const FORWARD_LEFT_MASK: u32 = 0b01111001111110111111001111011011; + const FORWARD_RIGHT_MASK: u32 = 0b01111101111111011111010111011101; + const BACKWARD_LEFT_MASK: u32 = 0b11111011111110111110101110111010; + const BACKWARD_RIGHT_MASK: u32 = 0b11111001111110011110110110111100; + + let not_occupied = !board.pieces_bits(); + let friendly_pieces = board.pieces_bits() & board.color_bits(); + let friendly_kings = friendly_pieces & board.king_bits(); + + let forward_left_movers = + not_occupied.rotate_right(7) & friendly_pieces & FORWARD_LEFT_MASK; + let forward_right_movers = + not_occupied.rotate_right(1) & friendly_pieces & FORWARD_RIGHT_MASK; + let backward_left_movers; + let backward_right_movers; + + if friendly_kings > 0 { + backward_left_movers = + not_occupied.rotate_left(1) & friendly_kings & BACKWARD_LEFT_MASK; + backward_right_movers = + not_occupied.rotate_left(7) & friendly_kings & BACKWARD_RIGHT_MASK; + } else { + backward_left_movers = 0; + backward_right_movers = 0; + } + + Self { + forward_left_movers, + forward_right_movers, + backward_left_movers, + backward_right_movers, + } + } + + const fn slides_light(board: CheckersBitBoard) -> Self { + const FORWARD_LEFT_MASK: u32 = 0b01111001111110111111001111011011; + const FORWARD_RIGHT_MASK: u32 = 0b01111101111111011111010111011101; + const BACKWARD_LEFT_MASK: u32 = 0b11111011111110111110101110111010; + const BACKWARD_RIGHT_MASK: u32 = 0b11111001111110011110110110111100; + + let not_occupied = !board.pieces_bits(); + let friendly_pieces = board.pieces_bits() & !board.color_bits(); + let friendly_kings = friendly_pieces & board.king_bits(); + + let backward_left_movers = + not_occupied.rotate_left(1) & friendly_pieces & BACKWARD_LEFT_MASK; + let backward_right_movers = + not_occupied.rotate_left(7) & friendly_pieces & BACKWARD_RIGHT_MASK; + let forward_left_movers; + let forward_right_movers; + + if friendly_kings > 0 { + forward_left_movers = not_occupied.rotate_right(7) & friendly_kings & FORWARD_LEFT_MASK; + forward_right_movers = + not_occupied.rotate_right(1) & friendly_kings & FORWARD_RIGHT_MASK; + } else { + forward_left_movers = 0; + forward_right_movers = 0; + } + + Self { + forward_left_movers, + forward_right_movers, + backward_left_movers, + backward_right_movers, + } + } + + const fn jumps_dark(board: CheckersBitBoard) -> Self { + const FORWARD_LEFT_MASK: u32 = 0b00110000111100111111001111000011; + const FORWARD_RIGHT_MASK: u32 = 0b00111100111111001111000011001100; + const BACKWARD_LEFT_MASK: u32 = 0b11110011111100111100001100110000; + const BACKWARD_RIGHT_MASK: u32 = 0b11111100111100001100110000111100; + + let not_occupied = !board.pieces_bits(); + let enemy_pieces = board.pieces_bits() & !board.color_bits(); + let friendly_pieces = board.pieces_bits() & board.color_bits(); + let friendly_kings = friendly_pieces & board.king_bits(); + + let forward_left_movers = not_occupied.rotate_right(14) + & enemy_pieces.rotate_right(7) + & friendly_pieces + & FORWARD_LEFT_MASK; + let forward_right_movers = not_occupied.rotate_right(2) + & enemy_pieces.rotate_right(1) + & friendly_pieces + & FORWARD_RIGHT_MASK; + let backward_left_movers; + let backward_right_movers; + + if friendly_kings > 0 { + backward_left_movers = not_occupied.rotate_left(2) + & enemy_pieces.rotate_left(1) + & friendly_kings & BACKWARD_LEFT_MASK; + backward_right_movers = not_occupied.rotate_left(14) + & enemy_pieces.rotate_left(7) + & friendly_kings & BACKWARD_RIGHT_MASK; + } else { + backward_left_movers = 0; + backward_right_movers = 0; + } + + let can_jump = if forward_left_movers != 0 + || forward_right_movers != 0 + || backward_left_movers != 0 + || backward_right_movers != 0 + { + 2 + } else { + 0 + }; + + Self { + forward_left_movers, + forward_right_movers, + backward_left_movers, + backward_right_movers: backward_right_movers | can_jump, + } + } + + const fn jumps_light(board: CheckersBitBoard) -> Self { + const FORWARD_LEFT_MASK: u32 = 0b00110000111100111111001111000011; + const FORWARD_RIGHT_MASK: u32 = 0b00111100111111001111000011001100; + const BACKWARD_LEFT_MASK: u32 = 0b11110011111100111100001100110000; + const BACKWARD_RIGHT_MASK: u32 = 0b11111100111100001100110000111100; + + let not_occupied = !board.pieces_bits(); + let enemy_pieces = board.pieces_bits() & board.color_bits(); + let friendly_pieces = board.pieces_bits() & !board.color_bits(); + let friendly_kings = friendly_pieces & board.king_bits(); + + let backward_left_movers = not_occupied.rotate_left(2) + & enemy_pieces.rotate_left(1) + & friendly_pieces + & BACKWARD_LEFT_MASK; + let backward_right_movers = not_occupied.rotate_left(14) + & enemy_pieces.rotate_left(7) + & friendly_pieces + & BACKWARD_RIGHT_MASK; + let forward_left_movers; + let forward_right_movers; + + if friendly_kings > 0 { + forward_left_movers = not_occupied.rotate_right(14) + & enemy_pieces.rotate_right(7) + & friendly_kings & FORWARD_LEFT_MASK; + forward_right_movers = not_occupied.rotate_right(2) + & enemy_pieces.rotate_right(1) + & friendly_kings & FORWARD_RIGHT_MASK; + } else { + forward_left_movers = 0; + forward_right_movers = 0; + } + + let can_jump = if forward_left_movers != 0 + || forward_right_movers != 0 + || backward_left_movers != 0 + || backward_right_movers != 0 + { + 2 + } else { + 0 + }; + + Self { + forward_left_movers, + forward_right_movers, + backward_left_movers, + backward_right_movers: backward_right_movers | can_jump, + } + } + + const fn has_jumps_dark(board: CheckersBitBoard) -> bool { + const FORWARD_LEFT_MASK: u32 = 0b00110000111100111111001111000011; + const FORWARD_RIGHT_MASK: u32 = 0b00111100111111001111000011001100; + const BACKWARD_LEFT_MASK: u32 = 0b11110011111100111100001100110000; + const BACKWARD_RIGHT_MASK: u32 = 0b11111100111100001100110000111100; + + let not_occupied = !board.pieces_bits(); + let enemy_pieces = board.pieces_bits() & !board.color_bits(); + let friendly_pieces = board.pieces_bits() & board.color_bits(); + + let forward_left_spaces = + not_occupied.rotate_right(14) & enemy_pieces.rotate_right(7) & FORWARD_LEFT_MASK; + let forward_right_spaces = + not_occupied.rotate_right(2) & enemy_pieces.rotate_right(1) & FORWARD_RIGHT_MASK; + + let forward_spaces = forward_left_spaces | forward_right_spaces; + + if board.king_bits() > 0 { + let backward_left_spaces = + not_occupied.rotate_left(2) & enemy_pieces.rotate_left(1) & BACKWARD_LEFT_MASK; + let backward_right_spaces = + not_occupied.rotate_left(14) & enemy_pieces.rotate_left(7) & BACKWARD_RIGHT_MASK; + let backward_spaces = backward_left_spaces | backward_right_spaces; + + let backward_spaces = board.king_bits() & backward_spaces; + friendly_pieces & (forward_spaces | backward_spaces) != 0 + } else { + friendly_pieces & forward_spaces != 0 + } + } + + const fn has_jumps_light(board: CheckersBitBoard) -> bool { + const FORWARD_LEFT_MASK: u32 = 0b00110000111100111111001111000011; + const FORWARD_RIGHT_MASK: u32 = 0b00111100111111001111000011001100; + const BACKWARD_LEFT_MASK: u32 = 0b11110011111100111100001100110000; + const BACKWARD_RIGHT_MASK: u32 = 0b11111100111100001100110000111100; + + let not_occupied = !board.pieces_bits(); + let enemy_pieces = board.pieces_bits() & board.color_bits(); + let friendly_pieces = board.pieces_bits() & !board.color_bits(); + + let backward_left_spaces = + not_occupied.rotate_left(2) & enemy_pieces.rotate_left(1) & BACKWARD_LEFT_MASK; + let backward_right_spaces = + not_occupied.rotate_left(14) & enemy_pieces.rotate_left(7) & BACKWARD_RIGHT_MASK; + + let backward_spaces = backward_left_spaces | backward_right_spaces; + + if board.king_bits() > 0 { + let forward_left_spaces = + not_occupied.rotate_right(14) & enemy_pieces.rotate_right(7) & FORWARD_LEFT_MASK; + let forward_right_spaces = + not_occupied.rotate_right(2) & enemy_pieces.rotate_right(1) & FORWARD_RIGHT_MASK; + let forward_spaces = forward_left_spaces | forward_right_spaces; + + let forward_spaces = board.king_bits() & forward_spaces; + friendly_pieces & (forward_spaces | backward_spaces) != 0 + } else { + friendly_pieces & backward_spaces != 0 + } + } + + #[inline(always)] + // TODO optimize + pub const fn has_jumps(board: CheckersBitBoard) -> bool { + match board.turn() { + PieceColor::Light => Self::has_jumps_light(board), + PieceColor::Dark => Self::has_jumps_dark(board), + } + } + + const fn has_jumps_at_dark(board: CheckersBitBoard, value: usize) -> bool { + const FORWARD_LEFT_MASK: u32 = 0b00110000111100111111001111000011; + const FORWARD_RIGHT_MASK: u32 = 0b00111100111111001111000011001100; + const BACKWARD_LEFT_MASK: u32 = 0b11110011111100111100001100110000; + const BACKWARD_RIGHT_MASK: u32 = 0b11111100111100001100110000111100; + + let not_occupied = !board.pieces_bits(); + let enemy_pieces = board.pieces_bits() & !board.color_bits(); + let friendly_pieces = board.pieces_bits() & board.color_bits(); + + let forward_left_spaces = + not_occupied.rotate_right(14) & enemy_pieces.rotate_right(7) & FORWARD_LEFT_MASK; + let forward_right_spaces = + not_occupied.rotate_right(2) & enemy_pieces.rotate_right(1) & FORWARD_RIGHT_MASK; + + let forward_spaces = forward_left_spaces | forward_right_spaces; + + if board.king_bits() > 0 { + let backward_left_spaces = + not_occupied.rotate_left(2) & enemy_pieces.rotate_left(1) & BACKWARD_LEFT_MASK; + let backward_right_spaces = + not_occupied.rotate_left(14) & enemy_pieces.rotate_left(7) & BACKWARD_RIGHT_MASK; + let backward_spaces = backward_left_spaces | backward_right_spaces; + + let backward_spaces = board.king_bits() & backward_spaces; + ((friendly_pieces & (forward_spaces | backward_spaces)) >> value) & 1 != 0 + } else { + ((friendly_pieces & forward_spaces) >> value) & 1 != 0 + } + } + + const fn has_jumps_at_light(board: CheckersBitBoard, value: usize) -> bool { + const FORWARD_LEFT_MASK: u32 = 0b00110000111100111111001111000011; + const FORWARD_RIGHT_MASK: u32 = 0b00111100111111001111000011001100; + const BACKWARD_LEFT_MASK: u32 = 0b11110011111100111100001100110000; + const BACKWARD_RIGHT_MASK: u32 = 0b11111100111100001100110000111100; + + let not_occupied = !board.pieces_bits(); + let enemy_pieces = board.pieces_bits() & board.color_bits(); + let friendly_pieces = board.pieces_bits() & !board.color_bits(); + + let backward_left_spaces = + not_occupied.rotate_left(2) & enemy_pieces.rotate_left(1) & BACKWARD_LEFT_MASK; + let backward_right_spaces = + not_occupied.rotate_left(14) & enemy_pieces.rotate_left(7) & BACKWARD_RIGHT_MASK; + + let backward_spaces = backward_left_spaces | backward_right_spaces; + + if board.king_bits() > 0 { + let forward_left_spaces = + not_occupied.rotate_right(14) & enemy_pieces.rotate_right(7) & FORWARD_LEFT_MASK; + let forward_right_spaces = + not_occupied.rotate_right(2) & enemy_pieces.rotate_right(1) & FORWARD_RIGHT_MASK; + let forward_spaces = forward_left_spaces | forward_right_spaces; + + let forward_spaces = board.king_bits() & forward_spaces; + ((friendly_pieces & (forward_spaces | backward_spaces)) >> value) & 1 != 0 + } else { + ((friendly_pieces & backward_spaces) >> value) & 1 != 0 + } + } + + #[inline(always)] + // TODO optimize + pub const fn has_jumps_at(board: CheckersBitBoard, value: usize) -> bool { + match board.turn() { + PieceColor::Light => Self::has_jumps_at_light(board, value), + PieceColor::Dark => Self::has_jumps_at_dark(board, value), + } + } + + const fn light_moves(board: CheckersBitBoard) -> Self { + let jumps = Self::jumps_light(board); + if jumps.is_empty() { + Self::slides_light(board) + } else { + jumps + } + } + + const fn dark_moves(board: CheckersBitBoard) -> Self { + let jumps = Self::jumps_dark(board); + if jumps.is_empty() { + Self::slides_dark(board) + } else { + jumps + } + } + + const fn filter_to_square(self, square: u8) -> Self { + let mask = 1 << square; + Self { + forward_left_movers: self.forward_left_movers & mask, + forward_right_movers: self.forward_right_movers & mask, + backward_left_movers: self.backward_left_movers & mask, + backward_right_movers: self.backward_right_movers & (mask | 2), + } + } + + pub fn moves(board: CheckersBitBoard) -> Self { + let moves = match board.turn() { + PieceColor::Dark => Self::dark_moves(board), + PieceColor::Light => Self::light_moves(board), + }; + + if board.turn == board.previous_turn { + moves.filter_to_square(board.previous_move_to) + } else { + moves + } + } + + /// Returns true if no moves are possible + pub const fn is_empty(self) -> bool { + (self.backward_left_movers + | (self.forward_left_movers) + | self.forward_right_movers + | self.backward_right_movers & 4294967293) + == 0 + } + + /// Returns true if the piece can jump + pub const fn can_jump(self) -> bool { + (self.backward_right_movers & 2) != 0 + } + + /// Returns true if the given move is possible + pub const fn contains(self, checker_move: Move) -> bool { + if checker_move.is_jump() != self.can_jump() { + return false; + } + + let bits = match checker_move.direction() { + MoveDirection::ForwardLeft => self.forward_left_movers, + MoveDirection::ForwardRight => self.forward_right_movers, + MoveDirection::BackwardLeft => self.backward_left_movers, + MoveDirection::BackwardRight => self.backward_right_movers, + }; + + (bits >> checker_move.start()) & 1 == 1 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn setup_empty_iter() -> PossibleMovesIter { + let moves = [MaybeUninit::uninit(); POSSIBLE_MOVES_ITER_SIZE]; + PossibleMovesIter { + moves, + index: 0, + length: 0, + } + } + + fn setup_add_move_to_iter_invalid() -> (PossibleMovesIter, PossibleMoves) { + let moves = PossibleMoves { + forward_left_movers: 0, + forward_right_movers: 0, + backward_left_movers: 0, + backward_right_movers: 0, + }; + let iter = setup_empty_iter(); + + (iter, moves) + } + + fn setup_add_move_to_iter_valid() -> (PossibleMovesIter, PossibleMoves) { + let moves = PossibleMoves { + forward_left_movers: u32::MAX, + forward_right_movers: u32::MAX, + backward_left_movers: u32::MAX, + backward_right_movers: u32::MAX, + }; + let iter = setup_empty_iter(); + + (iter, moves) + } + + #[test] + fn same() { + let start = CheckersBitBoard::new( + 0b11100111100111100111110111111011, + 0b00001100001111001111001111000011, + 0, + PieceColor::Dark, + ); + let flip = CheckersBitBoard::new( + 0b11100111100111100111110111111011, + 0b11110011110000110000110000111100, + 0, + PieceColor::Light, + ); + + assert_eq!( + PossibleMoves::has_jumps(start), + PossibleMoves::has_jumps(flip) + ) + } + + #[test] + fn iter_next() { + let test_move1 = Move::new(8, MoveDirection::ForwardLeft, false); + let test_move2 = Move::new(26, MoveDirection::ForwardRight, true); + let mut iter = setup_empty_iter(); + iter.length = 2; + + let ptr = iter.moves.as_mut().get_mut(0).unwrap(); + *ptr = MaybeUninit::new(test_move1); + + let ptr = iter.moves.as_mut().get_mut(1).unwrap(); + *ptr = MaybeUninit::new(test_move2); + + let recieved_move = iter.next(); + assert!(recieved_move.is_some()); + assert_eq!(recieved_move.unwrap(), test_move1); + + let recieved_move = iter.next(); + assert!(recieved_move.is_some()); + assert_eq!(recieved_move.unwrap(), test_move2); + + let recieved_move = iter.next(); + assert!(recieved_move.is_none()); + } + + #[test] + fn add_slide_forward_left_to_iter_invalid() { + const START: usize = 8; + let (mut iter, moves) = setup_add_move_to_iter_invalid(); + iter.add_slide_forward_left::(moves); + + assert_eq!(iter.index, 0); + assert_eq!(iter.length, 0); + } + + #[test] + fn add_slide_forward_left_to_iter_valid() { + const START: usize = 8; + let (mut iter, moves) = setup_add_move_to_iter_valid(); + iter.add_slide_forward_left::(moves); + + assert_eq!(iter.index, 0); + assert_eq!(iter.length, 1); + + let new_move = iter.next().unwrap(); + assert_eq!(new_move.start(), START as u32); + assert_eq!(new_move.direction(), MoveDirection::ForwardLeft); + assert!(!new_move.is_jump()); + } + + #[test] + fn add_slide_forward_right_to_iter_invalid() { + const START: usize = 26; + let (mut iter, moves) = setup_add_move_to_iter_invalid(); + iter.add_slide_forward_right::(moves); + + assert_eq!(iter.index, 0); + assert_eq!(iter.length, 0); + } + + #[test] + fn add_slide_forward_right_to_iter_valid() { + const START: usize = 26; + let (mut iter, moves) = setup_add_move_to_iter_valid(); + iter.add_slide_forward_right::(moves); + + assert_eq!(iter.index, 0); + assert_eq!(iter.length, 1); + + let new_move = iter.next().unwrap(); + assert_eq!(new_move.start(), START as u32); + assert_eq!(new_move.direction(), MoveDirection::ForwardRight); + assert!(!new_move.is_jump()); + } + + #[test] + fn add_slide_backward_left_to_iter_invalid() { + const START: usize = 17; + let (mut iter, moves) = setup_add_move_to_iter_invalid(); + iter.add_slide_backward_left::(moves); + + assert_eq!(iter.index, 0); + assert_eq!(iter.length, 0); + } + + #[test] + fn add_slide_backward_left_to_iter_valid() { + const START: usize = 17; + let (mut iter, moves) = setup_add_move_to_iter_valid(); + iter.add_slide_backward_left::(moves); + + assert_eq!(iter.index, 0); + assert_eq!(iter.length, 1); + + let new_move = iter.next().unwrap(); + assert_eq!(new_move.start(), START as u32); + assert_eq!(new_move.direction(), MoveDirection::BackwardLeft); + assert!(!new_move.is_jump()); + } + + #[test] + fn add_slide_backward_right_to_iter_invalid() { + const START: usize = 3; + let (mut iter, moves) = setup_add_move_to_iter_invalid(); + iter.add_slide_backward_right::(moves); + + assert_eq!(iter.index, 0); + assert_eq!(iter.length, 0); + } + + #[test] + fn add_slide_backward_right_to_iter_valid() { + const START: usize = 3; + let (mut iter, moves) = setup_add_move_to_iter_valid(); + iter.add_slide_backward_right::(moves); + + assert_eq!(iter.index, 0); + assert_eq!(iter.length, 1); + + let new_move = iter.next().unwrap(); + assert_eq!(new_move.start(), START as u32); + assert_eq!(new_move.direction(), MoveDirection::BackwardRight); + assert!(!new_move.is_jump()); + } + + #[test] + fn add_jump_forward_left_to_iter_invalid() { + const START: usize = 8; + let (mut iter, moves) = setup_add_move_to_iter_invalid(); + iter.add_jump_forward_left::(moves); + + assert_eq!(iter.index, 0); + assert_eq!(iter.length, 0); + } + + #[test] + fn add_jump_forward_left_to_iter_valid() { + const START: usize = 8; + let (mut iter, moves) = setup_add_move_to_iter_valid(); + iter.add_jump_forward_left::(moves); + + assert_eq!(iter.index, 0); + assert_eq!(iter.length, 1); + + let new_move = iter.next().unwrap(); + assert_eq!(new_move.start(), START as u32); + assert_eq!(new_move.direction(), MoveDirection::ForwardLeft); + assert!(new_move.is_jump()); + } + + #[test] + fn add_jump_forward_right_to_iter_invalid() { + const START: usize = 26; + let (mut iter, moves) = setup_add_move_to_iter_invalid(); + iter.add_jump_forward_right::(moves); + + assert_eq!(iter.index, 0); + assert_eq!(iter.length, 0); + } + + #[test] + fn add_jump_forward_right_to_iter_valid() { + const START: usize = 26; + let (mut iter, moves) = setup_add_move_to_iter_valid(); + iter.add_jump_forward_right::(moves); + + assert_eq!(iter.index, 0); + assert_eq!(iter.length, 1); + + let new_move = iter.next().unwrap(); + assert_eq!(new_move.start(), START as u32); + assert_eq!(new_move.direction(), MoveDirection::ForwardRight); + assert!(new_move.is_jump()); + } + + #[test] + fn add_jump_backward_left_to_iter_invalid() { + const START: usize = 17; + let (mut iter, moves) = setup_add_move_to_iter_invalid(); + iter.add_jump_backward_left::(moves); + + assert_eq!(iter.index, 0); + assert_eq!(iter.length, 0); + } + + #[test] + fn add_jump_backward_left_to_iter_valid() { + const START: usize = 17; + let (mut iter, moves) = setup_add_move_to_iter_valid(); + iter.add_jump_backward_left::(moves); + + assert_eq!(iter.index, 0); + assert_eq!(iter.length, 1); + + let new_move = iter.next().unwrap(); + assert_eq!(new_move.start(), START as u32); + assert_eq!(new_move.direction(), MoveDirection::BackwardLeft); + assert!(new_move.is_jump()); + } + + #[test] + fn add_jump_backward_right_to_iter_invalid() { + const START: usize = 3; + let (mut iter, moves) = setup_add_move_to_iter_invalid(); + iter.add_jump_backward_right::(moves); + + assert_eq!(iter.index, 0); + assert_eq!(iter.length, 0); + } + + #[test] + fn add_jump_backward_right_to_iter_valid() { + const START: usize = 3; + let (mut iter, moves) = setup_add_move_to_iter_valid(); + iter.add_jump_backward_right::(moves); + + assert_eq!(iter.index, 0); + assert_eq!(iter.length, 1); + + let new_move = iter.next().unwrap(); + assert_eq!(new_move.start(), START as u32); + assert_eq!(new_move.direction(), MoveDirection::BackwardRight); + assert!(new_move.is_jump()); + } + + #[test] + fn cant_jump_in_position_2_without_26() { + // This bug was bizarre, but it's caused by a white piece being in the + //second bit while there is no piece in the 26th bit. If you don't + // apply the bit mask for collision detection, then all of the light + // player moves become jumps. + let board = CheckersBitBoard::new(16908890, 401395713, 50332352, PieceColor::Light); + let possible_moves = PossibleMoves::moves(board); + assert!(!possible_moves.can_jump()) + } + + #[test] + fn not_has_jump_at_14_when_has_jump_at_20() { + // This bug was caused by me forgetting to `& 1` to the end of the + // `has_jump_at` functions. After playing a jump with one piece, I was + // able to continue jumping with completely different pieces + let board = CheckersBitBoard::new( + 0b11100111001111001111110111111011, + 0b00001100001111001111001111000011, + 0, + PieceColor::Dark, + ); + let possible_moves = PossibleMoves::moves(board); + assert!(!possible_moves.can_jump()) + } + + #[test] + fn test_send() { + fn assert_send() {} + assert_send::(); + assert_send::(); + } + + #[test] + fn test_sync() { + fn assert_sync() {} + assert_sync::(); + assert_sync::(); + } +} diff --git a/pdn/Cargo.toml b/pdn/Cargo.toml old mode 100644 new mode 100755 diff --git a/pdn/src/grammar.rs b/pdn/src/grammar.rs old mode 100644 new mode 100755 index 9529b59..ba3d086 --- a/pdn/src/grammar.rs +++ b/pdn/src/grammar.rs @@ -1,443 +1,443 @@ -use std::{iter::Peekable, sync::Arc}; - -use crate::tokens::{Color, PdnToken, PdnTokenBody, TokenHeader}; - -#[derive(Debug, Clone)] -pub struct PdnFile { - games: Vec, - game_separators: Vec, -} - -#[derive(Debug, Clone)] -pub struct Game { - header: Vec, - body: Vec, -} - -#[derive(Debug, Clone)] -pub struct PdnTag { - left_bracket: TokenHeader, - identifier_token: TokenHeader, - string_token: TokenHeader, - right_bracket: TokenHeader, - - identifier: Arc, - string: Arc, -} - -#[derive(Debug, Clone)] -pub enum BodyPart { - Move(GameMove), - Variation(Variation), - Comment(TokenHeader, Arc), - Setup(TokenHeader, Arc), - Nag(TokenHeader, usize), -} - -#[derive(Debug, Clone)] -pub struct Variation { - left_parenthesis: TokenHeader, - body: Vec, - right_parenthesis: TokenHeader, -} - -#[derive(Debug, Clone)] -pub struct GameMove { - move_number: Option<(TokenHeader, usize, Color)>, - game_move: Move, - move_strength: Option<(TokenHeader, Arc)>, -} - -#[derive(Debug, Clone)] -pub enum Move { - Normal(Square, TokenHeader, Square), - Capture(Square, Vec<(TokenHeader, Square)>), -} - -#[derive(Debug, Clone)] -pub enum Square { - Alpha(TokenHeader, char, char), - Num(TokenHeader, u8), -} - -/// Returns `Ok` if parsed successfully. If there are no tokens left, -/// `Err(None)` is returned. If the next token is not a square position, then -/// `Err(Some(token))` is returned. -fn parse_square(scanner: &mut impl Iterator) -> Result> { - let Some(token) = scanner.next() else { - return Err(None); - }; - let header = token.header; - let body = &token.body; - - match *body { - PdnTokenBody::AlphaSquare(letter, number) => Ok(Square::Alpha(header, letter, number)), - PdnTokenBody::NumSquare(number) => Ok(Square::Num(header, number)), - _ => Err(Some(token)), - } -} - -#[derive(Debug, Clone)] -pub enum MoveError { - EndOfFile, - NoStartSquare(Option), - NoEndSquare(Option), - InvalidCaptureSquares(Vec>), - NoMoveSeparator, -} - -fn parse_normal_move( - first_square: Square, - scanner: &mut impl Iterator, -) -> Result { - let Some(separator) = scanner.next() else { - return Err(MoveError::NoMoveSeparator); - }; - let square = match parse_square(scanner) { - Ok(square) => square, - Err(error) => return Err(MoveError::NoEndSquare(error)), - }; - Ok(Move::Normal(first_square, separator.header, square)) -} - -fn parse_capture_move( - first_square: Square, - scanner: &mut Peekable>, -) -> Result { - let mut captures = Vec::new(); - let mut errors = Vec::new(); - - while let Some(token) = scanner.peek() { - if token.body != PdnTokenBody::CaptureSeparator { - break; - } - - let separator = scanner.next().expect("separator should be next"); - match parse_square(scanner) { - Ok(square) => captures.push((separator.header, square)), - Err(error) => errors.push(error), - } - } - - if !errors.is_empty() { - Err(MoveError::InvalidCaptureSquares(errors)) - } else { - Ok(Move::Capture(first_square, captures)) - } -} - -fn parse_move(scanner: &mut Peekable>) -> Result { - let square = match parse_square(scanner) { - Ok(square) => square, - Err(error) => return Err(MoveError::NoStartSquare(error)), - }; - - let Some(token) = scanner.peek() else { - return Err(MoveError::NoMoveSeparator); - }; - let body = &token.body; - - match body { - PdnTokenBody::MoveSeparator => parse_normal_move(square, scanner), - PdnTokenBody::CaptureSeparator => parse_capture_move(square, scanner), - _ => Err(MoveError::NoMoveSeparator), - } -} - -#[derive(Debug, Clone)] -pub enum GameMoveError { - EndOfFile, - BadMove(MoveError), -} - -fn whitespace_if_found( - scanner: &mut Peekable>, -) -> Option { - let token = scanner.peek()?; - if let PdnTokenBody::Space(_) = token.body { - Some(scanner.next()?.header) - } else { - None - } -} - -fn parse_game_move( - scanner: &mut Peekable>, -) -> Result { - let Some(next_token) = scanner.peek() else { - return Err(GameMoveError::EndOfFile); - }; - - let move_number = match next_token.body { - PdnTokenBody::MoveNumber(number, color) => Some((next_token.header, number, color)), - _ => None, - }; - - if move_number.is_some() { - scanner.next(); - } - - whitespace_if_found(scanner); - - let game_move = parse_move(scanner); - - let move_strength = if let Some(token) = scanner.peek() { - if let PdnTokenBody::MoveStrength(string) = &token.body { - Some((token.header, string.clone())) - } else { - None - } - } else { - None - }; - - if move_strength.is_some() { - scanner.next(); - } - - match game_move { - Ok(game_move) => Ok(GameMove { - move_number, - game_move, - move_strength, - }), - Err(error) => Err(GameMoveError::BadMove(error)), - } -} - -#[derive(Debug, Clone)] -pub enum VariationError { - UnexpectedEnd(BodyError), - BadBody(BodyError), -} - -fn parse_variation( - scanner: &mut Peekable>, -) -> Result { - let left_parenthesis = scanner.next().expect("should start with left paren").header; - let body = parse_body_until(scanner, PdnTokenBody::RightParenthesis)?; - let right_parenthesis = scanner.next().expect("should end with right paren").header; - - Ok(Variation { - left_parenthesis, - body, - right_parenthesis, - }) -} - -#[derive(Debug, Clone)] -pub enum BodyPartError { - EndOfFile, - InvalidToken(PdnToken), - BadMove(GameMoveError), - BadVariation(VariationError), -} - -fn parse_body_part( - scanner: &mut Peekable>, -) -> Result { - let Some(token) = scanner.peek() else { - return Err(BodyPartError::EndOfFile); - }; - - match &token.body { - PdnTokenBody::MoveNumber(..) - | PdnTokenBody::AlphaSquare(..) - | PdnTokenBody::NumSquare(..) => match parse_game_move(scanner) { - Ok(mov) => Ok(BodyPart::Move(mov)), - Err(error) => Err(BodyPartError::BadMove(error)), - }, - PdnTokenBody::LeftParenthesis => match parse_variation(scanner) { - Ok(variation) => Ok(BodyPart::Variation(variation)), - Err(error) => Err(BodyPartError::BadVariation(error)), - }, - PdnTokenBody::Comment(string) => Ok(BodyPart::Comment(token.header, string.clone())), - PdnTokenBody::Setup(string) => Ok(BodyPart::Setup(token.header, string.clone())), - PdnTokenBody::Nag(number) => Ok(BodyPart::Nag(token.header, *number)), - _ => Err(BodyPartError::InvalidToken(token.clone())), - } -} - -pub type BodyError = Vec>; - -fn parse_body_until( - scanner: &mut Peekable>, - until: PdnTokenBody, -) -> Result, VariationError> { - let mut parts = Vec::new(); - - loop { - whitespace_if_found(scanner); - - let Some(token) = scanner.peek() else { - return Err(VariationError::UnexpectedEnd(parts)); - }; - - if token.body == until { - break; - } - - parts.push(parse_body_part(scanner)); - whitespace_if_found(scanner); - } - - if parts.iter().any(|r| r.is_err()) { - Err(VariationError::BadBody(parts)) - } else { - Ok(parts.iter().map(|r| r.as_ref().cloned().unwrap()).collect()) - } -} - -#[derive(Debug, Clone)] -pub enum PdnTagError { - EndOfFile, - NoStartBracket(PdnToken), - Unterminated(Vec), - NoIdentifier, - NoString, - NoEndBracket, -} - -fn parse_pdn_tag( - scanner: &mut Peekable>, -) -> Result { - whitespace_if_found(scanner); - - let Some(left_bracket) = scanner.next() else { - return Err(PdnTagError::EndOfFile); - }; - - if left_bracket.body != PdnTokenBody::LeftBracket { - return Err(PdnTagError::NoStartBracket(left_bracket)); - } - - whitespace_if_found(scanner); - - let Some(identifier_token) = scanner.next() else { - return Err(PdnTagError::Unterminated(vec![left_bracket])); - }; - - let PdnTokenBody::Identifier(identifier) = &identifier_token.body else { - return Err(PdnTagError::NoIdentifier); - }; - - whitespace_if_found(scanner); - - let Some(value_token) = scanner.next() else { - return Err(PdnTagError::Unterminated(vec![ - left_bracket, - identifier_token, - ])); - }; - - let PdnTokenBody::String(value) = &value_token.body else { - return Err(PdnTagError::NoIdentifier); - }; - - whitespace_if_found(scanner); - - let Some(right_bracket) = scanner.next() else { - return Err(PdnTagError::Unterminated(vec![ - left_bracket, - identifier_token, - value_token, - ])); - }; - - if right_bracket.body != PdnTokenBody::RightBracket { - return Err(PdnTagError::NoEndBracket); - } - - whitespace_if_found(scanner); - - Ok(PdnTag { - left_bracket: left_bracket.header, - identifier_token: identifier_token.header, - string_token: value_token.header, - right_bracket: right_bracket.header, - identifier: identifier.clone(), - string: value.clone(), - }) -} - -pub type HeaderError = Vec>; - -fn parse_header( - scanner: &mut Peekable>, -) -> Result, HeaderError> { - let mut tags = Vec::new(); - - loop { - let Some(token) = scanner.peek() else { - break; - }; - - if token.body != PdnTokenBody::LeftBracket { - break; - } - - tags.push(parse_pdn_tag(scanner)); - } - - if tags.iter().any(|r| r.is_err()) { - Err(tags) - } else { - Ok(tags.iter().map(|r| r.as_ref().cloned().unwrap()).collect()) - } -} - -#[derive(Debug, Clone)] -pub struct GameError { - header: Result, HeaderError>, - body: Result, VariationError>, -} - -fn parse_game(scanner: &mut Peekable>) -> Result { - let header = parse_header(scanner); - let body = parse_body_until(scanner, PdnTokenBody::Asterisk); - whitespace_if_found(scanner); - - if let Ok(header) = header { - if let Ok(body) = body { - Ok(Game { header, body }) - } else { - Err(GameError { - header: Ok(header), - body, - }) - } - } else { - Err(GameError { header, body }) - } -} - -pub type PdnError = Vec>; - -fn parse(scanner: &mut impl Iterator) -> Result { - let mut scanner = scanner.peekable(); - let mut games = Vec::new(); - let mut game_separators = Vec::new(); - - loop { - let Some(token) = scanner.peek() else { - break; - }; - - if token.body != PdnTokenBody::LeftBracket { - break; - } - - games.push(parse_game(&mut scanner)); - game_separators.push(scanner.next().unwrap().header); - } - - if games.iter().any(|r| r.is_err()) { - Err(games) - } else { - let games = games.iter().map(|r| r.as_ref().cloned().unwrap()).collect(); - Ok(PdnFile { - games, - game_separators, - }) - } -} +use std::{iter::Peekable, sync::Arc}; + +use crate::tokens::{Color, PdnToken, PdnTokenBody, TokenHeader}; + +#[derive(Debug, Clone)] +pub struct PdnFile { + games: Vec, + game_separators: Vec, +} + +#[derive(Debug, Clone)] +pub struct Game { + header: Vec, + body: Vec, +} + +#[derive(Debug, Clone)] +pub struct PdnTag { + left_bracket: TokenHeader, + identifier_token: TokenHeader, + string_token: TokenHeader, + right_bracket: TokenHeader, + + identifier: Arc, + string: Arc, +} + +#[derive(Debug, Clone)] +pub enum BodyPart { + Move(GameMove), + Variation(Variation), + Comment(TokenHeader, Arc), + Setup(TokenHeader, Arc), + Nag(TokenHeader, usize), +} + +#[derive(Debug, Clone)] +pub struct Variation { + left_parenthesis: TokenHeader, + body: Vec, + right_parenthesis: TokenHeader, +} + +#[derive(Debug, Clone)] +pub struct GameMove { + move_number: Option<(TokenHeader, usize, Color)>, + game_move: Move, + move_strength: Option<(TokenHeader, Arc)>, +} + +#[derive(Debug, Clone)] +pub enum Move { + Normal(Square, TokenHeader, Square), + Capture(Square, Vec<(TokenHeader, Square)>), +} + +#[derive(Debug, Clone)] +pub enum Square { + Alpha(TokenHeader, char, char), + Num(TokenHeader, u8), +} + +/// Returns `Ok` if parsed successfully. If there are no tokens left, +/// `Err(None)` is returned. If the next token is not a square position, then +/// `Err(Some(token))` is returned. +fn parse_square(scanner: &mut impl Iterator) -> Result> { + let Some(token) = scanner.next() else { + return Err(None); + }; + let header = token.header; + let body = &token.body; + + match *body { + PdnTokenBody::AlphaSquare(letter, number) => Ok(Square::Alpha(header, letter, number)), + PdnTokenBody::NumSquare(number) => Ok(Square::Num(header, number)), + _ => Err(Some(token)), + } +} + +#[derive(Debug, Clone)] +pub enum MoveError { + EndOfFile, + NoStartSquare(Option), + NoEndSquare(Option), + InvalidCaptureSquares(Vec>), + NoMoveSeparator, +} + +fn parse_normal_move( + first_square: Square, + scanner: &mut impl Iterator, +) -> Result { + let Some(separator) = scanner.next() else { + return Err(MoveError::NoMoveSeparator); + }; + let square = match parse_square(scanner) { + Ok(square) => square, + Err(error) => return Err(MoveError::NoEndSquare(error)), + }; + Ok(Move::Normal(first_square, separator.header, square)) +} + +fn parse_capture_move( + first_square: Square, + scanner: &mut Peekable>, +) -> Result { + let mut captures = Vec::new(); + let mut errors = Vec::new(); + + while let Some(token) = scanner.peek() { + if token.body != PdnTokenBody::CaptureSeparator { + break; + } + + let separator = scanner.next().expect("separator should be next"); + match parse_square(scanner) { + Ok(square) => captures.push((separator.header, square)), + Err(error) => errors.push(error), + } + } + + if !errors.is_empty() { + Err(MoveError::InvalidCaptureSquares(errors)) + } else { + Ok(Move::Capture(first_square, captures)) + } +} + +fn parse_move(scanner: &mut Peekable>) -> Result { + let square = match parse_square(scanner) { + Ok(square) => square, + Err(error) => return Err(MoveError::NoStartSquare(error)), + }; + + let Some(token) = scanner.peek() else { + return Err(MoveError::NoMoveSeparator); + }; + let body = &token.body; + + match body { + PdnTokenBody::MoveSeparator => parse_normal_move(square, scanner), + PdnTokenBody::CaptureSeparator => parse_capture_move(square, scanner), + _ => Err(MoveError::NoMoveSeparator), + } +} + +#[derive(Debug, Clone)] +pub enum GameMoveError { + EndOfFile, + BadMove(MoveError), +} + +fn whitespace_if_found( + scanner: &mut Peekable>, +) -> Option { + let token = scanner.peek()?; + if let PdnTokenBody::Space(_) = token.body { + Some(scanner.next()?.header) + } else { + None + } +} + +fn parse_game_move( + scanner: &mut Peekable>, +) -> Result { + let Some(next_token) = scanner.peek() else { + return Err(GameMoveError::EndOfFile); + }; + + let move_number = match next_token.body { + PdnTokenBody::MoveNumber(number, color) => Some((next_token.header, number, color)), + _ => None, + }; + + if move_number.is_some() { + scanner.next(); + } + + whitespace_if_found(scanner); + + let game_move = parse_move(scanner); + + let move_strength = if let Some(token) = scanner.peek() { + if let PdnTokenBody::MoveStrength(string) = &token.body { + Some((token.header, string.clone())) + } else { + None + } + } else { + None + }; + + if move_strength.is_some() { + scanner.next(); + } + + match game_move { + Ok(game_move) => Ok(GameMove { + move_number, + game_move, + move_strength, + }), + Err(error) => Err(GameMoveError::BadMove(error)), + } +} + +#[derive(Debug, Clone)] +pub enum VariationError { + UnexpectedEnd(BodyError), + BadBody(BodyError), +} + +fn parse_variation( + scanner: &mut Peekable>, +) -> Result { + let left_parenthesis = scanner.next().expect("should start with left paren").header; + let body = parse_body_until(scanner, PdnTokenBody::RightParenthesis)?; + let right_parenthesis = scanner.next().expect("should end with right paren").header; + + Ok(Variation { + left_parenthesis, + body, + right_parenthesis, + }) +} + +#[derive(Debug, Clone)] +pub enum BodyPartError { + EndOfFile, + InvalidToken(PdnToken), + BadMove(GameMoveError), + BadVariation(VariationError), +} + +fn parse_body_part( + scanner: &mut Peekable>, +) -> Result { + let Some(token) = scanner.peek() else { + return Err(BodyPartError::EndOfFile); + }; + + match &token.body { + PdnTokenBody::MoveNumber(..) + | PdnTokenBody::AlphaSquare(..) + | PdnTokenBody::NumSquare(..) => match parse_game_move(scanner) { + Ok(mov) => Ok(BodyPart::Move(mov)), + Err(error) => Err(BodyPartError::BadMove(error)), + }, + PdnTokenBody::LeftParenthesis => match parse_variation(scanner) { + Ok(variation) => Ok(BodyPart::Variation(variation)), + Err(error) => Err(BodyPartError::BadVariation(error)), + }, + PdnTokenBody::Comment(string) => Ok(BodyPart::Comment(token.header, string.clone())), + PdnTokenBody::Setup(string) => Ok(BodyPart::Setup(token.header, string.clone())), + PdnTokenBody::Nag(number) => Ok(BodyPart::Nag(token.header, *number)), + _ => Err(BodyPartError::InvalidToken(token.clone())), + } +} + +pub type BodyError = Vec>; + +fn parse_body_until( + scanner: &mut Peekable>, + until: PdnTokenBody, +) -> Result, VariationError> { + let mut parts = Vec::new(); + + loop { + whitespace_if_found(scanner); + + let Some(token) = scanner.peek() else { + return Err(VariationError::UnexpectedEnd(parts)); + }; + + if token.body == until { + break; + } + + parts.push(parse_body_part(scanner)); + whitespace_if_found(scanner); + } + + if parts.iter().any(|r| r.is_err()) { + Err(VariationError::BadBody(parts)) + } else { + Ok(parts.iter().map(|r| r.as_ref().cloned().unwrap()).collect()) + } +} + +#[derive(Debug, Clone)] +pub enum PdnTagError { + EndOfFile, + NoStartBracket(PdnToken), + Unterminated(Vec), + NoIdentifier, + NoString, + NoEndBracket, +} + +fn parse_pdn_tag( + scanner: &mut Peekable>, +) -> Result { + whitespace_if_found(scanner); + + let Some(left_bracket) = scanner.next() else { + return Err(PdnTagError::EndOfFile); + }; + + if left_bracket.body != PdnTokenBody::LeftBracket { + return Err(PdnTagError::NoStartBracket(left_bracket)); + } + + whitespace_if_found(scanner); + + let Some(identifier_token) = scanner.next() else { + return Err(PdnTagError::Unterminated(vec![left_bracket])); + }; + + let PdnTokenBody::Identifier(identifier) = &identifier_token.body else { + return Err(PdnTagError::NoIdentifier); + }; + + whitespace_if_found(scanner); + + let Some(value_token) = scanner.next() else { + return Err(PdnTagError::Unterminated(vec![ + left_bracket, + identifier_token, + ])); + }; + + let PdnTokenBody::String(value) = &value_token.body else { + return Err(PdnTagError::NoIdentifier); + }; + + whitespace_if_found(scanner); + + let Some(right_bracket) = scanner.next() else { + return Err(PdnTagError::Unterminated(vec![ + left_bracket, + identifier_token, + value_token, + ])); + }; + + if right_bracket.body != PdnTokenBody::RightBracket { + return Err(PdnTagError::NoEndBracket); + } + + whitespace_if_found(scanner); + + Ok(PdnTag { + left_bracket: left_bracket.header, + identifier_token: identifier_token.header, + string_token: value_token.header, + right_bracket: right_bracket.header, + identifier: identifier.clone(), + string: value.clone(), + }) +} + +pub type HeaderError = Vec>; + +fn parse_header( + scanner: &mut Peekable>, +) -> Result, HeaderError> { + let mut tags = Vec::new(); + + loop { + let Some(token) = scanner.peek() else { + break; + }; + + if token.body != PdnTokenBody::LeftBracket { + break; + } + + tags.push(parse_pdn_tag(scanner)); + } + + if tags.iter().any(|r| r.is_err()) { + Err(tags) + } else { + Ok(tags.iter().map(|r| r.as_ref().cloned().unwrap()).collect()) + } +} + +#[derive(Debug, Clone)] +pub struct GameError { + header: Result, HeaderError>, + body: Result, VariationError>, +} + +fn parse_game(scanner: &mut Peekable>) -> Result { + let header = parse_header(scanner); + let body = parse_body_until(scanner, PdnTokenBody::Asterisk); + whitespace_if_found(scanner); + + if let Ok(header) = header { + if let Ok(body) = body { + Ok(Game { header, body }) + } else { + Err(GameError { + header: Ok(header), + body, + }) + } + } else { + Err(GameError { header, body }) + } +} + +pub type PdnError = Vec>; + +fn parse(scanner: &mut impl Iterator) -> Result { + let mut scanner = scanner.peekable(); + let mut games = Vec::new(); + let mut game_separators = Vec::new(); + + loop { + let Some(token) = scanner.peek() else { + break; + }; + + if token.body != PdnTokenBody::LeftBracket { + break; + } + + games.push(parse_game(&mut scanner)); + game_separators.push(scanner.next().unwrap().header); + } + + if games.iter().any(|r| r.is_err()) { + Err(games) + } else { + let games = games.iter().map(|r| r.as_ref().cloned().unwrap()).collect(); + Ok(PdnFile { + games, + game_separators, + }) + } +} diff --git a/pdn/src/lib.rs b/pdn/src/lib.rs old mode 100644 new mode 100755 diff --git a/pdn/src/tokens.rs b/pdn/src/tokens.rs old mode 100644 new mode 100755 index d37d910..45e46e5 --- a/pdn/src/tokens.rs +++ b/pdn/src/tokens.rs @@ -1,284 +1,284 @@ -use std::sync::Arc; - -use snob::{csets, csets::CharacterSet, Scanner}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum Color { - White, - Black, -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum PdnTokenBody { - MoveNumber(usize, Color), - MoveSeparator, - CaptureSeparator, - AlphaSquare(char, char), - NumSquare(u8), - MoveStrength(Arc), - Nag(usize), - LeftParenthesis, - RightParenthesis, - LeftBracket, - RightBracket, - Asterisk, - Setup(Arc), - String(Arc), - Comment(Arc), - Identifier(Arc), - Space(Arc), -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct TokenHeader { - start: usize, - len: usize, -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct PdnToken { - pub header: TokenHeader, - pub body: PdnTokenBody, -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum TokenErrorType { - InvalidNumber(usize), - InvalidNag, - InvalidSquare, - UnterminatedSetup, - UnterminatedComment, - UnterminatedString, - InvalidToken, -} - -pub struct TokenError { - header: TokenHeader, - ty: TokenErrorType, -} - -pub struct PdnScanner { - scanner: Scanner, -} - -impl PdnScanner { - fn scan_string(&mut self) -> Option { - let mut string = String::new(); - loop { - if let Some(position) = self.scanner.many("\\\"".complement()) { - let part = self - .scanner - .goto(position) - .expect("position should be valid"); - string.push_str(&part); - } else if let Some(position) = self.scanner.starts_with("\\\"") { - self.scanner.goto(position); - string.push('"'); - } else { - break; - } - } - - if let Some(position) = self.scanner.any('"') { - self.scanner.goto(position); - Some(string) - } else { - None - } - } - - fn scan_unescaped_string(&mut self, terminator: char) -> Option { - let position = self.scanner.upto(terminator)?; - let string = self - .scanner - .goto(position) - .expect("position should be valid"); - let position = self - .scanner - .any(terminator) - .expect("there should be a terminator next"); - self.scanner.goto(position); - Some(string) - } - - fn scan_number(&mut self) -> Option { - let position = self.scanner.many(csets::AsciiDigits)?; - let number = self - .scanner - .goto(position) - .expect("position should be valid"); - let number: usize = number.parse().expect("should be a valid number"); - Some(number) - } - - fn scan_identifier(&mut self) -> Option { - let position = self - .scanner - .many(csets::AsciiLetters.union(csets::AsciiDigits).union('_'))?; - let identifier = self - .scanner - .goto(position) - .expect("position should be valid"); - Some(identifier) - } - - fn next_token(&mut self) -> Option> { - if self.scanner.is_at_end() { - return None; - } - - let token = if let Some(position) = self.scanner.any('-') { - self.scanner.goto(position); - Ok(PdnTokenBody::MoveSeparator) - } else if let Some(position) = self.scanner.any('x') { - self.scanner.goto(position); - Ok(PdnTokenBody::CaptureSeparator) - } else if let Some(position) = self.scanner.any('(') { - self.scanner.goto(position); - - // try a move strength token - if let Some(position) = self.scanner.many("?!") { - let char = self - .scanner - .char_at(position) - .expect("position should be valid"); - if char == ')' { - let strength = self - .scanner - .goto(position) - .expect("position should be valid"); - let position = self - .scanner - .any(')') - .expect("move strength should terminate"); - self.scanner.goto(position); - return Some(Ok(PdnTokenBody::MoveStrength(strength.into()))); - } - } - - Ok(PdnTokenBody::LeftParenthesis) - } else if let Some(position) = self.scanner.any(')') { - self.scanner.goto(position); - Ok(PdnTokenBody::RightParenthesis) - } else if let Some(position) = self.scanner.any('[') { - self.scanner.goto(position); - Ok(PdnTokenBody::LeftBracket) - } else if let Some(position) = self.scanner.any(']') { - self.scanner.goto(position); - Ok(PdnTokenBody::RightBracket) - } else if let Some(position) = self.scanner.any('*') { - self.scanner.goto(position); - Ok(PdnTokenBody::Asterisk) - } else if let Some(position) = self.scanner.any('$') { - self.scanner.goto(position); - match self.scan_number() { - Some(number) => Ok(PdnTokenBody::Nag(number)), - None => Err(TokenErrorType::InvalidNag), - } - } else if let Some(position) = self.scanner.any('/') { - self.scanner.goto(position); - match self.scan_unescaped_string('/') { - Some(string) => Ok(PdnTokenBody::Setup(string.into())), - None => Err(TokenErrorType::UnterminatedSetup), - } - } else if let Some(position) = self.scanner.any('{') { - self.scanner.goto(position); - match self.scan_unescaped_string('}') { - Some(string) => Ok(PdnTokenBody::Comment(string.into())), - None => Err(TokenErrorType::UnterminatedComment), - } - } else if let Some(position) = self.scanner.any('"') { - self.scanner.goto(position); - match self.scan_string() { - Some(string) => Ok(PdnTokenBody::String(string.into())), - None => Err(TokenErrorType::UnterminatedString), - } - } else if let Some(position) = self.scanner.many("?!") { - let strength = self - .scanner - .goto(position) - .expect("position should be valid"); - Ok(PdnTokenBody::MoveStrength(strength.into())) - } else if let Some(position) = self.scanner.any("abcdefgh") { - let letter = self - .scanner - .goto(position) - .expect("position should be valid") - .chars() - .next() - .expect("should contain one letter"); - if let Some(position) = self.scanner.any("12345678") { - let number = self - .scanner - .goto(position) - .expect("position should be valid") - .chars() - .next() - .expect("should contain one letter"); - Ok(PdnTokenBody::AlphaSquare(letter, number)) - } else { - self.scanner.advance(1); // skip over second character - Err(TokenErrorType::InvalidSquare) - } - } else if self.scanner.any(csets::AsciiUppercase).is_some() { - let identifier = self - .scan_identifier() - .expect("should be a valid identifier"); - Ok(PdnTokenBody::Identifier(identifier.into())) - } else if self.scanner.any(csets::AsciiDigits).is_some() { - let number = self.scan_number().expect("should be a valid number"); - if let Some(position) = self.scanner.starts_with("...") { - self.scanner.goto(position); - Ok(PdnTokenBody::MoveNumber(number, Color::Black)) - } else if let Some(position) = self.scanner.any('.') { - self.scanner.goto(position); - Ok(PdnTokenBody::MoveNumber(number, Color::White)) - } else if number < 100 { - Ok(PdnTokenBody::NumSquare(number as u8)) - } else { - Err(TokenErrorType::InvalidNumber(number)) - } - } else if let Some(position) = self.scanner.many(csets::AsciiWhitespace) { - let whitespace = self - .scanner - .goto(position) - .expect("position should be valid"); - Ok(PdnTokenBody::Space(whitespace.into())) - } else { - let position = self - .scanner - .upto(csets::AsciiLetters.union(csets::AsciiDigits.union("-x(?!)[]"))) - .unwrap_or_else(|| self.scanner.len()); - - self.scanner - .goto(position) - .expect("position should be valid"); - - Err(TokenErrorType::InvalidToken) - }; - - Some(token) - } -} - -impl Iterator for PdnScanner { - type Item = Result; - - fn next(&mut self) -> Option { - let start = self.scanner.position(); - let token = self.next_token()?; - let end = self.scanner.position(); - let len = end - start; - let header = TokenHeader { start, len }; - - let token = match token { - Ok(token) => Ok(PdnToken { - header, - body: token, - }), - Err(error) => Err(TokenError { header, ty: error }), - }; - - Some(token) - } -} +use std::sync::Arc; + +use snob::{csets, csets::CharacterSet, Scanner}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Color { + White, + Black, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum PdnTokenBody { + MoveNumber(usize, Color), + MoveSeparator, + CaptureSeparator, + AlphaSquare(char, char), + NumSquare(u8), + MoveStrength(Arc), + Nag(usize), + LeftParenthesis, + RightParenthesis, + LeftBracket, + RightBracket, + Asterisk, + Setup(Arc), + String(Arc), + Comment(Arc), + Identifier(Arc), + Space(Arc), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct TokenHeader { + start: usize, + len: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PdnToken { + pub header: TokenHeader, + pub body: PdnTokenBody, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum TokenErrorType { + InvalidNumber(usize), + InvalidNag, + InvalidSquare, + UnterminatedSetup, + UnterminatedComment, + UnterminatedString, + InvalidToken, +} + +pub struct TokenError { + header: TokenHeader, + ty: TokenErrorType, +} + +pub struct PdnScanner { + scanner: Scanner, +} + +impl PdnScanner { + fn scan_string(&mut self) -> Option { + let mut string = String::new(); + loop { + if let Some(position) = self.scanner.many("\\\"".complement()) { + let part = self + .scanner + .goto(position) + .expect("position should be valid"); + string.push_str(&part); + } else if let Some(position) = self.scanner.starts_with("\\\"") { + self.scanner.goto(position); + string.push('"'); + } else { + break; + } + } + + if let Some(position) = self.scanner.any('"') { + self.scanner.goto(position); + Some(string) + } else { + None + } + } + + fn scan_unescaped_string(&mut self, terminator: char) -> Option { + let position = self.scanner.upto(terminator)?; + let string = self + .scanner + .goto(position) + .expect("position should be valid"); + let position = self + .scanner + .any(terminator) + .expect("there should be a terminator next"); + self.scanner.goto(position); + Some(string) + } + + fn scan_number(&mut self) -> Option { + let position = self.scanner.many(csets::AsciiDigits)?; + let number = self + .scanner + .goto(position) + .expect("position should be valid"); + let number: usize = number.parse().expect("should be a valid number"); + Some(number) + } + + fn scan_identifier(&mut self) -> Option { + let position = self + .scanner + .many(csets::AsciiLetters.union(csets::AsciiDigits).union('_'))?; + let identifier = self + .scanner + .goto(position) + .expect("position should be valid"); + Some(identifier) + } + + fn next_token(&mut self) -> Option> { + if self.scanner.is_at_end() { + return None; + } + + let token = if let Some(position) = self.scanner.any('-') { + self.scanner.goto(position); + Ok(PdnTokenBody::MoveSeparator) + } else if let Some(position) = self.scanner.any('x') { + self.scanner.goto(position); + Ok(PdnTokenBody::CaptureSeparator) + } else if let Some(position) = self.scanner.any('(') { + self.scanner.goto(position); + + // try a move strength token + if let Some(position) = self.scanner.many("?!") { + let char = self + .scanner + .char_at(position) + .expect("position should be valid"); + if char == ')' { + let strength = self + .scanner + .goto(position) + .expect("position should be valid"); + let position = self + .scanner + .any(')') + .expect("move strength should terminate"); + self.scanner.goto(position); + return Some(Ok(PdnTokenBody::MoveStrength(strength.into()))); + } + } + + Ok(PdnTokenBody::LeftParenthesis) + } else if let Some(position) = self.scanner.any(')') { + self.scanner.goto(position); + Ok(PdnTokenBody::RightParenthesis) + } else if let Some(position) = self.scanner.any('[') { + self.scanner.goto(position); + Ok(PdnTokenBody::LeftBracket) + } else if let Some(position) = self.scanner.any(']') { + self.scanner.goto(position); + Ok(PdnTokenBody::RightBracket) + } else if let Some(position) = self.scanner.any('*') { + self.scanner.goto(position); + Ok(PdnTokenBody::Asterisk) + } else if let Some(position) = self.scanner.any('$') { + self.scanner.goto(position); + match self.scan_number() { + Some(number) => Ok(PdnTokenBody::Nag(number)), + None => Err(TokenErrorType::InvalidNag), + } + } else if let Some(position) = self.scanner.any('/') { + self.scanner.goto(position); + match self.scan_unescaped_string('/') { + Some(string) => Ok(PdnTokenBody::Setup(string.into())), + None => Err(TokenErrorType::UnterminatedSetup), + } + } else if let Some(position) = self.scanner.any('{') { + self.scanner.goto(position); + match self.scan_unescaped_string('}') { + Some(string) => Ok(PdnTokenBody::Comment(string.into())), + None => Err(TokenErrorType::UnterminatedComment), + } + } else if let Some(position) = self.scanner.any('"') { + self.scanner.goto(position); + match self.scan_string() { + Some(string) => Ok(PdnTokenBody::String(string.into())), + None => Err(TokenErrorType::UnterminatedString), + } + } else if let Some(position) = self.scanner.many("?!") { + let strength = self + .scanner + .goto(position) + .expect("position should be valid"); + Ok(PdnTokenBody::MoveStrength(strength.into())) + } else if let Some(position) = self.scanner.any("abcdefgh") { + let letter = self + .scanner + .goto(position) + .expect("position should be valid") + .chars() + .next() + .expect("should contain one letter"); + if let Some(position) = self.scanner.any("12345678") { + let number = self + .scanner + .goto(position) + .expect("position should be valid") + .chars() + .next() + .expect("should contain one letter"); + Ok(PdnTokenBody::AlphaSquare(letter, number)) + } else { + self.scanner.advance(1); // skip over second character + Err(TokenErrorType::InvalidSquare) + } + } else if self.scanner.any(csets::AsciiUppercase).is_some() { + let identifier = self + .scan_identifier() + .expect("should be a valid identifier"); + Ok(PdnTokenBody::Identifier(identifier.into())) + } else if self.scanner.any(csets::AsciiDigits).is_some() { + let number = self.scan_number().expect("should be a valid number"); + if let Some(position) = self.scanner.starts_with("...") { + self.scanner.goto(position); + Ok(PdnTokenBody::MoveNumber(number, Color::Black)) + } else if let Some(position) = self.scanner.any('.') { + self.scanner.goto(position); + Ok(PdnTokenBody::MoveNumber(number, Color::White)) + } else if number < 100 { + Ok(PdnTokenBody::NumSquare(number as u8)) + } else { + Err(TokenErrorType::InvalidNumber(number)) + } + } else if let Some(position) = self.scanner.many(csets::AsciiWhitespace) { + let whitespace = self + .scanner + .goto(position) + .expect("position should be valid"); + Ok(PdnTokenBody::Space(whitespace.into())) + } else { + let position = self + .scanner + .upto(csets::AsciiLetters.union(csets::AsciiDigits.union("-x(?!)[]"))) + .unwrap_or_else(|| self.scanner.len()); + + self.scanner + .goto(position) + .expect("position should be valid"); + + Err(TokenErrorType::InvalidToken) + }; + + Some(token) + } +} + +impl Iterator for PdnScanner { + type Item = Result; + + fn next(&mut self) -> Option { + let start = self.scanner.position(); + let token = self.next_token()?; + let end = self.scanner.position(); + let len = end - start; + let header = TokenHeader { start, len }; + + let token = match token { + Ok(token) => Ok(PdnToken { + header, + body: token, + }), + Err(error) => Err(TokenError { header, ty: error }), + }; + + Some(token) + } +} diff --git a/rustfmt.toml b/rustfmt.toml old mode 100644 new mode 100755 -- cgit v1.2.3