From 538df17868afff51d49927fb5c8a91724752f26c Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Fri, 28 Aug 2026 08:29:09 -0700 Subject: Fix _ShouldWeConsiderThisMove's SEE-purity bug and ComputeMoveScore's killer-mate edge case; fix PV-display cycle hang. _ShouldWeConsiderThisMove (QSearch's move-consider gate) read the raw, move-ordering-biased mvf[].iValue directly instead of going through ComputeMoveScore, so it inherited the same +120-ish flat bias (plus small MVV-LVA nudges) on winning/even captures that ComputeMoveScore was already fixed to strip out. Fixed via the same MOVE_SCORE_ORDERING_BIAS subtraction, now factored into a shared chess.h macro. Restoring the old effective leniency required an explicit QSEARCH_CONSIDER_MARGIN (120, A/B'd against 0/60/120 on ecm_ringers/confident_quick/hard_quick) rather than assuming the bug's magnitude was itself a meaningful margin -- net effect vs the pre-fix baseline is -2 solves on hard_quick, accepted as the cost of correctness (see lmr_testing/RESULTS.md for the full sweep). ComputeMoveScore separately mishandled quiet killer-mate moves: they can reach SORT_THESE_FIRST via generate.c's killer-mate bonus (unrelated to the capture-bias path), so the bias-subtraction was wrongly applied to a move that never had that bias. Gated the subtraction on IS_CAPTURE_OR_PROMOTION(mv); quiet moves (including killer-mate ones) now correctly collapse to 0, per the function's contract of estimating a move's value on the 100=1-pawn axis. Measured as a no-op on all three suites -- rare in practice, but a real correctness fix. Left a comment documenting two candidate refinements for scoring quiet moves as non-uniform future work, deliberately not implemented (each needs its own isolated test). FinishPVTailFromHash (cosmetic PV-display hash-walk, used only for printing) had no cycle detection, so a drawish/repeating position could spin until the output buffer filled instead of terminating naturally. Added visited-position-signature tracking and a marker. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01YGSMkwjqiCk4XhbfN7ugD2 --- src/util.c | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) (limited to 'src/util.c') diff --git a/src/util.c b/src/util.c index b69a166..1bcfca9 100755 --- a/src/util.c +++ b/src/util.c @@ -991,6 +991,9 @@ FinishPVTailFromHash(SEARCHER_THREAD_CONTEXT *ctx, MOVE mv; ULONG uPly = ctx->uPly; ULONG uLen; + ULONG u, uVisited = 0; + UINT64 u64Sig; + UINT64 u64VisitedSigs[MAX_PLY_PER_SEARCH]; #ifdef DEBUG POSITION board; memcpy(&board, &(ctx->sPosition), sizeof(POSITION)); @@ -999,6 +1002,29 @@ FinishPVTailFromHash(SEARCHER_THREAD_CONTEXT *ctx, if (NULL == g_pHashTable) return; do { + // This tail is display-only (cosmetic) -- it walks hash-table + // best-moves one at a time with no relation to the real, + // backed-up search score printed alongside it (that score + // covers only the PV up to the marker). Unlike the real + // search, there's no natural depth bound here, so a drawish + // position whose hash entries chain back into each other (very + // common in king-shuffle endgames) can loop forever. Break on + // the first repeated position instead of relying on the output + // buffer filling up, which just produces a wall of repeated + // moves that looks like -- but has no actual bearing on -- + // the printed score. + u64Sig = (ctx->sPosition.u64NonPawnSig ^ ctx->sPosition.u64PawnSig); + for (u = 0; u < uVisited; u++) + { + if (u64VisitedSigs[u] == u64Sig) + { + if (uLenRemain > 6) strcat(buf, ""); + goto unmake; + } + } + ASSERT(uVisited < MAX_PLY_PER_SEARCH); + u64VisitedSigs[uVisited++] = u64Sig; + mv = GetPonderMove(&ctx->sPosition); if (mv.uMove == 0) break; PV[ctx->uPly] = mv; @@ -1014,6 +1040,8 @@ FinishPVTailFromHash(SEARCHER_THREAD_CONTEXT *ctx, } while(1); + unmake: + // // Unmake the moves // -- cgit v1.3