You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

237 lines
9.4 KiB
Python

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'
}