summaryrefslogtreecommitdiff
path: root/src/testrecogn.c
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-09-08 20:25:26 -0700
committerScott Gasch <[email protected]>2026-09-08 20:25:26 -0700
commitac1db917dc50e372806780bc0ab2cc0570d8ca54 (patch)
tree245f7f7d4538369da74f1f8641041915e4122f9e /src/testrecogn.c
parent88a0787a7d19a5b4e19e540816f1d500e02dbfeb (diff)
Remove dead autoplay/autoplayer trees, land GNUmakefile/eval_tune companionsHEADmaster
- Remove autoplay/ and autoplayer/ entirely: old opponent-automation scrapers/harnesses (child_process.cc test scaffolding, compiled a.out binaries and a .dSYM bundle, saved book/position files, macOS ._-prefixed resource-fork cruft) that predate this repo's current tooling and were never referenced by anything still in use. - GNUmakefile: add DIAG_NO_QSEARCH_FUTILITY/CALIBRATE_QSEARCH_FUTILITY profile flags and testrecogn.o to the TEST=1 object list. Both are companions to already-committed work that never got their own build support committed: search.c's qsearch-futility calibration harness needs the two profile flags to be buildable at all, and testrecogn.c (added alongside the recogn.c bugfix, now committed here too) needs to be in TEST=1's OBJS to actually compile/link. - eval_tune/match_play.py, eval_tune/test_vs_head.sh: real fixes found and applied earlier this session -- match_play.py's opening-book leak (games weren't actually book-free), missing --hash/--cpus (games ran on the 64k-entry/single-cpu memset-zero defaults instead of this project's normal 256m/1cpu), a shared-logfile race across concurrent match workers, and a report-parsing deadlock on engine resignation. test_vs_head.sh reverted to comparing against head_reference/typhoon + the live working-tree binary -- it had been pointed at a since-deleted, long-stale one-off comparison binary (typhoon_allbitboards) since 92fc412, silently invalidating every "vs head" self-play check run through it since Sep 4. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Ka9o3S2eKqh4jNfmxVZ6fH
Diffstat (limited to 'src/testrecogn.c')
-rw-r--r--src/testrecogn.c536
1 files changed, 536 insertions, 0 deletions
diff --git a/src/testrecogn.c b/src/testrecogn.c
new file mode 100644
index 0000000..05773b5
--- /dev/null
+++ b/src/testrecogn.c
@@ -0,0 +1,536 @@
+/**
+
+Copyright (c) Scott Gasch
+
+Module Name:
+
+ testrecogn.c
+
+Abstract:
+
+ Cross-check the interior-node recognizers in recogn.c against
+ Syzygy EGTB ground truth (via ProbeEGTB/fathom). For each
+ registered recognizer material signature, build a batch of random
+ legal positions matching that exact signature, ask RecognLookup
+ for a recognizer-only verdict (fProbeEGTB == FALSE, so a hit is
+ guaranteed to have come from hand-written recognizer logic, not a
+ table probe), then verify the verdict against a direct ProbeEGTB
+ call on the same position whenever tablebase coverage is
+ available. This only proves anything when the box actually has
+ EGTB files installed and initialized (see CLAUDE.md's note on
+ /zscratch/egtb) -- if not, this still exercises RecognLookup/the
+ position builder for crashes but silently skips the real
+ cross-check for every position (reported in the trailing summary
+ line so that's visible rather than a quiet false pass).
+
+Author:
+
+ Scott Gasch ([email protected])
+
+Revision History:
+
+**/
+
+#include "chess.h"
+
+#ifdef TEST
+
+#define RECOGN_TEST_ITERATIONS_PER_CASE (150)
+
+typedef struct _RECOGN_CASE
+{
+ const char *szName;
+ ULONG uPawnLoA, uPawnHiA;
+ ULONG uKnightLoA, uKnightHiA;
+ ULONG uBishopLoA, uBishopHiA;
+ ULONG uPawnLoB, uPawnHiB;
+ ULONG uKnightLoB, uKnightHiB;
+ ULONG uBishopLoB, uBishopHiB;
+} RECOGN_CASE;
+
+//
+// One entry per material signature pair registered in
+// InitializeInteriorNodeRecognizers (recogn.c). "A"/"B" are abstract
+// slots, randomly assigned to WHITE/BLACK per generated position (see
+// _MakeRandomEndgame) so both color orderings get exercised.
+//
+static RECOGN_CASE g_rgRecognCases[] =
+{
+ // name pawnA knightA bishopA pawnB knightB bishopB
+ { "KK", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
+ { "KBK", 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 0 },
+ { "KBKB", 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 1, 2 },
+ { "KNK", 0, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0 },
+ { "KNKN", 0, 0, 1, 2, 0, 0, 0, 0, 1, 2, 0, 0 },
+ { "KNKB", 0, 0, 1, 2, 0, 0, 0, 0, 0, 0, 1, 2 },
+ { "KNBK", 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0 },
+ { "KNKP", 1, 3, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0 },
+ { "KBKP", 1, 3, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2 },
+ { "KPBKP", 1, 2, 0, 0, 1, 2, 1, 2, 0, 0, 0, 0 },
+ { "KPBK", 1, 2, 0, 0, 1, 2, 0, 0, 0, 0, 0, 0 },
+ { "KPK", 0, 0, 0, 0, 0, 0, 1, 3, 0, 0, 0, 0 },
+ { "KPKP", 1, 3, 0, 0, 0, 0, 1, 3, 0, 0, 0, 0 },
+};
+
+#define RECOGN_RANGE(lo, hi) \
+ ((lo) + (ULONG)(rand() % (((hi) - (lo)) + 1)))
+
+//
+// Build one random position with exactly uPawnA/uKnightA/uBishopA
+// pieces of those types for a randomly-picked color and
+// uPawnB/uKnightB/uBishopB for the other color, plus two kings.
+// Returns FALSE (caller should retry) if the random placement turned
+// out illegal (most commonly: a slider placed such that the side not
+// on move is left in check).
+//
+static FLAG
+_TryBuildRandomEndgame(OUT POSITION *pos,
+ IN ULONG uPawnA, IN ULONG uKnightA, IN ULONG uBishopA,
+ IN ULONG uPawnB, IN ULONG uKnightB, IN ULONG uBishopB)
+{
+ PIECE pBoard[128];
+ CHAR szFen[256];
+ CHAR *p;
+ COOR c, cWhiteKing, cBlackKing;
+ ULONG uColorA, uColorB;
+ ULONG uPawn[2], uKnight[2], uBishop[2];
+ ULONG uColor, u, x;
+
+ memset(pBoard, 0, sizeof(pBoard));
+
+ uColorA = RANDOM_COLOR;
+ uColorB = FLIP(uColorA);
+ uPawn[uColorA] = uPawnA;
+ uPawn[uColorB] = uPawnB;
+ uKnight[uColorA] = uKnightA;
+ uKnight[uColorB] = uKnightB;
+ uBishop[uColorA] = uBishopA;
+ uBishop[uColorB] = uBishopB;
+
+ do
+ {
+ cWhiteKing = RANDOM_COOR;
+ }
+ while (!IS_ON_BOARD(cWhiteKing));
+ do
+ {
+ cBlackKing = RANDOM_COOR;
+ }
+ while ((!IS_ON_BOARD(cBlackKing)) ||
+ (DISTANCE(cWhiteKing, cBlackKing) < 2));
+ pBoard[cWhiteKing] = WHITE_KING;
+ pBoard[cBlackKing] = BLACK_KING;
+
+ FOREACH_COLOR(uColor)
+ {
+ for (u = 0; u < uPawn[uColor]; u++)
+ {
+ do
+ {
+ c = RANDOM_COOR;
+ }
+ while ((!IS_ON_BOARD(c)) || (0 != pBoard[c]) ||
+ RANK1(c) || RANK8(c));
+ pBoard[c] = (WHITE == uColor) ? WHITE_PAWN : BLACK_PAWN;
+ }
+ for (u = 0; u < uKnight[uColor]; u++)
+ {
+ do
+ {
+ c = RANDOM_COOR;
+ }
+ while ((!IS_ON_BOARD(c)) || (0 != pBoard[c]));
+ pBoard[c] = (WHITE == uColor) ? WHITE_KNIGHT : BLACK_KNIGHT;
+ }
+ for (u = 0; u < uBishop[uColor]; u++)
+ {
+ do
+ {
+ c = RANDOM_COOR;
+ }
+ while ((!IS_ON_BOARD(c)) || (0 != pBoard[c]));
+ pBoard[c] = (WHITE == uColor) ? WHITE_BISHOP : BLACK_BISHOP;
+ }
+ }
+
+ memset(szFen, 0, sizeof(szFen));
+ p = szFen;
+ for (x = 0; x < 120; x++)
+ {
+ if ((x % 15 == 0) && (x != 0))
+ {
+ *p++ = '/';
+ }
+ if (!IS_ON_BOARD(x)) continue;
+ if (0 == pBoard[x])
+ {
+ *p++ = '1';
+ }
+ else
+ {
+ *p++ = *(PieceAbbrev(pBoard[x]) + 1);
+ }
+ }
+ *p++ = ' ';
+ *p++ = (RANDOM_COLOR == WHITE) ? 'w' : 'b';
+ strcat(p, " - - 0 1");
+
+ if (FALSE == FenToPosition(pos, szFen))
+ {
+ return(FALSE);
+ }
+ if (InCheck(pos, FLIP(pos->uToMove)))
+ {
+ return(FALSE);
+ }
+ return(TRUE);
+}
+
+static void
+_MakeRandomEndgame(OUT POSITION *pos,
+ IN ULONG uPawnA, IN ULONG uKnightA, IN ULONG uBishopA,
+ IN ULONG uPawnB, IN ULONG uKnightB, IN ULONG uBishopB)
+{
+ while (FALSE == _TryBuildRandomEndgame(pos, uPawnA, uKnightA, uBishopA,
+ uPawnB, uKnightB, uBishopB))
+ {
+ // retry with a fresh random placement
+ }
+}
+
+//
+// Does a recognizer's returned bound/exact verdict (iRecScore, uVal --
+// both relative to the position's side to move, same convention as
+// ProbeEGTB) agree with a coarse WDL-only EGTB probe (iEgtbScore, also
+// relative to side to move)? This mirrors the corrected reasoning
+// used in recogn.c's own (DEBUG-only) _SanityCheckRecognizers: a
+// LOWER/UPPER bound only licenses a directional claim on the side of
+// the bound that's actually pinned down (a positive lower bound forces
+// a real win; a negative lower bound makes no claim at all), not the
+// mirror-image claim a copy/paste of the EXACT case would produce.
+//
+static FLAG
+_RecognizerAgreesWithEGTB(IN SCORE iRecScore,
+ IN ULONG uVal,
+ IN SCORE iEgtbScore)
+{
+ switch (uVal)
+ {
+ case RECOGN_EXACT:
+ if (iRecScore == 0) return(iEgtbScore == 0);
+ if (iRecScore > 0) return(iEgtbScore > 0);
+ return(iEgtbScore < 0);
+
+ case RECOGN_LOWER:
+ if (iRecScore > 0) return(iEgtbScore > 0);
+ if (iRecScore == 0) return(iEgtbScore >= 0);
+ return(TRUE);
+
+ case RECOGN_UPPER:
+ if (iRecScore < 0) return(iEgtbScore < 0);
+ if (iRecScore == 0) return(iEgtbScore <= 0);
+ return(TRUE);
+
+ default:
+ return(TRUE);
+ }
+}
+
+void
+TestRecogn(void)
+/**
+
+Routine description:
+
+ Entry point: build random positions for every registered
+ recognizer material signature and cross-check RecognLookup's
+ verdict against direct EGTB probes wherever coverage exists.
+
+Parameters:
+
+ void
+
+Return value:
+
+ void
+
+**/
+{
+ SEARCHER_THREAD_CONTEXT *ctx;
+ POSITION pos;
+ ULONG uCase, uIter;
+ ULONG uPawnA, uKnightA, uBishopA, uPawnB, uKnightB, uBishopB;
+ SCORE iScore, iEgtb;
+ ULONG uVal;
+ ULONG uChecked = 0, uSkippedNoEgtb = 0, uUnrecognized = 0;
+ RECOGN_CASE *p;
+
+ Trace("Testing interior-node recognizers against EGTB ground truth...\n");
+ ctx = malloc(sizeof(SEARCHER_THREAD_CONTEXT));
+ if (NULL == ctx)
+ {
+ return;
+ }
+ //
+ // InitializeSearcherContext memsets the whole (tens-of-MB)
+ // SEARCHER_THREAD_CONTEXT -- fine once, catastrophic per-iteration
+ // in a loop like this one (same pitfall CLAUDE.md documents for
+ // the old EvalCommand bug). Init once, then use the cheap
+ // ReInitializeSearcherContext (just repositions + zeros counters)
+ // per generated position below.
+ //
+ InitializeSearcherContext(&pos, ctx);
+
+ for (uCase = 0; uCase < ARRAY_LENGTH(g_rgRecognCases); uCase++)
+ {
+ p = &g_rgRecognCases[uCase];
+ for (uIter = 0; uIter < RECOGN_TEST_ITERATIONS_PER_CASE; uIter++)
+ {
+ uPawnA = RECOGN_RANGE(p->uPawnLoA, p->uPawnHiA);
+ uKnightA = RECOGN_RANGE(p->uKnightLoA, p->uKnightHiA);
+ uBishopA = RECOGN_RANGE(p->uBishopLoA, p->uBishopHiA);
+ uPawnB = RECOGN_RANGE(p->uPawnLoB, p->uPawnHiB);
+ uKnightB = RECOGN_RANGE(p->uKnightLoB, p->uKnightHiB);
+ uBishopB = RECOGN_RANGE(p->uBishopLoB, p->uBishopHiB);
+
+ _MakeRandomEndgame(&pos, uPawnA, uKnightA, uBishopA,
+ uPawnB, uKnightB, uBishopB);
+ ReInitializeSearcherContext(&pos, ctx);
+
+ uVal = RecognLookup(ctx, &iScore, FALSE);
+ if (UNRECOGNIZED == uVal)
+ {
+ uUnrecognized++;
+ continue;
+ }
+
+ if ((iScore <= -NMATE) || (iScore >= NMATE))
+ {
+ UtilPanic(TESTCASE_FAILURE, &pos,
+ (void *)p->szName,
+ "recognizer score out of range",
+ NULL, __FILE__, __LINE__);
+ }
+
+ if (FALSE == ProbeEGTB(ctx, &iEgtb))
+ {
+ uSkippedNoEgtb++;
+ continue;
+ }
+ uChecked++;
+
+ if (FALSE == _RecognizerAgreesWithEGTB(iScore, uVal, iEgtb))
+ {
+ UtilPanic(TESTCASE_FAILURE, &pos,
+ (void *)p->szName,
+ "recognizer verdict disagrees with EGTB",
+ NULL, __FILE__, __LINE__);
+ }
+ }
+ }
+
+ free(ctx);
+ Trace("Recognizer/EGTB cross-check: %lu positions verified, "
+ "%lu skipped (no EGTB coverage), %lu unrecognized.\n",
+ uChecked, uSkippedNoEgtb, uUnrecognized);
+ if (0 == uChecked)
+ {
+ Trace("WARNING: zero positions were actually cross-checked "
+ "against EGTB -- no tablebase coverage available, so "
+ "this run only smoke-tested RecognLookup, not its "
+ "correctness.\n");
+ }
+}
+
+//
+// Build a KNKP (one knight, one pawn) position directly from square
+// placements -- white always plays the knight, black always plays the
+// pawn (color-symmetric by construction: _RecognizeKNKP determines
+// "strong side" dynamically from piece counts, not from hardcoded
+// color, so fixing white=knight loses no coverage and halves the
+// enumeration).
+//
+static FLAG
+_BuildKNKPPosition(OUT POSITION *pos,
+ IN COOR cStrongKing, IN COOR cWeakKing,
+ IN COOR cKnight, IN COOR cPawn,
+ IN ULONG uToMove)
+{
+ PIECE pBoard[128];
+ CHAR szFen[256];
+ CHAR *p;
+ ULONG x;
+
+ memset(pBoard, 0, sizeof(pBoard));
+ pBoard[cStrongKing] = WHITE_KING;
+ pBoard[cWeakKing] = BLACK_KING;
+ pBoard[cKnight] = WHITE_KNIGHT;
+ pBoard[cPawn] = BLACK_PAWN;
+
+ memset(szFen, 0, sizeof(szFen));
+ p = szFen;
+ for (x = 0; x < 120; x++)
+ {
+ if ((x % 15 == 0) && (x != 0))
+ {
+ *p++ = '/';
+ }
+ if (!IS_ON_BOARD(x)) continue;
+ if (0 == pBoard[x])
+ {
+ *p++ = '1';
+ }
+ else
+ {
+ *p++ = *(PieceAbbrev(pBoard[x]) + 1);
+ }
+ }
+ *p++ = ' ';
+ *p++ = (WHITE == uToMove) ? 'w' : 'b';
+ strcat(p, " - - 0 1");
+
+ if (FALSE == FenToPosition(pos, szFen))
+ {
+ return(FALSE);
+ }
+ if (InCheck(pos, FLIP(pos->uToMove)))
+ {
+ return(FALSE);
+ }
+ return(TRUE);
+}
+
+void
+TestRecognExhaustiveKNKP(void)
+/**
+
+Routine description:
+
+ Exhaustively enumerate every legal KNKP (one knight, one pawn)
+ position -- not a random sample -- and verify _RecognizeKNKP's
+ verdict against direct EGTB probes wherever coverage exists. This
+ is the acceptance bar a fixed/rewritten KNKP recognizer should meet
+ before ever being re-registered in
+ InitializeInteriorNodeRecognizers: "no counterexamples in N random
+ samples" is exactly the false confidence that let the original
+ KNKP recognizer ship with two independent, real losing lines (see
+ the comment on _RecognizeKNKP in recogn.c). This enumeration uses
+ left/right board mirror symmetry (pawns break rank symmetry, not
+ file symmetry) to roughly halve the raw square-placement count, and
+ fixes white=knight/black=pawn (also lossless -- see
+ _BuildKNKPPosition) -- what's left is still several million raw
+ square combinations, so this is deliberately NOT wired into
+ TestRecogn()'s automatic self-test sequence (that runs on every
+ engine startup and is expected to stay fast, per CLAUDE.md's
+ precommit_check.sh budget). Call this on demand instead when
+ actually validating a KNKP fix.
+
+Parameters:
+
+ void
+
+Return value:
+
+ void
+
+**/
+{
+ SEARCHER_THREAD_CONTEXT *ctx;
+ POSITION pos;
+ COOR cStrongKing, cWeakKing, cKnight, cPawn;
+ ULONG uToMove;
+ SCORE iScore, iEgtb;
+ ULONG uVal;
+ ULONG uTotal = 0, uChecked = 0, uSkipped = 0, uWrong = 0;
+ ULONG uReportedWrong = 0;
+
+ Trace("Exhaustive KNKP (1 knight, 1 pawn) verification against "
+ "EGTB ground truth -- this takes a while...\n");
+ ctx = malloc(sizeof(SEARCHER_THREAD_CONTEXT));
+ if (NULL == ctx)
+ {
+ return;
+ }
+ //
+ // Same fix as TestRecogn above: init the (tens-of-MB) context once,
+ // not once per generated position -- at millions of iterations the
+ // per-call memset inside InitializeSearcherContext dominates
+ // runtime completely (this is what made the first attempt at this
+ // function never finish within a 10-minute budget).
+ //
+ InitializeSearcherContext(&pos, ctx);
+
+ FOREACH_SQUARE(cStrongKing)
+ {
+ if (!IS_ON_BOARD(cStrongKing)) continue;
+ if (FILE(cStrongKing) > D) continue;
+
+ FOREACH_SQUARE(cWeakKing)
+ {
+ if (!IS_ON_BOARD(cWeakKing)) continue;
+ if (DISTANCE(cStrongKing, cWeakKing) < 2) continue;
+
+ FOREACH_SQUARE(cKnight)
+ {
+ if (!IS_ON_BOARD(cKnight)) continue;
+ if ((cKnight == cStrongKing) || (cKnight == cWeakKing)) continue;
+
+ FOREACH_SQUARE(cPawn)
+ {
+ if (!IS_ON_BOARD(cPawn)) continue;
+ if (RANK1(cPawn) || RANK8(cPawn)) continue;
+ if ((cPawn == cStrongKing) || (cPawn == cWeakKing) ||
+ (cPawn == cKnight)) continue;
+
+ for (uToMove = BLACK; uToMove <= WHITE; uToMove++)
+ {
+ uTotal++;
+ if (0 == (uTotal % 500000))
+ {
+ Trace(" ... %lu raw combos so far (%lu checked, "
+ "%lu wrong)\n", uTotal, uChecked, uWrong);
+ }
+ if (FALSE == _BuildKNKPPosition(&pos, cStrongKing,
+ cWeakKing, cKnight,
+ cPawn, uToMove))
+ {
+ continue;
+ }
+ ReInitializeSearcherContext(&pos, ctx);
+ uVal = _RecognizeKNKP(ctx, &iScore);
+ if (UNRECOGNIZED == uVal)
+ {
+ continue;
+ }
+ if (FALSE == ProbeEGTB(ctx, &iEgtb))
+ {
+ uSkipped++;
+ continue;
+ }
+ uChecked++;
+ if (FALSE == _RecognizerAgreesWithEGTB(iScore, uVal,
+ iEgtb))
+ {
+ uWrong++;
+ if (uReportedWrong < 10)
+ {
+ Trace("MISMATCH iScore=%d uVal=%lu "
+ "iEgtb=%d toMove=%lu\n",
+ iScore, uVal, iEgtb, pos.uToMove);
+ DumpPosition(&pos);
+ uReportedWrong++;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ free(ctx);
+ Trace("Exhaustive KNKP: %lu raw combos, %lu checked against EGTB, "
+ "%lu skipped (no EGTB coverage), %lu WRONG.\n",
+ uTotal, uChecked, uSkipped, uWrong);
+}
+
+#endif // TEST