package valueobjects import ( "fmt" "time" "knowfoolery/backend/shared/types" ) // UserStatistics tracks comprehensive user statistics type UserStatistics struct { // Game statistics totalGamesPlayed int64 totalGamesCompleted int64 totalGamesAbandoned int64 totalGamesTimedOut int64 // Question statistics totalQuestionsAttempted int64 totalQuestionsCorrect int64 totalQuestionsIncorrect int64 totalQuestionsTimeout int64 // Score statistics totalScore int64 highestScore int averageScore float64 bestStreak int // Best consecutive correct answers currentStreak int // Current consecutive correct answers // Time statistics totalPlayTime types.Duration averageSessionTime types.Duration fastestCorrectAnswer types.Duration slowestCorrectAnswer types.Duration // Hint statistics totalHintsUsed int64 hintsUsedPercentage float64 // Theme performance themePerformance map[types.ThemeID]*ThemePerformance // Difficulty performance difficultyPerformance map[types.DifficultyLevel]*DifficultyPerformance // Achievement tracking achievements []string // Activity tracking loginCount int64 lastLoginAt *types.Timestamp longestLoginStreak int currentLoginStreak int // Recent activity (last 30 days) recentGamesPlayed int64 recentCorrectRate float64 recentAverageScore float64 // Ranking information globalRank *int weeklyRank *int monthlyRank *int // Improvement tracking improvementRate float64 // Rate of improvement over time skillLevel string // "beginner", "intermediate", "advanced", "expert" // Update tracking lastCalculated types.Timestamp updatedAt types.Timestamp } // ThemePerformance tracks performance in a specific theme type ThemePerformance struct { ThemeID types.ThemeID `json:"theme_id"` QuestionsAttempted int64 `json:"questions_attempted"` QuestionsCorrect int64 `json:"questions_correct"` CorrectRate float64 `json:"correct_rate"` AverageScore float64 `json:"average_score"` BestScore int `json:"best_score"` LastPlayed *types.Timestamp `json:"last_played,omitempty"` } // DifficultyPerformance tracks performance at a specific difficulty level type DifficultyPerformance struct { Difficulty types.DifficultyLevel `json:"difficulty"` QuestionsAttempted int64 `json:"questions_attempted"` QuestionsCorrect int64 `json:"questions_correct"` CorrectRate float64 `json:"correct_rate"` AverageScore float64 `json:"average_score"` BestScore int `json:"best_score"` LastPlayed *types.Timestamp `json:"last_played,omitempty"` } // NewUserStatistics creates new user statistics func NewUserStatistics() *UserStatistics { now := types.NewTimestamp() return &UserStatistics{ totalGamesPlayed: 0, totalGamesCompleted: 0, totalGamesAbandoned: 0, totalGamesTimedOut: 0, totalQuestionsAttempted: 0, totalQuestionsCorrect: 0, totalQuestionsIncorrect: 0, totalQuestionsTimeout: 0, totalScore: 0, highestScore: 0, averageScore: 0.0, bestStreak: 0, currentStreak: 0, totalPlayTime: types.NewDuration(0), averageSessionTime: types.NewDuration(0), fastestCorrectAnswer: types.NewDuration(0), slowestCorrectAnswer: types.NewDuration(0), totalHintsUsed: 0, hintsUsedPercentage: 0.0, themePerformance: make(map[types.ThemeID]*ThemePerformance), difficultyPerformance: make(map[types.DifficultyLevel]*DifficultyPerformance), achievements: []string{}, loginCount: 0, lastLoginAt: nil, longestLoginStreak: 0, currentLoginStreak: 0, recentGamesPlayed: 0, recentCorrectRate: 0.0, recentAverageScore: 0.0, globalRank: nil, weeklyRank: nil, monthlyRank: nil, improvementRate: 0.0, skillLevel: "beginner", lastCalculated: now, updatedAt: now, } } // Getters // TotalGamesPlayed returns total games played func (s *UserStatistics) TotalGamesPlayed() int64 { return s.totalGamesPlayed } // TotalGamesCompleted returns total games completed func (s *UserStatistics) TotalGamesCompleted() int64 { return s.totalGamesCompleted } // TotalQuestionsAttempted returns total questions attempted func (s *UserStatistics) TotalQuestionsAttempted() int64 { return s.totalQuestionsAttempted } // TotalQuestionsCorrect returns total questions answered correctly func (s *UserStatistics) TotalQuestionsCorrect() int64 { return s.totalQuestionsCorrect } // TotalScore returns total score accumulated func (s *UserStatistics) TotalScore() int64 { return s.totalScore } // HighestScore returns the highest score achieved func (s *UserStatistics) HighestScore() int { return s.highestScore } // AverageScore returns the average score func (s *UserStatistics) AverageScore() float64 { return s.averageScore } // CorrectRate returns the overall correct answer rate func (s *UserStatistics) CorrectRate() float64 { if s.totalQuestionsAttempted == 0 { return 0.0 } return float64(s.totalQuestionsCorrect) / float64(s.totalQuestionsAttempted) } // BestStreak returns the best consecutive correct answers func (s *UserStatistics) BestStreak() int { return s.bestStreak } // CurrentStreak returns current consecutive correct answers func (s *UserStatistics) CurrentStreak() int { return s.currentStreak } // TotalPlayTime returns total time spent playing func (s *UserStatistics) TotalPlayTime() types.Duration { return s.totalPlayTime } // HintsUsedPercentage returns percentage of questions where hints were used func (s *UserStatistics) HintsUsedPercentage() float64 { return s.hintsUsedPercentage } // ThemePerformance returns performance for a specific theme func (s *UserStatistics) ThemePerformance(themeID types.ThemeID) *ThemePerformance { performance, exists := s.themePerformance[themeID] if !exists { return nil } // Return copy return &ThemePerformance{ ThemeID: performance.ThemeID, QuestionsAttempted: performance.QuestionsAttempted, QuestionsCorrect: performance.QuestionsCorrect, CorrectRate: performance.CorrectRate, AverageScore: performance.AverageScore, BestScore: performance.BestScore, LastPlayed: performance.LastPlayed, } } // DifficultyPerformance returns performance for a specific difficulty func (s *UserStatistics) DifficultyPerformance(difficulty types.DifficultyLevel) *DifficultyPerformance { performance, exists := s.difficultyPerformance[difficulty] if !exists { return nil } // Return copy return &DifficultyPerformance{ Difficulty: performance.Difficulty, QuestionsAttempted: performance.QuestionsAttempted, QuestionsCorrect: performance.QuestionsCorrect, CorrectRate: performance.CorrectRate, AverageScore: performance.AverageScore, BestScore: performance.BestScore, LastPlayed: performance.LastPlayed, } } // Achievements returns list of achievements func (s *UserStatistics) Achievements() []string { return append([]string(nil), s.achievements...) // Return copy } // LoginCount returns total login count func (s *UserStatistics) LoginCount() int64 { return s.loginCount } // SkillLevel returns current skill level func (s *UserStatistics) SkillLevel() string { return s.skillLevel } // GlobalRank returns global ranking if available func (s *UserStatistics) GlobalRank() *int { return s.globalRank } // UpdatedAt returns last update timestamp func (s *UserStatistics) UpdatedAt() types.Timestamp { return s.updatedAt } // Business methods // RecordGameStart records the start of a new game func (s *UserStatistics) RecordGameStart() { s.totalGamesPlayed++ s.markUpdated() } // RecordGameCompleted records a completed game func (s *UserStatistics) RecordGameCompleted(score int, questionsCorrect int, questionsTotal int, playTime types.Duration) { s.totalGamesCompleted++ s.totalScore += int64(score) if score > s.highestScore { s.highestScore = score } s.totalQuestionsAttempted += int64(questionsTotal) s.totalQuestionsCorrect += int64(questionsCorrect) s.totalQuestionsIncorrect += int64(questionsTotal - questionsCorrect) s.totalPlayTime = types.NewDuration(s.totalPlayTime.Duration + playTime.Duration) s.recalculate() } // RecordGameAbandoned records an abandoned game func (s *UserStatistics) RecordGameAbandoned() { s.totalGamesAbandoned++ s.currentStreak = 0 // Reset current streak on abandonment s.markUpdated() } // RecordGameTimedOut records a timed out game func (s *UserStatistics) RecordGameTimedOut() { s.totalGamesTimedOut++ s.currentStreak = 0 // Reset current streak on timeout s.markUpdated() } // RecordCorrectAnswer records a correct answer func (s *UserStatistics) RecordCorrectAnswer(hintUsed bool, timeTaken types.Duration) { s.totalQuestionsAttempted++ s.totalQuestionsCorrect++ s.currentStreak++ if s.currentStreak > s.bestStreak { s.bestStreak = s.currentStreak } if hintUsed { s.totalHintsUsed++ } // Update answer time records if s.fastestCorrectAnswer.Duration == 0 || timeTaken.Duration < s.fastestCorrectAnswer.Duration { s.fastestCorrectAnswer = timeTaken } if timeTaken.Duration > s.slowestCorrectAnswer.Duration { s.slowestCorrectAnswer = timeTaken } s.recalculate() } // RecordIncorrectAnswer records an incorrect answer func (s *UserStatistics) RecordIncorrectAnswer(hintUsed bool) { s.totalQuestionsAttempted++ s.totalQuestionsIncorrect++ s.currentStreak = 0 // Reset streak on incorrect answer if hintUsed { s.totalHintsUsed++ } s.recalculate() } // RecordTimeoutAnswer records a timeout func (s *UserStatistics) RecordTimeoutAnswer() { s.totalQuestionsAttempted++ s.totalQuestionsTimeout++ s.currentStreak = 0 // Reset streak on timeout s.recalculate() } // UpdateThemePerformance updates performance for a specific theme func (s *UserStatistics) UpdateThemePerformance( themeID types.ThemeID, questionsAttempted int64, questionsCorrect int64, score int, ) { performance := s.themePerformance[themeID] if performance == nil { performance = &ThemePerformance{ ThemeID: themeID, QuestionsAttempted: 0, QuestionsCorrect: 0, CorrectRate: 0.0, AverageScore: 0.0, BestScore: 0, LastPlayed: nil, } s.themePerformance[themeID] = performance } performance.QuestionsAttempted += questionsAttempted performance.QuestionsCorrect += questionsCorrect if performance.QuestionsAttempted > 0 { performance.CorrectRate = float64(performance.QuestionsCorrect) / float64(performance.QuestionsAttempted) } if score > performance.BestScore { performance.BestScore = score } // Update average score (simplified calculation) if performance.QuestionsAttempted > 0 { totalScore := performance.AverageScore * float64(performance.QuestionsAttempted-questionsAttempted) totalScore += float64(score) performance.AverageScore = totalScore / float64(performance.QuestionsAttempted) } now := types.NewTimestamp() performance.LastPlayed = &now s.markUpdated() } // UpdateDifficultyPerformance updates performance for a specific difficulty func (s *UserStatistics) UpdateDifficultyPerformance( difficulty types.DifficultyLevel, questionsAttempted int64, questionsCorrect int64, score int, ) { performance := s.difficultyPerformance[difficulty] if performance == nil { performance = &DifficultyPerformance{ Difficulty: difficulty, QuestionsAttempted: 0, QuestionsCorrect: 0, CorrectRate: 0.0, AverageScore: 0.0, BestScore: 0, LastPlayed: nil, } s.difficultyPerformance[difficulty] = performance } performance.QuestionsAttempted += questionsAttempted performance.QuestionsCorrect += questionsCorrect if performance.QuestionsAttempted > 0 { performance.CorrectRate = float64(performance.QuestionsCorrect) / float64(performance.QuestionsAttempted) } if score > performance.BestScore { performance.BestScore = score } // Update average score if performance.QuestionsAttempted > 0 { totalScore := performance.AverageScore * float64(performance.QuestionsAttempted-questionsAttempted) totalScore += float64(score) performance.AverageScore = totalScore / float64(performance.QuestionsAttempted) } now := types.NewTimestamp() performance.LastPlayed = &now s.markUpdated() } // AddAchievement adds an achievement func (s *UserStatistics) AddAchievement(achievement string) { // Check if achievement already exists for _, existing := range s.achievements { if existing == achievement { return } } s.achievements = append(s.achievements, achievement) s.markUpdated() } // RecordLogin records a login event func (s *UserStatistics) RecordLogin() { s.loginCount++ now := types.NewTimestamp() if s.lastLoginAt != nil { // Calculate if this extends the login streak daysSinceLastLogin := int(now.Sub(*s.lastLoginAt).Hours() / 24) if daysSinceLastLogin == 1 { // Consecutive day login s.currentLoginStreak++ } else if daysSinceLastLogin > 1 { // Streak broken s.currentLoginStreak = 1 } // Same day login doesn't change streak } else { // First login s.currentLoginStreak = 1 } if s.currentLoginStreak > s.longestLoginStreak { s.longestLoginStreak = s.currentLoginStreak } s.lastLoginAt = &now s.markUpdated() } // UpdateRankings updates ranking information func (s *UserStatistics) UpdateRankings(globalRank, weeklyRank, monthlyRank *int) { s.globalRank = globalRank s.weeklyRank = weeklyRank s.monthlyRank = monthlyRank s.markUpdated() } // UpdateRecentStatistics updates recent performance statistics func (s *UserStatistics) UpdateRecentStatistics( recentGamesPlayed int64, recentCorrectRate float64, recentAverageScore float64, ) { s.recentGamesPlayed = recentGamesPlayed s.recentCorrectRate = recentCorrectRate s.recentAverageScore = recentAverageScore s.markUpdated() } // Analysis methods // GetPerformanceCategory returns performance category based on statistics func (s *UserStatistics) GetPerformanceCategory() string { correctRate := s.CorrectRate() switch { case correctRate >= 0.9: return "excellent" case correctRate >= 0.8: return "very_good" case correctRate >= 0.7: return "good" case correctRate >= 0.6: return "average" case correctRate >= 0.5: return "below_average" default: return "needs_improvement" } } // GetActivityLevel returns activity level based on recent games func (s *UserStatistics) GetActivityLevel() string { switch { case s.recentGamesPlayed >= 50: return "very_active" case s.recentGamesPlayed >= 20: return "active" case s.recentGamesPlayed >= 5: return "moderate" case s.recentGamesPlayed >= 1: return "low" default: return "inactive" } } // IsImproving returns true if the user is showing improvement func (s *UserStatistics) IsImproving() bool { return s.improvementRate > 0.0 } // GetBestTheme returns the theme with the highest performance func (s *UserStatistics) GetBestTheme() *types.ThemeID { var bestTheme *types.ThemeID var bestRate float64 for themeID, performance := range s.themePerformance { if performance.QuestionsAttempted >= 5 && performance.CorrectRate > bestRate { bestRate = performance.CorrectRate themeIDCopy := themeID bestTheme = &themeIDCopy } } return bestTheme } // GetOptimalDifficulty returns the difficulty level that's most suitable func (s *UserStatistics) GetOptimalDifficulty() types.DifficultyLevel { correctRate := s.CorrectRate() switch { case correctRate >= 0.9: return types.DifficultyHard case correctRate >= 0.8: return types.DifficultyMedium case correctRate >= 0.6: return types.DifficultyEasy default: return types.DifficultyEasy } } // Validation // Validate performs validation on the statistics func (s *UserStatistics) Validate() *types.ValidationResult { result := types.NewValidationResult() // Check consistency if s.totalGamesPlayed < s.totalGamesCompleted+s.totalGamesAbandoned+s.totalGamesTimedOut { result.AddError("total games played is inconsistent with game outcomes") } if s.totalQuestionsAttempted != s.totalQuestionsCorrect+s.totalQuestionsIncorrect+s.totalQuestionsTimeout { result.AddError("total questions attempted is inconsistent with question outcomes") } // Check bounds if s.CorrectRate() < 0.0 || s.CorrectRate() > 1.0 { result.AddError("correct rate must be between 0.0 and 1.0") } if s.hintsUsedPercentage < 0.0 || s.hintsUsedPercentage > 1.0 { result.AddError("hints used percentage must be between 0.0 and 1.0") } // Check negative values if s.totalGamesPlayed < 0 || s.totalScore < 0 || s.loginCount < 0 { result.AddError("statistics cannot have negative values") } return result } // Helper methods // recalculate recalculates derived statistics func (s *UserStatistics) recalculate() { // Recalculate average score if s.totalGamesCompleted > 0 { s.averageScore = float64(s.totalScore) / float64(s.totalGamesCompleted) } // Recalculate hints used percentage if s.totalQuestionsAttempted > 0 { s.hintsUsedPercentage = float64(s.totalHintsUsed) / float64(s.totalQuestionsAttempted) } // Recalculate average session time if s.totalGamesCompleted > 0 { s.averageSessionTime = types.NewDuration(s.totalPlayTime.Duration / time.Duration(s.totalGamesCompleted)) } // Update skill level based on performance s.updateSkillLevel() s.lastCalculated = types.NewTimestamp() s.markUpdated() } // updateSkillLevel updates the skill level based on current statistics func (s *UserStatistics) updateSkillLevel() { correctRate := s.CorrectRate() gamesPlayed := s.totalGamesPlayed switch { case gamesPlayed >= 100 && correctRate >= 0.85: s.skillLevel = "expert" case gamesPlayed >= 50 && correctRate >= 0.75: s.skillLevel = "advanced" case gamesPlayed >= 20 && correctRate >= 0.60: s.skillLevel = "intermediate" default: s.skillLevel = "beginner" } } // markUpdated updates the timestamp func (s *UserStatistics) markUpdated() { s.updatedAt = types.NewTimestamp() } // ToSnapshot creates a read-only snapshot func (s *UserStatistics) ToSnapshot() *UserStatisticsSnapshot { // Copy theme performance themePerformanceCopy := make(map[types.ThemeID]*ThemePerformance) for k, v := range s.themePerformance { themePerformanceCopy[k] = &ThemePerformance{ ThemeID: v.ThemeID, QuestionsAttempted: v.QuestionsAttempted, QuestionsCorrect: v.QuestionsCorrect, CorrectRate: v.CorrectRate, AverageScore: v.AverageScore, BestScore: v.BestScore, LastPlayed: v.LastPlayed, } } // Copy difficulty performance difficultyPerformanceCopy := make(map[types.DifficultyLevel]*DifficultyPerformance) for k, v := range s.difficultyPerformance { difficultyPerformanceCopy[k] = &DifficultyPerformance{ Difficulty: v.Difficulty, QuestionsAttempted: v.QuestionsAttempted, QuestionsCorrect: v.QuestionsCorrect, CorrectRate: v.CorrectRate, AverageScore: v.AverageScore, BestScore: v.BestScore, LastPlayed: v.LastPlayed, } } return &UserStatisticsSnapshot{ TotalGamesPlayed: s.totalGamesPlayed, TotalGamesCompleted: s.totalGamesCompleted, TotalQuestionsAttempted: s.totalQuestionsAttempted, TotalQuestionsCorrect: s.totalQuestionsCorrect, TotalScore: s.totalScore, HighestScore: s.highestScore, AverageScore: s.averageScore, CorrectRate: s.CorrectRate(), BestStreak: s.bestStreak, CurrentStreak: s.currentStreak, TotalPlayTime: s.totalPlayTime, HintsUsedPercentage: s.hintsUsedPercentage, ThemePerformance: themePerformanceCopy, DifficultyPerformance: difficultyPerformanceCopy, Achievements: append([]string(nil), s.achievements...), LoginCount: s.loginCount, SkillLevel: s.skillLevel, GlobalRank: s.globalRank, WeeklyRank: s.weeklyRank, MonthlyRank: s.monthlyRank, UpdatedAt: s.updatedAt, } } // GetSummary returns a human-readable summary func (s *UserStatistics) GetSummary() string { return fmt.Sprintf("Games: %d played (%d completed) | Questions: %d/%d correct (%.1f%%) | Score: %d (avg: %.1f)", s.totalGamesPlayed, s.totalGamesCompleted, s.totalQuestionsCorrect, s.totalQuestionsAttempted, s.CorrectRate()*100, s.totalScore, s.averageScore, ) } // UserStatisticsSnapshot represents a read-only view of user statistics type UserStatisticsSnapshot struct { TotalGamesPlayed int64 `json:"total_games_played"` TotalGamesCompleted int64 `json:"total_games_completed"` TotalQuestionsAttempted int64 `json:"total_questions_attempted"` TotalQuestionsCorrect int64 `json:"total_questions_correct"` TotalScore int64 `json:"total_score"` HighestScore int `json:"highest_score"` AverageScore float64 `json:"average_score"` CorrectRate float64 `json:"correct_rate"` BestStreak int `json:"best_streak"` CurrentStreak int `json:"current_streak"` TotalPlayTime types.Duration `json:"total_play_time"` HintsUsedPercentage float64 `json:"hints_used_percentage"` ThemePerformance map[types.ThemeID]*ThemePerformance `json:"theme_performance"` DifficultyPerformance map[types.DifficultyLevel]*DifficultyPerformance `json:"difficulty_performance"` Achievements []string `json:"achievements"` LoginCount int64 `json:"login_count"` SkillLevel string `json:"skill_level"` GlobalRank *int `json:"global_rank,omitempty"` WeeklyRank *int `json:"weekly_rank,omitempty"` MonthlyRank *int `json:"monthly_rank,omitempty"` UpdatedAt types.Timestamp `json:"updated_at"` } // GetSummary returns a summary of the snapshot func (uss *UserStatisticsSnapshot) GetSummary() string { return fmt.Sprintf("Games: %d (%d completed) | Correct: %.1f%% | Skill: %s", uss.TotalGamesPlayed, uss.TotalGamesCompleted, uss.CorrectRate*100, uss.SkillLevel, ) }