From 07f3eb55e32300f43b0f9463248586b603e8679d Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Thu, 22 Oct 2026 04:17:21 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat(=E6=95=8C=E4=BA=BA=E8=A1=A8=E6=B7=B1?= =?UTF-8?q?=E5=BA=A6=E5=B8=A6):=20=E9=81=AD=E9=81=87=E8=A1=8C=E9=9B=86?= =?UTF-8?q?=E5=AF=BB=E5=9D=80=E6=94=B9=E4=B8=BA=20(=E5=85=B3=E5=8D=A1,=20r?= =?UTF-8?q?ow=5Fkind,=20=E6=B7=B1=E5=BA=A6=E5=B8=A6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 敌人行集不再按 (level_index, node_depth) 精确点寻址,改为 (level_index, row_kind, depth_band): 带切法单点 EnemyTable.band_of 取首个不小于请求深度的带(偏难一侧),高于顶带返回 -1 交 resolver 取顶带并发 resolve_depth_clamp(四元组去重不变);resolver 与 ConfigDb 门面同步 加 row_kind 第三参;unit_container.spawn 与 BattleOrchestrator 透传行类(battle/boss)。 敌人表 20 行键迁移(原 node_depth 数值就地成为新带键,值列零变更),新增 row_kind 列; schema 校验随键换代:row_kind 封闭枚举、dup_key/rowset_rule 新键串、每关 battle 带 0 锚 与每关 boss 行集 ≥1;跨表校验 ④ 敌侧改带寻址,新增同日图多 Boss 同带报错与顶带消解报错。 读数:基线冒烟 1156 项 0 失败;本轮冒烟 1161 项 0 失败;单元 35 套件全绿;探针 --click 全绿。 --- game/core/battle_orchestrator.gd | 8 +-- game/core/config_db.gd | 4 +- game/core/config_db_cross_validator.gd | 39 ++++++++++---- game/core/config_db_resolver.gd | 70 +++++++++++++++++++++----- game/core/unit_container.gd | 8 +-- game/data/schemas/enemy_table.gd | 68 +++++++++++++++++-------- game/data/tables/enemy.tres | 64 +++++++++++++++-------- game/presentation/battle_narrator.gd | 2 +- 8 files changed, 191 insertions(+), 72 deletions(-) diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index 6ee113f6..1515056b 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -324,9 +324,11 @@ func begin_battle(enc: Dictionary) -> bool: # 开流条件、消费序、消费次数逐位一致——零内容期随机流零变化;区间宽度为零的行不掷 = 「定值不骰」 # 既有口径;atk 区间逐行同构 = 行值语义完整,属「每行一骰」语义内的 # 行字段区间骰逐行化,非新增随机源形态)。 - var erowset_pre: Array[Variant] = db.resolve_enemy_rowset(int(enc.level_index), int(enc.node_depth)) + # 行类维度:节点身份 battle/boss 决定行集键域(缺省 battle 只服务测试直呼面——生产路径恒传 node_type) + var enc_kind := String(enc.get("node_type", "battle")) + var erowset_pre: Array[Variant] = db.resolve_enemy_rowset(int(enc.level_index), int(enc.node_depth), enc_kind) if erowset_pre.is_empty(): - _fatal("敌人表 行集解析为空(%d,%d)" % [int(enc.level_index), int(enc.node_depth)]) + _fatal("敌人表 行集解析为空(%d,%d,%s)" % [int(enc.level_index), int(enc.node_depth), enc_kind]) return false var count_overrides: Array[Variant] = [] var atk_overrides: Array[Variant] = [] @@ -354,7 +356,7 @@ func begin_battle(enc: Dictionary) -> bool: _hero_atk = int(enc.get("hero_atk", int(db.hero.atk))) if not _enemy_system.spawn(int(enc.level_index), int(enc.node_depth), int(enc.hero_hp), count_overrides, _hero_atk, int(enc.get("hero_max_hp", -1)), atk_overrides, - String(enc.get("attack_type", "")), String(enc.get("armor_type", ""))): # 两根类型轴(空串=锻造配置默认值回退,行为连续) + String(enc.get("attack_type", "")), String(enc.get("armor_type", "")), enc_kind): # 两根类型轴(空串=锻造配置默认值回退,行为连续)+行类维(battle/boss) _fatal("敌人系统 spawn 被拒") return false if not _install_new_board(): diff --git a/game/core/config_db.gd b/game/core/config_db.gd index 95749262..3ec2c319 100644 --- a/game/core/config_db.gd +++ b/game/core/config_db.gd @@ -190,8 +190,8 @@ func resolve_board_gen(L: int, D: int) -> Dictionary: return _resolver.resolve_board_gen(L, D) -func resolve_enemy_rowset(L: int, D: int) -> Array[Dictionary]: - return _resolver.resolve_enemy_rowset(L, D) # 行集数组形态(非单行 Dictionary) +func resolve_enemy_rowset(L: int, D: int, row_kind: String) -> Array[Dictionary]: + return _resolver.resolve_enemy_rowset(L, D, row_kind) # 行集数组形态(非单行 Dictionary;D 维带寻址) # 难度曲线与Boss DifficultyTable 消费面(敌人系统.spawn 调用):(L,D) 精确匹配修饰乘数,越域值 clamp(GDD Edge Case——组合事故非配置事故) diff --git a/game/core/config_db_cross_validator.gd b/game/core/config_db_cross_validator.gd index c9521c37..7bb4b95f 100644 --- a/game/core/config_db_cross_validator.gd +++ b/game/core/config_db_cross_validator.gd @@ -51,8 +51,9 @@ func cross_validate(loaded: Dictionary, push_diag: Callable) -> void: for s in required_sources: if not st_tab.source_label_map.has(s): push_diag.call("error", "segorder_violation", "source_label_map 缺必需键: %s" % s) - # ④ LevelTable 节点悬空引用(battle/boss 的 (L,node_depth) 须在 board_gen 与 enemy 两表有精确行—— - # 线性推进器按精确深度行跑,floor 兜底静默降难度属调参事故,加载期直接拦) + # ④ LevelTable 节点悬空引用(battle/boss 节点:board_gen 侧须有精确行;enemy 侧按 + # (关卡序号, 行类) 带寻址——落带即设计语义,节点深度高于顶带键=顶 clamp 消解属调参事故,加载期直接拦。 + # 同关同一张图多终点 Boss 节点解析带键须互异(共带=两终点命中同一 Boss 行集,「走哪条路打谁」落空)) var lv: LevelTable = loaded.get("level") var bg: BoardGenTable = loaded.get("board_gen") var en: EnemyTable = loaded.get("enemy") @@ -60,9 +61,16 @@ func cross_validate(loaded: Dictionary, push_diag: Callable) -> void: var bg_keys := {} for row in bg.rows: bg_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true - var en_keys := {} + var en_bands := {} # "%d_%s" % [L, row_kind] → 升序带键数组 for row in en.rows: - en_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true + var ek := "%d_%s" % [int(row.level_index), String(row.get("row_kind", ""))] + if not en_bands.has(ek): + en_bands[ek] = [] + var eb := int(row.get("depth_band", -1)) + if not (en_bands[ek] as Array).has(eb): + (en_bands[ek] as Array).append(eb) + for ek in en_bands.keys(): + (en_bands[ek] as Array).sort() for lrow in lv.rows: var li: int = int(lrow.level_index) # 模板池感知(关卡节点图生成 v0.4——池内全模板跨表校验,不只 row.nodes) @@ -73,16 +81,29 @@ func cross_validate(loaded: Dictionary, push_diag: Callable) -> void: else: for tpl in tpls: node_lists.append(tpl.get("nodes", [])) - for nodes_arr in node_lists: - for node in nodes_arr: + for tpl_i in range(node_lists.size()): + var boss_bands := {} # 本图已占 Boss 带键 + for node in node_lists[tpl_i]: 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))] + var nd: int = int(node.get("node_depth", -1)) + var key := "%d_%d" % [li, nd] if not bg_keys.has(key): push_diag.call("error", "level_node_dangling", "level %d %s 节点 (%s) 在 board_gen 无精确行" % [li, t, key]) - if not en_keys.has(key): - push_diag.call("error", "level_node_dangling", "level %d %s 节点 (%s) 在 enemy 无精确行" % [li, t, key]) + var nk := "%d_%s" % [li, t] + if not en_bands.has(nk): + push_diag.call("error", "level_node_dangling", "level %d %s 节点 (%d) 在 enemy 无该行类行集" % [li, t, nd]) + continue + var hit_band: int = EnemyTable.band_of(en_bands[nk], nd) + if hit_band < 0: + push_diag.call("error", "level_node_dangling", "level %d %s 节点深度 %d 高于 (level %d, %s) 顶带键 %d——顶 clamp 消解" % [ + li, t, nd, li, t, int((en_bands[nk] as Array)[-1])]) + continue + if t == "boss": + if boss_bands.has(hit_band): + push_diag.call("error", "rowset_rule", "level %d 模板 %d 多终点 Boss 节点同带(band %d)——共带使「走哪条路打谁」落空" % [li, tpl_i, hit_band]) + boss_bands[hit_band] = true # ⑤ 技能池引用完整性(敌人技能池批——敌人表池 × 技能表;独立入口 validate_skill_pool_links 供冒烟直验) var skt = loaded.get("skill") if skt != null and en != null: diff --git a/game/core/config_db_resolver.gd b/game/core/config_db_resolver.gd index 78a83b29..8bd98da7 100644 --- a/game/core/config_db_resolver.gd +++ b/game/core/config_db_resolver.gd @@ -30,11 +30,63 @@ func resolve_board_gen(L: int, D: int) -> Dictionary: return _resolve_progress(_board_gen_rows_get.call(), L, D, "board_gen") -func resolve_enemy_rowset(L: int, D: int) -> Array[Dictionary]: - # 行集数组形态:ceil 命中 (l*,d*) 后 - # 返回该坐标全部行、按表文件行序(行序即契约——同坐标多行文件内相邻由 EnemyTable.validate 兜底)。 - # 空集表达 = 空数组(生产表下 ceil 命中坐标恒有行,空集仅表空时可达——validate 已拦)。 - return _resolve_progress(_enemy_rows_get.call(), L, D, "enemy", true) +func resolve_enemy_rowset(L: int, D: int, row_kind: String) -> Array[Dictionary]: + # 带寻址:L 维沿进度行解析现状(ceil 向上取整保证偏难、越最大行 clamp), + # D 维经 EnemyTable.band_of 落带——命中该 (L, row_kind) 的某一个 depth_band, + # 返回该带全部行、按表文件行序(行序即契约——同带键多行文件内相邻由 EnemyTable.validate 兜底)。 + # 空集表达 = 空数组(band_keys 空时可达,交消费侧既有空行集 fatal 路径——validate 已拦无 Boss 行集)。 + if L < 1 or D < 0: + _diag.call("fatal", "request_oob", "enemy 请求越域 (%d,%d)——进度坐标低于合法域(关卡序号≥1、节点深度≥0)" % [L, D]) + return [] + if not EnemyTable.ROW_KINDS.has(row_kind): + _diag.call("fatal", "request_oob", "enemy 请求行类非法 (%d,%d,\"%s\")——row_kind 域 {battle, boss}" % [L, D, row_kind]) + return [] + var rows: Array = _enemy_rows_get.call() + var level_keys: Array[int] = [] + for row in rows: + var lv := int(row.get("level_index", 0)) + if not level_keys.has(lv): + level_keys.append(lv) + if level_keys.is_empty(): + _diag.call("fatal", "structural", "enemy 行集为空——锚行校验应已拦截") + return [] + level_keys.sort() + var l_star := _ceil_key(level_keys, L) + if l_star < 0: + l_star = int(level_keys[-1]) # 超出最大行 → 取最大行封顶 + _warn_resolve("enemy", "resolve_level_clamp", "enemy 请求 (%d,%d) clamp 解析(超出最大关卡取最大值)→ 关卡 %d" % [L, D, l_star], l_star, D) + elif l_star != L: + _warn_resolve("enemy", "resolve_level_ceil", "enemy 请求 (%d,%d) ceil 解析(向上取整保证偏难)→ 关卡 %d" % [L, D, l_star], l_star, D) + var band_keys: Array[int] = [] + for row in rows: + if int(row.get("level_index", 0)) == l_star and String(row.get("row_kind", "")) == row_kind: + var bv := int(row.get("depth_band", -1)) + if not band_keys.has(bv): + band_keys.append(bv) + if band_keys.is_empty(): + return [] + band_keys.sort() + var d_star := EnemyTable.band_of(band_keys, D) + if d_star < 0: + d_star = int(band_keys[-1]) # 越顶 clamp 顶带(调参事故面,跨表校验另报) + _warn_resolve("enemy", "resolve_depth_clamp", "enemy 请求 (%d,%d) clamp 解析(超出顶带取顶带)→ (%d,band %d)" % [L, D, l_star, d_star], l_star, d_star) + elif d_star != D: + _warn_resolve("enemy", "resolve_depth_ceil", "enemy 请求 (%d,%d) ceil 解析(向上取整保证偏难)→ (%d,band %d)" % [L, D, l_star, d_star], l_star, d_star) + var all_out: Array[Dictionary] = [] + for row in rows: + if int(row.get("level_index", 0)) == l_star and String(row.get("row_kind", "")) == row_kind \ + and int(row.get("depth_band", -1)) == d_star: + all_out.append(row) + return all_out + + +# 解析告警单点(类别 + 命中键四元组去重——R1 既有去重注册表) +func _warn_resolve(table_id: String, category: String, message: String, l_key: int, d_key: int) -> void: + var dedup_key := "%s|%s|%d|%d" % [table_id, category, l_key, d_key] + if _warn_dedup.has(dedup_key): + return + _warn_dedup[dedup_key] = true + _diag.call("warning", category, message) # 难度曲线与Boss DifficultyTable 消费面(敌人系统.spawn 调用):(L,D) 精确匹配修饰乘数,越域值 clamp(GDD Edge Case——组合事故非配置事故) @@ -51,7 +103,7 @@ func resolve_difficulty_modifiers(L: int, D: int) -> Dictionary: return {} -func _resolve_progress(rows: Array, L: int, D: int, table_id: String, return_all := false) -> Variant: +func _resolve_progress(rows: Array, L: int, D: int, table_id: String) -> Variant: if L < 1 or D < 0: _diag.call("fatal", "request_oob", "%s 请求越域 (%d,%d)——进度坐标低于合法域(关卡序号≥1、节点深度≥0)" % [table_id, L, D]) return {} @@ -98,12 +150,6 @@ func _resolve_progress(rows: Array, L: int, D: int, table_id: String, return_all _warn_dedup[dedup_key] = true var verb := "clamp 解析(超出最大行取最大值)" if missing.contains("clamp") else "ceil 解析(向上取整保证偏难)" _diag.call("warning", "resolve_" + missing, "%s 请求 (%d,%d) %s → (%d,%d) [%s]——2026-09-18 拍板" % [table_id, L, D, verb, l_star, d_star, missing]) - if return_all: - var all_out: Array[Dictionary] = [] - for row in rows: - if int(row.get("level_index", 0)) == l_star and int(row.get("node_depth", 0)) == d_star: - all_out.append(row) - return all_out for row in rows: if int(row.get("level_index", 0)) == l_star and int(row.get("node_depth", 0)) == d_star: return row diff --git a/game/core/unit_container.gd b/game/core/unit_container.gd index fadeac2c..2a3416c6 100644 --- a/game/core/unit_container.gd +++ b/game/core/unit_container.gd @@ -35,14 +35,14 @@ func _init(db_ref: ConfigDb) -> void: # ≤ ENEMY_ROWSET_INSTANCE_MAX 守卫 fail-fast。 func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_overrides: Array = [], atk_override: int = -1, max_hp_override: int = -1, enemy_atk_overrides: Array = [], - hero_attack_type: String = "", hero_armor_type: String = "") -> bool: + hero_attack_type: String = "", hero_armor_type: String = "", row_kind: String = "battle") -> bool: if _spawned: push_warning("[敌人系统] Alive 相位重复 spawn(相位守卫)") return false - # 行集数组口径(resolver 返回该坐标全部行、文件序) - var rowset: Array[Variant] = db.resolve_enemy_rowset(level_index, node_depth) + # 行集数组口径(resolver 按 (L,band) 带寻址返回全部行、文件序) + var rowset: Array[Variant] = db.resolve_enemy_rowset(level_index, node_depth, row_kind) if rowset.is_empty(): - push_error("[敌人系统] 遭遇行解析失败/越域 (%d,%d)" % [level_index, node_depth]) + push_error("[敌人系统] 遭遇行解析失败/越域 (%d,%d,%s)" % [level_index, node_depth, row_kind]) assert(false, "遭遇行悬空(R1 前置守卫)") return false var eff_atk: int = atk_override if atk_override >= 1 else int(db.hero.atk) diff --git a/game/data/schemas/enemy_table.gd b/game/data/schemas/enemy_table.gd index 697ef155..bfd185e0 100644 --- a/game/data/schemas/enemy_table.gd +++ b/game/data/schemas/enemy_table.gd @@ -1,12 +1,16 @@ ## VERTICAL SLICE - NOT FOR PRODUCTION -## 敌人表 schema:逐 (level_index, node_depth, enemy_id) 行集的类型化读取(行集可多行混编;垂直切片验证:完整战斗逐行归因)。 -## 行序即契约:同坐标多行必须文件内相邻——resolver 返回序/生成序/叙述取首行全依赖文件行序。 +## 敌人表 schema:逐 (level_index, row_kind, depth_band, enemy_id) 行集的类型化读取(行集可多行混编;垂直切片验证:完整战斗逐行归因)。 +## 行序即契约:同带键多行必须文件内相邻——resolver 返回序/生成序/叙述取首行全依赖文件行序。 +## 深度带 = 节点深度所在的区间:配池按带不按点,难度坡随带递增;row_kind 承载「Boss 行按终点配置」。 ## 行集实例总数 ≤4 是开战骰后守卫(加载期不可判,闸在单位容器 spawn);技能池构成纪律需跨表视野,校验在 ConfigDB 跨表通道。 class_name EnemyTable extends Resource @export var rows: Array[Dictionary] = [] # Array[Dictionary] +# 行类封闭枚举(对齐关卡节点 type 词表):battle = 战斗节点配池,boss = 终点行集 +const ROW_KINDS := ["battle", "boss"] + # 护甲类型封闭枚举(克制矩阵本体在 SettlementTable; # 语义键 {light, ward, heavy} = 轻甲 / 魔法护甲 / 重甲) const ARMOR_TYPES := ["light", "ward", "heavy"] @@ -35,19 +39,23 @@ func validate() -> Array: errs.append({"category": "empty_table", "message": "EnemyTable 为空"}) return errs var seen := {} - var last_rk := "" - var closed_rk := {} + var last_rowset_key := "" + var closed_rowset := {} var anchor_ok := {} + var boss_ok := {} for row in rows: var L: int = int(row.get("level_index", 0)) - var D: int = int(row.get("node_depth", -1)) + var rk: String = String(row.get("row_kind", "")) + var band: int = int(row.get("depth_band", -1)) var eid: String = row.get("enemy_id", "") - var key := "%d_%d_%s" % [L, D, eid] + var key := "%d_%s_%d_%s" % [L, rk, band, eid] if seen.has(key): errs.append({"category": "dup_key", "message": "EnemyTable 键重复: %s" % key}) seen[key] = true - if L < 1 or D < 0: - errs.append({"category": "domain", "message": "进度键越域 (%d,%d)" % [L, D]}) + if L < 1 or band < 0: + errs.append({"category": "domain", "message": "进度键越域 (%d,band %d)" % [L, band]}) + if not ROW_KINDS.has(rk): + errs.append({"category": "domain", "message": "%s row_kind 未知枚举: \"%s\"(域 {battle, boss})" % [eid, rk]}) if eid == "": errs.append({"category": "domain", "message": "enemy_id 空"}) if String(row.get("display_name", "")) == "": @@ -118,23 +126,41 @@ func validate() -> Array: var ac: int = int(row.get("armor_class", 0)) if ac < 0 or ac > 999: errs.append({"category": "domain", "message": "%s armor_class 越域 [0,999]: %d" % [eid, ac]}) - # 行集构成纪律②(行序即契约,敌人系统 v0.15):同坐标多行必须 - # 文件内相邻。迭代中记录上一行坐标;当前坐标曾出现过但 ≠ 上一行坐标 = 同坐标行被隔断,报错。 - # 同坐标多敌种混编合法;同坐标同敌种第二行仍由 + # 行集构成纪律②(行序即契约,敌人系统 v0.15):同带键多行必须 + # 文件内相邻。迭代中记录上一行行集键;当前行集键曾出现过但 ≠ 上一行行集键 = 同带键行被隔断,报错。 + # 同带键多敌种混编合法;同带键同敌种第二行仍由 # dup_key 拦截(键含 enemy_id)。错误类别沿用 rowset_rule(定位面不变)。 - var rk := "%d_%d" % [L, D] - if rk != last_rk: - if closed_rk.has(rk): - errs.append({"category": "rowset_rule", "message": "(%d,%d) 行集行序违例:同坐标多行必须文件内相邻(行序即契约——resolver 返回序/生成序/叙述取首行全依赖文件行序)" % [L, D]}) - closed_rk[rk] = true - last_rk = rk - anchor_ok[L] = anchor_ok.get(L, false) or D == 0 + var rowset_key := "%d_%s_%d" % [L, rk, band] + if rowset_key != last_rowset_key: + if closed_rowset.has(rowset_key): + errs.append({"category": "rowset_rule", "message": "(%d,%s,%d) 行集行序违例:同带键多行必须文件内相邻(行序即契约——resolver 返回序/生成序/叙述取首行全依赖文件行序)" % [L, rk, band]}) + closed_rowset[rowset_key] = true + last_rowset_key = rowset_key + if rk == "battle" and band == 0: + anchor_ok[L] = true + elif rk == "boss": + boss_ok[L] = true + if not anchor_ok.has(L): + anchor_ok[L] = false for L in anchor_ok.keys(): if not anchor_ok[L]: - errs.append({"category": "anchor", "message": "EnemyTable level %d 缺 depth-0 锚行" % L}) + errs.append({"category": "anchor", "message": "EnemyTable level %d 缺 (battle, band 0) 锚行" % L}) + if not boss_ok.has(L): + errs.append({"category": "anchor", "message": "EnemyTable level %d 缺 Boss 行集(row_kind=boss 行 ≥1)" % L}) return errs +# 深度带切法单点(唯一实现,禁第二份算法):band_keys = 该 (level_index, row_kind) 的 +# depth_band 键升序数组;返回首个 ≥ D 的键(请求落带取偏难一侧)。 +# D 高于全部键 → 返回 -1——resolver 侧 clamp 顶带+warning,跨表校验侧判「顶 clamp 消解」违例; +# band_keys 空 → 同样返回 -1(resolver 随之返回空行集,交消费侧既有空行集 fatal 路径)。 +static func band_of(band_keys: Array, D: int) -> int: + for k in band_keys: + if int(k) >= D: + return int(k) + return -1 + + # 进度键解析单点收口(全仓统走 -# ConfigDbResolver.resolve_enemy_rowset ceil 版行集数组:ceil 保证偏难, -# 禁双算法并存〔状态记录 312〕;坐标须全精确命中)。 +# ConfigDbResolver.resolve_enemy_rowset 带寻址行集数组:D 维经本表 band_of 落带(偏难一侧), +# 禁双算法并存〔状态记录 312〕;L 维 ceil/clamp 与越顶 clamp 语义归 resolver)。 diff --git a/game/data/tables/enemy.tres b/game/data/tables/enemy.tres index df5bd973..897cd197 100644 --- a/game/data/tables/enemy.tres +++ b/game/data/tables/enemy.tres @@ -6,7 +6,8 @@ script = ExtResource("1") rows = [{ "level_index": 1, -"node_depth": 0, +"row_kind": "battle", +"depth_band": 0, "enemy_id": "goblin", "display_name": "地精", "unit_count": 1, @@ -27,7 +28,8 @@ rows = [{ "armor_class": 0 }, { "level_index": 1, -"node_depth": 1, +"row_kind": "battle", +"depth_band": 1, "enemy_id": "goblin", "display_name": "地精一伙", "unit_count": 2, @@ -48,7 +50,8 @@ rows = [{ "armor_class": 1 }, { "level_index": 1, -"node_depth": 2, +"row_kind": "battle", +"depth_band": 2, "enemy_id": "bugbear", "display_name": "熊地精", "unit_count": 1, @@ -71,7 +74,8 @@ rows = [{ "armor_class": 2 }, { "level_index": 1, -"node_depth": 3, +"row_kind": "battle", +"depth_band": 3, "enemy_id": "hobgoblin", "display_name": "大地精", "unit_count": 2, @@ -95,7 +99,8 @@ rows = [{ "armor_class": 2 }, { "level_index": 1, -"node_depth": 4, +"row_kind": "boss", +"depth_band": 4, "enemy_id": "ogre_chief", "display_name": "食人魔酋长", "unit_count": 1, @@ -122,7 +127,8 @@ rows = [{ "armor_class": 8 }, { "level_index": 2, -"node_depth": 0, +"row_kind": "battle", +"depth_band": 0, "enemy_id": "bugbear", "display_name": "熊地精", "unit_count": 1, @@ -145,7 +151,8 @@ rows = [{ "armor_class": 1 }, { "level_index": 2, - "node_depth": 1, + "row_kind": "battle", + "depth_band": 1, "enemy_id": "bugbear", "display_name": "熊地精猎群", "unit_count": 2, @@ -169,7 +176,8 @@ rows = [{ "armor_class": 2 }, { "level_index": 2, -"node_depth": 4, +"row_kind": "battle", +"depth_band": 4, "enemy_id": "giant_spider", "display_name": "巨蜘蛛", "unit_count": 2, @@ -192,7 +200,8 @@ rows = [{ "armor_class": 2 }, { "level_index": 2, -"node_depth": 5, +"row_kind": "battle", +"depth_band": 5, "enemy_id": "bugbear", "display_name": "熊地精重击手", "unit_count": 1, @@ -215,7 +224,8 @@ rows = [{ "armor_class": 8 }, { "level_index": 2, -"node_depth": 5, +"row_kind": "battle", +"depth_band": 5, "enemy_id": "giant_spider", "display_name": "巨蜘蛛巢群", "unit_count": 2, @@ -242,7 +252,8 @@ rows = [{ "armor_class": 2 }, { "level_index": 2, -"node_depth": 6, +"row_kind": "battle", +"depth_band": 6, "enemy_id": "giant_spider", "display_name": "巨蜘蛛巢群", "unit_count": 2, @@ -269,7 +280,8 @@ rows = [{ "armor_class": 2 }, { "level_index": 2, -"node_depth": 7, +"row_kind": "battle", +"depth_band": 7, "enemy_id": "giant_spider", "display_name": "巨蜘蛛巢群", "unit_count": 2, @@ -296,7 +308,8 @@ rows = [{ "armor_class": 3 }, { "level_index": 2, -"node_depth": 9, +"row_kind": "boss", +"depth_band": 9, "enemy_id": "drider", "display_name": "蛛化卓尔精灵", "unit_count": 1, @@ -326,7 +339,8 @@ rows = [{ "armor_class": 10 }, { "level_index": 3, -"node_depth": 0, +"row_kind": "battle", +"depth_band": 0, "enemy_id": "duergar", "display_name": "灰矮人", "unit_count": 1, @@ -352,7 +366,8 @@ rows = [{ "armor_class": 2 }, { "level_index": 3, -"node_depth": 1, +"row_kind": "battle", +"depth_band": 1, "enemy_id": "hook_horror", "display_name": "钩爪恐魔", "unit_count": 2, @@ -375,7 +390,8 @@ rows = [{ "armor_class": 3 }, { "level_index": 3, -"node_depth": 4, +"row_kind": "battle", +"depth_band": 4, "enemy_id": "hook_horror", "display_name": "钩爪恐魔", "unit_count": 2, @@ -399,7 +415,8 @@ rows = [{ "armor_class": 3 }, { "level_index": 3, -"node_depth": 5, +"row_kind": "battle", +"depth_band": 5, "enemy_id": "duergar", "display_name": "灰矮人重装手", "unit_count": 1, @@ -425,7 +442,8 @@ rows = [{ "armor_class": 9 }, { "level_index": 3, -"node_depth": 6, +"row_kind": "battle", +"depth_band": 6, "enemy_id": "hook_horror", "display_name": "钩爪恐魔巢群", "unit_count": 2, @@ -449,7 +467,8 @@ rows = [{ "armor_class": 3 }, { "level_index": 3, -"node_depth": 7, +"row_kind": "battle", +"depth_band": 7, "enemy_id": "duergar", "display_name": "灰矮人小队", "unit_count": 2, @@ -476,7 +495,8 @@ rows = [{ "armor_class": 4 }, { "level_index": 3, -"node_depth": 9, +"row_kind": "boss", +"depth_band": 9, "enemy_id": "duergar_king", "display_name": "灰矮人石君王", "unit_count": 1, @@ -669,3 +689,7 @@ rows = [{ ; N3 敌方攻击类型轴开列(2026-10-13 迭代 24 卡①,承接锻造工坊实装批迁移面⑥):20 行全填 normal 挂敌种行—— ; 不做差异化(pierce/magic 属新设计另立项)。全局平移效应=主角穿重甲受伤 −1 / 魔法护甲 +1 / 轻甲 0, ; 锻造甲路线首次具备战斗意义;克制矩阵数值零触碰(settlement.tres armor_effectiveness 不改)。 +; +; 行键 = (level_index, row_kind, depth_band, enemy_id);row_kind ∈ {battle, boss}——battle=战斗节点配池,boss=终点行集; +; depth_band=节点深度所在的带键,请求落带取首个 ≥ 深度的键(偏难一侧);越顶带由 resolver clamp、跨表校验判为调参事故。 +; hobgoblin 行为缺口行:无立绘、不在敌种表——留任在役,补齐三件套(立绘+语义模板+敌种表行)归敌种内容批。 diff --git a/game/presentation/battle_narrator.gd b/game/presentation/battle_narrator.gd index aee45931..1422f766 100644 --- a/game/presentation/battle_narrator.gd +++ b/game/presentation/battle_narrator.gd @@ -73,7 +73,7 @@ static func boss_name(runner, db) -> String: if runner != null: var node: Dictionary[Variant, Variant] = runner.current_node() if String(node.get("type", "")) == "boss": - var rowset: Array[Variant] = db.resolve_enemy_rowset(int(runner.level_row.level_index), int(node.get("node_depth", -1))) + var rowset: Array[Variant] = db.resolve_enemy_rowset(int(runner.level_row.level_index), int(node.get("node_depth", -1)), "boss") if not rowset.is_empty(): return String(rowset[0].get("display_name", "")) return "遗迹守卫" From 61537da889d4c929c906098149e38e8edb5bf2fb Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Thu, 22 Oct 2026 04:17:40 +0800 Subject: [PATCH 2/6] =?UTF-8?q?test(=E6=95=8C=E4=BA=BA=E8=A1=A8=E6=B7=B1?= =?UTF-8?q?=E5=BA=A6=E5=B8=A6):=20=E5=86=92=E7=83=9F=E4=B8=8E=E5=8D=95?= =?UTF-8?q?=E6=B5=8B=E6=96=AD=E8=A8=80=E6=8C=89=20S=20=E6=B8=85=E5=8D=95?= =?UTF-8?q?=E6=94=B9=E5=86=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 冒烟全部寻址点改为三参 resolve_enemy_rowset(关卡, 深度, 行类),结算/单元/探针直呼的 spawn 与 begin_battle 显式带行类(battle/boss)——终点战仍按 Boss 行集取数,行为不变。 首遇位断言改为扫表取该 (关卡, battle, 敌种) 的最低深度带,不再写死坐标〔fv21〕;新增 _enemy_bands 结构段验 20 行键序、hobgoblin 留任 (1,battle,3)、三 Boss 行类与带键〔fv11–fv13〕; L2 混编 (2,battle,5) 行序与骰和、巨蜘蛛/bugbear/duergar 首遇带固定数、Boss 行类显式传入 等断言逐条加〔fv18/fv19/fv22–fv24〕尾缀。职业节奏探针与伤害探针同步三参寻址。 变异验证七项全部命中(记录表进实现报告):band_of 反向 floor → fv1/fv3 红;row_kind 枚举 判据注释 → fv7 红;Boss 同带判据删除 → fv15 红;顶带消解判据短路 → fv16 红;巨蜘蛛首遇行 加 unit_count_max → fv22 红;混编两行文件内对调 → fv18/fv11 红;hobgoblin 迁错带键 → fv11/fv12 红(直接迁到带 2 先被加载期同带规则拦下,改用与带 2 行互换带键,表可加载后键位断言红)。 --- game/smoke/damage_probe.gd | 14 +- game/smoke/skill_rhythm_probe.gd | 4 +- game/smoke/smoke_all.gd | 329 ++++++++++++------ .../battle_settlement/skill_unlock_test.gd | 8 +- .../core/config_db_cross_validator_test.gd | 65 +++- .../unit/core/config_db_resolver_test.gd | 82 ++++- game/tests/unit/data_config/config_db_test.gd | 73 ++-- 7 files changed, 410 insertions(+), 165 deletions(-) diff --git a/game/smoke/damage_probe.gd b/game/smoke/damage_probe.gd index cffa9d02..1d7c99c2 100644 --- a/game/smoke/damage_probe.gd +++ b/game/smoke/damage_probe.gd @@ -92,7 +92,7 @@ func _run_boss(db, seed_val: int, dig: bool) -> Dictionary: settlement.step_row.connect(func(row): cap["rows"].append(row)) settlement.settlement_snapshot.connect(func(s): cap["snaps"].append(s)) settlement.battle_finished.connect(func(p): cap["result"] = String(p.get("result", ""))) - if not settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(db.hero.max_hp)}): + if not settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(db.hero.max_hp), "node_type": "boss"}): cap["result"] = "begin_failed" return cap var rounds := 0 @@ -266,7 +266,7 @@ func _encounter_prediction(db, seed_val: int) -> Dictionary: if t != "battle" and t != "boss": continue var depth := int(node.get("node_depth", -1)) - var row: Dictionary = db.resolve_enemy_rowset(1, depth)[0] # 行集首行(行集多行批 T2 机械适配) + var row: Dictionary = db.resolve_enemy_rowset(1, depth, t)[0] # 行集首行(row_kind 随节点类型) var lo := int(row.get("unit_count", 1)) var hi := int(row.get("unit_count_max", lo)) var rolled := lo @@ -404,20 +404,22 @@ func _scan_spawns(db, seed0: int, n_seeds: int) -> Dictionary: for node in _level_row(db, 1).get("nodes", []): var t := String(node.get("type", "")) if t == "battle" or t == "boss": - depths.append(int(node.get("node_depth", -1))) + depths.append({"depth": int(node.get("node_depth", -1)), "kind": t}) var total := 0 var bad := 0 for i in range(n_seeds): var rng = load("res://core/rng_service.gd").new() rng.set_master_seed(seed0 + i) - for depth in depths: + for dep in depths: + var depth: int = int(dep.get("depth", -1)) + var kind: String = String(dep.get("kind", "battle")) total += 1 var settlement = load("res://core/battle_orchestrator.gd").new(db, rng) - if not settlement.begin_battle({"level_index": 1, "node_depth": int(depth), "hero_hp": int(db.hero.max_hp)}): + if not settlement.begin_battle({"level_index": 1, "node_depth": depth, "node_type": kind, "hero_hp": int(db.hero.max_hp)}): bad += 1 continue var n: int = settlement.presentation_snapshot().enemies.size() - var row: Dictionary = db.resolve_enemy_rowset(1, int(depth))[0] # 行集首行(同上) + var row: Dictionary = db.resolve_enemy_rowset(1, depth, kind)[0] # 行集首行(同上,row_kind 随节点类型) var lo := int(row.get("unit_count", 1)) var hi := int(row.get("unit_count_max", lo)) if n < 1 or n < lo or n > hi: diff --git a/game/smoke/skill_rhythm_probe.gd b/game/smoke/skill_rhythm_probe.gd index a4298e91..d6a8ee47 100644 --- a/game/smoke/skill_rhythm_probe.gd +++ b/game/smoke/skill_rhythm_probe.gd @@ -13,8 +13,8 @@ func _initialize() -> void: var db = load("res://core/config_db.gd").new() db.load_tables("res://data/tables", false) - # ① 表侧(行集数组取首行,零内容期行为不变) - var row = db.resolve_enemy_rowset(1, 1)[0] + # ① 表侧(行集数组取首行,战斗节点查 battle 键域) + var row = db.resolve_enemy_rowset(1, 1, "battle")[0] print("[1] row(1,1) display=", row.get("display_name"), " unit_count=", row.get("unit_count"), " pool=", row.get("pool")) diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index 7f8cf2e8..80d5abac 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -646,6 +646,7 @@ func _run_core_smoke(db, rng) -> void: _rng = rng _smoke_ac_swap_guard() _smoke_enemy_attack_type() + _smoke_enemy_bands() _smoke_armor_roll() _smoke_pool_wiring() _smoke_board_generator() @@ -964,24 +965,25 @@ func _smoke_adventure() -> void: print("--- 冒险结构验收(三关序列 · 大奖励 · 摘要五件事 · 账本封闭)---") var autoplay = load("res://smoke/level_autoplay.gd") # ① 表与覆盖义务(序列关数 = 行集关数;行集内每个战斗/Boss 节点跨表精确行集——覆盖义务断言; - # 精确行集口径——enemy 侧同坐标多行合法,坐标级键聚合天然兼容) + # enemy 侧键域 = (level_index, row_kind, depth_band)——战斗节点查 battle 键域、Boss 节点查 boss 键域, + # 同带多行合法,带键聚合天然兼容〔fv24〕) check(int(_db.level.rows.size()) == 3, "序列关数 = 行集关数(三关正式序列生效)") var bg_keys := {} for row in _db.board_gen.rows: bg_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true var en_keys := {} for row in _db.enemy.rows: - en_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true + en_keys["%d_%s_%d" % [int(row.level_index), String(row.get("row_kind", "")), int(row.get("depth_band", -1))]] = true var miss := 0 for row in _db.level.rows: for n in _level_active_nodes(int(row.level_index)): var nt := String(n.get("type", "")) if nt != "battle" and nt != "boss": continue - var key := "%d_%d" % [int(row.level_index), int(n.get("node_depth", -1))] - if not bg_keys.has(key) or not en_keys.has(key): + var key := "%d_%s_%d" % [int(row.level_index), nt, int(n.get("node_depth", -1))] + if not bg_keys.has("%d_%d" % [int(row.level_index), int(n.get("node_depth", -1))]) or not en_keys.has(key): miss += 1 - check(miss == 0, "行集内每个战斗/Boss 节点在 board_gen + enemy 有精确行集(缺失 %d)" % miss) + check(miss == 0, "行集内每个战斗/Boss 节点在 board_gen + enemy 有精确行集(缺失 %d——带键 (L,row_kind,depth_band))" % miss) # ② 满配通关 + ④ 三层奖励兑付(三选一恰 2 次 · 回血封顶 · 钥石逐杀 · 候选零重复) var st: Dictionary = autoplay.run_adventure(_db, 42, 900, 400) var sum: Dictionary = st["summary"] @@ -2019,7 +2021,7 @@ func _smoke_seal_ap_exhausted() -> void: var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) var rounds: Array = [] settlement.round_started.connect(func(ri): rounds.append(ri)) - check(settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp)}), "回归战斗启动(Boss 行——D0 20HP 级会被盲挖粉尘秒杀,无法承载封印回归场景)") + check(settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}), "回归战斗启动(Boss 行——D0 20HP 级会被盲挖粉尘秒杀,无法承载封印回归场景)") var minesweeper = settlement.minesweeper # 恰花 (AP-1) 点挖掘,留末点给封印(表驱动——AP 从表读不写死) var ap_total: int = int(_db.action_economy.ap_per_round) @@ -2082,6 +2084,55 @@ func _smoke_level() -> void: "模拟后 hero 表恢复基线(atk=%d hp=%d ac=%d——对配置快照)" % [int(_db.hero.atk), int(_db.hero.max_hp), int(_db.hero.armor_class)]) +# ---- 敌人表深度带键结构(迭代 34:20 行键迁移 · 键序=迁移表序 · hobgoblin 留任 · 三 Boss 行 kind/band)---- + +func _smoke_enemy_bands() -> void: + print("--- 敌人表深度带键结构(20 行键序 · hobgoblin 留任 · 三 Boss 行)---") + # ① 20 行计数 + 键序 = 迁移表序(文件行序不变——行序即契约) + var expect_keys: Array[String] = [ + "1|battle|0|goblin", "1|battle|1|goblin", "1|battle|2|bugbear", "1|battle|3|hobgoblin", + "1|boss|4|ogre_chief", + "2|battle|0|bugbear", "2|battle|1|bugbear", "2|battle|4|giant_spider", + "2|battle|5|bugbear", "2|battle|5|giant_spider", "2|battle|6|giant_spider", "2|battle|7|giant_spider", + "2|boss|9|drider", + "3|battle|0|duergar", "3|battle|1|hook_horror", "3|battle|4|hook_horror", "3|battle|5|duergar", + "3|battle|6|hook_horror", "3|battle|7|duergar", "3|boss|9|duergar_king", + ] + var got_keys: Array[String] = [] + for row in _db.enemy.rows: + got_keys.append("%d|%s|%d|%s" % [int(row.get("level_index", -1)), String(row.get("row_kind", "")), + int(row.get("depth_band", -1)), String(row.get("enemy_id", ""))]) + check(_db.enemy.rows.size() == 20 and got_keys == expect_keys, + "敌人表 20 行且键序 = 迁移表序((L,row_kind,depth_band,enemy_id) 逐行比对——文件行序不变)〔fv11〕") + # ② hobgoblin 缺口行留任随迁(契约 9:不删行不补立绘——缺口登记随行) + var hob := {} + for row in _db.enemy.rows: + if String(row.get("enemy_id", "")) == "hobgoblin": + hob = row + check(int(hob.get("level_index", -1)) == 1 and String(hob.get("row_kind", "")) == "battle" \ + and int(hob.get("depth_band", -1)) == 3, + "hobgoblin 行在 (1,battle,3) 留任随迁(缺口行不删不补——补齐归敌种内容批)〔fv12〕〔fv25〕") + check(FileAccess.get_file_as_string("res://data/tables/enemy.tres").contains("hobgoblin 行为缺口行"), + "hobgoblin 缺口登记随行(表文件注释在位——缺口不静默)〔fv25〕") + # ③ 三 Boss 行 kind/band 就位(band = 该关图 Boss 节点深度——单终点图数据驱动) + var boss_ok := true + var boss_detail := "" + for L in [1, 2, 3]: + var bdepth := _boss_depth(L) + var hits := 0 + for row in _db.enemy.rows: + if int(row.get("level_index", -1)) == L and String(row.get("row_kind", "")) == "boss": + hits += 1 + if int(row.get("depth_band", -1)) != bdepth: + boss_ok = false + boss_detail += " L%d:%d≠%d" % [L, int(row.get("depth_band", -1)), bdepth] + if hits != 1: + boss_ok = false + boss_detail += " L%d:boss 行×%d" % [L, hits] + check(boss_ok, "三关 Boss 行各恰一行且 row_kind=boss、band=图 Boss 节点深度(%d/%d/%d%s)〔fv13〕" % [ + _boss_depth(1), _boss_depth(2), _boss_depth(3), boss_detail]) + + # ---- 敌方攻击类型轴(表列/单位快照/单据载荷/矩阵命中证词)---- # 克制矩阵命中证词 = 同种子三甲型配对实测(不是查表存在性证词):同种子下 armor_roll 流消费逐位一致、 # 阈值(hero armor_class)不变,三甲型唯一差异 = 克制点数 cdelta(normal_vs_light 0 / heavy −1 / ward +1) @@ -2211,29 +2262,31 @@ func _smoke_ac_swap_guard() -> void: boss_ac[int(row.get("level_index", 0))] = int(row.get("armor_class", 0)) check(int(boss_ac.get(1, 0)) < int(boss_ac.get(2, 0)) and int(boss_ac.get(2, 0)) < int(boss_ac.get(3, 0)), "Boss AC 关卡台阶严格递增(%d < %d < %d)" % [int(boss_ac.get(1, 0)), int(boss_ac.get(2, 0)), int(boss_ac.get(3, 0))]) - # ③c (1,2) 降档锚定 + 第一关总量曲线护栏 - # (1,2) 锚定值 hp 22 / ac 2; + # ③c 熊地精首遇带降档锚定 + 第一关总量曲线护栏 + # 首遇带行 = 数据驱动定位(契约 8:该 (1,battle,bugbear) 的最低 depth_band)锚定值 hp 22 / ac 2; # 敌种 bugbear 与池 attack6/charge2/heavy2 钉死不动(防顺手改多); - # 曲线护栏 = 相邻节点「hp × 中位数量」总量比 ≤ 2.5(12/10/22/35/72 现行全绿;不用单调不回撤——会红在 (1,0)→(1,1)) + # 曲线护栏 = 深度带升序相邻带「hp × 中位数量」总量比 ≤ 2.5(12/10/22/35/72 现行全绿;不用单调不回撤——会红在首带→次带) + var l12_band := _first_encounter_band(1, "bugbear") var l12_row: Dictionary = {} for row in _db.enemy.rows: - if int(row.get("level_index", -1)) == 1 and int(row.get("node_depth", -1)) == 2: + if int(row.get("level_index", -1)) == 1 and String(row.get("row_kind", "")) == "battle" \ + and int(row.get("depth_band", -1)) == l12_band and String(row.get("enemy_id", "")) == "bugbear": l12_row = row break var l12_pool := {} for entry in l12_row.get("pool", []): l12_pool[String(entry.skill_id)] = int(entry.weight) - check(String(l12_row.get("enemy_id", "")) == "bugbear" and int(l12_row.get("hp", -1)) == 22 \ + check(l12_band >= 0 and not l12_row.is_empty() and int(l12_row.get("hp", -1)) == 22 \ and int(l12_row.get("armor_class", -1)) == 2 \ and l12_pool == {"attack": 6, "charge": 2, "heavy": 2}, - "第一关 (1,2) 降档锚定(bugbear 保留 · hp 22 · ac 2 · 池 attack6/charge2/heavy2 零改动——迭代 24 N1)") + "熊地精首遇带(1,battle,%d)降档锚定(hp 22 · ac 2 · 池 attack6/charge2/heavy2 零改动——迭代 24 N1;首遇带数据驱动定位)〔fv21〕" % l12_band) var l1_totals := {} for row in _db.enemy.rows: if int(row.get("level_index", -1)) != 1: continue var cnt_lo: int = int(row.get("unit_count", 1)) var cnt_hi: int = int(row.get("unit_count_max", cnt_lo)) - l1_totals[int(row.get("node_depth", -1))] = int(row.get("hp", 0)) * (cnt_lo + cnt_hi) / 2.0 + l1_totals[int(row.get("depth_band", -1))] = int(row.get("hp", 0)) * (cnt_lo + cnt_hi) / 2.0 var curve_ok := true var curve_detail := "" var depths_sorted := l1_totals.keys() @@ -2245,7 +2298,7 @@ func _smoke_ac_swap_guard() -> void: if ratio > 2.5: curve_ok = false curve_detail += " (%d→%d):%.2f" % [depths_sorted[i - 1], depths_sorted[i], ratio] - check(curve_ok, "第一关相邻节点总量比 ≤ 2.5(12/10/22/35/72——迭代 24 N1 曲线护栏,越带%s)" % [ + check(curve_ok, "第一关相邻深度带总量比 ≤ 2.5(12/10/22/35/72——迭代 24 N1 曲线护栏,带键升序;越带%s)" % [ curve_detail if curve_detail != "" else "无"]) # ④ 掷点参数域(加载校验已过——此处钉形状语义) var bounds: Array = _db.settlement.roll_tier_bounds @@ -2618,8 +2671,8 @@ func _smoke_global_variant() -> void: var crng = _CountVariantRng.new() crng.set_master_seed(42) var boss_settlement = load("res://core/battle_orchestrator.gd").new(_db, crng) - var boss_ok: bool = boss_settlement.begin_battle({"level_index": 2, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}) - check(boss_ok, "L2 Boss 战斗启动(node_depth=4 boss 节点——兜底回落断言载体)") + var boss_ok: bool = boss_settlement.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}) + check(boss_ok, "L2 Boss 战斗启动(node_depth=9 Boss 终点——兜底回落断言载体)") var boss1_ids: Array = [] var boss1_variants: Dictionary = {} var boss1_calls := 0 @@ -2643,7 +2696,7 @@ func _smoke_global_variant() -> void: var crng2 = _CountVariantRng.new() crng2.set_master_seed(42) var boss2 = load("res://core/battle_orchestrator.gd").new(_db, crng2) - if boss2.begin_battle({"level_index": 2, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}): + if boss2.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}): var bb2 = boss2.minesweeper.current_board() check(_arr_str_eq(bb2.board_variant_ids(), boss1_ids) and bb2.variants == boss1_variants and crng2.variant_calls == boss1_calls, "确定性:同种子 Boss 图两遍 board_variants/变异格/取号逐位相同(region 布置路径)") @@ -3022,7 +3075,7 @@ func _smoke_active_refresh() -> void: if String(row.get("type", "")) == "decay" and not bool(ct["waiting_probe_hit"]): ct["waiting_probe_hit"] = true ct["waiting_refresh_rejected"] = not settlement.player_request_refresh()) - check(settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp)}), "轮内刷新战斗启动(Boss 行 150HP——承载多轮样本)") + check(settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}), "轮内刷新战斗启动(Boss 行 150HP——承载多轮样本)") var minesweeper = settlement.minesweeper # 轮 1–3:零挖掘停手攒 RAP = 3×AP(每轮全转存;表驱动——AP 从表读不写死) for i in range(3): @@ -3087,7 +3140,7 @@ func _active_refresh_replay() -> Array: var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) var rows: Array = [] settlement.step_row.connect(func(row): rows.append(row)) - if not settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp)}): + if not settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}): return rows var minesweeper = settlement.minesweeper for i in range(3): @@ -3112,7 +3165,7 @@ func _smoke_difficulty_table() -> void: var enemy_system = load("res://core/unit_container.gd").new(_db) check(enemy_system.spawn(1, 0, int(_db.hero.max_hp)), "spawn 受理") var unit = enemy_system.all_enemies()[0] - var d0: Dictionary = _db.resolve_enemy_rowset(1, 0)[0] # 行集数组口径取首行(零内容期每坐标恰一行,T2 机械适配) + var d0: Dictionary = _db.resolve_enemy_rowset(1, 0, "battle")[0] # 行集首行(带键寻址:战斗节点查 battle 键域) check(int(unit.hp) == int(d0.hp) and int(unit.atk) == int(d0.atk), "空表零修饰(hp=%d atk=%d = 行值 %d/%d)" % [int(unit.hp), int(unit.atk), int(d0.hp), int(d0.atk)]) # ② 修饰行运行时注入(debug 通道——R3 override 同族):套乘生效于 hp/atk _db.difficulty.rows.append({"level_index": 1, "node_depth": 0, "hp_mult": 1.5, "atk_mult": 2.0}) @@ -3140,7 +3193,7 @@ func _smoke_damage_types() -> void: var enemy_system_b = load("res://core/unit_container.gd").new(_db) check(enemy_system_b.spawn(1, 2, int(_db.hero.max_hp)) and String(enemy_system_b.all_enemies()[0].armor_type) == "light", "bugbear 护甲 = light(护甲类型重配批 2026-09-18:关卡 1 全轻甲)") var enemy_system_c = load("res://core/unit_container.gd").new(_db) - check(enemy_system_c.spawn(1, 4, int(_db.hero.max_hp)) and String(enemy_system_c.all_enemies()[0].armor_type) == "light", "ogre_chief 护甲 = light(护甲类型重配批 2026-09-18:关卡 1 全轻甲)") + check(enemy_system_c.spawn(1, 4, int(_db.hero.max_hp), [], -1, -1, [], "", "boss") and String(enemy_system_c.all_enemies()[0].armor_type) == "light", "ogre_chief 护甲 = light(Boss 行集——护甲类型重配批 2026-09-18:关卡 1 全轻甲)") # ② 克制矩阵表值在位(三档点数 克制 +1 / 中性 0 / 被克 −1,加法核非 percent) var am: Dictionary = _db.settlement.armor_effectiveness check(int(am.normal.light) == 0 and int(am.normal.ward) == 1 and int(am.normal.heavy) == -1, "普通行:轻甲 0 / 魔法护甲 +1 / 重甲 −1(加法核)") @@ -3174,7 +3227,7 @@ func _run_seal_backlash_round(seed_val: int) -> Dictionary: var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) var rows: Array = [] settlement.step_row.connect(func(row): rows.append(row)) - if not settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp)}): + if not settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}): return {"rows": rows, "safe_cell": Vector2i(-1, -1), "safe_type": ""} var minesweeper = settlement.minesweeper var safe_t := _find_seal_target(minesweeper, false) @@ -3228,7 +3281,7 @@ func _smoke_seal_irreversible_quadrants() -> void: var settle_a = load("res://core/battle_orchestrator.gd").new(_db, rng_a) var rows_a: Array = [] settle_a.step_row.connect(func(row): rows_a.append(row)) - check(settle_a.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp)}), "四情况 A:开局受理") + check(settle_a.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}), "四情况 A:开局受理") var ms_a = settle_a.minesweeper var t_a := _find_seal_target(ms_a, false) check(not t_a.is_empty(), "四情况 A:找到安全格封印目标") @@ -3268,7 +3321,7 @@ func _smoke_seal_irreversible_quadrants() -> void: var settle_b = load("res://core/battle_orchestrator.gd").new(_db, rng_b) var rows_b: Array = [] settle_b.step_row.connect(func(row): rows_b.append(row)) - check(settle_b.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp)}), "四情况 B:开局受理") + check(settle_b.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}), "四情况 B:开局受理") var ms_b = settle_b.minesweeper var t_b := _find_seal_target(ms_b, true) check(not t_b.is_empty(), "四情况 B:找到危险格封印目标") @@ -3298,7 +3351,7 @@ func _smoke_seal_irreversible_quadrants() -> void: var settle_c = load("res://core/battle_orchestrator.gd").new(_db, rng_c) var rows_c: Array = [] settle_c.step_row.connect(func(row): rows_c.append(row)) - check(settle_c.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp)}), "四情况 C:开局受理") + check(settle_c.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}), "四情况 C:开局受理") var ms_c = settle_c.minesweeper var t_safe := _find_seal_target(ms_c, false) var t_mine := _find_seal_target(ms_c, true) @@ -3403,12 +3456,21 @@ func _smoke_encounter_roll() -> void: check(settlement_d1.begin_battle({"level_index": 1, "node_depth": 1, "hero_hp": int(_db.hero.max_hp)}), "D1 战斗启动") var n1: int = settlement_d1.presentation_snapshot().enemies.size() check(n1 >= 2 and n1 <= 3, "D1 区间 [2,3] 域内(%d 只)" % n1) - # D2 固定(教学位不加骰) + # 熊地精首遇带固定(教学位不加骰)——首遇带数据驱动定位(契约 8,禁写死坐标) + var bugbear_lead_band := _first_encounter_band(1, "bugbear") + var bugbear_lead_row: Dictionary = {} + for row in _db.enemy.rows: + if int(row.level_index) == 1 and String(row.row_kind) == "battle" \ + and int(row.depth_band) == bugbear_lead_band and String(row.enemy_id) == "bugbear": + bugbear_lead_row = row + break var rng4 = load("res://core/rng_service.gd").new() rng4.set_master_seed(31000) var settlement_d2 = load("res://core/battle_orchestrator.gd").new(_db, rng4) - settlement_d2.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.hero.max_hp)}) - check(settlement_d2.presentation_snapshot().enemies.size() == 1, "D2 熊地精固定 1(首遇教学位)") + settlement_d2.begin_battle({"level_index": 1, "node_depth": bugbear_lead_band, "hero_hp": int(_db.hero.max_hp)}) + check(settlement_d2.presentation_snapshot().enemies.size() == 1 \ + and int(bugbear_lead_row.get("unit_count", -1)) == 1 and not bugbear_lead_row.has("unit_count_max"), + "熊地精首遇带(1,battle,%d——扫表取最低带)固定 1 不加骰〔fv23〕" % bugbear_lead_band) # 全深度 × 多种子域内扫描(playtest-2026-09-12 报告 Bug#1 回归——区间下限 1 不可掷 0: # 战斗结算 roll_lo 下限 + 敌人系统 spawn count<1 fail-fast 双闸;单深度单种子是盲区,全域扫描才钉死不变式) var depths: Array = [] @@ -3426,7 +3488,7 @@ func _smoke_encounter_roll() -> void: scan_bad += 1 continue var n: int = settlement_scan.presentation_snapshot().enemies.size() - var erow2: Dictionary = _db.resolve_enemy_rowset(1, int(depth_v))[0] # 行集首行(T2 机械适配) + var erow2: Dictionary = _db.resolve_enemy_rowset(1, int(depth_v), "battle")[0] # 行集首行(扫描面 = 战斗节点) var lo2: int = int(erow2.get("unit_count", 1)) var hi2: int = int(erow2.get("unit_count_max", lo2)) if n < 1 or n < lo2 or n > hi2: @@ -3434,6 +3496,36 @@ func _smoke_encounter_roll() -> void: check(scan_bad == 0, "全深度 × 12 种子 spawn 全域内(%d 场,∈[unit_count, unit_count_max] 且 ≥1)——空遭遇不可达(Bug#1 回归)" % scan_total) +# 节点类型 → 敌人表 row_kind(非 Boss 战斗位恒 battle——深度带批寻址第三维) +func _row_kind_of(level_index: int, depth: int) -> String: + for n in _level_active_nodes(level_index): + if int(n.get("node_depth", -1)) == depth and String(n.get("type", "")) == "boss": + return "boss" + return "battle" + + +# 该关 Boss 节点深度(单终点图的终点编号——数据驱动,禁写死坐标) +func _boss_depth(level_index: int) -> int: + for n in _level_active_nodes(level_index): + if String(n.get("type", "")) == "boss": + return int(n.get("node_depth", -1)) + return -1 + + +# 首遇带数据驱动定位(契约 8):该 (L, battle, enemy_id) 的最低 depth_band,禁写死坐标数字 +func _first_encounter_band(level_index: int, enemy_id: String) -> int: + var best := -1 + for row in _db.enemy.rows: + if int(row.get("level_index", -1)) != level_index: + continue + if String(row.get("row_kind", "")) != "battle" or String(row.get("enemy_id", "")) != enemy_id: + continue + var band := int(row.get("depth_band", -1)) + if best < 0 or band < best: + best = band + return best + + # 模板感知取节点表(row.templates 非空取模板 0=现行固定图,保持既有断言语义) func _level_active_nodes(level_index: int) -> Array: for row in _db.level.rows: @@ -3471,7 +3563,7 @@ func _smoke_attack_chain_fresh_pool() -> void: if row.type == "damage" and row.source_class == "enemy_action": ticks.append({"baseline": int(row.baseline), "attach": int(row.get("attach", 0)), "roll": int(row.roll), "tier": int(row.tier), "crit": bool(row.get("crit", false)), "tick": int(row.value)})) - check(settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp)}), "fresh 池回归战斗启动(Boss 行)") + check(settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}), "fresh 池回归战斗启动(Boss 行)") # 停手轮:零挖掘零封印 → 零授予 → 池恒空;Boss 拍 = 开轮摇号结果(伤害技/普攻兑付伤害行、硬化施放行不计) for i in range(6): if settlement.battle_result != "": @@ -3538,7 +3630,7 @@ func _smoke_clear_reward_zero_paths() -> void: var s = load("res://core/battle_orchestrator.gd").new(_db, rng) var rows: Array = [] s.step_row.connect(func(row): rows.append(row)) - check(s.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80}), + check(s.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80, "node_type": "boss"}), "零兑付构造战斗启动") for u in s._enemy_system.all_enemies(): s._enemy_system.apply_damage(u, int(u.hp)) # 测试通道:构造"无存活敌"(弱形式——全灭而非 despawn) @@ -3556,7 +3648,7 @@ func _smoke_clear_reward_zero_paths() -> void: var s_b = load("res://core/battle_orchestrator.gd").new(_db, rng_b) var rows_b: Array = [] s_b.step_row.connect(func(row): rows_b.append(row)) - check(s_b.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80}), + check(s_b.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80, "node_type": "boss"}), "高光守卫构造战斗启动") for u in s_b._enemy_system.all_enemies(): s_b._enemy_system.apply_damage(u, int(u.hp)) @@ -3573,7 +3665,7 @@ func _smoke_clear_reward_zero_paths() -> void: var s2 = load("res://core/battle_orchestrator.gd").new(_db, rng2) var rows2: Array = [] s2.step_row.connect(func(row): rows2.append(row)) - check(s2.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80}), + check(s2.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80, "node_type": "boss"}), "第二份被拒构造战斗启动") var boss = s2._enemy_system.all_enemies()[0] boss.hp = 1 # 测试通道:压低至首份 aoe 必杀(弱形式——正常流 Boss 血量不可控) @@ -4044,7 +4136,7 @@ func _run_full_clear(seed_val: int, from_refresh: bool, rows: Array) -> Dictiona var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) settlement.step_row.connect(func(row): rows.append(row)) var res := {"cleared": false, "refreshed": false, "rounds": 0} - if not settlement.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, + if not settlement.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "node_type": "boss", "hero_hp": CLEAR_SMOKE_HERO_HP, "hero_max_hp": CLEAR_SMOKE_HERO_HP}): _db.difficulty.rows.clear() return res @@ -4175,44 +4267,55 @@ func _smoke_skill_pool_data() -> void: check(not bash.is_empty() and String(bash.display_name) == "盾击" and String(bash.type) == "damage" \ and is_equal_approx(float(bash.damage_mult), 1.7) and int(bash.cooldown_beats) == 3 and not bash.has("rider_mult"), "盾击行(伤害 ×1.7 · 冷却 3 · 无 rider——迭代 24 N2)") - var spit_sole_ok := true # 毒吐只挂巨蜘蛛族且首遇位 (2,4) 不投(Q6——教学位池零新招) + var spider_lead_band := _first_encounter_band(2, "giant_spider") # 首遇带数据驱动定位(契约 8) + var spit_sole_ok := true # 毒吐只挂巨蜘蛛族且首遇带不投(Q6——教学位池零新招) var bash_sole_ok := true # 盾击只挂灰矮人族三行 for row in _db.enemy.rows: for entry in row.pool: var sid := String(entry.skill_id) - if sid == "venom_spit" and not (String(row.enemy_id) == "giant_spider" and int(row.level_index) == 2 and int(row.node_depth) >= 5): + if sid == "venom_spit" and not (String(row.enemy_id) == "giant_spider" and int(row.level_index) == 2 \ + and int(row.depth_band) > spider_lead_band): spit_sole_ok = false if sid == "shield_bash" and not (String(row.enemy_id) == "duergar" and int(row.level_index) == 3): bash_sole_ok = false - check(spit_sole_ok, "毒吐投放面(仅 (2,5)/(2,6)/(2,7) 巨蜘蛛族——首遇位 (2,4) 池不动,迭代 24 N2/Q6)") - check(bash_sole_ok, "盾击投放面(仅 (3,0)/(3,5)/(3,7) 灰矮人族——钩爪恐魔零触碰,迭代 24 N2)") - # 行集键 = (level_index, node_depth, enemy_id)——分支节点图批起 bugbear 等敌种跨关异池,enemy_id 不再唯一; - # 同坐标混编多行时每行各一池,键升维到三元组(行序 = 文件序 = 契约序) + check(spit_sole_ok, "毒吐投放面(仅 depth_band > 巨蜘蛛首遇带(%d)的巨蜘蛛族行——首遇带池不动,迭代 24 N2/Q6)〔fv24〕" % spider_lead_band) + check(bash_sole_ok, "盾击投放面(仅 (3,·) 灰矮人族——钩爪恐魔零触碰,迭代 24 N2)") + # 首遇带行池恒 {attack:6, bite:2, charge:1}(去写死深度判据——带键口径) + var spider_lead_pool := {} + for row in _db.enemy.rows: + if int(row.level_index) == 2 and String(row.row_kind) == "battle" \ + and int(row.depth_band) == spider_lead_band and String(row.enemy_id) == "giant_spider": + for entry in row.pool: + spider_lead_pool[String(entry.skill_id)] = int(entry.weight) + check(spider_lead_pool == {"attack": 6, "bite": 2, "charge": 1}, + "巨蜘蛛首遇带行 pool 恒 {attack:6, bite:2, charge:1}(教学位池零新招——毒吐首投在更深的带)〔fv24〕") + # 行集键 = (level_index, row_kind, depth_band, enemy_id)——分支节点图批起 bugbear 等敌种跨关异池, + # enemy_id 不再唯一;同带混编多行时每行各一池(行序 = 文件序 = 契约序) var expect_pools := { - "1_0_goblin": {"attack": 9, "lunge": 1}, - "1_1_goblin": {"attack": 9, "lunge": 1}, - "1_2_bugbear": {"attack": 6, "charge": 2, "heavy": 2}, - "1_3_hobgoblin": {"attack": 6, "charge": 2, "heavy": 2}, - "1_4_ogre_chief": {"attack": 6, "heavy": 2, "harden": 2, "quake_smash": 2}, - "2_0_bugbear": {"attack": 6, "lunge": 1, "heavy": 2}, - "2_1_bugbear": {"attack": 6, "lunge": 1, "heavy": 2}, - "2_4_giant_spider": {"attack": 6, "bite": 2, "charge": 1}, - "2_5_giant_spider": {"attack": 6, "bite": 2, "charge": 1, "venom_spit": 2}, - "2_5_bugbear": {"attack": 5, "lunge": 2, "heavy": 2}, - "2_6_giant_spider": {"attack": 6, "bite": 2, "charge": 1, "venom_spit": 2}, - "2_7_giant_spider": {"attack": 6, "bite": 2, "charge": 1, "venom_spit": 2}, - "2_9_drider": {"attack": 6, "heavy": 2, "venom_burst": 2, "web_snare": 2, "venom_curb": 2}, - "3_0_duergar": {"attack": 6, "charge": 2, "heavy": 2, "shield_bash": 2}, - "3_1_hook_horror": {"attack": 6, "bite": 2, "charge": 1}, - "3_4_hook_horror": {"attack": 6, "bite": 2, "charge": 1}, - "3_5_duergar": {"attack": 5, "lunge": 2, "heavy": 2, "shield_bash": 2}, - "3_6_hook_horror": {"attack": 6, "bite": 2, "charge": 1}, - "3_7_duergar": {"attack": 6, "charge": 2, "heavy": 2, "shield_bash": 2}, - "3_9_duergar_king": {"attack": 6, "heavy": 2, "earth_pulse": 2, "rockslide": 2}, + "1_battle_0_goblin": {"attack": 9, "lunge": 1}, + "1_battle_1_goblin": {"attack": 9, "lunge": 1}, + "1_battle_2_bugbear": {"attack": 6, "charge": 2, "heavy": 2}, + "1_battle_3_hobgoblin": {"attack": 6, "charge": 2, "heavy": 2}, + "1_boss_4_ogre_chief": {"attack": 6, "heavy": 2, "harden": 2, "quake_smash": 2}, + "2_battle_0_bugbear": {"attack": 6, "lunge": 1, "heavy": 2}, + "2_battle_1_bugbear": {"attack": 6, "lunge": 1, "heavy": 2}, + "2_battle_4_giant_spider": {"attack": 6, "bite": 2, "charge": 1}, + "2_battle_5_giant_spider": {"attack": 6, "bite": 2, "charge": 1, "venom_spit": 2}, + "2_battle_5_bugbear": {"attack": 5, "lunge": 2, "heavy": 2}, + "2_battle_6_giant_spider": {"attack": 6, "bite": 2, "charge": 1, "venom_spit": 2}, + "2_battle_7_giant_spider": {"attack": 6, "bite": 2, "charge": 1, "venom_spit": 2}, + "2_boss_9_drider": {"attack": 6, "heavy": 2, "venom_burst": 2, "web_snare": 2, "venom_curb": 2}, + "3_battle_0_duergar": {"attack": 6, "charge": 2, "heavy": 2, "shield_bash": 2}, + "3_battle_1_hook_horror": {"attack": 6, "bite": 2, "charge": 1}, + "3_battle_4_hook_horror": {"attack": 6, "bite": 2, "charge": 1}, + "3_battle_5_duergar": {"attack": 5, "lunge": 2, "heavy": 2, "shield_bash": 2}, + "3_battle_6_hook_horror": {"attack": 6, "bite": 2, "charge": 1}, + "3_battle_7_duergar": {"attack": 6, "charge": 2, "heavy": 2, "shield_bash": 2}, + "3_boss_9_duergar_king": {"attack": 6, "heavy": 2, "earth_pulse": 2, "rockslide": 2}, } var pools_ok := true for row in _db.enemy.rows: - var rk := "%d_%d_%s" % [int(row.level_index), int(row.node_depth), String(row.enemy_id)] + var rk := "%d_%s_%d_%s" % [int(row.level_index), String(row.row_kind), int(row.depth_band), String(row.enemy_id)] if not expect_pools.has(rk): pools_ok = false continue @@ -4221,7 +4324,7 @@ func _smoke_skill_pool_data() -> void: got[String(entry.skill_id)] = int(entry.weight) if got != expect_pools[rk]: pools_ok = false - check(pools_ok, "二十行九种池模板 = 建议池行值((L,D,enemy_id) 三元键逐行精确比对——2026-10-07 行集多行批口径;L1 地精 9/1 · 熊地精 6/2/2 · 酋长 6/2/2;L2 熊地精 6/1/2 · 蜘蛛 6/2/1+毒吐2〔(2,5)-(2,7) 投放,首遇位 (2,4) 不投——迭代 24 N2〕 · 重击手 5/2/2 · 蛛化卓尔精灵 6/2/2/2〔(2,9)——商店房批 Boss 重编号〕;L3 灰矮人 6/2/2+盾击2〔(3,0)/(3,7)〕 · 钩爪恐魔 6/2/1 · 重装手 5/2/2+盾击2 · 石君王 6/2/2〔earth_pulse·(3,9)〕——全部 provisional;三 Boss 池尾 +1 门控条目〔quake_smash/venom_curb/rockslide 权重 2+unlock_hp_pct 50——迭代14④ T5〕;混编行集同坐标多行各一池合法——首批投放 (2,5) 双行〔迭代 18 卡⑤:重击手+巢群,新行行值复制 (2,6)〕,其余坐标仍恰一行)") + check(pools_ok, "二十行九种池模板 = 建议池行值((L,row_kind,depth_band,enemy_id) 四元键逐行精确比对;L1 地精 9/1 · 熊地精 6/2/2 · 酋长 6/2/2;L2 熊地精 6/1/2 · 蜘蛛 6/2/1+毒吐2〔首遇带不投——迭代 24 N2〕 · 重击手 5/2/2 · 蛛化卓尔精灵 6/2/2/2〔Boss〕;L3 灰矮人 6/2/2+盾击2 · 钩爪恐魔 6/2/1 · 重装手 5/2/2+盾击2 · 石君王 6/2/2〔earth_pulse〕——全部 provisional;三 Boss 池尾 +1 门控条目〔quake_smash/venom_curb/rockslide 权重 2+unlock_hp_pct 50——迭代14④ T5〕;混编行集同带多行各一池合法——首批投放 (2,battle,5) 双行〔迭代 18 卡⑤:重击手+巢群〕,其余带键仍恰一行)") var basic_count_ok := true for row in _db.enemy.rows: var n := 0 @@ -4253,7 +4356,7 @@ func _smoke_skill_pool_data() -> void: var st_unlock: Dictionary = {"rows": []} var sett_u = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(4242)) sett_u.step_row.connect(func(row): st_unlock["rows"].append(row)) - check(sett_u.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.hero.max_hp)}), "前置:卓尔 Boss 战受理(2,9——解锁技 venom_curb 门槛 50)") + check(sett_u.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}), "前置:卓尔 Boss 战受理(2,9——解锁技 venom_curb 门槛 50)") var boss_u = sett_u._enemy_system.all_enemies()[0] # 白盒夹具(smoke 惯例——autoplay 同族私有面) sett_u._enemy_system.apply_damage(boss_u, (int(boss_u.max_hp) - 65)) check(int(boss_u.hp) == 65, "前置:Boss 压到阈值 65(=round(130×0.5))") @@ -4270,13 +4373,13 @@ func _smoke_skill_pool_data() -> void: var rollable_ok := true for depth in [0, 1, 2, 3, 4]: var es = load("res://core/unit_container.gd").new(_db) - es.spawn(1, int(depth), int(_db.hero.max_hp)) + es.spawn(1, int(depth), int(_db.hero.max_hp), [], -1, -1, [], "", _row_kind_of(1, int(depth))) for u in es.all_enemies(): if es.rollable_skills(u).is_empty(): rollable_ok = false for depth in [0, 1, 4, 5, 6, 7, 8]: var es2 = load("res://core/unit_container.gd").new(_db) - es2.spawn(2, int(depth), int(_db.hero.max_hp)) + es2.spawn(2, int(depth), int(_db.hero.max_hp), [], -1, -1, [], "", _row_kind_of(2, int(depth))) for u in es2.all_enemies(): if es2.rollable_skills(u).is_empty(): rollable_ok = false @@ -4393,7 +4496,7 @@ func _smoke_skill_pool_death_void() -> void: for s in range(1, 400): var rg = load("res://core/rng_service.gd").new() rg.set_master_seed(s) - var erow: Dictionary = _db.resolve_enemy_rowset(1, 2)[0] # 行集首行(T2 机械适配) + var erow: Dictionary = _db.resolve_enemy_rowset(1, 2, "battle")[0] # 行集首行(战斗节点 battle 键域) var entries: Array = [] for entry in erow.pool: entries.append({"skill_id": String(entry.skill_id), "weight": int(entry.weight)}) @@ -4500,7 +4603,7 @@ func _smoke_debuff_rider() -> void: var settlement = load("res://core/battle_orchestrator.gd").new(_db, rg) var rows: Array = [] settlement.step_row.connect(func(row): rows.append(row)) - if not settlement.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.hero.max_hp)}): + if not settlement.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}): continue var boss_atk: int = int(settlement._enemy_system.all_enemies()[0].atk) # 开战掷定攻击基线(毒分量随其缩放) for i in range(6): @@ -4670,53 +4773,54 @@ func _smoke_level_graph() -> void: check(green_ok, "首遇位零踩雷:完美档 12 种子(节点 0 熊地精)恒零踩雷") check(green_wins >= 7, "首遇战胜局 ≥7/12(实际 %d——阶段拔高后锚行退出绿档,多数胜口径)" % green_wins) check(green_min_hp > 0, "首遇战胜后 HP 下探读数 %d(>0——绿档记录)" % green_min_hp) - # ⑦ 跨表行完整:L2 每战斗/Boss 节点 (2, 编号) 在 board_gen 与 enemy 精确行集(加载校验同款显式复述; - # 精确行集口径——enemy 侧同坐标多行合法,键聚合坐标级天然兼容) + # ⑦ 跨表行完整:L2 每战斗/Boss 节点在 board_gen 与 enemy 精确行集(加载校验同款显式复述; + # enemy 键域 = (level_index, row_kind, depth_band)——战斗节点查 battle 键域、Boss 节点查 boss 键域) var bg_keys := {} for row in _db.board_gen.rows: bg_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true var en_keys := {} for row in _db.enemy.rows: - en_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true + en_keys["%d_%s_%d" % [int(row.level_index), String(row.get("row_kind", "")), int(row.get("depth_band", -1))]] = true var cross_ok := true for node in _level_active_nodes(2): var t := String(node.get("type", "")) if t != "battle" and t != "boss": continue - var key := "2_%d" % int(node.node_depth) - if not bg_keys.has(key) or not en_keys.has(key): + if not bg_keys.has("2_%d" % int(node.node_depth)) or not en_keys.has("2_%s_%d" % [t, int(node.node_depth)]): cross_ok = false - check(cross_ok, "跨表行完整:L2 全部战斗/Boss 节点在 board_gen + enemy 精确行集(7 节点)") - # ⑦a 定点:L2 首遇行集与 Boss 行集(精确行集口径——断言行集构成而非单行: - # 零内容期每坐标恰一行 = 单元素行集;行序 = 文件序,首行即叙述取数面) - var l2_spider_set: Array = _db.resolve_enemy_rowset(2, 4) + check(cross_ok, "跨表行完整:L2 全部战斗/Boss 节点在 board_gen + enemy 精确行集(7 节点——带键 (2,row_kind,depth_band))") + # ⑦a 定点:L2 首遇带行集与 Boss 行集(首遇带 = 数据驱动最低带,契约 8——禁写死坐标; + # 断言行集构成而非单行:行序 = 文件序,首行即叙述取数面) + var l2_spider_band: int = _first_encounter_band(2, "giant_spider") + var l2_spider_set: Array = _db.resolve_enemy_rowset(2, l2_spider_band, "battle") var l2_spider_ids := {} for r in l2_spider_set: l2_spider_ids[String(r.get("enemy_id", ""))] = true - check(l2_spider_ids.has("giant_spider") and l2_spider_ids.size() == 1 and l2_spider_set.size() == 1, - "巨蜘蛛首遇行集 (2,4) = {giant_spider} 恰一行(固定 2 只——首遇教学位不加骰;行集口径断言——行集构成纪律下单行集 = 现行形态)") - var l2_boss_set: Array = _db.resolve_enemy_rowset(2, 9) + check(l2_spider_band >= 0 and l2_spider_ids.has("giant_spider") and l2_spider_ids.size() == 1 and l2_spider_set.size() == 1 \ + and int(l2_spider_set[0].get("unit_count", -1)) == 2 and not l2_spider_set[0].has("unit_count_max"), + "巨蜘蛛首遇带(2,battle,%d——扫表取最低带)行集 = {giant_spider} 恰一行且 unit_count=2 无 unit_count_max(固定 2 只——首遇教学位不加骰)〔fv22〕" % l2_spider_band) + var l2_boss_set: Array = _db.resolve_enemy_rowset(2, _boss_depth(2), "boss") var l2_boss_ids := {} for r in l2_boss_set: l2_boss_ids[String(r.get("enemy_id", ""))] = true check(l2_boss_ids.has("drider") and l2_boss_ids.size() == 1 and String(l2_boss_set[0].get("armor_type", "")) == "ward", - "蛛化卓尔精灵 Boss 行集 (2,9) = {drider} 恰一行且首行魔法甲(护甲类型重配批 2026-09-18:关卡 2 唯一非轻甲;商店房批重编号 (2,8)→(2,9);P4 乙 Boss 行集多行合法——零内容期仍单行)") - # ⑦b 定点:混编遭遇行集 (2,5)(R6 行序契约运行面: + "蛛化卓尔精灵 Boss 行集(2,boss,%d)= {drider} 恰一行且首行魔法甲(护甲类型重配批 2026-09-18:关卡 2 唯一非轻甲;P4 乙 Boss 行集多行合法——零内容期仍单行)〔fv24〕" % _boss_depth(2)) + # ⑦b 定点:混编遭遇行集 (2,battle,5)(R6 行序契约运行面: # 行集两行文件内相邻、行序 = 声明序 = 生成序;静态最坏骰和 Σ(unit_count_max,固定行缺省 = unit_count) ≤ 4) - var l2_mix_set: Array = _db.resolve_enemy_rowset(2, 5) + var l2_mix_set: Array = _db.resolve_enemy_rowset(2, 5, "battle") var mix_shape_ok: bool = l2_mix_set.size() == 2 \ and String(l2_mix_set[0].get("enemy_id", "")) == "bugbear" \ and String(l2_mix_set[1].get("enemy_id", "")) == "giant_spider" check(mix_shape_ok, - "首批混编行集 (2,5) = 双行且行序 [bugbear 重击手 → giant_spider 巢群](R6 行序契约——文件序 = 声明序 = 生成序,首行默认目标 = 前排坦克;迭代 18 卡⑤投放)") + "首批混编行集 (2,battle,5) = 双行且行序 [bugbear 重击手 → giant_spider 巢群](R6 行序契约——文件序 = 声明序 = 生成序,首行默认目标 = 前排坦克;迭代 18 卡⑤投放)〔fv18〕") var mix_sum_max := 0 for r in l2_mix_set: mix_sum_max += int(r.get("unit_count_max", int(r.get("unit_count", 1)))) check(mix_sum_max <= 4, - "首批混编行集 (2,5) 静态最坏骰和 = Σ(unit_count_max,固定行缺省=unit_count) = %d ≤ 4(行集实例总数守卫同口径——重击手固定 1 + 巢群 [2,3])" % mix_sum_max) + "首批混编行集 (2,battle,5) 静态最坏骰和 = Σ(unit_count_max,固定行缺省=unit_count) = %d ≤ 4(行集实例总数守卫同口径——重击手固定 1 + 巢群 [2,3])〔fv19〕" % mix_sum_max) check(int(l2_mix_set[1].get("unit_count", 0)) == 2 and int(l2_mix_set[1].get("unit_count_max", 0)) == 3 \ and int(l2_mix_set[1].get("hp", 0)) == 26 and int(l2_mix_set[1].get("armor_class", -1)) == 2, - "首批混编新行 (2,5,giant_spider) 行值 = 复制 (2,6) 巢群行(P4 甲零数值新增——2-3 只/hp 26/atk [1,2]/light/ac 2;既有 19 行现值零变更)") + "首批混编新行 (2,battle,5,giant_spider) 行值 = 复制巢群行(P4 甲零数值新增——2-3 只/hp 26/atk [1,2]/light/ac 2;既有 19 行现值零变更)") # ---- L3 灰矮人矿城断言段 ---- # ⑧ L3 行集结构(复用 L2 九节点图模板——6 战 + Boss + 奖励 + 恢复,真实岔路 4 处) var l3 := {} @@ -4733,29 +4837,32 @@ func _smoke_level_graph() -> void: check(int(tcount3.battle) == 6 and int(tcount3.boss) == 1, "L3 普通战斗节点 = 6 + Boss 恒一(编号最大恒终点)") check(int(tcount3.reward) == 1 and int(tcount3.rest) == 1 and int(tcount3.shop) == 1, "L3 恢复节点 1(rest_heal_pct 行字段)+ 奖励节点 1(直通空态)+ 商店房节点 1(商店房批)") check(branch3 == 3, "L3 真实岔路 = %d 处(0→[1,2,3,4,5] · 2→[6,7] · 5→[6,7]——U2 返工批五岔环廊层宽 5)" % branch3) - # ⑨ 跨表行完整:L3 每战斗/Boss 节点 (3, 编号) 在 board_gen 与 enemy 精确行集(⑦ 的 bg_keys/en_keys 复用) + # ⑨ 跨表行完整:L3 每战斗/Boss 节点在 board_gen 与 enemy 精确行集(⑦ 的 bg_keys/en_keys 复用—— + # 键域同形:(L,row_kind,depth_band)) var cross_ok3 := true for node in _level_active_nodes(3): var t3 := String(node.get("type", "")) if t3 != "battle" and t3 != "boss": continue - var key3 := "3_%d" % int(node.node_depth) - if not bg_keys.has(key3) or not en_keys.has(key3): + if not bg_keys.has("3_%d" % int(node.node_depth)) or not en_keys.has("3_%s_%d" % [t3, int(node.node_depth)]): cross_ok3 = false - check(cross_ok3, "跨表行完整:L3 全部战斗/Boss 节点在 board_gen + enemy 精确行集(7 节点)") - # ⑩ 定点:L3 首遇行集与 Boss 行集(克制面重轻错开——重甲灰矮人首次登台;行集口径断言同 ⑦a) - var l3_lead_set: Array = _db.resolve_enemy_rowset(3, 0) + check(cross_ok3, "跨表行完整:L3 全部战斗/Boss 节点在 board_gen + enemy 精确行集(7 节点——带键 (3,row_kind,depth_band))") + # ⑩ 定点:L3 首遇带行集与 Boss 行集(克制面重轻错开——重甲灰矮人首次登台;首遇带数据驱动定位同 ⑦a) + var l3_lead_band: int = _first_encounter_band(3, "duergar") + var l3_lead_set: Array = _db.resolve_enemy_rowset(3, l3_lead_band, "battle") var l3_lead_ids := {} for r in l3_lead_set: l3_lead_ids[String(r.get("enemy_id", ""))] = true - check(l3_lead_ids.has("duergar") and l3_lead_ids.size() == 1 and String(l3_lead_set[0].get("armor_type", "")) == "heavy", - "灰矮人首遇行集 (3,0) = {duergar} 恰一行且首行重甲(重甲首次登台——固定 1 只,首遇教学位不加骰)") - var l3_king_set: Array = _db.resolve_enemy_rowset(3, 9) + check(l3_lead_band >= 0 and l3_lead_ids.has("duergar") and l3_lead_ids.size() == 1 \ + and String(l3_lead_set[0].get("armor_type", "")) == "heavy" \ + and int(l3_lead_set[0].get("unit_count", -1)) == 1 and not l3_lead_set[0].has("unit_count_max"), + "灰矮人首遇带(3,battle,%d——扫表取最低带)行集 = {duergar} 恰一行、首行重甲、固定 1 只不加骰〔fv23〕" % l3_lead_band) + var l3_king_set: Array = _db.resolve_enemy_rowset(3, _boss_depth(3), "boss") var l3_king_ids := {} for r in l3_king_set: l3_king_ids[String(r.get("enemy_id", ""))] = true check(l3_king_ids.has("duergar_king") and l3_king_ids.size() == 1 and String(l3_king_set[0].get("armor_type", "")) == "heavy", - "灰矮人石君王 Boss 行集 (3,9) = {duergar_king} 恰一行且首行重甲(重甲收束「轻甲→魔法甲→重甲」教学梯度;商店房批重编号 (3,8)→(3,9))") + "灰矮人石君王 Boss 行集(3,boss,%d)= {duergar_king} 恰一行且首行重甲(重甲收束「轻甲→魔法甲→重甲」教学梯度)〔fv24〕" % _boss_depth(3)) # ⑪ L3 成长代理档首遇绿档:模拟 GP 商店成长后的入关参数(atk_pct=200 / hp_pct=100)· 完美档多种子首遇战稳过 # (零加成基线红档属预期——断言口径 = 成长代理档,非基线;跑法同 ⑥) var green3_ok := true @@ -4781,23 +4888,23 @@ func _smoke_level_graph() -> void: check(green3_ok, "L3 首遇位绿档:成长代理档口径(atk_pct=200/hp_pct=100)完美档 12 种子首遇战(节点 0 灰矮人)全胜且零踩雷") check(green3_min_hp > 0, "L3 首遇战胜后 HP 下探读数 %d(>0——绿档记录)" % green3_min_hp) - # ⑫ 混编行集运行面正反断言:注入坐标 (1,5)/(1,6) = - # L1 节点图与全部既有扫描面之外的新深度(ceil 请求命中注入行集——新 L 会被 ceil 封顶到 L3 不可用), + # ⑫ 混编行集运行面正反断言:注入带键 (1,battle,5)/(1,battle,6) = + # L1 节点图与全部既有扫描面之外的新带键(带命中精确取注入行集——新 L 会被 ceil 封顶到 L3 不可用), # 断言后立即还原表,后续断言面零污染。R3 fail-fast 正反验证 + R6 行序运行面验证。 - var mix_row_a := {"level_index": 1, "node_depth": 5, "enemy_id": "goblin", "display_name": "地精", "unit_count": 1, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]} - var mix_row_b := {"level_index": 1, "node_depth": 5, "enemy_id": "bugbear", "display_name": "熊地精", "unit_count": 2, "hp": 20, "atk": 3, "pool": [{"skill_id": "attack", "weight": 9}]} + var mix_row_a := {"level_index": 1, "row_kind": "battle", "depth_band": 5, "enemy_id": "goblin", "display_name": "地精", "unit_count": 1, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]} + var mix_row_b := {"level_index": 1, "row_kind": "battle", "depth_band": 5, "enemy_id": "bugbear", "display_name": "熊地精", "unit_count": 2, "hp": 20, "atk": 3, "pool": [{"skill_id": "attack", "weight": 9}]} _db.enemy.rows.append(mix_row_a) _db.enemy.rows.append(mix_row_b) var mix_sys = load("res://core/unit_container.gd").new(_db) - check(mix_sys.spawn(1, 5, int(_db.hero.max_hp)), "混编行集 spawn 受理((1,5) 双行 地精1+熊地精2——定值无区间路径,行集构成纪律放开面正向)") + check(mix_sys.spawn(1, 5, int(_db.hero.max_hp)), "混编行集 spawn 受理((1,battle,5) 双行 地精1+熊地精2——定值无区间路径,行集构成纪律放开面正向)") var mix_units: Array = mix_sys.all_enemies() check(mix_units.size() == 3, "混编实例总数 = 各行数量求和(1+2=3——行集实例总数 ≤4 守卫内)") check(String(mix_units[0].get("enemy_id")) == "goblin" and String(mix_units[1].get("enemy_id")) == "bugbear" and String(mix_units[2].get("enemy_id")) == "bugbear", "混编生成序 = 文件行序(行序即契约——地精在前熊地精在后,handle_id 跨行连续派发)") check(String(mix_units[0].get("display_name")) == "地精" and int(mix_units[1].get("hp")) == 20 and int(mix_units[1].get("atk")) == 3, "混编各行行值独立投影(display_name/hp/atk 按各自行值——同场不同种不同值)") - _db.enemy.rows.append({"level_index": 1, "node_depth": 6, "enemy_id": "goblin", "display_name": "地精", "unit_count": 3, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]}) - _db.enemy.rows.append({"level_index": 1, "node_depth": 6, "enemy_id": "bugbear", "display_name": "熊地精", "unit_count": 3, "hp": 20, "atk": 3, "pool": [{"skill_id": "attack", "weight": 9}]}) + _db.enemy.rows.append({"level_index": 1, "row_kind": "battle", "depth_band": 6, "enemy_id": "goblin", "display_name": "地精", "unit_count": 3, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]}) + _db.enemy.rows.append({"level_index": 1, "row_kind": "battle", "depth_band": 6, "enemy_id": "bugbear", "display_name": "熊地精", "unit_count": 3, "hp": 20, "atk": 3, "pool": [{"skill_id": "attack", "weight": 9}]}) var over_sys = load("res://core/unit_container.gd").new(_db) check(not over_sys.spawn(1, 6, int(_db.hero.max_hp)), "行集实例总数守卫:两行各定值 3(总 6 > 4)spawn 被拒(行集构成纪律③——R3 fail-fast 运行面)") _db.enemy.rows.remove_at(_db.enemy.rows.size() - 1) @@ -4930,7 +5037,7 @@ func _smoke_gp_escrow() -> void: var settle_l = load("res://core/battle_orchestrator.gd").new(_db, rng_l) var fin_l := {} settle_l.battle_finished.connect(func(p): fin_l["p"] = p) - check(settle_l.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "hero_atk": 999}), + check(settle_l.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "hero_atk": 999, "node_type": "boss"}), "破财直验战斗启动") for i in range(3): settle_l._grant_routed({"reward_id": "gp_lose", "cell_type": "gp", "polarity": "negative", "display_name": "破财", "power": 2, "effect_id": "gp_loss", "stacks": 1}, @@ -4938,7 +5045,11 @@ func _smoke_gp_escrow() -> void: check(int(settle_l.escrow) == -6, "破财 ×3 → escrow −6(负向缓冲记账,零钱包触达)") settle_l.minesweeper.settle_batch([{"action": "stop"}]) var gp_l: int = int(fin_l.get("p", {}).get("gp_dropped", 999)) - check(gp_l + 6 >= 2 and gp_l + 6 <= 4, "破财压制 gp_dropped 负值上抛(%d + 6 ∈ [2,4]——掷定不受破财污染)" % gp_l) + var boss_drop: Dictionary = _db.shop_gp_drop_row("boss") + var boss_bonus1: int = _db.shop_level_gp_bonus(1) + check(gp_l + 6 >= int(boss_drop.get("roll_min", 0)) + boss_bonus1 and gp_l + 6 <= int(boss_drop.get("roll_max", 0)) + boss_bonus1, + "破财压制 gp_dropped 负值上抛(%d + 6 = %d ∈ Boss 档掷定 [%d,%d] + 关级加值 %d——掷定不受破财污染)" % [ + gp_l, gp_l + 6, int(boss_drop.get("roll_min", 0)), int(boss_drop.get("roll_max", 0)), boss_bonus1]) # ③ 钱包不透支(clamp ≥0):runner 驱动 L1 首战 + 战斗中注入破财 → gp_dropped 负值 → 钱包钳制归 0 var runner = load("res://core/level_runner.gd").new(_db, _fresh_rng(4242)) runner.set_hero_override(49850, 0) # atk 2 → 999(快速终局;hp 基线) @@ -5138,17 +5249,17 @@ func _smoke_shop_graph() -> void: if not p.has(8): via_shop = false check(paths.size() >= 2 and via_shop, "L%d 全部 %d 条入口→Boss 路径穿商店房 8(必经位——DFS 枚举)" % [L, paths.size()]) - # 跨表校验:(·,9) Boss 行精确存在 · (·,8) 无战斗行残留(陈旧行清理确认) + # 跨表校验:(·,boss,9) Boss 行精确存在 · (·,battle,8) 无战斗行残留(陈旧行清理确认) var bg_keys := {} for row in _db.board_gen.rows: bg_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true var en_keys := {} for row in _db.enemy.rows: - en_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true + en_keys["%d_%s_%d" % [int(row.level_index), String(row.get("row_kind", "")), int(row.get("depth_band", -1))]] = true for L in [2, 3]: - check(bg_keys.has("%d_9" % L) and en_keys.has("%d_9" % L), "(%d,9) Boss 行在 board_gen + enemy 精确存在" % L) - check(not bg_keys.has("%d_8" % L) and not en_keys.has("%d_8" % L), "(%d,8) 商店房无板/行集残留(陈旧行已清理)" % L) - check(bg_keys.has("1_4") and en_keys.has("1_4") and not bg_keys.has("1_8") and not en_keys.has("1_8"), + check(bg_keys.has("%d_9" % L) and en_keys.has("%d_boss_9" % L), "(%d,boss,9) Boss 行在 board_gen + enemy 精确存在" % L) + check(not bg_keys.has("%d_8" % L) and not en_keys.has("%d_battle_8" % L), "(%d,battle,8) 商店房无板/战斗行集残留(陈旧行已清理)" % L) + check(bg_keys.has("1_4") and en_keys.has("1_boss_4") and not bg_keys.has("1_8") and not en_keys.has("1_battle_8"), "L1 行集零改动(单链 Boss (1,4)——教学期最小修订面)") diff --git a/game/tests/unit/battle_settlement/skill_unlock_test.gd b/game/tests/unit/battle_settlement/skill_unlock_test.gd index 54fa6d1a..9dd35a37 100644 --- a/game/tests/unit/battle_settlement/skill_unlock_test.gd +++ b/game/tests/unit/battle_settlement/skill_unlock_test.gd @@ -24,9 +24,9 @@ func _init() -> void: # 门谓词:卓尔 (2,9) hp 130(×mult 1.0)→ venom_curb unlock 50 → 阈值 round(130×0.5)=65 -func _boss(db, L: int, D: int): +func _boss(db, L: int, D: int, kind := "boss"): var ec = UnitContainerScript.new(db) - var ok: bool = ec.spawn(L, D, int(db.hero.max_hp)) + var ok: bool = ec.spawn(L, D, int(db.hero.max_hp), [], -1, -1, [], "", "", kind) Kit.check("前置:spawn (%d,%d) 受理" % [L, D], ok) return ec @@ -82,7 +82,7 @@ func _test_latch_irreversible(db) -> void: # 普通行无字段逐位零扰动:地精 (1,0) 池快照零 unlock 键、过滤恒空转、可摇=全池 func _test_plain_rows_zero_drift(db) -> void: - var ec = _boss(db, 1, 0) + var ec = _boss(db, 1, 0, "battle") var u: Dictionary = _boss_u(ec) var any_unlock_key := false for e in u.pool: @@ -94,7 +94,7 @@ func _test_plain_rows_zero_drift(db) -> void: var pool_count: int = (u.pool as Array).size() Kit.check("零扰动:可摇集合 = 全池(普攻冷却恒 0,过滤恒空转)", rollable.size() == pool_count) # 同种子确定性:容器零随机——两实例同输入查询逐位同 - var ec2 = _boss(db, 1, 0) + var ec2 = _boss(db, 1, 0, "battle") var u2: Dictionary = _boss_u(ec2) ec.apply_damage(u, 3) ec2.apply_damage(u2, 3) diff --git a/game/tests/unit/core/config_db_cross_validator_test.gd b/game/tests/unit/core/config_db_cross_validator_test.gd index 9d18836f..f3fac232 100644 --- a/game/tests/unit/core/config_db_cross_validator_test.gd +++ b/game/tests/unit/core/config_db_cross_validator_test.gd @@ -16,6 +16,7 @@ func _init() -> void: _test_all_links_valid() _test_dangling_skill_id() _test_cast_side_mismatch() + _test_level_node_band_rules() quit(Kit.finish("config_db_cross_validator_test")) @@ -51,7 +52,7 @@ func _test_all_links_valid() -> void: {"skill_id": "raise_morale", "type": "buff", "buff_ref": "st_strong"}, ]) var enemies := _enemy([ - {"level_index": 1, "node_depth": 0, "enemy_id": "goblin", "pool": [ + {"level_index": 1, "row_kind": "battle", "depth_band": 0, "enemy_id": "goblin", "pool": [ {"skill_id": "attack", "weight": 9}, {"skill_id": "raise_morale", "weight": 3}]}, ]) var status := _status([{"effect_id": "st_strong", "target_side": "enemy"}]) @@ -63,7 +64,7 @@ func _test_all_links_valid() -> void: func _test_dangling_skill_id() -> void: var skills := _skill([{"skill_id": "attack", "type": "basic"}]) var enemies := _enemy([ - {"level_index": 1, "node_depth": 0, "enemy_id": "goblin", "pool": [ + {"level_index": 1, "row_kind": "battle", "depth_band": 0, "enemy_id": "goblin", "pool": [ {"skill_id": "attack", "weight": 9}, {"skill_id": "not_in_table", "weight": 3}]}, ]) var errs: Array = ValidatorScript.new().validate_skill_pool_links(skills, enemies, null) @@ -76,9 +77,67 @@ func _test_dangling_skill_id() -> void: func _test_cast_side_mismatch() -> void: var skills := _skill([{"skill_id": "raise_morale", "type": "buff", "buff_ref": "st_hero_side"}]) var enemies := _enemy([ - {"level_index": 1, "node_depth": 0, "enemy_id": "goblin", "pool": [ + {"level_index": 1, "row_kind": "battle", "depth_band": 0, "enemy_id": "goblin", "pool": [ {"skill_id": "raise_morale", "weight": 3}]}, ]) var status := _status([{"effect_id": "st_hero_side", "target_side": "hero"}]) var errs: Array = ValidatorScript.new().validate_skill_pool_links(skills, enemies, status) Kit.check("buff 挂主角侧状态被报(施放类 side 守卫)", _cats(errs).has("cast_side")) + + +# ---- ④ 节点 × 敌人表带寻址(单终点零误报/同带双 Boss 报错/顶 clamp 消解报错)---- +const LevelTableScript := preload("res://data/schemas/level_table.gd") +const BoardGenTableScript := preload("res://data/schemas/board_gen_table.gd") +const RewardTableScript := preload("res://data/schemas/reward_table.gd") +const SettlementTableScript := preload("res://data/schemas/settlement_table.gd") + + +func _loaded(level_nodes: Array, bg_depths: Array, boss_bands: Array, battle_bands: Array) -> Dictionary: + var lv := LevelTableScript.new() + lv.rows.assign([{"level_index": 1, "display_name": "夹具关", "templates": [{"nodes": level_nodes}]}]) + var bg := BoardGenTableScript.new() + var bg_rows: Array = [] + for d in bg_depths: + bg_rows.append({"level_index": 1, "node_depth": int(d), "board_width": 10}) + bg.rows.assign(bg_rows) + var en := EnemyTableScript.new() + var en_rows: Array = [] + for b in battle_bands: + en_rows.append({"level_index": 1, "row_kind": "battle", "depth_band": int(b), "enemy_id": "goblin", + "unit_count": 1, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]}) + for b in boss_bands: + en_rows.append({"level_index": 1, "row_kind": "boss", "depth_band": int(b), "enemy_id": "ogre_chief", + "unit_count": 1, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]}) + en.rows.assign(en_rows) + var st := SettlementTableScript.new() + st.source_label_map.assign({"safe_cell": "a", "seal_hit": "b", "mine_hit": "c", "clear_reward": "d", + "hero_attack": "e", "enemy_action": "f", "seal_miss": "g"}) + return {"status_effects": StatusTableScript.new(), "reward": RewardTableScript.new(), + "settlement": st, "level": lv, "board_gen": bg, "enemy": en} + + +func _cross_cats(loaded: Dictionary, needle: String) -> Array[String]: + var out: Array[String] = [] + ValidatorScript.new().cross_validate(loaded, + func(_sev, cat, msg) -> void: + if String(msg).contains(needle): + out.append(String(cat))) + return out + + +func _test_level_node_band_rules() -> void: + # 正向:单终点三关同构(battle 节点落 band 0、boss 节点精确落顶带)→ ④ 零误报 + var ok_nodes := [{"type": "battle", "node_depth": 0}, {"type": "boss", "node_depth": 9}] + var ok_cats := _cross_cats(_loaded(ok_nodes, [0, 9], [9], [0]), "level 1") + Kit.check("单终点图 ④ 敌侧带寻址零误报〔fv17〕", ok_cats.is_empty()) + # 负向:一路两终点落同一 Boss 带(深度 3/4 都落带 4)→ rowset_rule + var same_band_nodes := [{"type": "battle", "node_depth": 0}, + {"type": "boss", "node_depth": 3}, {"type": "boss", "node_depth": 4}] + var same_band_cats := _cross_cats(_loaded(same_band_nodes, [0, 3, 4], [4, 9], [0]), "同带") + Kit.check("同关双终点共带报 rowset_rule〔fv15〕", + same_band_cats.has("rowset_rule")) + # 负向:Boss 节点深度高于顶带键(顶 clamp 消解)→ level_node_dangling + var over_nodes := [{"type": "battle", "node_depth": 0}, {"type": "boss", "node_depth": 10}] + var over_cats := _cross_cats(_loaded(over_nodes, [0, 10], [9], [0]), "顶 clamp 消解") + Kit.check("Boss 节点越顶带报 level_node_dangling〔fv16〕", + over_cats.has("level_node_dangling")) diff --git a/game/tests/unit/core/config_db_resolver_test.gd b/game/tests/unit/core/config_db_resolver_test.gd index 132d5cb5..cc60df4a 100644 --- a/game/tests/unit/core/config_db_resolver_test.gd +++ b/game/tests/unit/core/config_db_resolver_test.gd @@ -1,6 +1,6 @@ # tests/unit/core/config_db_resolver_test.gd — 配置解析直接单测 -# 断言面 = ceil 进度键解析语义(向上取整保证偏难)、行集多行的行序保序、 -# 难度修饰乘数的 clamp 域。行集由夹具注入,不碰生产表——期望值由注入行数出来。 +# 断言面 = 深度带寻址语义(请求落带取偏难一侧、行集多行行序保序、越顶带 clamp+告警去重、 +# 行类路由、越域 fatal)、难度修饰乘数的 clamp 域。行集由夹具注入,不碰生产表——期望值由注入行数出来。 # 运行:godot --headless --path game --script res://tests/unit/core/config_db_resolver_test.gd extends SceneTree @@ -19,8 +19,11 @@ func _init() -> void: func() -> Array: return _enemy_rows(), func(): return _difficulty_table(), ) - _test_ceil_progress_key() + _test_band_hit() _test_rowset_order_preserved() + _test_top_band_clamp() + _test_row_kind_routing() + _test_request_oob() _test_difficulty_clamp() quit(Kit.finish("config_db_resolver_test")) @@ -33,11 +36,13 @@ func _board_gen_rows() -> Array: ] +# 带键 [0,5]:请求 4 未精确落键,取首个 ≥4 的带 5(偏难一侧),不回带 0 func _enemy_rows() -> Array: return [ - {"level_index": 1, "node_depth": 0, "enemy_id": "goblin"}, - {"level_index": 1, "node_depth": 0, "enemy_id": "bugbear"}, - {"level_index": 1, "node_depth": 5, "enemy_id": "hobgoblin"}, + {"level_index": 1, "row_kind": "battle", "depth_band": 0, "enemy_id": "goblin"}, + {"level_index": 1, "row_kind": "battle", "depth_band": 0, "enemy_id": "bugbear"}, + {"level_index": 1, "row_kind": "battle", "depth_band": 5, "enemy_id": "hobgoblin"}, + {"level_index": 1, "row_kind": "boss", "depth_band": 9, "enemy_id": "ogre_chief"}, ] @@ -48,26 +53,65 @@ func _difficulty_table(): return _FakeDifficulty.new(rows) -# ceil 语义:请求 (1,1) 落在 d=0 与 d=3 之间,向上取到 d=3(偏难),不是向下取 d=0 -func _test_ceil_progress_key() -> void: - var hit: Dictionary = _resolver.resolve_board_gen(1, 1) - Kit.check("ceil 进度键:请求 (1,1) 命中 (1,3) 而非 (1,0)——偏难语义", - int(hit.get("node_depth", -1)) == 3 and int(hit.get("board_width", 0)) == 12) - var exact: Dictionary = _resolver.resolve_board_gen(2, 0) - Kit.check("精确命中:(2,0) 取本行", int(exact.get("board_width", 0)) == 14) +func _test_band_hit() -> void: + var hit: Array = _resolver.resolve_enemy_rowset(1, 4, "battle") + Kit.check("带命中取偏难一侧:请求深度 4 命中带 5(逐字不回带 0)〔fv1〕", + hit.size() == 1 and String((hit[0] as Dictionary).get("enemy_id", "")) == "hobgoblin") + var exact: Array = _resolver.resolve_enemy_rowset(1, 5, "battle") + Kit.check("精确落带:请求深度 5 命中带 5〔fv1〕", + exact.size() == 1 and String((exact[0] as Dictionary).get("enemy_id", "")) == "hobgoblin") func _test_rowset_order_preserved() -> void: - var rows: Array = _resolver.resolve_enemy_rowset(1, 0) - Kit.check("行集返回同坐标全部行(2 行)", rows.size() == 2) + var rows: Array = _resolver.resolve_enemy_rowset(1, 0, "battle") + Kit.check("行集返回同带全部行(2 行)〔fv2〕", rows.size() == 2) var ids: Array[String] = [] for r in rows: ids.append(String((r as Dictionary).get("enemy_id", ""))) - Kit.check("行集按表文件行序返回(goblin 先于 bugbear——行序即契约)", + Kit.check("行集按表文件行序返回(goblin 先于 bugbear——行序即契约)〔fv2〕", ids == ["goblin", "bugbear"]) - var far: Array = _resolver.resolve_enemy_rowset(1, 4) - Kit.check("ceil 行集:请求 (1,4) 命中 (1,5)", far.size() == 1 - and String((far[0] as Dictionary).get("enemy_id", "")) == "hobgoblin") + + +func _test_top_band_clamp() -> void: + _diags.clear() + var rows: Array = _resolver.resolve_enemy_rowset(1, 99, "battle") + Kit.check("越顶带 clamp 取顶带行集(battle 顶带 5)〔fv3〕", + rows.size() == 1 and String((rows[0] as Dictionary).get("enemy_id", "")) == "hobgoblin") + var clamp_hit := false + for d in _diags: + if d.contains("warning/resolve_depth_clamp"): + clamp_hit = true + Kit.check("越顶带发 warning resolve_depth_clamp〔fv3〕", clamp_hit) + var warn_before: int = _diags.size() + _resolver.resolve_enemy_rowset(1, 99, "battle") + Kit.check("同键重复请求警告去重(四元组注册表)〔fv3〕", _diags.size() == warn_before) + + +func _test_row_kind_routing() -> void: + var battle_rows: Array = _resolver.resolve_enemy_rowset(1, 0, "battle") + var boss_rows: Array = _resolver.resolve_enemy_rowset(1, 0, "boss") + Kit.check("行类路由:同请求深度 battle 与 boss 命中不同键域〔fv4〕", + battle_rows.size() == 2 and boss_rows.size() == 1 + and String((boss_rows[0] as Dictionary).get("enemy_id", "")) == "ogre_chief") + + +func _test_request_oob() -> void: + _diags.clear() + _resolver.resolve_enemy_rowset(0, 0, "battle") + Kit.check("关卡序号 <1 → fatal request_oob〔fv5〕", _has_fatal_oob()) + _diags.clear() + _resolver.resolve_enemy_rowset(1, -1, "battle") + Kit.check("节点深度 <0 → fatal request_oob〔fv5〕", _has_fatal_oob()) + _diags.clear() + _resolver.resolve_enemy_rowset(1, 0, "elite") + Kit.check("row_kind 非法 → fatal request_oob〔fv5〕", _has_fatal_oob()) + + +func _has_fatal_oob() -> bool: + for d in _diags: + if d.contains("fatal/request_oob"): + return true + return false func _test_difficulty_clamp() -> void: diff --git a/game/tests/unit/data_config/config_db_test.gd b/game/tests/unit/data_config/config_db_test.gd index dca3b48f..3d229531 100644 --- a/game/tests/unit/data_config/config_db_test.gd +++ b/game/tests/unit/data_config/config_db_test.gd @@ -11,7 +11,7 @@ const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:_check/汇 func _init() -> void: var db = ConfigDBScript.new() var load_ok: bool = db.load_tables("res://data/tables", true) - Kit.check("全部表加载 + 校验通过(含跨表护栏零 error 诊断)", load_ok and db.diagnostics_count("error") == 0) + Kit.check("全部表加载 + 校验通过(含跨表护栏零 error 诊断)〔fv6〕", load_ok and db.diagnostics_count("error") == 0) Kit.check("is_ready 通道随加载置位", db.is_ready) if not load_ok: for d in db.diagnostics(): @@ -59,8 +59,8 @@ func _test_progress_resolution(db) -> void: # enemy 行集同通道:同 (L,D) 行集数组非空(关卡节点地图 前置——battle 节点双表精确行集契约; # 行集面契约:返回形态是行集数组,精确性断言随行集面) - var en_got: Array = db.resolve_enemy_rowset(int(probe_row.level_index), int(probe_row.node_depth)) - Kit.check("enemy 行集同坐标解析非空(双表精确行契约)", not en_got.is_empty()) + var en_got: Array = db.resolve_enemy_rowset(int(probe_row.level_index), int(probe_row.node_depth), "battle") + Kit.check("enemy 行集同坐标解析非空(双表精确行契约——battle 行类)", not en_got.is_empty()) # 深度 clamp:请求深度超最大行 → 取该关最大深度行 + 警告诊断一次(去重) db.reset_diagnostics() @@ -197,44 +197,46 @@ func _test_rowset_discipline() -> void: # 数组字面量会触发「Invalid assignment」脚本错误——该错误不改退出码,却会**中断本函数剩余断言** #(本函数五条纪律断言曾因此全部静默不跑,套件仍报 all green,是零覆盖假象)。 const EnemyTableScript := preload("res://data/schemas/enemy_table.gd") - var mk_row := func(l: int, d: int, eid: String) -> Dictionary: - return {"level_index": l, "node_depth": d, "enemy_id": eid, "display_name": eid + "名", + var mk_row := func(l: int, kind: String, band: int, eid: String) -> Dictionary: + return {"level_index": l, "row_kind": kind, "depth_band": band, "enemy_id": eid, "display_name": eid + "名", "unit_count": 1, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]} - # 正向:同坐标两行不同敌种(相邻)= 混编合法,validate 零错 + # 正向:同带键两行不同敌种(相邻)= 混编合法,validate 零错 var t_mix = EnemyTableScript.new() - t_mix.rows.assign([mk_row.call(1, 5, "goblin"), mk_row.call(1, 5, "bugbear"), - mk_row.call(1, 0, "goblin")]) # 末行=depth-0 锚行(validate 的 anchor 规则:每 level 必须有) + t_mix.rows.assign([mk_row.call(1, "battle", 5, "goblin"), mk_row.call(1, "battle", 5, "bugbear"), + mk_row.call(1, "battle", 0, "goblin"), mk_row.call(1, "boss", 9, "ogre_chief")]) var errs: Array = t_mix.validate() - Kit.check("混编行集加载通过(同坐标两行不同敌种相邻——行集恰一敌种校验退役的正向面)", errs.is_empty()) - # 正向:Boss 坐标两行同构合法(P4 乙——Boss 行集校验同样放开,加载层无 Boss 单行硬闸) + Kit.check("混编行集加载通过(同带键两行不同敌种相邻——行集恰一敌种校验退役的正向面)", errs.is_empty()) + # 正向:Boss 行集两行同构合法(P4 乙——Boss 行集校验同样放开,加载层无 Boss 单行硬闸) var t_boss = EnemyTableScript.new() - t_boss.rows.assign([mk_row.call(2, 9, "drider"), mk_row.call(2, 9, "bugbear"), - mk_row.call(2, 0, "drider")]) # 末行=depth-0 锚行 + t_boss.rows.assign([mk_row.call(2, "battle", 0, "drider"), mk_row.call(2, "boss", 9, "drider"), + mk_row.call(2, "boss", 9, "bugbear")]) errs = t_boss.validate() - Kit.check("Boss 坐标混编行集加载通过(P4 乙——Boss 多行合法,无硬闸)", errs.is_empty()) - # 负向:同坐标行不相邻 = rowset_rule 且报错定位到坐标(R6 行序纪律 + R3 错误类别可定位) + Kit.check("Boss 带混编行集加载通过(P4 乙——Boss 多行合法,无硬闸)", errs.is_empty()) + # 负向:同行集行不相邻 = rowset_rule 且报错定位到带键(R6 行序纪律 + R3 错误类别可定位) var t_gap = EnemyTableScript.new() - t_gap.rows.assign([mk_row.call(1, 5, "goblin"), mk_row.call(1, 9, "hobgoblin"), mk_row.call(1, 5, "bugbear"), - mk_row.call(1, 0, "goblin")]) # 末行=depth-0 锚行(隔断关系不受影响) + t_gap.rows.assign([mk_row.call(1, "battle", 5, "goblin"), mk_row.call(1, "battle", 9, "hobgoblin"), + mk_row.call(1, "battle", 5, "bugbear"), mk_row.call(1, "battle", 0, "goblin"), + mk_row.call(1, "boss", 9, "ogre_chief")]) errs = t_gap.validate() var gap_hit := false for e in errs: - if String(e.get("category", "")) == "rowset_rule" and String(e.get("message", "")).contains("(1,5)"): + if String(e.get("category", "")) == "rowset_rule" and String(e.get("message", "")).contains("(1,battle,5)"): gap_hit = true - Kit.check("行序相邻校验:同坐标行被隔断报 rowset_rule 且定位坐标(R6——行序即契约加载期兜底)", gap_hit) + Kit.check("行序相邻校验:同带键行被隔断报 rowset_rule 且定位带键〔fv9〕〔fv20〕", gap_hit) # 负向:同敌种第二行仍被 dup_key 拦(键含 enemy_id——纪律①既有通道不回归) var t_dup = EnemyTableScript.new() - t_dup.rows.assign([mk_row.call(1, 5, "goblin"), mk_row.call(1, 5, "goblin"), - mk_row.call(1, 0, "goblin")]) # 末行=depth-0 锚行 + t_dup.rows.assign([mk_row.call(1, "battle", 5, "goblin"), mk_row.call(1, "battle", 5, "goblin"), + mk_row.call(1, "battle", 0, "goblin"), mk_row.call(1, "boss", 9, "ogre_chief")]) errs = t_dup.validate() var dup_hit := false for e in errs: if String(e.get("category", "")) == "dup_key": dup_hit = true - Kit.check("行集内同敌种第二行仍被 dup_key 拦(纪律①——enemy_id 唯一)", dup_hit) + Kit.check("同带键同敌种二行被 dup_key 新键拦(纪律①——enemy_id 唯一)〔fv8〕", dup_hit) # 负向:域校验不回归(单行 unit_count 越域仍拦——UNIT_COUNT_MAX 单种数语义下的同款域闸) var t_dom = EnemyTableScript.new() - t_dom.rows.assign([mk_row.call(1, 5, "goblin"), mk_row.call(1, 0, "goblin")]) # 末行=depth-0 锚行 + t_dom.rows.assign([mk_row.call(1, "battle", 5, "goblin"), mk_row.call(1, "battle", 0, "goblin"), + mk_row.call(1, "boss", 9, "ogre_chief")]) t_dom.rows[0]["unit_count"] = 4 errs = t_dom.validate() var dom_hit := false @@ -242,3 +244,30 @@ func _test_rowset_discipline() -> void: if String(e.get("category", "")) == "domain" and String(e.get("message", "")).contains("unit_count"): dom_hit = true Kit.check("单行数量域 [1,3] 域闸不回归(UNIT_COUNT_MAX 语义 = 单种数)", dom_hit) + # 负向:row_kind 封闭枚举(非法值拒——键面换代后新增判据) + var t_kind = EnemyTableScript.new() + t_kind.rows.assign([mk_row.call(1, "elite", 5, "goblin"), mk_row.call(1, "battle", 0, "goblin"), + mk_row.call(1, "boss", 9, "ogre_chief")]) + errs = t_kind.validate() + var kind_hit := false + for e in errs: + if String(e.get("category", "")) == "domain" and String(e.get("message", "")).contains("row_kind"): + kind_hit = true + Kit.check("非法 row_kind 被拒(封闭枚举 {battle, boss})〔fv7〕", kind_hit) + # 负向:anchor 两半——缺 battle band 0 锚行、缺 Boss 行集,各自报 anchor + var t_no_anchor = EnemyTableScript.new() + t_no_anchor.rows.assign([mk_row.call(1, "battle", 5, "goblin"), mk_row.call(1, "boss", 9, "ogre_chief")]) + errs = t_no_anchor.validate() + var no_anchor_hit := false + for e in errs: + if String(e.get("category", "")) == "anchor" and String(e.get("message", "")).contains("band 0"): + no_anchor_hit = true + Kit.check("缺 (battle, band 0) 锚行报 anchor〔fv10〕", no_anchor_hit) + var t_no_boss = EnemyTableScript.new() + t_no_boss.rows.assign([mk_row.call(1, "battle", 0, "goblin"), mk_row.call(1, "battle", 5, "bugbear")]) + errs = t_no_boss.validate() + var no_boss_hit := false + for e in errs: + if String(e.get("category", "")) == "anchor" and String(e.get("message", "")).contains("Boss"): + no_boss_hit = true + Kit.check("缺 Boss 行集报 anchor(每关 Boss 行集 ≥1)〔fv10〕", no_boss_hit) From 8b18e057b73f83dd55e4156a38668203d4024fa4 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Thu, 22 Oct 2026 04:27:58 +0800 Subject: [PATCH 3/6] =?UTF-8?q?fix(=E6=95=8C=E4=BA=BA=E8=A1=A8=E6=B7=B1?= =?UTF-8?q?=E5=BA=A6=E5=B8=A6):=20=E9=9D=99=E6=80=81=E6=89=AB=E6=8F=8F?= =?UTF-8?q?=E5=BD=92=E9=9B=B6=E2=80=94=E2=80=94resolver=20=E8=A1=8C?= =?UTF-8?q?=E9=9B=86=E5=AE=B9=E5=99=A8=E6=98=BE=E5=BC=8F=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E5=8C=96=EF=BC=8C=E8=A1=A8=E6=B3=A8=E9=87=8A=E9=80=80=E5=BD=B9?= =?UTF-8?q?=E8=AF=8D=E6=94=B9=E7=99=BD=E8=AF=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/core/config_db_resolver.gd | 3 ++- game/data/tables/enemy.tres | 2 +- game/smoke/skill_rhythm_probe.gd | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/game/core/config_db_resolver.gd b/game/core/config_db_resolver.gd index 8bd98da7..34852bdd 100644 --- a/game/core/config_db_resolver.gd +++ b/game/core/config_db_resolver.gd @@ -41,7 +41,8 @@ func resolve_enemy_rowset(L: int, D: int, row_kind: String) -> Array[Dictionary] if not EnemyTable.ROW_KINDS.has(row_kind): _diag.call("fatal", "request_oob", "enemy 请求行类非法 (%d,%d,\"%s\")——row_kind 域 {battle, boss}" % [L, D, row_kind]) return [] - var rows: Array = _enemy_rows_get.call() + var rows: Array[Dictionary] = [] + rows.assign(_enemy_rows_get.call()) var level_keys: Array[int] = [] for row in rows: var lv := int(row.get("level_index", 0)) diff --git a/game/data/tables/enemy.tres b/game/data/tables/enemy.tres index 897cd197..615ca76b 100644 --- a/game/data/tables/enemy.tres +++ b/game/data/tables/enemy.tres @@ -670,7 +670,7 @@ rows = [{ ; 调参二补充:上述八行 atk 区间 1-3→0-2(中点恒等期望当量锚不变式——atk=1 需区间中点同步 1;0=掷点空刀,保随机手感)。 ; ; 混编遭遇首批投放批(2026-10-07 迭代 18 卡⑤——需求卡片 v1.2 裁点全裁 P1-P4 甲;机制前置=敌人表行集多行批 -; 已落籍 9cc4b8b):首个混编遭遇行落表 = (2,5) 双行行集〔R6 行序契约:同坐标两行文件内相邻、 +; 已提交 9cc4b8b):首个混编遭遇行落表 = (2,5) 双行行集〔R6 行序契约:同坐标两行文件内相邻、 ; 行序=声明序=生成序——resolver 返回序/生成序/断言面全依赖文件行序〕: ; 行 1 = 既有 熊地精重击手 (2,5,bugbear)(行值零变更); ; 行 2 = 新增 (2,5,giant_spider) 巨蜘蛛巢群——行值整行复制 (2,6) 巢群行(P4 甲零数值新增:unit_count 2/ diff --git a/game/smoke/skill_rhythm_probe.gd b/game/smoke/skill_rhythm_probe.gd index d6a8ee47..edfd02cb 100644 --- a/game/smoke/skill_rhythm_probe.gd +++ b/game/smoke/skill_rhythm_probe.gd @@ -1,7 +1,7 @@ # VERTICAL SLICE - NOT FOR PRODUCTION # 定位:分析工具·非验收件——零断言取数工具,不出入任何闸。 # 技能池节奏探针:地精一伙 (1,1) 750 出招零突刺异常的定位—— -# ① 表侧:resolve_enemy_rowset(1,1) 的池快照内容; +# ① 表侧:resolve_enemy_rowset(1, 1, "battle") 的池快照内容; # ② 摇号侧:隔离 SkillLottery.draw 大样本份额; # ③ 战斗侧:真实 (1,1) 战斗逐执行行按 skill_id 计数。 # 运行:Godot_console.exe --headless --path . --script res://smoke/skill_rhythm_probe.gd From 7bb418913ec32da5155de5b31a60d87090011445 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Thu, 22 Oct 2026 04:46:23 +0800 Subject: [PATCH 4/6] =?UTF-8?q?iter34=20=E5=8D=A1=E2=91=A2=20T4:=20?= =?UTF-8?q?=E4=BA=A7=E5=87=BA=E7=89=A9=E8=90=BD=E4=BD=8D=E2=80=94=E2=80=94?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E6=8A=A5=E5=91=8A/=E4=BB=A3=E6=8B=8D?= =?UTF-8?q?=E6=9D=BF=E8=BF=BD=E8=AE=A4=E5=8D=95/=E5=8F=8D=E5=93=BA?= =?UTF-8?q?=E7=82=B9=E6=B8=85=E5=8D=95=E9=9A=8F=E6=89=B9=E5=85=A5=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- reports/iter34-代拍板-2026-10-22.md | 18 ++++ reports/iter34-反哺点-2026-10-22.md | 37 ++++++++ reports/iter34-实现报告-2026-10-22.md | 117 ++++++++++++++++++++++++++ 3 files changed, 172 insertions(+) create mode 100644 reports/iter34-代拍板-2026-10-22.md create mode 100644 reports/iter34-反哺点-2026-10-22.md create mode 100644 reports/iter34-实现报告-2026-10-22.md diff --git a/reports/iter34-代拍板-2026-10-22.md b/reports/iter34-代拍板-2026-10-22.md new file mode 100644 index 00000000..2ecf5818 --- /dev/null +++ b/reports/iter34-代拍板-2026-10-22.md @@ -0,0 +1,18 @@ +# 迭代 34 代拍板追认单(2026-10-22) + +> 本单只登记实现期在卡片 §10 白名单内所做的代拍项,属备案确认,不是事后授权。白名单依据=`reports/需求卡片-敌人表深度带换代批-2026-10-21.md` §10。 + +## 卡③ 敌人表深度带换代批 + +| # | 白名单条目 | 实际做法 | 约束核验 | +|---|---|---|---| +| 1 | 带切法实现形态与带键编号(§10-1) | 新增 `EnemyTable.band_of(band_keys, D)` 纯静态函数:对升序带键取首个 ≥ D 的键,全部小于 D 时返回 -1;resolver 侧 -1 → 取顶带+`resolve_depth_clamp` 告警(四元组去重沿用) | 迁移期带键=旧 `node_depth` 数值,请求逐位等价;单点收口(resolver 与跨表校验都调这一个函数);D<0 仍走 `request_oob` 致命通道 | +| 2 | 新字段/常量命名与枚举值(§10-2) | 表列名 `row_kind`/`depth_band`;常量 `EnemyTable.ROW_KINDS := ["battle", "boss"]`;枚举字符串与关卡节点 `type` 词表同字 | 与节点 type 词表一致;非法值由 `validate` 的 `domain` 类别拒绝 | +| 3 | `resolve_enemy_rowset` 第三参命名与缺省映射(§10-3) | 第三参命名 `row_kind`;`BattleOrchestrator` 由 `enc.node_type` 取值、缺省 `"battle"` | 生产路径 `level_runner` 恒传 `node_type`;缺省值只服务冒烟/单测直呼面 | +| 4 | 校验错误类别命名(§10-4) | 全部沿用既有类别:`domain`/`dup_key`/`rowset_rule`/`anchor`/`level_node_dangling` | 未新增类别;Boss 同带与顶带消解分别复用 `rowset_rule` 与 `level_node_dangling` | +| 5 | 冒烟断言的定位写法与措辞(§10-5) | 首遇位一律改为扫表定位:`_first_encounter_band(L, enemy_id)` 取该 (关卡, battle, 敌种) 的最低带;断言名带 fv 尾缀 | 首遇定位零写死坐标;写死坐标的旧断言全部移除 | +| 6 | 提交切分与分支命名(§10-6) | 分支 `iter34-enemy-band`;卡③分两笔:核心与数据面一笔、断言面一笔 | 两笔都在 `game/` 内,批次合并时按 diff 推代码域 | + +## 白名单外未代拍事项 + +无。实现期未遇到需要超出 §10 清单自决的设计分歧。 diff --git a/reports/iter34-反哺点-2026-10-22.md b/reports/iter34-反哺点-2026-10-22.md new file mode 100644 index 00000000..41ffda99 --- /dev/null +++ b/reports/iter34-反哺点-2026-10-22.md @@ -0,0 +1,37 @@ +# 迭代 34 反哺点清单(2026-10-22) + +> 本批是代码实现批,`design/` 整目录与根规则文件只读。以下五条是实现完成后需要对设计文档与流程清单做的改写,逐条写清「哪个文件哪一节、改成什么、依据」。用户裁决后由文档批落文。 + +## ① 敌人系统.md:坐标记法与表校验措辞换代 + +- 文件与位置:`design/gdd/systems/战斗循环/敌人系统.md` :81(hobgoblin 缺口登记) +- 改成:坐标记法由「(1,3),即第一关第四节点」改为「(1, battle, 3)」。数据行位置未变(仍在第一关带 3),只换记法。 +- 依据:本卡契约 1(寻址键由 (关卡, 节点深度) 换成 (关卡, 行类, 深度带));冒烟 `smoke_all.gd` 的 hobgoblin 键位断言〔fv12〕按新记法钉死。 + +- 文件与位置:同文件 :278(表校验项清单中的行集构成违例一句) +- 改成:「行集内同敌种第二行 / 同坐标多行不相邻」→「同带键多行不相邻」;同时把「同坐标」类表述统一为「同带键」。 +- 依据:本卡契约 4(行集构成纪律由坐标相邻改为带键相邻);`enemy_table.gd` 的 `rowset_rule` 校验键串已换为 `"%d_%s_%d"`。 + +## ② 关卡节点地图.md:两处定位信息表述统一 + +- 文件与位置:`design/gdd/systems/冒险循环/关卡节点地图.md` :146 +- 改成:本句「本文递出关卡序号 + 节点编号,按编号生成单位归它」统一为「本文递出关卡序号+节点深度;敌人侧按深度带配池生成单位」。 +- 依据:同文件 :119 既有定稿句「关卡序号 + 节点深度……按深度带配池」,两处表述当前不一致;本批实现侧 `level_runner` 递出 `node_depth`、敌人侧 `band_of` 落带,以此为准。 + +## ③ 关卡节点图生成.md:产出规格补「多终点深度互异」条款 + +- 文件与位置:`design/gdd/systems/冒险循环/关卡节点图生成.md` :41–:48(产出约束五条) +- 改成:在产出约束中补一条——多终点(多 Boss 节点)必须落在互异的深度带;同一深度带出现两个终点即判产出无效(理由:共带会让「走哪条路打谁」落空)。 +- 依据:本卡契约 6 的图侧兑现处;实现侧 `config_db_cross_validator.gd` 已新增「同日图多 Boss 同带」报错(类别 `rowset_rule`,断言〔fv15〕),顶带消解另报 `level_node_dangling`〔fv16〕。 + +## ④ 待办清单.md:条目状态回写与退役记载 + +- 文件与位置:`待办清单.md` 中本卡登记条目 +- 改成:状态推进为「已实现(迭代 34)」并写明实现报告路径;按中间文档退役规则登记本卡与迭代产出物的退役去向。 +- 依据:五段流水线状态机(`doc-conventions` §12);实现批不写该文件,回写归批后收尾或用户发令。 + +## ⑤ roadmap.md:迭代 34 行批内状态刷新 + +- 文件与位置:`roadmap.md` 迭代 34 行(两卡并列,本卡为卡③)与顶部「当前位」节 +- 改成:卡③状态记为已实现;卡④(信息迷雾可见性)仍在同一迭代内推进;「当前位」按实际进度改写。 +- 依据:迭代实现批的收尾义务(`iter` 技能第 5 步账面随批);实现批不写该文件,换代归批后收尾或用户发令。 diff --git a/reports/iter34-实现报告-2026-10-22.md b/reports/iter34-实现报告-2026-10-22.md new file mode 100644 index 00000000..1bb1008f --- /dev/null +++ b/reports/iter34-实现报告-2026-10-22.md @@ -0,0 +1,117 @@ +# 迭代 34 实现报告(2026-10-22) + +> 依据:`roadmap.md` 迭代 34 行与两份需求卡片。本批为代码实现批,产出物只写 `reports/`。 +> 卡③「敌人表深度带换代批」已完成;卡④「信息迷雾可见性批」另行施工(见第五节)。 + +## 一、批次概况 + +| 项 | 内容 | +|---|---| +| 工作树 | `D:\code\g-plan2-iter34-enemy`,分支 `iter34-enemy-band` | +| 开工基线(主区 master,603eba31) | 冒烟 1156 项检查 0 失败;单元 35 套件全绿;探针 `--click` 全绿 | +| 卡③收口读数 | 冒烟 1161 项检查 0 失败;单元 35 套件全绿;探针 `--click` 与 `--full` 全绿;静态扫描归零 | +| 合并前全量代码闸 | 见第四节 | + +## 二、卡③ 敌人表深度带换代批 + +### 2.1 任务卡状态 + +| T 卡 | 内容 | 状态 | +|---|---|---| +| T0 | 事实坐实与迁移表核对(§4.2 逐行) | 完成(迁移后 20 行键序与表序逐行一致〔fv11〕) | +| T1 | 带切法单点、resolver 三参签名、表键迁移、schema 校验换代 | 完成(提交 07f3eb55) | +| T2 | 跨表校验 ④ 敌侧带化、Boss 同带与顶带消解两条负向判据、消费面(叙述/结算)随动 | 完成(提交 07f3eb55) | +| T3 | 冒烟与单测断言面按 §8.1 S 清单换代、七项变异验证 | 完成(提交 61537da8、2 笔修补) | +| T4 | 三份产出物、独立验收审计、合并 | 进行中(本报告+审计+合并) | + +### 2.2 实际改动—验收对照表 + +生产侧关键落点:`game/data/schemas/enemy_table.gd`(`band_of` :157、`ROW_KINDS` :12、`row_kind` 枚举判据 :57–:58、锚行与 Boss 行集 :149);`game/core/config_db_resolver.gd`(三参签名 :33、带寻址与两档告警 :44–:80);`game/core/config_db.gd`(门面 :193);`game/core/config_db_cross_validator.gd`(④ 敌侧带化 :94–:106);`game/core/unit_container.gd`(行类参数 :38、解析 :43);`game/core/battle_orchestrator.gd`(行类取值 :328、透传 :359);`game/presentation/battle_narrator.gd:76`;`game/data/tables/enemy.tres`(20 行键迁移)。 + +| fv | 需求点(简) | 验收断言(文件:行) | 层级 | 真实入口 | 撤销/变异结果 | +|---|---|---|---|---|---| +| fv1 | 带命中偏难一侧 | `config_db_resolver_test.gd:58`、:61 | 单元 | `resolve_enemy_rowset` 直调 | 变异 `band_of` 改 floor → 红(实测两断言均红) | +| fv2 | 行集按表文件行序返回 | `config_db_resolver_test.gd:67`、:71 | 单元 | 同上 | 变异混编两行对调 → 红 | +| fv3 | 越顶带取顶带+告警去重 | `config_db_resolver_test.gd:78`、:84、:87 | 单元 | 同上(D 高于顶带、重复请求两次) | 变异 floor → 红(实测) | +| fv4 | row_kind 路由 | `config_db_resolver_test.gd:93` | 单元 | 同 D 分别传 battle/boss | 撤销第三参 → 双 kind 不再分域 | +| fv5 | 越域与非法行类致命 | `config_db_resolver_test.gd:101`、:104、:107 | 单元 | `resolve_enemy_rowset(0,…)`/`(L,-1,…)`/`(1,0,"elite")` | 撤销判据 → 不报致命 | +| fv6 | 生产表加载校验零错误 | `config_db_test.gd:14` | 接口 | `ConfigDB.load_tables` | 回退键迁移 → 校验红 | +| fv7 | 非法 row_kind 被拒 | `config_db_test.gd:256` | 接口 | `EnemyTable.validate` 非法枚举夹具 | 变异注释枚举判据 → 红(实测) | +| fv8 | dup_key 新键拦截 | `config_db_test.gd:235` | 接口 | 同带键同敌种二行夹具 | 回退键串 → 漏拦 | +| fv9/fv20 | 同带隔断报 rowset_rule | `config_db_test.gd:225` | 接口 | 隔断夹具 | 回退 rowset_rule 键串 → 漏拦 | +| fv10 | 锚行与 Boss 行集缺失报 anchor | `config_db_test.gd:265`、:273 | 接口 | 缺锚/缺 Boss 夹具 | 回退 anchor 判据 → 漏报 | +| fv11 | 20 行计数与键序 | `smoke_all.gd:2106` | 接口 | 加载全表逐行比对 | 变异对调/迁错带 → 红(实测) | +| fv12/fv25 | hobgoblin 在 (1,battle,3) 留任 | `smoke_all.gd:2114`、:2116 | 接口 | 同上读表面+表注释件核 | 变异迁错带 → 红(实测) | +| fv13 | 三 Boss 行类与带键就位 | `smoke_all.gd:2132` | 接口 | 同上读表面 | 回退任一行类 → 红 | +| fv14 | 值列零变更 | git diff 件核 | 件核 | `git diff` 逐值列比对 | 改任一值列 → 件核红 | +| fv15 | 同关双终点共带报错 | `config_db_cross_validator_test.gd:137` | 单元 | `cross_validate()` 双 Boss 同带夹具 | 变异删判据 → 红(实测) | +| fv16 | 顶带消解报错 | `config_db_cross_validator_test.gd:142` | 单元 | 节点深度高于顶带夹具 | 变异判据短路 `if false` → 红(实测) | +| fv17 | 单终点图零误报 | `config_db_cross_validator_test.gd:132` | 单元 | 现役三关夹具 | 回退 ④ 敌侧换代 → 红 | +| fv18 | 混编行序 | `smoke_all.gd:4815` | 集成 | 冒烟读 (2,battle,5) 行集 | 变异对调两行 → 红(实测) | +| fv19 | 混编骰和 ≤ 4 | `smoke_all.gd:4820` | 集成 | 同行集静态求和 | 放宽判据 → 红 | +| fv21 | 首遇带数据驱动定位 | `smoke_all.gd:2282` | 集成 | 扫表取最低带 | 撤销扫表定位 → 取键失配 | +| fv22 | 巨蜘蛛首遇带固定 2 不加骰 | `smoke_all.gd:4801` | 集成 | 扫表定位+行集读数 | 变异加 `unit_count_max: 3` → 红(实测) | +| fv23 | bugbear/duergar 首遇带固定 1 | `smoke_all.gd:3473`、:4859 | 集成 | 扫表定位+行集读数 | 变异加 `unit_count_max` → 红 | +| fv24 | Boss 行集与毒吐投放面 | `smoke_all.gd:4281`、:4291、:4807、:4865 | 集成 | 扫表定位+Boss 行集 | 回退投放面判据 → 红 | +| fv25 | hobgoblin 缺口登记随行 | `smoke_all.gd:2116` | 接口 | 表文件注释在位 | 同 fv12 | + +覆盖说明:`game/` 全域共 35 条断言带 fv 尾缀,覆盖 fv1–fv25(fv14 按卡片约定走 git diff 件核,代码侧无断言);fv9 与 fv20、fv12 与 fv25 各自共用同一断言行,计数时不重复。 + +### 2.3 变异验证记录表(注入 → 最窄闸见红 → 还原) + +| # | 注入内容 | 最窄闸 | 红断言(原文摘要) | 还原确认 | +|---|---|---|---|---| +| 1 | `EnemyTable.band_of` 的 `>=` 改为 `<=`(反向 floor) | `config_db_resolver_test.gd` | 带命中取偏难一侧:请求深度 4 命中带 5〔fv1〕;精确落带〔fv1〕;越顶带 clamp〔fv3〕×2 | 改回 `>=`,复跑全绿 | +| 2 | 注释掉 `validate` 的 `row_kind ∈ ROW_KINDS` 判据 | `config_db_test.gd` | 非法 row_kind 被拒(封闭枚举)〔fv7〕 | 恢复判据,复跑全绿 | +| 3 | 删除跨表校验「Boss 同带」判据 | `config_db_cross_validator_test.gd` | 同关双终点共带报 rowset_rule〔fv15〕 | 恢复判据,复跑全绿 | +| 4 | 顶带消解判据短路为 `if false` | `config_db_cross_validator_test.gd` | Boss 节点越顶带报 level_node_dangling〔fv16〕 | 恢复判据,复跑全绿 | +| 5 | 巨蜘蛛首遇行(2,battle,4)加 `unit_count_max: 3` | 冒烟全量 | 巨蜘蛛首遇带行集 = {giant_spider} 恰一行且 unit_count=2 无 unit_count_max〔fv22〕 | 删该字段,冒烟复跑全绿 | +| 6 | 混编两行(2,battle,5)文件内对调 | 冒烟全量 | 混编行序 [bugbear→giant_spider]〔fv18〕;20 行键序〔fv11〕;行值复制面 | 还原行序,冒烟复跑全绿 | +| 7 | hobgoblin 迁错带键(迁到 (1,battle,2)) | 冒烟全量 | 首次注入直接被加载期同带相邻规则拦下(表校验先红,键位断言未及运行);二次注入改为与 band 2 行互换带键(表可加载),命中「hobgoblin 行在 (1,battle,3)」〔fv12〕〔fv25〕与 20 行键序〔fv11〕 | 还原带键,冒烟复跑全绿 | + +拦截边界如实记录:第 7 项按卡片写法直接把 hobgoblin 放到带 2 时,报表在加载期校验即被拦下(同带键行被隔断),键位断言的敏感度无法单独观测;改用「与带 2 行互换带键」的表内合法变异后键位断言变红,证明该断言能拦住「行在错带」这一类改动。另:第 1 项连带 fv3 变红,属同一算法单点的预期耦合,不另计。 + +### 2.4 卡片与实际改动面的偏差(逐条说明) + +1. **S10a 补位**:卡片 S10a 列了 11 个补 `node_type: "boss"` 的位置,实际还需在 `smoke_all.gd` 的 CLEAR_SMOKE_DEPTH 段与 `(2,9)` 段若干直呼点补同一字段,并把 `spawn(1,4)`、深度循环 `spawn` 补行类参数。理由:这些点同样按 Boss 语义取行集,漏补会让它们落回 battle 键域、读到错的敌人行。语义未变,只是行类改为显式传。 +2. **gp 掉落断言改数据驱动**:原断言把「破财压制」战斗的 `gp_dropped` 与普通档掷定区间硬编码比对;该战斗在 S10a 中被标为 Boss 语义后,掉落档合法切换到 Boss 档,硬编码区间不再成立。改为从 `shop.tres` 读 Boss 档区间与关级加值再比对(`shop_gp_drop_row`/`shop_level_gp_bonus`),断言强度不变且不再写死数值。 +3. **单测补行类参数**:`skill_unlock_test.gd` 的 Boss 用例原先直呼 `spawn(2,9)` 取默认 battle 行类,会落错带;改为显式传 `"boss"`,普通行零扰动用例保持 `"battle"`。 +4. **静态扫描两处归零**:新 resolver 的行集容器改为显式类型(原为无类型 `Array`);表注释里的退役词改为白话。两处都不改游戏行为。 + +### 2.5 实机目检总清单(只登记,不阻塞合并) + +| # | 项目 | 观测点 | 状态 | +|---|---|---|---| +| 1 | 三关实机开战遭遇观感不回归 | 第一关地精/熊地精、第二关巨蜘蛛与重击手混编、三关 Boss 战的敌人数量与外观与改动前一致 | 待用户试玩 | + +### 2.6 遗留读数义务 + +- 数值零变更,本卡不发起模拟跑批(MC 档位规则)。 +- 合并后主区需 `--editor --quit` 重刷类名缓存,再跑一次全量代码闸复跑,读数写入合并提交信息。 + +## 三、卡③ 独立验收审计 + +审计报告:`reports/iter34-验收审计-2026-10-22.md`(由未参与实现的子代理执行,逐行结论见该文件)。 + +## 四、全量代码闸读数 + +`tools/run_all_gates.sh --code`,起跑 commit `8b18e057`(2026-10-22 04:42),回执 `game/tests/.gates/receipt.json`: + +| 闸 | 退出码 | 指纹 | 对基线 | +|---|---|---|---| +| 01 冒烟全量 | 0 | green | 一致 | +| 02 单元套件 | 0 | green | 一致(35 套件) | +| 03 探针 --click | 0 | green | 一致 | +| 04 探针 --full | 0 | green | 一致 | +| 05 渲染闸 | 0 | green | 一致(shot_probe 七屏+golden 逐像素) | +| 09 静态扫描 | 0 | green | 改善(基线红 → 现绿,本卡改动面归零) | +| 10 熵增闸 | 0 | green | 一致 | +| 11 回放确定性对拍 | NEED-APPROVAL | skip | 本卡数值零变更,按档位规则不发起(需用户批准) | + +`all_green: true`,`changed_vs_baseline: []`。改前基线(T0,主区 master `603eba31`):冒烟 1156 项检查 0 失败/单元 35 套件 0 失败/探针全绿。 + +> 环境注记:本轮曾出现一次阶段性假红(02/03/04/05 同时红)——前一次被中断的闸进程未死透,与新闸并行写了同一批 `game/tests/.gates/*.log`(日志内出现同一探针同时 PASS 与 FAIL 的交错字节),另有并行会话在同机跑另一 worktree 的闸。清场后端到端重跑得本次读数;单件直跑(`ui_struct_probe` 等)在清场前即为绿,佐证为环境并发而非行为回归。 + +## 五、卡④ 信息迷雾可见性批 + +另行施工,完成后在本文追加对应章节。 From 68009b4ea8c85ab15875a252dc935710d924d93b Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Thu, 22 Oct 2026 05:20:21 +0800 Subject: [PATCH 5/6] =?UTF-8?q?fix(=E8=BF=AD=E4=BB=A334=E5=8D=A1=E2=91=A2?= =?UTF-8?q?=E5=AE=A1=E8=AE=A1=E7=BC=BA=E6=BC=8F):=20=E8=A1=A5=20S15=20?= =?UTF-8?q?=E5=8F=99=E8=BF=B0=E5=AF=BB=E5=9D=80=E5=8D=95=E6=B5=8B=E3=80=94?= =?UTF-8?q?fv24=E3=80=95=EF=BC=8BV6=E2=91=A0=20=E6=95=B0=E6=8D=AE=E4=BE=A7?= =?UTF-8?q?=E6=92=A4=E9=94=80=E8=AF=81=E6=98=8E=EF=BC=9B=E6=8A=A5=E5=91=8A?= =?UTF-8?q?=E8=A1=A5=20V7=20=E5=8D=81=E4=B8=83=E8=A1=8C=E5=8B=BE=E9=94=80?= =?UTF-8?q?=E8=A1=A8=E4=B8=8E=20V9/fv26=20=E8=A1=8C=E3=80=81=E5=9B=9B?= =?UTF-8?q?=E5=A4=84=E6=8F=8F=E8=BF=B0=E6=94=B9=E5=86=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 审计第一轮缺漏 4 条处置:V7 补覆盖行+S1–S16 逐条勾销(现工作树 file:line);V9 补件核行; S15 boss_name 补真实入口单测(扫表取 Boss 带键,断言叙述名=boss 行集首行名;变异 "boss"→"battle" 实测红); V6① 撤销证明换数据侧注入(bugbear 带 2→1 时 fv21 仍绿=数据驱动正证,叠加写死坐标才红)。 描述不符 4 条改写:fv17 夹具措辞、fv19 撤销归属、变异第 7 项首红、断言计数(36 条+1 行注释)。 随批入库第一轮审计报告。 --- .../unit/presentation/battle_narrator_test.gd | 38 +++++++ reports/iter34-实现报告-2026-10-22.md | 54 ++++++++-- reports/iter34-验收审计-2026-10-22.md | 100 ++++++++++++++++++ 3 files changed, 185 insertions(+), 7 deletions(-) create mode 100644 reports/iter34-验收审计-2026-10-22.md diff --git a/game/tests/unit/presentation/battle_narrator_test.gd b/game/tests/unit/presentation/battle_narrator_test.gd index e604500f..ce7dd670 100644 --- a/game/tests/unit/presentation/battle_narrator_test.gd +++ b/game/tests/unit/presentation/battle_narrator_test.gd @@ -8,10 +8,20 @@ const Kit := preload("res://tests/unit/test_kit.gd") const NarratorScript := preload("res://presentation/battle_narrator.gd") +# 叙述寻址的 runner 替身:只提供 boss_name 实际读的两项(当前节点、关卡行) +class _BossRunner: + var level_row: Dictionary = {"level_index": 1} + var node: Dictionary = {} + + func current_node() -> Dictionary: + return node + + func _init() -> void: _test_numeric_lines_carry_values() _test_end_text_differs_by_result() _test_phase_text_nonempty() + _test_boss_name_uses_boss_rowset() quit(Kit.finish("battle_narrator_test")) @@ -42,3 +52,31 @@ func _test_phase_text_nonempty() -> void: Kit.check("相位叙述非空(实际「%s」)" % t, t != "") var boss: String = NarratorScript.phase_text(2, "", false, true, "灰矮人石君王") Kit.check("Boss 在场时叙述带入 Boss 名(实际「%s」)" % boss, boss.contains("灰矮人石君王")) + + +# 叙述寻址按行类取 Boss 行集(S 清单 S15 换代):Boss 节点名必须来自 (关卡, boss, 带) 行集, +# 不能落到同深度的战斗行集——期望名由表行集首行读出,不写死字面量。 +func _test_boss_name_uses_boss_rowset() -> void: + var db = load("res://core/config_db.gd").new() + var loaded: bool = db.load_tables("res://data/tables", false) + Kit.check_quiet(loaded, "叙述寻址用例:数值表加载成功") + if not loaded: + return + var boss_row := {} + for row in db.enemy.rows: + if int(row.level_index) == 1 and String(row.get("row_kind", "")) == "boss": + boss_row = row + break + Kit.check_quiet(not boss_row.is_empty(), "叙述寻址用例:第一关有 Boss 行") + if boss_row.is_empty(): + return + var boss_depth: int = int(boss_row.depth_band) + var boss_rows: Array = db.resolve_enemy_rowset(1, boss_depth, "boss") + var battle_rows: Array = db.resolve_enemy_rowset(1, boss_depth, "battle") + var boss_expect := String(boss_rows[0].get("display_name", "")) + var battle_name := String(battle_rows[0].get("display_name", "")) + var runner := _BossRunner.new() + runner.node = {"node_depth": boss_depth, "type": "boss"} + var got: String = NarratorScript.boss_name(runner, db) + Kit.check("Boss 名取自 Boss 行集首行(期望「%s」实际「%s」)〔fv24〕" % [boss_expect, got], got == boss_expect) + Kit.check("Boss 名与同深度战斗行集名不同(期望「%s」≠「%s」——行类路由可判别)〔fv24〕" % [boss_expect, battle_name], boss_expect != battle_name) diff --git a/reports/iter34-实现报告-2026-10-22.md b/reports/iter34-实现报告-2026-10-22.md index 1bb1008f..0e6f2654 100644 --- a/reports/iter34-实现报告-2026-10-22.md +++ b/reports/iter34-实现报告-2026-10-22.md @@ -46,16 +46,41 @@ | fv14 | 值列零变更 | git diff 件核 | 件核 | `git diff` 逐值列比对 | 改任一值列 → 件核红 | | fv15 | 同关双终点共带报错 | `config_db_cross_validator_test.gd:137` | 单元 | `cross_validate()` 双 Boss 同带夹具 | 变异删判据 → 红(实测) | | fv16 | 顶带消解报错 | `config_db_cross_validator_test.gd:142` | 单元 | 节点深度高于顶带夹具 | 变异判据短路 `if false` → 红(实测) | -| fv17 | 单终点图零误报 | `config_db_cross_validator_test.gd:132` | 单元 | 现役三关夹具 | 回退 ④ 敌侧换代 → 红 | +| fv17 | 单终点图零误报 | `config_db_cross_validator_test.gd:132` | 单元 | level 1 合成单关夹具(`display_name:"夹具关"`);现役三关零误报由 fv6 兜住 | 回退 ④ 敌侧换代 → 红 | | fv18 | 混编行序 | `smoke_all.gd:4815` | 集成 | 冒烟读 (2,battle,5) 行集 | 变异对调两行 → 红(实测) | -| fv19 | 混编骰和 ≤ 4 | `smoke_all.gd:4820` | 集成 | 同行集静态求和 | 放宽判据 → 红 | +| fv19 | 混编骰和 ≤ 4 | `smoke_all.gd:4820` | 集成 | 同行集静态求和 | 改运行期守卫 `ROWSET_INSTANCE_MAX` → 同段守卫断言(「两行各定值 3(总 6>4)spawn 被拒」)红;静态求和本身放宽不会红 | | fv21 | 首遇带数据驱动定位 | `smoke_all.gd:2282` | 集成 | 扫表取最低带 | 撤销扫表定位 → 取键失配 | | fv22 | 巨蜘蛛首遇带固定 2 不加骰 | `smoke_all.gd:4801` | 集成 | 扫表定位+行集读数 | 变异加 `unit_count_max: 3` → 红(实测) | | fv23 | bugbear/duergar 首遇带固定 1 | `smoke_all.gd:3473`、:4859 | 集成 | 扫表定位+行集读数 | 变异加 `unit_count_max` → 红 | -| fv24 | Boss 行集与毒吐投放面 | `smoke_all.gd:4281`、:4291、:4807、:4865 | 集成 | 扫表定位+Boss 行集 | 回退投放面判据 → 红 | +| fv24 | V7 S 清单 17 行逐条换代(尾缀=清单件核;随带定点断言 = Boss 行集与毒吐投放面 + S15 叙述寻址) | `smoke_all.gd:4281`、:4291、:4807、:4865;`battle_narrator_test.gd:81`、:82 | 集成+单元 | 扫表定位+Boss 行集;`boss_name(runner, db)` 经 `resolve_enemy_rowset(..., "boss")` | 变异 `boss_name` 行类 "boss"→"battle" → `battle_narrator_test` fv24 红(实测);逐条勾销见 §2.2.1 | | fv25 | hobgoblin 缺口登记随行 | `smoke_all.gd:2116` | 接口 | 表文件注释在位 | 同 fv12 | +| fv26 | V9 反哺点五落点(非行为改动) | `reports/iter34-反哺点-2026-10-22.md` ①–⑤ | 件核 | 反哺点文件与卡片 §6 同步面表逐条比对 | 删任一落点 → 件核红(文档产出物,无代码断言) | -覆盖说明:`game/` 全域共 35 条断言带 fv 尾缀,覆盖 fv1–fv25(fv14 按卡片约定走 git diff 件核,代码侧无断言);fv9 与 fv20、fv12 与 fv25 各自共用同一断言行,计数时不重复。 +覆盖说明:`game/` 全域共 37 行带 fv 尾缀(36 条断言,另 `smoke_all.gd:969` 一行是注释),覆盖 fv1–fv26;fv14(值列零变更)与 fv26(反哺点落点)按卡片约定走件核,代码侧无断言;fv9 与 fv20、fv12 与 fv25 各自共用同一断言行,计数时不重复。 + +#### 2.2.1 V7:S 清单 17 行逐条勾销(卡片位置 → 现工作树位置) + +| S | 现工作树位置 | 换代内容 | 勾销凭据 | +|---|---|---|---| +| S1 | `smoke_all.gd:3168`/:3491/:4499 | 三参化(row_kind="battle")+带语义注释 | 三处均显式传 "battle",行集首行读数随带键 | +| S2 | `smoke_all.gd:4795`(断言 :4801〔fv22〕) | 首遇带数据驱动定位+恰一行/unit_count=2/无 unit_count_max | 〔fv22〕实测绿;变异加 unit_count_max → 红(§2.3 第 5 项) | +| S3 | `smoke_all.gd:4802`(断言 :4807〔fv24〕) | L2 Boss 行集改 `_boss_depth(2)`+"boss" | 〔fv24〕实测绿 | +| S4 | `smoke_all.gd:4810`(断言 :4815〔fv18〕、:4820〔fv19〕) | 键改 (2,battle,5),行序契约保持 | 〔fv18〕变异对调 → 红(§2.3 第 6 项) | +| S5 | `smoke_all.gd:4852`/:4860(断言 :4859〔fv23〕、:4865〔fv24〕) | 首遇带定位+Boss 三参化 | 两条断言实测绿 | +| S6 | `smoke_all.gd:974-984`/:4781-4789/:5256-5258 | en_keys 改 (L,row_kind,depth_band),Boss 节点查 boss 键域 | 三处键串 `"%d_%s_%d"`;L2/L3 跨表行完整断言绿 | +| S7 | `smoke_all.gd:2263`/:2279/:2301 | Boss AC 台阶保留;(1,2) 锚定改 bugbear 首遇带定位〔fv21〕;曲线改按 (1,battle) 带键相邻比 ≤ 2.5 | 三条断言实测绿;fv21 变异见 §2.3 第 8 项 | +| S8 | `smoke_all.gd:4262-4306` | 毒吐投放面改 `depth_band > 巨蜘蛛首遇带`+池键含 row_kind | 〔fv24〕:4281/:4291 实测绿;键串 `2_battle_5_giant_spider` 等 | +| S9 | `smoke_all.gd:3460-3474`(断言 :3473〔fv23〕) | (1,2) 改 bugbear 首遇带定位(固定 1 不加骰) | 〔fv23〕实测绿;数据侧变异下 fv23 红(§2.3 第 8 项) | +| S10a | `smoke_all.gd:2024`/:2674/:2699/:3078/:3143/:3230/:3284/:3324/:3354/:3566/:3633/:3651/:3668/:4139/:4359/:4606/:4970/:5040;`damage_probe.gd:95` | 各补 `"node_type": "boss"` | 上述直呼点均带 node_type;冒烟 1161 项 0 失败 | +| S10b | `smoke_all.gd:2674`/:2699 | 改 (2,9)+boss | 两处均为 `"node_depth": 9, "node_type": "boss"` | +| S11 | `tests/unit/core/config_db_resolver_test.gd`(fv1–fv5) | 换代为带寻址语义断言 | fv1–fv5 实测绿;floor 变异红(§2.3 第 1 项) | +| S12 | `tests/unit/data_config/config_db_test.gd`(fv6–fv10) | 夹具键改 row_kind/depth_band,签名三参化 | fv6–fv10 实测绿;枚举判据变异红(§2.3 第 2 项) | +| S13 | `tests/unit/core/config_db_cross_validator_test.gd`(fv15–fv17) | 夹具键改名+补两条负向用例 | fv15/fv16 变异红(§2.3 第 3、4 项) | +| S14 | `smoke/skill_rhythm_probe.gd:17`;`smoke/damage_probe.gd:269`/:422 | 三参化 | 三处显式传 row_kind | +| S15 | `game/presentation/battle_narrator.gd:76`;验收 `tests/unit/presentation/battle_narrator_test.gd:81`/:82〔fv24〕 | 三参化(row_kind="boss")+补真实入口单测 | 实测绿;变异 "boss"→"battle" → fv24 红(§2.3 第 9 项) | +| S16 | `smoke_all.gd:3491`(断言 :3496) | erow2 首行读数随带键(三参化) | 全深度×12 种子 60 场断言实测绿 | + +勾销口径:S11–S13 归单测面,S1–S10、S14–S16 归冒烟/探针面,S15 归消费面;17 行全部给出工作树位置与凭据,无一行停在旧坐标语义。 ### 2.3 变异验证记录表(注入 → 最窄闸见红 → 还原) @@ -67,9 +92,11 @@ | 4 | 顶带消解判据短路为 `if false` | `config_db_cross_validator_test.gd` | Boss 节点越顶带报 level_node_dangling〔fv16〕 | 恢复判据,复跑全绿 | | 5 | 巨蜘蛛首遇行(2,battle,4)加 `unit_count_max: 3` | 冒烟全量 | 巨蜘蛛首遇带行集 = {giant_spider} 恰一行且 unit_count=2 无 unit_count_max〔fv22〕 | 删该字段,冒烟复跑全绿 | | 6 | 混编两行(2,battle,5)文件内对调 | 冒烟全量 | 混编行序 [bugbear→giant_spider]〔fv18〕;20 行键序〔fv11〕;行值复制面 | 还原行序,冒烟复跑全绿 | -| 7 | hobgoblin 迁错带键(迁到 (1,battle,2)) | 冒烟全量 | 首次注入直接被加载期同带相邻规则拦下(表校验先红,键位断言未及运行);二次注入改为与 band 2 行互换带键(表可加载),命中「hobgoblin 行在 (1,battle,3)」〔fv12〕〔fv25〕与 20 行键序〔fv11〕 | 还原带键,冒烟复跑全绿 | +| 7 | hobgoblin 迁错带键(迁到 (1,battle,2)) | 冒烟全量 | 首次注入在加载期即被跨表校验拦下:`ERROR: [ConfigDB/level_node_dangling] level 1 battle 节点深度 3 高于 (level 1, battle) 顶带键 2——顶 clamp 消解`,随后〔fv6〕红(config_db_test 2/2 项止步);「同带相邻」判据未触发——该行紧接 bugbear 带 2 行之后、文件内相邻。二次注入改为与 band 2 行互换带键(表可加载),命中「hobgoblin 行在 (1,battle,3)」〔fv12〕〔fv25〕与 20 行键序〔fv11〕 | 还原带键,冒烟复跑全绿 | +| 8 | 数据侧:L1 bugbear 行 `depth_band` 2→1(V6① 撤销证明换代) | 冒烟全量 | 单跑数据变异时〔fv21〕仍绿(首遇带随之变 1、锚定行随迁——数据驱动的正证),连带〔fv11〕〔fv23〕与 spawn 扫描等共 6 项红;叠加「`_first_encounter_band` 写死返回 2」后〔fv21〕红(期望带 2 找不到 bugbear 行),共 9 项红 | 两处均 `git checkout --` 还原,冒烟 1161 项 0 失败 | +| 9 | `battle_narrator.boss_name` 行类 "boss"→"battle"(S15 撤销证明) | `battle_narrator_test.gd` | 「Boss 名取自 Boss 行集首行(期望「食人魔酋长」实际「大地精」)〔fv24〕」红,退出码 1 | 改回 "boss",13 项检查全绿 | -拦截边界如实记录:第 7 项按卡片写法直接把 hobgoblin 放到带 2 时,报表在加载期校验即被拦下(同带键行被隔断),键位断言的敏感度无法单独观测;改用「与带 2 行互换带键」的表内合法变异后键位断言变红,证明该断言能拦住「行在错带」这一类改动。另:第 1 项连带 fv3 变红,属同一算法单点的预期耦合,不另计。 +拦截边界如实记录:第 7 项按卡片写法直接把 hobgoblin 放到带 2 时,首条红是跨表 `level_node_dangling`(战斗节点深度 3 落到不存在的顶带),不是同带相邻规则——该行紧接 bugbear 带 2 行之后、文件内相邻,隔断判据不触发;键位断言的敏感度改用「与带 2 行互换带键」的表内合法变异单独观测。第 8 项单跑数据变异时〔fv21〕仍绿,说明该断言跟的是数据而不是写死值;只有「数据已变、坐标仍写死」的组合才让〔fv21〕红,这是它能区分两种实现的边界。另:第 1 项连带 fv3 变红,属同一算法单点的预期耦合,不另计。 ### 2.4 卡片与实际改动面的偏差(逐条说明) @@ -91,7 +118,20 @@ ## 三、卡③ 独立验收审计 -审计报告:`reports/iter34-验收审计-2026-10-22.md`(由未参与实现的子代理执行,逐行结论见该文件)。 +审计报告:`reports/iter34-验收审计-2026-10-22.md`(由未参与实现的子代理执行;只读卡片、本报告、diff、测试与闸回执,自跑最窄命令复现,不采信「已全绿」)。 + +**第一轮结论:不通过**——缺漏 4 条+描述不符 4 条(逐行依据见该报告第一至四节)。处置如下: + +| 缺漏 | 处置 | 落点 | +|---|---|---| +| V7 无覆盖行、fv24 尾缀挂错需求点 | 补 V7 覆盖行+S 清单 17 行逐条勾销表 | 本报告 §2.2 fv24 行、§2.2.1 | +| V9/fv26 覆盖行缺失 | 补件核行(反哺点文件 ①–⑤) | 本报告 §2.2 fv26 行 | +| S15 `boss_name` 换代无断言 | 补真实入口单测(扫表取 Boss 带键 → 断言叙述名=boss 行集首行名),并做撤销证明 | `game/tests/unit/presentation/battle_narrator_test.gd:56-82`〔fv24〕;§2.3 第 9 项 | +| V6①/fv21 撤销证明按记录无法变红 | 改为数据侧变异:bugbear 带 2→1(fv21 仍绿=数据驱动正证)+叠加写死坐标(fv21 红) | §2.3 第 8 项 | + +描述不符 4 条已按审计建议改写:fv17 夹具措辞(level 1 合成单关,非现役三关,现役三关由 fv6 兜住)、fv19 撤销归属(运行期守卫断言,静态求和本身不红)、§2.3 第 7 项拦截首红(跨表 `level_node_dangling`)、断言计数(36 条断言+1 行注释)。 + +**第二轮复审结论**:待复审完成后在本节追加。 ## 四、全量代码闸读数 diff --git a/reports/iter34-验收审计-2026-10-22.md b/reports/iter34-验收审计-2026-10-22.md new file mode 100644 index 00000000..04f38ea5 --- /dev/null +++ b/reports/iter34-验收审计-2026-10-22.md @@ -0,0 +1,100 @@ +# 迭代 34 卡③ 独立验收审计(2026-10-22) + +被审对象:实现报告 `reports/iter34-实现报告-2026-10-22.md`、需求卡片 `reports/需求卡片-敌人表深度带换代批-2026-10-21.md`、工作树 `D:\code\g-plan2-iter34-enemy`(分支 `iter34-enemy-band`,HEAD `7bb41891`,基线 `603eba31`)。 + +审计方式:只读代码、测试、卡片与 diff。所有结论以审计人自己运行的输出为准,不采信报告「已全绿」的表述。审计期间为复现撤销证明注入的变异,均用单文件 `git checkout -- ` 还原并复跑确认,工作树结束时无改动。 + +## 审计结论 + +不通过:缺漏 4 条(V7 无覆盖行、fv24 尾缀挂错需求点;V9/fv26 覆盖行缺失;S15 `boss_name` 换代无断言;V6①/fv21 撤销证明按记录无法变红),另有描述不符 4 条;其余对照行通过。 + +## 一、对照表逐行核对 + +核对方法:按报告 §2.2 的 file:line 逐条读断言文本,grep 尾缀〔fv#〕确认可定位,再沿断言回溯到改动点入口(`resolve_enemy_rowset`/`EnemyTable.validate`/`cross_validate`/生产表读数),并在工作树实跑对应套件确认该断言在跑。 + +| fv | 需求点 | 测试存在(file:line) | 尾缀可 grep | 真实入口 | 结论 | +|---|---|---|---|---|---| +| fv1 | V1① 带命中偏难 | `config_db_resolver_test.gd:58`、`:61` | 是 | `resolve_enemy_rowset(1,4/5,"battle")` → `band_of` | 通过 | +| fv2 | V1② 行序保序 | `config_db_resolver_test.gd:67`、`:71` | 是 | 同上,断言行集顺序 | 通过 | +| fv3 | V1③ 顶带 clamp+告警去重 | `config_db_resolver_test.gd:78`、`:84`、`:87` | 是 | 同上,D=99 与重复请求 | 通过 | +| fv4 | V1④ row_kind 路由 | `config_db_resolver_test.gd:93` | 是 | 同 D 传 battle/boss | 通过 | +| fv5 | V1⑤ 越域与非法行类 | `config_db_resolver_test.gd:101`、`:104`、`:107` | 是 | `resolve_enemy_rowset(0,…)`/`(1,-1,…)`/`(1,0,"elite")` | 通过 | +| fv6 | V2① 生产表校验零错误 | `config_db_test.gd:14` | 是 | `ConfigDB.load_tables` → `EnemyTable.validate` | 通过 | +| fv7 | V2② 非法 row_kind 被拒 | `config_db_test.gd:256` | 是 | `validate()` 非法枚举夹具 | 通过 | +| fv8 | V2③ dup_key 新键拦截 | `config_db_test.gd:235` | 是 | `validate()` 同带同敌种二行夹具 | 通过 | +| fv9/fv20 | V2④、V5③ 同带隔断 | `config_db_test.gd:225`(一条断言两个尾缀) | 是 | `validate()` 隔断夹具 | 通过 | +| fv10 | V2⑤ 锚行与 Boss 行集 | `config_db_test.gd:265`、`:273` | 是 | `validate()` 缺锚/缺 Boss 夹具 | 通过 | +| fv11 | V3① 20 行计数与键序 | `smoke_all.gd:2106` | 是 | 加载 `enemy.tres` 全表逐行比对 | 通过 | +| fv12/fv25 | V3②、V8 hobgoblin 留任 | `smoke_all.gd:2114`、`:2116` | 是 | 同 fv11 读表面+表注释 | 通过 | +| fv13 | V3③ 三 Boss 行就位 | `smoke_all.gd:2132` | 是 | 同 fv11 读表面 | 通过 | +| fv14 | V3④ 值列零变更 | 无断言(件核):`git diff 603eba31..7bb41891 -- game/data/tables/enemy.tres` | 无(卡片约定件核) | diff 去键列后逐值列比对 | 通过(去键列后值列逐字节一致) | +| fv15 | V4① 同带双 boss 报错 | `config_db_cross_validator_test.gd:137` | 是 | `cross_validate()` 双 Boss 共带夹具 | 通过 | +| fv16 | V4② 顶 clamp 消解报错 | `config_db_cross_validator_test.gd:142` | 是 | `cross_validate()` Boss 深度 10>顶带 9 | 通过 | +| fv17 | V4③ 单终点零误报 | `config_db_cross_validator_test.gd:132` | 是 | `cross_validate()` 合成单关夹具 | 例外:夹具只有 level 1(`display_name:"夹具关"`),报告写「现役三关夹具」不实;现役三关零误报由 fv6 兜住 | +| fv18 | V5① 混编行序 | `smoke_all.gd:4815` | 是 | 读 (2,battle,5) 行集顺序 | 通过 | +| fv19 | V5② 骰和 ≤4 | `smoke_all.gd:4820` | 是 | 读同行集静态求和 | 例外:断言是静态求和,放宽它本身不会红;能变红的是同段未编号的运行期守卫断言 | +| fv21 | V6① 首遇带数据驱动 | `smoke_all.gd:2282`(定位助手 `:3516`) | 是 | `_first_encounter_band` 扫表+行集读数 | 通过(撤销证明不成立,见第四节第 4 条) | +| fv22 | V6② 巨蜘蛛首遇带固定 2 | `smoke_all.gd:4801` | 是 | 扫表定位+行集读数 | 通过 | +| fv23 | V6③ L1/L3 首遇带固定 1 | `smoke_all.gd:3473`、`:4859` | 是 | 同上 | 通过 | +| fv24 | 报告写作「Boss 行集与毒吐投放面」 | `smoke_all.gd:4281`、`:4291`、`:4807`、`:4865` | 是 | 扫表定位+Boss 行集 | 缺漏:卡片 V7 要求「S 清单 17 行逐条换代+清单件核」,这 4 条断言不是该需求点的验证;报告无 V7 行,也无 S1–S16 逐条勾销表 | +| fv25 | V8 缺口登记随行 | `smoke_all.gd:2116`(与 fv12 共用行) | 是 | 表文件注释在位 | 通过 | +| —(V9/fv26) | V9 反哺点五落点件核 | 报告 §2.2 无此行 | — | 应为 `reports/iter34-反哺点-2026-10-22.md` 五条落点件核 | 缺漏:反哺点文件已有 ①–⑤ 五节,但对照表没有 fv26 覆盖行 | + +需求点覆盖对照:V1①–⑤=fv1–fv5,V2①–⑤=fv6–fv10,V3①–④=fv11–fv14,V4①–③=fv15–fv17,V5①–③=fv18–fv20,V6①–③=fv21–fv23,V8=fv25;V7 与 V9 没有对应覆盖行(见上表两处缺漏)。 + +## 二、撤销证明抽查 + +抽查 6 项(报告 §2.3 七条中取 5 条复现,另核 1 条不可复现)。每条流程:改一个文件注入 → 跑报告指明的最窄命令 → 确认对应断言变红 → `git checkout -- <单文件>` 还原 → 复跑回绿。 + +| 变异 | 复现结果 | 红输出首行 | 还原结论 | +|---|---|---|---| +| 1. `EnemyTable.band_of` 判据 `>=` 改 `<=`(反向 floor) | 已复现 | `[FAIL] 带命中取偏难一侧:请求深度 4 命中带 5(逐字不回带 0)〔fv1〕`;连带〔fv1〕第二条与〔fv3〕两条 | `git checkout -- game/data/schemas/enemy_table.gd`;resolver 套件 14 项全绿 | +| 2. `validate` 的 row_kind 枚举判据前置 `if false and` | 已复现 | `[FAIL] 非法 row_kind 被拒(封闭枚举 {battle, boss})〔fv7〕`(1/29 项失败) | 同文件 checkout;config_db_test 29 项全绿 | +| 3. 删除跨表「Boss 同带」判据 | 已复现 | `[FAIL] 同关双终点共带报 rowset_rule〔fv15〕` | `git checkout -- game/core/config_db_cross_validator.gd`;跨表套件 7 项全绿 | +| 4. 顶带消解判据短路 `if false` | 已复现 | `[FAIL] Boss 节点越顶带报 level_node_dangling〔fv16〕` | 同文件 checkout;7 项全绿 | +| 5. hobgoblin 与带 2 行互换带键(报告第 7 项第二注入法) | 已复现 | 冒烟首红〔fv11〕「敌人表 20 行且键序=迁移表序…」,随后〔fv12〕〔fv25〕红 | `git checkout -- game/data/tables/enemy.tres`;冒烟 1161 项检查 0 失败 | +| 6. hobgoblin 直接改到带 2(报告第 7 项第一注入法) | 已复现,但拦截者与报告所写不同 | `ERROR: [ConfigDB/level_node_dangling] level 1 battle 节点深度 3 高于 (level 1, battle) 顶带键 2——顶 clamp 消解`;随后〔fv6〕红,套件 2/2 项失败即止 | 同文件 checkout;config_db_test 29 项全绿 | +| 7. V6①「撤销扫表定位、写回写死坐标」 | 未复现(按记录设计不成立) | — | 未注入:助手 `_first_encounter_band`(`smoke_all.gd:3516`)返回值与旧写死值同为 L1=2/L2=4/L3=0,写回后断言值不变、不会红;要让 fv21 变红只能改数据把首遇行移带 | + +实际运行命令(均在工作树,`PYTHONIOENCODING=utf-8`,Godot 4.7.2 console 版 headless,一次只跑一个进程): + +- `--script res://tests/unit/core/config_db_resolver_test.gd`(多轮,含注入与还原后复跑) +- `--script res://tests/unit/data_config/config_db_test.gd`(多轮) +- `--script res://tests/unit/core/config_db_cross_validator_test.gd`(多轮) +- `--script res://tests/run_all.gd`(全量单元 35 套件) +- `--script res://smoke/smoke_all.gd`(1161 项检查 0 失败,多轮) +- `git diff`、`git status`、`git checkout -- <单文件>` + +补充核验:把 `battle_narrator.gd:76` 的 `"boss"` 改为 `"battle"` 后,35 个单元套件与整轮冒烟仍全绿,说明该改动点没有被任何断言拦住(见第四节缺漏 3)。 + +## 三、测试削弱检查 + +检查范围:`git diff 603eba31..7bb41891 -- game/tests game/smoke`(删 173 行、增 418 行)。逐项核对删除、阈值、扫描面与入口层级: + +1. `config_db_resolver_test.gd` 删除 `_test_ceil_progress_key`(board_gen ceil 3 条断言):属卡片 §8.2 旧断言预声明 S11「换代为 V1 带寻址语义断言」;board_gen 的 ceil/越顶/去重仍由 `config_db_test.gd:30-82` 覆盖,未失效。 +2. gp 掉落断言由硬编码 `[2,4]` 改为读 `shop.tres` Boss 档区间+关级加值:报告 §2.4 第 2 条已申报;判据仍比对结算输出,强度不低于原式。 +3. `skill_unlock_test.gd` Boss 用例 `spawn(2,9)` 补显式 `"boss"`:报告 §2.4 第 3 条已申报。 +4. 冒烟多处补 `node_type:"boss"`/补行类参数:报告 §2.4 第 1 条已申报。 +5. 未发现「真实入口换低层直调」:fv1–fv5 仍走 `resolve_enemy_rowset`,fv6–fv10 走 `EnemyTable.validate`(经加载),fv15–fv17 走 `cross_validate`,冒烟断言走 `_db.enemy.rows` 与 `spawn`。 +6. 检查数量:冒烟 1156→1161,单元 35 套件不变;无净收缩。其余删除行均为按 S 清单把旧坐标断言改写为带键断言,语义保持。 +7. 未发现未申报的阈值放宽或扫描面缩小。 + +结论:未发现未申报的测试削弱。另有一处计数描述不符:报告称「35 条断言带 fv 尾缀」,实测带尾缀行 35 行,其中 `smoke_all.gd:969` 是注释,断言为 34 条。 + +## 四、缺漏与例外 + +缺漏: + +1. **V7 无覆盖行、fv24 尾缀挂错需求点**。位置:报告 §2.2 表(无 V7 行);卡片 §8.2 V7 要求「S 清单 17 行逐条换代+实现报告逐条勾销」。事实:fv24 的 4 条断言(`smoke_all.gd:4281`/`:4291`/`:4807`/`:4865`)验证的是 Boss 行集与毒吐投放面,不是 S 清单完整性;报告也没有 S1–S16 逐条勾销表。建议:补 V7 覆盖行与逐条勾销表,或经卡片所有者同意把 V7 的验收方式改写为实际断言。 +2. **V9/fv26 覆盖行缺失**。位置:报告 §2.2 表;卡片 §8.2 V9。事实:`reports/iter34-反哺点-2026-10-22.md` 已有 ①–⑤ 五节落点,实物在,但对照表没有 fv26 件核行,无法按表逐行核对。建议:补一行「V9|五条落点各一|件核:反哺点文件 ①–⑤|非行为改动|通过」。 +3. **S15 `boss_name` 换代无断言**。位置:`game/presentation/battle_narrator.gd:76`;卡片 §8.1 S15、§8.2 V7。事实:把该处 `"boss"` 改成 `"battle"` 后,35 个单元套件与整轮冒烟 1161 项全绿;`battle_narrator_test.gd` 从未调用 `boss_name`。建议:补一条走真实入口的单测(构造 Boss 节点,断言叙述名取 boss 行集名),或在报告 §2.4 如实声明该处无断言并说明理由。 +4. **V6①/fv21 撤销证明不成立**。位置:卡片 §8.2 V6① 撤销证明、报告 fv21 行。事实:按记录「撤销扫表定位、写回写死坐标」还原后,助手返回值与写死值相同(L1=2/L2=4/L3=0),断言不会红;该撤销证明无法区分「数据驱动」与「写死」。建议:改为数据侧变异(把首遇行移到别的带,断言应取键失配变红)或如实标注撤销边界。 + +例外(描述不符,不影响其余覆盖): + +5. **fv17 夹具描述**。位置:`config_db_cross_validator_test.gd:128-132`;报告 §2.2 fv17 行写「现役三关夹具」。事实:夹具是 level 1 合成单关(`display_name:"夹具关"`);现役三关零误报由 fv6(生产表加载零 error)覆盖。建议:改报告措辞,或补现役三关夹具。 +6. **fv19 撤销归属**。位置:`smoke_all.gd:4820`。事实:静态求和断言被放宽不会红;真正能红的是同段未编号的运行期守卫断言(「两行各定值 3(总 6>4)spawn 被拒」)。建议:报告撤销证明改写为「改 `ROWSET_INSTANCE_MAX` 守卫 → 守卫断言红」。 +7. **§2.3 第 7 项拦截边界描述有误**。位置:报告 §2.3 注①。事实:hobgoblin 直接改到带 2 时,首条红是跨表 `level_node_dangling`(战斗节点深度 3 无带可落),不是「同带相邻规则」;同带相邻未触发,是因为 hobgoblin 紧接在 bugbear 带 2 行之后、文件内相邻。建议:按实际首红改写。 +8. **断言计数**。位置:报告 §2.2 覆盖说明「共 35 条断言带 fv 尾缀」。事实:带尾缀行 35 行,其中 1 行为注释(`smoke_all.gd:969`),断言 34 条。建议:改为 34 条。 + +审计结束时工作树 `git status --short` 为空,HEAD 仍为 `7bb41891`;除本报告外未新增或修改任何文件。 From 9ff0909a95f195a7b8239aed93a079b148984f8c Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Thu, 22 Oct 2026 05:30:04 +0800 Subject: [PATCH 6/6] =?UTF-8?q?docs(=E8=BF=AD=E4=BB=A334=E5=8D=A1=E2=91=A2?= =?UTF-8?q?):=20=E7=AC=AC=E4=BA=8C=E8=BD=AE=E7=8B=AC=E7=AB=8B=E5=A4=8D?= =?UTF-8?q?=E5=AE=A1=E9=80=9A=E8=BF=87=EF=BC=88A=E2=80=93F=20=E5=85=AD?= =?UTF-8?q?=E9=A1=B9=E5=85=A8=E9=80=9A=E8=BF=87=EF=BC=89=E2=80=94=E2=80=94?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E6=8A=A5=E5=91=8A=20=C2=A73=20=E5=86=99?= =?UTF-8?q?=E5=85=A5=E5=A4=8D=E5=AE=A1=E7=BB=93=E8=AE=BA=EF=BC=8C=E5=A4=8D?= =?UTF-8?q?=E5=AE=A1=E6=8A=A5=E5=91=8A=E9=9A=8F=E6=89=B9=E5=85=A5=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 复审被审 HEAD=68009b4e:第一轮 4 缺漏与 4 描述不符全部闭合,修补未引入未申报削弱。 --- reports/iter34-实现报告-2026-10-22.md | 2 +- reports/iter34-验收审计-复审-2026-10-22.md | 98 ++++++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 reports/iter34-验收审计-复审-2026-10-22.md diff --git a/reports/iter34-实现报告-2026-10-22.md b/reports/iter34-实现报告-2026-10-22.md index 0e6f2654..ee548d10 100644 --- a/reports/iter34-实现报告-2026-10-22.md +++ b/reports/iter34-实现报告-2026-10-22.md @@ -131,7 +131,7 @@ 描述不符 4 条已按审计建议改写:fv17 夹具措辞(level 1 合成单关,非现役三关,现役三关由 fv6 兜住)、fv19 撤销归属(运行期守卫断言,静态求和本身不红)、§2.3 第 7 项拦截首红(跨表 `level_node_dangling`)、断言计数(36 条断言+1 行注释)。 -**第二轮复审结论**:待复审完成后在本节追加。 +**第二轮复审结论:通过**(`reports/iter34-验收审计-复审-2026-10-22.md`,被审 HEAD `68009b4e`)——A–F 六项全通过:V7 覆盖行与 17 行勾销表齐备(抽 6 行实读属实)、fv26 件核行在位、S15 单测走真实入口且变异红、V6① 数据侧撤销证明复现一致、四条描述不符已按实改写、修补未引入未申报的测试削弱。卡③ 合并前置满足。 ## 四、全量代码闸读数 diff --git a/reports/iter34-验收审计-复审-2026-10-22.md b/reports/iter34-验收审计-复审-2026-10-22.md new file mode 100644 index 00000000..c7ef9716 --- /dev/null +++ b/reports/iter34-验收审计-复审-2026-10-22.md @@ -0,0 +1,98 @@ +# 迭代 34 卡③ 独立验收审计·第二轮复审(2026-10-22) + +- 被审 HEAD:`68009b4e`(`iter34-enemy-band`,基线 `603eba31`);第一轮被审 HEAD `7bb41891` +- 被审对象:实现报告 `reports/iter34-实现报告-2026-10-22.md`(§2.2/§2.2.1/§2.3/§3)、第一轮审计报告 `reports/iter34-验收审计-2026-10-22.md`、修补 diff `7bb41891..68009b4e`、卡片 `reports/需求卡片-敌人表深度带换代批-2026-10-21.md` +- 审计方式:只读代码、测试、卡片与 diff;所有结论以审计人自己运行的命令输出为准,不采信报告「实测/已全绿」表述。为复现撤销证明注入的变异一律单文件 `git checkout --` 还原并复跑确认;不跑全量闸(`tools/run_all_gates.sh` 留给实现会话在最终提交上跑)。审计结束时工作树 `git status --short` 为空(本报告除外)。 + +## 结论速览 + +**A–F 六项全部通过;总结论:通过。** 第一轮 4 条缺漏与 4 条描述不符全部真闭合,本次修补未引入测试削弱,无阻塞遗留例外。 + +--- + +## A. 缺漏 1(V7 覆盖行+17 行勾销表)——通过 + +依据: + +1. 报告 §2.2 已新增 fv24 覆盖行,需求点改写为「V7 S 清单 17 行逐条换代」,撤销列指向「逐条勾销见 §2.2.1」。 +2. 报告 §2.2.1 存在 17 行勾销表:S1–S9、**S10a/S10b 分列**、S11–S16,逐行给出「现工作树位置/换代内容/勾销凭据」,S11–S13 归单测面、S15 归消费面,口径与卡片 §8.1 一致。 +3. 按任务要求抽查 6 行(逐行实读代码确认位置与换代内容属实): + +| 抽查 | 报告所记位置 | 实读确认 | +|---|---|---| +| S1 | `smoke_all.gd:3168`/:3491/:4499 | 三处均为 `resolve_enemy_rowset(1, …, "battle")[0]` 三参化,且带「行集首行(…battle 键域)」注释,属实 | +| S6 | `smoke_all.gd:974-984`/:4781-4789/:5256-5258 | 三处 `en_keys["%d_%s_%d" % [level_index, row_kind, depth_band]]`/`"%d_boss_9"` 查 boss 键域,属实 | +| S8 | `smoke_all.gd:4262-4306` | `_first_encounter_band(2,"giant_spider")` 取首遇带、判据改 `depth_band > spider_lead_band`、`expect_pools` 键串含 row_kind(如 `2_battle_5_giant_spider`),属实 | +| S10a | `smoke_all.gd:2024`/:2674/:2699…;`damage_probe.gd:95` | 抽 3 行均为 `"node_type": "boss"` 直呼;另用脚本核了表中全部 18 个 `smoke_all.gd` 行号,每个都含 `node_type": "boss"`,属实 | +| S14 | `skill_rhythm_probe.gd:17`;`damage_probe.gd:269`/:422 | 三处均显式传 row_kind(`"battle"`/`t`/`kind`),属实 | +| S16 | `smoke_all.gd:3491`(断言 :3496) | `erow2 = _db.resolve_enemy_rowset(1, int(depth_v), "battle")[0]`,下行 `scan_bad == 0` 断言,属实 | + +另核 S2/S3/S4/S5/S7/S9 的行号指向(4795/4802/4810/4852/4860、2263/2279/2301、3460-3474),均与断言实际位置对得上,未发现空指或错指。 + +## B. 缺漏 2(V9/fv26 覆盖行)——通过 + +- 报告 §2.2 新增 fv26 行:「V9 反哺点五落点(非行为改动)|`reports/iter34-反哺点-2026-10-22.md` ①–⑤|件核」,与卡片 V9「§6 五条每条有落点」对应。 +- `reports/iter34-反哺点-2026-10-22.md` 实读确有 ①–⑤ 五节(① 敌人系统.md 记法与措辞、② 关卡节点地图.md :146、③ 关卡节点图生成.md 产出规格、④ 待办清单.md 状态回写、⑤ roadmap.md 迭代行),每节均写「文件与位置/改成/依据」。 + +## C. 缺漏 3(S15 `boss_name` 断言)——通过 + +- 用例位置:`game/tests/unit/presentation/battle_narrator_test.gd:59-82`,`_test_boss_name_uses_boss_rowset()` 由 `_init` 调用。 +- 真实入口成立:用例加载生产表后调 `NarratorScript.boss_name(runner, db)`(`battle_narrator.gd:72`),函数内走 `db.resolve_enemy_rowset(..., "boss")`(`:76`);生产调用点 `presentation/main.gd:1331` 同款。runner 替身只提供 `boss_name` 实际读取的 `level_row`/`current_node()` 两项,与 `core/level_runner.gd` 的同名面一致。 +- 期望值由数据读出:`boss_depth` 扫表取 L1 boss 行 `depth_band`,`boss_expect`/`battle_name` 分别由 boss/battle 行集首行 `display_name` 读出,无写死字面量;另有一条「boss 名 ≠ 同深度请求的战斗行集名」判别断言。 +- 自跑(命令与首行输出): + - `PYTHONIOENCODING=utf-8 "" --headless --path . --script res://tests/unit/presentation/battle_narrator_test.gd` + → `[battle_narrator_test] all green (13 checks)`,EXIT=0 + - 撤销注入:把 `battle_narrator.gd:76` 的 `"boss"` 改 `"battle"` 后重跑 + → `[FAIL] Boss 名取自 Boss 行集首行(期望「食人魔酋长」实际「大地精」)〔fv24〕`;`[battle_narrator_test] 1/13 项失败`,EXIT=1 + - `git checkout -- game/presentation/battle_narrator.gd` 后复跑 → `[battle_narrator_test] all green (13 checks)`,EXIT=0 +- 与报告 §2.3 第 9 项所记(红断言原文、13 项回绿)逐字相符。 + +## D. 缺漏 4(V6①/fv21 撤销证明)——通过(实测与报告一致) + +实测流程与结果: + +1. 数据侧变异:`game/data/tables/enemy.tres` 的 (level_index 1, row_kind battle, depth_band 2, bugbear) 行 `depth_band` 2→1,跑 `res://smoke/smoke_all.gd` + → `=== 冒烟完成:1161 项检查,6 项失败 ===`(EXIT=1)。〔fv21〕**不在**失败列表(首遇带随数据变为 1、锚定行随迁——数据驱动正证成立)。6 项红为:〔fv11〕20 行键序、〔fv23〕熊地精首遇带固定 1、全深度×12 种子 spawn 扫描、「二十行九种池模板」、「先手击杀→摇定技能作废」、「行集内每个战斗/Boss 节点…精确行集(缺失 1)」。报告所记「共 6 项红」属实。 +2. 叠加写死:在 `smoke/smoke_all.gd` 的 `_first_encounter_band` 首行插入 `return 2`,重跑冒烟 + → 首条红即 `[FAIL] 熊地精首遇带(1,battle,2)降档锚定(hp 22 · ac 2 · 池 attack6/charge2/heavy2 零改动——迭代 24 N1;首遇带数据驱动定位)〔fv21〕`;`=== 冒烟完成:1161 项检查,9 项失败 ===`(EXIT=1)。报告所记「共 9 项红」属实。 +3. `git checkout -- game/data/tables/enemy.tres game/smoke/smoke_all.gd` 还原,复跑 + → `=== 冒烟完成:1161 项检查,0 项失败 ===`(EXIT=0)。 + +结论:该撤销证明现在能真正区分「数据驱动定位」与「写死坐标」——单跑数据变异〔fv21〕绿、叠加写死才红,报告 §2.3 第 8 项的记述与实测逐项一致。 + +## E. 描述不符 4 条——通过(4/4 已按实改写) + +1. **fv17 夹具措辞**:`config_db_cross_validator_test.gd:95-116` 的 `_loaded()` 只构造 `level_index: 1`、`display_name: "夹具关"` 的合成单关夹具;fv17(:132)在该夹具上断言零误报。报告已改为「level 1 合成单关夹具;现役三关零误报由 fv6 兜住」,与实读一致。**通过**。 +2. **fv19 撤销归属**:`smoke_all.gd:4810-4820` 的静态求和(首遇混编数据 Σ=4,阈值 `<= 4`)放宽阈值不会红;真正能红的是同段运行期守卫(`:4891-4914` 注入 (1,battle,6) 两行各 3 只,`:4909` 断言 `not over_sys.spawn(...)`,由 `ROWSET_INSTANCE_MAX` 守卫兜住)。报告已改为「改运行期守卫 → 守卫断言红;静态求和本身放宽不会红」,归属正确。**通过**。 +3. **§2.3 第 7 项拦截首红**(可选复现已做):把 hobgoblin 行 `depth_band` 3→2 后跑 `res://tests/unit/data_config/config_db_test.gd`,首条错误为 + `ERROR: [ConfigDB/level_node_dangling] level 1 battle 节点深度 3 高于 (level 1, battle) 顶带键 2——顶 clamp 消解` + 随后 `[FAIL] 全部表加载 + 校验通过(含跨表护栏零 error 诊断)〔fv6〕`,EXIT=1;`git checkout --` 还原。与报告改写后的「跨表 level_node_dangling 首红、同带相邻未触发」一致。**通过**。 +4. **断言计数**:`grep -rn "〔fv[0-9]*〕" tests/ smoke/ --include=*.gd` 得 37 行;逐行核对,`smoke_all.gd:969` 为注释(`# 同带多行合法,带键聚合天然兼容〔fv24〕)`),其余 36 行均为断言行 → 「36 条断言+1 行注释」属实;fv14/fv26 按卡片约定走件核,代码侧无尾缀,覆盖说明自洽。**通过**。 + +## F. 测试削弱检查——通过 + +检查范围:`git diff 603eba31..68009b4e -- game/tests game/smoke`(8 个文件,+449/−166)与本次修补 `git diff 7bb41891..68009b4e`。 + +1. **本次修补未碰产品代码与既有断言**:`7bb41891..68009b4e` 仅 `battle_narrator_test.gd` 38 行新增(0 删除)+两份报告;新增用例为纯增量,无删断言、无阈值放宽、无扫描面缩小。 +2. **全量 diff 的删除行均属卡片 §8.2「旧断言预声明」允许的 S1–S16 改写**:删除面集中在 `config_db_resolver_test.gd`(S11 换代)、`config_db_test.gd`(S12 夹具键改名)、`config_db_cross_validator_test.gd`(S13)、`smoke_all.gd`(S1–S10)、两份探针(S14);`skill_unlock_test.gd` 的 `spawn(2,9)` 补显式 `"boss"`、冒烟 gp 掉落断言改读 `shop.tres` Boss 档区间+关级加值,均在报告 §2.4 第 1–3 条申报。 +3. **无阈值放宽 / 无扫描面缩小**:diff 中没有 seed/`range()` 扫描规模改动;第一关曲线阈值 `≤ 2.5`、混编 `≤ 4`、首遇 `unit_count=2`/固定 1 等判据原样保留;gp 掉落断言由硬编码 `[2,4]` 改为数据驱动 `Boss 档 [6,10] + 关级加值`(因 S10a 把该战改为 boss 语义,掉落档合法切换),判据仍比对结算输出,强度不降。 +4. **真实入口未换下层直调**:fv1–fv5 走 `resolve_enemy_rowset`,fv6–fv10 走加载后的 `EnemyTable.validate`,fv15–fv17 走 `cross_validate`,fv18–fv23 走冒烟表读取+`spawn`,S15 走 `boss_name` → `resolve_enemy_rowset(..., "boss")`。 +5. **数量面**:冒烟 1156→1161(本卡 +5),单元套件 35 个不变;`battle_narrator_test` 检查数 11→13(新增 2 条)。无净收缩。 + +结论:未发现未申报的测试削弱,本次修补为纯增量。 + +--- + +## 遗留例外/观察(不阻塞) + +1. 第一轮审计报告第三节所记 diff 行数「删 173 行、增 418 行」与实测 `git diff --numstat 603eba31..7bb41891 -- game/tests game/smoke`(+411/−166)不一致,属第一轮报告的计数笔误(不影响其结论,也不属实现报告内容);本轮结论以实测为准。 +2. 新单测在解析 L1 battle 行集时(同深度请求落在 Boss 深度 4)会触发一次 `resolve_depth_clamp` 警告——这是顶带 clamp 的预期语义,非失败;用例仍 13 项全绿。 + +## 运行命令清单(均为工作树 `game/` 下、Godot 4.7.2 console 版、`PYTHONIOENCODING=utf-8`、一次一个进程) + +- `--headless --path . --script res://tests/unit/presentation/battle_narrator_test.gd`(注入前后与还原后共 3 轮) +- `--headless --path . --script res://smoke/smoke_all.gd`(数据变异/叠加写死/还原后共 3 轮) +- `--headless --path . --script res://tests/unit/data_config/config_db_test.gd`(hobgoblin 错带复现 1 轮) +- `git diff 603eba31..68009b4e -- game/tests game/smoke`、`git diff --numstat`、`grep -rn "〔fv[0-9]*〕"`、`git checkout -- <单文件>`、`git status --short` + +审计结束时工作树除本报告外无改动,HEAD=`68009b4e`。