## VERTICAL SLICE - NOT FOR PRODUCTION ## 排雷规则引擎:棋盘产物+玩家意图批次 → 揭示/封印判定/轮末事件流,install 四不变式守门(垂直切片验证:挖掘逐行归因)。 ## 零随机、零表读取、零 UI——判定只依赖棋盘事实与动作序列。 ## 板态对外只经 current_board()/reveal_record() 等查询面,禁跨模块直读写内部字典;轮末双发射源之棋盘路径由 round_end_board 外发。 class_name MinesweeperRules extends RefCounted ## 揭示事实流:每格首次进入已开集恰发一次(已开格零事件)。 ## 载荷键集(四处发射位并集):cell: Vector2i/kind: "safe"|"mine"/digit: int(安全格邻雷数;危险格 -1)/ ## variant_ids: Array(地形变异身份,危险格空集)/channel: "initial"|"dig"|"chain"|"fx_reveal"/ ## resource: bool(格面资源兑付资格:initial=false,其余 true)/dug_under_seal: bool(首格带封印直挖标记—— ## initial 通道不发此键)。 ## 发送时点:initial=install_board 初始展开逐格;dig/chain=_do_dig 揭示闭包内(本格 AP 扣费在其后); ## fx_reveal=fx_reveal 闭包内(零 AP 消耗零相位要求)。 ## 消费方依赖:战斗结算 _on_cell_revealed 以 resource 分流缓冲(fx_reveal 由兑付点直入账,禁二次缓冲)、 ## 以 kind 驱动积点计数、以 cell 取奖励快照——禁改键名与语义;事件序 cell_revealed 批先于 seal_judged 批。 signal cell_revealed(evt: Dictionary) ## 封印判定事实流:轮末事务开始时的封印位逐格判定(行主序确定性枚举,被拔的不判)。 ## 载荷键集:cell: Vector2i(封印格)/hit: bool(该格是否为雷)。 ## 发送时点:停手路径 _do_stop 委托行动经济之前;棋盘路径揭示闭包收尾(phase 非 Active 时)—— ## 全路径统一先于 round_end 发射(事件序契约 seal_judged < round_end)。 ## 消费方依赖:战斗结算 _on_seal_judged 收批后统一开封印命中单——载荷缺键即错判,禁改键名与语义。 signal seal_judged(evt: Dictionary) signal round_end_board(payload: Dictionary) ## 双发射源之棋盘路径(排雷玩法 外发;stopped 由 行动经济 直发) var db: ConfigDb var action_economy: ActionEconomy # 秘宝常时参数注入面(编排器 begin_battle 按装配集 kind 分发注入: # dig_range_bonus=挖掘切比雪夫距离放宽〔远程镐 dig_range_bonus magnitude〕/seal_range_free= # 封印不限相邻〔信标网络〕;缺省 0/false=零装配零扰动,棋盘事实不可变契约不受影响—— # 两参数只放宽「动作可达性」判据,禁碰危险格/安全格分布) var dig_range_bonus := 0 var seal_range_free := false # 棋盘状态私有(E2 封装收口):对外只暴露 current_board()/reveal_record() 等查询面, # 禁止跨模块直读/直写原始字典。 var _board: BoardData = null var _revealed := {} # Vector2i → {"kind": "safe"/"mine", "digit": int} var _seals := {} # Vector2i → true(棋盘单态;跨轮保留随图存续) var _removed := {} # Vector2i → true(中立移除危险格集;随 install/retire 清空) var _hero := Vector2i.ZERO var _spawn: Dictionary[Vector2i, bool] = {} func _init(db_ref: ConfigDb, action_economy_ref: ActionEconomy) -> void: db = db_ref action_economy = action_economy_ref # ---- 板态 ---- # 板产物只读出口(E2 收口——战斗结算 快照 / 组合根 rebuild / 探针经此取板,禁再直读字段) func current_board() -> BoardData: return _board # 已开格记录查询面(cell → {kind, digit};未开 = {})——呈现层渲染消费面(board_view 徽章/四格态) func reveal_record(c: Vector2i) -> Dictionary: return _revealed.get(c, {}) func hero_pos() -> Vector2i: return _hero func is_revealed(c: Vector2i) -> bool: return _revealed.has(c) func has_seal(c: Vector2i) -> bool: return _seals.has(c) func revealed_cells() -> Array: return _revealed.keys() func safe_remaining() -> int: var n := 0 for c in _board.safe_cells(): if not _revealed.has(c): n += 1 return n func clear_map_cleared() -> bool: return safe_remaining() == 0 # ---- 中立移除(P1 甲:邻接数字=原始雷数冻结零重算;P2 甲:移除格呈现退色降档; # 移除集=未揭示、无封印、是雷——封印判定基线零回溯;危险格不入清图分母/连锁/C_dig 模拟,判定逻辑零改动) ---- func remove_mines_in(cells: Array) -> Array: var out: Array[Variant] = [] for c in cells: var cell: Vector2i = c if not _board.in_bounds(cell) or is_revealed(cell) or has_seal(cell) or not _board.is_mine(cell): continue if _removed.has(cell): continue _removed[cell] = true out.append(cell) return out func is_removed(c: Vector2i) -> bool: return _removed.has(c) # 未开格存在查询(aux 道具 usable 预判消费——全板扫描只读面) func has_unrevealed() -> bool: for y in _board.height: for x in _board.width: if not _revealed.has(Vector2i(x, y)): return true return false # ---- install(编排边界相位 = 行动经济 Idle/Waiting;Active 拒绝——防轮内换板)---- # during_round = 轮内刷新编排路径(战斗结算 唯一调用方):Active 相位合法—— # 玩家挖掘中立即换图,棋盘状态(revealed/seals)随 install 重置,初始展开事件照发(initial 通道) func install_board(b: BoardData, during_round := false) -> Dictionary: if action_economy.phase == action_economy.PHASE_ACTIVE and not during_round: return _outcome(false, "rejected", "phase", "") if action_economy.phase == action_economy.PHASE_IDLE and during_round: return _outcome(false, "rejected", "phase", "") if b == null: return _outcome(false, "rejected", "board_invalid", "out_of_catalog") var inv := _install_invariant_faults(b) if not inv.is_empty(): push_error("[排雷玩法] install 不变式违例: %s" % str(inv)) return _outcome(false, "rejected", "board_invalid", inv[0]) _board = b _revealed.clear() _seals.clear() _removed.clear() _spawn = {} for c in b.spawn_cells(): _spawn[c] = true # 初始展开:O₀ = R₀ 恰等(免费/计入清图/无格面资源/永不触雷);整图一次跨轮不重放 for c in _initial_expansion_order(b): _revealed[c] = {"kind": "safe", "digit": b.digit_of(c)} cell_revealed.emit({ "cell": c, "kind": "safe", "digit": b.digit_of(c), "variant_ids": [], "channel": "initial", "resource": false, }) _hero = b.spawn_center return _outcome(true, "executed", "", "") func seal_count() -> int: return _seals.size() # during_round = 轮内刷新编排路径:Active 相位合法——换图前退役旧板 func retire_board(during_round := false) -> Dictionary: if _board == null: return _outcome(true, "executed", "", "") # 幂等受理 if action_economy.phase == action_economy.PHASE_ACTIVE and not during_round: return _outcome(false, "rejected", "phase", "") _board = null _revealed.clear() _seals.clear() _removed.clear() return _outcome(true, "executed", "", "") # 四不变式(诊断类别封闭枚举):inv1 出生区雷 / inv2a 出生区变异 / inv2b R₀ 变异 / inv3 数字恒真 / inv4 R₀=S func _install_invariant_faults(b: BoardData) -> Array: var faults: Array[String] = [] var r0 := {} for c in b.compute_r0(): r0[c] = true for c in b.spawn_cells(): if b.is_mine(c): faults.append("inv1_mine_in_spawn") if b.variant_ids(c).size() > 0: faults.append("inv2a_variant_in_spawn") for c in r0.keys(): if b.variant_ids(c).size() > 0: faults.append("inv2b_variant_in_r0") for c in b.all_cells(): if b.is_safe(c): var d := 0 for n in b.neighbors8(c): if b.is_mine(n): d += 1 if b.digit_of(c) != d: faults.append("inv3_digit_invariant") if r0.size() == b.safe_cells().size(): faults.append("inv4_r0_equals_s") return faults # 初始展开规范序:触发格 BFS、8 邻行主序(≡ 棋盘生成 F1 R₀ 算子;禁外扩环、禁只开 3×3) func _initial_expansion_order(b: BoardData) -> Array: var order: Array[Variant] = b.spawn_cells().duplicate() var seen := {} for c in order: seen[c] = true var queue: Array[Variant] = order.duplicate() while not queue.is_empty(): var c: Vector2i = queue.pop_front() if b.is_safe(c) and b.digit_of(c) == 0: for n in b.neighbors8(c): if b.is_safe(n) and not seen.has(n): seen[n] = true order.append(n) queue.append(n) return order # ---- 批次结算(唯一提交入口;单意图批 = 平凡形 ≡ 便捷 API)---- func settle_batch(intents: Array) -> Array: var outcomes: Array[Dictionary] = [] if intents.is_empty(): return outcomes if action_economy.phase != action_economy.PHASE_ACTIVE or _board == null: for intent in intents: outcomes.append(_outcome(false, "rejected", "phase", "")) return outcomes # F4 语义序仲裁:C_dig(真模拟清图)→ C_stop → C_other;类内稳定序 = 到达序 var indexed: Array[Dictionary] = [] for i in intents.size(): indexed.append({"i": i, "intent": intents[i], "cls": _classify(intents[i])}) indexed.sort_custom(func(a, b): if a.cls != b.cls: return a.cls < b.cls return a.i < b.i) var round_over := false for item in indexed: if round_over: outcomes.append(_outcome(false, "voided", "", "")) continue var res := _execute(item.intent) outcomes.append(res) if action_economy.phase != action_economy.PHASE_ACTIVE: round_over = true return outcomes func _classify(intent: Dictionary) -> int: # 0 = C_dig(执行后清图——真模拟,禁计数器捷径);1 = C_stop;2 = C_other if intent.get("action", "") == "dig": var c: Vector2i = intent.get("cell", Vector2i(-9, -9)) if _board_face_dig(c) == "" and not _board.is_mine(c): # 模拟 Chain(O, c) 后清图判定——挖封印格同口径:本格封印视为已隐式退役(其余封印照挡) var sim_seals: Dictionary[Variant, Variant] = _seals.duplicate() sim_seals.erase(c) var chain := _chain_closure(c, sim_seals) # 与真实执行同口径——空封印集捷径会在封印挡连锁时误判 C_dig(排雷玩法:真模拟) var remaining := 0 for sc in _board.safe_cells(): if not _revealed.has(sc) and not chain.has(sc): remaining += 1 if remaining == 0: return 0 return 2 if intent.get("action", "") == "stop": return 1 return 2 func _execute(intent: Dictionary) -> Dictionary: match intent.get("action", ""): "dig": return _do_dig(intent.get("cell", Vector2i(-9, -9))) "seal_place": return _do_seal_place(intent.get("cell", Vector2i(-9, -9))) "move": return _do_move(intent.get("cell", Vector2i(-9, -9))) "stop": return _do_stop() push_warning("[排雷玩法] 目录外意图: %s" % str(intent)) return _outcome(false, "rejected", "board_face", "out_of_catalog") # ---- 挖掘 ---- func _do_dig(c: Vector2i) -> Dictionary: var sub := _board_face_dig(c) if sub != "": return _outcome(false, "rejected", "board_face", sub) # 挖封印格(封印不可逆):本格封印事务内隐式退役——链闭包按「本格无封印、其余封印照挡」 # 计算(封印格为 0 时连锁照常外扩);request_dig 被拒则封印原地保留(事务性),受理后该封印轮末不判 # (发射面基线 = 在场封印——标中/标空兑付让位于挖掘本体结局,四情况②④)。 var was_sealed := has_seal(c) var eff_seals: Dictionary[Variant, Variant] = _seals if was_sealed: eff_seals = _seals.duplicate() eff_seals.erase(c) # 真模拟连锁与清图(F2/F1——单一实现两名字) var chain := _chain_closure(c, eff_seals) var clears: bool = _cleared_after(chain) # F2 口径:chain_size = |O′ ∖ (O ∪ {c})|——闭包含已开集 O,只数新开格(不含首格) var chain_size := 0 for cc in chain.keys(): if cc != c and not _revealed.has(cc): chain_size += 1 # 资源面/相位 + cleared 仲裁在 行动经济 事务内原子完成 if not action_economy.request_dig(chain_size, clears): return _outcome(false, "rejected", "resource_face", "") # 事务内执行:首格 + 连锁新格揭示(BFS 规范序,零特判);事件 = O′ ∖ O if was_sealed: _seals.erase(c) var reveal_order: Array[Variant] = _chain_reveal_order(c, eff_seals) for rc in reveal_order: if _revealed.has(rc): continue # 闭包可穿过已开区——已开格零事件(cell_revealed × |O′ ∖ O|) var is_mine := _board.is_mine(rc) var channel := "dig" if rc == c else "chain" # 格面资源资格:chain/dig 恒 true——安全格与危险格一视同仁(危险格亦携带, # 负向兑付归 战斗结算 极性路由);initial(封锁格)恒 false。资源本体与极性不在本系统。 # dug_under_seal = 首格带封印直挖标记(战斗结算 情况④兑付改判消费;连锁格恒 false——连锁不越封印) if is_mine: _revealed[rc] = {"kind": "mine", "digit": -1} cell_revealed.emit({ "cell": rc, "kind": "mine", "digit": -1, "variant_ids": [], "channel": channel, "resource": true, "dug_under_seal": was_sealed and rc == c, }) else: _revealed[rc] = {"kind": "safe", "digit": _board.digit_of(rc)} cell_revealed.emit({ "cell": rc, "kind": "safe", "digit": _board.digit_of(rc), "variant_ids": _board.variant_ids(rc), "channel": channel, "resource": true, "dug_under_seal": was_sealed and rc == c, }) if action_economy.phase != action_economy.PHASE_ACTIVE: _emit_round_end_seals_and_payload() return _outcome(true, "executed", "", "") func _board_face_dig(c: Vector2i) -> String: if not _board.in_bounds(c): return "out_of_catalog" if not _within_dig_range(c): return "not_adjacent" if is_revealed(c): return "target_revealed" if _removed.has(c): return "target_removed" # 中立移除格不可交互(拒绝先于消耗,pattern 1 board_face 腿) # 封印不拦挖掘(封印不可逆):有封印格直接挖 = 对承诺的兑现,兑付改判归 战斗结算。 return "" # fx_reveal 效果通道揭示(订阅类「揭示 1 格」效果的棋盘面执行口: # 不经行动经济〔零 AP 消耗零相位要求〕、不要求邻接〔任意揭示〕、只受理安全格〔信息红线=只揭真格, # 危险格/已开/出界/移除格=拒绝零副作用〕。连锁与揭示事件与 _do_dig 同构(chain 通道复用),资源兑付归 # 战斗结算 _flush_pending_reveals 直接入账——channel=fx_reveal 供归因) func fx_reveal(c: Vector2i) -> bool: if _board == null or not _board.in_bounds(c) or is_revealed(c) or _board.is_mine(c) or _removed.has(c): return false var chain := _chain_closure(c, _seals) for rc in _chain_reveal_order(c, _seals): if _revealed.has(rc): continue _revealed[rc] = {"kind": "safe", "digit": _board.digit_of(rc)} cell_revealed.emit({ "cell": rc, "kind": "safe", "digit": _board.digit_of(rc), "variant_ids": _board.variant_ids(rc), "channel": "fx_reveal", "resource": true, "dug_under_seal": false, }) return true # 连锁闭包:μX.[O∪{c} ⊆ X ∧ ∀x∈X:(x∈S ∧ num(x)=0 ⇒ N₈(x)∩S∖G ⊆ X)]——连锁不越封印 func _chain_closure(start: Vector2i, seal_set: Dictionary) -> Dictionary: var chain := {start: true} var queue: Array[Variant] = [start] while not queue.is_empty(): var c: Vector2i = queue.pop_front() if _board.is_safe(c) and not seal_set.has(c) and _board.digit_of(c) == 0: for n in _board.neighbors8(c): if _board.is_safe(n) and not seal_set.has(n) and not chain.has(n): chain[n] = true queue.append(n) return chain # 揭示规范序:首格 + 触发格 BFS、8 邻行主序 func _chain_reveal_order(start: Vector2i, seal_set: Dictionary) -> Array: var order: Array[Variant] = [start] var seen := {start: true} var queue: Array[Variant] = [start] while not queue.is_empty(): var c: Vector2i = queue.pop_front() if _board.is_safe(c) and not seal_set.has(c) and _board.digit_of(c) == 0: for n in _board.neighbors8(c): if _board.is_safe(n) and not seal_set.has(n) and not seen.has(n): seen[n] = true order.append(n) queue.append(n) return order func _cleared_after(chain: Dictionary) -> bool: for c in _board.safe_cells(): if not _revealed.has(c) and not chain.has(c): return false return true # ---- 封印 ---- func _do_seal_place(c: Vector2i) -> Dictionary: var sub := "" if not _board.in_bounds(c): sub = "out_of_catalog" elif not (seal_range_free or _adjacent_to_hero(c)): sub = "not_adjacent" # 信标网络装配时豁免(封印不限相邻——封印动作仍按标准价计费,规则放宽仅可达性) elif is_revealed(c): sub = "target_revealed" elif has_seal(c): sub = "seal_present" elif _removed.has(c): sub = "target_removed" # 中立移除格不可交互 if sub != "": return _outcome(false, "rejected", "board_face", sub) if not action_economy.try_spend("seal_place"): return _outcome(false, "rejected", "resource_face", "") _seals[c] = true # 封印动作零格子事件外发(发射面封闭;反馈经调用返回值命令式驱动) if action_economy.phase != action_economy.PHASE_ACTIVE: # 封印动作花完末点 AP 同样终结轮次(行动经济 三出口——AP 用完与动作种类无关;缺漏使 round_end # 永不发出、战斗结算 的 Waiting 结算点永不到达 = 整局软锁) _emit_round_end_seals_and_payload() return _outcome(true, "executed", "", "") # ---- 移动(瞬移;免费不经账本;BFS 限已探明区含已引爆危险格)---- func _do_move(c: Vector2i) -> Dictionary: var sub := "" if not _board.in_bounds(c): sub = "out_of_catalog" elif not is_revealed(c): sub = "target_concealed" elif c != _hero and compute_move_path(c).is_empty(): sub = "no_path" if sub != "": return _outcome(false, "rejected", "board_face", sub) _hero = c # 目标 = 当前格定义为零移动(幂等受理) return _outcome(true, "executed", "", "") func compute_move_path(target: Vector2i) -> Array: if _board == null or not is_revealed(target): return [] if target == _hero: return [] # BFS(8 邻行主序)——路径 = 合法性载体 + HUD框架预览数据源 var prev := {_hero: _hero} var queue: Array[Variant] = [_hero] while not queue.is_empty(): var c: Vector2i = queue.pop_front() if c == target: break for n in _board.neighbors8(c): if is_revealed(n) and not prev.has(n): prev[n] = c queue.append(n) if not prev.has(target): return [] var path: Array[Vector2i] = [] var cur := target while cur != _hero: path.push_front(cur) cur = prev[cur] return path # ---- 停手(经批次入口委托 行动经济;先发封印判定批再委托——事件序契约 seal_judged < round_end 全路径统一)---- func _do_stop() -> Dictionary: _emit_seals_batch() # 基线 = 事务开始封印位(停手不改封印位) if not action_economy.request_stop(): return _outcome(false, "rejected", "phase", "") # 行动经济 直发 stopped 路径 round_end;排雷玩法 禁双发(双发射源纪律) return _outcome(true, "executed", "", "") # ---- 轮末封印判定 + 棋盘路径 round_end 外发 ---- func _emit_seals_batch() -> void: # 判定基线 = 轮次结束事务开始时封印位;行主序枚举(确定性——排序键单点 BoardData.row_major_key); # 被拔的不判;诚实重复上报(去重归 战斗结算) var cells: Array[Variant] = _seals.keys() cells.sort_custom(func(a, b): return BoardData.row_major_key(a) < BoardData.row_major_key(b)) for c in cells: seal_judged.emit({"cell": c, "hit": _board.is_mine(c)}) func _emit_round_end_seals_and_payload() -> void: _emit_seals_batch() var payload: Dictionary[Variant, Variant] = action_economy.last_round_end # 逐字段取自 行动经济(读取权 = 接口契约) round_end_board.emit({"path_tag": payload.get("path_tag", ""), "continuation_hint": payload.get("continuation_hint", "")}) func _adjacent_to_hero(c: Vector2i) -> bool: var d := Vector2i(absi(c.x - _hero.x), absi(c.y - _hero.y)) return maxi(d.x, d.y) == 1 # 挖掘可达判据(远程镐 dig_range_bonus 放宽:切比雪夫距离 ≤ 1+加值, # 加值 0=恰相邻;开格仍走既有格规则=不挖开不相邻事实之外 anything) func _within_dig_range(c: Vector2i) -> bool: var d := Vector2i(absi(c.x - _hero.x), absi(c.y - _hero.y)) return maxi(d.x, d.y) <= 1 + dig_range_bonus # 本板危险格总数(巢穴预告「装板后揭示本板危险格数量」取数口; # 只读投影 BoardData.mines 规模,零分布泄露——数量非位置) func mine_total() -> int: return _board.mines.size() if _board != null else 0 func _outcome(accepted: bool, outcome: String, reason: String, sub: String) -> Dictionary: return {"accepted": accepted, "outcome": outcome, "reject_reason": reason, "sub_reason": sub}