summaryrefslogtreecommitdiff
path: root/src/eval.c
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-09-13 23:18:07 -0700
committerScott Gasch <[email protected]>2026-09-13 23:18:07 -0700
commit4ff9170fd96e3da049c747bf5215e908a94ff145 (patch)
tree6c8e6f846b42518d84357bb3b83014feddc83202 /src/eval.c
parent78d1889fe38db1fd45b58509c8b3bd535ce691b7 (diff)
Add king-safety mate-mining regression pipeline; retune file-storm/
queen-proximity king-safety weights and add a back-flight bonus from it. Mines real human-game checkmates and matched sound-draw controls (eval_tune/mine_king_mates.py, shard_mine.py, extract_king_features.py, match_controls.py, train_king_safety_model.py), then fits a logistic regression against _EvalKing's raw per-defect breakdown (engine-side support: chess.h's KING_DEFECT_DUMP_INFO, eval.c's KING_DEFECT_DUMP instrumentation, command.c's evalkingdump command, all gated behind the KING_DEFECT_DUMP=1 build flag) to see which defects actually predict getting mated. See eval_tune/king_safety_data/HANDOFF.md for the full methodology and findings. Two rounds of retuning landed from this data so far, both validated via sd10 curated-suite comparison plus per-position Crafty corroboration (tests/ECM.md) and gated self-play: - KingFileDefects/KingStormingPawnDefects (_ComputeFileStormDefects) bumped ~1.75-2x relative to the rest of the ring-scan defects -- the regression's most dominant, most stable tier-1 predictors, previously weighted the same as every other hand-picked defect. - KING_QUEEN_PROXIMITY_DANGER rescaled from -8/count to -32/count, and a new KING_BACK_FLIGHT_BONUS (+1/square) term added for empty, unattacked ring squares on the side of the king away from the attacker -- a real, non-collinear protective signal the current EmptyUnattackedSquare table didn't credit at all. Both calibrated by unstandardizing the regression's coefficients and anchoring against KING_MISSING_ONE_CASTLE_OPTION's already-live -23 value (same direct-EVAL_TERM unit family). Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01P9EDZVcqu6E2bNRm4zDrTf
Diffstat (limited to 'src/eval.c')
-rw-r--r--src/eval.c227
1 files changed, 221 insertions, 6 deletions
diff --git a/src/eval.c b/src/eval.c
index 0a8f71e..709968a 100644
--- a/src/eval.c
+++ b/src/eval.c
@@ -712,13 +712,44 @@ static SCORE KING_SAFETY_BY_COUNTER[42] =
// see the 180-position king-safety-term distribution sampled
// 2026-08-30 (median -27, p75 -16, only ~1% worse than -200) for the
// scale this needs to slot into.
+// 2026-09-14: rescaled from the original -8/count step (see the
+// 2026-08-30 comment above -- that step was calibrated against a
+// *removed* legacy term's own magnitude, not against real mate data).
+// king_safety_data/HANDOFF.md's mate-mining logistic regression found
+// `queenproximity` (after folding in the collinear front/back/sq3/sq4
+// atk_queen sub-features it was absorbing -- see
+// eval_tune/king_safety_data/HANDOFF.md's 2026-09-14 addendum) has a
+// per-unit raw coefficient of +1.00, calibrated against
+// `misscastleshort`'s own cleaned coefficient (+0.71) and this table's
+// sibling constant KING_MISSING_ONE_CASTLE_OPTION's already-live -23
+// value (same direct-EVAL_TERM unit family, unlike the file-storm
+// tables below which feed the separate uCounter/KING_SAFETY_BY_COUNTER
+// lookup and need their own, different calibration) -- see
+// (misscastleshort_live / misscastleshort_raw_coef) = k, then
+// k * queenproximity_raw_coef =~ -32/count. Validate via the usual
+// sd10 + self-play gate before trusting this magnitude.
static SCORE KING_QUEEN_PROXIMITY_DANGER[7] =
-{// 0 1 2 3 4 5 6
- +0, -8, -16, -24, -32, -40, -48
+{// 0 1 2 3 4 5 6
+ +0, -32, -64, -96, -128, -160, -192
};
static SCORE KING_MISSING_ONE_CASTLE_OPTION = -23;
+// 2026-09-14: new term, not a retune of an existing one.
+// king_safety_data/HANDOFF.md's regression found `back_flight` (an
+// empty, unattacked ring square on the side of the king AWAY from the
+// attacker) is a real, non-collinear PROTECTIVE signal (negative raw
+// coefficient -0.035, i.e. more open escape squares behind the king
+// predicts LESS mate risk) -- unlike `front_flight`, which is a real
+// danger signal already captured by EmptyUnattackedSquare's front
+// weight. Calibrated the same way as KING_QUEEN_PROXIMITY_DANGER above
+// (misscastleshort-anchored k =~ -32.4, applied to back_flight's raw
+// coefficient) =~ +1.1/square, rounded to +1. Small and deliberately
+// so -- this is the single most novel, least-collinear finding from
+// the regression, worth landing and validating in isolation before
+// combining with anything else.
+static SCORE KING_BACK_FLIGHT_BONUS = 1;
+
// Flat "this is bad" flags for _EvalTrappedPieces -- not an attempt to
// price the material outcome (search resolves that), just a nudge away
// from positions with a piece that looks stuck. ENPRISE_AND_TRAPPED is
@@ -788,7 +819,8 @@ static DNA_BASE_SIZE g_EvalDNA[] = {
DNA_ARRAY(KING_TO_CENTER),
DNA_ARRAY(KING_SAFETY_BY_COUNTER),
DNA_ARRAY(KING_QUEEN_PROXIMITY_DANGER),
- DNA_VAR(KING_MISSING_ONE_CASTLE_OPTION)
+ DNA_VAR(KING_MISSING_ONE_CASTLE_OPTION),
+ DNA_VAR(KING_BACK_FLIGHT_BONUS)
};
ULONG
@@ -2402,13 +2434,19 @@ Return value:
**/
{
+ // 2026-09-14: scaled up ~1.75-2x relative to the other _EvalKing
+ // defect tables (king_safety_data/HANDOFF.md, tier 1 -- these two
+ // came back as the dominant, most stable predictors of a real mate
+ // by a clear margin over every other defect, but previously shared
+ // the same +0..+4 magnitude as everything else). Keep this table in
+ // sync with _ComputeFileStormDefectsSplit's copy below.
static ULONG KingStormingPawnDefects[8] =
{
- +0, +4, +3, +1, +0, +0, +0, +0
+ +0, +7, +5, +2, +0, +0, +0, +0
};
static ULONG KingFileDefects[3] =
{
- +2, +1, +0
+ +4, +2, +0
};
ULONG ufColor = FLIP(uColor);
COOR c = pos->cNonPawns[uColor][0];
@@ -2449,6 +2487,79 @@ Return value:
}
+#ifdef KING_DEFECT_DUMP
+static void
+_ComputeFileStormDefectsSplit(IN POSITION *pos,
+ IN const PAWN_HASH_ENTRY *pHash,
+ IN ULONG uColor,
+ OUT ULONG *puOpenFile,
+ OUT ULONG *puStormingPawn)
+/**
+
+Routine description:
+
+ Debug-only sibling of _ComputeFileStormDefects that reports its two
+ independent sub-ideas (open/half-open file bonus vs. storming-pawn
+ distance penalty) separately instead of pre-summed, so a regression
+ can weigh them independently -- they already live in two distinct
+ hand-picked tables (KingFileDefects / KingStormingPawnDefects) with
+ no structural change needed to retune them individually once this
+ tells us their relative importance. See CLAUDE.md's king-safety
+ discussion. Logic intentionally identical to _ComputeFileStormDefects,
+ just split into two accumulators -- keep both in sync if either
+ changes.
+
+**/
+{
+ // Keep in sync with _ComputeFileStormDefects's copy above.
+ static ULONG KingStormingPawnDefects[8] =
+ {
+ +0, +7, +5, +2, +0, +0, +0, +0
+ };
+ static ULONG KingFileDefects[3] =
+ {
+ +4, +2, +0
+ };
+ ULONG ufColor = FLIP(uColor);
+ COOR c = pos->cNonPawns[uColor][0];
+ COOR cFileSq, cSquare;
+ ULONG u, v;
+ BITBOARD bb;
+ int w;
+
+ *puOpenFile = 0;
+ *puStormingPawn = 0;
+ for (w = -1; w <= 1; w++)
+ {
+ cFileSq = c + w;
+ if (!IS_ON_BOARD(cFileSq))
+ {
+ continue;
+ }
+ u = FILE(cFileSq) + 1;
+ v = (pHash->uCountPerFile[WHITE][u] > 0) +
+ (pHash->uCountPerFile[BLACK][u] > 0);
+ ASSERT((v >= 0) && (v <= 2));
+ *puOpenFile += (KingFileDefects[v] + ((v < 2) && ((u == 1) || (u == 8))));
+ bb = pos->bbPawns[ufColor] & BBFILE[u - 1];
+ if (bb)
+ {
+ if (uColor == WHITE)
+ {
+ cSquare = CoorFromBitBoardRank1ToRank8(&bb);
+ }
+ else
+ {
+ cSquare = CoorFromBitBoardRank8ToRank1(&bb);
+ }
+ ASSERT(IS_ON_BOARD(cSquare));
+ *puStormingPawn += KingStormingPawnDefects[DISTANCE(cSquare, c)];
+ }
+ }
+}
+#endif
+
+
static ULONG
_GetFileStormDefects(IN SEARCHER_THREAD_CONTEXT *ctx,
IN POSITION *pos,
@@ -4121,6 +4232,7 @@ Return value:
BITV bvDefend;
BITV bvPattern = 0;
ULONG uFlightSquares;
+ ULONG uBackFlightSquares;
ULONG uQueenNearKing = 0;
BITBOARD bb;
SCORE i;
@@ -4147,6 +4259,21 @@ Return value:
+15, +16, +17, 0
};
+ // 2026-09-14: color-dependent front/back ring-slot classification,
+ // mirroring EmptyAttackedSquare/EmptyUnattackedSquare's own existing
+ // asymmetry (WHITE's front is slots 0/1/2, BLACK's is 5/6/7) and
+ // matching eval_tune/train_king_safety_model.py's BUCKET_SLOTS --
+ // used to isolate the back-bucket flight-square bonus below. Slots
+ // 3/4 (the same-rank side squares) are neither front nor back, same
+ // as the regression's own treatment.
+ static const FLAG KING_RING_IS_BACK[2][8] =
+ {
+ // BLACK: back = slots 0,1,2 (front = 5,6,7)
+ { TRUE, TRUE, TRUE, FALSE, FALSE, FALSE, FALSE, FALSE },
+ // WHITE: back = slots 5,6,7 (front = 0,1,2)
+ { FALSE, FALSE, FALSE, FALSE, FALSE, TRUE, TRUE, TRUE },
+ };
+
static ULONG EmptyAttackedSquare[2][8] =
{
{
@@ -4181,6 +4308,10 @@ Return value:
2
};
+#ifdef KING_DEFECT_DUMP
+ KING_DEFECT_DUMP_INFO *pDump;
+#endif
+
ASSERT(IS_ON_BOARD(c));
p = pos->rgSquare[c].pPiece;
ASSERT(p && IS_KING(p));
@@ -4188,6 +4319,10 @@ Return value:
xColor = FLIP(uColor);
ASSERT(IS_VALID_COLOR(uColor));
pos->bbKingAttacks[uColor] |= g_KingAttacksBB[c];
+#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.
@@ -4201,12 +4336,16 @@ Return value:
// Prepare to do king safety
uCounter = (u >= KEEP_KING_AT_HOME_THRESHOLD) *
KING_INITIAL_COUNTER_BY_LOCATION[uColor][c];
+#ifdef KING_DEFECT_DUMP
+ pDump->uInitialCounterByLocation = uCounter;
+#endif
#ifdef EVAL_DUMP
Trace("%s Initial KS Counter: %u\n", COLOR_NAME(uColor), uCounter);
#endif
BITBOARD bbEnemyOcc = _BuildFriendlySideBB(pos, xColor);
uFlightSquares = 0;
+ uBackFlightSquares = 0;
u = 0;
ASSERT(KingSafetyDeltas[u] != 0);
do
@@ -4264,6 +4403,54 @@ Return value:
//
uQueenNearKing += (((bvAttack | bvXray) & QUEEN_BIT) != 0);
+#ifdef KING_DEFECT_DUMP
+ {
+ BITV bvEither = bvAttack | bvXray;
+ ASSERT(u < KING_RING_NUM_SLOTS);
+ pDump->fOnBoard[u] = TRUE;
+ pDump->fAttackedByPieceType[u][KING_RING_PAWN] =
+ ((bvEither & PAWN_BIT) != 0);
+ pDump->fAttackedByPieceType[u][KING_RING_MINOR] =
+ ((bvEither & MINOR_BIT) != 0);
+ pDump->fAttackedByPieceType[u][KING_RING_ROOK] =
+ ((bvEither & ROOK_BIT) != 0);
+ pDump->fAttackedByPieceType[u][KING_RING_QUEEN] =
+ ((bvEither & QUEEN_BIT) != 0);
+ if (bvAttack != 0)
+ {
+ // NB: bvDefend here still includes the own-king bit
+ // that the real code strips below via bvDefend &=
+ // ~KING_BIT once xrayed/multiply-attacked -- match
+ // that exact "is this square adequately defended"
+ // semantics rather than the raw pre-strip value.
+ BITV bvDefendForDump = bvDefend;
+ if (bvXray || (bvAttack & (bvAttack - 1)))
+ {
+ bvDefendForDump &= ~KING_BIT;
+ }
+ if (bvDefendForDump == 0)
+ {
+ pDump->fAttackedUndefended[u] = TRUE;
+ }
+ else
+ {
+ pDump->fAttackedDefended[u] = TRUE;
+ }
+ }
+ else
+ {
+ if (bvXray != 0)
+ {
+ pDump->fXrayedUnattacked[u] = TRUE;
+ }
+ if (!(pos->bbOccupied & sq))
+ {
+ pDump->fEmptyFlight[u] = TRUE;
+ }
+ }
+ }
+#endif
+
//
// Tried a branchless rewrite of this attacked/unattacked
// split (2026-09-09): compute both sides' contributions
@@ -4303,10 +4490,12 @@ Return value:
}
else
{
+ FLAG fEmpty = ((pos->bbOccupied & sq) == 0);
uCounter += (bvXray != 0);
ASSERT(u < 8);
uCounter += EmptyUnattackedSquare[uColor][u];
- uFlightSquares += ((pos->bbOccupied & sq) == 0);
+ uFlightSquares += fEmpty;
+ uBackFlightSquares += (fEmpty && KING_RING_IS_BACK[uColor][u]);
}
}
u++;
@@ -4327,6 +4516,12 @@ Return value:
// isolated sd10 suite testing before that landing).
//
uCounter += _GetFileStormDefects(ctx, pos, pHash, uColor);
+#ifdef KING_DEFECT_DUMP
+ pDump->uFileStormDefects = _GetFileStormDefects(ctx, pos, pHash, uColor);
+ _ComputeFileStormDefectsSplit(pos, pHash, uColor,
+ &pDump->uFileStormOpenFile,
+ &pDump->uFileStormStormingPawn);
+#endif
#ifdef EVAL_DUMP
Trace("%s KS Counter post-open file/stormers: %u\n", COLOR_NAME(uColor),
uCounter);
@@ -4351,6 +4546,14 @@ Return value:
// are scaling it based on the _other_ side's material.
uCounter = MINU(uCounter, 41);
i = KING_SAFETY_BY_COUNTER[uCounter];
+#ifdef KING_DEFECT_DUMP
+ pDump->uQueenNearKing = uQueenNearKing;
+ pDump->uCounterTotal = uCounter;
+ pDump->uSafetyTableValue = (ULONG)(-i);
+ pDump->fCastled = pos->fCastled[uColor];
+ pDump->uMinorsAtHome = pos->uMinorsAtHome[uColor];
+ pDump->uAttackerArmyScaler = pos->uArmyScaler[xColor];
+#endif
#ifdef CALIBRATE_POSITIONAL
// KING_SAFETY_BY_COUNTER is <= 0 throughout (a penalty); the
// calibration histogram bins magnitude, matching iKingSwingP90's
@@ -4375,6 +4578,14 @@ Return value:
KING_QUEEN_PROXIMITY_DANGER[MINU(uQueenNearKing, 6)],
"queen proximity danger");
+ ASSERT(uBackFlightSquares <= 3);
+ EVAL_TERM(uColor,
+ KING,
+ c,
+ iKingScore,
+ (SCORE)(KING_BACK_FLIGHT_BONUS * uBackFlightSquares),
+ "back rank flight bonus");
+
// Bonus for castling / penalty for loss of castle. Also, if side has
// not yet castled, be concerned with undeveloped minor pieces.
if (FALSE == pos->fCastled[uColor])
@@ -4415,6 +4626,10 @@ Return value:
((CASTLE_LONG[uColor] & pos->bvCastleInfo) == 0) *
(SCORE)uDevelopmentScaler) / 8),
"can't castle long");
+#ifdef KING_DEFECT_DUMP
+ pDump->fMissingCastleShort = ((CASTLE_SHORT[uColor] & pos->bvCastleInfo) == 0);
+ pDump->fMissingCastleLong = ((CASTLE_LONG[uColor] & pos->bvCastleInfo) == 0);
+#endif
}
}