fix spec review issues: bingo scoring, data model, edge cases

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 08:49:43 +01:00
parent 142455cdb8
commit cdf878fe9b

View File

@@ -57,7 +57,10 @@ Replaces the current `RoomHeader` component.
``` ```
- **Left:** "I❤ESC" as the app title (text, not emoji — the heart is the Unicode character ❤️) - **Left:** "I❤ESC" as the app title (text, not emoji — the heart is the Unicode character ❤️)
- **Right:** Room code (monospace, bold) + connection status dot (green/yellow/red) - **Right:** Room code (monospace, bold) + connection status dot
- Green = `connected`
- Yellow = `connecting`
- Red = `disconnected`
- `position: sticky; top: 0; z-index: 50` - `position: sticky; top: 0; z-index: 50`
- `padding-top: env(safe-area-inset-top)` for iPhone notch/Dynamic Island - `padding-top: env(safe-area-inset-top)` for iPhone notch/Dynamic Island
- Act badge removed — the game content makes the current phase obvious - Act badge removed — the game content makes the current phase obvious
@@ -135,26 +138,46 @@ Shows the player's current bingo card. Same `BingoCard` component as today, with
When a player completes a bingo line: When a player completes a bingo line:
1. **Server detects completion** — the server already detects bingo lines and awards 10 bonus points. Extended to mark the card as `completed`. 1. **Server detects completion** — the server already detects bingo lines and awards 10 bonus points. Extended to mark the card as `completed`.
2. **Card stored** — the completed card moves to a `completedBingoCards` array in game state (per player). Contains: `playerId`, `displayName`, `card` (the full card data), `completedAt` (timestamp). 2. **Card stored** — the completed card moves to a `completedBingoCards` array in game state (per player). Contains: `playerId`, `displayName`, `card` (the full card data with marked squares), `completedAt` (ISO 8601 timestamp).
3. **Player sees** — a celebration message ("Bingo!") + "Draw New Card" button. 3. **Player sees** — a celebration message ("Bingo!") + "Draw New Card" button.
4. **Redraw** — new WS message `request_new_bingo_card`. Server generates a fresh card with different tropes. The old card is already in `completedBingoCards`. 4. **Redraw** — new WS message `request_new_bingo_card`. Server generates a fresh card with tropes not present on the just-completed card (best effort — if the trope pool is exhausted, duplicates are allowed). The old card is already in `completedBingoCards`.
5. **New card**`myBingoCard` in game state updates to the fresh card. Player can continue playing. 5. **New card**`myBingoCard` in game state updates to the fresh card. Player can continue playing.
6. **Redraw is only available during `live-event`** — same act gate as tapping squares.
### Scoring Across Multiple Cards
Bingo points accumulate across all cards (completed + active). `getBingoScore()` sums:
- 2 points per tapped square across all completed cards and the active card
- 10 bonus points per completed bingo line (each completed card contributed at least one line; the active card may also have lines)
The `announcedBingo` set is changed to track `playerId:cardIndex` instead of just `playerId`, so multiple bingo announcements per player are possible.
### Data Changes ### Data Changes
Shared types additions: New Zod schema in `game-types.ts`:
``` ```typescript
completedBingoCards: { playerId: string, displayName: string, card: BingoCard, completedAt: string }[] export const completedBingoCardSchema = z.object({
playerId: z.string(),
displayName: z.string(),
card: bingoCardSchema,
completedAt: z.string(),
})
``` ```
New WS client message: Added to `gameStateSchema`:
```typescript
completedBingoCards: z.array(completedBingoCardSchema)
``` ```
{ type: "request_new_bingo_card" }
New WS client message in `ws-messages.ts` (added to `clientMessage` discriminated union):
```typescript
export const requestNewBingoCardMessage = z.object({ type: z.literal("request_new_bingo_card") })
``` ```
Game state changes: Game state changes:
- `myBingoCard` continues to represent the current active card - `myBingoCard` continues to represent the current active card
- `completedBingoCards` is a new array on `GameState` (visible to all — host needs it for verification) - `completedBingoCards` is a new array on `GameState` (visible to all — host needs it for verification)
- Display view receives `completedBingoCards` for bingo announcement purposes only (no card detail needed on projector)
--- ---
@@ -162,7 +185,7 @@ Game state changes:
Replaces both the current `Leaderboard` and `PlayerList` components as separate UI elements. Replaces both the current `Leaderboard` and `PlayerList` components as separate UI elements.
- **Lobby** (no scores yet): shows player list with names only — a "who's here" view - **Lobby** (no scores yet): shows player list with names only — a "who's here" view. The `Leaderboard` component renders a simplified layout: just rank numbers and names, no score columns.
- **All other acts**: full leaderboard table (rank, name, P/J/B/Q breakdown, total points) - **All other acts**: full leaderboard table (rank, name, P/J/B/Q breakdown, total points)
- **Scoring explanation**: "How scoring works" box at the bottom (same as current) - **Scoring explanation**: "How scoring works" box at the bottom (same as current)
@@ -195,7 +218,7 @@ Vertical stack of cards, only visible to the host. This tab contains all host-ex
### Bottom ### Bottom
7. **End Party** button — destructive (red), always available except when already ended 7. **End Party** button — destructive (red), always available except when already ended. This is the same action as advancing from scoring to ended, but available from any act as a shortcut. The act-specific "End Party" label in Act Controls (scoring → ended) is removed to avoid duplication.
--- ---
@@ -253,12 +276,31 @@ Unchanged. Full-screen passive view, no bottom nav. Continues to show:
### Game State Changes ### Game State Changes
- Add `completedBingoCards` to game state schema - Add `completedBingoCards` to game state schema (Zod schema + TypeScript type)
- Include in `getGameStateForPlayer` and `getGameStateForDisplay` - Include in `getGameStateForPlayer` (full card data for host verification) and `getGameStateForDisplay` (for announcements only)
- Update `getBingoScore()` to sum points across all completed cards + active card
- Change `announcedBingo` from `Set<string>` (playerId) to track `playerId:cardIndex` pairs, allowing multiple bingo announcements per player
### WS Handler Changes
- Add `request_new_bingo_card` case to handler, gated to `live-event` act
- Add the message to the `clientMessage` discriminated union in `ws-messages.ts`
### Route Transition Safety
- WebSocket connection and Zustand store are managed in the layout route, not child routes. Navigating between tabs (child routes) does not trigger WS reconnection or store reset.
--- ---
## 11. Migration / URL Compatibility ## 11. Migration / URL Compatibility
- Old URLs (`/play/ABCD`, `/host/ABCD`) should redirect to `/play/ABCD/game` and `/host/ABCD/game` respectively - Old URLs (`/play/ABCD`, `/host/ABCD`) should redirect to `/play/ABCD/game` and `/host/ABCD/game` respectively
- TanStack Router can handle this via the layout route's default redirect - Implemented via TanStack Router index routes that redirect (e.g., a `/play/$roomCode/` index route with `beforeLoad` that throws `redirect({ to: '/play/$roomCode/game' })`)
---
## 12. Label Naming
- Player bottom nav labels: "Game", "Bingo", "Leaderboard" (3 tabs, enough space for full labels)
- Host bottom nav labels: "Game", "Bingo", "Board", "Host" (4 tabs, "Board" is shortened from "Leaderboard" for space)
- Route paths use short names: `/game`, `/bingo`, `/board`, `/host`