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.

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

Contents

The instance folder holds the state schema and the two update scripts.

NameTypeDescription
gamehoster-instance-schema.jsonfileThe instance's own state schema: the fields that describe the whole room.
gamehoster-instance-update-backend.jsfileThe authoritative global update, backend only: the phase machine, scoring and serving.
gamehoster-instance-update-frontend.jsfileThe shared, predictable update, run on both sides: 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 -sync (how a client obtains it) and -audience (who may receive it). 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-sync": "sent",
    "gamehoster-instance-schema-audience": "all"
  },
  {
    "gamehoster-instance-schema-name": "winner",
    "gamehoster-instance-schema-type": "string",
    "gamehoster-instance-schema-default": "none",
    "gamehoster-instance-schema-sync": "sent",
    "gamehoster-instance-schema-audience": "all"
  },
  {
    "gamehoster-instance-schema-name": "courtW",
    "gamehoster-instance-schema-type": "int",
    "gamehoster-instance-schema-default": 800,
    "gamehoster-instance-schema-sync": "sent",
    "gamehoster-instance-schema-audience": "all"
  },
  {
    "gamehoster-instance-schema-name": "courtH",
    "gamehoster-instance-schema-type": "int",
    "gamehoster-instance-schema-default": 480,
    "gamehoster-instance-schema-sync": "sent",
    "gamehoster-instance-schema-audience": "all"
  },
  {
    "gamehoster-instance-schema-name": "paddleH",
    "gamehoster-instance-schema-type": "int",
    "gamehoster-instance-schema-default": 80,
    "gamehoster-instance-schema-sync": "sent",
    "gamehoster-instance-schema-audience": "all"
  },
  {
    "gamehoster-instance-schema-name": "paddleX",
    "gamehoster-instance-schema-type": "int",
    "gamehoster-instance-schema-default": 24,
    "gamehoster-instance-schema-sync": "sent",
    "gamehoster-instance-schema-audience": "all"
  },
  {
    "gamehoster-instance-schema-name": "paddleSpeed",
    "gamehoster-instance-schema-type": "number",
    "gamehoster-instance-schema-default": 8,
    "gamehoster-instance-schema-sync": "sent",
    "gamehoster-instance-schema-audience": "all"
  },
  {
    "gamehoster-instance-schema-name": "ballSpeed",
    "gamehoster-instance-schema-type": "number",
    "gamehoster-instance-schema-default": 6,
    "gamehoster-instance-schema-sync": "sent",
    "gamehoster-instance-schema-audience": "all"
  },
  {
    "gamehoster-instance-schema-name": "target",
    "gamehoster-instance-schema-type": "int",
    "gamehoster-instance-schema-default": 11,
    "gamehoster-instance-schema-sync": "sent",
    "gamehoster-instance-schema-audience": "all"
  }
]
pong · gamehoster-instance/gamehoster-instance-schema.json
NameTypeSyncAudienceDescription
phasestringsentallThe whole room's stage: waiting (lobby), playing, or over (results). Starts at waiting.
winnerstringsentallWhich side won once the phase is over: none, left or right.
courtWintsentallCourt width; 800.
courtHintsentallCourt height; 480.
paddleHintsentallPaddle height; 80.
paddleXintsentallDistance of each paddle from its wall; 24.
paddleSpeednumbersentallHow fast a paddle moves per moveY step; 8.
ballSpeednumbersentallThe ball's serve speed; 6.
targetintsentallPoints needed to win; 11.

These are all sync: "sent", audience: "all". The server computes each value and pushes it to everyone who can see the room, and the client treats it as truth. 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 shared set of numbers rather than hard-coding them. The canonical field-model table (every sync and audience 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.

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 → playing → over), scoring, serving, and spawning or destroying entities. Like every other update it splits into two files by trust, not by side. The shared gamehoster-instance-update-frontend.js holds the predictable part (pong's wall-and-paddle bounce) and runs on both sides. The private gamehoster-instance-update-backend.js runs after it, on the backend only, and owns what a client cannot predict: the phase machine, scoring, the serve (an authoritative Entropy() coin-flip) and spawning or destroying entities. The backend is the superset: it runs the shared file first and then its private extra; a client runs the shared file and nothing else. So the shared logic lives in exactly one file and cannot drift between the two sides. Both files are optional and independent. Each is a raw function body (no export, no header) and both always have Gamehoster.Time() (ms), Gamehoster.Tick() (integer) and the predictable Gamehoster.Random() ([0,1), seeded by the tick so it replays identically on both sides). Only the backend file additionally has the unpredictable Gamehoster.Entropy() for authoritative coin-flips; the shared file does not.

// gamehoster-instance-update-backend.js — the backend-only extra (step 3). It runs
// AFTER the shared gamehoster-instance-update-frontend.js, which the backend also runs,
// so the ball has already been bounced this tick. Here we own only what a client
// cannot predict: 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 shared pass 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 backend-only extra: phase machine, goal check and serve (the bounce now lives in the shared front-end file)

The backend pass owns everything global. 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 shared pass has already bounced the ball this tick, so the backend only 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. There is no bounce helper here anymore: that reflection lives solely in the shared gamehoster-instance-update-frontend.js, which the backend 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 reaches clients as the ball's confirmed state, not by being re-rolled on each side.

The backend-only extra: the authoritative 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, identical on server and client, safe inside shared logic.
Gamehoster.Entropy()Unpredictable random in [0,1): true randomness for authoritative decisions (serves, spawns, coin-flips); its outcome must land in a sent/server field or a spawn. Backend only.

gamehoster-instance-update-frontend.js

// gamehoster-instance-update-frontend.js — the front end predicts the ball's
// bounces off walls and paddles, the same reflection the server does. Scoring,
// serving and the phase machine are server-only and are NOT predicted here.

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 shared bounce, run on both sides

The shared pass runs the predictable part: the wall-and-paddle bounce, so the ball stays smooth between authoritative ticks. It runs on both sides (every client, and the backend, which runs it first before its private extra) so the reflection is written in exactly one place and stays identical on both. Scoring, serving and the phase machine are backend-only (they arrive as confirmed state, not prediction) and so the shared file cannot spawn or destroy. It reads shared / sent state and writes only shared fields.

The shared body (runs on both sides): reads shared / sent state, writes only shared fields; no spawn, destroy or Entropy().

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 the instance's shared / sent state; a write reaches shared fields only (a write outside that class is a silent no-op).
Gamehoster.Player.List()Ids of the players in this instance.
Gamehoster.Player.State.Get(id, name)Read a player's visible 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 predict entities' visible state: no spawn or destroy.
Gamehoster.Time() · Gamehoster.Tick() · Gamehoster.Random()Clock (ms), tick number, and the predictable random in [0,1). There is no Gamehoster.Entropy() here: a predict body writes only deterministic synced state.