Skip to main content

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.

Duels turn Sealearn’s solo practice loop into a head-to-head competition. Two students race through the same set of adaptive questions simultaneously — every answer is scored in real time, progress is broadcast to the opponent, and a winner is declared the moment both players finish. The entire system runs over a persistent Socket.IO connection, with Redis storing ephemeral invite and duel state between events.

Architecture Overview

The duel server (duel.socket.js) mounts on the main HTTP server and registers an authentication middleware that verifies a JWT on every socket handshake:
io.use((socket, next) => {
  const token = socket.handshake.auth.token;
  if (!token) return next(new Error("Sin token"));
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  socket.userId = decoded._id;
  next();
});
Once authenticated, each socket automatically joins a private room user:<userId> so that targeted events can be delivered even when a duel room has not yet been created. Duel state is persisted in two places:
  • Redis — invite records (keyed by UUID), active duel state, and pending_duel:<userId> entries for reconnecting players
  • MongoDB — a permanent Duel document created at acceptance and updated at completion or abandonment

In-Game Modifiers

Modifiers are one-shot powerups that a player can activate during an active duel to disrupt their opponent. The duel:use_modifier event targets a specific player by targetId; the modifier is pushed onto duel.players[targetId].modifiers in Redis and emitted to the target via duel:modifier_received.
Modifier IDLabelIconEffect
extra_questionsPreguntas extraAdds 3 questions to the opponent’s session
reduced_timeTiempo reducido⏱️Limits the opponent to 10 seconds per question
blackoutPantalla oscura🌑Shows a 3-second black screen to the opponent

Duel Lifecycle

1

Send Invite — duel:invite

The challenger emits duel:invite with { friendId, lessonId, conversationId }. The server generates a UUID, stores the invite in Redis, and forwards a duel:invited event to the recipient’s private room. The challenger receives duel:invite_sent with the inviteId.
socket.emit("duel:invite", {
  friendId: "64f3a...",
  lessonId: "64e1b...",
  conversationId: "64c9d..."
});
2

Receive Invite — duel:invited

The recipient’s client receives duel:invited containing { inviteId, lessonId, requesterId, conversationId }. The UI typically shows a modal or notification giving the recipient the option to accept or decline. The invite is stored in Redis with a TTL and will expire automatically if no response arrives in time.
3

Accept or Reject — duel:accept / duel:reject

If the recipient emits duel:accept with { inviteId }, the server:
  1. Fetches and deletes the invite from Redis
  2. Loads the lesson and runs getAdaptiveConfig() + selectQuestions() using the challenger’s user ID
  3. Sanitises the questions (strips correctBoolean, correctAnswers, shuffles options)
  4. Creates a duelState object in Redis and a permanent Duel document in MongoDB
  5. Both players are joined to the duel:<duelId> room
If the recipient emits duel:reject with { inviteId }, the invite is deleted from Redis and duel:rejected is sent back to the challenger.
4

Duel Starts — duel:start

Both players receive duel:start with { duelId, questions, opponentId }. If the challenger is temporarily disconnected when the duel is created, the payload is stored in Redis as pending_duel:<userId> (TTL 120 seconds) and delivered the next time that socket reconnects.
5

Answer Questions — duel:answer

Players emit duel:answer for each question:
socket.emit("duel:answer", {
  duelId: "uuid-...",
  questionId: "64f3b...",
  answer: "64f3c..."   // option _id, boolean, string, array, etc.
});
The server evaluates correctness (supporting multiple_choice, true_false, fill_blank, and order_items in duel mode), updates the player’s score, correct, and currentIndex in Redis, and emits two events:
  • duel:answer_result → to the answering player: { isCorrect, explanation, correctAnswer }
  • duel:opponent_progress → to the other player: { userId, currentIndex, score, correct, finished }
6

Duel Ends — duel:finished

When both players have finished: true, endDuel() is called. The winner is determined first by most correct answers, then by earliest finish time (finishedAt). The result is broadcast to duel:<duelId> as duel:finished:
{
  winner: "userId-of-winner",
  players: [
    { userId, score, correct, total, finishedAt, timeSpent },
    { userId, score, correct, total, finishedAt, timeSpent }
  ]
}
Results are also stored in Redis as duel_result:<userId> (TTL 300 seconds) for late-arriving clients, and a duel_result message is posted to the linked chat conversation (if conversationId was provided).
7

Abandon — duel:abandoned

If a player emits duel:abandon, the duel status is set to "abandoned" in Redis, abandonDuelInMongo is called, and duel:opponent_abandoned is emitted to the remaining player.

Duel Statistics

Every completed or abandoned duel updates the user’s duel statistics on the User model:
duelsStats: {
  total:  { type: Number, default: 0 },
  wins:   { type: Number, default: 0 },
  losses: { type: Number, default: 0 },
}
These counters are visible on public profiles and used for leaderboard calculations. XP earned during a duel is also contributed to the player’s league room weekly total via addLeagueXP.

Build docs developers (and LLMs) love