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:
344
README.md
Normal file
344
README.md
Normal 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
45
backend/.env.example
Normal 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
492
docs/API.md
Normal 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
374
docs/ARCHITECTURE.md
Normal 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
469
docs/INSTALLATION.md
Normal 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
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user