diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index 04aacc85..8acb1cc5 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -80,6 +80,7 @@ func _run_core_smoke(db, rng) -> void: _smoke_variation_framework() _smoke_variant_placement(1) _smoke_variant_placement(2) + _smoke_global_variant() _smoke_full_battle(1, 0, "L1 · 地精") _smoke_full_battle(2, 0, "L2 · 熊地精") _smoke_level() @@ -1265,8 +1266,8 @@ func _mk_stream(stream_name: String, scope: int) -> RandomNumberGenerator: func _smoke_variation_framework() -> void: print("--- 地形变异 框架面(首批四类型 + 候选池门控 + 跨字段互锁)---") - check(_db.variation.rows.size() == 6, "变异清单 6 行(荆棘丛/藤蔓缠绕/岩浆/烬覆 + 落石/矿尘——首批类型定稿批 + 第三关内容批)") - check(_db.variation.validate().is_empty(), "变异表校验零错误(6 行全过域与跨字段互锁)") + check(_db.variation.rows.size() == 7, "变异清单 7 行(荆棘丛/藤蔓缠绕/岩浆/烬覆 + 落石/矿尘 + 贫矿脉——首批类型定稿批 + 第三关内容批 + 全局作用域变异首发批)") + check(_db.variation.validate().is_empty(), "变异表校验零错误(7 行全过域与跨字段互锁——含 scope=global 三互锁)") var pool1_ids: Array = [] for r in _db.variant_pool(1, "dungeon"): pool1_ids.append(String(r.variant_id)) @@ -1300,6 +1301,165 @@ func _errs_has_cross_field(errs: Array) -> bool: return false +# ---- 全局作用域变异 首发批断言面(2026-10-04 迭代 14 ③号卡——T0-T5 集中:互锁/门控三维/ +# 替换制/零取号/资源面偏斜守恒与合成序/快照投影/呈现面锚)---- + +# 取号计数桩(variant_place 零取号断言——子类拦截计数,派生与取数语义零改动) +class _CountVariantRng extends RngServiceCore: + var variant_calls := 0 + func open_stream(purpose: String) -> RandomNumberGenerator: + if purpose == "variant_place": + variant_calls += 1 + return super.open_stream(purpose) + + +func _smoke_global_variant() -> void: + print("--- 全局作用域变异 首发批(门控三维 + 替换制 + 资源面偏斜)---") + # ① 跨字段互锁负样本(resource_face ⟺ scope=global 三条 + effect_value 新域 [0,50]) + var vt = load("res://data/schemas/variation_table.gd").new() + vt.rows.assign([{"variant_id": "g1", "display_name": "负甲", "unlock_level": 2, "scope": "region", + "effect": "resource_face", "effect_value": 30, "trigger": "always", "placement": "any", + "board_types": [], "counter_items": []}]) + check(_errs_has_cross_field(vt.validate()), "互锁负样本③:resource_face + scope=region 被拒(resource_face ⇒ global)") + vt.rows.assign([{"variant_id": "g2", "display_name": "负乙", "unlock_level": 2, "scope": "global", + "effect": "hide_number", "effect_value": 0, "trigger": "always", "placement": "any", + "board_types": [], "counter_items": []}]) + check(_errs_has_cross_field(vt.validate()), "互锁负样本④:scope=global + effect=hide_number 被拒(global ⇒ resource_face)") + vt.rows.assign([{"variant_id": "g3", "display_name": "负丙", "unlock_level": 2, "scope": "global", + "effect": "resource_face", "effect_value": 30, "trigger": "on_open", "placement": "any", + "board_types": [], "counter_items": []}]) + check(_errs_has_cross_field(vt.validate()), "互锁负样本⑤:scope=global + trigger=on_open 被拒(global ⇒ always 板级恒在)") + vt.rows.assign([{"variant_id": "g4", "display_name": "负丁", "unlock_level": 2, "scope": "global", + "effect": "resource_face", "effect_value": 30, "trigger": "always", "placement": "closed_only", + "board_types": [], "counter_items": []}]) + check(_errs_has_cross_field(vt.validate()), "互锁负样本⑥:scope=global + placement=closed_only 被拒(global ⇒ any 无格子布置域)") + vt.rows.assign([{"variant_id": "g5", "display_name": "负戊", "unlock_level": 2, "scope": "global", + "effect": "resource_face", "effect_value": 51, "trigger": "always", "placement": "any", + "board_types": [], "counter_items": []}]) + var vterr: Array = vt.validate() + var domain_hit := false + for e in vterr: + if String(e.get("category", "")) == "domain": + domain_hit = true + check(domain_hit, "互锁负样本⑦:resource_face effect_value=51 越域 [0,50] 被拒(不适用 flat_damage [1,9] 加法核)") + # ② 门控第三维(节点身份——global 行仅 boss/elite 入池;缺省/普通节点静默跳过零报错) + var pool_boss_ids: Array = [] + for r in _db.variant_pool(2, "underdark", "boss"): + pool_boss_ids.append(String(r.variant_id)) + check(pool_boss_ids == ["magma", "ash", "skewed_veins"], "门控三维:L2 underdark boss 池 = region 两行 + 贫矿脉(board_types ∅ 通配)") + var pool_battle_ids: Array = [] + for r in _db.variant_pool(2, "underdark", "battle"): + pool_battle_ids.append(String(r.variant_id)) + check(pool_battle_ids == ["magma", "ash"], "门控三维:battle 节点静默跳过 global 行(普通图零报错零警告)") + check(_db.variant_pool(2, "underdark").is_empty() == false and _db.variant_pool(2, "underdark").size() == 2, + "门控三维:缺省双参调用 = global 行跳过(既有调用口径零扰动)") + check(_db.variant_pool(1, "underdark", "boss").is_empty(), "L1 Boss 免疫:unlock_level=2 门控在 boss 节点同样生效(教学保护带 B)") + check(_db.global_variant_row(2, "underdark", "battle").is_empty(), "全局决议:battle 节点恒无全局型") + check(_db.global_variant_row(2, "underdark", "boss").get("variant_id", "") == "skewed_veins", "全局决议:L2 boss 取贫矿脉(确定性单行)") + check(_db.global_variant_row(1, "underdark", "boss").is_empty() and _db.global_variant_row(1, "dungeon", "boss").is_empty(), + "全局决议:L1 boss 全局池空 → 空行(兜底回落承载)") + check(_db.global_variant_row(2, "underdark", "elite").get("variant_id", "") == "skewed_veins", "elite 前向词位:boss 同池(首发零 elite 表行——词表位不造行)") + # ③ 替换制 + 零取号:L2 Boss 图恰一行板级变异 + 零 region 格 + variant_place 零消费 + 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 节点——替换制断言载体)") + if boss_ok: + var bb = boss_settlement.minesweeper.current_board() + check(_arr_str_eq(bb.board_variant_ids(), ["skewed_veins"]), "替换制:Boss 图 board_variants 恰一行 = 贫矿脉") + check(bb.variants.is_empty(), "替换制:Boss 图零 region 变异格(无格子级变异标记——呈现件闲置合法态)") + check(crng.variant_calls == 0, "零取号合法态:Boss 图 variant_place 零消费(全局取用确定性)") + var snap: Dictionary = boss_settlement.presentation_snapshot() + check(_arr_str_eq(snap.get("board_variants", []), ["skewed_veins"]), "快照投影:board_variants 呈现面(横幅/角标唯一读取面)") + # Boss 图确定性:同种子两遍逐位相同 + 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"}): + var bb2 = boss2.minesweeper.current_board() + check(_arr_str_eq(bb2.board_variant_ids(), ["skewed_veins"]) and crng2.variant_calls == 0, + "确定性:同种子 Boss 图两遍 board_variants/取号逐位相同") + # ④ 资源面偏斜投影(RewardEconomy 纯函数面——守恒/白名单/构造例/合成序) + var eco = load("res://core/reward_economy.gd").new(_db, load("res://core/rng_service.gd").new()) + var base_rows: Array = eco._rollable_rows() + var base_sum: int = eco._pool_weight(base_rows) + check(base_sum == 77, "偏斜基线:正向池现行合计 77(挖掘风险批口径)") + eco.board_skew = 0 + var off_rows: Array = eco._biased_cell_rows() + check(off_rows.size() == base_rows.size() and _rows_weights_equal(off_rows, base_rows), "off 态零扰动:board_skew=0 行集与基线逐位相同(不进投影域)") + eco.board_skew = 30 + var on_rows: Array = eco._biased_cell_rows() + check(eco._pool_weight(on_rows) == 77, "合计守恒:偏斜后正向池合计 = 77(整数最大余数重归一)") + var wmap := _rows_weight_map(on_rows) + check(wmap.get("weaken", -1) == 8 and wmap.get("ap_up", -1) == 6 and wmap.get("gp_coin", -1) == 2, + "白名单豁免:weaken/gp/action 三行偏斜后逐位不动") + check(wmap.get("heal_minor", -1) == 3 and wmap.get("ac_up", -1) == 7, "生存族下调:heal 4→3 / defense 11→7(v=30 构造例)") + check(wmap.get("attack_up", -1) == 15 and wmap.get("spark_aoe", -1) == 18 and wmap.get("pierce_strike", -1) == 9 and wmap.get("magic_bolt", -1) == 9, + "进攻族上浮:attack 14→15 / blast 16→18 / pierce 8→9 / magic 8→9(v=30 构造例,等比上浮守恒)") + eco.board_skew = 30 + eco.luck_bias = 2 + var combo_rows: Array = eco._biased_cell_rows() + var cw := _rows_weight_map(combo_rows) + check(eco._pool_weight(combo_rows) == 77 + 4 * 2, "合成序:图级偏斜先(乘法面)件级 luck_bias 后(加法 +2×四稀有类)——合计 77+8") + check(cw.get("heal_minor", -1) == 5 and cw.get("gp_coin", -1) == 4 and cw.get("pierce_strike", -1) == 11 and cw.get("magic_bolt", -1) == 11, + "合成序构造例:skew(heal 3)+2=5 / gp 2+2=4 / pierce 9+2=11 / magic 9+2=11(图级定型后玩家构筑叠加)") + eco.luck_bias = 0 + eco.board_skew = 50 + var v50: Dictionary = _rows_weight_map(eco._biased_cell_rows()) + check(eco._pool_weight(eco._biased_cell_rows()) == 77 and int(v50.get("heal_minor", -1)) > 0, "域上界 v=50:守恒成立且逐行权重恒正(域 [0,50] 安全)") + # 普通图摇号全链路确定性(off 路径)——同种子普通图资源映射逐位相同(golden 对拍另有批前快照) + var nrng = load("res://core/rng_service.gd").new() + nrng.set_master_seed(42) + var normal1 = load("res://core/battle_orchestrator.gd").new(_db, nrng) + normal1.begin_battle({"level_index": 2, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)}) + var n1: Dictionary = _board_resource_dump(normal1) + var nrng2 = load("res://core/rng_service.gd").new() + nrng2.set_master_seed(42) + var normal2 = load("res://core/battle_orchestrator.gd").new(_db, nrng2) + normal2.begin_battle({"level_index": 2, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)}) + check(n1 == _board_resource_dump(normal2) and not n1.is_empty(), "确定性:同种子普通图布置/摇号两遍逐位相同(off 全链路)") + + +func _arr_str_eq(a, b: Array) -> bool: + var arr: Array = a if a is Array else [] + if arr.size() != b.size(): + return false + for i in arr.size(): + if String(arr[i]) != String(b[i]): + return false + return true + + +func _rows_weights_equal(a: Array, b: Array) -> bool: + if a.size() != b.size(): + return false + for i in a.size(): + if String(a[i].get("reward_id", "")) != String(b[i].get("reward_id", "")) or int(a[i].weight) != int(b[i].weight): + return false + return true + + +func _rows_weight_map(rows: Array) -> Dictionary: + var m := {} + for r in rows: + m[String(r.get("reward_id", ""))] = int(r.weight) + return m + + +func _board_resource_dump(settlement) -> Dictionary: + var b = settlement.minesweeper.current_board() + var dump := {} + for c in b.variants.keys(): + dump["v:%d,%d" % [c.x, c.y]] = ",".join(b.variants[c]) + for y in b.height: + for x in b.width: + var ident = settlement._reward_economy.query_identity(Vector2i(x, y)) + if ident != null: + dump["r:%d,%d" % [x, y]] = String(ident.reward_id) + return dump + + # ---- 地形变异 布置段(首批类型定稿批 2026-09-19——棋盘设置顺序第 4 步断言面)---- # 布置发生在编排器战斗初始化(install 初始展开 + roll_board 资源格之后),生成器产物零变异—— # 本段经 begin_battle 走完整初始化序后断言:门控一致性 / 布置域 / 数量公式 / 连通片上限 / 确定性。 diff --git a/game/tests/unit/data_config/config_db_test.gd b/game/tests/unit/data_config/config_db_test.gd index bdf4c2bc..53cd86fd 100644 --- a/game/tests/unit/data_config/config_db_test.gd +++ b/game/tests/unit/data_config/config_db_test.gd @@ -81,13 +81,18 @@ func _test_progress_resolution(db) -> void: Kit.check("关卡越最大行 clamp 取最大关卡行", int(high.get("level_index", -99)) == max_l) -# ---- 变异池门控(unlock_level 闸 + ∅ board_types 通配)---- +# ---- 变异池门控(unlock_level 闸 + ∅ board_types 通配 + 节点身份第三维)---- func _test_variant_pool_gate(db) -> void: var rows: Array = db.variation.rows var gated_row := {} var wildcard_row := {} + var global_row := {} for row in rows: + if String(row.get("scope", "region")) != "region": + if global_row.is_empty(): + global_row = row # global 行第三维门控另测(全局作用域变异批 2026-10-04) + continue var tags: Array = row.get("board_types", []) if gated_row.is_empty() and not tags.is_empty(): gated_row = row @@ -104,6 +109,11 @@ func _test_variant_pool_gate(db) -> void: var u2: int = int(wildcard_row.unlock_level) var any_pool: Array = db.variant_pool(u2, "nonexistent_board_type_tag") Kit.check("∅ board_types 行通配(任一 board_type 命中)", _rows_contain(any_pool, wildcard_row)) + if not global_row.is_empty(): + var u3: int = int(global_row.unlock_level) + Kit.check("global 行 boss 节点入池(board_types ∅ 通配)", _rows_contain(db.variant_pool(u3, "nonexistent_board_type_tag", "boss"), global_row)) + Kit.check("global 行缺省双参调用跳过(既有调用口径零扰动)", not _rows_contain(db.variant_pool(u3, "nonexistent_board_type_tag"), global_row)) + Kit.check("global 行 battle 节点静默跳过(门控兜底零报错)", not _rows_contain(db.variant_pool(u3, "nonexistent_board_type_tag", "battle"), global_row)) func _rows_contain(pool: Array, row: Dictionary) -> bool: