summaryrefslogtreecommitdiff
path: root/src/searchsup.c
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-08-29 00:30:53 -0700
committerScott Gasch <[email protected]>2026-08-29 00:30:53 -0700
commit917bf1260ae217bdcbdbc11c2c37cdb5d3a8eae9 (patch)
tree8013781f51e53eaf1fe3d4f45367e920d4c39786 /src/searchsup.c
parent366eebaf6b72fc6b56ea06a71f0276069a501dc7 (diff)
Switch to Crafty-style killer ordering; fix mvNullmoveRefutations type-mixing bug and add a quiet-refutation killer backfill.
Killer tiers now try both of this ply's own killers before either ply-2-back one, matching Crafty's ordering. Two earlier attempts at this same swap were reverted for regressing; this pass lands on top of NumLeftoverMovesToSelect (more SelectBestWithHistory budget to reach these lower-tier slots) and a real bug fix below, and beats interleaved order head-to-head on solves, node count, and first-move beta cutoff across the three curated suites. The bug: mvNullmoveRefutations's empty-killer-slot backfill could only ever contain a capturing move (TryNullmovePruning only wrote it inside the capture-refutation branch), but IS_SAME_MOVE's mask includes the pCaptured bits, so that backfilled value could never match a real quiet candidate -- the backfill was silently dead code. Fixed by recording genuinely quiet null-move refutations into a new, separate mvNullmoveQuietRefutations array (kept separate so it can't clobber the capture history mvNullmoveRefutations still needs for the Botvinnik-Markoff same-piece-two-squares extension check) and backfilling the regular killer table from that instead. The check-evasion killer table intentionally does *not* get this backfill: a null-move refutation can never legitimately be an escaping-check move (null moves can't deliver check), so backfilling there risks IS_SAME_MOVE cross-context false positives instead of the old guaranteed-inert no-op. Measured at sd10 across ecm_ringers/ecm_confident_quick/ecm_hard_quick against head_reference (commit d11e973): 115/191 solves (vs. 116 baseline), 924.36M total nodes (vs. 933.23M), first-move beta cutoff within 0.1-0.9 points of baseline on all three suites -- and clearly better than the same fix under interleaved order (113/191 solves, 963.10M nodes), which loses to head_reference on every metric. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01EortUUkDVpsfrbqshBJYJg
Diffstat (limited to 'src/searchsup.c')
-rw-r--r--src/searchsup.c34
1 files changed, 28 insertions, 6 deletions
diff --git a/src/searchsup.c b/src/searchsup.c
index e0d8251..54e1814 100644
--- a/src/searchsup.c
+++ b/src/searchsup.c
@@ -1167,7 +1167,7 @@ WeShouldTryNullmovePruning(SEARCHER_THREAD_CONTEXT *ctx,
(FALSE == pi->fInCheck) &&
(iBeta != +INFINITY) &&
(FALSE == pi->fPvNode)) // <--- TODO: test this one please...
- {
+ {
if (uNullDepth <= 6 * ONE_PLY)
{
u = uNullDepth / ONE_PLY;
@@ -1183,7 +1183,7 @@ WeShouldTryNullmovePruning(SEARCHER_THREAD_CONTEXT *ctx,
}
return FALSE;
}
-
+
FLAG
TryNullmovePruning(SEARCHER_THREAD_CONTEXT *ctx,
@@ -1203,10 +1203,10 @@ TryNullmovePruning(SEARCHER_THREAD_CONTEXT *ctx,
SCORE iVerifyScore;
MOVE mv;
MOVE mvRef;
-
+
ASSERT(IS_VALID_SCORE(iAlpha) && IS_VALID_SCORE(iBeta));
ASSERT(iAlpha < iBeta);
-
+
// TODO: more experiments with quick null
// Ok, do it.
@@ -1265,7 +1265,7 @@ TryNullmovePruning(SEARCHER_THREAD_CONTEXT *ctx,
ASSERT(SanityCheckMove(pos, mv));
ASSERT(GET_COLOR(mv.pMoved) == pos->uToMove);
ASSERT(ctx->uPly >= 2);
-
+
// If we make a nullmove that fails because we lose a
// piece, remember that the piece in question is in danger.
if ((mv.pCaptured) && !IS_PAWN(mv.pCaptured))
@@ -1291,7 +1291,29 @@ TryNullmovePruning(SEARCHER_THREAD_CONTEXT *ctx,
}
ctx->mvNullmoveRefutations[ctx->uPly] = mv;
}
-
+ else if (!IS_CAPTURE_OR_PROMOTION(mv))
+ {
+ // A quiet move refuting our nullmove is itself decent
+ // evidence that the move is strong here -- not enough to
+ // extend on (that's the capture-only Botvinnik-Markoff
+ // case above), but worth remembering as a fallback guess
+ // for _NewKillerMove's empty-slot backfill, which is only
+ // useful when this is actually a quiet move: a killer
+ // slot only ever gets matched against generated quiet
+ // candidates (IS_SAME_MOVE compares the pCaptured/
+ // pPromoted bits too), so storing a capture there (the
+ // old unconditional behavior) could never match anything
+ // and was silently dead.
+ //
+ // Deliberately a *separate* array from mvNullmoveRefutations,
+ // not a shared slot -- that one is read two plies later
+ // (ctx->uPly - 2, above) expecting a capture, to detect the
+ // same-piece-two-different-squares Botvinnik-Markoff pattern.
+ // Writing a quiet move into that same slot would silently
+ // clobber the capture history the extension depends on.
+ ctx->mvNullmoveQuietRefutations[ctx->uPly] = mv;
+ }
+
// This is an idea from Tord Romstad on ccc: if we are below a
// history reduced node and we try a nullmove and the nullmove
// fails low and the move that refuted the nullmove involves