From 3b5bcbf016deb62b74f83f0e29c4158d79b33bc3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Oct 2026 10:17:05 +0000 Subject: [PATCH] feat(bench): Print how far each forced decision cleared its bound A `forced` row said which shortcut was taken, not how narrowly. The search is fail soft, so each decision's own search leaves a score on the far side of its bound, and its distance from that bound is the obvious thing to read root flips against: a scout that came back just under alpha, or a pass that just cleared beta, is the one a deeper search is most likely to overturn. Each row now carries `gap` after `alpha_gap`: for a trusted scout the alpha less its scout's score, for the null move the pass's score less beta, and for reverse futility the margin's floor less beta, each zero at the bound. A skip searched nothing and prints `-`. The search is unchanged and the bench is 11,088,536. The session test reads the new column and checks that it is never negative and is `-` only on a skip. Bench: 11088536 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01A57SbXV3vACDuNUX5zSKPQ --- arche-core/src/engine.rs | 33 ++++++++++++++++++++++++++++----- arche-core/src/forced.rs | 13 ++++++++++--- docs/INSTRUMENTS.md | 10 +++++++--- tests/forced_session.rs | 14 ++++++++++---- 4 files changed, 55 insertions(+), 15 deletions(-) diff --git a/arche-core/src/engine.rs b/arche-core/src/engine.rs index 0aab6085..37ce7005 100644 --- a/arche-core/src/engine.rs +++ b/arche-core/src/engine.rs @@ -1405,7 +1405,9 @@ impl AlphaBeta { /// A node decision just taken, the margin's or the pass's, offered to /// the forced decision instrument. True when it is the decision being - /// inverted, which the caller then does not take. + /// inverted, which the caller then does not take. `gap` is how far the + /// decision's bound cleared beta: the margin's floor or the pass's + /// score, less beta. // cold and out of line behind a bare is_some, for `sample`'s reason #[cold] #[inline(never)] @@ -1416,6 +1418,7 @@ impl AlphaBeta { eval: Score, alpha: Score, beta: Score, + gap: i32, ) -> bool { let address = forced::Address { kind, @@ -1436,6 +1439,7 @@ impl AlphaBeta { features: None, eval_beta: i32::from(eval) - i32::from(beta), alpha_gap: i32::from(alpha) - i32::from(eval), + gap: Some(gap), fen: board.to_fen(), }); false @@ -1495,6 +1499,7 @@ impl AlphaBeta { features: Some(features), eval_beta: eval_beta as i32, alpha_gap: alpha_gap as i32, + gap: None, fen: board.to_fen(), } }); @@ -1505,7 +1510,8 @@ impl AlphaBeta { /// forced decision instrument before it answers for its move. True when /// it is the decision being inverted. The board is the position the /// move left, which is the address, and the node's own evaluation is - /// read by stepping the move back, for kept events alone. + /// read by stepping the move back, for kept events alone. `scout` is + /// the scout's fail soft score, at or under alpha. #[cold] #[inline(never)] fn forced_scout( @@ -1514,6 +1520,7 @@ impl AlphaBeta { depth: u8, alpha: Score, beta: Score, + scout: Score, ) -> bool { let address = forced::Address { kind: forced::Kind::TrustedScout, @@ -1546,6 +1553,7 @@ impl AlphaBeta { features: Some(staged.features), eval_beta: eval_beta as i32, alpha_gap: alpha_gap as i32, + gap: Some(i32::from(alpha) - i32::from(scout)), fen, } }); @@ -2236,7 +2244,14 @@ impl AlphaBeta { } if floor >= beta { if self.forced.is_some() - && self.forced_node(forced::Kind::ReverseFutility, depth, eval, alpha, beta) + && self.forced_node( + forced::Kind::ReverseFutility, + depth, + eval, + alpha, + beta, + i32::from(floor) - i32::from(beta), + ) { margin_inverted = true; } else { @@ -2265,7 +2280,14 @@ impl AlphaBeta { let value = -result?; if value.score >= beta && !(self.forced.is_some() - && self.forced_node(forced::Kind::NullMove, depth, eval, alpha, beta)) + && self.forced_node( + forced::Kind::NullMove, + depth, + eval, + alpha, + beta, + i32::from(value.score) - i32::from(beta), + )) { // a mate found through a pass is not a mate, since the pass // is not a legal move, so the score is held under the window @@ -2378,7 +2400,8 @@ impl AlphaBeta { ); } if scout.score <= alpha - && !(self.forced.is_some() && self.forced_scout(staged, depth, alpha, beta)) + && !(self.forced.is_some() + && self.forced_scout(staged, depth, alpha, beta, scout.score)) { return Ok(scout); } diff --git a/arche-core/src/forced.rs b/arche-core/src/forced.rs index 75d1d78d..82606ff6 100644 --- a/arche-core/src/forced.rs +++ b/arche-core/src/forced.rs @@ -176,6 +176,12 @@ pub struct Event { pub(crate) features: Option, pub eval_beta: i32, pub alpha_gap: i32, + /// How far the decision's own bound cleared its window, in the + /// direction that took the decision: for a trusted scout, the alpha it + /// was read against less its fail soft score; for the null move, the + /// pass's score less beta; for reverse futility, the margin's floor + /// less beta. Zero at the bound. None on a skip, which searches nothing. + pub gap: Option, /// The deciding node's position. pub fen: String, } @@ -396,11 +402,11 @@ pub fn run( /// and what the default answered at each root. /// /// A row is `kind depth index searched generated history history_max -/// killer tt eval_beta alpha_gap answered visits root best_on +/// killer tt eval_beta alpha_gap gap answered visits root best_on /// best_forced score_on score_forced nodes_on nodes_forced fen`, /// whitespace separated with the deciding node's fen last. `root` is the /// root's place in the suite, which its line names. A move decision's own -/// columns print `-` on a node decision. +/// columns print `-` on a node decision, and `gap` prints `-` on a skip. impl fmt::Display for Report { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!( @@ -435,12 +441,13 @@ impl fmt::Display for Report { }; writeln!( f, - "{} {} {} {} {} {} {} {} {} {} {} {} {} {} {}", + "{} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {}", e.address.kind.word(), e.address.depth, features, e.eval_beta, e.alpha_gap, + e.gap.map_or_else(|| "-".to_string(), |gap| gap.to_string()), row.answered.map_or("-", Answered::word), row.visits, e.root, diff --git a/docs/INSTRUMENTS.md b/docs/INSTRUMENTS.md index e7f21f31..80073d60 100644 --- a/docs/INSTRUMENTS.md +++ b/docs/INSTRUMENTS.md @@ -432,9 +432,13 @@ quiets at once never meets the skips in it. The tests hold an armed search's move, score and node count to an unarmed one's. Each row is `kind depth index searched generated history history_max killer -tt eval_beta alpha_gap answered visits root best_on best_forced score_on -score_forced nodes_on nodes_forced fen`. The move columns are the ledger's, -read at the first visit, and print `-` on a node decision. Every skip at +tt eval_beta alpha_gap gap answered visits root best_on best_forced +score_on score_forced nodes_on nodes_forced fen`. The move columns are the +ledger's, read at the first visit, and print `-` on a node decision. `gap` +is how far the decision's own bound cleared its window, zero at the bound: +for a trusted scout the alpha less its fail soft score, for the null move +the pass's score less beta, and for reverse futility the margin's floor +less beta. A skip searched nothing and prints `-`. Every skip at depth four and up has an `eval_beta` at or under minus the skip's margin by construction. `answered` is, for a node decision, what answered the node once it was inverted. `visits` is how often the forced search met the address, and a diff --git a/tests/forced_session.rs b/tests/forced_session.rs index f05238c3..7e3dc9cd 100644 --- a/tests/forced_session.rs +++ b/tests/forced_session.rs @@ -24,18 +24,24 @@ fn the_forced_argument_prints_a_header_rows_and_a_line_a_kind_and_a_root() { assert!(printed.events() > 0, "header: {}", printed.header); for row in &printed.rows { let words: Vec<&str> = row.split(' ').collect(); - // twenty columns before a fen of six fields - assert_eq!(words.len(), 26, "row: {row}"); + // twenty one columns before a fen of six fields + assert_eq!(words.len(), 27, "row: {row}"); assert!( ["reverse_futility", "null_move", "skip", "trusted_scout"].contains(&words[0]), "row: {row}" ); // depth, visits, root, the two scores and the two node counts - for at in [1, 12, 13, 16, 17, 18, 19] { + for at in [1, 13, 14, 17, 18, 19, 20] { assert!(words[at].parse::().is_ok(), "field {at} of {row}"); } + // every decision but a skip cleared its bound, by zero or more + if words[0] == "skip" { + assert_eq!(words[11], "-", "row: {row}"); + } else { + assert!(words[11].parse::().is_ok(), "row: {row}"); + } // a kept decision the forced search never met is a failed row - assert!(words[12] != "0", "row: {row}"); + assert!(words[13] != "0", "row: {row}"); } let kinds = printed .summary