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