Two page APIs let an inscribed game be played together:
arcade.realtime: rooms where players on any arcade meet and send each
other small messages many times a second, such as positions, moves and chat.arcade.swap.trade: one player offers another an NFT for an NFT, a token
or coins, and both items move in one transaction, or neither does.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.
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.
arcade.realtime<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.
name: 1 to 60 characters. Rooms with the same name in the same game
are the same room on every arcade.game: which game's rooms these are. It defaults to the inscription
being played, so only copies of the same page meet. If your game has several
versions (new inscriptions), pass a short id they all share, like
'my-game' (letters, digits, . _ : -, up to 64), and every version
meets in the same rooms.room.online |
true when you are in the room |
room.me |
you, 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('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.
send rejects past either.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.
leave, and come back as join,
so a game that keeps its player list from those two events stays right.id, so other players see the same player return. If that
name was taken in the meantime the guest gets a new one: room.me changes,
so read it again rather than keeping a copy.send while the seat is being taken again is rejected; send state, and
the next message puts things right.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.
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:
// 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.
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.
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.
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.
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.
arcade.swap.tradeA trade window between two players in the same room. Load both scripts:
<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:
{inscription: '<txid>'}: an NFT,{token: <property id>, amount: '25'}: an amount of a token, as a decimal
string,{coins: '1.5'}: coins.Both players must be signed in; guests cannot trade.
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 | |
|---|---|
proposed |
you offered it |
incoming |
it was offered to you; show it and call answer |
accepted |
the other side said yes |
signed |
your half is signed and on its way to the other player |
broadcast |
the trade is sent; txid names it |
settled |
it is in a block and the NFT really moved: both sides moved |
declined, cancelled, failed |
it is over; why says why when there is a reason |
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:
arcade.swap.trade({with: player.id,
give: [{inscription: swordA}, {inscription: swordB},
{token: 26, amount: '50'}],
get: [{inscription: ring}]});
{token, amount} each way works too.give and get as lists for a trade made this way.bundles_from) on each
chain; before it, trade says the chain does not read them yet.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.
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().
A multiplayer game is listed like any other: see Games: listing yours on the
Games tab, and set "multiplayer": true in its game JSON.