# ============================================== # Breakpilot Drive - Game Session & Quiz DB Methods # ============================================== # Methods for saving/querying game sessions, quiz answers, and basic leaderboard. import json import logging from typing import Optional, List, Dict, Any logger = logging.getLogger(__name__) class SessionsMixin: """Mixin providing game session and quiz database methods for GameDatabase.""" async def save_game_session( self, student_id: str, game_mode: str, duration_seconds: int, distance_traveled: float, score: int, questions_answered: int, questions_correct: int, difficulty_level: int, metadata: Optional[Dict[str, Any]] = None, ) -> Optional[str]: """Save a game session and return the session ID.""" await self._ensure_connected() if not self._pool: return None try: async with self._pool.acquire() as conn: row = await conn.fetchrow( """ INSERT INTO game_sessions ( student_id, game_mode, duration_seconds, distance_traveled, score, questions_answered, questions_correct, difficulty_level, started_at, ended_at, metadata ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW() - make_interval(secs => $3), NOW(), $9) RETURNING id """, student_id, game_mode, duration_seconds, distance_traveled, score, questions_answered, questions_correct, difficulty_level, json.dumps(metadata) if metadata else None ) if row: return str(row["id"]) except Exception as e: logger.error(f"Failed to save game session: {e}") return None async def get_user_sessions( self, student_id: str, limit: int = 10 ) -> List[Dict[str, Any]]: """Get recent game sessions for a user.""" await self._ensure_connected() if not self._pool: return [] try: async with self._pool.acquire() as conn: rows = await conn.fetch( """ SELECT id, student_id, game_mode, duration_seconds, distance_traveled, score, questions_answered, questions_correct, difficulty_level, started_at, ended_at, metadata FROM game_sessions WHERE student_id = $1 ORDER BY ended_at DESC LIMIT $2 """, student_id, limit ) return [ { "session_id": str(row["id"]), "user_id": str(row["student_id"]), "game_mode": row["game_mode"], "duration_seconds": row["duration_seconds"], "distance_traveled": float(row["distance_traveled"]) if row["distance_traveled"] else 0.0, "score": row["score"], "questions_answered": row["questions_answered"], "questions_correct": row["questions_correct"], "difficulty_level": row["difficulty_level"], "started_at": row["started_at"].isoformat() if row["started_at"] else None, "ended_at": row["ended_at"].isoformat() if row["ended_at"] else None, } for row in rows ] except Exception as e: logger.error(f"Failed to get user sessions: {e}") return [] async def get_leaderboard( self, timeframe: str = "day", limit: int = 10 ) -> List[Dict[str, Any]]: """Get leaderboard data.""" await self._ensure_connected() if not self._pool: return [] # Timeframe filter timeframe_sql = { "day": "ended_at > NOW() - INTERVAL '1 day'", "week": "ended_at > NOW() - INTERVAL '7 days'", "month": "ended_at > NOW() - INTERVAL '30 days'", "all": "1=1", }.get(timeframe, "1=1") try: async with self._pool.acquire() as conn: rows = await conn.fetch( f""" SELECT student_id, SUM(score) as total_score FROM game_sessions WHERE {timeframe_sql} GROUP BY student_id ORDER BY total_score DESC LIMIT $1 """, limit ) return [ { "rank": i + 1, "user_id": str(row["student_id"]), "total_score": int(row["total_score"]), } for i, row in enumerate(rows) ] except Exception as e: logger.error(f"Failed to get leaderboard: {e}") return [] async def save_quiz_answer( self, session_id: str, question_id: str, subject: str, difficulty: int, is_correct: bool, answer_time_ms: int, ) -> bool: """Save an individual quiz answer.""" await self._ensure_connected() if not self._pool: return False try: async with self._pool.acquire() as conn: await conn.execute( """ INSERT INTO game_quiz_answers ( session_id, question_id, subject, difficulty, is_correct, answer_time_ms ) VALUES ($1, $2, $3, $4, $5, $6) """, session_id, question_id, subject, difficulty, is_correct, answer_time_ms ) return True except Exception as e: logger.error(f"Failed to save quiz answer: {e}") return False async def get_subject_stats( self, student_id: str ) -> Dict[str, Dict[str, Any]]: """Get per-subject statistics for a student.""" await self._ensure_connected() if not self._pool: return {} try: async with self._pool.acquire() as conn: rows = await conn.fetch( """ SELECT qa.subject, COUNT(*) as total, SUM(CASE WHEN qa.is_correct THEN 1 ELSE 0 END) as correct, AVG(qa.answer_time_ms) as avg_time_ms FROM game_quiz_answers qa JOIN game_sessions gs ON qa.session_id = gs.id WHERE gs.student_id = $1 GROUP BY qa.subject """, student_id ) return { row["subject"]: { "total": row["total"], "correct": row["correct"], "accuracy": row["correct"] / row["total"] if row["total"] > 0 else 0.0, "avg_time_ms": int(row["avg_time_ms"]) if row["avg_time_ms"] else 0, } for row in rows } except Exception as e: logger.error(f"Failed to get subject stats: {e}") return {}