diff --git a/game/data/tables/board_gen.tres b/game/data/tables/board_gen.tres index a783327e..15d9393b 100644 --- a/game/data/tables/board_gen.tres +++ b/game/data/tables/board_gen.tres @@ -70,6 +70,32 @@ rows = [{ "shave_ring_radius": 1, "r0_max_cap": 43 }, { +"level_index": 1, +"node_depth": 5, +"board_width": 12, +"board_height": 12, +"mine_density": 0.25, +"inferability_target": 0.88, +"solver_rules": "single_subset", +"min_safe_count": 60, +"max_gen_attempts": 24, +"board_type": "dungeon", +"shave_ring_radius": 1, +"r0_max_cap": 43 +}, { +"level_index": 1, +"node_depth": 6, +"board_width": 12, +"board_height": 12, +"mine_density": 0.25, +"inferability_target": 0.88, +"solver_rules": "single_subset", +"min_safe_count": 60, +"max_gen_attempts": 24, +"board_type": "dungeon", +"shave_ring_radius": 1, +"r0_max_cap": 43 +}, { "level_index": 2, "node_depth": 0, "board_width": 12, @@ -97,6 +123,32 @@ rows = [{ "r0_max_cap": 43 }, { "level_index": 2, +"node_depth": 2, +"board_width": 12, +"board_height": 12, +"mine_density": 0.25, +"inferability_target": 0.88, +"solver_rules": "single_subset", +"min_safe_count": 60, +"max_gen_attempts": 24, +"board_type": "underdark", +"shave_ring_radius": 1, +"r0_max_cap": 43 +}, { +"level_index": 2, +"node_depth": 3, +"board_width": 12, +"board_height": 12, +"mine_density": 0.25, +"inferability_target": 0.88, +"solver_rules": "single_subset", +"min_safe_count": 60, +"max_gen_attempts": 24, +"board_type": "underdark", +"shave_ring_radius": 1, +"r0_max_cap": 43 +}, { +"level_index": 2, "node_depth": 4, "board_width": 12, "board_height": 12, @@ -149,6 +201,19 @@ rows = [{ "r0_max_cap": 43 }, { "level_index": 2, +"node_depth": 8, +"board_width": 12, +"board_height": 12, +"mine_density": 0.25, +"inferability_target": 0.88, +"solver_rules": "single_subset", +"min_safe_count": 60, +"max_gen_attempts": 24, +"board_type": "underdark", +"shave_ring_radius": 1, +"r0_max_cap": 43 +}, { +"level_index": 2, "node_depth": 9, "board_width": 12, "board_height": 12, @@ -188,6 +253,32 @@ rows = [{ "r0_max_cap": 51 }, { "level_index": 3, +"node_depth": 2, +"board_width": 13, +"board_height": 13, +"mine_density": 0.25, +"inferability_target": 0.88, +"solver_rules": "single_subset", +"min_safe_count": 70, +"max_gen_attempts": 32, +"board_type": "deep_underdark", +"shave_ring_radius": 1, +"r0_max_cap": 51 +}, { +"level_index": 3, +"node_depth": 3, +"board_width": 13, +"board_height": 13, +"mine_density": 0.25, +"inferability_target": 0.88, +"solver_rules": "single_subset", +"min_safe_count": 70, +"max_gen_attempts": 32, +"board_type": "deep_underdark", +"shave_ring_radius": 1, +"r0_max_cap": 51 +}, { +"level_index": 3, "node_depth": 4, "board_width": 13, "board_height": 13, @@ -240,6 +331,19 @@ rows = [{ "r0_max_cap": 51 }, { "level_index": 3, +"node_depth": 8, +"board_width": 13, +"board_height": 13, +"mine_density": 0.25, +"inferability_target": 0.88, +"solver_rules": "single_subset", +"min_safe_count": 70, +"max_gen_attempts": 32, +"board_type": "deep_underdark", +"shave_ring_radius": 1, +"r0_max_cap": 51 +}, { +"level_index": 3, "node_depth": 9, "board_width": 14, "board_height": 14, @@ -251,6 +355,58 @@ rows = [{ "board_type": "deep_underdark", "shave_ring_radius": 1, "r0_max_cap": 59 +}, { +"level_index": 3, +"node_depth": 10, +"board_width": 14, +"board_height": 14, +"mine_density": 0.25, +"inferability_target": 0.88, +"solver_rules": "single_subset", +"min_safe_count": 82, +"max_gen_attempts": 32, +"board_type": "deep_underdark", +"shave_ring_radius": 1, +"r0_max_cap": 59 +}, { +"level_index": 3, +"node_depth": 11, +"board_width": 14, +"board_height": 14, +"mine_density": 0.25, +"inferability_target": 0.88, +"solver_rules": "single_subset", +"min_safe_count": 82, +"max_gen_attempts": 32, +"board_type": "deep_underdark", +"shave_ring_radius": 1, +"r0_max_cap": 59 +}, { +"level_index": 3, +"node_depth": 12, +"board_width": 14, +"board_height": 14, +"mine_density": 0.25, +"inferability_target": 0.88, +"solver_rules": "single_subset", +"min_safe_count": 82, +"max_gen_attempts": 32, +"board_type": "deep_underdark", +"shave_ring_radius": 1, +"r0_max_cap": 59 +}, { +"level_index": 3, +"node_depth": 13, +"board_width": 14, +"board_height": 14, +"mine_density": 0.25, +"inferability_target": 0.88, +"solver_rules": "single_subset", +"min_safe_count": 82, +"max_gen_attempts": 32, +"board_type": "deep_underdark", +"shave_ring_radius": 1, +"r0_max_cap": 59 }] ; 切片调参登记(SLICE-TUNED provisional,调参所有权仍归 C3): diff --git a/game/data/tables/enemy.tres b/game/data/tables/enemy.tres index 615ca76b..e8c5c1de 100644 --- a/game/data/tables/enemy.tres +++ b/game/data/tables/enemy.tres @@ -99,6 +99,31 @@ rows = [{ "armor_class": 2 }, { "level_index": 1, +"row_kind": "battle", +"depth_band": 6, +"enemy_id": "bugbear", +"display_name": "熊地精", +"unit_count": 1, +"unit_count_max": 2, +"hp": 30, +"atk": 1, +"atk_min": 0, +"atk_max": 2, +"pool": [{ +"skill_id": "attack", +"weight": 6 +}, { +"skill_id": "charge", +"weight": 2 +}, { +"skill_id": "heavy", +"weight": 2 +}], +"attack_type": "normal", +"armor_type": "light", +"armor_class": 4 +}, { +"level_index": 1, "row_kind": "boss", "depth_band": 4, "enemy_id": "ogre_chief", @@ -126,6 +151,34 @@ rows = [{ "armor_type": "light", "armor_class": 8 }, { +"level_index": 1, +"row_kind": "boss", +"depth_band": 6, +"enemy_id": "ogre_chief", + "display_name": "食人魔酋长", + "unit_count": 1, + "hp": 88, + "atk": 1, + "atk_min": 0, + "atk_max": 2, +"pool": [{ +"skill_id": "attack", +"weight": 6 +}, { +"skill_id": "heavy", +"weight": 2 +}, { +"skill_id": "harden", +"weight": 2 +}, { +"skill_id": "quake_smash", +"weight": 2, +"unlock_hp_pct": 50 +}], +"attack_type": "normal", +"armor_type": "light", +"armor_class": 8 +}, { "level_index": 2, "row_kind": "battle", "depth_band": 0, @@ -308,6 +361,65 @@ rows = [{ "armor_class": 3 }, { "level_index": 2, +"row_kind": "battle", +"depth_band": 9, + "enemy_id": "giant_spider", + "display_name": "巨蜘蛛巢群", + "unit_count": 2, + "unit_count_max": 3, + "hp": 40, + "atk": 2, +"atk_min": 1, +"atk_max": 3, +"pool": [{ +"skill_id": "attack", +"weight": 6 +}, { +"skill_id": "bite", +"weight": 2 +}, { +"skill_id": "charge", +"weight": 1 +}, { +"skill_id": "venom_spit", +"weight": 2 +}], +"attack_type": "normal", +"armor_type": "light", +"armor_class": 3 +}, { +"level_index": 2, +"row_kind": "boss", +"depth_band": 8, + "enemy_id": "drider", + "display_name": "蛛化卓尔精灵", + "unit_count": 1, + "hp": 112, + "atk": 3, + "atk_min": 2, + "atk_max": 4, +"pool": [{ +"skill_id": "attack", +"weight": 6 +}, { +"skill_id": "heavy", +"weight": 2 +}, { +"skill_id": "venom_burst", +"weight": 2 +}, { +"skill_id": "web_snare", +"weight": 2 +}, { +"skill_id": "venom_curb", +"weight": 2, +"unlock_hp_pct": 50 +}], +"attack_type": "normal", +"armor_type": "ward", +"armor_class": 10 +}, { +"level_index": 2, "row_kind": "boss", "depth_band": 9, "enemy_id": "drider", @@ -495,6 +607,34 @@ rows = [{ "armor_class": 4 }, { "level_index": 3, +"row_kind": "battle", +"depth_band": 13, +"enemy_id": "duergar", + "display_name": "灰矮人小队", + "unit_count": 2, + "unit_count_max": 3, + "hp": 42, + "atk": 3, + "atk_min": 2, + "atk_max": 4, +"pool": [{ +"skill_id": "attack", +"weight": 6 +}, { +"skill_id": "charge", +"weight": 2 +}, { +"skill_id": "heavy", +"weight": 2 +}, { +"skill_id": "shield_bash", +"weight": 2 +}], +"attack_type": "normal", +"armor_type": "heavy", +"armor_class": 4 +}, { +"level_index": 3, "row_kind": "boss", "depth_band": 9, "enemy_id": "duergar_king", @@ -521,6 +661,90 @@ rows = [{ "attack_type": "normal", "armor_type": "heavy", "armor_class": 12 +}, { +"level_index": 3, +"row_kind": "boss", +"depth_band": 11, + "enemy_id": "duergar_king", + "display_name": "灰矮人石君王", + "unit_count": 1, + "hp": 150, + "atk": 3, + "atk_min": 2, + "atk_max": 4, +"pool": [{ +"skill_id": "attack", +"weight": 6 +}, { +"skill_id": "heavy", +"weight": 2 +}, { +"skill_id": "earth_pulse", +"weight": 2 +}, { +"skill_id": "rockslide", +"weight": 2, +"unlock_hp_pct": 50 +}], +"attack_type": "normal", +"armor_type": "heavy", +"armor_class": 12 +}, { +"level_index": 3, +"row_kind": "boss", +"depth_band": 12, + "enemy_id": "duergar_king", + "display_name": "灰矮人石君王", + "unit_count": 1, + "hp": 162, + "atk": 3, + "atk_min": 2, + "atk_max": 4, +"pool": [{ +"skill_id": "attack", +"weight": 6 +}, { +"skill_id": "heavy", +"weight": 2 +}, { +"skill_id": "earth_pulse", +"weight": 2 +}, { +"skill_id": "rockslide", +"weight": 2, +"unlock_hp_pct": 50 +}], +"attack_type": "normal", +"armor_type": "heavy", +"armor_class": 12 +}, { +"level_index": 3, +"row_kind": "boss", +"depth_band": 13, + "enemy_id": "duergar_king", + "display_name": "灰矮人石君王", + "unit_count": 1, + "hp": 176, + "atk": 3, + "atk_min": 2, + "atk_max": 4, +"pool": [{ +"skill_id": "attack", +"weight": 6 +}, { +"skill_id": "heavy", +"weight": 2 +}, { +"skill_id": "earth_pulse", +"weight": 2 +}, { +"skill_id": "rockslide", +"weight": 2, +"unlock_hp_pct": 50 +}], +"attack_type": "normal", +"armor_type": "heavy", +"armor_class": 12 }] ; 切片调参登记 v3(2026-09-12 试玩批次 1 触发——F7 流程;C3 GDD F5 对照): diff --git a/game/tests/unit/core/level_shop_economy_test.gd b/game/tests/unit/core/level_shop_economy_test.gd index af4469a8..4eefdd2a 100644 --- a/game/tests/unit/core/level_shop_economy_test.gd +++ b/game/tests/unit/core/level_shop_economy_test.gd @@ -37,9 +37,60 @@ func _init() -> void: _smoke_shop_room() _smoke_shop_graph() _smoke_shop_pool() + _smoke_generated_coverage() quit(Kit.finish("level_shop_economy_test")) +# ---- 生成图深度扩面覆盖断言(迭代 35 卡② S5/V11——节点源=卡① 生成器产图, +# 敌侧/板侧解析一律走 ConfigDB 真实消费链,禁直读行集自算)---- + +func _smoke_generated_coverage() -> void: + var RngScript := preload("res://core/rng_service.gd") + var GenScript := preload("res://core/node_map_generator.gd") + var gen = GenScript.new(RngScript.new()) + var enemy_ok := true + var board_ok := true + var boss_band_ok := true + var readings := [] + for L in [1, 2, 3]: + var params: Dictionary = _db.level_gen_params_row(L) + if params.is_empty(): + Kit.check("L%d 参数档缺行(覆盖断言无源可扫)〔v11〕" % L, false) + continue + var dmin := 999 + var dmax := 0 + for seed_val in range(42, 62): + var g: Dictionary = gen.generate(params, seed_val) + var by_id := {} + for n in g["nodes"]: + by_id[int(n["id"])] = n + for n in g["nodes"]: + var d: int = int(n["depth"]) + dmin = mini(dmin, d) + dmax = maxi(dmax, d) + var kind := String(n["type"]) + if kind == "battle" or kind == "boss": + if _db.resolve_enemy_rowset(L, d, "boss" if kind == "boss" else "battle").is_empty(): + enemy_ok = false + var board: Dictionary = _db.resolve_board_gen(L, d) + if board.is_empty() or int(board.get("node_depth", -1)) != d: + board_ok = false + var bands := [] + for tid in g["terminals"]: + var bd: int = int(by_id[int(tid)]["depth"]) + var rs: Array[Dictionary] = _db.resolve_enemy_rowset(L, bd, "boss") + bands.append(int(rs[0].get("depth_band", -1)) if not rs.is_empty() else -1) + for i in range(bands.size()): + for j in range(i + 1, bands.size()): + if bands[i] == bands[j]: + boss_band_ok = false + readings.append("L%d 深度域 %d–%d" % [L, dmin, dmax]) + print("[v11] 生成图覆盖量程读数:" + "; ".join(readings)) + Kit.check("生成图每个 battle/boss 节点深度在敌表解析到非空行集(顶 clamp 消解禁)〔v11〕", enemy_ok) + Kit.check("生成图每个节点深度在 board_gen 精确命中(非空且 node_depth 等值)〔v11〕", board_ok) + Kit.check("多终点 Boss 各终点解析带键两两互异(L2/L3×20 种子)〔v11〕", boss_band_ok) + + # ---- 关卡节点地图·线性推进冒烟---- func _smoke_level() -> void: @@ -335,7 +386,7 @@ func _smoke_level_graph() -> void: _db.enemy.rows.remove_at(_db.enemy.rows.size() - 1) _db.enemy.rows.remove_at(_db.enemy.rows.size() - 1) _db.enemy.rows.remove_at(_db.enemy.rows.size() - 1) - Kit.check("混编断言段注入行全量还原(表回 20 行——首批混编投放后基线行数,迭代 18 卡⑤;后续断言面零污染)", int(_db.enemy.rows.size()) == 20) + Kit.check("混编断言段注入行全量还原(表回 28 行——迭代 35 生成图深度扩面覆盖批后的基线行数;首批混编投放基线 20 行+覆盖 8 行)", int(_db.enemy.rows.size()) == 28) # ⑬ 真图类型可见性(信息迷雾 v2.1):L2 入口停点纯核心面读数——候选 ⊆ 可见集 / # Boss 恒可见 / 层距 > N 的非豁免节点入雾。N 取数据表(代码零硬编码)。 @@ -710,7 +761,11 @@ func _smoke_shop_graph() -> void: 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]: Kit.check("(%d,boss,9) Boss 行在 board_gen + enemy 精确存在" % L, bg_keys.has("%d_9" % L) and en_keys.has("%d_boss_9" % L)) - Kit.check("(%d,battle,8) 商店房无板/战斗行集残留(陈旧行已清理)" % L, not bg_keys.has("%d_8" % L) and not en_keys.has("%d_battle_8" % L)) + # 迭代 35 生成图深度扩面覆盖批随动(旧判据属模板时代:深度 8 = 商店房专属位,故禁板行与战斗行): + # 生成图下深度 8 是正常可行深度 ⇒ 板侧改判「覆盖行在册」;战斗侧仍禁 (L,battle,8) 精确键 + #(战斗落带经顶带 9/13 解析,不落 8 号精确键——两侧判据都保留,只按现行设计换正负号)。 + Kit.check("(%d,8) 板侧生成图覆盖行在册·(%d,battle,8) 战斗精确键仍不存在" % [L, L], + bg_keys.has("%d_8" % L) and not en_keys.has("%d_battle_8" % L)) Kit.check("L1 行集零改动(单链 Boss (1,4)——教学期最小修订面)", 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")) diff --git a/game/tests/unit/data_config/table_structure_guard_test.gd b/game/tests/unit/data_config/table_structure_guard_test.gd index 7ec9c00d..2552719a 100644 --- a/game/tests/unit/data_config/table_structure_guard_test.gd +++ b/game/tests/unit/data_config/table_structure_guard_test.gd @@ -222,26 +222,31 @@ func _smoke_adventure_table() -> void: v18_ok and locked_n == 12) -# ---- 敌人表深度带键结构(迭代 34:20 行键迁移 · 键序=迁移表序 · hobgoblin 留任 · 三 Boss 行 kind/band)---- +# ---- 敌人表深度带键结构(迭代 34 建·迭代 35 覆盖批换代:28 行键序=覆盖表序·hobgoblin 留任·Boss 行集带键清单)---- func _smoke_enemy_bands() -> void: - print("--- 敌人表深度带键结构(20 行键序 · hobgoblin 留任 · 三 Boss 行)---") - # ① 20 行计数 + 键序 = 迁移表序(文件行序不变——行序即契约) + print("--- 敌人表深度带键结构(28 行键序 · hobgoblin 留任 · Boss 带键覆盖表)---") + # ① 28 行计数 + 键序 = 覆盖表序(文件行序不变——行序即契约) + # 迭代 35 生成图深度扩面覆盖批随动:战斗带顶扩到各关最大生成深度(L1→6/L2→9/L3→13)、 + # Boss 行按终点深度分带(L1 6/L2 8·9/L3 11·12·13),模板时代带键 4/9/9 全部在册不动 var expect_keys: Array[String] = [ "1|battle|0|goblin", "1|battle|1|goblin", "1|battle|2|bugbear", "1|battle|3|hobgoblin", - "1|boss|4|ogre_chief", + "1|battle|6|bugbear", + "1|boss|4|ogre_chief", "1|boss|6|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", + "2|battle|9|giant_spider", + "2|boss|8|drider", "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", + "3|battle|6|hook_horror", "3|battle|7|duergar", "3|battle|13|duergar", + "3|boss|9|duergar_king", "3|boss|11|duergar_king", "3|boss|12|duergar_king", "3|boss|13|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", ""))]) - Kit.check("敌人表 20 行且键序 = 迁移表序((L,row_kind,depth_band,enemy_id) 逐行比对——文件行序不变)〔fv11〕", - _db.enemy.rows.size() == 20 and got_keys == expect_keys) + Kit.check("敌人表 28 行且键序 = 覆盖表序((L,row_kind,depth_band,enemy_id) 逐行比对——文件行序不变)〔fv11〕", + _db.enemy.rows.size() == 28 and got_keys == expect_keys) # ② hobgoblin 缺口行留任随迁(契约 9:不删行不补立绘——缺口登记随行) var hob := {} for row in _db.enemy.rows: @@ -252,23 +257,27 @@ func _smoke_enemy_bands() -> void: and int(hob.get("depth_band", -1)) == 3) Kit.check("hobgoblin 缺口登记随行(表文件注释在位——缺口不静默)〔fv25〕", FileAccess.get_file_as_string("res://data/tables/enemy.tres").contains("hobgoblin 行为缺口行")) - # ③ 三 Boss 行 kind/band 就位(band = 该关图 Boss 节点深度——单终点图数据驱动) + # ③ Boss 行集带键 = 覆盖表(旧判据「各恰一行=图 Boss 节点深度」属单终点模板时代; + # 生成图多终点要求每终点深度解析到互异带键 ⇒ 每关 Boss 行必然多于一行—— + # 迭代 35 覆盖批随动改写为带键清单,「带键互异」的运行时把关在 level_shop_economy_test〔v11〕) + var boss_bands := {} + for row in _db.enemy.rows: + if String(row.get("row_kind", "")) == "boss": + var lk: int = int(row.get("level_index", -1)) + if not boss_bands.has(lk): + boss_bands[lk] = [] + boss_bands[lk].append(int(row.get("depth_band", -1))) + var expect_bands := {1: [4, 6], 2: [8, 9], 3: [9, 11, 12, 13]} 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: + var got: Array = boss_bands.get(L, []) + got.sort() + if got != expect_bands[L]: boss_ok = false - boss_detail += " L%d:boss 行×%d" % [L, hits] - Kit.check("三关 Boss 行各恰一行且 row_kind=boss、band=图 Boss 节点深度(%d/%d/%d%s)〔fv13〕" % [ - _boss_depth(1), _boss_depth(2), _boss_depth(3), boss_detail], boss_ok) + boss_detail += " L%d:%s≠%s" % [L, str(got), str(expect_bands[L])] + Kit.check("三关 Boss 行集带键 = 覆盖表(模板终点 4/9/9 在册+生成图终点 6/8/9/11/12/13 扩带)%s〔fv13〕" % boss_detail, + boss_ok) # ② 免伤墙归零护栏泛化保留(BUG-0003 事故类永久免疫):hero_defense 池任何 percent 行