A game ships two source files:
| Path | Holds |
|---|---|
/src/game.ts |
Required. gameConfig plus the game-logic hooks. The server runs these authoritatively. |
/src/frontend.tsx |
Required. The React client. Its single default export is a React component that renders the whole board. |
/src/game.ts holds the rules: pieces, spaces, applyActions, getAvailableActions, scoring, turn passing. /src/frontend.tsx holds everything the player sees. Both files exist from the moment the game is created; the starting template includes them.
The split is strict, and it is what keeps a game honest: the server is the sole authority over shared state. Your frontend never re-implements rules. It reads state and sends clicks; the server decides what those clicks mean, by running the same /src/game.ts you wrote.
import { useMatch, useGameState, useAvailableActions } from "boardweaver/react";
export default function App() {
const match = useMatch();
const game = useGameState();
const available = useAvailableActions();
return (
<div>
{game.spaces("plot").map((space) => (
<button
key={space.spaceId}
onClick={() => match.click({ type: "Click", spaceId: space.spaceId })}
>
{space.spaceId}
</button>
))}
</div>
);
}
Hard requirements:
react-dom, do not call createRoot.react for rendering primitives you don't need. react is available for hooks and types, but React itself is provided by the platform and version-locked, your bundle must not ship its own copy.Your frontend runs in a sandboxed frame with no network access, on an opaque origin, under a strict Content Security Policy. These are compile errors, and they would fail at runtime even if they weren't:
fetch, XMLHttpRequest, WebSocket, EventSource, the frame cannot talk to any server. All data arrives through the hooks.localStorage, sessionStorage, indexedDB, document.cookie, an opaque origin has no persistent storage.window, document, direct DOM access: use React. For frame size use useViewport(), which is authoritative; there is no window worth measuring.console and timers (setTimeout, setInterval, requestAnimationFrame) work normally.
This is not a restriction to work around: it is the containment boundary that lets untrusted game code run safely in a player's browser. If you find yourself needing one of these, the answer is almost always that the data belongs in game state, where the server owns it.
useImage(GameImage.Something) resolves a game image to a URL for <img src>. Image keys come from the same per-game GameImage enum exported by "boardweaver" that /src/game.ts uses.useGameTheme() returns the game's validated /theme.json document; pair it with useColorMode() to pick the light or dark side of each color pair.useViewport() gives the frame's current size in CSS pixels and re-renders on change.Only these imports resolve. Bare imports of anything else fail the build.
| Module | What it is |
|---|---|
boardweaver |
The same runtime the backend uses: GameImage, GameFont, defs, types. |
boardweaver/react |
The match API: every hook in this doc. See react-match-api. |
boardweaver/immer |
Version-locked Immer (produce, produceWithPatches, applyPatches, Patch) for client state. See react-client-state. |
boardweaver/dnd |
Drag and drop primitives. |
react, react/jsx-runtime |
Hooks and types. Provided by the platform. |
/src/** |
Your own tree, by relative or absolute path. |
The client bundle is /src/frontend.tsx and all of /src/game.ts, built together into one file that ships to every player on every match load. validate_code rejects it over 1024 KB, measured after minification.
game.ts is in there because the client runs your rules locally to predict the result of a click before the server answers. It is re-exported wholesale, so nothing in it is dropped for going unused by the UI: a large rules engine spends the budget whether or not your frontend touches it. There is currently no way to keep the rules server-only.
The bundle is minified, and comments never survive the build, so neither is worth trimming for size. What counts is executable code. Two things do not count at all:
react, boardweaver/react, boardweaver/dnd, boardweaver/immer, boardweaver/ui and boardweaver/motion are runtime externals resolved to the frame, not copies in your bundle.react-match-api: every hook, the click lifecycle, and what re-renders when.react-client-state: selection, tentative moves, and undo.build-react-game-instructions: the end-to-end build workflow.v2-game-source: what /src/game.ts must export, and the config object.v2-hooks: every hook's contract, and the action union.v2-entities: PieceDef / SpaceDef and the three state buckets.v2-game-state: the GameState read/write API, shared with useGameState().