# VERTICAL SLICE - NOT FOR PRODUCTION # Date: 2026-09-13(playtest-2026-09-12 报告 Top1——零承伤根因分割探针) # 2026-09-16 改掷点口径(护甲等级掷点批):defense_up 墙已随 AC 换装退役—— # H1 旧 defense_up 叠满墙失义(hero_defense 池现仅 off_guard 正向行,×0 墙结构面消失) # H2 Boss/敌攻击链健康:fresh 池下非蓄力拍 d_tick = max(下限, 池乘区(掷点档位基线))(伏击附加随八型批 2026-09-20 退役) # 快照盲区说明:settlement_snapshot 取在衰减段之后,时效 1 的 ac_up 当轮授予当轮衰减—— # 真人局 JSONL buffs[] 恒空 ≠ 状态为空,本探针改从结算行(授予/续存 effect 行的 pool_value)重建战斗段时点池值。 # 观测面:全部读战斗结算步骤行 / 表值 / 呈现快照(零核心穿透);池归属/档位/下限全读表(数据配置 Rule 1——零数值字面量)。 # 模式(-- 后参数):boss [seed](默认 42:Run A 零挖掘 fresh 池 + Run B 自动挖掘授予行为) # level [seed](全关遭遇回放 + 多种子 spawn 扫描——报告 Bug#1 证据面) # 运行:Godot_console.exe --headless --path . --script res://smoke/damage_probe.gd -- boss 42 # Godot_console.exe --headless --path . --script res://smoke/damage_probe.gd -- level 42 extends SceneTree const AutoPlayer := preload("res://smoke/auto_player.gd") var _checks := 0 var _fails := 0 func check(cond: bool, label: String) -> void: _checks += 1 if cond: print(" [PASS] " + label) else: _fails += 1 print(" [FAIL] " + label) func _initialize() -> void: print("=== 零承伤根因分割探针(playtest-2026-09-12 报告 Top1)===") var db = load("res://core/config_db.gd").new() if not db.load_tables("res://data/tables", false): print("表加载失败") quit(1) return var args := OS.get_cmdline_user_args() var mode := "boss" if args.size() >= 1 and (args[0] == "boss" or args[0] == "level"): mode = args[0] var seed_val := 42 if args.size() >= 2 and args[1].is_valid_int(): seed_val = int(args[1]) if mode == "boss": _probe_boss(db, seed_val) else: _probe_level(db, seed_val) print("=== 探针完成:%d 项检查,%d 项失败 ===" % [_checks, _fails]) quit(1 if _fails > 0 else 0) # ==================== boss 模式:逐结算行打印 + 根因判定 ==================== func _probe_boss(db, seed_val: int) -> void: var math = load("res://core/damage_math.gd") var clamp_lo: int = int(db.settlement.pool_clamp_lo) var clamp_hi: int = int(db.settlement.pool_clamp_hi) var hero_def_negative := 0 for eff_id in db.settlement.effect_pool_map.keys(): if String(db.settlement.effect_pool_map[eff_id]) != "hero_defense": continue var row: Dictionary = db.status_effect_row(eff_id) if not row.is_empty() and String(row.get("unit", "")) == "percent": hero_def_negative = mini(hero_def_negative, int(row.get("magnitude", 0))) print("表值面:hero_defense 池最负 percent 行 %d · pool_clamp [%d, %d] · damage_floor %d · AC 基准 %d(+ac_up×%d 层)" % [ hero_def_negative, clamp_lo, clamp_hi, int(db.settlement.damage_floor), int(db.hero.armor_class), int(db.status_effect_row("ac_up").get("max_stack", 0))]) print("--- Run A:fresh 池(零挖掘停手轮——无授予,攻击链裸验)---") var run_a := _run_boss(db, seed_val, false) _print_rounds(db, math, run_a, true) var a_ok := _assert_rounds(db, math, run_a, true) check(a_ok.fresh_and_full, "A1 攻击链健康(fresh 池·掷点口径):非蓄力拍 d_tick = max(下限, 掷点档位基线+附加)(×%d 拍)——H2 攻击链缺陷否证" % a_ok.n_attack) check(a_ok.arith_ok, "A2 池算术完整:全部非蓄力拍 d_tick == 掷点口径期望(×%d 拍)" % a_ok.n_attack) check(String(run_a.result) == "defeat", "A3 攻击链致死可达:fresh 池 Boss 在 %d 轮内击倒英雄(result=%s——伤害确实落地)" % [run_a.snaps.size(), str(run_a.result)]) var run_a2 := _run_boss(db, seed_val, false) check(JSON.stringify(run_a.rows) == JSON.stringify(run_a2.rows), "A4 同种子回放:Run A 两遍结算行逐行相同(含 armor_roll 掷点流)") print("--- Run B:自动挖掘(授予照常发生——当前数据承伤行为实测)---") var run_b := _run_boss(db, seed_val, true) _print_rounds(db, math, run_b, true) var b_ok := _assert_rounds(db, math, run_b, false) check(b_ok.arith_ok, "B1 池算术完整(含授予轮):全部非蓄力拍 d_tick == 掷点口径期望(×%d 拍)" % b_ok.n_attack) check(b_ok.wall_tick_positive, "B2 负池轮不归零(伤害下限):hero_def<0 的轮 d_tick 恒 ≥ damage_floor(×%d 轮,最深实测池 %d)" % [b_ok.n_wall, b_ok.deepest]) check(hero_def_negative * 16 > clamp_lo or hero_def_negative >= 0, "B3 墙归零不可达(表值判定):hero_defense 池最负行 %d——叠满不可触池底 %d(×0 免伤墙结构面已随 AC 换装消失)" % [hero_def_negative, clamp_lo]) func _run_boss(db, seed_val: int, dig: bool) -> 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 cap := {"rows": [], "snaps": [], "result": ""} 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)}): cap["result"] = "begin_failed" return cap var rounds := 0 while settlement.battle_result == "" and rounds < 40: rounds += 1 var start_round: int = settlement.round_index if dig: var minesweeper = settlement.minesweeper var guard := 400 while settlement.action_economy.phase == "Active" and settlement.round_index == start_round and guard > 0: 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 settlement.action_economy.phase == "Active": minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}]) if settlement.action_economy.phase == "Active" and settlement.round_index == start_round: minesweeper.settle_batch([{"action": "stop"}]) else: settlement.minesweeper.settle_batch([{"action": "stop"}]) return cap # 逐轮:授予行汇总 → 重建战斗段时点池值(effect_pool_map 表驱动归属)→ 敌行动行逐行打印 func _print_rounds(db, math, run: Dictionary, verbose: bool) -> void: var groups := _round_groups(run.rows) var beats := _played_beats(run, db) var rounds: Array = groups.keys() rounds.sort() for ri in rounds: var rows: Array = groups[ri] var an := _analyze_round(db, math, rows, beats) var grant_txt := "(无)" if not an.grants.is_empty(): grant_txt = " · ".join(an.grants) print(" [R%d] 授予: %s" % [ri, grant_txt]) for line in an.lines: print(" " + String(line)) var hp_txt := "" if int(ri) - 1 < run.snaps.size(): var s: Dictionary = run.snaps[int(ri) - 1] var hero: Dictionary = s.get("hero", {}) hp_txt = " · hero %d/%d · verdict %s" % [int(hero.get("hp", -1)), int(hero.get("max_hp", -1)), String(s.get("verdict", ""))] print(" [R%d] 池值@战斗段: hero_def=%d enemy_out=%d%s" % [ri, an.pd, an.pe, hp_txt]) func _assert_rounds(db, math, run: Dictionary, fresh_mode: bool) -> Dictionary: var groups := _round_groups(run.rows) var beats := _played_beats(run, db) var rounds: Array = groups.keys() rounds.sort() var out := {"fresh_and_full": true, "arith_ok": true, "wall_tick_positive": true, "n_attack": 0, "n_wall": 0, "deepest": 0} for ri in rounds: var an := _analyze_round(db, math, groups[ri], beats) for t in an.ticks: out.n_attack += 1 if fresh_mode and (an.pd != 0 or an.pe != 0): out.fresh_and_full = false if not bool(t.ok): out.arith_ok = false if fresh_mode and (int(t.tick) <= 0 or int(t.raw) <= 0): out.fresh_and_full = false if an.pd < 0: out.n_wall += 1 out.deepest = mini(out.deepest, an.pd) if int(t.tick) <= 0: out.wall_tick_positive = false return out func _round_groups(rows: Array) -> Dictionary: var groups := {} for row in rows: var ri := int(row.get("round_index", 0)) if not groups.has(ri): groups[ri] = [] groups[ri].append(row) return groups func _analyze_round(db, math, rows: Array, beats: Dictionary) -> Dictionary: var last_pool := {} var grants: Array = [] for row in rows: if String(row.get("type", "")) == "effect": var eid := String(row.get("effect_id", "")) var pv := int(row.get("pool_value", 0)) last_pool[eid] = pv grants.append("%s→池%d" % [String(row.get("display_name", "")), pv]) # 池归属表驱动(settlement.effect_pool_map),percent 池加算后 clamp(战斗结算 F2 同口径) var pools := {"hero_defense": 0, "enemy_output": 0, "hero_offense": 0, "flat_attach": 0} for eid in last_pool: var pool_name: String = String(db.settlement.effect_pool_map.get(eid, "")) if pools.has(pool_name): pools[pool_name] += int(last_pool[eid]) var clamp_lo: int = int(db.settlement.pool_clamp_lo) var clamp_hi: int = int(db.settlement.pool_clamp_hi) var pd: int = clampi(int(pools["hero_defense"]), clamp_lo, clamp_hi) var pe: int = clampi(int(pools["enemy_output"]), clamp_lo, clamp_hi) var lines: Array = [] var ticks: Array = [] for row in rows: var t := String(row.get("type", "")) if t == "cast": lines.append("拍=cast(施放行零兑付——增益技不读池,技能池批由蓄力行改型)") elif t == "damage" and String(row.get("source_class", "")) == "enemy_action": var baseline: int = int(row.get("baseline", 0)) var attach: int = int(row.get("attach", 0)) # 掷点口径重建(2026-09-16 护甲等级掷点批):基线按行留档档位打折(暴击 ×倍率),附加必中 var tier: int = int(row.get("tier", 100)) var crit: bool = bool(row.get("crit", false)) var base_factor: float = float(tier) * (float(db.settlement.crit_multiplier) / 100.0 if crit else 1.0) var rolled_base := int(floor(float(baseline) * base_factor / 100.0)) var raw: int = rolled_base + attach var tick: int = int(row.get("value", 0)) var expected: int = maxi(int(db.settlement.damage_floor), math.pool_damage(raw, pd, pe)) var roll_txt := "掷%s" % str(int(row.get("roll", -1))) var zeroed := "✓" if tick == expected else "✗ 算术不符!" lines.append("拍=%s %s/档%d%s 基线%d→掷后%d 附加%d hero_def=%d enemy_out=%d d_tick=%d 期望=%d %s" % [ String(beats.get(int(row.get("round_index", 0)), "?")), roll_txt, tier, "暴击" if crit else "", baseline, rolled_base, attach, pd, pe, tick, expected, zeroed]) ticks.append({"raw": raw, "tick": tick, "expected": expected, "ok": tick == expected}) elif t == "damage" and String(row.get("source_class", "")) == "hero_attack": lines.append("主角攻击 %d(普通 %d + 穿刺 %d + 魔法 %d)→ %s" % [ int(row.get("value", 0)), int(row.get("normal", 0)), int(row.get("pierce", 0)), int(row.get("magic", 0)), String(row.get("target", ""))]) return {"pd": pd, "pe": pe, "grants": grants, "lines": lines, "ticks": ticks} # 拍名重建(敌人技能池批):pending_skill 字段 = 该结算开轮摇定的动作 id(快照取在结算末尾)—— # round r 的拍 = 快照 r 的 enemies[0].pending_skill。快照 enemies 空时(敌死亡轮)回退 "?"——该轮敌已无行动行。 func _played_beats(run: Dictionary, db) -> Dictionary: var beats := {} for r in range(1, run.snaps.size() + 1): var s: Dictionary = run.snaps[r - 1] var en: Array = s.get("enemies", []) if en.size() > 0: beats[r] = String(en[0].get("pending_skill", "?")) return beats # ==================== level 模式:全关遭遇回放(Bug#1 证据面) ==================== func _probe_level(db, seed_val: int) -> void: print("--- 全关回放 seed %d(战×2→道具→战×2→恢复→Boss)---" % seed_val) var prediction := _encounter_prediction(db, seed_val) var cap := _run_level(db, seed_val) var battles: Array = cap.battles for b in battles: var depth: int = int(b.get("depth", -1)) var spawn: int = int(b.get("spawn", -1)) var predicted: int = int(prediction.get(depth, -1)) var verdict_txt := _battle_verdict_txt(cap, depth) var match_txt := "✓" if spawn == predicted else "✗ 与遭遇骰预测不符!" print(" d%d %s: spawn=%d(遭遇骰预测 %d %s)%s" % [depth, String(b.get("type", "")), spawn, predicted, match_txt, verdict_txt]) var all_match := true var all_ge_1 := true for b in battles: var spawn2: int = int(b.get("spawn", -1)) if spawn2 < 1: all_ge_1 = false if spawn2 != int(prediction.get(int(b.get("depth", -1)), -2)): all_match = false check(all_ge_1, "全部战斗 spawn ≥ 1(难度曲线与Boss Rule 8 区间下限——空遭遇在当前实现不可达)") check(all_match, "spawn 与独立遭遇骰预测逐战斗一致(encounter 封闭流同种子可回放)") var expect_battles := _battle_nodes_upto(db, 1, int(cap.get("end_node", -1))) check(battles.size() == expect_battles, "战斗推进数与节点链一致(实测 %d/预期 %d——败北截断或全链跑通)" % [battles.size(), expect_battles]) print(" 关卡结果: %s" % str(cap.result)) print("--- 多种子 spawn 扫描(报告 Bug#1 回归面:20 种子 × 5 战斗节点)---") var scan := _scan_spawns(db, seed_val, 20) check(scan.bad == 0, "%d 场战斗 spawn 全域内(∈[unit_count, unit_count_max] 且 ≥1)——越域 %d 场" % [scan.total, scan.bad]) func _encounter_prediction(db, seed_val: int) -> Dictionary: var rng = load("res://core/rng_service.gd").new() rng.set_master_seed(seed_val) var out := {} var scope := 0 var lrow := _level_row(db, 1) for node in lrow.get("nodes", []): var t := String(node.get("type", "")) if t != "battle" and t != "boss": continue var depth := int(node.get("node_depth", -1)) var row: Dictionary = db.resolve_enemy_rowset(1, depth) var lo := int(row.get("unit_count", 1)) var hi := int(row.get("unit_count_max", lo)) var rolled := lo if hi > lo: var rg := RandomNumberGenerator.new() rg.seed = rng.derive_at(scope) scope += 1 rolled = lo + rg.randi_range(0, hi - lo) out[depth] = rolled return out func _level_row(db, level_index: int) -> Dictionary: for row in db.level.rows: if int(row.level_index) == level_index: return row return {} func _run_level(db, seed_val: int) -> Dictionary: var rng = load("res://core/rng_service.gd").new() rng.set_master_seed(seed_val) var runner = load("res://core/level_runner.gd").new(db, rng) var cap := {"battles": [], "rows": [], "result": "", "end_node": -1} runner.step_row.connect(func(row): cap["rows"].append(row)) runner.node_entered.connect(_on_level_node_entered.bind(runner, cap)) runner.round_started.connect(_on_level_round_started.bind(runner, cap)) runner.level_finished.connect(_on_level_finished.bind(cap)) runner.start_level(1, seed_val) var guard_battles := 20 while cap["result"] == "" and guard_battles > 0: guard_battles -= 1 var settlement = runner.current_battle() if settlement == null: break _drive_battle(runner, settlement, cap) return cap func _on_level_finished(p: Dictionary, cap: Dictionary) -> void: cap["result"] = String(p.get("result", "")) cap["end_node"] = int(p.get("node_index", -1)) func _battle_nodes_upto(db, level_index: int, node_idx: int) -> int: var n := 0 var nodes: Array = _level_row(db, level_index).get("nodes", []) for i in range(mini(node_idx + 1, nodes.size())): var t := String(nodes[i].get("type", "")) if t == "battle" or t == "boss": n += 1 return n func _on_level_node_entered(node: Dictionary, _info: Dictionary, runner, cap: Dictionary) -> void: var t := String(node.get("type", "")) if t == "battle" or t == "boss": cap["battles"].append({"type": t, "depth": int(node.get("node_depth", -1)), "spawn": -1}) else: runner.continue_after_node() func _on_level_round_started(ri: int, runner, cap: Dictionary) -> void: if ri != 1 or cap["battles"].is_empty(): return var settlement = runner.current_battle() if settlement == null: return var s: Dictionary = settlement.presentation_snapshot() cap["battles"][cap["battles"].size() - 1]["spawn"] = s.enemies.size() func _drive_battle(runner, settlement, cap: Dictionary) -> void: var round_guard := 60 while cap["result"] == "" and runner.current_battle() == settlement and settlement.battle_result == "" and round_guard > 0: round_guard -= 1 var start_round: int = settlement.round_index var minesweeper = settlement.minesweeper var action_guard := 400 while runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" \ and settlement.round_index == start_round and action_guard > 0: action_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 settlement.action_economy.phase != "Active" or settlement.round_index != start_round: break minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}]) if runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" and settlement.round_index == start_round: minesweeper.settle_batch([{"action": "stop"}]) # 战斗判定摘要:round-1 victory 的战斗必须能从结算行分辨「首轮全灭」vs「空场」—— # 判据:首轮存在伤害行 = 有敌且被杀(wipe);零战斗行 = 空场(Bug#1 特征) func _battle_verdict_txt(cap: Dictionary, depth: int) -> String: # 逐战斗行段切分:round_index 回卷 = 新战斗(defense_scan 同口径) var battle_rows: Array = [] var segs: Array = [] var last_round := 0 for row in cap.rows: var r: int = int(row.get("round_index", 0)) if r < last_round: segs.append(battle_rows) battle_rows = [] last_round = r battle_rows.append(row) if not battle_rows.is_empty(): segs.append(battle_rows) var target := _battle_index_of_depth(depth, cap) if target >= 0 and target < segs.size(): return _summarize_rows(segs[target]) return "" func _battle_index_of_depth(depth: int, cap: Dictionary) -> int: for i in cap["battles"].size(): if int(cap["battles"][i].get("depth", -1)) == depth: return i return -1 func _summarize_rows(rows: Array) -> String: var verdict := "" var r1_rows: Array = [] for row in rows: if String(row.get("type", "")) == "verdict": verdict = String(row.get("display_name", "")) if int(row.get("round_index", 0)) == 1: r1_rows.append(row) if verdict != "战斗胜利" or r1_rows.is_empty(): return "· %s(%d 行)" % [verdict, rows.size()] var combat_rows := 0 var kills := 0 for row in r1_rows: var t := String(row.get("type", "")) if t == "damage": combat_rows += 1 if bool(row.get("killed", false)): kills += 1 if combat_rows > 0: return "· round-1 胜利 = 首轮全灭(wipe):战斗行 ×%d、击杀 ×%d——非空场" % [combat_rows, kills] return "· round-1 胜利且零战斗行 = 空场特征(Bug#1)!" func _scan_spawns(db, seed0: int, n_seeds: int) -> Dictionary: var depths: Array = [] 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))) 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: 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)}): bad += 1 continue var n: int = settlement.presentation_snapshot().enemies.size() var row: Dictionary = db.resolve_enemy_rowset(1, int(depth)) 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: bad += 1 return {"total": total, "bad": bad}