## VERTICAL SLICE - NOT FOR PRODUCTION ## 关卡节点表 schema:逐关行集(关数/显示名/休整回复/迷雾层数)的类型化读取(垂直切片验证:改表即改关卡)。 ## 图合法性只在 `validate_graph` 一处陈述(消费侧起关校验入口——关卡节点图生成 v1.1 产出规格为准); ## 表加载期不校验图(关卡表不再携带节点数据,图源=参数化生成器)。 class_name LevelTable extends Resource @export var rows: Array[Dictionary] = [] # Array[Dictionary] @export var info_visible_layers: int = 2 # 信息迷雾可见层数 N(int ≥1;类型仅在邻近 N 层内可见——关卡节点地图 v2.1) const NODE_TYPES := ["battle", "reward", "rest", "boss", "shop"] # 五类(关卡节点地图 v2.1) # 行单点读取(表访问收口——LevelRunner._find_level_row 禁直扫;键 = level_index) func row_by_level_index(level_index: int) -> Dictionary: for row in rows: if int(row.level_index) == level_index: return row return {} func validate() -> Array: var errs: Array[Dictionary] = [] if info_visible_layers < 1: errs.append({"category": "domain", "message": "info_visible_layers 越域 int≥1: %d" % info_visible_layers}) 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}) return errs # 图合法性校验单点(消费侧起关入口——生成图到手、进节点前调用;违规=致命,无软回退)。 # 判据=四检+良构基底:① 入口(无入边)≥1 且终点(无出边)≥1;② 全部节点从某入口可达且可达某终点 #(每条路径恰从一个入口出发、恰到好处一个终点,无孤儿无断头);③ 终点恒为 Boss(终点 type=boss 且 boss 无出边); # ④ 无环(出边目标深度严格大于源)。良构=句柄唯一·出边全命中·type 在五类枚举·battle/boss 带深度。 # 入口/终点个数与参数档的一致性归产出侧(关卡节点图生成 v1.1),本处不核个数;商店位归冒烟断言把关,本处禁检。 static func validate_graph(nodes: Array) -> Array: if nodes.is_empty(): return [{"category": "structural", "message": "图节点集为空"}] var well := _check_nodes_wellformed(nodes) if not (well["errs"] as Array).is_empty(): return well["errs"] var by_id: Dictionary[int, Dictionary] = {} by_id.assign(well["by_id"]) var edges := _check_edges(by_id, well["indeg"]) if not (edges["errs"] as Array).is_empty(): return edges["errs"] var stops := _check_terminals(by_id, edges["outs"], edges["indeg"]) if not (stops["errs"] as Array).is_empty(): return stops["errs"] return _check_reachability(by_id, edges["outs"], stops["entries"], stops["terminals"]) # 良构基底:字段齐·type 在枚举·深度非负·句柄唯一(返回 by_id 与入度骨架供后续检复用) static func _check_nodes_wellformed(nodes: Array) -> Dictionary: var errs: Array[Dictionary] = [] var by_id := {} var indeg := {} for i in nodes.size(): var node: Dictionary[Variant, Variant] = nodes[i] for k in ["id", "type", "depth", "next"]: if not node.has(k): errs.append({"category": "structural", "message": "节点[%d] 缺字段 %s" % [i, k]}) var t := String(node.get("type", "")) if not NODE_TYPES.has(t): errs.append({"category": "enum", "message": "未知节点类型: %s" % t}) var d: int = int(node.get("depth", -1)) if d < 0: errs.append({"category": "domain", "message": "节点 %s 深度越域(≥0): %d" % [str(node.get("id", i)), d]}) if t == "battle" and not node.has("depth"): errs.append({"category": "structural", "message": "battle 节点 %s 缺深度" % str(node.get("id", i))}) var nid: int = int(node.get("id", i)) if by_id.has(nid): errs.append({"category": "dup_key", "message": "节点句柄重复: %d" % nid}) by_id[nid] = node if not indeg.has(nid): indeg[nid] = 0 return {"errs": errs, "by_id": by_id, "indeg": indeg} # 出边良构+无环判据(④):目标句柄存在、目标深度严格大于源;同时产出去边表与入度计数 static func _check_edges(by_id: Dictionary, indeg: Dictionary) -> Dictionary: var errs: Array[Dictionary] = [] var outs := {} for nid in by_id.keys(): outs[nid] = [] for nid in by_id.keys(): for tid in (by_id[nid]["next"] as Array): if not by_id.has(int(tid)): errs.append({"category": "dangling_next", "message": "节点 %d 出边指向不存在句柄: %s" % [nid, tid]}) continue if int(by_id[int(tid)]["depth"]) <= int(by_id[nid]["depth"]): errs.append({"category": "structural", "message": "连线 %d→%d 违反前进方向(目标深度须严格大于源)" % [nid, int(tid)]}) outs[nid].append(int(tid)) indeg[int(tid)] = int(indeg[int(tid)]) + 1 return {"errs": errs, "outs": outs, "indeg": indeg} # 检①+检③:入口(无入边)≥1、终点(无出边)≥1、终点恒为 Boss 且 Boss 无出边 static func _check_terminals(by_id: Dictionary, outs: Dictionary, indeg: Dictionary) -> Dictionary: var errs: Array[Dictionary] = [] var entries := [] var terminals := [] for nid in by_id.keys(): if int(indeg[nid]) == 0: entries.append(nid) if not (outs[nid] as Array).is_empty(): if String(by_id[nid]["type"]) == "boss": errs.append({"category": "structural", "message": "Boss 节点 %d 带出边(终点不可外出)" % nid}) continue terminals.append(nid) if String(by_id[nid]["type"]) != "boss": errs.append({"category": "structural", "message": "终点 %d 非 Boss(终点恒为 Boss)" % nid}) if entries.is_empty(): errs.append({"category": "structural", "message": "入口节点数 = 0(须 ≥1)"}) if terminals.is_empty(): errs.append({"category": "structural", "message": "终点节点数 = 0(须 ≥1)"}) return {"errs": errs, "entries": entries, "terminals": terminals} # 检②:全部节点从某入口可达且可达某终点(无孤儿、无断头) static func _check_reachability(by_id: Dictionary, outs: Dictionary, entries: Array, terminals: Array) -> Array: var errs: Array[Dictionary] = [] var from_entry := {} for eid in entries: _mark_reachable(int(eid), outs, from_entry) var to_terminal := {} for tid in terminals: _mark_backward(int(tid), outs, to_terminal) for nid in by_id.keys(): if not from_entry.has(nid): errs.append({"category": "structural", "message": "节点 %d 从任何入口都不可达(孤儿)" % nid}) if not to_terminal.has(nid): errs.append({"category": "structural", "message": "节点 %d 沿连线到不了任何终点(断头)" % nid}) return errs # 正向可达集(含起点) static func _mark_reachable(start: int, outs: Dictionary, seen: Dictionary) -> void: var queue: Array[Variant] = [start] seen[start] = true while not queue.is_empty(): var cur: int = queue.pop_front() for tid in outs[cur]: if not seen.has(int(tid)): seen[int(tid)] = true queue.append(int(tid)) # 逆向可达集(含起点)——反向邻接由 outs 现推(图规模小,禁第二份邻接表持有) static func _mark_backward(target: int, outs: Dictionary, seen: Dictionary) -> void: var queue: Array[Variant] = [target] seen[target] = true while not queue.is_empty(): var cur: int = queue.pop_front() for nid in outs.keys(): if (outs[nid] as Array).has(cur) and not seen.has(int(nid)): seen[int(nid)] = true queue.append(int(nid)) ## 序列总关数(AdventureRunner levels_total / MetaProgression 关卡完成累加域的取数口径) func level_count() -> int: return rows.size()