#!/usr/bin/env python3 # -*- coding: utf-8 -*- """杠杆网格扫描驱动——临时批内工具,批末评估去留。 用法:python tools/t3_lever_scan.py [--n 100] [--seed0 73000] 流程:对 worktree 数据表打补丁 → 跑 adventure_monte_carlo.gd(N/策略,unique 逐局独立种子) → stdout RUNSUM 解析成逐关条件通关率 → git checkout 还原被改表(无论成败)。 补丁全部按锚文本定位(display_name/variant_id 上下文),打不上即 FAIL 不静默。 """ import json import re import subprocess import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent GODOT = r"D:\code\godot\Godot_v4.7.2-stable_win64_console.exe" VAR = ROOT / "game" / "data" / "tables" / "variation.tres" ENEMY = ROOT / "game" / "data" / "tables" / "enemy.tres" # 补丁集:config 名 → [(文件, 锚正则, 替换模板, 说明)] PATCHES = { "P1_magma1": [(VAR, r'("variant_id": "magma"[\s\S]{0,200}?"effect_value": )2', r'\g<1>1', "岩浆 2→1")], "P2_plb0": [(VAR, r"(per_level_bonus = )2", r"\g<1>0", "per_level_bonus 2→0")], "P3_drider100": [(ENEMY, r'("enemy_id": "drider"[\s\S]{0,400}?"hp": )130', r'\g<1>100', "L2 Boss 卓尔 130→100")], "P4_thorn2": [(VAR, r'("variant_id": "thorn"[\s\S]{0,200}?"effect_value": )1', r'\g<1>2', "荆棘 1→2")], "P5_chief86": [(ENEMY, r'("enemy_id": "ogre_chief"[\s\S]{0,400}?"hp": )72', r'\g<1>86', "L1 Boss 酋长 72→86")], "P7_percent4": [(VAR, r"(percent_of_cells = )5", r"\g<1>4", "percent_of_cells 5→4")], "P6_L1swarm": [ (ENEMY, r'("display_name": "地精一伙"[\s\S]{0,200}?"unit_count_max": )3', r'\g<1>4', "L1 d1 一伙 max 3→4"), (ENEMY, r'("display_name": "大地精"[\s\S]{0,200}?"unit_count_max": )3', r'\g<1>4', "L1 d3 大地精 max 3→4"), ], } def apply_patches(name): if name == "B0_none": print("[scan] 补丁生效:B0_none(零补丁配对基线)") return True edits = PATCHES.get(name) if edits is None: # 组合:P1+P2+P3 形如 "C_magma+plb+boss" parts = [p for p in name.split("+") if p in PATCHES] if not parts: print(f"[scan] 未知 config:{name}") return False edits = [e for p in parts for e in PATCHES[p]] touched = {} try: for path, pat, rep, desc in edits: txt = path.read_text(encoding="utf-8") new, n = re.subn(pat, rep, txt) if n != 1: print(f"[scan] FAIL 补丁锚不唯一(命中 {n}):{desc}") return False if path not in touched: touched[path] = txt path.write_text(new, encoding="utf-8") print(f"[scan] 补丁生效:{desc}") return True finally: pass # 还原统一由 restore() 做——调用方跑完再调 def restore(): for f in (VAR, ENEMY): subprocess.run(["git", "checkout", "--", str(f.relative_to(ROOT))], cwd=ROOT, capture_output=True, text=True) print("[scan] 表已还原(git checkout)") def run_mc(n, seed0): cmd = [GODOT, "--headless", "--path", str(ROOT / "game"), "--script", "res://sim/adventure_monte_carlo.gd", "--", str(n), str(seed0), "both", "min"] proc = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=3600) rows = {"normal": [], "perfect": []} for line in (proc.stdout or "").splitlines(): if line.startswith("RUNSUM "): r = json.loads(line[7:]) rows[r["policy"]].append(r) return rows def readings(rows): out = {} for pol, rs in rows.items(): n = len(rs) if n == 0: out[pol] = None continue d = {"n": n} for k in (1, 2, 3): reached = sum(1 for r in rs if r["levels_cleared"] >= k - 1) passed = sum(1 for r in rs if r["levels_cleared"] >= k) d[f"L{k}"] = (passed, reached, round(100 * passed / reached, 1) if reached else None) out[pol] = d return out def main(): args = sys.argv[1:] if not args: print(__doc__) return 2 name = args[0] n = 100 seed0 = 73000 for i, a in enumerate(args): if a == "--n" and i + 1 < len(args): n = int(args[i + 1]) if a == "--seed0" and i + 1 < len(args): seed0 = int(args[i + 1]) print(f"[scan] === config {name} · N={n}/策略 · seed0={seed0} ===") if not apply_patches(name): restore() return 1 try: rows = run_mc(n, seed0) finally: restore() rd = readings(rows) for pol in ("normal", "perfect"): d = rd.get(pol) if not d: print(f"[scan] {pol}: 无 RUNSUM(采集异常)") continue cells = " · ".join( f"L{k} {d[f'L{k}'][2]}% ({d[f'L{k}'][0]}/{d[f'L{k}'][1]})" for k in (1, 2, 3)) print(f"[scan] {pol}: {cells}") print("[scan] RESULTJSON " + json.dumps({"config": name, "n": n, "seed0": seed0, "readings": rd}, ensure_ascii=False)) return 0 if __name__ == "__main__": sys.exit(main())