diff --git a/Cargo.lock b/Cargo.lock index 40207c0..9ab5ecf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -189,15 +189,26 @@ dependencies = [ "libloading", ] +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + [[package]] name = "engine" version = "1.0.1" dependencies = [ "chrono", + "log", "queues", "serde", "serde_derive", "shakmaty", + "simplelog", "toml", "wasm-bindgen", "web-sys", @@ -279,6 +290,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "js-sys" version = "0.3.77" @@ -350,6 +367,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-traits" version = "0.2.19" @@ -360,12 +383,27 @@ dependencies = [ "libm", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -532,6 +570,17 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "simplelog" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" +dependencies = [ + "log", + "termcolor", + "time", +] + [[package]] name = "strsim" version = "0.8.0" @@ -584,6 +633,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "textwrap" version = "0.11.0" @@ -613,6 +671,39 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "toml" version = "0.8.20" @@ -784,6 +875,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -849,6 +949,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-targets" version = "0.52.6" diff --git a/Makefile b/Makefile index bd49f85..e08bafa 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ -# Build executables for Carp releases. Base rule is reserved for OpenBench EXE := Pluto_1.0.1 LXE := Pluto_1.0.1 _THIS := $(realpath $(dir $(abspath $(lastword $(MAKEFILE_LIST))))) TMPDIR := $(_THIS)/tmp +HCE := true ifeq ($(OS),Windows_NT) EXT := .exe @@ -16,45 +16,16 @@ else VER := linux endif +ifeq ($(HCE),true) + FEATURES := tuning,log,classical +else + FEATURES := tuning,log +endif + NAME := $(EXE)$(EXT) rule: - cargo rustc -r -p engine --bins -- -C target-cpu=native --emit link=$(NAME) + cargo rustc -r -p engine --bins --features $(FEATURES) -- -C target-cpu=native --emit link=$(NAME) tmp-dir: mkdir -p $(TMPDIR) - -x86-64 x86-64-v2 x86-64-v3 x86-64-v4 native: tmp-dir - cargo rustc -r -p engine --bins -- -C target-cpu=$@ -C profile-generate=$(TMPDIR) --emit link=$(LXE)-$(VER)-$@$(EXT) - ./$(LXE)-$(VER)-$@$(EXT) bench 16 - llvm-profdata merge -o $(TMPDIR)/merged.profdata $(TMPDIR) - - cargo rustc -r -p engine --bins -- -C target-feature=+crt-static -C target-cpu=$@ -C profile-use=$(TMPDIR)/merged.profdata --emit link=$(LXE)-$(VER)-$@$(EXT) - - rm -rf $(TMPDIR)/* - rm -f *.pdb - -syzygy: tmp-dir - cargo rustc -r -p engine --bins --features syzygy -- -C target-cpu=native -C profile-generate=$(TMPDIR) --emit link=$(LXE)-$(VER)$(EXT) - ./$(LXE)-$(VER)$(EXT) bench 16 - llvm-profdata merge -o $(TMPDIR)/merged.profdata $(TMPDIR) - - cargo rustc -r -p engine --bins --features syzygy -- -C target-feature=+crt-static -C target-cpu=native -C profile-use=$(TMPDIR)/merged.profdata --emit link=$(LXE)-$(VER)$(EXT) - - rm -rf $(TMPDIR)/* - rm -f *.pdb - -datagen: tmp-dir - cargo rustc -r -p tools -- -C target-cpu=native -C profile-generate=$(TMPDIR) --emit link=datagen$(EXT) - ./datagen$(EXT) datagen -g 256 -t 32 -n 5000 - ./datagen$(EXT) datagen -g 256 -t 32 -d 8 - llvm-profdata merge -o $(TMPDIR)/merged.profdata $(TMPDIR) - - cargo rustc -r -p tools -- -C target-cpu=native -C profile-use=$(TMPDIR)/merged.profdata --emit link=datagen$(EXT) - - rm -rf $(TMPDIR) - rm -rf $(_THIS)/data - rm -f *.pdb - -release: x86-64 x86-64-v2 x86-64-v3 x86-64-v4 - rm -rf $(TMPDIR) diff --git a/README.md b/README.md index b6359bd..14c5da7 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,15 @@ # Pluto Chess Engine - -by [Lxdovic](https://github.com/Lxdovic) +
+ + + Shows an illustrated sun in light color mode and a moon with stars in dark color mode. + + + [![License][license-badge]][license-link] + [![Release][release-badge]][release-link] + [![Commits][commits-badge]][commits-link] +
## Table of Contents @@ -19,9 +27,9 @@ by [Lxdovic](https://github.com/Lxdovic) ## Strength -| Version | Date | Estimated ELO | CCRL | -| -------------- | ----------- | ------------- | ---- | -| 1.0.0 | 11 May 2025 | 2870 | 2872 | +| Version | Date | Estimated ELO | CCRL 40/15 | CCRL Blitz 2+1 | +| -------------- | ----------- | ------------- | ---- | ---- | +| 1.0.0 | 11 May 2025 | 2870 | 2879 | 2976 | ## Testing @@ -54,15 +62,27 @@ Pluto is tested with OpenBench, a distributed testing framework for UCI chess en - **Killer Moves** - **Transposition Tables** - Evaluation: - Pluto adopted Efficiently Updatable Neural Networks for its evaluation function quite early in development. Earlier versions were using Simple Eval/Pesto Eval + - **HCE** (classical build, see how to build it in #Building) - **NNUE (768->512)x2->1** Trained using [Bullet](https://github.com/jw1912/bullet) and Stockfish data. ## Building -To build the engine, clone the repository and run the following command in your terminal: +To build the engine, clone the repository and use one of the following options: + +### NNUE + +This will build Pluto NNUE ```bash -make +cargo build --release --bin engine +``` + +### Classical + +This allows you to build Pluto HCE + +```bash +cargo build --release --bin engine --features classical ``` ## Contributors @@ -81,3 +101,10 @@ Pluto was built using these resources and tools - [Bullet](https://github.com/jw1912/bullet) -> great tool for building NNUEs - [Stockfish](https://stockfishchess.org/) -> some implementation examples and ideas +[license-badge]: https://img.shields.io/github/license/Lxdovic/Pluto?style=for-the-badge +[release-badge]: https://img.shields.io/github/v/release/Lxdovic/Pluto?style=for-the-badge +[commits-badge]: https://img.shields.io/github/commits-since/Lxdovic/Pluto/latest?style=for-the-badge + +[license-link]: https://github.com/Lxdovic/Pluto/blob/main/LICENSE +[release-link]: https://github.com/Lxdovic/Pluto/releases/latest +[commits-link]: https://github.com/Lxdovic/Pluto/commits/dev diff --git a/engine/Cargo.toml b/engine/Cargo.toml index 48ded00..dd56d6e 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -15,6 +15,12 @@ path = "src/lib.rs" name = "engine" path = "src/main.rs" +[features] +default = [] +tuning = [] +classical = [] +log = [] + [dependencies] queues = "1.1.0" shakmaty = "0.27.2" @@ -26,3 +32,5 @@ chrono = "0.4.40" web-sys = { version = "0.3.77", features = [ 'Worker', ] } +log = "0.4" +simplelog = "0.12" diff --git a/engine/src/build.rs b/engine/src/build.rs new file mode 100644 index 0000000..f50f482 --- /dev/null +++ b/engine/src/build.rs @@ -0,0 +1,14 @@ +fn main() { + if std::env::var("CARGO_FEATURE_TUNING").is_ok() { + println!("cargo:rustc-cfg=feature=\"tuning\""); + } + + // pluto classical means no use of NNUE, HCE is used instead + if std::env::var("CARGO_FEATURE_CLASSICAL").is_ok() { + println!("cargo:rustc-cfg=feature=\"classical\""); + } + + if std::env::var("CARGO_FEATURE_CLASSICAL").is_ok() { + println!("cargo:rustc-cfg=feature=\"log\""); + } +} diff --git a/engine/src/config.rs b/engine/src/config.rs index e826f54..e87666d 100644 --- a/engine/src/config.rs +++ b/engine/src/config.rs @@ -15,6 +15,7 @@ along with this program. If not, see . */ +use crate::logger::Logger; use std::fmt::{self}; #[derive(Debug)] @@ -44,334 +45,134 @@ pub struct OptionDescriptor { pub value: T, pub min: T, pub max: T, + pub tunable: bool, } -impl OptionDescriptor { - pub fn fmt_spsa(&self) -> String { - format!( - "{}, int, {}, {}, {}, {}, {}", - self.name, self.value, self.min, self.max, 2.25, 0.002 - ) - } -} -impl OptionDescriptor { - pub fn fmt_spsa(&self) -> String { - format!( - "{}, int, {}, {}, {}, {}, {}", - self.name, self.value, self.min, self.max, 2.25, 0.002 - ) - } -} -impl OptionDescriptor { - pub fn fmt_spsa(&self) -> String { - format!( - "{}, int, {}, {}, {}, {}, {}", - self.name, self.value, self.min, self.max, 2.25, 0.002 - ) - } -} -impl OptionDescriptor { - pub fn fmt_spsa(&self) -> String { - format!( - "{}, int, {}, {}, {}, {}, {}", - self.name, self.value, self.min, self.max, 2.25, 0.002 - ) - } -} -impl OptionDescriptor { - pub fn fmt_spsa(&self) -> String { - format!( - "{}, int, {}, {}, {}, {}, {}", - self.name, self.value, self.min, self.max, 2.25, 0.002 - ) - } -} -impl OptionDescriptor { - pub fn fmt_spsa(&self) -> String { - format!( - "{}, float, {}, {}, {}, {}, {}", - self.name, self.value, self.min, self.max, 2.25, 0.002 - ) - } +#[cfg(feature = "tuning")] +macro_rules! impl_fmt_spsa { + ($($t:ty => $ty_str:expr),*) => { + $(impl OptionDescriptor<$t> { + pub fn fmt_spsa(&self) -> String { + format!( + "{}, {}, {}, {}, {}, {}, {}", + self.name, $ty_str, self.value, self.min, self.max, 2.25, 0.002 + ) + } + })* + }; } -impl fmt::Display for OptionDescriptor { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "option name {} type {} default {} min {} max {}", - self.name, self.kind, self.value, self.min, self.max - ) - } -} -impl fmt::Display for OptionDescriptor { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "option name {} type {} default {} min {} max {}", - self.name, self.kind, self.value, self.min, self.max - ) - } -} -impl fmt::Display for OptionDescriptor { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "option name {} type {} default {} min {} max {}", - self.name, self.kind, self.value, self.min, self.max - ) - } -} -impl fmt::Display for OptionDescriptor { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "option name {} type {} default {} min {} max {}", - self.name, self.kind, self.value, self.min, self.max - ) - } -} -impl fmt::Display for OptionDescriptor { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "option name {} type {} default {} min {} max {}", - self.name, self.kind, self.value, self.min, self.max - ) - } -} -impl fmt::Display for OptionDescriptor { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "option name {} type {} default {} min {} max {}", - self.name, self.kind, self.value, self.min, self.max - ) - } -} +#[cfg(feature = "tuning")] +impl_fmt_spsa!( + i32 => "int", + u8 => "int", + usize => "int", + i64 => "int", + u64 => "int", + f64 => "float" +); -pub struct Config { - pub move_overhead: OptionDescriptor, - pub threads: OptionDescriptor, - pub hash: OptionDescriptor, - pub qsearch_depth: OptionDescriptor, - pub rfp_depth: OptionDescriptor, - pub rfp_base_margin: OptionDescriptor, - pub rfp_reduction_improving: OptionDescriptor, - pub fp_depth_margin: OptionDescriptor, - pub fp_base_margin: OptionDescriptor, - pub fp_margin_depth_factor: OptionDescriptor, - pub nmp_depth: OptionDescriptor, - pub nmp_margin: OptionDescriptor, - pub nmp_divisor: OptionDescriptor, - pub nmp_divisor_improving: OptionDescriptor, - pub lmp_move_margin: OptionDescriptor, - pub lmp_depth_factor: OptionDescriptor, - pub lmr_depth: OptionDescriptor, - pub lmr_move_margin: OptionDescriptor, - pub lmr_quiet_margin: OptionDescriptor, - pub lmr_quiet_divisor: OptionDescriptor, - pub lmr_base_margin: OptionDescriptor, - pub lmr_base_divisor: OptionDescriptor, - pub mo_tt_entry_value: OptionDescriptor, - pub mo_capture_value: OptionDescriptor, - pub mo_killer_value: OptionDescriptor, - pub tc_time_divisor: OptionDescriptor, - pub tc_elapsed_factor: OptionDescriptor, +macro_rules! impl_fmt_display { + ($($t:ty),*) => { + $(impl fmt::Display for OptionDescriptor<$t> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "option name {} type {} default {} min {} max {}", + self.name, self.kind, self.value, self.min, self.max + ) + } + })* + }; } -impl Config { - pub fn default() -> Self { - Self { - move_overhead: OptionDescriptor { - name: "MoveOverhead", - kind: OptionKind::Spin, - value: 0, - min: 0, - max: 10000, - }, - threads: OptionDescriptor { - name: "Threads", - kind: OptionKind::Spin, - value: 1, - min: 1, - max: 1, - }, - hash: OptionDescriptor { - name: "Hash", - kind: OptionKind::Spin, - value: 255, - min: 1, - max: 1024, - }, - qsearch_depth: OptionDescriptor { - name: "QSearchDepth", - kind: OptionKind::Spin, - value: 17, - min: 1, - max: 20, - }, - rfp_depth: OptionDescriptor { - name: "RFPDepth", - kind: OptionKind::Spin, - value: 11, - min: 1, - max: 20, - }, - rfp_base_margin: OptionDescriptor { - name: "RFPBaseMargin", - kind: OptionKind::Spin, - value: 57, - min: 1, - max: 200, - }, - rfp_reduction_improving: OptionDescriptor { - name: "RFPReductionImproving", - kind: OptionKind::Spin, - value: 24, - min: 1, - max: 200, - }, - fp_depth_margin: OptionDescriptor { - name: "FPDepthMargin", - kind: OptionKind::Spin, - value: 5, - min: 1, - max: 20, - }, - fp_base_margin: OptionDescriptor { - name: "FPBaseMargin", - kind: OptionKind::Spin, - value: 40, - min: 1, - max: 200, - }, - fp_margin_depth_factor: OptionDescriptor { - name: "FPMarginDepthFactor", - kind: OptionKind::Spin, - value: 35, - min: 1, - max: 200, - }, - nmp_depth: OptionDescriptor { - name: "NMPDepth", - kind: OptionKind::Spin, - value: 5, - min: 1, - max: 20, - }, - nmp_margin: OptionDescriptor { - name: "NMPMargin", - kind: OptionKind::Spin, - value: 7, - min: 1, - max: 20, - }, - nmp_divisor: OptionDescriptor { - name: "NMPDivisor", - kind: OptionKind::Spin, - value: 2, - min: 1, - max: 20, - }, - nmp_divisor_improving: OptionDescriptor { - name: "NMPDivisorImproving", - kind: OptionKind::Spin, - value: 10, - min: 1, - max: 20, - }, - lmp_move_margin: OptionDescriptor { - name: "LMPMoveMargin", - kind: OptionKind::Spin, - value: 5, - min: 1, - max: 20, - }, - lmp_depth_factor: OptionDescriptor { - name: "LMPDepthFactor", - kind: OptionKind::Spin, - value: 7, - min: 1, - max: 20, - }, - lmr_depth: OptionDescriptor { - name: "LMRDepth", - kind: OptionKind::Spin, - value: 15, - min: 1, - max: 20, - }, - lmr_move_margin: OptionDescriptor { - name: "LMRMoveMargin", - kind: OptionKind::Spin, - value: 5, - min: 1, - max: 20, - }, - lmr_quiet_margin: OptionDescriptor { - name: "LMRQuietMargin", - kind: OptionKind::String, - value: 2.59, - min: 0.0, - max: 10.0, - }, - lmr_quiet_divisor: OptionDescriptor { - name: "LMRQuietDivisor", - kind: OptionKind::String, - value: 2.02, - min: 1.0, - max: 10.0, - }, - lmr_base_margin: OptionDescriptor { - name: "LMRBaseMargin", - kind: OptionKind::String, - value: 1.31, - min: 0.0, - max: 10.0, - }, - lmr_base_divisor: OptionDescriptor { - name: "LMRBaseDivisor", - kind: OptionKind::String, - value: 3.28, - min: 1.0, - max: 10.0, - }, - mo_tt_entry_value: OptionDescriptor { - name: "MOTTEntryValue", - kind: OptionKind::Spin, - value: 229, - min: 1, - max: 500, - }, - mo_capture_value: OptionDescriptor { - name: "MOCaptureValue", - kind: OptionKind::Spin, - value: 55, - min: 0, - max: 500, - }, - mo_killer_value: OptionDescriptor { - name: "MOKillerValue", - kind: OptionKind::Spin, - value: 78, - min: 0, - max: 500, - }, - tc_time_divisor: OptionDescriptor { - name: "TCTimeDivisor", - kind: OptionKind::Spin, - value: 2, - min: 2, - max: 100, - }, - tc_elapsed_factor: OptionDescriptor { - name: "TCElapsedFactor", - kind: OptionKind::Spin, - value: 8, - min: 1, - max: 10, - }, +impl_fmt_display!(i32, u8, usize, i64, u64, f64); + +macro_rules! make_config { + ($($field:ident: $t:ty = ($name:expr, $kind:expr, $val:expr, $min:expr, $max:expr, $tunable:expr);)*) => { + pub struct Config { + $(pub $field: OptionDescriptor<$t>,)* } - } + + impl Config { + pub fn default() -> Self { + Self { + $( + $field: OptionDescriptor { + name: $name, + kind: $kind, + value: $val, + min: $min, + max: $max, + tunable: $tunable, + }, + )* + } + } + + pub fn set(&mut self, name: &str, val: &str) { + match name { + $( + $name => { + if !cfg!(feature = "tuning") && self.$field.tunable { + Logger::log(&format!("info string cannot modify tunable option in non tunable build")) + } + else if let Ok(parsed) = val.parse::<$t>() { + self.$field.value = parsed; + } else { + Logger::log(&format!("info string invalid value for {}: {}", $name, val)); + } + }, + )* + _ => Logger::log(&format!("info string unknown option: {}", name)), + } + } + + #[cfg(feature = "tuning")] + pub fn all_spsa(&self) -> Vec { + vec![ + $(self.$field.fmt_spsa(),)* + ] + } + + pub fn print_uci_options(&self) { + $( + + if cfg!(feature = "tuning") || !self.$field.tunable { + Logger::log(format!("{}", self.$field).as_str()); + } + )* + } + } + }; +} + +make_config! { + move_overhead: usize = ("MoveOverhead", OptionKind::Spin, 0, 0, 10000, false); + threads: u8 = ("Threads", OptionKind::Spin, 1, 1, 1, false); + hash: usize = ("Hash", OptionKind::Spin, 255, 1, 1024, false); + qsearch_depth: u8 = ("QSearchDepth", OptionKind::Spin, 17, 1, 20, true); + rfp_depth: u8 = ("RFPDepth", OptionKind::Spin, 11, 1, 20, true); + rfp_base_margin: i32 = ("RFPBaseMargin", OptionKind::Spin, 57, 1, 200, true); + rfp_reduction_improving: i32 = ("RFPReductionImproving", OptionKind::Spin, 24, 1, 200, true); + fp_depth_margin: u8 = ("FPDepthMargin", OptionKind::Spin, 5, 1, 20, true); + fp_base_margin: i32 = ("FPBaseMargin", OptionKind::Spin, 40, 1, 200, true); + fp_margin_depth_factor: i32 = ("FPMarginDepthFactor", OptionKind::Spin, 35, 1, 200, true); + nmp_depth: u8 = ("NMPDepth", OptionKind::Spin, 5, 1, 20, true); + nmp_margin: u8 = ("NMPMargin", OptionKind::Spin, 7, 1, 20, true); + nmp_divisor: u8 = ("NMPDivisor",OptionKind::Spin, 2, 1, 20, true); + nmp_divisor_improving: u8 = ("NMPDivisorImproving", OptionKind::Spin, 10, 1, 20, true); + lmp_move_margin: usize = ("LMPMoveMargin", OptionKind::Spin, 5, 1, 20, true); + lmp_depth_factor: u8 = ("LMPDepthFactor", OptionKind::Spin, 7, 1, 20, true); + lmr_depth: u8 = ("LMRDepth", OptionKind::Spin, 15, 1, 20, true); + lmr_move_margin: usize = ("LMRMoveMargin", OptionKind::Spin, 5, 1, 20, true); + lmr_quiet_margin: f64 = ("LMRQuietMargin", OptionKind::String, 2.59, 0.0, 10.0, true); + lmr_quiet_divisor: f64 = ("LMRQuietDivisor", OptionKind::String, 2.02, 1.0, 10.0, true); + lmr_base_margin: f64 = ("LMRBaseMargin", OptionKind::String, 1.31, 0.0, 10.0, true); + lmr_base_divisor: f64 = ("LMRBaseDivisor", OptionKind::String, 3.28, 1.0, 10.0, true); + mo_tt_entry_value: i32 = ("MOTTEntryValue", OptionKind::Spin, 229, 1, 500, true); + mo_capture_value: i32 = ("MOCaptureValue", OptionKind::Spin, 55, 0, 500, true); + mo_killer_value: i32 = ("MOKillerValue", OptionKind::Spin, 78, 0, 500, true); + tc_time_divisor: u64 = ("TCTimeDivisor", OptionKind::Spin, 2, 2, 100, true); + tc_elapsed_factor: i64 = ("TCElapsedFactor", OptionKind::Spin, 8, 1, 10, true); } diff --git a/engine/src/eval.rs b/engine/src/eval.rs index c8e8d1d..ec3e4c4 100644 --- a/engine/src/eval.rs +++ b/engine/src/eval.rs @@ -16,9 +16,25 @@ */ /// Position evaluation module containing piece-square tables and evaluation functions. +#[cfg(not(feature = "classical"))] use crate::nnue::{NNUEState, NNUE}; +use crate::packing::s; +#[cfg(feature = "classical")] +use crate::packing::{extract_eg, extract_mg}; +#[cfg(feature = "classical")] +use shakmaty::{attacks, Bitboard, Piece, Role, Square}; use shakmaty::{Chess, Color, Position}; +#[cfg(feature = "classical")] +#[derive(Default)] +pub struct EvalState { + phase: i32, + eval: [i32; 2], +} + +#[cfg(feature = "classical")] +type EvalRoleFn = fn(&Chess, Square, Piece) -> i32; + pub struct Eval {} impl Eval { @@ -32,13 +48,416 @@ impl Eval { false } + #[cfg(feature = "classical")] + fn eval_pawn(pos: &Chess, sq: Square, piece: Piece) -> i32 { + let doubled = Self::doubled(pos, sq, piece); + let isolated = Self::isolated(pos, sq, piece); + let passed = Self::passed(pos, sq, piece); + + doubled + isolated + passed + } + + #[cfg(feature = "classical")] + fn eval_knight(pos: &Chess, sq: Square, piece: Piece) -> i32 { + Self::mobility(pos, sq, piece) + } + + #[cfg(feature = "classical")] + fn eval_bishop(pos: &Chess, sq: Square, piece: Piece) -> i32 { + Self::mobility(pos, sq, piece) + } + + #[cfg(feature = "classical")] + fn eval_rook(pos: &Chess, sq: Square, piece: Piece) -> i32 { + let mobility = Self::mobility(pos, sq, piece); + let files = Self::rook_files(pos, sq); + + mobility + files + } + + #[cfg(feature = "classical")] + fn eval_queen(pos: &Chess, sq: Square, piece: Piece) -> i32 { + Self::mobility(pos, sq, piece) + } + + #[cfg(feature = "classical")] + fn eval_king(pos: &Chess, sq: Square, piece: Piece) -> i32 { + Self::king_shield(pos, sq, piece) + } + + #[cfg(feature = "classical")] + fn mobility(pos: &Chess, sq: Square, piece: Piece) -> i32 { + let attacks = attacks::attacks(sq, piece, pos.board().occupied()); + + MOBILITY[attacks.count()] + } + + #[cfg(feature = "classical")] + fn doubled(pos: &Chess, sq: Square, piece: Piece) -> i32 { + let pawns = pos.board().by_piece(piece); + let file = FILES_TABLE[sq.file() as usize]; + let count = pawns.intersect(file).count(); + + DOUBLED[count] + } + + #[cfg(feature = "classical")] + fn isolated(pos: &Chess, sq: Square, piece: Piece) -> i32 { + let isolation = ADJACENT_FILES_TABLE[sq.file() as usize]; + let our_pawns = pos.board().by_piece(piece); + + if isolation.intersect(our_pawns).count() == 0 { + return ISOLATED; + } + + 0 + } + + #[cfg(feature = "classical")] + fn passed(pos: &Chess, sq: Square, piece: Piece) -> i32 { + let isolation = ADJACENT_AND_FILE_TABLE[sq.file() as usize]; + let their_pawns = pos.board().by_piece(Piece { + role: piece.role, + color: piece.color.other(), + }); + + if isolation.intersect(their_pawns).count() == 0 { + return PASSED; + } + + 0 + } + + #[cfg(feature = "classical")] + fn king_shield(pos: &Chess, sq: Square, piece: Piece) -> i32 { + let our_pawns = pos.board().by_piece(Piece { + role: Role::Pawn, + color: piece.color, + }); + + let index = attacks::attacks(sq, piece, Bitboard(0)) + .intersect(our_pawns) + .count(); + + KING_SHIELD[index] + } + + #[cfg(feature = "classical")] + fn bishop_pair(pos: &Chess, state: &mut EvalState) { + let bishops = pos.board().bishops(); + let white_bishops = pos.board().white().intersect(bishops); + let black_bishops = pos.board().black().intersect(bishops); + + if white_bishops.count() >= 2 { + state.eval[Color::White as usize] += BISHOP_PAIR + } + + if black_bishops.count() >= 2 { + state.eval[Color::Black as usize] += BISHOP_PAIR + } + } + + #[cfg(feature = "classical")] + fn rook_files(pos: &Chess, sq: Square) -> i32 { + let board = pos.board(); + let us = pos.turn(); + let file = FILES_TABLE[sq.file() as usize]; + let all_pawns = board.pawns(); + + if file.intersect(all_pawns).count() == 0 { + return ROOK_FILES[0]; + } + + let our_pawns = board.by_piece(Piece { + role: Role::Pawn, + color: us, + }); + + if file.intersect(our_pawns).count() == 0 { + return ROOK_FILES[1]; + } + + 0 + } + #[cfg(feature = "classical")] + fn eval_piece(pos: &Chess, sq: Square, piece: Piece, state: &mut EvalState) { + let role_index = piece.role as usize - 1; + let piece_index = role_index * 2 + (!piece.color as usize); + let square_index = sq as usize; + let piece_score = EVAL_ROLES[role_index](pos, sq, piece); + + state.eval[piece.color as usize] += TABLE[piece_index][square_index] + piece_score; + state.phase += GAME_PHASES[piece_index]; + } + + #[cfg(feature = "classical")] + fn tempo(pos: &Chess, state: &mut EvalState) { + state.eval[pos.turn() as usize] += TEMPO; + } + + #[cfg(feature = "classical")] + pub fn eval(pos: &Chess) -> i32 { + let mut state = EvalState::default(); + + for (sq, piece) in pos.board() { + Self::eval_piece(pos, sq, piece, &mut state); + } + + Self::tempo(pos, &mut state); + Self::bishop_pair(pos, &mut state); + + let score = state.eval[Color::White as usize] - state.eval[Color::Black as usize]; + let mg = extract_mg(score); + let eg = extract_eg(score); + + (mg * state.phase + eg * (24 - state.phase)) / 24 + * if pos.turn() == Color::White { 1 } else { -1 } + } + + #[cfg(feature = "classical")] + pub const fn init_piece_table() -> [[i32; 64]; 12] { + let mut eg_table = [[0; 64]; 12]; + + let mut p = 0; + let mut pc = 0; + let mut sq = 0; + + while p < 6 { + while sq < 64 { + eg_table[pc][sq] = PIECE_VALUES[p] + PESTO_TABLE[p][sq ^ 56]; + eg_table[pc + 1][sq] = PIECE_VALUES[p] + PESTO_TABLE[p][sq]; + + sq += 1; + } + + sq = 0; + p += 1; + pc += 2; + } + + eg_table + } + + #[cfg(not(feature = "classical"))] pub fn nnue_eval(state: &NNUEState, pos: &Chess) -> i32 { #[rustfmt::skip] let (us, them) = match pos.turn() { Color::White => (state.stack[state.current].white, state.stack[state.current].black), - Color::Black => ( state.stack[state.current].black, state.stack[state.current].white), + Color::Black => (state.stack[state.current].black, state.stack[state.current].white), }; NNUE.evaluate(&us, &them) } } + +#[cfg(feature = "classical")] +const TABLE: [[i32; 64]; 12] = Eval::init_piece_table(); +#[cfg(feature = "classical")] +const PESTO_TABLE: [[i32; 64]; 6] = [PAWN, KNIGHT, BISHOP, ROOK, QUEEN, KING]; +#[rustfmt::skip] +pub const PIECE_VALUES: [i32; 6] = [s(82, 94), s(337, 281), s(365, 297), s(447, 512), s(1025, 936), s(0, 0)]; +#[cfg(feature = "classical")] +const TEMPO: i32 = s(28, 0); +#[cfg(feature = "classical")] +const GAME_PHASES: [i32; 12] = [0, 0, 1, 1, 1, 1, 2, 2, 4, 4, 0, 0]; +#[cfg(feature = "classical")] +const EVAL_ROLES: [EvalRoleFn; 6] = [ + Eval::eval_pawn, + Eval::eval_knight, + Eval::eval_bishop, + Eval::eval_rook, + Eval::eval_queen, + Eval::eval_king, +]; + +#[cfg(feature = "classical")] +const PASSED: i32 = s(10, 10); +#[cfg(feature = "classical")] +const ISOLATED: i32 = s(-10, -10); +#[cfg(feature = "classical")] +#[rustfmt::skip] +const KING_SHIELD: [i32; 9] = [s(0, 0), s(1, 1), s(2, 2), s(3, 3), s(4, 4), s(5, 5), s(6, 6), s(7, 7), s(8, 8)]; +#[cfg(feature = "classical")] +const ROOK_FILES: [i32; 2] = [s(20, 0), s(10, 0)]; +#[cfg(feature = "classical")] +const BISHOP_PAIR: i32 = s(10, 40); + +#[cfg(feature = "classical")] +const DOUBLED: [i32; 9] = [ + s(5, 5), + s(0, 0), + s(-5, -5), + s(-10, -10), + s(-15, -15), + s(-20, -20), + s(-25, -25), + s(-30, -30), + s(-35, -35), +]; +#[cfg(feature = "classical")] +const MOBILITY: [i32; 28] = [ + s(0, 0), + s(1, 1), + s(2, 2), + s(3, 3), + s(4, 4), + s(5, 5), + s(6, 6), + s(7, 7), + s(8, 8), + s(9, 9), + s(10, 10), + s(11, 11), + s(12, 12), + s(13, 13), + s(14, 14), + s(15, 15), + s(16, 16), + s(17, 17), + s(18, 18), + s(19, 19), + s(20, 20), + s(21, 21), + s(22, 22), + s(23, 23), + s(24, 24), + s(25, 25), + s(26, 26), + s(27, 27), +]; + +/* gives adjacent files for index i +* if i = 2: +* 0 1 0 1 0 0 0 0 +* 0 1 0 1 0 0 0 0 +* 0 1 0 1 0 0 0 0 +* 0 1 0 1 0 0 0 0 +* 0 1 0 1 0 0 0 0 +* 0 1 0 1 0 0 0 0 +* 0 1 0 1 0 0 0 0 +* 0 1 0 1 0 0 0 0 +*/ +#[cfg(feature = "classical")] +const ADJACENT_FILES_TABLE: [Bitboard; 8] = [ + Bitboard(0x202020202020202), + Bitboard(0x505050505050505), + Bitboard(0xa0a0a0a0a0a0a0a), + Bitboard(0x1414141414141414), + Bitboard(0x2828282828282828), + Bitboard(0x5050505050505050), + Bitboard(0xa0a0a0a0a0a0a0a0), + Bitboard(0x4040404040404040), +]; + +#[cfg(feature = "classical")] +const FILES_TABLE: [Bitboard; 8] = [ + Bitboard(0x101010101010101), + Bitboard(0x202020202020202), + Bitboard(0x404040404040404), + Bitboard(0x808080808080808), + Bitboard(0x1010101010101010), + Bitboard(0x2020202020202020), + Bitboard(0x4040404040404040), + Bitboard(0x8080808080808080), +]; + +/* gives adjacent and current files for index i +* if i = 2: +* 0 1 1 1 0 0 0 0 +* 0 1 1 1 0 0 0 0 +* 0 1 1 1 0 0 0 0 +* 0 1 1 1 0 0 0 0 +* 0 1 1 1 0 0 0 0 +* 0 1 1 1 0 0 0 0 +* 0 1 1 1 0 0 0 0 +* 0 1 1 1 0 0 0 0 +*/ +#[cfg(feature = "classical")] +const ADJACENT_AND_FILE_TABLE: [Bitboard; 8] = [ + Bitboard(0x101010101010101 & 0x202020202020202), + Bitboard(0x202020202020202 & 0x505050505050505), + Bitboard(0x404040404040404 & 0xa0a0a0a0a0a0a0a), + Bitboard(0x808080808080808 & 0x1414141414141414), + Bitboard(0x1010101010101010 & 0x2828282828282828), + Bitboard(0x2020202020202020 & 0x5050505050505050), + Bitboard(0x4040404040404040 & 0xa0a0a0a0a0a0a0a0), + Bitboard(0x8080808080808080 & 0x4040404040404040), +]; + +#[cfg(feature = "classical")] +#[rustfmt::skip] +const PAWN: [i32; 64] = [ + s( 0, 0), s( 0, 0), s( 0, 0), s( 0, 0), s( 0, 0), s( 0, 0), s( 0, 0), s( 0, 0), + s( 98, 178), s(134, 173), s( 61, 158), s( 95, 134), s( 68, 147), s(126, 132), s(34, 165), s(-11, 187), + s( -6, 94), s( 7, 100), s( 26, 85), s( 31, 67), s( 65, 56), s( 56, 53), s(25, 82), s(-20, 84), + s(-14, 32), s( 13, 24), s( 6, 13), s( 21, 5), s( 23, -2), s( 12, 4), s(17, 17), s(-23, 17), + s(-27, 13), s( -2, 9), s( -5, -3), s( 12, -7), s( 17, -7), s( 6, -8), s(10, 3), s(-25, -1), + s(-26, 4), s( -4, 7), s( -4, -6), s(-10, 1), s( 3, 0), s( 3, -5), s(33, -1), s(-12, -8), + s(-35, 13), s( -1, 8), s(-20, 8), s(-23, 10), s(-15, 13), s( 24, 0), s(38, 2), s(-22, -7), + s( 0, 0), s( 0, 0), s( 0, 0), s( 0, 0), s( 0, 0), s( 0, 0), s( 0, 0), s( 0, 0), +]; + +#[cfg(feature = "classical")] +#[rustfmt::skip] +const KNIGHT: [i32; 64] = [ + s(-167, -58), s(-89, -38), s(-34, -13), s(-49, -28), s( 61, -31), s(-97, -27), s(-15, -63), s(-107, -99), + s( -73, -25), s(-41, -8), s( 72, -25), s( 36, -2), s( 23, -9), s( 62, -25), s( 7, -24), s( -17, -52), + s( -47, -24), s( 60, -20), s( 37, 10), s( 65, 9), s( 84, -1), s(129, -9), s( 73, -19), s( 44, -41), + s( -9, -17), s( 17, 3), s( 19, 22), s( 53, 22), s( 37, 22), s( 69, 11), s( 18, 8), s( 22, -18), + s( -13, -18), s( 4, -6), s( 16, 16), s( 13, 25), s( 28, 16), s( 19, 17), s( 21, 4), s( -8, -18), + s( -23, -23), s( -9, -3), s( 12, -1), s( 10, 15), s( 19, 10), s( 17, -3), s( 25, -20), s( -16, -22), + s( -29, -42), s(-53, -20), s(-12, -10), s( -3, -5), s( -1, -2), s( 18, -20), s(-14, -23), s( -19, -44), + s(-105, -29), s(-21, -51), s(-58, -23), s(-33, -15), s(-17, -22), s(-28, -18), s(-19, -50), s( -23, -64), +]; + +#[cfg(feature = "classical")] +#[rustfmt::skip] +const BISHOP: [i32; 64] = [ + s(-29, -14), s( 4, -21), s(-82, -11), s(-37, -8), s(-25, -7), s(-42, -9), s( 7, -17), s( -8, -24), + s(-26, -8), s(16, -4), s(-18, 7), s(-13, -12), s( 30, -3), s( 59, -13), s( 18, -4), s(-47, -14), + s(-16, 2), s(37, -8), s( 43, 0), s( 40, -1), s( 35, -2), s( 50, 6), s( 37, 0), s( -2, 4), + s( -4, -3), s( 5, 9), s( 19, 12), s( 50, 9), s( 37, 14), s( 37, 10), s( 7, 3), s( -2, 2), + s( -6, -6), s(13, 3), s( 13, 13), s( 26, 19), s( 34, 7), s( 12, 10), s( 10, -3), s( 4, -9), + s( 0, -12), s(15, -3), s( 15, 8), s( 15, 10), s( 14, 13), s( 27, 3), s( 18, -7), s( 10, -15), + s( 4, -14), s(15, -18), s( 16, -7), s( 0, -1), s( 7, 4), s( 21, -9), s( 33, -15), s( 1, -27), + s(-33, -23), s(-3, -9), s(-14, -23), s(-21, -5), s(-13, -9), s(-12, -16), s(-39, -5), s(-21, -17), +]; + +#[cfg(feature = "classical")] +#[rustfmt::skip] +const ROOK: [i32; 64] = [ + s( 32, 13), s( 42, 10), s( 32, 18), s( 51, 15), s(63, 12), s( 9, 12), s( 31, 8), s( 43, 5), + s( 27, 11), s( 32, 13), s( 58, 13), s( 62, 11), s(80, -3), s(67, 3), s( 26, 8), s( 44, 3), + s( -5, 7), s( 19, 7), s( 26, 7), s( 36, 5), s(17, 4), s(45, -3), s( 61, -5), s( 16, -3), + s(-24, 4), s(-11, 3), s( 7, 13), s( 26, 1), s(24, 2), s(35, 1), s( -8, -1), s(-20, 2), + s(-36, 3), s(-26, 5), s(-12, 8), s( -1, 4), s( 9, -5), s(-7, -6), s( 6, -8), s(-23, -11), + s(-45, -4), s(-25, 0), s(-16, -5), s(-17, -1), s( 3, -7), s( 0, -12), s( -5, -8), s(-33, -16), + s(-44, -6), s(-16, -6), s(-20, 0), s( -9, 2), s(-1, -9), s(11, -9), s( -6, -11), s(-71, -3), + s(-19, -9), s(-13, 2), s( 1, 3), s( 17, -1), s(16, -5), s( 7, -13), s(-37, 4), s(-26, -20), +]; + +#[cfg(feature = "classical")] +#[rustfmt::skip] +const QUEEN: [i32; 64] = [ + s(-28, -9), s( 0, 22), s( 29, 22), s( 12, 27), s( 59, 27), s( 44, 19), s( 43, 10), s( 45, 20), + s(-24, -17), s(-39, 20), s( -5, 32), s( 1, 41), s(-16, 58), s( 57, 25), s( 28, 30), s( 54, 0), + s(-13, -20), s(-17, 6), s( 7, 9), s( 8, 49), s( 29, 47), s( 56, 35), s( 47, 19), s( 57, 9), + s(-27, 3), s(-27, 22), s(-16, 24), s(-16, 45), s( -1, 57), s( 17, 40), s( -2, 57), s( 1, 36), + s( -9, -18), s(-26, 28), s( -9, 19), s(-10, 47), s( -2, 31), s( -4, 34), s( 3, 39), s( -3, 23), + s(-14, -16), s( 2, -27), s(-11, 15), s( -2, 6), s( -5, 9), s( 2, 17), s( 14, 10), s( 5, 5), + s(-35, -22), s( -8, -23), s( 11, -30), s( 2, -16), s( 8, -16), s( 15, -23), s( -3, -36), s( 1, -32), + s( -1, -33), s(-18, -28), s( -9, -22), s( 10, -43), s(-15, -5), s(-25, -32), s(-31, -20), s(-50, -41), +]; + +#[cfg(feature = "classical")] +#[rustfmt::skip] +const KING: [i32; 64] = [ + s(-65, -74), s( 23, -35), s( 16, -18), s(-15, -18), s(-56, -11), s(-34, 15), s( 2, 4), s( 13, -17), + s( 29, -12), s( -1, 17), s(-20, 14), s( -7, 17), s( -8, 17), s( -4, 38), s(-38, 23), s(-29, 11), + s( -9, 10), s( 24, 17), s( 2, 23), s(-16, 15), s(-20, 20), s( 6, 45), s( 22, 44), s(-22, 13), + s(-17, -8), s(-20, 22), s(-12, 24), s(-27, 27), s(-30, 26), s(-25, 33), s(-14, 26), s(-36, 3), + s(-49, -18), s( -1, -4), s(-27, 21), s(-39, 24), s(-46, 27), s(-44, 23), s(-33, 9), s(-51, -11), + s(-14, -19), s(-14, -3), s(-22, 11), s(-46, 21), s(-44, 23), s(-30, 16), s(-15, 7), s(-27, -9), + s( 1, -27), s( 7, -11), s( -8, 4), s(-64, 13), s(-43, 14), s(-16, 4), s( 9, -5), s( 8, -17), + s(-15, -53), s( 36, -34), s( 12, -21), s(-54, -11), s( 8, -28), s(-28, -14), s( 24, -24), s( 14, -43), +]; diff --git a/engine/src/lib.rs b/engine/src/lib.rs index f70c380..ca384d3 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -30,7 +30,9 @@ mod bound; mod config; mod eval; mod logger; +#[cfg(not(feature = "classical"))] mod nnue; +mod packing; mod search; mod time_control; mod uci; diff --git a/engine/src/main.rs b/engine/src/main.rs index 6a96506..b1f8637 100644 --- a/engine/src/main.rs +++ b/engine/src/main.rs @@ -21,15 +21,44 @@ mod bound; mod config; mod eval; mod logger; +#[cfg(not(feature = "classical"))] mod nnue; +mod packing; mod search; mod time_control; mod uci; +#[cfg(feature = "log")] +use logger::Logger; +#[cfg(feature = "log")] +use simplelog::*; +#[cfg(feature = "log")] +use std::fs::OpenOptions; + #[cfg(not(target_arch = "wasm32"))] pub fn main() { use uci::UciReader; + #[cfg(feature = "log")] + { + let mut log_path = env::temp_dir(); + log_path.push("pluto.log"); + + let file = OpenOptions::new() + .create(true) + .append(true) + .open(&log_path) + .expect("Unable to open log file"); + + WriteLogger::init(LevelFilter::Debug, Config::default(), file).unwrap(); + + std::panic::set_hook(Box::new(|panic_info| { + Logger::log(format!("{:?}", panic_info).as_str()); + log::error!("Panic occurred: {:?}", panic_info); + log::logger().flush(); + })); + } + let args: Vec = env::args().collect(); UciReader::default().run(args); } diff --git a/engine/src/packing.rs b/engine/src/packing.rs new file mode 100644 index 0000000..08496c8 --- /dev/null +++ b/engine/src/packing.rs @@ -0,0 +1,12 @@ +#[inline] +pub const fn s(mg: i32, eg: i32) -> i32 { + ((eg as u32) << 16) as i32 + mg +} + +pub const fn extract_mg(value: i32) -> i32 { + value as i16 as i32 +} + +pub const fn extract_eg(value: i32) -> i32 { + ((value + 0x8000) >> 16) as i16 as i32 +} diff --git a/engine/src/search/info.rs b/engine/src/search/info.rs index 0957d1b..8444e64 100644 --- a/engine/src/search/info.rs +++ b/engine/src/search/info.rs @@ -15,13 +15,8 @@ along with this program. If not, see . */ +#[derive(Default)] pub struct SearchInfo { pub nodes: u32, pub depth: u8, } - -impl Default for SearchInfo { - fn default() -> Self { - SearchInfo { nodes: 0, depth: 0 } - } -} diff --git a/engine/src/search/killers.rs b/engine/src/search/killers.rs index 96379fa..0dd9ee1 100644 --- a/engine/src/search/killers.rs +++ b/engine/src/search/killers.rs @@ -16,15 +16,16 @@ */ use shakmaty::Move; +use std::u8; pub struct Killers { - table: [Vec; 64], + table: Vec>, } impl Killers { pub fn new() -> Self { Self { - table: [const { Vec::new() }; 64], + table: vec![Vec::new(); u8::MAX as usize], } } @@ -33,13 +34,15 @@ impl Killers { } pub fn store(&mut self, ply: usize, m: Move) { - if ply >= 64 { + if ply >= self.table.len() { return; } - if !self.get(ply).contains(&m) { - self.table[ply].pop(); - self.table[ply].insert(0, m); + let killers = &mut self.table[ply]; + + if !killers.contains(&m) { + killers.pop(); + killers.insert(0, m); } } } diff --git a/engine/src/search/mod.rs b/engine/src/search/mod.rs index f3599cf..feda94f 100644 --- a/engine/src/search/mod.rs +++ b/engine/src/search/mod.rs @@ -30,18 +30,22 @@ use info::SearchInfo; use killers::Killers; use params::SearchParams; use pv::PvTable; -use shakmaty::{Chess, Position}; +use shakmaty::Chess; +#[cfg(not(feature = "classical"))] +use shakmaty::Position; use tt::TranspositionTable; -use crate::config::Config; +#[cfg(not(feature = "classical"))] +use crate::nnue::NNUEState; use crate::search::history_stack::HistoryStack; -use crate::{nnue::NNUEState, time_control::time_controller::TimeController}; +use crate::{config::Config, time_control::time_controller::TimeController}; pub struct SearchState { pub game: Chess, pub params: SearchParams, pub info: SearchInfo, pub tc: TimeController, + #[cfg(not(feature = "classical"))] pub nnue: NNUEState, pub tt: TranspositionTable, pub hstack: HistoryStack, @@ -61,6 +65,7 @@ impl SearchState { info: SearchInfo::default(), tc: TimeController::default(), params: SearchParams::default(), + #[cfg(not(feature = "classical"))] nnue: NNUEState::from_board(Chess::default().board()), hstack: HistoryStack::new(), pv: PvTable::default(), diff --git a/engine/src/search/move_picker.rs b/engine/src/search/move_picker.rs index 5c8cfde..4de65ab 100644 --- a/engine/src/search/move_picker.rs +++ b/engine/src/search/move_picker.rs @@ -19,25 +19,11 @@ use super::{tt::TranspositionTableEntry, SearchState}; use shakmaty::{Move, MoveList, Role}; const MO_FACTOR: i32 = 10000; +use std::cmp::Reverse; pub struct MovePicker<'a> { - order: Vec<&'a Move>, - curr: usize, -} - -impl<'a> Iterator for MovePicker<'a> { - type Item = &'a Move; - - fn next(&mut self) -> Option { - if self.curr >= self.order.len() { - return None; - } - - let move_ref = self.order[self.curr]; - self.curr += 1; - - Some(move_ref) - } + moves: Vec<(&'a Move, i32)>, + index: usize, } impl<'a> MovePicker<'a> { @@ -49,14 +35,15 @@ impl<'a> MovePicker<'a> { ) -> Self { let mut scored_moves: Vec<(&'a Move, i32)> = moves .iter() - .map(|m_ref| (m_ref, Self::move_importance(state, entry, ply, m_ref))) + .map(|m| (m, Self::move_importance(state, entry, ply, m))) .collect(); - scored_moves.sort_by_key(|&(_, score)| -score); - - let order: Vec<&'a Move> = scored_moves.into_iter().map(|(m_ref, _)| m_ref).collect(); + scored_moves.sort_by_key(|&(_, score)| Reverse(score)); - Self { order, curr: 0 } + Self { + moves: scored_moves, + index: 0, + } } fn move_importance( @@ -89,3 +76,18 @@ impl<'a> MovePicker<'a> { state.hist.get(m.role(), m.to()) } } + +impl<'a> Iterator for MovePicker<'a> { + type Item = &'a Move; + + fn next(&mut self) -> Option { + if self.index >= self.moves.len() { + return None; + } + + let next_move = self.moves[self.index].0; + self.index += 1; + Some(next_move) + } +} + diff --git a/engine/src/search/pv.rs b/engine/src/search/pv.rs index d518908..b36679c 100644 --- a/engine/src/search/pv.rs +++ b/engine/src/search/pv.rs @@ -16,26 +16,27 @@ */ use shakmaty::{CastlingMode, Move}; +use std::u8; pub struct PvTable { - pub length: [i32; 64], + pub length: Vec, pub table: Vec>>, } impl PvTable { pub fn default() -> PvTable { + const MAX_DEPTH: usize = u8::MAX as usize + 1; + PvTable { - length: [0; 64], - table: vec![vec![None; 64]; 64], + length: vec![0; MAX_DEPTH], + table: vec![vec![None; MAX_DEPTH]; MAX_DEPTH], } } -} -impl PvTable { pub fn store(&mut self, ply: usize, m: Move) { self.table[ply][ply] = Some(m); - for next_ply in ply as i32 + 1..self.length[ply + 1] { + for next_ply in (ply as i32 + 1)..self.length[ply + 1] { self.table[ply][next_ply as usize] = self.table[ply + 1][next_ply as usize].clone(); } diff --git a/engine/src/search/search.rs b/engine/src/search/search.rs index 6f997d8..a5f8f19 100644 --- a/engine/src/search/search.rs +++ b/engine/src/search/search.rs @@ -19,13 +19,16 @@ use std::sync::atomic::AtomicBool; use std::sync::Arc; use crate::bound::Bound; -use crate::eval::Eval; +use crate::eval::{Eval, PIECE_VALUES}; use crate::logger::Logger; -use crate::nnue::OFF; -use crate::nnue::ON; +#[cfg(not(feature = "classical"))] +use crate::nnue::{OFF, ON}; +use crate::packing::extract_mg; use crate::time_control::time_mode::TimeMode; use shakmaty::zobrist::{Zobrist64, ZobristHash}; -use shakmaty::{CastlingMode, CastlingSide, Chess, EnPassantMode, Move, Piece, Position, Square}; +use shakmaty::{CastlingMode, Chess, EnPassantMode, Move, Position}; +#[cfg(not(feature = "classical"))] +use shakmaty::{CastlingSide, Piece, Square}; use super::move_picker::MovePicker; use super::SearchState; @@ -42,71 +45,73 @@ impl Search { } pub fn make_move(&mut self, pos: &mut Chess, m: &Move, eval: i32) { - self.state.nnue.push(); - let turn = pos.turn(); - let board = pos.board(); - - match m { - Move::EnPassant { from, to } => { - let ep_target = Square::from_coords(to.file(), from.rank()); // captured pawn - - self.state - .nnue - .manual_update::((!pos.turn()).pawn(), ep_target); - } - - Move::Castle { king, rook } => { - let side = CastlingSide::from_queen_side(rook < king); - let rook_target = Square::from_coords(side.rook_to_file(), rook.rank()); - - self.state.nnue.move_update(turn.rook(), *rook, rook_target); - self.state.nnue.move_update( - Piece { - color: turn, - role: m.role(), - }, - m.from().unwrap(), - m.to(), - ); - } + #[cfg(not(feature = "classical"))] + { + self.state.nnue.push(); + let turn = pos.turn(); + let board = pos.board(); - Move::Normal { - role, - from, - capture: _capture, - to, - promotion, - } => { - if m.is_capture() { - let target_piece = board.piece_at(*to).unwrap(); - let target_square = *to; + match m { + Move::EnPassant { from, to } => { + let ep_target = Square::from_coords(to.file(), from.rank()); // captured pawn self.state .nnue - .manual_update::(target_piece, target_square); + .manual_update::((!pos.turn()).pawn(), ep_target); } - let piece = Piece { - color: turn, - role: *role, - }; + Move::Castle { king, rook } => { + let side = CastlingSide::from_queen_side(rook < king); + let rook_target = Square::from_coords(side.rook_to_file(), rook.rank()); + + self.state.nnue.move_update(turn.rook(), *rook, rook_target); + self.state.nnue.move_update( + Piece { + color: turn, + role: m.role(), + }, + m.from().unwrap(), + m.to(), + ); + } - if m.is_promotion() { - let promoted_piece = Piece { + Move::Normal { + role, + from, + capture: _capture, + to, + promotion, + } => { + if m.is_capture() { + let target_piece = board.piece_at(*to).unwrap(); + let target_square = *to; + + self.state + .nnue + .manual_update::(target_piece, target_square); + } + + let piece = Piece { color: turn, - role: promotion.unwrap(), + role: *role, }; - self.state.nnue.manual_update::(piece, *from); - self.state.nnue.manual_update::(promoted_piece, *to); - } else { - self.state.nnue.move_update(piece, *from, *to); + if m.is_promotion() { + let promoted_piece = Piece { + color: turn, + role: promotion.unwrap(), + }; + + self.state.nnue.manual_update::(piece, *from); + self.state.nnue.manual_update::(promoted_piece, *to); + } else { + self.state.nnue.move_update(piece, *from, *to); + } } - } - _ => {} + _ => {} + } } - pos.play_unchecked(m); self.state .hstack @@ -114,6 +119,7 @@ impl Search { } pub fn undo_move(&mut self) { + #[cfg(not(feature = "classical"))] self.state.nnue.pop(); self.state.hstack.pop(); } @@ -128,9 +134,12 @@ impl Search { self.state.tc.stop = stop.clone(); let mut best_move = None; + let mut alpha = -100000; + let mut beta = 100000; + let mut current_depth = 0; /* Iterative deepening */ - for current_depth in 0..self.state.params.depth { + while current_depth < self.state.params.depth { if TimeMode::is_finite(&self.state.tc.time_mode) && (self.state.tc.elapsed() * self.state.cfg.tc_elapsed_factor.value) as u128 > self.state.tc.play_time @@ -140,17 +149,26 @@ impl Search { self.state.info.depth = current_depth + 1; let pos = self.state.game.clone(); - let iteration_score = self.negamax(&pos, self.state.info.depth, -100000, 100000, 0); + let iteration_score = self.negamax(&pos, self.state.info.depth, alpha, beta, 0); if self.state.tc.is_time_up() { break; } - best_move = Some(self.state.pv.get_best_move().unwrap()); + best_move = self.state.pv.get_best_move(); let elapsed = self.state.tc.elapsed(); let pv = self.state.pv.collect(); + if iteration_score <= alpha || iteration_score >= beta { + alpha = -100000; + beta = 100000; + continue; + } + + alpha = iteration_score - 60; + beta = iteration_score + 60; + if print { Logger::log(&format!( "info depth {} nodes {} nps {} score cp {} time {} pv {}", @@ -162,6 +180,12 @@ impl Search { pv.join(" ") )); } + + current_depth += 1; + } + + if best_move.is_none() { + best_move = self.state.pv.get_best_move(); } if print { @@ -182,7 +206,7 @@ impl Search { ) -> i32 { self.state.pv.update_length(ply); - if self.state.tc.is_time_up() { + if self.state.info.nodes % 2048 == 0 && self.state.tc.is_time_up() { return 0; } @@ -208,22 +232,14 @@ impl Search { return entry.score; } + #[cfg(not(feature = "classical"))] let static_eval = Eval::nnue_eval(&self.state.nnue, pos); + #[cfg(feature = "classical")] + let static_eval = Eval::eval(pos); + /* Improving */ - let improving = match ply { - ply if ply < 2 => false, - _ => { - static_eval >= { - let e = self.state.hstack.get_eval(ply - 2); - if let Some(e) = e { - e - } else { - static_eval - } - } - } - }; + let improving = static_eval >= self.state.hstack.get_eval(ply - 2).unwrap_or(static_eval); /* Threefold Detection */ if ply > 0 && self.state.hstack.count_zobrist(position_key) >= 1 { @@ -401,25 +417,41 @@ impl Search { } fn quiesce(&mut self, pos: &Chess, mut alpha: i32, beta: i32, limit: u8) -> i32 { + if self.state.info.nodes % 2048 == 0 && self.state.tc.is_time_up() { + return 0; + } self.state.info.nodes += 1; + #[cfg(not(feature = "classical"))] let stand_pat = Eval::nnue_eval(&self.state.nnue, pos); + #[cfg(feature = "classical")] + let stand_pat = Eval::eval(pos); + if limit == 0 { return stand_pat; } + if stand_pat >= beta { return beta; } + if alpha < stand_pat { alpha = stand_pat; } let moves = pos.capture_moves(); + let mp = MovePicker::new(&moves, &self.state, &Default::default(), 0); + + for m in mp { + let captured_value = extract_mg(PIECE_VALUES[m.capture().unwrap() as usize - 1]); + + if stand_pat + captured_value + 200 < alpha { + continue; + } - for m in moves { let mut pos = pos.clone(); - self.make_move(&mut pos, &m, stand_pat); + self.make_move(&mut pos, m, stand_pat); let score = -self.quiesce(&pos, -beta, -alpha, limit - 1); self.undo_move(); diff --git a/engine/src/uci.rs b/engine/src/uci.rs index e3e6a98..0eb8806 100644 --- a/engine/src/uci.rs +++ b/engine/src/uci.rs @@ -22,6 +22,7 @@ use std::sync::Arc; use std::{io, thread}; use crate::logger::Logger; +#[cfg(not(feature = "classical"))] use crate::nnue::NNUEState; #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] use crate::postMessage; @@ -133,11 +134,13 @@ impl UciController { let scope = tokens.remove().unwrap(); match scope { + #[cfg(feature = "tuning")] "spsa" => self.handle_print_spsa(tokens), _ => Logger::log(&format!("unknown scope: {}", scope)), } } + #[cfg(feature = "tuning")] fn handle_print_spsa(&self, tokens: &mut Queue<&str>) { let target = tokens.remove().unwrap(); @@ -147,31 +150,13 @@ impl UciController { } } + #[cfg(feature = "tuning")] fn handle_print_spsa_workload(&self) { - Logger::log(&self.search.state.cfg.qsearch_depth.fmt_spsa()); - Logger::log(&self.search.state.cfg.rfp_depth.fmt_spsa()); - Logger::log(&self.search.state.cfg.rfp_base_margin.fmt_spsa()); - Logger::log(&self.search.state.cfg.rfp_reduction_improving.fmt_spsa()); - Logger::log(&self.search.state.cfg.fp_base_margin.fmt_spsa()); - Logger::log(&self.search.state.cfg.fp_depth_margin.fmt_spsa()); - Logger::log(&self.search.state.cfg.fp_margin_depth_factor.fmt_spsa()); - Logger::log(&self.search.state.cfg.nmp_depth.fmt_spsa()); - Logger::log(&self.search.state.cfg.nmp_margin.fmt_spsa()); - Logger::log(&self.search.state.cfg.nmp_divisor.fmt_spsa()); - Logger::log(&self.search.state.cfg.nmp_divisor_improving.fmt_spsa()); - Logger::log(&self.search.state.cfg.lmp_move_margin.fmt_spsa()); - Logger::log(&self.search.state.cfg.lmp_depth_factor.fmt_spsa()); - Logger::log(&self.search.state.cfg.lmr_depth.fmt_spsa()); - Logger::log(&self.search.state.cfg.lmr_move_margin.fmt_spsa()); - Logger::log(&self.search.state.cfg.lmr_quiet_margin.fmt_spsa()); - Logger::log(&self.search.state.cfg.lmr_quiet_divisor.fmt_spsa()); - Logger::log(&self.search.state.cfg.lmr_base_margin.fmt_spsa()); - Logger::log(&self.search.state.cfg.lmr_base_divisor.fmt_spsa()); - Logger::log(&self.search.state.cfg.mo_tt_entry_value.fmt_spsa()); - Logger::log(&self.search.state.cfg.mo_capture_value.fmt_spsa()); - Logger::log(&self.search.state.cfg.mo_killer_value.fmt_spsa()); - Logger::log(&self.search.state.cfg.tc_time_divisor.fmt_spsa()); - Logger::log(&self.search.state.cfg.tc_elapsed_factor.fmt_spsa()); + self.search.state.cfg.all_spsa(); + + for line in self.search.state.cfg.all_spsa() { + println!("{}", line); + } } fn handle_bench(&mut self, stop: Arc) { @@ -192,9 +177,12 @@ impl UciController { let fen: Fen = position.parse().ok().unwrap(); let game = fen.into_position(CastlingMode::Standard).ok().unwrap(); - self.search.state.nnue = NNUEState::from_board(self.search.state.game.board()); + #[cfg(not(feature = "classical"))] + { + self.search.state.nnue = NNUEState::from_board(self.search.state.game.board()); + } self.search.state.game = game; - self.search.state.params.depth = 12; + self.search.state.params.depth = 10; self.search.state.tc.time_mode = TimeMode::Infinite; self.search.go(false, &stop); @@ -205,7 +193,11 @@ impl UciController { let elapsed = Local::now().timestamp_millis() - start_time; self.search.state.game = Chess::default(); - self.search.state.nnue = NNUEState::from_board(self.search.state.game.board()); + + #[cfg(not(feature = "classical"))] + { + self.search.state.nnue = NNUEState::from_board(self.search.state.game.board()); + } println!( "{} nodes {} nps", @@ -334,7 +326,10 @@ impl UciController { } } - self.search.state.nnue = NNUEState::from_board(self.search.state.game.board()); + #[cfg(not(feature = "classical"))] + { + self.search.state.nnue = NNUEState::from_board(self.search.state.game.board()); + } } fn handle_position_fen(&mut self, tokens: &mut Queue<&str>) { @@ -378,7 +373,10 @@ impl UciController { } } - self.search.state.nnue = NNUEState::from_board(self.search.state.game.board()); + #[cfg(not(feature = "classical"))] + { + self.search.state.nnue = NNUEState::from_board(self.search.state.game.board()); + } } fn handle_setoption(&mut self, tokens: &mut Queue<&str>) { @@ -405,70 +403,7 @@ impl UciController { self.search.state.tt = TranspositionTable::new(entries as usize); } - "QSearchDepth" => { - self.search.state.cfg.qsearch_depth.value = value.parse::().unwrap() - } - "RFPDepth" => self.search.state.cfg.rfp_depth.value = value.parse::().unwrap(), - "RFPBaseMargin" => { - self.search.state.cfg.rfp_base_margin.value = value.parse::().unwrap() - } - "RFPReductionImproving" => { - self.search.state.cfg.rfp_reduction_improving.value = value.parse::().unwrap() - } - "FPDepthMargin" => { - self.search.state.cfg.fp_depth_margin.value = value.parse::().unwrap() - } - "FPBaseMargin" => { - self.search.state.cfg.fp_base_margin.value = value.parse::().unwrap() - } - "FPMarginDepthFactor" => { - self.search.state.cfg.fp_margin_depth_factor.value = value.parse::().unwrap() - } - "NMPDepth" => self.search.state.cfg.nmp_depth.value = value.parse::().unwrap(), - "NMPMargin" => self.search.state.cfg.nmp_margin.value = value.parse::().unwrap(), - "NMPDivisor" => self.search.state.cfg.nmp_divisor.value = value.parse::().unwrap(), - "NMPDivisorImproving" => { - self.search.state.cfg.nmp_divisor_improving.value = value.parse::().unwrap() - } - "LMPMoveMargin" => { - self.search.state.cfg.lmp_move_margin.value = value.parse::().unwrap() - } - "LMPDepthFactor" => { - self.search.state.cfg.lmp_depth_factor.value = value.parse::().unwrap() - } - "LMRDepth" => self.search.state.cfg.lmr_depth.value = value.parse::().unwrap(), - "LMRMoveMargin" => { - self.search.state.cfg.lmr_move_margin.value = value.parse::().unwrap() - } - "LMRQuietMargin" => { - self.search.state.cfg.lmr_quiet_margin.value = value.parse::().unwrap() - } - "LMRQuietDivisor" => { - self.search.state.cfg.lmr_quiet_divisor.value = value.parse::().unwrap() - } - "LMRBaseMargin" => { - self.search.state.cfg.lmr_base_margin.value = value.parse::().unwrap() - } - "LMRBaseDivisor" => { - self.search.state.cfg.lmr_base_divisor.value = value.parse::().unwrap() - } - "MOTTEntryValue" => { - self.search.state.cfg.mo_tt_entry_value.value = value.parse::().unwrap() - } - "MOCaptureValue" => { - self.search.state.cfg.mo_capture_value.value = value.parse::().unwrap() - } - "MOKillerValue" => { - self.search.state.cfg.mo_killer_value.value = value.parse::().unwrap() - } - "TCTimeDivisor" => { - self.search.state.cfg.tc_time_divisor.value = value.parse::().unwrap() - } - "TCElapsedFactor" => { - self.search.state.cfg.tc_elapsed_factor.value = value.parse::().unwrap() - } - - _ => Logger::log(&format!("info string unknown option: {}", name)), + _ => self.search.state.cfg.set(name, value), } } @@ -495,35 +430,13 @@ impl UciController { Logger::log(r#"id name Pluto 1.0.1"#); Logger::log(r#"id author Lxdovic"#); - Logger::log(format!("{}", self.search.state.cfg.move_overhead).as_str()); - Logger::log(format!("{}", self.search.state.cfg.threads).as_str()); - Logger::log(format!("{}", self.search.state.cfg.hash).as_str()); - - // Values to tune - Logger::log(format!("{}", self.search.state.cfg.qsearch_depth).as_str()); - Logger::log(format!("{}", self.search.state.cfg.rfp_depth).as_str()); - Logger::log(format!("{}", self.search.state.cfg.rfp_base_margin).as_str()); - Logger::log(format!("{}", self.search.state.cfg.rfp_reduction_improving).as_str()); - Logger::log(format!("{}", self.search.state.cfg.fp_base_margin).as_str()); - Logger::log(format!("{}", self.search.state.cfg.fp_depth_margin).as_str()); - Logger::log(format!("{}", self.search.state.cfg.fp_margin_depth_factor).as_str()); - Logger::log(format!("{}", self.search.state.cfg.nmp_depth).as_str()); - Logger::log(format!("{}", self.search.state.cfg.nmp_margin).as_str()); - Logger::log(format!("{}", self.search.state.cfg.nmp_divisor).as_str()); - Logger::log(format!("{}", self.search.state.cfg.nmp_divisor_improving).as_str()); - Logger::log(format!("{}", self.search.state.cfg.lmp_move_margin).as_str()); - Logger::log(format!("{}", self.search.state.cfg.lmp_depth_factor).as_str()); - Logger::log(format!("{}", self.search.state.cfg.lmr_depth).as_str()); - Logger::log(format!("{}", self.search.state.cfg.lmr_move_margin).as_str()); - Logger::log(format!("{}", self.search.state.cfg.lmr_quiet_margin).as_str()); - Logger::log(format!("{}", self.search.state.cfg.lmr_quiet_divisor).as_str()); - Logger::log(format!("{}", self.search.state.cfg.lmr_base_margin).as_str()); - Logger::log(format!("{}", self.search.state.cfg.lmr_base_divisor).as_str()); - Logger::log(format!("{}", self.search.state.cfg.mo_tt_entry_value).as_str()); - Logger::log(format!("{}", self.search.state.cfg.mo_capture_value).as_str()); - Logger::log(format!("{}", self.search.state.cfg.mo_killer_value).as_str()); - Logger::log(format!("{}", self.search.state.cfg.tc_time_divisor).as_str()); - Logger::log(format!("{}", self.search.state.cfg.tc_elapsed_factor).as_str()); + #[cfg(not(feature = "classical"))] + Logger::log(r#"info string using NNUE eval"#); + + #[cfg(feature = "classical")] + Logger::log(r#"info string using HCE eval"#); + + self.search.state.cfg.print_uci_options(); Logger::log(r#"uciok"#); }