From 58054819ca806ff99b85582e399d0cb3831dd7f5 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Fri, 23 Oct 2026 03:03:23 +0800 Subject: [PATCH] =?UTF-8?q?test(=E8=BF=81=E7=A7=BB):=20=E6=89=B9=20B=20?= =?UTF-8?q?=E6=B3=A2=201=20G4=20=E5=A5=972=20=E6=8A=A4=E7=94=B2=E6=8E=B7?= =?UTF-8?q?=E7=82=B9=E5=8D=81=E4=BA=94=E6=9D=A1=E8=BF=81=E5=85=A5=20roll?= =?UTF-8?q?=5Fmultiplier=5Ftest.gd=E2=80=94=E2=80=94=E5=89=AA=E5=88=87?= =?UTF-8?q?=E6=8C=82=E8=BD=BD=E7=BB=BF=EF=BC=88=E5=AE=BF=E4=B8=BB=2020=20?= =?UTF-8?q?=E6=A3=80=E6=9F=A5=EF=BC=89=EF=BC=8B=E5=8F=98=E5=BC=82=2015/15?= =?UTF-8?q?=20=E9=AA=8C=E7=BA=A2=EF=BC=88=E6=9D=A13=20=E9=A6=96=E8=AF=95?= =?UTF-8?q?=E6=97=A0=E6=95=88=E6=8D=A2=20roll=20=E4=B8=8A=E7=95=8C?= =?UTF-8?q?=E6=94=B6=E7=AA=84=E5=B7=B2=E8=AE=B0=E8=B4=A6=EF=BC=89=EF=BC=9B?= =?UTF-8?q?=E5=A5=97=E5=86=85=E7=8B=AC=E5=8D=A0=20=5Ffirst=5Fhero=5Fattack?= =?UTF-8?q?=5Frows/=5Fweaken=5Frefresh=5Fprobe/=5Fac11=5Ftier=5Fsamples=20?= =?UTF-8?q?=E9=9A=8F=E8=BF=81=EF=BC=8C=5Frun=5Fbattle/=5Fauto=5Fplay=5Frou?= =?UTF-8?q?nd=20=E8=BF=87=E6=B8=A1=E5=89=AF=E6=9C=AC=EF=BC=88=E5=8E=BB=20G?= =?UTF-8?q?6=20=E4=B8=93=E5=B1=9E=20Idle=20=E6=96=AD=E8=A8=80=EF=BC=8C?= =?UTF-8?q?=E5=8E=9F=E4=BB=B6=E4=BF=9D=E7=95=99=EF=BC=89=EF=BC=9Bsmoke=5Fa?= =?UTF-8?q?ll.gd=20=E5=90=8C=E6=89=B9=E5=88=A0=E5=87=BD=E6=95=B0=E4=B8=89?= =?UTF-8?q?=20helper=20=E8=B0=83=E7=94=A8=E8=A1=8C=EF=BC=9B=E9=97=B801=20?= =?UTF-8?q?=E5=89=A9=E4=BD=99=E9=9D=A2=20998=20=E7=BB=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/smoke/smoke_all.gd | 146 ------------- .../battle_settlement/roll_multiplier_test.gd | 198 ++++++++++++++++++ reports/批B-断言迁移账本-2026-10-22.md | 1 + 3 files changed, 199 insertions(+), 146 deletions(-) diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index 208cf84d..bf542458 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_armor_roll() _smoke_pool_wiring() _smoke_type_glow() _smoke_variant_placement(1) @@ -1399,151 +1398,6 @@ func _smoke_level() -> void: -# ---- 护甲等级掷点·行为面(折扣档分布 / nat20 暴击 / 伤害下限 / 棋盘来源必中)---- - -func _smoke_armor_roll() -> void: - print("--- 护甲等级掷点(查档纯函数 + 暴击 + 伤害下限 + AC12 分布 + 棋盘必中)---") - # ① 查档纯函数:全掷点域逐位对照(AC10 / AC12)+ EV 锚(AC10 期望减免 21.25% —— registry 参考数) - var expect_ac10 := {100: 11, 75: 3, 50: 4, 25: 2} # 掷 10-20 全额 / 7-9 / 3-6 / 1-2 - var expect_ac12 := {100: 9, 75: 3, 50: 4, 25: 4} # 掷 12-20 全额 / 9-11 / 5-8 / 1-4 - for cfg in [{"ac": 10, "expect": expect_ac10}, {"ac": 12, "expect": expect_ac12}]: - var dist := {100: 0, 75: 0, 50: 0, 25: 0} - var tier_ok := true - for roll in range(1, 21): - var d: int = DamageMath.roll_discount_percent(roll, cfg.ac, _db.settlement.roll_tier_bounds, _db.settlement.roll_tier_discounts) - if not dist.has(d): - tier_ok = false - continue - dist[d] += 1 - check(tier_ok and dist == cfg.expect, "查档 AC%d 全掷点域分布(100/75/50/25 = %s)" % [cfg.ac, str([dist[100], dist[75], dist[50], dist[25]])]) - var ev_sum := 0 - for roll in range(1, 21): - ev_sum += DamageMath.roll_discount_percent(roll, 10, _db.settlement.roll_tier_bounds, _db.settlement.roll_tier_discounts) - check(ev_sum == 1575, "AC10 EV 锚:期望系数合计 %d/2000 = 期望减免 21.25%%(nat20 恒全额含内)" % ev_sum) - # ② nat20 暴击(行为面):探针按行为面扫描 - # 「主角先手首掷 = 20」的种子,不假设 armor_roll scope0 位 - var crit_seed := -1 - var crit_rows := {} - for s in range(1, 800): - crit_rows = _first_hero_attack_rows(s) - if int(crit_rows.get("roll", -1)) == int(_db.settlement.crit_roll): - crit_seed = s - break - check(crit_seed > 0, "nat20 种子可达(seed=%d——主角先手首掷命中 %d)" % [crit_seed, int(_db.settlement.crit_roll)]) - check(int(crit_rows.get("roll", -1)) == int(_db.settlement.crit_roll) and bool(crit_rows.get("crit", false)), - "nat20 → 行留档 roll=20 + crit 标记(主角先手普通分量)") - check(int(crit_rows.get("normal", -1)) == 2 * int(_db.hero.atk) and int(crit_rows.get("value", -1)) == 2 * int(_db.hero.atk), - "nat20 → 普通分量 ×2(%d = 2×%d,暴击倍率只乘该分量,穿刺/魔法不吃)" % [int(crit_rows.get("normal", -1)), int(_db.hero.atk)]) - # ③ 削弱续时(行为面):授予→衰减一轮→再授予 → 层数保持 1、剩余结算数重置 - # (削弱为续时语义,无叠层通道) - var weaken_probe := _weaken_refresh_probe() - check(int(weaken_probe.get("layers", -1)) == 1 and int(weaken_probe.get("remaining", -1)) == 2, - "削弱重复授予 → 层数保持 1、剩余结算数重置 2(续时语义——结构性消饱和)") - check(int(weaken_probe.get("pool", -999)) == int(_db.status_effect_row("weaken").magnitude), - "削弱单层池读数 = 行值(enemy_output 池单层贡献——×2 叠满触底 ×0 不可达,免伤轮根治)") - # ④ AC11 折扣档分布(行为面):ac_up 逐轮授给(时效 1)→ Boss 行攻击拍逐拍判定 - # (ac_up 层数恒 1——局内可达 AC 上限 10+1=11,探针随行取值) - var tiers := _ac11_tier_samples() - var tier_distinct := {} - var tiers_valid := true - for t in tiers: - tier_distinct[int(t)] = true - if not [100, 75, 50, 25].has(int(t)): - tiers_valid = false - check(tiers_valid and tiers.size() >= 3, "AC11 攻击拍逐拍判定(×%d 拍全部落档域 {100,75,50,25})" % tiers.size()) - check(tier_distinct.size() >= 2, "折扣档实际流动(AC11 实测档位 %s——≥2 档可见)" % str(tier_distinct.keys())) - # ⑤ 棋盘来源必中 + 吃掷点行留档(全战斗流扫描) - var run = _run_battle(1, 0) - var board_ok := true - var attack_ok := true - var all_positive := true - for row in run.stream: - if String(row.get("type", "")) != "damage": - continue - if int(row.get("value", 0)) <= 0: - all_positive = false - var src := String(row.get("source_class", "")) - if src == "hero_attack" or src == "enemy_action": - if not (row.has("roll") and row.has("ac") and row.has("tier")): - attack_ok = false - elif row.has("roll"): - board_ok = false # 棋盘来源(爆裂/直伤/清障炸药)必中全额——不掷不留档 - check(all_positive, "全战斗流零 v=0 伤害行(伤害下限全局生效)") - check(attack_ok, "hero_attack/enemy_action 伤害行掷点读数+档位随行留档(P2 归因)") - check(board_ok, "棋盘来源伤害行零掷点字段(爆裂/直伤/清障炸药/奖励即时必中全额)") - # ⑥ 全来源伤害下限(任何伤害落地取值域 [1,+∞) 无上限——棋盘克制直伤/反噬/地形变异/爆裂 - # 全部收在 damage_floor 闸内) - var df: int = int(_db.settlement.damage_floor) - check(DamageMath.counter_component(2, -1, df) == 1, "棋盘克制直伤贴底 → 下限 1(点数 2 vs 不利修正 −1)") - check(DamageMath.counter_component(1, -3, df) == 1, "克制修正触底穿透 → 下限 1(点数 1 vs 修正 −3)") - check(DamageMath.counter_component(0, 5, df) == 0, "空桶不吃修正恒 0(零点数分量无伤害可落地——边界不变不受下限影响)") - - -# 定位种子下首场战斗第一行主角攻击伤害行(停手轮——零授予零充能,普通分量 = 攻击基线裸值) -func _first_hero_attack_rows(seed_val: int) -> Dictionary: - var rng = load("res://core/rng_service.gd").new() - rng.set_master_seed(seed_val) - 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": 0, "hero_hp": int(_db.hero.max_hp)}): - return {} - settlement.minesweeper.settle_batch([{"action": "stop"}]) - for row in rows: - if String(row.get("type", "")) == "damage" and String(row.get("source_class", "")) == "hero_attack": - return row - return {} - - -# 削弱续时探测: -# 授予 → 衰减一轮 → 再授予,读 weaken 实例投影 {layers, remaining, pool}。 -# 直授通道(来源格真实坐标惯例——沿前例);depth 2 板(D2 熊地精)仅作宿主,行为与敌种无关。 -func _weaken_refresh_probe() -> Dictionary: - var rng = load("res://core/rng_service.gd").new() - rng.set_master_seed(20260916) - var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) - if not settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.hero.max_hp)}): - return {} - var src: Dictionary = {"source_class": "mine_hit", "origin_cell": Vector2i(0, 0), "round_index": settlement.round_index} - settlement._status_effects.apply_effect("weaken", src) - settlement._status_effects.decay_round() # remaining 2 → 1(先兑付后衰减——只借衰减器,不结算) - settlement._status_effects.apply_effect("weaken", src) # 续时:remaining ← 2,层数不动 - for eff in settlement._status_effects.query_active("enemy"): - if String(eff.effect_id) == "weaken": - # 走伤害控制器公有算术面 clamped_pool - return {"layers": int(eff.layers), "remaining": int(eff.remaining_settlements), "pool": int(settlement._damage.clamped_pool("enemy_output"))} - return {} - - -# AC11 折扣档采样:每停手轮前直授 ac_up ×1(时效 1 逐轮衰减)——敌拍逐拍判定留档。 -# (ac_up 只续时不叠层、层数恒 1——采样上限取 AC11; -# 技能出招为摇号制——扫描种子取首个 ≥2 档样本集。) -func _ac11_tier_samples() -> Array: - for seed_offset in range(20): - var rng = load("res://core/rng_service.gd").new() - rng.set_master_seed(20260917 + 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": 1, "node_depth": 2, "hero_hp": int(_db.hero.max_hp)}): - continue - var guard := 12 - while settlement.battle_result == "" and guard > 0: - guard -= 1 - settlement._status_effects.apply_effect("ac_up", {"source_class": "safe_cell", "origin_cell": Vector2i(0, 0), "round_index": settlement.round_index}) - settlement.minesweeper.settle_batch([{"action": "stop"}]) - var tiers: Array = [] - for row in rows: - if String(row.get("type", "")) == "damage" and String(row.get("source_class", "")) == "enemy_action": - tiers.append(int(row.get("tier", -1))) - var distinct := {} - for t in tiers: - distinct[int(t)] = true - if tiers.size() >= 3 and distinct.size() >= 2: - return tiers - return [] - - # ---- 主角乘区接线 + 硬化折半方向 ---- # 同种子停手轮恒同掷点(效果只改乘区、不动掷点流)→ 落地伤害精确关系: # 增伤 = 基线 ×1.5、乏力 = ×0.75(hero_offense 池乘攻击合计 = 单分量,无直乘旁路); diff --git a/game/tests/unit/battle_settlement/roll_multiplier_test.gd b/game/tests/unit/battle_settlement/roll_multiplier_test.gd index 3c284917..72de9055 100644 --- a/game/tests/unit/battle_settlement/roll_multiplier_test.gd +++ b/game/tests/unit/battle_settlement/roll_multiplier_test.gd @@ -13,6 +13,7 @@ extends SceneTree const Kit := preload("res://tests/unit/test_kit.gd") const ConfigDBScript := preload("res://core/config_db.gd") +const AutoPlayer := preload("res://smoke/auto_player.gd") # 自动玩家策略单一实现(迁移期按现路径 preload——账本 §五夹具前提) var _db @@ -29,6 +30,7 @@ func _init() -> void: push_error(" [%s] %s: %s" % [d.severity, d.category, d.message]) # 套执行序沿源编排序 _smoke_enemy_attack_type() + _smoke_armor_roll() quit(Kit.finish("roll_multiplier_test")) @@ -91,3 +93,199 @@ func _smoke_enemy_attack_type() -> void: 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") + + +# ---- 护甲等级掷点·行为面(折扣档分布 / nat20 暴击 / 伤害下限 / 棋盘来源必中)---- + +func _smoke_armor_roll() -> void: + print("--- 护甲等级掷点(查档纯函数 + 暴击 + 伤害下限 + AC12 分布 + 棋盘必中)---") + # ① 查档纯函数:全掷点域逐位对照(AC10 / AC12)+ EV 锚(AC10 期望减免 21.25% —— registry 参考数) + var expect_ac10 := {100: 11, 75: 3, 50: 4, 25: 2} # 掷 10-20 全额 / 7-9 / 3-6 / 1-2 + var expect_ac12 := {100: 9, 75: 3, 50: 4, 25: 4} # 掷 12-20 全额 / 9-11 / 5-8 / 1-4 + for cfg in [{"ac": 10, "expect": expect_ac10}, {"ac": 12, "expect": expect_ac12}]: + var dist := {100: 0, 75: 0, 50: 0, 25: 0} + var tier_ok := true + for roll in range(1, 21): + var d: int = DamageMath.roll_discount_percent(roll, cfg.ac, _db.settlement.roll_tier_bounds, _db.settlement.roll_tier_discounts) + if not dist.has(d): + tier_ok = false + continue + dist[d] += 1 + Kit.check("查档 AC%d 全掷点域分布(100/75/50/25 = %s)" % [cfg.ac, str([dist[100], dist[75], dist[50], dist[25]])], tier_ok and dist == cfg.expect) + var ev_sum := 0 + for roll in range(1, 21): + ev_sum += DamageMath.roll_discount_percent(roll, 10, _db.settlement.roll_tier_bounds, _db.settlement.roll_tier_discounts) + Kit.check("AC10 EV 锚:期望系数合计 %d/2000 = 期望减免 21.25%%(nat20 恒全额含内)" % ev_sum, ev_sum == 1575) + # ② nat20 暴击(行为面):探针按行为面扫描 + # 「主角先手首掷 = 20」的种子,不假设 armor_roll scope0 位 + var crit_seed := -1 + var crit_rows := {} + for s in range(1, 800): + crit_rows = _first_hero_attack_rows(s) + if int(crit_rows.get("roll", -1)) == int(_db.settlement.crit_roll): + crit_seed = s + break + Kit.check("nat20 种子可达(seed=%d——主角先手首掷命中 %d)" % [crit_seed, int(_db.settlement.crit_roll)], crit_seed > 0) + Kit.check("nat20 → 行留档 roll=20 + crit 标记(主角先手普通分量)", + int(crit_rows.get("roll", -1)) == int(_db.settlement.crit_roll) and bool(crit_rows.get("crit", false))) + Kit.check("nat20 → 普通分量 ×2(%d = 2×%d,暴击倍率只乘该分量,穿刺/魔法不吃)" % [int(crit_rows.get("normal", -1)), int(_db.hero.atk)], + int(crit_rows.get("normal", -1)) == 2 * int(_db.hero.atk) and int(crit_rows.get("value", -1)) == 2 * int(_db.hero.atk)) + # ③ 削弱续时(行为面):授予→衰减一轮→再授予 → 层数保持 1、剩余结算数重置 + # (削弱为续时语义,无叠层通道) + var weaken_probe := _weaken_refresh_probe() + Kit.check("削弱重复授予 → 层数保持 1、剩余结算数重置 2(续时语义——结构性消饱和)", + int(weaken_probe.get("layers", -1)) == 1 and int(weaken_probe.get("remaining", -1)) == 2) + Kit.check("削弱单层池读数 = 行值(enemy_output 池单层贡献——×2 叠满触底 ×0 不可达,免伤轮根治)", + int(weaken_probe.get("pool", -999)) == int(_db.status_effect_row("weaken").magnitude)) + # ④ AC11 折扣档分布(行为面):ac_up 逐轮授给(时效 1)→ Boss 行攻击拍逐拍判定 + # (ac_up 层数恒 1——局内可达 AC 上限 10+1=11,探针随行取值) + var tiers := _ac11_tier_samples() + var tier_distinct := {} + var tiers_valid := true + for t in tiers: + tier_distinct[int(t)] = true + if not [100, 75, 50, 25].has(int(t)): + tiers_valid = false + Kit.check("AC11 攻击拍逐拍判定(×%d 拍全部落档域 {100,75,50,25})" % tiers.size(), tiers_valid and tiers.size() >= 3) + Kit.check("折扣档实际流动(AC11 实测档位 %s——≥2 档可见)" % str(tier_distinct.keys()), tier_distinct.size() >= 2) + # ⑤ 棋盘来源必中 + 吃掷点行留档(全战斗流扫描) + var run = _run_battle(1, 0) + var board_ok := true + var attack_ok := true + var all_positive := true + for row in run.stream: + if String(row.get("type", "")) != "damage": + continue + if int(row.get("value", 0)) <= 0: + all_positive = false + var src := String(row.get("source_class", "")) + if src == "hero_attack" or src == "enemy_action": + if not (row.has("roll") and row.has("ac") and row.has("tier")): + attack_ok = false + elif row.has("roll"): + board_ok = false # 棋盘来源(爆裂/直伤/清障炸药)必中全额——不掷不留档 + Kit.check("全战斗流零 v=0 伤害行(伤害下限全局生效)", all_positive) + Kit.check("hero_attack/enemy_action 伤害行掷点读数+档位随行留档(P2 归因)", attack_ok) + Kit.check("棋盘来源伤害行零掷点字段(爆裂/直伤/清障炸药/奖励即时必中全额)", board_ok) + # ⑥ 全来源伤害下限(任何伤害落地取值域 [1,+∞) 无上限——棋盘克制直伤/反噬/地形变异/爆裂 + # 全部收在 damage_floor 闸内) + var df: int = int(_db.settlement.damage_floor) + Kit.check("棋盘克制直伤贴底 → 下限 1(点数 2 vs 不利修正 −1)", DamageMath.counter_component(2, -1, df) == 1) + Kit.check("克制修正触底穿透 → 下限 1(点数 1 vs 修正 −3)", DamageMath.counter_component(1, -3, df) == 1) + Kit.check("空桶不吃修正恒 0(零点数分量无伤害可落地——边界不变不受下限影响)", DamageMath.counter_component(0, 5, df) == 0) + + +# 定位种子下首场战斗第一行主角攻击伤害行(停手轮——零授予零充能,普通分量 = 攻击基线裸值) +func _first_hero_attack_rows(seed_val: int) -> Dictionary: + var rng = load("res://core/rng_service.gd").new() + rng.set_master_seed(seed_val) + 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": 0, "hero_hp": int(_db.hero.max_hp)}): + return {} + settlement.minesweeper.settle_batch([{"action": "stop"}]) + for row in rows: + if String(row.get("type", "")) == "damage" and String(row.get("source_class", "")) == "hero_attack": + return row + return {} + + +# 削弱续时探测: +# 授予 → 衰减一轮 → 再授予,读 weaken 实例投影 {layers, remaining, pool}。 +# 直授通道(来源格真实坐标惯例——沿前例);depth 2 板(D2 熊地精)仅作宿主,行为与敌种无关。 +func _weaken_refresh_probe() -> Dictionary: + var rng = load("res://core/rng_service.gd").new() + rng.set_master_seed(20260916) + var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) + if not settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.hero.max_hp)}): + return {} + var src: Dictionary = {"source_class": "mine_hit", "origin_cell": Vector2i(0, 0), "round_index": settlement.round_index} + settlement._status_effects.apply_effect("weaken", src) + settlement._status_effects.decay_round() # remaining 2 → 1(先兑付后衰减——只借衰减器,不结算) + settlement._status_effects.apply_effect("weaken", src) # 续时:remaining ← 2,层数不动 + for eff in settlement._status_effects.query_active("enemy"): + if String(eff.effect_id) == "weaken": + # 走伤害控制器公有算术面 clamped_pool + return {"layers": int(eff.layers), "remaining": int(eff.remaining_settlements), "pool": int(settlement._damage.clamped_pool("enemy_output"))} + return {} + + +# AC11 折扣档采样:每停手轮前直授 ac_up ×1(时效 1 逐轮衰减)——敌拍逐拍判定留档。 +# (ac_up 只续时不叠层、层数恒 1——采样上限取 AC11; +# 技能出招为摇号制——扫描种子取首个 ≥2 档样本集。) +func _ac11_tier_samples() -> Array: + for seed_offset in range(20): + var rng = load("res://core/rng_service.gd").new() + rng.set_master_seed(20260917 + 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": 1, "node_depth": 2, "hero_hp": int(_db.hero.max_hp)}): + continue + var guard := 12 + while settlement.battle_result == "" and guard > 0: + guard -= 1 + settlement._status_effects.apply_effect("ac_up", {"source_class": "safe_cell", "origin_cell": Vector2i(0, 0), "round_index": settlement.round_index}) + settlement.minesweeper.settle_batch([{"action": "stop"}]) + var tiers: Array = [] + for row in rows: + if String(row.get("type", "")) == "damage" and String(row.get("source_class", "")) == "enemy_action": + tiers.append(int(row.get("tier", -1))) + var distinct := {} + for t in tiers: + distinct[int(t)] = true + if tiers.size() >= 3 and distinct.size() >= 2: + return tiers + return [] + + +# ---- 全战斗回放驱动(过渡副本——G6 属主件 `_run_battle`/`_auto_play_round` 的宿主侧一致副本: +# armor_roll 棋盘来源必中扫描需真实流驱动而 G6 宿主未建;smoke_all.gd 原件保留供未迁套消费, +# 收尾批与原件去重。副本去 G6 专属「收束相位回 Idle」断言——该条随 `_run_battle` 本体迁其宿主, +# 本副本保持零断言纯驱动形态)---- +func _run_battle(L: int, D: int, axes: Dictionary = {}) -> Dictionary: + # 每遍全新 RngService(同 master 种子)——回放断言的隔离前提(地图战斗资源 AC"两套全新 RngService+地图战斗资源"同款) + var rng = load("res://core/rng_service.gd").new() + rng.set_master_seed(42) + var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) + var out := {"stream": [], "events": [], "rounds": 0, "result": "", "terminated": false} + settlement.step_row.connect(func(row): out.stream.append(row)) + settlement.battle_finished.connect(func(payload): out.result = payload.get("result", "")) + settlement.minesweeper.cell_revealed.connect(func(evt): out.events.append(evt)) + settlement.minesweeper.seal_judged.connect(func(evt): out.events.append(evt)) + settlement.minesweeper.round_end_board.connect(func(p): out.events.append({"ev": "round_end_board", "path_tag": p.path_tag})) + settlement.action_economy.round_end_stopped.connect(func(p): out.events.append({"ev": "round_end_stopped", "path_tag": p.path_tag})) + var enc := {"level_index": L, "node_depth": D, "hero_hp": _db.hero.max_hp} + enc.merge(axes, true) # 两根类型轴注入尾参(缺省零注入=回退面) + if not settlement.begin_battle(enc): + out.stream.append({"type": "verdict", "value": "begin_failed"}) + return out + var max_rounds := 40 + while out.result == "" and out.rounds < max_rounds: + out.rounds += 1 + _auto_play_round(settlement, out) + if out.result == "": + out.result = "timeout" + out.terminated = out.result == "victory" or out.result == "defeat" + return out + + +func _auto_play_round(settlement, out: Dictionary) -> void: + var minesweeper = settlement.minesweeper + var action_economy = settlement.action_economy + var start_round: int = settlement.round_index + # 确定性自动玩家:挖最近的已探明邻接未开格;无可挖则停手(结算窗口由信号回调提交) + var guard := 400 + while action_economy.phase == "Active" and settlement.round_index == start_round and guard > 0 and out.result == "": + guard -= 1 + var plan := AutoPlayer.choose_dig(minesweeper) + if plan.is_empty(): + break + if plan.hero_cell != minesweeper.hero_pos(): + minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}]) + if action_economy.phase != "Active" or settlement.round_index != start_round: + break + minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}]) + if action_economy.phase == "Active" and settlement.round_index == start_round and out.result == "": + minesweeper.settle_batch([{"action": "stop"}]) diff --git a/reports/批B-断言迁移账本-2026-10-22.md b/reports/批B-断言迁移账本-2026-10-22.md index 4647c7f2..7313ba3d 100644 --- a/reports/批B-断言迁移账本-2026-10-22.md +++ b/reports/批B-断言迁移账本-2026-10-22.md @@ -282,6 +282,7 @@ 逐套变异记账(每条独立一轮:改坏→跑新宿主→红→`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)。 +- `_smoke_armor_roll`(15 条):15/15 验红(条3 含一次无效首试换点,共 16 轮)。变异点——damage_math.gd:36 档位边界比较 `>`→`>=`(红「查档分布」双迭代+「EV 锚」连带——纯函数共面);settlement.tres roll_tier_discounts 75→74(红「EV 锚」1572≠1575,连带查档双迭代+AC11 落域);damage_controller.gd:540 暴击判定恒假(**首试无效变异**——「nat20 种子可达」只查掷点值==crit_roll 不查标记,判路面不被触及;该轮实际验红条4/条5 记为下两条的顺带证据)→换点 :539 roll 上界收窄 20 不可达(红「种子可达」seed=-1,连带条4/条5——种子扫描共同源);:375 hero 载荷 crit 标记写入恒假(红「crit 标记留档」红唯一 1/20);:369 暴击倍率传参 200→100(红「普通分量 ×2」红唯一);status_container.gd:52 续时刷新分支 pass 化(红「削弱续时」红唯一);:40 幅度快照 −1 偏移(红「削弱池读数=行值」红唯一);damage_math.gd:38 档值 +7 偏移出域(红「AC11 逐拍落域」,连带查档双迭代+EV 锚——纯函数共面;「折扣档流动」保持绿);damage_controller.gd:539 掷点折叠恒 nat 面(红「折扣档流动」——采样 distinct 恒 1 穷尽返空 ×0 拍连带「AC11 逐拍落域」);:515 board_blast 下限拆除恒 0(红「零 v=0 伤害行」红唯一——流中确有群伤行);:374 hero 载荷 tier 键 pass 化(红「掷点读数留档」红唯一);:504 board_typed 载荷塞 roll 键(红「棋盘行零掷点字段」红唯一);damage_math.gd counter_component 三点隔离变异:贴底特例 `sum==1→99`(红「贴底下限」红唯一)、负和特例 `sum<0→99`(红「触底穿透」红唯一)、空桶早退 `return 0→1`(红「空桶恒 0」红唯一)。 ## 十、支援线 9 项待裁闭合(2026-10-22 主会话裁)