Files
gplan-2/game/tests/unit/presentation/pacing_queue_test.gd
T

172 lines
8.4 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# VERTICAL SLICE - NOT FOR PRODUCTION
# 轮间结算节奏化断言(逻辑先行断言面):
# ① 演出队列排空前后核心逻辑状态逐位一致(演出关停不产生分叉——排播队列零核心触碰的结构证明);
# ② 零丢行:step_row 信号总数 = 明细带实收行数(排播/排空两路都不吞行);
# ③ 段间延迟取数面双态(常态 >0 / 减动效 =0 归零不跳过——pace_delay_for 直读)。
# 载体:main.tscn 实例(同 ui_click_probe 形态——组合根全链在座),真实冒险驱动至首战结算后断言。
# 运行:godot --headless --path game --script res://tests/unit/presentation/pacing_queue_test.gd(expect 0)
extends SceneTree
const RowPacerScript := preload("res://presentation/row_pacer.gd")
var _problems: Array = []
# 记参假站桩(d12 排播半):继承真类以过类型面,只记解析入参
class FakeStage extends BattleStageView:
var calls: Array[Array] = []
func enemy_standee_center(target_id: String, display_name: String) -> Variant:
calls.append([target_id, display_name])
return Vector2(10, 10)
# 记参假演出层(同上——play_* 全部只记不演)
class FakeFx extends SettlementFxView:
var calls: Array[Array] = []
func play_projectile(_from: Vector2, _to: Vector2, _glyph: String, _col: Color, _crit: bool = false) -> void:
calls.append(["projectile", _to])
func play_damage_number(_pos: Vector2, _text: String, _col: Color, _crit: bool = false) -> void:
calls.append(["mark", _pos])
func play_status_pulse(_pos: Vector2, _col: Color) -> void:
calls.append(["pulse", _pos])
func check(cond: bool, msg: String) -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
if not cond:
_problems.append(msg)
func _init() -> void:
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:272 (排播节奏——排空零分叉/零丢行/延迟双态组)
var main: Node = load("res://presentation/main.tscn").instantiate()
root.add_child(main)
await process_frame
# 隔离换注(SaveManager 是 autoload——原地 attach:先删后建防陈旧档泄入,
# 读盘在 attach 内完成;meta 经 save 引用透传零重建)
for p in ["user://test_pacing_queue_save.json", "user://test_pacing_queue_save.json.tmp"]:
if FileAccess.file_exists(p):
DirAccess.remove_absolute(p)
var sm = main.get_node("/root/SaveManager")
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()
await process_frame
# 入口停点选路进首战(真实停点链)
if not main.ui.modals.run.level_screen.visible:
_problems.append("入口停点未呈现(前置失守)")
_finish()
return
main.run_scope.runner.pick_next_node(int(main.run_scope.runner.next_candidate_ids()[0]))
await process_frame
# 等首战开打(board_changed 同步链——两帧余量)
await process_frame
var battle = main.run_scope.runner.current_battle()
if battle == null:
_problems.append("首战未开打(排播断言无载体)")
_finish()
return
battle.step_row.connect(_count_row.bind(rows_seen))
# 理性策略挖到触发结算(≥1 行入队即达断言前提——AP 一次消耗;挖格须邻英雄——邻接优先排序)
var ms = battle.minesweeper
# 理性策略驱动(auto_player 单一实现复用——damage_probe 同款两步消费:先 move 到 hero_cell 再 dig)
var ap = load("res://sim/auto_player.gd")
var acted := false
for guard in range(40):
var plan: Dictionary = ap.choose_dig(ms)
if plan.is_empty():
break
if plan.hero_cell != ms.hero_pos():
ms.settle_batch([{"action": "move", "cell": plan.hero_cell}])
if battle.action_economy.phase != "Active":
break
var outcomes: Array = ms.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
acted = not outcomes.is_empty() and bool(outcomes[0].accepted)
if acted:
break
# 轮末结算触发(step_row 行族只在轮末 emit——停手即结,damage_probe 同款)
if battle.action_economy.phase == "Active":
ms.settle_batch([{"action": "stop"}])
if not acted:
_problems.append("首战无受理动作(队列断言无行)")
_finish()
return
await process_frame
# ---- 断言①:排空前后核心状态逐位一致(演出关停零分叉)----
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.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.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]
last_text = String(cand_row.get("text", ""))
if last_text == "":
last_text = String(cand_row.get("display_name", ""))
if last_text != "":
break
check(last_text != "" and main.ui.battle.log_view.parsed_text().contains(last_text),
"末信号行原文在明细带(排空不吞行:『%s』)" % last_text)
# ---- 断言③:段间延迟取数面双态 ----
UiKit.motion_scale = 1.0
var d_on: float = main.pace_delay_for("damage", "heal")
var d_same: float = main.pace_delay_for("damage", "damage")
check(d_on > 0.0 and d_on >= d_same, "常态段间延迟 >0 且 ≥ 段内(%f/%f)" % [d_on, d_same])
UiKit.motion_scale = 0.5
check(main.pace_delay_for("damage", "heal") == 0.0, "减动效段间延迟归零(行不跳过——同帧直落快路)")
UiKit.motion_scale = 1.0
# ---- 断言④:排播两处取用按行 target_id 解析站桩(d12 排播半——假 stage 记参)----
var rp = RowPacerScript.new()
var fstage := FakeStage.new(null, root.get_node("/root/ConfigDB").presentation.battle_layout)
var ffx := FakeFx.new()
rp.stage = fstage
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", "地精"],
"结算标出按行 target_id 解析站桩(id 与名同传——d12)〔wd12〕")
rp._play_cross_zone_fx({"type": "damage", "target": "地精", "target_id": "goblin_b", "value": 3})
check(fstage.calls.size() == 2 and fstage.calls[1] == ["goblin_b", "地精"],
"跨区弹道按行 target_id 解析站桩(id 与名同传——d12)〔wd12〕")
check(ffx.calls.size() == 2, "两路解析成功后演出层各接一拍(记参面非空)〔wd12〕")
rp.free()
# ---- 断言⑤:会话更替即清队(wr4——前置夹具入队再走真实入口 _begin_session;
# 无夹具则改前改后皆绿:flush 无队列可清时两种形态同值)----
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.ui.screens.row_pacer.pending_rows().size() == 2, "会话更替夹具已入队(前提:%d 行)〔wr4〕" % main.ui.screens.row_pacer.pending_rows().size())
main.begin_session(false)
check(main.ui.screens.row_pacer.pending_rows().is_empty(), "会话更替后待播队列清零(_begin_session 内机制 flush)〔wr4〕")
_finish()
func _count_row(row: Dictionary, arr: Array) -> void:
arr.append(row.duplicate(true))
func _finish() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
for p in ["user://test_pacing_queue_save.json", "user://test_pacing_queue_save.json.tmp"]:
if FileAccess.file_exists(p):
DirAccess.remove_absolute(p)
if _problems.is_empty():
print("[pacing_queue_test] PASS — 排空零分叉/零丢行/延迟双态 全绿")
quit(0)
else:
for p in _problems:
push_error("[pacing_queue_test] FAIL — " + p)
print("[pacing_queue_test] FAIL — " + p)
quit(1)