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