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