Files
game-code-4/tests/unit/cases/test_offline_contract.gd
T
程基岩(engineering-lead) 08dc5020c3 W4 批次:三项设计裁定落地(Boss 战 Plan B / 离线契约 v2 / 长明灯加法化)
Boss 战修复(Plan B,用户拍板):
- HP 公式 15xPxhall_factor -> HP = P x boss_army_seconds_n(S=23.0→28.0 递增,
  每场军列约 23-28 秒清空,战斗时长成为可直接书写的设计旋钮,结构性消除
  高殿 P 抵消导致不可胜的旧 bug);hall_factor 降级为纯叙事/视觉参数
- 新增 souls_par 列(0→90000):开场冲锋 intro_dmg = HP x 10% x clamp(souls/souls_par,0,1),
  上限 20%->10%;删 hp_factor/intro_dmg_souls_factor/intro_dmg_cap_ratio 三列
- 点击伤害 P x κ x combo_mult,κ = (0.010 + 0.052 x click_ratio) x (1 + 0.30 x m06_owned),
  click_ratio = 已购点击升级 / 12(κ 0.010→0.0806,满配点击占比约 20.5%±3pp)
- 防双重计入:L5-12 点击升级 +1% output/s 留全局区(抬 P 同时抬军列 DPS 与点击
  伤害,可接受),不得再计入 κ 分子(recompute 注释固化)

离线契约 v2(P-8,改契约不改表):
- OfflineModel.settle 第三参 offline_mult -> offline_add(加法);各档效率
  min(rate+add, 1.0) 硬顶 100%,离线永不超在线
- 长明灯 x1.15 乘性 -> +5%/级 加法(满级 +30%,各档封顶 100%)
- 0-2h 100% 保持;8h = 6.2 等效小时(77.5% 效率)
- Gate #9 拆 9a/9b/9c:9a FAIL->PASS(0-2h 全额 + 8h∈[5.5,6.5]),
  9b 长明灯满级 8h∈[7.5,8.0] 且 ≤100%,9c 任意配置效率 ≤100%

数据与工具:
- gen_balance_sheets.py 补登 W3 遗留 7 行 tune(防重跑静默丢失),self_check 更新
- boss_battles/upgrades schema 同步(offline_mult->offline_add enum,漏改会违反导入)

测试(runner 7->8 用例,Node 交叉验证 145 断言全过):
- 可胜性哨兵重写为「十殿全可胜」:每殿 S_n<30、军列+点击≥HP、余时∈[2,8]、
  满配余时差≥4、点击占比∈[15,25]、H10≈20.5%±3pp
- κ 四档断言(0.010/0.036/0.062/0.0806,分母 12)
- 新增 test_offline_contract.gd:契约 v2 数值面 + 9a/9b/9c 全 PASS + β 缓议
2026-10-20 17:01:25 +08:00

85 lines
4.3 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends RefCounted
# 用例:W4 离线契约 v2(systems-spec §2.4/§2.5,用户 2026-10-20 拍板)
# 契约 v2:8h = 6.2 等效小时(效率 77.5%);24h = 12.6h;72h 封顶 = 17.4h(24.2%);
# 长明灯 6 条 × +5% 加法(满 +30%,各档速率硬顶 100%);效率硬顶 ≤ 100%。
# 门 #9 拆三条子检查(a 由 FAIL 转 PASS):9a 契约效率 / 9b 长明灯上限 / 9c 硬顶。
func run(a, data) -> void:
_test_contract_v2(a, data)
_test_gate_offline_v2(a, data)
func _settle(a, data, hours: float, add: float) -> float:
var r: Dictionary = OfflineModel.settle(hours * 3600.0, 1.0, add,
data.tune_num("tune_offline_band1_h", 2.0), data.tune_num("tune_offline_band1_rate", 1.0),
data.tune_num("tune_offline_band2_h", 8.0), data.tune_num("tune_offline_band2_rate", 0.7),
data.tune_num("tune_offline_band3_h", 24.0), data.tune_num("tune_offline_band3_rate", 0.4),
data.tune_num("tune_offline_band4_rate", 0.1),
data.tune_num("tune_offline_cap_h", 72.0))
return float(r["effective_hours"])
# ---- 契约 v2 数值面 ----
func _test_contract_v2(a, data) -> void:
# 衰减表一字不改(systems-spec §2.4:错的是契约不是表)
a.near(data.tune_num("tune_offline_band1_rate", 0.0), 1.0, 1e-9, "0-2h = 100%")
a.near(data.tune_num("tune_offline_band2_rate", 0.0), 0.7, 1e-9, "2-8h = 70%")
a.near(data.tune_num("tune_offline_band3_rate", 0.0), 0.4, 1e-9, "8-24h = 40%")
a.near(data.tune_num("tune_offline_band4_rate", 0.0), 0.1, 1e-9, ">24h = 10%")
a.near(data.tune_num("tune_offline_cap_h", 0.0), 72.0, 1e-9, "72h 封顶")
# 8h = 6.2 等效小时(77.5%)∈ [5.5, 7.0](用户口径)/ [5.5, 6.5](spec 口径,双重满足)
var e8 := _settle(a, data, 8.0, 0.0)
a.near(e8, 6.2, 1e-9, "8h 离线 = 6.2h 等效(契约 v2)")
a.true_(e8 >= 5.5 and e8 <= 7.0, "8h 等效 ∈ [5.5, 7.0](用户构造口径)")
a.true_(e8 >= 5.5 and e8 <= 6.5, "8h 等效 ∈ [5.5, 6.5](systems-spec §2.4 口径)")
a.near(e8 / 8.0, 0.775, 1e-9, "8h 效率 = 77.5%")
# 24h = 12.6h;72h 封顶 = 17.4h(24.2%)
a.near(_settle(a, data, 24.0, 0.0), 12.6, 1e-9, "24h 离线 = 12.6h 等效")
a.near(_settle(a, data, 72.0, 0.0), 17.4, 1e-9, "72h 封顶 = 17.4h 等效")
a.near(_settle(a, data, 72.0, 0.0) / 72.0, 17.4 / 72.0, 1e-9, "72h 效率 ≈ 24.2%")
a.near(_settle(a, data, 100.0, 0.0), 17.4, 1e-9, "100h 也被 72h 硬顶钳到 17.4h")
# 长明灯:数据面 = 6 条 × 0.05 加法(effect_type offline_add)
var lamp := 0.0
var lamp_rows := 0
for u in data.upgrades:
if u.type == "offline":
lamp += u.effect_param
lamp_rows += 1
a.eq(lamp_rows, 6, "长明灯 6 条")
a.near(lamp, 0.30, 1e-9, "长明灯满级 = 加法 +30%(旧 ×2.31 乘性已废止)")
# 长明灯满级:8h = 8.0h,效率恰好 100%(硬顶允许的最大值)
var e8l := _settle(a, data, 8.0, lamp)
a.near(e8l, 8.0, 1e-9, "长明灯满级 8h = 8.0h 等效")
a.near(e8l / 8.0, 1.0, 1e-9, "效率 = 100%(≤ 硬顶,离线不优于在线)")
# + m05(+25%)也不越过硬顶
var m05 := data.meta("meta_05")
var m05_add := 0.0
if m05 != null:
m05_add = m05.effect_param * float(m05.max_rank)
a.near(_settle(a, data, 8.0, lamp + m05_add), 8.0, 1e-9, "+ m05(+25%)仍钳 8.0h")
# 效率硬顶:任意配置/任意时长 ≤ 100%
for dur_h in [1.0, 8.0, 24.0, 72.0]:
for add in [0.0, lamp, lamp + m05_add, 5.0]:
var eff := _settle(a, data, dur_h, add)
a.true_(eff <= dur_h + 1e-9,
"效率硬顶:%sh + 加法 %.2f ⇒ %.2fh ≤ %sh" % [str(dur_h), add, eff, str(dur_h)])
# ---- 门 #9 三条子检查(a 由 FAIL 转 PASS) ----
func _test_gate_offline_v2(a, data) -> void:
var g: Dictionary = SimGates.gate_offline_v2(data)
a.true_(bool(g.get("pass", false)), "门 #9 三条子检查全过(a 由 FAIL 转 PASS)")
a.true_(str(g.get("detail", "")).contains("契约 v2"), "detail 标注契约 v2")
var sub: Dictionary = g.get("sub", {})
var g9a: Dictionary = sub.get("9a", {})
var g9b: Dictionary = sub.get("9b", {})
var g9c: Dictionary = sub.get("9c", {})
a.true_(bool(g9a.get("pass", false)),
"9a PASS:%s" % str(g9a.get("detail", "")))
a.true_(bool(g9b.get("pass", false)),
"9b PASS:%s" % str(g9b.get("detail", "")))
a.true_(bool(g9c.get("pass", false)),
"9c PASS:%s" % str(g9c.get("detail", "")))
a.true_(str(g.get("deferred_beta", "")).contains("β"), "进度口径 β 标记为待 sim 标定")