diff options
| author | Scott Gasch <[email protected]> | 2026-09-15 13:31:36 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-09-15 13:31:36 -0700 |
| commit | 390e675849ff42c8b7e1806f75552de988741fff (patch) | |
| tree | 17b8792aeaf74373bbaef3bac6b71bb5c0d74c69 /src/eval_tune/mine_king_mates.py | |
| parent | ca73728f7dbf01c4b39c7496729f81b1fbffa00b (diff) | |
Rewrite _EvalKing as a linear ring-scan and promote its terms to DNA-tunable constants; retune via mate-mining regression, ElasticNet/Ridge, and logistic-vs-material refits.
Splits _EvalKing's own king-location table from CountKingSafetyDefects' search-gate copy so future eval retunes can't silently perturb extension/reduction gate frequency. Bumps KING_BACK_FLIGHT_BONUS (+1 -> +3) and reorders/nudges the ring-scan sub-terms (minor/pawn/xray/undefended) to match relative rankings that replicated across three independent calibration methods, while leaving KING_QUEEN_PROXIMITY_DANGER and KING_MISSING_ONE_CASTLE_OPTION untouched pending further discussion.
Self-play (640 games) is roughly even (score 0.509, llr=+0.17, far from either SPRT bound) but consistently faster (nps candidate=1362140 vs head=1333682) at equal average depth -- landing as a wash/speed-neutral checkpoint, not a confirmed tactical improvement, for further tuning to build on.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Diffstat (limited to 'src/eval_tune/mine_king_mates.py')
| -rw-r--r-- | src/eval_tune/mine_king_mates.py | 72 |
1 files changed, 70 insertions, 2 deletions
diff --git a/src/eval_tune/mine_king_mates.py b/src/eval_tune/mine_king_mates.py index 7b5866b..678c5ea 100644 --- a/src/eval_tune/mine_king_mates.py +++ b/src/eval_tune/mine_king_mates.py @@ -17,6 +17,31 @@ import csv import random import sys +# has_hanging_material (Python SEE, same swap-list algorithm already +# validated against 800k TWIC positions) -- reused rather than +# reimplemented, so "quiet" means the identical thing here as it does +# for the generic TWIC pool. 2026-09-15: this project's original +# mine_king_mates.py design deliberately did NOT filter for quietness +# (a position N plies before mate is *supposed* to capture real, +# unresolved danger, including in-check/mid-tactic cases) -- fine for +# the mate-mining regression, which never fed FENs to a static-eval-vs- +# outcome fit, but wrong for Texel tuning, which needs every training +# position's static eval to be meaningful on its own. Counters below +# report the actual quiet-survival rate directly -- expected to be much +# lower than TWIC's ~93-94% given near-mate positions are inherently +# more likely to be mid-tactic by construction. +sys.path.insert(0, ".") +from tune_eval_dna import has_hanging_material + +g_uPositiveCandidates = 0 +g_uPositiveQuiet = 0 +g_uControlCandidates = 0 +g_uControlQuiet = 0 + + +def is_quiet_position(board): + return (not board.is_check()) and (not has_hanging_material(board)) + # Matches eval.c's DO_KING_SAFETY_THRESHOLD minus the VALUE_KING baseline # _EvalKing adds internally: real threshold is "enemy has at least a # rook+bishop of non-pawn, non-king material" before king safety is even @@ -61,12 +86,24 @@ def total_non_pawn_piece_count(board): MIDDLEGAME_MIN_TOTAL_NON_PAWN_PIECES = 8 +# 2026-09-15, by direct instruction: a promotion within the last N plies +# of the game means this "mate" is really an endgame pawn-race/technique +# conversion (queen a pawn, then mate the resulting weak/bare king) -- +# not a middlegame king-safety collapse. We want positions where one +# side mounts a real attack on a king still attempting fortification/ +# defense, not a K+P-and-a-few-pieces endgame decided by promotion. +# Matches the n_backs cap (14) -- if we'd sample as far back as 14 plies +# before mate, a promotion inside that same trailing window taints every +# n_back for this game, not just the ones after the promotion. +LATE_PROMOTION_EXCLUSION_WINDOW = 14 + def is_middlegame(board): return total_non_pawn_piece_count(board) >= MIDDLEGAME_MIN_TOTAL_NON_PAWN_PIECES -def mine_game(game, n_backs, min_ply_from_start=20): +def mine_game(game, n_backs, min_ply_from_start=20, + promotion_exclusion_window=LATE_PROMOTION_EXCLUSION_WINDOW): """Yields (fen, mated_color, n_back) for a game ending in checkmate.""" board = game.board() moves = list(game.mainline_moves()) @@ -80,6 +117,15 @@ def mine_game(game, n_backs, min_ply_from_start=20): mated_color = board.turn # side to move at mate = side that got mated total_plies = len(moves) + # Cheap check -- Move objects already carry .promotion, no replay + # needed. Skips the WHOLE game's positives, not just n_backs past + # the promotion, since a late-game promotion taints the game's + # suitability as a middlegame king-safety example regardless of + # which n_back we're sampling. + if any(mv.promotion is not None + for mv in moves[max(0, total_plies - promotion_exclusion_window):]): + return + for n_back in n_backs: idx = total_plies - n_back if idx < min_ply_from_start: @@ -95,6 +141,11 @@ def mine_game(game, n_backs, min_ply_from_start=20): continue if not is_middlegame(replay): continue + global g_uPositiveCandidates, g_uPositiveQuiet + g_uPositiveCandidates += 1 + if not is_quiet_position(replay): + continue + g_uPositiveQuiet += 1 yield replay.fen(), mated_color, n_back @@ -171,6 +222,11 @@ def mine_controls(game, n_needed, rng, min_ply=20, max_ply_from_end=15): ply += 1 if not is_middlegame(replay): continue + global g_uControlCandidates, g_uControlQuiet + g_uControlCandidates += 1 + if not is_quiet_position(replay): + continue + g_uControlQuiet += 1 for color in (chess.WHITE, chess.BLACK): attacker = not color if non_pawn_material_cp(replay, attacker) >= MATING_MATERIAL_THRESHOLD_CP: @@ -186,7 +242,11 @@ def main(): ap = argparse.ArgumentParser() ap.add_argument("pgn_file") ap.add_argument("out_csv") - ap.add_argument("--n-backs", type=int, nargs="+", default=[4, 6, 8, 10]) + ap.add_argument("--n-backs", type=int, nargs="+", + default=[4, 6, 8, 10, 12, 14], + help="Plies before mate to sample -- capped at 14 by " + "direct instruction (anything farther back is too " + "loosely connected to the actual mating attack).") ap.add_argument("--max-games", type=int, default=None, help="Stop after scanning this many games (0/None = all)") ap.add_argument("--control-ratio", type=float, default=1.0, @@ -309,6 +369,14 @@ def main(): print(f"Wrote {len(positives) + len(controls)} rows to {args.out_csv}", file=sys.stderr) + pos_rate = (g_uPositiveQuiet / g_uPositiveCandidates + if g_uPositiveCandidates else 0.0) + ctl_rate = (g_uControlQuiet / g_uControlCandidates + if g_uControlCandidates else 0.0) + print(f"Quiet-filter yield: positives {g_uPositiveQuiet}/" + f"{g_uPositiveCandidates} ({pos_rate:.1%}), controls " + f"{g_uControlQuiet}/{g_uControlCandidates} ({ctl_rate:.1%})", + file=sys.stderr) if __name__ == "__main__": |
