基于 Python 与 OpenCV 的自动扫雷程序实现
1. 项目背景与技术架构
扫雷(Minesweeper)作为一款经典的 Windows 游戏,不仅考验玩家的逻辑推理能力,其底层机制也涉及状态空间搜索与约束满足问题。随着计算机视觉技术的发展,利用 Python 结合 OpenCV、PIL 及 Win32GUI 库实现扫雷自动化成为展示图像处理与算法逻辑结合的典型案例。
本文介绍了使用 Python、OpenCV 和 Win32GUI 库开发自动扫雷程序的技术方案。通过截取游戏窗口图像,利用像素颜色识别方块状态,结合逻辑算法判断地雷位置与安全区域。文章涵盖环境搭建、图像预处理、特征提取及核心博弈策略的实现细节,旨在展示计算机视觉在自动化游戏中的应用。

扫雷(Minesweeper)作为一款经典的 Windows 游戏,不仅考验玩家的逻辑推理能力,其底层机制也涉及状态空间搜索与约束满足问题。随着计算机视觉技术的发展,利用 Python 结合 OpenCV、PIL 及 Win32GUI 库实现扫雷自动化成为展示图像处理与算法逻辑结合的典型案例。
本项目旨在构建一个能够自动识别游戏界面、分析方块状态并执行点击操作的智能代理。系统主要包含以下核心模块:
在开始编写代码之前,需要搭建稳定的 Python 运行环境。推荐使用 Python 3.6 及以上版本,Anaconda 发行版可简化依赖管理。
确保已安装以下核心库:
pip install opencv-python numpy pillow pywin32
本方案针对 Minesweeper Arbiter 进行优化,该工具提供了标准化的游戏接口,便于程序控制。需确保游戏窗口标题包含特定标识,且分辨率设置固定,以保证图像分割坐标的稳定性。
准确获取游戏窗口是自动化的第一步。通过 win32gui 库查找指定类名和标题名的窗口句柄。
import win32gui
class_name = "TMain"
title_name = "Minesweeper Arbiter " # 注意末尾空格
hwnd = win32gui.FindWindow(class_name, title_name)
if hwnd:
left, top, right, bottom = win32gui.GetWindowRect(hwnd)
else:
raise Exception("未找到游戏窗口")
获取到窗口矩形坐标后,需扣除标题栏、边框等非游戏区域,仅保留棋盘部分。通过经验值调整偏移量:
left += 15
top += 101
right -= 15
bottom -= 43
rect = (left, top, right, bottom)
使用 PIL 进行精确截取:
from PIL import ImageGrab
img = ImageGrab.grab().crop(rect)
*注:上述偏移量基于 Windows 10 系统测试得出,不同系统或主题下可能需要重新校准。
将截取后的图像划分为独立的单元格。已知每个雷块尺寸为 16x16 像素,根据窗口宽高计算行列数。
block_width, block_height = 16, 16
blocks_x = int((right - left) / block_width)
blocks_y = int((bottom - top) / block_height)
blocks_img = [[None for _ in range(blocks_y)] for _ in range(blocks_x)]
for y in range(blocks_y):
for x in range(blocks_x):
x1, y1 = x * block_width, y * block_height
x2, y2 = x1 + block_width, y1 + block_height
blocks_img[x][y] = img.crop((x1, y1, x2, y2))
此步骤将连续图像转化为二维数组结构,便于后续逐个像素分析。
识别是自动化的核心。采用中心点采样法,读取每个方块中心像素的颜色值,并与预设的标准色值进行比对。
def analyze_block(self, block, location):
# 转换为 OpenCV BGR 格式
block_cv = imageProcess.pil_to_cv(block)
# 获取中心点颜色
block_color = block_cv[8, 8]
x, y = location[0], location[1]
# 定义状态码:-1 未打开,-2 空白,9 地雷,0 旗帜,1-8 数字
if self.equal(block_color, self.rgb_to_bgr((192, 192, 192))): # 灰色背景
if not self.equal(block_cv[8, 1], self.rgb_to_bgr((255, 255, 255))):
self.blocks_num[x][y] = -2 # 已点开但无数字
self.is_started = True
else:
self.blocks_num[x][y] = -1 # 未点开
elif self.equal(block_color, self.rgb_to_bgr((0, 0, 255))): # 蓝色
self.blocks_num[x][y] = 1
elif self.equal(block_color, self.rgb_to_bgr((0, 128, 0))): # 绿色
self.blocks_num[x][y] = 2
elif self.equal(block_color, self.rgb_to_bgr((255, 0, 0))): # 红色
self.blocks_num[x][y] = 3
elif self.equal(block_color, self.rgb_to_bgr((0, 0, 128))): # 深蓝
self.blocks_num[x][y] = 4
elif self.equal(block_color, self.rgb_to_bgr((128, 0, 0))): # 深红
self.blocks_num[x][y] = 5
elif self.equal(block_color, self.rgb_to_bgr((0, 128, 128))): # 青色
self.blocks_num[x][y] = 6
elif self.equal(block_color, self.rgb_to_bgr((0, 0, 0))): # 黑色
if self.equal(block_cv[6, 6], self.rgb_to_bgr((255, 255, 255))):
self.blocks_num[x][y] = 9 # 地雷
elif self.equal(block_cv[5, 8], self.rgb_to_bgr((255, 0, 0))):
self.blocks_num[x][y] = 0 # 旗帜
else:
self.blocks_num[x][y] = 7 # 其他情况
elif self.equal(block_color, self.rgb_to_bgr((128, 128, 128))): # 深灰
self.blocks_num[x][y] = 8
else:
self.blocks_num[x][y] = -3 # 无效数据
该方法效率高,无需复杂的模板匹配即可满足实时性要求。若遇到光照变化,建议引入动态阈值校正。
算法部分采用确定性推理优先,随机猜测兜底的策略。
遍历所有已打开的数字方块,检查其周围九宫格内未打开的方块数量是否等于当前数字。若相等,则剩余未打开方块必为地雷。
def generate_kernel(k, k_width, k_height, block_location):
ls = []
loc_x, loc_y = block_location[0], block_location[1]
for now_y in range(k_height):
for now_x in range(k_width):
if k[now_y][now_x]:
rel_x, rel_y = now_x - 1, now_y - 1
ls.append((loc_y + rel_y, loc_x + rel_x))
return ls
kernel = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
# 边界处理:移除超出棋盘范围的核元素
# ... (省略边界判断代码)
to_visit = generate_kernel(kernel, 3, 3, location)
unopen_count = count_unopen_blocks(to_visit)
if unopen_count == self.blocks_num[x][y]:
mark_as_mine(to_visit)
当无法确定地雷时,统计九宫格内已标记的地雷数量。若已标记数量等于当前数字,则其余未打开方块均为安全区,可点击。
def mark_to_click_block(blocks):
for single_block in blocks:
if not self.blocks_is_mine[single_block[1]][single_block[0]] == 1:
if self.blocks_num[single_block[1]][single_block[0]] == -1:
if (single_block[1], single_block[0]) not in self.next_steps:
self.next_steps.append((single_block[1], single_block[0]))
mines_count = count_mines(to_visit)
if mines_count == block:
mark_to_click_block(to_visit)
若上述逻辑均无法推进,说明进入死局。此时需在剩余未打开方块中随机选择一个点击,期望打破僵局。
识别出目标坐标后,需将其转换为屏幕绝对坐标并执行点击。
if self.is_in_form(mouseOperation.get_mouse_point()):
for to_click in self.next_steps:
on_screen_location = self.rel_loc_to_real(to_click)
mouseOperation.mouse_move(on_screen_location[0], on_screen_location[1])
mouseOperation.mouse_click()
本文详细阐述了利用 Python 生态构建自动扫雷机器人的完整流程。从底层的窗口句柄获取到上层的博弈算法,展示了计算机视觉与逻辑推理的结合应用。该方案不仅适用于扫雷游戏,其图像识别与状态机控制的思路也可迁移至其他桌面自动化场景。未来可进一步引入深度学习模型提升复杂局面下的识别准确率。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online