diff options
| author | Scott Gasch <[email protected]> | 2026-09-13 23:18:07 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-09-13 23:18:07 -0700 |
| commit | 4ff9170fd96e3da049c747bf5215e908a94ff145 (patch) | |
| tree | 6c8e6f846b42518d84357bb3b83014feddc83202 /src/eval.c | |
| parent | 78d1889fe38db1fd45b58509c8b3bd535ce691b7 (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.c | 227 |
1 files changed, 221 insertions, 6 deletions
@@ -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 } } |
