# VERTICAL SLICE - NOT FOR PRODUCTION # 棋盘 Node2D 迁移批 Phase A 2026-09-23:push_input 端到端输入探针(ADR-0003 登记项转义务、 # ADR-0006 Risks 缓解面)。现役探针全走 BoardView.click_cell 直调(合成鼠标事件零命中已实证)—— # 本探针是真实输入路径的唯一自动化覆盖:鼠标事件 → 壳层 _gui_input → 板级公式换算(_cell_at)→ # 悬停/点选/chord 既有出口 → 组合根信号。四 case:①悬停可挖域(金框+十字光标)②悬停禁入 # (hover_forbidden 信号+灰禁红框)③悬停已开格(路径预览点迹+移动光标)④右键插旗(flag_place # 到达组合根+核心旗态)⑤双击 chord(禁入格双左击,核心拒挖零副作用)⑥左键挖(dig 到达组合根+核心揭格) # ⑦英雄精灵世界落位(棋盘小人贴图批回归闸——SubViewport 世界坐标判据,素材缺位树跳过)。 # 事件路径:Viewport.push_input(ev, in_local_coords=true)——落点用 BoardView.cell_center_local # (与 _cell_at 同一公式族)换算画布坐标,SubViewportContainer mouse_filter=IGNORE 不拦截。 # 运行:Godot_console.exe --headless --path . res://smoke/ui_input_probe.tscn extends Node var main var _problems: Array = [] var _actions: Array = [] # action_requested 收集([cell, kind]) var _chords: Array = [] # chord_attempted 收集 var _forbids: Array = [] # hover_forbidden 收集 func _ready() -> void: await get_tree().process_frame # autoload 就绪 main = load("res://presentation/main.tscn").instantiate() add_child(main) await get_tree().process_frame main._start_adventure_from_menu() await get_tree().process_frame if main.route_overlay.visible: main.route_overlay.press_candidate(0) # 入口选路停点 → 首战 for i in range(3): await get_tree().process_frame var bv = main.board_view var ms = main.settlement.minesweeper if main.settlement != null else null if ms == null or bv._cells.is_empty(): _pre_fail("战斗未就绪(cells=%d settlement=%s)" % [bv._cells.size(), str(main.settlement)]) return bv.action_requested.connect(func(c: Vector2i, kind: String): _actions.append([c, kind])) bv.chord_attempted.connect(func(c: Vector2i): _chords.append(c)) bv.hover_forbidden.connect(func(c: Vector2i): _forbids.append(c)) # ---- 先行移动(真实点击路径):R₀ 展开后主角邻域全已开(_diggable 空)——点击前沿已开格 # 迫近未开边界(与 ui_click_probe 行为同构:先 move 后 dig)---- var hero0: Vector2i = ms.hero_pos() var mv := Vector2i(-9, -9) for c in bv._board.neighbors8(hero0): if ms.is_revealed(c) and _has_hidden_neighbor(bv, ms, c): mv = c break if mv.x == -9: mv = _pick_frontier(bv, ms) if mv.x == -9: _pre_fail("前沿已开格选取失败(板面无可迫近边界)") return _push_click(_pos(bv, mv), MOUSE_BUTTON_LEFT) await get_tree().process_frame await get_tree().process_frame if not _has_action(mv, "move") or ms.hero_pos() == hero0: _pre_fail("先导移动未到达(actions=%s hero=%s→%s)" % [str(_actions), hero0, ms.hero_pos()]) return # ---- 目标格实证选取(不写死坐标;两枚相邻可挖格 + 禁入格 + 已开格)---- var flag_t := Vector2i(-9, -9) var dig_t := Vector2i(-9, -9) for c in bv._diggable: if not ms.has_flag(c): if flag_t.x == -9: flag_t = c elif dig_t.x == -9: dig_t = c break var forbid_t := _pick_forbidden(bv, ms) var reveal_t := _pick_revealed(ms) if flag_t.x == -9 or dig_t.x == -9 or forbid_t.x == -9 or reveal_t.x == -9: _pre_fail("目标格选取失败 flag=%s dig=%s forbid=%s reveal=%s" % [flag_t, dig_t, forbid_t, reveal_t]) return # ---- case① 悬停可挖域:motion → 悬停态到达(金框 + 十字光标)---- _push_motion(_pos(bv, flag_t)) await get_tree().process_frame if bv._hover != flag_t: _problems.append("case① 悬停未到达(_hover=%s 应=%s)" % [bv._hover, flag_t]) var u1 = bv._cells[flag_t] if not u1.hl_visible or u1.hl_color != UiKit.GOLD: _problems.append("case① 悬停金框缺失(visible=%s color=%s)" % [str(u1.hl_visible), str(u1.hl_color)]) if bv.mouse_default_cursor_shape != Input.CURSOR_CROSS: _problems.append("case① 挖掘光标未切十字(%d)" % bv.mouse_default_cursor_shape) # ---- case② 悬停禁入:hover_forbidden 信号 + 灰禁红框 ---- _push_motion(_pos(bv, forbid_t)) await get_tree().process_frame if _forbids.is_empty() or _forbids[_forbids.size() - 1] != forbid_t: _problems.append("case② hover_forbidden 未达(%s)" % [str(_forbids)]) if bv._cells[forbid_t].hl_color != UiKit.RED_FORBID: _problems.append("case② 禁入红框缺失(color=%s)" % str(bv._cells[forbid_t].hl_color)) # ---- case③ 悬停已开格:路径预览点迹 + 移动光标 ---- _push_motion(_pos(bv, reveal_t)) await get_tree().process_frame var dots := 0 for c in bv._cells: if bv._cells[c].dot_visible: dots += 1 if dots < 1: _problems.append("case③ 路径预览点迹未亮(dots=0)") if bv.mouse_default_cursor_shape != Input.CURSOR_ARROW: _problems.append("case③ 移动光标缺失(%d)" % bv.mouse_default_cursor_shape) # ---- case④ 右键插旗:flag_place 到达组合根 + 核心旗态落位 ---- _push_click(_pos(bv, flag_t), MOUSE_BUTTON_RIGHT) await get_tree().process_frame if not _has_action(flag_t, "flag_place"): _problems.append("case④ flag_place 未达组合根(actions=%s)" % [str(_actions)]) await get_tree().process_frame if not ms.has_flag(flag_t): _problems.append("case④ 核心旗态未落位") # ---- case⑤ 双击 chord:禁入格双左击(核心拒挖零副作用)→ chord_attempted ---- _push_click(_pos(bv, forbid_t), MOUSE_BUTTON_LEFT) _push_click(_pos(bv, forbid_t), MOUSE_BUTTON_LEFT) await get_tree().process_frame if _chords.is_empty() or _chords[_chords.size() - 1] != forbid_t: _problems.append("case⑤ chord_attempted 未达(chords=%s)" % [str(_chords)]) # ---- case⑥ 左键挖:dig 到达组合根 + 核心揭格落位 ---- _push_click(_pos(bv, dig_t), MOUSE_BUTTON_LEFT) await get_tree().process_frame if not _has_action(dig_t, "dig"): _problems.append("case⑥ dig 未达组合根(actions=%s)" % [str(_actions)]) await get_tree().process_frame if not ms.is_revealed(dig_t): _problems.append("case⑥ 核心揭格未落位(目标=%s)" % dig_t) # ---- case⑦ 英雄精灵世界落位(棋盘小人贴图批回归闸 2026-09-26):精灵在 SubViewport 世界内, # 落位 = 格×步距 + 脚底锚(横=格中线/纵=格高 75% 线——2026-09-26 二裁「脚踩格子 1/4 处」; # 禁带壳层 _svp_container.position 居中偏移——上批误用壳层 cell_origin 实测右偏 ~7 格+偏离 # 格心,实机截图用户报缺陷)。素材缺位树(hero==null,字形「人」回退在役)跳过;断言前等 # 0.3s 墙钟——headless 帧率无上限,帧数等待 < 位移 Tween 140ms 时会采到半途位置(首跑实证)。 ---- if bv._world.hero != null: await get_tree().create_timer(0.3).timeout var expect: Vector2 = Vector2(ms.hero_pos()) * (bv.cell_size + float(bv.SEP)) \ + Vector2(bv.cell_size * 0.5, bv.cell_size * bv.HERO_GROUND_FRAC) if bv._world.hero.position.distance_to(expect) > 0.5: _problems.append("case⑦ 英雄精灵世界落位偏移 pos=%s expect=%s" % [str(bv._world.hero.position), str(expect)]) if _problems.is_empty(): print("[ui_input_probe] PASS — 悬停金框/禁入信号/路径预览/flag_place/chord/dig/英雄落位 七断言全绿(真实 push_input 路径)") get_tree().quit(0) else: for p in _problems: push_error("[ui_input_probe] FAIL — " + p) print("[ui_input_probe] FAIL — " + "; ".join(_problems)) get_tree().quit(1) func _has_action(cell: Vector2i, kind: String) -> bool: for a in _actions: if a[0] == cell and a[1] == kind: return true return false func _pick_forbidden(bv, ms) -> Vector2i: var b = ms.current_board() for y in b.height: for x in b.width: var c := Vector2i(x, y) if not ms.is_revealed(c) and not bv._diggable.has(c): return c return Vector2i(-9, -9) func _pick_revealed(ms) -> Vector2i: var b = ms.current_board() var hero: Vector2i = ms.hero_pos() for y in b.height: for x in b.width: var c := Vector2i(x, y) if ms.is_revealed(c) and c != hero: return c return Vector2i(-9, -9) # 前沿已开格兜底选取(主角邻域无前沿时全板扫描) func _pick_frontier(bv, ms) -> Vector2i: var b = ms.current_board() for y in b.height: for x in b.width: var c := Vector2i(x, y) if ms.is_revealed(c) and _has_hidden_neighbor(bv, ms, c): return c return Vector2i(-9, -9) func _has_hidden_neighbor(bv, ms, c: Vector2i) -> bool: for n in bv._board.neighbors8(c): if not ms.is_revealed(n): return true return false # 合成事件落点 = BoardView 画布原点 + 格中心(cell_center_local 与 _cell_at 同一公式族) func _pos(bv, c: Vector2i) -> Vector2: return bv.global_position + bv.cell_center_local(c) func _push_motion(pos: Vector2) -> void: var ev := InputEventMouseMotion.new() ev.position = pos ev.global_position = pos main.get_viewport().push_input(ev, true) func _push_click(pos: Vector2, btn: int) -> void: var ev := InputEventMouseButton.new() ev.position = pos ev.global_position = pos ev.button_index = btn ev.pressed = true main.get_viewport().push_input(ev, true) func _pre_fail(why: String) -> void: push_error("[ui_input_probe] FAIL — " + why) print("[ui_input_probe] FAIL — " + why) get_tree().quit(1)