Files
gplan-2/game/tests/integration/node_map_templates_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

142 lines
8.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/integration/node_map_templates_test.gd — L2 集成用例:关卡节点图生成·确定性承载
# 断言面 = 生成图确定性套(批 B 断言迁移自 smoke_all.gd,G12 施工组;
# 主会话裁决六整套集成不拆——跨四系统且走落盘)。
# 跨系统面 = RngService(纯派生流零计数器占用)×
# LevelRunner(start_level/start_level_external 受理·生成图装配·单一图源)×
# AdventureRunner(开局种子承载·断点写读后同图)×
# SaveManager+MetaProgression(首次冒险布尔位收束置位落盘——真实档写盘)。
# 与单元侧同入口同协议:extends SceneTree+Kit.check 主通道;--script 模式无 autoload,
# ConfigDB 显式实例化构造注入;表加载失败不中止(沿被迁断言面的源语义——诊断展开,红由具体断言落)。
# 运行:godot --headless --path game --script res://tests/integration/node_map_templates_test.gd
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd")
const SaveManagerScript := preload("res://core/save_manager.gd")
const AdventureRunnerScript := preload("res://core/adventure_runner.gd")
var _db
var _bp_path := "user://smoke_node_map_save.json"
func _init() -> void:
_db = load("res://core/config_db.gd").new()
# 表加载失败不中止(承 G2/G3/G4/G5 同款:表损坏的红落在具体断言上)
var load_ok: bool = _db.load_tables("res://data/tables", true)
if not load_ok:
push_error("[node_map_templates_test] 表加载返回失败——诊断如下(红由具体断言落)")
for d in _db.diagnostics():
if d.severity == "error" or d.severity == "fatal":
push_error(" [%s] %s: %s" % [d.severity, d.category, d.message])
_smoke_generated_map_determinism()
quit(Kit.finish("node_map_templates_test"))
# ---- 关卡节点图生成·确定性承载断言面 ----
func _smoke_generated_map_determinism() -> void:
# ① 纯函数复现:图 = f(参数档, 种子)——同档同种子两次产图逐位同图,换种子必变图
var gen = NodeMapGenerator.new(_fresh_rng(910001))
var params2: Dictionary = _db.level_gen_params_row(2)
var g_a: Dictionary = gen.generate(params2, 910002)
var g_b: Dictionary = gen.generate(params2, 910002)
var g_c: Dictionary = gen.generate(params2, 910003)
var nodes_a: Array = g_a.get("nodes", [])
Kit.check("同参数档同种子两次产图逐位同图(D2 执行边界)",
nodes_a == (g_b.get("nodes", []) as Array))
Kit.check("同参数档换种子即变图(图确为种子函数,非恒定输出)",
nodes_a != (g_c.get("nodes", []) as Array))
Kit.check("产图节点携图内唯一句柄 id 与自入口 depth(消费侧寻址面)",
_not_empty_graph(nodes_a) and _handles_unique(nodes_a))
# ② 消费侧单一图源:起关装配的图与生成器直取逐位一致(runner 内不存第二份图数据)
var runner1 = load("res://core/level_runner.gd").new(_db, _fresh_rng(910004))
Kit.check("单关起关受理(种子 910004——生成图直供,无兜底图)",
runner1.start_level(2, 910004))
Kit.check("起关生效图 = 生成器同档同种子直取逐位一致(消费侧无第二图源)",
runner1.level_row.get("nodes", []) == (gen.generate(params2, 910004) as Dictionary).get("nodes", []))
# ③ 产图零统一流消费(纯派生流不占计数器号位——断点在任意号位重生成同图的机制承载)
var rng_c = _fresh_rng(910005)
var counter_before: int = rng_c.counter()
var runner2 = load("res://core/level_runner.gd").new(_db, rng_c)
Kit.check("冒险模式起关受理(入口停点)", runner2.start_level_external(3,
{"hero_hp": 20, "gp": 0}, {}, Callable(self, "_smoke_rest_stub"), {}, true))
Kit.check("起关产图零计数器消费(同图不依赖取数号位)", rng_c.counter() == counter_before)
Kit.check("入口停点候选 = 生成图全部无入边节点(单入口时恰 1)",
(runner2.next_candidates() as Array).size() >= 1)
# ④ 开局种子承载:新档(首局)种子 42 两次开局逐位同图,且与生成器直取一致
DirAccess.remove_absolute(_bp_path) # 测试隔离——上轮落盘残留先删,断点面从零档起
var sv_w = SaveManagerScript.new(_bp_path)
var adr_x = AdventureRunnerScript.new(_db, _fresh_rng(42))
adr_x.set_save_manager(sv_w) # 先挂档再开局——停点快照由真实写手落盘
Kit.check("首局口径种子 42 开局受理(defer_entry 路径)", adr_x.start_adventure(42,
{"hero_override": {}, "treasure_unlocked": {}}))
var adr_y = AdventureRunnerScript.new(_db, _fresh_rng(42))
Kit.check("同种子第二次开局受理", adr_y.start_adventure(42,
{"hero_override": {}, "treasure_unlocked": {}}))
Kit.check("种子 42 两次开局图逐位同图(教学局恒同图——首局固定种子的确定性承载)",
adr_x.runner().nodes() == adr_y.runner().nodes())
Kit.check("开局图 = L1 参数档在种子 42 下的生成图(开局无隐式取数分叉)",
adr_x.runner().nodes() == (gen.generate(_db.level_gen_params_row(1), 42) as Dictionary).get("nodes", []))
# ⑤ 断点写→恢复→再产图同图(续玩按主种子+参数档重生成,断点不存图快照)
Kit.check("停点断点落盘在档", sv_w.has_breakpoint())
var bp: Dictionary = sv_w.breakpoint_snapshot()
var adr_z = AdventureRunnerScript.new(_db, load("res://core/rng_service.gd").new())
Kit.check("断点续玩受理(代差标记键在档)", adr_z.resume_adventure(bp,
{"hero_override": {}, "treasure_unlocked": {}}))
Kit.check("续玩重生成图与中断前逐位同图", adr_z.runner().nodes() == adr_x.runner().nodes())
var adr_old = AdventureRunnerScript.new(_db, load("res://core/rng_service.gd").new())
var bp_old: Dictionary = bp.duplicate(true)
bp_old.erase("graph_gen_v2") # 换代前旧断点形态(模板池时代无代差标记键)
Kit.check("缺生成图代差标记键的旧断点被显式拒绝(禁按新语义静默回放)",
adr_old.resume_adventure(bp_old, {}) == false)
# ⑥ 关卡表不再携带图数据(图源换代后 level 行只剩行级字段)
var tpl_free := true
for li in range(1, _db.level_count() + 1):
if (_db.level_row(li) as Dictionary).has("templates"):
tpl_free = false
Kit.check("level 表三行均无 templates 键(模板数据已清出)",
tpl_free and _db.level_count() == 3)
# ⑦ 收束置位:新档 false → settle 置 true 恰一次(幂等)
var sv = SaveManagerScript.new(_bp_path + ".flag.json")
sv.load_from_disk()
Kit.check("新档首次冒险布尔位 = false(默认域——首局恒走固定种子 42)",
sv.is_first_adventure_done() == false)
var mp = load("res://core/meta_progression.gd").new(_db, sv)
mp.settle_adventure({"levels_cleared": 1, "keystone_total": 10, "cleared": false, "result": "failed"})
Kit.check("冒险收束点置位(三结局同写——写入只在收束挂点)", sv.is_first_adventure_done() == true)
mp.settle_adventure({"levels_cleared": 0, "keystone_total": 0, "cleared": false, "result": "failed"})
Kit.check("置位幂等(重复收束不翻转)", sv.is_first_adventure_done() == true)
DirAccess.remove_absolute(_bp_path)
DirAccess.remove_absolute(_bp_path + ".flag.json")
# 图数据良构前置(防空集假绿——节点数 > 1、句柄唯一、depth 与 next 键在位)
func _not_empty_graph(nodes: Array) -> bool:
if nodes.size() < 2:
return false
for node in nodes:
var n := node as Dictionary
if not n.has("id") or not n.has("depth") or not n.has("next"):
return false
return true
func _handles_unique(nodes: Array) -> bool:
var seen := {}
for node in nodes:
var id: int = int((node as Dictionary).get("id", -1))
if id < 0 or seen.has(id):
return false
seen[id] = true
return true
func _fresh_rng(seed_val: int):
var r = load("res://core/rng_service.gd").new()
r.new_adventure(seed_val)
return r
# ---- 恢复执行体(LevelRunner 注入用——与 AdventureRunner.apply_rest 同式占位;
# smoke_all.gd helper 的宿主自持私有副本——源文件收尾整体删除,宿主自给自足)----
func _smoke_rest_stub(pct: int, hp: int, max_hp: int) -> int:
return mini(int(floor(float(max_hp) * float(pct) / 100.0)), max_hp - hp)