Módulo: Juego singleplayer
Paquete raíz: com.versus.api.game
Depende de: match, questions, stats
Estado: ✅ implementado (Sprint 1-2)
Responsabilidad
Implementa los dos modos de juego para un solo jugador: Survival (preguntas BINARY, vidas) y Precision (preguntas NUMERIC, daño por desviación). Crea y gestiona la sesión de partida (Match + MatchPlayer) y delega al módulo stats al finalizar.
Modos de juego
Survival
- Preguntas de tipo
BINARY - 3 vidas iniciales (
SURVIVAL_INITIAL_LIVES = 3) - Acierto: +0 vidas, puntos =
50 * streak_actual - Fallo: −1 vida
- Fin: vidas = 0
Precision
- Preguntas de tipo
NUMERIC - 100 puntos de vida iniciales (
PRECISION_INITIAL_LIVES = 100) - Puntuación basada en desviación porcentual respecto al valor correcto
- Fallo total si la desviación supera el umbral definido (
tolerancePercent) - Fin: vidas ≤ 0
⚠️ TODO #59: La fórmula exacta de daño para Precision está pendiente de confirmación con el equipo. La implementación actual es aproximada.
Diagrama de clases
classDiagram
class SurvivalController {
<<RestController /api/game/survival>>
<<RequiresAuth>>
+POST /start : SurvivalStartResponse
+POST /answer : SurvivalAnswerResponse
}
class PrecisionController {
<<RestController /api/game/precision>>
<<RequiresAuth>>
+POST /start : PrecisionStartResponse
+POST /answer : PrecisionAnswerResponse
}
class GameService {
<<Service>>
-MatchRepository matchRepo
-MatchPlayerRepository playerRepo
-MatchRoundRepository roundRepo
-MatchAnswerRepository answerRepo
-QuestionService questionService
-StatsService statsService
+startSurvival(UUID userId) SurvivalStartResponse
+answerSurvival(UUID userId, SurvivalAnswerRequest) SurvivalAnswerResponse
+startPrecision(UUID userId) PrecisionStartResponse
+answerPrecision(UUID userId, PrecisionAnswerRequest) PrecisionAnswerResponse
-loadSession(UUID matchId, UUID userId) MatchPlayer
-createRound(UUID matchId, UUID questionId, int roundNum) MatchRound
-createAnswer(MatchRound, UUID userId, ...) MatchAnswer
-finishMatch(Match, MatchPlayer) void
-averageDeviation(UUID matchId, UUID userId) double
}
class SurvivalAnswerRequest {
<<DTO>>
+UUID matchId
+UUID questionId
+UUID selectedOptionId
}
class SurvivalAnswerResponse {
<<DTO>>
+boolean correct
+int lifeDelta
+int newLives
+int score
+int streak
+boolean gameOver
+QuestionResponse nextQuestion
}
class PrecisionAnswerRequest {
<<DTO>>
+UUID matchId
+UUID questionId
+double userValue
}
class PrecisionAnswerResponse {
<<DTO>>
+double deviation
+int lifeDelta
+int newLives
+double correctValue
+boolean gameOver
+QuestionResponse nextQuestion
}
SurvivalController --> GameService : delega
PrecisionController --> GameService : delega
GameService ..> SurvivalAnswerRequest : consume
GameService ..> SurvivalAnswerResponse : produce
GameService ..> PrecisionAnswerRequest : consume
GameService ..> PrecisionAnswerResponse : produce
Flujo de una partida Survival
sequenceDiagram
participant C as Cliente
participant SC as SurvivalController
participant GS as GameService
participant QS as QuestionService
participant DB as PostgreSQL
C->>SC: POST /api/game/survival/start
SC->>GS: startSurvival(userId)
GS->>DB: INSERT match (mode=SURVIVAL, status=IN_PROGRESS)
GS->>DB: INSERT match_player (lives=3, score=0, streak=0)
GS->>QS: findRandomActiveQuestion(BINARY)
GS->>DB: INSERT match_round (round=1, questionId)
GS-->>C: {matchId, sessionId, question (sin isCorrect)}
loop Mientras vidas > 0
C->>SC: POST /api/game/survival/answer {matchId, questionId, selectedOptionId}
SC->>GS: answerSurvival(userId, request)
GS->>DB: cargar MatchPlayer (validar ownership)
GS->>DB: cargar MatchRound actual
GS->>DB: cargar QuestionOption (verificar isCorrect)
alt Respuesta correcta
GS->>DB: UPDATE match_player (streak++, score += 50*streak)
GS->>DB: INSERT match_answer (isCorrect=true, lifeDelta=0)
else Respuesta incorrecta
GS->>DB: UPDATE match_player (lives--, streak=0)
GS->>DB: INSERT match_answer (isCorrect=false, lifeDelta=-1)
end
alt Vidas > 0
GS->>QS: findRandomActiveQuestion(BINARY)
GS->>DB: INSERT match_round (round++)
GS-->>C: {correct, lifeDelta, score, streak, nextQuestion}
else Vidas = 0
GS->>DB: UPDATE match (status=FINISHED, finishedAt)
GS->>StatsService: recordFinishedGame(userId, SURVIVAL, rounds, ...)
GS-->>C: {correct, lifeDelta, score, gameOver=true}
end
end
Cálculo de puntuación Survival
Acierto: score += 50 × streak_actual
streak++
lifeDelta = 0
Fallo: score += 0
streak = 0
lives--
lifeDelta = -1
Ejemplo de partida: [✓✓✓✗✓✓] → puntuaciones acumuladas: 50, 150, 300, 0, 50, 150
Cálculo de desviación Precision
desviación = |respuesta_usuario - correctValue| / correctValue × 100
Si desviación ≤ tolerancePercent (default 5%):
lifeDelta = 0 (acierto perfecto / dentro del margen)
Si desviación > tolerancePercent:
lifeDelta = -round(desviación) ← pendiente confirmación TODO#59
Endpoints
Survival
| Método | Ruta | Body | Respuesta |
|---|---|---|---|
POST |
/api/game/survival/start |
— | SurvivalStartResponse |
POST |
/api/game/survival/answer |
SurvivalAnswerRequest |
SurvivalAnswerResponse |
Precision
| Método | Ruta | Body | Respuesta |
|---|---|---|---|
POST |
/api/game/precision/start |
— | PrecisionStartResponse |
POST |
/api/game/precision/answer |
PrecisionAnswerRequest |
PrecisionAnswerResponse |
Errores comunes
| Situación | ErrorCode | HTTP |
|---|---|---|
| Match no encontrado o no pertenece al usuario | NOT_FOUND / FORBIDDEN |
404 / 403 |
| Partida ya finalizada | CONFLICT |
409 |
| No hay preguntas disponibles | NOT_FOUND |
404 |
Relación con otros módulos
graph LR
GS[GameService] -->|crea/actualiza| Match
GS -->|crea/actualiza| MatchPlayer
GS -->|crea| MatchRound
GS -->|crea| MatchAnswer
GS -->|obtiene preguntas| QuestionService
GS -->|registra estadísticas| StatsService
GameService es el único componente que escribe en Match* durante el juego singleplayer. El futuro multiplayer tendrá su propio service con WebSocket.
Extensión futura
- Los modos Binary Duel, Precision Duel y Sabotage usan las mismas entidades
Match*pero se orquestan mediante WebSocket (Sprint 3). - Añadir un endpoint
GET /api/game/{matchId}/summarypara historial de partida. - Considerar SSE (Server-Sent Events) para un timer server-side en modos con tiempo límite.