Skip to content

Sistema de XP

El sistema de XP (puntos de experiencia) asigna valor numérico al progreso del estudiante en la plataforma. Su propósito es doble:

  1. Nivel personal — Cada estudiante tiene un nivel del 1 al 10 con un nombre de rango. El nivel se muestra en el dashboard y en el portfolio público con una barra de progreso hacia el siguiente umbral.
  2. Criterio de badges — Los badges de categoría LEVEL (y algunos mixtos) usan el XP acumulado como condición de desbloqueo automático.

No hay leaderboard público ni desbloqueo de contenido por XP; el acceso al contenido se gobierna exclusivamente por el AccessLevelGuard basado en el plan de suscripción del usuario.


El XP total de un estudiante se calcula como la suma de eventos XP más la contribución de badges activos por tier.

Evento (eventType)XP otorgadoMódulo emisorCondición
LESSON_COMPLETED+10 XPProgressModulePrimera vez que se marca una lección como completada
ROUTE_COMPLETED+100 XPProgressModuleTodas las lecciones de un path marcadas como completadas
QUIZ_PASSED+20 XPQuizModuleIntento de quiz con passed = true
QUIZ_PERFECT+25 XPQuizModuleIntento de quiz con score = 100 (acumulable con QUIZ_PASSED)

Los badges activos del estudiante (no revocados) aportan XP adicional según su tier. Esta contribución no genera un StudentXPEvent — se suma al vuelo al calcular el total.

Tier del badgeXP aportado
bronze (tier 1)+25 XP
silver (tier 2)+50 XP
gold (tier 3)+100 XP
platinum (tier 4)+200 XP
legend (tier 5)+500 XP

xpTotal = Σ(StudentXPEvent.xp) + Σ(BADGE_TIER_XP[badge.tier])

Donde BADGE_TIER_XP = { 1: 25, 2: 50, 3: 100, 4: 200, 5: 500 }.

No existen multiplicadores, streaks de XP ni bonificaciones temporales. La fórmula es idéntica en XpService.getStudentXPSummary(), PortfolioService.getPublicPortfolio() y StudentBadgesService.calculateStats(), garantizando que dashboard y portfolio público muestren siempre el mismo total.


El nivel se deriva del XP total con la función xpToLevel:

nivel = min(10, max(1, floor(xpTotal / 500) + 1))

Esto significa 500 XP por nivel, con máximo en el nivel 10.

NivelXP mínimoXP máximo del tramoNombre de rango
10499Aprendiz IA
2500999Aprendiz IA
31 0001 499Aprendiz IA
41 5001 999Practicante IA
52 0002 499Practicante IA
62 5002 999Arquitecto IA
73 0003 499Arquitecto IA
83 5003 999Experto IA
94 0004 499Experto IA
104 500Maestro IA

Los umbrales del tramo actual (xpCurrentLevel, xpNextLevel) se calculan como:

xpCurrentLevel = (nivel - 1) * 500
xpNextLevel = nivel * 500

Al alcanzar el nivel 10 (xpTotal ≥ 4 500), el progreso se muestra al 100% y se omite el mensaje “XP para el siguiente nivel”.


flowchart TD
A[Estudiante] -->|completa lección| B[ProgressService.completeLesson]
A -->|completa path| C[ProgressService — path completion]
A -->|envía quiz| D[QuizService.submitAttempt]
B -->|emitEvent LESSON_COMPLETED +10| E[XpService.emitEvent]
C -->|emitEvent ROUTE_COMPLETED +100| E
D -->|passed=true → emitEvent QUIZ_PASSED +20| E
D -->|score=100 → emitEvent QUIZ_PERFECT +25| E
E -->|INSERT StudentXPEvent| F[(BD: StudentXPEvent)]
G[Dashboard / Portfolio] -->|getStudentXPSummary| H[XpService]
H -->|SELECT StudentXPEvent| F
H -->|SELECT StudentBadgeGrant activos| I[(BD: StudentBadgeGrant)]
H -->|xpTotal = eventos + badges| J[XPSummary]
J -->|nivel, levelName, thresholds| G

Archivo: apps/api/src/modules/xp/xp.service.ts
Módulo: XpModule (no es @Global() — los módulos consumidores deben importar XpModule)

emitEvent(userId, eventType, xp, metadata?)

Section titled “emitEvent(userId, eventType, xp, metadata?)”

Registra un evento XP. Operación fire-and-forget: nunca lanza excepciones al llamador; los errores se loguean internamente.

// Ejemplo de uso desde ProgressService
this.xpService.emitEvent(userId, 'LESSON_COMPLETED', 10).catch(() => {})
// Con metadata opcional (queda en StudentXPEvent.metadata)
this.xpService.emitEvent(userId, 'QUIZ_PASSED', 20, { quizId, attemptId }).catch(() => {})

getStudentXPSummary(userId): Promise<XPSummary>

Section titled “getStudentXPSummary(userId): Promise<XPSummary>”

Calcula y devuelve el resumen XP completo del estudiante:

interface XPSummary {
xpTotal: number // XP acumulado total (eventos + badges)
currentLevel: number // Nivel actual 1–10
levelName: string // Nombre del rango (ej. "Practicante IA")
xpCurrentLevel: number // Umbral inferior del tramo actual, ej. 1500
xpNextLevel: number // Umbral superior del tramo actual, ej. 2000
}
type XpEventType =
| 'LESSON_COMPLETED'
| 'ROUTE_COMPLETED'
| 'BADGE_EARNED' // Reservado — no emitido actualmente; las badges aportan XP por tier
| 'QUIZ_PASSED'
| 'QUIZ_PERFECT'
MóduloOperación
ProgressModuleemitEvent al completar lección o path
QuizModuleemitEvent al superar un quiz
DashboardModulegetStudentXPSummary para el widget del dashboard
StudentBadgesModulecalculateStudentXP auxiliar en evaluador de badges

Historial de eventos individuales. Cada fila representa una ganancia de XP.

model StudentXPEvent {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id])
eventType String // 'LESSON_COMPLETED' | 'ROUTE_COMPLETED' | 'QUIZ_PASSED' | 'QUIZ_PERFECT'
xp Int // Valor positivo del evento
metadata Json? // Contexto opcional: { quizId, attemptId, pathId, source: 'backfill' }
createdAt DateTime @default(now())
@@index([userId])
}

No existe un campo xpTotal desnormalizado en el modelo User. El total siempre se calcula sumando los eventos en tiempo real, más la contribución de badges activos.


El componente inline en DashboardPage.tsx muestra:

  • Círculo con número de nivel (gradiente naranja)
  • XP total en tipografía grande
  • Nombre del rango (ej. “Practicante IA”)
  • Barra de progreso del tramo actual (xpTotal - xpCurrentLevel / xpNextLevel - xpCurrentLevel)
  • XP restantes para el siguiente nivel

Datos servidos por GET /api/dashboardDashboardService.getSummary() → incluye xpTotal, currentLevel, levelName, xpCurrentLevel, xpNextLevel en el objeto stats.

El componente XPMeter en apps/web/src/components/portfolio/XPMeter.tsx es visible en el portfolio público del estudiante si settings.showXP = true. Muestra el XP total, nivel, barra de progreso y, si rankPercentile > 0, el percentil en la plataforma.

Los datos provienen de GET /api/portfolio/:usernamePortfolioService.getPublicPortfolio(), que calcula el XP con la misma fórmula que XpService.

BadgeSummary (devuelto por GET /api/student-badges/summary) incluye el campo xp calculado por StudentBadgesService.calculateStats(). Se usa para determinar el progreso hacia el siguiente badge de nivel.