summaryrefslogtreecommitdiff
path: root/src/searchsup.c
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/searchsup.c
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/searchsup.c')
-rw-r--r--src/searchsup.c37
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;