## 棋盘安装与变异布置域件(B1 域·BoardInstaller):生成受理、全局作用域变异决议、装板受理、 ## 资源摇号资格域组装(roll_board 恰一次——install 后、挖掘前)、region 变异决议与连通片布置。 ## 棋盘设置顺序 = 生成棋盘 → 打开状态初始化 → 战斗资源格设置 → 变异格设置(布置在初始展开与 ## 资源格之后,变异天然不落已开区)。宿主侧装板后状态复位(封印/完美计数清零、板序推进、 ## board_changed 发射、秘宝叠加层退役)留宿主薄壳——本件零复位职责。零 UI 零 await; ## 随机只经注入 RngServiceCore 的 board_gen/variant_place 两封闭流(流名与开流时点不变, ## 生成器只消费传入流——同种子同输入逐位复现)。 class_name BoardInstaller extends RefCounted var db: ConfigDb var rng: RngServiceCore var minesweeper: MinesweeperRules var reward_economy: RewardEconomy var encounter: Dictionary[String, Variant] # 宿主 encounter 同对象引用(assign 原地变异——跨战活引用) var fatal_cb: Callable # 协作件违约→宿主 _fatal 封印(单点接线——BoardGenerator 同款) var generator: BoardGenerator # 件内自建自接线(原宿主 _board_generator 成员随件迁移) func _init(bundle: Dictionary) -> void: for k in ["db", "rng", "minesweeper", "reward_economy", "encounter", "fatal_cb"]: assert(bundle.has(k), "[装板域件] 引用束缺键: %s" % k) db = bundle.db rng = bundle.rng minesweeper = bundle.minesweeper reward_economy = bundle.reward_economy encounter = bundle.encounter fatal_cb = bundle.fatal_cb generator = BoardGenerator.new() generator.fatal_handler = fatal_cb # 协作件违约→宿主 _fatal 封印(单点接线) func _fatal(msg: String) -> void: if fatal_cb.is_valid(): fatal_cb.call(msg) # 组装层注入 orchestrator._fatal(封印传导,不绕第二中止通道) else: push_error(msg) assert(false, msg) # 装板单入口(生成 → 全局变异决议 → 装板受理 → 资源摇号 → 变异布置); # 宿主态复位归宿主薄壳(_install_and_reset)——本件返回即装板完成、复位未行。 func install(during_round := false) -> bool: var stream: RandomNumberGenerator = rng.open_stream("board_gen") var board: BoardData = generator.generate(int(encounter.level_index), int(encounter.node_depth), stream, db) if board == null: return false # 全局作用域变异决议(装板序内前置于 roll_board:图级偏斜旋钮须在资源摇号前定型——详见 _resolve_global_variant) _resolve_global_variant(board) var res: Dictionary[Variant, Variant] = minesweeper.install_board(board, during_round) if not res.accepted: _fatal("排雷玩法 install 拒绝(不变式违例): %s" % str(res)) return false # 地图战斗资源 义务⑥:roll_board 恰一次(install 后、挖掘前);资格格 = 全部非封锁格(安全格 + 危险格,雷安同池) var r0 := {} for c in board.compute_r0(): r0[c] = true var eligible: Array[Dictionary] = [] for c in board.safe_cells(): if not r0.has(c): eligible.append({"cell": c}) for c in board.mines.keys(): if not r0.has(c): eligible.append({"cell": c}) if not reward_economy.roll_board(eligible): _fatal("地图战斗资源 roll_board 被拒") return false # 地形变异 布置段(棋盘设置顺序第 4 步)——布置在初始展开与资源格之后,变异天然不落已开区 _place_variants(board) return true # ---- 全局作用域变异 决议(装板序内 roll_board 之前)---- # boss/elite 图命中全局型:写整图 board_variants(恰一行)+ 注入资源面偏斜旋钮(图级投影在 # RewardEconomy.roll_board 消费);不消费 variant_place 流(全局取用确定性——零取号合法态)。 # 全局池空(含 L1 Boss 免疫/unlock 门控未达):字段留空、旋钮归零——_place_variants 原路走 # region 布置 = 兜底回落合法态(防配置缺行静默裸奔——provisional)。 func _resolve_global_variant(board: BoardData) -> void: board.board_variants.clear() reward_economy.board_skew = 0 var row: Dictionary[Variant, Variant] = db.combat.variation.global_variant_row(int(encounter.level_index), String(board.board_type), String(encounter.get("node_type", ""))) if row.is_empty(): return board.board_variants = [String(row.variant_id)] reward_economy.board_skew = int(row.get("effect_value", 0)) # ---- 地形变异 布置段(棋盘设置顺序第 4 步)---- # 候选池门控(unlock_level ≤ 关卡序号 + board_types 与本地形交集,∅ 通配)经 config_db.variant_pool; # 池空 = 合法态,静默零布置(框架 P0 口径不变)。零特判:本段只写格子变异数据快照,不改扫雷规则本体。 # 候选格域 = 安全格 − 初始展开区(伤害型 placement=closed_only 的结构保障——布置发生在初始展开之后, # 已开格天然排除);危险格不吃变异(挖雷一层惩罚 + 变异伤害双层叠罚污染逐行归因,首版排除——provisional)。 # 每图至多一种类型(候选池内均匀选一);数量 = round(候选池格数×percent) + # (关卡序号−1)×per_level_bonus——基数为候选池而非总格数(percent 语义 = 占可挖格比例, # 关卡递增由 per_level_bonus 绝对格数承载);连通片生长(8 邻),单片 ≤ pack_max。 # 替换制:整图 board_variants 非空 = 本图已带全局型,跳过 region 布置(详见 _resolve_global_variant)。 func _place_variants(board: BoardData) -> void: if not board.board_variants.is_empty(): return var pool: Array[Variant] = db.combat.variation.variant_pool(int(encounter.level_index), String(board.board_type)) if pool.is_empty(): return var r0 := {} for c in board.compute_r0(): r0[c] = true var candidates: Array[Vector2i] = [] for c in board.safe_cells(): if not r0.has(c): candidates.append(c) if candidates.is_empty(): return var stream: RandomNumberGenerator = rng.open_stream("variant_place") # 一板恰一 scope(换图/刷新图各取号一次) var vrow: Dictionary[Variant, Variant] = pool[stream.randi_range(0, pool.size() - 1)] var total: int = int(round(float(candidates.size()) * float(db.combat.variation.percent_of_cells) / 100.0)) \ + (int(encounter.level_index) - 1) * int(db.combat.variation.per_level_bonus) total = mini(total, candidates.size()) _place_variants_apply(board, candidates, stream, vrow, total) # 变异片生长施加面(布置域=候选格,数量=total,vrow 单型贯穿;取号序=stream 顺序消费) func _place_variants_apply(board: BoardData, candidates: Array[Vector2i], stream: RandomNumberGenerator, vrow: Dictionary, total: int) -> void: var placed := {} while placed.size() < total: var pack_size: int = mini(total - placed.size(), stream.randi_range(2, int(db.combat.variation.pack_max))) var free: Array[Vector2i] = [] for c in candidates: # 新片种子/生长格禁与其他已布片 8 邻贴接——否则两片粘连成一个超 pack_max 的连通分量 # (R3 随机流下实测暴露:独立生长的相邻片合流击穿单片上限断言) if placed.has(c) or _adjacent_to_packs(board, placed, c): continue free.append(c) if free.is_empty(): break # 生长域耗尽——该片就地收束(数量不足总目标为合法态,候选域碎片化时触发) var seed_c: Vector2i = free[stream.randi_range(0, free.size() - 1)] placed[seed_c] = true var pack: Array[Variant] = [seed_c] var pack_set := {seed_c: true} while pack.size() < pack_size: var fringe := _variants_fringe(board, pack, candidates, placed, pack_set) if fringe.is_empty(): break # 生长域耗尽——该片就地收束(数量不足总目标为合法态,候选域碎片化时触发) var nxt: Vector2i = fringe[stream.randi_range(0, fringe.size() - 1)] placed[nxt] = true pack_set[nxt] = true pack.append(nxt) for c in placed.keys(): board.variants[c] = [String(vrow.variant_id)] # 片生长待选格收集(本片内各生长格的 8 邻候选并集——未布置、未在途、不与他片贴接) func _variants_fringe(board: BoardData, pack: Array, candidates: Array[Vector2i], placed: Dictionary, pack_set: Dictionary) -> Array[Vector2i]: var fringe: Array[Vector2i] = [] for c in pack: for n in board.neighbors8(c): if candidates.has(n) and not placed.has(n) and not fringe.has(n) \ and not _adjacent_to_packs(board, placed, n, pack_set): fringe.append(n) return fringe # 候选格是否与其他已布片(own_pack 之外的 placed 格)8 邻贴接——片间隔离保证连通分量 = 单片 ≤ pack_max func _adjacent_to_packs(board: BoardData, placed: Dictionary, c: Vector2i, own_pack: Dictionary = {}) -> bool: for m in board.neighbors8(c): if placed.has(m) and not own_pack.has(m): return true return false