用Python+OpenCV实现了自动扫雷,突破世界记录,我们先来看一下效果吧。

中级 - 0.74秒 3BV/S=60.81

相信许多人很早就知道有扫雷这么一款经典的游(显卡测试)戏(软件),更是有不少人曾听说过中国雷圣,也是中国扫雷第一、世界综合排名第二的郭蔚嘉的顶顶大名。扫雷作为一款在Windows9x时代就已经诞生的经典游戏,从过去到现在依然都有着它独特的魅力:快节奏高精准的鼠标操作要求、快速的反应能力、刷新纪录的快感,这些都是扫雷给雷友们带来的、只属于扫雷的独一无二的兴奋点。

0x00 准备

准备动手制作一套扫雷自动化软件之前,你需要准备如下一些工具/软件/环境

- 开发环境

  1. Python3 环境 - 推荐3.6或者以上  [更加推荐Anaconda3,以下很多依赖库无需安装]

  2. numpy依赖库 [如有Anaconda则无需安装]

  3. PIL依赖库 [如有Anaconda则无需安装]

  4. opencv-python

  5. win32gui、win32api依赖库

  6. 支持Python的IDE [可选,如果你能忍受用文本编辑器写程序也可以]

- 扫雷软件

· Minesweeper Arbiter(必须使用MS-Arbiter来进行扫雷!)

好啦,那么我们的准备工作已经全部完成了!让我们开始吧~

0x01 实现思路

在去做一件事情之前最重要的是什么? 是将要做的这件事情在心中搭建一个步骤框架。 只有这样,才能保证在去做这件事的过程中,尽可能的做到深思熟虑,使得最终有个好的结果。 我们写程序也要尽可能做到在正式开始开发之前,在心中有个大致的思路。

对于本项目而言,大致的开发过程是这样的:

  1. 完成窗体内容截取部分

  2. 完成雷块分割部分

  3. 完成雷块类型识别部分

  4. 完成扫雷算法

好啦,既然我们有了个思路,那就撸起袖子大力干!

- 01 窗体截取

其实对于本项目而言,窗体截取是一个逻辑上简单,实现起来却相当麻烦的部分,而且还是必不可少的部分。 我们通过Spy++得到了以下两点信息:

class_name = "TMain"title_name = "Minesweeper Arbiter "ms_arbiter.exe的主窗体类别为"TMain"ms_arbiter.exe的主窗体名称为"Minesweeper Arbiter "

注意到了么?主窗体的名称后面有个空格。正是这个空格让笔者困扰了一会儿,只有加上这个空格,win32gui才能够正常的获取到窗体的句柄。

本项目采用了win32gui来获取窗体的位置信息,具体代码如下:

hwnd = win32gui.FindWindow(class_name, title_name)if hwnd:left, top, right, bottom = win32gui.GetWindowRect(hwnd)

通过以上代码,我们得到了窗体相对于整块屏幕的位置。之后我们需要通过PIL来进行扫雷界面的棋盘截取。

我们需要先导入PIL库

from PIL import ImageGrab

然后进行具体的操作。

left += 15top += 101right -= 15bottom -= 43rect = (left, top, right, bottom)img = ImageGrab.grab().crop(rect)

聪明的你肯定一眼就发现了那些奇奇怪怪的Magic Numbers,没错,这的确是Magic Numbers,是我们通过一点点细微调节得到的整个棋盘相对于窗体的位置。

注意:这些数据仅在Windows10下测试通过,如果在别的Windows系统下,不保证相对位置的正确性,因为老版本的系统可能有不同宽度的窗体边框。

橙色的区域是我们所需要的

好啦,棋盘的图像我们有了,下一步就是对各个雷块进行图像分割了~

- 02 雷块分割

在进行雷块分割之前,我们事先需要了解雷块的尺寸以及它的边框大小。经过笔者的测量,在ms_arbiter下,每一个雷块的尺寸为16px*16px。

知道了雷块的尺寸,我们就可以进行每一个雷块的裁剪了。首先我们需要知道在横和竖两个方向上雷块的数量。

block_width, block_height = 16, 16blocks_x = int((right - left) / block_width)blocks_y = int((bottom - top) / block_height)

之后,我们建立一个二维数组用于存储每一个雷块的图像,并且进行图像分割,保存在之前建立的数组中。

def crop_block(hole_img, x, y):x1, y1 = x * block_width, y * block_heightx2, y2 = x1 + block_width, y1 + block_heightreturn hole_img.crop((x1, y1, x2, y2))blocks_img = [[0 for i in range(blocks_y)] for i in range(blocks_x)]for y in range(blocks_y):for x in range(blocks_x):blocks_img[x][y] = crop_block(img, x, y)

将整个图像获取、分割的部分封装成一个库,随时调用就OK啦~在笔者的实现中,我们将这一部分封装成了imageProcess.py,其中函数get_frame()用于完成上述的图像获取、分割过程。

- 03 雷块识别

这一部分可能是整 个项目里除了扫雷算法本身之外最重要的部分了。 笔者在进行雷块检测的时候采用了比较简单的特征,高效并且可以满足要求。

def analyze_block(self, block, location):block = imageProcess.pil_to_cv(block)block_color = block[8, 8]x, y = location[0], location[1]# -1:Not opened# -2:Opened but blank# -3:Un initialized# Openedif self.equal(block_color, self.rgb_to_bgr((192, 192, 192))):if not self.equal(block[8, 1], self.rgb_to_bgr((255, 255, 255))):self.blocks_num[x][y] = -2self.is_started = Trueelse:self.blocks_num[x][y] = -1elif self.equal(block_color, self.rgb_to_bgr((0, 0, 255))):self.blocks_num[x][y] = 1elif self.equal(block_color, self.rgb_to_bgr((0, 128, 0))):self.blocks_num[x][y] = 2elif self.equal(block_color, self.rgb_to_bgr((255, 0, 0))):self.blocks_num[x][y] = 3elif self.equal(block_color, self.rgb_to_bgr((0, 0, 128))):self.blocks_num[x][y] = 4elif self.equal(block_color, self.rgb_to_bgr((128, 0, 0))):self.blocks_num[x][y] = 5elif self.equal(block_color, self.rgb_to_bgr((0, 128, 128))):self.blocks_num[x][y] = 6elif self.equal(block_color, self.rgb_to_bgr((0, 0, 0))):if self.equal(block[6, 6], self.rgb_to_bgr((255, 255, 255))):# Is mineself.blocks_num[x][y] = 9elif self.equal(block[5, 8], self.rgb_to_bgr((255, 0, 0))):# Is flagself.blocks_num[x][y] = 0else:self.blocks_num[x][y] = 7elif self.equal(block_color, self.rgb_to_bgr((128, 128, 128))):self.blocks_num[x][y] = 8else:self.blocks_num[x][y] = -3self.is_mine_form = Falseif self.blocks_num[x][y] == -3 or not self.blocks_num[x][y] == -1:self.is_new_start = False

可以看到,我们采用了读取每个雷块的中心点像素的方式来判断雷块的类别,并且针对插旗、未点开、已点开但是空白等情况进行了进一步判断。具体色值是笔者直接取色得到的,并且屏幕截图的色彩也没有经过压缩,所以通过中心像素结合其他特征点来判断类别已经足够了,并且做到了高效率。

在本项目中,我们实现的时候采用了如下标注方式:

  • 1-8:表示数字1到8

  • 9:表示是地雷

  • 0:表示插旗

  • -1:表示未打开

  • -2:表示打开但是空白

  • -3:表示不是扫雷游戏中的任何方块类型

通过这种简单快速又有效的方式,我们成功实现了高效率的图像识别。

- 04 扫雷算法实现

这可能是本篇文章最激动人心的部分了。 在这里我们需要先说明一下具体的扫雷算法思路:

  1. 遍历每一个已经有数字的雷块,判断在它周围的九宫格内未被打开的雷块数量是否和本身数字相同,如果相同则表明周围九宫格内全部都是地雷,进行标记。

  2. 再次遍历每一个有数字的雷块,取九宫格范围内所有未被打开的雷块,去除已经被上一次遍历标记为地雷的雷块,记录并且点开。

  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 - 1ls.append((loc_y + rel_y, loc_x + rel_x))return lskernel_width, kernel_height = 3, 3# Kernel mode:[Row][Col]kernel = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]# Left borderif x == 0:for i in range(kernel_height):kernel[i][0] = 0# Right borderif x == self.blocks_x - 1:for i in range(kernel_height):kernel[i][kernel_width - 1] = 0# Top borderif y == 0:for i in range(kernel_width):kernel[0][i] = 0# Bottom borderif y == self.blocks_y - 1:for i in range(kernel_width):kernel[kernel_height - 1][i] = 0# Generate the search mapto_visit = generate_kernel(kernel, kernel_width, kernel_height, location)

我们在这一部分通过检测当前雷块是否在棋盘的各个边缘来进行核的删除(在核中,1为保留,0为舍弃),之后通过generate_kernel函数来进行最终坐标的生成。

def count_unopen_blocks(blocks):count = 0for single_block in blocks:if self.blocks_num[single_block[1]][single_block[0]] == -1:count += 1return countdef mark_as_mine(blocks):for single_block in blocks:if self.blocks_num[single_block[1]][single_block[0]] == -1:self.blocks_is_mine[single_block[1]][single_block[0]] = 1unopen_blocks = count_unopen_blocks(to_visit)if unopen_blocks == self.blocks_num[x][y]:mark_as_mine(to_visit)

在完成核的生成之后,我们有了一个需要去检测的雷块“地址簿”:to_visit。之后,我们通过count_unopen_blocks函数来统计周围九宫格范围的未打开数量,并且和当前雷块的数字进行比对,如果相等则将所有九宫格内雷块通过mark_as_mine函数来标注为地雷。

def mark_to_click_block(blocks):for single_block in blocks:# Not Mineif not self.blocks_is_mine[single_block[1]][single_block[0]] == 1:# Click-ableif self.blocks_num[single_block[1]][single_block[0]] == -1:# Source Syntax: [y][x] - Convertedif not (single_block[1], single_block[0]) in self.next_steps:self.next_steps.append((single_block[1], single_block[0]))def count_mines(blocks):count = 0for single_block in blocks:if self.blocks_is_mine[single_block[1]][single_block[0]] == 1:count += 1return countmines_count = count_mines(to_visit)if mines_count == block:mark_to_click_block(to_visit)

扫雷流程中的第二步我们也采用了和第一步相近的方法来实现。先用和第一步完全一样的方法来生成需要访问的雷块的核,之后生成具体的雷块位置,通过count_mines函数来获取九宫格范围内所有雷块的数量,并且判断当前九宫格内所有雷块是否已经被检测出来。

如果是,则通过mark_to_click_block函数来排除九宫格内已经被标记为地雷的雷块,并且将剩余的安全雷块加入next_steps数组内。

# Analyze the number of blocksself.iterate_blocks_image(BoomMine.analyze_block)# Mark all minesself.iterate_blocks_number(BoomMine.detect_mine)# Calculate where to clickself.iterate_blocks_number(BoomMine.detect_to_click_block)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()

在最终的实现内,笔者将几个过程都封装成为了函数,并且可以通过iterate_blocks_number方法来对所有雷块都使用传入的函数来进行处理,这有点类似Python中Filter的作用。

之后笔者做的工作就是判断当前鼠标位置是否在棋盘之内,如果是,就会自动开始识别并且点击。具体的点击部分,笔者采用了作者为"wp"的一份代码(从互联网搜集而得),里面实现了基于win32api的窗体消息发送工作,进而完成了鼠标移动和点击的操作。具体实现封装在mouseOperation.py中,有兴趣可以在文末的Github Repo中查看。

完整代码已经打包整理好了,有需要的小伙伴,关注公众号Python源码回复01即可

我用Python破了扫雷的最快世界纪录。你能打破吗?相关推荐

  1. Python实现自动扫雷,轻松挤上世界排行榜,破世界纪录~

    导语: 对于许多90后.00后来说,扫雷这个电脑上自带的小游戏早就变成古早的历史,再一次提到扫雷这个名字的时候,对许多人来说,仿佛就是上世纪的事情了 扫雷 就像是偶尔点开微信的跳一跳小游戏,发现排行榜 ...

  2. python扫雷的代码及原理_基于Python实现的扫雷游戏实例代码

    摘要:这篇Python开发技术栏目下的"基于Python实现的扫雷游戏实例代码",介绍的技术点是"Python实现.Python.实例代码.扫雷游戏.扫雷.游戏" ...

  3. 记录一次网盘资源不给提取码的经历!另类编程思维,Python破之!

    前言: 记录一次网盘资源不给提取码的经历!另类编程思维,Python破之!可能这个标题的意思不是所有人都能够理解,简单说明一下,就是好不容易在网上找资源,然而那个分享网盘的朋友忘记给提取码了..... ...

  4. OD和CE使用示例-Python实现win98扫雷一键标雷外挂

    大家好,我是小小明,前面我在<基于概率分析的智能AI扫雷程序秒破雷界世界纪录>一文中的除了用AI算法自动扫雷外,后面还演示了使用内存外挂直接知道答案进行扫雷的方法. 前面我们通过内存外挂可 ...

  5. python小游戏扫雷怎么玩的技巧_用 Python 实现扫雷小游戏

    扫雷是一款益智类小游戏,最早于 1992 年由微软在 Windows 上发行,游戏适合于全年龄段,规则简单,即在最短的时间内找出所有非雷格子且在中间过程中不能踩到雷,踩到雷则失败,需重新开始. 本文我 ...

  6. python 课程设计扫雷报告_用 Python 实现扫雷小游戏

    扫雷是一款益智类小游戏,最早于 1992 年由微软在 Windows 上发行,游戏适合于全年龄段,规则简单,即在最短的时间内找出所有非雷格子且在中间过程中不能踩到雷,踩到雷则失败,需重新开始. 本文我 ...

  7. 基于概率分析的智能AI扫雷程序秒破雷界世界纪录

    大家好,我是小小明,上次的我带大家玩了数独: <让程序自动玩数独游戏让你秒变骨灰级数独玩家> <Python调用C语言实现数独计算逻辑提速100倍> 今天我将带你用非常高端的姿 ...

  8. 【Python】实现自动扫雷,挑战世界纪录

    前言 大家好,欢迎来到 编程教室 ! 今天给大家分享的这个案例是用 Python+OpenCV 实现了自动扫雷,并突破了人类的世界记录.(当然 这不算哈) 咱们废话不多说,先看成果~ 中级 - 0.7 ...

  9. 太肝了,挑战扫雷世界纪录:扫雷自动化Python+OpenCV实战(附迪迦250G人工智能学习资料)

    咱们废话不多说,先看成果~ 中级 - 0.74秒 3BV/S=60.81 相信许多人很早就知道有扫雷这么一款经典的游(显卡测试)戏(软件),更是有不少人曾听说过中国雷圣,也是中国扫雷第一.世界综合排名 ...

最新文章

  1. 关于第十五届深度学习组比赛中前三支队伍使用网线问题调查
  2. 二极管7种应用电路详解之二
  3. 使用webpack4搭建一个基于Vue的组件库
  4. c# ui 滚动 分页_UI备忘单:分页,无限滚动和“加载更多”按钮
  5. Cartographer—ROS中的安装
  6. 计算机各种硬盘的规格,硬盘规格参数表大全
  7. Excel文档VBA代码自动删除
  8. 一文总结熵,交叉熵与交叉熵损失
  9. 并发 锁和隔离等级的关系
  10. 数据中心降低运营成本战略解密
  11. 从概率观点,怎样看新冠病毒的疫情
  12. HDU 5145 NPY and girls (莫队分块离线)
  13. xml文件怎么转换成wps_Office 12使用XML格式存储文件 正式回击WPS
  14. 激光投影仪对比激光电视 成像原理和适用范围
  15. iOS iPhone各机型尺寸及导航栏高度
  16. 软件测试从业者年纪大了以后该怎么办?我能继续测试!可以自救...
  17. 支付宝批量转帐工具使用说明书
  18. 联通沃云联手阿里云推混合云解决方案 打造共赢云生态
  19. 液晶显示器汉字字模存储及显示
  20. 利用Windows 计划任务定时将本地文件复制到共享文件夹

热门文章

  1. 俺老刘终于可以将指针传递给lua了!
  2. 逼真到颤抖!Midjourney画出完美中国情侣,画师、演员、模特一键淘汰
  3. springboot自定义favicon.ico
  4. UML建模——使用EA工具开发时序图实践及经验
  5. python有什么库可以 从fig文件中读取数据_Python之matplotlib库学习:实现数据可视化...
  6. matlab3854芯片控制电路,高功率因数开关电源的研究与实现
  7. Qbao Network 1周年庆活动跟进—进入锦鲤池的幸运名单
  8. 后端Java一年经验面试记录--外企篇
  9. 一个炫酷大屏展示页的打造过程
  10. php9宫格抽奖程序_PHP抽奖算法程序代码分享