批3(QA harness·C自动试玩扩展): 新增game/sim/journey_player.gd+.tscn全动线机器自玩驱动件(开机→设置→存档三操作新档/删档/进档→营地五窗逐窗→Esc回退→大门开冒险→关卡推进→战斗→停点分发→终局收束→回菜单); 战斗策略委托既有auto_player,按钮/物件全真输入管线(ProbeUiUtil单点),两方法口如实标通道(地图候选=press_candidate自绘布局命中,棋盘格=click_cell BoardView公共口——棋盘管线面归ui_input_probe); 异常三类上报含机器自填归因初判+通道标签,fatal=诊断收集器级(battle_result=fatal_error),段间超时30s+战斗状态挂死90帧,屏互斥六对检查; 误报指纹ledger跨跑持久(同源第2次自动静默); 报告journey_report.js+命中30控件并入覆盖登记。读数: 首跑16类屏到达/上报2=机器首跑即复现在案缺陷「开局被开始层盖住」(营地交互修复批承接中——开始层可见直入locks_input谓词锁死战斗,C上报后夹具面隐藏继续走查,缺陷本体不掩盖); 二跑上报0/静默2/PASS exit=0(收敛机制实测生效); 冒险段0 fatal。实施期自修两坑: 陈旧槽文件夹具卫生(段首清槽)+开层排版帧(选档钮/面板继续钮同帧点击落空——选档空转假红与输入锁滞留两形态)。方案偏差6条回写qa-harness讨论文档§12
This commit is contained in:
1 parent
8d8e4255c3
commit
d7d4bc87d3
3 files changed
+594
-1
No files matched your search
@@ -0,0 +1,587 @@
|
||||
# journey_player.gd — 全动线机器自玩驱动(C·发现环节去人化)
|
||||
# 是什么:headless 场景驱动件——实例化 main、隔离存档,按旅程段驱动全动线(开机→设置→存档三操作
|
||||
# 新档/删档/进档→营地功能窗逐窗开合→Esc 回退→大门开冒险→关卡推进→战斗→各停点→终局收束→回菜单),
|
||||
# 每屏到达记账+段间异常上报,收尾落 JSONP 报告并入探针覆盖登记(真输入事件同源记账)。
|
||||
# 判据(契约 §5):每个屏到达过+段间零 fatal。
|
||||
# 异常上报三类(其余静默):①诊断收集器 fatal 级(settlement battle_result=fatal_error/fatal 屏)
|
||||
# ②段间超时(单段 >30s 未达预期停点;战斗段另设状态无变化挂死阈值)③屏状态与预期不符。
|
||||
# RID 泄漏等已知日志噪音静默。
|
||||
# 归因字段:每条上报机器自填——段名/期望屏/实测屏/上一动作/通道标签(真实输入-按钮/
|
||||
# 真实输入-场景热点/真实输入-键盘/方法-地图命中口/方法-棋盘口)。原归因制度残骸=本结构内字段。
|
||||
# 误报收敛:误报指纹(类+段+归因摘要)落 .gates/coverage/journey_misfire_ledger.json——同一指纹
|
||||
# 第 2 次起自动登记静默(计数照记、不再进上报清单);连 3 批零真缺陷判据收紧归用户复核轮。
|
||||
# 驱动纪律:按钮/场景物件全走真输入(push_input 管线,同探针判据——ProbeUiUtil 单点通道)。
|
||||
# 两处方法口如实标注通道:地图候选=press_candidate(自绘布局命中路径,探针族既定入口);
|
||||
# 棋盘格=click_cell(BoardView 公共点击口,ui_input_probe 先例——棋盘管线面由该探针承载)。
|
||||
# 停点分发的视图内部读数(_continue_btn/_node_pending 等)沿用探针族直读受控例外(iter53 反哺点 9 同款在册)。
|
||||
# 运行:Godot_console.exe --headless --path . res://sim/journey_player.tscn
|
||||
extends Node
|
||||
|
||||
const ProbeUiUtil := preload("res://probe/probe_ui_util.gd")
|
||||
const ProbeCoverageRecorder := preload("res://probe/probe_coverage_recorder.gd")
|
||||
const AutoPlayer := preload("res://sim/auto_player.gd")
|
||||
|
||||
const SEGMENT_TIMEOUT_MS := 30000 # 段间超时判据(契约 §5——单段 30s 未达预期停点)
|
||||
const BATTLE_STALL_FRAMES := 90 # 战斗段状态无变化挂死阈值(演播期豁免同探针口径)
|
||||
const ADVENTURE_BUDGET_FRAMES := 60000 # 冒险段帧预算(全停点循环兜底)
|
||||
const MISFIRE_PATH := "res://tests/.gates/coverage/journey_misfire_ledger.json"
|
||||
const REPORT_PATH := "res://tests/.gates/coverage/journey_report.js"
|
||||
|
||||
var main: Node
|
||||
var _recorder: RefCounted = null
|
||||
var _problems: Array = [] # 异常上报(三类;指纹二次起静默不入此列)
|
||||
var _silent_count := 0 # 误报静默笔数(回执可见)
|
||||
var _screens_seen: Dictionary = {} # 屏到达记账(判据面)
|
||||
var _screen_trace: Array = [] # 屏到达变迁轨迹(诊断面——新屏首现时点+上一动作)
|
||||
var _misfire: Dictionary = {} # 指纹 → 次数(跨跑落盘)
|
||||
var _last_state := {} # 战斗段状态哈希(挂死检测)
|
||||
var _stall := 0
|
||||
var _last_action := "(无)" # 归因初判:上一动作描述
|
||||
var _map_status_done := false # 地图屏状态角标一次性
|
||||
var _legend_done := false # 图例开合一次性(每战斗面一次)
|
||||
var _status_done := false # 战斗屏状态面板一次性
|
||||
var _refresh_done := false # 刷新确认链一次性
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
await get_tree().process_frame
|
||||
main = load("res://presentation/main.tscn").instantiate()
|
||||
add_child(main)
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
_load_misfire()
|
||||
_recorder = ProbeCoverageRecorder.new()
|
||||
_recorder.attach(main, "journey")
|
||||
ProbeUiUtil.isolate_save(main, "journey_player", false, false, true)
|
||||
main.save_manager.slots.attach_slot_template("user://journey_player_slot_%d.json")
|
||||
for i in range(1, 4): # 夹具清场——陈旧槽文件会翻有档态、新档钮缺席(open_camp_via_savesel 同款坑注)
|
||||
for suf in ["", ".tmp", ".corrupt.bak"]:
|
||||
var sp: String = main.save_manager.slots.slot_path(i) + suf
|
||||
if FileAccess.file_exists(sp):
|
||||
DirAccess.remove_absolute(sp)
|
||||
main.ui.modals.menu.debug_overlay.set_sliders(400, 200) # 高加成保 L1 可完成(探针先例——夹具注)
|
||||
main.ui.modals.menu.debug_overlay.seed_edit.text = "42"
|
||||
|
||||
await _phase_expect("开机落菜单", ["菜单"])
|
||||
await _phase_menu_settings()
|
||||
await _phase_new_slot(1)
|
||||
await _phase_camp_windows()
|
||||
await _phase_camp_esc_return()
|
||||
await _phase_new_and_delete(2)
|
||||
await _phase_load_slot(1)
|
||||
await _phase_gate_start()
|
||||
await _phase_adventure()
|
||||
await _phase_end_back_menu()
|
||||
|
||||
_recorder.dump_js()
|
||||
_dump_report()
|
||||
_save_misfire()
|
||||
var fatal := _fatal_count()
|
||||
if _problems.is_empty() and fatal == 0 and _screens_seen.has("菜单") and _screens_seen.has("终局"):
|
||||
print("[journey] PASS — 全动线走查完成(屏到达 %d 类,异常上报 0,fatal 0,误报静默 %d)" % [
|
||||
_screens_seen.size(), _silent_count])
|
||||
get_tree().quit(0)
|
||||
return
|
||||
print("[journey] FAIL — 异常 %d(静默 %d)/ fatal %d / 屏到达 %d 类" % [
|
||||
_problems.size(), _silent_count, fatal, _screens_seen.size()])
|
||||
for p in _problems:
|
||||
print("[journey] - " + JSON.stringify(p, " "))
|
||||
get_tree().quit(1)
|
||||
|
||||
|
||||
# ---- 屏状态读取与记账 ----
|
||||
|
||||
func _screen_state() -> Dictionary:
|
||||
var st := {
|
||||
"菜单": main.ui.screens.start_overlay != null and main.ui.screens.start_overlay.visible,
|
||||
"选档": main.ui.modals.menu.save_select_overlay.visible,
|
||||
"设置": main.ui.modals.menu.settings_overlay.visible,
|
||||
"生涯": main.ui.modals.menu.career_overlay.visible,
|
||||
"确认弹窗": main.ui.modals.menu.confirm_dialog.is_open(),
|
||||
"营地": main.ui.modals.camp.scene.visible,
|
||||
"锻造窗": main.ui.modals.camp.forge.visible,
|
||||
"树窗": main.ui.modals.camp.tree.visible,
|
||||
"秘宝窗": main.ui.modals.camp.treasure.visible,
|
||||
"图鉴窗": main.ui.modals.camp.codex.visible,
|
||||
"Esc浮窗": main.ui.modals.camp.esc_overlay.visible,
|
||||
"冒险": main.run_scope != null and main.run_scope.is_adventure,
|
||||
"关卡屏": main.ui.modals.run.level_screen != null and main.ui.modals.run.level_screen.visible,
|
||||
"三选一": main.ui.modals.run.curio_overlay.visible,
|
||||
"商店": main.ui.modals.menu.shop_overlay.visible,
|
||||
"大奖励": main.ui.screens.inter_overlay != null and main.ui.screens.inter_overlay.visible,
|
||||
"节点": main.ui.modals.run.node_overlay.visible,
|
||||
"终局": main.ui.modals.run.end_overlay != null and main.ui.modals.run.end_overlay.is_open(),
|
||||
}
|
||||
var in_battle: bool = bool(st["冒险"]) and not bool(st["关卡屏"]) and not bool(st["终局"]) \
|
||||
and main.run_scope != null and main.run_scope.settlement != null \
|
||||
and String(main.run_scope.settlement.battle_result) == ""
|
||||
st["战斗"] = in_battle
|
||||
return st
|
||||
|
||||
|
||||
func _record_screens() -> void:
|
||||
var st := _screen_state()
|
||||
for k in st:
|
||||
if bool(st[k]) and not _screens_seen.has(k):
|
||||
_screens_seen[k] = true
|
||||
_screen_trace.append({"t": Time.get_datetime_string_from_system(), "screen": k, "last_action": _last_action})
|
||||
_check_exclusion(st)
|
||||
|
||||
|
||||
var _excl_reported: Dictionary = {} # 屏互斥违约对(本跑一次性上报——跨跑由误报指纹静默)
|
||||
|
||||
# 屏级互斥(探针同款判据对):合法叠加仅「终局盖冒险」;违约=③屏状态与预期不符
|
||||
func _check_exclusion(st: Dictionary) -> void:
|
||||
var pairs := [["菜单", "营地"], ["菜单", "冒险"], ["营地", "冒险"], ["终局", "营地"], ["终局", "菜单"], ["菜单", "战斗"]]
|
||||
for pair in pairs:
|
||||
if bool(st.get(pair[0], false)) and bool(st.get(pair[1], false)):
|
||||
var fk: String = str(pair[0]) + "×" + str(pair[1])
|
||||
if not _excl_reported.has(fk):
|
||||
_excl_reported[fk] = true
|
||||
_report("screen_mismatch", "屏级互斥", "%s 与 %s 不应同场" % [pair[0], pair[1]])
|
||||
|
||||
|
||||
func _active_screens() -> Array:
|
||||
var on: Array = []
|
||||
var st := _screen_state()
|
||||
for k in st:
|
||||
if bool(st[k]):
|
||||
on.append(k)
|
||||
return on
|
||||
|
||||
|
||||
func _fatal_count() -> int:
|
||||
var n := 0
|
||||
if main.run_scope != null and main.run_scope.settlement != null \
|
||||
and String(main.run_scope.settlement.battle_result) == "fatal_error":
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
# ---- 异常上报(三类+归因+误报收敛) ----
|
||||
|
||||
func _report(kind: String, seg: String, expect: String) -> void:
|
||||
var finger_src := "%s|%s|%s|%s" % [kind, seg, expect, ",".join(_active_screens())]
|
||||
var finger := str(finger_src.hash())
|
||||
_misfire[finger] = int(_misfire.get(finger, 0)) + 1
|
||||
if int(_misfire[finger]) >= 2:
|
||||
_silent_count += 1 # 同一误报源第 2 次自动登记静默(契约 §5 收敛口径)
|
||||
return
|
||||
_problems.append({
|
||||
"kind": kind, "segment": seg, "expect": expect,
|
||||
"actual_screens": _active_screens(), "last_action": _last_action,
|
||||
"fatal": _fatal_count(), "at": Time.get_datetime_string_from_system(),
|
||||
})
|
||||
|
||||
|
||||
func _load_misfire() -> void:
|
||||
if FileAccess.file_exists(MISFIRE_PATH):
|
||||
var parsed: Variant = JSON.parse_string(FileAccess.get_file_as_string(MISFIRE_PATH))
|
||||
if parsed is Dictionary:
|
||||
_misfire = parsed
|
||||
|
||||
|
||||
func _save_misfire() -> void:
|
||||
var f := FileAccess.open(MISFIRE_PATH, FileAccess.WRITE)
|
||||
if f != null:
|
||||
f.store_string(JSON.stringify(_misfire, " "))
|
||||
f.close()
|
||||
|
||||
|
||||
# ---- 段间等待(超时判据承载) ----
|
||||
|
||||
func _frames(n: int = 2) -> void:
|
||||
for _i in n:
|
||||
await get_tree().process_frame
|
||||
_record_screens()
|
||||
|
||||
|
||||
# 期望屏集合在超时内逐帧到达;到达即记账返回。超时=异常②超时+③屏状态不符双记(同源指纹静默收敛)。
|
||||
func _phase_expect(seg: String, expect_keys: Array) -> void:
|
||||
_last_action = "等待段「%s」落点(期望 %s)" % [seg, ",".join(expect_keys)]
|
||||
var t0 := Time.get_ticks_msec()
|
||||
while Time.get_ticks_msec() - t0 < SEGMENT_TIMEOUT_MS:
|
||||
await get_tree().process_frame
|
||||
_record_screens()
|
||||
var st := _screen_state()
|
||||
var all_on := true
|
||||
for k in expect_keys:
|
||||
all_on = all_on and bool(st.get(k, false))
|
||||
if all_on:
|
||||
await _frames(2) # 开层排版帧——当帧可见时容器未排,紧随的控件真点击会落空(探针件同款坑)
|
||||
return
|
||||
_report("timeout", seg, "屏 %s 应在场" % ",".join(expect_keys))
|
||||
_report("screen_mismatch", seg, "屏 %s 应在场" % ",".join(expect_keys))
|
||||
_save_misfire()
|
||||
|
||||
|
||||
# ---- 旅程段 ----
|
||||
|
||||
func _phase_menu_settings() -> void:
|
||||
# 设置开合(开始层真实入口——功能界面全覆盖义务)
|
||||
var btn: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "设置", "prefix")
|
||||
if btn == null:
|
||||
_report("screen_mismatch", "设置入口", "开始层「设置」钮在场")
|
||||
return
|
||||
_last_action = "真点「设置」钮"
|
||||
ProbeUiUtil.push_click(main, btn)
|
||||
await _phase_expect("设置开", ["设置"])
|
||||
var back: Button = ProbeUiUtil.find_button(main.ui.modals.menu.settings_overlay, "返回", "prefix")
|
||||
if back != null:
|
||||
_last_action = "真点设置「返回」钮"
|
||||
ProbeUiUtil.push_click(main, back)
|
||||
await _phase_expect("设置收", ["菜单"])
|
||||
|
||||
|
||||
func _phase_new_slot(slot: int) -> void:
|
||||
# 开机→存档·新档(选档层真实入口:进入存档→槽位新档→落营地)
|
||||
var esb: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "进入存档", "prefix")
|
||||
if esb == null:
|
||||
_report("screen_mismatch", "选档入口", "开始层「进入存档」钮在场")
|
||||
return
|
||||
_last_action = "真点「进入存档」钮"
|
||||
ProbeUiUtil.push_click(main, esb)
|
||||
await _phase_expect("选档层开", ["选档"])
|
||||
var nb: Button = main.ui.modals.menu.save_select_overlay._new_btns.get(slot)
|
||||
if nb == null:
|
||||
_report("screen_mismatch", "新档槽%d" % slot, "槽%d 新档钮在场" % slot)
|
||||
return
|
||||
_last_action = "真点槽%d「新档」钮" % slot
|
||||
ProbeUiUtil.push_click(main, nb)
|
||||
await _phase_expect("新档落营地", ["营地"])
|
||||
|
||||
|
||||
func _phase_load_slot(slot: int) -> void:
|
||||
var esb: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "进入存档", "prefix")
|
||||
if esb == null:
|
||||
_report("screen_mismatch", "选档入口", "开始层「进入存档」钮在场")
|
||||
return
|
||||
_last_action = "真点「进入存档」钮(进档链)"
|
||||
ProbeUiUtil.push_click(main, esb)
|
||||
await _phase_expect("选档层开", ["选档"])
|
||||
var lb: Button = main.ui.modals.menu.save_select_overlay._load_btns.get(slot)
|
||||
if lb == null:
|
||||
_report("screen_mismatch", "进档槽%d" % slot, "槽%d 进档钮在场" % slot)
|
||||
return
|
||||
_last_action = "真点槽%d「进档」钮" % slot
|
||||
ProbeUiUtil.push_click(main, lb)
|
||||
await _phase_expect("进档落营地", ["营地"])
|
||||
|
||||
|
||||
func _phase_new_and_delete(slot: int) -> void:
|
||||
# 新档→篝火回菜单→选档删档(二次确认双路径真点击)——存档三操作收口
|
||||
await _phase_new_slot(slot)
|
||||
var camp: Control = main.ui.modals.camp.scene
|
||||
_last_action = "真点篝火热点(回退动线)"
|
||||
ProbeUiUtil.push_click_at(main, camp.get_hotspot_rect(&"campfire").get_center())
|
||||
await _phase_expect("篝火确认弹窗", ["确认弹窗"])
|
||||
var ok: Button = main.ui.modals.menu.confirm_dialog._confirm_btn
|
||||
if ok != null:
|
||||
_last_action = "真点篝火确认「确认」钮"
|
||||
ProbeUiUtil.push_click(main, ok)
|
||||
await _phase_expect("回菜单", ["菜单"])
|
||||
# 删档:进选档→槽位删档一击开窗→真点取消→再删→真点确认(文件三态走查归探针既有套件——C 只验到达)
|
||||
var esb: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "进入存档", "prefix")
|
||||
if esb != null:
|
||||
_last_action = "真点「进入存档」钮(删档链)"
|
||||
ProbeUiUtil.push_click(main, esb)
|
||||
await _phase_expect("选档层开", ["选档"])
|
||||
var db: Button = main.ui.modals.menu.save_select_overlay._delete_btns.get(slot)
|
||||
if db != null:
|
||||
_last_action = "真点槽%d「删档」钮一击" % slot
|
||||
ProbeUiUtil.push_click(main, db)
|
||||
await _phase_expect("删档确认窗", ["确认弹窗"])
|
||||
var cc: Button = main.ui.modals.menu.confirm_dialog._cancel_btn
|
||||
if cc != null:
|
||||
_last_action = "真点删档「取消」钮"
|
||||
ProbeUiUtil.push_click(main, cc)
|
||||
await _frames(2)
|
||||
_last_action = "真点槽%d「删档」钮二击" % slot
|
||||
ProbeUiUtil.push_click(main, db)
|
||||
await _phase_expect("删档确认窗", ["确认弹窗"])
|
||||
var ok2: Button = main.ui.modals.menu.confirm_dialog._confirm_btn
|
||||
if ok2 != null:
|
||||
_last_action = "真点删档「确认」钮"
|
||||
ProbeUiUtil.push_click(main, ok2)
|
||||
await _frames(3)
|
||||
var back: Button = ProbeUiUtil.find_button(main.ui.modals.menu.save_select_overlay, "返回", "prefix")
|
||||
if back != null:
|
||||
_last_action = "真点选档「返回」钮"
|
||||
ProbeUiUtil.push_click(main, back)
|
||||
await _phase_expect("选档收", ["菜单"])
|
||||
|
||||
|
||||
func _phase_camp_windows() -> void:
|
||||
# 营地功能窗逐窗真点开合(锻造/秘宝/树/图鉴/生涯——campfire/gate 归动线段)
|
||||
var views := {
|
||||
&"forge": ["锻造窗", main.ui.modals.camp.forge],
|
||||
&"chest": ["秘宝窗", main.ui.modals.camp.treasure],
|
||||
&"dummy": ["树窗", main.ui.modals.camp.tree],
|
||||
&"board": ["图鉴窗", main.ui.modals.camp.codex],
|
||||
&"notebook": ["生涯", main.ui.modals.menu.career_overlay],
|
||||
}
|
||||
for key: StringName in [&"forge", &"chest", &"dummy", &"board", &"notebook"]:
|
||||
var camp: Control = main.ui.modals.camp.scene
|
||||
_last_action = "真点营地物件热点 %s" % String(key)
|
||||
ProbeUiUtil.push_click_at(main, camp.get_hotspot_rect(key).get_center())
|
||||
await _phase_expect("%s窗开" % String(key), [views[key][0]])
|
||||
var back: Button = ProbeUiUtil.find_button(views[key][1], "返回营地")
|
||||
if back == null:
|
||||
back = ProbeUiUtil.find_button(views[key][1], "返回", "prefix")
|
||||
if back != null:
|
||||
_last_action = "真点 %s 窗返回钮" % String(key)
|
||||
ProbeUiUtil.push_click(main, back)
|
||||
await _frames(3)
|
||||
|
||||
|
||||
func _phase_camp_esc_return() -> void:
|
||||
# 营地 Esc 浮窗→「返回主菜单」真点(回退动线+浮窗功能面到达)
|
||||
_push_esc()
|
||||
await _phase_expect("Esc浮窗开", ["Esc浮窗"])
|
||||
var rb: Button = main.ui.modals.camp.esc_overlay._return_btn
|
||||
if rb != null:
|
||||
_last_action = "真点浮窗「返回主菜单」钮"
|
||||
ProbeUiUtil.push_click(main, rb)
|
||||
await _phase_expect("浮窗回菜单", ["菜单"])
|
||||
|
||||
|
||||
func _phase_gate_start() -> void:
|
||||
# 大门确认→开冒险(动线收口——确认后落点=冒险,非回菜单)
|
||||
var camp: Control = main.ui.modals.camp.scene
|
||||
_last_action = "真点大门热点"
|
||||
ProbeUiUtil.push_click_at(main, camp.get_hotspot_rect(&"gate").get_center())
|
||||
await _phase_expect("大门确认弹窗", ["确认弹窗"])
|
||||
var ok: Button = main.ui.modals.menu.confirm_dialog._confirm_btn
|
||||
if ok != null:
|
||||
_last_action = "真点大门「确认」钮"
|
||||
ProbeUiUtil.push_click(main, ok)
|
||||
await _phase_expect("冒险发起", ["冒险"])
|
||||
# 已知缺陷现场(发现环义务已尽):大门开局后开始层滞留盖屏=「开局被开始层盖住」
|
||||
# (营地交互修复批承接中、合回前 master 在案)——开始层可见直入 locks_input 谓词,战斗全程锁死。
|
||||
# C 上报后以夹具面隐藏开始层继续走查(通道=夹具修整,非真输入;缺陷本体不因此掩盖)。
|
||||
await _frames(2)
|
||||
if main.ui.screens.start_overlay != null and main.ui.screens.start_overlay.visible:
|
||||
_report("screen_mismatch", "大门开局", "开始层滞留盖屏(已知缺陷——营地交互修复批承接中)")
|
||||
_last_action = "夹具修整:隐藏滞留开始层以解锁战斗走查(缺陷已上报在案)"
|
||||
main.ui.screens.start_overlay.visible = false
|
||||
await _frames(2)
|
||||
|
||||
|
||||
func _phase_adventure() -> void:
|
||||
# 关卡推进段:入口选路→战斗→各停点分发→真终局收口(帧预算兜底)
|
||||
var budget := ADVENTURE_BUDGET_FRAMES
|
||||
while budget > 0:
|
||||
budget -= 1
|
||||
await get_tree().process_frame
|
||||
_record_screens()
|
||||
var st := _screen_state()
|
||||
if bool(st["节点"]):
|
||||
_last_action = "节点层「继续」(方法-视图口)"
|
||||
main.ui.modals.run.node_overlay.press_continue()
|
||||
continue
|
||||
if bool(st["三选一"]):
|
||||
_drive_curio()
|
||||
continue
|
||||
if bool(st["商店"]):
|
||||
var lv: Button = ProbeUiUtil.find_button(main.ui.modals.menu.shop_overlay, "离开商店")
|
||||
if lv != null:
|
||||
_last_action = "真点「离开商店」钮"
|
||||
ProbeUiUtil.push_click(main, lv)
|
||||
continue
|
||||
if bool(st["大奖励"]):
|
||||
var nb: Button = ProbeUiUtil.find_button(main.ui.screens.inter_overlay, "进入下一层", "prefix")
|
||||
if nb != null:
|
||||
_last_action = "真点大奖励「进入下一层」钮"
|
||||
ProbeUiUtil.push_click(main, nb)
|
||||
await _frames(2)
|
||||
continue
|
||||
if bool(st["关卡屏"]):
|
||||
_drive_map_once()
|
||||
continue
|
||||
if bool(st["战斗"]):
|
||||
await _drive_battle_once()
|
||||
continue
|
||||
var endo: Control = main.ui.modals.run.end_overlay
|
||||
if endo != null and endo.is_open():
|
||||
var armed: bool = main.run_scope.settlement_stop_timer.is_armed()
|
||||
var running: bool = main.run_scope.is_adventure and main.run_scope.adventure != null \
|
||||
and String(main.run_scope.adventure.state) == "running"
|
||||
if armed or running:
|
||||
_last_action = "真点结算面「继续/下一节点」钮"
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.end_overlay._btn)
|
||||
await _frames(2)
|
||||
continue
|
||||
break # 真终局(失败/放弃/打穿)——收口段交棒
|
||||
if budget <= 0:
|
||||
_last_action = "冒险段超时(诊断:%s)" % JSON.stringify({
|
||||
"pacer_pending": main.ui.screens.row_pacer.pending_rows().size(),
|
||||
"locked": main.ui.flow.locks_input(),
|
||||
"battle_result": String(main.run_scope.settlement.battle_result) if main.run_scope.settlement != null else "?",
|
||||
"level": int(main.run_scope.runner.level_row.get("level_index", -1)) if main.run_scope.runner != null else -1,
|
||||
"round": int(main.run_scope.settlement.round_index) if main.run_scope.settlement != null else -1,
|
||||
"ap": int(main.run_scope.settlement.action_economy.ap) if main.run_scope.settlement != null else -1,
|
||||
})
|
||||
_report("timeout", "冒险段", "帧预算耗尽未达真终局")
|
||||
_save_misfire()
|
||||
|
||||
|
||||
func _drive_map_once() -> void:
|
||||
# 地图屏:状态角标真点开合一次+候选命中(方法-地图命中口——自绘布局路径,探针族同款入口)
|
||||
if not _map_status_done and main.ui.modals.run.level_screen.status_badge != null \
|
||||
and not main.ui.modals.run.level_screen.status_badge.disabled:
|
||||
_map_status_done = true
|
||||
var badge: Button = main.ui.modals.run.level_screen.status_badge
|
||||
_last_action = "真点地图屏状态角标"
|
||||
ProbeUiUtil.push_click(main, badge)
|
||||
await _frames(3) # 面板开层排版帧——同帧点「继续」会落空、输入锁滞留全战役(实证)
|
||||
var resumed: Button = ProbeUiUtil.find_button(main.ui.modals.run.status_panel, "继续", "prefix")
|
||||
if resumed != null and main.ui.modals.run.status_panel.visible:
|
||||
ProbeUiUtil.push_click(main, resumed)
|
||||
_last_action = "地图候选命中(方法-地图命中口)"
|
||||
main.ui.modals.run.level_screen.press_candidate(0)
|
||||
|
||||
|
||||
func _drive_curio() -> void:
|
||||
# 三选一停点:待选态点首件+确认;兑付/已消费态点继续(方法-视图口——curio 停点既定驱动面)
|
||||
var co: Control = main.ui.modals.run.curio_overlay
|
||||
_last_action = "三选一停点分发(方法-视图口)"
|
||||
if co._continue_btn.visible:
|
||||
co.press_continue()
|
||||
elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._node_pending.is_empty():
|
||||
co.press_card(0)
|
||||
co.press_confirm()
|
||||
elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._pending.is_empty():
|
||||
co.press_card(0)
|
||||
co.press_confirm()
|
||||
else:
|
||||
co.press_continue()
|
||||
|
||||
|
||||
func _drive_battle_once() -> void:
|
||||
# 战斗单步:挂死检测+功能钮真点(图例/状态/刷新确认链/停手)+棋盘挖格(方法-棋盘口)
|
||||
var settlement = main.run_scope.settlement
|
||||
var diag: Dictionary = settlement.diagnostic_snapshot()
|
||||
var st := {
|
||||
"round": diag.round, "phase": diag.action_economy.phase, "ap": diag.action_economy.ap,
|
||||
"revealed": diag.minesweeper_revealed,
|
||||
}
|
||||
var pacer_idle: bool = main.ui.screens.row_pacer.pending_rows().is_empty()
|
||||
if JSON.stringify(st) == JSON.stringify(_last_state) and pacer_idle \
|
||||
and not main.ui.flow.locks_input():
|
||||
_stall += 1
|
||||
if _stall >= BATTLE_STALL_FRAMES:
|
||||
_report("timeout", "战斗段", "状态无变化达 %d 帧" % BATTLE_STALL_FRAMES)
|
||||
_stall = 0
|
||||
_save_misfire()
|
||||
_last_state = {} # 重置基线防同帧连报
|
||||
else:
|
||||
_stall = 0
|
||||
_last_state = st
|
||||
if not pacer_idle or main.ui.flow.locks_input():
|
||||
return
|
||||
var ap_bar: Control = main.ui.battle.board_zone.ap_bar
|
||||
# 功能钮真点(一次性——图例开合/状态面板开合/刷新确认链)
|
||||
if not _legend_done:
|
||||
_legend_done = true
|
||||
_last_action = "真点图例钮"
|
||||
ProbeUiUtil.push_click(main, ap_bar._btn_legend)
|
||||
await _frames(2)
|
||||
var lclose: Button = ProbeUiUtil.find_button(main.ui.modals.run.legend_overlay, "关闭")
|
||||
if lclose != null and main.ui.modals.run.legend_overlay.is_open():
|
||||
ProbeUiUtil.push_click(main, lclose)
|
||||
await _frames(2)
|
||||
return
|
||||
if not _status_done and not main.ui.battle.board_zone.status_badge.disabled:
|
||||
_status_done = true
|
||||
_last_action = "真点战斗屏状态角标"
|
||||
ProbeUiUtil.push_click(main, main.ui.battle.board_zone.status_badge)
|
||||
await _frames(3) # 面板开层排版帧(同地图屏实证坑)
|
||||
var resumed: Button = ProbeUiUtil.find_button(main.ui.modals.run.status_panel, "继续", "prefix")
|
||||
if resumed != null and main.ui.modals.run.status_panel.visible:
|
||||
ProbeUiUtil.push_click(main, resumed)
|
||||
await _frames(2)
|
||||
return
|
||||
if not _refresh_done and ap_bar.is_refresh_ready():
|
||||
_refresh_done = true
|
||||
_last_action = "真点「弃图刷新」钮+确认链"
|
||||
ProbeUiUtil.push_click(main, ap_bar._btn_refresh)
|
||||
await _frames(2)
|
||||
var cbtn: Button = ProbeUiUtil.find_button(ap_bar, "确认弃图")
|
||||
if cbtn != null:
|
||||
ProbeUiUtil.push_click(main, cbtn)
|
||||
await _frames(2)
|
||||
return
|
||||
# 棋盘动作(方法-棋盘口——BoardView 公共点击口,ui_input_probe 先例;C 的按钮面全走真输入)
|
||||
var minesweeper = settlement.minesweeper
|
||||
var plan: Dictionary = AutoPlayer.choose_dig(minesweeper)
|
||||
if plan.is_empty():
|
||||
plan = AutoPlayer.gamble_pick(minesweeper)
|
||||
if plan.is_empty():
|
||||
_last_action = "真点「停手」钮(无计划)"
|
||||
ProbeUiUtil.push_click(main, ap_bar._btn_stop)
|
||||
await _frames(2)
|
||||
return
|
||||
_last_action = "棋盘挖格 %s(方法-棋盘口)" % str(plan.dig_cell)
|
||||
if plan.hero_cell != minesweeper.hero_pos():
|
||||
main.ui.battle.board_zone.board_view.click_cell(plan.hero_cell, MOUSE_BUTTON_LEFT)
|
||||
main.ui.battle.board_zone.board_view.click_cell(plan.dig_cell, MOUSE_BUTTON_LEFT)
|
||||
await _frames(1)
|
||||
|
||||
|
||||
func _phase_end_back_menu() -> void:
|
||||
# 终局收束:真点「返回」→ 回菜单;结算框/中途面已在冒险段分发
|
||||
var t0 := Time.get_ticks_msec()
|
||||
while Time.get_ticks_msec() - t0 < SEGMENT_TIMEOUT_MS:
|
||||
await get_tree().process_frame
|
||||
_record_screens()
|
||||
var endo: Control = main.ui.modals.run.end_overlay
|
||||
if endo != null and endo.is_open() and not main.run_scope.settlement_stop_timer.is_armed():
|
||||
break
|
||||
if main.ui.modals.run.end_overlay == null or not main.ui.modals.run.end_overlay.is_open():
|
||||
_report("timeout", "终局收束", "真终局面未在超时内到达")
|
||||
_save_misfire()
|
||||
return
|
||||
var btn: Button = main.ui.modals.run.end_overlay._btn
|
||||
if btn != null:
|
||||
_last_action = "真点终局「返回」钮"
|
||||
ProbeUiUtil.push_click(main, btn)
|
||||
await _phase_expect("终局回菜单", ["菜单"])
|
||||
|
||||
|
||||
func _push_esc() -> void:
|
||||
var ev := InputEventKey.new()
|
||||
ev.keycode = KEY_ESCAPE
|
||||
ev.physical_keycode = KEY_ESCAPE
|
||||
ev.pressed = true
|
||||
main.get_viewport().push_input(ev, true) # 键盘真输入(通道=真实输入-键盘)
|
||||
|
||||
|
||||
# ---- 报告落盘(JSONP) ----
|
||||
|
||||
func _dump_report() -> void:
|
||||
var dir := DirAccess.open("res://")
|
||||
if dir != null:
|
||||
dir.make_dir_recursive("tests/.gates/coverage")
|
||||
var payload := {
|
||||
"meta": {
|
||||
"generated_at": Time.get_datetime_string_from_system(),
|
||||
"seed": OS.get_environment("PROBE_SEED"),
|
||||
"screens_seen_total": _screens_seen.size(),
|
||||
"anomalies_reported": _problems.size(),
|
||||
"anomalies_silent": _silent_count,
|
||||
"fatal_count": _fatal_count(),
|
||||
"anomaly_classes": "fatal/timeout/screen_mismatch(其余静默;误报指纹二次起静默)",
|
||||
},
|
||||
"screens_seen": _screens_seen.keys(),
|
||||
"screen_trace": _screen_trace,
|
||||
"anomalies": _problems,
|
||||
}
|
||||
var f := FileAccess.open(REPORT_PATH, FileAccess.WRITE)
|
||||
if f != null:
|
||||
var js := "/* 全动线自玩报告——journey_player.gd 随跑覆写;误报核销归用户复核轮。 */\n"
|
||||
js += "window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};\n"
|
||||
js += "window.__QA_COVERAGE__.journey = %s;\n" % JSON.stringify(payload, " ")
|
||||
f.store_string(js)
|
||||
f.close()
|
||||
print("[journey] 报告已落盘 %s(屏到达 %d 类 / 上报 %d / 静默 %d)" % [
|
||||
REPORT_PATH, _screens_seen.size(), _problems.size(), _silent_count])
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://cjourneyplayer1"]
|
||||
|
||||
[ext_resource type="Script" path="res://sim/journey_player.gd" id="1"]
|
||||
|
||||
[node name="JourneyPlayer" type="Node"]
|
||||
script = ExtResource("1")
|
||||
Reference in new issue
Block a user