108 lines
6.5 KiB
GDScript
108 lines
6.5 KiB
GDScript
# tests/integration/node_map_templates_test.gd — L2 集成用例:关卡节点图生成·模板池
|
||
# 断言面 = 模板池套(批 B 断言迁移自 smoke_all.gd `_smoke_node_map_templates`,G12 施工组;
|
||
# 主会话裁决六整套集成不拆——跨四系统且走落盘)。
|
||
# 跨系统面 = RngService(node_map 用途标签/抽签确定性/零消耗计账)×
|
||
# LevelRunner(start_level_external 受理/模板 0 回滚档)×
|
||
# AdventureRunner(first_script 全关零抽签/逐关恰一抽签)×
|
||
# 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")
|
||
|
||
var _db
|
||
|
||
|
||
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_node_map_templates()
|
||
quit(Kit.finish("node_map_templates_test"))
|
||
|
||
|
||
# ---- 关卡节点图生成·模板池冒烟(v0.4 §P0.6 断言面)----
|
||
func _smoke_node_map_templates() -> void:
|
||
# ① 扩签:node_map 入用途标签封闭词表(派生零改动——标签只进事件日志)
|
||
var svc = load("res://core/rng_service.gd").new()
|
||
Kit.check("node_map 标签入 rng_service 封闭词表(扩签通道=改代码+冒烟守卫)",
|
||
(svc.PURPOSE_TAGS as Array).has("node_map"))
|
||
# ② 池参数:三关各 5 模板、模板 0 存在(回滚档成员)
|
||
for li in range(1, 4):
|
||
var row := {}
|
||
for r in _db.level.rows:
|
||
if int(r.level_index) == li:
|
||
row = r
|
||
var tpls: Array = row.get("templates", [])
|
||
Kit.check("L%d 模板池 = 5 张(拍板点④——首版池规模)" % li, tpls.size() == 5)
|
||
Kit.check("L%d 模板 0 = 现行固定图(回滚档成员)" % li,
|
||
not tpls.is_empty() and String(tpls[0].get("display_name", "")).ends_with("(现行)"))
|
||
# ③ 强制取 0 = 模板 0 节点表逐位相等 + 零 scope 消费
|
||
var rng0 = load("res://core/rng_service.gd").new()
|
||
rng0.new_adventure(910001)
|
||
var runner0 = load("res://core/level_runner.gd").new(_db, rng0)
|
||
var c_before: int = rng0.counter()
|
||
Kit.check("强制 template_pick=0 起关受理", runner0.start_level_external(2, {"hero_hp": 20, "gp": 0}, {}, # start_level_external 签名收口(bonus 参数由 RunLedger 单类下传)
|
||
Callable(self, "_smoke_rest_stub"), {}, true, 0))
|
||
Kit.check("强制取模板零 RNG 消耗(零抽签——首次脚本语义)", rng0.counter() == c_before)
|
||
var tpl0: Dictionary = {}
|
||
for r in _db.level.rows:
|
||
if int(r.level_index) == 2:
|
||
tpl0 = r.get("templates", [])[0]
|
||
Kit.check("强制 0 的生效节点表 = 模板 0 逐位相等(回滚档成立)",
|
||
runner0.level_row.get("nodes", []) == tpl0.get("nodes", []))
|
||
# ④ 抽签确定性:同种子两实例同模板(同种子同操作序列 → 逐位同图)
|
||
var names := []
|
||
for i in range(2):
|
||
var rg = load("res://core/rng_service.gd").new()
|
||
rg.new_adventure(910002)
|
||
var rn = load("res://core/level_runner.gd").new(_db, rg)
|
||
rn.start_level_external(2, {"hero_hp": 20, "gp": 0}, {},
|
||
Callable(self, "_smoke_rest_stub"), {}, true, -1)
|
||
names.append(rn.active_template_name)
|
||
Kit.check("抽签确定性:同种子逐位同图(实测 %s)" % String(names[0]),
|
||
names[0] == names[1] and String(names[0]) != "")
|
||
# ⑤ N=1 池与现行固定图等价:单模板行过图校验(抽签取模 1 恒 0 = 回滚档)
|
||
var lt = load("res://data/schemas/level_table.gd").new()
|
||
lt.rows.assign([{"level_index": 9, "display_name": "t", "rest_heal_pct": 30,
|
||
"templates": [tpl0]}])
|
||
Kit.check("N=1 池表通过图校验(回滚档形态合法)", lt.validate().is_empty())
|
||
# ⑥ 首次脚本分支:first_script=true 全关模板 0;false = 抽签(scope 恰一差异)
|
||
var rngF = load("res://core/rng_service.gd").new()
|
||
rngF.new_adventure(910003)
|
||
var adrF = load("res://core/adventure_runner.gd").new(_db, rngF)
|
||
adrF.start_adventure(910003, {"first_script": true})
|
||
Kit.check("首次脚本模式:L1 取模板 0(全关零抽签——存档布尔位 false 语义)",
|
||
String(adrF.runner().active_template_name).ends_with("(现行)"))
|
||
var cF: int = rngF.counter()
|
||
var rngT = load("res://core/rng_service.gd").new()
|
||
rngT.new_adventure(910003)
|
||
var adrT = load("res://core/adventure_runner.gd").new(_db, rngT)
|
||
adrT.start_adventure(910003, {"first_script": false})
|
||
Kit.check("抽签模式恰多消费 1 scope(node_map 逐关恰一——L1 起关抽签)", rngT.counter() == cF + 1)
|
||
Kit.check("抽签模式 active_template_name 有值(MC 分层出数取数面在位)",
|
||
String(adrT.runner().active_template_name) != "")
|
||
# ⑦ 收束置位:新档 false → settle 置 true 恰一次(幂等)
|
||
DirAccess.remove_absolute("user://smoke_node_map_save.json") # 测试隔离——上轮 settle 写盘的残留档先删
|
||
var sv = load("res://core/save_manager.gd").new("user://smoke_node_map_save.json")
|
||
sv.load_from_disk()
|
||
Kit.check("新档首次冒险布尔位 = false(默认域)", 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)
|
||
|
||
|
||
# ---- 冒烟恢复执行体(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)
|