Backend Spring Boot para una aplicación de póker multijugador en tiempo real, con autenticación JWT, lógica de partidas/torneos, moderación y sistema de logros.
Autor: Marc Martín
Licencia: MIT
Última actualización: 30 de septiembre de 2025
Este proyecto representa el backend de una aplicación web y móvil de póker en línea, diseñado como proyecto de autoaprendizaje y desarrollo profesional. El objetivo es ofrecer una experiencia de póker realista y multijugador, con:
poker-backend/
├── admin/ # Código de uso de administrador
│ ├── controller/
│ ├── dto/
│ ├── service/
├── bot/ # Lógica de los bots de juego
├── chat/ # Lógica e implementación del chat
│ ├── controller/
│ ├── dto/
│ ├── service/
│ ├── repository/
│ └── model/
├── config/ # Seguridad (JWT), CORS, WebSocket
├── controller/ # Controladores REST
├── dto/ # DTOs de entrada/salida
├── estadisticas/ # Sistema de estadísticas
├── exception/ # Excepciones + GlobalExceptionHandler
├── logros/ # Sistema de logros
│ ├── controller/
│ ├── dto/
│ ├── service/
│ ├── repository/
│ ├── model/
│ └── LogroDataLoader.java
├── moderacion/ # Sistema de sanciones
│ ├── controller/
│ ├── dto/
│ ├── job/
│ ├── service/
│ ├── repository/
│ └── model/
├── torneo/ # Torneos + equipos + sala de espera
│ ├── controller/
│ ├── dto/
│ ├── equipos/
│ ├── equipos/
│ │ ├── controller/
│ │ ├── dto/
│ │ ├── service/
│ │ ├── repository/
│ │ ├── model/
│ ├── scheduler/
│ ├── service/
│ ├── repository/
│ ├── websocket/
│ └── model/
├── service/ # Lógica de negocio
│ ├── MesaService
│ ├── TurnoService
│ ├── BarajaService
│ ├── EvaluadorManoService
│ ├── BotService
│ └── ...
├── util/ # Filtrado de palabras
├── websocket/ # WebSocketService (eventos en tiempo real)
├── repository/ # Repositorios JPA
├── model/ # Entidades JPA
├── DataLoader.java
└── PokerBackendApplication.java
poker-frontend/ # (Futuro) Frontend React/Flutter
docs/ # Documentación Markdown (GitHub Pages)
├── index.md # Índice y selector de documentación
├── backend.md # Esta documentación
├── amigos.md # Documentación exclusiva para el sistema de amigos (aún no implementado)
└── frontend.md # Documentación para el frontend (aún no implementado)
./gradlew
)Configura estas variables en application.properties
:
# Base de datos
spring.datasource.url=jdbc:mysql://localhost:3306/pokerdb?useSSL=false&serverTimezone=UTC
spring.datasource.username=user
spring.datasource.password=pass
spring.jpa.hibernate.ddl-auto=update
# JWT
app.jwt.secret=super-secreto-cambiar-en-produccion
app.jwt.expiration=86400000
# Jobs programados
jobs.sanciones.delay=60000
docker run --name mysql_poker \
-e MYSQL_ROOT_PASSWORD=root \
-e MYSQL_DATABASE=pokerdb \
-e MYSQL_USER=user \
-e MYSQL_PASSWORD=pass \
-p 3306:3306 -d mysql:8.0
Verificar que el contenedor está ejecutándose:
docker ps
git clone https://github.com/Marukunai/poker_online.git
cd poker_online/poker-backend
Opción A: Línea de comandos
./gradlew bootRun
Opción B: Desde IDE
PokerBackendApplication.java
# Health check
GET http://localhost:8080/actuator/health
# Listar mesas
GET http://localhost:8080/api/mesas
Los usuarios se autentican mediante email y contraseña, obteniendo un token JWT que debe incluirse en todas las peticiones protegidas:
Authorization: Bearer {token}
Los endpoints administrativos están protegidos con:
@PreAuthorize("hasRole('ADMIN')")
GlobalExceptionHandler
unifica las respuestas de error en formato JSON:
{
"error": "Mensaje legible del error",
"status": 400,
"timestamp": "2025-09-30T10:40:47.4738402"
}
Excepción | Código HTTP | Descripción |
---|---|---|
ResourceNotFoundException |
404 | Recurso no encontrado |
UnauthorizedException |
401 | No autorizado |
AlreadyInactiveException |
400 | Recurso ya inactivo |
AlreadyHasAchievementException |
400 | Logro ya obtenido |
ActiveSanctionExistsException |
400 | Sanción activa existente |
Otras excepciones | 500 | Error interno del servidor |
Base URL:
http://localhost:8080
POST /api/auth/register
Content-Type: application/json
{
"username": "alice",
"email": "alice@email.com",
"password": "pass1234"
}
Respuesta:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
POST /api/auth/login
Content-Type: application/json
{
"email": "alice@email.com",
"password": "pass1234"
}
GET /api/auth/me
Authorization: Bearer {token}
GET /api/user/profile
Authorization: Bearer {token}
Incluye sanciones actuales y estadísticas.
PUT /api/user/me
Authorization: Bearer {token}
Content-Type: application/json
{
"username": "nuevoNombre",
"avatarUrl": "https://..."
}
POST /api/user/me/change-password
Authorization: Bearer {token}
Content-Type: application/json
{
"currentPassword": "pass1234",
"newPassword": "nuevaPass5678"
}
GET /api/user/ranking
GET /api/user/historial
Authorization: Bearer {token}
GET /api/user/public-profile/{userId}
GET /api/user/{userId}/resumen-completo
Incluye perfil + logros + historial de torneos + últimas 5 manos.
POST /api/turnos/iniciar/{mesaId}
Authorization: Bearer {token}
Inicializa la baraja, reparte cartas y prepara la primera ronda.
GET /api/turnos/actual/{mesaId}
POST /api/turnos/avanzar/{mesaId}
Authorization: Bearer {token}
POST /api/turnos/accion/{mesaId}?accion=RAISE&cantidad=50
Authorization: Bearer {token}
Acciones disponibles: FOLD
, CHECK
, CALL
, RAISE
, ALL_IN
POST /api/turnos/fase/{mesaId}/siguiente
Authorization: Bearer {token}
Cambia a la siguiente fase: Pre-Flop → Flop → Turn → River → Showdown
POST /api/mesas/privadas/crear
Authorization: Bearer {token}
Content-Type: application/json
{
"nombre": "Mesa de amigos",
"maxJugadores": 6,
"codigo": "AMIGOS123",
"fichasTemporales": 10000,
"smallBlind": 50,
"bigBlind": 100
}
Características:
POST /api/mesas/privadas/unirse
Content-Type: application/json
{
"email": "usuario@email.com",
"codigoAcceso": "AMIGOS123",
"fichasSolicitadas": 5000
}
POST /api/mesas/privadas/{codigo}/add-bot
Authorization: Bearer {token}
Restricciones:
maxJugadores
GET /api/torneos
GET /api/torneos/torneo?nombre=...
GET /api/torneos/pendientes
GET /api/torneos/encurso
GET /api/torneos/finalizados
GET /api/torneos/{id}
GET /api/torneos/{id}/estado
GET /api/torneos/{id}/nivel-ciegas
POST /api/torneos
Authorization: Bearer {token}
Content-Type: application/json
{
"nombre": "Torneo Mensual",
"buyIn": 1000,
"premio": 10000,
"maxJugadores": 50,
"tipoTorneo": "ELIMINACION_DIRECTA"
}
PATCH /api/torneos/{id}/estado?nuevoEstado=EN_CURSO
Authorization: Bearer {token}
DELETE /api/torneos/{id}
Authorization: Bearer {token}
POST /api/torneos/equipos
Authorization: Bearer {token}
Content-Type: application/json
{
"torneoId": 1,
"nombreEquipo": "Big Dogs",
"capitanId": 1
}
GET /api/torneos/equipos/torneo/{torneoId}
GET /api/torneos/equipos/{equipoId}
PUT /api/torneos/equipos/{equipoId}/puntos/{puntos}
Authorization: Bearer {token}
DELETE /api/torneos/equipos/{equipoId}
Authorization: Bearer {token}
PUT /api/torneos/equipos/actualizar-capitan
Authorization: Bearer {token}
Content-Type: application/json
{
"equipoId": 1,
"nuevoCapitanId": 4
}
Permisos: Solo capitán o admin pueden modificar.
GET /api/torneos/equipos/torneo/{torneoId}/ranking
GET /api/torneos/equipos/ranking/global
GET /api/torneos/equipos/ranking/anual/{year}
GET /api/torneos/equipos/ranking/mensual/{year}/{mes}
GET /api/torneos/equipos/{equipoId}/estadisticas
GET /api/torneos/equipos/{equipoId}/historial
# Añadir miembro
POST /api/torneos/equipos/miembros
Authorization: Bearer {token}
Content-Type: application/json
{
"equipoId": 1,
"userId": 4
}
# Listar miembros
GET /api/torneos/equipos/miembros/equipo/{equipoId}
# Ver miembro específico
GET /api/torneos/equipos/miembros/equipo/{equipoId}/user/{userId}
# Eliminar miembro
DELETE /api/torneos/equipos/miembros/{equipoId}/{miembroId}
Authorization: Bearer {token}
# Eliminar todos los miembros
DELETE /api/torneos/equipos/miembros/equipo/{equipoId}
Authorization: Bearer {token}
# Registrarse
POST /api/torneos/espera/registrar?torneoId=1&userId=4
Authorization: Bearer {token}
# Ver participantes
GET /api/torneos/espera/{torneoId}
# Limpiar sala
DELETE /api/torneos/espera/{torneoId}
Authorization: Bearer {token}
GET /api/logros
POST /api/logros/otorgar?userId=1&nombreLogro=Bluff Master
Authorization: Bearer {token}
Respuesta:
{
"message": "Logro otorgado correctamente"
}
GET /api/logros/usuario/{userId}
Incluye flags obtenido
y fechaObtencion
.
POST /api/logros/usuario/{userId}/asignar/{logroId}
Authorization: Bearer {token}
DELETE /api/logros/usuario/{userId}/eliminar/{logroId}
Authorization: Bearer {token}
Nota: Si el usuario ya tiene el logro, se lanza AlreadyHasAchievementException
→ 400.
POST /api/admin/sanciones/aplicar
Authorization: Bearer {token}
Content-Type: application/x-www-form-urlencoded
userId=5&motivo=COMPORTAMIENTO_TOXICO&tipo=SUSPENSION_TEMPORAL&descripcion=Lenguaje ofensivo&diasDuracion=7
Tipos de sanción:
BLOQUEO_CUENTA
SUSPENSION_TEMPORAL
SUSPENSION_PERMANENTE
PROHIBICION_CHAT
Prevención de duplicados: Si ya existe una sanción activa equivalente, lanza ActiveSanctionExistsException
→ 400.
POST /api/sanciones
Authorization: Bearer {token}
Content-Type: application/json
{
"userId": 5,
"tipo": "SUSPENSION_TEMPORAL",
"motivo": "COMPORTAMIENTO_TOXICO_EN_TORNEOS",
"descripcion": "Lenguaje tóxico reiterado durante el torneo",
"fechaFin": "2025-10-07T11:40:00Z"
}
GET /api/sanciones/usuario/{userId}
DELETE /api/sanciones/{sancionId}
Authorization: Bearer {token}
Nota: Si ya está inactiva, lanza AlreadyInactiveException
→ 400.
# Estadísticas de usuario
GET /api/estadisticas/usuario/{id}
# Ranking global
GET /api/estadisticas/ranking/global
# Ranking mensual
GET /api/estadisticas/ranking/mensual
# Historial de torneos
GET /api/torneos/usuario/{userId}/historial
# Unirse como espectador
POST /api/mesa/espectadores/{mesaId}/unirse
Authorization: Bearer {token}
# Salir de modo espectador
DELETE /api/mesa/espectadores/{mesaId}/salir
Authorization: Bearer {token}
# Listar espectadores
GET /api/mesa/espectadores/{mesaId}
# Ver datos de la mesa (espectador)
GET /api/mesa/espectadores/{mesaId}/datos
Restricciones:
Los roles rotan automáticamente en cada nueva mano.
User.fichas
: Fichas globales del usuarioUserMesa.fichasEnMesa
: Fichas activas en la mesa actualUserMesa.totalApostado
: Lo apostado en la partida actualMesa.pot
: Bote total en juegoCuando un jugador hace all-in con menos fichas que otros, se crean side pots para repartir proporcionalmente entre los jugadores elegibles.
INACTIVIDAD_EN_PARTIDAS
(advertencia)Las sanciones activas tipo BLOQUEO_CUENTA
, SUSPENSION_TEMPORAL
o SUSPENSION_PERMANENTE
impiden:
Tres sanciones de estilo ADVERTENCIA
conforman una sanción grave que deriva en SUSPENSION_TEMPORAL
o PROHIBICION_CHAT
dependiendo de los motivos de la sanción.
La sanción de PROHIBICION_CHAT
, como su nombre indica, impide el uso del chat hasta la finalización de la sanción (automáticamente a 1 día)
Evento | Descripción |
---|---|
turno |
Notifica de quién es el turno actual |
fase |
Cambio de fase + cartas comunitarias reveladas |
accion |
Acción realizada por un jugador (fold, call, raise…) |
bot_actuando |
Indica que un bot está procesando su turno |
showdown |
Revela ganadores, manos ganadoras y cartas |
sancion |
Notificación personalizada de sanción al jugador |
chat_bot |
Mensaje simulado enviado por un bot |
const socket = new SockJS('http://localhost:8080/ws');
const stompClient = Stomp.over(socket);
stompClient.connect({}, () => {
stompClient.subscribe('/topic/mesa/1', (message) => {
console.log(JSON.parse(message.body));
});
});
Los bots son jugadores controlados por IA con comportamiento realista y estratégico.
User
con esIA = true
CPU-42
User bot = User.builder()
.email("cpu...@bot.com")
.username("CPU-XX")
.esIA(true)
.nivelBot(DificultadBot.NORMAL)
.estiloBot(EstiloBot.AGRESIVO)
.build();
Nivel | Bluff | Slowplay | Evaluación contextual |
---|---|---|---|
FACIL | ❌ | ❌ | Decisiones simples |
NORMAL | ⚠️ | ❌ | Usa draws y cartas conectadas |
DIFICIL | ✅ | ✅ | Analiza fuerza + faroles estratégicos |
Estilo | Agresividad | Comportamiento |
---|---|---|
AGRESIVO | Alto (1.4x) | Muchos raise y all-in |
CONSERVADOR | Bajo (0.7x) | Cauto, se retira con facilidad |
LOOSE | Medio (1.2x) | Juega muchas manos |
TIGHT | Medio (0.8x) | Solo juega manos fuertes |
DEFAULT | 1.0x | Equilibrado |
Los bots evalúan:
Los bots envían frases contextuales vía WebSocket:
{
"tipo": "chat_bot",
"jugador": "CPU-42",
"mensaje": "¡A ver si aguantas esta!"
}
Gestionado por el enum FrasesBotChat.java
.
maxJugadores
Torneo
: Configuración generalParticipanteTorneo
: InscripcionesTorneoMesa
: Mesas del torneoBlindLevel
: Niveles de ciegasEquipoTorneo
: Equipos participantesMiembroEquipoTorneo
: Miembros de equiposEl sistema TorneoScheduler
(@Scheduled):
Más de 50 logros clasificados por categoría, otorgados automáticamente desde los servicios.
ESTRATEGIA
TORNEOS
CONTRA_BOTS
PARTIDAS_SIMPLES
ACCIONES_ESPECIALES
EQUIPO
Nombre | Categoría | Condición |
---|---|---|
All-In Maniaco | ESTRATEGIA | Hacer All-In 50 veces |
Bluff Maestro | ESTRATEGIA | Hacer farol en Flop y ganar |
Sin Fichas | ESTRATEGIA | Quedarse sin fichas globales |
Superviviente | ESTRATEGIA | Ganar con <5% de fichas |
Comeback | ACCIONES_ESPECIALES | Ganar mano con <10% de fichas iniciales |
Derrotador de Máquinas | CONTRA_BOTS | Ganar 10 partidas contra bots |
Victoria Privada | PARTIDAS_SIMPLES | Ganar una partida privada |
Jugador Rico | ACCIONES_ESPECIALES | Alcanzar 100K fichas globales |
Millonario | ACCIONES_ESPECIALES | Alcanzar 1M fichas globales |
Subidón | ACCIONES_ESPECIALES | Ganar 20K fichas en una partida |
Clasificado Pro | TORNEOS | Clasificarse en un torneo |
Jugador en equipo | EQUIPO | Participar en torneo por equipos |
Campeón por Equipos | EQUIPO | Ganar torneo por equipos |
Equipo o familia? | EQUIPO | Ganar 3 torneos con mismo equipo |
Arrasador en Equipo | EQUIPO | Ganar 3 torneos con cualquier equipo |
Capitán Estratégico | EQUIPO | Ser capitán y ganar |
Todos a una | EQUIPO | Todo el equipo clasifica a final |
Los logros se otorgan desde servicios mediante:
logroService.otorgarLogroSiNoTiene(userId, "NOMBRE_LOGRO");
Iconos asociados en /files/images/logros/
Las sanciones se sincronizan automáticamente con flags del usuario:
PROHIBICION_CHAT
activa → user.chatBloqueado = true
BLOQUEO_CUENTA
/ SUSPENSION_*
activa → user.bloqueado = true
SancionExpiryJob
ejecuta cada minuto:
chatBloqueado
/ bloqueado
COMPORTAMIENTO_TOXICO
COMPORTAMIENTO_TOXICO_EN_TORNEOS
INACTIVIDAD_EN_PARTIDAS
TRAMPA_DETECTADA
ABUSO_DE_SISTEMA
El sistema registra automáticamente el progreso mensual de cada usuario para análisis temporal.
fichas
: Saldo global del usuariobloqueado
: Flag de cuenta bloqueadachatBloqueado
: Flag de chat bloqueadoesIA
: Marca si es un botnivelBot
: Dificultad del botestiloBot
: Estilo de juego del botpot
: Bote total acumuladofase
: Fase actual (PRE_FLOP, FLOP, TURN, RIVER, SHOWDOWN)esPrivada
: Indica si es mesa privadacodigoAcceso
: Código para mesas privadascartasComunitarias
: Cartas reveladas en la mesafichasEnMesa
: Fichas activas en la mesatotalApostado
: Total apostado en la partida actualactivo
: Si el jugador sigue en la manoesSB/esBB/esDealer
: Roles asignadosactivo
: Si el turno está activofechaInicio
: Timestamp de inicioaccionRealizada
: Acción ejecutadacartasPrivadas
: Cartas del jugador (JSON)manoFinal
: Descripción de la mano ganadorafase
: Fase en que se decidiófichasGanadas
: Cantidad ganadafechaPartida
: TimestamptipoAccion
: FOLD, CHECK, CALL, RAISE, ALL_INcantidad
: Monto apostadotimestamp
: Momento de la acciónactivo
: Si la sanción está vigentetipo
: Tipo de sanciónmotivo
: Razón de la sanciónfechaInicio
/ fechaFin
: Período de vigenciadescripcion
: Detalles adicionalesfechaObtencion
: Timestamp de concesiónUser
y Logro
User.fichas
se actualiza solo al finalizar partidas no privadasUserMesa.fichasEnMesa
es independiente y se elimina al salirHistorialMano
registra cada showdown para estadísticasAccionPartida
permite reconstruir partidas completas@Scheduled(fixedDelayString = "${jobs.sanciones.delay:60000}")
Funciones:
fechaFin
pasada)chatBloqueado
: false si no hay PROHIBICION_CHAT
activasbloqueado
: false si no hay bloqueos/suspensiones activas@Scheduled(fixedRate = 300000) // Cada 5 minutos
Funciones:
fechaInicio
ha llegadoEN_CURSO
/api/auth/me
/api/user/me
/api/user/me/change-password
{
"baseURL": "http://localhost:8080",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
POST /api/auth/register
POST /api/auth/login
POST /api/mesas/privadas/crear
POST /api/mesas/privadas/CODIGO123/add-bot
POST /api/mesas/privadas/unirse
POST /api/turnos/iniciar/1
POST /api/turnos/accion/1?accion=RAISE&cantidad=100
GET /api/logros/usuario/1
# Base de datos
spring.datasource.url=${DATABASE_URL}
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}
# JWT
app.jwt.secret=${JWT_SECRET}
app.jwt.expiration=${JWT_EXPIRATION:86400000}
# CORS
cors.allowed.origins=${FRONTEND_URL}
# Jobs
jobs.sanciones.delay=${SANCION_JOB_DELAY:60000}
app.jwt.secret
por valor seguro aleatorioUser
, Mesa
, Turno
)version: '3.8'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: pokerdb
MYSQL_USER: user
MYSQL_PASSWORD: pass
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
backend:
build: ./poker-backend
ports:
- "8080:8080"
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/pokerdb
SPRING_DATASOURCE_USERNAME: user
SPRING_DATASOURCE_PASSWORD: pass
depends_on:
- mysql
volumes:
mysql_data:
git checkout -b feature/nueva-funcionalidad
)git commit -am 'Añadir nueva funcionalidad'
)git push origin feature/nueva-funcionalidad
)Abre un Issue en GitHub incluyendo:
MIT License
Copyright (c) 2025 Marc Martín
Se concede permiso, de forma gratuita, a cualquier persona que obtenga una copia de este software y archivos de documentación asociados (el “Software”), para utilizar el Software sin restricciones, incluyendo sin limitación los derechos a usar, copiar, modificar, fusionar, publicar, distribuir, sublicenciar, y/o vender copias del Software, y a permitir a las personas a las que se les proporcione el Software a hacer lo mismo, sujeto a las siguientes condiciones:
El aviso de copyright anterior y este aviso de permiso se incluirán en todas las copias o porciones sustanciales del Software.
EL SOFTWARE SE PROPORCIONA “TAL CUAL”, SIN GARANTÍA DE NINGÚN TIPO, EXPRESA O IMPLÍCITA, INCLUYENDO PERO NO LIMITADO A GARANTÍAS DE COMERCIALIZACIÓN, IDONEIDAD PARA UN PROPÓSITO PARTICULAR Y NO INFRACCIÓN. EN NINGÚN CASO LOS AUTORES O TITULARES DEL COPYRIGHT SERÁN RESPONSABLES DE NINGUNA RECLAMACIÓN, DAÑOS U OTRAS RESPONSABILIDADES, YA SEA EN UNA ACCIÓN DE CONTRATO, AGRAVIO O CUALQUIER OTRO MOTIVO, QUE SURJA DE O EN CONEXIÓN CON EL SOFTWARE O EL USO U OTRO TIPO DE ACCIONES EN EL SOFTWARE.
Desarrollador principal: Marc Martín
Twitter/X: @marukunai_03
GitHub: Marukunai
Repositorio: poker_online
Sí, bajo licencia MIT puedes usar, modificar y distribuir este software comercialmente, siempre que mantengas el aviso de copyright.
Por favor, NO abras un Issue público. Contacta directamente al desarrollador vía GitHub o email privado.
¡Absolutamente! El código está diseñado para ser extensible. Recomendamos crear nuevas clases de servicio que hereden de BasePokerService
.
Actualmente el límite es de 8 jugadores por mesa (estándar de Texas Hold’em), pero es configurable en Mesa.maxJugadores
.
No, los bots solo están disponibles en mesas privadas para practicar o jugar con amigos.
Los rankings se basan en múltiples métricas: partidas ganadas, fichas ganadas, puntos de torneo y rendimiento mensual. Cada ranking tiene su propio algoritmo de ordenación.
Sí, los torneos pueden configurarse como privados mediante flags adicionales (funcionalidad en desarrollo).
El sistema marca al jugador como inactivo y realiza FOLD automático en sus turnos. Al reconectar, puede volver a unirse si la partida sigue activa.
¡Gracias por usar Poker Online Backend!
Si encuentras útil este proyecto, considera darle una ⭐ en GitHub.
Última actualización: 30 de septiembre de 2025