diff --git a/game/core/level_runner.gd b/game/core/level_runner.gd index 64e4dad1..85b4c4ea 100644 --- a/game/core/level_runner.gd +++ b/game/core/level_runner.gd @@ -414,6 +414,52 @@ func visited_ids() -> Array: return out +# ---- 类型可见性读面(信息迷雾 v2.1:近处见类型,远处起雾)---- +# 可见集 = Boss/终点(type=="boss")∪ 当前候选(下一跳恒可见——生成承诺)∪ +# {id : |层(id) − 锚层| ≤ N};N = db.level.info_visible_layers(禁硬编码); +# 锚层 = 当前节点层,入口停点(无当前节点)= 0;层 = 入口 BFS 深度(与呈现层分层同源口径)。 +# 纯函数零状态:已过节点不豁免(单圈语义,零累积记忆);断点快照零新增字段。 +func type_visible_ids() -> Array[int]: + var out: Array[int] = [] + var layers := _bfs_layers() + var anchor := 0 + var cur := current_node() + if not cur.is_empty(): + anchor = int(layers.get(_node_id(cur), 0)) + var n: int = int(db.level.info_visible_layers) + for node in nodes(): + var nid := _node_id(node) + if String(node.get("type", "")) == "boss" \ + or (layers.has(nid) and absi(int(layers[nid]) - anchor) <= n): + out.append(nid) + for cid in next_candidate_ids(): + if not out.has(int(cid)): + out.append(int(cid)) + return out + + +# 入口 BFS 层表(id → 层深;入口层 0、出边逐层 +1,与 LevelMapScreen._assign_layers 同源)。 +# 不可达节点不落表(加载期孤儿校验应已拦截)。 +func _bfs_layers() -> Dictionary: + var layers := {} + var entry := _entry_node() + if entry.is_empty(): + return layers + var queue: Array[Dictionary] = [entry] + layers[_node_id(entry)] = 0 + var i := 0 + while i < queue.size(): + var cur: Dictionary[Variant, Variant] = queue[i] + i += 1 + var d := int(layers[_node_id(cur)]) + for nxt in _out_nodes(cur): + var nid := _node_id(nxt) + if not layers.has(nid): + layers[nid] = d + 1 + queue.append(nxt) + return layers + + func awaiting_choice() -> bool: return _phase == Phase.AWAITING_CHOICE diff --git a/game/data/schemas/level_table.gd b/game/data/schemas/level_table.gd index 67328b28..d37a4061 100644 --- a/game/data/schemas/level_table.gd +++ b/game/data/schemas/level_table.gd @@ -5,6 +5,7 @@ class_name LevelTable extends Resource @export var rows: Array[Dictionary] = [] # Array[Dictionary] +@export var info_visible_layers: int = 2 # 信息迷雾可见层数 N(int ≥1;类型仅在邻近 N 层内可见——关卡节点地图 v2.1) const NODE_TYPES := ["battle", "reward", "rest", "boss", "shop"] # 五类(关卡节点地图 v1.1) @@ -19,6 +20,8 @@ func row_by_level_index(level_index: int) -> Dictionary: func validate() -> Array: var errs: Array[Dictionary] = [] + if info_visible_layers < 1: + errs.append({"category": "domain", "message": "info_visible_layers 越域 int≥1: %d" % info_visible_layers}) if rows.is_empty(): errs.append({"category": "empty_table", "message": "LevelTable 为空"}) return errs diff --git a/game/data/tables/level.tres b/game/data/tables/level.tres index 8589bab1..ee4a96aa 100644 --- a/game/data/tables/level.tres +++ b/game/data/tables/level.tres @@ -4,6 +4,7 @@ [resource] script = ExtResource("1") +info_visible_layers = 2 rows = [ {"level_index": 1, "display_name": "第一层 · 地精洞窟", "rest_heal_pct": 30, "templates": [{"display_name": "地精洞窟·主线洞道(现行)", "nodes": [{"node_depth": 0, "type": "battle"}, {"node_depth": 1, "type": "battle"}, {"node_depth": 2, "type": "battle"}, {"node_depth": 3, "type": "battle"}, {"type": "reward"}, {"type": "rest"}, {"node_depth": 4, "type": "boss"}]}, {"display_name": "地精洞窟·先赏后整", "nodes": [{"node_depth": 0, "type": "battle"}, {"node_depth": 1, "type": "battle"}, {"node_depth": 2, "type": "battle"}, {"type": "reward"}, {"type": "rest"}, {"node_depth": 3, "type": "battle"}, {"node_depth": 4, "type": "boss"}]}, {"display_name": "地精洞窟·先整后赏", "nodes": [{"node_depth": 0, "type": "battle"}, {"node_depth": 1, "type": "battle"}, {"node_depth": 2, "type": "battle"}, {"type": "rest"}, {"type": "reward"}, {"node_depth": 3, "type": "battle"}, {"node_depth": 4, "type": "boss"}]}, {"display_name": "地精洞窟·赏战整", "nodes": [{"node_depth": 0, "type": "battle"}, {"node_depth": 1, "type": "battle"}, {"node_depth": 2, "type": "battle"}, {"type": "reward"}, {"node_depth": 3, "type": "battle"}, {"type": "rest"}, {"node_depth": 4, "type": "boss"}]}, {"display_name": "地精洞窟·整战赏", "nodes": [{"node_depth": 0, "type": "battle"}, {"node_depth": 1, "type": "battle"}, {"node_depth": 2, "type": "battle"}, {"type": "rest"}, {"node_depth": 3, "type": "battle"}, {"type": "reward"}, {"node_depth": 4, "type": "boss"}]}]}, {"level_index": 2, "display_name": "第二层 · 幽暗地域", "rest_heal_pct": 30, "templates": [{"display_name": "宽廊双干(现行)", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 4]}, {"node_depth": 2, "type": "reward", "next": [5, 6]}, {"node_depth": 3, "type": "rest", "next": [7]}, {"node_depth": 4, "type": "battle", "next": [7]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [7]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "三干中廊", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 5]}, {"node_depth": 2, "type": "reward", "next": [4, 5]}, {"node_depth": 3, "type": "rest", "next": [6]}, {"node_depth": 4, "type": "battle", "next": [6, 7]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [8]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "宽廊斜岔", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 5]}, {"node_depth": 2, "type": "reward", "next": [4, 6]}, {"node_depth": 3, "type": "rest", "next": [7]}, {"node_depth": 4, "type": "battle", "next": [7]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [7]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "三干宽门", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 4]}, {"node_depth": 2, "type": "reward", "next": [4, 5]}, {"node_depth": 3, "type": "rest", "next": [6, 7]}, {"node_depth": 4, "type": "battle", "next": [7]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [8]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "窄廊短支", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 4]}, {"node_depth": 2, "type": "reward", "next": [5]}, {"node_depth": 3, "type": "rest", "next": [5, 6]}, {"node_depth": 4, "type": "battle", "next": [6]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [7]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}]}, diff --git a/game/presentation/views/level_map_screen.gd b/game/presentation/views/level_map_screen.gd index bfe556d1..d0516d07 100644 --- a/game/presentation/views/level_map_screen.gd +++ b/game/presentation/views/level_map_screen.gd @@ -48,6 +48,7 @@ var _hover_id := -1 # 悬停预承诺候选(-1 = 无) var abandon_btn: Button # 放弃入口(可见性归组合根 set_abandon_available——只在冒险模式开) var status_badge: Button # 状态面板入口角标(顶栏 GP 徽右侧;连接归组合根) var _abandon_timer := ArmedTimer.new() # 放弃确认窗(_process 帧驱动倒计时机制收口;窗口=CONFIRM_WINDOW_S) +var _drawn_disp: Dictionary[int, Dictionary] = {} # 本停点实际绘制记录(_draw_graph 逐帧在实际绘制点重建——display_probe 消费面) var _node_icons := {} # 节点图标贴图(load_tex 守卫——缺位 null → 字形回退,冒烟/无素材检出安全) var _noise_rect: TextureRect # 屏底石纹(换层色板渐变承载面——null = 纯色回退无挂点) var _tint_level := 0 # 当前已应用 tint 的层号(0 = 未应用) @@ -415,17 +416,39 @@ func _id_of(node: Dictionary) -> int: return _runner.node_id_of(node) +# ---- 节点呈现决策面(信息迷雾 v2.1:类型仅在可见集内出示,Boss 标记不入雾)---- +# 纯函数、零内容读取:只消费类型与状态(节点内详情=打谁/给什么,图上从不显示)。 +static func node_display(node: Dictionary, nid: int, type_vis: Array, + is_current: bool, is_candidate: bool) -> Dictionary: + var t := String(node.get("type", "")) + var is_boss: bool = t == "boss" + var type_shown: bool = is_boss or type_vis.has(nid) + var label_text := String(NODE_TYPE_NAMES.get(t, t)) + if is_current: + label_text = "当前位置" + elif is_candidate: + label_text += " · 可选" + return { + "draw_icon": type_shown, + "draw_label": type_shown, + "label_text": label_text, + "boss_border": is_boss, + } + + # ---- 整图自绘(程序化占位——分层布局:层 = 入口 BFS 深度,同层垂直排布)---- func _draw_graph(canvas: Control) -> void: if _runner == null or not visible: return + _drawn_disp.clear() # 绘制记录随本帧重建——只反映实际发生的绘制(接线坏的判据面) var nodes: Array[Variant] = _runner.nodes() var pos := _layout(nodes, canvas) var current_id: int = _runner.current_node_id() # 当前节点 id(单链=下标 / 图=编号)——visited_ids() 已按编号空间解析 var visited: Array[Variant] = _runner.visited_ids() var cand_ids: Array[Variant] = _runner.next_candidate_ids() + var type_vis: Array[int] = _runner.type_visible_ids() # 本停点类型可见集(每帧一取——纯函数零状态) # 连线(通道)先行画(方块覆盖其上);悬停预承诺 = 所属通道再增亮(加粗) # 选路停点(候选非空)非候选通道降透明——金岔路从结构网分离(干线共线重叠缺陷的 # 视觉主因:不同源通道共用同一垂直干线,金边与石色边焊接成「整列可走」假象) @@ -454,39 +477,44 @@ func _draw_graph(canvas: Control) -> void: col = COL_ROOM_CURRENT var rect := Rect2(p - NODE_HALF, NODE_HALF * 2.0) canvas.draw_rect(rect, col) - if t == "boss": + var disp := node_display(node, nid, type_vis, nid == current_id, cand_ids.has(nid)) + var rec: Dictionary[String, Variant] = {"draw_icon": false, "draw_label": false, + "label_text": "", "boss_border": false} + if bool(disp["boss_border"]): # Boss 描边 = 紫赤 2px(§4.2 色相独占)+ 外缘 1px 深墨环(灰度冗余通道强制: # 紫赤灰度 72 对石纹噪底 65 灰度不可辨;灰度/色弱下 Boss 环经深墨环可辨) canvas.draw_rect(Rect2(rect.position - Vector2(3, 3), rect.size + Vector2(6, 6)), UiKit.INK_ROUTE, false, 1.0) canvas.draw_rect(rect, COL_BOSS_BORDER, false, 2.0) + rec["boss_border"] = true elif cand_ids.has(nid): # 悬停预承诺 = 亮金描边加粗(4px),常态候选 = 亮金描边 2px(spec States 悬停预承诺行) canvas.draw_rect(rect, COL_PASSAGE_LIT, false, 4.0 if _hover_id == nid else 2.0) # 节点图标(20px 烘焙 1:1 消费;双色墨制=乘法调制——当前金底深墨,暗底系 WARM 原色; # 贴图缺位 → 字形回退=贴图在位字形退役惯例) - var icon: Texture2D = _node_icons.get(t) - if icon != null: - var tint := UiKit.ICON_TINT_ON_GOLD if col == COL_ROOM_CURRENT else Color(1.0, 1.0, 1.0) - canvas.draw_texture_rect(icon, Rect2(p - Vector2(NODE_ICON_PX, NODE_ICON_PX) / 2.0, - Vector2(NODE_ICON_PX, NODE_ICON_PX)), false, tint) - else: - var font := get_theme_default_font() - var label := String(NODE_LABELS.get(t, "?")) - canvas.draw_string(font, p + Vector2(-6, 2), label, HORIZONTAL_ALIGNMENT_LEFT, -1, 16, UiKit.INK_ROUTE) + if bool(disp["draw_icon"]): + rec["draw_icon"] = true + var icon: Texture2D = _node_icons.get(t) + if icon != null: + var tint := UiKit.ICON_TINT_ON_GOLD if col == COL_ROOM_CURRENT else Color(1.0, 1.0, 1.0) + canvas.draw_texture_rect(icon, Rect2(p - Vector2(NODE_ICON_PX, NODE_ICON_PX) / 2.0, + Vector2(NODE_ICON_PX, NODE_ICON_PX)), false, tint) + else: + var font := get_theme_default_font() + var label := String(NODE_LABELS.get(t, "?")) + canvas.draw_string(font, p + Vector2(-6, 2), label, HORIZONTAL_ALIGNMENT_LEFT, -1, 16, UiKit.INK_ROUTE) # 编号(右上角——与图标占位区零重叠;双色墨随态) var num_ink := UiKit.INK_ROUTE if col == COL_ROOM_CURRENT else UiKit.WARM canvas.draw_string(get_theme_default_font(), Vector2(rect.end.x - 16.0, rect.position.y + 8.0), str(nid), HORIZONTAL_ALIGNMENT_RIGHT, 14, 10, num_ink) # 类型文字标签(demo ④:节点下方 11px;当前位=「当前位置」金、候选=类型名+「 · 可选」烛光黄、 # 其余=类型名暖米弱化;文字+图标+形状三通道) - var type_text := String(NODE_TYPE_NAMES.get(t, t)) - if nid == current_id: - type_text = "当前位置" - elif cand_ids.has(nid): - type_text += " · 可选" - var type_ink := COL_ROOM_CURRENT if nid == current_id else (UiKit.CANDLE if cand_ids.has(nid) else UiKit.WARM_FAINT) - canvas.draw_string(get_theme_default_font(), Vector2(p.x - 44.0, rect.end.y + 15.0), - type_text, HORIZONTAL_ALIGNMENT_CENTER, 88, 11, type_ink) + if bool(disp["draw_label"]): + rec["draw_label"] = true + rec["label_text"] = String(disp["label_text"]) + var type_ink := COL_ROOM_CURRENT if nid == current_id else (UiKit.CANDLE if cand_ids.has(nid) else UiKit.WARM_FAINT) + canvas.draw_string(get_theme_default_font(), Vector2(p.x - 44.0, rect.end.y + 15.0), + String(disp["label_text"]), HORIZONTAL_ALIGNMENT_CENTER, 88, 11, type_ink) + _drawn_disp[nid] = rec func _layout(nodes: Array, canvas: Control) -> Dictionary: @@ -513,6 +541,12 @@ func canvas_probe() -> Dictionary: return {"origin": _canvas.get_global_position(), "min_size": _canvas.custom_minimum_size} +# 类型可见性绘制只读记录(id → 本帧实际绘制事实:图标/文字/描边/文案;_draw_graph 在实际绘制点写入—— +# 接线层断言消费点:绕开决策面直接画,这里就露出「雾下也画了」) +func display_probe() -> Dictionary: + return _drawn_disp + + # 分层(layout/fit 共享件):layer = 入口 BFS 深度;同层 slot 垂直排布;未达节点兜底排末层 func _assign_layers(nodes: Array, layer: Dictionary, slot: Dictionary) -> void: var layer_counts := {} diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index 0dff5429..14963756 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -4923,6 +4923,25 @@ func _smoke_level_graph() -> void: _db.enemy.rows.remove_at(_db.enemy.rows.size() - 1) check(int(_db.enemy.rows.size()) == 20, "混编断言段注入行全量还原(表回 20 行——首批混编投放后基线行数,迭代 18 卡⑤;后续断言面零污染)") + # ⑬ 真图类型可见性(信息迷雾 v2.1):L2 入口停点纯核心面读数——候选 ⊆ 可见集 / + # Boss 恒可见 / 层距 > N 的非豁免节点入雾。N 取数据表(代码零硬编码)。 + var vis_runner = load("res://core/level_runner.gd").new(_db, _fresh_rng(42)) + var vis_ledger := {"hero_hp": 10, "gp": 0} + var vis_ok: bool = vis_runner.start_level_external(2, vis_ledger, {}, Callable(), {}, true, 0) + check(vis_ok and vis_runner.awaiting_choice() and vis_runner.current_node_id() == -1, + "可见性冒烟:L2 入口停点就位(defer_entry——无当前节点,锚层 0)") + var vis_ids: Array = vis_runner.type_visible_ids() + var vis_cand_in := true + for cid in vis_runner.next_candidate_ids(): + if not vis_ids.has(int(cid)): + vis_cand_in = false + check(vis_cand_in, "可见性冒烟:入口停点候选集 ⊆ 类型可见集(下一跳恒可见)〔fv4〕") + check(vis_ids.has(_boss_depth(2)), "可见性冒烟:L2 Boss(编号 %d)恒在可见集(终点不入雾)〔fv3〕" % _boss_depth(2)) + var vis_n: int = int(_db.level.info_visible_layers) + check(vis_n == 2, "可见性冒烟:N 读数据表 = %d(provisional 2——代码零硬编码)〔fv6〕" % vis_n) + check(not vis_ids.has(7) and not vis_ids.has(8), + "可见性冒烟:入口锚层 0 时层距 3/4 的节点 7/8 入雾(非豁免——入口 BFS 层差 > N)〔fv1〕") + # 冒烟隔离用全新 RngService(同 master 种子——回放确定性纪律) func _fresh_rng(seed_val: int): diff --git a/game/smoke/ui_input_probe.gd b/game/smoke/ui_input_probe.gd index a12d8246..3b455342 100644 --- a/game/smoke/ui_input_probe.gd +++ b/game/smoke/ui_input_probe.gd @@ -41,6 +41,59 @@ func _ready() -> void: if main.level_screen.visible: var ro = main.level_screen var cands: Array = main.run_scope.runner.next_candidate_ids() + # ---- ⑱ 信息迷雾呈现接线(回归闸):真机 display_probe 读实际绘制记录(雾下节点类型未画、 + # Boss 描边画了、候选类型画了)+点击雾下非候选节点零提交(雾不扩大可点域——候选集即合法集)---- + var disp: Dictionary = ro.display_probe() + var vis: Array = main.run_scope.runner.type_visible_ids() + var cur_id: int = main.run_scope.runner.current_node_id() + var fog_id := -1 + var boss_id := -1 + var nds_vis: Array = main.run_scope.runner.nodes() + for n in nds_vis: + var vnid: int = main.run_scope.runner.node_id_of(n) + if String(n.get("type", "")) == "boss": + boss_id = vnid + elif fog_id < 0 and vnid != cur_id and not cands.has(vnid) and not vis.has(vnid): + # 选区只按核心可见集判(不读绘制记录——选区与断言解耦,防「按记录选、再断言记录」自指) + fog_id = vnid + if boss_id < 0 or fog_id < 0 or not disp.has(fog_id) or not disp.has(boss_id) \ + or not disp.has(int(cands[0])): + _pre_fail("case⑱ 雾面节点选取失败或绘制记录缺位(boss=%d fog=%d 记录数=%d)" + % [boss_id, fog_id, disp.size()]) + return + if bool(disp[fog_id]["draw_icon"]) or bool(disp[fog_id]["draw_label"]): + _pre_fail("case⑱ 雾下节点类型仍出示(图标=%s 文字=%s——范围外只见结构)〔fv2〕" + % [str(disp[fog_id]["draw_icon"]), str(disp[fog_id]["draw_label"])]) + return + if not bool(disp[boss_id]["boss_border"]): + _pre_fail("case⑱ Boss 节点未带描边标记(id=%d)〔fv3〕" % boss_id) + return + if not bool(disp[int(cands[0])]["draw_icon"]): + _pre_fail("case⑱ 候选节点类型未出示(id=%d——下一跳恒可见)〔fv4〕" % int(cands[0])) + return + var fog_node := {} + for n in nds_vis: + if main.run_scope.runner.node_id_of(n) == fog_id: + fog_node = n + var fog_pos: Vector2 = ro.canvas_probe()["origin"] + ro.layout_probe(nds_vis)[fog_node] + # 零提交判据直接绑 route_chosen 发射:终态会被雾下节点类型(奖励/恢复/商店保持本屏、 + # 战斗/Boss 出屏)与运行器候选校验兜底,单看终态测不出命中面穿透——信号面才是提交面 + var submitted: Array = [] + var on_route := func(nid: int) -> void: submitted.append(nid) + ro.route_chosen.connect(on_route) + var evf := InputEventMouseButton.new() + evf.button_index = MOUSE_BUTTON_LEFT + evf.pressed = true + evf.position = fog_pos + evf.global_position = fog_pos + main.get_viewport().push_input(evf, true) + await get_tree().process_frame + await get_tree().process_frame + ro.route_chosen.disconnect(on_route) + if not submitted.is_empty() or not main.level_screen.visible or not main.run_scope.runner.awaiting_choice(): + _pre_fail("case⑱ 雾下节点点击产生提交(提交=%s visible=%s 选路态=%s——雾不扩大可点域)〔fv9〕" + % [str(submitted), str(main.level_screen.visible), str(main.run_scope.runner.awaiting_choice())]) + return var nds: Array = main.run_scope.runner.nodes() var tgt := {} for n in nds: @@ -95,9 +148,10 @@ func _ready() -> void: 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 + # 常数锚(画布拟合探针):直接引屏内导出常量(LAYER_BASE_X/LAYER_STEP_X/NODE_HALF)—— + # 与 _content_size 公式(LAYER_BASE_X + maxl*LAYER_STEP_X + NODE_HALF.x + 8)逐项同源,禁再手抄数字 + var need_x2: float = LevelMapScreen.LAYER_BASE_X + maxl2 * LevelMapScreen.LAYER_STEP_X \ + + LevelMapScreen.NODE_HALF.x + 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]) @@ -592,7 +646,7 @@ func _ready() -> void: if not bool(bv.type_glow_probe().get("enabled", false)): _problems.append("case⑰ 微光开关未恢复开态") if _problems.is_empty(): - print("[ui_input_probe] PASS — 悬停金框/禁入信号/路径预览/seal_place/chord/dig/英雄落位/影子构成/朝向/缩格重排/缺位回退/变异角标/视野迷雾/状态面板输入锁/未开格类型微光 十八断言全绿(真实 push_input 路径——⑫图上命中⑬画布拟合 2026-09-28 增,⑭变异角标 2026-09-29 增,④b 封印纹理槽回归 2026-10-02 增,⑮视野迷雾 2026-10-03 增〔+提灯接线断言 2026-10-05 迭代14⑤〕,⑯状态面板输入锁 2026-10-06 迭代17卡③增,⑰未开格类型微光 2026-10-17 迭代30卡①增)") + print("[ui_input_probe] PASS — 悬停金框/禁入信号/路径预览/seal_place/chord/dig/英雄落位/影子构成/朝向/缩格重排/缺位回退/变异角标/视野迷雾/状态面板输入锁/未开格类型微光 十八断言全绿(真实 push_input 路径——⑫图上命中⑬画布拟合 2026-09-28 增,⑭变异角标 2026-09-29 增,④b 封印纹理槽回归 2026-10-02 增,⑮视野迷雾 2026-10-03 增〔+提灯接线断言 2026-10-05 迭代14⑤〕,⑯状态面板输入锁 2026-10-06 迭代17卡③增,⑰未开格类型微光 2026-10-17 迭代30卡①增,⑱信息迷雾绘制接线(display_probe 读实际绘制记录)增)") get_tree().quit(0) else: for p in _problems: diff --git a/game/tests/run_all.gd b/game/tests/run_all.gd index 75305397..a2ee7b20 100644 --- a/game/tests/run_all.gd +++ b/game/tests/run_all.gd @@ -51,8 +51,10 @@ func _suites() -> Array: {"path": "res://tests/unit/core/treasure_items_test.gd", "expect": 0}, # 秘宝结构冻结断言面 {"path": "res://tests/unit/core/breakpoint_resume_test.gd", "expect": 0}, # 断点往返/续玩确定性/幂等/退出清断点 {"path": "res://tests/unit/core/curio_refresh_paid_test.gd", "expect": 0}, # 三选一刷新免费双源+GP 付费档(对齐商店 MC-1:价曲线/优先序/停点清零/钱包单口) + {"path": "res://tests/unit/core/level_map_type_vis_test.gd", "expect": 0}, # 信息迷雾——类型可见性(层距 N+Boss/候选豁免+单圈+N 读表+续玩同值) {"path": "res://tests/unit/presentation/pacing_queue_test.gd", "expect": 0}, # AC⑤/T9——排空零分叉/零丢行/延迟双态 {"path": "res://tests/unit/presentation/reward_overlay_test.gd", "expect": 0}, # 奖励节点 UI 直验(盲区补漏) + {"path": "res://tests/unit/presentation/level_map_info_fog_test.gd", "expect": 0}, # 信息迷雾——呈现显隐/Boss 豁免/详情零显示/大图拟合 {"path": "res://tests/unit/presentation/forge_workbench_test.gd", "expect": 0}, # 工作间两区六路线 UI 直验(V13 自动化前置面) {"path": "res://tests/unit/battle_settlement/damage_math_test.gd", "expect": 0}, {"path": "res://tests/unit/battle_settlement/damage_controller_test.gd", "expect": 0}, diff --git a/game/tests/unit/core/level_map_type_vis_test.gd b/game/tests/unit/core/level_map_type_vis_test.gd new file mode 100644 index 00000000..b3ca3d97 --- /dev/null +++ b/game/tests/unit/core/level_map_type_vis_test.gd @@ -0,0 +1,208 @@ +# tests/unit/core/level_map_type_vis_test.gd — 关卡图类型可见性(信息迷雾)直接单测 +# 断言面 = LevelRunner.type_visible_ids() 语义:层距 N 边界+Boss/终点与候选双豁免+ +# 单圈回雾(visited 不豁免)+N 读数据表(含越域 validate)+续玩重建逐位一致。 +# 图夹具白盒注入 level_row 与停点态(smoke_all ⑫ 白盒先例);层距=入口 BFS 层差。 +# 运行:godot --headless --path game --script res://tests/unit/core/level_map_type_vis_test.gd(expect 0) +extends SceneTree + +const Kit := preload("res://tests/unit/test_kit.gd") +const LevelTableScript := preload("res://data/schemas/level_table.gd") + + +func _init() -> void: + var db = load("res://core/config_db.gd").new() + var loaded: bool = db.load_tables("res://data/tables", false) + Kit.check_quiet(loaded, "可见性用例:数值表加载成功") + if not loaded: + quit(Kit.finish("level_map_type_vis_test")) + return + _test_field_in_table(db) + _test_layer_distance(db) + _test_candidate_exempt(db) + _test_boss_exempt(db) + _test_single_circle(db) + _test_single_chain_entry(db) + _test_n_from_table(db) + _test_resume_rebuild(db) + quit(Kit.finish("level_map_type_vis_test")) + + +# ---- 夹具 ---- + +# 图形态夹具(id = node_depth):单干 0→1→2→3→4→5→6→7 加一条 6→3 跨层跳边。 +# BFS 层:0=0/1=1/2=1/3=2/4=3/5=4/6=5/7=6——锚 6 时候选 3 层距 3 = N+1。 +func _graph_nodes() -> Array: + return [ + {"node_depth": 0, "type": "battle", "next": [1, 2]}, + {"node_depth": 1, "type": "battle", "next": [2]}, + {"node_depth": 2, "type": "reward", "next": [3]}, + {"node_depth": 3, "type": "rest", "next": [4]}, + {"node_depth": 4, "type": "battle", "next": [5]}, + {"node_depth": 5, "type": "battle", "next": [6]}, + {"node_depth": 6, "type": "battle", "next": [7, 3]}, + {"node_depth": 7, "type": "boss", "next": []}, + ] + + +# 单链夹具(无 next 键,仿 L1 现役模板形态):编号空间 = 数组下标,node_depth 只出现在战斗/Boss 上 +func _chain_nodes() -> Array: + return [ + {"node_depth": 0, "type": "battle"}, + {"node_depth": 1, "type": "battle"}, + {"node_depth": 2, "type": "battle"}, + {"node_depth": 3, "type": "battle"}, + {"type": "reward"}, + {"type": "rest"}, + {"node_depth": 4, "type": "boss"}, + ] + + +# validate 正样本(无跨层跳边的合法图:入口唯一、Boss 编号最大收尾、层宽 ≤5) +func _valid_graph_nodes() -> Array: + return [ + {"node_depth": 0, "type": "battle", "next": [1, 2]}, + {"node_depth": 1, "type": "battle", "next": [3]}, + {"node_depth": 2, "type": "reward", "next": [3]}, + {"node_depth": 3, "type": "battle", "next": [4]}, + {"node_depth": 4, "type": "boss", "next": []}, + ] + + +# 白盒 runner:夹具行集注入 db.level + level_row,停点态直设(anchor_id < 0 = 入口停点) +func _runner(db, nodes: Array, anchor_id: int, candidates: Array, + graph_mode: bool = true, n: int = 2): + var t := LevelTableScript.new() + t.info_visible_layers = n + t.rows = [{"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30, "nodes": nodes}] + db.level = t + var r = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new()) + r.level_row.assign({"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30, "nodes": nodes}) + r._graph_mode = graph_mode + r.visited.clear() + r._candidates.assign(candidates) + r.node_index = -1 + if anchor_id >= 0: + for i in nodes.size(): + if int((nodes[i] as Dictionary).get("node_depth", -1)) == anchor_id: + r.node_index = i + break + return r + + +# ---- ⑥ N 进数据表(含越域校验)〔fv6〕---- + +func _test_field_in_table(db) -> void: + var n_in_table: int = int(db.level.info_visible_layers) + Kit.check("N 落表:level.tres 读数 2(实际 %d)〔fv6〕" % n_in_table, n_in_table == 2) + var t := LevelTableScript.new() + t.rows = [{"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30, "nodes": _valid_graph_nodes()}] + t.info_visible_layers = 1 + Kit.check("N=1 合法:validate 零错〔fv6〕", t.validate().is_empty()) + t.info_visible_layers = 0 + Kit.check("N 越域 0:validate 判 domain 错〔fv6〕", _has_n_domain_err(t.validate())) + t.info_visible_layers = -1 + Kit.check("N 越域 -1:validate 判 domain 错〔fv6〕", _has_n_domain_err(t.validate())) + + +func _has_n_domain_err(errs: Array) -> bool: + for e in errs: + if String(e.get("category", "")) == "domain" \ + and String(e.get("message", "")).contains("info_visible_layers"): + return true + return false + + +# ---- ① 邻近 N 层见类型(层距边界)〔fv1〕---- + +func _test_layer_distance(db) -> void: + var nodes := _graph_nodes() + var r = _runner(db, nodes, 1, [nodes[2]]) # 锚 = 节点 1(层 1),候选 = 节点 2 + var vis: Array = r.type_visible_ids() + Kit.check("层距 N 内可见:锚层 1 取节点 4(层距 2)〔fv1〕", vis.has(4)) + Kit.check("层距 N+1 不可见:锚层 1 排节点 5(层距 3)〔fv1〕", not vis.has(5)) + Kit.check("层距 N+2 不可见:锚层 1 排节点 6(层距 4)〔fv1〕", not vis.has(6)) + + +# ---- ② 下一跳恒可见(跨层跳边候选)〔fv4〕---- + +func _test_candidate_exempt(db) -> void: + var nodes := _graph_nodes() + var r = _runner(db, nodes, 6, [nodes[3]]) # 锚 = 节点 6(层 5),候选 = 节点 3(层 2,层距 3) + var vis: Array = r.type_visible_ids() + Kit.check("候选恒显:跨层跳边候选(层距 N+1)仍在可见集〔fv4〕", vis.has(3)) + Kit.check("候选豁免不改层规则:同锚点非候选节点 2(层距 4)不可见〔fv4〕", not vis.has(2)) + + +# ---- ③ Boss/终点不入雾+⑤ 入口停点锚层 0〔fv3〕〔fv1〕---- + +func _test_boss_exempt(db) -> void: + var nodes := _graph_nodes() + var r = _runner(db, nodes, -1, [nodes[0]]) # 入口停点(无当前节点,锚层 0) + var vis: Array = r.type_visible_ids() + Kit.check("入口停点锚层 0:层距 N 内(节点 2)可见〔fv1〕", vis.has(2)) + Kit.check("入口停点锚层 0:层距 N+2(节点 4)不可见〔fv1〕", not vis.has(4)) + Kit.check("Boss 恒显:层距 6 的末位 Boss 仍在可见集〔fv3〕", vis.has(7)) + Kit.check("Boss 豁免不扩散:同层距的非 Boss 节点 6 不在可见集〔fv3〕", not vis.has(6)) + + +# ---- ④ 单圈回雾(visited 不豁免)〔fv7〕---- + +func _test_single_circle(db) -> void: + var nodes := _graph_nodes() + var r = _runner(db, nodes, 6, [nodes[7], nodes[3]]) + r.visited.assign([nodes[0], nodes[1], nodes[2], nodes[3], nodes[4], nodes[5], nodes[6]]) + var vis: Array = r.type_visible_ids() + Kit.check("单圈回雾:已过远端节点 0(层距 5)不因 visited 豁免〔fv7〕", not vis.has(0)) + Kit.check("单圈回雾:已过中距节点 2(层距 4)不可见〔fv7〕", not vis.has(2)) + Kit.check("单圈只按锚层取窗:已过近端节点 4(层距 2)仍可见〔fv7〕", vis.has(4)) + + +# ---- ⑤ 单链退化形态(编号空间 = 数组下标)〔fv1〕---- + +func _test_single_chain_entry(db) -> void: + var nodes := _chain_nodes() + var r = _runner(db, nodes, -1, [nodes[0]], false) + var vis: Array = r.type_visible_ids() + Kit.check("单链退化形态:入口锚层 0,下标 2 可见〔fv1〕", vis.has(2)) + Kit.check("单链退化形态:层距 N+1(下标 3)不可见〔fv1〕", not vis.has(3)) + Kit.check("单链编号空间 = 数组下标:Boss 以末位下标 6 入集(非 node_depth 4)〔fv1〕", + vis.has(6) and not vis.has(4)) + + +# ---- ⑥ N 读表:可见窗随表值随动〔fv6〕---- + +func _test_n_from_table(db) -> void: + var nodes := _graph_nodes() + var r = _runner(db, nodes, 1, [nodes[2]]) # 锚层 1 + db.level.info_visible_layers = 1 + Kit.check("N 读表:N=1 时节点 4(层距 2)出窗〔fv6〕", not r.type_visible_ids().has(4)) + db.level.info_visible_layers = 3 + Kit.check("N 读表:N=3 时节点 5(层距 3)入窗〔fv6〕", r.type_visible_ids().has(5)) + db.level.info_visible_layers = 10 + var all_in := true + for nid in 7: # 非豁免节点 0..6(7 = Boss 恒显,不作判据) + if not r.type_visible_ids().has(nid): + all_in = false + Kit.check("N 读表:N ≥ 全图层高 → 全部非豁免节点可见〔fv6〕", all_in) + + +# ---- ⑦ 续玩重建同值〔fv7〕---- + +func _test_resume_rebuild(db) -> void: + var nodes := _graph_nodes() + var t := LevelTableScript.new() + t.info_visible_layers = 2 + t.rows = [{"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30, "nodes": nodes}] + db.level = t + var ledger := {"hero_hp": 10, "gp": 0} + var resumed = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new()) + var ok: bool = resumed.start_level_external(1, ledger, {}, Callable(), {}, false, 0, + {"node_id": 2, "visited_ids": [0, 1, 2]}) + Kit.check_quiet(ok, "续玩用例:resume_state 重建停点成功") + if not ok: + return + Kit.check_quiet(resumed.awaiting_choice(), "续玩用例:重建后处于选路停点") + var direct = _runner(db, nodes, 2, [nodes[3]]) + direct.visited.assign([nodes[0], nodes[1], nodes[2]]) + Kit.check("续玩重建同值:resume 路径与同停点直入 type_visible_ids() 逐位一致〔fv7〕", + resumed.type_visible_ids() == direct.type_visible_ids()) diff --git a/game/tests/unit/core/level_map_type_vis_test.gd.uid b/game/tests/unit/core/level_map_type_vis_test.gd.uid new file mode 100644 index 00000000..124fad32 --- /dev/null +++ b/game/tests/unit/core/level_map_type_vis_test.gd.uid @@ -0,0 +1 @@ +uid://brh5an0s8q6ss diff --git a/game/tests/unit/presentation/level_map_info_fog_test.gd b/game/tests/unit/presentation/level_map_info_fog_test.gd new file mode 100644 index 00000000..732900f9 --- /dev/null +++ b/game/tests/unit/presentation/level_map_info_fog_test.gd @@ -0,0 +1,196 @@ +# tests/unit/presentation/level_map_info_fog_test.gd — 信息迷雾呈现显隐直接单测 +# 断言面 = LevelMapScreen.node_display 决策面(显隐/Boss 豁免/详情零显示/label 三态)+ +# display_probe 真机绘制面读数+大图(11 层×6 槽含双 Boss)画布拟合与布局包络。 +# 节点图夹具白盒注入 LevelRunner(level_row+停点态),屏体真机实例化走 open_choices 真实入口。 +# 运行:godot --headless --path game --script res://tests/unit/presentation/level_map_info_fog_test.gd(expect 0) +extends SceneTree + +const Kit := preload("res://tests/unit/test_kit.gd") +const LevelTableScript := preload("res://data/schemas/level_table.gd") +const ScreenScript := preload("res://presentation/views/level_map_screen.gd") + + +func _initialize() -> void: + call_deferred("_run") + + +func _run() -> void: + var db = load("res://core/config_db.gd").new() + var loaded: bool = db.load_tables("res://data/tables", false) + Kit.check_quiet(loaded, "迷雾呈现用例:数值表加载成功") + if not loaded: + quit(Kit.finish("level_map_info_fog_test")) + return + _test_fog_hides_type() + _test_boss_exempt_and_multi_boss() + _test_details_never_shown() + _test_label_three_states() + await _test_big_graph_fit(db) + quit(Kit.finish("level_map_info_fog_test")) + + +# 白盒 runner:夹具行集注入 db.level + level_row,入口停点态(node_index=-1,候选=入边首节点) +func _runner(db, nodes: Array, candidates: Array, n: int = 2): + var t := LevelTableScript.new() + t.info_visible_layers = n + t.rows = [{"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30, "nodes": nodes}] + db.level = t + var r = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new()) + r.level_row.assign({"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30, "nodes": nodes}) + r._graph_mode = true + r.node_index = -1 + r._candidates.assign(candidates) + return r + + +# ---- ① 范围外只见结构:雾下节点类型图标与文字均不绘制〔fv2〕---- + +func _test_fog_hides_type() -> void: + var node := {"node_depth": 3, "type": "battle"} + var shown: Dictionary = ScreenScript.node_display(node, 2, [2], false, false) + Kit.check("可见集内节点:图标/类型文字均绘制〔fv2〕", + bool(shown["draw_icon"]) and bool(shown["draw_label"])) + var fogged: Dictionary = ScreenScript.node_display(node, 3, [2], false, false) + Kit.check("雾下节点:图标与类型文字均不绘制(显隐语义,非灰化)〔fv2〕", + not bool(fogged["draw_icon"]) and not bool(fogged["draw_label"])) + Kit.check("雾下非 Boss 节点无 Boss 描边〔fv2〕", not bool(fogged["boss_border"])) + + +# ---- ② Boss/终点不入雾+多终点标记〔fv3〕---- + +func _test_boss_exempt_and_multi_boss() -> void: + var type_vis: Array = [1, 7, 9] # 双 Boss 图:7/9 均在可见集 + var boss_a: Dictionary = ScreenScript.node_display({"node_depth": 7, "type": "boss"}, 7, type_vis, false, false) + var boss_b: Dictionary = ScreenScript.node_display({"node_depth": 9, "type": "boss"}, 9, type_vis, false, false) + Kit.check("多终点:两个 Boss 节点均带 Boss 描边〔fv3〕", + bool(boss_a["boss_border"]) and bool(boss_b["boss_border"])) + Kit.check("Boss 不入雾:可见集与图标/文字三通道均在〔fv3〕", + bool(boss_a["draw_icon"]) and bool(boss_a["draw_label"]) and bool(boss_b["draw_icon"])) + var far_boss: Dictionary = ScreenScript.node_display({"node_depth": 9, "type": "boss"}, 9, [1], false, false) + Kit.check("Boss 豁免独立于可见集:不在 type_vis 的 Boss 仍出示类型〔fv3〕", + bool(far_boss["draw_icon"]) and bool(far_boss["draw_label"])) + var fogged_battle: Dictionary = ScreenScript.node_display({"node_depth": 5, "type": "battle"}, 5, type_vis, false, false) + Kit.check("Boss 豁免不扩散:雾下非 Boss 仍隐类型〔fv3〕", not bool(fogged_battle["draw_icon"])) + + +# ---- ③ 节点内详情图上从不显示〔fv5〕---- + +func _test_details_never_shown() -> void: + var plain := {"node_depth": 3, "type": "battle"} + var loaded := {"node_depth": 3, "type": "battle", "enemy_id": "goblin_secret", "reward_id": "reward_secret"} + var a: Dictionary = ScreenScript.node_display(plain, 3, [3], false, true) + var b: Dictionary = ScreenScript.node_display(loaded, 3, [3], false, true) + var same := true + for k in a.keys(): + if a[k] != b[k]: + same = false + Kit.check("决策面不消费内容键:携带 enemy_id/reward_id 与不带者输出逐键相等〔fv5〕", same) + Kit.check("label_text 不含内容值(实际「%s」)〔fv5〕" % String(b["label_text"]), + not String(b["label_text"]).contains("goblin_secret") and not String(b["label_text"]).contains("reward_secret")) + + +# ---- ④ label_text 三态文案〔fv2〕---- + +func _test_label_three_states() -> void: + var node := {"node_depth": 3, "type": "battle"} + Kit.check("当前位文案=「当前位置」〔fv2〕", + String(ScreenScript.node_display(node, 3, [3], true, false)["label_text"]) == "当前位置") + Kit.check("候选文案=「战斗 · 可选」〔fv2〕", + String(ScreenScript.node_display(node, 3, [3], false, true)["label_text"]) == "战斗 · 可选") + Kit.check("其余文案=类型名「战斗」〔fv2〕", + String(ScreenScript.node_display(node, 3, [3], false, false)["label_text"]) == "战斗") + + +# ---- 大图(11 层×6 槽含双 Boss)画布拟合与布局包络〔fv8〕---- + +# 大图夹具:编号按 12 层×6 槽构造,层 0 入口、末层双 Boss;含层 1 首节点→第 3 层节点的跨层跳边。 +# 跳边目标按 BFS 最短路径提前一层,其后继链随之提前——布局落 11 层(最大槽 7: +# 跳边目标并入第 2 层槽组)。断言按布局实测取最大层/最大槽,画布公式与之同源。 +func _big_graph_nodes() -> Array: + var layers: Array = [] + var total := 0 + var tiers := 12 + for l in tiers: + var width: int = 1 if l == 0 else (2 if l == tiers - 1 else 6) + var ids: Array = [] + for i in width: + ids.append(total) + total += 1 + layers.append(ids) + var nodes: Array = [] + var jump_from := -1 + var jump_to := -1 + for l in tiers: + for i in layers[l].size(): + var nid: int = layers[l][i] + var n := {"node_depth": nid} + if l == 0: + n["type"] = "battle" + n["next"] = layers[1].duplicate() # 入口扇出到全层(层宽 6) + elif l == tiers - 1: + n["type"] = "boss" + n["next"] = [] + else: + if i % 5 == 0: + n["type"] = "reward" if l % 2 == 0 else "rest" + else: + n["type"] = "battle" + var w2: int = layers[l + 1].size() + n["next"] = [layers[l + 1][i % w2], layers[l + 1][(i + 1) % w2]] + nodes.append(n) + jump_from = layers[1][0] + jump_to = layers[3][2] + for n in nodes: + if int(n["node_depth"]) == jump_from: + (n["next"] as Array).append(jump_to) + break + return nodes + + +func _test_big_graph_fit(db) -> void: + var nodes := _big_graph_nodes() + var entry: Dictionary = nodes[0] + var runner = _runner(db, nodes, [nodes[1]]) + var screen = ScreenScript.new() + root.add_child(screen) + screen.open_choices([nodes[1]], runner) + await process_frame + await process_frame + var min_size: Vector2 = screen.canvas_probe()["min_size"] + var pos: Dictionary = screen.layout_probe(nodes) + var origin: Vector2 = (pos[entry] as Vector2) - Vector2(ScreenScript.LAYER_BASE_X, ScreenScript.SLOT_BASE_Y) + # 最大层 / 最大槽按布局实测反解(公式与 _fit_canvas 同源) + var maxl := 0 + var maxslot := 0 + for node in nodes: + var p: Vector2 = pos[node] + maxl = maxi(maxl, int(round((p.x - origin.x - ScreenScript.LAYER_BASE_X) / ScreenScript.LAYER_STEP_X))) + maxslot = maxi(maxslot, int(round((p.y - origin.y - ScreenScript.SLOT_BASE_Y) / ScreenScript.SLOT_STEP_Y))) + Kit.check("大图夹具规模:≥11 层 × ≥6 槽(实测最大层 %d/最大槽 %d)〔fv8〕" % [maxl, maxslot], + maxl >= 10 and maxslot >= 5) + var need_w: float = ScreenScript.LAYER_BASE_X + maxl * ScreenScript.LAYER_STEP_X \ + + ScreenScript.NODE_HALF.x + 8.0 + var need_h: float = ScreenScript.SLOT_BASE_Y + maxslot * ScreenScript.SLOT_STEP_Y \ + + ScreenScript.NODE_HALF.y + 24.0 + Kit.check("大图拟合:画布 min.x 覆盖最深节点(%.1f ≥ %.1f)〔fv8〕" % [min_size.x, need_w], + min_size.x >= need_w - 0.01) + Kit.check("大图拟合:画布 min.y 覆盖最宽层底节点(%.1f ≥ %.1f)〔fv8〕" % [min_size.y, need_h], + min_size.y >= need_h - 0.01) + var fit_rect := Rect2(origin, min_size) + var all_in := true + for node in nodes: + var p: Vector2 = pos[node] + if not fit_rect.encloses(Rect2(p - ScreenScript.NODE_HALF, ScreenScript.NODE_HALF * 2.0)): + all_in = false + Kit.check("大图布局:全节点方块落在拟合画布内(origin=(%.1f,%.1f) size=(%.1f,%.1f))〔fv8〕" + % [origin.x, origin.y, min_size.x, min_size.y], all_in) + var disp: Dictionary = screen.display_probe() + var boss_ids: Array = [] + for node in nodes: + if String(node["type"]) == "boss": + boss_ids.append(int(node["node_depth"])) + Kit.check("大图呈现:两个终点 Boss 均有描边标记〔fv8〕", + disp.has(boss_ids[0]) and disp.has(boss_ids[1]) + and bool(disp[boss_ids[0]]["boss_border"]) and bool(disp[boss_ids[1]]["boss_border"])) + Kit.check("大图呈现:入口停点远处战斗节点(编号 32,层 6)类型入雾〔fv8〕", not bool(disp[32]["draw_icon"])) + screen.queue_free() diff --git a/game/tests/unit/presentation/level_map_info_fog_test.gd.uid b/game/tests/unit/presentation/level_map_info_fog_test.gd.uid new file mode 100644 index 00000000..801874ad --- /dev/null +++ b/game/tests/unit/presentation/level_map_info_fog_test.gd.uid @@ -0,0 +1 @@ +uid://bncwlglvp7tth diff --git a/reports/iter34-代拍板-2026-10-22.md b/reports/iter34-代拍板-2026-10-22.md index 2ecf5818..324eaffe 100644 --- a/reports/iter34-代拍板-2026-10-22.md +++ b/reports/iter34-代拍板-2026-10-22.md @@ -16,3 +16,18 @@ ## 白名单外未代拍事项 无。实现期未遇到需要超出 §10 清单自决的设计分歧。 + +## 卡④ 信息迷雾可见性批 + +> 白名单依据=`reports/需求卡片-信息迷雾可见性批-2026-10-21.md` §9(六条)。 + +| # | 白名单条目 | 实际做法 | 约束核验 | +|---|---|---|---| +| 1 | provisional N=2(§9-1) | `level.tres` 挂 `info_visible_layers = 2`;`validate` 只校验 int ≥ 1 | 定值仍在数据表,代码零硬编码;N=1/3 改表实测可见窗随动 | +| 2 | API/探针命名(§9-2) | `info_visible_layers`/`type_visible_ids`/`node_display`/`display_probe` | 均无 `fog_` 前缀,与战斗「视野迷雾」零撞词 | +| 3 | 雾下节点呈现落点(§9-3) | 图标/类型文字改读 `node_display` 输出;节点方块、编号、连线结构通道零改动 | 编号保留、类型文字不绘制;结构通道冒烟与渲染闸全绿 | +| 4 | 大图夹具规模(§9-4) | 12 个 BFS 层构造(跨层跳边压层后布局实得 11 层×6 槽)、含双 Boss、跨层跳边 | 满足 ≥10 层 × ≥5 槽;断言按布局实测 `maxl/maxslot` 推导 | +| 5 | 常数锚同源化方式(§9-5) | 探针直接引 `LevelMapScreen.LAYER_BASE_X/LAYER_STEP_X/NODE_HALF` 导出常量 | 与 `_fit_canvas` 公式逐项同源,无手抄数字副本 | +| 6 | 提交切分与分支命名(§9-6) | 分支 `iter34-fog`;三笔:实现与断言(`ed8e0f9f`)、⑱ 判据改绑(`4ef154b9`)、类型闸归零+探针补尾缀(`6f04f29b`) | 三笔均在 `game/` 内;报告随批落 `reports/` | +| 7 | 白名单外——审计缺漏 D1 的接线层验收手段(卡片 §9 未列,已呈报三案) | 呈报甲(补绘制输出观测面)/乙(改 §7.1 口径)/丙(单次快照降级)并推荐甲;用户回「继续未完成的任务」后按甲落地:`_draw_graph` 绘制点记录本帧绘制事实,`display_probe()` 改读该记录 | 改动集中在提交 `1970e0ef`,一笔可回退;未触碰冻结视觉件(零新画法/色值/线宽/缩放滚动),新增面为只读成员与记录赋值 | + diff --git a/reports/iter34-卡④-验收审计-2026-10-22.md b/reports/iter34-卡④-验收审计-2026-10-22.md new file mode 100644 index 00000000..eda8085d --- /dev/null +++ b/reports/iter34-卡④-验收审计-2026-10-22.md @@ -0,0 +1,164 @@ +# 迭代 34 卡④(信息迷雾可见性批)独立验收审计报告(2026-10-22) + +> 审计员:未参与本卡实现的独立子代理。审计期只读源码/测试/产出物,自跑命令取证;除本报告外未写任何文件。 +> 审计对象:`D:\code\g-plan2-iter34-fog`(分支 `iter34-fog`,任务指定被审 HEAD `477fd0f5`,审计起点 `43c0f3c1`)。 +> 审计期分支前进说明:审计过程中分支新增 `6757c21e`(docs-only,仅 `reports/iter34-反哺点-2026-10-22.md` 追加 ⑮ 体验验证清单登记,6 行)。本报告结论按任务指定 HEAD `477fd0f5` 的代码面给出;`6757c21e` 不触及代码与测试,不影响任何判定,仅在反哺点相符性一节并入核对。 + +## 1. 审计范围与基线 + +| 项 | 内容 | +|---|---| +| 被审 diff | `git diff 43c0f3c1..HEAD`:14 个文件(`game/` 11 件:核心 1/schema 1/数据 1/呈现 1/冒烟 2/单测 4(含 2 个 .uid)/manifest 1;`reports/` 3 件)。审计期 HEAD 前进后为 15 件(多 1 件 docs-only) | +| 工作树状态 | 审计开工与收尾 `git status --short` 均为空;4 次变异注入后均 `git checkout --` 还原并复核为空 | +| 环境 | Godot 4.7.2 console;python 前置 `PYTHONIOENCODING=utf-8`;长测前清理了残留 `ui_click_probe` Godot 进程(2 个),全程串行单实例 | +| 不采信项 | 实现报告 §5.7 的「已全绿」自述仅作对照,判定一律用下面自跑读数 | + +### 1.1 自跑命令与读数(原文) + +| # | 命令(工作目录) | 读数原文 | 退出码 | +|---|---|---|---| +| C1 | `Godot…console.exe --headless --path . --script res://tests/unit/core/level_map_type_vis_test.gd`(`game/`) | `[level_map_type_vis_test] all green (26 checks)` | 0 | +| C2 | 同上换 `res://tests/unit/presentation/level_map_info_fog_test.gd` | `[level_map_info_fog_test] all green (19 checks)` | 0 | +| C3 | 同上 `res://tests/run_all.gd` | `[run_all] all green (40 suites)` | 0 | +| C4 | `Godot…console.exe --headless --path . res://smoke/ui_input_probe.tscn`(`game/`) | `[ui_input_probe] PASS — …十八断言全绿`(见 §4 观察项 3) | 0 | +| C5 | `python tools/run_smoke.py`(工作树根) | `[run_smoke] 摘要: 冒烟完成:1211 项检查,0 项失败`+`[run_smoke] PASS(退出码 / 摘要失败数 / 脚本错误扫描 三闸全过)`;新增 5 条「可见性冒烟」断言逐条 `[PASS]`(含〔fv4〕〔fv3〕〔fv6〕〔fv1〕) | 0 | +| C6 | `bash game/tests/static_scan.sh` | `[audit_type_hints] PASS——产品码类型闸归零(R1/R2/R3)`;`[static_scan] S9 ok` … `结构审计 PASS` | 0 | +| C7 | `python tools/audit_type_hints.py` | `[audit_type_hints] PASS——产品码类型闸归零(R1/R2/R3)` | 0 | +| C8 | 读 `game/tests/.gates/receipt.json` | `commit: 4ef154b9…`;`09 静态扫描 exit: "1"`;`all_green: false`;`changed_vs_baseline: ["09 静态扫描:红项清单变化…"]` | — | + +要点:C1–C7 证明**当前代码面**的冒烟、单元、探针、静态扫描在我手上全绿;C8 证明**闸回执证据**停留在 `4ef154b9`,且该次 09 红、`all_green=false`,此后还有 2 个代码相关提交(`6f04f29b` 改 3 个 `game/` 文件、`477fd0f5`/`6757c21e` 仅文档)。这一差异见 §5.2 与 §6 缺漏 D2。 + +## 2. §7.1 对照表逐行结论(fv1–fv9) + +判定口径:①断言真实存在且尾缀可 grep;②真实入口符合卡片该行声明;③断言强度足以在改动撤销/单点变异时变红。行内「通过」均附自跑依据。 + +| fv | 断言存在(文件:行,实测 grep) | 真实入口核对 | 断言强度核对 | 结论 | +|---|---|---|---|---| +| **fv1 邻近 N 层见类型** | `level_map_type_vis_test.gd:121/122/123`(层距 2 可见、3/4 不可见)、`:142/143`(入口锚层 0)、`:166/167/168`(单链退化);`smoke_all.gd:4942-4943`(真图节点 7/8 入雾)。尾缀〔fv1〕共 8 处断言+3 处注释 | 核心 `type_visible_ids()` 直调(卡片该行声明即此)+冒烟真图 L2 入口停点 | 边界两侧都断言(N 内真、N+1 假),非恒真;M1 实测 4 条红(§3) | **通过** | +| **fv2 范围外只见结构** | `level_map_info_fog_test.gd:51/54/56`(显隐与无描边)、`:96/98/100`(label 三态);`ui_input_probe.gd:59-62`(⑱ 雾下节点双通道,尾缀在 :60)。尾缀〔fv2〕共 8 处 | 单元=`ScreenScript.node_display` 纯函数(卡片允许);探针=`ro.display_probe()`。**但 `display_probe()` 内部是逐节点重调 `node_display`(`level_map_screen.gd:536-547`),不是读取实际绘制输出**——卡片该行要求「真机 `display_probe()` 读绘制面(接线层验收——防『断言在、接线坏、不红』)」不成立 | 单元决策面断言有效(M2 红);⑱ 的「图标不出示」判据自指(fog_id 正是按 `draw_icon==false` 选出,再断言其 false,恒真;「文字不出示」判据有效)。**接线层(`_draw_graph` 是否消费决策面)零断言:我注入「两处 `if bool(disp[...])` 直接改 `if true`」=模拟接线坏,C2 仍 `19 checks` 全绿、C4 仍 PASS**(§3 W1) | **缺漏**(详见 §6 D1) | +| **fv3 Boss/终点不入雾+多终点标记** | `level_map_type_vis_test.gd:144/145`(远距 Boss 在集、同层距非 Boss 不在集);`level_map_info_fog_test.gd:65/67/70/73`(双 Boss 描边、Boss 三通道在、远 Boss 不入雾、豁免不扩散);`ui_input_probe.gd:63-64`;`smoke_all.gd:4939`(真图 L2 Boss=9 在集)。尾缀〔fv3〕共 9 处断言 | 核心可见集直调+`node_display` 直调+真机 `display_probe`(卡片该行声明即此)+冒烟真图 | 单 Boss 核心豁免与双 Boss 决策面均有断言;M3/M4 报告已证(我未复跑,M2 连带 `Boss 豁免不扩散〔fv3〕` 实测红,说明该条有敏感度)。观察项:双 Boss 夹具把 7/9 直接写进 `type_vis=[1,7,9]`,未从 `type_visible_ids()` 推导「均在集」,多终点核心成员性只有单 Boss 用例覆盖同一代码分支(非阻塞,见 §4 观察项 4) | **通过**(含 1 条观察项) | +| **fv4 下一跳恒可见** | `level_map_type_vis_test.gd:132/133`(跨层跳边候选层距 N+1 仍在集、同锚非候选不在集);`smoke_all.gd:4938`(真图候选 ⊆ 可见集);`ui_input_probe.gd:66-67`(真机候选 draw_icon)。尾缀〔fv4〕共 3 处 | 核心直调+冒烟真图+探针 | 跨层跳边是反例夹具,删候选豁免会红(报告 M5;未复跑);M2 不连带此条,逻辑独立 | **通过** | +| **fv5 节点内详情不显示** | `level_map_info_fog_test.gd:87/88`(携带 `enemy_id/reward_id` 与不带者逐键相等;label 不含内容值,实际打印「战斗 · 可选」)。尾缀〔fv5〕共 3 处 | `node_display` 纯函数(卡片该行声明即此) | 逐键相等+字符串不含,两条互补;M6 报告已证(未复跑)。`node_display` 正文只读 `type`,代码审读一致 | **通过** | +| **fv6 N 进数据表** | `level_map_type_vis_test.gd:96`(真表读数 2)、`:100/102/104`(N=1 合法、0/-1 判 domain 错)、`:178/180/186`(改表 N=1/3/10 可见窗随动);`smoke_all.gd:4940-4941`(真图读表=2)。尾缀〔fv6〕共 7 处断言 | `LevelTable.validate()`+`db.level.info_visible_layers` 读值(核心直调,卡片声明即此) | 不是「读一次字段」——改表值三档窗随动+越域双值判错,强度够;M7a/M7b 报告已证(未复跑) | **通过** | +| **fv7 单圈语义** | `level_map_type_vis_test.gd:155/156/157`(visited 不豁免:远/中距回雾、近端仍显)、`:207`(`start_level_external` resume_state 重建与直入逐位一致)。尾缀〔fv7〕共 6 处 | 核心直调+真实 resume 入口 | visited 显式注入后仍不可见=单圈成立;resume 断言两侧同函数、若两者同空会平凡相等,但同套件其余组已证函数非空输出,风险可接受 | **通过** | +| **fv8 画布自适应** | `level_map_info_fog_test.gd:169`(规模实测 maxl=10/maxslot=6)、`:175`(min.x 1334.5 ≥ 1334.5)、`:177`(min.y 449.0 ≥ 449.0)、`:185`(全节点方块在画布内)、`:192/195`(双 Boss 描边、远处战斗 32 入雾);`ui_input_probe.gd:146-149`(常数锚引屏内常量)+⑬ | `canvas_probe()`/`layout_probe()`+真机探针 ⑬。⑬ 的 BFS 独立推导、常数同源 | 公式两侧独立推导(布局实测反解 maxl/maxslot,非用 `_fit_canvas` 自己的层表);M9 实测 2 条红(§3)。观察项:`layout_probe` 的 `maxl` 由 `_content_size` 居中后的坐标反解,若宽度公式少项,反解仍正确——正是 M9 能红的原因,无缺陷 | **通过** | +| **fv9 显隐接线(雾不扩可点域)** | `ui_input_probe.gd:76-91`(真机 `push_input` 点击雾下非候选节点,`route_chosen` 零发射,尾缀 :89) | 真机 push_input+真实 `_candidate_at` 命中面;判据绑 `route_chosen` 信号面(可观测提交面),未绑兜底终态 | M10 实测红,原文 `提交=[3] visible=true 选路态=true`(§3);这是本卡最强的一条 | **通过** | + +**§7.1 汇总**:fv1、fv3、fv4、fv5、fv6、fv7、fv8、fv9 通过(fv3 含 1 条观察项);**fv2 判缺漏**——不是「断言不存在」,而是卡片该行声明的「读绘制面/接线层验收」用当前 `display_probe()` 实现不成立,且实测接线坏不红。 + +## 3. 变异复现记录(自跑) + +四张 T 卡各复现一条(T1→M1、T2→M2、T3→M9、T4→M10),全部「注入→最窄闸见红→`git checkout --` 还原→复跑绿」,另加一条审计自设的接线坏注入 W1。**无复现不一致**:四条红的位置与报告 §5.4 所写逐字一致。 + +| # | T 卡 | 注入内容(精确位置) | 红断言原文(自跑) | 还原后读数 | +|---|---|---|---|---| +| **M1** | T1 | `game/core/level_runner.gd:433`:`absi(...) <= n` → `< n` | `[FAIL] 层距 N 内可见:锚层 1 取节点 4(层距 2)〔fv1〕`;`[FAIL] 单圈只按锚层取窗:已过近端节点 4(层距 2)仍可见〔fv7〕`;`[FAIL] 单链退化形态:入口锚层 0,下标 2 可见〔fv1〕`;`[FAIL] N 读表:N=3 时节点 5(层距 3)入窗〔fv6〕`;`4/26 项失败`,退出码 1 | `git status --short` 空;C1 复跑 `all green (26 checks)` 退出码 0 | +| **M2** | T2 | `game/presentation/views/level_map_screen.gd` `node_display`:`"draw_icon": type_shown` → `true` | `[FAIL] 雾下节点:图标与类型文字均不绘制(显隐语义,非灰化)〔fv2〕`;`[FAIL] Boss 豁免不扩散:雾下非 Boss 仍隐类型〔fv3〕`;`[FAIL] 大图呈现:入口停点远处战斗节点(编号 32,层 6)类型入雾〔fv8〕`;`3/19 项失败`,退出码 1 | `git status --short` 空;C2 复跑 `all green (19 checks)` 退出码 0 | +| **M9** | T3 | `level_map_screen.gd:_fit_canvas` 宽公式去 `+ NODE_HALF.x`(`LAYER_BASE_X + maxl*LAYER_STEP_X + 8.0`) | `[FAIL] 大图拟合:画布 min.x 覆盖最深节点(1298.0 ≥ 1334.5)〔fv8〕`;`[FAIL] 大图布局:全节点方块落在拟合画布内(origin=(0.0,0.0) size=(1298.0,449.0))〔fv8〕`;`2/19 项失败`,退出码 1 | `git status --short` 空;C2 复跑 `all green (19 checks)` 退出码 0 | +| **M10** | T4 | `level_map_screen.gd:_candidate_at`:`for nid in _cand_ids` 改为遍历全节点 id(先构 `_all_ids`) | `[ui_input_probe] FAIL — case⑱ 雾下节点点击产生提交(提交=[3] visible=true 选路态=true——雾不扩大可点域)〔fv9〕`,退出码 1 | `git status --short` 空;C4 复跑 `PASS` 退出码 0 | +| **W1**(审计自设,报告未列) | T2 接线层 | `_draw_graph` 两处条件 `if bool(disp["draw_icon"]):`/`if bool(disp["draw_label"]):` 均改 `if true:`(模拟「决策面正确、接线坏」) | **不红**:C2 `all green (19 checks)` 退出码 0;C4 `PASS` 退出码 0 | `git status --short` 空;C2 复跑 `all green (19 checks)` 退出码 0 | + +W1 的结论:本卡唯一「防止接线坏」的声明(§7.1 fv2 真实入口格)在端到端两层闸上都不成立——这正是卡片要求「真机 `display_probe()` 读绘制面」的动机,而 `display_probe()` 只是决策面的第二次求值。 + +## 4. 测试削弱检查结论 + +| 检查面 | 结论 | +|---|---| +| 删除既有断言 | **无**。`git diff 43c0f3c1..HEAD -- game/smoke game/tests` 的全部删除行只有 3 行,均为 `ui_input_probe.gd` 的画布常数锚注释+`need_x2` 手抄公式行,属卡片 §7.1「旧断言预声明」第①类允许项(改为引 `LevelMapScreen.LAYER_BASE_X/LAYER_STEP_X/NODE_HALF`,判据与断言语义未变)。其余冒烟/探针文件只增不删 | +| 放宽阈值 | **无**。冒烟新增 5 条全为新判据;⑬ 容差 `-0.5` 未动;⑱ 无「非空即过」类宽松判据 | +| 缩小扫描范围 | **无**。新两套件扫描全节点;大图夹具 63 节点全量比对 | +| 真实入口换下层直调 | **fv2 一项成立**(§2、§6 D1):`display_probe` 是决策面重算,不是绘制面读取。其余各行入口与卡片声明一致 | +| 注释掉断言 | **无** | +| 越出卡片声明的削弱 | **无**(除 fv2 的入口口径问题外) | + +观察项(非削弱,但影响判据强度,建议回炉 T2 时一并处理): +1. ⑱ 的「雾下图标不出示」判据自指:fog_id 按 `not disp[vnid]["draw_icon"]` 选出,再断言 `not draw_icon`,该半句恒真;有效的是「文字通道不出示」半句与「雾面节点存在」的存在性(M2 时因选不到 fog 节点而走 `case⑱ 雾面节点选取失败` 分支变红)。建议改为选区与断言解耦(如按层距/候选集独立选雾节点)。 +2. fv3 双 Boss 夹具的 `type_vis` 是手写 `[1,7,9]`,只证「决策面对两个 Boss 都亮」,未从 `type_visible_ids()` 推导「两端点均在集」;核心多 Boss 成员性由单 Boss 用例覆盖同一分支,风险低。 +3. C4 的 PASS 文案仍列到 ⑰ 为止(「十八断言」按 18 个 case 数),未点名 ⑱;报告 §5.2 称「探针⑱ 5 项断言」,实现实为 4 个 `_pre_fail` 判据+1 个选区前置。均为措辞/计数瑕疵,不影响判据有效性。 + +## 5. 报告 / 代拍板 / 反哺点相符性 + +### 5.1 实现报告 §5.3 六条偏差逐条核对 + +| 偏差 | 核对结果 | +|---|---| +| 1 大图夹具 12 BFS 层→布局 11 层(实测 maxl=10/maxslot=6) | **如实**(C2 打印「实测最大层 10/最大槽 6」;夹具源码 `tiers := 12`+跨层跳边) | +| 2 ⑱ 零提交判据改绑 `route_chosen` | **如实**(M10 红在信号面;旧终态判据被屏态/运行器兜底吸收的解释与 M10 输出 `visible=true 选路态=true` 一致)。属卡片 §8 L1 写法自救,已申报 | +| 3 类型闸 4 处 R1(`6f04f29b` 归零) | **如实**(旧 `static_scan.log` 逐行列 4 处 R1;C6/C7 现为 PASS) | +| 4 常数锚行号 `:98-99` → `:142-145` | **如实**(现文件 `:146-149` 为注释+公式;卡片原行号已 T0 订正) | +| 5 画布下限 620×256 vs 冻结文档 620×240 | **如实**(`level_map_screen.gd:303`=`Vector2(620, 256)`;`design/gdd/ux/level-map.md:68` 写 620×240;反哺点 ⑧ 已登记) | +| 6 命中面 `_candidate_at` 零改动 | **如实**(diff 该函数零 hunk;M10 是我注入才改) | + +**未申报的偏差 1 条**:§5.2 fv2 行写「真机 `display_probe()` 读绘制面」,实际 `display_probe()` 只是 `node_display` 快照重算,`_draw_graph` 接线零断言(W1 证明)。报告 §5.3 未列此偏差。 + +**报告 §5.7 与回执不符 1 条**:报告表 09 行写「1 → 0」,读作最终已绿;但 `receipt.json` 的 `commit=4ef154b9`、`09 exit=1`、`all_green=false`,且该回执早于 `6f04f29b`(修 09 的提交)与分支尖。报告末句「最终合并回执见本批合并提交信息(起跑 commit=分支尖)」是未来义务,不是已落证据;而 §5.1 已把 T4 标「完成」。**「批末代码域闸全绿」目前只有窄验证(audit_type_hints PASS+两套件+探针),没有分支尖的 `run_all_gates.sh --code` 回执。** + +(补充事实:C8 的 `changed_vs_baseline` 是「09 红项清单变化」。存量基线 `baseline.json` 里 09 本就是 exit=1 的存量红;本批先新增 4 处 R1 被判「清单变化」,修复后应转绿。所以问题不在代码现状,而在回执证据未随修复重生成。) + +### 5.2 代拍板六条(卡④)白名单核对 + +| # | 白名单条目 | 核对结果 | +|---|---|---| +| 1 | provisional N=2 | **合规**:`level.tres` 挂 2;`validate` 只校验 int≥1;代码零硬编码 N(`level_runner.gd:429` 读 `db.level.info_visible_layers`);C1 证明改表值窗随动 | +| 2 | API/探针命名 | **合规**:`info_visible_layers`/`type_visible_ids`/`node_display`/`display_probe` 均无 `fog_` 前缀。`level_map_info_fog_test.gd` 及 `_test_fog_hides_type` 属卡片 T2 指定的测试文件名/测试内私有函数,不是跨层 API | +| 3 | 雾下呈现落点 | **合规**:编号、方块、连线绘制零改动(生产 diff 删除行只有图标/文字块重排与条件切换,色值/线宽/形制零变);C5 冒烟 1211 项、C6 渲染相关静态面全绿 | +| 4 | 大图夹具规模 | **合规**:12 BFS 层压成 11 层×6 槽、含双 Boss、含跨层跳边(1→3 层目标),满足 ≥10×≥5 | +| 5 | 常数锚同源化 | **合规**:探针直接引 `LevelMapScreen` 导出常量,无手抄副本 | +| 6 | 提交切分/分支名 | **合规**:`iter34-fog`,三笔均在 `game/`;报告/产出物在 `reports/` | + +白名单外自决:**未发现**(W1 揭示的是验收口径缺失,不是实现自决)。 + +### 5.3 反哺点 ⑥–⑮ 对卡片 §5 的覆盖 + +| 卡片 §5 同步面/呈报项 | 反哺点 | 结论 | +|---|---|---| +| 关卡节点地图(N 状态行+可见性语义) | ⑥+⑩ | 覆盖 | +| 关卡节点图生成(屏预算行+下一跳兑现标注) | ⑦ | 覆盖 | +| level-map(画布公式漂移+Information Hierarchy+**雾的正式视觉规格呈报项**) | ⑧(含呈报项,明确「禁本批落文」) | 覆盖 | +| 术语表(信息迷雾 vs 视野迷雾) | ⑨ | 覆盖 | +| 待办清单 | ④+⑪ | 覆盖 | +| roadmap | ⑤+⑫ | 覆盖 | +| **超预算图升级路径(呈报项)** | ⑬ | 覆盖 | +| 实机目检三项登记 | ⑮(审计期新增提交 `6757c21e`) | 覆盖 | +| 方法论反哺 | ⑭ | 额外,合理 | + +### 5.4 契约与代码纪律 + +| 检查 | 结论 | +|---|---| +| 契约 1/2(三层语义、Boss/终点/候选豁免) | 未破:结构通道全绘;类型由 `type_vis` 决定;Boss 在核心与决策面双豁免;候选追加进核心集 | +| 契约 3(N 只进数据表) | 未破:唯一字面量 2 是 `level_table.gd:8` 的 schema 默认值(卡片 T1 明确要求),运行期经 `db.level` 读 | +| 契约 4(显隐、零新画法) | 未破:无灰化/雾纹理/新色值/线宽变化 | +| 契约 5(单圈零记忆) | 未破:实现不读 `visited`;C1 单圈组绿 | +| 契约 6(不动冻结件/禁缩放滚动) | 未破:`_fit_canvas` 公式零改动;下限 620×256 是改前既有值(卡片 §5.3-5) | +| 契约 7(呈现只读) | 未破:呈现层不重算层距,只消费 `type_visible_ids()`;`node_display` 的 Boss 判定属卡片 T2 指定的决策面语义 | +| 契约 8 / §8 L3 | 未破:无生成器/行进/敌人表/校验放宽改动;`type_visible_ids` 无状态;候选未被雾遮 | +| core 零 UI 依赖 | 通过:`level_runner.gd` 仅注释提及 `LevelMapScreen`,无 UI 类型引用 | +| 数值零字面量 / RNG | 通过:N 读表;新代码无 `randomize()`、无自建 RNG | +| 行尾/类型闸 | 通过:核心 `.gd` 无 CRLF;`audit_type_hints` PASS(C7) | +| 头注/变更记录禁令 | 通过:新增文件头只述职责;无版本历史节;`git diff --check` 仅 3 个报告文件「EOF 空行」提示,非代码问题 | + +## 6. 总判定:**不通过** + +代码现状的自跑读数全部为绿(C1–C7),未发现测试削弱、契约破坏、越权自决或数值/RNG 违规。但按卡片 §7.1 与 T4/T5 的验收口径,有 **2 条缺漏**必须回炉后重审: + +### D1(回炉 T2)fv2「接线层验收」未成立,且卡片 T2 与 §7.1 口径冲突 + +- **缺什么**:证明 `_draw_graph` 真的消费 `node_display` 输出的断言。当前 `display_probe()`(`level_map_screen.gd:536-547`)逐节点重新调用 `node_display`,与绘制路径是两次独立求值;测试与探针都只读这个快照。 +- **在哪**:`level_map_info_fog_test.gd:187-195`(大图 `display_probe`)、`ui_input_probe.gd:46-68`(⑱ 前四段);证据=W1 注入(`_draw_graph` 两处条件改 `if true`)后 C2 `19 checks` 全绿、C4 PASS。 +- **口径冲突**:卡片 T2 把 `display_probe()` 定义为「id → `node_display` 快照」,而 §7.1 fv2 要求它「读绘制面(接线层验收)」。实现按 T2 落地,§7.1 判据不可能由该接口兑现。这属 spec-AC 不一致,需用户裁:要么 T2 改接口/补一层「绘制输出可观测」的探针(例如自绘记录器:给 canvas 包一层记录 draw_rect/draw_texture_rect/draw_string 的测试桩,或离屏渲染取雾节点区域像素),要么由用户修改 §7.1 fv2 的验收口径(实现批不得自决)。无论走哪条,当前状态下 fv2 的撤销证明只覆盖决策函数,不覆盖接线。 +- **附带**:⑱ 的雾下图标判据自指(§4 观察项 1),回炉时一并解除自指。 + +### D2(回炉 T4/T5)批末代码域闸无分支尖绿回执 + +- **缺什么**:与分支尖同 commit 的 `tools/run_all_gates.sh --code` 全绿回执。 +- **在哪**:`game/tests/.gates/receipt.json`——`commit=4ef154b9`、`09 静态扫描 exit="1"`、`all_green=false`;`6f04f29b`(修 09)与 `477fd0f5` 之后未重跑。实现报告 §5.1 却把 T4 标「完成」、§5.7 表写「09 1 → 0」。报告 §5.6 自己也写明「合并前回执须与分支尖同 commit」。 +- **处置**:在分支尖重跑 `tools/run_all_gates.sh --code` 落新回执(`commit` 字段=分支尖、`all_green: true`),再交复审;不得用窄验证替代。我的 C1–C7 已证明重跑应能绿,但回执是合并闸的强制证据,不能由审计代跑(会覆写既有闸产物)。 + +### 非阻塞观察项(不单独构成缺漏,建议随 D1/D2 一并处理) + +1. ⑱ 雾下图标判据自指(§4-1)。 +2. fv3 双 Boss 夹具手写 `type_vis`,多终点核心成员性无独立推导(§4-2)。 +3. 探针 PASS 文案未列 ⑱,报告「5 项断言」与 4 个判据不符(§4-3)。 + +**合并前置**:D1、D2 关闭并复审通过前,本卡不得合并(卡片 T5、§7.1 末段)。 diff --git a/reports/iter34-卡④-验收审计-复审-2026-10-22.md b/reports/iter34-卡④-验收审计-复审-2026-10-22.md new file mode 100644 index 00000000..b73a4e28 --- /dev/null +++ b/reports/iter34-卡④-验收审计-复审-2026-10-22.md @@ -0,0 +1,75 @@ +# 迭代 34 卡④(信息迷雾可见性批)独立验收审计·第二轮复审报告(2026-10-22) + +> 审计员:第二轮独立复审子代理,未参与实现、未参与第一轮审计。职责=复核甲案是否真关掉 D1、且无新问题。 +> 全程只读源码/测试/产出物+自跑最窄命令取证;除本报告外未写任何文件(注入变异均在跑完后 `git checkout --` 还原)。 +> 被审对象:`D:\code\g-plan2-iter34-fog`,分支 `iter34-fog`,被审 HEAD `de9cc102`(工作树开工/收尾 `git status --short` 均空)。 +> 被审改动:甲案两笔——`1970e0ef`(代码:`level_map_screen.gd`+`ui_input_probe.gd` 两文件)、`de9cc102`(账面:仅 `reports/` 三件)。审计起点=第一轮报告 `reports/iter34-卡④-验收审计-2026-10-22.md`(D1/D2 两条缺漏)。 +> 口径:不采信实现报告 §5/§六 的「已全绿/已闭合」自述,凡结论必附自跑读数或文件:行。 + +## 1. 自跑命令与读数原文 + +| # | 命令(工作目录=该工作树 `game/`) | 读数原文 | 退出码 | +|---|---|---|---| +| R1 | `…console.exe --headless --path . --script res://tests/unit/core/level_map_type_vis_test.gd` | `[level_map_type_vis_test] all green (26 checks)` | 0 | +| R2 | `…--script res://tests/unit/presentation/level_map_info_fog_test.gd` | `[level_map_info_fog_test] all green (19 checks)` | 0 | +| R3 | `…console.exe --headless --path . res://smoke/ui_input_probe.tscn` | `[ui_input_probe] PASS — …⑰未开格类型微光…⑱信息迷雾绘制接线(display_probe 读实际绘制记录)增)` | 0 | +| R4 | `…--script res://tests/run_all.gd` | `[run_all] unit/contract suite entry — 40 suites`;`[run_all] all green (40 suites)` | 0 | +| R5 | `PYTHONIOENCODING=utf-8 python tools/run_smoke.py` | `[run_smoke] 摘要: 冒烟完成:1211 项检查,0 项失败`;`[run_smoke] PASS(退出码 / 摘要失败数 / 脚本错误扫描 三闸全过)`;可见性断言逐条 `[PASS]`(含〔fv6〕N 读表=2、〔fv1〕层距 3/4 节点 7/8 入雾) | 0 | + +> 说明:R4/R5 只读验证单元/冒烟计数(run_all.gd 只写 `.gates/run_all_NN.log`,run_smoke.py 不写 `.gates`),**均未触碰 `receipt.json`**;未跑 `run_all_gates.sh`。R3 的 PASS 文案已点名 ⑱(第一轮观察项 3 已修)。 + +## 2. 四项逐条结论 + +### 项 1:D1 是否真关——**通过** + +- `display_probe()`(`level_map_screen.gd:546-547`)现只 `return _drawn_disp`,**不再自行调用 `node_display`**(全仓 `_drawn_disp` 仅 4 处:声明 :51、逐帧 clear :444、绘制点赋值 :517、返回 :547——无第二求值面)。 +- `_draw_graph` 在**实际绘制点**写记录:`boss_border`(:488,在描边画完之后)、`draw_icon`(:495,在 `if bool(disp["draw_icon"]):` 块内)、`draw_label`+`label_text`(:512-513,在 `if bool(disp["draw_label"]):` 块内),末 `_drawn_disp[nid] = rec`(:517)。记录随每帧 clear 重建(:444),只反映实际发生的绘制。 + +**W1 复现(第一轮判「不红」的那一发)**——把 :494/:511 两处 `if bool(disp["draw_icon"]|["draw_label"]):` 均改 `if true:`(`git diff` 净 2 行、无其它改动): + +- 呈现单元套件红:`ERROR: [FAIL] 大图呈现:入口停点远处战斗节点(编号 32,层 6)类型入雾〔fv8〕`(`level_map_info_fog_test.gd:195`)+`[level_map_info_fog_test] 1/19 项失败`,退出码 **1**。 +- 输入探针红:`[ui_input_probe] FAIL — case⑱ 雾下节点类型仍出示(图标=true 文字=true——范围外只见结构)〔fv2〕`,退出码 **1**。 +- `git checkout -- game/presentation/views/level_map_screen.gd` → `git status --short` 空 → 复跑 R2 `all green (19 checks)` 退出码 0。 + +**第一轮 W1=两入口全绿;本次两入口全红**——D1 关掉。**且两红原文与实现报告 §5.4 M11 行逐字一致**(§5.2 fv2 行「M11 绘制处绕开决策面 → 单元与探针双红(实测)」相符)。 + +**第二发新变异(验记录面不是摆设)**——绘制处条件保持原样(雾下节点实际不画图标),仅在 `_drawn_disp[nid] = rec` 前插一行无条件 `rec["draw_icon"] = true`(记录谎称画了图标、实际没画): + +- 呈现单元红:同〔fv8〕:195 一条,退出码 1;探针红:`case⑱ 雾下节点类型仍出示(图标=true 文字=false——范围外只见结构)〔fv2〕`(文字通道未被污染故=false,证断言确实逐值读记录),退出码 1。 +- 还原后 R2 `all green (19 checks)`。→ 记录的值是承重的(非仅存在性检查),断言确实消费绘制记录内容。 + +### 项 2:⑱ 自指是否解除——**通过** + +`ui_input_probe.gd` case⑱(:44-73)现况: + +- 选区=`fog_id`:`vnid != cur_id and not cands.has(vnid) and not vis.has(vnid)`(:56)——只读核心面 `vis = runner.type_visible_ids()`(:47)、`cur_id`(:48)、`cands`(:43),**不读 `disp`**(源码注释 :57 亦写明「选区只按核心可见集判,不读绘制记录」)。 +- 断言=`if bool(disp[fog_id]["draw_icon"]) or bool(disp[fog_id]["draw_label"])`(:64)——读呈现绘制记录。 +- Boss 选区按 `type=="boss"` 读节点数据(:54)、候选选区按 `cands[0]`(核心),断言分别读 `disp[boss_id]["boss_border"]`(:68)与 `disp[cands[0]]["draw_icon"]`(:71)。 + +选区全部来自核心/节点数据面,断言全部来自呈现绘制记录面——**两个不同面**,非第一轮「按 `not disp[...]["draw_icon"]` 选、再断言其 `not draw_icon`」的恒真自指。W1 变异能红即实证该断言不再恒真。→ 自指解除。 + +### 项 3:有无新削弱或新越权——**通过** + +- **删断言/放宽阈值/缩小扫描面/真实入口降级**:`git diff 43c0f3c1..HEAD -- game/` 的删除非注释行仅两类——① `level_map_screen.gd` 原「无条件绘制图标/字形/类型文字」块(改为经 `node_display` 决策面消费+写记录,属卡片 T2 本体,非删断言);② `ui_input_probe.gd` 旧手抄公式 `need_x2 = 70.0 + maxl2*122.0 + 26.0 + 8.0` 与旧 PASS 文案行。**无一条断言被删**。旧 `26.0`→新 `LevelMapScreen.NODE_HALF.x`(36.5,:153-154),阈值抬高 10.5px=**变严**(第一轮 F11 记探针低报仍绿,本次同源化后更紧),容差 `-0.5` 未动(:156)。真实入口 fv2 由「决策面快照」升为「绘制记录」=变强不变弱。 +- **`_drawn_disp` 是否只读观测面**:全仓仅 `level_map_screen.gd` 内 4 处引用(见项 1),**无新 `draw_rect/draw_texture_rect/draw_string`**、无色值/线宽/缩放滚动改动、不写存档、不引入持久状态语义;`clear`+记录赋值=纯只读观测面(仿 `canvas_probe`/`layout_probe` 先例)。甲案两文件之外零触达 core/data/smoke_all。 +- **早期返回空记录会否致平凡通过**:`_draw_graph` 早返回(`_runner == null or not visible`,:442)时记录不清空不重建。已核**无平凡通过面**——① 呈现单元 :192 先断言 `disp.has(boss_ids[0]) and disp.has(boss_ids[1])`,空记录即判红(且 :195 对缺失键 `disp[32]` 取 null 会报错、同样不放过),当前 19 项全绿反证记录已填充;② 探针 :59 前置判据 `not disp.has(fog_id) or not disp.has(boss_id) or not disp.has(int(cands[0]))` → `_pre_fail`(甲案新增),空记录走「记录缺位」红分支。两入口都对空记录设了拦截。 +- **代拍板第 7 行相符性**:卡片 §9 六条白名单(:216-221)无一条覆盖「把 `display_probe` 从 `node_display` 快照改为绘制记录观测面」这一接线层验收手段,属白名单外;代拍板第 7 行(`iter34-代拍板-2026-10-22.md:32`)如实备案「呈报甲/乙/丙三案并推荐甲,按甲落地:`_draw_graph` 绘制点记录本帧事实、`display_probe()` 改读该记录」,约束核验「改动集中 `1970e0ef` 一笔可回退、未触冻结视觉件、新增面为只读成员与记录赋值」——与代码实际做法、与本复审实测逐项一致。非静默越权。 + +### 项 4:D2 与账面相符——**通过** + +- `receipt.json`:`commit = de9cc102`=HEAD(分支尖)✓;`all_green = true` ✓;`changed_vs_baseline = []` ✓;闸项 01/02/03/04/05/09/10 全 `exit 0 / green`(09 静态扫描由「基线红→现绿·改善」,见 `inherited_red`);11 对拍按档位规则 NEED-APPROVAL(数值零变更,不发起,合规)。 +- 计数由自跑独立佐证:02 单元 **40 套件**(R4 `all green (40 suites)`)、01 冒烟 **1211 项 0 失败**(R5 摘要原文)——与回执 01/02 的 `exit 0` 相互印证。 +- 实现报告:§5.1 提交列表含 `1970e0ef`(:159「审计 D1 回炉 T2」)✓;§5.2 fv2 行与 §5.4 M11 行的红断言原文与项 1 实测逐字对得上 ✓;反哺点 ⑯(:100-104)写清「卡片 T2 :139 接口描述=`id → node_display 快照`滞后,与 §7.1 fv2『读绘制面』矛盾,须由**写卡段订正**、实现期不改卡片」✓。 + +## 3. 第一轮两条缺漏的闭合判定 + +- **D1(fv2 接线层验收未成立/⑱ 自指)=已闭合**。`display_probe()` 改读 `_draw_graph` 实际绘制记录、不再二次求值;第一轮判「不红」的 W1 注入(两处绘制条件 `→ if true`)本次在**呈现单元〔fv8〕与输入探针〔fv2〕两入口同时判红**,红原文与 M11 记录一致;⑱ 选区改按核心可见集/候选集/当前位独立选点、断言读绘制记录,自指解除;新变异(污染记录值)两入口亦红,证明记录值承重。 +- **D2(缺分支尖代码域全绿回执)=已闭合**。回执 `commit=de9cc102`=HEAD、`all_green=true`、`changed_vs_baseline=[]`,01/02/09 全绿,计数经自跑(1211 项 / 40 套件)独立印证。 + +## 4. 总判定:**通过(可合并)** + +甲案真关掉了 D1(W1 变异从「不红」变「双入口红」,实证接线层断言生效、记录非摆设、自指解除),D2 有分支尖全绿回执且与自跑读数相符。改动为只读观测面、零新画法/色值/线宽/缩放滚动/存档、无测试削弱、无静默越权(白名单外手段已备案并可整笔回退)。第一轮三条非阻塞观察项中,⑱ 自指(观察项 1)与 PASS 文案/计数(观察项 3)已随甲案修除;观察项 2(fv3 双 Boss 夹具手写 `type_vis`)按卡片 T2 指定写法保留、核心多 Boss 成员性由 `level_map_type_vis_test`〔fv3〕覆盖,维持非阻塞。 + +**唯一遗留(账面措辞,非阻塞、不构成缺漏)**:实现报告 §六 D2 行与 §5.7 行文字仍引用旧复跑 commit `8b9c6989`(甲案代码前的那次分支尖),而当前权威回执为 `de9cc102`;权威闸证据(`receipt.json`)本身正确、与 HEAD 一致、`all_green=true`,仅账面引用未随甲案后重跑刷新。合并前若再跑一次回执则会自然对齐;不影响 D2 闭合判定。 + +结论:卡④ D1/D2 两条缺漏均已闭合,无新削弱、无新越权,**通过,可合回 master**。 diff --git a/reports/iter34-反哺点-2026-10-22.md b/reports/iter34-反哺点-2026-10-22.md index 41ffda99..c20caf38 100644 --- a/reports/iter34-反哺点-2026-10-22.md +++ b/reports/iter34-反哺点-2026-10-22.md @@ -35,3 +35,70 @@ - 文件与位置:`roadmap.md` 迭代 34 行(两卡并列,本卡为卡③)与顶部「当前位」节 - 改成:卡③状态记为已实现;卡④(信息迷雾可见性)仍在同一迭代内推进;「当前位」按实际进度改写。 - 依据:迭代实现批的收尾义务(`iter` 技能第 5 步账面随批);实现批不写该文件,换代归批后收尾或用户发令。 + +## ⑥ 关卡节点地图.md:调参表 N 状态行与可见性语义注记 + +- 文件与位置:`design/gdd/systems/冒险循环/关卡节点地图.md` :172(调参表「信息迷雾可见层数」行)、:100(可见范围「邻近 N 层」节) +- 改成:调参表状态由「定值批落数据表」改为「已落数据表 provisional 2(`level.tres` `info_visible_layers`)」;:100 节补实现语义三句——层距=入口 BFS 层差、Boss/终点与当前候选双豁免、单圈零记忆(已过节点回雾)。 +- 依据:卡④ T1 已实现并在 `level_runner.type_visible_ids()` 钉死;`level_map_type_vis_test` 五组断言〔fv1〕〔fv3〕〔fv4〕〔fv7〕覆盖。 + +## ⑦ 关卡节点图生成.md:屏预算约束行与「下一跳必在可见范围」兑现标注 + +- 文件与位置:`design/gdd/systems/冒险循环/关卡节点图生成.md` :105–114(调参表)、:83(「下一跳必在可见范围」) +- 改成:调参表补一行「图包络 ≤ 屏预算(1280×800 下约 10 层×8 槽;超预算只许限参数档,禁改画布冻结公式)」;:83 该句标注「实现侧已兑现:候选集强制豁免类型可见性」。 +- 依据:卡④契约 6 与 G6 定值(参数限图);`type_visible_ids()` 候选豁免断言〔fv4〕。 + +## ⑧ level-map.md:画布公式漂移订正+类型可见性层级行 + +- 文件与位置:`design/gdd/ux/level-map.md` :68(节点图区「冻结公式」句) +- 改成:宽 70+层深×122+34 → 70+层深×122+44.5(`NODE_HALF.x`36.5+8);高 52+槽位×58+26+8 → 52+槽位×58+25+24(`NODE_HALF.y`25+标签行 24);下限 620×240 → 620×256(抬高容纳标签行)。Information Hierarchy 节补「类型可见性(邻近 N 层显隐)」层级行。 +- 依据:`level_map_screen.gd:289–304` 现行 `_fit_canvas` 公式与大图拟合断言〔fv8〕;下限定值 256 是当前代码与渲染闸全绿的实际约束。**注意**:本条只订正文档记数,不动冻结件本身(画布公式语义与像素档位不变),是否接受改写由用户裁。 +- **呈报项(禁本批落文)**:「雾的正式视觉规格」(雾纹理/灰化遮罩/明度层)属冻结件域,本卡只做显隐语义最小呈现,视觉规格待用户裁后另立。 + +## ⑨ 术语表.md:登记「信息迷雾」并区别于「视野迷雾」 + +- 文件与位置:`design/gdd/术语表.md` +- 改成:新增词条「信息迷雾」=关卡节点图上按邻近层数隐藏节点类型的信息可见性;与「视野迷雾」(战斗棋盘按英雄曼哈顿半径遮挡)并列,注明两系统同词根易混、代码标识符禁 `fog_` 前缀。 +- 依据:卡④契约与风险 3;实现侧 API 全用 `info_visible_layers`/`type_visible_ids`/`node_display`/`display_probe`。 + +## ⑩ 关卡节点地图.md:Boss 描边口径冲突择一 + +- 文件与位置:`design/gdd/systems/冒险循环/关卡节点地图.md` :67 +- 改成:Boss 描边口径「封印黑 #000000」与 `level-map.md`/现役代码「紫赤+深墨环」不一致,请文档批核对后择一(现役实现与视觉终稿样张 v1 为紫赤+深墨环)。 +- 依据:卡④ G8 定值(实现随现役代码+level-map.md);本批未动描边视觉。 + +## ⑪ 待办清单.md:卡④条目状态按停卡实况回写 + +- 文件与位置:`待办清单.md` 中卡④登记条目 +- 改成:**不得记为「已实现」**——实现与代码域全量闸已完成(回执 `commit=6f547c35`、`all_green=true`),但独立验收审计判不通过、缺漏 D1(fv2 接线层验收与卡片 T2 对 `display_probe` 的定义互相矛盾)待用户裁决,卡④ 停在审计门。状态写「实现完成·审计不通过待裁(迭代 34)」,缺口记:「`display_probe` 只做决策面二次求值,`_draw_graph` 接线零断言;审计注入接线坏不红」;并写明实现报告路径。裁决后按用户选择推进(改验收接口 → 回炉 T2;改卡片 §7.1 口径 → 改卡后重交审计)。 +- 依据:`reports/iter34-卡④-验收审计-2026-10-22.md` §6 D1;五段流水线状态机(`doc-conventions` §12);实现批不写该文件,回写归批后收尾或用户发令。 + +## ⑫ roadmap.md:迭代 34 行状态刷新(卡③已完成、卡④停审计门) + +- 文件与位置:`roadmap.md` 迭代 34 行与顶部「当前位」节 +- 改成:卡③(敌人表深度带换代批)记为已实现并已合回;卡④(信息迷雾可见性批)记为「实现完成·独立验收审计不通过待裁」,迭代 34 不得标记完成;「当前位」按此实况改写。 +- 依据:迭代实现批收尾义务;实现批不写该文件,换代归批后收尾或用户发令。 + +## ⑬ 呈报项:超预算图策略升级路径 + +- 位置:无文档落点(用户在会话中裁) +- 内容:若后续定值批需要大于「约 10 层×8 槽」的完整可见图,两条路都动冻结件——等比缩放(动「20px 烘焙 1:1」「2/3/4px 折线」)或滚动视口(新交互范式)。本卡按 G6 走参数限图,两案均需用户裁。 +- 依据:卡④风险 2 与 G6 淘汰案。 + +## ⑭ test-conventions.md:验收断言应绑可观测提交面(方法论反哺) + +- 文件与位置:`design/engineering/test-conventions.md` §9.4(撤销/变异证明) +- 改成:补一句判据——端到端断言的判据应绑「被测改动特有的可观测提交面」(信号发射、事件、入参),不绑多层防御下的终态;否则单点变异可能被下游兜底吸收、终态不变而假绿。示例:探针「雾不扩可点域」首版只看屏/选路态终态,命中面放宽的变异仍绿;改绑 `route_chosen` 发射面后变异即红。 +- 依据:卡④ T4 实测(`提交=[3] visible=true 选路态=true`;终态与未点击一致)。 + +## ⑮ 体验验证清单.md:登记卡④三条实机目检项 + +- 文件与位置:`体验验证清单.md` +- 改成:登记三条待真人目检项(只登记、不阻塞)——①雾下可读性观感(未知类型节点是否一眼可辨为「未知」而非空节点);②多终点图 Boss 标记观感(双 Boss 是否都带描边且可辨);③大图完整可见观感(11 层×6 槽图是否完整落在屏内、编号不重叠)。判据与观测点见 `reports/iter34-实现报告-2026-10-22.md` §5.5。 +- 依据:卡④ T5「实机目检总清单三条」+iter 技能第 5 步账面随批;实现批禁写该清单,登记归文档批或用户发令。 + +## ⑯ 需求卡片:T2 的 `display_probe` 接口描述与 §7.1 fv2 口径一致化 + +- 文件与位置:`reports/需求卡片-信息迷雾可见性批-2026-10-21.md` :139(T2 第三条) +- 改成:「新增 `func display_probe() -> Dictionary`(id → `node_display` 快照,只读断言面,仿 `canvas_probe` 先例)」→「(id → 本帧实际绘制事实:图标/文字/描边/文案,由 `_draw_graph` 在实际绘制点写入,只读断言面)」,与 §7.1 fv2 真实入口格「真机 `display_probe()` 读绘制面(接线层验收)」对齐。 +- 依据:迭代 34 卡④ 独立验收审计 D1(`reports/iter34-卡④-验收审计-2026-10-22.md` §6);实现按 §7.1 验收面(甲案)落地,卡片 T2 的接口描述是滞后一侧,须由写卡段订正——实现期不改卡片。 diff --git a/reports/iter34-实现报告-2026-10-22.md b/reports/iter34-实现报告-2026-10-22.md index ee548d10..36811584 100644 --- a/reports/iter34-实现报告-2026-10-22.md +++ b/reports/iter34-实现报告-2026-10-22.md @@ -154,4 +154,125 @@ ## 五、卡④ 信息迷雾可见性批 -另行施工,完成后在本文追加对应章节。 +### 5.1 工作树与任务卡状态 + +工作树 `D:\code\g-plan2-iter34-fog`,分支 `iter34-fog`,起自卡③合并后的 master `43c0f3c1`。提交:`ed8e0f9f`(T1–T3 实现与单测/冒烟/探针)、`4ef154b9`(⑱ 零提交判据改绑信号面)、`6f04f29b`(类型闸归零+探针补雾下文字通道断言)、`1970e0ef`(审计 D1 回炉 T2:绘制记录观测面+⑱ 选区解耦)。 + +| T 卡 | 内容 | 状态 | +|---|---|---| +| T0 | 事实坐实与基线读数 | 完成(改前基线:冒烟 1206 项 0 失败/单元套件全绿/探针全绿) | +| T1 | N 落表+越域校验+`type_visible_ids()`+核心套件 | 完成(提交 `ed8e0f9f`) | +| T2 | `node_display` 决策面+`display_probe`+`_draw_graph` 接线+呈现套件 | 完成(提交 `ed8e0f9f`,类型闸修补 `6f04f29b`) | +| T3 | 探针常数锚同源化+大图夹具(11 层×6 槽) | 完成(提交 `ed8e0f9f`) | +| T4 | 冒烟断言+探针⑱+变异汇总+代码域全量闸 | 完成(见 §5.4、§5.7) | +| T5 | 三份产出物+独立验收审计+合并 | 审计两轮完成(首轮不通过→甲案回炉→复审通过),合并进行中 | + +### 5.2 实际改动—验收对照表 + +生产侧落点:`game/core/level_runner.gd`(`type_visible_ids()` :422–440、`_bfs_layers()` :443–462);`game/presentation/views/level_map_screen.gd`(`node_display()` :420–437、`_draw_graph` 消费 :478、`display_probe()` :536–547);`game/data/schemas/level_table.gd:8`(字段)、:23–24(越域校验);`game/data/tables/level.tres:7`(`info_visible_layers = 2`);`game/smoke/ui_input_probe.gd:142–145`(常数锚同源化)、:44–90(case⑱);`game/smoke/smoke_all.gd:4938–4943`(真图可见性断言组)。 + +| fv | 需求点(简) | 验收断言(文件:行) | 层级 | 真实入口 | 撤销/变异结果 | +|---|---|---|---|---|---| +| fv1 | 邻近 N 层见类型 | `level_map_type_vis_test.gd:121`、:122、:123(层距边界);:142、:143(入口锚层 0);:166、:167、:168(单链退化);`smoke_all.gd:4942`(真图) | 单元+集成 | `LevelRunner.type_visible_ids()` 直调;冒烟真图读面 | M1 `<=N`→`= 10`),规模不变、语义不变。 +2. **⑱ 零提交判据改绑信号面**:按卡片原写法(命中面放宽到全节点 → ⑱ 红)首轮实测仍绿——点击确实穿透并提交,但雾下非战斗节点让本屏保持激活、运行器候选校验又把非法编号拒掉,终态与未点击时相同。改绑 `route_chosen` 发射面后单点变异即红(`提交=[3]`)。属卡片 §8 L1「断言写法」自救范围;同时给⑱ 各断言补 fv2/fv3/fv4/fv9 尾缀。 +3. **类型闸新增红**:T4 首轮全量闸暴露新增 4 处 R1(`Dictionary` 未参数化 1 处、`Array` 未参数化 3 处),已参数化归零(`6f04f29b`);修复后 09 转绿(相对基线为改善)。 +4. **常数锚行号**:卡片写 `ui_input_probe.gd:98-99`,工作树实落点 :142–145(T0 复核行号),同源化方式=直接引 `LevelMapScreen` 导出常量,未新增副本。 +5. **画布下限漂移**:`_fit_canvas` 现行为 `max(Vector2(620, 256))`,`level-map.md:68` 冻结公式写「下限 620×240」——本卡不动冻结件,登记反哺点。 +6. **命中面零改动**:`_candidate_at`(:373–386)卡内要求零改动,实现保持候选集循环,fv9 的变异证明正落在此函数。 + +### 5.4 变异验证记录表(注入 → 最窄闸见红 → 还原) + +| # | 注入内容 | 最窄闸 | 红断言(原文摘要) | 还原确认 | +|---|---|---|---|---| +| M1 | `type_visible_ids` 层距判定 `<= n` 改 `< n` | `level_map_type_vis_test` | 层距 N 内可见:锚层 1 取节点 4(层距 2)〔fv1〕;单链退化形态:入口锚层 0,下标 2 可见〔fv1〕(连带 fv6/fv7 各 1 条) | `git checkout` 还原,26 项全绿 | +| M2 | `node_display` 的 `draw_icon` 强制 true | `level_map_info_fog_test` | 雾下节点:图标与类型文字均不绘制〔fv2〕;Boss 豁免不扩散〔fv3〕;大图远处节点入雾〔fv8〕 | 还原,19 项全绿 | +| M3 | 删 `type_visible_ids` 的 Boss 豁免分支 | `level_map_type_vis_test` | Boss 恒显:层距 6 的末位 Boss 仍在可见集〔fv3〕;单链编号空间=数组下标〔fv1〕 | 还原,26 项全绿 | +| M4 | 删 `node_display` 的 Boss 豁免(`type_shown` 只看可见集) | `level_map_info_fog_test` | Boss 豁免独立于可见集:不在 type_vis 的 Boss 仍出示类型〔fv3〕 | 还原,19 项全绿 | +| M5 | 删 `type_visible_ids` 候选豁免追加段 | `level_map_type_vis_test` | 候选恒显:跨层跳边候选(层距 N+1)仍在可见集〔fv4〕 | 还原,26 项全绿 | +| M6 | `label_text` 拼接 `node.get("enemy_id")` | `level_map_info_fog_test` | 决策面不消费内容键〔fv5〕;label_text 不含内容值(实际「战斗goblin_secret · 可选」)〔fv5〕 | 还原,19 项全绿 | +| M7a | 删 `LevelTable.validate` 越域判据(`if false`) | `level_map_type_vis_test` | N 越域 0/-1:validate 判 domain 错〔fv6〕 | 还原,26 项全绿 | +| M7b | `type_visible_ids` 的 N 硬编码 2(不读表) | `level_map_type_vis_test` | N 读表:N=1 时节点 4 出窗〔fv6〕;N=3 时节点 5 入窗〔fv6〕;N≥层高全可见〔fv6〕 | 还原,26 项全绿 | +| M8 | 可见集加 `visited_ids().has(nid)`(累积可见) | `level_map_type_vis_test` | 单圈回雾:已过远端节点 0(层距 5)不因 visited 豁免〔fv7〕;中距节点 2 不可见〔fv7〕 | 还原,26 项全绿 | +| M9 | `_fit_canvas` 宽公式去 `NODE_HALF.x` 项 | `level_map_info_fog_test` | 大图拟合:画布 min.x 覆盖最深节点(1298.0 ≥ 1334.5)〔fv8〕;全节点落在画布内〔fv8〕 | 还原,19 项全绿 | +| M10 | `_candidate_at` 命中面放宽到全节点 | `ui_input_probe`(⑱) | 雾下节点点击产生提交(提交=[3] visible=true 选路态=true)〔fv9〕 | `git checkout` 还原,探针 PASS | +| M11 | `_draw_graph` 图标/文字两处条件改 `if true`(绕开决策面=接线坏) | `level_map_info_fog_test`+`ui_input_probe`(⑱) | 大图呈现:入口停点远处战斗节点(编号 32,层 6)类型入雾〔fv8〕;case⑱ 雾下节点类型仍出示(图标=true 文字=true——范围外只见结构)〔fv2〕 | `git checkout` 还原,19 项全绿+探针 PASS | + +### 5.5 实机目检总清单(只登记,不阻塞合并) + +| # | 项目 | 观测点 | 状态 | +|---|---|---|---| +| 1 | 雾下可读性观感 | 未知类型房间是否一眼可辨为「未知」而非「漏画」;编号仍在、类型图标与文字不出现 | 待用户试玩 | +| 2 | 多终点 Boss 标记观感 | 双 Boss 图上两个终点的紫赤描边是否都清楚可辨 | 待用户试玩 | +| 3 | 大图完整可见观感 | 大图(11 层×6 槽)整幅在画布内、无节点被裁切或挤在一角 | 待用户试玩 | + +### 5.6 遗留读数义务 + +- 数值零变更(`info_visible_layers` 是呈现参数,不进战斗结算),本卡不发起模拟跑批(MC 档位规则)。 +- 合并后主区需 `--editor --quit` 重刷类名缓存,再跑一次代码域全量闸复跑,读数写入合并提交信息。 +- 合并前回执须与分支尖同 commit(回执 `commit` 字段)。 + +### 5.7 全量代码闸读数 + +T4 里程碑跑批 `tools/run_all_gates.sh --code`,起跑 commit `4ef154b9`: + +| 闸 | 退出码 | 对基线 | +|---|---|---| +| 01 冒烟全量 | 0 | 一致(1211 项检查 0 失败) | +| 02 单元套件 | 0 | 一致(40 套件,含本卡新增 2 套件) | +| 03 探针 --click | 0 | 一致 | +| 04 探针 --full | 0 | 一致 | +| 05 渲染闸 | 0 | 一致 | +| 09 静态扫描 | 1 → 0 | 新增 4 处 R1(容器未参数化)→ `6f04f29b` 修复后归零,由红转绿(改善) | +| 10 熵增闸 | 0 | 一致 | +| 11 回放确定性对拍 | NEED-APPROVAL | 本卡数值零变更,按档位规则不发起(需用户批准) | + +`6f04f29b` 修复后窄验证:`audit_type_hints` PASS;两套件 26+19 项全绿;`ui_input_probe` PASS。 + +D1 回炉后在分支尖复跑(`de9cc102`,含代码笔 `1970e0ef`):`all_green: true`、`changed_vs_baseline: []`、01 冒烟 1211 项 0 失败、02 单元 40 套件全绿、03/04 探针绿、05 渲染闸绿、09 静态扫描 0(对基线「改善」)、10 熵增闸 0、11 对拍按档位规则 NEED-APPROVAL。此前那一次复跑(`8b9c6989`,甲案代码之前)已被本次取代。 + +## 六、卡④ 独立验收审计 + +审计报告:`reports/iter34-卡④-验收审计-2026-10-22.md`(未参与实现的子代理执行;被审 HEAD `6757c21e`;自跑 40 套件、探针、冒烟 1211 项,另自设变异注入)。 + +**第一轮结论:不通过**——2 条缺漏,无测试削弱、无契约破坏、无越权自决、无数值/RNG 违规。M1/M2/M9/M10 四条变异由审计自跑,「红的位置与报告 §5.4 逐字一致,无复现不一致」。 + +| 缺漏 | 内容 | 本批处置 | +|---|---|---| +| D2 回炉 T4/T5 | 缺与分支尖同 commit 的 `--code` 全绿回执(旧回执停在 `4ef154b9`、09 红、`all_green=false`) | **已闭合**:在分支尖重跑全量代码域闸,回执 `commit=de9cc102`(甲案代码回炉之后重跑)、`all_green=true`、`changed_vs_baseline=[]`(见 §5.7) | +| D1 回炉 T2 | fv2「接线层验收」未成立:`display_probe()` 是逐节点重调 `node_display` 的快照,与 `_draw_graph` 的实际绘制是两次独立求值;审计注入「`_draw_graph` 两处条件改 `if true`」模拟接线坏,单元 19 项与探针仍全绿。审计判为**卡片 T2 对 `display_probe` 的定义与 §7.1 fv2「读绘制面」要求互相矛盾(spec-AC 不一致)** | **甲案回炉(提交 `1970e0ef`)**:呈报后用户指令「继续未完成的任务」,按呈报推荐案甲落地——`_draw_graph` 在描边/图标/文字三处实际绘制点写入本帧绘制事实(新只读成员 `_drawn_disp`),`display_probe()` 改读该记录、不再二次求值;探针⑱ 选区改按核心 `type_visible_ids()` 独立选雾节点,解除「按记录选、再断言记录」自指;单元大图组两处呈现断言随动读绘制记录。新增撤销证明 M11(绕开决策面直接画 → 单元〔fv8〕与探针〔fv2〕双红)。该项在 §9 白名单外,已在代拍板追认单第 7 行备案,可整笔回退。 | + +审计附三条非阻塞观察项,处置:第 1、3 条已随甲案修除,第 2 条按卡片 T2 指定写法保留(核心多 Boss 成员性由 `level_map_type_vis_test` 的〔fv3〕用例覆盖同一分支)—— + +1. 探针 ⑱ 的「雾下图标不出示」判据自指——`fog_id` 正是按 `not draw_icon` 选出再断言其 `not draw_icon`,该半句恒真;有效的是文字通道半句与存在性判定(M2 时因选不到雾节点走「选取失败」分支变红)。建议选区与断言解耦(按层距/候选集独立选雾节点)。 +2. fv3 双 Boss 夹具手写 `type_vis=[1,7,9]`,未从 `type_visible_ids()` 推导两端点均在集;核心多 Boss 成员性由单 Boss 用例覆盖同一分支,风险低。 +3. 探针 PASS 文案只列到 ⑰、未点名 ⑱,§5.2 原称「探针⑱ 5 项断言」实为 4 个 `_pre_fail` 判据+1 个选区前置。两处均已订正:⑱ 已列入 PASS 文案,计数改写为「4 判据+1 前置」。 + +### 6.1 第二轮复审结论(`reports/iter34-卡④-验收审计-复审-2026-10-22.md`) + +被审 HEAD `de9cc102`(=分支尖=回执 commit)。执行者仍是未参与实现的子代理,自跑呈现单元、核心单元、输入探针与冒烟读数,并自己复现第一轮判「不红」的那发注入。 + +| 项 | 结论 | 复审自跑证据 | +|---|---|---| +| 项 1 D1 是否真关 | **通过** | 注入 `_draw_graph` 图标/文字两处条件改 `if true` → 呈现单元〔fv8〕与探针〔fv2〕双红,探针红原文 `case⑱ 雾下节点类型仍出示(图标=true 文字=true——范围外只见结构)〔fv2〕`;另发「只污染记录值」的新变异同样双红,证记录承重 | +| 项 2 ⑱ 自指解除 | **通过** | 选区改按核心 `type_visible_ids()`/候选集/当前位独立选点,断言读绘制记录,两侧不同面 | +| 项 3 新削弱/新越权 | **通过** | 无删断言、无放宽阈值(探针常数锚 26→36.5 属变严,容差 -0.5 未动);新增面为只读观测,零新画法/色值/线宽/缩放滚动/存档;白名单外手段已在代拍板第 7 行备案、可一笔回退 | +| 项 4 D2 与账面相符 | **通过** | `receipt.json` `commit=de9cc102`、`all_green=true`、`changed_vs_baseline=[]`;自跑冒烟 1211 项 0 失败、单元 40 套件绿 | + +**闭合判定:D1 已闭合、D2 已闭合,总判定通过(可合并)。** 观察项 1 与 3 已随甲案修除;观察项 2 按卡片 T2 指定写法保留,核心多 Boss 成员性由 `level_map_type_vis_test`〔fv3〕覆盖,维持非阻塞。复审指出的一处账面措辞(§5.7 与 §六 仍引旧复跑 commit `8b9c6989`)已在本节随文订正。 + +**合并状态:两轮审计均通过,合回 master。** 卡③ 已在 `43c0f3c1` 合回。 +