fix(blindtest): keep neutral DJ; only the contributor is misdirection-scored
Revert the DJ change: the DJ is again a neutral player (never the track's contributor), broadcast to all and counted as an eligible voter (votes blind). The contributor is a normal voter whose vote never scores on their own track; they only earn the who_added/mixed misdirection bonus (+50 per wrong guess). - engine: restore single round:start broadcast + DJ-included eligible voters - blindtest-round: restore neutral pickDj; contributor may vote (ignored in scoring) so the round can still end on "all voted" - client: DJ controls + everyone (DJ included) votes; reveal message simplified Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a9eb6098f2
commit
a2e3fae858
4 changed files with 29 additions and 37 deletions
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 })
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue