Ronda 1: 5 categorías con puntos normales (100-500) Ronda 2: 5 categorías diferentes con puntos x2 (200-1000) Backend: - question_service: soporta excluir categorías y multiplicador de puntos - game_manager: trackea current_round, start_round_2() carga nuevo tablero - game_events: emite round_started al completar ronda 1 Frontend: - useSocket: escucha evento round_started - Game.tsx: muestra indicador de ronda actual - types: GameRoom incluye current_round Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
194 lines
6.1 KiB
Python
194 lines
6.1 KiB
Python
from typing import Optional, List, Dict
|
||
from datetime import date
|
||
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(
|
||
self,
|
||
db: AsyncSession,
|
||
target_date: Optional[date] = None
|
||
) -> Dict[int, List[dict]]:
|
||
"""
|
||
Obtiene preguntas del día organizadas por categoría.
|
||
|
||
Args:
|
||
db: Sesión de BD
|
||
target_date: Fecha (default: hoy)
|
||
|
||
Returns:
|
||
Dict[category_id, List[question_dict]] para el tablero
|
||
"""
|
||
if target_date is None:
|
||
target_date = date.today()
|
||
|
||
# Buscar preguntas aprobadas para la fecha
|
||
query = select(Question).where(
|
||
and_(
|
||
Question.date_active == target_date,
|
||
Question.status == "approved"
|
||
)
|
||
).order_by(Question.category_id, Question.difficulty)
|
||
|
||
result = await db.execute(query)
|
||
questions = result.scalars().all()
|
||
|
||
# Organizar por categoría
|
||
board: Dict[int, List[dict]] = {}
|
||
for q in questions:
|
||
if q.category_id not in board:
|
||
board[q.category_id] = []
|
||
board[q.category_id].append({
|
||
"id": q.id,
|
||
"category_id": q.category_id,
|
||
"question_text": q.question_text,
|
||
"correct_answer": q.correct_answer,
|
||
"alt_answers": q.alt_answers or [],
|
||
"difficulty": q.difficulty,
|
||
"points": q.points,
|
||
"time_seconds": q.time_seconds,
|
||
"fun_fact": q.fun_fact,
|
||
"answered": False,
|
||
"selected": False
|
||
})
|
||
|
||
return board
|
||
|
||
async def get_board_for_game(
|
||
self,
|
||
db: AsyncSession,
|
||
target_date: Optional[date] = None,
|
||
exclude_categories: Optional[List[int]] = None,
|
||
point_multiplier: int = 1
|
||
) -> Dict[str, List[dict]]:
|
||
"""
|
||
Genera el tablero 5×5 para el juego.
|
||
Selecciona 5 categorías aleatorias y 1 pregunta por dificultad.
|
||
|
||
Args:
|
||
db: Database session
|
||
target_date: Date for questions (default: today)
|
||
exclude_categories: Category IDs to exclude (for round 2)
|
||
point_multiplier: Multiply points by this value (for round 2)
|
||
|
||
Returns:
|
||
Dict con category_id como string (para JSON) -> lista de preguntas
|
||
"""
|
||
full_board = await self.get_daily_questions(db, target_date)
|
||
|
||
if not full_board:
|
||
return {}
|
||
|
||
# Get available category IDs that have questions
|
||
available_categories = list(full_board.keys())
|
||
|
||
# Exclude categories from previous round
|
||
if exclude_categories:
|
||
available_categories = [
|
||
c for c in available_categories if c not in exclude_categories
|
||
]
|
||
|
||
if not available_categories:
|
||
return {}
|
||
|
||
# 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).copy()
|
||
# Apply point multiplier for round 2
|
||
if point_multiplier > 1:
|
||
selected_q["points"] = selected_q["points"] * point_multiplier
|
||
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,
|
||
db: AsyncSession,
|
||
question_id: int
|
||
) -> Optional[Question]:
|
||
"""Obtiene una pregunta por ID."""
|
||
result = await db.execute(
|
||
select(Question).where(Question.id == question_id)
|
||
)
|
||
return result.scalar_one_or_none()
|
||
|
||
async def mark_question_used(
|
||
self,
|
||
db: AsyncSession,
|
||
question_id: int
|
||
) -> bool:
|
||
"""Marca una pregunta como usada."""
|
||
question = await self.get_question_by_id(db, question_id)
|
||
if question:
|
||
question.status = "used"
|
||
await db.commit()
|
||
return True
|
||
return False
|
||
|
||
async def get_categories_with_questions(
|
||
self,
|
||
db: AsyncSession,
|
||
target_date: Optional[date] = None
|
||
) -> List[dict]:
|
||
"""
|
||
Obtiene categorías que tienen preguntas para la fecha.
|
||
Útil para mostrar solo categorías disponibles.
|
||
"""
|
||
board = await self.get_daily_questions(db, target_date)
|
||
|
||
# Obtener info de categorías
|
||
cat_ids = list(board.keys())
|
||
if not cat_ids:
|
||
return []
|
||
|
||
result = await db.execute(
|
||
select(Category).where(Category.id.in_(cat_ids))
|
||
)
|
||
categories = result.scalars().all()
|
||
|
||
return [
|
||
{
|
||
"id": c.id,
|
||
"name": c.name,
|
||
"icon": c.icon,
|
||
"color": c.color,
|
||
"question_count": len(board.get(c.id, []))
|
||
}
|
||
for c in categories
|
||
]
|
||
|
||
|
||
# Singleton
|
||
question_service = QuestionService()
|