Instance
An instance is a single Pong match: the court two players share and the state that drives it. Unlike
the static room in Pointing, this instance simulates every
tick, so it carries both state and two
update passes. The update-frontend.js pass is the
shared physics that both the server and every client run (so the ball stays smooth between snapshots);
the update-backend.js pass is the private, authoritative extra the server alone runs after
it (the phase machine, scoring and serving).
gamehoster-instance-schema.json
The match's shared state, all sent to every client. The
phase (waiting, playing or over) and
winner drive the flow; the rest are the fixed dimensions and speeds every side needs to
agree on: court size, paddle size, position and speed, ball speed, and the target score
that wins.
[
{
"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"
}
]
gamehoster-instance-update-frontend.js
The shared physics pass, run on both sides: on the server (which runs it first, as the authority) and on every client (as prediction, so the ball keeps moving between the sparse snapshots). It bounces the ball off the top and bottom walls and off either paddle, angling the rebound by where the ball met the paddle. Scoring, serving and the phase machine are deliberately left out: they are authoritative and live in the backend pass.
// The shared instance pass: bounce the ball off the walls and paddles. This file
// runs on both sides: on every client (prediction, so the ball stays smooth
// between authoritative ticks) and on the server, which runs it first. Scoring,
// serving and the phase machine are backend-only and live in the backend file.
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)
}
}
gamehoster-instance-update-backend.js
The backend-only pass, run by the server alone, after the shared pass has already bounced the ball
this tick. It owns everything a client must not guess: it starts a match once both players are
ready, judges a goal when the ball crosses an edge,
bumps the scorer, ends the game at the target score, and otherwise re-serves. Both the
opening serve direction and the ball's launch angle come from
Gamehoster.Entropy(), so the randomness is
authoritative and identical everywhere.
// The instance's backend-only pass (step 3): the private, authoritative extra.
// 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
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,
// a cross-cutting, authoritative outcome ──
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
}