Files
gplan-2/game/tests/unit/battle_settlement/trigger_cond_test.gd
T
DKChan 4868e11b9d 触发器卡(触发器批wr1-wr9): 触发条件机制实装+三Boss迁移+两示范投放——skill_table三常量词表+validate七反例校验段(四键白名单/词表出域/枚举算子限域/类型分派/普攻禁配);enemy_table unlock_hp_pct域校验段删(字段退役);skills.tres三Boss行加trigger闩型+enemy.tres八处unlock_hp_pct删+J面注释清理(技能表头注/酋长池注);unit_container快照trigger随读(技能行权威)+_entry_blocked收编_entry_locked(百分比阈值式T=round(max_hp×value/100)六算子统一/facts缺键=拦fail-safe)+rollable/special双面过滤facts缺省参(既有单参调用零破坏)+check_trigger_unlocks闩型换代;battle facts组包(摇号前一次成型,last_round_end空字典首轮映射none唯一落点)+mines计数埋点(负向blast守卫累加——正向spark_aoe/清图奖励同走blast分支不计入,轮末转存,开战清零)+解锁调用与pct读值源迁移;cross_validator池构成新纪律(每池至少一行无条件特殊技——普攻不计/trigger缺省行计数);示范投放:skills.tres孤注守势(buff/defensive_stance/lte35)+趁隙偷袭(damage×1.5/CD4/eq stopped)+status_effects护身行+地精两行w1+钩爪恐魔三行w2;测试换代:skill_unlock_test读值源与签名换代(申报①语义零变化)+round_flow expect_pools期望值随投放改写(申报②地精两行9/1→9/1/1·钩爪恐魔三行6/2/1→6/2/1/2)+十四行断言换代十六行+V4池纪律扩行(申报④)+巨蜘蛛band1误投修正(old_hook块与蜘蛛池同构误命中);trigger_cond_test新套件V1/V2/V4/V6/V7/V8/V9/V10全绿(manifest登记;V5对拍由skill_unlock/round_flow编排集成承载)。已知预期红:round_flow EV抽样对锚goblin(实测0.9148∉[1.00,1.20]——示范投放=池权重数值改动,EV稀释方向合理)——卡片T4-4预设停机呈报面:锚内子集判红,重锚归用户档位发起,实现批不自行重锚〔触发器批 V1-V10〕
2026-10-27 04:49:55 +08:00

192 lines
11 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.
# VERTICAL SLICE - NOT FOR PRODUCTION
# 触发条件机制单测(技能池情境条件触发器批——V1 出域拒绝/V2 双面过滤/V3 保底不逼/V4 闩型语义/
# V6 事实面供给/V7 池构成纪律/V8 低血防御/V9 停手偷袭/V10 缺省零扰动;V5 对拍由 skill_unlock_test
# 迁移断言与 round_flow 编排集成断言承载)。
# 运行:godot --headless --path game --script res://tests/unit/battle_settlement/trigger_cond_test.gd(expect 0)
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd")
func _init() -> void:
var db = load("res://core/config_db.gd").new()
if not db.load_tables("res://data/tables", false):
print("[trigger_cond_test] FAIL — 数值表加载失败")
quit(1)
return
# ---- V1 出域拒绝(六反例+合法行零 error——构造行夹具直改 rows 后重校验)----
var skill = db.skill
var base_rows: Array = skill.rows.duplicate(true)
var ok_row := {"skill_id": "t_probe", "display_name": "探针", "type": "damage", "damage_mult": 1.5,
"cooldown_beats": 2, "trigger": {"fact": "round_index", "op": "gte", "value": 3, "latch": false}}
var bad_rows := [
{"skill_id": "t_bad_fact", "display_name": "坏事实", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2,
"trigger": {"fact": "nope", "op": "eq", "value": 1}},
{"skill_id": "t_bad_op", "display_name": "坏算子", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2,
"trigger": {"fact": "round_index", "op": "xor", "value": 1}},
{"skill_id": "t_bad_key", "display_name": "坏键", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2,
"trigger": {"fact": "round_index", "op": "eq", "value": 1, "latch": false, "extra": 1}},
{"skill_id": "t_bad_latch", "display_name": "坏闩", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2,
"trigger": {"fact": "round_index", "op": "eq", "value": 1, "latch": "yes"}},
{"skill_id": "t_bad_enum_op", "display_name": "枚举坏算子", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2,
"trigger": {"fact": "last_round_end", "op": "gt", "value": "stopped"}},
{"skill_id": "t_bad_enum_val", "display_name": "枚举坏值", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2,
"trigger": {"fact": "last_round_end", "op": "eq", "value": "win"}},
{"skill_id": "t_bad_basic", "display_name": "普攻坏配", "type": "basic",
"trigger": {"fact": "round_index", "op": "eq", "value": 1}},
]
for bad in bad_rows:
skill.rows = base_rows.duplicate(true)
skill.rows.append(bad)
var errs: Array = skill.validate()
var hit := false
for e in errs:
if String((e as Dictionary).get("category", "")) != "empty_table" and String((e as Dictionary).get("message", "")).contains(String(bad.get("skill_id", ""))):
hit = true
Kit.check_quiet(hit, "V1 出域拒绝:%s(%s)〔触发器批 V1〕" % [String(bad.get("skill_id", "")), String(bad.get("display_name", ""))])
skill.rows = base_rows.duplicate(true)
skill.rows.append(ok_row)
Kit.check_quiet(skill.validate().is_empty(), "V1 合法触发行零 error〔触发器批 V1〕")
skill.rows = base_rows
# ---- V2 门槛过滤双面(spawn 真实表→直调 rollable/special)----
var ec = load("res://core/unit_container.gd").new(db)
Kit.check_quiet(ec.spawn(2, 9, 30, [], 8, 30, [], "", "", "boss"), "V2 前置:卓尔 Boss spawn")
var u = ec.all_enemies()[0]
Kit.check_quiet(int(u.hp) == int(u.max_hp), "V2 前置:满血(hp=max_hp)")
var r_full: Array = ec.rollable_skills(u)
var s_full: Array = ec.special_skills(u)
Kit.check_quiet(not _has(r_full, "venom_curb") and not _has(s_full, "venom_curb"),
"V2 条件假(满血):venom_curb 双面不入集合〔触发器批 V2〕")
u["hp"] = 15
ec.check_trigger_unlocks(u)
Kit.check_quiet(_has(ec.rollable_skills(u), "venom_curb") and _has(ec.special_skills(u), "venom_curb"),
"V2 条件真(半血过闩):venom_curb 双面入集合〔触发器批 V2〕")
# ---- V4 闩型语义(过门恰一次/治疗抬血不回锁/阈值式判别 max_hp=8 value=35:hp=3 触发 hp=4 不触发)----
Kit.check_quiet(int(u.hp) == 15 and ec.check_trigger_unlocks(u).is_empty(),
"V4 闩已置:二次复查零新解锁(过门恰一次)〔触发器批 V4〕")
u["hp"] = int(u.max_hp) # 治疗抬回满血
Kit.check_quiet(ec.check_trigger_unlocks(u).is_empty() and _has(ec.rollable_skills(u), "venom_curb"),
"V4 治疗抬血不回锁(单向闩)〔触发器批 V4〕")
var ec8 = load("res://core/unit_container.gd").new(db)
Kit.check_quiet(ec8.spawn(1, 0, 8, [], 6, 8, [], "", "", "battle"), "V4 前置:地精 spawn(hp 基线 8)")
var g = ec8.all_enemies()[0]
g["pool"] = [{"skill_id": "attack", "weight": 9, "cooldown_left": 0},
{"skill_id": "desperate_guard", "weight": 1, "cooldown_left": 0,
"trigger": {"fact": "self_hp_pct", "op": "lte", "value": 35, "latch": false}}]
g["hp"] = 4
Kit.check_quiet(_not_has(ec8.rollable_skills(g), "desperate_guard"),
"V4 阈值式判别:hp=4 > T=round(8×35/100)=3 不入可摇集合〔触发器批 V4〕")
g["hp"] = 3
Kit.check_quiet(_has(ec8.rollable_skills(g), "desperate_guard"),
"V4 阈值式判别:hp=3 ≤ T=3 入可摇集合(取整读数判别会在此漂移——阈值式钉死)〔触发器批 V4〕")
# ---- V6 事实面供给(负向 blast 递增/正向 blast 不递增/轮末转存/开战清零+facts 组包四键在位)----
var RigS = load("res://core/battle_orchestrator.gd")
var orch = RigS.new(db, _rng(700001))
Kit.check_quiet(orch.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8,
"hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [],
"feats": [], "tree_rules": [], "battle_index": 1, "voucher_rap": 0, "voucher_time_sand": 0,
"node_type": "battle", "adv_bucket": {}}), "V6 前置:战斗受理")
# facts 组包经真实摇号路径消费(mines 全零时条件技 sneak_ambush 不入集合——facts 四键在位的可观察面)
Kit.check_quiet(orch._mines_hit_total == 0 and orch._mines_hit_last == 0 and orch._mines_hit_round == 0,
"V6 开战清零:三计数全零起〔触发器批 V6〕")
# 正向 blast(spark_aoe)不递增——负向计数守卫
orch._apply_flat_row({"cell_type": "blast", "polarity": "positive", "power": 2,
"display_name": "爆裂", "reward_id": "spark_aoe"}, "reward", Vector2i(-1, -1), 1)
Kit.check_quiet(orch._mines_hit_total == 0 and orch._mines_hit_round == 0,
"V6 正向 blast(spark_aoe)不递增(负向守卫——清图/爆裂格零误计)〔触发器批 V6〕")
# 负向 blast(雷)递增
orch._apply_flat_row({"cell_type": "blast", "polarity": "negative", "power": 2,
"display_name": "警觉", "reward_id": "enemy_alert"}, "reward", Vector2i(-1, -1), 1)
Kit.check_quiet(orch._mines_hit_total == 1 and orch._mines_hit_round == 1,
"V6 负向 blast(雷)递增恰一次〔触发器批 V6〕")
# ---- V7 池构成纪律(现行全表零违例+构造全条件池报错——跨表校验真实入口)----
var cv = load("res://core/config_db_cross_validator.gd").new(db)
var pool_errs: Array = cv.validate_skill_pool_links(db.skill, db.enemy, db.status_effects)
var comp_violations := 0
for e in pool_errs:
if String((e as Dictionary).get("category", "")) == "pool_composition" and String((e as Dictionary).get("message", "")).contains("无条件特殊技"):
comp_violations += 1
Kit.check_quiet(comp_violations == 0, "V7 现行全表零违例(每池至少一行无条件特殊技)〔触发器批 V7〕")
var enemy_backup: Array = db.enemy.rows.duplicate(true)
var cond_only_row: Dictionary = (db.enemy.rows[0] as Dictionary).duplicate(true)
var cond_pool: Array[Variant] = []
for entry in (cond_only_row.get("pool", []) as Array):
var sid := String((entry as Dictionary).get("skill_id", ""))
var srow_c: Dictionary[Variant, Variant] = db.skill_row(sid)
if not srow_c.is_empty() and String(srow_c.get("type", "")) == "basic":
cond_pool.append(entry) # 普攻保留(普攻不计入无条件特殊技)
cond_pool.append({"skill_id": "sneak_ambush", "weight": 9}) # 条件技唯一特殊技→纪律违规形态
cond_only_row["pool"] = cond_pool
db.enemy.rows[0] = cond_only_row
var errs2: Array = cv.validate_skill_pool_links(db.skill, db.enemy, db.status_effects)
var hit2 := false
for e in errs2:
if String((e as Dictionary).get("message", "")).contains("无条件特殊技"):
hit2 = true
Kit.check_quiet(hit2, "V7 构造全条件池报 pool_composition 错〔触发器批 V7〕")
db.enemy.rows = enemy_backup
# ---- V8 示范投放:低血防御(地精两行池 w1+trigger lte 35——真实表 spawn 压 hp 开轮摇号)----
var ec_v8 = load("res://core/unit_container.gd").new(db)
Kit.check_quiet(ec_v8.spawn(1, 0, 8, [], 6, 8, [], "", "", "battle"), "V8 前置:地精 spawn")
var g8 = ec_v8.all_enemies()[0]
var has_dg := false
for e in (g8.get("pool", []) as Array):
if String((e as Dictionary).get("skill_id", "")) == "desperate_guard":
has_dg = true
Kit.check_quiet(has_dg and int(g8.max_hp) == 8,
"V8 前置:地精池含孤注守势 w1(真实表读值——阈值 T=round(8×35/100)=3)〔触发器批 V8〕")
Kit.check_quiet(_not_has(ec_v8.rollable_skills(g8), "desperate_guard"),
"V8 hp=8>3 不入可摇集合〔触发器批 V8〕")
g8["hp"] = 3
Kit.check_quiet(_has(ec_v8.rollable_skills(g8), "desperate_guard"),
"V8 hp=3≤3 入可摇集合(真实入口开轮摇号判据)〔触发器批 V8〕")
# ---- V9 示范投放:停手偷袭(钩爪恐魔池 w2+last_round_end eq stopped——facts 缺键=拦 fail-safe)----
var ec_v9 = load("res://core/unit_container.gd").new(db)
Kit.check_quiet(ec_v9.spawn(3, 1, 30, [], 8, 30, [], "", "", "battle"), "V9 前置:钩爪恐魔 spawn")
var h9 = ec_v9.all_enemies()[0]
Kit.check_quiet(_not_has(ec_v9.rollable_skills(h9), "sneak_ambush"),
"V9 首轮(facts 缺 last_round_end 键=fail-safe 拦)不入集合〔触发器批 V9〕")
Kit.check_quiet(_not_has(ec_v9.rollable_skills(h9, {"last_round_end": "cleared", "round_index": 2,
"mines_hit_last": 0, "mines_hit_total": 0}), "sneak_ambush"),
"V9 cleared 轮后不入集合〔触发器批 V9〕")
Kit.check_quiet(_has(ec_v9.rollable_skills(h9, {"last_round_end": "stopped", "round_index": 2,
"mines_hit_last": 0, "mines_hit_total": 0}),
"sneak_ambush"), "V9 stopped 轮后入集合(真实表 w2)〔触发器批 V9〕")
# ---- V10 缺省零扰动(无 trigger 行的池快照逐键无新增+摇号同种子不变——快照键策略钉死)----
var ec_v10 = load("res://core/unit_container.gd").new(db)
Kit.check_quiet(ec_v10.spawn(1, 0, 8, [], 6, 8, [], "", "", "battle"), "V10 前置:地精 spawn")
var g10 = ec_v10.all_enemies()[0]
var stray := 0
for e in (g10.get("pool", []) as Array):
for k in (e as Dictionary).keys():
if not ["skill_id", "weight", "cooldown_left", "trigger", "trigger_latched"].has(String(k)):
stray += 1
Kit.check_quiet(stray == 0, "V10 无 trigger 行池快照零新键(快照键策略钉死——防未来给普行塞键)〔触发器批 V10〕")
quit(Kit.finish("trigger_cond_test", "PASS — 触发条件 V1/V2/V4/V6/V7/V8/V9/V10 全绿(V5 对拍由 skill_unlock/round_flow 编排集成承载)"))
func _rng(seed_val: int):
var r = load("res://core/rng_service.gd").new()
r.set_master_seed(seed_val)
return r
func _has(arr: Array, sid: String) -> bool:
for e in arr:
if String((e as Dictionary).get("skill_id", "")) == sid:
return true
return false
func _not_has(arr: Array, sid: String) -> bool:
return not _has(arr, sid)