merge(迭代34 卡③ 敌人表深度带换代批): 遭遇寻址 (L,row_kind,depth_band) 三参化+表键迁移+校验/跨表/消费面随动+独立复审通过(回执 commit=9ff0909a,--code 全绿)
- 冒烟 1161 项检查 0 失败;单元 35 套件全绿;探针 --click/--full 全绿;渲染闸绿;静态扫描归零;熵增闸绿 - 11 回放确定性对拍:数值零变更按档位规则不发起(NEED-APPROVAL) - 独立验收审计两轮:首轮不通过(4 缺漏+4 描述不符)→ 修补后复审 A–F 全通过
This commit is contained in:
commit
43c0f3c118
21 files changed
+1052
-239
No files matched your search
@@ -324,9 +324,11 @@ func begin_battle(enc: Dictionary) -> bool:
|
||||
# 开流条件、消费序、消费次数逐位一致——零内容期随机流零变化;区间宽度为零的行不掷 = 「定值不骰」
|
||||
# 既有口径;atk 区间逐行同构 = 行值语义完整,属「每行一骰」语义内的
|
||||
# 行字段区间骰逐行化,非新增随机源形态)。
|
||||
var erowset_pre: Array[Variant] = db.resolve_enemy_rowset(int(enc.level_index), int(enc.node_depth))
|
||||
# 行类维度:节点身份 battle/boss 决定行集键域(缺省 battle 只服务测试直呼面——生产路径恒传 node_type)
|
||||
var enc_kind := String(enc.get("node_type", "battle"))
|
||||
var erowset_pre: Array[Variant] = db.resolve_enemy_rowset(int(enc.level_index), int(enc.node_depth), enc_kind)
|
||||
if erowset_pre.is_empty():
|
||||
_fatal("敌人表 行集解析为空(%d,%d)" % [int(enc.level_index), int(enc.node_depth)])
|
||||
_fatal("敌人表 行集解析为空(%d,%d,%s)" % [int(enc.level_index), int(enc.node_depth), enc_kind])
|
||||
return false
|
||||
var count_overrides: Array[Variant] = []
|
||||
var atk_overrides: Array[Variant] = []
|
||||
@@ -354,7 +356,7 @@ func begin_battle(enc: Dictionary) -> bool:
|
||||
_hero_atk = int(enc.get("hero_atk", int(db.hero.atk)))
|
||||
if not _enemy_system.spawn(int(enc.level_index), int(enc.node_depth), int(enc.hero_hp), count_overrides,
|
||||
_hero_atk, int(enc.get("hero_max_hp", -1)), atk_overrides,
|
||||
String(enc.get("attack_type", "")), String(enc.get("armor_type", ""))): # 两根类型轴(空串=锻造配置默认值回退,行为连续)
|
||||
String(enc.get("attack_type", "")), String(enc.get("armor_type", "")), enc_kind): # 两根类型轴(空串=锻造配置默认值回退,行为连续)+行类维(battle/boss)
|
||||
_fatal("敌人系统 spawn 被拒")
|
||||
return false
|
||||
if not _install_new_board():
|
||||
|
||||
@@ -190,8 +190,8 @@ func resolve_board_gen(L: int, D: int) -> Dictionary:
|
||||
return _resolver.resolve_board_gen(L, D)
|
||||
|
||||
|
||||
func resolve_enemy_rowset(L: int, D: int) -> Array[Dictionary]:
|
||||
return _resolver.resolve_enemy_rowset(L, D) # 行集数组形态(非单行 Dictionary)
|
||||
func resolve_enemy_rowset(L: int, D: int, row_kind: String) -> Array[Dictionary]:
|
||||
return _resolver.resolve_enemy_rowset(L, D, row_kind) # 行集数组形态(非单行 Dictionary;D 维带寻址)
|
||||
|
||||
|
||||
# 难度曲线与Boss DifficultyTable 消费面(敌人系统.spawn 调用):(L,D) 精确匹配修饰乘数,越域值 clamp(GDD Edge Case——组合事故非配置事故)
|
||||
|
||||
@@ -51,8 +51,9 @@ func cross_validate(loaded: Dictionary, push_diag: Callable) -> void:
|
||||
for s in required_sources:
|
||||
if not st_tab.source_label_map.has(s):
|
||||
push_diag.call("error", "segorder_violation", "source_label_map 缺必需键: %s" % s)
|
||||
# ④ LevelTable 节点悬空引用(battle/boss 的 (L,node_depth) 须在 board_gen 与 enemy 两表有精确行——
|
||||
# 线性推进器按精确深度行跑,floor 兜底静默降难度属调参事故,加载期直接拦)
|
||||
# ④ LevelTable 节点悬空引用(battle/boss 节点:board_gen 侧须有精确行;enemy 侧按
|
||||
# (关卡序号, 行类) 带寻址——落带即设计语义,节点深度高于顶带键=顶 clamp 消解属调参事故,加载期直接拦。
|
||||
# 同关同一张图多终点 Boss 节点解析带键须互异(共带=两终点命中同一 Boss 行集,「走哪条路打谁」落空))
|
||||
var lv: LevelTable = loaded.get("level")
|
||||
var bg: BoardGenTable = loaded.get("board_gen")
|
||||
var en: EnemyTable = loaded.get("enemy")
|
||||
@@ -60,9 +61,16 @@ func cross_validate(loaded: Dictionary, push_diag: Callable) -> void:
|
||||
var bg_keys := {}
|
||||
for row in bg.rows:
|
||||
bg_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true
|
||||
var en_keys := {}
|
||||
var en_bands := {} # "%d_%s" % [L, row_kind] → 升序带键数组
|
||||
for row in en.rows:
|
||||
en_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true
|
||||
var ek := "%d_%s" % [int(row.level_index), String(row.get("row_kind", ""))]
|
||||
if not en_bands.has(ek):
|
||||
en_bands[ek] = []
|
||||
var eb := int(row.get("depth_band", -1))
|
||||
if not (en_bands[ek] as Array).has(eb):
|
||||
(en_bands[ek] as Array).append(eb)
|
||||
for ek in en_bands.keys():
|
||||
(en_bands[ek] as Array).sort()
|
||||
for lrow in lv.rows:
|
||||
var li: int = int(lrow.level_index)
|
||||
# 模板池感知(关卡节点图生成 v0.4——池内全模板跨表校验,不只 row.nodes)
|
||||
@@ -73,16 +81,29 @@ func cross_validate(loaded: Dictionary, push_diag: Callable) -> void:
|
||||
else:
|
||||
for tpl in tpls:
|
||||
node_lists.append(tpl.get("nodes", []))
|
||||
for nodes_arr in node_lists:
|
||||
for node in nodes_arr:
|
||||
for tpl_i in range(node_lists.size()):
|
||||
var boss_bands := {} # 本图已占 Boss 带键
|
||||
for node in node_lists[tpl_i]:
|
||||
var t: String = String(node.get("type", ""))
|
||||
if t != "battle" and t != "boss":
|
||||
continue
|
||||
var key := "%d_%d" % [li, int(node.get("node_depth", -1))]
|
||||
var nd: int = int(node.get("node_depth", -1))
|
||||
var key := "%d_%d" % [li, nd]
|
||||
if not bg_keys.has(key):
|
||||
push_diag.call("error", "level_node_dangling", "level %d %s 节点 (%s) 在 board_gen 无精确行" % [li, t, key])
|
||||
if not en_keys.has(key):
|
||||
push_diag.call("error", "level_node_dangling", "level %d %s 节点 (%s) 在 enemy 无精确行" % [li, t, key])
|
||||
var nk := "%d_%s" % [li, t]
|
||||
if not en_bands.has(nk):
|
||||
push_diag.call("error", "level_node_dangling", "level %d %s 节点 (%d) 在 enemy 无该行类行集" % [li, t, nd])
|
||||
continue
|
||||
var hit_band: int = EnemyTable.band_of(en_bands[nk], nd)
|
||||
if hit_band < 0:
|
||||
push_diag.call("error", "level_node_dangling", "level %d %s 节点深度 %d 高于 (level %d, %s) 顶带键 %d——顶 clamp 消解" % [
|
||||
li, t, nd, li, t, int((en_bands[nk] as Array)[-1])])
|
||||
continue
|
||||
if t == "boss":
|
||||
if boss_bands.has(hit_band):
|
||||
push_diag.call("error", "rowset_rule", "level %d 模板 %d 多终点 Boss 节点同带(band %d)——共带使「走哪条路打谁」落空" % [li, tpl_i, hit_band])
|
||||
boss_bands[hit_band] = true
|
||||
# ⑤ 技能池引用完整性(敌人技能池批——敌人表池 × 技能表;独立入口 validate_skill_pool_links 供冒烟直验)
|
||||
var skt = loaded.get("skill")
|
||||
if skt != null and en != null:
|
||||
|
||||
@@ -30,11 +30,64 @@ func resolve_board_gen(L: int, D: int) -> Dictionary:
|
||||
return _resolve_progress(_board_gen_rows_get.call(), L, D, "board_gen")
|
||||
|
||||
|
||||
func resolve_enemy_rowset(L: int, D: int) -> Array[Dictionary]:
|
||||
# 行集数组形态:ceil 命中 (l*,d*) 后
|
||||
# 返回该坐标全部行、按表文件行序(行序即契约——同坐标多行文件内相邻由 EnemyTable.validate 兜底)。
|
||||
# 空集表达 = 空数组(生产表下 ceil 命中坐标恒有行,空集仅表空时可达——validate 已拦)。
|
||||
return _resolve_progress(_enemy_rows_get.call(), L, D, "enemy", true)
|
||||
func resolve_enemy_rowset(L: int, D: int, row_kind: String) -> Array[Dictionary]:
|
||||
# 带寻址:L 维沿进度行解析现状(ceil 向上取整保证偏难、越最大行 clamp),
|
||||
# D 维经 EnemyTable.band_of 落带——命中该 (L, row_kind) 的某一个 depth_band,
|
||||
# 返回该带全部行、按表文件行序(行序即契约——同带键多行文件内相邻由 EnemyTable.validate 兜底)。
|
||||
# 空集表达 = 空数组(band_keys 空时可达,交消费侧既有空行集 fatal 路径——validate 已拦无 Boss 行集)。
|
||||
if L < 1 or D < 0:
|
||||
_diag.call("fatal", "request_oob", "enemy 请求越域 (%d,%d)——进度坐标低于合法域(关卡序号≥1、节点深度≥0)" % [L, D])
|
||||
return []
|
||||
if not EnemyTable.ROW_KINDS.has(row_kind):
|
||||
_diag.call("fatal", "request_oob", "enemy 请求行类非法 (%d,%d,\"%s\")——row_kind 域 {battle, boss}" % [L, D, row_kind])
|
||||
return []
|
||||
var rows: Array[Dictionary] = []
|
||||
rows.assign(_enemy_rows_get.call())
|
||||
var level_keys: Array[int] = []
|
||||
for row in rows:
|
||||
var lv := int(row.get("level_index", 0))
|
||||
if not level_keys.has(lv):
|
||||
level_keys.append(lv)
|
||||
if level_keys.is_empty():
|
||||
_diag.call("fatal", "structural", "enemy 行集为空——锚行校验应已拦截")
|
||||
return []
|
||||
level_keys.sort()
|
||||
var l_star := _ceil_key(level_keys, L)
|
||||
if l_star < 0:
|
||||
l_star = int(level_keys[-1]) # 超出最大行 → 取最大行封顶
|
||||
_warn_resolve("enemy", "resolve_level_clamp", "enemy 请求 (%d,%d) clamp 解析(超出最大关卡取最大值)→ 关卡 %d" % [L, D, l_star], l_star, D)
|
||||
elif l_star != L:
|
||||
_warn_resolve("enemy", "resolve_level_ceil", "enemy 请求 (%d,%d) ceil 解析(向上取整保证偏难)→ 关卡 %d" % [L, D, l_star], l_star, D)
|
||||
var band_keys: Array[int] = []
|
||||
for row in rows:
|
||||
if int(row.get("level_index", 0)) == l_star and String(row.get("row_kind", "")) == row_kind:
|
||||
var bv := int(row.get("depth_band", -1))
|
||||
if not band_keys.has(bv):
|
||||
band_keys.append(bv)
|
||||
if band_keys.is_empty():
|
||||
return []
|
||||
band_keys.sort()
|
||||
var d_star := EnemyTable.band_of(band_keys, D)
|
||||
if d_star < 0:
|
||||
d_star = int(band_keys[-1]) # 越顶 clamp 顶带(调参事故面,跨表校验另报)
|
||||
_warn_resolve("enemy", "resolve_depth_clamp", "enemy 请求 (%d,%d) clamp 解析(超出顶带取顶带)→ (%d,band %d)" % [L, D, l_star, d_star], l_star, d_star)
|
||||
elif d_star != D:
|
||||
_warn_resolve("enemy", "resolve_depth_ceil", "enemy 请求 (%d,%d) ceil 解析(向上取整保证偏难)→ (%d,band %d)" % [L, D, l_star, d_star], l_star, d_star)
|
||||
var all_out: Array[Dictionary] = []
|
||||
for row in rows:
|
||||
if int(row.get("level_index", 0)) == l_star and String(row.get("row_kind", "")) == row_kind \
|
||||
and int(row.get("depth_band", -1)) == d_star:
|
||||
all_out.append(row)
|
||||
return all_out
|
||||
|
||||
|
||||
# 解析告警单点(类别 + 命中键四元组去重——R1 既有去重注册表)
|
||||
func _warn_resolve(table_id: String, category: String, message: String, l_key: int, d_key: int) -> void:
|
||||
var dedup_key := "%s|%s|%d|%d" % [table_id, category, l_key, d_key]
|
||||
if _warn_dedup.has(dedup_key):
|
||||
return
|
||||
_warn_dedup[dedup_key] = true
|
||||
_diag.call("warning", category, message)
|
||||
|
||||
|
||||
# 难度曲线与Boss DifficultyTable 消费面(敌人系统.spawn 调用):(L,D) 精确匹配修饰乘数,越域值 clamp(GDD Edge Case——组合事故非配置事故)
|
||||
@@ -51,7 +104,7 @@ func resolve_difficulty_modifiers(L: int, D: int) -> Dictionary:
|
||||
return {}
|
||||
|
||||
|
||||
func _resolve_progress(rows: Array, L: int, D: int, table_id: String, return_all := false) -> Variant:
|
||||
func _resolve_progress(rows: Array, L: int, D: int, table_id: String) -> Variant:
|
||||
if L < 1 or D < 0:
|
||||
_diag.call("fatal", "request_oob", "%s 请求越域 (%d,%d)——进度坐标低于合法域(关卡序号≥1、节点深度≥0)" % [table_id, L, D])
|
||||
return {}
|
||||
@@ -98,12 +151,6 @@ func _resolve_progress(rows: Array, L: int, D: int, table_id: String, return_all
|
||||
_warn_dedup[dedup_key] = true
|
||||
var verb := "clamp 解析(超出最大行取最大值)" if missing.contains("clamp") else "ceil 解析(向上取整保证偏难)"
|
||||
_diag.call("warning", "resolve_" + missing, "%s 请求 (%d,%d) %s → (%d,%d) [%s]——2026-09-18 拍板" % [table_id, L, D, verb, l_star, d_star, missing])
|
||||
if return_all:
|
||||
var all_out: Array[Dictionary] = []
|
||||
for row in rows:
|
||||
if int(row.get("level_index", 0)) == l_star and int(row.get("node_depth", 0)) == d_star:
|
||||
all_out.append(row)
|
||||
return all_out
|
||||
for row in rows:
|
||||
if int(row.get("level_index", 0)) == l_star and int(row.get("node_depth", 0)) == d_star:
|
||||
return row
|
||||
|
||||
@@ -35,14 +35,14 @@ func _init(db_ref: ConfigDb) -> void:
|
||||
# ≤ ENEMY_ROWSET_INSTANCE_MAX 守卫 fail-fast。
|
||||
func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_overrides: Array = [],
|
||||
atk_override: int = -1, max_hp_override: int = -1, enemy_atk_overrides: Array = [],
|
||||
hero_attack_type: String = "", hero_armor_type: String = "") -> bool:
|
||||
hero_attack_type: String = "", hero_armor_type: String = "", row_kind: String = "battle") -> bool:
|
||||
if _spawned:
|
||||
push_warning("[敌人系统] Alive 相位重复 spawn(相位守卫)")
|
||||
return false
|
||||
# 行集数组口径(resolver 返回该坐标全部行、文件序)
|
||||
var rowset: Array[Variant] = db.resolve_enemy_rowset(level_index, node_depth)
|
||||
# 行集数组口径(resolver 按 (L,band) 带寻址返回全部行、文件序)
|
||||
var rowset: Array[Variant] = db.resolve_enemy_rowset(level_index, node_depth, row_kind)
|
||||
if rowset.is_empty():
|
||||
push_error("[敌人系统] 遭遇行解析失败/越域 (%d,%d)" % [level_index, node_depth])
|
||||
push_error("[敌人系统] 遭遇行解析失败/越域 (%d,%d,%s)" % [level_index, node_depth, row_kind])
|
||||
assert(false, "遭遇行悬空(R1 前置守卫)")
|
||||
return false
|
||||
var eff_atk: int = atk_override if atk_override >= 1 else int(db.hero.atk)
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
## 敌人表 schema:逐 (level_index, node_depth, enemy_id) 行集的类型化读取(行集可多行混编;垂直切片验证:完整战斗逐行归因)。
|
||||
## 行序即契约:同坐标多行必须文件内相邻——resolver 返回序/生成序/叙述取首行全依赖文件行序。
|
||||
## 敌人表 schema:逐 (level_index, row_kind, depth_band, enemy_id) 行集的类型化读取(行集可多行混编;垂直切片验证:完整战斗逐行归因)。
|
||||
## 行序即契约:同带键多行必须文件内相邻——resolver 返回序/生成序/叙述取首行全依赖文件行序。
|
||||
## 深度带 = 节点深度所在的区间:配池按带不按点,难度坡随带递增;row_kind 承载「Boss 行按终点配置」。
|
||||
## 行集实例总数 ≤4 是开战骰后守卫(加载期不可判,闸在单位容器 spawn);技能池构成纪律需跨表视野,校验在 ConfigDB 跨表通道。
|
||||
class_name EnemyTable
|
||||
extends Resource
|
||||
|
||||
@export var rows: Array[Dictionary] = [] # Array[Dictionary]
|
||||
|
||||
# 行类封闭枚举(对齐关卡节点 type 词表):battle = 战斗节点配池,boss = 终点行集
|
||||
const ROW_KINDS := ["battle", "boss"]
|
||||
|
||||
# 护甲类型封闭枚举(克制矩阵本体在 SettlementTable;
|
||||
# 语义键 {light, ward, heavy} = 轻甲 / 魔法护甲 / 重甲)
|
||||
const ARMOR_TYPES := ["light", "ward", "heavy"]
|
||||
@@ -35,19 +39,23 @@ func validate() -> Array:
|
||||
errs.append({"category": "empty_table", "message": "EnemyTable 为空"})
|
||||
return errs
|
||||
var seen := {}
|
||||
var last_rk := ""
|
||||
var closed_rk := {}
|
||||
var last_rowset_key := ""
|
||||
var closed_rowset := {}
|
||||
var anchor_ok := {}
|
||||
var boss_ok := {}
|
||||
for row in rows:
|
||||
var L: int = int(row.get("level_index", 0))
|
||||
var D: int = int(row.get("node_depth", -1))
|
||||
var rk: String = String(row.get("row_kind", ""))
|
||||
var band: int = int(row.get("depth_band", -1))
|
||||
var eid: String = row.get("enemy_id", "")
|
||||
var key := "%d_%d_%s" % [L, D, eid]
|
||||
var key := "%d_%s_%d_%s" % [L, rk, band, eid]
|
||||
if seen.has(key):
|
||||
errs.append({"category": "dup_key", "message": "EnemyTable 键重复: %s" % key})
|
||||
seen[key] = true
|
||||
if L < 1 or D < 0:
|
||||
errs.append({"category": "domain", "message": "进度键越域 (%d,%d)" % [L, D]})
|
||||
if L < 1 or band < 0:
|
||||
errs.append({"category": "domain", "message": "进度键越域 (%d,band %d)" % [L, band]})
|
||||
if not ROW_KINDS.has(rk):
|
||||
errs.append({"category": "domain", "message": "%s row_kind 未知枚举: \"%s\"(域 {battle, boss})" % [eid, rk]})
|
||||
if eid == "":
|
||||
errs.append({"category": "domain", "message": "enemy_id 空"})
|
||||
if String(row.get("display_name", "")) == "":
|
||||
@@ -118,23 +126,41 @@ func validate() -> Array:
|
||||
var ac: int = int(row.get("armor_class", 0))
|
||||
if ac < 0 or ac > 999:
|
||||
errs.append({"category": "domain", "message": "%s armor_class 越域 [0,999]: %d" % [eid, ac]})
|
||||
# 行集构成纪律②(行序即契约,敌人系统 v0.15):同坐标多行必须
|
||||
# 文件内相邻。迭代中记录上一行坐标;当前坐标曾出现过但 ≠ 上一行坐标 = 同坐标行被隔断,报错。
|
||||
# 同坐标多敌种混编合法;同坐标同敌种第二行仍由
|
||||
# 行集构成纪律②(行序即契约,敌人系统 v0.15):同带键多行必须
|
||||
# 文件内相邻。迭代中记录上一行行集键;当前行集键曾出现过但 ≠ 上一行行集键 = 同带键行被隔断,报错。
|
||||
# 同带键多敌种混编合法;同带键同敌种第二行仍由
|
||||
# dup_key 拦截(键含 enemy_id)。错误类别沿用 rowset_rule(定位面不变)。
|
||||
var rk := "%d_%d" % [L, D]
|
||||
if rk != last_rk:
|
||||
if closed_rk.has(rk):
|
||||
errs.append({"category": "rowset_rule", "message": "(%d,%d) 行集行序违例:同坐标多行必须文件内相邻(行序即契约——resolver 返回序/生成序/叙述取首行全依赖文件行序)" % [L, D]})
|
||||
closed_rk[rk] = true
|
||||
last_rk = rk
|
||||
anchor_ok[L] = anchor_ok.get(L, false) or D == 0
|
||||
var rowset_key := "%d_%s_%d" % [L, rk, band]
|
||||
if rowset_key != last_rowset_key:
|
||||
if closed_rowset.has(rowset_key):
|
||||
errs.append({"category": "rowset_rule", "message": "(%d,%s,%d) 行集行序违例:同带键多行必须文件内相邻(行序即契约——resolver 返回序/生成序/叙述取首行全依赖文件行序)" % [L, rk, band]})
|
||||
closed_rowset[rowset_key] = true
|
||||
last_rowset_key = rowset_key
|
||||
if rk == "battle" and band == 0:
|
||||
anchor_ok[L] = true
|
||||
elif rk == "boss":
|
||||
boss_ok[L] = true
|
||||
if not anchor_ok.has(L):
|
||||
anchor_ok[L] = false
|
||||
for L in anchor_ok.keys():
|
||||
if not anchor_ok[L]:
|
||||
errs.append({"category": "anchor", "message": "EnemyTable level %d 缺 depth-0 锚行" % L})
|
||||
errs.append({"category": "anchor", "message": "EnemyTable level %d 缺 (battle, band 0) 锚行" % L})
|
||||
if not boss_ok.has(L):
|
||||
errs.append({"category": "anchor", "message": "EnemyTable level %d 缺 Boss 行集(row_kind=boss 行 ≥1)" % L})
|
||||
return errs
|
||||
|
||||
|
||||
# 深度带切法单点(唯一实现,禁第二份算法):band_keys = 该 (level_index, row_kind) 的
|
||||
# depth_band 键升序数组;返回首个 ≥ D 的键(请求落带取偏难一侧)。
|
||||
# D 高于全部键 → 返回 -1——resolver 侧 clamp 顶带+warning,跨表校验侧判「顶 clamp 消解」违例;
|
||||
# band_keys 空 → 同样返回 -1(resolver 随之返回空行集,交消费侧既有空行集 fatal 路径)。
|
||||
static func band_of(band_keys: Array, D: int) -> int:
|
||||
for k in band_keys:
|
||||
if int(k) >= D:
|
||||
return int(k)
|
||||
return -1
|
||||
|
||||
|
||||
# 进度键解析单点收口(全仓统走
|
||||
# ConfigDbResolver.resolve_enemy_rowset ceil 版行集数组:ceil 保证偏难,
|
||||
# 禁双算法并存〔状态记录 312〕;坐标须全精确命中)。
|
||||
# ConfigDbResolver.resolve_enemy_rowset 带寻址行集数组:D 维经本表 band_of 落带(偏难一侧),
|
||||
# 禁双算法并存〔状态记录 312〕;L 维 ceil/clamp 与越顶 clamp 语义归 resolver)。
|
||||
+45
-21
@@ -6,7 +6,8 @@
|
||||
script = ExtResource("1")
|
||||
rows = [{
|
||||
"level_index": 1,
|
||||
"node_depth": 0,
|
||||
"row_kind": "battle",
|
||||
"depth_band": 0,
|
||||
"enemy_id": "goblin",
|
||||
"display_name": "地精",
|
||||
"unit_count": 1,
|
||||
@@ -27,7 +28,8 @@ rows = [{
|
||||
"armor_class": 0
|
||||
}, {
|
||||
"level_index": 1,
|
||||
"node_depth": 1,
|
||||
"row_kind": "battle",
|
||||
"depth_band": 1,
|
||||
"enemy_id": "goblin",
|
||||
"display_name": "地精一伙",
|
||||
"unit_count": 2,
|
||||
@@ -48,7 +50,8 @@ rows = [{
|
||||
"armor_class": 1
|
||||
}, {
|
||||
"level_index": 1,
|
||||
"node_depth": 2,
|
||||
"row_kind": "battle",
|
||||
"depth_band": 2,
|
||||
"enemy_id": "bugbear",
|
||||
"display_name": "熊地精",
|
||||
"unit_count": 1,
|
||||
@@ -71,7 +74,8 @@ rows = [{
|
||||
"armor_class": 2
|
||||
}, {
|
||||
"level_index": 1,
|
||||
"node_depth": 3,
|
||||
"row_kind": "battle",
|
||||
"depth_band": 3,
|
||||
"enemy_id": "hobgoblin",
|
||||
"display_name": "大地精",
|
||||
"unit_count": 2,
|
||||
@@ -95,7 +99,8 @@ rows = [{
|
||||
"armor_class": 2
|
||||
}, {
|
||||
"level_index": 1,
|
||||
"node_depth": 4,
|
||||
"row_kind": "boss",
|
||||
"depth_band": 4,
|
||||
"enemy_id": "ogre_chief",
|
||||
"display_name": "食人魔酋长",
|
||||
"unit_count": 1,
|
||||
@@ -122,7 +127,8 @@ rows = [{
|
||||
"armor_class": 8
|
||||
}, {
|
||||
"level_index": 2,
|
||||
"node_depth": 0,
|
||||
"row_kind": "battle",
|
||||
"depth_band": 0,
|
||||
"enemy_id": "bugbear",
|
||||
"display_name": "熊地精",
|
||||
"unit_count": 1,
|
||||
@@ -145,7 +151,8 @@ rows = [{
|
||||
"armor_class": 1
|
||||
}, {
|
||||
"level_index": 2,
|
||||
"node_depth": 1,
|
||||
"row_kind": "battle",
|
||||
"depth_band": 1,
|
||||
"enemy_id": "bugbear",
|
||||
"display_name": "熊地精猎群",
|
||||
"unit_count": 2,
|
||||
@@ -169,7 +176,8 @@ rows = [{
|
||||
"armor_class": 2
|
||||
}, {
|
||||
"level_index": 2,
|
||||
"node_depth": 4,
|
||||
"row_kind": "battle",
|
||||
"depth_band": 4,
|
||||
"enemy_id": "giant_spider",
|
||||
"display_name": "巨蜘蛛",
|
||||
"unit_count": 2,
|
||||
@@ -192,7 +200,8 @@ rows = [{
|
||||
"armor_class": 2
|
||||
}, {
|
||||
"level_index": 2,
|
||||
"node_depth": 5,
|
||||
"row_kind": "battle",
|
||||
"depth_band": 5,
|
||||
"enemy_id": "bugbear",
|
||||
"display_name": "熊地精重击手",
|
||||
"unit_count": 1,
|
||||
@@ -215,7 +224,8 @@ rows = [{
|
||||
"armor_class": 8
|
||||
}, {
|
||||
"level_index": 2,
|
||||
"node_depth": 5,
|
||||
"row_kind": "battle",
|
||||
"depth_band": 5,
|
||||
"enemy_id": "giant_spider",
|
||||
"display_name": "巨蜘蛛巢群",
|
||||
"unit_count": 2,
|
||||
@@ -242,7 +252,8 @@ rows = [{
|
||||
"armor_class": 2
|
||||
}, {
|
||||
"level_index": 2,
|
||||
"node_depth": 6,
|
||||
"row_kind": "battle",
|
||||
"depth_band": 6,
|
||||
"enemy_id": "giant_spider",
|
||||
"display_name": "巨蜘蛛巢群",
|
||||
"unit_count": 2,
|
||||
@@ -269,7 +280,8 @@ rows = [{
|
||||
"armor_class": 2
|
||||
}, {
|
||||
"level_index": 2,
|
||||
"node_depth": 7,
|
||||
"row_kind": "battle",
|
||||
"depth_band": 7,
|
||||
"enemy_id": "giant_spider",
|
||||
"display_name": "巨蜘蛛巢群",
|
||||
"unit_count": 2,
|
||||
@@ -296,7 +308,8 @@ rows = [{
|
||||
"armor_class": 3
|
||||
}, {
|
||||
"level_index": 2,
|
||||
"node_depth": 9,
|
||||
"row_kind": "boss",
|
||||
"depth_band": 9,
|
||||
"enemy_id": "drider",
|
||||
"display_name": "蛛化卓尔精灵",
|
||||
"unit_count": 1,
|
||||
@@ -326,7 +339,8 @@ rows = [{
|
||||
"armor_class": 10
|
||||
}, {
|
||||
"level_index": 3,
|
||||
"node_depth": 0,
|
||||
"row_kind": "battle",
|
||||
"depth_band": 0,
|
||||
"enemy_id": "duergar",
|
||||
"display_name": "灰矮人",
|
||||
"unit_count": 1,
|
||||
@@ -352,7 +366,8 @@ rows = [{
|
||||
"armor_class": 2
|
||||
}, {
|
||||
"level_index": 3,
|
||||
"node_depth": 1,
|
||||
"row_kind": "battle",
|
||||
"depth_band": 1,
|
||||
"enemy_id": "hook_horror",
|
||||
"display_name": "钩爪恐魔",
|
||||
"unit_count": 2,
|
||||
@@ -375,7 +390,8 @@ rows = [{
|
||||
"armor_class": 3
|
||||
}, {
|
||||
"level_index": 3,
|
||||
"node_depth": 4,
|
||||
"row_kind": "battle",
|
||||
"depth_band": 4,
|
||||
"enemy_id": "hook_horror",
|
||||
"display_name": "钩爪恐魔",
|
||||
"unit_count": 2,
|
||||
@@ -399,7 +415,8 @@ rows = [{
|
||||
"armor_class": 3
|
||||
}, {
|
||||
"level_index": 3,
|
||||
"node_depth": 5,
|
||||
"row_kind": "battle",
|
||||
"depth_band": 5,
|
||||
"enemy_id": "duergar",
|
||||
"display_name": "灰矮人重装手",
|
||||
"unit_count": 1,
|
||||
@@ -425,7 +442,8 @@ rows = [{
|
||||
"armor_class": 9
|
||||
}, {
|
||||
"level_index": 3,
|
||||
"node_depth": 6,
|
||||
"row_kind": "battle",
|
||||
"depth_band": 6,
|
||||
"enemy_id": "hook_horror",
|
||||
"display_name": "钩爪恐魔巢群",
|
||||
"unit_count": 2,
|
||||
@@ -449,7 +467,8 @@ rows = [{
|
||||
"armor_class": 3
|
||||
}, {
|
||||
"level_index": 3,
|
||||
"node_depth": 7,
|
||||
"row_kind": "battle",
|
||||
"depth_band": 7,
|
||||
"enemy_id": "duergar",
|
||||
"display_name": "灰矮人小队",
|
||||
"unit_count": 2,
|
||||
@@ -476,7 +495,8 @@ rows = [{
|
||||
"armor_class": 4
|
||||
}, {
|
||||
"level_index": 3,
|
||||
"node_depth": 9,
|
||||
"row_kind": "boss",
|
||||
"depth_band": 9,
|
||||
"enemy_id": "duergar_king",
|
||||
"display_name": "灰矮人石君王",
|
||||
"unit_count": 1,
|
||||
@@ -650,7 +670,7 @@ rows = [{
|
||||
; 调参二补充:上述八行 atk 区间 1-3→0-2(中点恒等期望当量锚不变式——atk=1 需区间中点同步 1;0=掷点空刀,保随机手感)。
|
||||
;
|
||||
; 混编遭遇首批投放批(2026-10-07 迭代 18 卡⑤——需求卡片 v1.2 裁点全裁 P1-P4 甲;机制前置=敌人表行集多行批
|
||||
; 已落籍 9cc4b8b):首个混编遭遇行落表 = (2,5) 双行行集〔R6 行序契约:同坐标两行文件内相邻、
|
||||
; 已提交 9cc4b8b):首个混编遭遇行落表 = (2,5) 双行行集〔R6 行序契约:同坐标两行文件内相邻、
|
||||
; 行序=声明序=生成序——resolver 返回序/生成序/断言面全依赖文件行序〕:
|
||||
; 行 1 = 既有 熊地精重击手 (2,5,bugbear)(行值零变更);
|
||||
; 行 2 = 新增 (2,5,giant_spider) 巨蜘蛛巢群——行值整行复制 (2,6) 巢群行(P4 甲零数值新增:unit_count 2/
|
||||
@@ -669,3 +689,7 @@ rows = [{
|
||||
; N3 敌方攻击类型轴开列(2026-10-13 迭代 24 卡①,承接锻造工坊实装批迁移面⑥):20 行全填 normal 挂敌种行——
|
||||
; 不做差异化(pierce/magic 属新设计另立项)。全局平移效应=主角穿重甲受伤 −1 / 魔法护甲 +1 / 轻甲 0,
|
||||
; 锻造甲路线首次具备战斗意义;克制矩阵数值零触碰(settlement.tres armor_effectiveness 不改)。
|
||||
;
|
||||
; 行键 = (level_index, row_kind, depth_band, enemy_id);row_kind ∈ {battle, boss}——battle=战斗节点配池,boss=终点行集;
|
||||
; depth_band=节点深度所在的带键,请求落带取首个 ≥ 深度的键(偏难一侧);越顶带由 resolver clamp、跨表校验判为调参事故。
|
||||
; hobgoblin 行为缺口行:无立绘、不在敌种表——留任在役,补齐三件套(立绘+语义模板+敌种表行)归敌种内容批。
|
||||
@@ -73,7 +73,7 @@ static func boss_name(runner, db) -> String:
|
||||
if runner != null:
|
||||
var node: Dictionary[Variant, Variant] = runner.current_node()
|
||||
if String(node.get("type", "")) == "boss":
|
||||
var rowset: Array[Variant] = db.resolve_enemy_rowset(int(runner.level_row.level_index), int(node.get("node_depth", -1)))
|
||||
var rowset: Array[Variant] = db.resolve_enemy_rowset(int(runner.level_row.level_index), int(node.get("node_depth", -1)), "boss")
|
||||
if not rowset.is_empty():
|
||||
return String(rowset[0].get("display_name", ""))
|
||||
return "遗迹守卫"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# 定位:分析工具·非验收件——零断言取数工具,不出入任何闸。
|
||||
# 技能池节奏探针:地精一伙 (1,1) 750 出招零突刺异常的定位——
|
||||
# ① 表侧:resolve_enemy_rowset(1,1) 的池快照内容;
|
||||
# ① 表侧:resolve_enemy_rowset(1, 1, "battle") 的池快照内容;
|
||||
# ② 摇号侧:隔离 SkillLottery.draw 大样本份额;
|
||||
# ③ 战斗侧:真实 (1,1) 战斗逐执行行按 skill_id 计数。
|
||||
# 运行:Godot_console.exe --headless --path . --script res://smoke/skill_rhythm_probe.gd
|
||||
@@ -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
@@ -655,6 +655,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()
|
||||
@@ -974,24 +975,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"]
|
||||
@@ -2029,7 +2031,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)
|
||||
@@ -2092,6 +2094,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)
|
||||
@@ -2221,29 +2272,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()
|
||||
@@ -2255,7 +2308,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
|
||||
@@ -2628,8 +2681,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
|
||||
@@ -2653,7 +2706,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 布置路径)")
|
||||
@@ -3032,7 +3085,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):
|
||||
@@ -3097,7 +3150,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):
|
||||
@@ -3122,7 +3175,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})
|
||||
@@ -3150,7 +3203,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(加法核)")
|
||||
@@ -3184,7 +3237,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)
|
||||
@@ -3238,7 +3291,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:找到安全格封印目标")
|
||||
@@ -3278,7 +3331,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:找到危险格封印目标")
|
||||
@@ -3308,7 +3361,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)
|
||||
@@ -3413,12 +3466,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 = []
|
||||
@@ -3436,7 +3498,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:
|
||||
@@ -3444,6 +3506,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:
|
||||
@@ -3481,7 +3573,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 != "":
|
||||
@@ -3548,7 +3640,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)
|
||||
@@ -3566,7 +3658,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))
|
||||
@@ -3583,7 +3675,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 血量不可控)
|
||||
@@ -4054,7 +4146,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
|
||||
@@ -4185,44 +4277,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
|
||||
@@ -4231,7 +4334,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
|
||||
@@ -4263,7 +4366,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))")
|
||||
@@ -4280,13 +4383,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
|
||||
@@ -4403,7 +4506,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)})
|
||||
@@ -4510,7 +4613,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):
|
||||
@@ -4680,53 +4783,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 := {}
|
||||
@@ -4743,29 +4847,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
|
||||
@@ -4791,23 +4898,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)
|
||||
@@ -4940,7 +5047,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},
|
||||
@@ -4948,7 +5055,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 基线)
|
||||
@@ -5148,17 +5259,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)
|
||||
@@ -8,10 +8,20 @@ const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
const NarratorScript := preload("res://presentation/battle_narrator.gd")
|
||||
|
||||
|
||||
# 叙述寻址的 runner 替身:只提供 boss_name 实际读的两项(当前节点、关卡行)
|
||||
class _BossRunner:
|
||||
var level_row: Dictionary = {"level_index": 1}
|
||||
var node: Dictionary = {}
|
||||
|
||||
func current_node() -> Dictionary:
|
||||
return node
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_test_numeric_lines_carry_values()
|
||||
_test_end_text_differs_by_result()
|
||||
_test_phase_text_nonempty()
|
||||
_test_boss_name_uses_boss_rowset()
|
||||
quit(Kit.finish("battle_narrator_test"))
|
||||
|
||||
|
||||
@@ -42,3 +52,31 @@ func _test_phase_text_nonempty() -> void:
|
||||
Kit.check("相位叙述非空(实际「%s」)" % t, t != "")
|
||||
var boss: String = NarratorScript.phase_text(2, "", false, true, "灰矮人石君王")
|
||||
Kit.check("Boss 在场时叙述带入 Boss 名(实际「%s」)" % boss, boss.contains("灰矮人石君王"))
|
||||
|
||||
|
||||
# 叙述寻址按行类取 Boss 行集(S 清单 S15 换代):Boss 节点名必须来自 (关卡, boss, 带) 行集,
|
||||
# 不能落到同深度的战斗行集——期望名由表行集首行读出,不写死字面量。
|
||||
func _test_boss_name_uses_boss_rowset() -> void:
|
||||
var db = load("res://core/config_db.gd").new()
|
||||
var loaded: bool = db.load_tables("res://data/tables", false)
|
||||
Kit.check_quiet(loaded, "叙述寻址用例:数值表加载成功")
|
||||
if not loaded:
|
||||
return
|
||||
var boss_row := {}
|
||||
for row in db.enemy.rows:
|
||||
if int(row.level_index) == 1 and String(row.get("row_kind", "")) == "boss":
|
||||
boss_row = row
|
||||
break
|
||||
Kit.check_quiet(not boss_row.is_empty(), "叙述寻址用例:第一关有 Boss 行")
|
||||
if boss_row.is_empty():
|
||||
return
|
||||
var boss_depth: int = int(boss_row.depth_band)
|
||||
var boss_rows: Array = db.resolve_enemy_rowset(1, boss_depth, "boss")
|
||||
var battle_rows: Array = db.resolve_enemy_rowset(1, boss_depth, "battle")
|
||||
var boss_expect := String(boss_rows[0].get("display_name", ""))
|
||||
var battle_name := String(battle_rows[0].get("display_name", ""))
|
||||
var runner := _BossRunner.new()
|
||||
runner.node = {"node_depth": boss_depth, "type": "boss"}
|
||||
var got: String = NarratorScript.boss_name(runner, db)
|
||||
Kit.check("Boss 名取自 Boss 行集首行(期望「%s」实际「%s」)〔fv24〕" % [boss_expect, got], got == boss_expect)
|
||||
Kit.check("Boss 名与同深度战斗行集名不同(期望「%s」≠「%s」——行类路由可判别)〔fv24〕" % [boss_expect, battle_name], boss_expect != battle_name)
|
||||
@@ -0,0 +1,18 @@
|
||||
# 迭代 34 代拍板追认单(2026-10-22)
|
||||
|
||||
> 本单只登记实现期在卡片 §10 白名单内所做的代拍项,属备案确认,不是事后授权。白名单依据=`reports/需求卡片-敌人表深度带换代批-2026-10-21.md` §10。
|
||||
|
||||
## 卡③ 敌人表深度带换代批
|
||||
|
||||
| # | 白名单条目 | 实际做法 | 约束核验 |
|
||||
|---|---|---|---|
|
||||
| 1 | 带切法实现形态与带键编号(§10-1) | 新增 `EnemyTable.band_of(band_keys, D)` 纯静态函数:对升序带键取首个 ≥ D 的键,全部小于 D 时返回 -1;resolver 侧 -1 → 取顶带+`resolve_depth_clamp` 告警(四元组去重沿用) | 迁移期带键=旧 `node_depth` 数值,请求逐位等价;单点收口(resolver 与跨表校验都调这一个函数);D<0 仍走 `request_oob` 致命通道 |
|
||||
| 2 | 新字段/常量命名与枚举值(§10-2) | 表列名 `row_kind`/`depth_band`;常量 `EnemyTable.ROW_KINDS := ["battle", "boss"]`;枚举字符串与关卡节点 `type` 词表同字 | 与节点 type 词表一致;非法值由 `validate` 的 `domain` 类别拒绝 |
|
||||
| 3 | `resolve_enemy_rowset` 第三参命名与缺省映射(§10-3) | 第三参命名 `row_kind`;`BattleOrchestrator` 由 `enc.node_type` 取值、缺省 `"battle"` | 生产路径 `level_runner` 恒传 `node_type`;缺省值只服务冒烟/单测直呼面 |
|
||||
| 4 | 校验错误类别命名(§10-4) | 全部沿用既有类别:`domain`/`dup_key`/`rowset_rule`/`anchor`/`level_node_dangling` | 未新增类别;Boss 同带与顶带消解分别复用 `rowset_rule` 与 `level_node_dangling` |
|
||||
| 5 | 冒烟断言的定位写法与措辞(§10-5) | 首遇位一律改为扫表定位:`_first_encounter_band(L, enemy_id)` 取该 (关卡, battle, 敌种) 的最低带;断言名带 fv 尾缀 | 首遇定位零写死坐标;写死坐标的旧断言全部移除 |
|
||||
| 6 | 提交切分与分支命名(§10-6) | 分支 `iter34-enemy-band`;卡③分两笔:核心与数据面一笔、断言面一笔 | 两笔都在 `game/` 内,批次合并时按 diff 推代码域 |
|
||||
|
||||
## 白名单外未代拍事项
|
||||
|
||||
无。实现期未遇到需要超出 §10 清单自决的设计分歧。
|
||||
@@ -0,0 +1,37 @@
|
||||
# 迭代 34 反哺点清单(2026-10-22)
|
||||
|
||||
> 本批是代码实现批,`design/` 整目录与根规则文件只读。以下五条是实现完成后需要对设计文档与流程清单做的改写,逐条写清「哪个文件哪一节、改成什么、依据」。用户裁决后由文档批落文。
|
||||
|
||||
## ① 敌人系统.md:坐标记法与表校验措辞换代
|
||||
|
||||
- 文件与位置:`design/gdd/systems/战斗循环/敌人系统.md` :81(hobgoblin 缺口登记)
|
||||
- 改成:坐标记法由「(1,3),即第一关第四节点」改为「(1, battle, 3)」。数据行位置未变(仍在第一关带 3),只换记法。
|
||||
- 依据:本卡契约 1(寻址键由 (关卡, 节点深度) 换成 (关卡, 行类, 深度带));冒烟 `smoke_all.gd` 的 hobgoblin 键位断言〔fv12〕按新记法钉死。
|
||||
|
||||
- 文件与位置:同文件 :278(表校验项清单中的行集构成违例一句)
|
||||
- 改成:「行集内同敌种第二行 / 同坐标多行不相邻」→「同带键多行不相邻」;同时把「同坐标」类表述统一为「同带键」。
|
||||
- 依据:本卡契约 4(行集构成纪律由坐标相邻改为带键相邻);`enemy_table.gd` 的 `rowset_rule` 校验键串已换为 `"%d_%s_%d"`。
|
||||
|
||||
## ② 关卡节点地图.md:两处定位信息表述统一
|
||||
|
||||
- 文件与位置:`design/gdd/systems/冒险循环/关卡节点地图.md` :146
|
||||
- 改成:本句「本文递出关卡序号 + 节点编号,按编号生成单位归它」统一为「本文递出关卡序号+节点深度;敌人侧按深度带配池生成单位」。
|
||||
- 依据:同文件 :119 既有定稿句「关卡序号 + 节点深度……按深度带配池」,两处表述当前不一致;本批实现侧 `level_runner` 递出 `node_depth`、敌人侧 `band_of` 落带,以此为准。
|
||||
|
||||
## ③ 关卡节点图生成.md:产出规格补「多终点深度互异」条款
|
||||
|
||||
- 文件与位置:`design/gdd/systems/冒险循环/关卡节点图生成.md` :41–:48(产出约束五条)
|
||||
- 改成:在产出约束中补一条——多终点(多 Boss 节点)必须落在互异的深度带;同一深度带出现两个终点即判产出无效(理由:共带会让「走哪条路打谁」落空)。
|
||||
- 依据:本卡契约 6 的图侧兑现处;实现侧 `config_db_cross_validator.gd` 已新增「同日图多 Boss 同带」报错(类别 `rowset_rule`,断言〔fv15〕),顶带消解另报 `level_node_dangling`〔fv16〕。
|
||||
|
||||
## ④ 待办清单.md:条目状态回写与退役记载
|
||||
|
||||
- 文件与位置:`待办清单.md` 中本卡登记条目
|
||||
- 改成:状态推进为「已实现(迭代 34)」并写明实现报告路径;按中间文档退役规则登记本卡与迭代产出物的退役去向。
|
||||
- 依据:五段流水线状态机(`doc-conventions` §12);实现批不写该文件,回写归批后收尾或用户发令。
|
||||
|
||||
## ⑤ roadmap.md:迭代 34 行批内状态刷新
|
||||
|
||||
- 文件与位置:`roadmap.md` 迭代 34 行(两卡并列,本卡为卡③)与顶部「当前位」节
|
||||
- 改成:卡③状态记为已实现;卡④(信息迷雾可见性)仍在同一迭代内推进;「当前位」按实际进度改写。
|
||||
- 依据:迭代实现批的收尾义务(`iter` 技能第 5 步账面随批);实现批不写该文件,换代归批后收尾或用户发令。
|
||||
@@ -0,0 +1,157 @@
|
||||
# 迭代 34 实现报告(2026-10-22)
|
||||
|
||||
> 依据:`roadmap.md` 迭代 34 行与两份需求卡片。本批为代码实现批,产出物只写 `reports/`。
|
||||
> 卡③「敌人表深度带换代批」已完成;卡④「信息迷雾可见性批」另行施工(见第五节)。
|
||||
|
||||
## 一、批次概况
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 工作树 | `D:\code\g-plan2-iter34-enemy`,分支 `iter34-enemy-band` |
|
||||
| 开工基线(主区 master,603eba31) | 冒烟 1156 项检查 0 失败;单元 35 套件全绿;探针 `--click` 全绿 |
|
||||
| 卡③收口读数 | 冒烟 1161 项检查 0 失败;单元 35 套件全绿;探针 `--click` 与 `--full` 全绿;静态扫描归零 |
|
||||
| 合并前全量代码闸 | 见第四节 |
|
||||
|
||||
## 二、卡③ 敌人表深度带换代批
|
||||
|
||||
### 2.1 任务卡状态
|
||||
|
||||
| T 卡 | 内容 | 状态 |
|
||||
|---|---|---|
|
||||
| T0 | 事实坐实与迁移表核对(§4.2 逐行) | 完成(迁移后 20 行键序与表序逐行一致〔fv11〕) |
|
||||
| T1 | 带切法单点、resolver 三参签名、表键迁移、schema 校验换代 | 完成(提交 07f3eb55) |
|
||||
| T2 | 跨表校验 ④ 敌侧带化、Boss 同带与顶带消解两条负向判据、消费面(叙述/结算)随动 | 完成(提交 07f3eb55) |
|
||||
| T3 | 冒烟与单测断言面按 §8.1 S 清单换代、七项变异验证 | 完成(提交 61537da8、2 笔修补) |
|
||||
| T4 | 三份产出物、独立验收审计、合并 | 进行中(本报告+审计+合并) |
|
||||
|
||||
### 2.2 实际改动—验收对照表
|
||||
|
||||
生产侧关键落点:`game/data/schemas/enemy_table.gd`(`band_of` :157、`ROW_KINDS` :12、`row_kind` 枚举判据 :57–:58、锚行与 Boss 行集 :149);`game/core/config_db_resolver.gd`(三参签名 :33、带寻址与两档告警 :44–:80);`game/core/config_db.gd`(门面 :193);`game/core/config_db_cross_validator.gd`(④ 敌侧带化 :94–:106);`game/core/unit_container.gd`(行类参数 :38、解析 :43);`game/core/battle_orchestrator.gd`(行类取值 :328、透传 :359);`game/presentation/battle_narrator.gd:76`;`game/data/tables/enemy.tres`(20 行键迁移)。
|
||||
|
||||
| fv | 需求点(简) | 验收断言(文件:行) | 层级 | 真实入口 | 撤销/变异结果 |
|
||||
|---|---|---|---|---|---|
|
||||
| fv1 | 带命中偏难一侧 | `config_db_resolver_test.gd:58`、:61 | 单元 | `resolve_enemy_rowset` 直调 | 变异 `band_of` 改 floor → 红(实测两断言均红) |
|
||||
| fv2 | 行集按表文件行序返回 | `config_db_resolver_test.gd:67`、:71 | 单元 | 同上 | 变异混编两行对调 → 红 |
|
||||
| fv3 | 越顶带取顶带+告警去重 | `config_db_resolver_test.gd:78`、:84、:87 | 单元 | 同上(D 高于顶带、重复请求两次) | 变异 floor → 红(实测) |
|
||||
| fv4 | row_kind 路由 | `config_db_resolver_test.gd:93` | 单元 | 同 D 分别传 battle/boss | 撤销第三参 → 双 kind 不再分域 |
|
||||
| fv5 | 越域与非法行类致命 | `config_db_resolver_test.gd:101`、:104、:107 | 单元 | `resolve_enemy_rowset(0,…)`/`(L,-1,…)`/`(1,0,"elite")` | 撤销判据 → 不报致命 |
|
||||
| fv6 | 生产表加载校验零错误 | `config_db_test.gd:14` | 接口 | `ConfigDB.load_tables` | 回退键迁移 → 校验红 |
|
||||
| fv7 | 非法 row_kind 被拒 | `config_db_test.gd:256` | 接口 | `EnemyTable.validate` 非法枚举夹具 | 变异注释枚举判据 → 红(实测) |
|
||||
| fv8 | dup_key 新键拦截 | `config_db_test.gd:235` | 接口 | 同带键同敌种二行夹具 | 回退键串 → 漏拦 |
|
||||
| fv9/fv20 | 同带隔断报 rowset_rule | `config_db_test.gd:225` | 接口 | 隔断夹具 | 回退 rowset_rule 键串 → 漏拦 |
|
||||
| fv10 | 锚行与 Boss 行集缺失报 anchor | `config_db_test.gd:265`、:273 | 接口 | 缺锚/缺 Boss 夹具 | 回退 anchor 判据 → 漏报 |
|
||||
| fv11 | 20 行计数与键序 | `smoke_all.gd:2106` | 接口 | 加载全表逐行比对 | 变异对调/迁错带 → 红(实测) |
|
||||
| fv12/fv25 | hobgoblin 在 (1,battle,3) 留任 | `smoke_all.gd:2114`、:2116 | 接口 | 同上读表面+表注释件核 | 变异迁错带 → 红(实测) |
|
||||
| fv13 | 三 Boss 行类与带键就位 | `smoke_all.gd:2132` | 接口 | 同上读表面 | 回退任一行类 → 红 |
|
||||
| fv14 | 值列零变更 | git diff 件核 | 件核 | `git diff` 逐值列比对 | 改任一值列 → 件核红 |
|
||||
| fv15 | 同关双终点共带报错 | `config_db_cross_validator_test.gd:137` | 单元 | `cross_validate()` 双 Boss 同带夹具 | 变异删判据 → 红(实测) |
|
||||
| fv16 | 顶带消解报错 | `config_db_cross_validator_test.gd:142` | 单元 | 节点深度高于顶带夹具 | 变异判据短路 `if false` → 红(实测) |
|
||||
| fv17 | 单终点图零误报 | `config_db_cross_validator_test.gd:132` | 单元 | level 1 合成单关夹具(`display_name:"夹具关"`);现役三关零误报由 fv6 兜住 | 回退 ④ 敌侧换代 → 红 |
|
||||
| fv18 | 混编行序 | `smoke_all.gd:4815` | 集成 | 冒烟读 (2,battle,5) 行集 | 变异对调两行 → 红(实测) |
|
||||
| fv19 | 混编骰和 ≤ 4 | `smoke_all.gd:4820` | 集成 | 同行集静态求和 | 改运行期守卫 `ROWSET_INSTANCE_MAX` → 同段守卫断言(「两行各定值 3(总 6>4)spawn 被拒」)红;静态求和本身放宽不会红 |
|
||||
| fv21 | 首遇带数据驱动定位 | `smoke_all.gd:2282` | 集成 | 扫表取最低带 | 撤销扫表定位 → 取键失配 |
|
||||
| fv22 | 巨蜘蛛首遇带固定 2 不加骰 | `smoke_all.gd:4801` | 集成 | 扫表定位+行集读数 | 变异加 `unit_count_max: 3` → 红(实测) |
|
||||
| fv23 | bugbear/duergar 首遇带固定 1 | `smoke_all.gd:3473`、:4859 | 集成 | 扫表定位+行集读数 | 变异加 `unit_count_max` → 红 |
|
||||
| fv24 | V7 S 清单 17 行逐条换代(尾缀=清单件核;随带定点断言 = Boss 行集与毒吐投放面 + S15 叙述寻址) | `smoke_all.gd:4281`、:4291、:4807、:4865;`battle_narrator_test.gd:81`、:82 | 集成+单元 | 扫表定位+Boss 行集;`boss_name(runner, db)` 经 `resolve_enemy_rowset(..., "boss")` | 变异 `boss_name` 行类 "boss"→"battle" → `battle_narrator_test` fv24 红(实测);逐条勾销见 §2.2.1 |
|
||||
| fv25 | hobgoblin 缺口登记随行 | `smoke_all.gd:2116` | 接口 | 表文件注释在位 | 同 fv12 |
|
||||
| fv26 | V9 反哺点五落点(非行为改动) | `reports/iter34-反哺点-2026-10-22.md` ①–⑤ | 件核 | 反哺点文件与卡片 §6 同步面表逐条比对 | 删任一落点 → 件核红(文档产出物,无代码断言) |
|
||||
|
||||
覆盖说明:`game/` 全域共 37 行带 fv 尾缀(36 条断言,另 `smoke_all.gd:969` 一行是注释),覆盖 fv1–fv26;fv14(值列零变更)与 fv26(反哺点落点)按卡片约定走件核,代码侧无断言;fv9 与 fv20、fv12 与 fv25 各自共用同一断言行,计数时不重复。
|
||||
|
||||
#### 2.2.1 V7:S 清单 17 行逐条勾销(卡片位置 → 现工作树位置)
|
||||
|
||||
| S | 现工作树位置 | 换代内容 | 勾销凭据 |
|
||||
|---|---|---|---|
|
||||
| S1 | `smoke_all.gd:3168`/:3491/:4499 | 三参化(row_kind="battle")+带语义注释 | 三处均显式传 "battle",行集首行读数随带键 |
|
||||
| S2 | `smoke_all.gd:4795`(断言 :4801〔fv22〕) | 首遇带数据驱动定位+恰一行/unit_count=2/无 unit_count_max | 〔fv22〕实测绿;变异加 unit_count_max → 红(§2.3 第 5 项) |
|
||||
| S3 | `smoke_all.gd:4802`(断言 :4807〔fv24〕) | L2 Boss 行集改 `_boss_depth(2)`+"boss" | 〔fv24〕实测绿 |
|
||||
| S4 | `smoke_all.gd:4810`(断言 :4815〔fv18〕、:4820〔fv19〕) | 键改 (2,battle,5),行序契约保持 | 〔fv18〕变异对调 → 红(§2.3 第 6 项) |
|
||||
| S5 | `smoke_all.gd:4852`/:4860(断言 :4859〔fv23〕、:4865〔fv24〕) | 首遇带定位+Boss 三参化 | 两条断言实测绿 |
|
||||
| S6 | `smoke_all.gd:974-984`/:4781-4789/:5256-5258 | en_keys 改 (L,row_kind,depth_band),Boss 节点查 boss 键域 | 三处键串 `"%d_%s_%d"`;L2/L3 跨表行完整断言绿 |
|
||||
| S7 | `smoke_all.gd:2263`/:2279/:2301 | Boss AC 台阶保留;(1,2) 锚定改 bugbear 首遇带定位〔fv21〕;曲线改按 (1,battle) 带键相邻比 ≤ 2.5 | 三条断言实测绿;fv21 变异见 §2.3 第 8 项 |
|
||||
| S8 | `smoke_all.gd:4262-4306` | 毒吐投放面改 `depth_band > 巨蜘蛛首遇带`+池键含 row_kind | 〔fv24〕:4281/:4291 实测绿;键串 `2_battle_5_giant_spider` 等 |
|
||||
| S9 | `smoke_all.gd:3460-3474`(断言 :3473〔fv23〕) | (1,2) 改 bugbear 首遇带定位(固定 1 不加骰) | 〔fv23〕实测绿;数据侧变异下 fv23 红(§2.3 第 8 项) |
|
||||
| S10a | `smoke_all.gd:2024`/:2674/:2699/:3078/:3143/:3230/:3284/:3324/:3354/:3566/:3633/:3651/:3668/:4139/:4359/:4606/:4970/:5040;`damage_probe.gd:95` | 各补 `"node_type": "boss"` | 上述直呼点均带 node_type;冒烟 1161 项 0 失败 |
|
||||
| S10b | `smoke_all.gd:2674`/:2699 | 改 (2,9)+boss | 两处均为 `"node_depth": 9, "node_type": "boss"` |
|
||||
| S11 | `tests/unit/core/config_db_resolver_test.gd`(fv1–fv5) | 换代为带寻址语义断言 | fv1–fv5 实测绿;floor 变异红(§2.3 第 1 项) |
|
||||
| S12 | `tests/unit/data_config/config_db_test.gd`(fv6–fv10) | 夹具键改 row_kind/depth_band,签名三参化 | fv6–fv10 实测绿;枚举判据变异红(§2.3 第 2 项) |
|
||||
| S13 | `tests/unit/core/config_db_cross_validator_test.gd`(fv15–fv17) | 夹具键改名+补两条负向用例 | fv15/fv16 变异红(§2.3 第 3、4 项) |
|
||||
| S14 | `smoke/skill_rhythm_probe.gd:17`;`smoke/damage_probe.gd:269`/:422 | 三参化 | 三处显式传 row_kind |
|
||||
| S15 | `game/presentation/battle_narrator.gd:76`;验收 `tests/unit/presentation/battle_narrator_test.gd:81`/:82〔fv24〕 | 三参化(row_kind="boss")+补真实入口单测 | 实测绿;变异 "boss"→"battle" → fv24 红(§2.3 第 9 项) |
|
||||
| S16 | `smoke_all.gd:3491`(断言 :3496) | erow2 首行读数随带键(三参化) | 全深度×12 种子 60 场断言实测绿 |
|
||||
|
||||
勾销口径:S11–S13 归单测面,S1–S10、S14–S16 归冒烟/探针面,S15 归消费面;17 行全部给出工作树位置与凭据,无一行停在旧坐标语义。
|
||||
|
||||
### 2.3 变异验证记录表(注入 → 最窄闸见红 → 还原)
|
||||
|
||||
| # | 注入内容 | 最窄闸 | 红断言(原文摘要) | 还原确认 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | `EnemyTable.band_of` 的 `>=` 改为 `<=`(反向 floor) | `config_db_resolver_test.gd` | 带命中取偏难一侧:请求深度 4 命中带 5〔fv1〕;精确落带〔fv1〕;越顶带 clamp〔fv3〕×2 | 改回 `>=`,复跑全绿 |
|
||||
| 2 | 注释掉 `validate` 的 `row_kind ∈ ROW_KINDS` 判据 | `config_db_test.gd` | 非法 row_kind 被拒(封闭枚举)〔fv7〕 | 恢复判据,复跑全绿 |
|
||||
| 3 | 删除跨表校验「Boss 同带」判据 | `config_db_cross_validator_test.gd` | 同关双终点共带报 rowset_rule〔fv15〕 | 恢复判据,复跑全绿 |
|
||||
| 4 | 顶带消解判据短路为 `if false` | `config_db_cross_validator_test.gd` | Boss 节点越顶带报 level_node_dangling〔fv16〕 | 恢复判据,复跑全绿 |
|
||||
| 5 | 巨蜘蛛首遇行(2,battle,4)加 `unit_count_max: 3` | 冒烟全量 | 巨蜘蛛首遇带行集 = {giant_spider} 恰一行且 unit_count=2 无 unit_count_max〔fv22〕 | 删该字段,冒烟复跑全绿 |
|
||||
| 6 | 混编两行(2,battle,5)文件内对调 | 冒烟全量 | 混编行序 [bugbear→giant_spider]〔fv18〕;20 行键序〔fv11〕;行值复制面 | 还原行序,冒烟复跑全绿 |
|
||||
| 7 | hobgoblin 迁错带键(迁到 (1,battle,2)) | 冒烟全量 | 首次注入在加载期即被跨表校验拦下:`ERROR: [ConfigDB/level_node_dangling] level 1 battle 节点深度 3 高于 (level 1, battle) 顶带键 2——顶 clamp 消解`,随后〔fv6〕红(config_db_test 2/2 项止步);「同带相邻」判据未触发——该行紧接 bugbear 带 2 行之后、文件内相邻。二次注入改为与 band 2 行互换带键(表可加载),命中「hobgoblin 行在 (1,battle,3)」〔fv12〕〔fv25〕与 20 行键序〔fv11〕 | 还原带键,冒烟复跑全绿 |
|
||||
| 8 | 数据侧:L1 bugbear 行 `depth_band` 2→1(V6① 撤销证明换代) | 冒烟全量 | 单跑数据变异时〔fv21〕仍绿(首遇带随之变 1、锚定行随迁——数据驱动的正证),连带〔fv11〕〔fv23〕与 spawn 扫描等共 6 项红;叠加「`_first_encounter_band` 写死返回 2」后〔fv21〕红(期望带 2 找不到 bugbear 行),共 9 项红 | 两处均 `git checkout --` 还原,冒烟 1161 项 0 失败 |
|
||||
| 9 | `battle_narrator.boss_name` 行类 "boss"→"battle"(S15 撤销证明) | `battle_narrator_test.gd` | 「Boss 名取自 Boss 行集首行(期望「食人魔酋长」实际「大地精」)〔fv24〕」红,退出码 1 | 改回 "boss",13 项检查全绿 |
|
||||
|
||||
拦截边界如实记录:第 7 项按卡片写法直接把 hobgoblin 放到带 2 时,首条红是跨表 `level_node_dangling`(战斗节点深度 3 落到不存在的顶带),不是同带相邻规则——该行紧接 bugbear 带 2 行之后、文件内相邻,隔断判据不触发;键位断言的敏感度改用「与带 2 行互换带键」的表内合法变异单独观测。第 8 项单跑数据变异时〔fv21〕仍绿,说明该断言跟的是数据而不是写死值;只有「数据已变、坐标仍写死」的组合才让〔fv21〕红,这是它能区分两种实现的边界。另:第 1 项连带 fv3 变红,属同一算法单点的预期耦合,不另计。
|
||||
|
||||
### 2.4 卡片与实际改动面的偏差(逐条说明)
|
||||
|
||||
1. **S10a 补位**:卡片 S10a 列了 11 个补 `node_type: "boss"` 的位置,实际还需在 `smoke_all.gd` 的 CLEAR_SMOKE_DEPTH 段与 `(2,9)` 段若干直呼点补同一字段,并把 `spawn(1,4)`、深度循环 `spawn` 补行类参数。理由:这些点同样按 Boss 语义取行集,漏补会让它们落回 battle 键域、读到错的敌人行。语义未变,只是行类改为显式传。
|
||||
2. **gp 掉落断言改数据驱动**:原断言把「破财压制」战斗的 `gp_dropped` 与普通档掷定区间硬编码比对;该战斗在 S10a 中被标为 Boss 语义后,掉落档合法切换到 Boss 档,硬编码区间不再成立。改为从 `shop.tres` 读 Boss 档区间与关级加值再比对(`shop_gp_drop_row`/`shop_level_gp_bonus`),断言强度不变且不再写死数值。
|
||||
3. **单测补行类参数**:`skill_unlock_test.gd` 的 Boss 用例原先直呼 `spawn(2,9)` 取默认 battle 行类,会落错带;改为显式传 `"boss"`,普通行零扰动用例保持 `"battle"`。
|
||||
4. **静态扫描两处归零**:新 resolver 的行集容器改为显式类型(原为无类型 `Array`);表注释里的退役词改为白话。两处都不改游戏行为。
|
||||
|
||||
### 2.5 实机目检总清单(只登记,不阻塞合并)
|
||||
|
||||
| # | 项目 | 观测点 | 状态 |
|
||||
|---|---|---|---|
|
||||
| 1 | 三关实机开战遭遇观感不回归 | 第一关地精/熊地精、第二关巨蜘蛛与重击手混编、三关 Boss 战的敌人数量与外观与改动前一致 | 待用户试玩 |
|
||||
|
||||
### 2.6 遗留读数义务
|
||||
|
||||
- 数值零变更,本卡不发起模拟跑批(MC 档位规则)。
|
||||
- 合并后主区需 `--editor --quit` 重刷类名缓存,再跑一次全量代码闸复跑,读数写入合并提交信息。
|
||||
|
||||
## 三、卡③ 独立验收审计
|
||||
|
||||
审计报告:`reports/iter34-验收审计-2026-10-22.md`(由未参与实现的子代理执行;只读卡片、本报告、diff、测试与闸回执,自跑最窄命令复现,不采信「已全绿」)。
|
||||
|
||||
**第一轮结论:不通过**——缺漏 4 条+描述不符 4 条(逐行依据见该报告第一至四节)。处置如下:
|
||||
|
||||
| 缺漏 | 处置 | 落点 |
|
||||
|---|---|---|
|
||||
| V7 无覆盖行、fv24 尾缀挂错需求点 | 补 V7 覆盖行+S 清单 17 行逐条勾销表 | 本报告 §2.2 fv24 行、§2.2.1 |
|
||||
| V9/fv26 覆盖行缺失 | 补件核行(反哺点文件 ①–⑤) | 本报告 §2.2 fv26 行 |
|
||||
| S15 `boss_name` 换代无断言 | 补真实入口单测(扫表取 Boss 带键 → 断言叙述名=boss 行集首行名),并做撤销证明 | `game/tests/unit/presentation/battle_narrator_test.gd:56-82`〔fv24〕;§2.3 第 9 项 |
|
||||
| V6①/fv21 撤销证明按记录无法变红 | 改为数据侧变异:bugbear 带 2→1(fv21 仍绿=数据驱动正证)+叠加写死坐标(fv21 红) | §2.3 第 8 项 |
|
||||
|
||||
描述不符 4 条已按审计建议改写:fv17 夹具措辞(level 1 合成单关,非现役三关,现役三关由 fv6 兜住)、fv19 撤销归属(运行期守卫断言,静态求和本身不红)、§2.3 第 7 项拦截首红(跨表 `level_node_dangling`)、断言计数(36 条断言+1 行注释)。
|
||||
|
||||
**第二轮复审结论:通过**(`reports/iter34-验收审计-复审-2026-10-22.md`,被审 HEAD `68009b4e`)——A–F 六项全通过:V7 覆盖行与 17 行勾销表齐备(抽 6 行实读属实)、fv26 件核行在位、S15 单测走真实入口且变异红、V6① 数据侧撤销证明复现一致、四条描述不符已按实改写、修补未引入未申报的测试削弱。卡③ 合并前置满足。
|
||||
|
||||
## 四、全量代码闸读数
|
||||
|
||||
`tools/run_all_gates.sh --code`,起跑 commit `8b18e057`(2026-10-22 04:42),回执 `game/tests/.gates/receipt.json`:
|
||||
|
||||
| 闸 | 退出码 | 指纹 | 对基线 |
|
||||
|---|---|---|---|
|
||||
| 01 冒烟全量 | 0 | green | 一致 |
|
||||
| 02 单元套件 | 0 | green | 一致(35 套件) |
|
||||
| 03 探针 --click | 0 | green | 一致 |
|
||||
| 04 探针 --full | 0 | green | 一致 |
|
||||
| 05 渲染闸 | 0 | green | 一致(shot_probe 七屏+golden 逐像素) |
|
||||
| 09 静态扫描 | 0 | green | 改善(基线红 → 现绿,本卡改动面归零) |
|
||||
| 10 熵增闸 | 0 | green | 一致 |
|
||||
| 11 回放确定性对拍 | NEED-APPROVAL | skip | 本卡数值零变更,按档位规则不发起(需用户批准) |
|
||||
|
||||
`all_green: true`,`changed_vs_baseline: []`。改前基线(T0,主区 master `603eba31`):冒烟 1156 项检查 0 失败/单元 35 套件 0 失败/探针全绿。
|
||||
|
||||
> 环境注记:本轮曾出现一次阶段性假红(02/03/04/05 同时红)——前一次被中断的闸进程未死透,与新闸并行写了同一批 `game/tests/.gates/*.log`(日志内出现同一探针同时 PASS 与 FAIL 的交错字节),另有并行会话在同机跑另一 worktree 的闸。清场后端到端重跑得本次读数;单件直跑(`ui_struct_probe` 等)在清场前即为绿,佐证为环境并发而非行为回归。
|
||||
|
||||
## 五、卡④ 信息迷雾可见性批
|
||||
|
||||
另行施工,完成后在本文追加对应章节。
|
||||
@@ -0,0 +1,100 @@
|
||||
# 迭代 34 卡③ 独立验收审计(2026-10-22)
|
||||
|
||||
被审对象:实现报告 `reports/iter34-实现报告-2026-10-22.md`、需求卡片 `reports/需求卡片-敌人表深度带换代批-2026-10-21.md`、工作树 `D:\code\g-plan2-iter34-enemy`(分支 `iter34-enemy-band`,HEAD `7bb41891`,基线 `603eba31`)。
|
||||
|
||||
审计方式:只读代码、测试、卡片与 diff。所有结论以审计人自己运行的输出为准,不采信报告「已全绿」的表述。审计期间为复现撤销证明注入的变异,均用单文件 `git checkout -- <file>` 还原并复跑确认,工作树结束时无改动。
|
||||
|
||||
## 审计结论
|
||||
|
||||
不通过:缺漏 4 条(V7 无覆盖行、fv24 尾缀挂错需求点;V9/fv26 覆盖行缺失;S15 `boss_name` 换代无断言;V6①/fv21 撤销证明按记录无法变红),另有描述不符 4 条;其余对照行通过。
|
||||
|
||||
## 一、对照表逐行核对
|
||||
|
||||
核对方法:按报告 §2.2 的 file:line 逐条读断言文本,grep 尾缀〔fv#〕确认可定位,再沿断言回溯到改动点入口(`resolve_enemy_rowset`/`EnemyTable.validate`/`cross_validate`/生产表读数),并在工作树实跑对应套件确认该断言在跑。
|
||||
|
||||
| fv | 需求点 | 测试存在(file:line) | 尾缀可 grep | 真实入口 | 结论 |
|
||||
|---|---|---|---|---|---|
|
||||
| fv1 | V1① 带命中偏难 | `config_db_resolver_test.gd:58`、`:61` | 是 | `resolve_enemy_rowset(1,4/5,"battle")` → `band_of` | 通过 |
|
||||
| fv2 | V1② 行序保序 | `config_db_resolver_test.gd:67`、`:71` | 是 | 同上,断言行集顺序 | 通过 |
|
||||
| fv3 | V1③ 顶带 clamp+告警去重 | `config_db_resolver_test.gd:78`、`:84`、`:87` | 是 | 同上,D=99 与重复请求 | 通过 |
|
||||
| fv4 | V1④ row_kind 路由 | `config_db_resolver_test.gd:93` | 是 | 同 D 传 battle/boss | 通过 |
|
||||
| fv5 | V1⑤ 越域与非法行类 | `config_db_resolver_test.gd:101`、`:104`、`:107` | 是 | `resolve_enemy_rowset(0,…)`/`(1,-1,…)`/`(1,0,"elite")` | 通过 |
|
||||
| fv6 | V2① 生产表校验零错误 | `config_db_test.gd:14` | 是 | `ConfigDB.load_tables` → `EnemyTable.validate` | 通过 |
|
||||
| fv7 | V2② 非法 row_kind 被拒 | `config_db_test.gd:256` | 是 | `validate()` 非法枚举夹具 | 通过 |
|
||||
| fv8 | V2③ dup_key 新键拦截 | `config_db_test.gd:235` | 是 | `validate()` 同带同敌种二行夹具 | 通过 |
|
||||
| fv9/fv20 | V2④、V5③ 同带隔断 | `config_db_test.gd:225`(一条断言两个尾缀) | 是 | `validate()` 隔断夹具 | 通过 |
|
||||
| fv10 | V2⑤ 锚行与 Boss 行集 | `config_db_test.gd:265`、`:273` | 是 | `validate()` 缺锚/缺 Boss 夹具 | 通过 |
|
||||
| fv11 | V3① 20 行计数与键序 | `smoke_all.gd:2106` | 是 | 加载 `enemy.tres` 全表逐行比对 | 通过 |
|
||||
| fv12/fv25 | V3②、V8 hobgoblin 留任 | `smoke_all.gd:2114`、`:2116` | 是 | 同 fv11 读表面+表注释 | 通过 |
|
||||
| fv13 | V3③ 三 Boss 行就位 | `smoke_all.gd:2132` | 是 | 同 fv11 读表面 | 通过 |
|
||||
| fv14 | V3④ 值列零变更 | 无断言(件核):`git diff 603eba31..7bb41891 -- game/data/tables/enemy.tres` | 无(卡片约定件核) | diff 去键列后逐值列比对 | 通过(去键列后值列逐字节一致) |
|
||||
| fv15 | V4① 同带双 boss 报错 | `config_db_cross_validator_test.gd:137` | 是 | `cross_validate()` 双 Boss 共带夹具 | 通过 |
|
||||
| fv16 | V4② 顶 clamp 消解报错 | `config_db_cross_validator_test.gd:142` | 是 | `cross_validate()` Boss 深度 10>顶带 9 | 通过 |
|
||||
| fv17 | V4③ 单终点零误报 | `config_db_cross_validator_test.gd:132` | 是 | `cross_validate()` 合成单关夹具 | 例外:夹具只有 level 1(`display_name:"夹具关"`),报告写「现役三关夹具」不实;现役三关零误报由 fv6 兜住 |
|
||||
| fv18 | V5① 混编行序 | `smoke_all.gd:4815` | 是 | 读 (2,battle,5) 行集顺序 | 通过 |
|
||||
| fv19 | V5② 骰和 ≤4 | `smoke_all.gd:4820` | 是 | 读同行集静态求和 | 例外:断言是静态求和,放宽它本身不会红;能变红的是同段未编号的运行期守卫断言 |
|
||||
| fv21 | V6① 首遇带数据驱动 | `smoke_all.gd:2282`(定位助手 `:3516`) | 是 | `_first_encounter_band` 扫表+行集读数 | 通过(撤销证明不成立,见第四节第 4 条) |
|
||||
| fv22 | V6② 巨蜘蛛首遇带固定 2 | `smoke_all.gd:4801` | 是 | 扫表定位+行集读数 | 通过 |
|
||||
| fv23 | V6③ L1/L3 首遇带固定 1 | `smoke_all.gd:3473`、`:4859` | 是 | 同上 | 通过 |
|
||||
| fv24 | 报告写作「Boss 行集与毒吐投放面」 | `smoke_all.gd:4281`、`:4291`、`:4807`、`:4865` | 是 | 扫表定位+Boss 行集 | 缺漏:卡片 V7 要求「S 清单 17 行逐条换代+清单件核」,这 4 条断言不是该需求点的验证;报告无 V7 行,也无 S1–S16 逐条勾销表 |
|
||||
| fv25 | V8 缺口登记随行 | `smoke_all.gd:2116`(与 fv12 共用行) | 是 | 表文件注释在位 | 通过 |
|
||||
| —(V9/fv26) | V9 反哺点五落点件核 | 报告 §2.2 无此行 | — | 应为 `reports/iter34-反哺点-2026-10-22.md` 五条落点件核 | 缺漏:反哺点文件已有 ①–⑤ 五节,但对照表没有 fv26 覆盖行 |
|
||||
|
||||
需求点覆盖对照:V1①–⑤=fv1–fv5,V2①–⑤=fv6–fv10,V3①–④=fv11–fv14,V4①–③=fv15–fv17,V5①–③=fv18–fv20,V6①–③=fv21–fv23,V8=fv25;V7 与 V9 没有对应覆盖行(见上表两处缺漏)。
|
||||
|
||||
## 二、撤销证明抽查
|
||||
|
||||
抽查 6 项(报告 §2.3 七条中取 5 条复现,另核 1 条不可复现)。每条流程:改一个文件注入 → 跑报告指明的最窄命令 → 确认对应断言变红 → `git checkout -- <单文件>` 还原 → 复跑回绿。
|
||||
|
||||
| 变异 | 复现结果 | 红输出首行 | 还原结论 |
|
||||
|---|---|---|---|
|
||||
| 1. `EnemyTable.band_of` 判据 `>=` 改 `<=`(反向 floor) | 已复现 | `[FAIL] 带命中取偏难一侧:请求深度 4 命中带 5(逐字不回带 0)〔fv1〕`;连带〔fv1〕第二条与〔fv3〕两条 | `git checkout -- game/data/schemas/enemy_table.gd`;resolver 套件 14 项全绿 |
|
||||
| 2. `validate` 的 row_kind 枚举判据前置 `if false and` | 已复现 | `[FAIL] 非法 row_kind 被拒(封闭枚举 {battle, boss})〔fv7〕`(1/29 项失败) | 同文件 checkout;config_db_test 29 项全绿 |
|
||||
| 3. 删除跨表「Boss 同带」判据 | 已复现 | `[FAIL] 同关双终点共带报 rowset_rule〔fv15〕` | `git checkout -- game/core/config_db_cross_validator.gd`;跨表套件 7 项全绿 |
|
||||
| 4. 顶带消解判据短路 `if false` | 已复现 | `[FAIL] Boss 节点越顶带报 level_node_dangling〔fv16〕` | 同文件 checkout;7 项全绿 |
|
||||
| 5. hobgoblin 与带 2 行互换带键(报告第 7 项第二注入法) | 已复现 | 冒烟首红〔fv11〕「敌人表 20 行且键序=迁移表序…」,随后〔fv12〕〔fv25〕红 | `git checkout -- game/data/tables/enemy.tres`;冒烟 1161 项检查 0 失败 |
|
||||
| 6. hobgoblin 直接改到带 2(报告第 7 项第一注入法) | 已复现,但拦截者与报告所写不同 | `ERROR: [ConfigDB/level_node_dangling] level 1 battle 节点深度 3 高于 (level 1, battle) 顶带键 2——顶 clamp 消解`;随后〔fv6〕红,套件 2/2 项失败即止 | 同文件 checkout;config_db_test 29 项全绿 |
|
||||
| 7. V6①「撤销扫表定位、写回写死坐标」 | 未复现(按记录设计不成立) | — | 未注入:助手 `_first_encounter_band`(`smoke_all.gd:3516`)返回值与旧写死值同为 L1=2/L2=4/L3=0,写回后断言值不变、不会红;要让 fv21 变红只能改数据把首遇行移带 |
|
||||
|
||||
实际运行命令(均在工作树,`PYTHONIOENCODING=utf-8`,Godot 4.7.2 console 版 headless,一次只跑一个进程):
|
||||
|
||||
- `--script res://tests/unit/core/config_db_resolver_test.gd`(多轮,含注入与还原后复跑)
|
||||
- `--script res://tests/unit/data_config/config_db_test.gd`(多轮)
|
||||
- `--script res://tests/unit/core/config_db_cross_validator_test.gd`(多轮)
|
||||
- `--script res://tests/run_all.gd`(全量单元 35 套件)
|
||||
- `--script res://smoke/smoke_all.gd`(1161 项检查 0 失败,多轮)
|
||||
- `git diff`、`git status`、`git checkout -- <单文件>`
|
||||
|
||||
补充核验:把 `battle_narrator.gd:76` 的 `"boss"` 改为 `"battle"` 后,35 个单元套件与整轮冒烟仍全绿,说明该改动点没有被任何断言拦住(见第四节缺漏 3)。
|
||||
|
||||
## 三、测试削弱检查
|
||||
|
||||
检查范围:`git diff 603eba31..7bb41891 -- game/tests game/smoke`(删 173 行、增 418 行)。逐项核对删除、阈值、扫描面与入口层级:
|
||||
|
||||
1. `config_db_resolver_test.gd` 删除 `_test_ceil_progress_key`(board_gen ceil 3 条断言):属卡片 §8.2 旧断言预声明 S11「换代为 V1 带寻址语义断言」;board_gen 的 ceil/越顶/去重仍由 `config_db_test.gd:30-82` 覆盖,未失效。
|
||||
2. gp 掉落断言由硬编码 `[2,4]` 改为读 `shop.tres` Boss 档区间+关级加值:报告 §2.4 第 2 条已申报;判据仍比对结算输出,强度不低于原式。
|
||||
3. `skill_unlock_test.gd` Boss 用例 `spawn(2,9)` 补显式 `"boss"`:报告 §2.4 第 3 条已申报。
|
||||
4. 冒烟多处补 `node_type:"boss"`/补行类参数:报告 §2.4 第 1 条已申报。
|
||||
5. 未发现「真实入口换低层直调」:fv1–fv5 仍走 `resolve_enemy_rowset`,fv6–fv10 走 `EnemyTable.validate`(经加载),fv15–fv17 走 `cross_validate`,冒烟断言走 `_db.enemy.rows` 与 `spawn`。
|
||||
6. 检查数量:冒烟 1156→1161,单元 35 套件不变;无净收缩。其余删除行均为按 S 清单把旧坐标断言改写为带键断言,语义保持。
|
||||
7. 未发现未申报的阈值放宽或扫描面缩小。
|
||||
|
||||
结论:未发现未申报的测试削弱。另有一处计数描述不符:报告称「35 条断言带 fv 尾缀」,实测带尾缀行 35 行,其中 `smoke_all.gd:969` 是注释,断言为 34 条。
|
||||
|
||||
## 四、缺漏与例外
|
||||
|
||||
缺漏:
|
||||
|
||||
1. **V7 无覆盖行、fv24 尾缀挂错需求点**。位置:报告 §2.2 表(无 V7 行);卡片 §8.2 V7 要求「S 清单 17 行逐条换代+实现报告逐条勾销」。事实:fv24 的 4 条断言(`smoke_all.gd:4281`/`:4291`/`:4807`/`:4865`)验证的是 Boss 行集与毒吐投放面,不是 S 清单完整性;报告也没有 S1–S16 逐条勾销表。建议:补 V7 覆盖行与逐条勾销表,或经卡片所有者同意把 V7 的验收方式改写为实际断言。
|
||||
2. **V9/fv26 覆盖行缺失**。位置:报告 §2.2 表;卡片 §8.2 V9。事实:`reports/iter34-反哺点-2026-10-22.md` 已有 ①–⑤ 五节落点,实物在,但对照表没有 fv26 件核行,无法按表逐行核对。建议:补一行「V9|五条落点各一|件核:反哺点文件 ①–⑤|非行为改动|通过」。
|
||||
3. **S15 `boss_name` 换代无断言**。位置:`game/presentation/battle_narrator.gd:76`;卡片 §8.1 S15、§8.2 V7。事实:把该处 `"boss"` 改成 `"battle"` 后,35 个单元套件与整轮冒烟 1161 项全绿;`battle_narrator_test.gd` 从未调用 `boss_name`。建议:补一条走真实入口的单测(构造 Boss 节点,断言叙述名取 boss 行集名),或在报告 §2.4 如实声明该处无断言并说明理由。
|
||||
4. **V6①/fv21 撤销证明不成立**。位置:卡片 §8.2 V6① 撤销证明、报告 fv21 行。事实:按记录「撤销扫表定位、写回写死坐标」还原后,助手返回值与写死值相同(L1=2/L2=4/L3=0),断言不会红;该撤销证明无法区分「数据驱动」与「写死」。建议:改为数据侧变异(把首遇行移到别的带,断言应取键失配变红)或如实标注撤销边界。
|
||||
|
||||
例外(描述不符,不影响其余覆盖):
|
||||
|
||||
5. **fv17 夹具描述**。位置:`config_db_cross_validator_test.gd:128-132`;报告 §2.2 fv17 行写「现役三关夹具」。事实:夹具是 level 1 合成单关(`display_name:"夹具关"`);现役三关零误报由 fv6(生产表加载零 error)覆盖。建议:改报告措辞,或补现役三关夹具。
|
||||
6. **fv19 撤销归属**。位置:`smoke_all.gd:4820`。事实:静态求和断言被放宽不会红;真正能红的是同段未编号的运行期守卫断言(「两行各定值 3(总 6>4)spawn 被拒」)。建议:报告撤销证明改写为「改 `ROWSET_INSTANCE_MAX` 守卫 → 守卫断言红」。
|
||||
7. **§2.3 第 7 项拦截边界描述有误**。位置:报告 §2.3 注①。事实:hobgoblin 直接改到带 2 时,首条红是跨表 `level_node_dangling`(战斗节点深度 3 无带可落),不是「同带相邻规则」;同带相邻未触发,是因为 hobgoblin 紧接在 bugbear 带 2 行之后、文件内相邻。建议:按实际首红改写。
|
||||
8. **断言计数**。位置:报告 §2.2 覆盖说明「共 35 条断言带 fv 尾缀」。事实:带尾缀行 35 行,其中 1 行为注释(`smoke_all.gd:969`),断言 34 条。建议:改为 34 条。
|
||||
|
||||
审计结束时工作树 `git status --short` 为空,HEAD 仍为 `7bb41891`;除本报告外未新增或修改任何文件。
|
||||
@@ -0,0 +1,98 @@
|
||||
# 迭代 34 卡③ 独立验收审计·第二轮复审(2026-10-22)
|
||||
|
||||
- 被审 HEAD:`68009b4e`(`iter34-enemy-band`,基线 `603eba31`);第一轮被审 HEAD `7bb41891`
|
||||
- 被审对象:实现报告 `reports/iter34-实现报告-2026-10-22.md`(§2.2/§2.2.1/§2.3/§3)、第一轮审计报告 `reports/iter34-验收审计-2026-10-22.md`、修补 diff `7bb41891..68009b4e`、卡片 `reports/需求卡片-敌人表深度带换代批-2026-10-21.md`
|
||||
- 审计方式:只读代码、测试、卡片与 diff;所有结论以审计人自己运行的命令输出为准,不采信报告「实测/已全绿」表述。为复现撤销证明注入的变异一律单文件 `git checkout --` 还原并复跑确认;不跑全量闸(`tools/run_all_gates.sh` 留给实现会话在最终提交上跑)。审计结束时工作树 `git status --short` 为空(本报告除外)。
|
||||
|
||||
## 结论速览
|
||||
|
||||
**A–F 六项全部通过;总结论:通过。** 第一轮 4 条缺漏与 4 条描述不符全部真闭合,本次修补未引入测试削弱,无阻塞遗留例外。
|
||||
|
||||
---
|
||||
|
||||
## A. 缺漏 1(V7 覆盖行+17 行勾销表)——通过
|
||||
|
||||
依据:
|
||||
|
||||
1. 报告 §2.2 已新增 fv24 覆盖行,需求点改写为「V7 S 清单 17 行逐条换代」,撤销列指向「逐条勾销见 §2.2.1」。
|
||||
2. 报告 §2.2.1 存在 17 行勾销表:S1–S9、**S10a/S10b 分列**、S11–S16,逐行给出「现工作树位置/换代内容/勾销凭据」,S11–S13 归单测面、S15 归消费面,口径与卡片 §8.1 一致。
|
||||
3. 按任务要求抽查 6 行(逐行实读代码确认位置与换代内容属实):
|
||||
|
||||
| 抽查 | 报告所记位置 | 实读确认 |
|
||||
|---|---|---|
|
||||
| S1 | `smoke_all.gd:3168`/:3491/:4499 | 三处均为 `resolve_enemy_rowset(1, …, "battle")[0]` 三参化,且带「行集首行(…battle 键域)」注释,属实 |
|
||||
| S6 | `smoke_all.gd:974-984`/:4781-4789/:5256-5258 | 三处 `en_keys["%d_%s_%d" % [level_index, row_kind, depth_band]]`/`"%d_boss_9"` 查 boss 键域,属实 |
|
||||
| S8 | `smoke_all.gd:4262-4306` | `_first_encounter_band(2,"giant_spider")` 取首遇带、判据改 `depth_band > spider_lead_band`、`expect_pools` 键串含 row_kind(如 `2_battle_5_giant_spider`),属实 |
|
||||
| S10a | `smoke_all.gd:2024`/:2674/:2699…;`damage_probe.gd:95` | 抽 3 行均为 `"node_type": "boss"` 直呼;另用脚本核了表中全部 18 个 `smoke_all.gd` 行号,每个都含 `node_type": "boss"`,属实 |
|
||||
| S14 | `skill_rhythm_probe.gd:17`;`damage_probe.gd:269`/:422 | 三处均显式传 row_kind(`"battle"`/`t`/`kind`),属实 |
|
||||
| S16 | `smoke_all.gd:3491`(断言 :3496) | `erow2 = _db.resolve_enemy_rowset(1, int(depth_v), "battle")[0]`,下行 `scan_bad == 0` 断言,属实 |
|
||||
|
||||
另核 S2/S3/S4/S5/S7/S9 的行号指向(4795/4802/4810/4852/4860、2263/2279/2301、3460-3474),均与断言实际位置对得上,未发现空指或错指。
|
||||
|
||||
## B. 缺漏 2(V9/fv26 覆盖行)——通过
|
||||
|
||||
- 报告 §2.2 新增 fv26 行:「V9 反哺点五落点(非行为改动)|`reports/iter34-反哺点-2026-10-22.md` ①–⑤|件核」,与卡片 V9「§6 五条每条有落点」对应。
|
||||
- `reports/iter34-反哺点-2026-10-22.md` 实读确有 ①–⑤ 五节(① 敌人系统.md 记法与措辞、② 关卡节点地图.md :146、③ 关卡节点图生成.md 产出规格、④ 待办清单.md 状态回写、⑤ roadmap.md 迭代行),每节均写「文件与位置/改成/依据」。
|
||||
|
||||
## C. 缺漏 3(S15 `boss_name` 断言)——通过
|
||||
|
||||
- 用例位置:`game/tests/unit/presentation/battle_narrator_test.gd:59-82`,`_test_boss_name_uses_boss_rowset()` 由 `_init` 调用。
|
||||
- 真实入口成立:用例加载生产表后调 `NarratorScript.boss_name(runner, db)`(`battle_narrator.gd:72`),函数内走 `db.resolve_enemy_rowset(..., "boss")`(`:76`);生产调用点 `presentation/main.gd:1331` 同款。runner 替身只提供 `boss_name` 实际读取的 `level_row`/`current_node()` 两项,与 `core/level_runner.gd` 的同名面一致。
|
||||
- 期望值由数据读出:`boss_depth` 扫表取 L1 boss 行 `depth_band`,`boss_expect`/`battle_name` 分别由 boss/battle 行集首行 `display_name` 读出,无写死字面量;另有一条「boss 名 ≠ 同深度请求的战斗行集名」判别断言。
|
||||
- 自跑(命令与首行输出):
|
||||
- `PYTHONIOENCODING=utf-8 "<Godot>" --headless --path . --script res://tests/unit/presentation/battle_narrator_test.gd`
|
||||
→ `[battle_narrator_test] all green (13 checks)`,EXIT=0
|
||||
- 撤销注入:把 `battle_narrator.gd:76` 的 `"boss"` 改 `"battle"` 后重跑
|
||||
→ `[FAIL] Boss 名取自 Boss 行集首行(期望「食人魔酋长」实际「大地精」)〔fv24〕`;`[battle_narrator_test] 1/13 项失败`,EXIT=1
|
||||
- `git checkout -- game/presentation/battle_narrator.gd` 后复跑 → `[battle_narrator_test] all green (13 checks)`,EXIT=0
|
||||
- 与报告 §2.3 第 9 项所记(红断言原文、13 项回绿)逐字相符。
|
||||
|
||||
## D. 缺漏 4(V6①/fv21 撤销证明)——通过(实测与报告一致)
|
||||
|
||||
实测流程与结果:
|
||||
|
||||
1. 数据侧变异:`game/data/tables/enemy.tres` 的 (level_index 1, row_kind battle, depth_band 2, bugbear) 行 `depth_band` 2→1,跑 `res://smoke/smoke_all.gd`
|
||||
→ `=== 冒烟完成:1161 项检查,6 项失败 ===`(EXIT=1)。〔fv21〕**不在**失败列表(首遇带随数据变为 1、锚定行随迁——数据驱动正证成立)。6 项红为:〔fv11〕20 行键序、〔fv23〕熊地精首遇带固定 1、全深度×12 种子 spawn 扫描、「二十行九种池模板」、「先手击杀→摇定技能作废」、「行集内每个战斗/Boss 节点…精确行集(缺失 1)」。报告所记「共 6 项红」属实。
|
||||
2. 叠加写死:在 `smoke/smoke_all.gd` 的 `_first_encounter_band` 首行插入 `return 2`,重跑冒烟
|
||||
→ 首条红即 `[FAIL] 熊地精首遇带(1,battle,2)降档锚定(hp 22 · ac 2 · 池 attack6/charge2/heavy2 零改动——迭代 24 N1;首遇带数据驱动定位)〔fv21〕`;`=== 冒烟完成:1161 项检查,9 项失败 ===`(EXIT=1)。报告所记「共 9 项红」属实。
|
||||
3. `git checkout -- game/data/tables/enemy.tres game/smoke/smoke_all.gd` 还原,复跑
|
||||
→ `=== 冒烟完成:1161 项检查,0 项失败 ===`(EXIT=0)。
|
||||
|
||||
结论:该撤销证明现在能真正区分「数据驱动定位」与「写死坐标」——单跑数据变异〔fv21〕绿、叠加写死才红,报告 §2.3 第 8 项的记述与实测逐项一致。
|
||||
|
||||
## E. 描述不符 4 条——通过(4/4 已按实改写)
|
||||
|
||||
1. **fv17 夹具措辞**:`config_db_cross_validator_test.gd:95-116` 的 `_loaded()` 只构造 `level_index: 1`、`display_name: "夹具关"` 的合成单关夹具;fv17(:132)在该夹具上断言零误报。报告已改为「level 1 合成单关夹具;现役三关零误报由 fv6 兜住」,与实读一致。**通过**。
|
||||
2. **fv19 撤销归属**:`smoke_all.gd:4810-4820` 的静态求和(首遇混编数据 Σ=4,阈值 `<= 4`)放宽阈值不会红;真正能红的是同段运行期守卫(`:4891-4914` 注入 (1,battle,6) 两行各 3 只,`:4909` 断言 `not over_sys.spawn(...)`,由 `ROWSET_INSTANCE_MAX` 守卫兜住)。报告已改为「改运行期守卫 → 守卫断言红;静态求和本身放宽不会红」,归属正确。**通过**。
|
||||
3. **§2.3 第 7 项拦截首红**(可选复现已做):把 hobgoblin 行 `depth_band` 3→2 后跑 `res://tests/unit/data_config/config_db_test.gd`,首条错误为
|
||||
`ERROR: [ConfigDB/level_node_dangling] level 1 battle 节点深度 3 高于 (level 1, battle) 顶带键 2——顶 clamp 消解`
|
||||
随后 `[FAIL] 全部表加载 + 校验通过(含跨表护栏零 error 诊断)〔fv6〕`,EXIT=1;`git checkout --` 还原。与报告改写后的「跨表 level_node_dangling 首红、同带相邻未触发」一致。**通过**。
|
||||
4. **断言计数**:`grep -rn "〔fv[0-9]*〕" tests/ smoke/ --include=*.gd` 得 37 行;逐行核对,`smoke_all.gd:969` 为注释(`# 同带多行合法,带键聚合天然兼容〔fv24〕)`),其余 36 行均为断言行 → 「36 条断言+1 行注释」属实;fv14/fv26 按卡片约定走件核,代码侧无尾缀,覆盖说明自洽。**通过**。
|
||||
|
||||
## F. 测试削弱检查——通过
|
||||
|
||||
检查范围:`git diff 603eba31..68009b4e -- game/tests game/smoke`(8 个文件,+449/−166)与本次修补 `git diff 7bb41891..68009b4e`。
|
||||
|
||||
1. **本次修补未碰产品代码与既有断言**:`7bb41891..68009b4e` 仅 `battle_narrator_test.gd` 38 行新增(0 删除)+两份报告;新增用例为纯增量,无删断言、无阈值放宽、无扫描面缩小。
|
||||
2. **全量 diff 的删除行均属卡片 §8.2「旧断言预声明」允许的 S1–S16 改写**:删除面集中在 `config_db_resolver_test.gd`(S11 换代)、`config_db_test.gd`(S12 夹具键改名)、`config_db_cross_validator_test.gd`(S13)、`smoke_all.gd`(S1–S10)、两份探针(S14);`skill_unlock_test.gd` 的 `spawn(2,9)` 补显式 `"boss"`、冒烟 gp 掉落断言改读 `shop.tres` Boss 档区间+关级加值,均在报告 §2.4 第 1–3 条申报。
|
||||
3. **无阈值放宽 / 无扫描面缩小**:diff 中没有 seed/`range()` 扫描规模改动;第一关曲线阈值 `≤ 2.5`、混编 `≤ 4`、首遇 `unit_count=2`/固定 1 等判据原样保留;gp 掉落断言由硬编码 `[2,4]` 改为数据驱动 `Boss 档 [6,10] + 关级加值`(因 S10a 把该战改为 boss 语义,掉落档合法切换),判据仍比对结算输出,强度不降。
|
||||
4. **真实入口未换下层直调**:fv1–fv5 走 `resolve_enemy_rowset`,fv6–fv10 走加载后的 `EnemyTable.validate`,fv15–fv17 走 `cross_validate`,fv18–fv23 走冒烟表读取+`spawn`,S15 走 `boss_name` → `resolve_enemy_rowset(..., "boss")`。
|
||||
5. **数量面**:冒烟 1156→1161(本卡 +5),单元套件 35 个不变;`battle_narrator_test` 检查数 11→13(新增 2 条)。无净收缩。
|
||||
|
||||
结论:未发现未申报的测试削弱,本次修补为纯增量。
|
||||
|
||||
---
|
||||
|
||||
## 遗留例外/观察(不阻塞)
|
||||
|
||||
1. 第一轮审计报告第三节所记 diff 行数「删 173 行、增 418 行」与实测 `git diff --numstat 603eba31..7bb41891 -- game/tests game/smoke`(+411/−166)不一致,属第一轮报告的计数笔误(不影响其结论,也不属实现报告内容);本轮结论以实测为准。
|
||||
2. 新单测在解析 L1 battle 行集时(同深度请求落在 Boss 深度 4)会触发一次 `resolve_depth_clamp` 警告——这是顶带 clamp 的预期语义,非失败;用例仍 13 项全绿。
|
||||
|
||||
## 运行命令清单(均为工作树 `game/` 下、Godot 4.7.2 console 版、`PYTHONIOENCODING=utf-8`、一次一个进程)
|
||||
|
||||
- `--headless --path . --script res://tests/unit/presentation/battle_narrator_test.gd`(注入前后与还原后共 3 轮)
|
||||
- `--headless --path . --script res://smoke/smoke_all.gd`(数据变异/叠加写死/还原后共 3 轮)
|
||||
- `--headless --path . --script res://tests/unit/data_config/config_db_test.gd`(hobgoblin 错带复现 1 轮)
|
||||
- `git diff 603eba31..68009b4e -- game/tests game/smoke`、`git diff --numstat`、`grep -rn "〔fv[0-9]*〕"`、`git checkout -- <单文件>`、`git status --short`
|
||||
|
||||
审计结束时工作树除本报告外无改动,HEAD=`68009b4e`。
|
||||
Reference in new issue
Block a user