/dev / build
Start here — building a game for b3.games
Read this before you write code. It's the mental model + traps. The contract spec is the canonical reference for multiplayer; this page is the why-and-shape on top.
1 · What you're building
A single self-contained HTML file. One index.html with inline <style> + <script> — no bundler, no imports of your own modules, no asset folder. The file gets uploaded to Supabase storage and served inside an iframe on b3.games.
- 25MB cap on the HTML payload. Plenty of room for embedded base64 sprite sheets, textures, or a few inline GLTF models. Code split is impossible by design — single file, no bundler.
- Canvas-based gameplay is expected.
<canvas id="stage">+requestAnimationFramegame loop is the conventional shape. - External CDNs are allowed for libraries (Three.js via jsdelivr/unpkg). They count against your wall-clock budget but not your 25MB.
2 · How your game runs (the iframe shell)
Your HTML doesn't load at the top frame. The b3.games shell wraps it in an iframe:
b3.games (top frame, Next.js shell)
└── <iframe src="/api/games/<your-slug>/html?room=ABCDE">
└── your HTML lives herePractical implications:
- Read URL params from your iframe's own URL, not the parent. The shell forwards
?room=ABCDEto your iframe sonew URL(location.href).searchParams.get('room')works. - Don't use
postMessageto talk to the parent shell. The shell doesn't listen. Communicate via the multiplayer SDK (which abstracts WS to the backend) or just stay self-contained. - Cookies + localStorage belong to your iframe origin (the API route serving the HTML), not the b3.games top origin. Save player progress via the platform's save APIs, not ad-hoc localStorage you may not see again.
3 · Single-player first, multiplayer as a layer
Even if multiplayer is the goal, build solo first. The platform's upload validator passes any single-player HTML+canvas game; it only enforces the multiplayer contract when you opt in via the <meta name="b3-multiplayer"> tag.
The canonical 3-layer pattern (used by the reference templates):
- Solo / MP toggle (start screen) — "Play solo" vs "Play multiplayer"
- Lobby (only on MP path) — host code, join code, roster, start button
- Gameplay — same canvas + game loop for both paths; MP path also reads from the shared state via the SDK
Build (1) and (3) first as a fully working solo game. Then add (2) by following the contract. This gives you a fallback on every page open: even if multiplayer breaks, solo still works.
4 · Loading patterns (script tag traps)
/mp-runtime.jsmust load as a module:<script type="module" src="/mp-runtime.js"></script>
It uses top-levelexport, so a classic<script src>is a SyntaxError andwindow.B3.ctxnever installs.- Shared browser helpers (
/touch-controls.js,/dictionaries/*.js) load as classic scripts:<script src="/touch-controls.js"></script>
They're IIFEs that attach towindow.B3.<ns>. Don't addtype="module". - Three.js + WebGL games are slow on no-GPU runners. If you're building a 3D game, expect ~20s extra boot time on headless Linux Chromium (CI test runners). Design your UI to show a load spinner; don't assume the game is interactive within 3 seconds.
- No
eval/new Functionanywhere. Cloudflare Workers blocks them at runtime, and the upload validator flags them statically. Same for dynamicimport()of your own code (you don't have a bundler anyway).
5 · Build it MP-ready (even if you skip MP for now)
Pre-MP design choices that make adding multiplayer later trivial vs painful:
- Pure functions for game-state transitions:
tick(state, dtMs, inputs)→ new state, no side effects. The MP runtime calls this on every client; if it's a closure over DOM refs or timers, it won't replay deterministically. - No
Math.random()/Date.now()in game logic. Usectx.random()+ctx.now— the MP runtime injects these as seeded/synced values. The validator hard-blocksMath.random()in tick paths. - Team derivation by index, never by role: in N-team modes, your code is
team = playerIndex % teamCount. Never useconn.getMyRole()for team membership — that's host vs joiner, which can flip on disconnect. (We've been bitten by this twice; chess Real PvP regression in CR.18 was the second.) - All real-time inputs go through
ctx.emit('input', payload). Movement, shoot, pickup — every action rides the input frame channel. No out-of-band channels, no parent-frame postMessage. - Single-flight async patterns: if you have a recurring trigger (setInterval polling, etc.) that fires an async request, guard with
if (pending != null) return. Otherwise a 2nd fire arriving before the 1st reply causes state-capture races. - Handle
?room=CODEon direct page open. Real users click share links; if your game opens cold with that param it should auto-join the room. Reference: seemp-game-template/index.htmlfor the canonical pattern.
6 · Common traps (worth a re-read before submitting)
- Don't assume your iframe loads from the same origin every time. b3.games geo-redirects EU IPs to es.b3.games (Vercel), but US IPs stay on b3.games (Cloudflare). Both serve identical content, but don't hard-code an origin in any of your fetch URLs. Use relative paths or the SDK; never
fetch('https://b3.games/...'). - Validator runs at upload-time only. Static-parse rules check for
Math.random, missing meta tags, missing 5 SDK functions, etc. It does not run your code. Bugs in your game-logic still ship; you'll find them by playing. - The validator's failures come with
contractAnchordeeplinks back to the relevant rule on the contract page. If something fails on upload, click the "Copy AI-fix prompt" button — it bundles the failure + anchor link + your code into a clipboard payload you paste into your AI session. - Console errors fail E2E tests but not real users. Production has heavy console noise (Three.js warnings, WebGL probes, font preloads). The platform's allow-list filter ignores those. Don't ship code with
console.logspam, but don't panic over Three.js warnings.
7 · Quick reference
- /dev/multiplayer/contract — canonical AI-consumption MP spec (5 functions, ctx surface, 6 modes, 3 genre examples)
- /dev/multiplayer — MP dev tools landing (test harness, reference games, upload validator)
- /dev/multiplayer/upload — upload your HTML; runs the contract validator + returns per-rule pass/fail with copyable AI-fix prompts
- /dev/multiplayer/test — React reference harness for the WS protocol; useful when debugging connection issues
/games/mp-game-template/— canonical 3-layer reference (Solo + MP)/games/mp-game-template-turn-duel/— turn-based 1v1 reference/games/mp-game-template-turn-party/— turn-based N-player reference
8 · Suggested workflow (humans + AI)
- Read this page (you're here)
- Decide: solo-only, or MP from day-one, or solo-now-MP-later? If solo-only or solo-now, skip the contract and just build an HTML+canvas game.
- If MP: open the contract; copy its URL into your AI session along with your single-player HTML and ask "rewrite this to follow the b3.games multiplayer contract."
- Test locally if possible (open the HTML directly in your browser). For MP, use the test harness or two browser tabs against a deployed copy.
- Upload via /dev/multiplayer/upload. If the validator flags issues, click "Copy AI-fix prompt" on each failure card and paste back into your AI session. Iterate until it passes.
- Play the deployed game with a friend on a different network. Real-world latency surfaces races your local tabs hide.
Last updated: 2026-05-06. Synthesized from accumulated CR.16-CR.21 lessons + memory entries. If something on this page contradicts the contract, the contract wins.