summaryrefslogtreecommitdiff
path: root/src/script.c
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-09-16 00:08:27 -0700
committerScott Gasch <[email protected]>2026-09-16 00:08:27 -0700
commitb91c8453f3fe21c8ccd3568923d8e39141c9c945 (patch)
treeb908fea282ecd4075744590d3dfc88ffa903d943 /src/script.c
parentc0ac3d7845f60c7f6f2c3bfc306ceb0e4ac045ba (diff)
Add search self-tests (hash consistency, QSearch sanity) and a native EPD parser (`epd` command + ParseEpdRecord), fixing a silent-corruption bug in convert_epd.pl that broke every Black-to-move EPD conversion.
testsearch.c: TestSearchHashConsistency exercises the hash table through real search (repeat-with-warm-table and shallow-then-deep agreement) rather than synthetic store/probe calls. TestQSearchSanity checks termination and a node-count ceiling; its own header documents that the TEST=1 self-test build doesn't define PERF_COUNTERS, so it checks whole-search node count rather than the qsearch-only counter. A planned null-move zugzwang test was dropped in favor of tests/zugzwang.epd (best-move outcome, not an internal counter) after the three classic literature positions turned out not to structurally reach this engine's verification-search code path on this build. fen.c/script.c/command.c: new `epd <file>` command reads raw EPD directly via ParseEpdRecord, reusing ScriptCommand's existing suite-reporting machinery (factored into _ProcessSuiteLine/_PrintSuiteReport). Replaces the need for tests/convert_epd.pl, which had a real bug: its bm/am regex's [^b]+ filler breaks on any Black-to-move record (side-to-move field is literally "b"), and unscoped $fen/$id/$solution globals meant a failed match silently duplicated the previous record instead of erroring. tests/ep_to_epd.pl converts the existing setboard/solution/avoid/id/go suites back to real EPD; cross-checked ecm_ringers.ep_ via `script` against ecm_ringers.epd via `epd` -- byte-identical node counts and solve stats. Added .epd siblings for every genuine tactical suite in tests/. Added src/TESTING_PLAN.md: full audit of test*.c coverage gaps (dead SEE oracle, unwired eval/recognizer tests, zero hash-table correctness coverage, thin search self-tests) with a prioritized plan for closing them. Co-Authored-By: Claude Sonnet 5 <[email protected]>
Diffstat (limited to 'src/script.c')
-rw-r--r--src/script.c378
1 files changed, 262 insertions, 116 deletions
diff --git a/src/script.c b/src/script.c
index 0bc09e4..e8d6f9a 100644
--- a/src/script.c
+++ b/src/script.c
@@ -349,6 +349,160 @@ Return value:
+//
+// Shared by ScriptCommand and EpdCommand: process one already-read
+// line exactly the way a script file's line is processed -- "go"
+// triggers a search plus counter accumulation, anything else is
+// dispatched through the normal command parser. Factored out so
+// EpdCommand can feed it lines synthesized from a raw EPD record
+// (setboard/id/solution/avoid/go) instead of lines read verbatim from
+// a file, without duplicating this logic or its counter bookkeeping.
+//
+static void
+_ProcessSuiteLine(char *szLine, POSITION *pos)
+{
+ Trace("SCRIPT> %s\n", szLine);
+ if (!STRNCMPI(szLine, "go", 2))
+ {
+ (void)Think(pos);
+ g_SuiteCounters.u64TotalNodeCount +=
+ g_Options.u64NodesSearched;
+ g_SuiteCounters.u64TotalBetaCutoffs +=
+ g_Options.u64BetaCutoffs;
+ g_SuiteCounters.u64TotalBetaCutoffsOnFirstMove +=
+ g_Options.u64BetaCutoffsOnFirstMove;
+ g_SuiteCounters.u64TotalCounterMoveTries +=
+ g_Options.u64CounterMoveTries;
+ g_SuiteCounters.u64TotalCounterMoveHits +=
+ g_Options.u64CounterMoveHits;
+ g_SuiteCounters.u64TotalLeftoverTries +=
+ g_Options.u64LeftoverTries;
+ g_SuiteCounters.u64TotalLeftoverAlpha +=
+ g_Options.u64LeftoverAlpha;
+ g_SuiteCounters.u64TotalLeftoverFH +=
+ g_Options.u64LeftoverFH;
+ {
+ ULONG v;
+ for (v = 0; v < CM_EVIDENCE_BUCKETS; v++)
+ {
+ g_SuiteCounters.u64TotalCMEvidenceTries[v] +=
+ g_Options.u64CMEvidenceTries[v];
+ g_SuiteCounters.u64TotalCMEvidenceFH[v] +=
+ g_Options.u64CMEvidenceFH[v];
+ }
+ }
+ }
+ else
+ {
+ PushNewInput(szLine);
+ if (FALSE == g_fExitProgram)
+ {
+ ParseUserInput(FALSE);
+ }
+ }
+}
+
+
+//
+// Shared by ScriptCommand and EpdCommand: the end-of-suite banner and
+// statistics report. Verbatim extraction of what used to be
+// ScriptCommand's own tail -- see that function's history for the
+// meaning of each field.
+//
+static void
+_PrintSuiteReport(double dSuiteStart)
+{
+ ULONG u, v, uMax;
+ double dMult = (double)g_Options.uMyIncrement / (double)SUITE_NUM_HISTOGRAM;
+
+ Banner();
+ Trace("\n"
+ "TEST SCRIPT execution complete. Final statistics:\n"
+ "--------------------------------------------------\n"
+ " correct solutions : %u\n"
+ " incorrect solutions : %u\n"
+ " total problems : %u\n"
+ " total nodecount : %"
+ COMPILER_LONGLONG_UNSIGNED_FORMAT "\n"
+ " avg. search speed : %6.1f nps\n"
+ " avg. solution time : %3.1f sec\n"
+ " avg. search depth : %4.1f ply\n"
+ " 1st move beta cut : %5.2f percent\n"
+ " avg. eff. branching : %5.3f\n"
+ " counter move hit %% : %5.2f percent (%"
+ COMPILER_LONGLONG_UNSIGNED_FORMAT " tries)\n"
+ " leftover : %5.2f%% alpha, %5.2f%% FH (%"
+ COMPILER_LONGLONG_UNSIGNED_FORMAT " tries)\n"
+ " script time : %6.1f sec\n\n",
+ g_SuiteCounters.uCorrect,
+ g_SuiteCounters.uIncorrect,
+ g_SuiteCounters.uTotal,
+ g_SuiteCounters.u64TotalNodeCount,
+ ((double)g_SuiteCounters.u64TotalNodeCount /
+ (SystemTimeStamp() - dSuiteStart)),
+ ((g_SuiteCounters.uCorrect > 0) ?
+ (g_SuiteCounters.dSigmaSolutionTime /
+ (double)g_SuiteCounters.uCorrect) : 0.0),
+ ((double)g_SuiteCounters.uSigmaDepth /
+ (double)g_SuiteCounters.uTotal),
+ (100.0 * (double)g_SuiteCounters.u64TotalBetaCutoffsOnFirstMove /
+ ((double)g_SuiteCounters.u64TotalBetaCutoffs + 1.0)),
+ (g_SuiteCounters.dSigmaEBF /
+ ((double)g_SuiteCounters.uEBFCount + 1.0)),
+ (100.0 * (double)g_SuiteCounters.u64TotalCounterMoveHits /
+ ((double)g_SuiteCounters.u64TotalCounterMoveTries + 1.0)),
+ g_SuiteCounters.u64TotalCounterMoveTries,
+ (100.0 * (double)g_SuiteCounters.u64TotalLeftoverAlpha /
+ ((double)g_SuiteCounters.u64TotalLeftoverTries + 1.0)),
+ (100.0 * (double)g_SuiteCounters.u64TotalLeftoverFH /
+ ((double)g_SuiteCounters.u64TotalLeftoverTries + 1.0)),
+ g_SuiteCounters.u64TotalLeftoverTries,
+ (SystemTimeStamp() - dSuiteStart));
+
+ // Countermove evidence calibration -- FH% per evidence bucket,
+ // an ongoing check that COUNTERMOVE_EVIDENCE_THRESHOLD (chess.h)
+ // is still well-calibrated as the engine/data evolve.
+ {
+ static const ULONG uCMEvidenceFloors[CM_EVIDENCE_BUCKETS] =
+ { 0, 1, 100, 1000, 10000, 100000, 1000000 };
+ Trace("\ncountermove evidence calibration:\n");
+ for (v = 0; v < CM_EVIDENCE_BUCKETS; v++)
+ {
+ Trace(" evidence >= %8lu : %5.2f%% FH (%"
+ COMPILER_LONGLONG_UNSIGNED_FORMAT " tries)\n",
+ uCMEvidenceFloors[v],
+ (100.0 * (double)g_SuiteCounters.u64TotalCMEvidenceFH[v] /
+ ((double)g_SuiteCounters.u64TotalCMEvidenceTries[v] + 1.0)),
+ g_SuiteCounters.u64TotalCMEvidenceTries[v]);
+ }
+ }
+
+ // Histogram stuff
+ if (g_SuiteCounters.uTotal > 0) {
+ uMax = g_SuiteCounters.uHistogram[0];
+ for (u = 1; u < SUITE_NUM_HISTOGRAM; u++)
+ {
+ if (g_SuiteCounters.uHistogram[u] > uMax)
+ {
+ uMax = g_SuiteCounters.uHistogram[u];
+ }
+ }
+ for (u = 0; u < SUITE_NUM_HISTOGRAM; u++)
+ {
+ Trace("%4.1f .. %4.1f: ",
+ (double)u * dMult, (double)(u + 1) * dMult);
+ for (v = 0;
+ (uMax > 0) && (v < 50 * g_SuiteCounters.uHistogram[u] / uMax);
+ v++)
+ {
+ Trace("*");
+ }
+ Trace("\n");
+ }
+ }
+}
+
+
COMMAND(ScriptCommand)
/**
@@ -389,8 +543,6 @@ Return value:
FILE *p;
static CHAR szLine[SMALL_STRING_LEN_CHAR];
double dSuiteStart;
- ULONG u, v, uMax;
- double dMult;
if (argc < 2)
{
@@ -402,143 +554,137 @@ Return value:
Trace("Error (file doesn't exist): %s\n", argv[1]);
return;
}
-
+
p = fopen(argv[1], "rb");
if (NULL != p)
{
- // Doing the script...
dSuiteStart = SystemTimeStamp();
memset(&(g_SuiteCounters), 0, sizeof(g_SuiteCounters));
while(fgets(szLine, ARRAY_LENGTH(szLine), p))
{
- Trace("SCRIPT> %s\n", szLine);
- if (!STRNCMPI(szLine, "go", 2))
- {
- (void)Think(pos);
- g_SuiteCounters.u64TotalNodeCount +=
- g_Options.u64NodesSearched;
- g_SuiteCounters.u64TotalBetaCutoffs +=
- g_Options.u64BetaCutoffs;
- g_SuiteCounters.u64TotalBetaCutoffsOnFirstMove +=
- g_Options.u64BetaCutoffsOnFirstMove;
- g_SuiteCounters.u64TotalCounterMoveTries +=
- g_Options.u64CounterMoveTries;
- g_SuiteCounters.u64TotalCounterMoveHits +=
- g_Options.u64CounterMoveHits;
- g_SuiteCounters.u64TotalLeftoverTries +=
- g_Options.u64LeftoverTries;
- g_SuiteCounters.u64TotalLeftoverAlpha +=
- g_Options.u64LeftoverAlpha;
- g_SuiteCounters.u64TotalLeftoverFH +=
- g_Options.u64LeftoverFH;
- for (v = 0; v < CM_EVIDENCE_BUCKETS; v++)
- {
- g_SuiteCounters.u64TotalCMEvidenceTries[v] +=
- g_Options.u64CMEvidenceTries[v];
- g_SuiteCounters.u64TotalCMEvidenceFH[v] +=
- g_Options.u64CMEvidenceFH[v];
- }
- }
- else
- {
- PushNewInput(szLine);
- if (TRUE == g_fExitProgram) break;
- ParseUserInput(FALSE);
- }
+ _ProcessSuiteLine(szLine, pos);
+ if (TRUE == g_fExitProgram) break;
}
fclose(p);
+ _PrintSuiteReport(dSuiteStart);
+ }
+}
- // Banner and end results
- dMult = (double)g_Options.uMyIncrement / (double)SUITE_NUM_HISTOGRAM;
- Banner();
- Trace("\n"
- "TEST SCRIPT execution complete. Final statistics:\n"
- "--------------------------------------------------\n"
- " correct solutions : %u\n"
- " incorrect solutions : %u\n"
- " total problems : %u\n"
- " total nodecount : %"
- COMPILER_LONGLONG_UNSIGNED_FORMAT "\n"
- " avg. search speed : %6.1f nps\n"
- " avg. solution time : %3.1f sec\n"
- " avg. search depth : %4.1f ply\n"
- " 1st move beta cut : %5.2f percent\n"
- " avg. eff. branching : %5.3f\n"
- " counter move hit %% : %5.2f percent (%"
- COMPILER_LONGLONG_UNSIGNED_FORMAT " tries)\n"
- " leftover : %5.2f%% alpha, %5.2f%% FH (%"
- COMPILER_LONGLONG_UNSIGNED_FORMAT " tries)\n"
- " script time : %6.1f sec\n\n",
- g_SuiteCounters.uCorrect,
- g_SuiteCounters.uIncorrect,
- g_SuiteCounters.uTotal,
- g_SuiteCounters.u64TotalNodeCount,
- ((double)g_SuiteCounters.u64TotalNodeCount /
- (SystemTimeStamp() - dSuiteStart)),
- ((g_SuiteCounters.uCorrect > 0) ?
- (g_SuiteCounters.dSigmaSolutionTime /
- (double)g_SuiteCounters.uCorrect) : 0.0),
- ((double)g_SuiteCounters.uSigmaDepth /
- (double)g_SuiteCounters.uTotal),
- (100.0 * (double)g_SuiteCounters.u64TotalBetaCutoffsOnFirstMove /
- ((double)g_SuiteCounters.u64TotalBetaCutoffs + 1.0)),
- (g_SuiteCounters.dSigmaEBF /
- ((double)g_SuiteCounters.uEBFCount + 1.0)),
- (100.0 * (double)g_SuiteCounters.u64TotalCounterMoveHits /
- ((double)g_SuiteCounters.u64TotalCounterMoveTries + 1.0)),
- g_SuiteCounters.u64TotalCounterMoveTries,
- (100.0 * (double)g_SuiteCounters.u64TotalLeftoverAlpha /
- ((double)g_SuiteCounters.u64TotalLeftoverTries + 1.0)),
- (100.0 * (double)g_SuiteCounters.u64TotalLeftoverFH /
- ((double)g_SuiteCounters.u64TotalLeftoverTries + 1.0)),
- g_SuiteCounters.u64TotalLeftoverTries,
- (SystemTimeStamp() - dSuiteStart));
- // Countermove evidence calibration -- FH% per evidence bucket,
- // an ongoing check that COUNTERMOVE_EVIDENCE_THRESHOLD (chess.h)
- // is still well-calibrated as the engine/data evolve.
+COMMAND(EpdCommand)
+/**
+
+Routine description:
+
+ Runs a raw EPD test suite file directly -- no perl preprocessing
+ into a setboard/solution/go script first. Historically the only
+ way to run a suite from real-world EPD (the "board stm castle ep
+ [halfmove fullmove] bm <move>; id \"...\"; ..." format most public
+ tactical suites ship in) was tests/convert_epd.pl, which had a
+ real, silent-corruption bug: its single "everything between the
+ board and bm is [^b]+" regex breaks on any Black-to-move record
+ (the side-to-move field is literally the character "b"), and
+ because $fen/$id/$solution were unscoped globals rather than
+ per-line locals, a failed match silently carried the *previous*
+ record's board and solution forward instead of erroring --
+ confirmed live by feeding it a real 5-position batch, where the
+ one Black-to-move line came out as a byte-for-byte duplicate of
+ the line before it. This command replaces that whole external
+ preprocessing step with real per-record EPD parsing (see
+ ParseEpdRecord, fen.c) feeding the exact same
+ _ProcessSuiteLine/_PrintSuiteReport machinery ScriptCommand uses,
+ so a raw EPD file can just be handed to the engine directly.
+
+ Usage:
+
+ epd <required filename>
+
+Parameters:
+
+ The COMMAND macro hides four arguments from the input parser:
+
+ CHAR *szInput : the full line of input
+ ULONG argc : number of argument chunks
+ CHAR *argv[] : array of ptrs to each argument chunk
+ POSITION *pos : a POSITION pointer to operate on
+
+Return value:
+
+ void
+
+**/
+{
+ FILE *p;
+ static CHAR szLine[SMALL_STRING_LEN_CHAR];
+ static CHAR szSynth[SMALL_STRING_LEN_CHAR];
+ CHAR szFen[SMALL_STRING_LEN_CHAR];
+ CHAR szId[SMALL_STRING_LEN_CHAR];
+ CHAR szMoves[SUITE_MAX_SOLUTIONS + SUITE_MAX_AVOIDS][SMALL_STRING_LEN_CHAR];
+ ULONG uNumMoves;
+ FLAG fIsAvoid;
+ double dSuiteStart;
+ ULONG u;
+ ULONG uLineNum = 0;
+
+ if (argc < 2)
+ {
+ Trace("Error (Usage: epd <filename>): %s\n", argv[0]);
+ return;
+ }
+ if (FALSE == SystemDoesFileExist(argv[1]))
+ {
+ Trace("Error (file doesn't exist): %s\n", argv[1]);
+ return;
+ }
+
+ p = fopen(argv[1], "rb");
+ if (NULL != p)
+ {
+ dSuiteStart = SystemTimeStamp();
+ memset(&(g_SuiteCounters), 0, sizeof(g_SuiteCounters));
+ while(fgets(szLine, ARRAY_LENGTH(szLine), p))
{
- static const ULONG uCMEvidenceFloors[CM_EVIDENCE_BUCKETS] =
- { 0, 1, 100, 1000, 10000, 100000, 1000000 };
- Trace("\ncountermove evidence calibration:\n");
- for (v = 0; v < CM_EVIDENCE_BUCKETS; v++)
+ uLineNum++;
+ if (FALSE == ParseEpdRecord(szLine, szFen, ARRAY_LENGTH(szFen),
+ szId, ARRAY_LENGTH(szId),
+ szMoves, ARRAY_LENGTH(szMoves),
+ &uNumMoves, &fIsAvoid))
{
- Trace(" evidence >= %8lu : %5.2f%% FH (%"
- COMPILER_LONGLONG_UNSIGNED_FORMAT " tries)\n",
- uCMEvidenceFloors[v],
- (100.0 * (double)g_SuiteCounters.u64TotalCMEvidenceFH[v] /
- ((double)g_SuiteCounters.u64TotalCMEvidenceTries[v] + 1.0)),
- g_SuiteCounters.u64TotalCMEvidenceTries[v]);
+ if (szLine[0] && (szLine[0] != '\n') && (szLine[0] != '#'))
+ {
+ Trace("epd: skipping unparseable line %lu: %s",
+ uLineNum, szLine);
+ }
+ continue;
}
- }
- // Histogram stuff
- if (g_SuiteCounters.uTotal > 0) {
- uMax = g_SuiteCounters.uHistogram[0];
- for (u = 1; u < SUITE_NUM_HISTOGRAM; u++)
+ snprintf(szSynth, ARRAY_LENGTH(szSynth), "setboard %s\n", szFen);
+ _ProcessSuiteLine(szSynth, pos);
+ if (TRUE == g_fExitProgram) break;
+
+ if (szId[0])
{
- if (g_SuiteCounters.uHistogram[u] > uMax)
- {
- uMax = g_SuiteCounters.uHistogram[u];
- }
+ snprintf(szSynth, ARRAY_LENGTH(szSynth), "id %s\n", szId);
+ _ProcessSuiteLine(szSynth, pos);
}
- for (u = 0; u < SUITE_NUM_HISTOGRAM; u++)
+
+ for (u = 0; u < uNumMoves; u++)
{
- Trace("%4.1f .. %4.1f: ",
- (double)u * dMult, (double)(u + 1) * dMult);
- for (v = 0;
- (uMax > 0) && (v < 50 * g_SuiteCounters.uHistogram[u] / uMax);
- v++)
- {
- Trace("*");
- }
- Trace("\n");
+ snprintf(szSynth, ARRAY_LENGTH(szSynth), "%s %s\n",
+ fIsAvoid ? "avoid" : "solution", szMoves[u]);
+ _ProcessSuiteLine(szSynth, pos);
}
+
+ _ProcessSuiteLine("go\n", pos);
+ if (TRUE == g_fExitProgram) break;
}
+ fclose(p);
+ _PrintSuiteReport(dSuiteStart);
}
}
-FLAG
+
+FLAG
WeAreRunningASuite(void)
/**