AI でのドラクエ自力攻略 · 第1回

コーディングエージェントにドラクエを攻略させる世界を作った ── 画面は見せない、答えも教えない

ai llm agents devjournal

AI に RPG を攻略させてみたいと思ったことはないだろうか? 私もそう考えたうちの一人だ。私は攻略対象にファミコン版 Dragon Quest I(以下、DQ1)を選んだ(日本語版であることは念のため明示しておく)。 そして、私も AI を使って RPG の攻略を果たした多くの人の一人になった。

私は、AI による「自律」クリアを目標にしていた。 如何に判断を AI に任せるかのバランスが重要であると考えていた。 途中、間違えても良い、迷っても良いから、AI が人間と同じように試行錯誤し、目的を果たす姿が見たいと思った。

そんな自律攻略を果たした私のお話を主にエンジニア層に向けた物語として語ろう。

当初、私が思い描いていたもの

意味を読み取らないと解けないギミック

DQ1 には代表的なギミックがいくつもある。具体例を挙げてみよう。

攻略事項 ギミック ヒントの情報源
ようせいのふえの入手 町にある1マスの水を温泉と認識しなければならない リムルダールの宿屋にいる老人がマイラのお風呂からの相対位置を調べる必要があることを教えてくれる
ガライの墓への入り口の発見 暗闇で見えない出口を通り抜けなければならない ラダトームの城の東の戦士が暗闇の壁を押す必要があることを教えてくれる
竜王の城の侵入 玉座裏の階段をしらべて発見しなければならない リムルダールの西の島にいる老人が隠された入り口があることを教えてくれる。具体的な場所までは教えてくれない!

これ以外にもあるので、遊んだことがある人は思い出してみて欲しい。 私はこの活動で改めて DQ1 に触れてわかった。 全てのギミックには必ずヒントが用意されている。 とてもよく作られたゲームだと改めて感心した。

これらのギミックの特徴はそれらのヒントの「意味」を理解する必要があるということだ。 私がクリアした小さい頃、それらのヒントをちゃんと理解して解いたとはとても思えない! 果たして AI は自力でこれらのギミックを理解し、攻略できるのか?私はそれを思うだけでワクワクした。

詰まった AI には、相談できる友達が要る

どうしてもつまった時、AI が人間に「相談」するという機能を作るのが面白そうだなと思っていた。 今やゲーム攻略情報はネットで調べる時代だが、DQ1 が出た当時、ゲーム攻略情報は人伝に得るものだった。 当時ゲームをやっていた人間であれば兄弟や友達に色々と攻略について聞いただろう? その時代のゲームなのだから AI にだって攻略を手伝ってくれる友達が必要だ。その友達とは私のことだ。 AI が私に「相談」してくるところを想像して胸が熱くなった。

私はいったいどんな答えを返すだろうか? 1 つ言えることは決して直接的な答えは返すまい。 それは AI がゲームを攻略する楽しみを奪うことになってしまうのだから。

戦闘はコードで —— ガンビットの上位版

戦闘には専用ロジックをコーディングさせることを考えていた。 Final Fantasy XII のガンビットを知っているだろうか?その上位版だ。 AI に戦闘コードを書かせるのだ。どんなコードを書くのかとても楽しみだ。

これらが開始した当初の大まかな考え方だった。 長丁場になると思っていた。 2 年の試行錯誤を経た後、それらの経験の上に閃いた新しいアイデアが状況を一変させた。 そのアイデアを元にゼロから作り直したら 2 カ月で私の妄想は実現した。

最終的に採った方法

ここからは私が「最終的に」どんな方法を採って DQ1 を AI で自律攻略したかについてお話しする。 ここに至るまでに様々な方法を試したがそれらは別のお話だ。

攻略対象は自作の DQ1 シミュレータ

まず最初に「DQ1 を」と言ったがそれは不正確であることをお詫びする。 私は AI が DQ1 の世界を観て動く試行錯誤を素早く行うために DQ1 シミュレータを自作した。 ネットや実機動作確認で情報を集めてコーディングしたのだ。 もちろんコーディングエージェントを使い倒したので全てを手で作ったわけではない。 でも、意図的に完全ではない。 例えば以下のようなところは AI に易しくなるように作った。

  • 繰り返し実行できるよう高速性を重視し、冒険開始から終了までを数秒~数分で完了できるようにした。
  • 移動 NPC が道を塞いでも影響しないよう場所の入れ替わり機能を作った。
  • 動き回るカウンター内 NPC はカウンター前に位置を固定した。
  • 再現性付き乱数機能を作った。
  • どこでもセーブ・ロードできる機能を作った。

自作だからこそ、柔軟な対応が可能であった。

攻略の定義 ── 攻略コードを書いてもらう

この DQ1 シミュレータは観察用と行動用の公開 API を 1 セット提供する。 これにより AI は「コーディングにより DQ1 の世界を冒険できる」ようになった。 私の「DQ1 の AI 自律攻略」とは「ゲーム開始から竜王撃破までの攻略コードを非対話式コーディングエージェントに開発してもらうこと」と同義となったのである。 ここでいうコーディングエージェントは具体的には Claude Code のことである。 私は勇者あいちゃんと呼んでいる。以降、この記事でもそう呼ぶことにする。

Diagram: the strategy code reaches the game only through a public API, while the implementation stays hidden

勇者あいちゃんが書いたコード

実際に勇者あいちゃんが書いたコードの一部を見せよう。

行動用の公開 API

まずは行動用公開 API の利用例からだ。

ここで大事なのは TypeScript のコードそのものではない。勇者あいちゃんは単にプログラムを書いているのではなく、冒険をしているのだ! だから、この先はコードを読むというより、勇者の旅を眺めるつもりで見て欲しい。

01-throne-to-castle.ts

/**
 * 王の間で旅立ちの宝を集め、 ラダトーム城へ降りる。
 *
 * radatome-throne スタート。 王ラルス16世の脇の宝箱 3 つを開けて初期物資を得たら、
 * 南の扉 (4,7) を抜けて階段 (8,8) を降り、 radatome-castle へ移る。 扉は 1 マスきりの
 * 南口を塞いでおり、 城へ降りる唯一の経路。
 */
import type { GameClient } from '@agent/client/client.ts';

export const title = '王の間 → ラダトーム城';

export default function (client: GameClient): void {
  client.section('旅立ちの宝');
  client.intent('王の間の宝箱 3 つを開ける');
  client.walkAndOpenChest('chest-30ea8'); // (4,4)
  client.walkAndOpenChest('chest-1c37c'); // (5,4)
  client.walkAndOpenChest('chest-46e31'); // (6,1)

  client.section('城へ降りる');
  client.intent('南の扉を開けて階段を降り、 ラダトーム城へ');
  client.walkAndOpenDoor('door-1e311'); // (4,7)
  client.walkAndEnter('stairsDown-8aa4b'); // (8,8) → radatome-castle
}

GameClient がシミュレーターへの公開 API アクセサーである。 walkAndOpenChest は宝箱まで歩いて行って中身を取る複合行動を提供する。 勇者あいちゃんの思考を反映しやすいよう行動目的に特化した複合行動を最初から提供しているのが特徴だ。 section や intent は観戦者であり友達である私が楽しく見るために書いてもらったものだ。

観察用の公開 API

次に観察用公開 API の利用例を見せよう。

/**
 * この書の関所台帳。 章順に並べる ([../../client/checkpoints.ts](../../client/checkpoints.ts) の
 * `Checkpoint`)。 書き方の手引きは [CLAUDE.md](CLAUDE.md) の「関所」。
 *
 * 耐久な事実が新しく増えた章にだけ、 到達して確かめた節目を章順で足す。 まだ無ければ空配列。
 */
import type { Checkpoint } from '@agent/client/checkpoints.ts';

export default [
  {
    after: '01-throne-to-castle',
    description: 'ラダトーム城に降りた',
    holds: (c) => c.zonesVisited().includes('radatome-castle'),
  },
  // ... 省略 ...
  {
    after: '04-buy-equipment',
    description: 'ぶきやで武器(こんぼう)と防具(ぬののふく)を装備した',
    holds: (c) => {
      const inv = c.self().inventory;
      return inv.weapon === 'club' && inv.armor === 'cloth-armor';
    },
  },
  // ... 省略 ...
] satisfies Checkpoint[];

zonesVisited は訪れたことのあるゾーンのリストだ。 01-throne-to-castle の holds は、開始ゾーンの radatome-throne から次のゾーンの radatome-castle に移動できたことを宣言するコードである。

次に self は現在の自分の状態だ。 04-buy-equipment の holds は、こんぼうとぬののふくを装備したことを宣言するコードである。

公開 API に施した工夫

観察用と行動用の API についていくつかのポイントがある。

見せるのはインターフェイスと説明コメントだけ

インターフェイス+説明コメントと実装のディレクトリを明確に分け、 プロンプトでインターフェイス+説明コメントのディレクトリのみアクセスを許可した。 実装から攻略の手掛かりを得ることを封じるためである。

エージェントに対してこれをしてはダメよ!という指示は誤解されると逆に「それをやれ!」という意味になってしまうので、 見て良い範囲の明示をしただけであり、ダメなことの指示は可能な限り避けた。

内部 ID は隠し、公開 ID はハッシュに

NPC や宝箱などの内部 ID は見るだけでその実態がわかる実装になっていた。 内部 ID とは別にハッシュ化した公開 ID を使用する仕様にした。 以下は内部 ID と公開 ID の事例である。

内部 ID 公開 ID 説明
soldier-equipment soldier-90180 装備についての話をする兵士
throne-chest-gold chest-30ea8 ゴールドが入っている宝箱

状況を確認するにしても行動を起こすにしても ID が必要になるが、コーディングエージェントは ID から意図を読み取ることができてしまう。 試走中の勇者あいちゃんから意図が読み取れてしまうよという突っ込みを受け、内部 ID からハッシュ値を求めて公開 ID とする仕組みを導入した。 ゲーム上で視認できる情報までは消さなくて良いので、soldier- や chest- などの接頭辞は残すことにした。 内部 ID 自体をハッシュにしなかったのは、シミュレーションを実装するエージェントにとって利益があるから残したいという Claude Code との議論の結果である。

どこでもセーブと、その暗号化

いつでもどこでもセーブできる API を提供した。 勇者あいちゃんはそのセーブデータを自由にロードして先の見通しを立てる実験をいつでもできる。 セーブデータには内部 ID の記載や勇者あいちゃんにとっての未知情報も載っているので不意に見られないようにするために暗号化した。 プロンプトで制限することももちろんできたと思う。しかし、事前に防げるものは仕組み的に防いだ方が良いとの判断である。

シミュレーションと記憶のシステム

下層のシミュレーションあるいは上層のエージェント層に設けた情報管理システムについて軽く説明する。

シミュレーション層 ── 状態と純関数

シミュレーション層は単体テストをしやすくシンプルな作りにした。 シミュレーション層は、以下の定義の集合である。

  • ゲームの状態
  • ゲームの状態遷移を定義する純関数

わかりやすくコードを示そう。 ゲームの状態は例えば以下のような型定義である。

type SimState = {
  zoneId: ZoneId;
  heroName: string;
  player: Position;
  inventory: Inventory;
  stats: Stats;
  combat: CombatState | null;
  rngSeed: number;
  // ... 省略 ...
};

ゲームの状態遷移を定義する純関数の集合とは例えば以下のような関数群である。

type ActionResult = {
  state: SimState;
  frame: Frame;
};

function move(state: SimState, direction: Direction): ActionResult;
function openChest(state: SimState): ActionResult;
function attack(state: SimState): ActionResult;
// ... 省略 ...

二つの記憶 ── 行動履歴と景色

上層のエージェント層はシミュレーション層のプリミティブな操作を組み合わせて意図に合わせた単位で観察用と行動用の API を提供する。 さらには専用の記憶システムを提供する役割も果たしている。 観察用と行動用の API については既に事例を出して軽く説明済みなので、ここでは専用の記憶システムについて説明する。

1 つ目はとても単純なもので実行済みの全ての行動履歴である。

2 つ目は景色に関する記憶である。 勇者は自分を中心に 15 x 15 のマスを可視とし、一度見たマスごとの床種や NPC や扉や宝箱などを記憶する。 要は勇者あいちゃんが訪れた場所の景色に関する情報だ。

静的と動的、二つの手段

勇者あいちゃんには静的と動的の 2 種類の情報参照手段を用意した。 静的なものとは、セーブ時にその時点の行動履歴と景色情報を可読な JSON ファイルに出力したもののことである。 一方で動的なものとは、観察 API による情報参照のことである。 勇者あいちゃんは、これらの情報にいつでもアクセスしてよい。

ここで改めてこの攻略はコーディングエージェントによる攻略であることを思い起こしてもらいたい。 コーディングエージェントが必要とするならば、いつでも自発的にアクセスできる手段を用意したということである。 システムプロンプト化されて常に開示しているわけではない。 静的な JSON ファイルは、直接読んでも良いし、python を使って分析しても良い。 動的な情報参照は、コンソール出力経由で読んでも良いし、アルゴリズムに直接組み込んで使ってもよい。 私は静的な JSON ファイルのフォーマットに関する情報と、動的な観察用 API の情報は与える。 しかし、その具体的な扱い方についてはその一切をコーディングエージェントに委ねたところがポイントである。

例えば以下のような使い分けなどをしていたようである。

  • 静的
    • 次の目標決定のための情報収集
    • 戦闘アルゴリズムの調整
  • 動的
    • 探索アルゴリズムの実装
    • ワールドマップ未知エリア巡回・自動洞窟探索など
    • 戦闘アルゴリズムの実装
    • HP・MP が特定閾値を下回ったらホイミを使う・宿屋に向かうなど

6 人の勇者あいちゃん

最終的に自律攻略を達成するまでに計 6 人の勇者あいちゃんを冒険に送り出した。自律攻略を果たしたのは 6 人目の勇者あいちゃんである。

1 人目 ── まず「攻略できる」を証明する

初回の冒険は自作の DQ1 シミュレーションが攻略可能であることを証明することを目的としたベースライン実装であった。 対話式で私が大まかな攻略の道筋を立て、それを実装してもらった。 次はここに行く、その周辺でレベルを上げる、装備を買って、次はここに行くといった対話をしながら実装と実行を繰り返して行く作業だ。 勇者あいちゃんは想定外のときに例外を吐くコードを書いてくれたので、私は単に処理が成功することだけを見れば基本的には良かった。 私はたまに冒険の中身を確認した。 非効率な歩行には目をつぶろう。これは RTA ではないのだから。 おおよそ簡単な指示で攻略は進んでいった。 攻略証明が目的だったので勇者あいちゃんにはシミュレーション本体コードやネットの攻略情報を含む、全ての情報へのアクセスを許可したというのが大きいだろう。 これまでの経験で 2D マップに LLM は弱いと知っていたが、意外にも探索自体に手間取ることはなかった。私が行きたい場所を指示するだけでよかった。

2D マップに対する LLM の挙動については以下の記事を書いているので暇な方はどうぞ

上記の記事はローカル LLM を対象にしたものだが、私の観察によると Claude ですらも同じ理由で 2D マップには強くない。 しかし、Claude Code はスクリプトによるマップ解析を駆使し、マップをグラフ構造に変換するなどして弱点を切り抜けたようである。

四苦八苦したのはガライの墓と竜王の城の戦闘であった。 ガライの墓はしりょうのきしが強く、竜王の城は道中の長さによる MP 枯渇が問題となった。 スターキメラの回復力がすごくてダメージレースに勝てないことを1度経験したためか、最後までスターキメラには逃げの姿勢を継続していたのが印象的であった。

そして初回の冒険は最後まで走り切り、当初の目的だったシミュレーションが攻略可能であることを証明した。

このベースライン実装は、以降、開始から竜王撃破まで通しで走る回帰テストとして大いに役立った。

10 万行動が突きつけた現実

ベースライン実装で攻略に掛かった行動数は約 10 万であった。 ここでいう行動数とは移動や攻撃や呪文などのシミュレーション層の基礎的な行動をカウントしたものである。 私はこの数字を見て現実を知った。 先ほど「DQ1 の AI 自律攻略」とは「ゲーム開始から竜王撃破までの攻略コードを非対話式 Claude Code に開発してもらうこと」と述べたが、 この時点では全く違った考え方を持っていた。 私は「DQ1 の世界にアクセスする MCP を作って Claude Code に攻略してもらうこと」を考えていた。 しかし、私はこの方法が時間とコストの両方の面において非現実的であることを思い知ったのである。 一方で対話的に作ったものではあるが攻略可能なコードが手元に残った。 攻略の方向性が最終的に固まったのはこの時であった。

2〜4 人目 ── 情報を封鎖し、自律へ。そして詰まる

2 回目の冒険は情報封鎖をした状態で自律攻略ができるかの確認を主目的とした。ラダトームの城からガライの町までの道中を題材に、大まかな動きを見ながらプロンプトや機能の調整を行った。

3 回目の冒険で完全な自律攻略を開始した。しかし、ガライの町までは行けたがそこから詰まってしまった。 西への探索ではラダトームから見えるが遠回りしないと行けない岩山の洞窟への道を発見できず、東では沼地の洞窟を抜けた先の土地が探索できないと言い張った。 ここでいう詰まってしまった状態とは冒険を続けても一向に進展しない状態が続くことである。

3 回目の分析を経てプロンプトや機能を強化した 4 回目はリムルダールまではたどり着いたものの鍵屋を発見できず詰まってしまった。 リムルダールの鍵屋は屋根の下に隠れている。屋根の下を一歩進み出て鍵屋を発見するということができなかったのである。

5 人目 ── Fable 5、あれよあれよと

そんな時、救世主が現れた。当時使えるようになった Claude の新モデル、Fable 5 だ! 私は 4 回目と同じ条件で Fable 5 で冒険を開始してみた。 するとどうだろう。見違えるように冒険があれよあれよと進んでいく。 トークンの使用量は体感として 2 ~ 3 倍だった。しかし、冒険が詰まることはなくどんどんと進んでいった。 4 回目の半分ぐらいの時間でリムルダールの鍵屋で難なく鍵の入手も済まし、ラダトームのたいようのいし、マイラのようせいのふえもあっさり入手してしまった。 このままクリアしてしまうのではないか…しかし、そんなときに、私の手を離れ、Fable 5 が突然使えなくなってしまったのだった。

The hero dissolving into fading light beneath an eclipse, the red thread she laid still whole on the ground behind her, the dark castle now nearer


本記事は個人による非公式の試みであり、株式会社スクウェア・エニックスとは関係がなく、公認も提携もされていない。 「ドラゴンクエスト」「ファイナルファンタジー」および関連する名称は同社の商標であり、本記事は論評と紹介の目的で言及する。 カバーと挿絵は生成 AI (ChatGPT) で作成し、構成図は筆者が作図した。