#!/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")