The table was clearable behind a confirm. It should not be: a stand earns its place and loses it only to a better summer, so the control and clearScores() are gone and the screen says so.
4.0 KiB
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.
React + TypeScript + Vite. No art assets and no audio files: the weather is drawn as pixel rectangles in SVG, and the music is generated by a small chiptune engine built on the Web Audio API.
Playing
npm install
npm run dev
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 1–2, 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.
- Signs work, but with sharp diminishing returns — the fourth one barely earns its 15 cents.
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
Retiring writes every player's closing balance to a top-ten table kept in
localStorage, reachable from the title screen and from the standings. Runs
from the summer you just finished are picked out in yellow. Ties break on the
shorter season, then on the earlier date.
There is no way to clear the table. 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 table is per-browser, not per-device, and it is the one piece of state the game keeps between visits. Anything already in storage can be edited by hand, so every field is validated on the way back in and malformed rows are dropped rather than trusted. If storage is unavailable — a private window, or a browser set to block site data — the game plays normally and the table simply stays empty.
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-wave voices, noise percussion, look-ahead sequencer
tunes.ts the title, trading and closing themes
components/ the screens, and the CRT they are drawn on
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.
Sound
Audio starts on the first click or key press, as browsers require. Two pulse
voices, a triangle bass and filtered white noise for percussion, driven by a
scheduler that queues notes 200 ms ahead so the music does not stutter when
React re-renders. MUSIC and SOUND toggle independently.