RPA+OpenCV实现连连看游戏自动化
看到身边有人沉迷连连看微信小游戏,经典的拉高难度 + 看广告获取道具的套路,想过一关得要看个好多次 30s 广告
于是想验证一下关卡的砖块布局是否有可行的解法,如果有的话就通过 RPA (Robotic process automation) 自动化操作来通关
架构设计
用到的库主要是以下两个:
- pyautogui 用于获取屏幕截图、模拟鼠标操作
- opencv-python 用于砖块图案的识别
此外还涉及到 numpy
/pillow
/matplotlib
/scipy
这些数据与图像处理的库就不一一介绍了
环境为 Windows + Python3.10 + PC端微信小游戏
具体实现三个核心模块:
CV
类调用opencv
中的算法实现砖块的识别,生成游戏场景的对应的数据结构Board
类实现游戏场景的逻辑,并通过算法寻找可行的解法Operator
类实现与游戏窗口的交互,并调用上述两个类的方法生成操作序列来完成最终的自动化通关
实现流程
定位工具
由于后续的截图与点击操作都是基于屏幕坐标的,所以首先实现一个简单的定位工具,用于获取游戏窗口内特定位置坐标信息
import cv2 |
通过在需要获取坐标定位的时候可以抛出自定义的 StopException
异常,通过在弹出的窗口中点击模板位置获取相对坐标
pyautogui
库的 screenshot
方法返回的是 PIL
的 Image
对象,在传递给 opencv
的方法前需要先进行转换
游戏场景识别
有了游戏窗口内的坐标信息,就可以完成游戏场景的识别了,下面实现 CV
类将游戏场景截图转换为对应的数据结构
- 计算砖块图像单元尺寸
- 切分图像单元
- 计算图像单元的图像特征,通过特征相似度来判断是否为同一种砖块,并赋予类型 id
import time |
在截图前先激活游戏窗口并延迟一段时间,以保证截图不会被窗口切换等操作影响
计算单元格尺寸
通过游戏场景的画面尺寸计算出单元格的尺寸,以及单元格之间的间距,便于后续切分图像
这里简单的使用 scipy
库求解一个二元一次方程组即可
import scipy.optimize as opt |
切分单元格
使用 pillow
库的 Image.crop
方法切分图像,这里为了避免边缘的干扰,对每个单元格还进行了一定的裁剪
class CV(object): |
匹配单元格图案
通过计算单元格的图像特征,通过相似度来判断是否为同一种砖块,并赋予类型 id
由于这里的图案相似度较高,所以直接使用直方图相似度即可:判断两个图片中像素值的分布是否相似
class CV(object): |
这里有几个需要注意的地方:
- 为了避免重复计算,使用了一个缓存字典来存储图像的特征
- 为了避免图像中的噪声干扰,计算直方图特征时
bin
的数量设置为 64,即将原本为 256 颜色空间划分为 64 个区间,计算每个区间的像素数量 - 为了加快计算速度,分别计算了 RGB 三个通道的直方图特征后拼接在一起,这样特征维度是 64*3=192;如果同时计算三个通道,特征维度就是 64*64*64=262144,计算量会大大增加
- 为了提高不同图案的区分度,将直方图中像素数量最多的三个区间的像素数量置为 0,降低相同的背景颜色带来的影响
- 为了便于使用阈值判断相似度,对特征分布进行了归一化处理,并使用了
cv2.HISTCMP_CORREL
即相关系数作为相似度的计算方法,0 表示完全不相关,1 表示完全相关
比如其中一个图像单元与对应的直方图特征为:
可视化
为了方便调试,可以将识别结果可视化输出,检查是否正确识别了砖块的图案
class CV(object): |
游戏逻辑模拟
有了游戏场景的数据结构,就可以实现游戏逻辑的模拟了,下面实现 Board
类
- 通过矩阵操作模拟砖块的消除与移动
- 通过广度优先搜索来寻找符合消除条件的砖块
- 通过深度优先搜索来寻找可行的解法
class Board(object): |
这里的 grid
是一个 numpy
二维数组,每个元素是一个砖块的类型 id,空缺处为 None
模拟消除
大部分关卡在消除砖块后会向某一方向移动填充空缺,这里加入了额外的 gravity
参数来控制这一过程
另外为了便于后续的搜索过程,需要记录每一步的操作序列以便回退
class Board(object): |
在 apply_gravity
方法中,通过 numpy
的 flipud
/rot90
函数来实现矩阵的翻转与旋转,以简化不同方向的填充操作
寻找可消除砖块
通过广度优先搜索来寻找符合消除条件的砖块,根据游戏规则,两个砖块之间的路径不能有超过两个拐角
from collections import deque |
这里的几个需要注意的地方:
- 连接砖块的路径可以在外围,因此寻找路径时需要在原有矩阵的基础上进行扩展,这里使用了
numpy
的pad
函数在矩阵的外围填充了一圈None
- 搜索可消除砖块时优先选择与填充方向相反的砖块,可以更快的找到可行的解法:填充可能会导致原本可消除的砖块不再可消除,因此优先消除对砖块布局影响较小的砖块
- 广度优先搜索时需要记录路径的拐角次数,由于开始时可向任意方向移动,因此起点拐角次数初始化为
-1
寻找可行解法
class LimitExceededException(Exception): |
普通的深度优先搜索,当搜索到不可再消除时回退到上一步
由于搜索的深度很大,因此超过一定失败次数后直接停止搜索,返回能达到最大深度的解法
自动化操作
有了操作的序列,就可以实现自动化操作了,在 Operator
类中进行具体的模拟操作
添加了一个弹窗在开始时选择重力方向,以便后续模拟计算
class Operator(object): |
那么问题出现了:
- 场景中有些宝箱砖块,消除后会弹出看广告获取道具的界面,这时候需要额外的操作来关闭这个界面
- 当有填充方向时,布局极大概率是无解的,这时候需要通过点击看广告获取刷新道具来打乱布局
处理宝箱砖块
这里使用另一种图像特征算法来识别宝箱砖块:SIFT(Scale-Invariant Feature Transform) 尺度不变特征变换
class CV(object): |
提供一张宝箱砖块的图片,通过计算与每个单元图片间 SIFT
算子的匹配度,选择相似度最高的单元返回其坐标
在序列操作时消除宝箱砖块后额外点击指定位置关闭弹窗即可
class Operator(object): |
刷新布局
当布局无解时,在尽可能消除砖块后,自动点击刷新道具看广告后,重新识别布局求解消除路径,直至全部消除
class Board(obejct): |
这里打乱布局后砖块位置保持不变但图案改变,因此在循环中通过 mask
参数来过滤掉已经消除的砖块,
Game Over
比起直接玩游戏,写一个自动化脚本来通关可能更有意思也更有意义一些
就这个游戏本身来说,在自动生成布局时特意不保证可解性,往往一个关卡需要看 3-5 次广告才能通关,可见这类游戏的背后盈利模式
完整代码见 GitHub