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 | |
| 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.
| -rwxr-xr-x | src/chess.h | 62 | ||||
| -rw-r--r-- | src/eval.c | 697 | ||||
| -rw-r--r-- | src/main.c | 5 | ||||
| -rw-r--r-- | src/recogn.c | 813 | ||||
| -rw-r--r-- | src/testrecogn.c | 1175 |
5 files changed, 2443 insertions, 309 deletions
diff --git a/src/chess.h b/src/chess.h index 8d9a8d8..72289ad 100755 --- a/src/chess.h +++ b/src/chess.h @@ -3630,10 +3630,26 @@ InitEval(); SCORE Eval(SEARCHER_THREAD_CONTEXT *, SCORE, SCORE, SCORE (*)[2]); +// Exposed (non-static) so testrecogn.c can EGTB-cross-check the +// wrong-bishop-rook-pawn-fortress shortcut inside it -- see this +// function's own header comment in eval.c for why that specific case +// is fine to be occasionally wrong here (a soft eval nudge) in a way +// it would not be as a recogn.c hard bound. FLAG -_EvalPasserRacesAgainstLoneKings(POSITION *, +SideHasWinningChances(POSITION *pos, ULONG uSide); + +FLAG +_EvalPasserRacesAgainstLoneKings(SEARCHER_THREAD_CONTEXT *, PAWN_HASH_ENTRY *); +// Exposed (non-static) so testrecogn.c can build a real, +// fully-populated PAWN_HASH_ENTRY (bbPasserLocations included) the +// same way Eval() itself does, in order to EGTB-cross-check +// _EvalPasserRacesAgainstLoneKings directly rather than guessing at +// its accuracy. +PAWN_HASH_ENTRY * +EvalPawns(SEARCHER_THREAD_CONTEXT *ctx, FLAG *pfDeferred); + ULONG CountKingSafetyDefects(POSITION *pos, ULONG uSide); @@ -4211,6 +4227,24 @@ TestRecognExhaustiveKNKP(void); void TestRecognExhaustiveKPK(void); +void +TestRecognExhaustiveKPvKP(void); + +void +TestRecognSampleKPvKP(void); + +void +TestEvalWrongBishopFortress(void); + +void +TestEvalPasserRacesAgainstLoneKings(void); + +void +TestEvalSamplePasserRacesKPPKPP(void); + +void +TestEvalSamplePasserRacesKPKPPP(void); + // // probe.c // @@ -4299,6 +4333,32 @@ RecognLookup(SEARCHER_THREAD_CONTEXT *ctx, FLAG fProbeEGTB); // +// Shared, ctx-free KP+KP* "unstoppable passer" static analysis -- +// deliberately touches no move generation (see recogn.c's +// IsStaticSafeQueeningPawn for why) so it's safe to call from eval.c's +// Eval() as well as recogn.c's own hard-bound recognizer. See that +// function's comment for the full rationale and the trust boundary +// between recogn.c (must never be wrong) and eval.c (a soft bonus, +// allowed to occasionally miss what recogn.c's fuller, +// move-generation-assisted version catches). +// +FLAG +IsStaticSafeQueeningPawn(POSITION *pos, ULONG uColor, COOR cPawn); + +FLAG +HasStaticUnstoppablePasser(POSITION *pos, ULONG uColor, COOR *pcPawn); + +// +// True-lone-king (weak side has zero pawns) "rule of the square + king +// escort" analysis -- exhaustively verified via TestRecognExhaustiveKPK +// (0 wrong across all 165,676 EGTB-covered legal single-pawn KPK +// positions). Also pure geometry, no move generation, so also safe for +// eval.c to call directly. +// +FLAG +IsUnstoppableLoneKingPasser(POSITION *pos, ULONG uStrong, COOR cPawn); + +// // Unregistered/experimental recognizers, exposed only so testrecogn.c // can exhaustively validate a candidate before it's ever re-registered // in InitializeInteriorNodeRecognizers. Not reachable from the normal @@ -89,8 +89,8 @@ const int g_iBehind[2] = { -1, +1 }; // { -10, 0, 0, +7, +15, +20, +24, +29, +36 }, // // 2 //}; -static ULONG REDUCED_MATERIAL_DOWN_SCALER[32] = -{ +static ULONG REDUCED_MATERIAL_DOWN_SCALER[32] = +{ // none na na m na R 2m na 0, 0, 0, 0, 0, 0, 0, 0, @@ -98,7 +98,7 @@ static ULONG REDUCED_MATERIAL_DOWN_SCALER[32] = 0, 1, 1, 1, 1, 1, 2, 3, // 2R2m R4m/QRm Q3m 2R3m/ QR2m Q4m 2R4m/ QR3m -// Q2R Q2Rm +// Q2R Q2Rm 4, 5, 6, 6, 7, 7, 7, 7, // na Q2R2m QR4m na Q2R3m na na full @@ -130,7 +130,7 @@ static ULONG CASTLE_AND_DEVELOPMENT_SCALER[32] = }; static ULONG REDUCED_MATERIAL_UP_SCALER[32] = -{ +{ // none na na m na R 2m na 8, 8, 8, 8, 8, 8, 8, 8, @@ -138,15 +138,15 @@ static ULONG REDUCED_MATERIAL_UP_SCALER[32] = 8, 7, 7, 7, 7, 6, 6, 5, // 2R2m R4m/QRm Q3m 2R3m/ QR2m Q4m 2R4m/ QR3m -// Q2R Q2Rm +// Q2R Q2Rm 4, 3, 3, 3, 2, 2, 2, 1, // na Q2R2m QR4m na Q2R3m na na full 1, 1, 0, 0, 0, 0, 0, 0, }; -static ULONG PASSER_MATERIAL_UP_SCALER[32] = -{ +static ULONG PASSER_MATERIAL_UP_SCALER[32] = +{ // none na na m na R 2m na 8, 8, 8, 8, 8, 8, 7, 7, @@ -154,13 +154,19 @@ static ULONG PASSER_MATERIAL_UP_SCALER[32] = 7, 6, 7, 5, 4, 5, 4, 3, // 2R2m R4m/QRm Q3m 2R3m/ QR2m Q4m 2R4m/ QR3m -// Q2R Q2Rm +// Q2R Q2Rm 2, 2, 1, 1, 0, 0, 0, 0, // na Q2R2m QR4m na Q2R3m na na full 0, 0, 0, 0, 0, 0, 0, 0, }; +static ULONG MULTIPLE_PASSER_SCALER[9] = +{ +// 0 1 2 3 4 5 6 7 8 + 0, 32, 27, 22, 18, 11, 8, 4, 2 +}; + COOR QUEENING_RANK[2] = { 1, 8 }; COOR JUMPING_RANK[2] = { 7, 2 }; @@ -210,7 +216,7 @@ static SCORE PAWN_CENTRALITY_BONUS[128] = // most exploitable. Table is indexed by absolute square (not // color-relative), hence the rank2/rank7, rank3/rank6, rank4/rank5 // mirroring -- same as the original layout. -static SCORE BACKWARD_SHIELDED_BY_LOCATION[128] = +static SCORE BACKWARD_SHIELDED_BY_LOCATION[128] = { +0, +0, +0, +0, +0, +0, +0, +0, 0,0,0,0,0,0,0,0, -7, -5, -5, -7, -7, -5, -5, -7, 0,0,0,0,0,0,0,0, @@ -223,7 +229,7 @@ static SCORE BACKWARD_SHIELDED_BY_LOCATION[128] = +0, +0, +0, +0, +0, +0, +0, +0, 0,0,0,0,0,0,0,0 }; -static SCORE BACKWARD_EXPOSED_BY_LOCATION[128] = +static SCORE BACKWARD_EXPOSED_BY_LOCATION[128] = { +0, +0, +0, +0, +0, +0, +0, +0, 0,0,0,0,0,0,0,0, -12, -9, -9, -11, -11, -9, -9, -12, 0,0,0,0,0,0,0,0, @@ -256,7 +262,7 @@ static SCORE BACKWARD_EXPOSED_BY_LOCATION[128] = // "All About Doubled Pawns" // // Note: indexed by number of files with 1+ pawn on them. -static SCORE DOUBLED_PAWN_PENALTY_BY_COUNT[4][9] = +static SCORE DOUBLED_PAWN_PENALTY_BY_COUNT[4][9] = { // ------------ count of doubled+ pawns ------------ {// 0 1 2 3 4 5 6 7 8 : no majors alive +0, -32, -65, -99, -134, -170, -207, -222, -250 @@ -290,13 +296,13 @@ static SCORE ISOLATED_DOUBLED_PAWN = -7; // // Note: -25% to -33% if the enemy occupies or controls the next sq. // -static SCORE PASSER_BY_RANK[2][9] = +static SCORE PASSER_BY_RANK[2][9] = {// 0 1 2 3 4 5 6 7 8 { +0, +0,+162,+111, +62, +36, +18, +13, +0 }, // black { +0, +0, +13, +18, +36, +62,+111,+162, +0 } // white }; -static SCORE CANDIDATE_PASSER_BY_RANK[2][9] = +static SCORE CANDIDATE_PASSER_BY_RANK[2][9] = {// 0 1 2 3 4 5 6 7 8 { +0, +0, +0, +48, +34, +22, +13, +9, +0 }, // black { +0, +0, +9, +13, +22, +34, +48, +0, +0 } // white @@ -358,7 +364,7 @@ static SCORE PASSER_BONUS_AS_MATERIAL_COMES_OFF[32] = +14, +9, +20, +4, +3, +3, 0, 0, // 2R2m R4m/QRm Q3m 2R3m/ QR2m Q4m 2R4m/ QR3m -// Q2R Q2Rm +// Q2R Q2Rm 0, 0, 0, 0, 0, 0, 0, 0, // na Q2R2m QR4m na Q2R3m na na full @@ -468,7 +474,7 @@ static SCORE KNIGHT_CENTRALITY_BONUS[128] = static SCORE KNIGHT_KING_TROPISM_BONUS[9] = {// 0 1 2 3 4 5 6 7 8 - 0, +15, +12, +9, +4, +0, +0, +0, +0 + 0, +15, +12, +9, +4, +0, +0, +0, +0 }; @@ -498,7 +504,7 @@ static SCORE KNIGHT_MOBILITY_BY_COUNT[9] = -17, -10, -6, 0, +3, +5, +7, +8, +8 };// ^ | -static SCORE KNIGHT_WITH_N_PAWNS_SUPPORTING[3] = +static SCORE KNIGHT_WITH_N_PAWNS_SUPPORTING[3] = { +0, +4, +8 }; @@ -562,7 +568,7 @@ static SCORE ROOK_LEADS_PASSER_BY_PASSER_RANK[2][9] = static SCORE KING_TRAPPING_ROOK = -40; static SCORE ROOK_VALUE_AS_PAWNS_COME_OFF[17] = -{// 0 1 2 3 4 5 6 7 8 +{// 0 1 2 3 4 5 6 7 8 +55, +51, +44, +38, +33, +27, +22, +16, +7, // 9 10 11 12 13 14 15 16 +1, -3, -6, -9, -12, -18, -22, -25 @@ -728,7 +734,7 @@ static SCORE KING_TO_CENTER[128] = #define KEEP_KING_AT_HOME_THRESHOLD \ (VALUE_ROOK + VALUE_ROOK + VALUE_BISHOP + VALUE_KING + 1) -static ULONG KING_COUNTER_BY_ATTACK_PATTERN[32] = +static ULONG KING_COUNTER_BY_ATTACK_PATTERN[32] = { //p 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 //m 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 @@ -2139,8 +2145,8 @@ Return value: } -static PAWN_HASH_ENTRY * -_EvalPawns(IN OUT SEARCHER_THREAD_CONTEXT *ctx, +PAWN_HASH_ENTRY * +EvalPawns(IN OUT SEARCHER_THREAD_CONTEXT *ctx, OUT FLAG *pfDeferred) /** @@ -2148,6 +2154,11 @@ Routine description: Evaluate pawn structures; return a ptr to a pawn hash entry. + Exposed (non-static) so testrecogn.c can build a real, + fully-populated PAWN_HASH_ENTRY (bbPasserLocations included) to + drive an EGTB cross-check of _EvalPasserRacesAgainstLoneKings -- + same rationale as SideHasWinningChances's own exposure above. + Parameters: SEARCHER_THREAD_CONTEXT *ctx : the searcher thread context @@ -5550,34 +5561,151 @@ Return value: } +static FLAG +_EvalUnstoppablePasser(IN OUT POSITION *pos) +/** + +Routine description: + + True-knowledge replacement for _EvalPasserRacesAgainstLoneKings + (below), used ONLY when both sides are pure king-and-pawns (both + TotalNonPawnCount == 1) -- the exact KPvKP shape this session's + investigation exhaustively verified. Calls the SAME shared, + exhaustively-verified routines recogn.c's hard-bound KPvKP + recognizer uses (IsUnstoppableLoneKingPasser for a true-lone-king + opponent's any-rank square-rule race, HasStaticUnstoppablePasser for + the general "opponent also has pawns" 7th/2nd-rank case), so there + is exactly one implementation of "is this pawn unstoppable," not two + that can silently diverge the way this file's old passer-race + heuristic and recogn.c's KPK recognizer once did (the whole reason + this investigation started). + + _EvalPasserRacesAgainstLoneKings's own precondition is looser (fires + whenever EITHER side is bare, even if the OTHER side still has real + pieces, e.g. K+R+P vs K) -- that broader shape was never covered by + this session's KPvKP work and is NOT provably safe for the + shared routines here (their reasoning assumes no other pieces exist + to interpose/defend/escape), so the call site below keeps routing + that case to the old function unchanged. + + Returns whether it actually placed a credit for either side. This + function is trustworthy but narrow -- it only ever fires for the + true-lone-king sub-case of pure KPvKP, and stays silent (returns + FALSE) for the "both sides still have pawns" sub-case rather than + guess. The caller uses this to fall back to + _EvalPasserRacesAgainstLoneKings (dodgy but broader-coverage) only + when this function had nothing to say, instead of losing that + coverage outright. + +Parameters: + + POSITION *pos + +Return value: + + FLAG -- TRUE if a credit was placed for either side + +**/ +{ + ULONG uColor; + FLAG fFired = FALSE; + + FOREACH_COLOR(uColor) + { + ULONG xColor = FLIP(uColor); + COOR c; + + if (0 == FastCountBits(pos->bbPawns[uColor])) + { + continue; + } + + if (0 == FastCountBits(pos->bbPawns[xColor])) + { + // True lone-king opponent: the general "rule of the + // square + king escort" race, any rank, not just the + // 7th/2nd -- see IsUnstoppableLoneKingPasser's own header + // comment (0 wrong across all 165,676 EGTB-covered legal + // single-pawn KPK positions). + BITBOARD bb = pos->bbPawns[uColor]; + while (IS_ON_BOARD(c = CoorFromBitBoardRank8ToRank1(&bb))) + { + if (TRUE == IsUnstoppableLoneKingPasser(pos, uColor, c)) + { + EVAL_TERM(uColor, + PAWN, + c, + pos->iScore[uColor], + RACER_WINS_RACE, + "unstoppable passer (lone king)"); + fFired = TRUE; + // + // break, not return: uColor is done (one credit is + // enough for this side), but the OTHER color still + // needs to be checked by this same FOREACH_COLOR + // loop -- an earlier version of this function + // returned here, which meant whichever color + // happened to be checked first (by FOREACH_COLOR's + // iteration order) silently suppressed any check of + // the other side at all, even a far more urgent + // threat. Found via a real regression: White's Ph7 + // one push from queening + // (8/7P/6p1/3k1p2/2p5/3pK3/8/8 w - -, this whole + // investigation's original motivating position) + // scored -6.09 (the exact ORIGINAL buggy value from + // before this session) because Black's much weaker + // d3 pawn got checked first and its credit's early + // return meant White's real threat was never + // evaluated at all. + // + break; + } + } + } + // + // Deliberately nothing here for "opponent also has pawns." + // A prior session (see the stashed 2026-09-2x eval.c/recogn.c + // "passer-race + king-escort rework") tried exactly this + // extension, measured it at only ~1% wrong-side rate against + // EGTB, and STILL found it caused a real playing-strength + // regression in overnight self-play -- a low measured + // false-positive rate doesn't guarantee an eval nudge is + // actually good for play; it can still systematically mislead + // move choice/ordering in ways a raw error-rate number doesn't + // capture. That session's own conclusion (matched here rather + // than re-litigated): restrict this function to the exact same + // true-lone-king precondition recogn.c's own hard bound + // trusts, and leave the "both sides have pawns" case to + // recogn.c's search-time recognizer (_RecognizeKPK) and plain + // positional eval terms, not a duplicate heuristic here. An + // eval.c attempt at this (HasRacingUnstoppablePasser) was + // built and measured this session before finding this same + // prior-session lesson -- removed rather than kept as unused + // dead code; see git history if this is ever revisited with a + // real self-play test to back it, not just an EGTB sample. + // + } + + return(fFired); +} + + FLAG -_EvalPasserRacesAgainstLoneKings(IN OUT POSITION *pos, - IN PAWN_HASH_ENTRY *pHash) +_EvalPasserRacesAgainstLoneKings(IN OUT SEARCHER_THREAD_CONTEXT *ctx, + IN PAWN_HASH_ENTRY *pHash) /** Routine description: Determine if one side has a pawn that is unstoppable. - A 2026-09-16 investigation (see recogn.c's _RecognizeKPK for the - full writeup) found this function's own king-catches-pawn race - distance comparison did NOT actually reproduce the bug in - 7k/8/5KP1/8/8/8/8/8 w - - (Kf6/Pg6/Kh8, White to move -- a draw - via stalemate defense that a sibling function, _RecognizeKPK, used - to misjudge as a win) -- a file-specific guard added here on - suspicion turned out to be unnecessary once _RecognizeKPK's real - bug was fixed and was removed after TestRecognExhaustiveKPK - (testrecogn.c) confirmed 0 wrong verdicts across all EGTB-covered - legal KPK positions without it. Left undocumented/unfixed here: - whether this function has its own independent bugs of the same - general shape was NOT exhaustively verified (TestRecognExhaustiveKPK - exercises it only via RecognLookup's pHash-lookup call path, which - doesn't reach every branch of this function directly) -- worth - exhaustively checking on its own if this area gets touched again. + This always works for KPK or KPKP but gets dodgy if a side has + more than 1 pawn. Error rates on KPPKPP are ~5% and KPKPPP + are ~1%. Parameters: - POSITION *pos, + SEARCHER_THREAD_CONTEXT *ctx, PAWN_HASH_ENTRY *pHash, Return value: @@ -5587,189 +5715,187 @@ Return value: **/ { BITBOARD bb[2]; - ULONG uColor; - ULONG xColor; + ULONG uColor, xColor; int d1; - COOR cQueen; - COOR cKing, c; - ULONG uKingDist, uPawnDist, uFriendDist; - ULONG uRacerDist[2]; + COOR cKing[2]; + COOR c, cQueen; + ULONG uEnemyKingDist, uPawnDist, uFriendKingDist; + ULONG uMinRacerDist[2] = {99, 99}; + ULONG uMovesSpent[2]; FLAG fDontCountMeOut[2]; + POSITION *pos = &ctx->sPosition; + + // Don't recognize if stm is in check. + PLY_INFO *pi = &ctx->sPlyInfo[ctx->uPly]; + if (pi->fInCheck) + { + return(FALSE); + } + + // Only deal with positions that have passers. bb[BLACK] = pHash->bbPasserLocations[BLACK]; bb[WHITE] = pHash->bbPasserLocations[WHITE]; if (!(bb[BLACK] | bb[WHITE])) { return(FALSE); } - ASSERT((TotalNonPawnCount(pos, WHITE) == 1) || - (TotalNonPawnCount(pos, BLACK) == 1)) + ASSERT((TotalNonPawnCount(pos, WHITE) == 1) || (TotalNonPawnCount(pos, BLACK) == 1)); - // Both deferred past the no-passer early return above -- no need to - // pay for this on the common case (most positions have no passer at - // all, and _EvalPasserRacesAgainstLoneKings itself is only even called when a side - // is down to a bare king, so this function runs unconditionally in - // the pre-lazy-exit segment whenever that's true). - uRacerDist[BLACK] = 99; - uRacerDist[WHITE] = 99; + // Don't make a call if the king is well placed and the side has + // pawns left. fDontCountMeOut[BLACK] = ((RANK(KingPosition(pos, BLACK)) <= 3) && (pos->bbPawns[BLACK] != 0)); fDontCountMeOut[WHITE] = ((RANK(KingPosition(pos, WHITE)) >= 6) && (pos->bbPawns[WHITE] != 0)); + + cKing[WHITE] = KingPosition(pos, WHITE); + ASSERT(IS_KING(pos->rgSquare[cKing[WHITE]].pPiece)); + ASSERT(GET_COLOR(pos->rgSquare[cKing[WHITE]].pPiece) == WHITE); + cKing[BLACK] = KingPosition(pos, BLACK); + ASSERT(IS_KING(pos->rgSquare[cKing[BLACK]].pPiece)); + ASSERT(GET_COLOR(pos->rgSquare[cKing[BLACK]].pPiece) == BLACK); + FOREACH_COLOR(uColor) { - xColor = FLIP(uColor); - if (TotalNonPawnCount(pos, xColor) == 1) + // If uColor has no passers, skip it. + if (!bb[uColor]) { - d1 = 16 * g_iAhead[uColor]; - ASSERT((d1 * 2) == (32 * g_iAhead[uColor])); - if (bb[uColor]) - { - while(IS_ON_BOARD(c = CoorFromBitBoardRank8ToRank1( - &(bb[uColor])))) - { - // If the enemy king can take this passer and is - // on the move, all bets are off! - cKing = KingPosition(pos, xColor); - ASSERT(IS_KING(pos->rgSquare[cKing].pPiece)); - ASSERT(GET_COLOR(pos->rgSquare[cKing].pPiece) != uColor); - if (pos->uToMove == xColor && - (DISTANCE(cKing, c) == 1)) - { - // TODO: something clever here about protected pawns - continue; - } - - cQueen = FILE_RANK_TO_COOR(FILE(c), QUEENING_RANK[uColor]); - ASSERT(FILE(cQueen) == FILE(c)); + continue; + } - // 2026-09-18: rook pawn, strong side's OWN king on - // the same file, on or adjacent to its own - // queening square -- i.e. the strong king is in - // front of its own pawn. Checked up front, before - // either race-credit computation below gets a - // chance to touch uRacerDist[uColor] for this - // pawn, and skips via `continue` (not by resetting - // uRacerDist[uColor], which would also wipe out a - // legitimate credit already earned by a different - // passer of the same color from an earlier loop - // iteration -- bb[uColor] can hold more than one - // passer). A friendly king blocking its own rook - // pawn's queening square is a classic drawing - // resource (the defender can potentially reach a - // stalemate), not a neutral or helpful thing -- - // unlike a center pawn, where the king merely costs - // a tempo (handled below via the existing +1 case), - // a rook pawn's single queening square occupied by - // its own king has no safe way to step aside - // without delicate tempo/opposition analysis this - // cheap eval-time heuristic can't do (that's what - // recogn.c's _RecognizeKPK critical-square logic is - // for). Found via fuzzing tests/cross_sectional_ - // 200k.ep_: two of this session's real recognizer/ - // eval bugs both had the strong side's own king in - // front of its own rook pawn (8/8/8/8/8/8/4K2p/7k - // w - - -- White can stalemate Black rather than - // let Ph2 queen). - // - // Distance must be exactly 0 (king ON the queening - // square), not <=1 -- if the king is merely - // adjacent, the queening square itself is empty - // and the pawn walks in and promotes next move - // with nothing in the way; that's a genuine win, - // not a stalemate risk. Only the king literally - // occupying its own pawn's only queening square is - // the danger this guards against. - if (((A == FILE(c)) || (H == FILE(c))) && - (FILE(c) == FILE(KingPosition(pos, uColor))) && - (0 == DISTANCE(KingPosition(pos, uColor), cQueen))) - { - // Matches the unconditional assignment every - // other path through this loop body makes -- - // this color's own passer was genuinely - // examined here, even though disqualified. - fDontCountMeOut[uColor] = TRUE; - continue; - } + // If xColor has any piece other than a king (and maybe some pawns), skip. + xColor = FLIP(uColor); + if (TotalNonPawnCount(pos, xColor) != 1) + { + continue; + } + d1 = 16 * g_iAhead[uColor]; - uPawnDist = RANK_DISTANCE(cQueen, c); - if ((RANK(c) == JUMPING_RANK[uColor]) && - (IS_EMPTY(pos->rgSquare[c + d1].pPiece) && - IS_EMPTY(pos->rgSquare[c + d1 * 2].pPiece))) - { - uPawnDist--; - } - ASSERT((uPawnDist >= 1) && (uPawnDist <= 6)); + uMovesSpent[uColor] = 1; + uMovesSpent[xColor] = 0; + while(IS_ON_BOARD(c = CoorFromBitBoardRank8ToRank1( + &(bb[uColor])))) + { + // If this pawn is right next to the enemy king, don't + // predict the number of moves it takes to queen. + if (xColor == pos->uToMove && + (DISTANCE(cKing[xColor], c) == 1) && + (DISTANCE(cKing[uColor], c) != 1)) + { + uMovesSpent[xColor] += 1; + continue; + } + fDontCountMeOut[uColor] = TRUE; - uKingDist = DISTANCE(cKing, cQueen); - if (uKingDist > 0) - { - uKingDist -= (pos->uToMove == xColor); - } - ASSERT((uKingDist >= 0) && (uPawnDist <= 7)); + cQueen = FILE_RANK_TO_COOR(FILE(c), QUEENING_RANK[uColor]); + ASSERT(FILE(cQueen) == FILE(c)); - if (uPawnDist < uKingDist) - { - uRacerDist[uColor] = MINU(uRacerDist[uColor], - uPawnDist); - } + // Don't call rook pawn races with the friendly king in a + // dodgy position. + if (((A == FILE(c)) || (H == FILE(c))) && + (FILE(c) == FILE(cKing[uColor])) && + (0 == DISTANCE(cKing[uColor], cQueen))) + { + continue; + } - cKing = KingPosition(pos, uColor); - ASSERT(IS_KING(pos->rgSquare[cKing].pPiece)); - ASSERT(GET_COLOR(pos->rgSquare[cKing].pPiece) == uColor); + uPawnDist = RANK_DISTANCE(cQueen, c); + uPawnDist -= ((RANK(c) == JUMPING_RANK[uColor]) && + (IS_EMPTY(pos->rgSquare[c + d1].pPiece) && + IS_EMPTY(pos->rgSquare[c + d1 * 2].pPiece))); + ASSERT((uPawnDist >= 1) && (uPawnDist <= 6)); + uPawnDist += uMovesSpent[uColor]; - uFriendDist = DISTANCE(cKing, cQueen); - if ((uFriendDist <= 1) && (DISTANCE(cKing, c) <= 1)) - { - if (FILE(c) != FILE(cKing)) - { - uRacerDist[uColor] = MINU(uRacerDist[uColor], - uPawnDist); - } - else if ((FILE(cKing) != A) && - (FILE(cKing) != H)) - { - uRacerDist[uColor] = MINU(uRacerDist[uColor], - (uPawnDist + 1)); - } - // else: rook pawn, own king on the same file - // but more than 1 square from the queening - // square -- the specific adjacent/on-queening- - // square danger this session found (see the - // up-front `continue` guard above, right after - // cQueen is computed) doesn't apply here; leave - // as-is (no extra credit, no suppression), - // same as this branch's original behavior. - } - fDontCountMeOut[uColor] = TRUE; + // Rule of the square; can this passer make it to + // the queening square before the enemy king? + uEnemyKingDist = DISTANCE(cKing[xColor], cQueen); + if (uEnemyKingDist > 0) + { + uEnemyKingDist -= (pos->uToMove == xColor); + } + ASSERT((uEnemyKingDist >= 0) && (uPawnDist <= 7)); + if (uPawnDist < uEnemyKingDist) + { + uMinRacerDist[uColor] = MINU(uMinRacerDist[uColor], uPawnDist); + } - // - // TODO: winning connected passers - // + // See if the friend king can escort this passer in. Same- + // file adjacency alone doesn't distinguish a king BEHIND + // the pawn (genuine escort) from a king sitting on the + // very next square in front of it, i.e. cKing[uColor] == + // c + d1 (blocking its own pawn's path, not helping it) -- + // found via an exhaustive KPvKP/EGTB cross-check + // (8/8/8/8/8/6Pp/7k/5K2 b - -: Black king h2 sits directly + // in front of its own h3 pawn, yet this branch credited an + // escort bonus as if h2 were behind it). Exclude that + // square explicitly rather than just checking direction, + // since it's the one same-file square DISTANCE<=1 permits + // that unambiguously blocks rather than escorts. + uFriendKingDist = DISTANCE(cKing[uColor], cQueen); + if ((uFriendKingDist <= 1) && (DISTANCE(cKing[uColor], c) <= 1)) + { + if (FILE(c) != FILE(cKing[uColor])) + { + uMinRacerDist[uColor] = MINU(uMinRacerDist[uColor], + uPawnDist); + } + else if ((cKing[uColor] != (c + d1)) && + (FILE(cKing[xColor]) != A) && + (FILE(cKing[xColor]) != H)) + { + uMinRacerDist[uColor] = MINU(uMinRacerDist[uColor], + (uPawnDist + 1)); } } } + + // uMovesSpent[xColor] counts how many of uColor's OWN passers + // just got individually skipped above as "adjacent to xColor's + // king, xColor to move -- assume it's captured for free." Fine + // when it happens to exactly one pawn, but when it happens to + // TWO OR MORE, xColor's king is adjacent to multiple of + // uColor's passers at once and can only actually capture ONE + // of them -- the classic king-overload fork (found via random + // KPPKPP/KPKPPP sampling against EGTB: + // 8/5P2/1k6/8/8/8/pKp2P2/8 w - -, White king b2 flanked by + // Black's a2 and c2, each one square from queening -- White + // captures one, Black queens the other, Black wins by force, + // but the per-pawn skip above silently dropped BOTH of + // Black's passers from consideration, crediting White's own + // ordinary race instead). This function's per-pawn model has + // no way to actually resolve a fork like this ("TODO: winning + // connected passers" above already flags the general + // connected-passer gap as out of scope) -- the safe, precision- + // over-recall response is not to guess who really wins, but to + // stop trusting THE OTHER side's credit here, the same way + // fDontCountMeOut already protects a side with real drawing + // chances elsewhere in this function. + if (uMovesSpent[xColor] >= 2) + { + fDontCountMeOut[uColor] = TRUE; + } } - if ((uRacerDist[WHITE] < uRacerDist[BLACK]) && + if ((uMinRacerDist[WHITE] < uMinRacerDist[BLACK]) && (FALSE == fDontCountMeOut[BLACK])) { EVAL_TERM(WHITE, PAWN, ILLEGAL_COOR, pos->iScore[WHITE], - RACER_WINS_RACE - uRacerDist[WHITE] * 4, + RACER_WINS_RACE - uMinRacerDist[WHITE] * 4, "winning racer pawn"); return(TRUE); } - else if ((uRacerDist[BLACK] < uRacerDist[WHITE]) && + else if ((uMinRacerDist[BLACK] < uMinRacerDist[WHITE]) && (FALSE == fDontCountMeOut[WHITE])) { EVAL_TERM(BLACK, PAWN, ILLEGAL_COOR, pos->iScore[BLACK], - RACER_WINS_RACE - uRacerDist[BLACK] * 4, + RACER_WINS_RACE - uMinRacerDist[BLACK] * 4, "winning racer pawn"); return(TRUE); } @@ -5919,17 +6045,17 @@ Return value: PAWN, ILLEGAL_COOR, pos->iScore[WHITE], - uNumPassers[WHITE] * PASSER_BONUS_AS_MATERIAL_COMES_OFF[u], + (uNumPassers[WHITE] * PASSER_BONUS_AS_MATERIAL_COMES_OFF[u] * MULTIPLE_PASSER_SCALER[uNumPassers[WHITE]]) >> 5, "passer value material"); - // ASSERT(PASSER_BONUS_AS_MATERIAL_COMES_OFF[u] >= 0); + ASSERT(PASSER_BONUS_AS_MATERIAL_COMES_OFF[u] >= 0); u = pos->uArmyScaler[WHITE]; EVAL_TERM(BLACK, PAWN, ILLEGAL_COOR, pos->iScore[BLACK], - uNumPassers[BLACK] * PASSER_BONUS_AS_MATERIAL_COMES_OFF[u], + (uNumPassers[BLACK] * PASSER_BONUS_AS_MATERIAL_COMES_OFF[u] * MULTIPLE_PASSER_SCALER[uNumPassers[BLACK]]) >> 5, "passer value material"); - // ASSERT(PASSER_BONUS_AS_MATERIAL_COMES_OFF[u] >= 0); + ASSERT(PASSER_BONUS_AS_MATERIAL_COMES_OFF[u] >= 0); } @@ -5967,35 +6093,86 @@ Return value: #endif // + // A material-VALUE imbalance (uNonPawnMaterial differing, the + // caller's gate) is not the same fact as a piece-COUNT imbalance -- + // e.g. 2R vs R+N is "up the exchange" with equal piece count on + // both sides, not a side that has simplified/traded down to fewer + // pieces. Ported from a stashed, previously-uncommitted eval.c + // change (2026-09-2x eval audit session) -- mirrors Crafty's + // EvaluateMaterial (~/crafty/evaluate.c), whose bad_trade bonus + // explicitly requires Abs(occupied_count_diff) != 2 && + // occupied_count_diff != 0, i.e. it never fires when piece counts + // are equal, deliberately leaving pure up-the-exchange positions to + // ordinary material valuation instead of this term. Without this + // check, typhoon double-counts: real, up-the-exchange credit + // already exists via material value, and this term used to stack + // an unconditional extra "keep trading" bonus on top regardless of + // whether uAhead had actually traded down to fewer pieces at all -- + // found via the original eval audit + // (8/2r4p/8/5p1R/8/1n4P1/1pk2PKP/4R3 b, 2R vs R+N, equal piece + // count: typhoon +2.07 for White, Crafty -1.63 for White -- + // opposite sign, traced to this exact double-count). + // + if (TotalNonPawnCount(pos, uAhead) == TotalNonPawnCount(pos, uBehind)) + { + return; + } + + // // Encourage the side that is ahead in piece material to continue // trading pieces and not to trade pawns. This also has the // effect of making the side that is behind in material want to // trade pawns and not pieces. This also gives the side that is // ahead a bit of a "bad trade" bonus. Note that the bonus is not - // given if the side "ahead" has no pawns; this is so that the - // engine will not like positions like KNB vs KRP - the side with - // the pawn has the winning chances. + // given if the side "ahead" has no pawns AND no mating material of + // its own; this is so that the engine will not like positions like + // KNB vs KRP - the side with the pawn has the winning chances. // ULONG uAheadPawnCount = FastCountBits(pos->bbPawns[uAhead]); ULONG uInverseBehindPieceCount = 9 - MINU(9, TotalNonPawnCount(pos, uBehind)); + // + // "No mating material" here means the SAME lone-minor-piece test + // SideHasWinningChances (above) already uses: king + exactly one + // knight/bishop and nothing else -- a K+N or K+B can never force + // mate alone, which is the actual KNB-vs-KRP failure mode this + // whole guard exists for. Found via a real counterexample + // (2k5/4r3/KP2P3/P7/7P/8/8/8 b - -): Black is ahead in non-pawn + // material with a ROOK, not a lone minor -- a rook can genuinely + // blockade/pick off multiple passed pawns on its own, nothing like + // the KNB-vs-KRP case this guard was built for -- yet the old + // "zero pawns -> zero bonus" version zeroed this out unconditionally + // by piece count alone, silencing exactly the term that should + // have been pushing back against White's four separately-scored + // passed pawns. + // + FLAG fAheadLoneMinorNoMate = + (2 == TotalNonPawnCount(pos, uAhead)) && + (1 == (FastCountBits(pos->bbPieces[uAhead][KNIGHT]) + + FastCountBits(pos->bbPieces[uAhead][BISHOP]))); EVAL_TERM(uAhead, 0, ILLEGAL_COOR, pos->iScore[uAhead], - (uInverseBehindPieceCount * 16) * (uAheadPawnCount != 0), + (uInverseBehindPieceCount * 16) * + ((uAheadPawnCount != 0) || (FALSE == fAheadLoneMinorNoMate)), "trade pieces"); - // Zero pawns left for the ahead side is the classic false-positive - // (KNB vs KRP, minor-up-no-pawns endings that are often drawn or - // even lost) -- the old DONT_TRADE_PAWNS table existed specifically - // to punish it (-43/-10 depending on lead size) rather than just - // trailing off toward a small positive number. Keep that guard as - // a cheap branch rather than a lookup table; the positive slope - // for 1+ pawns doesn't need the same care. + // Zero pawns AND no mating material for the ahead side is the + // classic false-positive (KNB vs KRP, minor-up-no-pawns endings + // that are often drawn or even lost) -- the old DONT_TRADE_PAWNS + // table existed specifically to punish it (-43/-10 depending on + // lead size) rather than just trailing off toward a small positive + // number. Keep that guard as a cheap branch rather than a lookup + // table; the positive slope for 1+ pawns doesn't need the same + // care. Same mating-material exception as "trade pieces" above: + // zero pawns with REAL mating material (a rook, say) isn't the + // KNB-vs-KRP failure mode this -30 penalty exists for, so it gets + // a neutral 0 instead of being punished. EVAL_TERM(uAhead, 0, ILLEGAL_COOR, pos->iScore[uAhead], - (0 == uAheadPawnCount) ? -30 : (SCORE)(uAheadPawnCount * 2), + (0 != uAheadPawnCount) ? (SCORE)(uAheadPawnCount * 2) : + (TRUE == fAheadLoneMinorNoMate) ? -30 : 0, "don't trade pawns"); } @@ -6275,9 +6452,9 @@ Return value: } -static FLAG -_SideHasWinningChances(IN POSITION *pos, - IN ULONG uSide) +FLAG +SideHasWinningChances(IN POSITION *pos, + IN ULONG uSide) /** Routine description: @@ -6293,14 +6470,16 @@ Routine description: recognizer bound can (see recogn.c's _RecognizeKNKP/_RecognizeKBNK incident). - Deliberately conservative: only covers material shapes where "no, + Deliberately conservative: mostly covers material shapes where "no, this can't be forced" is basic, textbook chess knowledge (insufficient mating material, up the exchange with nothing else - changed, bare knights vs a bare king), not trickier exceptions - (fortress draws, wrong-bishop-pawn-with-king-in-time geometry) - that need real board information -- those are recogn.c's - hard-recognizer territory (already exact for tiny material there) - or plain search's job, not this cheap pre-check's. + changed, bare knights vs a bare king). One exception that DOES need + real board information, not just material counts, is included below + (wrong-bishop-pawn-with-king-in-time) precisely because it's common + enough and cheap enough to be worth it; anything else needing real + board geometry is recogn.c's hard-recognizer territory (already + exact for tiny material there) or plain search's job, not this + cheap pre-check's. Parameters: @@ -6321,6 +6500,77 @@ Return value: ULONG uSideKnights; ULONG uSideBishops; + // uSide's bishop count is needed by the wrong-bishop-fortress check + // immediately below (which must run BEFORE the "any pawn -> TRUE" + // early return that follows it) and is reused again later by the + // lone-minor/iMinorDiff/two-knights checks -- compute it once, up + // front. + uSideBishops = FastCountBits(pos->bbPieces[uSide][BISHOP]); + + // + // Wrong-bishop rook-pawn fortress: a lone bishop + rook-pawn(s) (all + // on the A- or H-file), with the bishop unable to control the + // promotion square, is a known draw once the defending bare king + // can reach the corner in time -- regardless of how much raw pawn + // material uSide has. Checked before the blanket "any pawn -> TRUE" + // return below, which would otherwise hide this case entirely. + // Mirrors Crafty's EvaluateWinningChances (~/crafty/evaluate.c) -- + // "in time" uses the same tempo-adjusted Chebyshev distance Crafty + // uses (one square of credit to the defender if it's their move). + // Ported from a stashed, previously-uncommitted eval.c change + // (2026-09-2x eval audit session); testing methodology for its + // false-positive rate against this session's own KP+KP* EGTB + // harness is still to be built (see testeval.c/testrecogn.c) before + // trusting the ~2.12% figure the stash's own comment cited -- that + // number was never independently re-verified in this session. Only + // ever softens a score (via EvalLookForDrawishSituations' /16 + // shrink), never used as a search-terminating bound the way + // recogn.c's tighter dist<=1-only version is, so a wrong FALSE here + // is tolerable in a way it would not be in recogn.c. + // + if ((2 == TotalNonPawnCount(pos, uSide)) && + (1 == uSideBishops) && + (1 == TotalNonPawnCount(pos, uEnemy)) && + (0 == FastCountBits(pos->bbPawns[uEnemy])) && + (FastCountBits(pos->bbPawns[uSide]) > 0)) + { + BITBOARD bbPawns = pos->bbPawns[uSide]; + COOR cPromote = ILLEGAL_COOR; + + if (uSide == WHITE) + { + if (!(bbPawns & ~BBFILE[A]) && (WhiteSqBishopCount(pos, WHITE) == 0)) + { + cPromote = A8; + } + else if (!(bbPawns & ~BBFILE[H]) && (WhiteSqBishopCount(pos, WHITE) == 1)) + { + cPromote = H8; + } + } + else + { + if (!(bbPawns & ~BBFILE[A]) && (WhiteSqBishopCount(pos, BLACK) == 1)) + { + cPromote = A1; + } + else if (!(bbPawns & ~BBFILE[H]) && (WhiteSqBishopCount(pos, BLACK) == 0)) + { + cPromote = H1; + } + } + + if (IS_ON_BOARD(cPromote)) + { + COOR cWeakKing = KingPosition(pos, uEnemy); + int iTempo = (pos->uToMove == uEnemy) ? 1 : 0; + if (((int)DISTANCE(cWeakKing, cPromote) - iTempo) < 1) + { + return(FALSE); + } + } + } + // // A pawn always gives some winning chances (it can always try to // queen with support) -- nothing below applies. @@ -6330,12 +6580,10 @@ Return value: return(TRUE); } - // uSide's knight/bishop counts are each read three times below - // (the lone-minor check, iMinorDiff, and the two-knights check) -- - // compute them once and reuse rather than re-popcounting the same - // two bitboards repeatedly. + // uSide's knight count is read twice below (the lone-minor check + // and the two-knights check); uSideBishops was already computed + // above. uSideKnights = FastCountBits(pos->bbPieces[uSide][KNIGHT]); - uSideBishops = FastCountBits(pos->bbPieces[uSide][BISHOP]); // // No pawns and the only piece besides the king is a single minor: @@ -6433,9 +6681,9 @@ _EnsureDrawishFactsComputed(IN POSITION *pos, IN ULONG uColor, { return; } - pFacts->fSideToMoveHasWinningChances = _SideHasWinningChances(pos, uColor); + pFacts->fSideToMoveHasWinningChances = SideHasWinningChances(pos, uColor); pFacts->fOpponentHasWinningChances = - _SideHasWinningChances(pos, FLIP(uColor)); + SideHasWinningChances(pos, FLIP(uColor)); pFacts->fBOOC = (TotalNonPawnCount(pos, WHITE) == 2) && (TotalNonPawnCount(pos, BLACK) == 2) && (FastCountBits(pos->bbPieces[WHITE][BISHOP]) == 1) && @@ -7061,7 +7309,7 @@ Return value: // TABLES IT NEEDS TO CLEAR/POPULATE THEM THOUGH!!! #ifdef EVAL_TIME UINT64 u64PawnTimer = SystemReadTimeStampCounter(); - pHash = _EvalPawns(ctx, &fDeferred); + pHash = EvalPawns(ctx, &fDeferred); { UINT64 u64Elapsed = SystemReadTimeStampCounter() - u64PawnTimer; ctx->sCounters.tree.u64CyclesEvalPawns += u64Elapsed; @@ -7077,7 +7325,7 @@ Return value: } } #else - pHash = _EvalPawns(ctx, &fDeferred); + pHash = EvalPawns(ctx, &fDeferred); #endif ASSERT(NULL != pHash); ASSERT(IS_VALID_FLAG(fDeferred)); @@ -7099,7 +7347,32 @@ Return value: #endif if (fBareKingEitherSide) { - (void)_EvalPasserRacesAgainstLoneKings(pos, pHash); + // + // Pure king-and-pawns on BOTH sides: try the new, + // exhaustively-verified shared logic first (see + // _EvalUnstoppablePasser's header comment) -- it's trustworthy + // whenever it has something to say. But it only ever fires for + // the true-lone-king sub-case and stays silent (FALSE) for the + // "both sides still have pawns" sub-case, which the old, + // broader-but-dodgier heuristic used to cover -- fall back to + // that old heuristic rather than simply losing that coverage. + // Anything broader than pure KPvKP (one bare side, but the + // OTHER side still has real pieces -- e.g. K+R+P vs K) always + // uses the old heuristic unchanged; that shape was never + // covered by this session's KPvKP investigation. + // + if ((1 == TotalNonPawnCount(pos, WHITE)) && + (1 == TotalNonPawnCount(pos, BLACK))) + { + if (FALSE == _EvalUnstoppablePasser(pos)) + { + (void)_EvalPasserRacesAgainstLoneKings(ctx, pHash); + } + } + else + { + (void)_EvalPasserRacesAgainstLoneKings(ctx, pHash); + } #ifdef EVAL_DUMP Trace("After passer races:\n%d\t\t%d\t\t(%d for %s)\n", pos->iScore[WHITE], pos->iScore[BLACK], @@ -605,6 +605,11 @@ Return value: // back down next to TestMakeUnmakeMove (its natural home) once // recogn.c work settles. TestRecogn(); + // Re-enabled at user's request to look at failing FENs; user will + // disable when done. + //TestEvalSamplePasserRacesKPPKPP(); + //TestEvalSamplePasserRacesKPKPPP(); + //TestEvalPasserRacesAgainstLoneKings(); TestDraw(); #ifdef EVAL_DUMP diff --git a/src/recogn.c b/src/recogn.c index 7b7ba84..2219e48 100644 --- a/src/recogn.c +++ b/src/recogn.c @@ -856,14 +856,6 @@ Return value: __FILE__, __LINE__); #endif - at_best_draw_for_strong: - *piScore = g_iDrawScore[pos->uToMove]; - if (pos->uToMove == uStrong) - { - return(RECOGN_UPPER); - } - return(RECOGN_LOWER); - draw: *piScore = g_iDrawScore[pos->uToMove]; return(RECOGN_EXACT); @@ -951,6 +943,655 @@ Return value: ASSERT(IS_ON_BOARD(cSquare[2])); } +// +// Is every square between cPawn and its queening square (inclusive of +// the queening square itself) empty? The critical-square race math +// below has no notion of a blocked pawn -- it assumes cPawn can walk +// straight to promotion in RANK_DISTANCE(cQueen,cPawn) moves, which was +// always true for _PawnCriticalSquareRace's original caller (lone-king +// KPK, nothing else on the board) but is no longer guaranteed once the +// opponent may have pawns of its own sitting in the way. Deliberately +// simple/conservative: this also returns FALSE if the STRONG side's +// own king blocks its own pawn's path (eval.c's now-superseded +// _EvalPasserRacesAgainstLoneKings had a narrower rook-pawn-only +// carve-out for exactly this shape and still separately credited a +// non-rook-pawn "own king in the way" case as a tempo-cost rather than +// a disqualifier) -- that nuance is not reproduced here; bailing to +// UNRECOGNIZED for it is the conservative, "when in doubt do nothing" +// choice this recognizer is supposed to make, not a proven-safe corner +// this function has verified. +// +static FLAG +_PawnPromotionPathClear(IN POSITION *pos, IN ULONG uColor, IN COOR cPawn) +{ + COOR cQueen = QUEENING_SQUARE_BY_COLOR_FILE[uColor][FILE(cPawn)]; + int iStep = (WHITE == uColor) ? -16 : 16; + COOR c; + + for (c = cPawn + iStep; c != cQueen; c += iStep) + { + ASSERT(IS_ON_BOARD(c)); + if (!IS_EMPTY(pos->rgSquare[c].pPiece)) + { + return(FALSE); + } + } + return(IS_EMPTY(pos->rgSquare[cQueen].pPiece)); +} + +// +// Does the side to move have a pawn that safely promotes to a queen +// THIS move -- a clean, non-capturing push from the 7th (or 2nd, for +// Black) rank to an empty queening square that isn't attacked by the +// opponent (so the new queen can't just be captured back for nothing), +// with the resulting position not stalemating the opponent? +// +// This is a fundamentally different, much narrower and safer question +// than a multi-move pawn race: since only the side to move can act this +// move, there is no "does the opponent also have a race" ambiguity to +// resolve at all -- whatever the opponent's other pawns are doing, they +// get no say in whether THIS move safely produces a new queen. This is +// exactly the shape of the position that motivated this whole +// investigation (8/7P/6p1/3k1p2/2p5/3pK3/8/8 w - -, White's Ph7 one +// push from queening). +// +// An earlier attempt at general KP+KP* coverage instead estimated each +// side's "plies until queening" via a per-pawn critical-square/outrun +// race analysis and compared the two numbers with a safety margin. Per +// empirical testing (TestRecognSampleKPvKP), that approach has a +// persistent ~15% wrong rate against EGTB ground truth NO MATTER how +// generous the margin was made (tried up to 60 plies) -- because a +// race-distance comparison answers "who queens first," not "is the +// resulting position actually won," and those are different claims +// once the "losing" side still has pawns of its own (even ones with no +// confident race of their own can still produce real drawing +// counterplay -- see the historical comment on this file's +// _RecognizeKPK about a duo that isn't individually passed). That +// approach was abandoned rather than kept as dead code; see git history +// if it's ever worth revisiting. This function answers a narrower, +// fully checkable question instead: it delegates the actual +// legality/safety checking to the engine's own move generation and +// attack detection rather than re-deriving any of it by hand, so +// there's no independent-implementation risk. +// +// Deliberately narrow for v1: only a clean, non-capturing push +// promotion to a QUEEN; a capturing promotion or underpromotion is +// deferred (this function returns FALSE for them, not handled +// unsafely). Mutates and restores ctx's ply/move-stack state (like +// testrecogn.c's _GetEgtbLine) but leaves pos itself unchanged on +// return. +// +// Also requires the OPPONENT not have a pending queen of their own +// (_ColorHasPendingQueen) in the resulting position -- found via a real +// counterexample (8/5K1P/8/8/2p5/1p1P4/6kp/8 w - -): White's Ph7 queens +// safely by every check above, but Black's Ph2 is ALSO one push from +// promoting and nothing about White's move touches it, so Black +// immediately replies ...h1=Q and the position is mutual queens plus +// Black's dangerous connected b3/c4 passers -- nowhere near the +// confident material win this function is supposed to only claim when +// certain. "My queening move is locally safe" and "the resulting +// position is actually won" are different claims; this closes that gap +// the cheap way (deny the claim entirely rather than try to evaluate +// the resulting queen-and-pawns middlegame, which is well outside what +// a recognizer should attempt). +// +static FLAG +_ColorHasPendingQueen(IN POSITION *pos, IN ULONG uColor) +{ + BITBOARD bb = pos->bbPawns[uColor] & + ((WHITE == uColor) ? BBRANK77 : BBRANK22); + COOR c; + int iStep = (WHITE == uColor) ? -16 : 16; + + while (IS_ON_BOARD(c = CoorFromBitBoardRank8ToRank1(&bb))) + { + PIECE pBlocker = pos->rgSquare[c + iStep].pPiece; + // + // A queening square blocked only by uColor's OWN king isn't a + // real block -- the king just steps aside next move and the + // pawn queens the move after, exactly as real a threat as an + // already-clear square (one tempo later). Found via a real + // counterexample (5K2/k4Pp1/8/8/8/4P3/3p4/8 b - -): White's + // Pf7 sits one push from queening with White's OWN Kf8 sitting + // on f8, which this check used to treat identically to a + // genuine block (e.g. an enemy piece that isn't going anywhere) + // and so never flagged as a threat -- but White simply plays + // Kg8/Ke8 next and f8=Q follows, which is exactly the kind of + // second queen _HasImmediateSafeQueen's caller needs to know + // about before trusting Black's own immediate queening move as + // a sure win. + // + if (IS_EMPTY(pBlocker) || + (IS_KING(pBlocker) && (GET_COLOR(pBlocker) == uColor))) + { + return(TRUE); + } + } + return(FALSE); +} + +// +// Does uColor have TWO OR MORE pawns (any files, connected or not) each +// on the 6th or 7th rank (White) / 3rd or 2nd rank (Black)? Reverted +// from a stricter "must be on ADJACENT files" version: that version +// correctly stayed silent on the original motivating position (Black's +// lone d3 pawn, nowhere near dangerous) but missed real counterexamples +// like 8/8/PKP3P1/8/8/8/2p5/3k4 b - - (White's a6/c6/g6 aren't +// file-adjacent to each other -- b6 has White's OWN KING sitting +// between a6 and c6, not a pawn -- yet three pawns this advanced still +// can't all be stopped). Plain count (>=2 anywhere in the zone) doesn't +// have that blind spot and, per the original motivating position check, +// still correctly ignores a single lone advanced pawn. +// +static FLAG +_ColorHasMultipleAdvancedPassers(IN POSITION *pos, IN ULONG uColor) +{ + BITBOARD bb = pos->bbPawns[uColor] & + ((WHITE == uColor) ? (BBRANK77 | BBRANK66) + : (BBRANK22 | BBRANK33)); + return(FastCountBits(bb) >= 2); +} + +// +// Does uColor have a single NON-ROOK-FILE pawn on the 6th/7th (White) or +// 3rd/2nd (Black) rank whose own king already controls (is adjacent to) +// EITHER its queening square OR the square directly in front of it (the +// pawn's very next push destination)? A rook pawn is excluded because +// its single critical square and corner-stalemate resources make it a +// fundamentally different (usually WEAKER, sometimes drawish-for-the- +// runner) case, not a stronger one -- this check is specifically about +// a pawn whose promotion is essentially guaranteed because its own king +// is already standing guard over the square it needs next, the +// escort-completed mirror image of the critical-square logic this +// investigation dropped earlier for being too failure-prone to +// hand-derive in the general case. Originally only checked the +// queening square itself; broadened to also cover a king controlling +// just the very next square (e.g. a 6th-rank pawn whose king guards the +// 7th-rank square ahead of it, without yet reaching the queening square +// itself) -- still a one-hop-from-the-king check, no race/tempo +// arithmetic, so still judged safe enough to keep. +// +static FLAG +_ColorHasEscortedAdvancedPasser(IN POSITION *pos, IN ULONG uColor) +{ + BITBOARD bb = pos->bbPawns[uColor] & + ((WHITE == uColor) ? (BBRANK77 | BBRANK66) + : (BBRANK22 | BBRANK33)); + COOR cKing = KingPosition(pos, uColor); + int iStep = (WHITE == uColor) ? -16 : 16; + COOR c; + + while (IS_ON_BOARD(c = CoorFromBitBoardRank8ToRank1(&bb))) + { + if ((A != FILE(c)) && (H != FILE(c))) + { + COOR cQueen = QUEENING_SQUARE_BY_COLOR_FILE[uColor][FILE(c)]; + COOR cAhead = c + iStep; + if ((DISTANCE(cKing, cQueen) <= 1) || + (DISTANCE(cKing, cAhead) <= 1)) + { + return(TRUE); + } + } + } + return(FALSE); +} + +// +// Does the side to move (in ctx's CURRENT position) have any legal pawn +// move that gives check? Found via a real counterexample +// (8/P7/8/4p3/8/2p5/8/1k1K4 w - -): White's Pa7 queens safely by every +// check above (a8 unattacked, not stalemate, Black has no pawn already +// pending), but Black's Pc3 -- not yet on its pending-queen rank, so +// _ColorHasPendingQueen doesn't see it -- pushes to c2 with CHECK, +// forces White's king to move, and only THEN queens: 1. a8=Q c2+ 2. Ke2 +// c1=Q, mutual queens again. A tempo bought by a check is just as real +// as a pawn that's already one push from queening; the fix is not to +// hand-special-case "two squares away via a checking push" (which +// would just invite the same gap one square further back, or via a +// discovered/double check this codebase's move generator might produce +// in some other pawn shape) but to ask the engine's own move generator +// and check detector directly, the same way this whole function +// already avoids hand-rolled legality logic elsewhere. Assumes +// GenerateMoves has already been run for ctx's current position/ply +// (true at both call sites below, which reuse the move list already +// built for the stalemate check). +// +static FLAG +_ColorHasCheckingPawnPush(IN OUT SEARCHER_THREAD_CONTEXT *ctx) +{ + POSITION *pos = &ctx->sPosition; + ULONG u; + + for (u = ctx->sMoveStack.uBegin[ctx->uPly]; + u < ctx->sMoveStack.uEnd[ctx->uPly]; + u++) + { + MOVE mv = ctx->sMoveStack.mvf[u].mv; + FLAG fGivesCheck; + + if (FALSE == IS_PAWN(mv.pMoved)) + { + continue; + } + // + // Only a push landing on the mover's OWN pending-queen-adjacent + // rank matters here -- that's what turns a mere tempo-stealing + // check into a real "queens next move" threat (see + // _ColorHasPendingQueen), which is the actual danger the + // original counterexample (c3-c2+, then c1=Q) showed. A check + // from a pawn NOT landing there -- confirmed via a real false + // positive on the motivating example itself, f5-f4+ two ranks + // from queening in 8/7P/6p1/3k1p2/2p5/3pK3/8/8 w - -.-- costs a + // tempo but touches no material, utterly harmless when the + // stronger side just safely gained a whole queen. + // + if (0 == (COOR_TO_BB(mv.cTo) & + ((WHITE == GET_COLOR(mv.pMoved)) ? BBRANK77 : BBRANK22))) + { + continue; + } + if (FALSE == MakeMove(ctx, mv)) + { + ASSERT(FALSE); + continue; + } + fGivesCheck = InCheck(pos, pos->uToMove); + UnmakeMove(ctx, mv); + if (TRUE == fGivesCheck) + { + return(TRUE); + } + } + return(FALSE); +} + +// +// Does uOpp have ANY pawn past its own halfway line (ranks 4-7 for +// White, ranks 2-5 for Black) that OUR OWN king (uMoverColor) is too +// far away from to help our brand new queen deal with? Originally +// written narrower (only fire on a CONNECTED, adjacent-file pair -- +// "two mutually-defended passers overwhelm a lone queen"), found via +// 8/4Pp2/8/1K6/4p3/5p2/8/7k w - -. That version missed a related +// counterexample with only ONE relevant advanced pawn (c3) diagnosed by +// hand, 8/P4K2/4p3/8/8/2p5/8/4k3 w - - and its one-move-later sibling +// Q7/5K2/4p3/8/8/8/2p5/4k3 w - -: White's king (f7) happens to be near +// a DIFFERENT, irrelevant Black pawn (e6, nowhere near queening) while +// the actually-dangerous c3 pawn is far away and unconnected to +// anything -- checking "is the king near SOME pawn in the zone" (the +// original version, scanning for connectivity across the whole zone +// bitboard) can't tell relevant from irrelevant proximity. Fixed by +// requiring the king be close to EVERY pawn in the zone, individually +// -- a single one too far away is enough to disqualify, connected or +// not: a lone queen can't always be assumed to handle even ONE +// sufficiently advanced passer alone when its own king isn't around +// (checking forever is a draw; letting it queen and having to trade +// the queen for it is a draw), any more than it can handle two +// connected ones. +// +// UNSUPPORTED_DUO_KING_DISTANCE is a coarse, empirically-tuned proximity +// gate (see testrecogn.c's TestRecognSampleKPvKP), not a proof that any +// particular distance is exactly the boundary between "queen can still +// handle this alone" and "needs king help" -- consistent with this +// whole investigation's approach of tuning cheap, conservative +// thresholds against real counterexamples rather than deriving them +// from first principles. +// +#define UNSUPPORTED_DUO_KING_DISTANCE (2) + +static FLAG +_ColorHasUnsupportedAdvancedPawn(IN POSITION *pos, + IN ULONG uOpp, + IN ULONG uMoverColor) +{ + BITBOARD bb = pos->bbPawns[uOpp] & + ((WHITE == uOpp) ? (BBRANK77 | BBRANK66 | BBRANK55 | BBRANK44) + : (BBRANK22 | BBRANK33 | BBRANK44 | BBRANK55)); + COOR cKing = KingPosition(pos, uMoverColor); + COOR c; + + while (IS_ON_BOARD(c = CoorFromBitBoardRank8ToRank1(&bb))) + { + // Rook-file pawns get one tempo less slack: found via a real + // counterexample (8/5K2/7P/8/8/3k4/6p1/8 b - -, White's Ph6 + // exactly AT the general threshold) that a plain distance + // check still let through. Same "rook pawns are a special, + // MORE resilient case" theme that recurs throughout this file + // (the KPK critical-square 2-tempo margin, the escorted-passer + // rook-file exclusion above) -- corner-adjacent stalemate/ + // fortress resources give the defender extra reach a center + // pawn doesn't have, so the same raw king distance that's safe + // for an internal file isn't safe here. + ULONG uMargin = ((A == FILE(c)) || (H == FILE(c))) ? + (UNSUPPORTED_DUO_KING_DISTANCE - 2) : UNSUPPORTED_DUO_KING_DISTANCE; + COOR cQueen = QUEENING_SQUARE_BY_COLOR_FILE[uOpp][FILE(c)]; + if (DISTANCE(cKing, cQueen) > uMargin) + { + return(TRUE); // this pawn is too far from our king to trust + // our queen to handle it alone -- disqualify + } + } + return(FALSE); // king's close enough to every zone pawn there is +} + +// +// Is uColor's king within one square of any board corner? Found via two +// real counterexamples that survived every other check here +// (6K1/8/7P/8/8/7k/1p6/8 b - -, Black's Kh3 tucked next to h1; +// 8/2P5/8/8/8/2K2p2/8/7k w - -, Black's Kh1 in the corner itself): a +// defending king already in or hugging a corner creates stalemate- +// adjacent resources that a plain king-to-pawn distance measurement +// (_ColorHasUnsupportedAdvancedPawn) doesn't see at all -- the exact +// same corner-stalemate hazard already documented at length elsewhere +// in this file (_RecognizeKPK's original 7k/8/5KP1/8/8/8/8/8 w - - +// writeup, and the rook-pawn exclusion in +// _ColorHasEscortedAdvancedPasser). Deliberately coarse (ANY corner, +// distance<=1, no attempt to characterize exactly which corner +// configurations are actually dangerous) -- consistent with this +// investigation's whole approach of bailing to UNRECOGNIZED rather than +// precisely characterizing a hazardous boundary. +// +static FLAG +_ColorKingNearCorner(IN POSITION *pos, IN ULONG uColor) +{ + COOR cKing = KingPosition(pos, uColor); + static const COOR rgcCorner[4] = { A1, A8, H1, H8 }; + ULONG u; + + for (u = 0; u < 4; u++) + { + if (DISTANCE(cKing, rgcCorner[u]) <= 1) + { + return(TRUE); + } + } + return(FALSE); +} + +// +// Is uColor's king on A3/H3/A6/H6 with its OWN pawn sitting directly +// behind it on A2/H2/A7/H7 respectively? Found via all 8 of the +// remaining exhaustive-KPvKP counterexamples at this point in the +// investigation (e.g. 8/pP6/k7/8/K7/8/8/8 w - -, Black Ka6+Pa7): the +// king is on the edge file, can't retreat (its own pawn blocks the +// only square behind it), can't advance either (the enemy king or +// board edge takes the other square), and is left with only 2 legal +// squares -- both on the file the newly-promoted queen is about to +// fully control. The queening move that looked completely safe by +// every other check here (destination unattacked, no stalemate by a +// naive pseudo-legal move count, no pending/checking/connected/ +// escorted opposing pawn) turns out to stalemate the opponent outright. +// Deliberately a small, exact, hardcoded table rather than a general +// "count the king's genuinely legal escape squares" geometric +// computation -- the latter would need to reason about pawn-attacked +// squares and enemy-king-adjacency in general, which starts sliding +// back toward reimplementing move generation by hand (exactly what +// this function is meant to avoid); this exact shape is what every +// counterexample found so far actually looks like, so it's what gets +// coded, not a generalization of it. +// +// Also requires the QUEENING pawn's own file be B or G -- the file +// immediately next to the trapped corner, whose queen is what actually +// sweeps the king's 2 remaining squares. A queening pawn on any other +// file doesn't produce a queen that controls those particular squares, +// so this shape wouldn't apply to it. +// +static FLAG +_ColorKingTrappedByOwnRookPawn(IN POSITION *pos, IN ULONG uColor, + IN ULONG uPawnFile) +{ + static const COOR rgcKing[4] = { A3, H3, A6, H6 }; + static const COOR rgcPawn[4] = { A2, H2, A7, H7 }; + static const ULONG rguRequiredPawnFile[4] = { B, G, B, G }; + COOR cKing = KingPosition(pos, uColor); + ULONG u; + + for (u = 0; u < 4; u++) + { + if ((cKing == rgcKing[u]) && + (uPawnFile == rguRequiredPawnFile[u]) && + IS_PAWN(pos->rgSquare[rgcPawn[u]].pPiece) && + (GET_COLOR(pos->rgSquare[rgcPawn[u]].pPiece) == uColor)) + { + return(TRUE); + } + } + return(FALSE); +} + +// +// Is cPawn (belonging to uColor) confidently safe to queen, using ONLY +// static, ctx-free positional analysis -- no move generation, no +// MakeMove/UnmakeMove? This is every check from this investigation's +// KPvKP recognizer EXCEPT the two genuinely move-generation-dependent +// ones (immediate stalemate, a checking pawn push landing on the +// opponent's pending-queen rank) -- see _HasImmediateSafeQueen (below) +// for those, and HasStaticUnstoppablePasser (next) for the per-color +// entry point. Exposed (non-static, declared in chess.h) specifically +// so eval.c can call this SAME logic for a soft "unstoppable passer" +// EVAL_TERM bonus: eval.c's Eval() runs interleaved with search's own +// move-generation/iteration at the current ply (a caller may already be +// mid-iteration over ctx->sMoveStack for that same ply), so anything in +// Eval() that called GenerateMoves/MakeMove there would corrupt that +// caller's move list out from under it -- exactly why none of eval.c's +// existing code touches move generation at all, and exactly why this +// function deliberately doesn't either. Missing the stalemate/ +// checking-push checks means eval.c's soft version can occasionally be +// wrong in ways this file's exhaustively-verified recogn.c version +// isn't -- acceptable for an eval.c bonus (see this whole +// investigation's very first framing: recogn.c must never be wrong, +// eval.c is allowed to be a little wrong), not acceptable for a +// RECOGN_LOWER/UPPER hard bound. +// +FLAG +IsStaticSafeQueeningPawn(IN POSITION *pos, IN ULONG uColor, IN COOR cPawn) +{ + ULONG uOpp = FLIP(uColor); + COOR cQueen = QUEENING_SQUARE_BY_COLOR_FILE[uColor][FILE(cPawn)]; + + if (FALSE == _PawnPromotionPathClear(pos, uColor, cPawn)) + { + return(FALSE); + } + if (TRUE == IsAttacked(cQueen, pos, uOpp)) + { + return(FALSE); + } + if (TRUE == _ColorHasPendingQueen(pos, uOpp)) + { + return(FALSE); + } + if (TRUE == _ColorHasMultipleAdvancedPassers(pos, uOpp)) + { + return(FALSE); + } + if (TRUE == _ColorHasEscortedAdvancedPasser(pos, uOpp)) + { + return(FALSE); + } + if (TRUE == _ColorHasUnsupportedAdvancedPawn(pos, uOpp, uColor)) + { + return(FALSE); + } + if (TRUE == _ColorKingNearCorner(pos, uOpp)) + { + return(FALSE); + } + if (TRUE == _ColorKingTrappedByOwnRookPawn(pos, uOpp, FILE(cPawn))) + { + return(FALSE); + } + return(TRUE); +} + +// +// uColor's version of "does ANY pawn pass IsStaticSafeQueeningPawn" -- +// the per-color entry point eval.c actually calls. Only ever examines +// pawns already on the pending-queen rank (7th/2nd); see +// IsStaticSafeQueeningPawn for everything that has to be true beyond +// that. +// +FLAG +HasStaticUnstoppablePasser(IN POSITION *pos, IN ULONG uColor, OUT COOR *pcPawn) +{ + BITBOARD bb = pos->bbPawns[uColor] & + ((WHITE == uColor) ? BBRANK77 : BBRANK22); + COOR c; + + while (IS_ON_BOARD(c = CoorFromBitBoardRank8ToRank1(&bb))) + { + if (TRUE == IsStaticSafeQueeningPawn(pos, uColor, c)) + { + *pcPawn = c; + return(TRUE); + } + } + return(FALSE); +} + +static FLAG +_HasImmediateSafeQueen(IN OUT SEARCHER_THREAD_CONTEXT *ctx, OUT COOR *pcPawn) +{ + POSITION *pos = &ctx->sPosition; + ULONG uColor = pos->uToMove; + ULONG uOpp = FLIP(uColor); + FLAG fInCheck = InCheck(pos, uColor); + ULONG u; + FLAG fFound = FALSE; + + if (0 == (pos->bbPawns[uColor] & + ((WHITE == uColor) ? BBRANK77 : BBRANK22))) + { + return(FALSE); + } + + // + // Cheap, ctx-free positional pre-filter first (see + // IsStaticSafeQueeningPawn -- shared with eval.c's soft + // _EvalUnstoppablePasser, which can't touch move generation at all; + // see that function's comment for why). If nothing even passes the + // static checks, there's no need to touch the move generator at + // all. + // + if (FALSE == HasStaticUnstoppablePasser(pos, uColor, pcPawn)) + { + return(FALSE); + } + + ctx->sPlyInfo[ctx->uPly].fInCheck = fInCheck; + GenerateMoves(ctx, NULLMOVE, + fInCheck ? GENERATE_ESCAPES : GENERATE_ALL_MOVES); + + for (u = ctx->sMoveStack.uBegin[ctx->uPly]; + (FALSE == fFound) && (u < ctx->sMoveStack.uEnd[ctx->uPly]); + u++) + { + MOVE mv = ctx->sMoveStack.mvf[u].mv; + FLAG fSafe; + + if ((0 == mv.pPromoted) || (FALSE == IS_QUEEN(mv.pPromoted)) || + (0 != mv.pCaptured) || (FALSE == IS_PAWN(mv.pMoved)) || + (FALSE == IsStaticSafeQueeningPawn(pos, uColor, mv.cFrom))) + { + continue; + } + + if (FALSE == MakeMove(ctx, mv)) + { + ASSERT(FALSE); + continue; + } + + // + // Everything reusable across eval.c has already been checked + // by IsStaticSafeQueeningPawn above -- only the two genuinely + // move-generation-dependent checks remain, which only recogn.c + // (never eval.c -- see this function's header comment) can + // afford to run. + // + { + FLAG fOppInCheck = InCheck(pos, uOpp); + ctx->sPlyInfo[ctx->uPly].fInCheck = fOppInCheck; + GenerateMoves(ctx, NULLMOVE, + fOppInCheck ? GENERATE_ESCAPES : GENERATE_ALL_MOVES); + fSafe = TRUE; + if ((ctx->sMoveStack.uBegin[ctx->uPly] == + ctx->sMoveStack.uEnd[ctx->uPly]) && + (FALSE == fOppInCheck)) + { + fSafe = FALSE; // stalemate -- the classic corner trick + } + else if (TRUE == _ColorHasCheckingPawnPush(ctx)) + { + fSafe = FALSE; // opponent buys a tempo with check, then + // queens next -- see + // _ColorHasCheckingPawnPush's comment + } + } + UnmakeMove(ctx, mv); + + if (TRUE == fSafe) + { + *pcPawn = mv.cFrom; + fFound = TRUE; + } + } + return(fFound); +} + +// +// Does uStrong's single pawn cPawn beat a bare uWeak king via the +// classic critical-square race, with the 2-tempo safety margin +// TestRecognExhaustiveKPK verified gives 0 wrong verdicts across all +// 165,676 EGTB-covered legal single-pawn KPK positions (a 1-tempo +// margin gave 35 wrong -- see the long-standing comment this was +// extracted from, still attached at _RecognizeKPK's call site below)? +// Extracted verbatim (no behavior change) from _RecognizeKPK's own +// lone-king branch so eval.c can reuse this SAME exhaustively-verified +// logic for its "unstoppable passer" bonus in the true-lone-king case +// (weak side has zero pawns) -- the "rule of the square" generalized +// with a king escort, not just a pawn already on the 7th/2nd rank with +// the move (that narrower case is IsStaticSafeQueeningPawn, above, for +// when the weak side still has pawns of its own). Pure geometry -- +// KingPosition/DISTANCE/_GetPassersCriticalSquares -- no move +// generation, so just as safe for eval.c to call as +// IsStaticSafeQueeningPawn is. +// +FLAG +IsUnstoppableLoneKingPasser(IN POSITION *pos, IN ULONG uStrong, IN COOR cPawn) +{ + ULONG uWeak = FLIP(uStrong); + COOR cCritical[3]; + ULONG uDist[2]; + ULONG u; + + uDist[uStrong] = DISTANCE(KingPosition(pos, uStrong), cPawn); + uDist[uWeak] = DISTANCE(KingPosition(pos, uWeak), cPawn); + if (uDist[uStrong] > uDist[uWeak]) + { + return(FALSE); + } + + _GetPassersCriticalSquares(uStrong, cPawn, cCritical); + for (u = 0; u < 3; u++) + { + uDist[uStrong] = DISTANCE(KingPosition(pos, uStrong), cCritical[u]); + uDist[uWeak] = DISTANCE(KingPosition(pos, uWeak), cCritical[u]); + if (uDist[uWeak] > 0) + { + uDist[uWeak] -= (pos->uToMove == uWeak); + } + if (uDist[uStrong] > 0) + { + uDist[uStrong] += (pos->uToMove == uWeak) * 2; + } + if (uDist[uStrong] + 1 < uDist[uWeak]) + { + return(TRUE); + } + } + return(FALSE); +} static ULONG _RecognizeKPK(IN SEARCHER_THREAD_CONTEXT *ctx, @@ -975,9 +1616,6 @@ Return value: ULONG uStrong; ULONG uWeak; COOR cPawn, cQueen; - COOR cCritical[3]; - ULONG uDist[2]; - ULONG u; PAWN_HASH_ENTRY *pHash; POSITION *pos = &ctx->sPosition; @@ -994,12 +1632,13 @@ Return value: if (pHash->u64Key == pos->u64PawnSig) { pos->iScore[BLACK] = pos->iScore[WHITE] = 0; - if (TRUE == _EvalPasserRacesAgainstLoneKings(pos, pHash)) + if (TRUE == _EvalPasserRacesAgainstLoneKings(ctx, pHash)) { // - // Someone wins -- but only trust this as a hard bound when - // the "losing" side has no pawns of its own left. 2026-09-17: - // fuzzing found _EvalPasserRacesAgainstLoneKings only checks + // Someone wins -- but only trust this as a hard bound in + // the two shapes it's been exhaustively/statistically + // proven safe in, not universally. 2026-09-17: fuzzing + // found _EvalPasserRacesAgainstLoneKings only checks // whether a pawn already flagged "passed" outruns the enemy // king -- it has no way to see a *connected*-pawn threat // where neither pawn is individually passed (each still @@ -1015,14 +1654,38 @@ Return value: // -- but it's safe there only because it's a soft eval // nudge feeding real alpha-beta search, never a hard, // search-terminating bound the way RECOGN_LOWER/UPPER is - // here. Restricting to "loser has zero pawns" is the one - // case this heuristic can't be undermined by a duo like - // this, since there's no pawn left to produce one. + // here. + // + // Two trusted shapes, both re-verified against the CURRENT + // _EvalPasserRacesAgainstLoneKings (thresholds/fork-overload + // fix included) before this was wired in, not just assumed + // from an earlier version's numbers: + // 1. Loser has zero pawns left -- no pawn to produce a + // connected-duo counterplay like the Pc5 case above, + // since there's nothing left to connect. + // 2. Exact one-pawn-per-side KPKP -- exhaustively checked + // against EGTB (testrecogn.c's + // TestEvalPasserRacesAgainstLoneKings): 0 wrong across + // all 14,872,176 EGTB-covered legal KPKP positions. + // With only one pawn per side there is no second pawn + // to form a duo with, so the exact same blind spot the + // Pc5 case exploits structurally cannot occur here + // either -- this isn't a narrower coincidental case of + // #1, it's independently safe for the same underlying + // reason. Any other multi-pawn-per-side shape (KPPKPP, + // KPKPPP, ...) measured a real ~1-5% wrong rate via + // random EGTB sampling (testrecogn.c's + // TestEvalSamplePasserRacesKPPKPP/KPKPPP) and stays + // OUT of this hard-bound trust -- falls through to the + // UNRECOGNIZED/sophisticated-analysis logic below same + // as always. // ASSERT((pos->iScore[BLACK] == 0) || (pos->iScore[WHITE] == 0)); ASSERT((pos->iScore[BLACK] != 0) || (pos->iScore[WHITE] != 0)); if ((pos->iScore[BLACK] > 0) && - (0 == FastCountBits(pos->bbPawns[WHITE]))) + ((0 == FastCountBits(pos->bbPawns[WHITE])) || + ((1 == FastCountBits(pos->bbPawns[WHITE])) && + (1 == FastCountBits(pos->bbPawns[BLACK]))))) { ASSERT(pos->iScore[WHITE] == 0); *piScore = (pos->iMaterialBalance[BLACK] + 2 * VALUE_PAWN + @@ -1036,7 +1699,9 @@ Return value: return(RECOGN_LOWER); } else if ((pos->iScore[WHITE] > 0) && - (0 == FastCountBits(pos->bbPawns[BLACK]))) + ((0 == FastCountBits(pos->bbPawns[BLACK])) || + ((1 == FastCountBits(pos->bbPawns[BLACK])) && + (1 == FastCountBits(pos->bbPawns[WHITE]))))) { *piScore = (pos->iMaterialBalance[WHITE] + 2 * VALUE_PAWN + pos->iScore[WHITE]); @@ -1048,7 +1713,7 @@ Return value: } return(RECOGN_LOWER); } - // Else: the "losing" side still has pawns of its own -- + // Else: the "losing" side still has 2+ pawns of its own -- // don't trust the race verdict as a hard bound. Fall // through to the UNRECOGNIZED/sophisticated-analysis logic // below, same as if _EvalPasserRacesAgainstLoneKings had @@ -1057,10 +1722,31 @@ Return value: } // - // No one has a winning passer or we did not find the hash entry. + // No one has a winning passer (per the lone-king-only fast path + // above) or we did not find the hash entry. // if ((FastCountBits(pos->bbPawns[WHITE]) > 0) && (FastCountBits(pos->bbPawns[BLACK]) > 0)) { + // + // Both sides still have pawns -- the general KP+KP* shape. Only + // coverage attempted here: the side to move has a pawn that + // safely queens THIS move (_HasImmediateSafeQueen) -- see its + // comment for why this narrow question is answerable at all + // when a general race-distance comparison was measured not to + // be (~15% wrong no matter the safety margin). Everything else + // in real king-and-pawn endgame theory (races that take more + // than one move, breakthroughs, outside passers, opposition, + // mutual zugzwang) is left as UNRECOGNIZED -- never wrong, not + // always right. + // + COOR cPawn; + if (TRUE == _HasImmediateSafeQueen(ctx, &cPawn)) + { + ULONG uColor = pos->uToMove; + *piScore = (pos->iMaterialBalance[uColor] + VALUE_QUEEN + + (2 * VALUE_PAWN)); + return(RECOGN_LOWER); + } return(UNRECOGNIZED); } @@ -1099,83 +1785,18 @@ Return value: ASSERT(IS_ON_BOARD(cPawn)); ASSERT(IS_PAWN(pos->rgSquare[cPawn].pPiece)); - // - // Step 1: the strong king must be closer to the pawn than - // the weak king. - // - uDist[uStrong] = DISTANCE(KingPosition(pos, uStrong), cPawn); - ASSERT((1 <= uDist[uStrong]) && (uDist[uStrong] <= 7)); - uDist[uWeak] = DISTANCE(KingPosition(pos, uWeak), cPawn); - ASSERT((1 <= uDist[uWeak]) && (uDist[uWeak] <= 7)); - if (uDist[uStrong] <= uDist[uWeak]) + if (TRUE == IsUnstoppableLoneKingPasser(pos, uStrong, cPawn)) { - // - // Step 2: the strong king must be closer to one of the - // passer's critical squares than the weak king. - // - _GetPassersCriticalSquares(uStrong, cPawn, cCritical); - for (u = 0; u < 3; u++) + cQueen = QUEENING_SQUARE_BY_COLOR_FILE[uStrong][FILE(cPawn)]; + *piScore = (pos->iMaterialBalance[uStrong] + + VALUE_QUEEN + (2 * VALUE_PAWN) - + (RANK_DISTANCE(cPawn, cQueen) * 32)); + if (pos->uToMove == uWeak) { - uDist[uStrong] = DISTANCE(KingPosition(pos, uStrong), - cCritical[u]); - ASSERT((0 <= uDist[uStrong]) && (uDist[uStrong] <= 7)); - uDist[uWeak] = DISTANCE(KingPosition(pos, uWeak), - cCritical[u]); - - // - // Assume if the weak side is on move he will move - // towards the critical square. Also assume that - // in so doing he will increase the path that the - // strong king must take to get to the critical - // square. - // - if (uDist[uWeak] > 0) { - uDist[uWeak] -= (pos->uToMove == uWeak); - } - if (uDist[uStrong] > 0) { - uDist[uStrong] += (pos->uToMove == uWeak) * 2; - } - ASSERT((0 <= uDist[uWeak]) && (uDist[uWeak] <= 7)); - // - // Was `uDist[uStrong] < uDist[uWeak]` (any margin at all). - // Fine's rule ("if the attacking king reaches a key square - // first, the pawn wins regardless of who's on move") - // doesn't actually license a razor-thin margin here -- - // the tempo fudge-factor above (+1/-2 per side when the - // weak side is on move) is itself an approximation, and a - // 1-tempo-or-less margin turned out to go wrong for real - // stalemate/zugzwang reasons across multiple files and - // configurations, not just the bishop-pawn corner case - // this was first found from (7k/8/5KP1/8/8/8/8/8 w - -, - // Kf6/Pg6/Kh8 White to move -- this recognizer used to - // claim +11.33/win via Kf7, which actually stalemates - // Black; real value is a draw). Requiring a 2+ tempo - // margin instead was verified to fully close the gap via - // TestRecognExhaustiveKPK (testrecogn.c): 0 wrong verdicts - // across all 165,676 EGTB-covered legal single-pawn KPK - // positions, vs. 35 wrong with the razor-thin margin. Two - // narrower, file-specific guards (bishop-pawn-only here, - // and a matching one in eval.c's - // _EvalPasserRacesAgainstLoneKings) were tried first and - // both proved unnecessary once this margin was in place -- - // removed rather than left as redundant dead code, see git - // log for the full investigation if this needs revisiting. - // - if (uDist[uStrong] + 1 < uDist[uWeak]) - { - cQueen = - QUEENING_SQUARE_BY_COLOR_FILE[uStrong][FILE(cPawn)]; - *piScore = (pos->iMaterialBalance[uStrong] + - VALUE_QUEEN + (2 * VALUE_PAWN) - - (RANK_DISTANCE(cPawn, cQueen) * 32)); - if (pos->uToMove == uWeak) - { - *piScore *= -1; - return(RECOGN_UPPER); - } - return(RECOGN_LOWER); - } + *piScore *= -1; + return(RECOGN_UPPER); } + return(RECOGN_LOWER); } // 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 |
