Instance

An instance is a single Pong match: the court two players share and the state that drives it. It simulates every tick, so it carries both state and one authoritative update. The update runs on the server: it bounces the ball off the walls and paddles, then runs the phase machine, scoring and the serve.

<gamehoster-config-contentRoot>/
  game.gamehoster.org/
    gamehoster-games/
      pong/
        gamehoster-instance/
          gamehoster-instance-schema.json
          gamehoster-instance-update.js
The gamehoster-instance/ folder: the state schema and the authoritative update.

Contents

The instance folder holds its state schema and one update script.

NameTypeDescription
gamehoster-instance-schema.jsonfileThe court and tuning constants.
gamehoster-instance-update.jsfileThe authoritative per-tick game logic.

gamehoster-instance-schema.json

The match's state, all public, streamed to both players. The phase (waiting, playing or over) and winner drive the flow; the rest are the fixed 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-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

gamehoster-instance-update.js

The instance's single authoritative update, run each tick on the server. It bounces the ball off the top and bottom walls and off either paddle, angling the rebound by where the ball met the paddle, then runs the phase machine: 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 single authoritative update, run each tick on the server: bounce the ball off
// the walls and paddles, then run the phase machine, scoring and the serve. (This was two passes,
// -update-frontend.js and -update-backend.js; both always ran on the server in this order.)

const here    = Gamehoster.Context.instanceId
const phase   = Gamehoster.Instance.State.Get(here, "phase")
const players = Gamehoster.Player.List()
const courtH  = Gamehoster.Instance.State.Get(here, "courtH")
const courtW  = Gamehoster.Instance.State.Get(here, "courtW")

// 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", courtW / 2)
  Gamehoster.Entity.State.Set(b, "y", 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
  // bump the public reset counter: the front-end smooth body snaps the ball to the centre on this
  // change instead of interpolating it across the court from wherever it went out.
  Gamehoster.Entity.State.Set(b, "resets", Gamehoster.Entity.State.Get(b, "resets") + 1)
}

// ── 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: bounce the ball off the top and bottom walls and off the paddles ──
const ball = Gamehoster.Entity.List("ball")[0]
if (ball === undefined) return
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)
  }
}

// ── judge a goal: the ball past a court edge scores for the other side ──
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)
  }
}
pong/gamehoster-instance/gamehoster-instance-update.js