#!/usr/bin/env python3 # -*- coding: utf-8 -*- """T5 骨架池生成器(一次性内容批工具,生成 game/data/tables/level.tres 模板池)。 15 张骨架 = 3 关 × 5(拍板点④)。U2 目检两条现行规则: ① 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")