Sweeps and determinism
Sweeps: every value you might ship
Tuning changes numbers, and a mechanic that works at today's jump speed can break at tomorrow's. test.sweep runs the same test across params, one test per combination, each on a fresh game with those params set:
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
},
);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]With several params, every combination runs:
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" });
});Sweeps need the game to expose params; see Exposing your game. Pass { play: { … } } as a fourth argument for other play() options.
Determinism: the same every time
Replays, recorded bug reports, multiplayer lockstep and keiko tests themselves rely on one property: the same input gives the same game. It breaks quietly, through a Math.random() in an enemy's AI or a Date.now() in a cooldown.
test.deterministic plays a script twice on fresh games and compares the state after every step. The second run gets a different Math.random() seed and a different date, so anything that depends on unseeded randomness or the wall clock shows up:
test.deterministic("ten seconds of play are reproducible", (game) => {
game.hold("ArrowRight", "2s").tap("Space").wait("8s");
});✖ 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.Fixing what it finds
- Randomness: use a seeded generator (debuff's
game.random, Phaser'sPhaser.Math.RND.sow([seed]), or a small one like mulberry32) and seed it once. - Time: use the
dtyour loop receives, notDate.now()differences. - Iteration order: iterate arrays rather than objects whose keys change shape.
If your engine seeds its own generator from Math.random() on purpose, check everything else with { sameSeed: true }:
test.deterministic("physics is reproducible", (game) => game.hold("ArrowRight", "3s"), { sameSeed: true });