138 lines
5.2 KiB
Python
138 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""杠杆网格扫描驱动——临时批内工具,批末评估去留。
|
||
|
||
用法:python tools/t3_lever_scan.py <config名> [--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())
|