summaryrefslogtreecommitdiff
path: root/src/searchsup.c
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-08-25 23:22:29 -0700
committerScott Gasch <[email protected]>2026-08-25 23:22:29 -0700
commit2245550e3c0344bcba093b24dc00f36b462fc940 (patch)
treeecef582eccd6588b0ca9d90fbaf76fe2fd1c2eef /src/searchsup.c
parentba803fe406ee2470aa461578f530d292380ec241 (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.c87
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