Files
gplan-2/game/smoke/auto_player.gd
T

252 lines
11 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
# 自动玩家策略集(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 {}