DogecoinArcade
@ Create a new account

Multiplayer and trades

docs/multiplayer.md

Jump to: How it works, in one paragraph · Rooms: `arcade.realtime` · Trades: `arcade.swap.trade` · Putting your game on the Apps tab

Two page APIs let an inscribed game be played together:

Neither knows anything about your game. A room is a name, a message is your own data, and a trade is "this piece for that". What they mean is up to you.

How it works, in one paragraph

Arcade nodes link to each other directly, peer to peer, with no server in the middle and no website involved. When your page joins a room, the arcade showing it joins that room on the mesh, and every other arcade with a player in the same room relays messages to it. Players are identified by their own chain address, proved by a signature their wallet makes once a day, so a name shown in your game is the name on the chain. Visitors who are not signed in play as guests.

Rooms: `arcade.realtime`

``html <script src="/r/realtime.js"></script> <script> arcade.realtime.join('town', {game: 'my-game'}).then(function (room) { if (!room.online) return playSolo(); // never stop anyone playing room.members().forEach(addPlayer); room.on('join', function (p) { addPlayer(p); }); room.on('leave', function (p) { removePlayer(p); }); room.on('message', function (data, p) { movePlayer(p, data); }); setInterval(function () { room.send({x: me.x, y: me.y}); }, 250); }); </script> ``

arcade.realtime.join(name, {game})

Resolves to a room. It never rejects: if there is no mesh, the page was opened outside an arcade viewer, or anything else goes wrong, the room has online: false and a reason in room.why. Play on alone in that case.

The room

room.onlinetrue when you are in the room
room.meyou, as a player (see below)
room.members()everyone in the room, you included
room.send(value)send to everyone else; a string as is, anything else as JSON
room.on('message', fn(data, player))data is parsed JSON when it parses
room.on('join', fn(player)), room.on('leave', fn(player))
room.on('rejoined', fn(me))your connection dropped for longer than the node waits (30 seconds) and you were seated again: everyone else saw you leave and come back, so send your state again (position, gear)
room.on('closed', fn(why))the room is gone for good (the node unreachable for about ten minutes); join again to retry
room.leave()leave the room

A player is {id, address, tag, guest, node}. Key your game state by id. address and tag are the player's chain address and @tag (without the @), proved by their own key; for a guest they are null and "", and guest is true.

Limits

When the node restarts

A node restarts now and then (an update, a reboot), and every room on it goes with it. Your page does not have to notice: the viewer takes the player's seat again by itself, retrying until the node is back, and the room carries on.

Leaving

Nothing is required. When the tab closes or navigates, the viewer leaves every room. Call room.leave() when a player moves to another part of your game, then join() the next room. Being in several rooms at once is fine.

Busy places: cells and full rooms

One room for a whole town means everybody hears everybody: a thousand players sending five messages a second is five thousand a second for each of them. Two helpers keep a crowd cheap:

```js // A full room sends you on: town, town#2, town#3 ... (room.name says which). arcade.realtime.join('town', {cap: 50});

// The world in square cells: you hear the cells within sight, and speak in your own. arcade.realtime.area('world', {cell: 64, view: 48, x: px, y: py}).then(function (a) { a.on('message', function (data, p) { movePlayer(p, data); }); a.on('join', addPlayer); a.on('leave', removePlayer); // as the player walks: a.move(px, py); // cells come and go by themselves a.send({x: px, y: py}); // heard by everyone who can see your cell }); ```

Keep view smaller than cell and a player is in at most four cells, whatever the size of the world. A player standing in two of your cells is one player to you: join once, leave when they are out of all of them.

Any node is the way in

A room is the same room on every node, so a player can use whichever node answers them fastest. The viewer showing a game asks its own node for the other nodes it can reach over https (their tunnel addresses, and arcade domains on the chain), times a hello to each, and plays through the quickest one if it is clearly faster than its own; it checks again every ten minutes, and goes back to its own node if the chosen one stops answering. A player signed in on one node is still themselves on another: their wallet signs that node's one-day room certificate. A node's tunnel carries only its room routes (/realtime/...) -- never the wallet or the rest of the arcade. Nothing changes for a game: arcade.realtime works the same.

Fast games: the fast lane

Rooms are made for games where a few updates a second is plenty. An action game (a shooter, a racer, a brawler) with up to about 8 players can ask for the fast lane, which connects the players' browsers directly to each other:

``js arcade.realtime.join('match-1', {fast: true}).then(function (room) { if (!room.fast) return useRoomSendInstead(); // this browser has no WebRTC room.fast.on('message', function (data, p) { moveTank(p, data); }); room.fast.on('peer', function (p, state) { showLink(p, state); }); setInterval(function () { room.fast.send({x: x, y: y, yaw: yaw}); }, 33); // 30 a second room.fast.send({kill: victimId}, {reliable: true}); // arrives, in order }); ``

When two players cannot connect directly. Some players sit behind routers that refuse direct connections (carrier NAT on some phone networks and satellite internet). Most connect anyway, often over IPv6. A pair that cannot connect is in state 'room': their fast sends go through the room instead, about three a second (reliable ones in order, and of the rest only the latest). The game keeps working for those two, slower. A relay that carries such pairs at full speed is planned as a service your game can pay node operators for.

The connections are made by the viewer, not your page. Your page still reaches nothing but the viewer, and the other players' addresses are never shown to it.

A service for your game: a bank, a dealer, a referee

Some games need something that is always in the room: a bank that pays out, a dealer, a referee. You can run one for your own game on your own node, with Python and no browser. It joins your game's rooms as your node's own account, so players see your @tag, and nobody can run one in your name: every node checks the certificate with its own Core.

```python from arcade.service import Room

with Room("mygame", "bank") as room: # your game's name, the room's name for event in room.events(): # join, leave, message -- comes back by itself after a restart if event["type"] == "message": room.send({"t": "ack", "to": event["member"]["id"]}) ```

Run it on the node's machine, as the user the node runs as: it reads the node's service key (~/.dogecoinarcade/realtime-service.key, made when the node starts, readable only by that user). The key works only from the machine itself, never through the door. python -m arcade.service mygame bank prints a room's events, a starting point. A service has the same limits as a player (512 bytes, about five messages a second); anything that must last goes on the chain.

Many nodes, one mesh

Every node carries its own players into the same rooms, so it does not matter which node a player uses. A node finds others from the chain's mesh announcements, from --mesh-peer, from what its peers pass on, and from the peers it reached before (mesh-peers.json, kept for 30 days), so a restart never depends on any one node still being there. It keeps four links and swaps its slowest for a faster peer when it finds one.

A node behind a router that cannot forward a port (carrier NAT, Starlink) still dials out. If it has IPv6 it announces that address and meets other IPv6 nodes halfway: both dial each other on the same beat of the clock, so each router lets the other in. And if cloudflared is installed, it also opens a free Cloudflare quick tunnel and announces that wss:// address, so even IPv4-only players' nodes can reach it. The mesh's own encryption runs inside the tunnel, so Cloudflare carries only ciphertext; nodes still link directly wherever they can, and --mesh-tunnel off turns it off.

The node's Pepecoin Core nodes ride the mesh too: for up to four linked arcades per chain (the quickest links), the node gives its Core a local peer address that leads, through the mesh link, to that arcade's Core. Two nodes behind routers that Core alone could never connect still sync and relay to each other. Core traffic is capped at 256 KB/s per link and sent in small pieces, so games stay quick; --mesh-core off turns it off. Anybody with a public address can run a node that everyone else links to automatically -- it announces itself, and the others dial it within a minute. More such nodes, from more people, make the mesh harder to break.

Trades: `arcade.swap.trade`

A trade window between two players in the same room. Load both scripts:

```html <script src="/r/realtime.js"></script> <script src="/r/swap.js"></script> <script> arcade.swap.onTrade(function (t) { if (t.status === 'incoming') showTradeWindow(t); // somebody offered you one if (t.status === 'settled') refreshInventory(); });

// Offer my sword for 25 of a token: arcade.swap.trade({with: player.id, give: {inscription: swordTxid}, get: {token: 26, amount: '25'}});

// The other player, answering: arcade.swap.answer(t.id, true); </script> ```

A side is one of:

Both players must be signed in; guests cannot trade.

Calls

trade({with, give, get})offer give for get to the player whose room id is with; resolves to {id}
answer(id, accept, why)say yes or no to a trade offered to you
cancel(id)withdraw before it is sent
items()what this player holds: {address, inscriptions, balances}, for drawing a trade window
onTrade(fn)fn(trade) on every change, on both sides

A trade event is {id, status, with, give, get, role, why, txid}. give and get are always from the point of view of the page that hears it, and with is the other player. status is one of:

status
proposedyou offered it
incomingit was offered to you; show it and call answer
acceptedthe other side said yes
signedyour half is signed and on its way to the other player
broadcastthe trade is sent; txid names it
settledit is in a block and the NFT really moved: both sides moved
declined, cancelled, failedit is over; why says why when there is a reason

Several things each way

Pass lists to trade several things at once -- two swords and 50 Gold for a ring, a stack of 20 of one token, two copies of the same piece:

``js arcade.swap.trade({with: player.id, give: [{inscription: swordA}, {inscription: swordB}, {token: 26, amount: '50'}], get: [{inscription: ring}]}); ``

What players see

Your page never touches a key. Each player approves in their own arcade viewer, on a card your page cannot draw or press:

The side giving an NFT signs first; when both sides are NFTs, the player who offered does. That half travels to the other player sealed to their key, because a signed half is a promise anybody holding it could complete. Each player needs to have published their tag (their messaging key) once; trade says so if one of them has not.

Settling

The trade is one transaction both players signed. Both items move in the same block, or nothing moves. Until broadcast, either player can walk away and nothing is spent. After settled, the NFT and the payment are where your game expects them; read them with items().

Putting your game on the Apps tab

A multiplayer game is listed like any other: see Games: listing yours on the Apps tab, and set "multiplayer": true in its game JSON.

← all docs