summaryrefslogtreecommitdiff
path: root/src/eval_tune/compare_king_safety_crafty.py
blob: a018fc7364fdde553a351ea13cf10d0bd4049754 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
#!/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 <fen>;
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()