summaryrefslogtreecommitdiff
path: root/src/command.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/command.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/command.c')
-rw-r--r--src/command.c201
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)
/**