Files
gplan-2/tools/t5_build_templates.py
T

240 lines
11 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(拍板点④)。U2 目检返工(2026-09-29)两条新规则:
① L1 单链前三位纯战斗(奖励/休整禁入前三——用户目检裁决);
② L2/L3 网状图 BFS 同深度层节点数上限 5,新池应用足宽度(每张最大层宽 ≥4——分支加宽)。
约束:战斗/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
# 前三位纯战斗(U2 规则①)——位 4-6 = {战斗3, 奖励, 休整} 排列变体,末位 Boss
L1_TEMPLATES = [
("地精洞窟·主线洞道(现行)", chain([("battle", 0), ("battle", 1), ("battle", 2), ("battle", 3),
("reward", None), ("rest", None), ("boss", 4)])),
("地精洞窟·先赏后整", chain([("battle", 0), ("battle", 1), ("battle", 2), ("reward", None),
("rest", None), ("battle", 3), ("boss", 4)])),
("地精洞窟·先整后赏", chain([("battle", 0), ("battle", 1), ("battle", 2), ("rest", None),
("reward", None), ("battle", 3), ("boss", 4)])),
("地精洞窟·赏战整", chain([("battle", 0), ("battle", 1), ("battle", 2), ("reward", None),
("battle", 3), ("rest", None), ("boss", 4)])),
("地精洞窟·整战赏", chain([("battle", 0), ("battle", 1), ("battle", 2), ("rest", None),
("battle", 3), ("reward", None), ("boss", 4)])),
]
def w0():
return graph({0: ("battle", 0, [1, 2, 3, 4, 5]), 1: ("battle", 1, [6]), 2: ("reward", 2, [6, 7]),
3: ("rest", 3, [6]), 4: ("battle", 4, [7]), 5: ("battle", 5, [6, 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, 3, 4]), 1: ("battle", 1, [5]), 2: ("reward", 2, [5, 6]),
3: ("rest", 3, [6, 7]), 4: ("battle", 4, [7]), 5: ("battle", 5, [8]),
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, [3, 4, 5, 6]), 2: ("reward", 2, [3, 4]),
3: ("rest", 3, [7]), 4: ("battle", 4, [7]), 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, 3, 4, 5]), 1: ("battle", 1, [2, 6]), 2: ("reward", 2, [7]),
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 w4():
return graph({0: ("battle", 0, [1, 2]), 1: ("battle", 1, [4, 5, 6]), 2: ("reward", 2, [3, 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, [])})
L2_WIRINGS = [("幽暗地域·五岔环廊(现行)", w0()), ("幽暗地域·四岔宽门", w1()),
("幽暗地域·深层四岔", w2()), ("幽暗地域·宽门赏后置", w3()), ("幽暗地域·双干汇流", w4())]
L3_WIRINGS = [("灰矮人矿城·五岔主竖井(现行)", w0()), ("灰矮人矿城·四岔宽门", w1()),
("灰矮人矿城·深层四岔", w2()), ("灰矮人矿城·宽门赏后置", w3()), ("灰矮人矿城·双干汇流", w4())]
def bfs_widths(nodes):
"""BFS 分层(图形态)→ 各层宽度列表。"""
ids = [n["node_depth"] for n in nodes]
adj = {n["node_depth"]: list(n.get("next", [])) for n in nodes}
inbound = set()
for nxt in adj.values():
inbound |= set(nxt)
entry = [i for i in ids if i not in inbound][0]
layer = {entry: 0}
frontier = [entry]
while frontier:
cur = frontier.pop(0)
for t in adj[cur]:
if t not in layer:
layer[t] = layer[cur] + 1
frontier.append(t)
widths = {}
for i in ids:
widths[layer[i]] = widths.get(layer[i], 0) + 1
return [widths[d] for d in sorted(widths)]
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))
# U2 规则②:BFS 同层 ≤5,且最大层宽 ≥4(分支应用足)
ws = bfs_widths(nodes)
if max(ws) > 5:
errs.append("层宽越上限 5: %s" % ws)
if max(ws) < 4:
errs.append("分支不足(最大层宽 %d < 4——U2 加宽裁决)" % max(ws))
else:
# U2 规则①:单链前三位纯战斗
for i in range(3):
if nodes[i]["type"] != "battle":
errs.append("第 %d 位非战斗(前三位纯战斗)" % (i + 1))
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, "层宽=%s" % bfs_widths(nodes), 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"
"; L2/L3 网状图约束(承袭):战斗节点编号恒 ∈ 遭遇表行集 {0,1,4,5,6,7}、Boss 9 最大编号、\n"
"; 商店房 8→Boss 必经位承袭;遭遇表按编号共用——零新增敌人数值。L1 五模板 = 单链排列变体\n"
"; (战斗深度序单调保难度坡,变化面 = 奖励/恢复位次)。\n"
"; 池内全模板加载期校验(level_table.gd validate + config_db 跨表校验模板感知);\n"
"; 行为读 LevelRunner._resolve_template_row(node_map 标签恰一 scope 抽签 / template_pick 强制)。\n"
"; 生成器 = tools/t5_build_templates.py(生成前自校验,一次性内容批工具随批入库留档)。\n"
"; U2 目检返工(M-3 迭代 5 返工批,2026-09-29——U2 目检 15/15 不过两规则缺口,全池重生成):\n"
"; ① L1 奖励/休整禁入前三位(用户目检裁决)——五链全部前三位纯战斗,位 4-6 = {战斗3,奖励,休整} 排列;\n"
"; ② L2/L3 BFS 同深度层上限 5 且应用足(每张最大层宽 4-5,旧池层宽最大 3 被否);模板 0 一并重排\n"
"; (待批点甲按推荐=随新规则重生成——首次冒险/钉固断言期望随新图更新;模板 0 名保持「(现行)」后缀=新现行图锚)。\n"
"; 加载校验两规则同落 level_table.gd validate(单链前三纯战斗/图形态层宽 ≤5)。\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")