Files
gplan-2/game/tests/unit/presentation/level_map_info_fog_test.gd
T
DKChan 9ee3bea194 refactor(迭代35卡②T2·二口+T3 S1/S2/S6/S8): 消费侧切源落定+断言面换代主体——① level_runner 图源换代(_generator 取代模板解析,_assign_generated_graph 读 level_gen_params 参数档产图并过 LevelTable.validate_graph,违规起关拒绝零兜底)+_entry_nodes 多入口首跳+_node_id 句柄单空间+enc node_depth 取节点 depth 值;② adventure_runner/main/save_manager 模板链路退役(first_script 参数与 template_pick 计算全删,断点 template_pick 键换生成图代差标记键 graph_gen_v2,缺键旧断点 resume 显式拒绝禁静默回放);③ level.tres 三行 templates 键清出(15 张模板数据出表,行级字段留,表尾注释改指参数档+生成器);④ sim/probe 随动(run_level/run_adventure 尾参退役、MC 出数字段 level_templates→level_maps 值=关卡#主种子、探针注释性质化、boss_depth 取节点 depth 键);⑤ 断言面换代:node_map_templates 宿主整段重写为生成图确定性套(纯函数复现/换种子变图/零计数器号位依赖/单一图源/种子 42 两开局同图/断点重生成同图/缺代差键拒绝/表无 templates/收束置位三条保留),breakpoint_resume 新增 P7 六断言(快照无已抽模板键、代差标记、停点句柄与已到序往返、续玩同图、旧断点拒绝、同种子重开局同图),S8 单测随动 first_script 键清退+断点夹具换代+meta 参数面 extra 键样换 treasure_unlocked(语义保持),S2 生成图取数面三宿主替换(level_active_nodes 走参数档+种子 42,消费点深度键换 depth,round_flow 死件 _boss_depth 随删),adventure_feats 覆盖义务段改真实解析链判据(敌侧落带≥请求深度+板侧命中+非空扫描面)、奖励节点段换「图侧含件+真实选路点选呈停兑付」双断言(旧单链必经形状前提随模板消亡,非放宽:经停即呈候选由真实 pick_next_node 入口证明)、商店余额段按每关恰一店投影改三关读数
2026-10-24 18:59:57 +08:00

192 lines
9.3 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,候选=首层首节点句柄 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.node_index = -1
r._candidates.assign(candidates)
return r
# ---- ① 范围外只见结构:雾下节点类型图标与文字均不绘制〔fv2〕----
func _test_fog_hides_type() -> void:
var node := {"id": 3, "type": "battle", "depth": 3, "next": []}
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({"id": 7, "type": "boss", "depth": 7, "next": []}, 7, type_vis, false, false)
var boss_b: Dictionary = ScreenScript.node_display({"id": 9, "type": "boss", "depth": 9, "next": []}, 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({"id": 9, "type": "boss", "depth": 9, "next": []}, 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({"id": 5, "type": "battle", "depth": 5, "next": []}, 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 := {"id": 3, "type": "battle", "depth": 3, "next": [4]}
var loaded := {"id": 3, "type": "battle", "depth": 3, "next": [4], "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 := {"id": 3, "type": "battle", "depth": 3, "next": [4]}
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 槽构造(id = 图内句柄 = 构造序号;depth 随构造序号严格前进——生成图连线铁律),
# 层 0 入口、末层双 Boss(id 61/62,句柄互异、next 恒空);含层 1 首节点→第 3 层节点的跨层长跳边。
# 跳边目标按 BFS 最短路径提前一层,其后继链随之逐层提前(目标并入第 2 层槽组)——布局落 11 层。
# 断言按布局实测取最大层/最大槽,画布公式与之同源。
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 = []
for l in tiers:
for i in layers[l].size():
var nid: int = layers[l][i]
var t := "battle"
var next_ids: Array = []
if l == tiers - 1:
t = "boss"
elif l == 0:
next_ids = layers[1].duplicate() # 入口扇出到全层(层宽 6)
else:
if i % 5 == 0:
t = "reward" if l % 2 == 0 else "rest"
var w2: int = layers[l + 1].size()
next_ids = [layers[l + 1][i % w2], layers[l + 1][(i + 1) % w2]]
nodes.append({"id": nid, "type": t, "depth": nid, "next": next_ids})
var jump_from: int = layers[1][0]
var jump_to: int = layers[3][2]
for n in nodes:
if int(n["id"]) == 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["id"]))
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()