【技能开发分享】如何开发一个猜数字游戏

极客教程

(yushi.xie) #1

猜数字游戏

大家好,我是Rokid开发者钰石,今天同大家分享一下我开发的一个语音小游戏的设计心得。

玩法

当你进入猜数字游戏并开始游戏后会生成一个0 - 100的随机数(这个数字就是你要猜的数),然后你可以说出你猜测的数字,跟随提示一步步找到真正的答案;

  • 比生成的数大,若琪回答你大了
  • 比生成的数小,若琪回答你小了
  • 与生成的数相等,若琪回答你正确了

游戏流程图

     +————————————+
     |            |
     |  打开猜数字  |
     |            |
     +————————————+
           |
           |
           |
           V
     +————————————+
     |            |                    
     | 开始游戏    | <---------------------------------+
     |            |                                   |
     +————————————+                                   |
           |                                          |
           |<-----------------------------------+     |
           |                                    |     |
           V                                    |     |     
     +--------------+        +-----------+      |     |
     |              |        |           |      |     |
+--- |说出猜测的数字  |------> |   小了     |     |     |
|    |              |        |           |      |     |
|    +--------------+        +-----------+      |     |
|           |                     |             |     |
|           |                     |             |     |
|           |                     |             |     |
|           V                     V             |     |
|    +-------------+         +----------+       |     |
|    |             |         |          |       |     |   
|    |     大了     |-------> | 猜错了,  |-------+     |
|    |             |         | 继续猜    |             |
|    +-------------+         +----------+             |
|                                                     |
|    +-------------+         +-----------+            |
|    |             |         |           |            |
+--->| 相等        | -------> |是否继续游戏|-----是-----+
     |             |         |           |
     +-------------+         +-----------+
                                   
                                   
                                   
* 只要捕捉到「退出技能」语句则会退出游戏                                   
     +-------------+         +-----------+
     |             |         |           |
     |  退出技能    |-------->| 游戏结束   |
     |             |         |           |
     +-------------+         +-----------+

语音交互设计

根据游戏的流程,一共设计了4个意图。

  1. 开始游戏 — START_GAME intent

  2. 猜数字— GUESS_NUMBER intent $number (number slot, slot_value 0 - 100)

  3. 继续游戏 — RE_START_GAME intent

  4. 退出技能 — STOP_GAME intent

     {
     	"intents": [
     {
     	"intent": "GUESS_NUMBER",
     	"slots": [
     		{
     			"name": "number",
     			"type": "ROKID.NUMBER_ZH"
     		}
     	],
     	"user_says": [
     		"$number"
     	]
     },
     {
     	"intent": "RE_START_GAME",
     	"slots": [],
     	"user_says": [
     		"继续游戏",
     		"再玩一次"
     	]
     },
     {
     	"intent": "START_GAME",
     	"slots": [],
     	"user_says": [
     		"开始游戏"
     	]
     },
     {
     	"intent": "STOP_GAME",
     	"slots": [],
     	"user_says": [
     		"退出猜数字",
     		"退出技能"
     	]
     }
     ]
     }
    

注:在猜数字时,用户念出的数字有很多,我们不需要自定义一个词表来一一列举每一个数字,可以直接使用平台提供的ROKID.NUMBER_ZH预定义词表来实现这个场景。

配置-js 设计

技能的服务端配置我选择的是js-engine模式。这个模式非常好,不需要我自己去搭建云服务器,只需要用js的方式写出自己的业务逻辑即可。

有几点需要注意的地方,讲在前面

  • 可以统一定义出tts语音信息,以便后期更新时修改。
  • console.log(Rokid.param)打印请求体信息以便可以更好的拿到自己想要的内容
  • 由于ASR上来时只有出现中文数字(例: 五十),所以要使用chineseToNumber方法将中文转数字,好做判断
var WELCOME_TTS = '欢迎来到猜数字,游戏开始后,我会生成一个0到100的随机数字,你可以告诉我你想到的数字,我会对你的每一次猜测进行提示,直到猜中后游戏结束。';
var START_GAME_TTS = '游戏开始,请说一个0到100的数字。';
var RE_START_GAME_TTS = START_GAME_TTS;
var STOP_GAME_TTS = '好的,再见';
var BINGGO_TTS = 'binggo, 恭喜你猜对了。你可以对我说再玩一次。';
var BIG_TTS = '大了';
var SMALL_TTS = '小了';
var ERROR_TTS = '我听不懂你在说什么!';

var chnNumChar = {
    零:0,
    一:1,
    二:2,
    三:3,
    四:4,
    五:5,
    六:6,
    七:7,
    八:8,
    九:9,
    0:0,
    1:1,
    2:2,
    3:3,
    4:4,
    5:5,
    6:6,
    7:7,
    8:8,
    9:9
};

var chnNameValue = {
    十:{value:10, secUnit:false},
    百:{value:100, secUnit:false},
    千:{value:1000, secUnit:false},
    万:{value:10000, secUnit:true},
    亿:{value:100000000, secUnit:true}
};

var chineseToNumber = function (chnStr){
    var rtn = 0;
    var section = 0;
    var number = 0;
    var secUnit = false;
    var str = chnStr.split('');

    for(var i = 0; i < str.length; i++){
        var num = chnNumChar[str[i]];
        if(typeof num !== 'undefined'){
            number = num;
            if(i === str.length - 1){
                section += number;
            }
        }else{
            var unit = chnNameValue[str[i]].value;
            secUnit = chnNameValue[str[i]].secUnit;
            if(secUnit){
                section = (section + number) * unit;
                rtn += section;
                section = 0;
            }else{
                section += (number * unit);
            }
            number = 0;
        }
    }
    return rtn + section;
};

exports.handler = function(event, context, callback) {
    var rokid = Rokid.handler(event, context,callback);
    rokid.registerHandlers(handlers);
    rokid.execute();
};
    
var handlers = {
    'ROKID.INTENT.WELCOME':function() {
        this.emit(':tts',{tts: WELCOME_TTS});
        this.callback();
    },
    'START_GAME': function() {
        var needGuessNumber = Math.floor(Math.random() * 100);
        var guessCount = 0;
        this.emit(':tts', {tts: START_GAME_TTS}, {needGuessNumber: needGuessNumber, guessCount: guessCount});
        this.callback(null);
    },
    'RE_START_GAME': function() {
        var needGuessNumber = Math.floor(Math.random() * 100);
        var guessCount = 0;
        this.emit(':tts', {tts: RE_START_GAME_TTS}, {needGuessNumber: needGuessNumber, guessCount: guessCount});
        this.callback(null);
    },
    'STOP_GAME': function() {
        this.emit(':tts', {tts: STOP_GAME_TTS}, {});
        this.callback(null);
    },
    'GUESS_NUMBER':function() {
        console.log(Rokid.param);
        console.log(Rokid.param.request);
        console.log(Rokid.param.request.content);
        console.log(Rokid.param.request.content.slots);
        console.log(Rokid.param.request.content.slots.number);
        console.log(Rokid.param.session);
        console.log(Rokid.param.session.attributes);
        console.log(Rokid.param.session.attributes.needGuessNumber);
        var number = Rokid.param.request.content.slots.number;
        console.log("get num : " + number);
        number = chineseToNumber(number);
        console.log("number change after : " + number);
        var needGuessNumber = Rokid.param.session.attributes.needGuessNumber;
        var guessCount = Rokid.param.session.attributes.guessCount;
        var tts = BINGGO_TTS;
        if (number > needGuessNumber) {
            tts = BIG_TTS;
            guessCount = guessCount + 1;
        } else if (number < needGuessNumber) {
            tts = SMALL_TTS;
            guessCount = guessCount + 1;
        } else if (number == needGuessNumber) {
            tts = BINGGO_TTS;
        } else {
            tts = ERROR_TTS;
        }
        this.emit(':tts',{tts: tts}, {needGuessNumber: needGuessNumber, guessCount: guessCount});
        this.callback(null);
    }
};

问题与解决

  • 怎样保存开始游戏时生成的随机数到用户猜到为止?
    答: 使用session来保存该值,代码实现this.emit(':tts', {tts: START_GAME_TTS}, {needGuessNumber: needGuessNumber, guessCount: guessCount});

  • 实际使用技能时,由于口误会唤醒其他技能而退出猜数字游戏,怎么解决?
    答: session在口误唤醒别的技能时会被清空,待api出新的解决方案


猜数字游戏 V2

解决问题

  • 在玩猜数字游戏时,机器对于一些数字识别不准或者你切换到其他技能时,在切换回猜数字技能,不能接着进行正常游戏的bug解决。

以下为最新代码:

var WELCOME_TTS = 'HI, 欢迎来到猜数字,游戏开始后,我会生成一个0到100的随机数字,你可以告诉我你想到的数字,我会对你的每一次猜测进行提示,直到猜中后游戏结束。';
var START_GAME_TTS = '游戏开始,请说一个0到100的数字。';
var RE_START_GAME_TTS = START_GAME_TTS;
var STOP_GAME_TTS = '好的,再见';
var BINGGO_TTS = 'binggo, 恭喜你猜对了。你可以对我说再玩一次。';
var BIG_TTS = '大了';
var SMALL_TTS = '小了';
var ERROR_TTS = '我听不懂你在说什么!';


var cnNum2ArabNum = function(cn){
    var arab, parts, cnChars = '零一二三四五六七八九';

    if (!cn) {
        return 0;
    }

    if (cn.indexOf('亿') !== -1){
        parts = cn.split('亿');
        return cnNum2ArabNum(parts[0]) * 1e8 + cnNum2ArabNum(parts[1]);
    }

    if (cn.indexOf('万') !== -1){
        parts = cn.split('万');
        return cnNum2ArabNum(parts[0]) * 1e4 + cnNum2ArabNum(parts[1]);
    }

    if (cn.indexOf('十') === 0){
        cn = '一' + cn;
    }

    arab = cn
        .replace(/[零一二三四五六七八九]/g, function (a) {
            return '+' + cnChars.indexOf(a);
        })
        .replace(/(十|百|千)/g, function(a, b){
            return '*' + (
                b == '十' ? 1e1 :
                b == '百' ? 1e2 : 1e3
            );
        });
    
    return (new Function('return ' + arab))()
};

exports.handler = function(event, context, callback) {
    var rokid = Rokid.handler(event, context,callback);
    rokid.registerHandlers(handlers);
    rokid.execute();
};
    
var handlers = {
    'ROKID.INTENT.WELCOME':function() {
        this.emit(':tts',{tts: WELCOME_TTS});
        this.callback();
    },
    'START_GAME': function() {
        var needGuessNumber = Math.floor(Math.random() * 100);
        var guessCount = 0;
        console.log(Rokid.param);
        console.log(Rokid.param.context);
        console.log(Rokid.param.context.user);
        console.log(Rokid.param.context.user.userId);
        var userId = Rokid.param.context.user.userId ? Rokid.param.context.user.userId : "test_user_id";
        console.log("userId : " + userId);
        console.log("needGuessNumber : " + needGuessNumber);
        Rokid.dbServer.set(userId, needGuessNumber, (error, result) => {
            if (error) {
             console.log("set db error : " + error);
             this.emit(':tts', {tts: ERROR_TTS});
             this.callback();
            } else {
             console.log("set db result : " + result);
             this.emit(':tts', {tts: START_GAME_TTS});
             this.callback();
            }
        });
    },
    'RE_START_GAME': function() {
        var needGuessNumber = Math.floor(Math.random() * 100);
        var userId = Rokid.param.context.user.userId ? Rokid.param.context.user.userId : "test_user_id";
        Rokid.dbServer.set(userId, needGuessNumber, function(error, result){});
        this.emit(':tts', {tts: RE_START_GAME_TTS});
        this.callback();
    },
    'STOP_GAME': function() {
        this.emit(':tts', {tts: STOP_GAME_TTS}, {});
        this.callback();
    },
    'GUESS_NUMBER':function() {
        console.log(Rokid.param);
        console.log(Rokid.param.request);
        console.log(Rokid.param.request.content);
        console.log(Rokid.param.request.content.slots);
        console.log(Rokid.param.request.content.slots.number);
        var number = Rokid.param.request.content.slots.number;
        
        console.log("get num : " + number);
        number = cnNum2ArabNum(number);
        console.log("number change after : " + number);
        
        var userId = Rokid.param.context.user.userId ? Rokid.param.context.user.userId : "test_user_id";
        Rokid.dbServer.get(userId, (error, result) => {
            
            console.log("get db result : " + result);
             
            // 取得存储的值
            var needGuessNumber = result;
            
            console.log("needGuessNumber : " + needGuessNumber);
            
            var tts = BINGGO_TTS;
            if (number > needGuessNumber) {
                tts = BIG_TTS;
            } else if (number < needGuessNumber) {
                tts = SMALL_TTS;
            } else if (number == needGuessNumber) {
                tts = BINGGO_TTS;
            } else {
                tts = ERROR_TTS;
            }
            this.emit(':tts',{tts: tts});
            this.callback();
        });
    }
};

解决方法说明:该版本已经放弃使用session来保存随机生成的数字,选择使用js engine的新特性存储来保存随机生成的数字,这样即使你退出该技能后还能保存你退出前游戏时的数

js engine 存储使用方法,点击我进行查看


Rokid开发者大赛第一季
开发者社区Pebble(月石)开发测试固件发布
(chao.xuc) #2

棒!


(zhe.wang) #3

这个有意思!:smiley:


(illxi) #4

很好 代码很有用


(chao.xuc) #5

(快乐陀螺人) #6

请教一下, 我的设备已经是开发版了, 如何唤醒这个游戏呢?


知道了 , 要说 “打开猜数字游戏” 我说 “打开猜数字” 怎么就打不开呢


(yushi.xie) #7

因为我设置的入口词是猜数字游戏