diff options
| author | Scott Gasch <[email protected]> | 2026-08-25 14:03:46 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-08-25 14:03:46 -0700 |
| commit | ba803fe406ee2470aa461578f530d292380ec241 (patch) | |
| tree | c083e1f88e1e8c978084b34b9b315c3eec682e32 /src/search.c | |
| parent | 29d73f4dd59554a349aa8e86e5ea65f28c912ec9 (diff) | |
Add LMR with PV-adjacency guard (v7), verified against saved binary
Late Move Reductions using a depth x movecount table (Ethereal-style
formula), gated off PV nodes and the ply directly below a PV node
(PLY_INFO.fIsPVNode), with magnitude-aware re-search on fail-high.
Verified node-for-node identical to the previously tested-good v7
binary on a canary position (sd 10) after reconstructing from a ZFS
snapshot of search.c/root.c/split.c taken just before that binary was
built.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Diffstat (limited to 'src/search.c')
| -rwxr-xr-x | src/search.c | 66 |
1 files changed, 60 insertions, 6 deletions
diff --git a/src/search.c b/src/search.c index 29bbc66..ff7e661 100755 --- a/src/search.c +++ b/src/search.c @@ -153,6 +153,7 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx, ULONG uStage = TRY_HASH_MOVE; ULONG u; ULONG uFutilityMargin = 0; + SCORE iMoveSee = 0; #ifdef DEBUG ASSERT(IS_VALID_SCORE(iAlpha)); ASSERT(IS_VALID_SCORE(iBeta)); @@ -191,6 +192,7 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx, } DTEnterNode(ctx, uDepth, FALSE, iAlpha, iBeta); iInitialAlpha = iAlpha; + pi->fIsPVNode = (iBeta != iAlpha + 1); ASSERT((IS_CHECKING_MOVE(mvLast) && (TRUE == pi->fInCheck)) || (!IS_CHECKING_MOVE(mvLast) && (FALSE == pi->fInCheck))); @@ -428,8 +430,19 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx, // prune away moves we will also make sure there is // no per-move extension. ASSERT(!uFutilityMargin); - if ((iRoughEval + VALUE_ROOK <= iAlpha) && - (uDepth <= TWO_PLY) && + // Narrowed from "<= TWO_PLY" to "< TWO_PLY": ExtraReduction's + // own safety cap guarantees a reduced move's child lands + // with remaining depth >= TWO_PLY, which used to hand every + // heavily-reduced line straight into futility's activation + // floor at exactly that value -- double jeopardy on the + // same move by construction, not by chance. Excluding just + // that exact floor value (not the whole fractional-depth + // window below it) un-stacks that specific overlap. + if (FALSE && // EXPERIMENT: disabled for the unscaled-LMR-table + // test -- isolate the new reduction table's + // effect without also stacking futility pruning + (iRoughEval + VALUE_ROOK <= iAlpha) && + (uDepth < TWO_PLY) && (ctx->uPly >= 2) && (iOrigExtend == 0) && (ctx->sPlyInfo[ctx->uPly - 2].iExtensionAmount <= 0) && @@ -517,6 +530,16 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx, } #endif + // SEE must be computed on the PRE-move position (it internally + // simulates removing mv.pMoved from mv.cFrom and adding x-rays -- + // see see.c) -- so grab it now, before MakeMove mutates *pos in + // place, for the futility check below to use. Only bother when + // futility mode is actually active at this node. + if (uFutilityMargin) + { + iMoveSee = SEE(pos, mv); + } + if (TRUE == MakeMove(ctx, mv)) { uLegalMoves++; @@ -556,11 +579,22 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx, iExtend)) { ASSERT(iExtend == 0); - iExtend = -ONE_PLY; - pi->iExtensionAmount = -ONE_PLY; + iExtend = -(ONE_PLY + ExtraReduction(uDepth, (x - 1), + GetMoveFailHighPercentage(mv))); + pi->iExtensionAmount = iExtend; + INC(ctx->sCounters.tree.u64HistoryPrunes); } - // Maybe even "futility prune" this move away. + // Maybe even "futility prune" this move away. Require two + // independent corroborating signals on top of the static + // margin, not just the move's ordering score alone: this + // specific move must have rarely caused a beta cutoff + // historically (same threshold WeShouldDoHistoryPruning + // uses), and it must not show a tactical gain per SEE -- + // ComputeMoveScore alone is PSQT/history/killer-bonus for + // quiet moves, with no exchange evaluation at all, so it + // was pruning purely on a positional-ordering number. +#ifdef PERF_COUNTERS if ((x != 0) && (uLegalMoves > 1) && (uFutilityMargin) && @@ -568,6 +602,25 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx, (iExtend <= 0) && (!IS_ESCAPING_CHECK(mv))) { + INC(ctx->sCounters.tree.u64FutilityCandidates); + } +#endif + if ((x != 0) && + (uLegalMoves > 1) && + (uFutilityMargin) && + (ComputeMoveScore(ctx, mv, (x - 1)) < uFutilityMargin) && + (iExtend <= 0) && + (!IS_ESCAPING_CHECK(mv)) && + // Stricter than WeShouldDoHistoryPruning's <= 10: that one + // gates a reduction with a fail-high re-search safety net + // (search.c's own counters show ~2% wrong-guess rate is + // fine there); this gates an outright, unverified skip -- + // no re-search, no recovery if wrong -- so demand much + // stronger evidence the move is truly hopeless. + (GetMoveFailHighPercentage(mv) <= 3) && + (iMoveSee <= 0)) + { + INC(ctx->sCounters.tree.u64FutilityPrunes); // TODO: test this more carefully ASSERT(!IS_CHECKING_MOVE(mv)); UnmakeMove(ctx, mv); @@ -599,8 +652,9 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx, // Research deeper if history pruning failed if ((iExtend < 0) && (iScore >= iBeta)) { - uNextDepth += ONE_PLY; + uNextDepth -= iExtend; // undo the full reduction, whatever its magnitude pi->iExtensionAmount = 0; + INC(ctx->sCounters.tree.u64HistoryPruneReSearches); iScore = -Search(ctx, -iBeta, -iAlpha, uNextDepth); } UnmakeMove(ctx, mv); |
