diff --git a/crates/chess/src/pieces.rs b/crates/chess/src/pieces.rs index ca49b72b..728ad864 100644 --- a/crates/chess/src/pieces.rs +++ b/crates/chess/src/pieces.rs @@ -3,7 +3,10 @@ // GNU General Public License v3.0 or later // https://www.gnu.org/licenses/gpl-3.0-standalone.html -use std::fmt::Display; +use std::{ + fmt::Display, + ops::{Index, IndexMut}, +}; use crate::definitions::NumberOf; @@ -168,6 +171,20 @@ impl TryFrom for Piece { } } +impl Index for [T; N] { + type Output = T; + + fn index(&self, piece: Piece) -> &Self::Output { + &self[piece as usize] + } +} + +impl IndexMut for [T; N] { + fn index_mut(&mut self, piece: Piece) -> &mut Self::Output { + &mut self[piece as usize] + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/engine/src/history.rs b/crates/engine/src/history.rs index 3f4aeb6b..8ed8b6e9 100644 --- a/crates/engine/src/history.rs +++ b/crates/engine/src/history.rs @@ -5,7 +5,7 @@ //! This module contains all history tables and consolidates them into a Histories object. -use chess::{bitboard::Bitboard, moves::Move, side::Side}; +use chess::{bitboard::Bitboard, moves::Move, pieces::Piece, side::Side}; use crate::{history::quiet_history::QuietHistory, score::LargeScoreType}; @@ -21,8 +21,14 @@ pub struct Histories { } impl Histories { - pub(crate) fn get(&self, side: Side, mv: Move, threats: Bitboard) -> LargeScoreType { - self.quiet_history.get(side, mv, threats) + pub(crate) fn get( + &self, + side: Side, + mv: Move, + piece: Piece, + threats: Bitboard, + ) -> LargeScoreType { + self.quiet_history.get(side, mv, piece, threats) } pub fn clear(&mut self) { diff --git a/crates/engine/src/history/quiet_history.rs b/crates/engine/src/history/quiet_history.rs index 903ffec6..b24ae478 100644 --- a/crates/engine/src/history/quiet_history.rs +++ b/crates/engine/src/history/quiet_history.rs @@ -3,14 +3,16 @@ // GNU General Public License v3.0 or later // https://www.gnu.org/licenses/gpl-3.0-standalone.html -use chess::{bitboard::Bitboard, definitions::NumberOf, moves::Move, side::Side}; +use chess::{bitboard::Bitboard, definitions::NumberOf, moves::Move, pieces::Piece, side::Side}; use crate::{ history::{ threat_bucket::{ThreatBucket, ThreatIndex}, - types::{self, FromToHistory}, + types::{self, FromToHistory, PieceToHistory}, }, + math, score::{LargeScoreType, Score}, + tuneable::quiet_history_factor, }; /// A single quiet-history cell, split into a threat-agnostic `factoriser` and a `bucket` indexed @@ -27,13 +29,13 @@ struct QuietHistoryEntry { } impl QuietHistoryEntry { - fn score(&self, threat_index: ThreatIndex) -> LargeScoreType { + fn score(&self, threat_index: &ThreatIndex) -> LargeScoreType { self.factorizer + self.bucket[threat_index.from()][threat_index.to()] } fn update( &mut self, - threat_index: ThreatIndex, + threat_index: &ThreatIndex, bonus: LargeScoreType, factorizer_bonus: LargeScoreType, ) { @@ -59,6 +61,7 @@ fn gravity(current: LargeScoreType, bonus: LargeScoreType, max: LargeScoreType) /// history), with each entry further split into threat buckets (see [`QuietHistoryEntry`]). pub struct QuietHistory { from_to_entries: [FromToHistory; NumberOf::SIDES], + piece_to_entries: [PieceToHistory; NumberOf::SIDES], } /// Safe calculation of the bonus applied to quiet moves that are inserted into the history table. @@ -78,24 +81,34 @@ pub(crate) fn calculate_bonus_for_depth(depth: i16) -> i16 { impl QuietHistory { pub(crate) fn new() -> Self { let from_to_entries = [types::default_from_to_history(); NumberOf::SIDES]; - Self { from_to_entries } + let piece_to_entries = [types::default_piece_to_history(); NumberOf::SIDES]; + Self { + from_to_entries, + piece_to_entries, + } } - pub(crate) fn get(&self, side: Side, mv: Move, threats: Bitboard) -> LargeScoreType { + pub(crate) fn get(&self, side: Side, mv: Move, pc: Piece, threats: Bitboard) -> LargeScoreType { let idx = ThreatIndex::new(&mv, threats); - self.from_to_entries[side][mv.from()][mv.to()].score(idx) + let from_to_score = self.from_to_entries[side][mv.from()][mv.to()].score(&idx); + let piece_to_score = self.piece_to_entries[side][pc][mv.to()].score(&idx); + + math::lerp(from_to_score, piece_to_score, quiet_history_factor()) } pub(crate) fn update( &mut self, side: Side, mv: Move, + piece: Piece, threats: Bitboard, bonus: LargeScoreType, factorizer_bonus: LargeScoreType, ) { let idx = ThreatIndex::new(&mv, threats); - self.from_to_entries[side][mv.from()][mv.to()].update(idx, bonus, factorizer_bonus); + self.from_to_entries[side][mv.from()][mv.to()].update(&idx, bonus, factorizer_bonus); + + self.piece_to_entries[side][piece][mv.to()].update(&idx, bonus, factorizer_bonus); } pub(crate) fn clear(&mut self) { @@ -114,7 +127,7 @@ mod tests { use crate::{defs::MAX_DEPTH, score::Score}; use super::{QuietHistory, calculate_bonus_for_depth}; - use chess::{bitboard::Bitboard, moves::Move, side::Side, square::Square}; + use chess::{bitboard::Bitboard, moves::Move, pieces::Piece, side::Side, square::Square}; #[test] fn initialize_history_table() { @@ -140,20 +153,21 @@ mod tests { // something worth hardcoding here. let mut history_table = QuietHistory::new(); let mv = Move::new(Square::B1, Square::A1, chess::moves::MoveFlag::Standard); + let piece = Piece::Pawn; let side = Side::Black; let score = 37; let no_threats = Bitboard::default(); - assert_eq!(history_table.get(side, mv, no_threats), 0); - history_table.update(side, mv, no_threats, score, score); - let after_first = history_table.get(side, mv, no_threats); + assert_eq!(history_table.get(side, mv, piece, no_threats), 0); + history_table.update(side, mv, piece, no_threats, score, score); + let after_first = history_table.get(side, mv, piece, no_threats); assert!( after_first > 0, "a positive bonus must raise the score above zero" ); - history_table.update(side, mv, no_threats, score, score); - let after_second = history_table.get(side, mv, no_threats); + history_table.update(side, mv, piece, no_threats, score, score); + let after_second = history_table.get(side, mv, piece, no_threats); assert!( after_second > after_first, "a second positive bonus must raise the score further" @@ -163,6 +177,7 @@ mod tests { #[test] fn threat_buckets_are_independent_of_untouched_buckets() { let mut history_table = QuietHistory::new(); + let piece = Piece::Pawn; let mv = Move::new(Square::B1, Square::A1, chess::moves::MoveFlag::Standard); let side = Side::Black; @@ -171,10 +186,10 @@ mod tests { let both_threatened = Bitboard::from(Square::B1) | Bitboard::from(Square::A1); // Only ever update the "both squares threatened" bucket. - history_table.update(side, mv, both_threatened, 1000, 1000); + history_table.update(side, mv, piece, both_threatened, 1000, 1000); - let untouched = history_table.get(side, mv, no_threats); - let touched = history_table.get(side, mv, both_threatened); + let untouched = history_table.get(side, mv, piece, no_threats); + let touched = history_table.get(side, mv, piece, both_threatened); assert!( untouched > 0, "the factoriser baseline should move on every update, regardless of threat state" @@ -184,7 +199,7 @@ mod tests { "the touched bucket should score higher than a bucket that never saw the bonus" ); - let from_only = history_table.get(side, mv, from_only_threatened); + let from_only = history_table.get(side, mv, piece, from_only_threatened); assert_eq!( from_only, untouched, "a bucket that was never updated should equal the other untouched buckets" @@ -196,15 +211,16 @@ mod tests { let mut history_table = QuietHistory::new(); let mv = Move::new(Square::B1, Square::A1, chess::moves::MoveFlag::Standard); let side = Side::Black; + let piece = Piece::Pawn; let threats = Bitboard::from(Square::B1) | Bitboard::from(Square::A1); // Hammer the same cell with maximal bonuses to try to force it past MAX_HISTORY - // a saturated entry must never be able to sort above KILLER_BONUS in the move picker. for _ in 0..10_000 { - history_table.update(side, mv, threats, i32::MAX, i32::MAX); + history_table.update(side, mv, piece, threats, i32::MAX, i32::MAX); } - let score = history_table.get(side, mv, threats); + let score = history_table.get(side, mv, piece, threats); assert!( score <= Score::MAX_HISTORY, "saturated quiet history entry ({score}) must not exceed MAX_HISTORY ({})", diff --git a/crates/engine/src/history/types.rs b/crates/engine/src/history/types.rs index b7b0c547..b61b87cc 100644 --- a/crates/engine/src/history/types.rs +++ b/crates/engine/src/history/types.rs @@ -9,6 +9,12 @@ use chess::definitions::NumberOf; /// Also known as 'butterfly' history. pub(crate) type FromToHistory = [[T; NumberOf::SQUARES]; NumberOf::SQUARES]; +pub(crate) type PieceToHistory = [[T; NumberOf::SQUARES]; NumberOf::PIECE_TYPES]; + pub(crate) fn default_from_to_history() -> FromToHistory { [[Default::default(); NumberOf::SQUARES]; NumberOf::SQUARES] } + +pub(crate) fn default_piece_to_history() -> PieceToHistory { + [[Default::default(); NumberOf::SQUARES]; NumberOf::PIECE_TYPES] +} diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index c1651d4a..1a10ec76 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -21,6 +21,7 @@ pub mod history; pub mod killers_table; mod lmr; pub mod log_level; +mod math; mod move_picker; pub(crate) mod node; pub(crate) mod node_types; diff --git a/crates/engine/src/math.rs b/crates/engine/src/math.rs new file mode 100644 index 00000000..0da5d31f --- /dev/null +++ b/crates/engine/src/math.rs @@ -0,0 +1,36 @@ +// Part of the byte-knight project. +// Author: Paul Tsouchlos (ptsouchlos) (developer.paul.123@gmail.com) +// GNU General Public License v3.0 or later +// https://www.gnu.org/licenses/gpl-3.0-standalone.html + +/// Linearly interpolate between a and b using a factor ([0 - 100]). +/// See also https://en.cppreference.com/cpp/numeric/lerp. +/// +/// # Arguments +/// - `a`: The first value. +/// - `b`: The second value. +/// - `factor`: The factor to use to scale between the two values. +pub(crate) fn lerp(a: i32, b: i32, factor: i32) -> i32 { + debug_assert!( + (0..=100).contains(&factor), + "factor must be between 0 and 100: {factor}" + ); + + let a_scale = 100 - factor; + let b_scale = factor; + + ((a * a_scale) + (b * b_scale)) / 100 +} + +#[cfg(test)] +mod tests { + #[test] + fn lerp_validation() { + let a = 20i32; + let b = 40i32; + + let factor = 50; + let result = super::lerp(a, b, factor); + assert_eq!(result, 30); + } +} diff --git a/crates/engine/src/move_picker.rs b/crates/engine/src/move_picker.rs index c3357d26..fcd02ea4 100644 --- a/crates/engine/src/move_picker.rs +++ b/crates/engine/src/move_picker.rs @@ -230,7 +230,7 @@ impl MovePicker { if is_killer { KILLER_BONUS } else { - histories.get(board.side_to_move(), *mv, threats) + histories.get(board.side_to_move(), *mv, piece, threats) } } } @@ -640,9 +640,13 @@ mod tests { let all_moves = move_generation::legal::generate_all_moves(&board); // Give a high history score to the move at index 3 let favored_mv = *all_moves.at(3).unwrap(); + let piece = board + .piece_type_on_square(favored_mv.from()) + .expect("Expected piece on board for move {mv}"); histories.quiet_history.update( board.side_to_move(), favored_mv, + piece, Bitboard::default(), Score::MAX_HISTORY, Score::MAX_HISTORY, diff --git a/crates/engine/src/search.rs b/crates/engine/src/search.rs index 59585c39..1d40d189 100644 --- a/crates/engine/src/search.rs +++ b/crates/engine/src/search.rs @@ -716,6 +716,7 @@ impl<'a, Log: LogLevel> Search<'a, Log> { td.histories.quiet_history.update( board.side_to_move(), mv, + piece, threats, bonus as LargeScoreType, bonus as LargeScoreType, @@ -724,13 +725,14 @@ impl<'a, Log: LogLevel> Search<'a, Log> { // Apply a penalty to all quiets searched so far. // The board is already in the parent state (we already unmade the move) // so it's safe to look up the piece on the board using mv.from(). - for &(prev_mv, _) in picker.searched_quiets() { + for &(prev_mv, prev_pc) in picker.searched_quiets() { if prev_mv == mv { continue; } td.histories.quiet_history.update( board.side_to_move(), prev_mv, + prev_pc, threats, -bonus as LargeScoreType, -bonus as LargeScoreType, @@ -1110,7 +1112,7 @@ mod tests { bitboard::Bitboard, board::Board, moves::{Move, MoveFlag}, - pieces::ALL_PIECES, + pieces::{ALL_PIECES, Piece}, square::Square, }; @@ -1359,9 +1361,11 @@ mod tests { Bitboard::from(to), Bitboard::from(from) | Bitboard::from(to), ] { - let score = td.histories.get(side, mv, threats); - if score > max_history { - max_history = score; + for piece in Piece::iter() { + let score = td.histories.get(side, mv, piece, threats); + if score > max_history { + max_history = score; + } } } } diff --git a/crates/engine/src/tuneable.rs b/crates/engine/src/tuneable.rs index 5aaee575..3beea143 100644 --- a/crates/engine/src/tuneable.rs +++ b/crates/engine/src/tuneable.rs @@ -39,6 +39,7 @@ tunable_params!( razoring_offset = 511, 250, 1000, 10, true; lmr_min_depth = 3, 1, 6, 1, false; lmr_min_moves_seen = 3, 1, 6, 1, false; + quiet_history_factor = 50, 1, 100, 1, true; ); pub(crate) const LMR_OFFSET: f64 = 0.2;