diff --git a/chess/src/square.rs b/chess/src/square.rs index bd028e29..3989ef3a 100644 --- a/chess/src/square.rs +++ b/chess/src/square.rs @@ -197,6 +197,12 @@ impl TryFrom<&str> for Square { } } +impl From for Square { + fn from(value: u8) -> Self { + Self::from_square_index(value) + } +} + /// Converts a file and rank tuple to a square /// /// # Arguments diff --git a/engine/src/history_table.rs b/engine/src/history_table.rs index 63756143..652dc87d 100644 --- a/engine/src/history_table.rs +++ b/engine/src/history_table.rs @@ -3,16 +3,18 @@ // GNU General Public License v3.0 or later // https://www.gnu.org/licenses/gpl-3.0-standalone.html -use chess::{ - definitions::NumberOf, - pieces::{PIECE_NAMES, Piece}, - side::Side, -}; +//! This module contains the definition and implementation for the history table. +//! See https://www.chessprogramming.org/History_Heuristic +//! This table gets updated where there is a cutoff. The new best move gets a bonus in the table, +//! while all other moves previously searched get a malus. The history table is used when scoring +//! moves for move ordering. + +use chess::{definitions::NumberOf, pieces::PIECE_NAMES, side::Side, square::Square}; use crate::score::{LargeScoreType, Score}; pub struct HistoryTable { - table: [[[LargeScoreType; NumberOf::SQUARES]; NumberOf::PIECE_TYPES]; NumberOf::SIDES], + table: [[[LargeScoreType; NumberOf::SQUARES]; NumberOf::SQUARES]; NumberOf::SIDES], } /// Safe calculation of the bonus applied to quiet moves that are inserted into the history table. @@ -25,36 +27,70 @@ pub struct HistoryTable { /// # Returns /// /// The calculated history score. -pub(crate) fn calculate_bonus_for_depth(depth: i16) -> i16 { +fn calculate_bonus_for_depth(depth: i16) -> i16 { depth .saturating_mul(Score::HISTORY_MULT) .saturating_sub(Score::HISTORY_OFFSET) } +/// Implementation of the history gravity formula. +/// See https://www.chessprogramming.org/History_Heuristic +/// +/// # Arguments +/// - `current_value`: The current value stored in the table. +/// - `clamped_bonus`: The clamped bonus value. +/// +/// # Returns +/// - An updated score value. +fn gravity_update(current_value: i32, clamped_bonus: i32) -> i32 { + current_value + clamped_bonus - current_value * clamped_bonus.abs() / Score::MAX_HISTORY +} + +/// History update type (+ or -) +pub(crate) enum HistoryUpdateType { + Bonus, + Malus, +} + impl HistoryTable { pub(crate) fn new() -> Self { - let table = - [[[Default::default(); NumberOf::SQUARES]; NumberOf::PIECE_TYPES]; NumberOf::SIDES]; + let table = [[[Default::default(); NumberOf::SQUARES]; NumberOf::SQUARES]; NumberOf::SIDES]; Self { table } } - pub(crate) fn get(&self, side: Side, piece: Piece, square: u8) -> LargeScoreType { - self.table[side as usize][piece as usize][square as usize] + pub(crate) fn get(&self, side: Side, from: Square, to: Square) -> LargeScoreType { + self.table[side as usize][from.to_square_index() as usize][to.to_square_index() as usize] + } + + fn set(&mut self, side: Side, from: Square, to: Square, value: LargeScoreType) { + self.table[side as usize][from.to_square_index() as usize][to.to_square_index() as usize] = + value; } - pub(crate) fn update(&mut self, side: Side, piece: Piece, square: u8, bonus: LargeScoreType) { - let current_value = self.table[side as usize][piece as usize][square as usize]; + pub(crate) fn update( + &mut self, + depth: i16, + side: Side, + from: Square, + to: Square, + update_type: HistoryUpdateType, + ) { + let bonus = match update_type { + HistoryUpdateType::Bonus => calculate_bonus_for_depth(depth) as LargeScoreType, + HistoryUpdateType::Malus => -calculate_bonus_for_depth(depth) as LargeScoreType, + }; + + let current_value = self.get(side, from, to); let clamped_bonus = bonus.clamp(-Score::MAX_HISTORY, Score::MAX_HISTORY); - let new_value = current_value + clamped_bonus - - current_value * clamped_bonus.abs() / Score::MAX_HISTORY; - self.table[side as usize][piece as usize][square as usize] = new_value; + let new_value = gravity_update(current_value, clamped_bonus); + self.set(side, from, to, new_value); } pub(crate) fn clear(&mut self) { for side in 0..NumberOf::SIDES { - for piece_type in 0..NumberOf::PIECE_TYPES { - for square in 0..NumberOf::SQUARES { - self.table[side][piece_type][square] = Default::default(); + for sq_from in 0..NumberOf::SQUARES { + for sq_to in 0..NumberOf::SQUARES { + self.table[side][sq_from][sq_to] = Default::default(); } } } @@ -84,20 +120,28 @@ impl Default for HistoryTable { #[cfg(test)] mod tests { - use crate::defs::MAX_DEPTH; + use crate::{ + defs::MAX_DEPTH, + history_table::{HistoryUpdateType, gravity_update}, + score::LargeScoreType, + }; use super::{HistoryTable, calculate_bonus_for_depth}; - use chess::{definitions::Squares, pieces::Piece, side::Side}; + use chess::{ + definitions::{NumberOf, Squares}, + side::Side, + square::Square, + }; #[test] fn initialize_history_table() { let history_table = HistoryTable::new(); // loop through all sides, piece types, and squares for side in 0..2 { - for piece_type in 0..6 { - for square in 0..64 { + for sq_from in 0..NumberOf::SQUARES { + for sq_to in 0..NumberOf::SQUARES { assert_eq!( - history_table.table[side][piece_type][square], + history_table.table[side][sq_from][sq_to], Default::default() ); } @@ -109,13 +153,25 @@ mod tests { fn store_and_read() { let mut history_table = HistoryTable::new(); let side = Side::Black; - let piece = Piece::Pawn; - let square = Squares::A1; - let score = 37; - history_table.update(side, piece, square, score); - assert_eq!(history_table.get(side, piece, square), score); - history_table.update(side, piece, square, score); - assert_eq!(history_table.get(side, piece, square), score + score); + let from: Square = Squares::B2.into(); + let to: Square = Squares::C3.into(); + let depth = 5; + let score = calculate_bonus_for_depth(depth) as LargeScoreType; + history_table.update(depth, side, from, to, HistoryUpdateType::Bonus); + + assert_eq!(history_table.get(side, from, to), score); + history_table.update(depth, side, from, to, HistoryUpdateType::Bonus); + assert_eq!( + history_table.get(side, from, to), + gravity_update(score, score) + ); + let current_value = history_table.get(side, from, to); + history_table.update(depth, side, from, to, HistoryUpdateType::Malus); + assert!(history_table.get(side, from, to) < score + score); + assert_eq!( + history_table.get(side, from, to), + gravity_update(current_value, -score) + ); } #[test] diff --git a/engine/src/move_order.rs b/engine/src/move_order.rs index 427a0c3a..91bfabf4 100644 --- a/engine/src/move_order.rs +++ b/engine/src/move_order.rs @@ -76,7 +76,7 @@ impl MoveOrder { return Self::Capture(victim, attacker); } - let score = history_table.get(stm, mv.piece(), mv.to()); + let score = history_table.get(stm, mv.from().into(), mv.to().into()); Self::Quiet(score) } @@ -132,10 +132,11 @@ mod tests { let second_mv = move_list.at(2).unwrap(); history_table.update( + depth as i16, board.side_to_move(), - second_mv.piece(), - second_mv.to(), - 300 * depth - 250, + second_mv.from().into(), + second_mv.to().into(), + crate::history_table::HistoryUpdateType::Bonus, ); let tt_entry = tt.get_entry(board.zobrist_hash()).unwrap(); let tt_move = tt_entry.board_move; diff --git a/engine/src/search.rs b/engine/src/search.rs index d3a1fb10..84b3858b 100644 --- a/engine/src/search.rs +++ b/engine/src/search.rs @@ -25,14 +25,14 @@ use crate::{ aspiration_window::AspirationWindow, defs::MAX_DEPTH, evaluation::ByteKnightEvaluation, - history_table::{self, HistoryTable}, + history_table::{HistoryTable, HistoryUpdateType}, inplace_incremental_sort::InplaceIncrementalSort, lmr, log_level::LogLevel, move_order::MoveOrder, node_types::{NodeType, NonPvNode, PvNode, RootNode}, principle_variation::PrincipleVariation, - score::{LargeScoreType, Score, ScoreType}, + score::{Score, ScoreType}, table::Table, traits::Eval, ttable::{self, TranspositionTableEntry}, @@ -584,22 +584,22 @@ impl<'a, Log: LogLevel> Search<'a, Log> { if alpha_use >= beta { // update history table for quiets if mv.is_quiet() { - // calculate history bonus - let bonus = history_table::calculate_bonus_for_depth(depth); self.history_table.update( + depth, board.side_to_move(), - mv.piece(), - mv.to(), - bonus as LargeScoreType, + mv.from().into(), + mv.to().into(), + HistoryUpdateType::Bonus, ); // apply a penalty to all quiets searched so far for mv in move_list.iter().take(i).filter(|mv| mv.is_quiet()) { self.history_table.update( + depth, board.side_to_move(), - mv.piece(), - mv.to(), - -bonus as LargeScoreType, + mv.from().into(), + mv.to().into(), + HistoryUpdateType::Malus, ); } } @@ -894,13 +894,11 @@ mod tests { use crate::{ evaluation::ByteKnightEvaluation, log_level::LogDebug, - score::Score, + score::{LargeScoreType, Score}, search::{Search, SearchParameters}, ttable::TranspositionTable, }; - use super::LargeScoreType; - fn run_search_tests(test_pairs: &[(&str, &str)], config: SearchParameters) { let mut ttable = TranspositionTable::default(); let mut history_table = Default::default(); @@ -1119,9 +1117,9 @@ mod tests { let side = board.side_to_move(); let mut max_history = LargeScoreType::MIN; - for piece in ALL_PIECES { - for square in 0..64 { - let score = history_table.get(side, piece, square); + for from in 0..64 { + for to in 0..64 { + let score = history_table.get(side, from.into(), to.into()); if score > max_history { max_history = score; }