Files
gplan-2/game/data/schemas/level_table.gd
T
DKChan b3f9b448e6 style(类型闸): level_table 图校验 by_id 改参数化字典——清偿迭代35卡② 引入的 S9 存量红
validate_graph 拆段时新写的 `var by_id: Dictionary = well["by_id"]` 命中 S9 R1
(显式非参数化容器注解),使 09 静态扫描在代码域批恒红。改为 Dictionary[int, Dictionary]
+assign 显式收编(Variant 不能隐式赋给参数化字典,实测报错后改此形态),行为逐字不变。
单测 level_table_validate_test 17 项全绿。
2026-10-25 03:28:27 +08:00

173 lines
7.9 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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))