Files
gplan-2/tools/t17_fanout2_rebuild.py
T

147 lines
5.9 KiB
Python
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.
#!/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()