マガジン

AIが直しやすいゲームコードにする:ルールと画面を分ける

遊びをつくる

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

ゲームの景色と、その下の動作の仕組みを二層に分けた線画。
記事のテーマをもとに制作したAI生成イラスト。

ゲームを作り始めてしばらくすると、「このコード、もう自分でも追えない」という感覚が来ます。AIに「ここを直して」と伝えても、どこを直せば安全かわからず、別の場所が壊れる。そういうとき、たいていの原因はゲームのルールと画面の処理が混在していることにあります。この記事では、完成したゲームを最初から作り直すのではなく、実際に困った箇所から少しずつ分離していく方法を説明します。

この記事の目次
  1. 混在したコードが修正を難しくする理由
  2. どこで分けるか:「状態の変化」と「状態の表示」
  3. 分割前後の例:スコアとライフの処理
  4. ルールだけを確認する:簡単なテストを書く
  5. 実際の進め方:小さく分ける手順
  6. どこまで分けるか:過剰な設計を避けるために

混在したコードが修正を難しくする理由

小さなゲームを作るとき、まず動くものを目指すのは正しい判断です。しかし「動く」状態を保ちながら手を入れ続けると、コードの中にゲームの判定ロジックとcanvasへの描画処理とボタンのクリック処理が混在するようになります。たとえば、残りライフを減らす処理のすぐ横にライフ表示を赤く点滅させるアニメーション処理が書かれている、といった状態です。

このとき「ライフが0になったらゲームオーバー画面を出す」という変更をAIに頼むと、AIはライフの判定に関係するコードを探して書き換えようとします。しかし描画処理が絡んでいるため、変更の影響範囲が読めず、慎重になるか、あるいは周辺のコードまで書き換えてしまいます。AIへの指示が短くても長くても、混在したコードでは修正の精度が上がりにくいのはこのためです。

分離の目的は「きれいなコードを書く」ことではありません。AIに渡すコードの範囲を絞れるようにすることが直接の目的です。ルールだけを取り出して「ここだけ直して」と渡せれば、描画が壊れる心配なく修正を依頼できます。

どこで分けるか:「状態の変化」と「状態の表示」

分け方の基準として、まず「状態の変化」と「状態の表示」を分けることから始めると整理しやすくなります。

  • 状態の変化(ルール):プレイヤーの位置、スコア、ライフ、ターン、フラグ。入力や時間の経過に対して値がどう変わるかを記述する部分。
  • 状態の表示(UI):現在の状態をcanvasやDOMに描く部分。値を読み取って画面に反映するだけで、値そのものは変えない。
  • 入力の受け取り(イベント):クリック・キー・タッチ。どのルール関数を呼ぶかを決めるだけで、描画には直接触らない。

この3つを完全に別ファイルにする必要はありません。最初の一歩として、「ルール」だけを関数や小さなオブジェクトにまとめるだけで十分です。コードの中で「この関数はdrawという名前がついているのにスコアを変更している」という箇所を探すと、混在のポイントが見えてきます。

「状態管理」「MVC」などのパターンを調べると詳しい設計論が見つかりますが、小さなゲームで最初から全部取り入れる必要はありません。困った箇所から分けていくのが実用的です。

分割前後の例:スコアとライフの処理

以下は説明用の簡略コードです。実際の動作を保証するものではなく、分離の考え方を示すための例として読んでください。

分割前:ルールと描画が一か所にある状態

javascript
function onHit() {
  state.life -= 1;
  // ライフの描画もここで直接行っている
  ctx.clearRect(0, 0, 200, 30);
  ctx.fillText('LIFE: ' + state.life, 10, 20);
  if (state.life <= 0) {
    ctx.fillText('GAME OVER', 80, 200);
    // ゲームオーバー判定と描画が同じ関数内
  }
}

この状態でAIに「ライフが0になったときの処理を変えたい」と伝えると、描画処理を含む関数全体が変更の対象になります。

分割後:ルールと描画を分けた状態

javascript
// ルール:状態だけ変える
function applyHit(state) {
  state.life -= 1;
  if (state.life <= 0) {
    state.phase = 'gameover';
  }
  return state;
}

// UI:状態を読んで描くだけ
function drawHUD(ctx, state) {
  ctx.clearRect(0, 0, 200, 30);
  ctx.fillText('LIFE: ' + state.life, 10, 20);
  if (state.phase === 'gameover') {
    ctx.fillText('GAME OVER', 80, 200);
  }
}

分割後は「ゲームオーバーになる条件を変えたい」ときにapplyHit だけを渡せばよく、描画が壊れる心配がありません。逆にライフの表示デザインを変えたいときは drawHUD だけを渡せます。AIへの指示の範囲がはっきりするため、「他の部分は変えないでほしい」という制約を守りやすくなります。

ルールだけを確認する:簡単なテストを書く

ルールを独立した関数にした最大の利点は、画面を開かなくてもルールの動作を確認できるようになることです。ブラウザのコンソールに次のように書けば、applyHit が意図通りに動くかを即座に確かめられます。

javascript
// ブラウザのコンソール、またはNode.jsで実行できる例
const s1 = applyHit({ life: 2, phase: 'playing' });
console.log(s1.life === 1 && s1.phase === 'playing'); // true

const s2 = applyHit({ life: 1, phase: 'playing' });
console.log(s2.life === 0 && s2.phase === 'gameover'); // true

これはテストフレームワークを導入しなくても書ける最小の確認です。AIにルールを修正してもらったあと、この確認を実行して期待通りの値が返ってくるかを見るだけで「描画なしでルールが正しく動いている」ことを伝えられます。

逆に言えば、この確認が書けない状態(ルールが描画に依存していて単体で動かせない状態)は、分離がまだ不完全なサインです。「関数を呼ぶとcanvasが必要になる」という状況に気づいたら、そこを次に分離する箇所として扱います。

実際の進め方:小さく分ける手順

既存のゲームコードを整理するとき、最初から全体を書き直そうとするとほぼ失敗します。次の手順で少しずつ進めると、動くゲームを壊さずに分離を進められます。

  1. 1. AIに直してほしい箇所を一つ決める。「ジャンプの高さを変えたい」「敵の出現ペースを変えたい」など、具体的な要望から始める。
  2. 2. その要望に関係する変数と計算だけを探す。コード全体を読まず、その変数がどこで変更されているかだけを追う。
  3. 3. 関連するコードを小さな関数に切り出す。関数は「状態を引数で受け取り、変更後の状態を返す」形にする。描画や副作用は持たせない。
  4. 4. 切り出した関数をコンソールで確認する。いくつかの入力値で期待通りの出力が得られるかを確認してから、ゲームの本体に戻す。
  5. 5. ゲームを動かして確認し、問題なければAIに渡す。「この関数を修正してほしい、他の関数は変えないでほしい」と明示することで、AIの変更範囲を絞れる。

この手順を一か所に使うだけで、次に同じ場所を修正するときのコストが下がります。全部を分離しなくても、今困っている箇所から始めれば十分です。ゲームが育つにつれて、修正が必要になった箇所が自然に整理されていきます。

どこまで分けるか:過剰な設計を避けるために

分離を進めていくと「もっと細かく分けるべきか」という問いが出てきます。その判断基準は「AIに渡す単位として説明できるか」です。「この関数はキャラクターの衝突判定だけをする」と説明できれば十分に独立しています。複数の責任を説明する必要があるなら、もう一段分けることを検討します。

一方で、100行に満たない小さなゲームを複数のファイルに分割する必要はほとんどありません。分割のコスト(ファイルを行き来する手間、モジュールの読み込み順の管理)が分離の利益を上回る場合は、同一ファイル内で関数を整理するだけで十分です。

アソバースに投稿するHTMLゲームの場合、単一ファイルの制約やZIPによる複数ファイル構成どちらの形でも投稿できます。設計の選択はコードの管理しやすさのためにあり、投稿方法に合わせて無理に変える必要はありません。ルールを関数にまとめるだけなら、単一ファイルのままでも実現できます。

この記事の本文はClaude(Anthropic)が執筆し、アソバース編集部が構成と事実確認を担当しています。コード例は説明用の簡略版であり、実際の動作検証を経たものではありません。