Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 69 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,39 +181,80 @@ The core game logic, the real-time game engine, the backend architecture, and th
---

# Database Schema
## Database Schema

Culture Quiz uses **PostgreSQL with Prisma ORM**. The database stores user accounts, quiz content, game sessions, tournament progression, answers, chat messages and gamification data.

### Entity relationships

```text
┌──────────────┐
│ User │
└──────┬───────┘
┌────────────────┼────────────────┐
│ │ │
▼ ▼ ▼
GlobalMessage UserBadge RoomParticipant
│ │ │
│ ▼ │
│ Badge │
│ │
│ ┌────────────┴────────────┐
│ │ │
│ ▼ ▼
│ ┌───────────┐ Answer
│ │ Room │ │
│ └─────┬─────┘ │
│ │ │
│ ┌─────────┴─────────┐ │
│ │ │ │
│ ▼ ▼ │
│ RoomQuestion Tournament │
│ │ │ │
│ ▼ │ │
│ Question ◄──────────────┘ │
│ │ │
│ ┌────┴────┐ │
│ ▼ ▼ │
│ Category AnswerChoice │
│ │
└─────────────────────────────────────────────┘


Tournament
┌───────────────┐
│ Room │
└───────┬───────┘
nextRoomId
┌───────────────┐
│ Next Room │
└───────────────┘
```

Our database relies on a robust relational PostgreSQL model mapped via Prisma. Here is an overview of the core entities and their relationships:

* **User (`User`):**
* *Key fields:* `id` (String/UUID), `username` (String), `email` (String), `status` (Enum: OFFLINE, ONLINE, IN_GAME), `xp` (Int), `isTwoFactorEnabled` (Boolean).
* *Relationships:* One-to-many relationships with `GlobalMessage` (chat), `Friendship` (sent/received), `RoomParticipant` (game history), and `UserBadge`.

* **Game Sessions (`Room` & `Tournament`):**
* *Key fields:* `id` (String/UUID), `mode` (Enum: SOLO, DUEL, TOURNAMENT), `status` (Enum: WAITING, IN_PROGRESS, FINISHED), `round` (Enum: SEMI_FINAL, FINAL - tournament rooms only).
* *Relationships:* A `Tournament` contains multiple `Room` entities and links to its `champion` (RoomParticipant). A `Room` has many `RoomParticipant`s and `RoomQuestion`s (ordered), links to an optional `winner` participant (null = draw), and to a `nextRoom` (self-relation: the winner advances through the bracket).

* **Answers (`Answer`):**
* *Key fields:* `isCorrect` (Boolean), `timeTakenMs` (Int - used for tie-breaking on equal scores).
* *Relationships:* Belongs to a `RoomParticipant` and a `Question`.

* **Participants (`RoomParticipant`):**
* *Key fields:* `score` (Int), `isBot` (Boolean - AI opponents), `userId` (nullable for bots).
* *Relationships:* Links a `User` to a `Room`; unique per `(roomId, userId)`.
### Main entities

* **Social (`GlobalMessage` & `Friendship`):**
* *Key fields (Message):* `id` (String/UUID), `content` (String), `createdAt` (DateTime).
* *Relationships (Message):* Belongs to an `author` (User).
* *Key fields (Friendship):* `status` (Enum: PENDING, ACCEPTED, DECLINED).
* *Relationships (Friendship):* Links a `sender` (User) to a `receiver` (User).
- **`User`** — Stores account, authentication, OAuth, profile, status and XP information.
- **`Room`** — Represents a game session. A room can be a `DUEL` or `TOURNAMENT` game. Solo vs AI matches are not persisted in the database, as we do not want them to be included in player statistics.
- **`RoomParticipant`** — Links a user (or a bot) to a room and stores their score. It is used for regular stats games and tournament matches.
- **`Tournament`** — Groups tournament rooms and connects them through `nextRoomId` to represent the tournament bracket. The winning `RoomParticipant` is stored as the tournament champion.
- **`Question`** — Stores the quiz questions and belongs to a `Category`.
- **`AnswerChoice`** — Stores the possible answers for a question and identifies the correct choice.
- **`RoomQuestion`** — Links questions to a specific room and stores their order during the game.
- **`Answer`** — Records a participant's answer, its correctness and the time taken.
- **`GlobalMessage`** — Stores messages sent in the global chat and their author.
- **`Badge`** / **`UserBadge`** — Handle the gamification and achievement system.

* **Trivia (`Question`, `Category`, `AnswerChoice`):**
* *Key fields:* `text` (String), `isCorrect` (Boolean for answers).
* *Relationships:* A `Category` contains multiple `Question`s. A `Question` contains multiple `AnswerChoice`s and links to game rooms via `RoomQuestion`.
Game informations are mostly used for the profile page and game stats.

* **Gamification (`Badge` & `UserBadge`):**
* *Key fields:* `code` (String), `name` (String), `description` (String).
* *Relationships:* A many-to-many relationship mapping a `User` to their earned `Badge`s through the `UserBadge` join table.
### Data integrity

The schema uses **foreign keys, unique constraints and explicit Prisma relations** to maintain consistency. For example, a user cannot participate twice in the same room, a badge cannot be awarded twice to the same user, and a question has a unique position within a given room.
*(Note: The full schema is detailed in our `backend/prisma/schema.prisma` file).*

---
Expand Down
29 changes: 16 additions & 13 deletions backend/src/events/events.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,18 +221,18 @@ implements OnGatewayConnection, OnGatewayDisconnect{
delay = this.random(4000, 14000);
break;

case "normal":
accuracy = 0.60;
case "medium":
accuracy = 0.50;
delay = this.random(8000, 16000);
break;

case "hard":
accuracy = 0.40;
accuracy = 0.25;
delay = this.random(10000, 18000);
break;

default:
accuracy = 0.55;
accuracy = 0.5;
delay = this.random(8000, 16000);
}
game.ai.accuracy = accuracy;
Expand Down Expand Up @@ -745,16 +745,19 @@ implements OnGatewayConnection, OnGatewayDisconnect{

try
{
await this.gameResultsService.recordMatch({
winner,
player1Id: game.player1Id,
player2Id: game.player2Id,
player1Score: game.player1Score,
player2Score: game.player2Score,
questions: game.questionHistory,
});
if (!game.ai)
{
await this.gameResultsService.recordMatch({
winner,
player1Id: game.player1Id,
player2Id: game.player2Id,
player1Score: game.player1Score,
player2Score: game.player2Score,
questions: game.questionHistory,
});

await this.awardGamification(game, winner);
await this.awardGamification(game, winner);
}
}
catch (error)
{
Expand Down
2 changes: 1 addition & 1 deletion backend/src/game/game.session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export class GameSession
question: string;
correct: string;
answers: string[];
difficulty: "easy" | "normal" | "hard",
difficulty: "easy" | "medium" | "hard",
category : string,
}[] = [];

Expand Down
2 changes: 1 addition & 1 deletion backend/src/trivia/trivia.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export class TriviaService
question: "La Terre est une...",
correct: "Planète",
answers: ["Planète", "Étoile", "Lune", "Comète"],
difficulty: "normal" as const,
difficulty: "medium" as const,
category: "Science",
},
{
Expand Down