T6 探针基建收口段: 三族样板收单点(probe_ui_util树寻址contains/begins_with参数化+isolate_save七份隔离样板/battle_driver五份战斗驱动)+level_autoplay双战斗循环合并_decide_act+test_kit收13套件_check样板(断言数逐套件零丢失对照表在报告)+BAD_DIR夹具共享+smoke_all五件(:953公有面clamped_pool/_battle_st七袋/check_silent并入quiet参/_pct转发删/load改preload);探针小件=main句柄Node类型注/魔法等待常量化/画布常数同源锚注/死参verbose删;红线守住=ui_input case④b与click循环体零触碰/拍板点三件未退役

This commit is contained in:
DKChan committed 2026-10-02 08:46:43 +08:00
1 parent 0691a188fd
commit 315e46038c
32 files changed
+587 -764

No files matched your search

@@ -16,15 +16,15 @@ const DamageControllerScript := preload("res://core/damage_controller.gd")
const TEST_SEED := 20260928 # 夹具种子(基建值非玩法数值——回放确定性契约的输入)
var _failures := 0
var _checks := 0
const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:_check/汇总退出样板单点
var _rows: Array = [] # 当前 run 的行协议收集(_on_row 绑定法——避 lambda 多行控制流坑)
func _init() -> void:
var db = ConfigDBScript.new()
var load_ok: bool = db.load_tables("res://data/tables", true)
_check("前置:表加载零 error", load_ok and db.diagnostics_count("error") == 0)
Kit.check("前置:表加载零 error", load_ok and db.diagnostics_count("error") == 0)
if load_ok:
_test_five_recipes_and_replay(db)
_test_policy_immediate_mode(db)
@@ -33,21 +33,7 @@ func _init() -> void:
_test_hero_death_halt(db)
_test_ac_merge(db)
_test_pool_clamp(db)
if _failures > 0:
push_error("[damage_controller_test] %d/%d 项失败" % [_failures, _checks])
quit(1)
return
print("[damage_controller_test] all green (%d checks)" % _checks)
quit(0)
func _check(desc: String, ok: bool) -> void:
_checks += 1
if ok:
print(" [PASS] " + desc)
else:
_failures += 1
push_error(" [FAIL] " + desc)
quit(Kit.finish("damage_controller_test"))
# ---- 夹具 ----
@@ -84,7 +70,7 @@ func _run_recipe_sequence(db) -> Array:
var units = h.units
var ctrl = h.ctrl
if not h.spawn_ok:
_check("前置:units.spawn(1,0) 成功(遭遇行在案)", false)
Kit.check("前置:units.spawn(1,0) 成功(遭遇行在案)", false)
return []
var hero_before: int = int(units.hero_unit().hp)
var enemy_hp_before: Array = []
@@ -122,27 +108,27 @@ func _run_recipe_sequence(db) -> Array:
for id in [t1, t2, t3, t4, t5]:
if ctrl.outcome(int(id)) != "redeemed":
outcomes_redeemed = false
_check("五配方单据 flush 后全部 redeemed(B 模式段界结账)", outcomes_redeemed)
Kit.check("五配方单据 flush 后全部 redeemed(B 模式段界结账)", outcomes_redeemed)
var hero_after: int = int(units.hero_unit().hp)
var r2: Dictionary = _row_by_display_prefix("T2敌方技")
var r5: Dictionary = _row_by_display_prefix("T5反噬")
_check("主角扣血 = 敌方技行值 + 反噬行值(逐行归因闭合)",
Kit.check("主角扣血 = 敌方技行值 + 反噬行值(逐行归因闭合)",
hero_before - hero_after == int(r2.get("value", -1)) + int(r5.get("value", -1)))
_check("敌方技行值 ≥ 下限 + rider(rider 后置不进乘区)", int(r2.get("value", 0)) >= floor_v + floor_v)
_check("反噬行值 = 落地下限(中性无类型表值)", int(r5.get("value", 0)) == floor_v)
Kit.check("敌方技行值 ≥ 下限 + rider(rider 后置不进乘区)", int(r2.get("value", 0)) >= floor_v + floor_v)
Kit.check("反噬行值 = 落地下限(中性无类型表值)", int(r5.get("value", 0)) == floor_v)
var r1: Dictionary = _row_by_display_prefix("T1主角攻击")
var ex1: Dictionary = r1.get("extra", {})
_check("主角攻击行 AC = 防守方 AC + 攻击加成(开单前快照一致)", int(ex1.get("ac", -1)) == threshold_expected)
_check("掷点 ∈ [1, 骰面]", int(ex1.get("roll", -1)) >= 1 and int(ex1.get("roll", -1)) <= int(h.db.settlement.roll_faces))
Kit.check("主角攻击行 AC = 防守方 AC + 攻击加成(开单前快照一致)", int(ex1.get("ac", -1)) == threshold_expected)
Kit.check("掷点 ∈ [1, 骰面]", int(ex1.get("roll", -1)) >= 1 and int(ex1.get("roll", -1)) <= int(h.db.settlement.roll_faces))
var tier_ok := false
for dv in h.db.settlement.roll_tier_discounts:
if int(dv) == int(ex1.get("tier", -1)):
tier_ok = true
if int(ex1.get("tier", -1)) == 100:
tier_ok = true
_check("掷点档位 ∈ 表档位值 ∪ 满额", tier_ok)
Kit.check("掷点档位 ∈ 表档位值 ∪ 满额", tier_ok)
var r4: Dictionary = _row_by_display_prefix("T4群伤")
var alive_at_blast: int = int(r4.get("extra", {}).get("count", -1))
@@ -158,10 +144,10 @@ func _run_recipe_sequence(db) -> Array:
var attack_landed: int = int(r1.get("value", 0))
var typed_landed: int = int(_row_by_display_prefix("T3克制直伤").get("value", 0))
if any_enemy_dead:
_check("敌方总扣血 ≥ 攻击+克制+群伤(死亡溢出合法,下界归因)",
Kit.check("敌方总扣血 ≥ 攻击+克制+群伤(死亡溢出合法,下界归因)",
enemy_drop_total >= attack_landed + typed_landed + int(r4.get("value", 0)) * maxi(0, alive_at_blast))
else:
_check("敌方总扣血 = 攻击 + 克制直伤 + 群伤×存活数(逐行归因闭合)",
Kit.check("敌方总扣血 = 攻击 + 克制直伤 + 群伤×存活数(逐行归因闭合)",
enemy_drop_total == attack_landed + typed_landed + int(r4.get("value", 0)) * alive_at_blast)
var protocol_ok := true
@@ -174,7 +160,7 @@ func _run_recipe_sequence(db) -> Array:
protocol_ok = false
if not (r.extra is Dictionary) or not r.extra.has("target"):
protocol_ok = false
_check("行协议公共字段封闭(type/source_class/display/value≥下限/extra.target)", protocol_ok)
Kit.check("行协议公共字段封闭(type/source_class/display/value≥下限/extra.target)", protocol_ok)
# 复现序列(供双跑对拍):逐行 (display, value)
var seq: Array = []
@@ -185,9 +171,9 @@ func _run_recipe_sequence(db) -> Array:
func _test_five_recipes_and_replay(db) -> void:
var run1: Array = _run_recipe_sequence(db)
_check("五配方序列产出 5 行伤害行", run1.size() == 5)
Kit.check("五配方序列产出 5 行伤害行", run1.size() == 5)
var run2: Array = _run_recipe_sequence(db)
_check("同种子双跑行序列逐位一致(回放确定性契约)", run1 == run2)
Kit.check("同种子双跑行序列逐位一致(回放确定性契约)", run1 == run2)
# ---- A 模式(开单即结——回切位) ----
@@ -201,8 +187,8 @@ func _test_policy_immediate_mode(db) -> void:
var floor_v: int = int(h.db.settlement.damage_floor)
var t: int = h.ctrl.submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit",
"A模式反噬", "棋盘兑付", floor_v, DamageControllerScript.TARGET_HERO, 1))
_check("A 模式开单即结(无 flush 即 redeemed)", h.ctrl.outcome(t) == "redeemed")
_check("A 模式主角即时扣血", int(h.units.hero_unit().hp) == int(h.db.hero.max_hp) - floor_v)
Kit.check("A 模式开单即结(无 flush 即 redeemed)", h.ctrl.outcome(t) == "redeemed")
Kit.check("A 模式主角即时扣血", int(h.units.hero_unit().hp) == int(h.db.hero.max_hp) - floor_v)
# ---- 来源敌死亡:队列中未结单据作废(死亡即不动手) ----
@@ -221,8 +207,8 @@ func _test_source_death_void(db) -> void:
var t: int = h.ctrl.submit(ticket)
h.units.apply_damage(src, int(src.max_hp)) # 敌当场倒下 → unit_died → 该敌未结单作废
h.ctrl.flush()
_check("来源敌死亡后其未结单据 voided(死亡即不动手)", h.ctrl.outcome(t) == "voided")
_check("作废单零行零扣血", int(h.units.hero_unit().hp) == hero_before and _rows.is_empty())
Kit.check("来源敌死亡后其未结单据 voided(死亡即不动手)", h.ctrl.outcome(t) == "voided")
Kit.check("作废单零行零扣血", int(h.units.hero_unit().hp) == hero_before and _rows.is_empty())
# ---- requires 前置门:前置单未实际有效兑付 → 后继单静默作废;无存活敌 blast = 合法零兑付 ----
@@ -238,7 +224,7 @@ func _test_requires_gate_and_zero(db) -> void:
var big: int = front_hp * 10
var t_clear: int = h.ctrl.flush_submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_BLAST, "clear_reward",
"清场群伤", "棋盘兑付", big, DamageControllerScript.TARGET_ALL_ALIVE, 1))
_check("flush_submit 同步开单即结(清图批契约)", h.ctrl.outcome(t_clear) == "redeemed" and h.units.query_alive("enemy").is_empty())
Kit.check("flush_submit 同步开单即结(清图批契约)", h.ctrl.outcome(t_clear) == "redeemed" and h.units.query_alive("enemy").is_empty())
var t_zero: int = h.ctrl.submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_BLAST, "clear_reward",
"空场群伤", "棋盘兑付", floor_v, DamageControllerScript.TARGET_ALL_ALIVE, 1))
var gated_ticket: Dictionary = h.ctrl.open_ticket(DamageControllerScript.KIND_HERO_ATTACK, "hero_attack",
@@ -246,8 +232,8 @@ func _test_requires_gate_and_zero(db) -> void:
gated_ticket["requires"] = t_zero
var t_gated: int = h.ctrl.submit(gated_ticket)
h.ctrl.flush()
_check("无存活敌 blast = 合法零兑付(zero 非 voided)", h.ctrl.outcome(t_zero) == "zero")
_check("前置单未实际有效 → 后继单 voided(requires 门)", h.ctrl.outcome(t_gated) == "voided")
Kit.check("无存活敌 blast = 合法零兑付(zero 非 voided)", h.ctrl.outcome(t_zero) == "zero")
Kit.check("前置单未实际有效 → 后继单 voided(requires 门)", h.ctrl.outcome(t_gated) == "voided")
# ---- 主角倒下:flush 余单全部作废(_halted 终止) ----
@@ -263,8 +249,8 @@ func _test_hero_death_halt(db) -> void:
var t_after: int = h.ctrl.submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit",
"余单", "棋盘兑付", 1, DamageControllerScript.TARGET_HERO, 2))
h.ctrl.flush()
_check("致死单 redeemed 且主角 hp 归零", h.ctrl.outcome(t_kill) == "redeemed" and int(h.units.hero_unit().hp) == 0)
_check("主角倒下后余单 voided(flush 终止)", h.ctrl.outcome(t_after) == "voided")
Kit.check("致死单 redeemed 且主角 hp 归零", h.ctrl.outcome(t_kill) == "redeemed" and int(h.units.hero_unit().hp) == 0)
Kit.check("主角倒下后余单 voided(flush 终止)", h.ctrl.outcome(t_after) == "voided")
# ---- AC 合流:override 入参 / 表值兜底 / ac 型状态叠层(不入池) ----
@@ -275,10 +261,10 @@ func _test_ac_merge(db) -> void:
if not h.spawn_ok:
return
var base_ac: int = int(h.db.hero.armor_class)
_check("缺省兜底:defender_ac(hero) = hero 表值", h.ctrl.defender_ac(h.units.hero_unit()) == base_ac)
Kit.check("缺省兜底:defender_ac(hero) = hero 表值", h.ctrl.defender_ac(h.units.hero_unit()) == base_ac)
var override_v: int = base_ac + int(h.db.settlement.damage_floor)
h.ctrl.set_hero_ac_override(override_v)
_check("override 合流入参生效", h.ctrl.defender_ac(h.units.hero_unit()) == override_v)
Kit.check("override 合流入参生效", h.ctrl.defender_ac(h.units.hero_unit()) == override_v)
# ac 型状态效果叠其上(找 hero 侧 unit=ac 行)
var ac_eff := {}
for row in h.db.status_effects.rows:
@@ -286,12 +272,12 @@ func _test_ac_merge(db) -> void:
ac_eff = row
break
if ac_eff.is_empty():
_check("前置:状态表存在 hero 侧 ac 型行", false)
Kit.check("前置:状态表存在 hero 侧 ac 型行", false)
return
var src := {"source_class": "safe_cell", "origin_cell": Vector2i(-1, -1), "round_index": 0}
h.effects.apply_effect(String(ac_eff.effect_id), src)
var expected: int = override_v + int(ac_eff.magnitude)
_check("ac 型效果叠层入 AC(层数×幅度,不入池)", h.ctrl.defender_ac(h.units.hero_unit()) == expected)
Kit.check("ac 型效果叠层入 AC(层数×幅度,不入池)", h.ctrl.defender_ac(h.units.hero_unit()) == expected)
# ---- 池乘区收口:池内加算 → clamp;ac 型不入池 ----
@@ -303,7 +289,7 @@ func _test_pool_clamp(db) -> void:
return
var lo: int = int(h.db.settlement.pool_clamp_lo)
var hi: int = int(h.db.settlement.pool_clamp_hi)
_check("空效果池读 0(表 clamp 域含 0 前提)", h.ctrl.clamped_pool("hero_offense") == 0 and lo <= 0 and hi >= 0)
Kit.check("空效果池读 0(表 clamp 域含 0 前提)", h.ctrl.clamped_pool("hero_offense") == 0 and lo <= 0 and hi >= 0)
# 找 hero_offense 池的 percent 型 hero 侧效果行
var pool_eff := {}
for row in h.db.status_effects.rows:
@@ -313,14 +299,14 @@ func _test_pool_clamp(db) -> void:
pool_eff = row
break
if pool_eff.is_empty():
_check("前置:hero_offense 池存在 percent 型 hero 侧行", false)
Kit.check("前置:hero_offense 池存在 percent 型 hero 侧行", false)
return
var src := {"source_class": "safe_cell", "origin_cell": Vector2i(-1, -1), "round_index": 0}
var first_ok: bool = h.effects.apply_effect(String(pool_eff.effect_id), src)
var expected1: int = clampi(int(pool_eff.magnitude), lo, hi)
_check("池值 = 单实例 L×m 加算后 clamp", first_ok and h.ctrl.clamped_pool("hero_offense") == expected1)
Kit.check("池值 = 单实例 L×m 加算后 clamp", first_ok and h.ctrl.clamped_pool("hero_offense") == expected1)
var second_ok: bool = h.effects.apply_effect(String(pool_eff.effect_id), src)
_check("refresh 政策重复授予受理且层数恒 1(叠层改续时裁决)——池值不变",
Kit.check("refresh 政策重复授予受理且层数恒 1(叠层改续时裁决)——池值不变",
second_ok and h.ctrl.clamped_pool("hero_offense") == expected1)
# ac 型效果不入 percent 池
var ac_eff2 := {}
@@ -331,7 +317,7 @@ func _test_pool_clamp(db) -> void:
if not ac_eff2.is_empty():
var before: int = h.ctrl.clamped_pool("hero_offense")
h.effects.apply_effect(String(ac_eff2.effect_id), src)
_check("ac 型效果不入池(percent 池隔离)", h.ctrl.clamped_pool("hero_offense") == before)
Kit.check("ac 型效果不入池(percent 池隔离)", h.ctrl.clamped_pool("hero_offense") == before)
# stack 政策效果:全新容器授予至满层 → 再授拒绝(满层拒绝原子——容器零写入)
var stack_eff := {}
for row3 in h.db.status_effects.rows:
@@ -339,7 +325,7 @@ func _test_pool_clamp(db) -> void:
stack_eff = row3
break
if stack_eff.is_empty():
_check("前置:状态表存在 stack 政策行", false)
Kit.check("前置:状态表存在 stack 政策行", false)
return
var effects2 = StatusContainerScript.new(h.db)
effects2.notify_battle_start()
@@ -348,4 +334,4 @@ func _test_pool_clamp(db) -> void:
for i in range(max_n + 1):
if effects2.apply_effect(String(stack_eff.effect_id), src):
granted += 1
_check("stack 政策恰授予 max_stack 层后拒绝(满层拒绝原子)", granted == max_n)
Kit.check("stack 政策恰授予 max_stack 层后拒绝(满层拒绝原子)", granted == max_n)