Skip to content

Commit

Permalink
centralize logic to normalize score to ply
Browse files Browse the repository at this point in the history
  • Loading branch information
brunocodutra committed Jan 5, 2025
1 parent f4f5cc4 commit a092b37
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 138 deletions.
72 changes: 30 additions & 42 deletions lib/search/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,24 +71,17 @@ impl Engine {
}
}

self.tt.set(
pos.zobrist(),
if score >= bounds.end {
Transposition::lower(draft, score.normalize(-ply), best)
} else if score <= bounds.start {
Transposition::upper(draft, score.normalize(-ply), best)
} else {
Transposition::exact(draft, score.normalize(-ply), best)
},
);
let score = ScoreBound::new(bounds, score, ply);
let tpos = Transposition::new(score, draft, best);
self.tt.set(pos.zobrist(), tpos);
}

/// An implementation of [mate distance pruning].
///
/// [mate distance pruning]: https://www.chessprogramming.org/Mate_Distance_Pruning
fn mdp(&self, ply: Ply, bounds: &Range<Score>) -> (Score, Score) {
let lower = Score::lower().normalize(ply);
let upper = Score::upper().normalize(ply + 1); // One can't mate in 0 plies!
let lower = Score::mated(ply);
let upper = Score::mating(ply + 1); // One can't mate in 0 plies!
(bounds.start.max(lower), bounds.end.min(upper))
}

Expand Down Expand Up @@ -194,7 +187,7 @@ impl Engine {
None => self.mdp(ply, &bounds),
Some(Outcome::DrawByThreefoldRepetition) if is_root => self.mdp(ply, &bounds),
Some(o) if o.is_draw() => return Ok(Pv::new(Score::new(0), [])),
Some(_) => return Ok(Pv::new(Score::lower().normalize(ply), [])),
Some(_) => return Ok(Pv::new(Score::mated(ply), [])),
};

if alpha >= beta {
Expand Down Expand Up @@ -232,7 +225,7 @@ impl Engine {
let is_pv = alpha + 1 < beta;
if let Some(t) = transposition {
if !is_pv && t.draft() >= draft {
let (lower, upper) = t.bounds().into_inner();
let (lower, upper) = t.score().range(ply).into_inner();
if lower >= upper || upper <= alpha || lower >= beta {
return Ok(transposed.convert());
}
Expand All @@ -250,7 +243,7 @@ impl Engine {
}

if let Some(t) = transposition {
if let Some(d) = self.rfp(*t.bounds().start() - beta, draft) {
if let Some(d) = self.rfp(t.score().lower(ply) - beta, draft) {
if !is_pv && t.draft() >= d {
#[cfg(not(test))]
// The reverse futility pruning heuristic is not exact.
Expand Down Expand Up @@ -302,7 +295,7 @@ impl Engine {
moves.sort_unstable_by_key(|(_, gain)| *gain);

if let Some(t) = transposition {
if let Some(d) = self.mcp(*t.bounds().start() - beta, draft) {
if let Some(d) = self.mcp(t.score().lower(ply) - beta, draft) {
if !is_root && t.draft() >= d {
for (m, _) in moves.iter().rev().skip(1) {
let mut next = pos.clone();
Expand Down Expand Up @@ -422,15 +415,20 @@ impl Engine {

match partial.score() {
score if (-lower..Score::upper()).contains(&-score) => {
draft = depth;
upper = lower / 2 + upper / 2;
lower = score - delta;
draft = depth;
}

score if (upper..Score::upper()).contains(&score) => {
draft = draft - 1;
upper = score + delta;
pv = partial;

#[cfg(not(test))]
{
// Reductions are not exact.
draft = draft - 1;
}
}

_ => {
Expand Down Expand Up @@ -477,7 +475,7 @@ mod tests {

let score = match pos.outcome() {
Some(o) if o.is_draw() => return Score::new(0),
Some(_) => return Score::lower().normalize(ply),
Some(_) => return Score::mated(ply),
None => pos.evaluate().saturate(),
};

Expand Down Expand Up @@ -524,7 +522,8 @@ mod tests {
#[map(|s: Selector| s.select(#pos.moves().flatten()))] m: Move,
) {
use Control::Unlimited;
e.tt.set(pos.zobrist(), Transposition::lower(d, s, m));
let tpos = Transposition::new(ScoreBound::Lower(s), d, m);
e.tt.set(pos.zobrist(), tpos);
assert_eq!(e.nw::<1>(&pos, b, d, p, &Unlimited), Ok(Pv::new(s, [])));
}

Expand All @@ -541,7 +540,8 @@ mod tests {
#[map(|s: Selector| s.select(#pos.moves().flatten()))] m: Move,
) {
use Control::Unlimited;
e.tt.set(pos.zobrist(), Transposition::upper(d, s, m));
let tpos = Transposition::new(ScoreBound::Upper(s), d, m);
e.tt.set(pos.zobrist(), tpos);
assert_eq!(e.nw::<1>(&pos, b, d, p, &Unlimited), Ok(Pv::new(s, [])));
}

Expand All @@ -554,12 +554,13 @@ mod tests {
#[filter((Value::lower()..Value::upper()).contains(&#b))] b: Score,
d: Depth,
#[filter(#p >= 0)] p: Ply,
#[filter(#sc.mate().is_none())] sc: Score,
#[filter(#s.mate().is_none())] s: Score,
#[map(|s: Selector| s.select(#pos.moves().flatten()))] m: Move,
) {
use Control::Unlimited;
e.tt.set(pos.zobrist(), Transposition::exact(d, sc, m));
assert_eq!(e.nw::<1>(&pos, b, d, p, &Unlimited), Ok(Pv::new(sc, [])));
let tpos = Transposition::new(ScoreBound::Exact(s), d, m);
e.tt.set(pos.zobrist(), tpos);
assert_eq!(e.nw::<1>(&pos, b, d, p, &Unlimited), Ok(Pv::new(s, [])));
}

#[proptest]
Expand All @@ -582,7 +583,7 @@ mod tests {
pos: Evaluator,
#[filter(!#b.is_empty())] b: Range<Score>,
d: Depth,
p: Ply,
#[filter(#p > 0)] p: Ply,
) {
let trigger = Trigger::armed();
let ctrl = Control::Limited(Counter::new(0), Timer::infinite(), &trigger);
Expand All @@ -595,7 +596,7 @@ mod tests {
pos: Evaluator,
#[filter(!#b.is_empty())] b: Range<Score>,
d: Depth,
p: Ply,
#[filter(#p > 0)] p: Ply,
) {
let trigger = Trigger::armed();
let ctrl = Control::Limited(Counter::new(u64::MAX), Timer::new(Duration::ZERO), &trigger);
Expand All @@ -609,7 +610,7 @@ mod tests {
pos: Evaluator,
#[filter(!#b.is_empty())] b: Range<Score>,
d: Depth,
p: Ply,
#[filter(#p > 0)] p: Ply,
) {
let trigger = Trigger::disarmed();
let ctrl = Control::Limited(Counter::new(u64::MAX), Timer::infinite(), &trigger);
Expand Down Expand Up @@ -649,11 +650,11 @@ mod tests {
#[filter(#pos.outcome().is_some_and(|o| o.is_decisive()))] pos: Evaluator,
#[filter(!#b.is_empty())] b: Range<Score>,
d: Depth,
p: Ply,
#[filter(#p > 0)] p: Ply,
) {
assert_eq!(
e.ab::<1>(&pos, b, d, p, &Control::Unlimited),
Ok(Pv::new(Score::lower().normalize(p), []))
Ok(Pv::new(Score::mated(p), []))
);
}

Expand All @@ -679,19 +680,6 @@ mod tests {
);
}

#[proptest]
fn search_can_be_limited_by_time(
mut e: Engine,
#[filter(#pos.outcome().is_none())] pos: Evaluator,
#[strategy(..10u8)] ms: u8,
) {
let timer = Instant::now();
let trigger = Trigger::armed();
let limits = Limits::Time(Duration::from_millis(ms.into()));
e.search(&pos, &limits, &trigger);
assert!(timer.elapsed() < Duration::from_secs(1));
}

#[proptest]
fn search_extends_time_to_find_some_pv(
mut e: Engine,
Expand Down
5 changes: 4 additions & 1 deletion lib/search/history.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use crate::chess::{Color, Move};
use crate::util::Assume;
use std::array;
use derive_more::Debug;
use std::sync::atomic::{AtomicI8, Ordering::Relaxed};
use std::{array, mem::size_of};

/// [Historical statistics] about a [`Move`].
///
/// [Historical statistics]: https://www.chessprogramming.org/History_Heuristic
#[derive(Debug)]
#[debug("History({})", size_of::<Self>())]
pub struct History([[[AtomicI8; 2]; 64]; 64]);

impl Default for History {
Expand Down Expand Up @@ -41,6 +43,7 @@ impl History {
#[cfg(test)]
mod tests {
use super::*;
use std::fmt::Debug;
use test_strategy::proptest;

#[proptest]
Expand Down
7 changes: 5 additions & 2 deletions lib/search/killers.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use crate::chess::{Color, Move};
use crate::search::Ply;
use crate::util::{Assume, Binary, Bits, Integer};
use std::array;
use derive_more::Debug;
use std::sync::atomic::{AtomicU32, Ordering::Relaxed};
use std::{array, mem::size_of};

/// A pair of [killer moves].
///
Expand All @@ -29,7 +30,7 @@ impl Killer {
}

impl Binary for Killer {
type Bits = Bits<u32, 32>;
type Bits = Bits<u32, { 2 * <Option<Move> as Binary>::Bits::BITS }>;

#[inline(always)]
fn encode(&self) -> Self::Bits {
Expand All @@ -49,6 +50,7 @@ impl Binary for Killer {
///
/// [killer moves]: https://www.chessprogramming.org/Killer_Move
#[derive(Debug)]
#[debug("Killers({})", size_of::<Self>())]
pub struct Killers([[AtomicU32; 2]; Ply::MAX as usize]);

impl Default for Killers {
Expand Down Expand Up @@ -82,6 +84,7 @@ mod tests {
use crate::util::Integer;
use proptest::sample::size_range;
use std::collections::HashSet;
use std::fmt::Debug;
use test_strategy::proptest;

#[proptest]
Expand Down
2 changes: 1 addition & 1 deletion lib/search/ply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ unsafe impl Integer for PlyRepr {
const MAX: Self::Repr = 95;

#[cfg(test)]
const MAX: Self::Repr = 3;
const MAX: Self::Repr = 7;
}

/// The number of half-moves played.
Expand Down
34 changes: 27 additions & 7 deletions lib/search/score.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::nnue::Value;
use crate::util::{Binary, Bits, Integer, Saturating};
use crate::{chess::Perspective, search::Ply};
use crate::{chess::Perspective, search::Ply, util::Assume};

#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[cfg_attr(test, derive(test_strategy::Arbitrary))]
Expand All @@ -16,14 +17,19 @@ unsafe impl Integer for ScoreRepr {
pub type Score = Saturating<ScoreRepr>;

impl Score {
const _CONDITION: () = const {
assert!(Value::MAX + Ply::MAX as i16 <= Self::MAX);
assert!(Value::MIN + Ply::MIN as i16 >= Self::MIN);
};

/// Returns number of plies to mate, if one is in the horizon.
///
/// Negative number of plies means the opponent is mating.
#[inline(always)]
pub fn mate(&self) -> Option<Ply> {
if *self <= Score::lower() - Ply::MIN {
if *self < Value::MIN {
Some((Score::lower() - *self).saturate())
} else if *self >= Score::upper() - Ply::MAX {
} else if *self > Value::MAX {
Some((Score::upper() - *self).saturate())
} else {
None
Expand All @@ -33,14 +39,28 @@ impl Score {
/// Normalizes mate scores relative to `ply`.
#[inline(always)]
pub fn normalize(&self, ply: Ply) -> Self {
if *self <= Score::lower() - Ply::MIN {
(*self + ply).min(Score::lower() - Ply::MIN)
} else if *self >= Score::upper() - Ply::MAX {
(*self - ply).max(Score::upper() - Ply::MAX)
if *self < Value::MIN {
Value::lower().convert::<Score>().assume().min(*self + ply)
} else if *self > Value::MAX {
Value::upper().convert::<Score>().assume().max(*self - ply)
} else {
*self
}
}

/// Mating score at `ply`
#[inline(always)]
pub fn mating(ply: Ply) -> Self {
(ply >= 0).assume();
Self::upper().normalize(ply)
}

/// Mated score at `ply`
#[inline(always)]
pub fn mated(ply: Ply) -> Self {
(ply >= 0).assume();
Self::lower().normalize(ply)
}
}

impl Perspective for Score {
Expand Down
Loading

0 comments on commit a092b37

Please sign in to comment.