# 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.run.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.crossrun.meta_tree.row_by_id(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)