Skip to content

Commit 2c2f6f5

Browse files
globs: ranked realtime match server for NEONGLOBS — matchmaking, bots, ELO, leaderboard; vendored parity-tested deterministic sim; cross-engine float-determinism + in-band WS bearer findings
1 parent b594066 commit 2c2f6f5

8 files changed

Lines changed: 893 additions & 0 deletions

File tree

NOTES.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,3 +370,17 @@ this repo is its proof.
370370
and no guidance; a design note in the plugin docs would spare every
371371
author the same deliberation. Eleven plugins now persist in
372372
`pluginDir` — the most settled seam in the api.
373+
374+
- **Cross-engine float determinism** (globs/): `Math.hypot`, `Math.pow`,
375+
and library trig are implementation-defined precision; node 24 and
376+
headless Chromium disagreed on 4/20 knife-edge bot-mirror matches when a
377+
game sim was replayed on both. Fix in the sim itself: `sqrt(x*x+y*y)`,
378+
a pinned pow literal, and range-reduced Taylor trig — then 40/40 matches
379+
bit-identical. Any plugin acting as physics authority for a browser
380+
client will hit this; not an api seam, but worth knowing before trusting
381+
"same JS everywhere".
382+
- **In-band WS bearer** (globs/): browsers can't set upgrade headers, so
383+
the plugin lifts `hello{token}` to an agent via `getAgent` with a
384+
synthetic headers-only request — valid for bearers per the auth.js
385+
contract, not for DPoP-bound tokens. Third WS plugin to want this;
386+
a documented `api.auth.getAgentFromToken(token)` would make it official.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ README and NOTES.md, ship the closest honest approximation.
9595
| `forge/` | #322-next | git forge — hosting, GitHub-light UI + JSON API, forks/PRs with real merges, issues as pod resources, did:nostr + xlogin, Bitcoin-anchored history (Blocktrails); zero deps, zero build |
9696
| `recordweb/` || [RecordWeb](https://github.com/recordweb) (RWP) node — institutional Records with `did:rwp`, content-addressed immutable snapshots in a version DAG (RFC 8785 + SHA-256 + Ed25519), one-way finalization, Merkle-rooted Cases; DID resolver + verify; **zero deps, zero seam gaps** — the four landed seams suffice for a from-scratch protocol |
9797
| `ripple/` | [webcontracts#4](https://github.com/webcontracts/webcontracts.github.io/issues/4) | Ryan Fugger's ORIGINAL Ripple (2004) — bilateral trustlines, BFS payment routing through chains of pre-granted credit, creditor-only settle, hash-chained transition log (trustline.v1); signed single-balance bookkeeping, bigint micro-units, UI page; single-server atomicity honest, federation is the named next flavour |
98+
| `globs/` || ranked realtime match server for [NEONGLOBS](https://melvincarvalho.github.io/neonglobs/) (a Globulos tribute) — matchmaking + bots + ELO + leaderboard over `ws.route`; server re-runs the game's deterministic sim as authority, vendored with a **bit-identical node↔Chromium parity contract** (the cross-engine float-determinism finding) |
9899

99100
Plus one directory that is a scenario, not a plugin:
100101
[`federation-demo/`](./federation-demo/)**two** JSS instances federating

compose.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ describe('composition: every plugin on one server', () => {
6262
plugins: [
6363
{ module: at('relay/plugin.js'), prefix: '/relay' },
6464
{ module: at('webrtc/plugin.js'), prefix: '/webrtc' },
65+
{ module: at('globs/plugin.js'), prefix: '/globs', config: { aimTimeMs: 300 } },
6566
{ module: at('terminal/plugin.js'), prefix: '/terminal', config: { token: 'compose-secret' } },
6667
{ module: at('tunnel/plugin.js'), prefix: '/tunnel' },
6768
{

globs/README.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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

Comments
 (0)