- Añade sistema de reconexión tras refresh/cierre del navegador - Persistencia de sesión en localStorage (3h TTL) - Banner de reconexión en Home - Evento rejoin_room en backend - Nuevas categorías: Series TV, Marvel/DC, Disney, Memes, Pokémon, Mitología - Correcciones de bugs: - Fix: juego bloqueado al fallar robo (steal decision) - Fix: jugador duplicado al cambiar de equipo - Fix: rotación incorrecta de turno tras fallo - Config: soporte para Cloudflare tunnel (allowedHosts) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
317 lines
9.9 KiB
Python
317 lines
9.9 KiB
Python
from typing import Optional
|
|
from datetime import datetime, timedelta
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select
|
|
from app.services.room_manager import room_manager
|
|
from app.services.ai_validator import ai_validator
|
|
from app.services.question_service import question_service
|
|
from app.models.game_session import GameSession
|
|
from app.config import get_settings
|
|
|
|
settings = get_settings()
|
|
|
|
|
|
class GameManager:
|
|
async def start_game(self, room_code: str, board: dict) -> Optional[dict]:
|
|
"""
|
|
Start a game in a room.
|
|
|
|
Args:
|
|
room_code: The room code
|
|
board: Dict of category_id -> list of questions
|
|
|
|
Returns:
|
|
Updated room state
|
|
"""
|
|
room = await room_manager.get_room(room_code)
|
|
if not room:
|
|
return None
|
|
|
|
# Check minimum players
|
|
if not room["teams"]["A"] or not room["teams"]["B"]:
|
|
return None
|
|
|
|
# Set up game state
|
|
room["status"] = "playing"
|
|
room["current_team"] = "A"
|
|
room["current_player_index"] = {"A": 0, "B": 0}
|
|
room["board"] = board
|
|
room["scores"] = {"A": 0, "B": 0}
|
|
|
|
await room_manager.update_room(room_code, room)
|
|
return room
|
|
|
|
async def select_question(
|
|
self,
|
|
room_code: str,
|
|
question_id: int,
|
|
category_id: int
|
|
) -> Optional[dict]:
|
|
"""Select a question from the board."""
|
|
room = await room_manager.get_room(room_code)
|
|
if not room or room["status"] != "playing":
|
|
return None
|
|
|
|
# Mark question as current
|
|
room["current_question"] = question_id
|
|
room["can_steal"] = False
|
|
|
|
# Find and mark question on board
|
|
if str(category_id) in room["board"]:
|
|
for q in room["board"][str(category_id)]:
|
|
if q["id"] == question_id:
|
|
q["selected"] = True
|
|
break
|
|
|
|
await room_manager.update_room(room_code, room)
|
|
return room
|
|
|
|
async def submit_answer(
|
|
self,
|
|
room_code: str,
|
|
question: dict,
|
|
player_answer: str,
|
|
is_steal: bool = False
|
|
) -> dict:
|
|
"""
|
|
Submit an answer for validation.
|
|
|
|
Returns:
|
|
dict with validation result and updated game state
|
|
"""
|
|
room = await room_manager.get_room(room_code)
|
|
if not room:
|
|
return {"error": "Room not found"}
|
|
|
|
# Validate answer with AI
|
|
result = await ai_validator.validate_answer(
|
|
question=question["question_text"],
|
|
correct_answer=question["correct_answer"],
|
|
alt_answers=question.get("alt_answers", []),
|
|
player_answer=player_answer
|
|
)
|
|
|
|
is_correct = result.get("valid", False)
|
|
points = question["points"]
|
|
|
|
if is_correct:
|
|
# Award points
|
|
current_team = room["current_team"]
|
|
room["scores"][current_team] += points
|
|
|
|
# Mark question as answered
|
|
category_id = str(question["category_id"])
|
|
if category_id in room["board"]:
|
|
for q in room["board"][category_id]:
|
|
if q["id"] == question["id"]:
|
|
q["answered"] = True
|
|
break
|
|
|
|
# Winner chooses next
|
|
room["current_question"] = None
|
|
room["can_steal"] = False
|
|
|
|
# Advance player rotation
|
|
team_players = room["teams"][current_team]
|
|
room["current_player_index"][current_team] = (
|
|
room["current_player_index"][current_team] + 1
|
|
) % len(team_players)
|
|
|
|
else:
|
|
if is_steal:
|
|
# Failed steal - penalize
|
|
stealing_team = room["current_team"]
|
|
penalty = int(points * settings.steal_penalty_multiplier)
|
|
room["scores"][stealing_team] = max(
|
|
0, room["scores"][stealing_team] - penalty
|
|
)
|
|
|
|
# Mark question as answered (nobody gets it)
|
|
category_id = str(question["category_id"])
|
|
if category_id in room["board"]:
|
|
for q in room["board"][category_id]:
|
|
if q["id"] == question["id"]:
|
|
q["answered"] = True
|
|
break
|
|
|
|
# Original team chooses next
|
|
room["current_team"] = "B" if stealing_team == "A" else "A"
|
|
room["current_question"] = None
|
|
room["can_steal"] = False
|
|
|
|
else:
|
|
# Original team failed - enable steal
|
|
failed_team = room["current_team"]
|
|
room["can_steal"] = True
|
|
|
|
# Advance failed team's player index (they had their turn)
|
|
team_players = room["teams"][failed_team]
|
|
room["current_player_index"][failed_team] = (
|
|
room["current_player_index"][failed_team] + 1
|
|
) % len(team_players)
|
|
|
|
# Switch to other team for potential steal
|
|
room["current_team"] = "B" if failed_team == "A" else "A"
|
|
|
|
# Check if game is over (all questions answered)
|
|
all_answered = all(
|
|
q["answered"]
|
|
for questions in room["board"].values()
|
|
for q in questions
|
|
)
|
|
if all_answered:
|
|
room["status"] = "finished"
|
|
|
|
await room_manager.update_room(room_code, room)
|
|
|
|
return {
|
|
"valid": is_correct,
|
|
"reason": result.get("reason", ""),
|
|
"points_earned": points if is_correct else 0,
|
|
"room": room
|
|
}
|
|
|
|
async def pass_steal(self, room_code: str, question_id: int) -> Optional[dict]:
|
|
"""Pass on stealing opportunity."""
|
|
room = await room_manager.get_room(room_code)
|
|
if not room:
|
|
return None
|
|
|
|
# Mark question as answered
|
|
for category_id, questions in room["board"].items():
|
|
for q in questions:
|
|
if q["id"] == question_id:
|
|
q["answered"] = True
|
|
break
|
|
|
|
# Switch back to original team for next selection
|
|
room["current_team"] = "B" if room["current_team"] == "A" else "A"
|
|
room["current_question"] = None
|
|
room["can_steal"] = False
|
|
|
|
await room_manager.update_room(room_code, room)
|
|
return room
|
|
|
|
async def get_current_player(self, room: dict) -> Optional[dict]:
|
|
"""Get the current player who should answer."""
|
|
team = room["current_team"]
|
|
if not team:
|
|
return None
|
|
|
|
players = room["teams"][team]
|
|
if not players:
|
|
return None
|
|
|
|
index = room["current_player_index"][team]
|
|
return players[index % len(players)]
|
|
|
|
def calculate_timer_end(self, time_seconds: int, is_steal: bool = False) -> datetime:
|
|
"""Calculate when the timer should end."""
|
|
if is_steal:
|
|
time_seconds = int(time_seconds * settings.steal_time_multiplier)
|
|
return datetime.utcnow() + timedelta(seconds=time_seconds)
|
|
|
|
|
|
# ============================================================
|
|
# Database Integration Methods
|
|
# ============================================================
|
|
|
|
async def create_db_session(
|
|
self,
|
|
db: AsyncSession,
|
|
room_code: str
|
|
) -> GameSession:
|
|
"""Crea una sesión de juego en PostgreSQL."""
|
|
session = GameSession(
|
|
room_code=room_code,
|
|
status="waiting"
|
|
)
|
|
db.add(session)
|
|
await db.commit()
|
|
await db.refresh(session)
|
|
return session
|
|
|
|
async def get_db_session(
|
|
self,
|
|
db: AsyncSession,
|
|
room_code: str
|
|
) -> Optional[GameSession]:
|
|
"""Obtiene sesión de BD por room_code."""
|
|
result = await db.execute(
|
|
select(GameSession).where(GameSession.room_code == room_code)
|
|
)
|
|
return result.scalar_one_or_none()
|
|
|
|
async def update_db_session(
|
|
self,
|
|
db: AsyncSession,
|
|
room_code: str,
|
|
**kwargs
|
|
) -> Optional[GameSession]:
|
|
"""Actualiza sesión en BD."""
|
|
session = await self.get_db_session(db, room_code)
|
|
if session:
|
|
for key, value in kwargs.items():
|
|
if hasattr(session, key):
|
|
setattr(session, key, value)
|
|
await db.commit()
|
|
await db.refresh(session)
|
|
return session
|
|
|
|
async def start_game_with_db(
|
|
self,
|
|
db: AsyncSession,
|
|
room_code: str
|
|
) -> Optional[dict]:
|
|
"""
|
|
Inicia juego: crea sesión en BD, carga tablero, actualiza Redis.
|
|
"""
|
|
# Crear sesión en BD
|
|
db_session = await self.create_db_session(db, room_code)
|
|
|
|
# Cargar tablero del día
|
|
board = await question_service.get_board_for_game(db)
|
|
|
|
if not board:
|
|
# No hay preguntas para hoy
|
|
return None
|
|
|
|
# Iniciar en Redis (método existente)
|
|
room = await self.start_game(room_code, board)
|
|
|
|
if room:
|
|
# Guardar session_id en Redis para referencia
|
|
room["db_session_id"] = db_session.id
|
|
await room_manager.update_room(room_code, room)
|
|
|
|
# Actualizar BD
|
|
await self.update_db_session(
|
|
db, room_code,
|
|
status="playing"
|
|
)
|
|
|
|
return room
|
|
|
|
async def finish_game(
|
|
self,
|
|
db: AsyncSession,
|
|
room_code: str,
|
|
team_a_score: int,
|
|
team_b_score: int,
|
|
questions_used: list
|
|
) -> Optional[GameSession]:
|
|
"""Finaliza el juego y guarda en BD."""
|
|
session = await self.update_db_session(
|
|
db, room_code,
|
|
status="finished",
|
|
team_a_score=team_a_score,
|
|
team_b_score=team_b_score,
|
|
questions_used=questions_used,
|
|
finished_at=datetime.utcnow()
|
|
)
|
|
return session
|
|
|
|
|
|
# Singleton instance
|
|
game_manager = GameManager()
|