TipTap Games
← Multiplayer

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.

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

  2. 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 from onChange (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.

  3. 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, sendTyped returns false.)

    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.

  4. End the match

    The host decides the game is over and reports it — once, keyed by peerId (which peerAtSeat gives 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