feat(凭证件裁削): 卡② 同调电池删除+完美保险迁专长+配额净删落地(迭代 31 卡②)

用户 2026-10-18 逐裁四项落码:
- T1/T2 同调电池整件删除:CURIO_KINDS 摘 kind、三字段与三处分发表分支删净、
  runner 侧 _battery_pending/断点键/getter/choose_battery_target 全清(呈现层零接线=有账本无入口)。
- T3 完美保险出池:kind/字段/分发摘除,_pending_insurance 与 level_runner 注入通道
  (voucher_insurance 键、set_voucher_grants 第三参)整链拆除,battle_orchestrator._fx_insurance 删。
- T4 引擎扩签 forgive_seal_miss(EFFECT_KINDS 31→32)+两账相减:新增 _perfect_forgiven 单向累加、
  谓词抽成 perfect_sweep_earned()(有效误标=误标总数−豁免数),_perfect_hit_false 保持纯净=契约 2;
  seal_missed 发射时机与消费点零改动=契约 3(危险直觉·守同事件订阅面在验收组里对拍)。
- T5 feats.tres 第 30 行完美保险专长:rarity rare/cap 省略/domain=battle 显式写出(战斗域首例)/
  desc「本场首次封印落空,不计入完美封印判定」;限次由行 when counter(insurance_used) lt 1 承载,
  do 自带 counter_id 记账由授予段消费点落账(代拍板 3 选型:单行落表,理由见实现报告)。
- T6 配额净删:TIER_QUOTAS rare 9→7、令牌池总量 104→102,validate() 硬断言过;smoke 配额锚/牌库
  构成/专长行数随批换代。
- T7 验收:feat_vouchers_test 39 项全绿(出池反例/断点缺键兼容/账本保留/配额锚/装载/限次一次/
  计数纯净/两账相减真值表/跨战清零/事件不吞)+forge_table 词表闭集断言 31→32。
  变异验证 6 条:5 条证红;「domain battle→adventure」在本 rig 不可观测(未红),已如实登记。
- 存量缺陷顺带修:旧 _emit_row("insurance_used","",…) 空来源会触发行来源断言(旧路径无消费点故从未走红),
  新订阅面给出 source_class=seal_miss。

基线(卡① 合回后 master 4f953ce8):冒烟 1151/0、单元 34 套全绿、探针 --click 全绿(user:// 存档移开态)。
This commit is contained in:
DKChan committed 2026-10-19 04:45:49 +08:00
1 parent 4f953ce829
commit 072c19b4a5
10 files changed
+183 -110

No files matched your search

+106 -21
View File
@@ -1,12 +1,18 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 凭证件五件+留任二件单测(刷新券/储备金袋/时沙/完美保险/同调电池 五件 + 留任二件——刷新写错账修复钉面)。
# 覆盖:刷新券→刷新存量账(非 _attune_refresh_x——写错账复发反例)/储备金袋→RAP/时沙→首轮 AP 池/
# 完美保险→标错豁免/同调电池→充能账(choose_battery_target 玩家点选——禁自动替选反例);
# 凭证件留任二件+两件出池+完美保险专长单测(刷新券/储备金袋/时沙 三件路由+留任二件——刷新写错账修复钉面)。
# 覆盖:刷新券→刷新存量账(非 _attune_refresh_x——写错账复发反例)/储备金袋→RAP/时沙→首轮 AP 池;
# 迭代 31 卡② 凭证件裁削批——同调电池整件删除、完美保险迁专长:两件出池反例(词表/兑付/符号清零/断点缺键)、
# 配额净删锚(稀有 7/总量 102)、完美保险专长验收组(装载/限次一次/计数纯净/两账相减判定/跨战清零/事件不吞);
# 留任 2 件路由与值不变(GP 钱包/当即回血)+行动点上限归属性轴(bonus.ap 落点)。
# 运行:godot --headless --path game --script res://tests/unit/core/feat_vouchers_test.gd(expect 0)
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd")
const EffectEngineScript := preload("res://core/effect_engine.gd")
func EffectEngineKindProbe(_db) -> bool:
return EffectEngineScript.EFFECT_KINDS.has("forgive_seal_miss")
func _init() -> void:
@@ -48,20 +54,36 @@ func _init() -> void:
Kit.check_quiet(a._apply_curio_pick("time_sand", "rare", 2), "时沙兑付受理〔专长批 V9〕")
Kit.check_quiet(a._time_sand_queue == 2, "时沙:首轮 AP 队列 +2(池增量非上限)〔专长批 V9〕")
# V9-4 完美保险:标错豁免待注入(节点级)
Kit.check_quiet(a._apply_curio_pick("perfect_insurance", "rare", 1), "完美保险兑付受理〔专长批 V9〕")
Kit.check_quiet(a._pending_insurance == 1, "完美保险:豁免待注入 1(节点级)〔专长批 V9〕")
# V9-5 同调电池:待分配充能 + 玩家点选(禁自动替选反例)+ 跨层写充能账
Kit.check_quiet(a._apply_curio_pick("attune_battery", "rare", 1), "同调电池兑付受理〔专长批 V9〕")
Kit.check_quiet(a.battery_pending() == 1, "同调电池:待分配充能 1〔专长批 V9〕")
Kit.check_quiet(not a.choose_battery_target("incense_stick"),
"电池反例:件不在装配集拒绝(禁自动替选——零副作用)〔专长批 V9·秘宝批换主语〕")
a._attunement = ["incense_stick"] # 注入装配集(incense_stick=新目录 active_charged)
a.run_ledger.attune_charges = {"incense_stick": 1} # 迭代 27 T3——四账住 RunLedger
Kit.check_quiet(a.choose_battery_target("incense_stick") and int(a.run_ledger.attune_charges["incense_stick"]) == 2,
"电池正例:玩家点选 incense_stick→充能账 +1(跨层写 attune_charges)〔专长批 V9·秘宝批换主语〕")
Kit.check_quiet(a.battery_pending() == 0, "电池:分配后待账清零〔专长批 V9〕")
# V23 两件出池(迭代 31 卡② 凭证件裁削批 R1——同调电池删除/完美保险迁专长)
Kit.check_quiet(not db.adventure.CURIO_KINDS.has("attune_battery")
and not db.adventure.CURIO_KINDS.has("perfect_insurance"),
"出池词表:CURIO_KINDS 零两件(三选一池物理不可再出)〔卡② R1〕")
Kit.check_quiet(not a._apply_curio_pick("attune_battery", "rare", 1)
and not a._apply_curio_pick("perfect_insurance", "rare", 1),
"出池反例:两件兑付不再受理(牌库无令牌=拒绝零副作用)〔卡② R1〕")
Kit.check_quiet(not a.has_method("choose_battery_target") and not a.has_method("battery_pending"),
"电池符号清零:runner 无点选函数与 getter(编译级孤儿面)〔卡② R1〕")
Kit.check_quiet(int(db.adventure.curio_cap("attune_battery")) == 99,
"出池后未知 kind 走分发表 default(cap 读数兜底 99——零崩溃面)〔卡② R1〕")
# 账本保留(契约 1):主动件充能账与每冒险重置入口零改动
a._attunement = ["incense_stick"]
a.run_ledger.attune_charges = {"incense_stick": 99}
a._seed_attune_charges()
Kit.check_quiet(int(a.run_ledger.attune_charges.get("incense_stick", -1)) != 99,
"账本保留:删件后 _seed_attune_charges 仍按装备重置充能账(attune_charges 语义零变化)〔卡② R1〕")
# V24 配额净删锚(R3——用户裁「净删」,rare 9→7/总量 104→102)
var rare_sum := 0
var rare_kinds: Array[String] = []
for k in db.adventure.CURIO_KINDS:
var q := int(db.adventure.curio_quota(k, "rare"))
rare_sum += q
if q > 0:
rare_kinds.append(k)
Kit.check_quiet(int(db.adventure.TIER_QUOTAS["rare"]) == 7 and rare_sum == 7
and int(db.adventure.deck_total()) == 102 and rare_kinds.has("ap") and rare_kinds.has("rap") and rare_kinds.has("time_sand")
and rare_kinds.size() == 3,
"配额锚:稀有档合计 7、投放品种={ap,rap,time_sand}、令牌池总量 102(validate 硬断言同批)〔卡② R3〕")
Kit.check_quiet(db.adventure.validate().is_empty(), "AdventureTable.validate() 零违例(两件出池后)〔卡② R3〕")
# V9-6 全链:凭证令牌兑付=牌库摘牌(选中即消尽)
a._curio_deck = [{"kind": "rap", "tier": "rare"}, {"kind": "attack", "tier": "common"}]
@@ -91,17 +113,80 @@ func _init() -> void:
sm.load_from_disk()
a.run_ledger.adventure_counters = {"pain_layers": 3, "twin_digs": 5} # 迭代 27 T3——四账住 RunLedger
a._refresh_stock = 2
a._battery_pending = 1
a.set_save_manager(sm)
a._on_level_finished({"result": "victory", "hp": 10, "node_index": 1, "node_id": 1, "node_type": "shop"})
var bp16: Dictionary = sm.breakpoint_snapshot()
Kit.check_quiet(int((bp16.get("adventure_counters", {}) as Dictionary).get("pain_layers", -1)) == 3
and int(bp16.get("refresh_stock", -1)) == 2 and int(bp16.get("battery_pending", -1)) == 1,
"V16:新账本三件入断点快照(adventure_counters/refresh_stock/battery_pending)〔专长批 V16〕")
and int(bp16.get("refresh_stock", -1)) == 2 and not bp16.has("battery_pending"),
"V16:账本入断点快照;电池键已随卡② 摘除(断点结构零该键)〔专长批 V16+卡② R1〕")
var b2 = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
Kit.check_quiet(b2.resume_adventure(bp16.duplicate(), {}), "V16 前置:续玩受理")
Kit.check_quiet(int(b2.run_ledger.adventure_counters.get("pain_layers", -1)) == 3 and b2.refresh_stock() == 2 and b2.battery_pending() == 1,
var bp_legacy: Dictionary = bp16.duplicate(true)
bp_legacy["battery_pending"] = 7 # 旧断点档遗留键(Alpha 无迁移——缺键/多键兼容面)
var b3 = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
Kit.check_quiet(b3.resume_adventure(bp_legacy, {}), "V16:旧档含 battery_pending 键续玩受理(缺键/多键零报错)〔卡② R1〕")
Kit.check_quiet(int(b2.run_ledger.adventure_counters.get("pain_layers", -1)) == 3 and b2.refresh_stock() == 2,
"V16:续玩回填逐项一致(计数恢复——跨战账不丢)〔专长批 V16〕")
# V25 完美保险专长验收组(迭代 31 卡② T4/T5——R2 全项:装载/限次一次/计数纯净/两账相减/跨战清零/事件不吞)
var fi: Dictionary = db.feats.row_by_id("perfect_insurance")
Kit.check_quiet(not fi.is_empty() and String(fi.get("rarity", "")) == "rare"
and String(fi.get("desc", "")).find("雷") == -1 and String(fi.get("desc", "")).find("封印") >= 0,
"专长行合法:第 30 行 perfect_insurance 在表、rarity rare、desc 零「雷」字面〔卡② R2〕")
Kit.check_quiet(db.feats.validate().is_empty(), "feats.validate() 零违例(新行订阅形态与词表合法)〔卡② R2〕")
var RigScript := load("res://core/battle_orchestrator.gd")
var RngScript := load("res://core/rng_service.gd")
var rig_rng = RngScript.new()
rig_rng.set_master_seed(778001) # EC7 未播种取数致命——rig 须先播种子
var orch = RigScript.new(db, rig_rng)
Kit.check_quiet(orch.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40,
"hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {},
"feats": ["perfect_insurance"], "tree_rules": ["sense_guard"], "battle_index": 1,
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
"voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}),
"保险 rig:战斗启动(feats=perfect_insurance 与 tree_rules=sense_guard 双订阅 seal_missed)〔卡② R2〕")
var safe_cells: Array = []
for c in orch.minesweeper.current_board().all_cells():
if not orch.minesweeper.current_board().is_mine(c):
safe_cells.append(c)
if safe_cells.size() >= 2:
break
Kit.check_quiet(safe_cells.size() == 2, "保险 rig:两枚安全格定位(去重闸按格实例——限次样本须异格)〔卡② R2〕")
Kit.check_quiet(int(orch._perfect_hit_false) == 0 and int(orch._perfect_forgiven) == 0,
"保险前置:误标账与豁免账双零起〔卡② R2〕")
orch._grant_seg._judge_seal({"cell": safe_cells[0], "hit": false}, 1)
Kit.check_quiet(int(orch._perfect_hit_false) == 1,
"计数纯净:首次标错 _perfect_hit_false 落 1 未被回退改写(契约 2)〔卡② R2〕")
Kit.check_quiet(int(orch._perfect_forgiven) == 1,
"豁免一次:首次标错 _perfect_forgiven=1(forgive_seal_miss 派发)〔卡② R2〕")
Kit.check_quiet(int(orch.action_economy.pending_ap_delta()) == 1,
"事件不吞:同一标错的 seal_missed 仍被危险直觉·守收到(queue AP+1)〔卡② R2〕")
orch._grant_seg._judge_seal({"cell": safe_cells[1], "hit": false}, 2)
Kit.check_quiet(int(orch._perfect_hit_false) == 2 and int(orch._perfect_forgiven) == 1,
"限次:第二次标错不获豁免(forgiven 恒 1——when counter lt 1)〔卡② R2〕")
Kit.check_quiet(not orch.perfect_sweep_earned(),
"判定相减:误标 2 − 豁免 1 = 有效 1 → 完美不成立〔卡② R2 真值表〕")
orch._perfect_hit_false = 1
orch._perfect_hit_true = 1
Kit.check_quiet(orch.perfect_sweep_earned(),
"判定相减:误标 1 − 豁免 1 = 有效 0 且标中≥1 → 完美成立〔卡② R2 真值表〕")
orch._perfect_hit_true = 0
Kit.check_quiet(not orch.perfect_sweep_earned(),
"判定守卫:标中数 0 时零误标也不成立(板级 ≥1 标中条件零松动)〔卡② R2 真值表〕")
var rig_rng2 = RngScript.new()
rig_rng2.set_master_seed(778002)
var orch2 = RigScript.new(db, rig_rng2)
Kit.check_quiet(orch2.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40,
"hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {},
"feats": ["perfect_insurance"], "tree_rules": [], "battle_index": 2,
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
"voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}),
"次战 rig:第二战编排启动(feats 同件)〔卡② R2〕")
var sc2 = safe_cells[0]
orch2._grant_seg._judge_seal({"cell": sc2, "hit": false}, 1)
Kit.check_quiet(int(orch2._perfect_forgiven) == 1 and int(orch2._perfect_hit_false) == 1,
"跨战清零:domain=battle 豁免账每战重建——次战首次标错再豁免一次〔卡② R2〕")
Kit.check_quiet(EffectEngineKindProbe(db), "词表闭集:forgive_seal_miss 在 EFFECT_KINDS(出域即加载致命——扩签合规)〔卡② R4〕")
sm.clear_breakpoint()
quit(Kit.finish("feat_vouchers"))