diff --git a/lib/search.rs b/lib/search.rs index ec673f35..e214e514 100644 --- a/lib/search.rs +++ b/lib/search.rs @@ -2,6 +2,7 @@ mod control; mod depth; mod driver; mod engine; +mod history; mod killers; mod limits; mod options; @@ -14,6 +15,7 @@ pub use control::*; pub use depth::*; pub use driver::*; pub use engine::*; +pub use history::*; pub use killers::*; pub use limits::*; pub use options::*; diff --git a/lib/search/engine.rs b/lib/search/engine.rs index e66f2d2a..1607c29f 100644 --- a/lib/search/engine.rs +++ b/lib/search/engine.rs @@ -21,6 +21,8 @@ pub struct Engine { tt: TranspositionTable, #[cfg_attr(test, strategy(LazyJust::new(Killers::default)))] killers: Killers, + #[cfg_attr(test, strategy(LazyJust::new(History::default)))] + history: History, } impl Default for Engine { @@ -41,31 +43,42 @@ impl Engine { driver: Driver::new(options.threads), tt: TranspositionTable::new(options.hash), killers: Killers::default(), + history: History::default(), } } - /// Records a `[Transposition`]. + #[allow(clippy::too_many_arguments)] fn record( &self, pos: &Evaluator, + moves: &[(Move, Value)], bounds: Range, depth: Depth, ply: Ply, best: Move, score: Score, ) { + let draft = depth - ply; if score >= bounds.end && best.is_quiet() { self.killers.insert(ply, pos.turn(), best); + self.history.update(best, pos.turn(), draft.get()); + for &(m, _) in moves.iter().rev() { + if m == best { + break; + } else if m.is_quiet() { + self.history.update(m, pos.turn(), -draft.get()); + } + } } self.tt.set( pos.zobrist(), if score >= bounds.end { - Transposition::lower(depth - ply, score.normalize(-ply), best) + Transposition::lower(draft, score.normalize(-ply), best) } else if score <= bounds.start { - Transposition::upper(depth - ply, score.normalize(-ply), best) + Transposition::upper(draft, score.normalize(-ply), best) } else { - Transposition::exact(depth - ply, score.normalize(-ply), best) + Transposition::exact(draft, score.normalize(-ply), best) }, ); } @@ -134,7 +147,7 @@ impl Engine { ply: Ply, ctrl: &Control, ) -> Result, Interrupted> { - if ply < N && depth > ply && bounds.start + 1 < bounds.end { + if ply.cast::() < N && depth > ply && bounds.start + 1 < bounds.end { self.pvs(pos, bounds, depth, ply, ctrl) } else { Ok(self.pvs::<0>(pos, bounds, depth, ply, ctrl)?.convert()) @@ -267,7 +280,7 @@ impl Engine { } else if killers.contains(m) { (m, Value::new(25)) } else if m.is_quiet() { - (m, Value::lower()) + (m, Value::lower() / 2 + self.history.get(m, pos.turn())) } else { let mut next = pos.material(); let material = next.evaluate(); @@ -313,7 +326,7 @@ impl Engine { }; if tail >= beta || moves.is_empty() { - self.record(pos, bounds, depth, ply, head, tail.score()); + self.record(pos, &[], bounds, depth, ply, head, tail.score()); return Ok(head >> tail); } @@ -327,7 +340,7 @@ impl Engine { next.play(m); self.tt.prefetch(next.zobrist()); - if gain < 0 && !pos.is_check() && !next.is_check() { + if gain <= Value::lower() / 2 && !pos.is_check() && !next.is_check() { if let Some(d) = self.lmp(alpha + next.evaluate(), draft) { if d <= 0 || -self.nw::<0>(&next, -alpha, d + ply, ply + 1, ctrl)? <= alpha { #[cfg(not(test))] @@ -345,7 +358,7 @@ impl Engine { Ok(partial) })?; - self.record(pos, bounds, depth, ply, head, tail.score()); + self.record(pos, &moves, bounds, depth, ply, head, tail.score()); Ok(head >> tail) } diff --git a/lib/search/history.rs b/lib/search/history.rs new file mode 100644 index 00000000..1fffde4c --- /dev/null +++ b/lib/search/history.rs @@ -0,0 +1,57 @@ +use crate::chess::{Color, Move}; +use crate::util::Assume; +use std::array; +use std::sync::atomic::{AtomicI8, Ordering::Relaxed}; + +/// [Historical statistics] about a [`Move`]. +/// +/// [Historical statistics]: https://www.chessprogramming.org/History_Heuristic +#[derive(Debug)] +pub struct History([[[AtomicI8; 2]; 64]; 64]); + +impl Default for History { + #[inline(always)] + fn default() -> Self { + History(array::from_fn(|_| { + array::from_fn(|_| [AtomicI8::new(0), AtomicI8::new(0)]) + })) + } +} + +impl History { + /// Update statistics about a [`Move`] for a side to move at a given draft. + #[inline(always)] + pub fn update(&self, m: Move, side: Color, bonus: i8) { + let bonus = bonus.max(-i8::MAX); + let slot = &self.0[m.whence() as usize][m.whither() as usize][side as usize]; + let result = slot.fetch_update(Relaxed, Relaxed, |h| { + Some((bonus as i16 - bonus.abs() as i16 * h as i16 / 127 + h as i16) as i8) + }); + + result.assume(); + } + + /// Returns the history bonus for a [`Move`]. + #[inline(always)] + pub fn get(&self, m: Move, side: Color) -> i8 { + self.0[m.whence() as usize][m.whither() as usize][side as usize].load(Relaxed) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use test_strategy::proptest; + + #[proptest] + fn update_only_changes_history_of_given_move( + c: Color, + b: i8, + m: Move, + #[filter((#m.whence(), #m.whither()) != (#n.whence(), #n.whither()))] n: Move, + ) { + let h = History::default(); + h.update(m, c, b); + assert_eq!(h.get(n, c), 0); + } +} diff --git a/lib/search/killers.rs b/lib/search/killers.rs index 9d5c0e45..6f2d2c3a 100644 --- a/lib/search/killers.rs +++ b/lib/search/killers.rs @@ -1,8 +1,8 @@ use crate::chess::{Color, Move}; use crate::search::Ply; use crate::util::{Assume, Binary, Bits, Integer}; -use std::sync::atomic::AtomicU32; -use std::{array, sync::atomic::Ordering::Relaxed}; +use std::array; +use std::sync::atomic::{AtomicU32, Ordering::Relaxed}; /// A pair of [killer moves]. /// diff --git a/lib/util/binary.rs b/lib/util/binary.rs index 18a459dd..72bf2154 100644 --- a/lib/util/binary.rs +++ b/lib/util/binary.rs @@ -27,11 +27,7 @@ impl Binary for Bits { } } -impl Binary for Option -where - T: Binary, - T::Bits: Default + Debug + Eq + PartialEq, -{ +impl> Binary for Option { type Bits = T::Bits; #[inline(always)] diff --git a/lib/util/saturating.rs b/lib/util/saturating.rs index b9a2ec4f..230542c1 100644 --- a/lib/util/saturating.rs +++ b/lib/util/saturating.rs @@ -1,4 +1,4 @@ -use crate::util::Integer; +use crate::util::{Integer, Signed}; use derive_more::{Debug, Display, Error}; use std::fmt::{self, Formatter}; use std::ops::{Add, Div, Mul, Neg, Sub}; @@ -9,19 +9,19 @@ use std::{cmp::Ordering, mem::size_of, num::Saturating as S, str::FromStr}; #[cfg_attr(test, derive(test_strategy::Arbitrary))] #[cfg_attr(test, arbitrary(bound(T, Self: Debug)))] #[debug("Saturating({self})")] -#[debug(bounds(T: Integer, T::Repr: Display))] +#[debug(bounds(T: Integer, T::Repr: Display))] #[repr(transparent)] pub struct Saturating(T); -unsafe impl Integer for Saturating { +unsafe impl> Integer for Saturating { type Repr = T::Repr; const MIN: Self::Repr = T::MIN; const MAX: Self::Repr = T::MAX; } -impl Eq for Saturating where Self: PartialEq {} +impl> Eq for Saturating where Self: PartialEq {} -impl PartialEq for Saturating { +impl, U: Integer> PartialEq for Saturating { #[inline(always)] fn eq(&self, other: &U) -> bool { if size_of::() > size_of::() { @@ -32,14 +32,14 @@ impl PartialEq for Saturating { } } -impl Ord for Saturating { +impl> Ord for Saturating { #[inline(always)] fn cmp(&self, other: &Self) -> Ordering { self.get().cmp(&other.get()) } } -impl PartialOrd for Saturating { +impl, U: Integer> PartialOrd for Saturating { #[inline(always)] fn partial_cmp(&self, other: &U) -> Option { if size_of::() > size_of::() { @@ -50,7 +50,7 @@ impl PartialOrd for Saturating { } } -impl Neg for Saturating +impl> Neg for Saturating where S: Neg>, { @@ -62,7 +62,7 @@ where } } -impl Add for Saturating +impl, U: Integer> Add for Saturating where S: Add>, S: Add>, @@ -79,7 +79,7 @@ where } } -impl Sub for Saturating +impl, U: Integer> Sub for Saturating where S: Sub>, S: Sub>, @@ -96,7 +96,7 @@ where } } -impl Mul for Saturating +impl, U: Integer> Mul for Saturating where S: Mul>, S: Mul>, @@ -113,7 +113,7 @@ where } } -impl Div for Saturating +impl, U: Integer> Div for Saturating where S: Div>, S: Div>, @@ -130,7 +130,7 @@ where } } -impl Display for Saturating +impl> Display for Saturating where T::Repr: Display, { @@ -144,7 +144,7 @@ where #[display("failed to parse saturating integer")] pub struct ParseSaturatingIntegerError; -impl FromStr for Saturating +impl> FromStr for Saturating where T::Repr: FromStr, {