diff --git a/engine/src/defs.rs b/engine/src/defs.rs index 33a8279..ef749f3 100644 --- a/engine/src/defs.rs +++ b/engine/src/defs.rs @@ -14,11 +14,11 @@ #[rustfmt::skip] const BANNER: &str = r#" - _ _ _ _ _ _ -| |__ _ _| |_ ___ ___| |___ _ (_)__ _| |_| |_ + _ _ _ _ _ _ +| |__ _ _| |_ ___ ___| |___ _ (_)__ _| |_| |_ | '_ \ || | _/ -_)___| / / ' \| / _` | ' \ _| |_.__/\_, |\__\___| |_\_\_||_|_\__, |_||_\__| - |__/ |___/ + |__/ |___/ "#; pub struct About; @@ -32,3 +32,4 @@ impl About { } pub(crate) const MAX_DEPTH: u8 = 128; +pub(crate) const MAX_KILLERS_PER_PLY: usize = 2; diff --git a/engine/src/engine.rs b/engine/src/engine.rs index a00d6b3..a8147f9 100644 --- a/engine/src/engine.rs +++ b/engine/src/engine.rs @@ -24,6 +24,7 @@ use crate::{ defs::About, history_table::HistoryTable, input_handler::{CommandProxy, EngineCommand, InputHandler}, + killer_moves_table::KillerMovesTable, log_level::{LogDebug, LogInfo, LogLevel}, search::SearchParameters, search_thread::SearchThread, @@ -35,6 +36,7 @@ pub struct ByteKnight { search_thread: SearchThread, transposition_table: Arc>, history_table: Arc>, + killers_table: Arc>, debug: bool, } @@ -45,18 +47,26 @@ impl ByteKnight { search_thread: SearchThread::new(), transposition_table: Default::default(), history_table: Default::default(), + killers_table: Default::default(), debug: false, } } fn clear_hash_tables(&mut self) { + // Clear transposition table if let Ok(tt) = self.transposition_table.lock().as_mut() { tt.clear(); } + // Clear history table if let Ok(ht) = self.history_table.lock().as_mut() { ht.clear(); } + + // Clear killers table + if let Ok(kt) = self.killers_table.lock().as_mut() { + kt.clear(); + } } /// Run the engine loop. This will block until the engine is told to quit by the input handler. @@ -212,6 +222,7 @@ impl ByteKnight { params, Arc::clone(&self.transposition_table), Arc::clone(&self.history_table), + Arc::clone(&self.killers_table), ); } } diff --git a/engine/src/killer_moves_table.rs b/engine/src/killer_moves_table.rs new file mode 100644 index 0000000..6aa672d --- /dev/null +++ b/engine/src/killer_moves_table.rs @@ -0,0 +1,68 @@ +use chess::moves::Move; + +use crate::defs::{MAX_DEPTH, MAX_KILLERS_PER_PLY}; + +pub struct KillerMovesTable { + table: [[Option; MAX_KILLERS_PER_PLY]; MAX_DEPTH as usize], +} + +impl KillerMovesTable { + pub(crate) fn new() -> Self { + let table = [[None; MAX_KILLERS_PER_PLY]; MAX_DEPTH as usize]; + + Self { table } + } + + pub(crate) fn get(&self, ply: u8) -> &[Option] { + assert!(ply < MAX_DEPTH, "Depth is out of bounds"); + + &self.table[ply as usize][..] + } + + fn get_mut(&mut self, ply: u8) -> &mut [Option] { + assert!(ply < MAX_DEPTH, "Depth is out of bounds"); + + &mut self.table[ply as usize][..] + } + + pub(crate) fn update(&mut self, ply: u8, mv: Move) { + assert!(ply < MAX_DEPTH, "Depth is out of bounds"); + + let current_killers = self.get_mut(ply); + if !current_killers[0].is_some_and(|killer_mv| killer_mv == mv) { + current_killers.swap(0, 1); + current_killers[0] = Some(mv); + } + } + + pub(crate) fn clear(&mut self) { + for item in self.table.as_flattened_mut() { + *item = None; + } + } +} + +impl Default for KillerMovesTable { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + + use crate::defs::{MAX_DEPTH, MAX_KILLERS_PER_PLY}; + + use super::KillerMovesTable; + + #[test] + + fn initialize_killers_table() { + let killers_table: KillerMovesTable = Default::default(); + for i in 0..MAX_DEPTH { + let killers = killers_table.get(i); + assert_eq!(killers, &[None, None]); + assert_eq!(killers.len(), MAX_KILLERS_PER_PLY); + } + } +} diff --git a/engine/src/lib.rs b/engine/src/lib.rs index 2a27a69..5a74d08 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -10,6 +10,7 @@ pub mod hce_values; pub mod history_table; mod inplace_incremental_sort; pub mod input_handler; +pub mod killer_moves_table; mod lmr; pub mod log_level; mod move_order; diff --git a/engine/src/move_order.rs b/engine/src/move_order.rs index 265df1b..dc449e1 100644 --- a/engine/src/move_order.rs +++ b/engine/src/move_order.rs @@ -5,7 +5,8 @@ use arrayvec::ArrayVec; use chess::{definitions::MAX_MOVE_LIST_SIZE, moves::Move, pieces::Piece, side::Side}; use crate::{ - evaluation::Evaluation, hce_values::ByteKnightValues, history_table, score::LargeScoreType, + evaluation::Evaluation, hce_values::ByteKnightValues, history_table, killer_moves_table, + score::LargeScoreType, }; #[derive(PartialEq, Eq, Copy, Clone, Debug, Default)] @@ -13,6 +14,7 @@ pub enum MoveOrder { #[default] TtMove, Capture(Piece, Piece), + Killer(LargeScoreType), Quiet(LargeScoreType), } @@ -44,6 +46,12 @@ impl Ord for MoveOrder { (MoveOrder::Capture(_, _), _) => Ordering::Less, (_, MoveOrder::Capture(_, _)) => Ordering::Greater, + // killer moves come next, according to their score + (MoveOrder::Killer(left_score), MoveOrder::Killer(right_score)) => { + right_score.cmp(left_score) + } + (MoveOrder::Killer(_), _) => Ordering::Less, + (_, MoveOrder::Killer(_)) => Ordering::Greater, // quiet moves come last, according to their score (MoveOrder::Quiet(left_score), MoveOrder::Quiet(right_score)) => { right_score.cmp(left_score) @@ -56,10 +64,12 @@ impl MoveOrder { /// Classify moves for move ordering purposes. #[allow(clippy::expect_used)] pub fn classify( + ply: u8, stm: Side, mv: &Move, tt_move: &Option, history_table: &history_table::HistoryTable, + killers_table: &killer_moves_table::KillerMovesTable, ) -> Self { if tt_move.is_some_and(|tt| *mv == tt) { return Self::TtMove; @@ -72,20 +82,37 @@ impl MoveOrder { } let score = history_table.get(stm, mv.piece(), mv.to()); + if killers_table + .get(ply) + .iter() + .any(|killer_mv| killer_mv.is_some_and(|k| k == *mv)) + { + return Self::Killer(score); + } + Self::Quiet(score) } pub fn classify_all( + ply: u8, stm: Side, moves: &[Move], tt_move: &Option, history_table: &history_table::HistoryTable, + killers_table: &killer_moves_table::KillerMovesTable, move_order: &mut ArrayVec, ) -> Result<()> { move_order.clear(); for mv in moves.iter() { - move_order.try_push(Self::classify(stm, mv, tt_move, history_table))?; + move_order.try_push(Self::classify( + ply, + stm, + mv, + tt_move, + history_table, + killers_table, + ))?; } Ok(()) @@ -107,6 +134,7 @@ mod tests { fn verify_move_ordering() { let mut tt = TranspositionTable::from_capacity(10); let mut history_table = crate::history_table::HistoryTable::new(); + let killers_table = crate::killer_moves_table::KillerMovesTable::new(); let move_gen = MoveGenerator::new(); let mut move_list = MoveList::new(); @@ -116,6 +144,7 @@ mod tests { assert!(move_list.len() >= 6); let depth = 3i32; + let ply = 3i32; let first_mv = move_list.at(4).unwrap(); tt.store_entry(TranspositionTableEntry::new( board.zobrist_hash(), @@ -132,13 +161,22 @@ mod tests { second_mv.to(), 300 * depth - 250, ); + // TODO + // killers_table.update(ply, mv); let tt_entry = tt.get_entry(board.zobrist_hash()).unwrap(); let tt_move = tt_entry.board_move; // sort the moves let moves = move_list .iter() .sorted_by_key(|mv| { - MoveOrder::classify(board.side_to_move(), mv, &Some(tt_move), &history_table) + MoveOrder::classify( + ply as u8, + board.side_to_move(), + mv, + &Some(tt_move), + &history_table, + &killers_table, + ) }) .collect::>(); diff --git a/engine/src/search.rs b/engine/src/search.rs index 56999ec..c8658d8 100644 --- a/engine/src/search.rs +++ b/engine/src/search.rs @@ -36,6 +36,7 @@ use crate::{ evaluation::ByteKnightEvaluation, history_table::{self, HistoryTable}, inplace_incremental_sort::InplaceIncrementalSort, + killer_moves_table::KillerMovesTable, lmr, log_level::LogLevel, move_order::MoveOrder, @@ -159,6 +160,7 @@ impl Display for SearchParameters { pub struct Search<'search_lifetime, Log> { transposition_table: &'search_lifetime mut TranspositionTable, history_table: &'search_lifetime mut HistoryTable, + killers_table: &'search_lifetime mut KillerMovesTable, move_gen: MoveGenerator, nodes: u64, parameters: SearchParameters, @@ -174,6 +176,7 @@ impl<'a, Log: LogLevel> Search<'a, Log> { parameters: &SearchParameters, ttable: &'a mut TranspositionTable, history_table: &'a mut HistoryTable, + killers_table: &'a mut KillerMovesTable, ) -> Self { // Initialize our LMR table as a 2D array of our LMR formula for depth and moves played let mut table = Table::::new(MAX_DEPTH as usize, MAX_MOVE_LIST_SIZE); @@ -182,6 +185,7 @@ impl<'a, Log: LogLevel> Search<'a, Log> { Self { transposition_table: ttable, history_table, + killers_table, move_gen: MoveGenerator::new(), nodes: 0, parameters: parameters.clone(), @@ -387,7 +391,7 @@ impl<'a, Log: LogLevel> Search<'a, Log> { let mut alpha_use = alpha; if depth <= 0 { - return self.quiescence::(board, alpha, beta, pv); + return self.quiescence::(ply as u8, board, alpha, beta, pv); } let mut local_pv = PrincipleVariation::new(); @@ -440,10 +444,12 @@ impl<'a, Log: LogLevel> Search<'a, Log> { } let classify_res = MoveOrder::classify_all( + ply as u8, board.side_to_move(), move_list.as_slice(), &tt_move, self.history_table, + self.killers_table, &mut order_list, ); @@ -535,6 +541,9 @@ impl<'a, Log: LogLevel> Search<'a, Log> { if alpha_use >= beta { // update history table for quiets if mv.is_quiet() { + // store the "killer" move + self.killers_table.update(ply as u8, mv); + // calculate history bonus let bonus = history_table::calculate_bonus_for_depth(depth); self.history_table.update( @@ -676,6 +685,7 @@ impl<'a, Log: LogLevel> Search<'a, Log> { /// fn quiescence( &mut self, + ply: u8, board: &mut Board, alpha: Score, beta: Score, @@ -727,10 +737,12 @@ impl<'a, Log: LogLevel> Search<'a, Log> { // sort moves by MVV/LVA let classify_res = MoveOrder::classify_all( + ply, board.side_to_move(), captures.as_slice(), &tt_move, self.history_table, + self.killers_table, &mut move_order_list, ); // TODO(PT): Should we log a message to the CLI or a log? @@ -752,7 +764,7 @@ impl<'a, Log: LogLevel> Search<'a, Log> { let score = if board.is_draw() { Score::DRAW } else { - let eval = -self.quiescence::(board, -beta, -alpha_use, &mut local_pv); + let eval = -self.quiescence::(ply, board, -beta, -alpha_use, &mut local_pv); self.nodes += 1; eval }; @@ -823,7 +835,9 @@ mod tests { fn run_search_tests(test_pairs: &[(&str, &str)], config: SearchParameters) { let mut ttable = TranspositionTable::default(); let mut history_table = Default::default(); - let mut search = Search::::new(&config, &mut ttable, &mut history_table); + let mut killers_table = Default::default(); + let mut search = + Search::::new(&config, &mut ttable, &mut history_table, &mut killers_table); for (fen, expected_move) in test_pairs { let mut board = Board::from_fen(fen).unwrap(); @@ -846,7 +860,9 @@ mod tests { let mut ttable = TranspositionTable::default(); let mut history_table = Default::default(); - let mut search = Search::::new(&config, &mut ttable, &mut history_table); + let mut killers_table = Default::default(); + let mut search = + Search::::new(&config, &mut ttable, &mut history_table, &mut killers_table); let res = search.search(&mut board.clone(), None); // b6a7 assert_eq!( @@ -866,7 +882,9 @@ mod tests { let mut ttable = Default::default(); let mut history_table = Default::default(); - let mut search = Search::::new(&config, &mut ttable, &mut history_table); + let mut killers_table = Default::default(); + let mut search = + Search::::new(&config, &mut ttable, &mut history_table, &mut killers_table); let res = search.search(&mut board, None); assert_eq!(res.best_move.unwrap().to_long_algebraic(), "b8a8") @@ -919,7 +937,9 @@ mod tests { let mut ttable = Default::default(); let mut history_table = Default::default(); - let mut search = Search::::new(&config, &mut ttable, &mut history_table); + let mut killers_table = Default::default(); + let mut search = + Search::::new(&config, &mut ttable, &mut history_table, &mut killers_table); let res = search.search(&mut board, None); assert!(res.best_move.is_none()); assert_eq!(res.score, Score::DRAW); @@ -937,7 +957,9 @@ mod tests { let mut ttable = Default::default(); let mut history_table = Default::default(); - let mut search = Search::::new(&config, &mut ttable, &mut history_table); + let mut killers_table = Default::default(); + let mut search = + Search::::new(&config, &mut ttable, &mut history_table, &mut killers_table); let res = search.search(&mut board, None); assert!(res.best_move.is_some()); @@ -954,7 +976,9 @@ mod tests { let mut ttable = Default::default(); let mut history_table = Default::default(); - let mut search = Search::::new(&config, &mut ttable, &mut history_table); + let mut killers_table = Default::default(); + let mut search = + Search::::new(&config, &mut ttable, &mut history_table, &mut killers_table); let res = search.search(&mut board, None); assert!(res.best_move.is_some()); println!("{}", res.best_move.unwrap().to_long_algebraic()); @@ -971,7 +995,9 @@ mod tests { let mut ttable = Default::default(); let mut history_table = Default::default(); - let mut search = Search::::new(&config, &mut ttable, &mut history_table); + let mut killers_table = Default::default(); + let mut search = + Search::::new(&config, &mut ttable, &mut history_table, &mut killers_table); let res = search.search(&mut board, None); assert!(res.best_move.is_some()); println!("{}", res.best_move.unwrap().to_long_algebraic()); @@ -1031,7 +1057,13 @@ mod tests { let mut ttable = Default::default(); let mut history_table = Default::default(); - let mut search = Search::::new(&config, &mut ttable, &mut history_table); + let mut killers_table = Default::default(); + let mut search = Search::::new( + &config, + &mut ttable, + &mut history_table, + &mut killers_table, + ); let res = search.search(&mut board, None); assert!(res.best_move.is_some()); diff --git a/engine/src/search_thread.rs b/engine/src/search_thread.rs index 4629d07..b4d78fd 100644 --- a/engine/src/search_thread.rs +++ b/engine/src/search_thread.rs @@ -28,6 +28,7 @@ use uci_parser::{UciMove, UciResponse}; use crate::{ history_table::HistoryTable, + killer_moves_table::KillerMovesTable, log_level::{LogDebug, LogInfo, LogLevel}, search::{Search, SearchParameters}, ttable::TranspositionTable, @@ -61,6 +62,7 @@ pub(crate) enum SearchThreadValue { SearchParameters, Arc>, Arc>, + Arc>, bool, ), Exit, @@ -94,17 +96,35 @@ impl SearchThread { 'search_loop: loop { let value = receiver.recv().unwrap(); match value { - SearchThreadValue::Params(mut board, params, ttable, history, is_debug) => { + SearchThreadValue::Params( + mut board, + params, + ttable, + history, + killer_moves, + is_debug, + ) => { let mut tt = ttable.lock().unwrap(); let mut hist_table = history.lock().unwrap(); + let mut killer_moves_table = killer_moves.lock().unwrap(); let flag = stop_flag.clone(); is_searching.store(true, Ordering::Relaxed); let result = if is_debug { - Search::::new(¶ms, &mut tt, &mut hist_table) - .search(&mut board, Some(flag)) + Search::::new( + ¶ms, + &mut tt, + &mut hist_table, + &mut killer_moves_table, + ) + .search(&mut board, Some(flag)) } else { - Search::::new(¶ms, &mut tt, &mut hist_table) - .search(&mut board, Some(flag)) + Search::::new( + ¶ms, + &mut tt, + &mut hist_table, + &mut killer_moves_table, + ) + .search(&mut board, Some(flag)) }; is_searching.store(false, Ordering::Relaxed); let best_move = result.best_move; @@ -156,6 +176,7 @@ impl SearchThread { params: SearchParameters, ttable: Arc>, history_table: Arc>, + killers_table: Arc>, ) { self.stop_search_flag.store(false, Ordering::Relaxed); self.sender @@ -164,6 +185,7 @@ impl SearchThread { params, ttable, history_table, + killers_table, Log::DEBUG, )) .unwrap(); diff --git a/src/bin/byte-knight/bench.rs b/src/bin/byte-knight/bench.rs index 2cf3330..bdcab2b 100644 --- a/src/bin/byte-knight/bench.rs +++ b/src/bin/byte-knight/bench.rs @@ -118,7 +118,8 @@ pub(crate) fn bench(depth: u8, epd_file: &Option) { let mut nodes = 0u64; let mut tt = Default::default(); let mut hist = Default::default(); - let mut search = Search::::new(&config, &mut tt, &mut hist); + let mut killers = Default::default(); + let mut search = Search::::new(&config, &mut tt, &mut hist, &mut killers); let max_fen_width = benchmark_strings.iter().map(|s| s.len()).max().unwrap();