Frontend

Everything so far has been the backend, the schemas and scripts the server runs. The front end is the browser half, and Chess is built on the front-end abstraction: the page hands over a <canvas> by id, and the game ships its drawing as snippets in a gamehoster-frontend/ folder beside its backend files. The game's baked client (gamehoster.client.js, generated and served with the game) owns the loop, the input and the per-object lifecycle. It hands each snippet a Gamehoster library that reads the whole world at once: Gamehoster.Instance, Gamehoster.Players and Gamehoster.Entities, each object a view with its fields at the top level plus .current (its newest authoritative sample), .prev and .data (its front-end persistent bag). Chess is turn-based, so there is no control loop and no prediction: setup caches the 2D context and the glyph table, render draws the board and pieces each frame, and two per-event input bodies turn a click or Space into a ready, and a click during play into a piece selection and a move. Every piece carries discrete file/rank ints read from .current, so it snaps to its square the moment the server confirms a move.

<gamehoster-config-contentRoot>/
  game.gamehoster.org/
    gamehoster-games/
      chess/
        gamehoster-frontend/
          gamehoster-frontend-setup.js
          gamehoster-frontend-render.js
          gamehoster-frontend-input-mouse-button-left-press.js
          gamehoster-frontend-input-keyboard-space-press.js
The gamehoster-frontend/ folder: setup, render, and the two per-event input bodies.

Contents

The folder holds a one-time setup, the per-frame render, and two per-event input bodies. There is no tick loop or predict body: chess is turn-based, so the client only draws the authoritative board and sends a command on a click or a key.

NameTypeDescription
gamehoster-frontend-setup.jsfileCache the 2D context and glyph table once, on connect.
gamehoster-frontend-render.jsfileDraw the board, pieces, selection and status each frame.
gamehoster-frontend-input-mouse-button-left-press.jsfileReady up, or select a piece and send a move.
gamehoster-frontend-input-keyboard-space-press.jsfileReady up on Space.

Loading the client

The Play page provides only a <canvas>, loads the game's baked client from the game server, and calls Gamehoster.start on the canvas with a random display name. The baked client is generic: it carries this game's config and the snapshot runtime, so the same client serves every game. All the game-specific drawing and input lives in the gamehoster-frontend/ snippets, which the client loads and runs for you.

(function () {
  var local = /^(localhost$|127\.|\[?::1)/.test(location.hostname)
  if (!local) window.GAMEHOSTER_SERVER = 'wss://game.gamehoster.org'
  var httpBase = local ? '' : 'https://game.gamehoster.org'
  function load(src, next) { var s = document.createElement('script'); s.src = src; s.onload = next; document.body.appendChild(s) }
  load(httpBase + '/chess/gamehoster.client.js', function () {
    Gamehoster.start('game', { name: 'P' + Math.floor(Math.random() * 1000) })
  })
})()
How the Play page boots the demo.

gamehoster-frontend/gamehoster-frontend-setup.js

Runs once, on connect. The page handed the client a canvas as the root element, so setup grabs it with Gamehoster.Root() and stashes its 2D context and the Unicode chess glyphs (keyed by colour then kind) in the game's front-end persistent bag with Gamehoster.Persistent.Set, ready for every render frame.

// Runs once, on connect. The page hands us the canvas as the root element; stash its 2D context and the
// Unicode chess glyphs (keyed by colour then kind) in the game's front-end persistent bag, so every
// render frame can reach them without touching the DOM or rebuilding the glyph table.
var canvas = Gamehoster.Root()
Gamehoster.Persistent.Set('ctx', canvas.getContext('2d'))
Gamehoster.Persistent.Set('glyph', {
  white: { king: '♔', queen: '♕', rook: '♖', bishop: '♗', knight: '♘', pawn: '♙' },
  black: { king: '♚', queen: '♛', rook: '♜', bishop: '♝', knight: '♞', pawn: '♟' }
})
chess/gamehoster-frontend/gamehoster-frontend-setup.js

gamehoster-frontend/gamehoster-frontend-render.js

The whole draw, run automatically each frame by the abstraction. It reads the context and glyphs back from the persistent bag and the world from Gamehoster.Instance, Gamehoster.Players and Gamehoster.Entities. The board is 480×480, eight 60px squares a side, so it draws straight in board coordinates with rank 0 at the bottom. Each piece is an entity view of type 'piece'; its file/rank are read from .current so the piece snaps to its square the tick the server confirms a move rather than sliding fractionally between squares. A bottom status bar shows the lobby prompt, whose turn it is, or the result. When the socket drops it overlays the board and reconnects on a click via Gamehoster.Mouse.pressed('left').

// The whole Chess draw, run automatically each frame by the abstraction. The board is 480×480 and the
// canvas matches it — eight 60px squares a side — so we draw straight in board coordinates with rank 0
// at the bottom. Chess is turn-based and every field is server-authoritative: a piece carries discrete
// file/rank ints, so we read them from each view's .current (its newest authoritative sample) rather
// than the eased top-level value. That way a piece SNAPS to its square the tick the server confirms the
// move, instead of sliding fractionally between squares. The selected square is front-end-only state,
// kept in the persistent bag by the mouse-press body.
var ctx = Gamehoster.Persistent.Get('ctx')
var canvas = Gamehoster.Root()
var W = canvas.width, H = canvas.height
var N = 8, SQ = W / N
ctx.clearRect(0, 0, W, H)

// the eight-by-eight board: light/dark squares, rank 0 drawn along the bottom
for (var f = 0; f < N; f++) for (var r = 0; r < N; r++) {
  ctx.fillStyle = (f + r) % 2 ? '#6b7280' : '#c7ccd4'
  ctx.fillRect(f * SQ, (N - 1 - r) * SQ, SQ, SQ)
}

// still connecting, or the socket dropped — overlay the board and offer a click to reconnect
var inst = Gamehoster.Instance
if (!inst || inst.phase == null) {
  ctx.fillStyle = 'rgba(12,14,19,0.82)'; ctx.fillRect(0, 0, W, H)
  ctx.fillStyle = '#e6e6e6'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.font = '20px sans-serif'
  ctx.fillText(Gamehoster.connected ? 'connecting…' : 'connection lost', W / 2, H / 2 - 8)
  if (!Gamehoster.connected) {
    ctx.fillStyle = '#7cc5ff'; ctx.font = '15px sans-serif'
    ctx.fillText('click to reconnect', W / 2, H / 2 + 20)
    if (Gamehoster.Mouse.pressed('left')) Gamehoster.Reconnect()
  }
  return
}

// highlight the square we have a piece selected on (front-end only), while a match is in play
var sel = Gamehoster.Persistent.Get('sel')
if (sel && inst.phase === 'playing') {
  ctx.fillStyle = 'rgba(124,197,255,.55)'
  ctx.fillRect(sel.file * SQ, (N - 1 - sel.rank) * SQ, SQ, SQ)
}

// the pieces — file/rank/colour/kind read authoritative so they snap to their square each move
var GLYPH = Gamehoster.Persistent.Get('glyph')
ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.font = (SQ * 0.8) + 'px serif'
var pieces = Gamehoster.Entities.ofType('piece')
for (var i = 0; i < pieces.length; i++) {
  var p = pieces[i].current
  ctx.fillStyle = p.color === 'white' ? '#fff' : '#111'
  ctx.fillText(GLYPH[p.color][p.kind], p.file * SQ + SQ / 2, (N - 1 - p.rank) * SQ + SQ / 2)
}

// status bar along the bottom edge
var me = Gamehoster.Players.self
ctx.fillStyle = 'rgba(11,12,15,.85)'; ctx.fillRect(0, H - 26, W, 26)
ctx.fillStyle = '#e6e6e6'; ctx.font = '14px sans-serif'; ctx.textAlign = 'left'; ctx.textBaseline = 'alphabetic'
var msg
if (inst.phase === 'lobby') msg = (me && me.ready ? 'Ready — waiting' : 'Click / SPACE to ready up') + ' (' + Gamehoster.Players.size + '/2)'
else if (inst.phase === 'playing') msg = 'You are ' + ((me && me.color) || '—') + ' · ' + inst.turn + ' to move' + (sel ? ' · click a square' : '')
else msg = inst.winner + ' wins — click / SPACE for a new game'
ctx.fillText(msg, 10, H - 8)
chess/gamehoster-frontend/gamehoster-frontend-render.js

gamehoster-frontend/gamehoster-frontend-input-mouse-button-left-press.js

Runs when the left button goes down. Between games it readies you up; during play it is click-to-select then click-to-move. The click's fractional position (Gamehoster.Mouse.fx/fy, 0..1 across the canvas) maps to a file and rank; the first click on a square holding a piece stores the selection in the persistent bag, and the next click sends a move from the selected square to the clicked one. The server checks legality and turn, so an illegal move is simply ignored and the authoritative board stands.

// A left click drives everything. Between games (in the lobby, or on the results screen) it readies us
// up. During play it is click-to-select then click-to-move: the first click on a square holding a piece
// selects it (kept in the front-end persistent bag), the next click sends a `move` from the selected
// square to the clicked one and clears the selection. The board is eight 60px squares, so the click's
// fractional position (fx,fy — 0..1 across the canvas) maps to file 0-7 left→right and rank 0-7
// bottom→top. The server checks the move is legal and that it is our turn; an illegal move is simply
// ignored and the authoritative board stands (there is no client-side prediction here).
var inst = Gamehoster.Instance
if (!inst || inst.phase == null) return                       // connecting / dropped — render offers the reconnect
if (inst.phase !== 'playing') { Gamehoster.Command('ready', {}); Gamehoster.Persistent.Set('sel', null); return }

var m = Gamehoster.Mouse
var file = Math.max(0, Math.min(7, Math.floor(m.fx * 8)))
var rank = Math.max(0, Math.min(7, 7 - Math.floor(m.fy * 8)))
var to = String.fromCharCode(97 + file) + (rank + 1)

var sel = Gamehoster.Persistent.Get('sel')
if (sel) {
  Gamehoster.Command('move', { from: String.fromCharCode(97 + sel.file) + (sel.rank + 1), to: to })
  Gamehoster.Persistent.Set('sel', null)
} else {
  // select only if a piece sits on the clicked square (read authoritative file/rank)
  var pieces = Gamehoster.Entities.ofType('piece'), hit = false
  for (var i = 0; i < pieces.length; i++) { var p = pieces[i].current; if (p.file === file && p.rank === rank) { hit = true; break } }
  if (hit) Gamehoster.Persistent.Set('sel', { file: file, rank: rank })
}
chess/gamehoster-frontend/gamehoster-frontend-input-mouse-button-left-press.js

gamehoster-frontend/gamehoster-frontend-input-keyboard-space-press.js

Runs when Space goes down. It sends the ready command — to start the first match, or to play again after a result. The server's ready handler only acts in the lobby, so it is always safe to send.

// Space readies us up — to start the first match, or to play again after a result. The server's ready
// handler only acts in the lobby, so it is always safe to send during play (it is ignored).
Gamehoster.Command('ready', {})
chess/gamehoster-frontend/gamehoster-frontend-input-keyboard-space-press.js