1. 程式人生 > >微信跳一跳輔助之JAVA版(最容易理解的演算法)實現原理分析

微信跳一跳輔助之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