Commit graph

18 commits

Author SHA1 Message Date
AyoubBenziza
7a41bbf8c1 feat: DJ can be contributor, volume, theme switch, clock sync, 90s blindtest
Playtest feedback round 2:
- blindtest DJ can now be anyone incl. the track's contributor (pickDj only
  excludes bots) — no more "immunity" for whoever isn't picked
- blindtest rounds last 90s (longer listen + typing)
- per-player volume slider (persisted), YT player setVolume + origin/enablejsapi
- theme switcher (light/dark/system) on home + room header
- clock sync: clients estimate the server-clock offset (time:sync) so the
  countdown is identical across devices (was off by each device's clock skew);
  also used for blindtest media sync

Note on duplicates: anti-repeat is per room code (quiz_played) — it works when
you replay in the SAME room, but a new room starts fresh. Verified e2e: two
chained games in one room share 0 questions.

Verified: typecheck 4/4, lint 4/4, 24 server tests, web build; e2e dedup (0
overlap) and clock/DJ behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 00:39:02 +02:00
AyoubBenziza
6362b97508 fix(web): blindtest sound, hidden vote status, authoritative tracks, quiz input
From friends playtest feedback:
- blindtest "who added it?" (and mixed): hide the per-player vote status — the
  contributor never validates, which gave them away
- blindtest sound: set YT player `origin`/`enablejsapi` (fixes the postMessage
  origin warning) and add a per-game "Activer le son" gesture for non-DJ
  (browsers block audible autoplay without a user gesture)
- track list: render each player's own tracks from a server-sent, authoritative
  blindtest:myTracks event (emitted on submit/remove/reconnect) instead of local
  state that desynced on remount — fixes the "3/3 but only 2 shown" case
- quiz: reset the free-answer input between questions (was pre-filled with the
  previous answer)

Verified e2e: myTracks emitted on submit + reconnect.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 00:16:27 +02:00
AyoubBenziza
42a36fc442 feat: blindtest needs 2 real players + bot difficulty setting
- blindtest is only available with >=3 players AND >=2 humans (a bot can't be DJ):
  lobby gate + tooltips updated, and server game:start rejects otherwise
- new botDifficulty setting (easy/normal/hard) drives how often bots answer
  correctly (quiz + blindtest who-added / title-artist); host picks it in the
  lobby when at least one bot is present

Verified e2e: blindtest start rejected with 1 human + 2 bots (NEED_THREE).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 18:03:03 +02:00
AyoubBenziza
8eac8d0123 feat: bots — virtual players to fill a game
- server-side bot players (isBot): host adds/removes them in the lobby
  (lobby:addBot / lobby:removeBot), capped at 8 players
- bots auto-vote each round: GameRound.botAnswer generates a plausible answer
  (quiz ~50% right, blindtest guesses a random player / title); engine schedules
  the votes within the answer window and triggers early end when all have voted
- bots are never DJ (pickDj excludes them) and are excluded from the blindtest
  track-submission gate (they don't submit)
- snapshot players carry `bot`; lobby shows a 🤖 marker + add/remove controls

Verified e2e: 2 bots added, they vote, game ends with all 3 players scored
and rounds end early.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 17:48:33 +02:00
AyoubBenziza
f0c327617b feat: leave-room button
- room:leave event: removes the player from the room (not just disconnected),
  reassigns host if the host leaves, deletes the room when empty
- store leaveRoom() emits it and clears the session (reset)
- room header gets a "Quitter" button (always available); end-screen "Quitter"
  now also leaves cleanly

Verified e2e: leaver removed from snapshot, host reassigned to the next player.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 17:33:10 +02:00
AyoubBenziza
6ed99ea803 feat: reconnection — keep your place across refresh / network drop
- each player gets a reconnectToken; room:create/join return it
- new room:rejoin event: re-attaches the existing player (by id+token) to the
  new socket, broadcasts player:rejoined + room:state, and resyncs the current
  round to that socket (engine.resync re-emits round:start with their view)
- client persists the session (roomCode/playerId/token/name) in localStorage;
  bootstraps the store from it and auto-rejoins on every socket connect;
  clears it on reset/quit or failed rejoin

Verified e2e: wrong token rejected, correct token re-attaches (player present
+ connected).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 17:26:22 +02:00
AyoubBenziza
b91d09b7e7 feat: category filter in lobby settings
- settings carry `categories` (empty = all); quiz/image pool filters by category
  (DB + in-code bank), threaded through prepareQuizForRoom/loadGroup/loadQuizPool
- public GET /api/categories lists available categories
- lobby: category chips (Toutes + each), shown when quiz/images is active
- shared: RoomSettings.categories + UpdateSettingsPayload

Verified e2e: /api/categories returns the list; filtering by ["Pokémon"] serves
only Pokémon questions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:37:33 +02:00
AyoubBenziza
03a0dfa998 feat: end-of-game round recap (all rounds with answers + scorers)
- shared: RoundRecap (+ scorers); game:end carries recap[]
- server: GameRound.recap(ctx) per mode (quiz: prompt + correct answer +
  image; blindtest: "title — artist" + addedBy + links); engine accumulates a
  history per round (with that round's scorers from the score deltas) and sends
  it at game:end
- client: GameEndView shows a collapsible "Récapitulatif des manches" — each
  round's label, answer, media thumbnail (image/youtube) and who scored
- engine test dummy round implements recap

Verified e2e: game:end recap lists every round with answer + scorers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 00:54:40 +02:00
AyoubBenziza
8bc184fab7 feat: bigger image_reveal, optional image prompt, mixed sub-mode toggles, Pokémon seed
- image_reveal display: larger, object-contain (whole image up to 58vh), no crop
- image prompt optional in back-office (defaults to "Qui est-ce ?"), overridable
- mixed mode reworked: select "Mixte" then toggle sub-modes (Quiz/Images/
  Blindtest). Blindtest sub-mode locked under 3 players; mixed itself no longer
  needs 3. Server quiz pool formats derived from gameType + mixedModes.
  shared: MixMode + RoomSettings.mixedModes (default quiz+image).
- DB: prompt uniqueness is now a partial index (excludes image_reveal) + unique
  index on image_url; lets many images share the generic prompt, dedup by URL.
- seed: db:seed:images pulls Pokémon (PokéAPI) as image_reveal questions with
  official artwork (remote URL) + FR/EN accepted answers; idempotent by image_url.

Verified: migration applied, Pokémon seed inserts FR+EN, image mode serves only
image_reveal, all checks green (23 tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 00:20:28 +02:00
AyoubBenziza
3f0cab8f5f feat: end-of-game track recap, no self-vote in who_added, festive home + icons
- game:end now carries the played blindtest tracks (title/artist/youtubeId/
  url/submittedByName); the end screen lists them as cards with a YouTube
  open link so players can recover/export the songs
- who_added vote no longer lets you pick yourself (filtered out)
- home page: animated halo, floating geek emojis, springy gradient title,
  hover/tap on buttons (Framer Motion) + Lucide icons
- lobby settings get Lucide icons (game type, quiz, blindtest mode, tracks,
  start)

Verified e2e: game:end includes the track list with links.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 16:04:14 +02:00
AyoubBenziza
1071a59b68 feat: pick pseudo inside the room + shareable invite link
Restructure onboarding (option B):
- home is just Create / Join (code) — no name field
- name is chosen in the room: room:create/join no longer require a name;
  new player:setName event. Nameless players are hidden from the snapshot
  (players/scores/submissions) and excluded from eligible voters and the
  3-player blindtest gate until they pick a pseudo.
- PseudoScreen with live DiceBear avatar preview gates RoomPage until set
- /join/:code auto-joins then redirects to the room (seamless invite links)
- RoomCode gains a "copier le lien" button (origin/join/CODE)

Verified e2e: nameless players excluded from the room until setName.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 15:32:24 +02:00
AyoubBenziza
6251146c19 feat: replay from lobby, contributor mislead card, editable track cards
Return to lobby (replay without recreating the room):
- lobby:return (host): reset to lobby, scores to 0, keep players + tracks
- client: end screen has "Rejouer" (host) + "Quitter"; store clears transient
  game state whenever the room goes back to lobby

Contributor has nothing to input:
- round:start carries a private `mine: true` to the contributor only (via
  RoundStart.secretPlayerId targeted emit); they're excluded from eligible
  voters. Client shows a "fais-les se tromper" card instead of the vote form.

Editable track submission:
- submitTrack ack returns trackId + youtubeId; new blindtest:removeTrack
- lobby shows submitted tracks as cards (YouTube thumbnail + title) with delete

Verified e2e (3 players): add/remove, private mine flag, return-to-lobby reset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 15:13:54 +02:00
AyoubBenziza
e3315d3b35 feat: mixed games by default, free-text quiz, mode-aware transitions
Mixed mode (default):
- gameType is now mixed | quiz | blindtest, default "mixed"; the lobby
  interleaves quiz + blindtest rounds so modes alternate
- lobby reworked: 3-way game-type switch, quiz count and/or blindtest config
  + track submission shown per selected type

Free-text quiz questions:
- quiz supports the `free` format (e.g. maths) alongside mcq/truefalse:
  payload without choices, {text} vote, tolerant matching on acceptedAnswers
- shared QuizQuestionPayload.choices optional; reveal truth carries answer
- match util moved to game/match.ts (shared by quiz + blindtest)
- repo/seed include free + acceptedAnswers; in-code bank gains free questions
- client quiz view renders a text input for free questions

Mode-aware transitions:
- RoundTransition themed per mode (colors/label/emoji); announces the mode
  on change (quiz <-> blindtest), lighter teaser within the same mode
- custom media buckets per mode: assets/transitions/{quiz,blindtest}/ + shared
- store tracks roundModeChanged

Verified e2e: mixed game alternates quiz/blindtest to completion; free-text
scoring covered by tests (22 pass).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 13:41:29 +02:00
AyoubBenziza
98e06b8206 feat(blindtest): YouTube blindtest mode with neutral DJ + synced playback
Server:
- BlindtestRound (GameRound): neutral DJ pick (never the contributor),
  per-mode scoring (title_artist fuzzy / who_added / mixed), reveal exposes
  title/artist/submittedBy; contributor doesn't score on own track
- track submission: blindtest:submitTrack validates via YouTube oEmbed
  (exists + embeddable) and captures title/artist; per-player quota; dedup
- DJ media relay: media:control (DJ only) → media:sync broadcast (engine)
- per-room shuffled track pool; submissions count in room snapshot
- shared: blindtest payloads, gameType/tracksPerPlayer in settings
- tests: DJ neutrality, scoring per mode, fuzzy match, youtube id extraction

Client:
- YouTube IFrame player (hidden behind a spinning disc — audio only, no
  title leak), DJ transport (play/pause/restart), non-DJ follows media:sync
  with latency compensation
- lobby: game-type switch (Quiz/Blindtest), blindtest config + per-player
  track submission UI
- vote forms per mode; reveal card with title/artist/who-added

Roadmap V1 step 6 (highest technical risk). Server flow verified end-to-end
over the wire (submit→start→DJ relay→vote→reveal→score).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 13:02:28 +02:00
AyoubBenziza
7dda00c65c feat: lead-in timer compensation + GIF support for round transitions
Server (option 2 — don't eat answer time):
- engine: leadMs prep delay (default 1600ms) before the clock starts; the
  round timer now fires at lead + duration, and scoring uses the post-lead
  start so the speed bonus spans the full answer window
- shared: round:start carries startsAt (answers begin) alongside endsAt
- engine test passes leadMs:0

Client:
- Countdown shows the full duration (no ticking) until startsAt, then counts
  down — so the WarioWare transition plays in full without stealing time
- store/ActiveRound carry startsAt

Custom transition media:
- RoundTransition auto-loads any gif/webp/apng/png/avif dropped in
  src/assets/transitions/ (import.meta.glob), plays one at random per round,
  falls back to the Framer animation when none are present
- README documenting how to add animations

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 11:25:17 +02:00
AyoubBenziza
2fce63530b feat(quiz): show per-player answer status during the round
- voteAck now carries `voted` (playerIds who validated) — never their answer,
  so no leak; the count/total stay for the progress text
- PlayerCards show a green "Prêt" check for players who answered and a
  pulsing "en attente" dot for those who haven't, during in_round only
- score stays visible on the cards alongside the status
- engine test updated for the new voteAck shape

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 10:30:03 +02:00
AyoubBenziza
9ed3b8a4cb feat(server): generic game engine — GameRound interface + orchestration loop
- game/round.ts: GameRound contract (start/submitAnswer/reveal/score) +
  RoundContext + RoomGameController (decouples room from engine)
- game/registry.ts: register/createRound — adding a mode = one registerRound
- game/engine.ts: server-authoritative orchestration. Per round:
  start → broadcast round:start → wait (timer OR all eligible voted) →
  reveal → apply score deltas → score:update; then game:end. DJ counts as
  eligible voter; round ends on first condition met.
- handlers: game:start (host-only, validates rounds/modes) + round:vote
  wired to the engine
- shared: add game:start client event
- engine.test.ts: covers both end conditions + voteAck (bun test)
- turbo/root: add test task

Roadmap V1 step 3. No UI yet — concrete quiz mode + client come in step 4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 15:29:49 +02:00
AyoubBenziza
56115b5a9f feat(shared,server): scaffold @nerdware/shared types + Fastify/Socket.IO server
- packages/shared (@nerdware/shared): domain model (Room, Player, Settings,
  Round, votes, scores) + fully typed Socket.IO events, consumed as source
- apps/server (@nerdware/server): Fastify boot + /health, typed Socket.IO
  attached to the HTTP server, env config, graceful shutdown
- gitignore: allow .env.example to be committed

Roadmap V1 step 1. Room logic (room:create/join) comes next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 14:55:52 +02:00