diff options
| author | Scott Gasch <[email protected]> | 2026-09-16 00:08:27 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-09-16 00:08:27 -0700 |
| commit | b91c8453f3fe21c8ccd3568923d8e39141c9c945 (patch) | |
| tree | b908fea282ecd4075744590d3dfc88ffa903d943 /src/script.c | |
| parent | c0ac3d7845f60c7f6f2c3bfc306ceb0e4ac045ba (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.c | 378 |
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) /** |
