├── .gitignore ├── requirements.txt ├── minesweeper.gif ├── position.scpt ├── common.py ├── README.md ├── mouse.py ├── test.py ├── screen.py ├── screen_capture.py └── main.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.pyc 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyobjc==2.5.1 2 | PyMouse==1.0 3 | Pillow==3.2.0 -------------------------------------------------------------------------------- /minesweeper.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dingyaguang117/minesweeper-helper/HEAD/minesweeper.gif -------------------------------------------------------------------------------- /position.scpt: -------------------------------------------------------------------------------- 1 | tell application "System Events" to tell application process "Minesweeper Deluxe" set pos to position of window 1 do shell script "echo " & item 1 of pos & "," & item 2 of pos end tell -------------------------------------------------------------------------------- /common.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 2 | __author__ = 'ding' 3 | import contextlib 4 | import time 5 | 6 | @contextlib.contextmanager 7 | def timer(msg): 8 | start = time.time() 9 | yield 10 | end = time.time() 11 | print "%s: %.2fs" % (msg, end-start) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Mac扫雷程序外挂 2 | -- 3 | 4 | ![截图](minesweeper.gif) 5 | 6 | ### 算法原理 7 | 8 | 其实最通用的算法就是,枚举所有的未知的格子得到排列组合,如果一个格子在所有合法的情况下都是雷(或者不是雷),那么可以断定一个格子一定是雷(或者不是雷). 9 | 10 | 但是这个算法的时间复杂度是 O(2^N), 不过可以进行优化. 比如: 对于不在任何已知格子旁边的未知格子, 因为并没有任何参照可以判定它是否是雷, 所以不必要参加搜索. 11 | 12 | 总之核心就是减少每次参与搜索的候选格子列表, 我采用的是每次搜索某个已知格子周围的所有未知格子, 得到所有的排列组合. 13 | 14 | 另外如果遇到无法判断的情况, 则选取概率(局部概率)最小的一个格子点击. 15 | 16 | ### 截图 17 | 采用 pyobjc 18 | 19 | ### 点击采用 20 | 采用pymouse (初始化非常慢) -------------------------------------------------------------------------------- /mouse.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 2 | __author__ = 'ding' 3 | 4 | print 'importing PyMouse Module...' 5 | from pymouse import PyMouse 6 | print 'done' 7 | from screen import get_base_xy, SIZE 8 | 9 | m = PyMouse() 10 | 11 | 12 | def flag(col, row, is_mine): 13 | x, y = get_base_xy(col, row) 14 | x += SIZE / 2 15 | y += SIZE / 2 16 | button = 2 if is_mine else 1 17 | m.click(x, y, button) 18 | 19 | 20 | if __name__ == '__main__': 21 | flag(0, 0, False) -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 2 | __author__ = 'ding' 3 | import unittest 4 | import screen 5 | screen.W, screen.H = 3, 3 6 | 7 | from main import is_valid, is_around_valid 8 | 9 | 10 | class TestValid(unittest.TestCase): 11 | def setUp(self): 12 | self.matrix = [ 13 | [1, 2, 3], 14 | ['*', 0, '*'], 15 | [' ', '*', 1] 16 | ] 17 | 18 | def test_is_valid(self): 19 | assert is_valid(self.matrix, 0, 1) == True 20 | assert is_valid(self.matrix, 0, 2) == False 21 | assert is_valid(self.matrix, 2, 2) == False 22 | 23 | def test_is_around_valid(self): 24 | print 'test_is_around_valid' 25 | assert is_around_valid(self.matrix, 1, 0) == True 26 | assert is_around_valid(self.matrix, 0, 1) == False 27 | assert is_around_valid(self.matrix, 2, 1) == False 28 | 29 | 30 | class TestSearch(unittest.TestCase): 31 | def setUp(self): 32 | self.matrix = [ 33 | [1, 2, 1], 34 | ['*', 0, '*'], 35 | [' ', '*', 1] 36 | ] 37 | 38 | def test_is_valid(self): 39 | assert is_valid(self.matrix, 0, 1) == True 40 | assert is_valid(self.matrix, 2, 2) == False 41 | 42 | 43 | 44 | 45 | if __name__ == '__main__': 46 | unittest.main() -------------------------------------------------------------------------------- /screen.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 2 | __author__ = 'ding' 3 | import os 4 | import time 5 | 6 | from collections import defaultdict 7 | from itertools import product 8 | from screen_capture import get_screenshot 9 | 10 | SIZE = 30 # size of grid 11 | W, H = 16, 16 # count of cols and rows 12 | 13 | offset = None # content offset of screen 14 | 15 | COLORS = { 16 | (0, 0, 15): 1, 17 | (0, 7, 0): 2, 18 | (15, 0, 0): 3, 19 | (0, 0, 7): 4, 20 | (7, 0, 0): 5, 21 | (0, 7, 7): 6, 22 | (0, 0, 0): '*', # mine 23 | } 24 | 25 | 26 | def find_offset(): 27 | ''' 28 | return top-left position of grid[0][0] 29 | ''' 30 | global offset 31 | if offset is None: 32 | offset = os.popen('osascript position.scpt').read() 33 | offset = map(int, offset.split(',')) 34 | offset = (offset[0] + 15, offset[1] + 97) # (15, 97) is offset in window 35 | print offset 36 | return offset 37 | 38 | 39 | def get_base_xy(col, row): 40 | ''' 41 | return top-left position of grid[col][row] 42 | ''' 43 | base_offset = find_offset() 44 | base_x = col * SIZE + base_offset[0] 45 | base_y = row * SIZE + base_offset[1] 46 | return base_x, base_y 47 | 48 | 49 | def get_matrix(matrix=None): 50 | t1 = time.time() 51 | #use pyobjc instead of ImageGrab.grab() 52 | im = get_screenshot() 53 | t2 = time.time() 54 | 55 | if matrix is None: 56 | matrix = [] 57 | for i in xrange(0, W): 58 | matrix.append([0] * H) 59 | 60 | for col, row in product(xrange(W), xrange(H)): 61 | # s = defaultdict(int) 62 | if matrix[col][row] != 0: 63 | continue 64 | 65 | base_x, base_y = get_base_xy(col, row) 66 | 67 | for _x, _y in product(xrange(SIZE - 4, 4, -1), xrange(SIZE - 4, 4, -1)): 68 | color = im.getpixel((base_x + _x, base_y + _y)) 69 | color = (color[0] >> 4, color[1] >> 4, color[2] >> 4) 70 | 71 | # s[color] += 1 72 | if color in COLORS: 73 | matrix[col][row] = COLORS[color] 74 | break 75 | 76 | if matrix[col][row] == 0 and im.getpixel((base_x, base_y))[0] < 200: 77 | matrix[col][row] = ' ' 78 | # print '-----', col, row 79 | # for color in s: 80 | # print color, s[color] 81 | t3 = time.time() 82 | print 'snapshot cost %.2fs, recognize cost %.2fs' % (t2 - t1, t3 - t2) 83 | return matrix 84 | 85 | 86 | def print_matrix(matrix): 87 | for row in xrange(H): 88 | for col in xrange(W): 89 | print matrix[col][row], 90 | print '' 91 | 92 | 93 | 94 | if __name__ == '__main__': 95 | print_matrix(get_matrix()) -------------------------------------------------------------------------------- /screen_capture.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 2 | __author__ = 'ding' 3 | 4 | import struct 5 | import Quartz.CoreGraphics as CG 6 | from PIL import Image 7 | 8 | class ScreenPixel(object): 9 | """Captures the screen using CoreGraphics, and provides access to 10 | the pixel values. 11 | """ 12 | 13 | def capture(self, region = None): 14 | """region should be a CGRect, something like: 15 | 16 | >>> import Quartz.CoreGraphics as CG 17 | >>> region = CG.CGRectMake(0, 0, 100, 100) 18 | >>> sp = ScreenPixel() 19 | >>> sp.capture(region=region) 20 | 21 | The default region is CG.CGRectInfinite (captures the full screen) 22 | """ 23 | 24 | if region is None: 25 | region = CG.CGRectInfinite 26 | else: 27 | # TODO: Odd widths cause the image to warp. This is likely 28 | # caused by offset calculation in ScreenPixel.pixel, and 29 | # could could modified to allow odd-widths 30 | if region.size.width % 2 > 0: 31 | emsg = "Capture region width should be even (was %s)" % ( 32 | region.size.width) 33 | raise ValueError(emsg) 34 | 35 | # Create screenshot as CGImage 36 | image = CG.CGWindowListCreateImage( 37 | region, 38 | CG.kCGWindowListOptionOnScreenOnly, 39 | CG.kCGNullWindowID, 40 | CG.kCGWindowImageDefault) 41 | 42 | # Intermediate step, get pixel data as CGDataProvider 43 | prov = CG.CGImageGetDataProvider(image) 44 | 45 | # Copy data out of CGDataProvider, becomes string of bytes 46 | self._data = CG.CGDataProviderCopyData(prov) 47 | 48 | # Get width/height of image 49 | self.width = CG.CGImageGetWidth(image) 50 | self.height = CG.CGImageGetHeight(image) 51 | 52 | def pixel(self, x, y): 53 | """Get pixel value at given (x,y) screen coordinates 54 | 55 | Must call capture first. 56 | """ 57 | 58 | # Pixel data is unsigned char (8bit unsigned integer), 59 | # and there are for (blue,green,red,alpha) 60 | data_format = "BBBB" 61 | 62 | # Calculate offset, based on 63 | # http://www.markj.net/iphone-uiimage-pixel-color/ 64 | offset = 4 * ((self.width*int(round(y))) + int(round(x))) 65 | 66 | # Unpack data from string into Python'y integers 67 | b, g, r, a = struct.unpack_from(data_format, self._data, offset=offset) 68 | 69 | # Return BGRA as RGBA 70 | return (r, g, b, a) 71 | 72 | 73 | def get_screenshot(): 74 | sp = ScreenPixel() 75 | sp.capture() 76 | im = Image.frombytes("RGBA", (sp.width, sp.height), sp._data) 77 | b, g, r, a = im.split() 78 | im = Image.merge("RGBA", (r, g, b, a)) 79 | return im -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 2 | __author__ = 'ding' 3 | import time 4 | from common import timer 5 | from itertools import product 6 | from screen import get_matrix, print_matrix, W, H 7 | from mouse import flag 8 | 9 | 10 | around = filter(lambda a: a!= (0, 0), product(xrange(-1, 2), xrange(-1, 2))) 11 | useless = set() 12 | 13 | def get_around_grids(col, row): 14 | ret = [] 15 | for offset in around: 16 | _col, _row = col + offset[0], row + offset[1] 17 | if _col < 0 or _col >= W or _row < 0 or _row >= H: 18 | continue 19 | ret.append((_col, _row)) 20 | return ret 21 | 22 | 23 | def is_valid(matrix, col, row): 24 | ''' 25 | judge if the num of col, row is valid 26 | ''' 27 | num_mine, num_unkown = 0, 0 28 | num = matrix[col][row] 29 | for _col, _row in get_around_grids(col, row): 30 | if matrix[_col][_row] == '*': 31 | num_mine += 1 32 | elif matrix[_col][_row] == 0: 33 | num_unkown += 1 34 | if num_mine > num or num_mine + num_unkown < num: 35 | return False 36 | return True 37 | 38 | 39 | def is_around_valid(matrix, col, row): 40 | for _col, _row in get_around_grids(col, row): 41 | if not 0 < matrix[_col][_row] < 9: 42 | continue 43 | if not is_valid(matrix, _col, _row): 44 | return False 45 | return True 46 | 47 | 48 | def uncertain_grids(matrix, col, row): 49 | if (col, row) in useless: 50 | return [] 51 | 52 | if not 0 < matrix[col][row] < 9: 53 | return [] 54 | 55 | ret = [] 56 | for _col, _row in get_around_grids(col, row): 57 | if matrix[_col][_row] == 0: 58 | ret.append((_col, _row)) 59 | 60 | if not ret: 61 | useless.add((col, row)) 62 | return ret 63 | 64 | 65 | def search(matrix, grids, index, result): 66 | ''' 67 | dfs all available combinations in given grids 68 | ''' 69 | if index == len(grids): 70 | l = [1 if matrix[col][row] == '*' else 0 for col, row in grids] 71 | result.append(l) 72 | return 73 | 74 | col, row = grids[index] 75 | # if it is mine 76 | matrix[col][row] = '*' 77 | if is_around_valid(matrix, col, row): 78 | search(matrix, grids, index+1, result) 79 | matrix[col][row] = 0 80 | # if it is not mine 81 | matrix[col][row] = ' ' 82 | if is_around_valid(matrix, col, row): 83 | search(matrix, grids, index+1, result) 84 | matrix[col][row] = 0 85 | 86 | 87 | def calc(matrix): 88 | ''' 89 | get all mines, safeties and unknow grid's probability 90 | ''' 91 | t1 = time.time() 92 | mines, safeties = set(), set() 93 | probability = {} 94 | 95 | for col in xrange(W): 96 | for row in xrange(H): 97 | candidates = uncertain_grids(matrix, col, row) 98 | if not candidates: 99 | continue 100 | print 'search: ', col, row, candidates 101 | results = [] 102 | search(matrix, candidates, 0, results) 103 | print results 104 | # enumerate results: 105 | # if some grid is mine (safety) in all result, we can affirm the grid is mine (safety) 106 | stat = reduce(lambda a, b: [x+y for x, y in zip(a,b)], results, [0]*len(candidates)) 107 | for candidate, num in zip(candidates, stat): 108 | if num == 0: 109 | safeties.add(candidate) 110 | elif num == len(results): 111 | mines.add(candidate) 112 | elif candidate not in safeties and candidate not in mines: 113 | p = num *1.0 / len(results) 114 | if candidate in probability: 115 | probability[candidate] = max(probability[candidate], p) 116 | else: 117 | probability[candidate] = p 118 | 119 | t2 = time.time() 120 | print 'calc cost %.2fs' % (t2-t1) 121 | return mines, safeties, probability 122 | 123 | def main(): 124 | matrix = None 125 | round = 1 126 | 127 | # get focus 128 | flag(0, 0, False) 129 | time.sleep(0.2) 130 | flag(7, 7, False) 131 | time.sleep(0.5) 132 | 133 | while True: 134 | print '-------- round %d --------' % round 135 | matrix = get_matrix(matrix) 136 | print_matrix(matrix) 137 | mines, safeties, probability = calc(matrix) 138 | print 'mines:', mines 139 | print 'safe:', safeties 140 | 141 | if not mines and not safeties: 142 | if not probability: 143 | break 144 | print 'probability:', probability 145 | items = sorted(probability.items(), key=lambda a:a[1]) 146 | position = items[0][0] 147 | flag(position[0], position[1], False) 148 | 149 | for position in mines: 150 | flag(position[0], position[1], True) 151 | for position in safeties: 152 | flag(position[0], position[1], False) 153 | 154 | round += 1 155 | if round == 20: 156 | break 157 | time.sleep(0.5) 158 | 159 | if __name__ == '__main__': 160 | raw_input('press any key to continue...') 161 | main() --------------------------------------------------------------------------------