废话部分
前段时间迷上了 Play游戏 上的扫雷,那段时间没事就玩,浪费了挺多时间。
后来玩着玩着,我脑子里面突然有了一个鬼使的想法:我为什么不做一个自动扫雷的程序呢?
不觉得很酷吗?作为一个程序猿,我觉得这太酷了,很符合我对写代码的想象。炫酷中并带着毫无用处。(警惕何武器)
程序流程设计
我把程序分成下面几个部分:
- 扫雷窗口检测;
- 雷区图形裁剪;
- 雷块图形检测;
- 剩余雷数和已用时间的识别和计算;
- 模拟点击;
- 算法部分。
下面一个模块一个模块地介绍。
首先是扫雷窗口检测,我们需要使用python程序自动查找到扫雷地窗口,然后获取到窗口的绝对位置。
完成窗口定位后,为了简化图形识别的计算,需要把雷区的图形裁切下来。

然后根据裁切出来的雷区图形,再将每个小方块分别裁切出来,大概是下面这个样子。

小方块一共有15种情况,分别是没有开过的方块、点开后空白的方块、数字1-8、旗帜方块、问号方块、地雷、踩中的地雷、标错的地雷。我们需要对这15种情况进行识别和分类。
剩余雷数与已用时间只需要对数字进行识别,这两个数字的图形相同,可以使用同一种识别方法。

模拟点击可以直接调用python的win32api模块,将这个功能封装成click函数,然后预设左键点击和右键点击两个操作模式,左键用来开启方块,右键用来标旗。这部分倒是没什么好多说的。
最后就是比较重要的算法部分了,我在网上看过几篇文章,其中我认为算法比较好的一篇是 最强扫雷AI算法详解+源码分享 百度搜“扫雷算法”第一篇就是。但是这篇文章设计了一些比较复杂的算法,我这几天又没有太多时间去学习,所以这篇文章的扫雷只用了最基本、最拉跨的简单概率计算。
简单说说思路,首先是“最显而易见的标雷”法,简单来说就是检测每个数字,如果一个数字等于周围未开启的方块数,则对未开启的方块进行标旗。

在进行一次遍历标旗后,再对图形进行检测,我称它为“一看就知道没有雷”的格子,大概意思就是检测每个数字,如果一个数字的值与周围8个方块中标旗的数量相等,则可以判断剩余所有未开启的方块均没有雷。

然后再重复第一步...
不过也有些时候会陷入“仅使用上面两种方法没办法继续排雷”的情况,这种情况的话,就需要计算每个方块的含雷概率了。

我使用的是最基础的方法,即是遍历整个雷区中的所有数字块,然后对数字块周围未开启的方块进行含雷概率计算。

不过有一种情况,就是有两个位置连续的数字,周围有相同的未开启块,这种时候理论上来说需要进行“概率的叠加计算”。

但是我懒得做(:D),所以只进行了简单的概率计算,比如上面那张图,如果只是简单的概率计算的话,每个未开启块的含雷概率相等,且为66.66%。如果一个未开启块在不同数字下的概率不同,则取最高概率保存。
这种方法只能说是简单粗暴,完全不是最好的方法,但是实现起来特别简单,也不需要用到其他什么特殊的算法,所以我就采用了这种方式(:D)。
程序设计大概就是这些内容,下面说说具体怎么用代码实现的。
代码实现
代码是边想边写的,所以没有经过优化,可能比较乱。
首先是导入包:
1 2 3 4 5 6 7 8 9 10 11 12 | # 自动扫雷程序 import win32api import win32gui import win32com.client import win32con from PIL import Image, ImageGrab import numpy import cv2 import matplotlib.pyplot as plt import time import random import sys |
然后通过win32gui获取窗口句柄。
1 2 3 4 | # 寻找窗口 title_name = 'Minesweeper' class_name = 'Minesweeper' winHandle = win32gui.FindWindow(class_name, title_name) |
判断是否成功找到窗口,并且记录相关坐标。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | # 记录窗口坐标 left = 0 right = 0 top = 0 bottom = 0 if(winHandle): print("找到窗口") left, top, right, bottom = win32gui.GetWindowRect(winHandle) print("窗口坐标:") print(str(left)+' '+str(right)+' '+str(top)+' '+str(bottom)) else: print("未找到窗口!") sys.exit(1) |
获取到窗口以后,就可以进行一些坐标参数的设定了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | # 设定雷数坐标 mine_num_left = left + 20 mine_num_right = left + 59 mine_num_top = top + 62 mine_num_bottom = top + 85 # 设定时间坐标 time_num_left = right - 57 time_num_right = right - 20 time_num_top = top + 62 time_num_bottom = top + 85 # 重置按钮位置 reset_btn_x = left - ((left - right)//2) reset_btn_y = top + 75 # 设定雷区坐标 left += 15 right -= 11 top += 101 bottom -= 11 # 雷块像素大小 block_width = 16 block_height = 16 # 剩余雷数字像素大小 mine_num_height = 23 mine_num_width = 13 # 时间数字像素大小 time_num_height = 23 time_num_width = 13 |
然后是图像获取的相关代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | # 图像识别颜色定义 # 数字1-8 周围雷数 # 0 未被打开 # ed 被打开 空白 # flag 红旗 # boom 普通雷 # boom_red 踩中的雷 # qm 问号 # 雷块 rgba_ed = [(225, (192, 192, 192)), (31, (128, 128, 128))] rgba_flag = [(54, (255, 255, 255)), (17, (255, 0, 0)), (109, (192, 192, 192)), (54, (128, 128, 128)), (22, (0, 0, 0))] rgba_0 = [(54, (255, 255, 255)), (148, (192, 192, 192)), (54, (128, 128, 128))] rgba_1 = [(185, (192, 192, 192)), (31, (128, 128, 128)), (40, (0, 0, 255))] rgba_2 = [(160, (192, 192, 192)), (31, (128, 128, 128)), (65, (0, 128, 0))] rgba_3 = [(62, (255, 0, 0)), (163, (192, 192, 192)), (31, (128, 128, 128))] rgba_4 = [(169, (192, 192, 192)), (31, (128, 128, 128)), (56, (0, 0, 128))] rgba_5 = [(70, (128, 0, 0)), (155, (192, 192, 192)), (31, (128, 128, 128))] rgba_6 = [(153, (192, 192, 192)), (31, (128, 128, 128)), (72, (0, 128, 128))] rgba_7 = [(181, (192, 192, 192)), (31, (128, 128, 128)), (44, (0, 0, 0))] rgba_8 = [(149, (192, 192, 192)), (107, (128, 128, 128))] rgba_boom = [(4, (255, 255, 255)), (144, (192, 192, 192)), (31, (128, 128, 128)), (77, (0, 0, 0))] rgba_boom_red = [(4, (255, 255, 255)), (144, (255, 0, 0)), (31, (128, 128, 128)), (77, (0, 0, 0))] rgba_wrong_boom = [(1, (255, 255, 255)), (46, (255, 0, 0)), (126, (192, 192, 192)), (31, (128, 128, 128)), (52, (0, 0, 0))] rgba_qm = [(54, (255, 255, 255)), (124, (192, 192, 192)), (54, (128, 128, 128)), (24, (0, 0, 0))] # 雷数 # 2、3、5 # 6和9 # 3、5 num_0 = [(126, (255, 0, 0)), (10, (128, 0, 0)), (163, (0, 0, 0))] num_1 = [(42, (255, 0, 0)), (52, (128, 0, 0)), (205, (0, 0, 0))] num_2 = [(107, (255, 0, 0)), (24, (128, 0, 0)), (168, (0, 0, 0))] num2_3 = [(10, (255, 0, 0)), (24, (128, 0, 0)), (29, (0, 0, 0))] num_4 = [(86, (255, 0, 0)), (30, (128, 0, 0)), (183, (0, 0, 0))] num3_5 = [(27, (255, 0, 0)), (6, (0, 0, 0))] num_6 = [(128, (255, 0, 0)), (12, (128, 0, 0)), (159, (0, 0, 0))] num_7 = [(63, (255, 0, 0)), (43, (128, 0, 0)), (193, (0, 0, 0))] num_8 = [(149, (255, 0, 0)), (150, (0, 0, 0))] num2_9 = [(31, (255, 0, 0)), (12, (128, 0, 0)), (20, (0, 0, 0))] |
1 2 3 4 5 | # 获取最新图像 def refresh_img(): rect = (left, top, right, bottom) img = ImageGrab.grab().crop(rect) return img |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | #扫描雷区图像 def get_block(x,y,img): # 获取x,y处的方块类型 r = -1 # 先计算坐标 x*=block_width y*=block_height # 图像准备 # rect = (left, top, right, bottom) # img = ImageGrab.grab().crop(rect) # plt.imshow(img) # plt.axis('off') # plt.show() block = img.crop((x, y, x + block_width, y + block_height)) # 显示 plt.imshow(block) plt.axis('off') plt.show() # 判断 block_colors = block.getcolors() if block_colors == rgba_0: # print("识别到:未开启") r = 0 elif block_colors == rgba_1: # print("识别到:1") r = 1 elif block_colors == rgba_2: # print("识别到:2") r = 2 elif block_colors == rgba_3: # print("识别到:3") r = 3 elif block_colors == rgba_4: # print("识别到:4") r = 4 elif block_colors == rgba_5: # print("识别到:5") r = 5 elif block_colors == rgba_6: # print("识别到:6") r = 6 elif block_colors == rgba_7: # print("识别到:7") r = 7 elif block_colors == rgba_8: # print("识别到:8") r = 8 elif block_colors == rgba_ed: # print("识别到:空白") r = 9 elif block_colors == rgba_flag: # print("识别到:旗帜") r = 10 elif block_colors == rgba_boom: # print("识别到:炸弹") r = 11 elif block_colors == rgba_boom_red: # print("识别到:炸弹(被踩中)") r = 12 elif block_colors == rgba_wrong_boom: # print("识别到:错误雷") r = 13 elif block_colors == rgba_qm: # print("识别到:问号") r = 14 else: plt.imshow(block) plt.axis('off') plt.show() print("识别失败:") print('x:',x,' y:',y) print("色彩代码:") print(block.getcolors()) # 暂时 sys.exit(0) return r |
通过调用get_block(x,y)方法,得到的返回值是识别到方块的内容。
然后是mine_map的更新函数,包括了更新所有的方块和计算每个未开启方块的概率。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | # 全图扫描(包含概率计算) def scan_map(x, y, img): mine_map = [[[0,-1.0] for k in range(x)] for k in range(y)] block_num = -1 # 扫描方块 for i in range(x): for j in range(y): mine_map[j][i][0] = get_block(i, j, img) # 计算概率 for i in range(x): for j in range(y): # mine_map是一个三维列表,其中的元素是二维列表,其中第一个是方块内容,第二个是含雷概率 block_num = mine_map[j][i][0] if 1 <= block_num <= 8: # 数字,对周围白色块计算概率 block0 = 0 # 未开块数量 b0_pos = [] # 未开块位置(元组) flag0 = 0 # 标旗数量 block_p = -1 # 含雷概率 try: for k in [-1,0,1]: # x轴 for l in [-1,0,1]: # y轴 if 0 <= (i+k) <= b_x - 1 and 0 <= (j+l) <= b_y - 1: if mine_map[j + l][i + k][0] == 0: # 未开启块 b0_pos.append((j+l,i+k)) block0+=1 if mine_map[j + l][i + k][0] == 10: flag0+=1 # 简单概率计算:[(方块数字 - 标旗数量) / 未开启方块数] 保留4位小数 block_p = round((block_num - flag0) / block0, 4) # 将计算得到的概率写到map的元组中 while b0_pos: # p[0]:y p[1]:x p = b0_pos.pop() if block_p > mine_map[p[0]][p[1]][1]: # 如果新计算得到概率更高,则写入 mine_map[p[0]][p[1]][1] = block_p except: # 不知道会不会出现这个问题,理论上不会,先加上 pass # 数字块,概率一定为 0 mine_map[j][i][1] = 0.0 elif block_num == 9: # 空白块,概率一定为 0 mine_map[j][i][1] = 0.0 elif block_num == 0: # 未开块,概率在数字块处理时被计算,不改变 pass else: # 旗帜、雷块(如果存在这种可能性的话)和其他可能出现的方块,概率一定为 1 mine_map[j][i][1] = 1.0 return mine_map |
接下来是对剩余雷数和已用时间的识别和计算,其中我把对数字图像的识别单独做成了一个函数,因为这两个部分的数字图像相同,所以可以对函数进行复用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | # 通过图像获取剩余雷数 def get_mine_num(num_img): mine_num = 0 # plt.imshow(num_img) # plt.axis('off') # plt.show() if num_img.getcolors() == num_0: mine_num = 0 elif num_img.getcolors() == num_1: mine_num = 1 elif num_img.getcolors() == num_2: # 2、3、5 num_img = num_img.crop((1, 1, 4, mine_num_height-1)) if num_img.getcolors() == num2_3: mine_num = 3 else: # 2、5 num_img = num_img.crop((0, 0, 3, 11)) if num_img.getcolors() == num3_5: mine_num = 5 else: mine_num = 2 elif num_img.getcolors() == num_4: mine_num = 4 elif num_img.getcolors() == num_6: # 6、9 num_img = num_img.crop((1, 1, 4, mine_num_height-1)) if num_img.getcolors() == num2_9: mine_num = 9 else: mine_num = 6 elif num_img.getcolors() == num_7: mine_num = 7 elif num_img.getcolors() == num_8: mine_num = 8 else: mine_num = 0 return mine_num def get_last_mine(): # 百位 num_img = ImageGrab.grab().crop((mine_num_left, mine_num_top, mine_num_right, mine_num_bottom)) plt.imshow(num_img) plt.axis('off') plt.show() mine_num = 0 num_img = ImageGrab.grab().crop((mine_num_left + mine_num_width*0, mine_num_top, mine_num_left + mine_num_width*1, mine_num_bottom)) mine_num += get_mine_num(num_img)*100 # 十位 num_img = ImageGrab.grab().crop((mine_num_left + mine_num_width*1, mine_num_top, mine_num_left + mine_num_width*2, mine_num_bottom)) mine_num += get_mine_num(num_img)*10 # 个位 num_img = ImageGrab.grab().crop((mine_num_left + mine_num_width*2, mine_num_top, mine_num_left + mine_num_width*3, mine_num_bottom)) mine_num += get_mine_num(num_img) return mine_num |
1 2 3 4 5 6 7 8 9 10 11 12 13 | # 获取时间 def get_time(): # 百位 time_num = 0 num_img = ImageGrab.grab().crop((time_num_left + time_num_width*0, time_num_top, time_num_left + time_num_width*1, time_num_bottom)) time_num += get_mine_num(num_img)*100 # 十位 num_img = ImageGrab.grab().crop((time_num_left + time_num_width*1, time_num_top, time_num_left + time_num_width*2, time_num_bottom)) time_num += get_mine_num(num_img)*10 # 个位 num_img = ImageGrab.grab().crop((time_num_left + time_num_width*2, time_num_top, time_num_left + time_num_width*3, time_num_bottom)) time_num += get_mine_num(num_img) return time_num |
然后是模拟点击功能,这部分实现没什么难度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | #模拟点击(x,y,点击方式[0 - 左键, 1 - 右键, 2 - 双键]) def click(x, y, c): # print("点击 x:",x," y:",y," 键值:",c) x = left+(x*block_width)+(block_width//2) y = top+(y*block_height)+(block_height//2) # 窗口聚焦并移动光标 shell = win32com.client.Dispatch("WScript.Shell") shell.SendKeys('%') win32gui.SetForegroundWindow(winHandle) win32api.SetCursorPos([x, y]) if c == 0: win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0) win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0) elif c == 1: win32api.mouse_event(win32con.MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, 0) win32api.mouse_event(win32con.MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0) elif c == 2: win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0) win32api.mouse_event(win32con.MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, 0) win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0) win32api.mouse_event(win32con.MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0) else: pass |
模拟点击功能预留了一个双键接口,玩过xp版扫雷的都知道,左右键同时按下可以实现自动排雷。不过后来在测试的时候发现,xp版扫雷的自动排雷在点击速度过快的情况下会出现图像更新延迟的情况,导致Python程序图像识别失败,还不如我自己设计排雷程序。所以后来这部分就被弃用了,不过代码只是注释掉了,没有删掉。
下面是reset按钮的点击,有了这个功能,就不用手动点reset按钮了,也可以实现循环运行了。
1 2 3 4 5 6 7 8 9 10 | # 点击reset按钮 def reset_btn_click(): # 窗口聚焦并移动光标 shell = win32com.client.Dispatch("WScript.Shell") shell.SendKeys('%') win32gui.SetForegroundWindow(winHandle) win32api.SetCursorPos([reset_btn_x, reset_btn_y]) win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0) win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0) |
接下来是开始运行前的一些参数设置了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | # 常量 SCAN_DELAY = 0.05 # 扫描延时 CLICK_DELAY = 0.002 # 点击延时 # 数据记录 total_rounds = 50 # 游戏总数 min_time = 999 # 最短耗时 average_time = 0 # 平均耗时 max_time = 0 # 最长耗时 win_times = 0 # 获胜次数 fail_times = 0 # 失败次数 win_rate = 0.0 # 获胜率 run_out_of_steps = 0 # 步数用完 total_mines = get_last_mine() # 总共雷数 first_fail = 0 # 第一次踩雷次数 total_time = 0 # 总耗时 total_time_win = 0 # 胜利局总耗时 max_time_win = 0 # 胜利局最长耗时 min_time_win = 999 # 胜利局最短耗时 average_time_win = 0 # 胜利局平均耗时 # 计算块数 b_x = (right-left)//block_width b_y = (bottom-top)//block_height |
最后是最重要的运行部分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 | for number_of_game in range(total_rounds): print("第",number_of_game+1,"局") # 尽量防止扫雷程序卡死 time.sleep(0.2) # 点击重置按钮 reset_btn_click() time.sleep(SCAN_DELAY) # 随机起始位置 x = random.randint(0 , b_x - 1) y = random.randint(0 , b_y - 1) # 检测雷数 last_mine = get_last_mine() # print("当前局雷数:", last_mine) # 开始执行 print("起始点击位置 x:"+str(x)+" y:"+str(y)) click(x, y, 0) time.sleep(SCAN_DELAY) # 如果刷新前刚完成点击事件,则延时0.05秒,防止扫雷软件刷新未完成 mine_map = scan_map(b_x, b_y, refresh_img()) mine_map_old = mine_map First = True # 是否是第一次点开 win = False # 是否胜利 Num_pos = [] # 用来记录所有数字坐标元组,Num_pos[0]是y,Num_pos[1]是x for number_of_steps in range(50000): if 0 < mine_map[y][x][0] < 9: # 数字 Num_pos.append((y,x)) if First: # 第一次点中数字,随机点另一个位置 print("第一次点中数字,重新随机选择起始位置") x = random.randint(0 , b_x - 1) y = random.randint(0 , b_y - 1) print("click x:"+str(x)+" y:"+str(y)) click(x, y, 0) time.sleep(SCAN_DELAY) mine_map = scan_map(b_x, b_y, refresh_img()) continue # print("found number: x:"+str(x)+" y:"+str(y)," number:",mine_map[y][x][0]) # 检查周围8格的未点击数量 block0 = 0 # 未开块数量 flag0 = 0 # 标旗数量 b0_pos = [] try: for i in [-1,0,1]: for j in [-1,0,1]: if 0 <= (x+j) <= b_x - 1 and 0 <= (y+i) <= b_y - 1: if mine_map[y + i][x + j][0] == 0: # print(x + j,y + i) block0+=1 b0_pos.append((y+i, x+j)) if mine_map[y + i][x + j][0] == 10: flag0+=1 # print("number of block not dig:",block0) except: # 不知道会不会出现这个问题,理论上不会,先加上 pass # 如果周围旗帜数等于数字,且存在未知块,则可以把所有空白点开 if mine_map[y][x][0] == flag0 and block0 != 0: while b0_pos: p = b0_pos.pop() click(p[1], p[0], 0) time.sleep(CLICK_DELAY) time.sleep(SCAN_DELAY) mine_map = scan_map(b_x, b_y, refresh_img()) # 如果周围存在未知格且雷数等于未知数,则可以进行标旗 if mine_map[y][x][0]-flag0 == block0 and block0 != 0: while b0_pos: p = b0_pos.pop() click(p[1], p[0], 1) last_mine-=1 time.sleep(SCAN_DELAY) mine_map = scan_map(b_x, b_y, refresh_img()) if last_mine == 0: # 所有雷找完,进入善后工作(检测所有未开块并点击,然后完成游戏) if get_last_mine() == 0: # double check print("雷已排完,进入善后工作") for i in range(b_x): for j in range(b_y): if mine_map[j][i][0] == 0: click(i,j,0) time.sleep(CLICK_DELAY) print("游戏结束,胜利") win = True win_times+=1 break else: # 出现错误,修正雷数(理论上来说不应该出现) print("出现错误,修正雷数") last_mine = get_last_mine() # 后面进行结尾处理 elif mine_map[y][x][0] == 9: # 空白 if First: First = False # 从0开始重新遍历整个map,直到找到一个数字 for y in range(0, b_y): for x in range(0, b_x): if mine_map[y][x][0] > 0 and mine_map[y][x][0] < 9: break else: continue break else: pass # 后面进行结尾处理 elif mine_map[y][x][0] == 0 or mine_map[y][x][0] == 10: # 未挖开(或标旗),寻找下一个数字 First = False # 后面进行结尾处理 elif mine_map[y][x][0] == 11 or mine_map[y][x][0] == 12 or mine_map[y][x][0] == 13: # 游戏结束,失败 if First: first_fail+=1 First = False print("踩雷,游戏结束。") fail_times+=1 break elif mine_map[y][x][0] == 14: # 问号(暂时) click(x,y,1) mine_map[y][x][0] = 12 continue elif mine_map[y][x][0] == -1: # 识别失败 First = False fail_times+=1 break else: # 暂时 First = False print(x,y) print("意外结束或运行结束:",mine_map[y][x][0]) fail_times+=1 break # 结尾处理 if x < b_x-1: x+=1 elif y < b_y-1: x=0 y+=1 else: # 到最后一格 # while Num_pos: # # 把所有数字点一遍 # p = Num_pos.pop() # click(p[1],p[0],2) # time.sleep(CLICK_DELAY) # time.sleep(SCAN_DELAY) # mine_map = scan_map(b_x, b_y, refresh_img()) x=0 y=0 if last_mine == 0: # 点完所有数字后再检测一次 print("游戏结束,胜利") win = True win_times+=1 break if mine_map == mine_map_old: # 执行到这里时,出现无法根据基本算法解题的情况 # 后续添加未开块的概率计算之类的功能 # double check last_mine = get_last_mine() if last_mine == 0: # 点完所有数字后再检测一次 print("游戏结束,胜利") win = True win_times+=1 break print("无法通过基本算法解题") # 搜索所有数字附近的未开块,并寻找含雷概率最低的进行点击 min_p = [[0,0],1.0] for i in range(b_x): for j in range(b_y): if 1 <= mine_map[j][i][0] <= 9: for k in [-1,0,1]: for l in [-1,0,1]: if 0 <= (i+k) <= b_x - 1 and 0 <= (j+l) <= b_y - 1: if mine_map[j+l][i+k][0] == 0: # 找到未开块 # print("找到未开块,x:",i+k," y:",j+l,"概率:",mine_map[j+l][i+k][1]) if mine_map[j+l][i+k][1] != -1: # 确保未开块的概率已经经过计算(不是默认值-1) if mine_map[j+l][i+k][1] <= min_p[1]: min_p[0][0] = i+k min_p[0][1] = j+l min_p[1] = mine_map[j+l][i+k][1] print("点击:x:",min_p[0][0]," y:",min_p[0][1],"概率为:",min_p[1]) click(min_p[0][0],min_p[0][1],0) time.sleep(SCAN_DELAY) # 刷新地图 mine_map = scan_map(b_x, b_y, refresh_img()) else: mine_map_old = mine_map else: # 为了防止算法不完善导致进入死循环状态,所以使用了for进行循环,并且限定了循环步数 print("循环步数用完") run_out_of_steps+=1 this_round_time = get_time() print("本局耗时:{}s".format(this_round_time)) if this_round_time > max_time: max_time = this_round_time if this_round_time < min_time: min_time = this_round_time total_time+=this_round_time if win: if this_round_time > max_time_win: max_time_win = this_round_time if this_round_time < min_time_win: min_time_win = this_round_time total_time_win+=this_round_time |
代码里面的注释还是写得挺完善的,就不单独多做解释了,最后就是把运行得到的结果计算并输出出来,整个程序就完成了。
1 2 3 4 5 6 7 8 9 10 11 12 | # 进行相关数据的计算及显示 win_rate = round(win_times/total_rounds,4)*100 average_time = round(total_time/total_rounds,2) if win_times != 0: average_time_win = round(total_time_win/win_times,2) print("在设定模式中,共{}颗雷,雷区大小为{}×{}".format(total_mines, b_x, b_y)) print("在{}局游戏中,成功{}次,失败{}次,步数用完{}次,获胜率{}%".format(total_rounds, win_times, fail_times, run_out_of_steps, win_rate)) print("其中,第一步就踩中雷的次数为{}次".format(first_fail)) print("全局总耗时{}s,其中,胜利局耗时{}s".format(total_time, total_time_win)) print("最长耗时{}s,最短耗时{}s,平均耗时{}s (包括失败)".format(max_time, min_time, average_time)) print("胜利局最长耗时{}s,胜利局最短耗时{}s,胜利局平均耗时{}s".format(max_time_win, min_time_win, average_time_win)) |
需要注意的是,代码里面包含了一些测试时使用的log打印和图像显示的代码,在实际运行当中要记得把他们删掉哦,不然可能会一直弹图像窗口出来,再加上扫雷程序从某种意义上来说会锁定鼠标指针,那到时候就只能等死咯。

我跑了500局中级难度测试,正确率在40%左右,其中有151局第一步就踩中雷。第一步就踩中雷的话就是运气问题了,这没办法。这个正确率感觉还是挺不错了。
目前来说,耗时上成绩也还不错,我自己跑的记录是初级1秒,中级6秒,高级20秒。

最后再放一个程序扫雷的过程吧。

简单总结
全文通过调用Windows的一些API,实现了扫雷窗口的查找,然后通过PIL实现了相关图像的提取和识别,最后通过识别得到的结果,进行相关的地雷排查和计算。
本文没有使用什么特殊的算法,就只是最简单的概率计算。写这个程序断断续续搞了好几天,但实际上真正投入在代码的编写和修改中的工作也就一两天,所以这个程序还相当不完善。做出这个程序其实也没什么意义,只是觉得好玩而已,感兴趣的事情就应该多去做(:D),毕竟,这就是写代码的乐趣嘛。 :ava_jellyfish:

