Multiplayer rooms
A room seats a handful of players in one match and relays what they say to each other. It is built for the turn-based kind of game (cards, board games, quizzes, artillery), not for a shared physics world.
What the arcade does, and what it does not
| The arcade | Your game |
|---|---|
| Seats players, keeps the presence list honest | The rules |
| Hands out a code and matches strangers | Whose turn it is |
| Relays messages, in order, to everyone in the room | What a message means |
| Notices a player who went away | What happens to their turn |
The room relays; it referees only what it can check without knowing your rules. Game state lives in the players' clients, so the arcade cannot tell a legal move from an illegal one. What it can do is refuse a move from the wrong seat, hold the deck so nobody sees another player's hand, and record a result only when every player reports the same one. Those three are opt-in, and they are covered further down.
What is still true even with all three on: a client can lie about anything the server does not hold. Do not settle coins on the outcome of a match.
There is no socket server. The host page polls once a second and pushes what it reads into your frame, so a message reaches the other players in roughly 0.4-1.2 seconds. That is invisible in a turn-based game and useless for a platformer.
Getting into a room
// Play with strangers: joins the oldest room with a free seat, or opens one.
const room = await taploka.room.quick({ max: 4 });
// Play with a friend: one creates, the other types the code.
const mine = await taploka.room.create({ max: 2, private: true });
showCode(mine.code); // "KX7Q"
const theirs = await taploka.room.join('KX7Q');
Every one of them resolves to the room state:
{
"id": "01M2…",
"code": "KX7Q",
"status": "open",
"seq": 12,
"host": "01M1…",
"max_players": 4,
"members": [
{ "id": "01M1…", "name": "Dika", "seat": 0, "is_host": true },
{ "id": "01M2…", "name": "Salsa", "seat": 1, "is_host": false }
]
}
seat is stable while a player is in the room and is what a game should use for turn order. The array order is not
a promise. id is the player's public id, the same one ready() gives you for the local player.
A player is in one room per game at a time: joining a second one leaves the first.
Joining from the game's page
Every open public room shows up on the game's page in the arcade, and tapping one opens the stage with that room
already picked. The code arrives in ready(); joining stays the game's own call:
const { room } = await taploka.ready();
await (room ? taploka.room.join(room) : taploka.room.quick({ max: 4 }));
room is null on a normal launch. A room is only listed while it is public and open, has somebody still in it and
has a free seat, so the code can be stale by the time the player taps it. Fall back to quick() on ROOM_NOT_FOUND,
ROOM_FULL and ROOM_STARTED. A private room is never listed: it is reachable by code only.
Playing
taploka.room.on('members', (state) => renderLobby(state));
taploka.room.on('started', (event) => beginMatch(event.payload.seats));
taploka.room.on('message', (event) => {
if (event.mine) return; // your own move, echoed back
applyMove(event.from, event.payload);
});
taploka.room.on('closed', () => showLobbyAgain());
await taploka.room.start(); // host only, needs two players
await taploka.room.send({ card: 'red-7' });
on() returns a function that removes the listener again.
Chat
chat() is the same relay with a text line instead of a payload, for the chatroom next to the board:
taploka.room.on('chat', (event) => addChatLine(event.payload.name, event.payload.text, event.mine));
await taploka.room.chat('gg wp'); // 200 characters, players only
The speaker's display name travels with the line, so a line still renders after they have left. The text is whatever
the player typed: escape it when you render, and do not put game state in it, send() is for that.
| Event | When | Payload |
|---|---|---|
message |
Another player called send() |
{ from, mine, payload, seq, at } |
chat |
Another player called chat() |
{ from, mine, payload: { text, name }, seq, at } |
move |
A refereed move under the turn rules | { seat, …your payload } |
turn |
The turn moved on | { seat, reason, deadline, seconds } |
turn.timeout |
A seat ran out of time | { seat } |
deck.created / deck.dealt |
The dealer set up or dealt | { deck, remaining }, { deck, hands: [{ seat, count }] } |
deck.drew / deck.played / deck.revealed |
A card moved | counts for the first, items for the other two |
result / result.disputed |
The match ended, agreed or not | { outcome, agreed } |
members |
The seat list changed | The room state |
member.joined / member.left |
Someone arrived or went | { user: { id, name }, seat } |
host.changed |
The host left and the room picked a new one | { user: { id, name } } |
started |
The host started the match | { seats: [{ id, seat }] } |
closed |
The last player left, or the room went idle | none |
Messages arrive in the order the server accepted them, and each carries the sequence number it was given. A player whose connection stutters catches up from the sequence they hold rather than missing anything.
Presence, dropping out, coming back
Presence is a poll: the host page keeps a seat by reading the room every second. A player who stops for 20 seconds
(closed tab, dead battery, tunnel) is marked gone, the others get member.left, and the seat frees up. If the host
is the one who went, the room picks a new host and tells everybody.
Coming back within that window is silent: the same seat, no second member.joined. Your game should expect a player
to reappear rather than treating every member.left as final.
An empty room stays open and keeps its code and its host: a player who reloads the page is gone for a second or two, and closing on the way out would hand them a different room every refresh. What ends a room is silence. Ten minutes without a message and the arcade closes it, empty or not, and the code stops working.
Limits
| Limit | Value |
|---|---|
| Players per room | 2-8, your max |
| Message size | 2 KB of JSON |
| Requests | 300 per minute per player (the poll is 60 of them) |
| Log kept | The last 200 events in a room |
| Presence timeout | 20 seconds |
| Idle room | Closed after 10 minutes of silence |
Send moves, not state: { card: 'red-7' }, not the whole table. If your payload is bumping against 2 KB, the room is
being used as a database.
Turns, when a game has them
Start the match with a clock and the server becomes the referee for turn order:
await taploka.room.start({ turnSeconds: 30 });
await taploka.room.move({ tile: 4 }); // rejected with NOT_YOUR_TURN if it is not
await taploka.room.pass(); // give the turn up early
move() is send() under the rules: refused unless it is your seat's turn, relayed as a move event, and the turn
passes on afterwards. send() stays unrefereed on purpose, for chat, "ready", a cursor position, anything that is not a
move.
A seat that runs out of time is passed over: the next poll that notices the deadline emits turn.timeout and then
turn. There is no ticking process, so the pass happens when somebody looks, within a second of the deadline. The
room state carries the clock:
{ "turn": { "seat": 1, "deadline": "2026-09-11T04:20:31+00:00", "seconds": 30 } }
A player who leaves mid-turn does not stall the game. Their seat drops out of the order and the turn lands on
whoever now sits after them. Start without turnSeconds and none of this applies; everyone may act at once.
The dealer, for hidden information
A deck is a list of strings the server holds, shuffles, and deals from. No client ever sees it, not even the host. That is what makes a card game possible in a room nobody referees.
await taploka.room.deck.create('deck', ['red-1', 'red-2', /* … */]); // host only, once
await taploka.room.deck.deal('deck', 7); // host: 7 to each seat
await taploka.room.deck.deal('deck', 1, { seat: 2 }); // or to one seat
const { cards, counts } = await taploka.room.deck.hand('deck');
// cards: ['red-1', 'blue-3', …] your hand, and only ever yours
// counts: [{ seat: 0, count: 7 }, { seat: 1, count: 7 }]
await taploka.room.deck.draw('deck', 1); // your turn: take from the top
await taploka.room.deck.play('deck', ['red-7']); // refused unless you hold it
await taploka.room.deck.reveal('deck', 1); // face up for everyone
The room is told counts, never cards: deck.dealt carries how many went to which seat, deck.drew carries a
number, and only deck.played and deck.revealed name the cards, because those are face up by then.
| Rule | Why |
|---|---|
| The host creates the deck, once per key | A second create would be a reshuffle mid-hand |
| Only the host deals | Dealing is not a move a player takes |
play() checks you hold the card |
The difference between playing a card and claiming you did |
| Playing two of a kind needs two of them | Cards are removed one at a time, not by value |
draw, play need your turn when turns are on |
Otherwise a fast client empties the deck |
Limits: 300 items per deck, 48 characters per item, 30 cards per deal or draw.
Ending a match everybody agrees on
const verdict = await taploka.room.result({ winner: 1, score: 42 });
// { status: 'pending' | 'agreed' | 'disputed', agreed, voters }
Every player still in the room sends their version. While somebody has not, the status is pending. When the last
one arrives:
- They match: the arcade records the outcome, emits
result, and closes the room. - They do not: nothing is recorded. The arcade emits
result.disputed, closes the room, and keeps no result at all. A result one client invented is worse than no result.
Key order does not matter; { winner: 1, score: 42 } and { score: 42, winner: 1 } are the same report.
Nothing is paid out either way. The record exists so a game can show a history and so the arcade has something to build on later. It is not a settlement.
Guests play too
A player who has not signed in gets a seat like anyone else. The arcade is guest-first, and a room needs a seat, not
an email. name is their display name, which for a guest is "Player 4F2A". Do not gate a lobby behind signing in.
What is still missing
- A referee that knows your rules. The server enforces whose turn it is and what cards you hold. It cannot tell whether playing that card was legal, because it does not know the game. A modified client can still play a red 7 on a blue 3.
- Coin stakes. There is no escrow and no payout. An agreed result is a record, not a settlement, and a match is not evidence anyone should be paid on.
- Voice and video. The frame has no microphone or camera access.
The gap between "the server holds the cards" and "the server knows the rules" is where cheating still lives. For a game between friends that gap does not matter; for a ladder with prizes it is the whole problem.