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/split.c | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) (limited to 'src/split.c') diff --git a/src/split.c b/src/split.c index b30cdb6..703f6a8 100755 --- a/src/split.c +++ b/src/split.c @@ -1046,6 +1046,7 @@ Return value: INT iOrigExtend; INT iExtend; MOVE mv; + SCORE iCheckSee; #ifdef DEBUG POSITION board; @@ -1080,22 +1081,30 @@ Return value: ASSERT(iExtend >= -ONE_PLY); ASSERT(iExtend <= +ONE_PLY); + // SEE on the PRE-move position -- see search.c's identical comment. + iCheckSee = 0; + if (IS_CHECKING_MOVE(mv)) + { + iCheckSee = SEE(&ctx->sPosition, mv); + } + if (MakeMove(ctx, mv)) { - ASSERT((IS_CHECKING_MOVE(mv) && + ASSERT((IS_CHECKING_MOVE(mv) && InCheck(&ctx->sPosition, ctx->sPosition.uToMove)) || - (!IS_CHECKING_MOVE(mv) && + (!IS_CHECKING_MOVE(mv) && !InCheck(&ctx->sPosition, ctx->sPosition.uToMove))); iRoughEval = GetRoughEvalScore(ctx, iAlpha, iBeta, TRUE); - + // Compute extension - ComputeMoveExtension(ctx, + ComputeMoveExtension(ctx, iAlpha, iBeta, - (ctx->sMoveStack.uBegin[ctx->uPly - 1] + + (ctx->sMoveStack.uBegin[ctx->uPly - 1] + uMoveNum), iRoughEval, uDepth, + iCheckSee, &iExtend); // @@ -1125,12 +1134,8 @@ Return value: iExtend)) { ASSERT(iExtend == 0); - iExtend = -(ONE_PLY + ExtraReduction(uDepth, - (g_SplitInfo[u].uAlreadyDone + - uMoveNum + 1), - GetMoveFailHighPercentage(mv))); - ctx->sPlyInfo[ctx->uPly].iExtensionAmount = iExtend; - INC(ctx->sCounters.tree.u64HistoryPrunes); + iExtend = -ONE_PLY; + ctx->sPlyInfo[ctx->uPly].iExtensionAmount = -ONE_PLY; } // @@ -1151,9 +1156,8 @@ Return value: // if ((iExtend < 0) && (iScore >= iBeta)) { - uDepth -= iExtend; // undo the full reduction, whatever its magnitude + uDepth += ONE_PLY; ctx->sPlyInfo[ctx->uPly].iExtensionAmount = 0; - INC(ctx->sCounters.tree.u64HistoryPruneReSearches); iScore = -Search(ctx, -iBeta, -iAlpha, uDepth); } UnmakeMove(ctx, mv); -- cgit v1.3