A plain prize pool pays whoever gives its phrase, and the phrase is in the game's own code. Anybody who reads it can claim without playing. A pool with a referee pays only for a win that really happened: the game plays from a seed the referee hands out, records what the player did, and sends the recording with the claim. The referee runs the recording through the game's own rules, and signs the claim only if it wins.
It works for any game whose rules can run without graphics and give the same answer every time: puzzles, card and board games, shooters, racers, anything turn-based or frame-based.
https://app.dogecoinarcade.com unless you name
another). It holds the key that signs winning claims, which is why it cannot
be on the chain: a key written on the chain would sign for anybody.Deleting the pool is one more inscription, as for any pool.
A plain pool's prizes are signed ahead of time. A refereed pool's address needs two signatures to pay a prize: the pool's, made ahead of time as usual, and the referee's, which it gives only after a winning replay. The referee's signature covers the whole claim, so the prize can only go to the wallet that played. A claim built by hand from the phrase alone is refused by the network itself, not just by arcade nodes.
Closing the pool needs neither: it takes your own wallet's key, so what is in the pool is always yours to take back, even if the referee is gone.
A judge is one JavaScript function:
function judge(seed, inputs, params) {
// replay the run from the seed and the recorded inputs
return {won: true, score: 1234};
}
| argument | |
|---|---|
seed |
64 hex characters, issued by the referee. All randomness comes from it |
inputs |
whatever your page recorded, as JSON, up to 1 MB |
params |
what the pool says, so one judge can serve several pools: {"stage": 10}. The referee adds params.claimer, the address the claim pays, and params.facts when the pool asks for them |
It returns an object of your own design, up to 2 KB: whatever your game or
use case needs to say about a run — a time, a score, the order of finishers,
a level reached. The referee keeps it exactly as returned. Two fields are read
only when a pool asks for them: won (true) for a pool that requires a win,
and score (a number) for one that requires score_min. A judge that never
feeds a pool can leave both out.
The referee must reach exactly the result the player's browser reached. So the game and the judge share one simulation, and that simulation is deterministic:
Math.random. A small seeded
generator is enough:js
function rng(seed) { // mulberry32 over the seed's first word
let a = parseInt(seed.slice(0, 8), 16) >>> 0;
return function () {
a = (a + 0x6D2B79F5) >>> 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
Date and performance do not exist in the judge.+ - * / and Math.sqrt give the same answer in
every engine; Math.sin, cos, atan2, exp and pow may not. Write your
own (a lookup table or a short series) and use it in the game too.The simplest way to get all of this right is to write the game's rules as one file that both the page and the judge load, and keep drawing separate.
Record what the player did, per tick, as compactly as you like. It is your format; the judge reads it. For example:
{"v": 1, "stage": 10, "moves": [[120, 0, 1], [3, 2, -1]]}
Run-length encoding (how many ticks, then the move) keeps a long run small. Cap in the judge anything a player could inflate: the stage, the loadout, the run's length.
The referee runs judges in QuickJS 1.19.4, with 64 MB and 5 seconds. Test yours in exactly that before you inscribe it:
pip install quickjs==1.19.4
import json, quickjs
ctx = quickjs.Context()
ctx.set_memory_limit(64 << 20); ctx.set_time_limit(5)
ctx.eval("delete globalThis.Date; Math.random = undefined;")
ctx.eval(open("judge.js").read())
ctx.set("s", seed); ctx.set("i", json.dumps(inputs)); ctx.set("p", json.dumps(params))
print(ctx.eval("JSON.stringify(judge(s, JSON.parse(i), JSON.parse(p)))"))
Play a run in the browser, save its seed and inputs, and check the judge says the same thing the game did. Then do it for a loss.
On the NFTs page, Inscribe one thing, choose the .js file. It must be
inscribed as text/javascript and be at most 256 KB.
Everything in the prize pools guide applies. Add referee:
{"name": "Final stage prize",
"prizepool": {"token": 19, "lot": "250", "lots": 10, "price": "0.01",
"game": "#226", "once": true, "phrase": "<your phrase>",
"referee": {"node": "https://app.dogecoinarcade.com",
"judge": "#<your judge>",
"require": {"won": true},
"params": {"stage": 10},
"seed_hours": 2}}}
| field | |
|---|---|
node |
the arcade that referees, by its address. Leave it out to make the node you are signed in on the referee |
judge |
your judge inscription, #number or id |
require |
{"won": true}, or {"score_min": N} |
params |
optional, up to 4 KB: handed to the judge |
seed_hours |
optional: how long a player has from starting a run to claiming its prize. 2 unless you say, up to 720 (30 days). Give a game whose runs are long, or that lets a player claim later, more |
A game where levels are token balances and gear is NFTs needs the judge to
know what the player really holds, or a replay could claim the best gear.
Name what to read, and the referee passes it to the judge as params.facts:
"facts": {"tokens": [21, 22],
"collections": [{"creator": "<address>", "collection": "Arsenal"}]}
params.facts = {tokens: {"21": "1500", "22": "0"}, // balances, in units
pieces: [{id, number, collection, json}]} // held pieces of those collections
It is what the player held when the seed was issued and still holds when the claim is judged: the smaller balance of each token, and only the pieces held at both moments. Gear handed to another wallet after a run starts counts for neither run. Up to 20 tokens and 10 collections.
Before a run that can win a prize:
parent.postMessage({arcade: "seed", seq: 1, pool: "<pool_id>"}, "*");
The answer comes back as a message with the same seq:
{arcade: "seed", seq: 1, seed, expires}: play the run from seed.{arcade: "seed", seq: 1, error}: no seed. "this pool has no referee"
means a plain pool, so claim the plain way.A seed belongs to the wallet that is looking and to that pool. It works once,
and lasts as long as the pool's seed_hours says (two hours unless it says):
a run has to be claimed within that long of starting. expires says when, in
seconds since 1970. Only seeds expire. The judge is an inscription and never does, and the pool
pays out until it is empty or you delete it. pool_id comes from GET /r/claimpools/<your game>.
A session that can pay more than one prize (experience from one pool, an item from another) asks for a seed for the whole game instead of one pool:
parent.postMessage({arcade: "seed", seq: 1, game: true}, "*");
// answers {seed, expires, pools: [pool ids]}
Every refereed pool of this page takes that seed once each, with the same
replay; each pool's judge and params decide what it pays for. The pools must
share a referee. The seed lasts as long as the longest seed_hours among them.
A game that pays as it is played -- a coin picked up, an item dropped -- asks for a SESSION seed once per play session, and claims each time something is earned:
parent.postMessage({arcade: "seed", seq: 1, game: true, session: true}, "*");
// answers {seed, expires, pools}: the game's pools that take a session seed
"session": true. Pair it
with "fee": "pool" (see Prize pools) and the player is never asked.params.since (where the new part of the record starts),
params.paid (every payment of this seed: {pool, lot, at, params}, with the
paying pool's own params) and params.prior (what it returned for the record
as last accepted, so it can carry on from there rather than replay a long
session). It answers about what is new: won while more was earned than paid.seed_hours among the game's session pools
(2 to 4 hours suits a play session); ask for a new one when it expires. What
the player holds (facts) is read when the seed is issued.When the player wins:
parent.postMessage({arcade: "claim", seq: 2, secret: "<phrase>",
pool: "<pool_id>", replay: {seed, inputs}}, "*");
The player sees the arcade's claim card, which says the referee checks their
run before it pays. The answers are those of any claim: heard, then
{ok, txid, piece} or {error}.
A pool with "fee": "pool" (see Prize pools) needs nothing of the player but
their run: the claim is made of the lot's own coins, so there is no card and no
key to unlock, and the prize is in the player's wallet as soon as the block
lands. Use it for prizes a game pays while it is being played.
| error | what to tell the player |
|---|---|
the replay did not win |
the run did not win under the judge. If it did in the game, the game and the judge disagree |
the judge did not say whether the replay won / the judge gave no score |
the pool requires won or score_min and the judge's result has no such field |
the replay scored N, under M |
a score_min pool, and the score was short |
that seed was used; a new run needs a new seed |
ask for a new seed and play again |
that seed expired; ... |
the same |
that seed was not issued for this pool |
the seed came from another pool |
this prize pays only for a replay of a win ... |
the claim came without replay |
this wallet has already claimed from this prize pool |
a once pool |
the judge ran too long / used too much memory / failed: ... |
the judge is broken for this run: a bug for the game's author |
that wallet has had its limit of replays judged this minute ... |
wait a minute |
this pool's referee did not answer ... |
the referee node is offline. The prize is still there |
A game can also keep its results on the chain — times, scores, a finished level — with the referee's word that each one really happened. Nobody has to run a score keeper: the result is an inscription, and any node, prize pool or leaderboard page can check it.
parent.postMessage({arcade: "seed", seq, judge: "<judge id>"}, "*").
Name another arcade as referee: "https://…" to use its referee.verify:parent.postMessage({arcade: "inscribe", seq: 2,
data: JSON.stringify({track: "Harbour"}),
contenttype: "application/json", json: {game: "racecondition"},
verify: {judge: "<judge id>", seed, inputs, params: {laps: 3}}}, "*");
// answers {ok, txid, result}
The referee runs the judge and signs what it said — the whole result
object, up to 2 KB — for this player's address over this content. The
signature goes into the inscription's JSON as attested. A copy inscribed by
anybody else, or over other content, does not check out. Verified means "this
is what the judge said", not "this was good": every run the judge scores is
verified, so a game keeps every result and decides for itself what counts.
A seed works once. If the player cancels the inscription card after the referee has signed, that run is spent; play again with a new seed.
GET /r/verified/<judge> lists every inscription carrying a result that
judge verified, newest first, from every player:
{"judge": "…", "referee": "02…", "next": 212,
"results": [{"id": "…", "number": 214, "creator": "n…", "creatortag": "pip",
"block": 1518123, "result": {"won": true, "score": 120.4, …},
"json": {"game": "racecondition"}}]}
referee=<key> names the referee the list trusts; this node's own unless
given. An attestation signed with any other key is left out — without that,
anybody could sign results with a key of their own.after=<number> pages on (pass next); limit up to 200.result its own way, and shows
creatortag. Names come from the chain, never from the result.A pool's facts can ask for the claimer's own verified results:
"facts": {"attested": ["<judge id>"]}
and its judge gets params.facts.attested: every result of that judge the
claimer inscribed, checked against this referee's key —
[{id, number, judge, block, result, json}]. A 16-race tour pays its
trophies this way: the trophy pool's judge totals the claimer's verified races.
params.claimer is the address the claim pays.
GET /r/referee on any arcade says whether and how it referees:
{"pubkey": "02…", "engine": "quickjs==1.19.4", "cpu_seconds": 5,
"memory_bytes": 67108864, "inputs_bytes": 1048576, "judge_bytes": 262144,
"seed_seconds": 7200, "judged_per_minute": 3}
pip install
"dogecoin-arcade[referee]", which adds QuickJS and the signing library.once is per wallet, not per person.