summaryrefslogtreecommitdiff
path: root/src/eval_tune/dna_diff.py
blob: 1b4428082430965fcf338792c6fcaf8261a79ffa (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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
#!/usr/bin/env python3
"""Human-readable diff between a baseline DNA dump and a tuned .dna
file: names each of g_EvalDNA's arrays and, for the 128-cell board-
shaped location tables, renders an actual 8x8 grid diff instead of a
wall of raw numbers.

2026-09-15: DNA_NAMES used to be a hand-maintained list "exact order
from eval.c:604-659" -- found to have drifted badly (wrong from index
0: eval.c's g_EvalDNA[] no longer even contains TRADE_PIECES/
DONT_TRADE_PAWNS, the list's first two entries, and roughly a dozen
other names had also renamed/moved/vanished over past sessions without
this list being updated). Every consumer of this list (this script,
dna_trend.py, tune_eval_dna.py's --freeze) had been silently mislabeling
or misdirecting against the wrong array for a while -- plausibly the
real explanation for at least some past "Texel tuning produced
nonsensical results" reports, since a diff/freeze targeting the wrong
array by name looks exactly like that from the output alone.

Fixed by deriving this list from eval.c directly at import time instead
of hand-maintaining a parallel copy that can silently drift again. Only
falls back to the hardcoded snapshot below (kept only as a last resort
for running this script somewhere eval.c isn't reachable) with a loud
warning -- never silently.
"""
import re
import sys
from pathlib import Path

# Last-resort fallback only -- see module docstring. Regenerate by
# running _derive_dna_names() with eval.c present and pasting the result
# here if this ever needs to be updated by hand again.
_FALLBACK_DNA_NAMES = [
    "REDUCED_MATERIAL_DOWN_SCALER", "CASTLE_AND_DEVELOPMENT_SCALER",
    "REDUCED_MATERIAL_UP_SCALER", "PASSER_MATERIAL_UP_SCALER",
    "PAWN_CENTRALITY_BONUS", "BACKWARD_SHIELDED_BY_LOCATION",
    "BACKWARD_EXPOSED_BY_LOCATION", "DOUBLED_PAWN_PENALTY_BY_COUNT",
    "ISOLATED_PAWN_BY_PAWNFILE", "ISOLATED_EXPOSED_PAWN",
    "ISOLATED_DOUBLED_PAWN", "PASSER_BY_RANK", "CANDIDATE_PASSER_BY_RANK",
    "CONNECTED_PASSERS_BY_RANK", "SUPPORTED_PASSER_BY_RANK",
    "KING_SUPPORTING_OWN_PASSER_BY_RANK", "OUTSIDE_PASSER_BY_DISTANCE",
    "PASSER_BONUS_AS_MATERIAL_COMES_OFF", "RACER_WINS_RACE",
    "UNDEVELOPED_MINORS_IN_OPENING", "BISHOP_OVER_KNIGHT_IN_ENDGAME",
    "BISHOP_PAIR_BONUS", "STATIONARY_PAWN_ON_BISHOP_COLOR",
    "TRANSIENT_PAWN_ON_BISHOP_COLOR", "BISHOP_MOBILITY_BY_SQUARES",
    "BISHOP_UNASSAILABLE_BY_DIST_FROM_EKING", "KNIGHT_CENTRALITY_BONUS",
    "KNIGHT_KING_TROPISM_BONUS", "KNIGHT_UNASSAILABLE_BY_DIST_FROM_EKING",
    "KNIGHT_ON_INTERESTING_SQUARE_BY_RANK", "KNIGHT_MOBILITY_BY_COUNT",
    "KNIGHT_WITH_N_PAWNS_SUPPORTING", "KNIGHT_IN_CLOSED_POSITION",
    "ROOK_ON_FULL_OPEN", "ROOK_ON_HALF_OPEN_WITH_ENEMY",
    "ROOK_ON_HALF_OPEN_WITH_FRIEND", "ROOK_WITH_ENEMY_PASSER",
    "ROOK_WITH_FRIEND_PASSER", "ROOK_BEHIND_PASSER_BY_PASSER_RANK",
    "ROOK_LEADS_PASSER_BY_PASSER_RANK", "KING_TRAPPING_ROOK",
    "ROOK_VALUE_AS_PAWNS_COME_OFF", "ROOK_CONNECTED_VERT",
    "ROOK_CONNECTED_HORIZ", "ROOK_MOBILITY_BY_SQUARES",
    "QUEEN_MOBILITY_BY_SQUARES", "QUEEN_KING_TROPISM",
    "KING_INITIAL_COUNTER_BY_LOCATION", "KING_EVAL_INITIAL_LOCATION",
    "KING_TO_CENTER", "KING_SAFETY_BY_COUNTER",
    "KING_QUEEN_PROXIMITY_DANGER", "KING_MISSING_ONE_CASTLE_OPTION",
    "KING_BACK_FLIGHT_BONUS", "KING_RING_ATTACK_MINOR",
    "KING_RING_ATTACK_ROOK", "KING_RING_ATTACK_PAWN",
    "KING_RING_UNDEFENDED", "KING_RING_XRAY_BONUS",
    "KING_RING_FRONT_FLIGHT_PENALTY", "KING_INITIAL_LOCATION_SCALE",
    "KING_ATTACK_PATTERN_SCALE", "KING_DANGER_ACCELERATOR",
]


def _derive_dna_names(eval_c_path="../eval.c"):
    """Parse g_EvalDNA[]'s initializer directly out of eval.c, in
    order -- the only way this list can't silently drift out of sync
    with the real array again."""
    text = Path(eval_c_path).read_text()
    m = re.search(r'static DNA_BASE_SIZE g_EvalDNA\[\] = \{(.*?)\n\};',
                  text, re.DOTALL)
    if not m:
        return None
    return re.findall(r'DNA_(?:ARRAY|MATRIX|VAR)\(([A-Za-z0-9_]+)\)', m.group(1))


DNA_NAMES = _derive_dna_names()
if DNA_NAMES is None:
    print("WARNING: could not find/parse g_EvalDNA[] in ../eval.c -- "
          "falling back to a hardcoded DNA_NAMES snapshot that WILL "
          "silently drift out of date. Run this from eval_tune/ with "
          "the real eval.c reachable if at all possible.", file=sys.stderr)
    DNA_NAMES = _FALLBACK_DNA_NAMES

# Arrays laid out as a 128-cell "0x88-style" board: 8 files + 8 padding
# zeros per rank, 8 ranks (see eval.c's literal formatting -- each row
# of the C initializer is one rank, padded to 16 slots). Rendered
# top-to-bottom as rank 8 -> rank 1 like the board is shown elsewhere.
BOARD128_NAMES = {
    "PAWN_CENTRALITY_BONUS", "BACKWARD_SHIELDED_BY_LOCATION",
    "BACKWARD_EXPOSED_BY_LOCATION", "STATIONARY_PAWN_ON_BISHOP_COLOR",
    "TRANSIENT_PAWN_ON_BISHOP_COLOR", "KNIGHT_CENTRALITY_BONUS",
    "KING_TO_CENTER",
}
# KING_INITIAL_COUNTER_BY_LOCATION is [2][128] -- one 128-board per color.
BOARD128_PAIR_NAMES = {"KING_INITIAL_COUNTER_BY_LOCATION"}

FILES = "ABCDEFGH"


def read_dna_file(path):
    rows = []
    with open(path) as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            rows.append([int(x) for x in line.split(",")])
    return rows


def diff_board128(old_row, new_row):
    lines = []
    for rank8_from_top in range(8):
        old_cells = old_row[rank8_from_top * 16: rank8_from_top * 16 + 8]
        new_cells = new_row[rank8_from_top * 16: rank8_from_top * 16 + 8]
        rank_label = 8 - rank8_from_top
        cell_strs = []
        for o, n in zip(old_cells, new_cells):
            if o == n:
                cell_strs.append(f"{n:4d}")
            else:
                cell_strs.append(f"{o:+d}->{n:+d}")
        lines.append(f"  {rank_label} " + " ".join(f"{s:>9s}" for s in cell_strs))
    lines.append("       " + "     ".join(f"{f}" for f in FILES))
    return lines


def report(baseline_rows, tuned_rows, names=DNA_NAMES, only_changed=True):
    assert len(baseline_rows) == len(tuned_rows) == len(names), (
        f"row count mismatch: baseline={len(baseline_rows)} "
        f"tuned={len(tuned_rows)} names={len(names)}"
    )
    any_change = False
    for name, old_row, new_row in zip(names, baseline_rows, tuned_rows):
        if old_row == new_row:
            if not only_changed:
                print(f"{name}: unchanged")
            continue
        any_change = True
        print(f"\n=== {name} ===")
        if name in BOARD128_PAIR_NAMES:
            half = len(old_row) // 2
            for color, lo, hi in (("BLACK", 0, half), ("WHITE", half, len(old_row))):
                if old_row[lo:hi] != new_row[lo:hi]:
                    print(f"  -- {color} --")
                    for line in diff_board128(old_row[lo:hi], new_row[lo:hi]):
                        print(" ", line)
        elif name in BOARD128_NAMES and len(old_row) == 128:
            for line in diff_board128(old_row, new_row):
                print(" ", line)
        else:
            diffs = [
                (i, o, n) for i, (o, n) in enumerate(zip(old_row, new_row)) if o != n
            ]
            print(f"  old: {old_row}")
            print(f"  new: {new_row}")
            print(f"  changed cells: {diffs}")
    if not any_change:
        print("No differences -- tuned DNA is identical to baseline.")


if __name__ == "__main__":
    baseline_path, tuned_path = sys.argv[1], sys.argv[2]
    report(read_dna_file(baseline_path), read_dna_file(tuned_path))