Player
The files that describe a Snake player: the one field they supply when they join, the per-instance state the server keeps for them, their name, colour, head cell, heading, alive flag and score, while they are connected, and the three scripts the server runs, one the moment they join, one when they leave, and one that picks the slice of the arena each viewer is sent.
<gamehoster-config-contentRoot>/
game.gamehoster.org/
gamehoster-games/
snake/
gamehoster-player/
gamehoster-player-join-schema.json
gamehoster-player-schema.json
gamehoster-player-join.js
gamehoster-player-leave.js
gamehoster-player-view.js
Contents
The player folder holds two schemas and the three lifecycle scripts.
| Name | Type | Description |
|---|---|---|
gamehoster-player-join-schema.json | file | The data a player supplies on connect. |
gamehoster-player-schema.json | file | The per-player state. |
gamehoster-player-join.js | file | Route the player in and start their snake. |
gamehoster-player-leave.js | file | Destroy a player's tail when they leave. |
gamehoster-player-view.js | file | Send each viewer just the arena near them. |
gamehoster-player-join-schema.json
The shape of the data a client sends to join. Snake asks for one field, the player's display
name, which the join script copies onto their state.
[
{
"gamehoster-player-join-schema-name": "name",
"gamehoster-player-join-schema-type": "string"
}
]
gamehoster-player-schema.json
The per-instance state carried for each player. Every field is
public, so name, color,
x, y, dir, alive and score stream to
everyone who can see the snake and draw it; the bounded head cell and heading carry
min/max range hints. The snake's server-only scratch, its
length, the direction currently held, a pending queued turn,
the moveCounter move cadence and its respawnAt timer, is not in the schema
at all: it lives in the free-form Player.Persistent bag,
which is never streamed.
[
{
"gamehoster-player-schema-name": "name",
"gamehoster-player-schema-type": "string",
"gamehoster-player-schema-visibility": "public"
},
{
"gamehoster-player-schema-name": "color",
"gamehoster-player-schema-type": "string",
"gamehoster-player-schema-visibility": "public"
},
{
"gamehoster-player-schema-name": "x",
"gamehoster-player-schema-type": "int",
"gamehoster-player-schema-default": 0,
"gamehoster-player-schema-visibility": "public",
"gamehoster-player-schema-min": 0,
"gamehoster-player-schema-max": 120
},
{
"gamehoster-player-schema-name": "y",
"gamehoster-player-schema-type": "int",
"gamehoster-player-schema-default": 0,
"gamehoster-player-schema-visibility": "public",
"gamehoster-player-schema-min": 0,
"gamehoster-player-schema-max": 80
},
{
"gamehoster-player-schema-name": "dir",
"gamehoster-player-schema-type": "int",
"gamehoster-player-schema-default": 0,
"gamehoster-player-schema-visibility": "public",
"gamehoster-player-schema-min": 0,
"gamehoster-player-schema-max": 3
},
{
"gamehoster-player-schema-name": "alive",
"gamehoster-player-schema-type": "bool",
"gamehoster-player-schema-default": false,
"gamehoster-player-schema-visibility": "public"
},
{
"gamehoster-player-schema-name": "score",
"gamehoster-player-schema-type": "int",
"gamehoster-player-schema-default": 0,
"gamehoster-player-schema-visibility": "public"
}
]
gamehoster-player-join.js
Runs on the server as a player connects. It routes them into an instance that still has room, or
creates one. It sets their public name and a random colour, and marks the snake not alive with an
immediate respawn, so the instance update spawns it on the next
tick at a clear cell. The snake's server-only scratch, its length, held direction, queued turn, move
counter and respawn timer, is seeded into the Player.Persistent bag, never streamed.
// Runs on the server as a player connects. It routes them into an instance that still has room (or
// opens a fresh one), gives their snake a name and a colour, and marks it dead with an immediate
// respawn. The instance update spawns the snake on its next tick, at a clear spot, at the starting
// length. Doing it that way keeps the one snake-placing routine in a single place, the update.
const open = Gamehoster.Instance.List().find(id =>
Gamehoster.Instance.Info(id).players < Gamehoster.Instance.Info(id).capacity)
Gamehoster.Instance.Join(open !== undefined ? open : Gamehoster.Instance.Create())
const me = Gamehoster.Context.playerId
const here = Gamehoster.Context.instanceId
const palette = ["#e6194b", "#3cb44b", "#4363d8", "#f58231", "#911eb4", "#42d4f4", "#f032e6", "#ffe119", "#7cc5ff", "#bfef45"]
// PUBLIC (streamed) head state. The snake starts dead with an immediate respawn; the instance update
// spawns it at a clear spot on its next tick.
Gamehoster.Player.State.Set(me, "name", Gamehoster.Join.Get("name"))
Gamehoster.Player.State.Set(me, "color", palette[Math.floor(Gamehoster.Entropy() * palette.length)])
Gamehoster.Player.State.Set(me, "alive", false)
Gamehoster.Player.State.Set(me, "score", 0)
// SERVER-ONLY per-snake scratch — its length, the direction currently held, a queued turn, the
// move-cadence counter and its respawn timer — live in Player.Persistent, never streamed.
Gamehoster.Player.Persistent.Set(me, {
length: Gamehoster.Instance.State.Get(here, "startLength"),
held: -1, pending: -1, moveCounter: 0, respawnAt: 0
})
gamehoster-player-leave.js
Runs when a player leaves. Their tail is made of entities they own, so it destroys the ones still on
the board; ownership is server-only, read from each tail's Entity.Persistent bag. Food
and other snakes stay as they are.
// A player has left. The engine reclaims their head state; their tail is made of entities they own,
// so we destroy the ones still on the board. Food and other snakes are untouched.
const me = Gamehoster.Context.playerId
for (const tid of Gamehoster.Entity.List("tail")) {
const mem = Gamehoster.Entity.Persistent.Get(tid) // owner is SERVER-ONLY (Entity.Persistent)
if (mem && mem.owner === me) Gamehoster.Entity.Destroy(tid)
}
gamehoster-player-view.js
Per-viewer relevance, the fog of war. It runs once per viewer at the send rate and includes only the snakes, tail cells and food near that viewer, measured the short way around the wrapped grid. Each player is sent just the slice of the arena on or just off their screen rather than all fifty snakes.
// Per-viewer relevance (fog of war). It runs once per viewer at the send rate and marks only the
// snakes, tail segments and food near that viewer, so each player is sent just the slice of the
// grid on (or just off) their screen rather than all 50 snakes and every segment and every piece of
// food on the whole arena. Distances are in grid cells, measured the short way round the wrap.
const me = Gamehoster.Context.playerId
const here = Gamehoster.Context.instanceId
const gridW = Gamehoster.Instance.State.Get(here, "gridW")
const gridH = Gamehoster.Instance.State.Get(here, "gridH")
const reach = Gamehoster.Instance.State.Get(here, "viewRadius")
const r2 = reach * reach
Gamehoster.View.Include(me)
const cx = Gamehoster.Player.State.Get(me, "x")
const cy = Gamehoster.Player.State.Get(me, "y")
function near(x, y) {
let dx = Math.abs(x - cx); if (dx > gridW / 2) dx = gridW - dx
let dy = Math.abs(y - cy); if (dy > gridH / 2) dy = gridH - dy
return dx * dx + dy * dy <= r2
}
for (const pid of Gamehoster.Player.List()) {
if (near(Gamehoster.Player.State.Get(pid, "x"), Gamehoster.Player.State.Get(pid, "y"))) Gamehoster.View.Include(pid)
}
for (const tid of Gamehoster.Entity.List("tail")) {
if (near(Gamehoster.Entity.State.Get(tid, "x"), Gamehoster.Entity.State.Get(tid, "y"))) Gamehoster.View.Include(tid)
}
for (const fid of Gamehoster.Entity.List("food")) {
if (near(Gamehoster.Entity.State.Get(fid, "x"), Gamehoster.Entity.State.Get(fid, "y"))) Gamehoster.View.Include(fid)
}