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