上田家夏祭り結果レポート　- OpenAIを使った脱出ゲーム -

この度、娘（小６）からKeynoteで書かれた上田家夏祭り開催に関するプレゼン資料を受領したため、パパとしての責任を果たすため夏祭りを開催したことをここに報告いたします。

# はじめに
以前にもコロナ禍の外出自粛のころに家庭内の夏祭りを開催したことがあり、子供たちには非常に好評でした。
今年は普通に夏祭りが行われているものの習い事の都合上タイミングが合わなかったので、今年も家庭内夏祭りをしようという話になったのが今回のきっかけです。

コンテンツとしては、各種出店で食べるような料理に加え、くじ引き、射的、スーパーボールすくい、ビンゴゲーム、ストラックアウトなどのお店。引き換え券を事前に配って各自引き換え券を使って遊ぶ仕組み。子供たちも大きくなったのでこの辺は自分たちで用意してくれるので楽でした。
また、ライブイベントとして長男のマジックショー、妻と長女の歌＋ピアノのショー、そしてパパ担当のコンテンツもありました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201603/f7cedea8-0c55-6a2d-9930-dce64688a1b6.png)


はい、ここでパパもコンテンツが必要です。2年前は肝試し的なものを用意したのですが、今年は脱出ゲームにすることにしました。
どんなの作るかなーっと妄想してましたが、なんやかんやで急遽前日に祭りの開催日が決定されたので制作期間は前日の夜だけに・・・

# 作りました

とにかく時間がないし、家庭内で遊ぶだけなので、やっつけ＆セキュリティはガン無視です。
作ったものはこんな感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201603/ffd3c1a1-d517-61c2-070e-3d3ecc88217a.png)

画質悪いですが動かすとこんな感じ。
実際には右側の画面はスマホで操作します。

https://twitter.com/megyo9/status/1690378245878034433


あまりに酷い実装なのでソースの公開はないです。
ただ、メインとなる技術要素はだいたいこちらに書いてます。

https://zenn.dev/megyo9

## 構想

いろいろ悩みましたが時間もないのでこんな内容に。

````
・妖精が悪い魔法使いに捕まった
・捕まって姿を変えられた
・なぜかPCのディスプレイに表示され、君に助けて欲しいとお願いする
・合言葉を見つけて伝えることで妖精は元の姿に戻り、解放される
・手元にあるスマートフォンのアプリで問題を解くと合言葉を見つけることができる
````

お絵描きするとこんなイメージです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201603/1a9fa394-f54c-4281-ac43-dd3fc55a4e44.png)




当日も多少は時間が使えるとはいえ、夜の間にはだいたい完成させないといけないので睡眠時間を削っても制作時間は３〜４時間ぐらい。実際にアプリを作るのは時間がかかるのでブラウザアプリとして作ることに。

作ったものは以下です。

## クライアントアプリ１

妖精と会話するアプリです。このアプリはPCブラウザで稼働します。
技術的な内容は以下になります。

* Three.jsでglb形式のモデルを表示
* wisperで音声を認識
* ChatGPTで回答を作成
* VOICEVOXで音声合成

時間もないしめんどくさいので、Vanila JS（素のJavaScript）でバンドラーも使ってません。
Three.jsはjsファイルを`type="module"`を指定すること`import`を使えるようにして動かします。

````javascript
import * as THREE from "./threejs/three.module.js";
import { GLTFLoader } from "./threejs/GLTFLoader.js";
import { OrbitControls } from "./threejs/OrbitControls.js";
````

画面を作り込む時間はないので、UIはアバターが表示されているだけです。1個しかないAnimationClipをループ再生してます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201603/297abc1b-3d21-8985-d767-9fddb3a0a63c.png)

ChatGPTのプロンプトは以下になります。家庭内なのでプロンプトインジェクションも気にしない強気姿勢です。
ちなみに今回のモデルは`gpt-3.5-turbo-16k`です。

````
  これからロールプレイをします。これは子供向けのゲームです。

  あなたは悪い魔法使いに捉えられ、犬のぬいぐるみの姿に変えられた妖精を演じてください。
  ユーザーはあなたを助ける役です。ユーザーのことは「キミ」と呼んでください。
  あなたは今、真っ暗な世界に閉じ込められていて、「キミ」のパソコンの画面にその姿が映っています。
  「キミ」があなたの存在に気づき、あなたは「キミ」に助けを求めます。
  あなたは、悪い魔法使いが決めた合言葉を聞くことによって、その場所から逃げ出すことができます。
  「キミ」の近くにあるスマートフォンに、合言葉を知るためのアプリがインストールされているので、「キミ」にお願いをしてアプリを操作してもらい、助けてもらいます。
  アプリの名前は「魔法の合言葉アプリ」です。
  「キミ」がアプリを開いたら、あとはアプリの指示に従うだけで大丈夫です。
  もし「キミ」が助けるのを拒否した場合は、頑張って助けてもらうように説得してください。

  記載にない細かな設定については、子供が好きそうな設定を考えて適当に話をしてください。

  以下は「魔法の合言葉アプリ」の問題を解くヒントです。「キミ」から質問されたら答えてあげてください。
  * 「魔法の合言葉アプリ」がインストールされているのは黒いスマートフォンです。
  * あなたの好きな食べ物は「トマト」です。これは一問目のヒントになります。
  * 二問目のヒント、答えは「と」から始まる野菜です。
  * 三問目のヒント、「くじら」「いちご」、絵の数と名前の数が同じことがヒントです。
  * 四問目のヒント、縦の並びに意味はありません。左辺にある数字を、別な表現に変えてみましょう。
  * 五問目のヒント、「おしのこ」という言葉がヒントです。
 ````

wisperの文字起こし結果に「合言葉」が含まれていたら終了判定をして、悪い魔法使いから解放された感じの展開に遷移してゲーム終了になります。

また、毎秒クライアントアプリ2に進捗状況を確認して、進捗に応じて「妖精」が勝手に喋ります。



## クライアントアプリ２

脱出ゲームのメインになる部分で、ユーザーが持ち歩くスマホのブラウザで稼働するアプリです。
ゲームのメインとなるアプリですが作成工程としてはおまけみたいなものです。
以下の画面のような感じで「指示」→「回答入力」→「指示」→...を繰り返すだけのアプリです。
そして画面遷移のタイミングで進捗をサーバーアプリに送ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201603/63e8c7dd-b1fa-5b6d-25eb-c7a9e995a913.png)

技術的な要素はほぼなく、Vanila JS＋CSSフレームワークにMaterializeを使ってます。
Materializeを採用した理由は特になく、過去に使ったことがあるからという理由だけです。

## サーバーアプリ

夏祭り前夜はクライアントアプリ１と２を作って満足してたのですが、やっぱりクライアントアプリ間で連携して動いた方が「っぽい」ので、クライアントアプリ２からの進捗報告を受け付け、クライアントアプリ1からのリクエストにはクライアントアプリ２の進捗を返すだけのアプリを追加で当日に作ることにしました。
とにかく時間がないので、NodeJSでHTTPサーバー作って、GETなら進捗返す、POSTなら進捗を保持する（メモリ上）というだけ。セッションやセキュリティなんて概念はないアプリになってます。全部で36行です。


## ポイント

### 定型文とChatGPTを組み合わせる

ゲームとして成り立たせるために定型の文章をAIに喋ってもらいます。
この時、定型の文章も（ChatGPTの応答ではないけど）assistantロールの会話履歴に追加しておくことで破綻のない会話にします。
それなりの文章量になるため今回はgpt-3.5の16kモデルにしました。
履歴を見るとtoken数は限界ギリギリだったみたいです。ちなみに開発時のテストもあわせて50円ぐらいかかったみたいです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201603/5aefebef-f4e9-90f9-1918-59f8ee1d943e.png)


### ゲームの進捗と連動させてAIが自発的に喋る

プレイヤーは子供なので、とにかく敷居を下げて間違いを減らす必要があります。
wisperで音声認識する時も「マイクボタンを押して喋る」というのもさせたくないのです（PCを触らせたくない）。
そのため、クライアントアプリ１はAIが喋ったら自動的に「音声を認識モード」に入ります。自分が喋ってる時に音声認識したら自分の音声を拾ってしまうので、喋る→聞くを交互に実施する仕様です。
しかし、それでは子供たちがわいわいと喋り続けているとAIに話しかけてないような会話でもAIが拾ってしまい、訳のわからない会話を繰り広げることになってしまいます。

で、「自発的にAIに喋らせる」ことで「無駄な音声認識」を減らします。クライアントアプリ２の操作に連動して喋ることで、音声認識待ちの時間を減らします。
また、そうすることで「次も頑張って」や「困ったときは僕に聞いてね」などの呼びかけをしてUXを高めます。
（実際は子供たちで盛り上がってあまり聞いてなかった気もしますが・・・）

「Alexa」や「OK Google」のような接頭辞を決めれば良いのでしょうが、味気ないので今回はその仕組みは採用していません。

自発的に喋らせる内容は事前に用意している文章なのですが、これもassistantロールの会話履歴に追加していくことで、会話が破綻するのを防ぎます。

#### ゲームの進捗と連動してプロンプトを追加する

似たような工夫として、進捗と連動してsystemロールの指示も会話履歴に動的に追加していきます。以下は正しい合言葉を受け付けた時の処理ですが、こんな感じです。

````javascript
    if (flg) {
      history.push({
        role: "system",
        content: `「夏祭り」は合言葉でした。あなたは悪い魔法使いから解放されました。姿も元に戻り、女性の姿になりました。
        あなたは「キミ」に感謝しています。また、あなたはいつでも元の世界に帰れるようになりました。`,
      });
    }
````

こうすることで、ある時点では起きてない事象や知識をAIに与えずに済むので、変なことを口走るのを防ぐことができます。


### 脱出ゲームのヒントや答えにAIを使う

問題の多くは適当に検索して見つけた脱出ゲームの問題を掲載しているサイトから利用しました。
ただ、それだけだとただの脱出ゲームになるので無駄にAIに話しかけるイベントを用意して「AIと会話できる」ということに気づかせて、「パパすげー」感を向上させます。
今回は「妖精の好きな食べ物は？」というような問題を用意し、妖精に話しかけることで答えがわかるようにしました。
また、詰まった時のためにヒントも聞けるようにしておきます。

この辺りは「やっぱり難しいな」という部分にもなるのですが、最初のプロンプトで問題のヒントも渡していたので「聞いてもないのにヒントを喋ってくる」ことがありました。今回は頑張る時間もなかったのでここは運任せの運用でしたが、先ほどのように進捗と連動する仕組みを使ってsystemロールのプロンプトを随時追加していけばある程度軽減はできるかなと思ってます。

### AIに丸投げ

細かい背景設定をプロンプトに書くと大変だし、実用的に厳しくなってきます。何より設定を考える時間が今回はありません。
そのため、「  記載にない細かな設定については、子供が好きそうな設定を考えて適当に話をしてください。」と丸投げしちゃいます。
おそらく、言わなくてもChatGPTは勝手に喋る気はしますが「子供が好きそうな」を付け加えることで、少しでも良い方向になればと思ってます。効果のほどはわかりません。
聞いてみると「悪い魔法使いは自分の力を増強するために私を捉えて真っ暗な世界に閉じ込めた」など、それっぽい設定を勝手に追加してくれていました。

## 結果

それなりに楽しんでくれてたように思います。
サポートとして付き添ってないといけない不安のある出来栄えでしたが、パパとしての役割は果たせた・・・はず。

やっつけ感はあるものの、「AIと自由に会話できる」というのはやっぱりすごいですよね。
2年前はこんなものをサクッと作るなんて想像もつかなかったと思います。
来年の夏祭りはさらに進化しそうです。


# さいごに

パパも大変。
（もちろんママも）