#!/usr/bin/env python3 """ Bake a tuned .dna file into eval.c as the new hardcoded baseline. The .dna format (see ExportEvalDNA/ImportEvalDNA in eval.c) is POSITIONAL: one line per entry in g_EvalDNA[], in that exact order, each line a flat comma-separated list matching that entry's declared size (DNA_VAR=1, DNA_ARRAY(x)=ARRAY_LENGTH(x), DNA_MATRIX(x)=ARRAY_LENGTH(x)*ARRAY_LENGTH(x[0])). There are no names in the file -- this script recovers the name/order from the g_EvalDNA[] initializer in eval.c itself, so it only works against the exact eval.c revision the .dna was tuned against. If arrays were added/reordered/resized since, per-entry count checks below will catch a misalignment and abort rather than silently writing values into the wrong array. This does NOT touch formatting/whitespace/line-breaks in eval.c's board-shaped grids -- it walks each array's existing initializer, replaces only the numeric literals in place (in textual order), and leaves everything else (comments, 8x8 layout) untouched, so `git diff` on a bake-in only shows the numbers that changed. """ import argparse import re import sys DNA_ENTRY_RE = re.compile(r'DNA_(VAR|ARRAY|MATRIX)\((\w+)\)') INT_RE = re.compile(r'-?\d+') # Declarations seen for DNA-tracked values are `static SCORE x = ...`, # `static ULONG x[...] = ...`, or (RACER_WINS_RACE) `SCORE x = ...` # with no `static` at all -- match any of these. DECL_RE_TMPL = r'(?:static\s+)?(?:SCORE|ULONG)\s+{name}\s*(\[[^=;]*\])?\s*=\s*' def find_g_eval_dna_order(src): m = re.search(r'static DNA_BASE_SIZE g_EvalDNA\[\]\s*=\s*\{(.*?)\};', src, re.S) if not m: sys.exit("could not find g_EvalDNA[] initializer in eval.c") return [(kind, name) for kind, name in DNA_ENTRY_RE.findall(m.group(1))] def mask_comments(text): """Blank out //... and /*...*/ comment bodies, preserving length/newlines so character offsets into the masked text stay valid in the original.""" out = list(text) i = 0 n = len(text) while i < n: if text[i:i+2] == "//": j = text.find("\n", i) j = n if j == -1 else j for k in range(i, j): out[k] = " " i = j elif text[i:i+2] == "/*": j = text.find("*/", i + 2) j = n if j == -1 else j + 2 for k in range(i, j): if out[k] != "\n": out[k] = " " i = j else: i += 1 return "".join(out) def find_declaration_span(src, name): """Return (value_start, value_end, is_scalar) for the `... name ... = ...;` declaration.""" m = re.search(DECL_RE_TMPL.format(name=re.escape(name)), src) if not m: sys.exit(f"could not find declaration of {name} in eval.c") value_start = m.end() if src[value_start:].lstrip().startswith("{"): # array/matrix: find the matching closing brace by depth counting i = src.index("{", value_start) depth = 0 j = i while True: if src[j] == "{": depth += 1 elif src[j] == "}": depth -= 1 if depth == 0: break j += 1 semi = src.index(";", j) return i, semi, False else: semi = src.index(";", value_start) return value_start, semi, True def replace_ints(text, new_values): """Replace only the numeric literals that are live code (not inside comments) with new_values, in order. Returns (new_text, matched, remaining).""" masked = mask_comments(text) matches = list(INT_RE.finditer(masked)) if len(matches) != len(new_values): return text, len(matches), len(new_values) - len(matches) out = [] pos = 0 for m, val in zip(matches, new_values): out.append(text[pos:m.start()]) out.append(str(val)) pos = m.end() out.append(text[pos:]) return "".join(out), len(matches), 0 def bake(eval_c_path, dna_path, out_path): src = open(eval_c_path).read() order = find_g_eval_dna_order(src) dna_lines = [l for l in open(dna_path).read().splitlines() if l.strip()] if len(dna_lines) != len(order): sys.exit(f"DNA file has {len(dna_lines)} lines but g_EvalDNA[] has " f"{len(order)} entries -- eval.c and the .dna file don't " f"match (different revision?). Aborting.") # Apply edits back-to-front by source position so earlier offsets # stay valid as we splice. edits = [] for (kind, name), line in zip(order, dna_lines): values = [int(v) for v in line.split(",") if v.strip() != ""] start, end, is_scalar = find_declaration_span(src, name) segment = src[start:end] new_segment, matched, remaining = replace_ints(segment, values) if remaining != 0 or matched != len(values): sys.exit(f"{name}: declaration has {matched} numeric literals " f"but .dna line has {len(values)} values -- aborting " f"(likely eval.c/.dna mismatch).") edits.append((start, end, new_segment, name)) edits.sort(key=lambda e: e[0], reverse=True) for start, end, new_segment, name in edits: src = src[:start] + new_segment + src[end:] with open(out_path, "w") as f: f.write(src) print(f"baked {len(order)} entries from {dna_path} into {out_path}") if __name__ == "__main__": ap = argparse.ArgumentParser() ap.add_argument("dna_file") ap.add_argument("--eval-c", default="../eval.c") ap.add_argument("--out", default=None, help="defaults to overwriting --eval-c in place") args = ap.parse_args() bake(args.eval_c, args.dna_file, args.out or args.eval_c)