diff options
| author | Scott Gasch <[email protected]> | 2026-10-15 11:12:37 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-10-15 11:12:37 -0700 |
| commit | ef2c2f667c4fc2c0303f56ea487183916f82fb67 (patch) | |
| tree | d781a71ea958ca421521b21c93eb9c2ce59df65c /src/util.c | |
| parent | d9419c104e9fe193649931cee167b819044c6ca3 (diff) | |
Fix several latent search/move-generation bugs found via NNUE annotation tooling
- probe.c: EGTB win/loss scores now render as +-MATE100 (literal +-NMATE)
instead of a misleading float, matching how a real forced mate prints.
- hash.c: fix a sign error in StoreExactScore's loss-side hash bound
(iValue +NMATE paired with HASH_FLAG_UPPER, should be -NMATE) -- dormant
until the probe.c fix made it reachable every EGTB loss.
- san.c: a checking castle move (O-O+/O-O-O+) printed as +-O/+-O-O --
the check-suffix pointer was captured before strcpy filled the
buffer, so '+' overwrote the leading 'O' instead of appending.
- search.c/searchsup.c: MATEMOVE/DRAWMOVE/RECOGNMOVE/EGTBMOVE now record
into the PV unconditionally instead of only when the ambient alpha-beta
window happens to strictly contain the score -- these are exact,
game-theoretic results, not heuristic bounds, so a PVS scout-window
tie (common once a line's value is already known) shouldn't suppress
them. This is why <#>/<=>/<REC>/<EGTB> almost never showed up in a
printed PV.
- util.c: WalkPV now reconstructs a still-missing final move/sentinel
directly from the board when the recorded PV runs out one ply short
of (or exactly at) a forced mate/stalemate/draw, instead of leaving
the printed PV silently truncated.
- gamelist.c/board.c/testsup.c: SetRootPosition now parses into a local
staging POSITION and validates IsBoardLegal + VerifyPositionConsistency
(both moved/enabled for all build profiles, not just TEST/EVAL_DUMP)
before committing to g_RootPosition. Previously a rejected setboard
(e.g. a hand-built FEN leaving the side not on move in check) still
left the partially-parsed, illegal position in g_RootPosition for the
next `go` to search -- GetRootPosition() hands out a stable pointer
straight to it with no defensive copy -- causing a crash instead of
the clean "Error (malformed FEN)" rejection already printed.
All verified via precommit_check.sh (TEST=1 self-test suite + DEBUG
smoke test) in addition to targeted manual repros for each bug.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Diffstat (limited to 'src/util.c')
| -rw-r--r-- | src/util.c | 123 |
1 files changed, 123 insertions, 0 deletions
@@ -1055,6 +1055,52 @@ FinishPVTailFromHash(SEARCHER_THREAD_CONTEXT *ctx, } +static FLAG +_HasAnyLegalMove(SEARCHER_THREAD_CONTEXT *ctx) +/** + +Routine description: + + True if the side to move at ctx->sPosition (ctx->uPly) has at + least one legal move. Used by WalkPV's mate/stalemate PV- + completion logic (below) -- GENERATE_ESCAPES/GENERATE_ALL_MOVES + only produce pseudo-legal moves (a generated move can still expose + the mover's own king), so "0 generated moves" is not the same + question as "0 legal moves"; this mirrors the same + generate-then-MakeMove-to-confirm pattern the real search's own + checkmate/stalemate detection (search.c) uses, rather than + trusting the generator's raw count. + +Parameters: + + SEARCHER_THREAD_CONTEXT *ctx + +Return value: + + FLAG + +**/ +{ + ULONG x; + FLAG fInCheckHere = InCheck(&ctx->sPosition, ctx->sPosition.uToMove); + + GenerateMoves(ctx, NULLMOVE, + (TRUE == fInCheckHere) ? GENERATE_ESCAPES : GENERATE_ALL_MOVES); + for (x = ctx->sMoveStack.uBegin[ctx->uPly]; + x < ctx->sMoveStack.uEnd[ctx->uPly]; + x++) + { + MOVE mv = ctx->sMoveStack.mvf[x].mv; + + if (TRUE == MakeMove(ctx, mv)) + { + UnmakeMove(ctx, mv); + return(TRUE); + } + } + return(FALSE); +} + char * WalkPV(SEARCHER_THREAD_CONTEXT *ctx) @@ -1143,6 +1189,83 @@ Return value: if (strlen(buf) > (SMALL_STRING_LEN_CHAR - 10)) break; } + // 2026-10-15: the recorded PV can legitimately run out one ply + // short of (or exactly at) a forced mate/stalemate -- see + // CLAUDE.md's MATEMOVE/DRAWMOVE/RECOGNMOVE/EGTBMOVE writeup. Those + // four sentinels now record unconditionally regardless of the + // ambient (alpha, beta) window, but the *move* immediately + // preceding such a leaf is written through the ordinary, strict + // "iScore > iAlpha" move-acceptance path (the hot per-node move + // loop) -- a forced mate's concluding move very often only ties + // (rather than strictly beats) a PVS scout/null window built + // around an already-known expected score, so that move's own + // PV[] slot is frequently never written either, and the loop + // above exits here with nothing further recorded. Reconstruct the + // missing tail directly from the board instead of touching that + // hot path: ctx->sPosition at this point already reflects every + // move printed so far (each one was MakeMove'd above), so (a) if + // it's already a proven draw, or already checkmate/stalemate + // outright (e.g. the position right after the last printed move, + // covers a mate delivered by the final printed move itself, like + // root.c's single-legal-move case), the sentinel alone completes + // it; (b) otherwise, if a forced mate ends exactly one ply past + // what's recorded, by definition of mate distance shrinking by + // exactly one ply along the true line, some legal move right here + // must deliver it -- find and print that move too. Runs once per + // call (PrintPV, on an improved root score), not in the search + // tree, so the extra legal-move verification here is cheap + // relative to the search itself. + if (0 == mv.uMove) + { + if (TRUE == IsDraw(ctx)) + { + strncat(buf, "<=>", ARRAY_LENGTH(buf) - strlen(buf) - 1); + } + else if (FALSE == _HasAnyLegalMove(ctx)) + { + strncat(buf, + (TRUE == InCheck(&ctx->sPosition, ctx->sPosition.uToMove)) ? + "<#>" : "<=>", + ARRAY_LENGTH(buf) - strlen(buf) - 1); + } + else + { + ULONG x; + FLAG fInCheckHere = InCheck(&ctx->sPosition, ctx->sPosition.uToMove); + + GenerateMoves(ctx, NULLMOVE, + (TRUE == fInCheckHere) ? + GENERATE_ESCAPES : GENERATE_ALL_MOVES); + for (x = ctx->sMoveStack.uBegin[ctx->uPly]; + x < ctx->sMoveStack.uEnd[ctx->uPly]; + x++) + { + MOVE mvCandidate = ctx->sMoveStack.mvf[x].mv; + + if (FALSE == MakeMove(ctx, mvCandidate)) + { + continue; + } + + if (FALSE == _HasAnyLegalMove(ctx)) + { + FLAG fOpponentInCheck = + InCheck(&ctx->sPosition, ctx->sPosition.uToMove); + + UnmakeMove(ctx, mvCandidate); + strncat(buf, MoveToSan(mvCandidate, &ctx->sPosition), + ARRAY_LENGTH(buf) - strlen(buf) - 1); + strncat(buf, " ", ARRAY_LENGTH(buf) - strlen(buf) - 1); + strncat(buf, + (TRUE == fOpponentInCheck) ? "<#>" : "<=>", + ARRAY_LENGTH(buf) - strlen(buf) - 1); + break; + } + UnmakeMove(ctx, mvCandidate); + } + } + } + // // Unmake the moves // |
