Command
The things a player can send. Chess has three:
ready accepts in the lobby, move plays a piece on your turn, and
resign gives up the game. Each is a directory holding a
param schema, a writes manifest, and a handler. Every field in
Chess is sent, so all three handlers run only on the server: nothing is predicted, and clients
see the result when the next frame arrives.
move
Plays a piece on your turn. Its params are the from and to squares in
algebraic form ("e2", "e4"). The handler is the rules engine: it rejects the
move unless the game is in play, it is your turn, and the piece and its path are legal, then applies the
capture, the move, castling, and the win-or-pass-turn outcome.
[
{
"gamehoster-command-schema-name": "from",
"gamehoster-command-schema-type": "string"
},
{
"gamehoster-command-schema-name": "to",
"gamehoster-command-schema-type": "string"
}
]
{
"writes": []
}
// Move a piece. `from`/`to` are algebraic squares ("e2", "e4"). This runs on the
// server (every field is `sent`, so nothing is predicted) and enforces genuine
// per-piece legality: pawn steps and diagonal captures, knight jumps, sliding
// bishop/rook/queen with path-blocking, the one-square king, and basic castling.
// A move is rejected with a bare `return` unless the game is in play, it is the
// sender's turn, the moved piece is theirs, and the move is legal for that piece.
const here = Gamehoster.Context.instanceId
if (Gamehoster.Instance.State.Get(here, "phase") !== "playing") return
const me = Gamehoster.Context.playerId
const myColor = Gamehoster.Player.State.Get(me, "color")
if (Gamehoster.Instance.State.Get(here, "turn") !== myColor) return // not your turn
// parse the two squares into 0-based file/rank (a1 = 0,0 … h8 = 7,7)
const from = Gamehoster.Command.Get("from")
const to = Gamehoster.Command.Get("to")
if (typeof from !== "string" || typeof to !== "string" || from.length < 2 || to.length < 2) return
const ff = from.charCodeAt(0) - 97, fr = from.charCodeAt(1) - 49
const tf = to.charCodeAt(0) - 97, tr = to.charCodeAt(1) - 49
if (ff < 0 || ff > 7 || fr < 0 || fr > 7 || tf < 0 || tf > 7 || tr < 0 || tr > 7) return
if (ff === tf && fr === tr) return // must actually move
const pieces = Gamehoster.Entity.List("piece")
const at = (f, r) => pieces.find(id =>
Gamehoster.Entity.State.Get(id, "file") === f && Gamehoster.Entity.State.Get(id, "rank") === r)
const moving = at(ff, fr)
if (moving === undefined) return // no piece to move
if (Gamehoster.Entity.State.Get(moving, "color") !== myColor) return // not your piece
const target = at(tf, tr)
const targetKind = target !== undefined ? Gamehoster.Entity.State.Get(target, "kind") : undefined
if (target !== undefined && Gamehoster.Entity.State.Get(target, "color") === myColor) return // no self-capture
// every square strictly between two collinear squares must be empty
const pathClear = (sf, sr, ef, er) => {
const sx = Math.sign(ef - sf), sy = Math.sign(er - sr)
let f = sf + sx, r = sr + sy
while (f !== ef || r !== er) {
if (at(f, r) !== undefined) return false
f += sx; r += sy
}
return true
}
const kind = Gamehoster.Entity.State.Get(moving, "kind")
const dx = tf - ff, dy = tr - fr
const adx = Math.abs(dx), ady = Math.abs(dy)
let legal = false
let castleRook // the rook to shift, if this move is a castle
if (kind === "pawn") {
const dir = myColor === "white" ? 1 : -1
const startRank = myColor === "white" ? 1 : 6
if (dx === 0 && dy === dir && target === undefined) {
legal = true // single step
} else if (dx === 0 && dy === 2 * dir && fr === startRank &&
target === undefined && at(ff, fr + dir) === undefined) {
legal = true // double step from home
} else if (adx === 1 && dy === dir && target !== undefined) {
legal = true // diagonal capture
}
} else if (kind === "knight") {
legal = (adx === 1 && ady === 2) || (adx === 2 && ady === 1)
} else if (kind === "bishop") {
legal = adx === ady && pathClear(ff, fr, tf, tr)
} else if (kind === "rook") {
legal = (dx === 0 || dy === 0) && pathClear(ff, fr, tf, tr)
} else if (kind === "queen") {
legal = (adx === ady || dx === 0 || dy === 0) && pathClear(ff, fr, tf, tr)
} else if (kind === "king") {
if (adx <= 1 && ady <= 1) {
legal = true // ordinary one-square king move
} else if (dy === 0 && adx === 2 && !Gamehoster.Entity.State.Get(moving, "moved")) {
// basic castling: king and rook unmoved, squares between them empty. (Does
// not test for check or squares the king passes through, a simplification.)
const rookF = dx > 0 ? 7 : 0
const rook = at(rookF, fr)
if (rook !== undefined &&
Gamehoster.Entity.State.Get(rook, "kind") === "rook" &&
Gamehoster.Entity.State.Get(rook, "color") === myColor &&
!Gamehoster.Entity.State.Get(rook, "moved") &&
pathClear(ff, fr, rookF, fr)) {
legal = true
castleRook = rook
}
}
}
if (!legal) return
// apply the move: remove any captured piece, then relocate the mover
if (target !== undefined) Gamehoster.Entity.Destroy(target)
Gamehoster.Entity.State.Set(moving, "file", tf)
Gamehoster.Entity.State.Set(moving, "rank", tr)
Gamehoster.Entity.State.Set(moving, "moved", true)
// on a castle, jump the rook to the far side of the king
if (castleRook !== undefined) {
Gamehoster.Entity.State.Set(castleRook, "file", dx > 0 ? 5 : 3)
Gamehoster.Entity.State.Set(castleRook, "moved", true)
}
// capturing the king ends the game (checkmate is simplified to king-capture);
// otherwise hand the turn to the opponent.
if (targetKind === "king") {
Gamehoster.Instance.State.Set(here, "winner", myColor)
Gamehoster.Instance.State.Set(here, "phase", "over")
} else {
Gamehoster.Instance.State.Set(here, "turn", myColor === "white" ? "black" : "white")
}
ready
Toggles your accept flag in the lobby. It takes no params and writes only player.ready.
Two ready players is the signal the instance update waits for
before it starts the game; once play has begun the command is ignored.
[]
{
"writes": ["player.ready"]
}
// Toggle the sending player's ready flag while in the lobby. `ready` is a `sent`
// field (server-authoritative), so this only takes effect on the server. Ignored
// once the game has started.
if (Gamehoster.Instance.State.Get(Gamehoster.Context.instanceId, "phase") !== "lobby") return
const me = Gamehoster.Context.playerId
Gamehoster.Player.State.Set(me, "ready", !Gamehoster.Player.State.Get(me, "ready"))
resign
Gives up the game in progress: the opponent is recorded as the winner and the phase moves to
over. It takes no params and writes nothing to the player; it only edits instance state, and
only while a game is being played.
[]
{
"writes": []
}
// The sending player resigns: the opponent wins and the game ends. Only valid
// while a game is in progress.
const here = Gamehoster.Context.instanceId
if (Gamehoster.Instance.State.Get(here, "phase") !== "playing") return
const me = Gamehoster.Context.playerId
const myColor = Gamehoster.Player.State.Get(me, "color")
Gamehoster.Instance.State.Set(here, "winner", myColor === "white" ? "black" : "white")
Gamehoster.Instance.State.Set(here, "phase", "over")