程序员如何玩成语猜猜看

1. 背景

随着前段时间微信的更新,小程序的热度又上了一个台阶;最近我发现微信亲戚群里面充斥着一个叫《成语猜猜看》的小游戏。本着学习研究的目的,我对这个小游戏进行了一些探索。

2. 玩法介绍

游戏玩法非常简单:根据图片的提示,从下面的散落的汉字中选出4个,组成正确的成语即可。
游戏分了很多等级,每个等级对应了若干关卡,随着等级的提高,关卡的数量也相应提高,当前是【御史】第27关。

成语猜猜看

3. 简单的分析

要实现这个小游戏本身很简单。
我的思路是:

  • 首先后端存有相当数量的成语,作为题目,并且每个成语都对应一个图片;
  • 后端提供一个获取题目的接口。
    参数:当前关卡;
    返回:(1)数组:当前关卡的备选答案(散落的汉字)(2)提示的图片
  • 后端提供一个提交答案的接口。
    参数:当前关卡,答案
    返回:答案是否正确
  • 前端根据根据当前的关卡获取题目,用户点选了4个汉字之后,提交答案。

于是游戏的主体就实现了。

4. 一些实践

(1)尝试抓取数据包

既然小游戏涉及了前后端的交互,那么我们就可以通过抓取数据包来分析。
基于以往对微信小程序开发的经验来看,微信限制了小程序的网络请求必须使用https协议,因此我使用了 Fildder 并做了相关配置之后,来抓取手机微信的https请求。
然而让我感到意外的是,在确保了 Fiddler 可以侦听到手机的 https 请求的情况下,我没有发现任何疑似成语猜猜看小游戏发出的数据包,包括了WebSocket。

难道这竟是个单机游戏?!!!

可是单击游戏要怎么有效得存储用户的游戏进度、金币数量这些敏感信息呢?

(2)小程序源码的获取

前段时间听说由于微信的漏洞,可以通过构造 url 获得任意微信小程序的源码,但是现在这个漏洞已经修复了,这个方法看来行不通。

换一个思路
在微信小程序的开发文档上看到过一句话:微信在运行小程序前,将小程序的包下载到手机里。
换句话说,这个小游戏的包,就在我手机里面,不需要再去想办法下载;通过在网上查找资料,最终确定了文件的位置:/data/data/com.tencent.mm/MicroMsg/ae7bf444d1f1cd061ed448cc1d581daa/appbrand/pkg/,文件包的格式为.wxapkg,所幸也有网友提供了解析该文件的方法:unpack wxapkg

于是便得到了小游戏的源码

源码的结构大致如下:


源码

首先将源代码格式化一下,不然没法看。

经过简要的分析发现游戏的主逻辑都在app-service.js这个文件里面,下面主要分析分析这个文件:
打开这个文件,首先映入眼帘的就是“相当数量的成语”,等级、成语解释、成语对应的图片连接,即图中的LEVEL_NAMES, ALL_IDIOM以及后面的几个数组。

app-service.js

验证了之前的猜想:这果然是个单击游戏。
那么他就只能将用户数据保存在本地了,看另一段代码:

App({
    globalData: {
        userInfo: "",
        PASS_LEVELS: "PassLevels",
        CURRENT_LEVELS: "CurrentLevels",
        TOTAL_POINT: "TotalPoint",
        LAST_SIGNIN: "LastSignin",
        TOTAL_SIGNIN_COUNT: "TotalSigninCount",
        SHARE_TIME: "ShareTime",
        SHARE_COUNT: "ShareCount"
    },
    onLaunch: function () {
        "" == wx.getStorageSync(this.globalData.PASS_LEVELS) && wx.setStorageSync(this.globalData.PASS_LEVELS, 1),
            "" == wx.getStorageSync(this.globalData.CURRENT_LEVELS) && wx.setStorageSync(this.globalData.CURRENT_LEVELS,
            1), "" == wx.getStorageSync(this.globalData.TOTAL_POINT) && wx.setStorageSync(this.globalData.TOTAL_POINT,
            300), "" == wx.getStorageSync(this.globalData.LAST_SIGNIN) && wx.setStorageSync(this.globalData.LAST_SIGNIN,
            0), "" == wx.getStorageSync(this.globalData.TOTAL_SIGNIN_COUNT) && wx.setStorageSync(this.globalData.TOTAL_SIGNIN_COUNT,
            0), "" == wx.getStorageSync(this.globalData.SHARE_TIME) && wx.setStorageSync(this.globalData.SHARE_TIME,
            0), "" == wx.getStorageSync(this.globalData.SHARE_COUNT) && wx.setStorageSync(this.globalData.SHARE_COUNT,
            0)
    },
    getLevel: function (t) {
        this.gloalData.level
    }
});

看到 globalData 中各个字段和名称以及 wx.getStorageSync方法,一切都明朗起来了:小游戏通过微信的接口,将用户数据存储在本地,下下次启动时在读取回来,以继续游戏。

(3)从源码中能得到什么?
  1. 所有关卡的答案 : ALL_IDIOMS,第level关的答案为ALL_IDIOMS[level-1]
  2. 如何通过用户的等级和当前等级的关数,计算总关数level。
    比如御史第27关,总关数是多少呢,这其中的计算规则又是什么?看下面的代码:
onLoad: function () {
    this.data.cur_level = Number(wx.getStorageSync(getApp().globalData.CURRENT_LEVELS));
    var a = Number(wx.getStorageSync(getApp().globalData.TOTAL_POINT));
    this.data.cur_level < 1 && (this.data.cur_level = 1);
    var e = "http://xcxcy.oss-cn-hangzhou.aliyuncs.com/cycck/res/obj_" + this.data.cur_level + ".jpg";
    this.data.cur_level > 501 && (e = "http://cyktc.oss-cn-beijing.aliyuncs.com/cyimg_rename/" + t.PIC[this.data
        .cur_level - 502]);
    var s;
    for (s = 1; s < 15 && !(this.data.cur_level < (2 * s + 1) * (2 * s + 1)); s++);
    var o = "http://cyktc.oss-cn-beijing.aliyuncs.com/level/level_" + s + ".png";
    this.getRandomArr();
    for (var r = [], n = 0; n < 4; n++) r[n] = 24;
    for (var i = [], n = 0; n < 24; n++) i[n] = !0;
    var l = !1;
    try {
        var c = wx.getSystemInfoSync();
        console.log(c.model), -1 != c.model.search("iPhone") || (l = !0)
    } catch (t) {}
    this.data.cur_level < 30 && (l = !1), this.setData({
        ans: r,
        array_show: i,
        main_img_url: e,
        level_icon_url: o,
        total_point: a,
        pointAdd: l
    })
},

该段代码应该是做了游戏开始时的初始化工作,其中就包含了将当前总关数,转换为等级 + 当前关数的方法,重点关注这个循环:

for (s = 1; s < 15 && !(this.data.cur_level < (2 * s + 1) * (2 * s + 1)); s++);
var o = "http://cyktc.oss-cn-beijing.aliyuncs.com/level/level_" + s + ".png";

合理猜测this.data.cur_level为当前的总关数,而s代表的用户当前的等级,我们看看当s取不同值时,下面的url是什么:

s = 1

通过这个循环得到了换算的方法:总过关数 = (2*当前等级 - 1)^2 + 当前等级的关数,比如御史第27关:
LEVEL_NAMES = ["学童", "童生", "秀才", "举人", "贡士", "进士", "翰林", "侍郎", "尚书", "大学士", "御史", "丞相", "太子少师", "太子太师"]
御史在LEVEL_NAMES中为第11项,所以当前等级=11
总过关数 = (2*11 - 1)^2 + 27 = 468
那么该关的答案为ALL_IDIOMS[468-1] = 斗折蛇行

5. 一个自动答题的脚本

到目前为止,我们可以做到通过当前的等级以及关数,计算出答案。如果更进一步,如何实现一个自动答题的脚本?

思路:

  1. 首先手动指定起始关卡:grade(等级)cur_level(当前等级的关数)。例如御史27关,grade = 御史cur_level = 27
  2. 通过gradecur_level 计算出当前的总关数level,公式为:
    level = (2*grade_index - 1)^2 + cur_level
    其中 grade_indexgradeLEVEL_NAMES 数组中的下标。例如御史第27关:level = (2 * 11 - 1)^2 + 27 = 468
  3. 通过level得到答案 ansans = ALL_IDIOMS[level - 1],那么御史27关答案为ALL_IDIOMS[468-1] = 斗折蛇行
  4. 在散落的汉字中找出答案,并依次点击,就可通过该关卡。
  5. 进入下一关,level = level + 1, 跳到第 3 步。

现在关键在于实现第 4 步。

第 4 步怎么做:
(1)利用 adb 相关命令获取手机截图,模拟点击等操作;
(2)使用 opencv 识别出截图中文字,从而判断如何点击;

抄起 Python 开始干

(1)首先引入 LEVEL_NAMES 和 ALL_IDIOMS
新建文件all_idioms.py,将这两个数组拷贝进来(这里ALL_IDIOMS中省略了大部分内容)

# 等级计算: 总过关数 - (2*(当前等级-1) + 1)^2 = 当前等级的关数
LEVEL_NAMES = ["学童", "童生", "秀才", "举人", "贡士", "进士", "翰林", "侍郎", "尚书", "大学士", "御史", "丞相", "太子少师", "太子太师"]
ALL_IDIOMS = [
"浓眉大眼","一本正经","长话短说","五颜六色","因小失大","一心两用","历历在目","羊入虎口","欺上瞒下","一日三秋"......]

(2)新建程序的主脚本 hick_idioms.py

def main():
    start_level_name = input("起始等级: ")
    start_cur_level = input("起始关: ")
    # 获得起始的关卡
    start_level = getTotalLevel(start_level_name, int(start_cur_level))

    while True:
        #随便点击一个位置,这是因为每过一关以后会弹出一个对话框,点击一次是为了消除对话框
        tap(200, 500)
        time.sleep(1)
        # 打印出这一关的答案
        print(ALL_IDIOMS[start_level])
        # 根据这一关的答案,生成用于匹配的图片模板,即将四个字转换成图片。
        generateTemplate(ALL_IDIOMS[start_level])
        #依次次匹配这四个字
        for i in range(4):
            refreshPic()
            img_rgb = cv2.imread("screen.png")
            img_gray = cv2.cvtColor(img_rgb, cv2.COLOR_BGR2GRAY)

            template = cv2.imread('%d.png' % i, 0)
            w, h = template.shape[::-1]
            res = cv2.matchTemplate(img_gray, template, cv2.TM_CCOEFF_NORMED)
            loc = np.where(res >= 0.6)
            pts = list(zip(*loc[::-1]))
            #如果匹配到了多个点,则随机选取一个(这是为了后面的纠错,若每次都选取固定的点,程序会陷入死循环)
            pt = pts[random.randint(0,len(pts)-1)]
            tap(pt[0], pt[1] + 1320)
        # 判断是否成功
        if isSuccess():
            #成功了进入下一关
            start_level = start_level + 1
        else:
            #失败了则撤回已经点选的答案,重新开始这一关,因为前面随机选取答案的机制,第一次没选对,第二次就有可能会选对......
            tap(325,1175)
            tap(425,1175)
            tap(550,1175)
            tap(675,1175)
if __name__ == '__main__':
    main()

程序中还有的其他函数实现没有给出,但不难实现。

6. 看看效果

2.gif

你可能感兴趣的:(程序员如何玩成语猜猜看)