Commit graph

13 commits

Author SHA1 Message Date
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
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
b3efa27287 feat: game history + question lang/active management
DB:
- quiz_question gains `lang` ('fr' default) and `active` (true default); migration 0002
- only active questions are served to games (pool filter)

History:
- engine writes a game_history row at game end (modes + ranked results), fire-and-forget
- public GET /api/history/:code lists a room's recent games
- lobby shows a "Parties précédentes" collapsible (date, modes, winner/scores)

Back-office:
- create form has a FR/EN language select; lang stored
- per-question active toggle (PATCH /questions/:id) — disable without deleting;
  inactive rows dimmed and labelled
- seeds set lang (opentdb=en, manual/pokemon=fr)

Verified e2e: migration applied; game played → /api/history returns it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 17:12:07 +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
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
a2e3fae858 fix(blindtest): keep neutral DJ; only the contributor is misdirection-scored
Revert the DJ change: the DJ is again a neutral player (never the track's
contributor), broadcast to all and counted as an eligible voter (votes blind).
The contributor is a normal voter whose vote never scores on their own track;
they only earn the who_added/mixed misdirection bonus (+50 per wrong guess).

- engine: restore single round:start broadcast + DJ-included eligible voters
- blindtest-round: restore neutral pickDj; contributor may vote (ignored in
  scoring) so the round can still end on "all voted"
- client: DJ controls + everyone (DJ included) votes; reveal message simplified

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 14:40:02 +02:00
AyoubBenziza
a9eb6098f2 feat(blindtest): contributor-as-DJ scoring, 3-player gate, 16:9 player + seek
- DJ is now the track's contributor (controls their own song); their identity
  is sent only to them so "who added" stays non-trivial. The contributor
  doesn't vote and is excluded from the eligible-voters count.
- Scoring: the contributor earns nothing except in who_added/mixed, where they
  get a misdirection bonus (50) per player who guesses the wrong adder.
- Blindtest (and mixed) require >= 3 connected players: lobby disables the
  options with a hint, and game:start rejects with NEED_THREE.
- Player card is now 16:9 and full width — the video banner shows at reveal;
  DJ gets a seek bar (current/duration) to jump anywhere, plus play/pause/restart.
- Mixed game order is shuffled (no predictable quiz/blindtest alternation).

Verified e2e (3 players): guard rejects at 2, DJ id hidden from others,
misdirection scoring correct. 23 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 14:30:47 +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