Your first multiplayer game
One small game — Tug — built end to end, so the one rule that governs everything (only the host writes, everyone else asks) is something you do, not something you read.
- 2–4 players
- Shared state + one message
- Ends with a result
This assumes you have a single-player game working — if not, start with Your first game. It also assumes your game is flagged multiplayer, so TipTap.net exists. We'll build Tug: everyone pulls a rope, and the first side to drag it 100 units wins.
Get into a match
Two questions before anything else — is this a multiplayer build, and is the platform around the frame right now? Then join a public match. The promise resolves when you're in the lobby, not when play starts, so wait for
onStart.Availability, then quickMatch, then onStart
// 1. Is this even a multiplayer game, and is a platform here? if (!TipTap.net || !TipTap.net.isAvailable()) { // Opened outside the platform, or not built as multiplayer — play solo / show a notice. return; } // 2. Join a public match. Resolves when you're in the LOBBY, not when it starts. const room = await TipTap.net.quickMatch({ minPlayers: 2, maxPlayers: 4 }); // 3. Wait for the platform to start the match before you deal anything. TipTap.net.onStart(() => startGame());✓ You're in a room with other players. See Multiplayer for the lobby and roster.
Share a board only the host writes
The rope is replicated state. This is the rule that governs the whole game: only the host writes it. So the host — inside
runOnHost— sets the starting value, and every client, host included, renders fromonChange(which also fires with the snapshot you get on joining, so a late joiner is never blank).Host initialises; everyone renders from onChange
// The rope position is shared state. Only the host may write it — so the host, // and only the host, sets the starting value. Everyone renders from onChange. function startGame() { TipTap.net.runOnHost(() => { TipTap.net.state.set("rope", 0); // −100 … +100 }); } // Every client draws from replicated state, including the snapshot it gets on join. TipTap.net.state.onChange((key, value) => { if (key === "rope") drawRope(value); });✓ Every player sees the same rope, including whoever joins late.
Let a non-host make a move
A non-host player can't write state — its tap is a request. It sends a typed message to the host; the host receives it, decides, and writes the result. That round-trip is the architecture. (The message rides a typed schema you declare and get approved — see Typed schemas; until it's approved,
sendTypedreturnsfalse.)Non-host asks; host decides and writes
// A NON-HOST cannot write state. Its move is a request: a typed message to the host. // (Declare a "tug.pull" schema and get it approved — see Typed schemas.) function onTap() { const mySide = TipTap.net.getLocalPeerId(); // who's pulling const sent = TipTap.net.sendTyped("tug.pull", { by: mySide }, { to: "host" }); if (!sent) tellPlayer("Move didn't send"); // false until the schema is approved } // The host receives every pull, decides, and writes the new rope position. TipTap.net.onTyped((schema, payload, from) => { TipTap.net.runOnHost(() => { if (schema !== "tug.pull") return; const rope = TipTap.net.state.get("rope") ?? 0; const dir = payload.by === TipTap.net.peerAtSeat(0).peerId ? -1 : +1; TipTap.net.state.set("rope", rope + dir); // everyone re-renders via onChange }); });✓ Anyone can pull, and the rope only ever changes because the host said so.
End the match
The host decides the game is over and reports it — once, keyed by
peerId(whichpeerAtSeatgives you). The result is the host's claim; the platform records it.Host reports the result, once
// The host — and only the host — decides the match is over and reports it, once. TipTap.net.runOnHost(() => { const rope = TipTap.net.state.get("rope") ?? 0; if (Math.abs(rope) >= 100) { const winnerSeat = rope < 0 ? 0 : 1; TipTap.net.reportResult({ outcome: "win", scores: { [TipTap.net.peerAtSeat(winnerSeat).peerId]: 1 }, // keyed by peerId }); } });✓ That's a complete multiplayer game: match → shared state → requests → result.
Where to go next
- Typed schemas → declare and get your messages approved.
- Advanced → turns, time, host changes, resilience, testing.
- Real-time simulation → for fast-twitch games, without writing netcode.
- Cookbook → complete multiplayer prompts to start from.

