From 2245550e3c0344bcba093b24dc00f36b462fc940 Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Tue, 25 Aug 2026 23:22:29 -0700 Subject: Tame check-extension compounding; verified neutral in self-play (0.4955) Fixes a real pathology: checks along a long unbroken forcing line could extend for free (net zero cost against the qsearch boundary), letting tree size blow up multiple orders of magnitude on positions like a near-all-check forced mate (ECM.089: 4.5B nodes / 31min at depth 12 before this change). - Main-search check extension: gate on SEE soundness (a losing sacrifice check gets a small consolation QUARTER_PLY instead of the full bonus a sound check gets), and flatten the sound-check bonus to a flat THREE_QUARTERS_PLY instead of a near-free ONE_PLY. - Lower the qsearch entry threshold to match (THREE_QUARTERS_PLY instead of ONE_PLY) so a lone check still buys one extra full-width ply as before; root.c trims QUARTER_PLY off the per-iteration depth budget so this doesn't add a blanket 1/4 ply to every search. - Qsearch's own check-widening (QSearchFromCheckNoStandPat) now relies on fCouldStandPat history plus a g_uIterateDepth/4 ceiling instead of an unconditional per-check grant, and QPLIES_OF_NON_CAPTURE_CHECKS moved from 1 to 2 to cover both "enter qsearch already in check" and "opponent's reply is the first real check" cases with one baseline window instead of ad hoc attacker-color tracking. Net effect on ECM.089 (sn 4M canary): ~7.5x fewer nodes and ~4x less time at depth 12 versus the original, unbounded behavior. Costs solve count on the full ECM suite (879 pos, sn 4M): 650 baseline -> 636 here -- expected and accepted, since ECM is unusually check-extension-heavy tactics and not representative of real games. Self-play vs baseline (1000 games, st 1) came back at B_SCORE=0.4955, ELO=-3.1+/-21.5 -- statistically neutral, confirming the fix costs nothing in real play. Co-Authored-By: Claude Sonnet 5 --- src/root.c | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) (limited to 'src/root.c') diff --git a/src/root.c b/src/root.c index 7c50a65..8d1adf8 100755 --- a/src/root.c +++ b/src/root.c @@ -456,17 +456,6 @@ Return value: ASSERT(d); Trace("Null move cutoff rate: %5.3f percent.\n", ((n / d) * 100.0)); - n = (double)(ctx->sCounters.tree.u64HistoryPruneReSearches); - d = (double)(ctx->sCounters.tree.u64HistoryPrunes) + 1; - Trace("History/LMR pruning: %"COMPILER_LONGLONG_UNSIGNED_FORMAT - " reduced, %5.3f percent needed a full-depth re-search.\n", - ctx->sCounters.tree.u64HistoryPrunes, ((n / d) * 100.0)); - n = (double)(ctx->sCounters.tree.u64FutilityPrunes); - d = (double)(ctx->sCounters.tree.u64FutilityCandidates) + 1; - Trace("Futility pruning: %"COMPILER_LONGLONG_UNSIGNED_FORMAT - " candidates, %5.3f percent actually pruned " - "(rest saved by fail-high%% / SEE gates).\n", - ctx->sCounters.tree.u64FutilityCandidates, ((n / d) * 100.0)); if (ctx->sCounters.egtb.uProbes > 0) { n = (double)(ctx->sCounters.egtb.uHits); @@ -1166,10 +1155,15 @@ Return value: if (iBeta > INFINITY) iBeta = +INFINITY; if (iAlpha < -INFINITY) iAlpha = -INFINITY; if (iAlpha >= iBeta) iAlpha = iBeta - 1; + // Was + HALF_PLY. Trimmed by QUARTER_PLY to compensate for + // search.c's qsearch-entry threshold moving from ONE_PLY down + // to THREE_QUARTERS_PLY -- that change alone would otherwise + // hand every line an extra ~1/4 ply of full-width search for + // free, not just check-heavy ones. iScore = RootSearch(ctx, iAlpha, iBeta, - uDepth * ONE_PLY + HALF_PLY); + uDepth * ONE_PLY + QUARTER_PLY); if (g_MoveTimer.bvFlags & TIMER_STOPPING) break; mv = ctx->mvRootMove; -- cgit v1.3