summaryrefslogtreecommitdiff
path: root/src/eval_tune/bake_dna.py
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-08-29 23:17:31 -0700
committerScott Gasch <[email protected]>2026-08-29 23:17:31 -0700
commitfa210fb9645afd27e2991b3ee8139be231d0b5ec (patch)
tree1ee7be4d51d3d2bc4ffa2c512db5270cea6b2af2 /src/eval_tune/bake_dna.py
parentf2613dfabb5ef3a7a08a73e74c83467dfad9ddcc (diff)
Various utils.
Diffstat (limited to 'src/eval_tune/bake_dna.py')
-rwxr-xr-xsrc/eval_tune/bake_dna.py151
1 files changed, 151 insertions, 0 deletions
diff --git a/src/eval_tune/bake_dna.py b/src/eval_tune/bake_dna.py
new file mode 100755
index 0000000..9d03644
--- /dev/null
+++ b/src/eval_tune/bake_dna.py
@@ -0,0 +1,151 @@
+#!/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)