diff options
| author | Scott Gasch <[email protected]> | 2026-08-25 23:22:29 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-08-25 23:22:29 -0700 |
| commit | 2245550e3c0344bcba093b24dc00f36b462fc940 (patch) | |
| tree | ecef582eccd6588b0ca9d90fbaf76fe2fd1c2eef /src/searchsup.c | |
| parent | ba803fe406ee2470aa461578f530d292380ec241 (diff) | |
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 <[email protected]>
Diffstat (limited to 'src/searchsup.c')
| -rw-r--r-- | src/searchsup.c | 87 |
1 files changed, 22 insertions, 65 deletions
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 <math.h> #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 |
