release/0.1.0 #18

Merged
ayoub merged 68 commits from release/0.1.0 into main 2026-06-11 18:36:42 +00:00
4 changed files with 29 additions and 37 deletions
Showing only changes of commit a2e3fae858 - Show all commits

View file

@ -127,26 +127,13 @@ export class GameEngine implements RoomGameController {
} }
this.room.status = "in_round" this.room.status = "in_round"
this.broadcastState() this.broadcastState()
// L'identité du DJ (= contributeur du titre) est privée : seul le DJ la this.io.to(this.room.code).emit("round:start", {
// reçoit, sinon "qui l'a ajouté" deviendrait trivial.
const base = {
type: round.type, type: round.type,
djId: this.runtime.djId ?? undefined,
startsAt: startedAt, startsAt: startedAt,
endsAt, endsAt,
payload: start.payload, payload: start.payload,
} })
const djSocketId = this.runtime.djId
? this.room.players.get(this.runtime.djId)?.socketId
: null
if (djSocketId) {
this.io.to(djSocketId).emit("round:start", {
...base,
djId: this.runtime.djId ?? undefined,
})
this.io.to(this.room.code).except(djSocketId).emit("round:start", base)
} else {
this.io.to(this.room.code).emit("round:start", base)
}
// Attend la première condition de fin : timer écoulé OU tous votés. // Attend la première condition de fin : timer écoulé OU tous votés.
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
@ -190,11 +177,10 @@ export class GameEngine implements RoomGameController {
} }
} }
/** Votants éligibles : joueurs connectés, hors DJ (= contributeur, ne vote pas). */ /** Votants éligibles : tous les joueurs connectés (le DJ neutre vote aussi). */
private eligibleVoters(): string[] { private eligibleVoters(): string[] {
const djId = this.runtime?.djId
return [...this.room.players.values()] return [...this.room.players.values()]
.filter((p) => p.connected && p.id !== djId) .filter((p) => p.connected)
.map((p) => p.id) .map((p) => p.id)
} }

View file

@ -44,16 +44,16 @@ describe("match", () => {
}) })
describe("BlindtestRound", () => { describe("BlindtestRound", () => {
test("start : le DJ est le contributeur du titre", () => { test("start : le DJ est neutre (jamais le contributeur)", () => {
const rooms = new RoomManager() const rooms = new RoomManager()
const { room, player: a } = rooms.create("Alice", "sa") const { room, player: a } = rooms.create("Alice", "sa")
rooms.join(room.code, "Bob", "sb") const { player: b } = rooms.join(room.code, "Bob", "sb")
room.blindtestTracks = [track(a.id)] room.blindtestTracks = [track(a.id)]
prepareBlindtestForRoom(room) prepareBlindtestForRoom(room)
const round = new BlindtestRound() const round = new BlindtestRound()
const start = round.start(room) const start = round.start(room)
expect(start.djId).toBe(a.id) // Alice a ajouté le titre → elle est DJ expect(start.djId).toBe(b.id) // Bob (pas Alice, la contributrice)
expect(start.payload).toEqual({ trackId: "t1", youtubeId: "abc12345678" }) expect(start.payload).toEqual({ trackId: "t1", youtubeId: "abc12345678" })
}) })
@ -67,7 +67,7 @@ describe("BlindtestRound", () => {
const ctx = ctxFor(room, t, "who_added") const ctx = ctxFor(room, t, "who_added")
round.submitAnswer(ctx, b.id, { guessedPlayerId: c.id }) // faux round.submitAnswer(ctx, b.id, { guessedPlayerId: c.id }) // faux
round.submitAnswer(ctx, c.id, { guessedPlayerId: a.id }) // juste round.submitAnswer(ctx, c.id, { guessedPlayerId: a.id }) // juste
round.submitAnswer(ctx, a.id, { guessedPlayerId: b.id }) // ignoré (contributrice) round.submitAnswer(ctx, a.id, { guessedPlayerId: b.id }) // contributrice : non scorée
const deltas = round.score(ctx) const deltas = round.score(ctx)
// Carol devine juste → 100 ; Alice trompe Bob (1 mauvaise devinette) → 50 // Carol devine juste → 100 ; Alice trompe Bob (1 mauvaise devinette) → 50
expect(deltas).toContainEqual({ playerId: c.id, delta: 100 }) expect(deltas).toContainEqual({ playerId: c.id, delta: 100 })

View file

@ -26,6 +26,17 @@ interface BlindtestRoundData {
track: BlindtestTrack 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. */ /** Résultat d'un joueur VOTANT (≠ contributeur) selon le mode. */
function voterResult( function voterResult(
mode: BlindtestMode, mode: BlindtestMode,
@ -60,8 +71,8 @@ export class BlindtestRound implements GameRound {
start(room: ServerRoom): RoundStart { start(room: ServerRoom): RoundStart {
const track = takeTrack(room) const track = takeTrack(room)
// Le DJ EST le contributeur : il lance/pilote son propre titre. // DJ neutre (jamais le contributeur) : il pilote la lecture et vote à l'aveugle.
const djId = track ? track.submittedBy : null const djId = track ? pickDj(room, track.submittedBy) : null
const payload: BlindtestRoundPayload | null = track const payload: BlindtestRoundPayload | null = track
? { trackId: track.id, youtubeId: track.youtubeId } ? { trackId: track.id, youtubeId: track.youtubeId }
: null : null
@ -69,9 +80,9 @@ export class BlindtestRound implements GameRound {
} }
submitAnswer(ctx: RoundContext, playerId: string, answer: Answer): void { submitAnswer(ctx: RoundContext, playerId: string, answer: Answer): void {
const { track } = ctx.data as BlindtestRoundData // Premier vote verrouillé (idempotent). Le contributeur peut voter mais son
// Le contributeur (DJ) ne vote pas ; premier vote verrouillé pour les autres. // vote ne lui rapporte rien (voir buildResults) ; ça évite de bloquer la fin.
if (playerId === track.submittedBy || ctx.votes.has(playerId)) { if (ctx.votes.has(playerId)) {
return return
} }
ctx.votes.set(playerId, answer) ctx.votes.set(playerId, answer)

View file

@ -102,7 +102,7 @@ export function BlindtestView({ snapshot }: { snapshot: RoomSnapshot }) {
<DjControls ready={ready} api={api} mediaControl={mediaControl} /> <DjControls ready={ready} api={api} mediaControl={mediaControl} />
)} )}
{!isDj && !showReveal && ( {!showReveal && (
<VoteForm <VoteForm
mode={snapshot.settings.blindtestMode} mode={snapshot.settings.blindtestMode}
snapshot={snapshot} snapshot={snapshot}
@ -115,7 +115,6 @@ export function BlindtestView({ snapshot }: { snapshot: RoomSnapshot }) {
{showReveal && truth && ( {showReveal && truth && (
<RevealCard <RevealCard
truth={truth} truth={truth}
isDj={isDj}
result={ result={
(reveal?.perPlayerResult as BlindtestPerPlayerResult)[playerId ?? ""] (reveal?.perPlayerResult as BlindtestPerPlayerResult)[playerId ?? ""]
} }
@ -151,7 +150,7 @@ function DjControls({
return ( return (
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<span className="text-muted-foreground text-center text-xs uppercase"> <span className="text-muted-foreground text-center text-xs uppercase">
Tu es le DJ 🎧 c'est ton titre, fais-le deviner 🤫 Tu es le DJ 🎧 pilote la lecture (et vote comme les autres)
</span> </span>
<div className="flex justify-center gap-2"> <div className="flex justify-center gap-2">
<Button <Button
@ -307,11 +306,9 @@ function VoteForm({
function RevealCard({ function RevealCard({
truth, truth,
isDj,
result, result,
}: { }: {
truth: BlindtestRevealTruth truth: BlindtestRevealTruth
isDj: boolean
result?: BlindtestPerPlayerResult[string] result?: BlindtestPerPlayerResult[string]
}) { }) {
return ( return (
@ -323,13 +320,11 @@ function RevealCard({
<Avatar seed={truth.submittedByName} className="size-6" /> <Avatar seed={truth.submittedByName} className="size-6" />
<span className="font-medium">{truth.submittedByName}</span> <span className="font-medium">{truth.submittedByName}</span>
</p> </p>
{result && (result.points > 0 || !isDj) && ( {result && (
<p <p
className={`text-sm font-medium ${result.points > 0 ? "text-green-500" : "text-red-500"}`} className={`text-sm font-medium ${result.points > 0 ? "text-green-500" : "text-red-500"}`}
> >
{result.points > 0 {result.points > 0 ? `+${result.points} points 🎉` : "Raté 💥"}
? `+${result.points} points ${isDj ? "🤫" : "🎉"}`
: "Raté 💥"}
</p> </p>
)} )}
</div> </div>