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
Contents
The instance folder holds the state schema, the two update scripts, and an optional once-only create body.
| Name | Type | Description |
|---|---|---|
gamehoster-instance-schema.json | file | The instance's own state schema: the fields that describe the whole room. |
gamehoster-instance-create.js | file | Optional: runs once when the instance is created, the place to set up its server-only persistent data. |
gamehoster-instance-update-backend.js | file | The instance's authoritative global pass on the server: the phase machine, scoring and serving. |
gamehoster-instance-update-frontend.js | file | The 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"
}
]
| Name | Type | Visibility | Description |
|---|---|---|---|
phase | string | public | The whole room's stage: waiting (lobby), playing, or over (results). Starts at waiting. |
winner | string | public | Which side won once the phase is over: none, left or right. |
courtW | int | public | Court width; 800. |
courtH | int | public | Court height; 480. |
paddleH | int | public | Paddle height; 80. |
paddleX | int | public | Distance of each paddle from its wall; 24. |
paddleSpeed | number | public | How fast a paddle moves per moveY step; 8. |
ballSpeed | number | public | The ball's serve speed; 6. |
target | int | public | Points 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:
| Key | Type | Description |
|---|---|---|
gamehoster-game-botsMax | int | The 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-botsZeroAt | int | The 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() })
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.
| Function | What 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
}
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().
| Function | What it does |
|---|---|
Gamehoster.Context.instanceId | The 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)
}
}
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.
| Function | What it does |
|---|---|
Gamehoster.Context.instanceId | The 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(). |