feat(m3-iter5-T5): 关卡节点图生成特性批·台阶一全落地——15张骨架(3关×5,模板0=现行图回滚档;L1单链排列变体/L2L3网状图变体,战斗id恒∈遭遇表行集,商店必经位承袭)+node_map扩签(标签不参与派生只进日志,黄金向量零动)+首次冒险固定脚本(存档布尔位唯一新增域,老档迁移=true,收束点置位幂等)+池内全模板加载校验(validate抽出与跨表校验模板感知)+MC分层出数(RUNSUM level_templates逐节点模板名,抽查五模板全被抽中);冒烟558→576(新增17断言:池参数/零scope强制/抽签确定性/N=1等价/首冒分支/收束置位幂等);生成器tools/t5_build_templates.py含生成前自校验

This commit is contained in:
DKChan committed 2026-09-28 23:20:00 +08:00
1 parent 8ffc5552f1
commit 3f73e16366
14 files changed
+471 -207

No files matched your search

+199
View File
@@ -0,0 +1,199 @@
#!/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")