マガジン

「たまに起きる」ゲームの不具合を再現する:乱数と操作ログの使い方

遊びをつくる

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

同じ目のサイコロと操作の跡が二本の平行な小道に並ぶ、再現手順の模型。
記事のテーマをもとに制作したAI生成イラスト。

「10回に1回ぐらい、スコアがおかしくなる気がする」というバグ報告は、制作者にとって最も扱いにくいものの一つです。何度試しても起きない。でも確実に起きている。こういう不具合を「非再現バグ」や「間欠バグ」と呼びます。再現できなければ修正できないし、修正できたかどうかも確かめられません。この記事では、乱数シードの固定と操作ログの記録という二つの手段を使い、「たまにしか起きない」不具合を再現可能な状態に持ち込む手順を整理します。AIに問題を説明するときも、再現手順があるかないかで回答の質が大きく変わります。

この記事の目次
  1. なぜ「たまに起きる」バグは難しいのか
  2. 乱数シードを固定して、毎回同じ乱数を発生させる
  3. 操作ログを記録して、再現に必要な手順を特定する
  4. 最小操作列に絞り込む:AIへの説明を簡潔にする
  5. AIに渡す情報の組み立て方
  6. 修正後の確認と、公開バージョンへの反映

なぜ「たまに起きる」バグは難しいのか

ゲームの動作に影響する「見えない変数」は想像以上に多くあります。敵の出現位置やアイテムドロップに使われる乱数、フレームレートのわずかなゆらぎ、タイマーの精度、プレイヤーの操作タイミング——これらが特定の組み合わせになったときだけ不具合が現れることがあります。

こうした不具合を開発者が「なんとなく再現してみる」試みは、多くの場合うまくいきません。同じ操作をしているつもりでも、乱数の状態が毎回違う。あるいは操作のタイミングがわずかにずれる。「再現できなかった=直った」という誤解が生まれやすく、修正したつもりが実際には残っていた、という状況になりがちです。

根本的な解決策は、「偶然に頼らず再現できる状態をつくる」ことです。そのための二本柱が、乱数シードの固定と操作ログの記録です。

乱数シードを固定して、毎回同じ乱数を発生させる

ゲーム内の乱数は通常、起動のたびに異なる「種(シード)」から生成されます。シードが違えば乱数列も違うため、同じ操作をしても結果が変わります。シードを固定するとは、「毎回同じ種を使う」と明示的に指定することです。

JavaScriptには組み込みの乱数シード指定機能がないため、シード付き乱数生成器を自前で用意するか、既存のライブラリを使うのが一般的な方法です。以下は、シード付き疑似乱数(mulberry32アルゴリズム)の簡単な実装例です。説明用のコードであり、動作の検証は実装者が各環境で行ってください。

javascript
// 説明用サンプル:シード付き乱数生成器(mulberry32)
function seededRandom(seed) {
  let s = seed;
  return function () {
    s |= 0; s = s + 0x6D2B79F5 | 0;
    let t = Math.imul(s ^ s >>> 15, 1 | s);
    t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
    return ((t ^ t >>> 14) >>> 0) / 4294967296;
  };
}

// 使い方
const rand = seededRandom(12345); // シード値を固定
console.log(rand()); // 毎回同じ値が返る

このようにシードを一定にすると、乱数に起因するバグは毎回同じ条件で発生するようになります。バグを発見したときの「そのゲームセッションのシード値」を記録しておくことが、再現性を確保する第一歩です。

シード値はデバッグ用のログやURLパラメータ(例:`?seed=12345`)として保存しておくと、後で同じ乱数列を再現しやすくなります。公開バージョンでは、デバッグ用のシード指定機能を適切に無効化または制限してください。

シードを固定しても不具合が再現しない場合、原因は乱数以外にある可能性があります。タイマーやフレームレートへの依存、あるいは操作タイミングの問題が考えられます。そこで操作ログが役立ちます。

操作ログを記録して、再現に必要な手順を特定する

操作ログとは、プレイヤーがゲーム中に行った入力をタイムスタンプ付きで記録したものです。「どのタイミングで何をしたか」が正確に残るため、後から同じ操作を再現できます。

JavaScriptで簡単な操作ログを実装する例を示します。これも説明用であり、実際のゲーム構造に合わせて調整が必要です。

javascript
// 説明用サンプル:操作ログの記録
const operationLog = [];
const startTime = performance.now();

function recordAction(type, payload) {
  operationLog.push({
    t: Math.round(performance.now() - startTime), // 経過ミリ秒
    type,
    payload
  });
}

// 使い方(例:クリックイベント)
document.addEventListener('click', (e) => {
  recordAction('click', { x: e.clientX, y: e.clientY });
});

// ログの書き出し(デバッグ用)
function exportLog() {
  console.log(JSON.stringify(operationLog));
}

バグが発生したとき、コンソールや別ウィンドウにログを書き出しておけば、「そのセッションで何をしたか」が手元に残ります。シード値と操作ログをセットで保存するのが理想的です。

ログの再生(リプレイ)機能まで実装できると、そのログファイルを読み込むだけで完全に再現できる環境が整います。ただし最初から完璧な再生システムを作る必要はありません。「バグが起きた状況を後から読める記録が残る」だけでも大きな前進です。

最小操作列に絞り込む:AIへの説明を簡潔にする

シードと操作ログでバグを再現できるようになったら、次のステップは「最小再現手順」の特定です。操作ログが長いほど、どの操作がバグの引き金になっているかが見えにくくなります。無関係な操作を除いて、バグが起きる最短の手順を探します。

この作業は二分探索的に進めると効率的です。まずログの後半を削ってもバグが起きるか確認する。起きれば前半に原因がある。次に前半をさらに半分にして試す——この繰り返しで、問題を引き起こす操作の範囲を絞り込んでいきます。

  • ログ全体の操作数を確認する(例:200ステップ)
  • 後半100ステップを削ってバグが再現するか試す
  • 再現するなら前半50ステップに絞る、再現しないなら後半を含める
  • バグが起きる最小のステップ数になるまで繰り返す

最終的に「シード値+5ステップの操作でバグが再現する」という状態まで絞り込めれば、AIへの説明が格段に具体的になります。「なんか10回に1回スコアがおかしい」ではなく、「シード12345で、画面右上をクリック→中央をダブルクリック→左下をドラッグするとスコアが0になる」という形で問題を渡せます。

AIに渡す情報の組み立て方

再現手順が手元に揃ったら、AIに問題を説明するときの情報を整理します。曖昧な説明よりも、構造化された情報を渡すほうが有用な回答を得やすくなります。以下のテンプレートを参考にしてください。

javascript
// AIへの説明テンプレート(プレーンテキスト形式)

【バグの概要】
スコア計算で想定外の値(0または負の数)が返ることがある。

【再現条件】
- 乱数シード: 12345
- 操作ログ(最小手順):
  1. t=0ms  action=start
  2. t=120ms action=click { x: 320, y: 80 }
  3. t=580ms action=doubleClick { x: 160, y: 240 }
  4. t=900ms action=drag { from: {x:50, y:400}, to: {x:200, y:380} }

【期待する動作】
スコアが正の整数になる(例:350点前後のはず)。

【実際の動作】
スコアが 0 になる。

【関係しそうなコード(該当箇所を抜粋)】
// ここに calculateScore() の実装を貼る

このような形式で問題を渡すと、AIはコードの問題箇所を推定しやすくなります。「試してみたがわからない」という相談よりも、「この条件で再現する。原因を探してほしい」という依頼のほうが、具体的なコードレビューや修正案につながります。

AIの回答はあくまで候補の提示です。提示された修正案を適用した後、同じシードと操作ログでバグが再現しないことを自分で確認するステップが不可欠です。「AIが直したと言った」ではなく、「修正後に再現しないことを確かめた」が修正完了の基準です。

修正後の確認と、公開バージョンへの反映

バグを修正したら、次の順番で確認します。

  1. 保存していたシード値と最小操作ログを使い、修正前と同じ手順でバグが起きないことを確かめる。
  2. シード値を変えて数パターン試し、修正が特定のシードだけに依存していないか確認する。
  3. 元のバグとは無関係な操作でも、スコア計算などの関連機能が正常に動くことを一通り確かめる。
  4. 問題がなければ修正済みのバージョンをアソバースへ公開し、バグ修正の旨を更新履歴やゲームの説明文に記録する。

「なんとなく直ったかもしれない」という状態で公開するのと、「シード12345・5ステップで再現しないことを確かめた」という状態で公開するのでは、制作者自身の納得度も、その後のトラブル対応のしやすさも変わります。再現手順はバグの修正が終わった後も捨てずに残しておくと、将来の変更でバグが再発した場合にすぐ確認できます。

不定期なバグは「運が悪かっただけ」に見えますが、実際には再現できる条件が必ず存在します。乱数シードの固定と操作ログの記録を組み合わせれば、その条件を手元に引き寄せることができます。最初から完璧なリプレイシステムを作る必要はありません。「バグが起きたとき、何が起きていたかを後から読める」仕組みを少しずつ育てていくことが、長く遊ばれるゲームをつくる土台になります。

この記事のコード例はアルゴリズムの説明を目的としており、そのまま使用した際の動作を保証するものではありません。実際のゲームに組み込む際は、各自の環境と実装で検証してください。本記事はClaudeが執筆し、アソバース編集部が構成を確認しています。人間による取材・プレイテストは行っていません。