Files
gplan-2/game/core/minesweeper_rules.gd
T
DKChan 32fdf957fd refactor(代码术语): 卡① flag→seal 全族换代+game/ 禁用词清扫(迭代 31 卡①)
标识符面:flag_*→seal_ 族(含 Godot 信号 flag_judged→seal_judged 与连接/回调同名、
FACTS 两键 flags_count→seals_count/dug_under_flag→dug_under_seal 全链、
.tres 键名与 schema .gd 同批同步);显式豁免零改动=size_flags_*(Godot 布局属性)+_fatal_flag。
中文面:金币→GP(金币袋→GP 袋)/插旗→封印/雷格→危险格/惩罚格→危险格/旗→封印·封印印记族,
game/ 内 *.gd/.tres/.md/.json 四类禁用词与 flag 双双零命中(V1/V4)。
守卫字面量不动(被检查的禁字不是使用):smoke_all V21 零「雷/旗」检查面、
ui_font_asset_test 字形集攻/雷/旗/封 保持原断言逻辑。
代拍板:板面单字槽 glyph「旗」→「封」(禁字纪律+单字位宽,消费面四处同名随动)。
素材换代(2026-10-18 用户裁案甲):assets/art/board/flag_seal.png→seal_mark.png(素材不入 git,
盘上改名+导入重刷),代码两处 res:// 字符串+COL_FLAG/CURSOR_FLAG 常量同步,
闸件四处随批(run_all_gates.sh/render_gate.sh/golden_baseline_refresh.sh/path_check.py)。
anchor.json 键名手改,重锚与改前改后 diff 随本卡收口(同裁案甲)。

基线读数(改前,主区 ed2dfaf6):冒烟 1151 检查 0 失败/单元 34 套全绿/探针 --click
存量红仅 screen_flow_probe(user:// 断点污染在册已知项)。
改后同批读数(本 worktree):冒烟 1151 检查 0 失败/探针 --click 同一存量红、其余全绿。
2026-10-19 02:52:38 +08:00

505 lines
20 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## VERTICAL SLICE - NOT FOR PRODUCTION
## 排雷规则引擎:棋盘产物+玩家意图批次 → 揭示/封印判定/轮末事件流,install 四不变式守门(垂直切片验证:挖掘逐行归因)。
## 零随机、零表读取、零 UI——判定只依赖棋盘事实与动作序列。
## 板态对外只经 current_board()/reveal_record() 等查询面,禁跨模块直读写内部字典;轮末双发射源之棋盘路径由 round_end_board 外发。
class_name MinesweeperRules
extends RefCounted
signal cell_revealed(evt: Dictionary)
signal seal_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〕/seal_range_free=
# 封印不限相邻〔信标网络〕;缺省 0/false=零装配零扰动,棋盘事实不可变契约不受影响——
# 两参数只放宽「动作可达性」判据,禁碰危险格/安全格分布)
var dig_range_bonus := 0
var seal_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 _seals := {} # 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_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
# ---- 中立移除(迭代 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_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 = 轮内刷新编排路径(2026-09-12 方案 A,战斗结算 唯一调用方):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 = 轮内刷新编排路径(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()
_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)
# 挖封印格(封印不可逆批 2026-09-19):本格封印事务内隐式退役——链闭包按「本格无封印、其余封印照挡」
# 计算(封印格为 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"
# 格面资源资格(2026-09-15 格面资源批):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" # 迭代 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, _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" # 迭代 13 T8/D6——中立移除格不可交互
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 结算点永不到达 = 整局软锁。playtest 缺陷②,行为探针复现)
_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,
# 2026-10-02 修复批);被拔的不判;诚实重复上报(去重归 战斗结算)
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
# 挖掘可达判据(迭代 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}