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 seq and the turn deadline. A game proposes a move; the board changes only when the next mp_state arrives. 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 gets mp_start when a match begins, or mp_cancel (with fallback_ai when 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 #

  1. 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.

  2. A server entry for it. apps/web/src/server/multiplayer/rules.ts maps the game id to a MatchRules object (parse state and move, whose turn, legality, apply, auto move, outcome, and view, which hides what a seat may not see). A game without an entry cannot go online.

  3. The manifest block. "multiplayer": { "seats": 2, "turnSeconds": 30 }. The portal sets init.features.multiplayer only when the manifest asks for it and the page has the match layer; see the Manifest reference.

  4. 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 #

  1. Show the online buttons only when bridge.multiplayer.available is true; otherwise play the computer.

  2. On "Play a friend" or "Quick match" call bridge.multiplayer.request("invite" | "quick"). An ack means the portal's lobby is open; nothing else changes in the game yet.

  3. mp_start gives the match id, your seat, the opponent's handle and resumed (true after a reload or a second tab). Draw the board from your seat's side.

  4. Every mp_state is the truth: replace your board with state, take seq, and count the turn clock down from deadlineAt - serverTime (never from the device clock). If seq moved by one and lastMove is set, animate that move first.

  5. When it is your turn, send bridge.multiplayer.move(matchId, seq, move) with the seq you last saw. conflict means the board moved on (a timeout, a second tab): wait for the next mp_state. invalid means the move was illegal or malformed.

  6. status: "finished" with result ends the match (normal, resign, timeout, abandoned). Record it, send your usual match_end event; online wins earn no extra XP.

ts
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_start again with resumed: true, then the current mp_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 until resume, and the next mp_state brings 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).

İlerlemeni kaydet

E-postana bir bağlantı gönderelim ya da Google ile devam et. Misafir olarak kazandıkların hesabına taşınır.