diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index b80fc3c4..208cf84d 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -153,7 +153,6 @@ var _rng func _run_core_smoke(db, rng) -> void: _db = db _rng = rng - _smoke_enemy_attack_type() _smoke_armor_roll() _smoke_pool_wiring() _smoke_type_glow() @@ -1399,67 +1398,6 @@ 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)]) -# ---- 敌方攻击类型轴(表列/单位快照/单据载荷/矩阵命中证词)---- -# 克制矩阵命中证词 = 同种子三甲型配对实测(不是查表存在性证词):同种子下 armor_roll 流消费逐位一致、 -# 阈值(hero armor_class)不变,三甲型唯一差异 = 克制点数 cdelta(normal_vs_light 0 / heavy −1 / ward +1) -# → heavy 伤害 == light −1、ward == light +1(要求基线伤害 ≥ 2——地板 1 会吞 −1 位移)。 -# 承伤位移红线(卡片 §8 L3):全局位移只允许 ±1 量级——非此量级 = 矩阵消费面有未盘点的第二落点(停机)。 -func _smoke_enemy_attack_type() -> void: - print("--- 敌方攻击类型轴(迭代 24 N3)---") - # ① 表列:20 行全填 normal(N3 全填不做差异化——Q9) - var at_ok := true - var at_detail := "" - for row in _db.enemy.rows: - var at := String(row.get("attack_type", "")) - if at != "normal": - at_ok = false - at_detail += " %s:%s" % [String(row.get("enemy_id", "?")), at] - check(at_ok, "敌方 %d 行 attack_type 全填 normal(迭代 24 N3 轴开列,越轨行%s)" % [ - _db.enemy.rows.size(), at_detail if at_detail != "" else "无"]) - # ②③④ 矩阵命中证词扫描:(3,0) 灰矮人 atk 区间 [2,4] 基线足够((1,2) 熊地精 atk 1 会塌地板); - # 同 seed 三甲型各开一场、各取首个敌方伤害行,对拍 ±1。 - var matrix_found := false - var snap_ok := true - var payload_at := "" - var t_light := -1 - var t_heavy := -1 - var t_ward := -1 - for seed_offset in range(40): - var totals := {} - for armor in ["light", "heavy", "ward"]: - var rng = load("res://core/rng_service.gd").new() - rng.set_master_seed(20261013 + seed_offset) - 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": 3, "node_depth": 0, "hero_hp": int(_db.hero.max_hp), "armor_type": armor}): - totals[armor] = -1 - continue - if armor == "light": - for u in settlement._enemy_system.all_enemies(): - if String(u.get("attack_type", "")) == "": - snap_ok = false - settlement.minesweeper.settle_batch([{"action": "stop"}]) - for row in rows: - if String(row.get("type", "")) == "damage" and String(row.get("source_class", "")) == "enemy_action": - totals[armor] = int(row.get("value", 0)) - if armor == "light": - payload_at = String(row.get("attack_type", "")) # 载荷摊平在行顶层(_emit_row 摊平 extra) - break - if not totals.has(armor): - totals[armor] = -1 - if int(totals.get("light", -1)) >= 2 and int(totals.get("heavy", -1)) >= 1 and int(totals.get("ward", -1)) >= 1: - t_light = int(totals["light"]) - t_heavy = int(totals["heavy"]) - t_ward = int(totals["ward"]) - matrix_found = (t_heavy == t_light - 1 and t_ward == t_light + 1) - break - check(snap_ok, "敌方单位快照 attack_type 非空(spawn 注入面——迭代 24 N3)") - check(matrix_found and t_light >= 2, - "克制矩阵命中证词:同种子三甲型实测 heavy %d == light %d − 1 · ward %d == light %d + 1(迭代 24 N3 全局平移 −1/0/+1)" % [ - t_heavy, t_light, t_ward, t_light]) - check(payload_at == "normal", "敌方伤害技单据载荷 attack_type 非空(= %s——第 9 参传值面,迭代 24 N3)" % payload_at) - # ---- 护甲等级掷点·行为面(折扣档分布 / nat20 暴击 / 伤害下限 / 棋盘来源必中)---- diff --git a/game/tests/run_all.gd b/game/tests/run_all.gd index dfab90ab..e8571156 100644 --- a/game/tests/run_all.gd +++ b/game/tests/run_all.gd @@ -59,6 +59,7 @@ func _suites() -> Array: {"path": "res://tests/unit/presentation/forge_workbench_test.gd", "expect": 0}, # 工作间两区六路线 UI 直验(V13 自动化前置面) {"path": "res://tests/unit/battle_settlement/damage_math_test.gd", "expect": 0}, {"path": "res://tests/unit/battle_settlement/damage_controller_test.gd", "expect": 0}, + {"path": "res://tests/unit/battle_settlement/roll_multiplier_test.gd", "expect": 0}, # 掷点与乘区宿主(敌攻击类型轴/护甲掷点行为面/乘区接线/伤害类型克制与标错/fresh 池攻击链——批 B 自 smoke_all 迁入) {"path": "res://tests/unit/battle_settlement/skill_unlock_test.gd", "expect": 0}, # Boss 半血解锁技·容器机制(门谓词/闩不可逆/双面过滤/保底不逼锁定/零扰动) {"path": "res://tests/unit/contracts/system_contracts_test.gd", "expect": 0}, {"path": "res://tests/unit/presentation/ui_font_asset_test.gd", "expect": 0}, diff --git a/game/tests/unit/battle_settlement/roll_multiplier_test.gd b/game/tests/unit/battle_settlement/roll_multiplier_test.gd new file mode 100644 index 00000000..3c284917 --- /dev/null +++ b/game/tests/unit/battle_settlement/roll_multiplier_test.gd @@ -0,0 +1,93 @@ +# tests/unit/battle_settlement/roll_multiplier_test.gd — 掷点与乘区单元测试宿主 +# 断言面 = 五套(批 B 断言迁移自 smoke_all.gd,G4 施工组;五套按源编排序分五笔原子提交迁入): +# 敌方攻击类型轴(表列全 normal / spawn 快照注入 / 单据载荷第 9 参 / 克制矩阵命中证词 ±1 位移) +# 护甲等级掷点·行为面(查档纯函数分布 / EV 锚 / nat20 暴击 / 削弱续时 / AC11 逐拍判定 / 伤害下限 / 棋盘来源必中) +# 主角乘区接线 + 硬化折半方向(×1.5 / ×0.75 / ×0.5——池乘区通道) +# 攻击类型 × 护甲克制 + 标错惩罚(护甲快照 / 克制矩阵表值 / 标错负向与标中正向兑付 / 同种子回放) +# 战斗结算 fresh 池攻击链回归(Bug#2——池空时按掷点档位足额兑现) +# 协议照单元侧惯例:extends SceneTree+Kit.check 主通道;--script 模式无 autoload, +# ConfigDB 显式实例化构造注入(各套自建 RngService——源套掷点隔离形态,宿主不持统一流); +# 表加载失败不中止(沿被迁断言面的源语义——诊断在此展开,表值变异的红由具体断言落,承 G2/G3 同款)。 +# 运行:godot --headless --path game --script res://tests/unit/battle_settlement/roll_multiplier_test.gd +extends SceneTree + +const Kit := preload("res://tests/unit/test_kit.gd") +const ConfigDBScript := preload("res://core/config_db.gd") + +var _db + + +func _init() -> void: + _db = ConfigDBScript.new() + # 表加载失败不中止(承 G2/G3 同款:表损坏的红落在具体断言上—— + # 诊断在此展开,后续断言按坏表实况逐条验红;collect_only=true 下成员照常赋值可评) + var load_ok: bool = _db.load_tables("res://data/tables", true) + if not load_ok: + push_error("[roll_multiplier_test] 表加载返回失败——诊断如下(红由具体断言落)") + for d in _db.diagnostics(): + if d.severity == "error" or d.severity == "fatal": + push_error(" [%s] %s: %s" % [d.severity, d.category, d.message]) + # 套执行序沿源编排序 + _smoke_enemy_attack_type() + quit(Kit.finish("roll_multiplier_test")) + + +# ---- 敌方攻击类型轴(表列/单位快照/单据载荷/矩阵命中证词)---- +# 克制矩阵命中证词 = 同种子三甲型配对实测(不是查表存在性证词):同种子下 armor_roll 流消费逐位一致、 +# 阈值(hero armor_class)不变,三甲型唯一差异 = 克制点数 cdelta(normal_vs_light 0 / heavy −1 / ward +1) +# → heavy 伤害 == light −1、ward == light +1(要求基线伤害 ≥ 2——地板 1 会吞 −1 位移)。 +# 承伤位移红线(卡片 §8 L3):全局位移只允许 ±1 量级——非此量级 = 矩阵消费面有未盘点的第二落点(停机)。 +func _smoke_enemy_attack_type() -> void: + print("--- 敌方攻击类型轴(迭代 24 N3)---") + # ① 表列:20 行全填 normal(N3 全填不做差异化——Q9) + var at_ok := true + var at_detail := "" + for row in _db.enemy.rows: + var at := String(row.get("attack_type", "")) + if at != "normal": + at_ok = false + at_detail += " %s:%s" % [String(row.get("enemy_id", "?")), at] + Kit.check("敌方 %d 行 attack_type 全填 normal(迭代 24 N3 轴开列,越轨行%s)" % [ + _db.enemy.rows.size(), at_detail if at_detail != "" else "无"], at_ok) + # ②③④ 矩阵命中证词扫描:(3,0) 灰矮人 atk 区间 [2,4] 基线足够((1,2) 熊地精 atk 1 会塌地板); + # 同 seed 三甲型各开一场、各取首个敌方伤害行,对拍 ±1。 + var matrix_found := false + var snap_ok := true + var payload_at := "" + var t_light := -1 + var t_heavy := -1 + var t_ward := -1 + for seed_offset in range(40): + var totals := {} + for armor in ["light", "heavy", "ward"]: + var rng = load("res://core/rng_service.gd").new() + rng.set_master_seed(20261013 + seed_offset) + 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": 3, "node_depth": 0, "hero_hp": int(_db.hero.max_hp), "armor_type": armor}): + totals[armor] = -1 + continue + if armor == "light": + for u in settlement._enemy_system.all_enemies(): + if String(u.get("attack_type", "")) == "": + snap_ok = false + settlement.minesweeper.settle_batch([{"action": "stop"}]) + for row in rows: + if String(row.get("type", "")) == "damage" and String(row.get("source_class", "")) == "enemy_action": + totals[armor] = int(row.get("value", 0)) + if armor == "light": + payload_at = String(row.get("attack_type", "")) # 载荷摊平在行顶层(_emit_row 摊平 extra) + break + if not totals.has(armor): + totals[armor] = -1 + if int(totals.get("light", -1)) >= 2 and int(totals.get("heavy", -1)) >= 1 and int(totals.get("ward", -1)) >= 1: + t_light = int(totals["light"]) + t_heavy = int(totals["heavy"]) + t_ward = int(totals["ward"]) + matrix_found = (t_heavy == t_light - 1 and t_ward == t_light + 1) + break + Kit.check("敌方单位快照 attack_type 非空(spawn 注入面——迭代 24 N3)", snap_ok) + Kit.check("克制矩阵命中证词:同种子三甲型实测 heavy %d == light %d − 1 · ward %d == light %d + 1(迭代 24 N3 全局平移 −1/0/+1)" % [ + t_heavy, t_light, t_ward, t_light], matrix_found and t_light >= 2) + Kit.check("敌方伤害技单据载荷 attack_type 非空(= %s——第 9 参传值面,迭代 24 N3)" % payload_at, payload_at == "normal") diff --git a/reports/批B-断言迁移账本-2026-10-22.md b/reports/批B-断言迁移账本-2026-10-22.md index cf43171b..4647c7f2 100644 --- a/reports/批B-断言迁移账本-2026-10-22.md +++ b/reports/批B-断言迁移账本-2026-10-22.md @@ -276,6 +276,13 @@ 施工发现(供后续组参考,接 G2 序号):⑦ 删套函数必须同批删 `_run_core_smoke` 调度体里的调用行——本组首轮删净四函数后闸 01 即 Parse Error「Function not found in base self」(调用行残留致红,三闸之二挂),补删四调用行后全过;G1/G2 记录有载此步(「同批删六调用行」),G3 首轮遗漏,后续组删套时以「函数+调用行」双检为鉴。 +**G4 单元·掷点与乘区 → game/tests/unit/battle_settlement/roll_multiplier_test.gd:五套 36 条逐套原子提交(沿 G3 起组内按套提交)。** +挂载终态=宿主 `_init` 自建 `_db`(ConfigDB 构造注入,`load_tables(..., true)` 失败不中止承 G2/G3 同款);各套自建 RngService(源套掷点隔离形态——逐战斗全新取号,宿主不持统一流成员)。helper 归属:`_first_hero_attack_rows/_weaken_refresh_probe/_ac11_tier_samples`(armor_roll 套内独占)、`_hero_attack_row_with`(pool_wiring 套内独占)、`_run_seal_backlash_round`(damage_types 套内独占)随迁且 smoke_all.gd 同批删原件;`_find_seal_target`(G5 未迁四套仍消费)与 `_run_battle/_auto_play_round`(G6 属主)双份过渡——宿主持一致副本、smoke_all.gd 原件保留(发现④),`_run_battle` 副本去 G6 专属「收束相位回 Idle」断言(该条随 G6 迁其宿主,副本保持零断言纯驱动形态)。`DamageMath` 全局 class_name 裸用(G3 同先例)。manifest 登记随套1 commit。 + +逐套变异记账(每条独立一轮:改坏→跑新宿主→红→`git checkout` 还原→`git status` 清洁确认): + +- `_smoke_enemy_attack_type`(4 条):4/4 验红。变异点——enemy.tres:26 goblin attack_type normal→pierce(红唯一 1/4,越轨行 goblin:pierce);unit_container.gd:110 快照注入硬编码空串(红 3/4——快照为载荷/矩阵共同源,条3 条4 连带属预期形态);damage_controller.gd:471 敌方技载荷 attack_type 置空串(红唯一 1/4);damage_controller.gd:577 `_counter_delta` 查表短路恒中性(红唯一 1/4)。 + ## 十、支援线 9 项待裁闭合(2026-10-22 主会话裁) 1. `extract_checks.py` **常驻保留**——承担清单维护义务(test-conventions §9.3 锚)的再提取/diff 职能;其 DEFAULT_SRC 悬空问题列入波 2:默认源改为三宿主(game/tests/unit、integration、system_test.gd)遍历,或显式路径参数,波 3 出口对账用显式模式。