用python开发自动扫雷游戏,破世界记录!

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

中级 - 0.74秒 3BV/S=60.81

相信许多人很早就知道有扫雷这么一款经典的游(显卡测试)戏(软件),更是有不少人曾听说过中国雷圣,也是中国扫雷第一、世界综合排名第二的郭蔚嘉的顶顶大名。

扫雷作为一款在Windows9x时代就已经诞生的经典游戏,从过去到现在依然都有着它独特的魅力:快节奏高精准的鼠标操作要求、快速的反应能力、刷新纪录的快感,这些都是扫雷给雷友们带来的、只属于扫雷的独一无二的兴奋点。

1

准备

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

开发环境

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

  • Numpy依赖库 [如有Anaconda则无需安装]

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

  • Opencv-python

  • Win32gui、win32api依赖库

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

扫雷软件

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

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

2

实现思路

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

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

  • 完成窗体内容截取部分

  • 完成雷块分割部分

  • 完成雷块类型识别部分

  • 完成扫雷算法

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

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 += 15  
top += 101  
right -= 15  
bottom -= 43  
  
rect = (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, 16  
  blocks\_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\_height  
        x2, y2 = x1 + block\_width, y1 + block\_height  
return 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  
  
    # Opened  
if 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\] = -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\[6, 6\], self.rgb\_to\_bgr((255, 255, 255))):  
            # Is mine  
self.blocks\_num\[x\]\[y\] = 9  
        elif self.equal(block\[5, 8\], self.rgb\_to\_bgr((255, 0, 0))):  
            # Is flag  
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  
self.is\_mine\_form = False  
  
if 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 扫雷算法实现

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

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

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

  • 如果以上方式无法继续进行,那么说明遇到了死局,选择在当前所有未打开的雷块中随机点击。(当然这个方法不是最优的,有更加优秀的解决方案,但是实现相对麻烦)

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

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



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\_width, kernel\_height = 3, 3  
  
# Kernel mode:\[Row\]\[Col\]  
 kernel = \[\[1, 1, 1\], \[1, 1, 1\], \[1, 1, 1\]\]  
  
# Left border  
if x == 0:  
for i in range(kernel\_height):  
         kernel\[i\]\[0\] = 0  
  
# Right border  
if x == self.blocks\_x - 1:  
for i in range(kernel\_height):  
         kernel\[i\]\[kernel\_width - 1\] = 0  
  
# Top border  
if y == 0:  
for i in range(kernel\_width):  
         kernel\[0\]\[i\] = 0  
  
# Bottom border  
if y == self.blocks\_y - 1:  
for i in range(kernel\_width):  
         kernel\[kernel\_height - 1\]\[i\] = 0  
  
# Generate the search map  
 to\_visit = generate\_kernel(kernel, kernel\_width, kernel\_height, location)  



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



def count\_unopen\_blocks(blocks):  
    count = 0  
for single\_block in blocks:  
if self.blocks\_num\[single\_block\[1\]\]\[single\_block\[0\]\] == -1:  
            count += 1  
return count  
  
def 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\]\] = 1  
  
unopen\_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 Mine  
if not self.blocks\_is\_mine\[single\_block\[1\]\]\[single\_block\[0\]\] == 1:  
# Click-able  
if self.blocks\_num\[single\_block\[1\]\]\[single\_block\[0\]\] == -1:  
  
# Source Syntax: \[y\]\[x\] - Converted  
if 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 = 0  
for single\_block in blocks:  
if self.blocks\_is\_mine\[single\_block\[1\]\]\[single\_block\[0\]\] == 1:  
            count += 1  
return count  
  
mines\_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 blocks  
self.iterate\_blocks\_image(BoomMine.analyze\_block)  
  
# Mark all mines  
self.iterate\_blocks\_number(BoomMine.detect\_mine)  
  
# Calculate where to click  
self.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的作用。

点击下方安全链接前往获取

CSDN大礼包:《Python入门&进阶学习资源包》免费分享

👉Python实战案例👈

光学理论是没用的,要学会跟着一起敲,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片

👉Python书籍和视频合集👈

观看零基础学习视频,看视频学习是最快捷也是最有效果的方式,跟着视频中老师的思路,从基础到深入,还是很容易入门的。

图片

👉Python副业创收路线👈

图片

这些资料都是非常不错的,朋友们如果有需要《Python学习路线&学习资料》,点击下方安全链接前往获取

CSDN大礼包:《Python入门&进阶学习资源包》免费分享

本文转自网络,如有侵权,请联系删除。

  • 18
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值