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

中级 - 0.74秒 3BV/S=60.81

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

▍准备

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

- 开发环境

  1. Python3 环境 - 推荐3.6或者以上 [更加推荐Anaconda3,以下很多依赖库无需安装]
  2. numpy依赖库 [如有Anaconda则无需安装]
  3. PIL依赖库 [如有Anaconda则无需安装]
  4. opencv-python
  5. win32gui、win32api依赖库
  6. 支持Python的IDE [可选,如果你能忍受用文本编辑器写程序也可以]

- 扫雷软件

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

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

▍ 实现思路

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

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

  1. 完成窗体内容截取部分
  2. 完成雷块分割部分
  3. 完成雷块类型识别部分
  4. 完成扫雷算法

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

- 01 窗体截取

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

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

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

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

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

我们需要先导入PIL库

然后进行具体的操作。

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

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

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

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

- 02 雷块分割

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

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

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

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

- 03 雷块识别

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

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

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

  • 1-8:表示数字1到8
  • 9:表示是地雷
  • 0:表示插旗
  • -1:表示未打开
  • -2:表示打开但是空白
  • -3:表示不是扫雷游戏中的任何方块类型

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

- 04 扫雷算法实现

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

  1. 遍历每一个已经有数字的雷块,判断在它周围的九宫格内未被打开的雷块数量是否和本身数字相同,如果相同则表明周围九宫格内全部都是地雷,进行标记。
  2. 再次遍历每一个有数字的雷块,取九宫格范围内所有未被打开的雷块,去除已经被上一次遍历标记为地雷的雷块,记录并且点开。
  3. 如果以上方式无法继续进行,那么说明遇到了死局,选择在当前所有未打开的雷块中随机点击。(当然这个方法不是最优的,有更加优秀的解决方案,但是实现相对麻烦)

基本的扫雷流程就是这样,那么让我们来亲手实现它吧~

首先我们需要一个能够找出一个雷块的九宫格范围的所有方块位置的方法。因为扫雷游戏的特殊性,在棋盘的四边是没有九宫格的边缘部分的,所以我们需要筛选来排除掉可能超过边界的访问。

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

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

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

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

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

1秒内通关扫雷?他创造属于自己的世界记录!Python实现自动扫雷的更多相关文章

  1. 利用Python实现自动扫雷

    自动扫雷一般分为两种,一种是读取内存数据,而另一种是通过分析图片获得数据,并通过模拟鼠标操作,这里我用的是第二种方式. 一.准备工作 我的版本是 python 3.6.1python的第三方库:win ...

  2. 通向高可扩展性之路(推特篇) ---- 一个推特用来支撑1亿5千万活跃用户、30万QPS、22MB每秒Firehose、以及5秒内推送信息的架构

    原文链接:http://highscalability.com/blog/2013/7/8/the-architecture-twitter-uses-to-deal-with-150m-active ...

  3. Jquery插件实现点击获取验证码后60秒内禁止重新获取

    通过jquery.cookie.js插件可以快速实现“点击获取验证码后60秒内禁止重新获取(防刷新)”的功能 先到官网(http://plugins.jquery.com/cookie/ )下载coo ...

  4. OAF 使用 javascript 使某个按钮在5秒内不能重复点击

    首先要保证按钮是BUTTON,并且按钮事件设置firePartialAction. public class CuxXXXXPGCO extends OAControllerImpl { public ...

  5. crontab在一秒内刷新多次导致部分脚本不生效的问题分析

    版权声明:本文由康中良原创文章,转载请注明出处: 文章原文链接:https://www.qcloud.com/community/article/182 来源:腾云阁 https://www.qclo ...

  6. 【转】安装Intel HAXM为Android 模拟器加速,30秒内启动完成

    http://www.cnblogs.com/Li-Cheng/p/4351966.html http://www.cnblogs.com/csulennon/p/4178404.html https ...

  7. 安装Intel HAXM为Android 模拟器加速,30秒内启动完成

    要求 必备知识 windows 7 基本操作. 运行环境 windows 7(64位); Android Studio 1.1.0;JDK 1.7.0_75(64位);android-sdk_r24 ...

  8. 微信公众号-5秒内不回复测试并处理方案,顺便复习php 时间执行

    在index.php中 file_put_contents('has_request.txt','请求时间:'.date('YmdHis')."\n",FILE_APPEND); ...

  9. 借助 Java 9 Jigsaw,如何在 60 秒内创建 JavaFX HelloWorld 程序?

    [编者按]本文作者为 Carl Dea,主要介绍利用 Jigsaw 项目在大约一分钟内编写标准化的"Hello World"消息代码.本文系国内 ITOM 管理平台 OneAPM ...

随机推荐

  1. python 入门 之 Json 序列化

    开发网站,离不了Json 但是一般情况,不支持python的其它对象,怎么办? 有办法:Json 序列化!!! 总体来说,需要序列化的数据类型为 字典,类,嵌套类. 下面是我做的一个demo,都包含了 ...

  2. Python Requests-学习笔记(4)-定制请求头和POST

    定制请求头 如果你想为请求添加HTTP头部,只要简单地传递一个 dict 给 headers 参数就可以了. import jsonurl = 'https://api.github.com/some ...

  3. 手动搭建I/O网络通信框架4:AIO编程模型,聊天室终极改造

    第一章:手动搭建I/O网络通信框架1:Socket和ServerSocket入门实战,实现单聊 第二章:手动搭建I/O网络通信框架2:BIO编程模型实现群聊 第三章:手动搭建I/O网络通信框架3:NI ...

  4. 并查集---体会以及模板&&How Many Tables - HDU 1213

    定义&&概念: 啥是并查集,就是将所有有相关性的元素放在一个集合里面,整体形成一个树型结构,它支持合并操作,但却不支持删除操作 实现步骤:(1)初始化,将所有节点的父亲节点都设置为自己 ...

  5. java集合中的一个移除数据陷阱(遍历集合自身并同时删除被遍历数据)

    下面是网上的其他解释,更能从本质上解释原因:Iterator 是工作在一个独立的线程中,并且拥有一个 mutex 锁. Iterator 被创建之后会建立一个指向原来对象的单链索引表,当原来的对象数量 ...

  6. PHP函数:array_key_exists

    array_key_exists()  - 检查数组里是否有指定的键名或索引. 注意:array_key_exists() 仅仅搜索第一维的键. 多维数组里嵌套的键不会被搜索到. 说明: rray_k ...

  7. Laravel异步队列全攻略

    最近项目需求,研究了laravel的异步队列.官方文档虽然很是详细,但也有些晦涩难懂,在此记录下步骤,供大家参考. 1.修改/config/queue.php文件 <?php return [ ...

  8. [转载]利用分块传输绕过WAF进行SQL注入

    原理 客户端给服务器发送数据的时候,如果我们利用协议去制作payload,就可以绕过http协议的waf,实现SQL注入 分块传输编码(Chunked transfer encoding)是HTTP中 ...

  9. [PHP]PHP设计模式:单例模式

    单例模式(职责模式): 简单的说,一个对象(在学习设计模式之前,需要比较了解面向对象思想)只负责一个特定的任务: 单例类: 1.构造函数需要标记为private(访问控制:防止外部代码使用new操作符 ...

  10. Spring Cloud+nacos+Feign,实现注册中心及配置中心

    写在前面 注册中心.配置中心的概念就不在这里解释了.发现服务原来一直用的是Eureka,因为这家伙闭源了,不爽.然后就发现了nacos,阿里巴巴的,好东西,一个搞定注册中心和配置中心.官网:https ...