diff options
| author | Mica White <botahamec@outlook.com> | 2025-12-08 19:56:48 -0500 |
|---|---|---|
| committer | Mica White <botahamec@outlook.com> | 2025-12-08 19:56:48 -0500 |
| commit | fdb2804883deb31e3aeb15bbe588dcc9b7b76bd0 (patch) | |
| tree | a4c51cd88664cab7b6673dcd3b425fb7a0202a38 | |
| parent | 93461aa9399d981db7d8611b3eb166636de4d971 (diff) | |
| -rwxr-xr-x[-rw-r--r--] | .gitignore | 0 | ||||
| -rwxr-xr-x[-rw-r--r--] | .idea/.gitignore | 0 | ||||
| -rwxr-xr-x[-rw-r--r--] | .idea/checkers.iml | 1 | ||||
| -rwxr-xr-x[-rw-r--r--] | .idea/compilerexplorer.settings.xml | 0 | ||||
| -rwxr-xr-x[-rw-r--r--] | .idea/modules.xml | 0 | ||||
| -rwxr-xr-x[-rw-r--r--] | .idea/vcs.xml | 0 | ||||
| -rwxr-xr-x | .vscode/settings.json | 5 | ||||
| -rwxr-xr-x[-rw-r--r--] | Cargo.toml | 1 | ||||
| -rwxr-xr-x | ampere.h | 66 | ||||
| -rwxr-xr-x[-rw-r--r--] | config.toml | 0 | ||||
| -rwxr-xr-x | default.profdata | bin | 0 -> 512 bytes | |||
| -rwxr-xr-x | default.profraw | bin | 0 -> 697720 bytes | |||
| -rwxr-xr-x[-rw-r--r--] | engine/.gitignore | 0 | ||||
| -rwxr-xr-x[-rw-r--r--] | engine/Cargo.toml | 3 | ||||
| -rwxr-xr-x | engine/src/c_abi.rs | 403 | ||||
| -rwxr-xr-x[-rw-r--r--] | engine/src/engine.rs | 548 | ||||
| -rwxr-xr-x[-rw-r--r--] | engine/src/eval.rs | 331 | ||||
| -rwxr-xr-x | engine/src/info.rs | 27 | ||||
| -rwxr-xr-x[-rw-r--r--] | engine/src/lazysort.rs | 174 | ||||
| -rwxr-xr-x[-rw-r--r--] | engine/src/lib.rs | 38 | ||||
| -rwxr-xr-x[-rw-r--r--] | engine/src/main.rs | 141 | ||||
| -rwxr-xr-x[-rw-r--r--] | engine/src/search.rs | 530 | ||||
| -rwxr-xr-x[-rw-r--r--] | engine/src/tablebase.rs | 372 | ||||
| -rwxr-xr-x[-rw-r--r--] | engine/src/transposition_table.rs | 354 | ||||
| -rwxr-xr-x[-rw-r--r--] | model/Cargo.toml | 36 | ||||
| -rwxr-xr-x[-rw-r--r--] | model/benches/bitboard.rs | 182 | ||||
| -rwxr-xr-x[-rw-r--r--] | model/proptest-regressions/board/tests.txt | 0 | ||||
| -rwxr-xr-x[-rw-r--r--] | model/src/board.rs | 1346 | ||||
| -rwxr-xr-x[-rw-r--r--] | model/src/board/tests.rs | 1114 | ||||
| -rwxr-xr-x[-rw-r--r--] | model/src/color.rs | 0 | ||||
| -rwxr-xr-x[-rw-r--r--] | model/src/coordinates.rs | 304 | ||||
| -rwxr-xr-x[-rw-r--r--] | model/src/lib.rs | 26 | ||||
| -rwxr-xr-x[-rw-r--r--] | model/src/moves.rs | 590 | ||||
| -rwxr-xr-x[-rw-r--r--] | model/src/piece.rs | 42 | ||||
| -rwxr-xr-x[-rw-r--r--] | model/src/possible_moves.rs | 2156 | ||||
| -rwxr-xr-x[-rw-r--r--] | pdn/Cargo.toml | 0 | ||||
| -rwxr-xr-x[-rw-r--r--] | pdn/src/grammar.rs | 886 | ||||
| -rwxr-xr-x[-rw-r--r--] | pdn/src/lib.rs | 0 | ||||
| -rwxr-xr-x[-rw-r--r--] | pdn/src/tokens.rs | 568 | ||||
| -rwxr-xr-x[-rw-r--r--] | rustfmt.toml | 0 |
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 Binary files differnew file mode 100755 index 0000000..dbfbafa --- /dev/null +++ b/default.profdata diff --git a/default.profraw b/default.profraw Binary files differnew file mode 100755 index 0000000..8edf8e8 --- /dev/null +++ b/default.profraw 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 |
