6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Amazon Echoでりゅうおうと世界の半分をもらう交渉をする。

Posted at

#やりたいこと

  • Amazon Echoとドラゴンクエストごっこをして遊びたい。
  • スキル開発におけるセッションとステート管理の練習

下記の記事を参考にして開発を進めます。
Alexaスキル開発トレーニングシリーズ 第3回 音声ユーザーインターフェースの設計 : Alexa Blogs

1.目的とストーリーの明確化
2.台本の作成
3.フロー図の作成
4.対話モデルへの反映

#1.目的とストーリーの明確化
目的は冒頭に書いた通りです。

#2.台本の作成
台本は決まっています。
ドラゴンクエスト大辞典を作ろうぜ!!第三版 Wiki*を参照しました。(一部改変)

よくきた***よ。わしが王の中の王 竜王だ。
わしは待っておった。そなたのような若者が現れることを。
もしわしの味方になれば世界の半分をお前にやろう。
どうじゃ?わしの味方になるか?(はい/いいえ)
 
(はい)
本当だな?(はい/いいえ)
 
(はい)
では世界の半分、闇の世界を与えよう!
そなたに復活の呪文を教えよう!

これを書き留めておくのだぞ。
おまえの旅は終わった。
さあゆっくり休むがよい!わあっはっはっはっ

(いいえ)
では、どうしてもこのわしを倒すというのだな!
愚か者め!思い知るがよい!

#3.フロー図の作成
クリップボード01.png

#4.対話モデルへの反映

  • インテントの洗い出し

ShareTheWorldIntentという壮大なインテントを用意します。
名前を尋ねるのは起動時に行うので、LaunchRequestで行い、Yes/Noの質問については、標準インテントにあるAMAZON.YesIntentAMAZON.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": "ドラクエ"
  }
}

#完成したコード

index.js
"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);
}
});

#コードの説明

index.js
"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については、下記の記事を参考にしてください。

index.js
// ステートの定義
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に入って、更に次の質問へと移ります。

index.js
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をセットしてから、受け取った名前で呼びかけてあげます。

index.js
// ステートハンドラの定義
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に入って、もう一回同じ質問を繰り返します。竜王は我慢強いのです。

index.js
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'`で応答してセッションが終了します。

#おわりに
あとは竜王を倒すのみです。検討を祈ります。

#参考リンク

6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?