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:
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)
|
||||
@@ -6,36 +6,21 @@ extends SceneTree
|
||||
|
||||
const DamageMathScript := preload("res://core/damage_math.gd")
|
||||
const ConfigDBScript := preload("res://core/config_db.gd")
|
||||
const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:_check/汇总退出样板单点
|
||||
|
||||
var _failures := 0
|
||||
var _checks := 0
|
||||
var db
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
db = ConfigDBScript.new()
|
||||
var load_ok: bool = db.load_tables("res://data/tables", true)
|
||||
_check("前置:settlement 表加载零 error", load_ok and db.diagnostics_count("error") == 0)
|
||||
Kit.check("前置:settlement 表加载零 error", load_ok and db.diagnostics_count("error") == 0)
|
||||
if load_ok:
|
||||
_test_pool_damage()
|
||||
_test_counter_component()
|
||||
_test_roll_discount_percent()
|
||||
_test_rolled_component()
|
||||
if _failures > 0:
|
||||
push_error("[damage_math_test] %d/%d 项失败" % [_failures, _checks])
|
||||
quit(1)
|
||||
return
|
||||
print("[damage_math_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_math_test"))
|
||||
|
||||
|
||||
func _floor_val() -> int:
|
||||
@@ -46,23 +31,23 @@ func _floor_val() -> int:
|
||||
|
||||
func _test_pool_damage() -> void:
|
||||
var d := _floor_val() + 9 # 表值推导的任意正探针量(非玩法行值)
|
||||
_check("中性双池 (0,0) 原样透传", DamageMathScript.pool_damage(d, 0, 0) == d)
|
||||
_check("负敌方池压低不增高", DamageMathScript.pool_damage(d, 0, int(db.settlement.pool_clamp_lo)) <= d)
|
||||
_check("正主角池抬升不低于原值", DamageMathScript.pool_damage(d, int(db.settlement.pool_clamp_hi), 0) >= d)
|
||||
_check("主角池单调不减(+50% vs +49% 探针档)", DamageMathScript.pool_damage(d, 50, 0) >= DamageMathScript.pool_damage(d, 49, 0))
|
||||
_check("对称性:池值互换结果不变(乘算交换)", DamageMathScript.pool_damage(d, 30, -20) == DamageMathScript.pool_damage(d, -20, 30))
|
||||
_check("输出非负(maxi(0,·) 封底)", DamageMathScript.pool_damage(d, int(db.settlement.pool_clamp_lo), int(db.settlement.pool_clamp_lo)) >= 0)
|
||||
Kit.check("中性双池 (0,0) 原样透传", DamageMathScript.pool_damage(d, 0, 0) == d)
|
||||
Kit.check("负敌方池压低不增高", DamageMathScript.pool_damage(d, 0, int(db.settlement.pool_clamp_lo)) <= d)
|
||||
Kit.check("正主角池抬升不低于原值", DamageMathScript.pool_damage(d, int(db.settlement.pool_clamp_hi), 0) >= d)
|
||||
Kit.check("主角池单调不减(+50% vs +49% 探针档)", DamageMathScript.pool_damage(d, 50, 0) >= DamageMathScript.pool_damage(d, 49, 0))
|
||||
Kit.check("对称性:池值互换结果不变(乘算交换)", DamageMathScript.pool_damage(d, 30, -20) == DamageMathScript.pool_damage(d, -20, 30))
|
||||
Kit.check("输出非负(maxi(0,·) 封底)", DamageMathScript.pool_damage(d, int(db.settlement.pool_clamp_lo), int(db.settlement.pool_clamp_lo)) >= 0)
|
||||
|
||||
|
||||
# ---- 克制分量:空桶不吃修正恒 0;落地下限保底([1,+∞) 全来源)----
|
||||
|
||||
func _test_counter_component() -> void:
|
||||
var floor_v := _floor_val()
|
||||
_check("空桶(点数≤0)恒 0 不吃修正", DamageMathScript.counter_component(0, floor_v, floor_v) == 0
|
||||
Kit.check("空桶(点数≤0)恒 0 不吃修正", DamageMathScript.counter_component(0, floor_v, floor_v) == 0
|
||||
and DamageMathScript.counter_component(-1, 5, floor_v) == 0)
|
||||
_check("正向修正直加", DamageMathScript.counter_component(floor_v, floor_v, floor_v) == floor_v * 2)
|
||||
_check("负修正触底保落地伤害下限", DamageMathScript.counter_component(floor_v, -floor_v * 10, floor_v) == floor_v)
|
||||
_check("下限参数缺省 = 0(无下限语义面)", DamageMathScript.counter_component(floor_v, -floor_v * 10) == 0)
|
||||
Kit.check("正向修正直加", DamageMathScript.counter_component(floor_v, floor_v, floor_v) == floor_v * 2)
|
||||
Kit.check("负修正触底保落地伤害下限", DamageMathScript.counter_component(floor_v, -floor_v * 10, floor_v) == floor_v)
|
||||
Kit.check("下限参数缺省 = 0(无下限语义面)", DamageMathScript.counter_component(floor_v, -floor_v * 10) == 0)
|
||||
|
||||
|
||||
# ---- 掷点折扣查档(纯函数):满阈值恒全额;档位边界/末档语义与表数组一致 ----
|
||||
@@ -71,22 +56,22 @@ func _test_roll_discount_percent() -> void:
|
||||
var threshold := 10 # 查档探针阈值(夹具值——档位参数本身全部来自表)
|
||||
var bounds: Array = db.settlement.roll_tier_bounds
|
||||
var discounts: Array = db.settlement.roll_tier_discounts
|
||||
_check("前置:档位数 = 边界数 + 1(查档契约——档数 = 边界数 + 1)", discounts.size() == bounds.size() + 1 and discounts.size() > 0)
|
||||
_check("掷点 ≥ 阈值恒全额 100", DamageMathScript.roll_discount_percent(threshold, threshold, bounds, discounts) == 100
|
||||
Kit.check("前置:档位数 = 边界数 + 1(查档契约——档数 = 边界数 + 1)", discounts.size() == bounds.size() + 1 and discounts.size() > 0)
|
||||
Kit.check("掷点 ≥ 阈值恒全额 100", DamageMathScript.roll_discount_percent(threshold, threshold, bounds, discounts) == 100
|
||||
and DamageMathScript.roll_discount_percent(threshold + 5, threshold, bounds, discounts) == 100)
|
||||
# 缺口 = 1:落首档(缺口 ∈ [1, bounds[0]] → discounts[0])
|
||||
var tier0_expected: int = int(discounts[0])
|
||||
if int(bounds[0]) >= 1:
|
||||
_check("缺口 1 落首档", DamageMathScript.roll_discount_percent(threshold - 1, threshold, bounds, discounts) == tier0_expected)
|
||||
Kit.check("缺口 1 落首档", DamageMathScript.roll_discount_percent(threshold - 1, threshold, bounds, discounts) == tier0_expected)
|
||||
# 缺口超末边界:落末档
|
||||
var last_expected: int = int(discounts[-1])
|
||||
_check("缺口超末边界落末档", DamageMathScript.roll_discount_percent(threshold - int(bounds[-1]) - 1, threshold, bounds, discounts) == last_expected)
|
||||
Kit.check("缺口超末边界落末档", DamageMathScript.roll_discount_percent(threshold - int(bounds[-1]) - 1, threshold, bounds, discounts) == last_expected)
|
||||
# 档位值降序(重档折扣更深)——表自检
|
||||
var descending := true
|
||||
for i in range(discounts.size() - 1):
|
||||
if int(discounts[i]) < int(discounts[i + 1]):
|
||||
descending = false
|
||||
_check("档位值随档位加深不升(折扣单调)", descending)
|
||||
Kit.check("档位值随档位加深不升(折扣单调)", descending)
|
||||
|
||||
|
||||
# ---- 吃掷点分量算术:折扣→(暴击)→floor→+克制修正(单一取整链)----
|
||||
@@ -94,9 +79,9 @@ func _test_roll_discount_percent() -> void:
|
||||
func _test_rolled_component() -> void:
|
||||
var base := float(_floor_val() + 7) # 表值推导探针基线
|
||||
var crit_mult: int = int(db.settlement.crit_multiplier)
|
||||
_check("满额折扣无暴击 = floor(基线) + 修正 0", DamageMathScript.rolled_component(base, 100, false, crit_mult, 0) == int(floor(base)))
|
||||
_check("满额折扣暴击按表倍率放大", DamageMathScript.rolled_component(base, 100, true, crit_mult, 0) == int(floor(base * float(crit_mult) / 100.0)))
|
||||
_check("折扣降低不高于满额", DamageMathScript.rolled_component(base, 50, false, crit_mult, 0) <= DamageMathScript.rolled_component(base, 100, false, crit_mult, 0))
|
||||
_check("克制修正加在 floor 之后(加法核)", DamageMathScript.rolled_component(base, 100, false, crit_mult, _floor_val()) == int(floor(base)) + _floor_val())
|
||||
_check("复合序 = 基线×折扣%×暴击%(floor 只在链末)",
|
||||
Kit.check("满额折扣无暴击 = floor(基线) + 修正 0", DamageMathScript.rolled_component(base, 100, false, crit_mult, 0) == int(floor(base)))
|
||||
Kit.check("满额折扣暴击按表倍率放大", DamageMathScript.rolled_component(base, 100, true, crit_mult, 0) == int(floor(base * float(crit_mult) / 100.0)))
|
||||
Kit.check("折扣降低不高于满额", DamageMathScript.rolled_component(base, 50, false, crit_mult, 0) <= DamageMathScript.rolled_component(base, 100, false, crit_mult, 0))
|
||||
Kit.check("克制修正加在 floor 之后(加法核)", DamageMathScript.rolled_component(base, 100, false, crit_mult, _floor_val()) == int(floor(base)) + _floor_val())
|
||||
Kit.check("复合序 = 基线×折扣%×暴击%(floor 只在链末)",
|
||||
DamageMathScript.rolled_component(base, 50, true, crit_mult, 0) == int(floor(base * 50.0 / 100.0 * float(crit_mult) / 100.0)))
|
||||
@@ -12,35 +12,19 @@ const ConfigDBScript := preload("res://core/config_db.gd")
|
||||
const RngServiceScript := preload("res://core/rng_service.gd")
|
||||
const StatusContainerScript := preload("res://core/status_container.gd")
|
||||
const DamageControllerScript := preload("res://core/damage_controller.gd")
|
||||
|
||||
var _failures := 0
|
||||
var _checks := 0
|
||||
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)
|
||||
_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_damage_controller_vocabularies()
|
||||
_test_rng_tag_closure(db)
|
||||
_test_ticket_nine_columns()
|
||||
_test_status_boolean_acceptance(db)
|
||||
if _failures > 0:
|
||||
push_error("[system_contracts_test] %d/%d 项失败" % [_failures, _checks])
|
||||
quit(1)
|
||||
return
|
||||
print("[system_contracts_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("system_contracts_test"))
|
||||
|
||||
|
||||
# ---- B1 兑付配方词表:五配方/三目标规则/双结账政策——常量非空且互异(封闭枚举负空间) ----
|
||||
@@ -55,11 +39,11 @@ func _test_damage_controller_vocabularies() -> void:
|
||||
for k in kinds:
|
||||
if String(k).is_empty():
|
||||
nonempty = false
|
||||
_check("五配方常量非空(开单面唯一分派键)", nonempty)
|
||||
Kit.check("五配方常量非空(开单面唯一分派键)", nonempty)
|
||||
var unique_kinds: Dictionary = {}
|
||||
for k in kinds:
|
||||
unique_kinds[String(k)] = true
|
||||
_check("五配方常量互异(词表封闭无别名)", unique_kinds.size() == kinds.size())
|
||||
Kit.check("五配方常量互异(词表封闭无别名)", unique_kinds.size() == kinds.size())
|
||||
var targets: Array = [
|
||||
DamageControllerScript.TARGET_FRONT_ALIVE, DamageControllerScript.TARGET_HERO,
|
||||
DamageControllerScript.TARGET_ALL_ALIVE,
|
||||
@@ -67,8 +51,8 @@ func _test_damage_controller_vocabularies() -> void:
|
||||
var unique_targets: Dictionary = {}
|
||||
for t in targets:
|
||||
unique_targets[String(t)] = true
|
||||
_check("三目标规则常量互异(结账时刻现场解析词表)", unique_targets.size() == targets.size())
|
||||
_check("双结账政策常量互异(A 即结 / B 攒批回切位)",
|
||||
Kit.check("三目标规则常量互异(结账时刻现场解析词表)", unique_targets.size() == targets.size())
|
||||
Kit.check("双结账政策常量互异(A 即结 / B 攒批回切位)",
|
||||
String(DamageControllerScript.POLICY_IMMEDIATE) != String(DamageControllerScript.POLICY_SEGMENT))
|
||||
|
||||
|
||||
@@ -76,9 +60,9 @@ func _test_damage_controller_vocabularies() -> void:
|
||||
|
||||
func _test_rng_tag_closure(db) -> void:
|
||||
var stream_label: String = String(db.settlement.armor_roll_stream)
|
||||
_check("settlement.armor_roll_stream ∈ RngService 封闭标签词表(EC12)",
|
||||
Kit.check("settlement.armor_roll_stream ∈ RngService 封闭标签词表(EC12)",
|
||||
RngServiceScript.PURPOSE_TAGS.has(stream_label))
|
||||
_check("RngService 用途标签词表非空且互异(封闭语义由流改标签)",
|
||||
Kit.check("RngService 用途标签词表非空且互异(封闭语义由流改标签)",
|
||||
RngServiceScript.PURPOSE_TAGS.size() > 0)
|
||||
|
||||
|
||||
@@ -95,9 +79,9 @@ func _test_ticket_nine_columns() -> void:
|
||||
for c in columns:
|
||||
if not t.has(c):
|
||||
all_present = false
|
||||
_check("单据九栏 + 可选两栏缺省齐备(开单面只写差异栏前提)", all_present)
|
||||
_check("缺省无类型轴(attack_type 空 = 恒不查克制矩阵)", String(t.attack_type) == "")
|
||||
_check("缺省无前置依赖(requires = -1)", int(t.requires) == -1)
|
||||
Kit.check("单据九栏 + 可选两栏缺省齐备(开单面只写差异栏前提)", all_present)
|
||||
Kit.check("缺省无类型轴(attack_type 空 = 恒不查克制矩阵)", String(t.attack_type) == "")
|
||||
Kit.check("缺省无前置依赖(requires = -1)", int(t.requires) == -1)
|
||||
|
||||
|
||||
func _bare_controller():
|
||||
@@ -129,18 +113,18 @@ func _test_status_boolean_acceptance(db) -> void:
|
||||
var any_row: Dictionary = {}
|
||||
if db.status_effects.rows.size() > 0:
|
||||
any_row = db.status_effects.rows[0]
|
||||
_check("前置:状态表非空", not any_row.is_empty())
|
||||
Kit.check("前置:状态表非空", not any_row.is_empty())
|
||||
if any_row.is_empty():
|
||||
return
|
||||
var probe_id: String = String(any_row.effect_id)
|
||||
var src := {"source_class": "safe_cell", "origin_cell": Vector2i(-1, -1), "round_index": 0}
|
||||
_check("域闭授予拒绝(EC1——战斗域外 apply_effect = false)", effects.apply_effect(probe_id, src) == false)
|
||||
_check("未知 effect_id 拒绝(EC2)", effects.apply_effect("no_such_effect_id__", src) == false)
|
||||
Kit.check("域闭授予拒绝(EC1——战斗域外 apply_effect = false)", effects.apply_effect(probe_id, src) == false)
|
||||
Kit.check("未知 effect_id 拒绝(EC2)", effects.apply_effect("no_such_effect_id__", src) == false)
|
||||
effects.notify_battle_start()
|
||||
_check("开域后受理已知 id(布尔受理口径 true 通道)", effects.apply_effect(probe_id, src) == true)
|
||||
Kit.check("开域后受理已知 id(布尔受理口径 true 通道)", effects.apply_effect(probe_id, src) == true)
|
||||
var active: Array = effects.query_active(String(any_row.target_side))
|
||||
var projected := false
|
||||
for a in active:
|
||||
if String(a.effect_id) == probe_id:
|
||||
projected = int(a.effective_value) == int(a.layers) * int(a.snap_magnitude)
|
||||
_check("受理后投影 effective_value = 层数×授予时快照(抗热重载 EC9 面)", projected)
|
||||
Kit.check("受理后投影 effective_value = 层数×授予时快照(抗热重载 EC9 面)", projected)
|
||||
@@ -6,13 +6,7 @@
|
||||
extends SceneTree
|
||||
|
||||
const AuxItemsScript := preload("res://core/aux_items.gd")
|
||||
|
||||
var _problems: Array = []
|
||||
|
||||
|
||||
func check(cond: bool, msg: String) -> void:
|
||||
if not cond:
|
||||
_problems.append(msg)
|
||||
const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:check/汇总退出样板单点(静默收集通道)
|
||||
|
||||
|
||||
class StubTable:
|
||||
@@ -59,39 +53,39 @@ func _init() -> void:
|
||||
var inv = _new_inv()
|
||||
var r1: Dictionary = inv.add_item("sapper_order", "safe_cell", Vector2i(3, 4), 2)
|
||||
var r2: Dictionary = inv.add_item("nest_pendulum", "flag_hit", Vector2i(5, 6), 3)
|
||||
check(int(r1.handle) == 1 and int(r2.handle) == 2, "句柄受理序单调(1→2——零随机)")
|
||||
check(String(r1.source_class) == "safe_cell" and r1.origin_cell == Vector2i(3, 4) and int(r1.round_index) == 2,
|
||||
Kit.check_quiet(int(r1.handle) == 1 and int(r2.handle) == 2, "句柄受理序单调(1→2——零随机)")
|
||||
Kit.check_quiet(String(r1.source_class) == "safe_cell" and r1.origin_cell == Vector2i(3, 4) and int(r1.round_index) == 2,
|
||||
"授予来源可归因(source/cell/round)")
|
||||
var inv_proj: Array = inv.query_inventory(Callable())
|
||||
check(inv_proj.size() == 2 and not bool(inv_proj[0].used), "库存投影两件未用")
|
||||
check(bool(inv.consume(int(r1.handle)).get("used", false)), "consume 返回已用快照回执")
|
||||
check(inv.consume(int(r1.handle)).is_empty(), "Used 不可逆——二次 consume 拒绝(空回执)")
|
||||
check(int(inv.inventory_count()) == 2, "Used 保留只读(库存计数不变)")
|
||||
Kit.check_quiet(inv_proj.size() == 2 and not bool(inv_proj[0].used), "库存投影两件未用")
|
||||
Kit.check_quiet(bool(inv.consume(int(r1.handle)).get("used", false)), "consume 返回已用快照回执")
|
||||
Kit.check_quiet(inv.consume(int(r1.handle)).is_empty(), "Used 不可逆——二次 consume 拒绝(空回执)")
|
||||
Kit.check_quiet(int(inv.inventory_count()) == 2, "Used 保留只读(库存计数不变)")
|
||||
|
||||
# ② 真视恒可用+边缘窗口∩板(3×3 中心贴角——角 4 格)
|
||||
var v_ok: Dictionary = inv.validate_use(int(r2.handle), Vector2i(0, 0), Vector2i(10, 10), Callable())
|
||||
check(bool(v_ok.ok) and v_ok.resolved.size() == 4,
|
||||
Kit.check_quiet(bool(v_ok.ok) and v_ok.resolved.size() == 4,
|
||||
"真视边缘窗口=窗口∩板(角 4 格——护栏只缩不扩)")
|
||||
# 预读点击级零信息:中心板内但回调恒 false(全已开)→拒绝零消耗
|
||||
var inv2 = _new_inv()
|
||||
var rp: Dictionary = inv2.add_item("loot_compass", "safe_cell", Vector2i(1, 1), 1)
|
||||
var v_zero: Dictionary = inv2.validate_use(int(rp.handle), Vector2i(5, 5), Vector2i(10, 10), Callable())
|
||||
check(not bool(v_zero.ok) and int(inv2.inventory_count()) == 1,
|
||||
Kit.check_quiet(not bool(v_zero.ok) and int(inv2.inventory_count()) == 1,
|
||||
"预读点击级零信息拒绝(不消耗——库存不变)")
|
||||
# 目标越板
|
||||
var v_oob: Dictionary = inv2.validate_use(int(rp.handle), Vector2i(12, 5), Vector2i(10, 10), Callable())
|
||||
check(not bool(v_zero.ok) and not bool(v_oob.ok), "越板目标拒绝")
|
||||
Kit.check_quiet(not bool(v_zero.ok) and not bool(v_oob.ok), "越板目标拒绝")
|
||||
|
||||
# ③ 叠加层+三清(retire 不动库存;reset 全清)
|
||||
inv2.apply_overlay("mine_reveal", v_ok.resolved)
|
||||
inv2.apply_overlay("reward_preview", [Vector2i(5, 5)])
|
||||
var ov: Dictionary = inv2.query_overlays()
|
||||
check(ov.mine_reveal.size() == 4 and ov.reward_preview.size() == 1, "叠加层两集落位(真视 4/预读 1)")
|
||||
Kit.check_quiet(ov.mine_reveal.size() == 4 and ov.reward_preview.size() == 1, "叠加层两集落位(真视 4/预读 1)")
|
||||
inv2.retire_overlays()
|
||||
check(inv2.query_overlays().mine_reveal.is_empty() and int(inv2.inventory_count()) == 1,
|
||||
Kit.check_quiet(inv2.query_overlays().mine_reveal.is_empty() and int(inv2.inventory_count()) == 1,
|
||||
"换图三清:叠加层消亡+库存不动(关卡域)")
|
||||
inv2.reset()
|
||||
check(inv2.inventory_count() == 0 and inv2.probe().used == 0, "关卡末 reset 全清(幂等)")
|
||||
Kit.check_quiet(inv2.inventory_count() == 0 and inv2.probe().used == 0, "关卡末 reset 全清(幂等)")
|
||||
|
||||
# ④ 零随机重放:同操作序列两遍,投影逐位相同
|
||||
var replay_a: Array = []
|
||||
@@ -106,13 +100,7 @@ func _init() -> void:
|
||||
replay_a = [inv3.query_inventory(Callable()), inv3.query_overlays(), inv3.probe()]
|
||||
else:
|
||||
replay_b = [inv3.query_inventory(Callable()), inv3.query_overlays(), inv3.probe()]
|
||||
check(str(replay_a) == str(replay_b), "零随机重放:库存+叠加层+探针投影逐位相同")
|
||||
Kit.check_quiet(str(replay_a) == str(replay_b), "零随机重放:库存+叠加层+探针投影逐位相同")
|
||||
|
||||
# ---- 汇总 ----
|
||||
if _problems.is_empty():
|
||||
print("[aux_items_test] PASS — 授予归因/Used 不可逆/真视恒可用/零信息拒绝/边缘窗口/三清/零随机重放 全过")
|
||||
quit(0)
|
||||
else:
|
||||
for p in _problems:
|
||||
push_error("[aux_items_test] FAIL — " + p)
|
||||
quit(1)
|
||||
quit(Kit.finish("aux_items_test", "PASS — 授予归因/Used 不可逆/真视恒可用/零信息拒绝/边缘窗口/三清/零随机重放 全过"))
|
||||
@@ -8,16 +8,11 @@ extends SceneTree
|
||||
|
||||
const SaveManagerScript := preload("res://core/save_manager.gd")
|
||||
const AdventureRunnerScript := preload("res://core/adventure_runner.gd")
|
||||
const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:check/汇总退出样板单点(静默收集通道)
|
||||
|
||||
var _problems: Array = []
|
||||
var _bp_path := "user://test_breakpoint_resume.json"
|
||||
|
||||
|
||||
func check(cond: bool, msg: String) -> void:
|
||||
if not cond:
|
||||
_problems.append(msg)
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
var db = load("res://core/config_db.gd").new()
|
||||
if not db.load_tables("res://data/tables", false):
|
||||
@@ -32,64 +27,64 @@ func _init() -> void:
|
||||
var adr_a = AdventureRunnerScript.new(db, rng_a)
|
||||
adr_a.set_save_manager(sm_a)
|
||||
var params := {"hero_override": {}, "aux_unlocked": {}}
|
||||
check(adr_a.start_adventure(1234, params), "冒险启动(入口停点)")
|
||||
check(sm_a.has_breakpoint(), "入口停点断点在档")
|
||||
check(int((sm_a.breakpoint_snapshot() as Dictionary).get("level_index", -1)) == 1, "断点关卡序号 = 1")
|
||||
check(adr_a.abandon(), "入口停点放弃受理(非战斗期)")
|
||||
check(not sm_a.has_breakpoint() and sm_a.write_to_disk(), "放弃清断点+写盘(退出=清除断点)")
|
||||
Kit.check_quiet(adr_a.start_adventure(1234, params), "冒险启动(入口停点)")
|
||||
Kit.check_quiet(sm_a.has_breakpoint(), "入口停点断点在档")
|
||||
Kit.check_quiet(int((sm_a.breakpoint_snapshot() as Dictionary).get("level_index", -1)) == 1, "断点关卡序号 = 1")
|
||||
Kit.check_quiet(adr_a.abandon(), "入口停点放弃受理(非战斗期)")
|
||||
Kit.check_quiet(not sm_a.has_breakpoint() and sm_a.write_to_disk(), "放弃清断点+写盘(退出=清除断点)")
|
||||
var sm_reload = SaveManagerScript.new(_bp_path)
|
||||
sm_reload.load_from_disk()
|
||||
check(not sm_reload.has_breakpoint(), "放弃后重读档无断点(「继续上局」隐藏口径)")
|
||||
Kit.check_quiet(not sm_reload.has_breakpoint(), "放弃后重读档无断点(「继续上局」隐藏口径)")
|
||||
|
||||
# ---- Part 2:中断基线——入口停点后选路进战,记板指纹与计数器 ----
|
||||
var rng_b = load("res://core/rng_service.gd").new()
|
||||
var sm_b = SaveManagerScript.new(_bp_path)
|
||||
var adr_b = AdventureRunnerScript.new(db, rng_b)
|
||||
adr_b.set_save_manager(sm_b)
|
||||
check(adr_b.start_adventure(1234, params), "基线冒险启动(同种子 1234)")
|
||||
Kit.check_quiet(adr_b.start_adventure(1234, params), "基线冒险启动(同种子 1234)")
|
||||
var runner_b = adr_b.runner()
|
||||
var c0: int = rng_b.counter()
|
||||
var cand_b: Array = runner_b.next_candidate_ids()
|
||||
var hp0: int = runner_b.hero_hp
|
||||
var bp_disk: Dictionary = sm_b.breakpoint_snapshot()
|
||||
check(int(bp_disk.get("rng_counter", -1)) == c0, "断点计数器 = 当前取数号位(快照契约)")
|
||||
check(runner_b.pick_next_node(int(cand_b[0])), "基线选路进首战")
|
||||
Kit.check_quiet(int(bp_disk.get("rng_counter", -1)) == c0, "断点计数器 = 当前取数号位(快照契约)")
|
||||
Kit.check_quiet(runner_b.pick_next_node(int(cand_b[0])), "基线选路进首战")
|
||||
var battle_b = runner_b.current_battle()
|
||||
check(battle_b != null, "基线首战开打")
|
||||
Kit.check_quiet(battle_b != null, "基线首战开打")
|
||||
var fp_b := _board_fingerprint(battle_b.minesweeper.current_board())
|
||||
var c1: int = rng_b.counter()
|
||||
|
||||
# ---- Part 3:续玩确定性——新进程对象自盘上断点重建,同种子续玩板逐位一致 ----
|
||||
var sm_c = SaveManagerScript.new(_bp_path)
|
||||
check(sm_c.load_from_disk() and sm_c.has_breakpoint(), "断点自盘读回在档")
|
||||
Kit.check_quiet(sm_c.load_from_disk() and sm_c.has_breakpoint(), "断点自盘读回在档")
|
||||
var bp_c: Dictionary = sm_c.breakpoint_snapshot()
|
||||
var rng_c = load("res://core/rng_service.gd").new()
|
||||
var adr_c = AdventureRunnerScript.new(db, rng_c)
|
||||
check(adr_c.resume_adventure(bp_c, params), "续玩受理")
|
||||
Kit.check_quiet(adr_c.resume_adventure(bp_c, params), "续玩受理")
|
||||
var runner_c = adr_c.runner()
|
||||
check(int(adr_c.master_seed) == 1234, "续玩主种子回填")
|
||||
check(int(rng_c.counter()) == c0, "续玩取数号位 = 断点号位(零重耗零跳号)")
|
||||
check(runner_c.hero_hp == hp0, "续玩生命账本回填")
|
||||
Kit.check_quiet(int(adr_c.master_seed) == 1234, "续玩主种子回填")
|
||||
Kit.check_quiet(int(rng_c.counter()) == c0, "续玩取数号位 = 断点号位(零重耗零跳号)")
|
||||
Kit.check_quiet(runner_c.hero_hp == hp0, "续玩生命账本回填")
|
||||
var cand_c: Array = runner_c.next_candidate_ids()
|
||||
check(cand_c.size() == cand_b.size() and int(cand_c[0]) == int(cand_b[0]), "续玩候选一致(停点重建)")
|
||||
check(runner_c.awaiting_choice(), "续玩处选路停点态")
|
||||
check(runner_c.pick_next_node(int(cand_c[0])), "续玩选路进首战")
|
||||
Kit.check_quiet(cand_c.size() == cand_b.size() and int(cand_c[0]) == int(cand_b[0]), "续玩候选一致(停点重建)")
|
||||
Kit.check_quiet(runner_c.awaiting_choice(), "续玩处选路停点态")
|
||||
Kit.check_quiet(runner_c.pick_next_node(int(cand_c[0])), "续玩选路进首战")
|
||||
var battle_c = runner_c.current_battle()
|
||||
var fp_c := _board_fingerprint(battle_c.minesweeper.current_board())
|
||||
check(fp_c == fp_b, "续玩首战棋盘与中断前时间线逐位一致(同种子同板)")
|
||||
check(int(rng_c.counter()) == c1, "续玩开板后号位与基线一致")
|
||||
Kit.check_quiet(fp_c == fp_b, "续玩首战棋盘与中断前时间线逐位一致(同种子同板)")
|
||||
Kit.check_quiet(int(rng_c.counter()) == c1, "续玩开板后号位与基线一致")
|
||||
|
||||
# ---- Part 4:续玩幂等——同一断点二次续玩(新进程对象)结果逐位同 ----
|
||||
var sm_d = SaveManagerScript.new(_bp_path)
|
||||
sm_d.load_from_disk()
|
||||
var rng_d = load("res://core/rng_service.gd").new()
|
||||
var adr_d = AdventureRunnerScript.new(db, rng_d)
|
||||
check(adr_d.resume_adventure(sm_d.breakpoint_snapshot(), params), "二次续玩受理")
|
||||
Kit.check_quiet(adr_d.resume_adventure(sm_d.breakpoint_snapshot(), params), "二次续玩受理")
|
||||
var runner_d = adr_d.runner()
|
||||
check(int(rng_d.counter()) == c0 and runner_d.hero_hp == hp0, "二次续玩状态逐位同(幂等)")
|
||||
check(runner_d.pick_next_node(int(runner_d.next_candidate_ids()[0])), "二次续玩选路")
|
||||
Kit.check_quiet(int(rng_d.counter()) == c0 and runner_d.hero_hp == hp0, "二次续玩状态逐位同(幂等)")
|
||||
Kit.check_quiet(runner_d.pick_next_node(int(runner_d.next_candidate_ids()[0])), "二次续玩选路")
|
||||
var battle_d = runner_d.current_battle()
|
||||
check(_board_fingerprint(battle_d.minesweeper.current_board()) == fp_b, "二次续玩板逐位同(幂等)")
|
||||
Kit.check_quiet(_board_fingerprint(battle_d.minesweeper.current_board()) == fp_b, "二次续玩板逐位同(幂等)")
|
||||
|
||||
# ---- Part 5:三选一停点中断→续玩(迭代 10 奖励重构——候选不重抽、已兑付不重付、次数计数一致)----
|
||||
var rng_e = load("res://core/rng_service.gd").new()
|
||||
@@ -97,50 +92,43 @@ func _init() -> void:
|
||||
var adr_e = AdventureRunnerScript.new(db, rng_e)
|
||||
adr_e.set_save_manager(sm_e)
|
||||
var params_e := {"hero_override": {}, "aux_unlocked": {"blast_kit": true}}
|
||||
check(adr_e.start_adventure(4321, params_e), "P5 冒险启动(seed 4321)")
|
||||
Kit.check_quiet(adr_e.start_adventure(4321, params_e), "P5 冒险启动(seed 4321)")
|
||||
adr_e._bonus_taken["attack"] = 1 # 白盒预置计数(断点附随件往返的判据面——计数不可由加算区反推)
|
||||
adr_e._on_level_finished({"result": "victory", "hp": 30, "node_index": 1, "node_id": 1, "node_type": "shop"})
|
||||
var cands_e: Array = (adr_e._bonus_pending.get("candidates", []) as Array).duplicate()
|
||||
check(not cands_e.is_empty(), "P5 非末关通关三选一停点在案(直调 victory——L1 非末关)")
|
||||
Kit.check_quiet(not cands_e.is_empty(), "P5 非末关通关三选一停点在案(直调 victory——L1 非末关)")
|
||||
var sm_f = SaveManagerScript.new(_bp_path)
|
||||
check(sm_f.load_from_disk() and sm_f.has_breakpoint(), "P5 选择停点断点在档(选择停点可中断契约)")
|
||||
Kit.check_quiet(sm_f.load_from_disk() and sm_f.has_breakpoint(), "P5 选择停点断点在档(选择停点可中断契约)")
|
||||
var bp_f: Dictionary = sm_f.breakpoint_snapshot()
|
||||
var bp_f_cands: Array = (bp_f.get("bonus_pending", {}) as Dictionary).get("candidates", []) as Array
|
||||
check(bp_f_cands == cands_e, "P5 断点附随件含候选(bonus_pending 往返)")
|
||||
check(int((bp_f.get("bonus_taken", {}) as Dictionary).get("attack", -1)) == 1, "P5 断点附随件含次数计数(bonus_taken 往返)")
|
||||
Kit.check_quiet(bp_f_cands == cands_e, "P5 断点附随件含候选(bonus_pending 往返)")
|
||||
Kit.check_quiet(int((bp_f.get("bonus_taken", {}) as Dictionary).get("attack", -1)) == 1, "P5 断点附随件含次数计数(bonus_taken 往返)")
|
||||
var got_cands := {"cands": [] as Array}
|
||||
var adr_f = AdventureRunnerScript.new(db, load("res://core/rng_service.gd").new())
|
||||
adr_f.set_save_manager(sm_f) # 兑付后停点快照落盘承载(与真机组合根同构)
|
||||
adr_f.bonus_choice_presented.connect(func(p): got_cands["cands"] = (p.get("candidates", []) as Array).duplicate())
|
||||
check(adr_f.resume_adventure(bp_f, params_e), "P5 续玩受理(选择态分支)")
|
||||
check(adr_f.runner() == null, "P5 续玩不重跑关卡(三选一停点直回,_start_current_level 未走)")
|
||||
check(got_cands["cands"] == cands_e, "P5 续玩候选不重抽(逐位一致)")
|
||||
check(int(adr_f._bonus_taken["attack"]) == 1, "P5 次数计数恢复一致(牌库剔除态续接)")
|
||||
check(not adr_f.continue_to_next_level(), "P5 未兑付推进拒绝(续玩态零自动推进同契约)")
|
||||
check(adr_f.pick_bonus(String(cands_e[0])), "P5 点选兑付受理")
|
||||
Kit.check_quiet(adr_f.resume_adventure(bp_f, params_e), "P5 续玩受理(选择态分支)")
|
||||
Kit.check_quiet(adr_f.runner() == null, "P5 续玩不重跑关卡(三选一停点直回,_start_current_level 未走)")
|
||||
Kit.check_quiet(got_cands["cands"] == cands_e, "P5 续玩候选不重抽(逐位一致)")
|
||||
Kit.check_quiet(int(adr_f._bonus_taken["attack"]) == 1, "P5 次数计数恢复一致(牌库剔除态续接)")
|
||||
Kit.check_quiet(not adr_f.continue_to_next_level(), "P5 未兑付推进拒绝(续玩态零自动推进同契约)")
|
||||
Kit.check_quiet(adr_f.pick_bonus(String(cands_e[0])), "P5 点选兑付受理")
|
||||
var picked_e: String = String(adr_f._bonus_result.get("picked", ""))
|
||||
if adr_f._bonus_taken.has(picked_e):
|
||||
check(int(adr_f._bonus_taken[picked_e]) == (2 if picked_e == "attack" else 1),
|
||||
Kit.check_quiet(int(adr_f._bonus_taken[picked_e]) == (2 if picked_e == "attack" else 1),
|
||||
"P5 兑付计数一致(%s——预置 1 + 本次兑付)" % picked_e)
|
||||
check(sm_f.has_breakpoint(), "P5 兑付后停点快照在档(已兑付待推进态可中断)")
|
||||
Kit.check_quiet(sm_f.has_breakpoint(), "P5 兑付后停点快照在档(已兑付待推进态可中断)")
|
||||
var bp_g: Dictionary = sm_f.breakpoint_snapshot()
|
||||
var adr_g = AdventureRunnerScript.new(db, load("res://core/rng_service.gd").new())
|
||||
adr_g.set_save_manager(sm_f) # 已兑付态续玩的后续快照同承载
|
||||
var got_result := {"r": {}}
|
||||
adr_g.bonus_resolved.connect(func(r): got_result["r"] = r.duplicate())
|
||||
check(adr_g.resume_adventure(bp_g, params_e), "P5 已兑付态续玩受理")
|
||||
check(String(got_result["r"].get("picked", "")) == picked_e, "P5 已兑付不重付(resolved 重发同件)")
|
||||
check(adr_g.continue_to_next_level(), "P5 已兑付续玩后推进受理(下一关开始)")
|
||||
Kit.check_quiet(adr_g.resume_adventure(bp_g, params_e), "P5 已兑付态续玩受理")
|
||||
Kit.check_quiet(String(got_result["r"].get("picked", "")) == picked_e, "P5 已兑付不重付(resolved 重发同件)")
|
||||
Kit.check_quiet(adr_g.continue_to_next_level(), "P5 已兑付续玩后推进受理(下一关开始)")
|
||||
|
||||
_clear_disk()
|
||||
if _problems.is_empty():
|
||||
print("[breakpoint_resume_test] PASS — 断点往返/续玩确定性/幂等/退出清断点 全绿")
|
||||
quit(0)
|
||||
else:
|
||||
for p in _problems:
|
||||
push_error("[breakpoint_resume_test] FAIL — " + p)
|
||||
print("[breakpoint_resume_test] FAIL — " + p)
|
||||
quit(1)
|
||||
quit(Kit.finish("breakpoint_resume_test", "PASS — 断点往返/续玩确定性/幂等/退出清断点 全绿"))
|
||||
|
||||
|
||||
func _board_fingerprint(b) -> String:
|
||||
|
||||
@@ -7,27 +7,12 @@
|
||||
extends SceneTree
|
||||
|
||||
const ConfigDBScript := preload("res://core/config_db.gd")
|
||||
const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:BAD 表目录夹具共享(copy_tables)
|
||||
|
||||
const SRC_DIR := "res://data/tables"
|
||||
const BAD_DIR := "user://aux_fixture_target_violation"
|
||||
|
||||
|
||||
func _copy_tables() -> bool:
|
||||
DirAccess.make_dir_recursive_absolute(BAD_DIR)
|
||||
var dir := DirAccess.open(SRC_DIR)
|
||||
if dir == null:
|
||||
return false
|
||||
dir.list_dir_begin()
|
||||
var f := dir.get_next()
|
||||
while f != "":
|
||||
if not dir.current_is_dir():
|
||||
var err := dir.copy(SRC_DIR + "/" + f, BAD_DIR + "/" + f)
|
||||
if err != OK:
|
||||
return false
|
||||
f = dir.get_next()
|
||||
return true
|
||||
|
||||
|
||||
func _inject_violation() -> bool:
|
||||
var path := BAD_DIR + "/aux_items.tres"
|
||||
var f := FileAccess.open(path, FileAccess.READ)
|
||||
@@ -58,7 +43,7 @@ func _inject_violation() -> bool:
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
if not _copy_tables() or not _inject_violation():
|
||||
if not Kit.copy_tables(SRC_DIR, BAD_DIR) or not _inject_violation():
|
||||
push_error("[aux_target_violation_test] 夹具目录准备失败")
|
||||
quit(2)
|
||||
return
|
||||
|
||||
@@ -5,16 +5,14 @@
|
||||
extends SceneTree
|
||||
|
||||
const ConfigDBScript := preload("res://core/config_db.gd")
|
||||
|
||||
var _failures := 0
|
||||
var _checks := 0
|
||||
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)
|
||||
_check("全部表加载 + 校验通过(含跨表护栏零 error 诊断)", load_ok and db.diagnostics_count("error") == 0)
|
||||
_check("is_ready 通道随加载置位", db.is_ready)
|
||||
Kit.check("全部表加载 + 校验通过(含跨表护栏零 error 诊断)", load_ok and db.diagnostics_count("error") == 0)
|
||||
Kit.check("is_ready 通道随加载置位", db.is_ready)
|
||||
if not load_ok:
|
||||
for d in db.diagnostics():
|
||||
if d.severity == "error":
|
||||
@@ -25,21 +23,7 @@ func _init() -> void:
|
||||
_test_shop_accessors(db)
|
||||
_test_meta_accessors(db)
|
||||
_test_manifest_dir_sync(db)
|
||||
if _failures > 0:
|
||||
push_error("[config_db_test] %d/%d 项失败" % [_failures, _checks])
|
||||
quit(1)
|
||||
return
|
||||
print("[config_db_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("config_db_test"))
|
||||
|
||||
|
||||
# ---- 进度坐标 ceil 解析(R1:向上下取整保证偏难;越最大行 clamp 取最大值 + 警告去重)----
|
||||
@@ -67,34 +51,34 @@ func _test_progress_resolution(db) -> void:
|
||||
# 精确命中:请求 = 表内现存 (L,D) → 原行返回(字段逐位相等)
|
||||
var probe_row: Dictionary = bg_rows[0]
|
||||
var got: Dictionary = db.resolve_board_gen(int(probe_row.level_index), int(probe_row.node_depth))
|
||||
_check("进度解析精确命中返回原行(board_gen 首行)",
|
||||
Kit.check("进度解析精确命中返回原行(board_gen 首行)",
|
||||
int(got.get("level_index", -99)) == int(probe_row.level_index)
|
||||
and int(got.get("node_depth", -99)) == int(probe_row.node_depth)
|
||||
and String(got.get("board_type", "")) == String(probe_row.get("board_type", "")))
|
||||
|
||||
# enemy 行集同通道:同 (L,D) 精确行存在(关卡节点地图 前置——battle 节点双表精确行契约)
|
||||
var en_got: Dictionary = db.resolve_enemy_rowset(int(probe_row.level_index), int(probe_row.node_depth))
|
||||
_check("enemy 行集同坐标解析非空(双表精确行契约)", not en_got.is_empty())
|
||||
Kit.check("enemy 行集同坐标解析非空(双表精确行契约)", not en_got.is_empty())
|
||||
|
||||
# 深度 clamp:请求深度超最大行 → 取该关最大深度行 + 警告诊断一次(去重)
|
||||
db.reset_diagnostics()
|
||||
var deep: Dictionary = db.resolve_board_gen(min_l, int(min_depths[-1]) + 1)
|
||||
_check("深度越最大行 clamp 取该关最大深度行",
|
||||
Kit.check("深度越最大行 clamp 取该关最大深度行",
|
||||
int(deep.get("level_index", -99)) == min_l and int(deep.get("node_depth", -99)) == int(min_depths[-1]))
|
||||
_check("clamp 解析发警告诊断(resolve_depth_clamp)", db.diagnostics_count("warning") >= 1)
|
||||
Kit.check("clamp 解析发警告诊断(resolve_depth_clamp)", db.diagnostics_count("warning") >= 1)
|
||||
var found_clamp := false
|
||||
for d in db.diagnostics():
|
||||
if d.category == "resolve_depth_clamp":
|
||||
found_clamp = true
|
||||
_check("警告类别 = resolve_depth_clamp", found_clamp)
|
||||
Kit.check("警告类别 = resolve_depth_clamp", found_clamp)
|
||||
var warn_before: int = db.diagnostics_count("warning")
|
||||
db.resolve_board_gen(min_l, int(min_depths[-1]) + 1)
|
||||
_check("同键重复请求警告去重(四元组注册表)", db.diagnostics_count("warning") == warn_before)
|
||||
Kit.check("同键重复请求警告去重(四元组注册表)", db.diagnostics_count("warning") == warn_before)
|
||||
|
||||
# 关卡 clamp:请求关卡超最大行 → 取最大关卡行
|
||||
db.reset_diagnostics()
|
||||
var high: Dictionary = db.resolve_board_gen(max_l + 1, 0)
|
||||
_check("关卡越最大行 clamp 取最大关卡行", int(high.get("level_index", -99)) == max_l)
|
||||
Kit.check("关卡越最大行 clamp 取最大关卡行", int(high.get("level_index", -99)) == max_l)
|
||||
|
||||
|
||||
# ---- 变异池门控(unlock_level 闸 + ∅ board_types 通配)----
|
||||
@@ -114,12 +98,12 @@ func _test_variant_pool_gate(db) -> void:
|
||||
var tag: String = String(gated_row.board_types[0])
|
||||
var before: Array = db.variant_pool(u - 1, tag)
|
||||
var after: Array = db.variant_pool(u, tag)
|
||||
_check("变异池 unlock 前不含未解锁行(level=unlock−1)", not _rows_contain(before, gated_row))
|
||||
_check("变异池 unlock 当级含该行", _rows_contain(after, gated_row))
|
||||
Kit.check("变异池 unlock 前不含未解锁行(level=unlock−1)", not _rows_contain(before, gated_row))
|
||||
Kit.check("变异池 unlock 当级含该行", _rows_contain(after, gated_row))
|
||||
if not wildcard_row.is_empty():
|
||||
var u2: int = int(wildcard_row.unlock_level)
|
||||
var any_pool: Array = db.variant_pool(u2, "nonexistent_board_type_tag")
|
||||
_check("∅ board_types 行通配(任一 board_type 命中)", _rows_contain(any_pool, wildcard_row))
|
||||
Kit.check("∅ board_types 行通配(任一 board_type 命中)", _rows_contain(any_pool, wildcard_row))
|
||||
|
||||
|
||||
func _rows_contain(pool: Array, row: Dictionary) -> bool:
|
||||
@@ -134,26 +118,26 @@ func _rows_contain(pool: Array, row: Dictionary) -> bool:
|
||||
func _test_shop_accessors(db) -> void:
|
||||
var products: Array = db.shop_products()
|
||||
if products.is_empty():
|
||||
_check("ShopTable products 行族非空(表已落籍前提)", false)
|
||||
Kit.check("ShopTable products 行族非空(表已落籍前提)", false)
|
||||
return
|
||||
_check("ShopTable products 行族非空", true)
|
||||
Kit.check("ShopTable products 行族非空", true)
|
||||
var first: Dictionary = products[0]
|
||||
var got: Dictionary = db.shop_product_row(String(first.product_id))
|
||||
_check("shop_product_row 按 product_id 精确命中首行", got == first)
|
||||
_check("shop_product_row 未知 id 返回空字典(容错通道)", db.shop_product_row("no_such_product_id__").is_empty())
|
||||
Kit.check("shop_product_row 按 product_id 精确命中首行", got == first)
|
||||
Kit.check("shop_product_row 未知 id 返回空字典(容错通道)", db.shop_product_row("no_such_product_id__").is_empty())
|
||||
var tier_rows: Array = db._shop_rows("gp_drops")
|
||||
if not tier_rows.is_empty():
|
||||
var tier0: String = String(tier_rows[0].tier)
|
||||
_check("shop_gp_drop_row 按 tier 命中", not db.shop_gp_drop_row(tier0).is_empty())
|
||||
Kit.check("shop_gp_drop_row 按 tier 命中", not db.shop_gp_drop_row(tier0).is_empty())
|
||||
var bonus_rows: Array = db._shop_rows("level_gp_bonus")
|
||||
if not bonus_rows.is_empty():
|
||||
var blv: int = int(bonus_rows[0].level_index)
|
||||
_check("shop_level_gp_bonus 按关卡序号命中行值", db.shop_level_gp_bonus(blv) == int(bonus_rows[0].bonus))
|
||||
_check("shop_level_gp_bonus 未登记关卡返回 0(兜底通道)", db.shop_level_gp_bonus(-1) == 0)
|
||||
Kit.check("shop_level_gp_bonus 按关卡序号命中行值", db.shop_level_gp_bonus(blv) == int(bonus_rows[0].bonus))
|
||||
Kit.check("shop_level_gp_bonus 未登记关卡返回 0(兜底通道)", db.shop_level_gp_bonus(-1) == 0)
|
||||
var price_rows: Array = db._shop_rows("price_level_bonus")
|
||||
if not price_rows.is_empty():
|
||||
var plv: int = int(price_rows[0].level_index)
|
||||
_check("shop_price_level_bonus 按关卡序号命中行值", db.shop_price_level_bonus(plv) == int(price_rows[0].bonus))
|
||||
Kit.check("shop_price_level_bonus 按关卡序号命中行值", db.shop_price_level_bonus(plv) == int(price_rows[0].bonus))
|
||||
|
||||
|
||||
# ---- MetaTable 消费面 ----
|
||||
@@ -161,19 +145,19 @@ func _test_shop_accessors(db) -> void:
|
||||
func _test_meta_accessors(db) -> void:
|
||||
var forge_rows: Array = db._meta_rows("forge_products")
|
||||
if forge_rows.is_empty():
|
||||
_check("MetaTable forge_products 行族非空", false)
|
||||
Kit.check("MetaTable forge_products 行族非空", false)
|
||||
else:
|
||||
var attr0: String = String(forge_rows[0].attr)
|
||||
_check("meta_forge_row 按 attr 命中首行", db.meta_forge_row(attr0) == forge_rows[0])
|
||||
_check("meta_forge_row 未知 attr 返回空字典", db.meta_forge_row("no_such_attr__").is_empty())
|
||||
Kit.check("meta_forge_row 按 attr 命中首行", db.meta_forge_row(attr0) == forge_rows[0])
|
||||
Kit.check("meta_forge_row 未知 attr 返回空字典", db.meta_forge_row("no_such_attr__").is_empty())
|
||||
var key_rows: Array = db._meta_rows("boss_keystone")
|
||||
if not key_rows.is_empty():
|
||||
var klv: int = int(key_rows[0].level_index)
|
||||
_check("meta_boss_keystone_row 按关卡序号命中", db.meta_boss_keystone_row(klv) == key_rows[0])
|
||||
Kit.check("meta_boss_keystone_row 按关卡序号命中", db.meta_boss_keystone_row(klv) == key_rows[0])
|
||||
var conv_rows: Array = db._meta_rows("conversion")
|
||||
if not conv_rows.is_empty():
|
||||
var cattr: String = String(conv_rows[0].attr)
|
||||
_check("meta_conversion_row 按 attr 命中", db.meta_conversion_row(cattr) == conv_rows[0])
|
||||
Kit.check("meta_conversion_row 按 attr 命中", db.meta_conversion_row(cattr) == conv_rows[0])
|
||||
|
||||
|
||||
# ---- TABLE_MANIFEST ↔ data/tables 目录同步(各系统→D2 单点契约的登记面)----
|
||||
@@ -195,4 +179,4 @@ func _test_manifest_dir_sync(db) -> void:
|
||||
for key in manifest.keys():
|
||||
if not disk_files.has(String(manifest[key])):
|
||||
synced = false
|
||||
_check("TABLE_MANIFEST 与 data/tables 目录逐文件同步(无缺登/无悬登)", synced)
|
||||
Kit.check("TABLE_MANIFEST 与 data/tables 目录逐文件同步(无缺登/无悬登)", synced)
|
||||
@@ -7,26 +7,12 @@
|
||||
extends SceneTree
|
||||
|
||||
const ConfigDBScript := preload("res://core/config_db.gd")
|
||||
const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:BAD 表目录夹具共享(copy_tables)
|
||||
|
||||
const SRC_DIR := "res://data/tables"
|
||||
const BAD_DIR := "user://aux_fixture_dangling_aux"
|
||||
|
||||
|
||||
func _copy_tables() -> bool:
|
||||
DirAccess.make_dir_recursive_absolute(BAD_DIR)
|
||||
var dir := DirAccess.open(SRC_DIR)
|
||||
if dir == null:
|
||||
return false
|
||||
dir.list_dir_begin()
|
||||
var f := dir.get_next()
|
||||
while f != "":
|
||||
if not dir.current_is_dir():
|
||||
if dir.copy(SRC_DIR + "/" + f, BAD_DIR + "/" + f) != OK:
|
||||
return false
|
||||
f = dir.get_next()
|
||||
return true
|
||||
|
||||
|
||||
func _inject_violation() -> bool:
|
||||
var path := BAD_DIR + "/reward.tres"
|
||||
var f := FileAccess.open(path, FileAccess.READ)
|
||||
@@ -50,7 +36,7 @@ func _inject_violation() -> bool:
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
if not _copy_tables() or not _inject_violation():
|
||||
if not Kit.copy_tables(SRC_DIR, BAD_DIR) or not _inject_violation():
|
||||
push_error("[reward_dangling_aux_test] 夹具目录准备失败")
|
||||
quit(2)
|
||||
return
|
||||
|
||||
@@ -6,33 +6,17 @@
|
||||
extends SceneTree
|
||||
|
||||
const RngServiceScript := preload("res://core/rng_service.gd")
|
||||
const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:_check/汇总退出样板单点
|
||||
|
||||
const TEST_SEED := 20260928 # 夹具种子(任意固定值;语义 = 可复现性,非玩法数值)
|
||||
|
||||
var _failures := 0
|
||||
var _checks := 0
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_test_derive_determinism()
|
||||
_test_counter_ledger()
|
||||
_test_stream_draw_determinism()
|
||||
_test_negative_seed_normalization()
|
||||
if _failures > 0:
|
||||
push_error("[rng_service_test] %d/%d 项失败" % [_failures, _checks])
|
||||
quit(1)
|
||||
return
|
||||
print("[rng_service_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("rng_service_test"))
|
||||
|
||||
|
||||
func _fresh_service():
|
||||
@@ -49,11 +33,11 @@ func _test_derive_determinism() -> void:
|
||||
for scope in range(8):
|
||||
if a.derive_at(scope) != b.derive_at(scope):
|
||||
same = false
|
||||
_check("同种子两实例 derive_at(0..7) 逐位一致", same)
|
||||
Kit.check("同种子两实例 derive_at(0..7) 逐位一致", same)
|
||||
var a2 = _fresh_service()
|
||||
var before: int = a2.counter()
|
||||
a2.derive_at(0)
|
||||
_check("derive_at 为纯函数不推计数器", a2.counter() == before)
|
||||
Kit.check("derive_at 为纯函数不推计数器", a2.counter() == before)
|
||||
|
||||
|
||||
# 全局取数计数器:唯一账本——open_stream 逐次 +1(scope 单调)
|
||||
@@ -64,13 +48,13 @@ func _test_counter_ledger() -> void:
|
||||
var c1: int = rng.counter()
|
||||
rng.open_stream("armor_roll")
|
||||
var c2: int = rng.counter()
|
||||
_check("open_stream 逐次推进全局计数器(+1/次)", c1 == c0 + 1 and c2 == c0 + 2)
|
||||
Kit.check("open_stream 逐次推进全局计数器(+1/次)", c1 == c0 + 1 and c2 == c0 + 2)
|
||||
# 用途标签只进事件日志不参与派生——同 scope 同派生种子(ADR-0007 裁丙口径)
|
||||
var rng_x = _fresh_service()
|
||||
var rng_y = _fresh_service()
|
||||
var s1: int = int(rng_x.open_stream("board_gen").seed)
|
||||
var s2: int = int(rng_y.open_stream("reward_roll").seed)
|
||||
_check("同 scope 异用途标签派生种子一致(标签不参与派生)", s1 == s2)
|
||||
Kit.check("同 scope 异用途标签派生种子一致(标签不参与派生)", s1 == s2)
|
||||
|
||||
|
||||
# 同种子两实例同标签流的实际取数序列逐位一致(回放确定性契约——存档快照=主种子+计数器的前提)
|
||||
@@ -84,7 +68,7 @@ func _test_stream_draw_determinism() -> void:
|
||||
var rb: int = b.open_stream("armor_roll").randi_range(1, faces)
|
||||
if ra != rb:
|
||||
same = false
|
||||
_check("同种子两实例同标签流取数 12 次逐位一致", same)
|
||||
Kit.check("同种子两实例同标签流取数 12 次逐位一致", same)
|
||||
|
||||
|
||||
# 负输入 abs 归一化(警告通道)——|−s| 与 s 同派生
|
||||
@@ -92,4 +76,4 @@ func _test_negative_seed_normalization() -> void:
|
||||
var pos = _fresh_service()
|
||||
var neg = RngServiceScript.new()
|
||||
neg.set_master_seed(-TEST_SEED)
|
||||
_check("负种子 abs 归一化后派生与正种子一致", pos.derive_at(0) == neg.derive_at(0))
|
||||
Kit.check("负种子 abs 归一化后派生与正种子一致", pos.derive_at(0) == neg.derive_at(0))
|
||||
@@ -7,23 +7,12 @@
|
||||
# 第一个真实测试:复制本文件为 tests/unit/<system>/<system>_<feature>_test.gd 并替换占位断言
|
||||
extends SceneTree
|
||||
|
||||
func _init() -> void:
|
||||
var failures := 0
|
||||
failures += _check("headless SceneTree 协议可用(quit(code) 通道)", _test_headless_protocol())
|
||||
failures += _check("命名规范可执行:test_[scenario]_[expected]", _test_naming_convention_followed())
|
||||
if failures > 0:
|
||||
push_error("[example_test] %d 项失败" % failures)
|
||||
quit(1)
|
||||
return
|
||||
print("[example_test] all green")
|
||||
quit(0)
|
||||
const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:check/汇总退出样板单点
|
||||
|
||||
func _check(desc: String, ok: bool) -> int:
|
||||
if not ok:
|
||||
push_error("[example_test] FAIL: " + desc)
|
||||
return 1
|
||||
print("[example_test] PASS: " + desc)
|
||||
return 0
|
||||
func _init() -> void:
|
||||
Kit.check("headless SceneTree 协议可用(quit(code) 通道)", _test_headless_protocol())
|
||||
Kit.check("命名规范可执行:test_[scenario]_[expected]", _test_naming_convention_followed())
|
||||
quit(Kit.finish("example_test"))
|
||||
|
||||
func _test_headless_protocol() -> bool:
|
||||
# 占位断言:验证脚本本身在 SceneTree 上下文可执行。
|
||||
|
||||
@@ -3,6 +3,17 @@ extends SceneTree
|
||||
# 原守 node_overlay.show_reward_items;奖励节点三选一与通关三选一合并共用 BonusPickOverlay 后,
|
||||
# 本套件改守共用面的「功能存在 ≠ 功能可达」盲区:选择外发/强制确认两段式/替换子面板两路/结果态。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/presentation/reward_overlay_test.gd(expect 0)
|
||||
# T6 收口批:断言走 test_kit 计数(Kit.check)+ 失败即停早退协议保留(顺序状态机——坏态下后继断言无效)。
|
||||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
|
||||
# 断言 + 失败即停(原直写 print FAIL + quit(1) + return 样板单点化——早退语义逐位保持)
|
||||
func _req(desc: String, ok: bool) -> bool:
|
||||
Kit.check(desc, ok)
|
||||
if not ok:
|
||||
quit(1)
|
||||
return false
|
||||
return true
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
@@ -25,17 +36,15 @@ func _run() -> void:
|
||||
for c in ov._choice_box.get_children():
|
||||
if c is Button:
|
||||
btns += 1
|
||||
if ov.visible != true or btns != 3 or ov._confirm_btn.visible or ov._continue_btn.visible:
|
||||
print("[reward_verify] FAIL — 选择态应三卡呈现且确认/继续隐藏(btns=%d)" % btns)
|
||||
quit(1)
|
||||
if not _req("选择态应三卡呈现且确认/继续隐藏(btns=%d)" % btns,
|
||||
ov.visible and btns == 3 and not ov._confirm_btn.visible and not ov._continue_btn.visible):
|
||||
return
|
||||
# ② 点选→高亮→确认外发(真实按钮路径);确认前改选以最后点选为准
|
||||
ov.press_card(0)
|
||||
ov.press_card(1)
|
||||
ov.press_confirm()
|
||||
if ov._confirm_btn.visible != true or picked.size() != 1 or String(picked[0]) != "gp":
|
||||
print("[reward_verify] FAIL — 点选确认未外发 pick_confirmed(gp)(%s)" % str(picked))
|
||||
quit(1)
|
||||
if not _req("点选确认未外发 pick_confirmed(gp)(%s)" % str(picked),
|
||||
ov._confirm_btn.visible and picked.size() == 1 and String(picked[0]) == "gp"):
|
||||
return
|
||||
# ③ 替换子面板:丢旧收新与放弃新件两路外发(通关渠道=携带序号 / 节点渠道=库存句柄——tag 直通)
|
||||
ov.open_replace([{"index": 0, "item_id": "probe_dust", "display_name": "显影尘"}], "爆破工兵包", 1, 1)
|
||||
@@ -43,33 +52,26 @@ func _run() -> void:
|
||||
for c in ov._replace_box.get_children():
|
||||
if c is Button:
|
||||
replace_btns += 1
|
||||
if replace_btns != 2 or ov._choice_box.visible:
|
||||
print("[reward_verify] FAIL — 替换面板应 2 钮(N+1 选 N+放弃)且三卡隐藏(btns=%d)" % replace_btns)
|
||||
quit(1)
|
||||
if not _req("替换面板应 2 钮(N+1 选 N+放弃)且三卡隐藏(btns=%d)" % replace_btns,
|
||||
replace_btns == 2 and not ov._choice_box.visible):
|
||||
return
|
||||
var replaced_first := false
|
||||
for c in ov._replace_box.get_children():
|
||||
if c is Button:
|
||||
c.pressed.emit()
|
||||
replaced_first = true
|
||||
break
|
||||
if replaced.size() != 1 or int(replaced[0]) != 0:
|
||||
print("[reward_verify] FAIL — 丢旧收新未外发 replace_chosen(0)(%s)" % str(replaced))
|
||||
quit(1)
|
||||
if not _req("丢旧收新未外发 replace_chosen(0)(%s)" % str(replaced),
|
||||
replaced_first and replaced.size() == 1 and int(replaced[0]) == 0):
|
||||
return
|
||||
# ④ 结果态:继续钮点亮、唯一出口
|
||||
ov.show_result("已选择:攻击力提升 · 攻击 +1")
|
||||
if ov._continue_btn.visible != true or ov._replace_box.visible:
|
||||
print("[reward_verify] FAIL — 结果态应点亮继续且收替换面板")
|
||||
quit(1)
|
||||
if not _req("结果态应点亮继续且收替换面板", ov._continue_btn.visible and not ov._replace_box.visible):
|
||||
return
|
||||
ov.press_continue()
|
||||
if continued.size() != 1:
|
||||
print("[reward_verify] FAIL — 继续未外发 continue_pressed")
|
||||
quit(1)
|
||||
if not _req("继续未外发 continue_pressed", continued.size() == 1):
|
||||
return
|
||||
ov.close_pick()
|
||||
if ov.visible:
|
||||
print("[reward_verify] FAIL — close_pick 应收屏")
|
||||
quit(1)
|
||||
if not _req("close_pick 应收屏", not ov.visible):
|
||||
return
|
||||
print("[reward_verify] PASS — 三卡两段式/确认外发/替换两路/结果态/收屏 全过")
|
||||
quit(0)
|
||||
quit(Kit.finish("reward_verify", "PASS — 三卡两段式/确认外发/替换两路/结果态/收屏 全过"))
|
||||
@@ -9,9 +9,7 @@ extends SceneTree
|
||||
|
||||
const ConfigDBScript := preload("res://core/config_db.gd")
|
||||
const BoardViewScript := preload("res://presentation/views/board_view.gd")
|
||||
|
||||
var _failures := 0
|
||||
var _checks := 0
|
||||
const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:_check/汇总退出样板单点
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
@@ -23,7 +21,7 @@ func _init() -> void:
|
||||
return
|
||||
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 not load_ok:
|
||||
quit(1)
|
||||
return
|
||||
@@ -33,25 +31,11 @@ func _init() -> void:
|
||||
var bt := String(row.get("board_type", ""))
|
||||
if bt != "" and not types.has(bt):
|
||||
types.append(bt)
|
||||
_check("前置:board_gen 词表非空(%d 型)" % types.size(), types.size() > 0)
|
||||
Kit.check("前置:board_gen 词表非空(%d 型)" % types.size(), types.size() > 0)
|
||||
_test_set_completeness(board_dir, types)
|
||||
_test_hue_domains(board_dir)
|
||||
_test_digit_ink_modes(board_dir, types)
|
||||
if _failures > 0:
|
||||
push_error("[terrain_tile_sets_test] %d/%d 项失败" % [_failures, _checks])
|
||||
quit(1)
|
||||
return
|
||||
print("[terrain_tile_sets_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("terrain_tile_sets_test"))
|
||||
|
||||
|
||||
# 套完整性:词表每个 board_type 的 16 墙 + 5 地面变体全部在盘(缺位 → BoardView 整批回落 dungeon 的
|
||||
@@ -69,7 +53,7 @@ func _test_set_completeness(board_dir: String, types: Array) -> void:
|
||||
var pf := board_dir.path_join("terrain_floor_%s_%d.png" % [String(bt), i])
|
||||
if not FileAccess.file_exists(pf):
|
||||
missing.append("floor_%d" % i)
|
||||
_check("图型 %s 瓦片套完整(16 墙 + %d 地面,缺=%s)" % [String(bt), variants, str(missing)], missing.is_empty())
|
||||
Kit.check("图型 %s 瓦片套完整(16 墙 + %d 地面,缺=%s)" % [String(bt), variants, str(missing)], missing.is_empty())
|
||||
|
||||
|
||||
# 色相域抽检:wall_center 上半幅(墙顶面)与 floor_1 全幅的均值色相/明度
|
||||
@@ -85,17 +69,17 @@ func _test_hue_domains(board_dir: String) -> void:
|
||||
var wall := _load_stats(board_dir, "terrain_wall_center_%s.png" % String(bt), true)
|
||||
var floor_img := _load_stats(board_dir, "terrain_floor_%s_1.png" % String(bt), false)
|
||||
if wall.is_empty() or floor_img.is_empty():
|
||||
_check("图型 %s 抽检样本可读" % String(bt), false)
|
||||
Kit.check("图型 %s 抽检样本可读" % String(bt), false)
|
||||
continue
|
||||
wall_v[bt] = float(wall["v"])
|
||||
floor_v[bt] = float(floor_img["v"])
|
||||
var band: Array = bands[bt]
|
||||
_check("图型 %s 墙顶均值色相在带 %s(实测 %.3f)" % [String(bt), str(band), float(wall["h"])],
|
||||
Kit.check("图型 %s 墙顶均值色相在带 %s(实测 %.3f)" % [String(bt), str(band), float(wall["h"])],
|
||||
float(wall["h"]) >= float(band[0]) and float(wall["h"]) <= float(band[1]))
|
||||
_check("图型 %s 图底分离(墙顶明度 %.3f > 地面 %.3f)" % [String(bt), wall_v[bt], floor_v[bt]],
|
||||
Kit.check("图型 %s 图底分离(墙顶明度 %.3f > 地面 %.3f)" % [String(bt), wall_v[bt], floor_v[bt]],
|
||||
wall_v[bt] > floor_v[bt])
|
||||
if wall_v.has("underdark") and wall_v.has("deep_underdark"):
|
||||
_check("deep_underdark 墙顶更暗于 underdark(更深蓝黑方向)", float(wall_v["deep_underdark"]) < float(wall_v["underdark"]))
|
||||
Kit.check("deep_underdark 墙顶更暗于 underdark(更深蓝黑方向)", float(wall_v["deep_underdark"]) < float(wall_v["underdark"]))
|
||||
|
||||
|
||||
# 均值读数:hue 取饱和度 > 0.05 的像素(墨线/灰黑不影响色相均值);v 取全幅均值
|
||||
@@ -150,26 +134,26 @@ func _test_digit_ink_modes(board_dir: String, types: Array) -> void:
|
||||
var tex: Texture2D = ImageTexture.create_from_image(Image.load_from_file(p))
|
||||
BoardViewScript._normalize_floor_luma(tex, 0.10, lumas)
|
||||
if not readable:
|
||||
_check("图型 %s 数字墨族断言样本可读" % String(bt), false)
|
||||
Kit.check("图型 %s 数字墨族断言样本可读" % String(bt), false)
|
||||
continue
|
||||
var mode: Dictionary = BoardViewScript._digit_ink_mode(lumas)
|
||||
var plate: bool = bool(mode["plate"])
|
||||
var dark: bool = bool(mode["dark"])
|
||||
var mode_name := "色牌垫底" if plate else ("深墨族直画" if dark else "亮墨族直画")
|
||||
if plate:
|
||||
_check("图型 %s 双峰纹理落牌(亮族 p90 %.2f/均值 %.2f,深族 p90 %.2f/均值 %.2f 均未过门)" % [
|
||||
Kit.check("图型 %s 双峰纹理落牌(亮族 p90 %.2f/均值 %.2f,深族 p90 %.2f/均值 %.2f 均未过门)" % [
|
||||
String(bt), float(mode["cr_p90_bright"]), float(mode["cr_mean_bright"]),
|
||||
float(mode["cr_p90_dark"]), float(mode["cr_mean_dark"])],
|
||||
float(mode["cr_mean_dark"]) < float(BoardViewScript.DIGIT_INK_CR_MEAN_DARK))
|
||||
else:
|
||||
var cr_p90 := float(mode["cr_p90_bright"]) if not dark else float(mode["cr_p90_dark"])
|
||||
var cr_mean := float(mode["cr_mean_bright"]) if not dark else float(mode["cr_mean_dark"])
|
||||
_check("图型 %s 所选%s 对 p90 ≥%.1f(实测 %.2f)" % [String(bt), mode_name,
|
||||
Kit.check("图型 %s 所选%s 对 p90 ≥%.1f(实测 %.2f)" % [String(bt), mode_name,
|
||||
float(BoardViewScript.DIGIT_INK_CR_P90), cr_p90],
|
||||
cr_p90 >= float(BoardViewScript.DIGIT_INK_CR_P90))
|
||||
var mean_gate := float(BoardViewScript.DIGIT_INK_CR_MEAN_BRIGHT) if not dark else float(BoardViewScript.DIGIT_INK_CR_MEAN_DARK)
|
||||
_check("图型 %s 所选%s 对均值 ≥%.1f(实测 %.2f)" % [String(bt), mode_name, mean_gate, cr_mean],
|
||||
Kit.check("图型 %s 所选%s 对均值 ≥%.1f(实测 %.2f)" % [String(bt), mode_name, mean_gate, cr_mean],
|
||||
cr_mean >= mean_gate)
|
||||
if expect_plate.has(bt):
|
||||
_check("图型 %s 呈现模式期望=%s(实测=%s)" % [String(bt),
|
||||
Kit.check("图型 %s 呈现模式期望=%s(实测=%s)" % [String(bt),
|
||||
"色牌" if bool(expect_plate[bt]) else "直画", mode_name], plate == bool(expect_plate[bt]))
|
||||
@@ -7,38 +7,22 @@
|
||||
extends SceneTree
|
||||
|
||||
const UiKitScript := preload("res://presentation/views/ui_kit.gd")
|
||||
|
||||
var _failures := 0
|
||||
var _checks := 0
|
||||
const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:_check/汇总退出样板单点
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
var path: String = UiKitScript.FONT_UI
|
||||
var f: Font = UiKitScript.load_font(path)
|
||||
_check("随包字体守卫装载非空(res://assets/fonts/ui_font_fusion_pixel_zh_hans.ttf)", f != null)
|
||||
Kit.check("随包字体守卫装载非空(res://assets/fonts/ui_font_fusion_pixel_zh_hans.ttf)", f != null)
|
||||
if f != null:
|
||||
_check("中文字形在集(攻/雷/旗/封)", f.has_char("攻".unicode_at(0)) and f.has_char("雷".unicode_at(0)) \
|
||||
Kit.check("中文字形在集(攻/雷/旗/封)", f.has_char("攻".unicode_at(0)) and f.has_char("雷".unicode_at(0)) \
|
||||
and f.has_char("旗".unicode_at(0)) and f.has_char("封".unicode_at(0)))
|
||||
_check("ASCII/数字字形在集(A/0/%)", f.has_char("A".unicode_at(0)) and f.has_char("0".unicode_at(0)) and f.has_char("%".unicode_at(0)))
|
||||
Kit.check("ASCII/数字字形在集(A/0/%)", f.has_char("A".unicode_at(0)) and f.has_char("0".unicode_at(0)) and f.has_char("%".unicode_at(0)))
|
||||
var sz := 18
|
||||
var w_cn: float = f.get_string_size("战斗胜利", HORIZONTAL_ALIGNMENT_LEFT, -1, sz).x
|
||||
var w_label: float = f.get_string_size("攻击 2", HORIZONTAL_ALIGNMENT_LEFT, -1, sz).x
|
||||
_check("中文采样文本宽度 > 0(C6 headless 判据)", w_cn > 0.0 and w_label > 0.0)
|
||||
Kit.check("中文采样文本宽度 > 0(C6 headless 判据)", w_cn > 0.0 and w_label > 0.0)
|
||||
# 注:不设「私用区 has_char=false」负探针——fusion pixel 实测覆盖 PUA 码位,负向覆盖非契约面(宁缺勿错)
|
||||
else:
|
||||
_check("字体缺失时后续断言跳过(缺位=回归,此行仅占位计数)", false)
|
||||
if _failures > 0:
|
||||
push_error("[ui_font_asset_test] %d/%d 项失败" % [_failures, _checks])
|
||||
quit(1)
|
||||
return
|
||||
print("[ui_font_asset_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)
|
||||
Kit.check("字体缺失时后续断言跳过(缺位=回归,此行仅占位计数)", false)
|
||||
quit(Kit.finish("ui_font_asset_test"))
|
||||
@@ -0,0 +1,72 @@
|
||||
# tests/unit/test_kit.gd — 单元测试协议样板单点(T6 样板收口批 2026-10-02)
|
||||
# 收口范围:11 份套件的 _check/_problems 样板(计数器 + PASS/FAIL 行 + 汇总退出)+
|
||||
# reward_dangling_aux/aux_target_violation 两致命类用例各自复制的 BAD 表目录夹具。
|
||||
# 用法(--script 模式无 autoload/全局标识符——禁 class_name 裸引用,使用方 preload):
|
||||
# const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
# Kit.check("断言描述", cond) # 计数 + 即时 PASS/FAIL 行
|
||||
# Kit.check_quiet(cond, "描述") # 计数 + 失败收集(finish 时统一汇总——原 _problems 风格,原参序)
|
||||
# quit(Kit.finish("suite_name")) # 汇总输出 + 返回退出码(0 全绿 / 1 有败)
|
||||
# 计数器为 static(套件 = 独立 Godot 子进程恰一份生命周期,见 run_all.gd 执行形态);
|
||||
# 单进程跑多套件须先 Kit.reset()。
|
||||
extends RefCounted
|
||||
|
||||
static var checks: int = 0
|
||||
static var failures: int = 0
|
||||
static var problems: Array = []
|
||||
|
||||
|
||||
# 主通道:计数 + 即时 PASS/FAIL 行(config_db/rng_service/damage_math/system_contracts/
|
||||
# damage_controller/ui_font_asset/terrain_tile_sets 等套件原 _check 同款输出,描述在前)
|
||||
static func check(desc: String, ok: bool) -> void:
|
||||
checks += 1
|
||||
if ok:
|
||||
print(" [PASS] " + desc)
|
||||
else:
|
||||
failures += 1
|
||||
push_error(" [FAIL] " + desc)
|
||||
|
||||
|
||||
# 静默收集通道:失败只记入 problems(即时零刷屏),finish 统一汇总。
|
||||
# 参序沿原函数式套件 aux_items/breakpoint_resume 的 check(cond, msg)——条件在前,与主通道相反。
|
||||
static func check_quiet(ok: bool, desc: String) -> void:
|
||||
checks += 1
|
||||
if not ok:
|
||||
failures += 1
|
||||
problems.append(desc)
|
||||
|
||||
|
||||
# 汇总退出:返回退出码(调用方 `quit(Kit.finish(...))`)。pass_line = 成功尾行覆盖
|
||||
#(保留原套件 PASS 摘要文案;缺省 = "all green (N checks)" 计数口径)。
|
||||
# 失败路径:push_error 计数汇总 + problems 逐条 FAIL 行(print + push_error 双通道——原两风格并档)。
|
||||
static func finish(suite: String, pass_line: String = "") -> int:
|
||||
if failures > 0:
|
||||
push_error("[%s] %d/%d 项失败" % [suite, failures, checks])
|
||||
for p in problems:
|
||||
print("[%s] FAIL — %s" % [suite, p])
|
||||
push_error("[%s] FAIL — %s" % [suite, p])
|
||||
return 1
|
||||
print("[%s] %s" % [suite, pass_line if pass_line != "" else "all green (%d checks)" % checks])
|
||||
return 0
|
||||
|
||||
|
||||
# BAD 表夹具共享(原 reward_dangling_aux/aux_target_violation 各自复制的 _copy_tables):
|
||||
# 整目录拷贝 src_dir → bad_dir(目录已建)。BAD_DIR 常量仍由各用例自持(名字互异——独立进程并行不互踩)。
|
||||
static func copy_tables(src_dir: String, bad_dir: String) -> bool:
|
||||
DirAccess.make_dir_recursive_absolute(bad_dir)
|
||||
var dir := DirAccess.open(src_dir)
|
||||
if dir == null:
|
||||
return false
|
||||
dir.list_dir_begin()
|
||||
var f := dir.get_next()
|
||||
while f != "":
|
||||
if not dir.current_is_dir():
|
||||
if dir.copy(src_dir + "/" + f, bad_dir + "/" + f) != OK:
|
||||
return false
|
||||
f = dir.get_next()
|
||||
return true
|
||||
|
||||
|
||||
static func reset() -> void:
|
||||
checks = 0
|
||||
failures = 0
|
||||
problems = []
|
||||
@@ -0,0 +1 @@
|
||||
uid://dtestkit123456
|
||||
Reference in new issue
Block a user