Files
esc/docs/superpowers/specs/2026-03-12-issue1-fixes-design.md

7.7 KiB

Issue #1 Fixes — Design Spec

Goal: Address all items in Gitea issue #1 — rework predictions to use full ESC entries with tap-to-assign UI, remove Dish of the Nation, rename acts, add player submission indicators, and make lobby code copyable.

Source: #1


1. Entry Data Model

Replace the current country-only lineup with full ESC entries (shown as Zod schemas, matching codebase conventions):

const entrySchema = z.object({
	country: z.object({ code: z.string(), name: z.string(), flag: z.string() }),
	artist: z.string(),
	song: z.string(),
})

const lineupSchema = z.object({
	year: z.number(),
	entries: z.array(entrySchema),
})

Note: The existing countrySchema gains a flag field — this is a breaking change to the country object shape throughout the codebase.

The data file changes from esc-2026.json to esc-2025.json using real ESC 2025 entries for testing. Each entry includes the flag emoji in the data file.

Display format everywhere: 🇩🇪 Abor — Süden

2. Prediction Model

Replace the current { predictedWinner, top3[], nulPointsPick } with 4 ordered picks:

type Prediction = {
	playerId: string
	first: string   // country code
	second: string  // country code
	third: string   // country code
	last: string    // country code
}

Validation: All 4 picks must be distinct country codes from the lineup.

Scoring model (for future implementation):

  • Any of 1st/2nd/3rd picks landing in the actual top 3 → points
  • 1st pick matching actual winner → bonus points
  • Last pick matching actual last place → bonus points

3. Prediction UI — Tap-to-Assign

Top section — 4 slot cards:

  • "1st Place", "2nd Place", "3rd Place", "Last Place"
  • Empty slots show placeholder text
  • Filled slots show the entry (flag + artist + song) with a tap-to-remove action

Bottom section — scrollable entry list:

  • All entries from the lineup
  • Already-assigned entries are dimmed/disabled
  • Tapping an unassigned entry shows a popover with only unfilled slot options (1st/2nd/3rd/Last)
  • After selecting a slot, the entry fills that slot, the popover closes, and the entry dims in the list

Submit button: Appears when all 4 slots are filled. After submission or when predictions are locked, the UI becomes read-only showing assigned entries in their slots.

Locked state: When advancing past Pre-Show, predictions lock. The form shows the player's submitted picks (or "Not submitted" if they missed the window).

4. Remove Dish of the Nation

Strip the entire Dish of the Nation feature:

Server:

  • Remove dish WS message handlers from handler.ts
  • Remove dish methods from GameManager
  • Remove dish persistence from GameService
  • Remove dish DB tables (dishes, dish_guesses) from schema

Client:

  • Delete dish-list.tsx, dish-host.tsx, dish-results.tsx
  • Remove dish state slices and actions from room-store.ts
  • Remove dish WS message handlers from use-websocket.ts
  • Remove dish UI from routes (play, host, display)

Shared:

  • Remove dish schemas from game-types.ts
  • Remove dish WS message types from ws-messages.ts

5. Player List — Prediction Checkmark

Add prediction submission status to the game state broadcast:

  • Shared: Add predictionSubmitted: Map<playerId, boolean> (or equivalent) to gameStateSchema in game-types.ts. This lives on the game state, not the player schema, since it's game-specific data.
  • Server: When building game state in GameManager.getGameStateForPlayer() / getGameStateForDisplay(), include which players have submitted predictions.
  • Client: player-list.tsx reads from game state and renders a checkmark icon (✓) next to player names that have submitted predictions.
  • Visible on all views (play, host, display).

6. Acts Naming

Rename internal act identifiers and add display names:

const ACTS = ["lobby", "pre-show", "live-event", "scoring", "ended"] as const
Internal ID Display Name Timing Intent
lobby Lobby Waiting room, players join
pre-show Pre-Show Before broadcast, predictions
live-event Live Event During broadcast
scoring Scoring After results, leaderboard
ended Ended Party over

Host control buttons: "Start Pre-Show", "Start Live Event", "Start Scoring", "End Party".

Predictions lock when advancing from Pre-Show to Live Event (previously act1 → act2).

DB migration: The Postgres actEnum must be updated from ["lobby", "act1", "act2", "act3", "ended"] to ["lobby", "pre-show", "live-event", "scoring", "ended"]. Since there is no production data to preserve, drop and recreate the enum (via Drizzle push or a migration). The predictions table columns also change from predictedWinner/top3/nulPointsPick to first/second/third/last — same approach, drop and recreate.

7. Lobby Code — Copy to Clipboard

On the display view (and anywhere the room code is shown prominently):

  • Wrap the room code in a tappable/clickable element
  • On click: navigator.clipboard.writeText(roomCode)
  • Show brief "Copied!" feedback (tooltip or temporary text swap)
  • Style to indicate interactivity (cursor pointer, subtle hover state)

Data Flow Changes

Prediction Flow (updated)

  1. Client taps entry → selects slot → slot fills in UI
  2. Client fills all 4 slots → submits submit_prediction with { first, second, third, last }
  3. Server validates: all 4 distinct, all valid country codes
  4. Server stores in GameManager, persists to DB
  5. Server broadcasts updated game state (includes hasSubmittedPrediction per player)
  6. All clients update player list checkmarks

Act Progression (updated names)

lobby → pre-show → live-event → scoring → ended

Predictions lock on pre-show → live-event transition.

Files Affected

Modified

  • packages/shared/src/game-types.ts — entry/lineup schemas, prediction model, add predictionSubmitted to game state
  • packages/shared/src/ws-messages.ts — remove dish messages, update prediction message
  • packages/shared/src/constants.ts — act names
  • packages/server/data/ — replace esc-2026.json with esc-2025.json
  • packages/server/src/games/game-manager.ts — remove dish logic, update prediction logic
  • packages/server/src/games/game-service.ts — remove dish persistence, update prediction columns
  • packages/server/src/rooms/room-manager.ts — act name references
  • packages/server/src/ws/handler.ts — remove dish handlers, update prediction handler
  • packages/server/src/db/schema.ts — remove dish tables, update prediction columns, update actEnum values
  • packages/server/tests/game-manager.test.ts — rewrite for new model
  • packages/server/tests/ws-handler.test.ts — update for changed messages
  • packages/client/src/stores/room-store.ts — remove dish state, update game state shape
  • packages/client/src/hooks/use-websocket.ts — remove dish handlers
  • packages/client/src/components/predictions-form.tsx — rewrite as tap-to-assign
  • packages/client/src/components/player-list.tsx — add prediction checkmark
  • packages/client/src/routes/play.$roomCode.tsx — remove dish UI
  • packages/client/src/routes/host.$roomCode.tsx — remove dish UI
  • packages/client/src/routes/display.$roomCode.tsx — remove dish UI, add copy-to-clipboard

Deleted

  • packages/client/src/components/dish-list.tsx
  • packages/client/src/components/dish-host.tsx
  • packages/client/src/components/dish-results.tsx

Created

  • packages/server/data/esc-2025.json — full ESC 2025 entry data