程序员如何玩成语猜猜看

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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容