- Add src/game/scoring.ts with computeRunningTotals and isGameOver - Build ScoreSheet grid: player columns, per-round scores, running totals after round 2+ - Small red "New Game" button in header top-right with full-screen confirmation overlay - Extend PageFrame with headerAction and overlay props - Move press animation/shadow from .pressable class to global button selector Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4.9 KiB
Black 7
Scoring app for the card game Last Panther, hosted at black7.org.
What this is
A mobile-first progressive web app (PWA) built with React and TypeScript. No backend — everything runs in the browser. Players use it to track scores during a game of Last Panther.
Tech stack
- React (UI)
- TypeScript (strict mode)
- PWA (installable, offline-capable)
- No server, no API — browser-only
Key constraints
- Mobile-first: design and test for phone-sized screens first; desktop is secondary
- No backend: all state lives in the browser (localStorage, sessionStorage, or in-memory)
- Installable: must meet PWA requirements (manifest, service worker, HTTPS)
Running the app
npm install
npm run dev # start dev server
npm run build # production build
Project conventions
- Components live in
src/components/ - Game logic (scoring rules, state) lives in
src/game/ - Types shared across the app live in
src/types.ts - Prefer small, focused components; keep game logic out of UI components
- No unnecessary comments — name things clearly instead
Pages and navigation
Navigation is a simple useState<Page> state machine in App.tsx — no router.
| Page | Key | Description |
|---|---|---|
| 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 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
type Page = 'title' | 'setup' | 'scoresheet' | 'enterscores'
interface GameState {
players: string[] // display names, edge-trimmed
rounds: number[][] // rounds[i][j] = player j's score in round i
}
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)— returnstotals[i][j], the cumulative score for playerjafter roundiisGameOver(totals)— true if any player's total in the last round is ≤ -200
Design system
Layout
- Full-window gradient background:
linear-gradient(to bottom, #a8d5e8, #4a9e6a)onbody - All pages have a rounded card (
border-radius: 2rem,border: 2px solid black) inset1remfrom window edges - Title screen: card background is transparent (gradient shows through)
- All other pages: card background is parchment
#f5f0e4viaPageFrame
PageFrame component
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— renderedposition: fixed; inset: 0over 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.
Colors
| Token | Value | Usage |
|---|---|---|
| Blue | #a8d5e8 |
Primary button background, gradient top |
| Green | #4a9e6a |
Gradient bottom |
| Parchment | #f5f0e4 |
Page card background |
| Parchment border | #c8b99a |
Input borders, diamond rule |
| Error red | #c0392b |
Invalid field border |
| Error bg | #fff5f5 |
Invalid field background |
| Remove red | #d94f4f |
Remove-player button background |
Buttons
Shadow and press animation are on the global button selector in src/index.css — every <button> gets them automatically.
- Resting shadow:
0 4px 12px rgba(0,0,0,0.25) - Pressed shadow:
0 2px 6px rgba(0,0,0,0.2)viabutton:active:not(:disabled) PrimaryButton: full-width, blue,border-radius: 1rem,font-size: 1.25rem,font-weight: 600. Acceptsdisabledprop — disabled state:opacity: 0.45, no shadow,cursor: not-allowed, no press animation.- Round buttons: circular,
border: none. Add button:3rem, blue. Remove button:2.5rem, red.
Typography
font-family: sans-serifeverywhere (a real web font will be chosen later)- Global
buttonbase:min-height: 2.75rem,font-size: 1rem,font-family: inherit