Files
gplan-2/game/tests/unit/data_config/forge_table_test.gd
T
DKChan 6c2dbc37c1 docs: ADR 体系废除 + architecture.md 确立为架构唯一事实源(2026-10-15 用户令:决策记录归 git commit log,不另存决策文档)
- 删除 design/dev/architecture/adr-0001~0007 共 7 份文件(内容留 git 历史可查)
- architecture.md:删「ADR 审计」「ADR 立项清单」两节;清除全文 ADR-000X 注记、QQ 表/S 表 ADR 引用;未决表关闭已落定项(QQ-01/02/03),立项清单在途事项转登记 QQ-11~14;补 A1 生成姿态事实、autoload 四件清单、board_generator 性能姿态;文档状态立「事实源地位」条
- CLAUDE.md:恢复 RNG 单点硬性红线(去 ADR-0007 引用);新增「架构事实唯一源」红线;规范导航架构行标注唯一事实源
- doc-conventions:§5 增「架构事实唯一源」行;§11 活文档清单补架构文档
- registry:architecture.yaml 删 18 处 adr 字段、autoload_set 补 SaveManager/MetaProgression、board_rending 更新为 Node2D 已落地事实;entities.yaml 12 处引用清除
- game/ 代码注释约 60 处清 ADR-000X 引用(事实与日期保留);豁免标记 ADR-0007-exempt → rng-seed-exempt(main.gd/static_scan.sh/architecture.md/06/iter27 五处同步)
- GDD/qa 实现报告/research 卡片/roadmap/tests README 引用全清;死链校验 1206 链接 0 死链、gdd_lint 0 不通过
- 代码中 adr/adr5 等为 adventure_runner 既有局部变量名(非 ADR 引用),未动
2026-10-15 23:52:30 +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("效果引擎效果类型闭集 31 种零新增(锻造工坊批不开效果类型词)〔锻造工坊批 V11〕",
EffectEngineScript.EFFECT_KINDS.size() == 31)
Kit.check("效果引擎事件词表在册 20 种(本批零扩签参照面)〔锻造工坊批 V11〕",
EffectEngineScript.EVENTS.size() == 20)