Docs Decisions

Contents

ADR-0004: What "deterministic" means for a game

  • Status: Proposed
  • Date: 2026-09-30

Context

Reelwright bans sequential randomness in its engine and plays timelines as a pure function of the frame number. Games can't: they generate worlds at runtime and accumulate state every tick. But everything Gamewright verifies depends on replay: bots, tours, saves and bug reports. unspent-ortho paid for weak determinism. Its world generator was bumped by hand 46 times in 16 days, and each bump moved every seed. Prop ids were positional, so saves broke (research/unspent-ortho-study.md §6.1).

Decision

  1. Simulation runs on a fixed tick, apart from rendering. Game state changes only in tick(intent). Presentation interpolates and never writes to the simulation.
  2. Replay promise: same bundle + same seed + same intent log + same engine version and platform ⇒ the same state hash after every tick. The state hash is SHA-256 over the JCS bytes of the simulation state.
  3. Seeds by name path, never by draw order: seed(child) = u32le(sha256(le32(parent) ‖ path)), with paths like world/region.r3/room.12/loot. Sequences come from PCG32 streams; placement uses the stateless pcg_hash. The core and the engine implement all three, checked against testdata/vectors.json, in native and web builds.
  4. Decisions come from integers. Generators decide with integer hashes and quantize any float that feeds a decision, because sin and pow can differ in the last bit between glibc and emscripten.
  5. No hidden inputs in simulation code. Kit simulation code may not call randi, randf, randomize, unseeded RandomNumberGenerator, Time or OS.get_ticks*. Presentation may read time for effects that never feed back into the simulation. A single check script enforces this (tools/check-hidden-inputs.sh).
  6. Stable ids. Anything that persists or is referenced gets an id derived from what it is (region.r3/prop.pine.17, indexed within its own stream), never from a global position in a list.
  7. The world stamp is a content hash. hash(engine sim version, kit versions, generator documents, root seed). A save records it. A change is detected, never hand-bumped, and a stale save is refused by name or migrated.
  8. Physics determinism is per template. Built-in kits use our own kinematic movement and collision queries, not Godot physics bodies, so they replay. A template may opt into Godot physics and give up exact replay; its bots then assert outcomes, not hashes.

Alternatives considered

  • Reelwright's four-level film determinism with exact golden frames. Film-specific. Games test state hashes after N ticks, plus a few screenshot goldens for look.
  • Lockstep-grade determinism across platforms (fixed-point math everywhere). Only a networked lockstep game needs it. Cost is high, and nothing needs it yet. The web-vs-native seed test tells us how far off we are.

Consequences

  • Bots, tours and saves can replay any session exactly on one platform.
  • Kits must keep simulation state serializable and separate from nodes. That's the pattern unspent-ortho already follows: a pure core with thin node systems.
  • Floating-point simulation may drift between native and web. Any test that compares the two compares generated worlds and decisions, not long float trajectories.

From docs/adr/0004-determinism-for-games.md in the Gamewright repository.