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

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)