Files
gplan-2/game/sim/verifier_batch.gd
T

209 lines
9.3 KiB
GDScript
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.
# VERTICAL SLICE - NOT FOR PRODUCTION
# 判定端 tools/balance_verifier.py 的数据源,本文件零判定逻辑
# 两段一次采集,stdout 以「VJSON 」前缀行出机器读 JSON(其余行为人读进度/摘要):
# adventure 段 全冒险连探基线(run_adventure 零永久强化)× 双画像 × na——止步分布、败退节点
# (摘要 defeat 明细 + 末场战斗敌方名)与逐关条件关卡完成率派生源;inject 档行带
# inject_tag/override(flat 三键)
# campaign 段 营地循环战役 × 双画像 × k 局——SaveManager 隔离档 + MetaProgression 真实账本:
# 逐局 hero_override() 注入 → 冒险 → settle_adventure 入账 → 花费策略买锻造,
# 回答「累计几次冒险成功后下一关可过」(元进度换算率标定的模拟读数源)
# 运行:Godot_console.exe --headless --path . --script res://sim/verifier_batch.gd -- [k=v ...]
# na=40(基线冒险局数) k=40(战役局数) seed0=8000
# only=adventure,campaign(缺省全段) spend=hp_first|atk_first|armor_first(锻造花费优先序)
# inject=max_hp:3,atk:1(冒险段 flat 注入档——三键 {max_hp, atk, armor_class},缺省全零白板;
# 换算率标定用:每档与白板同种子流配对,逐关条件关卡完成率差 = 该档的带平移量实测)
# 种子纪律:unique 逐局独立(掷点测量修复批 A 案——seed0 + 全局游标,每掷点恰消费一次)。
# 同 seed0 跨档复用 = 配对读数(同序种子流——分析侧按种子配对消方差)。
# 日志合规:全段共享 playtest_logger 会话(与真人试玩同日志),分析工具可直接消费。
# 存档红线:战役档用 user://verifier_campaign_<画像>.json 隔离路径,段首清陈旧段尾清理——
# 绝不碰 user://save.json(与 smoke 存档隔离纪律同源)。
extends SceneTree
# 花费策略主语=树脊柱两节点(钥石单币;攻/AC 无树来源
# 〔契约 9——唯一入口=锻造工坊路线,未实装〕)
const SPEND_ORDERS := {
"hp_first": ["sp_hp", "sp_hit"],
"atk_first": ["sp_hit", "sp_hp"],
"armor_first": ["sp_hp", "sp_hit"],
}
const OVERRIDE_KEYS := ["atk", "max_hp", "armor_class"] # 注入契约三键(同 AdventureRunner 白板形)
var _cursor := 0 # unique 种子全局游标(三段连续消费,段间不重叠)
func _initialize() -> void:
var opt := _parse_args(OS.get_cmdline_user_args())
var db = load("res://core/config_db.gd").new()
if not db.load_tables("res://data/tables", false):
print("表加载失败")
quit(1)
return
var logger = load("res://presentation/playtest_logger.gd").new()
logger.ap_per_round = int(db.action_economy.ap_per_round)
var log_path: String = logger.start_session(opt.seed0)
print("模拟日志: %s" % log_path)
var autoplay = load("res://sim/level_autoplay.gd")
var t0 := Time.get_ticks_msec()
var counts := {"adventure": 0, "campaign": 0}
if "adventure" in opt.only:
_seg_adventure(db, autoplay, logger, opt, counts)
if "campaign" in opt.only:
_seg_campaign(db, autoplay, logger, opt, counts)
logger.close_session()
print("数值验证采集完成:adventure %d 局 · campaign %d 局,耗时 %.1f 秒"
% [counts.adventure, counts.campaign, float(Time.get_ticks_msec() - t0) / 1000.0])
quit(0)
func _parse_args(args: PackedStringArray) -> Dictionary:
var opt := {"na": 40, "k": 40, "seed0": 8000, "only": "adventure,campaign",
"spend": "hp_first", "inject": ""}
for a in args:
var kv := String(a).split("=", true, 1)
if kv.size() != 2:
continue
var key := String(kv[0])
if opt.has(key):
if key in ["only", "spend", "inject"]:
opt[key] = String(kv[1])
else:
opt[key] = int(kv[1])
var segs: Array = []
for s in String(opt.only).split(","):
if s in ["adventure", "campaign"]:
segs.append(s)
opt["only"] = segs
if not SPEND_ORDERS.has(String(opt.spend)):
opt["spend"] = "hp_first"
opt["override"] = _parse_inject(String(opt.inject))
return opt
# inject 档解析:"max_hp:3,atk:1" → 三键 flat 字典(缺键 = 0、负值 clamp 0、未知键丢弃)
func _parse_inject(raw: String) -> Dictionary:
var out := {"atk": 0, "max_hp": 0, "armor_class": 0}
for part in raw.split(","):
var kv := String(part).split(":", true, 1)
if kv.size() == 2 and String(kv[0]) in OVERRIDE_KEYS:
out[String(kv[0])] = maxi(0, int(kv[1]))
return out
# 注入档人读标签(白板 = "白板";行键 inject_tag 供判定端分档聚合)
func _inject_tag(ov: Dictionary) -> String:
var parts: Array = []
for k in OVERRIDE_KEYS:
if int(ov.get(k, 0)) != 0:
parts.append("%s+%d" % [k, int(ov[k])])
return "白板" if parts.is_empty() else ",".join(parts)
func _next_seed(seed0: int) -> int:
var s: int = seed0 + _cursor
_cursor += 1
return s
func _emit(row: Dictionary) -> void:
print("VJSON " + JSON.stringify(row))
# ---- adventure 段:全冒险连探基线(零永久强化——止步分布/败退节点归因/逐关条件关卡完成率派生源);
# inject 档 = 同种子流配对的 OFAT 边际读数(行带 inject_tag/override flat 三键) ----
func _seg_adventure(db, autoplay, logger, opt: Dictionary, counts: Dictionary) -> void:
var ov: Dictionary = opt.override
var tag := _inject_tag(ov)
for policy in ["normal", "perfect"]:
for i in range(int(opt.na)):
var seed_val := _next_seed(int(opt.seed0))
var st: Dictionary = autoplay.run_adventure(db, seed_val, 0, 0, false, logger, policy, "min", ov)
var row := _adventure_row(st, policy, seed_val)
row["inject_tag"] = tag
row["override"] = ov.duplicate()
_emit(row)
counts.adventure += 1
print("进度 adventure %s %d/%d(%s)" % [policy, int(opt.na), int(opt.na), tag])
# 冒险局共用取数行(adventure/campaign 两段同构——summary 缺席 = 编排异常局,原样带 result 供判定端计异常)
func _adventure_row(st: Dictionary, policy: String, seed_val: int) -> Dictionary:
var sum: Dictionary = st.summary
var row := {
"seg": "adventure", "policy": policy, "seed": seed_val,
"result": String(st.result),
"levels_cleared": int(sum.get("levels_cleared", -1)),
"stopped_at": int(sum.get("stopped_at", -1)),
"keystone": int(sum.get("keystone_total", -1)),
"defeat": sum.get("defeat", {}),
"last_enemies": st.last_battle_enemies,
}
if not row.defeat.is_empty() and not st.last_battle_enemies.is_empty():
row.defeat["enemies"] = st.last_battle_enemies
return row
# ---- campaign 段:营地循环战役(隔离档真实账本——逐局注入/收束/购买全走正式侧路径) ----
func _seg_campaign(db, autoplay, logger, opt: Dictionary, counts: Dictionary) -> void:
for policy in ["normal", "perfect"]:
var save_path := "user://verifier_campaign_%s.json" % policy
_remove_save(save_path) # 段首清陈旧(新档 = 白板存档)
var save = load("res://core/save_manager.gd").new(save_path)
var meta = load("res://core/meta_progression.gd").new(db, save)
for r in range(int(opt.k)):
var seed_val := _next_seed(int(opt.seed0))
var ov: Dictionary = meta.hero_override()
var keystone_before: int = meta.meta_state.keystone_balance()
var st: Dictionary = autoplay.run_adventure(db, seed_val, 0, 0, false, logger, policy, "min", ov)
var row := _adventure_row(st, policy, seed_val)
row["seg"] = "campaign"
row["run_no"] = r + 1
row["spend"] = String(opt.spend)
row["override"] = ov
row["keystone_before"] = keystone_before
if st.summary.is_empty():
# 冒险未正常结算(timeout/advance_failed——编排异常零入账零购买,判定端计异常)
row["anomaly"] = true
_emit(row)
counts.campaign += 1
continue
var settle: Dictionary = meta.settle_adventure(st.summary)
row["keystone_income"] = int(settle.total)
row["purchases"] = _spend(meta, String(opt.spend))
row["keystone_after"] = meta.meta_state.keystone_balance()
row["tree"] = {"sp_hp": meta.save.meta_state.tree_state().nodes.get("sp_hp", 0), "sp_hit": meta.save.meta_state.tree_state().nodes.get("sp_hit", 0)}
row["new_unlocks"] = settle.new_unlocks
_emit(row)
counts.campaign += 1
if (r + 1) % 10 == 0:
print("进度 campaign %s %d/%d(树 血%d/命中%d · 钥石 %d)" % [policy, r + 1, int(opt.k),
meta.save.meta_state.tree_state().nodes.get("sp_hp", 0), meta.save.meta_state.tree_state().nodes.get("sp_hit", 0),
meta.meta_state.keystone_balance()])
_remove_save(save_path) # 段尾清理(与 smoke 存档纪律同源——不遗留隔离档)
# 花费策略(贪心——按优先序反复买当前可负担的最高优先项;树行门槛随买随涨自然外溢到次节点
# ——购买原语 tree_buy,行门槛=节点等级轴)
func _spend(meta, policy_name: String) -> Array:
var out: Array = []
var order: Array = SPEND_ORDERS.get(policy_name, SPEND_ORDERS["hp_first"])
var guard := 200
while guard > 0:
guard -= 1
var bought := false
for nid in order:
var price: int = int(meta.db.tree_node(nid).get("price", 0))
if price > 0 and meta.meta_state.keystone_balance() >= price and meta.tree_buy(nid):
out.append({"node_id": nid, "level": meta.save.meta_state.tree_state().nodes.get(nid, 0), "price": price})
bought = true
break
if not bought:
break
return out
func _remove_save(path: String) -> void:
if FileAccess.file_exists(path):
DirAccess.remove_absolute(path)