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,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