/** 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 (scott.gasch@gmail.com) 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); } } // // Fixed, known-tricky positions -- added as regression tests when a // real bug is found by hand rather than by the random fuzzer above. // The fuzzer's uniform random king placement (only constrained by // DISTANCE(cWhiteKing, cBlackKing) >= 2) essentially never lands on // the narrow "defending king already dug into the corner beside a // bishop-pawn's queening square" configuration this family of bug // lives in -- see TestRecognExhaustiveKPK below for the systematic // fix to that coverage gap. Cross-checked against EGTB the same way // as the random cases. // typedef struct _RECOGN_FEN_CASE { const char *szName; const char *szFen; } RECOGN_FEN_CASE; static RECOGN_FEN_CASE g_rgRecognFenCases[] = { // Bishop-pawn (g-file) stalemate-corner draw: Kf6/Pg6 vs lone // Kh8, White to move. _RecognizeKPK (via its // _EvalPasserRacesAgainstLoneKings "unstoppable passer" pre-check, // which fires first) used to call this a large win for White -- // Kf7?? actually stalemates Black immediately, and the position // is a draw with correct defense. Fixed 2026-09-16 in both // recogn.c's critical-squares branch and eval.c's // _EvalPasserRacesAgainstLoneKings. { "KPK_g_pawn_corner_stalemate", "7k/8/5KP1/8/8/8/8/8 w - -" }, // Mirror of the above on the b-file/queenside corner. { "KPK_b_pawn_corner_stalemate", "k7/8/1KP5/8/8/8/8/8 w - -" }, // Same motif with colors flipped (Black holds the pawn). { "KPK_g_pawn_corner_stalemate_black", "8/8/8/8/8/5kp1/8/7K b - -" }, }; // // Run every g_rgRecognFenCases entry through RecognLookup (the real, // full material-signature dispatch a live game would use -- not a // direct static-function call) and cross-check against EGTB the same // way the random cases above do. ctx must already be initialized // (InitializeSearcherContext) by the caller. // static void _TestRecognFenCases(IN OUT SEARCHER_THREAD_CONTEXT *ctx, IN OUT POSITION *pos) { ULONG u, uVal; SCORE iScore, iEgtb; RECOGN_FEN_CASE *p; for (u = 0; u < ARRAY_LENGTH(g_rgRecognFenCases); u++) { p = &g_rgRecognFenCases[u]; if (FALSE == FenToPosition(pos, (CHAR *)p->szFen)) { UtilPanic(TESTCASE_FAILURE, pos, (void *)p->szName, "fixed regression FEN failed to parse", NULL, __FILE__, __LINE__); } ReInitializeSearcherContext(pos, ctx); uVal = RecognLookup(ctx, &iScore, EGTB_PROBE_NEVER); if ((UNRECOGNIZED != uVal) && ((iScore <= -NMATE) || (iScore >= NMATE))) { UtilPanic(TESTCASE_FAILURE, pos, (void *)p->szName, "recognizer score out of range", NULL, __FILE__, __LINE__); } if (FALSE == ProbeEGTB(ctx, &iEgtb)) { Trace(" %s: no EGTB coverage, skipped cross-check " "(smoke-tested RecognLookup only)\n", p->szName); continue; } if ((UNRECOGNIZED != uVal) && (FALSE == _RecognizerAgreesWithEGTB(iScore, uVal, iEgtb))) { UtilPanic(TESTCASE_FAILURE, pos, (void *)p->szName, "recognizer verdict disagrees with EGTB", NULL, __FILE__, __LINE__); } } Trace("Fixed regression FEN cases: %lu checked.\n", (ULONG)ARRAY_LENGTH(g_rgRecognFenCases)); } 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. Also runs the fixed regression FEN list above first (cheap, catches any previously-found-by-hand bug immediately rather than relying on the random fuzzer to rediscover it by luck). 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); _TestRecognFenCases(ctx, &pos); 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, EGTB_PROBE_NEVER); 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); } // // Build a KQKP (one queen, one pawn) position directly from square // placements. fBlackHasQueen selects which color plays the queen side // (cStrongKing/cWeakKing name roles, not colors, in both cases) -- see // TestRecognExhaustiveKQKP's header comment for why both assignments // are enumerated explicitly rather than assumed symmetric. // static FLAG _BuildKQKPPosition(OUT POSITION *pos, IN COOR cStrongKing, IN COOR cWeakKing, IN COOR cQueen, IN COOR cPawn, IN ULONG uToMove, IN FLAG fBlackHasQueen) { PIECE pBoard[128]; CHAR szFen[256]; CHAR *p; ULONG x; memset(pBoard, 0, sizeof(pBoard)); if (fBlackHasQueen) { pBoard[cStrongKing] = BLACK_KING; pBoard[cWeakKing] = WHITE_KING; pBoard[cQueen] = BLACK_QUEEN; pBoard[cPawn] = WHITE_PAWN; } else { pBoard[cStrongKing] = WHITE_KING; pBoard[cWeakKing] = BLACK_KING; pBoard[cQueen] = WHITE_QUEEN; 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); } // // Does the side to move in ctx's current position have at least one // legal move? Used only to classify a mismatch as "the to-move side is // simply stalemated" vs. something else -- not part of the recognizer // itself. Mirrors root.c's _DetectPreMoveTerminalStates legal-move // count idiom (GenerateMoves then MakeMove/UnmakeMove-filter), just // short-circuiting on the first legal move found instead of counting // them all. // static FLAG _ToMoveHasLegalMove(IN OUT SEARCHER_THREAD_CONTEXT *ctx) { ULONG x; MOVE mv; FLAG fInCheck = InCheck(&ctx->sPosition, ctx->sPosition.uToMove); GenerateMoves(ctx, NULLMOVE, fInCheck ? GENERATE_ESCAPES : GENERATE_ALL_MOVES); for (x = ctx->sMoveStack.uBegin[ctx->uPly]; x < ctx->sMoveStack.uEnd[ctx->uPly]; x++) { mv = ctx->sMoveStack.mvf[x].mv; if (TRUE == MakeMove(ctx, mv)) { UnmakeMove(ctx, mv); return(TRUE); } } return(FALSE); } void TestRecognExhaustiveKQKP(void) /** Routine description: Exhaustively enumerate every legal KQKP (one queen, one pawn) position -- not a random sample -- and verify _RecognizeKQKP's verdict against direct EGTB probes wherever coverage exists. Same "no counterexamples in N random samples is false confidence" rationale as TestRecognExhaustiveKNKP/KPK -- doubly warranted here since _RecognizeKQKP's win/UNRECOGNIZED boundary (rook/bishop pawn on the square before queening) was scoped down from memory of classical endgame theory rather than derived from EGTB data, and this file has real precedent (_RecognizeKBKP's 2026-09-18 history) for a plausible-looking classical shortcut turning out unsound once fuzzed. Learned from TestRecognExhaustiveKPK's own 2026-09-18 history: fixing which COLOR plays the strong side is NOT a safe assumption in this codebase, even though RecognLookup/_RecognizeKQKP determine "strong side" dynamically from piece counts -- iterates both fBlackHasQueen assignments explicitly rather than trusting that symmetry. Unlike TestRecognExhaustiveKPK, this keeps the left/right mirror-halving restriction (FILE(cStrongKing) <= D, same as TestRecognExhaustiveKNKP) -- safe here because, unlike _RecognizeKPK's hand-typed critical-square table, _RecognizeKQKP contains no per-square lookup data at all: its only file-dependent test is "FILE(cPawn) == A/C/F/H", which treats both rook files and both bishop files identically by construction, so a left/right board reflection cannot expose an asymmetric transcription bug the way it did for KPK's table. Not wired into TestRecogn()'s automatic startup suite (same precommit_check.sh budget reasoning as KNKP/KPK) -- call this on demand when validating a KQKP-area change. Parameters: void Return value: void **/ { SEARCHER_THREAD_CONTEXT *ctx; POSITION pos; COOR cStrongKing, cWeakKing, cQueen, cPawn; ULONG uToMove; SCORE iScore, iEgtb; ULONG uVal; ULONG uTotal = 0, uChecked = 0, uSkipped = 0, uWrong = 0; ULONG uReportedWrong = 0; ULONG uWrongStalemated = 0; FLAG fStalemated; Trace("Exhaustive KQKP (1 queen, 1 pawn) verification against " "EGTB ground truth -- this takes a while...\n"); ctx = malloc(sizeof(SEARCHER_THREAD_CONTEXT)); if (NULL == ctx) { return; } 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(cQueen) { if (!IS_ON_BOARD(cQueen)) continue; if ((cQueen == cStrongKing) || (cQueen == cWeakKing)) continue; FOREACH_SQUARE(cPawn) { if (!IS_ON_BOARD(cPawn)) continue; if (RANK1(cPawn) || RANK8(cPawn)) continue; if ((cPawn == cStrongKing) || (cPawn == cWeakKing) || (cPawn == cQueen)) continue; for (uToMove = BLACK; uToMove <= WHITE; uToMove++) { ULONG uColorPass; for (uColorPass = 0; uColorPass < 2; uColorPass++) { uTotal++; if (0 == (uTotal % 500000)) { Trace(" ... %lu raw combos so far (%lu checked, " "%lu wrong)\n", uTotal, uChecked, uWrong); } if (FALSE == _BuildKQKPPosition(&pos, cStrongKing, cWeakKing, cQueen, cPawn, uToMove, (1 == uColorPass))) { continue; } ReInitializeSearcherContext(&pos, ctx); uVal = _RecognizeKQKP(ctx, &iScore); if (UNRECOGNIZED == uVal) { continue; } if (FALSE == ProbeEGTB(ctx, &iEgtb)) { uSkipped++; continue; } uChecked++; if (FALSE == _RecognizerAgreesWithEGTB(iScore, uVal, iEgtb)) { uWrong++; fStalemated = (FALSE == InCheck(&ctx->sPosition, ctx->sPosition.uToMove)) && (FALSE == _ToMoveHasLegalMove(ctx)); if (TRUE == fStalemated) { uWrongStalemated++; } if (uReportedWrong < 20) { Trace("MISMATCH (blackHasQueen=%lu) iScore=%d " "uVal=%lu iEgtb=%d toMove=%lu " "stalemated=%lu\n", uColorPass, iScore, uVal, iEgtb, pos.uToMove, fStalemated); DumpPosition(&pos); uReportedWrong++; } } } } } } } } { ULONG uWrongOther = uWrong - uWrongStalemated; free(ctx); Trace("Exhaustive KQKP: %lu raw combos, %lu checked against EGTB, " "%lu skipped (no EGTB coverage), %lu WRONG (%lu of those are " "the weak side simply stalemated, %lu are something else).\n", uTotal, uChecked, uSkipped, uWrong, uWrongStalemated, uWrongOther); } } // // Build a KPK (one pawn, one lone king) position directly from square // placements. fBlackHasPawn selects which color plays the pawn side // (cStrongKing/cWeakKing name roles, not colors, in both cases). // // 2026-09-18: the "color-symmetric by construction" claim this // function's comment used to make -- that fixing white=pawn always // and never testing black=pawn loses no coverage, since RecognLookup/ // _RecognizeKPK determine "strong side" dynamically from piece counts // -- turned out to be FALSE. Found via fuzzing tests/cross_ // sectional_200k.ep_: 8/8/8/8/8/8/4K2p/7k w - - 0 0 (White Ke2 lone // king, Black Kh1+Ph2, White to move) hit a RECOGNIZER_SANITY_FAIL -- // recognizer claimed Black winning, EGTB says draw (White can // stalemate Black rather than let the pawn queen). This exhaustive // test's original white-only enumeration reported 0 wrong across // 165,676 EGTB-covered positions, which looked like it cleared // _RecognizeKPK's post-2026-09-16 tempo-margin fix -- but it never // actually exercised the black-has-pawn code path (specifically // _GetPassersCriticalSquares' separate per-color lookup table) at // all, so that "0 wrong" was never evidence either way for this // failure mode. fBlackHasPawn added so the caller can enumerate both // color assignments explicitly, closing the gap instead of assuming // symmetry. // static FLAG _BuildKPKPosition(OUT POSITION *pos, IN COOR cStrongKing, IN COOR cWeakKing, IN COOR cPawn, IN ULONG uToMove, IN FLAG fBlackHasPawn) { PIECE pBoard[128]; CHAR szFen[256]; CHAR *p; ULONG x; memset(pBoard, 0, sizeof(pBoard)); if (fBlackHasPawn) { pBoard[cStrongKing] = BLACK_KING; pBoard[cWeakKing] = WHITE_KING; pBoard[cPawn] = BLACK_PAWN; } else { pBoard[cStrongKing] = WHITE_KING; pBoard[cWeakKing] = BLACK_KING; pBoard[cPawn] = WHITE_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 TestRecognExhaustiveKPK(void) /** Routine description: Exhaustively enumerate every legal KPK (one side has a single pawn, the other a lone king) position -- not a random sample -- and verify RecognLookup's verdict against direct EGTB probes wherever coverage exists. Same "no counterexamples in N random samples is false confidence" rationale as TestRecognExhaustiveKNKP above -- this is specifically the check that would have caught the 2026-09-16 bishop-pawn stalemate-corner bug (_RecognizeKPK / eval.c's _EvalPasserRacesAgainstLoneKings -- see g_rgRecognFenCases above for the concrete repro that WAS caught, by hand, from a real game position) immediately on introduction, since every king/pawn corner configuration is covered by construction here rather than left to the uniform random fuzzer's luck (TestRecogn's random KPK case ran 150 iterations per startup for who knows how many sessions and never once landed on the narrow corner-stalemate family). Much smaller search space than KNKP (no knight, so one fewer nested loop) -- fast enough it could arguably be folded into TestRecogn()'s automatic suite, but kept on-demand for now, consistent with the KNKP precedent, so precommit_check.sh's DEBUG smoke test budget isn't put at the mercy of however slow EGTB probing turns out to be at O(10^5) positions. Call this on demand when actually validating a KPK-area fix, the same way TestRecognExhaustiveKNKP is used for KNKP. 2026-09-18: the "fixes white=pawn/black=lone-king by construction, lossless" and "left/right mirror symmetry, roughly halves the count" claims this comment used to make were BOTH wrong -- found via fuzzing tests/cross_sectional_200k.ep_, which turned up a real _EvalPasserRacesAgainstLoneKings bug (8/8/8/8/8/8/4K2p/7k w - -, strong side's own king on its own rook pawn's queening square) that this test reported "0 wrong" on even after removing both shortcuts. Now iterates BOTH color assignments explicitly (see _BuildKPKPosition's fBlackHasPawn) over the FULL board (no cStrongKing file restriction) -- ~4x the raw combos, still fast enough for on-demand use. IMPORTANT REMAINING GAP, not yet closed: this still cannot exercise _RecognizeKPK's pawn-hash-cache-hit branch (the `pHash->u64Key == pos->u64PawnSig` check gating _EvalPasserRacesAgainstLoneKings). The pawn hash signature depends only on pawn placement, not king squares -- a freshly-built, isolated test position essentially never has a matching warm cache entry (ReInitializeSearcherContext doesn't pre-populate it), so this test structurally falls through to the critical-square path every time and never really exercises the cache-hit path a long, real search naturally warms into (many king placements sharing one pawn skeleton over a deep search tree). This is *why* three separate _EvalPasserRacesAgainstLoneKings bugs (this session's stalemate-corner case, the earlier connected- passers case, and the originally-committed bishop-pawn corner case) all made it past a "0 wrong" run of this test at some point -- a clean run here is NOT proof that function is sound. Confirmed by hand for the 2026-09-18 fix: forcing a warm cache via an explicit `eval` command before `go` on the repro position reproduced the bug pre-fix and confirmed the fix post-fix, but that verification step isn't automated here yet. Worth building: warm the pawn hash (e.g. call Eval() once per constructed position before RecognLookup) so this test can actually reach the branch it's nominally checking. Parameters: void Return value: void **/ { SEARCHER_THREAD_CONTEXT *ctx; POSITION pos; COOR cStrongKing, cWeakKing, cPawn; ULONG uToMove; SCORE iScore, iEgtb; ULONG uVal; ULONG uTotal = 0, uChecked = 0, uSkipped = 0, uWrong = 0; ULONG uReportedWrong = 0; Trace("Exhaustive KPK (1 pawn vs. lone king) verification against " "EGTB ground truth...\n"); ctx = malloc(sizeof(SEARCHER_THREAD_CONTEXT)); if (NULL == ctx) { return; } InitializeSearcherContext(&pos, ctx); FOREACH_SQUARE(cStrongKing) { if (!IS_ON_BOARD(cStrongKing)) continue; // 2026-09-18: mirror-halving restriction (FILE(cStrongKing) > // D) removed -- see _BuildKPKPosition's comment. The critical- // square lookup table this test is meant to validate is raw // hand-typed data, not derived by formula, so there's no // guarantee its A-file and H-file entries are true mirrors of // each other -- exactly the kind of transcription gap this // restriction could hide. Slower (full board instead of half) // but unambiguous. FOREACH_SQUARE(cWeakKing) { if (!IS_ON_BOARD(cWeakKing)) continue; if (DISTANCE(cStrongKing, cWeakKing) < 2) continue; FOREACH_SQUARE(cPawn) { if (!IS_ON_BOARD(cPawn)) continue; if (RANK1(cPawn) || RANK8(cPawn)) continue; if ((cPawn == cStrongKing) || (cPawn == cWeakKing)) continue; for (uToMove = BLACK; uToMove <= WHITE; uToMove++) { ULONG uColorPass; // 2026-09-18: loop both color assignments -- see // _BuildKPKPosition's comment. uColorPass 0 = // white has the pawn (original coverage), 1 = // black has the pawn (the gap that let the // fuzzer-found counterexample through undetected). for (uColorPass = 0; uColorPass < 2; uColorPass++) { uTotal++; if (0 == (uTotal % 100000)) { Trace(" ... %lu raw combos so far (%lu checked, " "%lu wrong)\n", uTotal, uChecked, uWrong); } if (FALSE == _BuildKPKPosition(&pos, cStrongKing, cWeakKing, cPawn, uToMove, (1 == uColorPass))) { continue; } ReInitializeSearcherContext(&pos, ctx); uVal = RecognLookup(ctx, &iScore, EGTB_PROBE_NEVER); if (UNRECOGNIZED == uVal) { continue; } if (FALSE == ProbeEGTB(ctx, &iEgtb)) { uSkipped++; continue; } uChecked++; if (FALSE == _RecognizerAgreesWithEGTB(iScore, uVal, iEgtb)) { uWrong++; if (uReportedWrong < 20) { Trace("MISMATCH (blackHasPawn=%lu) iScore=%d " "uVal=%lu iEgtb=%d toMove=%lu\n", uColorPass, iScore, uVal, iEgtb, pos.uToMove); DumpPosition(&pos); uReportedWrong++; } } } } } } } free(ctx); Trace("Exhaustive KPK: %lu raw combos, %lu checked against EGTB, " "%lu skipped (no EGTB coverage), %lu WRONG.\n", uTotal, uChecked, uSkipped, uWrong); } // // ============================================================================ // KP+KP* (general king-and-pawn endgame, ANY number of pawns per side) // recognizer verification, with EGTB-DTZ-based winning-line reconstruction // to classify WHY a mismatch happened. // ============================================================================ // // Everything above tests recognizers whose material shape has pawns on at // most one side (KPK) or exactly one pawn per side via fixed square // enumeration. Both sides having pawns, in general, is real king-and-pawn // endgame theory (breakthroughs, outside passers, opposition/corresponding // squares, mutual zugzwang) -- no cheap static recognizer classifies that // exhaustively. The design goal is NOT a recognizer that is always right, // but one that is never wrong: assert a bound only when a position is // provably, conservatively a clear win, and return UNRECOGNIZED (do // nothing) the moment there's any doubt. _RecognizeKPK (recogn.c) already // does exactly this today -- whenever both sides still have at least one // pawn, it currently falls straight through to UNRECOGNIZED unconditionally // (see its "no one has a winning passer" comment). Running this harness // against that baseline should show 0 wrong / 0 checked / 100% unrecognized // for the KPKP shape -- that's the correct, honest starting point. Rerun // after every incremental addition to that function's "both sides have // pawns" branch, watching the wrong-count stay at exactly 0 while the // checked-count rises. // #define EGTB_LINE_MAX_PLY (24) typedef struct _EGTB_LINE_MOVE { MOVE mv; FLAG fGivesCheck; } EGTB_LINE_MOVE; // // Follow Syzygy DTZ's own suggested move at every ply from the CURRENT // position in ctx, building a genuine, engine-verified winning line -- // not a search, just repeated GenerateMoves + ProbeEGTBRoot + MakeMove, // exactly the mechanism root.c's Iterate() uses to pick a single root // move (see ProbeEGTBRoot's own header comment). Stops at whichever // comes first: uMaxPly plies, a position with no legal moves (checkmate/ // stalemate), or ProbeEGTBRoot declining (draw/cursed-win/blessed-loss/ // out of tablebase range/no coverage at all) -- all meaningful stopping // points for a "why did the recognizer miss this" line, not failures. // Restores ctx to its original position before returning (unwinds every // move it made), so the caller can keep reusing ctx exactly as it found // it. // static ULONG _GetEgtbLine(IN OUT SEARCHER_THREAD_CONTEXT *ctx, OUT EGTB_LINE_MOVE *pLine, IN ULONG uMaxPly) { ULONG uPly; MOVE mv; SCORE iScore; FLAG fInCheck; for (uPly = 0; uPly < uMaxPly; uPly++) { fInCheck = InCheck(&ctx->sPosition, ctx->sPosition.uToMove); ctx->sPlyInfo[ctx->uPly].fInCheck = fInCheck; GenerateMoves(ctx, NULLMOVE, fInCheck ? GENERATE_ESCAPES : GENERATE_ALL_MOVES); if (ctx->sMoveStack.uBegin[ctx->uPly] == ctx->sMoveStack.uEnd[ctx->uPly]) { break; // checkmate or stalemate -- nothing more to follow } if (FALSE == ProbeEGTBRoot(ctx, &mv, &iScore)) { break; // draw/cursed/blessed/out of range/no coverage } pLine[uPly].mv = mv; if (FALSE == MakeMove(ctx, mv)) { ASSERT(FALSE); break; } pLine[uPly].fGivesCheck = InCheck(&ctx->sPosition, ctx->sPosition.uToMove); } // Unwind back to the original position, reverse order. { ULONG u = uPly; while (u > 0) { u--; UnmakeMove(ctx, pLine[u].mv); } } return(uPly); } // // Print an EGTB-DTZ line built by _GetEgtbLine in coordinate notation, // tagging each move with a cheap, mechanical classification (capture / // check / promotion / which piece moved) so a human scanning a batch of // mismatches can group them by pattern -- e.g. "every miss here starts // with a king move, not a pawn push" points straight at a king-escort/ // opposition case; "starts with a capture" points at a tactical shot the // margin-based race logic has no way to see; "promotes with check" flags // exactly the family named in the original request. Deliberately NOT // trying to name the endgame-theory concept itself (opposition, // corresponding squares, breakthrough) -- that's for a human reading the // tagged line plus the position dump, not this mechanical tagger. // static void _PrintEgtbLine(IN EGTB_LINE_MOVE *pLine, IN ULONG uLen) { ULONG u; const CHAR *pszPiece; MOVE mv; if (0 == uLen) { Trace(" (no EGTB line available -- draw/cursed/blessed/out of " "tablebase range/no coverage/terminal already)\n"); return; } for (u = 0; u < uLen; u++) { mv = pLine[u].mv; pszPiece = IS_KING(mv.pMoved) ? "K" : (IS_PAWN(mv.pMoved) ? "P" : "?"); Trace(" %2lu. %s%s%s%s%s\n", u + 1, pszPiece, MoveToIcs(mv), mv.pCaptured ? " (capture)" : "", mv.pPromoted ? " (promotes)" : "", pLine[u].fGivesCheck ? " (check)" : ""); } } // // Classification of the FIRST move of a mismatch's EGTB line -- the // single most useful signal for bucketing misses, since a recognizer's // verdict is computed BEFORE any move is made, so whatever the correct // first move turns out to be is exactly what that logic failed to // anticipate. // static const CHAR * _ClassifyFirstMove(IN EGTB_LINE_MOVE *pLine, IN ULONG uLen) { MOVE mv; if (0 == uLen) { return("no-line"); } mv = pLine[0].mv; if (mv.pPromoted) { return(pLine[0].fGivesCheck ? "promotes-with-check" : "promotes"); } if (mv.pCaptured) { return("capture"); } if (pLine[0].fGivesCheck) { return("check"); } if (IS_KING(mv.pMoved)) { return("king-move"); } if (IS_PAWN(mv.pMoved)) { return("pawn-push"); } return("other"); } // // Shared "we found a wrong verdict" reporting path for the KPvKP tests // below: dump the position, the recognizer's claim vs. EGTB's, the // mechanical first-move classification, and the full EGTB-DTZ line. ctx // must already be ReInitializeSearcherContext'd on pos (the failing // position) when this is called -- _GetEgtbLine needs a live, correctly // positioned ctx to generate moves and probe from. // static void _ReportRecognMismatch(IN OUT SEARCHER_THREAD_CONTEXT *ctx, IN POSITION *pos, IN const CHAR *pszCaseName, IN SCORE iRecScore, IN ULONG uVal, IN SCORE iEgtb) { EGTB_LINE_MOVE rgLine[EGTB_LINE_MAX_PLY]; ULONG uLen; Trace("MISMATCH [%s]: recognizer=%d (uVal=%lu) EGTB=%d toMove=%lu\n", pszCaseName, iRecScore, uVal, iEgtb, pos->uToMove); DumpPosition(pos); uLen = _GetEgtbLine(ctx, rgLine, EGTB_LINE_MAX_PLY); Trace(" first winning move classified as: %s\n", _ClassifyFirstMove(rgLine, uLen)); _PrintEgtbLine(rgLine, uLen); } // // Build a KP vs KP (exactly one pawn per side, otherwise bare kings) // position directly from square placements. // static FLAG _BuildKPvKPPosition(OUT POSITION *pos, IN COOR cWhiteKing, IN COOR cBlackKing, IN COOR cWhitePawn, IN COOR cBlackPawn, IN ULONG uToMove) { PIECE pBoard[128]; CHAR szFen[256]; CHAR *p; ULONG x; memset(pBoard, 0, sizeof(pBoard)); pBoard[cWhiteKing] = WHITE_KING; pBoard[cBlackKing] = BLACK_KING; pBoard[cWhitePawn] = WHITE_PAWN; pBoard[cBlackPawn] = 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 TestRecognExhaustiveKPvKP(void) /** Routine description: Exhaustively enumerate every legal KP vs KP (exactly one pawn per side, otherwise bare kings) position and verify RecognLookup's verdict against direct EGTB probes wherever coverage exists. This is the smallest slice of the general "both sides have pawns" shape and the only one small enough (~10^7 raw combos) to fully exhaust rather than sample -- the acceptance bar any new "both sides have pawns" detection logic added to _RecognizeKPK should clear before being trusted at all: zero wrong verdicts across the complete space, not "zero wrong in N random samples" (exactly the false confidence that let the pre-2026-09-16 KPK recognizer and the eval.c "general KPvKP" heuristic both ship with real bugs -- see chess.h/eval.c's writeups). On-demand, not part of the automatic startup suite -- same precedent as TestRecognExhaustiveKPK/KNKP. Parameters: void Return value: void **/ { SEARCHER_THREAD_CONTEXT *ctx; POSITION pos; COOR cWhiteKing, cBlackKing, cWhitePawn, cBlackPawn; ULONG uToMove; SCORE iScore, iEgtb; ULONG uVal; ULONG uTotal = 0, uChecked = 0, uSkipped = 0, uUnrecognized = 0, uWrong = 0; ULONG uReportedWrong = 0; Trace("Exhaustive KP vs KP (one pawn each) recognizer verification " "against EGTB -- this takes a while...\n"); ctx = malloc(sizeof(SEARCHER_THREAD_CONTEXT)); if (NULL == ctx) { return; } InitializeSearcherContext(&pos, ctx); FOREACH_SQUARE(cWhiteKing) { if (!IS_ON_BOARD(cWhiteKing)) continue; FOREACH_SQUARE(cBlackKing) { if (!IS_ON_BOARD(cBlackKing)) continue; if (DISTANCE(cWhiteKing, cBlackKing) < 2) continue; FOREACH_SQUARE(cWhitePawn) { if (!IS_ON_BOARD(cWhitePawn)) continue; if (RANK1(cWhitePawn) || RANK8(cWhitePawn)) continue; if ((cWhitePawn == cWhiteKing) || (cWhitePawn == cBlackKing)) continue; FOREACH_SQUARE(cBlackPawn) { if (!IS_ON_BOARD(cBlackPawn)) continue; if (RANK1(cBlackPawn) || RANK8(cBlackPawn)) continue; if ((cBlackPawn == cWhiteKing) || (cBlackPawn == cBlackKing) || (cBlackPawn == cWhitePawn)) continue; for (uToMove = BLACK; uToMove <= WHITE; uToMove++) { uTotal++; if (0 == (uTotal % 1000000)) { Trace(" ... %lu raw combos so far (%lu checked, " "%lu wrong)\n", uTotal, uChecked, uWrong); } if (FALSE == _BuildKPvKPPosition(&pos, cWhiteKing, cBlackKing, cWhitePawn, cBlackPawn, uToMove)) { continue; } ReInitializeSearcherContext(&pos, ctx); uVal = RecognLookup(ctx, &iScore, EGTB_PROBE_NEVER); if (UNRECOGNIZED == uVal) { uUnrecognized++; continue; } if (FALSE == ProbeEGTB(ctx, &iEgtb)) { uSkipped++; continue; } uChecked++; if (FALSE == _RecognizerAgreesWithEGTB(iScore, uVal, iEgtb)) { uWrong++; if (uReportedWrong < 20) { ReInitializeSearcherContext(&pos, ctx); _ReportRecognMismatch(ctx, &pos, "KPvKP-exh", iScore, uVal, iEgtb); uReportedWrong++; } } } } } } } free(ctx); Trace("Exhaustive KPvKP: %lu raw combos, %lu checked against EGTB, " "%lu skipped (no EGTB coverage), %lu unrecognized, %lu WRONG.\n", uTotal, uChecked, uSkipped, uUnrecognized, uWrong); } // // ============================================================================ // eval.c's _EvalPasserRacesAgainstLoneKings EGTB cross-check. // ============================================================================ // // Same "how dodgy is this, really" question the 2026-09-16 KPvKP // investigation asked of recogn.c's KPK/KPvKP recognizers, now asked // of the eval.c heuristic that fills the gap when both sides are pure // KPvKP but at least one side still has pawns -- the exact case // _EvalUnstoppablePasser (eval.c) deliberately stays silent on and // falls back to this function for (see that function's own header // comment). This is a SOFT eval nudge, not a recogn.c hard bound (a // wrong verdict biases a score, never corrupts a search bound), but // it's a much louder nudge than SideHasWinningChances's fortress // carve-out (RACER_WINS_RACE is +300, most of a minor piece) so it's // worth knowing its real false-positive rate rather than assuming // "dodgy" means "ignorable." // // Uses the exact same board-building code as // TestRecognExhaustiveKPvKP (_BuildKPvKPPosition) -- same enumeration, // same EGTB ground truth, different function under test -- so a // side-by-side comparison of the two WRONG counts is directly // meaningful. // // Unlike RecognLookup's uVal/iScore (side-to-move-relative, // LOWER/UPPER/EXACT bound semantics), _EvalPasserRacesAgainstLoneKings // writes directly into pos->iScore[WHITE]/[BLACK] (absolute color, // unconditional "this color is winning the race" credit -- always a // positive RACER_WINS_RACE-derived add, never negative, so whichever // color's iScore moved off zero is the color it claimed). A "wrong" // verdict here means it claimed a color is winning an unstoppable // pawn race when EGTB says that color does NOT win (a draw, or the // other color wins) -- the one direction that actually matters, since // a false "this side has no race credit" just falls through to // ordinary positional eval terms rather than asserting anything false. // void TestEvalPasserRacesAgainstLoneKings(void) /** Routine description: Exhaustively enumerate every legal KP vs KP (exactly one pawn per side, otherwise bare kings) position -- same space as TestRecognExhaustiveKPvKP -- and cross-check _EvalPasserRacesAgainstLoneKings's verdict (via a real, Eval()-populated PAWN_HASH_ENTRY from EvalPawns) against direct EGTB probes wherever coverage exists. On-demand, not part of the automatic startup suite -- same precedent as TestRecognExhaustiveKPvKP. Parameters: void Return value: void **/ { SEARCHER_THREAD_CONTEXT *ctx; POSITION pos; COOR cWhiteKing, cBlackKing, cWhitePawn, cBlackPawn; ULONG uToMove, uClaimedColor; SCORE iEgtb; FLAG fDeferred, fFired; PAWN_HASH_ENTRY *pHash; ULONG uTotal = 0, uChecked = 0, uSkipped = 0, uFired = 0; ULONG uWrong = 0, uMissed = 0; ULONG uReportedWrong = 0; Trace("Exhaustive KP vs KP (one pawn each) _EvalPasserRacesAgainst" "LoneKings verification against EGTB -- this takes a " "while...\n"); ctx = malloc(sizeof(SEARCHER_THREAD_CONTEXT)); if (NULL == ctx) { return; } InitializeSearcherContext(&pos, ctx); FOREACH_SQUARE(cWhiteKing) { if (!IS_ON_BOARD(cWhiteKing)) continue; FOREACH_SQUARE(cBlackKing) { if (!IS_ON_BOARD(cBlackKing)) continue; if (DISTANCE(cWhiteKing, cBlackKing) < 2) continue; FOREACH_SQUARE(cWhitePawn) { if (!IS_ON_BOARD(cWhitePawn)) continue; if (RANK1(cWhitePawn) || RANK8(cWhitePawn)) continue; if ((cWhitePawn == cWhiteKing) || (cWhitePawn == cBlackKing)) continue; FOREACH_SQUARE(cBlackPawn) { if (!IS_ON_BOARD(cBlackPawn)) continue; if (RANK1(cBlackPawn) || RANK8(cBlackPawn)) continue; if ((cBlackPawn == cWhiteKing) || (cBlackPawn == cBlackKing) || (cBlackPawn == cWhitePawn)) continue; for (uToMove = BLACK; uToMove <= WHITE; uToMove++) { uTotal++; if (0 == (uTotal % 1000000)) { Trace(" ... %lu raw combos so far (%lu fired, " "%lu wrong, %lu missed)\n", uTotal, uFired, uWrong, uMissed); } if (FALSE == _BuildKPvKPPosition(&pos, cWhiteKing, cBlackKing, cWhitePawn, cBlackPawn, uToMove)) { continue; } ReInitializeSearcherContext(&pos, ctx); ctx->sPosition.iScore[WHITE] = 0; ctx->sPosition.iScore[BLACK] = 0; // ReInitializeSearcherContext doesn't touch // sPlyInfo, so ctx->sPlyInfo[ctx->uPly].fInCheck // would otherwise be stale -- establish it // explicitly, same fix command.c's EvalCommand/ // EvalDetailCommand needed. _EvalPasserRacesAgainst // LoneKings now takes ctx directly (not pos) and // reads this field to bail out early on in-check // positions. ctx->sPlyInfo[ctx->uPly].fInCheck = InCheck(&ctx->sPosition, ctx->sPosition.uToMove); pHash = EvalPawns(ctx, &fDeferred); fFired = _EvalPasserRacesAgainstLoneKings(ctx, pHash); if (FALSE == ProbeEGTB(ctx, &iEgtb)) { uSkipped++; continue; } uChecked++; // iEgtb is relative to side to move; convert to // an absolute-color winner (or DRAW). if (FALSE == fFired) { if (0 != iEgtb) { uMissed++; } continue; } uFired++; uClaimedColor = (ctx->sPosition.iScore[WHITE] != 0) ? WHITE : BLACK; ASSERT(ctx->sPosition.iScore[uClaimedColor] > 0); if ((0 == iEgtb) || (((iEgtb > 0) ? ctx->sPosition.uToMove : FLIP(ctx->sPosition.uToMove)) != uClaimedColor)) { uWrong++; if (uReportedWrong < 20) { Trace("MISMATCH [EvalPasserRace-exh]: " "claimed=%s EGTB(stm-relative)=%d " "toMove=%s\n", COLOR_NAME(uClaimedColor), iEgtb, COLOR_NAME(ctx->sPosition.uToMove)); _ReportRecognMismatch(ctx, &pos, "EvalPasserRace-exh", ctx->sPosition.iScore[ uClaimedColor], RECOGN_LOWER, iEgtb); uReportedWrong++; } } } } } } } free(ctx); // // %u, not %lu: ULONG is `unsigned int` (32-bit) here, not // `unsigned long`. With 6 integer varargs, the first 5 land in // registers (SysV x86-64 ABI zero-extends a 32-bit register move, // masking a %lu/ULONG width mismatch) but the 6th spills to the // stack un-zero-extended -- %lu then reads 8 bytes and prints // garbage in the upper 32 bits. Found live: this printed "8599473172 // missed" on a run where uMissed was actually ~14M. Matching the // format specifier to the real type is the fix, not trimming the // argument count. // Trace("Exhaustive _EvalPasserRacesAgainstLoneKings: %u raw combos, " "%u checked against EGTB, %u skipped (no EGTB coverage), " "%u fired, %u WRONG (claimed a win that wasn't), %u missed " "(a real win it said nothing about).\n", uTotal, uChecked, uSkipped, uFired, uWrong, uMissed); } // // Random-sampling cross-check of _EvalPasserRacesAgainstLoneKings for // multi-pawn-per-side KP+KP* shapes (KPPKPP, KPKPPP, ...) that are far // too large to enumerate exhaustively (unlike the exact-one-pawn-each // KPvKP shape above, which is small enough to exhaust at ~15M raw // combos). This is what recogn.c:1635 (_RecognizeKPK) actually needs // validated before it could ever trust _EvalPasserRacesAgainstLoneKings // as a hard RECOGN_LOWER/UPPER bound when the "losing" side still has // pawns of its own -- today it only trusts a verdict when the losing // side has ZERO pawns left, specifically because that broader multi- // pawn shape was never validated. Requires 6-man Syzygy WDL coverage // for the 2-pawns-per-side case (KPPvKPP is 2 kings + 4 pawns = 6 men) // -- confirmed present on this box (matching .rtbw/.rtbz counts under // /zscratch/egtb) even though CLAUDE.md's older note says 6-man WDL is // missing; that note is stale, verified stale by directly diffing the // .rtbw/.rtbz basename sets rather than trusting the note. // static void _TestEvalSamplePasserRaces(IN ULONG uPawnA, IN ULONG uPawnB, IN ULONG uIterations, IN const CHAR *pszLabel) { SEARCHER_THREAD_CONTEXT *ctx; POSITION pos; ULONG uIter, uClaimedColor, uLoserColor, uLoserPawns, uWinnerPawns; SCORE iEgtb; FLAG fDeferred, fFired; PAWN_HASH_ENTRY *pHash; ULONG uChecked = 0, uSkipped = 0, uFired = 0, uWrong = 0, uUnrecognized = 0; ULONG uReportedWrong = 0; // Classify each wrong verdict by which side had 2+ pawns -- lets us // tell whether the LOSER's extra pawns (connected/backup passer // counterplay this per-pawn scan can't see) or the WINNER's extra // pawns (e.g. picking the wrong one of several candidates) is the // actual driver, rather than guessing at a fix. ULONG uWrongLoserMulti = 0, uWrongWinnerMulti = 0, uWrongBothMulti = 0, uWrongNeitherMulti = 0; // Does the true winner (per EGTB -- the side our verdict called // the LOSER) even have a pawn flagged "passed" at all? If not, the // real threat was invisible to this function from the start (a // detection gap upstream of any comparison logic here); if so, it // WAS visible but still lost the comparison/veto to the wrongly- // credited side (a bug in this function's own arithmetic). ULONG uWrongTrueWinnerNoPasser = 0, uWrongTrueWinnerHasPasser = 0; Trace("Random-sampling %s _EvalPasserRacesAgainstLoneKings against " "EGTB ground truth -- %u iterations...\n", pszLabel, uIterations); ctx = malloc(sizeof(SEARCHER_THREAD_CONTEXT)); if (NULL == ctx) { return; } InitializeSearcherContext(&pos, ctx); for (uIter = 0; uIter < uIterations; uIter++) { if ((uIter != 0) && (0 == (uIter % 500000))) { Trace(" ... %u iterations so far (%u fired, %u wrong, " "%u skipped, %u unrecognized)\n", uIter, uFired, uWrong, uSkipped, uUnrecognized); } _MakeRandomEndgame(&pos, uPawnA, 0, 0, uPawnB, 0, 0); ReInitializeSearcherContext(&pos, ctx); ctx->sPosition.iScore[WHITE] = 0; ctx->sPosition.iScore[BLACK] = 0; ctx->sPlyInfo[ctx->uPly].fInCheck = InCheck(&ctx->sPosition, ctx->sPosition.uToMove); pHash = EvalPawns(ctx, &fDeferred); fFired = _EvalPasserRacesAgainstLoneKings(ctx, pHash); if (FALSE == fFired) { uUnrecognized++; continue; } if (FALSE == ProbeEGTB(ctx, &iEgtb)) { uSkipped++; continue; } uChecked++; uFired++; uClaimedColor = (ctx->sPosition.iScore[WHITE] != 0) ? WHITE : BLACK; ASSERT(ctx->sPosition.iScore[uClaimedColor] > 0); if ((0 == iEgtb) || (((iEgtb > 0) ? ctx->sPosition.uToMove : FLIP(ctx->sPosition.uToMove)) != uClaimedColor)) { uWrong++; uLoserColor = FLIP(uClaimedColor); uWinnerPawns = FastCountBits(ctx->sPosition.bbPawns[uClaimedColor]); uLoserPawns = FastCountBits(ctx->sPosition.bbPawns[uLoserColor]); if ((uLoserPawns > 1) && (uWinnerPawns > 1)) { uWrongBothMulti++; } else if (uLoserPawns > 1) { uWrongLoserMulti++; } else if (uWinnerPawns > 1) { uWrongWinnerMulti++; } else { uWrongNeitherMulti++; } // uLoserColor here is the side OUR verdict didn't credit -- // since the verdict is wrong, that's actually the TRUE // winner per EGTB. Did it even have a pawn flagged passed? if (0 == pHash->bbPasserLocations[uLoserColor]) { uWrongTrueWinnerNoPasser++; } else { uWrongTrueWinnerHasPasser++; if (uReportedWrong < 20) { Trace(" (true winner %s DID have a flagged passer -- " "comparison/veto bug, not a detection gap)\n", COLOR_NAME(uLoserColor)); } } if (uReportedWrong < 20) { Trace("MISMATCH [%s]: claimed=%s (winner pawns=%u) " "loser=%s (loser pawns=%u) EGTB(stm-relative)=%d " "toMove=%s\n", pszLabel, COLOR_NAME(uClaimedColor), uWinnerPawns, COLOR_NAME(uLoserColor), uLoserPawns, iEgtb, COLOR_NAME(ctx->sPosition.uToMove)); _ReportRecognMismatch(ctx, &pos, pszLabel, ctx->sPosition.iScore[uClaimedColor], RECOGN_LOWER, iEgtb); uReportedWrong++; } } } free(ctx); Trace("%s sampling: %u iterations, %u checked against EGTB, " "%u fired, %u WRONG (claimed a win that wasn't), %u skipped " "(no EGTB coverage), %u unrecognized (didn't fire).\n", pszLabel, uIterations, uChecked, uFired, uWrong, uSkipped, uUnrecognized); Trace("%s wrong breakdown: %u loser-has-2+-pawns-only, " "%u winner-has-2+-pawns-only, %u both-have-2+-pawns, " "%u neither (both <=1 pawn -- shouldn't happen for a shape " "with a multi-pawn side, worth investigating if nonzero).\n", pszLabel, uWrongLoserMulti, uWrongWinnerMulti, uWrongBothMulti, uWrongNeitherMulti); Trace("%s wrong mechanism: %u true-winner-had-no-flagged-passer " "(detection gap -- the real threat was invisible to this " "function), %u true-winner-DID-have-a-flagged-passer " "(comparison/veto bug -- the info was there and still lost).\n", pszLabel, uWrongTrueWinnerNoPasser, uWrongTrueWinnerHasPasser); } void TestEvalSamplePasserRacesKPPKPP(void) /** Routine description: Random-sampling _EvalPasserRacesAgainstLoneKings cross-check for KPPKPP (2 pawns each side, 6 men total -- needs 6-man WDL Syzygy coverage). On-demand, not part of the automatic startup suite. Parameters: void Return value: void **/ { _TestEvalSamplePasserRaces(2, 2, 1000000, "KPPKPP"); } void TestEvalSamplePasserRacesKPKPPP(void) /** Routine description: Random-sampling _EvalPasserRacesAgainstLoneKings cross-check for KPKPPP (1 pawn one side, 3 the other -- 6 men total, asymmetric pawn-count shape). _MakeRandomEndgame randomly assigns which color gets which count, so a single run exercises both KPKPPP and KPPPKP. On-demand, not part of the automatic startup suite. Parameters: void Return value: void **/ { _TestEvalSamplePasserRaces(1, 3, 1000000, "KPKPPP"); } #define RECOGN_KPVKP_MAX_PAWNS_PER_SIDE (4) #define RECOGN_KPVKP_SAMPLE_ITERATIONS (20000) void TestRecognSampleKPvKP(void) /** Routine description: Random-sample the general "both sides have pawns" (KPKP material signature, any pawn count 1..RECOGN_KPVKP_MAX_PAWNS_PER_SIDE each side -- recogn.c's material-signature dispatch is presence-based, not count-based, so one registered "pawns only, both sides" recognizer already covers every pawn count via this single signature) shape, cross-checking RecognLookup's verdict against EGTB wherever coverage exists. Exhaustive enumeration is only tractable for exactly one pawn per side (TestRecognExhaustiveKPvKP above) -- beyond that this is sampling, not exhaustion, so a clean run here is evidence, not proof; keep TestRecognExhaustiveKPvKP as the actual acceptance bar and treat this as a wider (if shallower) smoke net, especially useful for 2-3 pawns/side where EGTB WDL coverage still fully exists (this box has full 5-man WDL+DTZ; 3 total pawns + 2 kings = 5 men) even though exhaustive enumeration of that space is not attempted. Every mismatch gets the same EGTB-DTZ winning-line reconstruction and first-move classification as the exhaustive test (see _ReportRecognMismatch) -- this is what actually matters for a material shape too large to enumerate: a handful of concrete, classified counterexamples to fix and turn into permanent regression FENs (see g_rgRecognFenCases above), not just a wrong count. On-demand, not part of the automatic startup suite. Parameters: void Return value: void **/ { SEARCHER_THREAD_CONTEXT *ctx; POSITION pos; ULONG uIter, uPawnA, uPawnB; SCORE iScore, iEgtb; ULONG uVal; ULONG uChecked = 0, uSkipped = 0, uUnrecognized = 0, uWrong = 0; ULONG uReportedWrong = 0; Trace("Random-sampling KP+KP* (both sides have pawns, up to %u each) " "against EGTB ground truth -- %u iterations...\n", RECOGN_KPVKP_MAX_PAWNS_PER_SIDE, RECOGN_KPVKP_SAMPLE_ITERATIONS); ctx = malloc(sizeof(SEARCHER_THREAD_CONTEXT)); if (NULL == ctx) { return; } InitializeSearcherContext(&pos, ctx); for (uIter = 0; uIter < RECOGN_KPVKP_SAMPLE_ITERATIONS; uIter++) { uPawnA = 1 + (rand() % RECOGN_KPVKP_MAX_PAWNS_PER_SIDE); uPawnB = 1 + (rand() % RECOGN_KPVKP_MAX_PAWNS_PER_SIDE); _MakeRandomEndgame(&pos, uPawnA, 0, 0, uPawnB, 0, 0); ReInitializeSearcherContext(&pos, ctx); uVal = RecognLookup(ctx, &iScore, EGTB_PROBE_NEVER); if (UNRECOGNIZED == uVal) { uUnrecognized++; continue; } if (FALSE == ProbeEGTB(ctx, &iEgtb)) { uSkipped++; continue; } uChecked++; if (FALSE == _RecognizerAgreesWithEGTB(iScore, uVal, iEgtb)) { uWrong++; if (uReportedWrong < 20) { ReInitializeSearcherContext(&pos, ctx); _ReportRecognMismatch(ctx, &pos, "KPKP-sample", iScore, uVal, iEgtb); uReportedWrong++; } } } free(ctx); Trace("KP+KP* sampling: %u iterations, %lu checked against EGTB, " "%lu skipped (no EGTB coverage), %lu unrecognized, %lu WRONG.\n", RECOGN_KPVKP_SAMPLE_ITERATIONS, uChecked, uSkipped, uUnrecognized, uWrong); if (0 == uChecked) { Trace("WARNING: zero positions were actually cross-checked against " "EGTB.\n"); } } // // ============================================================================ // Wrong-bishop-rook-pawn fortress (eval.c's SideHasWinningChances) // EGTB cross-check. // ============================================================================ // // Unlike every recogn.c check above, this is a SOFT eval.c heuristic -- // wrong answers only bias a score, never corrupt a search bound -- so // the acceptance bar is different: not "zero wrong, ever" but "measure // the false-positive rate and confirm it's the kind of small, tolerable // number the eval.c comment above SideHasWinningChances expects," the // same spirit as this session's original KPvKP investigation but with // a softer landing. A "false positive" here means SideHasWinningChances // claimed uStrong has NO winning chances (the fortress verdict) but // EGTB shows uStrong actually wins -- the direction that would make the // eval wrongly pessimistic about a real win, the only direction that // matters for a heuristic that's only ever used to softenscores, not to // claim a win. // #define WRONG_BISHOP_SAMPLE_ITERATIONS (20000) // // Build a random KBP+K position: uStrong has a king, one bishop, and // uPawnCount pawns all on a single file (uFile) -- the exact shape // SideHasWinningChances' fortress check looks at (a rook-file pawn // mass, doubled/tripled included, since the check itself never // verifies path-clearness or how many pawns share the file). uWeak has // only a bare king. // static FLAG _TryBuildWrongBishopPosition(OUT POSITION *pos, IN ULONG uStrong, IN ULONG uFile, IN ULONG uPawnCount) { PIECE pBoard[128]; CHAR szFen[256]; CHAR *p; COOR cStrongKing, cWeakKing, cBishop, cPawn; ULONG uWeak = FLIP(uStrong); ULONG u, x; FLAG fUsedRank[9]; memset(pBoard, 0, sizeof(pBoard)); memset(fUsedRank, 0, sizeof(fUsedRank)); do { cStrongKing = RANDOM_COOR; } while (!IS_ON_BOARD(cStrongKing)); do { cWeakKing = RANDOM_COOR; } while ((!IS_ON_BOARD(cWeakKing)) || (DISTANCE(cStrongKing, cWeakKing) < 2)); pBoard[cStrongKing] = (WHITE == uStrong) ? WHITE_KING : BLACK_KING; pBoard[cWeakKing] = (WHITE == uWeak) ? WHITE_KING : BLACK_KING; do { cBishop = RANDOM_COOR; } while ((!IS_ON_BOARD(cBishop)) || (0 != pBoard[cBishop])); pBoard[cBishop] = (WHITE == uStrong) ? WHITE_BISHOP : BLACK_BISHOP; for (u = 0; u < uPawnCount; u++) { ULONG uRank; ULONG uTries = 0; do { uRank = 2 + (rand() % 6); // 2..7 uTries++; } while (fUsedRank[uRank] && (uTries < 100)); if (fUsedRank[uRank]) { return(FALSE); // ran out of distinct ranks on this file } fUsedRank[uRank] = TRUE; cPawn = FILE_RANK_TO_COOR(uFile, uRank); if (0 != pBoard[cPawn]) { return(FALSE); } pBoard[cPawn] = (WHITE == uStrong) ? WHITE_PAWN : 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++ = (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); } void TestEvalWrongBishopFortress(void) /** Routine description: Random-sample the wrong-bishop-rook-pawn-fortress shape (KBP+ vs bare K, all pawns on one rook file) and measure how often SideHasWinningChances' fortress claim (uStrong has no winning chances) disagrees with EGTB in the direction that matters -- EGTB actually showing uStrong winning. This is a SOFT eval.c heuristic, not a recogn.c hard bound, so the goal is measuring a tolerable false-positive rate, not proving zero wrong. On-demand, not part of the automatic startup suite. Parameters: void Return value: void **/ { SEARCHER_THREAD_CONTEXT *ctx; POSITION pos; ULONG uIter, uStrong, uFile, uPawnCount; SCORE iEgtb, iRecScore; ULONG uClaimedFortress = 0, uChecked = 0, uSkipped = 0, uWrong = 0; ULONG uReportedWrong = 0; ULONG uRecognClaimed = 0, uRecognChecked = 0, uRecognSkipped = 0; ULONG uRecognWrong = 0, uRecognReportedWrong = 0; Trace("Random-sampling wrong-bishop-rook-pawn-fortress claims against " "EGTB -- %u iterations...\n", WRONG_BISHOP_SAMPLE_ITERATIONS); ctx = malloc(sizeof(SEARCHER_THREAD_CONTEXT)); if (NULL == ctx) { return; } InitializeSearcherContext(&pos, ctx); for (uIter = 0; uIter < WRONG_BISHOP_SAMPLE_ITERATIONS; uIter++) { uStrong = RANDOM_COLOR; uFile = (rand() & 1) ? A : H; uPawnCount = 1 + (rand() % 3); if (FALSE == _TryBuildWrongBishopPosition(&pos, uStrong, uFile, uPawnCount)) { continue; } ReInitializeSearcherContext(&pos, ctx); if (FALSE == SideHasWinningChances(&pos, uStrong)) { uClaimedFortress++; if (FALSE == ProbeEGTB(ctx, &iEgtb)) { uSkipped++; continue; } uChecked++; { FLAG fStrongWins = (pos.uToMove == uStrong) ? (iEgtb > 0) : (iEgtb < 0); if (TRUE == fStrongWins) { uWrong++; if (uReportedWrong < 20) { Trace("FALSE POSITIVE: fortress claimed for %lu but " "EGTB says %lu wins (iEgtb=%d toMove=%lu)\n", uStrong, uStrong, iEgtb, pos.uToMove); DumpPosition(&pos); uReportedWrong++; } } } } // // Same position, cross-checking recogn.c's OWN pre-existing // wrong-bishop hard bound (_RecognizeKBKP's bare-weak-king // branch, a flat DISTANCE<=1 check with NO tempo credit -- // written before this session's tempo-boundary fix to the // eval.c copy and never itself re-verified against EGTB in // this investigation) via the real RecognLookup dispatch, not // a direct static call -- exercises the actual material- // signature routing a real search would use. // { ULONG uVal = RecognLookup(ctx, &iRecScore, EGTB_PROBE_NEVER); if (UNRECOGNIZED != uVal) { uRecognClaimed++; if (TRUE == ProbeEGTB(ctx, &iEgtb)) { uRecognChecked++; if (FALSE == _RecognizerAgreesWithEGTB(iRecScore, uVal, iEgtb)) { uRecognWrong++; if (uRecognReportedWrong < 300) { Trace("recogn.c MISMATCH: iRecScore=%d uVal=%lu " "iEgtb=%d toMove=%lu\n", iRecScore, uVal, iEgtb, pos.uToMove); DumpPosition(&pos); uRecognReportedWrong++; } } } else { uRecognSkipped++; } } } } free(ctx); Trace("recogn.c's existing _RecognizeKBKP wrong-bishop hard bound: " "%lu claimed, %lu checked against EGTB, %lu skipped (no " "coverage), %lu WRONG.\n", uRecognClaimed, uRecognChecked, uRecognSkipped, uRecognWrong); Trace("Wrong-bishop fortress: %u iterations, %lu claimed fortress, " "%lu checked against EGTB, %lu skipped (no coverage), " "%lu WRONG (false positives).\n", WRONG_BISHOP_SAMPLE_ITERATIONS, uClaimedFortress, uChecked, uSkipped, uWrong); if (uChecked > 0) { Trace(" False-positive rate: %.2f%%\n", 100.0 * (double)uWrong / (double)uChecked); } } #endif // TEST