diff options
| author | Scott Gasch <[email protected]> | 2026-10-07 08:17:51 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-10-07 08:17:51 -0700 |
| commit | d14dbe2a50c220b2adac758c3d4f1a1fa67ece0b (patch) | |
| tree | cbf6d9928bcc41ef833b62b70479113f7d419925 /src/eval.c | |
| parent | 447130db3df87b40d080dc26429d45e9213c19f7 (diff) | |
Speed up eval/move-gen/SEE; remove EvalLookForDanger, overhaul _EvalKing
SPRT-confirmed improvement (1814 games, st 4.0): candidate nps=1498118
vs head=1378448, avg depth 14.30 vs 13.87, ELO=+24.2+/-16.0,
LOWER95=0.5118, LLR=+3.126 crossing the H1 bound.
Most of the diff is non-semantic speed work across eval.c, generate.c,
movesup.c, see.c, probe.c and friends. Two real semantic changes:
removed EvalLookForDanger and overhauled _EvalKing's king-safety scoring.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Diffstat (limited to 'src/eval.c')
| -rw-r--r-- | src/eval.c | 582 |
1 files changed, 395 insertions, 187 deletions
@@ -280,13 +280,13 @@ static SCORE DOUBLED_PAWN_PENALTY_BY_COUNT[4][9] = static SCORE ISOLATED_PAWN_BY_PAWNFILE[9] = { - 0, -7, -8, -9, -10, -10, -9, -8, -7 + 0, -8, -9, -10, -10, -10, -10, -9, -8 }; static SCORE ISOLATED_EXPOSED_PAWN = -5; // Turned down (2026-08-30): a per-pawn modifier that stacks on top of -// DOUBLED_PAWN_PENALTY_BY_COUNT's whole-position doubled-pawn aggregate +//< DOUBLED_PAWN_PENALTY_BY_COUNT's whole-position doubled-pawn aggregate // for the specific case of a pawn that's both isolated and doubled -- // a different angle (single-worst-case flag vs. whole-position // severity), not the same fact counted twice, so scaled down rather @@ -385,7 +385,7 @@ SCORE RACER_WINS_RACE = +300; static SCORE UNDEVELOPED_MINORS_IN_OPENING[5] = {// 0 1 2 3 4 - 0, -6, -10, -16, -22 + 0, -8, -14, -20, -23 }; // @@ -722,16 +722,19 @@ static SCORE KING_TO_CENTER[128] = // the other side's king safety. Note: this doesn't incl pawn // material. // +// 2026-10-06: also now the single dividing line for _EvalKing's whole +// safety-vs-activity split (see that function's header comment) -- +// below this, the king is actively encouraged out (KING_TO_CENTER) +// instead of just not being penalized for leaving. The separate, +// stricter KEEP_KING_AT_HOME_THRESHOLD used to gate the stay-home +// bonus on its own, independently of this one, leaving a dead band +// between the two thresholds where safety was scored but neither the +// stay-home bonus nor any come-out encouragement applied. Retired -- +// the stay-home bonus now uses this same threshold, so "safety scored" +// and "stay-home bonus applies" are exactly the same condition. #define DO_KING_SAFETY_THRESHOLD \ (VALUE_ROOK + VALUE_BISHOP + VALUE_KING + 1) -// -// If a side has less than this much material the other side's king -// can come out. Note: this doesn't incl pawn material. -// -#define KEEP_KING_AT_HOME_THRESHOLD \ - (VALUE_ROOK + VALUE_ROOK + VALUE_BISHOP + VALUE_KING + 1) - 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 @@ -2269,6 +2272,17 @@ Return value: FastCountBits(pos->bbPawns[WHITE] & (pos->bbPawns[BLACK] << 8)); ASSERT(pHash->uNumRammedPawns <= 8); + pHash->uTotalPawnCount = (UCHAR)FastCountBits(bbAllPawns); + ASSERT(pHash->uTotalPawnCount <= 16); + + // _EvalKnight's closed-position scaler, computed once per unique + // pawn structure instead of once per Eval() call -- see this + // field's comment in chess.h. + pHash->uClosedScaler = (UCHAR)(pHash->uTotalPawnCount - + (pHash->uNumUnmovedPawns[WHITE] + pHash->uNumUnmovedPawns[BLACK]) + + pHash->uNumRammedPawns); + ASSERT(pHash->uClosedScaler <= 32); + // Good/bad bishop pawn-structure score, cached per (color, color- // complex) instead of rescanned by every bishop of that color/ // complex in _EvalBishop (board_representation/EVAL.md-style note: @@ -3002,6 +3016,120 @@ Return value: } +static SCORE +_GetKingActivityBonus(IN SEARCHER_THREAD_CONTEXT *ctx, + IN POSITION *pos, + IN const PAWN_HASH_ENTRY *pHash, + IN ULONG uColor, + IN COOR cKing) +/** + +Routine description: + + Cache front end for _EvalKing's activity half (2026-10-06 safety/ + activity split): combines "supporting own passer" and "is this + king out of the action" into one cached lookup, since both only + ever apply together (same caller, same branch) and both depend on + exactly the same inputs -- pawn structure plus uColor's own king + square, nothing else. One key computation and one hash probe + serve both instead of two. Only called when the ENEMY (xColor) is + down to at most one non-king piece -- see _EvalKing. + + Per-king, not per-position: unlike _GetFileStormDefects (keyed on + both kings' squares, since "is the king's position safe" depends + on both), out-of-action's old formula subtracted the enemy king's + own distance to make a symmetric "who's closer" value, but that + only made sense paired with the old combined-both-sides gate + (uTotalNonPawnsBothSides==2, true or false identically for both + kings' calls). The new gate is per-call (keyed on xColor, each + king's own enemy), so the two calls can legitimately disagree -- + one king's own position might qualify while the other's doesn't -- + and a shared symmetric value can no longer be "added once, during + white's call" the way the old code assumed. Each call now computes + and caches its own independent value instead. + + Narrower key (just this king's square, not both) than + _GetFileStormDefects's as a direct consequence -- only this king's + own moves can invalidate it, not the opponent's too, so this + should measure a *better* hit rate than file-storm's own ~75-85% + in this same low-material regime, not a worse one. + + PROVISIONAL, like the formulas it combines: neither weight has + been through any self-play validation -- see the old out-of-action + formula's own removed comment (git history) for the outlier bug + that originally motivated shrinking its weight. Needs the same + calibration discipline as every other king-safety magnitude in + this file. Also trades away per-passer-square EVAL_TERM attribution + (summed into one combined value instead) -- EVAL_DUMP tracing + only, not used in release builds, by direct request for fewer + instructions in this inner loop. + +Parameters: + + SEARCHER_THREAD_CONTEXT *ctx + POSITION *pos + PAWN_HASH_ENTRY *pHash + ULONG uColor : whose king this bonus is for + COOR cKing : uColor's own king square (caller already has it -- + KingPosition(pos, uColor) redundantly) + +Return value: + + SCORE + +**/ +{ + UINT64 u64Key = pos->u64PawnSig ^ g_u64SigSeeds[cKing][KING][uColor]; + ULONG uSlot = (ULONG)u64Key & (KING_SAFETY_HASH_TABLE_SIZE - 1); + KING_SAFETY_HASH_ENTRY *pEntry = &ctx->rgKingSafetyHash[uSlot]; + SCORE i, iTotal; + COOR cSquare; + BITBOARD bb; + + INC(ctx->sCounters.kingactionhash.u64Probes); + if (pEntry->u64ActionKey == u64Key) + { + INC(ctx->sCounters.kingactionhash.u64Hits); + return pEntry->iActionBonus; + } + + // Supporting own passer -- bb here is exactly the own-passer + // squares at king-move distance 1 (g_KingAttacksBB is that mask + // by construction), so every popped square counts; no separate + // DISTANCE() filter or ASSERTs needed. The extra "leading the + // pawn toward promotion" bonus applies iff the passer square is + // in the king's own back ring (g_KingRingBackBB) -- side- and + // front-ring passers still get the base rank-scaled support + // bonus, just not this extra. + iTotal = 0; + bb = pHash->bbPasserLocations[uColor] & g_KingAttacksBB[cKing]; + while (IS_ON_BOARD(cSquare = CoorFromBitBoardRank8ToRank1(&bb))) + { + i = 2; + i += KING_SUPPORTING_OWN_PASSER_BY_RANK[uColor][RANK(cSquare)]; + i += 8 * ((COOR_TO_BB(cSquare) & g_KingRingBackBB[uColor][cKing]) != 0); + iTotal += i; + } + + // Out of action in KP-ish endgames, e.g. + // 8/8/1p6/p6k/P3K3/8/1P6/8 w - - 0 11 -- how close this king is + // to the whole pawn mass. The *2 preserves the old formula's own + // scale (it used to be a differential, this is now a plain sum, + // but the per-pawn magnitude is unchanged). + i = 0; + bb = pos->bbPawns[WHITE] | pos->bbPawns[BLACK]; + while (IS_ON_BOARD(cSquare = CoorFromBitBoardRank8ToRank1(&bb))) + { + i -= (SCORE)DISTANCE(cSquare, cKing); + } + iTotal += i * 2; + + pEntry->u64ActionKey = u64Key; + pEntry->iActionBonus = iTotal; + return iTotal; +} + + #ifdef CALIBRATE_POSITIONAL // // board_representation/EVAL.md section 9 recalibration (2026-09-05): @@ -4557,12 +4685,12 @@ Return value: "board centrality"); // Give a bonus to knights on a closed / busy board. - ASSERT(pos->uClosedScaler <= 32); + ASSERT(pHash->uClosedScaler <= 32); EVAL_TERM(uColor, KNIGHT, c, pos->iScore[uColor], - KNIGHT_IN_CLOSED_POSITION[pos->uClosedScaler], + KNIGHT_IN_CLOSED_POSITION[pHash->uClosedScaler], "in closed/open position"); // Outposted knights. See if square c is safe from enemy pawns; @@ -4786,12 +4914,10 @@ Return value: ROOK, c, pos->iScore[uColor], - ROOK_VALUE_AS_PAWNS_COME_OFF[ - FastCountBits(pos->bbPawns[BLACK] | pos->bbPawns[WHITE]) - ], + ROOK_VALUE_AS_PAWNS_COME_OFF[pHash->uTotalPawnCount], "increase in value/pawns"); - ASSERT((FastCountBits(pos->bbPawns[uColor]) + FastCountBits(pos->bbPawns[xColor])) == - (FastCountBits(pos->bbPawns[uColor] | pos->bbPawns[xColor]))); + ASSERT(pHash->uTotalPawnCount == + (FastCountBits(pos->bbPawns[uColor]) + FastCountBits(pos->bbPawns[xColor]))); // // Rook mobility (and update attack tables) -- board_representation/ @@ -5104,47 +5230,52 @@ Return value: } -static void -_EvalKing(IN OUT POSITION *pos, - IN const COOR c, - IN const PAWN_HASH_ENTRY *pHash, - IN SEARCHER_THREAD_CONTEXT *ctx, - IN const ULONG uTotalNonPawnsBothSides) +static SCORE FORCEINLINE +_EvalKingSafety(IN POSITION *pos, + IN const COOR c, + IN const PAWN_HASH_ENTRY *pHash, + IN SEARCHER_THREAD_CONTEXT *ctx, + IN const ULONG uColor, + IN const ULONG xColor, + IN const BITBOARD bbRing) /** Routine description: + Safety half of _EvalKing's 2026-10-06 safety-vs-activity split -- + see that function's header comment. Full ring-attack/danger- + pattern scoring, file-storm defects (cached), stay-on-initial- + square bonus, castling/development. Only called when the enemy + has enough material (DO_KING_SAFETY_THRESHOLD) to make an attack + worth scoring. FORCEINLINE: this and _EvalKingActivity are split + out of _EvalKing purely for readability -- _EvalKing calls exactly + one of the two per invocation (never both), so forcing both + inline reproduces the exact same generated code as the old single + monolithic function, not a real call. + Parameters: POSITION *pos, COOR c, PAWN_HASH_ENTRY *pHash, - ULONG uTotalNonPawnsBothSides : TotalNonPawnCount(pos, WHITE) + - TotalNonPawnCount(pos, BLACK), hoisted up to Eval() (chess.h's - TotalNonPawnCount is a POPCNT now, not a stored field -- see - its comment) since this function runs twice per Eval() call - (once per king) and used to recompute the identical sum both - times. + SEARCHER_THREAD_CONTEXT *ctx, + ULONG uColor, + ULONG xColor, + BITBOARD bbRing : g_KingAttacksBB[c], already computed by the caller Return value: - FLAG + SCORE **/ { - PIECE p; - ULONG uColor, xColor; - COOR cSquare; - ULONG u; BITV bvPattern; ULONG uQueenNearKing; - BITBOARD bb; - SCORE i; - SCORE iKingScore = 0; - #ifdef KING_DEFECT_DUMP - KING_DEFECT_DUMP_INFO *pDump; + COOR cSquare; + KING_DEFECT_DUMP_INFO *pDump = &ctx->sKingDefectDump[uColor]; #endif + SCORE iKingScore = 0; // 2026-09-15: linear-SCORE rewrite -- see king_safety_data/ // HANDOFF.md's 2026-09-15 addendum for the full rationale. Replaces @@ -5185,30 +5316,6 @@ Return value: // self-play regression, not the underlying feature signal itself. const ULONG uKingDangerAcceleratorBucketWidth = 56; - ASSERT(IS_ON_BOARD(c)); - p = pos->rgSquare[c].pPiece; - ASSERT(p && IS_KING(p)); - uColor = GET_COLOR(p); - xColor = FLIP(uColor); - ASSERT(IS_VALID_COLOR(uColor)); - - // Update attack bitvs for squares around the king. - BITBOARD bbRing = g_KingAttacksBB[c]; - pos->bbKingAttacks[uColor] |= bbRing; -#ifdef KING_DEFECT_DUMP - pDump = &ctx->sKingDefectDump[uColor]; - memset(pDump, 0, sizeof(KING_DEFECT_DUMP_INFO)); -#endif - - // Don't bother with king safety when the other side has no mating - // material. - u = pos->uNonPawnMaterial[xColor]; - ASSERT(u >= VALUE_KING); - if (u < DO_KING_SAFETY_THRESHOLD) - { - goto skip_safety; - } - // Depending on whether we are assessing a black or white king, // the "front" and "back" directions swap. Front is "towards // the center of the board at game start time". @@ -5377,11 +5484,6 @@ Return value: #endif // - // King's own file plus the two adjacent ones. EXPERIMENTAL, - // prototype-only (2026-09-05, board_representation/EVAL.md - // section 9's king-safety-hash discussion): now routed through - // _GetFileStormDefects's cache instead of computed inline. - // Already a direct SCORE as of the 2026-09-15 rewrite -- see // _ComputeFileStormDefects's comment. // iSafety += _GetFileStormDefects(ctx, pos, pHash, uColor); @@ -5393,15 +5495,15 @@ Return value: pDump->uFileStormStormingPawn; #endif - if (u >= KEEP_KING_AT_HOME_THRESHOLD) - { - iSafety += KING_INITIAL_LOCATION_SCALE * - KING_EVAL_INITIAL_LOCATION[uColor][c]; - } + // Unconditional within this whole function now -- KEEP_KING_AT_HOME_ + // THRESHOLD (a separate, stricter bar than DO_KING_SAFETY_THRESHOLD) + // used to gate this specifically, leaving a dead band between the + // two. Retired; see DO_KING_SAFETY_THRESHOLD's comment. + iSafety += KING_INITIAL_LOCATION_SCALE * + KING_EVAL_INITIAL_LOCATION[uColor][c]; #ifdef KING_DEFECT_DUMP pDump->uInitialCounterByLocation = - (u >= KEEP_KING_AT_HOME_THRESHOLD) ? - (ULONG)KING_EVAL_INITIAL_LOCATION[uColor][c] : 0; + (ULONG)KING_EVAL_INITIAL_LOCATION[uColor][c]; #endif // Danger-magnitude acceleration -- see KING_DANGER_ACCELERATOR's @@ -5454,138 +5556,255 @@ Return value: pDump->uAttackerArmyScaler = pos->uArmyScaler[xColor]; #endif - // Bonus for castling / penalty for loss of castle. Also, if side has - // not yet castled, be concerned with undeveloped minor pieces. + // Bonus for castling / penalty for loss of castle. Also, if + // side has not yet castled AND castling is still a live + // option, be concerned with undeveloped minor pieces. + // + // 2026-10-06: split the development penalty's gate from the + // missing-castle-option penalty's -- they need different + // conditions. Development should stop once the castling + // decision is permanently settled, whether that's by + // castling (the outer !fCastled check) or by losing both + // rights without ever castling (not caught by that check + // alone -- a side that never castles and never loses the + // right would otherwise be flagged "undeveloped" forever). + // The missing-castle-option penalty must do the opposite -- + // persist *after* a right is lost, not stop then -- since + // its whole purpose is discouraging throwing the right away + // in the first place; gating it the same way development is + // gated would silently disable it exactly when it's supposed + // to fire. + // + // Color-indexed instead of a WHITE/BLACK branch over two + // copy-pasted blocks -- one fewer per-piece branch for the + // processor to mispredict (board_representation/EVAL.md + // section 9). if (FALSE == pos->fCastled[uColor]) { ULONG uDevelopmentScaler = CASTLE_AND_DEVELOPMENT_SCALER[pos->uArmyScaler[uColor]]; if (uDevelopmentScaler > 0) { + static const BITV CASTLE_SHORT[2] = + { CASTLE_BLACK_SHORT, CASTLE_WHITE_SHORT }; + static const BITV CASTLE_LONG[2] = + { CASTLE_BLACK_LONG, CASTLE_WHITE_LONG }; + BITV bvBothRights = CASTLE_SHORT[uColor] | CASTLE_LONG[uColor]; + // One popcount answers both "can this side still + // castle at all" (>0, gates development below) and + // "how many options are missing" (2-count, scales + // the persistent penalty below) -- replaces two + // separate (==0) branches/multiplies/EVAL_TERM calls + // with one shared count. + ULONG uRightsRemaining = + FastCountBits(pos->bvCastleInfo & bvBothRights); + ASSERT(uRightsRemaining <= 2); + + // Branchless: (uRightsRemaining > 0) is 0 or 1, multiplied + // straight into the term instead of an `if` -- nothing + // left to mispredict. ASSERT(pos->uMinorsAtHome[uColor] <= 4); EVAL_TERM(uColor, 0, ILLEGAL_COOR, pos->iScore[uColor], - (SCORE)((UNDEVELOPED_MINORS_IN_OPENING[pos->uMinorsAtHome[uColor]] * + (SCORE)(((uRightsRemaining > 0) * + UNDEVELOPED_MINORS_IN_OPENING[pos->uMinorsAtHome[uColor]] * (SCORE)uDevelopmentScaler) / 8), "development"); - // Color-indexed instead of a WHITE/BLACK branch over - // two copy-pasted blocks -- one fewer per-piece branch - // for the processor to mispredict (board_representation/ - // EVAL.md section 9). - static const BITV CASTLE_SHORT[2] = - { CASTLE_BLACK_SHORT, CASTLE_WHITE_SHORT }; - static const BITV CASTLE_LONG[2] = - { CASTLE_BLACK_LONG, CASTLE_WHITE_LONG }; - EVAL_TERM(uColor, - KING, - c, - iKingScore, - (SCORE)((KING_MISSING_ONE_CASTLE_OPTION * - ((CASTLE_SHORT[uColor] & pos->bvCastleInfo) == 0) * - (SCORE)uDevelopmentScaler) / 8), - "can't castle short"); + // Deliberately NOT gated on uRightsRemaining > 0 -- + // see this block's header comment. Fires at full + // weight (both options gone) even long after the + // rights were lost, for as long as uDevelopmentScaler + // itself stays nonzero. EVAL_TERM(uColor, KING, c, iKingScore, (SCORE)((KING_MISSING_ONE_CASTLE_OPTION * - ((CASTLE_LONG[uColor] & pos->bvCastleInfo) == 0) * + (SCORE)(2 - uRightsRemaining) * (SCORE)uDevelopmentScaler) / 8), - "can't castle long"); + "missing castle option(s)"); #ifdef KING_DEFECT_DUMP pDump->fMissingCastleShort = ((CASTLE_SHORT[uColor] & pos->bvCastleInfo) == 0); pDump->fMissingCastleLong = ((CASTLE_LONG[uColor] & pos->bvCastleInfo) == 0); #endif } } + return iKingScore; +} + + +static SCORE FORCEINLINE +_EvalKingActivity(IN POSITION *pos, + IN const COOR c, + IN const PAWN_HASH_ENTRY *pHash, + IN SEARCHER_THREAD_CONTEXT *ctx, + IN const ULONG uColor, + IN const ULONG xColor) +/** + +Routine description: + + Activity half of _EvalKing's 2026-10-06 safety-vs-activity split -- + see that function's header comment. KING_TO_CENTER unconditionally; + once the enemy is down to at most one non-king piece, the sharper + passer-support/in-the-action guidance too. Only called when the + enemy lacks mating material (below DO_KING_SAFETY_THRESHOLD). + FORCEINLINE -- see _EvalKingSafety's comment for why this costs + nothing over the old monolithic function. + +Parameters: + + POSITION *pos, + COOR c, + PAWN_HASH_ENTRY *pHash, + SEARCHER_THREAD_CONTEXT *ctx, + ULONG uColor, + ULONG xColor + +Return value: + + SCORE + +**/ +{ + SCORE i; + SCORE iKingScore = 0; - skip_safety: // - // Special code for kings in the endgame + // Come out. KING_TO_CENTER applies unconditionally (no longer + // gated by uTotalNonPawnsBothSides<8 -- that was a different, + // combined-both-sides unit than this whole split now uses, and + // left a gap between "safety stops mattering" and "centralizing + // is actually encouraged"; see _EvalKing's header comment). // - u = uTotalNonPawnsBothSides; - if (u < 8) + i = KING_TO_CENTER[c]; + EVAL_TERM(uColor, + KING, + c, + iKingScore, + i, + "centralize king"); + + // Sharper late-game guidance once the enemy is down to at most + // one non-king piece (lone king, K+minor, K+rook, even K+queen + // -- though a lone queen's own material value already exceeds + // DO_KING_SAFETY_THRESHOLD on its own, VALUE_QUEEN=975 > + // VALUE_ROOK+VALUE_BISHOP=800, so that specific case can never + // actually reach this function at all; the count check below is + // correct on its own terms regardless). Per-call/per-king + // (xColor's own material), not the old combined-both-sides + // uTotalNonPawnsBothSides==2 -- the two kings' own enemies can + // legitimately disagree now. + if (TotalNonPawnCount(pos, xColor) <= 2) { - // Encourage kings to come to the center when safe. - i = KING_TO_CENTER[c]; + // Supporting-own-passer and out-of-action-in-KP-endgames, + // combined into one cached lookup (_GetKingActivityBonus) + // -- both depend only on (pawn structure + this king's own + // square), both only ever apply in this same branch, so + // one key computation/hash probe serves both instead of + // two. Trades away per-passer-square EVAL_TERM attribution + // (EVAL_DUMP tracing only, not used in release builds) for + // fewer instructions in the inner loop, by direct request + // -- see git history for the old separated version if + // finer dump granularity is ever needed again. + i = _GetKingActivityBonus(ctx, pos, pHash, uColor, c); EVAL_TERM(uColor, KING, c, iKingScore, i, - "centralize king"); + "king activity (passer support + in/out of action)"); + } + return iKingScore; +} - // Kings in front of passers in the late endgame are strong... - // -- bb here is exactly the own-passer squares at king-move - // distance 1 (g_KingAttacksBB is that mask by construction), so - // every popped square counts; no separate DISTANCE() filter or - // ASSERTs needed. The extra "leading the pawn toward promotion" - // bonus applies iff the passer square is in the king's own - // back ring (g_KingRingBackBB) -- side- and front-ring passers - // still get the base rank-scaled support bonus, just not this - // extra. - bb = pHash->bbPasserLocations[uColor] & g_KingAttacksBB[c]; - while(IS_ON_BOARD(cSquare = CoorFromBitBoardRank8ToRank1(&bb))) - { - i = 2; - i += KING_SUPPORTING_OWN_PASSER_BY_RANK[uColor][RANK(cSquare)]; - i += 8 * ((COOR_TO_BB(cSquare) & g_KingRingBackBB[uColor][c]) != 0); - EVAL_TERM(uColor, - KING, - c, - iKingScore, - i, - "supporting own passer"); - } - // - // Detect kings way out of the action in KP endgames: - // - // 8/8/1p6/p6k/P3K3/8/1P6/8 w - - 0 11 - // - if ((u == 2) && (uColor == WHITE)) - { - // 2026-09-16: this used to sum the raw per-pawn king- - // distance differential (each term in [-7,+7]) over EVERY - // pawn on the board, then multiply the whole sum by 12 -- - // unbounded in pawn count, so a genuine K+P endgame with - // 10+ pawns still on the board could reach several hundred - // centipawns from this one soft "my king is a bit more - // active" term (confirmed directly: CALIBRATE_MARGIN_ - // SAFETY-adjacent sampling turned up real +391/-384 - // outliers traced to exactly this line). That's worth more - // than a whole extra rook or bishop would be anywhere else - // in this eval, for a term that only ever fires when - // NEITHER side has a single piece left besides pawns (u==2 - // gate) -- clearly wrong on its face regardless of the - // exact right number. Kept the raw per-pawn sum (rather - // than normalizing to an average) deliberately -- more - // pawns still on the board is a real reason this term - // could matter more, not just noise to average away -- and - // instead shrunk the per-pawn weight from 12 to 2, so even - // an unrealistic worst case (16 pawns, every one at the - // max +/-7 distance differential) caps at 224 and realistic - // cases land around 40-80, versus the old 500-700+. - // PROVISIONAL: the "2" stops the runaway magnitude but - // isn't derived from any calibration. Needs the same self- - // play validation discipline as every other king-safety - // magnitude in this file before trusting the new ceiling. - i = 0; - bb = (pos->bbPawns[WHITE] | pos->bbPawns[BLACK]); - while(IS_ON_BOARD(cSquare = CoorFromBitBoardRank8ToRank1(&bb))) - { - i += (DISTANCE(cSquare, KingPosition(pos, BLACK)) - - DISTANCE(cSquare, KingPosition(pos, WHITE))); - } - EVAL_TERM(WHITE, - KING, - c, - iKingScore, - i * 2, - "in/out of \"action\""); - } +static void +_EvalKing(IN OUT POSITION *pos, + IN const COOR c, + IN const PAWN_HASH_ENTRY *pHash, + IN SEARCHER_THREAD_CONTEXT *ctx) +/** + +Routine description: + + 2026-10-06: restructured into an explicit safety-vs-activity split + on pos->uNonPawnMaterial[xColor] (this king's own enemy's + material) against DO_KING_SAFETY_THRESHOLD -- above it, stay + hidden (_EvalKingSafety: full ring-attack/danger scoring, stay- + on-initial-square bonus); below it, come out (_EvalKingActivity: + KING_TO_CENTER unconditionally, plus, once the enemy is down to + at most one non-king piece, the sharper passer-support/in-the- + action guidance). Replaces the old `goto skip_safety` plus two + *separate*, independently-thresholded gates (KEEP_KING_AT_HOME_ + THRESHOLD for the stay-home bonus, uTotalNonPawnsBothSides<8/==2 + -- a different, combined-both-sides unit -- for the endgame + terms) that didn't line up with each other or with this one, + leaving a dead band where safety was scored but neither a stay- + home nor a come-out incentive applied. This single threshold now + drives both halves, so the two outcomes are exact mirror images + of one decision rather than three loosely related ones. + + Split into two FORCEINLINE helpers purely for readability -- see + _EvalKingSafety's comment for why this costs nothing versus the + old single monolithic function. + + No longer takes uTotalNonPawnsBothSides -- the old combined-both- + sides gate it fed is gone, replaced by a per-call check + (TotalNonPawnCount(pos, xColor), this king's own enemy only, not a + hoisted both-sides sum), so there's no shared value left worth + hoisting out to Eval(). + +Parameters: + + POSITION *pos, + COOR c, + PAWN_HASH_ENTRY *pHash, + SEARCHER_THREAD_CONTEXT *ctx + +Return value: + + FLAG + +**/ +{ + PIECE p; + ULONG uColor, xColor; + ULONG u; + SCORE iKingScore; + + ASSERT(IS_ON_BOARD(c)); + p = pos->rgSquare[c].pPiece; + ASSERT(p && IS_KING(p)); + uColor = GET_COLOR(p); + xColor = FLIP(uColor); + ASSERT(IS_VALID_COLOR(uColor) && IS_VALID_COLOR(xColor)); + + // Update attack bitvs for squares around the king. + BITBOARD bbRing = g_KingAttacksBB[c]; + pos->bbKingAttacks[uColor] |= bbRing; +#ifdef KING_DEFECT_DUMP + { + KING_DEFECT_DUMP_INFO *pDump = &ctx->sKingDefectDump[uColor]; + memset(pDump, 0, sizeof(KING_DEFECT_DUMP_INFO)); + } +#endif + + // Safety-vs-activity split -- see this function's header comment + // (2026-10-06 restructuring). Don't bother with king safety when + // the other side has low material; instead, actively encourage + // this king out. + u = pos->uNonPawnMaterial[xColor]; + ASSERT(u >= VALUE_KING); + if (u >= DO_KING_SAFETY_THRESHOLD) + { + iKingScore = _EvalKingSafety(pos, c, pHash, ctx, uColor, xColor, bbRing); + } + else + { + iKingScore = _EvalKingActivity(pos, c, pHash, ctx, uColor, xColor); } pos->iScore[uColor] += iKingScore; ctx->sPlyInfo[ctx->uPly].iKingScore[uColor] = iKingScore; @@ -6483,7 +6702,7 @@ Return value: } -FLAG +FLAG FORCEINLINE SideHasWinningChances(IN POSITION *pos, IN ULONG uSide) /** @@ -7694,11 +7913,6 @@ Return value: pos->uNumTrapped[BLACK] = pos->uNumTrapped[WHITE] = 0; pos->iReducedMaterialDownScaler[BLACK] = pos->iReducedMaterialDownScaler[WHITE] = 0; - pos->uClosedScaler = (FastCountBits(pos->bbPawns[WHITE]) + FastCountBits(pos->bbPawns[BLACK])) - - (pHash->uNumUnmovedPawns[WHITE] + pHash->uNumUnmovedPawns[BLACK]) + - (pHash->uNumRammedPawns); - ASSERT(pos->uClosedScaler >= 0); - ASSERT(pos->uClosedScaler <= 32); pos->uMinorsAtHome[BLACK] = pos->uMinorsAtHome[WHITE] = 0; // @@ -7854,13 +8068,7 @@ Return value: #endif } - // Evaluate the two kings last. Compute both sides' non-pawn count - // once here rather than in _EvalKing itself -- it's called twice - // below (once per king) and used to recompute the identical - // TotalNonPawnCount(WHITE)+TotalNonPawnCount(BLACK) sum both times. - ULONG uTotalNonPawnsBothSides = - TotalNonPawnCount(pos, WHITE) + TotalNonPawnCount(pos, BLACK); - + // Evaluate the two kings last. c = KingPosition(pos, uColor); #ifdef DEBUG ASSERT(IS_ON_BOARD(c)); @@ -7872,7 +8080,7 @@ Return value: } #endif TIMED_EVAL_CALL(ctx, u64CyclesEvalKing, - _EvalKing(pos, c, pHash, ctx, uTotalNonPawnsBothSides)); + _EvalKing(pos, c, pHash, ctx)); #ifdef EVAL_DUMP Trace("After our K at %s:\n%d\t\t%d\t\t(%d for %s)\n", CoorToString(c), @@ -7893,7 +8101,7 @@ Return value: } #endif TIMED_EVAL_CALL(ctx, u64CyclesEvalKing, - _EvalKing(pos, c, pHash, ctx, uTotalNonPawnsBothSides)); + _EvalKing(pos, c, pHash, ctx)); #ifdef EVAL_DUMP Trace("After enemy K at %s:\n%d\t\t%d\t\t(%d for %s)\n", CoorToString(c), @@ -7929,7 +8137,7 @@ Return value: // slots first so a clean full eval reliably means a clean slate. ctx->cEnprise[ctx->uPly][0] = ctx->cEnprise[ctx->uPly][1] = ILLEGAL_COOR; ctx->cTrapped[ctx->uPly] = ILLEGAL_COOR; - _EvalLookForDanger(ctx); + //_EvalLookForDanger(ctx); _EvalTrappedPieces(ctx); // TODO: endgame-specific knowledge? e.g. B over N in an endgame |
