release/0.1.0 #18
4 changed files with 29 additions and 37 deletions
|
|
@ -127,26 +127,13 @@ export class GameEngine implements RoomGameController {
|
|||
}
|
||||
this.room.status = "in_round"
|
||||
this.broadcastState()
|
||||
// L'identité du DJ (= contributeur du titre) est privée : seul le DJ la
|
||||
// reçoit, sinon "qui l'a ajouté" deviendrait trivial.
|
||||
const base = {
|
||||
this.io.to(this.room.code).emit("round:start", {
|
||||
type: round.type,
|
||||
djId: this.runtime.djId ?? undefined,
|
||||
startsAt: startedAt,
|
||||
endsAt,
|
||||
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.
|
||||
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[] {
|
||||
const djId = this.runtime?.djId
|
||||
return [...this.room.players.values()]
|
||||
.filter((p) => p.connected && p.id !== djId)
|
||||
.filter((p) => p.connected)
|
||||
.map((p) => p.id)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,16 +44,16 @@ describe("match", () => {
|
|||
})
|
||||
|
||||
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 { 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)]
|
||||
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.djId).toBe(b.id) // Bob (pas Alice, la contributrice)
|
||||
expect(start.payload).toEqual({ trackId: "t1", youtubeId: "abc12345678" })
|
||||
})
|
||||
|
||||
|
|
@ -67,7 +67,7 @@ describe("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)
|
||||
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 })
|
||||
|
|
|
|||
|
|
@ -26,6 +26,17 @@ interface BlindtestRoundData {
|
|||
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. */
|
||||
function voterResult(
|
||||
mode: BlindtestMode,
|
||||
|
|
@ -60,8 +71,8 @@ export class BlindtestRound implements GameRound {
|
|||
|
||||
start(room: ServerRoom): RoundStart {
|
||||
const track = takeTrack(room)
|
||||
// Le DJ EST le contributeur : il lance/pilote son propre titre.
|
||||
const djId = track ? track.submittedBy : null
|
||||
// DJ neutre (jamais le contributeur) : il pilote la lecture et vote à l'aveugle.
|
||||
const djId = track ? pickDj(room, track.submittedBy) : null
|
||||
const payload: BlindtestRoundPayload | null = track
|
||||
? { trackId: track.id, youtubeId: track.youtubeId }
|
||||
: null
|
||||
|
|
@ -69,9 +80,9 @@ export class BlindtestRound implements GameRound {
|
|||
}
|
||||
|
||||
submitAnswer(ctx: RoundContext, playerId: string, answer: Answer): void {
|
||||
const { track } = ctx.data as BlindtestRoundData
|
||||
// Le contributeur (DJ) ne vote pas ; premier vote verrouillé pour les autres.
|
||||
if (playerId === track.submittedBy || ctx.votes.has(playerId)) {
|
||||
// Premier vote verrouillé (idempotent). Le contributeur peut voter mais son
|
||||
// vote ne lui rapporte rien (voir buildResults) ; ça évite de bloquer la fin.
|
||||
if (ctx.votes.has(playerId)) {
|
||||
return
|
||||
}
|
||||
ctx.votes.set(playerId, answer)
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ export function BlindtestView({ snapshot }: { snapshot: RoomSnapshot }) {
|
|||
<DjControls ready={ready} api={api} mediaControl={mediaControl} />
|
||||
)}
|
||||
|
||||
{!isDj && !showReveal && (
|
||||
{!showReveal && (
|
||||
<VoteForm
|
||||
mode={snapshot.settings.blindtestMode}
|
||||
snapshot={snapshot}
|
||||
|
|
@ -115,7 +115,6 @@ export function BlindtestView({ snapshot }: { snapshot: RoomSnapshot }) {
|
|||
{showReveal && truth && (
|
||||
<RevealCard
|
||||
truth={truth}
|
||||
isDj={isDj}
|
||||
result={
|
||||
(reveal?.perPlayerResult as BlindtestPerPlayerResult)[playerId ?? ""]
|
||||
}
|
||||
|
|
@ -151,7 +150,7 @@ function DjControls({
|
|||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<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>
|
||||
<div className="flex justify-center gap-2">
|
||||
<Button
|
||||
|
|
@ -307,11 +306,9 @@ function VoteForm({
|
|||
|
||||
function RevealCard({
|
||||
truth,
|
||||
isDj,
|
||||
result,
|
||||
}: {
|
||||
truth: BlindtestRevealTruth
|
||||
isDj: boolean
|
||||
result?: BlindtestPerPlayerResult[string]
|
||||
}) {
|
||||
return (
|
||||
|
|
@ -323,13 +320,11 @@ function RevealCard({
|
|||
<Avatar seed={truth.submittedByName} className="size-6" />
|
||||
<span className="font-medium">{truth.submittedByName}</span>
|
||||
</p>
|
||||
{result && (result.points > 0 || !isDj) && (
|
||||
{result && (
|
||||
<p
|
||||
className={`text-sm font-medium ${result.points > 0 ? "text-green-500" : "text-red-500"}`}
|
||||
>
|
||||
{result.points > 0
|
||||
? `+${result.points} points ${isDj ? "🤫" : "🎉"}`
|
||||
: "Raté 💥"}
|
||||
{result.points > 0 ? `+${result.points} points 🎉` : "Raté 💥"}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue