diff options
| author | Scott Gasch <[email protected]> | 2026-09-25 08:44:25 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-09-25 08:44:25 -0700 |
| commit | 9b28080f8b3dca4588cb2aaaf748451f45e3c626 (patch) | |
| tree | 54e0aff5d6ee1520c590c7c04a5ae2f8299e7497 /src/recogn.c | |
| parent | 66740dc89c7351c120c25788d0079055034f4942 (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.c | 234 |
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); |
