diff options
| author | Scott Gasch <[email protected]> | 2026-09-10 23:12:04 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-09-10 23:12:04 -0700 |
| commit | 8a4985116ae6536db3f77b8ca8c81d7e8f73843d (patch) | |
| tree | 7ac75d1ab593b2521045ce9c5ab66d8e1af9ccf3 /src/chess.h | |
| parent | 9d1e32db698855b939c4bc5df6b6a82509dd381c (diff) | |
Fix ONE_LEGAL_MOVE hash-move-shrink bug, tighten extension taper, add
a scoped check-evasion qsearch futility prune, and fix a pre-existing
PV-corruption crash.
- chess.h/generate.c: ONE_LEGAL_MOVE now reads a uTrueEscapeCount
snapshot taken before _ScoreAllEscapes excludes an already-tried
hash move, instead of the live post-exclusion MOVE_COUNT -- the old
version misreported "one legal reply to check" whenever the hash
move happened to be one of exactly two legal replies.
- root.c: g_uSoftExtendLimit/g_uHardExtendLimit taper onset moved from
2x/4x nominal search depth to 1.5x/3x (uniform 0.75x scaling of the
same band shape) -- a forcing chain of check/singular-reply
extensions could previously run fully unthrottled out to double the
nominal depth before any taper at all kicked in.
- search.c: QSearchFromCheckNoStandPat gains a scoped futility prune
for check-evasion replies, active only when this side already had a
stand-pat opportunity earlier in the line (fCouldStandPat). Only
applies to non-checking moves, uses the same calibrated
FUTILITY_BASE_MARGIN_BY_SOURCE table normal QSearch already trusts
(not a separate, unvalidated margin), computes its Eval() call
lazily (only once a non-checking candidate is actually reached, not
unconditionally per node), and never fires until at least one legal
move has already been searched (uLegalMoves > 0), so it can never
discard every reply to a check.
- search.c/searchsup.c: split CommonSearchInit's "too deep" bailout
into two separately-counted triggers (the rare MAX_PLY_PER_SEARCH-1
array-safety ceiling vs. the much more frequently-hit
g_uHardExtendLimit, now reachable sooner given the tightened taper
above) and surfaced both counts in root.c's per-search report.
- searchsup.c: UpdatePV no longer copies a child ply's stale PV tail
forward after writing a sentinel move (HASHMOVE/RECOGNMOVE/
DRAWMOVE/MATEMOVE) into the current ply's slot -- sPlyInfo[] is
reused across unrelated nodes at the same ply, so an earlier,
unrelated node's leftover sentinel in that slot could reproduce the
same sentinel value twice in a row and trip WalkPV's terminator
assert (util.c:1093), reproducing under MP via debug_smoke_test.sh.
Pre-existing, confirmed on unmodified HEAD; not introduced by any of
the above.
- eval_tune/match_play.py: EngineStats now tracks running average
search depth (already-parsed per-move data, just never surfaced)
and prints it alongside nps/ebf in the periodic status line, not
just the final summary. Added a ^C handler that dumps the same
CANDIDATE_SCORE/CANDIDATE_STATS/HEAD_STATS summary from whatever
games completed so far, then exits immediately rather than waiting
for the ThreadPoolExecutor's default wait=True teardown to let
in-flight games finish naturally.
Validated: precommit_check.sh (self-test + DEBUG smoke test) passing
throughout; self-play gauntlet (match_play.py --st 1) trending net
positive against HEAD through 1000 games.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TBtwX9aPrvwiM3gh4VnnZi
Diffstat (limited to 'src/chess.h')
| -rwxr-xr-x | src/chess.h | 51 |
1 files changed, 49 insertions, 2 deletions
diff --git a/src/chess.h b/src/chess.h index 78c0f94..430c7e5 100755 --- a/src/chess.h +++ b/src/chess.h @@ -788,7 +788,17 @@ typedef union _GENERATOR_FLAGS UCHAR uMoveCount; UCHAR uKingMoveCount; UCHAR uCheckingPieces; - UCHAR uUnused; + // Snapshot of MOVE_COUNT taken right after _GenerateEscapes + // finishes populating the move list for a check-evasion ply, + // before _ScoreAllEscapes swaps the hash move to the end and + // shrinks uEnd[ply] to keep the search from re-trying it. The + // live uEnd[ply]-uBegin[ply] count that trick leaves behind is + // "untried moves remaining," not "legal replies to check" -- + // using it for ONE_LEGAL_MOVE meant a position with two legal + // replies, one of which happened to be the hash move, read as + // having only one. This field is captured before that shrink + // so it always reflects the true legal-escape count. + UCHAR uTrueEscapeCount; }; ULONG uAllGenFlags; } @@ -868,7 +878,7 @@ MOVE_STACK; #define MOVE_COUNT(ctx, x) \ (((ctx)->sMoveStack.uEnd[(x)]) - (ctx)->sMoveStack.uBegin[(x)]) #define ONE_LEGAL_MOVE(ctx, x) \ - (MOVE_COUNT(ctx, x) == 1) + ((ctx)->sMoveStack.sGenFlags[(x)].uTrueEscapeCount == 1) #define NUM_KING_MOVES(ctx, x) \ ((ctx)->sMoveStack.sGenFlags[(x)].uKingMoveCount) #define NUM_CHECKING_PIECES(ctx, x) \ @@ -921,6 +931,15 @@ typedef struct _COUNTERS { UINT64 u64TotalNodeCount; UINT64 u64QNodeCount; + // CommonSearchInit's "too deep" bailout (searchsup.c), split by + // which of its two triggers fired -- the MAX_PLY_PER_SEARCH-1 + // array-safety ceiling (should be a rare, pathological corner + // case) vs. g_uHardExtendLimit (a real, tunable limit that a + // tighter extension taper makes reachable far more often, in + // exactly the long forced-check-chain positions the 2026-09-10/11 + // extension-taper/qsearch-prune work is about). + UINT64 u64TooDeepPlyLimit; + UINT64 u64TooDeepQsearchExtendLimit; UINT64 u64LeafCount; UINT64 u64TerminalPositionCount; UINT64 u64BetaCutoffs; @@ -955,6 +974,20 @@ typedef struct _COUNTERS // than an array so each term's call sites in eval.c can name // its own accumulator directly. // + // 2026-09-09: split out of the old "pre-lazy other" bucket -- + // u64CyclesEvalMaterialSetup (material balance + army-scaler + // calc) is paid unconditionally by *every* Eval() call + // (super-lazy, lazy, and full alike), while + // u64CyclesEvalPasserBadtradeBishop (passer races vs. a bare + // king, bad-trade detection, bishop pairs) is gated/rare on + // the first two and only unconditional (bishop pairs) inside + // calls that get this far. Bundling them together made the + // combined bucket's percentage read as "this work is + // expensive per call" when it was really "this bucket is + // paid by every call, not just full ones." + UINT64 u64CyclesEvalMaterialSetup; + UINT64 u64CyclesEvalPasserBadtradeBishop; + UINT64 u64CyclesEvalPawns; // // Split of the counter above by pawn-hash outcome (board_ @@ -1021,6 +1054,12 @@ typedef struct _COUNTERS // UINT64 u64CyclesEvalSuperLazy; + // 2026-09-09: EvalLookForDrawishSituations runs after + // u64CyclesEvalPostLazyMisc's window closes, only on the + // full-eval exit -- previously fell into "unaccounted" in + // root.c's eval breakdown report. + UINT64 u64CyclesEvalDrawish; + // Per-exit-path *total call cost*, i.e. the full entry-to-exit // elapsed time for a call that left via that path -- not to be // confused with the sub-timers above, which measure a single @@ -1060,6 +1099,13 @@ typedef struct _COUNTERS ULONG uEndgame; ULONG uBotvinnikMarkoff; ULONG uQExtend; + ULONG uQExtendChaseNodes; // qsearch nodes where the side to move + // is in check (every QSearch call that + // delegates to QSearchFromCheckNoStandPat) + // -- compare against + // sCounters.tree.u64QNodeCount for the + // fraction of all qsearch nodes spent + // on forced check evasions } extension; @@ -2450,6 +2496,7 @@ extern BITBOARD BBSQUARE[64]; extern BITBOARD BBROOK_PAWNS; extern BITBOARD BBPRECEEDING_RANKS[8][2]; extern BITBOARD BBADJACENT_FILES[8]; +extern BITBOARD BBADJACENT_FILES_INCLUSIVE[8]; extern BITBOARD BBADJACENT_RANKS[9]; // Ray-to-edge / knight-attack occupancy tables (data.c) -- built once |
