From 3f73e163669d64b998f8961223e2f3b15bc4a89e Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Mon, 28 Sep 2026 23:20:00 +0800 Subject: [PATCH] =?UTF-8?q?feat(m3-iter5-T5):=20=E5=85=B3=E5=8D=A1?= =?UTF-8?q?=E8=8A=82=E7=82=B9=E5=9B=BE=E7=94=9F=E6=88=90=E7=89=B9=E6=80=A7?= =?UTF-8?q?=E6=89=B9=C2=B7=E5=8F=B0=E9=98=B6=E4=B8=80=E5=85=A8=E8=90=BD?= =?UTF-8?q?=E5=9C=B0=E2=80=94=E2=80=9415=E5=BC=A0=E9=AA=A8=E6=9E=B6(3?= =?UTF-8?q?=E5=85=B3=C3=975,=E6=A8=A1=E6=9D=BF0=3D=E7=8E=B0=E8=A1=8C?= =?UTF-8?q?=E5=9B=BE=E5=9B=9E=E6=BB=9A=E6=A1=A3;L1=E5=8D=95=E9=93=BE?= =?UTF-8?q?=E6=8E=92=E5=88=97=E5=8F=98=E4=BD=93/L2L3=E7=BD=91=E7=8A=B6?= =?UTF-8?q?=E5=9B=BE=E5=8F=98=E4=BD=93,=E6=88=98=E6=96=97id=E6=81=92?= =?UTF-8?q?=E2=88=88=E9=81=AD=E9=81=87=E8=A1=A8=E8=A1=8C=E9=9B=86,?= =?UTF-8?q?=E5=95=86=E5=BA=97=E5=BF=85=E7=BB=8F=E4=BD=8D=E6=89=BF=E8=A2=AD?= =?UTF-8?q?)+node=5Fmap=E6=89=A9=E7=AD=BE(=E6=A0=87=E7=AD=BE=E4=B8=8D?= =?UTF-8?q?=E5=8F=82=E4=B8=8E=E6=B4=BE=E7=94=9F=E5=8F=AA=E8=BF=9B=E6=97=A5?= =?UTF-8?q?=E5=BF=97,=E9=BB=84=E9=87=91=E5=90=91=E9=87=8F=E9=9B=B6?= =?UTF-8?q?=E5=8A=A8)+=E9=A6=96=E6=AC=A1=E5=86=92=E9=99=A9=E5=9B=BA?= =?UTF-8?q?=E5=AE=9A=E8=84=9A=E6=9C=AC(=E5=AD=98=E6=A1=A3=E5=B8=83?= =?UTF-8?q?=E5=B0=94=E4=BD=8D=E5=94=AF=E4=B8=80=E6=96=B0=E5=A2=9E=E5=9F=9F?= =?UTF-8?q?,=E8=80=81=E6=A1=A3=E8=BF=81=E7=A7=BB=3Dtrue,=E6=94=B6=E6=9D=9F?= =?UTF-8?q?=E7=82=B9=E7=BD=AE=E4=BD=8D=E5=B9=82=E7=AD=89)+=E6=B1=A0?= =?UTF-8?q?=E5=86=85=E5=85=A8=E6=A8=A1=E6=9D=BF=E5=8A=A0=E8=BD=BD=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C(validate=E6=8A=BD=E5=87=BA=E4=B8=8E=E8=B7=A8=E8=A1=A8?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=E6=A8=A1=E6=9D=BF=E6=84=9F=E7=9F=A5)+MC?= =?UTF-8?q?=E5=88=86=E5=B1=82=E5=87=BA=E6=95=B0(RUNSUM=20level=5Ftemplates?= =?UTF-8?q?=E9=80=90=E8=8A=82=E7=82=B9=E6=A8=A1=E6=9D=BF=E5=90=8D,?= =?UTF-8?q?=E6=8A=BD=E6=9F=A5=E4=BA=94=E6=A8=A1=E6=9D=BF=E5=85=A8=E8=A2=AB?= =?UTF-8?q?=E6=8A=BD=E4=B8=AD);=E5=86=92=E7=83=9F558=E2=86=92576(=E6=96=B0?= =?UTF-8?q?=E5=A2=9E17=E6=96=AD=E8=A8=80:=E6=B1=A0=E5=8F=82=E6=95=B0/?= =?UTF-8?q?=E9=9B=B6scope=E5=BC=BA=E5=88=B6/=E6=8A=BD=E7=AD=BE=E7=A1=AE?= =?UTF-8?q?=E5=AE=9A=E6=80=A7/N=3D1=E7=AD=89=E4=BB=B7/=E9=A6=96=E5=86=92?= =?UTF-8?q?=E5=88=86=E6=94=AF/=E6=94=B6=E6=9D=9F=E7=BD=AE=E4=BD=8D?= =?UTF-8?q?=E5=B9=82=E7=AD=89);=E7=94=9F=E6=88=90=E5=99=A8tools/t5=5Fbuild?= =?UTF-8?q?=5Ftemplates.py=E5=90=AB=E7=94=9F=E6=88=90=E5=89=8D=E8=87=AA?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/core/adventure_runner.gd | 7 +- game/core/config_db.gd | 27 ++-- game/core/level_runner.gd | 32 ++++- game/core/meta_progression.gd | 2 + game/core/rng_service.gd | 2 +- game/core/save_manager.gd | 16 +++ game/data/schemas/level_table.gd | 94 ++++++++----- game/data/tables/level.tres | 164 ++--------------------- game/presentation/main.gd | 4 +- game/smoke/adventure_monte_carlo.gd | 1 + game/smoke/level_autoplay.gd | 12 +- game/smoke/smoke_all.gd | 78 +++++++++++ sim-data/t5_mc_layered_check.jsonl | 40 ++++++ tools/t5_build_templates.py | 199 ++++++++++++++++++++++++++++ 14 files changed, 471 insertions(+), 207 deletions(-) create mode 100644 sim-data/t5_mc_layered_check.jsonl create mode 100644 tools/t5_build_templates.py diff --git a/game/core/adventure_runner.gd b/game/core/adventure_runner.gd index 0a82bd82..2f267235 100644 --- a/game/core/adventure_runner.gd +++ b/game/core/adventure_runner.gd @@ -62,7 +62,8 @@ var _runner: LevelRunner # 当前关卡行进器(每关新建 var _pending_next_level := false # 大奖励已兑付、等 continue_to_next_level 确认 var _last_level_payload := {} # 最近一次关卡结束载荷(失败结算的败退节点明细来源——数值验证取数面) var _override := [0, 0] # TunePanel 百分比通道 [atk_pct, hp_pct](每关新 runner 都要应用) -var _base_override := {"atk": 0, "max_hp": 0, "armor_class": 0} # 永久强化 flat 注入(元进度实现批——冒险开始参数面 params.hero_override 存入,共享给 LevelRunner;全零 = 白板) +var _base_override := {"atk": 0, "max_hp": 0, "armor_class": 0} +var _first_script := false # 首次脚本模式(关卡节点图生成 v0.4——true = 全关零抽签取池内模板 0;测量夹具恒 false) # 永久强化 flat 注入(元进度实现批——冒险开始参数面 params.hero_override 存入,共享给 LevelRunner;全零 = 白板) func _init(db_ref, rng_ref) -> void: @@ -91,6 +92,7 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool: level_index = 1 _pending_next_level = false _base_override = _normalize_override(params.get("hero_override", {})) + _first_script = bool(params.get("first_script", false)) # 缺省 = 非首次(抽签路径;真实侧由呈现层读存档布尔位传入) ledger["hero_hp"] = int(db.hero.max_hp) + int(_base_override["max_hp"]) # 生命账本置满(表值+永久强化 flat——加算区刚归零) state = "running" return _start_current_level() @@ -164,7 +166,8 @@ func _start_current_level() -> bool: # 该回调重取 runner 实例),后 present_entry_choice()(入口候选呈现;顺序反了选路口会被呈现层 # 的 runner 空防御静默丢弃)。呈现零 RNG 消耗、pick 走同一 _enter_node——测量面逐位零漂移。 var ok: bool = _runner.start_level_external(level_index, ledger, bonus, purchased, - Callable(self, "apply_rest"), _base_override, true) + Callable(self, "apply_rest"), _base_override, true, + 0 if _first_script else -1) # 首次脚本 = 每关固定模板 0 零抽签(v0.4 拍板点①);否则逐关抽签 if ok: level_started.emit(level_index, String(_runner.level_row.get("display_name", ""))) _runner.present_entry_choice() diff --git a/game/core/config_db.gd b/game/core/config_db.gd index 21548327..d5dc1c81 100644 --- a/game/core/config_db.gd +++ b/game/core/config_db.gd @@ -169,15 +169,24 @@ func _cross_validate(loaded: Dictionary) -> void: en_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true for lrow in lv.rows: var li: int = int(lrow.level_index) - for node in lrow.nodes: - var t: String = String(node.get("type", "")) - if t != "battle" and t != "boss": - continue - var key := "%d_%d" % [li, int(node.get("node_depth", -1))] - if not bg_keys.has(key): - push_diag("error", "level_node_dangling", "level %d %s 节点 (%s) 在 board_gen 无精确行" % [li, t, key]) - if not en_keys.has(key): - push_diag("error", "level_node_dangling", "level %d %s 节点 (%s) 在 enemy 无精确行" % [li, t, key]) + # 模板池感知(关卡节点图生成 v0.4——池内全模板跨表校验,不只 row.nodes) + var node_lists: Array = [] + var tpls: Array = lrow.get("templates", []) + if tpls.is_empty(): + node_lists.append(lrow.get("nodes", [])) + else: + for tpl in tpls: + node_lists.append(tpl.get("nodes", [])) + for nodes_arr in node_lists: + for node in nodes_arr: + var t: String = String(node.get("type", "")) + if t != "battle" and t != "boss": + continue + var key := "%d_%d" % [li, int(node.get("node_depth", -1))] + if not bg_keys.has(key): + push_diag("error", "level_node_dangling", "level %d %s 节点 (%s) 在 board_gen 无精确行" % [li, t, key]) + if not en_keys.has(key): + push_diag("error", "level_node_dangling", "level %d %s 节点 (%s) 在 enemy 无精确行" % [li, t, key]) # ⑤ 技能池引用完整性(敌人技能池批——敌人表池 × 技能表;独立入口 validate_skill_pool_links 供冒烟直验) var skt = loaded.get("skill") if skt != null and en != null: diff --git a/game/core/level_runner.gd b/game/core/level_runner.gd index 2dd5b120..5bad590a 100644 --- a/game/core/level_runner.gd +++ b/game/core/level_runner.gd @@ -110,7 +110,7 @@ func restore_hero_base() -> void: # ---- 关卡生命周期 ---- -func start_level(level_index: int, seed_val: int, base_override_in: Dictionary = {}) -> bool: +func start_level(level_index: int, seed_val: int, base_override_in: Dictionary = {}, template_pick: int = -1) -> bool: # 单关测试模式(旧路径——既有语义一字不改):私有账本、内置恢复数学、自调 new_adventure(漂移① # 单关=冒险的历史简化在测试模式保留;正式种子域归 AdventureRunner.start_adventure) # base_override_in = 永久强化 flat 注入尾参(换算率标定批 2026-09-23——缺省 {} 全零白板, @@ -124,7 +124,6 @@ func start_level(level_index: int, seed_val: int, base_override_in: Dictionary = if row.is_empty(): push_error("[关卡节点地图] level_index %d 在 LevelTable 无行" % level_index) return false - level_row = row node_index = -1 level_result = "" _awaiting_choice = false @@ -143,6 +142,7 @@ func start_level(level_index: int, seed_val: int, base_override_in: Dictionary = break hero_hp = effective_max_hp() rng.new_adventure(seed_val) # 关卡起点 = 冒险起点(切片口径;跨关接续归 冒险结构) + level_row = _resolve_template_row(row, template_pick) # 池解析在播种后(node_map 抽签须已播种——EC7) _running = true var entry := _entry_node() if entry.is_empty(): @@ -166,7 +166,7 @@ func start_level(level_index: int, seed_val: int, base_override_in: Dictionary = # _enter_node(entry)——与旧自动进入逐位等价(测量面零漂移红线)。 func start_level_external(level_index: int, ledger: Dictionary, bonus_ref: Dictionary, purchased_ref: Dictionary, rest_handler: Callable, base_override_ref: Dictionary = {}, - defer_entry: bool = false) -> bool: + defer_entry: bool = false, template_pick: int = -1) -> bool: var row := _find_level_row(level_index) if row.is_empty(): push_error("[关卡节点地图] level_index %d 在 LevelTable 无行" % level_index) @@ -177,7 +177,7 @@ func start_level_external(level_index: int, ledger: Dictionary, bonus_ref: Dicti base_override = base_override_ref _shop_purchased = purchased_ref _rest_handler = rest_handler - level_row = row + level_row = _resolve_template_row(row, template_pick) node_index = -1 level_result = "" _awaiting_choice = false @@ -218,6 +218,30 @@ func _find_level_row(level_index: int) -> Dictionary: return {} +# ---- 模板池解析(关卡节点图生成 v0.4——M-3 迭代 5 T5)---- +# row.templates 非空 = 池形态:template_pick >= 0 = 强制取该模板(首次脚本模式/冒烟固定图,零 RNG 消费); +# -1 = 抽签(node_map 用途标签恰一 scope,均匀取模)。空池 = 旧单图形态原样返回(向后兼容零迁移)。 +# 解析产物 = row 副本换 nodes(下游 nodes()/校验/呈现全透明);active_template_name 供 MC 分层出数。 +var active_template_name := "" + +func _resolve_template_row(row: Dictionary, template_pick: int) -> Dictionary: + var templates: Array = row.get("templates", []) + active_template_name = "" + if templates.is_empty(): + return row + var idx := 0 + if template_pick >= 0: + idx = clampi(template_pick, 0, templates.size() - 1) + else: + var stream: RandomNumberGenerator = rng.open_stream("node_map") + idx = stream.randi_range(0, templates.size() - 1) + var tpl: Dictionary = templates[idx] + active_template_name = String(tpl.get("display_name", "")) + var out := row.duplicate(true) + out["nodes"] = tpl.get("nodes", []) + return out + + func nodes() -> Array: return level_row.get("nodes", []) diff --git a/game/core/meta_progression.gd b/game/core/meta_progression.gd index d0a0f141..3f8ae992 100644 --- a/game/core/meta_progression.gd +++ b/game/core/meta_progression.gd @@ -116,6 +116,8 @@ func refresh_workshop_unlocks() -> Array: # 明细 {big_reward, boss_drops: [{level, first, amount}], total, new_unlocks}; # 入账与标记经 save.apply_adventure_summary(boss_drops 副本随摘要入 recent_summaries)。 func settle_adventure(summary: Dictionary) -> Dictionary: + # 首次冒险布尔位置位(关卡节点图生成 v0.4——完成/失败/放弃三结局同经本收束挂点,写入只在收束) + save.mark_first_adventure_done() var drops: Array = [] for li in range(1, int(summary.get("levels_cleared", 0)) + 1): var first: bool = not save.boss_first_kill_marked(li) diff --git a/game/core/rng_service.gd b/game/core/rng_service.gd index 4e75a265..57da3383 100644 --- a/game/core/rng_service.gd +++ b/game/core/rng_service.gd @@ -14,7 +14,7 @@ extends Node # 用途标签封闭词表(并流前七流名全保留;新增用途走扩签通道 = 改代码 + 评审——清单封闭语义由「流」改「标签」) -const PURPOSE_TAGS := ["board_gen", "reward_roll", "encounter", "armor_roll", "behavior_roll", "variant_place", "gp_drop"] +const PURPOSE_TAGS := ["board_gen", "reward_roll", "encounter", "armor_roll", "behavior_roll", "variant_place", "gp_drop", "node_map"] # node_map = 关卡节点图生成 v0.4 扩签(M-3 迭代 5 T5——标签不参与派生只进事件日志,派生/黄金向量零改动) # 分段表达式书写(数据配置 R3 实现级规约——禁 ≥2^63 完整字面量) const GOLDEN_RATIO := (0x9E3779B9 << 32) | 0x7F4A7C15 # 0x9E3779B97F4A7C15 diff --git a/game/core/save_manager.gd b/game/core/save_manager.gd index f246dfd6..4d7bab6b 100644 --- a/game/core/save_manager.gd +++ b/game/core/save_manager.gd @@ -54,10 +54,23 @@ func _default_data() -> Dictionary: "level_clear_counts": {"1": 0, "2": 0, "3": 0}, "boss_first_kill": {}, "recent_summaries": [], + "first_adventure_done": false, }, } +# ---- 首次冒险布尔位(关卡节点图生成 v0.4——唯一新增存档字段,M-3 迭代 5 T5)---- +# false = 首次脚本模式(全关零抽签取池内模板 0=现行固定图);置位只在冒险收束挂点(MetaProgression.settle_adventure)。 +# 老档迁移:缺字段 = 该档已有游玩史 → true(不重放教学脚本)。 + +func is_first_adventure_done() -> bool: + return bool(_data["meta"].get("first_adventure_done", true)) + + +func mark_first_adventure_done() -> void: + _data["meta"]["first_adventure_done"] = true + + # ---- 读盘(启动时调用)---- # 返回 true = 已就绪可用(含新档/损坏兜底/迁移成功);false = 高版本拒读(默认数据运行、禁写盘)。 # 损坏兜底:原文件改名 .corrupt.bak(已存在则覆盖)+ push_warning + 默认数据继续——不静默丢档。 @@ -91,6 +104,9 @@ func load_from_disk() -> bool: parsed = stepped v = int(parsed.get("version", v + 1)) _data = _normalize(parsed) + # 老档迁移(关卡节点图生成 v0.4 布尔位——缺字段 = 已有游玩史,不重放教学脚本) + if not _data["meta"].has("first_adventure_done"): + _data["meta"]["first_adventure_done"] = true return true diff --git a/game/data/schemas/level_table.gd b/game/data/schemas/level_table.gd index 05f35ec1..20a10522 100644 --- a/game/data/schemas/level_table.gd +++ b/game/data/schemas/level_table.gd @@ -38,43 +38,65 @@ func validate() -> Array: var heal: int = int(row.get("rest_heal_pct", -1)) if heal < 0 or heal > 100: errs.append({"category": "domain", "message": "level %d rest_heal_pct 越域 [0,100]" % L}) - var nodes: Array = row.get("nodes", []) - if nodes.is_empty(): - errs.append({"category": "domain", "message": "level %d 节点链为空" % L}) + # 模板池(关卡节点图生成 v0.4——M-3 迭代 5 T5):row.templates 非空 = 池形态,池内全模板加载期校验 + # (违规致命——不只抽中的那张);空 = 旧单图形态(row.nodes 直读,向后兼容零迁移)。 + var node_lists: Array = [] # [ [上下文标签, nodes 数组], ... ] + var templates: Array = row.get("templates", []) + if templates.is_empty(): + node_lists.append(["", row.get("nodes", [])]) + else: + for j in templates.size(): + var tpl = templates[j] + if typeof(tpl) != TYPE_DICTIONARY or (tpl.get("nodes", []) as Array).is_empty(): + errs.append({"category": "domain", "message": "level %d 模板[%d] 缺 nodes 或为空" % [L, j]}) + continue + node_lists.append(["模板[%d]=%s" % [j, String(tpl.get("display_name", ""))], tpl.get("nodes", [])]) + for nl in node_lists: + for e in _validate_level_nodes(L, nl[0], nl[1]): + errs.append(e) + return errs + + +# 单节点链校验体(validate 抽出——row.nodes 与池内每张模板同跑一套;ctx = 消息上下文标签) +func _validate_level_nodes(L: int, ctx: String, nodes: Array) -> Array: + var errs: Array = [] + var tag: String = "" if ctx.is_empty() else "〔%s〕" % ctx + if nodes.is_empty(): + errs.append({"category": "domain", "message": "level %d%s 节点链为空" % [L, tag]}) + return errs + var boss_count := 0 + var battle_count := 0 + var is_graph := false + for node in nodes: + if node.has("next"): + is_graph = true + var t: String = String(node.get("type", "")) + if not NODE_TYPES.has(t): + errs.append({"category": "enum", "message": "level %d%s 未知节点类型: %s" % [L, tag, t]}) continue - var boss_count := 0 - var battle_count := 0 - var is_graph := false - for node in nodes: - if node.has("next"): - is_graph = true - var t: String = String(node.get("type", "")) - if not NODE_TYPES.has(t): - errs.append({"category": "enum", "message": "level %d 未知节点类型: %s" % [L, t]}) - continue - if t == "battle" or t == "boss": - if int(node.get("node_depth", -1)) < 0: - errs.append({"category": "domain", "message": "level %d %s 节点缺 node_depth" % [L, t]}) - battle_count += 1 - if t == "boss": - boss_count += 1 - if boss_count != 1: - errs.append({"category": "structural", "message": "level %d Boss 节点数 = %d(须恰一)" % [L, boss_count]}) - if battle_count < 2: - errs.append({"category": "structural", "message": "level %d 战斗节点(含 Boss)= %d(≥2 才构成关卡)" % [L, battle_count]}) - if not is_graph: - # 单链形态纪律(第一关零迁移):末节点必须是 Boss(关底收尾——冒险结构 关卡定义) - if String(nodes[nodes.size() - 1].get("type", "")) != "boss": - errs.append({"category": "structural", "message": "level %d 末节点非 Boss(单链以 Boss 收尾)" % L}) - # 图合法性(单链与图同一套算法——单链 = 出边恰一的退化形态,违规同样加载致命) - for e in _validate_graph(L, nodes, is_graph): - errs.append(e) + if t == "battle" or t == "boss": + if int(node.get("node_depth", -1)) < 0: + errs.append({"category": "domain", "message": "level %d%s %s 节点缺 node_depth" % [L, tag, t]}) + battle_count += 1 + if t == "boss": + boss_count += 1 + if boss_count != 1: + errs.append({"category": "structural", "message": "level %d%s Boss 节点数 = %d(须恰一)" % [L, tag, boss_count]}) + if battle_count < 2: + errs.append({"category": "structural", "message": "level %d%s 战斗节点(含 Boss)= %d(≥2 才构成关卡)" % [L, tag, battle_count]}) + if not is_graph: + # 单链形态纪律(第一关零迁移):末节点必须是 Boss(关底收尾——冒险结构 关卡定义) + if String(nodes[nodes.size() - 1].get("type", "")) != "boss": + errs.append({"category": "structural", "message": "level %d%s 末节点非 Boss(单链以 Boss 收尾)" % [L, tag]}) + # 图合法性(单链与图同一套算法——单链 = 出边恰一的退化形态,违规同样加载致命) + for e in _validate_graph(L, nodes, is_graph, tag): + errs.append(e) return errs # 图合法性核心(返回错误列表):id 解析(图=node_depth / 单链=数组下标)→ 出边表 → # 唯一性/前进方向/入口唯一/Boss 终点/双向可达六检。参考 关卡节点地图 v1.0 分支节点图规则。 -func _validate_graph(L: int, nodes: Array, is_graph: bool) -> Array: +func _validate_graph(L: int, nodes: Array, is_graph: bool, tag: String = "") -> Array: var errs: Array = [] var ids: Array = [] # 每节点 id(下标对齐) var nexts: Array = [] # 每节点出边 id 数组 @@ -83,11 +105,11 @@ func _validate_graph(L: int, nodes: Array, is_graph: bool) -> Array: var node: Dictionary = nodes[i] var nid: int = int(node.get("node_depth", i)) if is_graph else i if is_graph and not node.has("node_depth"): - errs.append({"category": "structural", "message": "level %d 图形态节点[%d] 缺 node_depth(全节点须编号化)" % [L, i]}) + errs.append({"category": "structural", "message": "level %d%s 图形态节点[%d] 缺 node_depth(全节点须编号化)" % [L, tag, i]}) nid = i # 降级继续(后续校验仍有意义) ids.append(nid) if id_to_idx.has(nid): - errs.append({"category": "dup_key", "message": "level %d 节点编号重复: %d(关卡内唯一定位地址)" % [L, nid]}) + errs.append({"category": "dup_key", "message": "level %d%s 节点编号重复: %d(关卡内唯一定位地址)" % [L, tag, nid]}) id_to_idx[nid] = i var nx: Array = [] if node.has("next"): @@ -100,9 +122,9 @@ func _validate_graph(L: int, nodes: Array, is_graph: bool) -> Array: for i in nodes.size(): for tid in nexts[i]: if not id_to_idx.has(tid): - errs.append({"category": "dangling_next", "message": "level %d 节点 %d 出边指向不存在编号: %d" % [L, ids[i], tid]}) + errs.append({"category": "dangling_next", "message": "level %d%s 节点 %d 出边指向不存在编号: %d" % [L, tag, ids[i], tid]}) elif tid <= ids[i]: - errs.append({"category": "structural", "message": "level %d 连线 %d→%d 违反前进方向(目标编号须严格更大)" % [L, ids[i], tid]}) + errs.append({"category": "structural", "message": "level %d%s 连线 %d→%d 违反前进方向(目标编号须严格更大)" % [L, tag, ids[i], tid]}) # ④ 入口唯一(无入边节点恰一——唯一入口) var has_inbound := {} for i in nodes.size(): @@ -113,7 +135,7 @@ func _validate_graph(L: int, nodes: Array, is_graph: bool) -> Array: if not has_inbound.has(ids[i]): entries.append(i) if entries.size() != 1: - errs.append({"category": "structural", "message": "level %d 入口节点数 = %d(须恰一——无入边节点唯一)" % [L, entries.size()]}) + errs.append({"category": "structural", "message": "level %d%s 入口节点数 = %d(须恰一——无入边节点唯一)" % [L, tag, entries.size()]}) # ⑤ Boss 恒唯一终点:无出边 + 编号最大(图形态;单链由"末节点 Boss"既有校验覆盖终点性) for i in nodes.size(): if String(nodes[i].get("type", "")) != "boss": diff --git a/game/data/tables/level.tres b/game/data/tables/level.tres index ce3c8f0c..4473278a 100644 --- a/game/data/tables/level.tres +++ b/game/data/tables/level.tres @@ -4,155 +4,17 @@ [resource] script = ExtResource("1") -rows = [{ -"level_index": 1, -"display_name": "第一层 · 地精洞窟", -"rest_heal_pct": 30, -"nodes": [{ -"node_depth": 0, -"type": "battle" -}, { -"node_depth": 1, -"type": "battle" -}, { -"type": "reward" -}, { -"node_depth": 2, -"type": "battle" -}, { -"node_depth": 3, -"type": "battle" -}, { -"type": "rest" -}, { -"node_depth": 4, -"type": "boss" -}] -}, { -"level_index": 2, -"display_name": "第二层 · 幽暗地域", -"rest_heal_pct": 30, -"nodes": [{ -"node_depth": 0, -"type": "battle", -"next": [1, 2] -}, { -"node_depth": 1, -"type": "battle", -"next": [3, 4] -}, { -"node_depth": 2, -"type": "reward", -"next": [4, 5] -}, { -"node_depth": 3, -"type": "rest", -"next": [6] -}, { -"node_depth": 4, -"type": "battle", -"next": [6, 7] -}, { -"node_depth": 5, -"type": "battle", -"next": [7] -}, { -"node_depth": 6, -"type": "battle", -"next": [8] -}, { -"node_depth": 7, -"type": "battle", -"next": [8] -}, { -"node_depth": 8, -"type": "shop", -"next": [9] -}, { -"node_depth": 9, -"type": "boss" -}] -}, { -"level_index": 3, -"display_name": "第三层 · 灰矮人矿城", -"rest_heal_pct": 30, -"nodes": [{ -"node_depth": 0, -"type": "battle", -"next": [1, 2] -}, { -"node_depth": 1, -"type": "battle", -"next": [3, 4] -}, { -"node_depth": 2, -"type": "reward", -"next": [4, 5] -}, { -"node_depth": 3, -"type": "rest", -"next": [6] -}, { -"node_depth": 4, -"type": "battle", -"next": [6, 7] -}, { -"node_depth": 5, -"type": "battle", -"next": [7] -}, { -"node_depth": 6, -"type": "battle", -"next": [8] -}, { -"node_depth": 7, -"type": "battle", -"next": [8] -}, { -"node_depth": 8, -"type": "shop", -"next": [9] -}, { -"node_depth": 9, -"type": "boss" -}] -}] +rows = [ +{"level_index": 1, "display_name": "第一层 · 地精洞窟", "rest_heal_pct": 30, "templates": [{"display_name": "地精洞窟·主线洞道(现行)", "nodes": [{"node_depth": 0, "type": "battle"}, {"node_depth": 1, "type": "battle"}, {"type": "reward"}, {"node_depth": 2, "type": "battle"}, {"node_depth": 3, "type": "battle"}, {"type": "rest"}, {"node_depth": 4, "type": "boss"}]}, {"display_name": "地精洞窟·休整前移", "nodes": [{"node_depth": 0, "type": "battle"}, {"node_depth": 1, "type": "battle"}, {"type": "rest"}, {"node_depth": 2, "type": "battle"}, {"type": "reward"}, {"node_depth": 3, "type": "battle"}, {"node_depth": 4, "type": "boss"}]}, {"display_name": "地精洞窟·先赏后战", "nodes": [{"node_depth": 0, "type": "battle"}, {"type": "reward"}, {"node_depth": 1, "type": "battle"}, {"type": "rest"}, {"node_depth": 2, "type": "battle"}, {"node_depth": 3, "type": "battle"}, {"node_depth": 4, "type": "boss"}]}, {"display_name": "地精洞窟·连战直入", "nodes": [{"node_depth": 0, "type": "battle"}, {"node_depth": 1, "type": "battle"}, {"type": "reward"}, {"type": "rest"}, {"node_depth": 2, "type": "battle"}, {"node_depth": 3, "type": "battle"}, {"node_depth": 4, "type": "boss"}]}, {"display_name": "地精洞窟·缓入长坡", "nodes": [{"node_depth": 0, "type": "battle"}, {"type": "rest"}, {"node_depth": 1, "type": "battle"}, {"type": "reward"}, {"node_depth": 2, "type": "battle"}, {"node_depth": 3, "type": "battle"}, {"node_depth": 4, "type": "boss"}]}]}, +{"level_index": 2, "display_name": "第二层 · 幽暗地域", "rest_heal_pct": 30, "templates": [{"display_name": "幽暗地域·四岔环廊(现行)", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 4]}, {"node_depth": 2, "type": "reward", "next": [4, 5]}, {"node_depth": 3, "type": "rest", "next": [6]}, {"node_depth": 4, "type": "battle", "next": [6, 7]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [8]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "幽暗地域·奖励窄门", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 4]}, {"node_depth": 2, "type": "reward", "next": [4]}, {"node_depth": 3, "type": "rest", "next": [5]}, {"node_depth": 4, "type": "battle", "next": [5, 6]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [8]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "幽暗地域·直入深层", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [4]}, {"node_depth": 2, "type": "reward", "next": [3, 4]}, {"node_depth": 3, "type": "rest", "next": [6]}, {"node_depth": 4, "type": "battle", "next": [5, 6]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [7]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "幽暗地域·休整远路", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [2, 4]}, {"node_depth": 2, "type": "reward", "next": [3]}, {"node_depth": 3, "type": "rest", "next": [6]}, {"node_depth": 4, "type": "battle", "next": [5, 6]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [8]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "幽暗地域·双线归束", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3]}, {"node_depth": 2, "type": "reward", "next": [3, 4]}, {"node_depth": 3, "type": "rest", "next": [4, 5]}, {"node_depth": 4, "type": "battle", "next": [6]}, {"node_depth": 5, "type": "battle", "next": [6]}, {"node_depth": 6, "type": "battle", "next": [7]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}]}, +{"level_index": 3, "display_name": "第三层 · 灰矮人矿城", "rest_heal_pct": 30, "templates": [{"display_name": "灰矮人矿城·主竖井(现行)", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 4]}, {"node_depth": 2, "type": "reward", "next": [4, 5]}, {"node_depth": 3, "type": "rest", "next": [6]}, {"node_depth": 4, "type": "battle", "next": [6, 7]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [8]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "灰矮人矿城·奖励窄门", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 4]}, {"node_depth": 2, "type": "reward", "next": [4]}, {"node_depth": 3, "type": "rest", "next": [5]}, {"node_depth": 4, "type": "battle", "next": [5, 6]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [8]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "灰矮人矿城·直入深层", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [4]}, {"node_depth": 2, "type": "reward", "next": [3, 4]}, {"node_depth": 3, "type": "rest", "next": [6]}, {"node_depth": 4, "type": "battle", "next": [5, 6]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [7]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "灰矮人矿城·休整远路", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [2, 4]}, {"node_depth": 2, "type": "reward", "next": [3]}, {"node_depth": 3, "type": "rest", "next": [6]}, {"node_depth": 4, "type": "battle", "next": [5, 6]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [8]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "灰矮人矿城·双线归束", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3]}, {"node_depth": 2, "type": "reward", "next": [3, 4]}, {"node_depth": 3, "type": "rest", "next": [4, 5]}, {"node_depth": 4, "type": "battle", "next": [6]}, {"node_depth": 5, "type": "battle", "next": [6]}, {"node_depth": 6, "type": "battle", "next": [7]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}]} +] -; 切片登记(provisional——C1/C3 未设计,结构位与数值均为 SLICE-TUNED): -; 节点链 = 用户 2026-09-11 拍板的线性第一关(战×2 → 随机主动道具 → 战×2 → 血量恢复点 → Boss)。 -; node_depth 轴 = 节点深度(难度 = f(关卡序号, 节点深度)——game-concept 锁定口径);Boss = Dmax 行(A1 第三轴登记约定)。 -; rest_heal_pct = 30:恢复点回复 max_hp 的百分比(随生命天赋同步放大,防高生命档恢复失效)。 -; Boss 板以 12×12 加密板区分。 -; -; 分支节点图批(2026-09-17 用户拍板——关卡节点地图 v1.0 图版第一次落地,P1 批): -; L2 行集 = 网状图首落地:入口层 1 节点(0) → 中间三层(2/3/2 节点) → Boss 层 1 节点(8),总 9 节点; -; 真实岔路 4 处(0→[1,2] 入口分流 · 1→[3,4] 恢复或战 · 2→[4,5] 奖励后分流 · 4→[6,7] 深层分流); -; 含恢复节点 1(节点 3,rest_heal_pct 行字段兑付)与奖励节点 1(节点 2,直通空态——功能道具渠道 P1 未定); -; 普通战斗节点 6(0/1/4/5/6/7)+ Boss(8,编号最大、恒唯一终点)。连线只朝前进方向(编号严格递增), -; 单入口(节点 0 无入边),全部节点沿连线可达 Boss——图合法性加载校验强制(level_table.gd)。 -; L1 单链零迁移:无 next 键 = 单链形态(隐式出边 = 数组顺序下一项),经同一套图校验(退化形态)。 -; 敌种生态 = DND 幽暗地域(Underdark)方向:熊地精兄弟(bugbear 复用,池差异化改造)+ 巨蜘蛛 -; giant_spider(新敌种,群行倾向)+ 蛛化卓尔精灵 drider(Boss 专属池,蜘蛛主题呼应)。行值全 provisional, -; 投值归 难度曲线与Boss。 -; -; 层名改名批(2026-09-20 用户拍板):第一层「苔藓回廊」退役(湿地植物意象与地精巢穴不契合), -; 改名「裂颚洞窟」——致敬 DND 5e《凡戴尔的失落矿坑》地精部族「裂颚」(Cragmaw)的洞窟巢穴: -; 地精盘踞的浅层洞窟、通道向下延伸,天然连通第二层幽暗地域。仅 display_name 字面变更, -; 节点结构 / 行值零改动。 -; -; 层名二改批(2026-09-20 用户拍板):「裂颚洞窟」→「地精洞窟」——不绑定 DND 专名部族 -; (规避 IP 指涉),直取地精巢穴意象;洞窟纵深连通幽暗地域的取向不变。 -; 仅 display_name 字面变更,节点结构 / 行值零改动。 -; -; 第三关内容批(2026-09-20 用户拍板走 proposal-level3-content-2026-09-20.md 推荐): -; L3 行集复用 L2 九节点图模板(6 战 + Boss + 奖励 + 恢复,真实岔路 4 处);灰矮人矿城主题 -; (DND 幽暗地域最深层方向)。仅行集落地,正式三关序列推进仍登记 Alpha 验收条件 -; (测试直入口 ≠ 序列生效)。行值全 provisional 归 难度曲线与Boss。 -; -; 商店房批(2026-09-20 用户拍板——GP+商店房实施批,关卡节点地图 v1.1 四类→五类):编排规则锚 = -; 每关恰 1 商店房、Boss 前一位必经位(参考哈迪斯商店房位置);L2/L3 行集 rewiring:原 Boss 行 (8, boss) -; 改为 (8, shop, next [9])、新增 (9, boss)——图六检全绿(Boss 恒最大编号 9、无出边恒终点;节点 6/7 出边 -; [8] 字面不变、指向现为商店房,全路径穿商店房 8);L1 行集零改动(教学期最小修订面,L1 掉落攒进 L2 首店)。 -; 必经位(全路径穿 8)由冒烟断言承载、非加载校验——level_table.gd 零新增图合法性检查。 -; 连带:board_gen/enemy 补 (2,9)/(3,9) 精确行(跨表校验 ④ 强制,行值照抄原 Boss (L,8) 行)。 -; 行值全 provisional 归 难度曲线与Boss。 +; 关卡节点图生成 v0.4 台阶一内容批(M-3 迭代 5 T5,2026-09-28——15 张骨架 = 3 关 × 5,拍板点④): +; 模板 0 = 现行固定图(回滚档成员——首次冒险脚本/池缩 N=1 时逐位等价迁移前行为); +; L1 五模板 = 单链排列变体(图形态两不可全:Boss 须最大编号 vs Boss 遭遇深度 4——战斗深度序单调 +; 保持难度坡,变化面 = 奖励/恢复位次);L2/L3 五模板 = 网状图变体(战斗节点编号恒 ∈ 遭遇表行集 +; {0,1,4,5,6,7}、Boss 9 最大编号、商店房 8→Boss 必经位承袭;遭遇表按编号共用——零新增敌人数值)。 +; 池内全模板加载期校验(level_table.gd validate + config_db 跨表校验模板感知); +; 行为读 LevelRunner._resolve_template_row(node_map 标签恰一 scope 抽签 / template_pick 强制)。 +; 生成器 = tools/t5_build_templates.py(生成前自校验,一次性内容批工具随批入库留档)。 diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 098c5ca7..3db580ee 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -242,7 +242,9 @@ func _start_adventure() -> void: logger.start_session(seed_val) adventure.set_hero_override(tune_panel.atk_pct(), tune_panel.hp_pct()) # 永久强化注入(元进度实现批):冒险开始参数面——meta 现算 override 显式传参(全零 = 白板) - if not adventure.start_adventure(seed_val, {"hero_override": meta.hero_override() if meta != null else {}}): + # 首次冒险脚本(关卡节点图生成 v0.4):存档布尔位 false = 全关固定模板 0(教学重放);置位后逐关抽签 + var first_script: bool = not save_manager.is_first_adventure_done() + if not adventure.start_adventure(seed_val, {"hero_override": meta.hero_override() if meta != null else {}, "first_script": first_script}): _fatal_screen("冒险启动失败——见输出日志") return diff --git a/game/smoke/adventure_monte_carlo.gd b/game/smoke/adventure_monte_carlo.gd index 28b73153..84f5b281 100644 --- a/game/smoke/adventure_monte_carlo.gd +++ b/game/smoke/adventure_monte_carlo.gd @@ -67,6 +67,7 @@ func _initialize() -> void: "encounter_max_hp_seq": st["encounter_max_hp_seq"], "reward_events": st["reward_events"], "shop_gp_seq": st["shop_gp_seq"], + "level_templates": st.get("level_templates", []), "wallet_count": (st["wallet_events"] as Array).size(), } var rs := String(sum.get("result", "")) diff --git a/game/smoke/level_autoplay.gd b/game/smoke/level_autoplay.gd index 509cd9c3..5c2ead70 100644 --- a/game/smoke/level_autoplay.gd +++ b/game/smoke/level_autoplay.gd @@ -25,7 +25,7 @@ extends RefCounted const AutoPlayer := preload("res://smoke/auto_player.gd") # O4 收口:自动玩家策略单一实现 -static func run_level(db, seed_val: int, level_index: int, atk_pct: int, hp_pct: int, collect_rows: bool = false, logger = null, policy: String = "normal", route: String = "min") -> Dictionary: +static func run_level(db, seed_val: int, level_index: int, atk_pct: int, hp_pct: int, collect_rows: bool = false, logger = null, policy: String = "normal", route: String = "min", template_pick: int = -1) -> Dictionary: var own_session := logger == null if own_session: logger = load("res://presentation/playtest_logger.gd").new() @@ -47,7 +47,7 @@ static func run_level(db, seed_val: int, level_index: int, atk_pct: int, hp_pct: runner.node_entered.connect(_on_node_entered.bind(runner, st, logger)) runner.level_finished.connect(_on_level_finished.bind(st, logger)) runner.set_hero_override(atk_pct, hp_pct) - if not runner.start_level(level_index, seed_val): + if not runner.start_level(level_index, seed_val, {}, template_pick): st.result = "start_failed" runner.restore_hero_base() if own_session: @@ -118,6 +118,8 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_ "visited_ids": [], "choices": 0, "last_battle_enemies": [], "summary": {}, "wallet_events": [], "encounter_max_hp_seq": [], "reward_events": [], "shop_gp_seq": [], # 商店房到达余额序(逐关 1 次——GP 跨关延续验收取数面) + + "level_templates": [], # 逐关图模板名(关卡节点图生成 v0.4 分层出数) } adr.step_row.connect(_on_step_row.bind(adr, st, collect_rows)) adr.node_entered.connect(_on_adventure_node_entered.bind(adr, st, logger)) @@ -125,7 +127,7 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_ adr.big_reward_paid.connect(_on_adventure_reward.bind(st, adr)) adr.adventure_finished.connect(_on_adventure_finished.bind(st, logger)) adr.set_hero_override(atk_pct, hp_pct) - if not adr.start_adventure(seed_val, {"hero_override": hero_override}): + if not adr.start_adventure(seed_val, {"hero_override": hero_override, "first_script": false}): # 测量夹具恒抽签(v0.4——首次脚本分支不进任何读数) st.result = "start_failed" if own_session: logger.close_session() @@ -152,6 +154,10 @@ static func _on_adventure_node_entered(node: Dictionary, info: Dictionary, adr, var r = adr.runner() var t := String(node.get("type", "")) st.visited_ids.append(int(r.node_id_of(node))) + if not st.has("level_templates"): + st["level_templates"] = [] + st["level_templates"].append(r.active_template_name) # MC 分层出数(关卡×图模板——v0.4 测量对账条款) + if t == "battle" or t == "boss": st.battles += 1 st.last_battle_enemies = [] # 末场战斗敌方名序列按战斗重置(失败局终值 = 死亡遭遇) diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index 7231d73f..a7f519fb 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -107,6 +107,84 @@ func _run_core_smoke(db, rng) -> void: _smoke_adventure() _smoke_save() _smoke_meta() + _smoke_node_map_templates() + + +# ---- 关卡节点图生成·模板池冒烟(M-3 迭代 5 T5——v0.4 §P0.6 断言面)---- +func _smoke_node_map_templates() -> void: + # ① 扩签:node_map 入用途标签封闭词表(派生零改动——标签只进事件日志) + var svc = load("res://core/rng_service.gd").new() + check((svc.PURPOSE_TAGS as Array).has("node_map"), "node_map 标签入 rng_service 封闭词表(扩签通道=改代码+冒烟守卫)") + # ② 池参数:三关各 5 模板、模板 0 存在(回滚档成员) + for li in range(1, 4): + var row := {} + for r in _db.level.rows: + if int(r.level_index) == li: + row = r + var tpls: Array = row.get("templates", []) + check(tpls.size() == 5, "L%d 模板池 = 5 张(拍板点④——首版池规模)" % li) + check(not tpls.is_empty() and String(tpls[0].get("display_name", "")).ends_with("(现行)"), + "L%d 模板 0 = 现行固定图(回滚档成员)" % li) + # ③ 强制取 0 = 模板 0 节点表逐位相等 + 零 scope 消费 + var rng0 = load("res://core/rng_service.gd").new() + rng0.new_adventure(910001) + var runner0 = load("res://core/level_runner.gd").new(_db, rng0) + var c_before: int = rng0.counter() + check(runner0.start_level_external(2, {"hero_hp": 20, "gp": 0}, {}, {}, + Callable(self, "_smoke_rest_stub"), {}, true, 0), "强制 template_pick=0 起关受理") + check(rng0.counter() == c_before, "强制取模板零 RNG 消耗(零抽签——首次脚本语义)") + var tpl0: Dictionary = {} + for r in _db.level.rows: + if int(r.level_index) == 2: + tpl0 = r.get("templates", [])[0] + check(runner0.level_row.get("nodes", []) == tpl0.get("nodes", []), + "强制 0 的生效节点表 = 模板 0 逐位相等(回滚档成立)") + # ④ 抽签确定性:同种子两实例同模板(同种子同操作序列 → 逐位同图) + var names := [] + for i in range(2): + var rg = load("res://core/rng_service.gd").new() + rg.new_adventure(910002) + var rn = load("res://core/level_runner.gd").new(_db, rg) + rn.start_level_external(2, {"hero_hp": 20, "gp": 0}, {}, {}, + Callable(self, "_smoke_rest_stub"), {}, true, -1) + names.append(rn.active_template_name) + check(names[0] == names[1] and String(names[0]) != "", + "抽签确定性:同种子逐位同图(实测 %s)" % String(names[0])) + # ⑤ N=1 池与现行固定图等价:单模板行过图校验(抽签取模 1 恒 0 = 回滚档) + var lt = load("res://data/schemas/level_table.gd").new() + lt.rows = [{"level_index": 9, "display_name": "t", "rest_heal_pct": 30, + "templates": [tpl0]}] + check(lt.validate().is_empty(), "N=1 池表通过图校验(回滚档形态合法)") + # ⑥ 首次脚本分支:first_script=true 全关模板 0;false = 抽签(scope 恰一差异) + var rngF = load("res://core/rng_service.gd").new() + rngF.new_adventure(910003) + var adrF = load("res://core/adventure_runner.gd").new(_db, rngF) + adrF.start_adventure(910003, {"first_script": true}) + check(String(adrF.runner().active_template_name).ends_with("(现行)"), + "首次脚本模式:L1 取模板 0(全关零抽签——存档布尔位 false 语义)") + var cF: int = rngF.counter() + var rngT = load("res://core/rng_service.gd").new() + rngT.new_adventure(910003) + var adrT = load("res://core/adventure_runner.gd").new(_db, rngT) + adrT.start_adventure(910003, {"first_script": false}) + check(rngT.counter() == cF + 1, "抽签模式恰多消费 1 scope(node_map 逐关恰一——L1 起关抽签)") + check(String(adrT.runner().active_template_name) != "", + "抽签模式 active_template_name 有值(MC 分层出数取数面在位)") + # ⑦ 收束置位:新档 false → settle 置 true 恰一次(幂等) + DirAccess.remove_absolute("user://smoke_node_map_save.json") # 测试隔离——上轮 settle 写盘的残留档先删 + var sv = load("res://core/save_manager.gd").new("user://smoke_node_map_save.json") + sv.load_from_disk() + check(sv.is_first_adventure_done() == false, "新档首次冒险布尔位 = false(默认域)") + var mp = load("res://core/meta_progression.gd").new(_db, sv) + mp.settle_adventure({"levels_cleared": 1, "keystone_total": 10, "cleared": false, "result": "failed"}) + check(sv.is_first_adventure_done() == true, "冒险收束点置位(三结局同写——写入只在收束挂点)") + mp.settle_adventure({"levels_cleared": 0, "keystone_total": 0, "cleared": false, "result": "failed"}) + check(sv.is_first_adventure_done() == true, "置位幂等(重复收束不翻转)") + + +# 冒烟恢复执行体(LevelRunner 注入用——与 AdventureRunner.apply_rest 同式占位) +func _smoke_rest_stub(pct: int, hp: int, max_hp: int) -> int: + return mini(int(floor(float(max_hp) * float(pct) / 100.0)), max_hp - hp) # ---- 冒险结构实现批冒烟(2026-09-21——AdventureTable 建表 + AdventureRunner + 冒险验收断言)---- diff --git a/sim-data/t5_mc_layered_check.jsonl b/sim-data/t5_mc_layered_check.jsonl new file mode 100644 index 00000000..18d0e584 --- /dev/null +++ b/sim-data/t5_mc_layered_check.jsonl @@ -0,0 +1,40 @@ +{"atk_pct":0,"battles":5,"choices":6,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20],"hero_dmg":29,"hp_pct":0,"keystone_total":0,"level_templates":["地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡"],"levels_cleared":0,"mine_hits":1,"policy":"normal","refreshes":0,"result":"failed","reward_events":[],"rounds":24,"route":"min","seed":78000,"shop_gp_seq":[],"stopped_at":1,"visited_count":7,"wallet_count":6} +{"atk_pct":0,"battles":9,"choices":9,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25],"hero_dmg":57,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","幽暗地域·直入深层","幽暗地域·直入深层","幽暗地域·直入深层","幽暗地域·直入深层"],"levels_cleared":1,"mine_hits":3,"policy":"normal","refreshes":0,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":44,"route":"min","seed":78001,"shop_gp_seq":[],"stopped_at":2,"visited_count":11,"wallet_count":10} +{"atk_pct":0,"battles":4,"choices":4,"cleared":false,"encounter_max_hp_seq":[20,20,20,20],"hero_dmg":18,"hp_pct":0,"keystone_total":0,"level_templates":["地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)"],"levels_cleared":0,"mine_hits":0,"policy":"normal","refreshes":0,"result":"failed","reward_events":[],"rounds":10,"route":"min","seed":78002,"shop_gp_seq":[],"stopped_at":1,"visited_count":5,"wallet_count":5} +{"atk_pct":0,"battles":8,"choices":9,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25],"hero_dmg":74,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","幽暗地域·奖励窄门","幽暗地域·奖励窄门","幽暗地域·奖励窄门","幽暗地域·奖励窄门"],"levels_cleared":1,"mine_hits":3,"policy":"normal","refreshes":0,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":56,"route":"min","seed":78003,"shop_gp_seq":[],"stopped_at":2,"visited_count":11,"wallet_count":9} +{"atk_pct":0,"battles":4,"choices":5,"cleared":false,"encounter_max_hp_seq":[20,20,20,20],"hero_dmg":16,"hp_pct":0,"keystone_total":0,"level_templates":["地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡"],"levels_cleared":0,"mine_hits":5,"policy":"normal","refreshes":0,"result":"failed","reward_events":[],"rounds":10,"route":"min","seed":78004,"shop_gp_seq":[],"stopped_at":1,"visited_count":6,"wallet_count":5} +{"atk_pct":0,"battles":5,"choices":6,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20],"hero_dmg":28,"hp_pct":0,"keystone_total":0,"level_templates":["地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)"],"levels_cleared":0,"mine_hits":2,"policy":"normal","refreshes":0,"result":"failed","reward_events":[],"rounds":18,"route":"min","seed":78005,"shop_gp_seq":[],"stopped_at":1,"visited_count":7,"wallet_count":6} +{"atk_pct":0,"battles":3,"choices":3,"cleared":false,"encounter_max_hp_seq":[20,20,20],"hero_dmg":24,"hp_pct":0,"keystone_total":0,"level_templates":["地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移"],"levels_cleared":0,"mine_hits":2,"policy":"normal","refreshes":0,"result":"failed","reward_events":[],"rounds":15,"route":"min","seed":78006,"shop_gp_seq":[],"stopped_at":1,"visited_count":4,"wallet_count":4} +{"atk_pct":0,"battles":5,"choices":6,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20],"hero_dmg":17,"hp_pct":0,"keystone_total":0,"level_templates":["地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战"],"levels_cleared":0,"mine_hits":2,"policy":"normal","refreshes":0,"result":"failed","reward_events":[],"rounds":19,"route":"min","seed":78007,"shop_gp_seq":[],"stopped_at":1,"visited_count":7,"wallet_count":6} +{"atk_pct":0,"battles":5,"choices":6,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20],"hero_dmg":26,"hp_pct":0,"keystone_total":0,"level_templates":["地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡"],"levels_cleared":0,"mine_hits":4,"policy":"normal","refreshes":0,"result":"failed","reward_events":[],"rounds":23,"route":"min","seed":78008,"shop_gp_seq":[],"stopped_at":1,"visited_count":7,"wallet_count":6} +{"atk_pct":0,"battles":5,"choices":6,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20],"hero_dmg":36,"hp_pct":0,"keystone_total":0,"level_templates":["地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入"],"levels_cleared":0,"mine_hits":2,"policy":"normal","refreshes":0,"result":"failed","reward_events":[],"rounds":27,"route":"min","seed":78009,"shop_gp_seq":[],"stopped_at":1,"visited_count":7,"wallet_count":6} +{"atk_pct":0,"battles":4,"choices":5,"cleared":false,"encounter_max_hp_seq":[20,20,20,20],"hero_dmg":28,"hp_pct":0,"keystone_total":0,"level_templates":["地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡"],"levels_cleared":0,"mine_hits":1,"policy":"normal","refreshes":0,"result":"failed","reward_events":[],"rounds":10,"route":"min","seed":78010,"shop_gp_seq":[],"stopped_at":1,"visited_count":6,"wallet_count":5} +{"atk_pct":0,"battles":3,"choices":4,"cleared":false,"encounter_max_hp_seq":[20,20,20],"hero_dmg":24,"hp_pct":0,"keystone_total":0,"level_templates":["地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战"],"levels_cleared":0,"mine_hits":1,"policy":"normal","refreshes":0,"result":"failed","reward_events":[],"rounds":14,"route":"min","seed":78011,"shop_gp_seq":[],"stopped_at":1,"visited_count":5,"wallet_count":4} +{"atk_pct":0,"battles":4,"choices":5,"cleared":false,"encounter_max_hp_seq":[20,20,20,20],"hero_dmg":23,"hp_pct":0,"keystone_total":0,"level_templates":["地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入"],"levels_cleared":0,"mine_hits":2,"policy":"normal","refreshes":0,"result":"failed","reward_events":[],"rounds":11,"route":"min","seed":78012,"shop_gp_seq":[],"stopped_at":1,"visited_count":6,"wallet_count":5} +{"atk_pct":0,"battles":11,"choices":13,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25,25,25],"hero_dmg":73,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束"],"levels_cleared":1,"mine_hits":1,"policy":"normal","refreshes":0,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":52,"route":"min","seed":78013,"shop_gp_seq":[69],"stopped_at":2,"visited_count":15,"wallet_count":12} +{"atk_pct":0,"battles":10,"choices":12,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25,25],"hero_dmg":76,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","幽暗地域·奖励窄门","幽暗地域·奖励窄门","幽暗地域·奖励窄门","幽暗地域·奖励窄门","幽暗地域·奖励窄门","幽暗地域·奖励窄门","幽暗地域·奖励窄门"],"levels_cleared":1,"mine_hits":6,"policy":"normal","refreshes":0,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":48,"route":"min","seed":78014,"shop_gp_seq":[76],"stopped_at":2,"visited_count":14,"wallet_count":11} +{"atk_pct":0,"battles":3,"choices":3,"cleared":false,"encounter_max_hp_seq":[20,20,20],"hero_dmg":27,"hp_pct":0,"keystone_total":0,"level_templates":["地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移"],"levels_cleared":0,"mine_hits":0,"policy":"normal","refreshes":0,"result":"failed","reward_events":[],"rounds":6,"route":"min","seed":78015,"shop_gp_seq":[],"stopped_at":1,"visited_count":4,"wallet_count":4} +{"atk_pct":0,"battles":9,"choices":9,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25],"hero_dmg":56,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","幽暗地域·直入深层","幽暗地域·直入深层","幽暗地域·直入深层","幽暗地域·直入深层"],"levels_cleared":1,"mine_hits":4,"policy":"normal","refreshes":0,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":49,"route":"min","seed":78016,"shop_gp_seq":[],"stopped_at":2,"visited_count":11,"wallet_count":10} +{"atk_pct":0,"battles":9,"choices":10,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25],"hero_dmg":60,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","幽暗地域·奖励窄门","幽暗地域·奖励窄门","幽暗地域·奖励窄门","幽暗地域·奖励窄门","幽暗地域·奖励窄门"],"levels_cleared":1,"mine_hits":1,"policy":"normal","refreshes":0,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":45,"route":"min","seed":78017,"shop_gp_seq":[],"stopped_at":2,"visited_count":12,"wallet_count":10} +{"atk_pct":0,"battles":7,"choices":7,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25],"hero_dmg":55,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","幽暗地域·直入深层","幽暗地域·直入深层"],"levels_cleared":1,"mine_hits":3,"policy":"normal","refreshes":0,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":31,"route":"min","seed":78018,"shop_gp_seq":[],"stopped_at":2,"visited_count":9,"wallet_count":8} +{"atk_pct":0,"battles":7,"choices":7,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25],"hero_dmg":42,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","幽暗地域·奖励窄门","幽暗地域·奖励窄门"],"levels_cleared":1,"mine_hits":4,"policy":"normal","refreshes":0,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":35,"route":"min","seed":78019,"shop_gp_seq":[],"stopped_at":2,"visited_count":9,"wallet_count":8} +{"atk_pct":0,"battles":5,"choices":6,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20],"hero_dmg":23,"hp_pct":0,"keystone_total":0,"level_templates":["地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入"],"levels_cleared":0,"mine_hits":0,"policy":"perfect","refreshes":2,"result":"failed","reward_events":[],"rounds":21,"route":"min","seed":78020,"shop_gp_seq":[],"stopped_at":1,"visited_count":7,"wallet_count":6} +{"atk_pct":0,"battles":5,"choices":6,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20],"hero_dmg":30,"hp_pct":0,"keystone_total":0,"level_templates":["地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入"],"levels_cleared":0,"mine_hits":0,"policy":"perfect","refreshes":1,"result":"failed","reward_events":[],"rounds":20,"route":"min","seed":78021,"shop_gp_seq":[],"stopped_at":1,"visited_count":7,"wallet_count":6} +{"atk_pct":0,"battles":11,"choices":13,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25,30,30],"hero_dmg":91,"hp_pct":0,"keystone_total":20,"level_templates":["地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","幽暗地域·休整远路","幽暗地域·休整远路","幽暗地域·休整远路","幽暗地域·休整远路","幽暗地域·休整远路","幽暗地域·休整远路","幽暗地域·休整远路","灰矮人矿城·主竖井(现行)","灰矮人矿城·主竖井(现行)"],"levels_cleared":2,"mine_hits":0,"policy":"perfect","refreshes":3,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25},{"from_level":2,"hp_bonus":5,"keystone_gained":10,"keystone_total":20,"new_max_hp":30}],"rounds":60,"route":"min","seed":78022,"shop_gp_seq":[59],"stopped_at":3,"visited_count":16,"wallet_count":12} +{"atk_pct":0,"battles":7,"choices":7,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25],"hero_dmg":55,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)"],"levels_cleared":1,"mine_hits":0,"policy":"perfect","refreshes":3,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":42,"route":"min","seed":78023,"shop_gp_seq":[],"stopped_at":2,"visited_count":9,"wallet_count":8} +{"atk_pct":0,"battles":8,"choices":8,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25],"hero_dmg":69,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","幽暗地域·直入深层","幽暗地域·直入深层","幽暗地域·直入深层"],"levels_cleared":1,"mine_hits":0,"policy":"perfect","refreshes":2,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":46,"route":"min","seed":78024,"shop_gp_seq":[],"stopped_at":2,"visited_count":10,"wallet_count":9} +{"atk_pct":0,"battles":10,"choices":11,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25,25],"hero_dmg":64,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束"],"levels_cleared":1,"mine_hits":0,"policy":"perfect","refreshes":2,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":52,"route":"min","seed":78025,"shop_gp_seq":[],"stopped_at":2,"visited_count":13,"wallet_count":11} +{"atk_pct":0,"battles":11,"choices":13,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25,25,25],"hero_dmg":105,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束"],"levels_cleared":1,"mine_hits":0,"policy":"perfect","refreshes":2,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":58,"route":"min","seed":78026,"shop_gp_seq":[90],"stopped_at":2,"visited_count":15,"wallet_count":12} +{"atk_pct":0,"battles":11,"choices":12,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25,25,25],"hero_dmg":98,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","幽暗地域·直入深层","幽暗地域·直入深层","幽暗地域·直入深层","幽暗地域·直入深层","幽暗地域·直入深层","幽暗地域·直入深层","幽暗地域·直入深层"],"levels_cleared":1,"mine_hits":0,"policy":"perfect","refreshes":4,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":71,"route":"min","seed":78027,"shop_gp_seq":[95],"stopped_at":2,"visited_count":14,"wallet_count":12} +{"atk_pct":0,"battles":5,"choices":6,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20],"hero_dmg":18,"hp_pct":0,"keystone_total":0,"level_templates":["地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战"],"levels_cleared":0,"mine_hits":0,"policy":"perfect","refreshes":1,"result":"failed","reward_events":[],"rounds":19,"route":"min","seed":78028,"shop_gp_seq":[],"stopped_at":1,"visited_count":7,"wallet_count":6} +{"atk_pct":0,"battles":4,"choices":4,"cleared":false,"encounter_max_hp_seq":[20,20,20,20],"hero_dmg":25,"hp_pct":0,"keystone_total":0,"level_templates":["地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)"],"levels_cleared":0,"mine_hits":0,"policy":"perfect","refreshes":1,"result":"failed","reward_events":[],"rounds":14,"route":"min","seed":78029,"shop_gp_seq":[],"stopped_at":1,"visited_count":5,"wallet_count":5} +{"atk_pct":0,"battles":13,"choices":17,"cleared":true,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25,30,30,30,30],"hero_dmg":118,"hp_pct":0,"keystone_total":30,"level_templates":["地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","灰矮人矿城·休整远路","灰矮人矿城·休整远路","灰矮人矿城·休整远路","灰矮人矿城·休整远路","灰矮人矿城·休整远路","灰矮人矿城·休整远路","灰矮人矿城·休整远路"],"levels_cleared":3,"mine_hits":0,"policy":"perfect","refreshes":5,"result":"completed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25},{"from_level":2,"hp_bonus":5,"keystone_gained":10,"keystone_total":20,"new_max_hp":30}],"rounds":92,"route":"min","seed":78030,"shop_gp_seq":[57,107],"stopped_at":0,"visited_count":20,"wallet_count":14} +{"atk_pct":0,"battles":7,"choices":7,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25],"hero_dmg":56,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","幽暗地域·奖励窄门","幽暗地域·奖励窄门"],"levels_cleared":1,"mine_hits":0,"policy":"perfect","refreshes":2,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":34,"route":"min","seed":78031,"shop_gp_seq":[],"stopped_at":2,"visited_count":9,"wallet_count":8} +{"atk_pct":0,"battles":9,"choices":11,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25],"hero_dmg":66,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)"],"levels_cleared":1,"mine_hits":0,"policy":"perfect","refreshes":1,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":42,"route":"min","seed":78032,"shop_gp_seq":[49],"stopped_at":2,"visited_count":13,"wallet_count":10} +{"atk_pct":0,"battles":12,"choices":15,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25,30,30,30],"hero_dmg":145,"hp_pct":0,"keystone_total":20,"level_templates":["地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","灰矮人矿城·休整远路","灰矮人矿城·休整远路","灰矮人矿城·休整远路","灰矮人矿城·休整远路","灰矮人矿城·休整远路"],"levels_cleared":2,"mine_hits":0,"policy":"perfect","refreshes":5,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25},{"from_level":2,"hp_bonus":5,"keystone_gained":10,"keystone_total":20,"new_max_hp":30}],"rounds":78,"route":"min","seed":78033,"shop_gp_seq":[58],"stopped_at":3,"visited_count":18,"wallet_count":13} +{"atk_pct":0,"battles":9,"choices":10,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25],"hero_dmg":55,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","幽暗地域·奖励窄门","幽暗地域·奖励窄门","幽暗地域·奖励窄门","幽暗地域·奖励窄门","幽暗地域·奖励窄门"],"levels_cleared":1,"mine_hits":0,"policy":"perfect","refreshes":1,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":42,"route":"min","seed":78034,"shop_gp_seq":[],"stopped_at":2,"visited_count":12,"wallet_count":10} +{"atk_pct":0,"battles":12,"choices":14,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25,30,30,30],"hero_dmg":108,"hp_pct":0,"keystone_total":20,"level_templates":["地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","灰矮人矿城·双线归束","灰矮人矿城·双线归束","灰矮人矿城·双线归束","灰矮人矿城·双线归束"],"levels_cleared":2,"mine_hits":0,"policy":"perfect","refreshes":2,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25},{"from_level":2,"hp_bonus":5,"keystone_gained":10,"keystone_total":20,"new_max_hp":30}],"rounds":70,"route":"min","seed":78035,"shop_gp_seq":[65],"stopped_at":3,"visited_count":17,"wallet_count":13} +{"atk_pct":0,"battles":9,"choices":10,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25],"hero_dmg":70,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","幽暗地域·奖励窄门","幽暗地域·奖励窄门","幽暗地域·奖励窄门","幽暗地域·奖励窄门","幽暗地域·奖励窄门"],"levels_cleared":1,"mine_hits":0,"policy":"perfect","refreshes":2,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":46,"route":"min","seed":78036,"shop_gp_seq":[],"stopped_at":2,"visited_count":12,"wallet_count":10} +{"atk_pct":0,"battles":7,"choices":7,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25],"hero_dmg":51,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)"],"levels_cleared":1,"mine_hits":0,"policy":"perfect","refreshes":1,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":36,"route":"min","seed":78037,"shop_gp_seq":[],"stopped_at":2,"visited_count":9,"wallet_count":8} +{"atk_pct":0,"battles":14,"choices":15,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25,25,25,30,30,30],"hero_dmg":122,"hp_pct":0,"keystone_total":20,"level_templates":["地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","灰矮人矿城·直入深层","灰矮人矿城·直入深层","灰矮人矿城·直入深层"],"levels_cleared":2,"mine_hits":0,"policy":"perfect","refreshes":3,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25},{"from_level":2,"hp_bonus":5,"keystone_gained":10,"keystone_total":20,"new_max_hp":30}],"rounds":88,"route":"min","seed":78038,"shop_gp_seq":[94],"stopped_at":3,"visited_count":18,"wallet_count":15} +{"atk_pct":0,"battles":10,"choices":11,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25,25],"hero_dmg":76,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束"],"levels_cleared":1,"mine_hits":0,"policy":"perfect","refreshes":3,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":50,"route":"min","seed":78039,"shop_gp_seq":[],"stopped_at":2,"visited_count":13,"wallet_count":11} diff --git a/tools/t5_build_templates.py b/tools/t5_build_templates.py new file mode 100644 index 00000000..df256a39 --- /dev/null +++ b/tools/t5_build_templates.py @@ -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")