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. |
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().