BEST PRACTICES — Codemon TCG
Convenciones de Código
- Backend: Java 21 + Spring Boot 3, Lombok solo cuando simplifica, inyección por constructor (nunca
@Autowireden campo),@Transactionalexplícito en servicios que modifican datos. - Frontend: Angular Standalone Components, TypeScript
strict: true, limpiar suscripciones contakeUntil(destroy$)enngOnDestroy, Tailwind-first (prohibido CSS custom salvo@apply). - Naming:
camelCaseen Java y TS,kebab-caseen archivos Angular,UPPER_SNAKEpara constantes. PrefijoIprohibido en interfaces TS. - Logs: SLF4J con nivel apropiado; nunca
System.out.println; incluir contexto (gameId,userId) en cada log del motor.
Convenciones de Commits
- Formato:
tipo(scope): descripción en inglés, imperativo, <72 chars. - Tipos válidos:
feat,fix,test,refactor,docs,chore,perf. - Scopes del proyecto:
auth,cards,game,lobby,payment,infra,engine. - Ejemplos:
feat(auth): add JWT refresh endpoint·fix(engine): prevent double attack in same phase·test(engine): add XY1 card handler coverage·perf(lobby): cache leaderboard in Redis. - Breaking changes: agregar
!después del scope y pie de mensajeBREAKING CHANGE: descripción.
Patrones de Diseño del Proyecto
- Facade —
GameEngineexpone una API única; los subsistemas internos no son accesibles directamente desde controllers. - Strategy —
CardHandler: cada carta tiene su handler registrado en un mapa; agregar una carta nueva = agregar un handler, sin tocar el motor. - Chain of Responsibility —
AttackPipeline: 9 pasos obligatorios en orden fijo (Validar → Daño Base → Debilidad → Resistencia → Status → Aplicar → KO → Premio → Evento). Ningún paso puede saltarse. - State —
GamePhaseenum controla qué acciones son legales en cada fase del turno. Una acción en la fase incorrecta lanzaIllegalGameActionException. - Repository — Spring Data JPA; nunca SQL nativo salvo en queries analíticas justificadas con
@Query. - Observer / Pub-Sub —
GameEventPublisherdesacopla el motor de los WebSockets; el engine no sabe que existe STOMP.
Flujo de Pull Request
- Crear rama
feature/PASO_SXX_YY-descripciondesdedevelopactualizado. - Ejecutar
./verify_paso.sh PASO_SXX_YY— si falla, no abrir el PR. - Correr
mvn verify: todos los tests deben pasar y cobertura ≥ umbral definido en pom.xml. - Referenciar en el título del PR:
[PASO_SXX_YY] HU-XXX: descripción. - Pedir al menos 1 review del equipo correspondiente (A=Backend, B=Frontend, C=DevOps).
- Mergear con Squash & Merge; nunca merge commit ni rebase a develop directamente.
- Eliminar la rama feature después del merge.
Sistema de PASOS — Reglas de Oro
- Siempre cargar como contexto:
CONVENCIONES.md,GLOSARIO.md,ESTADO_PASOS.mdyHISTORIAL_PASOS.mdantes de leer el PASO. - Leer el PASO completo antes de escribir una sola línea de código.
- Respetar los contratos de API definidos en
CONTRATOS_API.md; un cambio de firma requiere actualizar el contrato primero. - Ejecutar
./verify_paso.sh PASO_X_Yal terminar; la salida define si el PASO está DONE. - Revisar el Definition of Done en
DOD.md; no marcar como completo si algún criterio falla. - Actualizar
ESTADO_PASOS.mdeHISTORIAL_PASOS.mdantes de cerrar la sesión de trabajo.
Estrategia de Testing
- Pirámide: 70% unitarios (JUnit + Mockito), 20% integración (Testcontainers), 10% E2E (Playwright).
- Cobertura mínima: 80% global · 90% en
com.codemon.game.engine· 0% tolerancia de regresiones en el pipeline de ataque. - Tests unitarios del motor: patrón Given/When/Then estricto; un test por acción legal y uno por acción ilegal en cada fase.
- Testcontainers: obligatorio para repositorios que usan JSONB; nunca mockear la BD en tests de integración.
- Playwright E2E: dos contextos de Chromium simultáneos para simular PvP real; ejecutar en CI con
--reporter=html. - No testear: getters/setters triviales, Lombok generado, configuración de beans de Spring.
Manejo de Errores & Logging
- @ControllerAdvice centraliza todos los errores REST; nunca capturar excepciones en controllers y retornar
ResponseEntitymanualmente. - Formato de error estándar:
ProblemDetail(RFC 9457) con campostype,title,status,detail,instance. - Excepciones de dominio propias:
IllegalGameActionException,CardNotFoundException,DeckValidationException; nunca lanzarRuntimeExceptiongenérico. - Logging:
ERRORsolo para fallos irrecuperables;WARNpara situaciones anómalas esperadas;INFOpara flujos de negocio;DEBUGpara trazas del motor de juego. - Incluir siempre
gameIdyuserIden los logs del motor para poder correlacionar una partida completa en Grafana.
Base de Datos & Migraciones
- Regla cardinal de Flyway: nunca modificar un script
V{n}__*.sqlya ejecutado; crear siempre un nuevo script con el siguiente número. - Todo cambio de schema pasa por migración Flyway; prohibido alterar tablas a mano en cualquier entorno.
- Usar
@Transactional(isolation = SERIALIZABLE)en operaciones críticas del motor (procesar acción, acreditar moneda). - Evitar N+1: usar
@EntityGraphoJOIN FETCHexplícito en queries que navegan relaciones@ManyToMany. - El estado de partida se persiste como JSONB en
game_state; usar operadores@>y->>, nunca deserializar todo el objeto para filtrar. REFRESH MATERIALIZED VIEW CONCURRENTLY mv_leaderboardsolo al terminar una partida, nunca dentro de una transacción abierta.
WebSockets & Tiempo Real
- Toda suscripción STOMP en Angular debe vivir dentro de un
takeUntil(this.destroy$); un leak de suscripción puede dejar al jugador recibiendo eventos de una partida ya terminada. - El backend NO publica directamente a WebSocket desde
GameEngine; siempre a través deGameEventPublisherpara mantener el motor testeable. - Heartbeat cada 30 segundos (
user:presence:{userId}con TTL 30s en Redis); si el TTL expira, el jugador se considera desconectado y la partida se resuelve por abandono. - Mensajes de partida van a
/topic/game.{gameId}(ambos jugadores); mensajes privados a/user/queue/events. Nunca mezclar canales. - El proxy Nginx debe tener
proxy_set_header Upgrade $http_upgradeyConnection "upgrade"; sin esto el handshake WS falla silenciosamente.
Seguridad
- JWT: secret de mínimo 32 caracteres en
.env(nunca enapplication.yml); expiración de access token 15 min, refresh token 7 días con rotación. - Secrets: ninguna credencial, token ni password puede estar commiteado en el repositorio;
.envestá en.gitignore; usar.env.examplecon valores placeholder. - CORS: configurar orígenes permitidos explícitamente en
SecurityFilterChain; nuncaallowedOrigins("*")en producción. - Idempotencia de pagos: el webhook de Mercado Pago puede llegar varias veces; la acreditación de Codemones se guarda con clave de idempotencia y se ignora si ya fue procesada.
- Inputs: validar con
@Valid+ Bean Validation en todos los DTOs de entrada; rechazar con 400 antes de llegar al servicio. - OAuth2: al vincular cuentas sociales, buscar siempre por email antes de crear un usuario nuevo para evitar cuentas duplicadas.
Performance & Caché
- El leaderboard top-100 se lee de Redis (
ZREVRANGE leaderboard 0 99), nunca de PostgreSQL en tiempo de request. - Cooldown de 24h para abrir sobres: clave
cooldown:pack:{userId}conEXPIRE 86400en Redis; no usar la BD para esto. - Las imágenes de cartas se sirven desde MinIO con política de bucket ReadOnly; nunca almacenar imágenes en la BD ni en el filesystem del servidor.
- Lazy loading en Angular: cada ruta carga su módulo con
loadComponent; el bundle inicial debe quedar bajo 200 KB gzipped. - Virtual Threads habilitados (
spring.threads.virtual.enabled=true); permite manejar 50+ partidas concurrentes sin saturar el thread pool. - Usar
@Asyncpara el envío de emails OTP; el endpoint de registro no debe bloquearse esperando respuesta del servidor SMTP.
Variables de Entorno & Secretos — Guía Completa
¿Qué es una variable de entorno? Es un par CLAVE=valor que el sistema operativo inyecta al proceso cuando arranca. Tu código las lee en tiempo de ejecución — nunca están escritas en el código fuente.
¿Qué es el archivo .env? Una convención de desarrollo local. Herramientas como Docker Compose y Spring Boot lo leen al arrancar para simular las variables del entorno. Este archivo nunca debe existir en producción ni commitearse al repositorio.
- Regla #1 — .env va en .gitignore siempre. El historial de git es permanente. Un secret commiteado hoy sigue siendo accesible en el historial aunque lo borres mañana. Si accidentalmente se commitea, el secret debe rotarse de inmediato.
- Regla #2 — Usa .env.example como documentación. Este archivo SÍ se commitea, con los nombres de las variables pero sin valores reales. Cada developer nuevo copia
.env.example → .envy completa sus valores locales. - Regla #3 — En producción no existe el archivo .env. Las variables se inyectan desde: CI/CD secrets (GitHub Actions, GitLab CI), servicios administrados de secretos (AWS SSM Parameter Store, AWS Secrets Manager, HashiCorp Vault, Azure Key Vault), o variables de entorno del sistema operativo del servidor.
- Regla #4 — Perfiles por entorno.
spring.profiles.active=deven local,=proden producción. El perfil prod puede tener configuración más restrictiva (sin Swagger, sin datos de prueba, logs en formato JSON para Grafana). - Regla #5 — Rotación de secretos. Cada vez que un secret es expuesto (leak en logs, repo público, developer que deja el equipo), debe generarse uno nuevo y actualizarse en todos los entornos. JWT_SECRET rotado invalida todas las sesiones activas — planificar el rollout.
- Regla #6 — Nunca loguear variables de entorno.
log.info("Conectando con clave: " + jwtSecret)es un error clásico. Los logs van a Grafana, que puede ser accedido por más personas que el código fuente.
Escalera de madurez en producción: .env local → Variables de CI/CD → Docker/K8s Secrets → AWS SSM → HashiCorp Vault (con rotación automática y auditoría)
Reglas del Motor de Juego
- Nunca modificar
GameStatedirectamente desde fuera deGameEngine; toda acción pasa porprocessAction(). - El
AttackPipelinetiene 9 pasos en orden fijo; saltarse uno o reordenarlos produce resultados de daño incorrectos en cartas con resistencias/debilidades. - Cada
CardHandlerdebe registrarse en el mapa del motor en su constructor; un handler no registrado hace que la carta se comporte como si no tuviera efecto. - Las condiciones de estado (veneno, quemadura, confusión, parálisis) se aplican en
END_PHASE, no enATTACK_PHASE; aplicarlas en el momento incorrecto crea exploits competitivos. - Las 6 cartas de premio se asignan al inicio (
SETUP) y se revelan solo cuando el oponente hace KO; nunca antes. - El mulligan es obligatorio si la mano inicial no tiene ningún Pokémon Básico; el motor debe forzarlo antes de pasar a
DRAW_PHASE.