Files
gplan-2/game/smoke/verifier_batch.gd
T
DKChan 96fc61c701 feat(tree): 迭代21天赋树实装批 T1-T6 主体——12节点换代+六键接线+营地四柜台(2026-10-12 U0)
T1 schema 换代:MetaTreeTable FORK_GROUPS 4→3(fg_mastery 退役)/RULE_KEYS 2→6(sense/economy 四键扩签)/
  fork 单行双态(effects_a/b+name_a/b、每组恰一校验)/row 字段退役(行门槛主语=节点等级 level_gate)/
  requires 只留点亮前置(R1 裁撤案执行);meta_tree.tres 33→12 行重排(脊柱2/锻造2含彩蛋/钥石2/同调6,
  att_socket1 改根节点无前置,easter_ley 摘 atk/armor_class 分量=契约9)
T2 购买与彩蛋面:tree_can_buy easter 拒购+行门槛换轴;tree_buy_fork=解锁默认A;tree_toggle_fork 零币切态;
  tree_rule_keys 按激活态 effects_a/b 投影;_check_easter_egg 特化判定(两线拉满+socket全解锁);营地树行板双态钮
T3 六键接线:EVENTS+flag_missed/FACTS+battle_index(L2 扩签两处);rebuild 六键合成订阅
  (posture 窗=battle_index&&round_index 双闸/sense 两组=counter 闸+记账双行 domain:adventure 限次2次 provisional);
  level_runner battle_index 注入(visited battle 型计数);_judge_flag 去重闸后 flag_missed 发射(契约12);
  聚=_roll_gp_drop 入账前×1.5 provisional(escrow 不乘);散=_spend_discounted ×80%(current_price/pool_price
  双计价点,刷新价不折=§11②定义边界);_start_current_level 限次按关清零(_reset_tree_limited_counters 可测口)
T4 注入换代:hero_override=树淬炼flat+FORGE_ROUTE_BONUS预留0(命中不注入=契约6);Attunement.slots 判定源切
  树点亮数(谓词表退役);tree_buy(ks_refresh) 同账路由 buy_attune_refresh(V15 双入口)
T5 旧档废弃与删除:save_manager tree_struct=2 结构版本标记+_normalize 白名单重建+树域废弃重置(禁迁移折算);
  migrate_tree/buy_forge/forge_flat/forge_level/forge_price/meta_forge_row/forge_products 行族/
  attunement_growth 表+访问器/生涯页三投影/career forge_total 全删;treasure_item_table UNLOCK_COUNTER_KEYS 内联;
  verifier_batch 花费策略换代树节点;MC slots 画像常量3
T6 营地统一:四柜台并排横滚(树淬炼主界面/锻造工坊钥匙门+零交互占位面板/秘宝匣钥匙门+内容迁入/图鉴常开);
  暂停层锻造段连带删;camp data forge 投影删
存量红修复:_flush_pending_reveals 三处空 source_class 补 fx_reveal+类级豁免(a57489e9 落地件,冒烟51行)
测试随批:smoke 树段全重写(V1/V2/V3/V8/V9/V16)+_smoke_tree_rules V10 行为段(窗/限次/守侧五场景编排rig/聚散
  同种子对拍)+单元 tree_rules_v11_test(六键合成行闭集校验)+run_all 22 套;ui_probe/ui_click_probe 换代树点击路径
轻闸全绿:冒烟886/0+run_all 22套+probe --click 全绿
2026-10-12 07:54:38 +08:00

214 lines
9.9 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
# Date: 2026-09-22(数值验证工具批——判定端 tools/balance_verifier.py 的数据源,本文件零判定逻辑)
# 2026-09-23 单关直入口口径退役(用户拍板):原 level 段(直入口单关基线——满血零上下文进关,
# 与真实冒险的条件通关率脱节,曾误导曲线调参)整段移除;逐关难度读数由 adventure 段行内
# levels_cleared/stopped_at 派生(判定端 analyze_adventure_levels),零补采。
# 2026-09-23 换算率标定批:① adventure 段增 inject 注入档(永久强化 flat OFAT——标定换算率表的
# 边际读数:每档与白板同种子流配对,逐关条件通关率差 = 该档的带平移量实测);
# ② campaign 段增 armor_first 花费序 + 行带 spend 标签(多轨迹合并采集按序分组)。
# 两段一次采集,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://smoke/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
# 迭代 21 T5 换代——属性锻造柜台退役后花费策略主语=树脊柱两节点(钥石单币;攻/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://smoke/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.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.keystone_balance()
row["tree"] = {"sp_hp": meta.save.tree_level("sp_hp"), "sp_hit": meta.save.tree_level("sp_hit")}
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.tree_level("sp_hp"), meta.save.tree_level("sp_hit"),
meta.keystone_balance()])
_remove_save(save_path) # 段尾清理(与 smoke 存档纪律同源——不遗留隔离档)
# 花费策略(贪心——按优先序反复买当前可负担的最高优先项;树行门槛随买随涨自然外溢到次节点
# ——迭代 21 T5 换代:购买原语 buy_forge→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.keystone_balance() >= price and meta.tree_buy(nid):
out.append({"node_id": nid, "level": meta.save.tree_level(nid), "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)