KUMODeck
日本語

Multiplayer rooms

For games. Rooms are real-time sessions over one WebSocket: quick match, private rooms with a 6-letter code, a public lobby, message relay, shared room state, per-player state, host migration and reconnection. No server code to write.

Join a room#

const room = await kumo.rooms.quickMatch('duel');          // resolves when a room is ready
// or
const room = await kumo.rooms.create('duel', { private: true });
showCode(room.code);                                       // e.g. "K7QM3X"
// the other player:
const room = await kumo.rooms.join('k7qm3x');              // room id or code, case-insensitive
MethodNotes
rooms.quickMatch(mode)joins the fullest open public room of that mode, or waits in a queue until minPlayers are ready (or fillTimeoutSeconds pass)
rooms.cancelMatch()stop waiting (the pending quickMatch rejects with match_cancelled)
rooms.create(mode, opts)private, maxPlayers, metadata (≤ 2 KB, shown in the lobby), custom code, hostOnlyState
rooms.join(idOrCode)room_not_found, room_full, room_locked
rooms.list(mode?)open public rooms for a lobby, most players first

Modes come from config. A project without modes gets a built-in default mode (2–8 players) so two browser tabs can meet before you write any config:

{ "multiplayer": { "modes": [
  { "key": "duel", "minPlayers": 2, "maxPlayers": 2, "fillTimeoutSeconds": 20 },
  { "key": "party", "minPlayers": 1, "maxPlayers": 8, "fillTimeoutSeconds": 0 }
] } }

Talk#

room.send('move', { x, y });                     // to everyone else (fire-and-forget, ordered)
room.send('hit', { dmg: 3 }, { to: [targetId] }); // to specific players
room.on('message', ({ from, type, data }) => { … });

await room.setState({ round: 2 });               // shared state: everyone (you too) gets 'stateChanged'
await room.setMyState({ ready: true, skin: 'red' }); // your player state: 'playerStateChanged'
room.state;          // current shared state
room.players;        // [{ id, displayName, connected, joinedAt, state }]

State patches are applied in the same order on every client (a null value deletes a key), so all clients agree. Use send for high-frequency data (positions), state for things late joiners must see (scores, seats, settings).

Host#

room.hostId / room.isHost: the earliest-joined connected player. If the host leaves or drops, the next player becomes host immediately (hostChanged), and a returning player never takes it back. Use the host for authority: simulate on the host and broadcast snapshots. With hostOnlyState: true, only the host can setState. room.lock() (host only) stops new players from joining a match in progress.

Events#

EventPayload
message{ from, type, data }
playerJoined / playerLeftplayer / { playerId, reason: 'left' | 'timeout' }
playerDisconnected / playerReconnected{ playerId } — their seat is kept while they reconnect
stateChanged{ patch, state, by, version }
playerStateChanged{ playerId, patch, state }
hostChanged{ hostId, previousHostId }
reconnecting / resumedyour own connection dropped / came back (state refreshed from a snapshot)
closed{ reason } — left, seat_expired, connection_lost, replaced, shutdown, unauthorized

kumo.rooms.on('connection', state => …) reports connecting, open, reconnecting, closed.

Reconnects and page reloads#

The SDK reconnects automatically with backoff; the server keeps your seat for 20 seconds. After a page reload the Room object is gone but the seat is not:

const held = await kumo.rooms.fetchHeldRoom();   // { roomId, mode, code, expiresInMs } or null
if (held) room = await kumo.rooms.rejoin();       // back in, state restored from a snapshot

Or just start something new — create, join or quickMatch release the old seat as a normal leave.

Peer-to-peer mode#

By default every message goes through KUMODeck's servers. A mode with "transport": "p2p" instead sends game messages directly between players over WebRTC. KUMODeck still does the tedious parts: matchmaking, room codes, joining and leaving, choosing the host, relaying the WebRTC handshake, and handing out short-lived TURN relay credentials. Your code does not change:

{ "key": "coop", "minPlayers": 2, "maxPlayers": 4, "transport": "p2p", "p2p": { "maxPlayers": 4, "relay": "fallback" } }
const room = await kumo.rooms.quickMatch('coop');
room.transport;                                   // 'p2p'
room.send('pos', { x, y }, { reliable: false });  // unordered, no retransmit: good for positions
room.on('peer', ({ playerId, state, relayed }) => { … }); // connecting | connected | failed | closed
room.peers;                                       // [{ playerId, state, relayed }]

The SDK connects everyone to everyone (a mesh), uses the TURN relay as p2p.relay says (below), and keeps shared state working when the host leaves: the server picks the next host (same rule as above), which continues from its copy of the state, and unconfirmed setState calls are resent to it.

Warning — no referee. In peer-to-peer rooms the host's browser decides the shared state, and a modified client can send anything. Use the default server transport for competitive, prize or paid matches.

Warning — IP addresses. A direct WebRTC connection shows each player's IP address (roughly where they live) to the others. Choose with p2p.relay:

p2p.relayWhat happensGives up
alwaysevery message goes through the TURN relay; only the relay's address is visible; connects wherever the relay is reachablerelay traffic is billed by the amount sent
fallbackdirect first; the relay only when a direct connection failsplayers see each other's IP address when direct
neverdirect only; never any relay costplayers who cannot reach each other directly fail to connect (peer event state: 'failed')

Which to pick, and the price per match of each way, is in Play online together: the multiplayer Skill asks the creator once and picks it.

Players2–8 per mesh (p2p.maxPlayers, default 4 — every player uploads to every other player). A full mesh refuses joins with p2p_room_full (details.max)
Costrelay traffic is billed at cost as turn_egress_bytes (no markup); signaling is tiny
Errorswebrtc_unavailable (no WebRTC in this environment), turn_unavailable (a relay is required but not available), setState rejects with timeout if the host cannot be reached
Statethe same limits as server rooms (64 KB shared, 8 KB per player); it disappears when everyone leaves

To try it, open the game in two different browsers (or one normal and one private window), quick match the same p2p mode, and check room.peers shows connected in both.

Limits#

LimitValue
Messages30/s per connection (burst 60); excess is dropped with a warning event
Message size16 KB (payload_too_large)
Shared state64 KB total; player state 8 KB each
Players per roomup to 64 (per mode)
Idle socketclosed after 10 minutes outside any room (reconnects on the next call)

One browser profile = one player: a second tab replaces the first tab's connection. To test multiplayer on one machine, give each tab its own storage (the templates use ?player=2).

Note Rooms relay and synchronise; they do not run your game logic on the server. Competitive games should be host-authoritative (see examples/sky-duel), or run the match logic in your own Functions (Durable Objects give you stateful WebSocket rooms).