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"
  }
]
chess/gamehoster-commands/move/gamehoster-command-schema.json
{
  "writes": []
}
chess/gamehoster-commands/move/gamehoster-command.json
// 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")
}
chess/gamehoster-commands/move/gamehoster-command-handler.js

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.

[]
chess/gamehoster-commands/ready/gamehoster-command-schema.json
{
  "writes": ["player.ready"]
}
chess/gamehoster-commands/ready/gamehoster-command.json
// 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"))
chess/gamehoster-commands/ready/gamehoster-command-handler.js

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.

[]
chess/gamehoster-commands/resign/gamehoster-command-schema.json
{
  "writes": []
}
chess/gamehoster-commands/resign/gamehoster-command.json
// 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")
chess/gamehoster-commands/resign/gamehoster-command-handler.js