#やりたいこと
- Amazon Echoとドラゴンクエストごっこをして遊びたい。
- スキル開発におけるセッションとステート管理の練習
下記の記事を参考にして開発を進めます。
Alexaスキル開発トレーニングシリーズ 第3回 音声ユーザーインターフェースの設計 : Alexa Blogs
1.目的とストーリーの明確化
2.台本の作成
3.フロー図の作成
4.対話モデルへの反映
#1.目的とストーリーの明確化
目的は冒頭に書いた通りです。
#2.台本の作成
台本は決まっています。
ドラゴンクエスト大辞典を作ろうぜ!!第三版 Wiki*を参照しました。(一部改変)
よくきた***よ。わしが王の中の王 竜王だ。
わしは待っておった。そなたのような若者が現れることを。
もしわしの味方になれば世界の半分をお前にやろう。
どうじゃ?わしの味方になるか?(はい/いいえ)
(はい)
本当だな?(はい/いいえ)
(はい)
では世界の半分、闇の世界を与えよう!
そなたに復活の呪文を教えよう!
これを書き留めておくのだぞ。
おまえの旅は終わった。
さあゆっくり休むがよい!わあっはっはっはっ
(いいえ)
では、どうしてもこのわしを倒すというのだな!
愚か者め!思い知るがよい!
#4.対話モデルへの反映
- インテントの洗い出し
ShareTheWorldIntent
という壮大なインテントを用意します。
名前を尋ねるのは起動時に行うので、LaunchRequest
で行い、Yes/Noの質問については、標準インテントにあるAMAZON.YesIntent
とAMAZON.NoIntent
を使います。
LaunchRequest //名前を尋ねる
ShareTheWorldIntent //半分をやろう
AMAZON.YesIntent //はい
AMAZON.NoIntent //いいえ
- 発話の洗い出し
ドラクエの主人公は基本的に喋りませんので、発話パターンはかなり少なくなります。
以下の名前設定も主人公の発話というよりは、冒険を開始する際の設定入力という位置づけです。
ShareTheWorldIntent {firstName}
ShareTheWorldIntent 私は {firstName} です
ShareTheWorldIntent 名前は {firstName} です
ShareTheWorldIntent {firstName} だよ
名前は標準スロットタイプにあるAMAZON.FirstName
で受取ります。
{
"languageModel": {
"intents": [
{
"name": "AMAZON.CancelIntent",
"samples": []
},
{
"name": "AMAZON.HelpIntent",
"samples": []
},
{
"name": "AMAZON.NoIntent",
"samples": [
"いいえ",
"やだ",
"だめ",
"なし"
]
},
{
"name": "AMAZON.StopIntent",
"samples": []
},
{
"name": "AMAZON.YesIntent",
"samples": [
"はい",
"うん",
"いいよ",
"あり"
]
},
{
"name": "ShareTheWorldIntent",
"samples": [
"{firstName}",
"{firstName} と言います",
"{firstName} だよ",
"私は {firstName} です",
"名前は {firstName} です"
],
"slots": [
{
"name": "firstName",
"type": "AMAZON.FirstName"
}
]
}
],
"invocationName": "ドラクエ"
}
}
#完成したコード
"use strict";
const Alexa = require('alexa-sdk');
const askNameMsg = '名前を教えてください';
const shareTheWorldMsg = [
'わしが王の中の王<break time="0.1s"/>竜王だ。<break time="0.5s"/>',
'わし は 待っておった。そなたのような若者が現れることを。<break time="0.5s"/>',
'もし、わしの味方になれば世界の半分をお前にやろう。<break time="0.5s"/>',
'どうじゃ?わしの味方になるか?'
].join();
const combatMsg = [
'では、どうしてもこのわしを倒すというのだな!<break time="0.5s"/>',
'愚か者め!<break time="0.3s"/>思い知るがよい!'
].join();
const confirmMsg = '本当だな?';
const badEndMsg = [
'では世界の半分、闇の世界を与えよう!<break time="0.5s"/>',
'そなたに復活の呪文を教えよう!<break time="0.5s"/>',
'これを書き留めておくのだぞ。<break time="0.5s"/>',
'おまえの旅は終わった。<break time="0.5s"/>',
'さあゆっくり休むがよい!<break time="0.5s"/><say-as interpret-as="interjection">わっはっは</say-as>'
].join();
// ステートの定義
const states = {
DIALOGUEMODE: '_DIALOGUEMODE',
FINALQUESTIONMODE: '_FINALQUESTIONMODE'
};
exports.handler = function(event, context, callback) {
var alexa = Alexa.handler(event, context);
// alexa.appId = process.env.APP_ID;
alexa.registerHandlers(handlers, dialogueHandlers,finalquestionHandlers); // 既存のハンドラに加えてステートハンドラ(後半で定義)も登録
alexa.execute();
};
var handlers = {
'LaunchRequest': function () {
this.emit(':ask',askNameMsg);
},
'ShareTheWorldIntent': function () {
var firstName = this.event.request.intent.slots.firstName.value;
this.handler.state = states.DIALOGUEMODE; // ステートをセット
this.attributes['firstName'] = firstName; // 名前をセッションアトリビュートにセット
var message = 'よく来た' + firstName + 'よ。' + shareTheWorldMsg;
this.emit(':ask', message);
console.log(message);
}
};
// ステートハンドラの定義
var dialogueHandlers = Alexa.CreateStateHandler(states.DIALOGUEMODE, {
'AMAZON.NoIntent':function() {
this.handler.state = '';
this.attributes['STATE'] = undefined;
this.emit(':tell', combatMsg);
},
'AMAZON.YesIntent':function() {
this.handler.state = states.FINALQUESTIONMODE; // ステートをセット
this.emit(':ask', confirmMsg);
},
'Unhandled': function() {
var reprompt = 'どうじゃ?わしの味方になるか?';
this.emit(':ask', reprompt, reprompt);
}
});
var finalquestionHandlers = Alexa.CreateStateHandler(states.FINALQUESTIONMODE, {
'AMAZON.NoIntent':function() {
this.handler.state = '';
this.attributes['STATE'] = undefined;
this.emit(':tell', combatMsg);
},
'AMAZON.YesIntent':function() {
this.handler.state = '';
this.attributes['STATE'] = undefined;
this.handler.state = states.FINALQUESTIONMODE; // ステートをセット
this.emit(':tell', badEndMsg);
},
'Unhandled': function() {
var reprompt = 'どうじゃ?わしの味方になるか?';
this.emit(':ask', reprompt, reprompt);
}
});
#コードの説明
"use strict";
const Alexa = require('alexa-sdk');
const askNameMsg = '名前を教えてください';
const shareTheWorldMsg = [
'わしが王の中の王<break time="0.1s"/>竜王だ。<break time="0.5s"/>',
'わし は 待っておった。そなたのような若者が現れることを。<break time="0.5s"/>',
'もし、わしの味方になれば世界の半分をお前にやろう。<break time="0.5s"/>',
'どうじゃ?わしの味方になるか?'
].join();
const combatMsg = [
'では、どうしてもこのわしを倒すというのだな!<break time="0.5s"/>',
'愚か者め!<break time="0.3s"/>思い知るがよい!'
].join();
const confirmMsg = '本当だな?';
const badEndMsg = [
'では世界の半分、闇の世界を与えよう!<break time="0.5s"/>',
'そなたに復活の呪文を教えよう!<break time="0.5s"/>',
'これを書き留めておくのだぞ。<break time="0.5s"/>',
'おまえの旅は終わった。<break time="0.5s"/>',
'さあゆっくり休むがよい!<break time="0.5s"/><say-as interpret-as="interjection">わっはっは</say-as>'
].join();
竜王のセリフを定義しています。
やたらと喋るので、長いセリフについては可読性の観点から配列で記載して、join()
で結合しています。
竜王の発話に威厳を持たせるため、SSML(Speech Synthesis Markup Language : 音声合成マークアップ言語)形式で間を調整しています。
SSMLについては、下記の記事を参考にしてください。
// ステートの定義
const states = {
DIALOGUEMODE: '_DIALOGUEMODE',
FINALQUESTIONMODE: '_FINALQUESTIONMODE'
};
exports.handler = function(event, context, callback) {
var alexa = Alexa.handler(event, context);
// alexa.appId = process.env.APP_ID;
alexa.registerHandlers(handlers, dialogueHandlers,finalquestionHandlers); // 既存のハンドラに加えてステートハンドラ(後半で定義)も登録
alexa.execute();
};
ステートは2つ定義しました。
名前を聞いて、最初の竜王の問いかけを聞いた時点で、DIALOGUEMODE
に入ります。
「いいえ」と答えたらその時点で戦闘に突入して終了します。
「はい」と答えたら、FINALQUESTIONMODE
に入って、更に次の質問へと移ります。
var handlers = {
'LaunchRequest': function () {
this.emit(':ask',askNameMsg);
},
'ShareTheWorldIntent': function () {
var firstName = this.event.request.intent.slots.firstName.value;
this.handler.state = states.DIALOGUEMODE; // ステートをセット
this.attributes['firstName'] = firstName; // 名前をセッションアトリビュートにセット
var message = 'よく来た' + firstName + 'よ。' + shareTheWorldMsg;
this.emit(':ask', message);
console.log(message);
}
};
まず、LaunchRequest
に入って、名前を受け取ります。
そしてShareTheWorldIntent
に入って、ステートDIALOGUEMODE
をセットしてから、受け取った名前で呼びかけてあげます。
// ステートハンドラの定義
var dialogueHandlers = Alexa.CreateStateHandler(states.DIALOGUEMODE, {
'AMAZON.NoIntent':function() {
this.handler.state = '';
this.attributes['STATE'] = undefined;
this.emit(':tell', combatMsg);
},
'AMAZON.YesIntent':function() {
this.handler.state = states.FINALQUESTIONMODE; // ステートをセット
this.emit(':ask', confirmMsg);
},
'Unhandled': function() {
var reprompt = 'どうじゃ?わしの味方になるか?';
this.emit(':ask', reprompt, reprompt);
}
});
ステートがDIALOGUEMODE
の状態で、dialogueHandlers
にやって来ます。
答えが「いいえ」の場合、AMAZON.NoIntent
に入り、this.emit
の引数が':tell'
になっているため、戦闘に突入して終了します。ステートのリセットが必要かよくわかりませんでしたが、書いてみました。
「はい」の場合、AMAZON.YesIntent
に入り、ステートをFINALQUESTIONMODE
に更新して、追加の質問をします。
「はい/いいえ」どちらにも当てはまらなかった場合は、Unhandled
に入って、もう一回同じ質問を繰り返します。竜王は我慢強いのです。
var finalquestionHandlers = Alexa.CreateStateHandler(states.FINALQUESTIONMODE, {
'AMAZON.NoIntent':function() {
this.handler.state = '';
this.attributes['STATE'] = undefined;
this.emit(':tell', combatMsg);
},
'AMAZON.YesIntent':function() {
this.handler.state = '';
this.attributes['STATE'] = undefined;
this.handler.state = states.FINALQUESTIONMODE; // ステートをセット
this.emit(':tell', badEndMsg);
},
'Unhandled': function() {
var reprompt = 'どうじゃ?わしの味方になるか?';
this.emit(':ask', reprompt, reprompt);
}
});
前の質問で「はい」と答えると、ステートがFINALQUESTIONMODE
であるため、最後の質問に入ります。
「はい/いいえ」どちらにしても、``:tell'`で応答してセッションが終了します。
#おわりに
あとは竜王を倒すのみです。検討を祈ります。
#参考リンク