#!/usr/bin/env python3 # -*- coding: utf-8 -*- """T17 出度≤2 模板池重建器(规则「节点出度最大为 2」——一次性内容批工具)。 背景:网状模板若无出边上限约束,「前层广连全层」是层宽加宽的几何必然, 会出出度 3-5 的节点;规则「一个节点只有后续两个分叉」要求全池重生成。 本工具按新规则重生成 L2/L3 十张: 新规则两条件: ① 单节点出度 ≤2; ② BFS 层宽上限随出度约束调整为 4(数学:入口唯一下层宽序列 ≤1,2,4——上限 5 与 出度 ≤2 不相容:层 2 达 5 宽需前层 3 节点各出 2 边,回溯入口即违 ①)。 五张骨架(层宽序列 1-2-4-1-1 / 1-2-3-2-1 / 1-2-2-2-1——最大层宽 4 应用足): 类型序列保持现行(depth 0-9 = b,b,reward,rest,b,b,b,b,shop,boss——五张原表全同), 仅重连 next。L1 五链出度恒 1 零改动。 自校验(python 侧四检):出度 ≤2 / 层宽 ≤4 / 全节点可达 Boss / 入口唯一。 Godot 端 LevelTable.validate(含第七检出度检)为最终闸。 """ import json import re from collections import deque TRES = "game/data/tables/level.tres" # 五骨架:depth → (type, next)——类型序列与现行五张逐 depth 一致 TYPE_SEQ = {0: "battle", 1: "battle", 2: "reward", 3: "rest", 4: "battle", 5: "battle", 6: "battle", 7: "battle", 8: "shop", 9: "boss"} SKELETONS = { "A": {0: [1, 2], 1: [3, 4], 2: [5, 6], 3: [7], 4: [7], 5: [7], 6: [7], 7: [8], 8: [9], 9: []}, "B": {0: [1, 2], 1: [3, 5], 2: [4, 5], 3: [6], 4: [6, 7], 5: [7], 6: [8], 7: [8], 8: [9], 9: []}, "C": {0: [1, 2], 1: [3, 5], 2: [4, 6], 3: [7], 4: [7], 5: [7], 6: [7], 7: [8], 8: [9], 9: []}, "D": {0: [1, 2], 1: [3, 4], 2: [4, 5], 3: [6, 7], 4: [7], 5: [7], 6: [8], 7: [8], 8: [9], 9: []}, "E": {0: [1, 2], 1: [3, 4], 2: [5], 3: [5, 6], 4: [6], 5: [7], 6: [7], 7: [8], 8: [9], 9: []}, } # L2/L3 模板名 → 骨架(「(现行)」后缀钉模板 0 断言锚,随 A 位保持; # L3 首张名「五岔主竖井」与 L2「五岔环廊」同构=w0 复制,同映射 A) NAME_TO_SKEL = [ ("五岔环廊", "A"), ("主竖井", "A"), ("四岔宽门", "B"), ("深层四岔", "C"), ("宽门赏后置", "D"), ("双干汇流", "E"), ] NEW_NAMES = { "A": "宽廊双干(现行)", "B": "三干中廊", "C": "宽廊斜岔", "D": "三干宽门", "E": "窄廊短支", } def build_nodes(skel): nodes = [] for d in range(10): n = {"node_depth": d, "type": TYPE_SEQ[d], "next": list(SKELETONS[skel][d])} nodes.append(n) return nodes def validate(nodes, label): """python 侧四检:出度 / 层宽 / 可达 Boss / 入口唯一。""" errs = [] outs = {n["node_depth"]: n["next"] for n in nodes} for d, nx in outs.items(): if len(nx) > 2: errs.append("%s 节点 %d 出度 %d > 2" % (label, d, len(nx))) # 层宽(BFS) layer = {0: 0} q = deque([0]) widths = {} while q: cur = q.popleft() for t in outs.get(cur, []): if t not in layer: layer[t] = layer[cur] + 1 q.append(t) for d, lv in layer.items(): widths[lv] = widths.get(lv, 0) + 1 for lv, w in widths.items(): if w > 4: errs.append("%s 层 %d 宽 %d > 4" % (label, lv, w)) # 可达 9 seen = set(layer.keys()) if 9 not in seen: errs.append("%s Boss 9 不可达" % label) if len(seen) != len(nodes): errs.append("%s 有节点不可达:%s" % (label, sorted(set(outs) - seen))) # 入口唯一(无入边恰一 = 0) inbound = {t for nx in outs.values() for t in nx} entries = [d for d in outs if d not in inbound] if entries != [0]: errs.append("%s 入口非唯一/非 0:%s" % (label, entries)) return errs def rebuild_line(line, level_tag): """重写一行 level 的 templates(类型序列保持、next 按骨架重连)。""" obj = json.loads(line.rstrip().rstrip(",")) new_templates = [] for tpl in obj["templates"]: old_name = tpl["display_name"] skel = None for key, s in NAME_TO_SKEL: if key in old_name: skel = s break if skel is None: raise SystemExit("%s: 未知模板名 %s" % (level_tag, old_name)) # 类型序列保持校验(现行表与本工具 TYPE_SEQ 必须逐 depth 一致) for n in tpl["nodes"]: d = n["node_depth"] if n["type"] != TYPE_SEQ[d]: raise SystemExit("%s/%s depth %d 类型漂移:表 %s vs 工具 %s" % (level_tag, old_name, d, n["type"], TYPE_SEQ[d])) new_templates.append({"display_name": NEW_NAMES[skel], "nodes": build_nodes(skel)}) obj["templates"] = new_templates errs = [] for tpl in new_templates: errs += validate(tpl["nodes"], "%s/%s" % (level_tag, tpl["display_name"])) if errs: raise SystemExit("自校验失败:\n" + "\n".join(errs)) return json.dumps(obj, ensure_ascii=False) def main(): with open(TRES, "r", encoding="utf-8") as f: content = f.read() out_lines = [] lines = content.splitlines() for i, line in enumerate(lines): stripped = line.strip() if stripped.startswith('{"level_index"'): m = re.search(r'"level_index": (\d+)', stripped) if m and m.group(1) in ("2", "3"): rebuilt = rebuild_line(line, m.group(1)) had_comma = line.rstrip().endswith(",") # 行尾逗号语义保持(tres 行序列) out_lines.append(rebuilt + ("," if had_comma else "")) continue out_lines.append(line) with open(TRES, "w", encoding="utf-8", newline="\n") as f: f.write("\n".join(out_lines) + "\n") print("[t17_fanout2] L2/L3 十张模板已按出度≤2/层宽≤4 重建,自校验四检全过") if __name__ == "__main__": main()