release/0.1.0 #18

Merged
ayoub merged 68 commits from release/0.1.0 into main 2026-06-11 18:36:42 +00:00
5 changed files with 121 additions and 41 deletions
Showing only changes of commit f7ee2204d1 - Show all commits

View file

@ -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): {

View file

@ -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,
}
}
}

View file

@ -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,
}
}
}

View file

@ -332,47 +332,72 @@ 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) => (
<li key={r.index} className="bg-muted/40 flex gap-3 rounded-lg p-2">
{r.youtubeId ? (
<img
src={`https://img.youtube.com/vi/${r.youtubeId}/mqdefault.jpg`}
alt=""
className="h-12 w-16 shrink-0 rounded object-cover"
/>
) : r.imageUrl ? (
<img
src={recapImage(r.imageUrl)}
alt=""
className="h-12 w-16 shrink-0 rounded object-contain"
/>
) : 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")}
</p>
{r.type !== "blindtest" && (
<p className="line-clamp-1 text-xs">{r.label}</p>
)}
<p className="text-sm font-medium">
{r.answer}
{r.addedBy && (
<span className="text-muted-foreground font-normal">
{" "}
· {r.addedBy}
</span>
{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
src={`https://img.youtube.com/vi/${r.youtubeId}/mqdefault.jpg`}
alt=""
className="h-12 w-16 shrink-0 rounded object-cover"
/>
) : r.imageUrl ? (
<img
src={recapImage(r.imageUrl)}
alt=""
className="h-12 w-16 shrink-0 rounded object-contain"
/>
) : 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")}
</p>
{r.type !== "blindtest" && (
<p className="line-clamp-1 text-xs">{r.label}</p>
)}
</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>
</div>
</li>
))}
<p className="text-sm font-medium">
{r.answer}
{r.addedBy && (
<span className="text-muted-foreground font-normal">
{" "}
· {r.addedBy}
</span>
)}
</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>
)

View file

@ -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) -----------------------------