# VERTICAL SLICE - NOT FOR PRODUCTION # Validation Question: 线性第一关(战×2→道具→战×2→恢复→Boss)能否承载"数值提升→通关期望改善"的元进度验证 # Date: 2026-09-11(2026-09-17 分支节点图批:nodes 升级图结构——单链=出边恰一的退化形态) # 数据配置 LevelTable schema(关卡节点地图 v1.0 承载面)。 # 节点类型 closed enum:battle(战斗,须带 node_depth)/ reward(直通占位点)/ rest(血量恢复点)/ boss(关底 Boss,须带 node_depth) # / shop(商店房——GP+商店房实施批 2026-09-21 五类落地:同 reward/rest 无战斗行、无 node_depth 要求, # 图形态仍须编号化;零新增图合法性检查,必经位由冒烟断言承载非加载校验)。 # 图形态(行集内任一节点带 "next" 出边表即整行按图校验):节点编号 = node_depth(关卡内唯一定位地址, # 全节点须带,reward/rest 亦然);next = [出边节点编号...](连线只朝前进方向:目标编号严格大于自身)。 # 单链形态(零 "next" 键——第一关现状零迁移):隐式出边 = 数组顺序下一项,节点编号 = 数组下标 # (仅校验期内部使用;node_depth 仍只对 battle/boss 必填)——单链经同一套图合法性算法。 # 图合法性(违规加载致命,validate 报错由 ConfigDB 收敛为 fail-fast): # 编号唯一 · 入口唯一(无入边节点恰一)· Boss 恒唯一终点(无出边且编号最大)· 连线只朝前进方向(⇒无环) # · 从入口可达全部节点 · 全部节点沿连线可达 Boss。 class_name LevelTable extends Resource @export var rows: Array = [] # Array[Dictionary] const NODE_TYPES := ["battle", "reward", "rest", "boss", "shop"] # 商店房批 2026-09-21 四→五类(关卡节点地图 v1.1) func validate() -> Array: var errs: Array = [] if rows.is_empty(): errs.append({"category": "empty_table", "message": "LevelTable 为空"}) return errs var seen := {} for row in rows: var L: int = int(row.get("level_index", 0)) if seen.has(L): errs.append({"category": "dup_key", "message": "LevelTable level_index 重复: %d" % L}) seen[L] = true if L < 1: errs.append({"category": "domain", "message": "level_index 须 ≥1"}) if String(row.get("display_name", "")) == "": errs.append({"category": "domain", "message": "level %d display_name 空" % L}) 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}) 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) return errs # 图合法性核心(返回错误列表):id 解析(图=node_depth / 单链=数组下标)→ 出边表 → # 唯一性/前进方向/入口唯一/Boss 终点/双向可达六检。参考 关卡节点地图 v1.0 分支节点图规则。 func _validate_graph(L: int, nodes: Array, is_graph: bool) -> Array: var errs: Array = [] var ids: Array = [] # 每节点 id(下标对齐) var nexts: Array = [] # 每节点出边 id 数组 var id_to_idx := {} for i in nodes.size(): 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]}) nid = i # 降级继续(后续校验仍有意义) ids.append(nid) if id_to_idx.has(nid): errs.append({"category": "dup_key", "message": "level %d 节点编号重复: %d(关卡内唯一定位地址)" % [L, nid]}) id_to_idx[nid] = i var nx: Array = [] if node.has("next"): for v in node.get("next", []): nx.append(int(v)) elif i < nodes.size() - 1: nx.append(i + 1) # 单链隐式出边 = 数组下一项(id 即下标) nexts.append(nx) # ② 出边引用存在 + 前进方向(目标 id 严格递增 ⇒ 无环——环必含非递增边) 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]}) elif tid <= ids[i]: errs.append({"category": "structural", "message": "level %d 连线 %d→%d 违反前进方向(目标编号须严格更大)" % [L, ids[i], tid]}) # ④ 入口唯一(无入边节点恰一——唯一入口) var has_inbound := {} for i in nodes.size(): for tid in nexts[i]: has_inbound[tid] = true var entries: Array = [] for i in nodes.size(): 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()]}) # ⑤ Boss 恒唯一终点:无出边 + 编号最大(图形态;单链由"末节点 Boss"既有校验覆盖终点性) for i in nodes.size(): if String(nodes[i].get("type", "")) != "boss": continue if not nexts[i].is_empty(): errs.append({"category": "structural", "message": "level %d Boss 节点 %d 带出边(终点不可外出)" % [L, ids[i]]}) if is_graph and ids[i] != ids.max(): errs.append({"category": "structural", "message": "level %d Boss 编号 %d 非最大 %d(终点编号最大)" % [L, ids[i], ids.max()]}) if entries.size() == 1: # ⑥ 从入口可达全部(无孤儿)· ⑦ 全部节点可达 Boss(逆向)——邻接表统一下标空间 var fwd := [] var back := [] for i in nodes.size(): fwd.append([]) back.append([]) for i in nodes.size(): for tid in nexts[i]: if id_to_idx.has(tid): fwd[i].append(id_to_idx[tid]) back[id_to_idx[tid]].append(i) var reach := _reachable(entries[0], fwd, nodes.size()) for i in nodes.size(): if not reach.has(i): errs.append({"category": "structural", "message": "level %d 节点 %d 从入口不可达(孤儿节点)" % [L, ids[i]]}) var boss_idx := -1 for i in nodes.size(): if String(nodes[i].get("type", "")) == "boss": boss_idx = i if boss_idx >= 0: var to_boss := _reachable(boss_idx, back, nodes.size()) for i in nodes.size(): if not to_boss.has(i): errs.append({"category": "structural", "message": "level %d 节点 %d 沿连线不可达 Boss" % [L, ids[i]]}) return errs # BFS 可达集(邻接表 adj: 节点下标 → 下标数组) func _reachable(start: int, adj: Array, n: int) -> Dictionary: var seen := {} if start < 0 or start >= n: return seen var queue: Array = [start] seen[start] = true while not queue.is_empty(): var cur: int = queue.pop_front() if cur >= adj.size(): continue for nb in adj[cur]: if not seen.has(nb): seen[nb] = true queue.append(nb) return seen