├── src ├── datagen │ └── mod.rs ├── engine │ ├── time_manager.rs │ ├── mod.rs │ └── uci.rs ├── evaluation │ ├── features.rs │ ├── network.rs │ └── mod.rs ├── search │ ├── policy.rs │ ├── mod.rs │ ├── mcts.rs │ └── tree.rs ├── chess │ ├── mod.rs │ ├── zobrist.rs │ ├── generated.rs │ ├── game.rs │ ├── bitboard.rs │ ├── attacks.rs │ └── core.rs ├── bin │ ├── datagen.rs │ └── pabi.rs ├── lib.rs └── environment.rs ├── tests ├── data │ ├── syzygy │ │ ├── KBvK.rtbw │ │ ├── KBvK.rtbz │ │ ├── KNvK.rtbw │ │ ├── KNvK.rtbz │ │ ├── KPvK.rtbw │ │ ├── KPvK.rtbz │ │ ├── KQvK.rtbw │ │ ├── KQvK.rtbz │ │ ├── KRvK.rtbw │ │ └── KRvK.rtbz │ └── README.md ├── integration.rs └── chess.rs ├── .cargo └── config.toml ├── .github ├── dependabot.yml ├── workflows │ ├── lint.yml │ ├── coverage.yml │ ├── test.yml │ └── build.yml └── copilot-instructions.md ├── fuzz ├── fuzz_targets │ ├── parse_board.rs │ └── generate_moves.rs └── Cargo.toml ├── .gitignore ├── generated ├── bishop_attack_offsets.rs ├── rook_attack_offsets.rs ├── bishop_relevant_occupancies.rs ├── rook_relevant_occupancies.rs ├── king_attacks.rs ├── black_pawn_attacks.rs ├── knight_attacks.rs └── white_pawn_attacks.rs ├── rustfmt.toml ├── Makefile ├── justfile ├── README.md ├── Cargo.toml └── benches └── chess.rs /src/datagen/mod.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/engine/time_manager.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/evaluation/features.rs: -------------------------------------------------------------------------------- 1 | //! Extracts features from the position. 2 | -------------------------------------------------------------------------------- /src/evaluation/network.rs: -------------------------------------------------------------------------------- 1 | //! Policy + Value Neural Network model. 2 | -------------------------------------------------------------------------------- /tests/data/syzygy/KBvK.rtbw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirillbobyrev/pabi/HEAD/tests/data/syzygy/KBvK.rtbw -------------------------------------------------------------------------------- /tests/data/syzygy/KBvK.rtbz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirillbobyrev/pabi/HEAD/tests/data/syzygy/KBvK.rtbz -------------------------------------------------------------------------------- /tests/data/syzygy/KNvK.rtbw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirillbobyrev/pabi/HEAD/tests/data/syzygy/KNvK.rtbw -------------------------------------------------------------------------------- /tests/data/syzygy/KNvK.rtbz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirillbobyrev/pabi/HEAD/tests/data/syzygy/KNvK.rtbz -------------------------------------------------------------------------------- /tests/data/syzygy/KPvK.rtbw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirillbobyrev/pabi/HEAD/tests/data/syzygy/KPvK.rtbw -------------------------------------------------------------------------------- /tests/data/syzygy/KPvK.rtbz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirillbobyrev/pabi/HEAD/tests/data/syzygy/KPvK.rtbz -------------------------------------------------------------------------------- /tests/data/syzygy/KQvK.rtbw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirillbobyrev/pabi/HEAD/tests/data/syzygy/KQvK.rtbw -------------------------------------------------------------------------------- /tests/data/syzygy/KQvK.rtbz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirillbobyrev/pabi/HEAD/tests/data/syzygy/KQvK.rtbz -------------------------------------------------------------------------------- /tests/data/syzygy/KRvK.rtbw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirillbobyrev/pabi/HEAD/tests/data/syzygy/KRvK.rtbw -------------------------------------------------------------------------------- /tests/data/syzygy/KRvK.rtbz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirillbobyrev/pabi/HEAD/tests/data/syzygy/KRvK.rtbz -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | # Optimize for the target CPU architecture for maximum performance. 3 | rustflags = ["-C", "target-cpu=native"] 4 | -------------------------------------------------------------------------------- /src/search/policy.rs: -------------------------------------------------------------------------------- 1 | use super::tree; 2 | use crate::environment::Action; 3 | 4 | fn select(node: &tree::Node) { 5 | todo!() 6 | } 7 | -------------------------------------------------------------------------------- /tests/data/README.md: -------------------------------------------------------------------------------- 1 | # Data 2 | 3 | - [positions.fen](./positions.fen) contains 100000 arbitrary positions that can 4 | be used for the purposes of testing. 5 | -------------------------------------------------------------------------------- /src/search/mod.rs: -------------------------------------------------------------------------------- 1 | //! Implements [Monte Carlo Tree Search] (MCTS) algorithm. 2 | //! 3 | //! [Monte Carlo Tree Search]: https://en.wikipedia.org/wiki/Monte_Carlo_tree_search 4 | 5 | pub mod mcts; 6 | mod policy; 7 | mod tree; 8 | -------------------------------------------------------------------------------- /src/chess/mod.rs: -------------------------------------------------------------------------------- 1 | //! Implementation of chess environment, its rules and specifics. 2 | 3 | pub mod attacks; 4 | pub mod bitboard; 5 | pub mod core; 6 | pub mod game; 7 | pub mod position; 8 | pub mod zobrist; 9 | 10 | mod generated; 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | - package-ecosystem: github-actions 8 | directory: "/" 9 | schedule: 10 | interval: daily 11 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/parse_board.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | use pabi::chess::position; 4 | 5 | fuzz_target!(|data: &[u8]| { 6 | let input = match std::str::from_utf8(data) { 7 | Ok(input) => input, 8 | Err(_) => return, 9 | }; 10 | drop(position::Position::try_from(input)) 11 | }); 12 | -------------------------------------------------------------------------------- /src/evaluation/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module implements "static" [evaluation], i.e. predicting the relative 2 | //! value/score of given position without [`crate::search`]. 3 | //! 4 | //! For convenience, the score is returned in centipawn units. 5 | //! 6 | //! [evaluation]: https://www.chessprogramming.org/Evaluation 7 | 8 | pub(crate) mod features; 9 | pub(crate) mod network; 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Rust 2 | /target 3 | /tools/target 4 | 5 | /fuzz/target 6 | /fuzz/corpus 7 | /fuzz/artifacts 8 | 9 | # Visual Studio Code 10 | .vscode/* 11 | !.vscode/settings.json 12 | !.vscode/tasks.json 13 | !.vscode/launch.json 14 | !.vscode/extensions.json 15 | *.code-workspace 16 | 17 | # tarpaulin test coverage reports 18 | cobertura.xml 19 | 20 | # Makefile-generated symlinks. 21 | /pabi 22 | /pabi-* 23 | -------------------------------------------------------------------------------- /generated/bishop_attack_offsets.rs: -------------------------------------------------------------------------------- 1 | [ 2 | 0, 64, 96, 128, 160, 192, 224, 256, 320, 352, 384, 416, 448, 480, 512, 544, 576, 608, 640, 768, 3 | 896, 1024, 1152, 1184, 1216, 1248, 1280, 1408, 1920, 2432, 2560, 2592, 2624, 2656, 2688, 2816, 4 | 3328, 3840, 3968, 4000, 4032, 4064, 4096, 4224, 4352, 4480, 4608, 4640, 4672, 4704, 4736, 4768, 5 | 4800, 4832, 4864, 4896, 4928, 4992, 5024, 5056, 5088, 5120, 5152, 5184, 6 | ] 7 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # Some options here require nightly. 2 | edition = "2024" 3 | format_code_in_doc_comments = true 4 | format_macro_matchers = true 5 | group_imports = "StdExternalCrate" 6 | hex_literal_case = "Upper" 7 | imports_granularity = "Module" 8 | newline_style = "Unix" 9 | normalize_comments = true 10 | normalize_doc_attributes = true 11 | reorder_impl_items = true 12 | wrap_comments = true 13 | use_field_init_shorthand = true 14 | -------------------------------------------------------------------------------- /src/bin/datagen.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | /// Generates training data for the policy network through self-play. 4 | #[derive(Parser, Debug)] 5 | #[command(version, about)] 6 | struct Config { 7 | // TODO: Book to seed the starting positions from. 8 | // TODO: Number of games. 9 | // TODO: Output file. 10 | // TODO: Tablebase path. 11 | // TODO: Flatten Search config. 12 | } 13 | 14 | fn main() { 15 | let config = Config::parse(); 16 | println!("{:?}", config); 17 | } 18 | -------------------------------------------------------------------------------- /generated/rook_attack_offsets.rs: -------------------------------------------------------------------------------- 1 | [ 2 | 0, 4096, 6144, 8192, 10240, 12288, 14336, 16384, 20480, 22528, 23552, 24576, 25600, 26624, 3 | 27648, 28672, 30720, 32768, 33792, 34816, 35840, 36864, 37888, 38912, 40960, 43008, 44032, 4 | 45056, 46080, 47104, 48128, 49152, 51200, 53248, 54272, 55296, 56320, 57344, 58368, 59392, 5 | 61440, 63488, 64512, 65536, 66560, 67584, 68608, 69632, 71680, 73728, 74752, 75776, 76800, 6 | 77824, 78848, 79872, 81920, 86016, 88064, 90112, 92160, 94208, 96256, 98304, 7 | ] 8 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | lint: 11 | name: Lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout sources 15 | uses: actions/checkout@v4 16 | - name: Install toolchain 17 | uses: dtolnay/rust-toolchain@nightly 18 | with: 19 | components: rustfmt, clippy 20 | - uses: taiki-e/install-action@v2 21 | with: 22 | tool: just 23 | - run: just lint 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Supporting builds through `make` command is a requirement for OpenBench: 2 | # https://github.com/AndyGrant/OpenBench/wiki/Requirements-For-Public-Engines#basic-requirements 3 | 4 | # Variable for the output binary name, defaults to 'pabi' if not provided. 5 | EXE ?= pabi 6 | 7 | ifeq ($(OS),Windows_NT) 8 | EXE_SUFFIX := .exe 9 | else 10 | EXE_SUFFIX := 11 | endif 12 | 13 | # Compile the target and add a link to the binary for OpenBench to pick up. 14 | openbench: 15 | $(COMPILE_FLAGS) cargo rustc --profile=release --bin=pabi -- --emit link=$(EXE)$(EXE_SUFFIX) 16 | 17 | .PHONY: openbench 18 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pabi-fuzz" 3 | version = "0.0.0" 4 | authors = ["Automatically generated"] 5 | publish = false 6 | edition = "2021" 7 | 8 | [package.metadata] 9 | cargo-fuzz = true 10 | 11 | [dependencies] 12 | libfuzzer-sys = "0.4.2" 13 | itertools = "0.13.0" 14 | shakmaty = "0.27.0" 15 | pretty_assertions = "1.1.0" 16 | 17 | [dependencies.pabi] 18 | path = ".." 19 | 20 | [workspace] 21 | members = ["."] 22 | 23 | [[bin]] 24 | name = "parse_board" 25 | path = "fuzz_targets/parse_board.rs" 26 | test = false 27 | doc = false 28 | 29 | [[bin]] 30 | name = "generate_moves" 31 | path = "fuzz_targets/generate_moves.rs" 32 | test = false 33 | doc = false 34 | -------------------------------------------------------------------------------- /src/bin/pabi.rs: -------------------------------------------------------------------------------- 1 | //! The main entry point for the UCI engine binary. 2 | 3 | use std::env; 4 | 5 | fn main() -> anyhow::Result<()> { 6 | let args: Vec = env::args().collect(); 7 | 8 | // OpenBench command for determining the relative speed of an engine. 9 | if args.len() == 2 && args[1] == "bench" { 10 | pabi::engine::openbench(); 11 | return Ok(()); 12 | } 13 | 14 | pabi::print_engine_info(); 15 | pabi::print_binary_info(); 16 | 17 | let mut input = std::io::stdin().lock(); 18 | let mut output = std::io::stdout().lock(); 19 | let mut engine = pabi::engine::Engine::new(&mut input, &mut output); 20 | engine.uci_loop() 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Test Coverage 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | coverage: 11 | name: Test Coverage 12 | runs-on: ubuntu-latest 13 | # Ensure there are no conflicting pushes to Codecov. 14 | concurrency: 15 | group: test-coverage 16 | steps: 17 | - name: Checkout sources 18 | uses: actions/checkout@v4 19 | - name: Install stable toolchain 20 | uses: dtolnay/rust-toolchain@stable 21 | - name: Install tarpaulin 22 | run: cargo install cargo-tarpaulin 23 | - name: Cargo tarpaulin 24 | run: cargo tarpaulin --out Xml 25 | - name: Upload the report to Codecov 26 | if: github.event_name == 'push' 27 | uses: codecov/codecov-action@v5 28 | with: 29 | fail_ci_if_error: true 30 | env: 31 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 32 | -------------------------------------------------------------------------------- /tests/integration.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::Command; 2 | use predicates::boolean::PredicateBooleanExt; 3 | use predicates::str::contains; 4 | 5 | const BINARY_NAME: &str = "pabi"; 6 | 7 | #[test] 8 | fn uci_setup() { 9 | let mut cmd = Command::cargo_bin(BINARY_NAME).expect("Binary should be built"); 10 | 11 | drop( 12 | cmd.write_stdin("uci\n") // Write the uci command to stdin 13 | .assert() 14 | .success() 15 | .stdout( 16 | contains("id name") 17 | .and(contains("id author")) 18 | .and(contains("uciok")), 19 | ), 20 | ); 21 | } 22 | 23 | // #[test] 24 | // #[ignore] 25 | // fn openbench_output() { 26 | // let mut cmd = Command::cargo_bin(BINARY_NAME).expect("Binary should be 27 | // built"); let _ = cmd.arg("bench"); 28 | 29 | // drop( 30 | // cmd.assert() 31 | // .stdout(is_match(r"^\d+ nodes \d+ nps$").unwrap()) 32 | // .success(), 33 | // ); 34 | // } 35 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test Suite 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | test: 11 | name: Test Suite 12 | runs-on: ${{ matrix.os }} 13 | continue-on-error: ${{ matrix.experimental }} 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest] 17 | toolchain: [stable] 18 | experimental: [false] 19 | include: 20 | - os: ubuntu-latest 21 | toolchain: nightly 22 | experimental: true 23 | - os: macos-latest 24 | toolchain: stable 25 | experimental: true 26 | - os: macos-latest 27 | toolchain: nightly 28 | experimental: true 29 | steps: 30 | - name: Checkout sources 31 | uses: actions/checkout@v4 32 | - name: Install ${{ matrix.toolchain }} toolchain 33 | uses: dtolnay/rust-toolchain@master 34 | with: 35 | toolchain: ${{ matrix.toolchain }} 36 | - uses: taiki-e/install-action@v2 37 | with: 38 | tool: just 39 | - run: just test_all 40 | -------------------------------------------------------------------------------- /src/search/mcts.rs: -------------------------------------------------------------------------------- 1 | /// Parameters for MCTS search algorithm. 2 | #[derive(Debug)] 3 | struct Config { 4 | /// Number of search iterations to perform. 5 | iterations: u16, 6 | /// Number of threads to use. 7 | threads: u16, 8 | /// Exploration constant ($c_puct$ in the original paper). 9 | cpuct: f32, 10 | temperature: f32, 11 | /// Dirichlet distribution parameter for action selection at the root node. 12 | dirichlet_alpha: f32, 13 | /// Fraction of the dirichlet noise to add to the prior probabilities 14 | /// ($\epsilon$ in the original paper). 15 | dirichlet_exploration_weight: f32, 16 | } 17 | 18 | /// Implements AlphaZero's Monte Carlo Tree Search algorithm. 19 | /// 20 | /// 1. Selection: Start from root node and select the most promising child node. 21 | /// 2. Expansion: If the selected node is not a leaf node, expand it by adding a 22 | /// new child node. 23 | /// 3. Simulation: Run a simulation from the child node until a result is 24 | /// reached. 25 | /// 4. Backpropagation: Update the nodes on the path from the root to the 26 | /// selected node with the result. 27 | fn search(iterations: usize) { 28 | for _ in 0..iterations { 29 | todo!() 30 | } 31 | } 32 | 33 | fn backup() { 34 | todo!() 35 | } 36 | 37 | fn simulate() { 38 | todo!() 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | build: 11 | name: Test Suite 12 | runs-on: ${{ matrix.os }} 13 | continue-on-error: ${{ matrix.experimental }} 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest] 17 | toolchain: [stable, "1.80.0"] 18 | experimental: [false] 19 | include: 20 | - os: ubuntu-latest 21 | toolchain: nightly 22 | experimental: true 23 | - os: windows-latest 24 | toolchain: stable 25 | experimental: true 26 | - os: windows-latest 27 | toolchain: nightly 28 | experimental: true 29 | - os: macos-latest 30 | toolchain: stable 31 | experimental: true 32 | - os: macos-latest 33 | toolchain: nightly 34 | experimental: true 35 | steps: 36 | - name: Checkout sources 37 | uses: actions/checkout@v4 38 | - name: Install ${{ matrix.toolchain }} toolchain 39 | uses: dtolnay/rust-toolchain@master 40 | with: 41 | toolchain: ${{ matrix.toolchain }} 42 | - uses: taiki-e/install-action@v2 43 | with: 44 | tool: just 45 | - run: just build 46 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | # This is a collection of commonly used recipes for development. Most of them 2 | # are wrappers around Cargo with the right flags and settings. 3 | 4 | build: 5 | cargo build --profile=release 6 | 7 | # Runs the engine and enters UCI mode. 8 | run: 9 | cargo run --profile=release --bin=pabi 10 | 11 | # Starts self-play games for data generation. 12 | datagen: 13 | cargo run --profile=release --bin=datagen 14 | 15 | # Format all code. 16 | fmt: 17 | cargo +nightly fmt --all 18 | 19 | # Checks the code for bad formatting, errors and warnings. 20 | lint: 21 | cargo +nightly fmt --all -- --check 22 | cargo clippy --all-targets --all-features 23 | 24 | # Runs the linters and tries to apply automatic fixes. 25 | fix: fmt 26 | cargo clippy --all-targets --all-features --fix --allow-staged 27 | 28 | # Run most tests in debug mode to (potentially) catch more errors with 29 | # debug_assert. 30 | test: 31 | cargo test 32 | 33 | # Run tests that are slow and are not run by default. 34 | test_slow: 35 | cargo test --profile=release -- --ignored 36 | 37 | # Run all tests. 38 | test_all: test test_slow 39 | 40 | bench: 41 | cargo bench --profile=release 42 | 43 | # Lists all fuzzing targets that can be used as inputs for fuzz command. 44 | list_fuzz_targets: 45 | cd fuzz 46 | cargo +nightly fuzz list 47 | 48 | fuzz target: 49 | cd fuzz 50 | cargo +nightly fuzz run {{ target }} -- --profile=release 51 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/generate_moves.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use itertools::Itertools; 3 | use libfuzzer_sys::fuzz_target; 4 | use pabi::chess::position; 5 | use pretty_assertions::assert_eq; 6 | use shakmaty::{CastlingMode, Chess, Position}; 7 | 8 | fuzz_target!(|data: &[u8]| { 9 | let input = match std::str::from_utf8(data) { 10 | Ok(input) => input, 11 | Err(_) => return, 12 | }; 13 | let position = match position::Position::from_fen(input) { 14 | Ok(position) => position, 15 | Err(_) => return, 16 | }; 17 | let shakmaty_setup: shakmaty::fen::Fen = input 18 | .parse() 19 | .expect("when we parsed a valid position it should be accepted by shakmaty"); 20 | let shakmaty_position: Result = shakmaty_setup.into_position(CastlingMode::Standard); 21 | if shakmaty_position.is_err() { 22 | return; 23 | } 24 | assert_eq!( 25 | position 26 | .generate_moves() 27 | .iter() 28 | .map(|m| m.to_string()) 29 | .sorted() 30 | .collect::>(), 31 | shakmaty_position 32 | .as_ref() 33 | .unwrap() 34 | .legal_moves() 35 | .iter() 36 | .map(|m| m.to_uci(CastlingMode::Standard).to_string()) 37 | .sorted() 38 | .collect::>() 39 | ); 40 | assert_eq!(position.in_check(), shakmaty_position.is_check()); 41 | }); 42 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Modern and high-quality chess engine. For more information, see [README]. 2 | //! 3 | //! [README]: https://github.com/kirillbobyrev/pabi/blob/main/README.md 4 | 5 | // TODO: Re-export types for convenience. 6 | pub mod chess; 7 | pub mod datagen; 8 | pub mod engine; 9 | pub mod environment; 10 | pub mod evaluation; 11 | pub mod search; 12 | 13 | pub use engine::Engine; 14 | 15 | shadow_rs::shadow!(build); 16 | 17 | /// Features the engine is built with (e.g. build type and target). Produced by 18 | /// `build.rs`. 19 | const BUILD_FEATURES: &str = include_str!(concat!(env!("OUT_DIR"), "/features")); 20 | 21 | /// Returns the full engine version that can be used to identify how it was 22 | /// built in the first place. 23 | fn engine_version() -> String { 24 | format!( 25 | "{} (commit {}, branch {})", 26 | build::PKG_VERSION, 27 | build::SHORT_COMMIT, 28 | build::BRANCH 29 | ) 30 | } 31 | 32 | /// Prints information about the engine version, author and GitHub repository 33 | /// on engine startup. 34 | pub fn print_engine_info() { 35 | println!("Pabi chess engine {}", engine_version()); 36 | println!(""); 37 | } 38 | 39 | /// Prints information the build type, features and whether the build is clean 40 | /// on engine startup. 41 | pub fn print_binary_info() { 42 | println!("Release build: {}", !shadow_rs::is_debug()); 43 | println!("Features: {BUILD_FEATURES}"); 44 | if !shadow_rs::git_clean() { 45 | println!("Warning: built with uncommitted changes"); 46 | } 47 | println!(); 48 | } 49 | -------------------------------------------------------------------------------- /src/search/tree.rs: -------------------------------------------------------------------------------- 1 | use crate::environment::Action; 2 | 3 | /// Node stores (wins, draws, losses) statistics instead of expanded "value" 4 | /// (or total score), which is usually wins + 0.5 * draws. 5 | /// 6 | /// For more details, ses https://lczero.org/blog/2020/04/wdl-head/ 7 | // TODO: Measure the performance and see if switching to ArrayVec will make it 8 | // faster. 9 | pub(super) struct Node { 10 | children: Vec>, 11 | actions: Vec, 12 | prior: f32, 13 | /// Total number of search iterations that went through this node. 14 | visits: u32, 15 | /// Number of wins from the perspective of player to move. 16 | wins: u32, 17 | /// Number of losses from the perspective of player to move. 18 | losses: u32, 19 | } 20 | 21 | impl Default for Node { 22 | fn default() -> Self { 23 | Self { 24 | children: Vec::new(), 25 | actions: Vec::new(), 26 | prior: 0.0, 27 | visits: 0, 28 | wins: 0, 29 | losses: 0, 30 | } 31 | } 32 | } 33 | 34 | impl Node { 35 | fn expand(&mut self) { 36 | todo!() 37 | } 38 | 39 | /// Returns true if the node has been visited at least once. 40 | #[must_use] 41 | const fn visited(&self) -> bool { 42 | self.visits > 0 43 | } 44 | 45 | #[must_use] 46 | const fn is_leaf(&self) -> bool { 47 | todo!() 48 | } 49 | 50 | #[must_use] 51 | const fn is_terminal(&self) -> bool { 52 | todo!() 53 | } 54 | 55 | #[must_use] 56 | const fn draws(&self) -> u32 { 57 | self.visits - self.wins - self.losses 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /generated/bishop_relevant_occupancies.rs: -------------------------------------------------------------------------------- 1 | [ 2 | 18_049_651_735_527_936, 3 | 70_506_452_091_904, 4 | 275_415_828_992, 5 | 1_075_975_168, 6 | 38_021_120, 7 | 8_657_588_224, 8 | 2_216_338_399_232, 9 | 567_382_630_219_776, 10 | 9_024_825_867_763_712, 11 | 18_049_651_735_527_424, 12 | 70_506_452_221_952, 13 | 275_449_643_008, 14 | 9_733_406_720, 15 | 2_216_342_585_344, 16 | 567_382_630_203_392, 17 | 1_134_765_260_406_784, 18 | 4_512_412_933_816_832, 19 | 9_024_825_867_633_664, 20 | 18_049_651_768_822_272, 21 | 70_515_108_615_168, 22 | 2_491_752_130_560, 23 | 567_383_701_868_544, 24 | 1_134_765_256_220_672, 25 | 2_269_530_512_441_344, 26 | 2_256_206_450_263_040, 27 | 4_512_412_900_526_080, 28 | 9_024_834_391_117_824, 29 | 18_051_867_805_491_712, 30 | 637_888_545_440_768, 31 | 1_135_039_602_493_440, 32 | 2_269_529_440_784_384, 33 | 4_539_058_881_568_768, 34 | 1_128_098_963_916_800, 35 | 2_256_197_927_833_600, 36 | 4_514_594_912_477_184, 37 | 9_592_139_778_506_752, 38 | 19_184_279_556_981_248, 39 | 2_339_762_086_609_920, 40 | 4_538_784_537_380_864, 41 | 9_077_569_074_761_728, 42 | 562_958_610_993_152, 43 | 1_125_917_221_986_304, 44 | 2_814_792_987_328_512, 45 | 5_629_586_008_178_688, 46 | 11_259_172_008_099_840, 47 | 22_518_341_868_716_544, 48 | 9_007_336_962_655_232, 49 | 18_014_673_925_310_464, 50 | 2_216_338_399_232, 51 | 4_432_676_798_464, 52 | 11_064_376_819_712, 53 | 22_137_335_185_408, 54 | 44_272_556_441_600, 55 | 87_995_357_200_384, 56 | 35_253_226_045_952, 57 | 70_506_452_091_904, 58 | 567_382_630_219_776, 59 | 1_134_765_260_406_784, 60 | 2_832_480_465_846_272, 61 | 5_667_157_807_464_448, 62 | 11_333_774_449_049_600, 63 | 22_526_811_443_298_304, 64 | 9_024_825_867_763_712, 65 | 18_049_651_735_527_936, 66 | ] 67 | -------------------------------------------------------------------------------- /generated/rook_relevant_occupancies.rs: -------------------------------------------------------------------------------- 1 | [ 2 | 282_578_800_148_862, 3 | 565_157_600_297_596, 4 | 1_130_315_200_595_066, 5 | 2_260_630_401_190_006, 6 | 4_521_260_802_379_886, 7 | 9_042_521_604_759_646, 8 | 18_085_043_209_519_166, 9 | 36_170_086_419_038_334, 10 | 282_578_800_180_736, 11 | 565_157_600_328_704, 12 | 1_130_315_200_625_152, 13 | 2_260_630_401_218_048, 14 | 4_521_260_802_403_840, 15 | 9_042_521_604_775_424, 16 | 18_085_043_209_518_592, 17 | 36_170_086_419_037_696, 18 | 282_578_808_340_736, 19 | 565_157_608_292_864, 20 | 1_130_315_208_328_192, 21 | 2_260_630_408_398_848, 22 | 4_521_260_808_540_160, 23 | 9_042_521_608_822_784, 24 | 18_085_043_209_388_032, 25 | 36_170_086_418_907_136, 26 | 282_580_897_300_736, 27 | 565_159_647_117_824, 28 | 1_130_317_180_306_432, 29 | 2_260_632_246_683_648, 30 | 4_521_262_379_438_080, 31 | 9_042_522_644_946_944, 32 | 18_085_043_175_964_672, 33 | 36_170_086_385_483_776, 34 | 283_115_671_060_736, 35 | 565_681_586_307_584, 36 | 1_130_822_006_735_872, 37 | 2_261_102_847_592_448, 38 | 4_521_664_529_305_600, 39 | 9_042_787_892_731_904, 40 | 18_085_034_619_584_512, 41 | 36_170_077_829_103_616, 42 | 420_017_753_620_736, 43 | 699_298_018_886_144, 44 | 1_260_057_572_672_512, 45 | 2_381_576_680_245_248, 46 | 4_624_614_895_390_720, 47 | 9_110_691_325_681_664, 48 | 18_082_844_186_263_552, 49 | 36_167_887_395_782_656, 50 | 35_466_950_888_980_736, 51 | 34_905_104_758_997_504, 52 | 34_344_362_452_452_352, 53 | 33_222_877_839_362_048, 54 | 30_979_908_613_181_440, 55 | 26_493_970_160_820_224, 56 | 17_522_093_256_097_792, 57 | 35_607_136_465_616_896, 58 | 9_079_539_427_579_068_672, 59 | 8_935_706_818_303_361_536, 60 | 8_792_156_787_827_803_136, 61 | 8_505_056_726_876_686_336, 62 | 7_930_856_604_974_452_736, 63 | 6_782_456_361_169_985_536, 64 | 4_485_655_873_561_051_136, 65 | 9_115_426_935_197_958_144, 66 | ] 67 | -------------------------------------------------------------------------------- /src/environment.rs: -------------------------------------------------------------------------------- 1 | //! Interface for Reinforcement Learning environment to abstract the chess 2 | //! rules implementation. 3 | 4 | use std::fmt; 5 | use std::ops::Not; 6 | 7 | use anyhow::bail; 8 | 9 | /// A standard game of chess is played between two players: White (having the 10 | /// advantage of the first turn) and Black. 11 | #[allow(missing_docs, reason = "Enum variants are self-explanatory")] 12 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 13 | pub enum Player { 14 | White, 15 | Black, 16 | } 17 | 18 | impl Not for Player { 19 | type Output = Self; 20 | 21 | fn not(self) -> Self::Output { 22 | match self { 23 | Self::White => Self::Black, 24 | Self::Black => Self::White, 25 | } 26 | } 27 | } 28 | 29 | impl TryFrom<&str> for Player { 30 | type Error = anyhow::Error; 31 | 32 | fn try_from(color: &str) -> anyhow::Result { 33 | match color { 34 | "w" => Ok(Self::White), 35 | "b" => Ok(Self::Black), 36 | _ => bail!("color should be 'w' or 'b', got '{color}'"), 37 | } 38 | } 39 | } 40 | 41 | impl fmt::Display for Player { 42 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 43 | write!( 44 | f, 45 | "{}", 46 | match &self { 47 | Self::White => 'w', 48 | Self::Black => 'b', 49 | } 50 | ) 51 | } 52 | } 53 | 54 | /// Result of the game from the perspective of the player to move at root. 55 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 56 | pub enum GameResult { 57 | Win, 58 | Draw, 59 | Loss, 60 | } 61 | 62 | // TODO: Require features tensor? 63 | pub trait Observation {} 64 | 65 | pub trait Action: Sized { 66 | fn get_index(&self) -> u16; 67 | } 68 | 69 | /// Standard gym-like Reinforcement Learning environment interface. 70 | pub trait Environment: Sized { 71 | fn actions(&self) -> &[A]; 72 | fn apply(&mut self, action: &A) -> &O; 73 | fn result(&self) -> Option; 74 | } 75 | -------------------------------------------------------------------------------- /src/chess/zobrist.rs: -------------------------------------------------------------------------------- 1 | //! Zobrist hashing-related utilities`. 2 | 3 | use std::collections::HashMap; 4 | 5 | /// Zobrist keys are 64-bit unsigned integers that are computed once position is 6 | /// created and updated whenever a move is made. 7 | pub type Key = u64; 8 | 9 | #[derive(Debug)] 10 | pub(crate) struct RepetitionTable { 11 | table: HashMap, 12 | } 13 | 14 | impl RepetitionTable { 15 | /// Creates an empty repetition table. 16 | pub(crate) fn new() -> Self { 17 | Self { 18 | table: HashMap::new(), 19 | } 20 | } 21 | 22 | /// Returns true if the position has occurred 3 times. 23 | /// 24 | /// In the tournament setting 3-fold repetition is a draw. 25 | #[must_use] 26 | pub(crate) fn record(&mut self, key: Key) -> bool { 27 | let count = self.table.entry(key).or_insert(0); 28 | *count += 1; 29 | *count == 3 30 | } 31 | } 32 | 33 | #[cfg(test)] 34 | mod tests { 35 | use super::*; 36 | use crate::chess::core::Move; 37 | use crate::chess::position::Position; 38 | 39 | #[test] 40 | fn repetition_table() { 41 | let mut table = RepetitionTable::new(); 42 | 43 | let mut position = Position::starting(); 44 | let initial_hash = position.hash(); 45 | assert!(!table.record(initial_hash)); 46 | 47 | position.make_move(&Move::from_uci("g1f3").expect("valid move")); 48 | assert_ne!(initial_hash, position.hash()); 49 | assert!(!table.record(position.hash())); 50 | position.make_move(&Move::from_uci("g8f6").expect("valid move")); 51 | assert!(!table.record(position.hash())); 52 | 53 | position.make_move(&Move::from_uci("f3g1").expect("valid move")); 54 | assert!(!table.record(position.hash())); 55 | // Two-fold repetition. 56 | position.make_move(&Move::from_uci("f6g8").expect("valid move")); 57 | assert!(!table.record(position.hash())); 58 | 59 | position.make_move(&Move::from_uci("g1f3").expect("valid move")); 60 | assert!(!table.record(position.hash())); 61 | position.make_move(&Move::from_uci("g8f6").expect("valid move")); 62 | assert!(!table.record(position.hash())); 63 | 64 | position.make_move(&Move::from_uci("f3g1").expect("valid move")); 65 | assert!(!table.record(position.hash())); 66 | // Three-fold repetition. 67 | position.make_move(&Move::from_uci("f6g8").expect("valid move")); 68 | assert!(table.record(position.hash())); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Pabi 3 |

4 | 5 | [![Codecov](https://codecov.io/gh/kirillbobyrev/pabi/branch/main/graph/badge.svg)](https://codecov.io/gh/kirillbobyrev/pabi) 6 | [![Dependencies](https://deps.rs/repo/github/kirillbobyrev/pabi/status.svg)](https://deps.rs/repo/github/kirillbobyrev/pabi) 7 | 8 | [![Build](https://github.com/kirillbobyrev/pabi/actions/workflows/build.yml/badge.svg)](https://github.com/kirillbobyrev/pabi/actions/workflows/build.yml) 9 | [![Test Suite](https://github.com/kirillbobyrev/pabi/actions/workflows/test.yml/badge.svg)](https://github.com/kirillbobyrev/pabi/actions/workflows/test.yml) 10 | [![Lint](https://github.com/kirillbobyrev/pabi/actions/workflows/lint.yml/badge.svg)](https://github.com/kirillbobyrev/pabi/actions/workflows/lint.yml) 11 | 12 | Pabi is a modern chess engine that is currently under development. 13 | 14 | > [!WARNING] 15 | > This engine is still very early in the development phase and is not in a 16 | > functional state yet. 17 | 18 | ## Goals 19 | 20 | Pabi is inspired by existing Chess and Go engines (mainly [lc0] and [KataGo]), 21 | and the research that they are based on ([AlphaZero], [MuZero] and [MCTS]). Pabi 22 | strives to be a high-quality modern engine. 23 | 24 | Pabi strives to be **strong**. The ultimate goal is to enter the lists of top 25 | chess engines and participate in tournaments, such as and [TCEC] and [CCC]. 26 | 27 | 28 | 29 | 30 | ## Recipes 31 | 32 | Most commands for development, building the engine, testing, checking for errors 33 | and fuzzing it are supported as [just](https://github.com/casey/just) recipes. 34 | See [justfile](/justfile) for a complete list of frequently used commands. 35 | 36 | ## Code map 37 | 38 | For easier code navigation, see 39 | 40 | 41 | ## [Milestones] 42 | 43 | - [ ] [Proof of Concept] 44 | - [ ] [Stable] 45 | - [ ] [Strong] 46 | 47 | [AlphaZero]: https://en.wikipedia.org/wiki/AlphaZero 48 | [MuZero]: https://deepmind.google/discover/blog/muzero-mastering-go-chess-shogi-and-atari-without-rules/ 49 | [CCC]: https://www.chess.com/computer-chess-championship 50 | [KataGo]: https://github.com/lightvector/KataGo 51 | [MCTS]: https://en.wikipedia.org/wiki/Monte_Carlo_tree_search 52 | [Milestones]: https://github.com/kirillbobyrev/pabi/milestones 53 | [Proof of Concept]: https://github.com/kirillbobyrev/pabi/milestone/1 54 | [Stable]: https://github.com/kirillbobyrev/pabi/milestone/2 55 | [Strong]: https://github.com/kirillbobyrev/pabi/milestone/3 56 | [TCEC]: https://tcec-chess.com/ 57 | [lc0]: https://lczero.org/ 58 | -------------------------------------------------------------------------------- /.github/copilot-instructions.md: -------------------------------------------------------------------------------- 1 | # Copilot instructions 2 | 3 | You are assisting in the development of a high-performance chess engine written 4 | in Rust called "Pabi". Your role is to provide guidance on implementation 5 | details, algorithms, and optimization techniques. 6 | 7 | ## Project Vision 8 | 9 | Pabi aims to be a top-tier chess engine capable of: 10 | 11 | - Ranking in the top 10 engines in Blitz and Rapid time formats 12 | - Competing successfully in the Top Chess Engine Championship (TCEC) 13 | - Rivaling established engines like Stockfish and Leela Chess Zero 14 | 15 | ## Core Priorities (in order) 16 | 17 | 1. **Playing Strength**: Suggest optimal algorithms, evaluation techniques, and 18 | strategic enhancements 19 | 2. **Computational Performance**: Maximize speed and efficiency as they directly 20 | impact playing strength 21 | 3. **Code Elegance**: Maintain readability and maintainability without 22 | sacrificing performance 23 | 24 | ## Technical Architecture 25 | 26 | ### Search Algorithm 27 | 28 | - Implement Monte Carlo Tree Search (MCTS) with neural network policy guidance 29 | - Optimize tree traversal and node selection strategies 30 | - Apply efficient pruning techniques to focus computational resources 31 | 32 | ### Neural Network Implementation 33 | 34 | - Network training will be done in Python with JAX 35 | - Inference will be optimized for x86 CPUs using: 36 | - SIMD vectorization (AVX512, AVX2, SSE4.2) 37 | - Quantization techniques (INT8/INT4 where appropriate) 38 | - Memory-access optimization patterns 39 | 40 | ### Resource Management 41 | 42 | - Implement aggressive parallelization across 512 hardware threads 43 | - Design a memory manager that stays within 256 GiB RAM constraints 44 | - Include intelligent cleanup of search trees to prevent memory bloat 45 | - Utilize 1 TiB SSD efficiently for persistent storage needs 46 | 47 | ### Chess-Specific Optimizations 48 | 49 | - Use bitboard representation for maximum position analysis speed 50 | - Implement specialized move generators for different piece types 51 | - Apply Zobrist hashing for efficient position identification 52 | - Support 7-men Syzygy endgame tablebases with smart caching 53 | 54 | ### Training Pipeline 55 | 56 | - Self-play data generation following AlphaZero methodology 57 | - Distributed training to maximize learning efficiency 58 | - Continuous evaluation against benchmark positions 59 | 60 | ## Hardware Target Specifications 61 | 62 | - AMD EPYC 9754 (256 cores, 512 threads) 63 | - 256 GiB DDR5 RAM 64 | - 1 TiB SSD storage 65 | - 64-bit x86 architecture with AVX512/AVX2/SSE4.2 support 66 | - 7-men Syzygy tablebases on NVMe SSD with RAM caching 67 | 68 | When suggesting solutions, provide examples using Rust idioms for 69 | performance-critical code sections. Where appropriate, explain the tradeoffs 70 | between different approaches using analogies to help clarify complex concepts. 71 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Kirill Bobyrev "] 3 | categories = ["command-line-interface"] 4 | description = "Chess engine" 5 | documentation = "https://docs.rs/pabi" 6 | edition = "2024" 7 | homepage = "https://github.com/kirillbobyrev/pabi" 8 | keywords = ["chess", "engine", "machine-learning"] 9 | license = "Apache-2.0" 10 | name = "pabi" 11 | readme = "README.md" 12 | repository = "https://github.com/kirillbobyrev/pabi" 13 | rust-version = "1.85" 14 | version = "2025.3.13" 15 | include = [ 16 | "/src/", 17 | "/generated/", 18 | "/Cargo.toml", 19 | "/Cargo.lock", 20 | "build.rs", 21 | "LICENSE", 22 | "README.md", 23 | ] # Reduce the package size by only including things necessary for building it. 24 | 25 | [dependencies] 26 | anyhow = "1.0.83" 27 | arrayvec = "0.7.2" 28 | bitflags = "2.2.1" 29 | clap = { version = "4.5.15", features = ["derive", "wrap_help"] } 30 | indicatif = "0.17.8" 31 | itertools = "0.13.0" 32 | # Use SmallRng for performance. 33 | rand = { version = "0.9.0", features = ["small_rng"] } 34 | rand_distr = "0.5.1" 35 | rayon = "1.10.0" 36 | shadow-rs = "1.0.1" 37 | # TODO: Potentially remove this dependency. 38 | # Used for probing tablebases. 39 | shakmaty = "0.27.1" 40 | shakmaty-syzygy = "0.25.0" 41 | 42 | [build-dependencies] 43 | rand = "0.9.0" 44 | shadow-rs = "1.0.1" 45 | 46 | [dev-dependencies] 47 | assert_cmd = "2.0.16" 48 | criterion = "0.5.1" 49 | predicates = "3.1.2" 50 | pretty_assertions = "1.1.0" 51 | proptest = "1.5.0" 52 | shadow-rs = "1.0.1" 53 | # Used for testing and comparing against a reasonable baseline for correctness. 54 | shakmaty = "0.27.1" 55 | 56 | [[bench]] 57 | harness = false 58 | name = "chess" 59 | 60 | # TODO: Test this out once the benchmarks are available and tweak specific 61 | # values. So far, this gives around -8% on parsing FEN/EPD positions. 62 | [profile.release] 63 | codegen-units = 1 64 | lto = "fat" 65 | panic = "abort" 66 | strip = true 67 | # TODO: Tweak inline-threshold. 68 | # TODO: Set profile-generate and profile-use (https://github.com/kirillbobyrev/pabi/issues/9). 69 | 70 | [lints.rust] 71 | # absolute_paths_not_starting_with_crate = "warn" 72 | # let_underscore_drop = "warn" 73 | # macro_use_extern_crate = "warn" 74 | # missing_docs = "warn" 75 | # unused_extern_crates = "warn" 76 | # unused_import_braces = "warn" 77 | # unused_lifetimes = "warn" 78 | # unused_qualifications = "warn" 79 | keyword_idents_2018 = "deny" 80 | keyword_idents_2024 = "deny" 81 | trivial_casts = "deny" 82 | # trivial_numeric_casts = "deny" 83 | # unreachable_pub = "deny" 84 | # unused_results = "deny" 85 | 86 | [lints.clippy] 87 | missing_transmute_annotations = "allow" 88 | # cargo = "warn" 89 | # complexity = "warn" 90 | # correctness = "warn" 91 | # nursery = "warn" 92 | # pedantic = "warn" 93 | # style = "warn" 94 | suspicious = { level = "deny", priority = -1 } 95 | redundant_pattern_matching = "deny" 96 | perf = { level = "deny", priority = -1 } 97 | allow_attributes_without_reason = "deny" 98 | derivable_impls = "deny" 99 | 100 | [lints.rustdoc] 101 | broken_intra_doc_links = "deny" 102 | invalid_rust_codeblocks = "deny" 103 | unescaped_backticks = "deny" 104 | -------------------------------------------------------------------------------- /generated/king_attacks.rs: -------------------------------------------------------------------------------- 1 | [ 2 | Bitboard::from_bits(0x0000_0000_0000_0302), 3 | Bitboard::from_bits(0x0000_0000_0000_0705), 4 | Bitboard::from_bits(0x0000_0000_0000_0e0a), 5 | Bitboard::from_bits(0x0000_0000_0000_1c14), 6 | Bitboard::from_bits(0x0000_0000_0000_3828), 7 | Bitboard::from_bits(0x0000_0000_0000_7050), 8 | Bitboard::from_bits(0x0000_0000_0000_e0a0), 9 | Bitboard::from_bits(0x0000_0000_0000_c040), 10 | Bitboard::from_bits(0x0000_0000_0003_0203), 11 | Bitboard::from_bits(0x0000_0000_0007_0507), 12 | Bitboard::from_bits(0x0000_0000_000e_0a0e), 13 | Bitboard::from_bits(0x0000_0000_001c_141c), 14 | Bitboard::from_bits(0x0000_0000_0038_2838), 15 | Bitboard::from_bits(0x0000_0000_0070_5070), 16 | Bitboard::from_bits(0x0000_0000_00e0_a0e0), 17 | Bitboard::from_bits(0x0000_0000_00c0_40c0), 18 | Bitboard::from_bits(0x0000_0000_0302_0300), 19 | Bitboard::from_bits(0x0000_0000_0705_0700), 20 | Bitboard::from_bits(0x0000_0000_0e0a_0e00), 21 | Bitboard::from_bits(0x0000_0000_1c14_1c00), 22 | Bitboard::from_bits(0x0000_0000_3828_3800), 23 | Bitboard::from_bits(0x0000_0000_7050_7000), 24 | Bitboard::from_bits(0x0000_0000_e0a0_e000), 25 | Bitboard::from_bits(0x0000_0000_c040_c000), 26 | Bitboard::from_bits(0x0000_0003_0203_0000), 27 | Bitboard::from_bits(0x0000_0007_0507_0000), 28 | Bitboard::from_bits(0x0000_000e_0a0e_0000), 29 | Bitboard::from_bits(0x0000_001c_141c_0000), 30 | Bitboard::from_bits(0x0000_0038_2838_0000), 31 | Bitboard::from_bits(0x0000_0070_5070_0000), 32 | Bitboard::from_bits(0x0000_00e0_a0e0_0000), 33 | Bitboard::from_bits(0x0000_00c0_40c0_0000), 34 | Bitboard::from_bits(0x0000_0302_0300_0000), 35 | Bitboard::from_bits(0x0000_0705_0700_0000), 36 | Bitboard::from_bits(0x0000_0e0a_0e00_0000), 37 | Bitboard::from_bits(0x0000_1c14_1c00_0000), 38 | Bitboard::from_bits(0x0000_3828_3800_0000), 39 | Bitboard::from_bits(0x0000_7050_7000_0000), 40 | Bitboard::from_bits(0x0000_e0a0_e000_0000), 41 | Bitboard::from_bits(0x0000_c040_c000_0000), 42 | Bitboard::from_bits(0x0003_0203_0000_0000), 43 | Bitboard::from_bits(0x0007_0507_0000_0000), 44 | Bitboard::from_bits(0x000e_0a0e_0000_0000), 45 | Bitboard::from_bits(0x001c_141c_0000_0000), 46 | Bitboard::from_bits(0x0038_2838_0000_0000), 47 | Bitboard::from_bits(0x0070_5070_0000_0000), 48 | Bitboard::from_bits(0x00e0_a0e0_0000_0000), 49 | Bitboard::from_bits(0x00c0_40c0_0000_0000), 50 | Bitboard::from_bits(0x0302_0300_0000_0000), 51 | Bitboard::from_bits(0x0705_0700_0000_0000), 52 | Bitboard::from_bits(0x0e0a_0e00_0000_0000), 53 | Bitboard::from_bits(0x1c14_1c00_0000_0000), 54 | Bitboard::from_bits(0x3828_3800_0000_0000), 55 | Bitboard::from_bits(0x7050_7000_0000_0000), 56 | Bitboard::from_bits(0xe0a0_e000_0000_0000), 57 | Bitboard::from_bits(0xc040_c000_0000_0000), 58 | Bitboard::from_bits(0x0203_0000_0000_0000), 59 | Bitboard::from_bits(0x0507_0000_0000_0000), 60 | Bitboard::from_bits(0x0a0e_0000_0000_0000), 61 | Bitboard::from_bits(0x141c_0000_0000_0000), 62 | Bitboard::from_bits(0x2838_0000_0000_0000), 63 | Bitboard::from_bits(0x5070_0000_0000_0000), 64 | Bitboard::from_bits(0xa0e0_0000_0000_0000), 65 | Bitboard::from_bits(0x40c0_0000_0000_0000), 66 | ] 67 | -------------------------------------------------------------------------------- /generated/black_pawn_attacks.rs: -------------------------------------------------------------------------------- 1 | [ 2 | Bitboard::from_bits(0x0000_0000_0000_0000), 3 | Bitboard::from_bits(0x0000_0000_0000_0000), 4 | Bitboard::from_bits(0x0000_0000_0000_0000), 5 | Bitboard::from_bits(0x0000_0000_0000_0000), 6 | Bitboard::from_bits(0x0000_0000_0000_0000), 7 | Bitboard::from_bits(0x0000_0000_0000_0000), 8 | Bitboard::from_bits(0x0000_0000_0000_0000), 9 | Bitboard::from_bits(0x0000_0000_0000_0000), 10 | Bitboard::from_bits(0x0000_0000_0000_0002), 11 | Bitboard::from_bits(0x0000_0000_0000_0005), 12 | Bitboard::from_bits(0x0000_0000_0000_000a), 13 | Bitboard::from_bits(0x0000_0000_0000_0014), 14 | Bitboard::from_bits(0x0000_0000_0000_0028), 15 | Bitboard::from_bits(0x0000_0000_0000_0050), 16 | Bitboard::from_bits(0x0000_0000_0000_00a0), 17 | Bitboard::from_bits(0x0000_0000_0000_0040), 18 | Bitboard::from_bits(0x0000_0000_0000_0200), 19 | Bitboard::from_bits(0x0000_0000_0000_0500), 20 | Bitboard::from_bits(0x0000_0000_0000_0a00), 21 | Bitboard::from_bits(0x0000_0000_0000_1400), 22 | Bitboard::from_bits(0x0000_0000_0000_2800), 23 | Bitboard::from_bits(0x0000_0000_0000_5000), 24 | Bitboard::from_bits(0x0000_0000_0000_a000), 25 | Bitboard::from_bits(0x0000_0000_0000_4000), 26 | Bitboard::from_bits(0x0000_0000_0002_0000), 27 | Bitboard::from_bits(0x0000_0000_0005_0000), 28 | Bitboard::from_bits(0x0000_0000_000a_0000), 29 | Bitboard::from_bits(0x0000_0000_0014_0000), 30 | Bitboard::from_bits(0x0000_0000_0028_0000), 31 | Bitboard::from_bits(0x0000_0000_0050_0000), 32 | Bitboard::from_bits(0x0000_0000_00a0_0000), 33 | Bitboard::from_bits(0x0000_0000_0040_0000), 34 | Bitboard::from_bits(0x0000_0000_0200_0000), 35 | Bitboard::from_bits(0x0000_0000_0500_0000), 36 | Bitboard::from_bits(0x0000_0000_0a00_0000), 37 | Bitboard::from_bits(0x0000_0000_1400_0000), 38 | Bitboard::from_bits(0x0000_0000_2800_0000), 39 | Bitboard::from_bits(0x0000_0000_5000_0000), 40 | Bitboard::from_bits(0x0000_0000_a000_0000), 41 | Bitboard::from_bits(0x0000_0000_4000_0000), 42 | Bitboard::from_bits(0x0000_0002_0000_0000), 43 | Bitboard::from_bits(0x0000_0005_0000_0000), 44 | Bitboard::from_bits(0x0000_000a_0000_0000), 45 | Bitboard::from_bits(0x0000_0014_0000_0000), 46 | Bitboard::from_bits(0x0000_0028_0000_0000), 47 | Bitboard::from_bits(0x0000_0050_0000_0000), 48 | Bitboard::from_bits(0x0000_00a0_0000_0000), 49 | Bitboard::from_bits(0x0000_0040_0000_0000), 50 | Bitboard::from_bits(0x0000_0200_0000_0000), 51 | Bitboard::from_bits(0x0000_0500_0000_0000), 52 | Bitboard::from_bits(0x0000_0a00_0000_0000), 53 | Bitboard::from_bits(0x0000_1400_0000_0000), 54 | Bitboard::from_bits(0x0000_2800_0000_0000), 55 | Bitboard::from_bits(0x0000_5000_0000_0000), 56 | Bitboard::from_bits(0x0000_a000_0000_0000), 57 | Bitboard::from_bits(0x0000_4000_0000_0000), 58 | Bitboard::from_bits(0x0000_0000_0000_0000), 59 | Bitboard::from_bits(0x0000_0000_0000_0000), 60 | Bitboard::from_bits(0x0000_0000_0000_0000), 61 | Bitboard::from_bits(0x0000_0000_0000_0000), 62 | Bitboard::from_bits(0x0000_0000_0000_0000), 63 | Bitboard::from_bits(0x0000_0000_0000_0000), 64 | Bitboard::from_bits(0x0000_0000_0000_0000), 65 | Bitboard::from_bits(0x0000_0000_0000_0000), 66 | ] 67 | -------------------------------------------------------------------------------- /generated/knight_attacks.rs: -------------------------------------------------------------------------------- 1 | [ 2 | Bitboard::from_bits(0x0000_0000_0002_0400), 3 | Bitboard::from_bits(0x0000_0000_0005_0800), 4 | Bitboard::from_bits(0x0000_0000_000A_1100), 5 | Bitboard::from_bits(0x0000_0000_0014_2200), 6 | Bitboard::from_bits(0x0000_0000_0028_4400), 7 | Bitboard::from_bits(0x0000_0000_0050_8800), 8 | Bitboard::from_bits(0x0000_0000_00A0_1000), 9 | Bitboard::from_bits(0x0000_0000_0040_2000), 10 | Bitboard::from_bits(0x0000_0000_0204_0004), 11 | Bitboard::from_bits(0x0000_0000_0508_0008), 12 | Bitboard::from_bits(0x0000_0000_0A11_0011), 13 | Bitboard::from_bits(0x0000_0000_1422_0022), 14 | Bitboard::from_bits(0x0000_0000_2844_0044), 15 | Bitboard::from_bits(0x0000_0000_5088_0088), 16 | Bitboard::from_bits(0x0000_0000_A010_0010), 17 | Bitboard::from_bits(0x0000_0000_4020_0020), 18 | Bitboard::from_bits(0x0000_0002_0400_0402), 19 | Bitboard::from_bits(0x0000_0005_0800_0805), 20 | Bitboard::from_bits(0x0000_000A_1100_110A), 21 | Bitboard::from_bits(0x0000_0014_2200_2214), 22 | Bitboard::from_bits(0x0000_0028_4400_4428), 23 | Bitboard::from_bits(0x0000_0050_8800_8850), 24 | Bitboard::from_bits(0x0000_00A0_1000_10A0), 25 | Bitboard::from_bits(0x0000_0040_2000_2040), 26 | Bitboard::from_bits(0x0000_0204_0004_0200), 27 | Bitboard::from_bits(0x0000_0508_0008_0500), 28 | Bitboard::from_bits(0x0000_0A11_0011_0A00), 29 | Bitboard::from_bits(0x0000_1422_0022_1400), 30 | Bitboard::from_bits(0x0000_2844_0044_2800), 31 | Bitboard::from_bits(0x0000_5088_0088_5000), 32 | Bitboard::from_bits(0x0000_A010_0010_A000), 33 | Bitboard::from_bits(0x0000_4020_0020_4000), 34 | Bitboard::from_bits(0x0002_0400_0402_0000), 35 | Bitboard::from_bits(0x0005_0800_0805_0000), 36 | Bitboard::from_bits(0x000A_1100_110A_0000), 37 | Bitboard::from_bits(0x0014_2200_2214_0000), 38 | Bitboard::from_bits(0x0028_4400_4428_0000), 39 | Bitboard::from_bits(0x0050_8800_8850_0000), 40 | Bitboard::from_bits(0x00A0_1000_10A0_0000), 41 | Bitboard::from_bits(0x0040_2000_2040_0000), 42 | Bitboard::from_bits(0x0204_0004_0200_0000), 43 | Bitboard::from_bits(0x0508_0008_0500_0000), 44 | Bitboard::from_bits(0x0A11_0011_0A00_0000), 45 | Bitboard::from_bits(0x1422_0022_1400_0000), 46 | Bitboard::from_bits(0x2844_0044_2800_0000), 47 | Bitboard::from_bits(0x5088_0088_5000_0000), 48 | Bitboard::from_bits(0xA010_0010_A000_0000), 49 | Bitboard::from_bits(0x4020_0020_4000_0000), 50 | Bitboard::from_bits(0x0400_0402_0000_0000), 51 | Bitboard::from_bits(0x0800_0805_0000_0000), 52 | Bitboard::from_bits(0x1100_110A_0000_0000), 53 | Bitboard::from_bits(0x2200_2214_0000_0000), 54 | Bitboard::from_bits(0x4400_4428_0000_0000), 55 | Bitboard::from_bits(0x8800_8850_0000_0000), 56 | Bitboard::from_bits(0x1000_10A0_0000_0000), 57 | Bitboard::from_bits(0x2000_2040_0000_0000), 58 | Bitboard::from_bits(0x0004_0200_0000_0000), 59 | Bitboard::from_bits(0x0008_0500_0000_0000), 60 | Bitboard::from_bits(0x0011_0A00_0000_0000), 61 | Bitboard::from_bits(0x0022_1400_0000_0000), 62 | Bitboard::from_bits(0x0044_2800_0000_0000), 63 | Bitboard::from_bits(0x0088_5000_0000_0000), 64 | Bitboard::from_bits(0x0010_A000_0000_0000), 65 | Bitboard::from_bits(0x0020_4000_0000_0000), 66 | ] 67 | -------------------------------------------------------------------------------- /generated/white_pawn_attacks.rs: -------------------------------------------------------------------------------- 1 | [ 2 | Bitboard::from_bits(0x0000_0000_0000_0000), 3 | Bitboard::from_bits(0x0000_0000_0000_0000), 4 | Bitboard::from_bits(0x0000_0000_0000_0000), 5 | Bitboard::from_bits(0x0000_0000_0000_0000), 6 | Bitboard::from_bits(0x0000_0000_0000_0000), 7 | Bitboard::from_bits(0x0000_0000_0000_0000), 8 | Bitboard::from_bits(0x0000_0000_0000_0000), 9 | Bitboard::from_bits(0x0000_0000_0000_0000), 10 | Bitboard::from_bits(0x0000_0000_0002_0000), 11 | Bitboard::from_bits(0x0000_0000_0005_0000), 12 | Bitboard::from_bits(0x0000_0000_000a_0000), 13 | Bitboard::from_bits(0x0000_0000_0014_0000), 14 | Bitboard::from_bits(0x0000_0000_0028_0000), 15 | Bitboard::from_bits(0x0000_0000_0050_0000), 16 | Bitboard::from_bits(0x0000_0000_00a0_0000), 17 | Bitboard::from_bits(0x0000_0000_0040_0000), 18 | Bitboard::from_bits(0x0000_0000_0200_0000), 19 | Bitboard::from_bits(0x0000_0000_0500_0000), 20 | Bitboard::from_bits(0x0000_0000_0a00_0000), 21 | Bitboard::from_bits(0x0000_0000_1400_0000), 22 | Bitboard::from_bits(0x0000_0000_2800_0000), 23 | Bitboard::from_bits(0x0000_0000_5000_0000), 24 | Bitboard::from_bits(0x0000_0000_a000_0000), 25 | Bitboard::from_bits(0x0000_0000_4000_0000), 26 | Bitboard::from_bits(0x0000_0002_0000_0000), 27 | Bitboard::from_bits(0x0000_0005_0000_0000), 28 | Bitboard::from_bits(0x0000_000a_0000_0000), 29 | Bitboard::from_bits(0x0000_0014_0000_0000), 30 | Bitboard::from_bits(0x0000_0028_0000_0000), 31 | Bitboard::from_bits(0x0000_0050_0000_0000), 32 | Bitboard::from_bits(0x0000_00a0_0000_0000), 33 | Bitboard::from_bits(0x0000_0040_0000_0000), 34 | Bitboard::from_bits(0x0000_0200_0000_0000), 35 | Bitboard::from_bits(0x0000_0500_0000_0000), 36 | Bitboard::from_bits(0x0000_0a00_0000_0000), 37 | Bitboard::from_bits(0x0000_1400_0000_0000), 38 | Bitboard::from_bits(0x0000_2800_0000_0000), 39 | Bitboard::from_bits(0x0000_5000_0000_0000), 40 | Bitboard::from_bits(0x0000_a000_0000_0000), 41 | Bitboard::from_bits(0x0000_4000_0000_0000), 42 | Bitboard::from_bits(0x0002_0000_0000_0000), 43 | Bitboard::from_bits(0x0005_0000_0000_0000), 44 | Bitboard::from_bits(0x000a_0000_0000_0000), 45 | Bitboard::from_bits(0x0014_0000_0000_0000), 46 | Bitboard::from_bits(0x0028_0000_0000_0000), 47 | Bitboard::from_bits(0x0050_0000_0000_0000), 48 | Bitboard::from_bits(0x00a0_0000_0000_0000), 49 | Bitboard::from_bits(0x0040_0000_0000_0000), 50 | Bitboard::from_bits(0x0200_0000_0000_0000), 51 | Bitboard::from_bits(0x0500_0000_0000_0000), 52 | Bitboard::from_bits(0x0a00_0000_0000_0000), 53 | Bitboard::from_bits(0x1400_0000_0000_0000), 54 | Bitboard::from_bits(0x2800_0000_0000_0000), 55 | Bitboard::from_bits(0x5000_0000_0000_0000), 56 | Bitboard::from_bits(0xa000_0000_0000_0000), 57 | Bitboard::from_bits(0x4000_0000_0000_0000), 58 | Bitboard::from_bits(0x0000_0000_0000_0000), 59 | Bitboard::from_bits(0x0000_0000_0000_0000), 60 | Bitboard::from_bits(0x0000_0000_0000_0000), 61 | Bitboard::from_bits(0x0000_0000_0000_0000), 62 | Bitboard::from_bits(0x0000_0000_0000_0000), 63 | Bitboard::from_bits(0x0000_0000_0000_0000), 64 | Bitboard::from_bits(0x0000_0000_0000_0000), 65 | Bitboard::from_bits(0x0000_0000_0000_0000), 66 | ] 67 | -------------------------------------------------------------------------------- /benches/chess.rs: -------------------------------------------------------------------------------- 1 | //! Criterion benchmarks measure time of move generation and perft calculation. 2 | 3 | use std::fs; 4 | 5 | use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; 6 | use pabi::chess::position::Position; 7 | 8 | fn generate_moves(positions: &[Position]) { 9 | for position in positions { 10 | std::hint::black_box(position.generate_moves()); 11 | } 12 | } 13 | 14 | fn load_positions() -> Vec { 15 | fs::read_to_string(concat!( 16 | env!("CARGO_MANIFEST_DIR"), 17 | "/tests/data/positions.fen" 18 | )) 19 | .unwrap() 20 | .lines() 21 | .map(|line| Position::try_from(line).unwrap()) 22 | .collect() 23 | } 24 | 25 | fn bench_movegen(c: &mut Criterion) { 26 | let mut group = c.benchmark_group("Move generation"); 27 | let positions = load_positions(); 28 | 29 | group.throughput(Throughput::Elements(positions.len() as u64)); 30 | group.bench_with_input( 31 | BenchmarkId::new( 32 | "movegen_pabi", 33 | format!("{} arbitrary positions", positions.len()), 34 | ), 35 | &positions, 36 | |b, positions| { 37 | b.iter(|| generate_moves(positions)); 38 | }, 39 | ); 40 | group.finish(); 41 | } 42 | 43 | criterion_group! { 44 | name = movegen; 45 | config = Criterion::default().sample_size(100); 46 | targets = bench_movegen 47 | } 48 | 49 | fn bench_perft(c: &mut Criterion) { 50 | let mut group = c.benchmark_group("perft"); 51 | 52 | let test_cases = vec![ 53 | (Position::starting(), 5, 4_865_609), 54 | (Position::starting(), 6, 119_060_324), 55 | ( 56 | Position::from_fen("8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1").unwrap(), 57 | 6, 58 | 11_030_083, 59 | ), 60 | ( 61 | Position::from_fen("r2q1rk1/pP1p2pp/Q4n2/bbp1p3/Np6/1B3NBn/pPPP1PPP/R3K2R b KQ - 0 1") 62 | .unwrap(), 63 | 6, 64 | 706_045_033, 65 | ), 66 | ( 67 | Position::from_fen( 68 | "r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10", 69 | ) 70 | .unwrap(), 71 | 5, 72 | 164_075_551, 73 | ), 74 | ( 75 | Position::from_fen("r1bqkbnr/pppppppp/2n5/8/3P4/8/PPP1PPPP/RNBQKBNR w KQkq - 1 2") 76 | .unwrap(), 77 | 6, 78 | 336_655_487, 79 | ), 80 | ( 81 | Position::from_fen("rnbqkbnr/pppppppp/8/8/8/N7/PPPPPPPP/R1BQKBNR b KQkq - 1 1") 82 | .unwrap(), 83 | 6, 84 | 120_142_144, 85 | ), 86 | ]; 87 | 88 | for (position, depth, nodes) in test_cases { 89 | group.throughput(Throughput::Elements(nodes)); 90 | group.bench_with_input( 91 | BenchmarkId::new( 92 | "perft", 93 | format!("position {position}, depth {depth}, nodes {nodes}"), 94 | ), 95 | &depth, 96 | |b, &depth| { 97 | b.iter(|| { 98 | assert_eq!(pabi::chess::position::perft(&position, depth), nodes); 99 | }); 100 | }, 101 | ); 102 | } 103 | group.finish(); 104 | } 105 | 106 | criterion_group! { 107 | name = perft; 108 | config = Criterion::default().sample_size(10); 109 | targets = bench_perft 110 | } 111 | 112 | criterion_main!(movegen, perft); 113 | -------------------------------------------------------------------------------- /src/chess/generated.rs: -------------------------------------------------------------------------------- 1 | /// Arrays and values generated at or before build time. 2 | use crate::chess::bitboard::Bitboard; 3 | use crate::chess::core::{BOARD_SIZE, Piece, Square}; 4 | use crate::chess::zobrist::Key; 5 | 6 | // All keys required for Zobrist hashing of a chess position. 7 | pub(super) const BLACK_TO_MOVE: Key = 0x9E06_BAD3_9D76_1293; 8 | 9 | pub(super) const WHITE_CAN_CASTLE_SHORT: Key = 0xF05A_C573_DD61_D323; 10 | pub(super) const WHITE_CAN_CASTLE_LONG: Key = 0x41D8_B55B_A5FE_B78B; 11 | 12 | pub(super) const BLACK_CAN_CASTLE_SHORT: Key = 0x6809_8878_7A43_D289; 13 | pub(super) const BLACK_CAN_CASTLE_LONG: Key = 0x2F94_1F8D_FD3E_3D1F; 14 | 15 | // NOTE: The following keys are randomly generated in build.rs and are not 16 | // stable even between different builds of the same version. 17 | pub(super) const EN_PASSANT_FILES: [Key; 8] = 18 | include!(concat!(env!("OUT_DIR"), "/en_passant_zobrist_keys")); 19 | 20 | const PIECES_ZOBRIST_KEYS: [Key; 768] = include!(concat!(env!("OUT_DIR"), "/pieces_zobrist_keys")); 21 | 22 | pub(super) fn get_piece_key(piece: Piece, square: Square) -> Key { 23 | const NUM_PIECES: usize = 6; 24 | PIECES_ZOBRIST_KEYS[piece.player as usize * NUM_PIECES * BOARD_SIZE as usize 25 | + piece.kind as usize * BOARD_SIZE as usize 26 | + square as usize] 27 | } 28 | 29 | // Move generation-related precomputed bitboards. 30 | const BISHOP_ATTACKS_COUNT: usize = 5248; 31 | pub(super) static BISHOP_ATTACKS: [Bitboard; BISHOP_ATTACKS_COUNT] = include!(concat!( 32 | env!("CARGO_MANIFEST_DIR"), 33 | "/generated/bishop_attacks.rs" 34 | )); 35 | pub(super) const BISHOP_ATTACK_OFFSETS: [usize; BOARD_SIZE as usize] = include!(concat!( 36 | env!("CARGO_MANIFEST_DIR"), 37 | "/generated/bishop_attack_offsets.rs" 38 | )); 39 | pub(super) const BISHOP_RELEVANT_OCCUPANCIES: [u64; BOARD_SIZE as usize] = include!(concat!( 40 | env!("CARGO_MANIFEST_DIR"), 41 | "/generated/bishop_relevant_occupancies.rs" 42 | )); 43 | 44 | const ROOK_ATTACKS_COUNT: usize = 102_400; 45 | pub(super) static ROOK_ATTACKS: [Bitboard; ROOK_ATTACKS_COUNT] = include!(concat!( 46 | env!("CARGO_MANIFEST_DIR"), 47 | "/generated/rook_attacks.rs" 48 | )); 49 | pub(super) const ROOK_RELEVANT_OCCUPANCIES: [u64; BOARD_SIZE as usize] = include!(concat!( 50 | env!("CARGO_MANIFEST_DIR"), 51 | "/generated/rook_relevant_occupancies.rs" 52 | )); 53 | pub(super) const ROOK_ATTACK_OFFSETS: [usize; BOARD_SIZE as usize] = include!(concat!( 54 | env!("CARGO_MANIFEST_DIR"), 55 | "/generated/rook_attack_offsets.rs" 56 | )); 57 | 58 | pub(super) static RAYS: [Bitboard; BOARD_SIZE as usize * BOARD_SIZE as usize] = 59 | include!(concat!(env!("CARGO_MANIFEST_DIR"), "/generated/rays.rs")); 60 | pub(super) static BISHOP_RAYS: [Bitboard; BOARD_SIZE as usize * BOARD_SIZE as usize] = include!( 61 | concat!(env!("CARGO_MANIFEST_DIR"), "/generated/bishop_rays.rs") 62 | ); 63 | pub(super) static ROOK_RAYS: [Bitboard; BOARD_SIZE as usize * BOARD_SIZE as usize] = include!( 64 | concat!(env!("CARGO_MANIFEST_DIR"), "/generated/rook_rays.rs") 65 | ); 66 | 67 | pub(super) const KNIGHT_ATTACKS: [Bitboard; BOARD_SIZE as usize] = include!(concat!( 68 | env!("CARGO_MANIFEST_DIR"), 69 | "/generated/knight_attacks.rs" 70 | )); 71 | pub(super) const KING_ATTACKS: [Bitboard; BOARD_SIZE as usize] = include!(concat!( 72 | env!("CARGO_MANIFEST_DIR"), 73 | "/generated/king_attacks.rs" 74 | )); 75 | pub(super) const WHITE_PAWN_ATTACKS: [Bitboard; BOARD_SIZE as usize] = include!(concat!( 76 | env!("CARGO_MANIFEST_DIR"), 77 | "/generated/white_pawn_attacks.rs" 78 | )); 79 | pub(super) const BLACK_PAWN_ATTACKS: [Bitboard; BOARD_SIZE as usize] = include!(concat!( 80 | env!("CARGO_MANIFEST_DIR"), 81 | "/generated/black_pawn_attacks.rs" 82 | )); 83 | -------------------------------------------------------------------------------- /src/engine/mod.rs: -------------------------------------------------------------------------------- 1 | //! The engine puts all pieces together and manages resources effectively. It 2 | //! implements the [Universal Chess Interface] (UCI) for communication with the 3 | //! client (e.g. tournament runner with other engines or GUI/Lichess endpoint). 4 | //! 5 | //! [`Engine::uci_loop`] is the "main loop" of the engine which communicates 6 | //! with the environment and executes commands from the input stream. 7 | /// [Universal Chess Interface]: https://www.chessprogramming.org/UCI 8 | use core::panic; 9 | use std::io::{BufRead, Write}; 10 | use std::time::Duration; 11 | 12 | use crate::chess::core::Move; 13 | use crate::chess::position::Position; 14 | use crate::engine::uci::Command; 15 | use crate::environment::Player; 16 | 17 | mod time_manager; 18 | mod uci; 19 | 20 | /// The Engine connects everything together and handles commands sent by UCI 21 | /// server. It is created when the program is started and implement the "main 22 | /// loop" via [`Engine::uci_loop`]. 23 | pub struct Engine<'a, R: BufRead, W: Write> { 24 | /// Next search will start from this position. 25 | position: Position, 26 | debug: bool, 27 | // TODO: time_manager, 28 | // TODO: transposition_table 29 | /// UCI commands will be read from this stream. 30 | input: &'a mut R, 31 | /// Responses to UCI commands will be written to this stream. 32 | out: &'a mut W, 33 | } 34 | 35 | impl<'a, R: BufRead, W: Write> Engine<'a, R, W> { 36 | /// Creates a new instance of the engine with the starting position as the 37 | /// search root. 38 | #[must_use] 39 | pub fn new(input: &'a mut R, out: &'a mut W) -> Self { 40 | Self { 41 | position: Position::starting(), 42 | debug: false, 43 | input, 44 | out, 45 | } 46 | } 47 | 48 | /// Continuously reads the input stream and executes sent UCI commands until 49 | /// "quit" is sent. 50 | /// 51 | /// The implementation here does not aim to be complete and exhaustive, 52 | /// because the main goal is to make the engine work in relatively simple 53 | /// setups, making it work with all UCI-compatible GUIs and corrupted input 54 | /// is not a priority. For supported commands and their options see 55 | /// [`Command`]. 56 | /// 57 | /// NOTE: The assumption is that the UCI input stream is **correct**. It is 58 | /// tournament manager's responsibility to send uncorrupted input and make 59 | /// sure that the commands are in valid format. The engine won't spend too 60 | /// much time and effort on error recovery. If a command is not valid or 61 | /// unsupported yet, it will just be skipped. 62 | /// 63 | /// For example, if the UCI server sends a corrupted position or illegal 64 | /// moves to the engine, the behavior is undefined. 65 | pub fn uci_loop(&mut self) -> anyhow::Result<()> { 66 | loop { 67 | let mut line = String::new(); 68 | match self.input.read_line(&mut line) { 69 | Ok(0) => break, 70 | Ok(_) => {} 71 | Err(e) => { 72 | panic!("Error reading from input: {}", e); 73 | } 74 | } 75 | match Command::parse(&line) { 76 | Command::Uci => self.handshake()?, 77 | Command::Debug { on } => self.debug = on, 78 | Command::IsReady => self.sync()?, 79 | Command::SetOption { option, value } => match option { 80 | uci::EngineOption::Hash => match value { 81 | uci::OptionValue::Integer(_) => todo!(), 82 | uci::OptionValue::String(value) => writeln!( 83 | self.out, 84 | "info string Invalid value for Hash option: {value}" 85 | )?, 86 | }, 87 | uci::EngineOption::Threads => todo!(), 88 | uci::EngineOption::SyzygyTablebase => todo!(), 89 | }, 90 | Command::SetPosition { fen, moves } => self.set_position(fen, moves)?, 91 | Command::NewGame => self.new_game()?, 92 | Command::Go { 93 | wtime, 94 | btime, 95 | winc, 96 | binc, 97 | } => self.go(wtime, btime, winc, binc)?, 98 | Command::Stop => self.stop_search()?, 99 | Command::Quit => { 100 | self.stop_search()?; 101 | break; 102 | } 103 | Command::State => todo!(), 104 | Command::Unknown(command) => { 105 | writeln!(self.out, "info string Unsupported command: {command}")?; 106 | } 107 | } 108 | } 109 | Ok(()) 110 | } 111 | 112 | /// Responds to the `uci` handshake command by identifying the engine. 113 | fn handshake(&mut self) -> anyhow::Result<()> { 114 | writeln!( 115 | self.out, 116 | "id name {} {}", 117 | env!("CARGO_PKG_NAME"), 118 | crate::engine_version() 119 | )?; 120 | writeln!(self.out, "id author {}", env!("CARGO_PKG_AUTHORS"))?; 121 | writeln!(self.out, "uciok")?; 122 | Ok(()) 123 | } 124 | 125 | /// Syncs with the UCI server by responding with `readyok`. 126 | fn sync(&mut self) -> anyhow::Result<()> { 127 | writeln!(self.out, "readyok")?; 128 | Ok(()) 129 | } 130 | 131 | fn new_game(&mut self) -> anyhow::Result<()> { 132 | // TODO: Reset search state. 133 | // TODO: Clear transposition table. 134 | // TODO: Reset time manager. 135 | Ok(()) 136 | } 137 | 138 | /// Changes the position of the board to the one specified in the command. 139 | fn set_position(&mut self, fen: Option, moves: Vec) -> anyhow::Result<()> { 140 | match fen { 141 | Some(fen) => self.position = Position::from_fen(&fen)?, 142 | None => self.position = Position::starting(), 143 | }; 144 | for next_move in moves { 145 | match Move::from_uci(&next_move) { 146 | Ok(next_move) => self.position.make_move(&next_move), 147 | Err(_) => unreachable!(), 148 | } 149 | } 150 | Ok(()) 151 | } 152 | 153 | fn go( 154 | &mut self, 155 | wtime: Option, 156 | btime: Option, 157 | winc: Option, 158 | binc: Option, 159 | ) -> anyhow::Result<()> { 160 | let (time, increment) = match self.position.us() { 161 | Player::White => (wtime, winc), 162 | Player::Black => (btime, binc), 163 | }; 164 | todo!(); 165 | } 166 | 167 | /// Stops the search immediately. 168 | /// 169 | /// NOTE: This is a no-op for now. 170 | fn stop_search(&mut self) -> anyhow::Result<()> { 171 | // TODO: Implement this method. 172 | Ok(()) 173 | } 174 | } 175 | 176 | /// Runs search on a small set of positions to provide an estimate of engine's 177 | /// performance. 178 | /// 179 | /// Implementing `bench` CLI command is a [requirement for OpenBench]. 180 | /// 181 | /// NOTE: This function **has to run less than 60 seconds**. Ideally, it should 182 | /// be just under 5 seconds. 183 | /// 184 | /// See for 185 | /// more details. 186 | /// 187 | /// [requirement for OpenBench]: https://github.com/AndyGrant/OpenBench/wiki/Requirements-For-Public-Engines#basic-requirements 188 | pub fn openbench() { 189 | todo!() 190 | } 191 | 192 | // TODO: Add extensive test suite for the UCI protocol implementation. 193 | -------------------------------------------------------------------------------- /src/chess/game.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use shakmaty::Chess; 4 | use shakmaty_syzygy::{AmbiguousWdl, Tablebase}; 5 | 6 | use super::core::{Move, MoveList}; 7 | use crate::chess::position::Position; 8 | use crate::chess::zobrist::RepetitionTable; 9 | use crate::environment::{Action, Environment, GameResult, Observation, Player}; 10 | 11 | impl Action for Move { 12 | // Action space compression from lc0: 13 | // https://github.com/LeelaChessZero/lc0/blob/master/src/chess/bitboard.cc 14 | fn get_index(&self) -> u16 { 15 | todo!(); 16 | } 17 | } 18 | 19 | impl Observation for Position {} 20 | 21 | pub struct Game { 22 | position: Position, 23 | perspective: Player, 24 | repetitions: RepetitionTable, 25 | moves: MoveList, 26 | tablebase: Tablebase, 27 | threefold_repetition: bool, 28 | } 29 | 30 | impl Game { 31 | pub(super) fn new(root: Position, tablebase_dir: &Path) -> Self { 32 | let mut repetitions = RepetitionTable::new(); 33 | let _ = repetitions.record(root.hash()); 34 | 35 | let perspective = root.us(); 36 | let moves = root.generate_moves(); 37 | 38 | Self { 39 | position: root, 40 | perspective, 41 | repetitions, 42 | moves, 43 | tablebase: read_tablebase(tablebase_dir), 44 | threefold_repetition: false, 45 | } 46 | } 47 | } 48 | 49 | impl Environment for Game { 50 | fn actions(&self) -> &[Move] { 51 | &self.moves 52 | } 53 | 54 | fn apply(&mut self, action: &Move) -> &Position { 55 | self.position.make_move(action); 56 | self.threefold_repetition = self.repetitions.record(self.position.hash()); 57 | self.moves = self.position.generate_moves(); 58 | &self.position 59 | } 60 | 61 | fn result(&self) -> Option { 62 | debug_assert!(self.position.num_pieces() >= self.tablebase.max_pieces()); 63 | 64 | if self.threefold_repetition { 65 | return Some(GameResult::Draw); 66 | } 67 | if self.position.halfmove_clock_expired() { 68 | return Some(GameResult::Draw); 69 | } 70 | if self.position.num_pieces() == self.tablebase.max_pieces() { 71 | // TODO: This is a bit of a hack right now and not precise. Maybe 72 | // it's not that inmportant, but worth revisiting. 73 | let wdl = self 74 | .tablebase 75 | .probe_wdl(&to_shakmaty_position(&self.position)) 76 | .unwrap(); 77 | match wdl { 78 | AmbiguousWdl::Win | AmbiguousWdl::MaybeWin => { 79 | return if self.perspective == self.position.us() { 80 | Some(GameResult::Win) 81 | } else { 82 | Some(GameResult::Loss) 83 | }; 84 | } 85 | AmbiguousWdl::Draw | AmbiguousWdl::BlessedLoss | AmbiguousWdl::CursedWin => { 86 | return Some(GameResult::Draw); 87 | } 88 | AmbiguousWdl::Loss | AmbiguousWdl::MaybeLoss => { 89 | return if self.perspective == self.position.us() { 90 | Some(GameResult::Loss) 91 | } else { 92 | Some(GameResult::Win) 93 | }; 94 | } 95 | } 96 | } 97 | if self.moves.is_empty() { 98 | // Stalemate. 99 | if !self.position.in_check() { 100 | return Some(GameResult::Draw); 101 | } 102 | // Player to move is in checkmate. 103 | return if self.perspective == self.position.us() { 104 | Some(GameResult::Loss) 105 | } else { 106 | Some(GameResult::Win) 107 | }; 108 | } 109 | None 110 | } 111 | } 112 | 113 | fn read_tablebase(path: &Path) -> Tablebase { 114 | let mut tablebase = Tablebase::new(); 115 | tablebase.add_directory(path).unwrap(); 116 | tablebase 117 | } 118 | 119 | // TODO: Converting to FEN and back is ineffective. It's possible to manipulate 120 | // the bitboard values directly. 121 | fn to_shakmaty_position(position: &Position) -> Chess { 122 | position 123 | .to_string() 124 | .parse::() 125 | .unwrap() 126 | .into_position(shakmaty::CastlingMode::Standard) 127 | .unwrap() 128 | } 129 | 130 | #[cfg(test)] 131 | mod tests { 132 | use super::*; 133 | 134 | const TABLEBASE_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/syzygy"); 135 | 136 | #[test] 137 | fn syzygy_tablebases() { 138 | let tables = read_tablebase(TABLEBASE_PATH.as_ref()); 139 | assert_eq!(tables.max_pieces(), 3); 140 | } 141 | 142 | #[test] 143 | fn detect_repetition() { 144 | let mut game = Game::new(Position::starting(), TABLEBASE_PATH.as_ref()); 145 | assert!(game.result().is_none()); 146 | // Move 1. 147 | game.apply(&Move::from_uci("g1f3").unwrap()); 148 | assert!(game.result().is_none()); 149 | game.apply(&Move::from_uci("g8f6").unwrap()); 150 | assert!(game.result().is_none()); 151 | // Move 2: returning to starting position. 152 | game.apply(&Move::from_uci("f3g1").unwrap()); 153 | assert!(game.result().is_none()); 154 | game.apply(&Move::from_uci("f6g8").unwrap()); 155 | assert!(game.result().is_none()); 156 | // Move 3. 157 | game.apply(&Move::from_uci("g1f3").unwrap()); 158 | assert!(game.result().is_none()); 159 | game.apply(&Move::from_uci("g8f6").unwrap()); 160 | assert!(game.result().is_none()); 161 | // Move 4: returning to starting position with threefold repetition. 162 | game.apply(&Move::from_uci("f3g1").unwrap()); 163 | assert!(game.result().is_none()); 164 | game.apply(&Move::from_uci("f6g8").unwrap()); 165 | assert_eq!(game.result(), Some(GameResult::Draw)); 166 | } 167 | 168 | #[test] 169 | fn tablebase_adjudication() { 170 | // KQvKR position with a forced win for white. 171 | let mut game = Game::new( 172 | Position::from_fen("4k3/8/8/5r2/4KQ2/8/8/8 w - - 0 1").expect("valid_position"), 173 | TABLEBASE_PATH.as_ref(), 174 | ); 175 | // Test tablebases only support 3 pieces, so it will not be adjudicated 176 | // until the rook is captured. 177 | assert!(game.result().is_none()); 178 | 179 | // KQvK is a win after Qxg5 (rook capture). 180 | game.apply(&Move::from_uci("f4f5").unwrap()); 181 | assert_eq!(game.position.to_string(), "4k3/8/8/5Q2/4K3/8/8/8 b - - 0 1"); 182 | // Black is to move, but the game is evaluated from white's perspective at root. 183 | assert_eq!(game.perspective, Player::White); 184 | assert_eq!(game.result(), Some(GameResult::Win)); 185 | } 186 | 187 | #[test] 188 | fn stalemate() { 189 | let mut game = Game::new( 190 | Position::from_fen("3b2qk/p6p/1p3Q1P/8/8/n7/PP6/K7 b - - 3 2").expect("valid_position"), 191 | TABLEBASE_PATH.as_ref(), 192 | ); 193 | assert!(game.result().is_none()); 194 | 195 | // Black has no moves and is not in check. 196 | game.apply(&Move::from_uci("d8f6").unwrap()); 197 | assert!(game.moves.is_empty()); 198 | assert_eq!(game.result(), Some(GameResult::Draw)); 199 | } 200 | 201 | #[test] 202 | fn checkmate() { 203 | let mut game = Game::new( 204 | Position::from_fen("3b3k/p5qp/1p3Q1P/8/8/n7/PP6/K7 w - - 4 3").expect("valid_position"), 205 | TABLEBASE_PATH.as_ref(), 206 | ); 207 | assert!(game.result().is_none()); 208 | 209 | game.apply(&Move::from_uci("f6g7").unwrap()); 210 | assert!(game.moves.is_empty()); 211 | assert_eq!(game.result(), Some(GameResult::Win)); 212 | } 213 | 214 | #[test] 215 | fn fifty_move_rule() { 216 | // All legal moves are just moving the kings back and forth, the 217 | // halfmove clock expires on the next turn. 218 | let mut game = Game::new( 219 | Position::from_fen("8/5k2/3p4/1p1Pp2p/pP2Pp1P/P4P1K/8/8 b - - 99 50") 220 | .expect("valid_position"), 221 | TABLEBASE_PATH.as_ref(), 222 | ); 223 | assert!(game.result().is_none()); 224 | 225 | game.apply(&Move::from_uci("f7f6").unwrap()); 226 | assert_eq!(game.result(), Some(GameResult::Draw)); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/engine/uci.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | #[derive(Debug, PartialEq)] 4 | pub(super) enum Command { 5 | Uci, 6 | Debug { 7 | on: bool, 8 | }, 9 | IsReady, 10 | SetOption { 11 | option: EngineOption, 12 | value: OptionValue, 13 | }, 14 | SetPosition { 15 | fen: Option, 16 | moves: Vec, 17 | }, 18 | NewGame, 19 | Go { 20 | wtime: Option, 21 | btime: Option, 22 | winc: Option, 23 | binc: Option, 24 | }, 25 | Stop, 26 | Quit, 27 | /// This is an extension to the UCI protocol useful for debugging. The 28 | /// response will contain the static evaluation of the current position and 29 | /// the engine internal state (current settings, search options, 30 | /// transposition table information and so on). 31 | State, 32 | Unknown(String), 33 | } 34 | 35 | #[derive(Debug, PartialEq)] 36 | pub(super) enum EngineOption { 37 | Hash, 38 | SyzygyTablebase, 39 | Threads, 40 | } 41 | 42 | #[derive(Debug, PartialEq)] 43 | pub(super) enum OptionValue { 44 | Integer(usize), 45 | String(String), 46 | } 47 | 48 | fn parse_go(parts: &[&str]) -> Command { 49 | let mut wtime = None; 50 | let mut btime = None; 51 | let mut winc = None; 52 | let mut binc = None; 53 | 54 | let mut i = 1; 55 | 56 | while i < parts.len() { 57 | match parts[i] { 58 | "wtime" if i + 1 < parts.len() => { 59 | wtime = parts[i + 1].parse().map(Duration::from_micros).ok(); 60 | } 61 | "btime" if i + 1 < parts.len() => { 62 | btime = parts[i + 1].parse().map(Duration::from_micros).ok(); 63 | } 64 | "winc" if i + 1 < parts.len() => { 65 | winc = parts[i + 1].parse().map(Duration::from_micros).ok(); 66 | } 67 | "binc" if i + 1 < parts.len() => { 68 | binc = parts[i + 1].parse().map(Duration::from_micros).ok(); 69 | } 70 | _ => {} 71 | } 72 | if parts[i] == "infinite" { 73 | i += 1; 74 | } else { 75 | i += 2; 76 | } 77 | } 78 | 79 | Command::Go { 80 | wtime, 81 | btime, 82 | winc, 83 | binc, 84 | } 85 | } 86 | 87 | fn parse_setoption(parts: &[&str]) -> Command { 88 | if parts.len() > 3 && parts[1] == "name" { 89 | let name_end = parts 90 | .iter() 91 | .position(|&x| x == "value") 92 | .unwrap_or(parts.len()); 93 | let option = parts[2..name_end].join(" "); 94 | let option = match option.as_str() { 95 | "Hash" => EngineOption::Hash, 96 | "SyzygyTablebase" => EngineOption::SyzygyTablebase, 97 | "Threads" => EngineOption::Threads, 98 | _ => return Command::Unknown(parts.join(" ")), 99 | }; 100 | let value = if name_end < parts.len() { 101 | match option { 102 | EngineOption::Hash | EngineOption::Threads => parts[name_end + 1] 103 | .parse::() 104 | .ok() 105 | .map(OptionValue::Integer), 106 | EngineOption::SyzygyTablebase => { 107 | Some(OptionValue::String(parts[name_end + 1..].join(" "))) 108 | } 109 | } 110 | } else { 111 | None 112 | }; 113 | if let Some(value) = value { 114 | Command::SetOption { option, value } 115 | } else { 116 | Command::Unknown(parts.join(" ")) 117 | } 118 | } else { 119 | Command::Unknown(parts.join(" ")) 120 | } 121 | } 122 | 123 | fn parse_setposition(parts: &[&str]) -> Command { 124 | let fen_index = parts.iter().position(|&x| x == "fen"); 125 | let moves_index = parts.iter().position(|&x| x == "moves"); 126 | let fen = fen_index.map(|index| parts[index + 1..moves_index.unwrap_or(parts.len())].join(" ")); 127 | let moves = if let Some(moves_index) = moves_index { 128 | parts[moves_index + 1..] 129 | .iter() 130 | .map(|s| (*s).to_string()) 131 | .collect() 132 | } else { 133 | vec![] 134 | }; 135 | Command::SetPosition { fen, moves } 136 | } 137 | 138 | impl Command { 139 | pub(super) fn parse(input: &str) -> Self { 140 | let parts: Vec<&str> = input.split_whitespace().collect(); 141 | 142 | if parts.is_empty() { 143 | return Self::Unknown(input.to_string()); 144 | } 145 | 146 | match parts[0] { 147 | "uci" => Self::Uci, 148 | "debug" if parts.len() > 1 => Self::Debug { 149 | on: parts[1] == "on", 150 | }, 151 | "isready" => Self::IsReady, 152 | "setoption" => parse_setoption(&parts), 153 | "position" => parse_setposition(&parts), 154 | "ucinewgame" => Self::NewGame, 155 | "go" => parse_go(&parts), 156 | "stop" => Self::Stop, 157 | "quit" => Self::Quit, 158 | "state" => Self::State, 159 | _ => Self::Unknown(input.to_string()), 160 | } 161 | } 162 | } 163 | 164 | #[cfg(test)] 165 | mod tests { 166 | use super::*; 167 | 168 | #[test] 169 | fn parse_uci() { 170 | assert_eq!(Command::parse("uci"), Command::Uci); 171 | } 172 | 173 | #[test] 174 | fn parse_debug() { 175 | assert_eq!(Command::parse("debug on"), Command::Debug { on: true }); 176 | assert_eq!(Command::parse("debug off"), Command::Debug { on: false }); 177 | } 178 | 179 | #[test] 180 | fn parse_isready() { 181 | assert_eq!(Command::parse("isready"), Command::IsReady); 182 | } 183 | 184 | #[test] 185 | fn parse_setoption() { 186 | assert_eq!( 187 | Command::parse("setoption name Hash value 128"), 188 | Command::SetOption { 189 | option: EngineOption::Hash, 190 | value: OptionValue::Integer(128) 191 | } 192 | ); 193 | assert_eq!( 194 | Command::parse("setoption name SyzygyTablebase value /path/to/tablebase"), 195 | Command::SetOption { 196 | option: EngineOption::SyzygyTablebase, 197 | value: OptionValue::String("/path/to/tablebase".to_string()) 198 | } 199 | ); 200 | assert_eq!( 201 | Command::parse("setoption name Threads value 4"), 202 | Command::SetOption { 203 | option: EngineOption::Threads, 204 | value: OptionValue::Integer(4) 205 | } 206 | ); 207 | assert_eq!( 208 | Command::parse("setoption name InvalidOption value 123"), 209 | Command::Unknown("setoption name InvalidOption value 123".to_string()) 210 | ); 211 | } 212 | 213 | #[test] 214 | fn parse_position() { 215 | assert_eq!( 216 | Command::parse("position startpos moves e2e4 e7e5"), 217 | Command::SetPosition { 218 | fen: None, 219 | moves: vec!["e2e4".to_string(), "e7e5".to_string()] 220 | } 221 | ); 222 | assert_eq!( 223 | Command::parse( 224 | "position fen rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 moves e2e4 e7e5" 225 | ), 226 | Command::SetPosition { 227 | fen: Some("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1".to_string()), 228 | moves: vec!["e2e4".to_string(), "e7e5".to_string()] 229 | } 230 | ); 231 | } 232 | 233 | #[test] 234 | fn ucinewgame() { 235 | assert_eq!(Command::parse("ucinewgame"), Command::NewGame); 236 | } 237 | 238 | #[test] 239 | fn parse_go() { 240 | assert_eq!( 241 | Command::parse("go wtime 300000 btime 300000 winc 10000 binc 10000"), 242 | Command::Go { 243 | wtime: Some(Duration::from_micros(300_000)), 244 | btime: Some(Duration::from_micros(300_000)), 245 | winc: Some(Duration::from_micros(10000)), 246 | binc: Some(Duration::from_micros(10000)), 247 | } 248 | ); 249 | 250 | assert_eq!( 251 | Command::parse("go wtime 1000"), 252 | Command::Go { 253 | wtime: Some(Duration::from_micros(1000)), 254 | btime: None, 255 | winc: None, 256 | binc: None, 257 | } 258 | ); 259 | } 260 | 261 | #[test] 262 | fn parse_stop() { 263 | assert_eq!(Command::parse("stop"), Command::Stop); 264 | } 265 | 266 | #[test] 267 | fn parse_quit() { 268 | assert_eq!(Command::parse("quit"), Command::Quit); 269 | } 270 | 271 | #[test] 272 | fn parse_state() { 273 | assert_eq!(Command::parse("state"), Command::State); 274 | } 275 | 276 | #[test] 277 | fn unknown() { 278 | assert_eq!( 279 | Command::parse("unknown command"), 280 | Command::Unknown("unknown command".to_string()) 281 | ); 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /src/chess/bitboard.rs: -------------------------------------------------------------------------------- 1 | //! [`Bitboard`]-based representation for [`crate::chess::position::Position`]. 2 | //! [Bitboards] utilize the fact that modern processors operate on 64 bit 3 | //! integers, and the bit operations can be performed simultaneously. This 4 | //! results in very efficient calculation of possible attack vectors and other 5 | //! meaningful features that are required to calculate possible moves and 6 | //! evaluate position. The disadvantage is complexity that comes with bitboard 7 | //! implementation and inefficiency of some operations like "get piece type on 8 | //! given square" (efficiently handled by Square-centric board implementations 9 | //! that can be used together bitboard-based approach to compensate its 10 | //! shortcomings). 11 | //! 12 | //! The implementation is based on [PEXT Bitboards] idea, which is an 13 | //! improvement over Fancy Magic Bitboards. 14 | //! 15 | //! For visualizing and debugging the bitboards, there is a [BitboardCalculator] 16 | //! tool. 17 | //! 18 | //! [Bitboards]: https://www.chessprogramming.org/Bitboards 19 | //! [BitboardCalculator]: https://gekomad.github.io/Cinnamon/BitboardCalculator/ 20 | //! [PEXT Bitboards]: https://www.chessprogramming.org/BMI2#PEXTBitboards 21 | 22 | use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, Not, Shl, Shr, Sub, SubAssign}; 23 | use std::{fmt, mem}; 24 | 25 | use itertools::Itertools; 26 | 27 | use crate::chess::core::{BOARD_SIZE, BOARD_WIDTH, Direction, PieceKind, Square}; 28 | use crate::environment::Player; 29 | 30 | /// Represents a set of squares and provides common operations (e.g. AND, OR, 31 | /// XOR) over these sets. Each bit corresponds to one of 64 squares of the chess 32 | /// board. 33 | /// 34 | /// Mirroring [`Square`] semantics, the least significant bit corresponds to 35 | /// [`Square::A1`], and the most significant bit - to [`Square::H8`]. 36 | /// 37 | /// Bitboard is a thin wrapper around [u64]: 38 | /// 39 | /// ``` 40 | /// use std::mem::size_of; 41 | /// 42 | /// use pabi::chess::bitboard::Bitboard; 43 | /// 44 | /// assert_eq!(size_of::(), 8); 45 | /// ``` 46 | #[derive(Copy, Clone, PartialEq, Eq)] 47 | pub struct Bitboard { 48 | bits: u64, 49 | } 50 | 51 | impl Bitboard { 52 | /// Constructs Bitboard from pre-calculated bits. 53 | #[must_use] 54 | pub const fn from_bits(bits: u64) -> Self { 55 | Self { bits } 56 | } 57 | 58 | /// Constructs a bitboard representing empty set of squares. 59 | #[must_use] 60 | pub const fn empty() -> Self { 61 | Self::from_bits(0) 62 | } 63 | 64 | /// Constructs a bitboard representing the universal set, it contains all 65 | /// squares by setting all bits to binary one. 66 | #[must_use] 67 | pub const fn full() -> Self { 68 | Self::from_bits(u64::MAX) 69 | } 70 | 71 | /// Returns raw bits. 72 | #[must_use] 73 | pub const fn bits(self) -> u64 { 74 | self.bits 75 | } 76 | 77 | #[must_use] 78 | pub fn from_squares(squares: &[Square]) -> Self { 79 | let mut bits = 0u64; 80 | for &square in squares { 81 | bits |= 1u64 << (square as u8); 82 | } 83 | Self::from_bits(bits) 84 | } 85 | 86 | /// Adds given square to the set. 87 | pub(super) fn extend(&mut self, square: Square) { 88 | *self |= Self::from(square); 89 | } 90 | 91 | /// Clears given square from the set. 92 | pub(super) fn clear(&mut self, square: Square) { 93 | *self &= !Self::from(square); 94 | } 95 | 96 | /// Returns true if this bitboard contains given square. 97 | #[must_use] 98 | pub(super) const fn contains(self, square: Square) -> bool { 99 | (self.bits & (1u64 << square as u8)) != 0 100 | } 101 | 102 | #[must_use] 103 | pub(super) const fn as_square(self) -> Square { 104 | debug_assert!(self.bits.count_ones() == 1); 105 | unsafe { mem::transmute(self.bits.trailing_zeros() as u8) } 106 | } 107 | 108 | #[must_use] 109 | pub(crate) const fn count(self) -> u32 { 110 | self.bits.count_ones() 111 | } 112 | 113 | #[must_use] 114 | pub(super) const fn is_empty(self) -> bool { 115 | self.bits == 0 116 | } 117 | 118 | #[must_use] 119 | pub(super) const fn has_any(self) -> bool { 120 | self.bits != 0 121 | } 122 | 123 | #[must_use] 124 | pub(super) fn shift(self, direction: Direction) -> Self { 125 | match direction { 126 | Direction::Up => self << u32::from(BOARD_WIDTH), 127 | Direction::Down => self >> u32::from(BOARD_WIDTH), 128 | } 129 | } 130 | 131 | /// Flips the bitboard vertically. 132 | /// 133 | /// This is useful when we want to switch between the board point of view of 134 | /// White and Black. 135 | /// 136 | /// # Example 137 | /// 138 | /// ``` 139 | /// use pabi::chess::bitboard::Bitboard; 140 | /// 141 | /// let bb = Bitboard::from_bits(0x1E2222120E0A1222); 142 | /// assert_eq!( 143 | /// format!("{:?}", bb), 144 | /// ". 1 1 1 1 . . .\n\ 145 | /// . 1 . . . 1 . .\n\ 146 | /// . 1 . . . 1 . .\n\ 147 | /// . 1 . . 1 . . .\n\ 148 | /// . 1 1 1 . . . .\n\ 149 | /// . 1 . 1 . . . .\n\ 150 | /// . 1 . . 1 . . .\n\ 151 | /// . 1 . . . 1 . ." 152 | /// ); 153 | /// assert_eq!( 154 | /// format!("{:?}", bb.flip_perspective()), 155 | /// ". 1 . . . 1 . .\n\ 156 | /// . 1 . . 1 . . .\n\ 157 | /// . 1 . 1 . . . .\n\ 158 | /// . 1 1 1 . . . .\n\ 159 | /// . 1 . . 1 . . .\n\ 160 | /// . 1 . . . 1 . .\n\ 161 | /// . 1 . . . 1 . .\n\ 162 | /// . 1 1 1 1 . . ." 163 | /// ); 164 | /// ``` 165 | #[must_use] 166 | pub fn flip_perspective(&self) -> Self { 167 | Self::from_bits(self.bits.swap_bytes()) 168 | } 169 | 170 | /// An efficient way to iterate over the set squares. 171 | #[must_use] 172 | pub(super) const fn iter(self) -> BitboardIterator { 173 | BitboardIterator { bits: self.bits } 174 | } 175 | } 176 | 177 | impl fmt::Debug for Bitboard { 178 | /// The board is printed from A1 to H8, starting from bottom left corner to 179 | /// the top right corner, just like on the normal chess board from the 180 | /// perspective of White. 181 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 182 | const LINE_SEPARATOR: &str = "\n"; 183 | const SQUARE_SEPARATOR: &str = " "; 184 | write!( 185 | f, 186 | "{}", 187 | format!("{:#066b}", self.bits) 188 | .chars() 189 | .rev() 190 | .take(BOARD_SIZE as usize) 191 | .chunks(BOARD_WIDTH as usize) 192 | .into_iter() 193 | .map(|chunk| chunk 194 | .map(|ch| match ch { 195 | '1' => '1', 196 | '0' => '.', 197 | _ => unreachable!(), 198 | }) 199 | .join(SQUARE_SEPARATOR)) 200 | .collect::>() 201 | .iter() 202 | .rev() 203 | .join(LINE_SEPARATOR) 204 | ) 205 | } 206 | } 207 | 208 | impl BitOr for Bitboard { 209 | type Output = Self; 210 | 211 | fn bitor(self, rhs: Self) -> Self::Output { 212 | Self::from_bits(self.bits.bitor(rhs.bits)) 213 | } 214 | } 215 | 216 | impl BitOrAssign for Bitboard { 217 | fn bitor_assign(&mut self, rhs: Self) { 218 | self.bits.bitor_assign(rhs.bits); 219 | } 220 | } 221 | 222 | impl BitAnd for Bitboard { 223 | type Output = Self; 224 | 225 | fn bitand(self, rhs: Self) -> Self::Output { 226 | Self::from_bits(self.bits.bitand(rhs.bits)) 227 | } 228 | } 229 | 230 | impl BitAndAssign for Bitboard { 231 | fn bitand_assign(&mut self, rhs: Self) { 232 | self.bits.bitand_assign(rhs.bits); 233 | } 234 | } 235 | 236 | impl BitXor for Bitboard { 237 | type Output = Self; 238 | 239 | fn bitxor(self, rhs: Self) -> Self::Output { 240 | Self::from_bits(self.bits.bitxor(rhs.bits)) 241 | } 242 | } 243 | 244 | impl Sub for Bitboard { 245 | type Output = Self; 246 | 247 | /// [Relative component], i.e. Result = LHS \ RHS. 248 | /// 249 | /// [Relative component]: https://en.wikipedia.org/wiki/Complement_%28set_theory%29#Relative_complement 250 | fn sub(self, rhs: Self) -> Self::Output { 251 | self & !rhs 252 | } 253 | } 254 | 255 | impl SubAssign for Bitboard { 256 | fn sub_assign(&mut self, rhs: Self) { 257 | self.bitand_assign(!rhs); 258 | } 259 | } 260 | 261 | impl Not for Bitboard { 262 | type Output = Self; 263 | 264 | /// Returns [complement 265 | /// set](https://en.wikipedia.org/wiki/Complement_%28set_theory%29) of Self, 266 | /// i.e. flipping the set squares to unset and vice versa. 267 | fn not(self) -> Self::Output { 268 | Self::from_bits(!self.bits) 269 | } 270 | } 271 | 272 | impl Shl for Bitboard { 273 | type Output = Self; 274 | 275 | /// Shifts the bits to the left and ignores overflow. 276 | fn shl(self, rhs: u32) -> Self::Output { 277 | let (bits, _) = self.bits.overflowing_shl(rhs); 278 | Self::from_bits(bits) 279 | } 280 | } 281 | 282 | impl Shr for Bitboard { 283 | type Output = Self; 284 | 285 | /// Shifts the bits to the right and ignores overflow. 286 | fn shr(self, rhs: u32) -> Self::Output { 287 | let (bits, _) = self.bits.overflowing_shr(rhs); 288 | Self::from_bits(bits) 289 | } 290 | } 291 | 292 | impl From for Bitboard { 293 | fn from(square: Square) -> Self { 294 | Self::from_bits(1u64 << square as u8) 295 | } 296 | } 297 | 298 | /// Iterates over set squares in a given [Bitboard] from least significant 1 299 | /// bits (LS1B) to most significant 1 bits (MS1B) through implementing 300 | /// [bitscan] forward operation. 301 | /// 302 | /// [bitscan]: https://www.chessprogramming.org/BitScan 303 | pub(super) struct BitboardIterator { 304 | bits: u64, 305 | } 306 | 307 | impl Iterator for BitboardIterator { 308 | type Item = Square; 309 | 310 | fn next(&mut self) -> Option { 311 | if self.bits == 0 { 312 | return None; 313 | } 314 | // Get the LS1B and consume it from the iterator. 315 | let next_index = self.bits.trailing_zeros(); 316 | // Clear LS1B. 317 | self.bits &= self.bits - 1; 318 | // For performance reasons, it's better to convert directly: the 319 | // conversion is safe because trailing_zeros() will return a number in 320 | // the 0..64 range. 321 | Some(unsafe { mem::transmute(next_index as u8) }) 322 | } 323 | } 324 | 325 | impl ExactSizeIterator for BitboardIterator { 326 | fn len(&self) -> usize { 327 | self.bits.count_ones() as usize 328 | } 329 | } 330 | 331 | impl TryInto for Bitboard { 332 | type Error = anyhow::Error; 333 | 334 | fn try_into(self) -> anyhow::Result { 335 | if self.bits.count_ones() != 1 { 336 | anyhow::bail!( 337 | "bitboard should contain exactly 1 bit, got {}", 338 | self.bits.count_ones() 339 | ); 340 | } 341 | Ok(unsafe { mem::transmute(self.bits.trailing_zeros() as u8) }) 342 | } 343 | } 344 | 345 | /// Piece-centric representation of all material owned by one player. Uses 346 | /// [Bitboard] to store a set of squares occupied by each piece. The main user 347 | /// is [`crate::chess::position::Position`], [Bitboard] is not very useful on 348 | /// its own. 349 | #[derive(Clone, PartialEq, Eq)] 350 | pub(crate) struct Pieces { 351 | pub(super) king: Bitboard, 352 | pub(super) queens: Bitboard, 353 | pub(super) rooks: Bitboard, 354 | pub(super) bishops: Bitboard, 355 | pub(super) knights: Bitboard, 356 | pub(super) pawns: Bitboard, 357 | } 358 | 359 | impl Pieces { 360 | pub(super) const fn empty() -> Self { 361 | Self { 362 | king: Bitboard::empty(), 363 | queens: Bitboard::empty(), 364 | rooks: Bitboard::empty(), 365 | bishops: Bitboard::empty(), 366 | knights: Bitboard::empty(), 367 | pawns: Bitboard::empty(), 368 | } 369 | } 370 | 371 | pub(super) fn starting(player: Player) -> Self { 372 | match player { 373 | Player::White => Self { 374 | king: Square::E1.into(), 375 | queens: Square::D1.into(), 376 | rooks: Bitboard::from_squares(&[Square::A1, Square::H1]), 377 | bishops: Bitboard::from_squares(&[Square::C1, Square::F1]), 378 | knights: Bitboard::from_squares(&[Square::B1, Square::G1]), 379 | pawns: Bitboard::from_squares(&[ 380 | Square::A2, 381 | Square::B2, 382 | Square::C2, 383 | Square::D2, 384 | Square::E2, 385 | Square::F2, 386 | Square::G2, 387 | Square::H2, 388 | ]), 389 | }, 390 | Player::Black => Self { 391 | king: Square::E8.into(), 392 | queens: Square::D8.into(), 393 | rooks: Bitboard::from_squares(&[Square::A8, Square::H8]), 394 | bishops: Bitboard::from_squares(&[Square::C8, Square::F8]), 395 | knights: Bitboard::from_squares(&[Square::B8, Square::G8]), 396 | pawns: Bitboard::from_squares(&[ 397 | Square::A7, 398 | Square::B7, 399 | Square::C7, 400 | Square::D7, 401 | Square::E7, 402 | Square::F7, 403 | Square::G7, 404 | Square::H7, 405 | ]), 406 | }, 407 | } 408 | } 409 | 410 | #[must_use] 411 | pub(super) fn all(&self) -> Bitboard { 412 | self.king | self.queens | self.rooks | self.bishops | self.knights | self.pawns 413 | } 414 | 415 | #[must_use] 416 | pub(super) fn bitboard_for_mut(&mut self, piece: PieceKind) -> &mut Bitboard { 417 | match piece { 418 | PieceKind::King => &mut self.king, 419 | PieceKind::Queen => &mut self.queens, 420 | PieceKind::Rook => &mut self.rooks, 421 | PieceKind::Bishop => &mut self.bishops, 422 | PieceKind::Knight => &mut self.knights, 423 | PieceKind::Pawn => &mut self.pawns, 424 | } 425 | } 426 | 427 | #[must_use] 428 | pub(super) fn at(&self, square: Square) -> Option { 429 | // Early exit if square is empty 430 | let square_bit = Bitboard::from(square); 431 | if !(self.all() & square_bit).has_any() { 432 | return None; 433 | } 434 | 435 | // Check piece types in order of likelihood (for typical chess positions) 436 | if (self.pawns & square_bit).has_any() { 437 | Some(PieceKind::Pawn) 438 | } else if (self.rooks & square_bit).has_any() { 439 | Some(PieceKind::Rook) 440 | } else if (self.knights & square_bit).has_any() { 441 | Some(PieceKind::Knight) 442 | } else if (self.bishops & square_bit).has_any() { 443 | Some(PieceKind::Bishop) 444 | } else if (self.queens & square_bit).has_any() { 445 | Some(PieceKind::Queen) 446 | } else { 447 | Some(PieceKind::King) 448 | } 449 | } 450 | } 451 | 452 | #[cfg(test)] 453 | mod tests { 454 | use pretty_assertions::assert_eq; 455 | 456 | use super::*; 457 | use crate::chess::core::{BOARD_WIDTH, Rank, Square}; 458 | 459 | #[test] 460 | fn basics() { 461 | assert_eq!(Bitboard::full().bits, u64::MAX); 462 | assert_eq!(Bitboard::empty().bits, u64::MIN); 463 | 464 | assert_eq!(Bitboard::from(Square::A1).bits, 1); 465 | assert_eq!(Bitboard::from(Square::B1).bits, 2); 466 | assert_eq!(Bitboard::from(Square::D1).bits, 8); 467 | assert_eq!(Bitboard::from(Square::H8).bits, 1u64 << 63); 468 | 469 | assert_eq!( 470 | Bitboard::from(Square::D1) | Bitboard::from(Square::B1), 471 | Bitboard::from_bits(0b10 | 0b1000) 472 | ); 473 | } 474 | 475 | #[test] 476 | fn set_basics() { 477 | // Create a starting position. 478 | let white = Pieces::starting(Player::White); 479 | let black = Pieces::starting(Player::Black); 480 | 481 | // Check that each player has 16 pieces. 482 | assert_eq!(white.all().bits.count_ones(), 16); 483 | assert_eq!(black.all().bits.count_ones(), 16); 484 | // Check that each player has correct number of pieces (previous check 485 | // was not enough to confirm there are no overlaps). 486 | assert_eq!(white.king.bits.count_ones(), 1); 487 | assert_eq!(black.king.bits.count_ones(), 1); 488 | assert_eq!(white.queens.bits.count_ones(), 1); 489 | assert_eq!(black.queens.bits.count_ones(), 1); 490 | assert_eq!(white.rooks.bits.count_ones(), 2); 491 | assert_eq!(black.rooks.bits.count_ones(), 2); 492 | assert_eq!(white.bishops.bits.count_ones(), 2); 493 | assert_eq!(black.bishops.bits.count_ones(), 2); 494 | assert_eq!(white.knights.bits.count_ones(), 2); 495 | assert_eq!(black.knights.bits.count_ones(), 2); 496 | assert_eq!(white.pawns.bits.count_ones(), 8); 497 | assert_eq!(black.pawns.bits.count_ones(), 8); 498 | 499 | // Check few positions manually. 500 | assert_eq!(white.queens.bits, 1 << 3); 501 | assert_eq!(black.queens.bits, 1 << (3 + 8 * 7)); 502 | 503 | // Rank masks. 504 | assert_eq!( 505 | Rank::Rank1.mask() << u32::from(BOARD_WIDTH), 506 | Rank::Rank2.mask() 507 | ); 508 | assert_eq!( 509 | Rank::Rank5.mask() >> u32::from(BOARD_WIDTH), 510 | Rank::Rank4.mask() 511 | ); 512 | } 513 | 514 | #[test] 515 | fn bitboard_iterator() { 516 | let white = Pieces::starting(Player::White); 517 | 518 | let mut it = white.king.iter(); 519 | assert_eq!(it.next(), Some(Square::E1)); 520 | assert!(it.next().is_none()); 521 | 522 | let mut it = white.bishops.iter(); 523 | assert_eq!(it.next(), Some(Square::C1)); 524 | assert_eq!(it.next(), Some(Square::F1)); 525 | assert!(it.next().is_none()); 526 | 527 | // The order is important here: we are iterating from least significant 528 | // bits to most significant bits. 529 | assert_eq!( 530 | white.pawns.iter().collect::>(), 531 | vec![ 532 | Square::A2, 533 | Square::B2, 534 | Square::C2, 535 | Square::D2, 536 | Square::E2, 537 | Square::F2, 538 | Square::G2, 539 | Square::H2, 540 | ] 541 | ); 542 | } 543 | 544 | #[test] 545 | fn set_ops() { 546 | let bb = Bitboard::from_squares(&[ 547 | Square::A1, 548 | Square::B1, 549 | Square::C1, 550 | Square::D1, 551 | Square::E1, 552 | Square::F1, 553 | Square::H1, 554 | Square::A2, 555 | Square::B2, 556 | Square::C2, 557 | Square::D2, 558 | Square::G2, 559 | Square::F2, 560 | Square::H2, 561 | Square::F3, 562 | Square::E4, 563 | Square::E5, 564 | Square::C6, 565 | Square::A7, 566 | Square::B7, 567 | Square::C7, 568 | Square::D7, 569 | Square::F7, 570 | Square::G7, 571 | Square::H7, 572 | Square::A8, 573 | Square::C8, 574 | Square::D8, 575 | Square::E8, 576 | Square::F8, 577 | Square::G8, 578 | Square::H8, 579 | ]); 580 | assert_eq!( 581 | format!("{:?}", bb), 582 | "1 . 1 1 1 1 1 1\n\ 583 | 1 1 1 1 . 1 1 1\n\ 584 | . . 1 . . . . .\n\ 585 | . . . . 1 . . .\n\ 586 | . . . . 1 . . .\n\ 587 | . . . . . 1 . .\n\ 588 | 1 1 1 1 . 1 1 1\n\ 589 | 1 1 1 1 1 1 . 1" 590 | ); 591 | assert_eq!( 592 | format!("{:?}", !bb), 593 | ". 1 . . . . . .\n\ 594 | . . . . 1 . . .\n\ 595 | 1 1 . 1 1 1 1 1\n\ 596 | 1 1 1 1 . 1 1 1\n\ 597 | 1 1 1 1 . 1 1 1\n\ 598 | 1 1 1 1 1 . 1 1\n\ 599 | . . . . 1 . . .\n\ 600 | . . . . . . 1 ." 601 | ); 602 | assert_eq!( 603 | format!( 604 | "{:?}", 605 | bb - Bitboard::from_squares(&[Square::A1, Square::E4, Square::G8]) 606 | ), 607 | "1 . 1 1 1 1 . 1\n\ 608 | 1 1 1 1 . 1 1 1\n\ 609 | . . 1 . . . . .\n\ 610 | . . . . 1 . . .\n\ 611 | . . . . . . . .\n\ 612 | . . . . . 1 . .\n\ 613 | 1 1 1 1 . 1 1 1\n\ 614 | . 1 1 1 1 1 . 1" 615 | ); 616 | assert_eq!(!!bb, bb); 617 | assert_eq!(bb - !bb, bb); 618 | } 619 | 620 | #[test] 621 | // Check the debug output for few bitboards. 622 | fn bitboard_dump() { 623 | assert_eq!( 624 | format!("{:?}", Bitboard::empty()), 625 | ". . . . . . . .\n\ 626 | . . . . . . . .\n\ 627 | . . . . . . . .\n\ 628 | . . . . . . . .\n\ 629 | . . . . . . . .\n\ 630 | . . . . . . . .\n\ 631 | . . . . . . . .\n\ 632 | . . . . . . . ." 633 | ); 634 | assert_eq!( 635 | format!("{:?}", Bitboard::full()), 636 | "1 1 1 1 1 1 1 1\n\ 637 | 1 1 1 1 1 1 1 1\n\ 638 | 1 1 1 1 1 1 1 1\n\ 639 | 1 1 1 1 1 1 1 1\n\ 640 | 1 1 1 1 1 1 1 1\n\ 641 | 1 1 1 1 1 1 1 1\n\ 642 | 1 1 1 1 1 1 1 1\n\ 643 | 1 1 1 1 1 1 1 1" 644 | ); 645 | assert_eq!( 646 | format!( 647 | "{:?}", 648 | Bitboard::from(Square::G5) | Bitboard::from(Square::B8) 649 | ), 650 | ". 1 . . . . . .\n\ 651 | . . . . . . . .\n\ 652 | . . . . . . . .\n\ 653 | . . . . . . 1 .\n\ 654 | . . . . . . . .\n\ 655 | . . . . . . . .\n\ 656 | . . . . . . . .\n\ 657 | . . . . . . . ." 658 | ); 659 | } 660 | 661 | #[test] 662 | fn set_dump() { 663 | let white = Pieces::starting(Player::White); 664 | let black = Pieces::starting(Player::Black); 665 | 666 | assert_eq!( 667 | format!("{:?}", black.all()), 668 | "1 1 1 1 1 1 1 1\n\ 669 | 1 1 1 1 1 1 1 1\n\ 670 | . . . . . . . .\n\ 671 | . . . . . . . .\n\ 672 | . . . . . . . .\n\ 673 | . . . . . . . .\n\ 674 | . . . . . . . .\n\ 675 | . . . . . . . ." 676 | ); 677 | assert_eq!( 678 | format!("{:?}", white.all() | black.all()), 679 | "1 1 1 1 1 1 1 1\n\ 680 | 1 1 1 1 1 1 1 1\n\ 681 | . . . . . . . .\n\ 682 | . . . . . . . .\n\ 683 | . . . . . . . .\n\ 684 | . . . . . . . .\n\ 685 | 1 1 1 1 1 1 1 1\n\ 686 | 1 1 1 1 1 1 1 1" 687 | ); 688 | 689 | assert_eq!( 690 | format!("{:?}", white.king), 691 | ". . . . . . . .\n\ 692 | . . . . . . . .\n\ 693 | . . . . . . . .\n\ 694 | . . . . . . . .\n\ 695 | . . . . . . . .\n\ 696 | . . . . . . . .\n\ 697 | . . . . . . . .\n\ 698 | . . . . 1 . . ." 699 | ); 700 | assert_eq!( 701 | format!("{:?}", black.pawns), 702 | ". . . . . . . .\n\ 703 | 1 1 1 1 1 1 1 1\n\ 704 | . . . . . . . .\n\ 705 | . . . . . . . .\n\ 706 | . . . . . . . .\n\ 707 | . . . . . . . .\n\ 708 | . . . . . . . .\n\ 709 | . . . . . . . ." 710 | ); 711 | assert_eq!( 712 | format!("{:?}", black.knights), 713 | ". 1 . . . . 1 .\n\ 714 | . . . . . . . .\n\ 715 | . . . . . . . .\n\ 716 | . . . . . . . .\n\ 717 | . . . . . . . .\n\ 718 | . . . . . . . .\n\ 719 | . . . . . . . .\n\ 720 | . . . . . . . ." 721 | ); 722 | } 723 | 724 | #[test] 725 | fn flip_perspective() {} 726 | } 727 | -------------------------------------------------------------------------------- /src/chess/attacks.rs: -------------------------------------------------------------------------------- 1 | //! Mappings of occupied squares to the attacked squares for each piece. The 2 | //! mappings are pre-calculated where possible to provide an efficient way of 3 | //! generating moves. 4 | //! 5 | //! The implementation uses BMI2 (if available) for performance ([reference]), 6 | //! specifically the PEXT instruction for [PEXT Bitboards]. 7 | //! 8 | //! [reference]: https://www.chessprogramming.org/BMI2 9 | //! [PEXT Bitboards]: https://www.chessprogramming.org/BMI2#PEXTBitboards 10 | 11 | // TODO: This code is probably by far the least appealing in the project. 12 | // Refactor it and make it nicer. 13 | 14 | use super::generated; 15 | use crate::chess::bitboard::{Bitboard, Pieces}; 16 | use crate::chess::core::{BOARD_SIZE, Square}; 17 | use crate::environment::Player; 18 | 19 | pub(super) fn king_attacks(from: Square) -> Bitboard { 20 | generated::KING_ATTACKS[from as usize] 21 | } 22 | 23 | pub(super) fn queen_attacks(from: Square, occupancy: Bitboard) -> Bitboard { 24 | bishop_attacks(from, occupancy) | rook_attacks(from, occupancy) 25 | } 26 | 27 | pub(super) fn rook_attacks(from: Square, occupancy: Bitboard) -> Bitboard { 28 | generated::ROOK_ATTACKS[generated::ROOK_ATTACK_OFFSETS[from as usize] 29 | + pext( 30 | occupancy.bits(), 31 | generated::ROOK_RELEVANT_OCCUPANCIES[from as usize], 32 | ) as usize] 33 | } 34 | 35 | pub(super) fn bishop_attacks(from: Square, occupancy: Bitboard) -> Bitboard { 36 | generated::BISHOP_ATTACKS[generated::BISHOP_ATTACK_OFFSETS[from as usize] 37 | + pext( 38 | occupancy.bits(), 39 | generated::BISHOP_RELEVANT_OCCUPANCIES[from as usize], 40 | ) as usize] 41 | } 42 | 43 | pub(super) const fn knight_attacks(square: Square) -> Bitboard { 44 | generated::KNIGHT_ATTACKS[square as usize] 45 | } 46 | 47 | pub(super) const fn pawn_attacks(square: Square, player: Player) -> Bitboard { 48 | match player { 49 | Player::White => generated::WHITE_PAWN_ATTACKS[square as usize], 50 | Player::Black => generated::BLACK_PAWN_ATTACKS[square as usize], 51 | } 52 | } 53 | 54 | pub(super) const fn ray(from: Square, to: Square) -> Bitboard { 55 | generated::RAYS[(from as usize) * (BOARD_SIZE as usize) + to as usize] 56 | } 57 | 58 | pub(super) const fn bishop_ray(from: Square, to: Square) -> Bitboard { 59 | generated::BISHOP_RAYS[(from as usize) * (BOARD_SIZE as usize) + to as usize] 60 | } 61 | 62 | const fn rook_ray(from: Square, to: Square) -> Bitboard { 63 | generated::ROOK_RAYS[(from as usize) * (BOARD_SIZE as usize) + to as usize] 64 | } 65 | 66 | /// Parallel bit extract operation - extracts bits from `a` according to `mask`. 67 | /// Uses BMI2 PEXT instruction when available, falls back to software 68 | /// implementation. 69 | #[inline] 70 | fn pext(a: u64, mask: u64) -> u64 { 71 | #[cfg(target_arch = "x86_64")] 72 | { 73 | if cfg!(target_feature = "bmi2") { 74 | return unsafe { core::arch::x86_64::_pext_u64(a, mask) }; 75 | } 76 | } 77 | 78 | // Software fallback implementation 79 | let mut result = 0u64; 80 | let mut mask = mask; 81 | let mut scanning_bit = 1u64; 82 | 83 | while mask != 0 { 84 | let ls1b = 1u64 << mask.trailing_zeros(); 85 | if (a & ls1b) != 0 { 86 | result |= scanning_bit; 87 | } 88 | mask ^= ls1b; 89 | scanning_bit <<= 1; 90 | } 91 | result 92 | } 93 | 94 | #[derive(Debug)] 95 | pub(super) struct AttackInfo { 96 | pub(super) attacks: Bitboard, 97 | pub(super) checkers: Bitboard, 98 | pub(super) pins: Bitboard, 99 | // TODO: Get rid of the XRays. 100 | pub(super) xrays: Bitboard, 101 | pub(super) safe_king_squares: Bitboard, 102 | } 103 | 104 | impl AttackInfo { 105 | // TODO: Handle each piece separately. 106 | pub(super) fn new( 107 | they: Player, 108 | their: &Pieces, 109 | king: Square, 110 | our_occupancy: Bitboard, 111 | occupancy: Bitboard, 112 | ) -> Self { 113 | let mut result = Self { 114 | attacks: Bitboard::empty(), 115 | checkers: Bitboard::empty(), 116 | pins: Bitboard::empty(), 117 | xrays: Bitboard::empty(), 118 | safe_king_squares: Bitboard::empty(), 119 | }; 120 | result.safe_king_squares = !our_occupancy & king_attacks(king); 121 | let occupancy_without_king = occupancy - Bitboard::from(king); 122 | // King. 123 | let their_king = their.king.as_square(); 124 | result.attacks |= king_attacks(their_king); 125 | // Knights. 126 | for knight in their.knights.iter() { 127 | let targets = knight_attacks(knight); 128 | result.attacks |= targets; 129 | if targets.contains(king) { 130 | result.checkers.extend(knight); 131 | } 132 | } 133 | // Pawns. 134 | for pawn in their.pawns.iter() { 135 | let targets = pawn_attacks(pawn, they); 136 | result.attacks |= targets; 137 | if targets.contains(king) { 138 | result.checkers.extend(pawn); 139 | } 140 | } 141 | // Process sliding pieces (queens, bishops, rooks) 142 | let sliding_ctx = SlidingPieceContext { 143 | occupancy, 144 | occupancy_without_king, 145 | king, 146 | our_occupancy, 147 | }; 148 | 149 | // Queens can attack like both rooks and bishops 150 | for queen in their.queens.iter() { 151 | process_sliding_piece(&mut result, queen, &sliding_ctx, queen_attacks, ray); 152 | } 153 | for bishop in their.bishops.iter() { 154 | process_sliding_piece( 155 | &mut result, 156 | bishop, 157 | &sliding_ctx, 158 | bishop_attacks, 159 | bishop_ray, 160 | ); 161 | } 162 | for rook in their.rooks.iter() { 163 | process_sliding_piece(&mut result, rook, &sliding_ctx, rook_attacks, rook_ray); 164 | } 165 | result.safe_king_squares -= result.attacks; 166 | result 167 | } 168 | } 169 | 170 | /// Context for processing sliding pieces 171 | struct SlidingPieceContext { 172 | occupancy: Bitboard, 173 | occupancy_without_king: Bitboard, 174 | king: Square, 175 | our_occupancy: Bitboard, 176 | } 177 | 178 | /// Helper function to process sliding pieces (queens, bishops, rooks) uniformly 179 | #[inline] 180 | fn process_sliding_piece( 181 | result: &mut AttackInfo, 182 | piece_square: Square, 183 | ctx: &SlidingPieceContext, 184 | attack_fn: impl Fn(Square, Bitboard) -> Bitboard, 185 | ray_fn: impl Fn(Square, Square) -> Bitboard, 186 | ) { 187 | let targets = attack_fn(piece_square, ctx.occupancy); 188 | result.attacks |= targets; 189 | 190 | if targets.contains(ctx.king) { 191 | result.checkers.extend(piece_square); 192 | result.safe_king_squares -= attack_fn(piece_square, ctx.occupancy_without_king); 193 | return; // An attack can be either a check or a (potential) pin, not both 194 | } 195 | 196 | let attack_ray = ray_fn(piece_square, ctx.king); 197 | let blocker = (attack_ray & ctx.occupancy) - Bitboard::from(piece_square); 198 | 199 | if blocker.count() == 1 { 200 | if (blocker & ctx.our_occupancy).has_any() { 201 | result.pins |= blocker; 202 | } else { 203 | result.xrays |= blocker; 204 | } 205 | } 206 | } 207 | 208 | pub(super) const WHITE_SHORT_CASTLE_KING_WALK: Bitboard = 209 | Bitboard::from_bits(0x0000_0000_0000_0060); 210 | pub(super) const WHITE_SHORT_CASTLE_ROOK_WALK: Bitboard = 211 | Bitboard::from_bits(0x0000_0000_0000_0060); 212 | pub(super) const WHITE_LONG_CASTLE_KING_WALK: Bitboard = Bitboard::from_bits(0x0000_0000_0000_000C); 213 | pub(super) const WHITE_LONG_CASTLE_ROOK_WALK: Bitboard = Bitboard::from_bits(0x0000_0000_0000_000E); 214 | pub(super) const BLACK_SHORT_CASTLE_KING_WALK: Bitboard = 215 | Bitboard::from_bits(0x6000_0000_0000_0000); 216 | pub(super) const BLACK_SHORT_CASTLE_ROOK_WALK: Bitboard = 217 | Bitboard::from_bits(0x6000_0000_0000_0000); 218 | pub(super) const BLACK_LONG_CASTLE_KING_WALK: Bitboard = Bitboard::from_bits(0x0C00_0000_0000_0000); 219 | pub(super) const BLACK_LONG_CASTLE_ROOK_WALK: Bitboard = Bitboard::from_bits(0x0E00_0000_0000_0000); 220 | 221 | #[cfg(test)] 222 | mod tests { 223 | use pretty_assertions::assert_eq; 224 | 225 | use super::*; 226 | use crate::chess::core::Rank; 227 | use crate::chess::position::Position; 228 | 229 | #[test] 230 | fn sliders() { 231 | let occupancy = Bitboard::from_squares(&[ 232 | Square::F4, 233 | Square::C4, 234 | Square::A4, 235 | Square::B1, 236 | Square::D5, 237 | Square::G5, 238 | Square::G6, 239 | Square::E8, 240 | Square::E2, 241 | ]); 242 | assert_eq!( 243 | format!("{:?}", occupancy), 244 | ". . . . 1 . . .\n\ 245 | . . . . . . . .\n\ 246 | . . . . . . 1 .\n\ 247 | . . . 1 . . 1 .\n\ 248 | 1 . 1 . . 1 . .\n\ 249 | . . . . . . . .\n\ 250 | . . . . 1 . . .\n\ 251 | . 1 . . . . . ." 252 | ); 253 | assert_eq!( 254 | format!( 255 | "{:?}", 256 | Bitboard::from_bits(generated::BISHOP_RELEVANT_OCCUPANCIES[Square::E4 as usize]) 257 | ), 258 | ". . . . . . . .\n\ 259 | . 1 . . . . . .\n\ 260 | . . 1 . . . 1 .\n\ 261 | . . . 1 . 1 . .\n\ 262 | . . . . . . . .\n\ 263 | . . . 1 . 1 . .\n\ 264 | . . 1 . . . 1 .\n\ 265 | . . . . . . . ." 266 | ); 267 | let attacks = bishop_attacks(Square::E4, occupancy); 268 | println!("{:064b}", attacks.bits()); 269 | assert_eq!( 270 | format!("{:?}", attacks), 271 | ". . . . . . . .\n\ 272 | . . . . . . . .\n\ 273 | . . . . . . 1 .\n\ 274 | . . . 1 . 1 . .\n\ 275 | . . . . . . . .\n\ 276 | . . . 1 . 1 . .\n\ 277 | . . 1 . . . 1 .\n\ 278 | . 1 . . . . . 1" 279 | ); 280 | assert_eq!( 281 | format!( 282 | "{:?}", 283 | Bitboard::from_bits(generated::ROOK_RELEVANT_OCCUPANCIES[Square::E4 as usize]) 284 | ), 285 | ". . . . . . . .\n\ 286 | . . . . 1 . . .\n\ 287 | . . . . 1 . . .\n\ 288 | . . . . 1 . . .\n\ 289 | . 1 1 1 . 1 1 .\n\ 290 | . . . . 1 . . .\n\ 291 | . . . . 1 . . .\n\ 292 | . . . . . . . ." 293 | ); 294 | let attacks = rook_attacks(Square::E4, occupancy); 295 | println!("{:064b}", attacks.bits()); 296 | assert_eq!( 297 | format!("{:?}", attacks), 298 | ". . . . 1 . . .\n\ 299 | . . . . 1 . . .\n\ 300 | . . . . 1 . . .\n\ 301 | . . . . 1 . . .\n\ 302 | . . 1 1 . 1 . .\n\ 303 | . . . . 1 . . .\n\ 304 | . . . . 1 . . .\n\ 305 | . . . . . . . ." 306 | ); 307 | } 308 | 309 | #[test] 310 | fn king() { 311 | assert_eq!( 312 | format!("{:?}", king_attacks(Square::A1)), 313 | ". . . . . . . .\n\ 314 | . . . . . . . .\n\ 315 | . . . . . . . .\n\ 316 | . . . . . . . .\n\ 317 | . . . . . . . .\n\ 318 | . . . . . . . .\n\ 319 | 1 1 . . . . . .\n\ 320 | . 1 . . . . . ." 321 | ); 322 | assert_eq!( 323 | format!("{:?}", king_attacks(Square::H3)), 324 | ". . . . . . . .\n\ 325 | . . . . . . . .\n\ 326 | . . . . . . . .\n\ 327 | . . . . . . . .\n\ 328 | . . . . . . 1 1\n\ 329 | . . . . . . 1 .\n\ 330 | . . . . . . 1 1\n\ 331 | . . . . . . . ." 332 | ); 333 | assert_eq!( 334 | format!("{:?}", king_attacks(Square::D4)), 335 | ". . . . . . . .\n\ 336 | . . . . . . . .\n\ 337 | . . . . . . . .\n\ 338 | . . 1 1 1 . . .\n\ 339 | . . 1 . 1 . . .\n\ 340 | . . 1 1 1 . . .\n\ 341 | . . . . . . . .\n\ 342 | . . . . . . . ." 343 | ); 344 | assert_eq!( 345 | format!("{:?}", king_attacks(Square::F8)), 346 | ". . . . 1 . 1 .\n\ 347 | . . . . 1 1 1 .\n\ 348 | . . . . . . . .\n\ 349 | . . . . . . . .\n\ 350 | . . . . . . . .\n\ 351 | . . . . . . . .\n\ 352 | . . . . . . . .\n\ 353 | . . . . . . . ." 354 | ); 355 | } 356 | 357 | #[test] 358 | fn knight() { 359 | assert_eq!( 360 | format!("{:?}", knight_attacks(Square::A1)), 361 | ". . . . . . . .\n\ 362 | . . . . . . . .\n\ 363 | . . . . . . . .\n\ 364 | . . . . . . . .\n\ 365 | . . . . . . . .\n\ 366 | . 1 . . . . . .\n\ 367 | . . 1 . . . . .\n\ 368 | . . . . . . . ." 369 | ); 370 | assert_eq!( 371 | format!("{:?}", knight_attacks(Square::B1)), 372 | ". . . . . . . .\n\ 373 | . . . . . . . .\n\ 374 | . . . . . . . .\n\ 375 | . . . . . . . .\n\ 376 | . . . . . . . .\n\ 377 | 1 . 1 . . . . .\n\ 378 | . . . 1 . . . .\n\ 379 | . . . . . . . ." 380 | ); 381 | assert_eq!( 382 | format!("{:?}", knight_attacks(Square::H3)), 383 | ". . . . . . . .\n\ 384 | . . . . . . . .\n\ 385 | . . . . . . . .\n\ 386 | . . . . . . 1 .\n\ 387 | . . . . . 1 . .\n\ 388 | . . . . . . . .\n\ 389 | . . . . . 1 . .\n\ 390 | . . . . . . 1 ." 391 | ); 392 | assert_eq!( 393 | format!("{:?}", knight_attacks(Square::D4)), 394 | ". . . . . . . .\n\ 395 | . . . . . . . .\n\ 396 | . . 1 . 1 . . .\n\ 397 | . 1 . . . 1 . .\n\ 398 | . . . . . . . .\n\ 399 | . 1 . . . 1 . .\n\ 400 | . . 1 . 1 . . .\n\ 401 | . . . . . . . ." 402 | ); 403 | assert_eq!( 404 | format!("{:?}", knight_attacks(Square::F8)), 405 | ". . . . . . . .\n\ 406 | . . . 1 . . . 1\n\ 407 | . . . . 1 . 1 .\n\ 408 | . . . . . . . .\n\ 409 | . . . . . . . .\n\ 410 | . . . . . . . .\n\ 411 | . . . . . . . .\n\ 412 | . . . . . . . ." 413 | ); 414 | } 415 | 416 | #[test] 417 | fn pawn() { 418 | // Pawns can not be on the back ranks, hence the attack maps are empty. 419 | for square in Rank::Rank1.mask().iter().chain(Rank::Rank8.mask().iter()) { 420 | assert!(pawn_attacks(square, Player::White).is_empty()); 421 | assert!(pawn_attacks(square, Player::Black).is_empty()); 422 | } 423 | assert_eq!( 424 | format!("{:?}", pawn_attacks(Square::A2, Player::White)), 425 | ". . . . . . . .\n\ 426 | . . . . . . . .\n\ 427 | . . . . . . . .\n\ 428 | . . . . . . . .\n\ 429 | . . . . . . . .\n\ 430 | . 1 . . . . . .\n\ 431 | . . . . . . . .\n\ 432 | . . . . . . . ." 433 | ); 434 | assert_eq!( 435 | format!("{:?}", pawn_attacks(Square::A2, Player::Black)), 436 | ". . . . . . . .\n\ 437 | . . . . . . . .\n\ 438 | . . . . . . . .\n\ 439 | . . . . . . . .\n\ 440 | . . . . . . . .\n\ 441 | . . . . . . . .\n\ 442 | . . . . . . . .\n\ 443 | . 1 . . . . . ." 444 | ); 445 | assert_eq!( 446 | format!("{:?}", pawn_attacks(Square::D4, Player::White)), 447 | ". . . . . . . .\n\ 448 | . . . . . . . .\n\ 449 | . . . . . . . .\n\ 450 | . . 1 . 1 . . .\n\ 451 | . . . . . . . .\n\ 452 | . . . . . . . .\n\ 453 | . . . . . . . .\n\ 454 | . . . . . . . ." 455 | ); 456 | assert_eq!( 457 | format!("{:?}", pawn_attacks(Square::D4, Player::Black)), 458 | ". . . . . . . .\n\ 459 | . . . . . . . .\n\ 460 | . . . . . . . .\n\ 461 | . . . . . . . .\n\ 462 | . . . . . . . .\n\ 463 | . . 1 . 1 . . .\n\ 464 | . . . . . . . .\n\ 465 | . . . . . . . ." 466 | ); 467 | assert_eq!( 468 | format!("{:?}", pawn_attacks(Square::H5, Player::White)), 469 | ". . . . . . . .\n\ 470 | . . . . . . . .\n\ 471 | . . . . . . 1 .\n\ 472 | . . . . . . . .\n\ 473 | . . . . . . . .\n\ 474 | . . . . . . . .\n\ 475 | . . . . . . . .\n\ 476 | . . . . . . . ." 477 | ); 478 | assert_eq!( 479 | format!("{:?}", pawn_attacks(Square::H5, Player::Black)), 480 | ". . . . . . . .\n\ 481 | . . . . . . . .\n\ 482 | . . . . . . . .\n\ 483 | . . . . . . . .\n\ 484 | . . . . . . 1 .\n\ 485 | . . . . . . . .\n\ 486 | . . . . . . . .\n\ 487 | . . . . . . . ." 488 | ); 489 | } 490 | 491 | #[test] 492 | fn rays() { 493 | // Rays with source == destination don't exist. 494 | for square_idx in 0..BOARD_SIZE { 495 | let square = Square::try_from(square_idx).unwrap(); 496 | assert!(ray(square, square).is_empty()); 497 | } 498 | // Rays don't exist for squares not on the same diagonal or vertical. 499 | assert!(ray(Square::A1, Square::B3).is_empty()); 500 | assert!(ray(Square::A1, Square::H7).is_empty()); 501 | assert!(ray(Square::B2, Square::H5).is_empty()); 502 | assert!(ray(Square::F2, Square::H8).is_empty()); 503 | assert_eq!( 504 | format!("{:?}", ray(Square::B3, Square::F7)), 505 | ". . . . . . . .\n\ 506 | . . . . . . . .\n\ 507 | . . . . 1 . . .\n\ 508 | . . . 1 . . . .\n\ 509 | . . 1 . . . . .\n\ 510 | . 1 . . . . . .\n\ 511 | . . . . . . . .\n\ 512 | . . . . . . . ." 513 | ); 514 | assert_eq!( 515 | format!("{:?}", ray(Square::F7, Square::B3)), 516 | ". . . . . . . .\n\ 517 | . . . . . 1 . .\n\ 518 | . . . . 1 . . .\n\ 519 | . . . 1 . . . .\n\ 520 | . . 1 . . . . .\n\ 521 | . . . . . . . .\n\ 522 | . . . . . . . .\n\ 523 | . . . . . . . ." 524 | ); 525 | assert_eq!( 526 | format!("{:?}", ray(Square::C8, Square::H8)), 527 | ". . 1 1 1 1 1 .\n\ 528 | . . . . . . . .\n\ 529 | . . . . . . . .\n\ 530 | . . . . . . . .\n\ 531 | . . . . . . . .\n\ 532 | . . . . . . . .\n\ 533 | . . . . . . . .\n\ 534 | . . . . . . . ." 535 | ); 536 | assert_eq!( 537 | format!("{:?}", ray(Square::H1, Square::H8)), 538 | ". . . . . . . .\n\ 539 | . . . . . . . 1\n\ 540 | . . . . . . . 1\n\ 541 | . . . . . . . 1\n\ 542 | . . . . . . . 1\n\ 543 | . . . . . . . 1\n\ 544 | . . . . . . . 1\n\ 545 | . . . . . . . 1" 546 | ); 547 | assert_eq!( 548 | format!("{:?}", ray(Square::E4, Square::B4)), 549 | ". . . . . . . .\n\ 550 | . . . . . . . .\n\ 551 | . . . . . . . .\n\ 552 | . . . . . . . .\n\ 553 | . . 1 1 1 . . .\n\ 554 | . . . . . . . .\n\ 555 | . . . . . . . .\n\ 556 | . . . . . . . ." 557 | ); 558 | } 559 | 560 | #[test] 561 | fn basic_attack_info() { 562 | let position = Position::try_from("3kn3/3p4/8/6B1/8/6K1/3R4/8 b - - 0 1").unwrap(); 563 | let attacks = position.attack_info(); 564 | assert_eq!( 565 | format!("{:?}", attacks.attacks), 566 | ". . . 1 . . . .\n\ 567 | . . . 1 1 . . .\n\ 568 | . . . 1 . 1 . 1\n\ 569 | . . . 1 . . . .\n\ 570 | . . . 1 . 1 1 1\n\ 571 | . . . 1 1 1 . 1\n\ 572 | 1 1 1 1 1 1 1 1\n\ 573 | . . . 1 . . . ." 574 | ); 575 | assert_eq!( 576 | format!("{:?}", attacks.checkers), 577 | "\ 578 | . . . . . . . .\n\ 579 | . . . . . . . .\n\ 580 | . . . . . . . .\n\ 581 | . . . . . . 1 .\n\ 582 | . . . . . . . .\n\ 583 | . . . . . . . .\n\ 584 | . . . . . . . .\n\ 585 | . . . . . . . ." 586 | ); 587 | assert_eq!( 588 | format!("{:?}", attacks.pins), 589 | ". . . . . . . .\n\ 590 | . . . 1 . . . .\n\ 591 | . . . . . . . .\n\ 592 | . . . . . . . .\n\ 593 | . . . . . . . .\n\ 594 | . . . . . . . .\n\ 595 | . . . . . . . .\n\ 596 | . . . . . . . ." 597 | ); 598 | assert!(attacks.xrays.is_empty()); 599 | } 600 | 601 | #[test] 602 | fn xrays() { 603 | let position = Position::try_from("b6k/8/8/3p4/8/8/8/7K w - - 0 1").unwrap(); 604 | let attacks = position.attack_info(); 605 | assert_eq!( 606 | format!("{:?}", attacks.attacks), 607 | ". . . . . . 1 .\n\ 608 | . 1 . . . . 1 1\n\ 609 | . . 1 . . . . .\n\ 610 | . . . 1 . . . .\n\ 611 | . . 1 . 1 . . .\n\ 612 | . . . . . . . .\n\ 613 | . . . . . . . .\n\ 614 | . . . . . . . ." 615 | ); 616 | assert!(attacks.checkers.is_empty()); 617 | assert!(attacks.pins.is_empty()); 618 | assert_eq!( 619 | format!("{:?}", attacks.xrays), 620 | ". . . . . . . .\n\ 621 | . . . . . . . .\n\ 622 | . . . . . . . .\n\ 623 | . . . 1 . . . .\n\ 624 | . . . . . . . .\n\ 625 | . . . . . . . .\n\ 626 | . . . . . . . .\n\ 627 | . . . . . . . ." 628 | ); 629 | } 630 | 631 | #[test] 632 | fn rich_attack_info() { 633 | let position = 634 | Position::try_from("1k3q2/8/8/4PP2/q4K2/3nRBR1/3b1Nr1/5r2 w - - 0 1").unwrap(); 635 | let attacks = position.attack_info(); 636 | assert_eq!( 637 | format!("{:?}", attacks.attacks), 638 | "1 1 1 1 1 . 1 1\n\ 639 | 1 1 1 1 1 1 1 .\n\ 640 | 1 . 1 1 . 1 . 1\n\ 641 | 1 1 1 . 1 1 . .\n\ 642 | . 1 1 1 1 1 . .\n\ 643 | 1 1 1 . 1 . 1 .\n\ 644 | 1 1 1 . . 1 . 1\n\ 645 | 1 1 1 1 1 . 1 1" 646 | ); 647 | assert_eq!( 648 | format!("{:?}", attacks.checkers), 649 | ". . . . . . . .\n\ 650 | . . . . . . . .\n\ 651 | . . . . . . . .\n\ 652 | . . . . . . . .\n\ 653 | 1 . . . . . . .\n\ 654 | . . . 1 . . . .\n\ 655 | . . . . . . . .\n\ 656 | . . . . . . . ." 657 | ); 658 | assert_eq!( 659 | format!("{:?}", attacks.pins), 660 | ". . . . . . . .\n\ 661 | . . . . . . . .\n\ 662 | . . . . . . . .\n\ 663 | . . . . . 1 . .\n\ 664 | . . . . . . . .\n\ 665 | . . . . 1 . . .\n\ 666 | . . . . . . . .\n\ 667 | . . . . . . . ." 668 | ); 669 | assert!(attacks.xrays.is_empty()); 670 | } 671 | 672 | #[test] 673 | fn complicated_attack_info() { 674 | let position = 675 | Position::try_from("2r3r1/3p3k/1p3pp1/1B5P/5P2/2P1pqP1/PP4KP/3R4 w - - 0 34").unwrap(); 676 | let attacks = position.attack_info(); 677 | assert_eq!( 678 | format!("{:?}", attacks.checkers), 679 | ". . . . . . . .\n\ 680 | . . . . . . . .\n\ 681 | . . . . . . . .\n\ 682 | . . . . . . . .\n\ 683 | . . . . . . . .\n\ 684 | . . . . . 1 . .\n\ 685 | . . . . . . . .\n\ 686 | . . . . . . . ." 687 | ); 688 | assert_eq!( 689 | format!("{:?}", attacks.safe_king_squares), 690 | ". . . . . . . .\n\ 691 | . . . . . . . .\n\ 692 | . . . . . . . .\n\ 693 | . . . . . . . .\n\ 694 | . . . . . . . .\n\ 695 | . . . . . 1 . 1\n\ 696 | . . . . . . . .\n\ 697 | . . . . . . 1 ." 698 | ); 699 | assert!(attacks.xrays.is_empty()); 700 | } 701 | } 702 | -------------------------------------------------------------------------------- /tests/chess.rs: -------------------------------------------------------------------------------- 1 | // Many integration tests are slow and are marked as ignored. Use `just 2 | // test_all` to include them in the test suite run. 3 | 4 | use std::fs; 5 | 6 | use itertools::Itertools; 7 | use pabi::chess::core::Move; 8 | use pabi::chess::position::{Position, perft}; 9 | use pretty_assertions::assert_eq; 10 | use shakmaty::Position as ShakmatyPosition; 11 | 12 | #[must_use] 13 | pub fn sanitize_fen(position: &str) -> String { 14 | let mut position = position.trim(); 15 | for prefix in ["fen ", "epd "] { 16 | if let Some(stripped) = position.strip_prefix(prefix) { 17 | position = stripped; 18 | } 19 | } 20 | match position.split_ascii_whitespace().count() { 21 | 6 => position.to_string(), 22 | // Patch EPD to validate produced FEN. 23 | 4 => position.to_string() + " 0 1", 24 | _ => unreachable!(), 25 | } 26 | } 27 | 28 | fn expect_legal_position(input: &str) { 29 | let position = Position::from_fen(input).expect("we are parsing valid position: {input}"); 30 | assert_eq!(position.to_string(), sanitize_fen(input)); 31 | } 32 | 33 | #[test] 34 | #[allow(unused_results)] 35 | fn basic_positions() { 36 | // Full FEN. 37 | expect_legal_position("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"); 38 | expect_legal_position("2r3r1/p3k3/1p3pp1/1B5p/5P2/2P1p1P1/PP4KP/3R4 w - - 0 34"); 39 | expect_legal_position("rnbqk1nr/p3bppp/1p2p3/2ppP3/3P4/P7/1PP1NPPP/R1BQKBNR w KQkq c6 0 7"); 40 | expect_legal_position( 41 | "r2qkb1r/1pp1pp1p/p1np1np1/1B6/3PP1b1/2N1BN2/PPP2PPP/R2QK2R w KQkq - 0 7", 42 | ); 43 | expect_legal_position("r3k3/5p2/2p5/p7/P3r3/2N2n2/1PP2P2/2K2B2 w q - 0 24"); 44 | expect_legal_position("r1b1qrk1/ppp2pbp/n2p1np1/4p1B1/2PPP3/2NB1N1P/PP3PP1/R2QK2R w KQ e6 0 9"); 45 | expect_legal_position("8/8/8/8/2P5/3k4/8/KB6 b - c3 0 1"); 46 | expect_legal_position("rnbq1rk1/pp4pp/1b1ppn2/2p2p2/2PP4/1P2PN2/PB2BPPP/RN1Q1RK1 w - c6 0 9"); 47 | // Trimmed FEN. 48 | expect_legal_position("rnbqkb1r/pp2pppp/3p1n2/8/3NP3/2N5/PPP2PPP/R1BQKB1R b KQkq -"); 49 | } 50 | 51 | #[test] 52 | #[should_panic(expected = "expected 1 white king, got 0")] 53 | fn no_white_king() { 54 | let _ = setup("3k4/8/8/8/8/8/8/8 w - - 0 1"); 55 | } 56 | 57 | #[test] 58 | #[should_panic(expected = "expected 1 black king, got 0")] 59 | fn no_black_king() { 60 | let _ = setup("8/8/8/8/8/8/8/3K4 w - - 0 1"); 61 | } 62 | 63 | #[test] 64 | #[should_panic(expected = "expected 1 white king, got 3")] 65 | fn too_many_kings() { 66 | let _ = setup("1kkk4/8/8/8/8/8/8/1KKK4 w - - 0 1"); 67 | } 68 | 69 | #[test] 70 | #[should_panic(expected = "expected <= 8 white pawns, got 9")] 71 | fn too_many_white_pawns() { 72 | let _ = setup("rnbqkbnr/pppppppp/8/8/8/P7/PPPPPPPP/RNBQKBNR w KQkq - 0 1"); 73 | } 74 | 75 | #[test] 76 | #[should_panic(expected = "expected <= 8 black pawns, got 9")] 77 | fn too_many_black_pawns() { 78 | let _ = setup("rnbqkbnr/pppppppp/p7/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"); 79 | } 80 | 81 | #[test] 82 | #[should_panic(expected = "pawns can not be placed on backranks")] 83 | fn pawns_on_backranks() { 84 | let _ = setup("3kr3/8/8/8/8/5Q2/8/1KP5 w - - 0 1"); 85 | } 86 | 87 | #[test] 88 | #[should_panic(expected = "expected en passant square to be on rank 6, got 3")] 89 | fn wrong_en_passant_player() { 90 | let _ = setup("rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq e3 0 1"); 91 | } 92 | 93 | #[test] 94 | #[should_panic(expected = "expected en passant square to be on rank 3, got 4")] 95 | fn wrong_en_passant_rank() { 96 | let _ = setup("rnbqkbnr/pppp1ppp/8/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq e4 0 1"); 97 | } 98 | 99 | #[test] 100 | #[should_panic(expected = "en passant square is not beyond pushed pawn")] 101 | fn en_passant_not_beyond_pawn() { 102 | let _ = setup("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq d3 0 1"); 103 | } 104 | 105 | #[test] 106 | #[should_panic(expected = "more than 1 check after double pawn push is impossible")] 107 | fn en_passant_double_check() { 108 | let _ = setup("r2qkbnr/ppp3Np/8/4Q3/4P3/8/PP4PP/RNB1KB1R b KQkq e3 0 1"); 109 | } 110 | 111 | #[test] 112 | #[should_panic(expected = "expected <= 2 checks, got 3")] 113 | fn triple_check() { 114 | let _ = setup("2r3r1/P3k3/prp5/1B5p/5P2/2Q1n2p/PP4KP/3R4 w - - 0 34"); 115 | } 116 | 117 | #[test] 118 | #[should_panic( 119 | expected = "the only possible checks after double pawn push are either discovery targeting the \ 120 | original pawn square or the pushed pawn itself" 121 | )] 122 | fn check_with_unrelated_en_passant() { 123 | let _ = setup("rnbqk1nr/bb3p1p/1q2r3/2pPp3/3P4/7P/1PP1NpPP/R1BQKBNR w KQkq c6 0 1"); 124 | } 125 | 126 | #[test] 127 | #[should_panic(expected = "doubly pushed pawn can not be the only blocker on a diagonal")] 128 | fn double_push_blocks_existing_check() { 129 | Position::try_from("q6k/8/8/3pP3/8/8/8/7K w - d6 0 1").unwrap(); 130 | } 131 | 132 | #[test] 133 | fn clean_board_str() { 134 | // Prefix with "fen". 135 | assert!( 136 | Position::try_from( 137 | "fen rn1qkb1r/pp3ppp/2p1pn2/3p1b2/2PP4/5NP1/PP2PPBP/RNBQK2R w KQkq - 0 1" 138 | ) 139 | .is_ok() 140 | ); 141 | // Prefix with "epd". 142 | assert!( 143 | Position::try_from("epd rnbqkb1r/ppp1pp1p/5np1/3p4/3P1B2/5N2/PPP1PPPP/RN1QKB1R w KQkq -") 144 | .is_ok() 145 | ); 146 | // No prefix: infer EPD. 147 | assert!(Position::try_from("rnbqkbnr/pp2pppp/8/3p4/3P4/3B4/PPP2PPP/RNBQK1NR b KQkq -").is_ok()); 148 | // No prefix: infer FEN. 149 | assert!( 150 | Position::try_from("rnbqkbnr/pp2pppp/8/3p4/3P4/3B4/PPP2PPP/RNBQK1NR b KQkq - 0 1").is_ok() 151 | ); 152 | // Don't crash on unicode symbols. 153 | assert!(Position::try_from("8/8/8/8/8/8/8/8 b 88 🔠 🔠 ").is_err()); 154 | // Whitespaces at the start/end of the input are not accepted in from_fen but 155 | // will be cleaned up by try_from. 156 | assert!( 157 | Position::try_from("rnbqkb1r/ppp1pp1p/5np1/3p4/3P1B2/5N2/PPP1PPPP/RN1QKB1R w KQkq -\n") 158 | .is_ok() 159 | ); 160 | assert!( 161 | Position::try_from( 162 | "\n epd rnbqkb1r/ppp1pp1p/5np1/3p4/3P1B2/5N2/PPP1PPPP/RN1QKB1R w KQkq -" 163 | ) 164 | .is_ok() 165 | ); 166 | } 167 | 168 | #[test] 169 | fn no_crash() { 170 | assert!(Position::try_from("3k2p1N/82/8/8/7B/6K1/3R4/8 b - - 0 1").is_err()); 171 | assert!(Position::try_from("3kn3/R2p1N2/8/8/70000000000000000B/6K1/3R4/8 b - - 0 1").is_err()); 172 | assert!(Position::try_from("3kn3/R4N2/8/8/7B/6K1/3R4/8 b - - 0 48 b - - 0 4/8 b").is_err()); 173 | assert!(Position::try_from("\tfen3kn3/R2p1N2/8/8/7B/6K1/3R4/8 b - - 0 23").is_err()); 174 | assert!(Position::try_from("fen3kn3/R2p1N2/8/8/7B/6K1/3R4/8 b - - 0 23").is_err()); 175 | assert!( 176 | Position::from_fen( 177 | "\n epd rnbqkb1r/ppp1pp1p/5np1/3p4/3P1B2/5N2/PPP1PPPP/RN1QKB1R w KQkq -\n" 178 | ) 179 | .is_err() 180 | ); 181 | } 182 | 183 | #[test] 184 | #[ignore] 185 | fn arbitrary_positions() { 186 | for serialized_position in fs::read_to_string(concat!( 187 | env!("CARGO_MANIFEST_DIR"), 188 | "/tests/data/positions.fen" 189 | )) 190 | .unwrap() 191 | .lines() 192 | { 193 | let position = Position::try_from(serialized_position).unwrap(); 194 | assert_eq!(position.to_string(), sanitize_fen(serialized_position)); 195 | } 196 | } 197 | 198 | fn setup(input: &str) -> Position { 199 | Position::try_from(input).expect("parsing legal position: {input}") 200 | } 201 | 202 | fn get_moves(position: &Position) -> Vec { 203 | position 204 | .generate_moves() 205 | .iter() 206 | .map(Move::to_string) 207 | .sorted() 208 | .collect::>() 209 | } 210 | 211 | fn sorted_moves(moves: &[&str]) -> Vec { 212 | moves 213 | .iter() 214 | .map(|m| (*m).to_string()) 215 | .sorted() 216 | .collect::>() 217 | } 218 | 219 | #[test] 220 | fn starting_moves_generation() { 221 | assert_eq!( 222 | get_moves(&Position::starting()), 223 | sorted_moves(&[ 224 | "a2a3", "a2a4", "b1a3", "b1c3", "b2b3", "b2b4", "c2c3", "c2c4", "d2d3", "d2d4", "e2e3", 225 | "e2e4", "f2f3", "f2f4", "g1f3", "g1h3", "g2g3", "g2g4", "h2h3", "h2h4" 226 | ]) 227 | ); 228 | } 229 | 230 | #[test] 231 | fn basic_moves_generation() { 232 | assert_eq!( 233 | get_moves(&setup("2n4k/1PP5/6K1/3Pp1Q1/3N4/3P4/P3R3/8 w - e6 0 1")), 234 | sorted_moves(&[ 235 | "a2a3", "a2a4", "d5d6", "d5e6", "b7b8q", "b7b8r", "b7b8b", "b7b8n", "b7c8q", "b7c8r", 236 | "b7c8b", "b7c8n", "e2e1", "e2e3", "e2e4", "e2e5", "e2b2", "e2c2", "e2d2", "e2f2", 237 | "e2g2", "e2h2", "d4b3", "d4c2", "d4f3", "d4b5", "d4c6", "d4e6", "d4f5", "g5c1", "g5d2", 238 | "g5e3", "g5f4", "g5g4", "g5g3", "g5g2", "g5g1", "g5h4", "g5e5", "g5f5", "g5h5", "g5h6", 239 | "g5f6", "g5e7", "g5d8", "g6f5", "g6h5", "g6f6", "g6h6", "g6f7", 240 | ]) 241 | ); 242 | } 243 | 244 | #[test] 245 | fn double_check_evasions() { 246 | assert_eq!( 247 | get_moves(&setup("3kn3/R2p1N2/8/8/7B/6K1/3R4/8 b - - 0 1")), 248 | sorted_moves(&["d8c8"]) 249 | ); 250 | assert_eq!( 251 | get_moves(&setup("8/5Nk1/7p/4Bp2/3q4/8/8/5KR1 b - - 0 1")), 252 | sorted_moves(&["g7f8", "g7f7", "g7h7"]) 253 | ); 254 | assert_eq!( 255 | get_moves(&setup("8/5Pk1/7p/4Bp2/3q4/8/8/5KR1 b - - 0 1")), 256 | sorted_moves(&["g7f8", "g7f7", "g7h7"]) 257 | ); 258 | } 259 | 260 | #[test] 261 | fn check_evasions() { 262 | assert_eq!( 263 | get_moves(&setup("3kn3/R2p4/8/6B1/8/6K1/3R4/8 b - - 0 1")), 264 | sorted_moves(&["e8f6", "d8c8"]) 265 | ); 266 | assert_eq!( 267 | get_moves(&setup("2R5/8/6k1/8/8/8/PPn5/KR6 w - - 0 1")), 268 | sorted_moves(&["c8c2"]) 269 | ); 270 | } 271 | 272 | #[test] 273 | fn pins() { 274 | // The pawn is pinned but can capture en passant. 275 | assert_eq!( 276 | get_moves(&setup("6qk/8/8/3Pp3/8/8/K7/8 w - e6 0 1")), 277 | sorted_moves(&["a2a1", "a2a3", "a2b1", "a2b2", "a2b3", "d5e6"]) 278 | ); 279 | // The pawn is pinned but there is no en passant: it can't move. 280 | assert_eq!( 281 | get_moves(&setup("6qk/8/8/3Pp3/8/8/K7/8 w - - 0 1")), 282 | sorted_moves(&["a2a1", "a2a3", "a2b1", "a2b2", "a2b3"]) 283 | ); 284 | // The pawn is pinned and can't move. 285 | assert_eq!( 286 | get_moves(&setup("k7/1p6/8/8/8/8/8/4K2B b - - 0 1")), 287 | sorted_moves(&["a8a7", "a8b8"]) 288 | ); 289 | } 290 | 291 | // Artifacts from the fuzzer or perft. 292 | #[test] 293 | fn fuzzing_artifact_moves() { 294 | assert_eq!( 295 | get_moves(&setup( 296 | "2r3r1/3p3k/1p3pp1/1B5P/5P2/2P1pqP1/PP4KP/3R4 w - - 0 34" 297 | )), 298 | sorted_moves(&["g2g1", "g2f3", "g2h3"]) 299 | ); 300 | assert_eq!( 301 | get_moves(&setup("K7/8/8/8/1R2Pp1k/8/8/8 b - e3 0 1")), 302 | sorted_moves(&["h4h5", "h4h3", "h4g4", "h4g5", "h4g3", "f4f3"]) 303 | ); 304 | assert_eq!( 305 | get_moves(&setup( 306 | "2r3r1/3p3k/1p3pp1/1B5P/5p2/2P1p1P1/PP4KP/3R4 w - - 0 34" 307 | )), 308 | sorted_moves(&[ 309 | "a2a3", "a2a4", "b2b3", "b2b4", "c3c4", "b5a4", "b5a6", "b5c6", "b5d7", "b5c4", "b5d3", 310 | "b5e2", "b5f1", "g3g4", "h2h3", "h2h4", "h5h6", "h5g6", "g2f3", "g2f1", "g2g1", "g2h3", 311 | "g2h1", "d1a1", "d1b1", "d1c1", "d1e1", "d1f1", "d1g1", "d1h1", "d1d2", "d1d3", "d1d4", 312 | "d1d5", "d1d6", "d1d7", "g3f4", 313 | ]) 314 | ); 315 | assert_eq!( 316 | get_moves(&setup( 317 | "2r3r1/3p3k/1p3pp1/1B5p/5P2/2P2pP1/PP4KP/3R4 w - - 0 34" 318 | )), 319 | sorted_moves(&["g2f1", "g2f2", "g2f3", "g2g1", "g2h1", "g2h3"]) 320 | ); 321 | assert_eq!( 322 | get_moves(&setup( 323 | "2r3r1/P3k3/pp3p2/1B5p/5P2/2P3pP/PP4KP/3R4 w - - 0 1" 324 | )), 325 | sorted_moves(&[ 326 | "a2a3", "a2a4", "a7a8b", "a7a8n", "a7a8q", "a7a8r", "b2b3", "b2b4", "b5a4", "b5a6", 327 | "b5c4", "b5c6", "b5d3", "b5d7", "b5e2", "b5e8", "b5f1", "c3c4", "d1a1", "d1b1", "d1c1", 328 | "d1d2", "d1d3", "d1d4", "d1d5", "d1d6", "d1d7", "d1d8", "d1e1", "d1f1", "d1g1", "d1h1", 329 | "f4f5", "g2f1", "g2f3", "g2g1", "g2h1", "h2g3", "h3h4", 330 | ]) 331 | ); 332 | assert_eq!( 333 | get_moves(&setup( 334 | "2r3r1/p3k3/pp3p2/1B5p/5P2/2pqp1P1/PPK4P/3R4 w - - 0 34" 335 | )), 336 | sorted_moves(&["b5d3", "c2b3", "c2c1", "c2d3", "d1d3"]) 337 | ); 338 | assert_eq!( 339 | get_moves(&setup( 340 | "2r3r1/p3k3/pp3p2/1B5p/5P2/2P1p1P1/PP4Kr/3R4 w - - 0 1" 341 | )), 342 | sorted_moves(&["g2f1", "g2f3", "g2g1", "g2h2"]) 343 | ); 344 | assert_eq!( 345 | get_moves(&setup("r3k3/r7/8/5pP1/5QKN/8/8/6RR w - f6 0 1")), 346 | sorted_moves(&[ 347 | "f4f5", "h4f5", "g4f5", "g4f3", "g4g3", "g4h3", "g5f6", "g4h5" 348 | ]) 349 | ); 350 | assert_eq!( 351 | get_moves(&setup("4k1r1/8/8/4PpP1/6K1/8/8/8 w - f6 0 1")), 352 | sorted_moves(&[ 353 | "g4f4", "g4f3", "g4f5", "g4g3", "g4h3", "g4h4", "g4h5", "e5f6" 354 | ]) 355 | ); 356 | assert_eq!( 357 | get_moves(&setup("8/2p5/3p4/1P5r/KR3p1k/8/4P1P1/8 b - - 1 1")), 358 | sorted_moves(&[ 359 | "c7c6", "c7c5", "d6d5", "h5b5", "h5c5", "h5d5", "h5e5", "h5g5", "h5f5", "h5h6", "h5h7", 360 | "h5h8", "h4g4", "h4g5", "h4g3" 361 | ]) 362 | ); 363 | } 364 | 365 | #[test] 366 | fn castle() { 367 | // Can castle both sides. 368 | assert_eq!( 369 | get_moves(&setup("r3k2r/8/8/8/8/8/6N1/4K3 b kq - 0 1")), 370 | sorted_moves(&[ 371 | "a8a7", "a8a6", "a8a5", "a8a4", "a8a3", "a8a2", "a8a1", "a8b8", "a8c8", "a8d8", "h8f8", 372 | "h8g8", "h8h7", "h8h6", "h8h5", "h8h4", "h8h3", "h8h2", "h8h1", "e8e7", "e8d8", "e8d7", 373 | "e8f8", "e8f7", "e8c8", "e8g8" 374 | ]) 375 | ); 376 | // Castling short blocked by a check. 377 | assert_eq!( 378 | get_moves(&setup("r3k2r/8/8/8/8/8/6R1/4K3 b kq - 0 1")), 379 | sorted_moves(&[ 380 | "a8a7", "a8a6", "a8a5", "a8a4", "a8a3", "a8a2", "a8a1", "a8b8", "a8c8", "a8d8", "h8f8", 381 | "h8g8", "h8h7", "h8h6", "h8h5", "h8h4", "h8h3", "h8h2", "h8h1", "e8e7", "e8d8", "e8d7", 382 | "e8f8", "e8f7", "e8c8" 383 | ]) 384 | ); 385 | // Castling short blocked by our piece, castling long is not available. 386 | assert_eq!( 387 | get_moves(&setup("r3k2r/8/8/8/8/8/6R1/4K3 b k - 0 1")), 388 | sorted_moves(&[ 389 | "a8a7", "a8a6", "a8a5", "a8a4", "a8a3", "a8a2", "a8a1", "a8b8", "a8c8", "a8d8", "h8f8", 390 | "h8g8", "h8h7", "h8h6", "h8h5", "h8h4", "h8h3", "h8h2", "h8h1", "e8e7", "e8d8", "e8d7", 391 | "e8f8", "e8f7" 392 | ]) 393 | ); 394 | // Castling long is not blocked: the attacked square is not the one king will 395 | // walk through. 396 | assert_eq!( 397 | get_moves(&setup("r3k2r/8/8/8/8/8/1R6/4K3 b q - 0 1")), 398 | sorted_moves(&[ 399 | "a8a7", "a8a6", "a8a5", "a8a4", "a8a3", "a8a2", "a8a1", "a8b8", "a8c8", "a8d8", "h8f8", 400 | "h8g8", "h8h7", "h8h6", "h8h5", "h8h4", "h8h3", "h8h2", "h8h1", "e8e7", "e8d8", "e8d7", 401 | "e8f8", "e8f7", "e8c8" 402 | ]) 403 | ); 404 | // Castling long is blocked by an attack and the king is cut off. 405 | assert_eq!( 406 | get_moves(&setup("r3k2r/8/8/8/8/8/3R4/4K3 b kq - 0 1")), 407 | sorted_moves(&[ 408 | "a8a7", "a8a6", "a8a5", "a8a4", "a8a3", "a8a2", "a8a1", "a8b8", "a8c8", "a8d8", "h8f8", 409 | "h8g8", "h8h7", "h8h6", "h8h5", "h8h4", "h8h3", "h8h2", "h8h1", "e8e7", "e8f8", "e8f7", 410 | "e8g8" 411 | ]) 412 | ); 413 | } 414 | 415 | #[test] 416 | fn chess_programming_wiki_perft_positions() { 417 | // depth=1. 418 | // Position 1 is the starting position: handled in detail before. 419 | // Position 2. 420 | assert_eq!( 421 | get_moves(&setup( 422 | "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1" 423 | )) 424 | .len(), 425 | 48 426 | ); 427 | // Position 3. 428 | assert_eq!( 429 | get_moves(&setup("8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1")).len(), 430 | 14, 431 | ); 432 | // Position 4. 433 | assert_eq!( 434 | get_moves(&setup( 435 | "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1" 436 | )) 437 | .len(), 438 | 6 439 | ); 440 | // Mirrored. 441 | assert_eq!( 442 | get_moves(&setup( 443 | "r2q1rk1/pP1p2pp/Q4n2/bbp1p3/Np6/1B3NBn/pPPP1PPP/R3K2R b KQ - 0 1" 444 | )) 445 | .len(), 446 | 6 447 | ); 448 | // Position 5. 449 | assert_eq!( 450 | get_moves(&setup( 451 | "rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8" 452 | )) 453 | .len(), 454 | 44 455 | ); 456 | // Position 6 457 | assert_eq!( 458 | get_moves(&setup( 459 | "r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10" 460 | )) 461 | .len(), 462 | 46 463 | ); 464 | // "kiwipete" 465 | assert_eq!( 466 | get_moves(&setup( 467 | "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1" 468 | )) 469 | .len(), 470 | 48 471 | ); 472 | } 473 | 474 | #[test] 475 | fn make_moves() { 476 | let mut position = Position::starting(); 477 | position.make_move(&Move::from_uci("a2a4").unwrap()); 478 | position.make_move(&Move::from_uci("d7d5").unwrap()); 479 | position.make_move(&Move::from_uci("b2b4").unwrap()); 480 | position.make_move(&Move::from_uci("c7c6").unwrap()); 481 | position.make_move(&Move::from_uci("c1b2").unwrap()); 482 | position.make_move(&Move::from_uci("e7e6").unwrap()); 483 | position.make_move(&Move::from_uci("c2c3").unwrap()); 484 | position.make_move(&Move::from_uci("f7f5").unwrap()); 485 | position.make_move(&Move::from_uci("h2h4").unwrap()); 486 | 487 | assert_eq!( 488 | position.to_string(), 489 | "rnbqkbnr/pp4pp/2p1p3/3p1p2/PP5P/2P5/1B1PPPP1/RN1QKBNR b KQkq - 0 5" 490 | ); 491 | 492 | position.make_move(&Move::from_uci("g7g6").unwrap()); 493 | position.make_move(&Move::from_uci("g2g4").unwrap()); 494 | position.make_move(&Move::from_uci("h7h5").unwrap()); 495 | position.make_move(&Move::from_uci("f2f3").unwrap()); 496 | position.make_move(&Move::from_uci("h5g4").unwrap()); 497 | position.make_move(&Move::from_uci("f3g4").unwrap()); 498 | position.make_move(&Move::from_uci("f5g4").unwrap()); 499 | 500 | assert_eq!( 501 | position.to_string(), 502 | "rnbqkbnr/pp6/2p1p1p1/3p4/PP4pP/2P5/1B1PP3/RN1QKBNR w KQkq - 0 9" 503 | ); 504 | 505 | position.make_move(&Move::from_uci("d2d3").unwrap()); 506 | position.make_move(&Move::from_uci("g6g5").unwrap()); 507 | position.make_move(&Move::from_uci("e1d2").unwrap()); 508 | position.make_move(&Move::from_uci("g5h4").unwrap()); 509 | 510 | assert_eq!( 511 | position.to_string(), 512 | "rnbqkbnr/pp6/2p1p3/3p4/PP4pp/2PP4/1B1KP3/RN1Q1BNR w kq - 0 11" 513 | ); 514 | 515 | position.make_move(&Move::from_uci("d1e1").unwrap()); 516 | position.make_move(&Move::from_uci("e6e5").unwrap()); 517 | position.make_move(&Move::from_uci("d2c2").unwrap()); 518 | position.make_move(&Move::from_uci("a7a6").unwrap()); 519 | position.make_move(&Move::from_uci("b1d2").unwrap()); 520 | position.make_move(&Move::from_uci("b7b5").unwrap()); 521 | position.make_move(&Move::from_uci("a4a5").unwrap()); 522 | position.make_move(&Move::from_uci("e5e4").unwrap()); 523 | position.make_move(&Move::from_uci("d3e4").unwrap()); 524 | position.make_move(&Move::from_uci("b8d7").unwrap()); 525 | position.make_move(&Move::from_uci("e4d5").unwrap()); 526 | 527 | assert_eq!( 528 | position.to_string(), 529 | "r1bqkbnr/3n4/p1p5/Pp1P4/1P4pp/2P5/1BKNP3/R3QBNR b kq - 0 16" 530 | ); 531 | } 532 | 533 | #[test] 534 | #[ignore] 535 | fn arbitrary_positions_book() { 536 | let positions = fs::read_to_string(concat!( 537 | env!("CARGO_MANIFEST_DIR"), 538 | "/tests/data/positions.fen" 539 | )) 540 | .unwrap(); 541 | check_movegen_for_positions(positions); 542 | } 543 | 544 | #[test] 545 | #[ignore] 546 | fn ccrl_uho_positions() { 547 | let positions = fs::read_to_string(concat!( 548 | env!("CARGO_MANIFEST_DIR"), 549 | "/books/Chess324_xxl_big_+090_+119.epd" 550 | )) 551 | .unwrap(); 552 | check_movegen_for_positions(positions); 553 | } 554 | 555 | fn check_movegen_for_positions(positions: String) { 556 | for serialized_position in positions.lines() { 557 | let position = Position::from_fen(serialized_position).unwrap(); 558 | let shakmaty_setup: shakmaty::fen::Fen = serialized_position.parse().unwrap(); 559 | let shakmaty_position: shakmaty::Chess = shakmaty_setup 560 | .into_position(shakmaty::CastlingMode::Standard) 561 | .unwrap(); 562 | let moves = position.generate_moves(); 563 | assert_eq!( 564 | moves 565 | .iter() 566 | .map(ToString::to_string) 567 | .sorted() 568 | .collect::>(), 569 | shakmaty::Position::legal_moves(&shakmaty_position) 570 | .iter() 571 | .map(|m| m.to_uci(shakmaty::CastlingMode::Standard).to_string()) 572 | .sorted() 573 | .collect::>(), 574 | "position: {serialized_position}" 575 | ); 576 | assert_eq!(position.in_check(), shakmaty_position.is_check()); 577 | } 578 | } 579 | 580 | #[test] 581 | fn basic_moves() { 582 | let mut position = setup("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"); 583 | position.make_move(&Move::from_uci("e2e4").expect("valid move")); 584 | assert_eq!( 585 | position.to_string(), 586 | "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1" 587 | ); 588 | position.make_move(&Move::from_uci("e7e5").expect("valid move")); 589 | assert_eq!( 590 | position.to_string(), 591 | "rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2" 592 | ); 593 | position.make_move(&Move::from_uci("g1f3").expect("valid move")); 594 | assert_eq!( 595 | position.to_string(), 596 | "rnbqkbnr/pppp1ppp/8/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2" 597 | ); 598 | position.make_move(&Move::from_uci("e8e7").expect("valid move")); 599 | assert_eq!( 600 | position.to_string(), 601 | "rnbq1bnr/ppppkppp/8/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQ - 2 3" 602 | ); 603 | } 604 | 605 | #[test] 606 | fn promotion_moves() { 607 | let mut position = setup("2n4k/1PP5/6K1/3Pp1Q1/3N4/3P4/P3R3/8 w - - 0 1"); 608 | position.make_move(&Move::from_uci("b7c8q").expect("valid move")); 609 | assert_eq!( 610 | position.to_string(), 611 | "2Q4k/2P5/6K1/3Pp1Q1/3N4/3P4/P3R3/8 b - - 0 1" 612 | ); 613 | } 614 | 615 | #[test] 616 | fn castling_reset() { 617 | let mut position = setup("r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1"); 618 | position.make_move(&Move::from_uci("a1a8").expect("valid move")); 619 | assert_eq!(position.to_string(), "R3k2r/8/8/8/8/8/8/4K2R b Kk - 0 1"); 620 | } 621 | 622 | #[test] 623 | fn perft_starting_position() { 624 | let position = Position::starting(); 625 | assert_eq!(perft(&position, 0), 1); 626 | assert_eq!(perft(&position, 1), 20); 627 | assert_eq!(perft(&position, 2), 400); 628 | assert_eq!(perft(&position, 3), 8902); 629 | } 630 | 631 | #[test] 632 | #[ignore] 633 | fn perft_expensive_starting() { 634 | // Position 1. 635 | let position = Position::starting(); 636 | assert_eq!(perft(&position, 4), 197_281); 637 | assert_eq!(perft(&position, 5), 4_865_609); 638 | assert_eq!(perft(&position, 6), 119_060_324); 639 | } 640 | 641 | // Positions from https://www.chessprogramming.org/Perft_Results 642 | 643 | // Position 2. 644 | #[test] 645 | fn perft_kiwipete() { 646 | let position = setup("r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1"); 647 | assert_eq!(perft(&position, 1), 48); 648 | assert_eq!(perft(&position, 2), 2039); 649 | assert_eq!(perft(&position, 3), 97862); 650 | } 651 | 652 | #[test] 653 | #[ignore] 654 | fn perft_kiwipete_expensive() { 655 | // Position 2. 656 | let position = setup("r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1"); 657 | assert_eq!(perft(&position, 4), 4_085_603); 658 | assert_eq!(perft(&position, 5), 193_690_690); 659 | } 660 | 661 | // Position 3. 662 | #[test] 663 | fn perft_endgame() { 664 | let position = setup("8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1"); 665 | assert_eq!(perft(&position, 1), 14); 666 | assert_eq!(perft(&position, 2), 191); 667 | assert_eq!(perft(&position, 3), 2812); 668 | } 669 | 670 | #[test] 671 | #[ignore] 672 | fn perft_endgame_expensive() { 673 | let position = setup("r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1"); 674 | assert_eq!(perft(&position, 4), 4_085_603); 675 | assert_eq!(perft(&position, 5), 193_690_690); 676 | } 677 | 678 | // Position 4. 679 | #[test] 680 | fn perft_complex() { 681 | let position = setup("r2q1rk1/pP1p2pp/Q4n2/bbp1p3/Np6/1B3NBn/pPPP1PPP/R3K2R b KQ - 0 1"); 682 | assert_eq!(perft(&position, 1), 6); 683 | assert_eq!(perft(&position, 2), 264); 684 | assert_eq!(perft(&position, 3), 9467); 685 | } 686 | 687 | #[test] 688 | #[ignore] 689 | fn perft_complex_expensive() { 690 | let position = setup("r2q1rk1/pP1p2pp/Q4n2/bbp1p3/Np6/1B3NBn/pPPP1PPP/R3K2R b KQ - 0 1"); 691 | assert_eq!(perft(&position, 4), 422_333); 692 | assert_eq!(perft(&position, 5), 15_833_292); 693 | } 694 | 695 | // Position 5. 696 | #[test] 697 | fn perft_fifth() { 698 | let position = setup("rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8"); 699 | assert_eq!(perft(&position, 1), 44); 700 | assert_eq!(perft(&position, 2), 1486); 701 | assert_eq!(perft(&position, 3), 62379); 702 | } 703 | 704 | #[test] 705 | #[ignore] 706 | fn perft_fifth_expensive() { 707 | let position = setup("rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8"); 708 | assert_eq!(perft(&position, 4), 2_103_487); 709 | assert_eq!(perft(&position, 5), 89_941_194); 710 | } 711 | 712 | // Position 6. 713 | #[test] 714 | fn perft_sixth() { 715 | let position = 716 | setup("r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10"); 717 | assert_eq!(perft(&position, 1), 46); 718 | assert_eq!(perft(&position, 2), 2079); 719 | assert_eq!(perft(&position, 3), 89890); 720 | } 721 | 722 | #[test] 723 | #[ignore] 724 | fn perft_sixth_expensive() { 725 | let position = 726 | setup("r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10"); 727 | assert_eq!(perft(&position, 4), 3_894_594); 728 | assert_eq!(perft(&position, 5), 164_075_551); 729 | } 730 | 731 | // Other positions. 732 | 733 | #[test] 734 | #[ignore] 735 | fn perft_complex_middlegame() { 736 | let position = setup("rnbq1r1k/pppp1ppp/5n2/4p3/2B1P3/2P2N2/PP3PPP/RNBQK2R w KQ - 1 7"); 737 | assert_eq!(perft(&position, 1), 46); 738 | assert_eq!(perft(&position, 2), 1149); 739 | assert_eq!(perft(&position, 3), 51032); 740 | assert_eq!(perft(&position, 4), 1_352_097); 741 | } 742 | 743 | #[test] 744 | fn perft_endgame_promotions() { 745 | let position = setup("8/Pk6/8/8/8/8/6KP/8 w - - 0 1"); 746 | assert_eq!(perft(&position, 1), 13); 747 | assert_eq!(perft(&position, 2), 83); 748 | assert_eq!(perft(&position, 3), 949); 749 | assert_eq!(perft(&position, 4), 4848); 750 | assert_eq!(perft(&position, 5), 67834); 751 | assert_eq!(perft(&position, 6), 390_018); 752 | } 753 | 754 | #[test] 755 | fn perft_pawn_endgame() { 756 | let position = setup("8/8/1p4k1/1P6/8/8/6K1/8 w - - 0 1"); 757 | assert_eq!(perft(&position, 1), 8); 758 | assert_eq!(perft(&position, 2), 64); 759 | assert_eq!(perft(&position, 3), 358); 760 | assert_eq!(perft(&position, 4), 2362); 761 | assert_eq!(perft(&position, 5), 15118); 762 | assert_eq!(perft(&position, 6), 99412); 763 | } 764 | 765 | #[test] 766 | fn perft_queen_endgame() { 767 | let position = setup("8/8/8/8/8/4k3/6Q1/6K1 w - - 0 1"); 768 | assert_eq!(perft(&position, 1), 25); 769 | assert_eq!(perft(&position, 2), 97); 770 | assert_eq!(perft(&position, 3), 2422); 771 | assert_eq!(perft(&position, 4), 11436); 772 | assert_eq!(perft(&position, 5), 291_937); 773 | } 774 | 775 | #[test] 776 | #[ignore] 777 | fn perft_tactical_opening() { 778 | let position = setup("r1bqkb1r/pppppppp/2n5/8/8/4PN2/PPPPBPPP/RNBQK2R w KQkq - 0 1"); 779 | assert_eq!(perft(&position, 1), 29); 780 | assert_eq!(perft(&position, 2), 605); 781 | assert_eq!(perft(&position, 3), 18210); 782 | assert_eq!(perft(&position, 4), 413_607); 783 | } 784 | 785 | #[test] 786 | fn perft_advanced_pawn_race() { 787 | let position = setup("8/5k2/6p1/8/8/8/1p3P2/5K2 w - - 0 1"); 788 | assert_eq!(perft(&position, 1), 6); 789 | assert_eq!(perft(&position, 2), 72); 790 | assert_eq!(perft(&position, 3), 461); 791 | assert_eq!(perft(&position, 4), 5919); 792 | assert_eq!(perft(&position, 5), 38616); 793 | assert_eq!(perft(&position, 6), 565_553); 794 | } 795 | 796 | #[test] 797 | fn perft_queen_vs_pawns() { 798 | let position = setup("8/5k2/6p1/8/8/8/1p3P2/5K2 w - - 0 1"); 799 | assert_eq!(perft(&position, 1), 6); 800 | assert_eq!(perft(&position, 2), 72); 801 | assert_eq!(perft(&position, 3), 461); 802 | assert_eq!(perft(&position, 4), 5919); 803 | assert_eq!(perft(&position, 5), 38616); 804 | assert_eq!(perft(&position, 6), 565_553); 805 | } 806 | 807 | #[test] 808 | fn perft_promotion_options() { 809 | let position = setup("8/8/2P5/3k4/8/2K5/8/8 w - - 0 1"); 810 | assert_eq!(perft(&position, 5), 23744); 811 | } 812 | 813 | #[test] 814 | #[ignore] 815 | fn perft_cpw_challenge() { 816 | let position = setup("rnb1kbnr/pp1pp1pp/1qp2p2/8/Q1P5/N7/PP1PPPPP/1RB1KBNR b Kkq - 2 4"); 817 | assert_eq!(perft(&position, 7), 14_794_751_816); 818 | } 819 | 820 | #[test] 821 | fn repetition_hash() { 822 | let mut position = setup("8/5k2/6p1/8/8/8/1p3P2/5K2 w - - 0 1"); 823 | let initial_hash = position.hash(); 824 | position.make_move(&Move::from_uci("f1e2").expect("valid move")); 825 | assert_ne!(initial_hash, position.hash()); 826 | position.make_move(&Move::from_uci("f7f6").expect("valid move")); 827 | assert_ne!(initial_hash, position.hash()); 828 | position.make_move(&Move::from_uci("e2f1").expect("valid move")); 829 | assert_ne!(initial_hash, position.hash()); 830 | position.make_move(&Move::from_uci("f6f7").expect("valid move")); 831 | assert_eq!(position.to_string(), "8/5k2/6p1/8/8/8/1p3P2/5K2 w - - 4 3"); 832 | assert_eq!(initial_hash, position.hash()); 833 | } 834 | 835 | #[test] 836 | fn en_passant_hash() { 837 | assert_ne!( 838 | setup("6qk/8/8/3Pp3/8/8/K7/8 w - e6 0 1").hash(), 839 | setup("6qk/8/8/3Pp3/8/8/K7/8 w - - 0 1").hash() 840 | ); 841 | } 842 | 843 | #[test] 844 | fn castling_hash() { 845 | let mut position = setup("rnbqk1nr/p3bppp/1p2p3/2ppP3/3P4/P7/1PP1NPPP/R1BQKBNR w KQkq - 0 7"); 846 | let initial_hash = position.hash(); 847 | assert_ne!( 848 | initial_hash, 849 | setup("rnbqk1nr/p3bppp/1p2p3/2ppP3/3P4/P7/1PP1NPPP/R1BQKBNR w Qkq - 0 7").hash(), 850 | ); 851 | position.make_move(&Move::from_uci("e1d2").expect("valid move")); 852 | position.make_move(&Move::from_uci("e8d7").expect("valid move")); 853 | position.make_move(&Move::from_uci("d2e1").expect("valid move")); 854 | position.make_move(&Move::from_uci("d7e8").expect("valid move")); 855 | assert_eq!( 856 | position.to_string(), 857 | "rnbqk1nr/p3bppp/1p2p3/2ppP3/3P4/P7/1PP1NPPP/R1BQKBNR w - - 4 9" 858 | ); 859 | assert_ne!(initial_hash, position.hash()); 860 | } 861 | 862 | #[test] 863 | fn move_clock_hash() { 864 | assert_eq!( 865 | setup("6qk/8/8/3Pp3/8/8/K7/8 w - - 0 1").hash(), 866 | setup("6qk/8/8/3Pp3/8/8/K7/8 w - - 2 3").hash() 867 | ); 868 | } 869 | -------------------------------------------------------------------------------- /src/chess/core.rs: -------------------------------------------------------------------------------- 1 | //! Chess primitives commonly used within [`crate::chess`]. 2 | 3 | use std::fmt::{self, Write}; 4 | use std::mem; 5 | 6 | use anyhow::bail; 7 | use itertools::Itertools; 8 | 9 | use crate::chess::bitboard::Bitboard; 10 | use crate::environment::Player; 11 | 12 | #[allow(missing_docs, reason = "Self-explanatory constants")] 13 | pub const BOARD_WIDTH: u8 = 8; 14 | #[allow(missing_docs, reason = "Self-explanatory constants")] 15 | pub const BOARD_SIZE: u8 = BOARD_WIDTH * BOARD_WIDTH; 16 | 17 | /// Represents any kind of a legal chess move. A move is the only way to mutate 18 | /// [`crate::chess::position::Position`] and change the board state. Moves are 19 | /// not sorted according to their potential "value" by the move generator. The 20 | /// move representation has one-to-one correspondence with the UCI move 21 | /// representation. The moves can also be indexed and fed as an input to the 22 | /// Neural Network evaluators that would be able assess their potential without 23 | /// evaluating post-states. 24 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 25 | pub struct Move(u16); 26 | 27 | impl Move { 28 | // First 6 bits are reserved for the `from` square. 29 | const FROM_MASK: u16 = 0b0000_0000_0011_1111; 30 | // Next 3 bits are reserved for the promotion (if any). 31 | const PROMOTION_MASK: u16 = 0b0111_0000_0000_0000; 32 | const PROMOTION_OFFSET: u8 = 12; 33 | const TO_MASK: u16 = 0b0000_1111_1100_0000; 34 | // Next 6 bits are reserved for the `to` square. 35 | const TO_OFFSET: u8 = 6; 36 | 37 | #[must_use] 38 | pub fn new(from: Square, to: Square, promotion: Option) -> Self { 39 | debug_assert_ne!( 40 | from, to, 41 | "moves should have different source and target squares" 42 | ); 43 | let mut packed = from as u16 | ((to as u16) << Self::TO_OFFSET); 44 | if let Some(promo) = promotion { 45 | packed |= (promo as u16) << Self::PROMOTION_OFFSET; 46 | } 47 | Self(packed) 48 | } 49 | 50 | #[must_use] 51 | pub(super) fn from(&self) -> Square { 52 | let square = self.0 & Self::FROM_MASK; 53 | Square::try_from(square as u8).unwrap() 54 | } 55 | 56 | #[must_use] 57 | pub(super) fn to(&self) -> Square { 58 | let square = (self.0 & Self::TO_MASK) >> Self::TO_OFFSET; 59 | Square::try_from(square as u8).unwrap() 60 | } 61 | 62 | #[must_use] 63 | pub(super) fn promotion(&self) -> Option { 64 | let promo = (self.0 & Self::PROMOTION_MASK) >> Self::PROMOTION_OFFSET; 65 | unsafe { std::mem::transmute(promo as u8) } 66 | } 67 | 68 | /// Converts the move from UCI format to the internal representation. This 69 | /// is important for the communication between the engine and UCI server in 70 | /// `position` command. 71 | pub fn from_uci(uci: &str) -> anyhow::Result { 72 | Self::try_from(uci) 73 | } 74 | 75 | /// Converts the move from the perspective of one player to the other, as if 76 | /// the other player's backrank is rank 1. 77 | /// 78 | /// This is very useful for encoding the moves to reduce the action space 79 | /// and pass them as actions to the policy network. 80 | /// 81 | /// # Example 82 | /// 83 | /// ``` 84 | /// use pabi::chess::core::{Move, Square}; 85 | /// 86 | /// assert_eq!( 87 | /// Move::new(Square::E1, Square::E8, None).flip_perspective(), 88 | /// Move::new(Square::E8, Square::E1, None) 89 | /// ); 90 | /// ``` 91 | pub fn flip_perspective(&self) -> Self { 92 | Self::new( 93 | self.from().flip_perspective(), 94 | self.to().flip_perspective(), 95 | self.promotion(), 96 | ) 97 | } 98 | } 99 | 100 | impl TryFrom<&str> for Move { 101 | type Error = anyhow::Error; 102 | 103 | fn try_from(uci: &str) -> anyhow::Result { 104 | match uci.len() { 105 | 4 => Ok(Self::new( 106 | Square::try_from(&uci[..2])?, 107 | Square::try_from(&uci[2..4])?, 108 | None, 109 | )), 110 | 5 => Ok(Self::new( 111 | Square::try_from(&uci[..2])?, 112 | Square::try_from(&uci[2..4])?, 113 | Some(Promotion::from(uci.chars().nth(4).unwrap())), 114 | )), 115 | _ => bail!("UCI move should be 4 or 5 characters long, got {uci}"), 116 | } 117 | } 118 | } 119 | 120 | impl fmt::Display for Move { 121 | /// Serializes a move to UCI-compatible representation. 122 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 123 | write!(f, "{}{}", self.from(), self.to())?; 124 | if let Some(promotion) = self.promotion() { 125 | write!(f, "{}", PieceKind::from(promotion))?; 126 | } 127 | Ok(()) 128 | } 129 | } 130 | 131 | /// Size of [`MoveList`] and an upper bound of moves in a chess position (which 132 | /// [seems to be 218](https://www.chessprogramming.org/Chess_Position). 256 provides the best 133 | /// performance through optimal memory alignment. 134 | const MAX_MOVES: usize = 256; 135 | 136 | /// Moves are stored on stack to avoid memory allocations and improve 137 | /// performance. This is important for performance reasons and also prevents 138 | /// unnecessary copying that would occur if the moves would be stored in 139 | /// `std::Vec` with unknown capacity. 140 | pub type MoveList = arrayvec::ArrayVec; 141 | 142 | /// Board squares: from left to right, from bottom to the top ([Little-Endian Rank-File Mapping]): 143 | /// 144 | /// ``` 145 | /// use pabi::chess::core::Square; 146 | /// 147 | /// assert_eq!(Square::A1 as u8, 0); 148 | /// assert_eq!(Square::E1 as u8, 4); 149 | /// assert_eq!(Square::H1 as u8, 7); 150 | /// assert_eq!(Square::A4 as u8, 8 * 3); 151 | /// assert_eq!(Square::H8 as u8, 63); 152 | /// ``` 153 | /// 154 | /// Square is a compact representation using only one byte. 155 | /// 156 | /// ``` 157 | /// use pabi::chess::core::Square; 158 | /// use std::mem::size_of; 159 | /// 160 | /// assert_eq!(size_of::(), 1); 161 | /// ``` 162 | /// 163 | /// [Little-Endian Rank-File Mapping]: https://www.chessprogramming.org/Square_Mapping_Considerations#LittleEndianRankFileMapping 164 | #[repr(u8)] 165 | #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] 166 | #[rustfmt::skip] 167 | #[allow(missing_docs, reason = "Enum variants are self-explanatory")] 168 | pub enum Square { 169 | A1, B1, C1, D1, E1, F1, G1, H1, 170 | A2, B2, C2, D2, E2, F2, G2, H2, 171 | A3, B3, C3, D3, E3, F3, G3, H3, 172 | A4, B4, C4, D4, E4, F4, G4, H4, 173 | A5, B5, C5, D5, E5, F5, G5, H5, 174 | A6, B6, C6, D6, E6, F6, G6, H6, 175 | A7, B7, C7, D7, E7, F7, G7, H7, 176 | A8, B8, C8, D8, E8, F8, G8, H8, 177 | } 178 | 179 | impl Square { 180 | /// Connects file (column) and rank (row) to form a full square. 181 | #[must_use] 182 | pub const fn new(file: File, rank: Rank) -> Self { 183 | unsafe { mem::transmute(file as u8 + (rank as u8) * BOARD_WIDTH) } 184 | } 185 | 186 | /// Returns file (column) on which the square is located. 187 | #[must_use] 188 | pub const fn file(self) -> File { 189 | unsafe { mem::transmute(self as u8 % BOARD_WIDTH) } 190 | } 191 | 192 | /// Returns rank (row) on which the square is located. 193 | #[must_use] 194 | pub const fn rank(self) -> Rank { 195 | unsafe { mem::transmute(self as u8 / BOARD_WIDTH) } 196 | } 197 | 198 | #[must_use] 199 | pub fn shift(self, direction: Direction) -> Option { 200 | let shift: i8 = match direction { 201 | Direction::Up => BOARD_WIDTH as i8, 202 | Direction::Down => -(BOARD_WIDTH as i8), 203 | }; 204 | Self::try_from(self as i8 + shift).ok() 205 | } 206 | 207 | /// "Flips" the square vertically, i.e. returns the square as if the board 208 | /// is rotated 180° (POV of other player) and ranks are mirrored (rank 8 209 | /// becomes rank 1, rank 7 becomes rank 2 and so on). 210 | /// 211 | /// This is useful for encoding the moves from the perspective of the other 212 | /// player to compress action space. 213 | /// 214 | /// # Example 215 | /// 216 | /// ``` 217 | /// use pabi::chess::core::Square; 218 | /// 219 | /// assert_eq!(Square::E1.flip_perspective(), Square::E8); 220 | /// assert_eq!(Square::D4.flip_perspective(), Square::D5); 221 | /// ``` 222 | #[must_use] 223 | pub fn flip_perspective(self) -> Self { 224 | unsafe { mem::transmute(56 ^ self as u8) } 225 | } 226 | 227 | fn next(self) -> Option { 228 | let next = self as u8 + 1; 229 | if next == BOARD_SIZE { 230 | None 231 | } else { 232 | Some(unsafe { mem::transmute(next) }) 233 | } 234 | } 235 | 236 | /// Creates an iterator over all squares, starting from A1 (0) to H8 (63). 237 | #[must_use] 238 | pub fn iter() -> SquareIterator { 239 | SquareIterator { 240 | current: Some(Self::A1), 241 | } 242 | } 243 | } 244 | 245 | impl TryFrom for Square { 246 | type Error = anyhow::Error; 247 | 248 | /// Creates a square given its position on the board. 249 | /// 250 | /// # Errors 251 | /// 252 | /// If given square index is outside 0..[`BOARD_SIZE`] range. 253 | fn try_from(square_index: u8) -> anyhow::Result { 254 | match square_index { 255 | 0..BOARD_SIZE => Ok(unsafe { mem::transmute(square_index) }), 256 | _ => bail!("square index should be in 0..BOARD_SIZE, got {square_index}"), 257 | } 258 | } 259 | } 260 | 261 | impl TryFrom for Square { 262 | type Error = anyhow::Error; 263 | 264 | /// Creates a square given its position on the board. 265 | /// 266 | /// # Errors 267 | /// 268 | /// If given square index is outside 0..[`BOARD_SIZE`] range. 269 | fn try_from(square_index: i8) -> anyhow::Result { 270 | const MAX_INDEX: i8 = BOARD_SIZE as i8; 271 | match square_index { 272 | 0..MAX_INDEX => Ok(unsafe { mem::transmute(square_index) }), 273 | _ => bail!("square index should be in 0..BOARD_SIZE, got {square_index}"), 274 | } 275 | } 276 | } 277 | 278 | impl TryFrom<&str> for Square { 279 | type Error = anyhow::Error; 280 | 281 | fn try_from(square: &str) -> anyhow::Result { 282 | let (file, rank) = match square.chars().collect_tuple() { 283 | Some((file, rank)) => (file, rank), 284 | None => bail!( 285 | "square should be two-char, got {square} with {} chars", 286 | square.len() 287 | ), 288 | }; 289 | Ok(Self::new(file.try_into()?, rank.try_into()?)) 290 | } 291 | } 292 | 293 | /// Iterates over squares in the order from A1 to H8, from left to right, from 294 | /// bottom to the top. 295 | pub struct SquareIterator { 296 | current: Option, 297 | } 298 | 299 | impl Iterator for SquareIterator { 300 | type Item = Square; 301 | 302 | fn next(&mut self) -> Option { 303 | let result = self.current; 304 | self.current = self.current.and_then(Square::next); 305 | result 306 | } 307 | } 308 | 309 | impl fmt::Display for Square { 310 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 311 | write!(f, "{}{}", self.file(), self.rank()) 312 | } 313 | } 314 | 315 | /// Represents a column (vertical row) of the chessboard. In chess notation, it 316 | /// is normally represented with a lowercase letter. 317 | #[repr(u8)] 318 | #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] 319 | #[allow(missing_docs, reason = "Enum variants are self-explanatory")] 320 | pub enum File { 321 | A, 322 | B, 323 | C, 324 | D, 325 | E, 326 | F, 327 | G, 328 | H, 329 | } 330 | 331 | impl fmt::Display for File { 332 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 333 | write!(f, "{}", (b'a' + *self as u8) as char) 334 | } 335 | } 336 | 337 | impl TryFrom for File { 338 | type Error = anyhow::Error; 339 | 340 | fn try_from(file: char) -> anyhow::Result { 341 | match file { 342 | 'a'..='h' => Ok(unsafe { mem::transmute(file as u8 - b'a') }), 343 | _ => bail!("file should be within 'a'..='h', got '{file}'"), 344 | } 345 | } 346 | } 347 | 348 | impl TryFrom for File { 349 | type Error = anyhow::Error; 350 | 351 | fn try_from(column: u8) -> anyhow::Result { 352 | match column { 353 | 0..=7 => Ok(unsafe { mem::transmute(column) }), 354 | _ => bail!("file should be within 0..BOARD_WIDTH, got {column}"), 355 | } 356 | } 357 | } 358 | 359 | /// Represents a horizontal row of the chessboard. In chess notation, it is 360 | /// represented with a number. The implementation assumes zero-based values 361 | /// (i.e. rank 1 would be 0). 362 | #[repr(u8)] 363 | #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] 364 | #[allow(missing_docs, reason = "Enum variants are self-explanatory")] 365 | pub enum Rank { 366 | Rank1, 367 | Rank2, 368 | Rank3, 369 | Rank4, 370 | Rank5, 371 | Rank6, 372 | Rank7, 373 | Rank8, 374 | } 375 | 376 | impl Rank { 377 | /// Returns a pre-calculated bitboard mask with 1s set for squares of the 378 | /// given rank. 379 | pub(super) const fn mask(self) -> Bitboard { 380 | match self { 381 | Self::Rank1 => Bitboard::from_bits(0x0000_0000_0000_00FF), 382 | Self::Rank2 => Bitboard::from_bits(0x0000_0000_0000_FF00), 383 | Self::Rank3 => Bitboard::from_bits(0x0000_0000_00FF_0000), 384 | Self::Rank4 => Bitboard::from_bits(0x0000_0000_FF00_0000), 385 | Self::Rank5 => Bitboard::from_bits(0x0000_00FF_0000_0000), 386 | Self::Rank6 => Bitboard::from_bits(0x0000_FF00_0000_0000), 387 | Self::Rank7 => Bitboard::from_bits(0x00FF_0000_0000_0000), 388 | Self::Rank8 => Bitboard::from_bits(0xFF00_0000_0000_0000), 389 | } 390 | } 391 | 392 | pub(super) const fn backrank(player: Player) -> Self { 393 | match player { 394 | Player::White => Self::Rank1, 395 | Player::Black => Self::Rank8, 396 | } 397 | } 398 | 399 | pub(super) const fn pawns_starting(player: Player) -> Self { 400 | match player { 401 | Player::White => Self::Rank2, 402 | Player::Black => Self::Rank7, 403 | } 404 | } 405 | } 406 | 407 | impl TryFrom for Rank { 408 | type Error = anyhow::Error; 409 | 410 | fn try_from(rank: char) -> anyhow::Result { 411 | match rank { 412 | '1'..='8' => Ok(unsafe { mem::transmute(rank as u8 - b'1') }), 413 | _ => bail!("rank should be within '1'..='8', got '{rank}'"), 414 | } 415 | } 416 | } 417 | 418 | impl TryFrom for Rank { 419 | type Error = anyhow::Error; 420 | 421 | fn try_from(row: u8) -> anyhow::Result { 422 | match row { 423 | 0..=7 => Ok(unsafe { mem::transmute(row) }), 424 | _ => bail!("rank should be within 0..BOARD_WIDTH, got {row}"), 425 | } 426 | } 427 | } 428 | 429 | impl fmt::Display for Rank { 430 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 431 | write!(f, "{}", *self as u8 + 1) 432 | } 433 | } 434 | 435 | /// Standard [chess pieces] types for one player. 436 | /// 437 | /// [chess pieces]: https://en.wikipedia.org/wiki/Chess_piece 438 | #[allow(missing_docs, reason = "Enum variants are self-explanatory")] 439 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd)] 440 | pub enum PieceKind { 441 | Pawn, 442 | Knight, 443 | Bishop, 444 | Rook, 445 | Queen, 446 | King, 447 | } 448 | 449 | impl From for PieceKind { 450 | fn from(promotion: Promotion) -> Self { 451 | match promotion { 452 | Promotion::Knight => Self::Knight, 453 | Promotion::Bishop => Self::Bishop, 454 | Promotion::Rook => Self::Rook, 455 | Promotion::Queen => Self::Queen, 456 | } 457 | } 458 | } 459 | 460 | impl fmt::Display for PieceKind { 461 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 462 | f.write_char(match &self { 463 | Self::Pawn => 'p', 464 | Self::Knight => 'n', 465 | Self::Bishop => 'b', 466 | Self::Rook => 'r', 467 | Self::Queen => 'q', 468 | Self::King => 'k', 469 | }) 470 | } 471 | } 472 | 473 | /// Represents a specific piece owned by a player. 474 | pub struct Piece { 475 | #[allow(missing_docs, reason = "Enum variants are self-explanatory")] 476 | pub player: Player, 477 | #[allow(missing_docs, reason = "Enum variants are self-explanatory")] 478 | pub kind: PieceKind, 479 | } 480 | 481 | impl Piece { 482 | #[must_use] 483 | pub const fn plane(&self) -> usize { 484 | self.player as usize * 6 + self.kind as usize 485 | } 486 | } 487 | 488 | impl TryFrom for Piece { 489 | type Error = anyhow::Error; 490 | 491 | fn try_from(symbol: char) -> anyhow::Result { 492 | match symbol { 493 | 'P' => Ok(Self { 494 | player: Player::White, 495 | kind: PieceKind::Pawn, 496 | }), 497 | 'N' => Ok(Self { 498 | player: Player::White, 499 | kind: PieceKind::Knight, 500 | }), 501 | 'B' => Ok(Self { 502 | player: Player::White, 503 | kind: PieceKind::Bishop, 504 | }), 505 | 'R' => Ok(Self { 506 | player: Player::White, 507 | kind: PieceKind::Rook, 508 | }), 509 | 'Q' => Ok(Self { 510 | player: Player::White, 511 | kind: PieceKind::Queen, 512 | }), 513 | 'K' => Ok(Self { 514 | player: Player::White, 515 | kind: PieceKind::King, 516 | }), 517 | 'p' => Ok(Self { 518 | player: Player::Black, 519 | kind: PieceKind::Pawn, 520 | }), 521 | 'n' => Ok(Self { 522 | player: Player::Black, 523 | kind: PieceKind::Knight, 524 | }), 525 | 'b' => Ok(Self { 526 | player: Player::Black, 527 | kind: PieceKind::Bishop, 528 | }), 529 | 'r' => Ok(Self { 530 | player: Player::Black, 531 | kind: PieceKind::Rook, 532 | }), 533 | 'k' => Ok(Self { 534 | player: Player::Black, 535 | kind: PieceKind::King, 536 | }), 537 | 'q' => Ok(Self { 538 | player: Player::Black, 539 | kind: PieceKind::Queen, 540 | }), 541 | _ => bail!("piece symbol should be in \"PNBRQKpnbrqk\", got '{symbol}'"), 542 | } 543 | } 544 | } 545 | 546 | impl fmt::Display for Piece { 547 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 548 | f.write_char(match (&self.player, &self.kind) { 549 | // White: uppercase symbols. 550 | (Player::White, PieceKind::Pawn) => 'P', 551 | (Player::White, PieceKind::Knight) => 'N', 552 | (Player::White, PieceKind::Bishop) => 'B', 553 | (Player::White, PieceKind::Rook) => 'R', 554 | (Player::White, PieceKind::Queen) => 'Q', 555 | (Player::White, PieceKind::King) => 'K', 556 | // Black: lowercase symbols. 557 | (Player::Black, PieceKind::Pawn) => 'p', 558 | (Player::Black, PieceKind::Knight) => 'n', 559 | (Player::Black, PieceKind::Bishop) => 'b', 560 | (Player::Black, PieceKind::Rook) => 'r', 561 | (Player::Black, PieceKind::Queen) => 'q', 562 | (Player::Black, PieceKind::King) => 'k', 563 | }) 564 | } 565 | } 566 | 567 | bitflags::bitflags! { 568 | /// Track the ability to [castle] each side (kingside is often referred to 569 | /// as `O-O` castle, queenside -- `O-O-O`). 570 | /// 571 | /// Castling is possible if the following conditions are met: 572 | /// 573 | /// - The king and the castling rook must not have previously moved. 574 | /// - No square from the king's initial square to its final square may be under 575 | /// attack by an enemy piece. 576 | /// - All the squares between the king's initial and final squares 577 | /// (including the final square), and all the squares between the castling 578 | /// rook's initial and final squares (including the final square), must be 579 | /// vacant except for the king and castling rook. 580 | /// 581 | /// Castling is relatively straightforward in the Standard Chess but is 582 | /// often misunderstood in Chess960. An easy mnemonic is that the king and 583 | /// the rook end up on the same files for both Standard and FRC: 584 | /// 585 | /// - When castling short (`O-O`), the king ends up on [`File::G`] and the 586 | /// rook on [`File::F`] 587 | /// - When castling long (`O-O-O`), the king ends up on [`File::C`] and the 588 | /// rook on [`File::D`] 589 | /// 590 | /// [castle]: https://www.chessprogramming.org/Castling 591 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 592 | pub struct CastleRights : u8 { 593 | #[allow(missing_docs, reason = "Enum variants are self-explanatory")] 594 | const NONE = 0; 595 | #[allow(missing_docs, reason = "Enum variants are self-explanatory")] 596 | const WHITE_SHORT = 0b0001; 597 | #[allow(missing_docs, reason = "Enum variants are self-explanatory")] 598 | const WHITE_LONG = 0b0010; 599 | #[allow(missing_docs, reason = "Enum variants are self-explanatory")] 600 | const WHITE_BOTH = Self::WHITE_SHORT.bits() | Self::WHITE_LONG.bits(); 601 | #[allow(missing_docs, reason = "Enum variants are self-explanatory")] 602 | const BLACK_SHORT = 0b0100; 603 | #[allow(missing_docs, reason = "Enum variants are self-explanatory")] 604 | const BLACK_LONG = 0b1000; 605 | #[allow(missing_docs, reason = "Enum variants are self-explanatory")] 606 | const BLACK_BOTH = Self::BLACK_SHORT.bits() | Self::BLACK_LONG.bits(); 607 | #[allow(missing_docs, reason = "Enum variants are self-explanatory")] 608 | const ALL = Self::WHITE_BOTH.bits() | Self::BLACK_BOTH.bits(); 609 | } 610 | } 611 | 612 | impl TryFrom<&str> for CastleRights { 613 | type Error = anyhow::Error; 614 | 615 | /// Parses [`CastleRights`] for both players from the FEN format. The user 616 | /// is responsible for providing valid input cleaned up from the actual FEN 617 | /// chunk. 618 | /// 619 | /// # Errors 620 | /// 621 | /// Returns [`anyhow::Error`] if given pattern does not match 622 | /// 623 | /// [`CastleRights`] := (K)? (Q)? (k)? (q)? 624 | /// 625 | /// Note that both letters have to be either uppercase or lowercase. 626 | fn try_from(input: &str) -> anyhow::Result { 627 | // Enumerate all possibilities. 628 | match input.as_bytes() { 629 | // K Q k q 630 | // - - - - 631 | // 0 0 0 0 632 | b"-" => Ok(Self::NONE), 633 | // 0 0 0 1 634 | b"q" => Ok(Self::BLACK_LONG), 635 | // 0 0 1 0 636 | b"k" => Ok(Self::BLACK_SHORT), 637 | // 0 0 1 1 638 | b"kq" => Ok(Self::BLACK_BOTH), 639 | // 0 1 0 0 640 | b"Q" => Ok(Self::WHITE_LONG), 641 | // 0 1 0 1 642 | b"Qq" => Ok(Self::WHITE_LONG | Self::BLACK_LONG), 643 | // 0 1 1 0 644 | b"Qk" => Ok(Self::WHITE_LONG | Self::BLACK_SHORT), 645 | // 0 1 1 1 646 | b"Qkq" => Ok(Self::WHITE_LONG | Self::BLACK_BOTH), 647 | // 1 0 0 0 648 | b"K" => Ok(Self::WHITE_SHORT), 649 | // 1 0 0 1 650 | b"Kq" => Ok(Self::WHITE_SHORT | Self::BLACK_LONG), 651 | // 1 0 1 0 652 | b"Kk" => Ok(Self::WHITE_SHORT | Self::BLACK_SHORT), 653 | // 1 0 1 1 654 | b"Kkq" => Ok(Self::WHITE_SHORT | Self::BLACK_BOTH), 655 | // 1 1 0 0 656 | b"KQ" => Ok(Self::WHITE_BOTH), 657 | // 1 1 0 1 658 | b"KQq" => Ok(Self::WHITE_BOTH | Self::BLACK_LONG), 659 | // 1 1 1 0 660 | b"KQk" => Ok(Self::WHITE_BOTH | Self::BLACK_SHORT), 661 | // 1 1 1 1 662 | b"KQkq" => Ok(Self::ALL), 663 | _ => bail!("unknown castle rights: {input}"), 664 | } 665 | } 666 | } 667 | 668 | impl fmt::Display for CastleRights { 669 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 670 | if *self == Self::NONE { 671 | return f.write_char('-'); 672 | } 673 | if *self & Self::WHITE_SHORT != Self::NONE { 674 | f.write_char('K')?; 675 | } 676 | if *self & Self::WHITE_LONG != Self::NONE { 677 | f.write_char('Q')?; 678 | } 679 | if *self & Self::BLACK_SHORT != Self::NONE { 680 | f.write_char('k')?; 681 | } 682 | if *self & Self::BLACK_LONG != Self::NONE { 683 | f.write_char('q')?; 684 | } 685 | Ok(()) 686 | } 687 | } 688 | 689 | /// A pawn can be promoted to a queen, rook, bishop or a knight. 690 | #[allow(missing_docs, reason = "Enum variants are self-explanatory")] 691 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd)] 692 | pub enum Promotion { 693 | Knight = 1, 694 | Bishop = 2, 695 | Rook = 3, 696 | Queen = 4, 697 | } 698 | 699 | impl From for Promotion { 700 | fn from(c: char) -> Self { 701 | match c { 702 | 'n' => Self::Knight, 703 | 'b' => Self::Bishop, 704 | 'r' => Self::Rook, 705 | 'q' => Self::Queen, 706 | _ => unreachable!("unknown promotion piece, has to be in 'kbrq': {c}"), 707 | } 708 | } 709 | } 710 | 711 | /// Directions on the board from a perspective of White player. 712 | /// 713 | /// Traditionally those are North (Up), West (Left), East (Right), South (Down) 714 | /// and their combinations. However, using cardinal directions is confusing, 715 | /// hence they are replaced by relative directions. 716 | #[derive(Copy, Clone, Debug)] 717 | pub enum Direction { 718 | /// Also known as North. 719 | Up, 720 | /// Also known as South. 721 | Down, 722 | } 723 | 724 | impl Direction { 725 | pub(super) const fn opposite(self) -> Self { 726 | match self { 727 | Self::Up => Self::Down, 728 | Self::Down => Self::Up, 729 | } 730 | } 731 | } 732 | 733 | #[cfg(test)] 734 | mod tests { 735 | use std::mem::{size_of, size_of_val}; 736 | 737 | use pretty_assertions::assert_eq; 738 | 739 | use super::*; 740 | 741 | #[test] 742 | fn rank() { 743 | assert_eq!( 744 | ('1'..='9') 745 | .filter_map(|ch| Rank::try_from(ch).ok()) 746 | .collect::>(), 747 | vec![ 748 | Rank::Rank1, 749 | Rank::Rank2, 750 | Rank::Rank3, 751 | Rank::Rank4, 752 | Rank::Rank5, 753 | Rank::Rank6, 754 | Rank::Rank7, 755 | Rank::Rank8, 756 | ] 757 | ); 758 | assert_eq!( 759 | ('1'..='9') 760 | .filter_map(|idx| Rank::try_from(idx).ok()) 761 | .collect::>(), 762 | vec![ 763 | Rank::Rank1, 764 | Rank::Rank2, 765 | Rank::Rank3, 766 | Rank::Rank4, 767 | Rank::Rank5, 768 | Rank::Rank6, 769 | Rank::Rank7, 770 | Rank::Rank8, 771 | ] 772 | ); 773 | } 774 | 775 | #[test] 776 | #[should_panic(expected = "rank should be within '1'..='8', got '9'")] 777 | fn rank_from_incorrect_char() { 778 | let _ = Rank::try_from('9').unwrap(); 779 | } 780 | 781 | #[test] 782 | #[should_panic(expected = "rank should be within '1'..='8', got '0'")] 783 | fn rank_from_incorrect_char_zero() { 784 | let _ = Rank::try_from('0').unwrap(); 785 | } 786 | 787 | #[test] 788 | #[should_panic(expected = "rank should be within 0..BOARD_WIDTH, got 8")] 789 | fn rank_from_incorrect_index() { 790 | let _ = Rank::try_from(BOARD_WIDTH).unwrap(); 791 | } 792 | 793 | #[test] 794 | fn file() { 795 | assert_eq!( 796 | ('a'..='i') 797 | .filter_map(|ch| File::try_from(ch).ok()) 798 | .collect::>(), 799 | vec![ 800 | File::A, 801 | File::B, 802 | File::C, 803 | File::D, 804 | File::E, 805 | File::F, 806 | File::G, 807 | File::H, 808 | ] 809 | ); 810 | assert_eq!( 811 | (0..=BOARD_WIDTH) 812 | .filter_map(|idx| File::try_from(idx).ok()) 813 | .collect::>(), 814 | vec![ 815 | File::A, 816 | File::B, 817 | File::C, 818 | File::D, 819 | File::E, 820 | File::F, 821 | File::G, 822 | File::H, 823 | ] 824 | ); 825 | } 826 | 827 | #[test] 828 | #[should_panic(expected = "file should be within 'a'..='h', got 'i'")] 829 | fn file_from_incorrect_char() { 830 | let _ = File::try_from('i').unwrap(); 831 | } 832 | 833 | #[test] 834 | #[should_panic(expected = "file should be within 0..BOARD_WIDTH, got 8")] 835 | fn file_from_incorrect_index() { 836 | let _ = File::try_from(BOARD_WIDTH).unwrap(); 837 | } 838 | 839 | #[test] 840 | fn square() { 841 | let squares: Vec<_> = [ 842 | 0u8, 843 | BOARD_SIZE - 1, 844 | BOARD_WIDTH - 1, 845 | BOARD_WIDTH, 846 | BOARD_WIDTH * 2 + 5, 847 | BOARD_SIZE, 848 | ] 849 | .iter() 850 | .filter_map(|square| Square::try_from(*square).ok()) 851 | .collect(); 852 | assert_eq!( 853 | squares, 854 | vec![Square::A1, Square::H8, Square::H1, Square::A2, Square::F3,] 855 | ); 856 | let squares: Vec<_> = [ 857 | (File::B, Rank::Rank3), 858 | (File::F, Rank::Rank5), 859 | (File::H, Rank::Rank8), 860 | (File::E, Rank::Rank4), 861 | ] 862 | .iter() 863 | .map(|(file, rank)| Square::new(*file, *rank)) 864 | .collect(); 865 | assert_eq!( 866 | squares, 867 | vec![Square::B3, Square::F5, Square::H8, Square::E4] 868 | ); 869 | 870 | assert_eq!(Square::try_from(4u8).unwrap(), Square::E1); 871 | assert_eq!(Square::try_from(4i8).unwrap(), Square::E1); 872 | } 873 | 874 | #[test] 875 | #[should_panic(expected = "square index should be in 0..BOARD_SIZE, got 64")] 876 | fn square_from_incorrect_index() { 877 | let _ = Square::try_from(BOARD_SIZE).unwrap(); 878 | } 879 | 880 | #[test] 881 | fn primitive_size() { 882 | // Primitives will have small size thanks to the niche optimizations. 883 | assert_eq!(size_of::(), 1); 884 | assert_eq!(size_of::>(), 1); 885 | let square_to_pieces: [Option; BOARD_SIZE as usize] = 886 | [None; BOARD_SIZE as usize]; 887 | assert_eq!(size_of_val(&square_to_pieces), BOARD_SIZE as usize); 888 | } 889 | 890 | #[test] 891 | fn square_shift() { 892 | assert_eq!(Square::A2.shift(Direction::Up), Some(Square::A3)); 893 | assert_eq!(Square::B5.shift(Direction::Down), Some(Square::B4)); 894 | assert_eq!(Square::C1.shift(Direction::Down), None); 895 | assert_eq!(Square::G8.shift(Direction::Up), None); 896 | } 897 | 898 | #[test] 899 | fn correct_moves_from_uci() { 900 | assert_eq!( 901 | Move::from_uci("e2e4").unwrap(), 902 | Move::new(Square::E2, Square::E4, None) 903 | ); 904 | assert_eq!( 905 | Move::from_uci("e7e8").unwrap(), 906 | Move::new(Square::E7, Square::E8, None) 907 | ); 908 | assert_eq!( 909 | Move::from_uci("e7e8q").unwrap(), 910 | Move::new(Square::E7, Square::E8, Some(Promotion::Queen)) 911 | ); 912 | } 913 | } 914 | --------------------------------------------------------------------------------