Server: - quiz mode (GameRound): in-memory FR question bank (mcq + truefalse), per-room anti-repeat, first-vote lock, score = base + speed bonus ∝ time left; registered via registerRound, loaded at startup - shared: typed quiz payloads (QuizQuestionPayload / reveal truth / result) - tests: quiz scoring, vote lock, bounds, reveal (bun test) Client: - room store handles round:start / voteAck / reveal / game:end - RoomPage dispatches by status: lobby (host start controls) → quiz view (question, countdown, vote, reveal + scoreboard) → game-end view - replaces standalone lobby page Roadmap V1 step 4. Verified end-to-end over the wire (start→vote→reveal→ score→next round→game:end, no answer leaked, anti-repeat works). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
59 lines
2.2 KiB
TypeScript
59 lines
2.2 KiB
TypeScript
import { describe, expect, test } from "bun:test"
|
|
import { RoomManager } from "../../../rooms"
|
|
import type { RoundContext } from "../../round"
|
|
import { QuizRound } from "./quiz-round"
|
|
import { QUIZ_QUESTIONS } from "./questions"
|
|
|
|
const question = QUIZ_QUESTIONS[0] // "Link" → correctIndex 1
|
|
|
|
function makeCtx(): { ctx: RoundContext; p1: string; p2: string } {
|
|
const rooms = new RoomManager()
|
|
const { room, player: a } = rooms.create("Alice", "sa")
|
|
const { player: b } = rooms.join(room.code, "Bob", "sb")
|
|
const ctx: RoundContext = {
|
|
room,
|
|
djId: null,
|
|
votes: new Map(),
|
|
startedAt: 0,
|
|
endsAt: 10_000,
|
|
data: { question, votedAt: new Map() },
|
|
}
|
|
return { ctx, p1: a.id, p2: b.id }
|
|
}
|
|
|
|
describe("QuizRound", () => {
|
|
test("verrouille le premier vote (idempotent)", () => {
|
|
const round = new QuizRound(() => 1000)
|
|
const { ctx, p1 } = makeCtx()
|
|
round.submitAnswer(ctx, p1, { choiceIndex: 1 })
|
|
round.submitAnswer(ctx, p1, { choiceIndex: 0 }) // ignoré
|
|
expect(ctx.votes.get(p1)).toEqual({ choiceIndex: 1 })
|
|
})
|
|
|
|
test("ignore un index hors bornes", () => {
|
|
const round = new QuizRound(() => 1000)
|
|
const { ctx, p1 } = makeCtx()
|
|
round.submitAnswer(ctx, p1, { choiceIndex: 99 })
|
|
expect(ctx.votes.has(p1)).toBe(false)
|
|
})
|
|
|
|
test("reveal expose la vérité + le résultat par joueur", () => {
|
|
const round = new QuizRound(() => 1000)
|
|
const { ctx, p1, p2 } = makeCtx()
|
|
round.submitAnswer(ctx, p1, { choiceIndex: 1 }) // juste
|
|
round.submitAnswer(ctx, p2, { choiceIndex: 0 }) // faux
|
|
const { truth, perPlayerResult } = round.reveal(ctx)
|
|
expect(truth.correctIndex).toBe(question.correctIndex)
|
|
expect(perPlayerResult[p1]).toEqual({ choiceIndex: 1, correct: true })
|
|
expect(perPlayerResult[p2]).toEqual({ choiceIndex: 0, correct: false })
|
|
})
|
|
|
|
test("score = base + bonus rapidité pour les bonnes réponses uniquement", () => {
|
|
const round = new QuizRound(() => 1000) // vote à t=1000 sur 10000 → reste 90%
|
|
const { ctx, p1, p2 } = makeCtx()
|
|
round.submitAnswer(ctx, p1, { choiceIndex: 1 }) // juste, rapide
|
|
round.submitAnswer(ctx, p2, { choiceIndex: 0 }) // faux
|
|
const deltas = round.score(ctx)
|
|
expect(deltas).toEqual([{ playerId: p1, delta: 190 }]) // 100 + round(100 * 0.9)
|
|
})
|
|
})
|