summaryrefslogtreecommitdiff
path: root/src/recogn.c
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-09-23 15:12:10 -0700
committerScott Gasch <[email protected]>2026-09-23 15:12:10 -0700
commitf0b56611d8ef5ce2d121f532931de0252628d995 (patch)
tree9db1896f7f8308b28adc500853e440f0f5122d8e /src/recogn.c
parentb7e4cdfeb3c1aadb3f488f6794e6d1a748dc6c8f (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.c813
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);
}
//