# How to make a turn-based multiplayer game

> Start from a turn-based template (the card battler or the monster collector) with one command. Turns, validated actions, hidden information and replays come from Net Forge rules genres: deterministic modules the room server runs at 30 Hz behind the same netcode as the action games.

Net Forge, the open-source multiplayer browser game engine built for AI coding agents, is published on npm under `@netforge` with source at https://github.com/netnetdev/net-forge.

Source: https://forge.netnet.capital/docs/guides/turn-based-browser-game  
Last updated: 2026-10-23. Checked against @netforge/engine 0.2.0.

Start with `npm create netforge@latest my-turns -- --template cards --yes` for a one-against-one turn-based duel, or `--template collector` for turn-based battles in a shared world of up to eight players. Both are rules genres: the turn order, the action checks and each player's private view run on the server, and you change the content in `src/game.ts`.

```sh
npm create netforge@latest my-turns -- --template cards --yes
cd my-turns
npm run play -- --players 2
```

Turn-based games still need a server that decides whose turn it is, rejects moves out of turn and keeps secrets secret. Net Forge runs them on the same room server, netcode and playtest bots as its real-time games, so you do not trade one stack for another.

## What you get

| Template | Turn structure | Players | Hidden information |
|---|---|---|---|
| `cards` (card battler) | Alternating turns with a timer, mana per turn, a draft between duels | 2 (or 1 against the AI) | Hands, deck order and draft offers |
| `collector` (monster collector) | Turn-by-turn battles: a move, a swap, a snare or flee; the faster creature acts first | 8 in one marsh | Each player's battle, team and log |
| `tower-defense` | Real-time waves with a shared build phase and a ready vote | 4 | None; one shared board |

All three run as rules modules under `@netforge/engine/rules`, drawn by the shared `@netforge/tabletop` client kit.

## 1. Pick the template that matches your turns

```sh
npm create netforge@latest my-duel -- --template cards --yes
npm create netforge@latest my-collector -- --template collector --yes
```

The card battler is the closest match for a classic two-player turn-based game. The monster collector shows turn-based battles inside a world people walk around in together.

## 2. Change the turn rules in game.ts

In the card battler, the turn timer, the hand, the board and the mana curve are one declaration:

```ts title=src/game.ts
import { cardTable } from "@netforge/engine";

cardTable({
  health: 25,
  startHand: 4,
  handMax: 8,
  boardMax: 6,
  manaMax: 10,
  turnSeconds: 45,
  draftSeconds: 20,
  duels: 3,
  starter: ["blot_imp", "blot_imp", "quill_sprite", "quill_sprite", "ink_frog", "ink_frog", "margin_bat", "ledger_crab", "serif_fox", "splash"],
  ai: [{ health: 14, smart: 0.3 }, { health: 18, smart: 0.6 }, { health: 22, smart: 0.85 }],
});
```

In the monster collector, a battle's moves are declarations too:

```ts title=src/game.ts
import { creature, move } from "@netforge/engine";

move("tide_crash", { element: "tide", power: 70, accuracy: 0.8 });
move("thorn_curl", { element: "root", power: 0, effect: "guard" });
creature("thistlehog", { model: "style.lowpoly.fox", element: "root", hp: 50, atk: 52, def: 48, spd: 46, moves: ["tide_crash", "thorn_curl"], catchRate: 0.7 });
```

## 3. Understand the rules module behind it

You do not write this part to make a game from a template, but it is how the genre stays fair. A rules genre implements `RulesGenre<S>` from `@netforge/engine/rules`: `act` applies one player's action after validating it, `step` advances the world one tick, and `view` returns what one player may see. This sketch shows the two pieces that matter most for a turn-based game, a turn check and a private view:

```ts title=turns-sketch.ts
import type { RulesGenre } from "@netforge/engine/rules";

interface DuelState {
  turn: number;
  hands: Map<number, number[]>;
  board: number[];
}

// act: a move out of turn does nothing; the server never trusts the client.
const act: RulesGenre<DuelState>["act"] = (s, player, input, _avatar, emit) => {
  if (s.turn !== player || input.act === 0) return;
  const hand = s.hands.get(player) ?? [];
  const card = hand[input.a];
  if (card === undefined) return;
  hand.splice(input.a, 1);
  s.board.push(card);
  emit({ name: "card.played", players: [player] });
};

// view: the board is public, a hand is sent only to its owner.
const view: RulesGenre<DuelState>["view"] = (s, player) => ({
  turn: s.turn,
  board: s.board,
  hand: player === null ? [] : (s.hands.get(player) ?? []),
});

export { act, view };
```

State is integers and a seeded random word, with no clocks, so a run replays exactly from its inputs and a bot can play it headless.

## 4. Test turns with the bots

```sh
npm test
npm run check
```

The playtest seats bots, plays whole runs and reports duels or battles won, runs finished and softlocks, with the genre's own checks under `rules.checks`. For the card battler, four playtests that together simulate 17 minutes of duels finish in under half a second; for the monster collector, four keepers playing four minutes take 349 ms (Apple M2 Pro, Node 22).

## How it works

The room runs the rules module at 30 Hz even though the game is turn-based: the tick drives the turn timer, animations of the board and each player's cursor or walking avatar, which the client predicts so pointing and walking never wait. A turn action travels as an input; the server validates it in `act` and applies it, or ignores it. Each player gets the public state plus their own private state, sent only to them when it changes. Because the module is deterministic, the same seed and inputs replay the same run, which is what the playtest and run verification rely on. See [Netcode and prediction](https://forge.netnet.capital/docs/concepts/netcode-prediction.md) and [Testing with playtest bots](https://forge.netnet.capital/docs/concepts/playtest-bots.md).

## Alternatives

- **[boardgame.io](https://github.com/boardgameio/boardgame.io)** is an MIT-licensed engine for turn-based games in JavaScript, with state synced across clients, server and storage, game phases with their own rules and turn orders, generated bots and a lobby. It is view-layer agnostic. Its latest npm release is 0.50.2 from 2022-11-10 ([npm registry](https://registry.npmjs.org/boardgame.io)). Pick it for a 2D board or card game where you want a compact state model and your own UI.
- **[Colyseus](https://docs.colyseus.io/)** is an authoritative Node.js room server with matchmaking and binary delta state sync, with client SDKs for many engines. Pick it when you want to write turn logic as server code against your own client.

Pick Net Forge when you want a finished turn-based game with a 3D client, AI or seat bots and server-side hidden information already working.

## FAQ

### Can I make a turn-based game with Net Forge?

Yes. The card battler and the monster collector are turn-based templates. They run as rules genres on the room server, which validates every action and decides whose turn it is.

### How is hidden information kept hidden?

A rules genre's `view(state, player)` returns only what that player may see, and the room sends each player's private state to that player alone. In the card battler, deck order never leaves the server.

### Does a turn-based game need a 30 Hz server?

The tick drives timers and cursors, not turns. A turn changes only when a validated action arrives or the turn timer ends it. The cost is small: these modules run in well under a millisecond per tick.

### Can I write my own rules genre?

The `RulesGenre<S>` interface is public in `@netforge/engine/rules`. A new genre is template-level work: the ten templates are what `npm run play` runs locally today, so start from the nearest one.

### Is there a chess or board game template?

Not yet. The nearest templates are the card battler (two players, hidden hands) and the tower defense (one shared board).
