From 0a86ccf725d40081a26698dd35d72ea33c12b954 Mon Sep 17 00:00:00 2001 From: Kirill Bobyrev Date: Thu, 27 Jun 2024 17:52:58 -0700 Subject: [PATCH] chore: Continue overhaul, simplify some code and refactor a bit --- .gitignore | 4 + .vscode/settings.json | 2 +- Cargo.lock | 237 ++++++++++++++++++++++++++++-- Cargo.toml | 6 +- README.md | 9 +- justfile | 9 +- src/{main.rs => bin/pabi.rs} | 4 +- src/chess/attacks.rs | 2 +- src/chess/bitboard.rs | 6 +- src/chess/core.rs | 269 +++++++++++++++++++---------------- src/chess/generated.rs | 2 +- src/chess/mod.rs | 1 + src/chess/position.rs | 189 +++++++++++------------- src/{mcts => chess}/state.rs | 25 ---- src/chess/zobrist.rs | 70 ++++----- src/engine/mod.rs | 27 ++-- src/environment.rs | 18 +++ src/evaluation/brain.rs | 3 - src/mcts/environment.rs | 6 - src/mcts/mod.rs | 52 ------- src/mcts/parameters.rs | 0 src/mcts/worker.rs | 0 tests/integration.rs | 24 ++-- 23 files changed, 559 insertions(+), 406 deletions(-) rename src/{main.rs => bin/pabi.rs} (83%) rename src/{mcts => chess}/state.rs (74%) create mode 100644 src/environment.rs delete mode 100644 src/mcts/environment.rs delete mode 100644 src/mcts/parameters.rs delete mode 100644 src/mcts/worker.rs diff --git a/.gitignore b/.gitignore index 0b0211cfb..86bf6da4d 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,7 @@ # tarpaulin test coverage reports cobertura.xml + +# Makefile-generated symlinks. +/pabi +/pabi-* diff --git a/.vscode/settings.json b/.vscode/settings.json index 1df022c6a..cc93b710f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -40,6 +40,7 @@ "movetime", "negamax", "Nullmove", + "openbench", "pabi", "pdep", "Perft", @@ -58,7 +59,6 @@ "setoption", "shakmaty", "startpos", - "Stockfish", "sysinfo", "TABLEBASE", "tablebases", diff --git a/Cargo.lock b/Cargo.lock index 3fe779a44..6401cbfb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,12 +26,55 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +[[package]] +name = "anstream" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" +[[package]] +name = "anstyle-parse" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + [[package]] name = "anyhow" version = "1.0.86" @@ -154,6 +197,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -162,8 +206,23 @@ version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" dependencies = [ + "anstream", "anstyle", "clap_lex", + "strsim", + "terminal_size", +] + +[[package]] +name = "clap_derive" +version = "4.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -172,6 +231,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" +[[package]] +name = "colorchoice" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" + [[package]] name = "const_fn" version = "0.4.10" @@ -326,6 +391,16 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "float-cmp" version = "0.9.0" @@ -378,6 +453,12 @@ dependencies = [ "crunchy", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -425,7 +506,7 @@ checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" dependencies = [ "hermit-abi", "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -434,6 +515,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06d198e9919d9822d5f7083ba8530e04de87841eaf21ead9af8f2304efd57c89" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "itertools" version = "0.10.5" @@ -494,6 +581,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + [[package]] name = "libz-sys" version = "1.1.18" @@ -506,6 +599,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + [[package]] name = "log" version = "0.4.21" @@ -537,6 +636,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -562,17 +662,19 @@ checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" [[package]] name = "pabi" -version = "2024.6.16" +version = "2024.6.27" dependencies = [ "anyhow", "arrayvec", "assert_cmd", "bitflags", + "clap", "criterion", "itertools 0.13.0", "predicates", "pretty_assertions", "rand", + "rand_distr", "shadow-rs", "shakmaty", ] @@ -717,6 +819,16 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand", +] + [[package]] name = "rayon" version = "1.10.0" @@ -766,6 +878,19 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "ryu" version = "1.0.18" @@ -836,6 +961,12 @@ dependencies = [ "btoi", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "2.0.68" @@ -847,6 +978,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "terminal_size" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" +dependencies = [ + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "termtree" version = "0.4.1" @@ -978,6 +1119,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "vcpkg" version = "0.2.15" @@ -1079,7 +1226,7 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -1088,7 +1235,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets", + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", ] [[package]] @@ -1097,7 +1253,22 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -1106,28 +1277,46 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.5" @@ -1140,24 +1329,48 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.5" diff --git a/Cargo.toml b/Cargo.toml index 66ab64435..781441234 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ name = "pabi" readme = "README.md" repository = "https://github.com/kirillbobyrev/pabi" rust-version = "1.79" -version = "2024.6.16" +version = "2024.6.27" include = [ "/src/", "/generated/", @@ -63,7 +63,11 @@ perf = "deny" anyhow = "1.0.83" arrayvec = "0.7.2" bitflags = "2.2.1" +clap = { version = "4.5.7", features = ["derive", "wrap_help"] } itertools = "0.13.0" +# Use SmallRng for performance. +rand = { version = "0.8.5", features = ["small_rng"] } +rand_distr = "0.4.3" shadow-rs = "0.29.0" [build-dependencies] diff --git a/README.md b/README.md index 66de72ed5..32eeabff0 100644 --- a/README.md +++ b/README.md @@ -94,24 +94,24 @@ See [justfile](/justfile) for a complete list of frequently used commands. Rustdoc developer documentation is pushed at each commit to . -##### [`src/chess/`](/src/chess/) +#### [`src/chess/`](/src/chess/) Contains implementation of the chess environment and rules: [Bitboard]-based board representation, move generation, [Zobrist hashing]. This is the core of the engine: a fast move generator and convenient board implementation are crucial for engine's performance. -##### [`src/evaluation/`](/src/evaluation/) +#### [`src/evaluation/`](/src/evaluation/) Contains code that extracts features from a given position and runs "static" [position evaluation]: a Neural Network that considers just a single position and predicts how good the position is for the player to move. -##### [`src/search/`](/src/search/) +#### [`src/search/`](/src/search/) Implements Monte Carlo Tree Search ([MCTS]) and its extensions. -##### [`src/engine/`](/src/engine/) +#### [`src/engine/`](/src/engine/) Assembles all pieces together and manages resources to search effieciently under given time constraints. It also communicates back and forth with the tournament @@ -163,6 +163,7 @@ fuzzers: writing and running them is highly encouraged. [Rust]: https://www.rust-lang.org/ [Stable]: https://github.com/kirillbobyrev/pabi/milestone/2 [Strong]: https://github.com/kirillbobyrev/pabi/milestone/3 +[TCEC rules]: https://wiki.chessdom.org/Rules [TCEC]: https://tcec-chess.com/ [Universal Chess Interface]: http://wbec-ridderkerk.nl/html/UCIProtocol.html [Zobrist hashing]: https://www.chessprogramming.org/Zobrist_Hashing diff --git a/justfile b/justfile index 99671e4ab..b758d655b 100644 --- a/justfile +++ b/justfile @@ -1,5 +1,5 @@ # This is a collection of commonly used recipes for development. Most of them -# are wrappers around Cargo and Cargo extensions with certain setup. +# are wrappers around Cargo with the right flags and settings. build: cargo build --profile=release @@ -21,7 +21,7 @@ fix: cargo +nightly fmt --all cargo +nightly clippy --all-targets --all-features --fix --allow-staged -# Run most tests in debug mode to (potentially) cat more errors with +# Run most tests in debug mode to (potentially) catch more errors with # debug_assert. test: cargo test @@ -48,8 +48,3 @@ fuzz target: # Build developer documentation. doc: cargo doc --document-private-items --no-deps - -# Play a single game between two engine versions in 2'+1'' format and save the -# game PGN. -play engine1_cmd engine2_cmd outfile: - cutechess-cli -engine cmd={{ engine1_cmd }} -engine cmd={{ engine2_cmd }} -each proto=uci tc=120+1 -pgnout file min diff --git a/src/main.rs b/src/bin/pabi.rs similarity index 83% rename from src/main.rs rename to src/bin/pabi.rs index 99361acc5..2ac93e5ae 100644 --- a/src/main.rs +++ b/src/bin/pabi.rs @@ -1,10 +1,12 @@ +//! The main entry point for the UCI engine binary. + use std::env; fn main() -> anyhow::Result<()> { let args: Vec = env::args().collect(); if args.len() == 2 && args[1] == "bench" { - pabi::mcts::openbench(); + pabi::engine::openbench(); return Ok(()); } diff --git a/src/chess/attacks.rs b/src/chess/attacks.rs index 1eeb9073a..f65608fc6 100644 --- a/src/chess/attacks.rs +++ b/src/chess/attacks.rs @@ -410,7 +410,7 @@ mod tests { #[test] fn pawn() { // Pawns can not be on the back ranks, hence the attack maps are empty. - for square in Rank::One.mask().iter().chain(Rank::Eight.mask().iter()) { + for square in Rank::Rank1.mask().iter().chain(Rank::Rank8.mask().iter()) { assert!(pawn_attacks(square, Color::White).is_empty()); assert!(pawn_attacks(square, Color::Black).is_empty()); } diff --git a/src/chess/bitboard.rs b/src/chess/bitboard.rs index 426b7d116..ff1e9ea0d 100644 --- a/src/chess/bitboard.rs +++ b/src/chess/bitboard.rs @@ -458,10 +458,10 @@ mod tests { assert_eq!(black.queens.bits, 1 << (3 + 8 * 7)); // Rank masks. - assert_eq!(Rank::One.mask() << u32::from(BOARD_WIDTH), Rank::Two.mask()); + assert_eq!(Rank::Rank1.mask() << u32::from(BOARD_WIDTH), Rank::Rank2.mask()); assert_eq!( - Rank::Five.mask() >> u32::from(BOARD_WIDTH), - Rank::Four.mask() + Rank::Rank5.mask() >> u32::from(BOARD_WIDTH), + Rank::Rank4.mask() ); } diff --git a/src/chess/core.rs b/src/chess/core.rs index 17d339b75..a9217c035 100644 --- a/src/chess/core.rs +++ b/src/chess/core.rs @@ -20,22 +20,46 @@ pub const BOARD_SIZE: u8 = BOARD_WIDTH * BOARD_WIDTH; /// representation. The moves can also be indexed and fed as an input to the /// Neural Network evaluators that would be able assess their potential without /// evaluating post-states. -// TODO: Compress the representation to reduce memory footprint. #[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct Move { - pub(super) from: Square, - pub(super) to: Square, - pub(super) promotion: Option, -} +pub struct Move(u16); impl Move { + // First 6 bits are reserved for the `from` square. + const FROM_MASK: u16 = 0b0000_0000_0011_1111; + + // Next 6 bits are reserved for the `to` square. + const TO_OFFSET: u8 = 6; + const TO_MASK: u16 = 0b0000_1111_1100_0000; + + // Next 3 bits are reserved for the promotion (if any). + const PROMOTION_MASK: u16 = 0b0111_0000_0000_0000; + const PROMOTION_OFFSET: u8 = 12; + #[must_use] - pub(super) const fn new(from: Square, to: Square, promotion: Option) -> Self { - Self { - from, - to, - promotion, + pub(super) fn new(from: Square, to: Square, promotion: Option) -> Self { + let mut packed = from as u16 | ((to as u16) << Self::TO_OFFSET); + if let Some(promo) = promotion { + packed |= (promo as u16) << Self::PROMOTION_OFFSET; } + Self(packed) + } + + #[must_use] + pub(super) fn from(&self) -> Square { + let square = self.0 & Self::FROM_MASK; + Square::try_from(square as u8).unwrap() + } + + #[must_use] + pub(super) fn to(&self) -> Square { + let square = (self.0 & Self::TO_MASK) >> Self::TO_OFFSET; + Square::try_from(square as u8).unwrap() + } + + #[must_use] + pub(super) fn promotion(&self) -> Option { + let promo = (self.0 & Self::PROMOTION_MASK) >> Self::PROMOTION_OFFSET; + unsafe { std::mem::transmute(promo as u8) } } /// Converts the move from UCI format to the internal representation. This @@ -44,6 +68,11 @@ impl Move { pub fn from_uci(uci: &str) -> anyhow::Result { Self::try_from(uci) } + + #[must_use] + pub(super) fn as_packed_int(&self) -> u16 { + self.0 + } } impl TryFrom<&str> for Move { @@ -69,8 +98,8 @@ impl TryFrom<&str> for Move { impl fmt::Display for Move { /// Serializes a move in UCI format (used by [`pabi::uci`]). fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}{}", self.from, self.to)?; - if let Some(promotion) = self.promotion { + write!(f, "{}{}", self.from(), self.to())?; + if let Some(promotion) = self.promotion() { write!(f, "{}", PieceKind::from(promotion))?; } Ok(()) @@ -300,14 +329,14 @@ impl TryFrom for File { #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] #[allow(missing_docs)] pub enum Rank { - One, - Two, - Three, - Four, - Five, - Six, - Seven, - Eight, + Rank1, + Rank2, + Rank3, + Rank4, + Rank5, + Rank6, + Rank7, + Rank8, } impl Rank { @@ -315,28 +344,28 @@ impl Rank { /// given rank. pub(super) const fn mask(self) -> Bitboard { match self { - Self::One => Bitboard::from_bits(0x0000_0000_0000_00FF), - Self::Two => Bitboard::from_bits(0x0000_0000_0000_FF00), - Self::Three => Bitboard::from_bits(0x0000_0000_00FF_0000), - Self::Four => Bitboard::from_bits(0x0000_0000_FF00_0000), - Self::Five => Bitboard::from_bits(0x0000_00FF_0000_0000), - Self::Six => Bitboard::from_bits(0x0000_FF00_0000_0000), - Self::Seven => Bitboard::from_bits(0x00FF_0000_0000_0000), - Self::Eight => Bitboard::from_bits(0xFF00_0000_0000_0000), + Self::Rank1 => Bitboard::from_bits(0x0000_0000_0000_00FF), + Self::Rank2 => Bitboard::from_bits(0x0000_0000_0000_FF00), + Self::Rank3 => Bitboard::from_bits(0x0000_0000_00FF_0000), + Self::Rank4 => Bitboard::from_bits(0x0000_0000_FF00_0000), + Self::Rank5 => Bitboard::from_bits(0x0000_00FF_0000_0000), + Self::Rank6 => Bitboard::from_bits(0x0000_FF00_0000_0000), + Self::Rank7 => Bitboard::from_bits(0x00FF_0000_0000_0000), + Self::Rank8 => Bitboard::from_bits(0xFF00_0000_0000_0000), } } pub(super) const fn backrank(color: Color) -> Self { match color { - Color::White => Self::One, - Color::Black => Self::Eight, + Color::White => Self::Rank1, + Color::Black => Self::Rank8, } } pub(super) const fn pawns_starting(color: Color) -> Self { match color { - Color::White => Self::Two, - Color::Black => Self::Seven, + Color::White => Self::Rank2, + Color::Black => Self::Rank7, } } } @@ -427,21 +456,21 @@ impl fmt::Display for Color { #[allow(missing_docs)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd)] pub enum PieceKind { - King, - Queen, - Rook, - Bishop, - Knight, Pawn, + Knight, + Bishop, + Rook, + Queen, + King, } impl From for PieceKind { fn from(promotion: Promotion) -> Self { match promotion { - Promotion::Queen => Self::Queen, - Promotion::Rook => Self::Rook, - Promotion::Bishop => Self::Bishop, Promotion::Knight => Self::Knight, + Promotion::Bishop => Self::Bishop, + Promotion::Rook => Self::Rook, + Promotion::Queen => Self::Queen, } } } @@ -449,12 +478,12 @@ impl From for PieceKind { impl fmt::Display for PieceKind { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_char(match &self { - Self::King => 'k', - Self::Queen => 'q', - Self::Rook => 'r', - Self::Bishop => 'b', - Self::Knight => 'n', Self::Pawn => 'p', + Self::Knight => 'n', + Self::Bishop => 'b', + Self::Rook => 'r', + Self::Queen => 'q', + Self::King => 'k', }) } } @@ -462,7 +491,7 @@ impl fmt::Display for PieceKind { /// Represents a specific piece owned by a player. pub struct Piece { #[allow(missing_docs)] - pub owner: Color, + pub color: Color, #[allow(missing_docs)] pub kind: PieceKind, } @@ -470,7 +499,7 @@ pub struct Piece { impl Piece { #[must_use] pub const fn plane(&self) -> usize { - self.owner as usize * 6 + self.kind as usize + self.color as usize * 6 + self.kind as usize } } @@ -479,76 +508,76 @@ impl TryFrom for Piece { fn try_from(symbol: char) -> anyhow::Result { match symbol { - 'K' => Ok(Self { - owner: Color::White, - kind: PieceKind::King, - }), - 'Q' => Ok(Self { - owner: Color::White, - kind: PieceKind::Queen, + 'P' => Ok(Self { + color: Color::White, + kind: PieceKind::Pawn, }), - 'R' => Ok(Self { - owner: Color::White, - kind: PieceKind::Rook, + 'N' => Ok(Self { + color: Color::White, + kind: PieceKind::Knight, }), 'B' => Ok(Self { - owner: Color::White, + color: Color::White, kind: PieceKind::Bishop, }), - 'N' => Ok(Self { - owner: Color::White, - kind: PieceKind::Knight, + 'R' => Ok(Self { + color: Color::White, + kind: PieceKind::Rook, }), - 'P' => Ok(Self { - owner: Color::White, - kind: PieceKind::Pawn, + 'Q' => Ok(Self { + color: Color::White, + kind: PieceKind::Queen, }), - 'k' => Ok(Self { - owner: Color::Black, + 'K' => Ok(Self { + color: Color::White, kind: PieceKind::King, }), - 'q' => Ok(Self { - owner: Color::Black, - kind: PieceKind::Queen, + 'p' => Ok(Self { + color: Color::Black, + kind: PieceKind::Pawn, }), - 'r' => Ok(Self { - owner: Color::Black, - kind: PieceKind::Rook, + 'n' => Ok(Self { + color: Color::Black, + kind: PieceKind::Knight, }), 'b' => Ok(Self { - owner: Color::Black, + color: Color::Black, kind: PieceKind::Bishop, }), - 'n' => Ok(Self { - owner: Color::Black, - kind: PieceKind::Knight, + 'r' => Ok(Self { + color: Color::Black, + kind: PieceKind::Rook, }), - 'p' => Ok(Self { - owner: Color::Black, - kind: PieceKind::Pawn, + 'k' => Ok(Self { + color: Color::Black, + kind: PieceKind::King, }), - _ => bail!("piece symbol should be within \"KQRBNPkqrbnp\", got '{symbol}'"), + 'q' => Ok(Self { + color: Color::Black, + kind: PieceKind::Queen, + }), + _ => bail!("piece symbol should be in \"PNBRQKpnbrqk\", got '{symbol}'"), } } } impl fmt::Display for Piece { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_char(match (&self.owner, &self.kind) { + f.write_char(match (&self.color, &self.kind) { // White: uppercase symbols. - (Color::White, PieceKind::King) => 'K', - (Color::White, PieceKind::Queen) => 'Q', - (Color::White, PieceKind::Rook) => 'R', - (Color::White, PieceKind::Bishop) => 'B', - (Color::White, PieceKind::Knight) => 'N', (Color::White, PieceKind::Pawn) => 'P', + (Color::White, PieceKind::Knight) => 'N', + (Color::White, PieceKind::Bishop) => 'B', + (Color::White, PieceKind::Rook) => 'R', + (Color::White, PieceKind::Queen) => 'Q', + (Color::White, PieceKind::King) => 'K', // Black: lowercase symbols. - (Color::Black, PieceKind::King) => 'k', - (Color::Black, PieceKind::Queen) => 'q', - (Color::Black, PieceKind::Rook) => 'r', - (Color::Black, PieceKind::Bishop) => 'b', - (Color::Black, PieceKind::Knight) => 'n', (Color::Black, PieceKind::Pawn) => 'p', + (Color::Black, PieceKind::Knight) => 'n', + (Color::Black, PieceKind::Bishop) => 'b', + (Color::Black, PieceKind::Rook) => 'r', + (Color::Black, PieceKind::Queen) => 'q', + (Color::Black, PieceKind::King) => 'k', }) } } @@ -683,20 +712,20 @@ impl fmt::Display for CastleRights { #[allow(missing_docs)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd)] pub(crate) enum Promotion { - Queen, - Rook, - Bishop, - Knight, + Knight = 1, + Bishop = 2, + Rook = 3, + Queen = 4, } impl From for Promotion { fn from(c: char) -> Self { match c { - 'q' => Self::Queen, - 'r' => Self::Rook, - 'b' => Self::Bishop, 'n' => Self::Knight, - _ => unreachable!("unknown promotion piece, has to be in 'qrbn': {c}"), + 'b' => Self::Bishop, + 'r' => Self::Rook, + 'q' => Self::Queen, + _ => unreachable!("unknown promotion piece, has to be in 'kbrq': {c}"), } } } @@ -738,14 +767,14 @@ mod tests { .filter_map(|ch| Rank::try_from(ch).ok()) .collect::>(), vec![ - Rank::One, - Rank::Two, - Rank::Three, - Rank::Four, - Rank::Five, - Rank::Six, - Rank::Seven, - Rank::Eight, + Rank::Rank1, + Rank::Rank2, + Rank::Rank3, + Rank::Rank4, + Rank::Rank5, + Rank::Rank6, + Rank::Rank7, + Rank::Rank8, ] ); assert_eq!( @@ -753,14 +782,14 @@ mod tests { .filter_map(|idx| Rank::try_from(idx).ok()) .collect::>(), vec![ - Rank::One, - Rank::Two, - Rank::Three, - Rank::Four, - Rank::Five, - Rank::Six, - Rank::Seven, - Rank::Eight, + Rank::Rank1, + Rank::Rank2, + Rank::Rank3, + Rank::Rank4, + Rank::Rank5, + Rank::Rank6, + Rank::Rank7, + Rank::Rank8, ] ); } @@ -847,10 +876,10 @@ mod tests { vec![Square::A1, Square::H8, Square::H1, Square::A2, Square::F3,] ); let squares: Vec<_> = [ - (File::B, Rank::Three), - (File::F, Rank::Five), - (File::H, Rank::Eight), - (File::E, Rank::Four), + (File::B, Rank::Rank3), + (File::F, Rank::Rank5), + (File::H, Rank::Rank8), + (File::E, Rank::Rank4), ] .iter() .map(|(file, rank)| Square::new(*file, *rank)) diff --git a/src/chess/generated.rs b/src/chess/generated.rs index c406382ef..ef6f9603e 100644 --- a/src/chess/generated.rs +++ b/src/chess/generated.rs @@ -19,7 +19,7 @@ const PIECES_ZOBRIST_KEYS: [Key; 768] = include!(concat!(env!("OUT_DIR"), "/piec pub(super) fn get_piece_key(piece: Piece, square: Square) -> Key { const NUM_PIECES: usize = 6; - PIECES_ZOBRIST_KEYS[piece.owner as usize * NUM_PIECES * BOARD_SIZE as usize + PIECES_ZOBRIST_KEYS[piece.color as usize * NUM_PIECES * BOARD_SIZE as usize + piece.kind as usize * BOARD_SIZE as usize + square as usize] } diff --git a/src/chess/mod.rs b/src/chess/mod.rs index 837de7b1e..211300788 100644 --- a/src/chess/mod.rs +++ b/src/chess/mod.rs @@ -5,6 +5,7 @@ pub mod attacks; pub mod bitboard; pub mod core; pub mod position; +pub mod state; pub mod zobrist; mod generated; diff --git a/src/chess/position.rs b/src/chess/position.rs index fceea0da8..71e8e6464 100644 --- a/src/chess/position.rs +++ b/src/chess/position.rs @@ -13,7 +13,7 @@ use anyhow::{bail, Context}; use crate::chess::bitboard::{Bitboard, Pieces}; use crate::chess::core::{ - CastleRights, File, Move, MoveList, Piece, Color, Promotion, Rank, Square, BOARD_WIDTH, + CastleRights, Color, File, Move, MoveList, Piece, Promotion, Rank, Square, BOARD_WIDTH, }; use crate::chess::{attacks, generated, zobrist}; @@ -100,7 +100,7 @@ impl Position { self.us().opponent() } - pub(crate) fn pieces(&self, color : Color) -> &Pieces { + pub(crate) fn pieces(&self, color: Color) -> &Pieces { match color { Color::White => &self.white_pieces, Color::Black => &self.black_pieces, @@ -185,7 +185,7 @@ impl Position { } match Piece::try_from(symbol) { Ok(piece) => { - let pieces = match piece.owner { + let pieces = match piece.color { Color::White => &mut white_pieces, Color::Black => &mut black_pieces, }; @@ -439,36 +439,36 @@ impl Position { fn update_castling_rights(&mut self, next_move: &Move) { if self.castling.contains(CastleRights::WHITE_SHORT) { - if next_move.from == Square::E1 - || next_move.from == Square::H1 - || next_move.to == Square::H1 + if next_move.from() == Square::E1 + || next_move.from() == Square::H1 + || next_move.to() == Square::H1 { self.castling.remove(CastleRights::WHITE_SHORT); self.hash ^= generated::WHITE_CAN_CASTLE_SHORT; } } if self.castling.contains(CastleRights::WHITE_LONG) { - if next_move.from == Square::E1 - || next_move.from == Square::A1 - || next_move.to == Square::A1 + if next_move.from() == Square::E1 + || next_move.from() == Square::A1 + || next_move.to() == Square::A1 { self.castling.remove(CastleRights::WHITE_LONG); self.hash ^= generated::WHITE_CAN_CASTLE_LONG; } } if self.castling.contains(CastleRights::BLACK_SHORT) { - if next_move.from == Square::E8 - || next_move.from == Square::H8 - || next_move.to == Square::H8 + if next_move.from() == Square::E8 + || next_move.from() == Square::H8 + || next_move.to() == Square::H8 { self.castling.remove(CastleRights::BLACK_SHORT); self.hash ^= generated::BLACK_CAN_CASTLE_SHORT; } } if self.castling.contains(CastleRights::BLACK_LONG) { - if next_move.from == Square::E8 - || next_move.from == Square::A8 - || next_move.to == Square::A8 + if next_move.from() == Square::E8 + || next_move.from() == Square::A8 + || next_move.to() == Square::A8 { self.castling.remove(CastleRights::BLACK_LONG); self.hash ^= generated::BLACK_CAN_CASTLE_LONG; @@ -483,11 +483,11 @@ impl Position { }; // TODO: Update the hash. - if their_pieces.all().contains(next_move.to) { + if their_pieces.all().contains(next_move.to()) { // Capturing a piece resets the clock. self.halfmove_clock = 0; - let square = next_move.to; + let square = next_move.to(); for (piece, kind) in [ (&mut their_pieces.queens, PieceKind::Queen), @@ -500,7 +500,7 @@ impl Position { piece.clear(square); self.hash ^= generated::get_piece_key( Piece { - owner: self.side_to_move.opponent(), + color: self.side_to_move.opponent(), kind, }, square, @@ -520,7 +520,7 @@ impl Position { let previous_en_passant = self.en_passant_square; self.en_passant_square = None; - if !our_pieces.pawns.contains(next_move.from) { + if !our_pieces.pawns.contains(next_move.from()) { return false; } @@ -529,12 +529,12 @@ impl Position { // Check en passant. if let Some(en_passant_square) = previous_en_passant { - if next_move.to == en_passant_square { - let captured_pawn = Square::new(next_move.to.file(), next_move.from.rank()); + if next_move.to() == en_passant_square { + let captured_pawn = Square::new(next_move.to().file(), next_move.from().rank()); their_pieces.pawns.clear(captured_pawn); self.hash ^= generated::get_piece_key( Piece { - owner: self.side_to_move.opponent(), + color: self.side_to_move.opponent(), kind: PieceKind::Pawn, }, captured_pawn, @@ -542,81 +542,81 @@ impl Position { } } - our_pieces.pawns.clear(next_move.from); + our_pieces.pawns.clear(next_move.from()); self.hash ^= generated::get_piece_key( Piece { - owner: self.side_to_move, + color: self.side_to_move, kind: PieceKind::Pawn, }, - next_move.from, + next_move.from(), ); // Check promotions. // TODO: Debug assertions to make sure the promotion is valid. - if let Some(promotion) = next_move.promotion { + if let Some(promotion) = next_move.promotion() { match promotion { Promotion::Queen => { - our_pieces.queens.extend(next_move.to); + our_pieces.queens.extend(next_move.to()); self.hash ^= generated::get_piece_key( Piece { - owner: self.side_to_move, + color: self.side_to_move, kind: PieceKind::Queen, }, - next_move.to, + next_move.to(), ); }, Promotion::Rook => { - our_pieces.rooks.extend(next_move.to); + our_pieces.rooks.extend(next_move.to()); self.hash ^= generated::get_piece_key( Piece { - owner: self.side_to_move, + color: self.side_to_move, kind: PieceKind::Rook, }, - next_move.to, + next_move.to(), ); }, Promotion::Bishop => { - our_pieces.bishops.extend(next_move.to); + our_pieces.bishops.extend(next_move.to()); self.hash ^= generated::get_piece_key( Piece { - owner: self.side_to_move, + color: self.side_to_move, kind: PieceKind::Bishop, }, - next_move.to, + next_move.to(), ); }, Promotion::Knight => { - our_pieces.knights.extend(next_move.to); + our_pieces.knights.extend(next_move.to()); self.hash ^= generated::get_piece_key( Piece { - owner: self.side_to_move, + color: self.side_to_move, kind: PieceKind::Knight, }, - next_move.to, + next_move.to(), ); }, }; return true; } - our_pieces.pawns.extend(next_move.to); + our_pieces.pawns.extend(next_move.to()); self.hash ^= generated::get_piece_key( Piece { - owner: self.side_to_move, + color: self.side_to_move, kind: PieceKind::Pawn, }, - next_move.to, + next_move.to(), ); let single_push_square = next_move - .from + .from() .shift(self.side_to_move.pawn_push_direction()) .unwrap(); // Double push creates en passant square. - if next_move.from.rank() == Rank::pawns_starting(self.side_to_move) - && next_move.from.file() == next_move.to.file() - && single_push_square != next_move.to + if next_move.from().rank() == Rank::pawns_starting(self.side_to_move) + && next_move.from().file() == next_move.to().file() + && single_push_square != next_move.to() // Technically, this is not correct: https://github.com/jhlywa/chess.js/issues/294 && (their_pieces.pawns & attacks::pawn_attacks(single_push_square, self.side_to_move)).has_any() { @@ -635,23 +635,23 @@ impl Position { Color::Black => &mut self.black_pieces, }; - if !our_pieces.king.contains(next_move.from) { + if !our_pieces.king.contains(next_move.from()) { return false; } let backrank = Rank::backrank(self.side_to_move); // Check if the move is castling. - if next_move.from.rank() == backrank - && next_move.to.rank() == backrank - && next_move.from.file() == File::E + if next_move.from().rank() == backrank + && next_move.to().rank() == backrank + && next_move.from().file() == File::E { - if next_move.to.file() == File::G { + if next_move.to().file() == File::G { let from = Square::new(File::H, backrank); our_pieces.rooks.clear(from); self.hash ^= generated::get_piece_key( Piece { - owner: self.side_to_move, + color: self.side_to_move, kind: PieceKind::Rook, }, from, @@ -660,17 +660,17 @@ impl Position { our_pieces.rooks.extend(to); self.hash ^= generated::get_piece_key( Piece { - owner: self.side_to_move, + color: self.side_to_move, kind: PieceKind::Rook, }, to, ); - } else if next_move.to.file() == File::C { + } else if next_move.to().file() == File::C { let from = Square::new(File::A, backrank); our_pieces.rooks.clear(from); self.hash ^= generated::get_piece_key( Piece { - owner: self.side_to_move, + color: self.side_to_move, kind: PieceKind::Rook, }, from, @@ -679,7 +679,7 @@ impl Position { our_pieces.rooks.extend(to); self.hash ^= generated::get_piece_key( Piece { - owner: self.side_to_move, + color: self.side_to_move, kind: PieceKind::Rook, }, to, @@ -687,21 +687,21 @@ impl Position { } } - our_pieces.king.clear(next_move.from); + our_pieces.king.clear(next_move.from()); self.hash ^= generated::get_piece_key( Piece { - owner: self.side_to_move, + color: self.side_to_move, kind: PieceKind::King, }, - next_move.from, + next_move.from(), ); - our_pieces.king.extend(next_move.to); + our_pieces.king.extend(next_move.to()); self.hash ^= generated::get_piece_key( Piece { - owner: self.side_to_move, + color: self.side_to_move, kind: PieceKind::King, }, - next_move.to, + next_move.to(), ); true @@ -719,73 +719,52 @@ impl Position { (&mut our_pieces.bishops, PieceKind::Bishop), (&mut our_pieces.knights, PieceKind::Knight), ] { - if bitboard.contains(next_move.from) { - bitboard.clear(next_move.from); + if bitboard.contains(next_move.from()) { + bitboard.clear(next_move.from()); self.hash ^= generated::get_piece_key( Piece { - owner: self.side_to_move, + color: self.side_to_move, kind, }, - next_move.from, + next_move.from(), ); - bitboard.extend(next_move.to); + bitboard.extend(next_move.to()); self.hash ^= generated::get_piece_key( Piece { - owner: self.side_to_move, + color: self.side_to_move, kind, }, - next_move.to, + next_move.to(), ); return; } } } - /// Returns true if the player to move is in check. + // TODO: Figuring out if the king is in check is relatively easy: try all + // attacks on the king square. #[must_use] pub fn in_check(&self) -> bool { - // TODO: This is very expensive and is likely to be a bottleneck. - // Cache the attack info and/or whether the king is in check. - self.attack_info().checkers.has_any() + todo!() } - /// Returns true if the game is over. + /// Returns true if 50-move rule draw is in effect. #[must_use] - pub fn is_checkmate(&self) -> bool { - self.in_check() && self.generate_moves().is_empty() + pub fn halfmove_clock_expired(&self) -> bool { + self.halfmove_clock >= 100 } - /// Returns true if the player to move has no legal moves and is not - /// checkmated (i.e. the game is a draw) or if 50-move rule is in effect. - /// - /// Note that because position does not keep track of the 3-fold repetition - /// it is not taken into account. - #[must_use] - pub fn is_draw_on_board(&self) -> bool { - self.halfmove_clock >= 100 || (!self.in_check() && self.generate_moves().is_empty()) - } - - // #[must_use] - // pub fn is_capture(&self, next_move: &Move) -> bool { - // todo!() - // } - - // #[must_use] - // pub fn gives_check(&self, next_move: &Move) -> bool { - // todo!() - // } - #[must_use] pub(crate) fn at(&self, square: Square) -> Option { if let Some(kind) = self.white_pieces.at(square) { return Some(Piece { - owner: Color::White, + color: Color::White, kind, }); } if let Some(kind) = self.black_pieces.at(square) { return Some(Piece { - owner: Color::Black, + color: Color::Black, kind, }); } @@ -868,7 +847,7 @@ impl fmt::Display for Position { if empty_squares != 0 { write!(f, "{empty_squares}")?; } - if rank != Rank::One { + if rank != Rank::Rank1 { const RANK_SEPARATOR: char = '/'; write!(f, "{RANK_SEPARATOR}")?; } @@ -986,7 +965,7 @@ fn validate(position: &Position) -> anyhow::Result<()> { ) } if ((position.white_pieces.pawns | position.black_pieces.pawns) - & (Rank::One.mask() | Rank::Eight.mask())) + & (Rank::Rank1.mask() | Rank::Rank8.mask())) .has_any() { bail!("pawns can not be placed on backranks") @@ -998,8 +977,8 @@ fn validate(position: &Position) -> anyhow::Result<()> { } if let Some(en_passant_square) = position.en_passant_square { let expected_rank = match position.side_to_move { - Color::White => Rank::Six, - Color::Black => Rank::Three, + Color::White => Rank::Rank6, + Color::Black => Rank::Rank3, }; if en_passant_square.rank() != expected_rank { bail!( @@ -1153,7 +1132,7 @@ fn generate_pawn_moves( continue; } match to.rank() { - Rank::One | Rank::Eight => unsafe { + Rank::Rank1 | Rank::Rank8 => unsafe { moves.push_unchecked(Move::new(from, to, Some(Promotion::Queen))); moves.push_unchecked(Move::new(from, to, Some(Promotion::Rook))); moves.push_unchecked(Move::new(from, to, Some(Promotion::Bishop))); @@ -1207,7 +1186,7 @@ fn generate_pawn_moves( // TODO: This is probably better with self.side_to_move.opponent().backrank() // but might be slower. match to.rank() { - Rank::Eight | Rank::One => unsafe { + Rank::Rank8 | Rank::Rank1 => unsafe { moves.push_unchecked(Move::new(from, to, Some(Promotion::Queen))); moves.push_unchecked(Move::new(from, to, Some(Promotion::Rook))); moves.push_unchecked(Move::new(from, to, Some(Promotion::Bishop))); @@ -1339,11 +1318,11 @@ mod tests { ); assert_eq!( position.white_pieces.all() | position.black_pieces.all(), - Rank::One.mask() | Rank::Two.mask() | Rank::Seven.mask() | Rank::Eight.mask() + Rank::Rank1.mask() | Rank::Rank2.mask() | Rank::Rank7.mask() | Rank::Rank8.mask() ); assert_eq!( !(position.white_pieces.all() | position.black_pieces.all()), - Rank::Three.mask() | Rank::Four.mask() | Rank::Five.mask() | Rank::Six.mask() + Rank::Rank3.mask() | Rank::Rank4.mask() | Rank::Rank5.mask() | Rank::Rank6.mask() ); } } diff --git a/src/mcts/state.rs b/src/chess/state.rs similarity index 74% rename from src/mcts/state.rs rename to src/chess/state.rs index b33a26e46..e9b004e9d 100644 --- a/src/mcts/state.rs +++ b/src/chess/state.rs @@ -6,8 +6,6 @@ use crate::chess::zobrist::RepetitionTable; pub(super) struct State { position_history: ArrayVec, repetitions: RepetitionTable, - searched_nodes: u64, - // TODO: num_pruned for debugging } impl State { @@ -21,7 +19,6 @@ impl State { Self { position_history, repetitions, - searched_nodes: 1, } } @@ -29,30 +26,15 @@ impl State { pub(super) fn push(&mut self, position: Position) -> bool { let draw = self.repetitions.record(position.hash()); self.position_history.push(position); - self.searched_nodes += 1; draw } - pub(super) fn pop(&mut self) { - debug_assert!(!self.position_history.is_empty()); - debug_assert!(!self.repetitions.is_empty()); - - self.repetitions - .remove(self.position_history.last().unwrap().hash()); - self.position_history.pop(); - } - #[must_use] pub(super) fn last(&self) -> &Position { debug_assert!(!self.position_history.is_empty()); self.position_history.last().unwrap() } - #[must_use] - pub(super) fn searched_nodes(&self) -> u64 { - self.searched_nodes - } - /// Returns the number of full moves since the start of the search. #[must_use] pub(super) fn moves(&self) -> u8 { @@ -77,31 +59,24 @@ mod tests { #[test] fn detect_repetition() { let mut state = State::new(Position::starting()); - assert_eq!(state.searched_nodes(), 1); assert_eq!(state.moves(), 0); let mut position = Position::starting(); position.make_move(&Move::from_uci("e2e4").unwrap()); assert!(!state.push(position.clone())); - assert_eq!(state.searched_nodes(), 2); assert_eq!(state.moves(), 1); assert!(!state.push(position.clone())); - assert_eq!(state.searched_nodes(), 3); assert_eq!(state.moves(), 1); // 3-fold "repetition" (the same position was pushed multiple times). assert!(state.push(position.clone())); - assert_eq!(state.searched_nodes(), 4); assert_eq!(state.moves(), 2); position.make_move(&Move::from_uci("e7e5").unwrap()); // Next move is not a repetition. assert!(!state.push(position.clone())); - assert_eq!(state.searched_nodes(), 5); assert_eq!(state.moves(), 2); - - state.pop(); } } diff --git a/src/chess/zobrist.rs b/src/chess/zobrist.rs index d76916708..437d7e176 100644 --- a/src/chess/zobrist.rs +++ b/src/chess/zobrist.rs @@ -6,8 +6,6 @@ use std::collections::HashMap; /// created and updated whenever a move is made. pub type Key = u64; -// TODO: Maybe switch to a more efficient implementation, e.g. this is what -// Stockfish does: https://web.archive.org/web/20201107002606/https://marcelk.net/2013-04-06/paper/upcoming-rep-v2.pdf pub(crate) struct RepetitionTable { table: HashMap, } @@ -42,22 +40,6 @@ impl RepetitionTable { *count += 1; *count == 3 } - - /// Reduces the number of times a position has occurred by one. - pub(crate) fn remove(&mut self, key: Key) { - let count = self.table.entry(key).or_insert(0); - match count { - 1 => { - let _ = self.table.remove_entry(&key); - }, - 2 | 3 => { - *count -= 1; - }, - _ => { - unreachable!("can not call remove on position that has not occurred yet") - }, - } - } } #[cfg(test)] @@ -66,35 +48,35 @@ mod tests { use crate::chess::core::Move; use crate::chess::position::Position; - // #[test] - // fn repetition_table() { - // let mut table = RepetitionTable::new(); + #[test] + fn repetition_table() { + let mut table = RepetitionTable::new(); - // let mut position = Position::starting(); - // let initial_hash = position.hash(); - // assert!(!table.record(initial_hash)); + let mut position = Position::starting(); + let initial_hash = position.hash(); + assert!(!table.record(initial_hash)); - // position.make_move(&Move::from_uci("g1f3").expect("valid move")); - // assert_ne!(initial_hash, position.hash()); - // assert!(!table.record(position.hash())); - // position.make_move(&Move::from_uci("g8f6").expect("valid move")); - // assert!(!table.record(position.hash())); + position.make_move(&Move::from_uci("g1f3").expect("valid move")); + assert_ne!(initial_hash, position.hash()); + assert!(!table.record(position.hash())); + position.make_move(&Move::from_uci("g8f6").expect("valid move")); + assert!(!table.record(position.hash())); - // position.make_move(&Move::from_uci("f3g1").expect("valid move")); - // assert!(!table.record(position.hash())); - // // Two-fold repetition. - // position.make_move(&Move::from_uci("f6g8").expect("valid move")); - // assert!(!table.record(position.hash())); + position.make_move(&Move::from_uci("f3g1").expect("valid move")); + assert!(!table.record(position.hash())); + // Two-fold repetition. + position.make_move(&Move::from_uci("f6g8").expect("valid move")); + assert!(!table.record(position.hash())); - // position.make_move(&Move::from_uci("g1f3").expect("valid move")); - // assert!(!table.record(position.hash())); - // position.make_move(&Move::from_uci("g8f6").expect("valid move")); - // assert!(!table.record(position.hash())); + position.make_move(&Move::from_uci("g1f3").expect("valid move")); + assert!(!table.record(position.hash())); + position.make_move(&Move::from_uci("g8f6").expect("valid move")); + assert!(!table.record(position.hash())); - // position.make_move(&Move::from_uci("f3g1").expect("valid move")); - // assert!(!table.record(position.hash())); - // // Three-fold repetition. - // position.make_move(&Move::from_uci("f6g8").expect("valid move")); - // assert!(table.record(position.hash())); - // } + position.make_move(&Move::from_uci("f3g1").expect("valid move")); + assert!(!table.record(position.hash())); + // Three-fold repetition. + position.make_move(&Move::from_uci("f6g8").expect("valid move")); + assert!(table.record(position.hash())); + } } diff --git a/src/engine/mod.rs b/src/engine/mod.rs index d041e08d0..58c50182c 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -11,10 +11,10 @@ use std::time::Duration; use anyhow::bail; -use crate::chess::core::{Move, Color}; +use crate::chess::core::{Color, Move}; use crate::chess::position::Position; use crate::engine::uci::Command; -use crate::mcts::{find_best_move, Depth}; +use crate::mcts::Depth; mod time_manager; mod uci; @@ -99,9 +99,7 @@ impl<'a, R: BufRead, W: Write> Engine<'a, R, W> { binc, movetime, infinite, - } => self.go( - max_depth, wtime, btime, winc, binc, movetime, infinite, - )?, + } => self.go(max_depth, wtime, btime, winc, binc, movetime, infinite)?, Command::Stop => self.stop_search()?, Command::Quit => { self.stop_search()?; @@ -177,9 +175,7 @@ impl<'a, R: BufRead, W: Write> Engine<'a, R, W> { Color::White => (wtime, winc), Color::Black => (btime, binc), }; - let next_move = find_best_move(self.position.clone(), max_depth, time, self.out); - writeln!(self.out, "bestmove {next_move}")?; - Ok(()) + let next_move = todo!(); } /// Stops the search immediately. @@ -191,4 +187,19 @@ impl<'a, R: BufRead, W: Write> Engine<'a, R, W> { } } +/// Runs search on a small set of positions to provide an estimate of engine's +/// performance. +/// +/// Implementing `bench` CLI command is a [requirement for OpenBench]. +/// +/// NOTE: This function **has to run less than 60 seconds**. +/// +/// See for +/// more details. +/// +/// [requirement for OpenBench]: https://github.com/AndyGrant/OpenBench/wiki/Requirements-For-Public-Engines#basic-requirements +pub fn openbench() { + todo!() +} + // TODO: Add extensive test suite for the UCI protocol implementation. diff --git a/src/environment.rs b/src/environment.rs new file mode 100644 index 000000000..0f74a1114 --- /dev/null +++ b/src/environment.rs @@ -0,0 +1,18 @@ +use crate::chess::core::Color; + +/// Result of the game from the perspective of the player to move at root. +enum GameResult { + Win, + Loss, + Draw, +} + +trait Action: Sized { + fn get_index(&self) -> u16; +} + +trait Environment { + fn get_actions(&self) -> Vec; + fn apply(&mut self, action: impl Action); + fn get_result(&self) -> Option; +} diff --git a/src/evaluation/brain.rs b/src/evaluation/brain.rs index 7a64adaa1..e69de29bb 100644 --- a/src/evaluation/brain.rs +++ b/src/evaluation/brain.rs @@ -1,3 +0,0 @@ -//! Inference for [pabi-brain]. -//! -//! [pabi-brain]: https://github.com/kirillbobyrev/pabi-brain diff --git a/src/mcts/environment.rs b/src/mcts/environment.rs deleted file mode 100644 index 5fb704398..000000000 --- a/src/mcts/environment.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub(super) enum State { - Running, - Won, - Lost, - Draw, -} diff --git a/src/mcts/mod.rs b/src/mcts/mod.rs index f896e4082..6708c8823 100644 --- a/src/mcts/mod.rs +++ b/src/mcts/mod.rs @@ -2,59 +2,7 @@ //! //! [Monte Carlo Tree Search]: https://en.wikipedia.org/wiki/Monte_Carlo_tree_search -use std::io::Write; -use std::time::{Duration, Instant}; - -use crate::chess::core::Move; -use crate::chess::position::Position; -use crate::evaluation::QValue; - -mod environment; - -mod state; mod tree; -use state::State; /// Search depth in plies. pub type Depth = u8; - -pub(crate) struct Limiter { - pub(crate) timer: Instant, - pub(crate) depth: Option, - pub(crate) time: Option, -} - -/// Adding reserve time to ensure that the engine does not exceed the time -/// limit. -// TODO: Tweak/tune this. -const RESERVE: Duration = Duration::from_millis(100); - -/// Runs the search algorithm to find the best move under given time -/// constraints. -pub(crate) fn find_best_move( - root: Position, - max_depth: Option, - time: Option, - output: &mut impl Write, -) -> Move { - todo!() -} - -fn find_best_move_and_score(depth: Depth, state: &mut State) -> (Move, QValue) { - todo!() -} - -/// Runs search on a small set of positions to provide an estimate of engine's -/// performance. -/// -/// Implementing `bench` CLI command is a [requirement for OpenBench]. -/// -/// NOTE: This function **has to run less than 60 seconds**. -/// -/// See for -/// more details. -/// -/// [requirement for OpenBench]: https://github.com/AndyGrant/OpenBench/wiki/Requirements-For-Public-Engines#basic-requirements -pub fn openbench() { - todo!() -} diff --git a/src/mcts/parameters.rs b/src/mcts/parameters.rs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/mcts/worker.rs b/src/mcts/worker.rs deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/integration.rs b/tests/integration.rs index 4ade6fada..bafe3842d 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1,6 +1,6 @@ use assert_cmd::Command; use predicates::boolean::PredicateBooleanExt; -use predicates::str::{contains, is_match}; +use predicates::str::contains; const BINARY_NAME: &str = "pabi"; @@ -20,15 +20,15 @@ fn uci_setup() { ); } -#[test] -#[ignore] -fn openbench_output() { - let mut cmd = Command::cargo_bin(BINARY_NAME).expect("Binary should be built"); - let _ = cmd.arg("bench"); +// #[test] +// #[ignore] +// fn openbench_output() { +// let mut cmd = Command::cargo_bin(BINARY_NAME).expect("Binary should be built"); +// let _ = cmd.arg("bench"); - drop( - cmd.assert() - .stdout(is_match(r"^\d+ nodes \d+ nps$").unwrap()) - .success(), - ); -} +// drop( +// cmd.assert() +// .stdout(is_match(r"^\d+ nodes \d+ nps$").unwrap()) +// .success(), +// ); +// }