Playtest feedback round 2: - blindtest DJ can now be anyone incl. the track's contributor (pickDj only excludes bots) — no more "immunity" for whoever isn't picked - blindtest rounds last 90s (longer listen + typing) - per-player volume slider (persisted), YT player setVolume + origin/enablejsapi - theme switcher (light/dark/system) on home + room header - clock sync: clients estimate the server-clock offset (time:sync) so the countdown is identical across devices (was off by each device's clock skew); also used for blindtest media sync Note on duplicates: anti-repeat is per room code (quiz_played) — it works when you replay in the SAME room, but a new room starts fresh. Verified e2e: two chained games in one room share 0 questions. Verified: typecheck 4/4, lint 4/4, 24 server tests, web build; e2e dedup (0 overlap) and clock/DJ behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
142 lines
5.3 KiB
TypeScript
142 lines
5.3 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 un humain connecté (le contributeur peut tomber DJ)", () => {
|
|
const rooms = new RoomManager()
|
|
const { room, player: a } = rooms.create("Alice", "sa")
|
|
const { player: b } = rooms.join(room.code, "Bob", "sb")
|
|
room.blindtestTracks = [track(a.id)]
|
|
prepareBlindtestForRoom(room)
|
|
|
|
const round = new BlindtestRound()
|
|
const start = round.start(room)
|
|
// N'importe quel humain (Alice incluse), jamais un bot ni null.
|
|
expect([a.id, b.id]).toContain(start.djId!)
|
|
expect(start.payload).toEqual({ trackId: "t1", youtubeId: "abc12345678" })
|
|
})
|
|
|
|
test("start : un bot n'est jamais DJ", () => {
|
|
const rooms = new RoomManager()
|
|
const { room, player: a } = rooms.create("Alice", "sa")
|
|
rooms.addBot(room)
|
|
rooms.addBot(room)
|
|
room.blindtestTracks = [track(a.id)]
|
|
prepareBlindtestForRoom(room)
|
|
|
|
const round = new BlindtestRound()
|
|
// Alice est la seule humaine → DJ forcément Alice (les bots sont exclus).
|
|
expect(round.start(room).djId).toBe(a.id)
|
|
})
|
|
|
|
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 }) // contributrice : non scorée
|
|
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")
|
|
})
|
|
})
|