Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Do not reduce or extend at the root #651

Merged
merged 3 commits into from
Nov 20, 2024
Merged
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
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ readme = "README.md"
keywords = ["chess"]

[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage)'] }
unexpected_cfgs = { level = "warn", check-cfg = [
'cfg(coverage)',
'cfg(feature, values("used_linker"))',
] }

[dependencies]
arrayvec = { version = "0.7.6", default-features = false, features = ["std"] }
Expand Down
18 changes: 9 additions & 9 deletions lib/search/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,35 +32,35 @@ impl Driver {
///
/// The order in which elements are processed and on which thread is unspecified.
#[inline(always)]
pub fn drive<M, F>(&self, mut best: Pv, moves: &[M], f: F) -> Result<Pv, Interrupted>
pub fn drive<M, F>(&self, mut pv: Pv, moves: &[M], f: F) -> Result<Pv, Interrupted>
where
M: Sync,
F: Fn(&Pv, &M) -> Result<Pv, ControlFlow> + Sync,
{
match self {
Self::Sequential => {
for m in moves.iter().rev() {
best = match f(&best, m) {
Ok(pv) => pv.max(best),
pv = match f(&pv, m) {
Ok(partial) => partial.max(pv),
Err(ControlFlow::Break) => break,
Err(ControlFlow::Interrupt(e)) => return Err(e),
};
}

Ok(best)
Ok(pv)
}

Self::Parallel(e) => e.install(|| {
use Ordering::Relaxed;
let best = AtomicU64::new(IndexedPv(best, u32::MAX).encode().get());
let pv = AtomicU64::new(IndexedPv(pv, u32::MAX).encode().get());
let result = moves.par_iter().enumerate().rev().try_for_each(|(idx, m)| {
let pv = f(&IndexedPv::decode(Bits::new(best.load(Relaxed))), m)?;
best.fetch_max(IndexedPv(pv, idx.saturate()).encode().get(), Relaxed);
let partial = f(&IndexedPv::decode(Bits::new(pv.load(Relaxed))), m)?;
pv.fetch_max(IndexedPv(partial, idx.saturate()).encode().get(), Relaxed);
Ok(())
});

if matches!(result, Ok(()) | Err(ControlFlow::Break)) {
Ok(*IndexedPv::decode(Bits::new(best.into_inner())))
Ok(*IndexedPv::decode(Bits::new(pv.into_inner())))
} else {
Err(Interrupted)
}
Expand All @@ -81,7 +81,7 @@ impl Binary for IndexedPv {
let mut bits = Bits::default();
bits.push(self.score().encode());
bits.push(Bits::<u32, 32>::new(self.1));
bits.push(self.best().encode());
bits.push(self.deref().encode());
bits
}

Expand Down
143 changes: 55 additions & 88 deletions lib/search/engine.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::nnue::{Evaluator, Value};
use crate::search::*;
use crate::util::{Assume, Counter, Integer, Timer, Trigger};
use crate::{chess::Outcome, search::*};
use arrayvec::ArrayVec;
use std::{cell::RefCell, ops::Range, time::Duration};

Expand Down Expand Up @@ -45,7 +45,7 @@

/// Records a `[Transposition`].
fn record(&self, pos: &Evaluator, bounds: Range<Score>, depth: Depth, ply: Ply, pv: Pv) -> Pv {
let m = pv.best().assume();
let m = pv.assume();
if pv >= bounds.end && m.is_quiet() {
Self::KILLERS.with_borrow_mut(|ks| ks.insert(ply, pos.turn(), m));
}
Expand Down Expand Up @@ -108,17 +108,6 @@
Some(depth - r - (depth - ply) / 4)
}

/// A full alpha-beta search.
fn fw(
&self,
pos: &Evaluator,
depth: Depth,
ply: Ply,
ctrl: &Control,
) -> Result<Pv, Interrupted> {
self.pvs(pos, Score::lower()..Score::upper(), depth, ply, ctrl)
}

/// A [zero-window] alpha-beta search.
///
/// [zero-window]: https://www.chessprogramming.org/Null_Window
Expand Down Expand Up @@ -148,19 +137,22 @@
debug_assert!(!bounds.is_empty());

ctrl.interrupted()?;
let is_root = ply == 0;
let (alpha, beta) = match pos.outcome() {
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), None)),
Some(_) => return Ok(Pv::new(Score::lower().normalize(ply), None)),
};

let (alpha, beta) = self.mdp(ply, &bounds);
if alpha >= beta {
return Ok(Pv::new(alpha, None));
}

let transposition = match pos.outcome() {
Some(o) if o.is_draw() => return Ok(Pv::new(Score::new(0), None)),
Some(_) => return Ok(Pv::new(Score::lower().normalize(ply), None)),
None => self.tt.get(pos.zobrist()),
};

let transposition = self.tt.get(pos.zobrist());
let depth = match transposition {
_ if is_root => depth,

#[cfg(not(test))]
// Extensions are not exact.
Some(_) if pos.is_check() => depth + 1,
Expand All @@ -177,38 +169,38 @@
if !is_pv && t.depth() >= depth - ply {
let (lower, upper) = t.bounds().into_inner();
if lower >= upper || upper <= alpha || lower >= beta {
return Ok(Pv::new(t.score().normalize(ply), Some(t.best())));
return Ok(t.pv(ply));
}
}
}

let score = match transposition {
Some(t) => t.score().normalize(ply),
_ => pos.evaluate().saturate(),
let pv = match transposition {
None => Pv::new(pos.evaluate().saturate(), None),
Some(t) => t.pv(ply),
};

let quiesce = ply >= depth;
let alpha = match quiesce {
#[cfg(not(test))]
// The stand pat heuristic is not exact.
true => alpha.max(score),
true => pv.score().max(alpha),
_ => alpha,
};

if alpha >= beta || ply >= Ply::MAX {
return Ok(Pv::new(score, None));
} else if score - self.rfp(depth, ply) >= beta {
return Ok(pv);
} else if pv.score() - self.rfp(depth, ply) >= beta {
#[cfg(not(test))]
// The reverse futility pruning heuristic is not exact.
return Ok(Pv::new(score, None));
return Ok(pv);
} else if !is_pv && !pos.is_check() && pos.pieces(pos.turn()).len() > 1 {
if let Some(d) = self.nmp(score, beta, depth, ply) {
if let Some(d) = self.nmp(pv.score(), beta, depth, ply) {
let mut next = pos.clone();
next.pass();
if d <= ply || -self.nw(&next, -beta + 1, d, ply + 1, ctrl)? >= beta {
#[cfg(not(test))]
// The null move pruning heuristic is not exact.
return Ok(Pv::new(score, None));
return Ok(pv);
}
}
}
Expand All @@ -218,7 +210,7 @@
.filter(|ms| !quiesce || !ms.is_quiet())
.flatten()
.map(|m| {
if Some(m) == transposition.map(|t| t.best()) {
if Some(m) == *pv {
(m, Value::upper())
} else if Self::KILLERS.with_borrow(|ks| ks.contains(ply, pos.turn(), m)) {
(m, Value::new(25))
Expand All @@ -236,7 +228,7 @@
moves.sort_unstable_by_key(|(_, gain)| *gain);

let pv = match moves.pop() {
None => return Ok(Pv::new(score, None)),
None => return Ok(pv),
Some((m, _)) => {
let mut next = pos.clone();
next.play(m);
Expand Down Expand Up @@ -294,48 +286,46 @@
) -> Pv {
let ctrl = Control::Limited(Counter::new(nodes), Timer::new(time.end), interrupter);
let mut pv = Pv::new(Score::new(0), None);
let mut depth = Depth::new(0);

while depth < Depth::upper() {
use Control::*;
pv = self.fw(pos, depth, Ply::new(0), &Unlimited).assume();
depth = depth + 1;
if pv.best().is_some() {
break;
}
}

'id: for d in depth.get()..=limit.get() {
'id: for depth in Depth::iter() {
let mut overtime = time.end - time.start;
let mut depth = Depth::new(d);
let mut delta: i16 = 5;
let mut draft = depth;
let mut delta = 5i16;

let (mut lower, mut upper) = match d {
let (mut lower, mut upper) = match depth.get() {
..=4 => (Score::lower(), Score::upper()),
_ => (pv.score() - delta, pv.score() + delta),
};

let ctrl = if pv.is_none() {
&Control::Unlimited
} else if depth < limit {
&ctrl
} else {
break;
};

pv = 'aw: loop {
delta = delta.saturating_mul(2);
if ctrl.timer().remaining() < Some(overtime) {
break 'id;
}

let Ok(partial) = self.pvs(pos, lower..upper, depth, Ply::new(0), &ctrl) else {
let Ok(partial) = self.pvs(pos, lower..upper, draft, Ply::new(0), ctrl) else {
break 'id;
};

match partial.score() {
score if (-lower..Score::upper()).contains(&-score) => {
overtime /= 2;
depth = Depth::new(d);
draft = depth;

Check warning on line 321 in lib/search/engine.rs

View check run for this annotation

Codecov / codecov/patch

lib/search/engine.rs#L321

Added line #L321 was not covered by tests
upper = lower / 2 + upper / 2;
lower = score - delta;
}

score if (upper..Score::upper()).contains(&score) => {
overtime = time.end - time.start;
depth = depth - 1;
draft = draft - 1;

Check warning on line 328 in lib/search/engine.rs

View check run for this annotation

Codecov / codecov/patch

lib/search/engine.rs#L328

Added line #L328 was not covered by tests
upper = score + delta;
pv = partial;
}
Expand Down Expand Up @@ -408,10 +398,6 @@
alpha
}

fn negamax(pos: &Evaluator, depth: Depth, ply: Ply) -> Score {
alphabeta(pos, Score::lower()..Score::upper(), depth, ply)
}

#[proptest]
fn hash_is_an_upper_limit_for_table_size(o: Options) {
let e = Engine::with_options(&o);
Expand Down Expand Up @@ -475,17 +461,19 @@
) {
e.tt.set(pos.zobrist(), Transposition::exact(d, sc, m));

let ctrl = Control::Unlimited;
assert_eq!(e.nw(&pos, b, d, p, &ctrl), Ok(Pv::new(sc, Some(m))));
assert_eq!(
e.nw(&pos, b, d, p, &Control::Unlimited),
Ok(Pv::new(sc, Some(m)))
);
}

#[proptest]
fn nw_finds_score_bound(
e: Engine,
#[by_ref] e: Engine,
pos: Evaluator,
#[filter((Value::lower()..Value::upper()).contains(&#b))] b: Score,
d: Depth,
#[filter(#p >= 0)] p: Ply,
#[filter(#p > 0)] p: Ply,
) {
assert_eq!(
e.nw(&pos, b, d, p, &Control::Unlimited)? < b,
Expand Down Expand Up @@ -564,11 +552,11 @@

#[proptest]
fn pvs_returns_drawn_score_if_game_ends_in_a_draw(
e: Engine,
#[by_ref] e: Engine,
#[filter(#pos.outcome().is_some_and(|o| o.is_draw()))] pos: Evaluator,
#[filter(!#b.is_empty())] b: Range<Score>,
d: Depth,
p: Ply,
#[filter(#p > 0 || #pos.outcome() != Some(Outcome::DrawByThreefoldRepetition))] p: Ply,
) {
assert_eq!(
e.pvs(&pos, b, d, p, &Control::Unlimited),
Expand All @@ -590,39 +578,18 @@
);
}

#[proptest]
fn fw_finds_best_score(e: Engine, pos: Evaluator, d: Depth, #[filter(#p >= 0)] p: Ply) {
assert_eq!(e.fw(&pos, d, p, &Control::Unlimited)?, negamax(&pos, d, p));
}

#[proptest]
fn fw_does_not_depend_on_configuration(
x: Options,
y: Options,
pos: Evaluator,
d: Depth,
#[filter(#p >= 0)] p: Ply,
) {
let x = Engine::with_options(&x);
let y = Engine::with_options(&y);

let ctrl = Control::Unlimited;

assert_eq!(
x.fw(&pos, d, p, &ctrl)?.score(),
y.fw(&pos, d, p, &ctrl)?.score()
);
}

#[proptest]
fn search_finds_the_principal_variation(
mut e: Engine,
pos: Evaluator,
#[filter(#d > 1)] d: Depth,
) {
let interrupter = Trigger::armed();
let time = Duration::MAX..Duration::MAX;

assert_eq!(
e.search(&pos, &Limits::Depth(d), &Trigger::armed()).score(),
e.fw(&pos, d, Ply::new(0), &Control::Unlimited)?.score()
e.search(&pos, &Limits::Depth(d), &interrupter).score(),
e.aw(&pos, d, u64::MAX, &time, &interrupter).score()
);
}

Expand Down Expand Up @@ -653,7 +620,7 @@
#[filter(#pos.outcome().is_none())] pos: Evaluator,
) {
let limits = Duration::ZERO.into();
assert_ne!(e.search(&pos, &limits, &Trigger::armed()).best(), None);
assert_ne!(*e.search(&pos, &limits, &Trigger::armed()), None);
}

#[proptest]
Expand All @@ -662,7 +629,7 @@
#[filter(#pos.outcome().is_none())] pos: Evaluator,
) {
let limits = Depth::lower().into();
assert_ne!(e.search(&pos, &limits, &Trigger::armed()).best(), None);
assert_ne!(*e.search(&pos, &limits, &Trigger::armed()), None);
}

#[proptest]
Expand All @@ -671,6 +638,6 @@
#[filter(#pos.outcome().is_none())] pos: Evaluator,
) {
let limits = Limits::None;
assert_ne!(e.search(&pos, &limits, &Trigger::disarmed()).best(), None);
assert_ne!(*e.search(&pos, &limits, &Trigger::disarmed()), None);
}
}
Loading
Loading