feat(phase2): Add achievements and replay systems

Achievement System:
- Add Achievement model with condition types (streak, steal, specialist, etc.)
- Add AchievementManager service for tracking and awarding achievements
- Add Pydantic schemas for achievements (AchievementResponse, PlayerStats, etc.)
- Seed 18 achievements from design doc
- Add GET /api/game/achievements endpoint

Replay System:
- Add ReplayManager service for saving/loading game replays
- Add GET /api/replay/{code} and /api/replay/session/{id} endpoints
- Format replays for frontend consumption

Phase 2 tasks completed:
- F2.1: Achievement model and migration
- F2.2: Pydantic schemas
- F2.3: AchievementManager service
- F2.4: ReplayManager service
- F2.5: API endpoints
- F2.6: Seed 18 achievements data

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-26 08:24:02 +00:00
parent b3fab9f8df
commit 27ac4cb0cf
12 changed files with 804 additions and 131 deletions

View File

@@ -1,4 +1,4 @@
from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, HTTPException, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from datetime import date
@@ -8,6 +8,8 @@ from app.models.base import get_db
from app.models.question import Question
from app.models.category import Category
from app.schemas.game import RoomCreate, RoomJoin, GameState
from app.services.achievement_manager import achievement_manager
from app.schemas.achievement import AchievementResponse
router = APIRouter()
@@ -94,26 +96,8 @@ async def get_question(question_id: int):
}
@router.get("/achievements")
async def get_achievements():
"""Get list of all available achievements."""
return [
{"id": 1, "name": "Primera Victoria", "description": "Ganar tu primera partida", "icon": "🏆"},
{"id": 2, "name": "Racha de 3", "description": "Responder 3 correctas seguidas", "icon": "🔥"},
{"id": 3, "name": "Racha de 5", "description": "Responder 5 correctas seguidas", "icon": "🔥🔥"},
{"id": 4, "name": "Ladrón Novato", "description": "Primer robo exitoso", "icon": "🦝"},
{"id": 5, "name": "Ladrón Maestro", "description": "5 robos exitosos en una partida", "icon": "🦝👑"},
{"id": 6, "name": "Especialista Nintendo", "description": "10 correctas en Nintendo", "icon": "🍄"},
{"id": 7, "name": "Especialista Xbox", "description": "10 correctas en Xbox", "icon": "🎮"},
{"id": 8, "name": "Especialista PlayStation", "description": "10 correctas en PlayStation", "icon": "🎯"},
{"id": 9, "name": "Especialista Anime", "description": "10 correctas en Anime", "icon": "⛩️"},
{"id": 10, "name": "Especialista Música", "description": "10 correctas en Música", "icon": "🎵"},
{"id": 11, "name": "Especialista Películas", "description": "10 correctas en Películas", "icon": "🎬"},
{"id": 12, "name": "Especialista Libros", "description": "10 correctas en Libros", "icon": "📚"},
{"id": 13, "name": "Especialista Historia", "description": "10 correctas en Historia-Cultura", "icon": "🏛️"},
{"id": 14, "name": "Invicto", "description": "Ganar sin fallar ninguna pregunta", "icon": ""},
{"id": 15, "name": "Velocista", "description": "Responder correctamente en menos de 3 segundos", "icon": ""},
{"id": 16, "name": "Comeback", "description": "Ganar estando 500+ puntos abajo", "icon": "🔄"},
{"id": 17, "name": "Dominio Total", "description": "Responder las 5 preguntas de una categoría", "icon": "👑"},
{"id": 18, "name": "Arriesgado", "description": "Responder correctamente 3 preguntas de 500 pts", "icon": "🎰"},
]
@router.get("/achievements", response_model=list[AchievementResponse])
async def get_achievements(db: AsyncSession = Depends(get_db)):
"""Obtiene lista de todos los logros disponibles"""
achievements = await achievement_manager.get_all_achievements(db)
return achievements

View File

@@ -1,113 +1,26 @@
from fastapi import APIRouter, HTTPException, Depends
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from typing import List
from typing import Optional
from app.models.base import get_db
from app.models.game_session import GameSession
from app.models.game_event import GameEvent
from app.services.replay_manager import replay_manager
router = APIRouter()
@router.get("/{session_id}")
async def get_replay(
session_id: int,
db: AsyncSession = Depends(get_db)
):
"""
Get replay data for a game session.
Returns all events in chronological order.
"""
# Get session
result = await db.execute(
select(GameSession).where(GameSession.id == session_id)
)
session = result.scalar_one_or_none()
if not session:
@router.get("/session/{session_id}")
async def get_replay_by_session(session_id: int, db: AsyncSession = Depends(get_db)):
"""Obtiene un replay por session_id"""
replay = await replay_manager.get_replay_by_session(db, session_id)
if not replay:
raise HTTPException(status_code=404, detail="Session not found")
# Get all events
events_result = await db.execute(
select(GameEvent)
.where(GameEvent.session_id == session_id)
.order_by(GameEvent.timestamp)
)
events = events_result.scalars().all()
return {
"session": {
"id": session.id,
"room_code": session.room_code,
"team_a_score": session.team_a_score,
"team_b_score": session.team_b_score,
"status": session.status,
"created_at": session.created_at,
"finished_at": session.finished_at
},
"events": [
{
"id": e.id,
"event_type": e.event_type,
"player_name": e.player_name,
"team": e.team,
"question_id": e.question_id,
"answer_given": e.answer_given,
"was_correct": e.was_correct,
"was_steal": e.was_steal,
"points_earned": e.points_earned,
"timestamp": e.timestamp
}
for e in events
]
}
return replay
@router.get("/code/{room_code}")
async def get_replay_by_code(
room_code: str,
db: AsyncSession = Depends(get_db)
):
"""
Get replay data by room code.
"""
result = await db.execute(
select(GameSession).where(GameSession.room_code == room_code)
)
session = result.scalar_one_or_none()
if not session:
raise HTTPException(status_code=404, detail="Session not found")
return await get_replay(session.id, db)
@router.get("/")
async def list_replays(
limit: int = 20,
offset: int = 0,
db: AsyncSession = Depends(get_db)
):
"""
List recent finished game sessions.
"""
result = await db.execute(
select(GameSession)
.where(GameSession.status == "finished")
.order_by(GameSession.finished_at.desc())
.offset(offset)
.limit(limit)
)
sessions = result.scalars().all()
return [
{
"id": s.id,
"room_code": s.room_code,
"team_a_score": s.team_a_score,
"team_b_score": s.team_b_score,
"finished_at": s.finished_at
}
for s in sessions
]
@router.get("/{replay_code}")
async def get_replay(replay_code: str, db: AsyncSession = Depends(get_db)):
"""Obtiene un replay por su codigo"""
replay = await replay_manager.get_replay(db, replay_code)
if not replay:
raise HTTPException(status_code=404, detail="Replay not found")
return replay