Google Home でちゃんとジャンケンするアプリをつくってみた

# Google Homeでジャンケン

祭りに乗り遅れること1週間、やっと我が家にも Google Home が届きました。
さっそくセットアップを完了し、ラジオを流したり一通り標準の機能を確認した後ふと思いました。

**<h4>インテリジェントなAIが後ろにいる Google Home さんなら、ジャンケンも出来るだろう**</h4>

さっそく、試してみましょう！

## 何か思ってたのと違う

わたし  「ねぇ Google じゃんけんしよう！」
Google 「**はい、最初はグージャンケンポン！**」

…？？

わたし　「ねぇ Google じゃんけんしよう！」
Google 「**はい、最初はグージャンケンポン！**」
わたし　「ぐー！！」
Google　「**…**」

なにこれ私の知ってるジャンケンじゃない…

## ないんなら作ればいいじゃない

ということで、本題です。
幸い簡単に 会話する　Bot が作れる環境が（ほぼ）タダで試せます。
そうです、 Dialogflow です。

どういうものか分かりませんが、5分で簡単なBotが作れるって話だしジャンケンなんて単純なものは簡単に作れるでしょう！

が、現実は思ったより複雑でした。

ちなみにこれを書き終わってから知ったのですが、今回の使い方だと Event を使ったほうがよかったと思います。
[Dialogflowのeventを使ってみる。](https://qiita.com/yanosen_jp/items/dbeb68b88ae64e733f46)

結果だけ見たい人は

動画
[![](http://img.youtube.com/vi/C97nbJNACHs/0.jpg)](https://www.youtube.com/watch?v=C97nbJNACHs)

[コード](https://gist.github.com/82p/82f8d0536b3b36f4a42b691d7cf6edd2)

## 方針
まずは基本的な方針です。
こんなフローを考えます。

![2017-12-20_01h40_03.png](https://qiita-image-store.s3.amazonaws.com/0/83984/cc4fada3-07d5-8632-e4ec-98bd32bb2d04.png)

ジャンケンの手を認識して勝ち負けを判定する部分は Firebase で作ります。
同様に結果がでて、そのあともう一度ジャンケンをするか終了するかの判断も Firebase に投げます。

考えられる登場人物たちはこんな感じです。

### Entities
* ジャンケンの手(hand)
* ジャンケンが終わった後どうするか(nextAction)

### Intents
* じゃんけん
* あいこ
* もう一回

### Context
* ジャンケンの手を待つ(waitHand)
* あいこの状態(draw)
* 次の行動(next)


まとめるとこんな感じかな？
![2017-12-20_12h09_49.png](https://qiita-image-store.s3.amazonaws.com/0/83984/a8e69f3f-0c47-6b14-7e50-01fb66c56aea.png)


## 第一の壁「ジャンケンの手を認識してくれない。」
さて、ここまでできればさくっと作れるかと思ったのですが、そうはいきませんでした。

まず、Google Assistantは「ぐーちょきぱー」を認識できません。
これは簡単な話で、Google Assistant に 「ぐー」「ちょき」「ぱー」なんて言ってるアタマのパーな人はいません。

そう考えると Google Assistant が「ぐー」「ちょき」「ぱー」なんて言葉は認識しないのは当たり前です。

わたし 「ぐー」
Google (Goo)

わたし 「ちょき！」
Google (猪木)

わたし 「パー」
Google（パーティー）

![2017-12-20_02h02_58.png](https://qiita-image-store.s3.amazonaws.com/0/83984/c7a8ba90-bba1-a8ff-3764-6f3ec26c5ad8.png)


> Goo, パーティーはともかく猪木ってなんだよ…

Google先生に学習してもらうという手もあるのだと思いますが、Dialogflow初心者の私にはハードルが高すぎます。
ということでこんな Entity を作って無理やり対応しました。

![2017-12-20_01h46_48.png](https://qiita-image-store.s3.amazonaws.com/0/83984/02b257b9-dc81-8f33-3f45-5f75680dc00b.png)

>学習のさせ方をご存知の方はぜひコメントくださると助かります。

## 第二の壁 「Context」が分からない。

Google 先生に今、「何を聞いているのか」「会話のどのフェーズなのか」を定義するのが Context です。
しかしまだ Dialogflow が出始めなこともあり、 Context に関する情報があまりありません。

今回のジャンケンフローをもう一度確認すると、

![2017-12-20_12h14_21.png](https://qiita-image-store.s3.amazonaws.com/0/83984/dd48a61b-a2fd-26e2-1f91-6cbebf1a9e71.png)

**"ジャンケンを始める時やあいこの時"** と、**"ジャンケンの決着がついた後"** では **Context** は全く違うはずです。
**ジャンケンの結果が出ているのに、「グー」を認識しては困ります**し、**"あいこ" なのに次の手を聞いてくれない**のではジャンケンが出来ません。

これらの Context を firebase で設定する方法がまったくわかりませんでした。
結果として、 

1. Dialogflow では 各 Intent が終わったらリセットするように Console 上で設定する。
2. Firebase で結果次第で app.setContext する

という方針でうまく動きました。（ベストかどうかは分かりません）

### Dialogflow では認識したら Context をリセット

例えば、ホテルの予約システムで ホテル名と宿泊予定日の2つの Context がある場合、
システムが「宿泊先と宿泊日を教えてください」と聞き、ユーザが「ホテルソフト技研です」と答えた場合、次の質問は「宿泊日はいつですか？」であるべきです。

ホテル名というコンテキストは取得できた段階で適切に終了させるべきなのです。
（たぶんこういうことだと認識してるのですが…）

Context はリストで持つことができ複数のことが同時に聞けるのですが、今回の場合ジャンケンをしているときは、ジャンケンの手だけを聞いてほしいです。

ということで、各フェーズで適切に Context を終了させたいと思います。

Contextには lifespan と呼ばれる値があり、これが 0 になると、そのContextが終了したことになります。
Console上では以下のように設定すればOKです。

![2017-12-20_02h07_31.png](https://qiita-image-store.s3.amazonaws.com/0/83984/5e92e50f-b788-7a9e-192b-99cadaaab899.png)

![2017-12-20_02h07_41.png](https://qiita-image-store.s3.amazonaws.com/0/83984/75195a29-806f-12a7-9b87-7db5eb6b7087.png)
![2017-12-20_02h07_52.png](https://qiita-image-store.s3.amazonaws.com/0/83984/5a9db39a-a85c-ef2e-4d09-03abd11a58a7.png)


ハマったのが、 Firebase 上で DialogflowApp#setContext というメソッドがあるので、このメソッドで同様にlifespan を 0 にすれば上記の設定と同じことが実現できるのではと思ったのですがうまく行きませんでした。
なぜかは良く分かりません。

```js
  //Console 上では特にContextの無効化をせずに Firebase 上で無効化しようとする
  app.setContext('waitHand',0); //効かない
```


### Firebase 結果次第で Context を設定

無効化するのはなぜかできませんでしたが、新しい Context を設定することは出来ました。
ですので、ジャンケンの結果がでれば next コンテキストに、あいこなら draw Contextに飛ばすように設定します。

```js
  const actionHandlers = {
    // The default welcome intent has been matched, welcome the user (https://dialogflow.com/docs/events#default_welcome_intent)
    'janken': (app) => {
      // Use the Actions on Google lib to respond to Google requests; for other requests use JSON
      if (requestSource === googleAssistantRequest) {
        const hand = app.getArgument('hand');
        let result = janken(hand);
        //以下のように Firebase で Context を lifespan 0に設定しても効かないから脱出
        //app.setContext('waitHand',0);
        app.setContext('draw',0);
        app.setContext(result.context,5);
        if(result.context === 'draw'){
            app.ask(result.text);
            return;
        }
        console.log(JSON.stringify(result));
        app.ask(result.text + '。もう一度？');        
      } else {
        sendResponse('..when this message is called?'); // Send simple response to user
      }
    },
```

## 第3の壁 会話の終わり方が分からない
Google Home とのジャンケンに飽きてしまった場合アプリを終了させたいと思います。
その際、 Dialogflow の Intent 画面では、 End conversation で出来ます。

![2017-12-20_19h27_58.png](https://qiita-image-store.s3.amazonaws.com/0/83984/1856a3e2-1e64-5394-ab90-e2c058ae1fa8.png)

しかし、今回は終了するかの判断を Fulfillment で管理をしているので、そのスクリプト上でやりたいです。

しかし、その方法が分からない。結果から言うと、 app.tell() で最後の言葉を言って終了できました。

```js
  app.tell('じゃんけんを終わります');
```

これ、ドキュメントどこにあるんですか(´;ω;｀)

## おわりに

ということで[こちら](https://gist.github.com/82p/82f8d0536b3b36f4a42b691d7cf6edd2)が今回のアプリの全文です。

初めにも書きましたが、Eventを使ったほうがよかったと思いますし、うまく動かない部分もあります。
（というか昨日の夜記事書きながら弄っていたら一部動かなくなった。今日も眠いので直すのは週末かなあ）
まだまだ勉強し始めたばかりですが、まずは動画のように動くぐらいまではできたので満足。

テスト用のじゃんけんアプリを起動しますというところだけが残念です。
個人用のアプリとして publish はできないのでしょうか？