Frontend
Everything so far has been the backend: the schemas and scripts the server
runs. The front end is the browser half, and for a game it is small. It builds on the game's
baked client (gamehoster.client.js, generated from the definition and served
with the game), which gives you Gamehoster.connect, command sending, and a frame to read each
time you draw. On top of that sits one hand-written file, render.js, that wires input to
commands and paints the board.
Loading the client
The Play page loads the game's baked client from the game server (so it
carries this game's config and command handlers), then the renderer, then starts it on a
<canvas> with a random display name.
(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 () {
load('/src/games/chess/render.js', function () {
GamehosterGames.chess(document.getElementById('game'), { name: 'P' + Math.floor(Math.random() * 1000) })
})
})
})()
render.js
The whole renderer. It connects with your name, turns clicks into commands (click or
Space in the lobby sends ready; on your turn, a click picks a piece and a second
click sends move from-square to-square), and every animation frame it reads
conn.frame() and draws the board, the pieces and a status line straight from it. Chess is
turn-based and every field is server-authoritative, so the renderer draws exactly the confirmed state the
server sends: each move appears once the server has accepted it and returned the next frame, which keeps
both boards identical and rules out an illegal move ever showing locally.
// Chess renderer — draws the board and pieces from the frame, ready-up in the lobby,
// and click-from / click-to to send a move. Registered as GamehosterGames.chess.
(function () {
'use strict'
const GLYPH = { white: { king: '♔', queen: '♕', rook: '♖', bishop: '♗', knight: '♘', pawn: '♙' }, black: { king: '♚', queen: '♛', rook: '♜', bishop: '♝', knight: '♞', pawn: '♟' } }
window.GamehosterGames = window.GamehosterGames || {}
window.GamehosterGames.chess = function (canvas, opts) {
opts = opts || {}
const conn = Gamehoster.connect('chess', { name: opts.name || 'Player' })
const ctx = canvas.getContext('2d')
const N = 8, SQ = canvas.width / N
let sel = null // selected {file, rank}
const sq = (f, r) => String.fromCharCode(97 + f) + (r + 1)
canvas.addEventListener('click', (e) => {
const v = conn.frame(); if (!v) return
const r = canvas.getBoundingClientRect()
const cx = (e.clientX - r.left) * (canvas.width / r.width)
const cy = (e.clientY - r.top) * (canvas.height / r.height)
const file = Math.floor(cx / SQ), rank = N - 1 - Math.floor(cy / SQ) // rank 0 at bottom
if (v.instance.phase !== 'playing') { conn.send('ready'); return }
if (sel) { conn.send('move', { from: sq(sel.file, sel.rank), to: sq(file, rank) }); sel = null }
else { const p = v.entities.find(e2 => e2.type === 'piece' && e2.file === file && e2.rank === rank); if (p) sel = { file, rank } }
})
window.addEventListener('keydown', (e) => { if (e.key === ' ') { const v = conn.frame(); if (v && v.instance.phase !== 'playing') conn.send('ready'); e.preventDefault() } })
function draw() {
const v = conn.frame(), W = canvas.width, H = canvas.height
ctx.clearRect(0, 0, W, H)
for (let f = 0; f < N; f++) for (let r = 0; r < N; r++) {
ctx.fillStyle = (f + r) % 2 ? '#6b7280' : '#c7ccd4'
ctx.fillRect(f * SQ, (N - 1 - r) * SQ, SQ, SQ)
}
if (v) {
if (sel) { ctx.fillStyle = 'rgba(124,197,255,.55)'; ctx.fillRect(sel.file * SQ, (N - 1 - sel.rank) * SQ, SQ, SQ) }
ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.font = (SQ * 0.8) + 'px serif'
for (const p of v.entities) {
if (p.type !== 'piece') continue
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
const me = v && v.players.find(p => p.id === conn.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'
let msg = 'connecting…'
if (v) {
const s = v.instance
if (s.phase === 'lobby') msg = (me && me.ready ? 'Ready — waiting' : 'Click / SPACE to ready up') + ' (' + v.players.length + '/2)'
else if (s.phase === 'playing') msg = 'You are ' + (me && me.color || '—') + ' · ' + s.turn + ' to move' + (sel ? ' · click a square' : '')
else msg = s.winner + ' wins — click / SPACE for a new game'
}
ctx.fillText(msg, 10, H - 8)
requestAnimationFrame(draw)
}
requestAnimationFrame(draw)
return { close() { conn.close() } }
}
})()