227 lines
12 KiB
GDScript
227 lines
12 KiB
GDScript
# 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:
|
||
# oracle: design/gdd/systems/表现与支撑/数据配置.md:198 锻造表装载与就绪态(装载组)
|
||
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:
|
||
# oracle: design/gdd/systems/表现与支撑/数据配置.md:199 校验类别全覆盖:行 schema 正反逐类断言(生产行结构组)
|
||
var rows: Array = db.crossrun.forge.route_rows()
|
||
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.crossrun.forge.default_attack_type == "normal")
|
||
Kit.check("默认甲型轴 = light(常驻持有)〔锻造工坊批 V1〕", db.crossrun.forge.default_armor_type == "light")
|
||
# 类型轴分族与并联双通道前置(4b-1 裁)
|
||
Kit.check("长枪/法杖 ← 长剑练满(并联双通道①)〔锻造工坊批 V1〕",
|
||
String(db.crossrun.forge.route("spear").get("prereq_route", "")) == "sword"
|
||
and String(db.crossrun.forge.route("staff").get("prereq_route", "")) == "sword")
|
||
Kit.check("重甲/魔法护甲 ← 轻甲练满(并联双通道①)〔锻造工坊批 V1〕",
|
||
String(db.crossrun.forge.route("heavy").get("prereq_route", "")) == "light"
|
||
and String(db.crossrun.forge.route("ward").get("prereq_route", "")) == "light")
|
||
Kit.check("四待解锁路线一次性解锁价 = 200〔锻造工坊批 V1〕",
|
||
int(db.crossrun.forge.route("spear").get("unlock_price", 0)) == 200
|
||
and int(db.crossrun.forge.route("staff").get("unlock_price", 0)) == 200
|
||
and int(db.crossrun.forge.route("heavy").get("unlock_price", 0)) == 200
|
||
and int(db.crossrun.forge.route("ward").get("unlock_price", 0)) == 200)
|
||
# 甲型基础 AC(provisional 现役结构值——settlement.tres 矩阵同行)
|
||
Kit.check("甲型基础 AC = 轻 10 / 重 12 / 魔法护甲 9〔锻造工坊批 V1〕",
|
||
int(db.crossrun.forge.route("light").get("base_ac", 0)) == 10
|
||
and int(db.crossrun.forge.route("heavy").get("base_ac", 0)) == 12
|
||
and int(db.crossrun.forge.route("ward").get("base_ac", 0)) == 9)
|
||
# 按类型轴反查(甲路线 id=甲型同值的显式化入口)
|
||
Kit.check("forge_armor_route_by_type(ward) = 魔法护甲锻造〔锻造工坊批 V1〕",
|
||
String(db.crossrun.forge.armor_route_by_type("ward").get("route_id", "")) == "ward")
|
||
var wpn_hit := {}
|
||
for r in db.crossrun.forge.route_rows():
|
||
if String(r.get("kind", "")) == "weapon" and String(r.get("type_axis", "")) == "pierce":
|
||
wpn_hit = r
|
||
break
|
||
Kit.check("forge route_rows 按轴反查武器路线(pierce)=长枪锻造〔锻造工坊批 V1——访问器退役改直扫同判据〕",
|
||
String(wpn_hit.get("route_id", "")) == "spear")
|
||
# 缺行/未知轴 = 空字典合法态
|
||
Kit.check("未知 route_id / 未知类型轴 = 空字典〔锻造工坊批 V1〕",
|
||
db.crossrun.forge.route("blade").is_empty() and db.crossrun.forge.armor_route_by_type("plate").is_empty())
|
||
|
||
|
||
# ---- 几何价格梯(V2 价格面单测半边——购买写在 T3 冒烟)----
|
||
|
||
func _test_price_ladder(db) -> void:
|
||
# oracle: design/gdd/systems/跨局循环/锻造工坊.md:220 定价结构:每级价格随已购级数几何递增、基价与公比读表单点(价格梯组)
|
||
var row: Dictionary = db.crossrun.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)
|
||
# 六路线全量梯(断言补齐批 V11——floor 公式按表值推算不写死表值、逐级严格递增、满级档价=上限档推算值)
|
||
var all_ladder_ok := true
|
||
for frow in db.crossrun.forge.routes:
|
||
var base: int = int(frow["base_price"])
|
||
var ratio: int = int(frow["price_ratio_pct"])
|
||
var cap: int = int(frow["cap"])
|
||
var prev := 0
|
||
for owned in range(cap + 1):
|
||
var expect: int = int(floor(float(base) * pow(float(ratio) / 100.0, float(owned))))
|
||
if ForgeTableScript.level_price(frow, owned) != expect:
|
||
all_ladder_ok = false
|
||
if owned > 0 and expect <= prev:
|
||
all_ladder_ok = false # 逐级严格递增(ratio>100 几何保证)
|
||
prev = expect
|
||
Kit.check("六路线全量梯:floor 公式表值推算逐级严格递增、满级档价=上限档推算值〔断言补齐批 V11〕", all_ladder_ok)
|
||
|
||
|
||
# ---- 负向:出域行逐类构造各一行全部被拒(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:
|
||
# oracle: design/gdd/systems/表现与支撑/数据配置.md:199 校验类别全覆盖(负向 schema 组)
|
||
# 夹具本身须零违例(否则负向判读失效)
|
||
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 种」系设计文档事实快照,
|
||
# 现行 31;本套件零扩签,钉现行数(将来任何加词即转红提示复核)。
|
||
# 32→31:改写族清退批摘 spike(CI run 87 追绿——词表合法收窄的断言随动)。
|
||
|
||
func _test_effect_kinds_closed() -> void:
|
||
# oracle: design/engineering/architecture/architecture.md:89 事件/事实/效果类型/算子四词表封闭(特效闭集组)
|
||
Kit.check("效果引擎效果类型闭集 31 种(甲案词表退役 overflow/attack_segments+改写族清退摘 spike)〔锻造工坊批 V11〕",
|
||
EffectEngineScript.EFFECT_KINDS.size() == 31)
|
||
Kit.check("效果引擎事件词表在册 21 种(charge_full 位摘名——蓄力层删键批)〔锻造工坊批 V11〕",
|
||
EffectEngineScript.EVENTS.size() == 21)
|