Online matches
How a turn-based two-player game plays online on GamePortal: the server referees, the game draws. The messages and their fields are in the SDK reference; this page explains how the pieces fit, using Mangala (games/mangala) as the worked example.
Not
Status. Online play is open only to games the GamePortal team writes (ADR-008): the server has to run the game's rules, so a rules module is reviewed and added to the portal before a game can go online. External games keep everything else in these docs; their mp_* messages are refused with invalid.
The model #
The server is the referee and the only clock. It holds the match state, whose turn it is, the move counter
seqand the turn deadline. A game proposes a move; the board changes only when the nextmp_statearrives. The game never decides who won.Turn-based, two seats. Seat 0 moves first. Each turn has
turnSeconds(manifest, 10 to 120; Mangala uses 30). When a turn runs out the server plays a deterministic move for that seat (lastMove.auto: true); three timeouts in a row forfeit the match.The portal owns the lobby. The game only asks for a match (
mp_request); the invite link, the quick-match search, the "play the computer instead" button and joining an invite are portal screens. The game getsmp_startwhen a match begins, ormp_cancel(withfallback_aiwhen the player chose the computer).Privacy. The game sees seats, an opaque per-match seat id and the leaderboard handle (8 characters) of each player, never a user id, an e-mail, a session or the invite code. There is no chat: only the preset reactions of
MP_EMOTES, at most one per 3 seconds and 20 per match.
What a game needs #
A pure rules module. No DOM, no clock, no randomness: the initial state, legal moves, applying a move, the end and the winner, and an automatic move for timeouts. Mangala's is
games/mangala/src/rules.ts; the same file runs in the game and on the server.A server entry for it.
apps/web/src/server/multiplayer/rules.tsmaps the game id to aMatchRulesobject (parse state and move, whose turn, legality, apply, auto move, outcome, andview, which hides what a seat may not see). A game without an entry cannot go online.The manifest block.
"multiplayer": { "seats": 2, "turnSeconds": 30 }. The portal setsinit.features.multiplayeronly when the manifest asks for it and the page has the match layer; see the Manifest reference.A computer opponent. Online is an extra, never the only way to play: standalone, in an old portal, or when nobody is found, the game plays its own bots.
The flow in the game #
Show the online buttons only when
bridge.multiplayer.availableis true; otherwise play the computer.On "Play a friend" or "Quick match" call
bridge.multiplayer.request("invite" | "quick"). Anackmeans the portal's lobby is open; nothing else changes in the game yet.mp_startgives the match id, your seat, the opponent's handle andresumed(true after a reload or a second tab). Draw the board from your seat's side.Every
mp_stateis the truth: replace your board withstate, takeseq, and count the turn clock down fromdeadlineAt - serverTime(never from the device clock). Ifseqmoved by one andlastMoveis set, animate that move first.When it is your turn, send
bridge.multiplayer.move(matchId, seq, move)with theseqyou last saw.conflictmeans the board moved on (a timeout, a second tab): wait for the nextmp_state.invalidmeans the move was illegal or malformed.status: "finished"withresultends the match (normal,resign,timeout,abandoned). Record it, send your usualmatch_endevent; online wins earn no extra XP.
declare function drawBoard(state: unknown, animate: boolean): void;
let matchId = "";
let seq = -1;
bridge.on("mp_start", (start) => {
matchId = start.matchId;
seq = -1;
});
bridge.on("mp_state", (update) => {
if (update.matchId !== matchId || update.seq < seq) return; // another match, or out of order
const stepped = update.seq === seq + 1 && update.lastMove !== null;
seq = update.seq;
drawBoard(update.state, stepped);
});
async function playMove(move: unknown): Promise<void> {
const ack = await bridge.multiplayer.move(matchId, seq, move);
if (ack && !ack.ok) console.info("the next mp_state brings the board up to date:", ack.error);
}
void playMove;Reconnects, pauses and Esc / P #
Reload or a dropped connection. The portal looks for the player's running match on every handshake and sends
mp_startagain withresumed: true, then the currentmp_state. Draw it without replaying anything.Pause. A portal pause (
pause) freezes drawing and sound, but an online match goes on: the server's clock keeps running. Mangala pauses only computer matches; online it just stops drawing untilresume, and the nextmp_statebrings the board up to date.Esc and P. While embedded, Esc and P belong to the portal (the bridge sends
pauseRequest). Do not bind your own menus or dialogs to them in portal mode; close them with buttons.
Testing #
Rules. Unit-test the rules module exhaustively (Mangala:
games/mangala/test/rules.test.ts), including the automatic move and the end of the game.Server. The match contract test runs every rule of the referee against the mock backend and, with
TEST_DATABASE_URL, against Postgres (apps/web/src/server/multiplayer/match.contract.test.ts).End to end. Two browser contexts are two guests: one opens the invite, the other joins it from the link, both play moves through the server, one reloads, one resigns (
e2e/tests/mangala.spec.ts).