summaryrefslogtreecommitdiff
path: root/src/recogn.c
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-09-25 08:44:25 -0700
committerScott Gasch <[email protected]>2026-09-25 08:44:25 -0700
commit9b28080f8b3dca4588cb2aaaf748451f45e3c626 (patch)
tree54e0aff5d6ee1520c590c7c04a5ae2f8299e7497 /src/recogn.c
parent66740dc89c7351c120c25788d0079055034f4942 (diff)
Two ideas:
1. Nullmove R=3 + depth/6 along with some ShouldWeDoNullmoveSearch tweaking and calibrating. 2. Probe EGTB everywhere in the search as long as the table/position is resident in memory.
Diffstat (limited to 'src/recogn.c')
-rw-r--r--src/recogn.c234
1 files changed, 218 insertions, 16 deletions
diff --git a/src/recogn.c b/src/recogn.c
index 2219e48..427297b 100644
--- a/src/recogn.c
+++ b/src/recogn.c
@@ -719,6 +719,169 @@ Return value:
}
+ULONG
+_RecognizeKQKP(IN SEARCHER_THREAD_CONTEXT *ctx,
+ IN OUT SCORE *piScore)
+/**
+
+Routine description:
+
+ Recognize KQKP positions: exactly one queen vs exactly one pawn.
+ Deliberately narrow -- only handles the classically UNCONDITIONAL
+ win subset: the pawn has NOT yet reached the rank immediately
+ before queening (rank 7 for a White pawn, rank 2 for a Black one),
+ and the queen isn't hanging to the weak king or weak pawn (see
+ below). Once the weak pawn reaches that last rank it threatens to
+ promote into a second queen -- uncontested if nothing stops it, or
+ even a capturing promotion if it lands on the strong queen's square
+ -- either of which can rebalance material into a generic (and often
+ drawn) queen-vs-queen ending, regardless of which file the pawn is
+ on. That whole family of positions returns UNRECOGNIZED and falls
+ through to search/EGTB, same as every other case this function
+ doesn't recognize.
+
+ 2026-09-24, in order of discovery (all via TestRecognExhaustiveKQKP,
+ each one found immediately after "fixing" the previous):
+
+ First hole -- ~10% wrong across the enumeration, all one shape: the
+ queen placed adjacent to the weak king, undefended, with the weak
+ side to move (e.g. K1kQ4/p7/8/8/8/8/8/8 b - - 0 0 -- White Ka8,
+ Black Kc8, White Qd8, Black Pa7, Black to move: ...Kxd8 wins the
+ queen for free). The original version of this function never
+ considered queen safety at all.
+
+ Second hole, found immediately after fixing the first (king-
+ adjacency only got the wrong rate down to ~4%, not to zero): the
+ WEAK PAWN capturing the queen while promoting, e.g.
+ k1K1q3/3P4/8/8/8/8/8/8 w - - 0 0 (Black Ka8/Qe8, White Kc8/Pd7,
+ White to move) -- dxe8=Q captures Black's queen and promotes in the
+ same move, which EGTB confirms is winning for White even though
+ White has the pawn. Fixed by also checking whether the queen sits
+ on a square the weak pawn attacks (g_PawnAttackOriginBB), not just
+ king adjacency, and by dropping the earlier "unless defended by the
+ strong king" carve-out entirely for both threats -- a pawn capture-
+ promotion is catastrophic even if recaptured (nets down to a bare-
+ kings draw at best, never the claimed win).
+
+ Third hole, found immediately after fixing the second: the original
+ rook/bishop-file-only exclusion for a pawn on the queening-adjacent
+ rank missed the exact same "uncontested promotion creates a second
+ queen" problem on every OTHER file too, e.g.
+ k1K2q2/3P4/8/8/8/8/8/8 w - - 0 0 (White's d-pawn just pushes to
+ d8=Q, an ordinary center-file pawn, and EGTB says the resulting
+ KQKQ position is a draw, not the claimed win) and
+ K1k5/Q7/8/8/8/8/1p6/8 b - - 0 0 (Black's b2 knight-file pawn simply
+ promotes THIS move, ...b1=Q, queen too far away to stop it). Fixed
+ by dropping the rook/bishop-file restriction entirely: ANY pawn on
+ the rank immediately before queening is now excluded, not just
+ rook/bishop pawns.
+
+ After all three fixes: no more confidence left in reasoning our way
+ to a boundary here without re-deriving it from EGTB -- every "safe"
+ exception considered above (defended queen, non-rook/bishop file)
+ turned out to have a counterexample the first time it was actually
+ checked. Current scope is deliberately conservative as a result.
+
+Parameters:
+
+ SEARCHER_THREAD_CONTEXT *ctx,
+ SCORE *piScore
+
+Return value:
+
+ static ULONG
+
+**/
+{
+ ULONG uStrong, uWeak;
+ COOR cPawn, cQueen, cWeakKing;
+ POSITION *pos = &ctx->sPosition;
+
+ ASSERT((TotalNonPawnCount(pos, WHITE) <= 3) &&
+ (TotalNonPawnCount(pos, BLACK) <= 3));
+ ASSERT(_NothingBut(pos, PAWN | QUEEN, WHITE));
+ ASSERT(_NothingBut(pos, PAWN | QUEEN, BLACK));
+
+ //
+ // Call the side with the queen "strong"
+ //
+ uStrong = WHITE;
+ if (TotalNonPawnCount(pos, BLACK) > 1)
+ {
+ uStrong = BLACK;
+ }
+ uWeak = FLIP(uStrong);
+ ASSERT(TotalNonPawnCount(pos, uStrong) > 1);
+ ASSERT(TotalNonPawnCount(pos, uWeak) == 1);
+
+ //
+ // Only proven for exactly one queen vs exactly one pawn.
+ //
+ if ((FastCountBits(pos->bbPieces[uStrong][QUEEN]) != 1) ||
+ (FastCountBits(pos->bbPawns[uWeak]) != 1))
+ {
+ return(UNRECOGNIZED);
+ }
+
+ //
+ // Queen safety: if the weak king can capture the queen, OR the weak
+ // pawn can capture the queen (necessarily while promoting, since
+ // the pawn has only one square left to go -- see this function's
+ // 2026-09-24 header comment for why that's catastrophic even when
+ // "defended"), this is not an unconditional win by any of the
+ // reasoning below. Bail regardless of whose move it actually is and
+ // regardless of whether the strong king defends the queen's square
+ // -- deliberately not trying to reason through defended-vs-
+ // undefended exceptions here again.
+ //
+ cQueen = CoorOfSingleBit(pos->bbPieces[uStrong][QUEEN]);
+ ASSERT(IS_ON_BOARD(cQueen));
+ cWeakKing = KingPosition(pos, uWeak);
+ if ((DISTANCE(cQueen, cWeakKing) == 1) ||
+ (0 != (g_PawnAttackOriginBB[uWeak][cQueen] & pos->bbPawns[uWeak])))
+ {
+ return(UNRECOGNIZED);
+ }
+
+ cPawn = CoorOfSingleBit(pos->bbPawns[uWeak]);
+ ASSERT(IS_ON_BOARD(cPawn));
+ ASSERT(IS_PAWN(pos->rgSquare[cPawn].pPiece));
+
+ //
+ // Any pawn on the rank immediately before queening -- not just a
+ // rook/bishop pawn -- threatens to promote uncontested next move
+ // (or, if it's the weak side's own move right now, THIS move),
+ // producing a second queen and rebalancing material regardless of
+ // file. See this function's 2026-09-24 header comment: the file-
+ // restricted version of this check missed exactly this on a
+ // knight-file pawn (K1k5/Q7/8/8/8/8/1p6/8 b - -, Black's b2 pawn,
+ // queen too far away to stop ...b1=Q) as well as the original
+ // rook/bishop-file "fortress" reasoning it was written for.
+ //
+ if (((WHITE == uWeak) && (TRUE == RANK7(cPawn))) ||
+ ((BLACK == uWeak) && (TRUE == RANK2(cPawn))))
+ {
+ return(UNRECOGNIZED);
+ }
+
+ //
+ // Every other pawn placement is an unconditional win for the queen
+ // side with best play. Material balance is already reliably very
+ // positive for the queen side here (a queen vastly outweighs a lone
+ // pawn), so it's safe to use directly as a directional bound -- we
+ // don't need (or attempt) a precise mate-distance score, just a
+ // correctly-signed one.
+ //
+ *piScore = pos->iMaterialBalance[uStrong];
+ if (pos->uToMove == uWeak)
+ {
+ *piScore *= -1;
+ return(RECOGN_UPPER);
+ }
+ return(RECOGN_LOWER);
+}
+
+
static ULONG
_RecognizeKBKP(IN SEARCHER_THREAD_CONTEXT *ctx,
IN OUT SCORE *piScore)
@@ -1912,6 +2075,27 @@ Return value:
_MakeMaterialSig(1, 0, 0, 0, 0),
_MakeMaterialSig(0, 1, 0, 0, 0));
+ // KQ+KP+ DISABLED (like KBNK above): TestRecognExhaustiveKQKP found
+ // three structurally distinct correctness holes in a row, each only
+ // surfacing once the previous one was fixed -- queen safety vs. the
+ // weak king, queen safety vs. a promoting weak pawn, and any weak
+ // pawn (not just rook/bishop-file) reaching the queening-adjacent
+ // rank threatening to rebalance material via a second queen. After
+ // those three fixes, weak-side stalemate accounted for only 1,746
+ // of the remaining 42,398 wrong positions (12.7M checked) -- the
+ // other 40,652 are a fourth, still-unidentified failure mode. Given
+ // that pattern, this "queen always beats a lone pawn" classical
+ // claim has more real exceptions across the full space of legal
+ // positions than can safely be hand-derived from memory one fix at
+ // a time. Not registered until someone re-derives the actual
+ // correct boundary (most plausibly by deriving it FROM Syzygy WDL
+ // data rather than guessing and re-testing) and re-validates via
+ // TestRecognExhaustiveKQKP.
+ //
+ // _NewRecognizer(_RecognizeKQKP,
+ // _MakeMaterialSig(1, 0, 0, 0, 0),
+ // _MakeMaterialSig(0, 0, 0, 0, 1));
+
// KB+KP+ P N B R Q
_NewRecognizer(_RecognizeKBKP,
_MakeMaterialSig(1, 0, 0, 0, 0),
@@ -1943,7 +2127,7 @@ Return value:
ULONG
RecognLookup(IN SEARCHER_THREAD_CONTEXT *ctx,
IN OUT SCORE *piScore,
- IN FLAG fProbeEGTB)
+ IN ULONG uEgtbProbeMode)
/**
Routine description:
@@ -1954,11 +2138,23 @@ Routine description:
present on the board and 2. probe on-disk EGTB files if no useful
recognizer result is found.
+ uEgtbProbeMode is trinary (EGTB_PROBE_NEVER / EGTB_PROBE_ALWAYS /
+ EGTB_PROBE_IF_RESIDENT, chess.h) rather than a plain FLAG so that
+ "probe iff we believe the page is resident" can be decided in here,
+ on only the branch that's actually about to call ProbeEGTB, instead
+ of the caller computing EgtbProbeLikelyResident unconditionally as
+ a function argument before even knowing whether an interior
+ recognizer below is about to make the question moot. This matters
+ more than it used to: EgtbProbeLikelyResident can now involve real
+ mincore() syscalls for 6-man material (see its own comment in
+ probe.c) rather than just a cheap atomic load, so deferring it here
+ directly cuts how often that runs.
+
Parameters:
SEARCHER_THREAD_CONTEXT *ctx,
SCORE *piScore,
- FLAG fProbeEGTB
+ ULONG uEgtbProbeMode
Return value:
@@ -1972,9 +2168,7 @@ Return value:
RECOGNIZER *pRec;
SCORE iScore;
- //
- // Try interior node recognizers
- //
+ // Try interior node recognizers first.
if ((TotalNonPawnCount(pos, WHITE) <= 3) &&
(TotalNonPawnCount(pos, BLACK) <= 3))
{
@@ -2017,14 +2211,12 @@ Return value:
}
}
#endif
- ASSERT(_SanityCheckRecognizers(ctx, iScore, uVal));
return(uVal);
}
}
}
}
- //
// Try EGTB probe. No piece-count gate here: ProbeEGTB already
// checks the position's piece count against the dynamic
// TB_LARGEST (set from whatever tablebase files Fathom actually
@@ -2034,17 +2226,27 @@ Return value:
// ever added alongside the 5-man set already present -- see
// CLAUDE.md).
//
- if (FALSE != fProbeEGTB)
+ // EGTB_PROBE_IF_RESIDENT's EgtbProbeLikelyResident call happens
+ // right here, not in the caller -- this is the whole point of
+ // uEgtbProbeMode being trinary instead of a plain FLAG (see this
+ // function's header comment): every node above that an interior
+ // recognizer already resolved returned before ever reaching this
+ // line, so it never pays for a residency check (which can mean
+ // real mincore() syscalls for 6-man material) it wouldn't use.
+ if (EGTB_PROBE_NEVER != uEgtbProbeMode)
{
- if (TRUE == ProbeEGTB(ctx, &iScore))
+ FLAG fShouldProbe = (EGTB_PROBE_ALWAYS == uEgtbProbeMode) ||
+ EgtbProbeLikelyResident(ctx);
+ if (TRUE == fShouldProbe)
{
- //
- // Syzygy WDL probes are win/draw/loss only -- there's no
- // mate distance to adjust for the present ply here (see
- // EGTB_WIN_SCORE / EGTB_LOSS_SCORE in probe.c).
- //
- *piScore = iScore;
- return(RECOGN_EXACT);
+ if (TRUE == ProbeEGTB(ctx, &iScore))
+ {
+ // Syzygy WDL probes are win/draw/loss only -- there's no
+ // mate distance to adjust for the present ply here (see
+ // EGTB_WIN_SCORE / EGTB_LOSS_SCORE in probe.c).
+ *piScore = iScore;
+ return(RECOGN_EXACT);
+ }
}
}
return(UNRECOGNIZED);