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