Files
gplan-2/game/tests/unit/presentation/level_map_info_fog_test.gd
T
DKChan ed8e0f9f3d 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 全绿
2026-10-22 05:52:11 +08:00

197 lines
9.1 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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()