Skip to content

How the engine works

A look under the hood at how the WAGE engine runs a game. You don’t need this to play, host, or even build a game — but it explains why rulesets behave the way they do.

One engine process holds the authoritative state of every session. Phones are thin clients: they stream their GPS position and the actions a player takes, and they render whatever world snapshot the server sends back. The server:

  • validates every interaction (range, eligibility) before it counts,
  • decides all state changes by evaluating the ruleset,
  • and broadcasts the resulting world to everyone.

Consequences that matter to authors and players:

  • No client can cheat by editing the app — the server re-checks everything.
  • Reconnects are cheap — the live state lives on the server, so a phone that drops just resyncs to the current world.
  • The server is the referee for any dispute.

The engine advances each session in ticks at roughly 1 Hz. On every tick it, in order:

  1. moves bots according to their behavior trees,
  2. expires stale pending interaction claims (timed-out mutual confirmations),
  3. performs automatic bot interactions and automatic item pickups,
  4. evaluates the rules — checks each rule’s condition and runs the actions of those that match,
  5. finalizes item custody,
  6. recomputes each player’s objective from the objectives table,
  7. checks the countdown and win/elimination conditions, and
  8. broadcasts the updated world snapshot to all phones and the GM.

Between ticks, phones push GPS updates (about every two seconds) and player actions, which the next tick incorporates.

Zone and proximity conditions are edge-triggered: the engine tracks the previous state and fires only on the transition.

  • entered_zone fires the tick a player crosses in; left_zone when they cross out. Standing inside a zone does not re-fire entered_zone.
  • proximity fires when a pair first comes within range and won’t fire again for that pair until they separate and re-approach.

This is why “do something every second while in a zone” needs a tick (with an occupancy flag), not just entered_zone.

Three presentation/information lists are evaluated top-down, stopping at the first match: theme.playerMarkers, theme.playerHeadings.others, objectives, and visibility. Order entries most-specific first, with a catch-all (no when) last. A broad rule placed early will shadow the specific rules after it.

When a condition matches, it produces a binding — the players (or item) the match is about. The primary is same; the partner is other (for item interactions, the item is other). Actions and nested conditions refer to these. Inside an and, the first player-binding child establishes the binding and later children filter it. This is the model behind nearly every rule; it’s covered in depth in Selectors & values.

The engine stores and evaluates in SI base units: meters for distance and zone geometry, milliseconds for durations (everyMs, delayMs, confirmTimeoutMs), meters/second for speed. (countdownSeconds is the lone seconds value.) The apps convert to friendlier units at the display boundary; rulesets always author the canonical value. Errors that carry measurements report the structured numeric value and let the UI format it.

Each session gets a unique, four-letter, pronounceable code (SHON, ZOOT, NONA, TRIP). They’re generated from valid English consonant/vowel cluster shapes — letters only (no 0/O/1/I confusion), easy to say aloud and swipe-type, and case-insensitive on lookup. The keyspace (~12.7k codes) comfortably covers many simultaneous sessions; the generator rejects offensive strings. This is a deliberate trade of raw keyspace for human-friendliness — a code is typed once, so readability beats entropy.

Sessions live in the engine’s memory, keyed by join code, not in a database. They hold the world, players, items, and timers. An idle session is reaped after about an hour of no activity to free resources; there’s no long-term history. See Session lifecycle.