平时偶尔在微信上玩一些小游戏,某天发现一款称之为《最强连一连》的益智游戏,具体玩法概括就是"一笔画"的问题。玩了几关之后随着游戏的格子数量的增加,感觉自己的算力不够用了【汗】于是打算写个脚本用于辅助。
- windows环境下搭建好python编程环境,本人使用python3.8.3版本
- 安装adb(添加到系统环境变量)
- 安卓手机打开usb调试模式,本人手机:小米note3分辨率为1920*1080
- pip 安装好python opencv库
通过对游戏界面截图分析,发现游戏通关过程其实就是从起始点格子(内外层颜色不同的格子)、访问完全部的空白格子。看到这里很容易联想到把所有的格子抽象成N×M的二维数组,我们把不可访问的格子抽象成二维数组中的‘1’,可访问的格子抽象成‘0’,起始点格子抽象成‘S’,然后问题就变成了从二维数组中的‘S’点不重复访问数组中所有的‘0’点。然后通过深度优先算法暴力进行求解、并记录访问顺序。我们把格子相应的坐标与二维数组中的点进行映射,通过遍历保存的路径,然后在屏幕上点击相对应的坐标那么问题就解决了。
游戏运行界面:

经过上一步的分析我们需要获得游戏截图中所有的格子的坐标,提起图像处理那么现在该轮到opencv上场了,大致思路为:
5. 对游戏截图进行裁剪后,预处理灰度变换、滤波去噪点、二值化、膨胀处理
6. 使用findContours函数提取格子外轮廓,去掉边太短、两边差值过大的轮廓,选择相似面积最多的轮廓
7. 根据找到的轮廓按内颜色值不同,确定起始点格子
8. 找到格子轮廓的长与宽,以及轮廓之间的间距
9. 确定轮廓的左上顶点、然后枚举轮廓顶点坐标开始建图
图像预处理之后:

- 使用adb screencap命令对游戏界面进行截图
- 根据截图构建对应的二维数组
- 使用搜索算法求解、记录路径
- 遍历路径,使用adb tap或者touchscreen swipe命令点击相应的屏幕坐标点
- 游戏每次通关或通过累计5关后会弹出一个界面,使用adb tap命令点击相应坐标进入下一关
建图
"""
===================================
-*- coding:utf-8 -*-
Author :GadyPu
E_mail :Gadypy@gmail.com
Time :2020/9/20 0016 上午 11:44
FileName :create_map.py
===================================
"""
import cv2
import numpy as np
from find_path import FindPath
class CreateMap(object):
def __init__(self, img_path: str = '', cut_size: tuple = (320, 1600)):
self.img_path = img_path
self.cut_size = cut_size
self.points = []
self.start_ptn = None
self.x_min = 0
self.y_min = 0
def get_rect_area(self, point: tuple):
return abs(point[0] - point[2]) * abs(point[1] - point[3])
def img_process(self):
img = cv2.imdecode(np.fromfile(self.img_path, dtype = np.uint8), cv2.IMREAD_COLOR)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
gray = gray[self.cut_size[0]: self.cut_size[1], :]
media_blur = cv2.medianBlur(gray, 3)
thres = cv2.threshold(media_blur, 180, 255, cv2.THRESH_BINARY)[1]
dila = cv2.dilate(thres, (3, 3))
contours = cv2.findContours(dila, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)[0]
for c in contours:
#cv2.drawContours(img, [c], -1, (0, 0, 255), 2)
x, _x, y, _y = -1, 9999, -1, 9999
for [i] in c:
x = max(x, i[0])
_x = min(_x, i[0])
y = max(y, i[1])
_y = min(_y, i[1])
# 边太短的矩形丢弃
if abs(_x - x) < 40 or abs(_y - y) < 40:
continue
# 两边差值的绝对值控制在5以内
if abs(abs(_x - x) - abs(_y - y)) < 5:
self.points.append((_x, _y + self.cut_size[0], x, y + self.cut_size[0]))
# 寻找相似面积最多的轮廓
freq, freq_area = -1, -1
for i in range(len(self.points)):
area = self.get_rect_area(self.points[i])
count = 1
for j in range(len(self.points)):
if j == i:
continue
_area = self.get_rect_area(self.points[j])
if abs(area - _area) < 500:
count += 1
if count > freq:
freq = count
freq_area = area
point_temp = []
for i in self.points:
area = self.get_rect_area(i)
if abs(freq_area - area) < 500:
point_temp.append(i)
self.points.clear()
self.points = point_temp
self.points.reverse()
# 寻找起始点(内外颜色值不同)
count = { }
temp = None
for i in self.points:
x, y = (i[0] + i[2]) // 2, (i[1] + i[3]) // 2
d = tuple(img[y][x])
if d in count.keys():
count.pop(d)
temp = d
elif d != temp:
count[d] = i
for i in count:
self.start_ptn = count[i]
# cv2.imshow('', img)
# cv2.waitKey(0)
# cv2.destroyAllWindows()
def get_map_size(self):
x, _x, y, _y = 9999, -1, 9999, -1
for i in self.points:
x = min(x, i[0])
y = min(y, i[1])
_x = max(_x, i[2])
_y = max(_y, i[3])
self.x_min = x
self.y_min = y
dif_x, dif_y = 0, 0
H, W = self.points[0][3] - self.points[0][1], self.points[0][2] - self.points[0][0]
n = (_x - x) // W
m = (_y - y) // H
ptn = self.points[0]
for i in range(1, len(self.points)):
if abs(ptn[1] - self.points[i][1]) < 3:
dif_x = self.points[i][0] - ptn[2]
if dif_x > W:
continue
else:
break
else:
ptn = self.points[i]
dif_y = dif_x
if n * W + (n - 1) * dif_x > _x - x + n * 2:
n -= 1
if m * H + (m - 1) * dif_y > _y - y + m * 2:
m -= 1
return m, n, dif_x, dif_y, H, W
def creat_grid(self):
m, n, dif_x, dif_y, H, W = self.get_map_size()
grid = [[('0', (0, 0)) for _ in range(n)] for _ in range(m)]
for i in range(m):
for j in range(n):
_find = False
p1 = self.x_min + (dif_x + W) * j
p2 = self.y_min + (dif_y + H) * i
center_point = (None, None)
for ptn in self.points:
if abs(p1 - ptn[0]) < 5 and
abs(p2 - ptn[1]) < 5:
_find = True
center_point = (ptn[0] + ptn[2]) // 2, (ptn[1] + ptn[3]) // 2
if ptn == self.start_ptn:
grid[i][j] = ('S', center_point)
break
if grid[i][j][1] == (0, 0):
grid[i][j] = ('0' if _find else '1', center_point)
#print(grid)
return grid
def build_map(self, img_path):
self.img_path = img_path
self.points = []
self.start_ptn = None
self.x_min = 0
self.y_min = 0
self.img_process()
return self.creat_grid()
if __name__ == '__main__':
import time
t = time.time()
d = CreateMap()
grid = d.build_map('01.png')
p = FindPath()
print(p.find_path(grid))
print(time.time() - t)
寻找路径
"""
===================================
-*- coding:utf-8 -*-
Author :GadyPu
E_mail :Gadypy@gmail.com
Time :2020/9/20 0013 下午 07:59
FileName :find_path.py
===================================
"""
import copy
class FindPath(object):
def __init__(self):
self.dx = [0, 0, -1, 1]
self.dy = [-1, 1, 0, 0]
self.ret_path = []
self.blank = 0
self.start_x = -1
self.start_y = -1
def find_path(self, grid: list):
H, W = len(grid), len(grid[0])
vis = [[False for _ in range(W)] for _ in range(H)]
self.blank = 0
self.ret_path = []
for i in range(H):
for j in range(W):
if grid[i][j][0] == 'S':
self.start_x, self.start_y = i, j
vis[i][j] = True
elif grid[i][j][0] == '0':
self.blank += 1
self.dfs(self.start_x, self.start_y, grid, vis, H, W, 0, self.blank, [])
self.ret_path.insert(0, grid[self.start_x][self.start_y][1])
return self.ret_path
def dfs(self, x: int, y: int, grid: list, vis: list, H :int, W: int, step: int, tot: int, res: list):
if step == tot:
self.ret_path = copy.deepcopy(res)
print('find a answer!')
return None
for i in range(4):
_x, _y = x + self.dx[i], y + self.dy[i]
if _x < 0 or _y < 0 or _x >= H or _y >= W:
continue
if grid[_x][_y][0] == '0' and not vis[_x][_y]:
vis[_x][_y] = True
res.append(grid[_x][_y][1])
self.dfs(_x, _y, grid, vis, H, W, step + 1, tot, res)
vis[_x][_y] = False
res.pop()
if __name__ == '__main__':
pass
主程序
"""
===================================
-*- coding:utf-8 -*-
Author :GadyPu
E_mail :Gadypy@gmail.com
Time :2020/9/20 0016 上午 11:51
FileName :AutoRun.py
===================================
"""
import time
import signal
import os
from create_map import CreateMap
from find_path import FindPath
g_exit = True
class AutoRun():
def __init__(self):
self.get_map = CreateMap()
self.get_path = FindPath()
self.next_level_point = (536, 1417)
self.ad_point = (930, 660)
def handler(self, signum, frame):
global g_exit
g_exit = False
print('接收到ctrl c信号程序退出')
def run(self):
signal.signal(signal.SIGINT, self.handler)
signal.signal(signal.SIGTERM, self.handler)
level = 1
while g_exit:
os.system('adb shell screencap -p /sdcard/Download/01.png')
os.system('adb pull /sdcard/Download/01.png')
time.sleep(1)
grid = self.get_map.build_map('01.png')
path = self.get_path.find_path(grid)
print(path)
if not path or len(path) == 1:
time.sleep(0.2)
continue
for i in range(len(path) - 1):
x0, y0, x1, y1, t = path[i][0], path[i][1], path[i + 1][0], path[i + 1][1], 50
os.system("adb shell input touchscreen swipe %d %d %d %d %d" % (x0, y0, x1, y1, t))
time.sleep(1.5)
if level % 5 == 0:
os.system(f'adb shell input tap {self.ad_point[0]} {self.ad_point[1]}')
time.sleep(0.5)
os.system(f'adb shell input tap {self.next_level_point[0]} {self.next_level_point[1]}')
level += 1
os.system(f'adb shell input tap 0 0')
print('程序运行中...')
if __name__ == '__main__':
d = AutoRun()
d.run()
每台手机分辨率不一样,若要在其他机型上运行需要修改相应的坐标点。
adb tap命令执行太慢了每通关一关差不多要20s左右,到现在也没有啥好的办法。
