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

@@ -8,7 +8,7 @@ import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app.models.base import Base
from app.models import Category, Question, GameSession, GameEvent, Admin
from app.models import Category, Question, GameSession, GameEvent, Admin, Achievement
config = context.config
if config.config_file_name is not None:

View File

@@ -0,0 +1,44 @@
"""add_achievements_table
Revision ID: f207a5a45cfa
Revises: 65d30b7402cf
Create Date: 2026-01-26 08:14:59.697355
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'f207a5a45cfa'
down_revision: Union[str, None] = '65d30b7402cf'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('achievements',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=100), nullable=False),
sa.Column('description', sa.Text(), nullable=False),
sa.Column('icon', sa.String(length=10), nullable=False),
sa.Column('condition_type', sa.String(length=50), nullable=False),
sa.Column('condition_value', sa.Integer(), nullable=True),
sa.Column('category_id', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
sa.ForeignKeyConstraint(['category_id'], ['categories.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_index(op.f('ix_achievements_id'), 'achievements', ['id'], unique=False)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_achievements_id'), table_name='achievements')
op.drop_table('achievements')
# ### end Alembic commands ###

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

View File

@@ -3,5 +3,6 @@ from app.models.question import Question
from app.models.game_session import GameSession
from app.models.game_event import GameEvent
from app.models.admin import Admin
from app.models.achievement import Achievement
__all__ = ["Category", "Question", "GameSession", "GameEvent", "Admin"]
__all__ = ["Category", "Question", "GameSession", "GameEvent", "Admin", "Achievement"]

View File

@@ -0,0 +1,22 @@
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, func
from sqlalchemy.orm import relationship
from app.models.base import Base
class Achievement(Base):
__tablename__ = "achievements"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(100), nullable=False, unique=True)
description = Column(Text, nullable=False)
icon = Column(String(10), nullable=False)
condition_type = Column(String(50), nullable=False)
condition_value = Column(Integer, default=1)
category_id = Column(Integer, ForeignKey("categories.id"), nullable=True)
created_at = Column(DateTime, server_default=func.now())
# Relationships
category = relationship("Category")
def __repr__(self):
return f"<Achievement(id={self.id}, name='{self.name}')>"

View File

@@ -8,10 +8,20 @@ from app.schemas.game import (
StealAttempt
)
from app.schemas.admin import AdminCreate, AdminLogin, Token
from app.schemas.achievement import (
AchievementBase,
AchievementCreate,
AchievementResponse,
PlayerAchievement,
AchievementUnlock,
PlayerStats
)
__all__ = [
"QuestionCreate", "QuestionUpdate", "QuestionResponse",
"RoomCreate", "RoomJoin", "PlayerInfo", "GameState",
"AnswerSubmit", "StealAttempt",
"AdminCreate", "AdminLogin", "Token"
"AdminCreate", "AdminLogin", "Token",
"AchievementBase", "AchievementCreate", "AchievementResponse",
"PlayerAchievement", "AchievementUnlock", "PlayerStats"
]

View File

@@ -0,0 +1,50 @@
from pydantic import BaseModel
from typing import Optional, List
from datetime import datetime
class AchievementBase(BaseModel):
name: str
description: str
icon: str
condition_type: str
condition_value: int = 1
category_id: Optional[int] = None
class AchievementCreate(AchievementBase):
pass
class AchievementResponse(AchievementBase):
id: int
created_at: datetime
class Config:
from_attributes = True
class PlayerAchievement(BaseModel):
"""Logro desbloqueado por un jugador (almacenado en localStorage)"""
achievement_id: int
unlocked_at: datetime
game_session_id: Optional[int] = None
class AchievementUnlock(BaseModel):
"""Evento emitido cuando se desbloquea un logro"""
player_name: str
achievement: AchievementResponse
unlocked_at: datetime
class PlayerStats(BaseModel):
"""Estadisticas de jugador para calculo de logros"""
player_name: str
current_streak: int = 0
total_correct: int = 0
total_steals: int = 0
successful_steals: int = 0
category_correct: dict[int, int] = {} # category_id -> count
fastest_answer_seconds: Optional[float] = None
questions_500_correct: int = 0

View File

@@ -2,5 +2,14 @@ from app.services.ai_validator import AIValidator
from app.services.ai_generator import AIGenerator
from app.services.game_manager import GameManager
from app.services.room_manager import RoomManager
from app.services.replay_manager import ReplayManager
from app.services.achievement_manager import AchievementManager
__all__ = ["AIValidator", "AIGenerator", "GameManager", "RoomManager"]
__all__ = [
"AIValidator",
"AIGenerator",
"GameManager",
"RoomManager",
"ReplayManager",
"AchievementManager",
]

View File

@@ -0,0 +1,245 @@
from typing import Optional, List
from datetime import datetime
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.achievement import Achievement
from app.schemas.achievement import PlayerStats, AchievementUnlock, AchievementResponse
class AchievementManager:
"""Servicio para detectar y otorgar logros a jugadores durante las partidas."""
async def get_all_achievements(self, db: AsyncSession) -> List[Achievement]:
"""Obtiene todos los logros disponibles de la base de datos."""
result = await db.execute(select(Achievement))
return list(result.scalars().all())
async def check_achievements(
self,
db: AsyncSession,
player_stats: PlayerStats,
game_context: dict
) -> List[AchievementUnlock]:
"""
Verifica que logros ha desbloqueado el jugador.
Args:
db: Sesion de base de datos
player_stats: Estadisticas actuales del jugador
game_context: {
'won': bool,
'team_score': int,
'opponent_score': int,
'max_deficit_overcome': int, # para comeback
'categories_swept': List[int], # IDs de categorias con 5/5
'no_mistakes': bool, # para perfect_game
}
Returns:
Lista de logros desbloqueados
"""
achievements = await self.get_all_achievements(db)
unlocked: List[AchievementUnlock] = []
now = datetime.utcnow()
for achievement in achievements:
is_unlocked = self._evaluate_condition(
achievement=achievement,
stats=player_stats,
game_context=game_context
)
if is_unlocked:
achievement_response = AchievementResponse(
id=achievement.id,
name=achievement.name,
description=achievement.description,
icon=achievement.icon,
condition_type=achievement.condition_type,
condition_value=achievement.condition_value,
category_id=achievement.category_id,
created_at=achievement.created_at
)
unlocked.append(AchievementUnlock(
player_name=player_stats.player_name,
achievement=achievement_response,
unlocked_at=now
))
return unlocked
def _evaluate_condition(
self,
achievement: Achievement,
stats: PlayerStats,
game_context: dict
) -> bool:
"""Evalua si se cumple la condicion de un logro."""
condition_type = achievement.condition_type
condition_value = achievement.condition_value
if condition_type == "first_win":
return game_context.get("won", False)
elif condition_type == "streak":
return self._check_streak(stats, condition_value)
elif condition_type == "steal_success":
return self._check_steal_success(stats, condition_value)
elif condition_type == "category_specialist":
if achievement.category_id is not None:
return self._check_category_specialist(
stats, achievement.category_id, condition_value
)
return False
elif condition_type == "perfect_game":
return (
game_context.get("won", False) and
game_context.get("no_mistakes", False)
)
elif condition_type == "fast_answer":
return self._check_fast_answer(stats, condition_value)
elif condition_type == "comeback":
return (
game_context.get("won", False) and
game_context.get("max_deficit_overcome", 0) >= condition_value
)
elif condition_type == "category_sweep":
categories_swept = game_context.get("categories_swept", [])
return len(categories_swept) >= condition_value
elif condition_type == "high_stakes":
return self._check_high_stakes(stats, condition_value)
return False
def _check_streak(self, stats: PlayerStats, required: int) -> bool:
"""Verifica racha de respuestas correctas."""
return stats.current_streak >= required
def _check_steal_success(self, stats: PlayerStats, required: int) -> bool:
"""Verifica robos exitosos."""
return stats.successful_steals >= required
def _check_category_specialist(
self,
stats: PlayerStats,
category_id: int,
required: int
) -> bool:
"""Verifica especialista de categoria."""
return stats.category_correct.get(category_id, 0) >= required
def _check_fast_answer(self, stats: PlayerStats, max_seconds: int) -> bool:
"""Verifica respuesta rapida."""
if stats.fastest_answer_seconds is None:
return False
return stats.fastest_answer_seconds <= max_seconds
def _check_high_stakes(self, stats: PlayerStats, required: int) -> bool:
"""Verifica preguntas de 500 pts correctas."""
return stats.questions_500_correct >= required
def update_stats_on_answer(
self,
stats: PlayerStats,
was_correct: bool,
was_steal: bool,
category_id: int,
points: int,
answer_time_seconds: float
) -> PlayerStats:
"""
Actualiza estadisticas despues de una respuesta.
Args:
stats: Estadisticas actuales del jugador
was_correct: Si la respuesta fue correcta
was_steal: Si fue un intento de robo
category_id: ID de la categoria de la pregunta
points: Puntos de la pregunta
answer_time_seconds: Tiempo de respuesta en segundos
Returns:
PlayerStats actualizado
"""
# Crear copia mutable de las estadisticas
updated_category_correct = dict(stats.category_correct)
if was_correct:
# Actualizar racha
new_streak = stats.current_streak + 1
new_total_correct = stats.total_correct + 1
# Actualizar correctas por categoria
updated_category_correct[category_id] = (
updated_category_correct.get(category_id, 0) + 1
)
# Actualizar tiempo mas rapido
new_fastest = stats.fastest_answer_seconds
if new_fastest is None or answer_time_seconds < new_fastest:
new_fastest = answer_time_seconds
# Actualizar preguntas de 500 pts
new_500_correct = stats.questions_500_correct
if points == 500:
new_500_correct += 1
# Actualizar robos exitosos
new_successful_steals = stats.successful_steals
new_total_steals = stats.total_steals
if was_steal:
new_successful_steals += 1
new_total_steals += 1
return PlayerStats(
player_name=stats.player_name,
current_streak=new_streak,
total_correct=new_total_correct,
total_steals=new_total_steals,
successful_steals=new_successful_steals,
category_correct=updated_category_correct,
fastest_answer_seconds=new_fastest,
questions_500_correct=new_500_correct
)
else:
# Respuesta incorrecta - resetear racha
new_total_steals = stats.total_steals
if was_steal:
new_total_steals += 1
return PlayerStats(
player_name=stats.player_name,
current_streak=0,
total_correct=stats.total_correct,
total_steals=new_total_steals,
successful_steals=stats.successful_steals,
category_correct=updated_category_correct,
fastest_answer_seconds=stats.fastest_answer_seconds,
questions_500_correct=stats.questions_500_correct
)
def create_initial_stats(self, player_name: str) -> PlayerStats:
"""Crea estadisticas iniciales para un jugador."""
return PlayerStats(
player_name=player_name,
current_streak=0,
total_correct=0,
total_steals=0,
successful_steals=0,
category_correct={},
fastest_answer_seconds=None,
questions_500_correct=0
)
# Singleton instance
achievement_manager = AchievementManager()

View File

@@ -0,0 +1,219 @@
import hashlib
from typing import Optional, List
from datetime import datetime
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.game_session import GameSession
from app.models.game_event import GameEvent
class ReplayManager:
def generate_replay_code(self, session_id: int) -> str:
"""
Genera codigo unico de replay basado en session_id.
Args:
session_id: ID de la sesion de juego
Returns:
Codigo de replay de 8 caracteres
"""
# Usar hash corto del session_id + timestamp actual
timestamp = datetime.utcnow().isoformat()
data = f"{session_id}:{timestamp}"
hash_digest = hashlib.sha256(data.encode()).hexdigest()
# Retornar los primeros 8 caracteres en mayusculas
return hash_digest[:8].upper()
async def save_game_event(
self,
db: AsyncSession,
session_id: int,
event_type: str,
player_name: str,
team: str,
question_id: Optional[int] = None,
answer_given: Optional[str] = None,
was_correct: Optional[bool] = None,
was_steal: bool = False,
points_earned: int = 0
) -> GameEvent:
"""
Guarda un evento de juego para replay.
Args:
db: Sesion de base de datos
session_id: ID de la sesion de juego
event_type: Tipo de evento (question_selected, answer_submitted,
steal_attempted, steal_passed, game_finished)
player_name: Nombre del jugador
team: Equipo ('A' o 'B')
question_id: ID de la pregunta (opcional)
answer_given: Respuesta dada por el jugador (opcional)
was_correct: Si la respuesta fue correcta (opcional)
was_steal: Si fue un intento de robo
points_earned: Puntos ganados
Returns:
GameEvent creado
"""
event = GameEvent(
session_id=session_id,
event_type=event_type,
player_name=player_name,
team=team,
question_id=question_id,
answer_given=answer_given,
was_correct=was_correct,
was_steal=was_steal,
points_earned=points_earned
)
db.add(event)
await db.commit()
await db.refresh(event)
return event
async def get_replay(
self,
db: AsyncSession,
replay_code: str
) -> Optional[dict]:
"""
Obtiene replay completo por codigo.
Args:
db: Sesion de base de datos
replay_code: Codigo del replay
Returns:
Diccionario con datos del replay o None si no existe
"""
# Buscar session por room_code que contiene el replay_code
# El replay_code puede estar almacenado en room_code o como sufijo
result = await db.execute(
select(GameSession).where(
GameSession.room_code.contains(replay_code)
)
)
session = result.scalar_one_or_none()
if not session:
# Intentar busqueda exacta
result = await db.execute(
select(GameSession).where(
GameSession.room_code == replay_code
)
)
session = result.scalar_one_or_none()
if not session:
return None
return await self.get_replay_by_session(db, session.id)
async def get_replay_by_session(
self,
db: AsyncSession,
session_id: int
) -> Optional[dict]:
"""
Obtiene replay por session_id.
Args:
db: Sesion de base de datos
session_id: ID de la sesion
Returns:
Diccionario con datos del replay o None si no existe
"""
# Obtener la sesion
result = await db.execute(
select(GameSession).where(GameSession.id == session_id)
)
session = result.scalar_one_or_none()
if not session:
return None
# Obtener todos los eventos ordenados por timestamp
events_result = await db.execute(
select(GameEvent)
.where(GameEvent.session_id == session_id)
.order_by(GameEvent.timestamp.asc())
)
events = events_result.scalars().all()
return self.format_replay_for_frontend(session, list(events))
def format_replay_for_frontend(
self,
session: GameSession,
events: List[GameEvent]
) -> dict:
"""
Formatea replay para consumo del frontend.
Args:
session: Sesion de juego
events: Lista de eventos ordenados cronologicamente
Returns:
Diccionario formateado para el frontend
"""
# Formatear eventos para el frontend
formatted_events = []
for event in events:
formatted_event = {
"id": event.id,
"event_type": event.event_type,
"player_name": event.player_name,
"team": event.team,
"question_id": event.question_id,
"answer_given": event.answer_given,
"was_correct": event.was_correct,
"was_steal": event.was_steal,
"points_earned": event.points_earned,
"timestamp": event.timestamp.isoformat() if event.timestamp else None
}
formatted_events.append(formatted_event)
# Determinar ganador
winner = None
if session.status == "finished":
if session.team_a_score > session.team_b_score:
winner = "A"
elif session.team_b_score > session.team_a_score:
winner = "B"
else:
winner = "tie"
# Calcular duracion si la partida termino
duration_seconds = None
if session.finished_at and session.created_at:
duration = session.finished_at - session.created_at
duration_seconds = int(duration.total_seconds())
return {
"metadata": {
"session_id": session.id,
"room_code": session.room_code,
"status": session.status,
"created_at": session.created_at.isoformat() if session.created_at else None,
"finished_at": session.finished_at.isoformat() if session.finished_at else None,
"duration_seconds": duration_seconds
},
"final_scores": {
"team_a": session.team_a_score,
"team_b": session.team_b_score
},
"winner": winner,
"events": formatted_events,
"event_count": len(formatted_events)
}
# Singleton instance
replay_manager = ReplayManager()

View File

@@ -0,0 +1,176 @@
import asyncio
import sys
sys.path.insert(0, '/root/WebTriviasMulti/backend')
from sqlalchemy import select
from app.models.base import get_async_session
from app.models.achievement import Achievement
ACHIEVEMENTS = [
{
"name": "Primera Victoria",
"description": "Gana tu primera partida",
"icon": "🏆",
"condition_type": "first_win",
"condition_value": 1,
"category_id": None
},
{
"name": "Racha de 3",
"description": "Responde 3 correctas seguidas",
"icon": "🔥",
"condition_type": "streak",
"condition_value": 3,
"category_id": None
},
{
"name": "Racha de 5",
"description": "Responde 5 correctas seguidas",
"icon": "🔥🔥",
"condition_type": "streak",
"condition_value": 5,
"category_id": None
},
{
"name": "Ladrón Novato",
"description": "Primer robo exitoso",
"icon": "🦝",
"condition_type": "steal_success",
"condition_value": 1,
"category_id": None
},
{
"name": "Ladrón Maestro",
"description": "5 robos exitosos en una partida",
"icon": "🦝👑",
"condition_type": "steal_success",
"condition_value": 5,
"category_id": None
},
{
"name": "Especialista Nintendo",
"description": "10 correctas en Nintendo",
"icon": "🍄",
"condition_type": "category_specialist",
"condition_value": 10,
"category_id": 1
},
{
"name": "Especialista Xbox",
"description": "10 correctas en Xbox",
"icon": "🎮",
"condition_type": "category_specialist",
"condition_value": 10,
"category_id": 2
},
{
"name": "Especialista PlayStation",
"description": "10 correctas en PlayStation",
"icon": "🎯",
"condition_type": "category_specialist",
"condition_value": 10,
"category_id": 3
},
{
"name": "Especialista Anime",
"description": "10 correctas en Anime",
"icon": "⛩️",
"condition_type": "category_specialist",
"condition_value": 10,
"category_id": 4
},
{
"name": "Especialista Música",
"description": "10 correctas en Música",
"icon": "🎵",
"condition_type": "category_specialist",
"condition_value": 10,
"category_id": 5
},
{
"name": "Especialista Películas",
"description": "10 correctas en Películas",
"icon": "🎬",
"condition_type": "category_specialist",
"condition_value": 10,
"category_id": 6
},
{
"name": "Especialista Libros",
"description": "10 correctas en Libros",
"icon": "📚",
"condition_type": "category_specialist",
"condition_value": 10,
"category_id": 7
},
{
"name": "Especialista Historia",
"description": "10 correctas en Historia-Cultura",
"icon": "🏛️",
"condition_type": "category_specialist",
"condition_value": 10,
"category_id": 8
},
{
"name": "Invicto",
"description": "Gana sin fallar ninguna pregunta",
"icon": "",
"condition_type": "perfect_game",
"condition_value": 1,
"category_id": None
},
{
"name": "Velocista",
"description": "Responde correctamente en menos de 3 segundos",
"icon": "",
"condition_type": "fast_answer",
"condition_value": 3,
"category_id": None
},
{
"name": "Comeback",
"description": "Gana estando 500+ puntos abajo",
"icon": "🔄",
"condition_type": "comeback",
"condition_value": 500,
"category_id": None
},
{
"name": "Dominio Total",
"description": "Responde las 5 preguntas de una categoría correctamente",
"icon": "👑",
"condition_type": "category_sweep",
"condition_value": 5,
"category_id": None
},
{
"name": "Arriesgado",
"description": "Responde correctamente 3 preguntas de 500 pts",
"icon": "🎰",
"condition_type": "high_stakes",
"condition_value": 3,
"category_id": None
},
]
async def seed_achievements():
AsyncSessionLocal = get_async_session()
async with AsyncSessionLocal() as session:
# Verificar si ya existen
result = await session.execute(select(Achievement))
if result.scalars().first():
print("Achievements ya existen, saltando seed...")
return
# Insertar achievements
for data in ACHIEVEMENTS:
achievement = Achievement(**data)
session.add(achievement)
await session.commit()
print(f"Insertados {len(ACHIEVEMENTS)} achievements")
if __name__ == "__main__":
asyncio.run(seed_achievements())