Player
A player is the connected individual: the person on the other end of a session. A player
is nothing more than two pieces of state: the session-fixed join data they supply on
connect (a display name, say) and their per-instance state, the fields they carry inside
whatever instance they are in. There is one player
type, so the player files gather into a single
gamehoster-player/ folder inside the game.
The join data is settled once, on connect, and carries unchanged for the whole session. The per-instance state is created when the player is routed into an instance and stepped forward each tick. The files below are the whole of a player: the two schemas that shape that state, the scripts that route the player in, tear them down and update them, one optional script that scopes what each viewer sees, and an optional client-only body that predicts your own object ahead of the server.
<gamehoster-config-contentRoot>/
game.gamehoster.org/
gamehoster-games/
asteroids/
gamehoster-player/
gamehoster-player-join-schema.json
gamehoster-player-schema.json
gamehoster-player-join.js
gamehoster-player-leave.js
gamehoster-player-view.js
gamehoster-player-update.js
gamehoster-player-predict.js
Every .js file below is a raw function body: no export, no
function header. It starts straight into JavaScript and reaches the world through the
Gamehoster.* library. Which library calls are in scope depends on the file, and each section
lists exactly what that one can call. Four are always present:
Gamehoster.Time() (ms), Gamehoster.Tick() (integer),
Gamehoster.Random() ([0,1), predictable, seeded by the tick and
the running actor so it is deterministic), and Gamehoster.Entropy() ([0,1),
unpredictable true randomness for authoritative decisions). Every body here runs on the
server, so both randoms are in scope; the only difference is that an
Entropy() outcome must be written into state to reach a client, since a client can never
reproduce it. The one exception is the optional
gamehoster-player-predict.js, which runs in the
client and has its own smaller, prediction-only surface, set out in its own section below.
A body learns the ids in scope from
Gamehoster.Context, a record whose slots are filled for that body's context:
Gamehoster.Context.playerId (the player it acts as),
Gamehoster.Context.instanceId (the instance it is in) and
Gamehoster.Context.entityId (the entity it acts as); a slot that doesn't apply is
undefined. Every
state op names its id, Gamehoster.Player.State.Get(id, "field"),
Gamehoster.Instance.State.Set(id, "field", value) and so on, so a body's first lines are
usually const me = Gamehoster.Context.playerId and
const here = Gamehoster.Context.instanceId.
Writes do not apply the instant you make them. Each body writes into a scratch
buffer (your own reads see your own writes) and, when it returns, the engine flushes
that buffer. A command handler is bounded: it may write
only the fields its writes manifest lists, and a write
to anything else is dropped. The other server bodies here (join, leave, view, and the update body) are the
server's own code and write freely. A dropped write to a real field is a silent
no-op (the authoritative value stands); a write to a field that does not exist is a
contained error, logged and ignored. A field's visibility
decides only what is sent to a client, never what a body may write.
Contents
The player folder holds two schemas, the lifecycle and update scripts, an optional view script, and an optional client-only predict body.
| Name | Type | Description |
|---|---|---|
gamehoster-player-join-schema.json | file | The fixed data a player supplies on connect. |
gamehoster-player-schema.json | file | The player's per-instance state schema. |
gamehoster-player-join.js | file | On connect: route the player into an instance and set them up. |
gamehoster-player-leave.js | file | Clean up when a player leaves. |
gamehoster-player-view.js | file | Optional: which players and entities are relevant to a viewer. |
gamehoster-player-update.js | file | Optional: the player's one authoritative per-tick update, run on the server. |
gamehoster-player-predict.js | file | Optional: client-only prediction of your own object, run in the browser. |
gamehoster-player-join-schema.json
The fixed data a player supplies on connect: the one thing about them that is settled
for the whole session, before any instance is involved. It is an array of field objects
(the names are not used as keys), each with a -name, a
-type, and an optional -default. Where a
default is omitted the player must supply the value; where it is given, that value
stands in when they don't. Join data is fixed, never state, so these entries carry
no visibility. Pong asks only for a name:
[
{
"gamehoster-player-join-schema-name": "name",
"gamehoster-player-join-schema-type": "string"
}
]
| Key | Type | Meaning |
|---|---|---|
gamehoster-player-join-schema-name | string | The field's name. |
gamehoster-player-join-schema-type | string | Its type. |
gamehoster-player-join-schema-default | optional | A default value; omit it to force the player to supply one. |
gamehoster-player-schema.json
The player's per-instance state: the fields they carry inside an instance. Like the
join schema it is an array of field objects (a name is never used as an
object key) each carrying a fully-qualified -name and
-type. State fields additionally declare
-visibility (public, streamed to everyone who can see the object,
or private, streamed only to the player it belongs to), with an optional -default;
a bounded numeric field may also carry the -min/-max/-decimals
range hints. In pong a player is their name and
side, whether they're ready, their score, and the paddle position
y:
[
{
"gamehoster-player-schema-name": "name",
"gamehoster-player-schema-type": "string",
"gamehoster-player-schema-visibility": "public"
},
{
"gamehoster-player-schema-name": "side",
"gamehoster-player-schema-type": "string",
"gamehoster-player-schema-visibility": "public"
},
{
"gamehoster-player-schema-name": "ready",
"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-schema-name": "y",
"gamehoster-player-schema-type": "number",
"gamehoster-player-schema-default": 240,
"gamehoster-player-schema-visibility": "public",
"gamehoster-player-schema-min": 0,
"gamehoster-player-schema-max": 480,
"gamehoster-player-schema-decimals": 1
}
]
| Field | Type | visibility | What it is |
|---|---|---|---|
name | string | public | Display name, copied from the player's join data when they enter. |
side | string | public | Which paddle they hold: "left" or "right", assigned to whichever side is free. |
ready | bool | public | Whether they've readied up; starts false. |
score | int | public | Points this player has scored; starts 0. |
y | number | public | Paddle position, streamed to the opponent; the client eases it between updates so it stays smooth. Bounded 0–480 to one decimal place with -min/-max/-decimals. |
A field's visibility settles the whole of its networking: whether the value
is sent to clients, and to whom. There are exactly two values.
visibility | How the client gets it |
|---|---|
public | The default. Computed on the server and sent to every viewer the object is currently relevant to, whenever it changes. The client holds it and eases numeric fields between updates. |
private | Owner-only: on a player, sent to that player alone and to nobody else. A private hand of cards, a personal timer only its holder should see. |
private is only meaningful on a player, the one object with an owner; an
instance or entity has none, so
every one of their streamed fields is public. Pong's paddle y is public, so each
player sees the other's paddle; a hidden hand would be private and reach only its holder.
State that no client should ever see is not a schema field at all. Per-player
server-only data — a cooldown, an AI's target, matchmaking scratch — lives in the player's
persistent bag, Gamehoster.Player.Persistent(id), with .Get() and
.Set(value); .Get() returns the live object, so a body may mutate it in place. It is
never typed, tracked, serialised or streamed, the private counterpart to the streamed schema state, and the
same bag exists on the instance, each
entity and each bot. Values you can
recompute from public state (a bullet's velocity from its angle and speed) need not be stored at all.
A bounded numeric field may declare -min and
-max for its range and, on a number,
-decimals (a non-negative integer) for its significant decimal places. The
engine uses them to validate writes and, in future, to encode the field compactly on the wire. An unbounded
field — a tick counter, a running score — simply omits them.
These declarations govern only what crosses the wire. They are not a write rule: every
server body may read any field it can name, and only a command is
bounded in what it writes, by its writes manifest.
How a drawn field is eased or snapped is finally the front end's choice; the
Types reference lists the default per type, and a per-object
-smooth.js body may override it.
gamehoster-player-join.js
Runs on the server the moment a player connects, and does two jobs. First it
routes the player into an instance: it lists
the live instances, filters them (pong wants one still waiting for an opponent that has
room), and Joins that one, or Creates a fresh instance when none fits. Then it
sets the player up: it reads their fixed join
data, gives them whichever side is free, copies over their name, and
centres their paddle.
// route: join a waiting game that has room, else start a new one
const open = Gamehoster.Instance.List().find(id =>
Gamehoster.Instance.Info(id).players < Gamehoster.Instance.Info(id).capacity &&
Gamehoster.Instance.State.Get(id, "phase") === "waiting")
Gamehoster.Instance.Join(open !== undefined ? open : Gamehoster.Instance.Create())
// set up: whichever side is free. Now that we have joined, Context.playerId names
// us and Context.instanceId our court (both undefined until the Join above).
const me = Gamehoster.Context.playerId
const here = Gamehoster.Context.instanceId
const taken = Gamehoster.Player.List()
.filter(id => id !== me)
.map(id => Gamehoster.Player.State.Get(id, "side"))
Gamehoster.Player.State.Set(me, "side", taken.includes("left") ? "right" : "left")
Gamehoster.Player.State.Set(me, "name", Gamehoster.Join.Get("name"))
Gamehoster.Player.State.Set(me, "score", 0)
Gamehoster.Player.State.Set(me, "ready", false)
Gamehoster.Player.State.Set(me, "y", Gamehoster.Instance.State.Get(here, "courtH") / 2)
| Function | What it does |
|---|---|
Gamehoster.Context.playerId · Gamehoster.Context.instanceId | The ids in scope: the joining player, and their instance (which is undefined until the Instance.Join above, so read it after). |
Gamehoster.Join.Get(name) | A value the player supplied against the join schema. |
Gamehoster.Instance.List() | Ids of the live instances, to route between. |
Gamehoster.Instance.Info(id) | {players, capacity, …} for an instance, to choose between them. |
Gamehoster.Instance.State.Get(id, name) | Read an instance's state field: e.g. to filter by phase. |
Gamehoster.Instance.Join(id) | Place this player into that instance. |
Gamehoster.Instance.Create() | Make a fresh instance and return its id. |
Gamehoster.Player.List() | Ids of the players in the instance. |
Gamehoster.Player.State.Get(id, name) | Read another player's state field. |
Gamehoster.Player.State.Set(Gamehoster.Context.playerId, name, value) | Fill the joining player's per-instance state. |
Gamehoster.Time() · Gamehoster.Tick() · Gamehoster.Random() · Gamehoster.Entropy() | Clock (ms), tick number, predictable random in [0,1) (seeded by tick + actor), and unpredictable Entropy() for authoritative choices such as picking a starting side. |
gamehoster-player-leave.js
Runs on the server when a player leaves, to clean up. Pong keeps no per-player
resources beyond the paddle the engine reclaims, so it only drops the instance back to
waiting if a match was in play.
const here = Gamehoster.Context.instanceId
if (Gamehoster.Instance.State.Get(here, "phase") === "playing")
Gamehoster.Instance.State.Set(here, "phase", "waiting")
| Function | What it does |
|---|---|
Gamehoster.Context.playerId · Gamehoster.Context.instanceId | The ids in scope: the leaving player and their instance. |
Gamehoster.Instance.State.Get(id, name) · .Set(id, name, value) | Read or write the instance's state field. |
Gamehoster.Player.List() | Ids of the players still in the instance. |
Gamehoster.Player.State.Get(id, name) · .Set(id, name, value) | Read or write a remaining player's state field: e.g. hand a leaver's resource to whoever is left. |
Gamehoster.Entity.List(type) | Ids of the live entities of a type. |
Gamehoster.Entity.State.Get(id, name) | Read an entity's state field. |
Gamehoster.Entity.Destroy(id) | Remove an entity: e.g. a resource this player owned. |
Gamehoster.Time() · Gamehoster.Tick() · Gamehoster.Random() · Gamehoster.Entropy() | Clock (ms), tick number, predictable random in [0,1) (seeded by tick + actor), and unpredictable Entropy(). |
gamehoster-player-view.js
An optional script that decides relevance: which players and
entities a given viewer should receive at all. It is the outer
gate in front of every field's visibility: if an object is not relevant to
you, you receive none of its fields; inside a relevant object, each field still streams by
its own visibility. This is how partial visibility (areas of interest, fog of war) is expressed.
The script is viewer-centric: Gamehoster.Context.playerId is the
viewer, and the body runs on the backend at the send
rate, once per viewer. It calls Gamehoster.View.Include(id) for each player or entity
that should be relevant to that viewer, including ones about to become relevant, so they
appear just before they're needed and nothing pops
in late. If the file is absent, everything is relevant to everyone: full visibility.
Pong, with two paddles and one ball in a single court, has nothing to hide, so it omits the file.
// A viewer sees players near them, plus any about to come into range (pre-fetch).
const me = Gamehoster.Context.playerId
const [mx, my] = [Gamehoster.Player.State.Get(me, "x"), Gamehoster.Player.State.Get(me, "y")]
const near = (x, y, r) => (x - mx) ** 2 + (y - my) ** 2 < r * r
for (const id of Gamehoster.Player.List()) {
const x = Gamehoster.Player.State.Get(id, "x")
const y = Gamehoster.Player.State.Get(id, "y")
if (near(x, y, 600)) Gamehoster.View.Include(id) // visible now, or soon (pre-fetch margin)
}
for (const id of Gamehoster.Entity.List("prop"))
if (near(Gamehoster.Entity.State.Get(id, "x"), Gamehoster.Entity.State.Get(id, "y"), 600))
Gamehoster.View.Include(id)
| Function | What it does |
|---|---|
Gamehoster.Context.playerId · Gamehoster.Context.instanceId | The ids in scope: the viewer this pass is scoping the world for, and their instance. |
Gamehoster.View.Include(id) | Mark a player or entity relevant to the viewer, so its fields sync to them. |
Gamehoster.Player.List() | Ids of the players in the instance, to test against the viewer. |
Gamehoster.Player.State.Get(id, name) | Read a player's state field: e.g. their position. |
Gamehoster.Entity.List(type) | Ids of the live entities of a type. |
Gamehoster.Entity.State.Get(id, name) | Read an entity's state field. |
Gamehoster.Instance.State.Get(id, name) | Read the instance's state field: always relevant, so useful for tuning the test. |
Gamehoster.Time() · Gamehoster.Tick() · Gamehoster.Random() · Gamehoster.Entropy() | Clock (ms), tick number, predictable random, and unpredictable random. |
gamehoster-player-update.js
The player's own per-tick update: step 2 of the tick, where a player
reads the previous state and writes its own. It is one authoritative body, run on the
server, the only simulator. It reads any state it can name through Gamehoster.*
and writes the player's own fields; reading an object outside this instance simply finds nothing, and a write
to a field that does not exist is a contained error, never a throw. The client never runs it: the
front end runs no game logic, it draws the public state the
server streams it.
The body is optional, with no empty stub. A pong paddle has no autonomous per-tick
behaviour (it moves only in response to the moveY command) so
it ships no update file at all. A game whose ship moves itself each tick, like the drifting,
thrusting ship in asteroids, puts that step here; its own
client-side responsiveness lives separately in the predict body
below.
A game that has not been converted may instead ship the legacy pair,
gamehoster-player-update-frontend.js then gamehoster-player-update-backend.js: the
server runs them front then back, both authoritative server code writing the player's own state. The single
body above is the standard shape; the pair is still accepted for older games.
| Function | What it does |
|---|---|
Gamehoster.Context.playerId · Gamehoster.Context.instanceId | The ids in scope: this player and their instance. |
Gamehoster.Player.State.Get(Gamehoster.Context.playerId, name) · .Set(Gamehoster.Context.playerId, name, value) | Read and write this player's own state. |
Gamehoster.Instance.State.Get(id, name) | Read an instance state field. |
Gamehoster.Player.List() | Ids of the players in the instance. |
Gamehoster.Player.State.Get(id, name) | Read another player's state field. |
Gamehoster.Entity.List(type) | Ids of the live entities of a type. |
Gamehoster.Entity.State.Get(id, name) | Read an entity's state field. |
Gamehoster.Time() · Gamehoster.Tick() · Gamehoster.Random() · Gamehoster.Entropy() | Clock (ms), tick number, predictable random in [0,1) (seeded by tick + actor), and unpredictable Entropy() for authoritative choices. |
gamehoster-player-predict.js
An optional, client-only body that predicts the local player's own object ahead of the server. Everything the client draws is normally interpolated a fixed buffer behind the server, which would add a round trip of lag to your own moves. The predict body closes that gap: the client runtime runs it once per tick for your own object, fed your live input, and in one body it simulates the object forward from that input and reconciles it toward the authoritative state, so your object renders at the present. It is opt-in and only for your own object; everything else stays interpolated, and a game without a predict body simply has no prediction. Only asteroids ships one.
It runs in the browser, not on the server, so it has a smaller, prediction-only surface: it
owns just its own predicted copy of the object and never writes real game state, and it has no
Entropy(), no spawn and no destroy. The renderer feeds it
the live input each frame with conn.input(name, value) and reads its own object, already
predicted, straight from conn.frame(); it writes no prediction of its own. The full asteroids
body is on the asteroids player page.
| Function | What it does |
|---|---|
Gamehoster.Context.playerId · Gamehoster.Context.instanceId | The ids in scope: your own object and its instance. |
Gamehoster.Input.Get(name) | The live local input the renderer feeds this frame (thrust held, aim angle), the thing you simulate forward from. |
Gamehoster.Authoritative.Get(id, field) | The newest authoritative server value of a field: the target to reconcile toward. |
Gamehoster.Lead() | How many ticks the client clock leads the newest data, to extrapolate that target to the present. |
Gamehoster.Player.State.Get(me, field) · .Set(me, field, value) | Read and write your own predicted state: the client's own copy, never the server's. |
Gamehoster.Instance.State.Get(here, field) | Read a public instance state field: the tuning constants your motion needs. |
Gamehoster.Tick() | The present tick, the leading edge the prediction runs at. |