|
| 1 | +# globs — NEONGLOBS realtime match server |
| 2 | + |
| 3 | +Ranked online play for [NEONGLOBS](https://melvincarvalho.github.io/neonglobs/) |
| 4 | +(a Globulos tribute): matchmaking, server-authoritative physics, three bot |
| 5 | +levels, ELO ratings, and a leaderboard — as a #206 loader plugin. |
| 6 | + |
| 7 | +```js |
| 8 | +plugins: [{ id: 'globs', module: 'globs/plugin.js', prefix: '/globs' }] |
| 9 | +``` |
| 10 | + |
| 11 | +- `WS {prefix}/play` — the match protocol (documented at the top of |
| 12 | + `plugin.js`): `hello` → `queue` → `matched` → per round |
| 13 | + `phase`/`commit`/`reveal`/`result` → `matchEnd`. |
| 14 | +- `GET {prefix}/leaderboard` — top 50 by rating, player count, bot anchors. |
| 15 | + |
| 16 | +## Why this game networks so well |
| 17 | + |
| 18 | +NEONGLOBS rounds are *simultaneous secret commitments* resolved by a |
| 19 | +deterministic, fixed-timestep sim — a round is a pure function of its |
| 20 | +commitments. So the server relays one small message per round, re-runs the |
| 21 | +same sim as the single authority, and never streams a tick. `./sim.js` is |
| 22 | +vendored from the game repo, where `tools/parity.sh` proves node and |
| 23 | +headless Chromium produce **bit-identical** match results for the same |
| 24 | +seeds (that took work — see findings). |
| 25 | + |
| 26 | +## Identity and ranking |
| 27 | + |
| 28 | +`api.auth.getAgent` on the WS upgrade (node clients send `Authorization`), |
| 29 | +or `hello{token}` for browsers — the token is lifted to an agent via |
| 30 | +`getAgent` with a synthetic headers-only request, which the `auth.js` |
| 31 | +contract documents as sufficient for bearer verification. Guests play |
| 32 | +unrated. ELO: start 1200, K=32 vs humans, K=16 vs bots; bots are fixed |
| 33 | +anchors (EASY 800 / MEDIUM 1100 / HARD 1400) that never move. Both deltas |
| 34 | +are computed from pre-match ratings before either is applied. Disconnect |
| 35 | +mid-match forfeits. State: `elo.json` (atomic tmp+rename) and |
| 36 | +`matches.jsonl` in `pluginDir`. |
| 37 | + |
| 38 | +## Findings |
| 39 | + |
| 40 | +- **`ws.route` + `getAgent` + `pluginDir` covered the whole service** — a |
| 41 | + ranked realtime game server needed nothing beyond the documented surface. |
| 42 | + Zero new seams. |
| 43 | +- **Browser WS auth is the same gap every WS plugin has**: browsers cannot |
| 44 | + set upgrade headers, so the plugin lifts a bearer sent in-band |
| 45 | + (`hello{token}`) via `getAgent({ headers: { authorization } , ... })`. |
| 46 | + Works because bearer verification only reads headers; a DPoP-bound token |
| 47 | + would not survive this path (documented limitation, same as core's |
| 48 | + `.webrtc`). |
| 49 | +- **Cross-engine float determinism is real and it bites.** With stock |
| 50 | + `Math.hypot`/`Math.sin`/`Math.cos`/`Math.atan2`/`Math.pow`, node 24 and |
| 51 | + Chromium disagreed on 4 of 20 solver-mirror matches (implementation- |
| 52 | + defined precision; knife-edge games amplify ulps). The game and this |
| 53 | + vendored sim now use `sqrt(x²+y²)`, a pinned FRICTION literal, and |
| 54 | + range-reduced Taylor-series trig — after which 40/40 matches are |
| 55 | + bit-identical across engines. Any plugin that replays client physics |
| 56 | + should expect this. |
| 57 | +- **The server pushes `welcome` immediately after upgrade**, which beats a |
| 58 | + message listener attached after the client's `open` event — the test |
| 59 | + buffers from socket creation. Same race webrtc/ fixed on the server side |
| 60 | + for the opposite direction. |
| 61 | + |
| 62 | +## What maps / what doesn't |
| 63 | + |
| 64 | +Maps: the full ranked-match loop, bots, leaderboard, forfeits, keepalive |
| 65 | +pings. Doesn't: spectators, reconnection grace (disconnect = forfeit), |
| 66 | +multiple tables (soccer only, like the game), and rating decay — all |
| 67 | +protocol-compatible extensions, none blocked by the api. |
0 commit comments