211 lines
9.6 KiB
GDScript
211 lines
9.6 KiB
GDScript
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||
# Date: 2026-09-13(O4/W-3 收口——自动玩家策略四份复制单一实现)
|
||
# 自动玩家策略集(headless 冒烟/扫描/转储共用的理性玩家近似):
|
||
# choose_dig = 挖最近可挖格(先移到邻接已开格)。
|
||
# 2026-09-15 格面资源批:window_selection(道具窗口确定性选择)随道具窗口删除下线。
|
||
# 读取面:战斗结算.presentation_snapshot() / 排雷玩法 查询面(E1/E2 收口后的成文 API——禁直读核心状态)。
|
||
# 等价性证明:smoke_all 同种子回放断言(步骤流逐行相同)在重构后保持全绿。
|
||
extends RefCounted
|
||
|
||
|
||
# 行主序枚举未开格,找与已开集 8 邻接者;小人先移到该邻接已开格(BFS 可达)。无可挖 → {}
|
||
static func choose_dig(minesweeper) -> Dictionary:
|
||
var revealed: Array = minesweeper.revealed_cells()
|
||
var revealed_set := {}
|
||
for c in revealed:
|
||
revealed_set[c] = true
|
||
var board = minesweeper.current_board()
|
||
for y in board.height:
|
||
for x in board.width:
|
||
var c := Vector2i(x, y)
|
||
if minesweeper.is_revealed(c) or minesweeper.has_flag(c):
|
||
continue
|
||
for n in board.neighbors8(c):
|
||
if revealed_set.has(n):
|
||
var path: Array = minesweeper.compute_move_path(n)
|
||
if not path.is_empty() or n == minesweeper.hero_pos():
|
||
return {"dig_cell": c, "hero_cell": n}
|
||
return {}
|
||
|
||
|
||
# ==================== 普通策略档 v2·基础定式画像(2026-09-19 用户拍板——替代旧"无推理最近格"口径) ====================
|
||
# 扫雷推理分级(经典定式文献口径):新手级 = 单数字满足规则——「数字 = 邻雷未开数 ⇒ 全是雷(满足即标)」
|
||
# 「数字 = 已标旗数 ⇒ 余邻全安全(满足即开)」,教学一眼会、正常玩家两局即会的最低门槛判断;
|
||
# 进阶级 = 联动定式(1-1 / 1-2 / 1-2-1 / 1-2-2-1 / 子集缩减)与概率分析——本切片归完美档 subset 推理承载,
|
||
# 不入普通画像(新手不可靠掌握)。本档语义 = 只做新手级推理 + 无处可推时按既有口径盲挖边界格(新手冲动)+
|
||
# 不主动刷新(新手无刷新策略意识)。测量定位:教会基础规则后的真实玩家下限近似(旧"乱点/最近格"口径测的是
|
||
# 无认知玩家下限,对通关率带判读失真——2026-09-18/19 普通画像 0%/5% 越带下界与真人方向性结论矛盾)。
|
||
|
||
# 普通画像单步决策入口(每动作一查——挖掘揭示新数字后重推理):move/dig/flag/stop 四值(无 refresh)。
|
||
static func novice_action(minesweeper) -> Dictionary:
|
||
var ded := _deduce_single(minesweeper)
|
||
for c in ded["mines"]:
|
||
if not minesweeper.is_revealed(c) and not minesweeper.has_flag(c):
|
||
return _plan_on(minesweeper, c, "flag")
|
||
for c in ded["safe"]:
|
||
if not minesweeper.is_revealed(c) and not minesweeper.has_flag(c):
|
||
return _plan_on(minesweeper, c, "dig")
|
||
var plan := choose_dig(minesweeper)
|
||
if plan.is_empty():
|
||
return {"kind": "stop"}
|
||
return _plan_on(minesweeper, plan.dig_cell, "dig")
|
||
|
||
|
||
# 新手级推理不动点:仅 single 规则(约束余雷=0⇒未知全安全 / 余雷=未知数⇒全雷),无 subset 联动——
|
||
# 满足规则连锁(标一雷使邻数字余雷清零)由 while-changed 不动点承载。
|
||
static func _deduce_single(minesweeper) -> Dictionary:
|
||
var board = minesweeper.current_board()
|
||
var safe := {}
|
||
var mines := {}
|
||
var changed := true
|
||
while changed:
|
||
changed = false
|
||
for con in _constraints(minesweeper, board, mines):
|
||
if int(con["mines"]) == 0:
|
||
for c in con["unknown"].keys():
|
||
if not safe.has(c):
|
||
safe[c] = true
|
||
changed = true
|
||
elif int(con["mines"]) == (con["unknown"] as Dictionary).size():
|
||
for c in con["unknown"].keys():
|
||
if not mines.has(c):
|
||
mines[c] = true
|
||
changed = true
|
||
return {"safe": _row_major(safe), "mines": _row_major(mines)}
|
||
|
||
|
||
# ==================== 完美策略档(2026-09-14 用户拍板:OQ7 技能轴上界测量仪——只作测量,非玩法数值) ====================
|
||
# 策略语义:零盲挖(只挖已证安全格 ⇒ 构造保证零踩雷)· 已证雷格全标旗(旗先于挖——旗 = 纯收益
|
||
# 〔轮末 weaken + 魔法 首次充能〕且雷上旗不阻连锁)· 卡死且刷新资源可用 → 轮内刷新
|
||
# (经 settlement.player_request_refresh 核心编排)· 否则停手(残 AP 1:1 转存 RAP——下轮刷新资源)。
|
||
# 推理规则集 = 棋盘生成 F1 measure_inferability 同族(single + subset——solver_rules=single_subset 冻结域);
|
||
# 口径差异 = unknown 集不含 棋盘生成 生成期 safe_set oracle(真实玩家约束的未知邻格含雷格)——比生成期闸
|
||
# 保守,卡死率略高属口径诚实。输入只经 排雷玩法 查询面(reveal_record/is_revealed/has_flag/current_board
|
||
# 的邻居与可达面),禁读棋盘雷区真值(测推理上界,非全知作弊)。
|
||
# 待拍板(未实现):非卡死主动刷新、囤刀倾泻 vs 即用即耗、完美板目标加权。
|
||
|
||
# 单步决策入口(每动作一查——挖掘揭示新数字后重推理):move/dig/flag/refresh/stop 五值。
|
||
static func perfect_action(minesweeper, settlement) -> Dictionary:
|
||
var ded := _deduce(minesweeper)
|
||
for c in ded["mines"]:
|
||
if not minesweeper.is_revealed(c) and not minesweeper.has_flag(c):
|
||
return _plan_on(minesweeper, c, "flag")
|
||
for c in ded["safe"]:
|
||
if not minesweeper.is_revealed(c) and not minesweeper.has_flag(c):
|
||
return _plan_on(minesweeper, c, "dig")
|
||
if settlement != null and bool(settlement.can_refresh()):
|
||
return {"kind": "refresh"}
|
||
return {"kind": "stop"}
|
||
|
||
|
||
# 目标格需英雄 8 邻——不可达先移到目标的已开 8 邻格(行主序首个可达者;同 choose_dig 寻路口径)
|
||
static func _plan_on(minesweeper, c: Vector2i, kind: String) -> Dictionary:
|
||
var d := Vector2i(absi(c.x - minesweeper.hero_pos().x), absi(c.y - minesweeper.hero_pos().y))
|
||
if maxi(d.x, d.y) == 1:
|
||
return {"kind": kind, "cell": c}
|
||
var board = minesweeper.current_board()
|
||
for n in board.neighbors8(c):
|
||
if minesweeper.is_revealed(n) and (n == minesweeper.hero_pos() or not minesweeper.compute_move_path(n).is_empty()):
|
||
return {"kind": "move", "cell": n}
|
||
return {"kind": "stop"} # 不可达(理论不可达——已证格必邻接已开区);防御性停手防死循环
|
||
|
||
|
||
# 推理不动点:single(约束余雷=0⇒未知全安全 / 余雷=未知数⇒全雷)+ subset(U_a ⊂ U_b ⇒ 差集约束)。
|
||
# 约束只依赖 revealed/flags/mines 推断集——mines 集增长才需重建约束(safe 标记不改正约约束)。
|
||
static func _deduce(minesweeper) -> Dictionary:
|
||
var board = minesweeper.current_board()
|
||
var safe := {}
|
||
var mines := {}
|
||
var changed := true
|
||
while changed:
|
||
changed = false
|
||
var cons := _constraints(minesweeper, board, mines)
|
||
for con in cons:
|
||
if int(con["mines"]) == 0:
|
||
for c in con["unknown"].keys():
|
||
if not safe.has(c):
|
||
safe[c] = true
|
||
changed = true
|
||
elif int(con["mines"]) == (con["unknown"] as Dictionary).size():
|
||
for c in con["unknown"].keys():
|
||
if not mines.has(c):
|
||
mines[c] = true
|
||
changed = true
|
||
var cons2 := _constraints(minesweeper, board, mines)
|
||
for i in cons2.size():
|
||
for j in range(i + 1, cons2.size()):
|
||
var der := _subset_derived(cons2[i], cons2[j])
|
||
if der.is_empty():
|
||
continue
|
||
if int(der["mines"]) == 0:
|
||
for c in der["unknown"].keys():
|
||
if not safe.has(c):
|
||
safe[c] = true
|
||
changed = true
|
||
elif int(der["mines"]) == (der["unknown"] as Dictionary).size():
|
||
for c in der["unknown"].keys():
|
||
if not mines.has(c):
|
||
mines[c] = true
|
||
changed = true
|
||
return {"safe": _row_major(safe), "mines": _row_major(mines)}
|
||
|
||
|
||
# 约束集 = 已开数字格:mines_left = digit − 已知雷旗(旗位 + 已证雷格),unknown = 未开无旗 8 邻。
|
||
# 已爆雷格(kind=mine)不带数字约束——其贡献已含在邻格 digit 内(数字恒真不变式 inv3)。
|
||
static func _constraints(minesweeper, board, mines: Dictionary) -> Array:
|
||
var out: Array = []
|
||
for c in minesweeper.revealed_cells():
|
||
var rec: Dictionary = minesweeper.reveal_record(c)
|
||
if String(rec.get("kind", "")) != "safe":
|
||
continue
|
||
var d := int(rec.get("digit", -1))
|
||
if d < 0:
|
||
continue
|
||
var unknown := {}
|
||
var known := 0
|
||
for n in board.neighbors8(c):
|
||
if minesweeper.has_flag(n):
|
||
known += 1
|
||
elif not minesweeper.is_revealed(n):
|
||
if mines.has(n):
|
||
known += 1
|
||
else:
|
||
unknown[n] = true
|
||
var d_eff := d - known
|
||
if unknown.is_empty() or d_eff < 0:
|
||
continue # 零未知 = 无信息;负余雷 = 矛盾态(非法板/误旗)——不产约束
|
||
out.append({"mines": d_eff, "unknown": unknown})
|
||
return out
|
||
|
||
|
||
# pair 差集约束:U_a ⊂ U_b ⇒ {unknown = U_b∖U_a, mines = mines_b−mines_a}(互不包含/相等集 → 空)
|
||
static func _subset_derived(ca: Dictionary, cb: Dictionary) -> Dictionary:
|
||
var res := _subset_one_way(ca, cb)
|
||
if not res.is_empty():
|
||
return res
|
||
return _subset_one_way(cb, ca)
|
||
|
||
|
||
static func _subset_one_way(ca: Dictionary, cb: Dictionary) -> Dictionary:
|
||
var ua: Dictionary = ca["unknown"]
|
||
var ub: Dictionary = cb["unknown"]
|
||
if ua.is_empty() or ub.is_empty() or ua.size() >= ub.size():
|
||
return {}
|
||
for k in ua.keys():
|
||
if not ub.has(k):
|
||
return {}
|
||
var diff := {}
|
||
for k in ub.keys():
|
||
if not ua.has(k):
|
||
diff[k] = true
|
||
var dm := int(cb["mines"]) - int(ca["mines"])
|
||
if dm < 0:
|
||
return {} # 矛盾态防御(完美档无误旗——理论不可达)
|
||
return {"unknown": diff, "mines": dm}
|
||
|
||
|
||
static func _row_major(cells: Dictionary) -> Array:
|
||
var out: Array = cells.keys()
|
||
out.sort_custom(func(a, b): return a.y * 1000 + a.x < b.y * 1000 + b.x)
|
||
return out
|