test(敌人表深度带): 冒烟与单测断言按 S 清单改写
冒烟全部寻址点改为三参 resolve_enemy_rowset(关卡, 深度, 行类),结算/单元/探针直呼的 spawn 与 begin_battle 显式带行类(battle/boss)——终点战仍按 Boss 行集取数,行为不变。 首遇位断言改为扫表取该 (关卡, battle, 敌种) 的最低深度带,不再写死坐标〔fv21〕;新增 _enemy_bands 结构段验 20 行键序、hobgoblin 留任 (1,battle,3)、三 Boss 行类与带键〔fv11–fv13〕; L2 混编 (2,battle,5) 行序与骰和、巨蜘蛛/bugbear/duergar 首遇带固定数、Boss 行类显式传入 等断言逐条加〔fv18/fv19/fv22–fv24〕尾缀。职业节奏探针与伤害探针同步三参寻址。 变异验证七项全部命中(记录表进实现报告):band_of 反向 floor → fv1/fv3 红;row_kind 枚举 判据注释 → fv7 红;Boss 同带判据删除 → fv15 红;顶带消解判据短路 → fv16 红;巨蜘蛛首遇行 加 unit_count_max → fv22 红;混编两行文件内对调 → fv18/fv11 红;hobgoblin 迁错带键 → fv11/fv12 红(直接迁到带 2 先被加载期同带规则拦下,改用与带 2 行互换带键,表可加载后键位断言红)。
This commit is contained in:
1 parent
07f3eb55e3
commit
61537da889
7 files changed
+410
-165
No files matched your search
@@ -92,7 +92,7 @@ func _run_boss(db, seed_val: int, dig: bool) -> Dictionary:
|
||||
settlement.step_row.connect(func(row): cap["rows"].append(row))
|
||||
settlement.settlement_snapshot.connect(func(s): cap["snaps"].append(s))
|
||||
settlement.battle_finished.connect(func(p): cap["result"] = String(p.get("result", "")))
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(db.hero.max_hp)}):
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(db.hero.max_hp), "node_type": "boss"}):
|
||||
cap["result"] = "begin_failed"
|
||||
return cap
|
||||
var rounds := 0
|
||||
@@ -266,7 +266,7 @@ func _encounter_prediction(db, seed_val: int) -> Dictionary:
|
||||
if t != "battle" and t != "boss":
|
||||
continue
|
||||
var depth := int(node.get("node_depth", -1))
|
||||
var row: Dictionary = db.resolve_enemy_rowset(1, depth)[0] # 行集首行(行集多行批 T2 机械适配)
|
||||
var row: Dictionary = db.resolve_enemy_rowset(1, depth, t)[0] # 行集首行(row_kind 随节点类型)
|
||||
var lo := int(row.get("unit_count", 1))
|
||||
var hi := int(row.get("unit_count_max", lo))
|
||||
var rolled := lo
|
||||
@@ -404,20 +404,22 @@ func _scan_spawns(db, seed0: int, n_seeds: int) -> Dictionary:
|
||||
for node in _level_row(db, 1).get("nodes", []):
|
||||
var t := String(node.get("type", ""))
|
||||
if t == "battle" or t == "boss":
|
||||
depths.append(int(node.get("node_depth", -1)))
|
||||
depths.append({"depth": int(node.get("node_depth", -1)), "kind": t})
|
||||
var total := 0
|
||||
var bad := 0
|
||||
for i in range(n_seeds):
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(seed0 + i)
|
||||
for depth in depths:
|
||||
for dep in depths:
|
||||
var depth: int = int(dep.get("depth", -1))
|
||||
var kind: String = String(dep.get("kind", "battle"))
|
||||
total += 1
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(db, rng)
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": int(depth), "hero_hp": int(db.hero.max_hp)}):
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": depth, "node_type": kind, "hero_hp": int(db.hero.max_hp)}):
|
||||
bad += 1
|
||||
continue
|
||||
var n: int = settlement.presentation_snapshot().enemies.size()
|
||||
var row: Dictionary = db.resolve_enemy_rowset(1, int(depth))[0] # 行集首行(同上)
|
||||
var row: Dictionary = db.resolve_enemy_rowset(1, depth, kind)[0] # 行集首行(同上,row_kind 随节点类型)
|
||||
var lo := int(row.get("unit_count", 1))
|
||||
var hi := int(row.get("unit_count_max", lo))
|
||||
if n < 1 or n < lo or n > hi:
|
||||
|
||||
@@ -13,8 +13,8 @@ func _initialize() -> void:
|
||||
var db = load("res://core/config_db.gd").new()
|
||||
db.load_tables("res://data/tables", false)
|
||||
|
||||
# ① 表侧(行集数组取首行,零内容期行为不变)
|
||||
var row = db.resolve_enemy_rowset(1, 1)[0]
|
||||
# ① 表侧(行集数组取首行,战斗节点查 battle 键域)
|
||||
var row = db.resolve_enemy_rowset(1, 1, "battle")[0]
|
||||
print("[1] row(1,1) display=", row.get("display_name"), " unit_count=", row.get("unit_count"),
|
||||
" pool=", row.get("pool"))
|
||||
|
||||
|
||||
+220
-109
@@ -646,6 +646,7 @@ func _run_core_smoke(db, rng) -> void:
|
||||
_rng = rng
|
||||
_smoke_ac_swap_guard()
|
||||
_smoke_enemy_attack_type()
|
||||
_smoke_enemy_bands()
|
||||
_smoke_armor_roll()
|
||||
_smoke_pool_wiring()
|
||||
_smoke_board_generator()
|
||||
@@ -964,24 +965,25 @@ func _smoke_adventure() -> void:
|
||||
print("--- 冒险结构验收(三关序列 · 大奖励 · 摘要五件事 · 账本封闭)---")
|
||||
var autoplay = load("res://smoke/level_autoplay.gd")
|
||||
# ① 表与覆盖义务(序列关数 = 行集关数;行集内每个战斗/Boss 节点跨表精确行集——覆盖义务断言;
|
||||
# 精确行集口径——enemy 侧同坐标多行合法,坐标级键聚合天然兼容)
|
||||
# enemy 侧键域 = (level_index, row_kind, depth_band)——战斗节点查 battle 键域、Boss 节点查 boss 键域,
|
||||
# 同带多行合法,带键聚合天然兼容〔fv24〕)
|
||||
check(int(_db.level.rows.size()) == 3, "序列关数 = 行集关数(三关正式序列生效)")
|
||||
var bg_keys := {}
|
||||
for row in _db.board_gen.rows:
|
||||
bg_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true
|
||||
var en_keys := {}
|
||||
for row in _db.enemy.rows:
|
||||
en_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true
|
||||
en_keys["%d_%s_%d" % [int(row.level_index), String(row.get("row_kind", "")), int(row.get("depth_band", -1))]] = true
|
||||
var miss := 0
|
||||
for row in _db.level.rows:
|
||||
for n in _level_active_nodes(int(row.level_index)):
|
||||
var nt := String(n.get("type", ""))
|
||||
if nt != "battle" and nt != "boss":
|
||||
continue
|
||||
var key := "%d_%d" % [int(row.level_index), int(n.get("node_depth", -1))]
|
||||
if not bg_keys.has(key) or not en_keys.has(key):
|
||||
var key := "%d_%s_%d" % [int(row.level_index), nt, int(n.get("node_depth", -1))]
|
||||
if not bg_keys.has("%d_%d" % [int(row.level_index), int(n.get("node_depth", -1))]) or not en_keys.has(key):
|
||||
miss += 1
|
||||
check(miss == 0, "行集内每个战斗/Boss 节点在 board_gen + enemy 有精确行集(缺失 %d)" % miss)
|
||||
check(miss == 0, "行集内每个战斗/Boss 节点在 board_gen + enemy 有精确行集(缺失 %d——带键 (L,row_kind,depth_band))" % miss)
|
||||
# ② 满配通关 + ④ 三层奖励兑付(三选一恰 2 次 · 回血封顶 · 钥石逐杀 · 候选零重复)
|
||||
var st: Dictionary = autoplay.run_adventure(_db, 42, 900, 400)
|
||||
var sum: Dictionary = st["summary"]
|
||||
@@ -2019,7 +2021,7 @@ func _smoke_seal_ap_exhausted() -> void:
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
var rounds: Array = []
|
||||
settlement.round_started.connect(func(ri): rounds.append(ri))
|
||||
check(settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp)}), "回归战斗启动(Boss 行——D0 20HP 级会被盲挖粉尘秒杀,无法承载封印回归场景)")
|
||||
check(settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}), "回归战斗启动(Boss 行——D0 20HP 级会被盲挖粉尘秒杀,无法承载封印回归场景)")
|
||||
var minesweeper = settlement.minesweeper
|
||||
# 恰花 (AP-1) 点挖掘,留末点给封印(表驱动——AP 从表读不写死)
|
||||
var ap_total: int = int(_db.action_economy.ap_per_round)
|
||||
@@ -2082,6 +2084,55 @@ func _smoke_level() -> void:
|
||||
"模拟后 hero 表恢复基线(atk=%d hp=%d ac=%d——对配置快照)" % [int(_db.hero.atk), int(_db.hero.max_hp), int(_db.hero.armor_class)])
|
||||
|
||||
|
||||
# ---- 敌人表深度带键结构(迭代 34:20 行键迁移 · 键序=迁移表序 · hobgoblin 留任 · 三 Boss 行 kind/band)----
|
||||
|
||||
func _smoke_enemy_bands() -> void:
|
||||
print("--- 敌人表深度带键结构(20 行键序 · hobgoblin 留任 · 三 Boss 行)---")
|
||||
# ① 20 行计数 + 键序 = 迁移表序(文件行序不变——行序即契约)
|
||||
var expect_keys: Array[String] = [
|
||||
"1|battle|0|goblin", "1|battle|1|goblin", "1|battle|2|bugbear", "1|battle|3|hobgoblin",
|
||||
"1|boss|4|ogre_chief",
|
||||
"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|boss|9|drider",
|
||||
"3|battle|0|duergar", "3|battle|1|hook_horror", "3|battle|4|hook_horror", "3|battle|5|duergar",
|
||||
"3|battle|6|hook_horror", "3|battle|7|duergar", "3|boss|9|duergar_king",
|
||||
]
|
||||
var got_keys: Array[String] = []
|
||||
for row in _db.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", ""))])
|
||||
check(_db.enemy.rows.size() == 20 and got_keys == expect_keys,
|
||||
"敌人表 20 行且键序 = 迁移表序((L,row_kind,depth_band,enemy_id) 逐行比对——文件行序不变)〔fv11〕")
|
||||
# ② hobgoblin 缺口行留任随迁(契约 9:不删行不补立绘——缺口登记随行)
|
||||
var hob := {}
|
||||
for row in _db.enemy.rows:
|
||||
if String(row.get("enemy_id", "")) == "hobgoblin":
|
||||
hob = row
|
||||
check(int(hob.get("level_index", -1)) == 1 and String(hob.get("row_kind", "")) == "battle" \
|
||||
and int(hob.get("depth_band", -1)) == 3,
|
||||
"hobgoblin 行在 (1,battle,3) 留任随迁(缺口行不删不补——补齐归敌种内容批)〔fv12〕〔fv25〕")
|
||||
check(FileAccess.get_file_as_string("res://data/tables/enemy.tres").contains("hobgoblin 行为缺口行"),
|
||||
"hobgoblin 缺口登记随行(表文件注释在位——缺口不静默)〔fv25〕")
|
||||
# ③ 三 Boss 行 kind/band 就位(band = 该关图 Boss 节点深度——单终点图数据驱动)
|
||||
var boss_ok := true
|
||||
var boss_detail := ""
|
||||
for L in [1, 2, 3]:
|
||||
var bdepth := _boss_depth(L)
|
||||
var hits := 0
|
||||
for row in _db.enemy.rows:
|
||||
if int(row.get("level_index", -1)) == L and String(row.get("row_kind", "")) == "boss":
|
||||
hits += 1
|
||||
if int(row.get("depth_band", -1)) != bdepth:
|
||||
boss_ok = false
|
||||
boss_detail += " L%d:%d≠%d" % [L, int(row.get("depth_band", -1)), bdepth]
|
||||
if hits != 1:
|
||||
boss_ok = false
|
||||
boss_detail += " L%d:boss 行×%d" % [L, hits]
|
||||
check(boss_ok, "三关 Boss 行各恰一行且 row_kind=boss、band=图 Boss 节点深度(%d/%d/%d%s)〔fv13〕" % [
|
||||
_boss_depth(1), _boss_depth(2), _boss_depth(3), boss_detail])
|
||||
|
||||
|
||||
# ---- 敌方攻击类型轴(表列/单位快照/单据载荷/矩阵命中证词)----
|
||||
# 克制矩阵命中证词 = 同种子三甲型配对实测(不是查表存在性证词):同种子下 armor_roll 流消费逐位一致、
|
||||
# 阈值(hero armor_class)不变,三甲型唯一差异 = 克制点数 cdelta(normal_vs_light 0 / heavy −1 / ward +1)
|
||||
@@ -2211,29 +2262,31 @@ func _smoke_ac_swap_guard() -> void:
|
||||
boss_ac[int(row.get("level_index", 0))] = int(row.get("armor_class", 0))
|
||||
check(int(boss_ac.get(1, 0)) < int(boss_ac.get(2, 0)) and int(boss_ac.get(2, 0)) < int(boss_ac.get(3, 0)),
|
||||
"Boss AC 关卡台阶严格递增(%d < %d < %d)" % [int(boss_ac.get(1, 0)), int(boss_ac.get(2, 0)), int(boss_ac.get(3, 0))])
|
||||
# ③c (1,2) 降档锚定 + 第一关总量曲线护栏
|
||||
# (1,2) 锚定值 hp 22 / ac 2;
|
||||
# ③c 熊地精首遇带降档锚定 + 第一关总量曲线护栏
|
||||
# 首遇带行 = 数据驱动定位(契约 8:该 (1,battle,bugbear) 的最低 depth_band)锚定值 hp 22 / ac 2;
|
||||
# 敌种 bugbear 与池 attack6/charge2/heavy2 钉死不动(防顺手改多);
|
||||
# 曲线护栏 = 相邻节点「hp × 中位数量」总量比 ≤ 2.5(12/10/22/35/72 现行全绿;不用单调不回撤——会红在 (1,0)→(1,1))
|
||||
# 曲线护栏 = 深度带升序相邻带「hp × 中位数量」总量比 ≤ 2.5(12/10/22/35/72 现行全绿;不用单调不回撤——会红在首带→次带)
|
||||
var l12_band := _first_encounter_band(1, "bugbear")
|
||||
var l12_row: Dictionary = {}
|
||||
for row in _db.enemy.rows:
|
||||
if int(row.get("level_index", -1)) == 1 and int(row.get("node_depth", -1)) == 2:
|
||||
if int(row.get("level_index", -1)) == 1 and String(row.get("row_kind", "")) == "battle" \
|
||||
and int(row.get("depth_band", -1)) == l12_band and String(row.get("enemy_id", "")) == "bugbear":
|
||||
l12_row = row
|
||||
break
|
||||
var l12_pool := {}
|
||||
for entry in l12_row.get("pool", []):
|
||||
l12_pool[String(entry.skill_id)] = int(entry.weight)
|
||||
check(String(l12_row.get("enemy_id", "")) == "bugbear" and int(l12_row.get("hp", -1)) == 22 \
|
||||
check(l12_band >= 0 and not l12_row.is_empty() and int(l12_row.get("hp", -1)) == 22 \
|
||||
and int(l12_row.get("armor_class", -1)) == 2 \
|
||||
and l12_pool == {"attack": 6, "charge": 2, "heavy": 2},
|
||||
"第一关 (1,2) 降档锚定(bugbear 保留 · hp 22 · ac 2 · 池 attack6/charge2/heavy2 零改动——迭代 24 N1)")
|
||||
"熊地精首遇带(1,battle,%d)降档锚定(hp 22 · ac 2 · 池 attack6/charge2/heavy2 零改动——迭代 24 N1;首遇带数据驱动定位)〔fv21〕" % l12_band)
|
||||
var l1_totals := {}
|
||||
for row in _db.enemy.rows:
|
||||
if int(row.get("level_index", -1)) != 1:
|
||||
continue
|
||||
var cnt_lo: int = int(row.get("unit_count", 1))
|
||||
var cnt_hi: int = int(row.get("unit_count_max", cnt_lo))
|
||||
l1_totals[int(row.get("node_depth", -1))] = int(row.get("hp", 0)) * (cnt_lo + cnt_hi) / 2.0
|
||||
l1_totals[int(row.get("depth_band", -1))] = int(row.get("hp", 0)) * (cnt_lo + cnt_hi) / 2.0
|
||||
var curve_ok := true
|
||||
var curve_detail := ""
|
||||
var depths_sorted := l1_totals.keys()
|
||||
@@ -2245,7 +2298,7 @@ func _smoke_ac_swap_guard() -> void:
|
||||
if ratio > 2.5:
|
||||
curve_ok = false
|
||||
curve_detail += " (%d→%d):%.2f" % [depths_sorted[i - 1], depths_sorted[i], ratio]
|
||||
check(curve_ok, "第一关相邻节点总量比 ≤ 2.5(12/10/22/35/72——迭代 24 N1 曲线护栏,越带%s)" % [
|
||||
check(curve_ok, "第一关相邻深度带总量比 ≤ 2.5(12/10/22/35/72——迭代 24 N1 曲线护栏,带键升序;越带%s)" % [
|
||||
curve_detail if curve_detail != "" else "无"])
|
||||
# ④ 掷点参数域(加载校验已过——此处钉形状语义)
|
||||
var bounds: Array = _db.settlement.roll_tier_bounds
|
||||
@@ -2618,8 +2671,8 @@ func _smoke_global_variant() -> void:
|
||||
var crng = _CountVariantRng.new()
|
||||
crng.set_master_seed(42)
|
||||
var boss_settlement = load("res://core/battle_orchestrator.gd").new(_db, crng)
|
||||
var boss_ok: bool = boss_settlement.begin_battle({"level_index": 2, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"})
|
||||
check(boss_ok, "L2 Boss 战斗启动(node_depth=4 boss 节点——兜底回落断言载体)")
|
||||
var boss_ok: bool = boss_settlement.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"})
|
||||
check(boss_ok, "L2 Boss 战斗启动(node_depth=9 Boss 终点——兜底回落断言载体)")
|
||||
var boss1_ids: Array = []
|
||||
var boss1_variants: Dictionary = {}
|
||||
var boss1_calls := 0
|
||||
@@ -2643,7 +2696,7 @@ func _smoke_global_variant() -> void:
|
||||
var crng2 = _CountVariantRng.new()
|
||||
crng2.set_master_seed(42)
|
||||
var boss2 = load("res://core/battle_orchestrator.gd").new(_db, crng2)
|
||||
if boss2.begin_battle({"level_index": 2, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}):
|
||||
if boss2.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}):
|
||||
var bb2 = boss2.minesweeper.current_board()
|
||||
check(_arr_str_eq(bb2.board_variant_ids(), boss1_ids) and bb2.variants == boss1_variants and crng2.variant_calls == boss1_calls,
|
||||
"确定性:同种子 Boss 图两遍 board_variants/变异格/取号逐位相同(region 布置路径)")
|
||||
@@ -3022,7 +3075,7 @@ func _smoke_active_refresh() -> void:
|
||||
if String(row.get("type", "")) == "decay" and not bool(ct["waiting_probe_hit"]):
|
||||
ct["waiting_probe_hit"] = true
|
||||
ct["waiting_refresh_rejected"] = not settlement.player_request_refresh())
|
||||
check(settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp)}), "轮内刷新战斗启动(Boss 行 150HP——承载多轮样本)")
|
||||
check(settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}), "轮内刷新战斗启动(Boss 行 150HP——承载多轮样本)")
|
||||
var minesweeper = settlement.minesweeper
|
||||
# 轮 1–3:零挖掘停手攒 RAP = 3×AP(每轮全转存;表驱动——AP 从表读不写死)
|
||||
for i in range(3):
|
||||
@@ -3087,7 +3140,7 @@ func _active_refresh_replay() -> Array:
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
var rows: Array = []
|
||||
settlement.step_row.connect(func(row): rows.append(row))
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp)}):
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}):
|
||||
return rows
|
||||
var minesweeper = settlement.minesweeper
|
||||
for i in range(3):
|
||||
@@ -3112,7 +3165,7 @@ func _smoke_difficulty_table() -> void:
|
||||
var enemy_system = load("res://core/unit_container.gd").new(_db)
|
||||
check(enemy_system.spawn(1, 0, int(_db.hero.max_hp)), "spawn 受理")
|
||||
var unit = enemy_system.all_enemies()[0]
|
||||
var d0: Dictionary = _db.resolve_enemy_rowset(1, 0)[0] # 行集数组口径取首行(零内容期每坐标恰一行,T2 机械适配)
|
||||
var d0: Dictionary = _db.resolve_enemy_rowset(1, 0, "battle")[0] # 行集首行(带键寻址:战斗节点查 battle 键域)
|
||||
check(int(unit.hp) == int(d0.hp) and int(unit.atk) == int(d0.atk), "空表零修饰(hp=%d atk=%d = 行值 %d/%d)" % [int(unit.hp), int(unit.atk), int(d0.hp), int(d0.atk)])
|
||||
# ② 修饰行运行时注入(debug 通道——R3 override 同族):套乘生效于 hp/atk
|
||||
_db.difficulty.rows.append({"level_index": 1, "node_depth": 0, "hp_mult": 1.5, "atk_mult": 2.0})
|
||||
@@ -3140,7 +3193,7 @@ func _smoke_damage_types() -> void:
|
||||
var enemy_system_b = load("res://core/unit_container.gd").new(_db)
|
||||
check(enemy_system_b.spawn(1, 2, int(_db.hero.max_hp)) and String(enemy_system_b.all_enemies()[0].armor_type) == "light", "bugbear 护甲 = light(护甲类型重配批 2026-09-18:关卡 1 全轻甲)")
|
||||
var enemy_system_c = load("res://core/unit_container.gd").new(_db)
|
||||
check(enemy_system_c.spawn(1, 4, int(_db.hero.max_hp)) and String(enemy_system_c.all_enemies()[0].armor_type) == "light", "ogre_chief 护甲 = light(护甲类型重配批 2026-09-18:关卡 1 全轻甲)")
|
||||
check(enemy_system_c.spawn(1, 4, int(_db.hero.max_hp), [], -1, -1, [], "", "boss") and String(enemy_system_c.all_enemies()[0].armor_type) == "light", "ogre_chief 护甲 = light(Boss 行集——护甲类型重配批 2026-09-18:关卡 1 全轻甲)")
|
||||
# ② 克制矩阵表值在位(三档点数 克制 +1 / 中性 0 / 被克 −1,加法核非 percent)
|
||||
var am: Dictionary = _db.settlement.armor_effectiveness
|
||||
check(int(am.normal.light) == 0 and int(am.normal.ward) == 1 and int(am.normal.heavy) == -1, "普通行:轻甲 0 / 魔法护甲 +1 / 重甲 −1(加法核)")
|
||||
@@ -3174,7 +3227,7 @@ func _run_seal_backlash_round(seed_val: int) -> Dictionary:
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
var rows: Array = []
|
||||
settlement.step_row.connect(func(row): rows.append(row))
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp)}):
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}):
|
||||
return {"rows": rows, "safe_cell": Vector2i(-1, -1), "safe_type": ""}
|
||||
var minesweeper = settlement.minesweeper
|
||||
var safe_t := _find_seal_target(minesweeper, false)
|
||||
@@ -3228,7 +3281,7 @@ func _smoke_seal_irreversible_quadrants() -> void:
|
||||
var settle_a = load("res://core/battle_orchestrator.gd").new(_db, rng_a)
|
||||
var rows_a: Array = []
|
||||
settle_a.step_row.connect(func(row): rows_a.append(row))
|
||||
check(settle_a.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp)}), "四情况 A:开局受理")
|
||||
check(settle_a.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}), "四情况 A:开局受理")
|
||||
var ms_a = settle_a.minesweeper
|
||||
var t_a := _find_seal_target(ms_a, false)
|
||||
check(not t_a.is_empty(), "四情况 A:找到安全格封印目标")
|
||||
@@ -3268,7 +3321,7 @@ func _smoke_seal_irreversible_quadrants() -> void:
|
||||
var settle_b = load("res://core/battle_orchestrator.gd").new(_db, rng_b)
|
||||
var rows_b: Array = []
|
||||
settle_b.step_row.connect(func(row): rows_b.append(row))
|
||||
check(settle_b.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp)}), "四情况 B:开局受理")
|
||||
check(settle_b.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}), "四情况 B:开局受理")
|
||||
var ms_b = settle_b.minesweeper
|
||||
var t_b := _find_seal_target(ms_b, true)
|
||||
check(not t_b.is_empty(), "四情况 B:找到危险格封印目标")
|
||||
@@ -3298,7 +3351,7 @@ func _smoke_seal_irreversible_quadrants() -> void:
|
||||
var settle_c = load("res://core/battle_orchestrator.gd").new(_db, rng_c)
|
||||
var rows_c: Array = []
|
||||
settle_c.step_row.connect(func(row): rows_c.append(row))
|
||||
check(settle_c.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp)}), "四情况 C:开局受理")
|
||||
check(settle_c.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}), "四情况 C:开局受理")
|
||||
var ms_c = settle_c.minesweeper
|
||||
var t_safe := _find_seal_target(ms_c, false)
|
||||
var t_mine := _find_seal_target(ms_c, true)
|
||||
@@ -3403,12 +3456,21 @@ func _smoke_encounter_roll() -> void:
|
||||
check(settlement_d1.begin_battle({"level_index": 1, "node_depth": 1, "hero_hp": int(_db.hero.max_hp)}), "D1 战斗启动")
|
||||
var n1: int = settlement_d1.presentation_snapshot().enemies.size()
|
||||
check(n1 >= 2 and n1 <= 3, "D1 区间 [2,3] 域内(%d 只)" % n1)
|
||||
# D2 固定(教学位不加骰)
|
||||
# 熊地精首遇带固定(教学位不加骰)——首遇带数据驱动定位(契约 8,禁写死坐标)
|
||||
var bugbear_lead_band := _first_encounter_band(1, "bugbear")
|
||||
var bugbear_lead_row: Dictionary = {}
|
||||
for row in _db.enemy.rows:
|
||||
if int(row.level_index) == 1 and String(row.row_kind) == "battle" \
|
||||
and int(row.depth_band) == bugbear_lead_band and String(row.enemy_id) == "bugbear":
|
||||
bugbear_lead_row = row
|
||||
break
|
||||
var rng4 = load("res://core/rng_service.gd").new()
|
||||
rng4.set_master_seed(31000)
|
||||
var settlement_d2 = load("res://core/battle_orchestrator.gd").new(_db, rng4)
|
||||
settlement_d2.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.hero.max_hp)})
|
||||
check(settlement_d2.presentation_snapshot().enemies.size() == 1, "D2 熊地精固定 1(首遇教学位)")
|
||||
settlement_d2.begin_battle({"level_index": 1, "node_depth": bugbear_lead_band, "hero_hp": int(_db.hero.max_hp)})
|
||||
check(settlement_d2.presentation_snapshot().enemies.size() == 1 \
|
||||
and int(bugbear_lead_row.get("unit_count", -1)) == 1 and not bugbear_lead_row.has("unit_count_max"),
|
||||
"熊地精首遇带(1,battle,%d——扫表取最低带)固定 1 不加骰〔fv23〕" % bugbear_lead_band)
|
||||
# 全深度 × 多种子域内扫描(playtest-2026-09-12 报告 Bug#1 回归——区间下限 1 不可掷 0:
|
||||
# 战斗结算 roll_lo 下限 + 敌人系统 spawn count<1 fail-fast 双闸;单深度单种子是盲区,全域扫描才钉死不变式)
|
||||
var depths: Array = []
|
||||
@@ -3426,7 +3488,7 @@ func _smoke_encounter_roll() -> void:
|
||||
scan_bad += 1
|
||||
continue
|
||||
var n: int = settlement_scan.presentation_snapshot().enemies.size()
|
||||
var erow2: Dictionary = _db.resolve_enemy_rowset(1, int(depth_v))[0] # 行集首行(T2 机械适配)
|
||||
var erow2: Dictionary = _db.resolve_enemy_rowset(1, int(depth_v), "battle")[0] # 行集首行(扫描面 = 战斗节点)
|
||||
var lo2: int = int(erow2.get("unit_count", 1))
|
||||
var hi2: int = int(erow2.get("unit_count_max", lo2))
|
||||
if n < 1 or n < lo2 or n > hi2:
|
||||
@@ -3434,6 +3496,36 @@ func _smoke_encounter_roll() -> void:
|
||||
check(scan_bad == 0, "全深度 × 12 种子 spawn 全域内(%d 场,∈[unit_count, unit_count_max] 且 ≥1)——空遭遇不可达(Bug#1 回归)" % scan_total)
|
||||
|
||||
|
||||
# 节点类型 → 敌人表 row_kind(非 Boss 战斗位恒 battle——深度带批寻址第三维)
|
||||
func _row_kind_of(level_index: int, depth: int) -> String:
|
||||
for n in _level_active_nodes(level_index):
|
||||
if int(n.get("node_depth", -1)) == depth and String(n.get("type", "")) == "boss":
|
||||
return "boss"
|
||||
return "battle"
|
||||
|
||||
|
||||
# 该关 Boss 节点深度(单终点图的终点编号——数据驱动,禁写死坐标)
|
||||
func _boss_depth(level_index: int) -> int:
|
||||
for n in _level_active_nodes(level_index):
|
||||
if String(n.get("type", "")) == "boss":
|
||||
return int(n.get("node_depth", -1))
|
||||
return -1
|
||||
|
||||
|
||||
# 首遇带数据驱动定位(契约 8):该 (L, battle, enemy_id) 的最低 depth_band,禁写死坐标数字
|
||||
func _first_encounter_band(level_index: int, enemy_id: String) -> int:
|
||||
var best := -1
|
||||
for row in _db.enemy.rows:
|
||||
if int(row.get("level_index", -1)) != level_index:
|
||||
continue
|
||||
if String(row.get("row_kind", "")) != "battle" or String(row.get("enemy_id", "")) != enemy_id:
|
||||
continue
|
||||
var band := int(row.get("depth_band", -1))
|
||||
if best < 0 or band < best:
|
||||
best = band
|
||||
return best
|
||||
|
||||
|
||||
# 模板感知取节点表(row.templates 非空取模板 0=现行固定图,保持既有断言语义)
|
||||
func _level_active_nodes(level_index: int) -> Array:
|
||||
for row in _db.level.rows:
|
||||
@@ -3471,7 +3563,7 @@ func _smoke_attack_chain_fresh_pool() -> void:
|
||||
if row.type == "damage" and row.source_class == "enemy_action":
|
||||
ticks.append({"baseline": int(row.baseline), "attach": int(row.get("attach", 0)), "roll": int(row.roll),
|
||||
"tier": int(row.tier), "crit": bool(row.get("crit", false)), "tick": int(row.value)}))
|
||||
check(settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp)}), "fresh 池回归战斗启动(Boss 行)")
|
||||
check(settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}), "fresh 池回归战斗启动(Boss 行)")
|
||||
# 停手轮:零挖掘零封印 → 零授予 → 池恒空;Boss 拍 = 开轮摇号结果(伤害技/普攻兑付伤害行、硬化施放行不计)
|
||||
for i in range(6):
|
||||
if settlement.battle_result != "":
|
||||
@@ -3538,7 +3630,7 @@ func _smoke_clear_reward_zero_paths() -> void:
|
||||
var s = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
var rows: Array = []
|
||||
s.step_row.connect(func(row): rows.append(row))
|
||||
check(s.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80}),
|
||||
check(s.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80, "node_type": "boss"}),
|
||||
"零兑付构造战斗启动")
|
||||
for u in s._enemy_system.all_enemies():
|
||||
s._enemy_system.apply_damage(u, int(u.hp)) # 测试通道:构造"无存活敌"(弱形式——全灭而非 despawn)
|
||||
@@ -3556,7 +3648,7 @@ func _smoke_clear_reward_zero_paths() -> void:
|
||||
var s_b = load("res://core/battle_orchestrator.gd").new(_db, rng_b)
|
||||
var rows_b: Array = []
|
||||
s_b.step_row.connect(func(row): rows_b.append(row))
|
||||
check(s_b.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80}),
|
||||
check(s_b.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80, "node_type": "boss"}),
|
||||
"高光守卫构造战斗启动")
|
||||
for u in s_b._enemy_system.all_enemies():
|
||||
s_b._enemy_system.apply_damage(u, int(u.hp))
|
||||
@@ -3573,7 +3665,7 @@ func _smoke_clear_reward_zero_paths() -> void:
|
||||
var s2 = load("res://core/battle_orchestrator.gd").new(_db, rng2)
|
||||
var rows2: Array = []
|
||||
s2.step_row.connect(func(row): rows2.append(row))
|
||||
check(s2.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80}),
|
||||
check(s2.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80, "node_type": "boss"}),
|
||||
"第二份被拒构造战斗启动")
|
||||
var boss = s2._enemy_system.all_enemies()[0]
|
||||
boss.hp = 1 # 测试通道:压低至首份 aoe 必杀(弱形式——正常流 Boss 血量不可控)
|
||||
@@ -4044,7 +4136,7 @@ func _run_full_clear(seed_val: int, from_refresh: bool, rows: Array) -> Dictiona
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
settlement.step_row.connect(func(row): rows.append(row))
|
||||
var res := {"cleared": false, "refreshed": false, "rounds": 0}
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH,
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "node_type": "boss",
|
||||
"hero_hp": CLEAR_SMOKE_HERO_HP, "hero_max_hp": CLEAR_SMOKE_HERO_HP}):
|
||||
_db.difficulty.rows.clear()
|
||||
return res
|
||||
@@ -4175,44 +4267,55 @@ func _smoke_skill_pool_data() -> void:
|
||||
check(not bash.is_empty() and String(bash.display_name) == "盾击" and String(bash.type) == "damage" \
|
||||
and is_equal_approx(float(bash.damage_mult), 1.7) and int(bash.cooldown_beats) == 3 and not bash.has("rider_mult"),
|
||||
"盾击行(伤害 ×1.7 · 冷却 3 · 无 rider——迭代 24 N2)")
|
||||
var spit_sole_ok := true # 毒吐只挂巨蜘蛛族且首遇位 (2,4) 不投(Q6——教学位池零新招)
|
||||
var spider_lead_band := _first_encounter_band(2, "giant_spider") # 首遇带数据驱动定位(契约 8)
|
||||
var spit_sole_ok := true # 毒吐只挂巨蜘蛛族且首遇带不投(Q6——教学位池零新招)
|
||||
var bash_sole_ok := true # 盾击只挂灰矮人族三行
|
||||
for row in _db.enemy.rows:
|
||||
for entry in row.pool:
|
||||
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.node_depth) >= 5):
|
||||
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):
|
||||
spit_sole_ok = false
|
||||
if sid == "shield_bash" and not (String(row.enemy_id) == "duergar" and int(row.level_index) == 3):
|
||||
bash_sole_ok = false
|
||||
check(spit_sole_ok, "毒吐投放面(仅 (2,5)/(2,6)/(2,7) 巨蜘蛛族——首遇位 (2,4) 池不动,迭代 24 N2/Q6)")
|
||||
check(bash_sole_ok, "盾击投放面(仅 (3,0)/(3,5)/(3,7) 灰矮人族——钩爪恐魔零触碰,迭代 24 N2)")
|
||||
# 行集键 = (level_index, node_depth, enemy_id)——分支节点图批起 bugbear 等敌种跨关异池,enemy_id 不再唯一;
|
||||
# 同坐标混编多行时每行各一池,键升维到三元组(行序 = 文件序 = 契约序)
|
||||
check(spit_sole_ok, "毒吐投放面(仅 depth_band > 巨蜘蛛首遇带(%d)的巨蜘蛛族行——首遇带池不动,迭代 24 N2/Q6)〔fv24〕" % spider_lead_band)
|
||||
check(bash_sole_ok, "盾击投放面(仅 (3,·) 灰矮人族——钩爪恐魔零触碰,迭代 24 N2)")
|
||||
# 首遇带行池恒 {attack:6, bite:2, charge:1}(去写死深度判据——带键口径)
|
||||
var spider_lead_pool := {}
|
||||
for row in _db.enemy.rows:
|
||||
if int(row.level_index) == 2 and String(row.row_kind) == "battle" \
|
||||
and int(row.depth_band) == spider_lead_band and String(row.enemy_id) == "giant_spider":
|
||||
for entry in row.pool:
|
||||
spider_lead_pool[String(entry.skill_id)] = int(entry.weight)
|
||||
check(spider_lead_pool == {"attack": 6, "bite": 2, "charge": 1},
|
||||
"巨蜘蛛首遇带行 pool 恒 {attack:6, bite:2, charge:1}(教学位池零新招——毒吐首投在更深的带)〔fv24〕")
|
||||
# 行集键 = (level_index, row_kind, depth_band, enemy_id)——分支节点图批起 bugbear 等敌种跨关异池,
|
||||
# enemy_id 不再唯一;同带混编多行时每行各一池(行序 = 文件序 = 契约序)
|
||||
var expect_pools := {
|
||||
"1_0_goblin": {"attack": 9, "lunge": 1},
|
||||
"1_1_goblin": {"attack": 9, "lunge": 1},
|
||||
"1_2_bugbear": {"attack": 6, "charge": 2, "heavy": 2},
|
||||
"1_3_hobgoblin": {"attack": 6, "charge": 2, "heavy": 2},
|
||||
"1_4_ogre_chief": {"attack": 6, "heavy": 2, "harden": 2, "quake_smash": 2},
|
||||
"2_0_bugbear": {"attack": 6, "lunge": 1, "heavy": 2},
|
||||
"2_1_bugbear": {"attack": 6, "lunge": 1, "heavy": 2},
|
||||
"2_4_giant_spider": {"attack": 6, "bite": 2, "charge": 1},
|
||||
"2_5_giant_spider": {"attack": 6, "bite": 2, "charge": 1, "venom_spit": 2},
|
||||
"2_5_bugbear": {"attack": 5, "lunge": 2, "heavy": 2},
|
||||
"2_6_giant_spider": {"attack": 6, "bite": 2, "charge": 1, "venom_spit": 2},
|
||||
"2_7_giant_spider": {"attack": 6, "bite": 2, "charge": 1, "venom_spit": 2},
|
||||
"2_9_drider": {"attack": 6, "heavy": 2, "venom_burst": 2, "web_snare": 2, "venom_curb": 2},
|
||||
"3_0_duergar": {"attack": 6, "charge": 2, "heavy": 2, "shield_bash": 2},
|
||||
"3_1_hook_horror": {"attack": 6, "bite": 2, "charge": 1},
|
||||
"3_4_hook_horror": {"attack": 6, "bite": 2, "charge": 1},
|
||||
"3_5_duergar": {"attack": 5, "lunge": 2, "heavy": 2, "shield_bash": 2},
|
||||
"3_6_hook_horror": {"attack": 6, "bite": 2, "charge": 1},
|
||||
"3_7_duergar": {"attack": 6, "charge": 2, "heavy": 2, "shield_bash": 2},
|
||||
"3_9_duergar_king": {"attack": 6, "heavy": 2, "earth_pulse": 2, "rockslide": 2},
|
||||
"1_battle_0_goblin": {"attack": 9, "lunge": 1},
|
||||
"1_battle_1_goblin": {"attack": 9, "lunge": 1},
|
||||
"1_battle_2_bugbear": {"attack": 6, "charge": 2, "heavy": 2},
|
||||
"1_battle_3_hobgoblin": {"attack": 6, "charge": 2, "heavy": 2},
|
||||
"1_boss_4_ogre_chief": {"attack": 6, "heavy": 2, "harden": 2, "quake_smash": 2},
|
||||
"2_battle_0_bugbear": {"attack": 6, "lunge": 1, "heavy": 2},
|
||||
"2_battle_1_bugbear": {"attack": 6, "lunge": 1, "heavy": 2},
|
||||
"2_battle_4_giant_spider": {"attack": 6, "bite": 2, "charge": 1},
|
||||
"2_battle_5_giant_spider": {"attack": 6, "bite": 2, "charge": 1, "venom_spit": 2},
|
||||
"2_battle_5_bugbear": {"attack": 5, "lunge": 2, "heavy": 2},
|
||||
"2_battle_6_giant_spider": {"attack": 6, "bite": 2, "charge": 1, "venom_spit": 2},
|
||||
"2_battle_7_giant_spider": {"attack": 6, "bite": 2, "charge": 1, "venom_spit": 2},
|
||||
"2_boss_9_drider": {"attack": 6, "heavy": 2, "venom_burst": 2, "web_snare": 2, "venom_curb": 2},
|
||||
"3_battle_0_duergar": {"attack": 6, "charge": 2, "heavy": 2, "shield_bash": 2},
|
||||
"3_battle_1_hook_horror": {"attack": 6, "bite": 2, "charge": 1},
|
||||
"3_battle_4_hook_horror": {"attack": 6, "bite": 2, "charge": 1},
|
||||
"3_battle_5_duergar": {"attack": 5, "lunge": 2, "heavy": 2, "shield_bash": 2},
|
||||
"3_battle_6_hook_horror": {"attack": 6, "bite": 2, "charge": 1},
|
||||
"3_battle_7_duergar": {"attack": 6, "charge": 2, "heavy": 2, "shield_bash": 2},
|
||||
"3_boss_9_duergar_king": {"attack": 6, "heavy": 2, "earth_pulse": 2, "rockslide": 2},
|
||||
}
|
||||
var pools_ok := true
|
||||
for row in _db.enemy.rows:
|
||||
var rk := "%d_%d_%s" % [int(row.level_index), int(row.node_depth), String(row.enemy_id)]
|
||||
var rk := "%d_%s_%d_%s" % [int(row.level_index), String(row.row_kind), int(row.depth_band), String(row.enemy_id)]
|
||||
if not expect_pools.has(rk):
|
||||
pools_ok = false
|
||||
continue
|
||||
@@ -4221,7 +4324,7 @@ func _smoke_skill_pool_data() -> void:
|
||||
got[String(entry.skill_id)] = int(entry.weight)
|
||||
if got != expect_pools[rk]:
|
||||
pools_ok = false
|
||||
check(pools_ok, "二十行九种池模板 = 建议池行值((L,D,enemy_id) 三元键逐行精确比对——2026-10-07 行集多行批口径;L1 地精 9/1 · 熊地精 6/2/2 · 酋长 6/2/2;L2 熊地精 6/1/2 · 蜘蛛 6/2/1+毒吐2〔(2,5)-(2,7) 投放,首遇位 (2,4) 不投——迭代 24 N2〕 · 重击手 5/2/2 · 蛛化卓尔精灵 6/2/2/2〔(2,9)——商店房批 Boss 重编号〕;L3 灰矮人 6/2/2+盾击2〔(3,0)/(3,7)〕 · 钩爪恐魔 6/2/1 · 重装手 5/2/2+盾击2 · 石君王 6/2/2〔earth_pulse·(3,9)〕——全部 provisional;三 Boss 池尾 +1 门控条目〔quake_smash/venom_curb/rockslide 权重 2+unlock_hp_pct 50——迭代14④ T5〕;混编行集同坐标多行各一池合法——首批投放 (2,5) 双行〔迭代 18 卡⑤:重击手+巢群,新行行值复制 (2,6)〕,其余坐标仍恰一行)")
|
||||
check(pools_ok, "二十行九种池模板 = 建议池行值((L,row_kind,depth_band,enemy_id) 四元键逐行精确比对;L1 地精 9/1 · 熊地精 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 · 重装手 5/2/2+盾击2 · 石君王 6/2/2〔earth_pulse〕——全部 provisional;三 Boss 池尾 +1 门控条目〔quake_smash/venom_curb/rockslide 权重 2+unlock_hp_pct 50——迭代14④ T5〕;混编行集同带多行各一池合法——首批投放 (2,battle,5) 双行〔迭代 18 卡⑤:重击手+巢群〕,其余带键仍恰一行)")
|
||||
var basic_count_ok := true
|
||||
for row in _db.enemy.rows:
|
||||
var n := 0
|
||||
@@ -4253,7 +4356,7 @@ func _smoke_skill_pool_data() -> void:
|
||||
var st_unlock: Dictionary = {"rows": []}
|
||||
var sett_u = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(4242))
|
||||
sett_u.step_row.connect(func(row): st_unlock["rows"].append(row))
|
||||
check(sett_u.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.hero.max_hp)}), "前置:卓尔 Boss 战受理(2,9——解锁技 venom_curb 门槛 50)")
|
||||
check(sett_u.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}), "前置:卓尔 Boss 战受理(2,9——解锁技 venom_curb 门槛 50)")
|
||||
var boss_u = sett_u._enemy_system.all_enemies()[0] # 白盒夹具(smoke 惯例——autoplay 同族私有面)
|
||||
sett_u._enemy_system.apply_damage(boss_u, (int(boss_u.max_hp) - 65))
|
||||
check(int(boss_u.hp) == 65, "前置:Boss 压到阈值 65(=round(130×0.5))")
|
||||
@@ -4270,13 +4373,13 @@ func _smoke_skill_pool_data() -> void:
|
||||
var rollable_ok := true
|
||||
for depth in [0, 1, 2, 3, 4]:
|
||||
var es = load("res://core/unit_container.gd").new(_db)
|
||||
es.spawn(1, int(depth), int(_db.hero.max_hp))
|
||||
es.spawn(1, int(depth), int(_db.hero.max_hp), [], -1, -1, [], "", _row_kind_of(1, int(depth)))
|
||||
for u in es.all_enemies():
|
||||
if es.rollable_skills(u).is_empty():
|
||||
rollable_ok = false
|
||||
for depth in [0, 1, 4, 5, 6, 7, 8]:
|
||||
var es2 = load("res://core/unit_container.gd").new(_db)
|
||||
es2.spawn(2, int(depth), int(_db.hero.max_hp))
|
||||
es2.spawn(2, int(depth), int(_db.hero.max_hp), [], -1, -1, [], "", _row_kind_of(2, int(depth)))
|
||||
for u in es2.all_enemies():
|
||||
if es2.rollable_skills(u).is_empty():
|
||||
rollable_ok = false
|
||||
@@ -4393,7 +4496,7 @@ func _smoke_skill_pool_death_void() -> void:
|
||||
for s in range(1, 400):
|
||||
var rg = load("res://core/rng_service.gd").new()
|
||||
rg.set_master_seed(s)
|
||||
var erow: Dictionary = _db.resolve_enemy_rowset(1, 2)[0] # 行集首行(T2 机械适配)
|
||||
var erow: Dictionary = _db.resolve_enemy_rowset(1, 2, "battle")[0] # 行集首行(战斗节点 battle 键域)
|
||||
var entries: Array = []
|
||||
for entry in erow.pool:
|
||||
entries.append({"skill_id": String(entry.skill_id), "weight": int(entry.weight)})
|
||||
@@ -4500,7 +4603,7 @@ func _smoke_debuff_rider() -> void:
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rg)
|
||||
var rows: Array = []
|
||||
settlement.step_row.connect(func(row): rows.append(row))
|
||||
if not settlement.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.hero.max_hp)}):
|
||||
if not settlement.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}):
|
||||
continue
|
||||
var boss_atk: int = int(settlement._enemy_system.all_enemies()[0].atk) # 开战掷定攻击基线(毒分量随其缩放)
|
||||
for i in range(6):
|
||||
@@ -4670,53 +4773,54 @@ func _smoke_level_graph() -> void:
|
||||
check(green_ok, "首遇位零踩雷:完美档 12 种子(节点 0 熊地精)恒零踩雷")
|
||||
check(green_wins >= 7, "首遇战胜局 ≥7/12(实际 %d——阶段拔高后锚行退出绿档,多数胜口径)" % green_wins)
|
||||
check(green_min_hp > 0, "首遇战胜后 HP 下探读数 %d(>0——绿档记录)" % green_min_hp)
|
||||
# ⑦ 跨表行完整:L2 每战斗/Boss 节点 (2, 编号) 在 board_gen 与 enemy 精确行集(加载校验同款显式复述;
|
||||
# 精确行集口径——enemy 侧同坐标多行合法,键聚合坐标级天然兼容)
|
||||
# ⑦ 跨表行完整:L2 每战斗/Boss 节点在 board_gen 与 enemy 精确行集(加载校验同款显式复述;
|
||||
# enemy 键域 = (level_index, row_kind, depth_band)——战斗节点查 battle 键域、Boss 节点查 boss 键域)
|
||||
var bg_keys := {}
|
||||
for row in _db.board_gen.rows:
|
||||
bg_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true
|
||||
var en_keys := {}
|
||||
for row in _db.enemy.rows:
|
||||
en_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true
|
||||
en_keys["%d_%s_%d" % [int(row.level_index), String(row.get("row_kind", "")), int(row.get("depth_band", -1))]] = true
|
||||
var cross_ok := true
|
||||
for node in _level_active_nodes(2):
|
||||
var t := String(node.get("type", ""))
|
||||
if t != "battle" and t != "boss":
|
||||
continue
|
||||
var key := "2_%d" % int(node.node_depth)
|
||||
if not bg_keys.has(key) or not en_keys.has(key):
|
||||
if not bg_keys.has("2_%d" % int(node.node_depth)) or not en_keys.has("2_%s_%d" % [t, int(node.node_depth)]):
|
||||
cross_ok = false
|
||||
check(cross_ok, "跨表行完整:L2 全部战斗/Boss 节点在 board_gen + enemy 精确行集(7 节点)")
|
||||
# ⑦a 定点:L2 首遇行集与 Boss 行集(精确行集口径——断言行集构成而非单行:
|
||||
# 零内容期每坐标恰一行 = 单元素行集;行序 = 文件序,首行即叙述取数面)
|
||||
var l2_spider_set: Array = _db.resolve_enemy_rowset(2, 4)
|
||||
check(cross_ok, "跨表行完整:L2 全部战斗/Boss 节点在 board_gen + enemy 精确行集(7 节点——带键 (2,row_kind,depth_band))")
|
||||
# ⑦a 定点:L2 首遇带行集与 Boss 行集(首遇带 = 数据驱动最低带,契约 8——禁写死坐标;
|
||||
# 断言行集构成而非单行:行序 = 文件序,首行即叙述取数面)
|
||||
var l2_spider_band: int = _first_encounter_band(2, "giant_spider")
|
||||
var l2_spider_set: Array = _db.resolve_enemy_rowset(2, l2_spider_band, "battle")
|
||||
var l2_spider_ids := {}
|
||||
for r in l2_spider_set:
|
||||
l2_spider_ids[String(r.get("enemy_id", ""))] = true
|
||||
check(l2_spider_ids.has("giant_spider") and l2_spider_ids.size() == 1 and l2_spider_set.size() == 1,
|
||||
"巨蜘蛛首遇行集 (2,4) = {giant_spider} 恰一行(固定 2 只——首遇教学位不加骰;行集口径断言——行集构成纪律下单行集 = 现行形态)")
|
||||
var l2_boss_set: Array = _db.resolve_enemy_rowset(2, 9)
|
||||
check(l2_spider_band >= 0 and l2_spider_ids.has("giant_spider") and l2_spider_ids.size() == 1 and l2_spider_set.size() == 1 \
|
||||
and int(l2_spider_set[0].get("unit_count", -1)) == 2 and not l2_spider_set[0].has("unit_count_max"),
|
||||
"巨蜘蛛首遇带(2,battle,%d——扫表取最低带)行集 = {giant_spider} 恰一行且 unit_count=2 无 unit_count_max(固定 2 只——首遇教学位不加骰)〔fv22〕" % l2_spider_band)
|
||||
var l2_boss_set: Array = _db.resolve_enemy_rowset(2, _boss_depth(2), "boss")
|
||||
var l2_boss_ids := {}
|
||||
for r in l2_boss_set:
|
||||
l2_boss_ids[String(r.get("enemy_id", ""))] = true
|
||||
check(l2_boss_ids.has("drider") and l2_boss_ids.size() == 1 and String(l2_boss_set[0].get("armor_type", "")) == "ward",
|
||||
"蛛化卓尔精灵 Boss 行集 (2,9) = {drider} 恰一行且首行魔法甲(护甲类型重配批 2026-09-18:关卡 2 唯一非轻甲;商店房批重编号 (2,8)→(2,9);P4 乙 Boss 行集多行合法——零内容期仍单行)")
|
||||
# ⑦b 定点:混编遭遇行集 (2,5)(R6 行序契约运行面:
|
||||
"蛛化卓尔精灵 Boss 行集(2,boss,%d)= {drider} 恰一行且首行魔法甲(护甲类型重配批 2026-09-18:关卡 2 唯一非轻甲;P4 乙 Boss 行集多行合法——零内容期仍单行)〔fv24〕" % _boss_depth(2))
|
||||
# ⑦b 定点:混编遭遇行集 (2,battle,5)(R6 行序契约运行面:
|
||||
# 行集两行文件内相邻、行序 = 声明序 = 生成序;静态最坏骰和 Σ(unit_count_max,固定行缺省 = unit_count) ≤ 4)
|
||||
var l2_mix_set: Array = _db.resolve_enemy_rowset(2, 5)
|
||||
var l2_mix_set: Array = _db.resolve_enemy_rowset(2, 5, "battle")
|
||||
var mix_shape_ok: bool = l2_mix_set.size() == 2 \
|
||||
and String(l2_mix_set[0].get("enemy_id", "")) == "bugbear" \
|
||||
and String(l2_mix_set[1].get("enemy_id", "")) == "giant_spider"
|
||||
check(mix_shape_ok,
|
||||
"首批混编行集 (2,5) = 双行且行序 [bugbear 重击手 → giant_spider 巢群](R6 行序契约——文件序 = 声明序 = 生成序,首行默认目标 = 前排坦克;迭代 18 卡⑤投放)")
|
||||
"首批混编行集 (2,battle,5) = 双行且行序 [bugbear 重击手 → giant_spider 巢群](R6 行序契约——文件序 = 声明序 = 生成序,首行默认目标 = 前排坦克;迭代 18 卡⑤投放)〔fv18〕")
|
||||
var mix_sum_max := 0
|
||||
for r in l2_mix_set:
|
||||
mix_sum_max += int(r.get("unit_count_max", int(r.get("unit_count", 1))))
|
||||
check(mix_sum_max <= 4,
|
||||
"首批混编行集 (2,5) 静态最坏骰和 = Σ(unit_count_max,固定行缺省=unit_count) = %d ≤ 4(行集实例总数守卫同口径——重击手固定 1 + 巢群 [2,3])" % mix_sum_max)
|
||||
"首批混编行集 (2,battle,5) 静态最坏骰和 = Σ(unit_count_max,固定行缺省=unit_count) = %d ≤ 4(行集实例总数守卫同口径——重击手固定 1 + 巢群 [2,3])〔fv19〕" % mix_sum_max)
|
||||
check(int(l2_mix_set[1].get("unit_count", 0)) == 2 and int(l2_mix_set[1].get("unit_count_max", 0)) == 3 \
|
||||
and int(l2_mix_set[1].get("hp", 0)) == 26 and int(l2_mix_set[1].get("armor_class", -1)) == 2,
|
||||
"首批混编新行 (2,5,giant_spider) 行值 = 复制 (2,6) 巢群行(P4 甲零数值新增——2-3 只/hp 26/atk [1,2]/light/ac 2;既有 19 行现值零变更)")
|
||||
"首批混编新行 (2,battle,5,giant_spider) 行值 = 复制巢群行(P4 甲零数值新增——2-3 只/hp 26/atk [1,2]/light/ac 2;既有 19 行现值零变更)")
|
||||
# ---- L3 灰矮人矿城断言段 ----
|
||||
# ⑧ L3 行集结构(复用 L2 九节点图模板——6 战 + Boss + 奖励 + 恢复,真实岔路 4 处)
|
||||
var l3 := {}
|
||||
@@ -4733,29 +4837,32 @@ func _smoke_level_graph() -> void:
|
||||
check(int(tcount3.battle) == 6 and int(tcount3.boss) == 1, "L3 普通战斗节点 = 6 + Boss 恒一(编号最大恒终点)")
|
||||
check(int(tcount3.reward) == 1 and int(tcount3.rest) == 1 and int(tcount3.shop) == 1, "L3 恢复节点 1(rest_heal_pct 行字段)+ 奖励节点 1(直通空态)+ 商店房节点 1(商店房批)")
|
||||
check(branch3 == 3, "L3 真实岔路 = %d 处(0→[1,2,3,4,5] · 2→[6,7] · 5→[6,7]——U2 返工批五岔环廊层宽 5)" % branch3)
|
||||
# ⑨ 跨表行完整:L3 每战斗/Boss 节点 (3, 编号) 在 board_gen 与 enemy 精确行集(⑦ 的 bg_keys/en_keys 复用)
|
||||
# ⑨ 跨表行完整:L3 每战斗/Boss 节点在 board_gen 与 enemy 精确行集(⑦ 的 bg_keys/en_keys 复用——
|
||||
# 键域同形:(L,row_kind,depth_band))
|
||||
var cross_ok3 := true
|
||||
for node in _level_active_nodes(3):
|
||||
var t3 := String(node.get("type", ""))
|
||||
if t3 != "battle" and t3 != "boss":
|
||||
continue
|
||||
var key3 := "3_%d" % int(node.node_depth)
|
||||
if not bg_keys.has(key3) or not en_keys.has(key3):
|
||||
if not bg_keys.has("3_%d" % int(node.node_depth)) or not en_keys.has("3_%s_%d" % [t3, int(node.node_depth)]):
|
||||
cross_ok3 = false
|
||||
check(cross_ok3, "跨表行完整:L3 全部战斗/Boss 节点在 board_gen + enemy 精确行集(7 节点)")
|
||||
# ⑩ 定点:L3 首遇行集与 Boss 行集(克制面重轻错开——重甲灰矮人首次登台;行集口径断言同 ⑦a)
|
||||
var l3_lead_set: Array = _db.resolve_enemy_rowset(3, 0)
|
||||
check(cross_ok3, "跨表行完整:L3 全部战斗/Boss 节点在 board_gen + enemy 精确行集(7 节点——带键 (3,row_kind,depth_band))")
|
||||
# ⑩ 定点:L3 首遇带行集与 Boss 行集(克制面重轻错开——重甲灰矮人首次登台;首遇带数据驱动定位同 ⑦a)
|
||||
var l3_lead_band: int = _first_encounter_band(3, "duergar")
|
||||
var l3_lead_set: Array = _db.resolve_enemy_rowset(3, l3_lead_band, "battle")
|
||||
var l3_lead_ids := {}
|
||||
for r in l3_lead_set:
|
||||
l3_lead_ids[String(r.get("enemy_id", ""))] = true
|
||||
check(l3_lead_ids.has("duergar") and l3_lead_ids.size() == 1 and String(l3_lead_set[0].get("armor_type", "")) == "heavy",
|
||||
"灰矮人首遇行集 (3,0) = {duergar} 恰一行且首行重甲(重甲首次登台——固定 1 只,首遇教学位不加骰)")
|
||||
var l3_king_set: Array = _db.resolve_enemy_rowset(3, 9)
|
||||
check(l3_lead_band >= 0 and l3_lead_ids.has("duergar") and l3_lead_ids.size() == 1 \
|
||||
and String(l3_lead_set[0].get("armor_type", "")) == "heavy" \
|
||||
and int(l3_lead_set[0].get("unit_count", -1)) == 1 and not l3_lead_set[0].has("unit_count_max"),
|
||||
"灰矮人首遇带(3,battle,%d——扫表取最低带)行集 = {duergar} 恰一行、首行重甲、固定 1 只不加骰〔fv23〕" % l3_lead_band)
|
||||
var l3_king_set: Array = _db.resolve_enemy_rowset(3, _boss_depth(3), "boss")
|
||||
var l3_king_ids := {}
|
||||
for r in l3_king_set:
|
||||
l3_king_ids[String(r.get("enemy_id", ""))] = true
|
||||
check(l3_king_ids.has("duergar_king") and l3_king_ids.size() == 1 and String(l3_king_set[0].get("armor_type", "")) == "heavy",
|
||||
"灰矮人石君王 Boss 行集 (3,9) = {duergar_king} 恰一行且首行重甲(重甲收束「轻甲→魔法甲→重甲」教学梯度;商店房批重编号 (3,8)→(3,9))")
|
||||
"灰矮人石君王 Boss 行集(3,boss,%d)= {duergar_king} 恰一行且首行重甲(重甲收束「轻甲→魔法甲→重甲」教学梯度)〔fv24〕" % _boss_depth(3))
|
||||
# ⑪ L3 成长代理档首遇绿档:模拟 GP 商店成长后的入关参数(atk_pct=200 / hp_pct=100)· 完美档多种子首遇战稳过
|
||||
# (零加成基线红档属预期——断言口径 = 成长代理档,非基线;跑法同 ⑥)
|
||||
var green3_ok := true
|
||||
@@ -4781,23 +4888,23 @@ func _smoke_level_graph() -> void:
|
||||
check(green3_ok, "L3 首遇位绿档:成长代理档口径(atk_pct=200/hp_pct=100)完美档 12 种子首遇战(节点 0 灰矮人)全胜且零踩雷")
|
||||
check(green3_min_hp > 0, "L3 首遇战胜后 HP 下探读数 %d(>0——绿档记录)" % green3_min_hp)
|
||||
|
||||
# ⑫ 混编行集运行面正反断言:注入坐标 (1,5)/(1,6) =
|
||||
# L1 节点图与全部既有扫描面之外的新深度(ceil 请求命中注入行集——新 L 会被 ceil 封顶到 L3 不可用),
|
||||
# ⑫ 混编行集运行面正反断言:注入带键 (1,battle,5)/(1,battle,6) =
|
||||
# L1 节点图与全部既有扫描面之外的新带键(带命中精确取注入行集——新 L 会被 ceil 封顶到 L3 不可用),
|
||||
# 断言后立即还原表,后续断言面零污染。R3 fail-fast 正反验证 + R6 行序运行面验证。
|
||||
var mix_row_a := {"level_index": 1, "node_depth": 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, "node_depth": 5, "enemy_id": "bugbear", "display_name": "熊地精", "unit_count": 2, "hp": 20, "atk": 3, "pool": [{"skill_id": "attack", "weight": 9}]}
|
||||
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.enemy.rows.append(mix_row_a)
|
||||
_db.enemy.rows.append(mix_row_b)
|
||||
var mix_sys = load("res://core/unit_container.gd").new(_db)
|
||||
check(mix_sys.spawn(1, 5, int(_db.hero.max_hp)), "混编行集 spawn 受理((1,5) 双行 地精1+熊地精2——定值无区间路径,行集构成纪律放开面正向)")
|
||||
check(mix_sys.spawn(1, 5, int(_db.hero.max_hp)), "混编行集 spawn 受理((1,battle,5) 双行 地精1+熊地精2——定值无区间路径,行集构成纪律放开面正向)")
|
||||
var mix_units: Array = mix_sys.all_enemies()
|
||||
check(mix_units.size() == 3, "混编实例总数 = 各行数量求和(1+2=3——行集实例总数 ≤4 守卫内)")
|
||||
check(String(mix_units[0].get("enemy_id")) == "goblin" and String(mix_units[1].get("enemy_id")) == "bugbear" and String(mix_units[2].get("enemy_id")) == "bugbear",
|
||||
"混编生成序 = 文件行序(行序即契约——地精在前熊地精在后,handle_id 跨行连续派发)")
|
||||
check(String(mix_units[0].get("display_name")) == "地精" and int(mix_units[1].get("hp")) == 20 and int(mix_units[1].get("atk")) == 3,
|
||||
"混编各行行值独立投影(display_name/hp/atk 按各自行值——同场不同种不同值)")
|
||||
_db.enemy.rows.append({"level_index": 1, "node_depth": 6, "enemy_id": "goblin", "display_name": "地精", "unit_count": 3, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]})
|
||||
_db.enemy.rows.append({"level_index": 1, "node_depth": 6, "enemy_id": "bugbear", "display_name": "熊地精", "unit_count": 3, "hp": 20, "atk": 3, "pool": [{"skill_id": "attack", "weight": 9}]})
|
||||
_db.enemy.rows.append({"level_index": 1, "row_kind": "battle", "depth_band": 6, "enemy_id": "goblin", "display_name": "地精", "unit_count": 3, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]})
|
||||
_db.enemy.rows.append({"level_index": 1, "row_kind": "battle", "depth_band": 6, "enemy_id": "bugbear", "display_name": "熊地精", "unit_count": 3, "hp": 20, "atk": 3, "pool": [{"skill_id": "attack", "weight": 9}]})
|
||||
var over_sys = load("res://core/unit_container.gd").new(_db)
|
||||
check(not over_sys.spawn(1, 6, int(_db.hero.max_hp)), "行集实例总数守卫:两行各定值 3(总 6 > 4)spawn 被拒(行集构成纪律③——R3 fail-fast 运行面)")
|
||||
_db.enemy.rows.remove_at(_db.enemy.rows.size() - 1)
|
||||
@@ -4930,7 +5037,7 @@ func _smoke_gp_escrow() -> void:
|
||||
var settle_l = load("res://core/battle_orchestrator.gd").new(_db, rng_l)
|
||||
var fin_l := {}
|
||||
settle_l.battle_finished.connect(func(p): fin_l["p"] = p)
|
||||
check(settle_l.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "hero_atk": 999}),
|
||||
check(settle_l.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "hero_atk": 999, "node_type": "boss"}),
|
||||
"破财直验战斗启动")
|
||||
for i in range(3):
|
||||
settle_l._grant_routed({"reward_id": "gp_lose", "cell_type": "gp", "polarity": "negative", "display_name": "破财", "power": 2, "effect_id": "gp_loss", "stacks": 1},
|
||||
@@ -4938,7 +5045,11 @@ func _smoke_gp_escrow() -> void:
|
||||
check(int(settle_l.escrow) == -6, "破财 ×3 → escrow −6(负向缓冲记账,零钱包触达)")
|
||||
settle_l.minesweeper.settle_batch([{"action": "stop"}])
|
||||
var gp_l: int = int(fin_l.get("p", {}).get("gp_dropped", 999))
|
||||
check(gp_l + 6 >= 2 and gp_l + 6 <= 4, "破财压制 gp_dropped 负值上抛(%d + 6 ∈ [2,4]——掷定不受破财污染)" % gp_l)
|
||||
var boss_drop: Dictionary = _db.shop_gp_drop_row("boss")
|
||||
var boss_bonus1: int = _db.shop_level_gp_bonus(1)
|
||||
check(gp_l + 6 >= int(boss_drop.get("roll_min", 0)) + boss_bonus1 and gp_l + 6 <= int(boss_drop.get("roll_max", 0)) + boss_bonus1,
|
||||
"破财压制 gp_dropped 负值上抛(%d + 6 = %d ∈ Boss 档掷定 [%d,%d] + 关级加值 %d——掷定不受破财污染)" % [
|
||||
gp_l, gp_l + 6, int(boss_drop.get("roll_min", 0)), int(boss_drop.get("roll_max", 0)), boss_bonus1])
|
||||
# ③ 钱包不透支(clamp ≥0):runner 驱动 L1 首战 + 战斗中注入破财 → gp_dropped 负值 → 钱包钳制归 0
|
||||
var runner = load("res://core/level_runner.gd").new(_db, _fresh_rng(4242))
|
||||
runner.set_hero_override(49850, 0) # atk 2 → 999(快速终局;hp 基线)
|
||||
@@ -5138,17 +5249,17 @@ func _smoke_shop_graph() -> void:
|
||||
if not p.has(8):
|
||||
via_shop = false
|
||||
check(paths.size() >= 2 and via_shop, "L%d 全部 %d 条入口→Boss 路径穿商店房 8(必经位——DFS 枚举)" % [L, paths.size()])
|
||||
# 跨表校验:(·,9) Boss 行精确存在 · (·,8) 无战斗行残留(陈旧行清理确认)
|
||||
# 跨表校验:(·,boss,9) Boss 行精确存在 · (·,battle,8) 无战斗行残留(陈旧行清理确认)
|
||||
var bg_keys := {}
|
||||
for row in _db.board_gen.rows:
|
||||
bg_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true
|
||||
var en_keys := {}
|
||||
for row in _db.enemy.rows:
|
||||
en_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true
|
||||
en_keys["%d_%s_%d" % [int(row.level_index), String(row.get("row_kind", "")), int(row.get("depth_band", -1))]] = true
|
||||
for L in [2, 3]:
|
||||
check(bg_keys.has("%d_9" % L) and en_keys.has("%d_9" % L), "(%d,9) Boss 行在 board_gen + enemy 精确存在" % L)
|
||||
check(not bg_keys.has("%d_8" % L) and not en_keys.has("%d_8" % L), "(%d,8) 商店房无板/行集残留(陈旧行已清理)" % L)
|
||||
check(bg_keys.has("1_4") and en_keys.has("1_4") and not bg_keys.has("1_8") and not en_keys.has("1_8"),
|
||||
check(bg_keys.has("%d_9" % L) and en_keys.has("%d_boss_9" % L), "(%d,boss,9) Boss 行在 board_gen + enemy 精确存在" % L)
|
||||
check(not bg_keys.has("%d_8" % L) and not en_keys.has("%d_battle_8" % L), "(%d,battle,8) 商店房无板/战斗行集残留(陈旧行已清理)" % L)
|
||||
check(bg_keys.has("1_4") and en_keys.has("1_boss_4") and not bg_keys.has("1_8") and not en_keys.has("1_battle_8"),
|
||||
"L1 行集零改动(单链 Boss (1,4)——教学期最小修订面)")
|
||||
|
||||
|
||||
|
||||
@@ -24,9 +24,9 @@ func _init() -> void:
|
||||
|
||||
|
||||
# 门谓词:卓尔 (2,9) hp 130(×mult 1.0)→ venom_curb unlock 50 → 阈值 round(130×0.5)=65
|
||||
func _boss(db, L: int, D: int):
|
||||
func _boss(db, L: int, D: int, kind := "boss"):
|
||||
var ec = UnitContainerScript.new(db)
|
||||
var ok: bool = ec.spawn(L, D, int(db.hero.max_hp))
|
||||
var ok: bool = ec.spawn(L, D, int(db.hero.max_hp), [], -1, -1, [], "", "", kind)
|
||||
Kit.check("前置:spawn (%d,%d) 受理" % [L, D], ok)
|
||||
return ec
|
||||
|
||||
@@ -82,7 +82,7 @@ func _test_latch_irreversible(db) -> void:
|
||||
|
||||
# 普通行无字段逐位零扰动:地精 (1,0) 池快照零 unlock 键、过滤恒空转、可摇=全池
|
||||
func _test_plain_rows_zero_drift(db) -> void:
|
||||
var ec = _boss(db, 1, 0)
|
||||
var ec = _boss(db, 1, 0, "battle")
|
||||
var u: Dictionary = _boss_u(ec)
|
||||
var any_unlock_key := false
|
||||
for e in u.pool:
|
||||
@@ -94,7 +94,7 @@ func _test_plain_rows_zero_drift(db) -> void:
|
||||
var pool_count: int = (u.pool as Array).size()
|
||||
Kit.check("零扰动:可摇集合 = 全池(普攻冷却恒 0,过滤恒空转)", rollable.size() == pool_count)
|
||||
# 同种子确定性:容器零随机——两实例同输入查询逐位同
|
||||
var ec2 = _boss(db, 1, 0)
|
||||
var ec2 = _boss(db, 1, 0, "battle")
|
||||
var u2: Dictionary = _boss_u(ec2)
|
||||
ec.apply_damage(u, 3)
|
||||
ec2.apply_damage(u2, 3)
|
||||
|
||||
@@ -16,6 +16,7 @@ func _init() -> void:
|
||||
_test_all_links_valid()
|
||||
_test_dangling_skill_id()
|
||||
_test_cast_side_mismatch()
|
||||
_test_level_node_band_rules()
|
||||
quit(Kit.finish("config_db_cross_validator_test"))
|
||||
|
||||
|
||||
@@ -51,7 +52,7 @@ func _test_all_links_valid() -> void:
|
||||
{"skill_id": "raise_morale", "type": "buff", "buff_ref": "st_strong"},
|
||||
])
|
||||
var enemies := _enemy([
|
||||
{"level_index": 1, "node_depth": 0, "enemy_id": "goblin", "pool": [
|
||||
{"level_index": 1, "row_kind": "battle", "depth_band": 0, "enemy_id": "goblin", "pool": [
|
||||
{"skill_id": "attack", "weight": 9}, {"skill_id": "raise_morale", "weight": 3}]},
|
||||
])
|
||||
var status := _status([{"effect_id": "st_strong", "target_side": "enemy"}])
|
||||
@@ -63,7 +64,7 @@ func _test_all_links_valid() -> void:
|
||||
func _test_dangling_skill_id() -> void:
|
||||
var skills := _skill([{"skill_id": "attack", "type": "basic"}])
|
||||
var enemies := _enemy([
|
||||
{"level_index": 1, "node_depth": 0, "enemy_id": "goblin", "pool": [
|
||||
{"level_index": 1, "row_kind": "battle", "depth_band": 0, "enemy_id": "goblin", "pool": [
|
||||
{"skill_id": "attack", "weight": 9}, {"skill_id": "not_in_table", "weight": 3}]},
|
||||
])
|
||||
var errs: Array = ValidatorScript.new().validate_skill_pool_links(skills, enemies, null)
|
||||
@@ -76,9 +77,67 @@ func _test_dangling_skill_id() -> void:
|
||||
func _test_cast_side_mismatch() -> void:
|
||||
var skills := _skill([{"skill_id": "raise_morale", "type": "buff", "buff_ref": "st_hero_side"}])
|
||||
var enemies := _enemy([
|
||||
{"level_index": 1, "node_depth": 0, "enemy_id": "goblin", "pool": [
|
||||
{"level_index": 1, "row_kind": "battle", "depth_band": 0, "enemy_id": "goblin", "pool": [
|
||||
{"skill_id": "raise_morale", "weight": 3}]},
|
||||
])
|
||||
var status := _status([{"effect_id": "st_hero_side", "target_side": "hero"}])
|
||||
var errs: Array = ValidatorScript.new().validate_skill_pool_links(skills, enemies, status)
|
||||
Kit.check("buff 挂主角侧状态被报(施放类 side 守卫)", _cats(errs).has("cast_side"))
|
||||
|
||||
|
||||
# ---- ④ 节点 × 敌人表带寻址(单终点零误报/同带双 Boss 报错/顶 clamp 消解报错)----
|
||||
const LevelTableScript := preload("res://data/schemas/level_table.gd")
|
||||
const BoardGenTableScript := preload("res://data/schemas/board_gen_table.gd")
|
||||
const RewardTableScript := preload("res://data/schemas/reward_table.gd")
|
||||
const SettlementTableScript := preload("res://data/schemas/settlement_table.gd")
|
||||
|
||||
|
||||
func _loaded(level_nodes: Array, bg_depths: Array, boss_bands: Array, battle_bands: Array) -> Dictionary:
|
||||
var lv := LevelTableScript.new()
|
||||
lv.rows.assign([{"level_index": 1, "display_name": "夹具关", "templates": [{"nodes": level_nodes}]}])
|
||||
var bg := BoardGenTableScript.new()
|
||||
var bg_rows: Array = []
|
||||
for d in bg_depths:
|
||||
bg_rows.append({"level_index": 1, "node_depth": int(d), "board_width": 10})
|
||||
bg.rows.assign(bg_rows)
|
||||
var en := EnemyTableScript.new()
|
||||
var en_rows: Array = []
|
||||
for b in battle_bands:
|
||||
en_rows.append({"level_index": 1, "row_kind": "battle", "depth_band": int(b), "enemy_id": "goblin",
|
||||
"unit_count": 1, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]})
|
||||
for b in boss_bands:
|
||||
en_rows.append({"level_index": 1, "row_kind": "boss", "depth_band": int(b), "enemy_id": "ogre_chief",
|
||||
"unit_count": 1, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]})
|
||||
en.rows.assign(en_rows)
|
||||
var st := SettlementTableScript.new()
|
||||
st.source_label_map.assign({"safe_cell": "a", "seal_hit": "b", "mine_hit": "c", "clear_reward": "d",
|
||||
"hero_attack": "e", "enemy_action": "f", "seal_miss": "g"})
|
||||
return {"status_effects": StatusTableScript.new(), "reward": RewardTableScript.new(),
|
||||
"settlement": st, "level": lv, "board_gen": bg, "enemy": en}
|
||||
|
||||
|
||||
func _cross_cats(loaded: Dictionary, needle: String) -> Array[String]:
|
||||
var out: Array[String] = []
|
||||
ValidatorScript.new().cross_validate(loaded,
|
||||
func(_sev, cat, msg) -> void:
|
||||
if String(msg).contains(needle):
|
||||
out.append(String(cat)))
|
||||
return out
|
||||
|
||||
|
||||
func _test_level_node_band_rules() -> void:
|
||||
# 正向:单终点三关同构(battle 节点落 band 0、boss 节点精确落顶带)→ ④ 零误报
|
||||
var ok_nodes := [{"type": "battle", "node_depth": 0}, {"type": "boss", "node_depth": 9}]
|
||||
var ok_cats := _cross_cats(_loaded(ok_nodes, [0, 9], [9], [0]), "level 1")
|
||||
Kit.check("单终点图 ④ 敌侧带寻址零误报〔fv17〕", ok_cats.is_empty())
|
||||
# 负向:一路两终点落同一 Boss 带(深度 3/4 都落带 4)→ rowset_rule
|
||||
var same_band_nodes := [{"type": "battle", "node_depth": 0},
|
||||
{"type": "boss", "node_depth": 3}, {"type": "boss", "node_depth": 4}]
|
||||
var same_band_cats := _cross_cats(_loaded(same_band_nodes, [0, 3, 4], [4, 9], [0]), "同带")
|
||||
Kit.check("同关双终点共带报 rowset_rule〔fv15〕",
|
||||
same_band_cats.has("rowset_rule"))
|
||||
# 负向:Boss 节点深度高于顶带键(顶 clamp 消解)→ level_node_dangling
|
||||
var over_nodes := [{"type": "battle", "node_depth": 0}, {"type": "boss", "node_depth": 10}]
|
||||
var over_cats := _cross_cats(_loaded(over_nodes, [0, 10], [9], [0]), "顶 clamp 消解")
|
||||
Kit.check("Boss 节点越顶带报 level_node_dangling〔fv16〕",
|
||||
over_cats.has("level_node_dangling"))
|
||||
@@ -1,6 +1,6 @@
|
||||
# tests/unit/core/config_db_resolver_test.gd — 配置解析直接单测
|
||||
# 断言面 = ceil 进度键解析语义(向上取整保证偏难)、行集多行的行序保序、
|
||||
# 难度修饰乘数的 clamp 域。行集由夹具注入,不碰生产表——期望值由注入行数出来。
|
||||
# 断言面 = 深度带寻址语义(请求落带取偏难一侧、行集多行行序保序、越顶带 clamp+告警去重、
|
||||
# 行类路由、越域 fatal)、难度修饰乘数的 clamp 域。行集由夹具注入,不碰生产表——期望值由注入行数出来。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/core/config_db_resolver_test.gd
|
||||
extends SceneTree
|
||||
|
||||
@@ -19,8 +19,11 @@ func _init() -> void:
|
||||
func() -> Array: return _enemy_rows(),
|
||||
func(): return _difficulty_table(),
|
||||
)
|
||||
_test_ceil_progress_key()
|
||||
_test_band_hit()
|
||||
_test_rowset_order_preserved()
|
||||
_test_top_band_clamp()
|
||||
_test_row_kind_routing()
|
||||
_test_request_oob()
|
||||
_test_difficulty_clamp()
|
||||
quit(Kit.finish("config_db_resolver_test"))
|
||||
|
||||
@@ -33,11 +36,13 @@ func _board_gen_rows() -> Array:
|
||||
]
|
||||
|
||||
|
||||
# 带键 [0,5]:请求 4 未精确落键,取首个 ≥4 的带 5(偏难一侧),不回带 0
|
||||
func _enemy_rows() -> Array:
|
||||
return [
|
||||
{"level_index": 1, "node_depth": 0, "enemy_id": "goblin"},
|
||||
{"level_index": 1, "node_depth": 0, "enemy_id": "bugbear"},
|
||||
{"level_index": 1, "node_depth": 5, "enemy_id": "hobgoblin"},
|
||||
{"level_index": 1, "row_kind": "battle", "depth_band": 0, "enemy_id": "goblin"},
|
||||
{"level_index": 1, "row_kind": "battle", "depth_band": 0, "enemy_id": "bugbear"},
|
||||
{"level_index": 1, "row_kind": "battle", "depth_band": 5, "enemy_id": "hobgoblin"},
|
||||
{"level_index": 1, "row_kind": "boss", "depth_band": 9, "enemy_id": "ogre_chief"},
|
||||
]
|
||||
|
||||
|
||||
@@ -48,26 +53,65 @@ func _difficulty_table():
|
||||
return _FakeDifficulty.new(rows)
|
||||
|
||||
|
||||
# ceil 语义:请求 (1,1) 落在 d=0 与 d=3 之间,向上取到 d=3(偏难),不是向下取 d=0
|
||||
func _test_ceil_progress_key() -> void:
|
||||
var hit: Dictionary = _resolver.resolve_board_gen(1, 1)
|
||||
Kit.check("ceil 进度键:请求 (1,1) 命中 (1,3) 而非 (1,0)——偏难语义",
|
||||
int(hit.get("node_depth", -1)) == 3 and int(hit.get("board_width", 0)) == 12)
|
||||
var exact: Dictionary = _resolver.resolve_board_gen(2, 0)
|
||||
Kit.check("精确命中:(2,0) 取本行", int(exact.get("board_width", 0)) == 14)
|
||||
func _test_band_hit() -> void:
|
||||
var hit: Array = _resolver.resolve_enemy_rowset(1, 4, "battle")
|
||||
Kit.check("带命中取偏难一侧:请求深度 4 命中带 5(逐字不回带 0)〔fv1〕",
|
||||
hit.size() == 1 and String((hit[0] as Dictionary).get("enemy_id", "")) == "hobgoblin")
|
||||
var exact: Array = _resolver.resolve_enemy_rowset(1, 5, "battle")
|
||||
Kit.check("精确落带:请求深度 5 命中带 5〔fv1〕",
|
||||
exact.size() == 1 and String((exact[0] as Dictionary).get("enemy_id", "")) == "hobgoblin")
|
||||
|
||||
|
||||
func _test_rowset_order_preserved() -> void:
|
||||
var rows: Array = _resolver.resolve_enemy_rowset(1, 0)
|
||||
Kit.check("行集返回同坐标全部行(2 行)", rows.size() == 2)
|
||||
var rows: Array = _resolver.resolve_enemy_rowset(1, 0, "battle")
|
||||
Kit.check("行集返回同带全部行(2 行)〔fv2〕", rows.size() == 2)
|
||||
var ids: Array[String] = []
|
||||
for r in rows:
|
||||
ids.append(String((r as Dictionary).get("enemy_id", "")))
|
||||
Kit.check("行集按表文件行序返回(goblin 先于 bugbear——行序即契约)",
|
||||
Kit.check("行集按表文件行序返回(goblin 先于 bugbear——行序即契约)〔fv2〕",
|
||||
ids == ["goblin", "bugbear"])
|
||||
var far: Array = _resolver.resolve_enemy_rowset(1, 4)
|
||||
Kit.check("ceil 行集:请求 (1,4) 命中 (1,5)", far.size() == 1
|
||||
and String((far[0] as Dictionary).get("enemy_id", "")) == "hobgoblin")
|
||||
|
||||
|
||||
func _test_top_band_clamp() -> void:
|
||||
_diags.clear()
|
||||
var rows: Array = _resolver.resolve_enemy_rowset(1, 99, "battle")
|
||||
Kit.check("越顶带 clamp 取顶带行集(battle 顶带 5)〔fv3〕",
|
||||
rows.size() == 1 and String((rows[0] as Dictionary).get("enemy_id", "")) == "hobgoblin")
|
||||
var clamp_hit := false
|
||||
for d in _diags:
|
||||
if d.contains("warning/resolve_depth_clamp"):
|
||||
clamp_hit = true
|
||||
Kit.check("越顶带发 warning resolve_depth_clamp〔fv3〕", clamp_hit)
|
||||
var warn_before: int = _diags.size()
|
||||
_resolver.resolve_enemy_rowset(1, 99, "battle")
|
||||
Kit.check("同键重复请求警告去重(四元组注册表)〔fv3〕", _diags.size() == warn_before)
|
||||
|
||||
|
||||
func _test_row_kind_routing() -> void:
|
||||
var battle_rows: Array = _resolver.resolve_enemy_rowset(1, 0, "battle")
|
||||
var boss_rows: Array = _resolver.resolve_enemy_rowset(1, 0, "boss")
|
||||
Kit.check("行类路由:同请求深度 battle 与 boss 命中不同键域〔fv4〕",
|
||||
battle_rows.size() == 2 and boss_rows.size() == 1
|
||||
and String((boss_rows[0] as Dictionary).get("enemy_id", "")) == "ogre_chief")
|
||||
|
||||
|
||||
func _test_request_oob() -> void:
|
||||
_diags.clear()
|
||||
_resolver.resolve_enemy_rowset(0, 0, "battle")
|
||||
Kit.check("关卡序号 <1 → fatal request_oob〔fv5〕", _has_fatal_oob())
|
||||
_diags.clear()
|
||||
_resolver.resolve_enemy_rowset(1, -1, "battle")
|
||||
Kit.check("节点深度 <0 → fatal request_oob〔fv5〕", _has_fatal_oob())
|
||||
_diags.clear()
|
||||
_resolver.resolve_enemy_rowset(1, 0, "elite")
|
||||
Kit.check("row_kind 非法 → fatal request_oob〔fv5〕", _has_fatal_oob())
|
||||
|
||||
|
||||
func _has_fatal_oob() -> bool:
|
||||
for d in _diags:
|
||||
if d.contains("fatal/request_oob"):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _test_difficulty_clamp() -> void:
|
||||
|
||||
@@ -11,7 +11,7 @@ const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:_check/汇
|
||||
func _init() -> void:
|
||||
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)
|
||||
Kit.check("全部表加载 + 校验通过(含跨表护栏零 error 诊断)〔fv6〕", load_ok and db.diagnostics_count("error") == 0)
|
||||
Kit.check("is_ready 通道随加载置位", db.is_ready)
|
||||
if not load_ok:
|
||||
for d in db.diagnostics():
|
||||
@@ -59,8 +59,8 @@ func _test_progress_resolution(db) -> void:
|
||||
|
||||
# enemy 行集同通道:同 (L,D) 行集数组非空(关卡节点地图 前置——battle 节点双表精确行集契约;
|
||||
# 行集面契约:返回形态是行集数组,精确性断言随行集面)
|
||||
var en_got: Array = db.resolve_enemy_rowset(int(probe_row.level_index), int(probe_row.node_depth))
|
||||
Kit.check("enemy 行集同坐标解析非空(双表精确行契约)", not en_got.is_empty())
|
||||
var en_got: Array = db.resolve_enemy_rowset(int(probe_row.level_index), int(probe_row.node_depth), "battle")
|
||||
Kit.check("enemy 行集同坐标解析非空(双表精确行契约——battle 行类)", not en_got.is_empty())
|
||||
|
||||
# 深度 clamp:请求深度超最大行 → 取该关最大深度行 + 警告诊断一次(去重)
|
||||
db.reset_diagnostics()
|
||||
@@ -197,44 +197,46 @@ func _test_rowset_discipline() -> void:
|
||||
# 数组字面量会触发「Invalid assignment」脚本错误——该错误不改退出码,却会**中断本函数剩余断言**
|
||||
#(本函数五条纪律断言曾因此全部静默不跑,套件仍报 all green,是零覆盖假象)。
|
||||
const EnemyTableScript := preload("res://data/schemas/enemy_table.gd")
|
||||
var mk_row := func(l: int, d: int, eid: String) -> Dictionary:
|
||||
return {"level_index": l, "node_depth": d, "enemy_id": eid, "display_name": eid + "名",
|
||||
var mk_row := func(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}]}
|
||||
# 正向:同坐标两行不同敌种(相邻)= 混编合法,validate 零错
|
||||
# 正向:同带键两行不同敌种(相邻)= 混编合法,validate 零错
|
||||
var t_mix = EnemyTableScript.new()
|
||||
t_mix.rows.assign([mk_row.call(1, 5, "goblin"), mk_row.call(1, 5, "bugbear"),
|
||||
mk_row.call(1, 0, "goblin")]) # 末行=depth-0 锚行(validate 的 anchor 规则:每 level 必须有)
|
||||
t_mix.rows.assign([mk_row.call(1, "battle", 5, "goblin"), mk_row.call(1, "battle", 5, "bugbear"),
|
||||
mk_row.call(1, "battle", 0, "goblin"), mk_row.call(1, "boss", 9, "ogre_chief")])
|
||||
var errs: Array = t_mix.validate()
|
||||
Kit.check("混编行集加载通过(同坐标两行不同敌种相邻——行集恰一敌种校验退役的正向面)", errs.is_empty())
|
||||
# 正向:Boss 坐标两行同构合法(P4 乙——Boss 行集校验同样放开,加载层无 Boss 单行硬闸)
|
||||
Kit.check("混编行集加载通过(同带键两行不同敌种相邻——行集恰一敌种校验退役的正向面)", errs.is_empty())
|
||||
# 正向:Boss 行集两行同构合法(P4 乙——Boss 行集校验同样放开,加载层无 Boss 单行硬闸)
|
||||
var t_boss = EnemyTableScript.new()
|
||||
t_boss.rows.assign([mk_row.call(2, 9, "drider"), mk_row.call(2, 9, "bugbear"),
|
||||
mk_row.call(2, 0, "drider")]) # 末行=depth-0 锚行
|
||||
t_boss.rows.assign([mk_row.call(2, "battle", 0, "drider"), mk_row.call(2, "boss", 9, "drider"),
|
||||
mk_row.call(2, "boss", 9, "bugbear")])
|
||||
errs = t_boss.validate()
|
||||
Kit.check("Boss 坐标混编行集加载通过(P4 乙——Boss 多行合法,无硬闸)", errs.is_empty())
|
||||
# 负向:同坐标行不相邻 = rowset_rule 且报错定位到坐标(R6 行序纪律 + R3 错误类别可定位)
|
||||
Kit.check("Boss 带混编行集加载通过(P4 乙——Boss 多行合法,无硬闸)", errs.is_empty())
|
||||
# 负向:同行集行不相邻 = rowset_rule 且报错定位到带键(R6 行序纪律 + R3 错误类别可定位)
|
||||
var t_gap = EnemyTableScript.new()
|
||||
t_gap.rows.assign([mk_row.call(1, 5, "goblin"), mk_row.call(1, 9, "hobgoblin"), mk_row.call(1, 5, "bugbear"),
|
||||
mk_row.call(1, 0, "goblin")]) # 末行=depth-0 锚行(隔断关系不受影响)
|
||||
t_gap.rows.assign([mk_row.call(1, "battle", 5, "goblin"), mk_row.call(1, "battle", 9, "hobgoblin"),
|
||||
mk_row.call(1, "battle", 5, "bugbear"), mk_row.call(1, "battle", 0, "goblin"),
|
||||
mk_row.call(1, "boss", 9, "ogre_chief")])
|
||||
errs = t_gap.validate()
|
||||
var gap_hit := false
|
||||
for e in errs:
|
||||
if String(e.get("category", "")) == "rowset_rule" and String(e.get("message", "")).contains("(1,5)"):
|
||||
if String(e.get("category", "")) == "rowset_rule" and String(e.get("message", "")).contains("(1,battle,5)"):
|
||||
gap_hit = true
|
||||
Kit.check("行序相邻校验:同坐标行被隔断报 rowset_rule 且定位坐标(R6——行序即契约加载期兜底)", gap_hit)
|
||||
Kit.check("行序相邻校验:同带键行被隔断报 rowset_rule 且定位带键〔fv9〕〔fv20〕", gap_hit)
|
||||
# 负向:同敌种第二行仍被 dup_key 拦(键含 enemy_id——纪律①既有通道不回归)
|
||||
var t_dup = EnemyTableScript.new()
|
||||
t_dup.rows.assign([mk_row.call(1, 5, "goblin"), mk_row.call(1, 5, "goblin"),
|
||||
mk_row.call(1, 0, "goblin")]) # 末行=depth-0 锚行
|
||||
t_dup.rows.assign([mk_row.call(1, "battle", 5, "goblin"), mk_row.call(1, "battle", 5, "goblin"),
|
||||
mk_row.call(1, "battle", 0, "goblin"), mk_row.call(1, "boss", 9, "ogre_chief")])
|
||||
errs = t_dup.validate()
|
||||
var dup_hit := false
|
||||
for e in errs:
|
||||
if String(e.get("category", "")) == "dup_key":
|
||||
dup_hit = true
|
||||
Kit.check("行集内同敌种第二行仍被 dup_key 拦(纪律①——enemy_id 唯一)", dup_hit)
|
||||
Kit.check("同带键同敌种二行被 dup_key 新键拦(纪律①——enemy_id 唯一)〔fv8〕", dup_hit)
|
||||
# 负向:域校验不回归(单行 unit_count 越域仍拦——UNIT_COUNT_MAX 单种数语义下的同款域闸)
|
||||
var t_dom = EnemyTableScript.new()
|
||||
t_dom.rows.assign([mk_row.call(1, 5, "goblin"), mk_row.call(1, 0, "goblin")]) # 末行=depth-0 锚行
|
||||
t_dom.rows.assign([mk_row.call(1, "battle", 5, "goblin"), mk_row.call(1, "battle", 0, "goblin"),
|
||||
mk_row.call(1, "boss", 9, "ogre_chief")])
|
||||
t_dom.rows[0]["unit_count"] = 4
|
||||
errs = t_dom.validate()
|
||||
var dom_hit := false
|
||||
@@ -242,3 +244,30 @@ 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 封闭枚举(非法值拒——键面换代后新增判据)
|
||||
var t_kind = EnemyTableScript.new()
|
||||
t_kind.rows.assign([mk_row.call(1, "elite", 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)
|
||||
# 负向: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")])
|
||||
errs = t_no_anchor.validate()
|
||||
var no_anchor_hit := false
|
||||
for e in errs:
|
||||
if String(e.get("category", "")) == "anchor" and String(e.get("message", "")).contains("band 0"):
|
||||
no_anchor_hit = true
|
||||
Kit.check("缺 (battle, band 0) 锚行报 anchor〔fv10〕", no_anchor_hit)
|
||||
var t_no_boss = EnemyTableScript.new()
|
||||
t_no_boss.rows.assign([mk_row.call(1, "battle", 0, "goblin"), mk_row.call(1, "battle", 5, "bugbear")])
|
||||
errs = t_no_boss.validate()
|
||||
var no_boss_hit := false
|
||||
for e in errs:
|
||||
if String(e.get("category", "")) == "anchor" and String(e.get("message", "")).contains("Boss"):
|
||||
no_boss_hit = true
|
||||
Kit.check("缺 Boss 行集报 anchor(每关 Boss 行集 ≥1)〔fv10〕", no_boss_hit)
|
||||
Reference in new issue
Block a user