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.
This commit is contained in:
2026-09-08 17:58:40 -07:00
parent f2e084ef7a
commit 87474ad5ec
3 changed files with 28 additions and 10 deletions
+17 -7
View File
@@ -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 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. 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 As many as you like. There is no limit, and no meaningful sense in which
on two machines, two browsers or two profiles play completely independent players share anything while they are playing.
games — different seeds, different weather, different books. Nothing about a
season is shared or synchronised.
The only thing they have in common is the leaderboard they both post to at the The game holds no state on the server — it is a static bundle, and a season
end. The skin preference is the one thing still kept in the browser. 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 ## Running it anywhere
+3 -2
View File
@@ -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 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 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 `$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 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 **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. anyone willing to craft a request can post a plausible score under any name.
+8 -1
View File
@@ -53,7 +53,14 @@ interface Bucket {
} }
const posts = new Map<string, Bucket>() const posts = new Map<string, Bucket>()
const POST_WINDOW_MS = 60 * 60 * 1000 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 { function overPostLimit(ip: string): boolean {
const now = Date.now() const now = Date.now()