| name | boardgame.io |
| description | boardgame.io, bgio, game module, Game object, Client, Server, multiplayer, SocketIO, Local transport, turn, phase, move, event, activePlayers, flow, plugin, Random, secret state, playerView, MCTSBot, Tic-Tac-Toe tutorial. Triggers: "boardgame.io", "bgio", "Game object", "Client", "Server", "multiplayer", "SocketIO", "Local transport", "turn phase", "moves", "events", "activePlayers", "flow", "plugin", "random", "playerView", "MCTSBot" |
boardgame.io — Turn-Based Game Framework
boardgame.io is a framework for building turn-based games in JavaScript/TypeScript. It handles game state management, networking (multiplayer), random number generation, and bot AI — letting you focus on game logic.
ManaMesh uses it as the core game engine. See skill:manamesh-game-modules for how boardgame.io integrates with ManaMesh's crypto layer.
Core Concepts
State: G and ctx
All game state lives in two objects:
{
G: {},
ctx: {
turn: 0,
currentPlayer: '0',
numPlayers: 2,
phase: 'default',
activePlayers: null,
playOrder: ['0', '1'],
playOrderPos: 0,
gameover: undefined,
}
}
G — your domain state. Must be JSON-serializable (no classes/functions).
ctx — framework-managed metadata. Don't mutate directly.
Moves
Moves are pure reducer functions that modify G:
moves: {
drawCard: ({ G, ctx, playerID }) => {
const card = G.deck.pop();
G.hand.push({ card, player: playerID });
},
}
Move arguments after the context are passed from the client:
client.moves.drawCard("queen_of_spades");
Events
Events are framework-provided functions that change ctx (advance turn, end phase, etc.):
client.events.endTurn();
client.events.endTurn({ next: "1" });
client.events.setPhase("play");
client.events.endPhase();
client.events.endGame();
client.events.setActivePlayers({ all: "play" });
Game Object
const game = {
name: "my-game",
setup: ({ ctx }) => ({
deck: [],
hands: {},
}),
moves: {
},
turn: {
},
phases: {
},
events: { endGame: false },
endIf: ({ G, ctx }) => {
},
onEnd: ({ G, ctx }) => {
},
playerView: ({ G, ctx, playerID }) => G,
seed: "random-string",
disableUndo: true,
deltaState: true,
minPlayers: 2,
maxPlayers: 6,
};
Long-form Moves
For control over move behavior:
moves: {
placeBid: {
move: ({ G, ctx, playerID }, amount) => {
G.bids[playerID] = amount;
},
undoable: false,
redact: true,
client: false,
noLimit: true,
ignoreStaleStateID: true,
}
}
Turn Configuration
turn: {
order: TurnOrder.DEFAULT,
onBegin: ({ G, ctx, events }) => {},
onEnd: ({ G, ctx, events }) => {},
onMove: ({ G, ctx, events }) => {},
endIf: ({ G, ctx }) => true | { next: '0' },
minMoves: 1,
maxMoves: 1,
activePlayers: {
all: 'play',
value: { '0': 'bid', '1': 'play' },
},
}
TurnOrder Presets
import { TurnOrder } from 'boardgame.io/core';
turn: {
order: TurnOrder.DEFAULT,
order: TurnOrder.ONCE,
order: TurnOrder.CUSTOM(['1', '3']),
order: TurnOrder.CUSTOM_FROM('G.turnOrder'),
}
Custom Turn Order
turn: {
order: {
first: ({ G, ctx }) => 0,
next: ({ G, ctx }) => (ctx.playOrderPos + 1) % ctx.numPlayers,
playOrder: ({ G, ctx }) => ['0', '1'],
}
}
Phases
Phases override game config for a period of play:
phases: {
setup: {
start: true,
moves: { placeBid },
turn: { maxMoves: 1 },
onBegin: ({ G, ctx }) => { },
onEnd: ({ G, ctx }) => { },
endIf: ({ G }) => G.bidsPlaced,
next: 'play',
},
play: {
moves: { drawCard, playCard },
},
}
Stages
Stages apply to individual players within a turn:
turn: {
activePlayers: {
all: 'play',
value: { '0': 'bid', '1': 'wait' },
},
stages: {
bid: {
moves: { placeBid },
next: 'play',
},
play: {
moves: { drawCard, playCard },
},
},
}
Player moves to next stage via:
client.events.endStage();
client.events.setStage("play");
Active Players
client.events.setActivePlayers({
all: "play",
currentPlayer: "discard",
others: "wait",
value: { 0: "bid", 1: "discard" },
minMoves: 1,
maxMoves: 1,
});
Randomness
Use random in moves — never Math.random():
moves: {
rollDice: ({ G, random }) => {
G.die = random.D6();
G.dice = random.D6(3);
G.rng = random.Number();
G.deck = random.Shuffle(G.deck);
},
}
Set seed for deterministic replay:
const game = { seed: 42 };
Secret State
Use playerView to filter G per player (server-side):
import { PlayerView } from "boardgame.io/core";
const game = {
playerView: ({ G, ctx, playerID }) => {
return {
...G,
opponents: G.opponents.map((o) =>
o.id === playerID ? o : { ...o, hand: [] },
),
};
},
playerView: PlayerView.STRIP_SECRETS,
};
Hide moves that use secret state (run only on server):
moves: {
revealCard: {
move: ({ G, random }) => { G.secret = random.Number(); },
client: false,
},
}
Plugins
Plugins extend game state with private storage and API:
import { PluginPlayer } from "boardgame.io/plugins";
const game = {
plugins: [
PluginPlayer({
setup: (playerID) => ({ score: 0, hand: [] }),
playerView: (players, playerID) => ({
[playerID]: players[playerID],
}),
}),
],
};
Custom plugin shape:
{
name: 'my-plugin',
setup: ({ G, ctx }) => ({ }),
api: ({ G, ctx, data, playerID }) => ({
myMethod: () => data.value,
}),
flush: ({ G, ctx, data, api }) => data,
noClient: ({ G, ctx, data }) => data.isSecret,
isInvalid: ({ G, ctx, data }) => data.invalid ? 'invalid' : false,
playerView: ({ G, ctx, data, playerID }) => filtered,
}
Client (Plain JS)
import { Client } from "boardgame.io/client";
const client = Client({
game: MyGame,
numPlayers: 2,
debug: true,
});
client.start();
client.moves;
client.events;
client.log;
client.matchID;
client.playerID;
client.matchData;
client.getState();
client.subscribe(fn);
client.start();
client.stop();
client.reset();
client.undo();
client.redo();
client.sendChatMessage("hello");
import { Local } from "boardgame.io/multiplayer";
import { SocketIO } from "boardgame.io/multiplayer";
const client = Client({
game: MyGame,
multiplayer: Local(),
matchID: "match-id",
playerID: "0",
});
Client (React)
import { Client } from "boardgame.io/react";
import Board from "./Board";
const App = Client({
game: MyGame,
numPlayers: 2,
board: Board,
loading: LoadingComp,
debug: true,
multiplayer: SocketIO({ server: "http://localhost:8000" }),
});
<App matchID="match-id" playerID="0" />;
Board props received:
function Board({
G,
ctx,
moves,
events,
reset,
undo,
redo,
isActive,
playerID,
matchID,
}) {
return <div>{/* render G and ctx */}</div>;
}
Server
import { Server, Origins } from "boardgame.io/server";
const server = Server({
games: [MyGame],
origins: ["https://yourgame.com", Origins.LOCALHOST_IN_DEVELOPMENT],
db: new DbConnector(),
});
server.run(8000);
Lobby REST API (auto-hosted on same port)
GET /games → list game names
GET /games/:name → list matches
GET /games/:name/:id → match metadata
POST /games/:name/create → create match
POST /games/:name/:id/join → join match
POST /games/:name/:id/leave → leave match
POST /games/:name/:id/update → update player metadata
POST /games/:name/:id/playAgain → restart
Bots (AI)
import { Client } from "boardgame.io/client";
import { Local } from "boardgame.io/multiplayer";
import { MCTSBot } from "boardgame.io/bots";
const client = Client({
game: MyGame,
multiplayer: Local({
bots: {
1: MCTSBot,
},
}),
playerID: "0",
});
In the game definition, provide an ai.enumerate to define valid moves for the bot:
const game = {
ai: {
enumerate: (G, ctx) => {
let moves = [];
for (let i = 0; i < 9; i++) {
if (G.cells[i] === null) {
moves.push({ move: "clickCell", args: [i] });
}
}
return moves;
},
},
};
Bot implementation (src/ai/ai.ts): Step(client, bot) makes one move; Simulate(game, bots, state, depth) plays to gameover.
Key Source Files
| File | Purpose |
|---|
src/core/game.ts | ProcessGameConfig, game object defaults |
src/core/flow.ts | Flow() — turn/phase/stage engine |
src/types.ts | All TypeScript types: Game, State, Ctx, ClientOpts |
src/client/client.ts | _ClientImpl — client implementation |
src/client/transport/transport.ts | Abstract Transport class |
src/server/index.ts | Server() factory |
src/ai/ai.ts | Step, Simulate bot helpers |
src/plugins/ | Built-in plugins (Random, PluginPlayer) |
vendor/boardgame.io/src/ | Forked boardgame.io (ManaMesh's vendored version) |
Constraints / Gotchas
G must be JSON-serializable — no classes, functions, Maps, Sets. Use plain objects and arrays.
- Moves are pure reducers — don't depend on external state or have side effects. Use
events to advance game state.
client: false moves run server-only — client will wait for authoritative update. Essential for secret state.
- Optimistic updates — in multiplayer, client runs moves locally for responsiveness, then reconciles with server. If client computes wrong state (e.g., invalid move), server overrides.
- No
Math.random() in moves — use random.D6(), random.Shuffle(), etc. The PRNG state lives on the server.
deltaState: true — sends JSON Patch diffs instead of full state in multiplayer, reducing bandwidth.
- Undo/redo — enabled by default. Disable with
disableUndo: true or per-move with undoable: false.
playerView runs server-side in multiplayer — filters G before sending to each client. Not applied in single-player.
- No
crytography.md docs — boardgame.io has no built-in crypto docs. For secret state use playerView + client: false moves. ManaMesh adds SRA encryption on top via its own plugin.
- bots.md is missing from docs — bot authoring uses
ai.enumerate in game definition + MCTSBot from boardgame.io/bots.
ManaMesh Usage
ManaMesh uses a forked boardgame.io at vendor/boardgame.io/. The fork is at vendor/boardgame.io/ as a git submodule. ManaMesh's customizations:
skill:manamesh-game-modules — all games wrap boardgame.io with crypto plugins
skill:manamesh-crypto — SRA mental poker and other crypto primitives layered on top via custom plugin hooks (fnWrap, noClient)
skill:manamesh-p2p — P2P networking replaces boardgame.io's default SocketIO transport
The getBoardgameIOGame() function in each game module converts the crypto game definition into a plain boardgame.io Game object.
See Also
skill:manamesh-game-modules — how ManaMesh wraps boardgame.io for crypto games
skill:manamesh-crypto — ManaMesh's cryptographic layer (SRA, Shamir, EC ElGamal)
skill:manamesh-p2p — P2P networking replacing SocketIO transport
- https://boardgame.io (official docs)
- https://github.com/boardgameio/boardgame.io (commit 4f3c90d, ManaMesh's vendored version)