summaryrefslogtreecommitdiff
path: root/src/eval_tune/mine_king_mates.py
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-09-15 13:31:36 -0700
committerScott Gasch <[email protected]>2026-09-15 13:31:36 -0700
commit390e675849ff42c8b7e1806f75552de988741fff (patch)
tree17b8792aeaf74373bbaef3bac6b71bb5c0d74c69 /src/eval_tune/mine_king_mates.py
parentca73728f7dbf01c4b39c7496729f81b1fbffa00b (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.py72
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__":