Add game-over state and pre-populate names on new game

Highlights the winning player's column in green when the game ends,
changes the footer button to "New Game" (reusing the existing confirm
dialog), and pre-populates player names from the previous game when
starting a new one.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-28 21:52:10 -05:00
parent 9201273dc4
commit e5fa0ae66f
4 changed files with 20 additions and 7 deletions
+3 -1
View File
@@ -23,8 +23,10 @@ function clearGame() {
export default function App() { export default function App() {
const [game, setGame] = useState<GameState | null>(loadGame) const [game, setGame] = useState<GameState | null>(loadGame)
const [page, setPage] = useState<Page>(() => (game ? 'scoresheet' : 'title')) const [page, setPage] = useState<Page>(() => (game ? 'scoresheet' : 'title'))
const [previousPlayers, setPreviousPlayers] = useState<string[] | undefined>(undefined)
function handleNewGame() { function handleNewGame() {
setPreviousPlayers(game?.players)
clearGame() clearGame()
setGame(null) setGame(null)
setPage('setup') setPage('setup')
@@ -45,7 +47,7 @@ export default function App() {
} }
if (page === 'title') return <TitleScreen onNewGame={handleNewGame} /> if (page === 'title') return <TitleScreen onNewGame={handleNewGame} />
if (page === 'setup') return <PlayerSetup onStart={handleStart} /> if (page === 'setup') return <PlayerSetup onStart={handleStart} initialNames={previousPlayers} />
if (page === 'scoresheet') return <ScoreSheet game={game!} onScoreRound={() => setPage('enterscores')} onNewGame={handleNewGame} /> if (page === 'scoresheet') return <ScoreSheet game={game!} onScoreRound={() => setPage('enterscores')} onNewGame={handleNewGame} />
if (page === 'enterscores') return <EnterScores game={game!} onSubmit={handleSubmitScores} /> if (page === 'enterscores') return <EnterScores game={game!} onSubmit={handleSubmitScores} />
} }
+3 -2
View File
@@ -5,14 +5,15 @@ import styles from './PlayerSetup.module.css'
interface Props { interface Props {
onStart: (players: string[]) => void onStart: (players: string[]) => void
initialNames?: string[]
} }
function normalize(s: string) { function normalize(s: string) {
return s.trim().replace(/\s+/g, ' ') return s.trim().replace(/\s+/g, ' ')
} }
export default function PlayerSetup({ onStart }: Props) { export default function PlayerSetup({ onStart, initialNames }: Props) {
const [names, setNames] = useState<string[]>(['', '', '']) const [names, setNames] = useState<string[]>(initialNames ?? ['', '', ''])
function updateName(i: number, value: string) { function updateName(i: number, value: string) {
setNames(n => n.map((v, idx) => (idx === i ? value : v))) setNames(n => n.map((v, idx) => (idx === i ? value : v)))
+4
View File
@@ -49,6 +49,10 @@
border-top: 1px solid var(--color-parchment-border); border-top: 1px solid var(--color-parchment-border);
} }
.winnerCell {
background: rgba(74, 158, 106, 0.15);
}
/* Small red "New Game" button in header top-right */ /* Small red "New Game" button in header top-right */
.newGameBtn { .newGameBtn {
background: #d94f4f; background: #d94f4f;
+10 -4
View File
@@ -16,6 +16,8 @@ export default function ScoreSheet({ game, onScoreRound, onNewGame }: Props) {
const totals = computeRunningTotals(game.rounds) const totals = computeRunningTotals(game.rounds)
const gameOver = isGameOver(totals) const gameOver = isGameOver(totals)
const lastTotals = totals[totals.length - 1] ?? []
const winnerIndex = gameOver ? lastTotals.indexOf(Math.max(...lastTotals)) : -1
const newGameButton = ( const newGameButton = (
<button className={styles.newGameBtn} onClick={() => setConfirmingNewGame(true)}> <button className={styles.newGameBtn} onClick={() => setConfirmingNewGame(true)}>
@@ -42,7 +44,11 @@ export default function ScoreSheet({ game, onScoreRound, onNewGame }: Props) {
title="Score Sheet" title="Score Sheet"
headerAction={newGameButton} headerAction={newGameButton}
overlay={confirmingNewGame ? confirmOverlay : undefined} overlay={confirmingNewGame ? confirmOverlay : undefined}
footer={<PrimaryButton onClick={onScoreRound} disabled={gameOver}>Score Round</PrimaryButton>} footer={
<PrimaryButton onClick={gameOver ? () => setConfirmingNewGame(true) : onScoreRound}>
{gameOver ? 'New Game' : 'Score Round'}
</PrimaryButton>
}
> >
<div className={styles.tableWrapper}> <div className={styles.tableWrapper}>
<div <div
@@ -51,21 +57,21 @@ export default function ScoreSheet({ game, onScoreRound, onNewGame }: Props) {
> >
<div className={styles.labelCell} /> <div className={styles.labelCell} />
{game.players.map((name, j) => ( {game.players.map((name, j) => (
<div key={j} className={styles.headerCell}>{name}</div> <div key={j} className={styles.headerCell + (j === winnerIndex ? ' ' + styles.winnerCell : '')}>{name}</div>
))} ))}
{game.rounds.map((roundScores, i) => ( {game.rounds.map((roundScores, i) => (
<Fragment key={i}> <Fragment key={i}>
<div className={styles.labelCell}>Round {i + 1}</div> <div className={styles.labelCell}>Round {i + 1}</div>
{roundScores.map((score, j) => ( {roundScores.map((score, j) => (
<div key={j} className={styles.scoreCell}>{score}</div> <div key={j} className={styles.scoreCell + (j === winnerIndex ? ' ' + styles.winnerCell : '')}>{score}</div>
))} ))}
{i > 0 && ( {i > 0 && (
<> <>
<div className={`${styles.labelCell} ${styles.divider}`} /> <div className={`${styles.labelCell} ${styles.divider}`} />
{totals[i].map((total, j) => ( {totals[i].map((total, j) => (
<div key={j} className={`${styles.totalCell} ${styles.divider}`}>{total}</div> <div key={j} className={styles.totalCell + ' ' + styles.divider + (j === winnerIndex ? ' ' + styles.winnerCell : '')}>{total}</div>
))} ))}
</> </>
)} )}