200 lines
8.7 KiB
Python
200 lines
8.7 KiB
Python
#!/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")
|