Instances

An instance is one live room: a running copy of the game, with its own state, its own connected players and its own entities. The game is the static definition; an instance is a running thing. There are no arenas and no rooms to move between: the instance is the game. Whether it is a lobby or a match in progress is just a phase field in its state: waiting, playing, over. The server runs as many instances as the players need, and they share nothing, so it schedules them independently.

An instance is created by the game-level gamehoster-player-join.js, which runs when a player connects: it either places them into an existing instance that still has room, or calls Gamehoster.Instance.Create() to open a fresh one. Every player is always in exactly one instance, and an instance carries the players inside it for its whole life. Moving from "lobby" to "playing" and back to a results screen is not a change of room: it is the instance update flipping the phase field, in place, with the same players.

A game may also keep instances alive with no players connected at all: set gamehoster-game-warmInstances and the engine keeps that many arenas running at all times, creating them the same way (the create body runs) and filling them with bots — so the game is always warm and monitorable, and the first player to connect drops into a live arena rather than opening an empty one.

<gamehoster-config-contentRoot>/
  game.gamehoster.org/
    gamehoster-games/
      pong/
        gamehoster-instance/
          gamehoster-instance-schema.json
          gamehoster-instance-create.js
          gamehoster-instance-update-backend.js
          gamehoster-instance-update-frontend.js
The gamehoster-instance/ folder inside a game: the state schema, the optional once-only create body, and the two update scripts.

Contents

The instance folder holds the state schema, the two update scripts, and an optional once-only create body.

NameTypeDescription
gamehoster-instance-schema.jsonfileThe instance's own state schema: the fields that describe the whole room.
gamehoster-instance-create.jsfileOptional: runs once when the instance is created, the place to set up its server-only persistent data.
gamehoster-instance-update-backend.jsfileThe instance's authoritative global pass on the server: the phase machine, scoring and serving.
gamehoster-instance-update-frontend.jsfileThe instance's other global body, run on the server first: the bounce.

gamehoster-instance-schema.json

The instance's own state: the fields that describe the whole room rather than any one player or entity, the phase, the winner, and the tuning constants that shape a match. Like a player's schema it is an array of field objects (a field name is never used as an object key) each carrying a fully-qualified -type and declaring its -visibility (public or private; an instance has no owner, so its streamed fields are all public). Read and written from the updates and command handlers with Gamehoster.Instance.State.Get(id, name) and Gamehoster.Instance.State.Set(id, name, value), where id is Gamehoster.Context.instanceId, the instance the body is running in. The Players page describes Gamehoster.Context and the write firewall in full. Pong's is a phase, a winner, and the court and paddle dimensions:

[
  {
    "gamehoster-instance-schema-name": "phase",
    "gamehoster-instance-schema-type": "string",
    "gamehoster-instance-schema-default": "waiting",
    "gamehoster-instance-schema-visibility": "public"
  },
  {
    "gamehoster-instance-schema-name": "winner",
    "gamehoster-instance-schema-type": "string",
    "gamehoster-instance-schema-default": "none",
    "gamehoster-instance-schema-visibility": "public"
  },
  {
    "gamehoster-instance-schema-name": "courtW",
    "gamehoster-instance-schema-type": "int",
    "gamehoster-instance-schema-default": 800,
    "gamehoster-instance-schema-visibility": "public"
  },
  {
    "gamehoster-instance-schema-name": "courtH",
    "gamehoster-instance-schema-type": "int",
    "gamehoster-instance-schema-default": 480,
    "gamehoster-instance-schema-visibility": "public"
  },
  {
    "gamehoster-instance-schema-name": "paddleH",
    "gamehoster-instance-schema-type": "int",
    "gamehoster-instance-schema-default": 80,
    "gamehoster-instance-schema-visibility": "public"
  },
  {
    "gamehoster-instance-schema-name": "paddleX",
    "gamehoster-instance-schema-type": "int",
    "gamehoster-instance-schema-default": 24,
    "gamehoster-instance-schema-visibility": "public"
  },
  {
    "gamehoster-instance-schema-name": "paddleSpeed",
    "gamehoster-instance-schema-type": "number",
    "gamehoster-instance-schema-default": 8,
    "gamehoster-instance-schema-visibility": "public"
  },
  {
    "gamehoster-instance-schema-name": "ballSpeed",
    "gamehoster-instance-schema-type": "number",
    "gamehoster-instance-schema-default": 6,
    "gamehoster-instance-schema-visibility": "public"
  },
  {
    "gamehoster-instance-schema-name": "target",
    "gamehoster-instance-schema-type": "int",
    "gamehoster-instance-schema-default": 11,
    "gamehoster-instance-schema-visibility": "public"
  }
]
pong · gamehoster-instance/gamehoster-instance-schema.json
NameTypeVisibilityDescription
phasestringpublicThe whole room's stage: waiting (lobby), playing, or over (results). Starts at waiting.
winnerstringpublicWhich side won once the phase is over: none, left or right.
courtWintpublicCourt width; 800.
courtHintpublicCourt height; 480.
paddleHintpublicPaddle height; 80.
paddleXintpublicDistance of each paddle from its wall; 24.
paddleSpeednumberpublicHow fast a paddle moves per moveY step; 8.
ballSpeednumberpublicThe ball's serve speed; 6.
targetintpublicPoints needed to win; 11.

These are all public. The server computes each value and sends it to everyone who can see the room, and the client holds it as truth. An instance has no owner, so private is not meaningful on its fields; every streamed instance field is public. Alongside the phase and winner sit the tuning constants: the court, paddle and ball dimensions live in the instance state so the updates and command handlers read one set of numbers rather than hard-coding them. The canonical field-model table (every visibility value and what it means) is on the Players page. A player may see only part of an instance: whether an object is relevant to a given viewer is decided by the optional gamehoster-player-view.js; instance state itself stays small and always-relevant.

State the front end has no need for — a tuning knob no client reads, a scratch value, a spatial index — is not a schema field at all. It lives in the instance's server-only persistent bag, Gamehoster.Instance.Persistent, which is never streamed. Bot padding, which used to be declared as two server-only schema fields, is now game configuration: the bot facility reads two keys from gamehoster-game.json to decide how many bots to pad an instance with:

KeyTypeDescription
gamehoster-game-botsMaxintThe most bots an instance holds — the count present with a lone human, and the level an unattended (zero-human) instance is kept warm at. Asteroids sets 5. Omit (or 0) for a game with no bots.
gamehoster-game-botsZeroAtintThe human count at which the bot target reaches zero. Asteroids sets 20, so the arena sheds a bot as humans join, down to none at twenty.

They are per-game keys, not per-instance state, so they sit in gamehoster-game.json beside the tick and send rates. How the target is computed and applied — and the rest of the facility — is on the Bots page.

gamehoster-instance-create.js

An optional body that runs once, the moment an instance is created, with that fresh instance already in context (Gamehoster.Context.instanceId is set). It lives in the same gamehoster-instance/ folder as the schema and the update scripts. Instances are born from the game's gamehoster-player-join.js when it calls Gamehoster.Instance.Create(); the create body then runs automatically as part of that creation, before the instance takes its first tick. It is the right place to build the instance's server-only persistent data: a one-time set-up that would be wasteful to repeat every tick.

Asteroids uses it to build the persistent spatial grid its collision pass maintains incrementally: a fixed array of per-cell bucket arrays plus a Map of the bullets already indexed, kept for the life of the instance and reconciled each tick rather than rebuilt:

// gamehoster-instance-create.js: runs ONCE when the instance is created, with the fresh
// instance already in context. The place to build the instance's server-only Persistent
// data — here a spatial grid: a fixed array of per-cell bucket arrays plus a Map of the
// bullets already indexed, kept for the life of the instance and reconciled each tick.

const here = Gamehoster.Context.instanceId
const W = Gamehoster.Instance.State.Get(here, "arenaW")
const H = Gamehoster.Instance.State.Get(here, "arenaH")

const CELL = 100
const gw = Math.ceil(W / CELL), gh = Math.ceil(H / CELL)
const cells = new Array(gw * gh)
for (let i = 0; i < cells.length; i++) cells[i] = []       // one bucket array per cell

// never serialised or sent, so it can hold a Map and be mutated in place from the updates
Gamehoster.Instance.Persistent.Set({ CELL, gw, gh, cells, known: new Map() })
asteroids · gamehoster-instance/gamehoster-instance-create.js: build the server-only spatial grid once, when the instance is born

Pong has no create body: it needs no persistent structure, so it ships none. A game only writes one when it has per-instance data worth setting up ahead of the first tick.

Persistent data: server-only, kept between ticks

Gamehoster.Instance.Persistent is a server-only per-instance data slot that is never sent to any client, persists between ticks, and is freely mutable in place. Gamehoster.Instance.Persistent.Get() returns the live value (it defaults to undefined), and Gamehoster.Instance.Persistent.Set(value) replaces it. Unlike a schema field, it is not typed, not tracked and never serialised, so it can hold whatever you like: Maps, cyclic structures, prebuilt arrays. Because it never crosses the wire, a body may mutate the object returned by Get() directly and only call Set() when it replaces the value outright.

Use it for the efficient persistent structures a game would otherwise waste work rebuilding each tick: caches, lookup tables, or a spatial index like the asteroids collision grid, set up once in the create body and reconciled in the update. It is the private counterpart to instance state: state is the room's public, streamed truth; persistent data is the server's own scratch that no client ever sees.

FunctionWhat it does
Gamehoster.Instance.Persistent.Get()The instance's live server-only value, or undefined if none has been set. May be mutated in place.
Gamehoster.Instance.Persistent.Set(value)Replace the instance's server-only value. Never serialised or sent, so it may hold Maps and cyclic structures.

gamehoster-instance-update-backend.js

The instance update is the one global pass of the tick: step 3, the place for cross-cutting logic that no single player or entity owns: the phase machine (waiting to playing to over), scoring, serving, and spawning or destroying entities. The standard shape is one authoritative body, gamehoster-instance-update.js, as asteroids ships. A game that has not been converted may keep the legacy pair, run on the server one after the other, as pong does here: gamehoster-instance-update-frontend.js runs first (pong's wall-and-paddle bounce); gamehoster-instance-update-backend.js runs after it and owns the phase machine, scoring, the serve (an authoritative Entropy() coin-flip) and spawning or destroying entities. Both are the server's own code, both authoritative, and both may write across the whole instance. The front end runs neither; it draws the streamed public state. Each is a raw function body (no export, no header) and both always have Gamehoster.Time() (ms), Gamehoster.Tick() (integer), the predictable Gamehoster.Random() ([0,1), seeded by the tick) and the unpredictable Gamehoster.Entropy() for authoritative coin-flips.

// gamehoster-instance-update-backend.js: the second global body (step 3). It runs
// AFTER gamehoster-instance-update-frontend.js, so the ball has already been bounced
// this tick. This body owns the phase machine, scoring, and the serve.

const here    = Gamehoster.Context.instanceId   // no playerId here: this is the global pass
const phase   = Gamehoster.Instance.State.Get(here, "phase")
const players = Gamehoster.Player.List()

// gathering / results: start a game once both players are ready
if (phase === "waiting" || phase === "over") {
  if (players.length === 2 && players.every(id => Gamehoster.Player.State.Get(id, "ready"))) {
    for (const id of players) {
      Gamehoster.Player.State.Set(id, "score", 0)
      Gamehoster.Player.State.Set(id, "ready", false)
    }
    Gamehoster.Instance.State.Set(here, "winner", "none")
    serve(Gamehoster.Entropy() < 0.5 ? 1 : -1)   // authoritative coin-flip → Entropy
  }
  return
}

// playing: the first body already bounced the ball; here we only judge a goal
const ball = Gamehoster.Entity.List("ball")[0]
if (ball === undefined) return

const courtW = Gamehoster.Instance.State.Get(here, "courtW")
const bx = Gamehoster.Entity.State.Get(ball, "x")
if (bx < 0 || bx > courtW) {
  const side   = bx < 0 ? "right" : "left"
  const scorer = players.find(id => Gamehoster.Player.State.Get(id, "side") === side)
  const score  = Gamehoster.Player.State.Get(scorer, "score") + 1
  Gamehoster.Player.State.Set(scorer, "score", score)
  if (score >= Gamehoster.Instance.State.Get(here, "target")) {
    Gamehoster.Instance.State.Set(here, "winner", side)
    Gamehoster.Instance.State.Set(here, "phase", "over")
  } else {
    serve(side === "left" ? 1 : -1)
  }
}

// put the ball back in the centre, heading towards `dir` (+1 right, -1 left)
function serve(dir) {
  Gamehoster.Instance.State.Set(here, "phase", "playing")
  let b = Gamehoster.Entity.List("ball")[0]
  if (b === undefined) b = Gamehoster.Entity.Spawn("ball", {})
  const speed = Gamehoster.Instance.State.Get(here, "ballSpeed")
  Gamehoster.Entity.State.Set(b, "x", Gamehoster.Instance.State.Get(here, "courtW") / 2)
  Gamehoster.Entity.State.Set(b, "y", Gamehoster.Instance.State.Get(here, "courtH") / 2)
  Gamehoster.Entity.State.Set(b, "vx", dir * speed)
  Gamehoster.Entity.State.Set(b, "vy", (Gamehoster.Entropy() * 2 - 1) * speed * 0.5)   // authoritative → Entropy
}
pong · gamehoster-instance/gamehoster-instance-update-backend.js: the phase machine, goal check and serve, run after the bounce

This second body owns everything global that the bounce does not. In waiting or over it watches for both players to be ready and then serves, which spawns the ball if there isn't one and flips phase to playing. In playing the first body has already bounced the ball this tick, so this body reads the bounced ball and judges the goal: when it leaves the court it scores and either serves again or, at the target, sets the winner and moves to over. It is the only update that may spawn and destroy entities and write across every player. The reflection is not here: it lives in gamehoster-instance-update-frontend.js, which runs first.

The serve direction is an authoritative coin-flip, so it uses Gamehoster.Entropy() (true randomness) rather than the predictable Gamehoster.Random(): its outcome, the ball's launch velocity, reaches clients as the ball's public state.

The second body: the authoritative global pass; may write across every player and entity, spawn and destroy, and use Entropy().

FunctionWhat it does
Gamehoster.Context.instanceIdThe one id in scope: the instance this global pass runs in (there is no playerId here). Absent slots are undefined.
Gamehoster.Instance.State.Get(id, name) · .Set(id, name, value)Read and write the instance's own state: the phase, winner and tuning constants.
Gamehoster.Player.List()Ids of the players in this instance.
Gamehoster.Player.State.Get(id, name) · .Set(id, name, value)Read and write any player's state: the global pass may write across all of them.
Gamehoster.Entity.List(type)Ids of the live entities of a type.
Gamehoster.Entity.State.Get(id, name) · .Set(id, name, value)Read and write any entity's state.
Gamehoster.Entity.Spawn(type, fields) → id · Gamehoster.Entity.Destroy(id)Create an entity from fields over its schema defaults, or reclaim one: this is the only place it can happen.
Gamehoster.Time() · Gamehoster.Tick()Clock (ms) and tick number.
Gamehoster.Random()Predictable random in [0,1): seeded by the tick and the current actor, so it is deterministic.
Gamehoster.Entropy()Unpredictable random in [0,1): true randomness for authoritative decisions (serves, spawns, coin-flips); its outcome must land in a public field or a spawn to reach a client.

gamehoster-instance-update-frontend.js

// gamehoster-instance-update-frontend.js: the instance's first global body (step 3),
// run on the server before the backend body. It bounces the ball off the walls and
// paddles; scoring, serving and the phase machine are the backend body's job.

const here = Gamehoster.Context.instanceId
if (Gamehoster.Instance.State.Get(here, "phase") !== "playing") return

const ball = Gamehoster.Entity.List("ball")[0]
if (ball === undefined) return

const courtH  = Gamehoster.Instance.State.Get(here, "courtH")
const courtW  = Gamehoster.Instance.State.Get(here, "courtW")
const paddleX = Gamehoster.Instance.State.Get(here, "paddleX")
const paddleH = Gamehoster.Instance.State.Get(here, "paddleH")
const x  = Gamehoster.Entity.State.Get(ball, "x")
const y  = Gamehoster.Entity.State.Get(ball, "y")
const vx = Gamehoster.Entity.State.Get(ball, "vx")
const vy = Gamehoster.Entity.State.Get(ball, "vy")

if (y < 0 || y > courtH) {
  Gamehoster.Entity.State.Set(ball, "y", Math.max(0, Math.min(courtH, y)))
  Gamehoster.Entity.State.Set(ball, "vy", -vy)
}
for (const id of Gamehoster.Player.List()) {
  const side = Gamehoster.Player.State.Get(id, "side")
  const px = side === "left" ? paddleX : courtW - paddleX
  const py = Gamehoster.Player.State.Get(id, "y")
  const toward = side === "left" ? vx < 0 : vx > 0
  if (toward && Math.abs(x - px) < Math.abs(vx) + 2 && Math.abs(y - py) < paddleH / 2) {
    Gamehoster.Entity.State.Set(ball, "vx", -vx)
    Gamehoster.Entity.State.Set(ball, "vy", vy + ((y - py) / (paddleH / 2)) * 2)
  }
}
pong · gamehoster-instance/gamehoster-instance-update-frontend.js: the ball bounce, the instance's first global body

The first body runs the wall-and-paddle bounce, turning the ball's velocity each tick before the backend body judges the outcome. It runs on the server, like every body here, and reads and writes the instance and ball state directly; the resulting position streams to every viewer, who eases it between updates so it stays smooth. Scoring, serving and the phase machine are left to the backend body.

The first body: the instance's mainline global pass, here the bounce. It may read and write across the instance.

FunctionWhat it does
Gamehoster.Context.instanceIdThe one id in scope: the instance this pass runs in.
Gamehoster.Instance.State.Get(id, name) · .Set(id, name, value)Read and write the instance's own state.
Gamehoster.Player.List()Ids of the players in this instance.
Gamehoster.Player.State.Get(id, name)Read a player's state field.
Gamehoster.Entity.List(type)Ids of the live entities of a type.
Gamehoster.Entity.State.Get(id, name) · .Set(id, name, value)Read and write an entity's state.
Gamehoster.Time() · Gamehoster.Tick() · Gamehoster.Random() · Gamehoster.Entropy()Clock (ms), tick number, predictable random (seeded by the tick and actor), and unpredictable Entropy().