Skip to content

Tiny game engines, shared worlds

A two-player game stored each avatar as a synchronized document. Every pointer move wrote a new position. On a fast connection it looked acceptable. Under delay, avatars jumped backward, crossed walls, and occasionally ended in different places on each screen.

The sync engine had converged on the latest documents. It had not simulated a game.

Games add time to shared state. A useful tiny engine must decide when simulation advances, which inputs are valid, who computes outcomes, what history survives, and how clients draw states received late.

System map

text
player input
    |
    v
input event log -> simulation tick -> authoritative snapshot
       |                 |                    |
       |                 +-> deterministic rules
       |                                      v
       +-> reconnect replay              client buffer
                                              |
                                      interpolation
                                              |
                                           render

presence says who is connected
sync documents hold durable room state
game protocol orders time-sensitive inputs and snapshots

A turn-based game may collapse most of this into ordinary synchronized documents. A real-time game cannot.

Terms

Tick. One simulation step.

Fixed timestep. Simulation advances by a constant duration.

Variable timestep. Simulation advances by measured elapsed time.

Input event. A player's intent rather than a claimed outcome.

Snapshot. State captured at a known tick.

Authoritative state. State accepted by the process enforcing rules.

Peer-shaped state. State assembled from client-owned records.

Interpolation. Rendering between two known snapshots.

Prediction. Simulating local input before confirmation.

Rollback. Restoring earlier state and replaying late input.

Fixed ticks or variable time

Variable time is natural for rendering but poor for shared deterministic simulation. Devices measure different frame intervals, background tabs pause, and floating-point accumulation diverges.

Use a fixed simulation step:

js
const STEP_MS = 50;
let tick = 0;
let accumulator = 0;
let previous = performance.now();

function frame(now) {
  accumulator += Math.min(now - previous, 250);
  previous = now;

  while (accumulator >= STEP_MS) {
    state = simulate(state, inputsFor(tick), STEP_MS);
    tick += 1;
    accumulator -= STEP_MS;
  }

  render(state, accumulator / STEP_MS);
  requestAnimationFrame(frame);
}

The cap prevents a resumed tab from attempting thousands of steps. Rendering can still run at display rate. Turn-based games can use a logical turn number instead. State transitions need a shared order.

Determinism is a budget

A deterministic simulation produces the same state from the same initial state and ordered inputs.

Keep the simulation function pure. Do not read wall time, the DOM, network state, or unseeded randomness inside it. Control numeric operations and iteration order when cross-engine drift matters.

Randomness becomes an input:

  • The authority chooses and records a seed.
  • A deterministic generator consumes it in a defined order.
  • Random outcomes can also be explicit events.

Hash snapshots in tests and replay the same inputs in different runtimes. Perfect determinism is less important when clients only render authoritative snapshots. It matters for prediction, replay, and lockstep.

Authoritative versus peer-shaped worlds

An authoritative server receives input, validates it, advances the simulation, and publishes snapshots or events. Clients do not submit "my score is 900." They submit "pressed jump at input sequence 41."

This fits fast competitive games, hidden information, collision rules, and valuable rewards. It costs server compute, room scheduling, and latency handling.

Peer-shaped state assigns ownership to clients or merges records through a general sync service. It fits cooperative rooms where cheating has little value and updates do not need one high-frequency world step.

Peer-shaped does not mean peer-to-peer transport. A server can relay and persist documents while declining to simulate the world.

A hybrid is often best. General sync stores room settings, players, chat, ready state, and durable results. A small authoritative loop owns the active match.

Event logs and snapshots

Store player inputs or accepted domain events as immutable log entries:

ts
type GameInput = {
  roomId: string;
  playerId: string;
  sequence: number;
  targetTick: number;
  kind: "move" | "act";
  payload: unknown;
};

Validate that sequences increase, target ticks fall within an allowed window, and payloads match the player's current rights.

An event log supports replay, reconnect, and debugging. It grows forever unless compacted. Write snapshots every fixed number of ticks or after meaningful turns. A reconnecting client loads the latest snapshot, then applies later events.

Do not use a mutable LWW document as the event log. Concurrent events need separate immutable IDs.

Interpolation, prediction, and rollback

Snapshots arrive in bursts. Rendering only the newest position produces jumps.

Keep a short buffer and render slightly behind server time. Interpolate between the snapshots around the render target.

Interpolation does not change simulation state. It changes presentation.

Local prediction applies the player's input immediately. On confirmation, restore authoritative state and replay unacknowledged inputs. Rollback resimulates past ticks after late input. Both require more than a generic JSON sync engine.

Reconnects are a normal state

A reconnecting client needs:

  1. Room identity and authorization.
  2. The latest accepted snapshot and tick.
  3. Events after that snapshot.
  4. Its last acknowledged input sequence.
  5. A fresh presence connection.

Do not replay stale movement inputs from an offline write-ahead log minutes later. Time-sensitive inputs need an expiry or valid tick window. Durable turn submissions may remain valid, depending on game rules.

If retained history is insufficient, send a full snapshot. If the player's seat expired, reconnect as a spectator or require a new join.

Draw the cheating boundary

Anything computed only by an untrusted client can be changed by that client.

An authoritative server should validate:

  • Input rate and sequence.
  • Legal moves and cooldowns.
  • Position and speed constraints.
  • Ownership of pieces or cards.
  • Visibility of hidden information.
  • Score and reward changes.

Do not send hidden state to a client and rely on CSS to conceal it. A cooperative game among friends may trust clients. State that choice plainly.

Capability links authorize entry to a room, but a leaked write key may let anyone submit inputs. Competitive games need per-player sessions or signed seat tokens, not one shared room writer key.

When a general sync engine is enough

Use ordinary document sync for:

  • Turn-based boards with server-validated moves.
  • Shared puzzles where one LWW move register is acceptable.
  • Drawing strokes as immutable events.
  • Lobby ready state and room settings.
  • Cooperative inventories split into independent records.
  • Low-rate games where each transition is a transaction.

Add an authoritative game loop when:

  • Updates exceed a few state changes per second per player.
  • Physics or collision must agree.
  • Input order affects outcomes.
  • Hidden information matters.
  • Cheating has a meaningful reward.
  • Prediction and reconciliation are required.

Do not build a game server for a shared tic-tac-toe board. Do not pretend a generic LWW store is a game server for a real-time arena.

Failure modes

  • Simulation uses requestAnimationFrame time as authoritative time.
  • A background tab tries to catch up every missed tick.
  • Players upload positions rather than inputs.
  • Event IDs collide after local storage resets.
  • Snapshots omit the random generator state.
  • Interpolation mutates authoritative state.
  • Reconnect replays expired movement.
  • Presence is mistaken for a reserved player seat.
  • One shared write capability allows player impersonation.
  • The server trusts a client-reported score.
  • Log retention ends before the oldest supported reconnect.

Field checklist

This chapter closes Part II by adding time and authority to the local-first model. Chapter 21, Apps as conversation artifacts begins Part III with the durable objects agents create. For the room's ephemeral layer, return to Chapter 19.

Built from field notes on durable software systems.