Files
gplan-2/game/tests/unit/data_config/forge_table_test.gd
T
DKChan 072c19b4a5 feat(凭证件裁削): 卡② 同调电池删除+完美保险迁专长+配额净删落地(迭代 31 卡②)
用户 2026-10-18 逐裁四项落码:
- T1/T2 同调电池整件删除:CURIO_KINDS 摘 kind、三字段与三处分发表分支删净、
  runner 侧 _battery_pending/断点键/getter/choose_battery_target 全清(呈现层零接线=有账本无入口)。
- T3 完美保险出池:kind/字段/分发摘除,_pending_insurance 与 level_runner 注入通道
  (voucher_insurance 键、set_voucher_grants 第三参)整链拆除,battle_orchestrator._fx_insurance 删。
- T4 引擎扩签 forgive_seal_miss(EFFECT_KINDS 31→32)+两账相减:新增 _perfect_forgiven 单向累加、
  谓词抽成 perfect_sweep_earned()(有效误标=误标总数−豁免数),_perfect_hit_false 保持纯净=契约 2;
  seal_missed 发射时机与消费点零改动=契约 3(危险直觉·守同事件订阅面在验收组里对拍)。
- T5 feats.tres 第 30 行完美保险专长:rarity rare/cap 省略/domain=battle 显式写出(战斗域首例)/
  desc「本场首次封印落空,不计入完美封印判定」;限次由行 when counter(insurance_used) lt 1 承载,
  do 自带 counter_id 记账由授予段消费点落账(代拍板 3 选型:单行落表,理由见实现报告)。
- T6 配额净删:TIER_QUOTAS rare 9→7、令牌池总量 104→102,validate() 硬断言过;smoke 配额锚/牌库
  构成/专长行数随批换代。
- T7 验收:feat_vouchers_test 39 项全绿(出池反例/断点缺键兼容/账本保留/配额锚/装载/限次一次/
  计数纯净/两账相减真值表/跨战清零/事件不吞)+forge_table 词表闭集断言 31→32。
  变异验证 6 条:5 条证红;「domain battle→adventure」在本 rig 不可观测(未红),已如实登记。
- 存量缺陷顺带修:旧 _emit_row("insurance_used","",…) 空来源会触发行来源断言(旧路径无消费点故从未走红),
  新订阅面给出 source_class=seal_miss。

基线(卡① 合回后 master 4f953ce8):冒烟 1151/0、单元 34 套全绿、探针 --click 全绿(user:// 存档移开态)。
2026-10-19 04:45:49 +08:00

201 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.
# tests/unit/data_config/forge_table_test.gd — 锻造工坊配置表单测
# 覆盖:六路线 schema 零违例(id 唯一/类型轴枚举/值域——出域行逐类构造各一行全部被拒)
# + 满级特效留空(行字段闭集=无特效参数位 + 效果类型闭集 24 种零新增)+ 几何价格梯 20/30/45/67/101。
# 运行:godot --headless --path game --script res://tests/unit/data_config/forge_table_test.gd(expect 0)
# 协议(tests/README):extends SceneTree + quit(code);负向用例资源直构不碰生产表(enemy_table 先例)。
extends SceneTree
const ConfigDBScript := preload("res://core/config_db.gd")
const ForgeTableScript := preload("res://data/schemas/forge_table.gd")
const EffectEngineScript := preload("res://core/effect_engine.gd")
const Kit := preload("res://tests/unit/test_kit.gd")
func _init() -> void:
var db = ConfigDBScript.new()
var load_ok: bool = db.load_tables("res://data/tables", true)
Kit.check("生产 forge.tres 随全表加载 + validate 零违例〔锻造工坊批 V1〕", load_ok and db.diagnostics_count("error") == 0)
if load_ok:
_test_production_rows(db)
_test_price_ladder(db)
_test_negative_cases()
_test_effect_kinds_closed()
quit(Kit.finish("forge_table_test"))
# ---- 生产表六行正向(V1 生产面 + V11 行字段闭集)----
func _test_production_rows(db) -> void:
var rows: Array = db.forge_routes()
Kit.check("六路线行族规模恰 6(三武器+三甲)〔锻造工坊批 V1〕", rows.size() == 6)
var ids := []
for row in rows:
ids.append(String(row.get("route_id", "")))
ids.sort()
Kit.check("路线 id 全集 = {sword, spear, staff, light, heavy, ward}〔锻造工坊批 V1〕",
ids == ["heavy", "light", "spear", "staff", "sword", "ward"])
# V11:行字段闭集——键集恰 12 项,无特效参数位(满级特效本期不做,契约 7)
var expect_keys := ForgeTableScript.ROW_KEYS.duplicate()
expect_keys.sort()
var keys_closed := true
for row in rows:
var ks := (row as Dictionary).keys()
ks.sort()
if ks != expect_keys:
keys_closed = false
Kit.check("六行字段键集恰 = ROW_KEYS 闭集(无特效参数位)〔锻造工坊批 V11〕", keys_closed)
# 初始持有恰长剑+轻甲(语义继承自已退役属性锻造行)
var initial := []
for row in rows:
if bool(row.get("initial_owned", false)):
initial.append(String(row.get("route_id", "")))
initial.sort()
Kit.check("初始持有恰 sword+light〔锻造工坊批 V1〕", initial == ["light", "sword"])
# 默认两轴(契约 2 所有权移交——裸体态 normal/light)
Kit.check("默认攻击类型轴 = normal(常驻持有)〔锻造工坊批 V1〕", db.forge_default_attack_type() == "normal")
Kit.check("默认甲型轴 = light(常驻持有)〔锻造工坊批 V1〕", db.forge_default_armor_type() == "light")
# 类型轴分族与并联双通道前置(4b-1 裁)
Kit.check("长枪/法杖 ← 长剑练满(并联双通道①)〔锻造工坊批 V1〕",
String(db.forge_route("spear").get("prereq_route", "")) == "sword"
and String(db.forge_route("staff").get("prereq_route", "")) == "sword")
Kit.check("重甲/魔法护甲 ← 轻甲练满(并联双通道①)〔锻造工坊批 V1〕",
String(db.forge_route("heavy").get("prereq_route", "")) == "light"
and String(db.forge_route("ward").get("prereq_route", "")) == "light")
Kit.check("四待解锁路线一次性解锁价 = 200〔锻造工坊批 V1〕",
int(db.forge_route("spear").get("unlock_price", 0)) == 200
and int(db.forge_route("staff").get("unlock_price", 0)) == 200
and int(db.forge_route("heavy").get("unlock_price", 0)) == 200
and int(db.forge_route("ward").get("unlock_price", 0)) == 200)
# 甲型基础 AC(provisional 现役结构值——settlement.tres 矩阵同行)
Kit.check("甲型基础 AC = 轻 10 / 重 12 / 魔法护甲 9〔锻造工坊批 V1〕",
int(db.forge_route("light").get("base_ac", 0)) == 10
and int(db.forge_route("heavy").get("base_ac", 0)) == 12
and int(db.forge_route("ward").get("base_ac", 0)) == 9)
# 按类型轴反查(甲路线 id=甲型同值的显式化入口)
Kit.check("forge_armor_route_by_type(ward) = 魔法护甲锻造〔锻造工坊批 V1〕",
String(db.forge_armor_route_by_type("ward").get("route_id", "")) == "ward")
Kit.check("forge_weapon_route_by_type(pierce) = 长枪锻造〔锻造工坊批 V1〕",
String(db.forge_weapon_route_by_type("pierce").get("route_id", "")) == "spear")
# 缺行/未知轴 = 空字典合法态
Kit.check("未知 route_id / 未知类型轴 = 空字典〔锻造工坊批 V1〕",
db.forge_route("blade").is_empty() and db.forge_armor_route_by_type("plate").is_empty())
# ---- 几何价格梯(V2 价格面单测半边——购买写在 T3 冒烟)----
func _test_price_ladder(db) -> void:
var row: Dictionary = db.forge_route("sword")
var prices := []
for owned in range(5):
prices.append(ForgeTableScript.level_price(row, owned))
Kit.check("长剑各级价格 = 20/30/45/67/101(基础 20×150%^已购级数)〔锻造工坊批 V2〕",
prices == [20, 30, 45, 67, 101])
Kit.check("已满级(owned=5)再购价照算 151(cap 闸归写口,价格函数纯几何)〔锻造工坊批 V2〕",
ForgeTableScript.level_price(row, 5) == 151)
# ---- 负向:出域行逐类构造各一行全部被拒(V1)----
func _base_rows() -> Array[Dictionary]:
var out: Array[Dictionary] = []
var mk := func(rid: String, kind: String, axis: String, initial: bool, prereq: String, unlock: int) -> Dictionary:
return {"route_id": rid, "display_name": rid + "锻造", "kind": kind, "type_axis": axis,
"cap": 5, "per_level_gain": 1, "base_ac": 10 if kind == "armor" else 0,
"base_price": 20, "price_ratio_pct": 150, "initial_owned": initial,
"prereq_route": prereq, "unlock_price": unlock}
out.append(mk.call("sword", "weapon", "normal", true, "", 0))
out.append(mk.call("spear", "weapon", "pierce", false, "sword", 200))
out.append(mk.call("staff", "weapon", "magic", false, "sword", 200))
out.append(mk.call("light", "armor", "light", true, "", 0))
out.append(mk.call("heavy", "armor", "heavy", false, "light", 200))
out.append(mk.call("ward", "armor", "ward", false, "light", 200))
return out
func _has_cat(errs: Array, cat: String) -> bool:
for e in errs:
if String(e.get("category", "")) == cat:
return true
return false
# 多字段变异器(lambda 单表达式装不下的用例——命名函数承载)
func _mut_spear_initial(t) -> void:
t.routes[1]["initial_owned"] = true
t.routes[1]["prereq_route"] = ""
t.routes[1]["unlock_price"] = 0
func _test_negative_cases() -> void:
# 夹具本身须零违例(否则负向判读失效)
var t_ok = ForgeTableScript.new()
t_ok.routes = _base_rows()
Kit.check("负向夹具(合法六行)validate 零违例〔锻造工坊批 V1〕", t_ok.validate().is_empty())
var cases := []
# route_id 出域
cases.append(["route_id 出域即拒", func(t): t.routes[0]["route_id"] = "blade", "enum"])
# route_id 重复
cases.append(["route_id 重复即拒", func(t): t.routes[1]["route_id"] = "sword", "dup_key"])
# kind 出域
cases.append(["kind 出域即拒", func(t): t.routes[1]["kind"] = "shield", "enum"])
# 武器类型轴出域
cases.append(["武器 type_axis 出域即拒", func(t): t.routes[1]["type_axis"] = "ward", "enum"])
# 同攻击类型两路线
cases.append(["同攻击类型第二路线即拒", func(t): t.routes[2]["type_axis"] = "normal", "dup_key"])
# cap 越域
cases.append(["cap 越域即拒", func(t): t.routes[0]["cap"] = 0, "domain"])
# 每级增量越域
cases.append(["per_level_gain 越域即拒", func(t): t.routes[0]["per_level_gain"] = -1, "domain"])
# 基础价越域
cases.append(["base_price 越域即拒", func(t): t.routes[0]["base_price"] = 0, "domain"])
# 公比越域
cases.append(["price_ratio_pct 越域即拒", func(t): t.routes[0]["price_ratio_pct"] = 99, "domain"])
# 武器行 base_ac 非零
cases.append(["武器行 base_ac 非零即拒", func(t): t.routes[0]["base_ac"] = 5, "cross_field"])
# 甲行 base_ac 越域
cases.append(["甲行 base_ac 越域即拒", func(t): t.routes[3]["base_ac"] = -1, "domain"])
# 待解锁行无解锁价
cases.append(["待解锁行无一次性解锁价即拒", func(t): t.routes[1]["unlock_price"] = 0, "cross_field"])
# 初始持有行带前置
cases.append(["初始持有行带前置即拒", func(t): t.routes[0]["prereq_route"] = "spear", "cross_field"])
# 初始持有行带解锁价
cases.append(["初始持有行带解锁价即拒", func(t): t.routes[3]["unlock_price"] = 100, "cross_field"])
# 前置出域
cases.append(["prereq_route 出域即拒", func(t): t.routes[1]["prereq_route"] = "blade", "enum"])
# 前置类型不符(武器←甲)
cases.append(["前置类型不符即拒(武器行前置甲路线)", func(t): t.routes[1]["prereq_route"] = "light", "cross_field"])
# 前置自指
cases.append(["前置自指即拒", func(t): t.routes[1]["prereq_route"] = "spear", "cross_field"])
# 规模非六
cases.append(["行族规模非六即拒", func(t): t.routes.pop_back(), "structural"])
# 初始持有非一武器一甲(spear 转初始且清前置/解锁价——隔离出 coverage 单类)
cases.append(["初始持有非一武器一甲即拒", Callable(self, "_mut_spear_initial"), "coverage"])
# 默认轴≠初始路线类型轴
cases.append(["默认攻击轴≠初始武器类型轴即拒", func(t): t.default_attack_type = "pierce", "cross_field"])
# 默认甲轴出域
cases.append(["默认甲轴出域即拒", func(t): t.default_armor_type = "plate", "enum"])
# 同甲型两路线
cases.append(["同甲型第二路线即拒", func(t): t.routes[4]["type_axis"] = "light", "dup_key"])
for c in cases:
var label: String = c[0]
var mutate: Callable = c[1]
var expect_cat: String = c[2]
var t = ForgeTableScript.new()
t.routes = _base_rows()
mutate.call(t)
var errs: Array = t.validate()
Kit.check("锻造 schema 负向:%s〔锻造工坊批 V1〕" % label, _has_cat(errs, expect_cat))
# ---- V11:效果类型闭集零新增(满级特效本期不开新词)----
# 实数订正(L2 在册):卡片 F11「24 种」系 2026-10-07 设计文档事实快照——迭代 20 T1 +7、迭代 22 T3 +1
# 后现行 31;本批零扩签,钉现行数(将来任何一批加词即转红提示复核)。
func _test_effect_kinds_closed() -> void:
Kit.check("效果引擎效果类型闭集 32 种(锻造工坊批零新增口径随迭代 31 卡② 扩签 forgive_seal_miss 换代)〔锻造工坊批 V11〕",
EffectEngineScript.EFFECT_KINDS.size() == 32)
Kit.check("效果引擎事件词表在册 20 种(本批零扩签参照面)〔锻造工坊批 V11〕",
EffectEngineScript.EVENTS.size() == 20)