summaryrefslogtreecommitdiff
path: root/src/eval_tune/dna_diff.py
blob: ecdd6bbd055ffcfc65ea285c528924dcea886f5a (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
#!/usr/bin/env python3
"""Human-readable diff between a baseline DNA dump and a tuned .dna
file: names each of g_EvalDNA's 56 arrays (order taken directly from
eval.c's g_EvalDNA[] table) and, for the 128-cell board-shaped
location tables, renders an actual 8x8 grid diff instead of a wall of
raw numbers."""
import sys

# Exact order from eval.c:604-659 (g_EvalDNA[] initializer).
DNA_NAMES = [
    "TRADE_PIECES", "DONT_TRADE_PAWNS", "REDUCED_MATERIAL_DOWN_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_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", "OUTSIDE_PASSER_BY_DISTANCE",
    "PASSER_BONUS_AS_MATERIAL_COMES_OFF", "RACER_WINS_RACE",
    "UNDEVELOPED_MINORS_IN_OPENING", "BISHOP_OVER_KNIGHT_IN_ENDGAME",
    "BISHOP_PAIR", "STATIONARY_PAWN_ON_BISHOP_COLOR",
    "TRANSIENT_PAWN_ON_BISHOP_COLOR", "BISHOP_MOBILITY_BY_SQUARES",
    "BISHOP_MAX_MOBILITY_IN_A_ROW_BONUS",
    "BISHOP_UNASSAILABLE_BY_DIST_FROM_EKING", "BISHOP_IN_CLOSED_POSITION",
    "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_BY_DIST_FROM_EKING",
    "ROOK_ON_HALF_OPEN_WITH_ENEMY_BY_DIST_FROM_EKING",
    "ROOK_ON_HALF_OPEN_WITH_FRIEND_BY_DIST_FROM_EKING",
    "ROOK_BEHIND_PASSER_BY_PASSER_RANK", "ROOK_LEADS_PASSER_BY_PASSER_RANK",
    "KING_TRAPPING_ROOK", "ROOK_TRAPPING_EKING",
    "ROOK_VALUE_AS_PAWNS_COME_OFF", "ROOK_CONNECTED_VERT",
    "ROOK_CONNECTED_HORIZ", "ROOK_MOBILITY_BY_SQUARES",
    "ROOK_MAX_MOBILITY_IN_A_ROW_BONUS", "QUEEN_MOBILITY_BY_SQUARES",
    "QUEEN_OUT_EARLY", "QUEEN_KING_TROPISM",
    "QUEEN_ATTACKS_SQ_NEXT_TO_KING", "KING_INITIAL_COUNTER_BY_LOCATION",
    "KING_TO_CENTER", "KING_SAFETY_BY_COUNTER",
    "KING_MISSING_ONE_CASTLE_OPTION",
]

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