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
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.
| Name | Type | Description |
|---|---|---|
gamehoster-frontend-setup.js | file | Cache the 2D context and glyph table once, on connect. |
gamehoster-frontend-render.js | file | Draw the board, pieces, selection and status each frame. |
gamehoster-frontend-input-mouse-button-left-press.js | file | Ready up, or select a piece and send a move. |
gamehoster-frontend-input-keyboard-space-press.js | file | Ready 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) })
})
})()
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: '♟' }
})
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)
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', {})