summaryrefslogtreecommitdiff
path: root/src/chess.h
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-09-10 23:12:04 -0700
committerScott Gasch <[email protected]>2026-09-10 23:12:04 -0700
commit8a4985116ae6536db3f77b8ca8c81d7e8f73843d (patch)
tree7ac75d1ab593b2521045ce9c5ab66d8e1af9ccf3 /src/chess.h
parent9d1e32db698855b939c4bc5df6b6a82509dd381c (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-xsrc/chess.h51
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