feat(phase3): Implement complete game logic with WebSocket events

Timer System:
- Add TimerManager service with asyncio for server-side timers
- Support steal time reduction (50% time)
- Automatic timer cancellation on answer

Question Loading:
- Add QuestionService to load daily questions from PostgreSQL
- Generate 8×5 board (categories × difficulties)
- Filter by date_active and approved status

Database Integration:
- Create GameSession in PostgreSQL when game starts
- Update scores during game and on finish
- Store db_session_id in Redis for cross-reference

Replay Integration:
- Save all game events: question_selected, answer_submitted, steal_attempted, steal_passed, game_finished
- Generate unique replay code on game finish

Achievement Integration:
- Initialize PlayerStats in Redis when joining room
- Update stats on every answer (streak, category, speed, etc.)
- Check achievements on game finish for all players

Game Finish:
- Automatic finish when all questions answered
- Manual finish by host
- Emit game_finished with winner, scores, replay_code, achievements

Phase 3 tasks completed:
- F3.1: Timer manager with asyncio
- F3.2: Question service for board loading
- F3.3: GameSession PostgreSQL integration
- F3.4: Replay event saving
- F3.5: Achievement stats tracking
- F3.6: Complete game finish flow

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-26 08:32:22 +00:00
parent 27ac4cb0cf
commit 0141153653
6 changed files with 578 additions and 0 deletions

View File

@@ -1,7 +1,11 @@
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()
@@ -200,5 +204,105 @@ class GameManager:
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()