Duels are Sealearn’s real-time PvP mode. Two users compete head-to-head on questions from a shared lesson, each answering independently and simultaneously. The system is built entirely on Socket.IO — there are no REST endpoints for gameplay. A short-lived invite flow in Redis coordinates the match setup before the duel state is committed to MongoDB, keeping latency low during the critical start window. When the duel ends a result message is automatically posted to the shared chat conversation, giving both players a permanent record of the match.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/DerBasilisk/SEA-ServicioEvaluaconAsistida/llms.txt
Use this file to discover all available pages before exploring further.
The duel socket runs on the default Socket.IO namespace (
/) — not /chat. Connect to the root path. The /chat namespace is separate and used only for messaging.The Duel Model
Duel documents are persisted in MongoDB once an invite is accepted. During active gameplay the live state (player progress, scores, modifiers) is held in Redis for low-latency reads and synced to MongoDB at the end of the match.| Field | Type | Description |
|---|---|---|
_id | ObjectId | MongoDB document ID. |
duelId | string | UUID generated at invite-accept time; used as the Redis key and the room name duel:<duelId>. |
lesson | ObjectId → Lesson | The lesson whose questions were used. |
conversation | ObjectId → Conversation | null | The chat conversation the duel was initiated from, if any. The result is posted here when the duel ends. |
type | "direct" | "group" | Always "direct" for 1v1 duels. |
creator | ObjectId → User | User who sent the original invite. |
players | array | Embedded player subdocuments (see below). |
winner | ObjectId → User | null | Set when the duel finishes; null while in progress. |
questions | ObjectId[] → Question | Questions used in this duel (for audit / replay). |
status | "waiting" | "active" | "finished" | "abandoned" | Current duel state. |
startedAt | Date | null | When the duel transitioned to "active". |
endedAt | Date | null | When the duel transitioned to "finished" or "abandoned". |
maxPlayers | number | Maximum participants; defaults to 2 for direct duels. |
inviteCode | string | null | Random alphanumeric code for group duels. |
questionCount | number | Number of questions selected for this duel (default 5). |
| Field | Type | Description |
|---|---|---|
user | ObjectId → User | The participant. |
score | number | Total XP-equivalent points scored. |
correct | number | Number of correctly answered questions. |
timeSpent | number | null | Total milliseconds from start to last answer. |
finishedAt | Date | null | When this player answered their last question. |
abandoned | boolean | true if the player left before the duel ended. |
Modifiers
Modifiers are power-ups a player can activate during a duel to hinder their opponent. Each modifier may be used once per duel. They are passed as IDs in theduel:use_modifier event.
| ID | Label | Icon | Effect |
|---|---|---|---|
extra_questions | Preguntas extra | ➕ | Adds 3 extra questions to the opponent’s queue. |
reduced_time | Tiempo reducido | ⏱️ | Limits the opponent to 10 seconds per question. |
blackout | Pantalla oscura | 🌑 | Triggers a 3-second black screen for the opponent. |
Authentication
Every socket must present a JWT insocket.handshake.auth.token. The auth middleware decodes the token synchronously and attaches socket.userId. Connections without a valid token are rejected with "Token inválido".
user:<userId>. Invite and start events are delivered to this room so they are received regardless of what UI screen the user is on.
Client → Server Events
duel:invite
Challenge a friend to a duel on a specific lesson. The server stores the invite in Redis (TTL: 60 seconds) and forwards it to the recipient. If the recipient is offline, the event is silently lost — the client should inform the user that their friend may be unavailable.
| Field | Type | Description |
|---|---|---|
friendId | string | MongoDB _id of the friend to challenge. |
lessonId | string | MongoDB _id of the lesson to duel on. |
conversationId | string | The chat conversation ID where the duel result will be posted on completion. |
duel:accept
Accept a pending invite. The server fetches the invite from Redis, builds the duel state (selecting questions via the adaptive service), persists a Duel document in MongoDB, and emits duel:start to both players.
| Field | Type | Description |
|---|---|---|
inviteId | string | UUID received in the duel:invited event. |
duel:reject
Decline an incoming invite. The invite is removed from Redis and the requester receives duel:rejected.
| Field | Type | Description |
|---|---|---|
inviteId | string | UUID received in the duel:invited event. |
duel:join
Rejoin the duel room after a page reload or reconnection. The server responds with duel:state containing the full current duel snapshot from Redis.
| Field | Type | Description |
|---|---|---|
duelId | string | UUID of the duel to rejoin. |
duel:answer
Submit an answer to a question. The server evaluates correctness, updates the player’s score in Redis, emits progress to the opponent via duel:opponent_progress, and returns the result to the answering player via duel:answer_result. If both players have finished all their questions, endDuel is triggered automatically.
| Field | Type | Description |
|---|---|---|
duelId | string | UUID of the active duel. |
questionId | string | MongoDB _id of the question being answered. |
answer | string | boolean | string[] | The answer value. Format depends on question type: option _id for multiple_choice, true/false for true_false, a string for fill_blank, and an ordered array of item IDs for order_items. |
duel:use_modifier
Activate a modifier to apply a disadvantage to the opponent.
| Field | Type | Description |
|---|---|---|
duelId | string | UUID of the active duel. |
modifierId | "extra_questions" | "reduced_time" | "blackout" | The modifier to activate. |
targetId | string | The opponent’s user ID. |
duel:abandon
Forfeit the duel. The duel is marked "abandoned" in both Redis and MongoDB, and the opponent receives duel:opponent_abandoned.
| Field | Type | Description |
|---|---|---|
duelId | string | UUID of the duel to abandon. |
Server → Client Events
duel:invite_sent
Confirmation sent back to the requester after duel:invite is processed and forwarded.
| Field | Type | Description |
|---|---|---|
inviteId | string | UUID of the newly created invite. Use this to cancel the invite if the user changes their mind. |
duel:accepted
Sent back to the socket that emitted duel:accept, confirming that the accept was processed successfully and the duel has been created. The duel:start event follows immediately on the same socket.
| Field | Type | Description |
|---|---|---|
duelId | string | UUID of the newly created duel. |
duel:state
Sent in response to duel:join. Contains the full current duel snapshot from Redis, allowing a reconnecting client to restore its UI state without missing progress.
duel:invited
Delivered to the recipient’s user:<userId> room when they receive a duel challenge.
| Field | Type | Description |
|---|---|---|
inviteId | string | UUID to pass back to duel:accept or duel:reject. |
lessonId | string | Lesson the challenger wants to duel on. |
requesterId | string | User ID of the challenger. |
conversationId | string | Chat conversation the invite originated from. |
duel:start
Emitted to both players when the duel is ready to begin. Contains all questions and opponent information. The client should transition to the duel gameplay screen immediately upon receiving this event.
If the requester is offline when the invite is accepted, their
duel:start payload is stored in Redis for up to 120 seconds. It is delivered to their personal socket room the next time they connect.| Field | Type | Description |
|---|---|---|
duelId | string | UUID of the duel — required for all subsequent gameplay events. |
questions | array | Sanitised question objects (correct answers stripped). Question format varies by type — see below. |
opponentId | string | User ID of the opponent. |
multiple_choice—optionsarray with{ _id, text }objects in randomised order; correct answer removed.true_false—correctBooleanfield removed; client receives only the statement.fill_blank—correctAnswersremoved; client receives the prompt text.order_items—itemsreplaced byshuffledItems(same items in random order).match_pairs—pairsreplaced byleftItemsandrightItemsarrays, each shuffled independently.
duel:answer_result
Returned to the answering player immediately after duel:answer, confirming whether they were correct.
| Field | Type | Description |
|---|---|---|
isCorrect | boolean | Whether the submitted answer was correct. |
explanation | string | null | Optional explanation from the question document. |
correctAnswer | string | boolean | null | The correct answer value, revealed after submission. null for order_items. |
duel:opponent_progress
Broadcast to the opponent’s socket (in the duel:<duelId> room) after every answer, so the client can animate the opponent’s progress bar in real time.
| Field | Type | Description |
|---|---|---|
userId | string | The opponent’s user ID. |
currentIndex | number | Number of questions the opponent has answered so far. |
score | number | Opponent’s current score. |
correct | number | Number of correct answers so far. |
finished | boolean | true when the opponent has answered all their questions. |
duel:modifier_received
Delivered to the target player when an opponent activates a modifier against them. The client must apply the visual/mechanical effect immediately.
| Field | Type | Description |
|---|---|---|
modifier | object | The full modifier object: { id, label, icon, description }. |
duel:modifier_used
Confirmation sent back to the player who triggered the modifier.
| Field | Type | Description |
|---|---|---|
modifier | object | The activated modifier object. |
targetId | string | The user ID the modifier was applied to. |
duel:finished
Emitted to the duel:<duelId> room when both players have answered all questions. Contains final scores and the winner’s ID. A duel_result message is also automatically posted to the associated chat conversation, if one was provided when the invite was created.
| Field | Type | Description |
|---|---|---|
winner | string | User ID of the winner (determined by most correct answers; ties broken by timeSpent). |
players | array | Final stats for each player. |
| Field | Type | Description |
|---|---|---|
userId | string | Player’s user ID. |
score | number | Final score. |
correct | number | Total correct answers. |
total | number | Total number of questions in the duel. |
finishedAt | number | null | Unix timestamp (ms) when the player finished. |
timeSpent | number | null | Total milliseconds spent answering. |
duel:opponent_abandoned
Emitted to the remaining player when their opponent fires duel:abandon. The remaining player is considered the winner by default.
duel:rejected
Delivered to the requester when the recipient declines the invite.
| Field | Type | Description |
|---|---|---|
inviteId | string | UUID of the declined invite. |
duel:error
Emitted back to the originating socket when any handler throws an unhandled error.
| Field | Type | Description |
|---|---|---|
message | string | Human-readable error description. |