from typing import List, Optional from datetime import datetime, timedelta import math class ScoringEngine: BASE_POINTS_NO_HINT = 2 BASE_POINTS_WITH_HINT = 1 CONSECUTIVE_MULTIPLIER = 1.1 MAX_ATTEMPTS = 3 # Advanced scoring settings DIFFICULTY_MULTIPLIERS = {1: 1.0, 2: 1.5, 3: 2.0} # Easy, Medium, Hard TIME_BONUS_THRESHOLD = 0.5 # 50% of time remaining for bonus MAX_TIME_BONUS = 3 # Maximum bonus points STREAK_BONUS_THRESHOLD = 3 # Minimum streak for bonus MAX_STREAK_BONUS = 5 # Maximum streak bonus @staticmethod def calculate_points( is_correct: bool, hint_used: bool, attempt_number: int, difficulty_level: int = 1, time_taken: Optional[float] = None, time_limit: Optional[float] = None, streak_count: int = 0 ) -> dict: """ Calculate points with advanced scoring features Returns dict with breakdown of scoring """ if not is_correct or attempt_number > ScoringEngine.MAX_ATTEMPTS: return { 'base_points': 0, 'difficulty_bonus': 0, 'time_bonus': 0, 'streak_bonus': 0, 'total_points': 0, 'multipliers_applied': [] } # Base points base_points = ScoringEngine.BASE_POINTS_WITH_HINT if hint_used else ScoringEngine.BASE_POINTS_NO_HINT # Difficulty multiplier difficulty_multiplier = ScoringEngine.DIFFICULTY_MULTIPLIERS.get(difficulty_level, 1.0) difficulty_bonus = int(base_points * (difficulty_multiplier - 1.0)) # Time bonus (if answered quickly) time_bonus = 0 if time_taken and time_limit: time_remaining_ratio = max(0, (time_limit - time_taken) / time_limit) if time_remaining_ratio >= ScoringEngine.TIME_BONUS_THRESHOLD: time_bonus = int(ScoringEngine.MAX_TIME_BONUS * time_remaining_ratio) # Streak bonus streak_bonus = 0 if streak_count >= ScoringEngine.STREAK_BONUS_THRESHOLD: streak_multiplier = min( streak_count / ScoringEngine.STREAK_BONUS_THRESHOLD, ScoringEngine.MAX_STREAK_BONUS ) streak_bonus = int(base_points * streak_multiplier * 0.1) # 10% per streak threshold # Calculate total total_points = base_points + difficulty_bonus + time_bonus + streak_bonus # Track which multipliers were applied multipliers_applied = [] if difficulty_level > 1: multipliers_applied.append(f"Difficulty x{difficulty_multiplier}") if time_bonus > 0: multipliers_applied.append(f"Speed Bonus +{time_bonus}") if streak_bonus > 0: multipliers_applied.append(f"Streak Bonus +{streak_bonus}") return { 'base_points': base_points, 'difficulty_bonus': difficulty_bonus, 'time_bonus': time_bonus, 'streak_bonus': streak_bonus, 'total_points': total_points, 'multipliers_applied': multipliers_applied } @staticmethod def calculate_session_score(answers: List[dict]) -> dict: """ Calculate total session score with advanced features Returns detailed breakdown """ total_score = 0 consecutive_correct = 0 max_streak = 0 total_base_points = 0 total_bonuses = 0 streak_history = [] current_streak = 0 for i, answer in enumerate(answers): scoring_data = answer.get('scoring', {}) is_correct = answer.get('is_correct', False) if is_correct: current_streak += 1 consecutive_correct += 1 max_streak = max(max_streak, current_streak) # Add points with consecutive multiplier base_answer_score = scoring_data.get('total_points', 0) multiplier = ScoringEngine.CONSECUTIVE_MULTIPLIER ** (consecutive_correct - 1) final_answer_score = int(base_answer_score * multiplier) total_score += final_answer_score total_base_points += scoring_data.get('base_points', 0) total_bonuses += (scoring_data.get('difficulty_bonus', 0) + scoring_data.get('time_bonus', 0) + scoring_data.get('streak_bonus', 0)) else: if current_streak > 0: streak_history.append(current_streak) current_streak = 0 consecutive_correct = 0 # Add final streak if game ended on a streak if current_streak > 0: streak_history.append(current_streak) return { 'total_score': total_score, 'base_points': total_base_points, 'bonus_points': total_bonuses, 'max_streak': max_streak, 'final_streak': current_streak, 'streak_history': streak_history, 'questions_answered': len(answers), 'questions_correct': sum(1 for a in answers if a.get('is_correct', False)), 'accuracy_rate': sum(1 for a in answers if a.get('is_correct', False)) / max(len(answers), 1) * 100 } @staticmethod def get_max_session_duration() -> int: return 30 * 60 # 30 minutes in seconds @staticmethod def get_question_timeout() -> int: return 2 * 60 # 2 minutes in seconds @staticmethod def calculate_time_based_score( base_score: int, time_taken: float, time_limit: float, scoring_mode: str = 'linear' ) -> int: """ Calculate time-based scoring bonus Args: base_score: Base points for the question time_taken: Time taken to answer in seconds time_limit: Maximum time allowed in seconds scoring_mode: 'linear', 'exponential', or 'threshold' """ if time_taken >= time_limit: return base_score time_remaining_ratio = (time_limit - time_taken) / time_limit if scoring_mode == 'linear': # Linear bonus: more time remaining = more bonus bonus_multiplier = 1 + (time_remaining_ratio * 0.5) # Up to 50% bonus elif scoring_mode == 'exponential': # Exponential bonus: rewards very fast answers more bonus_multiplier = 1 + (time_remaining_ratio ** 2 * 0.8) # Up to 80% bonus elif scoring_mode == 'threshold': # Threshold bonus: bonus only if answered within certain time bonus_multiplier = 1.3 if time_remaining_ratio >= 0.7 else 1.0 # 30% bonus if answered in first 30% of time else: bonus_multiplier = 1.0 return int(base_score * bonus_multiplier) @staticmethod def get_difficulty_description(level: int) -> dict: """Get human-readable difficulty information""" descriptions = { 1: {'name': 'Easy', 'color': 'success', 'multiplier': '1x', 'description': 'Basic knowledge questions'}, 2: {'name': 'Medium', 'color': 'warning', 'multiplier': '1.5x', 'description': 'Intermediate difficulty'}, 3: {'name': 'Hard', 'color': 'error', 'multiplier': '2x', 'description': 'Challenging expert questions'} } return descriptions.get(level, descriptions[1]) @staticmethod def predict_session_outcome( current_answers: List[dict], questions_remaining: int, time_remaining: int ) -> dict: """ Predict possible session outcomes based on current performance """ if not current_answers: return { 'min_possible_score': 0, 'max_possible_score': questions_remaining * ScoringEngine.BASE_POINTS_NO_HINT * 2, 'projected_score': 0, 'confidence': 'low' } # Current performance metrics accuracy = sum(1 for a in current_answers if a.get('is_correct', False)) / len(current_answers) avg_points_per_question = sum(a.get('scoring', {}).get('total_points', 0) for a in current_answers) / len(current_answers) # Projections conservative_score = int(questions_remaining * avg_points_per_question * 0.8) # 20% discount optimistic_score = int(questions_remaining * avg_points_per_question * 1.2) # 20% premium projected_score = int(questions_remaining * avg_points_per_question) # Current session score current_session = ScoringEngine.calculate_session_score(current_answers) current_total = current_session['total_score'] confidence = 'high' if len(current_answers) >= 5 else 'medium' if len(current_answers) >= 3 else 'low' return { 'current_score': current_total, 'min_possible_final': current_total + conservative_score, 'max_possible_final': current_total + optimistic_score, 'projected_final': current_total + projected_score, 'accuracy_trend': accuracy, 'avg_points_per_question': avg_points_per_question, 'confidence': confidence, 'time_pressure': 'high' if time_remaining < 300 else 'medium' if time_remaining < 900 else 'low' }