diff --git a/game/data/schemas/level_table.gd b/game/data/schemas/level_table.gd index d9aea353..4d66fbc1 100644 --- a/game/data/schemas/level_table.gd +++ b/game/data/schemas/level_table.gd @@ -48,10 +48,24 @@ func validate() -> Array: # ④ 无环(出边目标深度严格大于源)。良构=句柄唯一·出边全命中·type 在五类枚举·battle/boss 带深度。 # 入口/终点个数与参数档的一致性归产出侧(关卡节点图生成 v1.1),本处不核个数;商店位归冒烟断言把关,本处禁检。 static func validate_graph(nodes: Array) -> Array: - var errs: Array[Dictionary] = [] if nodes.is_empty(): - errs.append({"category": "structural", "message": "图节点集为空"}) - return errs + 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 = 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(): @@ -73,8 +87,12 @@ static func validate_graph(nodes: Array) -> Array: by_id[nid] = node if not indeg.has(nid): indeg[nid] = 0 - if not errs.is_empty(): - return errs + 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] = [] @@ -87,25 +105,34 @@ static func validate_graph(nodes: Array) -> Array: errs.append({"category": "structural", "message": "连线 %d→%d 违反前进方向(目标深度须严格大于源)" % [nid, int(tid)]}) outs[nid].append(int(tid)) indeg[int(tid)] = int(indeg[int(tid)]) + 1 - if not errs.is_empty(): - return errs + 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 (outs[nid] as Array).is_empty(): - terminals.append(nid) - if String(by_id[nid]["type"]) != "boss": - errs.append({"category": "structural", "message": "终点 %d 非 Boss(终点恒为 Boss)" % nid}) - elif String(by_id[nid]["type"]) == "boss": - errs.append({"category": "structural", "message": "Boss 节点 %d 带出边(终点不可外出)" % 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)"}) - if not errs.is_empty(): - return errs + 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)