面向开发者 / 应用 API

Arcade 服务器

一个服务五个游戏的 WebSocket 服务器,旁边还有几条 HTTP 路由。五个里有四个,客户端根本不发送任何游戏输入,输入是一笔交易,而 socket 是你看着它落地的方式。

一个 socket,不是一个 API

一个进程在一个事件循环上跑五个游戏房间外加一个大厅,通过它自己连到节点的 socket 读取这条链, 并把结果扇出给每一个连上来的客户端。接口是那个 WebSocket;那些 HTTP 路由的存在是为了健康检查, 以及站点外壳在每一页上都要画的那两个数字。

游戏输入是一笔交易,不是一个帧

在竞技场里操控、在曲线上下注、一次旋转、在盘面上下注,全都是玩家发给某个合约的交易。这个 socket 承载的是你在看谁、一次存活探测,以及服务器对链上发生了什么的看法。客户端在它上面发的 任何东西都不授权任何事,也改变不了任何结果。

抓娃娃机房间是那个刻意的例外:驾驶一只爪子会变成每帧一笔交易,所以那些意图走 socket。即便 在那里接缝也很窄:三个动词,没有一个携带位置、时间或判定,而驾驶者是由浏览器早已在自己那次 拉动里发布到链上的一个原像授权的。

连接

接入点Value
Socketwss://minigame.picklechain.xyz/ws/<room>站点自己的 nginx 转发这次升级请求
HTTPhttps://minigame.picklechain.xyz/api/同一个进程,前缀被剥掉
你自己运行的一套栈http://127.0.0.1:3008Arcade 站点;服务器本身在容器网络上
Origin在升级请求上被检查不在已配置名单上的 Origin 会在握手之前被以 403 拒绝,所以这个 socket 根本不会打开

/ws/ 底下一个未知的路径会在升级时被以 404 拒绝,而一个超出下面那些 限制的连接会被以 429 拒绝,两者都是在任何 WebSocket 握手之前、以一个普通 HTTP 响应发在裸 socket 上的。一个只处理自己 socket 的 error 事件的客户端会看到一个 笼统的失败;如果你想知道是哪一种,就去读那个状态码。

六个房间

/ws/arena

房间
加入时
`hello`, then a full snapshot of the arena.

The snake arena. Steering is a transaction, so the socket carries no input for it.

/ws/crash

房间
加入时
`hello`, then the whole round: phase, time left, the commitment, every bet placed and every cash-out judged so far.

The curve. Bets are transactions.

/ws/claw

房间
加入时
`hello`, then the cabinet: what is left, where each capsule stands, the machine's constants and every attempt being driven.

The claw cabinet, and the ONLY room whose gameplay input rides this socket rather than the chain - a transaction per frame is not a game.

/ws/patch

房间
加入时
`hello`, then the machine: the bet ladder, the paytable, the meter and the spins still open.

The reels. A spin is a transaction; the socket watches it.

加入时
`hello`, then the table: every cell's multiplier, the current snapshot, the line so far and the bets still standing.

The board. Bets are transactions; the socket has one verb, and it buys nothing.

/ws/stack

房间
加入时
`hello`, then `stack`: every run being climbed with its rules state, the day's board and the rules' constants.

The tower. A start and every tap are transactions; the socket only listens, and says what the rules made of each tap.

/ws/derby

房间
加入时
`hello`, then `derby`: the current and next race, recent history, the rules and the paddock.

The race. A bet is a transaction; the socket only listens, and every runner is one of the chain's own oracle feeds.

/ws/glide

房间
加入时
`hello`, then `glide`: the day's board, every run in the air, the canyon cards and the rules.

The canyon. A start and every flap are transactions; the socket only listens, and the canyon it draws comes from a live price feed.

/ws/hall

房间
加入时
`hello` alone.

The lobby. Somebody who has not chosen a game sits here, and it is counted in the socket total.

每个房间都回答同样的开场序列:一个携带房间名、在玩人数、在看人数和结算运营者地址的 hello 帧,然后是那个房间状态的一份完整快照。这两个计数都是按房间的,它们过去无论你请求的是哪个房间都来自竞技场,于是宣布了一些并不在场的玩家。

客户端可以发什么

watch

every room
形态
{"t":"watch","account":"0x…"}

Says which account this socket is following, so the server can address one player directly - a refused join has to reach the person it was refused for. In the reels room it also arms that account.

值得注意。 It AUTHORISES NOTHING. The account is not verified, the contract decides who may play, and the address must match a 40-hex pattern or the message is ignored in silence.

ping

every room
形态
{"t":"ping","id":<any>}

Answered with `{t:"pong", id}`. The server also sends protocol-level pings and terminates a socket that stops answering them.

值得注意。 This is not the liveness mechanism the server relies on; it is the one a client can observe.

形态
{"t":"claim","tx":"0x…","secret":"0x…"} and three verbs after it

Takes the wheel of an attempt and drives it. The proof is a preimage: the hash of the secret must equal the seed the pull published on chain, so the only client that can drive an attempt is the one that made it - no signature, no challenge, no round trip.

值得注意。 None of the verbs carries a position, a time or a verdict. A wrong secret is refused and counted and never logged. A reconnect takes the wheel from the previous socket, which is told so.

quote

the trellis room only
形态
{"t":"quote", …}

Asks the operator to sign the price already printed in a square.

值得注意。 It carries no stake and no account, and the contract checks the clock, the lock and the float itself. A flood of them costs this process signatures and nothing else.

javascript
const socket = new WebSocket("wss://minigame.picklechain.xyz/ws/crash");

socket.addEventListener("open", () => {
  // Tell the server which account to address directly. It verifies nothing:
  // this only decides who a per-player frame is sent to.
  socket.send(JSON.stringify({ t: "watch", account: myAddress }));
});

socket.addEventListener("message", (event) => {
  const frame = JSON.parse(event.data);
  switch (frame.t) {
    case "hello": /* room, playing, watching, operator */ break;
    case "full":  /* the whole room, sent once after hello */ break;
    case "chain": /* resyncing, and the range of mini-blocks lost, if any */ break;
    default:      /* IGNORE. The frame list is not versioned and rooms gain frames. */
  }
});

服务器发什么

Every frame is a JSON object with a `t` discriminator. `hello` arrives first and carries the room, the number playing, the number watching and the operator's address; a full snapshot follows. After that the room pushes what changed.

Across the eight rooms: chain, tick, state, full, bet, settled, settle-failed, refused, refunded, refund-due, fulfil-failed, pull, claimed, released, blocked, armed, quote, feed, unsealed, void, race, started, finished, paddock, board, run, points, flight, over, cancelled, canyons, eat, death, pong.

链上的一个空洞会被宣告出来,而它在每个房间里意味着不同的事

chain 帧携带 resyncing 以及读取器取不到的那段迷你区块范围。它被 广播给每一个房间,而每个房间对它的处理都不同:一个与丢失范围重叠的回合会说自己的赢家列表 可能不全,而不是把它宣布成所有人;抓娃娃机房间则把丢失的范围送进自己的日志,好让里面的本金 能被找出来并退款。一个忽略这个帧的客户端,会为一幅不完整的画面画出一个自信的样子。

那些 HTTP 路由

Params
none
返回
{ok, loop:{p50Ms, p99Ms, maxMs, windowMs}}

Liveness, plus the event loop's recent delay. Eight rooms, a physics step and every socket's fan-out share one loop, so a loop that stalls stalls every quote, every echo and every frame at once.

值得注意。 This is the figure that answers `the arcade is slow sometimes`. It is measured over ten-second windows and reset each time.

Params
none
返回
{ok, reasons, miniBlock, appliedMiniBlock, appliedLagMs, resyncing, lost}

503 when the socket to the node is down, or when the last APPLIED mini-block is more than five seconds old.

值得注意。 It measures what was applied, not what arrived. Measuring arrival meant a wedged dispatch chain answered 200 while no input had been applied for minutes, which is the exact failure this probe exists to catch.

Params
none
返回
{total:{playing, sockets, spectators}, arena, crash, claw, patch, trellis, stack, derby, glide, …}

The whole arcade in one object: a summary per game and three numbers for the chrome every page draws. `playing` means somebody with something at stake right now, and each game measures that its own way.

值得注意。 PUBLIC AND UNAUTHENTICATED, which decides what may appear on it. The crash summary withholds its point until the curve has stopped, and the claw summary carries the published commitment and never a link beneath it - one link is the outcome of the next pull nobody has made.

Params
none
返回
{supply, remaining, claimed, cells, price, pullsPerAccount, commit, chainLeft, blocked, machine}

The cabinet on its own, so a page can state what is left without opening a socket.

值得注意。 503 when the claw room is not running.

Params
the pull id in the path
返回
one finished pull, whole

The seed the player published, every intent the server recorded, the tick the drop landed on and the verdict that came out of them - the arithmetic a reader can rerun.

值得注意。 A pull still being driven is a 404, deliberately: its trace is not finished and its link is not revealed. A revealed link is served only because the fulfilment that finished the pull already put it on chain.

Params
none
返回
{playing, watching, day, pot, top, leaders}

Stack's day without a socket: the UTC day, its pot in wei, the chain's own top three and the tallest runs this server has finished today.

值得注意。 503 when the stack room is not running. The pot and the top three are read off the contract once a minute and after each finish, so they can trail a finish by a moment.

Params
the run id in the path
返回
one run, whole: {id, a, t0Us, startMini, taps:[{us, mini, hash}], results, height, why, finished, rules}

Every tap of a run with the mini-block that carried it and what the rules made of it - the arithmetic a reader reruns with arcade-server/src/stack.js to check the height the operator reported.

值得注意。 A 404 for a run this process never saw or has already let go of; it keeps the last five hundred. The chain still holds every tap, and the rules still apply to them.

Params
none
返回
{playing, watching, k, phase, card, unsettled, current, next, history, rules}

Derby's board for a page that has no socket yet: the current race and the next one, recent history, and the rules (the feed allowlist, the wall and night cards, the stake bounds and the seed).

值得注意。 503 when the derby room is not running. `unsettled` counts bets still waiting on a settle, across every race this process still holds.

Params
the race number in the path
返回
one race, whole: {k, state, card, gate, line, runners, handicaps, pools, startPrices, startRounds, lineRounds, roundsFrom, scores, winners, why, hashes, bets}

The lineup, the handicaps, the rounds both seals named, the scores, the winners and every bet - the arithmetic a reader reruns with arcade-server/src/derby.js against the same public rounds.

值得注意。 A race this process never opened is a 404. `roundsFrom` says whether each end's rounds came from this room sealing them or were recomputed from the tape after a restart - the proof is the same either way.

Params
none
返回
{playing, watching, day, pot, top, leaders, canyons}

Glide's board for a page that has no socket yet, with every canyon's card alongside the day's pot and its leaders.

值得注意。 503 when the glide room is not running.

Params
the run id in the path
返回
one run, whole: {id, a, feed, symbol, r0, sigma, startUs, startMini, startHash, answers, flaps, result, rules}

r0, the rounds of the canyon this run flew, and every flap with the mini-block that carried it - the arithmetic a reader reruns with arcade-server/src/glide.js `replay` to get the same score.

值得注意。 A run this process never saw or has already let go of is a 404.

这四条全都不需要认证,而这正是决定了它们上面可以出现什么的东西。任何会泄露一个未完成回合的 东西都不会被发布:一个崩盘点在曲线停下来之前都被扣住,而抓娃娃机的哈希链只以它的承诺形式 发布,在 /fair 上揭示一环,纯粹是因为那次完成了这次拉动的履行已经把它写到链上了。

限制,以及什么会撞破它们

界限Value
Sockets256 total, 8 per addressthe upgrade is refused 429 before the handshake; the slot is released when the raw socket closes, not when the handshake succeeds
Originan allowlistthe upgrade is refused 403, so a page on another origin cannot drive this socket with a visitor's network
Frame size4 KiBtwo orders of magnitude above the largest real message; the library's default is 100 MiB
Inbound rate20 a second, burst 4045 and 90 in the claw room, because its driving is on this socket and a hand working a pad passes ten changes a second without trying
Overflowthe socket is closedclosing is kinder than ignoring - and in the claw room it ends a paid attempt, which is why that allowance is the wider one
Backpressure512 KiB buffereda socket further behind than that is cut and terminated rather than written to; a client that never reads would otherwise grow the process's heap without limit
Livenessa ping every half minuteanything that has not answered by the next one is terminated

一个慢读者会被断开,而不是被缓冲

那个库会把它写不出去的东西统统缓冲在这个进程的堆里,没有上限。所以一个落后超过半兆字节的 socket 会被切断并终止,而不是继续往里写:否则几个打开 socket 却从不读的客户端就能把容器 耗尽,而在一个持有密钥的进程上,那就是一次远程击杀。半兆字节大约是这个服务器发出的最大帧的 五十个;任何在读的客户端都不会落后那么多。

抓娃娃机房间的消息预算更宽,而那是刻意的

在别处,一个客户端每隔几秒发一次 ping,别的什么都不发,所以每秒二十条消息只可能是一个坏掉的 或者怀有恶意的客户端。抓娃娃机的驾驶每改变一次意图就发一条消息,一次按下和一次松开就是两条, 而一个双手操作手柄的玩家不费力就能超过每秒十条。终止那个 socket 等于在半途把方向盘从人手里 拿走,而他们付过钱的那次尝试会撞在栏杆上耗尽。所以那里的天花板设在一只手能产生的量之上, 而不是正好卡在那里。