ゲームの手触りをテストする:フレームではなくルールを検証しよう
多くの人が最初に書くゲームのテストは、こんな形をしています。
game.hold("ArrowRight", 30);
expect(game.state.player.x).toBe(230);これは通ります。そして誰かが加速度をいじった途端、壊れます。230 という数字はルールではなく、今日の調整を測っただけの値だったのです。こういうテストがあると調整がつらくなり、人は調整をやめるか、テストをやめてしまいます。
手触りはルールに宿ります。最高速に達するまでの速さ、速度に対するジャンプの高さ、プレイヤーが壁をすり抜けることはあり得るのか。これらを Keiko でテストする方法を紹介します。
条件を満たすまでの時間
「プレイヤーの最高速への到達は 0.25 秒未満」は時間についてのルールです。until は条件が成り立つまでゲームを進め、かかった時間を教えてくれます。
game.press("ArrowRight");
const steps = game.until((s) => s.player.vx >= game.param("player.speed"), { within: "1s" });
expect(steps * game.stepSeconds).toBeBelow(0.25);このテストは最高速をゲーム自身のパラメータから読むので、値を変えてもそのまま動き続けます。
物理を数式で
初速 v、重力 g のジャンプは v²/2g で頂点に達します。ジャンプがそう動くように作っているなら、ピクセルではなく数式をテストしましょう。
test.sweep("jump height follows v²/2g", { "player.jumpSpeed": [200, 290, 400] }, (game, values) => {
game.press("Space");
const y = game.trace("player.y", "1s");
const v = Number(values["player.jumpSpeed"]);
const g = Number(game.param("player.gravity"));
expect(y.start - y.min()).toBeCloseTo((v * v) / (2 * g), 6);
});trace はプレイヤーの高さを毎フレーム記録し、test.sweep は 3 つのジャンプ速度でチェックを実行します。将来の変更で空中制御にクセが入り、高速時に頂点が削られるようになったら、このテストがどの速度で壊れたかを教えてくれます。
絶対に起きてはいけないこと
ルールの中には不変条件もあります。プレイヤーはステージの外に出ない、体力はマイナスにならない、カメラはマップの外を映さない。during はこれらを毎フレーム確かめます。
game.press("ArrowLeft");
game.during("5s", (s) => s.player.x >= 0, "the player stays inside the level");破られたときには正確なフレームを教えてくれます。すり抜けバグを追うときには、それがまさに必要な手がかりになることがよくあります。
停止距離
減速もルールです。加速度 a、速度 v なら、止まるまでに v/a 秒かかります。
test.sweep("stops in speed ÷ acceleration", { "player.speed": [80, 140, 300] }, (game) => {
game.hold("ArrowRight", "0.5s");
const steps = game.until((s) => s.player.vx === 0, { within: "1s" });
const expected = Number(game.param("player.speed")) / Number(game.param("player.accel"));
expect(steps * game.stepSeconds).toBeCloseTo(expected, game.stepSeconds);
});このテストの最初のバージョンでは「0.2 秒以内に止まる」と検証していて、速度 300 で失敗しました。ゲームのバグではありません。主張のほうが間違っていたのです。ルールをきちんと書き下すことこそが、テストの価値の半分です。
フレーム単位で厳密なテストが正しいとき
正確な数字そのものが大事なこともあります。格闘ゲームのフレームデータ、パズルの解答、一致しなければならないリプレイ。そういうときはしっかり固定しましょう。ただし先に、ゲームが決定的であることを確かめてください。
test.deterministic("the opening is frame-exact", (game) => {
game.hold("ArrowRight", 20).tap("KeyZ").wait(40);
});習慣にする
- ルールを言葉で書く:「プレイヤーの最高速への到達は 0.25 秒未満」。
- それを捉える測り方を見つける:時間、高さ、距離、あるいは不変条件。
- 数字はコピーせず、ゲームのパラメータから読む。
- 調整しそうなパラメータは総当たりする。
こうして書いたテストは、調整すればするほど役に立つようになります。変更がどのルールを曲げたのかを教えてくれるからです。
詳しくは ゲームへの問いかけ方 と 総当たりと決定性 をどうぞ。