iter54-卡3:精英专属池批(elite 行类与字段互锁/dangling_base/effective_pool 池继承合并/ENCOUNTER_ROW_KINDS 寻址门暂不接线/goblin_elite 落表+存量冒烟同步+图鉴导出认得精英行)——对照表 R1-R7 全绿(elite_row_test 六组/五件存量套件直跑绿/生产表 error 诊断 0),T3.8 里程碑出口 --code 六闸全绿【验收通过】

This commit is contained in:
DKChan committed 2026-11-07 03:05:12 +08:00
1 parent 4c424af358
commit f9c9caf56b
14 files changed
+540 -40

No files matched your search

+1
View File
@@ -179,6 +179,7 @@ func _suites() -> Array:
{"path": "res://tests/unit/core/run_time_fatal_test.gd", "expect": 1, "negative": true}, # 卡② wr5/wr6/wr7 运行期违约通道(fatal 封印传导/未知标签/未播种)
{"path": "res://tests/unit/data_config/rng_service_test.gd", "expect": 0},
{"path": "res://tests/unit/data_config/table_structure_guard_test.gd", "expect": 0}, # 表结构守卫宿主(adventure 配额与同调目录/enemy 带键行序/AC 单轨与掷点参数/variation 门控互锁/difficulty 修饰/reward 九型/meta 行族)
{"path": "res://tests/unit/data_config/elite_row_test.gd", "expect": 0}, # 精英行校验/有效池合并/继承不复制/合并池纪律/暂不接线(精英专属池批)
{"path": "res://tests/unit/data_config/rng_stream_boot_test.gd", "expect": 0}, # 启动初始化与统一 RNG 流宿主(data/tables 加载链/统一流黄金向量 m=42/derive_at 恒等/open_stream 取号序/new_adventure 计数器重置——批 B 自 smoke_all 迁入;源 :70 事件日志落盘条按裁决一转办跨系统闭环宿主)
{"path": "res://tests/unit/core/treasure_items_test.gd", "expect": 0}, # 秘宝结构冻结断言面
{"path": "res://tests/unit/core/node_map_generator_test.gd", "expect": 0}, # 节点图生成器产出合格/可复现/岔路取舍/恰一店/归属锁定/战斗保底/出口结构/RNG 纯流面(卡① V1·V3·V5–V7·V9·V10)
@@ -527,7 +527,7 @@ func _smoke_skill_pool_data() -> void:
and is_equal_approx(float(venom.rider_mult), 0.75))
var venom_sole_ok := true
for row in _db.combat.enemy.rows:
for entry in row.pool:
for entry in _db.combat.enemy.effective_pool(row):
if String(entry.skill_id) == "venom_burst" and String(row.enemy_id) != "drider":
venom_sole_ok = false
Kit.check("毒爆独占性(仅蛛化卓尔精灵池引用——Boss 专属池独占技纪律)", venom_sole_ok)
@@ -538,7 +538,7 @@ func _smoke_skill_pool_data() -> void:
and int(snare.buff_duration) == 2 and int(snare.cooldown_beats) == 4)
var snare_sole_ok := true
for row in _db.combat.enemy.rows:
for entry in row.pool:
for entry in _db.combat.enemy.effective_pool(row):
if String(entry.skill_id) == "web_snare" and String(row.enemy_id) != "drider":
snare_sole_ok = false
Kit.check("蛛网束缚独占性(仅蛛化卓尔精灵池引用——实装面收窄,其余敌种不动)", snare_sole_ok)
@@ -557,7 +557,7 @@ func _smoke_skill_pool_data() -> void:
var spit_sole_ok := true # 毒吐只挂巨蜘蛛族且首遇带不投(Q6——教学位池零新招)
var bash_sole_ok := true # 盾击只挂灰矮人族三行
for row in _db.combat.enemy.rows:
for entry in row.pool:
for entry in _db.combat.enemy.effective_pool(row):
var sid := String(entry.skill_id)
if sid == "venom_spit" and not (String(row.enemy_id) == "giant_spider" and int(row.level_index) == 2 \
and int(row.depth_band) > spider_lead_band):
@@ -608,6 +608,8 @@ func _smoke_skill_pool_data() -> void:
"3_boss_11_duergar_king": {"attack": 6, "heavy": 2, "earth_pulse": 2, "rockslide": 2},
"3_boss_12_duergar_king": {"attack": 6, "heavy": 2, "earth_pulse": 2, "rockslide": 2},
"3_boss_13_duergar_king": {"attack": 6, "heavy": 2, "earth_pulse": 2, "rockslide": 2},
# 精英专属池批:首只精英=地精精英(有效池=基础池 goblin 9/1/1+附加技 heavy 2——expect_pools 比对读 effective_pool)
"1_elite_0_goblin_elite": {"attack": 9, "lunge": 1, "desperate_guard": 1, "heavy": 2},
}
var pools_ok := true
for row in _db.combat.enemy.rows:
@@ -616,15 +618,15 @@ func _smoke_skill_pool_data() -> void:
pools_ok = false
continue
var got := {}
for entry in row.pool:
for entry in _db.combat.enemy.effective_pool(row):
got[String(entry.skill_id)] = int(entry.weight)
if got != expect_pools[rk]:
pools_ok = false
Kit.check("二十九行十种池模板 = 建议池行值((L,row_kind,depth_band,enemy_id) 四元键逐行精确比对;L1 地精 9/1/1〔触发条件批+孤注守势 w1〕 · 熊地精 6/2/2 · 酋长 6/2/2;L2 熊地精 6/1/2 · 蜘蛛 6/2/1+毒吐2〔首遇带不投——迭代 24 N2〕 · 重击手 5/2/2 · 蛛化卓尔精灵 6/2/2/2〔Boss〕;L3 灰矮人 6/2/2+盾击2 · 钩爪恐魔 6/2/1/2〔触发条件批+趁隙偷袭 w2〕 · 重装手 5/2/2+盾击2 · 石君王 6/2/2〔earth_pulse〕——全部 provisional;三 Boss 池尾 +1 门控条目〔quake_smash/venom_curb/rockslide 权重 2——半血解锁收编技能行 trigger 闩型——迭代14④ T5/触发条件批迁移〕;混编行集同带多行各一池合法——首批投放 (2,battle,5) 双行〔迭代 18 卡⑤:重击手+巢群〕;迭代 35 覆盖批八行为顶档池克隆,逐行同检)", pools_ok)
Kit.check("三十行十一种池模板 = 建议池行值((L,row_kind,depth_band,enemy_id) 四元键逐行精确比对;L1 地精 9/1/1〔触发条件批+孤注守势 w1〕 · 熊地精 6/2/2 · 酋长 6/2/2;L2 熊地精 6/1/2 · 蜘蛛 6/2/1+毒吐2〔首遇带不投——迭代 24 N2〕 · 重击手 5/2/2 · 蛛化卓尔精灵 6/2/2/2〔Boss〕;L3 灰矮人 6/2/2+盾击2 · 钩爪恐魔 6/2/1/2〔触发条件批+趁隙偷袭 w2〕 · 重装手 5/2/2+盾击2 · 石君王 6/2/2〔earth_pulse〕——全部 provisional;三 Boss 池尾 +1 门控条目〔quake_smash/venom_curb/rockslide 权重 2——半血解锁收编技能行 trigger 闩型——迭代14④ T5/触发条件批迁移〕;混编行集同带多行各一池合法——首批投放 (2,battle,5) 双行〔迭代 18 卡⑤:重击手+巢群〕;迭代 35 覆盖批八行为顶档池克隆,逐行同检)", pools_ok)
var basic_count_ok := true
for row in _db.combat.enemy.rows:
var n := 0
for entry in row.pool:
for entry in _db.combat.enemy.effective_pool(row):
var srow: Dictionary = _db.combat.skill.row_by_id(String(entry.skill_id))
if not srow.is_empty() and String(srow.type) == "basic":
n += 1
@@ -637,7 +639,7 @@ func _smoke_skill_pool_data() -> void:
for row in _db.combat.enemy.rows:
var n_buff := 0
var n_debuff := 0
for entry in row.pool:
for entry in _db.combat.enemy.effective_pool(row):
var srow: Dictionary = _db.combat.skill.row_by_id(String(entry.skill_id))
if srow.is_empty():
continue
@@ -652,7 +654,7 @@ func _smoke_skill_pool_data() -> void:
var uncond_ok := true
for row in _db.combat.enemy.rows:
var n_uncond := 0
for entry in row.pool:
for entry in _db.combat.enemy.effective_pool(row):
var srow_u: Dictionary = _db.combat.skill.row_by_id(String(entry.skill_id))
if not srow_u.is_empty() and String(srow_u.type) != "basic" and not srow_u.has("trigger"):
n_uncond += 1
@@ -711,6 +711,7 @@ func _smoke_level_graph() -> void:
# ⑫ 混编行集运行面正反断言:注入带键 (1,battle,5)/(1,battle,6) =
# L1 节点图与全部既有扫描面之外的新带键(带命中精确取注入行集——新 L 会被 ceil 封顶到 L3 不可用),
# 断言后立即还原表,后续断言面零污染。R3 fail-fast 正反验证 + R6 行序运行面验证。
var base_n: int = _db.combat.enemy.rows.size() # 注入前基线行数(生产表行数不写死——精英行随批入表)
var mix_row_a := {"level_index": 1, "row_kind": "battle", "depth_band": 5, "enemy_id": "goblin", "display_name": "地精", "unit_count": 1, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]}
var mix_row_b := {"level_index": 1, "row_kind": "battle", "depth_band": 5, "enemy_id": "bugbear", "display_name": "熊地精", "unit_count": 2, "hp": 20, "atk": 3, "pool": [{"skill_id": "attack", "weight": 9}]}
_db.combat.enemy.rows.append(mix_row_a)
@@ -729,7 +730,7 @@ func _smoke_level_graph() -> void:
_db.combat.enemy.rows.remove_at(_db.combat.enemy.rows.size() - 1)
_db.combat.enemy.rows.remove_at(_db.combat.enemy.rows.size() - 1)
_db.combat.enemy.rows.remove_at(_db.combat.enemy.rows.size() - 1)
Kit.check("混编断言段注入行全量还原(表回 29 行——L1 混编批后的基线行数;首批混编投放基线 21 行+覆盖 8 行)", int(_db.combat.enemy.rows.size()) == 29)
Kit.check("混编断言段注入行全量还原(表回注入前基线行数 %d——相对基数断言,生产表行数不写死)" % base_n, int(_db.combat.enemy.rows.size()) == base_n)
# ⑬ 真图类型可见性(信息迷雾 v2.1):L2 入口停点纯核心面读数——候选 ⊆ 可见集 /
# 终点恒可见 / 入口 BFS 层距 > N 的非豁免节点入雾。N 取数据表(代码零硬编码)。
@@ -256,16 +256,16 @@ func _test_rowset_discipline() -> void:
if String(e.get("category", "")) == "domain" and String(e.get("message", "")).contains("unit_count"):
dom_hit = true
Kit.check("单行数量域 [1,3] 域闸不回归(UNIT_COUNT_MAX 语义 = 单种数)", dom_hit)
# 负向:row_kind 封闭枚举(非法值拒——键面换代后新增判据)
# 负向:row_kind 封闭枚举(非法值拒——键面换代后新增判据;elite 随精英专属池批转合法,非法样本换 miniboss)
var t_kind = EnemyTableScript.new()
t_kind.rows.assign([mk_row.call(1, "elite", 5, "goblin"), mk_row.call(1, "battle", 0, "goblin"),
t_kind.rows.assign([mk_row.call(1, "miniboss", 5, "goblin"), mk_row.call(1, "battle", 0, "goblin"),
mk_row.call(1, "boss", 9, "ogre_chief")])
errs = t_kind.validate()
var kind_hit := false
for e in errs:
if String(e.get("category", "")) == "domain" and String(e.get("message", "")).contains("row_kind"):
kind_hit = true
Kit.check("非法 row_kind 被拒(封闭枚举 {battle, boss})〔fv7〕", kind_hit)
Kit.check("非法 row_kind 被拒(封闭枚举 {battle, boss, elite})〔fv7〕", kind_hit)
# 负向:anchor 两半——缺 battle band 0 锚行、缺 Boss 行集,各自报 anchor
var t_no_anchor = EnemyTableScript.new()
t_no_anchor.rows.assign([mk_row.call(1, "battle", 5, "goblin"), mk_row.call(1, "boss", 9, "ogre_chief")])
@@ -0,0 +1,305 @@
# tests/unit/data_config/elite_row_test.gd — 精英行单元套(精英专属池批)
# 断言面 = 精英行加载期校验(字段互锁/引用校验/错误类别)·有效池合并(基础池取深度带最小行
# +附加技)·继承不复制(改基础池传到精英、改写返回不回灌)·合并池过池构成纪律(跨表件)·
# 暂不接线(寻址行类门只放 battle/boss,elite 行静置合法)·生产表 goblin_elite 行(随 T3 落表追加)。
# 断言从公开入口进出:EnemyTable.validate()/effective_pool()/validate_skill_pool_links()/
# resolve_enemy_rowset()/ConfigDB.load_tables()。夹具一律 rows.assign([...])(定型数组直赋会触发
# 脚本错误静默中断——config_db_test 同款坑注)。
# 运行:godot --headless --path game --script res://tests/unit/data_config/elite_row_test.gd(expect 0)
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd")
const EnemyTableScript := preload("res://data/schemas/enemy_table.gd")
const ValidatorScript := preload("res://core/config_db_cross_validator.gd")
const SkillTableScript := preload("res://data/schemas/skill_table.gd")
const StatusTableScript := preload("res://data/schemas/status_effect_table.gd")
const ResolverScript := preload("res://core/config_db_resolver.gd")
var _diags: Array[String] = [] # T2.4 夹具 resolver 诊断捕获
func _init() -> void:
_test_elite_validation_rejects()
_test_effective_pool_merge()
_test_base_pool_change_propagates()
_test_merged_pool_composition()
_test_elite_unreachable_by_encounter()
_test_production_goblin_elite()
quit(Kit.finish("elite_row_test", "PASS — 精英行校验/有效池/继承不复制/合并池纪律/暂不接线/生产表落行 全过"))
# 行构造助手(照 config_db_test mk_row 写全字段;elite 行形态=去掉 pool、加 base_ref/pool_extra)
func _mk_row(l: int, kind: String, band: int, eid: String) -> Dictionary:
return {"level_index": l, "row_kind": kind, "depth_band": band, "enemy_id": eid, "display_name": eid + "名",
"unit_count": 1, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]}
func _mk_elite_row(l: int, band: int, eid: String, base_ref: String, extra: Array) -> Dictionary:
return {"level_index": l, "row_kind": "elite", "depth_band": band, "enemy_id": eid, "display_name": eid + "名",
"unit_count": 1, "hp": 20, "atk": 2, "base_ref": base_ref, "pool_extra": extra}
# 合法夹具(三行——battle 锚/boss/elite 引用同关 battle 敌种)
func _legal_rows() -> Array:
var goblin := _mk_row(1, "battle", 0, "goblin")
goblin["pool"] = [{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}]
return [goblin, _mk_row(1, "boss", 9, "ogre_chief"),
_mk_elite_row(1, 0, "goblin_elite", "goblin", [{"skill_id": "heavy", "weight": 2}])]
# category 计数(错误列表按类别筛)
func _count_cat(errs: Array, cat: String) -> int:
var n := 0
for e in errs:
if String((e as Dictionary).get("category", "")) == cat:
n += 1
return n
# ---- R1:elite 行类合法与字段互锁、引用校验(合法零错+十一违例各自「含」预期类别)----
func _test_elite_validation_rejects() -> void:
# oracle: design/gdd/systems/战斗循环/敌人系统.md:325 elite 行字段互锁与基础引用违例加载期返回错误
var t_ok = EnemyTableScript.new()
t_ok.rows.assign(_legal_rows())
var errs: Array = t_ok.validate()
Kit.check("合法夹具 validate 零错(battle 锚+boss+elite 引用在位——实得 %s)" % str(errs), errs.is_empty())
# ① elite 删 base_ref → cross_field
var rows1 := _legal_rows()
rows1[2].erase("base_ref")
Kit.check("① elite 删 base_ref 报 cross_field〔精英批 V1〕", _count_cat(_validate_rows(rows1), "cross_field") >= 1)
# ② base_ref 与自身同名 → cross_field
var rows2 := _legal_rows()
rows2[2]["base_ref"] = "goblin_elite"
Kit.check("② base_ref 与 enemy_id 同名报 cross_field〔精英批 V1〕", _count_cat(_validate_rows(rows2), "cross_field") >= 1)
# ③ pool_extra 为空数组 → cross_field
var rows3 := _legal_rows()
rows3[2]["pool_extra"] = []
Kit.check("③ pool_extra 空数组报 cross_field〔精英批 V1〕", _count_cat(_validate_rows(rows3), "cross_field") >= 1)
# ④ elite 删 pool_extra 键 → cross_field
var rows4 := _legal_rows()
rows4[2].erase("pool_extra")
Kit.check("④ elite 删 pool_extra 键报 cross_field〔精英批 V1〕", _count_cat(_validate_rows(rows4), "cross_field") >= 1)
# ⑤ elite 加 pool → cross_field
var rows5 := _legal_rows()
rows5[2]["pool"] = [{"skill_id": "attack", "weight": 9}]
Kit.check("⑤ elite 带 pool 报 cross_field〔精英批 V1〕", _count_cat(_validate_rows(rows5), "cross_field") >= 1)
# ⑥ battle 行加 base_ref → cross_field
var rows6 := _legal_rows()
rows6[0]["base_ref"] = "ogre_chief"
Kit.check("⑥ battle 行带 base_ref 报 cross_field〔精英批 V1〕", _count_cat(_validate_rows(rows6), "cross_field") >= 1)
# ⑦ battle 行加 pool_extra → cross_field
var rows7 := _legal_rows()
rows7[0]["pool_extra"] = [{"skill_id": "heavy", "weight": 2}]
Kit.check("⑦ battle 行带 pool_extra 报 cross_field〔精英批 V1〕", _count_cat(_validate_rows(rows7), "cross_field") >= 1)
# ⑧ base_ref="hobgoblin"(同关无此 battle 敌种)→ dangling_base
var rows8 := _legal_rows()
rows8[2]["base_ref"] = "hobgoblin"
Kit.check("⑧ base_ref 悬空报 dangling_base〔精英批 V1〕", _count_cat(_validate_rows(rows8), "dangling_base") >= 1)
# ⑨ 再加 elite2 行、base_ref 改指它 → dangling_base 且 message 含「elite」
var rows9 := _legal_rows()
rows9.append(_mk_elite_row(1, 0, "goblin_elite2", "goblin", [{"skill_id": "heavy", "weight": 2}]))
rows9[2]["base_ref"] = "goblin_elite2"
var errs9: Array = _validate_rows(rows9)
var has_elite_base := false
for e in errs9:
if String((e as Dictionary).get("category", "")) == "dangling_base" \
and String((e as Dictionary).get("message", "")).contains("elite"):
has_elite_base = true
Kit.check("⑨ base_ref 指向 elite 行报 dangling_base 且 message 含 elite〔精英批 V1〕", has_elite_base)
# ⑩ pool_extra 含基础池已有 lunge → dup_key
var rows10 := _legal_rows()
rows10[2]["pool_extra"] = [{"skill_id": "heavy", "weight": 2}, {"skill_id": "lunge", "weight": 1}]
Kit.check("⑩ 附加技与基础池重复报 dup_key〔精英批 V1〕", _count_cat(_validate_rows(rows10), "dup_key") >= 1)
# ⑪ pool_extra 条目权重 0 → domain 且 message 含 pool_extra
var rows11 := _legal_rows()
rows11[2]["pool_extra"] = [{"skill_id": "heavy", "weight": 0}]
var errs11: Array = _validate_rows(rows11)
var has_extra_domain := false
for e in errs11:
if String((e as Dictionary).get("category", "")) == "domain" \
and String((e as Dictionary).get("message", "")).contains("pool_extra"):
has_extra_domain = true
Kit.check("⑪ pool_extra 权重越域报 domain 且定位含 pool_extra〔精英批 V1〕", has_extra_domain)
func _validate_rows(rows: Array) -> Array:
var t = EnemyTableScript.new()
t.rows.assign(rows)
return t.validate()
# ---- R2:有效池=基础池+附加技,基础池取深度带最小行(深带行在前防取首行假绿)----
func _test_effective_pool_merge() -> void:
# oracle: design/gdd/systems/战斗循环/敌人系统.md:149 精英有效池=同关战斗行类基础敌种深度带最小行的池+附加技
var rows := [
_mk_row(1, "battle", 3, "goblin"), # 深带行在前(取首行的错误实现必红)
_mk_row(1, "battle", 0, "goblin"),
_mk_row(1, "boss", 9, "ogre_chief"),
_mk_elite_row(1, 0, "goblin_elite", "goblin", [{"skill_id": "heavy", "weight": 2}]),
]
rows[0]["pool"] = [{"skill_id": "attack", "weight": 5}, {"skill_id": "charge", "weight": 2}]
rows[1]["pool"] = [{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}]
var t = EnemyTableScript.new()
t.rows.assign(rows)
var got := {}
for e in t.effective_pool(rows[3]):
got[String(e.get("skill_id", ""))] = int(e.get("weight", 0))
Kit.check("精英有效池=浅带基础池+附加技({attack9,lunge1,heavy2}——非深带行池)〔精英批 V2〕",
got.get("attack", 0) == 9 and got.get("lunge", 0) == 1 and got.get("heavy", 0) == 2 and got.size() == 3)
var got_b0 := {}
for e in t.effective_pool(rows[1]):
got_b0[String(e.get("skill_id", ""))] = int(e.get("weight", 0))
Kit.check("battle 行有效池=自身 pool(读法等价面——{attack9,lunge1})〔精英批 V2〕",
got_b0.get("attack", 0) == 9 and got_b0.get("lunge", 0) == 1 and got_b0.size() == 2)
# ---- R3:继承不复制——基础池改表传到精英、改写返回数组不回灌 ----
func _test_base_pool_change_propagates() -> void:
# oracle: design/gdd/systems/战斗循环/敌人系统.md:149 继承不复制——基础池调参自动传到精英
var rows := [
_mk_row(1, "battle", 3, "goblin"),
_mk_row(1, "battle", 0, "goblin"),
_mk_row(1, "boss", 9, "ogre_chief"),
_mk_elite_row(1, 0, "goblin_elite", "goblin", [{"skill_id": "heavy", "weight": 2}]),
]
rows[0]["pool"] = [{"skill_id": "attack", "weight": 5}, {"skill_id": "charge", "weight": 2}]
rows[1]["pool"] = [{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}]
var t = EnemyTableScript.new()
t.rows.assign(rows)
var first: Array = t.effective_pool(rows[3])
for e in first:
if String((e as Dictionary).get("skill_id", "")) == "attack":
e["weight"] = 99 # 改写返回数组条目——表内行不得被回灌
rows[1]["pool"][0]["weight"] = 7 # 基础行(depth_band==0)attack 权重改 7
var got := {}
for e in t.effective_pool(rows[3]):
got[String(e.get("skill_id", ""))] = int(e.get("weight", 0))
Kit.check("基础池改权重 7 传到精英有效池(按需现算零缓存)〔精英批 V3〕", got.get("attack", 0) == 7)
Kit.check("改写返回数组不回灌表内行(基础行 attack 仍=7)〔精英批 V3〕",
int(rows[1]["pool"][0]["weight"]) == 7)
# ---- R4:合并池过池构成纪律(跨表件校验对象=合并后的池)----
func _test_merged_pool_composition() -> void:
# oracle: design/gdd/systems/战斗循环/敌人系统.md:150 池构成纪律校验对象是合并后的池
var skills := SkillTableScript.new()
skills.rows.assign([
{"skill_id": "attack", "type": "basic"},
{"skill_id": "lunge", "type": "damage"},
{"skill_id": "heavy", "type": "damage"},
{"skill_id": "harden", "type": "buff"},
{"skill_id": "charge", "type": "buff"},
])
var status := StatusTableScript.new()
status.rows.assign([]) # 空状态表——buff 无 buff_ref 可解析时 cast_side 不报(cross_validator :179-180)
# 合规精英:基础 attack/lunge+附加 heavy → 恰一普攻/增益≤1/无条件特殊≥1 全合规
var enemy_ok = EnemyTableScript.new()
enemy_ok.rows.assign([
_mk_row(1, "battle", 0, "goblin"),
_mk_row(1, "boss", 9, "ogre_chief"),
_mk_elite_row(1, 0, "goblin_elite", "goblin", [{"skill_id": "heavy", "weight": 2}]),
])
enemy_ok.rows[0]["pool"] = [{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}]
enemy_ok.rows[1]["pool"] = [{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}] # boss 行自带合规池(纪律对全行生效——本组只数精英违规)
var errs_ok: Array = ValidatorScript.new().validate_skill_pool_links(skills, enemy_ok, status)
Kit.check("合规精英合并池 pool_composition=0(实得 %s)" % str(errs_ok), _count_cat(errs_ok, "pool_composition") == 0)
# 附加第二普攻:基础 attack+附加 attack → basic=2 → pool_composition=1 且 message 含 goblin_elite
var enemy_dup_basic = EnemyTableScript.new()
enemy_dup_basic.rows.assign([
_mk_row(1, "battle", 0, "goblin"),
_mk_row(1, "boss", 9, "ogre_chief"),
_mk_elite_row(1, 0, "goblin_elite", "goblin", [{"skill_id": "attack", "weight": 2}]),
])
enemy_dup_basic.rows[0]["pool"] = [{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}]
enemy_dup_basic.rows[1]["pool"] = [{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}] # boss 行自带合规池
var errs_dup: Array = ValidatorScript.new().validate_skill_pool_links(skills, enemy_dup_basic, status)
Kit.check("附加第二普攻报 pool_composition=1 且 message 含 goblin_elite〔精英批 V4〕",
_count_cat(errs_dup, "pool_composition") == 1 and _msgs(errs_dup).any(func(m: String) -> bool: return m.contains("goblin_elite")))
# 附加第二增益:基础 attack/lunge/harden+附加 charge → buff=2 → pool_composition=1
var enemy_dup_buff = EnemyTableScript.new()
enemy_dup_buff.rows.assign([
_mk_row(1, "battle", 0, "goblin"),
_mk_row(1, "boss", 9, "ogre_chief"),
_mk_elite_row(1, 0, "goblin_elite", "goblin", [{"skill_id": "charge", "weight": 2}]),
])
enemy_dup_buff.rows[0]["pool"] = [
{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}, {"skill_id": "harden", "weight": 2}]
enemy_dup_buff.rows[1]["pool"] = [{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}] # boss 行自带合规池
var errs_buff: Array = ValidatorScript.new().validate_skill_pool_links(skills, enemy_dup_buff, status)
Kit.check("合并后增益超 1 报 pool_composition=1 且 message 含 goblin_elite〔精英批 V4〕",
_count_cat(errs_buff, "pool_composition") == 1 and _msgs(errs_buff).any(func(m: String) -> bool: return m.contains("goblin_elite")))
func _msgs(errs: Array) -> Array[String]:
var out: Array[String] = []
for e in errs:
out.append(String((e as Dictionary).get("message", "")))
return out
# ---- R5:暂不接线——表里有 elite 行时寻址拿不到它(行类门改动特有判据)----
func _test_elite_unreachable_by_encounter() -> void:
# oracle: design/gdd/systems/战斗循环/敌人系统.md:151 精英行接线前不会被任何节点读到
_diags.clear()
var resolver = ResolverScript.new(
func(level, category, message) -> void:
_diags.append("%s/%s/%s" % [str(level), str(category), str(message)]),
func() -> Array: return [],
func() -> Array: return [
_mk_row(1, "battle", 0, "goblin"),
_mk_row(1, "boss", 9, "ogre_chief"),
_mk_elite_row(1, 0, "goblin_elite", "goblin", [{"skill_id": "heavy", "weight": 2}]),
],
func(): return null,
)
# ① 请求 elite → 空数组且诊断含 fatal 与 request_oob(只改枚举不改行类门时此请求会返回 goblin_elite——判据绑行类门改动)
var hit: Array = resolver.resolve_enemy_rowset(1, 0, "elite")
var has_fatal_oob := false
for d in _diags:
if d.contains("fatal") and d.contains("request_oob"):
has_fatal_oob = true
Kit.check("请求 elite 返回空且诊断含 fatal/request_oob〔精英批 V5〕", hit.is_empty() and has_fatal_oob)
# ② 同夹具 battle/boss 查询返回行中 elite 计数=0(非空前置:合计行数>0)
var battle_rows: Array = resolver.resolve_enemy_rowset(1, 0, "battle")
var boss_rows: Array = resolver.resolve_enemy_rowset(1, 9, "boss")
var elite_n := 0
for r in battle_rows + boss_rows:
if String((r as Dictionary).get("row_kind", "")) == "elite":
elite_n += 1
Kit.check("battle/boss 查询合计>0 且零 elite 行(非空前置在判据内)〔精英批 V5〕",
battle_rows.size() + boss_rows.size() > 0 and elite_n == 0)
# ---- R6:生产表地精精英落表(真实加载零错+行值逐位+有效池字典)----
func _test_production_goblin_elite() -> void:
# oracle: design/gdd/systems/战斗循环/敌人系统.md:147 精英=敌人表行类为 elite 的独立一行
var db = load("res://core/config_db.gd").new()
var ok: bool = db.load_tables("res://data/tables", true)
Kit.check("生产表 ConfigDB.load_tables 装载(is_ready=%s)〔精英批 V6〕" % str(db.is_ready), ok and db.is_ready)
Kit.check("生产表加载 error 级诊断计数=0(实得 %d)〔精英批 V6〕" % db.diagnostics_count("error"),
db.diagnostics_count("error") == 0)
var elite_rows: Array = []
for row in db.combat.enemy.rows:
if String(row.get("enemy_id", "")) == "goblin_elite":
elite_rows.append(row)
Kit.check("goblin_elite 行恰 1 条(实得 %d)〔精英批 V6〕" % elite_rows.size(), elite_rows.size() == 1)
if elite_rows.is_empty():
return
var r: Dictionary = elite_rows[0]
var checks := {
"level_index": 1, "row_kind": "elite", "depth_band": 0,
"display_name": "精英·地精", "hp": 20, "atk": 2, "atk_min": 1, "atk_max": 3,
"armor_class": 2, "armor_type": "light", "unit_count": 1, "base_ref": "goblin",
}
var bad: Array[String] = []
for k in checks:
if r.get(k) != checks[k]:
bad.append("%s=%s≠%s" % [k, str(r.get(k)), str(checks[k])])
Kit.check("行值 12 字段逐位相等(失配项 %s)〔精英批 V6〕" % [bad if not bad.is_empty() else "无"], bad.is_empty())
Kit.check("无 unit_count_max 键、无 pool 键(精英行形态面)〔精英批 V6〕",
not r.has("unit_count_max") and not r.has("pool"))
var got := {}
for e in db.combat.enemy.effective_pool(r):
got[String(e.get("skill_id", ""))] = int(e.get("weight", 0))
Kit.check("有效池字典={attack9,lunge1,desperate_guard1,heavy2}(实得 %s)〔精英批 V6〕" % str(got),
got.get("attack", 0) == 9 and got.get("lunge", 0) == 1 and got.get("desperate_guard", 0) == 1 and got.get("heavy", 0) == 2 and got.size() == 4)
@@ -1,7 +1,7 @@
# tests/unit/data_config/table_structure_guard_test.gd — 表结构守卫单元测试宿主
# 断言面 = data/tables 各表的结构锚与守卫,七套:
# adventure 冒险表(档位配额/件值对/同调目录 12 件/装配→战斗贯通)
# enemy 敌人表(29 行带键行序/混编行集/三 Boss 行)
# enemy 敌人表(30 行带键行序/混编行集/三 Boss 行/精英行)
# AC 单轨与掷点参数(status_effect/reward/settlement 三处无双轨/免伤墙护栏/敌 AC 带宽)
# variation 地形变异(行数/候选池门控/跨字段互锁负样本)
# difficulty 难度表(空表零行为/修饰套乘/越域 clamp)
@@ -208,18 +208,18 @@ func _smoke_adventure_table() -> void:
v18_ok and locked_n == 12)
# ---- 敌人表深度带键结构(迭代 34 建·L1 混编批换代:29 行键序=覆盖表序·(1,3) 混编行集·Boss 行集带键清单)----
# ---- 敌人表深度带键结构(迭代 34 建·L1 混编批换代:30 行键序=覆盖表序·(1,3) 混编行集·Boss 行集带键清单·精英行)----
func _smoke_enemy_bands() -> void:
# oracle: design/gdd/systems/冒险循环/难度曲线与Boss.md:316 难度与敌人带校验(敌人带组)
print("--- 敌人表深度带键结构(29 行键序 · (1,3) 混编 · Boss 带键覆盖表)---")
# ① 29 行计数 + 键序 = 覆盖表序(文件行序不变——行序即契约;(1,3) 两行相邻、熊地精在前)
print("--- 敌人表深度带键结构(30 行键序 · (1,3) 混编 · Boss 带键覆盖表 · 精英行)---")
# ① 30 行计数 + 键序 = 覆盖表序(文件行序不变——行序即契约;(1,3) 两行相邻、熊地精在前;精英行随精英专属池批)
# 迭代 35 生成图深度扩面覆盖批随动:战斗带顶扩到各关最大生成深度(L1→6/L2→9/L3→13)、
# Boss 行按终点深度分带(L1 6/L2 8·9/L3 11·12·13),模板时代带键 4/9/9 全部在册不动
var expect_keys: Array[String] = [
"1|battle|0|goblin", "1|battle|1|goblin", "1|battle|2|bugbear", "1|battle|3|bugbear", "1|battle|3|goblin",
"1|battle|6|bugbear",
"1|boss|4|ogre_chief", "1|boss|6|ogre_chief",
"1|boss|4|ogre_chief", "1|boss|6|ogre_chief", "1|elite|0|goblin_elite",
"2|battle|0|bugbear", "2|battle|1|bugbear", "2|battle|4|giant_spider",
"2|battle|5|bugbear", "2|battle|5|giant_spider", "2|battle|6|giant_spider", "2|battle|7|giant_spider",
"2|battle|9|giant_spider",
@@ -232,8 +232,8 @@ func _smoke_enemy_bands() -> void:
for row in _db.combat.enemy.rows:
got_keys.append("%d|%s|%d|%s" % [int(row.get("level_index", -1)), String(row.get("row_kind", "")),
int(row.get("depth_band", -1)), String(row.get("enemy_id", ""))])
Kit.check("敌人表 29 行且键序 = 覆盖表序((1,3) 两行相邻、熊地精在前)〔L1混编 v1〕",
_db.combat.enemy.rows.size() == 29 and got_keys == expect_keys)
Kit.check("敌人表 30 行且键序 = 覆盖表序((1,3) 两行相邻、熊地精在前;精英行接 boss 行后)〔L1混编 v1〕",
_db.combat.enemy.rows.size() == 30 and got_keys == expect_keys)
# ② (1,3) 混编构成与行序+静态最坏骰和+行值整行复用(R1-R4)
var rowset13: Array[Dictionary] = _db.combat.enemy.resolve_rowset(1, 3, "battle")
var ids13: Array[String] = []
@@ -386,6 +386,8 @@ func _smoke_ac_swap_guard() -> void:
and l12_pool == {"attack": 6, "charge": 2, "heavy": 2})
var l1_totals := {}
for row in _db.combat.enemy.rows:
if String(row.get("row_kind", "")) == "elite":
continue # 曲线护栏管遭遇曲线——elite 不进遭遇(敌人系统.md:151),不跳过则 band0 失真
if int(row.get("level_index", -1)) != 1:
continue
var cnt_lo: int = int(row.get("unit_count", 1))