From 1a0712fee7e3fa9bcf124a942d8ef15efe4578e7 Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Fri, 28 Aug 2026 11:10:14 -0700 Subject: Fix IID's killer-blind gate and its ordering-flag/history contamination of iValue; harden against a latent ComputeMoveExtension bug. DO_IID's "is the top move crappy" gate compared raw iValue against SORT_THESE_FIRST only, missing that ordinary killer moves (FIRST_KILLER through FOURTH_KILLER) sit below that threshold too -- a killer that already proved itself elsewhere in the tree was being treated as "crappy" and triggering an unnecessary shallow rescore. Fixed by also excluding killer-flagged moves from the gate. RescoreMovesViaSearch corrupted the winning move's real search score by OR-ing in SORT_THESE_FIRST to force it to sort first (`mvf[uBest].iValue |= SORT_THESE_FIRST`) -- unnecessary (SelectBest{With,No}History already find the true max by plain magnitude comparison, no flag needed) and actively dangerous: a later ComputeMoveScore() call on that same move, if it's a capture, would see the corrupted value, mistake it for generate.c's biased-capture-ordering format, and subtract the wrong bias entirely. Removed the OR; added an explicit PLY_INFO.fMovesRescoredByIID flag so ComputeMoveScore and the main search-loop's move-selection call can both recognize "this ply's iValue holds a real eval-axis score" without relying on bit-pattern inference. Consequently, ComputeMoveScore now trusts an IID-rescored move's score outright instead of running it through the capture-bias-subtraction or quiet-move-collapse-to-0 logic (both of which assume generate.c's ordering encoding, which a rescored ply no longer holds). Separately hardened it against quiet killer-mate moves, which can reach SORT_THESE_FIRST via a different, capture-unrelated path and were incorrectly getting the capture bias subtracted from them; they now correctly collapse to 0 like other quiet moves. Two follow-on ideas -- blending history into the real IID score (scaled or capped) and a exact-tie-only history tiebreak -- were implemented, measured, and rejected: blending invents a new, leak-prone move-scoring axis for no measured benefit, and the tiebreak-only compromise still cost solves relative to just trusting the real score outright. Main search's move-selection call now branches once per selection (not once per candidate move) between SelectBestNoHistory (IID-rescored plies) and SelectBestWithHistory (everyone else), keeping the overwhelmingly common non-rescored path at zero added cost. Net measured effect (ecm_ringers.ep_/ecm_confident_quick.ep_/ ecm_hard_quick.ep_, sn=5M): 10/90/9, down from a pre-existing 11/88/10 on ringers and hard specifically -- see lmr_testing/RESULTS.md for the full sweep of rejected alternatives and why the regression was accepted as the cost of removing a latent, leak-prone bug class rather than chasing the exact prior numbers. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01YGSMkwjqiCk4XhbfN7ugD2 --- src/movesup.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'src/movesup.c') diff --git a/src/movesup.c b/src/movesup.c index 8aa8ffa..dec090c 100755 --- a/src/movesup.c +++ b/src/movesup.c @@ -988,7 +988,7 @@ Return value: } -void FASTCALL +void FASTCALL SelectBestWithHistory(SEARCHER_THREAD_CONTEXT *ctx, ULONG u) /** @@ -1017,11 +1017,11 @@ Return value: SCORE iVal; MOVE mv; MOVE_STACK_MOVE_VALUE_FLAGS mvfTemp; - + ASSERT(ctx->sMoveStack.uBegin[ctx->uPly] <= uEnd); ASSERT(u >= ctx->sMoveStack.uBegin[ctx->uPly]); ASSERT(u < uEnd); - + // // Linear search from u..ctx->sMoveStack.uEnd[ctx->uPly] for the // move with the best value. @@ -1033,7 +1033,7 @@ Return value: iBestVal += g_HistoryCounters[mv.pMoved][mv.cTo]; } uLoc = u; - + for (v = u + 1; v < uEnd; v++) { iVal = ctx->sMoveStack.mvf[v].iValue; @@ -1058,7 +1058,7 @@ Return value: } -void FASTCALL +void FASTCALL SelectBestNoHistory(SEARCHER_THREAD_CONTEXT *ctx, ULONG u) /** -- cgit v1.3