Files
gplan-2/tools/t5_build_templates.py
T

200 lines
8.7 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 -*-
"""T5 骨架池生成器(M-3 迭代 5——一次性内容批工具,生成 game/data/tables/level.tres 模板池)。
15 张骨架 = 3 关 × 5(拍板点④),模板 0 = 现行固定图(回滚档成员)。
约束:战斗/Boss node_depth = 遭遇表行;图形态 id = node_depth → L2/L3 战斗 ∈ {0,1,4,5,6,7}、Boss=9;
L1 图形态两不可全(Boss 须最大 id vs 深度 4)→ L1 单链排列变体(战斗深度序单调)。
商店房每关恰 1、Boss 前必经位(L1 设计态 = 零商店——商店房批教学期修订面)。
"""
L1_BATTLES = [0, 1, 2, 3]
L1_BOSS = 4
L2_BATTLES = [0, 1, 4, 5, 6, 7]
Q = '"' # 引号常量(避免嵌套三引号)
def chain(seq):
nodes = []
for t, d in seq:
n = {"type": t}
if d is not None:
n["node_depth"] = d
nodes.append(n)
return nodes
def graph(spec):
nodes = []
for i in sorted(spec.keys()):
t, d, nxt = spec[i]
nodes.append({"node_depth": i, "type": t, "next": list(nxt)})
return nodes
L1_TEMPLATES = [
("地精洞窟·主线洞道(现行)", chain([("battle", 0), ("battle", 1), ("reward", None),
("battle", 2), ("battle", 3), ("rest", None), ("boss", 4)])),
("地精洞窟·休整前移", chain([("battle", 0), ("battle", 1), ("rest", None),
("battle", 2), ("reward", None), ("battle", 3), ("boss", 4)])),
("地精洞窟·先赏后战", chain([("battle", 0), ("reward", None), ("battle", 1), ("rest", None),
("battle", 2), ("battle", 3), ("boss", 4)])),
("地精洞窟·连战直入", chain([("battle", 0), ("battle", 1), ("reward", None), ("rest", None),
("battle", 2), ("battle", 3), ("boss", 4)])),
("地精洞窟·缓入长坡", chain([("battle", 0), ("rest", None), ("battle", 1), ("reward", None),
("battle", 2), ("battle", 3), ("boss", 4)])),
]
def w0():
return graph({0: ("battle", 0, [1, 2]), 1: ("battle", 1, [3, 4]), 2: ("reward", 2, [4, 5]),
3: ("rest", 3, [6]), 4: ("battle", 4, [6, 7]), 5: ("battle", 5, [7]),
6: ("battle", 6, [8]), 7: ("battle", 7, [8]), 8: ("shop", 8, [9]), 9: ("boss", 9, [])})
def w1():
return graph({0: ("battle", 0, [1, 2]), 1: ("battle", 1, [3, 4]), 2: ("reward", 2, [4]),
3: ("rest", 3, [5]), 4: ("battle", 4, [5, 6]), 5: ("battle", 5, [7]),
6: ("battle", 6, [8]), 7: ("battle", 7, [8]), 8: ("shop", 8, [9]), 9: ("boss", 9, [])})
def w2():
return graph({0: ("battle", 0, [1, 2]), 1: ("battle", 1, [4]), 2: ("reward", 2, [3, 4]),
3: ("rest", 3, [6]), 4: ("battle", 4, [5, 6]), 5: ("battle", 5, [7]),
6: ("battle", 6, [7]), 7: ("battle", 7, [8]), 8: ("shop", 8, [9]), 9: ("boss", 9, [])})
def w3():
return graph({0: ("battle", 0, [1, 2]), 1: ("battle", 1, [2, 4]), 2: ("reward", 2, [3]),
3: ("rest", 3, [6]), 4: ("battle", 4, [5, 6]), 5: ("battle", 5, [7]),
6: ("battle", 6, [8]), 7: ("battle", 7, [8]), 8: ("shop", 8, [9]), 9: ("boss", 9, [])})
def w4():
return graph({0: ("battle", 0, [1, 2]), 1: ("battle", 1, [3]), 2: ("reward", 2, [3, 4]),
3: ("rest", 3, [4, 5]), 4: ("battle", 4, [6]), 5: ("battle", 5, [6]),
6: ("battle", 6, [7]), 7: ("battle", 7, [8]), 8: ("shop", 8, [9]), 9: ("boss", 9, [])})
L2_WIRINGS = [("幽暗地域·四岔环廊(现行)", w0()), ("幽暗地域·奖励窄门", w1()),
("幽暗地域·直入深层", w2()), ("幽暗地域·休整远路", w3()), ("幽暗地域·双线归束", w4())]
L3_WIRINGS = [("灰矮人矿城·主竖井(现行)", w0()), ("灰矮人矿城·奖励窄门", w1()),
("灰矮人矿城·直入深层", w2()), ("灰矮人矿城·休整远路", w3()), ("灰矮人矿城·双线归束", w4())]
def validate(name, nodes, battles, boss_depth, has_shop):
errs = []
is_graph = any("next" in n for n in nodes)
ids = ([n["node_depth"] for n in nodes] if is_graph else list(range(len(nodes))))
if len(set(ids)) != len(ids):
errs.append("编号重复")
adj = {}
for i, n in enumerate(nodes):
if is_graph:
nxt = list(n.get("next", []))
else:
nxt = [i + 1] if i < len(nodes) - 1 else []
adj[ids[i]] = nxt
for src, nxt in adj.items():
for tid in nxt:
if tid not in ids:
errs.append("悬空出边 %d→%d" % (src, tid))
elif tid <= src:
errs.append("非前进边 %d→%d" % (src, tid))
if is_graph:
inbound = set()
for nxt in adj.values():
inbound |= set(nxt)
entries = [x for x in ids if x not in inbound]
if len(entries) != 1:
errs.append("入口数 %d" % len(entries))
reach, frontier = set(), list(entries)
while frontier:
cur = frontier.pop()
if cur in reach:
continue
reach.add(cur)
frontier += list(adj.get(cur, []))
if reach != set(ids):
errs.append("存在不可达节点: %s" % (set(ids) - reach))
bt = sorted(n["node_depth"] for n in nodes if n["type"] in ("battle", "boss"))
if bt != sorted(battles + [boss_depth]):
errs.append("战斗深度集合不符: %s" % bt)
shops = [n for n in nodes if n["type"] == "shop"]
if len(shops) != (1 if has_shop else 0):
errs.append("商店房数 %d" % len(shops))
if has_shop:
sid = shops[0]["node_depth"]
if is_graph:
if sid != boss_depth - 1 or adj[sid] != [boss_depth]:
errs.append("商店房非 Boss 前必经位")
elif nodes[-2]["type"] != "shop":
errs.append("单链商店房非 Boss 前一位")
return errs
all_ok = True
for name, nodes in L1_TEMPLATES:
e = validate(name, nodes, L1_BATTLES, L1_BOSS, False)
print(("PASS" if not e else "FAIL"), name, e)
all_ok &= not e
for name, nodes in L2_WIRINGS + L3_WIRINGS:
e = validate(name, nodes, L2_BATTLES, 9, True)
print(("PASS" if not e else "FAIL"), name, e)
all_ok &= not e
print("ALL OK" if all_ok else "HAS FAILURES")
if not all_ok:
raise SystemExit(1)
def nodes_json(nodes):
parts = []
for n in nodes:
kv = []
if "node_depth" in n:
kv.append('"node_depth": %d' % n["node_depth"])
kv.append('"type": "%s"' % n["type"])
if "next" in n:
kv.append('"next": [%s]' % ", ".join(str(x) for x in n["next"]))
parts.append("{" + ", ".join(kv) + "}")
return "[" + ", ".join(parts) + "]"
def tpl_json(name, nodes):
return '{"display_name": "%s", "nodes": %s}' % (name, nodes_json(nodes))
def row_json(li, dn, templates):
return ('{"level_index": %d, "display_name": "%s", "rest_heal_pct": 30, "templates": [%s]}'
% (li, dn, ", ".join(tpl_json(n, ns) for n, ns in templates)))
rows = [row_json(1, "第一层 · 地精洞窟", L1_TEMPLATES),
row_json(2, "第二层 · 幽暗地域", L2_WIRINGS),
row_json(3, "第三层 · 灰矮人矿城", L3_WIRINGS)]
header = (
"[gd_resource type=\"Resource\" script_class=\"LevelTable\" load_steps=2 format=3]\n"
"\n"
"[ext_resource type=\"Script\" path=\"res://data/schemas/level_table.gd\" id=\"1\"]\n"
"\n"
"[resource]\n"
"script = ExtResource(\"1\")\n"
"rows = [\n")
footer = (
"\n]\n"
"\n"
"; 关卡节点图生成 v0.4 台阶一内容批(M-3 迭代 5 T5,2026-09-28——15 张骨架 = 3 关 × 5,拍板点④):\n"
"; 模板 0 = 现行固定图(回滚档成员——首次冒险脚本/池缩 N=1 时逐位等价迁移前行为);\n"
"; L1 五模板 = 单链排列变体(图形态两不可全:Boss 须最大编号 vs Boss 遭遇深度 4——战斗深度序单调\n"
"; 保持难度坡,变化面 = 奖励/恢复位次);L2/L3 五模板 = 网状图变体(战斗节点编号恒 ∈ 遭遇表行集\n"
"; {0,1,4,5,6,7}、Boss 9 最大编号、商店房 8→Boss 必经位承袭;遭遇表按编号共用——零新增敌人数值)。\n"
"; 池内全模板加载期校验(level_table.gd validate + config_db 跨表校验模板感知);\n"
"; 行为读 LevelRunner._resolve_template_row(node_map 标签恰一 scope 抽签 / template_pick 强制)。\n"
"; 生成器 = tools/t5_build_templates.py(生成前自校验,一次性内容批工具随批入库留档)。\n")
with open("game/data/tables/level.tres", "w", encoding="utf-8", newline="\n") as f:
f.write(header + ",\n".join(rows) + footer)
print("level.tres written")