diff options
| author | Scott Gasch <[email protected]> | 2026-09-23 15:12:10 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-09-23 15:12:10 -0700 |
| commit | f0b56611d8ef5ce2d121f532931de0252628d995 (patch) | |
| tree | 9db1896f7f8308b28adc500853e440f0f5122d8e /src/testrecogn.c | |
| parent | b7e4cdfeb3c1aadb3f488f6794e6d1a748dc6c8f (diff) | |
Real chess knowledge in the KPK[P] recognizer. Sound (but not perfect)
knowledge in eval's EvalRacersAgainstLongKing code. Other minor changes.
Even in self-play.
Diffstat (limited to 'src/testrecogn.c')
| -rw-r--r-- | src/testrecogn.c | 1175 |
1 files changed, 1175 insertions, 0 deletions
diff --git a/src/testrecogn.c b/src/testrecogn.c index d320394..b64643e 100644 --- a/src/testrecogn.c +++ b/src/testrecogn.c @@ -882,4 +882,1179 @@ Return value: 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, FALSE); + 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, FALSE); + 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, FALSE); + 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 |
