Skip to content
Closed
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
19 changes: 18 additions & 1 deletion crates/chess/src/pieces.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -168,6 +171,20 @@ impl TryFrom<char> for Piece {
}
}

impl<T, const N: usize> Index<Piece> for [T; N] {
type Output = T;

fn index(&self, piece: Piece) -> &Self::Output {
&self[piece as usize]
}
}

impl<T, const N: usize> IndexMut<Piece> for [T; N] {
fn index_mut(&mut self, piece: Piece) -> &mut Self::Output {
&mut self[piece as usize]
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
12 changes: 9 additions & 3 deletions crates/engine/src/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand All @@ -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) {
Expand Down
56 changes: 36 additions & 20 deletions crates/engine/src/history/quiet_history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
) {
Expand All @@ -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<QuietHistoryEntry>; NumberOf::SIDES],
piece_to_entries: [PieceToHistory<QuietHistoryEntry>; NumberOf::SIDES],
}

/// Safe calculation of the bonus applied to quiet moves that are inserted into the history table.
Expand All @@ -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) {
Expand All @@ -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() {
Expand All @@ -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"
Expand All @@ -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;

Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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 ({})",
Expand Down
6 changes: 6 additions & 0 deletions crates/engine/src/history/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ use chess::definitions::NumberOf;
/// Also known as 'butterfly' history.
pub(crate) type FromToHistory<T> = [[T; NumberOf::SQUARES]; NumberOf::SQUARES];

pub(crate) type PieceToHistory<T> = [[T; NumberOf::SQUARES]; NumberOf::PIECE_TYPES];

pub(crate) fn default_from_to_history<T: Default + Copy>() -> FromToHistory<T> {
[[Default::default(); NumberOf::SQUARES]; NumberOf::SQUARES]
}

pub(crate) fn default_piece_to_history<T: Default + Copy>() -> PieceToHistory<T> {
[[Default::default(); NumberOf::SQUARES]; NumberOf::PIECE_TYPES]
}
1 change: 1 addition & 0 deletions crates/engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
36 changes: 36 additions & 0 deletions crates/engine/src/math.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
6 changes: 5 additions & 1 deletion crates/engine/src/move_picker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Expand Down Expand Up @@ -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,
Expand Down
14 changes: 9 additions & 5 deletions crates/engine/src/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -1110,7 +1112,7 @@ mod tests {
bitboard::Bitboard,
board::Board,
moves::{Move, MoveFlag},
pieces::ALL_PIECES,
pieces::{ALL_PIECES, Piece},
square::Square,
};

Expand Down Expand Up @@ -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;
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/engine/src/tuneable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down