summaryrefslogtreecommitdiff
path: root/src/dynamic.c
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-08-29 15:03:54 -0700
committerScott Gasch <[email protected]>2026-08-29 15:03:54 -0700
commit0b12137376929d96da81cd088d3d288cb1eec32d (patch)
tree28cbd63a95629719bd6145c9b1c6024b34854f4a /src/dynamic.c
parenta56b15320444fcfe3aabdd3768c81c733f3d776b (diff)
Dynamic move ordering overhaul: continuation-history, evidence-gated
countermove promotion, retired hung-piece-escape and NumLeftoverMovesToSelect. Full session was built on a "measure the pick, not the game" methodology: aggregate solve counts on curated suites are too noisy to tune move-ordering knobs against, so most decisions here came from per-move fail-high/alpha-raise rates at much larger sample sizes (leftover FH% instrumentation, a zero- selection-budget diagnostic that isolates a single best-of-remaining pick, and evidence-bucket calibration), not solve-count deltas alone. See CLAUDE.md's "Dynamic move ordering experiments" section for the reusable methodology and generate.c's _ScoreAllMoves comment for the resulting ordering hierarchy. Changes: - Added g_ContinuationHistory: same growth/decay math as the existing g_HistoryCounters butterfly table, additionally keyed by the previous move, so its magnitude is self-calibrated rather than a hand-picked constant. Flat, sufficient response across a 256x scale sweep. - Countermove-table matches now get a real GOOD_MOVE-tier promotion (previously the table was write-only, tracked for stats but never read for ordering), but only when the match's own accumulated history+continuation evidence clears COUNTERMOVE_EVIDENCE_THRESHOLD (10,000) -- a raw match with no track record was shown to perform identically to an ordinary leftover (~0.6-0.85% FH), so promoting on match alone would have repeated hung-piece-escape's mistake below. - Retired hung-piece-escape's unconditional GOOD_MOVE-tier promotion. Evidence-calibration showed the overwhelming majority of triggers (a zero-evidence population 250-1000x larger than countermove's) performed at the plain-leftover baseline -- the promotion was mostly free tier- escape treatment for moves that hadn't earned it. Replaced with FLEE_BONUS, a flat same-tier nudge inside SelectBestWithHistory (never escapes GOOD_MOVE/leftover classification, unlike a generation-time promotion) at the magnitude found to plateau a same-tier-nudge sweep. - Retired NumLeftoverMovesToSelect (the depth-indexed budget on how many leftover moves got a full selection scan before falling back to unsorted order). search.c's main move loop now always fully selects -- the leftover pool was shown to contain real, findable signal a bailout budget was discarding for a node-count savings that didn't hold up net- net once measured by solve counts and fail-high rates rather than raw node counts (noisy on small suites independent of this change). - Collapsed leftover-move instrumentation from sorted/raw pairs down to a single set now that "raw" (unsorted fallback) is structurally impossible; kept the countermove evidence-bucket calibration counters (ongoing check that COUNTERMOVE_EVIDENCE_THRESHOLD stays well- calibrated); removed the contested-node A/B harness and hung-piece evidence calibration now that the decisions they were built to inform are made. Net effect on the three curated suites (sd 10): solve counts wash (tied, +1, -1 across ringers/confident/hard), leftover fail-high rate improved consistently on all three (the intended, directly-measured target of this work). Not yet validated beyond sd 10 -- an sn-based run or eval_tune/match_play.py head-to-head gate is the natural next check before leaning on this as a proven strength gain rather than a directionally- sound, sd-10-clean change. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_014XePz6Sk4qQsTaP2jVJWJu
Diffstat (limited to 'src/dynamic.c')
-rwxr-xr-xsrc/dynamic.c118
1 files changed, 117 insertions, 1 deletions
diff --git a/src/dynamic.c b/src/dynamic.c
index 694fe50..5915721 100755
--- a/src/dynamic.c
+++ b/src/dynamic.c
@@ -43,6 +43,7 @@ Revision History:
extern double log(double);
ULONG g_HistoryCounters[14][128];
+ULONG g_ContinuationHistory[CONTINUATION_TABLE_SIZE];
SCORE g_iLMRQuietReduction[MAX_PLY_PER_SEARCH + 1][LMR_TABLE_MAX_MOVES + 1];
// Keyed by (cFrom, cTo, pMoved) -- the low 20 bits of mv.uMove -- rather
@@ -178,6 +179,7 @@ Return value:
ULONG u;
memset(g_HistoryCounters, 0, sizeof(g_HistoryCounters));
+ memset(g_ContinuationHistory, 0, sizeof(g_ContinuationHistory));
for (u = 0; u < FH_STATS_TABLE_SIZE; u++)
{
g_FailHighs[u].uWholeThing = 0x00010001;
@@ -780,6 +782,111 @@ Return value:
}
+static void
+_IncrementContinuationCounter(MOVE mvPrev, MOVE mv, ULONG uRemainingDepth)
+/**
+
+Routine description:
+
+ Continuation-history counterpart to _IncrementMoveHistoryCounter --
+ same (depth+1)^2 growth, same overflow-driven rescale, but keyed by
+ (mvPrev, mv) instead of mv alone. No-op if there was no previous
+ move (root).
+
+Parameters:
+
+ MOVE mvPrev,
+ MOVE mv,
+ ULONG uRemainingDepth
+
+Return value:
+
+ void
+
+**/
+{
+ ULONG uVal;
+ ULONG *pu;
+ ULONG x;
+
+ ASSERT(!IS_CAPTURE_OR_PROMOTION(mv));
+ if (0 == mvPrev.uMove)
+ {
+ return;
+ }
+ uVal = uRemainingDepth / ONE_PLY;
+ ASSERT(uVal >= 0);
+ ASSERT(uVal <= MAX_PLY_PER_SEARCH);
+ uVal += 1;
+ uVal *= uVal;
+ ASSERT(uVal > 0);
+
+ pu = &(g_ContinuationHistory[(MOVE_TO_CONT_KEY(mvPrev) * CONT_KEY_RANGE) +
+ MOVE_TO_CONT_KEY(mv)]);
+ LOCK_DYN;
+ *pu += uVal;
+ while (*pu & ~STRIP_OFF_FLAGS)
+ {
+ for (x = 0; x < CONTINUATION_TABLE_SIZE; x++)
+ {
+ g_ContinuationHistory[x] >>= 4;
+ }
+ }
+ UNLOCK_DYN;
+}
+
+
+static void
+_DecrementContinuationCounter(MOVE mvPrev, MOVE mv, ULONG uRemainingDepth)
+/**
+
+Routine description:
+
+ Continuation-history counterpart to _DecrementMoveHistoryCounter.
+ No-op if there was no previous move (root).
+
+Parameters:
+
+ MOVE mvPrev,
+ MOVE mv,
+ ULONG uRemainingDepth
+
+Return value:
+
+ void
+
+**/
+{
+ ULONG uVal;
+ ULONG *pu;
+
+ ASSERT(!IS_CAPTURE_OR_PROMOTION(mv));
+ if (0 == mvPrev.uMove)
+ {
+ return;
+ }
+ uVal = uRemainingDepth / ONE_PLY;
+ ASSERT(uVal >= 0);
+ ASSERT(uVal <= MAX_PLY_PER_SEARCH);
+ uVal /= 4;
+ uVal += 1;
+ ASSERT(uVal > 0);
+
+ pu = &(g_ContinuationHistory[(MOVE_TO_CONT_KEY(mvPrev) * CONT_KEY_RANGE) +
+ MOVE_TO_CONT_KEY(mv)]);
+ LOCK_DYN;
+ if (*pu >= uVal)
+ {
+ *pu -= uVal;
+ }
+ else
+ {
+ *pu = 0;
+ }
+ UNLOCK_DYN;
+}
+
+
void
UpdateDynamicMoveOrdering(IN SEARCHER_THREAD_CONTEXT *ctx,
IN ULONG uRemainingDepth,
@@ -811,6 +918,9 @@ Return value:
{
ULONG u;
MOVE mv;
+ MOVE mvPrev;
+
+ mvPrev.uMove = (ctx->uPly > 0) ? (ctx->sPlyInfo[ctx->uPly - 1]).mv.uMove : 0;
//
// Add this move to the killer list and increment its history count
@@ -820,6 +930,7 @@ Return value:
_NewKillerMove(ctx, mvBest, iScore);
_NewCounterMove(ctx, mvBest, uRemainingDepth);
_IncrementMoveHistoryCounter(mvBest, uRemainingDepth);
+ _IncrementContinuationCounter(mvPrev, mvBest, uRemainingDepth);
}
//
@@ -838,6 +949,7 @@ Return value:
if (!IS_CAPTURE_OR_PROMOTION(mv))
{
_DecrementMoveHistoryCounter(mv, uRemainingDepth);
+ _DecrementContinuationCounter(mvPrev, mv, uRemainingDepth);
}
}
}
@@ -923,7 +1035,7 @@ Return value:
**/
{
ULONG x, y;
-
+
LOCK_DYN;
for (x = 0; x <= WHITE_KING; x++)
{
@@ -932,5 +1044,9 @@ Return value:
g_HistoryCounters[x][y] >>= 1;
}
}
+ for (x = 0; x < CONTINUATION_TABLE_SIZE; x++)
+ {
+ g_ContinuationHistory[x] >>= 1;
+ }
UNLOCK_DYN;
}