Files
gplan-2/tools/t3_lever_scan.py
T

138 lines
5.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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())