Appearance
Determinism
The same seed and the same input give the same world, step for step. That is what makes scenario tests exact, kiln run repeatable, and recorded play sessions replayable. It holds as long as the simulation only reads three things from outside: the clock in ctx.time, the seeded Rng, and input through the input plugin.
Randomness: Rng
ts
const rng = res.mut(Rng)
rng.float() // [0, 1)
rng.float(-5, 5) // [-5, 5)
rng.int(1, 6) // 1..6 inclusive
rng.chance(0.25) // true a quarter of the time
rng.pick(['a', 'b']) // a random element
rng.shuffle(items) // in placeRng is seeded from seed in kiln.config.ts. Scenarios can override it (scenario(name, fn, { seed: 3 })), and so can the CLI (kiln run --seed 3, kiln shot --seed 3). Its state is part of snapshots and recordings.
For a stream of its own (a particle emitter, a level generator), seed a new RngState from the app's: new RngState(res.mut(Rng).u32()).
Time: ctx.time
| Field | |
|---|---|
time.step | The fixed step, 1 / fixedHz |
time.dt | step inside Fixed; the clamped frame delta elsewhere |
time.elapsed | Simulated seconds: tick * step |
time.tick | Fixed steps run so far |
time.frame | Frames run so far |
time.alpha | Interpolation between the last two fixed steps |
Count steps instead of adding up floats: subtracting time.step 3600 times leaves about 1e-12, so a remaining <= 0 check fires one step late. stepsFor(seconds, time.step) converts a duration to steps, and the built-in Timer, Lifetime and Delayed all count steps.
kiln check
bash
kiln checksrc/systems.ts:42:15 no-math-random Use res.mut(Rng).float() (seeded) instead of Math.random().
src/systems.ts:57:20 no-date-now Use ctx.time.elapsed (simulated seconds) instead of Date.now().It flags Math.random(), Date.now(), performance.now() and new Date() in src/ (vendored plugins included) and exits 1 if it finds any. Add kiln-allow in a comment on a line that needs one on purpose, such as a timestamp in a save file name.
Where determinism ends
- Physics. Rapier is deterministic on one machine, and runs the same WASM in the browser and under Bun, which usually matches. Cross-machine determinism is not guaranteed.
- Floating point across engines.
Math.sin,cosandpowcan differ in the last bit between Chrome's V8 and Bun's JavaScriptCore. Long, physics-heavy replays can drift because of it. - Rendering and cosmetics.
UpdateandRenderrun once per frame with a variabledt, so anything they compute depends on the frame rate. Keep gameplay inFixed.
Replays report the first frame where state stopped matching (divergedAt), so drift shows up as a number rather than a mystery. See Recordings and replay.