Add a persistent high score table

Retiring writes each player's closing balance to a top-ten table in
localStorage, reachable from the title screen and the standings, with
the summer just finished picked out in yellow.

Stored rows are user-editable, so every field is validated on load and
malformed entries are dropped. Storage being unavailable is a normal
case, not an error: the game plays on with an empty table.

Also stops the status line claiming DAY 00 on screens that have no day.
This commit is contained in:
2026-09-08 15:29:35 -07:00
parent 08b2cc5391
commit 71cbb02353
9 changed files with 280 additions and 5 deletions
+3
View File
@@ -11,11 +11,13 @@ export function GameOverScreen({
history,
days,
onRestart,
onScores,
}: {
players: Player[]
history: DayResult[]
days: number
onRestart: () => void
onScores: () => void
}) {
const ranked = [...players].sort((a, b) => b.assets - a.assets)
const best = history.reduce<DayResult | null>(
@@ -69,6 +71,7 @@ export function GameOverScreen({
<Btn kind="primary" onClick={onRestart}>
PLAY AGAIN
</Btn>
<Btn onClick={onScores}>HIGH SCORES</Btn>
</div>
</div>
)
+78
View File
@@ -0,0 +1,78 @@
import { useState } from 'react'
import { dollars } from '../game/engine'
import { MAX_SCORES, type Score } from '../game/highscores'
import { Btn, Line } from './Crt'
const shortDate = (at: number) =>
new Date(at).toLocaleDateString(undefined, { day: '2-digit', month: 'short' }).toUpperCase()
export function HighScoreScreen({
scores,
highlight,
onBack,
onClear,
onBlip,
}: {
scores: Score[]
highlight: string[]
onBack: () => void
onClear: () => void
onBlip: () => void
}) {
const [confirming, setConfirming] = useState(false)
const fresh = new Set(highlight)
return (
<div className="stack">
<Line className="center inv-line">$$ BEST STANDS IN LEMONSVILLE $$</Line>
<Line />
{scores.length === 0 ? (
<>
<Line className="center dim">NO STANDS HAVE CLOSED THEIR BOOKS YET.</Line>
<Line />
<Line className="center dim">RETIRE AT THE END OF A SUMMER TO</Line>
<Line className="center dim">TAKE A PLACE ON THIS LIST.</Line>
</>
) : (
scores.map((s, i) => (
<div className={`report-row score-row ${fresh.has(s.id) ? 'is-new' : ''}`} key={s.id}>
<span className="report-label">
{String(i + 1).padStart(2, ' ')}. {s.name}
{s.broke ? ' (BROKE)' : ''}
</span>
<span className="report-dots" aria-hidden />
<span className="score-days dim">
{s.days}D &middot; {shortDate(s.at)}
</span>
<span className="report-value money">{dollars(s.assets)}</span>
</div>
))
)}
<Line />
<Line className="center dim">TOP {MAX_SCORES}, KEPT IN THIS BROWSER.</Line>
<Line />
<div className="row center">
<Btn kind="primary" onClick={onBack}>
BACK
</Btn>
{scores.length > 0 && (
<Btn
onClick={() => {
onBlip()
if (confirming) {
onClear()
setConfirming(false)
} else {
setConfirming(true)
}
}}
>
{confirming ? 'REALLY WIPE?' : 'WIPE TABLE'}
</Btn>
)}
</div>
</div>
)
}
+3
View File
@@ -6,10 +6,12 @@ const BANNER = bannerRows('LEMONADE')
export function TitleScreen({
onStart,
onInstructions,
onScores,
seed,
}: {
onStart: () => void
onInstructions: () => void
onScores: () => void
seed: number
}) {
return (
@@ -29,6 +31,7 @@ export function TitleScreen({
START
</Btn>
<Btn onClick={onInstructions}>INSTRUCTIONS</Btn>
<Btn onClick={onScores}>HIGH SCORES</Btn>
</div>
</div>
)