From eeec246756afb67293750aa00033ad79de6c6a1c Mon Sep 17 00:00:00 2001 From: Bruno Dutra Date: Mon, 30 Dec 2024 21:57:31 +0100 Subject: [PATCH] separate late move reductions from futility pruning --- lib/search/driver.rs | 20 ++++++++--------- lib/search/engine.rs | 52 ++++++++++++++++++++++++++++---------------- 2 files changed, 43 insertions(+), 29 deletions(-) diff --git a/lib/search/driver.rs b/lib/search/driver.rs index 198669df..eff39b0e 100644 --- a/lib/search/driver.rs +++ b/lib/search/driver.rs @@ -43,12 +43,12 @@ impl Driver { f: F, ) -> Result<(Move, Pv), Interrupted> where - F: Fn(Score, Move, Value) -> Result, ControlFlow> + Sync, + F: Fn(Score, Move, Value, usize) -> Result, ControlFlow> + Sync, { match self { Self::Sequential => { - for &(m, gain) in moves.iter().rev() { - match f(tail.score(), m, gain) { + for (idx, &(m, gain)) in moves.iter().rev().enumerate() { + match f(tail.score(), m, gain, idx) { Err(ControlFlow::Break) => break, Err(ControlFlow::Continue) => continue, Err(ControlFlow::Interrupt(e)) => return Err(e), @@ -68,19 +68,19 @@ impl Driver { let score = AtomicI16::new(tail.score().get()); let (head, tail, _) = moves .par_iter() - .enumerate() .rev() - .map( - |(idx, &(m, gain))| match f(Score::new(score.load(Relaxed)), m, gain) { + .enumerate() + .map(|(idx, &(m, gain))| { + match f(Score::new(score.load(Relaxed)), m, gain, idx) { Err(ControlFlow::Break) => None, Err(ControlFlow::Continue) => Some(Ok(None)), Err(ControlFlow::Interrupt(e)) => Some(Err(e)), Ok(partial) => { score.fetch_max(partial.score().get(), Relaxed); - Some(Ok(Some((m, partial, idx)))) + Some(Ok(Some((m, partial, usize::MAX - idx)))) } - }, - ) + } + }) .while_some() .chain([Ok(Some((head, tail, usize::MAX)))]) .try_reduce( @@ -116,7 +116,7 @@ mod tests { }); assert_eq!( - Driver::new(c).drive(h, t, &ms, |_, _, v| Ok(Pv::new(v.saturate(), []))), + Driver::new(c).drive(h, t, &ms, |_, _, v, _| Ok(Pv::new(v.saturate(), []))), Ok((head, tail)) ) } diff --git a/lib/search/engine.rs b/lib/search/engine.rs index 6489045a..688bb406 100644 --- a/lib/search/engine.rs +++ b/lib/search/engine.rs @@ -92,17 +92,6 @@ impl Engine { (bounds.start.max(lower), bounds.end.min(upper)) } - /// An implementation of [reverse futility pruning]. - /// - /// [reverse futility pruning]: https://www.chessprogramming.org/Reverse_Futility_Pruning - fn rfp(&self, surplus: Score, draft: Depth) -> Option { - match surplus.get() { - ..0 => None, - 0..680 => Some(draft - (surplus + 40) / 120), - 680.. => Some(draft - 6), - } - } - /// An implementation of [null move pruning]. /// /// [null move pruning]: https://www.chessprogramming.org/Null_Move_Pruning @@ -123,10 +112,21 @@ impl Engine { } } - /// An implementation of [late move pruning]. + /// An implementation of [reverse futility pruning]. /// - /// [late move pruning]: https://www.chessprogramming.org/Late_Move_Reductions - fn lmp(&self, deficit: Score, draft: Depth) -> Option { + /// [reverse futility pruning]: https://www.chessprogramming.org/Reverse_Futility_Pruning + fn rfp(&self, surplus: Score, draft: Depth) -> Option { + match surplus.get() { + ..0 => None, + 0..680 => Some(draft - (surplus + 40) / 120), + 680.. => Some(draft - 6), + } + } + + /// An implementation of [futility pruning]. + /// + /// [futility pruning]: https://www.chessprogramming.org/Futility_Pruning + fn fp(&self, deficit: Score, draft: Depth) -> Option { let r = match deficit.get() { ..15 => return None, 15..50 => 1, @@ -137,6 +137,13 @@ impl Engine { Some(draft - r - draft / 4) } + /// An implementation of [late move reductions]. + /// + /// [late move reductions]: https://www.chessprogramming.org/Late_Move_Reductions + fn lmr(&self, draft: Depth, idx: usize) -> i8 { + draft.get().max(1).ilog2() as i8 * idx.max(1).ilog2() as i8 / 3 + } + /// The [alpha-beta] search. /// /// [alpha-beta]: https://www.chessprogramming.org/Alpha-Beta @@ -332,7 +339,7 @@ impl Engine { return Ok(head >> tail); } - let (head, tail) = self.driver.drive(head, tail, &moves, |score, m, gain| { + let (head, tail) = self.driver.drive(head, tail, &moves, |score, m, gain, n| { let alpha = match score { s if s >= beta => return Err(ControlFlow::Break), s => s.max(alpha), @@ -343,17 +350,24 @@ impl Engine { self.tt.prefetch(next.zobrist()); if gain <= Value::lower() / 2 && !pos.is_check() && !next.is_check() { - if let Some(d) = self.lmp(alpha + next.evaluate(), draft) { + if let Some(d) = self.fp(alpha + next.evaluate(), draft) { if d <= 0 || -self.nw::<0>(&next, -alpha, d + ply, ply + 1, ctrl)? <= alpha { #[cfg(not(test))] - // The late move pruning heuristic is not exact. + // The futility pruning heuristic is not exact. return Err(ControlFlow::Continue); } } } - let partial = match -self.nw(&next, -alpha, depth, ply + 1, ctrl)? { - partial if partial <= alpha || partial >= beta => partial, + let lmr = match self.lmr(draft, n) { + #[cfg(not(test))] + // The late move reduction heuristic is not exact. + r @ 1.. if !is_pv => r, + _ => 0, + }; + + let partial = match -self.nw(&next, -alpha, depth - lmr, ply + 1, ctrl)? { + partial if partial <= alpha || (partial >= beta && lmr <= 0) => partial, _ => -self.ab(&next, -beta..-alpha, depth, ply + 1, ctrl)?, };