├── .idea ├── .gitignore ├── Automatic-minesweeping.iml ├── inspectionProfiles │ └── profiles_settings.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── LICENSE ├── README.md ├── basic_operation.py ├── game.py ├── surround.py ├── use_keyboard_and_mouse.py ├── window.py ├── winmine.exe ├── 扫雷演示.mp4 └── 自动扫雷.exe /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/Automatic-minesweeping.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Zhang Yichuan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Automatic-minesweeping(自动扫雷) 2 | Automatic minesweeping for Windows XP classic version minesweeper by Python3.7 3 | 使用Python编写脚本,实现对经典扫雷游戏的程序自动完成游戏 4 | 5 | ## exe脚本使用方法: 6 | 1. 打开扫雷 7 | 2. 运行exe脚本程序 8 | 9 | ## 源码运行方式 10 | ```shell 11 | python game.py 12 | ``` 13 | 14 | ## My Operating environment: 15 | Windows 10 16 | Python 3.7 17 | Windows经典的Minesweeper游戏 18 | 19 | ## Program requirement: 20 | Numpy:用于矩阵运算 21 | pillow:用于截图,根据截图判断每个格子内容 22 | pywin32:用户获取窗口,使用鼠标键盘 23 | ```shell 24 | pip install numpy 25 | pip install pillow 26 | pip install pywin32 27 | ``` 28 | 29 | ## 主要思路: 30 | 1. 使用pywin32获取扫雷游戏的窗口,并模拟键盘鼠标操作进行点击和插旗 31 | 2. 利用截图中,各个块像素点的和来区分各个块,比如块“1”的像素点和为128664,块“红旗”的像素点和为129165,从而将图片信息转化为矩阵信息,矩阵对应着游戏中整个棋盘 32 | 3. 遍历矩阵每个元素,根据周边信息去判断什么时候插旗,什么时候点击,什么时候左右键同时 33 | 4. 循环遍历直到游戏失败或者胜利 34 | 35 | ## 代码结构: 36 | 1. window.py主要定义基础窗口类,用于获得基本的窗口信息 37 | 2. use_keyboard_and_mouse.py顾名思义定义了鼠标和键盘操作类 38 | 3. basic_operation.py封装了之前的类,实现了扫雷的基本操作,比如点击,插旗,重开等 39 | 4. game.py是脚本运行的主逻辑,进行循环遍历判断决策等。 40 | 5. surround.py定义了格子及其周边信息类,用于判断当前局势,方便决策 41 | 42 | ## 功能局限: 43 | 1. 目前只支持高级、中级、低级三种默认棋盘大小,不知道自定义模式 44 | 45 | ## 遇到的问题: 46 | 1. IndexError: list index out of range 47 | 原因:似乎是屏幕分辨率设置了缩进导致,设置为100%即可(win10-右键-显示设置-缩放与布局-100%) 48 | -------------------------------------------------------------------------------- /basic_operation.py: -------------------------------------------------------------------------------- 1 | """ 2 | 封装每局游戏的基本操作 3 | """ 4 | from use_keyboard_and_mouse import OperateWindow 5 | from numpy import sum 6 | 7 | 8 | class BasicOperation(object): 9 | def __init__(self): 10 | self.op = OperateWindow() 11 | self.length = self.op.chessboard.shape[1] 12 | self.height = self.op.chessboard.shape[0] 13 | self.find_mines = sum(self.op.chessboard == self.op.info_to_int['flag']) 14 | self.unknown_boxes = sum(self.op.chessboard == self.op.info_to_int['unknown']) 15 | 16 | # 更新棋盘chessboard 17 | def refresh(self): 18 | self.op.update_chessboard() 19 | self.find_mines = sum(self.op.chessboard == self.op.info_to_int['flag']) 20 | self.unknown_boxes = sum(self.op.chessboard == self.op.info_to_int['unknown']) 21 | 22 | # 标红旗 23 | def make_flag(self, left_offset, top_offset): 24 | assert self.op.chessboard[top_offset][left_offset] == self.op.info_to_int['unknown'] 25 | self.op.right_click(left_offset, top_offset) 26 | self.op.chessboard[top_offset][left_offset] = self.op.info_to_int['flag'] 27 | 28 | # 点击区域 29 | def go(self, left_offset, top_offset): 30 | assert self.op.chessboard[top_offset][left_offset] == self.op.info_to_int['unknown'] 31 | self.op.left_click(left_offset, top_offset) 32 | self.refresh() 33 | 34 | # 点击中央 35 | def center(self): 36 | # print("center:({},{})".format(self.length // 2, self.height // 2)) 37 | self.go(left_offset=self.length // 2, top_offset=self.height // 2) 38 | 39 | # 重开 40 | def remake(self): 41 | self.op.f2() 42 | # 第一步点击屏幕中央 43 | self.refresh() 44 | self.center() 45 | 46 | # 同时按下左右键打开一片区域 47 | def open_surround(self, left_offset, top_offset): 48 | self.op.left_and_right_click(left_offset, top_offset) 49 | self.refresh() 50 | 51 | 52 | if __name__ == '__main__': 53 | basic_op = BasicOperation() 54 | -------------------------------------------------------------------------------- /game.py: -------------------------------------------------------------------------------- 1 | """ 2 | 定义每局游戏 3 | """ 4 | from basic_operation import BasicOperation 5 | from surround import BoxWithSurround 6 | from random import shuffle 7 | 8 | 9 | class Game(object): 10 | def __init__(self): 11 | self.basic_op = BasicOperation() 12 | self.basic_op.refresh() 13 | self.info_to_int = self.basic_op.op.info_to_int 14 | self.int_to_info = self.basic_op.op.int_to_info 15 | self.length = self.basic_op.op.window.length 16 | self.height = self.basic_op.op.window.height 17 | self.max_mines = self.basic_op.op.window.max_mines 18 | 19 | # 运行 20 | def run(self): 21 | # 如果程序运行时已经输了或者已经赢了就重开 22 | if self.game_over() or self.win(): 23 | self.basic_op.remake() 24 | 25 | # 如果新开的一把,就从中间开始点 26 | if self.new(): 27 | self.basic_op.center() 28 | 29 | # 否则就继续游戏 30 | while not self.game_over() and not self.win(): 31 | self.choose_next_click() 32 | 33 | if self.game_over(): 34 | print('Game Over') 35 | if self.win(): 36 | print('Win') 37 | 38 | # 判断是否游戏失败 39 | def game_over(self): 40 | return self.info_to_int['mine'] in self.basic_op.op.chessboard 41 | 42 | # 判断是否胜利 43 | def win(self): 44 | find_all = self.basic_op.find_mines == self.max_mines 45 | 46 | # 解决旗子插完但是还有未点击格子的问题 47 | if find_all: 48 | for top_offset in range(self.height): 49 | for left_offset in range(self.length): 50 | if self.basic_op.op.chessboard[top_offset][left_offset] == self.info_to_int['unknown']: 51 | self.basic_op.go(left_offset, top_offset) 52 | return find_all 53 | 54 | # 判断是否是新的一盘 55 | def new(self): 56 | return self.basic_op.unknown_boxes == self.length * self.height 57 | 58 | # 判断不出有把握的,就点第一个unknown 59 | def random(self): 60 | unknown_list = [] 61 | for top_offset in range(self.height): 62 | for left_offset in range(self.length): 63 | if self.basic_op.op.chessboard[top_offset][left_offset] == self.info_to_int['unknown']: 64 | unknown_list.append((left_offset, top_offset)) 65 | 66 | shuffle(unknown_list) 67 | left_offset, top_offset = unknown_list[0] 68 | self.basic_op.go(left_offset, top_offset) 69 | return 70 | 71 | 72 | # 根据列表中的坐标,统计旗的数量和雷的数量 73 | def count_list(self, pos_list): 74 | map = { 75 | 'flag': 0, 76 | 'unknown': 0 77 | } 78 | for pos in pos_list: 79 | val = self.basic_op.op.chessboard[pos[0]][pos[1]] 80 | if val > 8: 81 | var = self.int_to_info[val] 82 | if var in map.keys(): 83 | map[var] += 1 84 | return map 85 | 86 | # 选择下一次点击的位置,决策部分 87 | def choose_next_click(self): 88 | # 首先遍历所有格子 89 | for top_offset in range(self.height): 90 | for left_offset in range(self.length): 91 | # 定义每个格子 92 | box = BoxWithSurround(self.basic_op.op.chessboard, 93 | top_offset, 94 | left_offset, 95 | self.info_to_int, 96 | self.int_to_info) 97 | 98 | # 只处理数字格子,并且周围有unknown格子 99 | if box.val <= 8 and len(box.distribution['unknown']) != 0: 100 | # 如果已经插旗数目等于当前格子的数字,则周围其他格子都不是雷 101 | if len(box.distribution['flag']) == box.val: 102 | self.basic_op.open_surround(left_offset, top_offset) 103 | return 104 | 105 | # 如果插旗数+未检测数等于当前数字,则未检测全是雷 106 | elif len(box.distribution['flag']) + len(box.distribution['unknown']) == box.val: 107 | for pos in box.distribution['unknown']: 108 | t, l = box.pos_to_offset(pos) 109 | self.basic_op.make_flag(left_offset=l, top_offset=t) 110 | return 111 | 112 | # 如果还不行,就要根据相邻数字进行组合推导 113 | elif len(box.distribution['digit']) > 0: 114 | for pos in box.distribution['digit']: 115 | t, l = box.pos_to_offset(pos) 116 | if t == top_offset or l == left_offset: 117 | # 找到两个相邻的数字 118 | neighbor = BoxWithSurround(self.basic_op.op.chessboard, 119 | top_offset=t, 120 | left_offset=l, 121 | info_to_int=self.info_to_int, 122 | int_to_info=self.int_to_info) 123 | 124 | # 分别获得两个格子周围的有效格子 125 | # 旗或者未检测 126 | # flag 或者 unknown 127 | box_surround = box.get_surround() 128 | neighbor_surround = neighbor.get_surround() 129 | 130 | # 共享的格子share 131 | share = list(set(box_surround) & set(neighbor_surround)) 132 | # 各自双方 133 | selves = [(t, l), (top_offset, left_offset)] 134 | 135 | # 删除掉共享和各自双方后,剩下的私有的格子 136 | box_surround = list(set(box_surround) - set(share) - set(selves)) 137 | neighbor_surround = list(set(neighbor_surround) - set(share) - set(selves)) 138 | 139 | # 统计 140 | # {'flag': 0, 'unknown': 2} 141 | box_map = self.count_list(box_surround) 142 | neighbor_map = self.count_list(neighbor_surround) 143 | share_map = self.count_list(share) 144 | 145 | # 如果box在公共最多的雷数 == neighbor在公共最少的雷数 146 | if min(box.val - box_map['flag'] - share_map['flag'], 2) == \ 147 | neighbor.val - neighbor_map['flag'] - neighbor_map['unknown'] - share_map['flag'] and \ 148 | share_map['unknown'] == 2: 149 | 150 | for pos in neighbor_surround: 151 | if self.basic_op.op.chessboard[pos[0]][pos[1]] == self.info_to_int['unknown']: 152 | self.basic_op.make_flag(left_offset=pos[1], top_offset=pos[0]) 153 | return 154 | 155 | # 如果box在公共最少的雷 == neighbor所有的雷 156 | if share_map['unknown'] == 2 and \ 157 | box.val - box_map['unknown'] - box_map['flag'] - share_map['flag'] == 1 and \ 158 | neighbor.val - neighbor_map['flag'] - share_map['flag'] == 1: 159 | 160 | for pos in neighbor_surround: 161 | if self.basic_op.op.chessboard[pos[0]][pos[1]] == self.info_to_int['unknown']: 162 | self.basic_op.go(left_offset=pos[1], top_offset=pos[0]) 163 | return 164 | 165 | # 如果没有找到百分百概率,则随机点 166 | self.random() 167 | 168 | 169 | if __name__ == '__main__': 170 | g = Game() 171 | g.run() 172 | -------------------------------------------------------------------------------- /surround.py: -------------------------------------------------------------------------------- 1 | """ 2 | 定义每个格子及其周围8个格子的类 3 | """ 4 | 5 | 6 | class BoxWithSurround(object): 7 | def __init__(self, chessboard, top_offset, left_offset, info_to_int, int_to_info): 8 | self.val = chessboard[top_offset][left_offset] 9 | self.chessboard = chessboard 10 | self.top_offset = top_offset 11 | self.left_offset = left_offset 12 | self.length = chessboard.shape[1] 13 | self.height = chessboard.shape[0] 14 | self.info_to_int = info_to_int 15 | self.int_to_info = int_to_info 16 | 17 | # 假设元素在电脑小键盘5的位置 18 | # 其相邻值就是对应的12346789的位置 19 | self.nearby = { 20 | '_1': self.is_out(-1, 1), 21 | '_2': self.is_out(0, 1), 22 | '_3': self.is_out(1, 1), 23 | '_4': self.is_out(-1, 0), 24 | '_6': self.is_out(1, 0), 25 | '_7': self.is_out(-1, -1), 26 | '_8': self.is_out(0, -1), 27 | '_9': self.is_out(1, -1) 28 | } 29 | 30 | # 统计nearby的分布情况 31 | self.distribution = { 32 | 'flag': [], 33 | 'unknown': [], 34 | 'null': [], 35 | 'digit': [], 36 | 'mine': [], 37 | 'red_mine': [] 38 | } 39 | self.set_distribution() 40 | 41 | # 判断是否出界 42 | def is_out(self, top_offset, left_offset): 43 | x = self.top_offset + top_offset 44 | y = self.left_offset + left_offset 45 | # 如果超出边界就是视为null 46 | if x < 0 or x >= self.height or y < 0 or y >= self.length: 47 | return self.info_to_int['null'] 48 | else: 49 | return self.chessboard[x][y] 50 | 51 | # 统计每个格子四周的分布情况 52 | def set_distribution(self): 53 | for key_val in self.nearby.items(): 54 | val_int = key_val[1] 55 | # 如果是flag null或者unknown 56 | if self.int_to_info.get(val_int, None): 57 | val_info = self.int_to_info[val_int] 58 | self.distribution[val_info].append(key_val[0]) 59 | # 如果是数字 60 | else: 61 | self.distribution['digit'].append(key_val[0]) 62 | # 统计总长度为8 63 | assert sum([len(key_val[1]) for key_val in self.distribution.items()]) == 8 64 | # print(self.distribution) 65 | 66 | # pos 2 offset 67 | def pos_to_offset(self, pos): 68 | dic = { 69 | # top_offset, left_offset 70 | '_1': (-1, 1), 71 | '_2': (0, 1), 72 | '_3': (1, 1), 73 | '_4': (-1, 0), 74 | '_6': (1, 0), 75 | '_7': (-1, -1), 76 | '_8': (0, -1), 77 | '_9': (1, -1) 78 | } 79 | assert pos in dic.keys() 80 | top_offset = self.top_offset + dic[pos][0] 81 | left_offset = self.left_offset + dic[pos][1] 82 | return top_offset, left_offset 83 | 84 | def get_surround(self): 85 | res = list() 86 | for pos in self.nearby.keys(): 87 | if self.nearby[pos] in [self.info_to_int['flag'], self.info_to_int['unknown']]: 88 | top_offset, left_offset = self.pos_to_offset(pos) 89 | if 0 <= top_offset < self.height and 0 <= left_offset < self.length: 90 | res.append((top_offset, left_offset)) 91 | # print("get_surround" + str(res)) 92 | return res 93 | -------------------------------------------------------------------------------- /use_keyboard_and_mouse.py: -------------------------------------------------------------------------------- 1 | """ 2 | 对GameWindow进行二次封装,增加键盘和鼠标操作 3 | """ 4 | 5 | from window import GameWindow 6 | from win32api import SetCursorPos, mouse_event, keybd_event 7 | from win32con import MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP, KEYEVENTF_KEYUP 8 | from PIL import ImageGrab # pip install pillow, 如果下载速度慢可以去pypi.org下载whl文件 9 | from numpy import array # pip install numpy, 如果下载速度慢可以去pypi.org下载whl文件 10 | from numpy import zeros, sum, int 11 | 12 | 13 | class OperateWindow(object): 14 | def __init__(self): 15 | self.window = GameWindow() 16 | self.img = None 17 | self.info_to_int = { 18 | 'flag': 10, 19 | 'null': 11, 20 | 'unknown': 12, 21 | 'mine': 13, 22 | 'red_mine': 14 23 | } 24 | self.int_to_info = dict(zip(self.info_to_int.values(), self.info_to_int.keys())) 25 | self.chessboard = None 26 | 27 | self.update_chessboard() 28 | 29 | # 单击鼠标左键 30 | # left_offset和top_offset表示 31 | # 从左往右第left_offset个,从上往下第top_offset个格子 32 | # 索引从0开始 33 | def left_click(self, left_offset, top_offset): 34 | pos = self.window.get_box(left_offset, top_offset) 35 | SetCursorPos(pos) 36 | mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0) 37 | mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0) 38 | 39 | # 单击鼠标右键 40 | # left_offset和top_offset表示 41 | # 从左往右第left_offset个,从上往下第top_offset个格子 42 | # 索引从0开始 43 | def right_click(self, left_offset, top_offset): 44 | pos = self.window.get_box(left_offset, top_offset) 45 | SetCursorPos(pos) 46 | mouse_event(MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, 0) 47 | mouse_event(MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0) 48 | 49 | # 鼠标左右键同时按下 50 | # left_offset和top_offset表示 51 | # 从左往右第left_offset个,从上往下第top_offset个格子 52 | # 索引从0开始 53 | def left_and_right_click(self, left_offset, top_offset): 54 | pos = self.window.get_box(left_offset, top_offset) 55 | SetCursorPos(pos) 56 | mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0) 57 | mouse_event(MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, 0) 58 | mouse_event(MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0) 59 | mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0) 60 | 61 | # 按下键盘F2 62 | def f2(self): 63 | key_f2 = 113 # f2对应的键盘输入值 64 | keybd_event(key_f2, 0, 0, 0) 65 | keybd_event(key_f2, 0, KEYEVENTF_KEYUP, 0) 66 | 67 | # 截图 68 | def get_img(self): 69 | self.img = array(ImageGrab.grab((self.window.get_side()))) 70 | 71 | # 棋盘 72 | def update_chessboard(self): 73 | self.get_img() 74 | self.chessboard = img_to_chessboard(self.img, 75 | self.window.length, 76 | self.window.height, 77 | self.window.pixel_size, 78 | self.info_to_int) 79 | 80 | 81 | def img_to_chessboard(img, length, height, pixel_size, info_to_int): 82 | """ 83 | 根据像素信息,将图片转换为棋盘信息 84 | 主要对比的方法是对每个块内所有像素值求和 85 | 恰好该值是独一无二的 86 | 因此可以用来表示对应的块 87 | """ 88 | assert len(img.shape) == 3 89 | assert img.shape[0] == height * pixel_size 90 | assert img.shape[1] == length * pixel_size 91 | assert img.shape[2] == 3 92 | 93 | map = { 94 | 141504: info_to_int['null'], 95 | 129165: info_to_int['flag'], 96 | 147294: info_to_int['unknown'], 97 | 97908: info_to_int['mine'], 98 | 51684: info_to_int['red_mine'], 99 | 128664: 1, 100 | 112384: 2, 101 | 121602: 3, 102 | 116416: 4, 103 | 110144: 5, 104 | 118464: 6, 105 | 116160: 7, 106 | 126912: 8 107 | } 108 | 109 | # d = set() 110 | 111 | chessboard = zeros((height, length), dtype=int) # 初始化棋盘 112 | for i in range(height): 113 | for j in range(length): 114 | # 遍历chessboard每个位置 115 | # 截取对应img位置上的小图片区域 116 | img_piece = img[i * pixel_size: (i + 1) * pixel_size, 117 | j * pixel_size: (j + 1) * pixel_size, :] 118 | assert img_piece.shape == (pixel_size, pixel_size, 3) 119 | 120 | sum_val = sum(img_piece) 121 | if sum_val in map.keys(): 122 | chessboard[i][j] = map[sum_val] 123 | else: 124 | # print("颜色判断错误", sum_val) 125 | chessboard[i][j] = sum_val 126 | # d.add(sum_val) 127 | 128 | # print(chessboard) 129 | # print(len(d)) 130 | return chessboard 131 | 132 | 133 | if __name__ == '__main__': 134 | op = OperateWindow() 135 | 136 | -------------------------------------------------------------------------------- /window.py: -------------------------------------------------------------------------------- 1 | """ 2 | 扫雷游戏窗口 3 | """ 4 | # pip install pywin32,如果下载速度慢可以去pypi.org下载whl文件,务必安装223版本,其他版本不行 5 | from win32gui import FindWindow, GetWindowRect, SetForegroundWindow, SendMessage 6 | from win32con import WM_SYSCOMMAND, SC_RESTORE 7 | 8 | 9 | class GameWindow(object): 10 | def __init__(self): 11 | self.hwnd = FindWindow(None, "扫雷") # 获取扫雷游戏窗口的句柄 12 | assert self.hwnd, "请先打开扫雷,再运行该脚本程序" 13 | SendMessage(self.hwnd, WM_SYSCOMMAND, SC_RESTORE, 0) # 还原最小化 14 | SetForegroundWindow(self.hwnd) # 窗口置顶 15 | 16 | self.pixel_size = 16 # 每个格子的大小固定,为16个像素 17 | self.left, self.top, self.right, self.bottom = GetWindowRect(self.hwnd) # 获取窗口坐标 18 | self.rank = None # 扫雷游戏的等级,分为:高级、中级、初级,不包含自定义模式 19 | self.max_mines = 0 # 扫雷游戏的所有雷的数目 20 | 21 | # 去除窗口边框,只保留所有格子 22 | self.left = self.left + 15 # 左边框15个像素宽 23 | self.right = self.right - 11 # 右边框11个像素宽 24 | self.bottom = self.bottom - 11 # 下边框11个像素宽 25 | self.top = self.top + 101 # 尚边框101个像素宽 26 | 27 | # 获得游戏横向和纵向的格子数目 28 | self.height = int((self.bottom - self.top) / self.pixel_size) # 扫雷游戏的纵向格子数目 29 | assert self.height in [16, 16, 9] 30 | self.length = int((self.right - self.left) / self.pixel_size) # 扫雷游戏的横向格子数目 31 | assert self.length in [30, 16, 9] 32 | 33 | # 获取游戏难度级别 34 | self.get_rank() 35 | self.get_max_mines() 36 | 37 | def get_side(self): 38 | return self.left, self.top, self.right, self.bottom 39 | 40 | def get_rank(self): 41 | length_to_rank = {30: '高级', 16: '中级', 9: '初级'} 42 | self.rank = length_to_rank[self.length] 43 | 44 | def get_max_mines(self): 45 | rank_to_max_mines = {'高级': 99, '中级': 40, '初级': 10} 46 | self.max_mines = rank_to_max_mines[self.rank] 47 | 48 | def get_box(self, left_offset, top_offset): 49 | # 根据角标返回对应格子的屏幕像素坐标 50 | # left_offset和top_offset表示 51 | # 从左往右第left_offset个,从上往下第top_offset个格子 52 | # 索引从0开始 53 | assert 0 <= left_offset < self.length 54 | assert 0 <= top_offset < self.height 55 | left_pos = self.left + left_offset * self.pixel_size + self.pixel_size // 2 56 | top_pos = self.top + top_offset * self.pixel_size + self.pixel_size // 2 57 | return left_pos, top_pos 58 | 59 | def get_center(self): 60 | # 获取游戏正中心格子的屏幕像素坐标 61 | left_offset = self.length // 2 62 | top_offset = self.height // 2 63 | center = self.get_box(left_offset, top_offset) 64 | return center 65 | 66 | 67 | if __name__ == '__main__': 68 | win = GameWindow() 69 | 70 | 71 | -------------------------------------------------------------------------------- /winmine.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buaazyc/Automatic-minesweeping/16add7de3d0386213b12b1456388723c3c772be8/winmine.exe -------------------------------------------------------------------------------- /扫雷演示.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buaazyc/Automatic-minesweeping/16add7de3d0386213b12b1456388723c3c772be8/扫雷演示.mp4 -------------------------------------------------------------------------------- /自动扫雷.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buaazyc/Automatic-minesweeping/16add7de3d0386213b12b1456388723c3c772be8/自动扫雷.exe --------------------------------------------------------------------------------