#!/usr/bin/env python3 """Empirical magnitude sanity check: for the same ~500 curated near-mate "quiet" positions used to derive the king-safety ring-defect features (see mine_king_mates.py / matched_combined_full.csv), compare typhoon's own king-safety subscore (iSafety, dumped via KING_DEFECT_DUMP's new 'realsafety' field -- see chess.h/eval.c/command.c 2026-09-15 additions) against Crafty's "kings" row from its `score` command breakdown, for the side whose king safety is actually being assessed in each row. Crafty is spawned fresh (one process per FEN, via `setboard ; score; quit`) rather than batched in one long-lived process -- testing showed a second setboard+score call inside one Crafty session silently reused the FIRST position's cached score, not a real re-evaluation, so batching would have silently produced 500 copies of the same number. """ import csv import re import subprocess import sys CRAFTY_BIN = "/home/scott/crafty/crafty" CRAFTY_CWD = "/home/scott/crafty" TYPHOON_KINGDUMP = "/usr/home/scott/typhoon/src/typhoon_kingdump" IN_CSV = "/tmp/typhoon/near_mate_500.csv" OUT_CSV = "/tmp/typhoon/king_safety_crafty_compare.csv" KINGS_ROW_RE = re.compile( r"kings\.+\s+([-\d.]+)\s+\|\s+([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)\s+\|\s+" r"([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)\s+\|" ) def crafty_kings_comp(fen, color): """Returns Crafty's 'kings' row 'comp' column (in pawns) for `color`.""" script = f"setboard {fen}\nscore\nquit\n" p = subprocess.run([CRAFTY_BIN], input=script, cwd=CRAFTY_CWD, capture_output=True, text=True, timeout=30) m = KINGS_ROW_RE.search(p.stdout) if not m: return None white_comp, black_comp = float(m.group(1)), float(m.group(5)) return white_comp if color == "white" else black_comp def load_rows(): with open(IN_CSV, newline="") as f: return list(csv.DictReader(f)) def typhoon_realsafety_batch(rows): """Batches all FENs through one typhoon_kingdump `script` call (this engine's setboard/eval IS known to work correctly across repeated calls in one session -- see extract_king_features.py's run_shard).""" script_path = "/tmp/typhoon/compare_batch.script" out_path = "/tmp/typhoon/compare_batch.out" with open(script_path, "w") as f: f.write("force\nnopost\n") for r in rows: f.write(f"setboard {r['fen']}\n") f.write("evalkingdump\n") cmd = [TYPHOON_KINGDUMP, "--batch", "--command", f"script {script_path}"] with open(out_path, "w") as logf: subprocess.run(cmd, stdout=logf, stderr=subprocess.STDOUT, timeout=300) dumps = [] with open(out_path) as f: for line in f: if line.startswith("KINGDEFECTDUMP"): color = re.search(r"color=(\S+)", line).group(1) realsafety = int(re.search(r"realsafety=(-?\d+)", line).group(1)) dumps.append((color, realsafety)) if len(dumps) != 2 * len(rows): raise RuntimeError(f"expected {2*len(rows)} dump lines, got {len(dumps)}") out = [] for i, r in enumerate(rows): black_color, black_rs = dumps[2 * i] white_color, white_rs = dumps[2 * i + 1] assert black_color == "black" and white_color == "white" out.append(black_rs if r["mated_color_or_sampled_color"] == "black" else white_rs) return out def main(): rows = load_rows() print(f"Loaded {len(rows)} rows", file=sys.stderr) typhoon_vals = typhoon_realsafety_batch(rows) print(f"typhoon: got {len(typhoon_vals)} realsafety values", file=sys.stderr) results = [] for i, r in enumerate(rows): crafty_comp = crafty_kings_comp(r["fen"], r["mated_color_or_sampled_color"]) if crafty_comp is None: continue results.append({ "fen": r["fen"], "color": r["mated_color_or_sampled_color"], "typhoon_realsafety_cp": typhoon_vals[i], "crafty_kings_comp_cp": round(crafty_comp * 100, 1), }) if (i + 1) % 50 == 0: print(f" ...{i+1}/{len(rows)} crafty positions done", file=sys.stderr) with open(OUT_CSV, "w", newline="") as f: w = csv.DictWriter(f, fieldnames=["fen", "color", "typhoon_realsafety_cp", "crafty_kings_comp_cp"]) w.writeheader() w.writerows(results) print(f"Wrote {len(results)} rows to {OUT_CSV}", file=sys.stderr) if __name__ == "__main__": main()