Instance
An instance is a single Snake arena: the wrapped grid many players share and the state that drives it. It simulates every tick, so it carries state, a create hook, and one authoritative update. The update runs on the server four times a second and is the whole of the game.
<gamehoster-config-contentRoot>/
game.gamehoster.org/
gamehoster-games/
snake/
gamehoster-instance/
gamehoster-instance-schema.json
gamehoster-instance-create.js
gamehoster-instance-update.js
Contents
The instance folder holds its state schema, a create hook, and one update script.
| Name | Type | Description |
|---|---|---|
gamehoster-instance-schema.json | file | The arena size and tuning constants. |
gamehoster-instance-create.js | file | Sets up the server-only scratch when an instance is created. |
gamehoster-instance-update.js | file | The authoritative per-tick game logic. |
gamehoster-instance-schema.json
The instance's state, all public. Four fields let the client
size and draw the world: gridW, gridH, cell and
viewRadius. The other three are tuning constants the update reads:
foodTarget, startLength and respawnDelay. (Genuinely
server-only scratch, like the tail-stamp sequence, is not in the schema; it lives in the instance's
Persistent bag instead.)
[
{
"gamehoster-instance-schema-name": "gridW",
"gamehoster-instance-schema-type": "int",
"gamehoster-instance-schema-default": 120,
"gamehoster-instance-schema-visibility": "public"
},
{
"gamehoster-instance-schema-name": "gridH",
"gamehoster-instance-schema-type": "int",
"gamehoster-instance-schema-default": 80,
"gamehoster-instance-schema-visibility": "public"
},
{
"gamehoster-instance-schema-name": "cell",
"gamehoster-instance-schema-type": "int",
"gamehoster-instance-schema-default": 16,
"gamehoster-instance-schema-visibility": "public"
},
{
"gamehoster-instance-schema-name": "viewRadius",
"gamehoster-instance-schema-type": "int",
"gamehoster-instance-schema-default": 34,
"gamehoster-instance-schema-visibility": "public"
},
{
"gamehoster-instance-schema-name": "foodTarget",
"gamehoster-instance-schema-type": "int",
"gamehoster-instance-schema-default": 70,
"gamehoster-instance-schema-visibility": "public"
},
{
"gamehoster-instance-schema-name": "startLength",
"gamehoster-instance-schema-type": "int",
"gamehoster-instance-schema-default": 4,
"gamehoster-instance-schema-visibility": "public"
},
{
"gamehoster-instance-schema-name": "respawnDelay",
"gamehoster-instance-schema-type": "int",
"gamehoster-instance-schema-default": 8,
"gamehoster-instance-schema-visibility": "public"
}
]
gamehoster-instance-create.js
Runs once when an instance is created. It sets up the server-only persistent scratch the tick uses: a
single incrementing seq that stamps each tail cell as it is laid, so the update always
knows which cell of a snake is the oldest and should expire first. The persistent state is never sent
to any client.
// Runs once when an instance is created, with that instance in context. We set up the server-only
// persistent scratch the tick uses: a single incrementing `seq` that stamps each tail segment as it
// is laid, so the update always knows which segment of a snake is the oldest and should expire
// first. `persistent` is never serialised or sent to any client.
Gamehoster.Instance.Persistent.Set({ seq: 1 })
gamehoster-instance-update.js
The instance's single authoritative update, run every tick on the server, four times a second. It is the whole of the game. It builds the occupancy for the tick, moves every snake whose move is due, lays a tail cell where the head was and trims the tail, eats food and grows, or crashes into a body and dies. Killing a snake scatters part of it as food. It then tops the food up to its target and spawns a fresh snake for any dead player whose timer is up.
// The instance's single authoritative update, run each tick on the server (4 ticks per second). It
// is the whole of the game:
//
// 1 occupancy: build the grid the instance holds this tick, what is in every cell
// 2 move: advance each snake whose move is due, lay a tail segment, trim the tail, eat food,
// or crash into a body and die
// 3 food: top the field up to its target (it only drops below by being eaten)
// 4 respawn: place a fresh snake for any dead player whose timer is up, and for new joiners
//
// A snake moves every OTHER tick, or every tick while it holds the direction it is already going.
// Its body is a run of separate `tail` entities that never move: each is laid where the head was and
// expires from the far end as the head advances, kept to length minus one. Eating a food raises the
// length, so for one move nothing expires and the snake grows. Killing a snake scatters part of it
// as food (each of its cells has a one in four chance), which can push the field above its target,
// so no new food appears until eating brings it back below. Directions: 0 right, 1 down, 2 left, 3 up.
const here = Gamehoster.Context.instanceId
const gridW = Gamehoster.Instance.State.Get(here, "gridW")
const gridH = Gamehoster.Instance.State.Get(here, "gridH")
const foodTarget = Gamehoster.Instance.State.Get(here, "foodTarget")
const startLen = Gamehoster.Instance.State.Get(here, "startLength")
const respawnDelay = Gamehoster.Instance.State.Get(here, "respawnDelay")
const tick = Gamehoster.Tick()
let P = Gamehoster.Instance.Persistent.Get()
if (!P || typeof P.seq !== "number") { P = { seq: 1 }; Gamehoster.Instance.Persistent.Set(P) }
const DX = [1, 0, -1, 0], DY = [0, 1, 0, -1]
const key = (x, y) => x + "," + y
const wrapX = (x) => ((x % gridW) + gridW) % gridW
const wrapY = (y) => ((y % gridH) + gridH) % gridH
// ── 1 · occupancy for this tick ──
const tailAt = new Map() // "x,y" -> tail entity id
const headAt = new Map() // "x,y" -> player id (living snakes)
const foodAt = new Map() // "x,y" -> food entity id
const ownerTails = new Map() // player id -> [{ id }], oldest segment first
for (const tid of Gamehoster.Entity.List("tail")) {
const x = Gamehoster.Entity.State.Get(tid, "x"), y = Gamehoster.Entity.State.Get(tid, "y")
tailAt.set(key(x, y), tid)
const tmem = Gamehoster.Entity.Persistent.Get(tid) // owner + seq are SERVER-ONLY (Entity.Persistent), never streamed
const owner = tmem ? tmem.owner : ""
let arr = ownerTails.get(owner); if (!arr) { arr = []; ownerTails.set(owner, arr) }
arr.push({ id: tid, seq: tmem ? tmem.seq : 0 })
}
for (const arr of ownerTails.values()) arr.sort((a, b) => a.seq - b.seq) // oldest (lowest seq) first
for (const fid of Gamehoster.Entity.List("food")) foodAt.set(key(Gamehoster.Entity.State.Get(fid, "x"), Gamehoster.Entity.State.Get(fid, "y")), fid)
for (const pid of Gamehoster.Player.List()) {
if (Gamehoster.Player.State.Get(pid, "alive")) headAt.set(key(Gamehoster.Player.State.Get(pid, "x"), Gamehoster.Player.State.Get(pid, "y")), pid)
}
function addFood(x, y) { foodAt.set(key(x, y), Gamehoster.Entity.Spawn("food", { x: x, y: y })) }
// kill a snake: scatter part of it as food, destroy its tail, schedule a respawn
function kill(pid) {
const hx = Gamehoster.Player.State.Get(pid, "x"), hy = Gamehoster.Player.State.Get(pid, "y")
headAt.delete(key(hx, hy))
if (Gamehoster.Entropy() < 0.25 && !foodAt.has(key(hx, hy))) addFood(hx, hy)
for (const t of (ownerTails.get(pid) || [])) {
const tx = Gamehoster.Entity.State.Get(t.id, "x"), ty = Gamehoster.Entity.State.Get(t.id, "y")
tailAt.delete(key(tx, ty))
if (Gamehoster.Entropy() < 0.25 && !foodAt.has(key(tx, ty))) addFood(tx, ty)
Gamehoster.Entity.Destroy(t.id)
}
ownerTails.set(pid, [])
Gamehoster.Player.State.Set(pid, "alive", false)
const mem = Gamehoster.Player.Persistent.Get(pid) // respawn timer is SERVER-ONLY (Player.Persistent)
if (mem) mem.respawnAt = tick + respawnDelay
}
// ── 2 · move every snake whose move is due ──
for (const pid of Gamehoster.Player.List()) {
if (!Gamehoster.Player.State.Get(pid, "alive")) continue
const mem = Gamehoster.Player.Persistent.Get(pid); if (!mem) continue // length/held/pending/moveCounter are SERVER-ONLY
const dir0 = Gamehoster.Player.State.Get(pid, "dir")
const held = mem.held
const moveEvery = (held !== -1 && held === dir0) ? 1 : 2 // boost while holding the current direction
const mc = mem.moveCounter + 1
if (mc < moveEvery) { mem.moveCounter = mc; continue }
mem.moveCounter = 0
// apply a queued turn, re-checked against the current heading so it is never a reversal
let dir = dir0
const pending = mem.pending
mem.pending = -1
if (pending !== -1 && pending !== dir && pending !== (dir + 2) % 4) dir = pending
const x = Gamehoster.Player.State.Get(pid, "x"), y = Gamehoster.Player.State.Get(pid, "y")
const nx = wrapX(x + DX[dir]), ny = wrapY(y + DY[dir]), nk = key(nx, ny)
// the head may enter the cell its own tail-tip is about to vacate this move
const myTails = ownerTails.get(pid) || []
const keep = mem.length - 1
let freeTip = null
if (myTails.length >= keep && myTails.length > 0) {
const t0 = myTails[0]; freeTip = key(Gamehoster.Entity.State.Get(t0.id, "x"), Gamehoster.Entity.State.Get(t0.id, "y"))
}
// crash into any body segment or another living head
if ((tailAt.has(nk) && nk !== freeTip) || (headAt.has(nk) && headAt.get(nk) !== pid)) { kill(pid); continue }
// eat a food on the new cell: grow, score, remove the food
if (foodAt.has(nk)) {
Gamehoster.Entity.Destroy(foodAt.get(nk)); foodAt.delete(nk)
mem.length = mem.length + 1
Gamehoster.Player.State.Set(pid, "score", Gamehoster.Player.State.Get(pid, "score") + 1)
}
// lay a tail segment where the head was, then advance the head. `owner`/`seq` are server-only
// (Entity.Persistent) — the client only needs the segment's x/y/color.
const tid = Gamehoster.Entity.Spawn("tail", { x: x, y: y, color: Gamehoster.Player.State.Get(pid, "color") })
Gamehoster.Entity.Persistent.Set(tid, { owner: pid, seq: P.seq++ })
tailAt.set(key(x, y), tid)
headAt.delete(key(x, y)); headAt.set(nk, pid)
Gamehoster.Player.State.Set(pid, "x", nx)
Gamehoster.Player.State.Set(pid, "y", ny)
Gamehoster.Player.State.Set(pid, "dir", dir)
// trim the tail back to length minus one (a snake that just ate keeps the extra segment this move)
myTails.push({ id: tid })
const keepNow = mem.length - 1
while (myTails.length > keepNow) {
const old = myTails.shift()
tailAt.delete(key(Gamehoster.Entity.State.Get(old.id, "x"), Gamehoster.Entity.State.Get(old.id, "y")))
Gamehoster.Entity.Destroy(old.id)
}
}
// ── 3 · top the food up to the target (it only falls below it by being eaten) ──
let foodCount = Gamehoster.Entity.List("food").length
let guard = foodTarget + 4
while (foodCount < foodTarget && guard-- > 0) {
let placed = false
for (let t = 0; t < 10; t++) {
const rx = Math.floor(Gamehoster.Entropy() * gridW), ry = Math.floor(Gamehoster.Entropy() * gridH), k = key(rx, ry)
if (tailAt.has(k) || headAt.has(k) || foodAt.has(k)) continue
addFood(rx, ry); foodCount++; placed = true; break
}
if (!placed) break // ten tries found no clear cell this pass; try again next tick
}
// ── 4 · spawn a fresh snake for each dead player whose timer is up (a new joiner starts dead too) ──
for (const pid of Gamehoster.Player.List()) {
if (Gamehoster.Player.State.Get(pid, "alive")) continue
const mem = Gamehoster.Player.Persistent.Get(pid); if (!mem) continue // respawn timer + snake scratch are SERVER-ONLY
if (tick < mem.respawnAt) continue
let hx = -1, hy = -1
for (let t = 0; t < 24; t++) {
const rx = Math.floor(Gamehoster.Entropy() * gridW), ry = Math.floor(Gamehoster.Entropy() * gridH)
if (!tailAt.has(key(rx, ry)) && !headAt.has(key(rx, ry)) && !foodAt.has(key(rx, ry))) { hx = rx; hy = ry; break }
}
if (hx < 0) continue // arena momentarily full; try again next tick
const dir = Math.floor(Gamehoster.Entropy() * 4)
const color = Gamehoster.Player.State.Get(pid, "color")
// lay the starting body behind the head, the tip (furthest back) with the lowest seq so it expires first
const arr = []
for (let k = startLen - 1; k >= 1; k--) {
const bx = wrapX(hx - DX[dir] * k), by = wrapY(hy - DY[dir] * k)
if (tailAt.has(key(bx, by)) || headAt.has(key(bx, by))) continue
const tid = Gamehoster.Entity.Spawn("tail", { x: bx, y: by, color: color }) // owner/seq → Entity.Persistent
Gamehoster.Entity.Persistent.Set(tid, { owner: pid, seq: P.seq++ })
tailAt.set(key(bx, by), tid); arr.push({ id: tid })
}
ownerTails.set(pid, arr)
headAt.set(key(hx, hy), pid)
Gamehoster.Player.State.Set(pid, "x", hx)
Gamehoster.Player.State.Set(pid, "y", hy)
Gamehoster.Player.State.Set(pid, "dir", dir)
mem.length = startLen
mem.held = -1
mem.pending = -1
mem.moveCounter = 0
Gamehoster.Player.State.Set(pid, "score", 0)
Gamehoster.Player.State.Set(pid, "alive", true)
}