docs: añade documentación completa del proyecto

- README.md: descripción general, stack, instalación rápida
- docs/API.md: referencia completa de API REST y WebSocket
- docs/ARCHITECTURE.md: arquitectura del sistema con diagramas
- docs/INSTALLATION.md: guía detallada de instalación
- backend/.env.example: plantilla de configuración

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-26 23:50:34 +00:00
parent ab201e113a
commit 6248037b47
5 changed files with 1724 additions and 0 deletions

344
README.md Normal file
View File

@@ -0,0 +1,344 @@
# Trivy - Trivia Multiplayer en Tiempo Real
<p align="center">
<img src="docs/logo.png" alt="Trivy Logo" width="200">
</p>
**Trivy** es un juego de trivia multijugador en tiempo real inspirado en Jeopardy. Dos equipos compiten respondiendo preguntas de diferentes categorías, con mecánicas de robo y validación de respuestas por IA.
## Características
- **Multijugador en tiempo real** - Hasta 8 jugadores (2 equipos × 4 jugadores)
- **8 categorías temáticas** - Nintendo, Xbox, PlayStation, Anime, Música, Películas, Libros, Historia
- **5 categorías por partida** - Rotación aleatoria entre partidas
- **Pool de 200 preguntas** - 5 opciones por cada categoría/dificultad
- **Validación de respuestas por IA** - Claude API para respuestas flexibles
- **Sistema de robo** - Oportunidad de robar puntos cuando el equipo contrario falla
- **Múltiples temas visuales** - DRRR, Retro Arcade, Minimal, Gaming RGB, Anime 90s
- **Efectos de sonido** - Sonidos personalizados por tema
- **Chat de equipo** - Comunicación privada entre compañeros
- **Reacciones emoji** - Expresiones en tiempo real
- **Panel de administración** - Gestión de preguntas, categorías y monitoreo
## Stack Tecnológico
### Backend
- **FastAPI** - Framework web async de alto rendimiento
- **Python-SocketIO** - WebSockets para comunicación en tiempo real
- **PostgreSQL** - Base de datos principal
- **Redis** - Estado de salas y sesiones en tiempo real
- **SQLAlchemy** - ORM async
- **Anthropic Claude API** - Validación inteligente de respuestas
### Frontend
- **React 18** - UI declarativa
- **TypeScript** - Tipado estático
- **Vite** - Build tool rápido
- **Zustand** - Estado global ligero
- **Framer Motion** - Animaciones fluidas
- **Tailwind CSS** - Estilos utilitarios
- **Socket.IO Client** - Comunicación WebSocket
- **Howler.js** - Gestión de audio
## Requisitos
- Python 3.11+
- Node.js 18+
- PostgreSQL 15+
- Redis 7+
- API Key de Anthropic (Claude)
## Instalación
### 1. Clonar repositorio
```bash
git clone https://gitea.local/frank/Trivy.git
cd Trivy
```
### 2. Configurar Backend
```bash
cd backend
# Crear entorno virtual
python -m venv venv
source venv/bin/activate # Linux/Mac
# o: venv\Scripts\activate # Windows
# Instalar dependencias
pip install -r requirements.txt
# Configurar variables de entorno
cp .env.example .env
# Editar .env con tus credenciales
```
### 3. Configurar Frontend
```bash
cd frontend
# Instalar dependencias
npm install
# Configurar variables de entorno
cp .env.example .env
# Editar .env con la URL del backend
```
### 4. Base de datos
```bash
# Con Docker
docker-compose up -d db redis
# O configurar PostgreSQL y Redis manualmente
# Crear base de datos: trivy
```
### 5. Migraciones
```bash
cd backend
alembic upgrade head
```
### 6. Datos iniciales
```bash
# Crear usuario admin
python -c "
from app.models.base import get_async_session
from app.models.admin import Admin
import asyncio
async def create_admin():
async with get_async_session()() as db:
admin = Admin(username='admin')
admin.set_password('admin123')
db.add(admin)
await db.commit()
asyncio.run(create_admin())
"
# Crear categorías y preguntas iniciales
python scripts/seed_data.py
```
## Ejecución
### Desarrollo
```bash
# Terminal 1 - Backend
cd backend
source venv/bin/activate
uvicorn app.main:socket_app --host 0.0.0.0 --port 8000 --reload
# Terminal 2 - Frontend
cd frontend
npm run dev -- --host
```
### Producción
```bash
# Backend con Gunicorn
gunicorn app.main:socket_app -w 4 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000
# Frontend build
cd frontend
npm run build
# Servir dist/ con nginx o similar
```
## Configuración
### Variables de entorno - Backend (.env)
```env
# Base de datos
DATABASE_URL=postgresql+asyncpg://user:pass@localhost:5432/trivy
# Redis
REDIS_URL=redis://localhost:6379
# Anthropic API
ANTHROPIC_API_KEY=sk-ant-...
# Configuración del juego
STEAL_PENALTY_MULTIPLIER=0.5
STEAL_TIME_MULTIPLIER=0.5
ROOM_TTL_HOURS=3
```
### Variables de entorno - Frontend (.env)
```env
VITE_API_URL=http://localhost:8000
VITE_WS_URL=http://localhost:8000
```
## Arquitectura
```
Trivy/
├── backend/
│ ├── app/
│ │ ├── api/ # Endpoints REST
│ │ │ ├── admin.py # Panel de administración
│ │ │ ├── auth.py # Autenticación
│ │ │ └── game.py # Datos del juego
│ │ ├── models/ # Modelos SQLAlchemy
│ │ ├── schemas/ # Schemas Pydantic
│ │ ├── services/ # Lógica de negocio
│ │ │ ├── room_manager.py # Gestión de salas (Redis)
│ │ │ ├── game_manager.py # Lógica del juego
│ │ │ ├── ai_validator.py # Validación con Claude
│ │ │ └── question_service.py # Carga de preguntas
│ │ ├── sockets/ # Eventos WebSocket
│ │ │ └── game_events.py
│ │ ├── config.py # Configuración
│ │ └── main.py # Aplicación ASGI
│ ├── alembic/ # Migraciones
│ └── requirements.txt
├── frontend/
│ ├── src/
│ │ ├── components/ # Componentes React
│ │ │ ├── chat/ # Chat y reacciones
│ │ │ ├── game/ # Componentes del juego
│ │ │ ├── lobby/ # Sala de espera
│ │ │ └── ui/ # Componentes UI genéricos
│ │ ├── hooks/ # Custom hooks
│ │ │ ├── useSocket.ts
│ │ │ └── useSound.ts
│ │ ├── pages/ # Páginas/rutas
│ │ ├── services/ # Servicios (socket)
│ │ ├── stores/ # Estado Zustand
│ │ ├── themes/ # Temas visuales
│ │ └── types/ # Tipos TypeScript
│ ├── public/
│ │ └── sounds/ # Archivos de audio
│ └── package.json
└── docs/ # Documentación
```
## Flujo del Juego
```
1. CREAR SALA
Host → create_room → Código de 6 caracteres
2. UNIRSE
Jugadores → join_room(código) → Asignación a equipo A/B
3. INICIAR
Host → start_game → Backend carga 5 categorías aleatorias
4. TURNO
Jugador actual → Selecciona pregunta del tablero
5. RESPONDER
30 segundos para escribir respuesta
Claude valida si es correcta
6. ROBO (si falla)
Equipo contrario puede intentar robar
Si falla el robo, pierde puntos
7. SIGUIENTE TURNO
Rotación de jugadores y equipos
8. FIN
Cuando todas las preguntas están contestadas
Equipo con más puntos gana
```
## API REST
### Autenticación
- `POST /api/auth/login` - Login admin
- `POST /api/auth/logout` - Logout
### Juego
- `GET /api/game/categories` - Lista de categorías
- `GET /api/game/today-questions` - Preguntas del día
### Admin
- `GET /api/admin/questions` - Listar preguntas
- `POST /api/admin/questions` - Crear pregunta
- `PUT /api/admin/questions/{id}` - Actualizar pregunta
- `DELETE /api/admin/questions/{id}` - Eliminar pregunta
- `GET /api/admin/categories` - Listar categorías
- `GET /api/admin/rooms/active` - Salas activas
## Eventos WebSocket
### Cliente → Servidor
- `create_room` - Crear nueva sala
- `join_room` - Unirse a sala existente
- `start_game` - Iniciar partida (solo host)
- `select_question` - Seleccionar pregunta
- `submit_answer` - Enviar respuesta
- `steal_decision` - Decidir si robar
- `chat_message` - Mensaje de chat
- `team_message` - Mensaje de equipo
- `send_reaction` - Enviar emoji
### Servidor → Cliente
- `room_created` - Sala creada
- `player_joined` - Jugador se unió
- `game_started` - Juego iniciado
- `question_selected` - Pregunta seleccionada
- `answer_result` - Resultado de respuesta
- `steal_prompt` - Oportunidad de robo
- `game_finished` - Juego terminado
- `error` - Error
## Temas Disponibles
| Tema | Descripción |
|------|-------------|
| DRRR | Inspirado en Durarara!! - Oscuro con acentos amarillos |
| Retro Arcade | Estética arcade 80s - Neón sobre negro |
| Minimal | Diseño limpio y moderno - Blanco con acentos |
| Gaming RGB | Gradientes RGB animados - Estilo gamer |
| Anime 90s | Colores pasteles - Nostalgia anime |
## Categorías
1. **Nintendo** 🍄 - Mario, Zelda, Pokémon, etc.
2. **Xbox** 🎮 - Halo, Forza, Game Pass, etc.
3. **PlayStation** 🎯 - God of War, Uncharted, etc.
4. **Anime** ⛩️ - Naruto, One Piece, Dragon Ball, etc.
5. **Música** 🎵 - Artistas, géneros, historia musical
6. **Películas** 🎬 - Cine clásico y moderno
7. **Libros** 📚 - Literatura universal
8. **Historia** 🏛️ - Historia y cultura general
## Contribuir
1. Fork el repositorio
2. Crear rama feature (`git checkout -b feature/nueva-funcionalidad`)
3. Commit cambios (`git commit -m 'feat: descripción'`)
4. Push a la rama (`git push origin feature/nueva-funcionalidad`)
5. Crear Pull Request
## Licencia
MIT License - ver [LICENSE](LICENSE)
## Créditos
Desarrollado por Frank con asistencia de Claude (Anthropic)
---
<p align="center">
<strong>¡Que gane el mejor equipo!</strong> 🏆
</p>

45
backend/.env.example Normal file
View File

@@ -0,0 +1,45 @@
# ===========================================
# Trivy Backend - Configuration
# ===========================================
# Database
DATABASE_URL=postgresql+asyncpg://trivy:trivy@localhost:5432/trivy
# Redis
REDIS_URL=redis://localhost:6379
# Anthropic API (Required for answer validation)
ANTHROPIC_API_KEY=sk-ant-api03-YOUR_API_KEY_HERE
# ===========================================
# Game Settings
# ===========================================
# Penalty multiplier when failing a steal (0.5 = lose 50% of question points)
STEAL_PENALTY_MULTIPLIER=0.5
# Time multiplier for steal attempts (0.5 = half the original time)
STEAL_TIME_MULTIPLIER=0.5
# How long rooms stay in Redis (hours)
ROOM_TTL_HOURS=3
# ===========================================
# Security
# ===========================================
# CORS origins (comma-separated)
CORS_ORIGINS=http://localhost:3000,http://localhost:5173
# Secret key for sessions (generate with: openssl rand -hex 32)
SECRET_KEY=your-secret-key-here
# ===========================================
# Optional
# ===========================================
# Log level (DEBUG, INFO, WARNING, ERROR)
LOG_LEVEL=INFO
# Enable SQL echo (for debugging)
SQL_ECHO=false

492
docs/API.md Normal file
View File

@@ -0,0 +1,492 @@
# API Reference - Trivy
## Base URL
```
http://localhost:8000/api
```
## Autenticación
El panel de administración usa autenticación básica HTTP.
```bash
# Ejemplo con curl
curl -u admin:admin123 http://localhost:8000/api/admin/questions
```
---
## Endpoints REST
### Game
#### GET /game/categories
Obtiene todas las categorías disponibles.
**Response:**
```json
[
{
"id": 1,
"name": "Nintendo",
"icon": "🍄",
"color": "#E60012"
},
{
"id": 2,
"name": "Xbox",
"icon": "🎮",
"color": "#107C10"
}
]
```
#### GET /game/today-questions
Obtiene las preguntas activas para hoy (sin respuestas).
**Response:**
```json
{
"1": [
{
"id": 1,
"category_id": 1,
"question_text": "¿En qué año se lanzó la NES?",
"difficulty": 1,
"points": 100,
"time_seconds": 15
}
]
}
```
---
### Admin - Preguntas
#### GET /admin/questions
Lista todas las preguntas.
**Query Parameters:**
- `status` (optional): `pending`, `approved`, `used`
- `category_id` (optional): ID de categoría
**Response:**
```json
[
{
"id": 1,
"category_id": 1,
"question_text": "¿En qué año se lanzó la NES?",
"correct_answer": "1983",
"alt_answers": ["1984"],
"difficulty": 1,
"points": 100,
"time_seconds": 15,
"date_active": "2024-01-26",
"status": "approved",
"fun_fact": "La NES salvó la industria del videojuego",
"created_at": "2024-01-25T10:00:00Z"
}
]
```
#### POST /admin/questions
Crea una nueva pregunta.
**Request Body:**
```json
{
"category_id": 1,
"question_text": "¿Cuál es el nombre del fontanero de Nintendo?",
"correct_answer": "Mario",
"alt_answers": ["Super Mario"],
"difficulty": 1,
"points": 100,
"time_seconds": 15,
"date_active": "2024-01-26",
"status": "approved",
"fun_fact": "Mario apareció por primera vez en Donkey Kong"
}
```
**Response:** `201 Created`
#### PUT /admin/questions/{id}
Actualiza una pregunta existente.
#### DELETE /admin/questions/{id}
Elimina una pregunta.
---
### Admin - Categorías
#### GET /admin/categories
Lista todas las categorías.
#### POST /admin/categories
Crea una nueva categoría.
**Request Body:**
```json
{
"name": "Ciencia",
"icon": "🔬",
"color": "#00BCD4"
}
```
---
### Admin - Salas
#### GET /admin/rooms/active
Obtiene las salas activas en Redis.
**Response:**
```json
[
{
"code": "ABC123",
"status": "playing",
"host": "Player1",
"teams": {
"A": [{"name": "Player1", "team": "A"}],
"B": [{"name": "Player2", "team": "B"}]
},
"scores": {"A": 500, "B": 300},
"current_team": "A"
}
]
```
---
## WebSocket Events
### Conexión
```javascript
import { io } from 'socket.io-client';
const socket = io('http://localhost:8000', {
transports: ['websocket', 'polling']
});
```
---
### Eventos Cliente → Servidor
#### create_room
Crea una nueva sala de juego.
```javascript
socket.emit('create_room', {
player_name: 'MiNombre'
});
```
#### join_room
Unirse a una sala existente.
```javascript
socket.emit('join_room', {
room_code: 'ABC123',
player_name: 'MiNombre',
team: 'A' // o 'B'
});
```
#### change_team
Cambiar de equipo en el lobby.
```javascript
socket.emit('change_team', {
team: 'B'
});
```
#### start_game
Iniciar el juego (solo host).
```javascript
socket.emit('start_game', {});
```
#### select_question
Seleccionar una pregunta del tablero.
```javascript
socket.emit('select_question', {
question_id: 1,
category_id: 1
});
```
#### submit_answer
Enviar respuesta a la pregunta actual.
```javascript
socket.emit('submit_answer', {
answer: 'Mi respuesta',
question: { /* objeto pregunta */ },
is_steal: false
});
```
#### steal_decision
Decidir si intentar robar.
```javascript
socket.emit('steal_decision', {
attempt: true, // o false para pasar
question_id: 1,
answer: 'Mi respuesta' // solo si attempt=true
});
```
#### chat_message
Enviar mensaje al chat general.
```javascript
socket.emit('chat_message', {
message: 'Hola a todos!'
});
```
#### team_message
Enviar mensaje al chat de equipo.
```javascript
socket.emit('team_message', {
room_code: 'ABC123',
team: 'A',
player_name: 'MiNombre',
message: 'Mensaje privado del equipo'
});
```
#### send_reaction
Enviar reacción emoji.
```javascript
socket.emit('send_reaction', {
emoji: '🎉',
room_code: 'ABC123',
player_name: 'MiNombre'
});
```
---
### Eventos Servidor → Cliente
#### room_created
Confirmación de sala creada.
```javascript
socket.on('room_created', (data) => {
console.log(data.room.code); // 'ABC123'
});
```
**Payload:**
```json
{
"room": {
"code": "ABC123",
"status": "waiting",
"host": "Player1",
"teams": {"A": [...], "B": []},
"scores": {"A": 0, "B": 0}
}
}
```
#### player_joined
Un jugador se unió a la sala.
```javascript
socket.on('player_joined', (data) => {
console.log('Nuevo jugador:', data.room.teams);
});
```
#### player_left
Un jugador abandonó la sala.
```javascript
socket.on('player_left', (data) => {
console.log('Jugador salió');
});
```
#### game_started
El juego ha comenzado.
```javascript
socket.on('game_started', (data) => {
// data.room.board contiene el tablero con las preguntas
console.log('Categorías:', Object.keys(data.room.board));
});
```
**Payload:**
```json
{
"room": {
"status": "playing",
"board": {
"1": [
{
"id": 1,
"question_text": "...",
"difficulty": 1,
"points": 100,
"answered": false
}
]
},
"current_team": "A",
"current_player_index": {"A": 0, "B": 0}
}
}
```
#### question_selected
Se seleccionó una pregunta.
```javascript
socket.on('question_selected', (data) => {
console.log('Pregunta:', data.question_id);
});
```
#### answer_result
Resultado de una respuesta.
```javascript
socket.on('answer_result', (data) => {
if (data.valid) {
console.log('¡Correcto! +', data.points_earned);
} else {
console.log('Incorrecto:', data.reason);
}
});
```
**Payload:**
```json
{
"valid": true,
"reason": "Respuesta correcta",
"points_earned": 100,
"was_steal": false,
"room": { /* estado actualizado */ }
}
```
#### steal_prompt
Oportunidad de robo disponible.
```javascript
socket.on('steal_prompt', (data) => {
console.log('¡Puedes robar!');
});
```
#### game_finished
El juego ha terminado.
```javascript
socket.on('game_finished', (data) => {
console.log('Ganador:', data.winner);
console.log('Puntuación final:', data.room.scores);
});
```
#### new_reaction
Nueva reacción de un jugador.
```javascript
socket.on('new_reaction', (data) => {
console.log(data.player_name, 'reaccionó con', data.emoji);
});
```
#### team_message
Mensaje de chat de equipo.
```javascript
socket.on('team_message', (data) => {
console.log(`[Equipo] ${data.player_name}: ${data.message}`);
});
```
#### error
Error del servidor.
```javascript
socket.on('error', (data) => {
console.error('Error:', data.message);
});
```
---
## Códigos de Error
| Código | Mensaje | Descripción |
|--------|---------|-------------|
| `room_not_found` | Room not found | La sala no existe |
| `room_full` | Room is full | La sala está llena |
| `not_host` | Only the host can start | Solo el host puede iniciar |
| `need_players` | Both teams need players | Ambos equipos necesitan jugadores |
| `not_your_turn` | Not your turn | No es tu turno |
| `question_answered` | Question already answered | Pregunta ya contestada |
---
## Rate Limiting
- API REST: 100 requests/minuto por IP
- WebSocket: Sin límite específico (controlado por lógica de juego)
## Timeouts
- Preguntas: 15-35 segundos según dificultad
- Robo: 50% del tiempo original
- Sala inactiva: 3 horas (configurable)

374
docs/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,374 @@
# Arquitectura de Trivy
## Visión General
Trivy utiliza una arquitectura de microservicios con comunicación en tiempo real mediante WebSockets.
```
┌─────────────────────────────────────────────────────────────────────┐
│ CLIENTES │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Browser │ │ Browser │ │ Browser │ │ Browser │ │
│ │ React App│ │ React App│ │ React App│ │ React App│ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │
│ └─────────────┴──────┬──────┴─────────────┘ │
│ │ │
│ WebSocket/HTTP │
└────────────────────────────┼─────────────────────────────────────────┘
┌────────────────────────────┼─────────────────────────────────────────┐
│ BACKEND │
│ │ │
│ ┌─────────────────────────▼─────────────────────────────┐ │
│ │ FastAPI + Socket.IO │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ REST API │ │ WebSocket │ │ ASGI │ │ │
│ │ │ Endpoints │ │ Events │ │ Middleware │ │ │
│ │ └──────┬──────┘ └──────┬──────┘ └─────────────┘ │ │
│ └─────────┼────────────────┼────────────────────────────┘ │
│ │ │ │
│ ┌─────────▼────────────────▼────────────────────────────┐ │
│ │ SERVICES │ │
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │
│ │ │ Room │ │ Game │ │ AI │ │ │
│ │ │ Manager │ │ Manager │ │ Validator │ │ │
│ │ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │ │
│ └────────┼───────────────┼───────────────┼──────────────┘ │
│ │ │ │ │
│ ┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐ │
│ │ Redis │ │ PostgreSQL│ │ Claude │ │
│ │ (State) │ │ (Data) │ │ API │ │
│ └───────────┘ └───────────┘ └───────────┘ │
└──────────────────────────────────────────────────────────────────────┘
```
## Componentes
### Frontend (React + TypeScript)
```
frontend/src/
├── components/ # Componentes reutilizables
│ ├── chat/ # EmojiReactions, ReactionOverlay, TeamChat
│ ├── game/ # Componentes del tablero
│ ├── lobby/ # Sala de espera
│ └── ui/ # SoundControl, botones, inputs
├── hooks/ # Custom hooks
│ ├── useSocket.ts # Gestión de WebSocket
│ └── useSound.ts # Gestión de audio
├── pages/ # Rutas principales
│ ├── Home.tsx # Crear/unir sala
│ ├── Lobby.tsx # Sala de espera
│ ├── Game.tsx # Tablero de juego
│ ├── Results.tsx # Resultados finales
│ └── admin/ # Panel de administración
├── services/
│ └── socket.ts # Singleton de Socket.IO
├── stores/ # Estado global (Zustand)
│ ├── gameStore.ts # Estado del juego
│ ├── themeStore.ts # Tema visual
│ └── soundStore.ts # Configuración de audio
├── themes/ # Definiciones de temas
└── types/ # Tipos TypeScript
```
### Backend (FastAPI + Python)
```
backend/app/
├── api/ # Endpoints REST
│ ├── admin.py # CRUD de preguntas/categorías
│ ├── auth.py # Autenticación admin
│ └── game.py # Datos públicos del juego
├── models/ # Modelos SQLAlchemy
│ ├── question.py # Preguntas
│ ├── category.py # Categorías
│ ├── admin.py # Administradores
│ └── game_session.py # Sesiones de juego
├── schemas/ # Schemas Pydantic
├── services/ # Lógica de negocio
│ ├── room_manager.py # Gestión de salas (Redis)
│ ├── game_manager.py # Lógica del juego
│ ├── question_service.py # Carga de preguntas
│ └── ai_validator.py # Validación con Claude
├── sockets/ # Eventos WebSocket
│ └── game_events.py # Todos los eventos del juego
├── config.py # Configuración
└── main.py # Punto de entrada ASGI
```
## Flujo de Datos
### 1. Creación de Sala
```
Cliente Backend Redis
│ │ │
│──── create_room ────────>│ │
│ │ │
│ │──── SETEX room:CODE ────>│
│ │ │
│ │<─── OK ─────────────────│
│ │ │
│<─── room_created ────────│ │
│ │ │
```
### 2. Unirse a Sala
```
Cliente Backend Redis
│ │ │
│──── join_room ──────────>│ │
│ │ │
│ │──── GET room:CODE ──────>│
│ │<─── room_data ──────────│
│ │ │
│ │──── SETEX (updated) ────>│
│ │ │
│<─── player_joined ───────│ │
│ │ │
│ │──── emit to room ───────>│ (otros clientes)
```
### 3. Inicio del Juego
```
Cliente Backend PostgreSQL
│ │ │
│──── start_game ─────────>│ │
│ │ │
│ │──── SELECT questions ───>│
│ │ (5 random cats) │
│ │<─── questions ──────────│
│ │ │
│ │──── SELECT 1 per diff ──>│
│ │<─── board data ─────────│
│ │ │
│<─── game_started ────────│ │
│ (with board) │ │
```
### 4. Responder Pregunta
```
Cliente Backend Claude API
│ │ │
│──── submit_answer ──────>│ │
│ │ │
│ │──── validate_answer ────>│
│ │ (question, answer) │
│ │ │
│ │<─── {valid, reason} ────│
│ │ │
│<─── answer_result ───────│ │
│ │ │
```
## Estado del Juego
### Redis (Estado en Tiempo Real)
```json
{
"room:ABC123": {
"code": "ABC123",
"status": "playing",
"host": "Player1",
"teams": {
"A": [{"name": "Player1", "team": "A", "socket_id": "..."}],
"B": [{"name": "Player2", "team": "B", "socket_id": "..."}]
},
"current_team": "A",
"current_player_index": {"A": 0, "B": 0},
"current_question": null,
"can_steal": false,
"scores": {"A": 500, "B": 300},
"board": {
"1": [/* preguntas categoría 1 */],
"3": [/* preguntas categoría 3 */]
}
},
"player:socket_id": {
"room": "ABC123",
"name": "Player1",
"team": "A"
}
}
```
### PostgreSQL (Datos Persistentes)
```sql
-- Categorías
categories (id, name, icon, color)
-- Preguntas (pool de 200)
questions (
id, category_id, question_text, correct_answer,
alt_answers[], difficulty, points, time_seconds,
date_active, status, fun_fact
)
-- Sesiones de juego (historial)
game_sessions (
id, room_code, status,
team_a_score, team_b_score,
questions_used[], created_at, finished_at
)
-- Eventos de juego (analytics)
game_events (
id, session_id, event_type, player_name,
question_id, data, created_at
)
-- Administradores
admins (id, username, password_hash)
```
## Validación de Respuestas con IA
El sistema usa Claude para validación flexible de respuestas:
```python
# ai_validator.py
async def validate_answer(
question: str,
correct_answer: str,
alt_answers: list,
player_answer: str
) -> dict:
prompt = f"""
Pregunta: {question}
Respuesta correcta: {correct_answer}
Respuestas alternativas: {alt_answers}
Respuesta del jugador: {player_answer}
¿La respuesta del jugador es correcta?
Considera sinónimos, abreviaciones, errores menores de ortografía.
Responde en JSON: {{"valid": bool, "reason": "explicación"}}
"""
response = await anthropic.messages.create(
model="claude-3-haiku-20240307",
messages=[{"role": "user", "content": prompt}]
)
return json.loads(response.content[0].text)
```
## Selección de Preguntas
Cada partida selecciona aleatoriamente:
1. **5 categorías** de las 8 disponibles
2. **1 pregunta por dificultad** de las 5 opciones disponibles
```python
# question_service.py
async def get_board_for_game(db, target_date):
# 1. Obtener todas las preguntas del día
full_board = await get_daily_questions(db, target_date)
# 2. Seleccionar 5 categorías aleatorias
selected_cats = random.sample(list(full_board.keys()), 5)
# 3. Para cada categoría, seleccionar 1 pregunta por dificultad
game_board = {}
for cat_id in selected_cats:
questions_by_diff = group_by_difficulty(full_board[cat_id])
selected = []
for diff in range(1, 6):
if diff in questions_by_diff:
selected.append(random.choice(questions_by_diff[diff]))
game_board[cat_id] = selected
return game_board
```
## Temas y Sonidos
### Estructura de Temas
```typescript
// themes/drrr.ts
export const drrrTheme: ThemeConfig = {
name: 'drrr',
displayName: 'DRRR Style',
colors: {
primary: '#FFD93D',
secondary: '#6BCB77',
accent: '#FF6B6B',
bg: '#1a1a2e',
text: '#EAEAEA',
textMuted: '#888888'
},
fonts: {
heading: '"Press Start 2P", monospace',
body: 'Inter, sans-serif'
}
}
```
### Estructura de Sonidos
```
public/sounds/
├── drrr/
│ ├── correct.mp3
│ ├── incorrect.mp3
│ ├── select.mp3
│ └── ...
├── retro/
├── minimal/
├── rgb/
└── anime/
```
Cada tema tiene sus propios archivos de audio, con fallback a sonidos generados por Web Audio API si no existen.
## Escalabilidad
### Horizontal Scaling
```
┌─────────────┐
│ NGINX │
│ (LB) │
└──────┬──────┘
┌─────────────────┼─────────────────┐
│ │ │
┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐
│ Backend │ │ Backend │ │ Backend │
│ Node 1 │ │ Node 2 │ │ Node 3 │
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ │ │
└────────────────┼────────────────┘
┌───────────┴───────────┐
│ │
┌─────▼─────┐ ┌─────▼─────┐
│ Redis │ │ PostgreSQL│
│ Cluster │ │ Primary │
└───────────┘ └───────────┘
```
### Consideraciones
1. **Socket.IO con Redis Adapter** - Para sincronizar eventos entre múltiples instancias
2. **Sticky Sessions** - Para mantener conexiones WebSocket
3. **PostgreSQL Read Replicas** - Para queries de lectura
4. **Redis Cluster** - Para alta disponibilidad del estado
## Seguridad
1. **CORS** - Configurado para dominios permitidos
2. **Rate Limiting** - En endpoints REST
3. **Input Validation** - Pydantic schemas
4. **SQL Injection** - Prevenido por SQLAlchemy ORM
5. **XSS** - React escapa contenido por defecto
6. **WebSocket Auth** - Validación de sala/jugador en cada evento

469
docs/INSTALLATION.md Normal file
View File

@@ -0,0 +1,469 @@
# Guía de Instalación - Trivy
## Requisitos Previos
### Software Requerido
| Software | Versión Mínima | Notas |
|----------|----------------|-------|
| Python | 3.11+ | Con pip |
| Node.js | 18+ | Con npm |
| PostgreSQL | 15+ | O Docker |
| Redis | 7+ | O Docker |
| Git | 2.0+ | Para clonar |
### API Keys
- **Anthropic Claude API** - Obtener en https://console.anthropic.com
## Opción 1: Instalación con Docker (Recomendado)
### 1. Clonar repositorio
```bash
git clone https://gitea.local/frank/Trivy.git
cd Trivy
```
### 2. Configurar variables de entorno
```bash
# Backend
cp backend/.env.example backend/.env
# Editar backend/.env:
DATABASE_URL=postgresql+asyncpg://trivy:trivy@db:5432/trivy
REDIS_URL=redis://redis:6379
ANTHROPIC_API_KEY=sk-ant-api03-YOUR_KEY_HERE
# Frontend
cp frontend/.env.example frontend/.env
# Editar frontend/.env:
VITE_API_URL=http://localhost:8000
VITE_WS_URL=http://localhost:8000
```
### 3. Iniciar con Docker Compose
```bash
docker-compose up -d
```
Esto iniciará:
- PostgreSQL en puerto 5432
- Redis en puerto 6379
- Backend en puerto 8000
- Frontend en puerto 3000
### 4. Ejecutar migraciones
```bash
docker-compose exec backend alembic upgrade head
```
### 5. Crear datos iniciales
```bash
docker-compose exec backend python scripts/seed_data.py
```
### 6. Acceder
- Frontend: http://localhost:3000
- Admin: http://localhost:3000/admin (admin/admin123)
- API: http://localhost:8000/docs
---
## Opción 2: Instalación Manual
### 1. Clonar repositorio
```bash
git clone https://gitea.local/frank/Trivy.git
cd Trivy
```
### 2. Configurar PostgreSQL
```bash
# Crear usuario y base de datos
sudo -u postgres psql
CREATE USER trivy WITH PASSWORD 'trivy';
CREATE DATABASE trivy OWNER trivy;
GRANT ALL PRIVILEGES ON DATABASE trivy TO trivy;
\q
```
### 3. Configurar Redis
```bash
# Ubuntu/Debian
sudo apt install redis-server
sudo systemctl start redis
sudo systemctl enable redis
# Verificar
redis-cli ping
# Respuesta: PONG
```
### 4. Configurar Backend
```bash
cd backend
# Crear entorno virtual
python3 -m venv venv
source venv/bin/activate
# Instalar dependencias
pip install -r requirements.txt
# Configurar variables de entorno
cp .env.example .env
```
Editar `backend/.env`:
```env
# Base de datos
DATABASE_URL=postgresql+asyncpg://trivy:trivy@localhost:5432/trivy
# Redis
REDIS_URL=redis://localhost:6379
# Anthropic API
ANTHROPIC_API_KEY=sk-ant-api03-YOUR_KEY_HERE
# Configuración del juego
STEAL_PENALTY_MULTIPLIER=0.5
STEAL_TIME_MULTIPLIER=0.5
ROOM_TTL_HOURS=3
# CORS (separar con comas)
CORS_ORIGINS=http://localhost:3000,http://localhost:5173
```
### 5. Ejecutar migraciones
```bash
cd backend
source venv/bin/activate
alembic upgrade head
```
### 6. Crear usuario admin
```bash
python -c "
import asyncio
from app.models.base import get_async_session
from app.models.admin import Admin
async def create_admin():
AsyncSession = get_async_session()
async with AsyncSession() as db:
admin = Admin(username='admin')
admin.set_password('admin123')
db.add(admin)
await db.commit()
print('Admin creado: admin/admin123')
asyncio.run(create_admin())
"
```
### 7. Crear categorías
```bash
python -c "
import asyncio
from app.models.base import get_async_session
from app.models.category import Category
categories = [
('Nintendo', '🍄', '#E60012'),
('Xbox', '🎮', '#107C10'),
('PlayStation', '🎯', '#003791'),
('Anime', '⛩️', '#FF6B9D'),
('Música', '🎵', '#1DB954'),
('Películas', '🎬', '#F5C518'),
('Libros', '📚', '#8B4513'),
('Historia', '🏛️', '#6B5B95'),
]
async def create_categories():
AsyncSession = get_async_session()
async with AsyncSession() as db:
for name, icon, color in categories:
cat = Category(name=name, icon=icon, color=color)
db.add(cat)
await db.commit()
print(f'{len(categories)} categorías creadas')
asyncio.run(create_categories())
"
```
### 8. Crear preguntas (ejecutar script completo)
```bash
python scripts/seed_questions.py
```
### 9. Configurar Frontend
```bash
cd frontend
# Instalar dependencias
npm install
# Configurar variables de entorno
cp .env.example .env
```
Editar `frontend/.env`:
```env
VITE_API_URL=http://localhost:8000
VITE_WS_URL=http://localhost:8000
```
### 10. Iniciar servicios
**Terminal 1 - Backend:**
```bash
cd backend
source venv/bin/activate
uvicorn app.main:socket_app --host 0.0.0.0 --port 8000 --reload
```
**Terminal 2 - Frontend:**
```bash
cd frontend
npm run dev -- --host
```
### 11. Acceder
- Frontend: http://localhost:3000
- Admin: http://localhost:3000/admin
- API Docs: http://localhost:8000/docs
---
## Configuración para Red Local
Para permitir acceso desde otros dispositivos en la red:
### 1. Obtener IP local
```bash
ip addr show | grep "inet " | grep -v 127.0.0.1
# Ejemplo: 192.168.1.100
```
### 2. Actualizar configuración
**backend/.env:**
```env
CORS_ORIGINS=http://localhost:3000,http://192.168.1.100:3000
```
**frontend/.env:**
```env
VITE_API_URL=http://192.168.1.100:8000
VITE_WS_URL=http://192.168.1.100:8000
```
### 3. Iniciar con host 0.0.0.0
```bash
# Backend
uvicorn app.main:socket_app --host 0.0.0.0 --port 8000
# Frontend
npm run dev -- --host
```
### 4. Acceder desde otros dispositivos
- http://192.168.1.100:3000
---
## Producción
### Backend con Gunicorn
```bash
pip install gunicorn
gunicorn app.main:socket_app \
-w 4 \
-k uvicorn.workers.UvicornWorker \
-b 0.0.0.0:8000 \
--access-logfile - \
--error-logfile -
```
### Frontend Build
```bash
cd frontend
npm run build
# Servir con cualquier servidor estático
# Ejemplo con serve:
npm install -g serve
serve -s dist -l 3000
```
### Nginx como Reverse Proxy
```nginx
# /etc/nginx/sites-available/trivy
server {
listen 80;
server_name trivy.example.com;
# Frontend
location / {
root /var/www/trivy/frontend/dist;
try_files $uri $uri/ /index.html;
}
# API
location /api {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# WebSocket
location /socket.io {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}
```
### Systemd Service
```ini
# /etc/systemd/system/trivy-backend.service
[Unit]
Description=Trivy Backend
After=network.target postgresql.service redis.service
[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/trivy/backend
Environment=PATH=/var/www/trivy/backend/venv/bin
ExecStart=/var/www/trivy/backend/venv/bin/gunicorn app.main:socket_app -w 4 -k uvicorn.workers.UvicornWorker -b 127.0.0.1:8000
Restart=always
[Install]
WantedBy=multi-user.target
```
```bash
sudo systemctl enable trivy-backend
sudo systemctl start trivy-backend
```
---
## Solución de Problemas
### Error: "Could not connect to Redis"
```bash
# Verificar Redis
redis-cli ping
# Si no responde, iniciar Redis
sudo systemctl start redis
```
### Error: "Database connection failed"
```bash
# Verificar PostgreSQL
sudo systemctl status postgresql
# Verificar credenciales
psql -U trivy -d trivy -h localhost
```
### Error: "CORS blocked"
Verificar que la URL del frontend esté en `CORS_ORIGINS` del backend.
### WebSocket no conecta
1. Verificar que `VITE_WS_URL` use `http://` (no `ws://`)
2. Verificar que el backend use `socket_app` (no `app`)
3. Verificar firewall/puertos
### Preguntas no cargan
```bash
# Verificar preguntas en BD
cd backend
source venv/bin/activate
python -c "
import asyncio
from sqlalchemy import select
from app.models.base import get_async_session
from app.models.question import Question
from datetime import date
async def check():
AsyncSession = get_async_session()
async with AsyncSession() as db:
result = await db.execute(
select(Question).where(Question.date_active == date.today())
)
qs = result.scalars().all()
print(f'Preguntas para hoy: {len(qs)}')
asyncio.run(check())
"
```
### Sonidos no funcionan
Los sonidos requieren archivos MP3 en `/public/sounds/{tema}/`. Si no existen, el sistema usa fallbacks generados por Web Audio API (tonos simples).
---
## Actualizaciones
```bash
cd Trivy
git pull origin main
# Backend
cd backend
source venv/bin/activate
pip install -r requirements.txt
alembic upgrade head
# Frontend
cd frontend
npm install
npm run build
# Reiniciar servicios
sudo systemctl restart trivy-backend
```