微信跳一跳辅助之JAVA版(最容易理解的算法)实现原理分析

上几周更新微信后,进入欢迎界面就提示出让玩一把微信小游戏《跳一跳》。一向不爱玩游戏的我(除了经典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吧。

技术原理

在分别看了排名第一的python版和这个号称最高分为近2万分的代码后,发现他们有一个共同点,那就是截图与模拟点击。通过adb工具将安卓手机连接到电脑后,通过adb来完成这个操作。
用到的命令如下:
1. adb截图命令,通过adb shell执行screencap命令 将手机的当前屏幕进行截屏,并保存在sdcard下的screenshot.png位置

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 来实现

全自动版实现算法

通过上面的介绍,大家应该都知道了如何实现一个半自动的跳一跳辅助了。但身为一名优秀的程序猿,很难摆脱懒惰的本性!
如果不通过人工去寻找棋子A与下一步棋盘的坐标,而是通过程序自动识别那就完美了!使其完成辅助程序的全自动功能。
那么如何让程序通过最简单的方法去找到跳一跳游戏中的棋子和下一步的中心坐标呢?我也不知道,看了大神的JAVA实现代码后感觉他写的这种算法挺简单也挺容易理解的,在这里分享出来,与君共勉。

棋子坐标寻找算法

先观察游戏图片,从中可以得知:棋子的颜色RGB值为404386。那么我们就可以遍历整个图片,获取出棋子这个颜色的坐标集合。

再通过分析,找到棋子中心坐标点。其坐标X中心点大致应为棋子坐标中最小的X与最大的X坐标的中心点,Y坐标应为棋子座标中的最大Y点,也就是最高值。

可得棋子的最终搜索JAVA代码为:
    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;
    }


下一步棋盘中心坐标寻找

棋子中心坐标A点有了,接下来就是下一步棋盘中心坐标B点。
搜索B点与搜索棋子的坐标方法很类似。
不同的是棋子的颜色是固定不变的,棋盘的颜色是可变的。

通过简单分析后,同样的,可将这个问题转化为如下图的数学题:


最后附上 棋子下一步棋盘中心点搜索实现的具体代码:
/**
 * 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<Integer, Integer> 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<Integer, Integer> 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<int[]> 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查阅。
最后感谢大神 burningcl的代码,让我对JAVA图象代码有了基础的了解!感谢大神!

为了学习,我也 burningcl大神在此代码的基础上加入了分数识别功能和一些中文注释,当程序跳到指定的期望的分数时程序将自动退出!项目地址为:   wxJumpHelper


  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

水中加点糖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值