added docs
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
# 6. Frontend (`wizard_website`)
|
||||
|
||||
React 19 + TypeScript, built with Vite, styled with CSS modules, served by
|
||||
nginx. No router and no state library: the server state is the app state.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── main.tsx # mounts <App/> in StrictMode
|
||||
├── App.tsx # picks the screen, connection banner, toasts, URL sync
|
||||
├── App.module.css # design tokens (CSS variables) + global resets
|
||||
├── api/
|
||||
│ ├── types.ts # TypeScript mirror of the protocol and engine JSON
|
||||
│ └── useWizard.ts # THE connection hook: websocket, state, actions
|
||||
├── components/ # reusable, game-agnostic-ish UI
|
||||
│ ├── Button/ # variants primary | secondary | ghost | danger, sizes sm | md | lg
|
||||
│ ├── PlayingCard/ # PlayingCard (face) and CardBack
|
||||
│ ├── Modal/ # dialog with backdrop, Escape to close
|
||||
│ └── Toasts/ # error messages bottom right
|
||||
├── screens/
|
||||
│ ├── HomeScreen/ # name, create lobby, join by code
|
||||
│ ├── LobbyScreen/ # code, players, AI, options, start
|
||||
│ └── GameScreen/ # the table and its parts (see below)
|
||||
└── util/cards.ts # suit names, card labels, hand sorting, legal-action helpers
|
||||
```
|
||||
|
||||
Every component has its own `*.module.css` next to it.
|
||||
|
||||
## Data flow
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
WS[(WebSocket)] -- state / welcome / error --> H[useWizard]
|
||||
H -- lobby, game, completedTrick, toasts --> App
|
||||
App --> Home[HomeScreen]
|
||||
App --> Lobby[LobbyScreen]
|
||||
App --> Game[GameScreen]
|
||||
Home -- actions.createLobby / joinLobby --> H
|
||||
Lobby -- actions.addAI / startGame / … --> H
|
||||
Game -- actions.play({type:'bid'|'play'}) --> H
|
||||
H -- JSON messages --> WS
|
||||
```
|
||||
|
||||
Components never talk to the websocket directly. They receive data as props
|
||||
and call functions from `actions`.
|
||||
|
||||
## `useWizard` (`src/api/useWizard.ts`)
|
||||
|
||||
The only stateful piece of the app.
|
||||
|
||||
**State it exposes**
|
||||
|
||||
| Value | Meaning |
|
||||
|-------|---------|
|
||||
| `status` | `connecting` \| `open` \| `closed` (will retry) \| `replaced` (opened in another tab, no retry) |
|
||||
| `playerId` | from `welcome` |
|
||||
| `lobby`, `game` | exactly what the last `state` message contained |
|
||||
| `completedTrick` | the last `trick_completed` event, kept for 2.2 s so the trick stays visible |
|
||||
| `toasts`, `dismissToast` | error messages, auto-removed after 4 s |
|
||||
| `actions` | `createLobby`, `joinLobby`, `leaveLobby`, `addAI`, `removePlayer`, `setOptions`, `startGame`, `play(action)`, `returnToLobby` |
|
||||
|
||||
**Connection behaviour**
|
||||
|
||||
- URL: `VITE_WS_URL` if set at build time, otherwise `new URL('../api/ws',
|
||||
location.href)` with `ws:`/`wss:`. On
|
||||
`https://game.marvinkrausser.com/wizard/website/` this gives
|
||||
`wss://game.marvinkrausser.com/wizard/api/ws`; in development
|
||||
(`http://localhost:5173/`) it gives `ws://localhost:5173/api/ws`.
|
||||
- On open it sends `hello` with the token from `sessionStorage`
|
||||
(key `wizard.token`); the `welcome` reply stores the (possibly new) token.
|
||||
Because `sessionStorage` is per tab, a reload resumes the same seat, while a
|
||||
second tab is a different player.
|
||||
- On close it reconnects with exponential backoff (1, 2, 4, 8, 10, 10 … s),
|
||||
except after close code 4000.
|
||||
- `send()` shows a toast if the socket is not open.
|
||||
|
||||
## Screen selection (`App.tsx`)
|
||||
|
||||
| Condition | Screen |
|
||||
|-----------|--------|
|
||||
| no lobby (or no player id yet) | `HomeScreen` |
|
||||
| lobby `waiting` or no game | `LobbyScreen` |
|
||||
| otherwise | `GameScreen` |
|
||||
|
||||
`App` also keeps `?lobby=CODE` in the address bar while you are in a lobby
|
||||
(so the URL is an invite link) and removes it when you are not. A red banner
|
||||
shows the connection status when it is not `open`.
|
||||
|
||||
## Screens
|
||||
|
||||
### HomeScreen
|
||||
|
||||
- Name input (max 20 chars), remembered in `localStorage` (`wizard.name`).
|
||||
- "Create lobby" → `actions.createLobby(name)`.
|
||||
- Code input + "Join" → `actions.joinLobby(code, name)`. The code is prefilled
|
||||
from `?lobby=` so invite links work.
|
||||
- Buttons are disabled while disconnected or without a name.
|
||||
|
||||
### LobbyScreen
|
||||
|
||||
- Big lobby code with "Copy code" / "Copy invite link" (clipboard API).
|
||||
- Player list with badges (Host, AI, offline, "(you)") and empty "Free seat"
|
||||
rows up to 6.
|
||||
- Host only: "+ Add AI player", "Remove" per player, the "Hidden bids"
|
||||
checkbox, and "Start game" (disabled with "Need N more players" until 3
|
||||
seats are filled). Others see the options read-only and "Waiting for the
|
||||
host…".
|
||||
- Shows a warning if AI players are present but the AI service is offline.
|
||||
- Explains the fixed bidding rule (bids may not add up to the cards in hand).
|
||||
|
||||
### GameScreen (`screens/GameScreen/`)
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Round 3 / 20 Lobby 7C7TAJ [Scoreboard] [Leave]│ top bar
|
||||
│ ┌ Round 2 complete ┐ │ RoundSummary (while bidding)
|
||||
│ [PlayerSeat] [PlayerSeat] … │ opponents in play order
|
||||
│ ┌────────────────────────────────────────────────────────┐ │
|
||||
│ │ [trump card] [TrickArea: cards + owner names] │ │ table
|
||||
│ │ Trump: Blue │ │
|
||||
│ └────────────────────────────────────────────────────────┘ │
|
||||
│ Your turn to bid │ status line
|
||||
│ [BidPanel 0 1 2 3] │ only when you bid
|
||||
│ [your PlayerSeat] [Hand: clickable cards] │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
| Part | File | Details |
|
||||
|------|------|---------|
|
||||
| `GameScreen` | `GameScreen.tsx` | layout, status text, trump display, which panels/modals to show |
|
||||
| `PlayerSeat` | `PlayerSeat.tsx` | name, AI/dealer(D)/offline chips, score, "tricks / bid" (green when on target, red when over), card backs for the hand size; gold glow when it is that player's turn |
|
||||
| `TrickArea` | `TrickArea.tsx` | cards of the current trick with owner names; right after a trick is complete it shows the completed trick with the winner highlighted and "X wins the trick" |
|
||||
| `Hand` | `Hand.tsx` | your cards sorted (wizards, trump suit, other suits, jesters); clickable only on your turn in the playing phase; illegal cards are dimmed and disabled |
|
||||
| `BidPanel` | `BidPanel.tsx` | buttons 0…round number; illegal bids disabled; shows the running sum of bids and explains a forbidden value |
|
||||
| `RoundSummary` | `RoundSummary.tsx` | result of the previous round (tricks/bid and score change per player) |
|
||||
| `Scoreboard` | `Scoreboard.tsx` | modal "block of truth": every round with total, change and tricks/bid, plus the current round |
|
||||
| `GameOver` | `GameOver.tsx` | ranking, winner, "Back to lobby" (host), "Leave", "Scoreboard" |
|
||||
|
||||
Derived display logic worth knowing (all in `GameScreen.tsx`):
|
||||
|
||||
- **Completed trick**: `TrickArea` shows `completedTrick` while the table is
|
||||
empty and the 2.2 s timer has not expired.
|
||||
- **Trump display**: normally `observation.trump_card`. While the last trick of
|
||||
the *previous* round is still displayed (`completedTrick.round_number !==
|
||||
round_number`), the trick's own `trump_card` is shown instead, so the table
|
||||
never shows a trick next to the wrong trump. The trump suit is derived with
|
||||
the same rule as the engine (only number cards define a suit), and the label
|
||||
says "Wizard turned up — no trump", "Jester turned up — no trump" or
|
||||
"Last round — no trump" when there is none.
|
||||
- **Round summary**: shown while `phase === 'bidding'` and the last entry of
|
||||
`round_history` is the previous round — i.e. from the start of a round until
|
||||
the last bid is placed. Derived from server state, so it survives reloads.
|
||||
- **Leaving** asks for confirmation (an AI takes the seat mid-game).
|
||||
|
||||
## Styling
|
||||
|
||||
- **CSS modules only.** Each component imports `styles` from its
|
||||
`*.module.css`; class names are scoped automatically.
|
||||
- **Design tokens** are CSS variables on the `.app` root element in
|
||||
`App.module.css` (`--bg`, `--surface`, `--accent`, `--gold`, `--suit-red`, …).
|
||||
All components use them, so the theme can be changed in one place.
|
||||
- The only global rules (`box-sizing`, `body { margin: 0 }`) are written with
|
||||
`:global(...)` inside `App.module.css`.
|
||||
- Responsive breakpoint: 720 px (cards overlap in the hand, the round summary
|
||||
moves into the page flow, the table stacks vertically).
|
||||
- Card corners are **not** rotated, so 6 and 9 cannot be confused.
|
||||
|
||||
## Build and configuration
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `vite.config.ts` | `base: './'` → relative asset URLs, so the build works under any path prefix; dev server proxies `/api/*` to `WIZARD_SERVER_URL` (default `http://localhost:8000`) and strips `/api`, like Caddy does in production |
|
||||
| `tsconfig.app.json` | strict TypeScript for `src/` (no unused locals/params, erasable syntax only → no enums) |
|
||||
| `eslint.config.js` | typescript-eslint + react-hooks + react-refresh |
|
||||
| `Dockerfile` | `node:22-alpine` runs `npm ci && npm run build` (optional build arg `VITE_WS_URL`), then `nginx:1.27-alpine` serves `dist/` |
|
||||
| `nginx.conf` | listens on **8082**; `/assets/` cached forever (hashed names), everything else falls back to `index.html` with `no-cache` |
|
||||
|
||||
Commands (in `wizard_website/`):
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev # http://localhost:5173
|
||||
npm run build # type-check (tsc -b) + production build into dist/
|
||||
npm run lint
|
||||
```
|
||||
|
||||
## Keeping types in sync
|
||||
|
||||
`src/api/types.ts` mirrors:
|
||||
|
||||
- `wizard_server/app/protocol.py` (client messages),
|
||||
- `Lobby.view()` / `Lobby.state_for()` in `wizard_server/app/lobby.py` (server
|
||||
messages),
|
||||
- the `to_dict()` methods in `wizard_game` (cards, actions, observation,
|
||||
events).
|
||||
|
||||
When you change one of those, update `types.ts`; `npm run build` then shows
|
||||
every place in the UI that needs to follow.
|
||||
Reference in New Issue
Block a user