Lemonade Stand: browser recreation of the Atari 8-bit BASIC game
Faithful day loop - weather report, cost of lemonade rising over the summer, glasses/signs/price decisions, thunderstorms, heat waves and street crews - with the $$ LEMONSVILLE DAILY FINANCIAL REPORT $$ laid out the way it printed. The simulation is kept out of React so a season can be run headlessly: a careful player compounds $2.00 into roughly $22 over twelve days, while an all-in player goes broke about seven times in ten. Weather scenes are pixel rectangles in SVG and the music is a small chiptune engine on the Web Audio API, so there are no binary assets. The screen is locked to a 4:3 tube and never scrolls.
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import { useState } from 'react'
|
||||
import { WEATHER } from '../game/constants'
|
||||
import { dollars } from '../game/engine'
|
||||
import type { DayConditions, DayResult, Player } from '../game/types'
|
||||
import { Btn, Line } from './Crt'
|
||||
import { Scene } from './Scene'
|
||||
|
||||
function Row({ label, value, strong }: { label: string; value: string; strong?: boolean }) {
|
||||
return (
|
||||
<div className={`report-row ${strong ? 'strong' : ''}`}>
|
||||
<span className="report-label">{label}</span>
|
||||
<span className="report-dots" aria-hidden />
|
||||
<span className="report-value">{value}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ReportScreen({
|
||||
results,
|
||||
players,
|
||||
conditions,
|
||||
onNextDay,
|
||||
onRetire,
|
||||
onBlip,
|
||||
}: {
|
||||
results: DayResult[]
|
||||
players: Player[]
|
||||
conditions: DayConditions
|
||||
onNextDay: () => void
|
||||
onRetire: () => void
|
||||
onBlip: () => void
|
||||
}) {
|
||||
// App remounts this each day, so these start fresh without a reset effect.
|
||||
const [index, setIndex] = useState(0)
|
||||
// The storm gets its own screen before the books are opened.
|
||||
const [stormSeen, setStormSeen] = useState(false)
|
||||
|
||||
const r = results[index]
|
||||
const player = players.find((p) => p.id === r.playerId)!
|
||||
const isLast = index === results.length - 1
|
||||
const everyoneBroke = players.every((p) => p.bankrupt)
|
||||
|
||||
if (conditions.storm && !stormSeen) {
|
||||
return (
|
||||
<div className="stack">
|
||||
<Line className="center inv-line">DAY {conditions.day} IN LEMONSVILLE</Line>
|
||||
<Scene conditions={conditions} />
|
||||
<Line className="warn">A THUNDERSTORM HIT LEMONSVILLE EARLIER</Line>
|
||||
<Line className="warn">TODAY, JUST AS THE STANDS WERE BEING</Line>
|
||||
<Line className="warn">SET UP. EVERYTHING WAS RUINED!!</Line>
|
||||
<Line />
|
||||
<div className="row center">
|
||||
<Btn
|
||||
kind="primary"
|
||||
onClick={() => {
|
||||
onBlip()
|
||||
setStormSeen(true)
|
||||
}}
|
||||
>
|
||||
SEE THE DAMAGE
|
||||
</Btn>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const events = [
|
||||
WEATHER[conditions.weather].label,
|
||||
conditions.heatWave ? 'HEAT WAVE' : null,
|
||||
conditions.streetCrew ? 'STREET CREWS' : null,
|
||||
conditions.storm ? 'THUNDERSTORM' : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' \u00b7 ')
|
||||
|
||||
return (
|
||||
<div className="stack">
|
||||
<Line className="center inv-line">$$ LEMONSVILLE DAILY FINANCIAL REPORT $$</Line>
|
||||
|
||||
<Line />
|
||||
<Line className="center accent">
|
||||
DAY {conditions.day} — {player.name}
|
||||
</Line>
|
||||
<Line className="center dim">{events}</Line>
|
||||
<Line />
|
||||
<Row label="GLASSES SOLD" value={String(r.glassesSold)} />
|
||||
<Row label="PRICE PER GLASS" value={`${r.decision.price}¢`} />
|
||||
<Row label="INCOME" value={dollars(r.income)} />
|
||||
<Line />
|
||||
<Row label="GLASSES MADE" value={String(r.decision.glasses)} />
|
||||
<Row label="COST OF LEMONADE" value={dollars(r.lemonadeCost)} />
|
||||
<Row label="COST OF SIGNS" value={dollars(r.signCost)} />
|
||||
<Row label="EXPENSES" value={dollars(r.expenses)} />
|
||||
<Line />
|
||||
<Row label="PROFIT" value={dollars(r.profit)} strong />
|
||||
<Row label="ASSETS" value={dollars(r.assetsAfter)} strong />
|
||||
|
||||
{player.bankrupt && (
|
||||
<>
|
||||
<Line />
|
||||
<Line className="warn">{player.name}, YOU DO NOT HAVE ENOUGH</Line>
|
||||
<Line className="warn">MONEY LEFT TO STAY IN BUSINESS.</Line>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Line />
|
||||
<div className="row center">
|
||||
{!isLast && (
|
||||
<Btn
|
||||
kind="primary"
|
||||
onClick={() => {
|
||||
onBlip()
|
||||
setIndex(index + 1)
|
||||
}}
|
||||
>
|
||||
NEXT REPORT ({index + 2}/{results.length})
|
||||
</Btn>
|
||||
)}
|
||||
{isLast && !everyoneBroke && (
|
||||
<Btn kind="primary" onClick={onNextDay}>
|
||||
DAY {conditions.day + 1}
|
||||
</Btn>
|
||||
)}
|
||||
{isLast && (
|
||||
<Btn kind={everyoneBroke ? 'primary' : 'normal'} onClick={onRetire}>
|
||||
{everyoneBroke ? 'SEE FINAL STANDINGS' : 'RETIRE'}
|
||||
</Btn>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user