Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions chess/src/square.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,12 @@ impl TryFrom<&str> for Square {
}
}

impl From<u8> for Square {
fn from(value: u8) -> Self {
Self::from_square_index(value)
}
}

/// Converts a file and rank tuple to a square
///
/// # Arguments
Expand Down
118 changes: 87 additions & 31 deletions engine/src/history_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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();
}
}
}
Expand Down Expand Up @@ -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()
);
}
Expand All @@ -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]
Expand Down
9 changes: 5 additions & 4 deletions engine/src/move_order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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;
Expand Down
30 changes: 14 additions & 16 deletions engine/src/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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,
);
}
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
}
Expand Down