From 8eac8d012315debf797e5581fda4e949e2232490 Mon Sep 17 00:00:00 2001
From: AyoubBenziza
Date: Thu, 11 Jun 2026 17:48:33 +0200
Subject: [PATCH 1/3] =?UTF-8?q?feat:=20bots=20=E2=80=94=20virtual=20player?=
=?UTF-8?q?s=20to=20fill=20a=20game?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- server-side bot players (isBot): host adds/removes them in the lobby
(lobby:addBot / lobby:removeBot), capped at 8 players
- bots auto-vote each round: GameRound.botAnswer generates a plausible answer
(quiz ~50% right, blindtest guesses a random player / title); engine schedules
the votes within the answer window and triggers early end when all have voted
- bots are never DJ (pickDj excludes them) and are excluded from the blindtest
track-submission gate (they don't submit)
- snapshot players carry `bot`; lobby shows a 🤖 marker + add/remove controls
Verified e2e: 2 bots added, they vote, game ends with all 3 players scored
and rounds end early.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
apps/server/src/game/engine.test.ts | 1 +
apps/server/src/game/engine.ts | 40 ++++++++++++++++
.../game/modes/blindtest/blindtest-round.ts | 25 +++++++++-
apps/server/src/game/modes/quiz/quiz-round.ts | 16 +++++++
apps/server/src/game/round.ts | 3 ++
apps/server/src/handlers.ts | 22 +++++++++
apps/server/src/rooms.ts | 47 +++++++++++++++++++
apps/web/src/components/lobby-view.tsx | 40 ++++++++++++++--
apps/web/src/store/room.ts | 5 ++
packages/shared/src/domain.ts | 2 +
packages/shared/src/events.ts | 2 +
11 files changed, 196 insertions(+), 7 deletions(-)
diff --git a/apps/server/src/game/engine.test.ts b/apps/server/src/game/engine.test.ts
index 991b29d..dfebac3 100644
--- a/apps/server/src/game/engine.test.ts
+++ b/apps/server/src/game/engine.test.ts
@@ -47,6 +47,7 @@ const dummyRound: GameRound = {
return deltas
},
recap: () => ({ label: "2+2 ?", answer: "4", answers: [] }),
+ botAnswer: () => ({ choiceIndex: 0 }),
}
function setup(roundDurationSec: number): {
diff --git a/apps/server/src/game/engine.ts b/apps/server/src/game/engine.ts
index 66137f8..3e6aaf2 100644
--- a/apps/server/src/game/engine.ts
+++ b/apps/server/src/game/engine.ts
@@ -195,6 +195,8 @@ export class GameEngine implements RoomGameController {
this.io.to(this.room.code).emit("round:start", base)
}
+ this.scheduleBotVotes()
+
// Attend la première condition de fin : timer écoulé OU tous votés.
await new Promise((resolve) => {
this.resolveRoundEnd = resolve
@@ -222,6 +224,44 @@ export class GameEngine implements RoomGameController {
this.runtime = null
}
+ /** Programme le vote automatique des bots (jamais DJ ni contributeur). */
+ private scheduleBotVotes(): void {
+ if (!this.runtime) {
+ return
+ }
+ const { djId, secretPlayerId, endsAt, startedAt } = this.runtime
+ const window = Math.max(0, endsAt - startedAt)
+ const bots = [...this.room.players.values()].filter(
+ (p) => p.isBot && p.id !== djId && p.id !== secretPlayerId
+ )
+ for (const bot of bots) {
+ // Vote après l'ouverture des réponses, réparti dans la première moitié.
+ const delay = this.leadMs + 700 + Math.random() * Math.min(window * 0.5, 3500)
+ setTimeout(() => this.botVote(bot.id), delay)
+ }
+ }
+
+ /** Exécute le vote d'un bot (no-op si la manche est finie ou s'il a déjà voté). */
+ private botVote(botId: string): void {
+ if (!this.runtime || this.room.status !== "in_round") {
+ return
+ }
+ if (this.runtime.votes.has(botId)) {
+ return
+ }
+ const ctx = this.context(this.runtime)
+ this.runtime.round.submitAnswer(ctx, botId, this.runtime.round.botAnswer(ctx, botId))
+ const total = this.eligibleVoters().length
+ this.io.to(this.room.code).emit("round:voteAck", {
+ count: this.runtime.votes.size,
+ total,
+ voted: [...this.runtime.votes.keys()],
+ })
+ if (total > 0 && this.runtime.votes.size >= total) {
+ this.endRound()
+ }
+ }
+
/** Termine la manche en cours (idempotent : timer et "tous votés" peuvent se croiser). */
private endRound(): void {
if (this.timer) {
diff --git a/apps/server/src/game/modes/blindtest/blindtest-round.ts b/apps/server/src/game/modes/blindtest/blindtest-round.ts
index c78ae68..db3541e 100644
--- a/apps/server/src/game/modes/blindtest/blindtest-round.ts
+++ b/apps/server/src/game/modes/blindtest/blindtest-round.ts
@@ -31,10 +31,10 @@ interface BlindtestRoundData {
track: BlindtestTrack
}
-/** Tire un DJ neutre : un joueur connecté qui n'a pas soumis ce titre. */
+/** Tire un DJ neutre : un humain 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
+ (p) => p.connected && !p.isBot && p.id !== submittedBy
)
if (eligible.length === 0) {
return null
@@ -98,6 +98,27 @@ export class BlindtestRound implements GameRound {
ctx.votes.set(playerId, answer)
}
+ botAnswer(ctx: RoundContext, playerId: string): Answer {
+ const { track } = ctx.data as BlindtestRoundData
+ const mode = ctx.room.settings.blindtestMode
+ // Devine un joueur au hasard (parfois le bon contributeur).
+ const others = [...ctx.room.players.values()]
+ .filter((p) => p.id !== playerId)
+ .map((p) => p.id)
+ const guessedPlayerId =
+ others[Math.floor(Math.random() * others.length)] ?? playerId
+ if (mode === "who_added") {
+ return { guessedPlayerId }
+ }
+ const right = Math.random() < 0.4
+ const title = right ? track.title : "?"
+ const artist = right ? track.artist : "?"
+ if (mode === "title_artist") {
+ return { title, artist }
+ }
+ return { title, artist, guessedPlayerId }
+ }
+
/** Résultat par joueur (votants + bonus de tromperie du contributeur). */
private buildResults(ctx: RoundContext): BlindtestPerPlayerResult {
const { track } = ctx.data as BlindtestRoundData
diff --git a/apps/server/src/game/modes/quiz/quiz-round.ts b/apps/server/src/game/modes/quiz/quiz-round.ts
index 54250a1..73d3825 100644
--- a/apps/server/src/game/modes/quiz/quiz-round.ts
+++ b/apps/server/src/game/modes/quiz/quiz-round.ts
@@ -147,6 +147,22 @@ export class QuizRound implements GameRound {
return deltas
}
+ botAnswer(ctx: RoundContext): Answer {
+ const { question } = ctx.data as QuizRoundData
+ // ~50% du temps le bot répond juste, sinon une réponse plausible mais fausse.
+ const rightish = Math.random() < 0.5
+ if (isTextFormat(question)) {
+ return {
+ text: rightish ? (question.acceptedAnswers?.[0] ?? "?") : "euh…",
+ }
+ }
+ const count = question.choices?.length ?? 1
+ if (rightish && typeof question.correctIndex === "number") {
+ return { choiceIndex: question.correctIndex }
+ }
+ return { choiceIndex: Math.floor(Math.random() * count) }
+ }
+
recap(ctx: RoundContext): RoundRecapInfo {
const { question } = ctx.data as QuizRoundData
const answer = isTextFormat(question)
diff --git a/apps/server/src/game/round.ts b/apps/server/src/game/round.ts
index e9a59a6..ce5e04f 100644
--- a/apps/server/src/game/round.ts
+++ b/apps/server/src/game/round.ts
@@ -63,6 +63,9 @@ export interface GameRound {
/** Renvoie les variations de score Ă appliquer (le moteur les applique). */
score(ctx: RoundContext): ScoreDelta[]
+ /** Réponse automatique d'un bot (vote plausible, parfois juste). */
+ botAnswer(ctx: RoundContext, playerId: string): Answer
+
/** Résumé de la manche pour le récap de fin de partie (intitulé + réponse + média). */
recap(ctx: RoundContext): RoundRecapInfo
}
diff --git a/apps/server/src/handlers.ts b/apps/server/src/handlers.ts
index 8ab1faf..0292d8d 100644
--- a/apps/server/src/handlers.ts
+++ b/apps/server/src/handlers.ts
@@ -149,6 +149,27 @@ export function registerRoomHandlers(
broadcastState(room)
})
+ socket.on("lobby:addBot", () => {
+ const code = socket.data.roomCode
+ const room = code ? rooms.get(code) : undefined
+ if (!room || room.hostId !== socket.data.playerId || room.status !== "lobby") {
+ return
+ }
+ if (rooms.addBot(room)) {
+ broadcastState(room)
+ }
+ })
+
+ socket.on("lobby:removeBot", () => {
+ const code = socket.data.roomCode
+ const room = code ? rooms.get(code) : undefined
+ if (!room || room.hostId !== socket.data.playerId) {
+ return
+ }
+ rooms.removeBot(room)
+ broadcastState(room)
+ })
+
socket.on("game:start", (ack) => {
const code = socket.data.roomCode
const room = code ? rooms.get(code) : undefined
@@ -192,6 +213,7 @@ export function registerRoomHandlers(
(p) =>
p.connected &&
p.name !== "" &&
+ !p.isBot &&
room.blindtestTracks.filter((t) => t.submittedBy === p.id).length < tpp
)
if (pending) {
diff --git a/apps/server/src/rooms.ts b/apps/server/src/rooms.ts
index d32dd67..2c8984a 100644
--- a/apps/server/src/rooms.ts
+++ b/apps/server/src/rooms.ts
@@ -18,8 +18,22 @@ export interface ServerPlayer {
socketId: string | null
/** Secret de reconnexion (refresh/coupure). */
reconnectToken: string
+ /** Joueur virtuel (vote automatiquement, jamais DJ). */
+ isBot: boolean
}
+const BOT_NAMES = [
+ "Bot BMO",
+ "Bot R2-D2",
+ "Bot GLaDOS",
+ "Bot Clank",
+ "Bot Wall-E",
+ "Bot K-2SO",
+ "Bot HAL",
+ "Bot Bender",
+]
+const MAX_PLAYERS = 8
+
/** Titre soumis pour le blindtest. `submittedBy` reste secret jusqu'au reveal. */
export interface BlindtestTrack {
id: string
@@ -94,6 +108,37 @@ export class RoomManager {
return this.rooms.get(code)
}
+ /** Ajoute un bot (joueur virtuel) Ă la room. Renvoie false si plein. */
+ addBot(room: ServerRoom): boolean {
+ if (room.players.size >= MAX_PLAYERS) {
+ return false
+ }
+ const used = new Set([...room.players.values()].map((p) => p.name))
+ const name =
+ BOT_NAMES.find((n) => !used.has(n)) ?? `Bot ${room.players.size + 1}`
+ const bot: ServerPlayer = {
+ id: randomUUID(),
+ name,
+ connected: true,
+ socketId: null,
+ reconnectToken: "",
+ isBot: true,
+ }
+ room.players.set(bot.id, bot)
+ room.scores.set(bot.id, 0)
+ return true
+ }
+
+ /** Retire un bot (le dernier ajouté). */
+ removeBot(room: ServerRoom): void {
+ const bots = [...room.players.values()].filter((p) => p.isBot)
+ const last = bots[bots.length - 1]
+ if (last) {
+ room.players.delete(last.id)
+ room.scores.delete(last.id)
+ }
+ }
+
/**
* Retire un joueur d'une room (départ volontaire). Réassigne l'hôte si besoin,
* supprime la room si plus personne. Renvoie la room (ou null si supprimée).
@@ -146,6 +191,7 @@ export class RoomManager {
id: p.id,
name: p.name,
connected: p.connected,
+ bot: p.isBot,
})),
settings: room.settings,
scores: named.map((p) => ({
@@ -168,6 +214,7 @@ export class RoomManager {
connected: true,
socketId,
reconnectToken: randomUUID(),
+ isBot: false,
}
}
diff --git a/apps/web/src/components/lobby-view.tsx b/apps/web/src/components/lobby-view.tsx
index ac9fd09..c236db7 100644
--- a/apps/web/src/components/lobby-view.tsx
+++ b/apps/web/src/components/lobby-view.tsx
@@ -136,7 +136,10 @@ export function LobbyView({ snapshot }: { snapshot: RoomSnapshot }) {
const playerId = useRoomStore((s) => s.playerId)
const updateSettings = useRoomStore((s) => s.updateSettings)
const startGame = useRoomStore((s) => s.startGame)
+ const addBot = useRoomStore((s) => s.addBot)
+ const removeBot = useRoomStore((s) => s.removeBot)
const isHost = snapshot.hostId === playerId
+ const botCount = snapshot.players.filter((p) => p.bot).length
const { gameType, mixedModes, blindtestMode, tracksPerPlayer, categories } =
snapshot.settings
const allCategories =
@@ -180,11 +183,13 @@ export function LobbyView({ snapshot }: { snapshot: RoomSnapshot }) {
tracks: showBlindtest ? totalTracks : 0,
shuffleOrder: isMixed,
})
- // Blindtest : tout le monde doit avoir soumis son quota de titres.
+ // Blindtest : tous les HUMAINS doivent avoir soumis leur quota (les bots non).
+ const botIds = new Set(snapshot.players.filter((p) => p.bot).map((p) => p.id))
+ const humanSubs = snapshot.submissions.filter((s) => !botIds.has(s.playerId))
const allSubmitted =
!showBlindtest ||
- (snapshot.submissions.length > 0 &&
- snapshot.submissions.every((s) => s.count >= tracksPerPlayer))
+ (humanSubs.length > 0 &&
+ humanSubs.every((s) => s.count >= tracksPerPlayer))
const canStart = rounds.length > 0 && allSubmitted
function toggleMix(mode: MixMode) {
@@ -241,12 +246,37 @@ export function LobbyView({ snapshot }: { snapshot: RoomSnapshot }) {
)}
- {p.id === snapshot.hostId ? "HĂ´te" : ""}
- {!p.connected && " · hors ligne"}
+ {p.bot ? "🤖 Bot" : p.id === snapshot.hostId ? "Hôte" : ""}
+ {!p.bot && !p.connected && " · hors ligne"}
))}
+ {isHost && (
+
+
+ Bots : {botCount}
+
+
+
+
+
+
+ )}
diff --git a/apps/web/src/store/room.ts b/apps/web/src/store/room.ts
index 5b72134..b4b1b8a 100644
--- a/apps/web/src/store/room.ts
+++ b/apps/web/src/store/room.ts
@@ -78,6 +78,8 @@ interface RoomState {
removeTrack: (trackId: string) => Promise
returnToLobby: () => void
leaveRoom: () => void
+ addBot: () => void
+ removeBot: () => void
vote: (choiceIndex: number) => void
voteText: (text: string) => void
voteBlindtest: (answer: BlindtestAnswer) => void
@@ -198,6 +200,9 @@ export const useRoomStore = create((set, get) => ({
get().reset()
},
+ addBot: () => socket.emit("lobby:addBot"),
+ removeBot: () => socket.emit("lobby:removeBot"),
+
vote: (choiceIndex) => {
if (get().hasVoted) {
return
diff --git a/packages/shared/src/domain.ts b/packages/shared/src/domain.ts
index 8c98ea2..98508d9 100644
--- a/packages/shared/src/domain.ts
+++ b/packages/shared/src/domain.ts
@@ -24,6 +24,8 @@ export interface Player {
id: string
name: string
connected: boolean
+ /** Joueur virtuel (bot) géré par le serveur. */
+ bot?: boolean
}
/** Configuration d'une épreuve planifiée dans la séquence de la partie. */
diff --git a/packages/shared/src/events.ts b/packages/shared/src/events.ts
index f0dfe38..8f71883 100644
--- a/packages/shared/src/events.ts
+++ b/packages/shared/src/events.ts
@@ -150,6 +150,8 @@ export interface ClientToServerEvents {
"room:rejoin": (payload: RejoinPayload, ack: Ack) => void
"player:setName": (payload: SetNamePayload) => void
"room:leave": () => void
+ "lobby:addBot": () => void
+ "lobby:removeBot": () => void
"lobby:updateSettings": (payload: UpdateSettingsPayload) => void
"lobby:return": () => void
"game:start": (ack: Ack) => void
--
2.45.3
From 42a36fc442fd5e62febe30a9a7b5f289566ebcb1 Mon Sep 17 00:00:00 2001
From: AyoubBenziza
Date: Thu, 11 Jun 2026 18:03:03 +0200
Subject: [PATCH 2/3] feat: blindtest needs 2 real players + bot difficulty
setting
- blindtest is only available with >=3 players AND >=2 humans (a bot can't be DJ):
lobby gate + tooltips updated, and server game:start rejects otherwise
- new botDifficulty setting (easy/normal/hard) drives how often bots answer
correctly (quiz + blindtest who-added / title-artist); host picks it in the
lobby when at least one bot is present
Verified e2e: blindtest start rejected with 1 human + 2 bots (NEED_THREE).
Co-Authored-By: Claude Opus 4.8 (1M context)
---
apps/server/src/game/bot.ts | 13 +++++
.../game/modes/blindtest/blindtest-round.ts | 10 ++--
apps/server/src/game/modes/quiz/quiz-round.ts | 5 +-
apps/server/src/handlers.ts | 11 ++--
apps/web/src/components/lobby-view.tsx | 51 ++++++++++++++++---
apps/web/src/store/room.ts | 1 +
packages/shared/src/domain.ts | 6 +++
packages/shared/src/events.ts | 2 +
8 files changed, 83 insertions(+), 16 deletions(-)
create mode 100644 apps/server/src/game/bot.ts
diff --git a/apps/server/src/game/bot.ts b/apps/server/src/game/bot.ts
new file mode 100644
index 0000000..fc15429
--- /dev/null
+++ b/apps/server/src/game/bot.ts
@@ -0,0 +1,13 @@
+// Réglage de difficulté des bots : probabilité de donner la bonne réponse.
+
+import type { BotDifficulty } from "@nerdware/shared"
+
+const CHANCE: Record = {
+ easy: 0.3,
+ normal: 0.55,
+ hard: 0.85,
+}
+
+export function botCorrectChance(difficulty: BotDifficulty): number {
+ return CHANCE[difficulty] ?? CHANCE.normal
+}
diff --git a/apps/server/src/game/modes/blindtest/blindtest-round.ts b/apps/server/src/game/modes/blindtest/blindtest-round.ts
index db3541e..53f86c9 100644
--- a/apps/server/src/game/modes/blindtest/blindtest-round.ts
+++ b/apps/server/src/game/modes/blindtest/blindtest-round.ts
@@ -19,6 +19,7 @@ import type {
} from "../../round"
import type { BlindtestTrack, ServerRoom } from "../../../rooms"
import { fuzzyMatch } from "../../match"
+import { botCorrectChance } from "../../bot"
import { takeTrack } from "./pool"
const TITLE_POINTS = 60
@@ -101,16 +102,19 @@ export class BlindtestRound implements GameRound {
botAnswer(ctx: RoundContext, playerId: string): Answer {
const { track } = ctx.data as BlindtestRoundData
const mode = ctx.room.settings.blindtestMode
- // Devine un joueur au hasard (parfois le bon contributeur).
+ const chance = botCorrectChance(ctx.room.settings.botDifficulty)
+ // Selon le niveau : devine le bon contributeur, sinon un joueur au hasard.
const others = [...ctx.room.players.values()]
.filter((p) => p.id !== playerId)
.map((p) => p.id)
const guessedPlayerId =
- others[Math.floor(Math.random() * others.length)] ?? playerId
+ Math.random() < chance
+ ? track.submittedBy
+ : (others[Math.floor(Math.random() * others.length)] ?? playerId)
if (mode === "who_added") {
return { guessedPlayerId }
}
- const right = Math.random() < 0.4
+ const right = Math.random() < chance
const title = right ? track.title : "?"
const artist = right ? track.artist : "?"
if (mode === "title_artist") {
diff --git a/apps/server/src/game/modes/quiz/quiz-round.ts b/apps/server/src/game/modes/quiz/quiz-round.ts
index 73d3825..4b07bd2 100644
--- a/apps/server/src/game/modes/quiz/quiz-round.ts
+++ b/apps/server/src/game/modes/quiz/quiz-round.ts
@@ -9,6 +9,7 @@ import type {
ScoreDelta,
} from "@nerdware/shared"
import { hasDb } from "../../../db"
+import { botCorrectChance } from "../../bot"
import { markQuestionPlayed } from "../../../db/quiz-repo"
import { fuzzyMatch } from "../../match"
import type {
@@ -149,8 +150,8 @@ export class QuizRound implements GameRound {
botAnswer(ctx: RoundContext): Answer {
const { question } = ctx.data as QuizRoundData
- // ~50% du temps le bot répond juste, sinon une réponse plausible mais fausse.
- const rightish = Math.random() < 0.5
+ // Le bot répond juste selon le niveau choisi, sinon une réponse plausible.
+ const rightish = Math.random() < botCorrectChance(ctx.room.settings.botDifficulty)
if (isTextFormat(question)) {
return {
text: rightish ? (question.acceptedAnswers?.[0] ?? "?") : "euh…",
diff --git a/apps/server/src/handlers.ts b/apps/server/src/handlers.ts
index 0292d8d..da2fe57 100644
--- a/apps/server/src/handlers.ts
+++ b/apps/server/src/handlers.ts
@@ -144,6 +144,7 @@ export function registerRoomHandlers(
roundDuration: payload.roundDuration,
tracksPerPlayer: payload.tracksPerPlayer,
categories: payload.categories,
+ botDifficulty: payload.botDifficulty,
rounds: payload.rounds,
}
broadcastState(room)
@@ -194,15 +195,17 @@ export function registerRoomHandlers(
return
}
const hasBlindtest = room.settings.rounds.some((r) => r.type === "blindtest")
- const connected = [...room.players.values()].filter(
+ const active = [...room.players.values()].filter(
(p) => p.connected && p.name !== ""
- ).length
- if (hasBlindtest && connected < 3) {
+ )
+ const connected = active.length
+ const humans = active.filter((p) => !p.isBot).length
+ if (hasBlindtest && (connected < 3 || humans < 2)) {
ack({
ok: false,
error: {
code: "NEED_THREE",
- message: "Le blindtest nécessite au moins 3 joueurs.",
+ message: "Le blindtest nécessite au moins 3 joueurs dont 2 réels.",
},
})
return
diff --git a/apps/web/src/components/lobby-view.tsx b/apps/web/src/components/lobby-view.tsx
index c236db7..9b42c82 100644
--- a/apps/web/src/components/lobby-view.tsx
+++ b/apps/web/src/components/lobby-view.tsx
@@ -51,6 +51,12 @@ const MODE_LABELS: Record = {
who_added: "Qui l'a ajouté ?",
mixed: "Mixte",
}
+const BOT_DIFFICULTIES = ["easy", "normal", "hard"] as const
+const BOT_DIFF_LABELS: Record<(typeof BOT_DIFFICULTIES)[number], string> = {
+ easy: "Facile",
+ normal: "Normal",
+ hard: "Difficile",
+}
const inputClass =
"border-input bg-background h-10 w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none disabled:opacity-50"
@@ -140,8 +146,14 @@ export function LobbyView({ snapshot }: { snapshot: RoomSnapshot }) {
const removeBot = useRoomStore((s) => s.removeBot)
const isHost = snapshot.hostId === playerId
const botCount = snapshot.players.filter((p) => p.bot).length
- const { gameType, mixedModes, blindtestMode, tracksPerPlayer, categories } =
- snapshot.settings
+ const {
+ gameType,
+ mixedModes,
+ blindtestMode,
+ tracksPerPlayer,
+ categories,
+ botDifficulty,
+ } = snapshot.settings
const allCategories =
useQuery({ queryKey: ["categories"], queryFn: fetchCategories }).data ?? []
const history =
@@ -159,9 +171,13 @@ export function LobbyView({ snapshot }: { snapshot: RoomSnapshot }) {
const myCount =
snapshot.submissions.find((s) => s.playerId === playerId)?.count ?? 0
- // Le blindtest exige au moins 3 joueurs connectés (DJ + 2 devineurs).
+ // Le blindtest exige 3 joueurs (DJ + 2 devineurs) DONT au moins 2 réels :
+ // un bot ne peut pas ĂŞtre DJ, donc il faut un humain DJ + un humain contributeur.
const connectedCount = snapshot.players.filter((p) => p.connected).length
- const blindtestAvailable = connectedCount >= 3
+ const humanCount = snapshot.players.filter(
+ (p) => p.connected && !p.bot
+ ).length
+ const blindtestAvailable = connectedCount >= 3 && humanCount >= 2
// Seul le blindtest "seul" retombe sur du quiz Ă <3 ; le mixte reste mixte
// (son sous-mode blindtest est juste désactivé tant qu'on n'est pas 3).
const effectiveType: GameType =
@@ -297,7 +313,7 @@ export function LobbyView({ snapshot }: { snapshot: RoomSnapshot }) {
{!blindtestAvailable && (
- Le blindtest se débloque à 3 joueurs.
+ Le blindtest se débloque à 3 joueurs dont 2 réels (un bot ne peut
+ pas ĂŞtre DJ).
)}
@@ -330,7 +347,7 @@ export function LobbyView({ snapshot }: { snapshot: RoomSnapshot }) {
)}
-
- {q.active ? : }
-
+
+
+
+ {q.active ? : }
+
+
+ {q.active ? "Désactiver" : "Activer"}
+
-
+
+
+
+
+
+ {connected ? "Connecté" : "Déconnecté"}
+
+
) {
+ return (
+
+ )
+}
+
+function Tooltip({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+function TooltipTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function TooltipContent({
+ className,
+ sideOffset = 0,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+ {children}
+
+
+
+ )
+}
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
--
2.45.3