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) })
    })
  })
})()
examples/chess/ — booting the demo

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() } }
  }
})()
src/games/chess/render.js