Files
gplan-2/game/tests/unit/data_config/forge_table_test.gd
T
DKChan 490ea35c53 工程(迭代51连带面随迁): 四红项测试与标注补改,行为零变更
- 归因: 四条红全部由迭代 51 合回(fb27dc2f)引入,重塑后两笔——
  1cd6e2b1 卡①(effect_engine.gd:14 删 charge_full 词表位)与
  372b4fdc 卡②(damage_controller.gd:50 _init 加 facts_ref 第五参;slot_store.gd:155
  migrate_legacy_save 公开化;breakpoint_store 新增 13 公开读写方法)。
  首现于 run 56(5d7579e7 合回,因动 game/tests/.gates/.gitignore 被判 full 域)——
  迭代 51 之后第一次代码域全量真跑;run 51 倒在导入步(runner 故障)、run 54 实为
  域=docs、run 55 零触发跑闸步 skipped,故这三跑都没照出。
- 随迁面:
  1) forge_table_test.gd:219 EVENTS 闭集计数 22→21(卡①残留面清单 13 处+注释 3 处
     未含测试内闭集计数断言,属卡片考证缺面)。
  2) system_contracts_test.gd 补 BattleFactsScript preload 与 :108 第五参;:102 注释
     「四个依赖」改「五个依赖」(定型标注面同步)。
  3) save_manager_domain_test.gd:481/492 改公开名 migrate_legacy_save。
  4) breakpoint_store.gd:225 bind_save_manager 参数补类型 sm: SaveManagerCore(09 R3)。
- 验证: 三套件单跑 exit 0(forge_table/system_contracts/save_manager_domain);
  audit_type_hints PASS 产品码类型闸归零;--code 档 09 静态扫描已离红名单。
- 一条与本轮无关的本机红在案: settlement_wiring_snapshot_test V1 同种子 golden 对拍
  本机(Windows)红、远端(Mac,同字节 run 56)绿——把码面改动全摘回纯净 master 单跑
  仍红,故非本批引入;golden 平台绑定成因未查,另条登记待裁。
2026-11-04 07:51:06 +08:00

221 lines
12 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.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:
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:
# 夹具本身须零违例(否则负向判读失效)
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;本套件零扩签,钉现行数(将来任何加词即转红提示复核)。
func _test_effect_kinds_closed() -> void:
Kit.check("效果引擎效果类型闭集 32 种(甲案词表退役——overflow/attack_segments 摘名)〔锻造工坊批 V11〕",
EffectEngineScript.EFFECT_KINDS.size() == 32)
Kit.check("效果引擎事件词表在册 21 种(charge_full 位摘名——蓄力层删键批)〔锻造工坊批 V11〕",
EffectEngineScript.EVENTS.size() == 21)