feat(m3-iter5-T5): 关卡节点图生成特性批·台阶一全落地——15张骨架(3关×5,模板0=现行图回滚档;L1单链排列变体/L2L3网状图变体,战斗id恒∈遭遇表行集,商店必经位承袭)+node_map扩签(标签不参与派生只进日志,黄金向量零动)+首次冒险固定脚本(存档布尔位唯一新增域,老档迁移=true,收束点置位幂等)+池内全模板加载校验(validate抽出与跨表校验模板感知)+MC分层出数(RUNSUM level_templates逐节点模板名,抽查五模板全被抽中);冒烟558→576(新增17断言:池参数/零scope强制/抽签确定性/N=1等价/首冒分支/收束置位幂等);生成器tools/t5_build_templates.py含生成前自校验

This commit is contained in:
DKChan committed 2026-09-28 23:20:00 +08:00
1 parent 8ffc5552f1
commit 3f73e16366
14 files changed
+471 -207

No files matched your search

+58 -36
View File
@@ -38,43 +38,65 @@ func validate() -> Array:
var heal: int = int(row.get("rest_heal_pct", -1))
if heal < 0 or heal > 100:
errs.append({"category": "domain", "message": "level %d rest_heal_pct 越域 [0,100]" % L})
var nodes: Array = row.get("nodes", [])
if nodes.is_empty():
errs.append({"category": "domain", "message": "level %d 节点链为空" % L})
# 模板池(关卡节点图生成 v0.4——M-3 迭代 5 T5):row.templates 非空 = 池形态,池内全模板加载期校验
# (违规致命——不只抽中的那张);空 = 旧单图形态(row.nodes 直读,向后兼容零迁移)。
var node_lists: Array = [] # [ [上下文标签, nodes 数组], ... ]
var templates: Array = row.get("templates", [])
if templates.is_empty():
node_lists.append(["", row.get("nodes", [])])
else:
for j in templates.size():
var tpl = templates[j]
if typeof(tpl) != TYPE_DICTIONARY or (tpl.get("nodes", []) as Array).is_empty():
errs.append({"category": "domain", "message": "level %d 模板[%d] 缺 nodes 或为空" % [L, j]})
continue
node_lists.append(["模板[%d]=%s" % [j, String(tpl.get("display_name", ""))], tpl.get("nodes", [])])
for nl in node_lists:
for e in _validate_level_nodes(L, nl[0], nl[1]):
errs.append(e)
return errs
# 单节点链校验体(validate 抽出——row.nodes 与池内每张模板同跑一套;ctx = 消息上下文标签)
func _validate_level_nodes(L: int, ctx: String, nodes: Array) -> Array:
var errs: Array = []
var tag: String = "" if ctx.is_empty() else "〔%s〕" % ctx
if nodes.is_empty():
errs.append({"category": "domain", "message": "level %d%s 节点链为空" % [L, tag]})
return errs
var boss_count := 0
var battle_count := 0
var is_graph := false
for node in nodes:
if node.has("next"):
is_graph = true
var t: String = String(node.get("type", ""))
if not NODE_TYPES.has(t):
errs.append({"category": "enum", "message": "level %d%s 未知节点类型: %s" % [L, tag, t]})
continue
var boss_count := 0
var battle_count := 0
var is_graph := false
for node in nodes:
if node.has("next"):
is_graph = true
var t: String = String(node.get("type", ""))
if not NODE_TYPES.has(t):
errs.append({"category": "enum", "message": "level %d 未知节点类型: %s" % [L, t]})
continue
if t == "battle" or t == "boss":
if int(node.get("node_depth", -1)) < 0:
errs.append({"category": "domain", "message": "level %d %s 节点缺 node_depth" % [L, t]})
battle_count += 1
if t == "boss":
boss_count += 1
if boss_count != 1:
errs.append({"category": "structural", "message": "level %d Boss 节点数 = %d(须恰一)" % [L, boss_count]})
if battle_count < 2:
errs.append({"category": "structural", "message": "level %d 战斗节点(含 Boss)= %d(≥2 才构成关卡)" % [L, battle_count]})
if not is_graph:
# 单链形态纪律(第一关零迁移):末节点必须是 Boss(关底收尾——冒险结构 关卡定义)
if String(nodes[nodes.size() - 1].get("type", "")) != "boss":
errs.append({"category": "structural", "message": "level %d 末节点非 Boss(单链以 Boss 收尾)" % L})
# 图合法性(单链与图同一套算法——单链 = 出边恰一的退化形态,违规同样加载致命)
for e in _validate_graph(L, nodes, is_graph):
errs.append(e)
if t == "battle" or t == "boss":
if int(node.get("node_depth", -1)) < 0:
errs.append({"category": "domain", "message": "level %d%s %s 节点缺 node_depth" % [L, tag, t]})
battle_count += 1
if t == "boss":
boss_count += 1
if boss_count != 1:
errs.append({"category": "structural", "message": "level %d%s Boss 节点数 = %d(须恰一)" % [L, tag, boss_count]})
if battle_count < 2:
errs.append({"category": "structural", "message": "level %d%s 战斗节点(含 Boss)= %d(≥2 才构成关卡)" % [L, tag, battle_count]})
if not is_graph:
# 单链形态纪律(第一关零迁移):末节点必须是 Boss(关底收尾——冒险结构 关卡定义)
if String(nodes[nodes.size() - 1].get("type", "")) != "boss":
errs.append({"category": "structural", "message": "level %d%s 末节点非 Boss(单链以 Boss 收尾)" % [L, tag]})
# 图合法性(单链与图同一套算法——单链 = 出边恰一的退化形态,违规同样加载致命)
for e in _validate_graph(L, nodes, is_graph, tag):
errs.append(e)
return errs
# 图合法性核心(返回错误列表):id 解析(图=node_depth / 单链=数组下标)→ 出边表 →
# 唯一性/前进方向/入口唯一/Boss 终点/双向可达六检。参考 关卡节点地图 v1.0 分支节点图规则。
func _validate_graph(L: int, nodes: Array, is_graph: bool) -> Array:
func _validate_graph(L: int, nodes: Array, is_graph: bool, tag: String = "") -> Array:
var errs: Array = []
var ids: Array = [] # 每节点 id(下标对齐)
var nexts: Array = [] # 每节点出边 id 数组
@@ -83,11 +105,11 @@ func _validate_graph(L: int, nodes: Array, is_graph: bool) -> Array:
var node: Dictionary = nodes[i]
var nid: int = int(node.get("node_depth", i)) if is_graph else i
if is_graph and not node.has("node_depth"):
errs.append({"category": "structural", "message": "level %d 图形态节点[%d] 缺 node_depth(全节点须编号化)" % [L, i]})
errs.append({"category": "structural", "message": "level %d%s 图形态节点[%d] 缺 node_depth(全节点须编号化)" % [L, tag, i]})
nid = i # 降级继续(后续校验仍有意义)
ids.append(nid)
if id_to_idx.has(nid):
errs.append({"category": "dup_key", "message": "level %d 节点编号重复: %d(关卡内唯一定位地址)" % [L, nid]})
errs.append({"category": "dup_key", "message": "level %d%s 节点编号重复: %d(关卡内唯一定位地址)" % [L, tag, nid]})
id_to_idx[nid] = i
var nx: Array = []
if node.has("next"):
@@ -100,9 +122,9 @@ func _validate_graph(L: int, nodes: Array, is_graph: bool) -> Array:
for i in nodes.size():
for tid in nexts[i]:
if not id_to_idx.has(tid):
errs.append({"category": "dangling_next", "message": "level %d 节点 %d 出边指向不存在编号: %d" % [L, ids[i], tid]})
errs.append({"category": "dangling_next", "message": "level %d%s 节点 %d 出边指向不存在编号: %d" % [L, tag, ids[i], tid]})
elif tid <= ids[i]:
errs.append({"category": "structural", "message": "level %d 连线 %d→%d 违反前进方向(目标编号须严格更大)" % [L, ids[i], tid]})
errs.append({"category": "structural", "message": "level %d%s 连线 %d→%d 违反前进方向(目标编号须严格更大)" % [L, tag, ids[i], tid]})
# ④ 入口唯一(无入边节点恰一——唯一入口)
var has_inbound := {}
for i in nodes.size():
@@ -113,7 +135,7 @@ func _validate_graph(L: int, nodes: Array, is_graph: bool) -> Array:
if not has_inbound.has(ids[i]):
entries.append(i)
if entries.size() != 1:
errs.append({"category": "structural", "message": "level %d 入口节点数 = %d(须恰一——无入边节点唯一)" % [L, entries.size()]})
errs.append({"category": "structural", "message": "level %d%s 入口节点数 = %d(须恰一——无入边节点唯一)" % [L, tag, entries.size()]})
# ⑤ Boss 恒唯一终点:无出边 + 编号最大(图形态;单链由"末节点 Boss"既有校验覆盖终点性)
for i in nodes.size():
if String(nodes[i].get("type", "")) != "boss":