From cb9e9c25f0c0366ab4d0fe7c2253375a1bfb45c6 Mon Sep 17 00:00:00 2001 From: Ludovic Date: Sat, 1 Aug 2026 00:44:08 +0200 Subject: [PATCH 1/5] feat(search): add transposition table bench 153707 --- src/search/mod.rs | 1 + src/search/move_picker.rs | 10 ++++-- src/search/search.rs | 39 +++++++++++++++++--- src/search/tt.rs | 75 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 7 deletions(-) create mode 100644 src/search/tt.rs diff --git a/src/search/mod.rs b/src/search/mod.rs index f26dd75..fa14b94 100644 --- a/src/search/mod.rs +++ b/src/search/mod.rs @@ -4,3 +4,4 @@ pub(crate) mod search; pub(crate) mod search_options; pub(crate) mod search_result; pub(crate) mod time; +pub(crate) mod tt; diff --git a/src/search/move_picker.rs b/src/search/move_picker.rs index f7a46e3..9a5d71a 100644 --- a/src/search/move_picker.rs +++ b/src/search/move_picker.rs @@ -5,10 +5,10 @@ pub struct MovePicker { } impl MovePicker { - pub(crate) fn new(moves: Vec) -> Self { + pub(crate) fn new(moves: Vec, tt_move: Option) -> Self { let mut scored_moves: Vec<(i32, Move)> = moves .into_iter() - .map(|m| (Self::score_move(&m), m)) + .map(|m| (Self::score_move(&m, tt_move), m)) .collect(); scored_moves.sort_unstable_by_key(|(s, _)| *s); @@ -19,9 +19,13 @@ impl MovePicker { self.scored_moves.pop().map(|(_, m)| m) } - fn score_move(m: &Move) -> i32 { + fn score_move(m: &Move, tt_move: Option) -> i32 { let mut score = 0; + if tt_move == Some(*m) { + score += 1000; + } + if m.is_promotion() { score += match m.promotion() { Some(Role::Queen) => 9, diff --git a/src/search/search.rs b/src/search/search.rs index cc708cd..3f69fc5 100644 --- a/src/search/search.rs +++ b/src/search/search.rs @@ -2,9 +2,11 @@ use crate::search::eval::{Eval, Score}; use crate::search::move_picker::MovePicker; use crate::search::search_options::BuiltSearchOptions; use crate::search::time::TimeManager; +use crate::search::tt::{TTBound, TranspositionTable}; use crate::search::{search_options::SearchOptions, search_result::SearchResult}; use shakmaty::uci::UciMove; -use shakmaty::{CastlingMode, Chess, Position}; +use shakmaty::zobrist::Zobrist64; +use shakmaty::{CastlingMode, Chess, EnPassantMode, Move, Position}; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::SystemTime; @@ -18,6 +20,7 @@ pub(crate) struct Search { stop: Arc, result: SearchResult, start_time: SystemTime, + tt: TranspositionTable, } impl Search { @@ -27,6 +30,7 @@ impl Search { stop, result: SearchResult::new(), start_time: SystemTime::now(), + tt: TranspositionTable::new(419_430), // 16 MB } } } @@ -35,6 +39,7 @@ impl Search { fn init(&mut self) { self.result = SearchResult::new(); self.start_time = SystemTime::now(); + self.tt.clear(); } pub(crate) fn run(&mut self) -> &SearchResult { @@ -114,7 +119,7 @@ impl Search { let mut best_move = UciMove::Null; let moves = pos.legal_moves(); - let mut mp = MovePicker::new(moves.to_vec()); + let mut mp = MovePicker::new(moves.to_vec(), None); while let Some(m) = mp.next() { let child = pos.clone().play(m).unwrap(); @@ -164,6 +169,20 @@ impl Search { return self.qsearch(pos, alpha, beta, ply); } + let is_root = ply == 0; + let position_key: Zobrist64 = pos.zobrist_hash::(EnPassantMode::Legal); + let entry = self.tt.probe(position_key); + + if entry.key == position_key + && !is_root + && entry.depth >= depth + && (entry.bound == TTBound::Exact + || (entry.bound == TTBound::Alpha && entry.score <= alpha) + || (entry.bound == TTBound::Beta && entry.score >= beta)) + { + return entry.score; + } + let moves = pos.legal_moves(); if moves.is_empty() { @@ -173,10 +192,12 @@ impl Search { } } + let start_alpha = alpha; let mut alpha = alpha; let mut best_score = -MATE_SCORE; + let mut best_move: Option = None; - let mut mp = MovePicker::new(moves.to_vec()); + let mut mp = MovePicker::new(moves.to_vec(), entry.best_move); while let Some(m) = mp.next() { let child = pos.clone().play(m).unwrap(); @@ -188,6 +209,7 @@ impl Search { if score > best_score { best_score = score; + best_move = Some(m); if score > alpha { alpha = score; @@ -195,10 +217,19 @@ impl Search { } if score >= beta { - return best_score; + break; } } + let bound = match best_score { + score if score <= start_alpha => TTBound::Alpha, + score if score >= beta => TTBound::Beta, + _ => TTBound::Exact, + }; + + self.tt + .store(position_key, depth, best_score, bound, best_move); + best_score } diff --git a/src/search/tt.rs b/src/search/tt.rs new file mode 100644 index 0000000..d97cd5d --- /dev/null +++ b/src/search/tt.rs @@ -0,0 +1,75 @@ +use shakmaty::{Move, zobrist::Zobrist64}; + +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum TTBound { + Exact, + Beta, + Alpha, +} + +#[derive(Debug, Clone)] +pub(crate) struct TTEntry { + pub(crate) key: Zobrist64, + pub(crate) depth: u32, + pub(crate) score: i32, + pub(crate) bound: TTBound, + // pub(crate) best_move: [u8; 2], + pub(crate) best_move: Option, +} + +impl Default for TTEntry { + fn default() -> Self { + TTEntry { + key: Zobrist64(0), + depth: 0, + score: 0, + bound: TTBound::Exact, + best_move: None, + } + } +} + +#[derive(Debug, Clone)] +pub(crate) struct TranspositionTable { + pub table: Vec, + length: usize, +} + +impl TranspositionTable { + pub(crate) fn new(length: usize) -> Self { + Self { + table: vec![TTEntry::default(); length], + length, + } + } + + pub(crate) fn probe(&self, key: Zobrist64) -> &TTEntry { + let index = key.0 as usize % self.length; + + &self.table[index] + } + + pub(crate) fn store( + &mut self, + key: Zobrist64, + depth: u32, + score: i32, + bound: TTBound, + best_move: Option, + ) { + let index = key.0 as usize % self.table.len(); + let entry = TTEntry { + key, + depth, + score, + bound, + best_move, + }; + + self.table[index] = entry; + } + + pub(crate) fn clear(&mut self) { + self.table = vec![TTEntry::default(); self.length]; + } +} From fd4c61904591fe9ec965bb4614c2522bff280e65 Mon Sep 17 00:00:00 2001 From: Ludovic Date: Wed, 5 Aug 2026 21:26:43 +0200 Subject: [PATCH 2/5] feat(tt): reduce entry size bench 154477 --- src/main.rs | 1 - src/search/eval.rs | 18 +++++++++--------- src/search/search.rs | 35 +++++++++++++++++++---------------- src/search/search_options.rs | 10 +++++++--- src/search/tt.rs | 30 +++++++++++++++++++----------- src/uci.rs | 9 +++++++-- 6 files changed, 61 insertions(+), 42 deletions(-) diff --git a/src/main.rs b/src/main.rs index aa5a05f..22ba0da 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ use std::env; - pub mod search; pub mod uci; diff --git a/src/search/eval.rs b/src/search/eval.rs index 66825cd..c598031 100644 --- a/src/search/eval.rs +++ b/src/search/eval.rs @@ -5,7 +5,7 @@ use shakmaty::{Chess, Position}; pub(crate) struct Eval; #[cfg_attr(any(), rustfmt::skip)] -const PAWN_TABLE: [i32; 64] = [ +const PAWN_TABLE: [i16; 64] = [ 0, 0, 0, 0, 0, 0, 0, 0, 50, 50, 50, 50, 50, 50, 50, 50, 10, 10, 20, 30, 30, 20, 10, 10, @@ -17,7 +17,7 @@ const PAWN_TABLE: [i32; 64] = [ ]; #[cfg_attr(any(), rustfmt::skip)] -const KNIGHT_TABLE: [i32; 64] = [ +const KNIGHT_TABLE: [i16; 64] = [ -50,-40,-30,-30,-30,-30,-40,-50, -40,-20, 0, 0, 0, 0,-20,-40, -30, 0, 10, 15, 15, 10, 0,-30, @@ -29,7 +29,7 @@ const KNIGHT_TABLE: [i32; 64] = [ ]; #[cfg_attr(any(), rustfmt::skip)] -const BISHOP_TABLE: [i32; 64] = [ +const BISHOP_TABLE: [i16; 64] = [ -20,-10,-10,-10,-10,-10,-10,-20, -10, 0, 0, 0, 0, 0, 0,-10, -10, 0, 5, 10, 10, 5, 0,-10, @@ -41,7 +41,7 @@ const BISHOP_TABLE: [i32; 64] = [ ]; #[cfg_attr(any(), rustfmt::skip)] -const ROOK_TABLE: [i32; 64] = [ +const ROOK_TABLE: [i16; 64] = [ 0, 0, 0, 0, 0, 0, 0, 0, 5, 10, 10, 10, 10, 10, 10, 5, -5, 0, 0, 0, 0, 0, 0, -5, @@ -53,7 +53,7 @@ const ROOK_TABLE: [i32; 64] = [ ]; #[cfg_attr(any(), rustfmt::skip)] -const QUEEN_TABLE: [i32; 64] = [ +const QUEEN_TABLE: [i16; 64] = [ -20,-10,-10, -5, -5,-10,-10,-20, -10, 0, 0, 0, 0, 0, 0,-10, -10, 0, 5, 5, 5, 5, 0,-10, @@ -65,7 +65,7 @@ const QUEEN_TABLE: [i32; 64] = [ ]; #[cfg_attr(any(), rustfmt::skip)] -const KING_TABLE: [i32; 64] = [ +const KING_TABLE: [i16; 64] = [ -30,-40,-40,-50,-50,-40,-40,-30, -30,-40,-40,-50,-50,-40,-40,-30, -30,-40,-40,-50,-50,-40,-40,-30, @@ -78,8 +78,8 @@ const KING_TABLE: [i32; 64] = [ #[derive(Debug)] pub(crate) enum Score { - Cp(i32), - Mate(i32), + Cp(i16), + Mate(u8), } impl Display for Score { @@ -92,7 +92,7 @@ impl Display for Score { } impl Eval { - pub(crate) fn simple(pos: &Chess) -> i32 { + pub(crate) fn simple(pos: &Chess) -> i16 { let mut score = 0; for (sq, piece) in pos.board() { diff --git a/src/search/search.rs b/src/search/search.rs index 3f69fc5..fe9829a 100644 --- a/src/search/search.rs +++ b/src/search/search.rs @@ -11,8 +11,8 @@ use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::SystemTime; -pub(crate) const MATE_SCORE: i32 = 30_000; -pub(crate) const MAX_DEPTH: u32 = 1024; +pub(crate) const MATE_SCORE: i16 = i16::MAX; +pub(crate) const MAX_DEPTH: u8 = u8::MAX; pub(crate) struct Search { opt: BuiltSearchOptions, @@ -25,12 +25,15 @@ pub(crate) struct Search { impl Search { pub(crate) fn from(opt: &SearchOptions, stop: Arc) -> Self { + let built_opt = opt.build(); + let hash = built_opt.hash; + Search { - opt: opt.build(), + opt: built_opt, stop, result: SearchResult::new(), start_time: SystemTime::now(), - tt: TranspositionTable::new(419_430), // 16 MB + tt: TranspositionTable::new(hash), } } } @@ -78,11 +81,11 @@ impl Search { break; } - match MAX_DEPTH as i32 > MATE_SCORE - score.abs() { + match MAX_DEPTH as i16 > MATE_SCORE - score.abs() { true => { self.result.score = match score > 0 { - true => Score::Mate((MATE_SCORE - score) / 2 + 1), - false => Score::Mate((-MATE_SCORE - score) / 2), + true => Score::Mate((MATE_SCORE - score) as u8 / 2 + 1), + false => Score::Mate((-MATE_SCORE - score) as u8 / 2), } } false => self.result.score = Score::Cp(score), @@ -109,11 +112,11 @@ impl Search { fn root_negamax( &mut self, pos: &Chess, - depth: u32, - alpha: i32, - beta: i32, + depth: u8, + alpha: i16, + beta: i16, ply: u32, - ) -> (i32, UciMove) { + ) -> (i16, UciMove) { let mut alpha = alpha; let mut best_score = -MATE_SCORE; let mut best_move = UciMove::Null; @@ -146,7 +149,7 @@ impl Search { (best_score, best_move) } - fn negamax(&mut self, pos: &Chess, depth: u32, alpha: i32, beta: i32, ply: u32) -> i32 { + fn negamax(&mut self, pos: &Chess, depth: u8, alpha: i16, beta: i16, ply: u32) -> i16 { self.result.nodes += 1; if let Some(search_nodes) = self.opt.nodes { @@ -173,7 +176,7 @@ impl Search { let position_key: Zobrist64 = pos.zobrist_hash::(EnPassantMode::Legal); let entry = self.tt.probe(position_key); - if entry.key == position_key + if let Some(entry) = entry && !is_root && entry.depth >= depth && (entry.bound == TTBound::Exact @@ -187,7 +190,7 @@ impl Search { if moves.is_empty() { match pos.is_check() { - true => return -MATE_SCORE + ply as i32, + true => return -MATE_SCORE + ply as i16, false => return 0, } } @@ -197,7 +200,7 @@ impl Search { let mut best_score = -MATE_SCORE; let mut best_move: Option = None; - let mut mp = MovePicker::new(moves.to_vec(), entry.best_move); + let mut mp = MovePicker::new(moves.to_vec(), entry.and_then(|e| e.best_move)); while let Some(m) = mp.next() { let child = pos.clone().play(m).unwrap(); @@ -233,7 +236,7 @@ impl Search { best_score } - fn qsearch(&mut self, pos: &Chess, alpha: i32, beta: i32, ply: u32) -> i32 { + fn qsearch(&mut self, pos: &Chess, alpha: i16, beta: i16, ply: u32) -> i16 { self.result.nodes += 1; if let Some(search_nodes) = self.opt.nodes { diff --git a/src/search/search_options.rs b/src/search/search_options.rs index 6aa5710..ee303f0 100644 --- a/src/search/search_options.rs +++ b/src/search/search_options.rs @@ -2,20 +2,22 @@ use shakmaty::{Chess, Color, Position}; #[derive(Debug, Clone)] pub(crate) struct SearchOptions { - pub(crate) depth: Option, + pub(crate) depth: Option, pub(crate) nodes: Option, pub(crate) move_time: Option, pub(crate) position: Chess, pub(crate) wtime: Option, pub(crate) btime: Option, + pub(crate) hash: u16, } #[derive(Debug, Clone)] pub(crate) struct BuiltSearchOptions { - pub(crate) depth: Option, + pub(crate) depth: Option, pub(crate) nodes: Option, pub(crate) time: Option, pub(crate) position: Chess, + pub(crate) hash: u16, } impl Default for SearchOptions { @@ -27,6 +29,7 @@ impl Default for SearchOptions { btime: None, move_time: None, position: Chess::default(), + hash: 16, } } } @@ -43,6 +46,7 @@ impl SearchOptions { nodes: self.nodes, position: self.position.clone(), time: self.move_time.or(turn_time.map(|t| t / 25)), + hash: self.hash, } } @@ -54,7 +58,7 @@ impl SearchOptions { self.btime = None; } - pub(crate) fn depth(mut self, depth: u32) -> Self { + pub(crate) fn depth(mut self, depth: u8) -> Self { self.depth = Some(depth); self } diff --git a/src/search/tt.rs b/src/search/tt.rs index d97cd5d..dc8bdf1 100644 --- a/src/search/tt.rs +++ b/src/search/tt.rs @@ -9,9 +9,9 @@ pub(crate) enum TTBound { #[derive(Debug, Clone)] pub(crate) struct TTEntry { - pub(crate) key: Zobrist64, - pub(crate) depth: u32, - pub(crate) score: i32, + pub(crate) sig: u16, + pub(crate) depth: u8, + pub(crate) score: i16, pub(crate) bound: TTBound, // pub(crate) best_move: [u8; 2], pub(crate) best_move: Option, @@ -20,7 +20,7 @@ pub(crate) struct TTEntry { impl Default for TTEntry { fn default() -> Self { TTEntry { - key: Zobrist64(0), + sig: 0, depth: 0, score: 0, bound: TTBound::Exact, @@ -36,30 +36,38 @@ pub(crate) struct TranspositionTable { } impl TranspositionTable { - pub(crate) fn new(length: usize) -> Self { + pub(crate) fn new(size: u16) -> Self { + let length = size as usize * 1024 * 1024 / std::mem::size_of::(); + Self { table: vec![TTEntry::default(); length], length, } } - pub(crate) fn probe(&self, key: Zobrist64) -> &TTEntry { + pub(crate) fn probe(&self, key: Zobrist64) -> Option<&TTEntry> { let index = key.0 as usize % self.length; - &self.table[index] + let entry = &self.table[index]; + + if entry.sig != key.0 as u16 { + return None; + } + + Some(&entry) } pub(crate) fn store( &mut self, key: Zobrist64, - depth: u32, - score: i32, + depth: u8, + score: i16, bound: TTBound, best_move: Option, ) { - let index = key.0 as usize % self.table.len(); + let index = key.0 as usize % self.length; let entry = TTEntry { - key, + sig: key.0 as u16, depth, score, bound, diff --git a/src/uci.rs b/src/uci.rs index 3b84ae4..ad4a258 100644 --- a/src/uci.rs +++ b/src/uci.rs @@ -142,7 +142,12 @@ impl Uci { let value = _queue.pop_front().unwrap_or(""); match value { - "Hash" => {} + "Hash" => { + self.search_options.hash = _queue + .pop_front() + .and_then(|s| s.parse::().ok()) + .unwrap_or(16) + } "Threads" => {} _ => {} } @@ -181,7 +186,7 @@ impl Uci { #[cfg_attr(any(), rustfmt::skip)] while let Some(arg) = queue.pop_front() { match arg { - "depth" => self.search_options.depth = queue.pop_front().and_then(|s| s.parse::().ok()), + "depth" => self.search_options.depth = queue.pop_front().and_then(|s| s.parse::().ok()), "nodes" => self.search_options.nodes = queue.pop_front().and_then(|s| s.parse::().ok()), "movetime" => self.search_options.move_time = queue.pop_front().and_then(|s| s.parse::().ok()), "wtime" => self.search_options.wtime = queue.pop_front().and_then(|s| s.parse::().ok()), From d8edc804fc9a445a5f65ce14884e3f3afd286ac6 Mon Sep 17 00:00:00 2001 From: Ludovic Date: Thu, 6 Aug 2026 09:37:55 +0200 Subject: [PATCH 3/5] fix(tt): fix mate score + avoid clearing between moves bench 154477 --- src/search/eval.rs | 2 +- src/search/search.rs | 12 ++++++++---- src/search/search_options.rs | 4 ++++ src/uci.rs | 13 +++++++++++-- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/search/eval.rs b/src/search/eval.rs index c598031..ed21e42 100644 --- a/src/search/eval.rs +++ b/src/search/eval.rs @@ -79,7 +79,7 @@ const KING_TABLE: [i16; 64] = [ #[derive(Debug)] pub(crate) enum Score { Cp(i16), - Mate(u8), + Mate(i8), } impl Display for Score { diff --git a/src/search/search.rs b/src/search/search.rs index fe9829a..0537a8f 100644 --- a/src/search/search.rs +++ b/src/search/search.rs @@ -11,7 +11,7 @@ use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::SystemTime; -pub(crate) const MATE_SCORE: i16 = i16::MAX; +pub(crate) const MATE_SCORE: i16 = 30_000; pub(crate) const MAX_DEPTH: u8 = u8::MAX; pub(crate) struct Search { @@ -40,9 +40,13 @@ impl Search { impl Search { fn init(&mut self) { + if self.opt.new_game { + println!("CLEARING"); + self.tt.clear(); + } + self.result = SearchResult::new(); self.start_time = SystemTime::now(); - self.tt.clear(); } pub(crate) fn run(&mut self) -> &SearchResult { @@ -84,8 +88,8 @@ impl Search { match MAX_DEPTH as i16 > MATE_SCORE - score.abs() { true => { self.result.score = match score > 0 { - true => Score::Mate((MATE_SCORE - score) as u8 / 2 + 1), - false => Score::Mate((-MATE_SCORE - score) as u8 / 2), + true => Score::Mate((MATE_SCORE - score) as i8 / 2 + 1), + false => Score::Mate((-MATE_SCORE - score) as i8 / 2), } } false => self.result.score = Score::Cp(score), diff --git a/src/search/search_options.rs b/src/search/search_options.rs index ee303f0..3b8d666 100644 --- a/src/search/search_options.rs +++ b/src/search/search_options.rs @@ -9,6 +9,7 @@ pub(crate) struct SearchOptions { pub(crate) wtime: Option, pub(crate) btime: Option, pub(crate) hash: u16, + pub(crate) new_game: bool, } #[derive(Debug, Clone)] @@ -18,6 +19,7 @@ pub(crate) struct BuiltSearchOptions { pub(crate) time: Option, pub(crate) position: Chess, pub(crate) hash: u16, + pub(crate) new_game: bool, } impl Default for SearchOptions { @@ -30,6 +32,7 @@ impl Default for SearchOptions { move_time: None, position: Chess::default(), hash: 16, + new_game: true, } } } @@ -47,6 +50,7 @@ impl SearchOptions { position: self.position.clone(), time: self.move_time.or(turn_time.map(|t| t / 25)), hash: self.hash, + new_game: self.new_game, } } diff --git a/src/uci.rs b/src/uci.rs index ad4a258..36c1eac 100644 --- a/src/uci.rs +++ b/src/uci.rs @@ -58,12 +58,17 @@ impl Uci { "setoption" => self.command_option(&mut queue), "bench" => self.command_bench(), "stop" => self.command_stop(), - "ucinewgame" => {} // TODO: implement ucinewgame command + "ucinewgame" => self.command_newgame(), "debug" => self.command_debug(&mut queue), _ => {} } } + fn command_newgame(&mut self) { + self.search_options.position = Chess::default(); + self.search_options.new_game = true; + } + fn command_debug(&mut self, _queue: &mut VecDeque<&str>) { println!("{:?}", self.search_options.position.board()); } @@ -188,9 +193,9 @@ impl Uci { match arg { "depth" => self.search_options.depth = queue.pop_front().and_then(|s| s.parse::().ok()), "nodes" => self.search_options.nodes = queue.pop_front().and_then(|s| s.parse::().ok()), - "movetime" => self.search_options.move_time = queue.pop_front().and_then(|s| s.parse::().ok()), "wtime" => self.search_options.wtime = queue.pop_front().and_then(|s| s.parse::().ok()), "btime" => self.search_options.btime = queue.pop_front().and_then(|s| s.parse::().ok()), + "movetime" => self.search_options.move_time = queue.pop_front().and_then(|s| s.parse::().ok()), _ => {} } } @@ -217,5 +222,9 @@ impl Uci { // Store the handle of the new search thread self.search_handle = Some(handle); + + if self.search_options.new_game { + self.search_options.new_game = false; + } } } From aa791796a1970b87a6587d8b63285c75c456ad9e Mon Sep 17 00:00:00 2001 From: Ludovic Date: Sat, 8 Aug 2026 12:31:17 +0200 Subject: [PATCH 4/5] feat(tt): add shared tt between uci and search bench 157031 --- src/search/search.rs | 22 ++++++++++------------ src/search/search_options.rs | 6 ------ src/search/tt.rs | 10 ++++------ src/uci.rs | 35 +++++++++++++++++++++++++---------- 4 files changed, 39 insertions(+), 34 deletions(-) diff --git a/src/search/search.rs b/src/search/search.rs index 0537a8f..733edea 100644 --- a/src/search/search.rs +++ b/src/search/search.rs @@ -14,37 +14,35 @@ use std::time::SystemTime; pub(crate) const MATE_SCORE: i16 = 30_000; pub(crate) const MAX_DEPTH: u8 = u8::MAX; -pub(crate) struct Search { +pub(crate) struct Search<'a> { opt: BuiltSearchOptions, #[allow(dead_code)] stop: Arc, result: SearchResult, start_time: SystemTime, - tt: TranspositionTable, + tt: &'a mut TranspositionTable, } -impl Search { - pub(crate) fn from(opt: &SearchOptions, stop: Arc) -> Self { +impl<'a> Search<'a> { + pub(crate) fn from( + opt: &SearchOptions, + stop: Arc, + tt: &'a mut TranspositionTable, + ) -> Self { let built_opt = opt.build(); - let hash = built_opt.hash; Search { opt: built_opt, stop, result: SearchResult::new(), start_time: SystemTime::now(), - tt: TranspositionTable::new(hash), + tt, } } } -impl Search { +impl<'a> Search<'a> { fn init(&mut self) { - if self.opt.new_game { - println!("CLEARING"); - self.tt.clear(); - } - self.result = SearchResult::new(); self.start_time = SystemTime::now(); } diff --git a/src/search/search_options.rs b/src/search/search_options.rs index 3b8d666..211d193 100644 --- a/src/search/search_options.rs +++ b/src/search/search_options.rs @@ -9,7 +9,6 @@ pub(crate) struct SearchOptions { pub(crate) wtime: Option, pub(crate) btime: Option, pub(crate) hash: u16, - pub(crate) new_game: bool, } #[derive(Debug, Clone)] @@ -18,8 +17,6 @@ pub(crate) struct BuiltSearchOptions { pub(crate) nodes: Option, pub(crate) time: Option, pub(crate) position: Chess, - pub(crate) hash: u16, - pub(crate) new_game: bool, } impl Default for SearchOptions { @@ -32,7 +29,6 @@ impl Default for SearchOptions { move_time: None, position: Chess::default(), hash: 16, - new_game: true, } } } @@ -49,8 +45,6 @@ impl SearchOptions { nodes: self.nodes, position: self.position.clone(), time: self.move_time.or(turn_time.map(|t| t / 25)), - hash: self.hash, - new_game: self.new_game, } } diff --git a/src/search/tt.rs b/src/search/tt.rs index dc8bdf1..8969c46 100644 --- a/src/search/tt.rs +++ b/src/search/tt.rs @@ -9,18 +9,17 @@ pub(crate) enum TTBound { #[derive(Debug, Clone)] pub(crate) struct TTEntry { - pub(crate) sig: u16, + pub(crate) key: Zobrist64, pub(crate) depth: u8, pub(crate) score: i16, pub(crate) bound: TTBound, - // pub(crate) best_move: [u8; 2], pub(crate) best_move: Option, } impl Default for TTEntry { fn default() -> Self { TTEntry { - sig: 0, + key: Zobrist64(0), depth: 0, score: 0, bound: TTBound::Exact, @@ -47,10 +46,9 @@ impl TranspositionTable { pub(crate) fn probe(&self, key: Zobrist64) -> Option<&TTEntry> { let index = key.0 as usize % self.length; - let entry = &self.table[index]; - if entry.sig != key.0 as u16 { + if entry.key != key { return None; } @@ -67,7 +65,7 @@ impl TranspositionTable { ) { let index = key.0 as usize % self.length; let entry = TTEntry { - sig: key.0 as u16, + key, depth, score, bound, diff --git a/src/uci.rs b/src/uci.rs index 36c1eac..c31ca1e 100644 --- a/src/uci.rs +++ b/src/uci.rs @@ -1,10 +1,11 @@ +use crate::search::tt::TranspositionTable; use crate::search::{search::Search, search_options::SearchOptions}; use shakmaty::fen::Fen; use shakmaty::uci::UciMove; use shakmaty::{CastlingMode, Chess, Position}; use std::collections::VecDeque; -use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; use std::thread::JoinHandle; const SEARCH_STACK_SIZE: usize = 8 * 1024 * 1024; // 8 MB @@ -15,6 +16,7 @@ pub(crate) struct Uci { stop: Arc, search_options: SearchOptions, search_handle: Option>, + tt: Arc>, } impl Default for Uci { @@ -24,6 +26,7 @@ impl Default for Uci { author: String::from("Ludovic Debever"), stop: Arc::new(AtomicBool::new(false)), search_options: SearchOptions::default(), + tt: Arc::new(Mutex::new(TranspositionTable::new(16))), search_handle: None, } } @@ -66,7 +69,7 @@ impl Uci { fn command_newgame(&mut self) { self.search_options.position = Chess::default(); - self.search_options.new_game = true; + self.tt.lock().unwrap().clear(); } fn command_debug(&mut self, _queue: &mut VecDeque<&str>) { @@ -118,10 +121,12 @@ impl Uci { let search_options = SearchOptions::default().depth(6); let stop = Arc::clone(&self.stop); + let tt = Arc::clone(&self.tt); let handle = std::thread::Builder::new() .stack_size(SEARCH_STACK_SIZE) .spawn(move || { - let mut search = Search::from(&search_options, stop); + let mut tt = tt.lock().unwrap(); + let mut search = Search::from(&search_options, stop, &mut tt); let result = search.run(); println!("{} nodes {} nps", result.nodes, result.nps); @@ -148,10 +153,21 @@ impl Uci { match value { "Hash" => { - self.search_options.hash = _queue + // Skip the "value" keyword before the actual number. + _queue.pop_front(); + let hash = _queue .pop_front() .and_then(|s| s.parse::().ok()) - .unwrap_or(16) + .unwrap_or(16); + + self.search_options.hash = hash; + self.tt = Arc::new(Mutex::new(TranspositionTable::new(hash))); + } + "Clear" => { + // "setoption name Clear Hash" arrives as two words; the first is consumed above. + if _queue.pop_front() == Some("Hash") { + self.tt.lock().unwrap().clear(); + } } "Threads" => {} _ => {} @@ -178,6 +194,7 @@ impl Uci { println!("id author {}", self.author); println!("option name Hash type spin default 1 min 1 max 16"); println!("option name Threads type spin default 1 min 1 max 1"); + println!("option name Clear Hash type button"); println!("uciok"); } @@ -209,11 +226,13 @@ impl Uci { self.stop.store(false, Ordering::Relaxed); let stop = Arc::clone(&self.stop); + let tt = Arc::clone(&self.tt); let search_options = self.search_options.clone(); let handle = std::thread::Builder::new() .stack_size(SEARCH_STACK_SIZE) .spawn(move || { - let mut search = Search::from(&search_options, stop); + let mut tt = tt.lock().unwrap(); + let mut search = Search::from(&search_options, stop, &mut tt); let result = search.run(); println!("bestmove {}", result.best_move); @@ -222,9 +241,5 @@ impl Uci { // Store the handle of the new search thread self.search_handle = Some(handle); - - if self.search_options.new_game { - self.search_options.new_game = false; - } } } From 2f5b805b87de07f1b88178d1f357f866e2339ef3 Mon Sep 17 00:00:00 2001 From: Ludovic Date: Sat, 8 Aug 2026 12:58:17 +0200 Subject: [PATCH 5/5] feat(tt): add generation bench 157031 --- src/search/search.rs | 2 ++ src/search/tt.rs | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/search/search.rs b/src/search/search.rs index 733edea..8c2882a 100644 --- a/src/search/search.rs +++ b/src/search/search.rs @@ -43,6 +43,7 @@ impl<'a> Search<'a> { impl<'a> Search<'a> { fn init(&mut self) { + self.tt.bump_generation(); self.result = SearchResult::new(); self.start_time = SystemTime::now(); } @@ -180,6 +181,7 @@ impl<'a> Search<'a> { if let Some(entry) = entry && !is_root + && entry.generation == self.tt.generation() && entry.depth >= depth && (entry.bound == TTBound::Exact || (entry.bound == TTBound::Alpha && entry.score <= alpha) diff --git a/src/search/tt.rs b/src/search/tt.rs index 8969c46..08f5d27 100644 --- a/src/search/tt.rs +++ b/src/search/tt.rs @@ -14,6 +14,7 @@ pub(crate) struct TTEntry { pub(crate) score: i16, pub(crate) bound: TTBound, pub(crate) best_move: Option, + pub(crate) generation: u8, } impl Default for TTEntry { @@ -22,6 +23,7 @@ impl Default for TTEntry { key: Zobrist64(0), depth: 0, score: 0, + generation: 0, bound: TTBound::Exact, best_move: None, } @@ -32,6 +34,7 @@ impl Default for TTEntry { pub(crate) struct TranspositionTable { pub table: Vec, length: usize, + generation: u8, } impl TranspositionTable { @@ -41,9 +44,18 @@ impl TranspositionTable { Self { table: vec![TTEntry::default(); length], length, + generation: 0, } } + pub(crate) fn generation(&self) -> u8 { + self.generation + } + + pub(crate) fn bump_generation(&mut self) { + self.generation = self.generation.wrapping_add(1); + } + pub(crate) fn probe(&self, key: Zobrist64) -> Option<&TTEntry> { let index = key.0 as usize % self.length; let entry = &self.table[index]; @@ -70,6 +82,7 @@ impl TranspositionTable { score, bound, best_move, + generation: self.generation, }; self.table[index] = entry; @@ -77,5 +90,6 @@ impl TranspositionTable { pub(crate) fn clear(&mut self) { self.table = vec![TTEntry::default(); self.length]; + self.generation = 0; } }