summaryrefslogtreecommitdiff
path: root/src/probe.c
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-09-18 09:43:17 -0700
committerScott Gasch <[email protected]>2026-09-18 09:43:17 -0700
commitf698c02de213d0ed8968fb7cca5f70df51fa0763 (patch)
tree080d6443fbe62d71de092f9acde61da387dff408 /src/probe.c
parent4a7ec9ac91939957fb3d3e5564fc24a385b422ff (diff)
Redesign lazy/super-lazy eval margins with position-specific percentile
tables, and fix four real search/recognizer bugs found while validating it Main theme: replace the flat, heavily-padded LAZY_EVAL_BASE_MARGIN / SUPER_LAZY_MARGIN_BY_ARMY constants with per-material-bucket percentile tables (king safety by attacker army, residual by own army, separate tables for the lazy vs super-lazy baselines) derived from a 30k-position calibration pool, targeting a measured ~1-1.5% exceeded-margin rate instead of an unmeasured "add lots of headroom" floor. QSearch's FUTILITY_BASE_MARGIN_LAZY/SUPERLAZY were re-derived to match the new, now-meaningful (p50-based) iPositional this produces. Self-play confirms the working tree beats both HEAD, a more liberal (~5% miss target) variant, and a more conservative (near-zero miss) variant of the same margin mechanism. Along the way, an initial self-play regression (this margin work bundled with several changes in one pass, one of which broke things) led to a day of ZFS-snapshot bisection and then targeted DEBUG-build fuzzing against a stratified 200k-position corpus (tests/cross_sectional_200k.ep_, new), which is what actually found and fixed four distinct, real, pre-existing engine bugs -- none of them related to the margin redesign itself: - searchsup.c (CommonSearchInit): three early-return paths (timer abort, draw, too-deep bailout) left a node's own PV[] row stale instead of clearing it, letting an ancestor's UpdatePV splice a leftover move from a completely unrelated node into the displayed PV -- root cause of a WalkPV/MakeMove assert ("moving a piece to capture, but nothing was on the target square"). - dynamic.c (RecordEnprisePieceAtPly): the two-slot en-prise dedup compared (square, piece) instead of square alone, so a same-square re-record with a different piece (legitimate -- a fail-high capture's victim is restored by UnmakeMove and can differ across calls at the same ply) shifted a duplicate square into both slots, tripping ValueOfMaterialInTroubleDespiteMove's two-distinct-squares assert. - recogn.c (_RecognizeKBKP): the wrong-colored-bishop corner-fortress draw shortcut fired even when the weak side has its own pawn(s), which can hand the strong side real zugzwang resources the fortress logic doesn't account for -- produced a false hard draw bound where EGTB shows a near-forced loss. - eval.c (_EvalPasserRacesAgainstLoneKings): a rook pawn's race was credited as winning even when the strong side's own king sits exactly on its own pawn's queening square (distance 0 specifically -- adjacent is fine, the square is empty) -- a classic stalemate resource the race heuristic can't see. Only reachable through _RecognizeKPK's pawn-hash-cache-hit path, which is why testrecogn.c's TestRecognExhaustiveKPK reported "0 wrong" the whole time; extended it to cover both color assignments and the full board (no mirror-shortcut) and documented the remaining cache-hit coverage gap for a future session to close. Also: match_play.py gains a local_ebf metric (geometric-mean of depth-normalized node-growth ratios between consecutive completed iterations) alongside the existing cumulative nodes**(1/depth) ebf, which turned out to be misleading when comparing engines with different lazy-exit aggressiveness -- and probe.c/hash.c pick up two small, independently-found correctness fixes (TB root-probe promotion piece missing its color bit; ASSERT(g_pHashTable) crashing on the legitimate "--hash none" config) noticed along the way. Co-Authored-By: Claude Sonnet 5 <[email protected]>
Diffstat (limited to 'src/probe.c')
-rw-r--r--src/probe.c26
1 files changed, 19 insertions, 7 deletions
diff --git a/src/probe.c b/src/probe.c
index 5054142..2bddf99 100644
--- a/src/probe.c
+++ b/src/probe.c
@@ -333,17 +333,29 @@ Return value:
static PIECE
-_TbPromotesToPiece(IN unsigned uPromotes)
+_TbPromotesToPiece(IN unsigned uPromotes, IN ULONG uSide)
/**
Routine description:
Map a TB_PROMOTES_* value from a Fathom root probe result to our
- own PIECE constant.
+ own PIECE constant. Must return a *colored* piece (BLACK_QUEEN|
+ uSide, matching san.c's "pPromoted = BLACK_QUEEN | pos->uToMove"
+ convention -- WHITE=1/BLACK=0 ORed onto the black-piece base value
+ selects the right color bit) since that's what move generation
+ actually stamps into mv.pPromoted. The previous version returned
+ the bare, colorless QUEEN/ROOK/BISHOP/KNIGHT constants (5/3/2/4 --
+ type-only values, never equal to any real generated move's
+ pPromoted), so ProbeEGTBRoot's move-list lookup below could never
+ match a promotion move for either side and always fell through to
+ ASSERT(FALSE) -- 100% reproducible whenever Fathom's suggested
+ root DTZ move happened to be a promotion, rare enough to need a
+ 200k-position stress run to surface.
Parameters:
unsigned uPromotes
+ ULONG uSide : WHITE or BLACK -- the side making the promoting move
Return value:
@@ -353,10 +365,10 @@ Return value:
{
switch (uPromotes)
{
- case TB_PROMOTES_QUEEN: return(QUEEN);
- case TB_PROMOTES_ROOK: return(ROOK);
- case TB_PROMOTES_BISHOP: return(BISHOP);
- case TB_PROMOTES_KNIGHT: return(KNIGHT);
+ case TB_PROMOTES_QUEEN: return(BLACK_QUEEN | uSide);
+ case TB_PROMOTES_ROOK: return(BLACK_ROOK | uSide);
+ case TB_PROMOTES_BISHOP: return(BLACK_BISHOP | uSide);
+ case TB_PROMOTES_KNIGHT: return(BLACK_KNIGHT | uSide);
default: return(0);
}
}
@@ -464,7 +476,7 @@ Return value:
cFrom = FROM64(TB_GET_FROM(uResult));
cTo = FROM64(TB_GET_TO(uResult));
- pPromotes = _TbPromotesToPiece(TB_GET_PROMOTES(uResult));
+ pPromotes = _TbPromotesToPiece(TB_GET_PROMOTES(uResult), pos->uToMove);
for (u = ctx->sMoveStack.uBegin[ctx->uPly];
u < ctx->sMoveStack.uEnd[ctx->uPly];