Files
2026-05-31 20:05:18 -05:00

118 lines
5.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 38 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; scores must sum to **-95** (3 players), **-110** (46 players), or **-100** (78 players)
- Each player starts at 0; the game ends when any player reaches **-200**
## Data model
```ts
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)` — 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
- Full-window gradient background: `linear-gradient(to bottom, #a8d5e8, #4a9e6a)` on `body`
- All pages have a rounded card (`border-radius: 2rem`, `border: 2px solid black`) inset `1rem` from window edges
- **Title screen**: card background is transparent (gradient shows through)
- **All other pages**: card background is parchment `#f5f0e4` via `PageFrame`
### 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` — 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`.
### 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)` via `button:active:not(:disabled)`
- **`PrimaryButton`**: full-width, blue, `border-radius: 1rem`, `font-size: 1.25rem`, `font-weight: 600`. Accepts `disabled` prop — 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-serif` everywhere (a real web font will be chosen later)
- Global `button` base: `min-height: 2.75rem`, `font-size: 1rem`, `font-family: inherit`