summaryrefslogtreecommitdiff
path: root/src/eval.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/eval.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/eval.c')
-rw-r--r--src/eval.c697
1 files changed, 485 insertions, 212 deletions
diff --git a/src/eval.c b/src/eval.c
index e84470b..3af0d92 100644
--- a/src/eval.c
+++ b/src/eval.c
@@ -89,8 +89,8 @@ const int g_iBehind[2] = { -1, +1 };
// { -10, 0, 0, +7, +15, +20, +24, +29, +36 }, // // 2
//};
-static ULONG REDUCED_MATERIAL_DOWN_SCALER[32] =
-{
+static ULONG REDUCED_MATERIAL_DOWN_SCALER[32] =
+{
// none na na m na R 2m na
0, 0, 0, 0, 0, 0, 0, 0,
@@ -98,7 +98,7 @@ static ULONG REDUCED_MATERIAL_DOWN_SCALER[32] =
0, 1, 1, 1, 1, 1, 2, 3,
// 2R2m R4m/QRm Q3m 2R3m/ QR2m Q4m 2R4m/ QR3m
-// Q2R Q2Rm
+// Q2R Q2Rm
4, 5, 6, 6, 7, 7, 7, 7,
// na Q2R2m QR4m na Q2R3m na na full
@@ -130,7 +130,7 @@ static ULONG CASTLE_AND_DEVELOPMENT_SCALER[32] =
};
static ULONG REDUCED_MATERIAL_UP_SCALER[32] =
-{
+{
// none na na m na R 2m na
8, 8, 8, 8, 8, 8, 8, 8,
@@ -138,15 +138,15 @@ static ULONG REDUCED_MATERIAL_UP_SCALER[32] =
8, 7, 7, 7, 7, 6, 6, 5,
// 2R2m R4m/QRm Q3m 2R3m/ QR2m Q4m 2R4m/ QR3m
-// Q2R Q2Rm
+// Q2R Q2Rm
4, 3, 3, 3, 2, 2, 2, 1,
// na Q2R2m QR4m na Q2R3m na na full
1, 1, 0, 0, 0, 0, 0, 0,
};
-static ULONG PASSER_MATERIAL_UP_SCALER[32] =
-{
+static ULONG PASSER_MATERIAL_UP_SCALER[32] =
+{
// none na na m na R 2m na
8, 8, 8, 8, 8, 8, 7, 7,
@@ -154,13 +154,19 @@ static ULONG PASSER_MATERIAL_UP_SCALER[32] =
7, 6, 7, 5, 4, 5, 4, 3,
// 2R2m R4m/QRm Q3m 2R3m/ QR2m Q4m 2R4m/ QR3m
-// Q2R Q2Rm
+// Q2R Q2Rm
2, 2, 1, 1, 0, 0, 0, 0,
// na Q2R2m QR4m na Q2R3m na na full
0, 0, 0, 0, 0, 0, 0, 0,
};
+static ULONG MULTIPLE_PASSER_SCALER[9] =
+{
+// 0 1 2 3 4 5 6 7 8
+ 0, 32, 27, 22, 18, 11, 8, 4, 2
+};
+
COOR QUEENING_RANK[2] = { 1, 8 };
COOR JUMPING_RANK[2] = { 7, 2 };
@@ -210,7 +216,7 @@ static SCORE PAWN_CENTRALITY_BONUS[128] =
// most exploitable. Table is indexed by absolute square (not
// color-relative), hence the rank2/rank7, rank3/rank6, rank4/rank5
// mirroring -- same as the original layout.
-static SCORE BACKWARD_SHIELDED_BY_LOCATION[128] =
+static SCORE BACKWARD_SHIELDED_BY_LOCATION[128] =
{
+0, +0, +0, +0, +0, +0, +0, +0, 0,0,0,0,0,0,0,0,
-7, -5, -5, -7, -7, -5, -5, -7, 0,0,0,0,0,0,0,0,
@@ -223,7 +229,7 @@ static SCORE BACKWARD_SHIELDED_BY_LOCATION[128] =
+0, +0, +0, +0, +0, +0, +0, +0, 0,0,0,0,0,0,0,0
};
-static SCORE BACKWARD_EXPOSED_BY_LOCATION[128] =
+static SCORE BACKWARD_EXPOSED_BY_LOCATION[128] =
{
+0, +0, +0, +0, +0, +0, +0, +0, 0,0,0,0,0,0,0,0,
-12, -9, -9, -11, -11, -9, -9, -12, 0,0,0,0,0,0,0,0,
@@ -256,7 +262,7 @@ static SCORE BACKWARD_EXPOSED_BY_LOCATION[128] =
// "All About Doubled Pawns"
//
// Note: indexed by number of files with 1+ pawn on them.
-static SCORE DOUBLED_PAWN_PENALTY_BY_COUNT[4][9] =
+static SCORE DOUBLED_PAWN_PENALTY_BY_COUNT[4][9] =
{ // ------------ count of doubled+ pawns ------------
{// 0 1 2 3 4 5 6 7 8 : no majors alive
+0, -32, -65, -99, -134, -170, -207, -222, -250
@@ -290,13 +296,13 @@ static SCORE ISOLATED_DOUBLED_PAWN = -7;
//
// Note: -25% to -33% if the enemy occupies or controls the next sq.
//
-static SCORE PASSER_BY_RANK[2][9] =
+static SCORE PASSER_BY_RANK[2][9] =
{// 0 1 2 3 4 5 6 7 8
{ +0, +0,+162,+111, +62, +36, +18, +13, +0 }, // black
{ +0, +0, +13, +18, +36, +62,+111,+162, +0 } // white
};
-static SCORE CANDIDATE_PASSER_BY_RANK[2][9] =
+static SCORE CANDIDATE_PASSER_BY_RANK[2][9] =
{// 0 1 2 3 4 5 6 7 8
{ +0, +0, +0, +48, +34, +22, +13, +9, +0 }, // black
{ +0, +0, +9, +13, +22, +34, +48, +0, +0 } // white
@@ -358,7 +364,7 @@ static SCORE PASSER_BONUS_AS_MATERIAL_COMES_OFF[32] =
+14, +9, +20, +4, +3, +3, 0, 0,
// 2R2m R4m/QRm Q3m 2R3m/ QR2m Q4m 2R4m/ QR3m
-// Q2R Q2Rm
+// Q2R Q2Rm
0, 0, 0, 0, 0, 0, 0, 0,
// na Q2R2m QR4m na Q2R3m na na full
@@ -468,7 +474,7 @@ static SCORE KNIGHT_CENTRALITY_BONUS[128] =
static SCORE KNIGHT_KING_TROPISM_BONUS[9] =
{// 0 1 2 3 4 5 6 7 8
- 0, +15, +12, +9, +4, +0, +0, +0, +0
+ 0, +15, +12, +9, +4, +0, +0, +0, +0
};
@@ -498,7 +504,7 @@ static SCORE KNIGHT_MOBILITY_BY_COUNT[9] =
-17, -10, -6, 0, +3, +5, +7, +8, +8
};// ^ |
-static SCORE KNIGHT_WITH_N_PAWNS_SUPPORTING[3] =
+static SCORE KNIGHT_WITH_N_PAWNS_SUPPORTING[3] =
{
+0, +4, +8
};
@@ -562,7 +568,7 @@ static SCORE ROOK_LEADS_PASSER_BY_PASSER_RANK[2][9] =
static SCORE KING_TRAPPING_ROOK = -40;
static SCORE ROOK_VALUE_AS_PAWNS_COME_OFF[17] =
-{// 0 1 2 3 4 5 6 7 8
+{// 0 1 2 3 4 5 6 7 8
+55, +51, +44, +38, +33, +27, +22, +16, +7,
// 9 10 11 12 13 14 15 16
+1, -3, -6, -9, -12, -18, -22, -25
@@ -728,7 +734,7 @@ static SCORE KING_TO_CENTER[128] =
#define KEEP_KING_AT_HOME_THRESHOLD \
(VALUE_ROOK + VALUE_ROOK + VALUE_BISHOP + VALUE_KING + 1)
-static ULONG KING_COUNTER_BY_ATTACK_PATTERN[32] =
+static ULONG KING_COUNTER_BY_ATTACK_PATTERN[32] =
{
//p 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
//m 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1
@@ -2139,8 +2145,8 @@ Return value:
}
-static PAWN_HASH_ENTRY *
-_EvalPawns(IN OUT SEARCHER_THREAD_CONTEXT *ctx,
+PAWN_HASH_ENTRY *
+EvalPawns(IN OUT SEARCHER_THREAD_CONTEXT *ctx,
OUT FLAG *pfDeferred)
/**
@@ -2148,6 +2154,11 @@ Routine description:
Evaluate pawn structures; return a ptr to a pawn hash entry.
+ Exposed (non-static) so testrecogn.c can build a real,
+ fully-populated PAWN_HASH_ENTRY (bbPasserLocations included) to
+ drive an EGTB cross-check of _EvalPasserRacesAgainstLoneKings --
+ same rationale as SideHasWinningChances's own exposure above.
+
Parameters:
SEARCHER_THREAD_CONTEXT *ctx : the searcher thread context
@@ -5550,34 +5561,151 @@ Return value:
}
+static FLAG
+_EvalUnstoppablePasser(IN OUT POSITION *pos)
+/**
+
+Routine description:
+
+ True-knowledge replacement for _EvalPasserRacesAgainstLoneKings
+ (below), used ONLY when both sides are pure king-and-pawns (both
+ TotalNonPawnCount == 1) -- the exact KPvKP shape this session's
+ investigation exhaustively verified. Calls the SAME shared,
+ exhaustively-verified routines recogn.c's hard-bound KPvKP
+ recognizer uses (IsUnstoppableLoneKingPasser for a true-lone-king
+ opponent's any-rank square-rule race, HasStaticUnstoppablePasser for
+ the general "opponent also has pawns" 7th/2nd-rank case), so there
+ is exactly one implementation of "is this pawn unstoppable," not two
+ that can silently diverge the way this file's old passer-race
+ heuristic and recogn.c's KPK recognizer once did (the whole reason
+ this investigation started).
+
+ _EvalPasserRacesAgainstLoneKings's own precondition is looser (fires
+ whenever EITHER side is bare, even if the OTHER side still has real
+ pieces, e.g. K+R+P vs K) -- that broader shape was never covered by
+ this session's KPvKP work and is NOT provably safe for the
+ shared routines here (their reasoning assumes no other pieces exist
+ to interpose/defend/escape), so the call site below keeps routing
+ that case to the old function unchanged.
+
+ Returns whether it actually placed a credit for either side. This
+ function is trustworthy but narrow -- it only ever fires for the
+ true-lone-king sub-case of pure KPvKP, and stays silent (returns
+ FALSE) for the "both sides still have pawns" sub-case rather than
+ guess. The caller uses this to fall back to
+ _EvalPasserRacesAgainstLoneKings (dodgy but broader-coverage) only
+ when this function had nothing to say, instead of losing that
+ coverage outright.
+
+Parameters:
+
+ POSITION *pos
+
+Return value:
+
+ FLAG -- TRUE if a credit was placed for either side
+
+**/
+{
+ ULONG uColor;
+ FLAG fFired = FALSE;
+
+ FOREACH_COLOR(uColor)
+ {
+ ULONG xColor = FLIP(uColor);
+ COOR c;
+
+ if (0 == FastCountBits(pos->bbPawns[uColor]))
+ {
+ continue;
+ }
+
+ if (0 == FastCountBits(pos->bbPawns[xColor]))
+ {
+ // True lone-king opponent: the general "rule of the
+ // square + king escort" race, any rank, not just the
+ // 7th/2nd -- see IsUnstoppableLoneKingPasser's own header
+ // comment (0 wrong across all 165,676 EGTB-covered legal
+ // single-pawn KPK positions).
+ BITBOARD bb = pos->bbPawns[uColor];
+ while (IS_ON_BOARD(c = CoorFromBitBoardRank8ToRank1(&bb)))
+ {
+ if (TRUE == IsUnstoppableLoneKingPasser(pos, uColor, c))
+ {
+ EVAL_TERM(uColor,
+ PAWN,
+ c,
+ pos->iScore[uColor],
+ RACER_WINS_RACE,
+ "unstoppable passer (lone king)");
+ fFired = TRUE;
+ //
+ // break, not return: uColor is done (one credit is
+ // enough for this side), but the OTHER color still
+ // needs to be checked by this same FOREACH_COLOR
+ // loop -- an earlier version of this function
+ // returned here, which meant whichever color
+ // happened to be checked first (by FOREACH_COLOR's
+ // iteration order) silently suppressed any check of
+ // the other side at all, even a far more urgent
+ // threat. Found via a real regression: White's Ph7
+ // one push from queening
+ // (8/7P/6p1/3k1p2/2p5/3pK3/8/8 w - -, this whole
+ // investigation's original motivating position)
+ // scored -6.09 (the exact ORIGINAL buggy value from
+ // before this session) because Black's much weaker
+ // d3 pawn got checked first and its credit's early
+ // return meant White's real threat was never
+ // evaluated at all.
+ //
+ break;
+ }
+ }
+ }
+ //
+ // Deliberately nothing here for "opponent also has pawns."
+ // A prior session (see the stashed 2026-09-2x eval.c/recogn.c
+ // "passer-race + king-escort rework") tried exactly this
+ // extension, measured it at only ~1% wrong-side rate against
+ // EGTB, and STILL found it caused a real playing-strength
+ // regression in overnight self-play -- a low measured
+ // false-positive rate doesn't guarantee an eval nudge is
+ // actually good for play; it can still systematically mislead
+ // move choice/ordering in ways a raw error-rate number doesn't
+ // capture. That session's own conclusion (matched here rather
+ // than re-litigated): restrict this function to the exact same
+ // true-lone-king precondition recogn.c's own hard bound
+ // trusts, and leave the "both sides have pawns" case to
+ // recogn.c's search-time recognizer (_RecognizeKPK) and plain
+ // positional eval terms, not a duplicate heuristic here. An
+ // eval.c attempt at this (HasRacingUnstoppablePasser) was
+ // built and measured this session before finding this same
+ // prior-session lesson -- removed rather than kept as unused
+ // dead code; see git history if this is ever revisited with a
+ // real self-play test to back it, not just an EGTB sample.
+ //
+ }
+
+ return(fFired);
+}
+
+
FLAG
-_EvalPasserRacesAgainstLoneKings(IN OUT POSITION *pos,
- IN PAWN_HASH_ENTRY *pHash)
+_EvalPasserRacesAgainstLoneKings(IN OUT SEARCHER_THREAD_CONTEXT *ctx,
+ IN PAWN_HASH_ENTRY *pHash)
/**
Routine description:
Determine if one side has a pawn that is unstoppable.
- A 2026-09-16 investigation (see recogn.c's _RecognizeKPK for the
- full writeup) found this function's own king-catches-pawn race
- distance comparison did NOT actually reproduce the bug in
- 7k/8/5KP1/8/8/8/8/8 w - - (Kf6/Pg6/Kh8, White to move -- a draw
- via stalemate defense that a sibling function, _RecognizeKPK, used
- to misjudge as a win) -- a file-specific guard added here on
- suspicion turned out to be unnecessary once _RecognizeKPK's real
- bug was fixed and was removed after TestRecognExhaustiveKPK
- (testrecogn.c) confirmed 0 wrong verdicts across all EGTB-covered
- legal KPK positions without it. Left undocumented/unfixed here:
- whether this function has its own independent bugs of the same
- general shape was NOT exhaustively verified (TestRecognExhaustiveKPK
- exercises it only via RecognLookup's pHash-lookup call path, which
- doesn't reach every branch of this function directly) -- worth
- exhaustively checking on its own if this area gets touched again.
+ This always works for KPK or KPKP but gets dodgy if a side has
+ more than 1 pawn. Error rates on KPPKPP are ~5% and KPKPPP
+ are ~1%.
Parameters:
- POSITION *pos,
+ SEARCHER_THREAD_CONTEXT *ctx,
PAWN_HASH_ENTRY *pHash,
Return value:
@@ -5587,189 +5715,187 @@ Return value:
**/
{
BITBOARD bb[2];
- ULONG uColor;
- ULONG xColor;
+ ULONG uColor, xColor;
int d1;
- COOR cQueen;
- COOR cKing, c;
- ULONG uKingDist, uPawnDist, uFriendDist;
- ULONG uRacerDist[2];
+ COOR cKing[2];
+ COOR c, cQueen;
+ ULONG uEnemyKingDist, uPawnDist, uFriendKingDist;
+ ULONG uMinRacerDist[2] = {99, 99};
+ ULONG uMovesSpent[2];
FLAG fDontCountMeOut[2];
+ POSITION *pos = &ctx->sPosition;
+
+ // Don't recognize if stm is in check.
+ PLY_INFO *pi = &ctx->sPlyInfo[ctx->uPly];
+ if (pi->fInCheck)
+ {
+ return(FALSE);
+ }
+
+ // Only deal with positions that have passers.
bb[BLACK] = pHash->bbPasserLocations[BLACK];
bb[WHITE] = pHash->bbPasserLocations[WHITE];
if (!(bb[BLACK] | bb[WHITE]))
{
return(FALSE);
}
- ASSERT((TotalNonPawnCount(pos, WHITE) == 1) ||
- (TotalNonPawnCount(pos, BLACK) == 1))
+ ASSERT((TotalNonPawnCount(pos, WHITE) == 1) || (TotalNonPawnCount(pos, BLACK) == 1));
- // Both deferred past the no-passer early return above -- no need to
- // pay for this on the common case (most positions have no passer at
- // all, and _EvalPasserRacesAgainstLoneKings itself is only even called when a side
- // is down to a bare king, so this function runs unconditionally in
- // the pre-lazy-exit segment whenever that's true).
- uRacerDist[BLACK] = 99;
- uRacerDist[WHITE] = 99;
+ // Don't make a call if the king is well placed and the side has
+ // pawns left.
fDontCountMeOut[BLACK] = ((RANK(KingPosition(pos, BLACK)) <= 3) &&
(pos->bbPawns[BLACK] != 0));
fDontCountMeOut[WHITE] = ((RANK(KingPosition(pos, WHITE)) >= 6) &&
(pos->bbPawns[WHITE] != 0));
+
+ cKing[WHITE] = KingPosition(pos, WHITE);
+ ASSERT(IS_KING(pos->rgSquare[cKing[WHITE]].pPiece));
+ ASSERT(GET_COLOR(pos->rgSquare[cKing[WHITE]].pPiece) == WHITE);
+ cKing[BLACK] = KingPosition(pos, BLACK);
+ ASSERT(IS_KING(pos->rgSquare[cKing[BLACK]].pPiece));
+ ASSERT(GET_COLOR(pos->rgSquare[cKing[BLACK]].pPiece) == BLACK);
+
FOREACH_COLOR(uColor)
{
- xColor = FLIP(uColor);
- if (TotalNonPawnCount(pos, xColor) == 1)
+ // If uColor has no passers, skip it.
+ if (!bb[uColor])
{
- d1 = 16 * g_iAhead[uColor];
- ASSERT((d1 * 2) == (32 * g_iAhead[uColor]));
- if (bb[uColor])
- {
- while(IS_ON_BOARD(c = CoorFromBitBoardRank8ToRank1(
- &(bb[uColor]))))
- {
- // If the enemy king can take this passer and is
- // on the move, all bets are off!
- cKing = KingPosition(pos, xColor);
- ASSERT(IS_KING(pos->rgSquare[cKing].pPiece));
- ASSERT(GET_COLOR(pos->rgSquare[cKing].pPiece) != uColor);
- if (pos->uToMove == xColor &&
- (DISTANCE(cKing, c) == 1))
- {
- // TODO: something clever here about protected pawns
- continue;
- }
-
- cQueen = FILE_RANK_TO_COOR(FILE(c), QUEENING_RANK[uColor]);
- ASSERT(FILE(cQueen) == FILE(c));
+ continue;
+ }
- // 2026-09-18: rook pawn, strong side's OWN king on
- // the same file, on or adjacent to its own
- // queening square -- i.e. the strong king is in
- // front of its own pawn. Checked up front, before
- // either race-credit computation below gets a
- // chance to touch uRacerDist[uColor] for this
- // pawn, and skips via `continue` (not by resetting
- // uRacerDist[uColor], which would also wipe out a
- // legitimate credit already earned by a different
- // passer of the same color from an earlier loop
- // iteration -- bb[uColor] can hold more than one
- // passer). A friendly king blocking its own rook
- // pawn's queening square is a classic drawing
- // resource (the defender can potentially reach a
- // stalemate), not a neutral or helpful thing --
- // unlike a center pawn, where the king merely costs
- // a tempo (handled below via the existing +1 case),
- // a rook pawn's single queening square occupied by
- // its own king has no safe way to step aside
- // without delicate tempo/opposition analysis this
- // cheap eval-time heuristic can't do (that's what
- // recogn.c's _RecognizeKPK critical-square logic is
- // for). Found via fuzzing tests/cross_sectional_
- // 200k.ep_: two of this session's real recognizer/
- // eval bugs both had the strong side's own king in
- // front of its own rook pawn (8/8/8/8/8/8/4K2p/7k
- // w - - -- White can stalemate Black rather than
- // let Ph2 queen).
- //
- // Distance must be exactly 0 (king ON the queening
- // square), not <=1 -- if the king is merely
- // adjacent, the queening square itself is empty
- // and the pawn walks in and promotes next move
- // with nothing in the way; that's a genuine win,
- // not a stalemate risk. Only the king literally
- // occupying its own pawn's only queening square is
- // the danger this guards against.
- if (((A == FILE(c)) || (H == FILE(c))) &&
- (FILE(c) == FILE(KingPosition(pos, uColor))) &&
- (0 == DISTANCE(KingPosition(pos, uColor), cQueen)))
- {
- // Matches the unconditional assignment every
- // other path through this loop body makes --
- // this color's own passer was genuinely
- // examined here, even though disqualified.
- fDontCountMeOut[uColor] = TRUE;
- continue;
- }
+ // If xColor has any piece other than a king (and maybe some pawns), skip.
+ xColor = FLIP(uColor);
+ if (TotalNonPawnCount(pos, xColor) != 1)
+ {
+ continue;
+ }
+ d1 = 16 * g_iAhead[uColor];
- uPawnDist = RANK_DISTANCE(cQueen, c);
- if ((RANK(c) == JUMPING_RANK[uColor]) &&
- (IS_EMPTY(pos->rgSquare[c + d1].pPiece) &&
- IS_EMPTY(pos->rgSquare[c + d1 * 2].pPiece)))
- {
- uPawnDist--;
- }
- ASSERT((uPawnDist >= 1) && (uPawnDist <= 6));
+ uMovesSpent[uColor] = 1;
+ uMovesSpent[xColor] = 0;
+ while(IS_ON_BOARD(c = CoorFromBitBoardRank8ToRank1(
+ &(bb[uColor]))))
+ {
+ // If this pawn is right next to the enemy king, don't
+ // predict the number of moves it takes to queen.
+ if (xColor == pos->uToMove &&
+ (DISTANCE(cKing[xColor], c) == 1) &&
+ (DISTANCE(cKing[uColor], c) != 1))
+ {
+ uMovesSpent[xColor] += 1;
+ continue;
+ }
+ fDontCountMeOut[uColor] = TRUE;
- uKingDist = DISTANCE(cKing, cQueen);
- if (uKingDist > 0)
- {
- uKingDist -= (pos->uToMove == xColor);
- }
- ASSERT((uKingDist >= 0) && (uPawnDist <= 7));
+ cQueen = FILE_RANK_TO_COOR(FILE(c), QUEENING_RANK[uColor]);
+ ASSERT(FILE(cQueen) == FILE(c));
- if (uPawnDist < uKingDist)
- {
- uRacerDist[uColor] = MINU(uRacerDist[uColor],
- uPawnDist);
- }
+ // Don't call rook pawn races with the friendly king in a
+ // dodgy position.
+ if (((A == FILE(c)) || (H == FILE(c))) &&
+ (FILE(c) == FILE(cKing[uColor])) &&
+ (0 == DISTANCE(cKing[uColor], cQueen)))
+ {
+ continue;
+ }
- cKing = KingPosition(pos, uColor);
- ASSERT(IS_KING(pos->rgSquare[cKing].pPiece));
- ASSERT(GET_COLOR(pos->rgSquare[cKing].pPiece) == uColor);
+ uPawnDist = RANK_DISTANCE(cQueen, c);
+ uPawnDist -= ((RANK(c) == JUMPING_RANK[uColor]) &&
+ (IS_EMPTY(pos->rgSquare[c + d1].pPiece) &&
+ IS_EMPTY(pos->rgSquare[c + d1 * 2].pPiece)));
+ ASSERT((uPawnDist >= 1) && (uPawnDist <= 6));
+ uPawnDist += uMovesSpent[uColor];
- uFriendDist = DISTANCE(cKing, cQueen);
- if ((uFriendDist <= 1) && (DISTANCE(cKing, c) <= 1))
- {
- if (FILE(c) != FILE(cKing))
- {
- uRacerDist[uColor] = MINU(uRacerDist[uColor],
- uPawnDist);
- }
- else if ((FILE(cKing) != A) &&
- (FILE(cKing) != H))
- {
- uRacerDist[uColor] = MINU(uRacerDist[uColor],
- (uPawnDist + 1));
- }
- // else: rook pawn, own king on the same file
- // but more than 1 square from the queening
- // square -- the specific adjacent/on-queening-
- // square danger this session found (see the
- // up-front `continue` guard above, right after
- // cQueen is computed) doesn't apply here; leave
- // as-is (no extra credit, no suppression),
- // same as this branch's original behavior.
- }
- fDontCountMeOut[uColor] = TRUE;
+ // Rule of the square; can this passer make it to
+ // the queening square before the enemy king?
+ uEnemyKingDist = DISTANCE(cKing[xColor], cQueen);
+ if (uEnemyKingDist > 0)
+ {
+ uEnemyKingDist -= (pos->uToMove == xColor);
+ }
+ ASSERT((uEnemyKingDist >= 0) && (uPawnDist <= 7));
+ if (uPawnDist < uEnemyKingDist)
+ {
+ uMinRacerDist[uColor] = MINU(uMinRacerDist[uColor], uPawnDist);
+ }
- //
- // TODO: winning connected passers
- //
+ // See if the friend king can escort this passer in. Same-
+ // file adjacency alone doesn't distinguish a king BEHIND
+ // the pawn (genuine escort) from a king sitting on the
+ // very next square in front of it, i.e. cKing[uColor] ==
+ // c + d1 (blocking its own pawn's path, not helping it) --
+ // found via an exhaustive KPvKP/EGTB cross-check
+ // (8/8/8/8/8/6Pp/7k/5K2 b - -: Black king h2 sits directly
+ // in front of its own h3 pawn, yet this branch credited an
+ // escort bonus as if h2 were behind it). Exclude that
+ // square explicitly rather than just checking direction,
+ // since it's the one same-file square DISTANCE<=1 permits
+ // that unambiguously blocks rather than escorts.
+ uFriendKingDist = DISTANCE(cKing[uColor], cQueen);
+ if ((uFriendKingDist <= 1) && (DISTANCE(cKing[uColor], c) <= 1))
+ {
+ if (FILE(c) != FILE(cKing[uColor]))
+ {
+ uMinRacerDist[uColor] = MINU(uMinRacerDist[uColor],
+ uPawnDist);
+ }
+ else if ((cKing[uColor] != (c + d1)) &&
+ (FILE(cKing[xColor]) != A) &&
+ (FILE(cKing[xColor]) != H))
+ {
+ uMinRacerDist[uColor] = MINU(uMinRacerDist[uColor],
+ (uPawnDist + 1));
}
}
}
+
+ // uMovesSpent[xColor] counts how many of uColor's OWN passers
+ // just got individually skipped above as "adjacent to xColor's
+ // king, xColor to move -- assume it's captured for free." Fine
+ // when it happens to exactly one pawn, but when it happens to
+ // TWO OR MORE, xColor's king is adjacent to multiple of
+ // uColor's passers at once and can only actually capture ONE
+ // of them -- the classic king-overload fork (found via random
+ // KPPKPP/KPKPPP sampling against EGTB:
+ // 8/5P2/1k6/8/8/8/pKp2P2/8 w - -, White king b2 flanked by
+ // Black's a2 and c2, each one square from queening -- White
+ // captures one, Black queens the other, Black wins by force,
+ // but the per-pawn skip above silently dropped BOTH of
+ // Black's passers from consideration, crediting White's own
+ // ordinary race instead). This function's per-pawn model has
+ // no way to actually resolve a fork like this ("TODO: winning
+ // connected passers" above already flags the general
+ // connected-passer gap as out of scope) -- the safe, precision-
+ // over-recall response is not to guess who really wins, but to
+ // stop trusting THE OTHER side's credit here, the same way
+ // fDontCountMeOut already protects a side with real drawing
+ // chances elsewhere in this function.
+ if (uMovesSpent[xColor] >= 2)
+ {
+ fDontCountMeOut[uColor] = TRUE;
+ }
}
- if ((uRacerDist[WHITE] < uRacerDist[BLACK]) &&
+ if ((uMinRacerDist[WHITE] < uMinRacerDist[BLACK]) &&
(FALSE == fDontCountMeOut[BLACK]))
{
EVAL_TERM(WHITE,
PAWN,
ILLEGAL_COOR,
pos->iScore[WHITE],
- RACER_WINS_RACE - uRacerDist[WHITE] * 4,
+ RACER_WINS_RACE - uMinRacerDist[WHITE] * 4,
"winning racer pawn");
return(TRUE);
}
- else if ((uRacerDist[BLACK] < uRacerDist[WHITE]) &&
+ else if ((uMinRacerDist[BLACK] < uMinRacerDist[WHITE]) &&
(FALSE == fDontCountMeOut[WHITE]))
{
EVAL_TERM(BLACK,
PAWN,
ILLEGAL_COOR,
pos->iScore[BLACK],
- RACER_WINS_RACE - uRacerDist[BLACK] * 4,
+ RACER_WINS_RACE - uMinRacerDist[BLACK] * 4,
"winning racer pawn");
return(TRUE);
}
@@ -5919,17 +6045,17 @@ Return value:
PAWN,
ILLEGAL_COOR,
pos->iScore[WHITE],
- uNumPassers[WHITE] * PASSER_BONUS_AS_MATERIAL_COMES_OFF[u],
+ (uNumPassers[WHITE] * PASSER_BONUS_AS_MATERIAL_COMES_OFF[u] * MULTIPLE_PASSER_SCALER[uNumPassers[WHITE]]) >> 5,
"passer value material");
- // ASSERT(PASSER_BONUS_AS_MATERIAL_COMES_OFF[u] >= 0);
+ ASSERT(PASSER_BONUS_AS_MATERIAL_COMES_OFF[u] >= 0);
u = pos->uArmyScaler[WHITE];
EVAL_TERM(BLACK,
PAWN,
ILLEGAL_COOR,
pos->iScore[BLACK],
- uNumPassers[BLACK] * PASSER_BONUS_AS_MATERIAL_COMES_OFF[u],
+ (uNumPassers[BLACK] * PASSER_BONUS_AS_MATERIAL_COMES_OFF[u] * MULTIPLE_PASSER_SCALER[uNumPassers[BLACK]]) >> 5,
"passer value material");
- // ASSERT(PASSER_BONUS_AS_MATERIAL_COMES_OFF[u] >= 0);
+ ASSERT(PASSER_BONUS_AS_MATERIAL_COMES_OFF[u] >= 0);
}
@@ -5967,35 +6093,86 @@ Return value:
#endif
//
+ // A material-VALUE imbalance (uNonPawnMaterial differing, the
+ // caller's gate) is not the same fact as a piece-COUNT imbalance --
+ // e.g. 2R vs R+N is "up the exchange" with equal piece count on
+ // both sides, not a side that has simplified/traded down to fewer
+ // pieces. Ported from a stashed, previously-uncommitted eval.c
+ // change (2026-09-2x eval audit session) -- mirrors Crafty's
+ // EvaluateMaterial (~/crafty/evaluate.c), whose bad_trade bonus
+ // explicitly requires Abs(occupied_count_diff) != 2 &&
+ // occupied_count_diff != 0, i.e. it never fires when piece counts
+ // are equal, deliberately leaving pure up-the-exchange positions to
+ // ordinary material valuation instead of this term. Without this
+ // check, typhoon double-counts: real, up-the-exchange credit
+ // already exists via material value, and this term used to stack
+ // an unconditional extra "keep trading" bonus on top regardless of
+ // whether uAhead had actually traded down to fewer pieces at all --
+ // found via the original eval audit
+ // (8/2r4p/8/5p1R/8/1n4P1/1pk2PKP/4R3 b, 2R vs R+N, equal piece
+ // count: typhoon +2.07 for White, Crafty -1.63 for White --
+ // opposite sign, traced to this exact double-count).
+ //
+ if (TotalNonPawnCount(pos, uAhead) == TotalNonPawnCount(pos, uBehind))
+ {
+ return;
+ }
+
+ //
// Encourage the side that is ahead in piece material to continue
// trading pieces and not to trade pawns. This also has the
// effect of making the side that is behind in material want to
// trade pawns and not pieces. This also gives the side that is
// ahead a bit of a "bad trade" bonus. Note that the bonus is not
- // given if the side "ahead" has no pawns; this is so that the
- // engine will not like positions like KNB vs KRP - the side with
- // the pawn has the winning chances.
+ // given if the side "ahead" has no pawns AND no mating material of
+ // its own; this is so that the engine will not like positions like
+ // KNB vs KRP - the side with the pawn has the winning chances.
//
ULONG uAheadPawnCount = FastCountBits(pos->bbPawns[uAhead]);
ULONG uInverseBehindPieceCount = 9 - MINU(9, TotalNonPawnCount(pos, uBehind));
+ //
+ // "No mating material" here means the SAME lone-minor-piece test
+ // SideHasWinningChances (above) already uses: king + exactly one
+ // knight/bishop and nothing else -- a K+N or K+B can never force
+ // mate alone, which is the actual KNB-vs-KRP failure mode this
+ // whole guard exists for. Found via a real counterexample
+ // (2k5/4r3/KP2P3/P7/7P/8/8/8 b - -): Black is ahead in non-pawn
+ // material with a ROOK, not a lone minor -- a rook can genuinely
+ // blockade/pick off multiple passed pawns on its own, nothing like
+ // the KNB-vs-KRP case this guard was built for -- yet the old
+ // "zero pawns -> zero bonus" version zeroed this out unconditionally
+ // by piece count alone, silencing exactly the term that should
+ // have been pushing back against White's four separately-scored
+ // passed pawns.
+ //
+ FLAG fAheadLoneMinorNoMate =
+ (2 == TotalNonPawnCount(pos, uAhead)) &&
+ (1 == (FastCountBits(pos->bbPieces[uAhead][KNIGHT]) +
+ FastCountBits(pos->bbPieces[uAhead][BISHOP])));
EVAL_TERM(uAhead,
0,
ILLEGAL_COOR,
pos->iScore[uAhead],
- (uInverseBehindPieceCount * 16) * (uAheadPawnCount != 0),
+ (uInverseBehindPieceCount * 16) *
+ ((uAheadPawnCount != 0) || (FALSE == fAheadLoneMinorNoMate)),
"trade pieces");
- // Zero pawns left for the ahead side is the classic false-positive
- // (KNB vs KRP, minor-up-no-pawns endings that are often drawn or
- // even lost) -- the old DONT_TRADE_PAWNS table existed specifically
- // to punish it (-43/-10 depending on lead size) rather than just
- // trailing off toward a small positive number. Keep that guard as
- // a cheap branch rather than a lookup table; the positive slope
- // for 1+ pawns doesn't need the same care.
+ // Zero pawns AND no mating material for the ahead side is the
+ // classic false-positive (KNB vs KRP, minor-up-no-pawns endings
+ // that are often drawn or even lost) -- the old DONT_TRADE_PAWNS
+ // table existed specifically to punish it (-43/-10 depending on
+ // lead size) rather than just trailing off toward a small positive
+ // number. Keep that guard as a cheap branch rather than a lookup
+ // table; the positive slope for 1+ pawns doesn't need the same
+ // care. Same mating-material exception as "trade pieces" above:
+ // zero pawns with REAL mating material (a rook, say) isn't the
+ // KNB-vs-KRP failure mode this -30 penalty exists for, so it gets
+ // a neutral 0 instead of being punished.
EVAL_TERM(uAhead,
0,
ILLEGAL_COOR,
pos->iScore[uAhead],
- (0 == uAheadPawnCount) ? -30 : (SCORE)(uAheadPawnCount * 2),
+ (0 != uAheadPawnCount) ? (SCORE)(uAheadPawnCount * 2) :
+ (TRUE == fAheadLoneMinorNoMate) ? -30 : 0,
"don't trade pawns");
}
@@ -6275,9 +6452,9 @@ Return value:
}
-static FLAG
-_SideHasWinningChances(IN POSITION *pos,
- IN ULONG uSide)
+FLAG
+SideHasWinningChances(IN POSITION *pos,
+ IN ULONG uSide)
/**
Routine description:
@@ -6293,14 +6470,16 @@ Routine description:
recognizer bound can (see recogn.c's _RecognizeKNKP/_RecognizeKBNK
incident).
- Deliberately conservative: only covers material shapes where "no,
+ Deliberately conservative: mostly covers material shapes where "no,
this can't be forced" is basic, textbook chess knowledge
(insufficient mating material, up the exchange with nothing else
- changed, bare knights vs a bare king), not trickier exceptions
- (fortress draws, wrong-bishop-pawn-with-king-in-time geometry)
- that need real board information -- those are recogn.c's
- hard-recognizer territory (already exact for tiny material there)
- or plain search's job, not this cheap pre-check's.
+ changed, bare knights vs a bare king). One exception that DOES need
+ real board information, not just material counts, is included below
+ (wrong-bishop-pawn-with-king-in-time) precisely because it's common
+ enough and cheap enough to be worth it; anything else needing real
+ board geometry is recogn.c's hard-recognizer territory (already
+ exact for tiny material there) or plain search's job, not this
+ cheap pre-check's.
Parameters:
@@ -6321,6 +6500,77 @@ Return value:
ULONG uSideKnights;
ULONG uSideBishops;
+ // uSide's bishop count is needed by the wrong-bishop-fortress check
+ // immediately below (which must run BEFORE the "any pawn -> TRUE"
+ // early return that follows it) and is reused again later by the
+ // lone-minor/iMinorDiff/two-knights checks -- compute it once, up
+ // front.
+ uSideBishops = FastCountBits(pos->bbPieces[uSide][BISHOP]);
+
+ //
+ // Wrong-bishop rook-pawn fortress: a lone bishop + rook-pawn(s) (all
+ // on the A- or H-file), with the bishop unable to control the
+ // promotion square, is a known draw once the defending bare king
+ // can reach the corner in time -- regardless of how much raw pawn
+ // material uSide has. Checked before the blanket "any pawn -> TRUE"
+ // return below, which would otherwise hide this case entirely.
+ // Mirrors Crafty's EvaluateWinningChances (~/crafty/evaluate.c) --
+ // "in time" uses the same tempo-adjusted Chebyshev distance Crafty
+ // uses (one square of credit to the defender if it's their move).
+ // Ported from a stashed, previously-uncommitted eval.c change
+ // (2026-09-2x eval audit session); testing methodology for its
+ // false-positive rate against this session's own KP+KP* EGTB
+ // harness is still to be built (see testeval.c/testrecogn.c) before
+ // trusting the ~2.12% figure the stash's own comment cited -- that
+ // number was never independently re-verified in this session. Only
+ // ever softens a score (via EvalLookForDrawishSituations' /16
+ // shrink), never used as a search-terminating bound the way
+ // recogn.c's tighter dist<=1-only version is, so a wrong FALSE here
+ // is tolerable in a way it would not be in recogn.c.
+ //
+ if ((2 == TotalNonPawnCount(pos, uSide)) &&
+ (1 == uSideBishops) &&
+ (1 == TotalNonPawnCount(pos, uEnemy)) &&
+ (0 == FastCountBits(pos->bbPawns[uEnemy])) &&
+ (FastCountBits(pos->bbPawns[uSide]) > 0))
+ {
+ BITBOARD bbPawns = pos->bbPawns[uSide];
+ COOR cPromote = ILLEGAL_COOR;
+
+ if (uSide == WHITE)
+ {
+ if (!(bbPawns & ~BBFILE[A]) && (WhiteSqBishopCount(pos, WHITE) == 0))
+ {
+ cPromote = A8;
+ }
+ else if (!(bbPawns & ~BBFILE[H]) && (WhiteSqBishopCount(pos, WHITE) == 1))
+ {
+ cPromote = H8;
+ }
+ }
+ else
+ {
+ if (!(bbPawns & ~BBFILE[A]) && (WhiteSqBishopCount(pos, BLACK) == 1))
+ {
+ cPromote = A1;
+ }
+ else if (!(bbPawns & ~BBFILE[H]) && (WhiteSqBishopCount(pos, BLACK) == 0))
+ {
+ cPromote = H1;
+ }
+ }
+
+ if (IS_ON_BOARD(cPromote))
+ {
+ COOR cWeakKing = KingPosition(pos, uEnemy);
+ int iTempo = (pos->uToMove == uEnemy) ? 1 : 0;
+ if (((int)DISTANCE(cWeakKing, cPromote) - iTempo) < 1)
+ {
+ return(FALSE);
+ }
+ }
+ }
+
//
// A pawn always gives some winning chances (it can always try to
// queen with support) -- nothing below applies.
@@ -6330,12 +6580,10 @@ Return value:
return(TRUE);
}
- // uSide's knight/bishop counts are each read three times below
- // (the lone-minor check, iMinorDiff, and the two-knights check) --
- // compute them once and reuse rather than re-popcounting the same
- // two bitboards repeatedly.
+ // uSide's knight count is read twice below (the lone-minor check
+ // and the two-knights check); uSideBishops was already computed
+ // above.
uSideKnights = FastCountBits(pos->bbPieces[uSide][KNIGHT]);
- uSideBishops = FastCountBits(pos->bbPieces[uSide][BISHOP]);
//
// No pawns and the only piece besides the king is a single minor:
@@ -6433,9 +6681,9 @@ _EnsureDrawishFactsComputed(IN POSITION *pos, IN ULONG uColor,
{
return;
}
- pFacts->fSideToMoveHasWinningChances = _SideHasWinningChances(pos, uColor);
+ pFacts->fSideToMoveHasWinningChances = SideHasWinningChances(pos, uColor);
pFacts->fOpponentHasWinningChances =
- _SideHasWinningChances(pos, FLIP(uColor));
+ SideHasWinningChances(pos, FLIP(uColor));
pFacts->fBOOC = (TotalNonPawnCount(pos, WHITE) == 2) &&
(TotalNonPawnCount(pos, BLACK) == 2) &&
(FastCountBits(pos->bbPieces[WHITE][BISHOP]) == 1) &&
@@ -7061,7 +7309,7 @@ Return value:
// TABLES IT NEEDS TO CLEAR/POPULATE THEM THOUGH!!!
#ifdef EVAL_TIME
UINT64 u64PawnTimer = SystemReadTimeStampCounter();
- pHash = _EvalPawns(ctx, &fDeferred);
+ pHash = EvalPawns(ctx, &fDeferred);
{
UINT64 u64Elapsed = SystemReadTimeStampCounter() - u64PawnTimer;
ctx->sCounters.tree.u64CyclesEvalPawns += u64Elapsed;
@@ -7077,7 +7325,7 @@ Return value:
}
}
#else
- pHash = _EvalPawns(ctx, &fDeferred);
+ pHash = EvalPawns(ctx, &fDeferred);
#endif
ASSERT(NULL != pHash);
ASSERT(IS_VALID_FLAG(fDeferred));
@@ -7099,7 +7347,32 @@ Return value:
#endif
if (fBareKingEitherSide)
{
- (void)_EvalPasserRacesAgainstLoneKings(pos, pHash);
+ //
+ // Pure king-and-pawns on BOTH sides: try the new,
+ // exhaustively-verified shared logic first (see
+ // _EvalUnstoppablePasser's header comment) -- it's trustworthy
+ // whenever it has something to say. But it only ever fires for
+ // the true-lone-king sub-case and stays silent (FALSE) for the
+ // "both sides still have pawns" sub-case, which the old,
+ // broader-but-dodgier heuristic used to cover -- fall back to
+ // that old heuristic rather than simply losing that coverage.
+ // Anything broader than pure KPvKP (one bare side, but the
+ // OTHER side still has real pieces -- e.g. K+R+P vs K) always
+ // uses the old heuristic unchanged; that shape was never
+ // covered by this session's KPvKP investigation.
+ //
+ if ((1 == TotalNonPawnCount(pos, WHITE)) &&
+ (1 == TotalNonPawnCount(pos, BLACK)))
+ {
+ if (FALSE == _EvalUnstoppablePasser(pos))
+ {
+ (void)_EvalPasserRacesAgainstLoneKings(ctx, pHash);
+ }
+ }
+ else
+ {
+ (void)_EvalPasserRacesAgainstLoneKings(ctx, pHash);
+ }
#ifdef EVAL_DUMP
Trace("After passer races:\n%d\t\t%d\t\t(%d for %s)\n",
pos->iScore[WHITE], pos->iScore[BLACK],