合并(迭代44): 组合根注册面批+战斗编排归约批+测速循环批v1.1三卡合回——S13零豁免三键真实清偿(main.gd 49→15/battle_orchestrator 38→15/tune_sim裸键消除)·批末全量域闸13/14绿(02/05/09三闸从基线红改善归绿·唯一红08c为并行写卡会话在飞件)·独立验收审计通过·卡③v1.1停卡重裁实况见实现报告§5

This commit is contained in:
DKChan committed 2026-10-30 16:23:46 +08:00
commit bee73db51b
54 files changed
+1833 -1226

No files matched your search

+1
View File
@@ -187,6 +187,7 @@ func _suites() -> Array:
{"path": "res://tests/unit/core/level_map_type_vis_test.gd", "expect": 0}, # 信息迷雾——类型可见性(层距 N+Boss/候选豁免+单圈+N 读表+续玩同值)
{"path": "res://tests/unit/presentation/pacing_queue_test.gd", "expect": 0}, # AC⑤/T9——排空零分叉/零丢行/延迟双态
{"path": "res://tests/unit/presentation/standee_fx_sweep_test.gd", "expect": 0}, # 呈现域缺陷扫掠(d2 蓄力档/d4 庆祝句柄/d5+d8 互斥/d12 站桩按 id 解析)
{"path": "res://tests/unit/presentation/tune_sim_batch_parity_test.gd", "expect": 0}, # 测速循环批双实现对拍(帧索引/运行态/进度串/stats/报告五面——信号步进 vs await 基线)
{"path": "res://tests/unit/presentation/standee_present_values_test.gd", "expect": 0}, # 战斗一屏样张批 V5–V9(血条危险红/名字行素底不压地台/徽标 FS_HINT/护甲 PNG 绑定/素材入包)
{"path": "res://tests/unit/presentation/reward_overlay_test.gd", "expect": 0}, # 奖励节点 UI 直验(盲区补漏)
{"path": "res://tests/unit/presentation/level_map_info_fog_test.gd", "expect": 0}, # 信息迷雾——呈现显隐/Boss 豁免/详情零显示/大图拟合
+4 -4
View File
@@ -67,9 +67,9 @@ func _check_main_scene(main: Node) -> void:
return
var run_scope: Node = main.get("run_scope")
check(run_scope != null and run_scope.is_inside_tree(), "闲时会话 run_scope 在树")
var start_overlay: Control = main.get("_start_overlay")
var start_overlay: Control = main.get("ui").screens.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)")
check(main.get("legend_overlay") != null, "图例覆盖层已装配(legend_overlay)")
check(main.get("status_panel") != null, "局内状态面板已装配(status_panel)")
check(main.get("ui").screens.debug_badge != null, "调试入口角标已装配(debug_badge)")
check(main.get("ui").modals.legend_overlay != null, "图例覆盖层已装配(legend_overlay)")
check(main.get("ui").modals.status_panel != null, "局内状态面板已装配(status_panel)")
+2 -2
View File
@@ -314,7 +314,7 @@ fi
# get_tree());排除注释行(行首 #/##)与字符串字面量(双引号包裹段内的 await 非代码),
# 口径仿 S1/S2 的 grep 排除手法。
# 自检:清单内文件缺位(路径写错/被改名)即判红——防扫面为空静默绿。
S8_FILES="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/damage_controller.gd"
S8_FILES="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/settlement/round_cycle.gd game/core/damage_controller.gd"
S8_MISS=0
for f in $S8_FILES; do
if [ ! -f "$f" ]; then
@@ -356,7 +356,7 @@ fi
# 自检:扫描面为空即判红(require_scan_face);B1 文件集与三族自家文件缺位即判红
# (文件被改名=豁免名单失效,宁可红不可静默放行)。
if require_scan_face "S7" "$DIR_CORE"; then
S7_B1_SET="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/damage_controller.gd"
S7_B1_SET="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/settlement/round_cycle.gd game/core/damage_controller.gd"
S7_MISS=0
for f in $S7_B1_SET game/core/unit_container.gd game/core/status_container.gd game/core/reward_economy.gd; do
if [ ! -f "$f" ]; then
+41 -41
View File
@@ -81,20 +81,20 @@ func _smoke_scene_split() -> void:
# V1a 启动态顶层断言:boot 后自树顶向下第一个可见内容层=start_overlay(headless 可判)
#(内容层遍历——屏壳是容器不当候选:camp 壳 boot 态 vis=true 但成员恒隐,裸树遍历会误命中)
var top: Node = _topmost_visible_layer(main)
check(top == main._start_overlay,
check(top == main.ui.screens.start_overlay,
"V1a 启动态顶层可见层=start_overlay(实测顶层=%s——菜单被冒险屏盖住即红)〔装配生命批 V1〕"
% (top.name if top != null else "null"))
# V1b 语义关系:start_overlay 在冒险屏内容之上(菜单屏壳在冒险屏壳之上——装配序修复判据)
check(main._menu_screen.get_index() > main._adventure_screen.get_index(),
check(main.ui.screens.menu_screen.get_index() > main.ui.screens.adventure_screen.get_index(),
"V1b 菜单屏壳在冒险屏壳之上(start_overlay 盖住战斗内容)〔装配生命批 V1〕")
# V1c 合同树序链逐对保序(直接子层全链=屏壳/模态/全屏态/非视觉节点;宿住层树序随宿主不参与)
var chain_detail := ""
var chain_prev: Node = null
var chain_ok := true
for entry: Dictionary in main.LAYER_CONTRACT:
for entry: Dictionary in main.ui.LAYER_CONTRACT:
if entry.has("host"):
continue
var node: Node = main._layer_node(String(entry["shell"]) if entry.has("shell") else String(entry["key"]))
var node: Node = main.ui.node(String(entry["shell"]) if entry.has("shell") else String(entry["key"]))
if node == null:
chain_ok = false
chain_detail += " 缺失:%s" % String(entry["key"])
@@ -105,7 +105,7 @@ func _smoke_scene_split() -> void:
chain_prev = node
check(chain_ok, "V1c 合同树序链逐对保序(运行期树序==LAYER_CONTRACT 序;违例:%s)〔装配生命批 V1〕" % chain_detail)
# ③ 探针挂载假设句柄在座(main.X 一级路径——探针迁移面收口证明)
check(main._start_overlay == main._menu_screen.start_overlay and main.camp_overlay == main._camp_screen.camp_overlay and main.level_screen == main._adventure_screen.level_screen and main._battle_root == main._adventure_screen.battle_root and main.board_view == main._adventure_screen.board_view,
check(main.ui.screens.start_overlay == main.ui.screens.menu_screen.start_overlay and main.ui.modals.camp_overlay == main.ui.screens.camp_screen.camp_overlay and main.ui.modals.level_screen == main.ui.screens.adventure_screen.level_screen and main.ui.screens.battle_root == main.ui.screens.adventure_screen.battle_root and main.ui.battle.board_zone.board_view == main.ui.screens.adventure_screen.board_view,
"V2 组合根句柄指向屏壳成员(探针挂载假设 main.X 零迁移)")
check(main.run_scope != null and not main.run_scope.is_adventure,
"V2 闲时会话自 _ready 即在(永非 null;迭代 27 T3——gp 影子账退役无账本成员)")
@@ -116,10 +116,10 @@ func _smoke_scene_split() -> void:
# 顶层可见内容层(V1a 用——候选=合同模态/全屏态+两屏壳的 boot 内容成员;屏壳本身是容器不当候选:
# camp 壳 boot 态 vis=true 但成员恒隐,裸树遍历自顶向下会先命中透明壳)
func _topmost_visible_layer(main: Node) -> Node:
var candidates: Array[Node] = [main._start_overlay, main._battle_root, main._inter_overlay,
main.legend_overlay, main.node_overlay, main.level_screen, main.curio_overlay,
main._end_overlay, main.shop_overlay, main.camp_overlay, main.settings_overlay,
main.status_panel, main.career_overlay, main.confirm_dialog, main.debug_overlay]
var candidates: Array[Node] = [main.ui.screens.start_overlay, main.ui.screens.battle_root, main.ui.screens.inter_overlay,
main.ui.modals.legend_overlay, main.ui.modals.node_overlay, main.ui.modals.level_screen, main.ui.modals.curio_overlay,
main.ui.modals.end_overlay, main.ui.modals.shop_overlay, main.ui.modals.camp_overlay, main.ui.modals.settings_overlay,
main.ui.modals.status_panel, main.ui.modals.career_overlay, main.ui.modals.confirm_dialog, main.ui.modals.debug_overlay]
var best: Node = null
var best_x := -1
var best_y := -1
@@ -167,7 +167,7 @@ func _smoke_boot_chain() -> void:
"V2b 设置已应用·减动效(UiKit.motion_scale=%s=settings_store.reduced_motion=%s)〔装配生命批 V2〕" % [
str(ui_kit.motion_scale), str(ss.reduced_motion)])
# ③ 启动态菜单可见
check(main._start_overlay.visible,
check(main.ui.screens.start_overlay.visible,
"V2c 启动态 _start_overlay.visible=true(boot 后玩家见菜单)〔装配生命批 V2〕")
# ④ 续玩钮可见态==SaveManager 断点在档态
var sm = main.get_node("/root/SaveManager")
@@ -186,22 +186,22 @@ func _smoke_boot_chain() -> void:
if FileAccess.file_exists(p):
DirAccess.remove_absolute(p)
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,
check(main.ui.screens.menu_screen.entersavebtn != null and main.ui.screens.menu_screen.settingsbtn != null
and main.ui.screens.menu_screen.quitbtn != null,
"V2d 三钮在位·进入存档/设置/退出游戏(续玩钮族退役后主菜单操作组承接)〔卡② mn1/mn8〕")
for old_btn in ["lvbtn", "resumebtn", "campbtn", "careerbtn"]:
check(not (old_btn in main._menu_screen),
check(not (old_btn in main.ui.screens.menu_screen),
"V2d 四旧钮句柄零命中·%s(彻底撤出——禁隐藏态禁置灰态残留)〔卡② mn2〕" % old_btn)
check(_count_visible_buttons(main._start_overlay) == 3,
check(_count_visible_buttons(main.ui.screens.start_overlay) == 3,
"V2d 渲染面可见按钮计数==3(旧钮接回即红——渲染面锚,句柄缝盲区闭合)〔卡② mn1/mn2〕")
# ⑤ 闲时会话(_ready 即建、永非 null、在树)
check(main.run_scope != null and not main.run_scope.is_adventure and main.run_scope.is_inside_tree(),
"V2e run_scope 非空且为闲时会话(is_adventure=false+is_inside_tree)〔装配生命批 V2〕")
# ⑥ 调试浮窗 boot 不开+菜单角标在(菜单优先——boot 不自动开浮窗,
# 入口=开始屏右下角「调试」角标+对局中两屏角标)
check(not main.debug_overlay.visible,
check(not main.ui.modals.debug_overlay.visible,
"V2f 调试浮窗 boot 不自动开(菜单优先)〔2026-10-16 用户令〕")
check(main.debug_badge != null and not main.debug_badge.visible,
check(main.ui.screens.debug_badge != null and not main.ui.screens.debug_badge.visible,
"V2f 调试浮层角标发布态恒隐(2026-10-25 删写死行批)")
main.queue_free()
await process_frame
@@ -223,7 +223,7 @@ func _smoke_run_lifecycle() -> void:
sm.slots.attach_path("user://smoke_run_lifecycle.json")
sm.meta_state.mark_first_adventure_done()
# ---- 第一局:开局→注脏→放弃→终局出口→会话收束 ----
main.debug_overlay.seed_edit.text = "42"
main.ui.modals.debug_overlay.seed_edit.text = "42"
main._start_adventure_from_menu()
await process_frame
check(main.run_scope != null and main.run_scope.is_adventure and main.run_scope.adventure != null,
@@ -243,22 +243,22 @@ func _smoke_run_lifecycle() -> void:
break
await process_frame
check(finished.done, "V3 冒险终局到达(abandon→adventure_finished 同步链)")
check(main._end_overlay.is_open(), "V3 终局面板弹起")
check(main.ui.modals.end_overlay.is_open(), "V3 终局面板弹起")
main._on_battle_end_continue() # 终局出口(回菜单+会话收束——手工置空清单已消亡)
await process_frame
check(not main.run_scope.is_adventure and main.run_scope.adventure == null and main.run_scope.runner == null,
"V3 会话收束(新闲时会话零冒险/关卡对象)")
check(main.run_scope.hints_shown.is_empty() and not main.run_scope.boss_active and main.run_scope.last_gain_amount == 0,
"V3 局态封印/账本全复位(queue_free 物化——注入脏状态零残留)")
check(main._start_overlay.visible, "V3 回开始菜单(终局出口路径)")
check(main.ui.screens.start_overlay.visible, "V3 回开始菜单(终局出口路径)")
# ---- 第二局:放弃终局无断点残留→直进开局→全新会话 ----
check(not sm.has_breakpoint(), "V3 放弃终局清断点(无残留断点)")
main.debug_overlay.seed_edit.text = "43"
main.ui.modals.debug_overlay.seed_edit.text = "43"
main._start_adventure_from_menu()
await process_frame
check(main.run_scope.is_adventure and main.run_scope.adventure != null and main.run_scope.hints_shown.is_empty(),
"V3 第二连局全新会话(两连局零泄漏)")
check(not main._start_overlay.visible, "V3 直进入局(菜单隐)")
check(not main.ui.screens.start_overlay.visible, "V3 直进入局(菜单隐)")
main.queue_free()
await process_frame
for p in ["user://smoke_run_lifecycle.json", "user://smoke_run_lifecycle.json.tmp"]:
@@ -281,14 +281,14 @@ func _smoke_sim_guards() -> void:
# wd6:闲时态 + 模拟运行态 → 调试两层入口守卫早退(开始层不收起、无结算对象)
check(main.run_scope.settlement == null and not main.run_scope.is_adventure,
"前置:闲时会话(settlement 空 + is_adventure=false)")
main._start_overlay.visible = true
main.ui.screens.start_overlay.visible = true
main._tune_sim._running = true
main._start_debug_levels()
check(main.run_scope.settlement == null and main._start_overlay.visible,
check(main.run_scope.settlement == null and main.ui.screens.start_overlay.visible,
"模拟运行态下调试两层入口守卫早退(开始层不收起)〔wd6〕")
main._tune_sim._running = false
# wd7:真实冒险关间态(is_adventure=true、settlement==null)→ 模拟入口守卫早退
main.debug_overlay.seed_edit.text = "42"
main.ui.modals.debug_overlay.seed_edit.text = "42"
main._start_adventure_from_menu()
await process_frame
check(main.run_scope.is_adventure and main.run_scope.settlement == null,
@@ -313,8 +313,8 @@ func _face_snapshot(main: Node) -> Dictionary:
var tu = main._treasure_ui
var bo = main._bonus
var cp = main._camp_pres
var mf = main._modal_flow
var rp = main._row_pacer
var mf = main.ui.modals.flow
var rp = main.ui.screens.row_pacer
return {
"treasure_settlement": tu._settlement_get.call(),
"treasure_runner": tu._runner_get.call(),
@@ -381,13 +381,13 @@ func _stage_faces(main: Node, tag: String, adv: String, run: String, set: String
if bool(snap["mf_rows_empty"]) != bool(extra[k]):
bad += " modal_flow.rows_empty_get 应=%s" % str(extra[k])
"level_vis":
if main.level_screen.visible != bool(extra[k]):
if main.ui.modals.level_screen.visible != bool(extra[k]):
bad += " level_screen.visible 应=%s" % str(extra[k])
"end_open":
if main._end_overlay.is_open() != bool(extra[k]):
if main.ui.modals.end_overlay.is_open() != bool(extra[k]):
bad += " _end_overlay.is_open 应=%s" % str(extra[k])
"start_vis":
if main._start_overlay.visible != bool(extra[k]):
if main.ui.screens.start_overlay.visible != bool(extra[k]):
bad += " _start_overlay.visible 应=%s" % str(extra[k])
check(bad.is_empty(), "%s(引用面违例:%s)〔装配生命批 V3〕" % [tag, bad])
@@ -419,7 +419,7 @@ func _smoke_lifecycle_matrix() -> void:
if FileAccess.file_exists(p):
DirAccess.remove_absolute(p)
sm.slots.attach_path("user://smoke_lifecycle.json")
main.debug_overlay.seed_edit.text = "42"
main.ui.modals.debug_overlay.seed_edit.text = "42"
# 阶段①闲时
await _stage_faces(main, "V3a 闲时", "null", "null", "null",
{"mf_start": true, "mf_inter": false, "mf_rows_empty": true, "start_vis": true})
@@ -430,7 +430,7 @@ func _smoke_lifecycle_matrix() -> void:
await _stage_faces(main, "V3b 开局停点", "alive", "alive", "null",
{"mf_start": false, "level_vis": true})
# 阶段③地图选路→战斗中(真实选路提交 press_candidate)
main.level_screen.press_candidate(0)
main.ui.modals.level_screen.press_candidate(0)
for i in range(10):
await process_frame
await _stage_faces(main, "V3c 战斗中", "alive", "alive", "alive", {"mf_start": false})
@@ -465,9 +465,9 @@ func _smoke_lifecycle_matrix() -> void:
check(main.run_scope.settlement == null or not main.run_scope.settlement.is_battle_live(),
"V3d 活战斗驱动完毕(战斗已收场——%d 轮,finished=%s)〔装配生命批 V3〕" % [guard, str(finished.done)])
await process_frame
main._flush_row_queue()
main.ui.screens.row_pacer.flush()
for i in range(12):
if main._end_overlay.is_open():
if main.ui.modals.end_overlay.is_open():
break
await process_frame
# 分支甲:战斗胜利停点(结算后)→确认进选路→放弃→终局面
@@ -482,13 +482,13 @@ func _smoke_lifecycle_matrix() -> void:
if finished.done:
break
await process_frame
main._flush_row_queue()
main.ui.screens.row_pacer.flush()
for i in range(8):
if main._end_overlay.is_open():
if main.ui.modals.end_overlay.is_open():
break
await process_frame
# 阶段⑤终局面(败北/放弃皆达)
check(finished.done and main._end_overlay.is_open(),
check(finished.done and main.ui.modals.end_overlay.is_open(),
"V3e 终局面到达(adventure_finished+终局面板开)〔装配生命批 V3〕")
await _stage_faces(main, "V3e 终局面", "alive", "alive", "null", {"mf_start": false, "end_open": true})
# 阶段⑥回菜单收束(旧局消亡+新会话即驻+无真空窗)
@@ -515,7 +515,7 @@ func _smoke_lifecycle_matrix() -> void:
main._open_save_select()
for i in range(2):
await process_frame
var ssel_new: Button = main.save_select_overlay._new_btns.get(1)
var ssel_new: Button = main.ui.modals.save_select_overlay._new_btns.get(1)
if ssel_new == null:
check(false, "V3g 营地入口缺失(选档层新档钮驱动中断)〔装配生命批 V3〕")
continue
@@ -551,7 +551,7 @@ func _smoke_lifecycle_matrix() -> void:
DirAccess.remove_absolute(slot_p)
# 阶段⑧两连局(第二局全新会话——旧局对象不复用、脏态零残留)
var first_adv = main.run_scope.adventure
main.debug_overlay.seed_edit.text = "43"
main.ui.modals.debug_overlay.seed_edit.text = "43"
main._start_adventure_from_menu()
for i in range(6):
await process_frame
@@ -560,12 +560,12 @@ func _smoke_lifecycle_matrix() -> void:
await _stage_faces(main, "V3h 两连局·开局停点", "alive", "alive", "null", {"mf_start": false, "level_vis": true})
check(main.run_scope.adventure.abandon(), "V3h 放弃受理(第二局)〔装配生命批 V3〕")
for i in range(12):
if main._end_overlay.is_open():
if main.ui.modals.end_overlay.is_open():
break
await process_frame
main._flush_row_queue()
main.ui.screens.row_pacer.flush()
for i in range(8):
if main._end_overlay.is_open():
if main.ui.modals.end_overlay.is_open():
break
await process_frame
await _stage_faces(main, "V3h 两连局·终局面", "alive", "alive", "null", {"mf_start": false, "end_open": true})
@@ -34,9 +34,31 @@ func _init() -> void:
_smoke_pool_wiring()
_smoke_damage_types()
_smoke_attack_chain_fresh_pool()
_smoke_hero_atk_derived()
quit(Kit.finish("roll_multiplier_test"))
# ---- 主角攻击基值派生读(注册面批同代编排归约 V4——_hero_atk 快照成员退役改派生读:
# enc 携 hero_atk 键受理后经派生读供 spawn 与攻击单 base_value 两消费点;缺省回退表值——
# 受理后不变量(encounter 受理后只读)。攻击单 base_value 贯通由 _smoke_damage_types 的
# hero_atk_val=100 全链路行值承载(同夹具既有覆盖——派生读在攻击链上工作即行值随 override 走)----
func _smoke_hero_atk_derived() -> void:
var rng_a = load("res://core/rng_service.gd").new()
rng_a.set_master_seed(4242)
var s_a = load("res://core/battle_orchestrator.gd").new(_db, rng_a)
Kit.check_quiet(s_a.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp), "hero_atk": 100}),
"起战受理(override 100)〔编排归约 V4〕")
Kit.check_quiet(int(s_a._enemy_system.hero_unit().get("atk", -1)) == 100,
"主角攻击单基值==enc 携带 override(_hero_atk 派生读贯通 spawn 消费点)〔编排归约 V4〕")
var rng_b = load("res://core/rng_service.gd").new()
rng_b.set_master_seed(4242)
var s_b = load("res://core/battle_orchestrator.gd").new(_db, rng_b)
Kit.check_quiet(s_b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)}),
"起战受理(缺省)〔编排归约 V4〕")
Kit.check_quiet(int(s_b._enemy_system.hero_unit().get("atk", -1)) == int(_db.combat.hero.atk),
"缺省主角攻击单基值==hero 表值(派生读回退面)〔编排归约 V4〕")
# ---- 敌方攻击类型轴(表列/单位快照/单据载荷/矩阵命中证词)----
# 克制矩阵命中证词 = 同种子三甲型配对实测(不是查表存在性证词):同种子下 armor_roll 流消费逐位一致、
# 阈值(hero armor_class)不变,三甲型唯一差异 = 克制点数 cdelta(normal_vs_light 0 / heavy −1 / ward +1)
@@ -83,9 +83,10 @@ func _run_battle(L: int, D: int, axes: Dictionary = {}) -> Dictionary:
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)
var out := {"stream": [], "events": [], "rounds": 0, "result": "", "terminated": false}
var out := {"stream": [], "events": [], "rounds": 0, "result": "", "terminated": false, "econ_refresh_used": -1}
settlement.step_row.connect(func(row): out.stream.append(row))
settlement.battle_finished.connect(func(payload): out.result = payload.get("result", ""))
settlement.settlement_snapshot.connect(func(snap): out.econ_refresh_used = int(snap.get("economy", {}).get("refresh_used", -1))) # 末次发射=终局轮快照
settlement.minesweeper.cell_revealed.connect(func(evt): out.events.append(evt))
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}))
@@ -108,6 +109,10 @@ func _run_battle(L: int, D: int, axes: Dictionary = {}) -> Dictionary:
# P0-3 回归断言:notify_combat_end 曾被并进行尾注释从不执行——
# 战斗收束后相位必须回 Idle(RAP 清零),任何「相位卡 Waiting」回归在此被抓
Kit.check("战斗收束后行动经济相位回 Idle(notify_combat_end 生效,实测=%s)" % String(action_economy.phase), action_economy.phase == "Idle")
# 复位边界(注册面批同代编排归约 V2——BattleFacts.reset_round 不碰 refresh_used 的可观测提交面):
# 终局序列含 facts.reset_round(),其后呈现快照的 refresh_used 必须等于末轮结算快照的 economy 值
Kit.check("L%d·D%d 终局后呈现快照 refresh_used==末轮结算快照 economy 值(复位不碰 refresh_used)〔编排归约 V2〕" % [L, D],
int(settlement.presentation_snapshot().get("refresh_used", -1)) == out.econ_refresh_used)
return out
@@ -1019,7 +1024,7 @@ func _smoke_counter_buckets() -> void:
if not oA.minesweeper.current_board().is_mine(ca):
safe_a = ca
break
oA._grant_seg._judge_seal({"cell": safe_a, "hit": false}, 1)
oA._rounds._grant_seg._judge_seal({"cell": safe_a, "hit": false}, 1)
Kit.check("V6 第1战标错落 battle 桶 insurance_used=1(_apply_counter_hit 静态写口经 ctx)〔无状态化 V6〕", EffectEngine.counter(oA.fx_host().counters_ctx, "insurance_used") == 1)
# 卡④ wr2①——完美保险标错行=单一扩展行型(原 insurance_used 实名行型收敛)
var v6_ins := {}
@@ -374,10 +374,10 @@ func _smoke_clear_reward_zero_paths() -> void:
Kit.check("高光守卫构造战斗启动", s_b.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80, "node_type": "boss"}))
for u in s_b._enemy_system.all_enemies():
s_b._enemy_system.apply_damage(u, int(u.hp))
s_b._perfect_hit_true = 1 # 测试通道:构造完美谓词成立(真值在正常流由封印判定累加)
s_b._perfect_hit_false = 0
s_b._facts.perfect_hit_true = 1 # 测试通道:构造完美谓词成立(真值在正常流由封印判定累加)
s_b._facts.perfect_hit_false = 0
rows_b.clear()
s_b.reward_grant().grant_clear_reward(1, s_b._board_from_midround_refresh, s_b.perfect_sweep_earned())
s_b.reward_grant().grant_clear_reward(1, s_b._facts.board_from_midround, s_b.perfect_sweep_earned())
Kit.check("② grant_clear_reward 守卫路径重走首份 → 未生效行仍在案", _has_source_row(rows_b, "reward_ineffective", "clear_reward", "clear_blast"))
Kit.check("② 首份零兑付 → 高光行不发(不记名义值 2)", _rows_of_type(rows_b, "highlight").is_empty())
# ③ 第二份被拒:唯一存活敌 HP ≤ 首份 aoe 量 → 首份击杀 → 第二份无存活敌(zero + 未生效行)
@@ -389,10 +389,10 @@ func _smoke_clear_reward_zero_paths() -> void:
Kit.check("第二份被拒构造战斗启动", s2.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80, "node_type": "boss"}))
var boss = s2._enemy_system.all_enemies()[0]
boss.hp = 1 # 测试通道:压低至首份 aoe 必杀(弱形式——正常流 Boss 血量不可控)
s2._perfect_hit_true = 1
s2._perfect_hit_false = 0
s2._facts.perfect_hit_true = 1
s2._facts.perfect_hit_false = 0
rows2.clear()
s2.reward_grant().grant_clear_reward(1, s2._board_from_midround_refresh, s2.perfect_sweep_earned())
s2.reward_grant().grant_clear_reward(1, s2._facts.board_from_midround, s2.perfect_sweep_earned())
Kit.check("③ 首份 aoe 真实兑付(击杀唯一存活敌)", _has_source_row(rows2, "damage", "clear_reward"))
Kit.check("③ 第二份被拒产出 reward_ineffective 行(零兑付)", _has_source_row(rows2, "reward_ineffective", "clear_reward", "clear_blast"))
var hl2 := _rows_of_type(rows2, "highlight")
@@ -116,19 +116,19 @@ func _init() -> void:
"feats": [], "tree_rules": [], "battle_index": 1, "voucher_rap": 0, "voucher_time_sand": 0,
"node_type": "battle", "adv_bucket": {}}), "V6 前置:战斗受理")
# facts 组包经真实摇号路径消费(mines 全零时条件技 sneak_ambush 不入集合——facts 四键在位的可观察面)
Kit.check_quiet(int(orch._mines_hits["total"]) == 0 and int(orch._mines_hits["last"]) == 0 and int(orch._mines_hits["round"]) == 0,
Kit.check_quiet(int(orch._rounds.mines_hits["total"]) == 0 and int(orch._rounds.mines_hits["last"]) == 0 and int(orch._rounds.mines_hits["round"]) == 0,
"V6 开战清零:三计数全零起〔触发器批 V6〕")
# 正向 blast(spark_aoe)不递增——负向计数守卫(flat 行应用面自编排器迁至奖励授予件
# ——迭代40 卡① 拆件;计数经授予件 battle_hooks.negative_blast 回宿主 _mines_hits)
var att_v6 := {"source_class": "reward", "cell": Vector2i(-1, -1), "ri": 1}
orch.reward_grant()._apply_flat_row({"cell_type": "blast", "polarity": "positive", "power": 2,
"display_name": "爆裂", "reward_id": "spark_aoe"}, att_v6)
Kit.check_quiet(int(orch._mines_hits["total"]) == 0 and int(orch._mines_hits["round"]) == 0,
Kit.check_quiet(int(orch._rounds.mines_hits["total"]) == 0 and int(orch._rounds.mines_hits["round"]) == 0,
"V6 正向 blast(spark_aoe)不递增(负向守卫——清图/爆裂格零误计)〔触发器批 V6〕")
# 负向 blast(雷)递增
orch.reward_grant()._apply_flat_row({"cell_type": "blast", "polarity": "negative", "power": 2,
"display_name": "警觉", "reward_id": "enemy_alert"}, att_v6)
Kit.check_quiet(int(orch._mines_hits["total"]) == 1 and int(orch._mines_hits["round"]) == 1,
Kit.check_quiet(int(orch._rounds.mines_hits["total"]) == 1 and int(orch._rounds.mines_hits["round"]) == 1,
"V6 负向 blast(雷)递增恰一次〔触发器批 V6〕")
# ---- V7 池构成纪律(现行全表零违例+构造全条件池报错——跨表校验真实入口)----
+30 -9
View File
@@ -66,12 +66,33 @@ func _smoke_hygiene_gates() -> void:
var mv: String = FileAccess.get_file_as_string("res://presentation/main.gd")
Kit.check("V8 GP 影子账残留零命中(main 源码面——文件已删+委托缝已拆)〔迭代27 T3〕",
not mv.contains("gp_shadow_ledger") and not mv.contains("_gp_ledger"))
# V1d 层序落位单口:main.gd 的 move_child 调用全部收在 _apply_layer_contract 内(合同装配单口——
# 函数体之外零调用点;出现第二处落位=硬编码层序,须改合同表而非加调用)
var fn_start: int = mv.find("func _apply_layer_contract")
var fn_end: int = mv.find("\nfunc ", fn_start + 10)
if fn_end < 0:
fn_end = mv.length()
var outside_calls: int = (mv.substr(0, fn_start) + mv.substr(fn_end)).count("move_child(")
Kit.check("V1d 层序落位单口(_apply_layer_contract 之外 move_child 调用点=%d 应=0)〔装配生命批 V1〕" % outside_calls,
outside_calls == 0)
# V1d 层序落位单口:注册面建件后扫全 presentation——move_child 只允许出现在 ui_registry.gd 的
# apply_layer_contract 函数体内(合同装配单口——出现第二处落位=硬编码层序,须改合同表而非加调用)
var scanned: int = 0
var outside_calls: int = 0
for fn_path: String in _list_scripts("res://presentation"):
var src: String = FileAccess.get_file_as_string(fn_path)
scanned += 1
var fn_start: int = src.find("func apply_layer_contract") if fn_path.contains("ui_registry.gd") else -1
var fn_end: int = src.find("\nfunc ", fn_start + 10) if fn_start >= 0 else -1
if fn_end < 0:
fn_end = src.length()
outside_calls += (src.substr(0, fn_start) + src.substr(fn_end)).count("move_child(")
Kit.check("V1d 层序落位单口(presentation %d 文件——apply_layer_contract 之外 move_child 调用点=%d 应=0)〔注册面 V1〕" % [scanned, outside_calls],
scanned > 0 and outside_calls == 0)
# 目录 .gd 清单(DirAccess 平铺——presentation 无子目录;扫描面>0 前置由调用侧断言承载)
static func _list_scripts(dir_path: String) -> PackedStringArray:
var out: PackedStringArray = PackedStringArray()
var dir := DirAccess.open(dir_path)
if dir == null:
return out
dir.list_dir_begin()
var fname := dir.get_next()
while fname != "":
if fname.ends_with(".gd"):
out.append(dir_path + "/" + fname)
fname = dir.get_next()
dir.list_dir_end()
return out
+1 -1
View File
@@ -850,7 +850,7 @@ func _drive_to_pref(autoplay, prefer: String) -> Dictionary:
r.pick_next_node(int(hit.get("id", -1)))
out["node"] = hit
if prefer == "battle":
out["enc"] = r.current_battle().encounter
out["enc"] = r.current_battle()._facts.encounter
else:
var cands: Array = (adr.stop().node_curio_candidates() as Dictionary).get("candidates", []) as Array
adr.stop().present_node_curio(cands)
+11 -11
View File
@@ -178,10 +178,10 @@ func _init() -> void:
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,
Kit.check_quiet(int(orch._facts.perfect_hit_false) == 0 and int(orch._facts.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,
orch._rounds._grant_seg._judge_seal({"cell": safe_cells[0], "hit": false}, 1)
Kit.check_quiet(int(orch._facts.perfect_hit_false) == 1,
"计数纯净:首次标错 _perfect_hit_false 落 1 未被回退改写(契约 2)〔卡② R2〕")
var ins_row := {}
for r in rows_orch:
@@ -191,7 +191,7 @@ func _init() -> void:
"豁免一次:首次标错 _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)
orch._rounds._grant_seg._judge_seal({"cell": safe_cells[1], "hit": false}, 2)
var ins_count := 0
var miss_hl_last := ""
for r in rows_orch:
@@ -203,11 +203,11 @@ func _init() -> void:
"限次:第二次标错不获豁免(forgiven 恒 1——when counter lt 1)〔卡② R2〕")
Kit.check_quiet(not orch.perfect_sweep_earned(),
"判定相减:误标 2 − 豁免 1 = 有效 1 → 完美不成立〔卡② R2 真值表〕")
orch._perfect_hit_false = 1
orch._perfect_hit_true = 1
orch._facts.perfect_hit_false = 1
orch._facts.perfect_hit_true = 1
Kit.check_quiet(orch.perfect_sweep_earned(),
"判定相减:误标1−豁免1=有效0且标中≥1→完美成立(保险每战限次一次迁移保绿)〔无状态化 V7〕")
orch._perfect_hit_true = 0
orch._facts.perfect_hit_true = 0
Kit.check_quiet(not orch.perfect_sweep_earned(),
"判定守卫:标中数 0 时零误标也不成立(整图 ≥1 标中条件零松动)〔卡② R2 真值表〕")
var rig_rng2 = RngScript.new()
@@ -222,7 +222,7 @@ func _init() -> void:
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)
orch2._rounds._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":
@@ -249,7 +249,7 @@ func _init() -> void:
if not o10.minesweeper.current_board().is_mine(c10):
sc10 = c10
break
o10._grant_seg._judge_seal({"cell": sc10, "hit": false}, 1)
o10._rounds._grant_seg._judge_seal({"cell": sc10, "hit": false}, 1)
var sm10: Dictionary = {}
for r10 in rows10:
if String(r10.get("type", "")) == "seal_miss":
@@ -276,14 +276,14 @@ func _init() -> void:
if not o11.minesweeper.current_board().is_mine(c11):
sc11 = c11
break
o11._grant_seg._judge_seal({"cell": sc11, "hit": false}, 1)
o11._rounds._grant_seg._judge_seal({"cell": sc11, "hit": false}, 1)
var sm11: Dictionary = {}
for r11 in rows11:
if String(r11.get("type", "")) == "seal_miss":
sm11 = r11
Kit.check_quiet(String(sm11.get("highlight", "")) == "",
"A11 豁免:首次标错获豁免 → seal_miss 行 highlight==空串〔卡① N7〕")
o11._perfect_hit_true = 1
o11._facts.perfect_hit_true = 1
Kit.check_quiet(o11.perfect_sweep_earned(),
"A11 真值:误标 1 − 豁免 1 = 有效 0 且标中≥1 → 完美成立(与 highlight==空 一致)〔卡① N7〕")
@@ -339,7 +339,7 @@ func _meta_settle_and_workshop() -> void:
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))
and ((adr_res.runner().current_battle()._facts.encounter.get("tree_rules", []) as Array) == rules_e))
_meta_smoke_cleanup()
@@ -705,16 +705,16 @@ func _smoke_tree_rules() -> void:
Kit.check("守侧 rig:首轮后 queue 干净(开局 posture queue 已兑现)",
int(orch.action_economy.pending_ap_delta()) == 0)
# 场景①:当回合新封印、结算判标错且封印仍在场 → queue +1(本轮 AP 零变化=queue 承载、下轮 notify_round_start 兑现)
orch._grant_seg._judge_seal({"cell": safe, "hit": false}, 1)
orch._rounds._grant_seg._judge_seal({"cell": safe, "hit": false}, 1)
Kit.check("守①:当回合标错且封印在场 → 下轮 AP+1(queue 排队——本轮 AP 逐位零变化)",
int(orch.action_economy.pending_ap_delta()) == 1)
# 场景③④:之前回合标错重判(封印在场状态)与同格重复——去重闸挡回,最多返还一次
orch._grant_seg._judge_seal({"cell": safe, "hit": false}, 2)
orch._grant_seg._judge_seal({"cell": safe, "hit": false}, 3)
orch._rounds._grant_seg._judge_seal({"cell": safe, "hit": false}, 2)
orch._rounds._grant_seg._judge_seal({"cell": safe, "hit": false}, 3)
Kit.check("守③④:旧封印重判零触发+同格最多一次(_seal_miss_seen 去重闸=「当回合标错动作」判别器)",
int(orch.action_economy.pending_ap_delta()) == 1)
# 场景②负样本:封印命中(hit=true 正向兑付路径)零触发
orch._grant_seg._judge_seal({"cell": safe, "hit": true}, 4)
orch._rounds._grant_seg._judge_seal({"cell": safe, "hit": true}, 4)
Kit.check("守②负样本:标中/被挖封印零触发(标错事件只在 miss 分支发射——发射面过滤承载)",
int(orch.action_economy.pending_ap_delta()) == 1)
# 场景⑤:末轮判出→无下一轮=queue 不兑现(编排器每战新建——队列实例随战亡,合法作废态)
@@ -746,7 +746,7 @@ func _smoke_tree_rules() -> void:
"feats": [], "tree_rules": [], "battle_index": 1,
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
"voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}))
orch3.encounter["tree_rules"] = []
orch3._facts.encounter["tree_rules"] = []
var gp_base: int = orch3._roll_gp_drop()
var orch4 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777005))
Kit.check("聚 rig:增益编排启动(tree_rules=economy_hoard)",
@@ -755,7 +755,7 @@ func _smoke_tree_rules() -> void:
"feats": [], "tree_rules": ["economy_hoard"], "battle_index": 1,
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
"voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}))
orch4.encounter["tree_rules"] = ["economy_hoard"]
orch4._facts.encounter["tree_rules"] = ["economy_hoard"]
var gp_hoard: int = orch4._roll_gp_drop()
Kit.check("聚:战斗掉落乘算 ×1.5(provisional——同种子配对 %d→%d,入账前=结算框显示实得)",
gp_hoard == int(round(float(gp_base) * 1.5)) and gp_hoard >= gp_base)
+1 -1
View File
@@ -72,7 +72,7 @@ func _seal_propagation(db) -> bool:
rng_a.set_master_seed(5)
var oc_bg = OrchestratorScript.new(db, rng_a)
var bi_bg = BoardInstallerScript.new({"db": db, "rng": rng_a, "minesweeper": oc_bg.minesweeper,
"reward_economy": oc_bg._reward_economy, "encounter": oc_bg.encounter, "fatal_cb": oc_bg._fatal})
"reward_economy": oc_bg._reward_economy, "encounter": oc_bg._facts.encounter, "fatal_cb": oc_bg._fatal})
bi_bg.generator._fatal("测试违约①棋盘生成")
if oc_bg.battle_result != "fatal_error":
push_error("[run_time_fatal_test/wr5] 棋盘生成违约未传导封印(battle_result=%s)" % str(oc_bg.battle_result))
+9 -6
View File
@@ -23,12 +23,15 @@ func _init() -> void:
Kit.check_quiet(load("res://core/settlement/decay_segment.gd").new(oc) != null, "V4 DecaySegment 直接 new()〔迭代27 T2〕")
Kit.check_quiet(load("res://core/settlement/verdict_segment.gd").new(oc) != null, "V4 VerdictSegment 直接 new()〔迭代27 T2〕")
# V4-2 编排器 _init 接线(段持宿主引用;段序编排面在位)
Kit.check_quiet(oc._grant_seg != null and oc._grant_seg.oc == oc, "V4 编排器接线·授予段(宿主回指)〔迭代27 T2〕")
Kit.check_quiet(oc._aura_seg != null and oc._aura_seg.oc == oc, "V4 编排器接线·增益/减益段〔迭代27 T2〕")
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〕")
# V4-2 编排器 _init 接线(段持宿主引用;段序编排面在位)——注册面批同代归约 V1:
# 段件宿主直持改经 _rounds(轮循环驱动件)承载,接线断言改指 _rounds 段族
Kit.check_quiet(oc._rounds != null and oc._rounds.oc == oc, "V1 编排器接线·轮循环驱动件在座且宿主回指〔编排归约 V1〕")
Kit.check_quiet(oc._facts != null, "V1 编排器接线·战斗事实件在座〔编排归约 V1〕")
Kit.check_quiet(oc._rounds._grant_seg != null and oc._rounds._grant_seg.oc == oc, "V4 编排器接线·授予段(宿主回指)〔迭代27 T2〕")
Kit.check_quiet(oc._rounds._aura_seg != null and oc._rounds._aura_seg.oc == oc, "V4 编排器接线·增益/减益段〔迭代27 T2〕")
Kit.check_quiet(oc._rounds._combat_seg != null and oc._rounds._combat_seg.oc == oc, "V4 编排器接线·战斗段〔迭代27 T2〕")
Kit.check_quiet(oc._rounds._decay_seg != null and oc._rounds._decay_seg.oc == oc, "V4 编排器接线·衰减段〔迭代27 T2〕")
Kit.check_quiet(oc._rounds._verdict_seg != null and oc._rounds._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()
+13 -8
View File
@@ -162,13 +162,13 @@ func _init() -> void:
"线香:装配起战(battle 级充能 1)")
var inv8 = load("res://core/treasure_items.gd").new(db)
var rec8: Dictionary = inv8.add_item("incense_stick", "attunement", Vector2i(-1, -1), 0)
ob8.treasure_inventory = inv8
ob8.reward_grant().treasure_inventory = inv8
var h8: int = int(rec8.get("handle", -1)) # handle 取 add_item 返回(snapshot 投影无 handle 键)
var tgt8: Vector2i = _first_closed_safe(ob8)
Kit.check_quiet(tgt8 != Vector2i(-9, -9), "线香:夹具存在未开安全格")
var v8: Dictionary = ob8.reward_grant().use_treasure(h8, tgt8)
Kit.check_quiet(bool(v8.get("ok", false)), "线香正常:单格真视受理(cell 目标解析)")
Kit.check_quiet(bool(ob8.treasure_inventory.query_overlays().get("mine_reveal", []).has(tgt8))
Kit.check_quiet(bool(ob8.reward_grant().treasure_inventory.query_overlays().get("mine_reveal", []).has(tgt8))
and ob8.minesweeper.is_revealed(tgt8) == false,
"线香断言:目标格入真视叠加层且未被开格(真视≠开格——信息红线)")
Kit.check_quiet(inv8.snapshot_inventory().is_empty() or bool(inv8.snapshot_inventory()[0].get("used", false)),
@@ -240,7 +240,7 @@ func _init() -> void:
"天眼:装配起战(level 级充能 1)")
var inv11 = load("res://core/treasure_items.gd").new(db)
var rec11: Dictionary = inv11.add_item("sky_eye", "attunement", Vector2i(-1, -1), 0)
ob11.treasure_inventory = inv11
ob11.reward_grant().treasure_inventory = inv11
var h11: int = int(rec11.get("handle", -1))
var tgt11: Vector2i = _first_closed_safe(ob11)
var v11: Dictionary = ob11.reward_grant().use_treasure(h11, tgt11)
@@ -254,7 +254,7 @@ func _init() -> void:
if ob11.minesweeper.is_revealed(c):
opened_safe = false
Kit.check_quiet(mines_shown == 0 or opened_safe, "天眼反例①:危险格仅显示不被开格(棋盘事实不可变)")
var overlay11: Array = ob11.treasure_inventory.query_overlays().get("mine_reveal", [])
var overlay11: Array = ob11.reward_grant().treasure_inventory.query_overlays().get("mine_reveal", [])
var overlay_ok := true
for c_v in (v11.get("resolved", []) as Array):
if not overlay11.has(c_v):
@@ -289,7 +289,7 @@ func _init() -> void:
"计数链:装配起战(GainJournal 携带)")
var inv13 = load("res://core/treasure_items.gd").new(db)
var rec13: Dictionary = inv13.add_item("incense_stick", "attunement", Vector2i(-1, -1), 0)
ob13.treasure_inventory = inv13
ob13.reward_grant().treasure_inventory = inv13
var h13: int = int(rec13.get("handle", -1))
var tgt13: Vector2i = _first_closed_safe(ob13)
if tgt13 != Vector2i(-9, -9):
@@ -306,8 +306,13 @@ func _init() -> void:
"装配不可变:双件装配冒险启动")
Kit.check_quiet(adr14.runner().pick_next_node(int(adr14.runner().next_candidate_ids()[0])), "装配不可变:进首战")
var ob14 = adr14.runner().current_battle()
Kit.check_quiet(ob14 != null and (ob14.attunement as Array) == ["morning_order", "wide_scout_band"],
Kit.check_quiet(ob14 != null and (ob14._facts.attunement as Array) == ["morning_order", "wide_scout_band"],
"装配不可变:战斗运行态装配集与冒险装配集逐位同源(运行零写口——快照对比)")
# 注册面批同代编排归约 V5:库存注入改址(level_runner 注入改经 reward_grant() 出口直注)后
# 引用同一性——战斗域经出口持有的库存与关卡域同一实例;use_treasure 受理链经新地址走通
# 由本套件 ob8/ob11/ob13 三段既有受理链承载(注入面同批改址后全绿即证据)。
Kit.check_quiet(ob14 != null and ob14.reward_grant().treasure_inventory == adr14.runner().treasure_inventory,
"库存注入改址:战斗域经 reward_grant 出口持有的库存与关卡域同一引用〔编排归约 V5〕")
# ⑮ 扩展行收敛(迭代 38 卡④ wr2④⑤——套件首次 feats 注入,V6 形态自 round_flow_skill_pool_test 移植:
# enc 带 feats 键+ledger,专长触发经 _judge_seal 真实封印判定管线)
@@ -332,7 +337,7 @@ func _init() -> void:
Kit.check_quiet(mines15.size() == 3, "封印匠:板上存在三枚危险格(断言前提)")
if mines15.size() == 3:
for mv in mines15:
ob15._grant_seg._judge_seal({"cell": mv, "hit": true}, 1)
ob15._rounds._grant_seg._judge_seal({"cell": mv, "hit": true}, 1)
var smith_row := {}
for r in rows15:
if String(r.get("effect_id", "")) == "mine_neutralized":
@@ -354,7 +359,7 @@ func _init() -> void:
cell15b = cv
break
if cell15b != null:
ob15b._grant_seg._judge_seal({"cell": cell15b, "hit": true}, 1)
ob15b._rounds._grant_seg._judge_seal({"cell": cell15b, "hit": true}, 1)
var beacon_row := {}
for r in rows15b:
if String(r.get("effect_id", "")) == "fx_preview_area":
@@ -58,7 +58,7 @@ func _init() -> void:
main._start_adventure_from_menu()
await process_frame
# 入口停点选路进首战(真实停点链)
if not main.level_screen.visible:
if not main.ui.modals.level_screen.visible:
_problems.append("入口停点未呈现(前置失守)")
_finish()
return
@@ -100,12 +100,12 @@ func _init() -> void:
# ---- 断言①:排空前后核心状态逐位一致(演出关停零分叉)----
var snap_before: String = JSON.stringify(battle.diagnostic_snapshot())
var hp_before: int = int(battle.action_economy.ap) + int(battle.action_economy.rap) * 0 # 观测面双录
main._flush_row_queue()
main.ui.screens.row_pacer.flush()
var snap_after: String = JSON.stringify(battle.diagnostic_snapshot())
check(snap_before == snap_after, "排空前后核心诊断快照逐位一致(AC⑤ 演出关停零分叉)")
# ---- 断言②:零丢行(结构判据:排空后队列恒空 + 末信号行原文在带——append_row 直落不吞行)----
check(rows_seen.size() > 0, "结算行已产生(断言前提)")
check(main._row_queue.is_empty(), "排空后队列恒空(flush 完整性)")
check(main.ui.screens.row_pacer.pending_rows().is_empty(), "排空后队列恒空(flush 完整性)")
var last_text := ""
for i in range(rows_seen.size() - 1, -1, -1):
var cand_row: Dictionary = rows_seen[i]
@@ -114,7 +114,7 @@ func _init() -> void:
last_text = String(cand_row.get("display_name", ""))
if last_text != "":
break
check(last_text != "" and main.log_view.parsed_text().contains(last_text),
check(last_text != "" and main.ui.battle.log_view.parsed_text().contains(last_text),
"末信号行原文在明细带(排空不吞行:『%s』)" % last_text)
# ---- 断言③:段间延迟取数面双态 ----
UiKit.motion_scale = 1.0
@@ -129,7 +129,7 @@ func _init() -> void:
var fstage := FakeStage.new(null)
var ffx := FakeFx.new()
rp.stage = fstage
rp.board_view = main.board_view
rp.board_view = main.ui.battle.board_zone.board_view
rp.settlement_fx = ffx
rp._play_settlement_mark({"type": "damage", "target": "地精", "target_id": "goblin_b", "value": 3})
check(fstage.calls.size() == 1 and fstage.calls[0] == ["goblin_b", "地精"],
@@ -144,9 +144,9 @@ func _init() -> void:
UiKit.motion_scale = 1.0 # 常态入队(减动效快路同帧直落不入队——夹具须真落队列)
main._on_step_row({"type": "damage", "target": "hero", "value": 1})
main._on_step_row({"type": "heal", "target": "hero", "value": 2})
check(main._row_queue.size() == 2, "会话更替夹具已入队(前提:%d 行)〔wr4〕" % main._row_queue.size())
check(main.ui.screens.row_pacer.pending_rows().size() == 2, "会话更替夹具已入队(前提:%d 行)〔wr4〕" % main.ui.screens.row_pacer.pending_rows().size())
main._begin_session(false)
check(main._row_queue.is_empty(), "会话更替后待播队列清零(_begin_session 内机制 flush)〔wr4〕")
check(main.ui.screens.row_pacer.pending_rows().is_empty(), "会话更替后待播队列清零(_begin_session 内机制 flush)〔wr4〕")
_finish()
@@ -722,8 +722,8 @@ func _smoke_diag_button_retired() -> void:
var main_src: String = FileAccess.get_file_as_string("res://presentation/main.gd")
Kit.check("V6 主区无 build_diag_button 调用残留(旧实现同批清理)",
not main_src.contains("build_diag_button"))
Kit.check("V6 转储入口连接随迁(组合根接浮窗 diagnose_requested 信号)",
main_src.contains("debug_overlay.diagnose_requested.connect(_on_diagnose_pressed)"))
Kit.check("V6 转储入口连接随迁(组合根经注册面接浮窗 diagnose_requested 信号)〔注册面 V2〕",
main_src.contains("ui.modals.debug_overlay.diagnose_requested.connect(_on_diagnose_pressed)"))
Kit.check("V6 转储编排承接函数在位(dump_diagnosis 卡死取证保留)",
main_src.contains("func _on_diagnose_pressed()"))
var asm_src: String = FileAccess.get_file_as_string("res://presentation/ui_assembler.gd")
@@ -0,0 +1,245 @@
# 双实现对拍配对套件(注册面批同代测速循环批 v1.1——帧相位等价的执行化证据):
# 产品码(process_frame 信号步进状态机)vs 夹具(现行 await 形态逐字副本——循环内 await+收尾四语句在 run() 尾)。
# 五面对拍(帧索引序列/运行态序列/进度串序列/stats 六键/报告全文)逐位相等=
# 每步恰一帧+相位同点+输出逐字的当轮真实执行证据(测量语义逐点等价四判据的落码面)。
# 拓扑钉死:测试宿主先连 process_frame 帧计数、后启动两形态——两形态下「计数先于步进体」相对次序同构,序列可比。
# n=3 常规案+n=0 退化案(同步收尾零帧——_mean/_p90 空守卫渲染面)。
# 运行:godot --headless --path game --script res://tests/unit/presentation/tune_sim_batch_parity_test.gd(expect 0)
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd")
const TuneSimScript := preload("res://presentation/tune_sim_controller.gd")
# 假调试面板(wd8 同形 setter 面+进度/运行态/报告三记录数组供序列对拍)
class FakePanel extends Node:
var running := false
var progress := ""
var report := ""
var atk := 0
var hp := 0
var progress_log: Array[String] = []
var report_log: Array[String] = []
var running_log: Array[bool] = []
func atk_pct() -> int:
return atk
func hp_pct() -> int:
return hp
func set_sim_running(r: bool) -> void:
running = r
running_log.append(r)
func set_sim_progress(t: String) -> void:
progress = t
progress_log.append(t)
func set_sim_report(t: String) -> void:
report = t
report_log.append(t)
# 假自动策略(固定返回值两档 victory/defeat 交替——wd8 :53-55 同款取值,零随机;
# 逐局记录帧索引与运行态读数——帧索引经宿主注入的计数 Callable 读取)
class FakeDriver extends RefCounted:
var ctrl
var frame_counter: Callable # 宿主帧计数(0 参 Callable——拓扑钉死:计数先于步进体)
var seen: Array[bool] = []
var frames: Array[int] = []
var calls := 0
func run_level(_db: Variant, _seed: int, _level: int, _atk: int, _hp: int, _other: bool, _logger: Variant) -> Dictionary:
calls += 1
seen.append(ctrl.is_running())
frames.append(int(frame_counter.call()))
if calls % 2 == 1:
return {"hero_dmg": 12.0, "rounds": 4, "result": "victory", "hp_left": 3.0, "hp_at_boss": 5.0}
return {"hero_dmg": 6.0, "rounds": 2, "result": "defeat",
"defeat_node_index": 3, "defeat_node_type": "battle", "hp_left": 0.0, "hp_at_boss": 0.0}
# 假配置库(TuneSimController 只读 db.run.action_economy.ap_per_round——wd8 同款最小域对象图)
class FakeActionEconomy:
var ap_per_round := 2
class FakeRun:
var action_economy := FakeActionEconomy.new()
class FakeDB:
var run := FakeRun.new()
# 现行 await 形态逐字副本(tune_sim_controller.gd run(n) 改形前形态——循环内 await+收尾四语句在 run() 尾;
# 全仓唯一在测夹具——产品码禁留第二实现,tests/ 不在 S13/S8/S9/S10 扫描面、夹具内循环 await 合法在册)。
# 与产品码新形态的申报差异仅一处:批末把 stats 六键束存 out_stats 成员供对拍读取
# (产品码 stats 经报告公开面观测——内部字段直查受控例外申报在卡 §17)。
class AwaitLoopRunner extends RefCounted:
const SEED_BASE := 4200
const ParityLogger := preload("res://presentation/playtest_logger.gd")
var db
var panel
var _base_hp := 0
var _driver = null
var _running := false
var out_stats: Dictionary = {}
func _init(db_ref, panel_ref, base_hp: int, driver = null) -> void:
db = db_ref
panel = panel_ref
_base_hp = base_hp
_driver = driver
func is_running() -> bool:
return _running
func run(n: int) -> void:
_running = true
var logger = ParityLogger.new()
logger.ap_per_round = int(db.run.action_economy.ap_per_round)
logger.start_session(SEED_BASE)
panel.set_sim_running(true)
panel.set_sim_progress("模拟中:0/%d …" % n)
var stats := {"wins": 0, "dmg_all": [], "hp_left_win": [], "hp_boss": [], "rounds_all": [], "death_nodes": {}}
var autoplay = _driver
for i in range(n):
_run_one_level(autoplay, stats, i, panel, logger)
panel.set_sim_progress("模拟中:%d/%d …" % [i + 1, n])
await panel.get_tree().process_frame
logger.close_session()
panel.set_sim_report(HudPresenter.tune_sim_report(n, panel.atk_pct(), panel.hp_pct(), stats, _base_hp))
panel.set_sim_running(false)
_running = false
out_stats = stats
func _run_one_level(autoplay, stats: Dictionary, i: int, panel, logger) -> void: # type-hint: ignore(驱动/面板/日志件鸭子型注入——同产品码构造缝)
var st = autoplay.run_level(db, SEED_BASE + i, 1, panel.atk_pct(), panel.hp_pct(), false, logger)
(stats["dmg_all"] as Array).append(st.hero_dmg)
(stats["rounds_all"] as Array).append(st.rounds)
if st.result == "victory":
stats["wins"] = int(stats["wins"]) + 1
(stats["hp_left_win"] as Array).append(st.hp_left)
(stats["hp_boss"] as Array).append(st.hp_at_boss)
elif st.result == "defeat":
var key := "%d.%s" % [st.defeat_node_index, st.defeat_node_type]
stats["death_nodes"][key] = int(stats["death_nodes"].get(key, 0)) + 1
func _init() -> void:
var frame_idx := {"i": -1} # 帧计数(宿主最先连——拓扑钉死「计数先于步进体」)
process_frame.connect(func() -> void: frame_idx["i"] += 1)
await process_frame # 树初始化就绪帧(SceneTree._init 时点 root 未挂树——面板 get_tree() 首帧后才有效;wd8 同款先例:panel 段前有 await 过帧)
var r3 := await _run_pair(3, frame_idx)
_check_n3(r3)
var r0 := await _run_pair(0, frame_idx)
_check_n0(r0)
quit(Kit.finish("tune_sim_batch_parity"))
# 对拍驱动:两形态各经 run(n) 真实入口(同参假面板/假驱动各自实例);先启动两形态、后分别 await——
# 两形态在调用点同一同步段内先后挂起(并行于同一帧时钟),驱动协程返回 int 承接 void run 的可等待面
func _run_pair(n: int, frame_idx: Dictionary) -> Dictionary:
# 两形态串行驱动(各经 run(n) 真实入口 await 调用——GDScript 协程动态调用必须 await,
# 无并行启动形态):帧序列各自记录、断言按相对首帧偏移归一后逐位比对——拓扑不变式
# 「计数先连、启动后于计数、计数先于步进体」在两形态下相对次序同构,等价证据结构不变。
var panel_a := FakePanel.new()
root.add_child(panel_a)
var drv_a := FakeDriver.new()
var a: Variant = TuneSimScript.new(FakeDB.new(), panel_a, 100, drv_a)
drv_a.ctrl = a
drv_a.frame_counter = func() -> int: return frame_idx["i"]
await a.run(n)
var panel_b := FakePanel.new()
root.add_child(panel_b)
var drv_b := FakeDriver.new()
var b: Variant = AwaitLoopRunner.new(FakeDB.new(), panel_b, 100, drv_b)
drv_b.ctrl = b
drv_b.frame_counter = func() -> int: return frame_idx["i"]
await b.run(n)
return {"a": a, "b": b, "pa": panel_a, "pb": panel_b, "da": drv_a, "db": drv_b}
func _check_n3(r: Dictionary) -> void:
var pa: FakePanel = r["pa"]
var pb: FakePanel = r["pb"]
var da: FakeDriver = r["da"]
var drv_b: FakeDriver = r["db"]
var a = r["a"]
var b = r["b"]
Kit.check("n=3 帧序列相对偏移逐位相等(%s vs %s)+相邻差恒一(每步恰一帧)〔测速 V1〕",
str(_norm(da.frames)) == str(_norm(drv_b.frames)) and da.frames.size() == 3
and _consecutive(da.frames) and _consecutive(drv_b.frames))
Kit.check("n=3 运行态读数序列逐位相等+收尾双运行态假+面板置假序列同构〔测速 V2〕",
str(da.seen) == str(drv_b.seen) and da.seen == [true, true, true]
and not a.is_running() and not b.is_running()
and not pa.running and not pb.running
and str(pa.running_log) == str(pb.running_log))
Kit.check("n=3 进度串序列逐位相等(%d 段)〔测速 V3〕",
str(pa.progress_log) == str(pb.progress_log) and pa.progress_log.size() == 4)
var got: Dictionary = r["b"].out_stats
var deaths: Dictionary = got.get("death_nodes", {})
Kit.check("n=3 夹具 stats 六键与按假驱动固定返回值重算的期望逐键相等〔测速 V4〕",
int(got.get("wins", -1)) == 2
and _arr_eq(got.get("dmg_all"), [12.0, 6.0, 12.0])
and _arr_eq(got.get("hp_left_win"), [3.0, 3.0])
and _arr_eq(got.get("hp_boss"), [5.0, 5.0])
and _arr_eq(got.get("rounds_all"), [4, 2, 4])
and deaths.size() == 1 and int(deaths.get("3.battle", -1)) == 1)
Kit.check("n=3 报告全文逐字相等且含渲染面四关键串〔测速 V5〕",
pa.report == pb.report and pa.report != ""
and pa.report.contains("关卡完成率") and pa.report.contains("关卡完成剩余 HP 均")
and pa.report.contains("败北节点") and pa.report.contains("N=3"))
func _check_n0(r: Dictionary) -> void:
var pa: FakePanel = r["pa"]
var pb: FakePanel = r["pb"]
var da: FakeDriver = r["da"]
var drv_b: FakeDriver = r["db"]
var a = r["a"]
var b = r["b"]
Kit.check("n=0 退化:两形态驱动零回调、帧索引序列空(同步收尾零帧)〔测速 V1〕",
da.frames.is_empty() and drv_b.frames.is_empty() and da.calls == 0 and drv_b.calls == 0)
Kit.check("n=0 运行态置假序列 [true, false] 双形态同构+收尾双假〔测速 V2〕",
str(pa.running_log) == str(pb.running_log) and pa.running_log == [true, false]
and not a.is_running() and not b.is_running())
Kit.check("n=0 进度串单段逐字相等〔测速 V3〕",
str(pa.progress_log) == str(pb.progress_log) and pa.progress_log == ["模拟中:0/0 …"])
Kit.check("n=0 夹具 stats 六键零计〔测速 V4〕",
int((r["b"].out_stats as Dictionary).get("wins", -1)) == 0
and ((r["b"].out_stats as Dictionary).get("dmg_all") as Array).is_empty()
and ((r["b"].out_stats as Dictionary).get("hp_left_win") as Array).is_empty()
and ((r["b"].out_stats as Dictionary).get("hp_boss") as Array).is_empty()
and ((r["b"].out_stats as Dictionary).get("rounds_all") as Array).is_empty()
and ((r["b"].out_stats as Dictionary).get("death_nodes") as Dictionary).is_empty())
Kit.check("n=0 报告全文逐字相等且非空(空 stats 渲染面)〔测速 V5〕",
pa.report == pb.report and pa.report != "")
# 帧序列归一(相对首帧偏移——串行驱动下两形态帧段错位、结构可比性归一承载)
func _norm(fr: Array) -> Array:
var out: Array = []
var base := int(fr[0]) if fr.size() > 0 else 0
for i in fr.size():
out.append(int(fr[i]) - base)
return out
# 相邻差恒一(每步恰占一帧——判据①的序列面证据)
func _consecutive(fr: Array) -> bool:
for i in range(1, fr.size()):
if int(fr[i]) - int(fr[i - 1]) != 1:
return false
return true
# 数组逐元素浮点比对(stats 桶重算期望——类型化比较,消 str 形态歧义)
func _arr_eq(a, b: Array) -> bool:
var aa := a as Array
if aa == null or aa.size() != b.size():
return false
for i in b.size():
if absf(float(aa[i]) - float(b[i])) > 0.0001:
return false
return true