// Épreuve Blindtest : un titre = une manche. Le contributeur du titre est le DJ // (il pilote la lecture, son identité reste cachée aux autres). Il ne marque pas // de points, SAUF en "qui l'a ajouté" : il gagne quand un joueur le devine mal. 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 /** Points gagnés par le contributeur pour chaque joueur qui le devine mal. */ const MISDIRECTION_POINTS = 50 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 } /** Résultat d'un joueur VOTANT (≠ contributeur) selon le mode. */ function voterResult( 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) // DJ neutre (jamais le contributeur) : il pilote la lecture et vote à l'aveugle. const djId = track ? pickDj(room, track.submittedBy) : null const payload: BlindtestRoundPayload | null = track ? { trackId: track.id, youtubeId: track.youtubeId } : null return { djId, payload, secretPlayerId: track?.submittedBy, data: track ? { track } : null, } } submitAnswer(ctx: RoundContext, playerId: string, answer: Answer): void { // Premier vote verrouillé (idempotent). Le contributeur peut voter mais son // vote ne lui rapporte rien (voir buildResults) ; ça évite de bloquer la fin. if (ctx.votes.has(playerId)) { return } ctx.votes.set(playerId, answer) } /** Résultat par joueur (votants + bonus de tromperie du contributeur). */ private buildResults(ctx: RoundContext): BlindtestPerPlayerResult { const { track } = ctx.data as BlindtestRoundData const mode = ctx.room.settings.blindtestMode const perPlayer: BlindtestPerPlayerResult = {} let wrongGuessers = 0 for (const player of ctx.room.players.values()) { if (player.id === track.submittedBy) { continue } const result = voterResult(mode, ctx.votes.get(player.id), track) perPlayer[player.id] = result if ( (mode === "who_added" || mode === "mixed") && result.guessedCorrect === false ) { wrongGuessers++ } } // Contributeur : pas de points hors who_added/mixed ; sinon tromperie. const misdirection = mode === "who_added" || mode === "mixed" ? wrongGuessers * MISDIRECTION_POINTS : 0 perPlayer[track.submittedBy] = { points: misdirection } return perPlayer } reveal(ctx: RoundContext): { truth: BlindtestRevealTruth perPlayerResult: BlindtestPerPlayerResult } { const { track } = ctx.data as BlindtestRoundData 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: this.buildResults(ctx), } } score(ctx: RoundContext): ScoreDelta[] { const perPlayer = this.buildResults(ctx) const deltas: ScoreDelta[] = [] for (const [playerId, result] of Object.entries(perPlayer)) { if (result.points > 0) { deltas.push({ playerId, delta: result.points }) } } return deltas } }