AIゲームの作り方 約12分で読めます

AI会話ゲームの作り方|NPCと話すコード全体と、20往復の費用の試算【2026年版】

宿屋のカウンターで、旅人と店主が向かい合って話している様子のイラスト。吹き出しが浮かんでいる

AIと会話するゲームは、ゲームの進行と正解をコードで持ち、台詞だけをAIに作らせる形で作ります。AIの返事をそのまま信じず、形を検査してから好感度や進行に反映します。この記事のコードは、宿屋の主人と話して「倉庫の鍵の場所」を聞き出す1本のHTMLです。

コードの動作は、決まった返事を返す偽のAIで確かめました。本物のAPIは呼んでいません。その代わり、20往復の会話にかかるAPI費用を、公式の料金表から計算して載せています。

この記事で分かること

  • AI会話ゲームの仕組みを、4つの部品に分けた全体像
  • そのまま動くコード全体と、動かして確かめた結果
  • AIに任せることと、コードで持つことの分け方
  • 20往復の会話にかかる費用の試算と、キャッシュが効く条件
  • 確かめられていないこと

すでに遊べるAI会話ゲームを探している場合は、AIと会話できるゲームおすすめが向いています。ここでは、自分で作る側の話です。

この記事の目次
  1. 01AI会話ゲームの仕組みは4つの部品
  2. 02コード全体
  3. 03動かして確かめた結果
  4. 04設計の要点
    1. 正解の文章は、AIに渡さずコードで持つ
    2. AIの返事は、検査してから使う
    3. 履歴は直近だけ送る
  5. 0520往復の費用を試算する
    1. キャッシュは、設定が一定の長さを超えないと効かない
  6. 06APIキーは、HTMLの中に書かない
  7. 07確かめられていないこと
  8. 08うまくいかないとき
  9. 09よくある疑問
  10. 10この記事のまとめ

AI会話ゲームの仕組みは4つの部品

プレイヤーが1回話すたびに、次の4つの処理が順に動きます。

AI会話ゲームの4つの部品を示した図。1は話しかける。入力を履歴に足す。2は履歴を絞る。直近6往復だけ送る。3はAIを呼ぶ。設定と履歴を送る。4は返事を検査する。形を確かめてから使う。検査を通った返事だけが、好感度や進行を動かす
検査を通った返事だけが、ゲームの状態を動かします

AIを呼ぶ部分は、コードの中の1つの関数にまとめます。偽のAIから本物のAPIへ替えるときに、書き換える場所がそこだけで済むためです。NPCにAIを使う考え方の全体は、AI NPCとはで扱っています。

コード全体

次のコードを、index.htmlという名前で保存してブラウザで開くと、そのまま動きます。サーバーは要りません。「こんにちは」「ありがとう」「お願い」を含む発言で好感度が上がり、「うるさい」「黙れ」で下がる、偽のAIが入っています。

<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>宿屋の主人と話すゲーム</title>
<style>
body{font:16px/1.6 sans-serif;max-width:560px;margin:0 auto;padding:12px}
#log{height:320px;overflow-y:auto;border:1px solid #bbb;padding:8px;background:#fafafa}
.me{color:#235}.npc{color:#820}.sys{color:#777;font-size:14px}
form{display:flex;gap:6px;margin-top:8px}input{flex:1;padding:8px;font-size:16px}
</style>
</head>
<body>
<h1>宿屋の主人ハル</h1>
<p>好感度を3以上にして、倉庫の鍵の場所を聞き出してください。</p>
<div id="log"></div>
<form id="f"><input id="t" autocomplete="off" placeholder="話しかける"><button>送る</button></form>
<p class="sys">好感度 <span id="aff">0</span> / 5</p>
<script>
// 1. ゲームの状態と正解は、コードの側で持つ(AIには渡さない)
const state = { affinity: 0, history: [] };
const KEY_PLACE = '鍵は、厨房の棚の裏にあるよ。';
const MAX_TURNS = 6; // AIに送る履歴は、直近6往復まで

const SYSTEM = 'あなたは宿屋の主人ハル。気さくだが、礼儀のない客には冷たい。' +
  '必ず次のJSONだけを返す。{"reply":"台詞","mood":"happy|neutral|angry","delta":-1|0|1}';

// 2. AIを呼ぶ部分。ここだけ差し替えれば、本物のAPIにできる
async function askNpc(messages) {
  // 本物のAPIにするときは、自分のサーバーの中継先に送る(APIキーをHTMLに書かない)
  // const r = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' },
  //   body: JSON.stringify({ system: SYSTEM, messages }) });
  // return (await r.json()).text;
  return mockAi(messages); // 動作確認用の、決まった返事を返す偽物
}

function mockAi(messages) {
  const last = messages[messages.length - 1].content;
  if (last.includes('壊れ')) return 'JSONではない返事';
  if (/ありがとう|お願い|こんにちは/.test(last)) return '{"reply":"おや、礼儀正しいね。気に入ったよ。","mood":"happy","delta":1}';
  if (/うるさい|黙れ/.test(last)) return '{"reply":"口の利き方に気をつけな。","mood":"angry","delta":-1}';
  return '{"reply":"ふうん。それで?","mood":"neutral","delta":0}';
}

// 3. AIの返事は、そのまま信じず、形を検査してから使う
function parseReply(text) {
  try {
    const o = JSON.parse(text);
    if (typeof o.reply !== 'string') throw 0;
    const delta = Number.isInteger(o.delta) ? Math.max(-1, Math.min(1, o.delta)) : 0;
    return { reply: o.reply, delta };
  } catch (e) {
    return { reply: '……(聞き取れなかったようだ)', delta: 0 }; // 失敗しても、ゲームは止めない
  }
}

function trimHistory(h) { return h.slice(-MAX_TURNS * 2); }

const log = document.getElementById('log');
function say(cls, text) {
  const p = document.createElement('p'); p.className = cls; p.textContent = text;
  log.appendChild(p); log.scrollTop = log.scrollHeight;
}

async function send(text) {
  say('me', 'あなた:' + text);
  state.history.push({ role: 'user', content: text });
  const raw = await askNpc(trimHistory(state.history));
  const { reply, delta } = parseReply(raw);
  state.history.push({ role: 'assistant', content: reply });
  state.affinity = Math.max(0, Math.min(5, state.affinity + delta));
  document.getElementById('aff').textContent = state.affinity;
  say('npc', 'ハル:' + reply);
  // 4. 鍵の場所は、好感度の条件をコードで確かめてから出す
  if (text.includes('鍵')) {
    say('npc', 'ハル:' + (state.affinity >= 3 ? KEY_PLACE : 'そんな話は、まだできないね。'));
  }
}

document.getElementById('f').addEventListener('submit', (e) => {
  e.preventDefault();
  const t = document.getElementById('t');
  if (t.value.trim()) send(t.value.trim());
  t.value = '';
});
</script>
</body>
</html>

動かして確かめた結果

コードを動かした画面と、確かめた6つの結果。好感度0で鍵を聞くと断られる。こんにちはを3回で好感度3。好感度3で鍵を聞くと場所を教えてくれる。壊れた返事は代わりの台詞で続行。うるさいで好感度が下がり2。履歴が34件に増えても、AIに送るのは12件
表示はWindowsのChromeで確かめました

画面を出さないChromeで、発言を順に送って確かめました。結果は次の表のとおりです。

確かめたこと 結果
好感度0で「鍵」と聞く 「そんな話は、まだできないね。」と断られた
「こんにちは」を3回送る 好感度が0から3になった
好感度3で「鍵」と聞く 「鍵は、厨房の棚の裏にあるよ。」と出た
JSONではない返事が返る 「……(聞き取れなかったようだ)」と出て、好感度は変わらず、ゲームは続いた
「うるさい」と送る 好感度が3から2に下がった
さらに10回話して、履歴が34件になる AIに送る履歴は12件(6往復分)に収まった
deltaが5の返事や、整数でない返事 5は1に切り詰められ、0.5は0として扱われた

設計の要点

AIに任せることとコードで持つことの分け方の図。AIに任せるのは、台詞の言い回し、想定外の雑談への受け答え、感情の向き。コードで持つのは、好感度の合計と増減の上限、鍵の場所など正解の文章、進行のフラグ、送る履歴の長さ、失敗したときの代わりの台詞
分け方の目安です

正解の文章は、AIに渡さずコードで持つ

鍵の場所のような正解は、コードの定数に置いています。システムプロンプトに入れると、会話の中で聞き出される恐れがあるためです。AIに渡すのは、キャラクターの口調と、返事の形だけにしました。この点は、本物のAIでは試していません。

AIの返事は、検査してから使う

返事の形を指定しても、JSONが崩れたり、決めていない値が入ったりすることがあります。コードのparseReplyは、replyが文字列かを確かめて、deltaを-1から1の整数に収めます。失敗したときは、決めておいた台詞を出して、ゲームを続けます。

履歴は直近だけ送る

会話が長くなると、毎回送る文字量が増えて、費用も増えます。コードでは、直近6往復だけを送っています。どのくらい違うかは、次の節で試算します。

20往復の費用を試算する

1プレイの費用は、「送る入力」と「返ってくる出力」のトークン数に、各社の単価をかけて出します。入力は、毎回「設定+履歴+今回の発言」を送るため、往復が増えるほど膨らみます。

試算の前提は次のとおりです。設定(キャラクターと世界観)は800トークンと仮定しました。プレイヤーの発言とNPCの返事は、この記事のコードの台詞に近い短文を用意して、OpenAI系のトークナイザー(o200k_base)で数えた平均です。プレイヤーは約7トークン、NPCは約30トークンでした。Claude・Geminiのトークンの数え方は、これと違います。

20往復の会話にかかる費用の試算。Claude Haiku 5.5は、全履歴を毎回送ると0.41円、直近6往復だけで0.36円、直近6往復と設定のキャッシュで0.13円。gpt-6-lunaは0.41円と0.36円で、キャッシュは1,024トークン未満で対象外。Gemini 2.5 Flash-Liteは0.41円と0.35円で、キャッシュは公式の表になく未計算。Claude Sonnet 5.5は8.3円、7.2円、2.4円
設定が長いほど、毎回送る設定の分が費用の大半を占めます

料金は、2026年10月10日に各社の公式ページで確かめた値(Claude Haiku 5.5は入力100万トークンあたり0.10ドル・出力0.50ドル)を使い、1ドル158.3円で計算しました。最新の料金は、公式ページか、当サイトのAI API料金計算機で確かめられます。

40往復にすると、Claude Haiku 5.5では、全履歴で約1.1円、直近6往復だけで約0.73円でした。往復が増えるほど、履歴を絞った効果が大きくなります。

キャッシュは、設定が一定の長さを超えないと効かない

毎回同じ設定の部分は、キャッシュに載せると、読み込みの単価が下がります。ただし、キャッシュに載せられる長さに下限があります。公式の資料では、次のとおりでした(2026年10月26日に確認)。

  • Claude:Claude Haiku 5.5・Claude Sonnet 5.5は512トークン以上
  • OpenAI:GPT-5.6以降は1,024トークン以上
  • Gemini:表にあるGemini 2.5 Flashは2,048、Gemini 3.8 Flashは4,096トークン以上

この記事の試算の設定は800トークンです。Claudeは512トークンを超えるので、キャッシュの試算に入れました。gpt-6-lunaは、1,024トークンに届かないため対象外にしています。Gemini 2.5 Flash-Liteは、公式の表に載っていないので計算していません。キャッシュの書き込み時の割増も、試算に入れていません。

APIキーは、HTMLの中に書かない

ブラウザで動くHTMLは、ソースを開けば誰でも読めます。APIキーを書くと、第三者に使われて、請求が自分に来るおそれがあります。コードのコメントにあるように、自分のサーバーの中継先にだけ送り、キーはサーバー側で付けるのが基本です。中継先のサーバーは、この記事では作っていません。公開の手順は、ブラウザゲームの公開方法にあります。

確かめられていないこと

  • 本物のAPIでの動作偽のAIで確かめています。本物のAIが、指定した形で返すかは、確かめていません。
  • 返事の速さ(待ち時間)と、会話が長くなったときの質
  • 鍵の場所を、会話で聞き出そうとする入力への強さ
  • トークン数は、OpenAI系の数え方での概算で、Claude・Geminiの実際の数ではありません
  • スマートフォンでの操作
  • 中継サーバーの作り方と、悪用を防ぐ上限の設定

確かめていないものは、動かして確かめてから、別の記事で扱います。

うまくいかないとき

症状 確認すること
送っても何も起きない ブラウザのコンソールにエラーがないかを見ます。コードを途中で切って貼っていないかも確かめます
いつも代わりの台詞が出る AIの返事が、JSONだけになっているかを確かめます。説明の文章が前後に付くと、JSON.parseが失敗します
好感度が一気に上がる delta の上限(1)をコードで掛けているかを確かめます
費用が思ったより高い 設定の長さと、送る履歴の件数(MAX_TURNS)を見直します
キャッシュが効かない 設定の長さが、各社の下限を超えているかを確かめます

関連記事AIと会話できるゲームおすすめ【2026年版】スマホ・Steam・ブラウザ別

よくある疑問

プログラムが苦手でも作れますか

この記事のコードは、1本のHTMLです。AIに書かせる方法は、ChatGPT・Geminiでゲームを作る手順にあります。ただし、本物のAPIをつなぐ部分は、この記事では確かめていません。

どのAIを使うのが安いですか

この試算では、20往復で、軽量モデルは1円未満、中間クラスのモデルは数円から10円弱でした。ただし、返事の質は比べていません。

ゲームの正解を、AIに判定させてはいけませんか

この記事では、判定をコードで持つ形にしました。AIに判定させた場合の安定性は、確かめていません。

この記事のまとめ

この記事のまとめ

  • ゲームの正解と進行はコードで持ち、台詞だけをAIに作らせる
  • AIの返事は検査してから使い、失敗しても止めない
  • 履歴を直近6往復に絞ると、40往復では費用が約3割下がった(Claude Haiku 5.5の試算)
  • キャッシュは設定が各社の下限を超えたときだけ効く
  • 本物のAPIでの動作は、確かめていない