#!/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))