Ranked online play for NEONGLOBS (a Globulos tribute): matchmaking, server-authoritative physics, three bot levels, ELO ratings, and a leaderboard — as a #206 loader plugin.
plugins: [{ id: 'globs', module: 'globs/plugin.js', prefix: '/globs' }]WS {prefix}/play— the match protocol (documented at the top ofplugin.js):hello→queue→matched→ per roundphase/commit/reveal/result→matchEnd.GET {prefix}/leaderboard— top 50 by rating, player count, bot anchors.
NEONGLOBS rounds are simultaneous secret commitments resolved by a
deterministic, fixed-timestep sim — a round is a pure function of its
commitments. So the server relays one small message per round, re-runs the
same sim as the single authority, and never streams a tick. ./sim.js is
vendored from the game repo, where tools/parity.sh proves node and
headless Chromium produce bit-identical match results for the same
seeds (that took work — see findings).
api.auth.getAgent on the WS upgrade (node clients send Authorization);
browsers use POST {prefix}/session — any credential getAgent
understands (bearer, DPoP, NIP-98) authenticates a real HTTP request
and buys a 24h HMAC macaroon-lite session (pattern #4; secret in
pluginDir) that the socket presents as hello{session}. This is how
xlogin users (Nostr extension /
guest key / Solid OIDC) get onto the leaderboard. A raw pod bearer still
works in-band as hello{token}. Guests play unrated. ELO: start 1200, K=32 vs humans, K=16 vs bots; bots are fixed
anchors (EASY 800 / MEDIUM 1100 / HARD 1400) that never move. Both deltas
are computed from pre-match ratings before either is applied. Disconnect
mid-match forfeits. State: elo.json (atomic tmp+rename) and
matches.jsonl in pluginDir.
ws.route+getAgent+pluginDircovered the whole service — a ranked realtime game server needed nothing beyond the documented surface. Zero new seams.- Browser WS auth is the same gap every WS plugin has: browsers cannot
set upgrade headers. The bearer-only in-band lift (
hello{token}) works because bearer verification reads only headers — but DPoP and NIP-98 are signed over a method+URL and cannot ride it. The resolution is the session bridge: authenticate a REALPOST /sessionrequest (all schemes verify naturally), hand back an HMAC macaroon-lite the socket can present. That combination — pattern #1's "real request" + pattern #4's self-verifying token — is probably the canonical answer for any WS plugin wanting full-scheme auth. - Cross-engine float determinism is real and it bites. With stock
Math.hypot/Math.sin/Math.cos/Math.atan2/Math.pow, node 24 and Chromium disagreed on 4 of 20 solver-mirror matches (implementation- defined precision; knife-edge games amplify ulps). The game and this vendored sim now usesqrt(x²+y²), a pinned FRICTION literal, and range-reduced Taylor-series trig — after which 40/40 matches are bit-identical across engines. Any plugin that replays client physics should expect this. - The server pushes
welcomeimmediately after upgrade, which beats a message listener attached after the client'sopenevent — the test buffers from socket creation. Same race webrtc/ fixed on the server side for the opposite direction.
Maps: the full ranked-match loop, bots, leaderboard, forfeits, keepalive pings. Doesn't: spectators, reconnection grace (disconnect = forfeit), multiple tables (soccer only, like the game), and rating decay — all protocol-compatible extensions, none blocked by the api.