決定的なリプレイとファジングでゲームをテストする
ゲームのバグ報告は、たいていこんな感じです。「2 つ目のドアの近くで床をすり抜けました。たぶん左を押してたと思います」。誰も再現できないので、誰も直せません。
根本的な問題は、ほとんどのゲームが決定的ではないことです。フレームのタイミングはばらつき、Math.random() は実行のたびに違う数字を返し、同じ入力でも結果が変わります。ここを直せば、さまざまなテストツールが使えるようになります。
決定性:すべての土台
debuff はゲームロジックを、固定ステップのクロック(ディスプレイのリフレッシュレートに関係なく毎秒 60 ステップ)とシード付き乱数で動かします。描画はステップの間を補間するので動きはなめらかなままですが、シミュレーションは常にまったく同じステップで進みます。
つまり、シードと入力を記録すれば、それがプレイの完全な記録になります。再生すれば、毎回まったく同じゲームが再現されます。
シナリオ:手触りのためのテスト
シナリオは、入力、待機、期待値を並べた JSON のスクリプトで、ヘッドレスで実行されます。
{
"steps": [
{ "press": "ArrowRight" },
{ "wait": 40 },
{ "release": "ArrowRight" },
{ "expect": { "state.player.x": { "gt": 80 } } },
{ "capture": "after-run" }
]
}npx debuff scenario # 実行してベースラインと比較
npx debuff scenario --update # 意図した変更を受け入れるexpect はゲームの状態を確かめ、capture はスクリーンショットを保存済みのベースラインと比べ、set は実行の途中でパラメータを変えます。これで、jumpSpeed を上げれば本当に穴を飛び越えられるかもテストできます。シナリオは CI で動き、コーディングエージェントにとっては自分の変更が正しいことを証明する手段になります。
リプレイ:バグをそのまま呼び戻す
開発パネルではプレイを録画し、リプレイとして保存やコピーができます。そのファイルをバグ報告に添付すれば、誰でもバグが起きる瞬間をそのまま見られます。エージェントも MCP サーバーの game_replay ツールでリプレイを読み込めます。
リプレイは入力とシードだけなので、そのままシナリオにもできます。今日のバグが、明日のリグレッションテストになるのです。
ファジング:でたらめな手で、本物のバグを
プレイヤーは、あなたがスクリプトには決して書かないようなことをします。debuff fuzz もそれをやります。ランダムな入力でゲームをヘッドレスに遊び、3 種類の発見を報告します。
- エラー: クラッシュ。解説付きのエラー(誰のバグか、どの行か)も一緒に
- 詰まった状態: ゲームの状態が長いあいだ変わらなくなる
- ループ: 新しい入力がないのに、状態が同じ値の間を行ったり来たりする
どの発見にも、その時点までの実行を再現するシナリオファイルが付いてきます。
npx debuff fuzz --seconds 120 --runs 5
npx debuff fuzz --seed 1234 # 同じ実行をそのまま繰り返す入力にもシードがあるので、同じシードなら必ず同じ入力になります。CI で動かすもよし、大きな変更のあと、エージェントにファジングを頼むもよしです。
debuff fuzzは次のリリースで登場します。
はじめよう
npm create debuff@latest my-game新しいプロジェクトには、お手本にできるシナリオが 1 つ入っています。