- CLAUDE.md 硬性红线新增「文件头零工作日志」;code-conventions.md 新增 §1.4.1(含反面案例) - 全仓 .gd 文件头注清偿:删除全部带日期条目、批次叙事段与迁移退役叙述,保留并改写为 现在时的职责、接口契约、GDD 锚内容(main.gd 头注 42→31 行、save_manager.gd 49→45 行) - 纯注释改动零行为变更,按用户令免测试免冒烟
509 lines
20 KiB
GDScript
509 lines
20 KiB
GDScript
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||
# Date: 2026-09-10
|
||
# 排雷玩法规则引擎:零随机/零表读取/零 UI——输入 = 棋盘生成 产物 + 意图序列,输出 = 三事件流。
|
||
# 核心契约:install 四不变式 · 初始展开 = R₀ 恰等 · 连锁不越旗 · 批次语义序仲裁(cleared > stop > 其余)。
|
||
# 旗语义:旗 = 一次性承诺——挖旗格放行(本格旗事务内
|
||
# 隐式退役、轮末不判);无拔旗动作(flag_pull 已出目录)。
|
||
# 实现 GDD:design/gdd/systems/排雷循环/排雷玩法.md(系统 ID A2·排雷玩法——registry 追溯锚,命名规范见 CLAUDE.md)
|
||
class_name MinesweeperRules
|
||
extends RefCounted
|
||
|
||
signal cell_revealed(evt: Dictionary)
|
||
signal flag_judged(evt: Dictionary)
|
||
signal round_end_board(payload: Dictionary) # 双发射源之棋盘路径(排雷玩法 外发;stopped 由 行动经济 直发)
|
||
|
||
var db: ConfigDb
|
||
var action_economy: ActionEconomy
|
||
# 秘宝常时参数注入面(迭代 22 秘宝批 T3——编排器 begin_battle 按装配集 kind 分发注入:
|
||
# dig_range_bonus=挖掘切比雪夫距离放宽〔远程镐 dig_range_bonus magnitude〕/flag_range_free=
|
||
# 插旗不限相邻〔信标网络〕;缺省 0/false=零装配零扰动,棋盘事实不可变契约不受影响——
|
||
# 两参数只放宽「动作可达性」判据,禁碰危险格/安全格分布)
|
||
var dig_range_bonus := 0
|
||
var flag_range_free := false
|
||
# 棋盘状态私有(E2 封装收口 2026-09-13):对外只暴露 current_board()/reveal_record() 等查询面,
|
||
# 禁止跨模块直读/直写原始字典——先例教训 = board_view 绕过查询面直读 revealed。
|
||
var _board: BoardData = null
|
||
var _revealed := {} # Vector2i → {"kind": "safe"/"mine", "digit": int}
|
||
var _flags := {} # Vector2i → true(棋盘单态;跨轮保留随图存续)
|
||
var _removed := {} # Vector2i → true(迭代 13 T8/D6——中立移除雷格集;随 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_flag(c: Vector2i) -> bool:
|
||
return _flags.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
|
||
|
||
|
||
# ---- 中立移除(迭代 13 T8/D6——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_flag(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 = 轮内刷新编排路径(2026-09-12 方案 A,战斗结算 唯一调用方):Active 相位合法——
|
||
# 玩家挖掘中立即换图,棋盘状态(revealed/flags)随 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()
|
||
_flags.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 flag_count() -> int:
|
||
return _flags.size()
|
||
|
||
|
||
# during_round = 轮内刷新编排路径(2026-09-12 方案 A):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()
|
||
_flags.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_flags: Dictionary[Variant, Variant] = _flags.duplicate()
|
||
sim_flags.erase(c)
|
||
var chain := _chain_closure(c, sim_flags) # 与真实执行同口径——空旗集捷径会在旗挡连锁时误判 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)))
|
||
"flag_place":
|
||
return _do_flag_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)
|
||
# 挖旗格(插旗不可逆批 2026-09-19):本格旗事务内隐式退役——链闭包按「本格无旗、其余旗照挡」
|
||
# 计算(旗格为 0 时连锁照常外扩);request_dig 被拒则旗原地保留(事务性),受理后该旗轮末不判
|
||
# (发射面基线 = 在场旗——标中/标空兑付让位于挖掘本体结局,四情况②④)。
|
||
var was_flagged := has_flag(c)
|
||
var eff_flags: Dictionary[Variant, Variant] = _flags
|
||
if was_flagged:
|
||
eff_flags = _flags.duplicate()
|
||
eff_flags.erase(c)
|
||
# 真模拟连锁与清图(F2/F1——单一实现两名字)
|
||
var chain := _chain_closure(c, eff_flags)
|
||
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_flagged:
|
||
_flags.erase(c)
|
||
var reveal_order: Array[Variant] = _chain_reveal_order(c, eff_flags)
|
||
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"
|
||
# 格面资源资格(2026-09-15 格面资源批):chain/dig 恒 true——安全格与雷格一视同仁(雷格亦携带,
|
||
# 负向兑付归 战斗结算 极性路由);initial(封锁格)恒 false。资源本体与极性不在本系统。
|
||
# dug_under_flag = 首格带旗直挖标记(战斗结算 情况④兑付改判消费;连锁格恒 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_flag": was_flagged 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_flag": was_flagged and rc == c,
|
||
})
|
||
if action_economy.phase != action_economy.PHASE_ACTIVE:
|
||
_emit_round_end_flags_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" # 迭代 13 T8/D6——中立移除格不可交互(拒绝先于消耗,pattern 1 board_face 腿)
|
||
# 旗不拦挖掘(插旗不可逆批 2026-09-19):有旗格直接挖 = 对承诺的兑现,兑付改判归 战斗结算。
|
||
return ""
|
||
|
||
|
||
# fx_reveal 效果通道揭示(迭代 20 专长实装批 T1——订阅类「揭示 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, _flags)
|
||
for rc in _chain_reveal_order(c, _flags):
|
||
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_flag": false,
|
||
})
|
||
return true
|
||
|
||
|
||
# 连锁闭包:μX.[O∪{c} ⊆ X ∧ ∀x∈X:(x∈S ∧ num(x)=0 ⇒ N₈(x)∩S∖G ⊆ X)]——连锁不越旗
|
||
func _chain_closure(start: Vector2i, flag_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 flag_set.has(c) and _board.digit_of(c) == 0:
|
||
for n in _board.neighbors8(c):
|
||
if _board.is_safe(n) and not flag_set.has(n) and not chain.has(n):
|
||
chain[n] = true
|
||
queue.append(n)
|
||
return chain
|
||
|
||
|
||
# 揭示规范序:首格 + 触发格 BFS、8 邻行主序
|
||
func _chain_reveal_order(start: Vector2i, flag_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 flag_set.has(c) and _board.digit_of(c) == 0:
|
||
for n in _board.neighbors8(c):
|
||
if _board.is_safe(n) and not flag_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_flag_place(c: Vector2i) -> Dictionary:
|
||
var sub := ""
|
||
if not _board.in_bounds(c):
|
||
sub = "out_of_catalog"
|
||
elif not (flag_range_free or _adjacent_to_hero(c)):
|
||
sub = "not_adjacent" # 信标网络装配时豁免(插旗不限相邻——旗动作仍按标准价计费,规则放宽仅可达性)
|
||
elif is_revealed(c):
|
||
sub = "target_revealed"
|
||
elif has_flag(c):
|
||
sub = "flag_present"
|
||
elif _removed.has(c):
|
||
sub = "target_removed" # 迭代 13 T8/D6——中立移除格不可交互
|
||
if sub != "":
|
||
return _outcome(false, "rejected", "board_face", sub)
|
||
if not action_economy.try_spend("flag_place"):
|
||
return _outcome(false, "rejected", "resource_face", "")
|
||
_flags[c] = true # 旗动作零格子事件外发(发射面封闭;反馈经调用返回值命令式驱动)
|
||
if action_economy.phase != action_economy.PHASE_ACTIVE:
|
||
# 旗动作花完末点 AP 同样终结轮次(行动经济 三出口——AP 用完与动作种类无关;缺漏使 round_end
|
||
# 永不发出、战斗结算 的 Waiting 结算点永不到达 = 整局软锁。playtest 缺陷②,行为探针复现)
|
||
_emit_round_end_flags_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
|
||
|
||
|
||
# ---- 停手(经批次入口委托 行动经济;先发旗判定批再委托——事件序契约 flag_judged < round_end 全路径统一)----
|
||
|
||
func _do_stop() -> Dictionary:
|
||
_emit_flags_batch() # 基线 = 事务开始旗位(停手不改旗位)
|
||
if not action_economy.request_stop():
|
||
return _outcome(false, "rejected", "phase", "")
|
||
# 行动经济 直发 stopped 路径 round_end;排雷玩法 禁双发(双发射源纪律)
|
||
return _outcome(true, "executed", "", "")
|
||
|
||
|
||
# ---- 轮末旗判定 + 棋盘路径 round_end 外发 ----
|
||
|
||
func _emit_flags_batch() -> void:
|
||
# 判定基线 = 轮次结束事务开始时旗位;行主序枚举(确定性——排序键单点 BoardData.row_major_key,
|
||
# 2026-10-02 修复批);被拔的不判;诚实重复上报(去重归 战斗结算)
|
||
var cells: Array[Variant] = _flags.keys()
|
||
cells.sort_custom(func(a, b): return BoardData.row_major_key(a) < BoardData.row_major_key(b))
|
||
for c in cells:
|
||
flag_judged.emit({"cell": c, "hit": _board.is_mine(c)})
|
||
|
||
|
||
func _emit_round_end_flags_and_payload() -> void:
|
||
_emit_flags_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
|
||
|
||
|
||
# 挖掘可达判据(迭代 22 秘宝批 T3——远程镐 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
|
||
|
||
|
||
# 本板危险格总数(迭代 22 秘宝批 T3——巢穴预告「装板后揭示本板危险格数量」取数口;
|
||
# 只读投影 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}
|