# tests/unit/contracts/system_contracts_test.gd — 系统间接口契约断言(宁缺勿错——只断言已定稿行为) # 运行:godot --headless --path game --script res://tests/unit/contracts/system_contracts_test.gd # 契约线清单来源:design/engineering/architecture/architecture.md「API Boundaries」表 + 各系统 AC 前言(规范源)。 # 覆盖面: # 各系统→D2 ConfigDB 类型化单点(TABLE_MANIFEST 词表封闭——伤害单据 kind/目标规则/结账政策常量); # D2↔B1 掷点用途标签 ∈ RngService 封闭词表(EC12——settlement.armor_roll_stream 成员性); # B1→D1 行协议公共字段形态(open_ticket 九栏缺省 + 协议公共键); # B4→B3 增益授予布尔受理口径(域闭 EC1 / 未知 id EC2 / 受理后投影)。 extends SceneTree const ConfigDBScript := preload("res://core/config_db.gd") const RngServiceScript := preload("res://core/rng_service.gd") const StatusContainerScript := preload("res://core/status_container.gd") const UnitContainerScript := preload("res://core/unit_container.gd") const DamageControllerScript := preload("res://core/damage_controller.gd") const BattleFactsScript := preload("res://core/battle_facts.gd") const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:_check/汇总退出样板单点 func _init() -> void: # oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组) var db = ConfigDBScript.new() var load_ok: bool = db.load_tables("res://data/tables", true) Kit.check("前置:表加载零 error", load_ok and db.diagnostics_count("error") == 0) if load_ok: _test_damage_controller_vocabularies() _test_rng_tag_closure(db) _test_ticket_nine_columns() _test_status_boolean_acceptance(db) _test_status_individual_isolation(db) _test_status_grant_all_per_unit(db) _test_status_death_clears(db) _test_status_merge_per_unit(db) _test_status_decay_and_clear(db) _test_status_stack_full_reject(db) _test_status_independent_branch(db) _test_status_snapshot_first_grant(db) quit(Kit.finish("system_contracts_test")) # ---- B1 兑付配方词表:五配方/三目标规则/双结账政策——常量非空且互异(封闭枚举负空间) ---- func _test_damage_controller_vocabularies() -> void: # oracle: design/engineering/architecture/architecture.md:89 (事件/事实/效果类型/算子四词表封闭——契约词表组) # oracle: design/engineering/architecture/architecture.md:89 (事件/事实/效果类型/算子四词表封闭——契约词表组) var kinds: Array = [ DamageControllerScript.KIND_HERO_ATTACK, DamageControllerScript.KIND_ENEMY_SKILL, DamageControllerScript.KIND_BOARD_TYPED, DamageControllerScript.KIND_BOARD_BLAST, DamageControllerScript.KIND_BOARD_FLAT_HERO, ] var nonempty := true for k in kinds: if String(k).is_empty(): nonempty = false Kit.check("五配方常量非空(开单面唯一分派键)", nonempty) var unique_kinds: Dictionary = {} for k in kinds: unique_kinds[String(k)] = true Kit.check("五配方常量互异(词表封闭无别名)", unique_kinds.size() == kinds.size()) var targets: Array = [ DamageControllerScript.TARGET_FRONT_ALIVE, DamageControllerScript.TARGET_HERO, DamageControllerScript.TARGET_ALL_ALIVE, ] var unique_targets: Dictionary = {} for t in targets: unique_targets[String(t)] = true Kit.check("三目标规则常量互异(结账时刻现场解析词表)", unique_targets.size() == targets.size()) Kit.check("双结账政策常量互异(A 即结 / B 攒批回切位)", String(DamageControllerScript.POLICY_IMMEDIATE) != String(DamageControllerScript.POLICY_SEGMENT)) # ---- D2↔B1:掷点用途标签 ∈ RngService 封闭词表(EC12——跨系统字面量契约的成员性断言) ---- func _test_rng_tag_closure(db) -> void: # oracle: design/engineering/architecture/architecture.md:90 (流 scope 封闭词表组) # oracle: design/engineering/architecture/architecture.md:90 (流 scope 封闭词表组) var stream_label: String = String(db.combat.settlement.damage_roll.armor_roll_stream) Kit.check("settlement.armor_roll_stream ∈ RngService 封闭标签词表(EC12)", RngServiceScript.PURPOSE_TAGS.has(stream_label)) var unique_tags: Dictionary = {} for t in RngServiceScript.PURPOSE_TAGS: unique_tags[String(t)] = true Kit.check("RngService 用途标签词表非空且互异(封闭语义由流改标签)", RngServiceScript.PURPOSE_TAGS.size() > 0 and unique_tags.size() == RngServiceScript.PURPOSE_TAGS.size()) # ---- B1→D1 行协议:单据九栏缺省齐备 + 协议公共键形态 ---- func _test_ticket_nine_columns() -> void: # oracle: design/gdd/systems/战斗循环/战斗结算.md:362 (单据九列协议组) # oracle: design/gdd/systems/战斗循环/战斗结算.md:362 (单据九列协议组) var ctrl = _bare_controller() var t: Dictionary = ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit", "契约探针", {"channel": "棋盘兑付", "base_value": 1, "target_rule": DamageControllerScript.TARGET_HERO, "ri": 0}) var columns := ["kind", "source_class", "origin_cell", "round_index", "display", "channel", "attack_type", "base_value", "target_rule", "rider", "highlight", "context", "requires", "source_unit"] var all_present := true for c in columns: if not t.has(c): all_present = false Kit.check("单据九栏 + 可选两栏缺省齐备(开单面只写差异栏前提)", all_present) Kit.check("缺省无类型轴(attack_type 空 = 恒不查克制矩阵)", String(t.attack_type) == "") Kit.check("缺省无前置依赖(requires = -1)", int(t.requires) == -1) func _bare_controller(): var db = ConfigDBScript.new() # 只取 open_ticket 纯构造面——五个依赖一律传真实实例,不用空桩: # DamageController 的 db/rng/units/effects/facts 全是定型标注,传无类型的空桩会触发 # 「Trying to assign value of type '' to a variable of type '…'」脚本错误——该错误不改退出码, # 却让赋值静默失败(成员留 null),脚本错误扫描落地后必红。三者均可零消费构造。 var units = UnitContainerScript.new(db) var effects = StatusContainerScript.new(db) return DamageControllerScript.new(db, RngServiceScript.new(), units, effects, BattleFactsScript.new()) # ---- B4→B3:增益授予布尔受理口径(受理/拒绝二元面 + 投影一致性) ---- func _test_status_boolean_acceptance(db) -> void: # oracle: design/gdd/systems/战斗循环/增益状态.md:250 (受理布尔面组) # oracle: design/gdd/systems/战斗循环/增益状态.md:250 (受理布尔面组) var effects = StatusContainerScript.new(db) # EC1 域闭:notify_battle_start 前授予 = false(受理面封闭) var any_row: Dictionary = {} if db.combat.status_effects.rows.size() > 0: any_row = db.combat.status_effects.rows[0] Kit.check("前置:状态表非空", not any_row.is_empty()) if any_row.is_empty(): return var probe_id: String = String(any_row.effect_id) var src := {"source_class": "safe_cell", "origin_cell": Vector2i(-1, -1), "round_index": 0} Kit.check("域闭授予拒绝(EC1——战斗域外 apply_effect = false)", effects.apply_effect(probe_id, StatusContainerScript.OWNER_HERO, src) == false) Kit.check("未知 effect_id 拒绝(EC2)", effects.apply_effect("no_such_effect_id__", StatusContainerScript.OWNER_HERO, src) == false) effects.notify_battle_start() Kit.check("开域后受理已知 id(布尔受理口径 true 通道)", effects.apply_effect(probe_id, StatusContainerScript.OWNER_HERO, src) == true) var active: Array = effects.query_active(StatusContainerScript.OWNER_HERO) var projected := false for a in active: if String(a.effect_id) == probe_id: projected = int(a.effective_value) == int(a.layers) * int(a.snap_magnitude) Kit.check("受理后投影 effective_value = 层数×授予时快照(抗热重载 EC9 面)", projected) # ---- B4 个体归属契约(增益状态 v1.5——敌方状态个体化批):个体隔离/全体逐体发/死亡即清/同体合并 ---- # 个体键直传字面量("e1"/"e2"/"e3"——拼写约定单点在 StatusContainer.owner_key_for,测试面模拟授予路径逐只授) func _test_status_individual_isolation(db) -> void: # oracle: design/gdd/systems/战斗循环/增益状态.md:251 (按个体归属组) # oracle: design/gdd/systems/战斗循环/增益状态.md:251 (按个体归属组) var effects = StatusContainerScript.new(db) effects.notify_battle_start() var src := {"source_class": "seal_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0} var wrow: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id("weaken") Kit.check("前置:weaken 行在册(累加策略——个体合并语义载体)", not wrow.is_empty() and String(wrow.stack_policy) == "accumulate") Kit.check("个体隔离:A 敌受理", effects.apply_effect("weaken", "e1", src) == true) Kit.check("个体隔离:B 敌受理(同名不同体互不合并)", effects.apply_effect("weaken", "e2", src) == true) Kit.check("个体隔离:A/B 各持一条(各体桶独立)〔个体化批 V1〕", effects.query_active("e1").size() == 1 and effects.query_active("e2").size() == 1) effects.decay_round() # A/B 各自 −1——先验证互不干扰后验证独立倒数 var a_after: Array = effects.query_active("e1") var b_after: Array = effects.query_active("e2") var a_rem: int = int(a_after[0].remaining_settlements) if a_after.size() == 1 else -1 var b_rem: int = int(b_after[0].remaining_settlements) if b_after.size() == 1 else -1 Kit.check("个体隔离:各体独立倒数(A=表值−1、B=表值−1 同步各减)〔个体化批 V1〕", a_rem == int(wrow.duration_settlements) - 1 and b_rem == int(wrow.duration_settlements) - 1) Kit.check("个体隔离:投影携带 owner_key(逐个体承载出户)〔个体化批 V1〕", a_after.size() == 1 and String(a_after[0].owner_key) == "e1" and String(b_after[0].owner_key) == "e2") func _test_status_grant_all_per_unit(db) -> void: # oracle: design/gdd/systems/战斗循环/增益状态.md:252 (授予即生效组) # oracle: design/gdd/systems/战斗循环/增益状态.md:252 (授予即生效组) var effects = StatusContainerScript.new(db) effects.notify_battle_start() var src := {"source_class": "mine_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0} var erow: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id("enrage") Kit.check("前置:enrage 行在册(叠加策略——逐体叠层载体)", not erow.is_empty() and String(erow.stack_policy) == "stack") # 模拟授予路径全体规则(reward_grant._apply_percent_row 同构):循环三敌逐只各发一条 var keys: Array[String] = ["e1", "e2", "e3"] var granted := 0 for k in keys: if effects.apply_effect("enrage", k, src): granted += 1 Kit.check("全体授予逐体发:三敌各受理一条(对全体=逐只各发)〔个体化批 V2〕", granted == 3) var e2_ok: bool = effects.apply_effect("enrage", "e2", src) # e2 再授一层——各体独立合并 Kit.check("全体授予逐体发:同体叠加至 2 层、异体层数不动〔个体化批 V2〕", e2_ok and int(effects.query_active("e2")[0].layers) == 2 and int(effects.query_active("e1")[0].layers) == 1 and int(effects.query_active("e3")[0].layers) == 1) effects.decay_round() var all_decayed := true for k in keys: for eff in effects.query_active(k): if int(eff.remaining_settlements) != int(erow.duration_settlements) - 1: all_decayed = false Kit.check("全体授予逐体发:各体同轮独立衰减 −1(逐桶各减)〔个体化批 V2〕", all_decayed) func _test_status_death_clears(db) -> void: # oracle: design/gdd/systems/战斗循环/增益状态.md:250 (战斗结束清空组) # oracle: design/gdd/systems/战斗循环/增益状态.md:250 (战斗结束清空组) var effects = StatusContainerScript.new(db) effects.notify_battle_start() var src := {"source_class": "seal_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0} effects.apply_effect("weaken", "e1", src) effects.apply_effect("weaken", "e2", src) Kit.check("死亡即清:purge_owner 返回移除数(e1 名下 1 条)〔个体化批 V3〕", effects.purge_owner("e1") == 1) Kit.check("死亡即清:e1 桶清零(重 purge 零移除)〔个体化批 V3〕", effects.query_active("e1").is_empty() and effects.purge_owner("e1") == 0) Kit.check("死亡即清:e2 不受影响(不转移不并回)〔个体化批 V3〕", effects.query_active("e2").size() == 1) var side_all: Array = effects.query_active_side("enemy") Kit.check("死亡即清:侧汇总只含存活个体实例〔个体化批 V3〕", side_all.size() == 1 and String(side_all[0].owner_key) == "e2") func _test_status_merge_per_unit(db) -> void: # oracle: design/gdd/systems/战斗循环/增益状态.md:253 (只登记不去重组) # oracle: design/gdd/systems/战斗循环/增益状态.md:253 (只登记不去重组) var effects = StatusContainerScript.new(db) effects.notify_battle_start() var src := {"source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "round_index": 0} var wrow: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id("weaken") var w_dur: int = int(wrow.duration_settlements) effects.apply_effect("weaken", "e1", src) effects.apply_effect("weaken", "e1", src) Kit.check("同体合并照旧:累加行同体再授时长累加(%d+%d——仍单实例)〔个体化批 V4〕" % [w_dur, w_dur], effects.query_active("e1").size() == 1 and int(effects.query_active("e1")[0].remaining_settlements) == w_dur * 2) var erow: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id("enrage") var max_stack: int = int(erow.max_stack) var granted_n := 0 for i in range(max_stack + 1): if effects.apply_effect("enrage", "e2", src): granted_n += 1 Kit.check("同体合并照旧:叠加行满层拒绝(恰授 max_stack=%d 层)〔个体化批 V4〕" % max_stack, granted_n == max_stack) Kit.check("同体合并照旧:满层时异体仍受理(拒绝只在同体内原子)〔个体化批 V4〕", effects.apply_effect("enrage", "e3", src) == true) # ---- B4 结算链契约(断言补齐批——①增益状态结算链):衰减逐值/战斗结束全清/满层拒绝/独立并存/首授快照 ---- func _test_status_decay_and_clear(db) -> void: # oracle: design/gdd/systems/战斗循环/增益状态.md:21 (先兑付后衰减组) # oracle: design/gdd/systems/战斗循环/增益状态.md:21 (先兑付后衰减组) var effects = StatusContainerScript.new(db) effects.notify_battle_start() var src := {"source_class": "seal_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0} var wrow: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id("weaken") var w_dur: int = int(wrow.duration_settlements) Kit.check("V1 前置:weaken 时长 ≥2(两拍衰减观测面)", w_dur >= 2) Kit.check("V1 前置:e1 受理", effects.apply_effect("weaken", "e1", src)) Kit.check("衰减逐值:首次 decay 返回移除数 0(未归零)〔断言补齐批 V1〕", effects.decay_round() == 0) Kit.check("衰减逐值:每结算恰 −1(remaining=%d−1)〔断言补齐批 V1〕" % (w_dur - 1), effects.query_active("e1").size() == 1 and int(effects.query_active("e1")[0].remaining_settlements) == w_dur - 1) Kit.check("到期移除:第二次 decay 返回移除数 1〔断言补齐批 V2〕", effects.decay_round() == 1) Kit.check("到期移除:归零实例当轮销毁(投影零行)〔断言补齐批 V2〕", effects.query_active("e1").is_empty()) effects.apply_effect("weaken", "e2", src) effects.apply_effect("attack_up", StatusContainerScript.OWNER_HERO, src) Kit.check("V2 前置:两桶各持实例", effects.query_active_side("enemy").size() == 1 and effects.query_active(StatusContainerScript.OWNER_HERO).size() == 1) Kit.check("战斗结束全清:notify_battle_end 返回 true 且全实例清空(两桶零行)〔断言补齐批 V2〕", effects.notify_battle_end() and effects.query_active_side("enemy").is_empty() and effects.query_active(StatusContainerScript.OWNER_HERO).is_empty()) func _test_status_stack_full_reject(db) -> void: # oracle: design/gdd/systems/战斗循环/增益状态.md:255 (满层拒绝原子且留痕组) # oracle: design/gdd/systems/战斗循环/增益状态.md:255 (满层拒绝原子且留痕组) var effects = StatusContainerScript.new(db) effects.notify_battle_start() var src := {"source_class": "seal_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0} var erow: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id("enrage") var max_stack: int = int(erow.max_stack) for i in range(max_stack): effects.apply_effect("enrage", "e1", src) Kit.check("V3 前置:满层(恰 %d 层)" % max_stack, effects.query_active("e1").size() == 1 and int(effects.query_active("e1")[0].layers) == max_stack) var before_layers: int = int(effects.query_active("e1")[0].layers) var before_rem: int = int(effects.query_active("e1")[0].remaining_settlements) Kit.check("满层拒绝:再授返 false〔断言补齐批 V3〕", effects.apply_effect("enrage", "e1", src) == false) Kit.check("满层拒绝:层数与剩余零写入(容器逐字不变——原子拒绝)〔断言补齐批 V3〕", int(effects.query_active("e1")[0].layers) == before_layers and int(effects.query_active("e1")[0].remaining_settlements) == before_rem) func _test_status_independent_branch(db) -> void: # oracle: design/gdd/systems/战斗循环/增益状态.md:257 (合成不设上下限组) # oracle: design/gdd/systems/战斗循环/增益状态.md:257 (合成不设上下限组) var effects = StatusContainerScript.new(db) effects.notify_battle_start() var probe := { "effect_id": "probe_independent_v4", "display_name": "并存试验", "target_side": "enemy", "magnitude": 7, "unit": "flat", "duration_settlements": 2, "stack_policy": "independent", "max_stack": 1, } db.combat.status_effects.rows.append(probe) # 直改已加载表(运行时注入试验行——并存分支零内容行,夹具面) var src := {"source_class": "seal_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0} Kit.check("independent 并存:第一次授予受理〔断言补齐批 V4〕", effects.apply_effect("probe_independent_v4", "e1", src)) Kit.check("independent 并存:第二次授予受理(并存——无合并无满层语义)〔断言补齐批 V4〕", effects.apply_effect("probe_independent_v4", "e1", src)) var both: Array = effects.query_active("e1") Kit.check("independent 并存:两实例各持时长 2 独立倒数〔断言补齐批 V4〕", both.size() == 2 and int(both[0].remaining_settlements) == 2 and int(both[1].remaining_settlements) == 2) db.combat.status_effects.rows.pop_back() # 测毕还原(表行零残留) Kit.check("independent 试验行测毕还原(row_by_id 零命中)", db.combat.status_effects.row_by_id("probe_independent_v4").is_empty()) func _test_status_snapshot_first_grant(db) -> void: # oracle: design/gdd/systems/战斗循环/增益状态.md:258 (快照抗重载组) # oracle: design/gdd/systems/战斗循环/增益状态.md:258 (快照抗重载组) var effects = StatusContainerScript.new(db) effects.notify_battle_start() var src_first := {"source_class": "seal_hit", "origin_cell": Vector2i(3, 4), "round_index": 1} var src_second := {"source_class": "mine_hit", "origin_cell": Vector2i(8, 9), "round_index": 5} effects.apply_effect("weaken", "e1", src_first) effects.apply_effect("weaken", "e1", src_second) # 累加:时长加、归因与快照沿首授 var inst: Array = effects.query_active("e1") Kit.check("首授快照:累加合并仍单实例〔断言补齐批 V5〕", inst.size() == 1) Kit.check("首授快照:origin_cell/round_index/source_class 沿首授(合并实例只持首授归因)〔断言补齐批 V5〕", String(inst[0].source_class) == "seal_hit" and inst[0].origin_cell == Vector2i(3, 4) and int(inst[0].round_index) == 1) Kit.check("首授快照:幅度快照 = 首授时表值(抗热重载域)〔断言补齐批 V5〕", int(inst[0].snap_magnitude) == int(db.combat.status_effects.row_by_id("weaken").magnitude))