diff options
| author | Scott Gasch <[email protected]> | 2026-09-18 09:43:17 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-09-18 09:43:17 -0700 |
| commit | f698c02de213d0ed8968fb7cca5f70df51fa0763 (patch) | |
| tree | 080d6443fbe62d71de092f9acde61da387dff408 /src/command.c | |
| parent | 4a7ec9ac91939957fb3d3e5564fc24a385b422ff (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/command.c')
| -rw-r--r-- | src/command.c | 201 |
1 files changed, 198 insertions, 3 deletions
diff --git a/src/command.c b/src/command.c index 5087aee..b201c31 100644 --- a/src/command.c +++ b/src/command.c @@ -910,12 +910,26 @@ Return value: if (argc >= 2 && !STRCMPI(argv[1], "marginsafety")) { #ifdef CALIBRATE_MARGIN_SAFETY - DumpMarginSafetyCalibration(); + // Optional 3rd argument: output path for the persisted raw + // swing histogram (default /var/tmp/typhoon/marginsafety_raw_ + // histograms.txt) -- needed so N shards of a sharded/parallel + // calibration run each write their own file instead of + // clobbering one shared path. + DumpMarginSafetyCalibration((argc >= 3) ? argv[2] : NULL); #else Trace("This binary was not built with CALIBRATE_MARGIN_SAFETY.\n"); #endif return; } + if (argc >= 2 && !STRCMPI(argv[1], "residual")) + { +#ifdef DUMP_RESIDUAL_SAMPLES + DumpResidualAndKingSafetyHistograms(); +#else + Trace("This binary was not built with DUMP_RESIDUAL_SAMPLES.\n"); +#endif + return; + } if (argc >= 2 && !STRCMPI(argv[1], "qsearchfutility")) { #ifdef CALIBRATE_QSEARCH_FUTILITY @@ -925,11 +939,20 @@ Return value: #endif return; } + if (argc >= 2 && !STRCMPI(argv[1], "exitrate")) + { + // Always-on, no build flag needed -- see eval.c's + // g_u64SuperLazyEvalsByBucket/g_u64LazyEvalsByBucket/ + // g_u64FullEvalsByBucket. + DumpEvalExitRateByBucket(); + return; + } #ifdef CALIBRATE_POSITIONAL if ((argc < 2) || STRCMPI(argv[1], "dump")) { Trace("Usage: calibrate dump | calibrate basemargin | " - "calibrate marginsafety | calibrate qsearchfutility\n"); + "calibrate marginsafety | calibrate qsearchfutility | " + "calibrate exitrate\n"); return; } DumpPositionalCalibration(); @@ -2882,7 +2905,179 @@ COMMAND(HelpCommand) } -void +void +ExecuteCommandLineDirectly(CHAR *szLine, POSITION *pos) +/** + +Routine description: + + Parse and execute exactly one command/move line against pos, right + now, without touching the global input event queue (g_InputEventList). + + This is the not-currently-searching half of ParseUserInput's dispatch + logic, factored out so a caller that already has a line of text in + hand -- and knows for certain nothing is searching -- can execute it + directly instead of round-tripping through PushNewInput()+ + ParseUserInput(). + + That round-trip is what ScriptCommand's _ProcessSuiteLine used to do + for every non-"go" script line, and it was a real bug (found + 2026-09-16): PushNewInput() appends to the *tail* of the shared + global queue and ParseUserInput() pops from the *head*, so it only + executes the line just pushed if the queue was empty at that moment. + It wasn't guaranteed to be -- PushNewInput() itself splits any + caller-supplied string on unquoted ';' into multiple queued entries + up front (see PushNewInput, input.c), so anything chained after + "script <file>" in the same --command string (e.g. "script foo.ep_; + calibrate marginsafety") was already sitting in the queue, ahead of + the script's own first line, before the script even started running. + The result: the very first script line's push-then-pop popped that + *pre-existing* queued command instead (executing it far too early, + with no data yet), and every line after that ran one line behind + forever (each push+pop executing the *previous* iteration's pushed + line, never the one just pushed), because in steady state a FIFO + queue with one push and one pop per iteration always yields what was + already at the head. Net effect: every `go` searched the *previous* + script line's position, not its own, and the true final position was + applied but never searched at all. Using this function instead means + a script line is parsed and dispatched inline, with no shared queue + involved, so there's nothing left in the queue to race against no + matter what the caller queued before or after "script". + +Parameters: + + CHAR *szLine : mutable; will be NUL-chopped at the first '\n' and + further split in place while tokenizing, same as ParseUserInput + does to its own copy of the input line. + POSITION *pos + +Return value: + + void + +**/ +{ + CHAR *pCapturedInput = NULL; + CHAR *p = szLine; + CHAR *q; + ULONG u; + CHAR *argv[MAX_ARGS]; + ULONG argc = 0; + FLAG fInQuote = FALSE; + MOVE mv; + + while((*p) && (isspace(*p))) p++; + if (((*p) && (*p == '#')) || + ((*p) && (*(p+1)) && (*p == '/') && (*(p+1) == '/'))) + { + return; + } + + q = p; + while(*q) + { + if (*q == '\n') + { + *q = '\0'; + break; + } + q++; + } + + pCapturedInput = STRDUP(p); + memset(argv, 0, sizeof(argv)); + do + { + while(*p && isspace(*p)) p++; + if (!*p) break; + argv[argc++] = p; + fInQuote = FALSE; + while (*p) + { + if (*p == '"') + { + fInQuote = !fInQuote; + } + else if (*p == '{') + { + fInQuote = TRUE; + } + else if (*p == '}') + { + fInQuote = FALSE; + } + else if (isspace(*p) && (FALSE == fInQuote)) + { + break; + } + p++; + } + if (!*p) break; + *p = '\0'; + p++; + } + while(argc < MAX_ARGS); + if (argc == 0) + { + SystemFreeMemory(pCapturedInput); + return; + } + + mv.uMove = 0; + switch(LooksLikeMove(argv[0])) + { + case MOVE_SAN: + mv = ParseMoveSan(argv[0], pos); + break; + case MOVE_ICS: + mv = ParseMoveIcs(argv[0], pos); + break; + case NOT_MOVE: + for (u = 0; u < ARRAY_LENGTH(g_ParserTable); u++) + { + if (!STRCMPI(g_ParserTable[u].szOpcode, argv[0])) + { + (*g_ParserTable[u].pFunction) + (pCapturedInput, argc, argv, pos); + SystemFreeMemory(pCapturedInput); + return; + } + } + if ((TRUE == LooksLikeFen(pCapturedInput)) && + (TRUE == SetRootPosition(pCapturedInput))) + { + SystemFreeMemory(pCapturedInput); + return; + } + break; +#ifdef DEBUG + default: + UtilPanic(SHOULD_NOT_GET_HERE, + NULL, NULL, NULL, NULL, + __FILE__, __LINE__); + break; +#endif + } + + if (mv.uMove) + { + if (FALSE == OfficiallyMakeMove(mv, 0, FALSE)) + { + UtilPanic(CANNOT_OFFICIALLY_MAKE_MOVE, + GetRootPosition(), + (void *)(size_t)mv.uMove, + NULL, + NULL, + __FILE__, __LINE__); + } + } else { + Trace("Unknown/illegal move/command: \"%s\"\n", pCapturedInput); + } + SystemFreeMemory(pCapturedInput); +} + + +void ParseUserInput(FLAG fSearching) /** |
