Skip to main content
This tutorial rebuilds the examples/games/tic-tac-toe example end to end. You will finish with three packages in a Bun workspace:
  • a game package that exports the authored tic-tac-toe definition,
  • an app browser package that renders it with React,
  • and an optional Bun cli package for terminal play.
We split into three packages so the same game can be imported by both the React UI and the CLI without dragging React or DOM types into the CLI build. The game package is worker-safe, meaning it does not import any Node, Bun, browser, or DOM APIs. That same package will also drop into a hosted multiplayer worker later (see tutorial: tic-tac-toe multiplayer) without any change. In short: game/src/index.ts exports game and match. The React app binds them with createOpenturnBindings. The CLI uses createLocalSession directly. Reference code: examples/games/tic-tac-toe. If you would rather just edit a working copy, that’s the source to copy.
This tutorial assumes a Bun workspace at the repo root with a package.json containing "workspaces": ["packages/*", "tic-tac-toe/*"] (or similar). If you are starting from a fresh checkout of the openturn repo, the workspace is already set up; otherwise add a top-level package.json with "workspaces" before running bun install.

1. Create the workspace packages

game/package.json

game/src/index.ts

Notice: the game has one move, one phase (the default "play"), a round-robin turn policy, and two views. The three helpers (placeMark, getWinner, isBoardFull) are pure TypeScript — nothing openturn-specific. Drop them into the same file:

2. Render it with React

The browser package lives in a folder called app/. Inside it, the openturn entry files (game.ts, page.tsx, openturn.ts) live in an inner app/ folder — that is the convention the CLI and deploy pipeline look for. So a fully qualified path looks like app/app/page.tsx (browser package app, openturn-app folder app, file page.tsx). Your own React components can live anywhere; this tutorial puts them in app/src/.

app/package.json (excerpt)

app/app/game.ts

app/app/openturn.ts

app/app/page.tsx

app/src/components/TicTacToeExperience.tsx

createOpenturnBindings(ticTacToe, { runtime: "local", match }) builds typed bindings, declares the deployment shape (in-process), and seeds the initial match. Capacity (playerIDs, minPlayers) lives on the game itself; the per-session match carries just the seated subset. useMatch() returns a mode-discriminated view; in local mode, view.state exposes snapshot, dispatch, reset, and a per-player getPlayerView helper. The dispatch map exposes every move by name: dispatch.placeMark(playerID, args) sends the event through the reducer.

3. Run it

From the root of the monorepo:
Open http://localhost:3000. Click cells. Fill a row. The game ends and result.winner pops into the snapshot.

4. Add a Bun CLI (optional)

cli/src/index.ts

createLocalSession is the same reducer React uses, just without the store wrapper. applyEvent is the low-level dispatch primitive. Run it:

What you learned

  • A game definition is a pure value you can share across a worker package, a React app, and a Bun CLI.
  • Gamekit moves compile to core transitions; move.invalid, move.endTurn, and move.finish are the outcomes you will use most.
  • createOpenturnBindings gives you typed React bindings with no boilerplate.
  • The CLI uses the raw createLocalSession because it does not need React.

What to do next