diff --git a/CLAUDE.md b/CLAUDE.md index 541da27..14a4ebe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,11 +43,16 @@ Navigation is a simple `useState` state machine in `App.tsx` — no router |------|-----|-------------| | Title screen | `'title'` | Shown on first load (no saved game). "New Game" button navigates to setup. | | Player setup | `'setup'` | Enter 3–6 player names, then "Start" saves the game and goes to scoresheet. | -| Score sheet | `'scoresheet'` | Shows current scores. "Score Round" → enter scores; "New Game" → clears state, back to title. | +| Score sheet | `'scoresheet'` | Shows per-round scores and running totals. Small red "New Game" button (top-right) opens a confirmation dialog before clearing state and going to setup. "Score Round" (footer) → enter scores. | | Enter scores | `'enterscores'` | Form to enter each player's score for the round. "Submit" → back to scoresheet. | On mount, `App.tsx` checks `localStorage` for a saved game and jumps straight to `'scoresheet'` if found. +## Game rules + +- Each round every player scores some points; all players' scores in a round must sum to **-110** +- Each player starts at 0; the game ends when any player reaches **-200** + ## Data model ```ts @@ -61,6 +66,13 @@ interface GameState { Persisted to `localStorage` under the key `currentGame`. No game history yet — only one game at a time. +## Scoring logic + +Pure functions live in `src/game/scoring.ts` (no React imports): + +- `computeRunningTotals(rounds)` — returns `totals[i][j]`, the cumulative score for player `j` after round `i` +- `isGameOver(totals)` — true if any player's total in the last round is ≤ -200 + ## Design system ### Layout @@ -72,8 +84,10 @@ Persisted to `localStorage` under the key `currentGame`. No game history yet — ### PageFrame component -Shared layout for non-title pages. Props: `title`, `children`, `footer?`. Renders: +Shared layout for non-title pages. Props: `title`, `children`, `footer?`, `headerAction?`, `overlay?`. Renders: - Fixed-position card with header (h1 + diamond rule divider), scrollable content area, optional sticky footer +- `headerAction` — rendered absolutely in the top-right of the header (used for the scoresheet's "New Game" button) +- `overlay` — rendered `position: fixed; inset: 0` over the full viewport with a dim backdrop; the caller provides the dialog box content centered inside it Diamond rule: CSS `::before` pseudo-element with `content: '◆'` centered on a thin border line. Color `#c8b99a`. @@ -91,10 +105,10 @@ Diamond rule: CSS `::before` pseudo-element with `content: '◆'` centered on a ### Buttons -All buttons share the same press animation: `translateY(2px)` + shadow reduction, 80ms ease transition. +Shadow and press animation are on the global `button` selector in `src/index.css` — every ` + ) + + const confirmOverlay = ( +
+

End this game?

+
+ + +
+
+ ) + return ( - - - + Score Round} + > +
+
+
+ {game.players.map((name, j) => ( +
{name}
+ ))} + + {game.rounds.map((roundScores, i) => ( + +
Round {i + 1}
+ {roundScores.map((score, j) => ( +
{score}
+ ))} + + {i > 0 && ( + <> +
+ {totals[i].map((total, j) => ( +
{total}
+ ))} + + )} + + ))} +
+
) -} +} \ No newline at end of file diff --git a/src/styles/shared.module.css b/src/styles/shared.module.css index 9b3319f..62b5c6b 100644 --- a/src/styles/shared.module.css +++ b/src/styles/shared.module.css @@ -7,13 +7,3 @@ flex-direction: column; overflow: hidden; } - -.pressable { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); - transition: transform 80ms ease, box-shadow 80ms ease; -} - -.pressable:active:not(:disabled) { - transform: translateY(2px); - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); -}