⚔️ VERSUS — Guía de coordinación técnica
Este documento es la referencia central del equipo. Si no sabes por dónde empezar, empieza aquí. Si tienes dudas de qué hace un endpoint, búscalo aquí. Si vas a crear algo nuevo, comprueba que no existe ya aquí.
🧠 Qué es Versus (versión rápida)
Un juego de preguntas multijugador con 5 modos de juego. Las preguntas se extraen automáticamente de la web con scrapers. Hay matchmaking, ranking global, perfiles y sistema de vidas.
Stack: - Backend: Spring Boot — API REST + WebSockets - Frontend: Angular — SPA - Base de datos: PostgreSQL - Scraping: Scrapy (Python) - Infraestructura: Docker
🗺️ Módulos del sistema
El proyecto se divide en 9 módulos. Cada issue pertenece a uno.
┌─────────────────────────────────────────────────────────┐
│ VERSUS │
│ │
│ [AUTH] [USERS] [QUESTIONS] [GAME] [MATCH] │
│ [STATS] [ACHIEVEMENTS] [SCRAPING] [ADMIN] │
└─────────────────────────────────────────────────────────┘
📦 Módulo 1 — AUTH
Issues: #39, #40, #84, #85
Autenticación con JWT. Login, registro, refresco de token y roles.
Endpoints
| Método | Ruta | Descripción | Issues |
|---|---|---|---|
POST |
/api/auth/register |
Registro de nuevo usuario | #85 |
POST |
/api/auth/login |
Login → devuelve accessToken + refreshToken |
#84 |
POST |
/api/auth/refresh |
Renueva el accessToken con el refreshToken | #84 |
POST |
/api/auth/logout |
Invalida el refreshToken | #84 |
Contrato de login
Request:
POST /api/auth/login
{
"email": "raul@versus.com",
"password": "mipassword"
}
Response:
{
"accessToken": "eyJhbGci...",
"refreshToken": "eyJhbGci...",
"user": {
"id": "uuid",
"username": "Raúl",
"role": "PLAYER",
"avatarUrl": "https://..."
}
}
Roles disponibles
| Rol | Valor | Acceso |
|---|---|---|
| Jugador | PLAYER |
Todo lo de juego |
| Moderador | MODERATOR |
+ gestión de reportes |
| Admin | ADMIN |
+ gestión de usuarios y spiders |
Para el frontend (#40)
- Implementar
AuthInterceptorque adjunte elBearer tokenen cada petición - Implementar
AuthGuardpara rutas protegidas - Guardar tokens en
localStorageosessionStorage
👤 Módulo 2 — USERS
Issues: #85
Gestión del perfil de usuario.
Endpoints
| Método | Ruta | Descripción |
|---|---|---|
GET |
/api/users/me |
Perfil del usuario autenticado |
PUT |
/api/users/me |
Actualizar username o avatar |
PUT |
/api/users/me/password |
Cambiar contrasena (requiere contrasena actual) |
PUT |
/api/users/me/avatar |
Seleccionar avatar por URL o subir imagen multipart |
DELETE |
/api/users/me |
Eliminar cuenta con soft delete y anonimizacion |
GET |
/api/users/:id |
Perfil público de cualquier usuario |
Contrato de perfil
GET /api/users/me
→ 200
{
"id": "uuid",
"username": "Raúl",
"email": "raul@versus.com",
"avatarUrl": "https://...",
"role": "PLAYER",
"createdAt": "2025-01-01T00:00:00Z"
}
Contratos de ajustes de cuenta
La pantalla /settings centraliza cuenta, avatar, notificaciones, audio y zona de peligro.
Cambiar password:
PUT /api/users/me/password
{
"currentPassword": "actual123",
"newPassword": "nueva1234"
}
204 No Content. La nueva contrasena debe tener minimo 8 caracteres.
Seleccionar avatar predefinido:
PUT /api/users/me/avatar
Content-Type: application/json
{
"avatarUrl": "https://api.dicebear.com/..."
}
Subir avatar propio:
PUT /api/users/me/avatar
Content-Type: multipart/form-data
file=<png|jpeg|max 2MB>
El backend devuelve UserMeResponse y el frontend actualiza topbar/perfil inmediatamente. Hasta que exista el modulo de almacenamiento, el upload se guarda como data:image/...;base64 en users.avatar_url.
Eliminar cuenta:
DELETE /api/users/me
204 No Content. En frontend se exige doble confirmacion escribiendo el username exacto. En backend se aplica soft delete con status=DELETED, is_active=false y anonimizacion de username/email/avatar/password.
Pantalla /settings (frontend)
- Cuenta: username editable; email visible pero dependiente del modulo de email para cambio real.
- Password: requiere password actual y confirmacion visual en el formulario.
- Avatar: galeria de avatares predefinidos con confirmacion
Aceptar/Cancelar; upload PNG/JPEG con crop basico y boton de subida. - Notificaciones: preferencias de solicitudes de amistad, invitaciones y logros guardadas en
localStorage. - Audio: controles
Efectos de sonido,Musica de fondo, silenciar todo y feedback reducido guardados enlocalStorage. - Zona de peligro: borrar cuenta exige escribir el username.
- Topbar: muestra username/avatar reales y XP calculado desde
/api/stats/memientras no exista campoxpdedicado.
❓ Módulo 3 — QUESTIONS
Issues: #41, #42, #43, #44, #52
Preguntas binarias y numéricas que alimentan los modos de juego.
Tipos de pregunta
| Tipo | Descripción | Ejemplo |
|---|---|---|
BINARY |
Dos opciones, una correcta | ¿Quién tiene más goles: Messi o Cristiano? |
NUMERIC |
Respuesta numérica libre | ¿Cuántos seguidores tiene Cristiano en Instagram? |
Endpoints
| Método | Ruta | Descripción | Issues |
|---|---|---|---|
GET |
/api/questions/random |
Pregunta aleatoria (opcionalmente por categoría o tipo) | #42 |
GET |
/api/questions/random?type=BINARY&category=football |
Filtros opcionales | #42, #43 |
GET |
/api/questions/:id |
Pregunta por ID | #41 |
GET |
/api/questions/categories |
Lista de categorías disponibles | #43 |
Contrato de pregunta BINARY
{
"id": "uuid",
"type": "BINARY",
"text": "¿Quién tiene más seguidores en Instagram?",
"category": "football",
"options": [
{ "id": "uuid-a", "text": "Cristiano Ronaldo" },
{ "id": "uuid-b", "text": "Lionel Messi" }
],
"scrapedAt": "2025-04-01T00:00:00Z"
}
⚠️ La opción correcta NO se envía al frontend hasta que el jugador responde. El backend la guarda internamente y la valida al recibir la respuesta.
Contrato de pregunta NUMERIC
{
"id": "uuid",
"type": "NUMERIC",
"text": "¿Cuántos seguidores tiene Cristiano Ronaldo en Instagram?",
"category": "football",
"unit": "millones",
"scrapedAt": "2025-04-01T00:00:00Z"
}
🎮 Módulo 4 — GAME (modos en solitario)
Issues: #53, #54, #55, #56, #57, #58, #59, #60, #61, #62
Los dos modos de un solo jugador: Supervivencia y Precisión.
Flujo general de partida
Frontend Backend
│ │
├─ POST /api/game/start ─────────►│ Crea sesión de partida
│◄── { sessionId, question } ─────┤ Devuelve 1ª pregunta
│ │
├─ POST /api/game/answer ────────►│ Valida respuesta
│◄── { correct, lifeDelta, ──────┤ Devuelve resultado
│ nextQuestion | gameOver } │
│ │
└─ (si gameOver) ─────────────────┤ Guarda historial
Endpoints modo Supervivencia
| Método | Ruta | Descripción | Issues |
|---|---|---|---|
POST |
/api/game/survival/start |
Inicia partida, devuelve sesión + 1ª pregunta | #53 |
POST |
/api/game/survival/answer |
Envía respuesta, recibe resultado y siguiente pregunta | #53, #55 |
Request answer:
{
"sessionId": "uuid",
"questionId": "uuid",
"optionId": "uuid-a"
}
Response answer:
{
"correct": true,
"livesRemaining": 3,
"lifeDelta": 0,
"streak": 4,
"scoreDelta": 150,
"nextQuestion": { ... },
"gameOver": false,
"achievementsUnlocked": []
}
Endpoints modo Precisión
| Método | Ruta | Descripción | Issues |
|---|---|---|---|
POST |
/api/game/precision/start |
Inicia partida numérica | #60 |
POST |
/api/game/precision/answer |
Envía número, recibe desviación y daño/curación | #59, #60 |
Request answer:
{
"sessionId": "uuid",
"questionId": "uuid",
"value": 650000000
}
Response answer:
{
"correctValue": 640000000,
"deviation": 1.56,
"deviationPercent": 1.56,
"lifeDelta": 5,
"livesRemaining": 105,
"nextQuestion": { ... },
"gameOver": false,
"achievementsUnlocked": []
}
📐
lifeDeltapositivo = curación, negativo = daño. El algoritmo de daño/curación se define en #59.
⚡ Módulo 5 — MATCH (modos multijugador)
Issues: #63, #64, #65, #66, #67, #68, #69, #70, #71, #72, #73, #74
Los tres modos PvP en tiempo real: Duelo binario, Duelo de precisión y Sabotaje.
⚠️ Este módulo usa WebSockets, no REST
La comunicación durante la partida es por WebSocket (STOMP sobre SockJS es el estándar en Spring Boot).
Conexión WebSocket
Frontend conecta a: ws://localhost:8080/ws
Suscripciones del cliente:
/user/queue/match → eventos de la partida (respuestas, vidas, resultado)
/topic/match/{matchId} → estado compartido de la sala
Envíos del cliente:
/app/match/answer → enviar respuesta
/app/match/ready → confirmar que está listo para empezar
Flujo de sala de espera → partida
1. POST /api/match/queue → Entrar en cola de matchmaking
2. Backend empareja dos jugadores → emite evento "MATCH_FOUND"
3. Frontend redirige a /sala/:matchId
4. Cada jugador envía /app/match/ready
5. Cuando ambos están listos → emite "MATCH_START" con 1ª pregunta
6. Cada pregunta → jugadores responden → backend procesa → emite resultado
7. Al terminar → emite "MATCH_END" con ganador y stats
Endpoints REST de sala (previos a la partida)
| Método | Ruta | Descripción | Issues |
|---|---|---|---|
POST |
/api/match/queue |
Entrar en cola de matchmaking | #66 |
DELETE |
/api/match/queue |
Salir de la cola | #66 |
POST |
/api/match/room |
Crear sala privada con código | #65 |
POST |
/api/match/room/join |
Unirse a sala privada por código | #65 |
GET |
/api/match/:matchId |
Estado actual de una sala | #65 |
Eventos WebSocket (backend → frontend)
| Evento | Canal | Payload |
|---|---|---|
MATCH_FOUND |
/user/queue/match |
{ matchId, opponent } |
MATCH_START |
/topic/match/{id} |
{ question, mode } |
QUESTION |
/topic/match/{id} |
{ question, timeLimit } |
ROUND_RESULT |
/topic/match/{id} |
{ player1Lives, player2Lives, correct } |
MATCH_END |
/topic/match/{id} |
{ winner, stats } |
Lógica de daño por modo
| Modo | Quién pierde vida | Cuándo |
|---|---|---|
| Duelo binario | El que falla | Al responder |
| Duelo de precisión | El que se desvía más | Al responder ambos |
| Sabotaje | El rival | Cuando el otro acierta mejor |
El algoritmo de daño al rival (Sabotaje) se implementa en #74.
📊 Módulo 6 — STATS & RANKING
Issues: #54, #76, #77, #78, #79
Historial de partidas, estadísticas personales y ranking global.
Endpoints
| Método | Ruta | Descripción | Issues |
|---|---|---|---|
GET |
/api/stats/me |
Estadísticas del usuario autenticado | #77 |
GET |
/api/stats/me?mode=SURVIVAL |
Filtradas por modo | #77 |
GET |
/api/stats/me/history |
Historial de partidas | #76 |
GET |
/api/ranking/:mode |
Top 100 de un modo | #78 |
GET |
/api/ranking/:mode/me |
Posición propia en el ranking | #78 |
Contrato de stats
GET /api/stats/me?mode=SURVIVAL
→ 200
{
"mode": "SURVIVAL",
"gamesPlayed": 42,
"gamesWon": 28,
"winRate": 66.6,
"bestStreak": 12,
"currentStreak": 3,
"avgDeviation": null
}
avgDeviationsolo aplica a modos numéricos (PRECISION, PRECISION_DUEL).
Modulo 7 - ACHIEVEMENTS
Sistema de logros y emblemas visibles en perfil/topbar.
Los logros se evaluan al terminar una partida singleplayer desde GameService. El catalogo inicial se siembra en arranque y cada logro solo puede desbloquearse una vez por usuario.
Endpoints
| Metodo | Ruta | Descripcion |
|---|---|---|
GET |
/api/achievements |
Catalogo completo con estado para el usuario autenticado |
Contrato de logro
{
"id": "uuid",
"key": "first_game",
"name": "Primeros pasos",
"description": "Juega tu primera partida.",
"iconKey": "first",
"category": "Primeros pasos",
"unlocked": true,
"unlockedAt": "2026-05-07T15:00:00Z"
}
Si un logro esta bloqueado, el backend devuelve name: "???", description: "???" e iconKey: "lock" para no revelar como desbloquearlo.
Eventos WebSocket
| Evento | Canal | Payload |
|---|---|---|
ACHIEVEMENT_UNLOCKED |
/user/queue/achievements |
{ "type": "ACHIEVEMENT_UNLOCKED", "achievement": { ... } } |
Frontend
- Toast global no bloqueante al desbloquear un logro.
- Perfil: seccion
Logroscon contadordesbloqueados/total, grid de catalogo y fecha si esta desbloqueado. - Topbar/avatar: muestra como emblema el logro desbloqueado mas reciente.
🕷️ Módulo 8 — SCRAPING
Scrapers en Scrapy que extraen datos reales y los insertan en PostgreSQL como preguntas.
Spiders planificadas
| Spider | Fuente | Tipo pregunta | Issue |
|---|---|---|---|
| RRSS (YouTube/TikTok/Twitch) | SocialBlade | NUMERIC | #97 ✅ |
| Estadísticas fútbol | FBref / Transfermarkt | NUMERIC + BINARY | #98 |
| Taquilla de cine | Box Office Mojo | NUMERIC | #98 |
| Capitales y geografía | Wikipedia | BINARY | #98 |
| Récords varios | Wikipedia / Guinness | NUMERIC | #98 |
Pipeline de datos
Spider (Scrapy)
│ yield QuestionItem(text, type, category, ...)
▼
DeerdaysScraperPipeline (#97 ✅)
├── Validación de calidad mínima
├── Deduplicación por SHA-256(text) → campo text_hash en questions
├── INSERT questions + question_options
└── UPDATE spider_runs (questionsInserted, errors, finishedAt)
│
▼
PostgreSQL → tabla questions (estado: PENDING_REVIEW)
│
▼
Moderador revisa → estado: ACTIVE
Ver documentación detallada del pipeline en
docs/scraping-pipeline.md.
Endpoints de gestión (solo ADMIN)
Implementados en #97. El identificador de ruta es el nombre del spider, no su UUID.
| Método | Ruta | Descripción | Issue |
|---|---|---|---|
GET |
/api/admin/spiders |
Lista de spiders con estado actual y último run | #97 ✅ |
POST |
/api/admin/spiders/{name}/run |
Lanza el spider por nombre. 202 con el SpiderRun creado. 404 si no existe, 409 si ya está en ejecución |
#97 ✅ |
GET |
/api/admin/spiders/{name}/runs |
Historial de runs del spider ordenados por fecha descendente | #97 ✅ |
🛡️ Módulo 9 — ADMIN & MODERACIÓN
Issues: #80, #81, #82
Gestión de preguntas reportadas y administración de la plataforma.
Endpoints de moderación
| Método | Ruta | Descripción | Issues |
|---|---|---|---|
POST |
/api/questions/{id}/report |
Reportar una pregunta (PLAYER autenticado) | #100 ✅ |
GET |
/api/moderation/reports |
Lista de reportes, filtrable por ?status= (MODERATOR+) |
#100 ✅ |
PUT |
/api/moderation/reports/{id}/resolve |
Resolver reporte: DISMISS / EDIT_QUESTION / DELETE_QUESTION (MODERATOR+) | #100 ✅ |
Contrato de reporte (POST /api/questions/{id}/report)
Request:
{ "reason": "WRONG_ANSWER", "comment": "Texto opcional" }
reason: WRONG_ANSWER, OUTDATED, OFFENSIVE, OTHER
Response 201: ReportResponse — ver docs/moderation.md
Contrato de resolución (PUT /api/moderation/reports/{id}/resolve)
Request:
{ "action": "DELETE_QUESTION" }
action: DISMISS, EDIT_QUESTION, DELETE_QUESTION
Response 200: ReportResponse con status, resolvedBy, resolvedAt y action rellenos.
Auto-flagging
Cuando una pregunta acumula 5 reportes PENDING, su estado cambia automáticamente a FLAGGED y deja de servirse en partidas hasta que un moderador la revisa.
Ver documentación completa en
docs/moderation.md.
Endpoints de administración
| Método | Ruta | Descripción | Issues |
|---|---|---|---|
GET |
/api/admin/users |
Lista de usuarios | #82 |
PUT |
/api/admin/users/:id/role |
Cambiar rol de usuario | #82 |
DELETE |
/api/admin/users/:id |
Eliminar usuario | #82 |
PUT |
/api/admin/questions/:id/status |
Activar/desactivar pregunta | #82 |
🔗 Orden de implementación recomendado
Para evitar bloqueos, seguir este orden. Cada fase desbloquea la siguiente.
FASE 1 — Base (sin esto nada funciona)
✦ #41 Modelo de preguntas (DB)
✦ #39 Middleware JWT y Roles (Backend)
✦ #84 Login (Backend)
✦ #40 Guard + AuthInterceptor (Frontend)
✦ #44 Seed inicial de preguntas
FASE 2 — Juego en solitario
✦ #42 Endpoint pregunta aleatoria
✦ #53 Lógica partida binaria individual
✦ #55 Lógica de vidas
✦ #56 Endpoints partida binaria
✦ #57 Vista juego Supervivencia (Frontend)
✦ #59 Algoritmo daño/curación por precisión
✦ #60 Endpoints partida numérica
✦ #61 Vista juego Precisión (Frontend)
FASE 3 — Multijugador
✦ #63 WebSockets Backend
✦ #64 WebSockets Frontend
✦ #65 Sistema de salas
✦ #66 Matchmaking
✦ #67 Sincronización de preguntas
✦ #68 Lógica duelo binario
✦ #71 Lógica duelo de precisión
✦ #73 Lógica modo Versus (Sabotaje)
FASE 4 — Stats, ranking y scraping
✦ #76 Historial de partidas
✦ #77 Estadísticas personales
✦ #78 Ranking global
✦ #45 Pipeline Scrapy → PostgreSQL
✦ #46–#50 Spiders individuales
FASE 5 — Moderación y admin
✦ #80 Reporte de preguntas
✦ #81 Panel de moderador
✦ #82 Panel de administrador
🤝 Normas de coordinación
Contrato entre backend y frontend
- El backend define el contrato (URL, método, request, response) en este documento antes de implementarlo.
- El frontend no espera a que el backend esté listo: usa datos mock con la misma estructura del contrato.
- Si el contrato cambia, se actualiza este documento y se avisa al equipo.
Códigos de error estándar
Todos los errores siguen este formato:
{
"error": "UNAUTHORIZED",
"message": "Token expirado o inválido",
"status": 401
}
| Código | Cuándo usarlo |
|---|---|
400 |
Request malformado o datos inválidos |
401 |
No autenticado |
403 |
Autenticado pero sin permisos |
404 |
Recurso no encontrado |
409 |
Conflicto (usuario ya existe, sala llena...) |
500 |
Error interno del servidor |
Variables de entorno
# Backend (application.properties / .env)
DB_URL=jdbc:postgresql://localhost:5432/versus
DB_USER=versus
DB_PASS=versus
JWT_SECRET=cambiame_en_produccion
JWT_EXPIRY=900 # 15 minutos
JWT_REFRESH_EXPIRY=604800 # 7 días
# Frontend (environment.ts)
API_URL=http://localhost:8080/api
WS_URL=ws://localhost:8080/ws
Última actualización: 23-04-2026 — Si modificas algo del contrato de API, actualiza este documento.