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

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

下記の記事を参考にして開発を進めます。
[Alexaスキル開発トレーニングシリーズ 第3回 音声ユーザーインターフェースの設計 : Alexa Blogs](https://developer.amazon.com/ja/blogs/alexa/post/cddf2434-42ee-422f-886d-5be9c36dba14/jp-training-chapter3)

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

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

#2.台本の作成
台本は決まっています。
[ドラゴンクエスト大辞典を作ろうぜ！！第三版 Wiki*](http://wikiwiki.jp/dqdic3rd/?%A1%DA%A4%E2%A4%B7%A1%A1%A4%EF%A4%B7%A4%CE%A1%A1%A4%DF%A4%AB%A4%BF%A4%CB%A4%CA%A4%EC%A4%D0%A1%A1%A4%BB%A4%AB%A4%A4%A4%CE%A1%A1%A4%CF%A4%F3%A4%D6%A4%F3%A4%F2%A1%A1%A1%FB%A1%FB%A1%FB%A1%FB%A4%CB%A1%A1%A4%E4%A4%ED%A4%A6%A1%A3%A1%DB)を参照しました。（一部改変）

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

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

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

#3.フロー図の作成
![クリップボード01.png](https://qiita-image-store.s3.amazonaws.com/0/228205/878f0193-9b27-c73c-4aee-6e2c35ad287c.png)


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

- インテントの洗い出し

`ShareTheWorldIntent`という壮大なインテントを用意します。
名前を尋ねるのは起動時に行うので、`LaunchRequest`で行い、Yes/Noの質問については、[標準インテント](https://developer.amazon.com/ja/docs/custom-skills/standard-built-in-intents.html#available-standard-built-in-intents)にある`AMAZON.YesIntent`と`AMAZON.NoIntent`を使います。

```
LaunchRequest //名前を尋ねる
ShareTheWorldIntent //半分をやろう
AMAZON.YesIntent //はい
AMAZON.NoIntent //いいえ
```

- 発話の洗い出し
 
ドラクエの主人公は基本的に喋りませんので、発話パターンはかなり少なくなります。
以下の名前設定も主人公の発話というよりは、冒険を開始する際の設定入力という位置づけです。

```
ShareTheWorldIntent {firstName}
ShareTheWorldIntent 私は {firstName} です
ShareTheWorldIntent 名前は {firstName} です
ShareTheWorldIntent {firstName} だよ
```
名前は[標準スロットタイプ](https://developer.amazon.com/ja/docs/custom-skills/slot-type-reference.html)にある`AMAZON.FirstName`で受取ります。

```json:インテントスキーマ
{
  "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": "ドラクエ"
  }
}
```
#完成したコード

```javascript: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);
}
});
```
#コードの説明
```javascript: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については、下記の記事を参考にしてください。

- [Alexaスキル開発トレーニングシリーズ 番外編 : SSMLによる発音の調整とスキルのテスト : Alexa Blogs](https://developer.amazon.com/ja/blogs/alexa/post/ff4f8bf0-4c86-4c5f-9c50-467816b0a017/jptraining6plus)

```javascript: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`に入って、更に次の質問へと移ります。

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

```javascript: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`に入って、もう一回同じ質問を繰り返します。竜王は我慢強いのです。

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

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

#参考リンク
- [Alexaスキル開発トレーニングシリーズ 第3回 音声ユーザーインターフェースの設計 : Alexa Blogs](https://developer.amazon.com/ja/blogs/alexa/post/cddf2434-42ee-422f-886d-5be9c36dba14/jp-training-chapter3)
- [Alexaスキル開発トレーニングシリーズ 番外編 : SSMLによる発音の調整とスキルのテスト : Alexa Blogs](https://developer.amazon.com/ja/blogs/alexa/post/ff4f8bf0-4c86-4c5f-9c50-467816b0a017/jptraining6plus)