Merge pull request 'feat: rebus game mode' (#25) from feature/rebus-mode into dev

Reviewed-on: #25
This commit is contained in:
ayoub 2026-07-06 12:48:22 +00:00
commit 69811e0667
12 changed files with 184 additions and 29 deletions

View file

@ -10,7 +10,7 @@ import { env } from "../env"
import { db } from "../db" import { db } from "../db"
import { quizCategory, quizQuestion, type NewQuizQuestion } from "../db/schema" import { quizCategory, quizQuestion, type NewQuizQuestion } from "../db/schema"
const FORMATS = ["mcq", "truefalse", "free", "image_reveal"] as const const FORMATS = ["mcq", "truefalse", "free", "image_reveal", "rebus"] as const
type AdminFormat = (typeof FORMATS)[number] type AdminFormat = (typeof FORMATS)[number]
const IMAGE_EXT = [".jpg", ".jpeg", ".png", ".webp", ".gif", ".avif"] const IMAGE_EXT = [".jpg", ".jpeg", ".png", ".webp", ".gif", ".avif"]
@ -56,7 +56,7 @@ function validate(
lang: body.lang === "en" ? "en" : "fr", lang: body.lang === "en" ? "en" : "fr",
} }
if (format === "free" || format === "image_reveal") { if (format === "free" || format === "image_reveal" || format === "rebus") {
const answers = (body.acceptedAnswers ?? []) const answers = (body.acceptedAnswers ?? [])
.map((a) => a.trim()) .map((a) => a.trim())
.filter(Boolean) .filter(Boolean)

View file

@ -17,6 +17,7 @@ const poolByRoom = new WeakMap<ServerRoom, QueueItem[]>()
const QUIZ_FORMATS: QuizFormat[] = ["mcq", "truefalse", "free"] const QUIZ_FORMATS: QuizFormat[] = ["mcq", "truefalse", "free"]
const IMAGE_FORMATS: QuizFormat[] = ["image_reveal"] const IMAGE_FORMATS: QuizFormat[] = ["image_reveal"]
const REBUS_FORMATS: QuizFormat[] = ["rebus"]
function shuffle<T>(items: T[]): T[] { function shuffle<T>(items: T[]): T[] {
const arr = [...items] const arr = [...items]
@ -32,7 +33,7 @@ async function loadGroup(
room: ServerRoom, room: ServerRoom,
need: number, need: number,
formats: QuizFormat[], formats: QuizFormat[],
isImage: boolean, allowHardcoded: boolean,
categories: string[] categories: string[]
): Promise<QueueItem[]> { ): Promise<QueueItem[]> {
if (need <= 0) { if (need <= 0) {
@ -47,8 +48,8 @@ async function loadGroup(
console.error("[quiz] chargement DB échoué, fallback banque en dur", err) console.error("[quiz] chargement DB échoué, fallback banque en dur", err)
} }
} }
if (items.length < need && !isImage) { if (items.length < need && allowHardcoded) {
// Complément depuis la banque en dur (uniquement le quiz, pas d'images en dur). // Complément depuis la banque en dur (quiz + rébus ; pas d'images en dur).
const bank = shuffle( const bank = shuffle(
QUIZ_QUESTIONS.filter( QUIZ_QUESTIONS.filter(
(q) => (q) =>
@ -68,16 +69,24 @@ async function loadGroup(
export async function prepareQuizForRoom(room: ServerRoom): Promise<void> { export async function prepareQuizForRoom(room: ServerRoom): Promise<void> {
const quizRounds = room.settings.rounds.filter((r) => r.type === "quiz") const quizRounds = room.settings.rounds.filter((r) => r.type === "quiz")
const needImage = quizRounds.filter((r) => r.pool === "image").length const needImage = quizRounds.filter((r) => r.pool === "image").length
const needQuiz = quizRounds.length - needImage const needRebus = quizRounds.filter((r) => r.pool === "rebus").length
const needQuiz = quizRounds.length - needImage - needRebus
const cats = room.settings.categories const cats = room.settings.categories
const quizGroup = await loadGroup(room, needQuiz, QUIZ_FORMATS, false, cats) const quizGroup = await loadGroup(room, needQuiz, QUIZ_FORMATS, true, cats)
const imageGroup = await loadGroup(room, needImage, IMAGE_FORMATS, true, cats) const imageGroup = await loadGroup(room, needImage, IMAGE_FORMATS, false, cats)
// Les rébus ne sont pas filtrés par catégorie (catégorie « Rébus » à part).
const rebusGroup = await loadGroup(room, needRebus, REBUS_FORMATS, true, [])
// On assemble la file dans l'ordre des manches (chacune pioche dans son sous-pool). // On assemble la file dans l'ordre des manches (chacune pioche dans son sous-pool).
const queue: QueueItem[] = [] const queue: QueueItem[] = []
for (const r of quizRounds) { for (const r of quizRounds) {
const group = r.pool === "image" ? imageGroup : quizGroup const group =
r.pool === "image"
? imageGroup
: r.pool === "rebus"
? rebusGroup
: quizGroup
const item = group.shift() const item = group.shift()
if (item) { if (item) {
queue.push(item) queue.push(item)

View file

@ -154,4 +154,82 @@ export const QUIZ_QUESTIONS: QuizQuestion[] = [
category: "Manga", category: "Manga",
difficulty: 3, difficulty: 3,
}, },
// Rébus : devine l'œuvre à partir des emojis (réponse libre, matching tolérant).
{
id: "r-sonic",
format: "rebus",
prompt: "🦔 + 💨 + 💍",
acceptedAnswers: ["Sonic", "Sonic the Hedgehog"],
category: "Rébus",
difficulty: 1,
},
{
id: "r-mario",
format: "rebus",
prompt: "🍄 + 🔨 + 🐢",
acceptedAnswers: ["Mario", "Super Mario", "Super Mario Bros"],
category: "Rébus",
difficulty: 1,
},
{
id: "r-pacman",
format: "rebus",
prompt: "🟡 + 👻 + 🕹️",
acceptedAnswers: ["Pac-Man", "Pacman", "Pac Man"],
category: "Rébus",
difficulty: 1,
},
{
id: "r-pokemon",
format: "rebus",
prompt: "⚡ + 🐭 + 🔴⚪",
acceptedAnswers: ["Pokémon", "Pokemon"],
category: "Rébus",
difficulty: 1,
},
{
id: "r-harry-potter",
format: "rebus",
prompt: "⚡ + 🧙‍♂️ + 🎓",
acceptedAnswers: ["Harry Potter", "Harry"],
category: "Rébus",
difficulty: 2,
},
{
id: "r-lotr",
format: "rebus",
prompt: "💍 + 🌋 + 🧙‍♂️",
acceptedAnswers: [
"Le Seigneur des Anneaux",
"Seigneur des Anneaux",
"Lord of the Rings",
"LOTR",
],
category: "Rébus",
difficulty: 2,
},
{
id: "r-star-wars",
format: "rebus",
prompt: "⭐ + ⚔️ + 🌌",
acceptedAnswers: ["Star Wars", "La Guerre des Étoiles"],
category: "Rébus",
difficulty: 1,
},
{
id: "r-minecraft",
format: "rebus",
prompt: "⛏️ + 🟩 + 🧟",
acceptedAnswers: ["Minecraft"],
category: "Rébus",
difficulty: 1,
},
{
id: "r-zelda",
format: "rebus",
prompt: "🗡️ + 🛡️ + 🧝‍♂️ + 🐴",
acceptedAnswers: ["Zelda", "The Legend of Zelda", "Legend of Zelda"],
category: "Rébus",
difficulty: 2,
},
] ]

View file

@ -44,7 +44,11 @@ function asText(answer: Answer): string | null {
/** Formats à réponse libre (saisie texte + matching tolérant). */ /** Formats à réponse libre (saisie texte + matching tolérant). */
function isTextFormat(question: QuizQuestion): boolean { function isTextFormat(question: QuizQuestion): boolean {
return question.format === "free" || question.format === "image_reveal" return (
question.format === "free" ||
question.format === "image_reveal" ||
question.format === "rebus"
)
} }
/** Réponse correcte ? Selon le format de la question. */ /** Réponse correcte ? Selon le format de la question. */

View file

@ -10,6 +10,7 @@ import {
Music, Music,
Play, Play,
Plus, Plus,
Puzzle,
Shuffle, Shuffle,
Trash2, Trash2,
type LucideIcon, type LucideIcon,
@ -43,6 +44,7 @@ const GAME_TYPES: {
{ value: "mixed", Icon: Shuffle, needsThree: false }, { value: "mixed", Icon: Shuffle, needsThree: false },
{ value: "quiz", Icon: Brain, needsThree: false }, { value: "quiz", Icon: Brain, needsThree: false },
{ value: "image", Icon: ImageIcon, needsThree: false }, { value: "image", Icon: ImageIcon, needsThree: false },
{ value: "rebus", Icon: Puzzle, needsThree: false },
{ value: "blindtest", Icon: Headphones, needsThree: true }, { value: "blindtest", Icon: Headphones, needsThree: true },
] ]
const BOT_DIFFICULTIES = ["easy", "normal", "hard"] as const const BOT_DIFFICULTIES = ["easy", "normal", "hard"] as const
@ -110,6 +112,7 @@ function shuffle<T>(items: T[]): T[] {
function buildRounds(opts: { function buildRounds(opts: {
quiz: number quiz: number
image: number image: number
rebus: number
tracks: number tracks: number
shuffleOrder: boolean shuffleOrder: boolean
}): RoundConfig[] { }): RoundConfig[] {
@ -122,6 +125,10 @@ function buildRounds(opts: {
type: "quiz" as const, type: "quiz" as const,
pool: "image" as const, pool: "image" as const,
})), })),
...Array.from({ length: opts.rebus }, () => ({
type: "quiz" as const,
pool: "rebus" as const,
})),
...Array.from({ length: opts.tracks }, () => ({ type: "blindtest" as const })), ...Array.from({ length: opts.tracks }, () => ({ type: "blindtest" as const })),
] ]
return opts.shuffleOrder ? shuffle(rounds) : rounds return opts.shuffleOrder ? shuffle(rounds) : rounds
@ -154,6 +161,7 @@ export function LobbyView({ snapshot }: { snapshot: RoomSnapshot }) {
const [quizCount, setQuizCount] = useState(5) const [quizCount, setQuizCount] = useState(5)
const [imageCount, setImageCount] = useState(5) const [imageCount, setImageCount] = useState(5)
const [rebusCount, setRebusCount] = useState(5)
const [busy, setBusy] = useState(false) const [busy, setBusy] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
@ -179,6 +187,8 @@ export function LobbyView({ snapshot }: { snapshot: RoomSnapshot }) {
effectiveType === "quiz" || (isMixed && mixedModes.includes("quiz")) effectiveType === "quiz" || (isMixed && mixedModes.includes("quiz"))
const showImage = const showImage =
effectiveType === "image" || (isMixed && mixedModes.includes("image")) effectiveType === "image" || (isMixed && mixedModes.includes("image"))
const showRebus =
effectiveType === "rebus" || (isMixed && mixedModes.includes("rebus"))
const showBlindtest = const showBlindtest =
effectiveType === "blindtest" || effectiveType === "blindtest" ||
(isMixed && mixedModes.includes("blindtest") && blindtestAvailable) (isMixed && mixedModes.includes("blindtest") && blindtestAvailable)
@ -186,6 +196,7 @@ export function LobbyView({ snapshot }: { snapshot: RoomSnapshot }) {
const rounds = buildRounds({ const rounds = buildRounds({
quiz: showQuiz ? quizCount : 0, quiz: showQuiz ? quizCount : 0,
image: showImage ? imageCount : 0, image: showImage ? imageCount : 0,
rebus: showRebus ? rebusCount : 0,
tracks: showBlindtest ? totalTracks : 0, tracks: showBlindtest ? totalTracks : 0,
shuffleOrder: isMixed, shuffleOrder: isMixed,
}) })
@ -295,7 +306,7 @@ export function LobbyView({ snapshot }: { snapshot: RoomSnapshot }) {
{/* Mode de jeu */} {/* Mode de jeu */}
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<span className="text-sm font-medium">{t.lobby.gameMode}</span> <span className="text-sm font-medium">{t.lobby.gameMode}</span>
<div className="grid grid-cols-4 gap-2"> <div className="grid grid-cols-3 gap-2">
{GAME_TYPES.map(({ value, Icon, needsThree }) => { {GAME_TYPES.map(({ value, Icon, needsThree }) => {
const locked = needsThree && !blindtestAvailable const locked = needsThree && !blindtestAvailable
const active = effectiveType === value const active = effectiveType === value
@ -339,7 +350,7 @@ export function LobbyView({ snapshot }: { snapshot: RoomSnapshot }) {
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<span className="text-sm font-medium">{t.lobby.subModes}</span> <span className="text-sm font-medium">{t.lobby.subModes}</span>
<div className="flex gap-2"> <div className="flex gap-2">
{(["quiz", "image", "blindtest"] as const).map((m) => { {(["quiz", "image", "rebus", "blindtest"] as const).map((m) => {
const locked = m === "blindtest" && !blindtestAvailable const locked = m === "blindtest" && !blindtestAvailable
const on = mixedModes.includes(m) && !locked const on = mixedModes.includes(m) && !locked
const tile = ( const tile = (
@ -400,6 +411,21 @@ export function LobbyView({ snapshot }: { snapshot: RoomSnapshot }) {
</div> </div>
)} )}
{/* Nombre de rébus */}
{showRebus && (
<div className="flex items-center justify-between">
<span className="flex items-center gap-1.5 text-sm font-medium">
<Puzzle className="size-4" /> {t.lobby.rebusCount}
</span>
<Stepper
value={rebusCount}
min={1}
max={20}
onChange={setRebusCount}
/>
</div>
)}
{/* Filtre de catégories (quiz / images) */} {/* Filtre de catégories (quiz / images) */}
{(showQuiz || showImage) && allCategories.length > 0 && ( {(showQuiz || showImage) && allCategories.length > 0 && (
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">

View file

@ -35,7 +35,8 @@ export function QuizView({ snapshot }: { snapshot: RoomSnapshot }) {
const truth = reveal ? (reveal.truth as QuizRevealTruth) : null const truth = reveal ? (reveal.truth as QuizRevealTruth) : null
const showReveal = truth !== null const showReveal = truth !== null
const isImage = question.format === "image_reveal" const isImage = question.format === "image_reveal"
const isText = question.format === "free" || isImage const isRebus = question.format === "rebus"
const isText = question.format === "free" || isImage || isRebus
const myResult = reveal const myResult = reveal
? (reveal.perPlayerResult as QuizPerPlayerResult)[playerId ?? ""] ? (reveal.perPlayerResult as QuizPerPlayerResult)[playerId ?? ""]
: undefined : undefined
@ -74,9 +75,15 @@ export function QuizView({ snapshot }: { snapshot: RoomSnapshot }) {
)} )}
</header> </header>
<h2 className="font-heading text-xl font-semibold text-balance"> {isRebus ? (
{question.prompt} <div className="bg-card flex min-h-32 items-center justify-center rounded-2xl p-6 text-center text-4xl leading-relaxed text-balance sm:text-5xl">
</h2> {question.prompt}
</div>
) : (
<h2 className="font-heading text-xl font-semibold text-balance">
{question.prompt}
</h2>
)}
{isImage && question.imageUrl && ( {isImage && question.imageUrl && (
<ImageReveal <ImageReveal

View file

@ -4,7 +4,7 @@ import { AnimatePresence, motion } from "framer-motion"
import { useI18n } from "@/i18n/context" import { useI18n } from "@/i18n/context"
/** Catégorie visuelle de la transition (image_reveal est distinct du quiz). */ /** Catégorie visuelle de la transition (image_reveal est distinct du quiz). */
export type TransitionKind = "quiz" | "image" | "blindtest" export type TransitionKind = "quiz" | "image" | "rebus" | "blindtest"
const VISIBLE_MS = 1300 // durée d'affichage avant le wipe de sortie (≈ leadMs serveur) const VISIBLE_MS = 1300 // durée d'affichage avant le wipe de sortie (≈ leadMs serveur)
@ -19,11 +19,17 @@ const ALL_MEDIA = import.meta.glob(
{ eager: true, import: "default" } { eager: true, import: "default" }
) as Record<string, string> ) as Record<string, string>
const MEDIA: Record<TransitionKind, string[]> = { quiz: [], image: [], blindtest: [] } const MEDIA: Record<TransitionKind, string[]> = {
quiz: [],
image: [],
rebus: [],
blindtest: [],
}
const SHARED_MEDIA: string[] = [] const SHARED_MEDIA: string[] = []
for (const [path, url] of Object.entries(ALL_MEDIA)) { for (const [path, url] of Object.entries(ALL_MEDIA)) {
if (path.includes("/transitions/quiz/")) MEDIA.quiz.push(url) if (path.includes("/transitions/quiz/")) MEDIA.quiz.push(url)
else if (path.includes("/transitions/image/")) MEDIA.image.push(url) else if (path.includes("/transitions/image/")) MEDIA.image.push(url)
else if (path.includes("/transitions/rebus/")) MEDIA.rebus.push(url)
else if (path.includes("/transitions/blindtest/")) MEDIA.blindtest.push(url) else if (path.includes("/transitions/blindtest/")) MEDIA.blindtest.push(url)
else SHARED_MEDIA.push(url) else SHARED_MEDIA.push(url)
} }
@ -46,6 +52,13 @@ const THEME: Record<
kind: "Image", kind: "Image",
emoji: "🖼️", emoji: "🖼️",
}, },
rebus: {
gradient: "from-amber-500 via-orange-600 to-rose-600",
accent: "text-yellow-200",
label: "RÉBUS",
kind: "Rébus",
emoji: "🧩",
},
blindtest: { blindtest: {
gradient: "from-fuchsia-500 via-purple-600 to-indigo-700", gradient: "from-fuchsia-500 via-purple-600 to-indigo-700",
accent: "text-yellow-300", accent: "text-yellow-300",

View file

@ -60,6 +60,7 @@ export const en: Dict = {
quizCount: "Quiz questions", quizCount: "Quiz questions",
imageCount: "Images to guess", imageCount: "Images to guess",
imageHint: "Requires “Image” questions created in the back-office.", imageHint: "Requires “Image” questions created in the back-office.",
rebusCount: "Rebuses to guess",
categories: "Categories", categories: "Categories",
allCategories: "All", allCategories: "All",
botLevel: "🤖 Bot difficulty", botLevel: "🤖 Bot difficulty",
@ -83,11 +84,13 @@ export const en: Dict = {
mixed: "Mixed", mixed: "Mixed",
quiz: "Quiz", quiz: "Quiz",
image: "Images", image: "Images",
rebus: "Rebus",
blindtest: "Blindtest", blindtest: "Blindtest",
}, },
mixModes: { mixModes: {
quiz: "Quiz", quiz: "Quiz",
image: "Images", image: "Images",
rebus: "Rebus",
blindtest: "Blindtest", blindtest: "Blindtest",
}, },
blindtestModes: { blindtestModes: {
@ -142,6 +145,7 @@ export const en: Dict = {
kind: { kind: {
quiz: "Question", quiz: "Question",
image: "Image", image: "Image",
rebus: "Rebus",
blindtest: "Track", blindtest: "Track",
}, },
ready: "READY ?!", ready: "READY ?!",

View file

@ -58,6 +58,7 @@ export const fr = {
quizCount: "Questions de quiz", quizCount: "Questions de quiz",
imageCount: "Images à deviner", imageCount: "Images à deviner",
imageHint: "Nécessite des questions « Image » créées dans le back-office.", imageHint: "Nécessite des questions « Image » créées dans le back-office.",
rebusCount: "Rébus à deviner",
categories: "Catégories", categories: "Catégories",
allCategories: "Toutes", allCategories: "Toutes",
botLevel: "🤖 Niveau des bots", botLevel: "🤖 Niveau des bots",
@ -81,11 +82,13 @@ export const fr = {
mixed: "Mixte", mixed: "Mixte",
quiz: "Quiz", quiz: "Quiz",
image: "Images", image: "Images",
rebus: "Rébus",
blindtest: "Blindtest", blindtest: "Blindtest",
}, },
mixModes: { mixModes: {
quiz: "Quiz", quiz: "Quiz",
image: "Images", image: "Images",
rebus: "Rébus",
blindtest: "Blindtest", blindtest: "Blindtest",
}, },
blindtestModes: { blindtestModes: {
@ -141,6 +144,7 @@ export const fr = {
kind: { kind: {
quiz: "Question", quiz: "Question",
image: "Image", image: "Image",
rebus: "Rébus",
blindtest: "Titre", blindtest: "Titre",
}, },
ready: "PRÊT ?!", ready: "PRÊT ?!",

View file

@ -180,7 +180,9 @@ function QuestionRow({
{q.lang.toUpperCase()} · {q.source} {q.lang.toUpperCase()} · {q.source}
{!q.active && " · désactivée"} {!q.active && " · désactivée"}
</p> </p>
{q.format === "free" || q.format === "image_reveal" ? ( {q.format === "free" ||
q.format === "image_reveal" ||
q.format === "rebus" ? (
<p className="text-muted-foreground text-xs"> <p className="text-muted-foreground text-xs">
{q.acceptedAnswers?.join(", ")} {q.acceptedAnswers?.join(", ")}
</p> </p>
@ -258,7 +260,7 @@ function QuestionForm({ onCreated }: { onCreated: () => void }) {
function submit() { function submit() {
const input: NewQuestionInput = { format, prompt, category, difficulty, lang } const input: NewQuestionInput = { format, prompt, category, difficulty, lang }
if (format === "free") { if (format === "free" || format === "rebus") {
input.acceptedAnswers = answers input.acceptedAnswers = answers
.split("\n") .split("\n")
.map((a) => a.trim()) .map((a) => a.trim())
@ -296,6 +298,7 @@ function QuestionForm({ onCreated }: { onCreated: () => void }) {
<option value="truefalse">Vrai / Faux</option> <option value="truefalse">Vrai / Faux</option>
<option value="free">Réponse libre</option> <option value="free">Réponse libre</option>
<option value="image_reveal">Image à deviner</option> <option value="image_reveal">Image à deviner</option>
<option value="rebus">Rébus</option>
</select> </select>
<select <select
className={inputClass} className={inputClass}
@ -412,7 +415,7 @@ function QuestionForm({ onCreated }: { onCreated: () => void }) {
</div> </div>
)} )}
{(format === "free" || format === "image_reveal") && ( {(format === "free" || format === "image_reveal" || format === "rebus") && (
<textarea <textarea
className={`${inputClass} h-24 py-2`} className={`${inputClass} h-24 py-2`}
placeholder="Réponses acceptées (une par ligne)" placeholder="Réponses acceptées (une par ligne)"

View file

@ -68,8 +68,8 @@ interface RoomState {
mediaSync: MediaSyncPayload | null mediaSync: MediaSyncPayload | null
boomKey: number boomKey: number
roundKey: number roundKey: number
/** Catégorie visuelle de la manche (image_reveal distinct du quiz). */ /** Catégorie visuelle de la manche (image_reveal / rébus distincts du quiz). */
roundKind: "quiz" | "image" | "blindtest" roundKind: "quiz" | "image" | "rebus" | "blindtest"
/** Le mode a-t-il changé par rapport à la manche précédente ? */ /** Le mode a-t-il changé par rapport à la manche précédente ? */
roundModeChanged: boolean roundModeChanged: boolean
@ -321,7 +321,9 @@ socket.on("round:start", (payload) =>
? "blindtest" ? "blindtest"
: fmt === "image_reveal" : fmt === "image_reveal"
? "image" ? "image"
: "quiz" : fmt === "rebus"
? "rebus"
: "quiz"
return { return {
round: { round: {
type: payload.type, type: payload.type,

View file

@ -8,10 +8,10 @@ export type RoomStatus = "lobby" | "in_round" | "reveal" | "scores" | "ended"
export type RoundType = "blindtest" | "quiz" export type RoundType = "blindtest" | "quiz"
/** Type de partie choisi au lobby : mélange par défaut, ou un seul mode. */ /** Type de partie choisi au lobby : mélange par défaut, ou un seul mode. */
export type GameType = "mixed" | "quiz" | "image" | "blindtest" export type GameType = "mixed" | "quiz" | "image" | "rebus" | "blindtest"
/** Sous-modes activables quand la partie est "mixed". */ /** Sous-modes activables quand la partie est "mixed". */
export type MixMode = "quiz" | "image" | "blindtest" export type MixMode = "quiz" | "image" | "rebus" | "blindtest"
/** Modes de blindtest, déterminent la forme du vote et le barème. */ /** Modes de blindtest, déterminent la forme du vote et le barème. */
export type BlindtestMode = "title_artist" | "who_added" | "mixed" export type BlindtestMode = "title_artist" | "who_added" | "mixed"
@ -20,7 +20,12 @@ export type BlindtestMode = "title_artist" | "who_added" | "mixed"
export type BotDifficulty = "easy" | "normal" | "hard" export type BotDifficulty = "easy" | "normal" | "hard"
/** Formats de question quiz. */ /** Formats de question quiz. */
export type QuizFormat = "mcq" | "truefalse" | "free" | "image_reveal" export type QuizFormat =
| "mcq"
| "truefalse"
| "free"
| "image_reveal"
| "rebus"
/** Joueur connecté à une room. */ /** Joueur connecté à une room. */
export interface Player { export interface Player {
@ -34,8 +39,8 @@ export interface Player {
/** Configuration d'une épreuve planifiée dans la séquence de la partie. */ /** Configuration d'une épreuve planifiée dans la séquence de la partie. */
export interface RoundConfig { export interface RoundConfig {
type: RoundType type: RoundType
/** Pour une manche quiz : dans quel sous-pool piocher (quiz "classique" ou images). */ /** Pour une manche quiz : dans quel sous-pool piocher (quiz "classique", images ou rébus). */
pool?: "quiz" | "image" pool?: "quiz" | "image" | "rebus"
} }
/** Réglages de la room, modifiables dans le lobby par l'hôte. */ /** Réglages de la room, modifiables dans le lobby par l'hôte. */