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/searchsup.c | 87 +++++++++++++++------------------------------------------ 1 file changed, 22 insertions(+), 65 deletions(-) (limited to 'src/searchsup.c') diff --git a/src/searchsup.c b/src/searchsup.c index 7af1a18..37eca66 100644 --- a/src/searchsup.c +++ b/src/searchsup.c @@ -18,7 +18,6 @@ Revision History: **/ -#include #include "chess.h" extern SCORE g_iRootScore[2]; @@ -225,8 +224,7 @@ Return value: ASSERT((uMoveNum > 0) || (uLegalMoves == 0)); if ((uRemainingDepth >= TWO_PLY) && (iBeta == (iAlpha + 1)) && - (FALSE == ctx->sPlyInfo[ctx->uPly - 1].fIsPVNode) && - (uLegalMoves > 3) && + (uLegalMoves > 5) && (0 == iExtend) && // (iRoughEval + ComputeMoveScore(ctx, mv, uMoveNum - 1) + 200 < iAlpha) && (!IS_ESCAPING_CHECK(mv)) && @@ -246,66 +244,10 @@ Return value: } -#define LMR_TABLE_MAX_DEPTH 32 -#define LMR_TABLE_MAX_MOVES 63 - -static INT g_iLMRTable[LMR_TABLE_MAX_DEPTH + 1][LMR_TABLE_MAX_MOVES + 1]; - -void -InitializeLMRTable(void) -{ - ULONG d, m; - double r; - - g_iLMRTable[0][0] = 0; - for (d = 0; d <= LMR_TABLE_MAX_DEPTH; d++) - { - for (m = 0; m <= LMR_TABLE_MAX_MOVES; m++) - { - if ((d < 1) || (m < 1)) - { - g_iLMRTable[d][m] = 0; - continue; - } - r = 0.7844 + (log((double)d) * log((double)m) / 2.4696); - if (r < 0.0) r = 0.0; - g_iLMRTable[d][m] = (INT)((r * ONE_PLY) + 0.5); - } - } -} - - -INT -ExtraReduction(IN ULONG uRemainingDepth, - IN ULONG uMoveNum, - IN ULONG uFailHighPct) -{ - INT iExtra; - ULONG d = uRemainingDepth / ONE_PLY; - ULONG m = uMoveNum; - - if (d > LMR_TABLE_MAX_DEPTH) d = LMR_TABLE_MAX_DEPTH; - if (m > LMR_TABLE_MAX_MOVES) m = LMR_TABLE_MAX_MOVES; - iExtra = g_iLMRTable[d][m]; - - if (uFailHighPct == 0) - { - iExtra += QUARTER_PLY; - } - - if ((INT)uRemainingDepth - ONE_PLY - iExtra < TWO_PLY) - { - iExtra = MAX((INT)uRemainingDepth - ONE_PLY - TWO_PLY, 0); - } - ASSERT(iExtra >= 0); - return(iExtra); -} - - SCORE ComputeMoveScore(IN SEARCHER_THREAD_CONTEXT *ctx, IN MOVE mv, - IN ULONG uMoveNum) + IN ULONG uMoveNum) { SCORE iMoveScore = (PIECE_VALUE(mv.pCaptured) + PIECE_VALUE(mv.pPromoted)); @@ -335,6 +277,7 @@ ComputeMoveExtension(IN SEARCHER_THREAD_CONTEXT *ctx, IN ULONG uMoveNum, IN SCORE iRoughEval, IN ULONG uDepth, + IN SCORE iCheckSee, IN OUT INT *piExtend) /** @@ -391,18 +334,32 @@ Return value: ASSERT(*piExtend >= 0); #endif - // Checking move extension. + // Checking move extension. Crafty-style soundness gate: a check + // that's really just a losing sacrifice (SEE < 0) still gets a small + // consolation extension rather than none at all -- SEE can't see + // multi-move combinations, so a "losing" sacrifice check is sometimes + // actually a winning sham sacrifice a few moves later, and giving it + // zero search priority risks missing exactly those. It just doesn't + // get to compound the way a materially sound check does. if (IS_CHECKING_MOVE(mv)) { ASSERT(InCheck(pos, pos->uToMove)); INC(ctx->sCounters.extension.uCheck); - *piExtend += ONE_PLY; - if (pos->uNonPawnMaterial[FLIP(pos->uToMove)] > - (VALUE_KING + VALUE_BISHOP)) + if (iCheckSee < 0) { + *piExtend += QUARTER_PLY; goto enforce_ceiling; } - *piExtend -= THREE_QUARTERS_PLY; + else + { + *piExtend += THREE_QUARTERS_PLY; + if (pos->uNonPawnMaterial[FLIP(pos->uToMove)] > + (VALUE_KING + VALUE_BISHOP)) + { + goto enforce_ceiling; + } + *piExtend -= HALF_PLY; + } } // Pawn push extension -- cgit v1.3