マガジン

テキストアドベンチャーの作り方:コマンド解析の基本

遊びをつくる

執筆にClaudeを使用しています。

遊ばれた回数、好意、感想を表す三種類の小さな札を比べる人の手。
記事のテーマをもとに制作したAI生成イラスト。

テキストアドベンチャーを作ろうとして最初に詰まるのは、「プレイヤーが入力した文字をどう解釈するか」という問題です。絵も音もない分、ゲームとプレイヤーをつなぐ唯一の接点がコマンド解析であり、ここの設計がゲームの遊びやすさを決めます。 この記事では、テキストアドベンチャー特有の「コマンドを受け取って意味を取り出す」仕組みに絞って、考え方・実装の手順・失敗しやすい点を順に説明します。Phaser や Unity などのゲームエンジンを使わず、HTMLとJavaScriptだけで動く構成を前提にしていますが、考え方はどの言語でも共通です。

この記事の目次
  1. コマンド解析とは何をする処理か
  2. 解析方式を選ぶ:3つのアプローチ
  3. 動詞・名詞分割方式の実装手順
  4. 失敗しやすい点と対策
  5. 解析の先:文脈を持たせる
  6. 次の行動

コマンド解析とは何をする処理か

プレイヤーが「北へ行く」「剣を拾う」「老人に話しかける」と入力したとき、プログラムはその文字列から「何をしたいのか」を取り出さなければなりません。この処理をコマンド解析(パース)と呼びます。

解析の目的は、入力文字列を「動詞(アクション)」と「名詞(対象)」のペアに変換することです。たとえば「剣を拾う」であれば { action: "拾う", target: "剣" } という構造に変換できれば、あとのゲームロジックはこのオブジェクトを使って処理を分岐させるだけで済みます。

テキストアドベンチャーのコマンド解析が難しいのは、同じ意図でも表現が無数にあるからです。「拾う」「取る」「手に入れる」「ゲット」はすべて同じアクションを指す可能性があります。この揺れをどこまで吸収するかが、設計の核心になります。

解析方式を選ぶ:3つのアプローチ

完全一致方式

入力を trim() して小文字化したあと、あらかじめ用意したコマンド文字列と完全一致させる方式です。実装が最も単純で、バグも出にくいです。

コマンド一覧: ["北へ進む", "剣を拾う", "扉を開ける", ...]
入力 === コマンド一覧[i] であれば対応処理を実行

欠点は、「剣を取る」のように言い換えただけで動かなくなることです。ヒント機能や選択肢UIと組み合わせることが前提になります。プロトタイプや短編作品に向いています。

動詞・名詞分割方式

入力を「動詞」と「名詞」に分けて解釈する方式です。各動詞にエイリアス(別名)を持たせることで、表現の揺れを吸収します。

js
const verbMap = {
  "拾う": ["取る", "ゲット", "手に入れる", "pick", "get"],
  "調べる": ["見る", "確認する", "examine", "look"],
  "移動": ["行く", "進む", "歩く", "go", "move"],
};

入力を空白・助詞で分割したあと、各トークンが verbMap のいずれかのエイリアスに一致すれば、正規化された動詞として扱います。名詞側も同様に nounMap を用意しておきます。この方式が、中規模のテキストアドベンチャーでは最もバランスがよいです。

NLP(自然言語処理)方式

形態素解析ライブラリを使い、入力を品詞単位に分解してから解釈する方式です。表現の自由度が上がる反面、ライブラリの導入コストがかかり、ブラウザで動かす場合はファイルサイズにも注意が必要です。kuromoji.js などのブラウザ対応ライブラリが選択肢になります。小規模・中規模のゲームでは動詞・名詞分割方式で十分なケースがほとんどです。

動詞・名詞分割方式の実装手順

1. 入力の正規化

全角・半角、大文字・小文字、前後の空白を統一します。

js
function normalize(input) {
  return input
    .trim()
    .toLowerCase()
    .replace(/[A-Za-z0-9]/g, s => String.fromCharCode(s.charCodeAt(0) - 0xFEE0));
}

正規化を最初に行うことで、その後のマッチング処理を単純に保てます。

2. 動詞と名詞を取り出す

正規化した文字列を、助詞・助動詞で区切ってトークンに分割します。日本語の助詞(を・に・で・へ・は・が)をセパレータとして使うと、多くのケースに対応できます。

js
function tokenize(input) {
  return input.split(/[をにでへはが、。\s]+/).filter(t => t.length > 0);
}
// "剣を拾う" → ["剣", "拾う"]
// "北へ進む" → ["北", "進む"]

トークンをそれぞれ verbMap と nounMap に照合し、一致すれば正規化された動詞・名詞に変換します。

3. ゲームロジックへ渡す

js
function parse(input) {
  const tokens = tokenize(normalize(input));
  let action = null;
  let target = null;

  for (const token of tokens) {
    if (!action) action = resolveVerb(token);   // verbMapで解決
    if (!target) target = resolveNoun(token);   // nounMapで解決
  }
  return { action, target };
}

parse() の戻り値を受け取ったゲームロジック側は、action と target の組み合わせでスイッチすればよいだけになります。解析ロジックとゲームロジックを分離することで、どちらも独立してテストできます。

4. 解析できなかった場合の処理

action が null のまま戻ってきた場合は、「その言葉は分かりません」といったメッセージを表示します。このとき「使えるコマンドは『調べる』『拾う』『移動』などです」のようにヒントを添えると、プレイヤーの離脱を防げます。エラーを無言で握りつぶさないことが、テキストアドベンチャーの操作感に直結します。

失敗しやすい点と対策

助詞ありきの分割が崩れるケース:「北」のように助詞なしで一語だけ入力されると、分割後のトークンが ["北"] だけになります。動詞がなくても action を推測できるよう、「方角だけの入力は移動とみなす」「アイテム名だけの入力は調べるとみなす」などのデフォルト動詞ルールを持っておくと安全です。

同名の動詞と名詞が衝突するケース:「火を使う」の「火」が動詞マップにも名詞マップにも存在すると、解析が誤る場合があります。マップの作成時にキーの重複をチェックするユーティリティを書いておくと、デバッグが楽になります。

コマンドが増えるとマップが肥大するケース:動詞・名詞のマップはゲームが大きくなるほど増えます。JSONファイルに外出しして読み込む形にすると、ゲームロジックとデータを分離でき、後からの追加・修正が容易になります。

プレイヤーが試した入力をログしない:開発中に実際に「どんな入力をしたが解析できなかったか」を記録しておくと、エイリアスの抜けを発見できます。ブラウザのコンソールに未解析入力を出力するだけでも、テスト効率が大きく上がります。

解析の先:文脈を持たせる

コマンド解析ができたら、次の課題は「文脈」です。「それを拾う」の「それ」が何を指すかを解決するには、直前の入力・出力の履歴をゲーム側が保持しておく必要があります。lastMentionedNoun のような変数に直前に言及した名詞を保存しておき、名詞が解析できなかった場合のフォールバックとして使う方法が最も実装しやすいです。

また、現在のシーン(部屋・場面)に存在しないアイテムや出口を指定したときは、解析は成功しても「ここにはそれがありません」と返すロジックが必要です。解析層と存在チェック層を分けて設計しておくと、どちらの問題かが素早く特定できます。

次の行動

動詞・名詞分割方式から始めてみてください。まず5〜10個の動詞と10〜20個の名詞だけでマップを作り、簡単な一部屋のシーンで解析が通るか確認するのが最短の出発点です。動作確認ができたら、HTMLとJavaScriptにまとめてアソバースに投稿すると、他のプレイヤーからの反応をもとにコマンドの抜けを見つけやすくなります。