From f7ee2204d193ef1d9a5e47effbfae94a1f7fec2f Mon Sep 17 00:00:00 2001 From: AyoubBenziza Date: Thu, 11 Jun 2026 01:00:50 +0200 Subject: [PATCH] feat: round recap shows each player's answer - shared: RoundRecap gains answers[] (RoundPlayerAnswer: text + correct) - server: quiz/blindtest recap() build per-player answers (choice text / free text / blindtest guess formatted by mode), with correctness - client: recap lists each player's answer per round, green/red by correctness, with their points when they scored Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/server/src/game/engine.test.ts | 2 +- .../game/modes/blindtest/blindtest-round.ts | 30 +++++ apps/server/src/game/modes/quiz/quiz-round.ts | 16 +++ apps/web/src/components/game-end-view.tsx | 105 +++++++++++------- packages/shared/src/domain.ts | 9 ++ 5 files changed, 121 insertions(+), 41 deletions(-) diff --git a/apps/server/src/game/engine.test.ts b/apps/server/src/game/engine.test.ts index cb8f010..991b29d 100644 --- a/apps/server/src/game/engine.test.ts +++ b/apps/server/src/game/engine.test.ts @@ -46,7 +46,7 @@ const dummyRound: GameRound = { } return deltas }, - recap: () => ({ label: "2+2 ?", answer: "4" }), + recap: () => ({ label: "2+2 ?", answer: "4", answers: [] }), } function setup(roundDurationSec: number): { diff --git a/apps/server/src/game/modes/blindtest/blindtest-round.ts b/apps/server/src/game/modes/blindtest/blindtest-round.ts index 70ae6b6..20a46c9 100644 --- a/apps/server/src/game/modes/blindtest/blindtest-round.ts +++ b/apps/server/src/game/modes/blindtest/blindtest-round.ts @@ -160,13 +160,43 @@ export class BlindtestRound implements GameRound { recap(ctx: RoundContext): RoundRecapInfo { const { track } = ctx.data as BlindtestRoundData + const mode = ctx.room.settings.blindtestMode const submitter = ctx.room.players.get(track.submittedBy) + const nameOf = (id?: string) => + (id && ctx.room.players.get(id)?.name) || "?" + + const answers = [] + for (const [playerId, vote] of ctx.votes) { + if (playerId === track.submittedBy) { + continue + } + const a = vote as { + title?: string + artist?: string + guessedPlayerId?: string + } + let text = "" + if (mode === "who_added") { + text = nameOf(a.guessedPlayerId) + } else if (mode === "title_artist") { + text = `${a.title ?? ""} / ${a.artist ?? ""}` + } else { + text = `${a.title ?? ""} / ${a.artist ?? ""} → ${nameOf(a.guessedPlayerId)}` + } + answers.push({ + playerId, + answer: text, + correct: voterResult(mode, vote, track).points > 0, + }) + } + return { label: "Blindtest", answer: `${track.title} — ${track.artist}`, youtubeId: track.youtubeId, url: track.url, addedBy: submitter?.name ?? "?", + answers, } } } diff --git a/apps/server/src/game/modes/quiz/quiz-round.ts b/apps/server/src/game/modes/quiz/quiz-round.ts index 056f138..54250a1 100644 --- a/apps/server/src/game/modes/quiz/quiz-round.ts +++ b/apps/server/src/game/modes/quiz/quiz-round.ts @@ -152,12 +152,28 @@ export class QuizRound implements GameRound { const answer = isTextFormat(question) ? (question.acceptedAnswers?.[0] ?? "") : (question.choices?.[question.correctIndex ?? 0] ?? "") + const answers = [] + for (const player of ctx.room.players.values()) { + const vote = ctx.votes.get(player.id) + if (!vote) { + continue + } + const text = isTextFormat(question) + ? (asText(vote) ?? "") + : (question.choices?.[asChoice(vote) ?? -1] ?? "?") + answers.push({ + playerId: player.id, + answer: text, + correct: isCorrect(question, vote), + }) + } return { label: question.prompt, answer, category: question.category, imageUrl: question.format === "image_reveal" ? question.imageUrl : undefined, + answers, } } } diff --git a/apps/web/src/components/game-end-view.tsx b/apps/web/src/components/game-end-view.tsx index c232202..1434dab 100644 --- a/apps/web/src/components/game-end-view.tsx +++ b/apps/web/src/components/game-end-view.tsx @@ -332,47 +332,72 @@ function RoundsRecap({ Récapitulatif des manches ({recap.length})
    - {recap.map((r) => ( -
  • - {r.youtubeId ? ( - - ) : r.imageUrl ? ( - - ) : null} -
    -

    - {r.index + 1} · {r.category ?? (r.type === "blindtest" ? "Blindtest" : "Quiz")} -

    - {r.type !== "blindtest" && ( -

    {r.label}

    - )} -

    - {r.answer} - {r.addedBy && ( - - {" "} - · {r.addedBy} - + {recap.map((r) => { + const pointsBy = new Map(r.scorers.map((s) => [s.playerId, s.points])) + return ( +

  • + {r.youtubeId ? ( + + ) : r.imageUrl ? ( + + ) : null} +
    +

    + {r.index + 1} ·{" "} + {r.category ?? (r.type === "blindtest" ? "Blindtest" : "Quiz")} +

    + {r.type !== "blindtest" && ( +

    {r.label}

    )} -

    -

    - {r.scorers.length > 0 - ? r.scorers - .map((s) => `${nameOf(s.playerId)} +${s.points}`) - .join(" · ") - : "Personne n'a marqué"} -

    -
    -
  • - ))} +

    + {r.answer} + {r.addedBy && ( + + {" "} + · {r.addedBy} + + )} +

    +
      + {r.answers.length === 0 ? ( +
    • + Aucune réponse +
    • + ) : ( + r.answers.map((a) => ( +
    • + + {nameOf(a.playerId)} :{" "} + + + {a.answer || "—"} + + {pointsBy.get(a.playerId) ? ( + + {" "} + +{pointsBy.get(a.playerId)} + + ) : null} +
    • + )) + )} +
    + + + ) + })}
) diff --git a/packages/shared/src/domain.ts b/packages/shared/src/domain.ts index a460365..8afd99e 100644 --- a/packages/shared/src/domain.ts +++ b/packages/shared/src/domain.ts @@ -90,6 +90,13 @@ export interface RoundRecapScorer { points: number } +/** Réponse d'un joueur sur une manche (récap). */ +export interface RoundPlayerAnswer { + playerId: string + answer: string + correct: boolean +} + /** Récapitulatif d'une manche jouée (affiché en fin de partie). */ export interface RoundRecap { index: number @@ -106,6 +113,8 @@ export interface RoundRecap { addedBy?: string /** Joueurs ayant marqué sur cette manche. */ scorers: RoundRecapScorer[] + /** Réponse de chaque joueur ayant répondu. */ + answers: RoundPlayerAnswer[] } // --- Snapshot diffusé au client (room:state) -----------------------------