summaryrefslogtreecommitdiff
path: root/src/searchsup.c
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-09-15 23:17:33 -0700
committerScott Gasch <[email protected]>2026-09-15 23:17:33 -0700
commitc0ac3d7845f60c7f6f2c3bfc306ceb0e4ac045ba (patch)
treee2b7427e212bd5b557f15131b815517a86b20486 /src/searchsup.c
parent0bb4ae3052cb8ff9ffcf951b3ef0e6f50a7efdfa (diff)
Rework check/pawn-push/recapture extension gating on real SEE (GetMoveSee, generalized from GetCheckSee), restrict LMR to leftover moves only, and tighten recapture's window back to 1x after measuring a 6.8x node blowup from sharing the wider window.
Head-to-head match at 1170 games confirms parity with the prior head (score=0.510, LLR still between bounds) -- landing as a neutral-or-better baseline rather than a regression.
Diffstat (limited to 'src/searchsup.c')
-rw-r--r--src/searchsup.c169
1 files changed, 113 insertions, 56 deletions
diff --git a/src/searchsup.c b/src/searchsup.c
index 32e1be8..7ab8695 100644
--- a/src/searchsup.c
+++ b/src/searchsup.c
@@ -437,17 +437,25 @@ ComputeMoveScore(IN SEARCHER_THREAD_CONTEXT *ctx,
SCORE
-GetCheckSee(IN SEARCHER_THREAD_CONTEXT *ctx,
+GetMoveSee(IN SEARCHER_THREAD_CONTEXT *ctx,
IN MOVE mv,
IN ULONG uMoveNum)
/**
Routine description:
- SEE for a checking move, used by ComputeMoveExtension's callers
- (Search, HelpSearch, and eventually QSearch) to decide how much to
- extend/reduce a check. Must be computed on the PRE-move position --
- see.c's exchange walk needs the piece still sitting on cFrom.
+ SEE for a checking move or a non-pawn capture, used by
+ ComputeMoveExtension's callers (Search, HelpSearch, and eventually
+ QSearch) to decide how much to extend/reduce a check, and (as of
+ the recapture-extension soundness gate) to judge a recapture too.
+ One call site, one SEE per move regardless of how many of
+ ComputeMoveExtension's gates end up wanting it -- SEE is one of
+ the hottest routines in the tree, so callers compute this at most
+ once per move and pass the single result into ComputeMoveExtension
+ via iMoveSee, rather than each gate calling it independently. Must
+ be computed on the PRE-move position -- see.c's exchange walk needs
+ the piece still sitting on cFrom, and by the time
+ ComputeMoveExtension runs the move has already been played.
Move generation/scoring (_ScoreAllMoves, _ScoreAllEscapes in
generate.c) already runs a real SEE on this same (pos, mv) pair
@@ -460,7 +468,7 @@ Routine description:
Parameters:
SEARCHER_THREAD_CONTEXT *ctx : searcher thread context
- MOVE mv : the checking move
+ MOVE mv : the checking move or capture needing a soundness read
ULONG uMoveNum : mv's index in ctx->sMoveStack.mvf[], or (ULONG)-1
if mv did not come from the move stack (e.g. the hash move)
@@ -472,7 +480,7 @@ Return value:
{
BITV bvFlags;
- ASSERT(IS_CHECKING_MOVE(mv));
+ ASSERT(IS_CHECKING_MOVE(mv) || (mv.pCaptured != NULL));
if (uMoveNum != (ULONG)-1)
{
ASSERT(uMoveNum < MAX_MOVE_STACK);
@@ -493,7 +501,8 @@ void ComputeMoveExtension(IN SEARCHER_THREAD_CONTEXT *ctx,
IN ULONG uMoveNum,
IN SCORE iRoughEval,
IN ULONG uDepth,
- IN SCORE iCheckSee,
+ IN SCORE iMoveSee,
+ IN FLAG fIsLeftoverMove,
IN OUT INT *piExtend)
/**
@@ -561,36 +570,65 @@ Return value:
{
ASSERT(InCheck(pos, pos->uToMove));
INC(ctx->sCounters.extension.uCheck);
- if (iCheckSee < 0)
+ if (iMoveSee < 0)
{
*piExtend += QUARTER_PLY;
- goto enforce_ceiling;
+ INC(ctx->sCounters.extension.uCheckByQuarterPly[QUARTER_PLY / QUARTER_PLY]);
}
else
{
- *piExtend += THREE_QUARTERS_PLY;
if (pos->uNonPawnMaterial[FLIP(pos->uToMove)] >
(VALUE_KING + VALUE_BISHOP))
{
- goto enforce_ceiling;
+ *piExtend += THREE_QUARTERS_PLY;
+ INC(ctx->sCounters.extension.uCheckByQuarterPly[THREE_QUARTERS_PLY / QUARTER_PLY]);
+ }
+ else
+ {
+ *piExtend += QUARTER_PLY;
+ INC(ctx->sCounters.extension.uCheckByQuarterPly[QUARTER_PLY / QUARTER_PLY]);
}
- *piExtend -= HALF_PLY;
}
}
- // Pawn push extension
- if (IS_PAWN(mv.pMoved))
+ // Pawn push extension. RANK7/RANK2 already mean "one step from
+ // promotion", so the pawn is a passer by definition -- no separate
+ // passed-pawn test needed. Two cheap, eval-independent refinements
+ // (both read only incrementally-maintained POSITION bitboards, no
+ // Eval()/pawn-hash dependency, no sig-check needed):
+ //
+ // 1. Skip entirely if the promotion square itself is already
+ // occupied (friend or foe) -- a push that can just be captured
+ // or that's blocked from ever queening isn't worth the ply.
+ // 2. The size of the bonus depends on whether the pawn's own
+ // king/rook/queen actually defends it on cTo, not just
+ // whether the king happens to be nearby (a king 2 squares
+ // away that can't recapture doesn't help this pawn at all).
+ if (IS_PAWN(mv.pMoved) && (RANK7(mv.cTo) || RANK2(mv.cTo)))
{
- if (((GET_COLOR(mv.pMoved) == WHITE) && RANK7(mv.cTo)) ||
- ((GET_COLOR(mv.pMoved) == BLACK) && RANK2(mv.cTo)))
+ ULONG uPawnColor = GET_COLOR(mv.pMoved);
+ COOR cPromo = mv.cTo + ((uPawnColor == WHITE) ? 0x10 : -0x10);
+
+ if (!(pos->bbOccupied & COOR_TO_BB(cPromo)))
{
- *piExtend += THREE_QUARTERS_PLY;
- if (DISTANCE(pos->cNonPawns[GET_COLOR(mv.pMoved)][0], mv.cTo) < 3)
+ *piExtend += HALF_PLY;
+ if (!fIsLeftoverMove ||
+ ((g_KingAttacksBB[mv.cTo] &
+ COOR_TO_BB(pos->cNonPawns[uPawnColor][0])) ||
+ (_RookAttacksBB(mv.cTo, pos->bbOccupied) &
+ (pos->bbPieces[uPawnColor][ROOK] |
+ pos->bbPieces[uPawnColor][QUEEN])) ||
+ (_BishopAttacksBB(mv.cTo, pos->bbOccupied) &
+ pos->bbPieces[uPawnColor][QUEEN])))
{
*piExtend += QUARTER_PLY;
+ INC(ctx->sCounters.extension.uPawnPushByQuarterPly[THREE_QUARTERS_PLY / QUARTER_PLY]);
+ }
+ else
+ {
+ INC(ctx->sCounters.extension.uPawnPushByQuarterPly[HALF_PLY / QUARTER_PLY]);
}
INC(ctx->sCounters.extension.uPawnPush);
- goto enforce_ceiling;
}
}
@@ -600,28 +638,27 @@ Return value:
ASSERT(IS_ESCAPING_CHECK(mv));
iMoveScore = iRoughEval + ComputeMoveScore(ctx, mv, uMoveNum);
- // One legal move in reply to check... threshold doubled
- // 2026-09-06, same stopgap reasoning as search.c's qsearch
- // check-extension gate (board_representation/EVAL.md section
- // 9) -- was ">1", stale against CountKingSafetyDefects' old
- // CHECK_VECTOR-based scale.
+ // This is forced and cheap (1 move at the parent node).
if (ONE_LEGAL_MOVE(ctx, ctx->uPly - 1) &&
CountKingSafetyDefects(&ctx->sPosition, uColor) > 2)
{
- *piExtend += (QUARTER_PLY +
- HALF_PLY *
- (iMoveScore + VALUE_KNIGHT > iAlpha));
+ FLAG fBigBonus = (iMoveScore + VALUE_KNIGHT > iAlpha);
+ *piExtend += (QUARTER_PLY + HALF_PLY * fBigBonus);
INC(ctx->sCounters.extension.uOneLegalMove);
+ INC(ctx->sCounters.extension.uOneLegalMoveByQuarterPly[
+ fBigBonus ?
+ (THREE_QUARTERS_PLY / QUARTER_PLY) :
+ (QUARTER_PLY / QUARTER_PLY)]);
}
- // Singular response to check...
- if ((mv.pCaptured) &&
- (iRoughEval + 400 < iAlpha) &&
- (iMoveScore + 75 > iAlpha))
+ // One "good" way to response to check? Kinda dodgy but "works".
+ if (!fIsLeftoverMove &&
+ mv.pCaptured &&
+ (iRoughEval + 300 < iAlpha) &&
+ (iMoveScore + 50 > iAlpha))
{
*piExtend += ONE_PLY;
INC(ctx->sCounters.extension.uSingularReplyToCheck);
- goto enforce_ceiling;
}
}
@@ -643,30 +680,34 @@ Return value:
}
}
- // Recapture extension
+ // Recapture extension. Own tighter (1x) window restored --
+ // measured on ECM.721 (ringers): sharing the outer 2x window
+ // grew this position's recapture-fire count 6.8x vs HEAD
+ // (17,569,874 -> 34,337,674 total nodes), far more than the
+ // ~2x growth every other extension category saw from stacking
+ // alone -- all in refuted alternative lines, not the winning
+ // PV (which has zero recaptures on it). The 2x window did
+ // recover ECM.735/ECM.443 by making trade-dense *correct*
+ // lines eligible, but it's a net loss on positions where the
+ // expensive trades are all in the lines being ruled out
+ // instead. Back to the tighter window as the better tradeoff.
if ((iRoughEval >= (g_iRootScore[uColor] - _window[ctx->uPly])) &&
- (iRoughEval <= (g_iRootScore[uColor] + _window[ctx->uPly])))
+ (iRoughEval <= (g_iRootScore[uColor] + _window[ctx->uPly])) &&
+ (uDepth < THREE_PLY))
{
- if (uDepth <= THREE_PLY)
+ if (!fIsLeftoverMove &&
+ mv.pCaptured &&
+ !IS_PAWN(mv.pCaptured) &&
+ (iMoveSee >= 0) &&
+ ((PIECE_VALUE(mv.pCaptured) == PIECE_VALUE(mvLast.pCaptured)) ||
+ ((mvLast.pPromoted) && (mv.cTo == mvLast.cTo))))
{
- if ((mv.pCaptured) && !IS_PAWN(mv.pCaptured) &&
- ((PIECE_VALUE(mv.pCaptured) ==
- PIECE_VALUE(mvLast.pCaptured)) ||
- ((mvLast.pPromoted) && (mv.cTo == mvLast.cTo))))
- {
- iMoveScore = ComputeMoveScore(ctx, mv, uMoveNum);
- if (iMoveScore >= 0)
- {
- *piExtend += HALF_PLY;
- INC(ctx->sCounters.extension.uRecapture);
- }
- }
+ *piExtend += HALF_PLY;
+ INC(ctx->sCounters.extension.uRecapture);
}
}
}
- enforce_ceiling:
-
// Don't let extensions add up to > 1 ply
#ifdef DEBUG
iM = MIN(ONE_PLY, *piExtend);
@@ -675,13 +716,29 @@ Return value:
*piExtend = MINU(ONE_PLY, *piExtend);
ASSERT(iM == *piExtend);
- // Scale back extensions that are very far from the root:
+ // Scale back extensions that are very far from the root. Table
+ // built in root.c from g_uIterateDepth (see its own construction
+ // there for the exact arithmetic -- this is just a summary, keep
+ // the two in sync):
+ //
+ // ┌─────────────────────────┬───────────┬────────────────────────────────┐
+ // │ ply range (× iterate │ reduction │ net effect │
+ // │ depth) │ │ │
+ // ├─────────────────────────┼───────────┼────────────────────────────────┤
+ // │ u < 1.5× │ 0 │ full extension kept │
+ // ├─────────────────────────┼───────────┼────────────────────────────────┤
+ // │ 1.5× ≤ u < 1.875× │ -1/4 ply │ │
+ // ├─────────────────────────┼───────────┼────────────────────────────────┤
+ // │ 1.875× ≤ u < 2.25× │ -1/2 ply │ │
+ // ├─────────────────────────┼───────────┼────────────────────────────────┤
+ // │ 2.25× ≤ u < 3× │ -3/4 ply │ only a +1 ply extension │
+ // │ │ │ can survive here │
+ // ├─────────────────────────┼───────────┼────────────────────────────────┤
+ // │ u ≥ 3× (hard limit) │ -all ply │ no extensions beyond here │
+ // └─────────────────────────┴───────────┴────────────────────────────────┘
+ //
+ // NOTE: check extension in qsearch is handled differently, see search.c.
//
- // Iterate g_Soft g_Hard
- // | | | | | | | | |
- // 0 1 2 3/2 3 4 (x Iterate)
- // |-------------------|----|----|---------|----...
- // |full full full full|-1/4|-1/2|-3/4 -3/4|none...
ASSERT(ctx->uPly != 0);
ASSERT(ctx->uPly <= MAX_PLY_PER_SEARCH);
*piExtend -= g_uExtensionReduction[ctx->uPly-1];