252 lines
11 KiB
GDScript
252 lines
11 KiB
GDScript
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||
# 自动玩家策略集(headless 冒烟/扫描/转储共用的理性玩家近似):
|
||
# choose_dig = 挖最近可挖格(先移到邻接已开格)。
|
||
# 无 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_seal(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·基础定式画像 ====================
|
||
# 扫雷推理分级(经典定式文献口径):新手级 = 单数字满足规则——「数字 = 邻雷未开数 ⇒ 全是雷(满足即标)」
|
||
# 「数字 = 已封印数 ⇒ 余邻全安全(满足即开)」,教学一眼会、正常玩家两局即会的最低门槛判断;
|
||
# 进阶级 = 联动定式(1-1 / 1-2 / 1-2-1 / 1-2-2-1 / 子集缩减)与概率分析——本切片归完美档 subset 推理承载,
|
||
# 不入普通画像(新手不可靠掌握)。本档语义 = 只做新手级推理 + 无处可推时按既有口径盲挖边界格(新手冲动)+
|
||
# 不主动刷新(新手无刷新策略意识)。测量定位:教会基础规则后的真实玩家下限近似
|
||
# (乱点/最近格口径测的是无认知玩家下限,对关卡完成率带判读失真,不作普通画像)。
|
||
|
||
# 普通画像单步决策入口(每动作一查——挖掘揭示新数字后重推理):move/dig/seal/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_seal(c):
|
||
return _plan_on(minesweeper, c, "seal")
|
||
for c in ded["safe"]:
|
||
if not minesweeper.is_revealed(c) and not minesweeper.has_seal(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)}
|
||
|
||
|
||
# ==================== 完美策略档(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_seal/current_board
|
||
# 的邻居与可达面),禁读棋盘雷区真值(测推理上界,非全知作弊)。
|
||
# 待拍板(未实现):非卡死主动刷新、囤刀倾泻 vs 即用即耗、完美板目标加权。
|
||
|
||
# 单步决策入口(每动作一查——挖掘揭示新数字后重推理):move/dig/seal/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_seal(c):
|
||
return _plan_on(minesweeper, c, "seal")
|
||
for c in ded["safe"]:
|
||
if not minesweeper.is_revealed(c) and not minesweeper.has_seal(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/seals/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_seal(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
|
||
|
||
|
||
# 卡死赌挖选格(普通档 v3.1):概率启发近似真人「挑看起来最安全的一格」。
|
||
# 危险度 = 该格对每个邻接已证数字约束取(余雷数/未知数)比值中的最小值(= 任一邻数字视角下的
|
||
# 最低雷占比——single 已证安全格在进入本函数前已被 perfect_action 消化,故卡死态比值恒 >0);
|
||
# 同危险度行主序先到先得。纯查询面零 RNG(确定性保回放);寻路同 choose_dig(目标已开 8 邻
|
||
# 首个可达者为 hero_cell)。
|
||
static func gamble_pick(minesweeper) -> Dictionary:
|
||
var cons := _constraints(minesweeper, minesweeper.current_board(), {})
|
||
var best_h := 2.0
|
||
var best := Vector2i(-1, -1)
|
||
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_seal(c):
|
||
continue
|
||
var adj := false
|
||
for n in board.neighbors8(c):
|
||
if revealed_set.has(n):
|
||
adj = true
|
||
break
|
||
if not adj:
|
||
continue
|
||
var h := 1.0
|
||
for con in cons:
|
||
if (con["unknown"] as Dictionary).has(c):
|
||
var ratio := float(con["mines"]) / float((con["unknown"] as Dictionary).size())
|
||
h = minf(h, ratio)
|
||
if h < best_h - 0.0001:
|
||
best_h = h
|
||
best = c
|
||
if best.x < 0:
|
||
return {}
|
||
for n in board.neighbors8(best):
|
||
if minesweeper.is_revealed(n) and (n == minesweeper.hero_pos() or not minesweeper.compute_move_path(n).is_empty()):
|
||
return {"dig_cell": best, "hero_cell": n}
|
||
return {}
|