├── .gitignore ├── engine ├── src │ ├── body │ │ ├── nnue │ │ │ ├── mod.rs │ │ │ ├── net │ │ │ │ ├── output_bias.bin │ │ │ │ ├── feature_bias.bin │ │ │ │ ├── output_weights.bin │ │ │ │ └── feature_weights.bin │ │ │ └── inference.rs │ │ ├── mod.rs │ │ ├── stat_vec.rs │ │ ├── lmr.rs │ │ ├── pv_table.rs │ │ ├── history.rs │ │ ├── movegen.rs │ │ ├── position.rs │ │ ├── tt.rs │ │ └── search.rs │ ├── uci │ │ ├── mod.rs │ │ ├── timeman.rs │ │ ├── bench.rs │ │ └── handler.rs │ ├── main.rs │ └── lib.rs ├── Cargo.toml └── Cargo.lock ├── .cargo └── config.toml ├── Cargo.toml ├── datagen ├── Cargo.toml └── src │ ├── main.rs │ ├── tables.rs │ └── script.rs ├── makefile ├── LICENSE ├── info ├── NETS.txt └── CHANGELOG.txt ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target -------------------------------------------------------------------------------- /engine/src/body/nnue/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod inference; 2 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = ['-C', 'target-cpu=native'] 3 | -------------------------------------------------------------------------------- /engine/src/uci/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod bench; 2 | pub mod handler; 3 | mod timeman; 4 | -------------------------------------------------------------------------------- /engine/src/body/nnue/net/output_bias.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1337crisis/svart/HEAD/engine/src/body/nnue/net/output_bias.bin -------------------------------------------------------------------------------- /engine/src/body/nnue/net/feature_bias.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1337crisis/svart/HEAD/engine/src/body/nnue/net/feature_bias.bin -------------------------------------------------------------------------------- /engine/src/body/nnue/net/output_weights.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1337crisis/svart/HEAD/engine/src/body/nnue/net/output_weights.bin -------------------------------------------------------------------------------- /engine/src/body/nnue/net/feature_weights.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1337crisis/svart/HEAD/engine/src/body/nnue/net/feature_weights.bin -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "engine", 4 | "datagen" 5 | ] 6 | 7 | [profile.release] 8 | lto = "fat" 9 | codegen-units = 1 10 | panic = "abort" 11 | -------------------------------------------------------------------------------- /engine/src/body/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod history; 2 | mod lmr; 3 | pub mod movegen; 4 | pub mod nnue; 5 | pub mod position; 6 | pub mod pv_table; 7 | pub mod search; 8 | mod stat_vec; 9 | pub mod tt; 10 | -------------------------------------------------------------------------------- /engine/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "engine" 3 | version = "0.1.0" 4 | edition = "2021" 5 | default-run = "engine" 6 | 7 | [dependencies] 8 | cozy-chess = "0.3.2" 9 | once_cell = "1.17.1" 10 | -------------------------------------------------------------------------------- /datagen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "datagen" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | cozy-chess = "0.3.2" 8 | chrono = "0.4.24" 9 | fastrand = "1.9.0" 10 | ctrlc = "3.2.5" 11 | tabled = "0.12.0" 12 | engine = { path = "../engine" } 13 | -------------------------------------------------------------------------------- /engine/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | #![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)] 3 | let binding = std::env::args().nth(1); 4 | let arg = binding.as_deref(); 5 | if arg == Some("bench") { 6 | engine::uci::bench::bench(); 7 | return; 8 | } 9 | 10 | engine::uci::handler::uci_loop(); 11 | } 12 | -------------------------------------------------------------------------------- /datagen/src/main.rs: -------------------------------------------------------------------------------- 1 | mod script; 2 | mod tables; 3 | 4 | fn main() { 5 | #![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)] 6 | let binding = std::env::args().nth(1); 7 | let arg = binding.as_deref(); 8 | match arg { 9 | Some("table") => { 10 | tables::print_net_history(); 11 | return; 12 | } 13 | Some("datagen") => { 14 | script::root().unwrap(); 15 | return; 16 | } 17 | _ => {} 18 | } 19 | 20 | println!("lol no args"); 21 | } 22 | -------------------------------------------------------------------------------- /engine/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod definitions { 2 | pub const MAX_PLY: usize = 128; 3 | pub const MAX_MOVES_POSITION: usize = 218; 4 | 5 | pub const NONE: i32 = 32002; 6 | pub const INFINITY: i32 = 32001; 7 | 8 | pub const MATE: i32 = 32000; 9 | pub const MATE_IN: i32 = MATE - MAX_PLY as i32; 10 | 11 | pub const TB_WIN: i32 = MATE_IN; 12 | pub const TB_WIN_IN_PLY: i32 = TB_WIN - MAX_PLY as i32; 13 | pub const TB_LOSS_IN_PLY: i32 = -TB_WIN_IN_PLY; 14 | 15 | pub const TIME_OVERHEAD: u64 = 5; 16 | 17 | pub const NOMOVE: u16 = 0b0000_0000_0000_0000; 18 | } 19 | 20 | pub mod body; 21 | pub mod uci; 22 | -------------------------------------------------------------------------------- /engine/src/body/stat_vec.rs: -------------------------------------------------------------------------------- 1 | pub struct StaticVec { 2 | data: [T; N], 3 | len: usize, 4 | } 5 | 6 | impl StaticVec { 7 | pub fn new(default: T) -> Self { 8 | Self { 9 | data: [default; N], 10 | len: 0, 11 | } 12 | } 13 | 14 | pub fn push(&mut self, item: T) { 15 | self.data[self.len] = item; 16 | self.len += 1; 17 | } 18 | 19 | pub fn len(&self) -> usize { 20 | self.len 21 | } 22 | 23 | #[must_use] 24 | pub fn as_slice(&self) -> &[T] { 25 | &self.data[..self.len] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /engine/src/body/lmr.rs: -------------------------------------------------------------------------------- 1 | const BASE_REDUCTION: f64 = 0.75; 2 | const DIVISOR: f64 = 2.25; 3 | 4 | pub struct LMRTable { 5 | pub table: [[i32; 64]; 64], 6 | } 7 | 8 | impl LMRTable { 9 | pub fn new() -> LMRTable { 10 | let mut lmr = LMRTable { 11 | table: [[0; 64]; 64], 12 | }; 13 | 14 | for d in 0..64 { 15 | for m in 0..64 { 16 | let ld = f64::ln(d as f64); 17 | let lp = f64::ln(m as f64); 18 | lmr.table[d][m] = (BASE_REDUCTION + ld * lp / DIVISOR) as i32; 19 | } 20 | } 21 | 22 | lmr 23 | } 24 | 25 | #[must_use] 26 | pub fn reduction(&self, depth: i32, move_count: i32) -> i32 { 27 | let d = (depth.min(63)) as usize; 28 | let c = (move_count.min(63)) as usize; 29 | 30 | self.table[d][c] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /engine/src/uci/timeman.rs: -------------------------------------------------------------------------------- 1 | use crate::definitions::TIME_OVERHEAD; 2 | 3 | #[must_use] 4 | pub fn time_for_move(time: u64, inc: u64, moves_to_go: Option) -> (u64, u64) { 5 | // Accounting for overhead 6 | let time = time - TIME_OVERHEAD; 7 | let opt_time: f64; 8 | let max_time: f64; 9 | 10 | // repeating TC 11 | if let Some(mtg) = moves_to_go { 12 | let mtg = mtg.min(50); 13 | let scale = 0.7 / f64::from(mtg); 14 | let eight = 0.8 * time as f64; 15 | 16 | opt_time = (scale * time as f64).min(eight); 17 | max_time = (5. * opt_time).min(eight); 18 | // normal TC 19 | } else { 20 | let temp = ((time / 20) + (inc * 3 / 4)) as f64; 21 | opt_time = 0.6 * temp; 22 | max_time = (temp * 2.).min(time as f64); 23 | } 24 | 25 | // The optimum time is used right after a depth is cleared in the ID loop. 26 | // Max time is used in the search function as usual for a global stop light. 27 | (opt_time as u64, max_time as u64) 28 | } 29 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | EXE = svart 2 | 3 | ifeq ($(OS),Windows_NT) 4 | NAME := $(EXE).exe 5 | V1NAME := $(EXE)-x86_64-win-v1.exe 6 | V2NAME := $(EXE)-x86_64-win-v2.exe 7 | V3NAME := $(EXE)-x86_64-win-v3.exe 8 | V4NAME := $(EXE)-x86_64-win-v4.exe 9 | else 10 | NAME := $(EXE) 11 | V1NAME := $(EXE)-x86_64-linux-v1 12 | V2NAME := $(EXE)-x86_64-linux-v2 13 | V3NAME := $(EXE)-x86_64-linux-v3 14 | V4NAME := $(EXE)-x86_64-linux-v4 15 | endif 16 | 17 | rule: 18 | cargo rustc --release -p engine --bin engine -- -C target-cpu=native --emit link=$(NAME) 19 | 20 | release: 21 | cargo rustc --release -p engine --bin engine -- -C target-cpu=x86-64 --emit link=$(V1NAME) 22 | cargo rustc --release -p engine --bin engine -- -C target-cpu=x86-64-v2 --emit link=$(V2NAME) 23 | cargo rustc --release -p engine --bin engine -- -C target-cpu=x86-64-v3 --emit link=$(V3NAME) 24 | cargo rustc --release -p engine --bin engine -- -C target-cpu=x86-64-v4 --emit link=$(V4NAME) 25 | 26 | data: 27 | cargo rustc --release -p datagen --bin datagen -- -C target-cpu=native 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 crippa1337 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/src/body/pv_table.rs: -------------------------------------------------------------------------------- 1 | use crate::definitions::MAX_PLY; 2 | use crate::uci::handler::reverse_castling_move; 3 | use cozy_chess::{Board, Move}; 4 | 5 | pub struct PVTable { 6 | pub length: usize, 7 | pub table: [Option; MAX_PLY], 8 | } 9 | 10 | impl PVTable { 11 | pub fn new() -> Self { 12 | PVTable { 13 | length: 0, 14 | table: [None; MAX_PLY], 15 | } 16 | } 17 | 18 | pub fn store(&mut self, board: &Board, mv: Move, old: &Self) { 19 | let mv = reverse_castling_move(board, mv); 20 | self.table[0] = Some(mv); 21 | self.table[1..=old.length].copy_from_slice(&old.table[..old.length]); 22 | self.length = old.length + 1; 23 | } 24 | 25 | pub fn moves(&self) -> &[Option] { 26 | &self.table[..self.length] 27 | } 28 | 29 | pub fn pv_string(&self) -> String { 30 | let mut pv = String::new(); 31 | for &mv in self.moves() { 32 | pv.push(' '); 33 | pv.push_str(mv.unwrap().to_string().as_str()); 34 | } 35 | 36 | pv 37 | } 38 | 39 | pub fn best_move(&self) -> Option { 40 | self.table[0] 41 | } 42 | } 43 | 44 | impl Default for PVTable { 45 | fn default() -> Self { 46 | Self::new() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /engine/src/body/history.rs: -------------------------------------------------------------------------------- 1 | use cozy_chess::{Board, Move}; 2 | 3 | pub const MAX_HISTORY: i32 = i16::MAX as i32; 4 | 5 | #[derive(Clone)] 6 | pub struct History { 7 | pub table: [[[i32; 64]; 64]; 2], 8 | } 9 | 10 | impl History { 11 | pub fn new() -> History { 12 | History { 13 | table: [[[0; 64]; 64]; 2], 14 | } 15 | } 16 | 17 | #[must_use] 18 | pub fn get_score(&self, board: &Board, mv: Move) -> i32 { 19 | let color = board.side_to_move() as usize; 20 | let from = mv.from as usize; 21 | let to = mv.to as usize; 22 | 23 | self.table[color][from][to] 24 | } 25 | 26 | pub fn update_table(&mut self, board: &Board, mv: Move, depth: i32) { 27 | let delta = (16 * (depth * depth)).min(1200); 28 | let bonus = if POSITIVE { delta } else { -delta }; 29 | 30 | self.update_score(board, mv, bonus); 31 | } 32 | 33 | pub fn update_score(&mut self, board: &Board, mv: Move, bonus: i32) { 34 | let scaled_bonus = bonus - self.get_score(board, mv) * bonus.abs() / MAX_HISTORY; 35 | 36 | let color = board.side_to_move() as usize; 37 | let from = mv.from as usize; 38 | let to = mv.to as usize; 39 | 40 | self.table[color][from][to] += scaled_bonus; 41 | } 42 | 43 | pub fn age_table(&mut self) { 44 | self.table 45 | .iter_mut() 46 | .flatten() 47 | .flatten() 48 | .for_each(|x| *x /= 2); 49 | } 50 | } 51 | 52 | impl Default for History { 53 | fn default() -> Self { 54 | Self::new() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /info/NETS.txt: -------------------------------------------------------------------------------- 1 | ╭───────────┬───────────────────────────────────────┬────────────────────────────────────────────────────────────────┬───────────────────┬──────────────────────────────╮ 2 | │ network │ training │ data │ report │ notes │ 3 | ├───────────┼───────────────────────────────────────┼────────────────────────────────────────────────────────────────┼───────────────────┼──────────────────────────────┤ 4 | │ svart0001 │ lr 0.01 epochs 45 drop at 30 wdl 0.3 │ 90M D7 fens self-play from UHO_XXL book at varying plies │ equal to hce │ how unremarkable │ 5 | │ svart0002 │ lr 0.01 epochs 30 wdl 0.1 │ -||- │ 98.96 +/- 26.44 │ │ 6 | │ svart0003 │ lr 0.01 epochs 80 drop at 30 wdl 0.3 │ -||- │ -63.23 +/- 97.40 │ │ 7 | │ svart0004 │ lr 0.01 epochs 80 drop at 30 wdl 0.1 │ 91M D8 fens generated internally with 12 random opening moves │ 401.50 +/- 41.91 │ there it is! │ 8 | │ svart0005 │ lr 0.01 epochs 45 drop at 30 wdl 0.25 │ 252M | 210M d8 and 40M 5kn │ 109.42 +- 26.52 │ RL looking great │ 9 | │ svart0006 │ lr 0.01 epochs 60 drop at 30 wdl 0.25 │ 410M fens | svart0005 data interleaved with 160M 5kn by Plutie │ 14.33 +- 8.27 │ hidden layer size 256 -> 512 │ 10 | │ svart0007 │ lr 0.01 epochs 60 drop at 30 wdl 0.25 │ -||- │ -0.22 +- 3.64 │ CReLu -> SCReLu │ 11 | ╰───────────┴───────────────────────────────────────┴────────────────────────────────────────────────────────────────┴───────────────────┴──────────────────────────────╯ 12 | -------------------------------------------------------------------------------- /datagen/src/tables.rs: -------------------------------------------------------------------------------- 1 | use tabled::{settings::Style, Table, Tabled}; 2 | 3 | #[derive(Tabled)] 4 | struct Net { 5 | network: &'static str, 6 | training: &'static str, 7 | data: &'static str, 8 | report: &'static str, 9 | notes: &'static str, 10 | } 11 | 12 | pub fn print_net_history() { 13 | let nets = vec![ 14 | Net { 15 | network: "svart0001", 16 | training: "lr 0.01 epochs 45 drop at 30 wdl 0.3", 17 | data: "90M D7 fens self-play from UHO_XXL book at varying plies", 18 | report: "equal to hce", 19 | notes: "how unremarkable", 20 | }, 21 | Net { 22 | network: "svart0002", 23 | training: "lr 0.01 epochs 30 wdl 0.1", 24 | data: "-||-", 25 | report: "98.96 +/- 26.44", 26 | notes: "", 27 | }, 28 | Net { 29 | network: "svart0003", 30 | training: "lr 0.01 epochs 80 drop at 30 wdl 0.3", 31 | data: "-||-", 32 | report: "-63.23 +/- 97.40", 33 | notes: "", 34 | }, 35 | Net { 36 | network: "svart0004", 37 | training: "lr 0.01 epochs 80 drop at 30 wdl 0.1", 38 | data: "91M D8 fens generated internally with 12 random opening moves", 39 | report: "401.50 +/- 41.91 ", 40 | notes: "there it is!", 41 | }, 42 | Net { 43 | network: "svart0005", 44 | training: "lr 0.01 epochs 45 drop at 30 wdl 0.25", 45 | data: "252M | 210M d8 and 40M 5kn", 46 | report: "109.42 +- 26.52 ", 47 | notes: "RL looking great", 48 | }, 49 | Net { 50 | network: "svart0006", 51 | training: "lr 0.01 epochs 60 drop at 30 wdl 0.25", 52 | data: "410M fens | svart0005 data interleaved with 160M 5kn by Plutie", 53 | report: "14.33 +- 8.27 ", 54 | notes: "hidden layer size 256 -> 512", 55 | }, 56 | Net { 57 | network: "svart0007", 58 | training: "lr 0.01 epochs 60 drop at 30 wdl 0.25", 59 | data: "-||-", 60 | report: "-0.22 +- 3.64 ", 61 | notes: "CReLu -> SCReLu", 62 | }, 63 | ]; 64 | 65 | let style = Style::rounded(); 66 | let table = Table::new(nets).with(style).to_string(); 67 | println!("{table}"); 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Svart 4 | [![License][license-badge]][license-link] 5 | [![Release][release-badge]][release-link] 6 | [![Commits][commits-badge]][commits-link] 7 | 8 |
9 | A free and open source UCI chess engine written in Rust. 10 | 11 | Svart is not a complete chess program and requires a [UCI-compatible graphical user interface](https://cutechess.com/) in order to be used graphically. 12 | 13 | 14 | # UCI Options 15 | ### Hash 16 | >``1 ≤ X ≤ 1000000`` 17 | > 18 | >Default 16 19 | > 20 | >Megabytes of memory allocated for the [Transposition Table](https://en.wikipedia.org/wiki/Transposition_table). 21 | 22 | 23 | 24 | ### Threads 25 | >``1 ≤ X ≤ 1024`` 26 | > 27 | >Default 1 28 | > 29 | >Amount of threads used, including the UCI handler. 30 | 31 | 32 | # History [OUTDATED] 33 | 34 | | Version | CCRL 40/15 | CCRL Blitz | MCERL | CEGT 4/40 | 35 | | --------- | -------------- | -------------- | ------------ | -------------- | 36 | | Svart 6 | 3187±23 [#71] | 3255±19 | | 3141±11 [#64] | 37 | | Svart 5 | 3171±19 | 3259±17 [#73] | 3229 [#93] | 3130±9 | 38 | | Svart 4 | 3043±21 | 3138±17 | 3147 [#119] | | 39 | | Svart 3.1 | 2883±21 | 2888±20 | 2921 [#169] | | 40 | | Svart 2 | 2462±24 | 2461±20 | 2528 [#226] | | 41 | 42 | 43 | # Compilation 44 | Compile Svart using [Cargo](https://doc.rust-lang.org/cargo/). 45 | 46 | ``` 47 | $ git clone https://github.com/1337crisis/svart 48 | $ cd svart 49 | $ make [rule / release / data] 50 | ``` 51 | 52 | 53 | # Releases 54 | Svart's release scheme follows the [microarchitecture levels](https://en.wikipedia.org/wiki/X86-64#Microarchitecture_levels). 55 | 56 | **x86_64-v1** is the slowest but compatible with almost anything.
57 | **x86_64-v2** is faster but is compatible with CPUs pre-Haswell/Excavator.
58 | **x86_64-v3** is faster still and recommended on modern systems.
59 | **x86_64-v4** is the fastest but requires AVX-512 support. 60 | 61 | 62 | [commits-badge]:https://img.shields.io/github/commits-since/1337crisis/svart/latest?style=for-the-badge 63 | [commits-link]:https://github.com/1337crisis/svart/commits/master 64 | [release-badge]:https://img.shields.io/github/v/release/1337crisis/svart?style=for-the-badge&label=official%20release 65 | [release-link]:https://github.com/1337crisis/svart/releases/latest 66 | [license-badge]:https://img.shields.io/github/license/1337crisis/svart?style=for-the-badge&label=license&color=success 67 | [license-link]:https://github.com/1337crisis/svart/blob/master/LICENSE 68 | -------------------------------------------------------------------------------- /engine/src/uci/bench.rs: -------------------------------------------------------------------------------- 1 | use std::time::Instant; 2 | 3 | use super::handler::SearchType; 4 | use crate::body::{nnue::inference::NNUEState, search::Search, tt::TT}; 5 | use cozy_chess::Board; 6 | 7 | const FENS: [&str; 62] = [ 8 | "r3k2r/2pb1ppp/2pp1q2/p7/1nP1B3/1P2P3/P2N1PPP/R2QK2R w KQkq a6 0 14", 9 | "4rrk1/2p1b1p1/p1p3q1/4p3/2P2n1p/1P1NR2P/PB3PP1/3R1QK1 b - - 2 24", 10 | "r3qbrk/6p1/2b2pPp/p3pP1Q/PpPpP2P/3P1B2/2PB3K/R5R1 w - - 16 42", 11 | "6k1/1R3p2/6p1/2Bp3p/3P2q1/P7/1P2rQ1K/5R2 b - - 4 44", 12 | "8/8/1p2k1p1/3p3p/1p1P1P1P/1P2PK2/8/8 w - - 3 54", 13 | "7r/2p3k1/1p1p1qp1/1P1Bp3/p1P2r1P/P7/4R3/Q4RK1 w - - 0 36", 14 | "r1bq1rk1/pp2b1pp/n1pp1n2/3P1p2/2P1p3/2N1P2N/PP2BPPP/R1BQ1RK1 b - - 2 10", 15 | "3r3k/2r4p/1p1b3q/p4P2/P2Pp3/1B2P3/3BQ1RP/6K1 w - - 3 87", 16 | "2r4r/1p4k1/1Pnp4/3Qb1pq/8/4BpPp/5P2/2RR1BK1 w - - 0 42", 17 | "4q1bk/6b1/7p/p1p4p/PNPpP2P/KN4P1/3Q4/4R3 b - - 0 37", 18 | "2q3r1/1r2pk2/pp3pp1/2pP3p/P1Pb1BbP/1P4Q1/R3NPP1/4R1K1 w - - 2 34", 19 | "1r2r2k/1b4q1/pp5p/2pPp1p1/P3Pn2/1P1B1Q1P/2R3P1/4BR1K b - - 1 37", 20 | "r3kbbr/pp1n1p1P/3ppnp1/q5N1/1P1pP3/P1N1B3/2P1QP2/R3KB1R b KQkq b3 0 17", 21 | "8/6pk/2b1Rp2/3r4/1R1B2PP/P5K1/8/2r5 b - - 16 42", 22 | "1r4k1/4ppb1/2n1b1qp/pB4p1/1n1BP1P1/7P/2PNQPK1/3RN3 w - - 8 29", 23 | "8/p2B4/PkP5/4p1pK/4Pb1p/5P2/8/8 w - - 29 68", 24 | "3r4/ppq1ppkp/4bnp1/2pN4/2P1P3/1P4P1/PQ3PBP/R4K2 b - - 2 20", 25 | "5rr1/4n2k/4q2P/P1P2n2/3B1p2/4pP2/2N1P3/1RR1K2Q w - - 1 49", 26 | "1r5k/2pq2p1/3p3p/p1pP4/4QP2/PP1R3P/6PK/8 w - - 1 51", 27 | "q5k1/5ppp/1r3bn1/1B6/P1N2P2/BQ2P1P1/5K1P/8 b - - 2 34", 28 | "r1b2k1r/5n2/p4q2/1ppn1Pp1/3pp1p1/NP2P3/P1PPBK2/1RQN2R1 w - - 0 22", 29 | "r1bqk2r/pppp1ppp/5n2/4b3/4P3/P1N5/1PP2PPP/R1BQKB1R w KQkq - 0 5", 30 | "r1bqr1k1/pp1p1ppp/2p5/8/3N1Q2/P2BB3/1PP2PPP/R3K2n b Q - 1 12", 31 | "r1bq2k1/p4r1p/1pp2pp1/3p4/1P1B3Q/P2B1N2/2P3PP/4R1K1 b - - 2 19", 32 | "r4qk1/6r1/1p4p1/2ppBbN1/1p5Q/P7/2P3PP/5RK1 w - - 2 25", 33 | "r7/6k1/1p6/2pp1p2/7Q/8/p1P2K1P/8 w - - 0 32", 34 | "r3k2r/ppp1pp1p/2nqb1pn/3p4/4P3/2PP4/PP1NBPPP/R2QK1NR w KQkq - 1 5", 35 | "3r1rk1/1pp1pn1p/p1n1q1p1/3p4/Q3P3/2P5/PP1NBPPP/4RRK1 w - - 0 12", 36 | "5rk1/1pp1pn1p/p3Brp1/8/1n6/5N2/PP3PPP/2R2RK1 w - - 2 20", 37 | "8/1p2pk1p/p1p1r1p1/3n4/8/5R2/PP3PPP/4R1K1 b - - 3 27", 38 | "8/4pk2/1p1r2p1/p1p4p/Pn5P/3R4/1P3PP1/4RK2 w - - 1 33", 39 | "8/5k2/1pnrp1p1/p1p4p/P6P/4R1PK/1P3P2/4R3 b - - 1 38", 40 | "8/8/1p1kp1p1/p1pr1n1p/P6P/1R4P1/1P3PK1/1R6 b - - 15 45", 41 | "8/8/1p1k2p1/p1prp2p/P2n3P/6P1/1P1R1PK1/4R3 b - - 5 49", 42 | "8/8/1p4p1/p1p2k1p/P2npP1P/4K1P1/1P6/3R4 w - - 6 54", 43 | "8/8/1p4p1/p1p2k1p/P2n1P1P/4K1P1/1P6/6R1 b - - 6 59", 44 | "8/5k2/1p4p1/p1pK3p/P2n1P1P/6P1/1P6/4R3 b - - 14 63", 45 | "8/1R6/1p1K1kp1/p6p/P1p2P1P/6P1/1Pn5/8 w - - 0 67", 46 | "1rb1rn1k/p3q1bp/2p3p1/2p1p3/2P1P2N/PP1RQNP1/1B3P2/4R1K1 b - - 4 23", 47 | "4rrk1/pp1n1pp1/q5p1/P1pP4/2n3P1/7P/1P3PB1/R1BQ1RK1 w - - 3 22", 48 | "r2qr1k1/pb1nbppp/1pn1p3/2ppP3/3P4/2PB1NN1/PP3PPP/R1BQR1K1 w - - 4 12", 49 | "2r2k2/8/4P1R1/1p6/8/P4K1N/7b/2B5 b - - 0 55", 50 | "6k1/5pp1/8/2bKP2P/2P5/p4PNb/B7/8 b - - 1 44", 51 | "2rqr1k1/1p3p1p/p2p2p1/P1nPb3/2B1P3/5P2/1PQ2NPP/R1R4K w - - 3 25", 52 | "r1b2rk1/p1q1ppbp/6p1/2Q5/8/4BP2/PPP3PP/2KR1B1R b - - 2 14", 53 | "6r1/5k2/p1b1r2p/1pB1p1p1/1Pp3PP/2P1R1K1/2P2P2/3R4 w - - 1 36", 54 | "rnbqkb1r/pppppppp/5n2/8/2PP4/8/PP2PPPP/RNBQKBNR b KQkq c3 0 2", 55 | "2rr2k1/1p4bp/p1q1p1p1/4Pp1n/2PB4/1PN3P1/P3Q2P/2RR2K1 w - f6 0 20", 56 | "3br1k1/p1pn3p/1p3n2/5pNq/2P1p3/1PN3PP/P2Q1PB1/4R1K1 w - - 0 23", 57 | "2r2b2/5p2/5k2/p1r1pP2/P2pB3/1P3P2/K1P3R1/7R w - - 23 93", 58 | "5k2/4q1p1/3P1pQb/1p1B4/pP5p/P1PR4/5PP1/1K6 b - - 0 38", 59 | "6k1/6p1/8/6KQ/1r6/q2b4/8/8 w - - 0 32", 60 | "5rk1/1rP3pp/p4n2/3Pp3/1P2Pq2/2Q4P/P5P1/R3R1K1 b - - 0 32", 61 | "4r1k1/4r1p1/8/p2R1P1K/5P1P/1QP3q1/1P6/3R4 b - - 0 1", 62 | "R4r2/4q1k1/2p1bb1p/2n2B1Q/1N2pP2/1r2P3/1P5P/2B2KNR w - - 3 31", 63 | "r6k/pbR5/1p2qn1p/P2pPr2/4n2Q/1P2RN1P/5PBK/8 w - - 2 31", 64 | "rn2k3/4r1b1/pp1p1n2/1P1q1p1p/3P4/P3P1RP/1BQN1PR1/1K6 w - - 6 28", 65 | "3q1k2/3P1rb1/p6r/1p2Rp2/1P5p/P1N2pP1/5B1P/3QRK2 w - - 1 42", 66 | "4r2k/1p3rbp/2p1N1p1/p3n3/P2NB1nq/1P6/4R1P1/B1Q2RK1 b - - 4 32", 67 | "4r1k1/1q1r3p/2bPNb2/1p1R3Q/pB3p2/n5P1/6B1/4R1K1 w - - 2 36", 68 | "3qr2k/1p3rbp/2p3p1/p7/P2pBNn1/1P3n2/6P1/B1Q1RR1K b - - 1 30", 69 | "3qk1b1/1p4r1/1n4r1/2P1b2B/p3N2p/P2Q3P/8/1R3R1K w - - 2 39", 70 | ]; 71 | 72 | pub fn bench() { 73 | let mut tt = TT::new(16); 74 | let b = Board::default(); 75 | let nnue = NNUEState::from_board(&b); 76 | let history = crate::body::history::History::new(); 77 | 78 | let mut search = Search::new(&tt, &nnue, &history, &vec![b.hash()]); 79 | let mut tot_nodes = 0; 80 | let mut tot_time = 0; 81 | 82 | for fen in FENS.iter() { 83 | let board = Board::from_fen(fen, false).unwrap(); 84 | search.nnue.refresh(&board); 85 | 86 | let timer = Instant::now(); 87 | search.iterative_deepening::(&board, SearchType::Depth(12), false); 88 | tot_time += timer.elapsed().as_millis(); 89 | tot_nodes += search.info.nodes; 90 | 91 | search.game_reset(); 92 | tt.reset(); 93 | search = Search::new(&tt, &nnue, &history, &vec![board.hash()]); 94 | } 95 | 96 | println!( 97 | "Bench: {tot_time} ms {tot_nodes} nodes {} nps", 98 | tot_nodes / (tot_time as u64 / 1000).max(1) 99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /engine/src/body/movegen.rs: -------------------------------------------------------------------------------- 1 | use super::{position::is_capture, search::Search}; 2 | use crate::definitions::INFINITY; 3 | use cozy_chess::{Board, Color, Move, Piece, Rank, Square}; 4 | 5 | #[derive(PartialEq)] 6 | pub struct MoveEntry { 7 | pub mv: Move, 8 | pub score: i32, 9 | } 10 | 11 | pub fn pure_moves(board: &Board) -> Vec { 12 | let mut move_list: Vec = Vec::new(); 13 | 14 | board.generate_moves(|moves| { 15 | move_list.extend(moves); 16 | false 17 | }); 18 | 19 | move_list 20 | } 21 | 22 | pub fn all_moves( 23 | search: &Search, 24 | board: &Board, 25 | tt_move: Option, 26 | ply: usize, 27 | ) -> Vec { 28 | let mut move_list: Vec = Vec::new(); 29 | 30 | board.generate_moves(|moves| { 31 | move_list.extend(moves); 32 | false 33 | }); 34 | 35 | let move_list: Vec = move_list 36 | .iter() 37 | .map(|mv| MoveEntry { 38 | mv: *mv, 39 | score: score_moves(search, board, *mv, tt_move, ply), 40 | }) 41 | .collect(); 42 | 43 | move_list 44 | } 45 | 46 | pub fn capture_moves( 47 | search: &Search, 48 | board: &Board, 49 | tt_move: Option, 50 | ply: usize, 51 | ) -> Vec { 52 | let enemy_pieces = board.colors(!board.side_to_move()); 53 | let mut captures_list: Vec = Vec::new(); 54 | 55 | // Assigns ep_square to the square that can be attacked 56 | let ep = board.en_passant(); 57 | let mut ep_square: Option = None; 58 | if let Some(ep) = ep { 59 | if board.side_to_move() == Color::White { 60 | ep_square = Some(Square::new(ep, Rank::Sixth)); 61 | } else { 62 | ep_square = Some(Square::new(ep, Rank::Third)); 63 | } 64 | } 65 | 66 | // Generates all moves and filters out the ones that are not captures 67 | board.generate_moves(|mut moves| { 68 | let mut permissible = enemy_pieces; 69 | if let Some(epsq) = ep_square { 70 | if moves.piece == Piece::Pawn { 71 | permissible |= epsq.bitboard(); 72 | } 73 | } 74 | moves.to &= permissible; 75 | captures_list.extend(moves); 76 | false 77 | }); 78 | 79 | // Assigns a score to each move based on MVV-LVA 80 | let captures_list: Vec = captures_list 81 | .iter() 82 | .map(|mv| MoveEntry { 83 | mv: *mv, 84 | score: score_moves(search, board, *mv, tt_move, ply), 85 | }) 86 | .collect(); 87 | 88 | captures_list 89 | } 90 | 91 | // Most Valuable Victim - Least Valuable Aggressor (MVV-LVA) 92 | #[must_use] 93 | pub fn mvvlva(board: &Board, mv: Move) -> i32 { 94 | #[rustfmt::skip] 95 | let mvvlva: [[i32; 7]; 7] = [ 96 | [0, 0, 0, 0, 0, 0, 0], 97 | [0, 105, 104, 103, 102, 101, 100], 98 | [0, 205, 204, 203, 202, 201, 200], 99 | [0, 305, 304, 303, 302, 301, 300], 100 | [0, 405, 404, 403, 402, 401, 400], 101 | [0, 505, 504, 503, 502, 501, 500], 102 | [0, 605, 604, 603, 602, 601, 600], 103 | ]; 104 | 105 | let from_square = mv.from; 106 | let to_square = mv.to; 107 | let attacker = piece_num_at(board, from_square); 108 | let mut victim = piece_num_at(board, to_square); 109 | 110 | // En Passant 111 | if victim == 0 { 112 | victim = 1 113 | } 114 | 115 | mvvlva[victim as usize][attacker as usize] 116 | } 117 | 118 | // Used to index MVV-LVA table 119 | #[must_use] 120 | pub fn piece_num_at(board: &Board, square: Square) -> i16 { 121 | let piece = board.piece_on(square); 122 | if piece.is_none() { 123 | return 0; 124 | } 125 | 126 | match piece.unwrap() { 127 | Piece::Pawn => 1, 128 | Piece::Knight => 2, 129 | Piece::Bishop => 3, 130 | Piece::Rook => 4, 131 | Piece::Queen => 5, 132 | Piece::King => 6, 133 | } 134 | } 135 | 136 | #[must_use] 137 | pub fn score_moves( 138 | search: &Search, 139 | board: &Board, 140 | mv: Move, 141 | tt_move: Option, 142 | ply: usize, 143 | ) -> i32 { 144 | if let Some(tmove) = tt_move { 145 | if mv == tmove { 146 | return INFINITY + 1_000_000; 147 | } 148 | } 149 | 150 | if mv.promotion.is_some() { 151 | return 310_000; 152 | } 153 | 154 | // Returns between 200100..200605 155 | if is_capture(board, mv) { 156 | return mvvlva(board, mv) + 200_000; 157 | } 158 | 159 | if search.info.killers[ply][0] == Some(mv) { 160 | return 100_000; 161 | } else if search.info.killers[ply][1] == Some(mv) { 162 | return 95_000; 163 | } 164 | 165 | search.info.history.get_score(board, mv) 166 | } 167 | 168 | pub struct Picker { 169 | moves: Vec, 170 | index: usize, 171 | } 172 | 173 | impl Picker { 174 | pub fn new(moves: Vec) -> Self { 175 | Self { moves, index: 0 } 176 | } 177 | 178 | pub fn pick_move(&mut self) -> Option { 179 | let open_list = &mut self.moves[self.index..]; 180 | let best_index = open_list 181 | .iter() 182 | .enumerate() 183 | .max_by_key(|(_, entry)| entry.score)? 184 | .0; 185 | self.index += 1; 186 | open_list.swap(0, best_index); 187 | Some(open_list[0].mv) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /engine/src/body/position.rs: -------------------------------------------------------------------------------- 1 | use super::nnue::inference::{NNUEState, ACTIVATE, DEACTIVATE}; 2 | use cozy_chess::{Board, File, Move, Piece, Rank, Square}; 3 | 4 | pub fn play_move(board: &mut Board, nnue: &mut Box, mv: Move) { 5 | nnue.push(); 6 | 7 | let stm = board.side_to_move(); 8 | let piece = board.piece_on(mv.from).unwrap(); 9 | 10 | // Remove the from-square piece 11 | nnue.update_feature::(mv.from, piece, stm); 12 | 13 | // Remove the target-square piece 14 | // This also handles the move of the rook in castling 15 | if let Some((color, p)) = board.color_on(mv.to).zip(board.piece_on(mv.to)) { 16 | nnue.update_feature::(mv.to, p, color); 17 | } 18 | 19 | // Remove the en passant'd pawn 20 | if let Some(ep_file) = board.en_passant() { 21 | if piece == Piece::Pawn && mv.to == Square::new(ep_file, Rank::Sixth.relative_to(stm)) { 22 | nnue.update_feature::( 23 | Square::new(ep_file, Rank::Fifth.relative_to(stm)), 24 | Piece::Pawn, 25 | !stm, 26 | ); 27 | } 28 | } 29 | 30 | // Castling 31 | if Some(stm) == board.color_on(mv.to) { 32 | let rank = Rank::First.relative_to(stm); 33 | // King side 34 | if mv.from.file() < mv.to.file() { 35 | // Move the rook 36 | nnue.update_feature::(Square::new(File::F, rank), Piece::Rook, stm); 37 | 38 | // Move the king 39 | nnue.update_feature::(Square::new(File::G, rank), Piece::King, stm); 40 | // Queen side 41 | } else { 42 | nnue.update_feature::(Square::new(File::D, rank), Piece::Rook, stm); 43 | nnue.update_feature::(Square::new(File::C, rank), Piece::King, stm); 44 | } 45 | } else { 46 | // The only thing left is to add the moved piece to it's target-square. 47 | // This also handles the promotion of a pawn 48 | let new_piece = mv.promotion.unwrap_or(piece); 49 | nnue.update_feature::(mv.to, new_piece, stm) 50 | } 51 | 52 | board.play_unchecked(mv); 53 | } 54 | 55 | #[must_use] 56 | pub fn is_ep(board: &Board, mv: Move) -> bool { 57 | let stm = board.side_to_move(); 58 | let piece = board.piece_on(mv.from).unwrap(); 59 | 60 | if piece == Piece::Pawn { 61 | if let Some(ep_file) = board.en_passant() { 62 | if mv.to == Square::new(ep_file, Rank::Sixth.relative_to(stm)) { 63 | return true; 64 | } 65 | } 66 | } 67 | 68 | false 69 | } 70 | 71 | #[must_use] 72 | pub fn is_capture(board: &Board, mv: Move) -> bool { 73 | board.colors(!board.side_to_move()).has(mv.to) || is_ep(board, mv) 74 | } 75 | 76 | #[must_use] 77 | pub fn is_quiet(board: &Board, mv: Move) -> bool { 78 | mv.promotion.is_none() && !is_capture(board, mv) 79 | } 80 | 81 | #[cfg(test)] 82 | mod tests { 83 | use crate::body::position::{is_capture, is_quiet}; 84 | 85 | #[test] 86 | fn quiet_moves() { 87 | use cozy_chess::{Board, Move, Piece, Square}; 88 | 89 | let board_1 = Board::from_fen( 90 | "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 91 | false, 92 | ) 93 | .unwrap(); 94 | let board_2 = Board::from_fen("8/1k6/1q3R2/8/2p5/5K2/1p6/8 w - - 0 1", false).unwrap(); 95 | let board_3 = Board::from_fen("2q5/1k5P/5n2/8/8/5K2/1p6/8 b - - 0 1", false).unwrap(); 96 | 97 | // Quiet pawn move 98 | let mv = Move { 99 | from: Square::A2, 100 | to: Square::A3, 101 | promotion: None, 102 | }; 103 | 104 | // Queen promotion 105 | let mv_1 = Move { 106 | from: Square::H7, 107 | to: Square::H8, 108 | promotion: Some(Piece::Queen), 109 | }; 110 | 111 | // Quiet rook move 112 | let mv_2 = Move { 113 | from: Square::F6, 114 | to: Square::F8, 115 | promotion: None, 116 | }; 117 | 118 | // Queen takes awn 119 | let mv_3 = Move { 120 | from: Square::E2, 121 | to: Square::B2, 122 | promotion: None, 123 | }; 124 | 125 | // Queen promotion 126 | let mv_4 = Move { 127 | from: Square::B2, 128 | to: Square::B1, 129 | promotion: Some(Piece::Queen), 130 | }; 131 | 132 | // Knight takes pawn 133 | let mv_5 = Move { 134 | from: Square::F6, 135 | to: Square::H7, 136 | promotion: None, 137 | }; 138 | 139 | // Quiet check 140 | let mv_6 = Move { 141 | from: Square::C8, 142 | to: Square::C3, 143 | promotion: None, 144 | }; 145 | 146 | assert!(is_quiet(&board_1, mv)); 147 | assert!(!is_quiet(&board_2, mv_1)); 148 | assert!(is_quiet(&board_2, mv_2)); 149 | assert!(!is_quiet(&board_2, mv_3)); 150 | assert!(!is_quiet(&board_3, mv_4)); 151 | assert!(!is_quiet(&board_3, mv_5)); 152 | assert!(is_quiet(&board_3, mv_6)); 153 | } 154 | 155 | #[test] 156 | fn capture_moves() { 157 | use cozy_chess::{Board, Move, Piece, Square}; 158 | let board_1 = Board::from_fen( 159 | "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 160 | false, 161 | ) 162 | .unwrap(); 163 | 164 | let board_2 = Board::from_fen("8/1k6/1q3R2/8/2p5/5K2/1p6/8 w - - 0 1", false).unwrap(); 165 | let board_3 = Board::from_fen("2q5/1k5P/5n2/8/8/5K2/1p6/8 b - - 0 1", false).unwrap(); 166 | 167 | // Quiet pawn push 168 | let mv = Move { 169 | from: Square::A2, 170 | to: Square::A3, 171 | promotion: None, 172 | }; 173 | 174 | // Rook takes queen 175 | let mv_1 = Move { 176 | from: Square::F6, 177 | to: Square::B6, 178 | promotion: None, 179 | }; 180 | 181 | // Quiet king move 182 | let mv_2 = Move { 183 | from: Square::F3, 184 | to: Square::E2, 185 | promotion: None, 186 | }; 187 | 188 | // Queen promotion 189 | let mv_3 = Move { 190 | from: Square::B2, 191 | to: Square::B1, 192 | promotion: Some(Piece::Queen), 193 | }; 194 | 195 | // Knight takes pawn 196 | let mv_4 = Move { 197 | from: Square::F6, 198 | to: Square::H7, 199 | promotion: None, 200 | }; 201 | 202 | assert!(!is_capture(&board_1, mv)); 203 | assert!(is_capture(&board_2, mv_1)); 204 | assert!(!is_capture(&board_3, mv_2)); 205 | assert!(!is_capture(&board_3, mv_3)); 206 | assert!(is_capture(&board_3, mv_4)); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /engine/src/body/nnue/inference.rs: -------------------------------------------------------------------------------- 1 | // Svart uses a 768->512x2->1 perspective NNUE, largely inspired by Viridithas and Carp. 2 | // A huge thanks to Cosmo and Dede for their help with the implementation. 3 | // 4 | // I hope to further improve the network as well as make the code more original in the future. 5 | use crate::definitions::MAX_PLY; 6 | use cozy_chess::{Board, Color, Piece, Square}; 7 | 8 | const FEATURES: usize = 768; 9 | const HIDDEN: usize = 512; 10 | 11 | // clipped relu bounds 12 | const CR_MIN: i16 = 0; 13 | const CR_MAX: i16 = 255; 14 | 15 | // quantization 16 | const QAB: i32 = 255 * 64; 17 | const SCALE: i32 = 400; 18 | 19 | pub const ACTIVATE: bool = true; 20 | pub const DEACTIVATE: bool = false; 21 | 22 | struct Parameters { 23 | feature_weights: [i16; FEATURES * HIDDEN], 24 | feature_bias: [i16; HIDDEN], 25 | output_weights: [i16; HIDDEN * 2], // perspective aware 26 | output_bias: i16, 27 | } 28 | 29 | // the model is read from binary files at compile time 30 | static MODEL: Parameters = Parameters { 31 | feature_weights: unsafe { std::mem::transmute(*include_bytes!("net/feature_weights.bin")) }, 32 | feature_bias: unsafe { std::mem::transmute(*include_bytes!("net/feature_bias.bin")) }, 33 | output_weights: unsafe { std::mem::transmute(*include_bytes!("net/output_weights.bin")) }, 34 | output_bias: unsafe { std::mem::transmute(*include_bytes!("net/output_bias.bin")) }, 35 | }; 36 | 37 | #[derive(Clone)] 38 | pub struct NNUEState { 39 | pub accumulators: [Accumulator; MAX_PLY], 40 | pub current_acc: usize, 41 | } 42 | 43 | // The accumulator represents the 44 | // hidden layer from both perspectives 45 | #[derive(Clone, Copy, PartialEq, Debug)] 46 | pub struct Accumulator { 47 | white: [i16; HIDDEN], 48 | black: [i16; HIDDEN], 49 | } 50 | 51 | impl Default for Accumulator { 52 | fn default() -> Self { 53 | Self { 54 | white: MODEL.feature_bias, 55 | black: MODEL.feature_bias, 56 | } 57 | } 58 | } 59 | 60 | impl Accumulator { 61 | // efficiently update the change of a feature 62 | fn efficiently_update(&mut self, idx: (usize, usize)) { 63 | fn update_perspective(acc: &mut [i16; HIDDEN], idx: usize) { 64 | // we iterate over the weights corresponding to the feature that has been changed 65 | // and then update the activations in the hidden layer accordingly 66 | let feature_weights = acc 67 | .iter_mut() 68 | // the column of the weight matrix corresponding to the index of the feature 69 | .zip(&MODEL.feature_weights[idx..idx + HIDDEN]); 70 | 71 | for (activation, &weight) in feature_weights { 72 | if ACTIVATE { 73 | *activation += weight; 74 | } else { 75 | *activation -= weight; 76 | } 77 | } 78 | } 79 | 80 | update_perspective::(&mut self.white, idx.0); 81 | update_perspective::(&mut self.black, idx.1); 82 | } 83 | } 84 | 85 | impl NNUEState { 86 | // Referencing Viridithas' implementation: 87 | // 88 | // The NNUEState is too large to be allocated on the stack at the time of writing, so we have to box it. 89 | // This is done by allocating the memory manually and then constructing the object in place. 90 | // Why not just box normally? Because rustc in debug mode will first allocate on the stack 91 | // before moving it to the heap when boxxing, which would blow the stack. 92 | pub fn from_board(board: &Board) -> Box { 93 | let mut boxed: Box = unsafe { 94 | let layout = std::alloc::Layout::new::(); 95 | let ptr = std::alloc::alloc_zeroed(layout); 96 | if ptr.is_null() { 97 | std::alloc::handle_alloc_error(layout); 98 | } 99 | Box::from_raw(ptr.cast()) 100 | }; 101 | 102 | // initialize the first state 103 | boxed.accumulators[0] = Accumulator::default(); 104 | for sq in board.occupied() { 105 | let piece = board.piece_on(sq).unwrap(); 106 | let color = board.color_on(sq).unwrap(); 107 | let idx = weight_column_index(sq, piece, color); 108 | 109 | boxed.accumulators[0].efficiently_update::(idx); 110 | } 111 | 112 | boxed 113 | } 114 | 115 | pub fn refresh(&mut self, board: &Board) { 116 | // reset the accumulator stack 117 | self.current_acc = 0; 118 | self.accumulators[self.current_acc] = Accumulator::default(); 119 | 120 | // update the first accumulator 121 | for sq in board.occupied() { 122 | let piece = board.piece_on(sq).unwrap(); 123 | let color = board.color_on(sq).unwrap(); 124 | let idx = weight_column_index(sq, piece, color); 125 | 126 | self.accumulators[self.current_acc].efficiently_update::(idx); 127 | } 128 | } 129 | 130 | /// Copy and push the current accumulator to the "top" 131 | pub fn push(&mut self) { 132 | self.accumulators[self.current_acc + 1] = self.accumulators[self.current_acc]; 133 | self.current_acc += 1; 134 | } 135 | 136 | pub fn pop(&mut self) { 137 | self.current_acc -= 1; 138 | } 139 | 140 | pub fn update_feature(&mut self, sq: Square, piece: Piece, color: Color) { 141 | let idx = weight_column_index(sq, piece, color); 142 | 143 | self.accumulators[self.current_acc].efficiently_update::(idx); 144 | } 145 | 146 | pub fn evaluate(&self, stm: Color) -> i32 { 147 | let acc = &self.accumulators[self.current_acc]; 148 | 149 | let (us, them) = match stm { 150 | Color::White => (acc.white.iter(), acc.black.iter()), 151 | Color::Black => (acc.black.iter(), acc.white.iter()), 152 | }; 153 | 154 | // Add on the bias 155 | let mut output = MODEL.output_bias as i32; 156 | 157 | // Add on the activations from one perspective with clipped ReLU 158 | for (&value, &weight) in us.zip(&MODEL.output_weights[..HIDDEN]) { 159 | output += (value.clamp(CR_MIN, CR_MAX) as i32) * (weight as i32); 160 | } 161 | 162 | // ... other perspective 163 | for (&value, &weight) in them.zip(&MODEL.output_weights[HIDDEN..]) { 164 | output += (value.clamp(CR_MIN, CR_MAX) as i32) * (weight as i32); 165 | } 166 | 167 | // Quantization 168 | output * SCALE / QAB 169 | } 170 | } 171 | 172 | // Returns white's and black's feature weight index respectively 173 | // i.e where the feature's weight column is in the weight matrix. 174 | #[must_use] 175 | fn weight_column_index(sq: Square, piece: Piece, color: Color) -> (usize, usize) { 176 | // The jump from one perspective to the other 177 | const COLOR_STRIDE: usize = 64 * 6; 178 | // The jump from one piece type to the next 179 | const PIECE_STRIDE: usize = 64; 180 | let p = match piece { 181 | Piece::Pawn => 0, 182 | Piece::Knight => 1, 183 | Piece::Bishop => 2, 184 | Piece::Rook => 3, 185 | Piece::Queen => 4, 186 | Piece::King => 5, 187 | }; 188 | 189 | let c = color as usize; 190 | 191 | let white_idx = c * COLOR_STRIDE + p * PIECE_STRIDE + sq as usize; 192 | let black_idx = (1 ^ c) * COLOR_STRIDE + p * PIECE_STRIDE + sq.flip_rank() as usize; 193 | 194 | (white_idx * HIDDEN, black_idx * HIDDEN) 195 | } 196 | 197 | #[cfg(test)] 198 | mod tests { 199 | use super::*; 200 | use crate::body::{movegen, position::play_move, search::Search, tt::TT}; 201 | 202 | #[test] 203 | fn nnue_update_feature() { 204 | let board: Board = Board::default(); 205 | let mut state = NNUEState::from_board(&board); 206 | 207 | let old_acc = state.accumulators[0]; 208 | 209 | state.update_feature::(Square::A3, Piece::Pawn, Color::White); 210 | state.update_feature::(Square::A3, Piece::Pawn, Color::White); 211 | 212 | assert_eq!(old_acc, state.accumulators[0]); 213 | } 214 | 215 | #[test] 216 | fn nnue_moves() { 217 | let board = Board::default(); 218 | let tt = TT::new(16); 219 | let nnue = NNUEState::from_board(&board); 220 | let history = crate::body::history::History::new(); 221 | 222 | let mut search = Search::new(&tt, &nnue, &history, &vec![board.hash()]); 223 | 224 | let moves = movegen::all_moves(&search, &board, None, 0); 225 | let initial_white = search.nnue.accumulators[0].white; 226 | let initial_black = search.nnue.accumulators[0].black; 227 | 228 | for mv in moves { 229 | let mv = mv.mv; 230 | let mut new_b = board.clone(); 231 | play_move(&mut new_b, &mut search.nnue, mv); 232 | assert_ne!(initial_white, search.nnue.accumulators[1].white); 233 | assert_ne!(initial_black, search.nnue.accumulators[1].black); 234 | search.nnue.pop(); 235 | assert_eq!(initial_white, search.nnue.accumulators[0].white); 236 | assert_eq!(initial_black, search.nnue.accumulators[0].black); 237 | } 238 | } 239 | 240 | #[test] 241 | fn nnue_incremental() { 242 | let fens: [&str; 13] = [ 243 | "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 244 | "4r1k1/4r1p1/8/p2R1P1K/5P1P/1QP3q1/1P6/3R4 b - - 0 1", 245 | "1n2k3/r3r1bn/pp1p4/1P1q1p1p/3P4/P3P1RP/1BQN1PR1/1K6 w - - 6 28", 246 | "8/3r1b2/3r1Pk1/1N6/5ppP/1q2P1Q1/7K/4RB2 w - - 0 66", 247 | "rnbqkbnr/1pp1ppp1/p7/2PpP2p/8/8/PP1P1PPP/RNBQKBNR w KQkq d6 0 5", 248 | "rnbqkbnr/1pp1p3/p4pp1/2PpP2p/8/3B1N2/PP1P1PPP/RNBQK2R w KQkq - 0 7", 249 | "rnbqk2r/1pp1p1P1/p4np1/2Pp3p/8/3B1N2/PP1P1PPP/RNBQK2R w KQkq - 1 9", 250 | "rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2", 251 | "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1", 252 | "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1", 253 | "rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8", 254 | "r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10", 255 | "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1", 256 | ]; 257 | 258 | for fen in fens { 259 | let mut board = Board::from_fen(fen, false).unwrap(); 260 | let tt = TT::new(16); 261 | let nnue = NNUEState::from_board(&board); 262 | let history = crate::body::history::History::new(); 263 | 264 | let mut search = Search::new(&tt, &nnue, &history, &vec![board.hash()]); 265 | search.nnue.refresh(&board); 266 | let moves = movegen::all_moves(&search, &board, None, 0); 267 | 268 | for mv in moves { 269 | let mut board2 = Board::from_fen(fen, false).unwrap(); 270 | 271 | board2.play_unchecked(mv.mv); 272 | play_move(&mut board, &mut search.nnue, mv.mv); 273 | 274 | let state2 = NNUEState::from_board(&board2); 275 | assert_eq!(search.nnue.accumulators[1], state2.accumulators[0]); 276 | assert_ne!(search.nnue.accumulators[0], state2.accumulators[0]); 277 | 278 | search.nnue.pop(); 279 | board = Board::from_fen(fen, false).unwrap(); 280 | } 281 | } 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /engine/src/body/tt.rs: -------------------------------------------------------------------------------- 1 | use crate::definitions::{NOMOVE, TB_LOSS_IN_PLY, TB_WIN_IN_PLY}; 2 | 3 | use cozy_chess::{Move, Piece, Square}; 4 | use std::sync::atomic::{AtomicU64, Ordering}; 5 | 6 | #[derive(Clone, Copy, PartialEq, Debug)] 7 | pub enum TTFlag { 8 | None, 9 | Exact, 10 | LowerBound, 11 | UpperBound, 12 | } 13 | 14 | #[derive(Clone, Copy, Debug, PartialEq)] 15 | pub struct PackedMove(u16); 16 | 17 | impl PackedMove { 18 | pub fn new(mv: Option) -> Self { 19 | if mv.is_none() { 20 | return Self(NOMOVE); 21 | } 22 | 23 | let mv = mv.unwrap(); 24 | let from = mv.from as u16; // 0..63, 6 bits 25 | let to = mv.to as u16; // 0..63, 6 bits 26 | 27 | // First bit represents promotion, next 2 bits represent piece type 28 | let promotion: u16 = match mv.promotion { 29 | None => 0b000, 30 | Some(Piece::Knight) => 0b100, 31 | Some(Piece::Bishop) => 0b101, 32 | Some(Piece::Rook) => 0b110, 33 | Some(Piece::Queen) => 0b111, 34 | _ => unreachable!(), 35 | }; 36 | 37 | // 6 + 6 + 3 bits and one for padding gets a 2 byte move 38 | let packed = from | to << 6 | promotion << 12; 39 | 40 | Self(packed) 41 | } 42 | 43 | pub fn unpack(self) -> Move { 44 | let from = Square::index((self.0 & 0b111111) as usize); 45 | let to = Square::index(((self.0 >> 6) & 0b111111) as usize); 46 | 47 | let promotion = match (self.0 >> 12) & 0b111 { 48 | 0b000 => None, 49 | 0b100 => Some(Piece::Knight), 50 | 0b101 => Some(Piece::Bishop), 51 | 0b110 => Some(Piece::Rook), 52 | 0b111 => Some(Piece::Queen), 53 | _ => unreachable!(), 54 | }; 55 | 56 | Move { 57 | from, 58 | to, 59 | promotion, 60 | } 61 | } 62 | } 63 | 64 | #[derive(Clone, Copy, Debug, PartialEq)] 65 | pub struct AgeAndFlag(pub u8); 66 | 67 | impl AgeAndFlag { 68 | fn new(age: u8, flag: TTFlag) -> Self { 69 | let flag = match flag { 70 | TTFlag::None => 0b00, 71 | TTFlag::Exact => 0b01, 72 | TTFlag::LowerBound => 0b10, 73 | TTFlag::UpperBound => 0b11, 74 | }; 75 | 76 | Self(age << 2 | flag) 77 | } 78 | 79 | fn age(&self) -> u8 { 80 | self.0 >> 2 81 | } 82 | 83 | pub fn flag(&self) -> TTFlag { 84 | match self.0 & 0b11 { 85 | 0b00 => TTFlag::None, 86 | 0b01 => TTFlag::Exact, 87 | 0b10 => TTFlag::LowerBound, 88 | 0b11 => TTFlag::UpperBound, 89 | _ => unreachable!(), 90 | } 91 | } 92 | } 93 | 94 | #[derive(Clone, Copy, Debug)] 95 | pub struct TTEntry { 96 | pub mv: PackedMove, // 2 byte move wrapper (6 sq + 6 sq + 3 promo bits) 97 | pub key: u16, // 2 bytes 98 | pub score: i16, // 2 bytes 99 | pub depth: u8, // 1 byte 100 | pub age_flag: AgeAndFlag, // 1 byte wrapper (6 age + 2 flag bits) 101 | } 102 | 103 | impl TTEntry { 104 | #[must_use] 105 | fn quality(&self) -> u16 { 106 | let age = self.age_flag.age(); 107 | (age * 2 + self.depth) as u16 108 | } 109 | } 110 | 111 | // Thank you to Spamdrew and Cosmo for help in implementing the atomic TT 112 | impl From for TTEntry { 113 | fn from(data: u64) -> Self { 114 | // SAFETY: This is safe because all fields of TTEntry are (at base) integral types, and order is known. 115 | unsafe { std::mem::transmute(data) } 116 | } 117 | } 118 | 119 | impl From for u64 { 120 | fn from(entry: TTEntry) -> Self { 121 | // SAFETY: This is safe because all bitpatterns of `u64` are valid. 122 | unsafe { std::mem::transmute(entry) } 123 | } 124 | } 125 | 126 | pub struct TT { 127 | pub entries: Vec, 128 | pub epoch: u8, 129 | } 130 | 131 | impl TT { 132 | pub fn new(mb: u64) -> Self { 133 | let hash_size = mb * 1024 * 1024; 134 | let size = hash_size / std::mem::size_of::() as u64; 135 | let mut entries = Vec::with_capacity(size as usize); 136 | 137 | for _ in 0..size { 138 | entries.push(AtomicU64::new(0)); 139 | } 140 | 141 | Self { entries, epoch: 0 } 142 | } 143 | 144 | #[must_use] 145 | pub fn index(&self, key: u64) -> usize { 146 | // Cool hack Cosmo taught me 147 | let key = key as u128; 148 | let len = self.entries.len() as u128; 149 | ((key * len) >> 64) as usize 150 | } 151 | 152 | #[must_use] 153 | pub fn probe(&self, key: u64) -> TTEntry { 154 | let atomic = &self.entries[self.index(key)]; 155 | let entry = atomic.load(Ordering::Relaxed); 156 | 157 | TTEntry::from(entry) 158 | } 159 | 160 | pub fn age(&mut self) { 161 | // Cap at 63 for wrapping into 6 bits 162 | const EPOCH_MAX: u8 = 63; 163 | 164 | if self.epoch == EPOCH_MAX { 165 | self.epoch = 0; 166 | 167 | self.entries.iter_mut().for_each(|a| { 168 | let entry = a.load(Ordering::Relaxed); 169 | let mut entry = TTEntry::from(entry); 170 | 171 | entry.age_flag = AgeAndFlag::new(0, entry.age_flag.flag()); 172 | 173 | a.store(entry.into(), Ordering::Relaxed); 174 | }) 175 | } 176 | 177 | self.epoch += 1; 178 | } 179 | 180 | pub fn store( 181 | &self, 182 | key: u64, 183 | mv: Option, 184 | score: i16, 185 | depth: u8, 186 | flag: TTFlag, 187 | ply: usize, 188 | ) { 189 | let target_index = self.index(key); 190 | let target_atomic = &self.entries[target_index]; 191 | let mut target: TTEntry = target_atomic.load(Ordering::Relaxed).into(); 192 | 193 | let entry = TTEntry { 194 | key: key as u16, 195 | mv: PackedMove::new(mv), 196 | score: score_to_tt(score, ply), 197 | depth, 198 | age_flag: AgeAndFlag::new(self.epoch, flag), 199 | }; 200 | 201 | // Only replace entries of similar or higher quality 202 | if entry.quality() >= target.quality() { 203 | let positions_differ = target.key != entry.key; 204 | 205 | target.key = entry.key; 206 | target.score = entry.score; 207 | target.depth = entry.depth; 208 | target.age_flag = entry.age_flag; 209 | 210 | // Do not overwrite the move if there was no new best move 211 | if mv.is_some() || positions_differ { 212 | target.mv = entry.mv; 213 | } 214 | 215 | target_atomic.store(target.into(), Ordering::Relaxed); 216 | } 217 | } 218 | 219 | pub fn prefetch(&self, key: u64) { 220 | #[cfg(target_arch = "x86_64")] 221 | unsafe { 222 | use std::arch::x86_64::{_mm_prefetch, _MM_HINT_T0}; 223 | 224 | let index = self.index(key); 225 | let entry = &self.entries[index]; 226 | 227 | _mm_prefetch((entry as *const AtomicU64).cast::(), _MM_HINT_T0); 228 | } 229 | } 230 | 231 | pub fn reset(&mut self) { 232 | self.entries.iter().for_each(|a| { 233 | a.store(0, Ordering::Relaxed); 234 | }) 235 | } 236 | } 237 | 238 | #[must_use] 239 | pub fn score_to_tt(score: i16, ply: usize) -> i16 { 240 | if score >= TB_WIN_IN_PLY as i16 { 241 | score + ply as i16 242 | } else if score <= TB_LOSS_IN_PLY as i16 { 243 | score - ply as i16 244 | } else { 245 | score 246 | } 247 | } 248 | 249 | #[must_use] 250 | pub fn score_from_tt(score: i16, ply: usize) -> i16 { 251 | if score >= TB_WIN_IN_PLY as i16 { 252 | score - ply as i16 253 | } else if score <= TB_LOSS_IN_PLY as i16 { 254 | score + ply as i16 255 | } else { 256 | score 257 | } 258 | } 259 | 260 | const _TT_TEST: () = assert!(std::mem::size_of::() == 8); 261 | 262 | #[cfg(test)] 263 | mod tests { 264 | use super::*; 265 | 266 | #[test] 267 | fn tt_reset() { 268 | let mut tt = TT::new(1); 269 | let mv = Move { 270 | from: Square::A1, 271 | to: Square::A2, 272 | promotion: None, 273 | }; 274 | tt.store(5, Some(mv), 1, 3, TTFlag::UpperBound, 22); 275 | assert_eq!(tt.probe(5).score, 1); 276 | 277 | tt.reset(); 278 | tt.entries.iter().for_each(|e| { 279 | let e = e.load(Ordering::Relaxed); 280 | let e = TTEntry::from(e); 281 | 282 | assert_eq!(e.score, 0); 283 | assert_eq!(e.age_flag, AgeAndFlag(0)); 284 | assert_eq!(e.depth, 0); 285 | assert_eq!(e.key, 0); 286 | assert_eq!(e.mv, PackedMove(NOMOVE)); 287 | }); 288 | } 289 | 290 | #[test] 291 | fn packed_moves() { 292 | let mv = Move { 293 | from: Square::A1, 294 | to: Square::A2, 295 | promotion: None, 296 | }; 297 | let packed = PackedMove::new(Some(mv)); 298 | assert_eq!(packed.unpack(), mv); 299 | 300 | let mv = Move { 301 | from: Square::B7, 302 | to: Square::A2, 303 | promotion: Some(Piece::Knight), 304 | }; 305 | let packed = PackedMove::new(Some(mv)); 306 | assert_eq!(packed.unpack(), mv); 307 | 308 | let mv = Move { 309 | from: Square::C1, 310 | to: Square::A2, 311 | promotion: Some(Piece::Bishop), 312 | }; 313 | let packed = PackedMove::new(Some(mv)); 314 | assert_eq!(packed.unpack(), mv); 315 | 316 | let mv = Move { 317 | from: Square::H3, 318 | to: Square::H4, 319 | promotion: Some(Piece::Rook), 320 | }; 321 | let packed = PackedMove::new(Some(mv)); 322 | assert_eq!(packed.unpack(), mv); 323 | 324 | let mv = Move { 325 | from: Square::D8, 326 | to: Square::D7, 327 | promotion: Some(Piece::Queen), 328 | }; 329 | let packed = PackedMove::new(Some(mv)); 330 | assert_eq!(packed.unpack(), mv); 331 | } 332 | 333 | #[test] 334 | fn age_flag() { 335 | let entry = TTEntry { 336 | key: 0, 337 | mv: PackedMove(NOMOVE), 338 | score: 0, 339 | depth: 0, 340 | age_flag: AgeAndFlag::new(5, TTFlag::Exact), 341 | }; 342 | 343 | assert_eq!(entry.age_flag.age(), 0b0000_0101); 344 | assert_eq!(entry.age_flag.age(), 5); 345 | assert_eq!(entry.age_flag.flag(), TTFlag::Exact); 346 | 347 | let entry = TTEntry { 348 | key: 0, 349 | mv: PackedMove(NOMOVE), 350 | score: 0, 351 | depth: 0, 352 | age_flag: AgeAndFlag::new(63, TTFlag::UpperBound), 353 | }; 354 | 355 | assert_eq!(entry.age_flag.age(), 0b0011_1111); 356 | assert_eq!(entry.age_flag.age(), 63); 357 | assert_eq!(entry.age_flag.flag(), TTFlag::UpperBound); 358 | 359 | let entry = TTEntry { 360 | key: 0, 361 | mv: PackedMove(NOMOVE), 362 | score: 0, 363 | depth: 0, 364 | age_flag: AgeAndFlag::new(0, TTFlag::LowerBound), 365 | }; 366 | 367 | assert_eq!(entry.age_flag.age(), 0b0000_0000); 368 | assert_eq!(entry.age_flag.age(), 0); 369 | assert_eq!(entry.age_flag.flag(), TTFlag::LowerBound); 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /datagen/src/script.rs: -------------------------------------------------------------------------------- 1 | use engine::{ 2 | body::{movegen, nnue::inference::NNUEState, position::is_quiet, search::Search, tt::TT}, 3 | definitions, 4 | uci::handler::SearchType, 5 | }; 6 | 7 | use std::{ 8 | error::Error, 9 | fs::File, 10 | io::{stdin, stdout, BufWriter, Write}, 11 | path::{Path, PathBuf}, 12 | sync::atomic::{AtomicBool, AtomicU64, Ordering}, 13 | time::Instant, 14 | }; 15 | 16 | use cozy_chess::{Board, Color, GameStatus, Piece}; 17 | 18 | const DEFAULT: &str = "\x1b[0m"; 19 | const WHITE: &str = "\x1b[38;5;15m"; 20 | const ORANGE: &str = "\x1b[38;5;208m"; 21 | const GREEN: &str = "\x1b[38;5;40m"; 22 | const RED: &str = "\x1b[38;5;196m"; 23 | 24 | static STOP_FLAG: AtomicBool = AtomicBool::new(false); 25 | static FENS: AtomicU64 = AtomicU64::new(0); 26 | static WHITE_WINS: AtomicU64 = AtomicU64::new(0); 27 | static BLACK_WINS: AtomicU64 = AtomicU64::new(0); 28 | static DRAWS: AtomicU64 = AtomicU64::new(0); 29 | 30 | #[derive(Debug)] 31 | struct Parameters { 32 | // The number of games to generate 33 | games: usize, 34 | // The number of threads to use 35 | threads: usize, 36 | // Search type for the evaluations 37 | st: SearchType, 38 | } 39 | 40 | impl Parameters { 41 | fn new(games: usize, threads: usize, st: SearchType) -> Self { 42 | Self { games, threads, st } 43 | } 44 | } 45 | 46 | pub fn root() -> Result<(), Box> { 47 | // Get the number of games to generate 48 | println!("How many games would you like to gen? [1, 100M]"); 49 | let mut inp_games = String::new(); 50 | stdin().read_line(&mut inp_games).unwrap(); 51 | 52 | let games = inp_games.trim().parse::()?; 53 | if !(1..=100_000_000).contains(&games) { 54 | panic!("Invalid game range! {games}, needs to be between [1, 100M]") 55 | } 56 | 57 | // Get the number of threads to use 58 | println!("How many threads would you like to use? [1, 64]"); 59 | let mut inp_threads = String::new(); 60 | stdin().read_line(&mut inp_threads).unwrap(); 61 | 62 | let threads = inp_threads.trim().parse::()?; 63 | if !(1..=64).contains(&threads) { 64 | panic!("Invalid thread range! {threads}, needs to be between [1, 64]") 65 | } 66 | 67 | // Get the search type for the search evaluations 68 | println!("What search type would you like to use? [depth, nodes]"); 69 | let mut inp_st = String::new(); 70 | stdin().read_line(&mut inp_st).unwrap(); 71 | 72 | let st = match inp_st.trim().to_lowercase().as_str() { 73 | "depth" => { 74 | println!("What depth would you like to use? [1, 100]"); 75 | let mut inp_depth = String::new(); 76 | stdin().read_line(&mut inp_depth).unwrap(); 77 | 78 | let depth = inp_depth.trim().parse::()?; 79 | if !(1..=100).contains(&depth) { 80 | panic!("Invalid depth range! {depth}, needs to be between [1, 100]") 81 | } 82 | 83 | SearchType::Depth(depth) 84 | } 85 | "nodes" => { 86 | println!("What nodes would you like to use? [1, 100M]"); 87 | let mut inp_nodes = String::new(); 88 | stdin().read_line(&mut inp_nodes).unwrap(); 89 | 90 | let nodes = inp_nodes.trim().parse::()?; 91 | if !(1..=100_000_000).contains(&nodes) { 92 | panic!("Invalid nodes range! {nodes}, needs to be between [1, 100M]") 93 | } 94 | 95 | SearchType::Nodes(nodes) 96 | } 97 | _ => { 98 | panic!("Invalid search type! {inp_st}") 99 | } 100 | }; 101 | 102 | // Let the user confirm the parameters 103 | let params = Parameters::new(games, threads, st); 104 | println!( 105 | "\n{GREEN}Confirmed parameters: {DEFAULT}[games: {WHITE}{}{DEFAULT}, threads: {WHITE}{}{DEFAULT}, search type: {WHITE}{:?}{DEFAULT}]", 106 | params.games, params.threads, params.st 107 | ); 108 | if params.games % params.threads != 0 { 109 | println!("{ORANGE}WARNING: {DEFAULT}The number of games is not divisible by the number of threads!"); 110 | } 111 | println!("Press enter to continue..."); 112 | stdin().read_line(&mut String::new()).unwrap(); 113 | 114 | generate_main(params); 115 | 116 | println!("We're done B)"); 117 | Ok(()) 118 | } 119 | 120 | fn generate_main(params: Parameters) { 121 | ctrlc::set_handler(move || { 122 | STOP_FLAG.store(true, Ordering::SeqCst); 123 | println!("Stopping generation..."); 124 | }) 125 | .expect("Failed to set CTRL+C handler."); 126 | 127 | let run_id = chrono::Local::now().format("%Y-%m-%d_%H-%M-%S").to_string(); 128 | println!("{RED}ATTENTION: {DEFAULT}This run will be saved to data-{run_id}"); 129 | println!( 130 | "{GREEN}Generating {DEFAULT}{} games with {} threads on {:?}...", 131 | params.games, params.threads, params.st 132 | ); 133 | 134 | let data_dir = PathBuf::from("data").join(run_id); 135 | std::fs::create_dir_all(&data_dir).unwrap(); 136 | 137 | std::thread::scope(|s| { 138 | for i in 0..params.threads { 139 | let path = &data_dir; 140 | let tprms = ¶ms; 141 | s.spawn(move || { 142 | generate_thread(i, path, tprms); 143 | }); 144 | } 145 | }) 146 | } 147 | 148 | fn generate_thread(id: usize, data_dir: &Path, options: &Parameters) { 149 | let tt = TT::new(16); 150 | let nnue = NNUEState::from_board(&Board::default()); 151 | 152 | // not implemented properly 153 | let history = engine::body::history::History::new(); 154 | let mut search = Search::new(&tt, &nnue, &history, &vec![]); 155 | let rng = fastrand::Rng::new(); 156 | 157 | let mut board; 158 | let mut game_buffer: Vec<(i32, String)> = vec![]; 159 | let mut hashes: Vec; 160 | 161 | let mut output_file = File::create(data_dir.join(format!("thread_{id}.txt"))).unwrap(); 162 | let mut output_buffer = BufWriter::new(&mut output_file); 163 | 164 | let games_per_thread = (options.games / options.threads).max(1); // at least one game 165 | let timer = Instant::now(); 166 | 167 | 'main: for games_played in 0..games_per_thread { 168 | // Information print from main thread 169 | if id == 0 && games_played != 0 && games_played % 64 == 0 { 170 | let fens = FENS.load(Ordering::Relaxed); 171 | let elapsed = timer.elapsed().as_secs_f64(); 172 | let fens_per_sec = fens as f64 / elapsed; 173 | 174 | let ww = WHITE_WINS.load(Ordering::Relaxed); 175 | let bw = BLACK_WINS.load(Ordering::Relaxed); 176 | let dr = DRAWS.load(Ordering::Relaxed); 177 | let tot_games = ww + bw + dr; 178 | let percentage = (games_played as f64 / games_per_thread as f64) * 100.0; 179 | 180 | let time_per_game = elapsed / games_played as f64; 181 | let etr = (games_per_thread - games_played) as f64 * time_per_game; 182 | 183 | println!("\n{GREEN}Generated {DEFAULT}{fens} FENs [{fens_per_sec:.2} FEN/s]"); 184 | println!("{GREEN}Time per game: {DEFAULT}{time_per_game:.2}s."); 185 | println!("In total: {tot_games} [{percentage:.2}%] games are done."); 186 | println!("White wins: {ww}, Black wins: {bw}, Draws: {dr}"); 187 | println!("Elapsed time: {elapsed:.2}s. {RED}ETR: {etr:.2}s{DEFAULT}"); 188 | 189 | stdout().flush().unwrap(); 190 | } 191 | 192 | // Reset everything from previous game 193 | output_buffer.flush().unwrap(); 194 | board = Board::default(); 195 | search.game_reset(); 196 | hashes = vec![]; 197 | 198 | // Play a new game 199 | // First we get a 'random' starting position 200 | let random_moves = rng.usize(11..=12); 201 | for _ in 0..random_moves { 202 | let moves = movegen::pure_moves(&board); 203 | let mv = moves[rng.usize(..moves.len())]; 204 | board.play_unchecked(mv); 205 | 206 | // ... make sure that the position isn't over 207 | if board.status() != GameStatus::Ongoing { 208 | continue 'main; 209 | } 210 | } 211 | search.nnue.refresh(&board); 212 | 213 | // ... make sure that the exit isn't absurd 214 | let (score, _) = search.data_search(&board, SearchType::Depth(8)); 215 | if score.abs() > 1000 { 216 | continue 'main; 217 | } 218 | 219 | // ... play the rest of the game 220 | 221 | // REMINDER TO SELF 222 | // the game history of the search struct might not be taken care of properly? 223 | let (game_result, winner) = loop { 224 | let status = board.status(); 225 | if draw(&board, &mut hashes) { 226 | break (GameStatus::Drawn, None); 227 | } 228 | 229 | if status != GameStatus::Ongoing { 230 | break (status, Some(!board.side_to_move())); 231 | } 232 | 233 | search.go_reset(); 234 | //search.tt.age(); 235 | let (mut score, best_move) = search.data_search(&board, options.st); 236 | 237 | // filter noisy positions 238 | let not_in_check = board.checkers().is_empty(); 239 | let okay_score = score.abs() < definitions::TB_WIN_IN_PLY; 240 | let okay_move = is_quiet(&board, best_move); 241 | if not_in_check && okay_score && okay_move { 242 | // Always report scores from white's perspective 243 | score = if board.side_to_move() == Color::White { 244 | score 245 | } else { 246 | -score 247 | }; 248 | 249 | game_buffer.push((score, format!("{}", board))); 250 | } 251 | 252 | board.play_unchecked(best_move); 253 | search.nnue.refresh(&board); 254 | }; 255 | 256 | // Always report wins from white's perspective 257 | let result_output = match (game_result, winner) { 258 | (GameStatus::Drawn, _) => { 259 | DRAWS.fetch_add(1, Ordering::Relaxed); 260 | "0.5".to_string() 261 | } 262 | (GameStatus::Won, Some(Color::White)) => { 263 | WHITE_WINS.fetch_add(1, Ordering::Relaxed); 264 | "1".to_string() 265 | } 266 | (GameStatus::Won, Some(Color::Black)) => { 267 | BLACK_WINS.fetch_add(1, Ordering::Relaxed); 268 | "0".to_string() 269 | } 270 | _ => unreachable!(), 271 | }; 272 | 273 | // Write the result 274 | FENS.fetch_add(game_buffer.len() as u64, Ordering::Relaxed); 275 | for (score, fen) in game_buffer.drain(..) { 276 | writeln!(output_buffer, "{fen} | {score} | {result_output}").unwrap(); 277 | } 278 | 279 | // Safely abort with CTRLC handler since otherwise 280 | // our files could get truncated and the data get lost. 281 | if STOP_FLAG.load(Ordering::SeqCst) { 282 | break 'main; 283 | } 284 | } 285 | 286 | fn draw(board: &Board, hashes: &mut Vec) -> bool { 287 | // Material draw 288 | if board.occupied().len() == 2 289 | || (board.occupied().len() == 3 290 | && !(board.pieces(Piece::Bishop) | board.pieces(Piece::Knight)).is_empty()) 291 | { 292 | return true; 293 | } 294 | 295 | // 3-fold repetition 296 | let hash = board.hash(); 297 | hashes.push(hash); 298 | if hashes.iter().filter(|&&h| h == hash).count() >= 3 { 299 | return true; 300 | } 301 | 302 | // 50-move rule is handled by board.status() 303 | false 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /info/CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | Since the release of Svart 2 I have decided to start keeping a changelog 2 | so that myself and others could more easily keep track of what changes 3 | were made when and what impact it had on the engine. 4 | The idea of doing this was inspired by Gedas and his changelog for chess.cpp. 5 | 6 | ==================================================================================== 7 | Release 1 [February 10 2023] 8 | 9 | History Heuristic (#10) 10 | 11 | Score of svart-history vs svart-master: 804 - 634 - 445 [0.545] 1883 12 | ... svart-history playing White: 431 - 293 - 217 [0.573] 941 13 | ... svart-history playing Black: 373 - 341 - 228 [0.517] 942 14 | ... White vs Black: 772 - 666 - 445 [0.528] 1883 15 | Elo difference: 31.5 +/- 13.7, LOS: 100.0 %, DrawRatio: 23.6 % 16 | SPRT: llr 2.96 (100.4%), lbound -2.94, ubound 2.94 - H1 was accepted 17 | 18 | ==================================================================================== 19 | 1.1 [February 11 2023] 20 | 21 | Null Move Pruning (#11) 22 | 23 | Score of svart-nmp vs svart-master: 161 - 79 - 70 [0.632] 310 24 | ... svart-nmp playing White: 79 - 45 - 32 [0.609] 156 25 | ... svart-nmp playing Black: 82 - 34 - 38 [0.656] 154 26 | ... White vs Black: 113 - 127 - 70 [0.477] 310 27 | Elo difference: 94.1 +/- 35.0, LOS: 100.0 %, DrawRatio: 22.6 % 28 | SPRT: llr 2.95 (100.3%), lbound -2.94, ubound 2.94 - H1 was accepted 29 | 30 | ==================================================================================== 31 | 1.2 [February 21 2023] 32 | 33 | Reverse Futility Pruning (#13) 34 | 35 | Score of svart-rfp vs svart-master: 209 - 125 - 105 [0.596] 439 36 | ... svart-rfp playing White: 99 - 61 - 60 [0.586] 220 37 | ... svart-rfp playing Black: 110 - 64 - 45 [0.605] 219 38 | ... White vs Black: 163 - 171 - 105 [0.491] 439 39 | Elo difference: 67.3 +/- 28.7, LOS: 100.0 %, DrawRatio: 23.9 % 40 | SPRT: llr 2.98 (101.2%), lbound -2.94, ubound 2.94 - H1 was accepted 41 | 42 | ==================================================================================== 43 | 1.3 [February 23 2023] 44 | 45 | board.status() for mate checking (#14) 46 | 47 | Score of svart-dev vs svart-master: 944 - 780 - 681 [0.534] 2405 48 | ... svart-dev playing White: 485 - 380 - 338 [0.544] 1203 49 | ... svart-dev playing Black: 459 - 400 - 343 [0.525] 1202 50 | ... White vs Black: 885 - 839 - 681 [0.510] 2405 51 | Elo difference: 23.7 +/- 11.8, LOS: 100.0 %, DrawRatio: 28.3 % 52 | SPRT: llr 2.95 (100.2%), lbound -2.94, ubound 2.94 - H1 was accepted 53 | 54 | ==================================================================================== 55 | 1.4 [February 25 2023] 56 | 57 | ((Another)) History improvement (#15) 58 | 59 | Score of svart-dev vs svart-master: 368 - 284 - 306 [0.544] 958 60 | ... svart-dev playing White: 189 - 124 - 165 [0.568] 478 61 | ... svart-dev playing Black: 179 - 160 - 141 [0.520] 480 62 | ... White vs Black: 349 - 303 - 306 [0.524] 958 63 | Elo difference: 30.5 +/- 18.2, LOS: 99.9 %, DrawRatio: 31.9 % 64 | SPRT: llr 2.98 (101.3%), lbound -2.94, ubound 2.94 - H1 was accepted 65 | 66 | ==================================================================================== 67 | 1.5 [February 26 2023] 68 | 69 | TT usage in QSearch (#17) 70 | 71 | Score of svart-dev vs svart-master: 302 - 217 - 196 [0.559] 715 72 | ... svart-dev playing White: 162 - 99 - 96 [0.588] 357 73 | ... svart-dev playing Black: 140 - 118 - 100 [0.531] 358 74 | ... White vs Black: 280 - 239 - 196 [0.529] 715 75 | Elo difference: 41.5 +/- 21.8, LOS: 100.0 %, DrawRatio: 27.4 % 76 | SPRT: llr 2.98 (101.2%), lbound -2.94, ubound 2.94 - H1 was accepted 77 | 78 | ==================================================================================== 79 | 1.6 [February 27 2023] 80 | 81 | Late Move Reductions (#18) 82 | 83 | Score of svart-dev vs svart-master: 89 - 25 - 58 [0.686] 172 84 | ... svart-dev playing White: 42 - 17 - 27 [0.645] 86 85 | ... svart-dev playing Black: 47 - 8 - 31 [0.727] 86 86 | ... White vs Black: 50 - 64 - 58 [0.459] 172 87 | Elo difference: 135.8 +/- 43.9, LOS: 100.0 %, DrawRatio: 33.7 % 88 | SPRT: llr 2.96 (100.7%), lbound -2.94, ubound 2.94 - H1 was accepted 89 | 90 | ==================================================================================== 91 | 1.7 [March 1 2023] 92 | 93 | TT: Aging and smaller entries (#19) 94 | 95 | Score of svart-dev vs svart-master: 171 - 105 - 194 [0.570] 470 96 | ... svart-dev playing White: 84 - 52 - 99 [0.568] 235 97 | ... svart-dev playing Black: 87 - 53 - 95 [0.572] 235 98 | ... White vs Black: 137 - 139 - 194 [0.498] 470 99 | Elo difference: 49.1 +/- 24.1, LOS: 100.0 %, DrawRatio: 41.3 % 100 | SPRT: llr 2.96 (100.4%), lbound -2.94, ubound 2.94 - H1 was accepted 101 | 102 | ==================================================================================== 103 | Release 2 [March 8 2023] 104 | 105 | Late Move Pruning (#22) 106 | 107 | Score of svart-dev vs svart-master: 146 - 85 - 189 (0.57) 108 | Ptnml: WW WD DD/WL LD LL 109 | distr: 22 71 72 36 9 110 | LLR: 2.99 (-2.94, 2.94) [0.00, 10.00] 111 | Games:420 W:34.8% L:20.2% D:45.0% 112 | Elo difference: 50.82 +/- 24.69 113 | 114 | ==================================================================================== 115 | 2.1 [March 13 2023] 116 | 117 | Improved LMP (#24) 118 | 119 | Score of svart-dev vs svart-master after 1300 games: 368 - 295 - 627 (0.52) 120 | Ptnml: WW WD DD/WL LD LL 121 | Distr: 48 177 252 130 36 122 | LLR: 3.07 (-2.94, 2.94) [0.00, 10.00] 123 | Stats: W: 28.3% L: 22.7% D: 48.2% TF: 0 124 | Elo difference: 19.68 +/- 13.58 125 | 126 | 127 | Tuned with CTT 128 | 129 | Score of svart-dev vs svart-master after 12432 games: 3305 - 3110 - 6017 (0.51) 130 | Ptnml: WW WD DD/WL LD LL 131 | Distr: 427 1561 2363 1510 355 132 | LLR: 2.94 (-2.94, 2.94) [0.00, 5.00] 133 | Stats: W: 26.6% L: 25.0% D: 48.4% TF: 0 134 | White advantage: -3.44 +/- 4.38 135 | Elo difference: 5.45 +/- 4.38 136 | 137 | ==================================================================================== 138 | 2.2 [March 16 2023] 139 | 140 | Improved aspiration windows (#26) 141 | depth -= 1 upon fail highs 142 | 143 | Score of dev vs master after 35300 games: 9791 - 9426 - 16074 (0.51) 144 | Ptnml: WW WD DD/WL LD LL 145 | Distr: 1280 4402 6528 4274 1161 146 | LLR: 2.94 (-2.94, 2.94) [0.00, 5.00] 147 | Stats: W: 27.7% L: 26.7% D: 45.5% TF: 0 148 | White advantage: -2.32 +/- 2.32 149 | Elo difference: 3.59 +/- 2.67 150 | 151 | 152 | Lower window size 153 | 154 | Score of asp2 vs asp1 after 1150 games: 354 - 280 - 504 (0.53) 155 | Ptnml: WW WD DD/WL LD LL 156 | Distr: 47 169 197 120 35 157 | LLR: 3.00 (-2.94, 2.94) [0.00, 10.00] 158 | Stats: W: 30.8% L: 24.3% D: 43.8% TF: 0 159 | White advantage: -0.00 +/- 15.05 160 | Elo difference: 22.62 +/- 15.06 161 | 162 | ==================================================================================== 163 | 2.3 [March 18 2023] 164 | 165 | Improved NMP (#29) 166 | 167 | Score of dev vs master after 1274 games: 347 - 278 - 649 (0.53) 168 | Ptnml: WW WD DD/WL LD LL 169 | Distr: 45 186 223 159 24 170 | LLR: 2.99 (-2.94, 2.94) [0.00, 10.00] 171 | Stats: W: 27.2% L: 21.8% D: 50.9% TF: 0 172 | White advantage: 9.00 +/- 13.35 173 | Elo difference: 18.84 +/- 13.35 174 | 175 | ==================================================================================== 176 | Release 3 [April 3 2023] 177 | 178 | Introduction of NNUE (#30) 179 | 180 | Score of svart3 vs svart2 after 512 rounds: 855 - 41 - 128 (0.90) 181 | Ptnml: WW WD DD/WL LD LL 182 | Distr: 357 110 35 10 0 183 | Stats: W: 83.5% L: 4.0% D: 12.5% TF: 0 184 | Elo difference: 376.85 +/- 28.65 185 | 186 | ==================================================================================== 187 | Release 3.1 [April 6 2023] 188 | 189 | Better timeman (#31) 190 | STC 191 | 192 | Score of dev vs master after 465 rounds: 274 - 169 - 487 (0.56) 193 | Ptnml: WW WD DD/WL LD LL 194 | Distr: 39 144 181 85 16 195 | LLR: 3.05 (-2.94, 2.94) [0.00, 5.00] 196 | Stats: W: 29.5% L: 18.2% D: 52.4% TF: 0 197 | Elo difference: 39.39 +/- 15.39 198 | 199 | LTC 200 | 201 | Score of dev vs master after 220 rounds: 129 - 76 - 235 (0.56) 202 | Ptnml: WW WD DD/WL LD LL 203 | Distr: 19 62 96 39 4 204 | LLR: 2.97 (-2.94, 2.94) [0.00, 10.00] 205 | Stats: W: 29.3% L: 17.3% D: 53.4% TF: 0 206 | Elo difference: 42.05 +/- 22.13 207 | 208 | Better repeating timeman (b114a9C) 209 | 40/20 210 | 211 | Score of dev vs master: 134 - 78 - 178 [0.572] 390 212 | ... dev playing White: 64 - 40 - 91 [0.562] 195 213 | ... dev playing Black: 70 - 38 - 87 [0.582] 195 214 | ... White vs Black: 102 - 110 - 178 [0.490] 390 215 | Elo difference: 50.2 +/- 25.5, LOS: 100.0 %, DrawRatio: 45.6 % 216 | SPRT: llr 2.73 (92.8%), lbound -2.94, ubound 2.94 // lost the last output 217 | 218 | Fix repetitions (#34) [April 10 2023] 219 | 220 | ELO | 29.02 +- 11.80 (95%) 221 | SPRT | 8.0+0.08s Threads=1 Hash=16MB 222 | LLR | 2.95 (-2.94, 2.94) [0.00, 5.00] 223 | GAMES | N: 1800 W: 559 L: 409 D: 832 224 | 225 | ==================================================================================== 226 | 3.2 [April 13 2023] 227 | 228 | Svart0005 (#35) 229 | 230 | ELO | 109.42 +- 26.52 (95%) 231 | SPRT | 8.0+0.08s Threads=1 Hash=16MB 232 | LLR | 2.97 (-2.94, 2.94) [0.00, 5.00] 233 | GAMES | N: 528 W: 271 L: 110 D: 147 234 | https://engineprogramming.pythonanywhere.com/test/23/ 235 | 236 | Svart0006 (#36) 237 | 238 | ELO | 14.33 +- 8.27 (95%) 239 | SPRT | 8.0+0.08s Threads=1 Hash=16MB 240 | LLR | 3.00 (-2.94, 2.94) [0.00, 5.00] 241 | GAMES | N: 4440 W: 1545 L: 1362 D: 1533 242 | https://engineprogramming.pythonanywhere.com/test/28/ 243 | 244 | ==================================================================================== 245 | 3.3 [April 17 2023] 246 | 247 | Check Extensions (#38) 248 | 249 | ELO | 13.40 +- 7.86 (95%) 250 | SPRT | 8.0+0.08s Threads=1 Hash=16MB 251 | LLR | 2.95 (-2.94, 2.94) [0.00, 5.00] 252 | GAMES | N: 4488 W: 1427 L: 1254 D: 1807 253 | https://engineprogramming.pythonanywhere.com/test/70/ 254 | 255 | ==================================================================================== 256 | 3.4 [April 20 2023] 257 | 258 | Improving in RFP (#39) 259 | 260 | ELO | 22.42 +- 10.55 (95%) 261 | SPRT | 8.0+0.08s Threads=1 Hash=16MB 262 | LLR | 2.99 (-2.94, 2.94) [0.00, 5.00] 263 | GAMES | N: 2560 W: 867 L: 702 D: 991 264 | https://engineprogramming.pythonanywhere.com/test/166/ 265 | 266 | Move RFP (#40) 267 | 268 | ELO | 14.43 +- 8.11 (95%) 269 | SPRT | 8.0+0.08s Threads=1 Hash=16MB 270 | LLR | 2.95 (-2.94, 2.94) [0.00, 5.00] 271 | GAMES | N: 4024 W: 1233 L: 1066 D: 1725 272 | https://engineprogramming.pythonanywhere.com/test/167/ 273 | 274 | ==================================================================================== 275 | Release 4 [April 23 2023] 276 | 277 | Futility Pruning (#42) 278 | 279 | ELO | 16.69 +- 8.79 (95%) 280 | SPRT | 8.0+0.08s Threads=1 Hash=16MB 281 | LLR | 3.02 (-2.94, 2.94) [0.00, 5.00] 282 | GAMES | N: 3520 W: 1115 L: 946 D: 1459 283 | https://engineprogramming.pythonanywhere.com/test/221/ 284 | 285 | ==================================================================================== 286 | 4.1 [April 28 2023] 287 | 288 | Three-fold LMR 289 | 290 | ELO | 33.76 +- 13.11 (95%) 291 | SPRT | 8.0+0.08s Threads=1 Hash=16MB 292 | LLR | 2.97 (-2.94, 2.94) [0.00, 5.00] 293 | GAMES | N: 1600 W: 548 L: 393 D: 659 294 | https://chess.swehosting.se/test/335/ 295 | 296 | ==================================================================================== 297 | 4.2 [May 14 2023] 298 | 299 | Internal Iterative Reduction (IIR) 300 | 301 | ELO | 18.68 +- 9.43 (95%) 302 | SPRT | 8.0+0.08s Threads=1 Hash=16MB 303 | LLR | 2.96 (-2.94, 2.94) [0.00, 5.00] 304 | GAMES | N: 3016 W: 952 L: 790 D: 1274 305 | https://chess.swehosting.se/test/757/ 306 | 307 | ==================================================================================== 308 | Release 5 [May 31/June 1 2023] 309 | 310 | Fail-Hard NMP 311 | 312 | ELO | 29.81 +- 12.10 (95%) 313 | SPRT | 8.0+0.08s Threads=1 Hash=16MB 314 | LLR | 2.95 (-2.94, 2.94) [0.00, 5.00] 315 | GAMES | N: 1776 W: 571 L: 419 D: 786 316 | https://chess.swehosting.se/test/1198/ 317 | 318 | TT Move alpha 319 | 320 | ELO | 36.49 +- 13.38 (95%) 321 | SPRT | 8.0+0.08s Threads=1 Hash=16MB 322 | LLR | 2.96 (-2.94, 2.94) [0.00, 5.00] 323 | GAMES | N: 1424 W: 463 L: 314 D: 647 324 | https://chess.swehosting.se/test/1203/ 325 | 326 | ==================================================================================== 327 | 5.1 [June 3] 328 | 329 | Node count TM 330 | 331 | ELO | 8.15 +- 5.55 (95%) 332 | SPRT | 8.0+0.08s Threads=1 Hash=16MB 333 | LLR | 3.03 (-2.94, 2.94) [0.00, 5.00] 334 | GAMES | N: 8104 W: 2277 L: 2087 D: 3740 335 | https://chess.swehosting.se/test/1278/ 336 | 337 | ELO | 14.31 +- 7.76 (95%) 338 | SPRT | 40.0+0.40s Threads=1 Hash=256MB 339 | LLR | 2.99 (-2.94, 2.94) [0.00, 5.00] 340 | GAMES | N: 3888 W: 1061 L: 901 D: 1926 341 | https://chess.swehosting.se/test/1276/ 342 | 343 | ==================================================================================== 344 | 5.2 [June 10] 345 | 346 | Use more of the increment time 347 | 348 | STC - https://chess.swehosting.se/test/1488/ 349 | ELO | 9.41 +- 6.19 (95%) 350 | SPRT | 8.0+0.08s Threads=1 Hash=16MB 351 | LLR | 2.95 (-2.94, 2.94) [0.00, 5.00] 352 | GAMES | N: 6576 W: 1877 L: 1699 D: 3000 353 | 354 | LTC - https://chess.swehosting.se/test/1490/ 355 | ELO | 4.02 +- 3.22 (95%) 356 | SPRT | 40.0+0.40s Threads=1 Hash=256MB 357 | LLR | 2.98 (-2.94, 2.94) [0.00, 5.00] 358 | GAMES | N: 22376 W: 5740 L: 5481 D: 11155 359 | 360 | ==================================================================================== 361 | 5.3 [July 2] 362 | 363 | Update best move in asp. window fail high 364 | 365 | STC - https://chess.swehosting.se/test/2188/ 366 | ELO | 11.44 +- 6.90 (95%) 367 | SPRT | 8.0+0.08s Threads=1 Hash=32MB 368 | LLR | 3.02 (-2.94, 2.94) [0.00, 5.00] 369 | GAMES | N: 5288 W: 1525 L: 1351 D: 2412 370 | 371 | ==================================================================================== 372 | Release 6 [25 August] 373 | 374 | Implements SMP. 375 | 376 | ==================================================================================== 377 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "android_system_properties" 7 | version = "0.1.5" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 10 | dependencies = [ 11 | "libc", 12 | ] 13 | 14 | [[package]] 15 | name = "autocfg" 16 | version = "1.1.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 19 | 20 | [[package]] 21 | name = "bitflags" 22 | version = "1.3.2" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 25 | 26 | [[package]] 27 | name = "bumpalo" 28 | version = "3.12.2" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "3c6ed94e98ecff0c12dd1b04c15ec0d7d9458ca8fe806cea6f12954efe74c63b" 31 | 32 | [[package]] 33 | name = "bytecount" 34 | version = "0.6.3" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" 37 | 38 | [[package]] 39 | name = "cc" 40 | version = "1.0.79" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 43 | 44 | [[package]] 45 | name = "cfg-if" 46 | version = "1.0.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 49 | 50 | [[package]] 51 | name = "chrono" 52 | version = "0.4.24" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" 55 | dependencies = [ 56 | "iana-time-zone", 57 | "js-sys", 58 | "num-integer", 59 | "num-traits", 60 | "time", 61 | "wasm-bindgen", 62 | "winapi", 63 | ] 64 | 65 | [[package]] 66 | name = "core-foundation-sys" 67 | version = "0.8.4" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" 70 | 71 | [[package]] 72 | name = "cozy-chess" 73 | version = "0.3.2" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "f0aa2dcdbe66b10019af0f81ee4f404ca12c1d24e56f47e7f2588954f8bfa306" 76 | dependencies = [ 77 | "cozy-chess-types", 78 | ] 79 | 80 | [[package]] 81 | name = "cozy-chess-types" 82 | version = "0.2.1" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "6f4a557c6659b8705d301732f473c9a61659c740ffda3e3485e79d1d4b7a1e12" 85 | 86 | [[package]] 87 | name = "ctrlc" 88 | version = "3.2.5" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "bbcf33c2a618cbe41ee43ae6e9f2e48368cd9f9db2896f10167d8d762679f639" 91 | dependencies = [ 92 | "nix", 93 | "windows-sys", 94 | ] 95 | 96 | [[package]] 97 | name = "datagen" 98 | version = "0.1.0" 99 | dependencies = [ 100 | "chrono", 101 | "cozy-chess", 102 | "ctrlc", 103 | "engine", 104 | "fastrand", 105 | "tabled", 106 | ] 107 | 108 | [[package]] 109 | name = "engine" 110 | version = "0.1.0" 111 | dependencies = [ 112 | "cozy-chess", 113 | "once_cell", 114 | ] 115 | 116 | [[package]] 117 | name = "fastrand" 118 | version = "1.9.0" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" 121 | dependencies = [ 122 | "instant", 123 | ] 124 | 125 | [[package]] 126 | name = "fnv" 127 | version = "1.0.7" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 130 | 131 | [[package]] 132 | name = "heck" 133 | version = "0.4.1" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 136 | 137 | [[package]] 138 | name = "iana-time-zone" 139 | version = "0.1.56" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" 142 | dependencies = [ 143 | "android_system_properties", 144 | "core-foundation-sys", 145 | "iana-time-zone-haiku", 146 | "js-sys", 147 | "wasm-bindgen", 148 | "windows", 149 | ] 150 | 151 | [[package]] 152 | name = "iana-time-zone-haiku" 153 | version = "0.1.2" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 156 | dependencies = [ 157 | "cc", 158 | ] 159 | 160 | [[package]] 161 | name = "instant" 162 | version = "0.1.12" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 165 | dependencies = [ 166 | "cfg-if", 167 | ] 168 | 169 | [[package]] 170 | name = "js-sys" 171 | version = "0.3.62" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "68c16e1bfd491478ab155fd8b4896b86f9ede344949b641e61501e07c2b8b4d5" 174 | dependencies = [ 175 | "wasm-bindgen", 176 | ] 177 | 178 | [[package]] 179 | name = "libc" 180 | version = "0.2.144" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" 183 | 184 | [[package]] 185 | name = "log" 186 | version = "0.4.17" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 189 | dependencies = [ 190 | "cfg-if", 191 | ] 192 | 193 | [[package]] 194 | name = "nix" 195 | version = "0.26.2" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" 198 | dependencies = [ 199 | "bitflags", 200 | "cfg-if", 201 | "libc", 202 | "static_assertions", 203 | ] 204 | 205 | [[package]] 206 | name = "num-integer" 207 | version = "0.1.45" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 210 | dependencies = [ 211 | "autocfg", 212 | "num-traits", 213 | ] 214 | 215 | [[package]] 216 | name = "num-traits" 217 | version = "0.2.15" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 220 | dependencies = [ 221 | "autocfg", 222 | ] 223 | 224 | [[package]] 225 | name = "once_cell" 226 | version = "1.17.1" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" 229 | 230 | [[package]] 231 | name = "papergrid" 232 | version = "0.9.0" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "1fdfe703c51ddc52887ad78fc69cd2ea78d895ffcd6e955c9d03566db8ab5bb1" 235 | dependencies = [ 236 | "bytecount", 237 | "fnv", 238 | "unicode-width", 239 | ] 240 | 241 | [[package]] 242 | name = "proc-macro-error" 243 | version = "1.0.4" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 246 | dependencies = [ 247 | "proc-macro-error-attr", 248 | "proc-macro2", 249 | "quote", 250 | "syn 1.0.109", 251 | "version_check", 252 | ] 253 | 254 | [[package]] 255 | name = "proc-macro-error-attr" 256 | version = "1.0.4" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 259 | dependencies = [ 260 | "proc-macro2", 261 | "quote", 262 | "version_check", 263 | ] 264 | 265 | [[package]] 266 | name = "proc-macro2" 267 | version = "1.0.57" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "c4ec6d5fe0b140acb27c9a0444118cf55bfbb4e0b259739429abb4521dd67c16" 270 | dependencies = [ 271 | "unicode-ident", 272 | ] 273 | 274 | [[package]] 275 | name = "quote" 276 | version = "1.0.27" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" 279 | dependencies = [ 280 | "proc-macro2", 281 | ] 282 | 283 | [[package]] 284 | name = "static_assertions" 285 | version = "1.1.0" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 288 | 289 | [[package]] 290 | name = "syn" 291 | version = "1.0.109" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 294 | dependencies = [ 295 | "proc-macro2", 296 | "quote", 297 | "unicode-ident", 298 | ] 299 | 300 | [[package]] 301 | name = "syn" 302 | version = "2.0.16" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" 305 | dependencies = [ 306 | "proc-macro2", 307 | "quote", 308 | "unicode-ident", 309 | ] 310 | 311 | [[package]] 312 | name = "tabled" 313 | version = "0.12.0" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "da1a2e56bbf7bfdd08aaa7592157a742205459eff774b73bc01809ae2d99dc2a" 316 | dependencies = [ 317 | "papergrid", 318 | "tabled_derive", 319 | "unicode-width", 320 | ] 321 | 322 | [[package]] 323 | name = "tabled_derive" 324 | version = "0.6.0" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "99f688a08b54f4f02f0a3c382aefdb7884d3d69609f785bd253dc033243e3fe4" 327 | dependencies = [ 328 | "heck", 329 | "proc-macro-error", 330 | "proc-macro2", 331 | "quote", 332 | "syn 1.0.109", 333 | ] 334 | 335 | [[package]] 336 | name = "time" 337 | version = "0.1.45" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" 340 | dependencies = [ 341 | "libc", 342 | "wasi", 343 | "winapi", 344 | ] 345 | 346 | [[package]] 347 | name = "unicode-ident" 348 | version = "1.0.8" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" 351 | 352 | [[package]] 353 | name = "unicode-width" 354 | version = "0.1.10" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" 357 | 358 | [[package]] 359 | name = "version_check" 360 | version = "0.9.4" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 363 | 364 | [[package]] 365 | name = "wasi" 366 | version = "0.10.0+wasi-snapshot-preview1" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 369 | 370 | [[package]] 371 | name = "wasm-bindgen" 372 | version = "0.2.85" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "5b6cb788c4e39112fbe1822277ef6fb3c55cd86b95cb3d3c4c1c9597e4ac74b4" 375 | dependencies = [ 376 | "cfg-if", 377 | "wasm-bindgen-macro", 378 | ] 379 | 380 | [[package]] 381 | name = "wasm-bindgen-backend" 382 | version = "0.2.85" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "35e522ed4105a9d626d885b35d62501b30d9666283a5c8be12c14a8bdafe7822" 385 | dependencies = [ 386 | "bumpalo", 387 | "log", 388 | "once_cell", 389 | "proc-macro2", 390 | "quote", 391 | "syn 2.0.16", 392 | "wasm-bindgen-shared", 393 | ] 394 | 395 | [[package]] 396 | name = "wasm-bindgen-macro" 397 | version = "0.2.85" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "358a79a0cb89d21db8120cbfb91392335913e4890665b1a7981d9e956903b434" 400 | dependencies = [ 401 | "quote", 402 | "wasm-bindgen-macro-support", 403 | ] 404 | 405 | [[package]] 406 | name = "wasm-bindgen-macro-support" 407 | version = "0.2.85" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "4783ce29f09b9d93134d41297aded3a712b7b979e9c6f28c32cb88c973a94869" 410 | dependencies = [ 411 | "proc-macro2", 412 | "quote", 413 | "syn 2.0.16", 414 | "wasm-bindgen-backend", 415 | "wasm-bindgen-shared", 416 | ] 417 | 418 | [[package]] 419 | name = "wasm-bindgen-shared" 420 | version = "0.2.85" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "a901d592cafaa4d711bc324edfaff879ac700b19c3dfd60058d2b445be2691eb" 423 | 424 | [[package]] 425 | name = "winapi" 426 | version = "0.3.9" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 429 | dependencies = [ 430 | "winapi-i686-pc-windows-gnu", 431 | "winapi-x86_64-pc-windows-gnu", 432 | ] 433 | 434 | [[package]] 435 | name = "winapi-i686-pc-windows-gnu" 436 | version = "0.4.0" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 439 | 440 | [[package]] 441 | name = "winapi-x86_64-pc-windows-gnu" 442 | version = "0.4.0" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 445 | 446 | [[package]] 447 | name = "windows" 448 | version = "0.48.0" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" 451 | dependencies = [ 452 | "windows-targets 0.48.0", 453 | ] 454 | 455 | [[package]] 456 | name = "windows-sys" 457 | version = "0.45.0" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 460 | dependencies = [ 461 | "windows-targets 0.42.2", 462 | ] 463 | 464 | [[package]] 465 | name = "windows-targets" 466 | version = "0.42.2" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 469 | dependencies = [ 470 | "windows_aarch64_gnullvm 0.42.2", 471 | "windows_aarch64_msvc 0.42.2", 472 | "windows_i686_gnu 0.42.2", 473 | "windows_i686_msvc 0.42.2", 474 | "windows_x86_64_gnu 0.42.2", 475 | "windows_x86_64_gnullvm 0.42.2", 476 | "windows_x86_64_msvc 0.42.2", 477 | ] 478 | 479 | [[package]] 480 | name = "windows-targets" 481 | version = "0.48.0" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" 484 | dependencies = [ 485 | "windows_aarch64_gnullvm 0.48.0", 486 | "windows_aarch64_msvc 0.48.0", 487 | "windows_i686_gnu 0.48.0", 488 | "windows_i686_msvc 0.48.0", 489 | "windows_x86_64_gnu 0.48.0", 490 | "windows_x86_64_gnullvm 0.48.0", 491 | "windows_x86_64_msvc 0.48.0", 492 | ] 493 | 494 | [[package]] 495 | name = "windows_aarch64_gnullvm" 496 | version = "0.42.2" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 499 | 500 | [[package]] 501 | name = "windows_aarch64_gnullvm" 502 | version = "0.48.0" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" 505 | 506 | [[package]] 507 | name = "windows_aarch64_msvc" 508 | version = "0.42.2" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 511 | 512 | [[package]] 513 | name = "windows_aarch64_msvc" 514 | version = "0.48.0" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" 517 | 518 | [[package]] 519 | name = "windows_i686_gnu" 520 | version = "0.42.2" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 523 | 524 | [[package]] 525 | name = "windows_i686_gnu" 526 | version = "0.48.0" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" 529 | 530 | [[package]] 531 | name = "windows_i686_msvc" 532 | version = "0.42.2" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 535 | 536 | [[package]] 537 | name = "windows_i686_msvc" 538 | version = "0.48.0" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" 541 | 542 | [[package]] 543 | name = "windows_x86_64_gnu" 544 | version = "0.42.2" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 547 | 548 | [[package]] 549 | name = "windows_x86_64_gnu" 550 | version = "0.48.0" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" 553 | 554 | [[package]] 555 | name = "windows_x86_64_gnullvm" 556 | version = "0.42.2" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 559 | 560 | [[package]] 561 | name = "windows_x86_64_gnullvm" 562 | version = "0.48.0" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" 565 | 566 | [[package]] 567 | name = "windows_x86_64_msvc" 568 | version = "0.42.2" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 571 | 572 | [[package]] 573 | name = "windows_x86_64_msvc" 574 | version = "0.48.0" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" 577 | -------------------------------------------------------------------------------- /engine/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "android_system_properties" 7 | version = "0.1.5" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 10 | dependencies = [ 11 | "libc", 12 | ] 13 | 14 | [[package]] 15 | name = "autocfg" 16 | version = "1.1.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 19 | 20 | [[package]] 21 | name = "bitflags" 22 | version = "1.3.2" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 25 | 26 | [[package]] 27 | name = "bumpalo" 28 | version = "3.12.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" 31 | 32 | [[package]] 33 | name = "bytecount" 34 | version = "0.6.3" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" 37 | 38 | [[package]] 39 | name = "cc" 40 | version = "1.0.79" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 43 | 44 | [[package]] 45 | name = "cfg-if" 46 | version = "1.0.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 49 | 50 | [[package]] 51 | name = "chrono" 52 | version = "0.4.24" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" 55 | dependencies = [ 56 | "iana-time-zone", 57 | "js-sys", 58 | "num-integer", 59 | "num-traits", 60 | "time", 61 | "wasm-bindgen", 62 | "winapi", 63 | ] 64 | 65 | [[package]] 66 | name = "codespan-reporting" 67 | version = "0.11.1" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" 70 | dependencies = [ 71 | "termcolor", 72 | "unicode-width", 73 | ] 74 | 75 | [[package]] 76 | name = "core-foundation-sys" 77 | version = "0.8.3" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" 80 | 81 | [[package]] 82 | name = "cozy-chess" 83 | version = "0.3.2" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "f0aa2dcdbe66b10019af0f81ee4f404ca12c1d24e56f47e7f2588954f8bfa306" 86 | dependencies = [ 87 | "cozy-chess-types", 88 | ] 89 | 90 | [[package]] 91 | name = "cozy-chess-types" 92 | version = "0.2.1" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "6f4a557c6659b8705d301732f473c9a61659c740ffda3e3485e79d1d4b7a1e12" 95 | 96 | [[package]] 97 | name = "ctrlc" 98 | version = "3.2.5" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "bbcf33c2a618cbe41ee43ae6e9f2e48368cd9f9db2896f10167d8d762679f639" 101 | dependencies = [ 102 | "nix", 103 | "windows-sys", 104 | ] 105 | 106 | [[package]] 107 | name = "cxx" 108 | version = "1.0.94" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93" 111 | dependencies = [ 112 | "cc", 113 | "cxxbridge-flags", 114 | "cxxbridge-macro", 115 | "link-cplusplus", 116 | ] 117 | 118 | [[package]] 119 | name = "cxx-build" 120 | version = "1.0.94" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b" 123 | dependencies = [ 124 | "cc", 125 | "codespan-reporting", 126 | "once_cell", 127 | "proc-macro2", 128 | "quote", 129 | "scratch", 130 | "syn 2.0.12", 131 | ] 132 | 133 | [[package]] 134 | name = "cxxbridge-flags" 135 | version = "1.0.94" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb" 138 | 139 | [[package]] 140 | name = "cxxbridge-macro" 141 | version = "1.0.94" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" 144 | dependencies = [ 145 | "proc-macro2", 146 | "quote", 147 | "syn 2.0.12", 148 | ] 149 | 150 | [[package]] 151 | name = "fastrand" 152 | version = "1.9.0" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" 155 | dependencies = [ 156 | "instant", 157 | ] 158 | 159 | [[package]] 160 | name = "fnv" 161 | version = "1.0.7" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 164 | 165 | [[package]] 166 | name = "heck" 167 | version = "0.4.1" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 170 | 171 | [[package]] 172 | name = "iana-time-zone" 173 | version = "0.1.54" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "0c17cc76786e99f8d2f055c11159e7f0091c42474dcc3189fbab96072e873e6d" 176 | dependencies = [ 177 | "android_system_properties", 178 | "core-foundation-sys", 179 | "iana-time-zone-haiku", 180 | "js-sys", 181 | "wasm-bindgen", 182 | "windows", 183 | ] 184 | 185 | [[package]] 186 | name = "iana-time-zone-haiku" 187 | version = "0.1.1" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" 190 | dependencies = [ 191 | "cxx", 192 | "cxx-build", 193 | ] 194 | 195 | [[package]] 196 | name = "instant" 197 | version = "0.1.12" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 200 | dependencies = [ 201 | "cfg-if", 202 | ] 203 | 204 | [[package]] 205 | name = "js-sys" 206 | version = "0.3.61" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" 209 | dependencies = [ 210 | "wasm-bindgen", 211 | ] 212 | 213 | [[package]] 214 | name = "libc" 215 | version = "0.2.140" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" 218 | 219 | [[package]] 220 | name = "link-cplusplus" 221 | version = "1.0.8" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" 224 | dependencies = [ 225 | "cc", 226 | ] 227 | 228 | [[package]] 229 | name = "log" 230 | version = "0.4.17" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 233 | dependencies = [ 234 | "cfg-if", 235 | ] 236 | 237 | [[package]] 238 | name = "nix" 239 | version = "0.26.2" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" 242 | dependencies = [ 243 | "bitflags", 244 | "cfg-if", 245 | "libc", 246 | "static_assertions", 247 | ] 248 | 249 | [[package]] 250 | name = "num-integer" 251 | version = "0.1.45" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 254 | dependencies = [ 255 | "autocfg", 256 | "num-traits", 257 | ] 258 | 259 | [[package]] 260 | name = "num-traits" 261 | version = "0.2.15" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 264 | dependencies = [ 265 | "autocfg", 266 | ] 267 | 268 | [[package]] 269 | name = "once_cell" 270 | version = "1.17.1" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" 273 | 274 | [[package]] 275 | name = "papergrid" 276 | version = "0.9.0" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "1fdfe703c51ddc52887ad78fc69cd2ea78d895ffcd6e955c9d03566db8ab5bb1" 279 | dependencies = [ 280 | "bytecount", 281 | "fnv", 282 | "unicode-width", 283 | ] 284 | 285 | [[package]] 286 | name = "proc-macro-error" 287 | version = "1.0.4" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 290 | dependencies = [ 291 | "proc-macro-error-attr", 292 | "proc-macro2", 293 | "quote", 294 | "syn 1.0.109", 295 | "version_check", 296 | ] 297 | 298 | [[package]] 299 | name = "proc-macro-error-attr" 300 | version = "1.0.4" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 303 | dependencies = [ 304 | "proc-macro2", 305 | "quote", 306 | "version_check", 307 | ] 308 | 309 | [[package]] 310 | name = "proc-macro2" 311 | version = "1.0.54" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "e472a104799c74b514a57226160104aa483546de37e839ec50e3c2e41dd87534" 314 | dependencies = [ 315 | "unicode-ident", 316 | ] 317 | 318 | [[package]] 319 | name = "quote" 320 | version = "1.0.26" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" 323 | dependencies = [ 324 | "proc-macro2", 325 | ] 326 | 327 | [[package]] 328 | name = "scratch" 329 | version = "1.0.5" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" 332 | 333 | [[package]] 334 | name = "static_assertions" 335 | version = "1.1.0" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 338 | 339 | [[package]] 340 | name = "svart" 341 | version = "4.2.0" 342 | dependencies = [ 343 | "chrono", 344 | "cozy-chess", 345 | "ctrlc", 346 | "fastrand", 347 | "once_cell", 348 | "tabled", 349 | ] 350 | 351 | [[package]] 352 | name = "syn" 353 | version = "1.0.109" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 356 | dependencies = [ 357 | "proc-macro2", 358 | "quote", 359 | "unicode-ident", 360 | ] 361 | 362 | [[package]] 363 | name = "syn" 364 | version = "2.0.12" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "79d9531f94112cfc3e4c8f5f02cb2b58f72c97b7efd85f70203cc6d8efda5927" 367 | dependencies = [ 368 | "proc-macro2", 369 | "quote", 370 | "unicode-ident", 371 | ] 372 | 373 | [[package]] 374 | name = "tabled" 375 | version = "0.12.0" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "da1a2e56bbf7bfdd08aaa7592157a742205459eff774b73bc01809ae2d99dc2a" 378 | dependencies = [ 379 | "papergrid", 380 | "tabled_derive", 381 | "unicode-width", 382 | ] 383 | 384 | [[package]] 385 | name = "tabled_derive" 386 | version = "0.6.0" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "99f688a08b54f4f02f0a3c382aefdb7884d3d69609f785bd253dc033243e3fe4" 389 | dependencies = [ 390 | "heck", 391 | "proc-macro-error", 392 | "proc-macro2", 393 | "quote", 394 | "syn 1.0.109", 395 | ] 396 | 397 | [[package]] 398 | name = "termcolor" 399 | version = "1.2.0" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" 402 | dependencies = [ 403 | "winapi-util", 404 | ] 405 | 406 | [[package]] 407 | name = "time" 408 | version = "0.1.45" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" 411 | dependencies = [ 412 | "libc", 413 | "wasi", 414 | "winapi", 415 | ] 416 | 417 | [[package]] 418 | name = "unicode-ident" 419 | version = "1.0.8" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" 422 | 423 | [[package]] 424 | name = "unicode-width" 425 | version = "0.1.10" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" 428 | 429 | [[package]] 430 | name = "version_check" 431 | version = "0.9.4" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 434 | 435 | [[package]] 436 | name = "wasi" 437 | version = "0.10.0+wasi-snapshot-preview1" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 440 | 441 | [[package]] 442 | name = "wasm-bindgen" 443 | version = "0.2.84" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" 446 | dependencies = [ 447 | "cfg-if", 448 | "wasm-bindgen-macro", 449 | ] 450 | 451 | [[package]] 452 | name = "wasm-bindgen-backend" 453 | version = "0.2.84" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" 456 | dependencies = [ 457 | "bumpalo", 458 | "log", 459 | "once_cell", 460 | "proc-macro2", 461 | "quote", 462 | "syn 1.0.109", 463 | "wasm-bindgen-shared", 464 | ] 465 | 466 | [[package]] 467 | name = "wasm-bindgen-macro" 468 | version = "0.2.84" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" 471 | dependencies = [ 472 | "quote", 473 | "wasm-bindgen-macro-support", 474 | ] 475 | 476 | [[package]] 477 | name = "wasm-bindgen-macro-support" 478 | version = "0.2.84" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" 481 | dependencies = [ 482 | "proc-macro2", 483 | "quote", 484 | "syn 1.0.109", 485 | "wasm-bindgen-backend", 486 | "wasm-bindgen-shared", 487 | ] 488 | 489 | [[package]] 490 | name = "wasm-bindgen-shared" 491 | version = "0.2.84" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" 494 | 495 | [[package]] 496 | name = "winapi" 497 | version = "0.3.9" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 500 | dependencies = [ 501 | "winapi-i686-pc-windows-gnu", 502 | "winapi-x86_64-pc-windows-gnu", 503 | ] 504 | 505 | [[package]] 506 | name = "winapi-i686-pc-windows-gnu" 507 | version = "0.4.0" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 510 | 511 | [[package]] 512 | name = "winapi-util" 513 | version = "0.1.5" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 516 | dependencies = [ 517 | "winapi", 518 | ] 519 | 520 | [[package]] 521 | name = "winapi-x86_64-pc-windows-gnu" 522 | version = "0.4.0" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 525 | 526 | [[package]] 527 | name = "windows" 528 | version = "0.46.0" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "cdacb41e6a96a052c6cb63a144f24900236121c6f63f4f8219fef5977ecb0c25" 531 | dependencies = [ 532 | "windows-targets", 533 | ] 534 | 535 | [[package]] 536 | name = "windows-sys" 537 | version = "0.45.0" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 540 | dependencies = [ 541 | "windows-targets", 542 | ] 543 | 544 | [[package]] 545 | name = "windows-targets" 546 | version = "0.42.2" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 549 | dependencies = [ 550 | "windows_aarch64_gnullvm", 551 | "windows_aarch64_msvc", 552 | "windows_i686_gnu", 553 | "windows_i686_msvc", 554 | "windows_x86_64_gnu", 555 | "windows_x86_64_gnullvm", 556 | "windows_x86_64_msvc", 557 | ] 558 | 559 | [[package]] 560 | name = "windows_aarch64_gnullvm" 561 | version = "0.42.2" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 564 | 565 | [[package]] 566 | name = "windows_aarch64_msvc" 567 | version = "0.42.2" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 570 | 571 | [[package]] 572 | name = "windows_i686_gnu" 573 | version = "0.42.2" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 576 | 577 | [[package]] 578 | name = "windows_i686_msvc" 579 | version = "0.42.2" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 582 | 583 | [[package]] 584 | name = "windows_x86_64_gnu" 585 | version = "0.42.2" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 588 | 589 | [[package]] 590 | name = "windows_x86_64_gnullvm" 591 | version = "0.42.2" 592 | source = "registry+https://github.com/rust-lang/crates.io-index" 593 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 594 | 595 | [[package]] 596 | name = "windows_x86_64_msvc" 597 | version = "0.42.2" 598 | source = "registry+https://github.com/rust-lang/crates.io-index" 599 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 600 | -------------------------------------------------------------------------------- /engine/src/uci/handler.rs: -------------------------------------------------------------------------------- 1 | use super::timeman::time_for_move; 2 | 3 | use crate::body::{ 4 | history::History, 5 | nnue::inference::NNUEState, 6 | search::{clear_nodes, store_stop, Search}, 7 | tt::TT, 8 | }; 9 | use crate::definitions::MATE; 10 | 11 | use cozy_chess::{Board, Color, Move, Piece, Square}; 12 | 13 | #[derive(Debug, PartialEq, Copy, Clone)] 14 | pub enum SearchType { 15 | // opt_time and maxtime 16 | Time(u64, u64), 17 | Nodes(u64), 18 | Depth(usize), 19 | Infinite, 20 | } 21 | 22 | struct UCIOptions { 23 | hash: u64, 24 | threads: u32, 25 | } 26 | 27 | impl UCIOptions { 28 | fn new() -> Self { 29 | Self { 30 | hash: 16, 31 | threads: 1, 32 | } 33 | } 34 | } 35 | 36 | impl Default for UCIOptions { 37 | fn default() -> Self { 38 | Self::new() 39 | } 40 | } 41 | 42 | fn id() { 43 | println!("id name Svart 6"); 44 | println!("id author Cristopher Torgrip"); 45 | } 46 | 47 | fn options() { 48 | println!("option name Hash type spin default 16 min 1 max 1000000"); 49 | println!("option name Threads type spin default 1 min 1 max 1024"); 50 | } 51 | 52 | pub fn uci_loop() { 53 | // This should (and will be) made into an object in and of itself later 54 | let mut uci_set = false; 55 | let mut board_set = false; 56 | let mut board = Board::default(); 57 | let mut stored_input: Option = None; 58 | 59 | let mut uci_options = UCIOptions::default(); 60 | let mut tt = TT::new(uci_options.hash); 61 | 62 | let mut nnue = NNUEState::from_board(&board); 63 | let mut history = History::new(); 64 | let mut game_history = vec![]; 65 | 66 | loop { 67 | let line = if let Some(si) = stored_input.clone() { 68 | stored_input = None; 69 | si 70 | } else { 71 | read_input().unwrap() 72 | }; 73 | 74 | let words: Vec<&str> = line.split_whitespace().collect(); 75 | if words.is_empty() { 76 | continue; 77 | } 78 | 79 | if !uci_set { 80 | match words[0] { 81 | "uci" => { 82 | id(); 83 | options(); 84 | println!("uciok"); 85 | uci_set = true; 86 | } 87 | "quit" => { 88 | break; 89 | } 90 | "bench" => { 91 | super::bench::bench(); 92 | break; 93 | } 94 | "position" => set_position( 95 | &mut board, 96 | &mut nnue, 97 | &mut game_history, 98 | &mut board_set, 99 | words, 100 | ), 101 | "go" => { 102 | let mut search = Search::new(&tt, &nnue, &history, &game_history); 103 | 104 | if board_set { 105 | search.iterative_deepening::(&board, SearchType::Infinite, true); 106 | } else { 107 | search.iterative_deepening::( 108 | &Board::default(), 109 | SearchType::Infinite, 110 | true, 111 | ); 112 | } 113 | } 114 | _ => (), 115 | } 116 | } else { 117 | match words[0] { 118 | "uci" => { 119 | id(); 120 | options(); 121 | println!("uciok"); 122 | 123 | continue; 124 | } 125 | "isready" => { 126 | println!("readyok"); 127 | continue; 128 | } 129 | "ucinewgame" => { 130 | board = Board::default(); 131 | tt = TT::new(uci_options.hash); 132 | nnue.refresh(&board); 133 | history = History::new(); 134 | game_history = vec![board.hash()]; 135 | board_set = true; 136 | 137 | continue; 138 | } 139 | "setoption" => { 140 | if words[1] == "name" && words[2] == "Hash" && words[3] == "value" { 141 | if let Ok(s) = words[4].parse::() { 142 | if s > 1_000_000 { 143 | continue; 144 | } 145 | 146 | uci_options.hash = s; 147 | tt = TT::new(uci_options.hash); 148 | } 149 | } 150 | 151 | if words[1] == "name" && words[2] == "Threads" && words[3] == "value" { 152 | if let Ok(t) = words[4].parse::() { 153 | if !(1..=1024).contains(&t) { 154 | continue; 155 | } 156 | 157 | uci_options.threads = t; 158 | } 159 | } 160 | 161 | continue; 162 | } 163 | "position" => set_position( 164 | &mut board, 165 | &mut nnue, 166 | &mut game_history, 167 | &mut board_set, 168 | words, 169 | ), 170 | "go" => { 171 | if board_set { 172 | // Static depth search 173 | if words.iter().any(|&x| x == "depth") { 174 | if let Ok(d) = words 175 | [words.iter().position(|&x| x == "depth").unwrap() + 1] 176 | .parse::() 177 | { 178 | go( 179 | &board, 180 | SearchType::Depth(d), 181 | &mut tt, 182 | &nnue, 183 | &mut history, 184 | &game_history, 185 | &mut stored_input, 186 | &uci_options, 187 | ); 188 | } 189 | } else if words.iter().any(|&x| x == "nodes") { 190 | if let Ok(n) = words 191 | [words.iter().position(|&x| x == "nodes").unwrap() + 1] 192 | .parse::() 193 | { 194 | go( 195 | &board, 196 | SearchType::Nodes(n), 197 | &mut tt, 198 | &nnue, 199 | &mut history, 200 | &game_history, 201 | &mut stored_input, 202 | &uci_options, 203 | ); 204 | } 205 | // Infinite search 206 | } else if words.iter().any(|&x| x == "infinite") { 207 | go( 208 | &board, 209 | SearchType::Infinite, 210 | &mut tt, 211 | &nnue, 212 | &mut history, 213 | &game_history, 214 | &mut stored_input, 215 | &uci_options, 216 | ); 217 | // Static time search 218 | } else if words.iter().any(|&x| x == "movetime") { 219 | if let Ok(t) = words 220 | [words.iter().position(|&x| x == "movetime").unwrap() + 1] 221 | .parse::() 222 | { 223 | go( 224 | &board, 225 | SearchType::Time(t, t), 226 | &mut tt, 227 | &nnue, 228 | &mut history, 229 | &game_history, 230 | &mut stored_input, 231 | &uci_options, 232 | ); 233 | } 234 | // Time search 235 | } else if words.iter().any(|&x| x == "wtime" || x == "btime") { 236 | if board.side_to_move() == Color::White { 237 | if let Ok(t) = words 238 | [words.iter().position(|&x| x == "wtime").unwrap() + 1] 239 | .parse::() 240 | { 241 | // Increment 242 | let inc = if words.iter().any(|&x| x == "winc") { 243 | match words 244 | [words.iter().position(|&x| x == "winc").unwrap() + 1] 245 | .parse::() 246 | { 247 | Ok(i) => i, 248 | Err(_) => panic!("Could not parse increment"), 249 | } 250 | } else { 251 | 0 252 | }; 253 | 254 | let mtg = if words.iter().any(|&x| x == "movestogo") { 255 | match words[words 256 | .iter() 257 | .position(|&x| x == "movestogo") 258 | .unwrap() 259 | + 1] 260 | .parse::() 261 | { 262 | Ok(m) => Some(m), 263 | Err(_) => None, 264 | } 265 | } else { 266 | None 267 | }; 268 | 269 | let (opt, max) = time_for_move(t, inc, mtg); 270 | 271 | go( 272 | &board, 273 | SearchType::Time(opt, max), 274 | &mut tt, 275 | &nnue, 276 | &mut history, 277 | &game_history, 278 | &mut stored_input, 279 | &uci_options, 280 | ); 281 | } 282 | } else if let Ok(t) = words 283 | [words.iter().position(|&x| x == "btime").unwrap() + 1] 284 | .parse::() 285 | { 286 | // Increment 287 | let inc = if words.iter().any(|&x| x == "binc") { 288 | match words 289 | [words.iter().position(|&x| x == "binc").unwrap() + 1] 290 | .parse::() 291 | { 292 | Ok(i) => i, 293 | Err(_) => panic!("Could not parse increment"), 294 | } 295 | } else { 296 | 0 297 | }; 298 | 299 | let mtg = if words.iter().any(|&x| x == "movestogo") { 300 | match words 301 | [words.iter().position(|&x| x == "movestogo").unwrap() + 1] 302 | .parse::() 303 | { 304 | Ok(m) => Some(m), 305 | Err(_) => None, 306 | } 307 | } else { 308 | None 309 | }; 310 | 311 | let (opt, max) = time_for_move(t, inc, mtg); 312 | 313 | go( 314 | &board, 315 | SearchType::Time(opt, max), 316 | &mut tt, 317 | &nnue, 318 | &mut history, 319 | &game_history, 320 | &mut stored_input, 321 | &uci_options, 322 | ); 323 | }; 324 | } else { 325 | continue; 326 | } 327 | } 328 | continue; 329 | } 330 | "eval" => { 331 | println!("{}", nnue.evaluate(board.side_to_move())); 332 | } 333 | "quit" => { 334 | break; 335 | } 336 | _ => { 337 | continue; 338 | } 339 | } 340 | } 341 | } 342 | } 343 | 344 | fn check_castling_move(board: &Board, mut mv: Move) -> Move { 345 | if board.piece_on(mv.from) == Some(Piece::King) { 346 | mv.to = match (mv.from, mv.to) { 347 | (Square::E1, Square::G1) => Square::H1, 348 | (Square::E8, Square::G8) => Square::H8, 349 | (Square::E1, Square::C1) => Square::A1, 350 | (Square::E8, Square::C8) => Square::A8, 351 | _ => mv.to, 352 | }; 353 | } 354 | mv 355 | } 356 | 357 | pub fn reverse_castling_move(board: &Board, mut mv: Move) -> Move { 358 | if board.piece_on(mv.from) == Some(Piece::King) { 359 | mv.to = match (mv.from, mv.to) { 360 | (Square::E1, Square::H1) => Square::G1, 361 | (Square::E8, Square::H8) => Square::G8, 362 | (Square::E1, Square::A1) => Square::C1, 363 | (Square::E8, Square::A8) => Square::C8, 364 | _ => mv.to, 365 | }; 366 | } 367 | mv 368 | } 369 | 370 | fn read_input() -> Result { 371 | let mut line = String::new(); 372 | let bytes_read = std::io::stdin().read_line(&mut line).unwrap(); 373 | 374 | if bytes_read == 0 { 375 | // EOF 376 | return Err(()); 377 | } 378 | 379 | Ok(line.trim().to_string()) 380 | } 381 | 382 | #[allow(clippy::borrowed_box, clippy::too_many_arguments)] 383 | fn go( 384 | board: &Board, 385 | st: SearchType, 386 | tt: &mut TT, 387 | nnue: &Box, 388 | history: &mut History, 389 | game_history: &Vec, 390 | stored_input: &mut Option, 391 | uci_options: &UCIOptions, 392 | ) { 393 | let mut search = Search::new(tt, nnue, history, game_history); 394 | let mut secondary_searchers = vec![]; 395 | 396 | for _ in 0..uci_options.threads - 1 { 397 | secondary_searchers.push(Search::new(tt, nnue, history, game_history)); 398 | } 399 | 400 | std::thread::scope(|s| { 401 | s.spawn(|| { 402 | search.iterative_deepening::(board, st, false); 403 | }); 404 | 405 | for searcher in secondary_searchers.iter_mut() { 406 | s.spawn(|| { 407 | searcher.iterative_deepening::(board, st, false); 408 | }); 409 | } 410 | 411 | *stored_input = handle_stop_and_quit(); 412 | }); 413 | 414 | *history = search.info.history; 415 | history.age_table(); 416 | tt.age(); 417 | 418 | store_stop(false); 419 | clear_nodes(); 420 | } 421 | 422 | fn handle_stop_and_quit() -> Option { 423 | loop { 424 | let line = read_input().unwrap(); 425 | 426 | match line.as_str().trim() { 427 | "stop" => { 428 | crate::body::search::store_stop(true); 429 | return None; 430 | } 431 | "quit" => std::process::exit(0), 432 | "isready" => println!("readyok"), 433 | _ => { 434 | if crate::body::search::load_stop() { 435 | return Some(line); 436 | } 437 | } 438 | } 439 | } 440 | } 441 | 442 | fn set_position( 443 | board: &mut Board, 444 | nnue: &mut NNUEState, 445 | game_history: &mut Vec, 446 | board_set: &mut bool, 447 | words: Vec<&str>, 448 | ) { 449 | if words[1] == "startpos" { 450 | *board = Board::default(); 451 | *board_set = true; 452 | *game_history = vec![board.hash()] 453 | } else if words[1] == "fen" { 454 | // Put together the split fen string 455 | let mut fen = String::new(); 456 | for word in words.iter().skip(2) { 457 | if *word == "moves" { 458 | break; 459 | } 460 | fen.push_str(word); 461 | fen.push(' '); 462 | } 463 | 464 | if let Ok(b) = Board::from_fen(fen.trim(), false) { 465 | *board = b; 466 | *board_set = true; 467 | *game_history = vec![board.hash()] 468 | } 469 | } 470 | 471 | if words.iter().any(|&x| x == "moves") && *board_set { 472 | for word in words 473 | .iter() 474 | .skip(words.iter().position(|&x| x == "moves").unwrap() + 1) 475 | { 476 | let mut mv: Move = word.parse().unwrap(); 477 | mv = check_castling_move(board, mv); 478 | board.play_unchecked(mv); 479 | game_history.push(board.hash()); 480 | } 481 | } 482 | 483 | if *board_set { 484 | nnue.refresh(board); 485 | } 486 | } 487 | 488 | pub fn pretty_print( 489 | depth: usize, 490 | seldepth: usize, 491 | score: i32, 492 | nodes: u64, 493 | timer: u128, 494 | pv: String, 495 | ) { 496 | const DEFAULT: &str = "\x1b[0m"; 497 | const GREY: &str = "\x1b[90m"; 498 | const GREEN: &str = "\x1b[32m"; 499 | const BRIGHT_GREEN: &str = "\x1b[92m"; 500 | const BRIGHT_CYAN: &str = "\x1b[96m"; 501 | const BRIGHT_YELLOW: &str = "\x1b[93m"; 502 | const RED: &str = "\x1b[31m"; 503 | const BRIGHT_RED: &str = "\x1b[91m"; 504 | 505 | let t = match timer { 506 | 0..=999 => { 507 | format!("{GREY}{}ms{DEFAULT}", timer as f64) 508 | } 509 | 1000..=59_999 => { 510 | format!("{GREY}{:.2}s{DEFAULT}", timer as f64 / 1000.) 511 | } 512 | 60_000..=3_599_999 => { 513 | format!("{GREY}{:.2}m{DEFAULT}", timer as f64 / 60_000.) 514 | } 515 | 3_600_000..=86_399_999 => { 516 | format!("{GREY}{:.2}h{DEFAULT}", timer as f64 / 3_600_000.) 517 | } 518 | 86_400_000.. => { 519 | format!("{GREY}{:.2}d{DEFAULT}", timer as f64 / 86_400_000.) 520 | } 521 | }; 522 | 523 | let mate = ((MATE - score) / 2) + ((MATE - score) & 1); 524 | let norm_score = score as f32 / 100.; 525 | let sc = match score { 526 | 501..=15_000 => format!("{BRIGHT_CYAN}+{:.2}{DEFAULT}", norm_score), 527 | 101..=500 => format!("{GREEN}+{:.2}{DEFAULT}", norm_score), 528 | 11..=100 => format!("{BRIGHT_GREEN}+{:.2}{DEFAULT}", norm_score), 529 | 0..=10 => format!("{GREY}+{:.2}{DEFAULT}", norm_score), 530 | -10..=-1 => format!("{GREY}{:.2}{DEFAULT}", norm_score), 531 | -100..=-11 => format!("{BRIGHT_RED}{:.2}{DEFAULT}", norm_score), 532 | -15000..=-101 => format!("{RED}{:.2}{DEFAULT}", norm_score), 533 | 534 | 15_001..=32_000 => format!("{BRIGHT_YELLOW}#{}{DEFAULT}", mate), 535 | -32_000..=-15_001 => format!("{BRIGHT_YELLOW}#-{}{DEFAULT}", mate), 536 | 537 | _ => unreachable!(), 538 | }; 539 | 540 | let d = format!("{}/{}", depth, seldepth); 541 | 542 | let timer = timer.max(1); 543 | let knps: String; 544 | let n: String; 545 | if nodes < 1000 { 546 | knps = format!( 547 | "{GREY}{}no/s{DEFAULT}", 548 | nodes / (timer as u64 / 1000).max(1) 549 | ); 550 | n = format!("{nodes}"); 551 | } else { 552 | knps = format!("{GREY}{}kn/s{DEFAULT}", nodes / timer as u64); 553 | n = format!("{}k", nodes / 1000); 554 | } 555 | 556 | let str = pv.as_str(); 557 | let pv_width = 125; 558 | let pv = if str.len() > pv_width { 559 | str[..pv_width].to_string() 560 | } else { 561 | str.to_string() 562 | }; 563 | 564 | println!("{d: <7} {sc: <8} {n: <8} {knps: <18} {t: <15} {pv}"); 565 | } 566 | -------------------------------------------------------------------------------- /engine/src/body/search.rs: -------------------------------------------------------------------------------- 1 | use super::movegen::Picker; 2 | use super::nnue::inference::NNUEState; 3 | use super::position::{is_capture, is_quiet, play_move}; 4 | use super::{ 5 | history::History, 6 | lmr::LMRTable, 7 | movegen, 8 | pv_table::PVTable, 9 | stat_vec::StaticVec, 10 | tt::{score_from_tt, AgeAndFlag, PackedMove, TTFlag, TT}, 11 | }; 12 | 13 | use crate::definitions::*; 14 | use crate::uci::handler::SearchType; 15 | 16 | use cozy_chess::{BitBoard, Board, Color, GameStatus, Move, Piece}; 17 | use once_cell::sync::Lazy; 18 | use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; 19 | use std::time::Instant; 20 | 21 | static LMR: Lazy = Lazy::new(LMRTable::new); 22 | static STOP: AtomicBool = AtomicBool::new(false); 23 | static NODES: AtomicU64 = AtomicU64::new(0); 24 | 25 | const RFP_MARGIN: i32 = 75; 26 | const LMP_TABLE: [usize; 4] = [0, 5, 8, 18]; 27 | const FP_COEFFICIENT: i32 = 100; 28 | const FP_MARGIN: i32 = 75; 29 | const FP_DEPTH: i32 = 6; 30 | 31 | pub struct StackEntry { 32 | pub eval: i32, 33 | } 34 | 35 | impl Default for StackEntry { 36 | fn default() -> Self { 37 | StackEntry { eval: NONE } 38 | } 39 | } 40 | 41 | pub struct SearchInfo { 42 | pub search_type: SearchType, 43 | pub timer: Option, 44 | pub base_optimum: Option, 45 | pub max_time: Option, 46 | pub nodes: u64, 47 | prev_nodes: u64, 48 | pub node_table: [[u64; 64]; 64], 49 | pub seldepth: usize, 50 | pub game_history: Vec, 51 | pub killers: [[Option; 2]; MAX_PLY], 52 | pub history: History, 53 | pub stack: [StackEntry; MAX_PLY], 54 | } 55 | 56 | impl SearchInfo { 57 | pub fn new() -> Self { 58 | SearchInfo { 59 | search_type: SearchType::Depth(0), 60 | timer: None, 61 | base_optimum: None, 62 | max_time: None, 63 | nodes: 0, 64 | prev_nodes: 0, 65 | node_table: [[0; 64]; 64], 66 | seldepth: 0, 67 | game_history: vec![], 68 | killers: [[None; 2]; MAX_PLY], 69 | history: History::new(), 70 | stack: std::array::from_fn(|_| StackEntry::default()), 71 | } 72 | } 73 | } 74 | 75 | impl Default for SearchInfo { 76 | fn default() -> Self { 77 | Self::new() 78 | } 79 | } 80 | 81 | pub fn store_stop(stop: bool) { 82 | STOP.store(stop, Ordering::SeqCst); 83 | } 84 | 85 | pub fn load_stop() -> bool { 86 | STOP.load(Ordering::SeqCst) 87 | } 88 | 89 | pub fn clear_nodes() { 90 | NODES.store(0, Ordering::SeqCst); 91 | } 92 | 93 | fn add_nodes(nodes: u64) { 94 | NODES.fetch_add(nodes, Ordering::SeqCst); 95 | } 96 | 97 | fn load_nodes() -> u64 { 98 | NODES.load(Ordering::SeqCst) 99 | } 100 | 101 | pub struct Search<'a> { 102 | pub nnue: Box, 103 | pub tt: &'a TT, 104 | pub info: SearchInfo, 105 | } 106 | 107 | impl<'a> Search<'a> { 108 | #[allow(clippy::borrowed_box, clippy::ptr_arg)] 109 | pub fn new( 110 | tt: &'a TT, 111 | nnue: &Box, 112 | history: &History, 113 | game_history: &Vec, 114 | ) -> Self { 115 | let mut s = Search { 116 | tt, 117 | nnue: nnue.clone(), 118 | info: SearchInfo::new(), 119 | }; 120 | 121 | s.info.history = history.clone(); 122 | s.info.game_history = game_history.clone(); 123 | 124 | s 125 | } 126 | 127 | /* 128 | Zero Window Search - A way to reduce the search space in alpha-beta like search algorithms, 129 | to perform a boolean test, whether a move produces a worse or better score than a passed value. 130 | (https://www.chessprogramming.org/Null_Window) 131 | */ 132 | #[must_use] 133 | #[allow(clippy::too_many_arguments)] 134 | fn zw_search( 135 | &mut self, 136 | main_thread: bool, 137 | board: &Board, 138 | pv: &mut PVTable, 139 | alpha: i32, 140 | beta: i32, 141 | depth: i32, 142 | ply: usize, 143 | ) -> i32 { 144 | self.pvsearch::(main_thread, board, pv, alpha, beta, depth, ply) 145 | } 146 | 147 | #[must_use] 148 | #[allow(clippy::too_many_arguments)] 149 | pub fn pvsearch( 150 | &mut self, 151 | main_thread: bool, 152 | board: &Board, 153 | pv: &mut PVTable, 154 | mut alpha: i32, 155 | beta: i32, 156 | mut depth: i32, 157 | ply: usize, 158 | ) -> i32 { 159 | // Every 1024 nodes, check if it's time to stop 160 | if let (Some(timer), Some(max)) = (self.info.timer, self.info.max_time) { 161 | if main_thread 162 | && self.info.nodes % 1024 == 0 163 | && timer.elapsed().as_millis() as u64 >= max 164 | { 165 | store_stop(true); 166 | } 167 | } 168 | 169 | if load_stop() && ply > 0 { 170 | return 0; 171 | } 172 | 173 | if self.info.nodes % 2048 == 0 { 174 | add_nodes(self.info.nodes - self.info.prev_nodes); 175 | self.info.prev_nodes = self.info.nodes; 176 | } 177 | 178 | let stm = board.side_to_move(); 179 | 180 | if ply >= MAX_PLY { 181 | return self.nnue.evaluate(stm); 182 | } 183 | 184 | let hash_key = board.hash(); 185 | self.tt.prefetch(hash_key); 186 | self.info.seldepth = self.info.seldepth.max(ply); 187 | depth = depth.max(0); 188 | let mut old_pv = PVTable::new(); 189 | pv.length = 0; 190 | 191 | match board.status() { 192 | GameStatus::Won => return ply as i32 - MATE, 193 | GameStatus::Drawn => return 8 - (self.info.nodes as i32 & 7), 194 | _ => (), 195 | } 196 | 197 | let root = ply == 0; 198 | 199 | if !root { 200 | if self.repetition(board, hash_key) { 201 | return 8 - (self.info.nodes as i32 & 7); 202 | } 203 | 204 | // Mate distance pruning 205 | let mate_alpha = alpha.max(ply as i32 - MATE); 206 | let mate_beta = beta.min(MATE - (ply as i32 + 1)); 207 | if mate_alpha >= mate_beta { 208 | return mate_alpha; 209 | } 210 | } 211 | 212 | let in_check = !board.checkers().is_empty(); 213 | 214 | // Escape condition 215 | if depth == 0 && !in_check { 216 | return self.qsearch::(board, alpha, beta, ply); 217 | } 218 | 219 | let tt_entry = self.tt.probe(hash_key); 220 | let tt_hit = tt_entry.key == hash_key as u16; 221 | let tt_score = score_from_tt(tt_entry.score, ply) as i32; 222 | let mut tt_move: Option = None; 223 | 224 | if tt_hit && u64::from(tt_entry) != 0 { 225 | tt_move = Some(PackedMove::unpack(tt_entry.mv)); 226 | 227 | if !PV && i32::from(tt_entry.depth) >= depth { 228 | debug_assert!(tt_score != NONE && tt_entry.age_flag != AgeAndFlag(0)); 229 | let flag = tt_entry.age_flag.flag(); 230 | 231 | if (flag == TTFlag::Exact) 232 | || (flag == TTFlag::LowerBound && tt_score >= beta) 233 | || (flag == TTFlag::UpperBound && tt_score <= alpha) 234 | { 235 | return tt_score; 236 | } 237 | } 238 | } 239 | 240 | let eval = if tt_hit { 241 | // Use the TT score if available since eval is expensive 242 | // and any score from the TT is better than the static eval 243 | tt_score 244 | } else if in_check { 245 | // If we're in check, it's unstable to use the static eval 246 | -INFINITY 247 | } else { 248 | self.nnue.evaluate(stm) 249 | }; 250 | 251 | // Internal Iterative Reduction (IIR) 252 | if depth >= 3 && !tt_hit { 253 | depth -= 1 254 | } 255 | 256 | // Improving 257 | // If the previous eval from our point of view is worse than what it currently is 258 | // then we are improving our position. This is used in some heuristics to improve pruning. 259 | self.info.stack[ply].eval = eval; 260 | let mut improving = false; 261 | if ply >= 2 { 262 | improving = !in_check && eval > self.info.stack[ply - 2].eval; 263 | } 264 | 265 | if !PV && !in_check { 266 | // Reverse Futility Pruning (RFP) 267 | // If static eval plus a margin can beat beta, then we can safely prune this node. 268 | // The margin is multiplied by depth to make it harder to prune at higher depths 269 | // as pruning there can be inaccurate as it prunes a large amount of potential nodes 270 | // and static eval isn't the most accurate. 271 | if depth < 9 && eval >= beta + RFP_MARGIN * depth / (i32::from(improving) + 1) { 272 | return eval; 273 | } 274 | 275 | // Null Move Pruning (NMP) 276 | // If we can give the opponent a free move and still cause a beta cutoff, 277 | // we can safely prune this node. This does not work in zugzwang positions 278 | // because then it is always better to give a free move, hence some checks for it are needed. 279 | if depth >= 3 && eval >= beta && !self.non_pawn_material(board, stm).is_empty() { 280 | let r = 3 + depth / 3 + 3.min((eval.saturating_sub(beta)) / 200); 281 | let new_b = board.null_move().unwrap(); 282 | 283 | let score = -self.zw_search( 284 | main_thread, 285 | &new_b, 286 | &mut old_pv, 287 | -beta, 288 | -beta + 1, 289 | depth - r, 290 | ply + 1, 291 | ); 292 | 293 | if score >= beta { 294 | return beta; 295 | } 296 | } 297 | } 298 | 299 | let old_alpha = alpha; 300 | let mut best_score = -INFINITY; 301 | let mut best_move: Option = None; 302 | let mut moves_played = 0; 303 | 304 | let move_list = movegen::all_moves(self, board, tt_move, ply); 305 | let mut quiet_moves = StaticVec::, MAX_MOVES_POSITION>::new(None); 306 | let mut picker = Picker::new(move_list); 307 | 308 | let lmr_threshold = if PV { 5 } else { 3 }; 309 | let mut quiets_checked = 0; 310 | let quiets_to_check = match depth { 311 | d @ 1..=3 => LMP_TABLE[d as usize], 312 | _ => MAX_MOVES_POSITION, 313 | }; 314 | 315 | // Check extension 316 | depth += i32::from(in_check); 317 | 318 | while let Some(mv) = picker.pick_move() { 319 | let is_quiet = is_quiet(board, mv); 320 | let lmr_reduction = LMR.reduction(depth, moves_played.max(1)); 321 | let lmr_depth = 0.max(depth - lmr_reduction); 322 | 323 | if is_quiet { 324 | quiets_checked += 1; 325 | 326 | if !PV && !in_check && best_score > TB_LOSS_IN_PLY { 327 | // Late Move Pruning (LMP) 328 | // If we have searched too many moves, we stop searching here 329 | if quiets_checked >= quiets_to_check { 330 | break; 331 | } 332 | 333 | // Futility Pruning (FP) 334 | // If static eval plus a margin can't beat alpha, we stop searching here 335 | let fp_margin = lmr_depth * FP_COEFFICIENT + FP_MARGIN; 336 | if lmr_depth < FP_DEPTH && eval + fp_margin <= alpha { 337 | break; 338 | } 339 | } 340 | 341 | quiet_moves.push(Some(mv)); 342 | } 343 | 344 | let mut new_b = board.clone(); 345 | play_move(&mut new_b, &mut self.nnue, mv); 346 | 347 | moves_played += 1; 348 | self.info.game_history.push(board.hash()); 349 | self.info.nodes += 1; 350 | let previous_nodes = self.info.nodes; 351 | let gives_check = !board.checkers().is_empty(); 352 | 353 | let mut score: i32; 354 | if moves_played == 1 { 355 | score = -self.pvsearch::( 356 | main_thread, 357 | &new_b, 358 | &mut old_pv, 359 | -beta, 360 | -alpha, 361 | depth - 1, 362 | ply + 1, 363 | ); 364 | } else { 365 | // Late Move Reduction (LMR) 366 | // Assuming our move ordering is good, later moves will be worse 367 | // and can be searched with a reduced depth, if they beat alpha 368 | // we do a full re-search. 369 | let r = if depth >= 3 && moves_played > lmr_threshold { 370 | // Probe LMR table (src/lmr.rs) 371 | let mut r = LMR.reduction(depth, moves_played); 372 | 373 | // Bonus for non PV nodes 374 | r += i32::from(!PV); 375 | 376 | // Malus for capture moves and checks 377 | r -= i32::from(is_capture(board, mv)); 378 | r -= i32::from(gives_check); 379 | 380 | r.clamp(1, depth - 1) 381 | } else { 382 | 1 383 | }; 384 | 385 | score = -self.zw_search( 386 | main_thread, 387 | &new_b, 388 | &mut old_pv, 389 | -alpha - 1, 390 | -alpha, 391 | depth - r, 392 | ply + 1, 393 | ); 394 | 395 | // Three-fold LMR 396 | // If the ZW beats alpha, then it might be 397 | // worth looking at this good position fully 398 | if score > alpha && r > 1 { 399 | score = -self.zw_search( 400 | main_thread, 401 | &new_b, 402 | &mut old_pv, 403 | -alpha - 1, 404 | -alpha, 405 | depth - 1, 406 | ply + 1, 407 | ); 408 | } 409 | 410 | if (alpha + 1..beta).contains(&score) { 411 | score = -self.pvsearch::( 412 | main_thread, 413 | &new_b, 414 | &mut old_pv, 415 | -beta, 416 | -alpha, 417 | depth - 1, 418 | ply + 1, 419 | ); 420 | } 421 | } 422 | 423 | self.info.game_history.pop(); 424 | self.nnue.pop(); 425 | 426 | if root { 427 | // Difference in node count 428 | self.info.node_table[mv.from as usize][mv.to as usize] += 429 | self.info.nodes - previous_nodes; 430 | } 431 | 432 | if score <= best_score { 433 | continue; 434 | } 435 | best_score = score; 436 | 437 | if score <= alpha { 438 | continue; 439 | } 440 | // New best move 441 | alpha = score; 442 | best_move = Some(mv); 443 | pv.store(board, mv, &old_pv); 444 | 445 | // Fail-high 446 | if score >= beta { 447 | if is_quiet { 448 | // Killer moves 449 | self.info.killers[ply][1] = self.info.killers[ply][0]; 450 | self.info.killers[ply][0] = Some(mv); 451 | 452 | // History Heuristic 453 | self.info.history.update_table::(board, mv, depth); 454 | let qi = quiet_moves.as_slice(); 455 | let qi = &qi[..quiet_moves.len() - 1]; 456 | for qm in qi { 457 | self.info 458 | .history 459 | .update_table::(board, qm.unwrap(), depth); 460 | } 461 | } 462 | 463 | break; 464 | } 465 | } 466 | 467 | self.tt.prefetch(hash_key); 468 | 469 | let flag = if best_score >= beta { 470 | TTFlag::LowerBound 471 | } else if best_score != old_alpha { 472 | TTFlag::Exact 473 | } else { 474 | best_move = None; 475 | TTFlag::UpperBound 476 | }; 477 | 478 | debug_assert!((-INFINITY..=INFINITY).contains(&best_score)); 479 | 480 | if !load_stop() { 481 | self.tt.store( 482 | hash_key, 483 | best_move, 484 | best_score as i16, 485 | depth as u8, 486 | flag, 487 | ply, 488 | ); 489 | } 490 | 491 | best_score 492 | } 493 | 494 | #[must_use] 495 | fn qsearch( 496 | &mut self, 497 | board: &Board, 498 | mut alpha: i32, 499 | beta: i32, 500 | ply: usize, 501 | ) -> i32 { 502 | if let (Some(timer), Some(max)) = (self.info.timer, self.info.max_time) { 503 | if self.info.nodes % 1024 == 0 && timer.elapsed().as_millis() as u64 >= max { 504 | store_stop(true); 505 | return 0; 506 | } 507 | } 508 | 509 | if load_stop() && ply > 0 { 510 | return 0; 511 | } 512 | 513 | if self.info.nodes % 2048 == 0 { 514 | add_nodes(self.info.nodes - self.info.prev_nodes); 515 | self.info.prev_nodes = self.info.nodes; 516 | } 517 | 518 | let stm = board.side_to_move(); 519 | 520 | if ply >= MAX_PLY { 521 | return self.nnue.evaluate(stm); 522 | } 523 | 524 | let hash_key = board.hash(); 525 | self.tt.prefetch(hash_key); 526 | self.info.seldepth = self.info.seldepth.max(ply); 527 | 528 | let stand_pat = self.nnue.evaluate(stm); 529 | alpha = alpha.max(stand_pat); 530 | if stand_pat >= beta { 531 | return stand_pat; 532 | } 533 | 534 | let tt_entry = self.tt.probe(hash_key); 535 | let tt_hit = tt_entry.key == hash_key as u16; 536 | let mut tt_move: Option = None; 537 | 538 | if tt_hit && u64::from(tt_entry) != 0 { 539 | tt_move = Some(PackedMove::unpack(tt_entry.mv)); 540 | 541 | if !PV { 542 | let tt_score = score_from_tt(tt_entry.score, ply) as i32; 543 | let flag = tt_entry.age_flag.flag(); 544 | debug_assert!(tt_score != NONE && tt_entry.age_flag != AgeAndFlag(0)); 545 | 546 | if (flag == TTFlag::Exact) 547 | || (flag == TTFlag::LowerBound && tt_score >= beta) 548 | || (flag == TTFlag::UpperBound && tt_score <= alpha) 549 | { 550 | return tt_score; 551 | } 552 | } 553 | } 554 | 555 | let captures = movegen::capture_moves(self, board, tt_move, ply); 556 | let mut picker = Picker::new(captures); 557 | let mut best_score = stand_pat; 558 | let mut best_move: Option = None; 559 | 560 | while let Some(mv) = picker.pick_move() { 561 | let mut new_b = board.clone(); 562 | play_move(&mut new_b, &mut self.nnue, mv); 563 | 564 | self.info.nodes += 1; 565 | 566 | let score = -self.qsearch::(&new_b, -beta, -alpha, ply + 1); 567 | 568 | self.nnue.pop(); 569 | 570 | if score <= best_score { 571 | continue; 572 | } 573 | best_score = score; 574 | 575 | if score <= alpha { 576 | continue; 577 | } 578 | alpha = score; 579 | best_move = Some(mv); 580 | 581 | if score >= beta { 582 | break; 583 | } 584 | } 585 | 586 | self.tt.prefetch(hash_key); 587 | 588 | let flag = if best_score >= beta { 589 | TTFlag::LowerBound 590 | } else { 591 | TTFlag::UpperBound 592 | }; 593 | 594 | if !load_stop() { 595 | self.tt 596 | .store(hash_key, best_move, best_score as i16, 0, flag, ply); 597 | } 598 | 599 | best_score 600 | } 601 | 602 | pub fn iterative_deepening( 603 | &mut self, 604 | board: &Board, 605 | st: SearchType, 606 | pretty: bool, 607 | ) { 608 | let depth: usize; 609 | let mut opt_time: Option = None; 610 | let mut goal_nodes: Option = None; 611 | 612 | match st { 613 | SearchType::Time(opt, max) => { 614 | depth = MAX_PLY; 615 | self.info.timer = Some(Instant::now()); 616 | self.info.max_time = Some(max); 617 | self.info.base_optimum = Some(opt); 618 | opt_time = Some(opt); 619 | } 620 | SearchType::Infinite => { 621 | depth = MAX_PLY; 622 | } 623 | SearchType::Depth(d) => depth = d.min(MAX_PLY), 624 | SearchType::Nodes(n) => { 625 | depth = MAX_PLY; 626 | goal_nodes = Some(n); 627 | } 628 | }; 629 | 630 | // SMP - might clean up in the future 631 | if !MAIN_THREAD { 632 | let mut s = 0; 633 | let mut phony_bm: Option = None; 634 | 635 | for d in 1..=depth { 636 | s = self.aspiration_window( 637 | false, 638 | board, 639 | &mut PVTable::new(), 640 | s, 641 | d as i32, 642 | &mut phony_bm, 643 | ); 644 | 645 | if load_stop() { 646 | break; 647 | } 648 | } 649 | 650 | return; 651 | } 652 | 653 | let info_timer = Instant::now(); 654 | let mut best_move: Option = None; 655 | let mut score = 0; 656 | let mut pv = PVTable::new(); 657 | 658 | for d in 1..=depth { 659 | self.info.seldepth = 0; 660 | score = self.aspiration_window(true, board, &mut pv, score, d as i32, &mut best_move); 661 | 662 | // Max time is up 663 | if load_stop() && d > 1 { 664 | break; 665 | } 666 | 667 | best_move = pv.best_move(); 668 | 669 | let mut n = load_nodes(); 670 | if n == 0 { 671 | n = self.info.nodes; 672 | } 673 | 674 | if pretty { 675 | crate::uci::handler::pretty_print( 676 | d, 677 | self.info.seldepth, 678 | score, 679 | load_nodes(), 680 | info_timer.elapsed().as_millis(), 681 | pv.pv_string(), 682 | ); 683 | } else { 684 | println!( 685 | "info depth {} seldepth {} score {} nodes {} time {} nps {} pv{}", 686 | d, 687 | self.info.seldepth, 688 | format_score(score), 689 | n, 690 | info_timer.elapsed().as_millis(), 691 | n / info_timer.elapsed().as_secs().max(1), 692 | pv.pv_string() 693 | ); 694 | } 695 | 696 | // Nodes search type 697 | if let Some(nodes) = goal_nodes { 698 | if self.info.nodes >= nodes { 699 | break; 700 | } 701 | } 702 | 703 | // Optimal time check 704 | if let Some(mut opt) = opt_time { 705 | // Time bound adjustments 706 | #[rustfmt::skip] 707 | let best_move_fraction = 708 | self.info.node_table 709 | [best_move.unwrap().from as usize] 710 | [best_move.unwrap().to as usize] as f64 711 | / self.info.nodes as f64; 712 | 713 | let time_factor = (1.5 - best_move_fraction) * 1.35; 714 | opt = (self.info.base_optimum.unwrap() as f64 * time_factor) as u64; 715 | 716 | if info_timer.elapsed().as_millis() as u64 >= opt { 717 | break; 718 | } 719 | } 720 | } 721 | 722 | store_stop(true); 723 | 724 | println!("bestmove {}", best_move.unwrap()); 725 | } 726 | 727 | fn aspiration_window( 728 | &mut self, 729 | main_thread: bool, 730 | board: &Board, 731 | pv: &mut PVTable, 732 | prev_eval: i32, 733 | mut depth: i32, 734 | best_move: &mut Option, 735 | ) -> i32 { 736 | let mut score: i32; 737 | let init_depth = depth; 738 | 739 | // Window size 740 | let mut delta = 25; 741 | 742 | // Window bounds 743 | let mut alpha = -INFINITY; 744 | let mut beta = INFINITY; 745 | 746 | if depth >= 5 { 747 | alpha = (-INFINITY).max(prev_eval - delta); 748 | beta = (INFINITY).min(prev_eval + delta); 749 | } 750 | 751 | loop { 752 | score = self.pvsearch::(main_thread, board, pv, alpha, beta, depth, 0); 753 | 754 | if load_stop() { 755 | return 0; 756 | } 757 | 758 | // Search failed low 759 | if score <= alpha { 760 | beta = (alpha + beta) / 2; 761 | alpha = (-INFINITY).max(score - delta); 762 | depth = init_depth; 763 | } 764 | // Search failed high 765 | else if score >= beta { 766 | beta = (INFINITY).min(score + delta); 767 | 768 | depth -= i32::from(score.abs() < MATE_IN); 769 | 770 | *best_move = pv.best_move(); 771 | } 772 | // Search succeeded 773 | else { 774 | return score; 775 | } 776 | 777 | delta += delta / 2; 778 | debug_assert!(alpha >= -INFINITY && beta <= INFINITY); 779 | } 780 | } 781 | 782 | fn repetition(&self, board: &Board, hash: u64) -> bool { 783 | self.info 784 | .game_history 785 | .iter() 786 | .rev() 787 | .take(board.halfmove_clock() as usize) 788 | // Skip the current position 789 | .skip(1) 790 | // Two fold 791 | .any(|&key| key == hash) 792 | } 793 | 794 | fn non_pawn_material(&self, board: &Board, color: Color) -> BitBoard { 795 | (board.pieces(Piece::Knight) 796 | | board.pieces(Piece::Bishop) 797 | | board.pieces(Piece::Rook) 798 | | board.pieces(Piece::Queen)) 799 | & board.colors(color) 800 | } 801 | 802 | pub fn go_reset(&mut self) { 803 | STOP.store(false, Ordering::Relaxed); 804 | NODES.store(0, Ordering::Relaxed); 805 | self.info.search_type = SearchType::Depth(0); 806 | self.info.timer = None; 807 | self.info.max_time = None; 808 | self.info.base_optimum = None; 809 | self.info.nodes = 0; 810 | self.info.node_table = [[0; 64]; 64]; 811 | self.info.seldepth = 0; 812 | self.info.killers = [[None; 2]; MAX_PLY]; 813 | self.info.history.age_table(); 814 | } 815 | 816 | pub fn game_reset(&mut self) { 817 | STOP.store(false, Ordering::Relaxed); 818 | NODES.store(0, Ordering::Relaxed); 819 | self.info = SearchInfo::new(); 820 | self.info.game_history = vec![Board::default().hash()]; 821 | } 822 | 823 | pub fn data_search(&mut self, board: &Board, st: SearchType) -> (i32, Move) { 824 | let depth: usize; 825 | let mut goal_nodes: Option = None; 826 | match st { 827 | SearchType::Depth(d) => depth = (d).min(MAX_PLY), 828 | SearchType::Nodes(n) => { 829 | depth = MAX_PLY; 830 | goal_nodes = Some(n); 831 | } 832 | _ => unreachable!(), 833 | }; 834 | 835 | let mut best_move: Option = None; 836 | 837 | let mut score = 0; 838 | let mut pv = PVTable::new(); 839 | 840 | for d in 1..=depth { 841 | self.info.seldepth = 0; 842 | score = self.aspiration_window(true, board, &mut pv, score, d as i32, &mut best_move); 843 | 844 | if load_stop() && d > 1 { 845 | break; 846 | } 847 | 848 | best_move = pv.best_move(); 849 | 850 | if let Some(nodes) = goal_nodes { 851 | if self.info.nodes >= nodes { 852 | break; 853 | } 854 | } 855 | } 856 | 857 | (score, best_move.unwrap()) 858 | } 859 | } 860 | 861 | pub fn format_score(score: i32) -> String { 862 | debug_assert!(score < NONE); 863 | let print_score: String; 864 | if score >= MATE_IN { 865 | print_score = format!("mate {}", (((MATE - score) / 2) + ((MATE - score) & 1))); 866 | } else if score <= -MATE_IN { 867 | print_score = format!("mate {}", -(((MATE + score) / 2) + ((MATE + score) & 1))); 868 | } else { 869 | print_score = format!("cp {score}"); 870 | } 871 | 872 | print_score 873 | } 874 | 875 | #[cfg(test)] 876 | mod tests { 877 | use super::*; 878 | 879 | #[test] 880 | fn repetitions() { 881 | const FENS: [&str; 3] = [ 882 | "5k2/4q1p1/3P1pQb/1p1B4/pP5p/P1PR4/5PP1/1K6 b - - 0 38", 883 | "6k1/6p1/8/6KQ/1r6/q2b4/8/8 w - - 0 32", 884 | "5rk1/1rP3pp/p4n2/3Pp3/1P2Pq2/2Q4P/P5P1/R3R1K1 b - - 0 32", 885 | ]; 886 | 887 | let tt = TT::new(16); 888 | let nnue = NNUEState::from_board(&Board::default()); 889 | let history = History::new(); 890 | let mut search = Search::new(&tt, &nnue, &history, &vec![]); 891 | 892 | for fen in FENS.iter() { 893 | let board = Board::from_fen(fen, false).unwrap(); 894 | search.nnue.refresh(&board); 895 | 896 | let (score, _) = search.data_search(&board, SearchType::Depth(20)); 897 | 898 | assert!((-10..=10).contains(&score), "{score} {fen}"); 899 | 900 | search.game_reset(); 901 | } 902 | } 903 | } 904 | --------------------------------------------------------------------------------