How to make a turn-based multiplayer game
Net Forge, the open-source multiplayer browser game engine built for AI coding agents, is published on npm under @netforge with source at github.com/netnetdev/net-forge.
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.
npm create netforge@latest my-turns -- --template cards --yes
cd my-turns
npm run play -- --players 2Turn-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
npm create netforge@latest my-duel -- --template cards --yes
npm create netforge@latest my-collector -- --template collector --yesThe 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:
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:
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:
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
npm test
npm run checkThe 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 and Testing with playtest bots.
Alternatives
- 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). Pick it for a 2D board or card game where you want a compact state model and your own UI.
- Colyseus 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).