diff options
Diffstat (limited to 'src/eval_tune/bake_dna.py')
| -rwxr-xr-x | src/eval_tune/bake_dna.py | 151 |
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) |
