test(smoke): 元进度实现批 T5——_smoke_meta 三段扩至 27 断言(549 绿)+ ui_probe/ui_click_probe 营地段(隔离存档换注保确定性)

This commit is contained in:
DKChan committed 2026-09-21 15:17:38 +08:00
1 parent 2e419a17a3
commit 816d0b9279
3 files changed
+280 -2

No files matched your search

@@ -3084,6 +3084,177 @@ func _smoke_meta() -> void:
var brow: Dictionary = _db.meta_boss_keystone_row(li) var brow: Dictionary = _db.meta_boss_keystone_row(li)
boss_ok = boss_ok and not brow.is_empty() and int(brow.get("first_kill", 0)) >= int(brow.get("repeat_kill", -1)) boss_ok = boss_ok and not brow.is_empty() and int(brow.get("first_kill", 0)) >= int(brow.get("repeat_kill", -1))
check(boss_ok, "boss_keystone L1–L3 行连续可达且 first_kill ≥ repeat_kill 全行(首杀全量/重复小额)") check(boss_ok, "boss_keystone L1–L3 行连续可达且 first_kill ≥ repeat_kill 全行(首杀全量/重复小额)")
_meta_pricing_and_buy()
_meta_settle_and_workshop()
_meta_injection()
# 冒烟隔离存档名册(段首清陈旧 + 段尾清理——契约:测试不污染 user://save.json 真实档)
const _META_SMOKE_PATHS := ["user://smoke_meta_buy.json", "user://smoke_meta_dry.json",
"user://smoke_meta_settle.json", "user://smoke_meta_workshop.json",
"user://smoke_meta_cap.json", "user://smoke_meta_white.json"]
func _meta_smoke_cleanup() -> void:
for p in _META_SMOKE_PATHS:
for suffix in ["", ".tmp", ".corrupt.bak"]:
if FileAccess.file_exists(p + suffix):
DirAccess.remove_absolute(p + suffix)
func _meta_new_save(path: String):
var sv = load("res://core/save_manager.gd").new(path)
sv.load_from_disk()
return sv
# ---- 元进度 ②几何定价 ③购买(余额充足持久 / 不足拒绝 / attr 出域拒绝)----
func _meta_pricing_and_buy() -> void:
print("--- 元进度·几何定价与购买(隔离档 smoke_meta_buy/dry)---")
_meta_smoke_cleanup()
var sv = _meta_new_save(_META_SMOKE_PATHS[0])
var mp = load("res://core/meta_progression.gd").new(_db, sv)
# ② 几何定价:tier 0/1/2 价格 == 迭代 floor(p × ratio/100) 复算值(tier 0 = base_price)且严格递增
for attr in MetaTable.FORGE_ATTRS:
var row: Dictionary = _db.meta_forge_row(attr)
var base := int(row.get("base_price", 0))
var ratio := int(row.get("price_ratio_pct", 100))
var all_ok := true
var prev := 0
for tier in 3:
sv.set_forge_level(attr, tier)
var expect := base
for _i in tier:
expect = int(floor(float(expect) * float(ratio) / 100.0))
all_ok = all_ok and mp.forge_price(attr) == expect and (tier == 0 or expect > prev)
prev = expect
check(all_ok, "%s 几何定价 tier 0/1/2 = 迭代 floor 复算且严格递增(base %d × ratio %d%%)" % [attr, base, ratio])
sv.set_forge_level("atk", 0)
# ③ 购买成功:级+1、余额−价;buy 内含写盘——reload 后持久
sv.add_keystone(1000)
var bal0: int = sv.keystone_balance()
var lvl0: int = sv.forge_level("atk")
var price0: int = mp.forge_price("atk")
check(mp.buy_forge("atk") and sv.forge_level("atk") == lvl0 + 1 and sv.keystone_balance() == bal0 - price0,
"购买成功(Lv %d→%d、余额 %d−%d)" % [lvl0, lvl0 + 1, bal0, price0])
var sv_r = _meta_new_save(_META_SMOKE_PATHS[0])
check(sv_r.forge_level("atk") == lvl0 + 1 and sv_r.keystone_balance() == bal0 - price0, "购买持久(reload 后级与余额不变)")
# ③ 余额不足 / attr 出域:false 零副作用
var sv_dry = _meta_new_save(_META_SMOKE_PATHS[1])
var mp_dry = load("res://core/meta_progression.gd").new(_db, sv_dry)
check(not mp_dry.buy_forge("atk") and sv_dry.keystone_balance() == 0 and sv_dry.forge_level("atk") == 0,
"余额不足 false 零副作用(0 余额购买拒绝)")
check(not mp_dry.buy_forge("luck") and sv_dry.keystone_balance() == 0, "attr 出域 false 零副作用(luck 不在词表)")
# ---- 元进度 ④注入可见 + 白板成立(验收 4/2/3——程序面;MC 逐位回归归 T7 全量验证)----
func _meta_injection() -> void:
print("--- 元进度·注入可见与白板(隔离档 smoke_meta_white/dry)---")
var sv = _meta_new_save(_META_SMOKE_PATHS[1])
var mp = load("res://core/meta_progression.gd").new(_db, sv)
sv.add_keystone(mp.forge_price("atk"))
check(mp.buy_forge("atk") and int(mp.hero_override().get("atk", -1)) == int(_db.meta_forge_row("atk").get("flat_value", 0)),
"买 atk 1 级 → hero_override().atk == 行 flat(验收4)")
var adr = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(920251))
check(adr.start_adventure(920251, {"hero_override": mp.hero_override()}), "注入参数冒险启动(start_adventure params 通道)")
var r = adr.runner()
var ov: Dictionary = mp.hero_override()
check(int(r.effective_atk()) == int(_db.hero.atk) + int(ov.get("atk", 0)),
"首战 hero_atk = 表值 + 永久强化 flat(begin_battle 同表达式——下一局能用到强化)")
check(int(r.effective_max_hp()) == int(_db.hero.max_hp) + int(ov.get("max_hp", 0))
and int(adr.ledger["hero_hp"]) == int(_db.hero.max_hp) + int(ov.get("max_hp", 0)),
"首战 hero_max_hp 与开局生命账本 = 表值 + 注入档")
# 白板:同种子下 空参数 = 零强化参数 = 纯表值(逐位一致——注入通道默认值漏改在此转红)
var adr_w = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(920251))
check(adr_w.start_adventure(920251), "白板冒险启动(无 params 实参——B1 空参数路径)")
var sv_white = _meta_new_save(_META_SMOKE_PATHS[5])
var adr_z = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(920251))
check(adr_z.start_adventure(920251, {"hero_override": load("res://core/meta_progression.gd").new(_db, sv_white).hero_override()}),
"零强化参数冒险启动(新档 hero_override 全零)")
var rw = adr_w.runner()
var rz = adr_z.runner()
check(int(rw.effective_atk()) == int(_db.hero.atk) and int(rw.effective_max_hp()) == int(_db.hero.max_hp)
and int(rz.effective_atk()) == int(rw.effective_atk()) and int(rz.effective_max_hp()) == int(rw.effective_max_hp())
and int(rz.effective_ac()) == int(rw.effective_ac()),
"白板成立:空参数 = 零强化参数 = 纯表值(atk/max_hp/ac 三轴逐位一致——验收 2/3)")
check(int(rw.effective_ac()) == int(_db.hero.armor_class) and int(r.effective_ac()) == int(_db.hero.armor_class),
"ac 轴:白板恒表值、未换强化时注入零位移")
_meta_smoke_cleanup()
# ---- 元进度 ⑥Boss 两档 ⑦钥石不丢 ⑧工坊解锁 ⑩摘要副本与 cap(合成摘要驱动——settle 纯账面零跑局)----
func _meta_settle_and_workshop() -> void:
print("--- 元进度·收束入账与工坊(隔离档 smoke_meta_settle/workshop/cap)---")
var sv = _meta_new_save(_META_SMOKE_PATHS[2])
var mp = load("res://core/meta_progression.gd").new(_db, sv)
var row1: Dictionary = _db.meta_boss_keystone_row(1)
var row2: Dictionary = _db.meta_boss_keystone_row(2)
# ⑥ 新档首通 L1:first = true、金额 = first_kill;total = big_reward + Σ drops;余额 = 累计入账
var d1: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
"stopped_at": 2, "keystone_total": 3, "master_seed": 11})
var drop1: Dictionary = d1["boss_drops"][0]
check(bool(drop1.get("first", false)) and int(drop1.get("amount", 0)) == int(row1.get("first_kill", -1))
and int(drop1.get("level", 0)) == 1, "新档首通 L1:boss_drops[0] {first:true, amount=first_kill}(验收5)")
check(int(d1.get("total", 0)) == 3 + int(row1.get("first_kill", 0)), "明细 total = big_reward + Σ drops")
check(sv.keystone_balance() == 3 + int(row1.get("first_kill", 0)), "余额 = 累计入账(大奖励 + Boss 两档)")
# ⑥ 同档再通 → 重复杀档且恒定不递减(第三轮同额)
var d2: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
"stopped_at": 2, "keystone_total": 3, "master_seed": 12})
var drop2: Dictionary = d2["boss_drops"][0]
check(not bool(drop2.get("first", false)) and int(drop2.get("amount", 0)) == int(row1.get("repeat_kill", -1)),
"再通 L1:{first:false, amount=repeat_kill}(首杀标记置位后转小额)")
var d3: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
"stopped_at": 2, "keystone_total": 3, "master_seed": 13})
check(not bool(d3["boss_drops"][0].get("first", true)) and int(d3["boss_drops"][0].get("amount", 0)) == int(row1.get("repeat_kill", -1)),
"重复杀第三轮恒定不递减(验收5——无衰减曲线)")
# 混合归因:通关两关 → L1 重复 + L2 首杀
var d4: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 2,
"stopped_at": 0, "keystone_total": 4, "master_seed": 14})
check(not bool(d4["boss_drops"][0].get("first", true)) and bool(d4["boss_drops"][1].get("first", false))
and int(d4["boss_drops"][1].get("amount", 0)) == int(row2.get("first_kill", -1)),
"两关通关逐关归因:L1 重复档 + L2 首杀档(first_kill 金额)")
# ⑦ 败北零通关摘要:余额只增不减(零入账)、clear_counts 不变
var bal_before: int = sv.keystone_balance()
var cc1_before: int = sv.clear_count(1)
var dz: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 0,
"stopped_at": 1, "keystone_total": 0, "master_seed": 15})
check(dz["boss_drops"].is_empty() and sv.keystone_balance() == bal_before and sv.clear_count(1) == cc1_before,
"败北零通关摘要:零入账零标记(钥石只增不减——验收1;败北局 clear_counts 不变)")
# ⑧ 工坊解锁:settle 内已随 refresh——总通关次数达阈值件数 == unlocked 数;重复 refresh 幂等
var total_clears: int = sv.clear_count(1) + sv.clear_count(2) + sv.clear_count(3)
var expect_n := 0
for row in _db.meta_workshop_items():
if total_clears >= int(row.get("clear_threshold", 99)):
expect_n += 1
var got_n := 0
for st_row in mp.workshop_status():
got_n += 1 if bool(st_row.get("unlocked", false)) else 0
check(got_n == expect_n and mp.refresh_workshop_unlocks().is_empty(),
"工坊解锁数 = 总通关 %d 次达阈值件数(%d 件)且重复 refresh 幂等" % [total_clears, expect_n])
var sv_r = _meta_new_save(_META_SMOKE_PATHS[2])
var r_n := 0
for st_row in load("res://core/meta_progression.gd").new(_db, sv_r).workshop_status():
r_n += 1 if bool(st_row.get("unlocked", false)) else 0
check(r_n == expect_n, "工坊解锁持久(reload 后一致)")
# ⑧ 干净档:通关 1 次 → 恰两件阈值 1 道具新解锁(new_unlocks 明细)
var sv_w = _meta_new_save(_META_SMOKE_PATHS[3])
var mp_w = load("res://core/meta_progression.gd").new(_db, sv_w)
var u1: Dictionary = mp_w.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
"stopped_at": 2, "keystone_total": 0, "master_seed": 16})
check(u1["new_unlocks"].size() == 2 and sv_w.item_unlocked("probe_dust") and sv_w.item_unlocked("sapper_order")
and not sv_w.item_unlocked("loot_compass"), "通关 1 次 → 恰两件阈值 1 道具解锁(new_unlocks 明细正确)")
# ⑩ recent_summaries:元素含 boss_drops 副本、最新在前、cap 10
var rs: Array = sv.recent_summaries()
check(rs.size() >= 5 and String(rs[0].get("result", "")) != "" and rs[0].has("boss_drops")
and int(rs[0].get("master_seed", -1)) == 15, "摘要最新在前 + 元素含 boss_drops 副本(复盘可归因——验收5)")
var sv_c = _meta_new_save(_META_SMOKE_PATHS[4])
var mp_c = load("res://core/meta_progression.gd").new(_db, sv_c)
for i in 12:
mp_c.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 0,
"stopped_at": 1, "keystone_total": 0, "master_seed": i})
check(sv_c.recent_summaries().size() == 10 and int(sv_c.recent_summaries()[0].get("master_seed", -1)) == 11,
"摘要 cap 10 且最新在前(12 连 settle 截断后 seed 11 居首)")
_meta_smoke_cleanup()
# ---- 战斗日志视图·行格式化呈现层同步闸(2026-09-20 真人试玩卡断 bug 回归:八型批伏击退役拆除 # ---- 战斗日志视图·行格式化呈现层同步闸(2026-09-20 真人试玩卡断 bug 回归:八型批伏击退役拆除
@@ -37,7 +37,53 @@ func _ready() -> void:
add_child(main) add_child(main)
await get_tree().process_frame await get_tree().process_frame
var t_ui_ready := Time.get_ticks_msec() - t0 var t_ui_ready := Time.get_ticks_msec() - t0
# 满配直达 Boss(基线在节点 2-4 就会败北——探针目标是 Boss 战的结算路径) # ---- 营地购买合成点击路径(元进度实现批——campbtn/购买/关闭全走真实按钮信号路径;
# 隔离存档换注 main.meta 保确定性(禁碰真实档 user://save.json);陈旧档段首清理)----
for p0 in ["user://smoke_click_probe_camp.json", "user://smoke_click_probe_camp.json.tmp",
"user://smoke_click_probe_reset.json", "user://smoke_click_probe_reset.json.tmp"]:
if FileAccess.file_exists(p0):
DirAccess.remove_absolute(p0)
var iso_c = load("res://core/save_manager.gd").new("user://smoke_click_probe_camp.json")
iso_c.load_from_disk()
main.meta = load("res://core/meta_progression.gd").new(main.db, iso_c)
var campbtn: Button = _find_button_by_prefix(main._start_overlay, "营地")
if campbtn == null:
_problems.append("营地入口按钮缺失(开始层 campbtn)")
else:
campbtn.pressed.emit() # 真实按钮信号路径 → 组合根 _open_camp
await get_tree().process_frame
if not main.camp_overlay.visible:
_problems.append("营地层未打开(点击路径)")
elif main.camp_overlay._forge_rows.is_empty():
_problems.append("锻造柜台行缺失(点击路径)")
else:
var buy_btn: Button = main.camp_overlay._forge_rows[0].btn
if not buy_btn.disabled:
_problems.append("零余额锻造购买按钮未禁用(禁用态失守)")
iso_c.add_keystone(9999)
main.camp_overlay.refresh_camp(main._camp_data())
await get_tree().process_frame
if buy_btn.disabled:
_problems.append("余额充足后购买按钮仍禁用(禁用态未随刷新)")
var bal_before: int = iso_c.keystone_balance()
var buy_attr: String = String(main.camp_overlay._forge_rows[0].attr)
var price: int = main.meta.forge_price(buy_attr)
buy_btn.pressed.emit() # 合成购买点击 → _on_camp_buy → meta.buy_forge(写调用唯一归组合根)
await get_tree().process_frame
if int(iso_c.keystone_balance()) != bal_before - price:
_problems.append("营地购买未出账(%d→%d 应 −%d)" % [bal_before, iso_c.keystone_balance(), price])
var iso_chk = load("res://core/save_manager.gd").new("user://smoke_click_probe_camp.json")
iso_chk.load_from_disk()
if int(iso_chk.forge_level(buy_attr)) != 1:
_problems.append("营地购买未持久(reload 后级 != 1)")
main._on_camp_closed()
await get_tree().process_frame
if main.camp_overlay.visible or not main._start_overlay.visible:
_problems.append("营地关闭后未恢复开始层")
# 换回全新零档(营地购买态不泄入后续冒险段——确定性;档案残留下次段首清理兜底)
var iso_reset = load("res://core/save_manager.gd").new("user://smoke_click_probe_reset.json")
iso_reset.load_from_disk()
main.meta = load("res://core/meta_progression.gd").new(main.db, iso_reset)
main.tune_panel.set_sliders(200, 100) main.tune_panel.set_sliders(200, 100)
var t1 := Time.get_ticks_msec() var t1 := Time.get_ticks_msec()
main._start_full_level() main._start_full_level()
@@ -272,3 +318,14 @@ func _dump_state() -> void:
str(diag.battle_result), str(diag.settling)]) str(diag.battle_result), str(diag.settling)])
print(" board_installed=%s facts_path='%s' refresh_used=%s flags=%s" % [ print(" board_installed=%s facts_path='%s' refresh_used=%s flags=%s" % [
str(diag.minesweeper_board_installed), str(diag.facts_path), str(diag.refresh_used_round), str(_flags_placed)]) str(diag.minesweeper_board_installed), str(diag.facts_path), str(diag.refresh_used_round), str(_flags_placed)])
# 开始层按钮按文案前缀寻址(campbtn 句柄未存组合根成员——树遍历只读;与 ui_probe 同款助手)
func _find_button_by_prefix(root: Node, prefix: String) -> Button:
if root is Button and String(root.text).begins_with(prefix):
return root
for child in root.get_children():
var found := _find_button_by_prefix(child, prefix)
if found != null:
return found
return null
@@ -13,15 +13,54 @@ func _ready() -> void:
var main = load("res://presentation/main.tscn").instantiate() var main = load("res://presentation/main.tscn").instantiate()
add_child(main) add_child(main)
await get_tree().process_frame await get_tree().process_frame
var problems: Array = []
# ---- 放弃入口(2026-09-21 用户拍板——台账 119):默认隐藏(单关测试直入口无冒险可放弃)---- # ---- 放弃入口(2026-09-21 用户拍板——台账 119):默认隐藏(单关测试直入口无冒险可放弃)----
if main.route_overlay.abandon_btn == null or main.route_overlay.abandon_btn.visible: if main.route_overlay.abandon_btn == null or main.route_overlay.abandon_btn.visible:
print("[ui_probe] FAIL — 放弃按钮默认应隐藏(单关测试直入口关)") print("[ui_probe] FAIL — 放弃按钮默认应隐藏(单关测试直入口关)")
get_tree().quit(1) get_tree().quit(1)
return return
# ---- 营地覆盖层(元进度实现批——主菜单入口/开合/余额文案/购买禁用态;
# 隔离存档换注 main.meta 保确定性(禁碰真实档 user://save.json——SaveManager 路径注入纪律)----
var iso = load("res://core/save_manager.gd").new("user://smoke_ui_probe_camp.json")
iso.load_from_disk()
main.meta = load("res://core/meta_progression.gd").new(main.db, iso)
var campbtn: Button = _find_button_by_prefix(main._start_overlay, "营地")
if campbtn == null:
problems.append("营地入口按钮缺失(开始层 campbtn)")
else:
campbtn.pressed.emit() # 合成信号路径(真实按钮 → 组合根 _open_camp)
await get_tree().process_frame
if not main.camp_overlay.visible:
problems.append("营地层未打开(campbtn 路径)")
elif main._start_overlay.visible:
problems.append("营地层打开时开始层未隐藏(规范 2.3 遮挡纪律)")
else:
if not String(main.camp_overlay._balance_label.text).begins_with("钥石"):
problems.append("营地余额文案缺失(%s)" % main.camp_overlay._balance_label.text)
if main.camp_overlay._forge_rows.size() != 3:
problems.append("锻造柜台行数错误(%d ≠ 3)" % main.camp_overlay._forge_rows.size())
else:
var zero_disabled := true
for card in main.camp_overlay._forge_rows:
zero_disabled = zero_disabled and bool(card.btn.disabled) # 零余额全禁用
if not zero_disabled:
problems.append("零余额下锻造购买按钮未全禁用(余额不足禁用态失守)")
if main.camp_overlay._workshop_rows.size() != 6:
problems.append("工坊柜台行数错误(%d ≠ 6)" % main.camp_overlay._workshop_rows.size())
main._on_camp_closed()
await get_tree().process_frame
if main.camp_overlay.visible or not main._start_overlay.visible:
problems.append("营地关闭后未恢复开始层(开合往返失守)")
# 换回全新零档(营地段购买态不泄入后续冒险段——确定性;先删后建防陈旧档泄入)
for p in ["user://smoke_ui_probe_camp.json", "user://smoke_ui_probe_camp.json.tmp"]:
if FileAccess.file_exists(p):
DirAccess.remove_absolute(p)
var iso_zero = load("res://core/save_manager.gd").new("user://smoke_ui_probe_camp.json")
iso_zero.load_from_disk()
main.meta = load("res://core/meta_progression.gd").new(main.db, iso_zero)
main._start_full_level() main._start_full_level()
await get_tree().process_frame await get_tree().process_frame
await get_tree().process_frame await get_tree().process_frame
var problems: Array = []
# ---- 放弃入口·冒险模式可见 + 战斗中按下被拒(真实按钮路径——入口挂选路停点,战斗中拒绝为防御分支)---- # ---- 放弃入口·冒险模式可见 + 战斗中按下被拒(真实按钮路径——入口挂选路停点,战斗中拒绝为防御分支)----
if main.route_overlay.abandon_btn == null or not main.route_overlay.abandon_btn.visible: if main.route_overlay.abandon_btn == null or not main.route_overlay.abandon_btn.visible:
problems.append("冒险模式放弃按钮未开放(选路停点入口在案)") problems.append("冒险模式放弃按钮未开放(选路停点入口在案)")
@@ -154,3 +193,14 @@ func _ready() -> void:
push_error("[ui_probe] FAIL — " + p) push_error("[ui_probe] FAIL — " + p)
print("[ui_probe] FAIL — " + "; ".join(problems)) print("[ui_probe] FAIL — " + "; ".join(problems))
get_tree().quit(1) get_tree().quit(1)
# 开始层按钮按文案前缀寻址(campbtn 句柄未存组合根成员——树遍历只读;text 前缀匹配首枚按钮)
func _find_button_by_prefix(root: Node, prefix: String) -> Button:
if root is Button and String(root.text).begins_with(prefix):
return root
for child in root.get_children():
var found := _find_button_by_prefix(child, prefix)
if found != null:
return found
return null