From 917bf1260ae217bdcbdbc11c2c37cdb5d3a8eae9 Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Sat, 29 Aug 2026 00:30:53 -0700 Subject: 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 Claude-Session: https://claude.ai/code/session_01EortUUkDVpsfrbqshBJYJg --- src/dynamic.c | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) (limited to 'src/dynamic.c') diff --git a/src/dynamic.c b/src/dynamic.c index c6e86e3..694fe50 100755 --- a/src/dynamic.c +++ b/src/dynamic.c @@ -289,10 +289,21 @@ Return value: { ctx->mvKillerEscapes[uPly][1] = ctx->mvKillerEscapes[uPly][0]; ctx->mvKillerEscapes[uPly][0] = mv; - if (ctx->mvKillerEscapes[uPly][1].uMove == 0) - { - ctx->mvKillerEscapes[uPly][1] = ctx->mvNullmoveRefutations[uPly]; - } + // No mvNullmoveQuietRefutations backfill here, unlike the + // regular killer table below: TryNullmovePruning asserts + // !InCheck() before probing, and a null move doesn't move + // any piece, so the move that refutes it is necessarily + // played from a position where the opponent was *not* in + // check either -- it can never legitimately be a check + // evasion. Backfilling with it here wouldn't be dead code + // the way the old capture-into-quiet-slot bug was (that + // was mathematically guaranteed to never match); since + // IS_SAME_MOVE ignores bvFlags, an ordinary quiet move + // from some unrelated non-check position could coincide + // with a real escaping candidate by cFrom/cTo/pMoved/ + // pCaptured/pPromoted alone and pick up an undeserved + // SECOND_KILLER bonus in evasion ordering. Leave the slot + // empty instead of risking that. } ASSERT(!IS_SAME_MOVE(ctx->mvKillerEscapes[uPly][0], ctx->mvKillerEscapes[uPly][1])); @@ -304,9 +315,9 @@ Return value: { ctx->mvKiller[uPly][1] = ctx->mvKiller[uPly][0]; ctx->mvKiller[uPly][0] = mv; - if (ctx->mvKiller[uPly][1].uMove == 0) + if (ctx->mvKiller[uPly][1].uMove == 0) { - ctx->mvKiller[uPly][1] = ctx->mvNullmoveRefutations[uPly]; + ctx->mvKiller[uPly][1] = ctx->mvNullmoveQuietRefutations[uPly]; } } ASSERT(!IS_SAME_MOVE(ctx->mvKiller[uPly][0], ctx->mvKiller[uPly][1])); -- cgit v1.3