summaryrefslogtreecommitdiff
path: root/src/util.c
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-10-15 11:12:37 -0700
committerScott Gasch <[email protected]>2026-10-15 11:12:37 -0700
commitef2c2f667c4fc2c0303f56ea487183916f82fb67 (patch)
treed781a71ea958ca421521b21c93eb9c2ce59df65c /src/util.c
parentd9419c104e9fe193649931cee167b819044c6ca3 (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.c123
1 files changed, 123 insertions, 0 deletions
diff --git a/src/util.c b/src/util.c
index 60912de..23a1647 100644
--- a/src/util.c
+++ b/src/util.c
@@ -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
//