summaryrefslogtreecommitdiff
path: root/src/eval.c
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-10-07 08:17:51 -0700
committerScott Gasch <[email protected]>2026-10-07 08:17:51 -0700
commitd14dbe2a50c220b2adac758c3d4f1a1fa67ece0b (patch)
treecbf6d9928bcc41ef833b62b70479113f7d419925 /src/eval.c
parent447130db3df87b40d080dc26429d45e9213c19f7 (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.c582
1 files changed, 395 insertions, 187 deletions
diff --git a/src/eval.c b/src/eval.c
index 18c3464..e102daa 100644
--- a/src/eval.c
+++ b/src/eval.c
@@ -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