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.

763 lines
23 KiB
Go

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,
)
}