iter34 卡④ T1-T3: 信息迷雾类型可见性——层距 N+Boss/候选豁免+单圈回雾(N 读表)+呈现显隐决策面+画布大图断言与探针常数同源

- level_table.gd: info_visible_layers 字段(int≥1)+validate 越域 domain 错;level.tres 挂 2
- level_runner.gd: type_visible_ids() 纯函数(入口 BFS 层差 ≤N ∪ Boss ∪ 候选,锚=当前节点层/入口停点 0;visited 不豁免)+_bfs_layers()
- level_map_screen.gd: node_display() 静态决策面+display_probe()+_draw_graph 图标/文字/Boss 描边三处改读决策面(结构通道零改动)
- 新单测 level_map_type_vis_test(26 断言)/level_map_info_fog_test(19 断言);run_all 注册两行
- smoke_all ⑬ 真图 L2 入口停点可见性组(+5 断言);ui_input_probe ⑬ 常数锚同源化+⑱ 雾不扩可点域

轻闸读数:冒烟 1211 项 0 失败(主区基线 1206,+5=新增组)/probe_gate --click 全绿
This commit is contained in:
DKChan committed 2026-10-22 05:52:11 +08:00
1 parent 43c0f3c118
commit ed8e0f9f3d
11 files changed
+571 -21

No files matched your search

@@ -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())
@@ -0,0 +1 @@
uid://brh5an0s8q6ss