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 入口证明)、商店余额段按每关恰一店投影改三关读数
This commit is contained in:
1 parent
6566200220
commit
9ee3bea194
22 files changed
+460
-362
No files matched your search
@@ -1,18 +1,21 @@
|
||||
# tests/integration/node_map_templates_test.gd — L2 集成用例:关卡节点图生成·模板池
|
||||
# 断言面 = 模板池套(批 B 断言迁移自 smoke_all.gd `_smoke_node_map_templates`,G12 施工组;
|
||||
# tests/integration/node_map_templates_test.gd — L2 集成用例:关卡节点图生成·确定性承载
|
||||
# 断言面 = 生成图确定性套(批 B 断言迁移自 smoke_all.gd,G12 施工组;
|
||||
# 主会话裁决六整套集成不拆——跨四系统且走落盘)。
|
||||
# 跨系统面 = RngService(node_map 用途标签/抽签确定性/零消耗计账)×
|
||||
# LevelRunner(start_level_external 受理/模板 0 回滚档)×
|
||||
# AdventureRunner(first_script 全关零抽签/逐关恰一抽签)×
|
||||
# 跨系统面 = RngService(纯派生流零计数器占用)×
|
||||
# LevelRunner(start_level/start_level_external 受理·生成图装配·单一图源)×
|
||||
# AdventureRunner(开局种子承载·断点写读后同图)×
|
||||
# SaveManager+MetaProgression(首次冒险布尔位收束置位落盘——真实档写盘)。
|
||||
# 与单元侧同入口同协议:extends SceneTree+Kit.check 主通道;--script 模式无 autoload,
|
||||
# ConfigDB 显式实例化构造注入;表加载失败不中止(沿被迁断言面的源语义——诊断展开,红由断言落)。
|
||||
# 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:
|
||||
@@ -24,84 +27,115 @@ func _init() -> void:
|
||||
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()
|
||||
_smoke_generated_map_determinism()
|
||||
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) != "")
|
||||
# ---- 关卡节点图生成·确定性承载断言面 ----
|
||||
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 恰一次(幂等)
|
||||
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")
|
||||
var sv = SaveManagerScript.new(_bp_path + ".flag.json")
|
||||
sv.load_from_disk()
|
||||
Kit.check("新档首次冒险布尔位 = false(默认域)", sv.is_first_adventure_done() == false)
|
||||
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")
|
||||
|
||||
|
||||
# ---- 冒烟恢复执行体(LevelRunner 注入用——与 AdventureRunner.apply_rest 同式占位;
|
||||
# 图数据良构前置(防空集假绿——节点数 > 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)
|
||||
Reference in new issue
Block a user