jcoffey-dev 8a5d0cb6df Shared leaderboard, a games hub, AGPL, and two new street conditions
The high score table moves out of the browser and into a service, so
every player is on one board. It is a Node process with SQLite, no build
step and no native modules - node:sqlite ships with the runtime and Node
runs the TypeScript directly.

Everything in a request is treated as hostile: names forced to a
printable subset, every number range-checked, and scores refused if they
could not have happened in the days claimed. Bodies capped, submissions
rate limited per address. It still cannot prove a score is real, and
server/README.md says so plainly rather than implying otherwise.

The game degrades properly without it: the board says it cannot reach
town, posting happens behind the closing standings, and play is
untouched.

Deployment is three containers behind the host's nginx - the hub at /,
the game at /lemonade/, the scores service at /api/ - so the game keeps
its own container and a second game is just another service.

Licensing: AGPL-3.0-or-later, Affero because the leaderboard is a network
service. NOTICE.md credits Bob Jamison and Charlie Kellner, records that
this is clean-room work, and is honest about the one thing it is not:
some on-screen wording is quoted from the original and cannot be
licensed by us.

The street can now get better as well as worse. The summer fair brings
the town out and lifts what they will pay; a rival on the next corner
takes a share and lingers. Both show in the art and in the crowd. Over
500 seasons a player who reads the briefing survives every time and
finishes around $25.80; one who ignores it goes broke 70% of the time.
2026-09-08 16:17:47 -07:00

Lemonade Stand

A browser recreation of the Atari 8-bit BASIC classic — the one you typed in, ran on a TV, and lost two dollars to on a cloudy day.

It ships in two skins over one game. 1979 is the machine as it was: Atari blue, block letters, scanlines, chiptune. Remaster is the same rules with cel-shaded art, a crowd that turns up in proportion to trade, and a funk band. The rules never differ between them — only the paint. Switch any time with the button under the set.

React + TypeScript + Vite, and not one binary asset: every pixel is SVG drawn in code and every note is generated by a small synth on the Web Audio API.

Playing

npm install
npm run dev

It loads from cassette first, because that is how it arrived. Pressing PLAY on the recorder is also what unlocks the audio — browsers want a gesture before they will make a sound, and that happens to be exactly the gesture the game already wanted.

One to four players share the keyboard, hot-seat style. Each morning you see the weather, then decide three things: how many glasses to make, how many 15-cent signs to put up, and what to charge. Then the town votes with its pocket money.

  • Lemonade costs 2 cents a glass on days 12, 4 cents through day 7, and 5 cents after that.
  • Everything you make is made today. Unsold glasses are poured away.
  • A hot, dry day brings a bigger crowd that will pay more. A heat wave is better still.
  • A cloudy day can turn into a thunderstorm, and a thunderstorm ruins every glass you made.
  • Street crews close the road now and then, and they tend to stay for a second day.
  • The summer fair brings half the town past your stand, in a spending mood.
  • A rival sometimes sets up on the next corner and takes a share of the street until they get bored and move on.
  • Signs work, but with sharp diminishing returns — the fourth one barely earns its 15 cents.

Once the prices are in, the stand trades for a few seconds before the books open: the crowd arrives at the rate you actually sold, the tally climbs, and the till fills. It can be skipped, but the day is worth watching.

You are broke when you cannot afford a single glass. Otherwise the summer runs as long as you like; RETIRE closes the books and shows the standings.

High scores

One board, shared by everybody. Retiring posts every player's closing balance to the scores service, and the top 50 comes back ranked best first — ties break on the shorter season, then the earlier date. Runs from the summer you just finished are picked out in yellow.

There is no way to clear it. A stand leaves the list only by being pushed off the bottom by a better one, so a good summer stands until somebody beats it.

The board lives in server/, not in the browser, so it is the one part of the game that needs a line out. With the service unreachable the game plays exactly as normal and the board says so plainly rather than breaking; posting happens behind the closing standings, so a slow network never holds up the end of a season. See server/README.md for the API, the validation and what a board with no accounts can and cannot promise.

How it is put together

src/
  game/       pure simulation - no React, no DOM
    constants.ts  costs, weather profiles, the demand curve
    engine.ts     rolls each day and settles the takings
    reducer.ts    the day/turn state machine
    rng.ts        seeded mulberry32, so a run can be replayed
    highscores.ts the persisted table, with validation on load
  audio/
    synth.ts      pulse voices, a drum kit, filters, look-ahead sequencer
    tunes.ts      the 1979 chiptunes
    funk.ts       the remaster's band
  skin.ts       which of the two skins is on, and where it is kept
  components/
    PixelScene    the 1979 artwork, in chunky rectangles
    CelScene      the remaster's artwork, and the people on the street
    Scene         picks between them, so no screen needs to care

The simulation is deliberately kept out of React: engine.ts takes a player, a decision and a day and hands back a DayResult. That is what makes it possible to run a few hundred seasons in a script and check that a careful player grows their two dollars while a reckless one goes broke about seven times in ten.

The demand curve

The original BASIC listing is not reproduced line for line. constants.ts holds a reconstruction tuned to behave the way the game plays: cheap lemonade sells out, sales fall to nothing once the price stops being a bargain, heat lifts both the crowd and the price people will tolerate, and signs help a lot at first and hardly at all later.

The television

An Atari 800 fed a 4:3 television, and a television has no scrollbar. The tube is locked to 4:3 and everything inside it is sized in container units, so the picture stays in proportion at any size. If a page ever did come out taller than the tube, Fit scales it down rather than clipping or scrolling it.

The people on the street

How many figures walk on is driven by trade, not decoration. Before you have priced anything it comes from the forecast and the day's conditions — a hot day or the fair fills the pavement, road works and a rival empty it, a downpour leaves one soul hurrying past under an umbrella. Once the day has traded it comes from the glasses that actually crossed the counter, so the report shows you the crowd you earned. In a heat wave they fan themselves.

Sound

Audio starts on the first gesture, as browsers require. The 1979 skin gets the chiptune: two pulse voices, a triangle bass and filtered noise, with real Fourier-built duty cycles rather than a plain square.

The remaster gets a band. Same engine, more of it — a sine kick with a pitch envelope, a snare with body under the crack, sixteenth hats, and a sawtooth bass through a sweeping resonant lowpass, which is where the funk actually lives. Patterns are written in sixteenths with a light shuffle, and chords are 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

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.

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.

Deploying

Three containers behind the host's nginx, which does TLS and routing and serves nothing off disk:

Path Container What it is
/ games-hub the list of games
/lemonade/ lemonade-web this game, a static bundle
/api/ lemonade-scores the shared leaderboard
docker compose -f deploy/compose.yml up -d --build

deploy/nginx-host.conf.example is the host-side server block. The game's asset paths are baked in at build time, so BASE_PATH in the compose file has to match the path nginx proxies it to. Only the scores container writes anything: a named volume holding the SQLite database, which is the one piece of state that must survive a redeploy.

Licence

AGPL-3.0-or-later — see LICENSE.

Affero rather than plain GPL because the leaderboard is a network service: anyone running a modified copy of it for other people has to offer them the source. Running the game on its own imposes nothing extra.

The simulation, the artwork and the music are original work. The game's rules are not anyone's property, and some of the on-screen wording is quoted from the 1979 original as homage rather than authored here. NOTICE.md sets out exactly what came from where, and credits the people who wrote it first.

S
Description
Imported from github.com during the 2026-09-20 standup (local dir: lemonade)
Readme AGPL-3.0
163 KiB
Languages
TypeScript 79.7%
CSS 15.5%
HTML 4.3%
Dockerfile 0.5%