スイープと決定性
スイープ:出荷するかもしれないすべての値で
調整すれば数値が変わります。今日のジャンプ速度で動いたメカニクスが、明日のジャンプ速度では壊れるかもしれません。test.sweep は同じテストを複数のパラメータで、組み合わせごとに 1 テストずつ、それぞれパラメータを設定したまっさらなゲームで実行します。
ts
test.sweep(
"jump height follows jump speed",
{ "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); // v² / 2g
},
);text
pass jump height follows jump speed [player.jumpSpeed=200]
pass jump height follows jump speed [player.jumpSpeed=290]
pass jump height follows jump speed [player.jumpSpeed=400]パラメータが複数あれば、すべての組み合わせが実行されます。
ts
test.sweep("clears the first gap", { "player.jumpSpeed": [260, 290, 320], "player.gravity": [800, 900] }, (game) => {
game.hold("ArrowRight", "0.4s").tap("Space");
game.until((s) => s.player.x > 120, { within: "2s" });
});スイープにはゲームがパラメータを公開している必要があります。ゲームを公開するを見てください。ほかの play() オプションは、4 番目の引数に { play: { … } } として渡します。
決定性:毎回同じ
リプレイ、記録されたバグ報告、マルチプレイのロックステップ、そして Keiko のテスト自体も、ひとつの性質に頼っています。同じ入力なら同じゲームになることです。この性質は、敵 AI の中の Math.random() や、クールダウンの中の Date.now() によって、こっそり壊れます。
test.deterministic はスクリプトをまっさらなゲームで 2 回プレイし、毎ステップ後のステートを比較します。2 回目は Math.random() のシードと日付が異なるので、シードなしの乱数や実時間の時計に依存しているところが浮かび上がります。
ts
test.deterministic("ten seconds of play are reproducible", (game) => {
game.hold("ArrowRight", "2s").tap("Space").wait("8s");
});text
✖ the two runs diverged at tick 37: state.enemies.0.x was 112 in the first run and 118 in the second.
Look for Math.random(), Date.now() or real-time timers in game logic: use a seeded generator
and the time step your loop passes in.見つかったものを直す
- 乱数: シード付きの生成器(debuff の
game.random、Phaser のPhaser.Math.RND.sow([seed])、または mulberry32 のような小さなもの)を使い、一度だけシードを設定します。 - 時間:
Date.now()の差分ではなく、ループが受け取るdtを使います。 - 反復の順序: キーの形が変わるオブジェクトではなく、配列をイテレートします。
ゲームが乱数をすべて Math.random() から取っているなら(Phaser.Math.Between のようなエンジンのヘルパーはそうです)、Keiko がすでにシードを入れています。なので、ゲームはシードからそっくりそのまま再現されます。それ以外のもの(時計、実時間のタイマー)は { sameSeed: true } でチェックしましょう。シードの異なる実行が食い違ったときは、エラーがこれを提案してくれます。
ts
test.deterministic("physics is reproducible", (game) => game.hold("ArrowRight", "3s"), { sameSeed: true });