feat: 5 categorías rotativas por partida + pool de 200 preguntas + mejoras UI

Cambios principales:
- Tablero ahora muestra 5 categorías aleatorias (de 8 disponibles)
- Pool de 200 preguntas (8 cats × 5 diffs × 5 opciones)
- Preguntas rotan aleatoriamente entre partidas
- Diseño mejorado estilo Jeopardy con efectos visuales
- Socket singleton para conexión persistente
- Nuevos sonidos: game_start, player_join, question_reveal, hover, countdown
- Control de volumen vertical
- Barra de progreso del timer en modal de preguntas
- Animaciones mejoradas con Framer Motion

Backend:
- question_service: selección aleatoria de 5 categorías
- room_manager: fix retorno de create_room
- game_events: carga board desde DB, await en enter_room

Frontend:
- Game.tsx: tablero dinámico, efectos hover, mejor scoreboard
- useSocket: singleton service, eventos con sonidos
- SoundControl: slider vertical
- soundStore: 5 nuevos efectos de sonido

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-01-26 23:44:55 +00:00
parent e5a2b016a0
commit ab201e113a
8 changed files with 631 additions and 264 deletions

View File

@@ -1,11 +1,15 @@
from typing import Optional, List, Dict
from datetime import date
from sqlalchemy import select, and_
import random
from sqlalchemy import select, and_, func
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.question import Question
from app.models.category import Category
# Number of categories per game
CATEGORIES_PER_GAME = 5
class QuestionService:
async def get_daily_questions(
@@ -64,16 +68,49 @@ class QuestionService:
target_date: Optional[date] = None
) -> Dict[str, List[dict]]:
"""
Genera el tablero 8×5 para el juego.
Si no hay suficientes preguntas, retorna lo disponible.
Genera el tablero 5×5 para el juego.
Selecciona 5 categorías aleatorias y 1 pregunta por dificultad.
Returns:
Dict con category_id como string (para JSON) -> lista de preguntas
"""
board = await self.get_daily_questions(db, target_date)
full_board = await self.get_daily_questions(db, target_date)
# Convertir keys a string para JSON
return {str(k): v for k, v in board.items()}
if not full_board:
return {}
# Get available category IDs that have questions
available_categories = list(full_board.keys())
# Select random categories (up to CATEGORIES_PER_GAME)
num_categories = min(CATEGORIES_PER_GAME, len(available_categories))
selected_categories = random.sample(available_categories, num_categories)
# Build the game board with selected categories
game_board: Dict[str, List[dict]] = {}
for cat_id in selected_categories:
questions_by_difficulty: Dict[int, List[dict]] = {}
# Group questions by difficulty
for q in full_board[cat_id]:
diff = q["difficulty"]
if diff not in questions_by_difficulty:
questions_by_difficulty[diff] = []
questions_by_difficulty[diff].append(q)
# Select one random question per difficulty
selected_questions = []
for difficulty in range(1, 6): # 1-5
if difficulty in questions_by_difficulty:
questions = questions_by_difficulty[difficulty]
selected_q = random.choice(questions)
selected_questions.append(selected_q)
if selected_questions:
game_board[str(cat_id)] = selected_questions
return game_board
async def get_question_by_id(
self,

View File

@@ -59,9 +59,9 @@ class RoomManager:
)
# Add player to room
await self.add_player(room_code, player_name, "A", socket_id)
room = await self.add_player(room_code, player_name, "A", socket_id)
return room_state
return room
async def get_room(self, room_code: str) -> Optional[dict]:
"""Get room state by code."""

View File

@@ -50,7 +50,7 @@ def register_socket_events(sio: socketio.AsyncServer):
await room_manager.init_player_stats(room["code"], player_name)
# Join socket room
sio.enter_room(sid, room["code"])
await sio.enter_room(sid, room["code"])
await sio.emit("room_created", {"room": room}, to=sid)
@@ -75,7 +75,7 @@ def register_socket_events(sio: socketio.AsyncServer):
await room_manager.init_player_stats(room_code, player_name)
# Join socket room
sio.enter_room(sid, room_code)
await sio.enter_room(sid, room_code)
# Notify all players
await sio.emit("player_joined", {"room": room}, room=room_code)
@@ -147,13 +147,18 @@ def register_socket_events(sio: socketio.AsyncServer):
)
return
# Get board from data or generate
board = data.get("board", {})
updated_room = await game_manager.start_game(room_code, board)
# Load board from database and start game
async with await get_db_session() as db:
updated_room = await game_manager.start_game_with_db(db, room_code)
if updated_room:
await sio.emit("game_started", {"room": updated_room}, room=room_code)
else:
await sio.emit(
"error",
{"message": "No hay preguntas disponibles para hoy. Contacta al administrador."},
to=sid
)
@sio.event
async def select_question(sid, data):