nerdware/packages/shared/src/domain.ts
AyoubBenziza 42a36fc442 feat: blindtest needs 2 real players + bot difficulty setting
- blindtest is only available with >=3 players AND >=2 humans (a bot can't be DJ):
  lobby gate + tooltips updated, and server game:start rejects otherwise
- new botDifficulty setting (easy/normal/hard) drives how often bots answer
  correctly (quiz + blindtest who-added / title-artist); host picks it in the
  lobby when at least one bot is present

Verified e2e: blindtest start rejected with 1 human + 2 bots (NEED_THREE).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 18:03:03 +02:00

152 lines
4.7 KiB
TypeScript

// Modèle de domaine partagé entre client et serveur.
// Le serveur est la source de vérité ; le client n'affiche qu'un état reçu.
/** Statut courant d'une room (machine à états du jeu). */
export type RoomStatus = "lobby" | "in_round" | "reveal" | "scores" | "ended"
/** Types d'épreuves disponibles. Étendre = ajouter une valeur + un module GameRound. */
export type RoundType = "blindtest" | "quiz"
/** Type de partie choisi au lobby : mélange par défaut, ou un seul mode. */
export type GameType = "mixed" | "quiz" | "image" | "blindtest"
/** Sous-modes activables quand la partie est "mixed". */
export type MixMode = "quiz" | "image" | "blindtest"
/** Modes de blindtest, déterminent la forme du vote et le barème. */
export type BlindtestMode = "title_artist" | "who_added" | "mixed"
/** Niveau des bots : pilote leur probabilité de répondre juste. */
export type BotDifficulty = "easy" | "normal" | "hard"
/** Formats de question quiz. */
export type QuizFormat = "mcq" | "truefalse" | "free" | "image_reveal"
/** Joueur connecté à une room. */
export interface Player {
id: string
name: string
connected: boolean
/** Joueur virtuel (bot) géré par le serveur. */
bot?: boolean
}
/** Configuration d'une épreuve planifiée dans la séquence de la partie. */
export interface RoundConfig {
type: RoundType
/** Pour une manche quiz : dans quel sous-pool piocher (quiz "classique" ou images). */
pool?: "quiz" | "image"
}
/** Réglages de la room, modifiables dans le lobby par l'hôte. */
export interface RoomSettings {
/** Type de partie choisi dans le lobby (pilote l'UI partagée). */
gameType: GameType
/** Sous-modes actifs quand gameType === "mixed". */
mixedModes: MixMode[]
blindtestMode: BlindtestMode
/** Durée d'une manche en secondes (def. 60). */
roundDuration: number
/** Nombre de titres que chaque joueur soumet (blindtest). */
tracksPerPlayer: number
/** Catégories de quiz autorisées (vide = toutes). */
categories: string[]
/** Niveau des bots ajoutés à la partie. */
botDifficulty: BotDifficulty
/** Séquence d'épreuves de la partie. */
rounds: RoundConfig[]
}
/** Réglages par défaut d'une nouvelle room. */
export const DEFAULT_ROOM_SETTINGS: RoomSettings = {
gameType: "mixed",
mixedModes: ["quiz", "image"],
blindtestMode: "title_artist",
roundDuration: 60,
tracksPerPlayer: 2,
categories: [],
botDifficulty: "normal",
rounds: [],
}
// --- Votes ---------------------------------------------------------------
/** Vote blindtest, forme selon RoomSettings.blindtestMode. */
export type BlindtestAnswer =
| { title: string; artist: string }
| { guessedPlayerId: string }
| { title: string; artist: string; guessedPlayerId: string }
/** Vote quiz, forme selon le format de la question. */
export type QuizAnswer = { choiceIndex: number } | { text: string }
/** Réponse soumise par un joueur pour la manche en cours. */
export type Answer = BlindtestAnswer | QuizAnswer
// --- Scores --------------------------------------------------------------
/** Score d'un joueur. */
export interface PlayerScore {
playerId: string
score: number
}
/** Variation de score appliquée à l'issue d'une manche. */
export interface ScoreDelta {
playerId: string
delta: number
}
/** Joueur ayant marqué sur une manche (récap de fin de partie). */
export interface RoundRecapScorer {
playerId: string
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
type: RoundType
/** Intitulé (question quiz) ou libellé du mode. */
label: string
/** Bonne réponse révélée. */
answer: string
category?: string
imageUrl?: string
youtubeId?: string
url?: string
/** Blindtest : qui avait ajouté le titre. */
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) -----------------------------
/**
* Vue publique de la room poussée à tous les clients.
* Ne contient JAMAIS de réponse non révélée ni d'info secrète (ex: submittedBy).
*/
export interface RoomSnapshot {
code: string
status: RoomStatus
hostId: string
players: Player[]
settings: RoomSettings
scores: PlayerScore[]
/** Index de la manche en cours, -1 dans le lobby. */
currentRound: number
/** Nombre total de manches planifiées. */
totalRounds: number
/** Blindtest : nombre de titres soumis par joueur (comptes seulement, jamais les titres). */
submissions: { playerId: string; count: number }[]
}