- data_config: ConfigDB 表加载+跨表护栏零诊断/进度坐标 ceil-clamp 解析与警告去重/变异池门控/shop·meta 消费面/manifest↔目录同步/越域 fatal 退出码(独立进程 expect=1,deferred 触发保 quit 通道)/统一随机流(派生确定性/计数器账本/标签不参与派生/负种子归一化) - battle_settlement: DamageMath 四纯函数(池乘区/克制分量/掷点查档/吃掷点分量)+DamageController 伤害单据五配方+B/A 双结账模式+来源敌死作废+requires 前置门+主角倒下 halt+AC 合流三通道+池 clamp 与 refresh 政策 - contracts: 五配方/三目标/双政策词表封闭+掷点标签∈RNG 封闭词表(EC12)+单据九栏缺省+增益布尔受理口径(EC1/EC2) - 红线: 期望值全由表读值推导(零玩法数值字面量)/零自建 RNG(夹具种子仅求复现) - 轻闸: 冒烟 557/0 + ui_input_probe 十一断言全绿(基线零漂移)
103 lines
5.6 KiB
GDScript
103 lines
5.6 KiB
GDScript
# tests/unit/battle_settlement/damage_math_test.gd — 战斗结算·伤害公式族纯函数单元测试
|
||
# 运行:godot --headless --path game --script res://tests/unit/battle_settlement/damage_math_test.gd
|
||
# 协议:DamageMath = 零状态纯算术宿主(headless 可跑);掷点档位参数全读 settlement 表(零玩法数值字面量)。
|
||
# 注:0/100 等出现在断言中的数值 = 语义常量(中性池/百分比满额),非玩法行值。
|
||
extends SceneTree
|
||
|
||
const DamageMathScript := preload("res://core/damage_math.gd")
|
||
const ConfigDBScript := preload("res://core/config_db.gd")
|
||
|
||
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)
|
||
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)
|
||
|
||
|
||
func _floor_val() -> int:
|
||
return int(db.settlement.damage_floor)
|
||
|
||
|
||
# ---- F2 乘区:池内加算 → 池间乘算 → floor(单一取整规则)----
|
||
|
||
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)
|
||
|
||
|
||
# ---- 克制分量:空桶不吃修正恒 0;落地下限保底([1,+∞) 全来源)----
|
||
|
||
func _test_counter_component() -> void:
|
||
var floor_v := _floor_val()
|
||
_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)
|
||
|
||
|
||
# ---- 掷点折扣查档(纯函数):满阈值恒全额;档位边界/末档语义与表数组一致 ----
|
||
|
||
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
|
||
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)
|
||
# 缺口超末边界:落末档
|
||
var last_expected: int = int(discounts[-1])
|
||
_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)
|
||
|
||
|
||
# ---- 吃掷点分量算术:折扣→(暴击)→floor→+克制修正(单一取整链)----
|
||
|
||
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 只在链末)",
|
||
DamageMathScript.rolled_component(base, 50, true, crit_mult, 0) == int(floor(base * 50.0 / 100.0 * float(crit_mult) / 100.0)))
|