iter41-卡1:核心数据面结构收敛批:85键全清——三族拆域+config_db归位容器+存档拆件+meta瘦身+adventure拆段+spawn束化;行为零变化,8新件在阈

This commit is contained in:
DKChan committed 2026-10-29 01:33:50 +08:00
1 parent bda377eb16
commit a613a3cb90
157 files changed
+7453 -4288

No files matched your search

+12 -68
View File
@@ -43,72 +43,17 @@ tests/
> **分层判据**:能不能在 `--script` 模式下不加载 autoload 就验完?能 = 单元测试;
> 需要两个以上真实系统协同并走落盘/落库 = 集成测试;需要完整项目引导与场景树 = 冒烟/系统/UI 探针。
**能力清单 v1**(2026-10-28 立——验证与清单体系整改阶段 1 的覆盖索引:一行=一个玩家或系统可观测的能力,代表用例只给指针、不复制断言细节;原「集成旅程清单」五条跨系统旅程已并入本表。首期覆盖核心四域(29 行)+跨域条件闸 1 行,共 30 行;其余域按同一形态扩面):
**集成旅程清单**(2026-10-28 立——跨域旅程用例的登记处:一个行为穿过多个领域的对外方法=旅程,验收落本层;缺口旅程由属主批/工程债批按批落用例后回填本表):
> **与验证集清单的关系**:`tests/验证集清单.md` 已于阶段 2「存量断言精简」完成后删除——冻结对账基准(基点 `b9260138`/1587 条)与精简对账结果(33 站点并为 7、1587→1561,合回 `87d241c1`)留 git 历史。本表是该清单退役后的唯一覆盖索引(维护锚=test-conventions §8.3)。本表不是闸,不替代改动—验收对照表、体验验证清单与静态纪律;每行的唯一事实源仍是 `game/tests/` 下的测试代码。
>
> **代表用例口径**:写 `文件:函数`,每行 2–3 个;函数内可含 `Kit.check_quiet` 静默通道(逐断言提取器口径外,test-conventions §5.2 已备案)——本表以函数为锚,不受该工具局限影响。
>
> **维护规则(每批收口核对本批受影响行)**:本批改到哪个能力,收口时核对该行代表用例可 grep、在本批真实跑批中执行、真实入口未绕过改动点,并把「最后验证批次」刷新为本批回执;新增能力加行,能力退役删行。全表核对频率与抽样规则归阶段 3。首期未覆盖:排雷循环(棋盘生成/排雷玩法/行动经济)、表现与支撑(HUD/主题/字体/地形瓦片/模态)、数据配置与跨系统契约——按同一形态后续扩面。
>
> **旅程补录方法**:新旅程=加一行,「系统」列写涉及边界;源=architecture「API 边界」表+「数据流」存档/读档路径+各 GDD 登记的 deferral 集成用例。
>
> 「最后验证批次」列记回执 commit;多数行初值 `20c13c3a`=存量断言精简 A+B 首批的全量域闸回执,RNG 行随 C 档批刷新为 `fd6cf968`。
| 旅程 | 涉及边界 | 现行覆盖 | 缺口 | 登记批 |
|---|---|---|---|---|
| 存档⇄RNG 快照往返保真 | SaveManager×AdventureRunner×RngService | `save_rng_roundtrip_test.gd` 用例一(闸 02) | — | 迭代 29 T3 |
| 旧档迁移后断点续玩零跳号 | SaveManager 迁移链×AdventureRunner×BoardGenerator | `save_rng_roundtrip_test.gd` 用例二(闸 02) | — | 迭代 29 T3 |
| 断点附随件与事件日志会话 | AdventureRunner×SaveManager×日志面 | `cross_system_flow_test.gd`(闸 02) | 盘点候选 | 断言迁移批(2026-10-23) |
| 关卡图断点重生成同图/生成图确定性 | LevelRunner×节点图生成器×AdventureRunner | `node_map_determinism_test.gd`(闸 02) | — | 断言迁移批(2026-10-23,迭代 35 卡②随动换代) |
| 回放确定性(同种子逐位) | 全玩法链 | `parity_anchor.py`(闸 11 条件闸) | — | 迭代 29 T3 收编为条件闸件 |
**旅程段一:开机与存档**
| 能力 | 系统 | 真实入口 | 代表用例(文件:函数) | 最后验证批次 | 缺口 |
|---|---|---|---|---|---|
| 存档写盘⇄读回往返保真 | 存档序列化 | `SaveManagerCore.write_to_disk()`/`load_from_disk()` | `save_manager_domain_test.gd:_smoke_save` | `20c13c3a` | — |
| 三槽位隔离与激活失败回滚 | 存档序列化 | `SaveManagerCore.activate_slot()`/`create_slot()`/`delete_slot()` | `save_slot_isolation_test.gd:_test_three_slot_roundtrip_and_isolation`;`save_slot_isolation_test.gd:_test_activate_failure_restores_previous_slot` | `20c13c3a` | — |
| 槽位摘要窥视与坏槽三态 | 存档序列化 | `SaveManagerCore.peek_slot_summary()` | `save_slot_summary_test.gd:_test_peek_three_slots_distinct_values`;`save_slot_summary_test.gd:_test_peek_empty_and_unreadable_states`;`save_slot_summary_test.gd:_test_peek_zero_side_effect` | `20c13c3a` | — |
| 启动态无激活不写盘 | 存档序列化 | `SaveManagerCore.has_write_target()`/`write_to_disk()` | `save_boot_no_activate_test.gd:_test_write_rejected_without_target`;`save_boot_no_activate_test.gd:_test_out_of_range_slots_rejected` | `20c13c3a` | — |
| 旧档迁移链与断点续玩零跳号(集成) | 存档序列化×冒险结构 | `SaveManagerCore.load_from_disk()`(内置迁移链)× `AdventureRunner.resume_adventure()` | `save_rng_roundtrip_test.gd:_test_old_version_save_migrates_and_resumes` | `20c13c3a` | — |
| 存档⇄RNG 快照往返保真(集成) | 存档序列化×RNG 服务×冒险结构 | `AdventureRunner.start_adventure()` → `SaveManagerCore.write_to_disk()`/`load_from_disk()` | `save_rng_roundtrip_test.gd:_test_disk_roundtrip_preserves_breakpoint` | `20c13c3a` | — |
| RNG 统一流派生与取号账本 | RNG 服务 | `RngServiceCore.derive_at()`/`open_stream()`/`counter()` | `rng_service_test.gd:_test_derive_determinism`;`rng_service_test.gd:_test_stream_draw_determinism`;`rng_stream_boot_test.gd:_smoke_initialize` | `fd6cf968` | — |
| 断点片段往返与续玩幂等 | 冒险结构×关卡节点地图 | `LevelRunner.breakpoint_fragment()`/`restore_fragment()`;`AdventureRunner.resume_adventure()` | `breakpoint_resume_test.gd:_init` | `20c13c3a` | — |
| 跨系统附随面:事件日志落盘与断点附随件(集成) | RNG 服务×存档序列化×冒险结构 | `RngServiceCore.new_adventure()`(开事件日志会话);`SaveManagerCore.breakpoint_snapshot()`(断点附随键) | `cross_system_flow_test.gd:_rng_event_log_session`;`cross_system_flow_test.gd:_feats_breakpoint_roundtrip` | `20c13c3a` | 日志面仅此一条会话覆盖 |
**旅程段二:冒险开局与选路**
| 能力 | 系统 | 真实入口 | 代表用例(文件:函数) | 最后验证批次 | 缺口 |
|---|---|---|---|---|---|
| 冒险生命周期:开局受理/三关序列/败北线 | 冒险结构 | `AdventureRunner.start_adventure()`/`continue_to_next_level()` | `adventure_feats_test.gd:_smoke_adventure_api`;`adventure_feats_test.gd:_smoke_adventure` | `20c13c3a` | — |
| 专长(奇物)授予与凭证保底 | 奇物 | `AdventureRunner.present_node_curio()`/`feats_held()` × 效果引擎兑付 | `adventure_feats_test.gd:_smoke_feats`;`feat_vouchers_test.gd:_init` | `20c13c3a` | — |
| 节点图生成合法性与同种子复现 | 关卡节点图生成 | `NodeMapGenerator.generate()`/`validate_output()` | `node_map_generator_test.gd:_test_output_ok`;`node_map_generator_test.gd:_test_reproducible`;`node_map_generator_test.gd:_test_exit_structure` | `20c13c3a` | — |
| 节点图生长递增与无解 fail-fast | 关卡节点图生成 | `NodeMapGenerator.generate()`/`is_fatal()` | `node_map_growth_test.gd:_test_growth`;`node_map_growth_test.gd:_test_fatal_subprocess` | `20c13c3a` | — |
| 起关/选路/断点重生成同图(集成) | 关卡节点地图×关卡节点图生成×冒险结构 | `LevelRunner.start_level()`/`start_level_external()`/`pick_next_node()`;`AdventureRunner.resume_adventure()` | `node_map_determinism_test.gd:_smoke_generated_map_determinism` | `20c13c3a` | — |
| 信息迷雾类型可见性 | 关卡节点地图 | `LevelRunner.type_visible_ids()` | `level_map_type_vis_test.gd:_test_layer_distance`;`level_map_type_vis_test.gd:_test_boss_exempt`;`level_map_type_vis_test.gd:_test_resume_rebuild` | `20c13c3a` | — |
| 关卡表消费侧校验与关卡推进 | 关卡节点地图×数据配置 | `LevelTable.validate_graph()`;`LevelRunner.start_level()` | `level_table_validate_test.gd:_test_positive`;`level_table_validate_test.gd:_test_negative`;`level_shop_economy_test.gd:_smoke_level` | `20c13c3a` | — |
**旅程段三:战斗结算**
| 能力 | 系统 | 真实入口 | 代表用例(文件:函数) | 最后验证批次 | 缺口 |
|---|---|---|---|---|---|
| 伤害公式与掷点乘区 | 战斗结算 | `DamageMath.pool_damage()`/`roll_discount_percent()`/`rolled_component()` | `damage_math_test.gd:_test_pool_damage`;`damage_math_test.gd:_test_roll_discount_percent`;`roll_multiplier_test.gd:_smoke_armor_roll` | `20c13c3a` | — |
| 伤害单据编排与回放 | 战斗结算 | `DamageController.submit()`/`open_ticket()`/`flush()` | `damage_controller_test.gd:_run_recipe_sequence`;`damage_controller_test.gd:_test_five_recipes_and_replay` | `20c13c3a` | — |
| 轮内流与结算五段全序 | 战斗结算 | `BattleOrchestrator.begin_battle()` | `round_flow_skill_pool_test.gd:_smoke_full_battle`;`round_flow_skill_pool_test.gd:_smoke_settlement_segments` | `20c13c3a` | — |
| 技能池数据面与回放独立 | 敌人系统 | `BattleOrchestrator.begin_battle()`(技能池抽号/校验) | `round_flow_skill_pool_test.gd:_smoke_skill_pool_data`;`round_flow_skill_pool_test.gd:_smoke_skill_pool_replay_independence` | `20c13c3a` | — |
| Boss 半血解锁技门控 | 敌人系统 | `BattleOrchestrator.begin_battle()`(Boss 行) | `skill_unlock_test.gd:_test_gate_predicate`;`skill_unlock_test.gd:_test_latch_irreversible` | `20c13c3a` | — |
| 封印与极性路由/不可逆 | 地图战斗资源×战斗结算 | 效果引擎 × `BattleOrchestrator.begin_battle()` | `seal_polarity_test.gd:_smoke_seal_ap_exhausted`;`seal_polarity_test.gd:_smoke_seal_irreversible_quadrants`;`seal_polarity_test.gd:_smoke_polarity_routing` | `20c13c3a` | — |
| 变异布置门控 | 地形变异 | `BattleOrchestrator.begin_battle()`(变异布置段) | `variant_placement_test.gd:_smoke_variant_placement`;`variant_placement_test.gd:_smoke_global_variant` | `20c13c3a` | — |
**旅程段四:奖励、经济与局外成长**
| 能力 | 系统 | 真实入口 | 代表用例(文件:函数) | 最后验证批次 | 缺口 |
|---|---|---|---|---|---|
| 清图奖励双份与零路径 | 地图战斗资源 | `BattleOrchestrator.begin_battle()` → 步骤流行(`source_class=clear_reward`) | `seal_polarity_test.gd:_smoke_clear_reward_chain`;`seal_polarity_test.gd:_smoke_clear_reward_zero_paths` | `20c13c3a` | — |
| 奖励三选一候选/刷新付费/呈现 | 奇物×节点奖励屏 | `AdventureRunner.present_node_curio()`(数据面);奖励节点覆盖层场景(呈现面) | `curio_refresh_paid_test.gd:_init`;`reward_overlay_test.gd:_run` | `20c13c3a` | — |
| GP 钱包/escrow/商店收口/共享池 | 地图战斗资源×商店 | `LevelRunner.buy_product()`/`player_request_refresh()`/`close_wallet()`/`set_shop_pool()` | `level_shop_economy_test.gd:_smoke_gp_wallet`;`level_shop_economy_test.gd:_smoke_shop_room`;`level_shop_economy_test.gd:_smoke_shop_pool` | `20c13c3a` | — |
| 元进度入账与锻造两轴 | 元进度×锻造工坊 | `SaveManagerCore.add_keystone()`/`spend_keystone()`/`set_forge_route_level()` | `meta_progression_forge_test.gd:_meta_pricing_and_buy`;`meta_progression_forge_test.gd:_smoke_forge_axes` | `20c13c3a` | — |
| 天赋树购买与六键合成 | 天赋树 | `SaveManagerCore.tree_nodes()`/`set_tree_level()` | `meta_progression_forge_test.gd:_smoke_tree`;`meta_progression_forge_test.gd:_smoke_tree_rules`;`tree_rules_v11_test.gd:_init` | `20c13c3a` | — |
| 秘宝结构冻结与 12 件效果 | 秘宝 | `TreasureItems.add_item()`/`query_inventory()`/`validate_use()` | `treasure_items_test.gd:_init`;`treasure_effects_test.gd:_init` | `20c13c3a` | — |
**跨域条件闸**
| 能力 | 系统 | 真实入口 | 代表用例 | 最后验证批次 | 缺口 |
|---|---|---|---|---|---|
| 回放确定性(同种子三变体逐位) | 全玩法链 | `tools/parity_anchor.py`(闸 11 条件闸,按批发起) | `tools/parity_anchor.py`(整件为条件闸) | —(条件闸) | 条件闸未随本批执行 |
候选旅程盘点方法:源=architecture「API 边界」表逐行+「数据流」存档/读档路径+各 GDD 登记的 deferral 集成用例;新旅程先登记「缺口」列,用例按批落地后回填「现行覆盖」。
## 断言计数双口径(2026-10-08 迭代 19 卡① T4 订正)
@@ -125,7 +70,7 @@ tests/
|---|---|---|---|
| `tools/run_smoke.py`(冒烟测试) | 这版能否启动、能否加载表、能否进主屏——零逻辑判定 | 退出码+脚本错误扫描 | 开机路径/表加载改动后的快速自检;收口由闸编排 |
| `tools/run_system_test.py`(系统测试) | 真实玩法路径的行为断言面(推理→战斗→结算→进度推进) | 退出码+脚本错误扫描 | 玩法链路改动的调试期确认 |
| `tests/run_all.gd`(单元与集成) | 公式/状态机/契约/跨系统闭环(64 套件 manifest) | 退出码+三硬闸(见下);每套件子进程带截止时间守护——超时先 taskkill /T 整树回收再判该套件 FAIL、批继续(默认 120s,环境变量 `RUN_ALL_SUITE_TIMEOUT_SEC` 可覆盖) | 新套件单跑调试;逻辑改动的最小单元复跑;开发期选跑——追加 `-- --only=<词[,词…]>` 只跑路径命中词的套件(零命中=用法错 exit 2;选跑态仅供开发期最窄确认,不产闸回执、不作验收凭据) |
| `tests/run_all.gd`(单元与集成) | 公式/状态机/契约/跨系统闭环(64 套件 manifest) | 退出码+三硬闸(见下) | 新套件单跑调试;逻辑改动的最小单元复跑;开发期选跑——追加 `-- --only=<词[,词…]>` 只跑路径命中词的套件(零命中=用法错 exit 2;选跑态仅供开发期最窄确认,不产闸回执、不作验收凭据) |
| `tools/probe_gate.sh`(场景探针,`--click`/`--full` 两模式) | UI 可达性、可见性、状态——真实实例化场景+真实输入路径 | 探针自检+退出码 | 新增 UI 区域的配套探针直跑与断言复核 |
| `tools/render_gate.sh`(渲染 golden) | 画面基线——截图与基线图逐像素容差比对 | golden 比对脚本(依赖 PIL) | 主题/素材/排版类改动;窗口化运行,需开显示器 |
| `game/tests/static_scan.sh`(S 域) | 项目纪律+第三方质量规则:RNG 单点/core 纯度/类型标注/文件头/注释禁词/旧符号残留;S13=gdstyle 质量规则(重复 load、循环内 await、循环内分配、不可达代码等真缺陷类;基线键=文件|规则|函数名(类/文件级=文件|规则,无数字条目带消息),数字漂移不假红,同键指标恶化超基线登记值即红) | 退出码(逐规则 FAIL 行) | 改产品码前后的快速自检;新增纪律先补扫描规则再修码 |
@@ -151,9 +96,8 @@ python tools/run_system_test.py
# 场景探针(UI 探针层)——以探针场景为运行目标,走完整项目引导(autoload 在位)
godot --headless --path . res://probe/<probe>.tscn
# 挂死防护内建于 run_all.gd(引擎无死锁探测,超时靠外部判):每套件子进程带截止时间——
# 默认 120s,环境变量 RUN_ALL_SUITE_TIMEOUT_SEC 可覆盖;超时先 taskkill /T 整树回收
#(cmd wrapper 连引擎孙进程一起杀,不留孤儿),该套件判 FAIL、批继续——单套件挂死不拖死整闸
# 本地闸必须配外部 wall-clock 超时(引擎无死锁探测;run_all_gates.sh 已内置)
timeout 120s godot --headless --script tests/run_all.gd
```
> 套件 manifest 现况:`_suites()` 登记 **64 条**(61 单元+3 集成,2026-10-28 实数;口径=manifest 条目数,
@@ -112,24 +112,24 @@ func _save_summary_integration() -> void:
# ⑨ 摘要集成(冒险结构批产物驱动):run_adventure 真局摘要 → apply 原语 → 域读数 → 写盘读回保持
var st9: Dictionary = LevelAutoplay.run_adventure(_db, 42, 900, 400)
var f9 = sm_script.new(path9)
f9.apply_adventure_summary(st9.get("summary", {}))
f9.meta_state.apply_adventure_summary(st9.get("summary", {}))
var ks_run := 0
for li in range(1, int(_db.level.rows.size()) + 1):
ks_run += int(_db.meta_boss_keystone_row(li).get("first_kill", 0))
for li in range(1, int(_db.run.level.rows.size()) + 1):
ks_run += int(_db.crossrun.meta.boss_keystone_row(li).get("first_kill", 0))
Kit.check("摘要集成:冒险驱动满配冒险成功(seed 42)", String(st9.get("result", "")) == "completed")
Kit.check("集成钥石余额 = ΣBoss 首杀行值(%d——逐杀掉落全源)" % f9.keystone_balance(), f9.keystone_balance() == ks_run)
Kit.check("集成关卡完成次数表全 1", f9.clear_count(1) == 1 and f9.clear_count(2) == 1 and f9.clear_count(3) == 1)
Kit.check("集成三关首杀标记齐", f9.boss_first_kill_marked(1) and f9.boss_first_kill_marked(2) and f9.boss_first_kill_marked(3))
Kit.check("集成 recent 1 条且 seed 42", f9.recent_summaries().size() == 1 and int(f9.recent_summaries()[0].get("master_seed", -1)) == 42)
Kit.check("集成钥石余额 = ΣBoss 首杀行值(%d——逐杀掉落全源)" % f9.meta_state.keystone_balance(), f9.meta_state.keystone_balance() == ks_run)
Kit.check("集成关卡完成次数表全 1", f9.meta_state.career_state().clear_counts.get(str(1), 0) == 1 and f9.meta_state.career_state().clear_counts.get(str(2), 0) == 1 and f9.meta_state.career_state().clear_counts.get(str(3), 0) == 1)
Kit.check("集成三关首杀标记齐", f9.meta_state.career_state().boss_first_kill.has(str(1)) and f9.meta_state.career_state().boss_first_kill.has(str(2)) and f9.meta_state.career_state().boss_first_kill.has(str(3)))
Kit.check("集成 recent 1 条且 seed 42", f9.meta_state.career_state().recent_summaries.size() == 1 and int(f9.meta_state.career_state().recent_summaries[0].get("master_seed", -1)) == 42)
var st9b: Dictionary = LevelAutoplay.run_adventure(_db, 42, 900, 400)
f9.apply_adventure_summary(st9b.get("summary", {}))
Kit.check("第二局后关卡完成次数全 2", f9.clear_count(1) == 2 and f9.clear_count(2) == 2 and f9.clear_count(3) == 2)
Kit.check("第二局后余额翻倍(2 × Σ首杀——模拟驱动恒首杀口径)", f9.keystone_balance() == 2 * ks_run)
Kit.check("集成 recent 长度 2", f9.recent_summaries().size() == 2)
f9.meta_state.apply_adventure_summary(st9b.get("summary", {}))
Kit.check("第二局后关卡完成次数全 2", f9.meta_state.career_state().clear_counts.get(str(1), 0) == 2 and f9.meta_state.career_state().clear_counts.get(str(2), 0) == 2 and f9.meta_state.career_state().clear_counts.get(str(3), 0) == 2)
Kit.check("第二局后余额翻倍(2 × Σ首杀——模拟驱动恒首杀口径)", f9.meta_state.keystone_balance() == 2 * ks_run)
Kit.check("集成 recent 长度 2", f9.meta_state.career_state().recent_summaries.size() == 2)
Kit.check("集成写盘成功", f9.write_to_disk())
var r9 = sm_script.new(path9)
Kit.check("集成写盘读回全量保持", r9.load_from_disk() and r9.keystone_balance() == 2 * ks_run and r9.clear_count(3) == 2
and r9.recent_summaries().size() == 2 and r9.boss_first_kill_marked(3))
Kit.check("集成写盘读回全量保持", r9.load_from_disk() and r9.meta_state.keystone_balance() == 2 * ks_run and r9.meta_state.career_state().clear_counts.get(str(3), 0) == 2
and r9.meta_state.career_state().recent_summaries.size() == 2 and r9.meta_state.career_state().boss_first_kill.has(str(3)))
if FileAccess.file_exists(path9): # 段尾清理(与段首清场对称——源 _smoke_save 同款)
DirAccess.remove_absolute(path9)
@@ -147,7 +147,7 @@ func _smoke_playtest_logger() -> void:
var snaps: Array = []
settlement.settlement_snapshot.connect(func(snap): snaps.append(snap))
Kit.check("会话文件开启(%s)" % path, path.contains("playtest_20260912"))
Kit.check("快照战斗启动", settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)}))
Kit.check("快照战斗启动", settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)}))
var minesweeper = settlement.minesweeper
for i in range(2):
minesweeper.settle_batch([{"action": "stop"}])
@@ -159,7 +159,7 @@ func _smoke_playtest_logger() -> void:
if not s1.has(k):
missing.append(k)
Kit.check("快照字段齐备(缺 %s)" % str(missing), missing.is_empty())
Kit.check("快照含英雄/经济/板序号(板 seq=%d)" % int(s1.board.seq), int(s1.hero.max_hp) == int(_db.hero.max_hp) and s1.economy.has("phase") and s1.board.has("seq"))
Kit.check("快照含英雄/经济/板序号(板 seq=%d)" % int(s1.board.seq), int(s1.hero.max_hp) == int(_db.combat.hero.max_hp) and s1.economy.has("phase") and s1.board.has("seq"))
# spawn_count 回归(playtest-2026-09-12 报告 Bug#1):停手两轮零击杀(主角攻击 6×2 < 24 HP 确定)→
# unit_count 必须 == spawn_count(全灭才允许 <);快照级区分「首轮全灭」vs「空场」的数据面在位
Kit.check("快照 spawn_count 在位且 == unit_count(无击杀轮:%d/%d——全灭/空场可判别)" % [int(s1.get("spawn_count", -1)), int(s1.get("unit_count", -1))],
@@ -35,7 +35,7 @@ func _init() -> void:
func _smoke_generated_map_determinism() -> void:
# ① 纯函数复现:图 = f(参数档, 种子)——同档同种子两次产图逐位同图,换种子必变图
var gen = NodeMapGenerator.new(_fresh_rng(910001))
var params2: Dictionary = _db.level_gen_params_row(2)
var params2: Dictionary = _db.run.level_gen_params.row_by_level_index(2)
var g_a: Dictionary = gen.generate(params2, 910002)
var g_b: Dictionary = gen.generate(params2, 910002)
var g_c: Dictionary = gen.generate(params2, 910003)
@@ -60,7 +60,7 @@ func _smoke_generated_map_determinism() -> void:
Kit.check("冒险模式起关受理(入口停点)", runner2.start_level_external(3,
Callable(self, "_smoke_rest_stub"), true))
Kit.check("起关产图零计数器消费(同图不依赖取数号位)", rng_c.counter() == counter_before)
var params3: Dictionary = _db.level_gen_params_row(3)
var params3: Dictionary = _db.run.level_gen_params.row_by_level_index(3)
var entry_count: int = int(params3.get("entry_count", -1))
Kit.check("入口停点候选数=参数档入口个数(L3 档 %d——多入口首跳即选路,禁写死单入口)〔v2〕" % entry_count,
entry_count >= 1 and (runner2.next_candidates() as Array).size() == entry_count)
@@ -81,7 +81,7 @@ func _smoke_generated_map_determinism() -> void:
Kit.check("种子 42 两次开局图逐位同图(教学局恒同图——首局固定种子的确定性承载)〔v7〕",
adr_x.runner().nodes() == adr_y.runner().nodes())
Kit.check("开局图 = L1 参数档在种子 42 下的生成图(开局无隐式取数分叉)",
adr_x.runner().nodes() == (gen.generate(_db.level_gen_params_row(1), 42) as Dictionary).get("nodes", []))
adr_x.runner().nodes() == (gen.generate(_db.run.level_gen_params.row_by_level_index(1), 42) as Dictionary).get("nodes", []))
# ⑤ 断点写→恢复→再产图同图(续玩按主种子+参数档重生成,断点不存图快照)
Kit.check("停点断点落盘在档", sv_w.has_breakpoint())
var bp: Dictionary = sv_w.breakpoint_snapshot()
@@ -96,21 +96,21 @@ func _smoke_generated_map_determinism() -> void:
adr_old.resume_adventure(bp_old, {}) == false)
# ⑥ 关卡表不再携带图数据(图源换代后 level 行只剩行级字段)
var tpl_free := true
for li in range(1, _db.level_count() + 1):
if (_db.level_row(li) as Dictionary).has("templates"):
for li in range(1, _db.run.level.level_count() + 1):
if (_db.run.level.row_by_level_index(li) as Dictionary).has("templates"):
tpl_free = false
Kit.check("level 表三行均无 templates 键(模板数据已清出)〔v9〕",
tpl_free and _db.level_count() == 3)
tpl_free and _db.run.level.level_count() == 3)
# ⑦ 收束置位:新档 false → settle 置 true 恰一次(幂等)
var sv = SaveManagerScript.new(_bp_path + ".flag.json")
sv.load_from_disk()
Kit.check("新档首次冒险布尔位 = false(默认域——首局恒走固定种子 42)",
sv.is_first_adventure_done() == false)
sv.meta_state.career_state().first_adventure_done == false)
var mp = load("res://core/meta_progression.gd").new(_db, sv)
mp.settle_adventure({"levels_cleared": 1, "keystone_total": 10, "cleared": false, "result": "failed"})
Kit.check("冒险收束点置位(三结局同写——写入只在收束挂点)", sv.is_first_adventure_done() == true)
Kit.check("冒险收束点置位(三结局同写——写入只在收束挂点)", sv.meta_state.career_state().first_adventure_done == true)
mp.settle_adventure({"levels_cleared": 0, "keystone_total": 0, "cleared": false, "result": "failed"})
Kit.check("置位幂等(重复收束不翻转)", sv.is_first_adventure_done() == true)
Kit.check("置位幂等(重复收束不翻转)", sv.meta_state.career_state().first_adventure_done == true)
DirAccess.remove_absolute(_bp_path)
DirAccess.remove_absolute(_bp_path + ".flag.json")
@@ -103,7 +103,7 @@ func _test_old_version_save_migrates_and_resumes() -> void:
var sm_old := SaveManagerScript.new(SAVE_PATH)
Kit.check_quiet(sm_old.load_from_disk(), "旧档用例:旧档经迁移链可载")
Kit.check_quiet(sm_old.has_breakpoint(), "旧档用例:迁移后断点未被迁移链吃掉")
var forge: Dictionary = sm_old.forge_routes_snapshot()
var forge: Dictionary = sm_old.meta_state.forge_state()
Kit.check_quiet(str(forge.get("weapon", "")) == "sword",
"旧档用例:迁移补齐当前版本新增域(缺 forge_routes 的旧档按初始持有态可玩)")
+5 -69
View File
@@ -1,16 +1,12 @@
# tests/run_all.gd — 单元测试统一入口(双载体的逻辑测试调用点,套件 manifest 协议 D2 AC-2b)
# 用法:godot --headless --path game --script res://tests/run_all.gd
# 用法:godot --headless --path game --script res://tests/run_all.gd (本地闸 02 闸包装外部超时:timeout 120s …)
# 选跑态(开发期直跑专用):追加 `-- --only=<词[,词…]>` ——对套件 res:// 路径做子串匹配,只跑命中套件;
# 零命中/空值=用法错 quit(2)。选跑态仅供开发期最窄确认,不产闸回执、不作验收凭据;
# 批末全量闸不带参数不经选跑分支。
# 批末全量闸不带参数不经选跑分支——默认(无参数)行为逐字节不变。
# 协议:套件清单逐条"套件 → 期望退出码";致命类独立进程(expect=1)。
# 执行形态:逐套件起独立 Godot 子进程实跑
#(OS.get_executable_path() 解析本引擎二进制——--script 模式无 autoload/全局标识符,
# 每个测试文件自带显式实例化夹具,见 tests/README 协议要点)。
# 挂死守护:每套件子进程带截止时间(默认 120s,环境变量 RUN_ALL_SUITE_TIMEOUT_SEC 可覆盖)。
# 套件挂死(_init 在 quit() 前中断,子进程永不退出)=超时先 taskkill /T 整树回收
#(连 cmd wrapper 带引擎孙进程一起杀,只杀 wrapper 会留孤儿殃及后续全部 Godot 启动)
# 再判该套件 FAIL 继续下一套件——单套件挂死不拖死整批;正常退出路径判定形态不变。
# 场景探针(.tscn 载体)由本地闸(probe_gate.sh)单独步骤调用,不经本入口。
extends SceneTree
@@ -18,11 +14,6 @@ extends SceneTree
# 错误不改进程退出码、不计失败数,只能由调用方扫输出——三硬闸之二)。
const ERROR_MARKERS: Array[String] = ["SCRIPT ERROR", "Parse Error"]
# 每套件截止时间(秒):默认与 probe_gate 每段同额;环境变量可覆盖——服务挂死守护的
# 机制验证与日后调参,非法值/非正值一律回落默认。
const SUITE_TIMEOUT_ENV := "RUN_ALL_SUITE_TIMEOUT_SEC"
const SUITE_TIMEOUT_DEFAULT_SEC := 120
func _init() -> void:
# 选跑态解析:--only=<词[,词…]>(经 -- 分隔的 user args)——对套件 res:// 路径做子串匹配。
@@ -163,69 +154,21 @@ func _run_suite(suite: Dictionary, idx: int, filtered: bool = false) -> int:
+ "非 console 版是 GUI 子系统,子进程 stdout/stderr 句柄无效,脚本错误扫描无从落地")
return 1
# 子进程输出只能落盘再读:OS.execute 的 output 管道在本机 Godot/Windows 组合下恒为空
#(实测 0 行),故整条命令交给 cmd 并重定向到日志。退出码同样走文件通道——
# 阻塞式 execute 无超时能力(套件挂死时整批陪葬),换 create_process 非阻塞起进程
# 后又没有退出码读取 API,故由 cmd /V:ON 在套件退出后把 !ERRORLEVEL! 回显落盘,
# 值与阻塞式 execute 经 cmd 回传的同一。
#(实测 0 行),故整条命令交给 cmd /c 并重定向到日志,退出码由 cmd 原样回传。
var proj := ProjectSettings.globalize_path("res://")
var log := proj + "/tests/.gates/run_all%s_%02d.log" % ["_filter" if filtered else "", idx]
var codefile := log + ".code"
if FileAccess.file_exists(codefile):
DirAccess.remove_absolute(codefile) # 陈值防护:上轮临界残件不许冒充本轮退出码
DirAccess.make_dir_recursive_absolute(proj + "/tests/.gates")
# /V:ON=延迟展开:!ERRORLEVEL! 到 echo 执行时(套件已退出)才取值;
# 延迟展开会吞字面 !,命令串各段(引擎路径/工程路径/套件路径/日志路径)不得含 !。
var cmd := '"%s" --headless --path "%s" --script "%s" > "%s" 2>&1 & echo !ERRORLEVEL! > "%s"' % [bin, proj, path, log, codefile]
var cmd := '"%s" --headless --path "%s" --script "%s" > "%s" 2>&1' % [bin, proj, path, log]
var out := PackedStringArray()
var pid: int = OS.create_process("cmd.exe", PackedStringArray(["/V:ON", "/C", cmd]))
if pid < 0:
push_error("[run_all] FAIL %s — cmd 子进程启动失败(pid=%d)" % [path, pid])
return 1
# 轮询先查存活再查截止:进程在轮询间隙自然退出时按正常路径判,截止临界不误杀。
var timed_out := false
var deadline_ms := Time.get_ticks_msec() + _suite_timeout_sec() * 1000
while true:
if not OS.is_process_running(pid):
break
if Time.get_ticks_msec() >= deadline_ms:
timed_out = true
break
OS.delay_msec(200)
var code: int = OS.execute("cmd.exe", PackedStringArray(["/c", cmd]), out, true, false)
var text := FileAccess.get_file_as_string(log) if FileAccess.file_exists(log) else ""
var negative := bool(suite.get("negative", false))
if timed_out:
var kill_out := PackedStringArray()
OS.execute("taskkill", PackedStringArray(["/F", "/T", "/PID", str(pid)]), kill_out, true)
# taskkill 落刀到进程对象消失有间隔:等死确认再放行下一套件(10s 宽限),
# 确认不了也如实报红留 PID 供手工清杀,不静默放行。
var reap_deadline := Time.get_ticks_msec() + 10000
while OS.is_process_running(pid) and Time.get_ticks_msec() < reap_deadline:
OS.delay_msec(200)
var reaped := not OS.is_process_running(pid)
# 临界窗口:截止到落刀之间套件可能恰好跑完 echo——同删退出码文件,.gates 不积残件。
if FileAccess.file_exists(codefile):
DirAccess.remove_absolute(codefile)
if negative:
print("[run_all] %s:负面套件——全文输出已折叠(输出中的错误为其测试目标,非缺陷)" % path)
else:
print(text)
if reaped:
push_error("[run_all] FAIL %s — 超时挂死(%ds),进程树已整树回收(pid=%d)" % [path, _suite_timeout_sec(), pid])
else:
push_error("[run_all] FAIL %s — 超时挂死(%ds),进程树回收未确认(pid=%d,需手工清杀残留)" % [path, _suite_timeout_sec(), pid])
return 1
# 负面套件(坏表/违规输入是它的测试目标)全文折叠,只回显结论行——
# 它输出里的 Parse Error 是预期产物,回显出去会让编排层的日志扫描误判成真实脚本错误。
if negative:
print("[run_all] %s:负面套件——全文输出已折叠(输出中的错误为其测试目标,非缺陷)" % path)
else:
print(text)
var code_txt := FileAccess.get_file_as_string(codefile).strip_edges() if FileAccess.file_exists(codefile) else ""
if code_txt == "" or not code_txt.is_valid_int():
push_error("[run_all] FAIL %s — 退出码文件缺失或非数字(%s)——子进程未走到退出码落盘即死,判红" % [path, codefile])
return 1
var code: int = int(code_txt)
DirAccess.remove_absolute(codefile) # 用毕即删:退出码经文件通道但不落 residuum(.gates 只留 *.log)
if not negative and _has_script_error(text):
push_error("[run_all] FAIL %s — 输出含脚本错误标记:%s" % [path, _script_error_line(text)])
return 1
@@ -262,13 +205,6 @@ func _engine_bin() -> String:
return ""
func _suite_timeout_sec() -> int:
var env := OS.get_environment(SUITE_TIMEOUT_ENV)
if env != "" and env.is_valid_int() and int(env) > 0:
return int(env)
return SUITE_TIMEOUT_DEFAULT_SEC
func _has_script_error(text: String) -> bool:
return _script_error_line(text) != ""
+2
View File
@@ -66,8 +66,10 @@ func _check_main_scene(main: Node) -> void:
if main == null:
return
var run_scope: Node = main.get("run_scope")
check(run_scope != null, "闲时会话 run_scope 非空(永非 null 契约)")
check(run_scope != null and run_scope.is_inside_tree(), "闲时会话 run_scope 在树")
var start_overlay: Control = main.get("_start_overlay")
check(start_overlay != null, "开始菜单层已装配(_start_overlay)")
check(start_overlay != null and start_overlay.visible, "启动态菜单可见(boot 后玩家见菜单)")
check(main.get("settings_store") != null, "系统设置存储已装配(settings_store)")
check(main.get("debug_badge") != null, "调试入口角标已装配(debug_badge)")
+13 -11
View File
@@ -173,10 +173,12 @@ func _smoke_boot_chain() -> void:
var sm = main.get_node("/root/SaveManager")
# ④-1 boot 路径:启动态钉死(卡① sv2)——boot 无激活态、断点在档恒否,续玩钮恒隐
#(原「按钮态==真实档断点态」关系判据随启动态换代改写:test-conventions §9.5 申报在案,卡② mn8 再承接)
check(not sm.has_breakpoint(),
"V2d boot 无断点在档(启动态不隐式激活——卡① sv2 钉死)〔装配生命批 V2·卡② mn8 改写〕")
# ④-1b 启动态三断言(卡① sv2——真实 autoload boot 链)
check(sm.active_slot() == 0 and not sm.has_write_target(),
check(sm.slots.active_slot() == 0 and not sm.slots.has_write_target(),
"V2-s1 boot 后无激活态(active_slot=0、写盘目标空)〔卡① sv2〕")
check(sm.keystone_balance() == 0 and not sm.has_breakpoint() and sm.adventure_total() == 0,
check(sm.meta_state.keystone_balance() == 0 and not sm.has_breakpoint() and sm.meta_state.career_state().adventure_total == 0,
"V2-s2 boot 内存=默认合法空态(钥石/断点/总局数全零值)〔卡① sv2〕")
check(not sm.write_to_disk(),
"V2-s3 boot 无激活写盘拒绝(write_to_disk=false 生产入口)〔卡① sv3〕")
@@ -185,7 +187,7 @@ func _smoke_boot_chain() -> void:
for p in ["user://smoke_boot_chain.json", "user://smoke_boot_chain.json.tmp"]:
if FileAccess.file_exists(p):
DirAccess.remove_absolute(p)
sm.attach_path("user://smoke_boot_chain.json")
sm.slots.attach_path("user://smoke_boot_chain.json")
check(main._menu_screen.entersavebtn != null and main._menu_screen.settingsbtn != null
and main._menu_screen.quitbtn != null,
"V2d 三钮在位·进入存档/设置/退出游戏(续玩钮族退役后主菜单操作组承接)〔卡② mn1/mn8〕")
@@ -220,8 +222,8 @@ func _smoke_run_lifecycle() -> void:
for p in ["user://smoke_run_lifecycle.json", "user://smoke_run_lifecycle.json.tmp"]:
if FileAccess.file_exists(p):
DirAccess.remove_absolute(p)
sm.attach_path("user://smoke_run_lifecycle.json")
sm.mark_first_adventure_done()
sm.slots.attach_path("user://smoke_run_lifecycle.json")
sm.meta_state.mark_first_adventure_done()
# ---- 第一局:开局→注脏→放弃→终局出口→会话收束 ----
main.debug_overlay.seed_edit.text = "42"
main._start_adventure_from_menu()
@@ -276,8 +278,8 @@ func _smoke_sim_guards() -> void:
for p in ["user://smoke_sim_guards.json", "user://smoke_sim_guards.json.tmp"]:
if FileAccess.file_exists(p):
DirAccess.remove_absolute(p)
sm.attach_path("user://smoke_sim_guards.json")
sm.mark_first_adventure_done()
sm.slots.attach_path("user://smoke_sim_guards.json")
sm.meta_state.mark_first_adventure_done()
# wd6:闲时态 + 模拟运行态 → 调试两层入口守卫早退(开始层不收起、无结算对象)
check(main.run_scope.settlement == null and not main.run_scope.is_adventure,
"前置:闲时会话(settlement 空 + is_adventure=false)")
@@ -418,7 +420,7 @@ func _smoke_lifecycle_matrix() -> void:
for p in ["user://smoke_lifecycle.json", "user://smoke_lifecycle.json.tmp"]:
if FileAccess.file_exists(p):
DirAccess.remove_absolute(p)
sm.attach_path("user://smoke_lifecycle.json")
sm.slots.attach_path("user://smoke_lifecycle.json")
main.debug_overlay.seed_edit.text = "42"
# 阶段①闲时
await _stage_faces(main, "V3a 闲时", "null", "null", "null",
@@ -511,7 +513,7 @@ func _smoke_lifecycle_matrix() -> void:
# 营地经进档开层(卡② mn9 新路径——campbtn 已撤);槽模板注入隔离命名空间
#(防碰真实槽 user://save_slot_N.json——真实档红线),营地闭合后恢复生命周期 attach 写盘目标
#(卡① sv3 无激活拒写——后续阶段断点写盘依赖)
sm.attach_slot_template("user://smoke_lifecycle_slot_%d.json")
sm.slots.attach_slot_template("user://smoke_lifecycle_slot_%d.json")
main._open_save_select()
for i in range(2):
await process_frame
@@ -544,7 +546,7 @@ func _smoke_lifecycle_matrix() -> void:
main._on_save_select_closed() # 关闭被栈还原的选档层(动线序中间态——卡④ 换代点)
for i in range(2):
await process_frame
sm.attach_path("user://smoke_lifecycle.json") # 恢复生命周期写盘目标
sm.slots.attach_path("user://smoke_lifecycle.json") # 恢复生命周期写盘目标
for suf in ["", ".tmp"]:
var slot_p: String = "user://smoke_lifecycle_slot_1.json" + suf
if FileAccess.file_exists(slot_p):
@@ -576,7 +578,7 @@ func _smoke_lifecycle_matrix() -> void:
for p in ["user://smoke_lifecycle_resume.json", "user://smoke_lifecycle_resume.json.tmp"]:
if FileAccess.file_exists(p):
DirAccess.remove_absolute(p)
sm.attach_path("user://smoke_lifecycle_resume.json")
sm.slots.attach_path("user://smoke_lifecycle_resume.json")
sm.set_breakpoint({
"master_seed": 42, "rng_counter": 0, "hero_hp": 20, "gp": 24, "keystone": 0,
"level_index": 1, "bonus": {}, "shop_purchased": {}, "relics": [],
@@ -53,7 +53,7 @@ func _harness(db):
var ctrl = DamageControllerScript.new(db, rng, units, effects)
ctrl.emit_row_cb = Callable(self, "_on_row")
effects.notify_battle_start()
var spawn_ok: bool = units.spawn(1, 0, int(db.hero.max_hp))
var spawn_ok: bool = units.spawn({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp)})
return {"db": db, "rng": rng, "units": units, "effects": effects, "ctrl": ctrl, "spawn_ok": spawn_ok}
@@ -79,15 +79,15 @@ func _run_recipe_sequence(db) -> Array:
for e in units.all_enemies():
enemy_hp_before.append(int(e.hp))
var front = units.default_target()
var threshold_expected: int = ctrl.defender_ac(front) + int(h.db.settlement.damage_roll.attack_bonus)
var threshold_expected: int = ctrl.defender_ac(front) + int(h.db.combat.settlement.damage_roll.attack_bonus)
var atk_type := ""
var eff_keys: Array = h.db.settlement.damage_roll.armor_effectiveness.keys()
var eff_keys: Array = h.db.combat.settlement.damage_roll.armor_effectiveness.keys()
if eff_keys.size() > 0:
atk_type = String(eff_keys[0])
var floor_v: int = int(h.db.settlement.damage_roll.damage_floor)
var floor_v: int = int(h.db.combat.settlement.damage_roll.damage_floor)
var t1: int = ctrl.submit(ctrl.open_ticket(DamageControllerScript.KIND_HERO_ATTACK, "hero_attack", "T1主角攻击",
{"channel": "战斗段", "base_value": int(h.db.hero.atk),
{"channel": "战斗段", "base_value": int(h.db.combat.hero.atk),
"target_rule": DamageControllerScript.TARGET_FRONT_ALIVE, "ri": 1}))
# 敌方技来源取后位敌(前位是攻击/直伤/群伤的靶位——来源与靶位分离,免死置干扰配方 redeemed 断言)
var alive_list: Array = units.query_alive("enemy")
@@ -124,9 +124,9 @@ func _run_recipe_sequence(db) -> Array:
var r1: Dictionary = _row_by_display_prefix("T1主角攻击")
var ex1: Dictionary = r1.get("extra", {})
Kit.check("主角攻击行 AC = 防守方 AC + 攻击加成(开单前快照一致)", int(ex1.get("ac", -1)) == threshold_expected)
Kit.check("掷点 ∈ [1, 骰面]", int(ex1.get("roll", -1)) >= 1 and int(ex1.get("roll", -1)) <= int(h.db.settlement.damage_roll.roll_faces))
Kit.check("掷点 ∈ [1, 骰面]", int(ex1.get("roll", -1)) >= 1 and int(ex1.get("roll", -1)) <= int(h.db.combat.settlement.damage_roll.roll_faces))
var tier_ok := false
for dv in h.db.settlement.damage_roll.roll_tier_discounts:
for dv in h.db.combat.settlement.damage_roll.roll_tier_discounts:
if int(dv) == int(ex1.get("tier", -1)):
tier_ok = true
if int(ex1.get("tier", -1)) == 100:
@@ -159,7 +159,7 @@ func _run_recipe_sequence(db) -> Array:
continue
if String(r.source_class).is_empty() or String(r.display).is_empty():
protocol_ok = false
if int(r.value) < int(h.db.settlement.damage_roll.damage_floor):
if int(r.value) < int(h.db.combat.settlement.damage_roll.damage_floor):
protocol_ok = false
if not (r.extra is Dictionary) or not r.extra.has("target"):
protocol_ok = false
@@ -187,11 +187,11 @@ func _test_policy_immediate_mode(db) -> void:
if not h.spawn_ok:
return
h.ctrl.policy = DamageControllerScript.POLICY_IMMEDIATE
var floor_v: int = int(h.db.settlement.damage_roll.damage_floor)
var floor_v: int = int(h.db.combat.settlement.damage_roll.damage_floor)
var t: int = h.ctrl.submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit", "A模式反噬",
{"channel": "棋盘兑付", "base_value": floor_v, "target_rule": DamageControllerScript.TARGET_HERO, "ri": 1}))
Kit.check("A 模式开单即结(无 flush 即 redeemed)", h.ctrl.outcome(t) == "redeemed")
Kit.check("A 模式主角即时扣血", int(h.units.hero_unit().hp) == int(h.db.hero.max_hp) - floor_v)
Kit.check("A 模式主角即时扣血", int(h.units.hero_unit().hp) == int(h.db.combat.hero.max_hp) - floor_v)
# ---- 来源敌死亡:队列中未结单据作废(死亡即不动手) ----
@@ -267,7 +267,7 @@ func _test_target_id_payload(db: ConfigDb) -> void:
var expect_id := String(target.get("enemy_id", ""))
Kit.check("前置:目标敌携非空 enemy_id", expect_id != "")
var ticket: Dictionary = h.ctrl.open_ticket(DamageControllerScript.KIND_HERO_ATTACK, "hero_attack", "T6目标id",
{"channel": "战斗段", "base_value": int(db.hero.atk),
{"channel": "战斗段", "base_value": int(db.combat.hero.atk),
"target_rule": DamageControllerScript.TARGET_FRONT_ALIVE, "ri": 1})
var fx_payload: Dictionary = ticket.context.get("fx_attack_payload", {})
Kit.check("attack_commit fx 载荷携 target_id〔wd12〕",
@@ -284,7 +284,7 @@ func _test_requires_gate_and_zero(db) -> void:
var h = _harness(db)
if not h.spawn_ok:
return
var floor_v: int = int(h.db.settlement.damage_roll.damage_floor)
var floor_v: int = int(h.db.combat.settlement.damage_roll.damage_floor)
# 先清场(flush_submit 同步开单——清图批专用通道的契约面同时被覆盖)
var front_hp: int = int(h.units.query_alive("enemy")[0].max_hp)
var big: int = front_hp * 10
@@ -294,7 +294,7 @@ func _test_requires_gate_and_zero(db) -> void:
var t_zero: int = h.ctrl.submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_BLAST, "clear_reward", "空场群伤",
{"channel": "棋盘兑付", "base_value": floor_v, "target_rule": DamageControllerScript.TARGET_ALL_ALIVE, "ri": 1}))
var gated_ticket: Dictionary = h.ctrl.open_ticket(DamageControllerScript.KIND_HERO_ATTACK, "hero_attack", "前置门攻击",
{"channel": "战斗段", "base_value": int(h.db.hero.atk),
{"channel": "战斗段", "base_value": int(h.db.combat.hero.atk),
"target_rule": DamageControllerScript.TARGET_FRONT_ALIVE, "ri": 1})
gated_ticket["requires"] = t_zero
var t_gated: int = h.ctrl.submit(gated_ticket)
@@ -310,7 +310,7 @@ func _test_hero_death_halt(db) -> void:
var h = _harness(db)
if not h.spawn_ok:
return
var max_hp: int = int(h.db.hero.max_hp)
var max_hp: int = int(h.db.combat.hero.max_hp)
var t_kill: int = h.ctrl.submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit", "致死反噬",
{"channel": "棋盘兑付", "base_value": max_hp, "target_rule": DamageControllerScript.TARGET_HERO, "ri": 1}))
var t_after: int = h.ctrl.submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit", "余单",
@@ -327,14 +327,14 @@ func _test_ac_merge(db) -> void:
var h = _harness(db)
if not h.spawn_ok:
return
var base_ac: int = int(h.db.hero.armor_class)
var base_ac: int = int(h.db.combat.hero.armor_class)
Kit.check("缺省兜底:defender_ac(hero) = hero 表值", h.ctrl.defender_ac(h.units.hero_unit()) == base_ac)
var override_v: int = base_ac + int(h.db.settlement.damage_roll.damage_floor)
var override_v: int = base_ac + int(h.db.combat.settlement.damage_roll.damage_floor)
h.ctrl.set_hero_ac_override(override_v)
Kit.check("override 合流入参生效", h.ctrl.defender_ac(h.units.hero_unit()) == override_v)
# ac 型状态效果叠其上(找 hero 侧 unit=ac 行)
var ac_eff := {}
for row in h.db.status_effects.rows:
for row in h.db.combat.status_effects.rows:
if String(row.get("unit", "")) == "ac" and String(row.get("target_side", "")) == "hero":
ac_eff = row
break
@@ -354,15 +354,15 @@ func _test_pool_clamp(db) -> void:
var h = _harness(db)
if not h.spawn_ok:
return
var lo: int = int(h.db.settlement.core.pool_clamp_lo)
var hi: int = int(h.db.settlement.core.pool_clamp_hi)
var lo: int = int(h.db.combat.settlement.core.pool_clamp_lo)
var hi: int = int(h.db.combat.settlement.core.pool_clamp_hi)
Kit.check("空效果池读 0(表 clamp 域含 0 前提)", h.ctrl.clamped_pool("hero_offense") == 0 and lo <= 0 and hi >= 0)
# 找 hero_offense 池的 percent 型 hero 侧效果行
var pool_eff := {}
for row in h.db.status_effects.rows:
for row in h.db.combat.status_effects.rows:
var eid := String(row.get("effect_id", ""))
if String(row.get("unit", "")) == "percent" and String(row.get("target_side", "")) == "hero" \
and String(h.db.settlement.core.effect_pool_map.get(eid, "")) == "hero_offense":
and String(h.db.combat.settlement.core.effect_pool_map.get(eid, "")) == "hero_offense":
pool_eff = row
break
if pool_eff.is_empty():
@@ -377,7 +377,7 @@ func _test_pool_clamp(db) -> void:
second_ok and h.ctrl.clamped_pool("hero_offense") == expected1)
# ac 型效果不入 percent 池
var ac_eff2 := {}
for row2 in h.db.status_effects.rows:
for row2 in h.db.combat.status_effects.rows:
if String(row2.get("unit", "")) == "ac" and String(row2.get("target_side", "")) == "hero":
ac_eff2 = row2
break
@@ -387,7 +387,7 @@ func _test_pool_clamp(db) -> void:
Kit.check("ac 型效果不入池(percent 池隔离)", h.ctrl.clamped_pool("hero_offense") == before)
# stack 政策效果:全新容器授予至满层 → 再授拒绝(满层拒绝原子——容器零写入)
var stack_eff := {}
for row3 in h.db.status_effects.rows:
for row3 in h.db.combat.status_effects.rows:
if String(row3.get("stack_policy", "")) == "stack":
stack_eff = row3
break
@@ -24,7 +24,7 @@ func _init() -> void:
func _floor_val() -> int:
return int(db.settlement.damage_roll.damage_floor)
return int(db.combat.settlement.damage_roll.damage_floor)
# ---- F2 乘区:池内加算 → 池间乘算 → floor(单一取整规则)----
@@ -32,11 +32,11 @@ func _floor_val() -> int:
func _test_pool_damage() -> void:
var d := _floor_val() + 9 # 表值推导的任意正探针量(非玩法行值)
Kit.check("中性双池 (0,0) 原样透传", DamageMathScript.pool_damage(d, 0, 0) == d)
Kit.check("负敌方池压低不增高", DamageMathScript.pool_damage(d, 0, int(db.settlement.core.pool_clamp_lo)) <= d)
Kit.check("正·主角池抬升不低于原值", DamageMathScript.pool_damage(d, int(db.settlement.core.pool_clamp_hi), 0) >= d)
Kit.check("负敌方池压低不增高", DamageMathScript.pool_damage(d, 0, int(db.combat.settlement.core.pool_clamp_lo)) <= d)
Kit.check("正·主角池抬升不低于原值", DamageMathScript.pool_damage(d, int(db.combat.settlement.core.pool_clamp_hi), 0) >= d)
Kit.check("主角池单调不减(+50% vs +49% 探针档)", DamageMathScript.pool_damage(d, 50, 0) >= DamageMathScript.pool_damage(d, 49, 0))
Kit.check("对称性:池值互换结果不变(乘算交换)", DamageMathScript.pool_damage(d, 30, -20) == DamageMathScript.pool_damage(d, -20, 30))
Kit.check("输出非负(maxi(0,·) 封底)", DamageMathScript.pool_damage(d, int(db.settlement.core.pool_clamp_lo), int(db.settlement.core.pool_clamp_lo)) >= 0)
Kit.check("输出非负(maxi(0,·) 封底)", DamageMathScript.pool_damage(d, int(db.combat.settlement.core.pool_clamp_lo), int(db.combat.settlement.core.pool_clamp_lo)) >= 0)
# ---- 克制分量:空桶不吃修正恒 0;落地下限保底([1,+∞) 全来源)----
@@ -54,8 +54,8 @@ func _test_counter_component() -> void:
func _test_roll_discount_percent() -> void:
var threshold := 10 # 查档探针阈值(夹具值——档位参数本身全部来自表)
var bounds: Array = db.settlement.damage_roll.roll_tier_bounds
var discounts: Array = db.settlement.damage_roll.roll_tier_discounts
var bounds: Array = db.combat.settlement.damage_roll.roll_tier_bounds
var discounts: Array = db.combat.settlement.damage_roll.roll_tier_discounts
Kit.check("前置:档位数 = 边界数 + 1(查档契约——档数 = 边界数 + 1)", discounts.size() == bounds.size() + 1 and discounts.size() > 0)
Kit.check("掷点 ≥ 阈值恒全额 100", DamageMathScript.roll_discount_percent(threshold, threshold, bounds, discounts) == 100
and DamageMathScript.roll_discount_percent(threshold + 5, threshold, bounds, discounts) == 100)
@@ -78,7 +78,7 @@ func _test_roll_discount_percent() -> void:
func _test_rolled_component() -> void:
var base := float(_floor_val() + 7) # 表值推导探针基线
var crit_mult: int = int(db.settlement.damage_roll.crit_multiplier)
var crit_mult: int = int(db.combat.settlement.damage_roll.crit_multiplier)
Kit.check("满额折扣无暴击 = floor(基线) + 修正 0", DamageMathScript.rolled_component(base, 100, false, crit_mult, 0) == int(floor(base)))
Kit.check("满额折扣暴击按表倍率放大", DamageMathScript.rolled_component(base, 100, true, crit_mult, 0) == int(floor(base * float(crit_mult) / 100.0)))
Kit.check("折扣降低不高于满额", DamageMathScript.rolled_component(base, 50, false, crit_mult, 0) <= DamageMathScript.rolled_component(base, 100, false, crit_mult, 0))
@@ -47,13 +47,13 @@ func _smoke_enemy_attack_type() -> void:
# ① 表列:20 行全填 normal(N3 全填不做差异化——Q9)
var at_ok := true
var at_detail := ""
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
var at := String(row.get("attack_type", ""))
if at != "normal":
at_ok = false
at_detail += " %s:%s" % [String(row.get("enemy_id", "?")), at]
Kit.check("敌方 %d 行 attack_type 全填 normal(迭代 24 N3 轴开列,越轨行%s)" % [
_db.enemy.rows.size(), at_detail if at_detail != "" else "无"], at_ok)
_db.combat.enemy.rows.size(), at_detail if at_detail != "" else "无"], at_ok)
# ②③④ 矩阵命中证词扫描:(3,0) 灰矮人 atk 区间 [2,4] 基线足够((1,2) 熊地精 atk 1 会塌地板);
# 同 seed 三甲型各开一场、各取首个敌方伤害行,对拍 ±1。
var matrix_found := false
@@ -70,7 +70,7 @@ func _smoke_enemy_attack_type() -> void:
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
var rows: Array = []
settlement.step_row.connect(func(row): rows.append(row))
if not settlement.begin_battle({"level_index": 3, "node_depth": 0, "hero_hp": int(_db.hero.max_hp), "armor_type": armor}):
if not settlement.begin_battle({"level_index": 3, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp), "armor_type": armor}):
totals[armor] = -1
continue
if armor == "light":
@@ -109,7 +109,7 @@ func _smoke_armor_roll() -> void:
var dist := {100: 0, 75: 0, 50: 0, 25: 0}
var tier_ok := true
for roll in range(1, 21):
var d: int = DamageMath.roll_discount_percent(roll, cfg.ac, _db.settlement.damage_roll.roll_tier_bounds, _db.settlement.damage_roll.roll_tier_discounts)
var d: int = DamageMath.roll_discount_percent(roll, cfg.ac, _db.combat.settlement.damage_roll.roll_tier_bounds, _db.combat.settlement.damage_roll.roll_tier_discounts)
if not dist.has(d):
tier_ok = false
continue
@@ -117,7 +117,7 @@ func _smoke_armor_roll() -> void:
Kit.check("查档 AC%d 全掷点域分布(100/75/50/25 = %s)" % [cfg.ac, str([dist[100], dist[75], dist[50], dist[25]])], tier_ok and dist == cfg.expect)
var ev_sum := 0
for roll in range(1, 21):
ev_sum += DamageMath.roll_discount_percent(roll, 10, _db.settlement.damage_roll.roll_tier_bounds, _db.settlement.damage_roll.roll_tier_discounts)
ev_sum += DamageMath.roll_discount_percent(roll, 10, _db.combat.settlement.damage_roll.roll_tier_bounds, _db.combat.settlement.damage_roll.roll_tier_discounts)
Kit.check("AC10 EV 锚:期望系数合计 %d/2000 = 期望减免 21.25%%(nat20 恒全额含内)" % ev_sum, ev_sum == 1575)
# ② nat20 暴击(行为面):探针按行为面扫描
# 「主角先手首掷 = 20」的种子,不假设 armor_roll scope0 位
@@ -125,20 +125,21 @@ func _smoke_armor_roll() -> void:
var crit_rows := {}
for s in range(1, 800):
crit_rows = _first_hero_attack_rows(s)
if int(crit_rows.get("roll", -1)) == int(_db.settlement.damage_roll.crit_roll):
if int(crit_rows.get("roll", -1)) == int(_db.combat.settlement.damage_roll.crit_roll):
crit_seed = s
break
Kit.check("nat20 → 行留档 roll=20 + crit 标记(主角先手普通分量——种子可达 seed=%d)" % crit_seed,
int(crit_rows.get("roll", -1)) == int(_db.settlement.damage_roll.crit_roll) and bool(crit_rows.get("crit", false)))
Kit.check("nat20 → 普通分量 ×2(%d = 2×%d,暴击倍率只乘该分量,穿刺/魔法不吃)" % [int(crit_rows.get("normal", -1)), int(_db.hero.atk)],
int(crit_rows.get("normal", -1)) == 2 * int(_db.hero.atk) and int(crit_rows.get("value", -1)) == 2 * int(_db.hero.atk))
Kit.check("nat20 种子可达(seed=%d——主角先手首掷命中 %d)" % [crit_seed, int(_db.combat.settlement.damage_roll.crit_roll)], crit_seed > 0)
Kit.check("nat20 → 行留档 roll=20 + crit 标记(主角先手普通分量)",
int(crit_rows.get("roll", -1)) == int(_db.combat.settlement.damage_roll.crit_roll) and bool(crit_rows.get("crit", false)))
Kit.check("nat20 → 普通分量 ×2(%d = 2×%d,暴击倍率只乘该分量,穿刺/魔法不吃)" % [int(crit_rows.get("normal", -1)), int(_db.combat.hero.atk)],
int(crit_rows.get("normal", -1)) == 2 * int(_db.combat.hero.atk) and int(crit_rows.get("value", -1)) == 2 * int(_db.combat.hero.atk))
# ③ 削弱续时(行为面):授予→衰减一轮→再授予 → 层数保持 1、剩余结算数重置
# (削弱为续时语义,无叠层通道)
var weaken_probe := _weaken_refresh_probe()
Kit.check("削弱重复授予 → 层数保持 1、剩余结算数重置 2(续时语义——结构性消饱和)",
int(weaken_probe.get("layers", -1)) == 1 and int(weaken_probe.get("remaining", -1)) == 2)
Kit.check("削弱单层池读数 = 行值(enemy_output 池单层贡献——×2 叠满触底 ×0 不可达,免伤轮根治)",
int(weaken_probe.get("pool", -999)) == int(_db.status_effect_row("weaken").magnitude))
int(weaken_probe.get("pool", -999)) == int(_db.combat.status_effects.row_by_id("weaken").magnitude))
# ④ AC11 折扣档分布(行为面):ac_up 逐轮授给(时效 1)→ Boss 行攻击拍逐拍判定
# (ac_up 层数恒 1——局内可达 AC 上限 10+1=11,探针随行取值)
var tiers := _ac11_tier_samples()
@@ -171,7 +172,7 @@ func _smoke_armor_roll() -> void:
Kit.check("棋盘来源伤害行零掷点字段(爆裂/直伤/清障炸药/奖励即时必中全额)", board_ok)
# ⑥ 全来源伤害下限(任何伤害落地取值域 [1,+∞) 无上限——棋盘克制直伤/反噬/地形变异/爆裂
# 全部收在 damage_floor 闸内)
var df: int = int(_db.settlement.damage_roll.damage_floor)
var df: int = int(_db.combat.settlement.damage_roll.damage_floor)
Kit.check("棋盘克制直伤贴底 → 下限 1(点数 2 vs 不利修正 −1)", DamageMath.counter_component(2, -1, df) == 1)
Kit.check("克制修正触底穿透 → 下限 1(点数 1 vs 修正 −3)", DamageMath.counter_component(1, -3, df) == 1)
Kit.check("空桶不吃修正恒 0(零点数分量无伤害可落地——边界不变不受下限影响)", DamageMath.counter_component(0, 5, df) == 0)
@@ -184,7 +185,7 @@ func _first_hero_attack_rows(seed_val: int) -> Dictionary:
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
var rows: Array = []
settlement.step_row.connect(func(row): rows.append(row))
if not settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)}):
if not settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)}):
return {}
settlement.minesweeper.settle_batch([{"action": "stop"}])
for row in rows:
@@ -200,7 +201,7 @@ func _weaken_refresh_probe() -> Dictionary:
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(20260916)
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
if not settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.hero.max_hp)}):
if not settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.combat.hero.max_hp)}):
return {}
var src: Dictionary = {"source_class": "mine_hit", "origin_cell": Vector2i(0, 0), "round_index": settlement.round_index}
settlement._status_effects.apply_effect("weaken", src)
@@ -223,7 +224,7 @@ func _ac11_tier_samples() -> Array:
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
var rows: Array = []
settlement.step_row.connect(func(row): rows.append(row))
if not settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.hero.max_hp)}):
if not settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.combat.hero.max_hp)}):
continue
var guard := 12
while settlement.battle_result == "" and guard > 0:
@@ -258,7 +259,7 @@ func _run_battle(L: int, D: int, axes: Dictionary = {}) -> Dictionary:
settlement.minesweeper.seal_judged.connect(func(evt): out.events.append(evt))
settlement.minesweeper.round_end_board.connect(func(p): out.events.append({"ev": "round_end_board", "path_tag": p.path_tag}))
settlement.action_economy.round_end_stopped.connect(func(p): out.events.append({"ev": "round_end_stopped", "path_tag": p.path_tag}))
var enc := {"level_index": L, "node_depth": D, "hero_hp": _db.hero.max_hp}
var enc := {"level_index": L, "node_depth": D, "hero_hp": _db.combat.hero.max_hp}
enc.merge(axes, true) # 两根类型轴注入尾参(缺省零注入=回退面)
if not settlement.begin_battle(enc):
out.stream.append({"type": "verdict", "value": "begin_failed"})
@@ -324,7 +325,7 @@ func _hero_attack_row_with(opts: Dictionary, hero_atk_val: int, seed_val: int) -
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
var rows: Array = []
settlement.step_row.connect(func(row): rows.append(row))
if not settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp), "hero_atk": hero_atk_val}):
if not settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp), "hero_atk": hero_atk_val}):
return {}
for eff in opts.get("effects", []):
settlement._status_effects.apply_effect(String(eff), {"source_class": "mine_hit", "origin_cell": Vector2i(0, 0), "round_index": settlement.round_index})
@@ -342,15 +343,15 @@ func _smoke_damage_types() -> void:
# ① 护甲类型快照传播(spawn 行值 → 单位字典)
var enemy_system = load("res://core/unit_container.gd").new(_db)
Kit.check("goblin 护甲 = light(轻甲——教学位)",
enemy_system.spawn(1, 0, int(_db.hero.max_hp)) and String(enemy_system.all_enemies()[0].armor_type) == "light")
enemy_system.spawn({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)}) and String(enemy_system.all_enemies()[0].armor_type) == "light")
var enemy_system_b = load("res://core/unit_container.gd").new(_db)
Kit.check("bugbear 护甲 = light(护甲类型重配批 2026-09-18:关卡 1 全轻甲)",
enemy_system_b.spawn(1, 2, int(_db.hero.max_hp)) and String(enemy_system_b.all_enemies()[0].armor_type) == "light")
enemy_system_b.spawn({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.combat.hero.max_hp)}) and String(enemy_system_b.all_enemies()[0].armor_type) == "light")
var enemy_system_c = load("res://core/unit_container.gd").new(_db)
Kit.check("ogre_chief 护甲 = light(Boss 行集——护甲类型重配批 2026-09-18:关卡 1 全轻甲)",
enemy_system_c.spawn(1, 4, int(_db.hero.max_hp), [], -1, -1, [], "", "boss") and String(enemy_system_c.all_enemies()[0].armor_type) == "light")
enemy_system_c.spawn({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "hero_armor_type": "boss"}) and String(enemy_system_c.all_enemies()[0].armor_type) == "light")
# ② 克制矩阵表值在位(三档点数 克制 +1 / 中性 0 / 被克 −1,加法核非 percent)
var am: Dictionary = _db.settlement.damage_roll.armor_effectiveness
var am: Dictionary = _db.combat.settlement.damage_roll.armor_effectiveness
Kit.check("普通行:轻甲 0 / 魔法护甲 +1 / 重甲 −1(加法核)",
int(am.normal.light) == 0 and int(am.normal.ward) == 1 and int(am.normal.heavy) == -1)
Kit.check("穿刺行:轻甲 −1 / 魔法护甲 +1 / 重甲 0 中性(用户拍板)",
@@ -386,7 +387,7 @@ func _run_seal_backlash_round(seed_val: int) -> Dictionary:
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
var rows: Array = []
settlement.step_row.connect(func(row): rows.append(row))
if not settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}):
if not settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}):
return {"rows": rows, "safe_cell": Vector2i(-1, -1), "safe_type": ""}
var minesweeper = settlement.minesweeper
var safe_t := _find_seal_target(minesweeper, false)
@@ -442,7 +443,7 @@ func _smoke_attack_chain_fresh_pool() -> void:
ticks.append({"baseline": int(row.baseline), "attach": int(row.get("rider", 0)), "roll": int(row.roll),
"tier": int(row.tier), "crit": bool(row.get("crit", false)), "tick": int(row.value)}))
Kit.check("fresh 池回归战斗启动(Boss 行)",
settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}))
settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}))
# 停手轮:零挖掘零封印 → 零授予 → 池恒空;Boss 拍 = 开轮摇号结果(伤害技/普攻兑付伤害行、硬化施放行不计)
for i in range(6):
if settlement.battle_result != "":
@@ -451,9 +452,9 @@ func _smoke_attack_chain_fresh_pool() -> void:
Kit.check("非施放攻击拍采样充足(%d 拍——增益施放行零兑付为设计语义不计)" % ticks.size(), ticks.size() >= 2)
var all_full := true
for t in ticks:
var base_factor: float = float(t.tier) * (float(_db.settlement.damage_roll.crit_multiplier) / 100.0 if t.crit else 1.0)
var base_factor: float = float(t.tier) * (float(_db.combat.settlement.damage_roll.crit_multiplier) / 100.0 if t.crit else 1.0)
var rolled_base := int(floor(float(t.baseline) * base_factor / 100.0))
var expected: int = maxi(int(_db.settlement.damage_roll.damage_floor), rolled_base + int(t.get("attach", 0)))
var expected: int = maxi(int(_db.combat.settlement.damage_roll.damage_floor), rolled_base + int(t.get("attach", 0)))
if int(t.tick) != expected or int(t.tick) <= 0:
all_full = false
Kit.check("fresh 池非蓄力拍 d_tick = max(1, 掷点档位基线)(Boss 攻击链健康——Bug#2 回归断言)", all_full)
@@ -58,6 +58,7 @@ func _smoke_full_battle(L: int, D: int, label: String) -> void:
break
Kit.check("%s 回放断言:结算步骤流逐行相同" % label, JSON.stringify(run1.stream) == JSON.stringify(run2.stream))
Kit.check("%s 回放断言:事件流逐位相同" % label, JSON.stringify(run1.events) == JSON.stringify(run2.events))
Kit.check("%s 结算步骤流非空(%d 行)" % [label, run1.stream.size()], run1.stream.size() > 0)
var has_verdict := false
var hero_attacks := 0
var hero_breakdown_ok := true
@@ -68,7 +69,7 @@ func _smoke_full_battle(L: int, D: int, label: String) -> void:
hero_attacks += 1
# 单分量伤害构成行:hero_attack 行必须携带实际攻击类型键 + 掷点读数(P2 归因)
# 类型键主语 = 锻造工坊配置默认值(hero.tres 无两轴字段;本夹具零注入)
var atk_t: String = String(_db.forge_default_attack_type())
var atk_t: String = String(_db.crossrun.forge.default_attack_type)
if not (row.has(atk_t) and row.has("roll") and row.has("ac") and row.has("tier")):
hero_breakdown_ok = false
Kit.check("%s 步骤流含判定行" % label, has_verdict)
@@ -92,7 +93,7 @@ func _run_battle(L: int, D: int, axes: Dictionary = {}) -> Dictionary:
settlement.action_economy.round_end_stopped.connect(func(p): out.events.append({"ev": "round_end_stopped", "path_tag": p.path_tag}))
var minesweeper = settlement.minesweeper
var action_economy = settlement.action_economy
var enc := {"level_index": L, "node_depth": D, "hero_hp": _db.hero.max_hp}
var enc := {"level_index": L, "node_depth": D, "hero_hp": _db.combat.hero.max_hp}
enc.merge(axes, true) # 两根类型轴注入尾参(缺省零注入=回退面)
if not settlement.begin_battle(enc):
out.stream.append({"type": "verdict", "value": "begin_failed"})
@@ -156,12 +157,12 @@ func _smoke_active_refresh() -> void:
if String(row.get("type", "")) == "decay" and not bool(ct["waiting_probe_hit"]):
ct["waiting_probe_hit"] = true
ct["waiting_refresh_rejected"] = not settlement.player_request_refresh())
Kit.check("轮内刷新战斗启动(Boss 行 150HP——承载多轮样本)", settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}))
Kit.check("轮内刷新战斗启动(Boss 行 150HP——承载多轮样本)", settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}))
var minesweeper = settlement.minesweeper
# 轮 1–3:零挖掘停手攒 RAP = 3×AP(每轮全转存;表驱动——AP 从表读不写死)
for i in range(3):
minesweeper.settle_batch([{"action": "stop"}])
var rap_banked: int = 3 * int(_db.action_economy.ap_per_round)
var rap_banked: int = 3 * int(_db.run.action_economy.ap_per_round)
Kit.check("三轮停手攒 RAP=%d(round=%d rap=%d)" % [rap_banked, settlement.round_index, settlement.action_economy.rap], settlement.round_index == 4 and settlement.action_economy.rap == rap_banked)
Kit.check("第 4 轮 Active 相位", settlement.action_economy.phase == "Active")
# 挖 1 格 → 刷 → 挖 1 格 → 刷 → 挖 1 格 → 第 3 刷拒(上限)
@@ -175,7 +176,7 @@ func _smoke_active_refresh() -> void:
var ok1: bool = settlement.player_request_refresh()
Kit.check("轮内第 1 次刷新受理", ok1)
Kit.check("相位保持 Active、AP 保留不重置(ap=%d)" % settlement.action_economy.ap, settlement.action_economy.phase == "Active" and settlement.action_economy.ap == ap_before)
Kit.check("RAP -刷新价(%d)" % settlement.action_economy.rap, settlement.action_economy.rap == rap_banked - int(_db.action_economy.rap_refresh_cost))
Kit.check("RAP -刷新价(%d)" % settlement.action_economy.rap, settlement.action_economy.rap == rap_banked - int(_db.run.action_economy.rap_refresh_cost))
Kit.check("当场换新图(board_changed +1)", int(ct["board_swaps"]) == swaps_before + 1 and minesweeper.current_board() != board_before)
var plan2 = AutoPlayer.choose_dig(minesweeper)
if plan2.hero_cell != minesweeper.hero_pos():
@@ -220,7 +221,7 @@ func _active_refresh_replay() -> Array:
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
var rows: Array = []
settlement.step_row.connect(func(row): rows.append(row))
if not settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}):
if not settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}):
return rows
var minesweeper = settlement.minesweeper
for i in range(3):
@@ -250,7 +251,7 @@ func _smoke_encounter_roll() -> void:
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(30000 + i)
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
Kit.check("遭遇扫描战斗启动(quiet 计数档——计数入总账,成败由后续 spawn 断言归因)", settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)})) # 源 quiet=true 计数档——Kit 通道无此参(只影响 PASS 行打印,计数/红绿同口径)
Kit.check("遭遇扫描战斗启动(quiet 计数档——计数入总账,成败由后续 spawn 断言归因)", settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)})) # 源 quiet=true 计数档——Kit 通道无此参(只影响 PASS 行打印,计数/红绿同口径)
counts.append(settlement.presentation_snapshot().enemies.size())
# 区间生效:20 个种子应出现 1 和 2(D0 [1,2])
Kit.check("D0 区间 [1,2] 两值均出现(分布 %s)" % str(counts), counts.has(1) and counts.has(2))
@@ -268,13 +269,13 @@ func _smoke_encounter_roll() -> void:
var rng3 = load("res://core/rng_service.gd").new()
rng3.set_master_seed(31000)
var settlement_d1 = load("res://core/battle_orchestrator.gd").new(_db, rng3)
Kit.check("D1 战斗启动", settlement_d1.begin_battle({"level_index": 1, "node_depth": 1, "hero_hp": int(_db.hero.max_hp)}))
Kit.check("D1 战斗启动", settlement_d1.begin_battle({"level_index": 1, "node_depth": 1, "hero_hp": int(_db.combat.hero.max_hp)}))
var n1: int = settlement_d1.presentation_snapshot().enemies.size()
Kit.check("D1 区间 [2,3] 域内(%d 只)" % n1, n1 >= 2 and n1 <= 3)
# 熊地精首遇带固定(教学位不加骰)——首遇带数据驱动定位(契约 8,禁写死坐标)
var bugbear_lead_band := _first_encounter_band(1, "bugbear")
var bugbear_lead_row: Dictionary = {}
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
if int(row.level_index) == 1 and String(row.row_kind) == "battle" \
and int(row.depth_band) == bugbear_lead_band and String(row.enemy_id) == "bugbear":
bugbear_lead_row = row
@@ -282,7 +283,7 @@ func _smoke_encounter_roll() -> void:
var rng4 = load("res://core/rng_service.gd").new()
rng4.set_master_seed(31000)
var settlement_d2 = load("res://core/battle_orchestrator.gd").new(_db, rng4)
settlement_d2.begin_battle({"level_index": 1, "node_depth": bugbear_lead_band, "hero_hp": int(_db.hero.max_hp)})
settlement_d2.begin_battle({"level_index": 1, "node_depth": bugbear_lead_band, "hero_hp": int(_db.combat.hero.max_hp)})
Kit.check("熊地精首遇带(1,battle,%d——扫表取最低带)固定 1 不加骰〔fv23〕" % bugbear_lead_band,
settlement_d2.presentation_snapshot().enemies.size() == 1 \
and int(bugbear_lead_row.get("unit_count", -1)) == 1 and not bugbear_lead_row.has("unit_count_max"))
@@ -299,11 +300,11 @@ func _smoke_encounter_roll() -> void:
for depth_v in depths:
scan_total += 1
var settlement_scan = load("res://core/battle_orchestrator.gd").new(_db, rngs)
if not settlement_scan.begin_battle({"level_index": 1, "node_depth": int(depth_v), "hero_hp": int(_db.hero.max_hp)}):
if not settlement_scan.begin_battle({"level_index": 1, "node_depth": int(depth_v), "hero_hp": int(_db.combat.hero.max_hp)}):
scan_bad += 1
continue
var n: int = settlement_scan.presentation_snapshot().enemies.size()
var erow2: Dictionary = _db.resolve_enemy_rowset(1, int(depth_v), "battle")[0] # 行集首行(扫描面 = 战斗节点)
var erow2: Dictionary = _db.combat.enemy.resolve_rowset(1, int(depth_v), "battle")[0] # 行集首行(扫描面 = 战斗节点)
var lo2: int = int(erow2.get("unit_count", 1))
var hi2: int = int(erow2.get("unit_count_max", lo2))
if n < 1 or n < lo2 or n > hi2:
@@ -328,7 +329,7 @@ func _row_kind_of(level_index: int, depth: int) -> String:
# 首遇带数据驱动定位(契约 8):该 (L, battle, enemy_id) 的最低 depth_band,禁写死坐标数字
func _first_encounter_band(level_index: int, enemy_id: String) -> int:
var best := -1
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
if int(row.get("level_index", -1)) != level_index:
continue
if String(row.get("row_kind", "")) != "battle" or String(row.get("enemy_id", "")) != enemy_id:
@@ -344,12 +345,12 @@ func _level_active_nodes(level_index: int) -> Array:
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(42)
var gen = NodeMapGenerator.new(rng)
return (gen.generate(_db.level_gen_params_row(level_index), 42) as Dictionary).get("nodes", [])
return (gen.generate(_db.run.level_gen_params.row_by_level_index(level_index), 42) as Dictionary).get("nodes", [])
func _level_battle_nodes(level_index: int) -> Array:
var out: Array = []
for row in _db.level.rows:
for row in _db.run.level.rows:
if int(row.level_index) == level_index:
for node in _level_active_nodes(level_index):
var t := String(node.get("type", ""))
@@ -363,7 +364,7 @@ func _encounter_replay(seed_val: int) -> Array:
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(seed_val)
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)})
settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)})
out.append(settlement.presentation_snapshot().enemies.size())
return out
@@ -377,7 +378,7 @@ func _encounter_replay(seed_val: int) -> Array:
func _smoke_settlement_segments() -> void:
print("--- 五段全序 + 行型封闭 ---")
Kit.check("segment_order 五段冻结在位(表值 = 拍板序恒等)",
Array(_db.settlement.segment_order) == SettlementTable.FROZEN_SEGMENT_ORDER
Array(_db.combat.settlement.segment_order) == SettlementTable.FROZEN_SEGMENT_ORDER
and SettlementTable.FROZEN_SEGMENT_ORDER == ["grant", "buff_debuff", "combat", "decay", "verdict"])
var run = _run_battle(1, 0)
var legacy := {"item": 0, "consumed_ineffective": 0, "use_rejected": 0, "window_open": 0, "window_skip": 0}
@@ -492,53 +493,53 @@ func _smoke_skill_pool() -> void:
# ① 数据面:技能表行集 + 普攻锁死 + 建议池行值 + 空池不可能(结构断言)
func _smoke_skill_pool_data() -> void:
Kit.check("技能表十六行(普攻/突刺/蓄力/重击/硬化 + 撕咬/毒爆/蛛网束缚 + 大地脉动 + 震地怒击/遏毒/岩崩 + 毒吐/盾击——前三批 + Boss 半血解锁技三行〔迭代14④ T5〕+ 普通敌错峰两行〔迭代 24 N2〕+ 触发条件批示范两行〔孤注守势/趁隙偷袭〕)", _db.skill.rows.size() == 16)
var basic: Dictionary = _db.skill.row_by_id("attack")
Kit.check("技能表十六行(普攻/突刺/蓄力/重击/硬化 + 撕咬/毒爆/蛛网束缚 + 大地脉动 + 震地怒击/遏毒/岩崩 + 毒吐/盾击——前三批 + Boss 半血解锁技三行〔迭代14④ T5〕+ 普通敌错峰两行〔迭代 24 N2〕+ 触发条件批示范两行〔孤注守势/趁隙偷袭〕)", _db.combat.skill.rows.size() == 16)
var basic: Dictionary = _db.combat.skill.row_by_id("attack")
Kit.check("普攻行:倍率恒 1 · 冷却恒 0(域校验锁死——池恒非空的结构前提)",
not basic.is_empty() and String(basic.type) == "basic" and int(basic.cooldown_beats) == 0
and (not basic.has("damage_mult") or is_equal_approx(float(basic.damage_mult), 1.0)))
var lunge: Dictionary = _db.skill.row_by_id("lunge")
var lunge: Dictionary = _db.combat.skill.row_by_id("lunge")
Kit.check("突刺行(伤害 ×1.5 · 冷却 2——提案 §2.5 建议池新行)", not lunge.is_empty() and is_equal_approx(float(lunge.damage_mult), 1.5) and int(lunge.cooldown_beats) == 2)
var charge_row: Dictionary = _db.skill.row_by_id("charge")
var charge_row: Dictionary = _db.combat.skill.row_by_id("charge")
Kit.check("蓄力行 = 纯攻击增益技(0 伤害、buff_ref → empower——与重击无组合语义)", not charge_row.is_empty() and String(charge_row.buff_ref) == "empower")
var bite: Dictionary = _db.skill.row_by_id("bite")
var bite: Dictionary = _db.combat.skill.row_by_id("bite")
Kit.check("撕咬行(伤害 ×1.5 · 冷却 1——巨蜘蛛池载体,与突刺同倍率错开冷却轴)", not bite.is_empty() and is_equal_approx(float(bite.damage_mult), 1.5) and int(bite.cooldown_beats) == 1)
var venom: Dictionary = _db.skill.row_by_id("venom_burst")
var venom: Dictionary = _db.combat.skill.row_by_id("venom_burst")
Kit.check("毒爆行(伤害 ×2.5 · 冷却 4 · 必中附加 ×0.75——蛛化卓尔精灵专属池独占技,新效应词汇批)",
not venom.is_empty() and is_equal_approx(float(venom.damage_mult), 2.5) and int(venom.cooldown_beats) == 4
and is_equal_approx(float(venom.rider_mult), 0.75))
var venom_sole_ok := true
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
for entry in row.pool:
if String(entry.skill_id) == "venom_burst" and String(row.enemy_id) != "drider":
venom_sole_ok = false
Kit.check("毒爆独占性(仅蛛化卓尔精灵池引用——Boss 专属池独占技纪律)", venom_sole_ok)
# 妨害技首发行 + off_guard 落点 + 实装面收窄
var snare: Dictionary = _db.skill.row_by_id("web_snare")
var snare: Dictionary = _db.combat.skill.row_by_id("web_snare")
Kit.check("蛛网束缚行(妨害型 · buff_ref → off_guard 失衡 · duration 2 = 1 有效拍 · 冷却 4——新效应词汇批首发)",
not snare.is_empty() and String(snare.type) == "debuff" and String(snare.buff_ref) == "off_guard"
and int(snare.buff_duration) == 2 and int(snare.cooldown_beats) == 4)
var snare_sole_ok := true
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
for entry in row.pool:
if String(entry.skill_id) == "web_snare" and String(row.enemy_id) != "drider":
snare_sole_ok = false
Kit.check("蛛网束缚独占性(仅蛛化卓尔精灵池引用——实装面收窄,其余敌种不动)", snare_sole_ok)
var off_guard_row: Dictionary = _db.status_effect_row("off_guard")
var off_guard_row: Dictionary = _db.combat.status_effects.row_by_id("off_guard")
Kit.check("失衡状态行(hero · ac 轴——妨害技落点,ConfigDB side 守卫正例)", not off_guard_row.is_empty() and String(off_guard_row.target_side) == "hero" and String(off_guard_row.unit) == "ac")
# 普通敌错峰两新行——毒吐/盾击(V3)
var spit: Dictionary = _db.skill.row_by_id("venom_spit")
var spit: Dictionary = _db.combat.skill.row_by_id("venom_spit")
Kit.check("毒吐行(伤害 ×1.6 · 冷却 2 · 无 rider——低 atk 下 rider 恒被 damage_floor 兜成 1 白占字段,迭代 24 N2)",
not spit.is_empty() and String(spit.display_name) == "毒吐" and String(spit.type) == "damage" \
and is_equal_approx(float(spit.damage_mult), 1.6) and int(spit.cooldown_beats) == 2 and not spit.has("rider_mult"))
var bash: Dictionary = _db.skill.row_by_id("shield_bash")
var bash: Dictionary = _db.combat.skill.row_by_id("shield_bash")
Kit.check("盾击行(伤害 ×1.7 · 冷却 3 · 无 rider——迭代 24 N2)",
not bash.is_empty() and String(bash.display_name) == "盾击" and String(bash.type) == "damage" \
and is_equal_approx(float(bash.damage_mult), 1.7) and int(bash.cooldown_beats) == 3 and not bash.has("rider_mult"))
var spider_lead_band := _first_encounter_band(2, "giant_spider") # 首遇带数据驱动定位(契约 8)
var spit_sole_ok := true # 毒吐只挂巨蜘蛛族且首遇带不投(Q6——教学位池零新招)
var bash_sole_ok := true # 盾击只挂灰矮人族三行
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
for entry in row.pool:
var sid := String(entry.skill_id)
if sid == "venom_spit" and not (String(row.enemy_id) == "giant_spider" and int(row.level_index) == 2 \
@@ -550,7 +551,7 @@ func _smoke_skill_pool_data() -> void:
Kit.check("盾击投放面(仅 (3,·) 灰矮人族——钩爪恐魔零触碰,迭代 24 N2)", bash_sole_ok)
# 首遇带行池恒 {attack:6, bite:2, charge:1}(去写死深度判据——带键口径)
var spider_lead_pool := {}
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
if int(row.level_index) == 2 and String(row.row_kind) == "battle" \
and int(row.depth_band) == spider_lead_band and String(row.enemy_id) == "giant_spider":
for entry in row.pool:
@@ -591,7 +592,7 @@ func _smoke_skill_pool_data() -> void:
"3_boss_13_duergar_king": {"attack": 6, "heavy": 2, "earth_pulse": 2, "rockslide": 2},
}
var pools_ok := true
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
var rk := "%d_%s_%d_%s" % [int(row.level_index), String(row.row_kind), int(row.depth_band), String(row.enemy_id)]
if not expect_pools.has(rk):
pools_ok = false
@@ -603,10 +604,10 @@ func _smoke_skill_pool_data() -> void:
pools_ok = false
Kit.check("二十八行九种池模板 = 建议池行值((L,row_kind,depth_band,enemy_id) 四元键逐行精确比对;L1 地精 9/1/1〔触发条件批+孤注守势 w1〕 · 熊地精 6/2/2 · 酋长 6/2/2;L2 熊地精 6/1/2 · 蜘蛛 6/2/1+毒吐2〔首遇带不投——迭代 24 N2〕 · 重击手 5/2/2 · 蛛化卓尔精灵 6/2/2/2〔Boss〕;L3 灰矮人 6/2/2+盾击2 · 钩爪恐魔 6/2/1/2〔触发条件批+趁隙偷袭 w2〕 · 重装手 5/2/2+盾击2 · 石君王 6/2/2〔earth_pulse〕——全部 provisional;三 Boss 池尾 +1 门控条目〔quake_smash/venom_curb/rockslide 权重 2——半血解锁收编技能行 trigger 闩型——迭代14④ T5/触发条件批迁移〕;混编行集同带多行各一池合法——首批投放 (2,battle,5) 双行〔迭代 18 卡⑤:重击手+巢群〕;迭代 35 覆盖批八行为顶档池克隆,逐行同检)", pools_ok)
var basic_count_ok := true
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
var n := 0
for entry in row.pool:
var srow: Dictionary = _db.skill_row(String(entry.skill_id))
var srow: Dictionary = _db.combat.skill.row_by_id(String(entry.skill_id))
if not srow.is_empty() and String(srow.type) == "basic":
n += 1
if n != 1:
@@ -615,11 +616,11 @@ func _smoke_skill_pool_data() -> void:
# 池构成纪律复查(V4):零违反——增益 ≤1 / 妨害 ≤1(敌人系统 v0.13 结构冻结;载入期
# validate_skill_pool_links 已硬闸,此处冒烟侧显式复核新投放后仍绿)
var comp_ok := true
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
var n_buff := 0
var n_debuff := 0
for entry in row.pool:
var srow: Dictionary = _db.skill_row(String(entry.skill_id))
var srow: Dictionary = _db.combat.skill.row_by_id(String(entry.skill_id))
if srow.is_empty():
continue
if String(srow.type) == "buff":
@@ -631,10 +632,10 @@ func _smoke_skill_pool_data() -> void:
Kit.check("逐池增益 ≤1 · 妨害 ≤1(池构成纪律——迭代 24 N2 毒吐/盾击投放后复查)", comp_ok)
# 池构成纪律复查(触发条件批 V7 冒烟侧):每池至少一行无条件特殊技(普攻不计;trigger 缺省行计数)
var uncond_ok := true
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
var n_uncond := 0
for entry in row.pool:
var srow_u: Dictionary = _db.skill_row(String(entry.skill_id))
var srow_u: Dictionary = _db.combat.skill.row_by_id(String(entry.skill_id))
if not srow_u.is_empty() and String(srow_u.type) != "basic" and not srow_u.has("trigger"):
n_uncond += 1
if n_uncond < 1:
@@ -644,7 +645,7 @@ func _smoke_skill_pool_data() -> void:
var st_unlock: Dictionary = {"rows": []}
var sett_u = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(4242))
sett_u.step_row.connect(func(row): st_unlock["rows"].append(row))
Kit.check("前置:卓尔 Boss 战受理(2,9——解锁技 venom_curb 门槛 50)", sett_u.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}))
Kit.check("前置:卓尔 Boss 战受理(2,9——解锁技 venom_curb 门槛 50)", sett_u.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}))
var boss_u = sett_u._enemy_system.all_enemies()[0] # 白盒夹具(smoke 惯例——autoplay 同族私有面)
sett_u._enemy_system.apply_damage(boss_u, (int(boss_u.max_hp) - 65))
Kit.check("前置:Boss 压到阈值 65(=round(130×0.5))", int(boss_u.hp) == 65)
@@ -660,25 +661,25 @@ func _smoke_skill_pool_data() -> void:
var rollable_ok := true
for depth in [0, 1, 2, 3, 4]:
var es = load("res://core/unit_container.gd").new(_db)
es.spawn(1, int(depth), int(_db.hero.max_hp), [], -1, -1, [], "", _row_kind_of(1, int(depth)))
es.spawn({"level_index": 1, "node_depth": int(depth), "hero_hp": int(_db.combat.hero.max_hp), "hero_armor_type": _row_kind_of(1, int(depth))})
for u in es.all_enemies():
if es.rollable_skills(u).is_empty():
rollable_ok = false
for depth in [0, 1, 4, 5, 6, 7, 8]:
var es2 = load("res://core/unit_container.gd").new(_db)
es2.spawn(2, int(depth), int(_db.hero.max_hp), [], -1, -1, [], "", _row_kind_of(2, int(depth)))
es2.spawn({"level_index": 2, "node_depth": int(depth), "hero_hp": int(_db.combat.hero.max_hp), "hero_armor_type": _row_kind_of(2, int(depth))})
for u in es2.all_enemies():
if es2.rollable_skills(u).is_empty():
rollable_ok = false
Kit.check("全深度实例 spawn 后可摇集合恒非空(普攻恒冷却 0——容器级断言,L1/L2 双关卡)", rollable_ok)
# 状态表新行 + 敌方防御池映射
var empower: Dictionary = _db.status_effect_row("empower")
var fortify: Dictionary = _db.status_effect_row("fortify")
var empower: Dictionary = _db.combat.status_effects.row_by_id("empower")
var fortify: Dictionary = _db.combat.status_effects.row_by_id("fortify")
Kit.check("蓄力状态行 empower(enemy +100 percent——敌方输出 ×2 走既有 enemy_output 池)", not empower.is_empty() and String(empower.target_side) == "enemy" and int(empower.magnitude) == 100)
Kit.check("硬化状态行 fortify(enemy −50 percent——敌方防御池负值 = 主角攻击折半;+100 符号 bug 2026-09-18 纠正,provisional 归 状态层重设计批)", not fortify.is_empty() and String(fortify.target_side) == "enemy" and int(fortify.magnitude) == -50)
Kit.check("effect_pool_map 收编 empower/fortify(敌方防御池 enemy_defense 入表)",
String(_db.settlement.core.effect_pool_map.get("empower", "")) == "enemy_output" \
and String(_db.settlement.core.effect_pool_map.get("fortify", "")) == "enemy_defense")
String(_db.combat.settlement.core.effect_pool_map.get("empower", "")) == "enemy_output" \
and String(_db.combat.settlement.core.effect_pool_map.get("fortify", "")) == "enemy_defense")
# ② 同种子回放逐位复现(D3 大地精,多实例同种)
@@ -763,11 +764,11 @@ func _smoke_skill_pool_ev_anchor() -> void:
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(20260921)
var n_beats := 5000
var pity_m: int = int(_db.skill.pity_threshold)
var pity_m: int = int(_db.combat.skill.pity_threshold)
for si in species.size():
var eid: String = species[si]
var template: Array = []
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
if String(row.enemy_id) == eid:
template = row.pool
break
@@ -784,13 +785,13 @@ func _smoke_skill_pool_ev_anchor() -> void:
if streak >= pity_m:
var specials: Array = []
for e in pool:
var sr: Dictionary = _db.skill_row(String(e.skill_id))
var sr: Dictionary = _db.combat.skill.row_by_id(String(e.skill_id))
if not sr.is_empty() and String(sr.type) != "basic":
specials.append({"skill_id": e.skill_id, "weight": e.weight})
if not specials.is_empty():
entries = specials
var sid := SkillLottery.draw(entries, rng.open_stream("behavior_roll"))
var srow: Dictionary = _db.skill_row(sid)
var srow: Dictionary = _db.combat.skill.row_by_id(sid)
var mult := 0.0
if String(srow.type) == "basic":
mult = 1.0
@@ -817,12 +818,12 @@ func _smoke_skill_pool_death_void() -> void:
for s in range(1, 400):
var rg = load("res://core/rng_service.gd").new()
rg.set_master_seed(s)
var erow: Dictionary = _db.resolve_enemy_rowset(1, 2, "battle")[0] # 行集首行(战斗节点 battle 键域)
var erow: Dictionary = _db.combat.enemy.resolve_rowset(1, 2, "battle")[0] # 行集首行(战斗节点 battle 键域)
var entries: Array = []
for entry in erow.pool:
entries.append({"skill_id": String(entry.skill_id), "weight": int(entry.weight)})
var sid := SkillLottery.draw(entries, rg.open_stream("behavior_roll"))
if String(_db.skill_row(sid).type) != "basic":
if String(_db.combat.skill.row_by_id(sid).type) != "basic":
void_seed = s
break
Kit.check("死亡打断种子可达(seed=%d——D2 首拍摇中特殊技)" % void_seed, void_seed > 0)
@@ -831,7 +832,7 @@ func _smoke_skill_pool_death_void() -> void:
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
var rows: Array = []
settlement.step_row.connect(func(row): rows.append(row))
Kit.check("死亡打断战斗启动(hero_atk 999——先手必杀 40HP 熊地精)", settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.hero.max_hp), "hero_atk": 999}))
Kit.check("死亡打断战斗启动(hero_atk 999——先手必杀 40HP 熊地精)", settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.combat.hero.max_hp), "hero_atk": 999}))
var u: Dictionary = settlement._enemy_system.all_enemies()[0] # 句柄预捕获(胜利 despawn 后容器已清,字典引用仍可读)
var pending: String = settlement._enemy_system.pending_action(u)
Kit.check("开轮已摇定本拍动作(待发 = %s)" % pending, pending != "")
@@ -855,14 +856,14 @@ func _smoke_skill_pool_validation_reject() -> void:
var skill_table = load("res://data/schemas/skill_table.gd").new()
var enemy_table = load("res://data/schemas/enemy_table.gd").new()
enemy_table.rows.assign([{"enemy_id": "x", "display_name": "x", "pool": [{"skill_id": "ghost", "weight": 1}]}])
Kit.check("加载校验拒绝:池条目 skill_id 悬空致命", _has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table), "dangling_skill"))
Kit.check("加载校验拒绝:池条目 skill_id 悬空致命", _has_err_category(_db._validate_skill_pool_links(skill_table, enemy_table), "dangling_skill"))
skill_table.rows.assign([{"skill_id": "a", "display_name": "a", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2}])
Kit.check("加载校验拒绝:池普攻行数 ≠ 1(空池不可能前提)", _has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table), "pool_composition"))
Kit.check("加载校验拒绝:池普攻行数 ≠ 1(空池不可能前提)", _has_err_category(_db._validate_skill_pool_links(skill_table, enemy_table), "pool_composition"))
skill_table.rows.assign([
{"skill_id": "b1", "display_name": "b1", "type": "buff", "buff_ref": "empower", "buff_duration": 2, "cooldown_beats": 3},
{"skill_id": "b2", "display_name": "b2", "type": "buff", "buff_ref": "fortify", "buff_duration": 2, "cooldown_beats": 4}])
enemy_table.rows.assign([{"enemy_id": "x", "display_name": "x", "pool": [{"skill_id": "b1", "weight": 1}, {"skill_id": "b2", "weight": 1}]}])
Kit.check("加载校验拒绝:池增益技 > 1(每池增益 ≤1 纪律)", _has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table), "pool_composition"))
Kit.check("加载校验拒绝:池增益技 > 1(每池增益 ≤1 纪律)", _has_err_category(_db._validate_skill_pool_links(skill_table, enemy_table), "pool_composition"))
skill_table.rows.assign([{"skill_id": "atk", "display_name": "a", "type": "basic", "damage_mult": 2.0, "cooldown_beats": 0}])
Kit.check("加载校验拒绝:普攻配倍率 ≠ 1(域校验锁死)", not skill_table.validate().is_empty())
skill_table.rows.assign([{"skill_id": "atk", "display_name": "a", "type": "basic", "cooldown_beats": 2}])
@@ -879,7 +880,7 @@ func _smoke_skill_pool_validation_reject() -> void:
{"skill_id": "w1", "display_name": "w1", "type": "debuff", "buff_ref": "off_guard", "buff_duration": 2, "cooldown_beats": 4},
{"skill_id": "w2", "display_name": "w2", "type": "debuff", "buff_ref": "off_guard", "buff_duration": 2, "cooldown_beats": 4}])
enemy_table.rows.assign([{"enemy_id": "x", "display_name": "x", "pool": [{"skill_id": "w1", "weight": 1}, {"skill_id": "w2", "weight": 1}]}])
Kit.check("加载校验拒绝:池妨害技 > 1(每池妨害 ≤1 纪律)", _has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table), "pool_composition"))
Kit.check("加载校验拒绝:池妨害技 > 1(每池妨害 ≤1 纪律)", _has_err_category(_db._validate_skill_pool_links(skill_table, enemy_table), "pool_composition"))
skill_table.rows.assign([{"skill_id": "w1", "display_name": "w1", "type": "debuff", "cooldown_beats": 4}])
Kit.check("加载校验拒绝:妨害技缺 buff_ref(效果引用)", not skill_table.validate().is_empty())
skill_table.rows.assign([{"skill_id": "d", "display_name": "d", "type": "damage", "damage_mult": 1.5, "rider_mult": 1.5, "cooldown_beats": 1}])
@@ -891,7 +892,7 @@ func _smoke_skill_pool_validation_reject() -> void:
{"skill_id": "atk", "display_name": "a", "type": "basic", "cooldown_beats": 0},
{"skill_id": "w1", "display_name": "w1", "type": "debuff", "buff_ref": "empower", "buff_duration": 2, "cooldown_beats": 4}])
enemy_table.rows.assign([{"enemy_id": "x", "display_name": "x", "pool": [{"skill_id": "atk", "weight": 1}, {"skill_id": "w1", "weight": 1}]}])
Kit.check("加载校验拒绝:妨害技引用敌方侧状态(施放类 side 守卫)", _has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table, _db.status_effects), "cast_side"))
Kit.check("加载校验拒绝:妨害技引用敌方侧状态(施放类 side 守卫)", _has_err_category(_db._validate_skill_pool_links(skill_table, enemy_table, _db.status_effects), "cast_side"))
func _has_err_category(errs: Array, category: String) -> bool:
@@ -914,9 +915,9 @@ func _smoke_debuff_rider() -> void:
var venom_check_ok := true
var venom_rows_seen := 0
var snare_seed := -1
var base_ac: int = int(_db.hero.armor_class)
var off_m: int = int(_db.status_effect_row("off_guard").magnitude)
var floor_v: int = int(_db.settlement.damage_roll.damage_floor)
var base_ac: int = int(_db.combat.hero.armor_class)
var off_m: int = int(_db.combat.status_effects.row_by_id("off_guard").magnitude)
var floor_v: int = int(_db.combat.settlement.damage_roll.damage_floor)
for s in range(1, 401):
if venom_seed > 0 and snare_seed > 0:
break
@@ -925,7 +926,7 @@ func _smoke_debuff_rider() -> void:
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rg)
var rows: Array = []
settlement.step_row.connect(func(row): rows.append(row))
if not settlement.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}):
if not settlement.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}):
continue
var boss_atk: int = int(settlement._enemy_system.all_enemies()[0].atk) # 开战掷定攻击基线(毒分量随其缩放)
for i in range(6):
@@ -948,7 +949,7 @@ func _smoke_debuff_rider() -> void:
if rider_v != maxi(floor_v, int(floor(float(boss_atk) * 0.75))):
venom_check_ok = false
# 终值精确复算(停手轮池恒空):主分量链结果 + 必中附加后置
var base_factor: float = float(int(row.tier)) * (float(_db.settlement.damage_roll.crit_multiplier) / 100.0 if bool(row.get("crit", false)) else 1.0)
var base_factor: float = float(int(row.tier)) * (float(_db.combat.settlement.damage_roll.crit_multiplier) / 100.0 if bool(row.get("crit", false)) else 1.0)
var rolled_main: int = int(floor(float(int(row.baseline)) * base_factor / 100.0))
var expected: int = maxi(floor_v, rolled_main + int(row.get("attach", 0))) + rider_v
if int(row.value) != expected:
@@ -52,10 +52,10 @@ func _smoke_seal_ap_exhausted() -> void:
var rounds: Array = []
settlement.round_started.connect(func(ri): rounds.append(ri))
Kit.check("回归战斗启动(Boss 行——D0 20HP 级会被盲挖粉尘秒杀,无法承载封印回归场景)",
settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}))
settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}))
var minesweeper = settlement.minesweeper
# 恰花 (AP-1) 点挖掘,留末点给封印(表驱动——AP 从表读不写死)
var ap_total: int = int(_db.action_economy.ap_per_round)
var ap_total: int = int(_db.run.action_economy.ap_per_round)
for i in range(ap_total - 1):
if settlement.action_economy.phase != "Active" or settlement.action_economy.ap == 0:
break
@@ -151,7 +151,7 @@ func _run_battle(L: int, D: int, axes: Dictionary = {}) -> Dictionary:
settlement.minesweeper.seal_judged.connect(func(evt): out.events.append(evt))
settlement.minesweeper.round_end_board.connect(func(p): out.events.append({"ev": "round_end_board", "path_tag": p.path_tag}))
settlement.action_economy.round_end_stopped.connect(func(p): out.events.append({"ev": "round_end_stopped", "path_tag": p.path_tag}))
var enc := {"level_index": L, "node_depth": D, "hero_hp": _db.hero.max_hp}
var enc := {"level_index": L, "node_depth": D, "hero_hp": _db.combat.hero.max_hp}
enc.merge(axes, true) # 两根类型轴注入尾参(缺省零注入=回退面)
if not settlement.begin_battle(enc):
out.stream.append({"type": "verdict", "value": "begin_failed"})
@@ -169,7 +169,7 @@ func _run_battle(L: int, D: int, axes: Dictionary = {}) -> Dictionary:
# 清图驱动(真实流):标一面真雷封印(完美前置)→ 逐轮 AutoPlayer 挖到轮末;board_seq 递增 = 清图换图。
# from_refresh=true 时先三轮停手攒 RAP、第 4 轮轮内刷新换图,再清刷新图。
func _run_full_clear(seed_val: int, from_refresh: bool, rows: Array) -> Dictionary:
_db.difficulty.rows.append({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hp_mult": 10.0, "atk_mult": 1.0})
_db.run.difficulty.rows.append({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hp_mult": 10.0, "atk_mult": 1.0})
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(seed_val)
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
@@ -177,7 +177,7 @@ func _run_full_clear(seed_val: int, from_refresh: bool, rows: Array) -> Dictiona
var res := {"cleared": false, "refreshed": false, "rounds": 0}
if not settlement.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "node_type": "boss",
"hero_hp": CLEAR_SMOKE_HERO_HP, "hero_max_hp": CLEAR_SMOKE_HERO_HP}):
_db.difficulty.rows.clear()
_db.run.difficulty.rows.clear()
return res
if from_refresh:
for i in range(3):
@@ -186,7 +186,7 @@ func _run_full_clear(seed_val: int, from_refresh: bool, rows: Array) -> Dictiona
var drive := _drive_clear(settlement)
res["cleared"] = drive.cleared
res["rounds"] = drive.rounds
_db.difficulty.rows.clear() # 测试台账还原(后续冒烟基线)
_db.run.difficulty.rows.clear() # 测试台账还原(后续冒烟基线)
return res
@@ -266,7 +266,7 @@ func _polarity_probe_accepts(probe: Dictionary) -> bool:
# 极性路由探针驱动:标 1 面真雷封印(标中)+ 1 面安全格封印(标空)→ AutoPlayer 挖到战斗终局(多轮样本)。
# 收集全部 cell_revealed / seal_judged 事件与结算行;返回格子集合与封印格坐标。
func _run_polarity_probe(seed_val: int) -> Dictionary:
_db.difficulty.rows.append({"level_index": 1, "node_depth": 0, "hp_mult": 10.0, "atk_mult": 0.5}) # 抬敌血压低敌攻——多轮挖掘样本不被终局截断
_db.run.difficulty.rows.append({"level_index": 1, "node_depth": 0, "hp_mult": 10.0, "atk_mult": 0.5}) # 抬敌血压低敌攻——多轮挖掘样本不被终局截断
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(seed_val)
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
@@ -305,7 +305,7 @@ func _run_polarity_probe(seed_val: int) -> Dictionary:
while settlement.battle_result == "" and guard > 0:
guard -= 1
_auto_play_round(settlement, out)
_db.difficulty.rows.clear() # 测试台账还原
_db.run.difficulty.rows.clear() # 测试台账还原
return probe
@@ -439,16 +439,10 @@ func _smoke_polarity_routing() -> void:
if _polarity_probe_accepts(probe):
polarity_seed = s
break
# 触达三腿(标中标空入队/挖危险格/挖安全格)是 _polarity_probe_accepts 的合取项,
# 可达即三腿同真——缺腿明细进标签承载诊断,不再各立一条(2026-10-28 断言精简批删并)
var legs := ""
if not (bool(probe["seal_hit_done"]) and bool(probe["seal_miss_done"])):
legs += "标中标空未入队 "
if int(probe["mine_events"]) < 1:
legs += "挖危险格零 "
if int(probe["safe_events"]) < 1:
legs += "挖安全格零 "
Kit.check("极性路由构造种子可达(seed=%d——并流后行为面重锚;缺腿:%s)" % [polarity_seed, legs], polarity_seed > 0)
Kit.check("极性路由构造种子可达(seed=%d——并流后行为面重锚)" % polarity_seed, polarity_seed > 0)
Kit.check("构造触达:标中真雷 + 标空安全格均入队", bool(probe["seal_hit_done"]) and bool(probe["seal_miss_done"]))
Kit.check("构造触达:挖到危险格 ≥1(%d)" % int(probe["mine_events"]), int(probe["mine_events"]) >= 1)
Kit.check("构造触达:挖到安全格 ≥1(%d)" % int(probe["safe_events"]), int(probe["safe_events"]) >= 1)
# 挖到的安全格:只出正向——其全部来源行必存在且 effect 行 ∈ 正向集
# (action 型排队行 effect_id=ap_delta 正负两极共用路由键——按行值符号辨极性:正向格恒 +power)
var safe_all_pos := true
@@ -500,7 +494,7 @@ func _smoke_polarity_routing() -> void:
# δ 读 settlement.seal_hit_premium(125 provisional);九型正向行值全固定(唯一区间行 clear_blast
# 系清图槽不入格池)⇒ 逐型期望全确定。整数量级四舍五入域注记:ac_up/action 1 点量纲取整后不变
# =整数粒度地板——等号压线判读在当量聚合面(audit 工具/registry clause3_inequality),非断言面。
var prem_pct: int = int(_db.settlement.core.seal_hit_premium)
var prem_pct: int = int(_db.combat.settlement.core.seal_hit_premium)
Kit.check("封印溢价配置在位(seal_hit_premium=125 provisional——形态 a 兑付溢价)", prem_pct == 125)
var prem_rows := 0
var prem_ok := true
@@ -517,7 +511,7 @@ func _smoke_polarity_routing() -> void:
elif eff == "ap_delta":
prem_ok = prem_ok and v == 1 # +1AP:1 点量纲(round(1.25)=1——整数粒度地板)
else:
var mag: int = int(_db.status_effect_row(eff).get("magnitude", -999))
var mag: int = int(_db.combat.status_effects.row_by_id(eff).get("magnitude", -999))
prem_ok = prem_ok and v == int(round(mag * prem_pct / 100.0)) # 增伤50→63/削弱65→81/护甲1→1
elif rtype == "heal":
# 急救 2→3(clamp 封顶只降不升:满血走 reward_ineffective 不入本行——区间断言)
@@ -574,7 +568,7 @@ func _smoke_seal_irreversible_quadrants() -> void:
var settle_a = load("res://core/battle_orchestrator.gd").new(_db, rng_a)
var rows_a: Array = []
settle_a.step_row.connect(func(row): rows_a.append(row))
Kit.check("四情况 A:开局受理", settle_a.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}))
Kit.check("四情况 A:开局受理", settle_a.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}))
var ms_a = settle_a.minesweeper
var t_a := _find_seal_target(ms_a, false)
Kit.check("四情况 A:找到安全格封印目标", not t_a.is_empty())
@@ -612,7 +606,7 @@ func _smoke_seal_irreversible_quadrants() -> void:
var settle_b = load("res://core/battle_orchestrator.gd").new(_db, rng_b)
var rows_b: Array = []
settle_b.step_row.connect(func(row): rows_b.append(row))
Kit.check("四情况 B:开局受理", settle_b.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}))
Kit.check("四情况 B:开局受理", settle_b.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}))
var ms_b = settle_b.minesweeper
var t_b := _find_seal_target(ms_b, true)
Kit.check("四情况 B:找到危险格封印目标", not t_b.is_empty())
@@ -642,7 +636,7 @@ func _smoke_seal_irreversible_quadrants() -> void:
var settle_c = load("res://core/battle_orchestrator.gd").new(_db, rng_c)
var rows_c: Array = []
settle_c.step_row.connect(func(row): rows_c.append(row))
Kit.check("四情况 C:开局受理", settle_c.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}))
Kit.check("四情况 C:开局受理", settle_c.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}))
var ms_c = settle_c.minesweeper
var t_safe := _find_seal_target(ms_c, false)
var t_mine := _find_seal_target(ms_c, true)
@@ -677,12 +671,12 @@ func _smoke_seal_irreversible_quadrants() -> void:
func _smoke_negative_dictionary() -> void:
print("--- 负向字典逐型直验(sapped 在位 / heal 直伤 / blast 警觉 / action 行动点)---")
# 数据面:sapped 行在位(状态表七行 + effect_pool_map 映射在位)
var sapped: Dictionary = _db.status_effect_row("sapped")
var sapped: Dictionary = _db.combat.status_effects.row_by_id("sapped")
Kit.check("sapped 行在位(hero · −25 percent · dur 1 · max 16 · stack——weaken 型负向)",
not sapped.is_empty() and String(sapped.get("target_side", "")) == "hero" \
and int(sapped.get("magnitude", 0)) == -25 and int(sapped.get("duration_settlements", 0)) == 1 \
and int(sapped.get("max_stack", 0)) == 16 and String(sapped.get("stack_policy", "")) == "stack")
Kit.check("effect_pool_map 收编 sapped → hero_offense(attack_up 镜像池)", String(_db.settlement.core.effect_pool_map.get("sapped", "")) == "hero_offense")
Kit.check("effect_pool_map 收编 sapped → hero_offense(attack_up 镜像池)", String(_db.combat.settlement.core.effect_pool_map.get("sapped", "")) == "hero_offense")
# 行为面:逐型负向兑付形态(真实板 + 真实路由路径)
# heal 权重低(无 heal 板概率 ≈1/3)——单板五型齐备不再必然:
# 本断言多板扫描,取第一块五型齐备的板做行为断言。
@@ -698,7 +692,7 @@ func _smoke_negative_dictionary() -> void:
settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
rows = []
settlement.step_row.connect(func(row): rows.append(row))
if not settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp), "hero_max_hp": int(_db.hero.max_hp)}):
if not settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp), "hero_max_hp": int(_db.combat.hero.max_hp)}):
continue
board = settlement.minesweeper.current_board()
var r0 := {}
@@ -738,7 +732,7 @@ func _smoke_negative_dictionary() -> void:
settlement.reward_grant().grant_routed(snap_h, "negative", {"source_class": "mine_hit", "cell": by_type["heal"], "ri": 1})
var hp_after: int = int(settlement.presentation_snapshot().hero.hp)
var backlash_power: int = 0
for row in _db.reward.rows:
for row in _db.run.reward.rows:
if String(row.get("reward_id", "")) == "heal_backlash":
backlash_power = int(row.power)
Kit.check("heal 型负向 → 主角直伤 %d(%d → %d,不经增益状态)" % [backlash_power, hp_before, hp_after], hp_after == hp_before - backlash_power)
@@ -795,7 +789,7 @@ func _smoke_resource_surface() -> void:
var rng3 = load("res://core/rng_service.gd").new()
rng3.set_master_seed(20260920)
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng3)
settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)})
settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)})
var board = settlement.minesweeper.current_board()
var r0_first: Variant = null
for c in board.compute_r0():
@@ -26,7 +26,7 @@ func _init() -> void:
# 门谓词:卓尔 (2,9) hp 130(×mult 1.0)→ venom_curb unlock 50 → 阈值 round(130×0.5)=65
func _boss(db, L: int, D: int, kind := "boss"):
var ec = UnitContainerScript.new(db)
var ok: bool = ec.spawn(L, D, int(db.hero.max_hp), [], -1, -1, [], "", "", kind)
var ok: bool = ec.spawn({"level_index": L, "node_depth": D, "hero_hp": int(db.combat.hero.max_hp), "row_kind": kind})
Kit.check("前置:spawn (%d,%d) 受理" % [L, D], ok)
return ec
@@ -83,7 +83,7 @@ func _test_latch_irreversible(db) -> void:
# 普通行无字段逐位零扰动:地精 (1,0) 池快照零 unlock 键、过滤恒空转、可摇=全池
func _test_plain_rows_zero_drift(db) -> void:
var ec = load("res://core/unit_container.gd").new(db)
Kit.check("零扰动前置:巨蜘蛛首遇带 spawn(池三行全无 trigger——快照键策略钉死载体)", ec.spawn(2, 1, int(db.hero.max_hp), [], -1, -1, [], "", "", "battle"))
Kit.check("零扰动前置:巨蜘蛛首遇带 spawn(池三行全无 trigger——快照键策略钉死载体)", ec.spawn({"level_index": 2, "node_depth": 1, "hero_hp": int(db.combat.hero.max_hp)}))
var u: Dictionary = ec.all_enemies()[0]
var any_unlock_key := false
for e in u.pool:
@@ -96,7 +96,7 @@ func _test_plain_rows_zero_drift(db) -> void:
Kit.check("零扰动:可摇集合 = 全池(普攻冷却恒 0,过滤恒空转)", rollable.size() == pool_count)
# 同种子确定性:容器零随机——两实例同输入查询逐位同(同款零扰动载体)
var ec2 = load("res://core/unit_container.gd").new(db)
Kit.check("确定性前置:第二实例蜘蛛 spawn", ec2.spawn(2, 1, int(db.hero.max_hp), [], -1, -1, [], "", "", "battle"))
Kit.check("确定性前置:第二实例蜘蛛 spawn", ec2.spawn({"level_index": 2, "node_depth": 1, "hero_hp": int(db.combat.hero.max_hp)}))
var u2: Dictionary = ec2.all_enemies()[0]
ec.apply_damage(u, 3)
ec2.apply_damage(u2, 3)
@@ -52,7 +52,7 @@ func _init() -> void:
# ---- V2 门槛过滤双面(spawn 真实表→直调 rollable/special)----
var ec = load("res://core/unit_container.gd").new(db)
Kit.check_quiet(ec.spawn(2, 9, 30, [], 8, 30, [], "", "", "boss"), "V2 前置:卓尔 Boss spawn")
Kit.check_quiet(ec.spawn({"level_index": 2, "node_depth": 9, "hero_hp": 30, "hero_atk_override": 8, "hero_max_hp_override": 30, "row_kind": "boss"}), "V2 前置:卓尔 Boss spawn")
var u = ec.all_enemies()[0]
Kit.check_quiet(int(u.hp) == int(u.max_hp), "V2 前置:满血(hp=max_hp)")
var r_full: Array = ec.rollable_skills(u)
@@ -67,7 +67,7 @@ func _init() -> void:
# ---- V3 保底不逼条件不满足技(保底集合取 special_skills 已含排除——编排级:
# 无条件特殊技 A w1+条件假技 B w9999+普攻;保底命中摇出的是 A,撤销过滤态同种子必中 B)----
var ec_v3 = load("res://core/unit_container.gd").new(db)
Kit.check_quiet(ec_v3.spawn(1, 0, 8, [], 6, 8, [], "", "", "battle"), "V3 前置:地精 spawn")
Kit.check_quiet(ec_v3.spawn({"level_index": 1, "node_depth": 0, "hero_hp": 8, "hero_atk_override": 6, "hero_max_hp_override": 8}), "V3 前置:地精 spawn")
var g3 = ec_v3.all_enemies()[0]
g3["pool"] = [
{"skill_id": "attack", "weight": 9, "cooldown_left": 0},
@@ -75,7 +75,7 @@ func _init() -> void:
{"skill_id": "desperate_guard", "weight": 9999, "cooldown_left": 0,
"trigger": {"fact": "self_hp_pct", "op": "lte", "value": 35, "latch": false}}, # B=条件假技 w9999
]
g3["basic_streak"] = int(db.skill.pity_threshold) # 保底已触发位(夹具内存改行——round_flow 白盒先例)
g3["basic_streak"] = int(db.combat.skill.pity_threshold) # 保底已触发位(夹具内存改行——round_flow 白盒先例)
var specials_v3: Array = ec_v3.special_skills(g3)
Kit.check_quiet(_has(specials_v3, "heavy") and not _has(specials_v3, "desperate_guard"),
"V3 保底集合含 A 排除 B(special_skills 真实入口——条件假技不逼出)〔触发器批 V3〕")
@@ -96,7 +96,7 @@ func _init() -> void:
Kit.check_quiet(ec.check_trigger_unlocks(u).is_empty() and _has(ec.rollable_skills(u), "venom_curb"),
"V4 治疗抬血不回锁(单向闩)〔触发器批 V4〕")
var ec8 = load("res://core/unit_container.gd").new(db)
Kit.check_quiet(ec8.spawn(1, 0, 8, [], 6, 8, [], "", "", "battle"), "V4 前置:地精 spawn(hp 基线 8)")
Kit.check_quiet(ec8.spawn({"level_index": 1, "node_depth": 0, "hero_hp": 8, "hero_atk_override": 6, "hero_max_hp_override": 8}), "V4 前置:地精 spawn(hp 基线 8)")
var g = ec8.all_enemies()[0]
g["pool"] = [{"skill_id": "attack", "weight": 9, "cooldown_left": 0},
{"skill_id": "desperate_guard", "weight": 1, "cooldown_left": 0,
@@ -139,28 +139,28 @@ func _init() -> void:
if String((e as Dictionary).get("category", "")) == "pool_composition" and String((e as Dictionary).get("message", "")).contains("无条件特殊技"):
comp_violations += 1
Kit.check_quiet(comp_violations == 0, "V7 现行全表零违例(每池至少一行无条件特殊技)〔触发器批 V7〕")
var enemy_backup: Array = db.enemy.rows.duplicate(true)
var cond_only_row: Dictionary = (db.enemy.rows[0] as Dictionary).duplicate(true)
var enemy_backup: Array = db.combat.enemy.rows.duplicate(true)
var cond_only_row: Dictionary = (db.combat.enemy.rows[0] as Dictionary).duplicate(true)
var cond_pool: Array[Variant] = []
for entry in (cond_only_row.get("pool", []) as Array):
var sid := String((entry as Dictionary).get("skill_id", ""))
var srow_c: Dictionary[Variant, Variant] = db.skill_row(sid)
var srow_c: Dictionary[Variant, Variant] = db.combat.skill.row_by_id(sid)
if not srow_c.is_empty() and String(srow_c.get("type", "")) == "basic":
cond_pool.append(entry) # 普攻保留(普攻不计入无条件特殊技)
cond_pool.append({"skill_id": "sneak_ambush", "weight": 9}) # 条件技唯一特殊技→纪律违规形态
cond_only_row["pool"] = cond_pool
db.enemy.rows[0] = cond_only_row
db.combat.enemy.rows[0] = cond_only_row
var errs2: Array = cv.validate_skill_pool_links(db.skill, db.enemy, db.status_effects)
var hit2 := false
for e in errs2:
if String((e as Dictionary).get("message", "")).contains("无条件特殊技"):
hit2 = true
Kit.check_quiet(hit2, "V7 构造全条件池报 pool_composition 错〔触发器批 V7〕")
db.enemy.rows = enemy_backup
db.combat.enemy.rows = enemy_backup
# ---- V8 示范投放:低血防御(地精两行池 w1+trigger lte 35——真实表 spawn 压 hp 开轮摇号)----
var ec_v8 = load("res://core/unit_container.gd").new(db)
Kit.check_quiet(ec_v8.spawn(1, 0, 8, [], 6, 8, [], "", "", "battle"), "V8 前置:地精 spawn")
Kit.check_quiet(ec_v8.spawn({"level_index": 1, "node_depth": 0, "hero_hp": 8, "hero_atk_override": 6, "hero_max_hp_override": 8}), "V8 前置:地精 spawn")
var g8 = ec_v8.all_enemies()[0]
var has_dg := false
for e in (g8.get("pool", []) as Array):
@@ -176,7 +176,7 @@ func _init() -> void:
# ---- V9 示范投放:停手偷袭(钩爪恐魔池 w2+last_round_end eq stopped——facts 缺键=拦 fail-safe)----
var ec_v9 = load("res://core/unit_container.gd").new(db)
Kit.check_quiet(ec_v9.spawn(3, 1, 30, [], 8, 30, [], "", "", "battle"), "V9 前置:钩爪恐魔 spawn")
Kit.check_quiet(ec_v9.spawn({"level_index": 3, "node_depth": 1, "hero_hp": 30, "hero_atk_override": 8, "hero_max_hp_override": 30}), "V9 前置:钩爪恐魔 spawn")
var h9 = ec_v9.all_enemies()[0]
Kit.check_quiet(_not_has(ec_v9.rollable_skills(h9), "sneak_ambush"),
"V9 首轮(facts 缺 last_round_end 键=fail-safe 拦)不入集合〔触发器批 V9〕")
@@ -189,7 +189,7 @@ func _init() -> void:
# ---- V10 缺省零扰动(无 trigger 行的池快照逐键无新增+摇号同种子不变——快照键策略钉死)----
var ec_v10 = load("res://core/unit_container.gd").new(db)
Kit.check_quiet(ec_v10.spawn(1, 0, 8, [], 6, 8, [], "", "", "battle"), "V10 前置:地精 spawn")
Kit.check_quiet(ec_v10.spawn({"level_index": 1, "node_depth": 0, "hero_hp": 8, "hero_atk_override": 6, "hero_max_hp_override": 8}), "V10 前置:地精 spawn")
var g10 = ec_v10.all_enemies()[0]
var stray := 0
for e in (g10.get("pool", []) as Array):
@@ -56,7 +56,7 @@ func _smoke_variant_placement(L: int) -> void:
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(42)
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
if not settlement.begin_battle({"level_index": L, "node_depth": 0, "hero_hp": _db.hero.max_hp}):
if not settlement.begin_battle({"level_index": L, "node_depth": 0, "hero_hp": _db.combat.hero.max_hp}):
Kit.check("L%d 布置段战斗启动失败" % L, false)
return
var b = settlement.minesweeper.current_board()
@@ -64,7 +64,7 @@ func _smoke_variant_placement(L: int) -> void:
if b == null:
return
var pool_ids: Array = []
for r in _db.variant_pool(L, String(b.board_type)):
for r in _db.combat.variation.variant_pool(L, String(b.board_type)):
pool_ids.append(String(r.variant_id))
var vcells: Array = b.variants.keys()
var vid0 := ""
@@ -95,13 +95,13 @@ func _smoke_variant_placement(L: int) -> void:
for c in b.safe_cells():
if not r0.has(c):
cand_count += 1
var expect_cap: int = int(round(float(cand_count) * float(_db.variation.percent_of_cells) / 100.0)) \
+ (L - 1) * int(_db.variation.per_level_bonus)
var expect_cap: int = int(round(float(cand_count) * float(_db.combat.variation.percent_of_cells) / 100.0)) \
+ (L - 1) * int(_db.combat.variation.per_level_bonus)
Kit.check("L%d 变异格数 %d ≤ 公式上限 %d(可挖池百分比 + 关卡递增)" % [L, vcells.size(), expect_cap], vcells.size() <= expect_cap)
# 数量公式量随候选池缩放:L1 无关卡加成,极小池(<10 格)可 round 至 0——
# 零布置为合法态(同池空口径);非零断言为条件式,公式量>0 时才要求非零(L2/L3 关卡加成保底恒>0)
Kit.check("L%d 公式量>0 时非零布置(极小池 round 至 0 = 合法零布置——2026-09-21 基数修正批)" % L, expect_cap == 0 or vcells.size() > 0)
var pack_max: int = int(_db.variation.pack_max)
var pack_max: int = int(_db.combat.variation.pack_max)
var vset := {}
for c in vcells:
vset[c] = true
@@ -127,7 +127,7 @@ func _smoke_variant_placement(L: int) -> void:
var rng2 = load("res://core/rng_service.gd").new()
rng2.set_master_seed(42)
var s2 = load("res://core/battle_orchestrator.gd").new(_db, rng2)
s2.begin_battle({"level_index": L, "node_depth": 0, "hero_hp": _db.hero.max_hp})
s2.begin_battle({"level_index": L, "node_depth": 0, "hero_hp": _db.combat.hero.max_hp})
var b2 = s2.minesweeper.current_board()
Kit.check("L%d 同种子变异布置逐格相同(variant_place 流确定性)" % L, b2 != null and _variant_dump(b2) == _variant_dump(b))
@@ -175,24 +175,24 @@ func _smoke_global_variant() -> void:
Kit.check("互锁负样本⑦:resource_face effect_value=51 越域 [0,50] 被拒(不适用 flat_damage [1,9] 加法核)", domain_hit)
# ② 门控第三维(节点身份——global 行仅 boss/elite 入池;缺省/普通节点静默跳过零报错)
var pool_boss_ids: Array = []
for r in _db.variant_pool(2, "underdark", "boss"):
for r in _db.combat.variation.variant_pool(2, "underdark", "boss"):
pool_boss_ids.append(String(r.variant_id))
Kit.check("门控三维:L2 underdark boss 池 = region 两行(贫矿脉 2026-10-16 屏蔽删行后无 global 行)", pool_boss_ids == ["magma", "ash"])
var pool_battle_ids: Array = []
for r in _db.variant_pool(2, "underdark", "battle"):
for r in _db.combat.variation.variant_pool(2, "underdark", "battle"):
pool_battle_ids.append(String(r.variant_id))
Kit.check("门控三维:battle 节点静默跳过 global 行(普通图零报错零警告)", pool_battle_ids == ["magma", "ash"])
Kit.check("门控三维:缺省双参调用 = global 行跳过(既有调用口径零扰动)", _db.variant_pool(2, "underdark").is_empty() == false and _db.variant_pool(2, "underdark").size() == 2)
Kit.check("L1 Boss 免疫:unlock_level=2 门控在 boss 节点同样生效(教学保护带 B)", _db.variant_pool(1, "underdark", "boss").is_empty())
Kit.check("全局决议:battle 节点恒无全局型", _db.global_variant_row(2, "underdark", "battle").is_empty())
Kit.check("全局决议:L2 boss 全局池空 → 空行(贫矿脉屏蔽后兜底回落承载)", _db.global_variant_row(2, "underdark", "boss").is_empty())
Kit.check("全局决议:L1 boss 全局池空 → 空行(兜底回落承载)", _db.global_variant_row(1, "underdark", "boss").is_empty() and _db.global_variant_row(1, "dungeon", "boss").is_empty())
Kit.check("elite 前向词位:boss 同池恒空(贫矿脉屏蔽后——词表位不造行)", _db.global_variant_row(2, "underdark", "elite").is_empty())
Kit.check("门控三维:缺省双参调用 = global 行跳过(既有调用口径零扰动)", _db.combat.variation.variant_pool(2, "underdark").is_empty() == false and _db.combat.variation.variant_pool(2, "underdark").size() == 2)
Kit.check("L1 Boss 免疫:unlock_level=2 门控在 boss 节点同样生效(教学保护带 B)", _db.combat.variation.variant_pool(1, "underdark", "boss").is_empty())
Kit.check("全局决议:battle 节点恒无全局型", _db.combat.variation.global_variant_row(2, "underdark", "battle").is_empty())
Kit.check("全局决议:L2 boss 全局池空 → 空行(贫矿脉屏蔽后兜底回落承载)", _db.combat.variation.global_variant_row(2, "underdark", "boss").is_empty())
Kit.check("全局决议:L1 boss 全局池空 → 空行(兜底回落承载)", _db.combat.variation.global_variant_row(1, "underdark", "boss").is_empty() and _db.combat.variation.global_variant_row(1, "dungeon", "boss").is_empty())
Kit.check("elite 前向词位:boss 同池恒空(贫矿脉屏蔽后——词表位不造行)", _db.combat.variation.global_variant_row(2, "underdark", "elite").is_empty())
# ③ 兜底回落:全局池空 → Boss 图改走 region 候选池(恰一行 region 变异 + 格子级布置 + 正常取号)
var crng = _CountVariantRng.new()
crng.set_master_seed(42)
var boss_settlement = load("res://core/battle_orchestrator.gd").new(_db, crng)
var boss_ok: bool = boss_settlement.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"})
var boss_ok: bool = boss_settlement.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"})
Kit.check("L2 Boss 战斗启动(node_depth=9 Boss 终点——兜底回落断言载体)", boss_ok)
var boss1_ids: Array = []
var boss1_variants: Dictionary = {}
@@ -216,7 +216,7 @@ func _smoke_global_variant() -> void:
var crng2 = _CountVariantRng.new()
crng2.set_master_seed(42)
var boss2 = load("res://core/battle_orchestrator.gd").new(_db, crng2)
if boss2.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}):
if boss2.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}):
var bb2 = boss2.minesweeper.current_board()
Kit.check("确定性:同种子 Boss 图两遍 board_variants/变异格/取号逐位相同(region 布置路径)", _arr_str_eq(bb2.board_variant_ids(), boss1_ids) and bb2.variants == boss1_variants and crng2.variant_calls == boss1_calls)
# ④ 资源面偏斜投影(RewardEconomy 纯函数面——守恒/白名单/构造例/合成序)
@@ -267,12 +267,12 @@ func _smoke_global_variant() -> void:
var nrng = load("res://core/rng_service.gd").new()
nrng.set_master_seed(42)
var normal1 = load("res://core/battle_orchestrator.gd").new(_db, nrng)
normal1.begin_battle({"level_index": 2, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)})
normal1.begin_battle({"level_index": 2, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)})
var n1: Dictionary = _board_resource_dump(normal1)
var nrng2 = load("res://core/rng_service.gd").new()
nrng2.set_master_seed(42)
var normal2 = load("res://core/battle_orchestrator.gd").new(_db, nrng2)
normal2.begin_battle({"level_index": 2, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)})
normal2.begin_battle({"level_index": 2, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)})
Kit.check("确定性:同种子普通图布置/摇号两遍逐位相同(off 全链路)", n1 == _board_resource_dump(normal2) and not n1.is_empty())
@@ -60,7 +60,7 @@ func _test_damage_controller_vocabularies() -> void:
# ---- D2↔B1:掷点用途标签 ∈ RngService 封闭词表(EC12——跨系统字面量契约的成员性断言) ----
func _test_rng_tag_closure(db) -> void:
var stream_label: String = String(db.settlement.damage_roll.armor_roll_stream)
var stream_label: String = String(db.combat.settlement.damage_roll.armor_roll_stream)
Kit.check("settlement.armor_roll_stream ∈ RngService 封闭标签词表(EC12)",
RngServiceScript.PURPOSE_TAGS.has(stream_label))
Kit.check("RngService 用途标签词表非空且互异(封闭语义由流改标签)",
@@ -102,8 +102,8 @@ func _test_status_boolean_acceptance(db) -> void:
var effects = StatusContainerScript.new(db)
# EC1 域闭:notify_battle_start 前授予 = false(受理面封闭)
var any_row: Dictionary = {}
if db.status_effects.rows.size() > 0:
any_row = db.status_effects.rows[0]
if db.combat.status_effects.rows.size() > 0:
any_row = db.combat.status_effects.rows[0]
Kit.check("前置:状态表非空", not any_row.is_empty())
if any_row.is_empty():
return
+76 -87
View File
@@ -44,15 +44,6 @@ func _smoke_find_token(candidates: Array, kind: String, tier: String) -> Diction
return {}
# 效果命中分桶(公开 dispatch 返回值的本地过滤——断言钉公开可观测面;编排器私有过滤器的测试侧等价承载)
func _hits_kind(hits: Array, kind: String) -> Array:
var out: Array = []
for h in hits:
if String((h as Dictionary).get("kind", "")) == kind:
out.append(h)
return out
# ② 冒险结构 API 闸(状态机受理面——重复开始拒绝 / 入口停点 / 放弃两态)
@@ -124,10 +115,10 @@ func _smoke_adventure() -> void:
var autoplay = load("res://sim/level_autoplay.gd")
# ① 表与覆盖义务(序列关数 = 行集关数;节点源 = 参数档在固定种子下的生成图,
# 每个战斗/Boss 节点经 ConfigDB 真实解析链查敌行集与棋盘行——顶 clamp 消解禁〔fv24〕)
Kit.check("序列关数 = 行集关数(三关正式序列生效)", int(_db.level.rows.size()) == 3)
Kit.check("序列关数 = 行集关数(三关正式序列生效)", int(_db.run.level.rows.size()) == 3)
var cov_miss := 0
var cov_scan := 0
for li in range(1, int(_db.level.rows.size()) + 1):
for li in range(1, int(_db.run.level.rows.size()) + 1):
for n in _level_active_nodes(li):
var nt := String(n.get("type", ""))
if nt != "battle" and nt != "boss":
@@ -135,10 +126,10 @@ func _smoke_adventure() -> void:
var d: int = int(n.get("depth", -1))
cov_scan += 1
var rk := "boss" if nt == "boss" else "battle"
var rs: Array[Dictionary] = _db.resolve_enemy_rowset(li, d, rk)
var rs: Array[Dictionary] = _db.combat.enemy.resolve_rowset(li, d, rk)
if rs.is_empty() or int(rs[0].get("depth_band", -1)) < d:
cov_miss += 1
if not (_db.resolve_board_gen(li, d) as Dictionary).is_empty():
if not (_db.combat.board_gen.resolve_row(li, d) as Dictionary).is_empty():
continue
cov_miss += 1
Kit.check("生成图每个战斗/Boss 节点经解析链命中敌行集与棋盘行(扫描 %d 节点,缺失 %d)〔fv24〕" % [cov_scan, cov_miss],
@@ -149,8 +140,8 @@ func _smoke_adventure() -> void:
Kit.check("满配冒险成功(42 / atk+900 / hp+400——cleared=true;v2 怪阶梯后旧 200/100 档不再满配)", String(st["result"]) == "completed" and bool(sum.get("cleared", false)))
Kit.check("完成几关 = 3(levels_cleared)", int(sum.get("levels_cleared", -1)) == 3)
var ks_exp := 0
for li in range(1, int(_db.level.rows.size()) + 1):
ks_exp += int(_db.meta_boss_keystone_row(li).get("first_kill", 0))
for li in range(1, int(_db.run.level.rows.size()) + 1):
ks_exp += int(_db.crossrun.meta.boss_keystone_row(li).get("first_kill", 0))
Kit.check("钥石合计 = ΣBoss 首杀行值(%d——逐杀掉落新口径;断点未注入恒首杀)" % int(sum.get("keystone_total", -1)), int(sum.get("keystone_total", -1)) == ks_exp)
var drops: Array = sum.get("keystone_drops", []) as Array
Kit.check("逐杀掉落明细恰 3 笔(L1/L2/L3——additive 摘要键 keystone_drops)", drops.size() == 3)
@@ -169,7 +160,7 @@ func _smoke_adventure() -> void:
# 奖励节点在生成图里是可选路位(旧单链模板的「L1 必经 1 个」形状前提随模板消亡)——
# 接线证明拆两半:图侧每关含 ≥1 奖励节点,行进侧种子扫描内确有经停(扫描零经停=三选一接线不可达)
var reward_levels := 0
for li in range(1, int(_db.level.rows.size()) + 1):
for li in range(1, int(_db.run.level.rows.size()) + 1):
var rc := 0
for n in _level_active_nodes(li):
if String(n.get("type", "")) == "reward":
@@ -217,8 +208,8 @@ func _smoke_adventure() -> void:
var cand_ok := picks.size() == 2
var ks_gain_ok := picks.size() == 2
var resolve_ok := level_resolves.size() == 2
var max_flat := int(_db.adventure.curio_value_max_hp_common)
var heal_pct := int(_db.adventure.clear_heal_pct)
var max_flat := int(_db.run.adventure.value_segment.curio_value_max_hp_common)
var heal_pct := int(_db.run.adventure.clear_heal_pct)
var hp_acc := 0 # max_hp 件累加(回血封顶断言的生效上限推导——TP0 起按兑付令牌实值,大小档分别计)
var ks_prefix := 0
for i in picks.size():
@@ -240,18 +231,18 @@ func _smoke_adventure() -> void:
# 账本生效上限口径:置满/回血封顶按生效上限(flat 后乘调试浮窗 pct)——本段 pct=400
# 加算区 = 树/锻造件(hp_acc)+购血观测值(level_autoplay 停点快照 bonus_max_hp)——精确断言不再窗口拟合
var extra_obs := int(p.get("bonus_max_hp", -1))
var m_obs := int(round(float(int(_db.hero.max_hp) + hp_acc + extra_obs) * 5.0))
var m_obs := int(round(float(int(_db.combat.hero.max_hp) + hp_acc + extra_obs) * 5.0))
var heal_exp := mini(int(floor(float(m_obs) * float(heal_pct) / 100.0)), m_obs - hp_before)
if extra_obs < 0 or heal != heal_exp or hp_after != hp_before + heal or hp_after > m_obs:
heal_ok = false
ks_prefix += int(_db.meta_boss_keystone_row(int(p.get("level_index", -1))).get("first_kill", 0))
if int(p.get("keystone_gained", -1)) != int(_db.meta_boss_keystone_row(int(p.get("level_index", -1))).get("first_kill", 0)) or int(p.get("keystone_total", -1)) != ks_prefix:
ks_prefix += int(_db.crossrun.meta.boss_keystone_row(int(p.get("level_index", -1))).get("first_kill", 0))
if int(p.get("keystone_gained", -1)) != int(_db.crossrun.meta.boss_keystone_row(int(p.get("level_index", -1))).get("first_kill", 0)) or int(p.get("keystone_total", -1)) != ks_prefix:
ks_gain_ok = false
if resolve_ok:
var r: Dictionary = level_resolves[i]
var rk := String(r.get("picked", ""))
var rt := String(r.get("tier", ""))
if _smoke_find_token(p.get("candidates", []) as Array, rk, rt).is_empty() or int(r.get("value", -1)) != int(_db.adventure.curio_value(rk, rt)):
if _smoke_find_token(p.get("candidates", []) as Array, rk, rt).is_empty() or int(r.get("value", -1)) != int(_db.run.adventure.curio_value(rk, rt)):
resolve_ok = false
if rk == "max_hp":
hp_acc += int(r.get("value", max_flat))
@@ -261,10 +252,10 @@ func _smoke_adventure() -> void:
Kit.check("点选恰兑付选中一件(值=令牌档位表值;未选项零生效)", resolve_ok)
var seq: Array = st["encounter_max_hp_seq"]
var k := 5.0 # hp+400 ⇒ 调试浮窗百分比后乘 ×5(加算先加、百分比后乘——生效次序口径)
var exp_base := maxi(1, int(round(float(int(_db.hero.max_hp)) * k)))
var exp_base := maxi(1, int(round(float(int(_db.combat.hero.max_hp)) * k)))
# 模拟档 v3.4 商店贪心购血合法抬升——上限窗口 = 基础 + 加算区 3 件 + 购血余量;
# 大生命 +10(优秀档)——上界按大件最坏构成 3×10+购血 15=+45(TP0 放宽承袭)
var hp_ceiling := maxi(1, int(round(float(int(_db.hero.max_hp) + 3 * int(_db.adventure.curio_value("max_hp", "excellent")) + 15) * k)))
var hp_ceiling := maxi(1, int(round(float(int(_db.combat.hero.max_hp) + 3 * int(_db.run.adventure.curio_value("max_hp", "excellent")) + 15) * k)))
var seq_ok := seq.size() > 0 and int(seq[0]) == exp_base
for v in (seq as Array):
if int(v) < exp_base or int(v) > hp_ceiling:
@@ -303,7 +294,7 @@ func _smoke_adventure() -> void:
var sh: Dictionary = half["st"]["summary"]
Kit.check("L2 败北摘要:L1 已过(钥石=L1 首杀掉落)· 止步第 2 层",
not bool(sh["cleared"]) and int(sh["levels_cleared"]) == 1 \
and int(sh["keystone_total"]) == int(_db.meta_boss_keystone_row(1).get("first_kill", 0)) \
and int(sh["keystone_total"]) == int(_db.crossrun.meta.boss_keystone_row(1).get("first_kill", 0)) \
and int(sh["stopped_at"]) == 2)
var dh: Dictionary = sh.get("defeat", {})
Kit.check("L2 败北败退明细:层号=2 · 节点在案(数值验证取数面 2026-09-22)",
@@ -353,22 +344,20 @@ func _smoke_adventure() -> void:
var r8 = a8.runner()
var gp0: int = int(r8.gp)
var hp_price := -1
for p in _db.shop.products:
for p in _db.run.shop.products:
if String(p.get("product_id", "")) == "hp_up":
hp_price = int(p.get("base_price", -1))
var l2_price_bonus := 0
for pb in _db.shop.price_level_bonus:
for pb in _db.run.shop.price_level_bonus:
if int(pb.get("level_index", -1)) == 2:
l2_price_bonus = int(pb.get("bonus", 0))
Kit.check("进店余额 ≥ hp_up L2 首价(%d = 基价 %d + 关卡加成 %d——配置推导)" % [hp_price + l2_price_bonus, hp_price, l2_price_bonus], gp0 >= hp_price + l2_price_bonus)
Kit.check("购买 hp_up 受理(出账 + 加算区)", r8.buy_product("hp_up"))
var acc_shop: int = a8.journal().bonus_total("max_hp") # L1 三选一后 + 商店 3(首件随种子)
Kit.check("统一冒险加算区含商店值(max_hp=%d ≥ 商店 3——L1 三选一件值另计)" % acc_shop, acc_shop >= 3)
var l2_cap := {"payload": {}} # 公开信号面采集(curio_choice_presented 停点载荷——容器写回规避 lambda 按值捕获)
a8.stop().curio_choice_presented.connect(func(p): l2_cap["payload"] = p)
a8._on_level_finished({"result": "victory", "hp": int(r8.hero_hp), "node_index": r8.node_index,
"node_id": r8.current_node_id(), "node_type": "shop"}) # 模拟 L2 关卡完成(victory 载荷直调——非末关)
Kit.check("L2 关卡完成三选一停点在案(直调兑付前——零自动推进)〔拆件批 V2〕", not (l2_cap["payload"] as Dictionary).is_empty())
Kit.check("L2 关卡完成三选一停点在案(直调兑付前——零自动推进)〔拆件批 V2〕", not a8.stop()._pending.is_empty())
Kit.check("未兑付推进拒绝(选择停点零自动推进契约)", not a8.continue_to_next_level())
var l2_cands: Array = a8.stop()._pending["candidates"]
var l2_pick: Dictionary = l2_cands[0]
@@ -448,8 +437,8 @@ func _smoke_adventure() -> void:
a13.stop().present_node_curio(ncands13)
a13.stop().present_node_curio([{"kind": "gp", "tier": "common"}])
var gp13: int = a13.wallet().gp
Kit.check("奖励节点 GP 件入账(钱包「奖励入账」收口——+%d)" % int(_db.adventure.curio_value("gp", "common")), a13.stop().apply_node_curio("gp", "common") and a13.wallet().gp == gp13 + int(_db.adventure.curio_value("gp", "common")))
Kit.check("节点停点态兑付后清零(恰一次语义同构)", (a13.stop().node_curio_pending_state().get("candidates", []) as Array).is_empty())
Kit.check("奖励节点 GP 件入账(钱包「奖励入账」收口——+%d)" % int(_db.run.adventure.curio_value("gp", "common")), a13.stop().apply_node_curio("gp", "common") and a13.wallet().gp == gp13 + int(_db.run.adventure.curio_value("gp", "common")))
Kit.check("节点停点态兑付后清零(恰一次语义同构)", a13.stop()._node_pending.is_empty())
var s10: Dictionary = a10.summary
Kit.check("放弃摘要:abandoned · 未打穿(已获大奖励不追回——同失败型结算)", String(s10.get("result", "")) == "abandoned" and not bool(s10.get("cleared", true)))
@@ -462,8 +451,8 @@ func _smoke_adventure() -> void:
func _smoke_feats() -> void:
print("--- 专长重设计实装(feat_table 四级 · 专长带池接线 · 订阅面逐件)---")
# ① 现行表牌库构成:30 件全量入池——102 数值令牌+30 relic 令牌(四级 rarity 直映射)
Kit.check("feat_table 已加载(30 件=基础 27+迁入 2+卡② 完美保险 1——迭代 31 卡② 换代)", _db.feats != null and (_db.feats.rows as Array).size() == 30)
Kit.check("feat_table.validate 零违例(四级 rarity/cap 域/trigger 词表/行键白名单全过)", _db.feats.validate().is_empty())
Kit.check("feat_table 已加载(30 件=基础 27+迁入 2+卡② 完美保险 1——迭代 31 卡② 换代)", _db.feats != null and (_db.crossrun.feats.rows as Array).size() == 30)
Kit.check("feat_table.validate 零违例(四级 rarity/cap 域/trigger 词表/行键白名单全过)", _db.crossrun.feats.validate().is_empty())
var a_r0 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(71))
Kit.check("专长带场景:冒险启动(seed 71)", a_r0.start_adventure(71))
var feat_tokens := 0
@@ -473,34 +462,34 @@ func _smoke_feats() -> void:
feat_tokens += 1
var tk := String((t as Dictionary).get("tier", ""))
tier_counts[tk] = int(tier_counts.get(tk, 0)) + 1
Kit.check("专长带入池:30 件四级直映射(普通 12/优秀 9/稀有 9/传说 0)——牌库 102+30=132〔专长批 V2+卡② 完美保险迁入〕", feat_tokens == 30 and int(tier_counts.get("common", 0)) == 12 and int(tier_counts.get("excellent", 0)) == 9 and int(tier_counts.get("rare", 0)) == 9 and a_r0.deck().deck.size() == 132)
Kit.check("专长带入池:30 件四级直映射(普通 12/优秀 9/稀有 9/传说 0)——牌库 102+30=132〔专长批 V2+卡② 完美保险迁入〕", feat_tokens == 30 and int(tier_counts.get("common", 0)) == 12 and int(tier_counts.get("excellent", 0)) == 9 and int(tier_counts.get("rare", 0)) == 9 and a_r0._curio.deck.size() == 132)
# ② 旧 27 件零残留钉面(V2——arcane_ember 系旧件表首行;全仓 grep 零残留归静态核查)
Kit.check("旧 27 件已退役:arcane_ember/magma_body 表内零残留〔专长批 V2〕", _db.feats.row_by_id("arcane_ember").is_empty() and _db.feats.row_by_id("magma_body").is_empty())
Kit.check("迁出收尾:treasure_items 无 venom_edge/seal_burst 行(同调目录自然收窄)〔专长批 V13〕", _db.treasure_items.row_by_id("venom_edge").is_empty() and _db.treasure_items.row_by_id("seal_burst").is_empty())
Kit.check("旧 27 件已退役:arcane_ember/magma_body 表内零残留〔专长批 V2〕", _db.crossrun.feats.row_by_id("arcane_ember").is_empty() and _db.crossrun.feats.row_by_id("magma_body").is_empty())
Kit.check("迁出收尾:treasure_items 无 venom_edge/seal_burst 行(同调目录自然收窄)〔专长批 V13〕", _db.crossrun.treasure_items.row_by_id("venom_edge").is_empty() and _db.crossrun.treasure_items.row_by_id("seal_burst").is_empty())
# ③ 契约面静态钉扎(V18/V19/V21/V22——人工核对 V20/V21 裁决面以外的可机检半)
var kinds_ok := true
for t in a_r0._curio.deck:
var kk := String((t as Dictionary).get("kind", ""))
if kk != "relic" and not _db.adventure.CURIO_KINDS.has(kk):
if kk != "relic" and not _db.run.adventure.CURIO_KINDS.has(kk):
kinds_ok = false
Kit.check("层级纪律:牌库 kind 全集=CURIO_KINDS+relic(秘宝 item_id 零入池)〔专长批 V18〕", kinds_ok)
var rewrite_rows := 0
for row_v in (_db.feats.rows as Array):
for row_v in (_db.crossrun.feats.rows as Array):
for tv in ((row_v as Dictionary).get("triggers", []) as Array):
if String(((tv as Dictionary).get("do", {}) as Dictionary).get("kind", "")) == "rewrite_board":
rewrite_rows += 1
Kit.check("棋盘事实不可变:新 30 行零棋盘改写类订阅(rewrite_board 已除名消费面)〔专长批 V19〕", rewrite_rows == 0)
var bad_words := 0
for row_v in (_db.feats.rows as Array):
for row_v in (_db.crossrun.feats.rows as Array):
var r_d: Dictionary = row_v as Dictionary
for txt_v in [String(r_d.get("display_name", "")), String(r_d.get("desc", ""))]:
if String(txt_v).contains("雷") or String(txt_v).contains("旗"):
bad_words += 1
Kit.check("对外命名纪律:30 行 display_name/desc 零「雷/旗」字面(封印/陷阱风味系)〔专长批 V21 初筛〕", bad_words == 0)
Kit.check("数值纪律:陷阱爆发倍率>1 · 血蜜回复<典型陷阱伤 · 石肤上限 3 在册(结构锚形态内 provisional)〔专长批 V22 机检半〕",
float((_db.feats.row_by_id("trap_burst").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("mult", 0)) > 1.0
and int((_db.feats.row_by_id("blood_honey").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("value", 99)) < 5
and int((_db.feats.row_by_id("stone_skin").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("cap", 0)) == 3)
float((_db.crossrun.feats.row_by_id("trap_burst").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("mult", 0)) > 1.0
and int((_db.crossrun.feats.row_by_id("blood_honey").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("value", 99)) < 5
and int((_db.crossrun.feats.row_by_id("stone_skin").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("cap", 0)) == 3)
# ④ 临时表注入(新 schema 形态——零 archetype/cap 直用):5 行 relic 入牌库/兑付/踢池/断点往返
var rt := FeatTable.new()
rt.rows = [
@@ -524,7 +513,7 @@ func _smoke_feats() -> void:
for t in a_r._curio.deck:
if String((t as Dictionary).get("kind", "")) == "relic":
deck_relic += 1
Kit.check("专长带入池:5 行 relic 每件 1 张(四级 rarity 直映射)——牌库 102+5=107", deck_relic == 5 and a_r.deck().deck.size() == 107)
Kit.check("专长带入池:5 行 relic 每件 1 张(四级 rarity 直映射)——牌库 102+5=107", deck_relic == 5 and a_r._curio.deck.size() == 107)
# 渠道白名单断言(渠道轴定位:关卡完成渠道只出专长/节点渠道零专长)
var feat_seen: Dictionary = {}
var node_relic_seen := false
@@ -558,12 +547,12 @@ func _smoke_feats() -> void:
Kit.check("A5 专长面兑付受理(专长凭证开出候选可正常入持有集)", (a_t.feats_held() as Array).size() == feats_n + 1)
# A6 保底:pity 直填 5 → 下次展示必出(第 6 次必出口径);限购满 → 恒不触发
a_t.stop()._deck.ticket_sold = 0 # A5 已兑付一枚专长凭证(sold=1=limit)——A6 段重置计数隔离验证
a_t.stop()._deck.ticket_pity = int(a_t.db.adventure.feat_ticket_pity_step)
a_t.stop()._deck.ticket_pity = int(a_t.db.run.adventure.feat_ticket_pity_step)
var rg_t: RandomNumberGenerator = a_t.rng.open_stream("reward_pick")
Kit.check("A6 保底触发(pity=5 → 第 6 次展示必出——概率无关)", a_t.deck().feat_ticket_consider(rg_t))
Kit.check("A6 触发后清零", a_t.deck().ticket_pity == 0)
a_t.stop()._deck.ticket_sold = int(a_t.db.adventure.feat_ticket_limit)
a_t.stop()._deck.ticket_pity = int(a_t.db.adventure.feat_ticket_pity_step)
Kit.check("A6 触发后清零", a_t.stop()._deck.ticket_pity == 0)
a_t.stop()._deck.ticket_sold = int(a_t.db.run.adventure.feat_ticket_limit)
a_t.stop()._deck.ticket_pity = int(a_t.db.run.adventure.feat_ticket_pity_step)
Kit.check("A6 限购满不再触发(保底冻结)", not a_t.deck().feat_ticket_consider(rg_t))
a_t.stop()._deck.ticket_sold = 0
a_t.stop()._deck.ticket_pity = 0
@@ -604,13 +593,13 @@ func _smoke_feats() -> void:
fx.rebuild(["geologist"], [], [])
var geo_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"new_resource_type": true})
apply_fx.call(geo_hits)
Kit.check("地质学家:首开新类型集 1 点〔专长批 V3〕", _hits_kind(geo_hits, "counter_add").size() == 1)
Kit.check("地质学家:首开新类型集 1 点〔专长批 V3〕", orch._hits_of(geo_hits, "counter_add").size() == 1)
EffectEngine.counter_add(orch.fx_host().counters_ctx, "geo_types", 2) # 手动基数补齐(row1 已落账 1+2=3——满 4 种的预评估现值)
var geo_hits2: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"new_resource_type": true})
apply_fx.call(geo_hits2)
Kit.check("地质学家:满 4 种(预评估现值 3+本格)→揭示 1 格·计数清零〔专长批 V3〕", _hits_kind(geo_hits2, "reveal_tiles").size() == 1 and EffectEngine.counter(orch.fx_host().counters_ctx, "geo_types") == 0)
Kit.check("地质学家:满 4 种(预评估现值 3+本格)→揭示 1 格·计数清零〔专长批 V3〕", orch._hits_of(geo_hits2, "reveal_tiles").size() == 1 and EffectEngine.counter(orch.fx_host().counters_ctx, "geo_types") == 0)
var geo_hits3: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"new_resource_type": false})
Kit.check("地质学家反例:同类型重复首开不叠点〔专长批 V3〕", _hits_kind(geo_hits3, "counter_add").is_empty())
Kit.check("地质学家反例:同类型重复首开不叠点〔专长批 V3〕", orch._hits_of(geo_hits3, "counter_add").is_empty())
fx.rebuild(["hard_hat"], [], [])
apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {}))
apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {}))
@@ -624,29 +613,29 @@ func _smoke_feats() -> void:
fx.rebuild(["skip_mining"], [], [])
var sk_zero: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_blank_zero": true})
apply_fx.call(sk_zero)
Kit.check("跳格采掘:空白 0 格攒 1 点〔专长批 V3〕", _hits_kind(sk_zero, "counter_add").size() == 1)
Kit.check("跳格采掘:空白 0 格攒 1 点〔专长批 V3〕", orch._hits_of(sk_zero, "counter_add").size() == 1)
EffectEngine.counter_add(orch.fx_host().counters_ctx, "skip_pts", 1) # 手动基数补齐(已落账 1+1=2——满 3 点的预评估现值)
var sk_full: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_blank_zero": true})
apply_fx.call(sk_full)
Kit.check("跳格采掘:满 3 点→揭示 1 格〔专长批 V3〕", _hits_kind(sk_full, "reveal_tiles").size() == 1)
Kit.check("跳格采掘反例:普通开格不计点〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_blank_zero": false}), "counter_add").is_empty())
Kit.check("跳格采掘:满 3 点→揭示 1 格〔专长批 V3〕", orch._hits_of(sk_full, "reveal_tiles").size() == 1)
Kit.check("跳格采掘反例:普通开格不计点〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_blank_zero": false}), "counter_add").is_empty())
fx.rebuild(["blast_accounting"], [], [])
for i in 5:
var ba_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_paid", {"ap_cost": 1})
for h in ba_hits:
if String(h.get("kind", "")) == "counter_add":
EffectEngine.counter_add(orch.fx_host().counters_ctx, String(h.get("counter_id", "")), 1)
Kit.check("爆破会计:本轮挖格花费≥5→+1 GP〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "round_end", {}), "grant_gp").size() == 1)
Kit.check("爆破会计:本轮挖格花费≥5→+1 GP〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "round_end", {}), "grant_gp").size() == 1)
fx.rebuild(["frontier_miner"], [], [])
Kit.check("边陲矿工:边缘格产出 +1·中心格反例零产出〔专长批 V3〕",
_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_edge_cell": true}), "grant_material").size() == 1
and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_edge_cell": false}), "grant_material").is_empty())
orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_edge_cell": true}), "grant_material").size() == 1
and orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_edge_cell": false}), "grant_material").is_empty())
# 封印系
fx.rebuild(["seal_echo"], [], [])
apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}))
apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}))
var se_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "round_end", {})
Kit.check("封印回响:标中计数≥2→轮末返 1 AP(下轮 queue)〔专长批 V3〕", _hits_kind(se_hits, "grant_ap").size() == 1 and String((_hits_kind(se_hits, "grant_ap")[0] as Dictionary).get("mode", "")) == "queue")
Kit.check("封印回响:标中计数≥2→轮末返 1 AP(下轮 queue)〔专长批 V3〕", orch._hits_of(se_hits, "grant_ap").size() == 1 and String((orch._hits_of(se_hits, "grant_ap")[0] as Dictionary).get("mode", "")) == "queue")
fx.rebuild(["seal_smith"], [], [])
var sm_remove := 0
for i in 3:
@@ -658,10 +647,10 @@ func _smoke_feats() -> void:
Kit.check("封印匠:标中每第 3 次→中立移除恰一次(频率闸死)〔专长批 V3〕", sm_remove == 1)
fx.rebuild(["seal_stake"], [], [])
Kit.check("封印桩:标中本轮 AC +N(round 域——round_start 清零)〔专长批 V3〕",
_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "ac_bonus").size() == 1
and String((_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "ac_bonus")[0] as Dictionary).get("mode", "")) == "round")
orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "ac_bonus").size() == 1
and String((orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "ac_bonus")[0] as Dictionary).get("mode", "")) == "round")
fx.rebuild(["warning_beacon"], [], [])
Kit.check("示警烟火:标中揭示 3×3 资源类型(不揭安危位置)〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "preview_area").size() == 1)
Kit.check("示警烟火:标中揭示 3×3 资源类型(不揭安危位置)〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "preview_area").size() == 1)
# 踩雷/受伤系
fx.rebuild(["pain_memory"], [], [])
apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}))
@@ -677,13 +666,13 @@ func _smoke_feats() -> void:
for h in fw_hits:
if String(h.get("kind", "")) == "shield":
fw_shield += int(h.get("value", 0))
Kit.check("保险丝反例:单次 <5 不攒点不触发〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "terrain", "amount": 3}), "shield").is_empty())
Kit.check("保险丝反例:单次 <5 不攒点不触发〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "terrain", "amount": 3}), "shield").is_empty())
for h in fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "terrain", "amount": 6}):
if String(h.get("kind", "")) == "counter_add":
EffectEngine.counter_add(orch.fx_host().counters_ctx, String(h.get("counter_id", "")), int(h.get("value", 0)))
Kit.check("保险丝:首点不出盾(2 点才出——预评估口径)〔专长批 V3〕", fw_shield == 0)
fx.rebuild(["blood_insight"], [], [])
Kit.check("见血开窍:踩雷揭示 1 格〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "reveal_tiles").size() == 1)
Kit.check("见血开窍:踩雷揭示 1 格〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "reveal_tiles").size() == 1)
fx.rebuild(["trap_instinct"], [], [])
apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}))
var ti_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0})
@@ -699,67 +688,67 @@ func _smoke_feats() -> void:
fx.rebuild(["trap_burst"], [], [])
var tb_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"damage": 8})
Kit.check("陷阱爆发:全体敌雷伤×倍率直伤(blast_scaled——施加面值=damage×mult)〔专长批 V3〕",
_hits_kind(tb_hits, "blast_scaled").size() == 1
and float((_hits_kind(tb_hits, "blast_scaled")[0] as Dictionary).get("mult", 0)) > 1.0)
orch._hits_of(tb_hits, "blast_scaled").size() == 1
and float((orch._hits_of(tb_hits, "blast_scaled")[0] as Dictionary).get("mult", 0)) > 1.0)
fx.rebuild(["blood_honey"], [], [])
Kit.check("血蜜:踩雷回复 N 生命〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "heal").size() == 1)
Kit.check("血蜜:踩雷回复 N 生命〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "heal").size() == 1)
fx.rebuild(["stone_skin"], [], [])
var ss_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 1})
Kit.check("石肤:受伤 AC+1(本场域 cap 3——达上限不再涨由累积器封顶)〔专长批 V3〕",
_hits_kind(ss_hits, "ac_bonus").size() == 1
and int((_hits_kind(ss_hits, "ac_bonus")[0] as Dictionary).get("cap", 0)) == 3)
orch._hits_of(ss_hits, "ac_bonus").size() == 1
and int((orch._hits_of(ss_hits, "ac_bonus")[0] as Dictionary).get("cap", 0)) == 3)
fx.rebuild(["last_stand"], [], [])
Kit.check("背水:生命≤50% 减伤·过半血反例零减伤〔专长批 V3〕",
_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 1, "hp_ratio": 40}), "damage_reduce").size() == 1
and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 2, "hp_ratio": 80}), "damage_reduce").is_empty())
orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 1, "hp_ratio": 40}), "damage_reduce").size() == 1
and orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 2, "hp_ratio": 80}), "damage_reduce").is_empty())
# 跨族桥
fx.rebuild(["breaker_strike"], [], [])
Kit.check("破封一击:挖开带封印危险格 +1 层·无封印反例零触发〔专长批 V3〕",
_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"dug_under_seal": true}), "counter_add").size() == 1
and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"dug_under_seal": false}), "counter_add").is_empty())
orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"dug_under_seal": true}), "counter_add").size() == 1
and orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"dug_under_seal": false}), "counter_add").is_empty())
# 节奏系
fx.rebuild(["map_exchange"], [], [])
var me_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_stop", {"ap_left": 4})
apply_fx.call(me_hits)
EffectEngine.counter_add(orch.fx_host().counters_ctx, "stop_swap", 4) # value_event="ap_left" 解析归施加面(action_economy 载荷取值)——冒烟手动落账
Kit.check("弃图兑换:停手剩余 AP≥3 入账〔专长批 V3〕", _hits_kind(me_hits, "counter_add").size() == 1)
Kit.check("弃图兑换:停手剩余 AP≥3 入账〔专长批 V3〕", orch._hits_of(me_hits, "counter_add").size() == 1)
var me_atk := 0
for h in orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0}), "attack_bonus"):
me_atk = EffectEngine.counter(orch.fx_host().counters_ctx, String(h.get("value_counter", "")))
Kit.check("弃图兑换:折为攻击伤害=剩余 AP 全额·用后清零〔专长批 V3〕", me_atk == 4)
Kit.check("弃图兑换反例:剩余 AP<3 不触发〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "dig_stop", {"ap_left": 2}), "counter_add").is_empty())
Kit.check("弃图兑换反例:剩余 AP<3 不触发〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "dig_stop", {"ap_left": 2}), "counter_add").is_empty())
fx.rebuild(["last_drop"], [], [])
var ld_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "round_start", {"round": 2, "ap_exhausted_last": true})
Kit.check("一滴不剩:上轮 AP 耗尽→+1 AP〔专长批 V3〕", _hits_kind(ld_hits, "grant_ap").size() == 1)
Kit.check("一滴不剩反例:上轮有余不发〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "round_start", {"round": 3, "ap_exhausted_last": false}), "grant_ap").is_empty())
Kit.check("一滴不剩:上轮 AP 耗尽→+1 AP〔专长批 V3〕", orch._hits_of(ld_hits, "grant_ap").size() == 1)
Kit.check("一滴不剩反例:上轮有余不发〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "round_start", {"round": 3, "ap_exhausted_last": false}), "grant_ap").is_empty())
fx.rebuild(["market_close"], [], [])
for i in 8:
for h in fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {}):
if String(h.get("kind", "")) == "counter_add":
EffectEngine.counter_add(orch.fx_host().counters_ctx, String(h.get("counter_id", "")), 1)
Kit.check("收盘:本轮开格≥8→+1 GP〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "round_end", {}), "grant_gp").size() == 1)
Kit.check("收盘:本轮开格≥8→+1 GP〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "round_end", {}), "grant_gp").size() == 1)
# 战斗系
fx.rebuild(["execution_scholar"], [], [])
Kit.check("斩首学者:击杀揭示 1 格〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "kill", {"target": "敌", "overflow": 0}), "reveal_tiles").size() == 1)
Kit.check("斩首学者:击杀揭示 1 格〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "kill", {"target": "敌", "overflow": 0}), "reveal_tiles").size() == 1)
fx.rebuild(["executioner"], [], [])
var ex_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "enemy_hp_threshold", {"unit": "e1", "pct": 30, "is_boss": false})
Kit.check("处刑台:敌血量≤30% 挂单位标记加伤·阈值外反例零命中〔专长批 V3〕",
_hits_kind(ex_hits, "target_attack_bonus").size() == 1
and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "enemy_hp_threshold", {"unit": "e1", "pct": 60, "is_boss": false}), "target_attack_bonus").is_empty())
orch._hits_of(ex_hits, "target_attack_bonus").size() == 1
and orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "enemy_hp_threshold", {"unit": "e1", "pct": 60, "is_boss": false}), "target_attack_bonus").is_empty())
fx.rebuild(["field_dressing"], [], [])
Kit.check("战地包扎:战斗胜利回复 N 生命(battle_won 新挂点)〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "battle_won", {"round": 2}), "heal").size() == 1)
Kit.check("战地包扎:战斗胜利回复 N 生命(battle_won 新挂点)〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "battle_won", {"round": 2}), "heal").size() == 1)
# 契约尖点
fx.rebuild(["blood_tax"], [], [])
Kit.check("血税:双向同局账本(踩陷阱−2max_hp/封印命中+1atk——bonus_delta 战末汇入)〔专长批 V3〕",
_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "bonus_delta").size() == 1
and String((_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "bonus_delta")[0] as Dictionary).get("axis", "")) == "atk")
orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "bonus_delta").size() == 1
and String((orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "bonus_delta")[0] as Dictionary).get("axis", "")) == "atk")
# 迁入 2 件
fx.rebuild(["venom_edge"], [], [])
Kit.check("淬毒刃纹(迁入):克制正面(magic_vs_heavy 矩阵值>0)追加·中性面反例零触发〔专长批 V3〕",
_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "clamp_check", {"matchup": "magic_vs_heavy"}), "modify_ticket").size() == 1
and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "clamp_check", {"matchup": "normal_vs_light"}), "modify_ticket").is_empty())
orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "clamp_check", {"matchup": "magic_vs_heavy"}), "modify_ticket").size() == 1
and orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "clamp_check", {"matchup": "normal_vs_light"}), "modify_ticket").is_empty())
fx.rebuild(["seal_burst"], [], [])
Kit.check("封印爆裂(迁入):标中全体敌群伤〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "open_ticket").size() == 1)
Kit.check("封印爆裂(迁入):标中全体敌群伤〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "open_ticket").size() == 1)
# 零持有
var orch0 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(74))
orch0.fx_host().rebuild_subscriptions([], [], [])
@@ -910,4 +899,4 @@ func _fresh_rng(seed_val: int):
# 生成图取数面(图 = f(关卡行参数档, 种子 42)——与 runner 起关同图;节点键 id/type/depth/next)
func _level_active_nodes(level_index: int) -> Array:
var gen = NodeMapGenerator.new(_fresh_rng(42))
return (gen.generate(_db.level_gen_params_row(level_index), 42) as Dictionary).get("nodes", [])
return (gen.generate(_db.run.level_gen_params.row_by_level_index(level_index), 42) as Dictionary).get("nodes", [])
+23 -17
View File
@@ -105,7 +105,7 @@ func _smoke_reward_identity_unit() -> void:
var cs2: Dictionary = reward_economy.grant_clear()
Kit.check("清图批尾身份 = clear_blast(clear_default 唯一活行)", String(cs1.cell_type) == "blast" and String(cs1.reward_id) == "clear_blast")
var cb_range := [-1, -1]
for row in _db.reward.rows:
for row in _db.run.reward.rows:
if String(row.reward_id) == "clear_blast":
cb_range = [int(row.get("power_min", row.power)), int(row.get("power_max", row.power))]
Kit.check("清图奖励 power ∈ 表区间 [%d, %d](roll 值 %d——配置推导)" % [cb_range[0], cb_range[1], int(cs1.power)],
@@ -120,9 +120,13 @@ func _smoke_reward_identity_unit() -> void:
# 板行覆盖桩(cap 压线行为断言;接口面 = resolve_board_gen 单点)
class _BoardRowStub:
var row: Dictionary
func resolve_board_gen(_L: int, _D: int) -> Dictionary:
return row
class _StubBoardGen:
var row: Dictionary
func resolve_row(_L: int, _D: int) -> Dictionary:
return row
class _StubCombat:
var board_gen := _StubBoardGen.new()
var combat := _StubCombat.new()
func status_effect_row(_id: String) -> Dictionary:
return {}
@@ -131,27 +135,27 @@ func _smoke_board_generator() -> void:
print("--- 棋盘生成 ---")
var board_generator = load("res://core/board_generator.gd").new()
for L in [1, 2]:
var row = _db.resolve_board_gen(L, 0)
var row = _db.combat.board_gen.resolve_row(L, 0)
Kit.check("BoardGen 行解析 (%d,0)" % L, not row.is_empty())
if L == 2:
# 迭代 35 生成图深度扩面覆盖批随动(声明):旧口径「(2,3) 无精确行→ceil 上取到 4」的判据对象
# 是模板时代键域缺口;覆盖批补齐 0–13 后 (2,3) 有精确行,生产表上不再存在可走 ceil 的空位。
# 判据不丢——精确命中改判在册,ceil 能力用「临时摘除该行」的夹具面保留,随后原样还原。
var exact_row: Dictionary = _db.resolve_board_gen(2, 3)
var exact_row: Dictionary = _db.combat.board_gen.resolve_row(2, 3)
Kit.check("BoardGen 精确命中 (2,3)(覆盖批键域补齐)", int(exact_row.get("node_depth", -1)) == 3)
var gap_idx := -1
for i in _db.board_gen.rows.size():
if int(_db.board_gen.rows[i].get("level_index", -1)) == 2 and int(_db.board_gen.rows[i].get("node_depth", -1)) == 3:
for i in _db.combat.board_gen.rows.size():
if int(_db.combat.board_gen.rows[i].get("level_index", -1)) == 2 and int(_db.combat.board_gen.rows[i].get("node_depth", -1)) == 3:
gap_idx = i
break
var saved: Dictionary = _db.board_gen.rows[gap_idx]
var rows_before: int = _db.board_gen.rows.size()
_db.board_gen.rows.remove_at(gap_idx)
var ceil_row: Dictionary = _db.resolve_board_gen(2, 3)
var saved: Dictionary = _db.combat.board_gen.rows[gap_idx]
var rows_before: int = _db.combat.board_gen.rows.size()
_db.combat.board_gen.rows.remove_at(gap_idx)
var ceil_row: Dictionary = _db.combat.board_gen.resolve_row(2, 3)
Kit.check("R1 ceil 能力不失(夹具摘除 (2,3)→向上取整到 4·偏难口径)", int(ceil_row.get("node_depth", -1)) == 4)
_db.board_gen.rows.insert(gap_idx, saved)
_db.combat.board_gen.rows.insert(gap_idx, saved)
Kit.check("夹具表原样还原(行数与解析回到覆盖批现值)",
_db.board_gen.rows.size() == rows_before and int(_db.resolve_board_gen(2, 3).get("node_depth", -1)) == 3)
_db.combat.board_gen.rows.size() == rows_before and int(_db.combat.board_gen.resolve_row(2, 3).get("node_depth", -1)) == 3)
var b1 = board_generator.generate(L, 0, _rng.open_stream("board_gen"), _db)
Kit.check("L%d 生成产物非空" % L, b1 != null)
if b1 == null:
@@ -172,6 +176,8 @@ func _smoke_board_generator() -> void:
b1.degraded_product == (mode == "accept_best"))
Kit.check("L%d 非法产物标记 = (accept_mode==accept_worst)(%s→%s)" % [L, mode, str(b1.illegal_product)],
b1.illegal_product == (mode == "accept_worst"))
Kit.check("L%d 降级/非法产物标记互斥(两出口不可同真)" % L,
not b1.degraded_product or not b1.illegal_product)
Kit.check("L%d 产物断言:实际雷数 %d ≤ 目标雷数 N=%d(先布后删只减不增)" % [L, int(b1.metrics.actual_mines), int(b1.metrics.target_mines)],
int(b1.metrics.actual_mines) <= int(b1.metrics.target_mines))
Kit.check("L%d attempts_used ∈ [1,%d]" % [L, int(row.max_gen_attempts)],
@@ -200,7 +206,7 @@ func _smoke_board_generator() -> void:
row9["r0_max_cap"] = BoardData.SPAWN_AREA_CELLS
row9["max_gen_attempts"] = 4
var stub9 := _BoardRowStub.new()
stub9.row = row9
stub9.combat.board_gen.row = row9
var b9 = board_generator.generate(1, 0, _mk_stream("board_gen", 909), stub9)
Kit.check("L1 cap 压线 9:出图 R₀=%d %s(三分出口诚实降级——超阈即 accept_worst 非法标记,玩家永不卡死)" % [int(b9.metrics.r0_count), "≤9" if int(b9.metrics.r0_count) <= BoardData.SPAWN_AREA_CELLS else ">9→illegal_product"],
b9 != null and (int(b9.metrics.r0_count) <= BoardData.SPAWN_AREA_CELLS or b9.illegal_product))
@@ -227,7 +233,7 @@ func _smoke_power_interval() -> void:
# 行清单与区间值全部随表走——现行区间行 heal_minor/clear_blast 只是恰然,不再是锚。
var interval_rows := {} # rid → [lo, hi](读表)
var anchors_ok := true
for row in _db.reward.rows:
for row in _db.run.reward.rows:
var rid := String(row.reward_id)
if row.has("power_min") or row.has("power_max"):
var lo: int = int(row.get("power_min", -1))
@@ -297,7 +303,7 @@ func _smoke_power_interval() -> void:
func _row_power(rid: String) -> int:
for row in _db.reward.rows:
for row in _db.run.reward.rows:
if String(row.reward_id) == rid:
return int(row.power)
return -1
+3 -3
View File
@@ -41,7 +41,7 @@ func _test_generate_shape_and_mines() -> void:
if b == null:
Kit.check("生成返回棋盘", false)
return
var row: Dictionary = _db.resolve_board_gen(1, 0)
var row: Dictionary = _db.combat.board_gen.resolve_row(1, 0)
var want_w: int = int(row.get("board_width", 0))
var want_h: int = int(row.get("board_height", 0))
Kit.check("棋盘尺寸与 board_gen 表一致(%dx%d)" % [want_w, want_h],
@@ -60,7 +60,7 @@ func _test_target_mine_count_single_source() -> void:
var bad := 0
var checked := 0
for L in [1, 2, 3]:
var row: Dictionary = _db.resolve_board_gen(L, 0)
var row: Dictionary = _db.combat.board_gen.resolve_row(L, 0)
if row.is_empty():
continue
var params := {
@@ -77,7 +77,7 @@ func _test_target_mine_count_single_source() -> void:
if b == null:
Kit.check("生成返回棋盘(雷数单点段)", false)
return
var row1: Dictionary = _db.resolve_board_gen(1, 0)
var row1: Dictionary = _db.combat.board_gen.resolve_row(1, 0)
Kit.check("产物断言读数 = target_mine_count 同源(metrics.target_mines)〔wd14〕",
int(b.metrics.get("target_mines", -1)) == BoardGeneratorScript.target_mine_count({
"w": int(row1.board_width), "h": int(row1.board_height), "density": float(row1.mine_density)}))
@@ -110,13 +110,13 @@ func _init() -> void:
Kit.check_quiet(adr_f.resume_adventure(bp_f, params_e), "P5 续玩受理(选择态分支)")
Kit.check_quiet(adr_f.runner() == null, "P5 续玩不重跑关卡(三选一停点直回,_start_current_level 未走)")
Kit.check_quiet(got_cands["cands"] == cands_e, "P5 续玩候选不重抽(逐位一致)")
Kit.check_quiet(int(adr_f.deck().taken["attack"]) == 1, "P5 次数计数恢复一致(牌库剔除态续接)")
Kit.check_quiet(int(adr_f._curio.taken["attack"]) == 1, "P5 次数计数恢复一致(牌库剔除态续接)")
Kit.check_quiet(not adr_f.continue_to_next_level(), "P5 未兑付推进拒绝(续玩态零自动推进同契约)")
var pick_e: Dictionary = cands_e[0]
Kit.check_quiet(adr_f.stop().pick_curio(String(pick_e.get("kind", "")), String(pick_e.get("tier", "")), String(pick_e.get("feat_id", ""))), "P5 点选兑付受理")
var picked_e: String = String(adr_f.stop()._resolved.get("picked", ""))
if adr_f.deck().taken.has(picked_e):
Kit.check_quiet(int(adr_f.deck().taken[picked_e]) == (2 if picked_e == "attack" else 1),
if adr_f._curio.taken.has(picked_e):
Kit.check_quiet(int(adr_f._curio.taken[picked_e]) == (2 if picked_e == "attack" else 1),
"P5 兑付计数一致(%s——预置 1 + 本次兑付)" % picked_e)
Kit.check_quiet(sm_f.has_breakpoint(), "P5 兑付后停点快照在档(已兑付待推进态可中断)")
var bp_g: Dictionary = sm_f.breakpoint_snapshot()
@@ -67,6 +67,7 @@ func _test_dangling_skill_id() -> void:
{"skill_id": "attack", "weight": 9}, {"skill_id": "not_in_table", "weight": 3}]},
])
var errs: Array = ValidatorScript.new().validate_skill_pool_links(skills, enemies, null)
Kit.check("池内悬空 skill_id 被报(跨表护栏不静默放过)", not errs.is_empty())
Kit.check("悬空项的错误类别可定位(category 非空)",
not errs.is_empty() and String((errs[0] as Dictionary).get("category", "")) != "")
+13 -12
View File
@@ -54,16 +54,17 @@ func _difficulty_table():
func _test_band_hit() -> void:
var hit: Array = _resolver.resolve_enemy_rowset(1, 4, "battle")
var hit: Array = _resolver.combat.enemy.resolve_rowset(1, 4, "battle")
Kit.check("带命中取偏难一侧:请求深度 4 命中带 5(逐字不回带 0)〔fv1〕",
hit.size() == 1 and String((hit[0] as Dictionary).get("enemy_id", "")) == "hobgoblin")
var exact: Array = _resolver.resolve_enemy_rowset(1, 5, "battle")
var exact: Array = _resolver.combat.enemy.resolve_rowset(1, 5, "battle")
Kit.check("精确落带:请求深度 5 命中带 5〔fv1〕",
exact.size() == 1 and String((exact[0] as Dictionary).get("enemy_id", "")) == "hobgoblin")
func _test_rowset_order_preserved() -> void:
var rows: Array = _resolver.resolve_enemy_rowset(1, 0, "battle")
var rows: Array = _resolver.combat.enemy.resolve_rowset(1, 0, "battle")
Kit.check("行集返回同带全部行(2 行)〔fv2〕", rows.size() == 2)
var ids: Array[String] = []
for r in rows:
ids.append(String((r as Dictionary).get("enemy_id", "")))
@@ -73,7 +74,7 @@ func _test_rowset_order_preserved() -> void:
func _test_top_band_clamp() -> void:
_diags.clear()
var rows: Array = _resolver.resolve_enemy_rowset(1, 99, "battle")
var rows: Array = _resolver.combat.enemy.resolve_rowset(1, 99, "battle")
Kit.check("越顶带 clamp 取顶带行集(battle 顶带 5)〔fv3〕",
rows.size() == 1 and String((rows[0] as Dictionary).get("enemy_id", "")) == "hobgoblin")
var clamp_hit := false
@@ -82,13 +83,13 @@ func _test_top_band_clamp() -> void:
clamp_hit = true
Kit.check("越顶带发 warning resolve_depth_clamp〔fv3〕", clamp_hit)
var warn_before: int = _diags.size()
_resolver.resolve_enemy_rowset(1, 99, "battle")
_resolver.combat.enemy.resolve_rowset(1, 99, "battle")
Kit.check("同键重复请求警告去重(四元组注册表)〔fv3〕", _diags.size() == warn_before)
func _test_row_kind_routing() -> void:
var battle_rows: Array = _resolver.resolve_enemy_rowset(1, 0, "battle")
var boss_rows: Array = _resolver.resolve_enemy_rowset(1, 0, "boss")
var battle_rows: Array = _resolver.combat.enemy.resolve_rowset(1, 0, "battle")
var boss_rows: Array = _resolver.combat.enemy.resolve_rowset(1, 0, "boss")
Kit.check("行类路由:同请求深度 battle 与 boss 命中不同键域〔fv4〕",
battle_rows.size() == 2 and boss_rows.size() == 1
and String((boss_rows[0] as Dictionary).get("enemy_id", "")) == "ogre_chief")
@@ -96,13 +97,13 @@ func _test_row_kind_routing() -> void:
func _test_request_oob() -> void:
_diags.clear()
_resolver.resolve_enemy_rowset(0, 0, "battle")
_resolver.combat.enemy.resolve_rowset(0, 0, "battle")
Kit.check("关卡序号 <1 → fatal request_oob〔fv5〕", _has_fatal_oob())
_diags.clear()
_resolver.resolve_enemy_rowset(1, -1, "battle")
_resolver.combat.enemy.resolve_rowset(1, -1, "battle")
Kit.check("节点深度 <0 → fatal request_oob〔fv5〕", _has_fatal_oob())
_diags.clear()
_resolver.resolve_enemy_rowset(1, 0, "elite")
_resolver.combat.enemy.resolve_rowset(1, 0, "elite")
Kit.check("row_kind 非法 → fatal request_oob〔fv5〕", _has_fatal_oob())
@@ -114,11 +115,11 @@ func _has_fatal_oob() -> bool:
func _test_difficulty_clamp() -> void:
var m: Dictionary = _resolver.resolve_difficulty_modifiers(1, 0)
var m: Dictionary = _resolver.run.difficulty.resolve_modifiers(1, 0)
Kit.check("难度乘数越域被 clamp:hp 99 → 上限 10", absf(float(m.get("hp_mult", 0.0)) - 10.0) < 0.001)
Kit.check("难度乘数越域被 clamp:atk 0.01 → 下限 0.1", absf(float(m.get("atk_mult", 0.0)) - 0.1) < 0.001)
Kit.check("未登记坐标返回空修饰(零值不伪装成 ×1)",
_resolver.resolve_difficulty_modifiers(3, 3).is_empty())
_resolver.run.difficulty.resolve_modifiers(3, 3).is_empty())
class _FakeDifficulty:
@@ -13,8 +13,8 @@ func _init() -> void:
print("[curio_refresh_paid_test] FAIL — 数值表加载失败")
quit(1)
return
var base: int = int(db.adventure.shop_refresh_cost_base)
var curve: float = float(db.adventure.shop_refresh_cost_curve)
var base: int = int(db.run.adventure.shop_refresh_cost_base)
var curve: float = float(db.run.adventure.shop_refresh_cost_curve)
Kit.check_quiet(base > 0 and curve > 1.0, "商店刷新旋钮在档(base=%d curve=%.2f——三选一付费档同源读取)" % [base, curve])
# ---- ① 停点在途谓词:无停点时免费/付费刷新皆拒绝(付费路径扣费前置校验防空转烧钱包)----
@@ -28,21 +28,20 @@ func _init() -> void:
Kit.check_quiet(adr.stop().curio_stop_pending() == true, "关卡完成停点在途=谓词 true")
adr.refresh_bank()._refresh_x = 1
adr.refresh_bank()._refresh_y = 2
Kit.check_quiet(adr.stop().refresh_curio() and adr.refresh_bank().refresh_y() == 2
and adr.refresh_bank().free_left() == adr.refresh_bank().refresh_y() + adr.refresh_bank().refresh_stock(), "X 优先消耗(x1y2→x0y2)")
Kit.check_quiet(adr.stop().refresh_curio() and adr.refresh_bank().refresh_y() == 1, "X 穷转 Y 池(y2→y1)")
Kit.check_quiet(adr.stop().refresh_curio() and adr.refresh_bank()._refresh_x == 0 and adr.refresh_bank()._refresh_y == 2, "X 优先消耗(x1y2→x0y2)")
Kit.check_quiet(adr.stop().refresh_curio() and adr.refresh_bank()._refresh_y == 1, "X 穷转 Y 池(y2→y1)")
# ---- ③ GP 付费档:计数递增爬价 + 零触碰免费位 ----
Kit.check_quiet(adr.refresh_bank().gp_cost() == base, "首刷报价=基价(免费位尚余 y1,报价独立于免费位)")
Kit.check_quiet(adr.stop().refresh_curio_paid() and adr.refresh_bank().refresh_y() == 1, "付费刷新不触碰免费位(y1 保持)")
Kit.check_quiet(adr.stop().refresh_curio_paid() and adr.refresh_bank()._refresh_y == 1, "付费刷新不触碰免费位(y1 保持)")
Kit.check_quiet(adr.refresh_bank().gp_cost() == int(ceil(float(base) * curve)), "二刷价爬升=ceil(基价×曲线)(已付费刷=1)")
Kit.check_quiet(adr.stop().refresh_curio_paid() and adr.refresh_bank().gp_cost() == int(ceil(float(base) * curve * curve)), "三刷价再爬升=ceil(基价×曲线²)(已付费刷=2)")
# ---- ④ 停点重建清零付费计数(对齐商店进店清零——不结转)----
adr.stop().present_node_curio(_cands())
Kit.check_quiet(adr.refresh_bank().gp_cost() == base, "节点停点重建=付费计数清零回基价")
Kit.check_quiet(adr.refresh_bank()._paid_refreshes == 0 and adr.refresh_bank().gp_cost() == base, "节点停点重建=付费计数清零回基价")
adr.stop().refresh_curio_paid()
Kit.check_quiet(adr.refresh_bank().gp_cost() == int(ceil(float(base) * curve)), "付费刷新计数在途递增(=1)")
Kit.check_quiet(adr.refresh_bank()._paid_refreshes == 1, "付费刷新计数在途递增(=1)")
adr.stop().present_node_curio(_cands())
Kit.check_quiet(adr.refresh_bank().gp_cost() == base, "再碰三选一付费计数仍清零(同 X 余量不结转口径)")
@@ -57,10 +56,10 @@ func _init() -> void:
var bp_y := {"level_index": 1, "hero_hp": 10, "attune_refresh_y": 2, "graph_gen_v2": 1}
var adr_r = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
Kit.check_quiet(adr_r.resume_adventure(bp_y, {"hero_override": {}, "attune_refresh_y": 3}), "续玩受理(最小断点+参数面)")
Kit.check_quiet(adr_r.refresh_bank().refresh_y() == 2, "断点有键=Y 余量随断点还原(花 1 剩 2——不再整额重注入=满血复活堵死)")
Kit.check_quiet(adr_r.refresh_bank()._refresh_y == 2, "断点有键=Y 余量随断点还原(花 1 剩 2——不再整额重注入=满血复活堵死)")
var adr_o = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
Kit.check_quiet(adr_o.resume_adventure({"level_index": 1, "hero_hp": 10, "graph_gen_v2": 1}, {"hero_override": {}, "attune_refresh_y": 3}), "旧断点续玩受理(快照缺键)")
Kit.check_quiet(adr_o.refresh_bank().refresh_y() == 3, "旧断点缺键=回退参数面整额注入(旧档行为零变化)")
Kit.check_quiet(adr_o.refresh_bank()._refresh_y == 3, "旧断点缺键=回退参数面整额注入(旧档行为零变化)")
quit(Kit.finish("curio_refresh_paid"))
+23 -49
View File
@@ -32,9 +32,8 @@ func _init() -> void:
# V9-1 刷新券:入存量账(冒险级)——反例=写进 _attune_refresh_x(X 每停点重置会吞存量)
a.refresh_bank().reset_x() # X 归零基线(模拟新停点)
a.stop()._pending = {"candidates": [{"kind": "refresh", "tier": "excellent"}]} # 停点候选直写(构造面——pick_curio 真实入口前置)
Kit.check_quiet(a.stop().pick_curio("refresh", "excellent"), "刷新券兑付受理〔专长批 V9〕")
Kit.check_quiet(a.refresh_bank().refresh_stock() == 1 and a.refresh_bank().free_left() == a.refresh_bank().refresh_stock() + a.refresh_bank().refresh_y(),
Kit.check_quiet(a.stop()._apply_curio_pick("refresh", "excellent", 1), "刷新券兑付受理〔专长批 V9〕")
Kit.check_quiet(a.refresh_bank().refresh_stock() == 1 and a.refresh_bank()._refresh_x == 0,
"F-1 修复:刷新券入存量账、X 账零写入(写错账反例不复发)〔专长批 V9〕")
a.refresh_bank().reset_x() # 再碰停点——X 重置但存量不吞
Kit.check_quiet(a.refresh_bank().refresh_stock() == 1 and a.refresh_bank().free_left() == 1,
@@ -51,50 +50,44 @@ func _init() -> void:
Kit.check_quiet(deck_refresh_before == 0 or true, "牌库 refresh 令牌读数在域(投放 %d)" % deck_refresh_before)
# V9-2 储备金袋:待注入账 + 战首注入(节点级)
a.stop()._pending = {"candidates": [{"kind": "rap", "tier": "rare"}]} # 停点候选直写(构造面——pick_curio 真实入口前置)
Kit.check_quiet(a.stop().pick_curio("rap", "rare"), "储备金袋兑付受理〔专长批 V9〕")
Kit.check_quiet(a.stop()._apply_curio_pick("rap", "rare", 3), "储备金袋兑付受理〔专长批 V9〕")
Kit.check_quiet(a.stop()._pending_rap == 3, "储备金袋:待注入 RAP +3(节点级账)〔专长批 V9〕")
# V9-3 时沙:下一战首轮 AP 队列(回合级池增量)
a.stop()._pending = {"candidates": [{"kind": "time_sand", "tier": "rare"}]} # 停点候选直写(构造面——pick_curio 真实入口前置)
Kit.check_quiet(a.stop().pick_curio("time_sand", "rare"), "时沙兑付受理〔专长批 V9〕")
Kit.check_quiet(a.stop()._apply_curio_pick("time_sand", "rare", 2), "时沙兑付受理〔专长批 V9〕")
Kit.check_quiet(a.stop()._time_sand_queue == 2, "时沙:首轮 AP 队列 +2(池增量非上限)〔专长批 V9〕")
# V23 两件出池(R1——同调电池删除/完美保险迁专长)
Kit.check_quiet(not db.adventure.CURIO_KINDS.has("attune_battery")
and not db.adventure.CURIO_KINDS.has("perfect_insurance"),
Kit.check_quiet(not db.run.adventure.CURIO_KINDS.has("attune_battery")
and not db.run.adventure.CURIO_KINDS.has("perfect_insurance"),
"出池词表:CURIO_KINDS 零两件(三选一池物理不可再出)〔卡② R1〕")
Kit.check_quiet(not a.stop().consume_pool_purchase("attune_battery", "rare")
and not a.stop().consume_pool_purchase("perfect_insurance", "rare"),
Kit.check_quiet(not a.stop()._apply_curio_pick("attune_battery", "rare", 1)
and not a.stop()._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,
Kit.check_quiet(int(db.run.adventure.curio_cap("attune_battery")) == 99,
"出池后未知 kind 走分发表 default(cap 读数兜底 99——零崩溃面)〔卡② R1〕")
# 账本保留(契约 1):主动件充能与每冒险种行入口零改动(单源化后=差额种行幂等——不吞装配)
a._attunement = ["incense_stick"]
a._loadout.set_attunement(["incense_stick"])
a._loadout_seeded = false # 模拟首关 bind 时点(分派走 seed_for_adventure 分支)
a._loadout_on_level_inventory() # 种行(首关 bind 时点语义)
var incense_left := 0
for r in a.runner().treasure_inventory.snapshot_inventory():
if String(r.get("item_id", "")) == "incense_stick" and String(r.get("source_class", "")) == "attunement" and not bool(r.get("used", false)):
incense_left += 1
Kit.check_quiet(incense_left == 1,
Kit.check_quiet(a._loadout.charges_left("incense_stick", a.runner().treasure_inventory) == 1,
"账本保留:种行按装备表值投行(线香 battle 级 1——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"))
for k in db.run.adventure.CURIO_KINDS:
var q := int(db.run.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")
Kit.check_quiet(int(db.run.adventure.TIER_QUOTAS["rare"]) == 7 and rare_sum == 7
and int(db.run.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〕")
Kit.check_quiet(db.run.adventure.validate().is_empty(), "AdventureTable.validate() 零违例(两件出池后)〔卡② R3〕")
# V9-6 全链:凭证令牌兑付=牌库摘牌(选中即消尽)〔拆件批 V2——cs3 凭证拿到即开现役断言承继〕
a._curio.deck = [{"kind": "rap", "tier": "rare"}, {"kind": "attack", "tier": "common"}]
@@ -106,17 +99,17 @@ func _init() -> void:
a._curio.deck = [{"kind": "gp", "tier": "common"}, {"kind": "heal", "tier": "excellent"}]
a.stop()._pending = {"candidates": [{"kind": "gp", "tier": "common"}]}
var gp_taken_before := int(a._curio.taken.get("gp", 0))
Kit.check_quiet(a.stop().pick_curio("gp", "common") and int(a.deck().taken.get("gp", 0)) == gp_taken_before + 1,
Kit.check_quiet(a.stop().pick_curio("gp", "common") and int(a._curio.taken.get("gp", 0)) == gp_taken_before + 1,
"留任小GP 袋:GP 通道兑付受理(gp 计数入账——路由零变化)〔专长批 V10〕")
var hp_before: int = a.stats().hero_hp
a.stop()._pending = {"candidates": [{"kind": "heal", "tier": "excellent"}]}
Kit.check_quiet(a.stop().pick_curio("heal", "excellent") and a.stats().hero_hp == mini(hp_before + int(db.adventure.curio_value("heal", "excellent")), a.effective_max_hp()),
Kit.check_quiet(a.stop().pick_curio("heal", "excellent") and a.stats().hero_hp == mini(hp_before + int(db.run.adventure.curio_value("heal", "excellent")), a.effective_max_hp()),
"留任应急口粮:当即回血 +6 封顶生效上限〔专长批 V10〕")
# V10-2 行动点上限归属性轴:ap kind 走加算区第四轴
a._curio.deck = [{"kind": "ap", "tier": "rare"}]
a.stop()._pending = {"candidates": a._curio.deck.duplicate()}
var ap_bonus_before: int = a.journal().bonus_total("ap")
Kit.check_quiet(a.stop().pick_curio("ap", "rare") and a.journal().bonus_total("ap") == ap_bonus_before + int(db.adventure.curio_value("ap", "rare")),
Kit.check_quiet(a.stop().pick_curio("ap", "rare") and a.journal().bonus_total("ap") == ap_bonus_before + int(db.run.adventure.curio_value("ap", "rare")),
"行动点上限归属性轴:ap 兑付写加算区第四轴(bonus.ap 承载)〔专长批 V10〕")
# V16——断点扩容往返:新账本随断点快照/续玩回填(breakpoint_resume_test P5 同构)
@@ -149,11 +142,11 @@ func _init() -> void:
Kit.check_quiet(int(b2.adventure_counters.get("pain_layers", -1)) == 3 and b2.refresh_bank().refresh_stock() == 2,
"V16:续玩回填逐项一致(adventure 桶随断点往返·计数恢复)〔无状态化 V3〕")
# V25 完美保险专长验收组(R2 全项:装载/限次一次/计数纯净/两账相减/跨战清零/事件不吞)
var fi: Dictionary = db.feats.row_by_id("perfect_insurance")
var fi: Dictionary = db.crossrun.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〕")
Kit.check_quiet(db.crossrun.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()
@@ -172,30 +165,17 @@ func _init() -> void:
if safe_cells.size() >= 2:
break
Kit.check_quiet(safe_cells.size() == 2, "保险 rig:两枚安全格定位(去重闸按格实例——限次样本须异格)〔卡② R2〕")
var rows_orch: Array[Dictionary] = []
orch.step_row.connect(func(row): rows_orch.append(row)) # step_row 公开信号面(insurance_used 载荷/seal_miss highlight=豁免账与误标账观测面)
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〕")
var ins_row := {}
for r in rows_orch:
if String(r.get("effect_id", "")) == "insurance_used":
ins_row = r
Kit.check_quiet(not ins_row.is_empty() and int(ins_row.get("forgiven", -1)) == 1,
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)
var ins_count := 0
var miss_hl_last := ""
for r in rows_orch:
if String(r.get("effect_id", "")) == "insurance_used":
ins_count += 1
if String(r.get("type", "")) == "seal_miss":
miss_hl_last = String(r.get("highlight", ""))
Kit.check_quiet(ins_count == 1 and miss_hl_last == "perfect_broken",
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 真值表〕")
@@ -215,15 +195,9 @@ func _init() -> void:
"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 rows_orch2: Array[Dictionary] = []
orch2.step_row.connect(func(row): rows_orch2.append(row)) # step_row 公开信号面(insurance_used 载荷=豁免账观测面)
var sc2 = safe_cells[0]
orch2._grant_seg._judge_seal({"cell": sc2, "hit": false}, 1)
var ins_row2 := {}
for r in rows_orch2:
if String(r.get("effect_id", "")) == "insurance_used":
ins_row2 = r
Kit.check_quiet(not ins_row2.is_empty() and int(ins_row2.get("forgiven", -1)) == 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〕")
+7 -9
View File
@@ -23,18 +23,18 @@ func _init() -> void:
# ---- hr1:生效公式逐值(表值+永久强化 flat+加算区后乘调试 pct——与改前同式)----
var journal_a = GainJournalScript.new()
var hero = HeroStatsScript.new(db, {"atk": 3, "max_hp": 5, "armor_class": 2}, journal_a)
Kit.check_quiet(hero.effective_max_hp() == int(db.hero.max_hp) + 5,
Kit.check_quiet(hero.effective_max_hp() == int(db.combat.hero.max_hp) + 5,
"hr1 生效上限 = 表值+永久强化 flat(pct=0 乘法恒等)")
Kit.check_quiet(hero.effective_atk() == int(db.hero.atk) + 3,
Kit.check_quiet(hero.effective_atk() == int(db.combat.hero.atk) + 3,
"hr1 生效攻击 = 表值+永久强化 flat")
Kit.check_quiet(hero.effective_ac() == int(db.forge_armor_route_by_type(db.forge_default_armor_type()).get("base_ac", 0)) + 2,
Kit.check_quiet(hero.effective_ac() == int(db.crossrun.forge.armor_route_by_type(db.crossrun.forge.default_armor_type).get("base_ac", 0)) + 2,
"hr1 生效护甲 = 甲型基础 AC+甲路线等级加值(纯 flat 无 pct)")
hero.set_debug_override(50, 50)
Kit.check_quiet(hero.effective_max_hp() == int(round(float(int(db.hero.max_hp) + 5) * 1.5)),
Kit.check_quiet(hero.effective_max_hp() == int(round(float(int(db.combat.hero.max_hp) + 5) * 1.5)),
"hr1 调试 pct=50 后乘——上限 ×1.5(叠加次序:flat 先加 pct 后乘)")
hero.set_debug_override(0, 0)
journal_a.gain_bonus("max_hp", 4, "battle_export", "max_hp")
Kit.check_quiet(hero.effective_max_hp() == int(db.hero.max_hp) + 5 + 4,
Kit.check_quiet(hero.effective_max_hp() == int(db.combat.hero.max_hp) + 5 + 4,
"hr1 加算区 flat 与永久强化 flat 同列基数(三段叠加——公式消费日记汇总)")
# hr1 回血封顶公式逐值(算术宿主=HeroStats.rest_heal 单点——改前 LevelRunner.rest_heal 同式)
@@ -119,8 +119,7 @@ func _init() -> void:
Kit.check_quiet(w3.gp == 37 and j3.keystone_gained() == 3 and j3.vision_used() == 1
and s3.hero_hp == 21 and (s3.feats_held() as Array).has("twin_pick"),
"hr2 片段往返:三件账面键逐值(生命/特性/钱包/钥石/真视)")
var jfrag: Dictionary = j3.breakpoint_fragment().get("gain_journal", {}) as Dictionary
Kit.check_quiet((jfrag.get("records", []) as Array).size() == 3 and (jfrag.get("details", []) as Array).size() == 3,
Kit.check_quiet((j3._gain_log as Array).size() == 3 and (j3._bonus_detail as Array).size() == 3,
"hr2 片段往返:日记全量还原")
# hr2 旧档回退(无日记键→汇总直载跳过比对)
@@ -128,8 +127,7 @@ func _init() -> void:
legacy.erase("gain_journal")
var j4 = GainJournalScript.new()
j4.restore_fragment(legacy, {})
Kit.check_quiet(j4.bonus_total("atk") == 7 and j4.bonus_total("armor_class") == 1
and ((j4.breakpoint_fragment().get("gain_journal", {}) as Dictionary).get("records", []) as Array).is_empty(),
Kit.check_quiet(j4.bonus_total("atk") == 7 and j4.bonus_total("armor_class") == 1 and (j4._gain_log as Array).is_empty(),
"hr2 旧档缺日记→汇总键直载(跳过比对——旧档行为零变化)")
# hr2 日记在档推导比对不符→拒绝载入(fail-fast——汇总不回填)
@@ -102,7 +102,7 @@ func _runner(db, nodes: Array, anchor_id: int, candidates: Array, n: int = 2):
# ---- ⑥ N 进数据表(含越域校验)〔fv6〕----
func _test_field_in_table(db) -> void:
var n_in_table: int = int(db.level.info_visible_layers)
var n_in_table: int = int(db.run.level.info_visible_layers)
Kit.check("N 落表:level.tres 读数 2(实际 %d)〔fv6〕" % n_in_table, n_in_table == 2)
var t := LevelTableScript.new()
t.rows = [{"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30, "nodes": _valid_graph_nodes()}]
@@ -184,11 +184,11 @@ func _test_single_chain_entry(db) -> void:
func _test_n_from_table(db) -> void:
var nodes := _graph_nodes()
var r = _runner(db, nodes, 1, [nodes[2]]) # 锚层 1
db.level.info_visible_layers = 1
db.run.level.info_visible_layers = 1
Kit.check("N 读表:N=1 时节点 4(层距 2)出窗〔fv6〕", not r.type_visible_ids().has(4))
db.level.info_visible_layers = 3
db.run.level.info_visible_layers = 3
Kit.check("N 读表:N=3 时节点 5(层距 3)入窗〔fv6〕", r.type_visible_ids().has(5))
db.level.info_visible_layers = 10
db.run.level.info_visible_layers = 10
var all_in := true
for nid in 7: # 非豁免节点 0..6(7 = Boss 恒显,不作判据)
if not r.type_visible_ids().has(nid):
+60 -60
View File
@@ -33,7 +33,7 @@ var _graph_gen = null # 生成图取数单实例(图 = f(参数档, 种子)
# 参数档 × 种子产图(与 LevelRunner._assign_generated_graph 同一产图路径与同一参数档表)
func _generated_graph(level_index: int, seed_val: int) -> Dictionary:
var params: Dictionary = _db.level_gen_params_row(level_index)
var params: Dictionary = _db.run.level_gen_params.row_by_level_index(level_index)
if params.is_empty():
return {}
if _graph_gen == null:
@@ -221,13 +221,13 @@ func _path_in_graph(nodes: Array, path: Array) -> bool:
# 敌侧覆盖判据:请求深度经 band_of 落带(非 -1)——解析落带 ≥ 请求深度即等价判据(禁顶 clamp 消解)
func _enemy_band_covers(level_index: int, depth: int, kind: String) -> bool:
var rows: Array[Dictionary] = _db.resolve_enemy_rowset(level_index, depth, kind)
var rows: Array[Dictionary] = _db.combat.enemy.resolve_rowset(level_index, depth, kind)
return not rows.is_empty() and int(rows[0].get("depth_band", -1)) >= depth
# 板侧覆盖判据:该深度在 board_gen 精确命中(node_depth 等值)
func _board_row_exact(level_index: int, depth: int) -> bool:
var board: Dictionary = _db.resolve_board_gen(level_index, depth)
var board: Dictionary = _db.combat.board_gen.resolve_row(level_index, depth)
return not board.is_empty() and int(board.get("node_depth", -1)) == depth
@@ -259,14 +259,14 @@ func _init() -> void:
func _smoke_table_migration() -> void:
print("--- 数值入表(shop 两 knob + hero 显示名 · 原值迁移)---")
Kit.check("shop.relic_base_price 表值 == 迁移前字面量(15.0)〔wd11〕", _db.shop.relic_base_price == 15.0)
Kit.check("shop.spend_discount 表值 == 迁移前字面量(0.8)〔wd11〕", _db.shop.spend_discount == 0.8)
Kit.check("hero.display_name 表值 == 迁移前字面量(探险者)〔wd16〕", _db.hero.display_name == "探险者")
Kit.check("shop.relic_base_price 表值 == 迁移前字面量(15.0)〔wd11〕", _db.run.shop.relic_base_price == 15.0)
Kit.check("shop.spend_discount 表值 == 迁移前字面量(0.8)〔wd11〕", _db.run.shop.spend_discount == 0.8)
Kit.check("hero.display_name 表值 == 迁移前字面量(探险者)〔wd16〕", _db.combat.hero.display_name == "探险者")
# 消费端读表:spawn 真实构造链的英雄单位显示名与表值同
var units = load("res://core/unit_container.gd").new(_db)
var spawned: bool = units.spawn(1, 0, int(_db.hero.max_hp))
var spawned: bool = units.spawn({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)})
Kit.check("spawn 后英雄单位 display_name 读表同值(探险者)〔wd16〕",
spawned and String(units.hero_unit().get("display_name", "")) == String(_db.hero.display_name))
spawned and String(units.hero_unit().get("display_name", "")) == String(_db.combat.hero.display_name))
# ---- 生成图深度扩面覆盖断言(迭代 35 卡② S5/V11——节点源=卡① 生成器产图,
@@ -279,7 +279,7 @@ func _smoke_generated_coverage() -> void:
var scanned := 0
var readings: Array = []
for L in [1, 2, 3]:
if _db.level_gen_params_row(L).is_empty():
if _db.run.level_gen_params.row_by_level_index(L).is_empty():
Kit.check("L%d 参数档缺行(覆盖断言无源可扫)〔v11〕" % L, false)
continue
var dmin := 999
@@ -306,7 +306,7 @@ func _smoke_generated_coverage() -> void:
board_ok = false
var bands: Array = []
for tid in _terminal_ids(nodes):
var rs: Array[Dictionary] = _db.resolve_enemy_rowset(L, int(by_id[int(tid)].get("depth", -1)), "boss")
var rs: Array[Dictionary] = _db.combat.enemy.resolve_rowset(L, int(by_id[int(tid)].get("depth", -1)), "boss")
bands.append(int(rs[0].get("depth_band", -1)) if not rs.is_empty() else -1)
for i in range(bands.size()):
for j in range(i + 1, bands.size()):
@@ -325,9 +325,9 @@ func _smoke_generated_coverage() -> void:
func _smoke_level() -> void:
print("--- 关卡节点图·生成图推进(参数档×种子产图·满配冒险成功/恢复公式/败北线/回放确定性)---")
var autoplay = load("res://sim/level_autoplay.gd")
var hero_base := {"atk": int(_db.hero.atk), "hp": int(_db.hero.max_hp), "ac": int(_db.hero.armor_class)} # 配置基线快照——⑤ 回基线对比用(改表自动跟随,不再硬编码)
var hero_base := {"atk": int(_db.combat.hero.atk), "hp": int(_db.combat.hero.max_hp), "ac": int(_db.combat.hero.armor_class)} # 配置基线快照——⑤ 回基线对比用(改表自动跟随,不再硬编码)
var nodes1: Array = _level_active_nodes(1)
var params1: Dictionary = _db.level_gen_params_row(1)
var params1: Dictionary = _db.run.level_gen_params.row_by_level_index(1)
Kit.check("L1 现行起关图取数非空(种子 %d × 参数档 → 节点 %d 颗)" % [ACTIVE_GRAPH_SEED, nodes1.size()], not nodes1.is_empty())
# ① 满配冒险成功路径(atk+200%/hp+100%——测试面板同源的属性覆盖)
var win = autoplay.run_level(_db, ACTIVE_GRAPH_SEED, 1, 200, 100)
@@ -354,10 +354,10 @@ func _smoke_level() -> void:
boss_depth1 == int(params1.get("layers", -1)) and int(by_id1.get(last_id, {}).get("depth", -1)) == boss_depth1)
Kit.check("L1 终点恰一个(大岔口自 L2 起的图侧投影)", _terminal_ids(nodes1).size() == 1 and _ids_of_type(nodes1, "boss").size() == 1)
Kit.check("L1 生成图含岔口(出度 ≥2 节点 %d 处)" % _fork_ids(nodes1).size(), _fork_ids(nodes1).size() >= 1)
Kit.check("进 Boss HP 携带在域内(%d)" % win.hp_at_boss, win.hp_at_boss >= 1 and win.hp_at_boss <= int(_db.hero.max_hp) * 3)
Kit.check("进 Boss HP 携带在域内(%d)" % win.hp_at_boss, win.hp_at_boss >= 1 and win.hp_at_boss <= int(_db.combat.hero.max_hp) * 3)
# ② 恢复点公式:回复 = min(floor(生效max_hp×rest_heal_pct/100), 缺口)——两档缺口各验一条分支
#(真入口驱动:起关→首战→选恢复候选;缺口由夹具造,恢复比例读关卡行)
var pct1: int = int(_db.level_row(1).get("rest_heal_pct", 0))
var pct1: int = int(_db.run.level.row_by_level_index(1).get("rest_heal_pct", 0))
var rest_wide := _drive_to_rest(1, ACTIVE_GRAPH_SEED, 100000)
if rest_wide.is_empty():
Kit.check("恢复点公式无源(现行图入口后一位无可直达的恢复候选)", false)
@@ -382,9 +382,9 @@ func _smoke_level() -> void:
Kit.check("关卡回放断言:同种子两遍结算步骤流逐行相同", JSON.stringify(rep1.rows) == JSON.stringify(rep2.rows))
Kit.check("关卡步骤流非空(%d 行)" % rep1.rows.size(), rep1.rows.size() > 0)
# ⑤ 属性覆盖恢复:模拟后 hero 表回基线(调整板不泄漏到后续会话;基线 = 函数入口的配置快照,改表自动跟随)
Kit.check("模拟后 hero 表恢复基线(atk=%d hp=%d ac=%d——对配置快照)" % [int(_db.hero.atk), int(_db.hero.max_hp), int(_db.hero.armor_class)],
int(_db.hero.atk) == int(hero_base.atk) and int(_db.hero.max_hp) == int(hero_base.hp) \
and int(_db.hero.armor_class) == int(hero_base.ac))
Kit.check("模拟后 hero 表恢复基线(atk=%d hp=%d ac=%d——对配置快照)" % [int(_db.combat.hero.atk), int(_db.combat.hero.max_hp), int(_db.combat.hero.armor_class)],
int(_db.combat.hero.atk) == int(hero_base.atk) and int(_db.combat.hero.max_hp) == int(hero_base.hp) \
and int(_db.combat.hero.armor_class) == int(hero_base.ac))
# 手搓战斗状态袋单点(7 处字面量收在一处;_autoplay_battle 直接消费的最小字段族。
@@ -439,10 +439,10 @@ func _smoke_level_graph() -> void:
print("--- 关卡节点图·分支节点图(L2 幽暗地域·生成图)---")
var autoplay = load("res://sim/level_autoplay.gd")
# ① 关卡表行级校验零错误(关卡表只承载序号/显示名/恢复比例——图合法性由 validate_graph 在起关时承担)
Kit.check("LevelTable 行级校验零错误(图合法性移交 validate_graph)", _db.level.validate().is_empty())
Kit.check("LevelTable 行级校验零错误(图合法性移交 validate_graph)", _db.run.level.validate().is_empty())
# ② L2 生成图形状读数(性质断言——规模/型别/岔口全部由参数档与逐节点扫描互校)
var nodes2: Array = _level_active_nodes(2)
var params2: Dictionary = _db.level_gen_params_row(2)
var params2: Dictionary = _db.run.level_gen_params.row_by_level_index(2)
Kit.check("L2 现行起关图取数非空(种子 %d × 参数档 → 节点 %d 颗)" % [ACTIVE_GRAPH_SEED, nodes2.size()], not nodes2.is_empty())
var tcount := _type_tally(nodes2)
var branch := _fork_ids(nodes2).size()
@@ -600,7 +600,7 @@ func _smoke_level_graph() -> void:
# ⑦a 定点:L2 首遇带行集与各终点 Boss 行集(首遇带 = 数据驱动最低带,契约 8——禁写死坐标;
# 多终点图逐终点解析,断言行集构成而非单行:行序 = 文件序,首行即叙述取数面)
var l2_spider_band: int = _first_encounter_band(2, "giant_spider")
var l2_spider_set: Array = _db.resolve_enemy_rowset(2, l2_spider_band, "battle")
var l2_spider_set: Array = _db.combat.enemy.resolve_rowset(2, l2_spider_band, "battle")
var l2_spider_ids := {}
for r in l2_spider_set:
l2_spider_ids[String(r.get("enemy_id", ""))] = true
@@ -610,14 +610,14 @@ func _smoke_level_graph() -> void:
var l2_terms := _terminal_depths(nodes2)
var l2_boss_ok := not l2_terms.is_empty()
for d in l2_terms:
var l2_boss_set: Array[Dictionary] = _db.resolve_enemy_rowset(2, int(d), "boss")
var l2_boss_set: Array[Dictionary] = _db.combat.enemy.resolve_rowset(2, int(d), "boss")
if l2_boss_set.size() != 1 or String(l2_boss_set[0].get("enemy_id", "")) != "drider" \
or String(l2_boss_set[0].get("armor_type", "")) != "ward":
l2_boss_ok = false
Kit.check("L2 全部终点深度 %s 的 Boss 行集均 = {drider} 恰一行且首行魔法甲(护甲类型重配批:关卡 2 唯一非轻甲;P4 乙 Boss 行集多行合法——零内容期仍单行)〔fv24〕" % str(l2_terms), l2_boss_ok)
# ⑦b 定点:混编遭遇行集 (2,battle,5)(R6 行序契约运行面:
# 行集两行文件内相邻、行序 = 声明序 = 生成序;静态最坏骰和 Σ(unit_count_max,固定行缺省 = unit_count) ≤ 4)
var l2_mix_set: Array = _db.resolve_enemy_rowset(2, 5, "battle")
var l2_mix_set: Array = _db.combat.enemy.resolve_rowset(2, 5, "battle")
var mix_shape_ok: bool = l2_mix_set.size() == 2 \
and String(l2_mix_set[0].get("enemy_id", "")) == "bugbear" \
and String(l2_mix_set[1].get("enemy_id", "")) == "giant_spider"
@@ -632,7 +632,7 @@ func _smoke_level_graph() -> void:
# ---- L3 灰矮人矿城断言段 ----
# ⑧ L3 生成图形状读数(与 ② 同一性质判据面——规模/型别/岔口/多终点全部对参数档与扫描读数互校)
var nodes3: Array = _level_active_nodes(3)
var params3: Dictionary = _db.level_gen_params_row(3)
var params3: Dictionary = _db.run.level_gen_params.row_by_level_index(3)
Kit.check("L3 现行起关图取数非空(种子 %d × 参数档 → 节点 %d 颗)" % [ACTIVE_GRAPH_SEED, nodes3.size()], not nodes3.is_empty())
var tcount3 := _type_tally(nodes3)
var branch3 := _fork_ids(nodes3).size()
@@ -654,7 +654,7 @@ func _smoke_level_graph() -> void:
Kit.check("L3 现行图每节点深度 board_gen 精确命中(非空且 node_depth 等值·违例 %d 处)" % int(cov3["board_bad"]), int(cov3["board_bad"]) == 0)
# ⑩ 定点:L3 首遇带行集与各终点 Boss 行集(克制面重轻错开——重甲灰矮人首次登台;首遇带数据驱动定位同 ⑦a)
var l3_lead_band: int = _first_encounter_band(3, "duergar")
var l3_lead_set: Array = _db.resolve_enemy_rowset(3, l3_lead_band, "battle")
var l3_lead_set: Array = _db.combat.enemy.resolve_rowset(3, l3_lead_band, "battle")
var l3_lead_ids := {}
for r in l3_lead_set:
l3_lead_ids[String(r.get("enemy_id", ""))] = true
@@ -665,7 +665,7 @@ func _smoke_level_graph() -> void:
var l3_terms := _terminal_depths(nodes3)
var l3_boss_ok := not l3_terms.is_empty()
for d in l3_terms:
var l3_king_set: Array[Dictionary] = _db.resolve_enemy_rowset(3, int(d), "boss")
var l3_king_set: Array[Dictionary] = _db.combat.enemy.resolve_rowset(3, int(d), "boss")
if l3_king_set.size() != 1 or String(l3_king_set[0].get("enemy_id", "")) != "duergar_king" \
or String(l3_king_set[0].get("armor_type", "")) != "heavy":
l3_boss_ok = false
@@ -700,23 +700,23 @@ func _smoke_level_graph() -> void:
# 断言后立即还原表,后续断言面零污染。R3 fail-fast 正反验证 + R6 行序运行面验证。
var mix_row_a := {"level_index": 1, "row_kind": "battle", "depth_band": 5, "enemy_id": "goblin", "display_name": "地精", "unit_count": 1, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]}
var mix_row_b := {"level_index": 1, "row_kind": "battle", "depth_band": 5, "enemy_id": "bugbear", "display_name": "熊地精", "unit_count": 2, "hp": 20, "atk": 3, "pool": [{"skill_id": "attack", "weight": 9}]}
_db.enemy.rows.append(mix_row_a)
_db.enemy.rows.append(mix_row_b)
_db.combat.enemy.rows.append(mix_row_a)
_db.combat.enemy.rows.append(mix_row_b)
var mix_sys = load("res://core/unit_container.gd").new(_db)
Kit.check("混编行集 spawn 受理((1,battle,5) 双行 地精1+熊地精2——定值无区间路径,行集构成纪律放开面正向)", mix_sys.spawn(1, 5, int(_db.hero.max_hp)))
Kit.check("混编行集 spawn 受理((1,battle,5) 双行 地精1+熊地精2——定值无区间路径,行集构成纪律放开面正向)", mix_sys.spawn({"level_index": 1, "node_depth": 5, "hero_hp": int(_db.combat.hero.max_hp)}))
var mix_units: Array = mix_sys.all_enemies()
Kit.check("混编实例总数 = 各行数量求和(1+2=3——行集实例总数 ≤4 守卫内)", mix_units.size() == 3)
Kit.check("混编生成序 = 文件行序(行序即契约——地精在前熊地精在后,handle_id 跨行连续派发)", String(mix_units[0].get("enemy_id")) == "goblin" and String(mix_units[1].get("enemy_id")) == "bugbear" and String(mix_units[2].get("enemy_id")) == "bugbear")
Kit.check("混编各行行值独立投影(display_name/hp/atk 按各自行值——同场不同种不同值)", String(mix_units[0].get("display_name")) == "地精" and int(mix_units[1].get("hp")) == 20 and int(mix_units[1].get("atk")) == 3)
_db.enemy.rows.append({"level_index": 1, "row_kind": "battle", "depth_band": 6, "enemy_id": "goblin", "display_name": "地精", "unit_count": 3, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]})
_db.enemy.rows.append({"level_index": 1, "row_kind": "battle", "depth_band": 6, "enemy_id": "bugbear", "display_name": "熊地精", "unit_count": 3, "hp": 20, "atk": 3, "pool": [{"skill_id": "attack", "weight": 9}]})
_db.combat.enemy.rows.append({"level_index": 1, "row_kind": "battle", "depth_band": 6, "enemy_id": "goblin", "display_name": "地精", "unit_count": 3, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]})
_db.combat.enemy.rows.append({"level_index": 1, "row_kind": "battle", "depth_band": 6, "enemy_id": "bugbear", "display_name": "熊地精", "unit_count": 3, "hp": 20, "atk": 3, "pool": [{"skill_id": "attack", "weight": 9}]})
var over_sys = load("res://core/unit_container.gd").new(_db)
Kit.check("行集实例总数守卫:两行各定值 3(总 6 > 4)spawn 被拒(行集构成纪律③——R3 fail-fast 运行面)", not over_sys.spawn(1, 6, int(_db.hero.max_hp)))
_db.enemy.rows.remove_at(_db.enemy.rows.size() - 1)
_db.enemy.rows.remove_at(_db.enemy.rows.size() - 1)
_db.enemy.rows.remove_at(_db.enemy.rows.size() - 1)
_db.enemy.rows.remove_at(_db.enemy.rows.size() - 1)
Kit.check("混编断言段注入行全量还原(表回 28 行——迭代 35 生成图深度扩面覆盖批后的基线行数;首批混编投放基线 20 行+覆盖 8 行)", int(_db.enemy.rows.size()) == 28)
Kit.check("行集实例总数守卫:两行各定值 3(总 6 > 4)spawn 被拒(行集构成纪律③——R3 fail-fast 运行面)", not over_sys.spawn({"level_index": 1, "node_depth": 6, "hero_hp": int(_db.combat.hero.max_hp)}))
_db.combat.enemy.rows.remove_at(_db.combat.enemy.rows.size() - 1)
_db.combat.enemy.rows.remove_at(_db.combat.enemy.rows.size() - 1)
_db.combat.enemy.rows.remove_at(_db.combat.enemy.rows.size() - 1)
_db.combat.enemy.rows.remove_at(_db.combat.enemy.rows.size() - 1)
Kit.check("混编断言段注入行全量还原(表回 28 行——迭代 35 生成图深度扩面覆盖批后的基线行数;首批混编投放基线 20 行+覆盖 8 行)", int(_db.combat.enemy.rows.size()) == 28)
# ⑬ 真图类型可见性(信息迷雾 v2.1):L2 入口停点纯核心面读数——候选 ⊆ 可见集 /
# 终点恒可见 / 入口 BFS 层距 > N 的非豁免节点入雾。N 取数据表(代码零硬编码)。
@@ -734,7 +734,7 @@ func _smoke_level_graph() -> void:
if not vis_ids.has(int(bid)):
vis_boss_ok = false
Kit.check("可见性冒烟:L2 全部终点 Boss 句柄 %s 恒在可见集(终点不入雾)〔fv3〕" % str(_ids_of_type(nodes2, "boss")), vis_boss_ok)
var vis_n: int = int(_db.level.info_visible_layers)
var vis_n: int = int(_db.run.level.info_visible_layers)
Kit.check("可见性冒烟:N 读数据表 = %d(provisional 2——代码零硬编码)〔fv6〕" % vis_n, vis_n == 2)
# 层距判据按图自算入口 BFS 层(与 runner 同源口径:入口层 0、出边逐层 +1)——
# 锚层 0 时层距 ≤ N 者恒可见、层距 > N 的非终点者恒不可见(两条扫描面都须非空)
@@ -823,7 +823,7 @@ func _smoke_gp_wallet() -> void:
var fin_b := {}
boss.step_row.connect(func(row): rows_b.append(row))
boss.battle_finished.connect(func(p): fin_b["p"] = p)
Kit.check("L2 Boss 战启动((2,9)——Boss 档掷定 [6,10] + 关级加值 1)", boss.begin_battle({"level_index": 2, "node_depth": 9, "node_type": "boss", "hero_hp": int(_db.hero.max_hp)}))
Kit.check("L2 Boss 战启动((2,9)——Boss 档掷定 [6,10] + 关级加值 1)", boss.begin_battle({"level_index": 2, "node_depth": 9, "node_type": "boss", "hero_hp": int(_db.combat.hero.max_hp)}))
var guard_b := 60
while boss.battle_result == "" and guard_b > 0:
guard_b -= 1
@@ -862,7 +862,7 @@ func _smoke_gp_escrow() -> void:
settlement.step_row.connect(func(row): rows.append(row))
var fin := {}
settlement.battle_finished.connect(func(p): fin["p"] = p)
if not settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp), "hero_atk": 999}):
if not settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp), "hero_atk": 999}):
continue
var target := _find_gp_dig(settlement.minesweeper, settlement)
if target.is_empty():
@@ -896,15 +896,15 @@ func _smoke_gp_escrow() -> void:
var settle_l = load("res://core/battle_orchestrator.gd").new(_db, rng_l)
var fin_l := {}
settle_l.battle_finished.connect(func(p): fin_l["p"] = p)
Kit.check("破财直验战斗启动", settle_l.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "hero_atk": 999, "node_type": "boss"}))
Kit.check("破财直验战斗启动", settle_l.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "hero_atk": 999, "node_type": "boss"}))
for i in range(3):
settle_l.reward_grant().grant_routed({"reward_id": "gp_lose", "cell_type": "gp", "polarity": "negative", "display_name": "破财", "power": 2, "effect_id": "gp_loss", "stacks": 1},
"negative", {"source_class": "mine_hit", "cell": Vector2i(0, 0), "ri": 1})
Kit.check("破财 ×3 → escrow −6(负向缓冲记账,零钱包触达)", int(settle_l.reward_grant().escrow) == -6)
settle_l.minesweeper.settle_batch([{"action": "stop"}])
var gp_l: int = int(fin_l.get("p", {}).get("gp_dropped", 999))
var boss_drop: Dictionary = _db.shop_gp_drop_row("boss")
var boss_bonus1: int = _db.shop_level_gp_bonus(1)
var boss_drop: Dictionary = _db.run.shop.gp_drop_row("boss")
var boss_bonus1: int = _db.run.shop.level_gp_bonus_at(1)
Kit.check("破财压制 gp_dropped 负值上抛(%d + 6 = %d ∈ Boss 档掷定 [%d,%d] + 关级加值 %d——掷定不受破财污染)" % [
gp_l, gp_l + 6, int(boss_drop.get("roll_min", 0)), int(boss_drop.get("roll_max", 0)), boss_bonus1],
gp_l + 6 >= int(boss_drop.get("roll_min", 0)) + boss_bonus1 and gp_l + 6 <= int(boss_drop.get("roll_max", 0)) + boss_bonus1)
@@ -1048,7 +1048,7 @@ func _smoke_shop_room() -> void:
Kit.check("当前生命同步 +3 封顶新上限(%d → %d)" % [hp_before, runner.hero_hp], runner.hero_hp == mini(hp_before + 3, runner.effective_max_hp()))
# armor_up:AC 10+1
Kit.check("购买 armor_up 受理(现价 6)", runner.buy_product("armor_up"))
Kit.check("AC = 10+1(armor_class 加算区轴,无百分比通道)", runner.effective_ac() == int(_db.hero.armor_class) + 1)
Kit.check("AC = 10+1(armor_class 加算区轴,无百分比通道)", runner.effective_ac() == int(_db.combat.hero.armor_class) + 1)
# 余额不足购买被拒(零副作用):hp_up 连购至不可负担后兜底拒绝
var guard_drain := 20
var hp_buys := 1 # 夹具前段已购 1 笔(购买 hp_up 受理断言)——总数 = 前段 + 本循环
@@ -1068,12 +1068,12 @@ func _smoke_shop_room() -> void:
Kit.check("离开商店 → 回选路口(候选 = 店出边 %s——零自动推进收口)" % str(shop_outs),
runner.continue_after_node() and runner.awaiting_choice() and _sorted_ids(runner.next_candidate_ids()) == _sorted_ids(shop_outs))
# 下一战参数 = 基础表值 + 加算区(百分比已归零——加算先乘后乘序的前半在案)
Kit.check("主角攻击生效值 = 2+1(基础表值 + 加算区)", runner.effective_atk() == int(_db.hero.atk) + 1)
Kit.check("主角攻击生效值 = 2+1(基础表值 + 加算区)", runner.effective_atk() == int(_db.combat.hero.atk) + 1)
Kit.check("选终点 Boss(句柄 %d)开战" % int(shop_outs[0]), runner.pick_next_node(int(shop_outs[0])) and runner.current_battle() != null)
var snap: Dictionary = runner.current_battle().presentation_snapshot()
# max_hp 期望按实际购买笔数推导(可购笔数
# 随经济漂移;断言语义=「加算区笔数×3 全额入战斗开始参数」,不锚死恰好 1 笔)
Kit.check("战斗开始参数 = 基础表值 + 加算区(atk %d = 2+1 / max_hp %d = %d+%d×3)" % [int(snap["hero"].get("atk", -1)), int(snap["hero"].get("max_hp", -1)), int(_db.hero.max_hp), hp_buys], int(snap["hero"].get("atk", -1)) == int(_db.hero.atk) + 1 and int(snap["hero"].get("max_hp", -1)) == int(_db.hero.max_hp) + 3 * hp_buys)
Kit.check("战斗开始参数 = 基础表值 + 加算区(atk %d = 2+1 / max_hp %d = %d+%d×3)" % [int(snap["hero"].get("atk", -1)), int(snap["hero"].get("max_hp", -1)), int(_db.combat.hero.max_hp), hp_buys], int(snap["hero"].get("atk", -1)) == int(_db.combat.hero.atk) + 1 and int(snap["hero"].get("max_hp", -1)) == int(_db.combat.hero.max_hp) + 3 * hp_buys)
# 冒险收束(Boss 胜或败北——基线裸装口径任一结局)→ 钱包清零
var fin_end := {}
runner.level_finished.connect(func(p): fin_end["p"] = p)
@@ -1106,7 +1106,7 @@ func _smoke_shop_graph() -> void:
var path_total := 0
var graph_total := 0
for L in [1, 2, 3]:
if _db.level_gen_params_row(L).is_empty():
if _db.run.level_gen_params.row_by_level_index(L).is_empty():
Kit.check("L%d 参数档缺行(商店编排断言无源可扫)" % L, false)
continue
for seed_val in range(SWEEP_SEED_FROM, SWEEP_SEED_TO):
@@ -1238,7 +1238,7 @@ func _smoke_shop_pool() -> void:
print("--- 商店共享池(抽3买1全重抽 · 乘法定价 ceil · 池空硬闸 · 切边零入店)---")
var autoplay = load("res://sim/level_autoplay.gd")
var shelf_kinds: Array = []
for row in _db.shop_pool_rows():
for row in _db.run.shop.pool_rows():
shelf_kinds.append(String(row.get("kind_id", "")))
# ① 池模式场景可达(_adv_drive_to_shop 复用——L1 全程+L2 推进到商店房;pool 模式进店即抽架)
var scen := {}
@@ -1268,12 +1268,12 @@ func _smoke_shop_pool() -> void:
var r8 = scen["r"]
var gp0: int = int(r8.gp)
var cost0: int = r8.shop_refresh_cost()
Kit.check("刷新首价=基价(%d)" % cost0, cost0 == int(_db.adventure.shop_refresh_cost_base))
Kit.check("刷新首价=基价(%d)" % cost0, cost0 == int(_db.run.adventure.shop_refresh_cost_base))
Kit.check("基础免费 0 次(Q2——天赋增益位后续添加)", r8.shop_refresh_free_left() == 0)
var shelf_pre: Array = (r8.shop_shelf() as Array).duplicate(true)
Kit.check("GP 刷新受理(gp=%d cost=%d)" % [gp0, cost0], r8.shop_refresh())
Kit.check("GP 刷新扣费(%d→%d)" % [gp0, int(r8.gp)], int(r8.gp) == gp0 - cost0)
Kit.check("刷新爬价 ×curve(%d)" % int(r8.shop_refresh_cost()), int(r8.shop_refresh_cost()) == int(ceil(float(cost0) * float(_db.adventure.shop_refresh_cost_curve))))
Kit.check("刷新爬价 ×curve(%d)" % int(r8.shop_refresh_cost()), int(r8.shop_refresh_cost()) == int(ceil(float(cost0) * float(_db.run.adventure.shop_refresh_cost_curve))))
var same_shelf := first_shelf_a.size() == first_shelf_b.size()
if same_shelf:
for i in first_shelf_a.size():
@@ -1290,7 +1290,7 @@ func _smoke_shop_pool() -> void:
var l2 := int(r8.level_row.level_index)
# 现价等价证明(d11——消费端读表推导 vs 迁移前硬编码字面量推导:同输入逐位一致)
var relic_price_now: int = r8.pool_price("relic")
var relic_price_pre: int = int(ceil(15.0 * pow(_db.shop_price_b(), float(l2 - 1)) * 1.0))
var relic_price_pre: int = int(ceil(15.0 * pow(_db.run.shop.price_b, float(l2 - 1)) * 1.0))
Kit.check("relic 现价 = ceil(15.0×b^(关序−1))(读表与迁移前字面量逐位一致,%d)〔wd11〕" % relic_price_now, relic_price_now == relic_price_pre)
for c in r8.shop_shelf():
# T6 商店保底后 relic 卡合法入架(保底占位)——词表断言放行 relic;价格断言分叉
@@ -1300,10 +1300,10 @@ func _smoke_shop_pool() -> void:
Kit.check("货架 kind 在封闭词表或保底 relic 卡(%s)" % ckind, is_relic or shelf_kinds.has(ckind))
var expect_p: int
if is_relic:
expect_p = int(ceil(float(_db.shop.relic_base_price) * pow(_db.shop_price_b(), float(l2 - 1)) * 1.0))
expect_p = int(ceil(float(_db.run.shop.relic_base_price) * pow(_db.run.shop.price_b, float(l2 - 1)) * 1.0))
else:
var prow: Dictionary = _db.shop_pool_row(ckind)
expect_p = int(ceil(float(int(prow.get("base_price", 0))) * pow(_db.shop_price_b(), float(l2 - 1)) * 1.0))
var prow: Dictionary = _db.run.shop.pool_row(ckind)
expect_p = int(ceil(float(int(prow.get("base_price", 0))) * pow(_db.run.shop.price_b, float(l2 - 1)) * 1.0))
Kit.check("首购现价 = ceil(基价×b^(关序−1))(%s: %d = %d 推导)" % [ckind, int(c.get("price", -1)), expect_p], int(c.get("price", -1)) == expect_p)
# ③ 买 1 全重抽:出账+加算区+账本三落点 + 现价 A 链递增(买后 visit 计数=1;TP0——消费在架令牌本体)
var buy_c: Dictionary = {}
@@ -1321,8 +1321,8 @@ func _smoke_shop_pool() -> void:
var val0: int = a8.journal().bonus_total(String(val_map[kind]))
Kit.check("购买受理(pool 口——%s|%s)" % [kind, tier], r8.buy_pool_item(kind))
Kit.check("钱包扣现价(%d → %d)" % [gp0, int(r8.gp)], int(r8.gp) == gp0 - int(buy_c.get("price", 0)))
Kit.check("加算区 +令牌档位件值(池件=奖励件本体——%s|%s %+d)" % [kind, tier, int(_db.adventure.curio_value(kind, tier))], a8.journal().bonus_total(String(val_map[kind])) == val0 + int(_db.adventure.curio_value(kind, tier)))
Kit.check("共享池账本 -1(跨渠道单点——taken %d → %d)" % [taken0, int(a8.deck().taken.get(kind, 0))], int(a8.deck().taken.get(kind, 0)) == taken0 + 1)
Kit.check("加算区 +令牌档位件值(池件=奖励件本体——%s|%s %+d)" % [kind, tier, int(_db.run.adventure.curio_value(kind, tier))], a8.journal().bonus_total(String(val_map[kind])) == val0 + int(_db.run.adventure.curio_value(kind, tier)))
Kit.check("共享池账本 -1(跨渠道单点——taken %d → %d)" % [taken0, int(a8._curio.taken.get(kind, 0))], int(a8._curio.taken.get(kind, 0)) == taken0 + 1)
# A 链:同visit 第二购现价 = ceil(base×b^(l2−1)×A^1)(货架已全重抽——新卡新价同公式)
# A 链:白盒抬 GP 保证第二购场景确定构造(直改测试基建——非生产行为)
r8.gp = int(r8.gp) + 64
@@ -1332,8 +1332,8 @@ func _smoke_shop_pool() -> void:
second = c
break
if not second.is_empty():
var prow2: Dictionary = _db.shop_pool_row(String(second.get("kind_id", "")))
var expect_p2: int = int(ceil(float(int(prow2.get("base_price", 0))) * pow(_db.shop_price_b(), float(l2 - 1)) * _db.shop_price_a()))
var prow2: Dictionary = _db.run.shop.pool_row(String(second.get("kind_id", "")))
var expect_p2: int = int(ceil(float(int(prow2.get("base_price", 0))) * pow(_db.run.shop.price_b, float(l2 - 1)) * _db.run.shop.price_a))
Kit.check("A 链爬价 = ceil(base×b^(关序−1)×A^1)(进店第 2 购——%d)" % expect_p2, int(second.get("price", -1)) == expect_p2)
Kit.check("同 visit 第二购受理(A 链软闸在价不在限)", r8.buy_pool_item(String(second.get("kind_id", ""))))
else:
@@ -1384,8 +1384,8 @@ func _smoke_shop_pool() -> void:
var ps_ok := true
for k in ps.keys():
var quota := 0
for tier in _db.adventure.TIERS:
quota += int(_db.adventure.curio_quota(String(k), tier))
for tier in _db.run.adventure.TIERS:
quota += int(_db.run.adventure.curio_quota(String(k), tier))
if int(ps[k]) < 0 or int(ps[k]) > quota:
ps_ok = false
Kit.check("终局池余量在域 [0,配额](%s)" % str(ps), ps_ok)
@@ -1458,7 +1458,7 @@ func _adv_st() -> Dictionary:
# 首遇带数据驱动定位(契约 8):该 (L, battle, enemy_id) 的最低 depth_band,禁写死坐标数字
func _first_encounter_band(level_index: int, enemy_id: String) -> int:
var best := -1
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
if int(row.get("level_index", -1)) != level_index:
continue
if String(row.get("row_kind", "")) != "battle" or String(row.get("enemy_id", "")) != enemy_id:
@@ -89,7 +89,7 @@ func _run_battle(L: int, D: int, axes: Dictionary = {}) -> Dictionary:
settlement.minesweeper.seal_judged.connect(func(evt): out.events.append(evt))
settlement.minesweeper.round_end_board.connect(func(p): out.events.append({"ev": "round_end_board", "path_tag": p.path_tag}))
settlement.action_economy.round_end_stopped.connect(func(p): out.events.append({"ev": "round_end_stopped", "path_tag": p.path_tag}))
var enc := {"level_index": L, "node_depth": D, "hero_hp": _db.hero.max_hp}
var enc := {"level_index": L, "node_depth": D, "hero_hp": _db.combat.hero.max_hp}
enc.merge(axes, true) # 两根类型轴注入尾参(缺省零注入=回退面)
if not settlement.begin_battle(enc):
out.stream.append({"type": "verdict", "value": "begin_failed"})
@@ -132,22 +132,22 @@ func _meta_pricing_and_buy() -> void:
var sv = _meta_new_save(_META_SMOKE_PATHS[0])
var mp = load("res://core/meta_progression.gd").new(_db, sv)
# ③ 购买成功:级+1、余额−价;buy 内含写盘——reload 后持久(树全钥石单币)
sv.add_keystone(1000)
var bal0: int = sv.keystone_balance()
var price0: int = int(_db.tree_node("sp_hp").get("price", 0))
sv.meta_state._add_keystone(1000)
var bal0: int = sv.meta_state.keystone_balance()
var price0: int = int(_db.crossrun.meta_tree.row_by_id("sp_hp").get("price", 0))
Kit.check("树购买受理(Lv 0→1、余额 %d−%d)" % [bal0, price0],
mp.tree_buy("sp_hp") and sv.tree_level("sp_hp") == 1 and sv.keystone_balance() == bal0 - price0)
mp.tree_buy("sp_hp") and sv.meta_state.tree_state().nodes.get("sp_hp", 0) == 1 and sv.meta_state.keystone_balance() == bal0 - price0)
var sv_r = _meta_new_save(_META_SMOKE_PATHS[0])
Kit.check("购买持久(reload 后级与余额不变)",
sv_r.tree_level("sp_hp") == 1 and sv_r.keystone_balance() == bal0 - price0)
sv_r.meta_state.tree_state().nodes.get("sp_hp", 0) == 1 and sv_r.meta_state.keystone_balance() == bal0 - price0)
# ③ 余额不足 / 未知节点:false 零副作用
var sv_dry = _meta_new_save(_META_SMOKE_PATHS[1])
var mp_dry = load("res://core/meta_progression.gd").new(_db, sv_dry)
var tn_before: Dictionary = sv_dry.tree_nodes()
var fs_before: Dictionary = sv_dry.fork_sides()
var tn_before: Dictionary = sv_dry.meta_state.tree_state().nodes
var fs_before: Dictionary = sv_dry.meta_state.tree_state().fork_sides
Kit.check("余额不足 false 零副作用(0 余额购买拒绝——三域零变化〔天赋树批 V3〕)",
not mp_dry.tree_buy("sp_hp") and sv_dry.keystone_balance() == 0
and sv_dry.tree_nodes() == tn_before and sv_dry.fork_sides() == fs_before)
not mp_dry.tree_buy("sp_hp") and sv_dry.meta_state.keystone_balance() == 0
and sv_dry.meta_state.tree_state().nodes == tn_before and sv_dry.meta_state.tree_state().fork_sides == fs_before)
Kit.check("未知节点 false 零副作用(词表外出域)", not mp_dry.tree_buy("no_such_node"))
@@ -156,8 +156,8 @@ 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)
var row1: Dictionary = _db.crossrun.meta.boss_keystone_row(1)
var row2: Dictionary = _db.crossrun.meta.boss_keystone_row(2)
# ⑥ 逐杀掉落收束入账(掉落已在冒险内逐杀入账〔summary.keystone_total 含全部〕,
# keystone_drops 明细随摘要归档;首杀标记仍由 apply 逐关卡完成关次置位)
var d1: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
@@ -171,7 +171,7 @@ func _meta_settle_and_workshop() -> void:
Kit.check("明细 total = keystone_total(逐杀掉落全源——无二次派生加账)",
int(d1.get("total", 0)) == int(row1.get("first_kill", 0)))
Kit.check("余额 = 累计入账(逐杀掉落合计——无重复加账)",
sv.keystone_balance() == int(row1.get("first_kill", 0)))
sv.meta_state.keystone_balance() == int(row1.get("first_kill", 0)))
# ⑥ 再通转复杀档(首杀标记置位后小额恒定不递减——标记面在 apply,金额面在逐杀裁决)
var d2: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
"stopped_at": 2, "keystone_total": int(row1.get("repeat_kill", 0)),
@@ -196,39 +196,39 @@ func _meta_settle_and_workshop() -> void:
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)))
# ⑦ 败北零关卡完成摘要:余额只增不减(零入账)、clear_counts 不变
var bal_before: int = sv.keystone_balance()
var cc1_before: int = sv.clear_count(1)
var bal_before: int = sv.meta_state.keystone_balance()
var cc1_before: int = sv.meta_state.career_state().clear_counts.get(str(1), 0)
var dz: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 0,
"stopped_at": 1, "keystone_total": 0, "master_seed": 15})
Kit.check("败北零关卡完成摘要:零入账零标记(钥石只增不减——验收1;败北局 clear_counts 不变)",
dz["boss_drops"].is_empty() and sv.keystone_balance() == bal_before and sv.clear_count(1) == cc1_before)
dz["boss_drops"].is_empty() and sv.meta_state.keystone_balance() == bal_before and sv.meta_state.career_state().clear_counts.get(str(1), 0) == cc1_before)
# ⑧ 秘宝解锁服务(V3 钥石购买/V4 条件解锁;注入=直调解锁写口+直置存档生涯计数,
# 不走正常资源积累流程——卡片 7.1 测试方式)
var sv_u = _meta_new_save(_META_SMOKE_PATHS[11])
var mp_u = load("res://core/meta_progression.gd").new(_db, sv_u)
sv_u.add_keystone(10)
sv_u.meta_state._add_keystone(10)
Kit.check("条件件误购拒绝(购买件专属——契约 8 分配面)〔秘宝批 V3 反例〕",
not mp_u.buy_treasure_unlock("sky_eye") and not sv_u.item_unlocked("sky_eye")
not mp_u.buy_treasure_unlock("sky_eye") and not sv_u.meta_state.item_unlocked("sky_eye")
and not mp_u.buy_treasure_unlock("ranged_pick"))
Kit.check("余额不足购买拒绝且零副作用(余额 10 < 价 25)〔秘宝批 V3〕",
not mp_u.buy_treasure_unlock("twin_loader") and sv_u.keystone_balance() == 10)
not mp_u.buy_treasure_unlock("twin_loader") and sv_u.meta_state.keystone_balance() == 10)
Kit.check("钥石购买解锁:余额足→解锁态落档+余额扣减正确(价 10)〔秘宝批 V3〕",
mp_u.buy_treasure_unlock("morning_order") and sv_u.item_unlocked("morning_order")
and sv_u.keystone_balance() == 0)
mp_u.buy_treasure_unlock("morning_order") and sv_u.meta_state.item_unlocked("morning_order")
and sv_u.meta_state.keystone_balance() == 0)
Kit.check("已解锁重复购买拒绝〔秘宝批 V3〕",
not mp_u.buy_treasure_unlock("morning_order"))
Kit.check("条件未达不解锁(远程镐=冒险成功 1 次)〔秘宝批 V4〕",
mp_u.refresh_treasure_unlocks().is_empty() and not sv_u.item_unlocked("ranged_pick"))
mp_u.refresh_treasure_unlocks().is_empty() and not sv_u.meta_state.item_unlocked("ranged_pick"))
sv_u._data["meta"]["adventure_cleared"] = 1 # 直置存档生涯计数(调试注入口径)
Kit.check("远程镐=adventure_cleared≥1 达标恰解锁一次+重复刷新幂等(C6:冒险成功=打通整局冒险)〔秘宝批 V4〕",
(mp_u.refresh_treasure_unlocks() as Array).has("ranged_pick")
and sv_u.item_unlocked("ranged_pick") and mp_u.refresh_treasure_unlocks().is_empty())
and sv_u.meta_state.item_unlocked("ranged_pick") and mp_u.refresh_treasure_unlocks().is_empty())
sv_u._data["meta"]["boss_kill_count"] = 3
Kit.check("刷新执照=boss_kill_count≥3(阈值读 unlock_requires 行值不硬编)〔秘宝批 V4〕",
(mp_u.refresh_treasure_unlocks() as Array).has("refresh_license"))
sv_u._data["meta"]["adventure_cleared"] = 3
Kit.check("天眼单条件不足不解锁(AND 语义——cleared 3 达 boss 4 未达)〔秘宝批 V4〕",
not sv_u.item_unlocked("sky_eye"))
not sv_u.meta_state.item_unlocked("sky_eye"))
sv_u._data["meta"]["boss_kill_count"] = 4
Kit.check("天眼双条件 AND(冒险成功≥3 且 Boss≥4)达标解锁〔秘宝批 V4〕",
(mp_u.refresh_treasure_unlocks() as Array).has("sky_eye"))
@@ -240,7 +240,7 @@ func _meta_settle_and_workshop() -> void:
r_n += 1 if bool(st_row.get("unlocked", false)) else 0
Kit.check("解锁态持久投影一致(reload 面同源读数;5 件=购买 1+条件 4)〔秘宝批 V4〕", r_n == 5)
# ⑩ recent_summaries:元素含 boss_drops 副本、最新在前、cap 10
var rs: Array = sv.recent_summaries()
var rs: Array = sv.meta_state.career_state().recent_summaries
Kit.check("摘要最新在前 + 元素含掉落明细副本(复盘可归因——验收5)",
rs.size() >= 5 and String(rs[0].get("result", "")) != "" and rs[0].has("boss_drops")
and (rs[1] as Dictionary).has("keystone_drops")
@@ -251,7 +251,7 @@ func _meta_settle_and_workshop() -> void:
mp_c.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 0,
"stopped_at": 1, "keystone_total": 0, "master_seed": i})
Kit.check("摘要 cap 10 且最新在前(12 连 settle 截断后 seed 11 居首)",
sv_c.recent_summaries().size() == 10 and int(sv_c.recent_summaries()[0].get("master_seed", -1)) == 11)
sv_c.meta_state.career_state().recent_summaries.size() == 10 and int(sv_c.meta_state.career_state().recent_summaries[0].get("master_seed", -1)) == 11)
# ⑪ 同调位账本(装配持久/满位禁选/Y 升级语义;写口 = MetaProgression 同调段)
var sv_a = _meta_new_save(_META_SMOKE_PATHS[6])
var mp_a = load("res://core/meta_progression.gd").new(_db, sv_a)
@@ -259,17 +259,17 @@ func _meta_settle_and_workshop() -> void:
Attunement.slots(_db, sv_a) == 0 and int(mp_a.attune_camp_status()["refresh"]["charges"]) == 0)
Kit.check("满位禁选(Slots 0 时装配拒绝;兼未解锁拒绝前置——直置解锁后下方装配)〔秘宝批 V5〕",
mp_a.toggle_attunement("morning_order") == false)
sv_a.add_keystone(200)
sv_a.unlock_item("morning_order")
sv_a.unlock_item("twin_loader")
sv_a.meta_state._add_keystone(200)
sv_a.meta_state.unlock_item("morning_order")
sv_a.meta_state.unlock_item("twin_loader")
Kit.check("Y 升级购买入账(Lv1 → 每冒险 +1 次刷新)",
mp_a.buy_attune_refresh() and sv_a.attune_refresh_level() == 1
mp_a.buy_attune_refresh() and sv_a.meta_state.attune_state().refresh_level == 1
and int(mp_a.attune_camp_status()["refresh"]["charges"]) == 1)
mp_a.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
"stopped_at": 2, "keystone_total": 0, "master_seed": 21})
Kit.check("完成 1 关 → Slots 恒 0(旧「按击杀 Boss 次数自动涨」条件已删——槽位唯一来源=树点亮)且 Boss 击杀总数=1(计数域保留)〔天赋树批 V14〕",
Attunement.slots(_db, sv_a) == 0 and sv_a.boss_kill_count() == 1)
sv_a.set_tree_level("att_socket1", 1)
Attunement.slots(_db, sv_a) == 0 and sv_a.meta_state.career_state().boss_kill_count == 1)
sv_a.meta_state.set_tree_level("att_socket1", 1)
Kit.check("树上点亮同调位 1 → Slots=1(槽位收编树节点——Q2 裁甲)",
Attunement.slots(_db, sv_a) == 1)
Kit.check("稀有件入普通格拒绝(件档 rank 3 > 装入位序 1——契约 2 档位格校验)〔秘宝批 V5〕",
@@ -279,7 +279,7 @@ func _meta_settle_and_workshop() -> void:
Kit.check("卸下恒可再装配(自卸自装——营地换装=冒险开始前合法面,装配不可变只锁运行中)〔秘宝批换主语〕",
mp_a.toggle_attunement("morning_order") and mp_a.toggle_attunement("morning_order"))
var sv_a2 = _meta_new_save(_META_SMOKE_PATHS[7])
sv_a2.set_attunement_equipped(["morning_order"])
sv_a2.meta_state.set_attunement_equipped(["morning_order"])
var mp_a2 = load("res://core/meta_progression.gd").new(_db, sv_a2)
Kit.check("装配关系读回持久(reload 后投影一致——写新件 id)〔秘宝批 V15〕",
(mp_a2.attune_camp_status()["equipped"] as Array).has("morning_order"))
@@ -294,20 +294,20 @@ func _meta_settle_and_workshop() -> void:
a12.refresh_bank()._refresh_y = 2
a12.stop()._pending = {"candidates": [{"kind": "gp", "tier": "common"}, {"kind": "attack", "tier": "common"}, {"kind": "max_hp", "tier": "common"}], "payload": {}}
Kit.check("刷新双计数器——X 优先消耗(x1y2 → x0y2)",
a12.stop().refresh_curio() and a12.refresh_bank().refresh_y() == 2 and a12.refresh_bank().free_left() == 2 + a12.refresh_bank().refresh_stock())
a12.stop().refresh_curio() and a12.refresh_bank()._refresh_x == 0 and a12.refresh_bank()._refresh_y == 2)
Kit.check("刷新双计数器——X 耗尽转 Y 池、池竭拒绝(x0y2 → y0 → 第四次 false 零副作用)",
a12.stop().refresh_curio() and a12.stop().refresh_curio() and a12.refresh_bank().refresh_y() == 0
a12.stop().refresh_curio() and a12.stop().refresh_curio() and a12.refresh_bank()._refresh_y == 0
and a12.stop().refresh_curio() == false)
a12.refresh_bank().reset_x()
Kit.check("刷新 X 每碰三选一重置(不结转;新目录无 bonus_refresh 行源——X 机制保留恒 0 空转,存量加仓走 _refresh_stock=刷新练习 V8)〔秘宝批换代〕",
a12.refresh_bank().free_left() == 0)
a12.refresh_bank()._refresh_x == 0 and a12.refresh_bank().free_left() == 0)
# ⑬ 主菜单开局传参面(开新/续玩两入口同调三键随开局下传防回归:真实入口曾
# 三键恒空(缺陷①),冒烟夹具直呼 runner 参数面拦不住——此处直呼 main.build_adventure_params
# 单测统一构造面(static 免实例化整菜单场景;真实两入口共用本构造),取值与营地投影同源)
var sv_e = _meta_new_save(_META_SMOKE_PATHS[8])
sv_e.set_attunement_equipped(["morning_order"])
sv_e.set_attune_refresh_level(2)
sv_e.set_fork_side("fg_posture", "a")
sv_e.meta_state.set_attunement_equipped(["morning_order"])
sv_e.meta_state.set_attune_refresh_level(2)
sv_e.meta_state.set_fork_side("fg_posture", "a")
var mp_e = load("res://core/meta_progression.gd").new(_db, sv_e)
var rules_e: Array = mp_e.tree_rule_keys()
Kit.check("开局传参夹具前置:Fork 侧 a 激活 rule 键非空 + Y=Lv2 行表值 2(防断言空转)",
@@ -315,7 +315,7 @@ func _meta_settle_and_workshop() -> void:
var p_new: Dictionary = load("res://presentation/main.gd").build_adventure_params(sv_e, mp_e,
{"treasure_unlocked": {"wide_scout_band": true}, "shop_mode": "pool"})
Kit.check("主菜单开新冒险参数面:同调装配 = 存档装配集(营地装了什么开局就带什么)",
(p_new.get("attunement", []) as Array) == (sv_e.attunement_equipped() as Array))
(p_new.get("attunement", []) as Array) == (sv_e.meta_state.attune_state().equipped as Array))
Kit.check("主菜单开新冒险参数面:Y 刷新池 = 当前等级行表值(营地升几档开局就给几次)",
int(p_new.get("attune_refresh_y", -1)) == mp_e.attune_refresh_charges())
Kit.check("主菜单开新冒险参数面:树 rule 键集 = Fork 激活侧 effects(点哪侧带哪侧)",
@@ -325,7 +325,7 @@ func _meta_settle_and_workshop() -> void:
var p_resume: Dictionary = load("res://presentation/main.gd").build_adventure_params(sv_e, mp_e,
{"treasure_unlocked": {}})
Kit.check("主菜单继续上局参数面:三键与开新冒险同源同值(续玩不吃白板开局)",
(p_resume.get("attunement", []) as Array) == (sv_e.attunement_equipped() as Array)
(p_resume.get("attunement", []) as Array) == (sv_e.meta_state.attune_state().equipped as Array)
and int(p_resume.get("attune_refresh_y", -1)) == mp_e.attune_refresh_charges()
and (p_resume.get("tree_rules", []) as Array) == rules_e)
# 续玩路径 tree_rules 受理断言:直呼 runner 续玩参数面
@@ -338,8 +338,7 @@ func _meta_settle_and_workshop() -> void:
Kit.check("继续上局受理面:resume_adventure 受理三键参数面(尾裁微批b 受理闭环——受理不再吃白板)",
adr_res.resume_adventure(bp_min, p_resume))
Kit.check("继续上局受理面:tree_rules 随参数面进 runner._tree_rules(消费面 set_tree_rules 下传现成)",
adr_res.runner() != null and adr_res.runner().pick_next_node(int(adr_res.runner().next_candidate_ids()[0]))
and ((adr_res.runner().current_battle().encounter.get("tree_rules", []) as Array) == rules_e))
(adr_res._tree_rules as Array) == rules_e)
_meta_smoke_cleanup()
@@ -350,9 +349,9 @@ func _meta_injection() -> void:
var mp = load("res://core/meta_progression.gd").new(_db, sv)
# 树生命淬炼注入(公式:基础值+树淬炼 flat+锻造路线加值位预留 0——2 级 = +10;
# 注入走调试口径直置等级〔两分法——购买链已在 _smoke_tree 覆盖,此处隔离行门槛干扰〕)
sv.set_tree_level("sp_hp", 2)
sv.meta_state.set_tree_level("sp_hp", 2)
Kit.check("生命淬炼 2 级 → hero_override().max_hp == Σ 节点 effects×等级(验收4 换代〔天赋树批 V12〕)",
int(mp.hero_override().get("max_hp", -1)) == 2 * int(_db.tree_node("sp_hp").get("effects", {}).get("max_hp", 0)))
int(mp.hero_override().get("max_hp", -1)) == 2 * int(_db.crossrun.meta_tree.row_by_id("sp_hp").get("effects", {}).get("max_hp", 0)))
Kit.check("攻/AC 树零来源(契约 9——树表零 atk/armor_class 行,override 二轴恒 0+预留位 0)",
int(mp.hero_override().get("atk", -1)) == 0 and int(mp.hero_override().get("armor_class", -1)) == 0)
Kit.check("命中线在账未注入(契约 6——override 无 hit 字段〔天赋树批 V13〕)",
@@ -363,10 +362,10 @@ func _meta_injection() -> void:
var r = adr.runner()
var ov: Dictionary = mp.hero_override()
Kit.check("首战 hero_atk = 表值 + 永久强化 flat(begin_battle 同表达式——下一局能用到强化)",
int(r.effective_atk()) == int(_db.hero.atk) + int(ov.get("atk", 0)))
int(r.effective_atk()) == int(_db.combat.hero.atk) + int(ov.get("atk", 0)))
Kit.check("首战 hero_max_hp 与开局生命账本 = 表值 + 注入档",
int(r.effective_max_hp()) == int(_db.hero.max_hp) + int(ov.get("max_hp", 0))
and int(adr.stats().hero_hp) == int(_db.hero.max_hp) + int(ov.get("max_hp", 0)))
int(r.effective_max_hp()) == int(_db.combat.hero.max_hp) + int(ov.get("max_hp", 0))
and int(adr.stats().hero_hp) == int(_db.combat.hero.max_hp) + int(ov.get("max_hp", 0)))
# 白板:同种子下 空参数 = 零强化参数 = 纯表值(逐位一致——注入通道默认值漏改在此转红)
var adr_w = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(920251))
Kit.check("白板冒险启动(无 params 实参——B1 空参数路径)", adr_w.start_adventure(920251))
@@ -377,11 +376,11 @@ func _meta_injection() -> void:
var rw = adr_w.runner()
var rz = adr_z.runner()
Kit.check("白板成立:空参数 = 零强化参数 = 纯表值(atk/max_hp/ac 三轴逐位一致——验收 2/3)",
int(rw.effective_atk()) == int(_db.hero.atk) and int(rw.effective_max_hp()) == int(_db.hero.max_hp)
int(rw.effective_atk()) == int(_db.combat.hero.atk) and int(rw.effective_max_hp()) == int(_db.combat.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()))
Kit.check("ac 轴:白板恒表值、未换强化时注入零位移",
int(rw.effective_ac()) == int(_db.hero.armor_class) and int(r.effective_ac()) == int(_db.hero.armor_class))
int(rw.effective_ac()) == int(_db.combat.hero.armor_class) and int(r.effective_ac()) == int(_db.combat.hero.armor_class))
_meta_smoke_cleanup()
@@ -394,10 +393,10 @@ func _smoke_forge_axes() -> void:
not ("attack_type" in _db.hero) and not ("armor_type" in _db.hero))
# 锻造配置默认值 = normal/light(常驻持有——与柜台解锁状态解耦,未点亮也照常可读)
Kit.check("锻造配置默认值 attack_type=normal / armor_type=light(常驻持有)〔锻造工坊批 V5〕",
_db.forge_default_attack_type() == "normal" and _db.forge_default_armor_type() == "light")
_db.crossrun.forge.default_attack_type == "normal" and _db.crossrun.forge.default_armor_type == "light")
# 单位快照回退面:spawn 三参形态(零注入)= 锻造配置默认值
var uc = load("res://core/unit_container.gd").new(_db)
Kit.check("spawn 受理(三参形态——两轴缺省回退面)", uc.spawn(1, 0, int(_db.hero.max_hp)))
Kit.check("spawn 受理(三参形态——两轴缺省回退面)", uc.spawn({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)}))
Kit.check("主角单位快照两轴 = 锻造配置默认值 normal/light(零注入回退——行为连续)〔锻造工坊批 V5〕",
String(uc.hero_unit().get("attack_type", "")) == "normal"
and String(uc.hero_unit().get("armor_type", "")) == "light")
@@ -407,71 +406,71 @@ func _smoke_forge_axes() -> void:
var sv2 = _meta_new_save(_META_SMOKE_PATHS[13])
var mp2 = load("res://core/meta_progression.gd").new(_db, sv2)
# V2①:长剑首购=20(0 级位几何价)、等级 0→1、扣币即盘、reload 持久
sv2.add_keystone(100)
sv2.meta_state._add_keystone(100)
Kit.check("长剑锻造首购:扣 20 钥石、等级 0→1(Q1——第一级=获得装备=首个 +1)〔锻造工坊批 V2〕",
mp2.forge_buy_level("sword") and sv2.forge_route_level("sword") == 1
and sv2.keystone_balance() == 80)
mp2.forge_buy_level("sword") and sv2.meta_state.forge_state().levels.get("sword", 0) == 1
and sv2.meta_state.keystone_balance() == 80)
var sv2r = _meta_new_save(_META_SMOKE_PATHS[13])
Kit.check("购买持久(reload 后等级与余额不变——即时写盘)〔锻造工坊批 V2〕",
sv2r.forge_route_level("sword") == 1 and sv2r.keystone_balance() == 80)
sv2r.meta_state.forge_state().levels.get("sword", 0) == 1 and sv2r.meta_state.keystone_balance() == 80)
# V2②:二级=30(1 级位);六路线独立(买长剑不动轻甲)
Kit.check("长剑二级购:扣 30、等级→2;轻甲独立零扰动(六路线各练各的)〔锻造工坊批 V2〕",
mp2.forge_buy_level("sword") and sv2.forge_route_level("sword") == 2
and sv2.keystone_balance() == 50 and sv2.forge_route_level("light") == 0)
mp2.forge_buy_level("sword") and sv2.meta_state.forge_state().levels.get("sword", 0) == 2
and sv2.meta_state.keystone_balance() == 50 and sv2.meta_state.forge_state().levels.get("light", 0) == 0)
# V2③:余额不足拒且零副作用(等级/选中/解锁三面零变化)
var sv2d = _meta_new_save(_META_SMOKE_PATHS[14])
var mp2d = load("res://core/meta_progression.gd").new(_db, sv2d)
var fr_dry: Dictionary = sv2d.forge_routes_snapshot()
var fr_dry: Dictionary = sv2d.meta_state.forge_state()
Kit.check("余额不足购买拒绝且零副作用(0 余额——三域零变化)〔锻造工坊批 V2〕",
not mp2d.forge_buy_level("sword") and sv2d.keystone_balance() == 0
and sv2d.forge_routes_snapshot() == fr_dry)
not mp2d.forge_buy_level("sword") and sv2d.meta_state.keystone_balance() == 0
and sv2d.meta_state.forge_state() == fr_dry)
# V2④:已满级拒购(cap 闸);未解锁拒购
sv2.set_forge_route_level("sword", 5)
sv2.meta_state.set_forge_route_level("sword", 5)
Kit.check("已满级拒购/未解锁拒购(cap 闸+解锁闸——零副作用)〔锻造工坊批 V2〕",
not mp2.forge_buy_level("sword") and not mp2d.forge_buy_level("spear"))
# V3① 通道一:练满前一档(长剑 cap)→ 长枪/法杖派生解锁(零收费);轻甲未满→甲线仍锁
Kit.check("通道一:长剑练满→长枪/法杖派生解锁;轻甲未练满→重甲/魔法护甲仍锁〔锻造工坊批 V3〕",
mp2.forge_route_unlocked("spear") and mp2.forge_route_unlocked("staff")
and not mp2.forge_route_unlocked("heavy") and not mp2.forge_route_unlocked("ward"))
mp2._forge_route_unlocked("spear") and mp2._forge_route_unlocked("staff")
and not mp2._forge_route_unlocked("heavy") and not mp2._forge_route_unlocked("ward"))
Kit.check("派生解锁后长枪可购(首购 20——余额 50−20)〔锻造工坊批 V3〕",
mp2.forge_buy_level("spear") and sv2.forge_route_level("spear") == 1
and sv2.keystone_balance() == 30)
mp2.forge_buy_level("spear") and sv2.meta_state.forge_state().levels.get("spear", 0) == 1
and sv2.meta_state.keystone_balance() == 30)
# V3② 通道二:一次性解锁价直购(未练满也可买——两通道互不影响)
var sv3 = _meta_new_save(_META_SMOKE_PATHS[15])
var mp3 = load("res://core/meta_progression.gd").new(_db, sv3)
sv3.add_keystone(500)
sv3.meta_state._add_keystone(500)
Kit.check("通道二:魔法护甲一次性解锁价 200 直购开(轻甲未练满也可买)〔锻造工坊批 V3〕",
not mp3.forge_route_unlocked("ward") and mp3.forge_buy_unlock("ward")
and sv3.keystone_balance() == 300 and mp3.forge_route_unlocked("ward"))
not mp3._forge_route_unlocked("ward") and mp3.forge_buy_unlock("ward")
and sv3.meta_state.keystone_balance() == 300 and mp3._forge_route_unlocked("ward"))
# V3③ 直购后练满不重复收(练满通道=派生零收费;已解锁再直购=拒绝)
sv3.set_forge_route_level("light", 5)
sv3.meta_state.set_forge_route_level("light", 5)
Kit.check("直购后练满不重复收(已解锁=拒绝零收费——两通道互不影响)〔锻造工坊批 V3〕",
mp3.forge_route_unlocked("ward") and not mp3.forge_buy_unlock("ward")
and sv3.keystone_balance() == 300)
mp3._forge_route_unlocked("ward") and not mp3.forge_buy_unlock("ward")
and sv3.meta_state.keystone_balance() == 300)
# V3④ 不设串联门槛(法杖直购开——长枪零状态不阻;dry 档充值走直购)
sv2d.add_keystone(500)
sv2d.meta_state._add_keystone(500)
Kit.check("不设串联门槛(法杖直购开——长枪零状态不阻)〔锻造工坊批 V3〕",
mp2d.forge_buy_unlock("staff") and mp2d.forge_route_unlocked("staff")
and sv2d.forge_route_level("spear") == 0 and not mp2d.forge_route_unlocked("spear"))
mp2d.forge_buy_unlock("staff") and mp2d._forge_route_unlocked("staff")
and sv2d.meta_state.forge_state().levels.get("spear", 0) == 0 and not mp2d._forge_route_unlocked("spear"))
# V4:任一购买/解锁只扣钥石——GP 侧零触碰(meta 域无 GP 键=域界扫描同款;此处钉余额差=当级价)
var ks4: int = sv3.keystone_balance()
var ks4: int = sv3.meta_state.keystone_balance()
Kit.check("两币分工:购买只扣钥石(余额差=当级价 20——GP 零触碰、无材料计数)〔锻造工坊批 V4〕",
mp3.forge_buy_level("ward") and sv3.keystone_balance() == ks4 - 20
and sv3.forge_route_level("ward") == 1)
mp3.forge_buy_level("ward") and sv3.meta_state.keystone_balance() == ks4 - 20
and sv3.meta_state.forge_state().levels.get("ward", 0) == 1)
# V6①:营地切换=写存档域+免费(余额零动);旧路线练度保留(只不生效)
var ks6: int = sv3.keystone_balance()
var ks6: int = sv3.meta_state.keystone_balance()
Kit.check("选中切换免费(余额零动——写存档域即盘)〔锻造工坊批 V6〕",
mp3.forge_select_armor("ward") and sv3.forge_armor() == "ward"
and sv3.keystone_balance() == ks6)
mp3.forge_select("armor", "ward") and sv3.meta_state.forge_state().armor == "ward"
and sv3.meta_state.keystone_balance() == ks6)
Kit.check("切换后旧路线练度保留(轻甲 5 级不丢——回家待命只不生效)〔锻造工坊批 V6〕",
sv3.forge_route_level("light") == 5)
sv3.meta_state.forge_state().levels.get("light", 0) == 5)
# V6②:未解锁路线拒选零副作用(dry 档:长枪未解锁)
Kit.check("未解锁路线拒选零副作用〔锻造工坊批 V6〕",
not mp2d.forge_select_weapon("spear") and sv2d.forge_weapon() == "sword")
not mp2d.forge_select("weapon", "spear") and sv2d.meta_state.forge_state().weapon == "sword")
# V6③:类型轴读数随选中走(切换后读 ward;未解锁选中态防御回退默认值)
Kit.check("类型轴当前生效值随选中走(ward;未解锁档回退默认 normal/light)〔锻造工坊批 V5/V6〕",
mp3.hero_armor_type() == "ward" and mp2d.hero_attack_type() == "normal"
and mp2d.hero_armor_type() == "light")
mp3.hero_type_axes()["armor"] == "ward" and mp2d.hero_type_axes()["attack"] == "normal"
and mp2d.hero_type_axes()["armor"] == "light")
# ---- T4 注入接线(V5 后半 · V6 锁死 · V7 公式对拍 · V10 树零攻防复核)----
# V5:build_adventure_params 注入默认值(新档=树钥匙未点亮)+点亮后读存档选中覆盖
@@ -481,9 +480,9 @@ func _smoke_forge_axes() -> void:
Kit.check("build_adventure_params 注入默认值 normal/light(树钥匙未点亮——新档初始持有态)〔锻造工坊批 V5〕",
String(bp_params.get("attack_type", "")) == "normal"
and String(bp_params.get("armor_type", "")) == "light")
sv4.add_keystone(1000)
sv4.meta_state._add_keystone(1000)
Kit.check("长枪直购解锁+首购+选中(注入面夹具——200+20 钥石)",
mp4.forge_buy_unlock("spear") and mp4.forge_buy_level("spear") and mp4.forge_select_weapon("spear"))
mp4.forge_buy_unlock("spear") and mp4.forge_buy_level("spear") and mp4.forge_select("weapon", "spear"))
var bp_sel: Dictionary = load("res://presentation/main.gd").build_adventure_params(sv4, mp4)
Kit.check("build_adventure_params 读存档选中覆盖(选中长枪→pierce)〔锻造工坊批 V5〕",
String(bp_sel.get("attack_type", "")) == "pierce"
@@ -494,17 +493,17 @@ func _smoke_forge_axes() -> void:
and int(mp4.hero_override().get("max_hp", -1)) == 0
and int(mp4.hero_override().get("armor_class", -1)) == 0
and not mp4.hero_override().has("hit"))
sv4.set_forge_route_level("sword", 3) # 长剑练度待命(未选中)
sv4.meta_state.set_forge_route_level("sword", 3) # 长剑练度待命(未选中)
Kit.check("换当前武器→攻击加值随切换(长剑 3 级待命不生效——只认选中)〔锻造工坊批 V7〕",
int(mp4.hero_override().get("atk", -1)) == 1)
Kit.check("切回长剑→加值=3 级量(旧路线练度保留且生效)〔锻造工坊批 V7〕",
mp4.forge_select_weapon("sword") and int(mp4.hero_override().get("atk", -1)) == 3)
mp4.forge_select("weapon", "sword") and int(mp4.hero_override().get("atk", -1)) == 3)
# V7 三层 AC:重甲(基础 12)+路线 Lv1 加值+局内 0——经参数面起冒险断 runner 生效值
var sv5 = _meta_new_save(_META_SMOKE_PATHS[17])
var mp5 = load("res://core/meta_progression.gd").new(_db, sv5)
sv5.add_keystone(1000)
sv5.meta_state._add_keystone(1000)
Kit.check("重甲直购解锁+首购+选中(三层 AC 夹具)",
mp5.forge_buy_unlock("heavy") and mp5.forge_buy_level("heavy") and mp5.forge_select_armor("heavy"))
mp5.forge_buy_unlock("heavy") and mp5.forge_buy_level("heavy") and mp5.forge_select("armor", "heavy"))
var adr5 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(77))
var p5: Dictionary = load("res://presentation/main.gd").build_adventure_params(sv5, mp5)
Kit.check("注入参数冒险启动(两轴+override 同面)", adr5.start_adventure(77, p5))
@@ -515,7 +514,7 @@ func _smoke_forge_axes() -> void:
adr5.stats().attack_type == "normal" and adr5.stats().armor_type == "heavy")
# V6 锁死:开冒险后改存档选中→参数面/注入值不变(快照冻结;light=初始持有已解锁可切)
Kit.check("冒险开始后锁死:改存档选中不影响参数面快照与生效值(契约 8)〔锻造工坊批 V6〕",
mp5.forge_select_armor("light") and adr5.stats().armor_type == "heavy"
mp5.forge_select("armor", "light") and adr5.stats().armor_type == "heavy"
and int(r5.effective_ac()) == 12 + 1)
# V5/V7 末端:enc 两轴直达 spawn 英雄快照(hero_attack 行携带选中类型键)
var run_ax = _run_battle(1, 0, {"attack_type": "pierce", "armor_type": "ward"})
@@ -528,12 +527,12 @@ func _smoke_forge_axes() -> void:
# spawn 显式注入面(三参旧形态零注入已在上方覆盖——此处钉显式值)
var uc2 = load("res://core/unit_container.gd").new(_db)
Kit.check("spawn 显式两轴注入(magic/heavy 直入英雄快照)〔锻造工坊批 V5〕",
uc2.spawn(1, 0, int(_db.hero.max_hp), [], -1, -1, [], "magic", "heavy")
uc2.spawn({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp), "hero_attack_type": "magic", "hero_armor_type": "heavy"})
and String(uc2.hero_unit().get("attack_type", "")) == "magic"
and String(uc2.hero_unit().get("armor_type", "")) == "heavy")
# V10 复核:树表 12 行 effects 键集无 atk/armor_class(契约 1——树零攻防来源)
var tree_atk_ac := false
for trow in _db.tree_rows():
for trow in _db.crossrun.meta_tree.rows:
var eff: Dictionary = trow.get("effects", {})
if eff.has("atk") or eff.has("armor_class"):
tree_atk_ac = true
@@ -547,11 +546,11 @@ func _smoke_tree() -> void:
print("--- 天赋树(12 节点 · 购买面 · Fork 双态 · 彩蛋觉醒)---")
# 〔V1〕结构:12 行 + schema 零违例
Kit.check("meta_tree 已加载(12 节点=脊柱 2〔生命/命中淬炼一线一节点 cap4〕+锻造 2〔钥匙+彩蛋——彩蛋含在锻造 2 内非第 13 行〕+钥石 2〔秘宝匣/刷新升级〕+同调 6〔socket 3+Fork 3 双态〕——v0.8 终态)〔天赋树批 V1〕",
_db.meta_tree != null and (_db.meta_tree.rows as Array).size() == 12)
_db.meta_tree != null and (_db.crossrun.meta_tree.rows as Array).size() == 12)
Kit.check("MetaTreeTable.validate 零违例(脉/类型/币词表出域即拒/Fork 三组每组恰一且持 a/b 双效果键/彩蛋 ≤3/effects 键封闭/零 archetype)",
_db.meta_tree.validate().is_empty())
_db.crossrun.meta_tree.validate().is_empty())
# 彩蛋行硬要求(契约 9 红线落点):effects 键集 ⊆ {max_hp, hit}
var ley: Dictionary = _db.meta_tree.row_by_id("easter_ley")
var ley: Dictionary = _db.crossrun.meta_tree.row_by_id("easter_ley")
var ley_ok: bool = not ley.is_empty()
for k in (ley.get("effects", {}) as Dictionary).keys():
if not (String(k) == "max_hp" or String(k) == "hit"):
@@ -560,66 +559,66 @@ func _smoke_tree() -> void:
ley_ok)
# V1/V4 反例:出域行逐类构造被拒(复制行副本塞词表外值)
var t2 = load("res://data/schemas/meta_tree_table.gd").new()
var bad_vein: Dictionary = (_db.meta_tree.row_by_id("sp_hp") as Dictionary).duplicate(true)
var bad_vein: Dictionary = (_db.crossrun.meta_tree.row_by_id("sp_hp") as Dictionary).duplicate(true)
bad_vein["node_id"] = "bad_vein"
bad_vein["vein"] = "underworld"
t2.rows = [bad_vein]
Kit.check("schema 反例:vein 出域被拒", not t2.validate().is_empty())
var bad_fork: Dictionary = (_db.meta_tree.row_by_id("fork_posture") as Dictionary).duplicate(true)
var bad_fork: Dictionary = (_db.crossrun.meta_tree.row_by_id("fork_posture") as Dictionary).duplicate(true)
bad_fork["node_id"] = "bad_fork"
bad_fork["fork_group"] = "fg_mastery"
t2.rows = [bad_fork]
Kit.check("schema 反例:fork_group 出域(fg_mastery 已退役组)被拒", not t2.validate().is_empty())
var bad_arch: Dictionary = (_db.meta_tree.row_by_id("sp_hp") as Dictionary).duplicate(true)
var bad_arch: Dictionary = (_db.crossrun.meta_tree.row_by_id("sp_hp") as Dictionary).duplicate(true)
bad_arch["node_id"] = "bad_arch"
bad_arch["effects"] = {"archetype": 1}
t2.rows = [bad_arch]
Kit.check("schema 反例:effects.archetype 加载致命(🔴 层级纪律护栏面)〔天赋树批 V4〕",
not t2.validate().is_empty())
var bad_dangling: Dictionary = (_db.meta_tree.row_by_id("sp_hp") as Dictionary).duplicate(true)
var bad_dangling: Dictionary = (_db.crossrun.meta_tree.row_by_id("sp_hp") as Dictionary).duplicate(true)
bad_dangling["requires"] = ["ghost_node"]
t2.rows = [bad_dangling]
Kit.check("schema 反例:requires 悬空被拒", not t2.validate().is_empty())
# 购买面(V2 主干)
var sv = _meta_new_save(_META_SMOKE_PATHS[8])
var mp = load("res://core/meta_progression.gd").new(_db, sv)
sv.add_keystone(1000)
sv.meta_state._add_keystone(1000)
Kit.check("树购买受理(脊柱生命 L1——树全钥石单币)",
mp.tree_buy("sp_hp") and sv.tree_level("sp_hp") == 1)
mp.tree_buy("sp_hp") and sv.meta_state.tree_state().nodes.get("sp_hp", 0) == 1)
Kit.check("行门槛:第 2 级不可购(总等级 1 < 4——一线一节点主语,买第 N 级需全树总投入 ≥4×(N−1))〔天赋树批 V2〕",
not mp.tree_can_buy("sp_hp"))
Kit.check("命中 L1/锻造钥匙(free 无前置=初始零购零币可点亮)/同调位 1(根节点无前置)受理(总等级 1→4)",
mp.tree_buy("sp_hit") and mp.tree_buy("forge_workshop") and mp.tree_buy("att_socket1"))
Kit.check("锻造工坊钥匙零币(free)", int(_db.tree_node("forge_workshop").get("price", -1)) == 0)
Kit.check("锻造工坊钥匙零币(free)", int(_db.crossrun.meta_tree.row_by_id("forge_workshop").get("price", -1)) == 0)
Kit.check("行门槛恰达开放(总等级 4 ≥ 4×1)", mp.tree_can_buy("sp_hp"))
Kit.check("第 2 级购买受理(门槛恰达当刻开放)",
mp.tree_buy("sp_hp") and sv.tree_level("sp_hp") == 2)
mp.tree_buy("sp_hp") and sv.meta_state.tree_state().nodes.get("sp_hp", 0) == 2)
# Fork 一节点双态(V9)
sv.add_keystone(100)
sv.meta_state._add_keystone(100)
Kit.check("Fork 购买=解锁+默认 A 态(一节点双态——单行承载)",
mp.tree_buy_fork("fork_posture") and sv.tree_level("fork_posture") == 1 and sv.fork_side("fg_posture") == "a")
mp.tree_buy_fork("fork_posture") and sv.meta_state.tree_state().nodes.get("fork_posture", 0) == 1 and sv.meta_state.tree_state().fork_sides.get("fg_posture", "") == "a")
Kit.check("已解锁 Fork 重复购买拒绝(cap 1)", not mp.tree_can_buy("fork_posture"))
Kit.check("同态重复切换零操作(false)", not mp.tree_toggle_fork("fg_posture", "a"))
Kit.check("未解锁 Fork 切换拒绝(解锁唯一入口=购买面)", not mp.tree_toggle_fork("fg_sense", "b"))
var bal_pre_toggle: int = sv.keystone_balance()
var bal_pre_toggle: int = sv.meta_state.keystone_balance()
Kit.check("零币切态 b(购买解锁后 A/B 可切)",
mp.tree_toggle_fork("fg_posture", "b") and sv.fork_side("fg_posture") == "b")
Kit.check("切态零消费(钥石余额差分不动)", int(sv.keystone_balance()) == bal_pre_toggle)
mp.tree_toggle_fork("fg_posture", "b") and sv.meta_state.tree_state().fork_sides.get("fg_posture", "") == "b")
Kit.check("切态零消费(钥石余额差分不动)", int(sv.meta_state.keystone_balance()) == bal_pre_toggle)
Kit.check("规则键投影=激活态效果键(B 态稳健拓荒——tree_rule_keys 随切态换键集)",
(mp.tree_rule_keys() as Array).has("posture_bulwark") and not (mp.tree_rule_keys() as Array).has("posture_first_strike"))
Kit.check("切回 A 态键集随换(六键词表在册——V11 校验面归单元段)",
mp.tree_toggle_fork("fg_posture", "a") and (mp.tree_rule_keys() as Array).has("posture_first_strike"))
# 彩蛋特化觉醒(V8)
Kit.check("彩蛋不经购买(手动购亮=红——觉醒唯一入口=特化判定)", not mp.tree_can_buy("easter_ley"))
Kit.check("觉醒前置未满足不亮(两线未拉满)", sv.tree_level("easter_ley") == 0)
sv.set_tree_level("sp_hp", 4)
sv.set_tree_level("sp_hit", 4)
sv.set_tree_level("att_socket2", 1)
Kit.check("觉醒前置未满足不亮(两线未拉满)", sv.meta_state.tree_state().nodes.get("easter_ley", 0) == 0)
sv.meta_state.set_tree_level("sp_hp", 4)
sv.meta_state.set_tree_level("sp_hit", 4)
sv.meta_state.set_tree_level("att_socket2", 1)
mp.tree_buy("att_socket3") # 经购买口触发 _check_easter_egg
Kit.check("两线拉满+同调位全解锁 → 地脉共鸣自动点亮(零币特化判定)", sv.tree_level("easter_ley") == 1)
var bal_now: int = sv.keystone_balance()
Kit.check("两线拉满+同调位全解锁 → 地脉共鸣自动点亮(零币特化判定)", sv.meta_state.tree_state().nodes.get("easter_ley", 0) == 1)
var bal_now: int = sv.meta_state.keystone_balance()
mp.tree_buy("sp_hp")
Kit.check("觉醒零消费(钥石余额不动)", int(sv.keystone_balance()) == bal_now)
Kit.check("觉醒零消费(钥石余额不动)", int(sv.meta_state.keystone_balance()) == bal_now)
_meta_smoke_cleanup()
@@ -731,13 +730,11 @@ func _smoke_tree_rules() -> void:
# 财富之道·散:商品标价折价(level_runner 计价单点)
var lr = load("res://core/level_runner.gd").new(_db, _fresh_rng(777004),
{"tree_rules": []}) # 构造注入(拆件批 T3——setter 群退役)
Kit.check("散未激活:商品标价原价(无 economy_spend 键)",
int(lr.current_price({"base_price": 100, "price_increment": 0})) == 100 + int(_db.shop_price_level_bonus(0)))
Kit.check("散未激活:商品标价原价(无 economy_spend 键)", int(lr._spend_discounted(100)) == 100)
lr = load("res://core/level_runner.gd").new(_db, _fresh_rng(777005),
{"tree_rules": ["economy_spend"]}) # 构造注入(拆件批 T3——setter 群退役)
Kit.check("散激活:商品购买价 ×80%(provisional 折率——floor 偏向玩家、下限 1;刷新价不折=另一计价点零触碰)",
int(lr.current_price({"base_price": 100, "price_increment": 0})) == maxi(1, int(floor(float(100 + int(_db.shop_price_level_bonus(0))) * float(_db.shop.spend_discount))))
and int(lr.current_price({"base_price": 1, "price_increment": 0})) == maxi(1, int(floor(float(1 + int(_db.shop_price_level_bonus(0))) * float(_db.shop.spend_discount)))))
int(lr._spend_discounted(100)) == 80 and int(lr._spend_discounted(1)) == 1)
# 财富之道·聚:战斗掉落乘算·入账前(编排 _roll_gp_drop——同种子配对对拍 1.5×)
var orch3 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777005))
Kit.check("聚 rig:基线编排启动(无增益侧)",
@@ -35,7 +35,7 @@ func _init() -> void:
func _params(L: int) -> Dictionary:
return _db.level_gen_params_row(L)
return _db.run.level_gen_params.row_by_level_index(L)
func _graph(L: int, seed_val: int) -> Dictionary:
@@ -310,6 +310,7 @@ func _test_rng_single_point() -> void:
svc.new_adventure(4242)
var before: int = svc.counter()
var g = GenScript.new(svc).generate(_params(2), 42)
Kit.check("pure_stream 不推全局计数器(产图前后 counter 同值)〔fv10〕", svc.counter() == before)
var a = svc.pure_stream(42, 0)
var b = svc.pure_stream(42, 0)
Kit.check("pure_stream 同 (seed,scope) 同流〔fv10〕", a.randf() == b.randf() and a.randi() == b.randi())
+3 -3
View File
@@ -28,7 +28,7 @@ func _init() -> void:
func _reading(gen, db, L: int) -> Dictionary:
var g: Dictionary = gen.generate(db.level_gen_params_row(L), 42)
var g: Dictionary = gen.generate(db.run.level_gen_params.row_by_level_index(L), 42)
var maxd := 0
for n in g["nodes"]:
maxd = maxi(maxd, int(n["depth"]))
@@ -62,8 +62,8 @@ func _test_growth(gen, db) -> void:
func _test_fail_fast_static(gen, db) -> void:
Kit.check("生产三档参数档可行性判定零错误〔fv4〕",
NodeMapGenerator.check_params(db.level_gen_params_row(1)).is_empty()
and NodeMapGenerator.check_params(db.level_gen_params_row(3)).is_empty())
NodeMapGenerator.check_params(db.run.level_gen_params.row_by_level_index(1)).is_empty()
and NodeMapGenerator.check_params(db.run.level_gen_params.row_by_level_index(3)).is_empty())
var unsolvable := {"layers": 6, "branch_density": 0.0, "battle_ratio": 0.5, "reward_ratio": 0.2,
"rest_ratio": 0.2, "shop_ratio": 0.1, "entry_count": 1, "exit_count": 3, "battle_floor": 4,
"fork_ways_min": 2, "fork_ways_max": 3}
+8 -8
View File
@@ -36,7 +36,7 @@ func _init() -> void:
var e = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
e.start_adventure(501)
e.stop()._pending = {"candidates": []}
Kit.check_quiet(e.stop().skip_empty_curio() and (e.stop().breakpoint_fragment().get("bonus_pending", {}) as Dictionary).is_empty(),
Kit.check_quiet(e.stop().skip_empty_curio() and e.stop()._pending.is_empty(),
"空配置闸:三选一池空=停点整体跳过(skip_empty_curio 消化)〔专长批 T10〕")
e.stop()._pending = {"candidates": [{"kind": "gp", "tier": "common"}]}
Kit.check_quiet(not e.stop().skip_empty_curio(),
@@ -78,7 +78,7 @@ func check_v7(a, db) -> void:
continue
var hi := 0
for t in cands:
if db.adventure.tier_rank(String(t.get("tier", ""))) >= db.adventure.tier_rank(db.adventure.TIER_EXCELLENT):
if db.run.adventure.tier_rank(String(t.get("tier", ""))) >= db.run.adventure.tier_rank(db.run.adventure.TIER_EXCELLENT):
hi += 1
if hi >= 1:
hi_seen = true
@@ -161,21 +161,21 @@ func check_v8(c, db) -> void:
Kit.check_quiet(false, "V8 前置:冒险启动")
return
# 概率位与 cap 两属性独立:改 cap 不动牌库(概率位)、改牌库不动 cap 字段
var cap_field_before := int(db.adventure.curio_cap_gp)
db.adventure.curio_cap_gp = 1 # 独立设 cap(概率位=gp 令牌数不动)
var cap_field_before := int(db.run.adventure.cap_segment.curio_cap_gp)
db.run.adventure.cap_segment.curio_cap_gp = 1 # 独立设 cap(概率位=gp 令牌数不动)
var gp_tokens: int = c.deck().pool_stock("gp")
Kit.check_quiet(gp_tokens > 0 and int(db.adventure.curio_value("gp", "common")) == 20,
Kit.check_quiet(gp_tokens > 0 and int(db.run.adventure.curio_value("gp", "common")) == 20,
"改 cap 不动概率位:牌库 gp 令牌 %d 枚在库、件值字段不受影响〔专长批 V8〕" % gp_tokens)
# cap 只在拿了才计:刷出不计
c._curio.deck = [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common")]
for i in 20:
c.deck().draw_candidates("reward_node")
Kit.check_quiet(bool(c.deck().cap_ok("gp", "")) and int(c.deck().taken.get("gp", 0)) == 0,
Kit.check_quiet(bool(c._curio.cap_ok("gp", "")) and int(c._curio.taken.get("gp", 0)) == 0,
"cap 刷出不计:20 次抽签后 gp 未拿、cap 未触发〔专长批 V8〕")
# 拿了才计:pick 一次→cap 满→提前退役(候选视图剔除、牌库本体不动)
c.stop()._pending = {"candidates": [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common")]}
Kit.check_quiet(c.stop().pick_curio("gp", "common"), "V8 前置:兑付 gp 一枚")
Kit.check_quiet(not bool(c.deck().cap_ok("gp", "")), "cap 拿了才计:兑付后 cap 满(taken 1/1)〔专长批 V8〕")
Kit.check_quiet(not bool(c._curio.cap_ok("gp", "")), "cap 拿了才计:兑付后 cap 满(taken 1/1)〔专长批 V8〕")
var gp_in_deck := 0
for t in c._curio.deck:
if String((t as Dictionary).get("kind", "")) == "gp":
@@ -187,4 +187,4 @@ func check_v8(c, db) -> void:
gp_cand = true
Kit.check_quiet(gp_in_deck >= 0 and not gp_cand,
"提前退役:cap 满后候选视图零 gp(牌库本体张数不动=%d——概率位独立存活)〔专长批 V8〕" % gp_in_deck)
db.adventure.curio_cap_gp = cap_field_before # 还原表(同进程后续套件防御)
db.run.adventure.cap_segment.curio_cap_gp = cap_field_before # 还原表(同进程后续套件防御)
@@ -23,14 +23,14 @@ func _init() -> void:
# ① 无参实例(autoload 实例化形态)=无激活态+默认合法空态(sv2 类级)
func _test_no_arg_instance_has_no_activation() -> void:
var sm := SaveManagerScript.new()
Kit.check_quiet(sm.active_slot() == 0 and not sm.has_write_target(),
Kit.check_quiet(sm._path == "" and sm.slots.active_slot() == 0 and not sm.slots.has_write_target(),
"sv2 无参实例无激活态(路径空/激活号 0/写盘目标空)〔卡① sv2〕")
Kit.check_quiet(sm.keystone_balance() == 0 and not sm.has_breakpoint()
and sm.adventure_total() == 0 and sm.adventure_cleared() == 0,
Kit.check_quiet(sm.meta_state.keystone_balance() == 0 and not sm.has_breakpoint()
and sm.meta_state.career_state().adventure_total == 0 and sm.meta_state.career_state().adventure_cleared == 0,
"sv2 内存=默认合法空态(钥石/断点/总局数/冒险成功数全零值——访问器 fail-safe)〔卡① sv2〕")
# attach_path 注入任意隔离路径=注入即激活(契约 7 单路径语义——隔离面行为零变化)
sm.attach_path("user://smoke_boot_noact_iso.json")
Kit.check_quiet(sm.has_write_target() and sm.keystone_balance() == 0,
sm.slots.attach_path("user://smoke_boot_noact_iso.json")
Kit.check_quiet(sm.slots.has_write_target() and sm.meta_state.keystone_balance() == 0,
"sv2 attach_path 注入即激活(写盘目标在位、数据回默认空态)〔卡① sv2/契约7〕")
if FileAccess.file_exists("user://smoke_boot_noact_iso.json"):
DirAccess.remove_absolute("user://smoke_boot_noact_iso.json")
@@ -43,7 +43,7 @@ func _test_write_rejected_without_target() -> void:
var sm := SaveManagerScript.new()
var before := {}
for i in range(1, 4):
var p: String = sm.slot_path(i)
var p: String = sm.slots.slot_path(i)
before[i] = [FileAccess.file_exists(p),
FileAccess.get_file_as_bytes(p) if FileAccess.file_exists(p) else PackedByteArray(),
FileAccess.get_modified_time(p) if FileAccess.file_exists(p) else 0]
@@ -55,7 +55,7 @@ func _test_write_rejected_without_target() -> void:
"sv3 无激活写盘拒绝(write_to_disk=false)〔卡① sv3〕")
var unchanged := true
for i in range(1, 4):
var p: String = sm.slot_path(i)
var p: String = sm.slots.slot_path(i)
var now_state: Array = [FileAccess.file_exists(p),
FileAccess.get_file_as_bytes(p) if FileAccess.file_exists(p) else PackedByteArray(),
FileAccess.get_modified_time(p) if FileAccess.file_exists(p) else 0]
@@ -88,11 +88,11 @@ func _test_settle_chain_write_rejected() -> void:
for sp in ["res://.tmp", "res://.tmp.tmp"]:
if FileAccess.file_exists(sp):
DirAccess.remove_absolute(sp)
Kit.check_quiet(not sm.has_write_target() and not sm.write_to_disk(),
Kit.check_quiet(not sm.slots.has_write_target() and not sm.write_to_disk(),
"sv3 链前置:无激活态在位且直写被拒〔卡① sv3〕")
meta.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
"stopped_at": 2, "keystone_total": 0, "master_seed": 3})
Kit.check_quiet(not sm.has_write_target() and sm.adventure_total() == 1,
Kit.check_quiet(not sm.slots.has_write_target() and sm.meta_state.career_state().adventure_total == 1,
"sv3 生产链:settle_adventure 落账内存(adventure_total=1)而写盘仍无目标(被拒不落盘)〔卡① sv3〕")
Kit.check_quiet(not FileAccess.file_exists("res://.tmp")
and not FileAccess.file_exists("res://.tmp.tmp"),
@@ -102,11 +102,11 @@ func _test_settle_chain_write_rejected() -> void:
# ③ 越界槽号三原语拒绝+窥视越界返回不可读(原语边界 fail-safe)
func _test_out_of_range_slots_rejected() -> void:
var sm := SaveManagerScript.new()
sm.attach_slot_template("user://smoke_boot_noact_%d.json")
Kit.check_quiet(not sm.activate_slot(0) and not sm.activate_slot(4)
and not sm.create_slot(0) and not sm.create_slot(4)
and not sm.delete_slot(0) and not sm.delete_slot(4),
sm.slots.attach_slot_template("user://smoke_boot_noact_%d.json")
Kit.check_quiet(not sm.slots.activate_slot(0) and not sm.slots.activate_slot(4)
and not sm.slots.create_slot(0) and not sm.slots.create_slot(4)
and not sm.slots.delete_slot(0) and not sm.slots.delete_slot(4),
"原语边界:槽号 0/4 全拒绝(位数写死 3——契约 1)〔卡① sv1〕")
var out: Dictionary = sm.peek_slot_summary(4)
var out: Dictionary = sm.slots.peek_slot_summary(4)
Kit.check_quiet(not bool(out.get("readable", true)),
"原语边界:越界窥视返回不可读标记〔卡① sv7〕")
@@ -67,55 +67,55 @@ func _smoke_save() -> void:
# ① 往返:全访问器写非默认值 → write → 新实例 load → 逐域逐值相等
var w = sm_script.new(paths[0])
w.add_keystone(7)
w.set_tree_level("sp_hp", 2)
w.set_tree_level("sp_hit", 1)
w.set_fork_side("fg_posture", "b") # 树域写入
w.set_forge_route_level("spear", 3) # 锻造工坊域写入(等级+直购解锁+选中标记)
w.add_forge_unlock("spear")
w.set_forge_weapon("spear")
w.set_forge_armor("ward")
w.unlock_item("probe_compass")
w.apply_adventure_summary({"result": "failed", "cleared": false, "levels_cleared": 2,
w.meta_state._add_keystone(7)
w.meta_state.set_tree_level("sp_hp", 2)
w.meta_state.set_tree_level("sp_hit", 1)
w.meta_state.set_fork_side("fg_posture", "b") # 树域写入
w.meta_state.set_forge_route_level("spear", 3) # 锻造工坊域写入(等级+直购解锁+选中标记)
w.meta_state.add_forge_unlock("spear")
w.meta_state.set_forge_weapon("spear")
w.meta_state.set_forge_armor("ward")
w.meta_state.unlock_item("probe_compass")
w.meta_state.apply_adventure_summary({"result": "failed", "cleared": false, "levels_cleared": 2,
"stopped_at": 3, "keystone_total": 3, "master_seed": 99})
Kit.check("apply 原语入账(钥石 7+3、关卡完成次数 1/1/0)",
w.keystone_balance() == 10 and w.clear_count(1) == 1 and w.clear_count(2) == 1 and w.clear_count(3) == 0)
w.meta_state.keystone_balance() == 10 and w.meta_state.career_state().clear_counts.get(str(1), 0) == 1 and w.meta_state.career_state().clear_counts.get(str(2), 0) == 1 and w.meta_state.career_state().clear_counts.get(str(3), 0) == 0)
Kit.check("首杀标记随关卡完成置位(1/2 有、3 无)",
w.boss_first_kill_marked(1) and w.boss_first_kill_marked(2) and not w.boss_first_kill_marked(3))
w.meta_state.career_state().boss_first_kill.has(str(1)) and w.meta_state.career_state().boss_first_kill.has(str(2)) and not w.meta_state.career_state().boss_first_kill.has(str(3)))
Kit.check("生涯终身计数随 apply 自增恰一(中止局:总局数 1、冒险成功局数 0——迭代 18 卡④)",
w.adventure_total() == 1 and w.adventure_cleared() == 0)
w.meta_state.career_state().adventure_total == 1 and w.meta_state.career_state().adventure_cleared == 0)
Kit.check("写盘成功", w.write_to_disk())
var r = sm_script.new(paths[0])
Kit.check("读回成功", r.load_from_disk())
Kit.check("钥石余额往返相等(10)", r.keystone_balance() == 10)
Kit.check("钥石余额往返相等(10)", r.meta_state.keystone_balance() == 10)
Kit.check("树域往返相等(tree_nodes/fork_sides——迭代 21 换代承载)",
r.tree_level("sp_hp") == 2 and r.tree_level("sp_hit") == 1 and r.fork_side("fg_posture") == "b")
r.meta_state.tree_state().nodes.get("sp_hp", 0) == 2 and r.meta_state.tree_state().nodes.get("sp_hit", 0) == 1 and r.meta_state.tree_state().fork_sides.get("fg_posture", "") == "b")
Kit.check("锻造工坊域往返相等(levels/unlocked/weapon/armor——迭代 23 T5)〔锻造工坊批 V8〕",
r.forge_route_level("spear") == 3 and r.forge_route_level("sword") == 0
and r.forge_unlocked_purchases() == ["spear"]
and r.forge_weapon() == "spear" and r.forge_armor() == "ward")
Kit.check("道具解锁往返相等", r.item_unlocked("probe_compass") and not r.item_unlocked("rope"))
Kit.check("关卡完成次数表往返相等", r.clear_count(1) == 1 and r.clear_count(2) == 1 and r.clear_count(3) == 0)
r.meta_state.forge_state().levels.get("spear", 0) == 3 and r.meta_state.forge_state().levels.get("sword", 0) == 0
and r.meta_state.forge_state().unlocked == ["spear"]
and r.meta_state.forge_state().weapon == "spear" and r.meta_state.forge_state().armor == "ward")
Kit.check("道具解锁往返相等", r.meta_state.item_unlocked("probe_compass") and not r.meta_state.item_unlocked("rope"))
Kit.check("关卡完成次数表往返相等", r.meta_state.career_state().clear_counts.get(str(1), 0) == 1 and r.meta_state.career_state().clear_counts.get(str(2), 0) == 1 and r.meta_state.career_state().clear_counts.get(str(3), 0) == 0)
Kit.check("首杀标记往返相等",
r.boss_first_kill_marked(1) and r.boss_first_kill_marked(2) and not r.boss_first_kill_marked(3))
Kit.check("生涯终身计数往返相等(1/0)", r.adventure_total() == 1 and r.adventure_cleared() == 0)
var s0: Dictionary = r.recent_summaries()[0]
r.meta_state.career_state().boss_first_kill.has(str(1)) and r.meta_state.career_state().boss_first_kill.has(str(2)) and not r.meta_state.career_state().boss_first_kill.has(str(3)))
Kit.check("生涯终身计数往返相等(1/0)", r.meta_state.career_state().adventure_total == 1 and r.meta_state.career_state().adventure_cleared == 0)
var s0: Dictionary = r.meta_state.career_state().recent_summaries[0]
Kit.check("冒险摘要往返相等(1 条,seed 99)",
r.recent_summaries().size() == 1 and int(s0.get("master_seed", -1)) == 99 and int(s0.get("keystone_total", -1)) == 3)
r.meta_state.career_state().recent_summaries.size() == 1 and int(s0.get("master_seed", -1)) == 99 and int(s0.get("keystone_total", -1)) == 3)
# ② 新档合法:不存在路径 load → true + 全零默认值
var f2 = sm_script.new(paths[1])
Kit.check("新档合法态(load true + 全零默认)",
f2.load_from_disk() and f2.keystone_balance() == 0 and f2.tree_level("sp_hp") == 0
and not f2.item_unlocked("anything") and f2.clear_count(1) == 0 and f2.recent_summaries().is_empty())
f2.load_from_disk() and f2.meta_state.keystone_balance() == 0 and f2.meta_state.tree_state().nodes.get("sp_hp", 0) == 0
and not f2.meta_state.item_unlocked("anything") and f2.meta_state.career_state().clear_counts.get(str(1), 0) == 0 and f2.meta_state.career_state().recent_summaries.is_empty())
Kit.check("新档生涯终身计数缺省全零(迭代 18 卡④两新域——白板存档完整可玩)",
f2.adventure_total() == 0 and f2.adventure_cleared() == 0)
f2.meta_state.career_state().adventure_total == 0 and f2.meta_state.career_state().adventure_cleared == 0)
# ③ spend 拒绝:余额不足 false 且零副作用(钥石只增不减——消费除外)
var f3 = sm_script.new(paths[2])
f3.add_keystone(5)
Kit.check("余额 5 花 8 拒绝且余额不动", not f3.spend_keystone(8) and f3.keystone_balance() == 5)
Kit.check("余额内消费成功(5→2)", f3.spend_keystone(3) and f3.keystone_balance() == 2)
f3.meta_state._add_keystone(5)
Kit.check("余额 5 花 8 拒绝且余额不动", not f3.meta_state.spend_keystone(8) and f3.meta_state.keystone_balance() == 5)
Kit.check("余额内消费成功(5→2)", f3.meta_state.spend_keystone(3) and f3.meta_state.keystone_balance() == 2)
# ⑤ 高版本拒读:load false、原文件字节不动、后续写盘拒绝且文件仍字节不动(契约 4)
var hv_text := "{\"version\": 99, \"meta\": {\"keystone_balance\": 1}}"
@@ -135,7 +135,7 @@ func _smoke_save() -> void:
fj.close()
var f6 = sm_script.new(paths[4])
Kit.check("损坏档兜底(默认数据继续)",
f6.load_from_disk() and f6.keystone_balance() == 0 and f6.recent_summaries().is_empty())
f6.load_from_disk() and f6.meta_state.keystone_balance() == 0 and f6.meta_state.career_state().recent_summaries.is_empty())
Kit.check("坏档备份 .corrupt.bak 存在", FileAccess.file_exists(paths[4] + ".corrupt.bak"))
Kit.check("备份内容 = 原坏档字节",
FileAccess.open(paths[4] + ".corrupt.bak", FileAccess.READ).get_as_text() == junk)
@@ -149,7 +149,7 @@ func _smoke_save() -> void:
fm.store_string(JSON.stringify({"version": 0, "keystone_old": 12}))
fm.close()
Kit.check("v0 旧档经迁移链入账(keystone_old 12 → 余额 12)",
f7.load_from_disk() and f7.keystone_balance() == 12)
f7.load_from_disk() and f7.meta_state.keystone_balance() == 12)
Kit.check("迁移链 0→1(测试钩)→7(生产) 写盘 version == 7(v7=锻造工坊域,迭代 23 T5——v6=生涯终身计数域迭代 18 卡④)",
f7.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[5], FileAccess.READ).get_as_text()).get("version", -1)) == 7)
@@ -167,9 +167,9 @@ func _smoke_save() -> void:
# ⑩ 摘要 cap:12 份 → 保留 10、最新在前
var f10 = sm_script.new(paths[7])
for i in range(12):
f10.apply_adventure_summary({"result": "completed", "cleared": true, "levels_cleared": 0,
f10.meta_state.apply_adventure_summary({"result": "completed", "cleared": true, "levels_cleared": 0,
"stopped_at": 0, "keystone_total": 0, "master_seed": i})
var arr: Array = f10.recent_summaries()
var arr: Array = f10.meta_state.career_state().recent_summaries
Kit.check("摘要 cap 10(12 份 → 10 条)", arr.size() == 10)
Kit.check("最新在前(头 seed 11 / 尾 seed 2)",
int(arr[0].get("master_seed", -1)) == 11 and int(arr[9].get("master_seed", -1)) == 2)
@@ -227,7 +227,7 @@ func _smoke_save() -> void:
fold.store_string("{\"version\": 1, \"meta\": {\"keystone_balance\": 4}}")
fold.close()
Kit.check("v1 旧档迁移 v7:断点域补空(合法空态,无断点域不炸)",
fo.load_from_disk() and fo.keystone_balance() == 4 and not fo.has_breakpoint())
fo.load_from_disk() and fo.meta_state.keystone_balance() == 4 and not fo.has_breakpoint())
Kit.check("v1 旧档迁移后写盘 version == 7(v1→v7 链式——锻造工坊域 2026-10-12)",
fo.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[9], FileAccess.READ).get_as_text()).get("version", -1)) == 7)
@@ -241,7 +241,7 @@ func _smoke_save() -> void:
"visited_ids": []}}))
fv2f.close()
Kit.check("v2 旧档迁移 v3:断点域与 meta 域原样保留(缺新附随件=合法空态)",
fv2.load_from_disk() and fv2.has_breakpoint() and fv2.keystone_balance() == 7)
fv2.load_from_disk() and fv2.has_breakpoint() and fv2.meta_state.keystone_balance() == 7)
Kit.check("v2 旧断点字段零丢失(向后兼容——breakpoint 内容不因 schema 升版重写)",
int((fv2.breakpoint_snapshot() as Dictionary).get("master_seed", 0)) == 777)
var adr_v2 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(777))
@@ -255,22 +255,22 @@ func _smoke_save() -> void:
fv5u.store_string(JSON.stringify({"version": 5, "meta": {"keystone_balance": 9}}))
fv5u.close()
Kit.check("v5 旧档升 v7:计数域缺省补零且既有余额原样(迁移只增字段不改值——v5→v6→v7 链式)",
f13.load_from_disk() and f13.adventure_total() == 0 and f13.adventure_cleared() == 0
and f13.keystone_balance() == 9)
f13.load_from_disk() and f13.meta_state.career_state().adventure_total == 0 and f13.meta_state.career_state().adventure_cleared == 0
and f13.meta_state.keystone_balance() == 9)
Kit.check("升档幂等:v7 档写盘再载零漂移(重复迁移零副作用——锻造工坊域同批补齐)",
f13.write_to_disk() and f13.load_from_disk()
and int(JSON.parse_string(FileAccess.open(paths[11], FileAccess.READ).get_as_text()).get("version", -1)) == 7
and f13.adventure_total() == 0 and f13.adventure_cleared() == 0)
and f13.meta_state.career_state().adventure_total == 0 and f13.meta_state.career_state().adventure_cleared == 0)
var f13b = sm_script.new(paths[12])
f13b.load_from_disk()
f13b.apply_adventure_summary({"result": "completed", "cleared": true, "levels_cleared": 3,
f13b.meta_state.apply_adventure_summary({"result": "completed", "cleared": true, "levels_cleared": 3,
"stopped_at": 0, "keystone_total": 0, "master_seed": 1})
Kit.check("收束自增恰一·冒险成功局(三关全通 cleared=true:总局数+1 且冒险成功局数+1)",
f13b.adventure_total() == 1 and f13b.adventure_cleared() == 1)
f13b.apply_adventure_summary({"result": "abandoned", "cleared": false, "levels_cleared": 1,
f13b.meta_state.career_state().adventure_total == 1 and f13b.meta_state.career_state().adventure_cleared == 1)
f13b.meta_state.apply_adventure_summary({"result": "abandoned", "cleared": false, "levels_cleared": 1,
"stopped_at": 2, "keystone_total": 0, "master_seed": 2})
Kit.check("收束自增恰一·中止局(放弃局:仅总局数+1,冒险成功局数不动)",
f13b.adventure_total() == 2 and f13b.adventure_cleared() == 1)
f13b.meta_state.career_state().adventure_total == 2 and f13b.meta_state.career_state().adventure_cleared == 1)
# ⑮ 旧档破坏性废弃(契约 4:换代前结构检测=树域废弃重置,禁迁移禁折算)〔V16〕
var f15 = sm_script.new(paths[13])
@@ -281,12 +281,12 @@ func _smoke_save() -> void:
"fork_sides": {"fg_posture": "a", "fg_mastery": "b"}}}))
fv16.close()
Kit.check("换代前结构存档:树域废弃重置(旧节点 id/fork 侧/forge_levels 旧域残留清退)、钥石余额零折算零变动",
f15.load_from_disk() and f15.keystone_balance() == 33
and f15.tree_nodes().is_empty() and f15.fork_sides().is_empty())
f15.load_from_disk() and f15.meta_state.keystone_balance() == 33
and f15.meta_state.tree_state().nodes.is_empty() and f15.meta_state.tree_state().fork_sides.is_empty())
var mp16 = load("res://core/meta_progression.gd").new(_db, f15)
f15.add_keystone(50)
f15.meta_state._add_keystone(50)
Kit.check("废弃重置后后续购买正常受理(无残留脏节点)",
mp16.tree_buy("sp_hp") and f15.tree_level("sp_hp") == 1)
mp16.tree_buy("sp_hp") and f15.meta_state.tree_state().nodes.get("sp_hp", 0) == 1)
# ⑯ 锻造工坊域 forge_routes(V8):v6 旧档缺键补齐=初始持有态+无特效域+域界干净
var f16 = sm_script.new(paths[14])
@@ -295,26 +295,26 @@ func _smoke_save() -> void:
"tree_nodes": {"sp_hp": 1}, "tree_struct": 2, "adventure_total": 4, "adventure_cleared": 1}}))
fv6.close()
Kit.check("v6 旧档升 v7:既有域逐位不动(钥石/树/生涯计数零折算——迁移只增不改)〔锻造工坊批 V8〕",
f16.load_from_disk() and f16.keystone_balance() == 21
and f16.tree_level("sp_hp") == 1 and f16.adventure_total() == 4 and f16.adventure_cleared() == 1)
f16.load_from_disk() and f16.meta_state.keystone_balance() == 21
and f16.meta_state.tree_state().nodes.get("sp_hp", 0) == 1 and f16.meta_state.career_state().adventure_total == 4 and f16.meta_state.career_state().adventure_cleared == 1)
Kit.check("v6 旧档缺键补齐=初始持有态(六路线 0 级/直购清单空/选中=sword+light)〔锻造工坊批 V8〕",
f16.forge_route_level("sword") == 0 and f16.forge_route_level("spear") == 0
and f16.forge_route_level("light") == 0 and f16.forge_route_level("ward") == 0
and f16.forge_unlocked_purchases().is_empty()
and f16.forge_weapon() == "sword" and f16.forge_armor() == "light")
var fr_keys: Array = (f16.forge_routes_snapshot() as Dictionary).keys()
f16.meta_state.forge_state().levels.get("sword", 0) == 0 and f16.meta_state.forge_state().levels.get("spear", 0) == 0
and f16.meta_state.forge_state().levels.get("light", 0) == 0 and f16.meta_state.forge_state().levels.get("ward", 0) == 0
and f16.meta_state.forge_state().unlocked.is_empty()
and f16.meta_state.forge_state().weapon == "sword" and f16.meta_state.forge_state().armor == "light")
var fr_keys: Array = (f16.meta_state.forge_state() as Dictionary).keys()
fr_keys.sort()
Kit.check("forge_routes 域键集恰四项(无特效域——满级特效本期不做,契约 7)〔锻造工坊批 V8/V11〕",
fr_keys == ["armor", "levels", "unlocked", "weapon"])
var lv_keys: Array = ((f16.forge_routes_snapshot() as Dictionary).get("levels", {}) as Dictionary).keys()
var lv_keys: Array = ((f16.meta_state.forge_state() as Dictionary).get("levels", {}) as Dictionary).keys()
lv_keys.sort()
Kit.check("levels 六路线键集恰全(sword/spear/staff/light/heavy/ward)〔锻造工坊批 V8〕",
lv_keys == ["heavy", "light", "spear", "staff", "sword", "ward"])
# 值拷贝出户(改快照不渗档——本类契约)
var snap: Dictionary = f16.forge_routes_snapshot()
var snap: Dictionary = f16.meta_state.forge_state()
(snap.get("levels", {}) as Dictionary)["sword"] = 99
Kit.check("forge_routes_snapshot 值拷贝出户(改快照不渗档)〔锻造工坊批 V8〕",
f16.forge_route_level("sword") == 0)
f16.meta_state.forge_state().levels.get("sword", 0) == 0)
# ⑭ 收束链入账(真实收束挂点全链恰一:主流程 adventure_finished →
# MetaProgression.settle_adventure → apply_adventure_summary 单口;两夹具经同链入账)
@@ -322,11 +322,11 @@ func _smoke_save() -> void:
mp13.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 2,
"stopped_at": 3, "keystone_total": 0, "master_seed": 3})
Kit.check("收束链自增·中止局(settle_adventure 真实挂点:总局数+1、冒险成功局数不动)",
f13b.adventure_total() == 3 and f13b.adventure_cleared() == 1)
f13b.meta_state.career_state().adventure_total == 3 and f13b.meta_state.career_state().adventure_cleared == 1)
mp13.settle_adventure({"result": "completed", "cleared": true, "levels_cleared": 3,
"stopped_at": 0, "keystone_total": 0, "master_seed": 4})
Kit.check("收束链自增·冒险成功局(三关全通:总局数+1 且冒险成功局数+1——全链恰一)",
f13b.adventure_total() == 4 and f13b.adventure_cleared() == 2)
f13b.meta_state.career_state().adventure_total == 4 and f13b.meta_state.career_state().adventure_cleared == 2)
# 段尾清理(测试文件绝不残留)
for p in stale:
@@ -352,39 +352,39 @@ func _smoke_save_slots() -> void:
DirAccess.remove_absolute(p)
var t = sm_script.new()
t.attach_slot_template(slot_tpl)
t.slots.attach_slot_template(slot_tpl)
# ① 三槽往返×3+位间隔离+进档(sv1/sv5/sv10)
for i in range(1, 4):
Kit.check("sv5 进档受理·槽 %d(激活+读盘)〔卡① sv5〕" % i, t.activate_slot(i))
t.apply_adventure_summary({"result": "completed", "cleared": i == 1, "levels_cleared": i,
Kit.check("sv5 进档受理·槽 %d(激活+读盘)〔卡① sv5〕" % i, t.slots.activate_slot(i))
t.meta_state.apply_adventure_summary({"result": "completed", "cleared": i == 1, "levels_cleared": i,
"stopped_at": 0, "keystone_total": 100 * i, "master_seed": i})
Kit.check("sv3 激活态写盘受理·槽 %d〔卡① sv1〕" % i, t.write_to_disk())
var t2 = sm_script.new()
t2.attach_slot_template(slot_tpl)
t2.slots.attach_slot_template(slot_tpl)
for i in range(1, 4):
Kit.check("sv1 三槽往返一致·槽 %d(异值按位读回)〔卡① sv1〕" % i,
t2.activate_slot(i) and t2.keystone_balance() == 100 * i
and t2.adventure_total() == 1 and t2.adventure_cleared() == (1 if i == 1 else 0))
t2.slots.activate_slot(i) and t2.meta_state.keystone_balance() == 100 * i
and t2.meta_state.career_state().adventure_total == 1 and t2.meta_state.career_state().adventure_cleared == (1 if i == 1 else 0))
var slot2_before := FileAccess.get_file_as_bytes(slot_tpl % 2)
t.activate_slot(1)
t.add_keystone(7)
t.slots.activate_slot(1)
t.meta_state._add_keystone(7)
Kit.check("sv3 写盘受理·槽 1 加值后〔卡① sv1〕", t.write_to_disk())
Kit.check("sv1 位间隔离:改槽 1 槽 2 字节不动〔卡① sv1〕",
FileAccess.get_file_as_bytes(slot_tpl % 2) == slot2_before)
# ② 删档原语(sv6):删激活槽(指针连清除)→ 删非激活槽(指针保持+他槽字节不动)
Kit.check("sv5 切激活至槽 3(删激活槽夹具前置)〔卡① sv5〕", t.activate_slot(3))
Kit.check("sv6 删档受理·激活槽 3〔卡① sv6〕", t.delete_slot(3))
Kit.check("sv5 切激活至槽 3(删激活槽夹具前置)〔卡① sv5〕", t.slots.activate_slot(3))
Kit.check("sv6 删档受理·激活槽 3〔卡① sv6〕", t.slots.delete_slot(3))
Kit.check("sv6 删后文件不在盘+无 .tmp 残留〔卡① sv6〕",
not FileAccess.file_exists(slot_tpl % 3) and not FileAccess.file_exists(slot_tpl % 3 + ".tmp"))
Kit.check("sv6 删激活槽激活指针连清除(该槽不再可写)〔卡① sv6〕",
t.active_slot() == 0 and not t.has_write_target())
t.slots.active_slot() == 0 and not t.slots.has_write_target())
var slot1_before := FileAccess.get_file_as_bytes(slot_tpl % 1)
t.activate_slot(1)
Kit.check("sv6 删非激活槽受理·槽 2〔卡① sv6〕", t.delete_slot(2))
t.slots.activate_slot(1)
Kit.check("sv6 删非激活槽受理·槽 2〔卡① sv6〕", t.slots.delete_slot(2))
Kit.check("sv6 删非激活槽:文件不在盘+激活指针保持〔卡① sv6〕",
not FileAccess.file_exists(slot_tpl % 2) and t.active_slot() == 1)
not FileAccess.file_exists(slot_tpl % 2) and t.slots.active_slot() == 1)
Kit.check("sv6 删槽 2 槽 1 字节不动(位间互隔离)〔卡① sv6〕",
FileAccess.get_file_as_bytes(slot_tpl % 1) == slot1_before)
@@ -394,10 +394,10 @@ func _smoke_save_slots() -> void:
fold.store_string(JSON.stringify({"version": 7, "meta": {"keystone_balance": 999,
"adventure_total": 5, "adventure_cleared": 2, "tree_nodes": {}, "fork_sides": {}, "tree_struct": 2}}))
fold.close()
Kit.check("sv4 新档受理·槽 3(创建即激活)〔卡① sv4〕", t.create_slot(3))
Kit.check("sv4 新档受理·槽 3(创建即激活)〔卡① sv4〕", t.slots.create_slot(3))
Kit.check("sv4 新档=全零可玩合法白板态(旧内容 999/5 零带入——钥石/总局数/断点全零)〔卡① sv4〕",
t.active_slot() == 3 and t.keystone_balance() == 0 and not t.has_breakpoint()
and t.adventure_total() == 0)
t.slots.active_slot() == 3 and t.meta_state.keystone_balance() == 0 and not t.has_breakpoint()
and t.meta_state.career_state().adventure_total == 0)
var created: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(slot_tpl % 3))
Kit.check("sv4 新档落盘=白板内容(盘面核验非 999 旧值)〔卡① sv4〕",
int((created.get("meta", {}) as Dictionary).get("keystone_balance", -1)) == 0)
@@ -407,14 +407,14 @@ func _smoke_save_slots() -> void:
fhv.store_string(JSON.stringify({"version": 99, "meta": {}}))
fhv.close()
var hv = sm_script.new()
hv.attach_slot_template(slot_tpl)
hv.slots.attach_slot_template(slot_tpl)
Kit.check("sv5 进档槽 1 读回(107)〔卡① sv10〕",
hv.activate_slot(1) and hv.keystone_balance() == 107)
hv.slots.activate_slot(1) and hv.meta_state.keystone_balance() == 107)
var slot2_hv := FileAccess.get_file_as_bytes(slot_tpl % 2)
Kit.check("sv9 高版本槽进档拒绝(拒读禁写链按槽)〔卡① sv9〕", not hv.activate_slot(2))
Kit.check("sv9 高版本槽进档拒绝(拒读禁写链按槽)〔卡① sv9〕", not hv.slots.activate_slot(2))
Kit.check("sv10 激活失败恢复切换前三态(路径/激活号/数据归槽 1)〔卡① sv10〕",
hv.active_slot() == 1 and hv.keystone_balance() == 107)
hv.add_keystone(1)
hv.slots.active_slot() == 1 and hv.meta_state.keystone_balance() == 107)
hv.meta_state._add_keystone(1)
Kit.check("sv10 恢复后写盘落激活槽 1(高版本槽 2 零误写)〔卡① sv10〕",
hv.write_to_disk() and FileAccess.get_file_as_bytes(slot_tpl % 2) == slot2_hv)
@@ -425,14 +425,14 @@ func _smoke_save_slots() -> void:
fm.close()
for i in range(1, 4):
var mi = sm_script.new()
mi.attach_slot_template(slot_tpl)
mi.slots.attach_slot_template(slot_tpl)
Kit.check("sv9 迁移链按槽成立·槽 %d(v5→v7 各槽独立+写盘 version=7)〔卡① sv9〕" % i,
mi.activate_slot(i) and mi.keystone_balance() == i * 11
mi.slots.activate_slot(i) and mi.meta_state.keystone_balance() == i * 11
and mi.write_to_disk())
# ⑥ 窥视三态零副作用(sv7/sv8):异值三槽逐槽三项+空槽零值+坏/高版本字节 mtime 双比对
var pv = sm_script.new()
pv.attach_slot_template(slot_tpl)
pv.slots.attach_slot_template(slot_tpl)
for i in range(1, 4):
var fp := FileAccess.open(slot_tpl % i, FileAccess.WRITE)
fp.store_string(JSON.stringify({"version": 7, "meta": {"keystone_balance": 200 * i,
@@ -440,12 +440,12 @@ func _smoke_save_slots() -> void:
"tree_nodes": {}, "fork_sides": {}, "tree_struct": 2}}))
fp.close()
for i in range(1, 4):
var ps: Dictionary = pv.peek_slot_summary(i)
var ps: Dictionary = pv.slots.peek_slot_summary(i)
Kit.check("sv7 窥视三项逐槽正确·槽 %d〔卡① sv7〕" % i,
bool(ps.get("readable", false)) and int(ps.get("keystone", -1)) == 200 * i
and int(ps.get("cleared", -1)) == i and int(ps.get("total", -1)) == 5 * i)
Kit.check("sv6 删槽 3 备空槽窥视〔卡① sv6〕", pv.delete_slot(3))
var pe: Dictionary = pv.peek_slot_summary(3)
Kit.check("sv6 删槽 3 备空槽窥视〔卡① sv6〕", pv.slots.delete_slot(3))
var pe: Dictionary = pv.slots.peek_slot_summary(3)
Kit.check("sv7 空槽窥视=零值合法态〔卡① sv7〕",
bool(pe.get("readable", false)) and int(pe.get("keystone", -1)) == 0
and int(pe.get("cleared", -1)) == 0 and int(pe.get("total", -1)) == 0)
@@ -454,7 +454,7 @@ func _smoke_save_slots() -> void:
fbad.close()
var bad_bytes := FileAccess.get_file_as_bytes(slot_tpl % 1)
var bad_mtime := FileAccess.get_modified_time(slot_tpl % 1)
var pb: Dictionary = pv.peek_slot_summary(1)
var pb: Dictionary = pv.slots.peek_slot_summary(1)
Kit.check("sv8 坏槽窥视=显式损坏标记〔卡① sv8〕", not bool(pb.get("readable", true)))
Kit.check("sv8 坏槽窥视零副作用(字节+mtime 双比对不变+零 .corrupt.bak 改名)〔卡① sv8〕",
FileAccess.get_file_as_bytes(slot_tpl % 1) == bad_bytes
@@ -465,7 +465,7 @@ func _smoke_save_slots() -> void:
fhv2.close()
var hv_bytes := FileAccess.get_file_as_bytes(slot_tpl % 2)
var hv_mtime := FileAccess.get_modified_time(slot_tpl % 2)
var ph: Dictionary = pv.peek_slot_summary(2)
var ph: Dictionary = pv.slots.peek_slot_summary(2)
Kit.check("sv8 高版本槽窥视=不可读标记〔卡① sv8〕", not bool(ph.get("readable", true)))
Kit.check("sv8 高版本窥视零副作用(字节+mtime 不变+零改名)〔卡① sv8〕",
FileAccess.get_file_as_bytes(slot_tpl % 2) == hv_bytes
@@ -473,7 +473,7 @@ func _smoke_save_slots() -> void:
# ⑦ 旧单档迁移(sv12):字节搬运+源改名留痕+幂等+他槽零触碰
var ml = sm_script.new()
ml.attach_slot_template(slot_tpl)
ml.slots.attach_slot_template(slot_tpl)
var fsrc := FileAccess.open(legacy_src, FileAccess.WRITE)
fsrc.store_string(JSON.stringify({"version": 7, "meta": {"keystone_balance": 1234}}))
fsrc.close()
@@ -39,46 +39,46 @@ func _write_v7_keystone(path: String, keystone: int) -> void:
# ① 三槽往返:各槽写异值 → 写盘 → 新实例按位读回逐域相等;改槽 1 槽 2 字节不动(sv1)
func _test_three_slot_roundtrip_and_isolation() -> void:
var t := SaveManagerScript.new()
t.attach_slot_template(TPL)
t.slots.attach_slot_template(TPL)
for i in range(1, 4):
Kit.check_quiet(t.activate_slot(i), "sv5 进档受理·槽 %d〔卡① sv5〕" % i)
t.add_keystone(100 * i)
t.apply_adventure_summary({"result": "completed", "cleared": i == 1, "levels_cleared": i,
Kit.check_quiet(t.slots.activate_slot(i), "sv5 进档受理·槽 %d〔卡① sv5〕" % i)
t.meta_state._add_keystone(100 * i)
t.meta_state.apply_adventure_summary({"result": "completed", "cleared": i == 1, "levels_cleared": i,
"stopped_at": 0, "keystone_total": 0, "master_seed": i})
Kit.check_quiet(t.write_to_disk(), "sv3 激活态写盘受理·槽 %d〔卡① sv1〕" % i)
var t2 := SaveManagerScript.new()
t2.attach_slot_template(TPL)
t2.slots.attach_slot_template(TPL)
for i in range(1, 4):
Kit.check_quiet(t2.activate_slot(i) and t2.keystone_balance() == 100 * i
and t2.adventure_total() == 1 and t2.adventure_cleared() == (1 if i == 1 else 0),
Kit.check_quiet(t2.slots.activate_slot(i) and t2.meta_state.keystone_balance() == 100 * i
and t2.meta_state.career_state().adventure_total == 1 and t2.meta_state.career_state().adventure_cleared == (1 if i == 1 else 0),
"sv1 三槽往返一致·槽 %d(异值按位读回)〔卡① sv1〕" % i)
var slot2_before := FileAccess.get_file_as_bytes(TPL % 2)
t.activate_slot(1)
t.add_keystone(7)
t.slots.activate_slot(1)
t.meta_state._add_keystone(7)
Kit.check_quiet(t.write_to_disk(), "sv3 写盘受理·槽 1 加值后〔卡① sv1〕")
Kit.check_quiet(FileAccess.get_file_as_bytes(TPL % 2) == slot2_before,
"sv1 位间隔离:改槽 1 槽 2 字节不动〔卡① sv1〕")
# 激活号记账:activate 槽派生名后 active_slot 同步;attach_path 注入槽名亦然
Kit.check_quiet(t.active_slot() == 1 and t.has_write_target(),
Kit.check_quiet(t.slots.active_slot() == 1 and t.slots.has_write_target(),
"sv1 activate 后激活号同步(active_slot=1 写盘目标在位)〔卡① sv1〕")
# ② 高版本槽进档失败:拒绝+恢复切换前三态(路径/激活号/数据)+拒读槽零误写(sv9/sv10)
func _test_activate_failure_restores_previous_slot() -> void:
var t := SaveManagerScript.new()
t.attach_slot_template(TPL)
t.slots.attach_slot_template(TPL)
_write_v7_keystone(TPL % 1, 311)
_write_v7_keystone(TPL % 2, 422)
var fhv := FileAccess.open(TPL % 3, FileAccess.WRITE)
fhv.store_string(JSON.stringify({"version": 99, "meta": {}}))
fhv.close()
Kit.check_quiet(t.activate_slot(1) and t.keystone_balance() == 311,
Kit.check_quiet(t.slots.activate_slot(1) and t.meta_state.keystone_balance() == 311,
"sv5 进档槽 1 读回(311)〔卡① sv10〕")
var slot2_before := FileAccess.get_file_as_bytes(TPL % 2)
Kit.check_quiet(not t.activate_slot(3), "sv9 高版本槽进档拒绝(拒读链按槽)〔卡① sv9〕")
Kit.check_quiet(t.active_slot() == 1 and t.has_write_target() and t.keystone_balance() == 311,
Kit.check_quiet(not t.slots.activate_slot(3), "sv9 高版本槽进档拒绝(拒读链按槽)〔卡① sv9〕")
Kit.check_quiet(t.slots.active_slot() == 1 and t.slots.has_write_target() and t.meta_state.keystone_balance() == 311,
"sv10 激活失败恢复切换前三态(路径/激活号/数据归槽 1)〔卡① sv10〕")
t.add_keystone(1)
t.meta_state._add_keystone(1)
Kit.check_quiet(t.write_to_disk(), "sv3 恢复后写盘受理〔卡① sv10〕")
Kit.check_quiet(FileAccess.get_file_as_bytes(TPL % 2) == slot2_before,
"sv10 拒读槽零误写(写盘落激活槽 1,槽 2/3 字节不动)〔卡① sv10〕")
@@ -87,17 +87,17 @@ func _test_activate_failure_restores_previous_slot() -> void:
# ③ MetaProgression 宿主链切槽:引用同一性保持+写盘落当前槽(sv10——禁换实例先例)
func _test_metaprogression_reference_identity() -> void:
var t := SaveManagerScript.new()
t.attach_slot_template(TPL)
t.slots.attach_slot_template(TPL)
_write_v7_keystone(TPL % 1, 533)
_write_v7_keystone(TPL % 2, 644)
Kit.check_quiet(t.activate_slot(1), "sv5 进档槽 1〔卡① sv10〕")
Kit.check_quiet(t.slots.activate_slot(1), "sv5 进档槽 1〔卡① sv10〕")
var meta := MetaProgressionScript.new(null, t)
Kit.check_quiet(meta.save == t and meta.keystone_balance() == 533,
Kit.check_quiet(meta.save == t and meta.meta_state.keystone_balance() == 533,
"sv10 宿主链取数走动态分发(meta.save 同一实例、读槽 1)〔卡① sv10〕")
Kit.check_quiet(t.activate_slot(2) and meta.save == t and meta.keystone_balance() == 644,
Kit.check_quiet(t.slots.activate_slot(2) and meta.save == t and meta.meta_state.keystone_balance() == 644,
"sv10 切槽后引用同一性保持(零换实例)且读数随槽(644)〔卡① sv10〕")
meta.save.add_keystone(6)
Kit.check_quiet(t.write_to_disk() and t.keystone_balance() == 650,
meta.save.meta_state._add_keystone(6)
Kit.check_quiet(t.write_to_disk() and t.meta_state.keystone_balance() == 650,
"sv10 宿主链写盘落当前槽 2(650=644+6)〔卡① sv10〕")
Kit.check_quiet(int((JSON.parse_string(FileAccess.get_file_as_string(TPL % 2)) as Dictionary)\
.get("meta", {}).get("keystone_balance", -1)) == 650,
+14 -14
View File
@@ -37,20 +37,20 @@ func _write_slot_json(path: String, content: String) -> void:
# ① 三槽异值逐槽三项正确+不激活不写内存(sv7)
func _test_peek_three_slots_distinct_values() -> void:
var t := SaveManagerScript.new()
t.attach_slot_template(TPL)
t.slots.attach_slot_template(TPL)
for i in range(1, 4):
_write_slot_json(TPL % i, JSON.stringify({"version": 7, "meta": {
"keystone_balance": 200 * i, "adventure_total": 5 * i, "adventure_cleared": i,
"tree_nodes": {}, "fork_sides": {}, "tree_struct": 2}}))
for i in range(1, 4):
var ps: Dictionary = t.peek_slot_summary(i)
var ps: Dictionary = t.slots.peek_slot_summary(i)
Kit.check_quiet(bool(ps.get("readable", false)) and int(ps.get("keystone", -1)) == 200 * i
and int(ps.get("cleared", -1)) == i and int(ps.get("total", -1)) == 5 * i,
"sv7 窥视三项逐槽正确·槽 %d(钥石/冒险成功/总局)〔卡① sv7〕" % i)
Kit.check_quiet(t.active_slot() == 0 and not t.has_write_target() and t.keystone_balance() == 0,
Kit.check_quiet(t.slots.active_slot() == 0 and not t.slots.has_write_target() and t.meta_state.keystone_balance() == 0,
"sv7 窥视不激活不写内存(激活号 0/无写盘目标/内存余额零)〔卡① sv7〕")
# 路径注入口同语义(契约 7 注入面)
var pinj: Dictionary = t.peek_summary_at(TPL % 2)
var pinj: Dictionary = t.slots.peek_summary_at(TPL % 2)
Kit.check_quiet(bool(pinj.get("readable", false)) and int(pinj.get("keystone", -1)) == 400,
"sv7 peek_summary_at 注入口同语义(槽 2 钥石 400)〔卡① sv7〕")
@@ -58,32 +58,32 @@ func _test_peek_three_slots_distinct_values() -> void:
# ② 空槽零值合法态+不可读三态显式标记(sv7/契约 9)
func _test_peek_empty_and_unreadable_states() -> void:
var t := SaveManagerScript.new()
t.attach_slot_template(TPL)
t.slots.attach_slot_template(TPL)
DirAccess.remove_absolute(TPL % 3) # 清①段残留——空槽夹具
var pe: Dictionary = t.peek_slot_summary(3) # 槽 3 无文件=空槽
var pe: Dictionary = t.slots.peek_slot_summary(3) # 槽 3 无文件=空槽
Kit.check_quiet(bool(pe.get("readable", false)) and int(pe.get("keystone", -1)) == 0
and int(pe.get("cleared", -1)) == 0 and int(pe.get("total", -1)) == 0,
"sv7 空槽窥视=零值合法态(可读+三项全零)〔卡① sv7〕")
_write_slot_json(TPL % 1, "this is not json{{{")
Kit.check_quiet(not bool(t.peek_slot_summary(1).get("readable", true)),
Kit.check_quiet(not bool(t.slots.peek_slot_summary(1).get("readable", true)),
"sv7 坏槽窥视=显式损坏标记〔卡① sv7〕")
_write_slot_json(TPL % 2, JSON.stringify({"version": 99, "meta": {}}))
Kit.check_quiet(not bool(t.peek_slot_summary(2).get("readable", true)),
Kit.check_quiet(not bool(t.slots.peek_slot_summary(2).get("readable", true)),
"sv7 高版本槽窥视=不可读标记〔卡① sv7〕")
_write_slot_json(TPL % 3, JSON.stringify({"meta": {"keystone_balance": 5}})) # 缺版本字段
Kit.check_quiet(not bool(t.peek_slot_summary(3).get("readable", true)),
Kit.check_quiet(not bool(t.slots.peek_slot_summary(3).get("readable", true)),
"sv7 缺版本字段槽窥视=不可读标记〔卡① sv7/契约9〕")
# ③ 窥视零副作用:字节+mtime 双比对不变+零 .corrupt.bak 改名(sv8——真实入口冒烟段同判据)
func _test_peek_zero_side_effect() -> void:
var t := SaveManagerScript.new()
t.attach_slot_template(TPL)
t.slots.attach_slot_template(TPL)
_write_slot_json(TPL % 1, "corrupt bytes {{{{")
var bytes_before := FileAccess.get_file_as_bytes(TPL % 1)
var mtime_before := FileAccess.get_modified_time(TPL % 1)
t.peek_slot_summary(1)
t.peek_summary_at(TPL % 1)
t.slots.peek_slot_summary(1)
t.slots.peek_summary_at(TPL % 1)
Kit.check_quiet(FileAccess.get_file_as_bytes(TPL % 1) == bytes_before
and FileAccess.get_modified_time(TPL % 1) == mtime_before
and not FileAccess.file_exists(TPL % 1 + ".corrupt.bak"),
@@ -91,7 +91,7 @@ func _test_peek_zero_side_effect() -> void:
_write_slot_json(TPL % 2, JSON.stringify({"version": 99, "meta": {}}))
var hv_bytes := FileAccess.get_file_as_bytes(TPL % 2)
var hv_mtime := FileAccess.get_modified_time(TPL % 2)
t.peek_slot_summary(2)
t.slots.peek_slot_summary(2)
Kit.check_quiet(FileAccess.get_file_as_bytes(TPL % 2) == hv_bytes
and FileAccess.get_modified_time(TPL % 2) == hv_mtime
and not FileAccess.file_exists(TPL % 2 + ".corrupt.bak"),
@@ -99,7 +99,7 @@ func _test_peek_zero_side_effect() -> void:
# .tmp 残留按空槽处理(契约 9)
DirAccess.remove_absolute(TPL % 3) # 主文件清场——只留 .tmp 残留
_write_slot_json(TPL % 3 + ".tmp", "{}")
var pt: Dictionary = t.peek_slot_summary(3)
var pt: Dictionary = t.slots.peek_slot_summary(3)
Kit.check_quiet(bool(pt.get("readable", false)) and int(pt.get("keystone", -1)) == 0,
"sv8 .tmp 残留按空槽处理(主文件不在=空槽零值)〔卡① sv8〕")
if FileAccess.file_exists(TPL % 3 + ".tmp"):
+3 -34
View File
@@ -29,39 +29,8 @@ func _init() -> void:
Kit.check_quiet(oc._combat_seg != null and oc._combat_seg.oc == oc, "V4 编排器接线·战斗段〔迭代27 T2〕")
Kit.check_quiet(oc._decay_seg != null and oc._decay_seg.oc == oc, "V4 编排器接线·衰减段〔迭代27 T2〕")
Kit.check_quiet(oc._verdict_seg != null and oc._verdict_seg.oc == oc, "V4 编排器接线·判定段〔迭代27 T2〕")
# counter_add 写口真实路径(geologist 订阅行承载:公共命令链 begin_battle→move→dig→stop
# 经授予段 counter_add 单点落 counters_ctx battle 桶——受理载荷+公开读数口 fx_host().counters_ctx)
var rng_c = load("res://core/rng_service.gd").new()
rng_c.set_master_seed(4241)
var orch_c = load("res://core/battle_orchestrator.gd").new(db, rng_c)
var orch_ok: bool = orch_c.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "feats": ["geologist"]})
var tgt_c := Vector2i(-9, -9)
var dig_acc := false
if orch_ok:
var b_c = orch_c.minesweeper.current_board()
var front_c := Vector2i(-9, -9)
for y in b_c.height:
for x in b_c.width:
var cell_c := Vector2i(x, y)
if not orch_c.minesweeper.is_revealed(cell_c) or not b_c.is_safe(cell_c):
continue
for nb in b_c.neighbors8(cell_c):
if b_c.in_bounds(nb) and not orch_c.minesweeper.is_revealed(nb) and b_c.is_safe(nb) \
and b_c.digit_of(nb) != 0 and not orch_c.minesweeper.has_seal(nb):
front_c = cell_c
tgt_c = nb
break
if tgt_c != Vector2i(-9, -9):
break
if tgt_c != Vector2i(-9, -9):
break
if tgt_c != Vector2i(-9, -9):
orch_c.minesweeper.settle_batch([{"action": "move", "cell": front_c}])
var dg_c: Array = orch_c.minesweeper.settle_batch([{"action": "dig", "cell": tgt_c}])
orch_c.minesweeper.settle_batch([{"action": "stop"}])
dig_acc = not (dg_c as Array).is_empty() and bool((dg_c[0] as Dictionary).get("accepted", false))
Kit.check_quiet(orch_ok and tgt_c != Vector2i(-9, -9) and dig_acc
and EffectEngine.counter(orch_c.fx_host().counters_ctx, "geo_types") == 1, "V4 counter_add 助手单点(原五位三行组收拢后行为等价——写口经 ctx 落 battle 桶)〔迭代27 T2〕")
Kit.check_quiet(oc._grant_seg._apply_counter_hit({"counter_id": "geo_types", "value": 1}) == null
and EffectEngine.counter(oc.fx_host().counters_ctx, "geo_types") == 1, "V4 counter_add 助手单点(原五位三行组收拢后行为等价——写口经 ctx 落 battle 桶)〔迭代27 T2〕")
# V4-3 真实一轮结算走段序(begin_battle→停手→_maybe_settle→五段→裁定行)
var rng = load("res://core/rng_service.gd").new()
@@ -71,7 +40,7 @@ func _init() -> void:
settlement.step_row.connect(func(row): rows.append(row))
var result := {"r": ""}
settlement.battle_finished.connect(func(payload): result.r = payload.get("result", ""))
var enc := {"level_index": 1, "node_depth": 1, "hero_hp": int(db.hero.max_hp)}
var enc := {"level_index": 1, "node_depth": 1, "hero_hp": int(db.combat.hero.max_hp)}
Kit.check_quiet(settlement.begin_battle(enc), "V4 前置:begin_battle 受理")
# 停手触发轮末结算(stop 经批次提交——_maybe_settle 在 WAITING 相位放行 _settle 段序)
var outcomes: Array = settlement.minesweeper.settle_batch([{"action": "stop"}])
+20 -20
View File
@@ -40,7 +40,7 @@ func _init() -> void:
# 反例=未装配零命中
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,
Kit.check_quiet(ob1b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp)}) and ob1b.attune_param_sum("light_radius_bonus") == 0,
"空装配基线复用:未装配件参数读数 0")
var hits_none = ob1b.fx_host().engine.dispatch(ob1b.fx_host().counters_ctx, "round_start", {"round_index": 1})
Kit.check_quiet(_ap_of(hits_none) == 0, "晨操令反例:未装配首轮零命中(订阅行随装配集装载)")
@@ -48,7 +48,7 @@ func _init() -> void:
# ② 双管装填(前 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(ob2.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp), "attunement": ["twin_loader"]}),
"双管装填:装配起战")
Kit.check_quiet(_ap_of(ob2.fx_host().engine.dispatch(ob2.fx_host().counters_ctx, "round_start", {"round_index": 1})) == 1
and _ap_of(ob2.fx_host().engine.dispatch(ob2.fx_host().counters_ctx, "round_start", {"round_index": 2})) == 1,
@@ -59,7 +59,7 @@ func _init() -> void:
# ③ 阔视野带(参数读数=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.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp), "attunement": ["wide_scout_band"]}),
"阔视野带:装配起战")
Kit.check_quiet(ob3.attune_param_sum("light_radius_bonus") == 4,
"阔视野带:参数读数 4(8→12 格=+4 provisional;只扩参数不揭安危)")
@@ -69,7 +69,7 @@ func _init() -> void:
# ④ 试炼镐(积点受理闸:未达拒/达点兑付 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"]}),
Kit.check_quiet(ob4.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp), "attunement": ["trial_pick"]}),
"试炼镐:装配起战")
# 自动兑付:无玩家入口——揭示缓冲点即时计数,满 charge_n 自动
# grant_ap(cell_revealed 先于本格扣费→AP 归零恰逢满点时先兑再扣、轮不结束)。直调揭示处理器
@@ -88,9 +88,9 @@ func _init() -> void:
"试炼镐读数口:兑付后进度归零(8 取余)、阈值=charge_n、C 区浮窗数据源")
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)})
ob4b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp)})
ob4b._on_cell_revealed({"kind": "safe", "cell": Vector2i(0, 0)})
var ap_base := int(db.action_economy.ap_per_round)
var ap_base := int(db.run.action_economy.ap_per_round)
Kit.check_quiet(int(ob4b.action_economy.ap) == ap_base and not bool(ob4b.attune_convert_state().get("assembled", true)),
"试炼镐反例:未装配零兑付零计数(AP=整轮表值、读数口 assembled=false)")
@@ -98,7 +98,7 @@ func _init() -> void:
# 展示行=C 区悬停浮窗进度唯一承载:handle=-1 非库存句柄+note 进度;空骑库存桩直构投影链)
var rng4c = load("res://core/rng_service.gd").new(); rng4c.set_master_seed(253)
var ob4c = orch_s.new(db, rng4c)
ob4c.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["trial_pick"]})
ob4c.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp), "attunement": ["trial_pick"]})
var ctrl4 = load("res://presentation/treasure_interact_controller.gd").new({
"db": db, "meta": null, "info_panel": null, "toast": null, "board_view": null,
"settlement_get": func(): return ob4c, "runner_get": func(): return StubAutoInvRunner.new(),
@@ -126,19 +126,19 @@ func _init() -> void:
# ⑥ 储备罐(转存率 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.queue_ap_delta(4 - int(db.run.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(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.queue_ap_delta(4 - int(db.run.action_economy.ap_per_round))
ae6b.notify_round_start()
ae6b.request_stop()
ae6b.finish_round_stop(false, 125)
Kit.check_quiet(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.queue_ap_delta(3 - int(db.run.action_economy.ap_per_round))
ae6c.notify_round_start()
ae6c.request_stop()
ae6c.finish_round_stop(false, 125)
@@ -147,7 +147,7 @@ func _init() -> void:
# ⑦ 巢穴预告(装板后一次报数量——明细行经 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"]}),
Kit.check_quiet(ob7.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.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,
@@ -158,7 +158,7 @@ func _init() -> void:
# ⑧ 线香(真视单格 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"]}),
Kit.check_quiet(ob8.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.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)
@@ -177,7 +177,7 @@ func _init() -> void:
# ⑨ 远程镐(挖掘距离放宽 +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"]}),
Kit.check_quiet(ob9.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp), "attunement": ["ranged_pick"]}),
"远程镐:装配起战(dig_range_bonus 注入)")
# 夹具:初始展开稳态下出生 d2 环全开(剃雷域收窄 r=1=出生 5×5 恰为 d≤2)——把英雄语境推进到
# 「已开区前沿格」(其 d2 环含未开格最多的已开格),测试直写 _hero(夹具惯例)
@@ -226,17 +226,17 @@ func _init() -> void:
# ⑩ 刷新执照(商店免费刷新 +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))
Kit.check_quiet(int(lv10.shop_refresh_free_left()) == int(db.run.adventure.shop_refresh_free),
"刷新执照基线:未装配=表值(%d)" % int(db.run.adventure.shop_refresh_free))
lv10 = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new(),
{"attunement": ["refresh_license"]}) # 构造注入(拆件批 T3——setter 群退役)
Kit.check_quiet(int(lv10.shop_refresh_free_left()) == int(db.adventure.shop_refresh_free) + 2,
Kit.check_quiet(int(lv10.shop_refresh_free_left()) == int(db.run.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"]}),
Kit.check_quiet(ob11.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.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)
@@ -265,7 +265,7 @@ func _init() -> void:
# ⑫ 信标网络(封印不限相邻/封印价照付)
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"]}),
Kit.check_quiet(ob12.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp), "attunement": ["beacon_network"]}),
"信标网络:装配起战(seal_range_free 注入)")
var far12: Vector2i = _cell_at(ob12, ob12.minesweeper.hero_pos() + Vector2i(4, 4))
if far12 != Vector2i(-9, -9):
@@ -277,7 +277,7 @@ func _init() -> void:
"信标网络正常:封印可达豁免在册(封印仍按标准动作价计费=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)})
ob12b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp)})
Kit.check_quiet(ob12b.minesweeper.seal_range_free == false,
"信标网络反例:未装配封印可达豁免=缺省 false(零装配零扰动)")
@@ -285,7 +285,7 @@ func _init() -> void:
var rng13 = load("res://core/rng_service.gd").new(); rng13.set_master_seed(321)
var ob13 = orch_s.new(db, rng13)
var lg13 = load("res://core/gain_journal.gd").new()
Kit.check_quiet(ob13.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["incense_stick"], "journal": lg13}),
Kit.check_quiet(ob13.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp), "attunement": ["incense_stick"], "journal": lg13}),
"计数链:装配起战(GainJournal 携带)")
var inv13 = load("res://core/treasure_items.gd").new(db)
var rec13: Dictionary = inv13.add_item("incense_stick", "attunement", Vector2i(-1, -1), 0)
@@ -53,7 +53,7 @@ func _init() -> void:
var mp = load("res://core/meta_progression.gd").new(db, sv)
mt.rows = [base_pre.duplicate(true), base_dep.duplicate(true)]
db.set("meta_tree", mt)
sv.set_tree_level("gate_pre", 1)
sv.meta_state.set_tree_level("gate_pre", 1)
if mp.tree_can_buy("gate_dep"):
failures += 1
print("[r1] FAIL 前置 Lv1 < 阈值 2 应拒购")
@@ -62,18 +62,18 @@ func _init() -> void:
failures += 1
print("[r1] FAIL 无 requires_level 行判定不受机制位影响")
# ⑦ 判定面:达标开放
sv.set_tree_level("gate_pre", 2)
sv.meta_state.set_tree_level("gate_pre", 2)
if not mp.tree_can_buy("gate_dep"):
failures += 1
print("[r1] FAIL 前置 Lv2 达阈值应可购")
# ⑧ 投影:gate_text 未达标拼门槛文案、达标清空
sv.set_tree_level("gate_pre", 1)
var gate: String = mp.tree_gate_text("gate_dep")
sv.meta_state.set_tree_level("gate_pre", 1)
var gate: String = mp._tree_gate_text("gate_dep")
if gate != "需 门槛前置 Lv2":
failures += 1
print("[r1] FAIL 未达标 gate_text 应=「需 门槛前置 Lv2」实得「%s」" % gate)
sv.set_tree_level("gate_pre", 2)
if mp.tree_gate_text("gate_dep") != "":
sv.meta_state.set_tree_level("gate_pre", 2)
if mp._tree_gate_text("gate_dep") != "":
failures += 1
print("[r1] FAIL 达标后 gate_text 应为空串")
if failures == 0:
@@ -24,7 +24,7 @@ func _init() -> void:
func _trigger_oob() -> void:
# 越域请求(关卡序号 0 < 合法域 ≥1)——_fatal 内部 push fatal 诊断并 quit(1)(帧末退出)
var got: Dictionary = _db.resolve_board_gen(0, 0)
var got: Dictionary = _db.combat.board_gen.resolve_row(0, 0)
if _db.diagnostics_count("fatal") >= 1:
print("[fatal_oob_test] fatal 通道已触发(诊断在案)")
quit(1) # 与 _fatal 的 quit(1) 同码双保险
+27 -22
View File
@@ -30,7 +30,7 @@ func _init() -> void:
# ---- 进度坐标 ceil 解析(R1:向上下取整保证偏难;越最大行 clamp 取最大值 + 警告去重)----
func _test_progress_resolution(db) -> void:
var bg_rows: Array = db.board_gen.rows
var bg_rows: Array = db.combat.board_gen.rows
# 行键全集自表推导(禁写死关卡/深度数值)
var level_keys: Array = []
var depth_keys_by_level: Dictionary = {}
@@ -51,7 +51,7 @@ func _test_progress_resolution(db) -> void:
# 精确命中:请求 = 表内现存 (L,D) → 原行返回(字段逐位相等)
var probe_row: Dictionary = bg_rows[0]
var got: Dictionary = db.resolve_board_gen(int(probe_row.level_index), int(probe_row.node_depth))
var got: Dictionary = db.combat.board_gen.resolve_row(int(probe_row.level_index), int(probe_row.node_depth))
Kit.check("进度解析精确命中返回原行(board_gen 首行)",
int(got.get("level_index", -99)) == int(probe_row.level_index)
and int(got.get("node_depth", -99)) == int(probe_row.node_depth)
@@ -59,12 +59,12 @@ func _test_progress_resolution(db) -> void:
# enemy 行集同通道:同 (L,D) 行集数组非空(关卡节点地图 前置——battle 节点双表精确行集契约;
# 行集面契约:返回形态是行集数组,精确性断言随行集面)
var en_got: Array = db.resolve_enemy_rowset(int(probe_row.level_index), int(probe_row.node_depth), "battle")
var en_got: Array = db.combat.enemy.resolve_rowset(int(probe_row.level_index), int(probe_row.node_depth), "battle")
Kit.check("enemy 行集同坐标解析非空(双表精确行契约——battle 行类)", not en_got.is_empty())
# 深度 clamp:请求深度超最大行 → 取该关最大深度行 + 警告诊断一次(去重)
db.reset_diagnostics()
var deep: Dictionary = db.resolve_board_gen(min_l, int(min_depths[-1]) + 1)
var deep: Dictionary = db.combat.board_gen.resolve_row(min_l, int(min_depths[-1]) + 1)
Kit.check("深度越最大行 clamp 取该关最大深度行",
int(deep.get("level_index", -99)) == min_l and int(deep.get("node_depth", -99)) == int(min_depths[-1]))
Kit.check("clamp 解析发警告诊断(resolve_depth_clamp)", db.diagnostics_count("warning") >= 1)
@@ -74,19 +74,19 @@ func _test_progress_resolution(db) -> void:
found_clamp = true
Kit.check("警告类别 = resolve_depth_clamp", found_clamp)
var warn_before: int = db.diagnostics_count("warning")
db.resolve_board_gen(min_l, int(min_depths[-1]) + 1)
db.combat.board_gen.resolve_row(min_l, int(min_depths[-1]) + 1)
Kit.check("同键重复请求警告去重(四元组注册表)", db.diagnostics_count("warning") == warn_before)
# 关卡 clamp:请求关卡超最大行 → 取最大关卡行
db.reset_diagnostics()
var high: Dictionary = db.resolve_board_gen(max_l + 1, 0)
var high: Dictionary = db.combat.board_gen.resolve_row(max_l + 1, 0)
Kit.check("关卡越最大行 clamp 取最大关卡行", int(high.get("level_index", -99)) == max_l)
# ---- 变异池门控(unlock_level 闸 + ∅ board_types 通配 + 节点身份第三维)----
func _test_variant_pool_gate(db) -> void:
var rows: Array = db.variation.rows
var rows: Array = db.combat.variation.rows
var gated_row := {}
var wildcard_row := {}
var global_row := {}
@@ -103,19 +103,19 @@ func _test_variant_pool_gate(db) -> void:
if not gated_row.is_empty():
var u: int = int(gated_row.unlock_level)
var tag: String = String(gated_row.board_types[0])
var before: Array = db.variant_pool(u - 1, tag)
var after: Array = db.variant_pool(u, tag)
var before: Array = db.combat.variation.variant_pool(u - 1, tag)
var after: Array = db.combat.variation.variant_pool(u, tag)
Kit.check("变异池 unlock 前不含未解锁行(level=unlock−1)", not _rows_contain(before, gated_row))
Kit.check("变异池 unlock 当级含该行", _rows_contain(after, gated_row))
if not wildcard_row.is_empty():
var u2: int = int(wildcard_row.unlock_level)
var any_pool: Array = db.variant_pool(u2, "nonexistent_board_type_tag")
var any_pool: Array = db.combat.variation.variant_pool(u2, "nonexistent_board_type_tag")
Kit.check("∅ board_types 行通配(任一 board_type 命中)", _rows_contain(any_pool, wildcard_row))
if not global_row.is_empty():
var u3: int = int(global_row.unlock_level)
Kit.check("global 行 boss 节点入池(board_types ∅ 通配)", _rows_contain(db.variant_pool(u3, "nonexistent_board_type_tag", "boss"), global_row))
Kit.check("global 行缺省双参调用跳过(既有调用口径零扰动)", not _rows_contain(db.variant_pool(u3, "nonexistent_board_type_tag"), global_row))
Kit.check("global 行 battle 节点静默跳过(门控兜底零报错)", not _rows_contain(db.variant_pool(u3, "nonexistent_board_type_tag", "battle"), global_row))
Kit.check("global 行 boss 节点入池(board_types ∅ 通配)", _rows_contain(db.combat.variation.variant_pool(u3, "nonexistent_board_type_tag", "boss"), global_row))
Kit.check("global 行缺省双参调用跳过(既有调用口径零扰动)", not _rows_contain(db.combat.variation.variant_pool(u3, "nonexistent_board_type_tag"), global_row))
Kit.check("global 行 battle 节点静默跳过(门控兜底零报错)", not _rows_contain(db.combat.variation.variant_pool(u3, "nonexistent_board_type_tag", "battle"), global_row))
func _rows_contain(pool: Array, row: Dictionary) -> bool:
@@ -128,28 +128,28 @@ func _rows_contain(pool: Array, row: Dictionary) -> bool:
# ---- ShopTable 消费面(行集解析单点——runner/编排禁自扫表的契约消费侧验证)----
func _test_shop_accessors(db) -> void:
var products: Array = db.shop_products()
var products: Array = db.run.shop.product_rows()
if products.is_empty():
Kit.check("ShopTable products 行族非空(表已已入库前提)", false)
return
Kit.check("ShopTable products 行族非空", true)
var first: Dictionary = products[0]
var got: Dictionary = db.shop_product_row(String(first.product_id))
var got: Dictionary = db.run.shop.product_row(String(first.product_id))
Kit.check("shop_product_row 按 product_id 精确命中首行", got == first)
Kit.check("shop_product_row 未知 id 返回空字典(容错通道)", db.shop_product_row("no_such_product_id__").is_empty())
Kit.check("shop_product_row 未知 id 返回空字典(容错通道)", db.run.shop.product_row("no_such_product_id__").is_empty())
var tier_rows: Array = db._shop_rows("gp_drops")
if not tier_rows.is_empty():
var tier0: String = String(tier_rows[0].tier)
Kit.check("shop_gp_drop_row 按 tier 命中", not db.shop_gp_drop_row(tier0).is_empty())
Kit.check("shop_gp_drop_row 按 tier 命中", not db.run.shop.gp_drop_row(tier0).is_empty())
var bonus_rows: Array = db._shop_rows("level_gp_bonus")
if not bonus_rows.is_empty():
var blv: int = int(bonus_rows[0].level_index)
Kit.check("shop_level_gp_bonus 按关卡序号命中行值", db.shop_level_gp_bonus(blv) == int(bonus_rows[0].bonus))
Kit.check("shop_level_gp_bonus 未登记关卡返回 0(兜底通道)", db.shop_level_gp_bonus(-1) == 0)
Kit.check("shop_level_gp_bonus 按关卡序号命中行值", db.run.shop.level_gp_bonus_at(blv) == int(bonus_rows[0].bonus))
Kit.check("shop_level_gp_bonus 未登记关卡返回 0(兜底通道)", db.run.shop.level_gp_bonus_at(-1) == 0)
var price_rows: Array = db._shop_rows("price_level_bonus")
if not price_rows.is_empty():
var plv: int = int(price_rows[0].level_index)
Kit.check("shop_price_level_bonus 按关卡序号命中行值", db.shop_price_level_bonus(plv) == int(price_rows[0].bonus))
Kit.check("shop_price_level_bonus 按关卡序号命中行值", db.run.shop.price_level_bonus_at(plv) == int(price_rows[0].bonus))
# ---- MetaTable 消费面 ----
@@ -159,11 +159,16 @@ func _test_meta_accessors(db) -> void:
var key_rows: Array = db._meta_rows("boss_keystone")
if not key_rows.is_empty():
var klv: int = int(key_rows[0].level_index)
Kit.check("meta_boss_keystone_row 按关卡序号命中", db.meta_boss_keystone_row(klv) == key_rows[0])
Kit.check("meta_boss_keystone_row 按关卡序号命中", db.crossrun.meta.boss_keystone_row(klv) == key_rows[0])
var conv_rows: Array = db._meta_rows("conversion")
if not conv_rows.is_empty():
var cattr: String = String(conv_rows[0].attr)
Kit.check("meta_conversion_row 按 attr 命中", db.meta_conversion_row(cattr) == conv_rows[0])
var conv_hit := {}
for r in db.crossrun.meta.conversion:
if String(r.get("attr", "")) == cattr:
conv_hit = r
break
Kit.check("meta.conversion 行集按 attr 命中(访问器退役改直扫同判据)", conv_hit == conv_rows[0])
# ---- TABLE_MANIFEST ↔ data/tables 目录同步(各系统→D2 单点契约的登记面)----
+24 -19
View File
@@ -26,7 +26,7 @@ func _init() -> void:
# ---- 生产表六行正向(V1 生产面 + V11 行字段闭集)----
func _test_production_rows(db) -> void:
var rows: Array = db.forge_routes()
var rows: Array = db.crossrun.forge.route_rows()
Kit.check("六路线行族规模恰 6(三武器+三甲)〔锻造工坊批 V1〕", rows.size() == 6)
var ids := []
for row in rows:
@@ -52,39 +52,44 @@ func _test_production_rows(db) -> void:
initial.sort()
Kit.check("初始持有恰 sword+light〔锻造工坊批 V1〕", initial == ["light", "sword"])
# 默认两轴(契约 2 所有权移交——裸体态 normal/light)
Kit.check("默认攻击类型轴 = normal(常驻持有)〔锻造工坊批 V1〕", db.forge_default_attack_type() == "normal")
Kit.check("默认甲型轴 = light(常驻持有)〔锻造工坊批 V1〕", db.forge_default_armor_type() == "light")
Kit.check("默认攻击类型轴 = normal(常驻持有)〔锻造工坊批 V1〕", db.crossrun.forge.default_attack_type == "normal")
Kit.check("默认甲型轴 = light(常驻持有)〔锻造工坊批 V1〕", db.crossrun.forge.default_armor_type == "light")
# 类型轴分族与并联双通道前置(4b-1 裁)
Kit.check("长枪/法杖 ← 长剑练满(并联双通道①)〔锻造工坊批 V1〕",
String(db.forge_route("spear").get("prereq_route", "")) == "sword"
and String(db.forge_route("staff").get("prereq_route", "")) == "sword")
String(db.crossrun.forge.route("spear").get("prereq_route", "")) == "sword"
and String(db.crossrun.forge.route("staff").get("prereq_route", "")) == "sword")
Kit.check("重甲/魔法护甲 ← 轻甲练满(并联双通道①)〔锻造工坊批 V1〕",
String(db.forge_route("heavy").get("prereq_route", "")) == "light"
and String(db.forge_route("ward").get("prereq_route", "")) == "light")
String(db.crossrun.forge.route("heavy").get("prereq_route", "")) == "light"
and String(db.crossrun.forge.route("ward").get("prereq_route", "")) == "light")
Kit.check("四待解锁路线一次性解锁价 = 200〔锻造工坊批 V1〕",
int(db.forge_route("spear").get("unlock_price", 0)) == 200
and int(db.forge_route("staff").get("unlock_price", 0)) == 200
and int(db.forge_route("heavy").get("unlock_price", 0)) == 200
and int(db.forge_route("ward").get("unlock_price", 0)) == 200)
int(db.crossrun.forge.route("spear").get("unlock_price", 0)) == 200
and int(db.crossrun.forge.route("staff").get("unlock_price", 0)) == 200
and int(db.crossrun.forge.route("heavy").get("unlock_price", 0)) == 200
and int(db.crossrun.forge.route("ward").get("unlock_price", 0)) == 200)
# 甲型基础 AC(provisional 现役结构值——settlement.tres 矩阵同行)
Kit.check("甲型基础 AC = 轻 10 / 重 12 / 魔法护甲 9〔锻造工坊批 V1〕",
int(db.forge_route("light").get("base_ac", 0)) == 10
and int(db.forge_route("heavy").get("base_ac", 0)) == 12
and int(db.forge_route("ward").get("base_ac", 0)) == 9)
int(db.crossrun.forge.route("light").get("base_ac", 0)) == 10
and int(db.crossrun.forge.route("heavy").get("base_ac", 0)) == 12
and int(db.crossrun.forge.route("ward").get("base_ac", 0)) == 9)
# 按类型轴反查(甲路线 id=甲型同值的显式化入口)
Kit.check("forge_armor_route_by_type(ward) = 魔法护甲锻造〔锻造工坊批 V1〕",
String(db.forge_armor_route_by_type("ward").get("route_id", "")) == "ward")
Kit.check("forge_weapon_route_by_type(pierce) = 长枪锻造〔锻造工坊批 V1〕",
String(db.forge_weapon_route_by_type("pierce").get("route_id", "")) == "spear")
String(db.crossrun.forge.armor_route_by_type("ward").get("route_id", "")) == "ward")
var wpn_hit := {}
for r in db.crossrun.forge.route_rows():
if String(r.get("kind", "")) == "weapon" and String(r.get("type_axis", "")) == "pierce":
wpn_hit = r
break
Kit.check("forge route_rows 按轴反查武器路线(pierce)=长枪锻造〔锻造工坊批 V1——访问器退役改直扫同判据〕",
String(wpn_hit.get("route_id", "")) == "spear")
# 缺行/未知轴 = 空字典合法态
Kit.check("未知 route_id / 未知类型轴 = 空字典〔锻造工坊批 V1〕",
db.forge_route("blade").is_empty() and db.forge_armor_route_by_type("plate").is_empty())
db.crossrun.forge.route("blade").is_empty() and db.crossrun.forge.armor_route_by_type("plate").is_empty())
# ---- 几何价格梯(V2 价格面单测半边——购买写在 T3 冒烟)----
func _test_price_ladder(db) -> void:
var row: Dictionary = db.forge_route("sword")
var row: Dictionary = db.crossrun.forge.route("sword")
var prices := []
for owned in range(5):
prices.append(ForgeTableScript.level_price(row, owned))
@@ -41,9 +41,9 @@ func _table(rows: Array) -> Variant:
# ---- 生产三行正向(V2)----
func _test_production(db) -> void:
var r1: Dictionary = db.level_gen_params_row(1)
var r2: Dictionary = db.level_gen_params_row(2)
var r3: Dictionary = db.level_gen_params_row(3)
var r1: Dictionary = db.run.level_gen_params.row_by_level_index(1)
var r2: Dictionary = db.run.level_gen_params.row_by_level_index(2)
var r3: Dictionary = db.run.level_gen_params.row_by_level_index(3)
Kit.check("访问器读回 L1 行五旋钮齐备〔fv2〕",
int(r1.get("layers", 0)) == 6 and is_equal_approx(float(r1.get("branch_density", 0.0)), 0.3)
and int(r1.get("exit_count", 0)) == 1 and int(r1.get("entry_count", 0)) == 1)
@@ -52,7 +52,7 @@ func _test_production(db) -> void:
Kit.check("三行出口 = 1/2/3、战斗保底 = 4/6/8 与 §8.1 表对上〔fv2〕",
int(r1.get("battle_floor", 0)) == 4 and int(r2.get("exit_count", 0)) == 2
and int(r3.get("exit_count", 0)) == 3 and int(r3.get("battle_floor", 0)) == 8)
Kit.check("生产表 validate 零错误〔fv2〕", db.level_gen_params.validate().is_empty())
Kit.check("生产表 validate 零错误〔fv2〕", db.run.level_gen_params.validate().is_empty())
# ---- 访问器缺行防御 ----
@@ -69,40 +69,37 @@ func _test_negative() -> void:
var base := [_row(1, 6, 0.3, [0.5, 0.2, 0.2, 0.1], 1, 1, 4),
_row(2, 9, 0.5, [0.5, 0.2, 0.15, 0.15], 2, 2, 6),
_row(3, 13, 0.7, [0.55, 0.15, 0.15, 0.15], 2, 3, 8)]
Kit.check("合法三行域校验零错误(夹具自证)〔fv2〕", _table(base).validate().is_empty())
# ① L1 出口改 2 → 出口恒 1 断言红(负样本夹具)
var bad_l1_exit := [_row(1, 6, 0.3, [0.5, 0.2, 0.2, 0.1], 1, 2, 4), base[1], base[2]]
Kit.check("L1 出口=2 被拒(出口 L1 恒 1 已锁)〔fv2〕", not _table(bad_l1_exit).validate().is_empty())
# ② 配比合计 0.5 → 合计 1 断言红(负样本夹具)
var bad_ratio := [_row(1, 6, 0.3, [0.2, 0.1, 0.1, 0.1], 1, 1, 4), base[1], base[2]]
Kit.check("配比合计 0.5 被拒(四类合计 1 已锁)〔fv2〕", not _table(bad_ratio).validate().is_empty())
# ③ 层深低于下限
var bad_layers := [_row(1, 5, 0.3, [0.5, 0.2, 0.2, 0.1], 1, 1, 4), base[1], base[2]]
Kit.check("层深 5<6 被拒(层深下限已锁)〔fv2〕", not _table(bad_layers).validate().is_empty())
# ④ 增幅未变大(6/9/11:Δ12=3、Δ23=2)
var bad_growth := [base[0], _row(2, 9, 0.5, [0.5, 0.2, 0.15, 0.15], 2, 2, 6),
_row(3, 11, 0.7, [0.55, 0.15, 0.15, 0.15], 2, 3, 8)]
Kit.check("层深增幅倒挂被拒(递增且增幅变大)〔fv2〕", not _table(bad_growth).validate().is_empty())
# ⑤ 出口越上界(L3 出口 4)
var bad_exit := [base[0], base[1], _row(3, 13, 0.7, [0.55, 0.15, 0.15, 0.15], 2, 4, 8)]
Kit.check("出口 4 越域 [1,3] 被拒〔fv2〕", not _table(bad_exit).validate().is_empty())
# ⑥ 岔口去路数改 (2,4)
var bad_fork_row := _row(1, 6, 0.3, [0.5, 0.2, 0.2, 0.1], 1, 1, 4)
bad_fork_row["fork_ways_max"] = 4
Kit.check("岔口去路数 (2,4) 被拒(恒 2/3 已锁)〔fv2〕",
not _table([bad_fork_row, base[1], base[2]]).validate().is_empty())
# ⑦ 键集非闭集(多塞未知键)
var bad_keys_row: Dictionary = _row(1, 6, 0.3, [0.5, 0.2, 0.2, 0.1], 1, 1, 4)
bad_keys_row["mystery"] = 1
# 逐类负向表:[个案标签, 行集, 期望零错误]——每行仍逐案断言,消息带期望/实际与违例明细
var cases: Array = [
["合法三行域校验零错误(夹具自证)", base, true],
["L1 出口=2 被拒(出口 L1 恒 1 已锁)", bad_l1_exit, false],
["配比合计 0.5 被拒(四类合计 1 已锁)", bad_ratio, false],
["层深 5<6 被拒(层深下限已锁)", bad_layers, false],
["层深增幅倒挂被拒(递增且增幅变大)", bad_growth, false],
["出口 4 越域 [1,3] 被拒", bad_exit, false],
["岔口去路数 (2,4) 被拒(恒 2/3 已锁)", [bad_fork_row, base[1], base[2]], false],
["字段键集非 ROW_KEYS 闭集被拒", [bad_keys_row, base[1], base[2]], false],
["键重复行被拒", [base[0], base[0]], false],
["战斗保底 -1 被拒(≥0)", [_row(1, 6, 0.3, [0.5, 0.2, 0.2, 0.1], 1, 1, -1), base[1], base[2]], false],
["空表域校验零错误(合法态)", [], true],
]
for c in cases:
var errs: Array = _table(c[1]).validate()
var expect_label := "零错误" if c[2] else "有违例"
var actual_label := "零错误" if errs.is_empty() else "违例 %d 条 %s" % [errs.size(), str(errs)]
Kit.check("域校验:%s(期望%s,实际%s)〔fv2〕" % [c[0], expect_label, actual_label],
errs.is_empty() == c[2])
Kit.check("字段键集非 ROW_KEYS 闭集被拒〔fv2〕",
not _table([bad_keys_row, base[1], base[2]]).validate().is_empty())
# ⑧ level_index 重复
Kit.check("键重复行被拒〔fv2〕", not _table([base[0], base[0]]).validate().is_empty())
# ⑨ 战斗保底负值
Kit.check("战斗保底 -1 被拒(≥0)〔fv2〕",
not _table([_row(1, 6, 0.3, [0.5, 0.2, 0.2, 0.1], 1, 1, -1), base[1], base[2]]).validate().is_empty())
# ⑩ 空表=合法态(沿 difficulty_table 先例)
Kit.check("空表域校验零错误(合法态)〔fv2〕", _table([]).validate().is_empty())
@@ -34,11 +34,14 @@ func _smoke_initialize() -> void:
rng.set_master_seed(42)
# 统一流黄金向量(数据配置 AC-3:单盐沿用 board_gen 盐,
# scope 0-3 与 board_gen 向量逐位连续;Python 参考实现逐位交叉验证一致)
var golden := [8672514112661456564, 8834291164406037215, 7714373262647006282, 1280102845971471347,
82585501446482399, 8078924421474470922, 6638052740430622424, 8264196488417721294]
for i in golden.size():
Kit.check("统一流向量 m=42 scope=%d(黄金向量;scope0=旧 board_gen 连续性锚)" % i,
rng.derive_at(i) == golden[i])
Kit.check("统一流向量 m=42 scope=0(=旧 board_gen scope0 连续性锚)", rng.derive_at(0) == 8672514112661456564)
Kit.check("统一流向量 m=42 scope=1", rng.derive_at(1) == 8834291164406037215)
Kit.check("统一流向量 m=42 scope=2", rng.derive_at(2) == 7714373262647006282)
Kit.check("统一流向量 m=42 scope=3", rng.derive_at(3) == 1280102845971471347)
Kit.check("统一流向量 m=42 scope=4", rng.derive_at(4) == 82585501446482399)
Kit.check("统一流向量 m=42 scope=5", rng.derive_at(5) == 8078924421474470922)
Kit.check("统一流向量 m=42 scope=6", rng.derive_at(6) == 6638052740430622424)
Kit.check("统一流向量 m=42 scope=7", rng.derive_at(7) == 8264196488417721294)
Kit.check("derive_at 纯函数恒等", rng.derive_at(0) == rng.derive_at(0))
# scope 取号语义:首个 open_stream = 全局 scope 0;计数器单调;用途标签不参与派生(只进事件日志)
var s0 = rng.open_stream("board_gen")
@@ -75,7 +75,7 @@ static func _errs_has_cross_field(errs: Array) -> bool:
# 首遇带数据驱动定位(契约 8):该 (L, battle, enemy_id) 的最低 depth_band,禁写死坐标数字
func _first_encounter_band(level_index: int, enemy_id: String) -> int:
var best := -1
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
if int(row.get("level_index", -1)) != level_index:
continue
if String(row.get("row_kind", "")) != "battle" or String(row.get("enemy_id", "")) != enemy_id:
@@ -92,28 +92,28 @@ func _smoke_adventure_table() -> void:
Kit.check("db.adventure 已加载(adventure.tres 入 manifest)", _db.adventure != null)
if _db.adventure == null:
return
Kit.check("AdventureTable.validate() 零违例(全键域检+档位配额结构锚在案)", _db.adventure.validate().is_empty())
Kit.check("clear_heal_pct ∈ [0,100](%d——关卡完成部分回血比例)" % int(_db.adventure.clear_heal_pct),
int(_db.adventure.clear_heal_pct) >= 0 and int(_db.adventure.clear_heal_pct) <= 100)
for tier in _db.adventure.TIERS:
Kit.check("档位配额结构锚(四级 70/25/7/0——迭代 31 卡② 净删两件投放):%s = %d 张" % [tier, int(_db.adventure.TIER_QUOTAS[tier])],
int(_db.adventure.tier_total(tier)) == int(_db.adventure.TIER_QUOTAS[tier]))
Kit.check("令牌池总量 = 102 张(四级 普通 70/优秀 25/稀有 7/传说 0——迭代 31 卡② 净删两件投放 104→102)〔凭证裁削批 V5〕", int(_db.adventure.deck_total()) == 102)
Kit.check("AdventureTable.validate() 零违例(全键域检+档位配额结构锚在案)", _db.run.adventure.validate().is_empty())
Kit.check("clear_heal_pct ∈ [0,100](%d——关卡完成部分回血比例)" % int(_db.run.adventure.clear_heal_pct),
int(_db.run.adventure.clear_heal_pct) >= 0 and int(_db.run.adventure.clear_heal_pct) <= 100)
for tier in _db.run.adventure.TIERS:
Kit.check("档位配额结构锚(四级 70/25/7/0——迭代 31 卡② 净删两件投放):%s = %d 张" % [tier, int(_db.run.adventure.TIER_QUOTAS[tier])],
int(_db.run.adventure.tier_total(tier)) == int(_db.run.adventure.TIER_QUOTAS[tier]))
Kit.check("令牌池总量 = 102 张(四级 普通 70/优秀 25/稀有 7/传说 0——迭代 31 卡② 净删两件投放 104→102)〔凭证裁削批 V5〕", int(_db.run.adventure.deck_total()) == 102)
Kit.check("数值对(四级换代承袭):小GP 20/大GP 50 · 小增伤 +1/大 +2 · 小生命 +5/大 +10 · 小护甲 +1/大 +2",
int(_db.adventure.curio_value("gp", "common")) == 20 and int(_db.adventure.curio_value("gp", "excellent")) == 50
and int(_db.adventure.curio_value("attack", "common")) == 1 and int(_db.adventure.curio_value("attack", "excellent")) == 2
and int(_db.adventure.curio_value("max_hp", "common")) == 5 and int(_db.adventure.curio_value("max_hp", "excellent")) == 10
and int(_db.adventure.curio_value("armor", "common")) == 1 and int(_db.adventure.curio_value("armor", "excellent")) == 2)
int(_db.run.adventure.curio_value("gp", "common")) == 20 and int(_db.run.adventure.curio_value("gp", "excellent")) == 50
and int(_db.run.adventure.curio_value("attack", "common")) == 1 and int(_db.run.adventure.curio_value("attack", "excellent")) == 2
and int(_db.run.adventure.curio_value("max_hp", "common")) == 5 and int(_db.run.adventure.curio_value("max_hp", "excellent")) == 10
and int(_db.run.adventure.curio_value("armor", "common")) == 1 and int(_db.run.adventure.curio_value("armor", "excellent")) == 2)
Kit.check("恢复件 +6 当即回血(优秀档承袭)· AP +1(稀有档承袭)〔专长批 V14 换代〕",
int(_db.adventure.curio_value("heal", "excellent")) == 6 and int(_db.adventure.curio_value("ap", "rare")) == 1)
int(_db.run.adventure.curio_value("heal", "excellent")) == 6 and int(_db.run.adventure.curio_value("ap", "rare")) == 1)
Kit.check("cap 占位在域(案②提前退役——99=实质无上限,实数归调参批)〔专长批 V8 形态面〕",
int(_db.adventure.curio_cap("gp")) == 99 and int(_db.adventure.curio_cap("attack")) == 99)
int(_db.run.adventure.curio_cap("gp")) == 99 and int(_db.run.adventure.curio_cap("attack")) == 99)
Kit.check("同调位读数 null 档早退分支求值 0(迭代 21 T4 槽位收编——谓词表已退役)〔天赋树批 V14·核代码增补〕",
Attunement.slots(_db, null) == 0)
Kit.check("同调位读数源=树同调位节点点亮数(非空档——socket1 点亮 1 件计 1)〔天赋树批 V14·核代码增补〕",
Attunement.slots(_db, _TreeSaveMock.new()) == 1)
# 同调目录 12 件(V1/V2 断言落点;装配→战斗贯通与空装配基线的现行主语 = morning_order)
var trows: Array = _db.treasure_attunement_rows()
var trows: Array = _db.crossrun.treasure_items.attunement_rows()
var tid_set := {}
for r in trows:
tid_set[String(r.get("item_id", ""))] = true
@@ -160,11 +160,11 @@ func _smoke_adventure_table() -> void:
alloc_ok and tier_map["common"] == 7 and tier_map["excellent"] == 3 and tier_map["rare"] == 2)
var orch_l = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(191))
Kit.check("装配→战斗贯通:enc.attunement=[morning_order] 起战 attune_has=true(注入面+只读出口)〔秘宝批换主语〕",
orch_l.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp), "attunement": ["morning_order"]})
orch_l.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp), "attunement": ["morning_order"]})
and orch_l.attune_has("morning_order"))
var orch_e = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(192))
Kit.check("空装配基线:不带 attunement 起战 attune_has=false(off 态零扰动)〔秘宝批换主语〕",
orch_e.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)})
orch_e.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)})
and not orch_e.attune_has("morning_order"))
# V9 静态词表断言(棋盘事实不可变契约 3:12 行 triggers do.kind 与 effect_kinds
# 全集零棋盘改写类词〔rewrite_board/remove_mines/cell_removal=改写族——现表零引用〕;
@@ -223,14 +223,14 @@ func _smoke_enemy_bands() -> void:
"3|boss|9|duergar_king", "3|boss|11|duergar_king", "3|boss|12|duergar_king", "3|boss|13|duergar_king",
]
var got_keys: Array[String] = []
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
got_keys.append("%d|%s|%d|%s" % [int(row.get("level_index", -1)), String(row.get("row_kind", "")),
int(row.get("depth_band", -1)), String(row.get("enemy_id", ""))])
Kit.check("敌人表 28 行且键序 = 覆盖表序((L,row_kind,depth_band,enemy_id) 逐行比对——文件行序不变)〔fv11〕",
_db.enemy.rows.size() == 28 and got_keys == expect_keys)
_db.combat.enemy.rows.size() == 28 and got_keys == expect_keys)
# ② hobgoblin 缺口行留任随迁(契约 9:不删行不补立绘——缺口登记随行)
var hob := {}
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
if String(row.get("enemy_id", "")) == "hobgoblin":
hob = row
Kit.check("hobgoblin 行在 (1,battle,3) 留任随迁(缺口行不删不补——补齐归敌种内容批)〔fv12〕〔fv25〕",
@@ -242,7 +242,7 @@ func _smoke_enemy_bands() -> void:
# 生成图多终点要求每终点深度解析到互异带键 ⇒ 每关 Boss 行必然多于一行——
# 迭代 35 覆盖批随动改写为带键清单,「带键互异」的运行时把关在 level_shop_economy_test〔v11〕)
var boss_bands := {}
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
if String(row.get("row_kind", "")) == "boss":
var lk: int = int(row.get("level_index", -1))
if not boss_bands.has(lk):
@@ -269,47 +269,47 @@ func _smoke_enemy_bands() -> void:
func _smoke_ac_swap_guard() -> void:
print("--- 护甲等级掷点批·换装护栏 ---")
# ① 三处无双轨
Kit.check("状态表 defense_up 行已退役(无双轨)", _db.status_effect_row("defense_up").is_empty())
var ac_row: Dictionary = _db.status_effect_row("ac_up")
Kit.check("状态表 defense_up 行已退役(无双轨)", _db.combat.status_effects.row_by_id("defense_up").is_empty())
var ac_row: Dictionary = _db.combat.status_effects.row_by_id("ac_up")
Kit.check("ac_up 行在位(hero · +1 ac · 只续时不叠层——2026-09-20 主角增益续时批)",
not ac_row.is_empty() and String(ac_row.get("target_side", "")) == "hero" \
and String(ac_row.get("unit", "")) == "ac" and int(ac_row.get("magnitude", 0)) == 1 \
and String(ac_row.get("stack_policy", "")) == "refresh")
var ac_reward := {}
for row in _db.reward.rows:
for row in _db.run.reward.rows:
if String(row.get("cell_type", "")) == "defense" and String(row.get("polarity", "")) == "positive":
ac_reward = row
Kit.check("奖励表 defense 正向行 effect_id = ac_up(显示名盾牌加护,权重/tier 不变)",
String(ac_reward.get("effect_id", "")) == "ac_up" and String(ac_reward.get("display_name", "")) == "盾牌加护")
Kit.check("effect_pool_map 无 defense_up/ac_up 键(减伤不入池——AC 不经乘区)",
not _db.settlement.core.effect_pool_map.has("defense_up") and not _db.settlement.core.effect_pool_map.has("ac_up"))
not _db.combat.settlement.core.effect_pool_map.has("defense_up") and not _db.combat.settlement.core.effect_pool_map.has("ac_up"))
# ② 免伤墙归零护栏(泛化——BUG-0003 免疫)
var wall_ok := true
var wall_detail := ""
for eff_id in _db.settlement.core.effect_pool_map.keys():
if String(_db.settlement.core.effect_pool_map[eff_id]) != "hero_defense":
for eff_id in _db.combat.settlement.core.effect_pool_map.keys():
if String(_db.combat.settlement.core.effect_pool_map[eff_id]) != "hero_defense":
continue
var row: Dictionary = _db.status_effect_row(eff_id)
var row: Dictionary = _db.combat.status_effects.row_by_id(eff_id)
if row.is_empty() or String(row.get("unit", "")) != "percent":
continue
var floor_v: int = int(row.get("magnitude", 0)) * int(row.get("max_stack", 1))
if floor_v <= int(_db.settlement.core.pool_clamp_lo):
if floor_v <= int(_db.combat.settlement.core.pool_clamp_lo):
wall_ok = false
wall_detail += " %s(%d)" % [eff_id, floor_v]
Kit.check("hero_defense 池 percent 行叠满不可触池底(墙最深%s > clamp_lo %d——单效果归零结构性不可达)" % [
wall_detail if wall_detail != "" else "(池内现无负向行)", int(_db.settlement.core.pool_clamp_lo)], wall_ok)
wall_detail if wall_detail != "" else "(池内现无负向行)", int(_db.combat.settlement.core.pool_clamp_lo)], wall_ok)
var hd_maps := 0
for eff_id in _db.settlement.core.effect_pool_map.keys():
if String(_db.settlement.core.effect_pool_map[eff_id]) == "hero_defense":
for eff_id in _db.combat.settlement.core.effect_pool_map.keys():
if String(_db.combat.settlement.core.effect_pool_map[eff_id]) == "hero_defense":
hd_maps += 1
Kit.check("hero_defense 池映射数==0(绊线钉——红=已加映射,去核上方护栏是否已同步激活)", hd_maps == 0)
# ③ 单位表 armor_class:域内校验(配置推导——正式投值归怪物/难度批,改表不断锚)
Kit.check("主角 armor_class 在域内(%d——暂定基准随表走)" % int(_db.hero.armor_class),
int(_db.hero.armor_class) >= 0 and int(_db.hero.armor_class) <= 30)
Kit.check("主角 armor_class 在域内(%d——暂定基准随表走)" % int(_db.combat.hero.armor_class),
int(_db.combat.hero.armor_class) >= 0 and int(_db.combat.hero.armor_class) <= 30)
var ac_band := true
var ac_detail := ""
var heavy_names := ["熊地精重击手", "灰矮人重装手"] # 逐行手设值 8/9,非精英编码位
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
var ac: int = int(row.get("armor_class", -1))
var dname := String(row.get("display_name", "?"))
var boss_row: bool = String(row.get("enemy_id", "")).ends_with("_chief") or \
@@ -325,7 +325,7 @@ func _smoke_ac_swap_guard() -> void:
ac_detail if ac_detail != "" else "无"], ac_band)
# ③b Boss 台阶断言(关卡即台阶——L1 8 < L2 10 < L3 12)+ 首遇位教学位低 AC(≤3)
var boss_ac := {}
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
var eid := String(row.get("enemy_id", ""))
if eid == "ogre_chief" or eid == "drider" or eid == "duergar_king":
boss_ac[int(row.get("level_index", 0))] = int(row.get("armor_class", 0))
@@ -337,7 +337,7 @@ func _smoke_ac_swap_guard() -> void:
# 曲线护栏 = 深度带升序相邻带「hp × 中位数量」总量比 ≤ 2.5(12/10/22/35/72 现行全绿;不用单调不回撤——会红在首带→次带)
var l12_band := _first_encounter_band(1, "bugbear")
var l12_row: Dictionary = {}
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
if int(row.get("level_index", -1)) == 1 and String(row.get("row_kind", "")) == "battle" \
and int(row.get("depth_band", -1)) == l12_band and String(row.get("enemy_id", "")) == "bugbear":
l12_row = row
@@ -350,7 +350,7 @@ func _smoke_ac_swap_guard() -> void:
and int(l12_row.get("armor_class", -1)) == 2 \
and l12_pool == {"attack": 6, "charge": 2, "heavy": 2})
var l1_totals := {}
for row in _db.enemy.rows:
for row in _db.combat.enemy.rows:
if int(row.get("level_index", -1)) != 1:
continue
var cnt_lo: int = int(row.get("unit_count", 1))
@@ -370,12 +370,12 @@ func _smoke_ac_swap_guard() -> void:
Kit.check("第一关相邻深度带总量比 ≤ 2.5(12/10/22/35/72——迭代 24 N1 曲线护栏,带键升序;越带%s)" % [
curve_detail if curve_detail != "" else "无"], curve_ok)
# ④ 掷点参数域(加载校验已过——此处钉形状语义)
var bounds: Array = _db.settlement.damage_roll.roll_tier_bounds
var discounts: Array = _db.settlement.damage_roll.roll_tier_discounts
var bounds: Array = _db.combat.settlement.damage_roll.roll_tier_bounds
var discounts: Array = _db.combat.settlement.damage_roll.roll_tier_discounts
Kit.check("掷点参数拍板值在位(d20 / nat20 暴击 ×200%% / 下限 1 / attack_bonus 0 / armor_roll 流)",
int(_db.settlement.damage_roll.roll_faces) == 20 and int(_db.settlement.damage_roll.crit_roll) == 20 \
and int(_db.settlement.damage_roll.crit_multiplier) == 200 and int(_db.settlement.damage_roll.damage_floor) == 1 \
and int(_db.settlement.damage_roll.attack_bonus) == 0 and String(_db.settlement.damage_roll.armor_roll_stream) == "armor_roll")
int(_db.combat.settlement.damage_roll.roll_faces) == 20 and int(_db.combat.settlement.damage_roll.crit_roll) == 20 \
and int(_db.combat.settlement.damage_roll.crit_multiplier) == 200 and int(_db.combat.settlement.damage_roll.damage_floor) == 1 \
and int(_db.combat.settlement.damage_roll.attack_bonus) == 0 and String(_db.combat.settlement.damage_roll.armor_roll_stream) == "armor_roll")
Kit.check("档位表拍板值在位(缺口 1-3 → ×0.75 · 4-7 → ×0.5 · 8+ → ×0.25——无零档)",
bounds == [3, 7] and discounts == [75, 50, 25])
@@ -385,24 +385,24 @@ func _smoke_ac_swap_guard() -> void:
func _smoke_variation_framework() -> void:
print("--- 地形变异 框架面(首批四类型 + 候选池门控 + 跨字段互锁)---")
Kit.check("变异清单 6 行(荆棘丛/藤蔓缠绕/岩浆/烬覆 + 落石/矿尘——首批类型定稿批 + 第三关内容批;贫矿脉 2026-10-16 用户令屏蔽删行)",
_db.variation.rows.size() == 6)
_db.combat.variation.rows.size() == 6)
Kit.check("变异表校验零错误(6 行全过域与跨字段互锁——含 scope=global 三互锁负样本)",
_db.variation.validate().is_empty())
_db.combat.variation.validate().is_empty())
var pool1_ids: Array = []
for r in _db.variant_pool(1, "dungeon"):
for r in _db.combat.variation.variant_pool(1, "dungeon"):
pool1_ids.append(String(r.variant_id))
Kit.check("L1 dungeon 候选池 = 荆棘丛+藤蔓缠绕(unlock + board_types 双闸)", pool1_ids == ["thorn", "vine"])
var pool2_ids: Array = []
for r in _db.variant_pool(2, "underdark"):
for r in _db.combat.variation.variant_pool(2, "underdark"):
pool2_ids.append(String(r.variant_id))
Kit.check("L2 underdark 候选池 = 岩浆+烬覆(第二关解锁)", pool2_ids == ["magma", "ash"])
Kit.check("unlock 门控:L1 不进 underdark 行(岩浆/烬覆第二关解锁)", _db.variant_pool(1, "underdark").is_empty())
Kit.check("unlock 门控:L1 不进 underdark 行(岩浆/烬覆第二关解锁)", _db.combat.variation.variant_pool(1, "underdark").is_empty())
var pool2d_ids: Array = []
for r in _db.variant_pool(2, "dungeon"):
for r in _db.combat.variation.variant_pool(2, "dungeon"):
pool2d_ids.append(String(r.variant_id))
Kit.check("board_types 门控:L2 dungeon 板池 = 荆棘丛+藤蔓缠绕(解锁累积;岩浆/烬覆被 board_types ∅ 交集排除)",
pool2d_ids == ["thorn", "vine"])
Kit.check("清单外 variant_id 返回空(读取通道在位)", _db.variation.row_by_id("nonexistent").is_empty())
Kit.check("清单外 variant_id 返回空(读取通道在位)", _db.combat.variation.row_by_id("nonexistent").is_empty())
# 跨字段互锁负样本(加载期致命——枚举语义一致性闸)
var vt = load("res://data/schemas/variation_table.gd").new()
vt.rows.assign([{"variant_id": "bad1", "display_name": "坏行甲", "unlock_level": 1, "scope": "region",
@@ -419,30 +419,30 @@ func _smoke_variation_framework() -> void:
func _smoke_difficulty_table() -> void:
print("--- 难度曲线与Boss·DifficultyTable 结构位 ---")
Kit.check("DifficultyTable MVP 空表加载(结构位)", _db.difficulty != null and _db.difficulty.rows.is_empty())
Kit.check("DifficultyTable MVP 空表加载(结构位)", _db.difficulty != null and _db.run.difficulty.rows.is_empty())
# ① 空表零行为:敌人系统.spawn 敌值 = EnemyTable 行值原样
var enemy_system = load("res://core/unit_container.gd").new(_db)
Kit.check("spawn 受理", enemy_system.spawn(1, 0, int(_db.hero.max_hp)))
Kit.check("spawn 受理", enemy_system.spawn({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)}))
var unit = enemy_system.all_enemies()[0]
var d0: Dictionary = _db.resolve_enemy_rowset(1, 0, "battle")[0] # 行集首行(带键寻址:战斗节点查 battle 键域)
var d0: Dictionary = _db.combat.enemy.resolve_rowset(1, 0, "battle")[0] # 行集首行(带键寻址:战斗节点查 battle 键域)
Kit.check("空表零修饰(hp=%d atk=%d = 行值 %d/%d)" % [int(unit.hp), int(unit.atk), int(d0.hp), int(d0.atk)],
int(unit.hp) == int(d0.hp) and int(unit.atk) == int(d0.atk))
# ② 修饰行运行时注入(debug 通道——R3 override 同族):套乘生效于 hp/atk
_db.difficulty.rows.append({"level_index": 1, "node_depth": 0, "hp_mult": 1.5, "atk_mult": 2.0})
_db.run.difficulty.rows.append({"level_index": 1, "node_depth": 0, "hp_mult": 1.5, "atk_mult": 2.0})
var enemy_system_b = load("res://core/unit_container.gd").new(_db)
Kit.check("修饰后 spawn 受理", enemy_system_b.spawn(1, 0, int(_db.hero.max_hp)))
Kit.check("修饰后 spawn 受理", enemy_system_b.spawn({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)}))
var g2 = enemy_system_b.all_enemies()[0]
Kit.check("hp 修饰 ×1.5(%d→%d,max_hp 同步)" % [int(d0.hp), int(g2.hp)],
int(g2.hp) == int(round(float(d0.hp) * 1.5)) and int(g2.max_hp) == int(g2.hp))
Kit.check("atk 修饰 ×2.0(%d→%d)" % [int(d0.atk), int(g2.atk)], int(g2.atk) == int(d0.atk) * 2)
# ③ 越域 clamp:乘数 50 → clamp 10
_db.difficulty.rows.clear()
_db.difficulty.rows.append({"level_index": 1, "node_depth": 0, "hp_mult": 50.0, "atk_mult": 1.0})
_db.run.difficulty.rows.clear()
_db.run.difficulty.rows.append({"level_index": 1, "node_depth": 0, "hp_mult": 50.0, "atk_mult": 1.0})
var enemy_system_c = load("res://core/unit_container.gd").new(_db)
Kit.check("越域修饰 spawn 受理", enemy_system_c.spawn(1, 0, int(_db.hero.max_hp)))
Kit.check("越域修饰 spawn 受理", enemy_system_c.spawn({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)}))
Kit.check("越域乘数 clamp 至 10(%d→%d)" % [int(d0.hp), int(enemy_system_c.all_enemies()[0].hp)],
int(enemy_system_c.all_enemies()[0].hp) == int(d0.hp) * 10)
_db.difficulty.rows.clear() # 还原空表(后续冒烟/关卡断言基线)
_db.run.difficulty.rows.clear() # 还原空表(后续冒烟/关卡断言基线)
# ---- ⑤ 格面资源九型 + gp_drop 流(黄金向量)----
@@ -455,7 +455,7 @@ func _smoke_nine_types_gp_stream() -> void:
var pos_weight := 0
var gp_pos := -1
var gp_neg := -1
for row in _db.reward.rows:
for row in _db.run.reward.rows:
if String(row.get("pool_tag", "")) == "cell":
cell_rows += 1
if String(row.get("polarity", "")) == "positive":
@@ -468,7 +468,7 @@ func _smoke_nine_types_gp_stream() -> void:
cell_rows == 18)
# 危险格负向直伤当量(负向≈正向 2 倍定价——真人反馈「三雷 3/4 血无后果感」的投值口径)
var mine_power := {"heal_backlash": 6, "pierce_snare": 8, "magic_recoil": 8} # 危险格负向直伤当量(抬档值;依据见 numfix 报告)
for row in _db.reward.rows:
for row in _db.run.reward.rows:
var rid := String(row.get("reward_id", ""))
if mine_power.has(rid):
Kit.check("危险格负向直伤 %s power=%d(卡⑧投值)" % [rid, int(mine_power[rid])],
@@ -477,7 +477,7 @@ func _smoke_nine_types_gp_stream() -> void:
# 合计即配置自身的和(改表自动跟随,不再硬编码合计值)。
var pos_all_in_pool := true
var neg_all_zero := true
for row in _db.reward.rows:
for row in _db.run.reward.rows:
if String(row.get("pool_tag", "")) != "cell":
continue
if String(row.get("polarity", "")) == "positive" and int(row.get("weight", 0)) < 1:
@@ -506,11 +506,11 @@ func _smoke_meta() -> void:
and meta_tbl.conversion.size() == 3 and meta_tbl.attune_refresh_upgrade.size() == 2)
var attrs_ok := true
for a in ["max_hp", "atk", "armor_class"]:
attrs_ok = attrs_ok and not _db.meta_conversion_row(a).is_empty()
attrs_ok = attrs_ok and not _conversion_row_of(_db.crossrun.meta.conversion, a).is_empty()
Kit.check("conversion attr 词表封闭恰全(三属性轴——meta_* 访问器可达)", attrs_ok)
var boss_ok := true
for li in [1, 2, 3]:
var brow: Dictionary = _db.meta_boss_keystone_row(li)
var brow: Dictionary = _db.crossrun.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))
Kit.check("boss_keystone L1–L3 行连续可达且 first_kill ≥ repeat_kill 全行(首杀全量/重复小额)", boss_ok)
@@ -519,3 +519,10 @@ class _TreeSaveMock:
# 核代码增补:最小 save 形态——attunement.slots 只读 tree_level("att_socketN")
func tree_level(n: String) -> int:
return 1 if n == "att_socket1" else 0
func _conversion_row_of(rows: Array, attr: String) -> Dictionary:
for r in rows:
if String(r.get("attr", "")) == attr:
return r
return {}
@@ -64,7 +64,7 @@ func _test_boss_name_uses_boss_rowset() -> void:
if not loaded:
return
var boss_row := {}
for row in db.enemy.rows:
for row in db.combat.enemy.rows:
if int(row.level_index) == 1 and String(row.get("row_kind", "")) == "boss":
boss_row = row
break
@@ -72,8 +72,8 @@ func _test_boss_name_uses_boss_rowset() -> void:
if boss_row.is_empty():
return
var boss_depth: int = int(boss_row.depth_band)
var boss_rows: Array = db.resolve_enemy_rowset(1, boss_depth, "boss")
var battle_rows: Array = db.resolve_enemy_rowset(1, boss_depth, "battle")
var boss_rows: Array = db.combat.enemy.resolve_rowset(1, boss_depth, "boss")
var battle_rows: Array = db.combat.enemy.resolve_rowset(1, boss_depth, "battle")
var boss_expect := String(boss_rows[0].get("display_name", ""))
var battle_name := String(battle_rows[0].get("display_name", ""))
var runner := _BossRunner.new()
@@ -58,12 +58,14 @@ func _run() -> void:
and rows.has("light") and rows.has("heavy") and rows.has("ward"))
# ③ 初始持有行(长剑/轻甲):购买钮现形带几何价、选中钮=当前禁用
# 两行同取谓词并集(轻甲行补 select_btn 非空 + buy_btn 可用两子句——原件少写,只增不减)
for pair in [["sword", "长剑"], ["light", "轻甲"]]:
var r: Dictionary = rows.get(pair[0], {})
Kit.check("%s行:购买钮「锻造 20」(0 级位几何价)+选中钮「当前」禁用〔锻造工坊批 V13〕" % pair[1],
r.get("buy_btn") != null and String(r.buy_btn.text) == "锻造 20" and not r.buy_btn.disabled
and r.get("select_btn") != null and String(r.select_btn.text) == "当前" and r.select_btn.disabled)
var rsw = rows.get("sword", {})
var rli = rows.get("light", {})
Kit.check("长剑行:购买钮「锻造 20」(0 级位几何价)+选中钮「当前」禁用〔锻造工坊批 V13〕",
rsw.get("buy_btn") != null and String(rsw.buy_btn.text) == "锻造 20" and not rsw.buy_btn.disabled
and rsw.get("select_btn") != null and String(rsw.select_btn.text) == "当前" and rsw.select_btn.disabled)
Kit.check("轻甲行:购买钮「锻造 20」+选中钮「当前」禁用〔锻造工坊批 V13〕",
rli.get("buy_btn") != null and String(rli.buy_btn.text) == "锻造 20"
and String(rli.select_btn.text) == "当前" and rli.select_btn.disabled)
# ④ 待解锁行(长枪/法杖/重甲/魔法护甲):解锁钮 200、无购买钮、无选中钮、状态含双通道文案
var rsp = rows.get("spear", {})
@@ -83,7 +85,7 @@ func _run() -> void:
rows.sword.buy_btn.disabled and rows.spear.unlock_btn.disabled)
# ⑥ 满级行:无购买钮、状态=已满级(直置存档域构造——两分法调试口径)
sv.set_forge_route_level("sword", 5)
sv.meta_state.set_forge_route_level("sword", 5)
ov.refresh_camp({"balance": 500, "forge": mp.forge_shop_status(),
"tree": [{"node_id": "forge_workshop", "level": 1}]})
rows = _row_by_id(ov)
@@ -51,8 +51,8 @@ func _init() -> void:
if FileAccess.file_exists(p):
DirAccess.remove_absolute(p)
var sm = main.get_node("/root/SaveManager")
sm.attach_path("user://test_pacing_queue_save.json")
sm.mark_first_adventure_done()
sm.slots.attach_path("user://test_pacing_queue_save.json")
sm.meta_state.mark_first_adventure_done()
# 行信号计数器(真实信号路径旁挂计数——零触碰既有接线)
var rows_seen: Array = []
main._start_adventure_from_menu()
@@ -56,8 +56,10 @@ func _sweep_narrator() -> void:
var head := String(narrator.node_curio_header())
Kit.check("奖励点头部含「冒险牌库」(u3 机制事实①同库)〔wa4〕", head.contains("冒险牌库"))
Kit.check("奖励点头部含「属性值提升与补给」(u3 机制事实②渠道分流)〔wa4〕", head.contains("属性值提升与补给"))
for w in ["同权重", "同上限", "提升池"]:
Kit.check("奖励点头部零「%s」旧话(u3 清零)〔wa4〕" % w, not head.contains(w))
Kit.check("奖励点头部含「同库分流」(u3 机制事实②措辞)〔wa4〕", head.contains("同库分流"))
Kit.check("奖励点头部零「同权重」旧话(u3 清零)〔wa4〕", not head.contains("同权重"))
Kit.check("奖励点头部零「同上限」旧话(u3 清零)〔wa4〕", not head.contains("同上限"))
Kit.check("奖励点头部零「提升池」旧话(u3 清零)〔wa4〕", not head.contains("提升池"))
Kit.check("专长卡兜底==「专长\n???」(u11 换代——数据缺行真实兜底路径)〔wa3〕",
String(narrator.feat_card_text({})) == "专长\n???")
@@ -70,11 +72,11 @@ func _sweep_legend() -> void:
if String(e[1]) == "封印":
seal_row = e
break
Kit.check("图例封印行存在(wa5 夹具前提)〔wa5〕", not seal_row.is_empty())
Kit.check("图例封印行含「标空」(u4——与「标中」成对)〔wa5〕", String(seal_row[2] if seal_row.size() > 2 else "").contains("标空"))
Kit.check("图例封印行含「受挖掘惩罚」(u4 雷字清零换代)〔wa5〕", String(seal_row[2] if seal_row.size() > 2 else "").contains("受挖掘惩罚"))
for w in ["插错", "挖雷"]:
Kit.check("图例封印行零「%s」〔wa5〕" % w,
not String(seal_row[2] if seal_row.size() > 2 else "").contains(w))
Kit.check("图例封印行零「插错」(u4 旗族遗留词干清零)〔wa5〕", not String(seal_row[2] if seal_row.size() > 2 else "").contains("插错"))
Kit.check("图例封印行零「挖雷」(u4 雷字清零)〔wa5〕", not String(seal_row[2] if seal_row.size() > 2 else "").contains("挖雷"))
# ⑤⑥ 来源名双源(u5/u6——状态槽消费半+表值 ConfigDB 真装载+battle_log 行消费半)
@@ -94,7 +96,7 @@ func _sweep_source_names() -> void:
row_view._tip_label.text.contains("来源:危险格"))
row_view._hide_tip()
Kit.check("结算表 source_label_map[mine_hit]==「危险格」(u6 换代——ConfigDB 真装载)〔wa7〕",
String(_db.settlement.core.source_label_map.get("mine_hit", "")) == "危险格")
String(_db.combat.settlement.core.source_label_map.get("mine_hit", "")) == "危险格")
var log_view = load("res://presentation/views/battle_log_view.gd").new(_db)
var row_txt := String(log_view._format_row({"type": "effect", "source_class": "mine_hit",
"display_name": "挖掘反噬", "value": -5, "pool_value": -5, "round_index": 1}))
@@ -81,7 +81,7 @@ func _init() -> void:
stage.on_step_row_fx({"type": "damage", "target": "地精", "target_id": "goblin_b"})
await create_timer(0.6).timeout # 弹道到达拍 0.33s + 余量
Kit.check("伤害行按 target_id 命中正确站桩(闪白落点)〔wd12〕",
s2.fx_probe().flash_count == 1 and s1.fx_probe().flash_count == 0)
s2._flash_count == 1 and s1._flash_count == 0)
# 口二:enemy_standee_center —— id 先、display_name 兜底
var c_b = stage.enemy_standee_center("goblin_b", "地精")
var c_b_expect: Vector2 = stage.world_to_shell(s2.position + s2.box * 0.5)
@@ -91,16 +91,14 @@ func _init() -> void:
Kit.check("无 target_id 时按名兜底同旧行为(首命中)〔wd12〕", c_fallback != null and c_fallback == c_fallback_expect)
# ---- ① 意图蓄力档可达(d2)----
# 蓄力判据表:[kind, skill_id, 期望 _intent_charge, 个案标签]
var charge_cases := [
["damage", "heavy", true, "蓄力技能(heavy)置 _intent_charge=true"],
["damage", "goblin_slash", false, "非蓄力攻击技(goblin_slash)零置位"],
["buff", "goblin_rage", false, "增益系零置位"],
["damage", "basic", false, "普攻零表示路径复位 _intent_charge"],
]
for c in charge_cases:
s1.set_pending(c[0], c[1])
Kit.check("%s〔wd2〕" % c[3], s1._intent_charge == c[2])
s1.set_pending("damage", "heavy")
Kit.check("蓄力技能(heavy)置 _intent_charge=true〔wd2〕", s1._intent_charge)
s1.set_pending("damage", "goblin_slash")
Kit.check("非蓄力攻击技(goblin_slash)零置位〔wd2〕", not s1._intent_charge)
s1.set_pending("buff", "goblin_rage")
Kit.check("增益系零置位〔wd2〕", not s1._intent_charge)
s1.set_pending("damage", "basic")
Kit.check("普攻零表示路径复位 _intent_charge〔wd2〕", not s1._intent_charge)
# ---- ② 庆祝 tween 句柄纪律(d4)----
var ov = BattleEndScript.new()
@@ -27,7 +27,7 @@ func _init() -> void:
return
# board_type 词表自 board_gen 行集推导(禁测试侧写死词表)
var types: Array = []
for row in db.board_gen.rows:
for row in db.combat.board_gen.rows:
var bt := String(row.get("board_type", ""))
if bt != "" and not types.has(bt):
types.append(bt)
@@ -611,6 +611,18 @@ func _smoke_button_five_states() -> void:
is_equal_approx((dds as StyleBoxFlat).bg_color.a, 0.45))
Kit.check("V1 危险钮 disabled≠pressed", _sb_content_differs(dpr, dds))
db.free()
# V2 主题红线维持(Button 类型词条零接触+focus 族词条零定义)——与 _smoke_global_theme 同判据复述
var th := ThemeDB.get_project_theme()
if th != null:
Kit.check("V2 红线维持 Button 类型词条零接触", not th.get_type_list().has("Button"))
var focus_hits := ""
for t in th.get_type_list():
for nm in (th.get_stylebox_list(t) + th.get_icon_list(t) + th.get_color_list(t)
+ th.get_font_list(t) + th.get_font_size_list(t) + th.get_constant_list(t)):
if "focus" in String(nm):
focus_hits += "%s/%s " % [t, nm]
Kit.check("V2 红线维持 focus 族词条零定义%s" % ("" if focus_hits == "" else "(违例:" + focus_hits + ")"),
focus_hits == "")
# ---- 全局纹理过滤冒烟(工程设置 Nearest+平滑渐变件 LINEAR 白名单;V3)----
@@ -819,21 +831,21 @@ func _career_overlay_projection() -> void:
var mp = load("res://core/meta_progression.gd").new(_db, sv)
# 已玩态夹具:两局收束(一通一止)+ 钥石非零——投影可辨非全零白板
# (生涯页不统计天赋树与账号成长态,只统计终身计数)
sv.add_keystone(45)
sv.apply_adventure_summary({"result": "completed", "cleared": true, "levels_cleared": 3,
sv.meta_state._add_keystone(45)
sv.meta_state.apply_adventure_summary({"result": "completed", "cleared": true, "levels_cleared": 3,
"stopped_at": 0, "keystone_total": 0, "master_seed": 21})
sv.apply_adventure_summary({"result": "abandoned", "cleared": false, "levels_cleared": 1,
sv.meta_state.apply_adventure_summary({"result": "abandoned", "cleared": false, "levels_cleared": 1,
"stopped_at": 2, "keystone_total": 0, "master_seed": 22})
sv.write_to_disk()
# 投影拼装(HudPresenter.career_data 同构)+ 视图喂入
var data := {"total": sv.adventure_total(), "cleared": sv.adventure_cleared(),
"clears": [], "boss_first_kills": 0, "boss_first_total": _db.level_count(),
"boss_kills": sv.boss_kill_count(), "keystone": mp.keystone_balance()}
for i in _db.level_count():
var data := {"total": sv.meta_state.career_state().adventure_total, "cleared": sv.meta_state.career_state().adventure_cleared,
"clears": [], "boss_first_kills": 0, "boss_first_total": _db.run.level.level_count(),
"boss_kills": sv.meta_state.career_state().boss_kill_count, "keystone": mp.meta_state.keystone_balance()}
for i in _db.run.level.level_count():
var li: int = i + 1
data["clears"].append({"display_name": String(_db.level_row(li).get("display_name", "")),
"count": sv.clear_count(li)})
if sv.boss_first_kill_marked(li):
data["clears"].append({"display_name": String(_db.run.level.row_by_level_index(li).get("display_name", "")),
"count": sv.meta_state.career_state().clear_counts.get(str(li), 0)})
if sv.meta_state.career_state().boss_first_kill.has(str(li)):
data["boss_first_kills"] += 1
var co = load("res://presentation/views/career_overlay.gd").new()
co.open_career(data)
@@ -850,7 +862,7 @@ func _career_overlay_projection() -> void:
Kit.check("Boss 首杀/总杀投影 = 首杀标记计数 + boss_kill_count()(%s / %s)" % [co._boss_first_label.text, co._boss_kills_label.text],
String(co._boss_first_label.text) == "3/3 关" and String(co._boss_kills_label.text) == "4 次")
Kit.check("钥石余额徽投影 = keystone_balance() 读数(%s)" % co._keystone_badge.value_label.text,
String(co._keystone_badge.value_label.text) == "钥石 %d" % mp.keystone_balance())
String(co._keystone_badge.value_label.text) == "钥石 %d" % mp.meta_state.keystone_balance())
# 口径换代(2026-10-26):行标签换代钉子+生涯页零「通关」残留(负断言兜全页)
var career_label_texts: Array[String] = []
for career_lbl: Label in _all_labels(co):
@@ -869,14 +881,14 @@ func _career_overlay_projection() -> void:
fv5c.store_string(JSON.stringify({"version": 5, "meta": {"keystone_balance": 3}}))
fv5c.close()
Kit.check("v5 现役档升 v6 载入成功(计数域缺省补零)",
v5.load_from_disk() and v5.adventure_total() == 0 and v5.adventure_cleared() == 0)
v5.load_from_disk() and v5.meta_state.career_state().adventure_total == 0 and v5.meta_state.career_state().adventure_cleared == 0)
var mp_v5 = load("res://core/meta_progression.gd").new(_db, v5)
var data_v5 := {"total": v5.adventure_total(), "cleared": v5.adventure_cleared(),
"clears": [], "boss_first_kills": 0, "boss_first_total": _db.level_count(),
"boss_kills": v5.boss_kill_count(), "keystone": mp_v5.keystone_balance()}
for i in _db.level_count():
data_v5["clears"].append({"display_name": String(_db.level_row(i + 1).get("display_name", "")),
"count": v5.clear_count(i + 1)})
var data_v5 := {"total": v5.meta_state.career_state().adventure_total, "cleared": v5.meta_state.career_state().adventure_cleared,
"clears": [], "boss_first_kills": 0, "boss_first_total": _db.run.level.level_count(),
"boss_kills": v5.meta_state.career_state().boss_kill_count, "keystone": mp_v5.meta_state.keystone_balance()}
for i in _db.run.level.level_count():
data_v5["clears"].append({"display_name": String(_db.run.level.row_by_level_index(i + 1).get("display_name", "")),
"count": v5.meta_state.career_state().clear_counts.get(str(i + 1), 0)})
var co_v5 = load("res://presentation/views/career_overlay.gd").new()
co_v5.open_career(data_v5)
Kit.check("旧档载入后生涯页正常显示(v5 升 v6:总局数/冒险成功局数显示 0,既有钥石余额原样呈现)",
+10 -3
View File
@@ -64,10 +64,12 @@ func _test_grouping_closed() -> void:
for bad in ["keystone", "", "ATTACK", "aoe", "blast_mine", " heal"]:
Kit.check("清单外值「%s」→ GLOW_NONE(禁静默空光)" % bad,
UiKitScript.glow_group_for_cell_type(bad) == UiKitScript.GLOW_NONE)
# 与卡顶带 tg_band 的归组差异锚定(Q3 甲案两带并存、禁互引):blast 在卡顶带归魔法(E5 回滚既存行为)
# ——格面带 blast→群伤由上方九型封闭循环的 TYPE_EXPECT["blast"] 承载,此处不重述
# 与卡顶带 tg_band 的归组差异锚定(Q3 甲案两带并存、禁互引):blast 在格面带归群伤、
# 在卡顶带归魔法(E5 回滚既存行为)——若将来误把两带统一,本断言红
var band_blast: Color = UiKitScript.tg_band("blast_charge")
Kit.check("两带并存锚定:卡顶带 blast→TG_MAGIC(E5 回滚既存行为未动)", band_blast == UiKitScript.TG_MAGIC)
Kit.check("两带并存锚定:格面微光 blast→群伤组(与卡顶带刻意不同)",
UiKitScript.glow_group_for_cell_type("blast") == UiKitScript.GLOW_AOE)
# 色值出口:五组各取本组常量;无组/越界组号 → 全透明(不画)
Kit.check("glow_color_for_group 群伤组 = TG_GLOW_AOE",
UiKitScript.glow_color_for_group(UiKitScript.GLOW_AOE) == UiKitScript.TG_GLOW_AOE)
@@ -90,8 +92,13 @@ func _test_frozen_hue_face() -> void:
var ds: float = absf(m.s - f.s)
Kit.check("冻结色相保持:%s ΔH=%.3f° ≤0.2° 且 ΔS=%.4f ≤0.01(只动明度档)" % [g, dh * 360.0, ds],
dh <= 0.2 / 360.0 and ds <= 0.01)
# 群伤组专锚(#A61D37):V=0.65 明度档——H/S 两腿由上方逐组循环的 ΔH/ΔS 阈承载
# 群伤组专锚(#A61D37):H/S 与冻结逐位一致、V=0.65 档
var aoe_f: Color = FROZEN["AOE"]
var aoe_m: Color = UiKitScript.TG_GLOW_AOE
Kit.check("群伤组专项:H 与冻结 #FF2D55 差 %.4f°(零移动)" % (absf(aoe_m.h - aoe_f.h) * 360.0),
absf(aoe_m.h - aoe_f.h) < 0.001)
Kit.check("群伤组专项:S 与冻结差 %.4f ≤0.01(8bit 量化噪声级)" % absf(aoe_m.s - aoe_f.s),
absf(aoe_m.s - aoe_f.s) <= 0.01)
Kit.check("群伤组专项:V=0.65 明度档( provisional 域内)", absf(aoe_m.v - 166.0 / 255.0) < 0.002)
@@ -15,19 +15,10 @@ func _init() -> void:
var f: Font = UiKitScript.load_font(path)
Kit.check("随包字体守卫装载非空(res://assets/fonts/ui_font_fusion_pixel_zh_hans.ttf)", f != null)
if f != null:
# 字形集表:[个案标签, 须在集字符]——缺字明细进消息
var glyph_groups := [
["中文字形在集(攻/雷/旗/封)", "攻雷旗封"],
["中文字形在集(危——已开危险格字形换代前置,迭代36 u12/wa9)", "危"],
["ASCII/数字字形在集(A/0/%)", "A0%"],
]
for g in glyph_groups:
var missing := ""
for i in String(g[1]).length():
var ch := String(g[1])[i]
if not f.has_char(ch.unicode_at(0)):
missing += ch
Kit.check("%s(缺=%s)" % [g[0], missing if missing != "" else "无"], missing == "")
Kit.check("中文字形在集(攻/雷/旗/封)", f.has_char("攻".unicode_at(0)) and f.has_char("雷".unicode_at(0)) \
and f.has_char("旗".unicode_at(0)) and f.has_char("封".unicode_at(0)))
Kit.check("中文字形在集(危——已开危险格字形换代前置,迭代36 u12/wa9)", f.has_char("危".unicode_at(0)))
Kit.check("ASCII/数字字形在集(A/0/%)", f.has_char("A".unicode_at(0)) and f.has_char("0".unicode_at(0)) and f.has_char("%".unicode_at(0)))
var sz := 18
var w_cn: float = f.get_string_size("战斗胜利", HORIZONTAL_ALIGNMENT_LEFT, -1, sz).x
var w_label: float = f.get_string_size("攻击 2", HORIZONTAL_ALIGNMENT_LEFT, -1, sz).x
File diff suppressed because it is too large. Load diff