// Épreuve Blindtest : un titre = une manche. Un DJ neutre (jamais le // contributeur) pilote la lecture ; tout le monde vote à l'aveugle. import type { Answer, BlindtestMode, BlindtestPerPlayerResult, BlindtestPlayerResult, BlindtestRevealTruth, BlindtestRoundPayload, ScoreDelta, } from "@nerdware/shared" import type { GameRound, RoundContext, RoundStart } from "../../round" import type { BlindtestTrack, ServerRoom } from "../../../rooms" import { fuzzyMatch } from "../../match" import { takeTrack } from "./pool" const TITLE_POINTS = 60 const ARTIST_POINTS = 40 const WHO_POINTS = 100 interface BlindtestRoundData { track: BlindtestTrack } /** Tire un DJ neutre : un joueur connecté qui n'a pas soumis ce titre. */ function pickDj(room: ServerRoom, submittedBy: string): string | null { const eligible = [...room.players.values()].filter( (p) => p.connected && p.id !== submittedBy ) if (eligible.length === 0) { return null } return eligible[Math.floor(Math.random() * eligible.length)].id } /** Calcule le résultat d'un vote selon le mode. */ function resultFor( mode: BlindtestMode, answer: Answer | undefined, track: BlindtestTrack ): BlindtestPlayerResult { const a = (answer ?? {}) as { title?: string artist?: string guessedPlayerId?: string } let points = 0 const result: BlindtestPlayerResult = { points: 0 } if (mode === "title_artist" || mode === "mixed") { result.titleCorrect = a.title ? fuzzyMatch(a.title, track.title) : false result.artistCorrect = a.artist ? fuzzyMatch(a.artist, track.artist) : false if (result.titleCorrect) points += TITLE_POINTS if (result.artistCorrect) points += ARTIST_POINTS } if (mode === "who_added" || mode === "mixed") { result.guessedCorrect = a.guessedPlayerId === track.submittedBy if (result.guessedCorrect) points += WHO_POINTS } result.points = points return result } export class BlindtestRound implements GameRound { readonly type = "blindtest" as const start(room: ServerRoom): RoundStart { const track = takeTrack(room) const djId = track ? pickDj(room, track.submittedBy) : null const payload: BlindtestRoundPayload | null = track ? { trackId: track.id, youtubeId: track.youtubeId } : null return { djId, payload, data: track ? { track } : null } } submitAnswer(ctx: RoundContext, playerId: string, answer: Answer): void { // Premier vote verrouillé (idempotent). if (ctx.votes.has(playerId)) { return } ctx.votes.set(playerId, answer) } reveal(ctx: RoundContext): { truth: BlindtestRevealTruth perPlayerResult: BlindtestPerPlayerResult } { const { track } = ctx.data as BlindtestRoundData const mode = ctx.room.settings.blindtestMode const perPlayerResult: BlindtestPerPlayerResult = {} for (const player of ctx.room.players.values()) { perPlayerResult[player.id] = resultFor(mode, ctx.votes.get(player.id), track) } const submitter = ctx.room.players.get(track.submittedBy) return { truth: { title: track.title, artist: track.artist, youtubeId: track.youtubeId, submittedBy: track.submittedBy, submittedByName: submitter?.name ?? "?", }, perPlayerResult, } } score(ctx: RoundContext): ScoreDelta[] { const { track } = ctx.data as BlindtestRoundData const mode = ctx.room.settings.blindtestMode const deltas: ScoreDelta[] = [] for (const [playerId, answer] of ctx.votes) { // Le contributeur ne marque pas sur son propre titre. if (playerId === track.submittedBy) { continue } const { points } = resultFor(mode, answer, track) if (points > 0) { deltas.push({ playerId, delta: points }) } } return deltas } }