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