560 lines
22 KiB
GDScript
560 lines
22 KiB
GDScript
## 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
|
||
|
||
|
||
# 伤害型变异过滤(terrain_kind fact 取数面——variation 表 effect=flat_damage 判据;
|
||
# 🔴 R4 甲永不对冲边界=仅限伤害型,信息型(hide_number 族)永不出现在本集。自编排器归位)
|
||
func harmful_variants(vids: Array) -> Array:
|
||
var out: Array[Variant] = []
|
||
for vid_v in vids:
|
||
var vid := String(vid_v)
|
||
var vrow: Dictionary[Variant, Variant] = db.combat.variation.row_by_id(vid)
|
||
if String(vrow.get("effect", "")) == "flat_damage":
|
||
out.append(vid)
|
||
return out
|
||
|
||
|
||
# 邻接伤害型变异收集(terrain_kind fact 取数面——seal_hit 发射点用:封印命中相邻八格的伤害型变异集;
|
||
# 🔴 R4 甲永不对冲边界=仅限伤害型,信息型〔hide_number 族〕不入集。自编排器归位)
|
||
func harmful_adjacent(cell: Vector2i) -> Array:
|
||
var out: Array[Variant] = []
|
||
var board: BoardData = current_board()
|
||
if board == null:
|
||
return out
|
||
for n in board.neighbors8(cell):
|
||
out.append_array(harmful_variants(board.variant_ids(n)))
|
||
return out
|
||
|
||
|
||
# 已开格记录查询面(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)
|
||
_reveal_chain(c, reveal_order, was_sealed)
|
||
if action_economy.phase != action_economy.PHASE_ACTIVE:
|
||
_emit_round_end_seals_and_payload()
|
||
return _outcome(true, "executed", "", "")
|
||
|
||
|
||
## 连锁揭示域:首格+连锁新格揭示(BFS 规范序——cell_revealed 发射序即链序,整体随段搬迁)。
|
||
## 格面资源资格:chain/dig 恒 true——安全格与危险格一视同仁(危险格亦携带,
|
||
## 负向兑付归 战斗结算 极性路由);initial(封锁格)恒 false。资源本体与极性不在本系统。
|
||
## dug_under_seal = 首格带封印直挖标记(战斗结算 情况④兑付改判消费;连锁格恒 false——连锁不越封印)
|
||
func _reveal_chain(c: Vector2i, reveal_order: Array, was_sealed: bool) -> void:
|
||
for rc in reveal_order:
|
||
if _revealed.has(rc):
|
||
continue # 闭包可穿过已开区——已开格零事件(cell_revealed × |O′ ∖ O|)
|
||
var channel := "dig" if rc == c else "chain"
|
||
if _board.is_mine(rc):
|
||
_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,
|
||
})
|
||
|
||
|
||
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 通道复用),资源兑付归
|
||
# battle_fx_host 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
|
||
|
||
|
||
## 挖一格→真实翻开集只读预览(sim 信息面出口——与 _classify 同口径:本格封印视为已隐式退役、
|
||
## 其余封印照挡;复用 _chain_closure 闭包;零状态写入、零事件发射)
|
||
func preview_reveal(c: Vector2i) -> Array[Vector2i]:
|
||
var sim_seals := _seals.duplicate()
|
||
sim_seals.erase(c)
|
||
var closure := _chain_closure(c, sim_seals)
|
||
var out: Array[Vector2i] = []
|
||
for cc in closure.keys():
|
||
if cc != c and not _revealed.has(cc):
|
||
out.append(cc) # F2 口径:新开格集——不含首格与已开格
|
||
return out
|
||
|
||
|
||
# 连锁闭包:μ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() # 逐字段取自 行动经济(读取权 = 接口契约;属性→方法 K5)
|
||
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}
|