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) <noreply@anthropic.com>
This commit is contained in:
parent
03a0dfa998
commit
f7ee2204d1
5 changed files with 121 additions and 41 deletions
|
|
@ -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): {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -332,7 +332,9 @@ function RoundsRecap({
|
|||
<ListChecks className="size-4" /> Récapitulatif des manches ({recap.length})
|
||||
</summary>
|
||||
<ul className="flex flex-col gap-2 p-3 pt-0">
|
||||
{recap.map((r) => (
|
||||
{recap.map((r) => {
|
||||
const pointsBy = new Map(r.scorers.map((s) => [s.playerId, s.points]))
|
||||
return (
|
||||
<li key={r.index} className="bg-muted/40 flex gap-3 rounded-lg p-2">
|
||||
{r.youtubeId ? (
|
||||
<img
|
||||
|
|
@ -349,7 +351,8 @@ function RoundsRecap({
|
|||
) : null}
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-muted-foreground text-[10px] uppercase">
|
||||
{r.index + 1} · {r.category ?? (r.type === "blindtest" ? "Blindtest" : "Quiz")}
|
||||
{r.index + 1} ·{" "}
|
||||
{r.category ?? (r.type === "blindtest" ? "Blindtest" : "Quiz")}
|
||||
</p>
|
||||
{r.type !== "blindtest" && (
|
||||
<p className="line-clamp-1 text-xs">{r.label}</p>
|
||||
|
|
@ -363,16 +366,38 @@ function RoundsRecap({
|
|||
</span>
|
||||
)}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-[10px]">
|
||||
{r.scorers.length > 0
|
||||
? r.scorers
|
||||
.map((s) => `${nameOf(s.playerId)} +${s.points}`)
|
||||
.join(" · ")
|
||||
: "Personne n'a marqué"}
|
||||
</p>
|
||||
<ul className="mt-1 flex flex-col gap-0.5">
|
||||
{r.answers.length === 0 ? (
|
||||
<li className="text-muted-foreground text-[10px]">
|
||||
Aucune réponse
|
||||
</li>
|
||||
) : (
|
||||
r.answers.map((a) => (
|
||||
<li key={a.playerId} className="text-[10px]">
|
||||
<span className="text-muted-foreground">
|
||||
{nameOf(a.playerId)} :{" "}
|
||||
</span>
|
||||
<span
|
||||
className={
|
||||
a.correct ? "text-green-500" : "text-red-400"
|
||||
}
|
||||
>
|
||||
{a.answer || "—"}
|
||||
</span>
|
||||
{pointsBy.get(a.playerId) ? (
|
||||
<span className="text-muted-foreground">
|
||||
{" "}
|
||||
+{pointsBy.get(a.playerId)}
|
||||
</span>
|
||||
) : null}
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</details>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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) -----------------------------
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue