summaryrefslogtreecommitdiff
path: root/src/generate.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/generate.c')
-rwxr-xr-xsrc/generate.c113
1 files changed, 110 insertions, 3 deletions
diff --git a/src/generate.c b/src/generate.c
index 3e3c3a5..765c082 100755
--- a/src/generate.c
+++ b/src/generate.c
@@ -2433,6 +2433,56 @@ Routine description:
We have just generated all the moves, now score them. See comments
inline below about order.
+ Full move-ordering hierarchy for a normal (not-in-check) node, from
+ highest-scored to lowest -- "vNext", the dynamic-move-ordering
+ overhaul from the 2026-08-29 session (see CLAUDE.md's "Dynamic move
+ ordering experiments" section for the methodology behind it):
+
+ 1. Hash move -- already handled by Search(), never (re)generated
+ here; excluded from this list entirely.
+ 2. Killer that also threatens/delivers mate -- FIRST_KILLER (or
+ whichever killer slot matched) OR'd with SORT_THESE_FIRST.
+ 3. Winning/even captures & promotions -- SORT_THESE_FIRST, MVV/LVA
+ tiebreak; strictly-winning-by-raw-material trades skip SEE, an
+ even or ambiguous trade (or any promotion) gets the full
+ exchange walk.
+ 4. Killers, this ply's slot 0/1, then ply-2's slot 0/1 (Crafty-style
+ ordering, this ply's own pair before either ply-2-back one) --
+ FIRST_KILLER/SECOND_KILLER/THIRD_KILLER/FOURTH_KILLER.
+ 5. A countermove-table match (exact move, keyed by whatever the
+ opponent just played) *whose own accumulated history+
+ continuation-history evidence clears COUNTERMOVE_EVIDENCE_
+ THRESHOLD* -- COUNTERMOVE_TIER_BONUS, placed below both ply-2
+ killer slots (conservative default; never validated as
+ deserving to outrank them). An *unevidenced* match gets no
+ special treatment at all and falls through to tier 6 -- see
+ chess.h's COUNTERMOVE_TIER_BONUS/COUNTERMOVE_EVIDENCE_THRESHOLD
+ comments for why the promotion is evidence-gated rather than
+ automatic on a bare match.
+ 6. GOOD_MOVE boundary. Below this, "leftover" (see search.c's
+ fIsLeftoverMove) -- eligible for EFP pruning, but every leftover
+ is still fully ranked via SelectBestWithHistory (movesup.c); the
+ old NumLeftoverMovesToSelect depth-indexed selection budget was
+ retired once evidence showed the leftover pool has real,
+ findable signal a bailout was discarding. An ordinary leftover
+ quiet move's score is:
+ PSQT (g_iPSQT[piece][to], 0..1000)
+ + g_HistoryCounters[piece][to] (classic butterfly history,
+ (depth+1)^2 growth on fail-high, decays on a miss or global
+ aging pass)
+ + CONTINUATION_SCALE * g_ContinuationHistory[(prev move, this
+ move)] (same growth/decay math as history, additionally
+ keyed by the previous move -- self-calibrating, no magic
+ constant needed; CONTINUATION_SCALE=1 found flat/sufficient
+ across a 256x sweep)
+ + FLEE_BONUS if this move's origin square is currently en
+ prise (flat same-tier nudge; the old unconditional GOOD_
+ MOVE-tier promotion for these moves was retired after
+ evidence-calibration showed most such moves, unbacked by
+ real track record, perform identically to an ordinary
+ leftover -- see the RETIRED comment below).
+ 7. Losing captures -- negative, SEE-verified.
+
Parameters:
IN MOVE_STACK *pStack,
@@ -2454,7 +2504,9 @@ Return value:
ULONG uHashMoveLoc = (ULONG)-1;
ULONG uColor = pos->uToMove;
PRECOMP_KILLERS sKillers[4];
- COOR cEnprise = FindEnprisePiece(ctx, uColor);
+ ULONG uCounterMoveIdx = 0;
+ ULONG uCounterMoveContKey = 0;
+ FLAG fHaveCounterMove = FALSE;
//
// We have generated all moves here. We also know that we are not
@@ -2506,6 +2558,18 @@ Return value:
sKillers[3].uBonus |=
(SORT_THESE_FIRST * (IS_KILLERMATE_MOVE(sKillers[3].mv) != 0));
+ // EXPERIMENT: countermove-match tier. Precompute the previous
+ // move's countermove-table index once per node (mirrors killer
+ // precompute above) so the per-move loop below is just an
+ // IS_SAME_MOVE check, not a recompute.
+ fHaveCounterMove = FALSE;
+ if ((uPly > 0) && (0 != (ctx->sPlyInfo[uPly - 1]).mv.uMove))
+ {
+ uCounterMoveIdx = MOVE_TO_INDEX((ctx->sPlyInfo[uPly - 1]).mv);
+ uCounterMoveContKey = MOVE_TO_CONT_KEY((ctx->sPlyInfo[uPly - 1]).mv);
+ fHaveCounterMove = TRUE;
+ }
+
//
// Score moves
//
@@ -2587,8 +2651,51 @@ Return value:
s |= (IS_SAME_MOVE(sKillers[1].mv, mv) * sKillers[1].uBonus);
s |= (IS_SAME_MOVE(sKillers[2].mv, mv) * sKillers[2].uBonus);
s |= (IS_SAME_MOVE(sKillers[3].mv, mv) * sKillers[3].uBonus);
- s |= ((GOOD_MOVE + PIECE_VALUE(mv.pMoved) / 2) *
- (mv.cFrom == cEnprise));
+
+ // RETIRED: hung-piece-escape used to get an unconditional
+ // (GOOD_MOVE + PIECE_VALUE/2) promotion here for any move
+ // whose origin square was flagged en-prise, regardless of
+ // any track record. Evidence-calibration data showed the
+ // overwhelming majority of triggers (the "zero accumulated
+ // history/continuation evidence" bucket -- 250-1000x more
+ // populous than the equivalent countermove-match bucket)
+ // had a fail-high rate statistically identical to an
+ // ordinary, unprivileged leftover move (~0.22-0.25% vs.
+ // ~0.19-0.21% baseline). The escape motif alone, with no
+ // verification the destination is actually safe or the
+ // threat was real, isn't a trustworthy enough signal to
+ // justify an unconditional tier promotion -- unlike
+ // killers (self-evidencing by construction) or a
+ // sufficiently-evidenced countermove match. Retired
+ // rather than evidence-gated: a well-evidenced escape
+ // still gets ranked via the existing, already-validated
+ // history/continuation scoring below, same as any other
+ // leftover -- no separate mechanism needed for that case.
+
+ // Countermove-match tier, evidence-gated -- see chess.h's
+ // COUNTERMOVE_TIER_BONUS/COUNTERMOVE_EVIDENCE_THRESHOLD
+ // comment. Calibration data showed a raw match, with no
+ // track record, performs identically to an ordinary
+ // leftover (~0.6-0.85% FH) -- promoting on match alone
+ // repeats hung-piece-escape's mistake. Only promote once
+ // the same history+continuation evidence already used
+ // to score ordinary leftovers clears a threshold where
+ // it demonstrably beats killer-ply2's own average.
+ // Written into iValue itself (not a selection-time-only
+ // nudge) so a qualifying match actually escapes
+ // GOOD_MOVE/leftover classification.
+ if ((TRUE == fHaveCounterMove) &&
+ (IS_SAME_MOVE(mv, ctx->mvCounter[uCounterMoveIdx][0]) ||
+ IS_SAME_MOVE(mv, ctx->mvCounter[uCounterMoveIdx][1])))
+ {
+ ULONG uCMEvidence = g_HistoryCounters[mv.pMoved][mv.cTo] +
+ g_ContinuationHistory[(uCounterMoveContKey * CONT_KEY_RANGE) +
+ MOVE_TO_CONT_KEY(mv)];
+ if (uCMEvidence >= COUNTERMOVE_EVIDENCE_THRESHOLD)
+ {
+ s |= COUNTERMOVE_TIER_BONUS;
+ }
+ }
ASSERT(s >= 0);
}
pStack->mvf[u].iValue = s;