address spec review feedback: fill gaps, fix ambiguities

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 11:26:45 +01:00
parent 1d9fc62bf3
commit 6dcdea41be

View File

@@ -84,6 +84,9 @@ Three distinct views, all served from the same client app:
6. Only the host can advance between acts.
7. Max 10 participants (1 host + 9 players).
8. Rooms auto-expire after 12 hours. Host can end the room early via the Host tab.
9. Room codes are generated with collision detection — if a code is already in use by an active room, regenerate.
10. Display names must be unique within a room. Server rejects a join if the name is taken.
11. **Late joiners:** Players can join at any point before `ended`. Late joiners miss games that have already started — no retroactive predictions after Act 1, no bingo card if joining mid-Act 2. They participate in whatever is currently active.
### Reconnection
@@ -115,6 +118,7 @@ JSON messages with a `type` field discriminator.
| `next_question` | — | Host advances to next question |
| `buzz_quiz` | — | Player buzzes in |
| `judge_answer` | correct (boolean) | Host judges buzzer answer |
| `submit_actual_results` | winner, second, third, lastPlace | Host enters real ESC results |
**Server → Client (state updates):**
@@ -135,31 +139,32 @@ JSON messages with a `type` field discriminator.
| `quiz_buzz` | playerId, displayName | Someone buzzed |
| `quiz_result` | playerId, correct, points | Answer judged |
| `leaderboard_update` | sorted player scores | After any scoring event |
| `actual_results_entered` | winner, second, third, lastPlace | Host submitted real results |
| `predictions_revealed` | per-player prediction scores | Prediction scores unveiled |
| `room_ended` | finalLeaderboard | Room ends |
---
## Games
### Prediction Voting (Act 1)
### Prediction Voting (lobby + Act 1)
- Available as soon as players join the lobby.
- Each player submits: predicted winner (1 country), top 3 finishers (3 countries), nul points candidate (1 country).
- Predictions lock when the host advances to Act 2.
- Available as soon as players join the lobby, through Act 1. Predictions lock when the host advances to Act 2.
- Each player submits: predicted winner (1 country), top 3 finishers (3 countries, excluding the predicted winner), nul points candidate (1 country).
- Scores are hidden until the final leaderboard reveal in Act 3.
- Country selection from the hardcoded ESC 2026 lineup.
### Dish of the Nation (Act 1)
### Dish of the Nation (lobby + Act 1)
- Host adds dishes via the Host tab as people arrive (name + correct country).
- Host adds dishes via the Host tab as people arrive (name + correct country). This can start during the lobby phase and continue into Act 1.
- Players see the list of dishes and submit a country guess per dish.
- Host triggers reveal — display shows each dish with the correct country and who guessed right.
- Host triggers reveal before Act 2 — display shows each dish with the correct country and who guessed right.
- Low-stakes icebreaker.
### Live Jury Voting (Act 2)
- Host taps "Open Voting" after each country's performance.
- All player phones show a 1-12 rating slider/selector for that country.
- All player phones show a 1-12 discrete integer rating for that country (1 = worst, 12 = best, matching Eurovision convention).
- Host taps "Close Voting" → server aggregates ratings.
- Display shows the group's results with a dramatic "12 points go to..." moment for the top-rated act.
- Running totals feed into the leaderboard.
@@ -168,17 +173,17 @@ JSON messages with a `type` field discriminator.
### ESC Bingo (Act 2, passive)
- Bingo cards generated server-side when Act 2 starts.
- Each player gets a unique card — a random subset of tropes from `bingo-tropes.json` arranged in a grid (e.g., 4×4 = 16 squares).
- Each player gets a unique card — a random subset of tropes from `bingo-tropes.json` arranged in a **4×4 grid (16 squares, no free space)**. The trope pool must contain at least 30 tropes to ensure sufficient card variety.
- Players tap squares on their phone as tropes happen during the broadcast.
- Server validates bingo (full row/column/diagonal) and announces on the display.
- Server validates bingo: any complete row, column, or diagonal (4 in a line). Multiple players can achieve bingo. Each player receives the bingo bonus once (first bingo only); subsequent lines score nothing extra.
- Runs passively alongside jury voting — player phone shows tabs for Jury and Bingo during Act 2.
### Quiz / Trivia (Act 3)
- Host starts the quiz round from the Host tab.
- Display shows the question (from `quiz-questions.json`). Player phones show a buzzer button.
- First player to buzz gets to answer. Host judges correct/incorrect via the Host tab.
- If incorrect, the question reopens for remaining players.
- First player to buzz gets to answer (verbal answer in the room). Host judges correct/incorrect via the Host tab.
- If incorrect, that player is excluded from re-buzzing on this question. The question reopens for remaining players. Multiple incorrect buzzes are tracked in the `quiz_answers` table.
- Points based on difficulty tier (easy/medium/hard), values from scoring config.
- Host advances to next question manually. Can end the quiz at any time.
@@ -212,7 +217,9 @@ All values in `packages/server/data/scoring.json`, editable without code changes
**Jury scoring formula:** Each round, the group average rating is computed. A player's jury points for that round = `jury_max_per_round - abs(player_rating - group_average)`, clamped to a minimum of 0. This rewards voting with the group consensus.
**Prediction scoring:** Resolved after the actual ESC results are known. The host enters the actual top 3 + last place into the Host tab during/after Act 3, and the server calculates prediction scores.
**Prediction scoring:** Resolved after the actual ESC results are known. The host enters the actual results into the Host tab during/after Act 3 via a dedicated "Enter Results" form: winner, 2nd place, 3rd place, and last place finisher. The server compares each player's predictions against these actual results and calculates scores. This triggers the dramatic final leaderboard reveal on the display.
**Jury scoring edge case:** With very few voters (1-2), the consensus formula produces trivial results (a solo voter always matches the average). This is acceptable for a party game — the scenario is unlikely with 5-10 players, and the points are still valid.
---
@@ -220,6 +227,8 @@ All values in `packages/server/data/scoring.json`, editable without code changes
PostgreSQL via Drizzle ORM. Lightweight — most queries are "get everything for this room."
**Score computation:** Scores are computed on the fly by querying the individual game tables (jury_votes, bingo_cards, quiz_answers, dish_guesses, predictions + actual_results). No materialized scores table — with ≤10 players the joins are trivial and this avoids consistency issues.
### Tables
**rooms**
@@ -227,6 +236,10 @@ PostgreSQL via Drizzle ORM. Lightweight — most queries are "get everything for
- `code` (varchar(4), unique, indexed)
- `current_act` (enum: lobby, act1, act2, act3, ended)
- `host_session_id` (uuid)
- `actual_winner` (varchar, nullable — set when host enters results)
- `actual_second` (varchar, nullable)
- `actual_third` (varchar, nullable)
- `actual_last` (varchar, nullable)
- `created_at` (timestamp)
- `expires_at` (timestamp, default: created_at + 12h)
@@ -257,7 +270,6 @@ PostgreSQL via Drizzle ORM. Lightweight — most queries are "get everything for
**jury_votes**
- `id` (uuid, PK)
- `player_id` (FK → players)
- `room_id` (FK → rooms)
- `jury_round_id` (FK → jury_rounds)
- `rating` (integer, 1-12)
@@ -285,7 +297,6 @@ PostgreSQL via Drizzle ORM. Lightweight — most queries are "get everything for
- `room_id` (FK → rooms)
- `question_id` (varchar, references quiz-questions.json)
- `status` (enum: showing, buzzing, judging, resolved)
- `buzzed_player_id` (FK → players, nullable)
**quiz_answers**
- `id` (uuid, PK)
@@ -300,7 +311,8 @@ PostgreSQL via Drizzle ORM. Lightweight — most queries are "get everything for
- **Server:** Hono + WebSocket service on Uberspace (existing asteroid, accessed via `ssh serve`). Deployed to `~/services/celebrate-esc/`. Managed by systemd user unit.
- **Client:** Vite production build, static files served from `/var/www/virtual/<user>/html/celebrate-esc/`.
- **Backend routing:** `uberspace web backend set /celebrate-esc/api --http --port <port> --remove-prefix` for the API/WebSocket.
- **Backend routing:** `uberspace web backend set /celebrate-esc/api --http --port <port> --remove-prefix` for the API and WebSocket.
- **WebSocket endpoint:** `/celebrate-esc/api/ws/:roomCode` — clients connect here with their session token as a query parameter.
- **Database:** PostgreSQL on the same Uberspace host (existing or new instance — check `~/CLAUDE.md` on the server).
- **Deploy script:** Idempotent `deploy.sh` that builds client, syncs files, restarts the service.
@@ -325,6 +337,7 @@ PostgreSQL via Drizzle ORM. Lightweight — most queries are "get everything for
| UI | shadcn/ui + Tailwind v4 | — |
| Routing | TanStack Router | Hono router |
| State | Zustand | In-memory + PostgreSQL |
| PWA | vite-plugin-pwa (asset caching) | — |
| Realtime | WebSocket (native) | Hono WebSocket |
| DB | — | Drizzle + PostgreSQL |
| Validation | Zod | Zod |