# 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 世界坐标判据,素材缺位树跳过) # ⑧影子构成与贴脚 ⑨右移朝向+到位回站帧+缩放档 ⑩变格边长即时重排 ⑪贴图缺位回退字形 #(⑧–⑪=呈现层状态断言批 2026-09-26——Node2D 展示几何/状态面单元化,A 档方案)。 # 事件路径: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 const ProbeUiUtil := preload("res://smoke/probe_ui_util.gd") # T6 收口:隔离样板单点 var main: Node 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 # 存档隔离(迭代 8 卡①②——断点写盘面随冒险挂 main.save_manager;探针禁碰真实档 user://save.json, # 同 ui_click_probe 隔离纪律。首次冒险布尔位置位=对齐真实档游玩史口径,模板抽签路径不变; # isolate_save 先删后建——上次运行的残留断点会触发互斥确认早退) ProbeUiUtil.isolate_save(main, "smoke_input_probe_save", true, false, true) main._start_adventure_from_menu() await get_tree().process_frame # ---- ⑫ 图上命中真实输入(关卡画面实装批 2026-09-28 回归闸):入口停点候选节点中心 # push_input 真实鼠标路径 motion+click → 悬停预承诺置位 → route_chosen 到达组合根。 # 由来=真机 bug:深层节点(L1 Boss 内容 x=802)在固定画布命中区外,press_candidate 直调 # 命中函数探针全绿、真鼠标点不中——本 case 封住输入管线,⑬封住几何拟合。 if main.level_screen.visible: var ro = main.level_screen var cands: Array = main.runner.next_candidate_ids() var nds: Array = main.runner.nodes() var tgt := {} for n in nds: if main.runner.node_id_of(n) == int(cands[0]): tgt = n var npos: Vector2 = ro.layout_probe(nds)[tgt] var gpos: Vector2 = ro.canvas_probe()["origin"] + npos var evm := InputEventMouseMotion.new() evm.position = gpos evm.global_position = gpos main.get_viewport().push_input(evm, true) await get_tree().process_frame if int(ro._hover_id) != int(cands[0]): _pre_fail("图上悬停预承诺未置位(hover=%s 候选=%s)" % [str(ro._hover_id), str(cands[0])]) var evc := InputEventMouseButton.new() evc.button_index = MOUSE_BUTTON_LEFT evc.pressed = true evc.position = gpos evc.global_position = gpos main.get_viewport().push_input(evc, true) await get_tree().process_frame await get_tree().process_frame if main.level_screen.visible or main.runner.awaiting_choice(): _pre_fail("图上真实点击未提交选路(overlay 可见=%s 选路态=%s)" % [str(main.level_screen.visible), str(main.runner.awaiting_choice())]) for i in range(3): await get_tree().process_frame # ---- ⑬ 画布随图拟合(同 bug 的几何面回归):复刻分层公式独立推导最深节点内容 x, # 断言画布 min 尺寸覆盖之(open_choices 每 stop 拟合——L1 单链 Boss x=70+6*122=802 曾越界) var ro2 = main.level_screen var nds2: Array = main.runner.nodes() var layer2 := {} var inbound2 := {} for n2 in nds2: for tid2 in main.runner.node_out_ids(n2): inbound2[tid2] = true var queue2: Array = [] for n2 in nds2: if not inbound2.has(main.runner.node_id_of(n2)): queue2.append(n2) layer2[n2] = 0 while not queue2.is_empty(): var cur2: Dictionary = queue2.pop_front() for tid2 in main.runner.node_out_ids(cur2): var tg2 := {} for n2 in nds2: if main.runner.node_id_of(n2) == tid2: tg2 = n2 if tg2.is_empty() or layer2.has(tg2): continue layer2[tg2] = int(layer2[cur2]) + 1 queue2.append(tg2) var maxl2 := 0 for k2 in layer2.keys(): maxl2 = maxi(maxl2, int(layer2[k2])) # 常数锚(画布拟合探针):70/122/26/8 与 presentation/views/level_map_screen.gd 的 _content_size # 公式(70 + maxl*122 + NODE_HALF.x + 8)同源复刻——改常数须两侧同步(本探针只读断言层,真源在屏) var need_x2: float = 70.0 + maxl2 * 122.0 + 26.0 + 8.0 var cvmin: Dictionary = ro2.canvas_probe() if float(cvmin["min_size"].x) < need_x2 - 0.5: _pre_fail("画布拟合不足(min.x=%.0f < 最深内容 %.0f——深层节点不可点回归)" % [cvmin["min_size"].x, need_x2]) var bv = main.board_view var ms = main.settlement.minesweeper if main.settlement != null else null if ms == null or bv.cells_probe().is_empty(): _pre_fail("战斗未就绪(cells=%d settlement=%s)" % [bv.cells_probe().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 # ---- 目标格实证选取(不写死坐标;两枚相邻可挖格 + 禁入格 + 已开格)---- # 并流批(ADR-0007 裁丙 2026-09-27)后首战板面种子随全局序漂移——_diggable(主角 8 邻未开域) # 不足两枚时,沿已开区真点击移位到「≥2 未开邻」的前沿格再选;移位失败即收手交由选取失败分支报状态 for _expand in range(16): if bv.diggable_probe().size() >= 2: break var mv_t := Vector2i(-9, -9) for y in bv._board.height: for x in bv._board.width: var c := Vector2i(x, y) if not ms.is_revealed(c): continue var hidden := 0 for n in bv._board.neighbors8(c): if not ms.is_revealed(n): hidden += 1 if hidden >= 2: mv_t = c break if mv_t.x != -9: break if mv_t.x == -9: break var hero_before: Vector2i = ms.hero_pos() _push_click(_pos(bv, mv_t), MOUSE_BUTTON_LEFT) await get_tree().process_frame await get_tree().process_frame if ms.hero_pos() == hero_before: break var flag_t := Vector2i(-9, -9) var dig_t := Vector2i(-9, -9) for c in bv.diggable_probe(): 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_probe() != flag_t: _problems.append("case① 悬停未到达(_hover=%s 应=%s)" % [bv.hover_probe(), flag_t]) var u1 = bv.cells_probe()[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_probe()[forbid_t].hl_color != UiKit.RED_FORBID: _problems.append("case② 禁入红框缺失(color=%s)" % str(bv.cells_probe()[forbid_t].hl_color)) # ---- case③ 悬停已开格:路径预览点迹 + 移动光标 ---- _push_motion(_pos(bv, reveal_t)) await get_tree().process_frame var dots := 0 for c in bv.cells_probe(): if bv.cells_probe()[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④b 旗纹理槽回归断言(P0-1 修复批 2026-10-02):T13 批曾在 paint 尾部无条件 # `u.tex_flag = null` 覆盖 T17 批的条件赋值=素材在位时插旗不可见(headless 缺盘走字形回退 # 掩盖)——有盘断言纹理在槽、缺盘断言字形回退,两分支防双写踩踏复活 ---- var flag_unit = bv.cells_probe().get(flag_t) if bv._flag_tex != null: if flag_unit == null or flag_unit.tex_flag != bv._flag_tex: _problems.append("case④b 旗纹理槽未挂封印旗纹理(tex_flag=%s)" % ["无单元" if flag_unit == null else str(flag_unit.tex_flag)]) elif flag_unit == null or flag_unit.glyph != "旗": _problems.append("case④b 缺盘字形回退缺失(glyph=%s)" % ["无单元" if flag_unit == null else String(flag_unit.glyph)]) # ---- 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 世界内, # 落位 = 格×步距 + 脚底锚(横=格中线/纵=格底往上 25% 线=HERO_GROUND_FRAC 自底边口径——「脚踩 # 格子 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 * (1.0 - 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)]) # ---- case⑧ 影子构成与贴脚(小人立体感批回归闸 2026-09-26):椭圆影=英雄子件、中心贴脚锚 #(上批 (0,2) 下坠脱脚用户报缺陷)、z 相对回落格面之上英雄之下、黑色半透明、渐变纹理组件 # 合成;父子 use_parent_material 双挂(结算期随世界去饱和与格面同口径)。 ---- if bv._world.hero != null: var kids: Array = bv._world.hero.get_children() if kids.size() != 1 or not (kids[0] is Sprite2D): _problems.append("case⑧ 影子子件缺失或类型不对(children=%d)" % kids.size()) else: var sh: Sprite2D = kids[0] if sh.position != Vector2.ZERO: _problems.append("case⑧ 影子未贴脚(pos=%s 应=ZERO)" % str(sh.position)) if sh.z_index != -1: _problems.append("case⑧ 影子 z 相对档不对(%d 应=-1)" % sh.z_index) if absf(sh.modulate.a - 0.35) > 0.01: _problems.append("case⑧ 影子透明度漂移(%s 应=0.35)" % str(sh.modulate.a)) if not (sh.texture is GradientTexture2D): _problems.append("case⑧ 影子纹理非径向渐变组件(%s)" % str(sh.texture)) if not sh.use_parent_material or not bv._world.hero.use_parent_material: _problems.append("case⑧ 去饱和材质链漏挂(shadow=%s hero=%s)" % [str(sh.use_parent_material), str(bv._world.hero.use_parent_material)]) # ---- case⑨ 右移朝向+到位回站帧+缩放档(贴图批回归闸):素材基准朝左——向右移动 flip_h=true; # 到位(0.3s 墙钟 > 位移 Tween 140ms)纹理回站帧、scale 恒 1.5 均匀档。无可达右邻已开格跳过。 ---- if bv._world.hero != null: var hp9: Vector2i = ms.hero_pos() var mv9 := Vector2i(-9, -9) for n in bv._board.neighbors8(hp9): if n.x == hp9.x + 1 and ms.is_revealed(n): mv9 = n break if mv9.x == -9: print("[ui_input_probe] SKIP case⑨(无可达右邻已开格)") else: _push_click(_pos(bv, mv9), MOUSE_BUTTON_LEFT) await get_tree().create_timer(0.3).timeout var sp9: Sprite2D = bv._world.hero if not sp9.flip_h: _problems.append("case⑨ 右移后未翻转朝向(flip_h=false,素材基准朝左)") if sp9.texture != bv._hero_tex_idle: _problems.append("case⑨ 到位未回站帧(texture=%s)" % str(sp9.texture)) if sp9.scale != Vector2.ONE * bv.HERO_SPRITE_SCALE: _problems.append("case⑨ 缩放档漂移(%s 应=%s 均匀)" % [str(sp9.scale), str(bv.HERO_SPRITE_SCALE)]) # ---- case⑩ 变格边长即时重排(窗口缩放路径回归闸):set_cell_area 换小格 → 英雄即时落新脚锚、 # 缩放均匀不残弹跳形;还原 → 46px 档复验。 ---- if bv._world.hero != null: var hp10: Vector2i = ms.hero_pos() bv.set_cell_area(Vector2(300, 300)) var expect10: Vector2 = Vector2(hp10) * (bv.cell_size + float(bv.SEP)) \ + Vector2(bv.cell_size * 0.5, bv.cell_size * (1.0 - bv.HERO_GROUND_FRAC)) if bv._world.hero.position.distance_to(expect10) > 0.5 or bv._world.hero.scale != Vector2.ONE * bv.HERO_SPRITE_SCALE: _problems.append("case⑩ 缩格后英雄未即时重排(pos=%s expect=%s scale=%s)" % [str(bv._world.hero.position), str(expect10), str(bv._world.hero.scale)]) bv.set_cell_area(bv.size) var expect10b: Vector2 = Vector2(hp10) * (bv.cell_size + float(bv.SEP)) \ + Vector2(bv.cell_size * 0.5, bv.cell_size * (1.0 - bv.HERO_GROUND_FRAC)) if bv._world.hero.position.distance_to(expect10b) > 0.5: _problems.append("case⑩ 还原后英雄未复位(pos=%s expect=%s)" % [str(bv._world.hero.position), str(expect10b)]) # ---- case⑪ 贴图缺位回退(字形「人」回归闸):texture 置 null + refresh → 英雄格数字 0 时字形 # 必须回退「人」(数字格仍显数字——回退判据与抑制判据同源);验毕还原纹理重绘。 ---- if bv._world.hero != null: var hp11: Vector2i = ms.hero_pos() var u11 = bv.cells_probe()[hp11] var tex_saved: Texture2D = bv._hero_tex_idle bv._hero_tex_idle = null bv.refresh() var digit11: int = int(ms.reveal_record(hp11).get("digit", 0)) if digit11 == 0: if u11.glyph != "人": _problems.append("case⑪ 贴图缺位未回退字形「人」(glyph=%s)" % u11.glyph) elif u11.glyph != str(digit11): _problems.append("case⑪ 缺位态数字格字形被破坏(glyph=%s 应=%d)" % [u11.glyph, digit11]) bv._hero_tex_idle = tex_saved bv.refresh() # ---- case⑭ 变异角标(迭代 6 T6——flat_damage 未开格视觉标记;resolver 注入面+落位+复位) ---- var mark_t: Vector2i = _pick_forbidden(bv, ms) # 任一未开格(禁入=未开非相邻) if mark_t.x >= 0: bv.set_variant_mark_resolver(func(_c): return "flat_damage") bv.refresh() if String(bv.cells_probe()[mark_t].mutation_mark) != "flat_damage": _problems.append("case⑭ 变异角标未落位(mutation_mark=%s)" % str(bv.cells_probe()[mark_t].mutation_mark)) bv.set_variant_mark_resolver(Callable()) bv.refresh() if String(bv.cells_probe()[mark_t].mutation_mark) != "": _problems.append("case⑭ 角标未随 resolver 复位(mutation_mark=%s)" % str(bv.cells_probe()[mark_t].mutation_mark)) else: _problems.append("case⑭ 无未开格可验(板面异常)") # ---- case⑮ 视野迷雾(迭代 11 第④批 T5——三态呈态/半径圈/移动跟随/开关两态零扰动/提灯插值位; # 纯呈现零逻辑——迷雾开关切换前后 minesweeper 状态摘要逐位一致) ---- var fogp: Dictionary = bv.fog_params_probe() if int(bv.FOG_RADIUS) != 8 or int(bv.FOG_LANTERN_BONUS) != 2 or int(fogp["lantern"]) != 0 or not fogp["enabled"]: _problems.append("case⑮ 迷雾参数漂移(R=%s bonus=%s lantern=%s enabled=%s 应=8/2/0/true)" % [str(bv.FOG_RADIUS), str(bv.FOG_LANTERN_BONUS), str(fogp["lantern"]), str(fogp["enabled"])]) else: var hero15: Vector2i = ms.hero_pos() var b15 = ms.current_board() var fog_ok := true var cells_f1: Dictionary = bv.cells_probe() for y in b15.height: for x in b15.width: var c15 := Vector2i(x, y) var d15: int = absi(c15.x - hero15.x) + absi(c15.y - hero15.y) var expect15: int = 0 if d15 > bv.FOG_RADIUS: expect15 = 2 if ms.is_revealed(c15) else 1 var got15: int = int(cells_f1[c15].fog_state) if got15 != expect15: fog_ok = false _problems.append("case⑮ 迷雾呈态不符(%s dist=%d got=%d expect=%d)" % [str(c15), d15, got15, expect15]) break if not fog_ok: break # 半径边界格恰在圈内(曼哈顿距离恰 =R 的格 fog_state=0——存在性校验,12×12 板必有) var has_boundary := false var cells_f2: Dictionary = bv.cells_probe() for y in b15.height: for x in b15.width: if absi(x - hero15.x) + absi(y - hero15.y) == int(bv.FOG_RADIUS): has_boundary = true if int(cells_f2[Vector2i(x, y)].fog_state) != 0: _problems.append("case⑮ 边界圈内格被误遮(dist==R 应 fog_state=0)") break if has_boundary: break if not has_boundary: _problems.append("case⑮ 板面无 dist==R 格(板面异常)") # 移动跟随:把英雄挪到板角(move 动作逐格逼近成本高——直接白-box 校验公式换心后远格态翻转: # 取一个对当前英雄 dist>R 的已开格(记忆态),以远角为新心重算应翻转/保持一致性由公式承载, # 此处只验 set_fog_enabled(false) 全板归零与 true 恢复——开关两态契约主断言) var digest_before: Array = [] for y in b15.height: for x in b15.width: var c16 := Vector2i(x, y) digest_before.append(ms.is_revealed(c16)) bv.set_fog_enabled(false) var all_clear := true var cells_f3: Dictionary = bv.cells_probe() for y in b15.height: for x in b15.width: if int(cells_f3[Vector2i(x, y)].fog_state) != 0: all_clear = false if not all_clear: _problems.append("case⑮ 迷雾关未全板归零(off 态应 fog_state=0)") var digest_off: Array = [] for y in b15.height: for x in b15.width: var c17 := Vector2i(x, y) digest_off.append(ms.is_revealed(c17)) if digest_off != digest_before: _problems.append("case⑮ 迷雾开关改写逻辑态(两态结算逐位一致 🔴 契约破)") bv.set_fog_enabled(true) if int(bv.cells_probe()[Vector2i(0, 0)].fog_state) == 0 \ and absi(0 - hero15.x) + absi(0 - hero15.y) > int(bv.FOG_RADIUS): _problems.append("case⑮ 迷雾复开后远角未复原遮蔽(fog_state=0)") if _problems.is_empty(): print("[ui_input_probe] PASS — 悬停金框/禁入信号/路径预览/flag_place/chord/dig/英雄落位/影子构成/朝向/缩格重排/缺位回退/变异角标/视野迷雾 十六断言全绿(真实 push_input 路径——⑫图上命中⑬画布拟合 2026-09-28 增,⑭变异角标 2026-09-29 增,④b 旗纹理槽回归 2026-10-02 增,⑮视野迷雾 2026-10-03 增)") 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() var digp: Dictionary = bv.diggable_probe() for y in b.height: for x in b.width: var c := Vector2i(x, y) if not ms.is_revealed(c) and not digp.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)