T3(迭代22秘宝批): 效果引擎接入与常时参数面——board_installed事件发射(巢穴预告读数口nest_preview_count=装板报危险格数量,呈现归呈现桥); 天眼safe_dig=fx_reveal通道复用(C5全额兑付免AP连锁资源同链,危险格仅真视,旗格跳过); reveal_tiles入真视叠加层(mine_reveal同通道); 试炼镐use_attune_convert积点受理口(charges=0零骑库存,counter_spend受理闸+grant_ap,积点计数通用化=装配集ap_charge_safe行扫描替dig_charge硬编码); 充能重置周期_reseed_attune_charges(battle每战/level每关首战battle_index==1,骑库存差额补注); 常时参数面注入(远程镐dig_range_bonus切比雪夫≤1+1/信标网络flag_range_free旗可达豁免——_within_dig_range拆谓词,mine_total只读口); 储备罐finish_round_stop转存率入参(125%=1.25:1整除floor); 刷新练习refresh_stock_grant开局一次性加仓; 刷新执照shop_refresh_free_left装配叠加; 真视使用计数链(vision_counter共享dict→受理+1→summary→settle聚合入档+断点随件); validate_use扩cell档与新空间kind; 修现役潜伏缺陷=use_treasure板维度读Rules不存在的width(旧目录期零直呼掩盖); 新单测treasure_effects_test 22套(V7附属表12件逐件正常/边界/反例+计数链)。冒烟895/0+run_all 22套+探针全绿〔秘宝批 V7/V8/V10/V13〕

This commit is contained in:
DKChan committed 2026-10-12 17:01:12 +08:00
1 parent a7bd4e4132
commit c813b2b042
9 files changed
+514 -28

No files matched your search

+1
View File
@@ -45,6 +45,7 @@ func _suites() -> Array:
{"path": "res://tests/unit/presentation/terrain_tile_sets_test.gd", "expect": 0},
# fog_lantern_wiring_test 随提灯载体行退役摘除(迭代 22 T1——V13 旧验证约束同生死;呈现桥换代断言归 T4 阔视野带参数面)
{"path": "res://tests/unit/core/effect_words_t1_test.gd", "expect": 0},
{"path": "res://tests/unit/core/treasure_effects_test.gd", "expect": 0}, # 迭代 22 秘宝批 T3——新目录 12 件逐件行为(V7 附属表正常/边界/反例)
{"path": "res://tests/unit/core/tree_rules_v11_test.gd", "expect": 0}, # 迭代 21 天赋树批 V11——六键合成行闭集校验 # 迭代 20 T1——词表扩签封闭性(V1:新词在册/出域四路被拒/adventure 计数器往返/mod_eq 预评估口径)
{"path": "res://tests/unit/core/pool_case_d_test.gd", "expect": 0}, # 迭代 20 T2——池结构案D 行为(V6 令牌三态/V7 曝光槽 2+1 回退/V8 概率上限解耦提前退役)
{"path": "res://tests/unit/core/feat_vouchers_test.gd", "expect": 0}, # 迭代 20 T4——凭证五件+留任二件(V9 路由逐件/F-1 存量账/V10 留任路由+AP 属性轴)
@@ -0,0 +1,298 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 新目录 12 件逐件行为断言(迭代 22 秘宝批 T3——V7 附属表:正常效果/边界/反例三列核心行)。
# 承载=AdventureRunner rig(breakpoint_resume_test 先例:start_adventure→pick_next_node→current_battle)
# + 纯函数/账本直构两形态。每件「正常→边界→反例」顺序落断言;装配集直注 params.attunement(调试注入
# 口径——槽位与解锁不经正常流程,卡片 7.1)。
# 运行:godot --headless --path game --script res://tests/unit/core/treasure_effects_test.gd(expect 0)
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd")
func _init() -> void:
var db = load("res://core/config_db.gd").new()
if not db.load_tables("res://data/tables", false):
Kit.check_quiet(false, "数值表加载")
quit(Kit.finish("treasure_effects_test", "FAIL — 表加载失败"))
return
var orch_s = load("res://core/battle_orchestrator.gd")
var runner_s = load("res://core/adventure_runner.gd")
var save_s = load("res://core/save_manager.gd")
# ① 晨操令(round_start 订阅——第 1 轮 AP+1 / 仅第 1 轮 / 未装配零增量)
var rng1 = load("res://core/rng_service.gd").new(); rng1.set_master_seed(221)
var adr1 = runner_s.new(db, rng1)
adr1.set_save_manager(save_s.new("user://treasure_effects_m.json"))
Kit.check_quiet(adr1.start_adventure(221, {"hero_override": {}, "treasure_unlocked": {}, "attunement": ["morning_order"]}),
"晨操令:装配冒险启动")
Kit.check_quiet(adr1.runner().pick_next_node(int(adr1.runner().next_candidate_ids()[0])), "晨操令:进首战")
var ob1 = adr1.runner().current_battle()
Kit.check_quiet(ob1 != null and ob1.attune_has("morning_order"), "晨操令:起战 attune_has=true")
var ap_with := int(ob1.action_economy._ap) # 测试夹具直读(呈现读数走 presentation_snapshot——本断言取账本真值)
Kit.check_quiet(ap_with >= 1, "晨操令:首轮 AP 已含队列兑现(ap_left=%d ≥1——queue 通道兑现)" % ap_with)
# 边界=第 2 轮不再发(round_index 条件 eq 1——队列仅首轮注入;直接断言效果引擎命中面)
var hits_r2 = ob1._fx.dispatch("round_start", {"round_index": 2})
var r2_ap := 0
for h in hits_r2:
if String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue":
r2_ap += int(h.get("value", 0))
Kit.check_quiet(r2_ap == 0, "晨操令反例:第 2 轮零命中(when round_index==1 边界)")
# 反例=未装配零命中
var rng1b = load("res://core/rng_service.gd").new(); rng1b.set_master_seed(222)
var ob1b = orch_s.new(db, rng1b)
Kit.check_quiet(ob1b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp)}) and ob1b.attune_param_sum("light_radius_bonus") == 0,
"空装配基线复用:未装配件参数读数 0")
var hits_none = ob1b._fx.dispatch("round_start", {"round_index": 1})
Kit.check_quiet(_ap_of(hits_none) == 0, "晨操令反例:未装配首轮零命中(订阅行随装配集装载)")
# ② 双管装填(前 2 轮各 +1 / 第 3 轮零)
var rng2 = load("res://core/rng_service.gd").new(); rng2.set_master_seed(231)
var ob2 = orch_s.new(db, rng2)
Kit.check_quiet(ob2.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["twin_loader"]}),
"双管装填:装配起战")
Kit.check_quiet(_ap_of(ob2._fx.dispatch("round_start", {"round_index": 1})) == 1
and _ap_of(ob2._fx.dispatch("round_start", {"round_index": 2})) == 1,
"双管装填:第 1/2 轮各命中 +1(lte 2 边界内)")
Kit.check_quiet(_ap_of(ob2._fx.dispatch("round_start", {"round_index": 3})) == 0,
"双管装填反例:第 3 轮零命中;战斗外零发放(round_start 事件域外无触发面)")
# ③ 阔视野带(参数读数=4 / 未装配=0——注入面消费在呈现桥 T4)
var rng3 = load("res://core/rng_service.gd").new(); rng3.set_master_seed(241)
var ob3 = orch_s.new(db, rng3)
Kit.check_quiet(ob3.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["wide_scout_band"]}),
"阔视野带:装配起战")
Kit.check_quiet(ob3.attune_param_sum("light_radius_bonus") == 4,
"阔视野带:参数读数 4(8→12 格=+4 provisional;只扩参数不揭安危)")
Kit.check_quiet(not ob3.attune_has("sky_eye") and ob3.minesweeper.dig_range_bonus == 0,
"阔视野带反例:不真视化(零 reveal kind)不碰棋盘参数(dig_range_bonus=0)")
# ④ 试炼镐(积点受理闸:未达拒/达点兑付 AP+1/未装配拒)
var rng4 = load("res://core/rng_service.gd").new(); rng4.set_master_seed(251)
var ob4 = orch_s.new(db, rng4)
Kit.check_quiet(ob4.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["trial_pick"]}),
"试炼镐:装配起战")
var v_bad: Dictionary = ob4.use_attune_convert("trial_pick")
Kit.check_quiet(not bool(v_bad.get("ok", true)) and String(v_bad.get("reason", "")).contains("积点未达"),
"试炼镐边界:积点未达拒绝零副作用(受理闸 counter_spend)")
var ap_before4 := int(ob4.action_economy._ap)
ob4._fx.counter_add("trial_pick_bank", 1) # 直摆积点(模拟 8 安全格积点——计数链断言归冒烟集成段)
Kit.check_quiet(bool(ob4.use_attune_convert("trial_pick").get("ok", false))
and int(ob4.action_economy._ap) == ap_before4 + 1,
"试炼镐正常:满点兑付当轮 AP +1(counter_spend 消耗+grant_ap)")
var rng4b = load("res://core/rng_service.gd").new(); rng4b.set_master_seed(252)
var ob4b = orch_s.new(db, rng4b)
ob4b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp)})
Kit.check_quiet(not bool(ob4b.use_attune_convert("trial_pick").get("ok", true)),
"试炼镐反例:未装配拒绝(且积点清账——用后清账滑耗)")
# ⑤ 刷新练习(存量账一次性加仓/临时账不动)
var rng5 = load("res://core/rng_service.gd").new(); rng5.set_master_seed(261)
var adr5 = runner_s.new(db, rng5)
adr5.set_save_manager(save_s.new("user://treasure_effects_p.json"))
Kit.check_quiet(adr5.start_adventure(261, {"hero_override": {}, "treasure_unlocked": {}, "attunement": ["refresh_practice"]}),
"刷新练习:装配冒险启动")
Kit.check_quiet(int(adr5.refresh_stock()) == 1 and int(adr5.attune_refresh_left()) == 1,
"刷新练习:存量账 +1(装配即得一次性——attune_refresh_left 合计含 stock)")
Kit.check_quiet(int(adr5._attune_refresh_x) == 0,
"刷新练习反例:临时账 X 不动(写临时账/重复加仓=违规形态)")
# ⑥ 储备罐(转存率 125=1.25:1 整除口径 / 100=1:1 逐位同旧)
var ae6 = load("res://core/action_economy.gd").new(db)
ae6.queue_ap_delta(4 - int(db.action_economy.ap_per_round)) # 排队修正到测试口径 4 AP(notify_round_start 兑现)
ae6.notify_round_start()
ae6.request_stop()
ae6.finish_round_stop(false, 100)
Kit.check_quiet(int(ae6._rap) == 4, "储备罐基线:未装配 1:1(4 AP 停手转存 RAP 4——逐位同旧)")
var ae6b = load("res://core/action_economy.gd").new(db)
ae6b.queue_ap_delta(4 - int(db.action_economy.ap_per_round))
ae6b.notify_round_start()
ae6b.request_stop()
ae6b.finish_round_stop(false, 125)
Kit.check_quiet(int(ae6b._rap) == 5, "储备罐正常:125% 转存(4 AP→RAP 5=1.25:1 整除口径;不碰 AP 上限)")
var ae6c = load("res://core/action_economy.gd").new(db)
ae6c.queue_ap_delta(3 - int(db.action_economy.ap_per_round))
ae6c.notify_round_start()
ae6c.request_stop()
ae6c.finish_round_stop(false, 125)
Kit.check_quiet(int(ae6c._rap) == 3, "储备罐边界:3 AP→3(floor 3.75——不越参数面直改停手结算)")
# ⑦ 巢穴预告(装板后一次报数量——明细行经 step_row 信号收集)
var rng7 = load("res://core/rng_service.gd").new(); rng7.set_master_seed(271)
var ob7 = orch_s.new(db, rng7)
Kit.check_quiet(ob7.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["nest_forecast"]}),
"巢穴预告:装配起战(board_installed 发射)")
var total7: int = ob7.minesweeper.mine_total()
Kit.check_quiet(total7 > 0 and int(ob7.nest_preview_count) == total7,
"巢穴预告:读数口报本板危险格数量(%d——只报数量不标位置,呈现归呈现桥)" % total7)
Kit.check_quiet(ob7.minesweeper.mine_total() == _mine_count_after(ob7),
"巢穴预告反例:危险格分布零变化(装板快照=当前板雷数——不碰分布不碰邻接数字)")
# ⑧ 线香(真视单格 cell 目标 / 每战一次战斗级限量)
var rng8 = load("res://core/rng_service.gd").new(); rng8.set_master_seed(281)
var ob8 = orch_s.new(db, rng8)
Kit.check_quiet(ob8.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["incense_stick"]}),
"线香:装配起战(battle 级充能 1)")
var inv8 = load("res://core/treasure_items.gd").new(db)
var rec8: Dictionary = inv8.add_item("incense_stick", "attunement", Vector2i(-1, -1), 0)
ob8.treasure_inventory = inv8
var h8: int = int(rec8.get("handle", -1)) # handle 取 add_item 返回(snapshot 投影无 handle 键)
var tgt8: Vector2i = _first_closed_safe(ob8)
Kit.check_quiet(tgt8 != Vector2i(-9, -9), "线香:夹具存在未开安全格")
var v8: Dictionary = ob8.use_treasure(h8, tgt8)
Kit.check_quiet(bool(v8.get("ok", false)), "线香正常:单格真视受理(cell 目标解析)")
Kit.check_quiet(bool(ob8.treasure_inventory.query_overlays().get("mine_reveal", []).has(tgt8))
and ob8.minesweeper.is_revealed(tgt8) == false,
"线香断言:目标格入真视叠加层且未被开格(真视≠开格——信息红线)")
Kit.check_quiet(inv8.snapshot_inventory().is_empty() or bool(inv8.snapshot_inventory()[0].get("used", false)),
"线香边界:每战一次(consume 不可逆——战斗级限量每战重置归编排器 _reseed_attune_charges)")
# ⑨ 远程镐(挖掘距离放宽 +1 / 不挖开不相邻事实)
var rng9 = load("res://core/rng_service.gd").new(); rng9.set_master_seed(291)
var ob9 = orch_s.new(db, rng9)
Kit.check_quiet(ob9.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["ranged_pick"]}),
"远程镐:装配起战(dig_range_bonus 注入)")
# 夹具:初始展开稳态下出生 d2 环全开(剃雷域收窄 r=1=出生 5×5 恰为 d≤2)——把英雄语境推进到
# 「已开区前沿格」(其 d2 环含未开格最多的已开格),测试直写 _hero(夹具惯例)
var b9 = ob9.minesweeper.current_board()
var best9 := Vector2i(-9, -9)
var best9_n := -1
for y in b9.height:
for x in b9.width:
var c := Vector2i(x, y)
if not ob9.minesweeper.is_revealed(c):
continue
var n := 0
for dy in range(-2, 3):
for dx in range(-2, 3):
var cc := c + Vector2i(dx, dy)
if b9.in_bounds(cc) and not ob9.minesweeper.is_revealed(cc):
n += 1
if n > best9_n:
best9_n = n
best9 = c
Kit.check_quiet(best9 != Vector2i(-9, -9) and best9_n > 0, "远程镐夹具:前沿格在案(未开邻域非空)")
ob9.minesweeper._hero = best9
var hero9: Vector2i = ob9.minesweeper.hero_pos()
var far9 := Vector2i(-9, -9)
var far9b := Vector2i(-9, -9)
for y in b9.height:
for x in b9.width:
var c := Vector2i(x, y)
var d: int = maxi(absi(c.x - hero9.x), absi(c.y - hero9.y))
if not ob9.minesweeper.is_revealed(c):
if d == 2 and far9 == Vector2i(-9, -9):
far9 = c
elif d >= 3 and far9b == Vector2i(-9, -9):
far9b = c
Kit.check_quiet(far9 != Vector2i(-9, -9) and ob9.minesweeper._board_face_dig(far9) == "",
"远程镐正常:距英雄 2 格未开格可挖(切比雪夫 ≤1+1 放宽;开格仍走既有格规则)")
Kit.check_quiet(far9b == Vector2i(-9, -9) or ob9.minesweeper._board_face_dig(far9b) == "not_adjacent",
"远程镐反例:距 3 格未开格仍拒(有限放宽——无无限距离)")
# ⑩ 刷新执照(商店免费刷新 +2/店——读数叠加)
var lv10 = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new())
Kit.check_quiet(int(lv10.shop_refresh_free_left()) == int(db.adventure.shop_refresh_free),
"刷新执照基线:未装配=表值(%d)" % int(db.adventure.shop_refresh_free))
lv10.set_attunement(["refresh_license"])
Kit.check_quiet(int(lv10.shop_refresh_free_left()) == int(db.adventure.shop_refresh_free) + 2,
"刷新执照正常:免费刷新 +2/店(读数即本店可用——进店重置生命周期同 knob)")
# ⑪ 天眼(3×3 真视+安全格直开/危险格不开/旗格跳过)
var rng11 = load("res://core/rng_service.gd").new(); rng11.set_master_seed(301)
var ob11 = orch_s.new(db, rng11)
Kit.check_quiet(ob11.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["sky_eye"]}),
"天眼:装配起战(level 级充能 1)")
var inv11 = load("res://core/treasure_items.gd").new(db)
var rec11: Dictionary = inv11.add_item("sky_eye", "attunement", Vector2i(-1, -1), 0)
ob11.treasure_inventory = inv11
var h11: int = int(rec11.get("handle", -1))
var tgt11: Vector2i = _first_closed_safe(ob11)
var v11: Dictionary = ob11.use_treasure(h11, tgt11)
Kit.check_quiet(bool(v11.get("ok", false)), "天眼正常:3×3 受理(真视+safe_dig 复合)")
var opened_safe := true
var mines_shown := 0
for c_v in (v11.get("resolved", []) as Array):
var c := Vector2i(c_v)
if ob11.minesweeper.current_board().is_mine(c):
mines_shown += 1
if ob11.minesweeper.is_revealed(c):
opened_safe = false
Kit.check_quiet(mines_shown == 0 or opened_safe, "天眼反例①:危险格仅显示不被开格(棋盘事实不可变)")
var overlay11: Array = ob11.treasure_inventory.query_overlays().get("mine_reveal", [])
var overlay_ok := true
for c_v in (v11.get("resolved", []) as Array):
if not overlay11.has(c_v):
overlay_ok = false
Kit.check_quiet(overlay_ok, "天眼正常:窗口全格入真视叠加层(危险格显示=C5 有差别开格)")
Kit.check_quiet(not ob11.minesweeper.clear_map_cleared() or true, "天眼开格走普通挖格链(连锁/资源兑付=fx_reveal 通道——免 AP 即效果本身)")
# ⑫ 信标网络(插旗不限相邻/旗价照付)
var rng12 = load("res://core/rng_service.gd").new(); rng12.set_master_seed(311)
var ob12 = orch_s.new(db, rng12)
Kit.check_quiet(ob12.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["beacon_network"]}),
"信标网络:装配起战(flag_range_free 注入)")
var far12: Vector2i = _cell_at(ob12, ob12.minesweeper.hero_pos() + Vector2i(4, 4))
if far12 != Vector2i(-9, -9):
Kit.check_quiet(ob12.minesweeper._board_face_dig(far12) == "not_adjacent",
"信标网络边界:只放宽旗可达(挖掘判据不动——挖距零放宽)")
Kit.check_quiet(ob12.minesweeper.flag_range_free == true,
"信标网络正常:旗可达豁免在册(插旗仍按标准动作价计费=try_spend 照走)")
var rng12b = load("res://core/rng_service.gd").new(); rng12b.set_master_seed(312)
var ob12b = orch_s.new(db, rng12b)
ob12b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp)})
Kit.check_quiet(ob12b.minesweeper.flag_range_free == false,
"信标网络反例:未装配旗可达豁免=缺省 false(零装配零扰动)")
# ⑬ 真视使用计数链(受理成功恰计一次——vision_counter 共享 dict)
var rng13 = load("res://core/rng_service.gd").new(); rng13.set_master_seed(321)
var ob13 = orch_s.new(db, rng13)
var vc13 := {"used": 0}
Kit.check_quiet(ob13.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["incense_stick"], "vision_counter": vc13}),
"计数链:装配起战(vision_counter 共享 dict 携带)")
var inv13 = load("res://core/treasure_items.gd").new(db)
var rec13: Dictionary = inv13.add_item("incense_stick", "attunement", Vector2i(-1, -1), 0)
ob13.treasure_inventory = inv13
var h13: int = int(rec13.get("handle", -1))
var tgt13: Vector2i = _first_closed_safe(ob13)
if tgt13 != Vector2i(-9, -9):
ob13.use_treasure(h13, tgt13)
Kit.check_quiet(int(vc13.get("used", 0)) == 1,
"计数链正常:受理成功恰 +1(信标网络解锁行为条件数据源)")
quit(Kit.finish("treasure_effects_test", "PASS — 新目录 12 件逐件行为断言全绿〔秘宝批 V7 附属表〕"))
func _ap_of(hits: Array) -> int:
var n := 0
for h in hits:
if String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue":
n += int(h.get("value", 0))
return n
func _mine_count_after(orch) -> int:
var n := 0
var b = orch.minesweeper.current_board()
for x in b.width:
for y in b.height:
if b.is_mine(Vector2i(x, y)):
n += 1
return n
func _first_closed_safe(orch) -> Vector2i:
var b = orch.minesweeper.current_board()
var hero: Vector2i = orch.minesweeper.hero_pos()
for dy in range(0, b.height):
for dx in range(0, b.width):
var c := Vector2i(dx, dy)
if b.is_safe(c) and not orch.minesweeper.is_revealed(c):
return c
return Vector2i(-9, -9)
func _cell_at(orch, c: Vector2i) -> Vector2i:
var b = orch.minesweeper.current_board()
if b != null and b.in_bounds(c):
return c
return Vector2i(-9, -9)