Files
gplan-2/game/tests/unit/presentation/pacing_queue_test.gd
T
DKChan ea322599f7 卡②(结构小件批p1-p7+V15): 七点定点修落地+运行期违约通道负面套件+wr10 偏运缝拆验收——①p1 状态角标判据去重(main_overlay_flow._sync_status_entry 一组重复赋值语句删除,battle_live 局部量随之退役=wr1 载体);②p2 确认窗开合归模态栈单口(main 两处 _on_confirm_* 改 _close_modal;存档删除入口改 root._open_modal(root.confirm_dialog) 后再 open,遮挡记账/挂起兜底/开侧同步同口);③p3 棋盘动作与 stop 两入口显式 settlement 空防护(锁谓词异常时不崩于空引用);④p4 会话更替即清队:_begin_session 首行 _row_pacer.flush()(待播行不住局态=机制保证非时序约定);⑤p5 缝拆:RewardEconomy 删跨域直持(_fx/_ctx/set_fx)改注入 luck_bias_query: Callable,reward_roll 发射与 bias_rewards 求和收回 BattleFxHost.luck_bias_for(cell_count)(三参形 dispatch,不带 facts——禁复用 fx_emit 的第四实参会打开 when 事实条件面),组装点 set_fx 一行换赋值一行 1 换 1;类变量 15−2+2 净 15,gdstyle 无第 16 枚;⑥p6/p7 RngService 未知用途标签与未播种取数两分支加 violation_handler 记录位+release 下 return null 空引用即刻崩(fail-fast 传导,合法路径不可达)——debug 下 assert 先中断函数本体两态不可判别,故记录位是唯一可观测面;⑦board_generator/reward_economy 各加 fatal_handler 注入位并由组装点接 orchestrator._fatal(release 剥 assert 后仍经 battle_result 封印真中止)。新件 tests/unit/core/run_time_fatal_test.gd(manifest 登记 expect=1 negative=true):wr5 协作件 _fatal 经组装点传导 p5 封印+缺省形态不外溢/wr6 未知标签通道/wr7 未播种通道(各用新建 RngService 实例,播种断言 _counter==0)。wr10 三条入 variant_placement_test=判定面夹具白盒装载(现网数据无 bias_rewards 行,沿 tree_rules_v11_test 先例)+接线同源+权重生效 77+4*2。验收:variant_placement 49 checks 全绿(wr10 ①②③ PASS:空订阅 0/命中 1 行/装载后 2;查询口有效且与宿主同源;偏运池实测 85);run_time_fatal_test 退出码 1 三点绿;pacing_queue 绿(wr4 前置夹具真入队 2 行→更替后清零);ui_click_probe 问题 0(wr2 遮挡账开/关两侧断言);round_flow_skill_pool 130 checks 绿;wr11 dump_battle 六组(L{1,2}×S{42,779401,779501})与改前 before_* 逐字节 cmp 全等;gdstyle 基线外新违规 0、恶化 1(level_map_screen 类变量 22>21=iter38 存量非本批触碰面)、基线文件零改动〔结构小件批 V15〕
2026-10-27 18:39:53 +08:00

169 lines
7.8 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:
if not cond:
_problems.append(msg)
func _init() -> void:
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.attach_path("user://test_pacing_queue_save.json")
sm.mark_first_adventure_done()
# 行信号计数器(真实信号路径旁挂计数——零触碰既有接线)
var rows_seen: Array = []
main._start_adventure_from_menu()
await process_frame
# 入口停点选路进首战(真实停点链)
if not main.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._flush_row_queue()
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 完整性)")
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.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)
var ffx := FakeFx.new()
rp.stage = fstage
rp.board_view = main.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._row_queue.size() == 2, "会话更替夹具已入队(前提:%d 行)〔wr4〕" % main._row_queue.size())
main._begin_session(false)
check(main._row_queue.is_empty(), "会话更替后待播队列清零(_begin_session 内机制 flush)〔wr4〕")
_finish()
func _count_row(row: Dictionary, arr: Array) -> void:
arr.append(row.duplicate(true))
func _finish() -> void:
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)