├── .codecov.yml ├── fatalii ├── src │ ├── main.rs │ └── lib.rs ├── Cargo.toml └── tests │ └── cli.rs ├── .pre-commit-config.yaml ├── tuner ├── src │ ├── lib.rs │ ├── training.rs │ ├── file_reader.rs │ ├── error_function.rs │ ├── main.rs │ ├── optimizer.rs │ └── feature_evaluator.rs └── Cargo.toml ├── uci ├── src │ ├── uci_in.rs │ ├── lib.rs │ ├── uci_in │ │ ├── quit.rs │ │ ├── stop.rs │ │ ├── is_ready.rs │ │ ├── ucinewgame.rs │ │ ├── uci.rs │ │ ├── debug.rs │ │ ├── set_option.rs │ │ ├── position.rs │ │ └── go.rs │ ├── parser.rs │ ├── uci_score.rs │ ├── uci_out.rs │ └── uci_option.rs ├── Cargo.toml └── tests │ └── test_buffer.rs ├── eval ├── Cargo.toml └── src │ ├── lib.rs │ ├── eval.rs │ ├── score_pair.rs │ ├── game_phase.rs │ ├── mobility.rs │ └── pawn_structure.rs ├── movegen ├── src │ ├── direction.rs │ ├── lib.rs │ ├── piece_targets.rs │ ├── side.rs │ ├── rook.rs │ ├── bishop.rs │ ├── move_generator │ │ ├── king_not_xrayed_generator.rs │ │ ├── king_xrayed_generator.rs │ │ └── king_in_check_generator.rs │ ├── queen.rs │ ├── repetition_tracker.rs │ ├── performance_tester.rs │ ├── castling_squares.rs │ ├── file.rs │ ├── rank.rs │ ├── king.rs │ ├── knight.rs │ ├── attacks_to.rs │ └── piece.rs ├── Cargo.toml ├── benches │ └── bench_movegen.rs └── tests │ └── perft.rs ├── Cargo.toml ├── engine ├── src │ ├── lib.rs │ ├── engine_out.rs │ ├── engine_options.rs │ ├── best_move_handler.rs │ └── engine.rs ├── Cargo.toml └── tests │ ├── mock_engine_out.rs │ └── engine.rs ├── .github └── workflows │ ├── tests.yml │ ├── bench.yml │ ├── lint.yml │ └── coverage.yml ├── .gitignore ├── search ├── Cargo.toml ├── src │ ├── lib.rs │ ├── search_options.rs │ ├── counter_table.rs │ ├── lmr_table.rs │ ├── move_candidates.rs │ ├── node_counter.rs │ ├── history_table.rs │ ├── pv_table.rs │ ├── time_manager.rs │ ├── search.rs │ ├── search_params.rs │ ├── aspiration_window.rs │ └── searcher.rs └── benches │ └── bench_search.rs ├── LICENSE └── README.md /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: 75% 6 | patch: off 7 | -------------------------------------------------------------------------------- /fatalii/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | if let Err(e) = fatalii::run() { 3 | eprintln!("{e}"); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/doublify/pre-commit-rust 3 | rev: v1.0 4 | hooks: 5 | - id: fmt 6 | - id: cargo-check 7 | - id: clippy 8 | -------------------------------------------------------------------------------- /tuner/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod error_function; 2 | pub mod eval_params; 3 | pub mod feature_evaluator; 4 | pub mod file_reader; 5 | pub mod optimizer; 6 | 7 | mod position_features; 8 | mod training; 9 | -------------------------------------------------------------------------------- /uci/src/uci_in.rs: -------------------------------------------------------------------------------- 1 | pub mod debug; 2 | pub mod go; 3 | pub mod is_ready; 4 | pub mod position; 5 | pub mod quit; 6 | pub mod set_option; 7 | pub mod stop; 8 | pub mod uci; 9 | pub mod ucinewgame; 10 | -------------------------------------------------------------------------------- /uci/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use parser::{Parser, ParserMessage}; 2 | pub use uci_out::UciOut; 3 | 4 | pub mod uci_in; 5 | pub mod uci_option; 6 | 7 | mod parser; 8 | mod uci_move; 9 | mod uci_out; 10 | mod uci_score; 11 | -------------------------------------------------------------------------------- /eval/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "eval" 3 | version = "0.1.0" 4 | authors = ["Patrick Heck <49785565+FitzOReilly@users.noreply.github.com>"] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | movegen = { path = "../movegen" } 9 | -------------------------------------------------------------------------------- /movegen/src/direction.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Copy, Debug)] 2 | pub enum Direction { 3 | North = 0, 4 | South = 1, 5 | East = 2, 6 | West = 3, 7 | NorthEast = 4, 8 | NorthWest = 5, 9 | SouthEast = 6, 10 | SouthWest = 7, 11 | } 12 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [profile.release-lto] 2 | inherits = "release" 3 | lto = true 4 | 5 | [workspace] 6 | members = [ 7 | "engine", 8 | "eval", 9 | "fatalii", 10 | "movegen", 11 | "search", 12 | "tuner", 13 | "uci", 14 | ] 15 | resolver = "2" 16 | -------------------------------------------------------------------------------- /fatalii/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fatalii" 3 | version = "0.9.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | engine = { path = "../engine" } 8 | eval = { path = "../eval" } 9 | search = { path = "../search" } 10 | uci = { path = "../uci" } 11 | 12 | [dev-dependencies] 13 | assert_matches = "1.5.0" 14 | rexpect = "0.6.0" 15 | -------------------------------------------------------------------------------- /engine/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use crate::engine::{Engine, EngineError}; 2 | pub use crate::engine_options::{ 3 | EngineOptions, Variant, DEFAULT_HASH_BYTES, DEFAULT_HASH_MB, DEFAULT_MOVE_OVERHEAD_MILLIS, 4 | }; 5 | pub use crate::engine_out::EngineOut; 6 | 7 | mod best_move_handler; 8 | mod engine; 9 | mod engine_options; 10 | mod engine_out; 11 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push] 4 | 5 | env: 6 | CARGO_TERM_COLOR: always 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Build 16 | run: cargo build --verbose 17 | - name: Run tests 18 | run: cargo test --verbose 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | -------------------------------------------------------------------------------- /eval/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use crate::eval::Eval; 2 | pub use crate::game_phase::GamePhase; 3 | pub use crate::score::{Score, ScoreVariant, BLACK_WIN, EQ_POSITION, NEG_INF, POS_INF, WHITE_WIN}; 4 | 5 | pub mod complex; 6 | pub mod eval; 7 | pub mod material_mobility; 8 | pub mod mobility; 9 | pub mod params; 10 | pub mod pawn_structure; 11 | pub mod score; 12 | pub mod score_pair; 13 | 14 | mod game_phase; 15 | -------------------------------------------------------------------------------- /engine/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "engine" 3 | version = "0.1.0" 4 | authors = ["Patrick Heck <49785565+FitzOReilly@users.noreply.github.com>"] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | crossbeam-channel = "0.5" 9 | thiserror = "2.0.11" 10 | movegen = { path = "../movegen" } 11 | search = { path = "../search" } 12 | 13 | [dev-dependencies] 14 | more-asserts = "0.3" 15 | eval = { path = "../eval" } 16 | -------------------------------------------------------------------------------- /movegen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "movegen" 3 | version = "0.1.0" 4 | authors = ["Patrick Heck <49785565+FitzOReilly@users.noreply.github.com>"] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | bitflags = "2.8.0" 9 | smallvec = "1.13.2" 10 | thiserror = "2.0.11" 11 | 12 | [dev-dependencies] 13 | criterion = "0.5.1" 14 | rand = "0.9" 15 | 16 | [[bench]] 17 | name = "bench_movegen" 18 | harness = false 19 | -------------------------------------------------------------------------------- /search/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "search" 3 | version = "0.1.0" 4 | authors = ["Patrick Heck <49785565+FitzOReilly@users.noreply.github.com>"] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | crossbeam-channel = "0.5" 9 | eval = { path = "../eval" } 10 | movegen = { path = "../movegen" } 11 | 12 | [dev-dependencies] 13 | criterion = "0.5.1" 14 | 15 | [[bench]] 16 | name = "bench_search" 17 | harness = false 18 | -------------------------------------------------------------------------------- /uci/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "uci" 3 | version = "0.1.0" 4 | authors = ["Patrick Heck <49785565+FitzOReilly@users.noreply.github.com>"] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | regex = "1" 9 | thiserror = "2.0.11" 10 | engine = { path = "../engine" } 11 | eval = { path = "../eval" } 12 | movegen = { path = "../movegen" } 13 | search = { path = "../search" } 14 | 15 | [dev-dependencies] 16 | assert_matches = "1.5.0" 17 | -------------------------------------------------------------------------------- /engine/src/engine_out.rs: -------------------------------------------------------------------------------- 1 | use movegen::r#move::Move; 2 | use search::search::SearchResult; 3 | use std::error::Error; 4 | 5 | pub trait EngineOut { 6 | fn info_depth_finished( 7 | &self, 8 | search_result: Option, 9 | ) -> Result<(), Box>; 10 | 11 | fn info_string(&self, s: &str) -> Result<(), Box>; 12 | 13 | fn best_move(&self, search_result: Option) -> Result<(), Box>; 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/bench.yml: -------------------------------------------------------------------------------- 1 | name: Benchmarks 2 | 3 | on: [push] 4 | 5 | env: 6 | CARGO_TERM_COLOR: always 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Build 16 | run: cargo build --release --verbose 17 | - name: Run expensive tests 18 | run: cargo test --release --verbose -- --ignored 19 | - name: Run benchmarks 20 | run: cargo bench --verbose 21 | -------------------------------------------------------------------------------- /search/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use search_options::SearchOptions; 2 | 3 | pub mod alpha_beta; 4 | pub mod aspiration_window; 5 | pub mod search; 6 | pub mod search_params; 7 | pub mod searcher; 8 | 9 | mod alpha_beta_entry; 10 | mod counter_table; 11 | mod history_table; 12 | mod lmr_table; 13 | mod move_candidates; 14 | mod move_selector; 15 | mod node_counter; 16 | mod pv_table; 17 | mod search_data; 18 | mod search_options; 19 | mod static_exchange_eval; 20 | mod time_manager; 21 | -------------------------------------------------------------------------------- /eval/src/eval.rs: -------------------------------------------------------------------------------- 1 | use crate::Score; 2 | use movegen::{position::Position, side::Side}; 3 | 4 | pub trait Eval { 5 | fn eval(&mut self, pos: &Position) -> Score; 6 | 7 | fn eval_relative(&mut self, pos: &Position) -> Score { 8 | match pos.side_to_move() { 9 | Side::White => self.eval(pos), 10 | Side::Black => -self.eval(pos), 11 | } 12 | } 13 | } 14 | 15 | pub trait HasMatingMaterial { 16 | fn has_mating_material(&self, s: Side) -> bool; 17 | } 18 | -------------------------------------------------------------------------------- /tuner/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tuner" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | clap = { version = "4.5.28", features = ["derive"] } 10 | nalgebra = { version = "0.33.2", features = ["serde-serialize"] } 11 | nalgebra-sparse = "0.10.0" 12 | rand = "0.9.0" 13 | serde = "1.0.217" 14 | serde_json = "1.0.138" 15 | eval = { path = "../eval" } 16 | movegen = { path = "../movegen" } 17 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Set up Python 13 | uses: actions/setup-python@v5 14 | with: 15 | python-version: 3 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | python -m pip install pre-commit 20 | - name: Lint 21 | run: | 22 | pre-commit run --all-files --show-diff-on-failure 23 | -------------------------------------------------------------------------------- /uci/src/uci_in/quit.rs: -------------------------------------------------------------------------------- 1 | use crate::parser::{ParserMessage, UciError}; 2 | use crate::UciOut; 3 | use engine::Engine; 4 | use std::error::Error; 5 | 6 | pub fn run_command( 7 | _uci_out: &mut UciOut, 8 | args: &str, 9 | _engine: &mut Engine, 10 | ) -> Result, Box> { 11 | // There must be no arguments after "quit" 12 | if !args.trim().is_empty() { 13 | return Err(Box::new(UciError::InvalidArgument( 14 | args.trim_end().to_string(), 15 | ))); 16 | } 17 | 18 | Ok(Some(ParserMessage::Quit)) 19 | } 20 | -------------------------------------------------------------------------------- /uci/src/uci_in/stop.rs: -------------------------------------------------------------------------------- 1 | use crate::parser::{ParserMessage, UciError}; 2 | use crate::UciOut; 3 | use engine::Engine; 4 | use std::error::Error; 5 | 6 | pub fn run_command( 7 | _uci_out: &mut UciOut, 8 | args: &str, 9 | engine: &mut Engine, 10 | ) -> Result, Box> { 11 | // There must be no arguments after "stop" 12 | if !args.trim().is_empty() { 13 | return Err(Box::new(UciError::InvalidArgument( 14 | args.trim_end().to_string(), 15 | ))); 16 | } 17 | 18 | engine.stop(); 19 | Ok(None) 20 | } 21 | -------------------------------------------------------------------------------- /uci/src/uci_in/is_ready.rs: -------------------------------------------------------------------------------- 1 | use crate::parser::{ParserMessage, UciError}; 2 | use crate::UciOut; 3 | use engine::Engine; 4 | use std::error::Error; 5 | 6 | pub fn run_command( 7 | uci_out: &mut UciOut, 8 | args: &str, 9 | _engine: &mut Engine, 10 | ) -> Result, Box> { 11 | // There must be no arguments after "isready" 12 | if !args.trim().is_empty() { 13 | return Err(Box::new(UciError::InvalidArgument( 14 | args.trim_end().to_string(), 15 | ))); 16 | } 17 | 18 | uci_out.ready_ok()?; 19 | Ok(None) 20 | } 21 | -------------------------------------------------------------------------------- /uci/src/uci_in/ucinewgame.rs: -------------------------------------------------------------------------------- 1 | use crate::parser::{ParserMessage, UciError}; 2 | use crate::UciOut; 3 | use engine::Engine; 4 | use std::error::Error; 5 | 6 | pub fn run_command( 7 | _uci_out: &mut UciOut, 8 | args: &str, 9 | engine: &mut Engine, 10 | ) -> Result, Box> { 11 | // There must be no arguments after "ucinewgame" 12 | if !args.trim().is_empty() { 13 | return Err(Box::new(UciError::InvalidArgument( 14 | args.trim_end().to_string(), 15 | ))); 16 | } 17 | 18 | engine.clear_position_history(); 19 | Ok(None) 20 | } 21 | -------------------------------------------------------------------------------- /movegen/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod bishop; 2 | pub mod bitboard; 3 | pub mod fen; 4 | pub mod file; 5 | pub mod king; 6 | pub mod knight; 7 | pub mod r#move; 8 | pub mod move_generator; 9 | pub mod pawn; 10 | pub mod performance_tester; 11 | pub mod piece; 12 | pub mod position; 13 | pub mod position_history; 14 | pub mod queen; 15 | pub mod rank; 16 | pub mod rook; 17 | pub mod side; 18 | pub mod square; 19 | pub mod transposition_table; 20 | pub mod zobrist; 21 | 22 | mod attacks_to; 23 | mod castling_squares; 24 | mod direction; 25 | mod piece_targets; 26 | mod ray; 27 | mod ray_lookup_tables; 28 | mod repetition_tracker; 29 | -------------------------------------------------------------------------------- /uci/src/uci_in/uci.rs: -------------------------------------------------------------------------------- 1 | use crate::parser::{ParserMessage, UciError}; 2 | use crate::UciOut; 3 | use engine::Engine; 4 | use std::error::Error; 5 | 6 | pub fn run_command( 7 | uci_out: &mut UciOut, 8 | args: &str, 9 | _engine: &mut Engine, 10 | ) -> Result, Box> { 11 | // There must be no arguments after "uci" 12 | if !args.trim().is_empty() { 13 | return Err(Box::new(UciError::InvalidArgument( 14 | args.trim_end().to_string(), 15 | ))); 16 | } 17 | 18 | uci_out.id()?; 19 | uci_out.all_options()?; 20 | uci_out.uci_ok()?; 21 | Ok(None) 22 | } 23 | -------------------------------------------------------------------------------- /search/src/search_options.rs: -------------------------------------------------------------------------------- 1 | use movegen::r#move::MoveList; 2 | use std::time::Duration; 3 | 4 | #[derive(Clone, Debug, Default)] 5 | pub struct SearchOptions { 6 | pub search_moves: Option, 7 | pub ponder: bool, 8 | pub white_time: Option, 9 | pub black_time: Option, 10 | pub white_inc: Option, 11 | pub black_inc: Option, 12 | pub moves_to_go: Option, 13 | pub depth: Option, 14 | pub nodes: Option, 15 | pub mate_in: Option, 16 | pub movetime: Option, 17 | pub infinite: bool, 18 | pub move_overhead: Duration, 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | 3 | on: [push] 4 | 5 | jobs: 6 | coverage: 7 | runs-on: ubuntu-latest 8 | env: 9 | CARGO_TERM_COLOR: always 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Install Rust 13 | run: rustup update stable 14 | - name: Install cargo-llvm-cov 15 | uses: taiki-e/install-action@cargo-llvm-cov 16 | - name: Generate code coverage 17 | run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info 18 | - name: Upload coverage to Codecov 19 | uses: codecov/codecov-action@v4 20 | with: 21 | files: lcov.info 22 | fail_ci_if_error: true 23 | token: ${{ secrets.CODECOV_TOKEN }} 24 | -------------------------------------------------------------------------------- /movegen/src/piece_targets.rs: -------------------------------------------------------------------------------- 1 | use crate::bitboard::Bitboard; 2 | use crate::piece::Piece; 3 | use crate::square::Square; 4 | 5 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 6 | pub struct PieceTargets { 7 | piece: Piece, 8 | origin: Square, 9 | targets: Bitboard, 10 | } 11 | 12 | impl PieceTargets { 13 | pub fn new(piece: Piece, origin: Square, targets: Bitboard) -> PieceTargets { 14 | PieceTargets { 15 | piece, 16 | origin, 17 | targets, 18 | } 19 | } 20 | 21 | pub fn piece(&self) -> Piece { 22 | self.piece 23 | } 24 | 25 | pub fn origin(&self) -> Square { 26 | self.origin 27 | } 28 | 29 | pub fn targets(&self) -> Bitboard { 30 | self.targets 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /uci/src/uci_in/debug.rs: -------------------------------------------------------------------------------- 1 | use crate::parser::{ParserMessage, UciError}; 2 | use crate::UciOut; 3 | use engine::{Engine, EngineOut}; 4 | use std::error::Error; 5 | 6 | pub fn run_command( 7 | uci_out: &mut UciOut, 8 | args: &str, 9 | _engine: &mut Engine, 10 | ) -> Result, Box> { 11 | match args.trim() { 12 | "on" => { 13 | uci_out.set_debug(true); 14 | uci_out.info_string("debug on")?; 15 | } 16 | "off" => { 17 | uci_out.info_string("debug off")?; 18 | uci_out.set_debug(false); 19 | } 20 | _ => { 21 | return Err(Box::new(UciError::InvalidArgument(format!( 22 | "debug {}", 23 | args.trim_end() 24 | )))); 25 | } 26 | } 27 | 28 | Ok(None) 29 | } 30 | -------------------------------------------------------------------------------- /engine/src/engine_options.rs: -------------------------------------------------------------------------------- 1 | use movegen::file::File; 2 | use std::time::Duration; 3 | 4 | pub const DEFAULT_HASH_MB: usize = 16; 5 | pub const DEFAULT_HASH_BYTES: usize = DEFAULT_HASH_MB * 2_usize.pow(20); 6 | 7 | pub const DEFAULT_MOVE_OVERHEAD_MILLIS: usize = 10; 8 | 9 | #[derive(Clone, Debug)] 10 | pub struct EngineOptions { 11 | pub hash_size: usize, 12 | pub move_overhead: Duration, 13 | pub variant: Variant, 14 | } 15 | 16 | #[derive(Clone, Copy, Debug)] 17 | pub enum Variant { 18 | Standard, 19 | Chess960(File, File), // Kingside castling file, queenside castling file 20 | } 21 | 22 | impl Default for EngineOptions { 23 | fn default() -> Self { 24 | EngineOptions { 25 | hash_size: DEFAULT_HASH_BYTES, 26 | move_overhead: Duration::from_millis(DEFAULT_MOVE_OVERHEAD_MILLIS as u64), 27 | variant: Variant::Standard, 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /search/src/counter_table.rs: -------------------------------------------------------------------------------- 1 | use movegen::{piece::Piece, r#move::Move, square::Square}; 2 | 3 | #[derive(Debug, Clone)] 4 | pub struct CounterTable { 5 | table: [Move; Piece::NUM_PIECES * Square::NUM_SQUARES], 6 | } 7 | 8 | impl CounterTable { 9 | pub fn new() -> Self { 10 | CounterTable { 11 | table: [Move::NULL; Piece::NUM_PIECES * Square::NUM_SQUARES], 12 | } 13 | } 14 | 15 | pub fn update(&mut self, p: Piece, to: Square, m: Move) { 16 | self.table[Self::idx(p, to)] = m; 17 | } 18 | 19 | pub fn counter(&self, p: Piece, to: Square) -> Move { 20 | self.table[Self::idx(p, to)] 21 | } 22 | 23 | pub fn clear(&mut self) { 24 | for entry in self.table.iter_mut() { 25 | *entry = Move::NULL; 26 | } 27 | } 28 | 29 | fn idx(p: Piece, s: Square) -> usize { 30 | p.idx() * Square::NUM_SQUARES + s.idx() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /movegen/src/side.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Not; 2 | 3 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 4 | #[repr(u8)] 5 | pub enum Side { 6 | White = 0, 7 | Black = 1, 8 | } 9 | 10 | impl Not for Side { 11 | type Output = Self; 12 | 13 | fn not(self) -> Self::Output { 14 | match self { 15 | Self::White => Self::Black, 16 | Self::Black => Self::White, 17 | } 18 | } 19 | } 20 | 21 | impl Not for &Side { 22 | type Output = Side; 23 | 24 | fn not(self) -> Self::Output { 25 | match self { 26 | Self::Output::White => Self::Output::Black, 27 | Self::Output::Black => Self::Output::White, 28 | } 29 | } 30 | } 31 | 32 | #[cfg(test)] 33 | mod tests { 34 | use super::*; 35 | 36 | #[test] 37 | fn not() { 38 | assert_eq!(Side::Black, !Side::White); 39 | assert_eq!(Side::White, !Side::Black); 40 | assert_eq!(Side::Black, !&Side::White); 41 | assert_eq!(Side::White, !&Side::Black); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /search/src/lmr_table.rs: -------------------------------------------------------------------------------- 1 | const LEN_DEPTH: usize = 64; 2 | const LEN_MOVE_COUNT: usize = 64; 3 | 4 | #[derive(Debug, Clone)] 5 | pub struct LmrTable { 6 | table: [[u8; 64]; 64], 7 | } 8 | 9 | impl LmrTable { 10 | pub fn new(centi_base: usize, centi_divisor: usize) -> Self { 11 | let mut table = [[0; LEN_MOVE_COUNT]; LEN_DEPTH]; 12 | let base = centi_base as f64 / 100.0; 13 | let divisor = centi_divisor as f64 / 100.0; 14 | for (depth, table_row) in table.iter_mut().enumerate().skip(1) { 15 | let log_depth = (depth as f64).log2(); 16 | for (move_count, reduction) in table_row.iter_mut().enumerate().skip(1) { 17 | let log_move_count = (move_count as f64).log2(); 18 | *reduction = (base + log_depth * log_move_count / divisor) as u8; 19 | } 20 | } 21 | Self { table } 22 | } 23 | 24 | pub fn late_move_depth_reduction(&self, depth: usize, move_count: usize) -> usize { 25 | self.table[depth.min(LEN_DEPTH - 1)][move_count.min(LEN_MOVE_COUNT - 1)].into() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Patrick Heck 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /engine/tests/mock_engine_out.rs: -------------------------------------------------------------------------------- 1 | use engine::EngineOut; 2 | use movegen::r#move::Move; 3 | use search::search::SearchResult; 4 | use std::error::Error; 5 | 6 | pub struct MockEngineOut { 7 | search_info_callback: Box)>, 8 | best_move_callback: Box)>, 9 | } 10 | 11 | unsafe impl Send for MockEngineOut {} 12 | unsafe impl Sync for MockEngineOut {} 13 | 14 | impl EngineOut for MockEngineOut { 15 | fn info_depth_finished( 16 | &self, 17 | search_result: Option, 18 | ) -> Result<(), Box> { 19 | (self.search_info_callback)(search_result); 20 | Ok(()) 21 | } 22 | 23 | fn info_string(&self, _s: &str) -> Result<(), Box> { 24 | Ok(()) 25 | } 26 | 27 | fn best_move(&self, search_result: Option) -> Result<(), Box> { 28 | (self.best_move_callback)(search_result); 29 | Ok(()) 30 | } 31 | } 32 | 33 | impl MockEngineOut { 34 | pub fn new( 35 | search_info_callback: Box)>, 36 | best_move_callback: Box)>, 37 | ) -> Self { 38 | Self { 39 | search_info_callback, 40 | best_move_callback, 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tuner/src/training.rs: -------------------------------------------------------------------------------- 1 | use movegen::position::Position; 2 | 3 | use crate::position_features::{EvalType, FeatureVector, PositionFeatures}; 4 | 5 | #[derive(Debug, Clone, Copy)] 6 | pub enum Outcome { 7 | WhiteWin, 8 | Draw, 9 | BlackWin, 10 | } 11 | 12 | #[derive(Debug, Clone)] 13 | pub struct TrainingPosition { 14 | pub pos: Position, 15 | pub outcome: Outcome, 16 | } 17 | 18 | #[derive(Debug, Clone)] 19 | pub struct TrainingFeatures { 20 | pub features: PositionFeatures, 21 | pub grad: FeatureVector, 22 | pub outcome: Outcome, 23 | } 24 | 25 | impl From<&TrainingPosition> for TrainingFeatures { 26 | fn from(tp: &TrainingPosition) -> Self { 27 | let features = PositionFeatures::from(&tp.pos); 28 | let grad = features.grad(); 29 | Self { 30 | features, 31 | grad, 32 | outcome: tp.outcome, 33 | } 34 | } 35 | } 36 | 37 | impl From<&Outcome> for EvalType { 38 | fn from(o: &Outcome) -> Self { 39 | match o { 40 | Outcome::WhiteWin => 1.0, 41 | Outcome::Draw => 0.5, 42 | Outcome::BlackWin => 0.0, 43 | } 44 | } 45 | } 46 | 47 | impl From for EvalType { 48 | fn from(o: Outcome) -> Self { 49 | Self::from(&o) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /uci/tests/test_buffer.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::sync::{Arc, Mutex}; 3 | 4 | #[derive(Clone, Debug)] 5 | pub struct TestBuffer { 6 | buf: Arc>>, 7 | } 8 | 9 | impl Default for TestBuffer { 10 | fn default() -> Self { 11 | Self::new() 12 | } 13 | } 14 | 15 | impl TestBuffer { 16 | pub fn new() -> Self { 17 | Self { 18 | buf: Arc::new(Mutex::new(Vec::new())), 19 | } 20 | } 21 | 22 | pub fn into_inner(self) -> Vec { 23 | Arc::try_unwrap(self.buf) 24 | .expect("More than one Arc refers to the inner Vec") 25 | .into_inner() 26 | .expect("Error accessing inner value of mutex") 27 | } 28 | 29 | pub fn into_string(self) -> String { 30 | String::from_utf8(self.into_inner()).expect("Error converting Vec to String") 31 | } 32 | 33 | pub fn split_off(&mut self, at: usize) -> Vec { 34 | self.buf.lock().expect("Error locking mutex").split_off(at) 35 | } 36 | } 37 | 38 | impl io::Write for TestBuffer { 39 | fn write(&mut self, buf: &[u8]) -> io::Result { 40 | self.buf.lock().expect("Error locking mutex").write(buf) 41 | } 42 | 43 | fn flush(&mut self) -> io::Result<()> { 44 | self.buf.lock().expect("Error locking mutex").flush() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /movegen/src/rook.rs: -------------------------------------------------------------------------------- 1 | use crate::bitboard::Bitboard; 2 | use crate::ray::Ray; 3 | use crate::square::Square; 4 | 5 | pub struct Rook; 6 | 7 | impl Rook { 8 | pub fn targets(origin: Square, occupied: Bitboard) -> Bitboard { 9 | Ray::file_targets(origin, occupied) | Ray::rank_targets(origin, occupied) 10 | } 11 | } 12 | 13 | #[cfg(test)] 14 | mod tests { 15 | use super::*; 16 | 17 | #[test] 18 | fn rank_and_file_targets() { 19 | assert_eq!( 20 | Bitboard::C4 21 | | Bitboard::D3 22 | | Bitboard::D2 23 | | Bitboard::D5 24 | | Bitboard::D6 25 | | Bitboard::E4 26 | | Bitboard::F4 27 | | Bitboard::G4, 28 | Rook::targets( 29 | Square::D4, 30 | Bitboard::C4 31 | | Bitboard::B4 32 | | Bitboard::A4 33 | | Bitboard::D2 34 | | Bitboard::D6 35 | | Bitboard::G4 36 | ) 37 | ); 38 | } 39 | 40 | #[test] 41 | fn non_blocking_occupancy_targets() { 42 | assert_eq!( 43 | Rook::targets(Square::D4, Bitboard::EMPTY), 44 | Rook::targets( 45 | Square::D4, 46 | Bitboard::C3 | Bitboard::C5 | Bitboard::E3 | Bitboard::E5 47 | ) 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /movegen/src/bishop.rs: -------------------------------------------------------------------------------- 1 | use crate::bitboard::Bitboard; 2 | use crate::ray::Ray; 3 | use crate::square::Square; 4 | 5 | pub struct Bishop; 6 | 7 | impl Bishop { 8 | pub fn targets(origin: Square, occupied: Bitboard) -> Bitboard { 9 | Ray::diagonal_targets(origin, occupied) | Ray::anti_diagonal_targets(origin, occupied) 10 | } 11 | } 12 | 13 | #[cfg(test)] 14 | mod tests { 15 | use super::*; 16 | 17 | #[test] 18 | fn diagonal_targets() { 19 | assert_eq!( 20 | Bitboard::C3 21 | | Bitboard::E3 22 | | Bitboard::F2 23 | | Bitboard::C5 24 | | Bitboard::B6 25 | | Bitboard::E5 26 | | Bitboard::F6 27 | | Bitboard::G7, 28 | Bishop::targets( 29 | Square::D4, 30 | Bitboard::C3 31 | | Bitboard::B2 32 | | Bitboard::A1 33 | | Bitboard::F2 34 | | Bitboard::B6 35 | | Bitboard::G7 36 | ) 37 | ); 38 | } 39 | 40 | #[test] 41 | fn non_blocking_occupancy_targets() { 42 | assert_eq!( 43 | Bishop::targets(Square::D4, Bitboard::EMPTY), 44 | Bishop::targets( 45 | Square::D4, 46 | Bitboard::C4 | Bitboard::D3 | Bitboard::D5 | Bitboard::E4 47 | ) 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /eval/src/score_pair.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Add, AddAssign, Mul, Sub, SubAssign}; 2 | 3 | use crate::Score; 4 | 5 | #[derive(Debug, Clone, Copy)] 6 | pub struct ScorePair(pub Score, pub Score); 7 | 8 | impl Mul for ScorePair { 9 | type Output = Self; 10 | 11 | fn mul(self, rhs: Score) -> Self::Output { 12 | Self(self.0 * rhs, self.1 * rhs) 13 | } 14 | } 15 | 16 | impl Mul<&Score> for ScorePair { 17 | type Output = Self; 18 | 19 | fn mul(self, rhs: &Score) -> Self::Output { 20 | self * *rhs 21 | } 22 | } 23 | 24 | impl Mul for Score { 25 | type Output = ScorePair; 26 | 27 | fn mul(self, rhs: ScorePair) -> Self::Output { 28 | ScorePair(self * rhs.0, self * rhs.1) 29 | } 30 | } 31 | 32 | impl Mul<&ScorePair> for Score { 33 | type Output = ScorePair; 34 | 35 | fn mul(self, rhs: &ScorePair) -> Self::Output { 36 | self * *rhs 37 | } 38 | } 39 | 40 | impl Add for ScorePair { 41 | type Output = Self; 42 | 43 | fn add(self, rhs: Self) -> Self::Output { 44 | Self(self.0 + rhs.0, self.1 + rhs.1) 45 | } 46 | } 47 | 48 | impl AddAssign for ScorePair { 49 | fn add_assign(&mut self, rhs: Self) { 50 | *self = *self + rhs; 51 | } 52 | } 53 | 54 | impl Sub for ScorePair { 55 | type Output = Self; 56 | 57 | fn sub(self, rhs: Self) -> Self::Output { 58 | Self(self.0 - rhs.0, self.1 - rhs.1) 59 | } 60 | } 61 | 62 | impl SubAssign for ScorePair { 63 | fn sub_assign(&mut self, rhs: Self) { 64 | *self = *self - rhs 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /movegen/benches/bench_movegen.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion, Throughput}; 2 | use movegen::fen::Fen; 3 | use movegen::performance_tester::PerformanceTester; 4 | use movegen::position::Position; 5 | use movegen::position_history::PositionHistory; 6 | 7 | fn perft(c: &mut Criterion, group_name: &str, pos: Position, min_depth: usize, max_depth: usize) { 8 | let bytes = 32 * 64 * 1024; 9 | 10 | let mut group = c.benchmark_group(group_name); 11 | for depth in min_depth..=max_depth { 12 | group.throughput(Throughput::Elements(depth as u64)); 13 | group.bench_with_input(BenchmarkId::from_parameter(depth), &depth, |b, &depth| { 14 | b.iter_batched( 15 | || PerformanceTester::new(PositionHistory::new(pos.clone()), bytes), 16 | |mut perft| perft.count_nodes(depth), 17 | BatchSize::SmallInput, 18 | ); 19 | }); 20 | } 21 | group.finish(); 22 | } 23 | 24 | fn perft_initial_position(c: &mut Criterion) { 25 | let group_name = "Perft initial position"; 26 | let pos = Position::initial(); 27 | let min_depth = 0; 28 | let max_depth = 5; 29 | 30 | perft(c, group_name, pos, min_depth, max_depth); 31 | } 32 | 33 | fn perft_middlegame_position(c: &mut Criterion) { 34 | let group_name = "Perft middlegame position"; 35 | // Position from https://www.chessprogramming.org/Perft_Results 36 | let fen = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1"; 37 | let pos = Fen::str_to_pos(fen).unwrap(); 38 | let min_depth = 0; 39 | let max_depth = 4; 40 | 41 | perft(c, group_name, pos, min_depth, max_depth); 42 | } 43 | 44 | criterion_group!(benches, perft_initial_position, perft_middlegame_position); 45 | criterion_main!(benches); 46 | -------------------------------------------------------------------------------- /movegen/src/move_generator/king_not_xrayed_generator.rs: -------------------------------------------------------------------------------- 1 | use crate::move_generator::move_generator_template::MoveGeneratorTemplate; 2 | 3 | use crate::attacks_to::AttacksTo; 4 | use crate::bitboard::Bitboard; 5 | use crate::square::Square; 6 | 7 | // Since the king is not xrayed, legality checks are only done for: 8 | // - King move targets 9 | // - Castles 10 | pub struct KingNotXrayedGenerator; 11 | 12 | impl MoveGeneratorTemplate for KingNotXrayedGenerator { 13 | fn non_capture_target_filter(attacks_to_king: &AttacksTo, targets: Bitboard) -> Bitboard { 14 | targets & !attacks_to_king.pos.occupancy() 15 | } 16 | 17 | fn capture_target_filter(attacks_to_king: &AttacksTo, targets: Bitboard) -> Bitboard { 18 | targets 19 | & attacks_to_king 20 | .pos 21 | .side_occupancy(!attacks_to_king.pos.side_to_move()) 22 | } 23 | 24 | fn pawn_capture_target_filter(attacks_to_king: &AttacksTo, targets: Bitboard) -> Bitboard { 25 | targets 26 | & (attacks_to_king 27 | .pos 28 | .side_occupancy(!attacks_to_king.pos.side_to_move()) 29 | | attacks_to_king.pos.en_passant_square()) 30 | } 31 | 32 | fn is_legal_non_capture( 33 | _attacks_to_king: &AttacksTo, 34 | _origin: Square, 35 | _target: Square, 36 | ) -> bool { 37 | true 38 | } 39 | 40 | fn is_legal_capture(_attacks_to_king: &AttacksTo, _origin: Square, _target: Square) -> bool { 41 | true 42 | } 43 | 44 | fn is_legal_en_passant_capture( 45 | _attacks_to_king: &AttacksTo, 46 | _origin: Square, 47 | _target: Square, 48 | ) -> bool { 49 | true 50 | } 51 | 52 | fn is_legal_king_move(_attacks_to_king: &AttacksTo, _origin: Square, _target: Square) -> bool { 53 | true 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /uci/src/parser.rs: -------------------------------------------------------------------------------- 1 | use crate::UciOut; 2 | use engine::Engine; 3 | use std::collections::HashMap; 4 | use std::error::Error; 5 | 6 | type UciInputHandler = 7 | dyn Fn(&mut UciOut, &str, &mut Engine) -> Result, Box>; 8 | 9 | #[derive(Debug, PartialEq, Eq)] 10 | pub enum ParserMessage { 11 | Quit, 12 | } 13 | 14 | pub struct Parser { 15 | commands: HashMap>, 16 | uci_out: UciOut, 17 | } 18 | 19 | #[derive(Debug, thiserror::Error)] 20 | pub enum UciError { 21 | #[error("Uci error: Invalid argument `{0}`")] 22 | InvalidArgument(String), 23 | #[error("Uci error: Unknown command `{0}`")] 24 | UnknownCommand(String), 25 | } 26 | 27 | impl Parser { 28 | pub fn new(uci_out: UciOut) -> Self { 29 | Self { 30 | commands: HashMap::new(), 31 | uci_out, 32 | } 33 | } 34 | 35 | pub fn run_command( 36 | &mut self, 37 | s: &str, 38 | engine: &mut Engine, 39 | ) -> Result, Box> { 40 | debug_assert!(s.ends_with('\n') || s.is_empty()); 41 | // From the UCI specification: 42 | // If the engine or the GUI receives an unknown command or token it should just 43 | // ignore it and try to parse the rest of the string in this line. 44 | let mut tail = s; 45 | while let Some((cmd, args)) = split_first_word(tail) { 46 | if let Some(handler) = self.commands.get(cmd) { 47 | return handler(&mut self.uci_out, args, engine); 48 | } 49 | tail = args; 50 | } 51 | Err(Box::new(UciError::UnknownCommand(s.trim_end().to_string()))) 52 | } 53 | 54 | pub fn register_command(&mut self, cmd: String, handler: Box) { 55 | self.commands.insert(cmd, handler); 56 | } 57 | } 58 | 59 | pub fn split_first_word(s: &str) -> Option<(&str, &str)> { 60 | s.trim_start().split_once(|c: char| c.is_whitespace()) 61 | } 62 | -------------------------------------------------------------------------------- /search/src/move_candidates.rs: -------------------------------------------------------------------------------- 1 | use movegen::r#move::{Move, MoveList}; 2 | 3 | #[derive(Debug, Clone)] 4 | pub struct MoveData { 5 | pub r#move: Move, 6 | pub subtree_size: u64, 7 | } 8 | 9 | #[derive(Debug, Default, Clone)] 10 | pub struct MoveCandidates { 11 | pub move_list: Vec, 12 | pub current_idx: usize, 13 | pub alpha_raised_count: usize, 14 | } 15 | 16 | impl From<&MoveList> for MoveCandidates { 17 | fn from(move_list: &MoveList) -> Self { 18 | Self { 19 | move_list: move_list 20 | .iter() 21 | .map(|&x| MoveData { 22 | r#move: x, 23 | subtree_size: 0, 24 | }) 25 | .collect(), 26 | ..Default::default() 27 | } 28 | } 29 | } 30 | 31 | impl MoveCandidates { 32 | pub fn move_to_front(&mut self, best_move: Move) { 33 | let idx = self.index(best_move); 34 | let slice = &mut self.move_list[0..=idx]; 35 | slice.rotate_right(1); 36 | } 37 | 38 | pub fn set_subtree_size(&mut self, m: Move, node_count: u64) { 39 | let idx = self.index(m); 40 | self.move_list[idx].subtree_size = node_count; 41 | } 42 | 43 | pub fn order_by_subtree_size(&mut self) { 44 | self.move_list[self.alpha_raised_count..] 45 | .sort_unstable_by_key(|md| u64::MAX - md.subtree_size); 46 | } 47 | 48 | pub fn reset_counts(&mut self) { 49 | self.current_idx = 0; 50 | self.alpha_raised_count = 0; 51 | } 52 | 53 | fn index(&self, m: Move) -> usize { 54 | self.move_list 55 | .iter() 56 | .position(|x| x.r#move == m) 57 | .unwrap_or_else(|| { 58 | panic!( 59 | "Expected to find move {m} in candidates: {:?}", 60 | self.move_list 61 | .iter() 62 | .map(|x| format!("{}", x.r#move)) 63 | .collect::>(), 64 | ) 65 | }) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /eval/src/game_phase.rs: -------------------------------------------------------------------------------- 1 | use std::cmp; 2 | 3 | use movegen::piece::{self, Piece}; 4 | 5 | const KING_PHASE: usize = 0; 6 | const QUEEN_PHASE: usize = 4; 7 | const ROOK_PHASE: usize = 2; 8 | const BISHOP_PHASE: usize = 1; 9 | const KNIGHT_PHASE: usize = 1; 10 | const PAWN_PHASE: usize = 0; 11 | 12 | #[derive(Debug, Clone, Default)] 13 | pub struct GamePhase(usize); 14 | 15 | impl GamePhase { 16 | pub const MAX: usize = 2 17 | * (KING_PHASE 18 | + QUEEN_PHASE 19 | + 2 * ROOK_PHASE 20 | + 2 * BISHOP_PHASE 21 | + 2 * KNIGHT_PHASE 22 | + 8 * PAWN_PHASE); 23 | 24 | pub fn game_phase_clamped(&self) -> usize { 25 | cmp::min(Self::MAX, self.0) 26 | } 27 | 28 | pub fn add_piece(&mut self, pt: piece::Type) { 29 | self.0 += match pt { 30 | piece::Type::Pawn => PAWN_PHASE, 31 | piece::Type::Knight => KNIGHT_PHASE, 32 | piece::Type::Bishop => BISHOP_PHASE, 33 | piece::Type::Rook => ROOK_PHASE, 34 | piece::Type::Queen => QUEEN_PHASE, 35 | piece::Type::King => KING_PHASE, 36 | }; 37 | } 38 | 39 | pub fn remove_piece(&mut self, pt: piece::Type) { 40 | self.0 -= match pt { 41 | piece::Type::Pawn => PAWN_PHASE, 42 | piece::Type::Knight => KNIGHT_PHASE, 43 | piece::Type::Bishop => BISHOP_PHASE, 44 | piece::Type::Rook => ROOK_PHASE, 45 | piece::Type::Queen => QUEEN_PHASE, 46 | piece::Type::King => KING_PHASE, 47 | }; 48 | } 49 | } 50 | 51 | #[derive(Debug, Clone, Default)] 52 | pub struct PieceCounts([usize; Piece::NUM_PIECES]); 53 | 54 | impl PieceCounts { 55 | pub fn count(&self, p: Piece) -> usize { 56 | self.0[p.idx()] 57 | } 58 | 59 | pub fn add(&mut self, p: Piece) { 60 | debug_assert!(self.0[p.idx()] <= 9); 61 | self.0[p.idx()] += 1; 62 | } 63 | 64 | pub fn remove(&mut self, p: Piece) { 65 | debug_assert_ne!(0, self.0[p.idx()]); 66 | self.0[p.idx()] -= 1; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /uci/src/uci_score.rs: -------------------------------------------------------------------------------- 1 | use eval::{Score, ScoreVariant}; 2 | use std::fmt; 3 | 4 | #[derive(Debug, PartialEq, Eq)] 5 | pub struct UciScore(ScoreVariant); 6 | 7 | impl From for UciScore { 8 | fn from(s: ScoreVariant) -> Self { 9 | Self(s) 10 | } 11 | } 12 | 13 | impl From for UciScore { 14 | fn from(s: Score) -> Self { 15 | Self(ScoreVariant::from(s)) 16 | } 17 | } 18 | 19 | impl fmt::Display for UciScore { 20 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 21 | match self.0 { 22 | ScoreVariant::Centipawns(cp) => write!(f, "cp {cp}"), 23 | ScoreVariant::Mate(_, dist) => write!(f, "mate {dist}"), 24 | } 25 | } 26 | } 27 | 28 | #[cfg(test)] 29 | mod tests { 30 | use super::*; 31 | use movegen::side::Side; 32 | 33 | #[test] 34 | fn score_conversion() { 35 | let s = UciScore::from(0); 36 | assert_eq!(ScoreVariant::Centipawns(0), s.0); 37 | assert_eq!("cp 0", format!("{s}")); 38 | 39 | let s = UciScore::from(ScoreVariant::Mate(Side::White, 0)); 40 | assert_eq!(ScoreVariant::Mate(Side::White, 0), s.0); 41 | assert_eq!("mate 0", format!("{s}")); 42 | let s = UciScore::from(ScoreVariant::Mate(Side::White, 1)); 43 | assert_eq!(ScoreVariant::Mate(Side::White, 1), s.0); 44 | assert_eq!("mate 1", format!("{s}")); 45 | let s = UciScore::from(167); 46 | assert_eq!(ScoreVariant::Centipawns(167), s.0); 47 | assert_eq!("cp 167", format!("{s}")); 48 | 49 | let s = UciScore::from(ScoreVariant::Mate(Side::Black, 0)); 50 | assert_eq!(ScoreVariant::Mate(Side::Black, 0), s.0); 51 | assert_eq!("mate 0", format!("{s}")); 52 | let s = UciScore::from(ScoreVariant::Mate(Side::Black, -1)); 53 | assert_eq!(ScoreVariant::Mate(Side::Black, -1), s.0); 54 | assert_eq!("mate -1", format!("{s}")); 55 | let s = UciScore::from(-225); 56 | assert_eq!(ScoreVariant::Centipawns(-225), s.0); 57 | assert_eq!("cp -225", format!("{s}")); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /fatalii/src/lib.rs: -------------------------------------------------------------------------------- 1 | use engine::{Engine, EngineOptions, DEFAULT_HASH_BYTES}; 2 | use eval::complex::Complex; 3 | use search::alpha_beta::AlphaBeta; 4 | use std::error::Error; 5 | use std::io; 6 | use std::sync::{Arc, Mutex}; 7 | use uci::uci_in::{ 8 | debug, go, is_ready, position, quit, set_option, stop, uci as cmd_uci, ucinewgame, 9 | }; 10 | use uci::UciOut; 11 | use uci::{Parser, ParserMessage}; 12 | 13 | pub fn run() -> Result<(), Box> { 14 | let engine_options = Arc::new(Mutex::new(EngineOptions::default())); 15 | let uci_out = UciOut::new( 16 | Box::new(io::stdout()), 17 | env!("CARGO_PKG_VERSION"), 18 | Arc::clone(&engine_options), 19 | ); 20 | let evaluator = Box::new(Complex::new()); 21 | let search_algo = AlphaBeta::new(evaluator, DEFAULT_HASH_BYTES); 22 | let mut engine = Engine::new(search_algo, uci_out.clone(), engine_options); 23 | 24 | let mut parser = Parser::new(uci_out); 25 | parser.register_command(String::from("debug"), Box::new(debug::run_command)); 26 | parser.register_command(String::from("go"), Box::new(go::run_command)); 27 | parser.register_command(String::from("isready"), Box::new(is_ready::run_command)); 28 | parser.register_command(String::from("position"), Box::new(position::run_command)); 29 | parser.register_command(String::from("quit"), Box::new(quit::run_command)); 30 | parser.register_command(String::from("setoption"), Box::new(set_option::run_command)); 31 | parser.register_command(String::from("stop"), Box::new(stop::run_command)); 32 | parser.register_command(String::from("uci"), Box::new(cmd_uci::run_command)); 33 | parser.register_command( 34 | String::from("ucinewgame"), 35 | Box::new(ucinewgame::run_command), 36 | ); 37 | 38 | let reader = io::stdin(); 39 | let mut buffer = String::new(); 40 | loop { 41 | reader.read_line(&mut buffer)?; 42 | match parser.run_command(&buffer, &mut engine) { 43 | Ok(Some(ParserMessage::Quit)) => break, 44 | Err(e) => eprintln!("{e}"), 45 | _ => {} 46 | } 47 | buffer.clear(); 48 | } 49 | Ok(()) 50 | } 51 | -------------------------------------------------------------------------------- /search/src/node_counter.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | #[derive(Debug, Clone)] 4 | pub struct NodeCounter { 5 | node_counts: Vec>, 6 | eval_count: Vec, 7 | max_depth: usize, 8 | } 9 | 10 | impl NodeCounter { 11 | pub fn new() -> Self { 12 | Self { 13 | node_counts: Vec::new(), 14 | eval_count: Vec::new(), 15 | max_depth: 0, 16 | } 17 | } 18 | 19 | pub fn increment_nodes(&mut self, search_depth: usize, ply: usize) { 20 | self.reserve(search_depth); 21 | self.node_counts[search_depth - 1][ply.min(search_depth)].0 += 1; 22 | } 23 | 24 | pub fn increment_cache_hits(&mut self, search_depth: usize, ply: usize) { 25 | self.reserve(search_depth); 26 | self.node_counts[search_depth - 1][ply.min(search_depth)].1 += 1; 27 | } 28 | 29 | pub fn increment_eval_calls(&mut self, search_depth: usize) { 30 | self.reserve(search_depth); 31 | self.eval_count[search_depth - 1] += 1; 32 | } 33 | 34 | pub fn sum_nodes(&self) -> u64 { 35 | self.node_counts 36 | .iter() 37 | .map(|nc| nc.iter().map(|x| x.0).sum::()) 38 | .sum() 39 | } 40 | 41 | fn reserve(&mut self, search_depth: usize) { 42 | if search_depth > self.max_depth { 43 | debug_assert!(search_depth == self.max_depth + 1); 44 | self.node_counts.push(vec![(0, 0); search_depth + 1]); 45 | self.eval_count.push(0); 46 | self.max_depth += 1; 47 | } 48 | } 49 | } 50 | 51 | impl fmt::Display for NodeCounter { 52 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 53 | for d in 1..=self.max_depth { 54 | writeln!(f, "Search depth {d}:")?; 55 | writeln!(f, "\tEvaluate calls: {}", self.eval_count[d - 1])?; 56 | for p in 0..=d { 57 | let nc = &self.node_counts[d - 1][p]; 58 | writeln!( 59 | f, 60 | "\tPly / moves made / cache hits: {} / {} / {}", 61 | p, nc.0, nc.1, 62 | )?; 63 | } 64 | } 65 | Ok(()) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /search/src/history_table.rs: -------------------------------------------------------------------------------- 1 | use movegen::{ 2 | piece::Piece, 3 | position::Position, 4 | r#move::{Move, MoveList}, 5 | square::Square, 6 | }; 7 | 8 | const MAX_BONUS: i32 = (i16::MAX / 2) as i32; 9 | const HISTORY_DIVISOR: i32 = 16384; 10 | 11 | #[derive(Debug, Clone)] 12 | pub struct HistoryTable { 13 | table: [i16; Piece::NUM_PIECES * Square::NUM_SQUARES], 14 | } 15 | 16 | impl HistoryTable { 17 | pub fn new() -> Self { 18 | HistoryTable { 19 | table: [0; Piece::NUM_PIECES * Square::NUM_SQUARES], 20 | } 21 | } 22 | 23 | pub fn update(&mut self, m: Move, depth: usize, moves_tried: &MoveList, pos: &Position) { 24 | let bonus = Self::bonus(depth); 25 | // Add a bonus to the fail-high move 26 | let piece = pos 27 | .piece_at(m.origin()) 28 | .expect("Expected a piece at move origin"); 29 | self.update_history(piece, m.target(), bonus); 30 | // Subtract a penalty to all other tried moves 31 | for mt in moves_tried.iter() { 32 | let piece = pos 33 | .piece_at(mt.origin()) 34 | .expect("Expected a piece at move origin"); 35 | self.update_history(piece, mt.target(), -bonus); 36 | } 37 | } 38 | 39 | fn bonus(depth: usize) -> i32 { 40 | (16 * (depth * depth) as i32 + 128 * (depth as i32 - 1).max(0)).min(MAX_BONUS) 41 | } 42 | 43 | fn update_history(&mut self, p: Piece, s: Square, delta: i32) { 44 | let idx = Self::idx(p, s); 45 | let current = &mut self.table[idx]; 46 | *current += (delta - *current as i32 * delta.abs() / HISTORY_DIVISOR) as i16; 47 | } 48 | 49 | pub fn value(&self, p: Piece, to: Square) -> i16 { 50 | self.table[Self::idx(p, to)] 51 | } 52 | 53 | pub fn clear(&mut self) { 54 | for entry in self.table.iter_mut() { 55 | *entry = 0; 56 | } 57 | } 58 | 59 | // Reduce the weight of old entries 60 | pub fn decay(&mut self) { 61 | for entry in self.table.iter_mut() { 62 | *entry /= 2; 63 | } 64 | } 65 | 66 | fn idx(p: Piece, s: Square) -> usize { 67 | p.idx() * Square::NUM_SQUARES + s.idx() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /movegen/src/queen.rs: -------------------------------------------------------------------------------- 1 | use crate::bitboard::Bitboard; 2 | use crate::ray::Ray; 3 | use crate::square::Square; 4 | 5 | pub struct Queen; 6 | 7 | impl Queen { 8 | pub fn targets(origin: Square, occupied: Bitboard) -> Bitboard { 9 | Ray::file_targets(origin, occupied) 10 | | Ray::rank_targets(origin, occupied) 11 | | Ray::diagonal_targets(origin, occupied) 12 | | Ray::anti_diagonal_targets(origin, occupied) 13 | } 14 | } 15 | 16 | #[cfg(test)] 17 | mod tests { 18 | use super::*; 19 | 20 | #[test] 21 | fn targets() { 22 | assert_eq!( 23 | Bitboard::C4 24 | | Bitboard::D3 25 | | Bitboard::D2 26 | | Bitboard::D5 27 | | Bitboard::D6 28 | | Bitboard::E4 29 | | Bitboard::F4 30 | | Bitboard::G4 31 | | Bitboard::C3 32 | | Bitboard::E3 33 | | Bitboard::F2 34 | | Bitboard::C5 35 | | Bitboard::B6 36 | | Bitboard::E5 37 | | Bitboard::F6 38 | | Bitboard::G7, 39 | Queen::targets( 40 | Square::D4, 41 | Bitboard::C4 42 | | Bitboard::B4 43 | | Bitboard::A4 44 | | Bitboard::D2 45 | | Bitboard::D6 46 | | Bitboard::G4 47 | | Bitboard::C3 48 | | Bitboard::B2 49 | | Bitboard::A1 50 | | Bitboard::F2 51 | | Bitboard::B6 52 | | Bitboard::G7 53 | ) 54 | ); 55 | } 56 | 57 | #[test] 58 | fn non_blocking_occupancy_targets() { 59 | assert_eq!( 60 | Queen::targets(Square::D4, Bitboard::EMPTY), 61 | Queen::targets( 62 | Square::D4, 63 | Bitboard::B3 64 | | Bitboard::B5 65 | | Bitboard::C2 66 | | Bitboard::C6 67 | | Bitboard::E2 68 | | Bitboard::E6 69 | | Bitboard::F3 70 | | Bitboard::F5 71 | ) 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /search/src/pv_table.rs: -------------------------------------------------------------------------------- 1 | use movegen::r#move::{Move, MoveList}; 2 | use std::fmt; 3 | 4 | #[derive(Debug, Clone)] 5 | pub struct PvTable { 6 | table: Vec, 7 | indices: Vec, 8 | max_depth: usize, 9 | } 10 | 11 | impl PvTable { 12 | pub fn new() -> Self { 13 | PvTable { 14 | table: Vec::new(), 15 | indices: Vec::new(), 16 | max_depth: 0, 17 | } 18 | } 19 | 20 | pub fn pv(&self, depth: usize) -> &[Move] { 21 | let begin = self.index(depth); 22 | let end = begin + depth; 23 | &self.table[begin..end] 24 | } 25 | 26 | pub fn pv_into_movelist(&self, depth: usize) -> MoveList { 27 | let mut res = MoveList::with_capacity(depth); 28 | for (i, m) in self.pv(depth).iter().enumerate() { 29 | match m { 30 | &Move::NULL => { 31 | res.truncate(i); 32 | break; 33 | } 34 | _ => res.push(*m), 35 | } 36 | } 37 | res 38 | } 39 | 40 | pub fn update_move_and_copy(&mut self, depth: usize, m: Move) { 41 | debug_assert!(depth > 0); 42 | self.reserve(depth); 43 | let begin = self.index(depth); 44 | let end = begin + depth; 45 | self.table[begin] = m; 46 | debug_assert!(begin + 1 >= depth); 47 | for i in begin + 1..end { 48 | self.table[i] = self.table[i - depth]; 49 | } 50 | } 51 | 52 | pub fn update_move_and_truncate(&mut self, depth: usize, m: Move) { 53 | debug_assert!(depth > 0); 54 | self.reserve(depth); 55 | let begin = self.index(depth); 56 | self.table[begin] = m; 57 | if depth > 1 { 58 | self.table[begin + 1] = Move::NULL; 59 | } 60 | } 61 | 62 | fn index(&self, depth: usize) -> usize { 63 | debug_assert!(depth > 0); 64 | debug_assert!(depth <= self.max_depth); 65 | self.indices[depth - 1] 66 | } 67 | 68 | fn reserve(&mut self, depth: usize) { 69 | if depth > self.max_depth { 70 | debug_assert!(depth == self.max_depth + 1); 71 | self.indices.push(self.table.len()); 72 | self.max_depth += 1; 73 | for _ in 0..depth { 74 | self.table.push(Move::NULL); 75 | } 76 | } 77 | } 78 | } 79 | 80 | impl fmt::Display for PvTable { 81 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 82 | for d in 1..=self.max_depth { 83 | writeln!(f, "Depth {}: {}", d, self.pv_into_movelist(d))?; 84 | } 85 | Ok(()) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tuner/src/file_reader.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::File, 3 | io::{self, BufRead}, 4 | path::Path, 5 | }; 6 | 7 | use eval::{eval::HasMatingMaterial, Eval}; 8 | use movegen::{fen::Fen, side::Side}; 9 | 10 | use crate::{ 11 | feature_evaluator::FeatureEvaluator, 12 | position_features::EvalType, 13 | training::{Outcome, TrainingFeatures, TrainingPosition}, 14 | }; 15 | 16 | pub fn read_training_data( 17 | filename: &str, 18 | pos_evaluator: &mut (impl Eval + HasMatingMaterial), 19 | feature_evaluator: &FeatureEvaluator, 20 | ) -> Vec { 21 | let mut parsed_count = 0; 22 | let mut training_data = Vec::new(); 23 | 24 | if let Ok(lines) = read_lines(filename) { 25 | for line in lines.map_while(Result::ok) { 26 | let mut s = line.split(" c9 "); 27 | let short_fen = s.next().unwrap(); 28 | let pos = Fen::shortened_str_to_pos(short_fen).unwrap(); 29 | let outcome = match s.next().unwrap().split('\"').nth(1).unwrap() { 30 | "1-0" => Outcome::WhiteWin, 31 | "1/2-1/2" => Outcome::Draw, 32 | "0-1" => Outcome::BlackWin, 33 | invalid => panic!("Invalid outcome: {invalid}"), 34 | }; 35 | let pos_eval = pos_evaluator.eval(&pos); 36 | let training_pos = TrainingPosition { pos, outcome }; 37 | let training_features = TrainingFeatures::from(&training_pos); 38 | let feature_eval = feature_evaluator.eval(&training_features.features); 39 | 40 | // Exclude draws by insufficient material 41 | if pos_eval != 0 42 | || pos_evaluator.has_mating_material(Side::White) 43 | && pos_evaluator.has_mating_material(Side::Black) 44 | { 45 | // Validate that the evaluations match 46 | assert!( 47 | ((pos_eval as EvalType) - feature_eval).abs() < 1.0, 48 | "Evaluations don't match\nPosition: {short_fen}\n\ 49 | Position Eval: {pos_eval}\nFeature Eval: {feature_eval}", 50 | ); 51 | training_data.push(training_features); 52 | } 53 | parsed_count += 1; 54 | } 55 | let training_count = training_data.len(); 56 | let filtered_count = parsed_count - training_count; 57 | println!("Positions: {parsed_count} parsed, {filtered_count} filtered out, will use {training_count} for training"); 58 | } 59 | training_data 60 | } 61 | 62 | fn read_lines

(filename: P) -> io::Result>> 63 | where 64 | P: AsRef, 65 | { 66 | let file = File::open(filename)?; 67 | Ok(io::BufReader::new(file).lines()) 68 | } 69 | -------------------------------------------------------------------------------- /uci/src/uci_in/set_option.rs: -------------------------------------------------------------------------------- 1 | use crate::parser::{split_first_word, ParserMessage, UciError}; 2 | use crate::uci_option::{OptionType, OPTIONS}; 3 | use crate::UciOut; 4 | use engine::{Engine, EngineOut}; 5 | use std::error::Error; 6 | 7 | pub fn run_command( 8 | uci_out: &mut UciOut, 9 | args: &str, 10 | engine: &mut Engine, 11 | ) -> Result, Box> { 12 | match split_first_word(args.trim_end()) { 13 | Some(("name", args_after_name)) => { 14 | let mut name_parts = Vec::new(); 15 | let mut value = None; 16 | let mut remaining = args_after_name; 17 | while let Some((word, tail)) = split_first_word(remaining) { 18 | let word_lower = word.to_lowercase(); 19 | if word_lower == "value" { 20 | value = Some(tail.trim().to_string()); 21 | break; 22 | } 23 | name_parts.push(word_lower); 24 | remaining = tail; 25 | } 26 | 27 | if name_parts.is_empty() { 28 | return make_err_invalid_argument(args); 29 | } 30 | 31 | let name = name_parts.join(" "); 32 | 33 | let opt = match OPTIONS.iter().find(|&x| x.name.to_lowercase() == name) { 34 | Some(o) => o, 35 | None => return make_err_invalid_argument(args), 36 | }; 37 | 38 | match &opt.r#type { 39 | OptionType::Check(props) => { 40 | let val = match value { 41 | Some(v) => match v.parse::() { 42 | Ok(v) => v, 43 | Err(_) => return make_err_invalid_argument(args), 44 | }, 45 | None => return make_err_invalid_argument(args), 46 | }; 47 | uci_out.info_string(&(props.fun)(engine, val))?; 48 | } 49 | OptionType::Spin(props) => { 50 | let val = match value { 51 | Some(v) => match v.parse::() { 52 | Ok(v) => v, 53 | Err(_) => return make_err_invalid_argument(args), 54 | }, 55 | None => return make_err_invalid_argument(args), 56 | }; 57 | if val < props.min || val > props.max { 58 | return make_err_invalid_argument(args); 59 | } 60 | uci_out.info_string(&(props.fun)(engine, val))?; 61 | } 62 | _ => todo!("Implement other types!"), 63 | } 64 | } 65 | _ => return make_err_invalid_argument(args), 66 | }; 67 | 68 | Ok(None) 69 | } 70 | 71 | fn make_err_invalid_argument(args: &str) -> Result, Box> { 72 | Err(Box::new(UciError::InvalidArgument(format!( 73 | "setoption {}", 74 | args.trim_end() 75 | )))) 76 | } 77 | -------------------------------------------------------------------------------- /movegen/tests/perft.rs: -------------------------------------------------------------------------------- 1 | use movegen::fen::Fen; 2 | use movegen::performance_tester::PerformanceTester; 3 | use movegen::position::Position; 4 | use movegen::position_history::PositionHistory; 5 | 6 | const BYTES: usize = 32 * 64 * 1024; 7 | 8 | #[test] 9 | fn perft_initial_position_low_depth() { 10 | let pos_history = PositionHistory::new(Position::initial()); 11 | let mut perft = PerformanceTester::new(pos_history, BYTES); 12 | assert_eq!(1, perft.count_nodes(0)); 13 | assert_eq!(20, perft.count_nodes(1)); 14 | assert_eq!(400, perft.count_nodes(2)); 15 | assert_eq!(8_902, perft.count_nodes(3)); 16 | } 17 | 18 | #[test] 19 | #[ignore] 20 | fn perft_initial_position_high_depth() { 21 | let pos_history = PositionHistory::new(Position::initial()); 22 | let mut perft = PerformanceTester::new(pos_history, BYTES); 23 | assert_eq!(197_281, perft.count_nodes(4)); 24 | assert_eq!(4_865_609, perft.count_nodes(5)); 25 | assert_eq!(119_060_324, perft.count_nodes(6)); 26 | } 27 | 28 | #[test] 29 | fn perft_middlegame_position_low_depth() { 30 | // Position from https://www.chessprogramming.org/Perft_Results 31 | let fen = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1"; 32 | let pos_history = PositionHistory::new(Fen::str_to_pos(fen).unwrap()); 33 | let mut perft = PerformanceTester::new(pos_history, BYTES); 34 | assert_eq!(1, perft.count_nodes(0)); 35 | assert_eq!(48, perft.count_nodes(1)); 36 | assert_eq!(2_039, perft.count_nodes(2)); 37 | assert_eq!(97_862, perft.count_nodes(3)); 38 | } 39 | 40 | #[test] 41 | #[ignore] 42 | fn perft_middlegame_position_high_depth() { 43 | // Position from https://www.chessprogramming.org/Perft_Results 44 | let fen = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1"; 45 | let pos_history = PositionHistory::new(Fen::str_to_pos(fen).unwrap()); 46 | let mut perft = PerformanceTester::new(pos_history, BYTES); 47 | assert_eq!(4_085_603, perft.count_nodes(4)); 48 | assert_eq!(193_690_690, perft.count_nodes(5)); 49 | // assert_eq!(8_031_647_685, perft.count_nodes(6)); 50 | } 51 | 52 | #[test] 53 | fn perft_tricky_position_low_depth() { 54 | // Position from https://www.chessprogramming.org/Perft_Results 55 | let fen = "rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8"; 56 | let pos_history = PositionHistory::new(Fen::str_to_pos(fen).unwrap()); 57 | let mut perft = PerformanceTester::new(pos_history, BYTES); 58 | assert_eq!(1, perft.count_nodes(0)); 59 | assert_eq!(44, perft.count_nodes(1)); 60 | assert_eq!(1_486, perft.count_nodes(2)); 61 | assert_eq!(62_379, perft.count_nodes(3)); 62 | } 63 | 64 | #[test] 65 | #[ignore] 66 | fn perft_tricky_position_high_depth() { 67 | // Position from https://www.chessprogramming.org/Perft_Results 68 | let fen = "rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8"; 69 | let pos_history = PositionHistory::new(Fen::str_to_pos(fen).unwrap()); 70 | let mut perft = PerformanceTester::new(pos_history, BYTES); 71 | assert_eq!(2_103_487, perft.count_nodes(4)); 72 | assert_eq!(89_941_194, perft.count_nodes(5)); 73 | } 74 | -------------------------------------------------------------------------------- /engine/tests/engine.rs: -------------------------------------------------------------------------------- 1 | mod mock_engine_out; 2 | 3 | use crossbeam_channel::unbounded; 4 | use engine::{Engine, EngineOptions}; 5 | use eval::material_mobility::MaterialMobility; 6 | use mock_engine_out::MockEngineOut; 7 | use more_asserts::assert_le; 8 | use movegen::position::Position; 9 | use movegen::position_history::PositionHistory; 10 | use search::alpha_beta::AlphaBeta; 11 | use search::SearchOptions; 12 | use std::sync::{Arc, Mutex}; 13 | use std::thread; 14 | use std::time::Duration; 15 | use std::time::Instant; 16 | 17 | const EVALUATOR: MaterialMobility = MaterialMobility::new(); 18 | const TABLE_SIZE: usize = 16 * 1024; 19 | 20 | #[test] 21 | fn search_timeout() { 22 | let search_algo = AlphaBeta::new(Box::new(EVALUATOR), TABLE_SIZE); 23 | let (sender, receiver) = unbounded(); 24 | let mut engine = Engine::new( 25 | search_algo, 26 | MockEngineOut::new( 27 | Box::new(|_res| {}), 28 | Box::new(move |_res| { 29 | sender.send(true).unwrap(); 30 | }), 31 | ), 32 | Arc::new(Mutex::new(EngineOptions::default())), 33 | ); 34 | engine.set_position_history(Some(PositionHistory::new(Position::initial()))); 35 | 36 | let movetime = Duration::from_millis(1000); 37 | let waittime = Duration::from_millis(2000); 38 | let tol = 80; 39 | 40 | let start = Instant::now(); 41 | assert!(engine 42 | .search(SearchOptions { 43 | movetime: Some(movetime), 44 | ..Default::default() 45 | }) 46 | .is_ok()); 47 | assert!(receiver.recv_timeout(waittime).is_ok()); 48 | let stop = Instant::now(); 49 | assert_le!( 50 | (stop.duration_since(start).as_millis() as i128 - movetime.as_millis() as i128).abs(), 51 | tol 52 | ); 53 | println!("Search time (movetime): {:?}", stop.duration_since(start)); 54 | } 55 | 56 | #[test] 57 | fn search_timeout_aborted() { 58 | let search_algo = AlphaBeta::new(Box::new(EVALUATOR), TABLE_SIZE); 59 | let (sender, receiver) = unbounded(); 60 | let mut engine = Engine::new( 61 | search_algo, 62 | MockEngineOut::new( 63 | Box::new(move |_res| {}), 64 | Box::new(move |_res| { 65 | sender.send(true).unwrap(); 66 | }), 67 | ), 68 | Arc::new(Mutex::new(EngineOptions::default())), 69 | ); 70 | engine.set_position_history(Some(PositionHistory::new(Position::initial()))); 71 | 72 | let movetime = Duration::from_millis(1000); 73 | let waittime = Duration::from_millis(2000); 74 | let sleeptime = Duration::from_millis(100); 75 | let tol = 80; 76 | 77 | let start = Instant::now(); 78 | assert!(engine 79 | .search(SearchOptions { 80 | movetime: Some(movetime), 81 | ..Default::default() 82 | }) 83 | .is_ok()); 84 | thread::sleep(sleeptime); 85 | engine.stop(); 86 | assert!(receiver.recv_timeout(waittime).is_ok()); 87 | let stop = Instant::now(); 88 | assert_le!( 89 | (stop.duration_since(start).as_millis() as i128 - sleeptime.as_millis() as i128).abs(), 90 | tol 91 | ); 92 | println!("Search time (abort): {:?}", stop.duration_since(start)); 93 | } 94 | -------------------------------------------------------------------------------- /tuner/src/error_function.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | feature_evaluator::WeightVector, 3 | position_features::{EvalType, FeatureVector}, 4 | }; 5 | 6 | pub struct ErrorFunction { 7 | k: EvalType, 8 | sum_of_squared_errors_epoch: f64, 9 | datapoint_count_epoch: usize, 10 | sum_of_squared_errors_batch: f64, 11 | datapoint_count_batch: usize, 12 | grad: WeightVector, 13 | } 14 | 15 | impl ErrorFunction { 16 | pub fn new(k: EvalType) -> Self { 17 | Self { 18 | k, 19 | sum_of_squared_errors_epoch: 0.0, 20 | datapoint_count_epoch: 0, 21 | sum_of_squared_errors_batch: 0.0, 22 | datapoint_count_batch: 0, 23 | grad: WeightVector::from_element(0.0), 24 | } 25 | } 26 | 27 | pub fn clear(&mut self) { 28 | self.sum_of_squared_errors_epoch = 0.0; 29 | self.datapoint_count_epoch = 0; 30 | self.clear_batch(); 31 | } 32 | 33 | pub fn clear_batch(&mut self) { 34 | self.sum_of_squared_errors_batch = 0.0; 35 | self.datapoint_count_batch = 0; 36 | for g in self.grad.iter_mut() { 37 | *g = 0.0; 38 | } 39 | } 40 | 41 | pub fn add_datapoint( 42 | &mut self, 43 | outcome: EvalType, 44 | eval: EvalType, 45 | grad_features: &FeatureVector, 46 | ) { 47 | let sigmoid = self.sigmoid(eval); 48 | let squared_error = (outcome - sigmoid).powi(2); 49 | self.sum_of_squared_errors_epoch += squared_error; 50 | self.datapoint_count_epoch += 1; 51 | self.sum_of_squared_errors_batch += squared_error; 52 | self.datapoint_count_batch += 1; 53 | let grad_sigmoid = self.k * sigmoid * (1.0 - sigmoid); 54 | let outer_grad = (outcome - sigmoid) * grad_sigmoid; 55 | for (_row, col, feat) in grad_features.triplet_iter() { 56 | self.grad[(col, 0)] += outer_grad * feat; 57 | } 58 | } 59 | 60 | pub fn datapoint_count_epoch(&self) -> usize { 61 | self.datapoint_count_epoch 62 | } 63 | 64 | pub fn mean_squared_error_epoch(&self) -> f64 { 65 | self.sum_of_squared_errors_epoch / self.datapoint_count_epoch as f64 66 | } 67 | 68 | pub fn datapoint_count_batch(&self) -> usize { 69 | self.datapoint_count_batch 70 | } 71 | 72 | pub fn mean_squared_error_batch(&self) -> f64 { 73 | self.sum_of_squared_errors_batch / self.datapoint_count_batch as f64 74 | } 75 | 76 | pub fn grad(&self) -> WeightVector { 77 | -2.0 * self.grad / self.datapoint_count_batch as EvalType 78 | } 79 | 80 | fn sigmoid(&self, e: EvalType) -> EvalType { 81 | 1.0 / (1.0 + (-self.k * e).exp()) 82 | } 83 | } 84 | 85 | #[cfg(test)] 86 | mod tests { 87 | use super::ErrorFunction; 88 | 89 | #[test] 90 | fn sigmoid_within_range_and_increasing() { 91 | let err_fn = ErrorFunction::new(1.0); 92 | let mut prev = -1.0; 93 | let mut s; 94 | for x in -10..=10 { 95 | s = err_fn.sigmoid(x as f64); 96 | assert!(s >= 0.0); 97 | assert!(s <= 1.0); 98 | assert!(s > prev); 99 | prev = s; 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /engine/src/best_move_handler.rs: -------------------------------------------------------------------------------- 1 | use crate::engine_out::EngineOut; 2 | use crossbeam_channel::Receiver; 3 | use movegen::r#move::Move; 4 | use movegen::side::Side; 5 | use search::search::SearchResult; 6 | use search::SearchOptions; 7 | use std::sync::{Arc, Mutex}; 8 | use std::thread; 9 | 10 | pub struct BestMoveHandler { 11 | pub thread: Option>, 12 | } 13 | 14 | impl BestMoveHandler { 15 | pub fn new( 16 | receiver: Receiver, 17 | engine_out: impl EngineOut + Send + 'static, 18 | ) -> Self { 19 | let options = Arc::new(Mutex::new(SearchOptions::default())); 20 | let mut side_to_move = None; 21 | let mut best_move = None; 22 | 23 | let thread = thread::spawn(move || loop { 24 | let message = receiver.recv().expect("Error receiving BestMoveCommand"); 25 | match message { 26 | BestMoveCommand::SetOptions(new_options) => match options.lock() { 27 | Ok(mut opt) => *opt = *new_options, 28 | Err(e) => panic!("{}", e), 29 | }, 30 | BestMoveCommand::SetSideToMove(s) => side_to_move = s, 31 | BestMoveCommand::DepthFinished(res) => engine_out 32 | .info_depth_finished(Self::search_result_to_relative(Some(res), side_to_move)) 33 | .expect("Error writing search info"), 34 | BestMoveCommand::Stop(StopReason::Command) => { 35 | match options.lock() { 36 | Ok(mut opt) => opt.infinite = false, 37 | Err(e) => panic!("{}", e), 38 | } 39 | engine_out 40 | .best_move(best_move.take()) 41 | .expect("Error writing best move"); 42 | } 43 | BestMoveCommand::Stop(StopReason::Finished(new_best_move)) => { 44 | best_move = Some(new_best_move); 45 | match options.lock() { 46 | Ok(opt) => { 47 | if !opt.infinite { 48 | engine_out 49 | .best_move(best_move.take()) 50 | .expect("Error writing best move"); 51 | } 52 | } 53 | Err(e) => panic!("{}", e), 54 | } 55 | } 56 | BestMoveCommand::Terminate => break, 57 | } 58 | }); 59 | 60 | Self { 61 | thread: Some(thread), 62 | } 63 | } 64 | 65 | fn search_result_to_relative( 66 | search_result: Option, 67 | side_to_move: Option, 68 | ) -> Option { 69 | search_result.map( 70 | |res| match side_to_move.expect("Expected Some(Side), got None") { 71 | Side::White => res, 72 | Side::Black => -res, 73 | }, 74 | ) 75 | } 76 | } 77 | 78 | #[derive(Clone, Debug)] 79 | pub enum BestMoveCommand { 80 | SetOptions(Box), 81 | SetSideToMove(Option), 82 | DepthFinished(SearchResult), 83 | Stop(StopReason), 84 | Terminate, 85 | } 86 | 87 | #[derive(Clone, Copy, Debug)] 88 | pub enum StopReason { 89 | Command, 90 | Finished(Move), 91 | } 92 | -------------------------------------------------------------------------------- /search/src/time_manager.rs: -------------------------------------------------------------------------------- 1 | use crate::SearchOptions; 2 | use movegen::side::Side; 3 | use std::{cmp, time::Duration}; 4 | 5 | const DEFAULT_MOVES_TO_GO: usize = 40; 6 | 7 | pub struct TimeManager; 8 | 9 | impl TimeManager { 10 | pub fn calc_movetime_hard_limit( 11 | side_to_move: Side, 12 | options: &SearchOptions, 13 | ) -> Option { 14 | if let Some(dur) = options.movetime { 15 | return Some(dur); 16 | } 17 | 18 | const MIN_TIME: Duration = Duration::from_millis(0); 19 | 20 | let moves_to_go = options.moves_to_go.unwrap_or(DEFAULT_MOVES_TO_GO); 21 | let (time_millis, inc_millis) = match side_to_move { 22 | Side::White if options.white_time.is_some() => ( 23 | options 24 | .white_time 25 | .unwrap_or(Duration::from_millis(0)) 26 | .as_millis(), 27 | options 28 | .white_inc 29 | .unwrap_or(Duration::from_millis(0)) 30 | .as_millis(), 31 | ), 32 | Side::Black if options.black_time.is_some() => ( 33 | options 34 | .black_time 35 | .unwrap_or(Duration::from_millis(0)) 36 | .as_millis(), 37 | options 38 | .black_inc 39 | .unwrap_or(Duration::from_millis(0)) 40 | .as_millis(), 41 | ), 42 | _ => return None, 43 | }; 44 | let quot = (moves_to_go as f64).sqrt() as u64; 45 | let max_time = time_millis as u64 / quot + inc_millis as u64; 46 | let hard_time_limit = match time_millis.checked_sub(options.move_overhead.as_millis()) { 47 | Some(t) => Duration::from_millis(cmp::min(t as u64, max_time)), 48 | None => MIN_TIME, 49 | }; 50 | Some(hard_time_limit) 51 | } 52 | 53 | pub fn calc_movetime_soft_limit( 54 | side_to_move: Side, 55 | options: &SearchOptions, 56 | ) -> Option { 57 | let moves_to_go = options.moves_to_go.unwrap_or(DEFAULT_MOVES_TO_GO); 58 | let (time_millis, inc_millis) = match side_to_move { 59 | Side::White if options.white_time.is_some() => ( 60 | options 61 | .white_time 62 | .unwrap_or(Duration::from_millis(0)) 63 | .as_millis(), 64 | options 65 | .white_inc 66 | .unwrap_or(Duration::from_millis(0)) 67 | .as_millis(), 68 | ), 69 | Side::Black if options.black_time.is_some() => ( 70 | options 71 | .black_time 72 | .unwrap_or(Duration::from_millis(0)) 73 | .as_millis(), 74 | options 75 | .black_inc 76 | .unwrap_or(Duration::from_millis(0)) 77 | .as_millis(), 78 | ), 79 | _ => return None, 80 | }; 81 | // We don't add the full increment to the soft limit. Otherwise we would 82 | // be running into the hard limit almost every move if we have very 83 | // little time left. 84 | const INC_DIVISOR: u64 = 2; 85 | let soft_limit = time_millis as u64 / moves_to_go as u64 + inc_millis as u64 / INC_DIVISOR; 86 | Some(Duration::from_millis(soft_limit)) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /movegen/src/repetition_tracker.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use std::hash::Hash; 3 | 4 | #[derive(Clone, Debug)] 5 | pub struct RepetitionTracker { 6 | history: Vec, 7 | plies_since_last_irreversible: Vec, 8 | } 9 | 10 | impl RepetitionTracker 11 | where 12 | K: Debug + Eq + Hash, 13 | { 14 | pub fn new() -> Self { 15 | Self { 16 | history: Vec::new(), 17 | plies_since_last_irreversible: Vec::new(), 18 | } 19 | } 20 | 21 | pub fn current_pos_repetitions(&self) -> usize { 22 | let hash = match self.history.last() { 23 | Some(h) => h, 24 | None => return 0, 25 | }; 26 | // We only need to check every second position because the side to move 27 | // must be the same in each repetition 28 | const STEP: usize = 2; 29 | 1 + self 30 | .history 31 | .iter() 32 | .rev() 33 | .take(self.plies_since_last_irreversible.last().unwrap_or(&0) + 1) 34 | .skip(2) 35 | .step_by(STEP) 36 | .filter(|&x| x == hash) 37 | .count() 38 | } 39 | 40 | pub fn push(&mut self, hash: K, is_reversible: bool) { 41 | self.history.push(hash); 42 | self.plies_since_last_irreversible 43 | .push(match is_reversible { 44 | true => self.plies_since_last_irreversible.last().unwrap_or(&0) + 1, 45 | false => 0, 46 | }); 47 | } 48 | 49 | pub fn pop(&mut self) { 50 | self.history.pop(); 51 | self.plies_since_last_irreversible.pop(); 52 | } 53 | } 54 | 55 | #[cfg(test)] 56 | mod tests { 57 | use super::*; 58 | 59 | #[test] 60 | fn repetitions() { 61 | let key_0 = "0"; 62 | let key_1 = "1"; 63 | let key_2 = "2"; 64 | let key_irr = "irr"; 65 | let mut rep_tracker = RepetitionTracker::new(); 66 | 67 | rep_tracker.push(key_0, true); 68 | assert_eq!(1, rep_tracker.current_pos_repetitions()); 69 | rep_tracker.push(key_1, true); 70 | assert_eq!(1, rep_tracker.current_pos_repetitions()); 71 | rep_tracker.push(key_2, true); 72 | assert_eq!(1, rep_tracker.current_pos_repetitions()); 73 | rep_tracker.push(key_1, true); 74 | assert_eq!(2, rep_tracker.current_pos_repetitions()); 75 | rep_tracker.push(key_0, true); 76 | assert_eq!(2, rep_tracker.current_pos_repetitions()); 77 | rep_tracker.push(key_1, true); 78 | assert_eq!(3, rep_tracker.current_pos_repetitions()); 79 | rep_tracker.push(key_irr, false); 80 | assert_eq!(1, rep_tracker.current_pos_repetitions()); 81 | rep_tracker.push(key_1, true); 82 | assert_eq!(1, rep_tracker.current_pos_repetitions()); 83 | 84 | rep_tracker.pop(); 85 | assert_eq!(1, rep_tracker.current_pos_repetitions()); 86 | rep_tracker.pop(); 87 | assert_eq!(3, rep_tracker.current_pos_repetitions()); 88 | rep_tracker.pop(); 89 | assert_eq!(2, rep_tracker.current_pos_repetitions()); 90 | rep_tracker.pop(); 91 | assert_eq!(2, rep_tracker.current_pos_repetitions()); 92 | rep_tracker.pop(); 93 | assert_eq!(1, rep_tracker.current_pos_repetitions()); 94 | rep_tracker.pop(); 95 | assert_eq!(1, rep_tracker.current_pos_repetitions()); 96 | rep_tracker.pop(); 97 | assert_eq!(1, rep_tracker.current_pos_repetitions()); 98 | rep_tracker.pop(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /eval/src/mobility.rs: -------------------------------------------------------------------------------- 1 | use movegen::{ 2 | bishop::Bishop, bitboard::Bitboard, knight::Knight, piece, position::Position, queen::Queen, 3 | rook::Rook, side::Side, 4 | }; 5 | 6 | use crate::{params, score_pair::ScorePair, Score}; 7 | 8 | #[derive(Debug, Clone, Default)] 9 | pub struct MobilityCounts { 10 | pub knight_mob: [i8; params::KNIGHT_MOB_LEN], 11 | pub bishop_mob: [i8; params::BISHOP_MOB_LEN], 12 | pub rook_mob: [i8; params::ROOK_MOB_LEN], 13 | pub queen_mob: [i8; params::QUEEN_MOB_LEN], 14 | } 15 | 16 | #[derive(Debug, Clone, Default)] 17 | pub struct Mobility; 18 | 19 | impl Mobility { 20 | pub fn scores(&self, pos: &Position) -> ScorePair { 21 | let mob_counts = Self::mobility_counts(pos); 22 | let mut scores = ScorePair(0, 0); 23 | 24 | scores += mob_counts 25 | .knight_mob 26 | .iter() 27 | .zip(¶ms::MOBILITY_KNIGHT) 28 | .map(|(n, s)| *n as Score * s) 29 | .fold(ScorePair(0, 0), |acc, x| acc + x); 30 | scores += mob_counts 31 | .bishop_mob 32 | .iter() 33 | .zip(¶ms::MOBILITY_BISHOP) 34 | .map(|(n, s)| *n as Score * s) 35 | .fold(ScorePair(0, 0), |acc, x| acc + x); 36 | scores += mob_counts 37 | .rook_mob 38 | .iter() 39 | .zip(¶ms::MOBILITY_ROOK) 40 | .map(|(n, s)| *n as Score * s) 41 | .fold(ScorePair(0, 0), |acc, x| acc + x); 42 | scores += mob_counts 43 | .queen_mob 44 | .iter() 45 | .zip(¶ms::MOBILITY_QUEEN) 46 | .map(|(n, s)| *n as Score * s) 47 | .fold(ScorePair(0, 0), |acc, x| acc + x); 48 | 49 | scores 50 | } 51 | 52 | pub fn mobility_counts(pos: &Position) -> MobilityCounts { 53 | let mut mob_counts = MobilityCounts::default(); 54 | Self::mobility_counts_one_side(pos, Side::White, &mut mob_counts); 55 | Self::mobility_counts_one_side(pos, Side::Black, &mut mob_counts); 56 | mob_counts 57 | } 58 | 59 | fn mobility_counts_one_side(pos: &Position, side: Side, mob_counts: &mut MobilityCounts) { 60 | let side_as_int = 1 - 2 * (side as i8); 61 | let occupancy = pos.occupancy(); 62 | let own_occupancy = pos.side_occupancy(side); 63 | 64 | let mut own_knights = pos.piece_occupancy(side, piece::Type::Knight); 65 | while own_knights != Bitboard::EMPTY { 66 | let origin = own_knights.square_scan_forward_reset(); 67 | let targets = Knight::targets(origin) & !own_occupancy; 68 | mob_counts.knight_mob[targets.pop_count()] += side_as_int; 69 | } 70 | 71 | let mut own_bishops = pos.piece_occupancy(side, piece::Type::Bishop); 72 | while own_bishops != Bitboard::EMPTY { 73 | let origin = own_bishops.square_scan_forward_reset(); 74 | let targets = Bishop::targets(origin, occupancy) & !own_occupancy; 75 | mob_counts.bishop_mob[targets.pop_count()] += side_as_int; 76 | } 77 | 78 | let mut own_rooks = pos.piece_occupancy(side, piece::Type::Rook); 79 | while own_rooks != Bitboard::EMPTY { 80 | let origin = own_rooks.square_scan_forward_reset(); 81 | let targets = Rook::targets(origin, occupancy) & !own_occupancy; 82 | mob_counts.rook_mob[targets.pop_count()] += side_as_int; 83 | } 84 | 85 | let mut own_queens = pos.piece_occupancy(side, piece::Type::Queen); 86 | while own_queens != Bitboard::EMPTY { 87 | let origin = own_queens.square_scan_forward_reset(); 88 | let targets = Queen::targets(origin, occupancy) & !own_occupancy; 89 | mob_counts.queen_mob[targets.pop_count()] += side_as_int; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /movegen/src/performance_tester.rs: -------------------------------------------------------------------------------- 1 | use std::cmp; 2 | 3 | use crate::move_generator::MoveGenerator; 4 | use crate::position_history::PositionHistory; 5 | use crate::r#move::MoveList; 6 | use crate::transposition_table::{TranspositionTable, TtEntry}; 7 | use crate::zobrist::Zobrist; 8 | 9 | const AGE: u8 = 0; 10 | 11 | #[derive(Clone, Copy, Debug, Default)] 12 | struct TableEntry { 13 | is_valid: bool, 14 | depth: usize, 15 | num_nodes: usize, 16 | } 17 | 18 | impl TtEntry for TableEntry { 19 | fn is_valid(&self) -> bool { 20 | self.is_valid 21 | } 22 | 23 | fn depth(&self) -> usize { 24 | self.depth 25 | } 26 | 27 | fn age(&self) -> u8 { 28 | AGE 29 | } 30 | 31 | fn prio(&self, other: &Self, _age: u8) -> cmp::Ordering { 32 | self.depth.cmp(&other.depth).reverse() 33 | } 34 | } 35 | 36 | pub struct PerformanceTester { 37 | pos_history: PositionHistory, 38 | transpos_table: TranspositionTable, 39 | } 40 | 41 | impl PerformanceTester { 42 | pub fn new(pos_history: PositionHistory, bytes: usize) -> PerformanceTester { 43 | Self { 44 | pos_history, 45 | transpos_table: TranspositionTable::new(bytes), 46 | } 47 | } 48 | 49 | pub fn count_nodes(&mut self, depth: usize) -> usize { 50 | let mut move_list_stack = vec![MoveList::new(); depth]; 51 | self.count_nodes_recursive(&mut move_list_stack, depth) 52 | } 53 | 54 | fn count_nodes_recursive( 55 | &mut self, 56 | move_list_stack: &mut Vec, 57 | depth: usize, 58 | ) -> usize { 59 | let hash = self.pos_history.current_pos_hash(); 60 | match self.transpos_table.get(&hash) { 61 | Some(entry) if entry.depth == depth => entry.num_nodes, 62 | _ => { 63 | let mut num_nodes = 0; 64 | 65 | match depth { 66 | 0 => { 67 | debug_assert!(move_list_stack.is_empty()); 68 | num_nodes = 1; 69 | } 70 | _ => { 71 | debug_assert!(!move_list_stack.is_empty()); 72 | let mut move_list = move_list_stack.pop().unwrap(); 73 | MoveGenerator::generate_moves( 74 | &mut move_list, 75 | self.pos_history.current_pos(), 76 | ); 77 | match depth { 78 | 1 => { 79 | debug_assert!(move_list_stack.is_empty()); 80 | num_nodes = move_list.len(); 81 | } 82 | _ => { 83 | for m in move_list.iter() { 84 | self.pos_history.do_move(*m); 85 | num_nodes += 86 | self.count_nodes_recursive(move_list_stack, depth - 1); 87 | self.pos_history.undo_last_move(); 88 | } 89 | self.transpos_table.insert( 90 | hash, 91 | TableEntry { 92 | is_valid: true, 93 | depth, 94 | num_nodes, 95 | }, 96 | ); 97 | } 98 | } 99 | move_list_stack.push(move_list); 100 | } 101 | }; 102 | num_nodes 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /movegen/src/castling_squares.rs: -------------------------------------------------------------------------------- 1 | use crate::bitboard::Bitboard; 2 | use crate::file::File; 3 | use crate::rank::Rank; 4 | use crate::square::Square; 5 | 6 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] 7 | pub struct CastlingSquaresInner { 8 | pub non_blocked: Bitboard, // Squares passed by king or rook 9 | pub non_attacked: Bitboard, // Squares passed by the king (including start and end square) 10 | } 11 | 12 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] 13 | pub struct CastlingSquares { 14 | white_kingside: CastlingSquaresInner, 15 | white_queenside: CastlingSquaresInner, 16 | black_kingside: CastlingSquaresInner, 17 | black_queenside: CastlingSquaresInner, 18 | } 19 | 20 | impl CastlingSquares { 21 | pub fn new(queen_rook_file: File, king_file: File, king_rook_file: File) -> CastlingSquares { 22 | let mut cs = CastlingSquares { 23 | ..Default::default() 24 | }; 25 | const KINGSIDE_KING_TARGET_FILE: File = File::G; 26 | const KINGSIDE_ROOK_TARGET_FILE: File = File::F; 27 | const QUEENSIDE_KING_TARGET_FILE: File = File::C; 28 | const QUEENSIDE_ROOK_TARGET_FILE: File = File::D; 29 | 30 | // Kingside 31 | let min_kingside_file = king_file.idx(); 32 | let max_kingside_file = KINGSIDE_KING_TARGET_FILE.idx(); 33 | for file in min_kingside_file..=max_kingside_file { 34 | cs.white_kingside.non_attacked |= 35 | Bitboard::from(Square::from((File::from_idx(file), Rank::R1))); 36 | cs.black_kingside.non_attacked |= 37 | Bitboard::from(Square::from((File::from_idx(file), Rank::R8))); 38 | } 39 | 40 | let min_kingside_file = min_kingside_file.min(KINGSIDE_ROOK_TARGET_FILE.idx()); 41 | let max_kingside_file = max_kingside_file.max(king_rook_file.idx()); 42 | for idx in min_kingside_file..=max_kingside_file { 43 | let file = File::from_idx(idx); 44 | if file == king_file || file == king_rook_file { 45 | continue; 46 | } 47 | cs.white_kingside.non_blocked |= Bitboard::from(Square::from((file, Rank::R1))); 48 | cs.black_kingside.non_blocked |= Bitboard::from(Square::from((file, Rank::R8))); 49 | } 50 | 51 | // Queenside 52 | let min_queenside_file = king_file.idx().min(QUEENSIDE_KING_TARGET_FILE.idx()); 53 | let max_queenside_file = king_file.idx().max(QUEENSIDE_KING_TARGET_FILE.idx()); 54 | for file in min_queenside_file..=max_queenside_file { 55 | cs.white_queenside.non_attacked |= 56 | Bitboard::from(Square::from((File::from_idx(file), Rank::R1))); 57 | cs.black_queenside.non_attacked |= 58 | Bitboard::from(Square::from((File::from_idx(file), Rank::R8))); 59 | } 60 | 61 | let min_queenside_file = min_queenside_file.min(queen_rook_file.idx()); 62 | let max_queenside_file = max_queenside_file.max(QUEENSIDE_ROOK_TARGET_FILE.idx()); 63 | for idx in min_queenside_file..=max_queenside_file { 64 | let file = File::from_idx(idx); 65 | if file == king_file || file == queen_rook_file { 66 | continue; 67 | } 68 | cs.white_queenside.non_blocked |= Bitboard::from(Square::from((file, Rank::R1))); 69 | cs.black_queenside.non_blocked |= Bitboard::from(Square::from((file, Rank::R8))); 70 | } 71 | 72 | cs 73 | } 74 | 75 | pub fn white_kingside(&self) -> &CastlingSquaresInner { 76 | &self.white_kingside 77 | } 78 | 79 | pub fn white_queenside(&self) -> &CastlingSquaresInner { 80 | &self.white_queenside 81 | } 82 | 83 | pub fn black_kingside(&self) -> &CastlingSquaresInner { 84 | &self.black_kingside 85 | } 86 | 87 | pub fn black_queenside(&self) -> &CastlingSquaresInner { 88 | &self.black_queenside 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /movegen/src/file.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 4 | pub struct File(u8); 5 | 6 | impl File { 7 | pub const NUM_FILES: usize = 8; 8 | 9 | pub const A: Self = File(0); 10 | pub const B: Self = File(1); 11 | pub const C: Self = File(2); 12 | pub const D: Self = File(3); 13 | pub const E: Self = File(4); 14 | pub const F: Self = File(5); 15 | pub const G: Self = File(6); 16 | pub const H: Self = File(7); 17 | 18 | pub const fn from_idx(idx: usize) -> File { 19 | debug_assert!(idx < Self::NUM_FILES); 20 | File(idx as u8) 21 | } 22 | 23 | pub const fn idx(&self) -> usize { 24 | self.0 as usize 25 | } 26 | 27 | pub fn from_ascii(c: u8) -> Result { 28 | match c { 29 | b'a'..=b'h' => Ok(File::from_idx((c - b'a') as usize)), 30 | _ => Err(format!("Invalid file `{}`", c as char)), 31 | } 32 | } 33 | 34 | pub fn to_ascii(self) -> u8 { 35 | debug_assert!(self.idx() < Self::NUM_FILES); 36 | self.0 + b'a' 37 | } 38 | } 39 | 40 | impl fmt::Display for File { 41 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 42 | let file_char = self.to_ascii() as char; 43 | write!(f, "{file_char}") 44 | } 45 | } 46 | 47 | #[cfg(test)] 48 | mod tests { 49 | use super::*; 50 | 51 | #[test] 52 | fn from_idx() { 53 | assert_eq!(File::A, File::from_idx(0)); 54 | assert_eq!(File::B, File::from_idx(1)); 55 | assert_eq!(File::C, File::from_idx(2)); 56 | assert_eq!(File::D, File::from_idx(3)); 57 | assert_eq!(File::E, File::from_idx(4)); 58 | assert_eq!(File::F, File::from_idx(5)); 59 | assert_eq!(File::G, File::from_idx(6)); 60 | assert_eq!(File::H, File::from_idx(7)); 61 | } 62 | 63 | #[test] 64 | fn idx() { 65 | assert_eq!(0, File::A.idx()); 66 | assert_eq!(1, File::B.idx()); 67 | assert_eq!(2, File::C.idx()); 68 | assert_eq!(3, File::D.idx()); 69 | assert_eq!(4, File::E.idx()); 70 | assert_eq!(5, File::F.idx()); 71 | assert_eq!(6, File::G.idx()); 72 | assert_eq!(7, File::H.idx()); 73 | } 74 | 75 | #[test] 76 | fn from_ascii() { 77 | assert_eq!(Ok(File::A), File::from_ascii(b'a')); 78 | assert_eq!(Ok(File::B), File::from_ascii(b'b')); 79 | assert_eq!(Ok(File::C), File::from_ascii(b'c')); 80 | assert_eq!(Ok(File::D), File::from_ascii(b'd')); 81 | assert_eq!(Ok(File::E), File::from_ascii(b'e')); 82 | assert_eq!(Ok(File::F), File::from_ascii(b'f')); 83 | assert_eq!(Ok(File::G), File::from_ascii(b'g')); 84 | assert_eq!(Ok(File::H), File::from_ascii(b'h')); 85 | assert_eq!( 86 | Err(String::from("Invalid file `i`")), 87 | File::from_ascii(b'i') 88 | ); 89 | } 90 | 91 | #[test] 92 | fn to_ascii() { 93 | assert_eq!(b'a', File::A.to_ascii()); 94 | assert_eq!(b'b', File::B.to_ascii()); 95 | assert_eq!(b'c', File::C.to_ascii()); 96 | assert_eq!(b'd', File::D.to_ascii()); 97 | assert_eq!(b'e', File::E.to_ascii()); 98 | assert_eq!(b'f', File::F.to_ascii()); 99 | assert_eq!(b'g', File::G.to_ascii()); 100 | assert_eq!(b'h', File::H.to_ascii()); 101 | } 102 | 103 | #[test] 104 | fn fmt() { 105 | assert_eq!("a", format!("{}", File::A)); 106 | assert_eq!("b", format!("{}", File::B)); 107 | assert_eq!("c", format!("{}", File::C)); 108 | assert_eq!("d", format!("{}", File::D)); 109 | assert_eq!("e", format!("{}", File::E)); 110 | assert_eq!("f", format!("{}", File::F)); 111 | assert_eq!("g", format!("{}", File::G)); 112 | assert_eq!("h", format!("{}", File::H)); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /movegen/src/rank.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 4 | pub struct Rank(u8); 5 | 6 | impl Rank { 7 | pub const NUM_RANKS: usize = 8; 8 | 9 | pub const R1: Self = Rank(0); 10 | pub const R2: Self = Rank(1); 11 | pub const R3: Self = Rank(2); 12 | pub const R4: Self = Rank(3); 13 | pub const R5: Self = Rank(4); 14 | pub const R6: Self = Rank(5); 15 | pub const R7: Self = Rank(6); 16 | pub const R8: Self = Rank(7); 17 | 18 | pub const fn from_idx(idx: usize) -> Rank { 19 | debug_assert!(idx < Self::NUM_RANKS); 20 | Rank(idx as u8) 21 | } 22 | 23 | pub const fn idx(&self) -> usize { 24 | self.0 as usize 25 | } 26 | 27 | pub fn from_ascii(c: u8) -> Result { 28 | match c { 29 | b'1'..=b'8' => Ok(Rank::from_idx((c - b'1') as usize)), 30 | _ => Err(format!("Invalid rank `{}`", c as char)), 31 | } 32 | } 33 | 34 | pub fn to_ascii(self) -> u8 { 35 | debug_assert!(self.idx() < Self::NUM_RANKS); 36 | self.0 + b'1' 37 | } 38 | } 39 | 40 | impl fmt::Display for Rank { 41 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 42 | let rank_char = self.to_ascii() as char; 43 | write!(f, "{rank_char}") 44 | } 45 | } 46 | 47 | #[cfg(test)] 48 | mod tests { 49 | use super::*; 50 | 51 | #[test] 52 | fn from_idx() { 53 | assert_eq!(Rank::R1, Rank::from_idx(0)); 54 | assert_eq!(Rank::R2, Rank::from_idx(1)); 55 | assert_eq!(Rank::R3, Rank::from_idx(2)); 56 | assert_eq!(Rank::R4, Rank::from_idx(3)); 57 | assert_eq!(Rank::R5, Rank::from_idx(4)); 58 | assert_eq!(Rank::R6, Rank::from_idx(5)); 59 | assert_eq!(Rank::R7, Rank::from_idx(6)); 60 | assert_eq!(Rank::R8, Rank::from_idx(7)); 61 | } 62 | 63 | #[test] 64 | fn idx() { 65 | assert_eq!(0, Rank::R1.idx()); 66 | assert_eq!(1, Rank::R2.idx()); 67 | assert_eq!(2, Rank::R3.idx()); 68 | assert_eq!(3, Rank::R4.idx()); 69 | assert_eq!(4, Rank::R5.idx()); 70 | assert_eq!(5, Rank::R6.idx()); 71 | assert_eq!(6, Rank::R7.idx()); 72 | assert_eq!(7, Rank::R8.idx()); 73 | } 74 | 75 | #[test] 76 | fn from_ascii() { 77 | assert_eq!(Ok(Rank::R1), Rank::from_ascii(b'1')); 78 | assert_eq!(Ok(Rank::R2), Rank::from_ascii(b'2')); 79 | assert_eq!(Ok(Rank::R3), Rank::from_ascii(b'3')); 80 | assert_eq!(Ok(Rank::R4), Rank::from_ascii(b'4')); 81 | assert_eq!(Ok(Rank::R5), Rank::from_ascii(b'5')); 82 | assert_eq!(Ok(Rank::R6), Rank::from_ascii(b'6')); 83 | assert_eq!(Ok(Rank::R7), Rank::from_ascii(b'7')); 84 | assert_eq!(Ok(Rank::R8), Rank::from_ascii(b'8')); 85 | assert_eq!( 86 | Err(String::from("Invalid rank `9`")), 87 | Rank::from_ascii(b'9') 88 | ); 89 | } 90 | 91 | #[test] 92 | fn to_ascii() { 93 | assert_eq!(b'1', Rank::R1.to_ascii()); 94 | assert_eq!(b'2', Rank::R2.to_ascii()); 95 | assert_eq!(b'3', Rank::R3.to_ascii()); 96 | assert_eq!(b'4', Rank::R4.to_ascii()); 97 | assert_eq!(b'5', Rank::R5.to_ascii()); 98 | assert_eq!(b'6', Rank::R6.to_ascii()); 99 | assert_eq!(b'7', Rank::R7.to_ascii()); 100 | assert_eq!(b'8', Rank::R8.to_ascii()); 101 | } 102 | 103 | #[test] 104 | fn fmt() { 105 | assert_eq!("1", format!("{}", Rank::R1)); 106 | assert_eq!("2", format!("{}", Rank::R2)); 107 | assert_eq!("3", format!("{}", Rank::R3)); 108 | assert_eq!("4", format!("{}", Rank::R4)); 109 | assert_eq!("5", format!("{}", Rank::R5)); 110 | assert_eq!("6", format!("{}", Rank::R6)); 111 | assert_eq!("7", format!("{}", Rank::R7)); 112 | assert_eq!("8", format!("{}", Rank::R8)); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /movegen/src/king.rs: -------------------------------------------------------------------------------- 1 | use crate::bitboard::Bitboard; 2 | use crate::square::Square; 3 | 4 | pub struct King; 5 | 6 | impl King { 7 | pub fn targets(origin: Square) -> Bitboard { 8 | Self::TARGETS_ALL_SQUARES[origin.idx()] 9 | } 10 | 11 | const TARGETS_ALL_SQUARES: [Bitboard; Square::NUM_SQUARES] = [ 12 | Bitboard(0x302), 13 | Bitboard(0x705), 14 | Bitboard(0xe0a), 15 | Bitboard(0x1c14), 16 | Bitboard(0x3828), 17 | Bitboard(0x7050), 18 | Bitboard(0xe0a0), 19 | Bitboard(0xc040), 20 | Bitboard(0x30203), 21 | Bitboard(0x70507), 22 | Bitboard(0xe0a0e), 23 | Bitboard(0x1c141c), 24 | Bitboard(0x382838), 25 | Bitboard(0x705070), 26 | Bitboard(0xe0a0e0), 27 | Bitboard(0xc040c0), 28 | Bitboard(0x3020300), 29 | Bitboard(0x7050700), 30 | Bitboard(0xe0a0e00), 31 | Bitboard(0x1c141c00), 32 | Bitboard(0x38283800), 33 | Bitboard(0x70507000), 34 | Bitboard(0xe0a0e000), 35 | Bitboard(0xc040c000), 36 | Bitboard(0x302030000), 37 | Bitboard(0x705070000), 38 | Bitboard(0xe0a0e0000), 39 | Bitboard(0x1c141c0000), 40 | Bitboard(0x3828380000), 41 | Bitboard(0x7050700000), 42 | Bitboard(0xe0a0e00000), 43 | Bitboard(0xc040c00000), 44 | Bitboard(0x30203000000), 45 | Bitboard(0x70507000000), 46 | Bitboard(0xe0a0e000000), 47 | Bitboard(0x1c141c000000), 48 | Bitboard(0x382838000000), 49 | Bitboard(0x705070000000), 50 | Bitboard(0xe0a0e0000000), 51 | Bitboard(0xc040c0000000), 52 | Bitboard(0x3020300000000), 53 | Bitboard(0x7050700000000), 54 | Bitboard(0xe0a0e00000000), 55 | Bitboard(0x1c141c00000000), 56 | Bitboard(0x38283800000000), 57 | Bitboard(0x70507000000000), 58 | Bitboard(0xe0a0e000000000), 59 | Bitboard(0xc040c000000000), 60 | Bitboard(0x302030000000000), 61 | Bitboard(0x705070000000000), 62 | Bitboard(0xe0a0e0000000000), 63 | Bitboard(0x1c141c0000000000), 64 | Bitboard(0x3828380000000000), 65 | Bitboard(0x7050700000000000), 66 | Bitboard(0xe0a0e00000000000), 67 | Bitboard(0xc040c00000000000), 68 | Bitboard(0x203000000000000), 69 | Bitboard(0x507000000000000), 70 | Bitboard(0xa0e000000000000), 71 | Bitboard(0x141c000000000000), 72 | Bitboard(0x2838000000000000), 73 | Bitboard(0x5070000000000000), 74 | Bitboard(0xa0e0000000000000), 75 | Bitboard(0x40c0000000000000), 76 | ]; 77 | } 78 | 79 | #[cfg(test)] 80 | mod tests { 81 | use super::*; 82 | 83 | #[test] 84 | fn targets() { 85 | assert_eq!( 86 | Bitboard::A2 | Bitboard::B1 | Bitboard::B2, 87 | King::targets(Square::A1) 88 | ); 89 | assert_eq!( 90 | Bitboard::A7 | Bitboard::B8 | Bitboard::B7, 91 | King::targets(Square::A8) 92 | ); 93 | assert_eq!( 94 | Bitboard::H2 | Bitboard::G1 | Bitboard::G2, 95 | King::targets(Square::H1) 96 | ); 97 | assert_eq!( 98 | Bitboard::H7 | Bitboard::G8 | Bitboard::G7, 99 | King::targets(Square::H8) 100 | ); 101 | 102 | assert_eq!( 103 | Bitboard::A1 | Bitboard::A2 | Bitboard::B2 | Bitboard::C1 | Bitboard::C2, 104 | King::targets(Square::B1) 105 | ); 106 | 107 | assert_eq!( 108 | Bitboard::A1 109 | | Bitboard::A2 110 | | Bitboard::A3 111 | | Bitboard::B1 112 | | Bitboard::B3 113 | | Bitboard::C1 114 | | Bitboard::C2 115 | | Bitboard::C3, 116 | King::targets(Square::B2) 117 | ); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /tuner/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::{self, File}, 3 | io::Write, 4 | }; 5 | 6 | use clap::{Args, Parser, Subcommand}; 7 | use eval::complex::Complex; 8 | use tuner::{ 9 | error_function::ErrorFunction, 10 | eval_params::EvalParams, 11 | feature_evaluator::{initialize_weights, FeatureEvaluator}, 12 | file_reader, 13 | optimizer::{self, AdamParams, Checkpoint}, 14 | }; 15 | 16 | #[derive(Debug, Parser)] 17 | #[command(author, version, about, long_about = None)] 18 | #[command(propagate_version = true)] 19 | struct Cli { 20 | #[command(subcommand)] 21 | command: Commands, 22 | } 23 | 24 | #[derive(Debug, Subcommand)] 25 | enum Commands { 26 | /// Extract evaluation parameters from the engine 27 | Extract(ExtractArgs), 28 | /// Optimize parameter weights 29 | Optimize(OptimizeArgs), 30 | /// Print evaluation parameters 31 | Print(PrintArgs), 32 | } 33 | 34 | #[derive(Debug, Args)] 35 | struct ExtractArgs { 36 | #[arg(short, long)] 37 | weight_file_prefix: String, 38 | } 39 | 40 | #[derive(Debug, Args)] 41 | struct OptimizeArgs { 42 | #[arg(short, long)] 43 | training_data_file: String, 44 | #[arg(short, long)] 45 | weight_file_prefix: String, 46 | #[arg(short, long)] 47 | num_epochs: u32, 48 | #[arg(short, long, default_value_t = 0)] 49 | start_epoch: u32, 50 | } 51 | 52 | #[derive(Debug, Args)] 53 | struct PrintArgs { 54 | #[arg(short, long)] 55 | weight_file: String, 56 | } 57 | 58 | fn main() -> std::io::Result<()> { 59 | let cli = Cli::parse(); 60 | 61 | match &cli.command { 62 | Commands::Extract(args) => extract_weights(&args.weight_file_prefix)?, 63 | Commands::Optimize(args) => optimize( 64 | &args.training_data_file, 65 | &args.weight_file_prefix, 66 | args.start_epoch, 67 | args.num_epochs, 68 | )?, 69 | Commands::Print(args) => write_weights(&args.weight_file)?, 70 | }; 71 | 72 | Ok(()) 73 | } 74 | 75 | fn extract_weights(weight_file_prefix: &str) -> std::io::Result<()> { 76 | let weights = initialize_weights(); 77 | let checkpoint = Checkpoint { 78 | weights, 79 | ..Default::default() 80 | }; 81 | let serialized = serde_json::to_string(&checkpoint)?; 82 | let filename = format!("{weight_file_prefix}{:04}.json", 0); 83 | let mut file = File::create(filename)?; 84 | file.write_all(serialized.as_bytes())?; 85 | Ok(()) 86 | } 87 | 88 | fn optimize( 89 | training_data_file: &str, 90 | weight_file_prefix: &str, 91 | start_epoch: u32, 92 | num_epochs: u32, 93 | ) -> std::io::Result<()> { 94 | let filename = format!("{weight_file_prefix}{:04}.json", start_epoch); 95 | let contents = fs::read_to_string(filename)?; 96 | let initial_checkpoint: Checkpoint = serde_json::from_str(&contents)?; 97 | let mut weights = initial_checkpoint.weights; 98 | 99 | let adam_params = AdamParams { 100 | ..initial_checkpoint.params 101 | }; 102 | 103 | let k = 1.0; 104 | let mut error_fn = ErrorFunction::new(k); 105 | 106 | let mut pos_evaluator = Complex::new(); 107 | let feature_evaluator = FeatureEvaluator::new(); 108 | 109 | let mut training_features = 110 | file_reader::read_training_data(training_data_file, &mut pos_evaluator, &feature_evaluator); 111 | 112 | optimizer::adam( 113 | weight_file_prefix, 114 | &mut weights, 115 | &mut error_fn, 116 | &mut training_features, 117 | adam_params, 118 | num_epochs as i32, 119 | ) 120 | } 121 | 122 | fn write_weights(weight_file: &str) -> std::io::Result<()> { 123 | let contents = fs::read_to_string(weight_file)?; 124 | let final_checkpoint: Checkpoint = serde_json::from_str(&contents)?; 125 | let weights = final_checkpoint.weights; 126 | let eval_params = EvalParams::from(&weights); 127 | println!("{eval_params}"); 128 | Ok(()) 129 | } 130 | -------------------------------------------------------------------------------- /search/benches/bench_search.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion, Throughput}; 2 | use crossbeam_channel::{unbounded, Receiver}; 3 | use eval::complex::Complex; 4 | use eval::Eval; 5 | use movegen::fen::Fen; 6 | use movegen::position::Position; 7 | use movegen::position_history::PositionHistory; 8 | use search::alpha_beta::AlphaBeta; 9 | use search::search::{Search, SearchInfo, SearchResult}; 10 | use search::searcher::Searcher; 11 | use search::SearchOptions; 12 | use std::time::Duration; 13 | 14 | const TIMEOUT_PER_BENCH: Duration = Duration::from_millis(10000); 15 | 16 | fn evaluator() -> impl Eval { 17 | Complex::new() 18 | } 19 | 20 | struct SearchBencher { 21 | searcher: Searcher, 22 | result_receiver: Receiver, 23 | } 24 | 25 | impl SearchBencher { 26 | fn new(search_algo: impl Search + Send + 'static) -> SearchBencher { 27 | let (sender, receiver) = unbounded(); 28 | let info_callback = Box::new(move |search_info| { 29 | sender.send(search_info).unwrap(); 30 | }); 31 | 32 | SearchBencher { 33 | searcher: Searcher::new(search_algo, info_callback), 34 | result_receiver: receiver, 35 | } 36 | } 37 | 38 | fn search(&mut self, pos_hist: PositionHistory, depth: usize) -> SearchResult { 39 | let search_options = SearchOptions { 40 | depth: Some(depth), 41 | ..Default::default() 42 | }; 43 | self.searcher.search(pos_hist, search_options); 44 | loop { 45 | let search_result = match self.result_receiver.recv_timeout(TIMEOUT_PER_BENCH) { 46 | Ok(SearchInfo::DepthFinished(res)) => res, 47 | unexp => panic!("Expected SearchInfo::DepthFinished(_), got {:?}", unexp), 48 | }; 49 | assert!( 50 | search_result.depth() <= depth, 51 | "Expected max depth: {}, actual depth: {}", 52 | depth, 53 | search_result.depth() 54 | ); 55 | if search_result.depth() == depth { 56 | self.searcher.stop(); 57 | loop { 58 | if let Ok(SearchInfo::Stopped(_)) = self.result_receiver.recv() { 59 | break; 60 | } 61 | } 62 | return search_result; 63 | } 64 | } 65 | } 66 | } 67 | 68 | fn alpha_beta( 69 | c: &mut Criterion, 70 | group_name: &str, 71 | pos: Position, 72 | min_depth: usize, 73 | max_depth: usize, 74 | ) { 75 | let table_size = 16 * 1024 * 1024; 76 | let pos_history = PositionHistory::new(pos); 77 | 78 | let mut group = c.benchmark_group(group_name); 79 | for depth in min_depth..=max_depth { 80 | group.throughput(Throughput::Elements(depth as u64)); 81 | group.bench_with_input(BenchmarkId::from_parameter(depth), &depth, |b, &depth| { 82 | b.iter_batched( 83 | || SearchBencher::new(AlphaBeta::new(Box::new(evaluator()), table_size)), 84 | |mut searcher| searcher.search(pos_history.clone(), depth), 85 | BatchSize::SmallInput, 86 | ); 87 | }); 88 | } 89 | group.finish(); 90 | } 91 | 92 | fn alpha_beta_initial_position(c: &mut Criterion) { 93 | let group_name = "Alpha-Beta initial position"; 94 | let pos = Position::initial(); 95 | let min_depth = 1; 96 | let max_depth = 6; 97 | 98 | alpha_beta(c, group_name, pos, min_depth, max_depth); 99 | } 100 | 101 | fn alpha_beta_middlegame_position(c: &mut Criterion) { 102 | let group_name = "Alpha-Beta middlegame position"; 103 | // Position from https://www.chessprogramming.org/Perft_Results 104 | let fen = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1"; 105 | let pos = Fen::str_to_pos(fen).unwrap(); 106 | let min_depth = 1; 107 | let max_depth = 6; 108 | 109 | alpha_beta(c, group_name, pos, min_depth, max_depth); 110 | } 111 | 112 | criterion_group!( 113 | benches, 114 | alpha_beta_initial_position, 115 | alpha_beta_middlegame_position, 116 | ); 117 | criterion_main!(benches); 118 | -------------------------------------------------------------------------------- /uci/src/uci_in/position.rs: -------------------------------------------------------------------------------- 1 | use crate::parser::{split_first_word, ParserMessage, UciError}; 2 | use crate::uci_move::UciMove; 3 | use crate::UciOut; 4 | use engine::{Engine, Variant}; 5 | use movegen::fen::Fen; 6 | use movegen::position::Position as Pos; 7 | use movegen::position_history::PositionHistory; 8 | use std::error::Error; 9 | 10 | pub fn run_command( 11 | _uci_out: &mut UciOut, 12 | args: &str, 13 | engine: &mut Engine, 14 | ) -> Result, Box> { 15 | let (mut pos_hist, moves_str) = match split_first_word(args) { 16 | Some(("fen", tail)) => parse_fen(tail, engine)?, 17 | Some(("startpos", tail)) => (PositionHistory::new(Pos::initial()), tail), 18 | _ => { 19 | return Err(Box::new(UciError::InvalidArgument(format!( 20 | "position {}", 21 | args.trim_end() 22 | )))) 23 | } 24 | }; 25 | 26 | let var = engine.variant(); 27 | 28 | match split_first_word(moves_str) { 29 | Some(("moves", tail)) => { 30 | let iter = tail.split_whitespace(); 31 | match var { 32 | Variant::Standard => { 33 | for move_str in iter { 34 | match UciMove::str_to_move(pos_hist.current_pos(), move_str) { 35 | Some(m) => pos_hist.do_move(m), 36 | None => { 37 | return Err(Box::new(UciError::InvalidArgument(format!( 38 | "Invalid move `{}` in command: position {}", 39 | move_str, 40 | args.trim_end() 41 | )))) 42 | } 43 | } 44 | } 45 | } 46 | Variant::Chess960(king_rook, queen_rook) => { 47 | for move_str in iter { 48 | match UciMove::str_to_move_chess_960( 49 | pos_hist.current_pos(), 50 | move_str, 51 | king_rook, 52 | queen_rook, 53 | ) { 54 | Some(m) => pos_hist.do_move(m), 55 | None => { 56 | return Err(Box::new(UciError::InvalidArgument(format!( 57 | "Invalid move `{}` in command: position {}", 58 | move_str, 59 | args.trim_end() 60 | )))) 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } 67 | None => {} 68 | _ => { 69 | return Err(Box::new(UciError::InvalidArgument(format!( 70 | "position {}", 71 | args.trim_end() 72 | )))) 73 | } 74 | }; 75 | 76 | engine.set_position_history(Some(pos_hist)); 77 | Ok(None) 78 | } 79 | 80 | fn parse_fen<'a>( 81 | args: &'a str, 82 | engine: &'a Engine, 83 | ) -> Result<(PositionHistory, &'a str), Box> { 84 | let trimmed = args.trim_start(); 85 | match trimmed 86 | .chars() 87 | .scan(0, |count, c| { 88 | *count += c.is_whitespace() as usize; 89 | Some(*count) 90 | }) 91 | .position(|c| c == 6) 92 | { 93 | Some(fen_end) => { 94 | let opt_pos = match engine.variant() { 95 | Variant::Standard => Fen::str_to_pos(&trimmed[..fen_end]), 96 | Variant::Chess960(_, _) => Fen::str_to_pos_chess_960(&trimmed[..fen_end]), 97 | }; 98 | match opt_pos { 99 | Ok(pos) => { 100 | if let Variant::Chess960(_, _) = engine.variant() { 101 | engine.set_variant(Variant::Chess960( 102 | pos.kingside_rook_start_file(), 103 | pos.queenside_rook_start_file(), 104 | )); 105 | } 106 | Ok((PositionHistory::new(pos), &trimmed[fen_end..])) 107 | } 108 | Err(e) => Err(Box::new(UciError::InvalidArgument(format!( 109 | "position fen {args}\n{e}", 110 | )))), 111 | } 112 | } 113 | None => Err(Box::new(UciError::InvalidArgument(format!( 114 | "position fen {}", 115 | args.trim_end() 116 | )))), 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Tests](https://github.com/FitzOReilly/fatalii/actions/workflows/tests.yml/badge.svg)](https://github.com/FitzOReilly/fatalii/actions/workflows/tests.yml) 2 | [![codecov](https://codecov.io/gh/FitzOReilly/fatalii/branch/main/graph/badge.svg?token=KJNHD6Z7ZM)](https://codecov.io/gh/FitzOReilly/fatalii) 3 | 4 | UCI compliant chess engine 5 | 6 | ## Play online 7 | Challenge me on Lichess: https://lichess.org/@/FataliiBot 8 | 9 | ## Download 10 | Binaries for Linux and Windows are available on the 11 | [release page](https://github.com/FitzOReilly/fatalii/releases). 12 | 13 | ## Building from source 14 | To build the engine from source, you need a [Rust](https://www.rust-lang.org/) 15 | compiler. Clone the repo: 16 | ``` 17 | # Using SSH 18 | git clone git@github.com:FitzOReilly/fatalii.git 19 | # Alternatively, using HTTPS 20 | git clone https://github.com/FitzOReilly/fatalii.git 21 | ``` 22 | and build the engine: 23 | ``` 24 | cd fatalii 25 | cargo build --profile release-lto --bin fatalii 26 | ``` 27 | The binary will be in `target/release-lto/fatalii`. 28 | 29 | ## Usage 30 | Fatalii supports the UCI protocol (universal chess interface), so it can be used 31 | with a UCI compatible GUI. Some choices are 32 | [Cute Chess](https://cutechess.com/), [Arena](http://www.playwitharena.de/) or 33 | [Lucas Chess](https://lucaschess.pythonanywhere.com/). 34 | 35 | 36 | ## UCI options 37 | - `Hash` \ 38 | The size of the hash table in MB 39 | - `Move Overhead` \ 40 | Subtract this value from the movetime to compensate for network delays or GUI overheads 41 | - `UCI_Chess960` \ 42 | Enable Chess960 if this is set to true 43 | 44 | ## Supported variants 45 | Fatalii supports both standard chess and Chess960 (a.k.a. Fischer Random Chess). 46 | 47 | ## Features 48 | - Bitboards using file rank mapping 49 | - Move generator using kindergarten bitboards for sliding pieces 50 | - Evaluation 51 | - Piece square tables (symmetrical) 52 | - Pawn structure: passed, isolated, backward and doubled pawns 53 | - Mobility 54 | - Bishop pair 55 | - Tempo 56 | - King tropism 57 | - Tapered evaluation for all parameters 58 | - Tuned with training positions from the 59 | [Zurichess dataset quiet-labeled.v7](https://bitbucket.org/zurichess/tuner/downloads/quiet-labeled.v7.epd.gz) 60 | - Search 61 | - Iterative deepening 62 | - Principal variation search 63 | - Aspiration windows 64 | - Quiescence search 65 | - Move ordering 66 | - Root move ordering based on the previous iteration and on subtree size 67 | - Principal variation move 68 | - Hash move from the transposition table (if there are multiple TT entries 69 | for the position, all of them will be used) 70 | - Queen promotions 71 | - Winning and equal captures (estimated by static exchange evaluation (SEE)) 72 | - Killer heuristic 73 | - Countermove heuristic 74 | - History heuristic 75 | - Losing captures (negative SEE) 76 | - Underpromotions last 77 | - Pruning 78 | - Fail-soft alpha-beta pruning 79 | - Null move pruning 80 | - Futility pruning 81 | - Reverse futility pruning 82 | - Late move reductions 83 | - Late move pruning 84 | - SEE pruning 85 | - Delta pruning in quiescence search 86 | - Check extensions 87 | - Transposition table 88 | - Zobrist hashing 89 | - 4 entries per bucket 90 | - Replacement scheme based on entry age and depth 91 | - Draw detection 92 | - 3-fold repetition 93 | - 50 move rule 94 | - Insufficient material 95 | 96 | ## Thanks to 97 | - The [Chess Programming Wiki](https://www.chessprogramming.org). It has been 98 | extremely helpful during development. A lot of ideas have been taken from it 99 | and I've also learned a lot from it. 100 | - Other open source chess engines. 101 | - [Lichess](https://lichess.org/) for being an awesome chess site and for 102 | letting bots play there. And https://github.com/lichess-bot-devs/lichess-bot 103 | for making it easy to create such a bot. 104 | - The folks at [CCRL](https://www.computerchess.org.uk/ccrl/404/) for rating the engine. 105 | - [Cute Chess](https://cutechess.com/). The CLI was extensively used for 106 | self-play testing. 107 | 108 | ### More helpful resources 109 | - The UCI specification: https://www.shredderchess.com/download/div/uci.zip 110 | - http://talkchess.com/forum3/index.php 111 | - https://andrewra.dev/2019/08/05/testing-in-rust-writing-to-stdout/ 112 | - Evaluation tuning 113 | - Andrew Grant's paper: 114 | https://github.com/AndyGrant/Ethereal/blob/master/Tuning.pdf 115 | - Good explanation of gradient descent optimization algorithms: 116 | https://www.ruder.io/optimizing-gradient-descent/ 117 | - The Zurichess datasets: https://bitbucket.org/zurichess/tuner/downloads/ 118 | - Tuning search parameters 119 | - https://chess-tuning-tools.readthedocs.io/en/latest/ 120 | -------------------------------------------------------------------------------- /search/src/search.rs: -------------------------------------------------------------------------------- 1 | use crate::search_params::SearchParamsOptions; 2 | use crate::SearchOptions; 3 | use crossbeam_channel::{Receiver, Sender}; 4 | use eval::Score; 5 | use movegen::position_history::PositionHistory; 6 | use movegen::r#move::{Move, MoveList}; 7 | use std::fmt; 8 | use std::ops::Neg; 9 | 10 | pub const MAX_SEARCH_DEPTH: usize = 127; 11 | pub const REPETITIONS_TO_DRAW: usize = 3; 12 | pub const PLIES_WITHOUT_PAWN_MOVE_OR_CAPTURE_TO_DRAW: usize = 100; 13 | 14 | #[derive(Debug, Clone, PartialEq, Eq)] 15 | pub struct SearchResult { 16 | depth: u8, 17 | selective_depth: u8, 18 | score: Score, 19 | nodes: u64, 20 | time_us: u64, 21 | hash_load_factor_permille: u16, 22 | best_move: Move, 23 | pv: MoveList, 24 | } 25 | 26 | impl SearchResult { 27 | #[allow(clippy::too_many_arguments)] 28 | pub fn new( 29 | depth: usize, 30 | selective_depth: usize, 31 | score: Score, 32 | nodes: u64, 33 | time_us: u64, 34 | hash_load_factor_permille: u16, 35 | best_move: Move, 36 | pv: MoveList, 37 | ) -> Self { 38 | debug_assert!(depth <= MAX_SEARCH_DEPTH); 39 | Self { 40 | depth: depth as u8, 41 | selective_depth: selective_depth as u8, 42 | score, 43 | nodes, 44 | time_us, 45 | hash_load_factor_permille, 46 | best_move, 47 | pv, 48 | } 49 | } 50 | 51 | pub fn depth(&self) -> usize { 52 | self.depth as usize 53 | } 54 | 55 | pub fn selective_depth(&self) -> usize { 56 | self.selective_depth as usize 57 | } 58 | 59 | pub fn score(&self) -> Score { 60 | self.score 61 | } 62 | 63 | pub fn nodes(&self) -> u64 { 64 | self.nodes 65 | } 66 | 67 | pub fn time_ms(&self) -> u64 { 68 | self.time_us / 1000 69 | } 70 | 71 | pub fn time_us(&self) -> u64 { 72 | self.time_us 73 | } 74 | 75 | pub fn nodes_per_second(&self) -> i32 { 76 | match self.time_us() { 77 | 0 => -1, 78 | _ => (1_000_000 * self.nodes() / self.time_us()) as i32, 79 | } 80 | } 81 | 82 | pub fn hash_load_factor_permille(&self) -> u16 { 83 | self.hash_load_factor_permille 84 | } 85 | 86 | pub fn best_move(&self) -> Move { 87 | self.best_move 88 | } 89 | 90 | pub fn principal_variation(&self) -> &MoveList { 91 | &self.pv 92 | } 93 | } 94 | 95 | impl Neg for SearchResult { 96 | type Output = Self; 97 | 98 | // Changes the sign of the score and leaves the best move unchanged 99 | fn neg(self) -> Self::Output { 100 | Self::new( 101 | self.depth(), 102 | self.selective_depth(), 103 | -self.score(), 104 | self.nodes(), 105 | self.time_us(), 106 | self.hash_load_factor_permille(), 107 | self.best_move(), 108 | self.principal_variation().clone(), 109 | ) 110 | } 111 | } 112 | 113 | impl fmt::Display for SearchResult { 114 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 115 | write!( 116 | f, 117 | "depth: {}, selective depth: {}, score: {}, nodes: {}, time in us: {}, hash load factor permille: {}, best move: {}", 118 | self.depth(), 119 | self.selective_depth(), 120 | self.score(), 121 | self.nodes(), 122 | self.time_us(), 123 | self.hash_load_factor_permille(), 124 | self.best_move() 125 | ) 126 | } 127 | } 128 | 129 | #[derive(Debug)] 130 | pub enum SearchCommand { 131 | SetHashSize(usize, Sender<()>), 132 | ClearHashTable(Sender<()>), 133 | SetSearchParams(SearchParamsOptions, Sender<()>), 134 | Search(Box<(PositionHistory, SearchOptions)>), 135 | Stop, 136 | Terminate, 137 | } 138 | 139 | #[derive(Debug)] 140 | pub enum SearchInfo { 141 | DepthFinished(SearchResult), 142 | Stopped(Move), 143 | Terminated, 144 | } 145 | 146 | impl fmt::Display for SearchInfo { 147 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 148 | match self { 149 | SearchInfo::DepthFinished(search_res) => write!(f, "Depth finished: {search_res}"), 150 | SearchInfo::Stopped(best_move) => write!(f, "Search stopped: {best_move}"), 151 | SearchInfo::Terminated => write!(f, "Search terminated"), 152 | } 153 | } 154 | } 155 | 156 | pub trait Search { 157 | fn set_hash_size(&mut self, bytes: usize); 158 | 159 | fn clear_hash_table(&mut self); 160 | 161 | fn set_params(&mut self, params: SearchParamsOptions); 162 | 163 | fn search( 164 | &mut self, 165 | pos_history: PositionHistory, 166 | search_options: SearchOptions, 167 | command_receiver: &Receiver, 168 | info_sender: &Sender, 169 | ); 170 | } 171 | -------------------------------------------------------------------------------- /search/src/search_params.rs: -------------------------------------------------------------------------------- 1 | use eval::Score; 2 | 3 | // Minimum depth for principal variation search. Disable null-window searches below this depth. 4 | pub const MIN_PVS_DEPTH: usize = 3; 5 | 6 | // Minimum depth for null move pruning. 7 | pub const MIN_NULL_MOVE_PRUNE_DEPTH: usize = 3; 8 | 9 | // Minimum depth for late move reductions. 10 | pub const MIN_LATE_MOVE_REDUCTION_DEPTH: usize = 3; 11 | 12 | // Enable futility pruning if the evaluation plus this value is less than alpha. 13 | const FUTILITY_MARGIN_BASE: Score = 12; 14 | const FUTILITY_MARGIN_PER_DEPTH: Score = 235; 15 | const FUTILITY_PRUNING_MAX_DEPTH: usize = 5; 16 | 17 | // Enable reverse futility pruning if the evaluation plus this value is greater than or equal to beta. 18 | const REVERSE_FUTILITY_MARGIN_BASE: Score = 115; 19 | const REVERSE_FUTILITY_MARGIN_PER_DEPTH: Score = 51; 20 | const REVERSE_FUTILITY_PRUNING_MAX_DEPTH: usize = 6; 21 | 22 | // Late move pruning 23 | const LATE_MOVE_PRUNING_BASE: usize = 4; 24 | const LATE_MOVE_PRUNING_FACTOR: usize = 1; 25 | const LATE_MOVE_PRUNING_MAX_DEPTH: usize = 5; 26 | 27 | // Late move reductions 28 | const LATE_MOVE_REDUCTIONS_CENTI_BASE: usize = 25; 29 | const LATE_MOVE_REDUCTIONS_CENTI_DIVISOR: usize = 500; 30 | 31 | // Prune a move if the static evaluation plus the move's potential improvement 32 | // plus this value is less than alpha. 33 | pub const DELTA_PRUNING_MARGIN_MOVE: Score = 200; 34 | 35 | // Prune all moves if the static evaluation plus this value is less than alpha. 36 | pub const DELTA_PRUNING_MARGIN_ALL_MOVES: Score = 1800; 37 | 38 | // Static exchange evaluation pruning 39 | const SEE_PRUNING_MARGIN_QUIET: Score = -100; 40 | const SEE_PRUNING_MARGIN_TACTICAL: Score = -50; 41 | const SEE_PRUNING_MAX_DEPTH: usize = 4; 42 | 43 | // Aspiration windows initial width and grow rate on fail-low/hign 44 | const ASPIRATION_WINDOW_INITIAL_WIDTH: i32 = 101; 45 | const ASPIRATION_WINDOW_GROW_RATE: i32 = 15; 46 | 47 | pub struct SearchParams { 48 | pub futility_margin_base: Score, 49 | pub futility_margin_per_depth: Score, 50 | pub futility_pruning_max_depth: usize, 51 | pub reverse_futility_margin_base: Score, 52 | pub reverse_futility_margin_per_depth: Score, 53 | pub reverse_futility_pruning_max_depth: usize, 54 | pub late_move_pruning_base: usize, 55 | pub late_move_pruning_factor: usize, 56 | pub late_move_pruning_max_depth: usize, 57 | pub late_move_reductions_centi_base: usize, 58 | pub late_move_reductions_centi_divisor: usize, 59 | pub see_pruning_margin_quiet: Score, 60 | pub see_pruning_margin_tactical: Score, 61 | pub see_pruning_max_depth: usize, 62 | pub aspiration_window_initial_width: i32, 63 | pub aspiration_window_grow_rate: i32, 64 | } 65 | 66 | impl Default for SearchParams { 67 | fn default() -> Self { 68 | Self { 69 | futility_margin_base: FUTILITY_MARGIN_BASE, 70 | futility_margin_per_depth: FUTILITY_MARGIN_PER_DEPTH, 71 | futility_pruning_max_depth: FUTILITY_PRUNING_MAX_DEPTH, 72 | reverse_futility_margin_base: REVERSE_FUTILITY_MARGIN_BASE, 73 | reverse_futility_margin_per_depth: REVERSE_FUTILITY_MARGIN_PER_DEPTH, 74 | reverse_futility_pruning_max_depth: REVERSE_FUTILITY_PRUNING_MAX_DEPTH, 75 | late_move_pruning_base: LATE_MOVE_PRUNING_BASE, 76 | late_move_pruning_factor: LATE_MOVE_PRUNING_FACTOR, 77 | late_move_pruning_max_depth: LATE_MOVE_PRUNING_MAX_DEPTH, 78 | late_move_reductions_centi_base: LATE_MOVE_REDUCTIONS_CENTI_BASE, 79 | late_move_reductions_centi_divisor: LATE_MOVE_REDUCTIONS_CENTI_DIVISOR, 80 | see_pruning_margin_quiet: SEE_PRUNING_MARGIN_QUIET, 81 | see_pruning_margin_tactical: SEE_PRUNING_MARGIN_TACTICAL, 82 | see_pruning_max_depth: SEE_PRUNING_MAX_DEPTH, 83 | aspiration_window_initial_width: ASPIRATION_WINDOW_INITIAL_WIDTH, 84 | aspiration_window_grow_rate: ASPIRATION_WINDOW_GROW_RATE, 85 | } 86 | } 87 | } 88 | 89 | #[derive(Debug, Default)] 90 | pub struct SearchParamsOptions { 91 | pub futility_margin_base: Option, 92 | pub futility_margin_per_depth: Option, 93 | pub futility_pruning_max_depth: Option, 94 | pub reverse_futility_margin_base: Option, 95 | pub reverse_futility_margin_per_depth: Option, 96 | pub reverse_futility_pruning_max_depth: Option, 97 | pub late_move_pruning_base: Option, 98 | pub late_move_pruning_factor: Option, 99 | pub late_move_pruning_max_depth: Option, 100 | pub late_move_reductions_centi_base: Option, 101 | pub late_move_reductions_centi_divisor: Option, 102 | pub see_pruning_margin_quiet: Option, 103 | pub see_pruning_margin_tactical: Option, 104 | pub see_pruning_max_depth: Option, 105 | pub aspiration_window_initial_width: Option, 106 | pub aspiration_window_grow_rate: Option, 107 | } 108 | -------------------------------------------------------------------------------- /fatalii/tests/cli.rs: -------------------------------------------------------------------------------- 1 | use assert_matches::assert_matches; 2 | use rexpect::{error::Error, process::wait::WaitStatus, spawn}; 3 | use std::{thread, time::Duration}; 4 | 5 | #[test] 6 | #[ignore] 7 | fn test_cli() -> Result<(), Error> { 8 | let _ = spawn("cargo build --release", Some(120000))?; 9 | 10 | uci_commands()?; 11 | quit_while_timer_running()?; 12 | go_all_options()?; 13 | chess_960()?; 14 | stress()?; 15 | 16 | Ok(()) 17 | } 18 | 19 | fn uci_commands() -> Result<(), Error> { 20 | let mut p = spawn("cargo run --release", Some(30000))?; 21 | 22 | p.send_line("uci")?; 23 | p.exp_string("id name")?; 24 | p.exp_string("id author")?; 25 | p.exp_string("option name")?; 26 | p.exp_string("option name UCI_Chess960 type check default false")?; 27 | p.exp_string("uciok")?; 28 | 29 | p.send_line("isready")?; 30 | p.exp_string("readyok")?; 31 | 32 | p.send_line("debug on")?; 33 | p.exp_string("info string")?; 34 | p.send_line("debug off")?; 35 | p.exp_string("info string")?; 36 | 37 | p.send_line("ucinewgame")?; 38 | 39 | p.send_line("position startpos")?; 40 | p.send_line("go infinite")?; 41 | thread::sleep(Duration::from_millis(100)); 42 | p.send_line("stop")?; 43 | thread::sleep(Duration::from_millis(10)); 44 | p.exp_string("bestmove")?; 45 | 46 | p.send_line("ucinewgame")?; 47 | p.send_line("go infinite")?; 48 | p.exp_string("Engine error")?; 49 | 50 | assert_matches!(p.process.status(), Some(WaitStatus::StillAlive)); 51 | p.send_line("quit")?; 52 | thread::sleep(Duration::from_millis(100)); 53 | assert_matches!(p.process.status(), Some(WaitStatus::Exited(_, 0))); 54 | 55 | Ok(()) 56 | } 57 | 58 | fn quit_while_timer_running() -> Result<(), Error> { 59 | let mut p = spawn("cargo run --release", Some(30000))?; 60 | 61 | p.send_line("uci")?; 62 | p.send_line("isready")?; 63 | p.send_line("ucinewgame")?; 64 | p.send_line("position startpos")?; 65 | p.send_line("go wtime 121000 btime 121000")?; 66 | thread::sleep(Duration::from_millis(100)); 67 | p.send_line("quit")?; 68 | thread::sleep(Duration::from_millis(1000)); 69 | assert_matches!(p.process.status(), Some(WaitStatus::Exited(_, 0))); 70 | 71 | Ok(()) 72 | } 73 | 74 | fn go_all_options() -> Result<(), Error> { 75 | let mut p = spawn("cargo run --release", Some(30000))?; 76 | 77 | p.send_line("uci")?; 78 | p.send_line("isready")?; 79 | p.send_line("ucinewgame")?; 80 | p.send_line("position startpos")?; 81 | 82 | p.send_line("go searchmoves e2e4 d2d4 wtime 500 btime 500")?; 83 | thread::sleep(Duration::from_millis(500)); 84 | p.exp_string("bestmove")?; 85 | 86 | p.send_line("go ponder wtime 500 btime 500")?; 87 | thread::sleep(Duration::from_millis(500)); 88 | p.exp_string("bestmove")?; 89 | 90 | p.send_line("go wtime 500 btime 500 winc 100 binc 100 movestogo 40 depth 3 nodes 1000 mate 5")?; 91 | thread::sleep(Duration::from_millis(500)); 92 | p.exp_string("bestmove")?; 93 | 94 | p.send_line("go movetime 100")?; 95 | p.exp_string("bestmove")?; 96 | 97 | p.send_line("go infinite")?; 98 | thread::sleep(Duration::from_millis(100)); 99 | p.send_line("stop")?; 100 | p.exp_string("bestmove")?; 101 | 102 | p.send_line("quit")?; 103 | thread::sleep(Duration::from_millis(400)); 104 | assert_matches!(p.process.status(), Some(WaitStatus::Exited(_, 0))); 105 | 106 | Ok(()) 107 | } 108 | 109 | fn chess_960() -> Result<(), Error> { 110 | let mut p = spawn("cargo run --release", Some(30000))?; 111 | 112 | p.send_line("setoption name UCI_Chess960 value true")?; 113 | p.send_line("position fen rnkbnqbr/pppppppp/8/8/8/8/PPPPPPPP/RNKBNQBR w HAha - 0 1")?; 114 | p.send_line("go depth 7")?; 115 | p.exp_string("bestmove")?; 116 | 117 | p.send_line("position fen rkbnqbrn/pppppppp/8/8/8/8/PPPPPPPP/RKBNQBRN w GAga - 0 1 moves e2e4 e7e5 h1g3 h8g6 f1c4 f8c5 d1c3 d8c6 d2d3 d7d6 c1e3")?; 118 | p.send_line("go depth 7")?; 119 | p.exp_string("bestmove")?; 120 | 121 | assert_matches!(p.process.status(), Some(WaitStatus::StillAlive)); 122 | p.send_line("quit")?; 123 | thread::sleep(Duration::from_millis(100)); 124 | assert_matches!(p.process.status(), Some(WaitStatus::Exited(_, 0))); 125 | 126 | Ok(()) 127 | } 128 | 129 | fn stress() -> Result<(), Error> { 130 | let mut p = spawn("cargo run --release", Some(30000))?; 131 | 132 | p.send_line("uci")?; 133 | p.send_line("debug on")?; 134 | 135 | for hash_size in [1, 8, 64] { 136 | p.send_line(format!("setoption name Hash value {}", hash_size).as_str())?; 137 | p.send_line("isready")?; 138 | 139 | for i in 0..10_000 { 140 | println!("Hash size: {}, iteration: {}", hash_size, i); 141 | p.send_line("ucinewgame")?; 142 | p.send_line("isready")?; 143 | p.send_line("position startpos")?; 144 | p.send_line("isready")?; 145 | p.send_line("go wtime 0")?; 146 | p.send_line("isready")?; 147 | } 148 | } 149 | Ok(()) 150 | } 151 | -------------------------------------------------------------------------------- /search/src/aspiration_window.rs: -------------------------------------------------------------------------------- 1 | use crate::search_params::SearchParams; 2 | use eval::{Score, NEG_INF, POS_INF}; 3 | 4 | #[derive(Debug)] 5 | pub struct AspirationWindow { 6 | score: i32, 7 | width_down: i32, 8 | width_up: i32, 9 | grow_rate: i32, 10 | alpha: Score, 11 | beta: Score, 12 | } 13 | 14 | impl AspirationWindow { 15 | pub fn infinite() -> Self { 16 | Self { 17 | score: 0, 18 | width_up: POS_INF as i32, 19 | width_down: POS_INF as i32, 20 | grow_rate: SearchParams::default().aspiration_window_grow_rate, 21 | alpha: NEG_INF, 22 | beta: POS_INF, 23 | } 24 | } 25 | 26 | pub fn new(s: Score, initial_width: i32, grow_rate: i32) -> Self { 27 | Self { 28 | score: s as i32, 29 | width_up: initial_width, 30 | width_down: initial_width, 31 | grow_rate, 32 | alpha: calc_alpha(s as i32, initial_width), 33 | beta: calc_beta(s as i32, initial_width), 34 | } 35 | } 36 | 37 | pub fn alpha(&self) -> Score { 38 | self.alpha 39 | } 40 | 41 | pub fn beta(&self) -> Score { 42 | self.beta 43 | } 44 | 45 | pub fn widen_down(&mut self) { 46 | self.width_down = (self.width_down * self.grow_rate).clamp(0, self.score - NEG_INF as i32); 47 | self.alpha = (self.score - self.width_down) as Score; 48 | } 49 | 50 | pub fn widen_up(&mut self) { 51 | self.width_up = (self.width_up * self.grow_rate).clamp(0, POS_INF as i32 - self.score); 52 | self.beta = (self.score + self.width_up) as Score; 53 | } 54 | } 55 | 56 | fn calc_alpha(score: i32, width_down: i32) -> Score { 57 | (score - width_down).clamp(NEG_INF as i32, POS_INF as i32) as Score 58 | } 59 | 60 | fn calc_beta(score: i32, width_up: i32) -> Score { 61 | (score + width_up).clamp(NEG_INF as i32, POS_INF as i32) as Score 62 | } 63 | 64 | #[cfg(test)] 65 | mod tests { 66 | use super::*; 67 | 68 | #[test] 69 | fn infinite() { 70 | let aw = AspirationWindow::infinite(); 71 | assert_eq!(NEG_INF, aw.alpha()); 72 | assert_eq!(POS_INF, aw.beta()); 73 | } 74 | 75 | #[test] 76 | fn widen() { 77 | let score = 200; 78 | let mut aw = AspirationWindow::new( 79 | score, 80 | SearchParams::default().aspiration_window_initial_width, 81 | SearchParams::default().aspiration_window_grow_rate, 82 | ); 83 | assert_eq!( 84 | score - SearchParams::default().aspiration_window_initial_width as Score, 85 | aw.alpha() 86 | ); 87 | assert_eq!( 88 | score + SearchParams::default().aspiration_window_initial_width as Score, 89 | aw.beta() 90 | ); 91 | 92 | aw.widen_down(); 93 | assert_eq!( 94 | score 95 | - (SearchParams::default().aspiration_window_initial_width 96 | * SearchParams::default().aspiration_window_grow_rate) 97 | as Score, 98 | aw.alpha() 99 | ); 100 | assert_eq!( 101 | score + SearchParams::default().aspiration_window_initial_width as Score, 102 | aw.beta() 103 | ); 104 | 105 | aw.widen_up(); 106 | assert_eq!( 107 | score 108 | - (SearchParams::default().aspiration_window_initial_width 109 | * SearchParams::default().aspiration_window_grow_rate) 110 | as Score, 111 | aw.alpha() 112 | ); 113 | assert_eq!( 114 | score 115 | + (SearchParams::default().aspiration_window_initial_width 116 | * SearchParams::default().aspiration_window_grow_rate) 117 | as Score, 118 | aw.beta() 119 | ); 120 | 121 | let mut prev_alpha = aw.alpha() + 1; 122 | while aw.alpha() != prev_alpha { 123 | prev_alpha = aw.alpha(); 124 | aw.widen_down(); 125 | } 126 | assert_eq!(NEG_INF, aw.alpha()); 127 | let mut prev_beta = aw.beta() - 1; 128 | while aw.beta() != prev_beta { 129 | prev_beta = aw.beta(); 130 | aw.widen_up(); 131 | } 132 | assert_eq!(POS_INF, aw.beta()); 133 | 134 | let neg_score = -1000; 135 | let mut neg_aw = AspirationWindow::new( 136 | neg_score, 137 | SearchParams::default().aspiration_window_initial_width, 138 | SearchParams::default().aspiration_window_grow_rate, 139 | ); 140 | let mut prev_alpha = neg_aw.alpha() + 1; 141 | while neg_aw.alpha() != prev_alpha { 142 | prev_alpha = neg_aw.alpha(); 143 | neg_aw.widen_down(); 144 | } 145 | assert_eq!(NEG_INF, neg_aw.alpha()); 146 | let mut prev_beta = neg_aw.beta() - 1; 147 | while neg_aw.beta() != prev_beta { 148 | prev_beta = neg_aw.beta(); 149 | neg_aw.widen_up(); 150 | } 151 | assert_eq!(POS_INF, neg_aw.beta()); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /uci/src/uci_in/go.rs: -------------------------------------------------------------------------------- 1 | use crate::parser::{split_first_word, ParserMessage, UciError}; 2 | use crate::uci_move::UciMove; 3 | use crate::UciOut; 4 | use engine::{Engine, EngineError}; 5 | use movegen::r#move::MoveList; 6 | use search::SearchOptions; 7 | use std::collections::HashSet; 8 | use std::error::Error; 9 | use std::time::Duration; 10 | 11 | pub fn run_command( 12 | uci_out: &mut UciOut, 13 | args: &str, 14 | engine: &mut Engine, 15 | ) -> Result, Box> { 16 | let options = parse_options(uci_out, args, engine)?; 17 | run(options, engine) 18 | } 19 | 20 | fn parse_options( 21 | uci_out: &UciOut, 22 | go_args: &str, 23 | engine: &Engine, 24 | ) -> Result> { 25 | let mut options = SearchOptions::default(); 26 | let mut seen_options = HashSet::new(); 27 | let mut s = go_args; 28 | while let Some((opt, tail)) = split_first_word(s) { 29 | if !seen_options.insert(opt) { 30 | return Err(Box::new(UciError::InvalidArgument(format!( 31 | "Option `{opt}` must not appear more than once in\ngo {go_args}", 32 | )))); 33 | } 34 | s = match opt { 35 | "searchmoves" => parse_moves(tail, &mut options.search_moves, engine)?, 36 | "ponder" => { 37 | options.ponder = true; 38 | tail 39 | } 40 | name @ "wtime" => parse_duration(uci_out, name, tail, &mut options.white_time)?, 41 | name @ "btime" => parse_duration(uci_out, name, tail, &mut options.black_time)?, 42 | name @ "winc" => parse_duration(uci_out, name, tail, &mut options.white_inc)?, 43 | name @ "binc" => parse_duration(uci_out, name, tail, &mut options.black_inc)?, 44 | name @ "movestogo" => parse_usize(uci_out, name, tail, &mut options.moves_to_go)?, 45 | name @ "depth" => parse_usize(uci_out, name, tail, &mut options.depth)?, 46 | name @ "nodes" => parse_usize(uci_out, name, tail, &mut options.nodes)?, 47 | name @ "mate" => parse_usize(uci_out, name, tail, &mut options.mate_in)?, 48 | name @ "movetime" => parse_duration(uci_out, name, tail, &mut options.movetime)?, 49 | "infinite" => { 50 | options.infinite = true; 51 | tail 52 | } 53 | _ => { 54 | return Err(Box::new(UciError::InvalidArgument(format!( 55 | "go {}", 56 | go_args.trim_end() 57 | )))) 58 | } 59 | } 60 | } 61 | Ok(options) 62 | } 63 | 64 | fn parse_usize<'a>( 65 | uci_out: &UciOut, 66 | opt: &str, 67 | args: &'a str, 68 | number: &mut Option, 69 | ) -> Result<&'a str, Box> { 70 | debug_assert!(number.is_none()); 71 | match split_first_word(args) { 72 | Some((first_word, tail)) => { 73 | let signed_num = first_word.parse::()?; 74 | if signed_num < 0 { 75 | uci_out.warn(&format!( 76 | "ignoring negative value `{opt} {signed_num}`, using `{opt} 0` instead", 77 | ))?; 78 | } 79 | *number = Some(signed_num.max(0) as usize); 80 | Ok(tail) 81 | } 82 | _ => Err(Box::new(UciError::InvalidArgument(format!("go {args}")))), 83 | } 84 | } 85 | 86 | fn parse_duration<'a>( 87 | uci_out: &UciOut, 88 | opt: &str, 89 | args: &'a str, 90 | dur: &mut Option, 91 | ) -> Result<&'a str, Box> { 92 | debug_assert!(dur.is_none()); 93 | match split_first_word(args) { 94 | Some((first_word, tail)) => { 95 | let signed_dur = first_word.parse::()?; 96 | if signed_dur < 0 { 97 | uci_out.warn(&format!( 98 | "ignoring negative value `{opt} {signed_dur}`, using `{opt} 0` instead", 99 | ))?; 100 | } 101 | *dur = Some(Duration::from_millis(signed_dur.max(0) as u64)); 102 | Ok(tail) 103 | } 104 | _ => Err(Box::new(UciError::InvalidArgument(format!("go {args}")))), 105 | } 106 | } 107 | 108 | fn parse_moves<'a>( 109 | args: &'a str, 110 | search_moves: &mut Option, 111 | engine: &Engine, 112 | ) -> Result<&'a str, Box> { 113 | debug_assert!(search_moves.is_none()); 114 | let pos = match engine.position() { 115 | Some(p) => p, 116 | None => return Err(Box::new(EngineError::SearchWithoutPosition)), 117 | }; 118 | let mut move_list = MoveList::new(); 119 | let mut s = args; 120 | while let Some((move_str, tail)) = split_first_word(s) { 121 | match UciMove::str_to_move(pos, move_str) { 122 | Some(m) => move_list.push(m), 123 | None => break, 124 | } 125 | s = tail; 126 | } 127 | *search_moves = Some(move_list); 128 | Ok(s) 129 | } 130 | 131 | fn run( 132 | options: SearchOptions, 133 | engine: &mut Engine, 134 | ) -> Result, Box> { 135 | match engine.search(options) { 136 | Ok(_) => Ok(None), 137 | Err(e) => Err(e.into()), 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /movegen/src/move_generator/king_xrayed_generator.rs: -------------------------------------------------------------------------------- 1 | use crate::move_generator::move_generator_template::MoveGeneratorTemplate; 2 | 3 | use crate::attacks_to::AttacksTo; 4 | use crate::bitboard::Bitboard; 5 | use crate::pawn::Pawn; 6 | use crate::square::Square; 7 | 8 | // Legality checks are done for 9 | // - King move targets 10 | // - Castles 11 | // - Other pieces: 12 | // Only if the piece is attacked and xrayed (potentially pinned to the king) 13 | // - En passant: 14 | // Only if own or opponent pawn is attacked and xrayed (potentially pinned to the king) 15 | pub struct KingXrayedGenerator; 16 | 17 | impl MoveGeneratorTemplate for KingXrayedGenerator { 18 | fn non_capture_target_filter(attacks_to_king: &AttacksTo, targets: Bitboard) -> Bitboard { 19 | targets & !attacks_to_king.pos.occupancy() 20 | } 21 | 22 | fn capture_target_filter(attacks_to_king: &AttacksTo, targets: Bitboard) -> Bitboard { 23 | targets 24 | & attacks_to_king 25 | .pos 26 | .side_occupancy(!attacks_to_king.pos.side_to_move()) 27 | } 28 | 29 | fn pawn_capture_target_filter(attacks_to_king: &AttacksTo, targets: Bitboard) -> Bitboard { 30 | targets 31 | & (attacks_to_king 32 | .pos 33 | .side_occupancy(!attacks_to_king.pos.side_to_move()) 34 | | attacks_to_king.pos.en_passant_square()) 35 | } 36 | 37 | fn is_legal_non_capture(attacks_to_king: &AttacksTo, origin: Square, target: Square) -> bool { 38 | debug_assert!(!attacks_to_king.each_xray.is_empty()); 39 | let pos = attacks_to_king.pos; 40 | let origin_bb = Bitboard::from(origin); 41 | match origin_bb & attacks_to_king.all_attack_targets & attacks_to_king.xrays_to_target { 42 | Bitboard::EMPTY => true, 43 | _ => { 44 | let target_bb = Bitboard::from(target); 45 | let own_king = Bitboard::from(attacks_to_king.target); 46 | let occupancy_after_move = pos.occupancy() & !origin_bb | target_bb; 47 | let king_in_check_after_move = attacks_to_king 48 | .each_xray 49 | .iter() 50 | .filter(|x| x.targets() & origin_bb != Bitboard::EMPTY) 51 | .map(|x| Self::sliding_piece_targets(x, occupancy_after_move)) 52 | .any(|x| x & own_king != Bitboard::EMPTY); 53 | !king_in_check_after_move 54 | } 55 | } 56 | } 57 | 58 | fn is_legal_capture(attacks_to_king: &AttacksTo, origin: Square, target: Square) -> bool { 59 | debug_assert!(!attacks_to_king.each_xray.is_empty()); 60 | let pos = attacks_to_king.pos; 61 | let origin_bb = Bitboard::from(origin); 62 | match origin_bb & attacks_to_king.all_attack_targets & attacks_to_king.xrays_to_target { 63 | Bitboard::EMPTY => true, 64 | _ => { 65 | let own_king = Bitboard::from(attacks_to_king.target); 66 | let occupancy_after_move = pos.occupancy() & !origin_bb; 67 | let king_in_check_after_move = attacks_to_king 68 | .each_xray 69 | .iter() 70 | .filter(|x| { 71 | (x.origin() != target) && (x.targets() & origin_bb != Bitboard::EMPTY) 72 | }) 73 | .map(|x| Self::sliding_piece_targets(x, occupancy_after_move)) 74 | .any(|x| x & own_king != Bitboard::EMPTY); 75 | !king_in_check_after_move 76 | } 77 | } 78 | } 79 | 80 | fn is_legal_en_passant_capture( 81 | attacks_to_king: &AttacksTo, 82 | origin: Square, 83 | target: Square, 84 | ) -> bool { 85 | debug_assert!(!attacks_to_king.each_xray.is_empty()); 86 | let pos = attacks_to_king.pos; 87 | let origin_bb = Bitboard::from(origin); 88 | let target_bb = Bitboard::from(target); 89 | let captured_square = Pawn::push_origin(target, pos.side_to_move()); 90 | let captured_bb = Bitboard::from(captured_square); 91 | match (origin_bb | captured_bb) 92 | & attacks_to_king.all_attack_targets 93 | & attacks_to_king.xrays_to_target 94 | { 95 | Bitboard::EMPTY => true, 96 | _ => { 97 | let own_king = Bitboard::from(attacks_to_king.target); 98 | let occupancy_after_move = pos.occupancy() & !origin_bb & !captured_bb | target_bb; 99 | let king_in_check_after_move = attacks_to_king 100 | .each_xray 101 | .iter() 102 | .filter(|x| x.targets() & (origin_bb | captured_bb) != Bitboard::EMPTY) 103 | .map(|x| Self::sliding_piece_targets(x, occupancy_after_move)) 104 | .any(|x| x & own_king != Bitboard::EMPTY); 105 | !king_in_check_after_move 106 | } 107 | } 108 | } 109 | 110 | fn is_legal_king_move(_attacks_to_king: &AttacksTo, _origin: Square, _target: Square) -> bool { 111 | debug_assert!(_attacks_to_king.each_slider_attack.is_empty()); 112 | true 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /movegen/src/knight.rs: -------------------------------------------------------------------------------- 1 | use crate::bitboard::Bitboard; 2 | use crate::square::Square; 3 | 4 | pub struct Knight; 5 | 6 | impl Knight { 7 | pub fn targets(origin: Square) -> Bitboard { 8 | Self::TARGETS_ALL_SQUARES[origin.idx()] 9 | } 10 | 11 | const TARGETS_ALL_SQUARES: [Bitboard; Square::NUM_SQUARES] = [ 12 | Bitboard(0x20400), 13 | Bitboard(0x50800), 14 | Bitboard(0xa1100), 15 | Bitboard(0x142200), 16 | Bitboard(0x284400), 17 | Bitboard(0x508800), 18 | Bitboard(0xa01000), 19 | Bitboard(0x402000), 20 | Bitboard(0x2040004), 21 | Bitboard(0x5080008), 22 | Bitboard(0xa110011), 23 | Bitboard(0x14220022), 24 | Bitboard(0x28440044), 25 | Bitboard(0x50880088), 26 | Bitboard(0xa0100010), 27 | Bitboard(0x40200020), 28 | Bitboard(0x204000402), 29 | Bitboard(0x508000805), 30 | Bitboard(0xa1100110a), 31 | Bitboard(0x1422002214), 32 | Bitboard(0x2844004428), 33 | Bitboard(0x5088008850), 34 | Bitboard(0xa0100010a0), 35 | Bitboard(0x4020002040), 36 | Bitboard(0x20400040200), 37 | Bitboard(0x50800080500), 38 | Bitboard(0xa1100110a00), 39 | Bitboard(0x142200221400), 40 | Bitboard(0x284400442800), 41 | Bitboard(0x508800885000), 42 | Bitboard(0xa0100010a000), 43 | Bitboard(0x402000204000), 44 | Bitboard(0x2040004020000), 45 | Bitboard(0x5080008050000), 46 | Bitboard(0xa1100110a0000), 47 | Bitboard(0x14220022140000), 48 | Bitboard(0x28440044280000), 49 | Bitboard(0x50880088500000), 50 | Bitboard(0xa0100010a00000), 51 | Bitboard(0x40200020400000), 52 | Bitboard(0x204000402000000), 53 | Bitboard(0x508000805000000), 54 | Bitboard(0xa1100110a000000), 55 | Bitboard(0x1422002214000000), 56 | Bitboard(0x2844004428000000), 57 | Bitboard(0x5088008850000000), 58 | Bitboard(0xa0100010a0000000), 59 | Bitboard(0x4020002040000000), 60 | Bitboard(0x400040200000000), 61 | Bitboard(0x800080500000000), 62 | Bitboard(0x1100110a00000000), 63 | Bitboard(0x2200221400000000), 64 | Bitboard(0x4400442800000000), 65 | Bitboard(0x8800885000000000), 66 | Bitboard(0x100010a000000000), 67 | Bitboard(0x2000204000000000), 68 | Bitboard(0x4020000000000), 69 | Bitboard(0x8050000000000), 70 | Bitboard(0x110a0000000000), 71 | Bitboard(0x22140000000000), 72 | Bitboard(0x44280000000000), 73 | Bitboard(0x88500000000000), 74 | Bitboard(0x10a00000000000), 75 | Bitboard(0x20400000000000), 76 | ]; 77 | } 78 | 79 | #[cfg(test)] 80 | mod tests { 81 | use super::*; 82 | 83 | #[test] 84 | fn targets() { 85 | assert_eq!(Bitboard::B3 | Bitboard::C2, Knight::targets(Square::A1)); 86 | assert_eq!(Bitboard::B6 | Bitboard::C7, Knight::targets(Square::A8)); 87 | assert_eq!(Bitboard::G3 | Bitboard::F2, Knight::targets(Square::H1)); 88 | assert_eq!(Bitboard::G6 | Bitboard::F7, Knight::targets(Square::H8)); 89 | 90 | assert_eq!( 91 | Bitboard::A4 | Bitboard::C4 | Bitboard::D3 | Bitboard::D1, 92 | Knight::targets(Square::B2) 93 | ); 94 | assert_eq!( 95 | Bitboard::A5 | Bitboard::C5 | Bitboard::D6 | Bitboard::D8, 96 | Knight::targets(Square::B7) 97 | ); 98 | assert_eq!( 99 | Bitboard::H4 | Bitboard::F4 | Bitboard::E3 | Bitboard::E1, 100 | Knight::targets(Square::G2) 101 | ); 102 | assert_eq!( 103 | Bitboard::H5 | Bitboard::F5 | Bitboard::E6 | Bitboard::E8, 104 | Knight::targets(Square::G7) 105 | ); 106 | 107 | assert_eq!( 108 | Bitboard::A2 109 | | Bitboard::A4 110 | | Bitboard::B1 111 | | Bitboard::B5 112 | | Bitboard::D1 113 | | Bitboard::D5 114 | | Bitboard::E2 115 | | Bitboard::E4, 116 | Knight::targets(Square::C3) 117 | ); 118 | assert_eq!( 119 | Bitboard::A5 120 | | Bitboard::A7 121 | | Bitboard::B4 122 | | Bitboard::B8 123 | | Bitboard::D4 124 | | Bitboard::D8 125 | | Bitboard::E5 126 | | Bitboard::E7, 127 | Knight::targets(Square::C6) 128 | ); 129 | assert_eq!( 130 | Bitboard::D2 131 | | Bitboard::D4 132 | | Bitboard::E1 133 | | Bitboard::E5 134 | | Bitboard::G1 135 | | Bitboard::G5 136 | | Bitboard::H2 137 | | Bitboard::H4, 138 | Knight::targets(Square::F3) 139 | ); 140 | assert_eq!( 141 | Bitboard::D5 142 | | Bitboard::D7 143 | | Bitboard::E4 144 | | Bitboard::E8 145 | | Bitboard::G4 146 | | Bitboard::G8 147 | | Bitboard::H5 148 | | Bitboard::H7, 149 | Knight::targets(Square::F6) 150 | ); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /tuner/src/optimizer.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::File, io::Write}; 2 | 3 | use rand::{rng, seq::SliceRandom}; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::{ 7 | error_function::ErrorFunction, 8 | feature_evaluator::{FeatureEvaluator, WeightVector}, 9 | training::TrainingFeatures, 10 | }; 11 | 12 | const STORE_EVERY: usize = 10; 13 | 14 | #[derive(Debug, Clone, Serialize, Deserialize)] 15 | pub struct AdamParams { 16 | pub batch_size: usize, 17 | pub validation_ratio: f64, 18 | pub learning_rate: f64, 19 | pub beta_1: f64, 20 | pub beta_2: f64, 21 | pub epsilon: f64, 22 | pub epoch: i32, 23 | pub t: i32, 24 | pub m: WeightVector, 25 | pub v: f64, 26 | } 27 | 28 | impl Default for AdamParams { 29 | fn default() -> Self { 30 | Self { 31 | batch_size: 32, 32 | validation_ratio: 0.1, 33 | learning_rate: 0.001, 34 | beta_1: 0.9, 35 | beta_2: 0.999, 36 | epsilon: 1e-8, 37 | epoch: 0, 38 | t: 0, 39 | m: WeightVector::zeros(), 40 | v: 0.0, 41 | } 42 | } 43 | } 44 | 45 | #[derive(Debug, Serialize, Deserialize)] 46 | pub struct Checkpoint { 47 | pub params: AdamParams, 48 | pub weights: WeightVector, 49 | pub training_error: Option, 50 | pub validation_error: Option, 51 | } 52 | 53 | impl Default for Checkpoint { 54 | fn default() -> Self { 55 | Self { 56 | params: Default::default(), 57 | weights: WeightVector::zeros(), 58 | training_error: Default::default(), 59 | validation_error: Default::default(), 60 | } 61 | } 62 | } 63 | 64 | pub fn adam( 65 | weight_file_prefix: &str, 66 | weights: &mut WeightVector, 67 | error_fn: &mut ErrorFunction, 68 | training_features: &mut [TrainingFeatures], 69 | params: AdamParams, 70 | num_epochs: i32, 71 | ) -> std::io::Result<()> { 72 | let mut evaluator = FeatureEvaluator::from(&*weights); 73 | 74 | let mut m = params.m; 75 | let mut v = params.v; 76 | let mut t = params.t; 77 | let batch_size = params.batch_size; 78 | let validation_ratio = params.validation_ratio; 79 | let learning_rate = params.learning_rate; 80 | let beta_1 = params.beta_1; 81 | let beta_2 = params.beta_2; 82 | let epsilon = params.epsilon; 83 | 84 | for epoch in params.epoch + 1..=params.epoch + num_epochs { 85 | training_features.shuffle(&mut rng()); 86 | let mut iter_batch = training_features.chunks(batch_size); 87 | let batch_count = iter_batch.len(); 88 | let training_batch_count = ((1.0 - validation_ratio) * batch_count as f64) as usize; 89 | let mut current_batch_count = 0; 90 | for batch in iter_batch.by_ref() { 91 | t += 1; 92 | for pos in batch { 93 | error_fn.add_datapoint( 94 | pos.outcome.into(), 95 | evaluator.eval(&pos.features), 96 | &pos.grad, 97 | ); 98 | } 99 | let grad = error_fn.grad(); 100 | let grad_squared = grad.dot(&grad); 101 | 102 | m = beta_1 * m + (1.0 - beta_1) * grad; 103 | v = beta_2 * v + (1.0 - beta_2) * grad_squared; 104 | 105 | let m_hat = m / (1.0 - beta_1.powi(t)); 106 | let v_hat = v / (1.0 - beta_2.powi(t)); 107 | 108 | *weights -= learning_rate / (v_hat.sqrt() + epsilon) * m_hat; 109 | evaluator.update_weights(weights); 110 | error_fn.clear_batch(); 111 | current_batch_count += 1; 112 | if current_batch_count >= training_batch_count { 113 | break; 114 | } 115 | } 116 | 117 | let training_pos_count = error_fn.datapoint_count_epoch(); 118 | error_fn.clear(); 119 | for batch in iter_batch { 120 | for pos in batch { 121 | error_fn.add_datapoint( 122 | pos.outcome.into(), 123 | evaluator.eval(&pos.features), 124 | &pos.grad, 125 | ); 126 | } 127 | } 128 | let validation_pos_count = error_fn.datapoint_count_epoch(); 129 | let validation_error = error_fn.mean_squared_error_epoch(); 130 | println!( 131 | "Epoch {epoch}, trained with {training_pos_count} random positions, \ 132 | validated with {validation_pos_count} positions, validation error: {validation_error}", 133 | ); 134 | 135 | if epoch as usize % STORE_EVERY == 0 { 136 | let checkpoint = Checkpoint { 137 | params: AdamParams { 138 | epoch, 139 | m, 140 | v, 141 | t, 142 | ..params 143 | }, 144 | weights: *weights, 145 | training_error: None, 146 | validation_error: Some(validation_error), 147 | }; 148 | 149 | let serialized = serde_json::to_string(&checkpoint)?; 150 | let filename = format!("{weight_file_prefix}{:04}.json", epoch); 151 | let mut file = File::create(filename)?; 152 | file.write_all(serialized.as_bytes())?; 153 | } 154 | 155 | error_fn.clear(); 156 | } 157 | Ok(()) 158 | } 159 | -------------------------------------------------------------------------------- /tuner/src/feature_evaluator.rs: -------------------------------------------------------------------------------- 1 | use eval::params::{self, DISTANCE_LEN}; 2 | use nalgebra::SVector; 3 | 4 | use crate::position_features::{ 5 | EvalType, PositionFeatures, NUM_FEATURES, PST_SIZE, START_IDX_BACKWARD_PAWN, 6 | START_IDX_BISHOP_PAIR, START_IDX_DOUBLED_PAWN, START_IDX_ISOLATED_PAWN, START_IDX_KING_TROPISM, 7 | START_IDX_MOBILITY, START_IDX_PASSED_PAWN, START_IDX_PST, START_IDX_TEMPO, 8 | }; 9 | 10 | type Weight = f64; 11 | pub type WeightVector = SVector; 12 | 13 | #[derive(Debug, Clone)] 14 | pub struct FeatureEvaluator { 15 | weights: WeightVector, 16 | } 17 | 18 | impl Default for FeatureEvaluator { 19 | fn default() -> Self { 20 | Self::new() 21 | } 22 | } 23 | 24 | impl From<&WeightVector> for FeatureEvaluator { 25 | fn from(weights: &WeightVector) -> Self { 26 | Self { weights: *weights } 27 | } 28 | } 29 | 30 | impl FeatureEvaluator { 31 | pub fn new() -> Self { 32 | Self { 33 | weights: initialize_weights(), 34 | } 35 | } 36 | 37 | pub fn update_weights(&mut self, weights: &WeightVector) { 38 | for (w, new) in self.weights.iter_mut().zip(weights.iter()) { 39 | *w = *new; 40 | } 41 | } 42 | 43 | pub fn eval(&self, features: &PositionFeatures) -> EvalType { 44 | (&features.feature_vec * self.weights)[0] 45 | } 46 | } 47 | 48 | pub fn initialize_weights() -> WeightVector { 49 | let mut weights = WeightVector::from_element(0.0); 50 | 51 | let mut pst_idx = START_IDX_PST; 52 | for pst in [ 53 | params::PST_PAWN, 54 | params::PST_KNIGHT, 55 | params::PST_BISHOP, 56 | params::PST_ROOK, 57 | params::PST_QUEEN, 58 | params::PST_KING, 59 | ] { 60 | for square_idx in 0..PST_SIZE { 61 | weights[pst_idx + 2 * square_idx] = pst[square_idx].0.into(); 62 | weights[pst_idx + 2 * square_idx + 1] = pst[square_idx].1.into(); 63 | } 64 | pst_idx += 2 * PST_SIZE; 65 | } 66 | 67 | weights[START_IDX_TEMPO] = params::TEMPO.0.into(); 68 | weights[START_IDX_TEMPO + 1] = params::TEMPO.1.into(); 69 | 70 | weights[START_IDX_PASSED_PAWN] = params::PASSED_PAWN.0.into(); 71 | weights[START_IDX_PASSED_PAWN + 1] = params::PASSED_PAWN.1.into(); 72 | weights[START_IDX_ISOLATED_PAWN] = params::ISOLATED_PAWN.0.into(); 73 | weights[START_IDX_ISOLATED_PAWN + 1] = params::ISOLATED_PAWN.1.into(); 74 | weights[START_IDX_BACKWARD_PAWN] = params::BACKWARD_PAWN.0.into(); 75 | weights[START_IDX_BACKWARD_PAWN + 1] = params::BACKWARD_PAWN.1.into(); 76 | weights[START_IDX_DOUBLED_PAWN] = params::DOUBLED_PAWN.0.into(); 77 | weights[START_IDX_DOUBLED_PAWN + 1] = params::DOUBLED_PAWN.1.into(); 78 | 79 | initialize_mobility(&mut weights); 80 | 81 | weights[START_IDX_BISHOP_PAIR] = params::BISHOP_PAIR.0.into(); 82 | weights[START_IDX_BISHOP_PAIR + 1] = params::BISHOP_PAIR.1.into(); 83 | 84 | let mut king_tropism_idx = START_IDX_KING_TROPISM; 85 | for distance in [ 86 | params::DISTANCE_FRIENDLY_PAWN, 87 | params::DISTANCE_ENEMY_PAWN, 88 | params::DISTANCE_FRIENDLY_KNIGHT, 89 | params::DISTANCE_ENEMY_KNIGHT, 90 | params::DISTANCE_FRIENDLY_BISHOP, 91 | params::DISTANCE_ENEMY_BISHOP, 92 | params::DISTANCE_FRIENDLY_ROOK, 93 | params::DISTANCE_ENEMY_ROOK, 94 | params::DISTANCE_FRIENDLY_QUEEN, 95 | params::DISTANCE_ENEMY_QUEEN, 96 | params::DISTANCE_FRIENDLY_KING, 97 | params::DISTANCE_ENEMY_KING, 98 | ] { 99 | for distance_idx in 0..DISTANCE_LEN { 100 | weights[king_tropism_idx + 2 * distance_idx] = distance[distance_idx].0.into(); 101 | weights[king_tropism_idx + 2 * distance_idx + 1] = distance[distance_idx].1.into(); 102 | } 103 | king_tropism_idx += 2 * DISTANCE_LEN; 104 | } 105 | 106 | weights 107 | } 108 | 109 | pub fn initialize_mobility(weights: &mut WeightVector) { 110 | let mut idx = START_IDX_MOBILITY; 111 | for mob in params::MOBILITY_KNIGHT { 112 | weights[idx] = mob.0.into(); 113 | weights[idx + 1] = mob.1.into(); 114 | idx += 2; 115 | } 116 | for mob in params::MOBILITY_BISHOP { 117 | weights[idx] = mob.0.into(); 118 | weights[idx + 1] = mob.1.into(); 119 | idx += 2; 120 | } 121 | for mob in params::MOBILITY_ROOK { 122 | weights[idx] = mob.0.into(); 123 | weights[idx + 1] = mob.1.into(); 124 | idx += 2; 125 | } 126 | for mob in params::MOBILITY_QUEEN { 127 | weights[idx] = mob.0.into(); 128 | weights[idx + 1] = mob.1.into(); 129 | idx += 2; 130 | } 131 | } 132 | 133 | #[cfg(test)] 134 | mod tests { 135 | use eval::{complex::Complex, Eval}; 136 | use movegen::fen::Fen; 137 | 138 | use crate::{feature_evaluator::EvalType, position_features::PositionFeatures}; 139 | 140 | use super::FeatureEvaluator; 141 | 142 | #[test] 143 | fn tuner_eval_matches_actual_eval() { 144 | let fens = ["8/6pk/5p2/P1R4P/1P5P/5K2/2P5/6r1 b - - 2 70"]; 145 | 146 | let mut evaluator = Complex::new(); 147 | let feature_evaluator = FeatureEvaluator::new(); 148 | 149 | for fen in fens { 150 | let pos = Fen::str_to_pos(fen).unwrap(); 151 | let exp_eval = evaluator.eval(&pos); 152 | let features = PositionFeatures::from(&pos); 153 | let act_eval = feature_evaluator.eval(&features); 154 | // There may be small differences in the evaluations: 155 | // - The position evaluator rounds down the result, the feature evaluator doesn't 156 | // - Rounding errors 157 | assert!( 158 | ((exp_eval as EvalType) - act_eval).abs() < 1.0, 159 | "Evaluations don't match\nExpected: {exp_eval}\nActual: {act_eval}", 160 | ); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /movegen/src/move_generator/king_in_check_generator.rs: -------------------------------------------------------------------------------- 1 | use crate::move_generator::move_generator_template::MoveGeneratorTemplate; 2 | 3 | use crate::attacks_to::AttacksTo; 4 | use crate::bitboard::Bitboard; 5 | use crate::pawn::Pawn; 6 | use crate::r#move::MoveList; 7 | use crate::square::Square; 8 | 9 | // In check, the only legal moves are: 10 | // - Move the king to safety 11 | // - Capture the attacker 12 | // - Block the attack (only if the attacker is a sliding piece) 13 | // In double check, only king moves are legal 14 | pub struct KingInCheckGenerator; 15 | 16 | impl MoveGeneratorTemplate for KingInCheckGenerator { 17 | fn non_capture_target_filter(attacks_to_king: &AttacksTo, targets: Bitboard) -> Bitboard { 18 | debug_assert_eq!(1, attacks_to_king.attack_origins.pop_count()); 19 | // Blocking is only possible, if the king is attacked by a sliding piece. 20 | // Otherwise, the king must move or the attacker must be captured. 21 | match attacks_to_king.each_slider_attack.first() { 22 | Some(slider) => targets & slider.targets() & !attacks_to_king.pos.occupancy(), 23 | None => Bitboard::EMPTY, 24 | } 25 | } 26 | 27 | fn capture_target_filter(attacks_to_king: &AttacksTo, targets: Bitboard) -> Bitboard { 28 | debug_assert_eq!(1, attacks_to_king.attack_origins.pop_count()); 29 | targets & attacks_to_king.attack_origins 30 | } 31 | 32 | fn pawn_capture_target_filter(attacks_to_king: &AttacksTo, targets: Bitboard) -> Bitboard { 33 | targets & (attacks_to_king.attack_origins | attacks_to_king.pos.en_passant_square()) 34 | } 35 | 36 | fn is_legal_non_capture(attacks_to_king: &AttacksTo, origin: Square, target: Square) -> bool { 37 | let pos = attacks_to_king.pos; 38 | let origin_bb = Bitboard::from(origin); 39 | let target_bb = Bitboard::from(target); 40 | let occupancy_after_move = pos.occupancy() & !origin_bb | target_bb; 41 | let own_king = Bitboard::from(attacks_to_king.target); 42 | let king_in_check_after_move = attacks_to_king 43 | .each_xray 44 | .iter() 45 | .map(|x| Self::sliding_piece_targets(x, occupancy_after_move)) 46 | .any(|x| x & own_king != Bitboard::EMPTY); 47 | !king_in_check_after_move 48 | } 49 | 50 | fn is_legal_capture(attacks_to_king: &AttacksTo, origin: Square, target: Square) -> bool { 51 | let pos = attacks_to_king.pos; 52 | let origin_bb = Bitboard::from(origin); 53 | let occupancy_after_move = pos.occupancy() & !origin_bb; 54 | let own_king = Bitboard::from(attacks_to_king.target); 55 | let king_in_check_after_move = attacks_to_king 56 | .each_xray 57 | .iter() 58 | .filter(|x| (x.origin() != target) && (x.targets() & origin_bb != Bitboard::EMPTY)) 59 | .map(|x| Self::sliding_piece_targets(x, occupancy_after_move)) 60 | .any(|x| x & own_king != Bitboard::EMPTY); 61 | !king_in_check_after_move 62 | } 63 | 64 | fn is_legal_en_passant_capture( 65 | attacks_to_king: &AttacksTo, 66 | origin: Square, 67 | target: Square, 68 | ) -> bool { 69 | let pos = attacks_to_king.pos; 70 | let origin_bb = Bitboard::from(origin); 71 | let target_bb = Bitboard::from(target); 72 | let captured_square = Pawn::push_origin(target, pos.side_to_move()); 73 | let captured_bb = Bitboard::from(captured_square); 74 | if captured_bb == attacks_to_king.attack_origins { 75 | // Our king is attacked by the pawn that just moved. In this case we must check if our 76 | // own pawn is pinned to the king. The opponent's pawn is not blocking a sliding attack 77 | // (otherwise our king would already have been attacked before the opponent's move 78 | // which would be an illegal position). 79 | let own_king = Bitboard::from(attacks_to_king.target); 80 | let occupancy_after_move = pos.occupancy() & !origin_bb & !captured_bb | target_bb; 81 | let king_in_check_after_move = attacks_to_king 82 | .each_xray 83 | .iter() 84 | .filter(|x| x.targets() & origin_bb != Bitboard::EMPTY) 85 | .map(|x| Self::sliding_piece_targets(x, occupancy_after_move)) 86 | .any(|x| x & own_king != Bitboard::EMPTY); 87 | !king_in_check_after_move 88 | } else { 89 | // Our king is attacked by a sliding piece (after a discovered attack). Capturing en 90 | // passant is not possible because the attacker will not be blocked. 91 | false 92 | } 93 | } 94 | 95 | fn is_legal_king_move(attacks_to_king: &AttacksTo, origin: Square, target: Square) -> bool { 96 | debug_assert_ne!(Bitboard::EMPTY, attacks_to_king.attack_origins); 97 | let pos = attacks_to_king.pos; 98 | let target_bb = Bitboard::from(target); 99 | match target_bb & attacks_to_king.xrays_to_target { 100 | Bitboard::EMPTY => true, 101 | _ => { 102 | let origin_bb = Bitboard::from(origin); 103 | let occupancy_after_move = pos.occupancy() & !origin_bb | target_bb; 104 | let king_in_check_after_move = attacks_to_king.each_slider_attack.iter().any(|x| { 105 | (x.targets() & origin_bb != Bitboard::EMPTY) 106 | && (Self::sliding_piece_targets(x, occupancy_after_move) & target_bb) 107 | != Bitboard::EMPTY 108 | }); 109 | !king_in_check_after_move 110 | } 111 | } 112 | } 113 | 114 | // Castling is illegal while in check 115 | fn generate_castles(_move_list: &mut MoveList, _attacks_to_king: &AttacksTo) {} 116 | fn generate_white_castles(_move_list: &mut MoveList, _attacks_to_king: &AttacksTo) {} 117 | fn generate_black_castles(_move_list: &mut MoveList, _attacks_to_king: &AttacksTo) {} 118 | } 119 | -------------------------------------------------------------------------------- /eval/src/pawn_structure.rs: -------------------------------------------------------------------------------- 1 | use crate::params; 2 | use crate::score_pair::ScorePair; 3 | 4 | use movegen::bitboard::Bitboard; 5 | use movegen::pawn::Pawn; 6 | use movegen::piece; 7 | use movegen::position::Position; 8 | use movegen::side::Side; 9 | use movegen::square::Square; 10 | 11 | #[derive(Debug, Clone)] 12 | pub struct PawnStructure { 13 | current_pos: Position, 14 | scores: ScorePair, 15 | } 16 | 17 | impl Default for PawnStructure { 18 | fn default() -> Self { 19 | Self::new() 20 | } 21 | } 22 | 23 | impl PawnStructure { 24 | pub fn new() -> Self { 25 | Self { 26 | current_pos: Position::empty(), 27 | scores: ScorePair(0, 0), 28 | } 29 | } 30 | 31 | pub fn scores(&self) -> ScorePair { 32 | self.scores 33 | } 34 | 35 | pub fn update(&mut self, pos: &Position) { 36 | let old_white_pawns = self 37 | .current_pos 38 | .piece_occupancy(Side::White, piece::Type::Pawn); 39 | let new_white_pawns = pos.piece_occupancy(Side::White, piece::Type::Pawn); 40 | let old_black_pawns = self 41 | .current_pos 42 | .piece_occupancy(Side::Black, piece::Type::Pawn); 43 | let new_black_pawns = pos.piece_occupancy(Side::Black, piece::Type::Pawn); 44 | 45 | if old_white_pawns != new_white_pawns || old_black_pawns != new_black_pawns { 46 | let white_pawns = pos.piece_occupancy(Side::White, piece::Type::Pawn); 47 | let black_pawns = pos.piece_occupancy(Side::Black, piece::Type::Pawn); 48 | let passed_pawn_score = 49 | Self::passed_pawn_count(white_pawns, black_pawns) as i16 * params::PASSED_PAWN; 50 | let isolated_pawn_score = 51 | Self::isolated_pawn_count(white_pawns, black_pawns) as i16 * params::ISOLATED_PAWN; 52 | let backward_pawn_score = 53 | Self::backward_pawn_count(white_pawns, black_pawns) as i16 * params::BACKWARD_PAWN; 54 | let doubled_pawn_score = 55 | Self::doubled_pawn_count(white_pawns, black_pawns) as i16 * params::DOUBLED_PAWN; 56 | self.scores = 57 | passed_pawn_score + isolated_pawn_score + backward_pawn_score + doubled_pawn_score; 58 | self.current_pos = pos.clone(); 59 | } 60 | } 61 | 62 | pub fn passed_pawn_count(white_pawns: Bitboard, black_pawns: Bitboard) -> i8 { 63 | Self::passed_pawn_count_one_side(white_pawns, black_pawns, Side::White) 64 | - Self::passed_pawn_count_one_side(black_pawns, white_pawns, Side::Black) 65 | } 66 | 67 | pub fn isolated_pawn_count(white_pawns: Bitboard, black_pawns: Bitboard) -> i8 { 68 | Self::isolated_pawn_count_one_side(white_pawns) 69 | - Self::isolated_pawn_count_one_side(black_pawns) 70 | } 71 | 72 | pub fn backward_pawn_count(white_pawns: Bitboard, black_pawns: Bitboard) -> i8 { 73 | Self::backward_pawn_count_one_side(white_pawns, black_pawns, Side::White) 74 | - Self::backward_pawn_count_one_side(black_pawns, white_pawns, Side::Black) 75 | } 76 | 77 | pub fn doubled_pawn_count(white_pawns: Bitboard, black_pawns: Bitboard) -> i8 { 78 | let mut doubled_pawn_count = 0; 79 | for file in [ 80 | Bitboard::FILE_A, 81 | Bitboard::FILE_B, 82 | Bitboard::FILE_C, 83 | Bitboard::FILE_D, 84 | Bitboard::FILE_E, 85 | Bitboard::FILE_F, 86 | Bitboard::FILE_G, 87 | Bitboard::FILE_H, 88 | ] { 89 | let white_pawns_on_file = (white_pawns & file).pop_count() as i8; 90 | doubled_pawn_count += std::cmp::max(0, white_pawns_on_file - 1); 91 | let black_pawns_on_file = (black_pawns & file).pop_count() as i8; 92 | doubled_pawn_count -= std::cmp::max(0, black_pawns_on_file - 1); 93 | } 94 | doubled_pawn_count 95 | } 96 | 97 | fn passed_pawn_count_one_side( 98 | own_pawns: Bitboard, 99 | opp_pawns: Bitboard, 100 | side_to_move: Side, 101 | ) -> i8 { 102 | let all_pawns = own_pawns | opp_pawns; 103 | let opp_pawn_attack_targets = Pawn::attack_targets(opp_pawns, !side_to_move); 104 | 105 | let mut passed_count = 0; 106 | let mut own_pawns_mut = own_pawns; 107 | while own_pawns_mut != Bitboard::EMPTY { 108 | let pawn = own_pawns_mut.square_scan_forward_reset(); 109 | passed_count += 110 | Self::is_passed(all_pawns, opp_pawn_attack_targets, pawn, side_to_move) as i8; 111 | } 112 | passed_count 113 | } 114 | 115 | fn is_passed( 116 | all_pawns: Bitboard, 117 | opp_pawn_attack_targets: Bitboard, 118 | pawn: Square, 119 | side_to_move: Side, 120 | ) -> bool { 121 | let pawn_bb = Bitboard::from(pawn); 122 | Pawn::front_span(pawn_bb, side_to_move) & (all_pawns) == Bitboard::EMPTY 123 | && Pawn::front_fill(pawn_bb, side_to_move) & opp_pawn_attack_targets == Bitboard::EMPTY 124 | } 125 | 126 | fn isolated_pawn_count_one_side(own_pawns: Bitboard) -> i8 { 127 | let mut isolated_count = 0; 128 | let mut own_pawns_mut = own_pawns; 129 | while own_pawns_mut != Bitboard::EMPTY { 130 | let pawn = own_pawns_mut.square_scan_forward_reset(); 131 | isolated_count += Self::is_isolated(own_pawns, pawn) as i8; 132 | } 133 | isolated_count 134 | } 135 | 136 | fn is_isolated(own_pawns: Bitboard, pawn: Square) -> bool { 137 | let pawn_file = Bitboard::from(pawn).file_fill(); 138 | let adjacent_files = pawn_file.east_one() | pawn_file.west_one(); 139 | adjacent_files & own_pawns == Bitboard::EMPTY 140 | } 141 | 142 | fn backward_pawn_count_one_side( 143 | own_pawns: Bitboard, 144 | opp_pawns: Bitboard, 145 | side_to_move: Side, 146 | ) -> i8 { 147 | let own_pawn_stops = Pawn::push_targets(own_pawns, Bitboard::EMPTY, side_to_move).0; 148 | let own_front_attack_span = Pawn::front_attack_span(own_pawns, side_to_move); 149 | let opp_attack_targets = Pawn::attack_targets(opp_pawns, !side_to_move); 150 | let backward_pawn_targets = own_pawn_stops & !own_front_attack_span & opp_attack_targets; 151 | let backward_pawns = Pawn::single_push_origins(backward_pawn_targets, side_to_move); 152 | backward_pawns.pop_count() as i8 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /engine/src/engine.rs: -------------------------------------------------------------------------------- 1 | use crate::best_move_handler::{BestMoveCommand, BestMoveHandler, StopReason}; 2 | use crate::engine_out::EngineOut; 3 | use crate::{EngineOptions, Variant}; 4 | use crossbeam_channel::{unbounded, Sender}; 5 | use movegen::position::Position; 6 | use movegen::position_history::PositionHistory; 7 | use movegen::side::Side; 8 | use search::search::{Search, SearchInfo}; 9 | use search::search_params::SearchParamsOptions; 10 | use search::searcher::Searcher; 11 | use search::SearchOptions; 12 | use std::sync::{Arc, Mutex}; 13 | use std::time::Duration; 14 | 15 | #[derive(Debug, thiserror::Error)] 16 | pub enum EngineError { 17 | #[error("Engine error: Cannot search without a position")] 18 | SearchWithoutPosition, 19 | } 20 | 21 | pub struct Engine { 22 | searcher: Searcher, 23 | pos_hist: Option, 24 | best_move_handler: BestMoveHandler, 25 | best_move_sender: Sender, 26 | engine_options: Arc>, 27 | } 28 | 29 | impl Engine { 30 | pub fn new( 31 | search_algo: impl Search + Send + 'static, 32 | engine_out: impl EngineOut + Send + 'static, 33 | engine_options: Arc>, 34 | ) -> Self { 35 | let (best_move_sender, best_move_receiver) = unbounded(); 36 | let best_move_handler = BestMoveHandler::new(best_move_receiver, engine_out); 37 | let best_move_sender_clone = best_move_sender.clone(); 38 | 39 | let search_info_callback = Box::new(move |info| match info { 40 | SearchInfo::DepthFinished(res) => { 41 | let _ = best_move_sender_clone.send(BestMoveCommand::DepthFinished(res.clone())); 42 | } 43 | SearchInfo::Stopped(best_move) => { 44 | let _ = best_move_sender_clone 45 | .send(BestMoveCommand::Stop(StopReason::Finished(best_move))); 46 | } 47 | SearchInfo::Terminated => {} 48 | }); 49 | 50 | let searcher = Searcher::new(search_algo, search_info_callback); 51 | 52 | Self { 53 | searcher, 54 | pos_hist: None, 55 | best_move_handler, 56 | best_move_sender, 57 | engine_options, 58 | } 59 | } 60 | 61 | pub fn hash_size(&self) -> usize { 62 | match self.engine_options.lock() { 63 | Ok(opt) => opt.hash_size, 64 | Err(e) => panic!("{}", e), 65 | } 66 | } 67 | 68 | pub fn set_hash_size(&self, bytes: usize) { 69 | match self.engine_options.lock() { 70 | Ok(mut opt) => opt.hash_size = bytes, 71 | Err(e) => panic!("{}", e), 72 | } 73 | self.searcher.set_hash_size(bytes); 74 | } 75 | 76 | pub fn move_overhead(&self) -> Duration { 77 | match self.engine_options.lock() { 78 | Ok(opt) => opt.move_overhead, 79 | Err(e) => panic!("{}", e), 80 | } 81 | } 82 | 83 | pub fn set_move_overhead(&mut self, move_overhead: Duration) { 84 | match self.engine_options.lock() { 85 | Ok(mut opt) => opt.move_overhead = move_overhead, 86 | Err(e) => panic!("{}", e), 87 | }; 88 | } 89 | 90 | pub fn set_search_params(&mut self, search_params: SearchParamsOptions) { 91 | self.searcher.set_search_params(search_params); 92 | } 93 | 94 | pub fn variant(&self) -> Variant { 95 | match self.engine_options.lock() { 96 | Ok(opt) => opt.variant, 97 | Err(e) => panic!("{}", e), 98 | } 99 | } 100 | 101 | pub fn set_variant(&self, variant: Variant) { 102 | match self.engine_options.lock() { 103 | Ok(mut opt) => opt.variant = variant, 104 | Err(e) => panic!("{}", e), 105 | }; 106 | } 107 | 108 | pub fn set_position_history(&mut self, pos_hist: Option) { 109 | self.pos_hist = pos_hist; 110 | } 111 | 112 | pub fn clear_position_history(&mut self) { 113 | self.pos_hist = None; 114 | self.searcher.clear_hash_table(); 115 | } 116 | 117 | pub fn search(&mut self, options: SearchOptions) -> Result<(), EngineError> { 118 | let mut search_options = options.clone(); 119 | search_options.move_overhead = self.move_overhead(); 120 | self.clear_best_move(); 121 | self.set_search_options(options); 122 | self.search_with_options(search_options)?; 123 | Ok(()) 124 | } 125 | 126 | pub fn stop(&self) { 127 | self.stop_best_move_handler(); 128 | self.searcher.stop(); 129 | } 130 | 131 | pub fn position(&self) -> Option<&Position> { 132 | self.pos_hist 133 | .as_ref() 134 | .map(|pos_hist| pos_hist.current_pos()) 135 | } 136 | 137 | fn search_with_options(&mut self, search_options: SearchOptions) -> Result<(), EngineError> { 138 | match &self.pos_hist { 139 | Some(pos_hist) => { 140 | self.set_side_to_move(Some(pos_hist.current_pos().side_to_move())); 141 | self.searcher.search(pos_hist.clone(), search_options); 142 | Ok(()) 143 | } 144 | None => Err(EngineError::SearchWithoutPosition), 145 | } 146 | } 147 | 148 | fn clear_best_move(&self) { 149 | self.searcher.stop(); 150 | } 151 | 152 | fn set_search_options(&self, options: SearchOptions) { 153 | self.best_move_sender 154 | .send(BestMoveCommand::SetOptions(Box::new(options))) 155 | .expect("Error sending BestMoveCommand"); 156 | } 157 | 158 | fn set_side_to_move(&self, side: Option) { 159 | self.best_move_sender 160 | .send(BestMoveCommand::SetSideToMove(side)) 161 | .expect("Error sending BestMoveCommand"); 162 | } 163 | 164 | fn stop_best_move_handler(&self) { 165 | self.best_move_sender 166 | .send(BestMoveCommand::Stop(StopReason::Command)) 167 | .expect("Error sending BestMoveCommand"); 168 | } 169 | } 170 | 171 | impl Drop for Engine { 172 | fn drop(&mut self) { 173 | self.best_move_sender 174 | .send(BestMoveCommand::Terminate) 175 | .expect("Error sending BestMoveCommand"); 176 | if let Some(thread) = self.best_move_handler.thread.take() { 177 | thread 178 | .join() 179 | .expect("Error joining best move handler thread"); 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /search/src/searcher.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | search::{Search, SearchCommand, SearchInfo}, 3 | search_params::SearchParamsOptions, 4 | SearchOptions, 5 | }; 6 | use crossbeam_channel::{bounded, unbounded, Receiver, Sender}; 7 | use movegen::position_history::PositionHistory; 8 | 9 | use std::thread; 10 | 11 | pub struct Searcher { 12 | command_sender: Sender, 13 | info_sender: Sender, 14 | worker: Worker, 15 | search_info_handler: SearchInfoHandler, 16 | } 17 | 18 | impl Searcher { 19 | pub fn set_hash_size(&self, bytes: usize) { 20 | let (sender, receiver) = bounded(1); 21 | self.command_sender 22 | .send(SearchCommand::SetHashSize(bytes, sender)) 23 | .expect("Error sending SearchCommand"); 24 | receiver 25 | .recv() 26 | .expect_err("Expected sender to disconnect after SetHashSize"); 27 | } 28 | 29 | pub fn clear_hash_table(&self) { 30 | let (sender, receiver) = bounded(1); 31 | self.command_sender 32 | .send(SearchCommand::ClearHashTable(sender)) 33 | .expect("Error sending SearchCommand"); 34 | receiver 35 | .recv() 36 | .expect_err("Expected sender to disconnect after ClearHashTable"); 37 | } 38 | 39 | pub fn set_search_params(&self, search_params: SearchParamsOptions) { 40 | let (sender, receiver) = bounded(1); 41 | self.command_sender 42 | .send(SearchCommand::SetSearchParams(search_params, sender)) 43 | .expect("Error sending SearchCommand"); 44 | receiver 45 | .recv() 46 | .expect_err("Expected sender to disconnect after SetSearchParams"); 47 | } 48 | 49 | pub fn search(&self, pos_hist: PositionHistory, search_options: SearchOptions) { 50 | self.stop(); 51 | self.command_sender 52 | .send(SearchCommand::Search(Box::new((pos_hist, search_options)))) 53 | .expect("Error sending SearchCommand"); 54 | } 55 | 56 | pub fn stop(&self) { 57 | self.command_sender 58 | .send(SearchCommand::Stop) 59 | .expect("Error sending SearchCommand"); 60 | } 61 | 62 | pub fn clone_command_sender(&self) -> Sender { 63 | self.command_sender.clone() 64 | } 65 | 66 | pub fn new( 67 | search_algo: impl Search + Send + 'static, 68 | info_callback: Box, 69 | ) -> Self { 70 | let (command_sender, command_receiver) = unbounded(); 71 | let (info_sender, info_receiver) = unbounded(); 72 | 73 | let worker = Worker::new(search_algo, command_receiver, info_sender.clone()); 74 | let search_info_handler = SearchInfoHandler::new(info_receiver, info_callback); 75 | 76 | Self { 77 | command_sender, 78 | info_sender, 79 | worker, 80 | search_info_handler, 81 | } 82 | } 83 | } 84 | 85 | impl Drop for Searcher { 86 | fn drop(&mut self) { 87 | self.command_sender 88 | .send(SearchCommand::Stop) 89 | .expect("Error sending SearchCommand"); 90 | self.command_sender 91 | .send(SearchCommand::Terminate) 92 | .expect("Error sending SearchCommand"); 93 | if let Some(thread) = self.worker.thread.take() { 94 | thread.join().expect("Error joining search thread"); 95 | } 96 | 97 | self.info_sender 98 | .send(SearchInfo::Terminated) 99 | .expect("Error sending SearchInfo"); 100 | if let Some(thread) = self.search_info_handler.thread.take() { 101 | thread.join().expect("Error joining SearchInfoHandler"); 102 | } 103 | } 104 | } 105 | 106 | struct Worker { 107 | thread: Option>, 108 | } 109 | 110 | impl Worker { 111 | fn new( 112 | mut search_algo: impl Search + Send + 'static, 113 | mut command_receiver: Receiver, 114 | mut info_sender: Sender, 115 | ) -> Self { 116 | let thread = thread::spawn(move || loop { 117 | let message = command_receiver 118 | .recv() 119 | .expect("Error receiving SearchCommand"); 120 | 121 | match message { 122 | SearchCommand::SetHashSize(bytes, _sender) => { 123 | Self::set_hash_size(&mut search_algo, bytes); 124 | } 125 | SearchCommand::ClearHashTable(_sender) => { 126 | Self::clear_hash_table(&mut search_algo); 127 | } 128 | SearchCommand::SetSearchParams(search_params, _sender) => { 129 | Self::set_search_params(&mut search_algo, search_params); 130 | } 131 | SearchCommand::Search(inner) => { 132 | let (pos_hist, search_options) = *inner; 133 | Self::search( 134 | &mut search_algo, 135 | pos_hist, 136 | search_options, 137 | &mut command_receiver, 138 | &mut info_sender, 139 | ); 140 | } 141 | SearchCommand::Stop => {} 142 | SearchCommand::Terminate => break, 143 | } 144 | }); 145 | Self { 146 | thread: Some(thread), 147 | } 148 | } 149 | 150 | fn set_hash_size(search: &mut impl Search, bytes: usize) { 151 | search.set_hash_size(bytes); 152 | } 153 | 154 | fn clear_hash_table(search: &mut impl Search) { 155 | search.clear_hash_table(); 156 | } 157 | 158 | fn set_search_params(search: &mut impl Search, search_params: SearchParamsOptions) { 159 | search.set_params(search_params); 160 | } 161 | 162 | fn search( 163 | search: &mut impl Search, 164 | pos_hist: PositionHistory, 165 | search_options: SearchOptions, 166 | command_receiver: &mut Receiver, 167 | info_sender: &mut Sender, 168 | ) { 169 | search.search(pos_hist, search_options, command_receiver, info_sender); 170 | } 171 | } 172 | 173 | struct SearchInfoHandler { 174 | thread: Option>, 175 | } 176 | 177 | impl SearchInfoHandler { 178 | fn new( 179 | info_receiver: Receiver, 180 | mut info_callback: Box, 181 | ) -> Self { 182 | let thread = thread::spawn(move || loop { 183 | match info_receiver.recv() { 184 | Ok(SearchInfo::Terminated) => break, 185 | Ok(res) => info_callback(res), 186 | Err(_) => break, 187 | } 188 | }); 189 | Self { 190 | thread: Some(thread), 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /movegen/src/attacks_to.rs: -------------------------------------------------------------------------------- 1 | use crate::bishop::Bishop; 2 | use crate::king::King; 3 | use crate::knight::Knight; 4 | use crate::pawn::Pawn; 5 | use crate::queen::Queen; 6 | use crate::rook::Rook; 7 | 8 | use crate::bitboard::Bitboard; 9 | use crate::piece; 10 | use crate::piece_targets::PieceTargets; 11 | use crate::position::Position; 12 | use crate::side::Side; 13 | use crate::square::Square; 14 | 15 | pub struct AttacksTo<'a> { 16 | pub pos: &'a Position, 17 | pub target: Square, 18 | pub all_attack_targets: Bitboard, 19 | pub attack_origins: Bitboard, 20 | pub each_slider_attack: Vec, 21 | pub xrays_to_target: Bitboard, 22 | pub each_xray: Vec, 23 | } 24 | 25 | impl AttacksTo<'_> { 26 | pub fn new(pos: &Position, target: Square, attacking_side: Side) -> AttacksTo { 27 | let (all_pawn_targets, pawn_origins) = 28 | Self::pawn_attacks_towards_target(pos, target, attacking_side); 29 | let (all_knight_targets, knight_origins) = Self::attacks_towards_target( 30 | pos, 31 | piece::Type::Knight, 32 | &Knight::targets, 33 | target, 34 | attacking_side, 35 | ); 36 | let ( 37 | all_bishop_targets, 38 | bishop_origins, 39 | mut each_bishop_attack, 40 | bishop_xrays, 41 | mut each_bishop_xray, 42 | ) = Self::slider_attacks_towards_target( 43 | pos, 44 | piece::Type::Bishop, 45 | &Bishop::targets, 46 | target, 47 | attacking_side, 48 | ); 49 | let (all_rook_targets, rook_origins, mut each_rook_attack, rook_xrays, mut each_rook_xray) = 50 | Self::slider_attacks_towards_target( 51 | pos, 52 | piece::Type::Rook, 53 | &Rook::targets, 54 | target, 55 | attacking_side, 56 | ); 57 | let ( 58 | all_queen_targets, 59 | queen_origins, 60 | mut each_queen_attack, 61 | queen_xrays, 62 | mut each_queen_xray, 63 | ) = Self::slider_attacks_towards_target( 64 | pos, 65 | piece::Type::Queen, 66 | &Queen::targets, 67 | target, 68 | attacking_side, 69 | ); 70 | let (all_king_targets, king_origins) = Self::attacks_towards_target( 71 | pos, 72 | piece::Type::King, 73 | &King::targets, 74 | target, 75 | attacking_side, 76 | ); 77 | 78 | let mut each_slider_attack = Vec::new(); 79 | each_slider_attack.append(&mut each_bishop_attack); 80 | each_slider_attack.append(&mut each_rook_attack); 81 | each_slider_attack.append(&mut each_queen_attack); 82 | 83 | let all_attack_targets = all_pawn_targets 84 | | all_knight_targets 85 | | all_bishop_targets 86 | | all_rook_targets 87 | | all_queen_targets 88 | | all_king_targets; 89 | let attack_origins = pawn_origins 90 | | knight_origins 91 | | bishop_origins 92 | | rook_origins 93 | | queen_origins 94 | | king_origins; 95 | 96 | let xrays_to_target = bishop_xrays | rook_xrays | queen_xrays; 97 | let mut each_xray = Vec::new(); 98 | each_xray.append(&mut each_bishop_xray); 99 | each_xray.append(&mut each_rook_xray); 100 | each_xray.append(&mut each_queen_xray); 101 | 102 | AttacksTo { 103 | pos, 104 | target, 105 | all_attack_targets, 106 | attack_origins, 107 | each_slider_attack, 108 | xrays_to_target, 109 | each_xray, 110 | } 111 | } 112 | 113 | fn pawn_attacks_towards_target( 114 | pos: &Position, 115 | target: Square, 116 | attacking_side: Side, 117 | ) -> (Bitboard, Bitboard) { 118 | let target_bb = Bitboard::from(target); 119 | let pawns = pos.piece_occupancy(attacking_side, piece::Type::Pawn); 120 | 121 | let east_targets = Pawn::east_attack_targets(pawns, attacking_side); 122 | let east_origins = Pawn::east_attack_origins(east_targets & target_bb, attacking_side); 123 | let west_targets = Pawn::west_attack_targets(pawns, attacking_side); 124 | let west_origins = Pawn::west_attack_origins(west_targets & target_bb, attacking_side); 125 | 126 | let all_attack_targets = east_targets | west_targets; 127 | let attack_origins = east_origins | west_origins; 128 | 129 | (all_attack_targets, attack_origins) 130 | } 131 | 132 | fn attacks_towards_target( 133 | pos: &Position, 134 | piece_type: piece::Type, 135 | piece_targets: &impl Fn(Square) -> Bitboard, 136 | target: Square, 137 | attacking_side: Side, 138 | ) -> (Bitboard, Bitboard) { 139 | let target_bb = Bitboard::from(target); 140 | let mut pieces = pos.piece_occupancy(attacking_side, piece_type); 141 | let mut all_attack_targets = Bitboard::EMPTY; 142 | let mut attack_origins = Bitboard::EMPTY; 143 | while pieces != Bitboard::EMPTY { 144 | let attack_origin = pieces.square_scan_forward_reset(); 145 | let attack_targets = piece_targets(attack_origin); 146 | all_attack_targets |= attack_targets; 147 | if attack_targets & target_bb != Bitboard::EMPTY { 148 | attack_origins |= Bitboard::from(attack_origin); 149 | } 150 | } 151 | 152 | (all_attack_targets, attack_origins) 153 | } 154 | 155 | fn slider_attacks_towards_target( 156 | pos: &Position, 157 | piece_type: piece::Type, 158 | piece_targets: &impl Fn(Square, Bitboard) -> Bitboard, 159 | target: Square, 160 | attacking_side: Side, 161 | ) -> ( 162 | Bitboard, 163 | Bitboard, 164 | Vec, 165 | Bitboard, 166 | Vec, 167 | ) { 168 | let target_bb = Bitboard::from(target); 169 | let mut pieces = pos.piece_occupancy(attacking_side, piece_type); 170 | let mut all_attack_targets = Bitboard::EMPTY; 171 | let mut attack_origins = Bitboard::EMPTY; 172 | let mut each_slider_attack = Vec::new(); 173 | let mut xrays_to_target = Bitboard::EMPTY; 174 | let mut each_xray = Vec::new(); 175 | while pieces != Bitboard::EMPTY { 176 | let attack_origin = pieces.square_scan_forward_reset(); 177 | let attack_targets = piece_targets(attack_origin, pos.occupancy()); 178 | all_attack_targets |= attack_targets; 179 | if attack_targets & target_bb != Bitboard::EMPTY { 180 | attack_origins |= Bitboard::from(attack_origin); 181 | each_slider_attack.push(PieceTargets::new( 182 | piece::Piece::new(attacking_side, piece_type), 183 | attack_origin, 184 | attack_targets, 185 | )); 186 | } 187 | let xray_targets = piece_targets(attack_origin, Bitboard::EMPTY); 188 | if xray_targets & target_bb != Bitboard::EMPTY { 189 | xrays_to_target |= xray_targets; 190 | each_xray.push(PieceTargets::new( 191 | piece::Piece::new(attacking_side, piece_type), 192 | attack_origin, 193 | xray_targets, 194 | )); 195 | } 196 | } 197 | 198 | ( 199 | all_attack_targets, 200 | attack_origins, 201 | each_slider_attack, 202 | xrays_to_target, 203 | each_xray, 204 | ) 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /uci/src/uci_out.rs: -------------------------------------------------------------------------------- 1 | use crate::uci_move::UciMove; 2 | use crate::uci_option::{OptionType, UciOption, OPTIONS}; 3 | use crate::uci_score::UciScore; 4 | use engine::{EngineOptions, EngineOut, Variant}; 5 | use movegen::r#move::Move; 6 | use search::search::SearchResult; 7 | use std::error::Error; 8 | use std::io::Write; 9 | use std::sync::{Arc, Mutex}; 10 | 11 | struct UciOutInner { 12 | writer: Box, 13 | engine_version: String, 14 | engine_options: Arc>, 15 | debug: bool, 16 | } 17 | 18 | #[derive(Clone)] 19 | pub struct UciOut { 20 | inner: Arc>, 21 | } 22 | 23 | impl EngineOut for UciOut { 24 | fn info_depth_finished( 25 | &self, 26 | search_result: Option, 27 | ) -> Result<(), Box> { 28 | match search_result { 29 | Some(res) => match self.inner.lock() { 30 | Ok(mut inner) => { 31 | let move_to_str: Box String> = match inner.engine_options.lock() 32 | { 33 | Ok(opt) => match opt.variant { 34 | Variant::Standard => Box::new(UciMove::move_to_str), 35 | Variant::Chess960(king_rook, queen_rook) => Box::new(move |m| { 36 | UciMove::move_to_str_chess_960(m, king_rook, queen_rook) 37 | }), 38 | }, 39 | Err(e) => panic!("{}", e), 40 | }; 41 | let pv_str = res 42 | .principal_variation() 43 | .iter() 44 | .take_while(|m| **m != Move::NULL) 45 | .map(|m| move_to_str(*m)) 46 | .collect::>() 47 | .join(" "); 48 | Ok(writeln!( 49 | inner.writer, 50 | "info depth {} seldepth {} score {} nodes {} nps {} time {} hashfull {} pv {}", 51 | res.depth(), 52 | res.selective_depth(), 53 | UciScore::from(res.score()), 54 | res.nodes(), 55 | res.nodes_per_second(), 56 | res.time_ms(), 57 | res.hash_load_factor_permille(), 58 | pv_str 59 | )?) 60 | } 61 | Err(e) => { 62 | self.info_string(format!("{e}").as_str())?; 63 | panic!("{e}") 64 | } 65 | }, 66 | None => Ok(()), 67 | } 68 | } 69 | 70 | fn info_string(&self, s: &str) -> Result<(), Box> { 71 | match self.inner.lock() { 72 | Ok(mut inner) => match inner.debug { 73 | true => Ok(writeln!(inner.writer, "info string {s}")?), 74 | false => Ok(()), 75 | }, 76 | Err(e) => panic!("{e}"), 77 | } 78 | } 79 | 80 | fn best_move(&self, search_result: Option) -> Result<(), Box> { 81 | match search_result { 82 | Some(res) => match self.inner.lock() { 83 | Ok(mut inner) => { 84 | let move_to_str: Box String> = match inner.engine_options.lock() 85 | { 86 | Ok(opt) => match opt.variant { 87 | Variant::Standard => Box::new(UciMove::move_to_str), 88 | Variant::Chess960(king_rook, queen_rook) => Box::new(move |m| { 89 | UciMove::move_to_str_chess_960(m, king_rook, queen_rook) 90 | }), 91 | }, 92 | Err(e) => panic!("{e}"), 93 | }; 94 | Ok(writeln!(inner.writer, "bestmove {}", move_to_str(res))?) 95 | } 96 | Err(e) => { 97 | self.info_string(format!("{e}").as_str())?; 98 | panic!("{e}"); 99 | } 100 | }, 101 | None => Ok(()), 102 | } 103 | } 104 | } 105 | 106 | impl UciOut { 107 | pub fn new( 108 | writer: Box, 109 | engine_version: &str, 110 | engine_options: Arc>, 111 | ) -> Self { 112 | Self { 113 | inner: Arc::new(Mutex::new(UciOutInner { 114 | writer, 115 | engine_version: String::from(engine_version), 116 | debug: false, 117 | engine_options, 118 | })), 119 | } 120 | } 121 | 122 | pub fn set_debug(&self, tf: bool) { 123 | match self.inner.lock() { 124 | Ok(mut inner) => inner.debug = tf, 125 | Err(e) => panic!("{e}"), 126 | } 127 | } 128 | 129 | pub fn id(&mut self) -> Result<(), Box> { 130 | match self.inner.lock() { 131 | Ok(mut inner) => { 132 | let version = inner.engine_version.clone(); 133 | Ok(write!( 134 | inner.writer, 135 | "id name Fatalii {version}\nid author Patrick Heck\n", 136 | )?) 137 | } 138 | Err(e) => { 139 | self.info_string(format!("{e}").as_str())?; 140 | panic!("{e}") 141 | } 142 | } 143 | } 144 | 145 | pub fn all_options(&mut self) -> Result<(), Box> { 146 | for opt in OPTIONS { 147 | self.option(&opt)?; 148 | } 149 | Ok(()) 150 | } 151 | 152 | pub fn uci_ok(&mut self) -> Result<(), Box> { 153 | match self.inner.lock() { 154 | Ok(mut inner) => Ok(writeln!(inner.writer, "uciok")?), 155 | Err(e) => { 156 | self.info_string(format!("{e}").as_str())?; 157 | panic!("{e}") 158 | } 159 | } 160 | } 161 | 162 | pub fn ready_ok(&mut self) -> Result<(), Box> { 163 | match self.inner.lock() { 164 | Ok(mut inner) => Ok(writeln!(inner.writer, "readyok")?), 165 | Err(e) => { 166 | self.info_string(format!("{e}").as_str())?; 167 | panic!("{e}") 168 | } 169 | } 170 | } 171 | 172 | pub fn warn(&self, s: &str) -> Result<(), Box> { 173 | match self.inner.lock() { 174 | Ok(mut inner) => Ok(writeln!(inner.writer, "info string warning: {s}")?), 175 | Err(e) => panic!("{e}"), 176 | } 177 | } 178 | 179 | fn option(&mut self, opt: &UciOption) -> Result<(), Box> { 180 | match &opt.r#type { 181 | OptionType::Check(props) => match self.inner.lock() { 182 | Ok(mut inner) => writeln!( 183 | inner.writer, 184 | "option name {} type check default {}", 185 | opt.name, props.default, 186 | )?, 187 | Err(e) => { 188 | self.info_string(format!("{e}").as_str())?; 189 | panic!("{e}") 190 | } 191 | }, 192 | OptionType::Spin(props) => match self.inner.lock() { 193 | Ok(mut inner) => writeln!( 194 | inner.writer, 195 | "option name {} type spin default {} min {} max {}", 196 | opt.name, props.default, props.min, props.max, 197 | )?, 198 | Err(e) => { 199 | self.info_string(format!("{e}").as_str())?; 200 | panic!("{e}") 201 | } 202 | }, 203 | OptionType::Button | OptionType::Combo | OptionType::String => { 204 | unimplemented!(); 205 | } 206 | } 207 | Ok(()) 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /uci/src/uci_option.rs: -------------------------------------------------------------------------------- 1 | use engine::{Engine, Variant, DEFAULT_HASH_MB, DEFAULT_MOVE_OVERHEAD_MILLIS}; 2 | use eval::Score; 3 | use movegen::file::File; 4 | use search::search_params::SearchParamsOptions; 5 | use std::time::Duration; 6 | 7 | #[allow(dead_code)] 8 | pub enum OptionType { 9 | Button, 10 | Check(CheckProps), 11 | Combo, 12 | Spin(SpinProps), 13 | String, 14 | } 15 | 16 | pub struct CheckProps { 17 | pub default: bool, 18 | pub fun: fn(&mut Engine, value: bool) -> String, 19 | } 20 | 21 | pub struct SpinProps { 22 | pub default: i64, 23 | pub min: i64, 24 | pub max: i64, 25 | pub fun: fn(&mut Engine, value: i64) -> String, 26 | } 27 | 28 | pub struct UciOption { 29 | pub name: &'static str, 30 | pub r#type: OptionType, 31 | } 32 | 33 | pub const OPTIONS: [UciOption; 3] = [ 34 | UciOption { 35 | name: "Hash", 36 | r#type: OptionType::Spin(SpinProps { 37 | default: DEFAULT_HASH_MB as i64, 38 | min: 1, 39 | max: 65536, 40 | fun: set_hash_size, 41 | }), 42 | }, 43 | UciOption { 44 | name: "Move Overhead", 45 | r#type: OptionType::Spin(SpinProps { 46 | default: DEFAULT_MOVE_OVERHEAD_MILLIS as i64, 47 | min: 0, 48 | max: 10000, 49 | fun: set_move_overhead, 50 | }), 51 | }, 52 | UciOption { 53 | name: "UCI_Chess960", 54 | r#type: OptionType::Check(CheckProps { 55 | default: false, 56 | fun: set_chess_960, 57 | }), 58 | }, 59 | ]; 60 | 61 | fn set_hash_size(engine: &mut Engine, megabytes: i64) -> String { 62 | let bytes = 2_usize.pow(20) * megabytes as usize; 63 | engine.set_hash_size(bytes); 64 | format!("Hash set to {megabytes} MB") 65 | } 66 | 67 | fn set_move_overhead(engine: &mut Engine, move_overhead: i64) -> String { 68 | engine.set_move_overhead(Duration::from_millis(move_overhead as u64)); 69 | format!("Move Overhead set to {move_overhead} ms") 70 | } 71 | 72 | fn set_chess_960(engine: &mut Engine, enable: bool) -> String { 73 | engine.set_variant(Variant::Chess960(File::H, File::A)); 74 | match enable { 75 | true => String::from("Chess 960 enabled"), 76 | false => String::from("Chess 960 disabled"), 77 | } 78 | } 79 | 80 | #[allow(dead_code)] 81 | fn set_futility_margin_base(engine: &mut Engine, margin_base: i64) -> String { 82 | engine.set_search_params(SearchParamsOptions { 83 | futility_margin_base: Some(margin_base as Score), 84 | ..Default::default() 85 | }); 86 | format!("futility-margin-base set to {margin_base}") 87 | } 88 | 89 | #[allow(dead_code)] 90 | fn set_futility_margin_per_depth(engine: &mut Engine, margin_per_depth: i64) -> String { 91 | engine.set_search_params(SearchParamsOptions { 92 | futility_margin_per_depth: Some(margin_per_depth as Score), 93 | ..Default::default() 94 | }); 95 | format!("futility-margin-per-depth set to {margin_per_depth}") 96 | } 97 | 98 | #[allow(dead_code)] 99 | fn set_futility_pruning_max_depth(engine: &mut Engine, depth: i64) -> String { 100 | engine.set_search_params(SearchParamsOptions { 101 | futility_pruning_max_depth: Some(depth as usize), 102 | ..Default::default() 103 | }); 104 | format!("futility-pruning-max-depth set to {depth}") 105 | } 106 | 107 | #[allow(dead_code)] 108 | fn set_reverse_futility_margin_base(engine: &mut Engine, margin_base: i64) -> String { 109 | engine.set_search_params(SearchParamsOptions { 110 | reverse_futility_margin_base: Some(margin_base as Score), 111 | ..Default::default() 112 | }); 113 | format!("reverse-futility-margin-base set to {margin_base}") 114 | } 115 | 116 | #[allow(dead_code)] 117 | fn set_reverse_futility_margin_per_depth(engine: &mut Engine, margin_per_depth: i64) -> String { 118 | engine.set_search_params(SearchParamsOptions { 119 | reverse_futility_margin_per_depth: Some(margin_per_depth as Score), 120 | ..Default::default() 121 | }); 122 | format!("reverse-futility-margin-per-depth set to {margin_per_depth}") 123 | } 124 | 125 | #[allow(dead_code)] 126 | fn set_reverse_futility_pruning_max_depth(engine: &mut Engine, depth: i64) -> String { 127 | engine.set_search_params(SearchParamsOptions { 128 | reverse_futility_pruning_max_depth: Some(depth as usize), 129 | ..Default::default() 130 | }); 131 | format!("reverse-futility-pruning-max-depth set to {depth}") 132 | } 133 | 134 | #[allow(dead_code)] 135 | fn set_late_move_pruning_base(engine: &mut Engine, base: i64) -> String { 136 | engine.set_search_params(SearchParamsOptions { 137 | late_move_pruning_base: Some(base as usize), 138 | ..Default::default() 139 | }); 140 | format!("late-move-pruning-base set to {base}") 141 | } 142 | 143 | #[allow(dead_code)] 144 | fn set_late_move_pruning_factor(engine: &mut Engine, factor: i64) -> String { 145 | engine.set_search_params(SearchParamsOptions { 146 | late_move_pruning_factor: Some(factor as usize), 147 | ..Default::default() 148 | }); 149 | format!("late-move-pruning-factor set to {factor}") 150 | } 151 | 152 | #[allow(dead_code)] 153 | fn set_late_move_pruning_max_depth(engine: &mut Engine, depth: i64) -> String { 154 | engine.set_search_params(SearchParamsOptions { 155 | late_move_pruning_max_depth: Some(depth as usize), 156 | ..Default::default() 157 | }); 158 | format!("late-move-pruning-max-depth set to {depth}") 159 | } 160 | 161 | #[allow(dead_code)] 162 | fn set_late_move_reductions_centi_base(engine: &mut Engine, centi_base: i64) -> String { 163 | engine.set_search_params(SearchParamsOptions { 164 | late_move_reductions_centi_base: Some(centi_base as usize), 165 | ..Default::default() 166 | }); 167 | format!("late-move-reductions-centi-base set to {centi_base}") 168 | } 169 | 170 | #[allow(dead_code)] 171 | fn set_late_move_reductions_centi_divisor(engine: &mut Engine, centi_divisor: i64) -> String { 172 | engine.set_search_params(SearchParamsOptions { 173 | late_move_reductions_centi_divisor: Some(centi_divisor as usize), 174 | ..Default::default() 175 | }); 176 | format!("late-move-reductions-centi-divisor set to {centi_divisor}") 177 | } 178 | 179 | #[allow(dead_code)] 180 | fn set_see_pruning_margin_quiet(engine: &mut Engine, margin_quiet: i64) -> String { 181 | engine.set_search_params(SearchParamsOptions { 182 | see_pruning_margin_quiet: Some(margin_quiet as Score), 183 | ..Default::default() 184 | }); 185 | format!("see-pruning-margin-quiet set to {margin_quiet}") 186 | } 187 | 188 | #[allow(dead_code)] 189 | fn set_see_pruning_margin_tactical(engine: &mut Engine, margin_tactical: i64) -> String { 190 | engine.set_search_params(SearchParamsOptions { 191 | see_pruning_margin_tactical: Some(margin_tactical as Score), 192 | ..Default::default() 193 | }); 194 | format!("see-pruning-margin-tactical set to {margin_tactical}") 195 | } 196 | 197 | #[allow(dead_code)] 198 | fn set_see_pruning_max_depth(engine: &mut Engine, depth: i64) -> String { 199 | engine.set_search_params(SearchParamsOptions { 200 | see_pruning_max_depth: Some(depth as usize), 201 | ..Default::default() 202 | }); 203 | format!("see-pruning-max-depth set to {depth}") 204 | } 205 | 206 | #[allow(dead_code)] 207 | fn set_aspiration_window_initial_width(engine: &mut Engine, width: i64) -> String { 208 | engine.set_search_params(SearchParamsOptions { 209 | aspiration_window_initial_width: Some(width as i32), 210 | ..Default::default() 211 | }); 212 | format!("aspiration-window-initial-width set to {width}") 213 | } 214 | 215 | #[allow(dead_code)] 216 | fn set_aspiration_window_grow_rate(engine: &mut Engine, grow_rate: i64) -> String { 217 | engine.set_search_params(SearchParamsOptions { 218 | aspiration_window_grow_rate: Some(grow_rate as i32), 219 | ..Default::default() 220 | }); 221 | format!("aspiration-window-grow-rate set to {grow_rate}") 222 | } 223 | -------------------------------------------------------------------------------- /movegen/src/piece.rs: -------------------------------------------------------------------------------- 1 | use crate::side::Side; 2 | 3 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 4 | #[repr(u8)] 5 | pub enum Type { 6 | // The pieces are encoded this way to make the 4 possible promotion pieces fit into 2 bits. 7 | Pawn = 5, 8 | Knight = 0, 9 | Bishop = 1, 10 | Rook = 2, 11 | Queen = 3, 12 | King = 4, 13 | } 14 | 15 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 16 | pub struct Piece(u8); 17 | 18 | impl Piece { 19 | pub const NUM_PIECES: usize = 12; 20 | 21 | pub const WHITE_PAWN: Self = Self::new(Side::White, Type::Pawn); 22 | pub const WHITE_KNIGHT: Self = Self::new(Side::White, Type::Knight); 23 | pub const WHITE_BISHOP: Self = Self::new(Side::White, Type::Bishop); 24 | pub const WHITE_ROOK: Self = Self::new(Side::White, Type::Rook); 25 | pub const WHITE_QUEEN: Self = Self::new(Side::White, Type::Queen); 26 | pub const WHITE_KING: Self = Self::new(Side::White, Type::King); 27 | pub const BLACK_PAWN: Self = Self::new(Side::Black, Type::Pawn); 28 | pub const BLACK_KNIGHT: Self = Self::new(Side::Black, Type::Knight); 29 | pub const BLACK_BISHOP: Self = Self::new(Side::Black, Type::Bishop); 30 | pub const BLACK_ROOK: Self = Self::new(Side::Black, Type::Rook); 31 | pub const BLACK_QUEEN: Self = Self::new(Side::Black, Type::Queen); 32 | pub const BLACK_KING: Self = Self::new(Side::Black, Type::King); 33 | 34 | pub const fn new(s: Side, t: Type) -> Self { 35 | // Bit 0: piece side 36 | // Bits 1-3: piece type 37 | Piece(s as u8 | (t as u8) << 1) 38 | } 39 | 40 | pub fn piece_side(&self) -> Side { 41 | unsafe { std::mem::transmute::(self.0 & 0x1) } 42 | } 43 | 44 | pub fn piece_type(&self) -> Type { 45 | unsafe { std::mem::transmute::(self.0 >> 1) } 46 | } 47 | 48 | pub fn idx(&self) -> usize { 49 | self.0 as usize 50 | } 51 | 52 | pub fn from_ascii(c: u8) -> Result { 53 | match c { 54 | b'P' => Ok(Piece::WHITE_PAWN), 55 | b'N' => Ok(Piece::WHITE_KNIGHT), 56 | b'B' => Ok(Piece::WHITE_BISHOP), 57 | b'R' => Ok(Piece::WHITE_ROOK), 58 | b'Q' => Ok(Piece::WHITE_QUEEN), 59 | b'K' => Ok(Piece::WHITE_KING), 60 | b'p' => Ok(Piece::BLACK_PAWN), 61 | b'n' => Ok(Piece::BLACK_KNIGHT), 62 | b'b' => Ok(Piece::BLACK_BISHOP), 63 | b'r' => Ok(Piece::BLACK_ROOK), 64 | b'q' => Ok(Piece::BLACK_QUEEN), 65 | b'k' => Ok(Piece::BLACK_KING), 66 | _ => Err(format!("Invalid piece `{}`", c as char)), 67 | } 68 | } 69 | 70 | pub fn to_ascii(self) -> u8 { 71 | match self { 72 | Piece::WHITE_PAWN => b'P', 73 | Piece::WHITE_KNIGHT => b'N', 74 | Piece::WHITE_BISHOP => b'B', 75 | Piece::WHITE_ROOK => b'R', 76 | Piece::WHITE_QUEEN => b'Q', 77 | Piece::WHITE_KING => b'K', 78 | Piece::BLACK_PAWN => b'p', 79 | Piece::BLACK_KNIGHT => b'n', 80 | Piece::BLACK_BISHOP => b'b', 81 | Piece::BLACK_ROOK => b'r', 82 | Piece::BLACK_QUEEN => b'q', 83 | Piece::BLACK_KING => b'k', 84 | _ => panic!("Invalid piece encoding `{self:?}`"), 85 | } 86 | } 87 | } 88 | 89 | #[cfg(test)] 90 | mod tests { 91 | use super::*; 92 | 93 | #[test] 94 | fn bit_representation() { 95 | let wp = Piece::new(Side::White, Type::Pawn); 96 | assert_eq!(Side::White, wp.piece_side()); 97 | assert_eq!(Type::Pawn, wp.piece_type()); 98 | let wp = Piece::new(Side::White, Type::Knight); 99 | assert_eq!(Side::White, wp.piece_side()); 100 | assert_eq!(Type::Knight, wp.piece_type()); 101 | let wp = Piece::new(Side::White, Type::Bishop); 102 | assert_eq!(Side::White, wp.piece_side()); 103 | assert_eq!(Type::Bishop, wp.piece_type()); 104 | let wp = Piece::new(Side::White, Type::Rook); 105 | assert_eq!(Side::White, wp.piece_side()); 106 | assert_eq!(Type::Rook, wp.piece_type()); 107 | let wp = Piece::new(Side::White, Type::Queen); 108 | assert_eq!(Side::White, wp.piece_side()); 109 | assert_eq!(Type::Queen, wp.piece_type()); 110 | let wp = Piece::new(Side::White, Type::King); 111 | assert_eq!(Side::White, wp.piece_side()); 112 | assert_eq!(Type::King, wp.piece_type()); 113 | let wp = Piece::new(Side::Black, Type::Pawn); 114 | assert_eq!(Side::Black, wp.piece_side()); 115 | assert_eq!(Type::Pawn, wp.piece_type()); 116 | let wp = Piece::new(Side::Black, Type::Knight); 117 | assert_eq!(Side::Black, wp.piece_side()); 118 | assert_eq!(Type::Knight, wp.piece_type()); 119 | let wp = Piece::new(Side::Black, Type::Bishop); 120 | assert_eq!(Side::Black, wp.piece_side()); 121 | assert_eq!(Type::Bishop, wp.piece_type()); 122 | let wp = Piece::new(Side::Black, Type::Rook); 123 | assert_eq!(Side::Black, wp.piece_side()); 124 | assert_eq!(Type::Rook, wp.piece_type()); 125 | let wp = Piece::new(Side::Black, Type::Queen); 126 | assert_eq!(Side::Black, wp.piece_side()); 127 | assert_eq!(Type::Queen, wp.piece_type()); 128 | let wp = Piece::new(Side::Black, Type::King); 129 | assert_eq!(Side::Black, wp.piece_side()); 130 | assert_eq!(Type::King, wp.piece_type()); 131 | } 132 | 133 | #[test] 134 | fn idx() { 135 | assert_eq!(10, Piece::WHITE_PAWN.idx()); 136 | assert_eq!(0, Piece::WHITE_KNIGHT.idx()); 137 | assert_eq!(2, Piece::WHITE_BISHOP.idx()); 138 | assert_eq!(4, Piece::WHITE_ROOK.idx()); 139 | assert_eq!(6, Piece::WHITE_QUEEN.idx()); 140 | assert_eq!(8, Piece::WHITE_KING.idx()); 141 | assert_eq!(11, Piece::BLACK_PAWN.idx()); 142 | assert_eq!(1, Piece::BLACK_KNIGHT.idx()); 143 | assert_eq!(3, Piece::BLACK_BISHOP.idx()); 144 | assert_eq!(5, Piece::BLACK_ROOK.idx()); 145 | assert_eq!(7, Piece::BLACK_QUEEN.idx()); 146 | assert_eq!(9, Piece::BLACK_KING.idx()); 147 | } 148 | 149 | #[test] 150 | fn from_ascii() { 151 | assert_eq!(Ok(Piece::WHITE_PAWN), Piece::from_ascii(b'P')); 152 | assert_eq!(Ok(Piece::WHITE_KNIGHT), Piece::from_ascii(b'N')); 153 | assert_eq!(Ok(Piece::WHITE_BISHOP), Piece::from_ascii(b'B')); 154 | assert_eq!(Ok(Piece::WHITE_ROOK), Piece::from_ascii(b'R')); 155 | assert_eq!(Ok(Piece::WHITE_QUEEN), Piece::from_ascii(b'Q')); 156 | assert_eq!(Ok(Piece::WHITE_KING), Piece::from_ascii(b'K')); 157 | assert_eq!(Ok(Piece::BLACK_PAWN), Piece::from_ascii(b'p')); 158 | assert_eq!(Ok(Piece::BLACK_KNIGHT), Piece::from_ascii(b'n')); 159 | assert_eq!(Ok(Piece::BLACK_BISHOP), Piece::from_ascii(b'b')); 160 | assert_eq!(Ok(Piece::BLACK_ROOK), Piece::from_ascii(b'r')); 161 | assert_eq!(Ok(Piece::BLACK_QUEEN), Piece::from_ascii(b'q')); 162 | assert_eq!(Ok(Piece::BLACK_KING), Piece::from_ascii(b'k')); 163 | assert_eq!( 164 | Err(String::from("Invalid piece `!`")), 165 | Piece::from_ascii(b'!') 166 | ); 167 | } 168 | 169 | #[test] 170 | fn to_ascii() { 171 | assert_eq!(b'P', Piece::WHITE_PAWN.to_ascii()); 172 | assert_eq!(b'N', Piece::WHITE_KNIGHT.to_ascii()); 173 | assert_eq!(b'B', Piece::WHITE_BISHOP.to_ascii()); 174 | assert_eq!(b'R', Piece::WHITE_ROOK.to_ascii()); 175 | assert_eq!(b'Q', Piece::WHITE_QUEEN.to_ascii()); 176 | assert_eq!(b'K', Piece::WHITE_KING.to_ascii()); 177 | assert_eq!(b'p', Piece::BLACK_PAWN.to_ascii()); 178 | assert_eq!(b'n', Piece::BLACK_KNIGHT.to_ascii()); 179 | assert_eq!(b'b', Piece::BLACK_BISHOP.to_ascii()); 180 | assert_eq!(b'r', Piece::BLACK_ROOK.to_ascii()); 181 | assert_eq!(b'q', Piece::BLACK_QUEEN.to_ascii()); 182 | assert_eq!(b'k', Piece::BLACK_KING.to_ascii()); 183 | } 184 | 185 | #[test] 186 | #[should_panic] 187 | fn invalid_to_ascii() { 188 | let p = Piece(0x80); 189 | p.to_ascii(); 190 | } 191 | } 192 | --------------------------------------------------------------------------------