Files
gplan-2/game/smoke/ui_click_probe.gd
T
DKChan 3dffbdea23 test(smoke): T4 探针独立性断言——独立屏激活期战斗屏宿主隐藏/战斗节点提交后恢复
ui_probe 入口停点段:+独立性断言二支(level_screen.visible 且 _battle_root.hidden=零透出契约;press_candidate 进首战后 _battle_root 恢复+level_screen 不可见=屏级互斥恢复);断言消息 route_overlay 残留清零+注释措辞随独立屏。ui_click_probe 入口段:+独立性断言(屏激活期宿主隐藏)。
轻闸=冒烟 557/0+五探针全 PASS 零脚本错误。
2026-09-28 06:45:04 +08:00

351 lines
17 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
# 实现 GDD:design/ux/battle.md(§AC 场景级测试 + §Interaction Map——UI 行为随机化探针 v3)。
# Date: 2026-09-12(战斗屏改造批扩展;基础 = 2026-09-11 问题3 复现器 v2——既有断言全保留)
# 合成鼠标事件走真实呈现层路径(BoardView.click_cell / 按钮信号 → 组合根动作通道),
# 随机混合玩家行为——插旗/拔旗、提前停手、弃图刷新确认链、图例模态、明细回看模态、
# tooltip 悬停、无效点击——扫描结算停滞(缺陷②回归载体;缺陷④窗口契约随道具窗口删除退役 2026-09-15)
# + 模态遮挡断言(缺陷⑤回归:图例/回看/终局 overlay 打开时上方遮挡层隐藏、输入锁定)
# + 性能 AC:场景实例化到首格可点 ≤500ms 探针计时。
# 行为序列由 PROBE_SEED 决定(shell 传参);程序玩法数值不受影响(随机只用于探针策略)。
# 运行:PROBE_SEED=7 Godot_console.exe --headless --path . res://smoke/ui_click_probe.tscn
extends Node
const AutoPlayer := preload("res://smoke/auto_player.gd") # O4:自动玩家策略单一实现
var main
var _last_state := {}
var _stall_frames := 0
var _done := false
var _rand := RandomNumberGenerator.new()
var _flags_placed: Array = []
var _ap_spent_this_round := 0
var _problems: Array = [] # 新增覆盖的问题收集(退出码合并判定)
var _tested_legend := false
var _tested_review := false
var _tested_refresh_chain := false
var _tested_tooltip := false
var _inter_confirms := 0 # 大奖励屏确认计数(冒险推进停点——首见确认推进 L2,再见 = L2 通关收口)
var _route_clicks := 0 # 选路覆盖层点击计数(岔路使用率观测面)
func _ready() -> void:
var seed_env := OS.get_environment("PROBE_SEED")
_rand.seed = int(seed_env) if seed_env.is_valid_int() else 7
await get_tree().process_frame
var t0 := Time.get_ticks_msec()
main = load("res://presentation/main.tscn").instantiate()
add_child(main)
await get_tree().process_frame
var t_ui_ready := Time.get_ticks_msec() - t0
# ---- 营地购买合成点击路径(元进度实现批——campbtn/购买/关闭全走真实按钮信号路径;
# 隔离存档换注 main.meta 保确定性(禁碰真实档 user://save.json);陈旧档段首清理)----
for p0 in ["user://smoke_click_probe_camp.json", "user://smoke_click_probe_camp.json.tmp",
"user://smoke_click_probe_reset.json", "user://smoke_click_probe_reset.json.tmp"]:
if FileAccess.file_exists(p0):
DirAccess.remove_absolute(p0)
var iso_c = load("res://core/save_manager.gd").new("user://smoke_click_probe_camp.json")
iso_c.load_from_disk()
main.meta = load("res://core/meta_progression.gd").new(main.db, iso_c)
var campbtn: Button = _find_button_by_prefix(main._start_overlay, "营地")
if campbtn == null:
_problems.append("营地入口按钮缺失(开始层 campbtn)")
else:
campbtn.pressed.emit() # 真实按钮信号路径 → 组合根 _open_camp
await get_tree().process_frame
if not main.camp_overlay.visible:
_problems.append("营地层未打开(点击路径)")
elif main.camp_overlay._forge_rows.is_empty():
_problems.append("锻造柜台行缺失(点击路径)")
else:
var buy_btn: Button = main.camp_overlay._forge_rows[0].btn
if not buy_btn.disabled:
_problems.append("零余额锻造购买按钮未禁用(禁用态失守)")
iso_c.add_keystone(9999)
main.camp_overlay.refresh_camp(main._camp_data())
await get_tree().process_frame
if buy_btn.disabled:
_problems.append("余额充足后购买按钮仍禁用(禁用态未随刷新)")
var bal_before: int = iso_c.keystone_balance()
var buy_attr: String = String(main.camp_overlay._forge_rows[0].attr)
var price: int = main.meta.forge_price(buy_attr)
buy_btn.pressed.emit() # 合成购买点击 → _on_camp_buy → meta.buy_forge(写调用唯一归组合根)
await get_tree().process_frame
if int(iso_c.keystone_balance()) != bal_before - price:
_problems.append("营地购买未出账(%d→%d 应 −%d)" % [bal_before, iso_c.keystone_balance(), price])
var iso_chk = load("res://core/save_manager.gd").new("user://smoke_click_probe_camp.json")
iso_chk.load_from_disk()
if int(iso_chk.forge_level(buy_attr)) != 1:
_problems.append("营地购买未持久(reload 后级 != 1)")
main._on_camp_closed()
await get_tree().process_frame
if main.camp_overlay.visible or not main._start_overlay.visible:
_problems.append("营地关闭后未恢复开始层")
# 换回全新零档(营地购买态不泄入后续冒险段——确定性;档案残留下次段首清理兜底)
var iso_reset = load("res://core/save_manager.gd").new("user://smoke_click_probe_reset.json")
iso_reset.load_from_disk()
main.meta = load("res://core/meta_progression.gd").new(main.db, iso_reset)
main.tune_panel.set_sliders(400, 200) # 高加成保 L1 可通关——L2 岔路选路段只能经冒险推进到达(单关直入口退役)
var t1 := Time.get_ticks_msec()
main._start_adventure_from_menu()
await get_tree().process_frame
# 停点前移批 2026-09-23:关卡开始 = 入口选路停点——press_candidate 图上命中路径选入口候选进首战(候选按钮退役 2026-09-28)
#(首格可点 AC 口径随之含入口选路一步:菜单点击 → 停点呈现 → pick → 棋盘安装)
if main.level_screen.visible:
# 独立性断言(形态升格批 2026-09-28):独立屏激活期战斗屏宿主整体隐藏
if main._battle_root.visible:
_problems.append("独立屏激活期战斗屏宿主未隐藏(零透出契约失守)")
main.level_screen.press_candidate(0)
await get_tree().process_frame
await get_tree().process_frame
var t_entry := Time.get_ticks_msec() - t1
# 性能 AC:进入战斗屏到首格可点 ≤500ms(棋盘 install 完成口径);
# 场景实例化冷启动总时长(含全部脚本首载编译——引擎启动域,非 AC 口径)另列观测
var t_total := Time.get_ticks_msec() - t0
var first_cell_ms := t_entry
if first_cell_ms > 500:
_problems.append("进入战斗屏首格可点超时 %dms > 500ms" % first_cell_ms)
if main.board_view._cells.is_empty():
_problems.append("首格未可点(cells=0)")
print("[click_probe] 首格可点计时 %dms(进入战斗屏口径,≤500ms AC)· 场景冷启动 %dms(含 UI 构建 %dms,观测项)" % [
first_cell_ms, t_total, t_ui_ready])
var action_budget := 20000
while not _done and action_budget > 0:
action_budget -= 1
await get_tree().process_frame
if main.node_overlay.visible:
main.node_overlay.press_continue()
_flags_placed.clear()
continue
if main.level_screen.visible:
# 节点图选路口(零自动推进停点)——图上命中路径交替选候选(岔路使用行为;布局决策 2 改造)
main.level_screen.press_candidate(_route_clicks % 2)
_route_clicks += 1
_flags_placed.clear()
continue
if main.shop_overlay.visible:
# 商店房覆盖层(GP+商店房批——L2 商店为 Boss 前必经位):零购离开(真实 leave 信号路径)放行选路
main.shop_overlay.leave_pressed.emit()
_flags_placed.clear()
continue
if main._inter_overlay.visible:
# 大奖励屏(冒险级停点——非末关通关恰一次):首见确认推进 L2(真实按钮信号路径);再见 = L2 已通关,收口
if not main._input_locked():
_problems.append("大奖励屏打开时输入未锁定")
_inter_confirms += 1
if _inter_confirms == 1:
var next_btn: Button = _find_button_by_prefix(main._inter_overlay, "进入下一层")
if next_btn == null:
_problems.append("大奖励屏确认按钮缺失")
else:
next_btn.pressed.emit()
await get_tree().process_frame
await get_tree().process_frame
if main.runner == null or int(main.runner.level_row.get("level_index", -1)) != 2:
_problems.append("大奖励确认后未推进到第二关(level_index != 2)")
else:
_done = true
print("[click_probe] 第二关大奖励屏(L2 通关)——探针收口 · 选路口点击 %d 次" % _route_clicks)
break
_flags_placed.clear()
continue
if main._end_overlay.is_open():
# 冒险终局(失败/放弃/打穿)——单关直入口退役后无二段入口,见终局即收口(模态断言照旧)
if not main._input_locked():
_problems.append("终局 overlay 打开时输入未锁定")
_done = true
print("[click_probe] 冒险终局:%s · 选路口点击 %d 次" % [
main._end_overlay.verdict_text().replace("\n", " "), _route_clicks])
break
if main.settlement == null or main.runner == null:
continue
var diag: Dictionary = main.settlement.diagnostic_snapshot() # 诊断观测面(私有字段零直读)
var st := {
"node": main.runner.node_index, "round": main.settlement.round_index,
"phase": main.settlement.action_economy.phase, "ap": main.settlement.action_economy.ap,
"settling": diag.settling,
}
if JSON.stringify(st) == JSON.stringify(_last_state):
_stall_frames += 1
if _stall_frames == 30:
_report_stall(st)
return
else:
_stall_frames = 0
_ap_spent_this_round = 0
_last_state = st
if main.settlement.battle_result == "" and main.settlement.action_economy.phase == "Active":
_run_modal_probes_once()
_play_one_action()
print("[click_probe] 循环退出(done=%s budget=%s)问题=%d" % [
str(_done), str(action_budget), _problems.size()])
for p in _problems:
push_error("[click_probe] FAIL — " + p)
var exit_code := 0
if not _done:
exit_code = 1
if not _problems.is_empty():
exit_code = 1
get_tree().quit(exit_code)
# ---- 模态/tooltip/刷新链一次性覆盖(真实信号路径驱动) ----
func _run_modal_probes_once() -> void:
# ① 图例模态(pattern 6):打开 → 遮挡物隐藏 + 输入锁定;关闭 → 恢复
if not _tested_legend:
_tested_legend = true
main.ap_bar._btn_legend.pressed.emit()
if not main.legend_overlay.is_open():
_problems.append("图例打开失败")
if main.review_overlay.is_open() or main._end_overlay.is_open() or main._start_overlay.visible:
_problems.append("图例打开时上方遮挡层未隐藏(缺陷⑤复发)")
if not main._input_locked():
_problems.append("图例打开时棋盘输入未锁定")
var legend_has_flag := false
for e in main.legend_overlay.ENTRIES:
if String(e[1]) == "旗":
legend_has_flag = true
if not legend_has_flag:
_problems.append("图例缺『旗』格态条目(格态图例基线)")
main.legend_overlay.close()
if main.legend_overlay.is_open() or main._input_locked():
_problems.append("图例关闭后未恢复输入/可见性")
# ② 结算明细回看模态(pattern 6):B 区明细带点击入口 → overlay → 关闭恢复
if not _tested_review:
_tested_review = true
main._open_review_from_band()
if not main.review_overlay.is_open():
_problems.append("明细回看打开失败")
if not main._input_locked():
_problems.append("明细回看打开时输入未锁定")
main.review_overlay.close()
if main.review_overlay.is_open() or main._input_locked():
_problems.append("明细回看关闭后未恢复")
# ③ tooltip(状态效果槽悬停——来源+语义+剩余):有槽可见时触发
if not _tested_tooltip:
var row = main.stage.hero_standee.slot_row # 状态 tooltip 载体 2026-09-26 迁 B 区站桩
for child in row.get_children():
if child is PanelContainer and child.visible and child.has_meta("eff"):
_tested_tooltip = true
child.mouse_entered.emit()
if not row._tip.visible:
_problems.append("状态槽 tooltip 未显示")
elif not row._tip_label.text.contains("来源") or not row._tip_label.text.contains("剩余"):
_problems.append("状态槽 tooltip 缺来源/剩余(%s)" % row._tip_label.text.replace("\n", "|"))
child.mouse_exited.emit()
if row._tip.visible:
_problems.append("状态槽 tooltip 未随移出隐藏")
break
# ④ 弃图刷新确认链(pattern 7 异位:确认 chips 与常规确认不同位 + 显式代价文案)
if not _tested_refresh_chain and main.ap_bar.is_refresh_ready():
_tested_refresh_chain = true
var rap_before: int = int(main.settlement.action_economy.rap)
var board_before = main.settlement.minesweeper.current_board()
main.ap_bar.press_refresh_for_probe()
if not main.ap_bar.confirm_visible():
_problems.append("弃图刷新确认 chips 未出现(异位确认缺失)")
main.ap_bar.press_confirm_for_probe()
if int(main.settlement.action_economy.rap) != rap_before - 8:
_problems.append("弃图刷新未扣 RAP8(%d→%d)" % [rap_before, int(main.settlement.action_economy.rap)])
if main.settlement.action_economy.phase != "Active":
_problems.append("轮内刷新后相位丢失(%s)" % main.settlement.action_economy.phase)
if main.settlement.minesweeper.current_board() == board_before:
_problems.append("轮内刷新未当场换图")
if main._refresh_used_round != 1:
_problems.append("刷新轮内计数错误(%d)" % main._refresh_used_round)
func _play_one_action() -> void:
var settlement = main.settlement
if settlement == null:
return # 结算竞态防抖:battle_finished 与本轮动作调度同帧到达时 settlement 可能已被置空(基线既有噪声——加固)
var minesweeper = settlement.minesweeper
# 行为①:随机插旗(右键——未开相邻格),约 20% 轮次
if _ap_spent_this_round == 0 and _rand.randf() < 0.2:
var target := _random_unknown_adjacent(minesweeper)
if target.x >= 0:
main.board_view.click_cell(target, MOUSE_BUTTON_RIGHT)
_flags_placed.append(target)
_ap_spent_this_round += 1
return
# 行为②:随机拔旗(右键已插旗格),约 10% 轮次
if _rand.randf() < 0.1 and not _flags_placed.is_empty():
var fc: Vector2i = _flags_placed.pop_back()
if minesweeper.has_flag(fc) and not minesweeper.is_revealed(fc):
main.board_view.click_cell(fc, MOUSE_BUTTON_RIGHT)
_ap_spent_this_round += 1
return
# 行为③:随机无效点击(非相邻格/已开格),约 8% 轮次
if _rand.randf() < 0.08:
main.board_view.click_cell(Vector2i(_rand.randi_range(0, minesweeper.current_board().width - 1), _rand.randi_range(0, minesweeper.current_board().height - 1)), MOUSE_BUTTON_LEFT)
return
# 挖掘主策略(最近可挖格)
var plan = AutoPlayer.choose_dig(minesweeper) # O4 收口:策略单一实现
if not plan.is_empty():
if plan.hero_cell != minesweeper.hero_pos():
main.board_view.click_cell(plan.hero_cell, MOUSE_BUTTON_LEFT)
main.board_view.click_cell(plan.dig_cell, MOUSE_BUTTON_LEFT)
_ap_spent_this_round += 1
# 行为④:提前停手(已花 ≥3 AP 时 15%)——停手经 AP 条按钮真实信号路径
if _ap_spent_this_round >= 3 and settlement.action_economy.phase == "Active" and _rand.randf() < 0.15:
_click_stop()
elif settlement.action_economy.phase == "Active":
_click_stop()
func _random_unknown_adjacent(minesweeper) -> Vector2i:
var hero: Vector2i = minesweeper.hero_pos()
var cands: Array = []
for dy in range(-1, 2):
for dx in range(-1, 2):
if dx == 0 and dy == 0:
continue
var c: Vector2i = hero + Vector2i(dx, dy)
if minesweeper.current_board().in_bounds(c) and not minesweeper.is_revealed(c) and not minesweeper.has_flag(c):
cands.append(c)
if cands.is_empty():
return Vector2i(-1, -1)
return cands[_rand.randi_range(0, cands.size() - 1)]
func _click_stop() -> void:
# 停手走真实按钮信号路径(AP 条 → 组合根 settle_batch——排雷玩法 D1×6① 回归载体)
main.ap_bar._btn_stop.pressed.emit()
func _report_stall(st: Dictionary) -> void:
var why := ""
if st.phase == "Waiting":
why = "结算停滞:Waiting 相位无推进(结算应同步走完——窗口段已删除)"
elif st.phase == "Active" and st.ap == 0:
why = "Active 相位 AP=0(应在归零瞬间翻 Waiting)"
elif st.phase == "Idle" and main.settlement.battle_result == "":
why = "Idle 相位但战斗未结束(begin 失败?)"
else:
why = "状态无推进(battle_result='%s')" % str(main.settlement.battle_result)
print("[click_probe] 🔴 卡死复现(PROBE_SEED=%s):%s" % [OS.get_environment("PROBE_SEED"), why])
_dump_state()
get_tree().quit(2)
func _dump_state() -> void:
var diag: Dictionary = main.settlement.diagnostic_snapshot() # 诊断观测面(私有字段零直读)
print(" node_index=%s round=%s phase=%s ap=%s rap=%s" % [
str(main.runner.node_index), str(main.settlement.round_index),
str(main.settlement.action_economy.phase), str(main.settlement.action_economy.ap), str(main.settlement.action_economy.rap)])
print(" battle_result='%s' settling=%s" % [
str(diag.battle_result), str(diag.settling)])
print(" board_installed=%s facts_path='%s' refresh_used=%s flags=%s" % [
str(diag.minesweeper_board_installed), str(diag.facts_path), str(diag.refresh_used_round), str(_flags_placed)])
# 开始层按钮按文案前缀寻址(campbtn 句柄未存组合根成员——树遍历只读;与 ui_probe 同款助手)
func _find_button_by_prefix(root: Node, prefix: String) -> Button:
if root is Button and String(root.text).begins_with(prefix):
return root
for child in root.get_children():
var found := _find_button_by_prefix(child, prefix)
if found != null:
return found
return null