- DJ is now the track's contributor (controls their own song); their identity is sent only to them so "who added" stays non-trivial. The contributor doesn't vote and is excluded from the eligible-voters count. - Scoring: the contributor earns nothing except in who_added/mixed, where they get a misdirection bonus (50) per player who guesses the wrong adder. - Blindtest (and mixed) require >= 3 connected players: lobby disables the options with a hint, and game:start rejects with NEED_THREE. - Player card is now 16:9 and full width — the video banner shows at reveal; DJ gets a seek bar (current/duration) to jump anywhere, plus play/pause/restart. - Mixed game order is shuffled (no predictable quiz/blindtest alternation). Verified e2e (3 players): guard rejects at 2, DJ id hidden from others, misdirection scoring correct. 23 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
128 lines
4.8 KiB
TypeScript
128 lines
4.8 KiB
TypeScript
import { describe, expect, test } from "bun:test"
|
|
import { RoomManager, type BlindtestTrack } from "../../../rooms"
|
|
import type { RoundContext } from "../../round"
|
|
import { BlindtestRound } from "./blindtest-round"
|
|
import { prepareBlindtestForRoom } from "./pool"
|
|
import { fuzzyMatch, normalize } from "../../match"
|
|
|
|
function track(submittedBy: string): BlindtestTrack {
|
|
return {
|
|
id: "t1",
|
|
youtubeId: "abc12345678",
|
|
url: "https://youtu.be/abc12345678",
|
|
title: "The Legend of Zelda Main Theme",
|
|
artist: "Koji Kondo",
|
|
submittedBy,
|
|
}
|
|
}
|
|
|
|
function ctxFor(
|
|
room: ReturnType<RoomManager["create"]>["room"],
|
|
t: BlindtestTrack,
|
|
mode: "title_artist" | "who_added" | "mixed"
|
|
): RoundContext {
|
|
room.settings.blindtestMode = mode
|
|
return {
|
|
room,
|
|
djId: null,
|
|
votes: new Map(),
|
|
startedAt: 0,
|
|
endsAt: 10_000,
|
|
data: { track: t },
|
|
}
|
|
}
|
|
|
|
describe("match", () => {
|
|
test("normalize retire accents/ponctuation/casse", () => {
|
|
expect(normalize("Pokémon: Rouge & Bleu!")).toBe("pokemon rouge bleu")
|
|
})
|
|
test("fuzzyMatch tolère fautes et réponses partielles", () => {
|
|
expect(fuzzyMatch("zelda", "The Legend of Zelda")).toBe(true)
|
|
expect(fuzzyMatch("koji kondo", "Koji Kondo")).toBe(true)
|
|
expect(fuzzyMatch("mario", "The Legend of Zelda")).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("BlindtestRound", () => {
|
|
test("start : le DJ est le contributeur du titre", () => {
|
|
const rooms = new RoomManager()
|
|
const { room, player: a } = rooms.create("Alice", "sa")
|
|
rooms.join(room.code, "Bob", "sb")
|
|
room.blindtestTracks = [track(a.id)]
|
|
prepareBlindtestForRoom(room)
|
|
|
|
const round = new BlindtestRound()
|
|
const start = round.start(room)
|
|
expect(start.djId).toBe(a.id) // Alice a ajouté le titre → elle est DJ
|
|
expect(start.payload).toEqual({ trackId: "t1", youtubeId: "abc12345678" })
|
|
})
|
|
|
|
test("who_added : le contributeur gagne par tromperie (mauvaises devinettes)", () => {
|
|
const rooms = new RoomManager()
|
|
const { room, player: a } = rooms.create("Alice", "sa")
|
|
const { player: b } = rooms.join(room.code, "Bob", "sb")
|
|
const { player: c } = rooms.join(room.code, "Carol", "sc")
|
|
const t = track(a.id) // Alice = contributrice/DJ
|
|
const round = new BlindtestRound()
|
|
const ctx = ctxFor(room, t, "who_added")
|
|
round.submitAnswer(ctx, b.id, { guessedPlayerId: c.id }) // faux
|
|
round.submitAnswer(ctx, c.id, { guessedPlayerId: a.id }) // juste
|
|
round.submitAnswer(ctx, a.id, { guessedPlayerId: b.id }) // ignoré (contributrice)
|
|
const deltas = round.score(ctx)
|
|
// Carol devine juste → 100 ; Alice trompe Bob (1 mauvaise devinette) → 50
|
|
expect(deltas).toContainEqual({ playerId: c.id, delta: 100 })
|
|
expect(deltas).toContainEqual({ playerId: a.id, delta: 50 })
|
|
expect(deltas.find((d) => d.playerId === b.id)).toBeUndefined()
|
|
})
|
|
|
|
test("who_added : points si on devine le bon contributeur", () => {
|
|
const rooms = new RoomManager()
|
|
const { room, player: a } = rooms.create("Alice", "sa")
|
|
const { player: b } = rooms.join(room.code, "Bob", "sb")
|
|
const t = track(a.id)
|
|
const round = new BlindtestRound()
|
|
const ctx = ctxFor(room, t, "who_added")
|
|
round.submitAnswer(ctx, b.id, { guessedPlayerId: a.id }) // juste
|
|
const deltas = round.score(ctx)
|
|
expect(deltas).toEqual([{ playerId: b.id, delta: 100 }])
|
|
})
|
|
|
|
test("title_artist : 60 titre + 40 artiste, fuzzy", () => {
|
|
const rooms = new RoomManager()
|
|
const { room, player: a } = rooms.create("Alice", "sa")
|
|
const { player: b } = rooms.join(room.code, "Bob", "sb")
|
|
const t = track(a.id)
|
|
const round = new BlindtestRound()
|
|
const ctx = ctxFor(room, t, "title_artist")
|
|
round.submitAnswer(ctx, b.id, { title: "zelda", artist: "koji kondo" })
|
|
const { perPlayerResult } = round.reveal(ctx)
|
|
expect(perPlayerResult[b.id]).toMatchObject({
|
|
titleCorrect: true,
|
|
artistCorrect: true,
|
|
points: 100,
|
|
})
|
|
expect(round.score(ctx)).toEqual([{ playerId: b.id, delta: 100 }])
|
|
})
|
|
|
|
test("le contributeur ne marque pas sur son propre titre", () => {
|
|
const rooms = new RoomManager()
|
|
const { room, player: a } = rooms.create("Alice", "sa")
|
|
const t = track(a.id)
|
|
const round = new BlindtestRound()
|
|
const ctx = ctxFor(room, t, "who_added")
|
|
round.submitAnswer(ctx, a.id, { guessedPlayerId: a.id })
|
|
expect(round.score(ctx)).toEqual([])
|
|
})
|
|
|
|
test("reveal expose le titre, l'artiste et le contributeur", () => {
|
|
const rooms = new RoomManager()
|
|
const { room, player: a } = rooms.create("Alice", "sa")
|
|
const t = track(a.id)
|
|
const round = new BlindtestRound()
|
|
const ctx = ctxFor(room, t, "title_artist")
|
|
const { truth } = round.reveal(ctx)
|
|
expect(truth.submittedBy).toBe(a.id)
|
|
expect(truth.submittedByName).toBe("Alice")
|
|
expect(truth.title).toBe("The Legend of Zelda Main Theme")
|
|
})
|
|
})
|