diff options
| author | Scott Gasch <[email protected]> | 2026-09-15 23:17:33 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-09-15 23:17:33 -0700 |
| commit | c0ac3d7845f60c7f6f2c3bfc306ceb0e4ac045ba (patch) | |
| tree | e2b7427e212bd5b557f15131b815517a86b20486 /src/searchsup.c | |
| parent | 0bb4ae3052cb8ff9ffcf951b3ef0e6f50a7efdfa (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.c | 169 |
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]; |
