はじめに
現在、大分県を舞台としたアドベンチャーゲームを開発しています。「ポートピア連続殺人事件」や「オホーツクに消ゆ」など、あの時代のゲームのような雰囲気を目指しています。ただ、今回はApple Foundation Models(以下AFM)のオンデバイスモデルを活用し、コマンドを指定する代わりに、自分の言葉で登場人物に話しかけられるテキストアドベンチャーゲームとして開発しています。
ゲーム自体は概ね完成し、シナリオの微調整とデバッグに取り掛かっています。開発を進める中で、小規模なLLMをゲームに応用する際の知見が少し溜まってきました。そこで、本編の公開に先立ち、エンジンの仕組みを試せる小さなデモ「血の池の夜」を作りました。GitHubでソースコードも公開しています。
この連載では、テキストアドベンチャーゲームでLLMをどこに使うか、登場人物らしい会話をどう作るか、そして毎回違う返事をするゲームをどうテストするかを、実装と開発中に起きた失敗を交えて紹介します。
第1回は、デモの内容とエンジン全体の設計を説明します。Swiftの細かな実装は次回以降に扱います。
なお、この記事は2026年10月10日時点のデモの実装を対象にしています。測定値には、それぞれの測定日と環境を併記しています。
1. デモ「血の池の夜」でできること
舞台は、別府にあるスナック「血の池」。主人公は、従業員の「葵」と食事を済ませ、開店前の店に戻ってきます。

カウンターでは、「ママ」がグラスを拭きながら支度をしています。そこから「若いお客さんにも来てもらうには、何を変えたらいいか」という話が始まります。
プレイヤーは、たとえば次のように入力できます。
このお店の入り口って、入りにくくない?
流す曲を変えてみたら?
看板を新しくするのはどう?
画面に並んだ選択肢を選ぶ代わりに、自分の言葉で話題を振ります。店内ではママの意見を聞き、控え室では葵と二人で相談できます。(本編では捜査のために控え室に入るようになっており、その仕様を引きずっています。現実のお店では客がバックヤードに入るのはご法度です。)
葵、ちょっと控え室で話そう。
場所を変えると、そこにいる人物や会話の内容も変わります。話し相手は、文の頭で名前を呼ぶか、画面のメニューで指定できます。移動先も画面から選べます。
その場に登場人物が複数いるとき、誰に話しかけたのかを自由入力だけで判断するのは難しい課題です。現実なら視線なども手がかりになりますが、テキストではそうした情報がないため、名前の呼びかけや画面での選択を使っています。
店の相談以外にも、ママや葵の好きなものや普段の暮らしを聞いたり、プレイヤー自身の好みを話したりできます。
葵、好きな映画は?
俺はジャズが好きなんだ
このデモには、クリア条件やエンディングはありません。店内と控え室を行き来しながら、台本の会話、自由な雑談、記憶の仕組みを試すためのものです。
2. なぜAFMを使うのか
AppleのFoundation Models frameworkには、Apple Intelligenceの端末内モデルに加え、クラウド上のPrivate Cloud Compute(PCC)で動作するモデルも用意されています。PCCのモデルはPrivateCloudComputeLanguageModel、オンデバイスのモデルはSystemLanguageModelとして提供されています。詳しくはAppleのPCCモデルの説明とSystemLanguageModelの説明を参照してください。
ただし、PCCの利用には条件があります。2026年10月13日時点のAppleの案内では、App Store Small Business Programへの加入、各アプリのApp Storeでの初回ダウンロード数が200万未満であること、アカウントへのPCCの利用権限(entitlement)の付与が必要です。今回の開発環境では現時点でPCCを利用できておらず、また、オンデバイスで処理することが目的でもあるので、PCCでの動作や性能は未検証です。
今回のデモでは、SystemLanguageModel.defaultを使い、入力の解釈・自由会話・記憶の取り出しをすべてオンデバイスで処理します。 モデルが利用できる状態であれば、インターネット接続なしで動作します。PCCへの切り替えやフォールバックは行っていません。
今回のゲームで端末内モデルを選んだ理由は、繰り返し会話するゲームを、外部のLLM APIに接続せずに動かしたかったからです。この構成なら、プレイヤーが入力した言葉を外部に送らずに処理でき、外部APIの呼び出し料金も発生しません。
一方で、開発中には、話題の取り違えや、同じ話の繰り返しが起きました。自由に話せることと、こちらの意図したゲーム進行が成立することは、別々に考える必要があると感じました。
そこで、今回はAFMに任せる仕事を絞り、ゲームの進行をコードと台本で制御する設計にしました。
3. 設計方針:物語の進行は台本とルールで決める
今回の構成では、場面の流れ、地の文、重要な台詞、場所の移動を、台本とルールエンジンが決めます。AFMは、プレイヤーの言葉を台本につなぎ、定型台詞を用意していない会話への返事を生成します。
たとえば、店の音楽の話題では、台本に次のやり取りを書いてあります。
ママ:音楽は昭和歌謡よ、決まってるじゃない。……葵ちゃんの映画音楽? あれ、眠くならない?
葵:失礼ね。夜に合うのよ、ああいうのが。
これは実際の台本にある定型台詞で、LLMの生成例ではありません。
ママは昭和歌謡を流したい。葵は映画音楽が夜に合うと考えていて、ママの言葉に言い返す。この人物同士のやり取りは、作者が書いたとおりに出力します。台詞に伴う表情などの演出も、台本で指定します。
ただし、プレイヤーに「音楽について話す」というボタンを押してもらう必要はありません。「流す曲を変えてみたら?」という言葉を、その話題への入力として解釈します。
担当を整理すると、次のようになります。
| 処理 | 担当 |
|---|---|
| プレイヤーの自由入力がどの話題・移動に当たるかの解釈 | AFMと補正のルール |
| 定型台詞の選択、場面やフラグの更新 | ルールエンジン |
| 地の文と重要な台詞の本文 | 台本 |
| 定型台詞のない話題や雑談への返事 | AFM |
| プレイヤーが話した好みなどの抽出 | AFMと検査のルール |
言葉の解釈には揺れがありますが、ルールエンジンによる進行は、同じ台本・状態・解釈済みの入力なら同じ結果になります。 自由会話で生成する返事の文面には、引き続き揺れがあります。
この分担には、テスト上の利点もあります。生成された台詞の全文を固定しなくても、「音楽の話題として扱われたか」「台本の所定の行が出たか」を確認できるからです。
エンジンの全体図
まず、入力から画面への表示までの全体図を見てください。プレイヤーについての記憶の取り出しと、その後の会話への反映も含めています。記憶の詳しい実装は第3回で扱いますが、今回はその役割を紹介します。
青い枠がAFM、黄色い枠がコードで処理するルール、緑の枠が台本データです。まずは、青い3つの枠と、中央のルールエンジンに注目してください。
- ①入力の解釈:プレイヤーの言葉を、話題や移動の候補に当てはめる。
- ②自由会話:定型台詞で受けない会話に、人物として返事をする。
- ③記憶の取り出し:プレイヤーが話した自分のことを覚え、その発言を聞いた人物の後の会話に使う。
中央のルールエンジンが、解釈された入力を受けて台本の行を選び、場面やフラグを更新します。図の各枠の下には、対応する型・関数・ファイル名も載せています。候補の作成や解釈の補正、出力の検査などの細かな実装は、次回以降に説明します。
ここからは、この全体図に沿ってAFMの3つの役割を見ていきます。
4. AFMが担当する3つの役割
4.1 プレイヤーの自由入力を、今選べる候補に分類する
最初の役割は、プレイヤーの文を、台本にある話題や移動の候補に当てはめることです。
流す曲を変えてみたら? → 意図:音楽
控え室で話そう。 → 移動:控え室
上の例では、「流す曲を変えてみたら?」を「音楽について話したい」、「控え室で話そう」を「控え室に移動したい」という意味の入力として扱います。意図:音楽と移動:控え室は、それぞれの解釈をゲーム内で表すための記法です。ただし、AFMが常にこのように解釈できるとは限らず、別の話題や移動に取り違えることもあります。
解釈の候補は固定ではなく、今いる場所や話し相手に応じて変わります。実装では、その時点の候補を使って、AFMが出力できる値を定めた「構造化出力のスキーマ」を毎回作ります。AFMには、その候補の一つ、または「どれでもない」を選ばせています。
ここでAFMが出力するのは、選んだ候補を示す記号です。コードでその記号を話題や移動の入力に変換し、ルールエンジンへ渡します。どの台詞を表示するか、どの場面へ移るかは、その後の処理で決まります。
モデルの解釈結果には、ルールによる補正も加えています。たとえば、開発中には「今度一緒に映画見ようよ」という発言で、控え室への移動が選ばれてしまいました。この発言は、台本に用意した話題にも、今いる場所からの移動にも当たりません。本来は「どれでもない」に分類し、移動せずに自由会話で返事をする想定です。「どれでもない」は発言を無視する意味ではなく、定型台詞や移動に結び付けずに会話として受けるための選択です。
こうした取り違えを減らすため、モデルが移動を選んだ場合には、発言に行き先や移動の言い方が含まれているかをルールで確認しています。
対応する実装は、InputInterpreter.swiftです。動的スキーマと補正の詳細は、第2回で説明します。
4.2 登場人物として、短い返事を生成する
定型台詞のない話題や雑談には、AFMが人物として返事をします。今回の開発では、返事が長くなると、自己矛盾や設定にない話が出やすくなると感じました。そのため、現状では2〜3文を目安に生成しています。
モデルに渡すのは、人物の名前だけではありません。
- 人物の設定、性格、話し方、台詞の例
- 今の場面での振る舞い
- 今の話題について、その人物が持っている考え
- これまで定型台詞で話したことと、直前のやり取り
- その人物が知っている主人公の好みなど
たとえば、赤いドアについて、ママは「開ける楽しみがある」と考えています。葵はママの前ではドアを変える案を出しますが、本当は中が見えない感じを気に入っています。控え室では、その本音を話すように設定しています。同じ質問でも、誰に、どこで聞いたかによって、返事に使う情報や話し方が変わります。
自由会話では、人物の立場や考えを台本に書き、AFMがその場の言い方を作ります。ただし、物語の展開に関わる話題では、AFMに返事を自由生成させると、作者が予定していない約束や関係の変化を語ってしまうおそれがあります。
たとえば、主人公と葵の将来については、このデモの会話で勝手に話を進めてほしくありません。そこで、入力が「二人の先の話」と解釈された場合は自由生成せず、葵の「……今夜は、その話はなし。お店の話でしょ。」など、台本に用意した台詞を表示します。将来の話をかわして、今夜の店の相談に戻すところまで、作者が決めているわけです。(主人公と葵の将来については、本編をプレイしていただけるとわかるようになっています。)
自由会話では、1往復ごとに新しいモデルセッションを作り、必要な情報を組み立てて渡しています。人物の設定と規則はinstructions側に、プレイヤーの発言や記憶は入力側に入れます。
生成後には、相手の呼び方、同じ台詞の繰り返し、今の発言と関係のない話題の引きずり、作品世界の外に出てしまう言葉などを検査します。必要なら1回だけ作り直し、それでも使えない場合は用意した定型台詞に戻します。これは不自然な返事を減らすための仕組みで、会話の品質を完全に保証するものではありません。
実装は、Talk.swiftとSafeTalker.swiftにあります。
4.3 プレイヤーが話した、自分のことを覚える
三つ目は、プレイヤーが話した好みや出身などを取り出す機能です。
たとえば「俺はジャズが好きなんだ」という発言から、好きな音楽として「ジャズ」を抽出し、検査を通った値だけを記憶します。今回、記憶できる項目は、好きな食べ物、飲み物、音楽、映画、店の雰囲気、出身、趣味、苦手なものの8種類です。
記憶は、会話履歴を丸ごと持ち続ける仕組みとは別に管理しています。返事の後にバックグラウンドで抽出し、後の会話で話題が合うときに使います。
ここで大切なのは、誰がその発言を聞いたかです。
控え室で葵にだけ話した好みは、ママの会話用プロンプトには入れません。一方、店内にはママと葵がいるため、そこで抽出・検査を通った情報は、二人が聞いた情報として記憶します。
記憶を会話に入れるかどうかはコードで決めます。ただし、渡した情報を返事にうまく反映できるかには生成の揺れがあり、抽出自体にも誤りが残ります。抽出の誤りでゲームの進行が変わるのを避けるため、今回は記憶を雑談にだけ使い、物語の進行条件には使っていません。
実装は、ProtagonistMemory.swiftにあります。
5. 一回の入力が画面に出るまで
先ほどの全体図のうち、①入力の解釈からルールエンジンを通り、定型台詞を表示する経路を追ってみます。
具体例として、店内で葵から「あなたはどう思う?」と聞かれた後に、ママに「流す曲を変えてみたら?」と入力した場合を追います。
- 入力を検査し、現在の状態から話題・移動の候補を作る。
- 入力を分類し、補正を経て「意図:音楽」として扱う。
- ルールエンジンが台本の条件に合う行を選ぶ。
- ママ、葵の順に、先ほど紹介した定型台詞を表示する。
この場合、AFMは入力の解釈に使いますが、表示する2行の台詞は生成しません。
この音楽の定型台詞は、表示済みかどうかを記録し、同じプレイ中には再び表示しないようにしています。そのため、もう一度音楽の話題を振っても、先ほどの2行は繰り返しません。代わりに、台本に書いた「返事をする人物の音楽についての考え」をAFMに渡し、今の発言に対する返事を生成します。最初は台本どおりのやり取りで人物の立場を示し、その後は同じ話題について自由に会話を続けられる仕組みです。
入力の解釈に失敗した場合は、「どれでもない」として処理を続けます。自由会話の生成が止められた場合にも定型台詞に戻せるようにし、失敗時の動作をコード側に用意しています。
6. ソースコードの構成
実装は、SwiftUIのアプリと、SwiftパッケージのYukemuriに分かれています。
UI/
UI/Resources/scenario.json 台本と人物設定
UI/Resources/Graphics/ 背景と人物の画像
UI/ SwiftUIの画面と操作
Yukemuri/
Sources/YukemuriEngine/ LLMを使わないルールエンジン
Sources/YukemuriLLM/ 入力の解釈・自由会話・記憶
Sources/yukemuri/ CLIと評価用コマンド
Tests/ ユニットテスト
Evaluation/ 評価用の入力と期待結果
台本のscenario.jsonには、台詞だけでなく、話題の説明と発言例、人物の設定、知識、台詞の条件、場面や演出の情報も書いてあります。
ルールエンジンのYukemuriEngineは、Foundation Modelsに依存しません。解釈済みの入力とゲームの状態を受け取り、新しい状態と、表示すべき行や演出を返します。
YukemuriLLMのGameSession.swiftが、入力の解釈、ルールエンジン、会話の生成、記憶をつなぎます。画面とCLIの両方が、この仕組みを使います。
台本の書き方に興味があれば、データ仕様も参照してください。
7. デモの試し方と現在の限界
動作環境と起動
このデモの動作環境は、次のとおりです。Foundation Models framework全体の最低要件ではなく、このリポジトリの実装を対象にした要件です。
- macOS 27以降のAppleシリコン搭載Mac
- Apple Intelligenceが有効で、端末内モデルが利用できる状態
- ソースからビルドする場合はXcode 27以降(Swift 6.2)
GitHubのReleases(v1.0)からDMG版をダウンロードできます。配布アプリはAppleの公証を受けていないため、初回起動時の手順はREADMEを参照してください。
ソースから動かす場合は、リポジトリを取得して、UI/UI.xcodeprojをXcodeで開き、UIスキームをMy Macで実行します。
Apple Intelligenceが無効、モデルが準備中などでAFMを使えない場合は、エラーを表示して停止します。
最初に試してほしいこと
最初は、次の順に試すと仕組みが分かりやすいと思います。
- 冒頭の会話に一度返事をし、葵の「あなたはどう思う?」まで進めてから、ママに店で流す音楽の話を振る。
- 同じ話題を別の言い方でもう一度振り、定型台詞の後の自由会話を見る。
- 控え室に移り、葵にだけ自分の好きな音楽などを話す。
- 別の会話を挟んで葵に「俺の好きな音楽、覚えてる?」と聞き、店内へ戻ってママにも同じ質問をする。
記憶の抽出はバックグラウンドで行うため、好みを話した直後ではまだ反映されていない場合があります。また、生成の返事は毎回同じにはなりません。
ソースからデバッグ版を動かすと、メニューの「開発」から、解釈の候補と結果、返事の生成元、モデルに渡したプロンプト、抽出した記憶を確認できます。見た目の会話だけでなく、内部で何が起きたかを追えます。
配布するリリース版はプレイログを残しません。デバッグ版は入力やプロンプトを端末内のログに記録しますが、「開発」メニューから記録を止められます。入力の検査で止めた発言など、本文を記録しない入力もあります。
生成には待ち時間と揺れがある
2026年10月9日のM4 MacBook Air・macOS 27.0.1での測定では、入力の解釈は中央値0.5秒、2〜3文の台詞生成は中央値3.2秒でした。デモの評価と100ターンの試しプレイで得た値で、端末や入力によって変わります。
定型台詞で答える場合にも解釈の待ち時間があり、自由会話の場合はさらに生成の時間がかかります。生成が長時間戻らなかった事例もあり、待ち時間の扱いは課題として残っています。
また、ルールによる補正や出力の検査を入れても、話題の取り違え、繰り返し、設定と食い違う返事を完全にはなくせていません。テストで見つかった事例を台本・知識・ルールの改善につなげ、その入力を回帰テストに追加しています。
8. 次回以降に扱うこと
連載では、次の順に実装と検証を紹介する予定です。
| 回 | 内容 |
|---|---|
| 第1回 | 全体設計とデモ「血の池の夜」(この記事) |
| 第2回 | 自由入力を台本の選択肢につなぐ |
| 第3回 | 登場人物の会話と記憶を作る |
| 第4回 | LLMを使ったゲームのテスト設計 |
| 第5回 | プレイで見つかった失敗と改善 |
次回は、自由入力をどう話題・移動の候補につなぐかを扱います。現在の状態から候補を作り、DynamicGenerationSchemaで出力の形を制約し、モデルの取り違えをルールで補正するまでを、ソースコードに沿って説明します。
