From 87474ad5ec6f707bf79d9d42eaddab90da6d082f Mon Sep 17 00:00:00 2001 From: John Coffey Date: Tue, 8 Sep 2026 17:58:40 -0700 Subject: [PATCH] Say plainly that there is no player limit, and stop one classroom hitting it The README heading read 'Two people, two browsers', which sounds like a cap. It is not one: the server holds no game state at all, so any number of people play independent games and the server only ever sees a finished score. The only real cap is four to a game, and that is a keyboard sharing the same stand rather than anything technical. Checking that turned up a case where it would not have been true. A classroom, an office or a household all arrive from one address, and the submission limit of thirty an hour was low enough that a class finishing together would have started losing scores to a 429. Raised to 120 and verified with a burst of 130: the first 120 land, the rest are refused. What actually keeps rubbish off the board is the plausibility check, not this limit. --- README.md | 24 +++++++++++++++++------- server/README.md | 5 +++-- server/src/index.ts | 9 ++++++++- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c9a6969..04a1367 100644 --- a/README.md +++ b/README.md @@ -134,15 +134,25 @@ just slash-separated notes in a step (`F4/A4/C5`). Both run off one scheduler that queues notes 200 ms ahead, so the groove does not stutter when React re-renders. `MUSIC` and `SOUND` toggle independently. -## Two people, two browsers +## How many people can play -The game itself is a static bundle and lives entirely in the page. Two people -on two machines, two browsers or two profiles play completely independent -games — different seeds, different weather, different books. Nothing about a -season is shared or synchronised. +As many as you like. There is no limit, and no meaningful sense in which +players share anything while they are playing. -The only thing they have in common is the leaderboard they both post to at the -end. The skin preference is the one thing still kept in the browser. +The game holds no state on the server — it is a static bundle, and a season +lives entirely in the page. Two people, or two hundred, on any mix of +machines, browsers and profiles get completely independent games: different +seeds, different weather, different books. The server never learns a game is +happening; it only ever sees a finished score being posted at the end. + +The one real cap is **four players to a game**, and that is a keyboard +limitation rather than a technical one — they are taking turns at the same +stand, hot-seat style. Nothing stops four separate people playing four +separate games at the same moment. + +Everyone posts to the same leaderboard, and everyone reads the same one. That +is the only thing players have in common. The skin preference is the one thing +still kept in the browser. ## Running it anywhere diff --git a/server/README.md b/server/README.md index ace06cf..bc6e6c2 100644 --- a/server/README.md +++ b/server/README.md @@ -25,9 +25,10 @@ Everything in a request is treated as hostile. Names are forced to a printable uppercase subset and cut to 12 characters. Every number must be an integer in range, and a score is refused if it could not have happened: assets above `$2.00 + $25 a day`, or glasses above 400 a day, are rejected as impossible for -the days claimed. Bodies are capped at 4 KB and submissions at 30 an hour per +the days claimed. Bodies are capped at 4 KB and submissions at 120 an hour per address, which is why `TRUST_PROXY=1` matters behind nginx — otherwise every -request looks like it came from the proxy. +request looks like it came from the proxy and one busy classroom would lock +everyone else out. **It cannot prove a score is real.** There are no accounts and no signing, so anyone willing to craft a request can post a plausible score under any name. diff --git a/server/src/index.ts b/server/src/index.ts index 9a132f9..1673f0a 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -53,7 +53,14 @@ interface Bucket { } const posts = new Map() const POST_WINDOW_MS = 60 * 60 * 1000 -const POST_LIMIT = 30 +/* + * Per address, per hour. Generous on purpose: a classroom, an office or a + * household all arrive from one address, and thirty was low enough that a + * class finishing a season together would have started losing scores to a + * 429. What actually keeps rubbish off the board is the plausibility check + * in validate.ts, not this - this only stops the database being hammered. + */ +const POST_LIMIT = 120 function overPostLimit(ip: string): boolean { const now = Date.now()