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/searchsup.c | |
| 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/searchsup.c')
| -rw-r--r-- | src/searchsup.c | 37 |
1 files changed, 35 insertions, 2 deletions
diff --git a/src/searchsup.c b/src/searchsup.c index 89ad729..b4b9d1f 100644 --- a/src/searchsup.c +++ b/src/searchsup.c @@ -50,6 +50,32 @@ Return value: ASSERT(u < MAX_PLY_PER_SEARCH); this->PV[u] = mv; u++; + + // HASHMOVE/RECOGNMOVE/DRAWMOVE/MATEMOVE are sentinels, not real + // moves -- WalkPV (util.c) treats any of them as a hard PV + // truncation point and asserts the slot right after one is + // always a zero terminator. Copying the child ply's PV tail + // forward below would instead leave whatever move happens to + // already be sitting in that reused sPlyInfo slot (sPlyInfo[] is + // shared across every node that ever occupies this ply, never + // zeroed between unrelated nodes) -- if some earlier, unrelated + // node at this same ply also once wrote the same sentinel there + // and nothing since overwrote it, that stale leftover reproduces + // the sentinel value a second time in a row and trips the assert + // (util.c:1093, found via debug_smoke_test.sh under MP). Hard- + // terminate immediately instead of inheriting the child's tail. + if ((mv.uMove == HASHMOVE.uMove) || + (mv.uMove == RECOGNMOVE.uMove) || + (mv.uMove == DRAWMOVE.uMove) || + (mv.uMove == MATEMOVE.uMove)) + { + if (u < MAX_PLY_PER_SEARCH) + { + this->PV[u].uMove = 0; + } + return; + } + while(u < MAX_PLY_PER_SEARCH) { this->PV[u] = next->PV[u]; @@ -987,9 +1013,16 @@ Return value: // the past they will call MakeMove this must cut off one ply // before the hard limit. // - if ((ctx->uPly >= (MAX_PLY_PER_SEARCH - 1)) || - (ctx->sSearchFlags.uQsearchDepth >= g_uHardExtendLimit)) + if (ctx->uPly >= (MAX_PLY_PER_SEARCH - 1)) + { + INC(ctx->sCounters.tree.u64TooDeepPlyLimit); + *piScore = *piBeta; + DTTRACE("<toodeep/>"); + goto end; + } + if (ctx->sSearchFlags.uQsearchDepth >= g_uHardExtendLimit) { + INC(ctx->sCounters.tree.u64TooDeepQsearchExtendLimit); *piScore = *piBeta; DTTRACE("<toodeep/>"); goto end; |
