diff options
| author | Scott Gasch <[email protected]> | 2026-09-23 15:12:10 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-09-23 15:12:10 -0700 |
| commit | f0b56611d8ef5ce2d121f532931de0252628d995 (patch) | |
| tree | 9db1896f7f8308b28adc500853e440f0f5122d8e /src/recogn.c | |
| parent | b7e4cdfeb3c1aadb3f488f6794e6d1a748dc6c8f (diff) | |
Real chess knowledge in the KPK[P] recognizer. Sound (but not perfect)
knowledge in eval's EvalRacersAgainstLongKing code. Other minor changes.
Even in self-play.
Diffstat (limited to 'src/recogn.c')
| -rw-r--r-- | src/recogn.c | 813 |
1 files changed, 717 insertions, 96 deletions
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); } // |
