diff --git a/public/games-assets/moustache-banner.webp b/public/games-assets/moustache-banner.webp
new file mode 100644
index 0000000..73249ef
Binary files /dev/null and b/public/games-assets/moustache-banner.webp differ
diff --git a/public/games-assets/moustache-logo.webp b/public/games-assets/moustache-logo.webp
new file mode 100644
index 0000000..e9ef669
Binary files /dev/null and b/public/games-assets/moustache-logo.webp differ
diff --git a/public/games-assets/six-qui-prend-banner.webp b/public/games-assets/six-qui-prend-banner.webp
new file mode 100644
index 0000000..31e05a7
Binary files /dev/null and b/public/games-assets/six-qui-prend-banner.webp differ
diff --git a/public/games-assets/six-qui-prend-logo.webp b/public/games-assets/six-qui-prend-logo.webp
new file mode 100644
index 0000000..b6f3317
Binary files /dev/null and b/public/games-assets/six-qui-prend-logo.webp differ
diff --git a/src/games/index.ts b/src/games/index.ts
index 2b06f7a..532b8b5 100644
--- a/src/games/index.ts
+++ b/src/games/index.ts
@@ -8,7 +8,9 @@ import { azulConfig } from "./azul/config";
import { caboConfig } from "./cabo/config";
import { chouineursConfig } from "./chouineurs/config";
import { harmonieConfig } from "./harmonie/config";
+import { moustacheConfig } from "./moustache/config";
import { odinCardsConfig } from "./odin_cards/config";
+import { sixQuiPrendConfig } from "./six_qui_prend/config";
import { skyjoConfig } from "./skyjo/config";
// Built-in games (compiled into the app).
@@ -19,6 +21,8 @@ export const games: GameConfig[] = [
harmonieConfig,
odinCardsConfig,
chouineursConfig,
+ sixQuiPrendConfig,
+ moustacheConfig,
];
// Imported games for the active profile, kept in sync in-memory so the
diff --git a/src/games/moustache/config.ts b/src/games/moustache/config.ts
new file mode 100644
index 0000000..48761a3
--- /dev/null
+++ b/src/games/moustache/config.ts
@@ -0,0 +1,28 @@
+import { GameConfig } from "../../types";
+
+// Les équipes sont retirées au sort à chaque manche : on ne note donc que
+// l'équipe gagnante des 4 manches, puis chacun révèle ses points au décompte
+// final (5e et dernière manche). Le plus de points l'emporte.
+export const moustacheConfig: GameConfig = {
+ imagePath: "/games-assets/moustache-logo.webp",
+ bannerPath: "/games-assets/moustache-banner.webp",
+ id: "moustache",
+ name: "Moustache",
+ minPlayers: 3,
+ maxPlayers: 6,
+ scoreType: "positive",
+ teamRounds: 4,
+ fixedRounds: 5, // 4 manches en équipes + le décompte final
+ teams: [
+ { id: "bleu", label: "Équipe bleue", color: "#2563eb" },
+ { id: "rouge", label: "Équipe rouge", color: "#dc2626" },
+ ],
+ description:
+ "Les équipes changent à chaque manche : notez l'équipe gagnante des 4 manches, puis chacun révèle ses points. Le plus de points gagne.",
+ icon: "Drama",
+ colors: {
+ // Le rose de la moustache du morse : distinctif, et sans confusion
+ // possible avec le bleu / rouge des équipes.
+ primary: "#e5709b",
+ },
+};
diff --git a/src/games/six_qui_prend/config.ts b/src/games/six_qui_prend/config.ts
new file mode 100644
index 0000000..fcce0e7
--- /dev/null
+++ b/src/games/six_qui_prend/config.ts
@@ -0,0 +1,19 @@
+import { GameConfig } from "../../types";
+
+export const sixQuiPrendConfig: GameConfig = {
+ imagePath: "/games-assets/six-qui-prend-logo.webp",
+ bannerPath: "/games-assets/six-qui-prend-banner.webp",
+ id: "six_qui_prend",
+ name: "6 qui prend !",
+ minPlayers: 2,
+ maxPlayers: 10,
+ scoreType: "negative",
+ targetScore: 66,
+ targetScoreCondition: "reaches", // la partie s'arrête dès qu'un joueur atteint 66
+ description:
+ "Évitez de ramasser les têtes de bœuf ! La partie s'arrête quand un joueur atteint 66 points. Le plus petit score gagne.",
+ icon: "Beef",
+ colors: {
+ primary: "#d81f26", // Rouge vif de la boite
+ },
+};
diff --git a/src/pages/GameOver.tsx b/src/pages/GameOver.tsx
index 34777ad..5236b49 100644
--- a/src/pages/GameOver.tsx
+++ b/src/pages/GameOver.tsx
@@ -313,7 +313,38 @@ export default function GameOver() {
- {session.rounds.map((round) => (
+ {session.rounds.map((round) => {
+ // Manche jouée en équipes : une seule ligne avec l'équipe
+ // gagnante, plutôt qu'une rangée de zéros.
+ const team = round.winningTeamId
+ ? gameConfig?.teams?.find(
+ (t) => t.id === round.winningTeamId,
+ )
+ : undefined;
+ if (team) {
+ return (
+
+
+ #{round.roundNumber}
+
+
+
+ {team.label}
+
+
+
+ );
+ }
+ return (
- ))}
+ );
+ })}
Total
diff --git a/src/pages/PlayGame.tsx b/src/pages/PlayGame.tsx
index ddbd9db..66578bb 100644
--- a/src/pages/PlayGame.tsx
+++ b/src/pages/PlayGame.tsx
@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
-import { Check, Undo2, Award, Trash2, BookOpen } from "lucide-react";
+import { Check, Undo2, Award, Trash2, BookOpen, Users } from "lucide-react";
import * as Icons from "lucide-react";
import { useLiveQuery } from "dexie-react-hooks";
import { db } from "../database/db";
@@ -32,6 +32,7 @@ export default function PlayGame() {
>({});
const [currentCategoryIndex, setCurrentCategoryIndex] = useState(0);
+ const [selectedTeamId, setSelectedTeamId] = useState(null);
// Keep active session players updated if a global player rename happens
const playersInDb = useLiveQuery(() => db.players.toArray());
@@ -103,6 +104,17 @@ export default function PlayGame() {
return dbPlayer ? { ...p, name: dbPlayer.name } : p;
});
+ // Jeux en équipes (ex: Moustache) : les premières manches ne notent que
+ // l'équipe gagnante, la dernière sert au décompte final des points.
+ const teams = gameConfig.teams;
+ const isTeamGame = Boolean(teams?.length && gameConfig.teamRounds);
+ const isTeamRound =
+ isTeamGame && activeSession.rounds.length < (gameConfig.teamRounds ?? 0);
+ const isFinalTally = isTeamGame && !isTeamRound;
+
+ const teamWins = (teamId: string) =>
+ activeSession.rounds.filter((r) => r.winningTeamId === teamId).length;
+
const calculateTotalScore = (playerId: string) => {
return calculatePlayerTotalScore(
playerId,
@@ -146,6 +158,17 @@ export default function PlayGame() {
};
const handleValidateRound = async () => {
+ // Manche jouée en équipes : on n'enregistre que l'équipe gagnante.
+ if (isTeamRound) {
+ if (!selectedTeamId) return;
+ await addRound(
+ activeSession.players.map((p) => ({ playerId: p.id, score: 0 })),
+ selectedTeamId,
+ );
+ setSelectedTeamId(null);
+ return;
+ }
+
const scores: RoundScore[] = activeSession.players.map((p) => {
// For games with categories (like Harmonies), we store the sum as the main score
// and keep the details in the `details` object
@@ -305,8 +328,30 @@ export default function PlayGame() {
+ {/* Manches gagnées par équipe (les points restent secrets jusqu'au bout) */}
+ {isTeamGame && teams && (
+