上几周更新微信后,进入欢迎界面就提示出让玩一把微信小游戏《跳一跳》。一向不爱玩游戏的我(除了经典QQ飞车、CS外),当时抱着没兴趣的态度简单看了下,没有玩。与朋友玩耍时,常听他们聊起这个小游戏,偶尔也在网页和微信公众号上看见些关于这个小游戏的一些话题,为了不落伍,我决定继续随大流一把。
于是乎玩了几把后,发现自己最高分才30来分,感觉咋这么容易就挂了,而打开朋友圈排名一看,咋都这么牛逼,居然有人能玩好几大百。于是问了下朋友,了解下有没有什么技巧,他们告诉我说有外挂(心里对那些玩的分数挺高的就没有那么崇拜了),于是乎我就在网上搜索了下关于跳一跳的相关外挂,看了下可谓是琳琅满目,啥语言的都有。
我先下了个github上排名第一的(地址为:https://github.com/wangshub/wechat_jump_game),某大神是用python写的,然后我快速过了遍,对于实现的算法部分没看懂,很尴尬。通过这件事让我对python的强悍又有了重新的认知,不愧是短小精悍的语言!
然后我暂时放弃了python版本的,选择了一个我目前最熟悉的语言版本的:JAVA
从github的列表中我迅速锁定了一个标题名为:腾讯微信跳一跳破解(目前最高19844分)的版本(地址为:https://github.com/burningcl/wechat_jump_hack),决定把它下下来,尝试一把,不愧又是大神写的,一把下来看着分数差不多了,在800来分时,我手动把它停掉了,据说如果跳的分数太高,不容易传上去。
顺利打下NO.1,首次装逼成功!
作为一名充满探索精神的程序猿,我决定还是要弄清其原理,因为这个小辅助看起来如此简单,之前我也从来没有对这种小应用研究过,于是我决定一定要搞明白这个JAVA版本跳一跳辅助的原理,就算是我的游戏辅助的helloWorld吧。
adb shell /system/bin/screencap -p /sdcard/screenshot.png
2.adb拉取图片命令,通过adb的pull命令拉取手机位于sdcard/screenshot.png的图片到电脑中
adb pull /sdcard/screenshot.png
3.adb滑动命令,通过input swipe命令去模拟滑动,其参数的意思为startX,startY,endX,endY,duration,也就是模拟触摸屏幕的开始与结束的横竖坐标,最后的参数duration代表按下的时间毫秒值,时间越短代表按的时间也就越少。
shell input swipe %d %d %d %d %d
通过以上分析,我们可以得知,在这个小辅助中起着最重要的命令是第3个swipe命令了。那么如何计算swipe中按下的值呢?
先观察下游戏,简单分析后,可化为如下初中数学题:
已知A、B两点。A点坐标为(startX,startY),B点坐标为(endX,endY),棋子速度为V(像素/毫秒)。
求棋子要从A点到达B点的时间。
看到这个问题是不是感觉很简单,求出两点间的距离S,再乘以速度V就搞定了!
两点间的距离直接用中学学的两点间的距离公式即可.如果忘了就百度下,比如这样:
通过上面的分析后,想必每位都已经明白了所谓跳一跳外挂的基本原理了。
如果你会安卓开发,那么就完全能用做出一个“半自动”的跳一跳辅助了,通过WindowManager在小程序的最上层加入一个自定义的层,然后用户通过最外层的点击来获取两点间的距离,然后再通过计算,算出距离所要花的时间,再调用input touchscreen swipe命令即可。
另外,这个跳一跳小程序让我想到了传说中的微信自动抢红包利器,它是基于AccessibilityService 实现的。单从整体看感觉和这个跳一跳差不多,仔细一想仿佛知道原因了。AccessibilityService 只能获取出安卓的控件,像view,各种layout这样的,而小程序这种应该获取不出来,所以就不能通过AccessibilityService 来实现
public static final int R_TARGET = 40;
public static final int G_TARGET = 43;
public static final int B_TARGET = 86;
public int[] find(BufferedImage image) {
if (image == null) {
return null;
}
int width = image.getWidth();
int height = image.getHeight();
int[] ret = {0, 0};
int maxX = Integer.MIN_VALUE;
int minX = Integer.MAX_VALUE;
int maxY = Integer.MIN_VALUE;
int minY = Integer.MAX_VALUE;
for (int i = 0; i < width; i++) {
for (int j = height / 4; j < height * 3 / 4; j++) {//提高搜索速度,因为棋子只会存在于整个图的中部位置
int pixel = image.getRGB(i, j);
int r = (pixel & 0xff0000) >> 16;
int g = (pixel & 0xff00) >> 8;
int b = (pixel & 0xff);
if (ToleranceHelper.match(r, g, b, R_TARGET, G_TARGET, B_TARGET, 16)) {
maxX = Integer.max(maxX, i);
minX = Integer.min(minX, i);
maxY = Integer.max(maxY, j);
minY = Integer.min(minY, j);
}
}
}
ret[0] = (maxX + minX) / 2 +3;
ret[1] = maxY;
System.out.println(maxX + ", " + minX);
System.out.println("pos, x: " + ret[0] + ", y: " + ret[1]);
return ret;
}
/**
* desc:棋盘位置搜索
*/
public class BoardPositionSearcher implements PositionSearcher {
private Logger logger = LoggerFactory.getLogger(this.getClass());
private BottleFinder bottleFinder = new BottleFinder();
private int[] myPos;
public int[] getMyPos() {
return myPos;
}
public void setMyPos(int[] myPos) {
this.myPos = myPos;
}
@Override
public int[] seach(BufferedImage screenShotImg) {
if (screenShotImg == null) {
return null;
}
int width = screenShotImg.getWidth();
int height = screenShotImg.getHeight();
//先获取出0 200这一点的像素,即顶部某的一点
int pixel = screenShotImg.getRGB(0, 200);
int r1 = (pixel & 0xff0000) >> 16;
int g1 = (pixel & 0xff00) >> 8;
int b1 = (pixel & 0xff);
Map map = new HashMap<>();
//一列一列地搜索,在map中放入遍历点像素在这一行中出现的次数
for (int i = 0; i < width; i++) {
pixel = screenShotImg.getRGB(i, height - 1);
map.put(pixel, map.getOrDefault(pixel, 0) + 1);
}
//获取出存在于map中像素出现次数最多的像素
int max = 0;
for (Map.Entry entry : map.entrySet()) {
if (entry.getValue() > max) {
pixel = entry.getKey();
max = entry.getValue();
}
}
int r2 = (pixel & 0xff0000) >> 16;
int g2 = (pixel & 0xff00) >> 8;
int b2 = (pixel & 0xff);
//获取出游戏背景从顶到底的颜色RGB值
int t = 16;
int minR = Integer.min(r1, r2) - t;
int maxR = Integer.max(r1, r2) + t;
int minG = Integer.min(g1, g2) - t;
int maxG = Integer.max(g1, g2) + t;
int minB = Integer.min(b1, b2) - t;
int maxB = Integer.max(b1, b2) + t;
logger.trace(minR + ", " + minG + ", " + minB);
logger.trace(maxR + ", " + maxG + ", " + maxB);
int[] ret = new int[6];
int targetR = 0, targetG = 0, targetB = 0;
boolean found = false;
//遍历寻找棋盘顶点坐标,在棋子坐标上方搜索,从游戏背景4分之一处开始搜索,提高速度
for (int backgroundY = height / 4; backgroundY < myPos[1]; backgroundY++) {
for (int backgroundX = 0; backgroundX < width; backgroundX++) {
int dx = Math.abs(backgroundX - myPos[0]);
int dy = Math.abs(backgroundY - myPos[1]);
if (dy > dx) {
//如果这一点到棋子X的距离比这一点到棋子Y的距离小,就跳出循环, WHY?
continue;
}
//获取出扫描这一点的RGB值
pixel = screenShotImg.getRGB(backgroundX, backgroundY);
int r = (pixel & 0xff0000) >> 16;
int g = (pixel & 0xff00) >> 8;
int b = (pixel & 0xff);
//如果这一点的RGB值不在屏幕背景色的区间内
if (r < minR || r > maxR || g < minG || g > maxG || b < minB || b > maxB) {
ret[0] = backgroundX;
ret[1] = backgroundY;
//则下一步的顶点坐标为这个点
logger.trace("top, x: " + backgroundX + ", y: " + backgroundY);
//遍历这个点向下5个高度的像素
for (int k = 0; k < 5; k++) {
pixel = screenShotImg.getRGB(backgroundX, backgroundY + k);
targetR += (pixel & 0xff0000) >> 16;
targetG += (pixel & 0xff00) >> 8;
targetB += (pixel & 0xff);
}
//取出这个点的像素平均值
targetR /= 5;
targetG /= 5;
targetB /= 5;
found = true;
break;
}
}
if (found) {
break;
}
}
//判断是否为白点
if (targetR == BottleFinder.TARGET && targetG == BottleFinder.TARGET && targetB == BottleFinder.TARGET) {
return bottleFinder.find(screenShotImg, ret[0], ret[1]);
}
boolean[][] matchMap = new boolean[width][height];
boolean[][] vMap = new boolean[width][height];
ret[2] = Integer.MAX_VALUE;
ret[3] = Integer.MAX_VALUE;
ret[4] = Integer.MIN_VALUE;
ret[5] = Integer.MAX_VALUE;
Queue queue = new ArrayDeque<>();
queue.add(ret);
while (!queue.isEmpty()) {
int[] item = queue.poll();
int i = item[0];
int j = item[1];
if (j >= myPos[1]) {
// 已搜索到棋子的Y值位置了,结束本次搜索,跳出循环
continue;
}
if (i < Integer.max(ret[0] - 300, 0) || i >= Integer.min(ret[0] + 300, width) || j < Integer.max(0, ret[1] - 400) || j >= Integer.max(height, ret[1] + 400) || vMap[i][j]) {
//对于距离棋子坐标太远的跳出循环(即棋子坐标左、右、上、下的坐标),以及已经搜索过的坐标也跳出循环
continue;
}
vMap[i][j] = true;
pixel = screenShotImg.getRGB(i, j);
int r = (pixel & 0xff0000) >> 16;
int g = (pixel & 0xff00) >> 8;
int b = (pixel & 0xff);
//将每一个坐标点与棋盘顶点的RGB值比较
matchMap[i][j] = ToleranceHelper.match(r, g, b, targetR, targetG, targetB, 16);
if (i == ret[0] && j == ret[1]) {
logger.trace(matchMap[i][j] + "");
}
//如果在棋盘面上
if (matchMap[i][j]) {
//获取出最左边的棋盘坐标
if (i < ret[2]) {
ret[2] = i;
ret[3] = j;
}//获取最上的棋盘坐标
else if (i == ret[2] && j < ret[3]) {
ret[2] = i;
ret[3] = j;
}
//获取出最右边的棋盘坐标
if (i > ret[4]) {
ret[4] = i;
ret[5] = j;
}//获取出最上的棋盘坐标
else if (i == ret[4] && j < ret[5]) {
ret[4] = i;
ret[5] = j;
}
//获取出最上的坐标点
if (j < ret[1]) {
ret[0] = i;
ret[1] = j;
}
//将目标点左右上下的点放入队列中,实现递归
queue.add(buildArray(i - 1, j));
queue.add(buildArray(i + 1, j));
queue.add(buildArray(i, j - 1));
queue.add(buildArray(i, j + 1));
}
}
logger.trace("left, x: " + ret[2] + ", y: " + ret[3]);
logger.trace("right, x: " + ret[4] + ", y: " + ret[5]);
return ret;
}
private int[] buildArray(int i, int j) {
int[] ret = {i, j};
return ret;
}
}
完整代码请移步大神的github查阅。