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.
440 lines
11 KiB
Go
440 lines
11 KiB
Go
package valueobjects
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/knowfoolery/backend/shared/errors"
|
|
)
|
|
|
|
type LeaderboardStatistics struct {
|
|
totalEntries int32
|
|
activeEntries int32
|
|
totalScoreSum int64
|
|
averageScore float32
|
|
highestScore int32
|
|
lowestScore int32
|
|
medianScore float32
|
|
totalGamesPlayed int64
|
|
uniquePlayers int32
|
|
lastUpdated time.Time
|
|
createdAt time.Time
|
|
resetCount int32
|
|
lastResetAt time.Time
|
|
topScoreStreaks map[string]int32
|
|
competitiveness float32
|
|
activityLevel float32
|
|
growthRate float32
|
|
retentionRate float32
|
|
metadata map[string]interface{}
|
|
}
|
|
|
|
func NewLeaderboardStatistics() *LeaderboardStatistics {
|
|
now := time.Now()
|
|
return &LeaderboardStatistics{
|
|
totalEntries: 0,
|
|
activeEntries: 0,
|
|
totalScoreSum: 0,
|
|
averageScore: 0.0,
|
|
highestScore: 0,
|
|
lowestScore: 0,
|
|
medianScore: 0.0,
|
|
totalGamesPlayed: 0,
|
|
uniquePlayers: 0,
|
|
lastUpdated: now,
|
|
createdAt: now,
|
|
resetCount: 0,
|
|
lastResetAt: time.Time{},
|
|
topScoreStreaks: make(map[string]int32),
|
|
competitiveness: 0.0,
|
|
activityLevel: 0.0,
|
|
growthRate: 0.0,
|
|
retentionRate: 0.0,
|
|
metadata: make(map[string]interface{}),
|
|
}
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) TotalEntries() int32 {
|
|
return ls.totalEntries
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) ActiveEntries() int32 {
|
|
return ls.activeEntries
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) TotalScoreSum() int64 {
|
|
return ls.totalScoreSum
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) AverageScore() float32 {
|
|
return ls.averageScore
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) HighestScore() int32 {
|
|
return ls.highestScore
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) LowestScore() int32 {
|
|
return ls.lowestScore
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) MedianScore() float32 {
|
|
return ls.medianScore
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) TotalGamesPlayed() int64 {
|
|
return ls.totalGamesPlayed
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) UniquePlayers() int32 {
|
|
return ls.uniquePlayers
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) LastUpdated() time.Time {
|
|
return ls.lastUpdated
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) CreatedAt() time.Time {
|
|
return ls.createdAt
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) ResetCount() int32 {
|
|
return ls.resetCount
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) LastResetAt() time.Time {
|
|
return ls.lastResetAt
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) TopScoreStreaks() map[string]int32 {
|
|
result := make(map[string]int32)
|
|
for k, v := range ls.topScoreStreaks {
|
|
result[k] = v
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) Competitiveness() float32 {
|
|
return ls.competitiveness
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) ActivityLevel() float32 {
|
|
return ls.activityLevel
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) GrowthRate() float32 {
|
|
return ls.growthRate
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) RetentionRate() float32 {
|
|
return ls.retentionRate
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) Metadata() map[string]interface{} {
|
|
result := make(map[string]interface{})
|
|
for k, v := range ls.metadata {
|
|
result[k] = v
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) UpdateScoreStatistics(scores []int32) error {
|
|
if len(scores) == 0 {
|
|
return nil
|
|
}
|
|
|
|
ls.totalEntries = int32(len(scores))
|
|
ls.activeEntries = ls.totalEntries
|
|
|
|
var sum int64
|
|
highest := scores[0]
|
|
lowest := scores[0]
|
|
|
|
for _, score := range scores {
|
|
sum += int64(score)
|
|
if score > highest {
|
|
highest = score
|
|
}
|
|
if score < lowest {
|
|
lowest = score
|
|
}
|
|
}
|
|
|
|
ls.totalScoreSum = sum
|
|
ls.averageScore = float32(sum) / float32(len(scores))
|
|
ls.highestScore = highest
|
|
ls.lowestScore = lowest
|
|
|
|
ls.medianScore = ls.calculateMedian(scores)
|
|
ls.lastUpdated = time.Now()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) calculateMedian(scores []int32) float32 {
|
|
if len(scores) == 0 {
|
|
return 0
|
|
}
|
|
|
|
sorted := make([]int32, len(scores))
|
|
copy(sorted, scores)
|
|
|
|
for i := 0; i < len(sorted)-1; i++ {
|
|
for j := i + 1; j < len(sorted); j++ {
|
|
if sorted[i] > sorted[j] {
|
|
sorted[i], sorted[j] = sorted[j], sorted[i]
|
|
}
|
|
}
|
|
}
|
|
|
|
n := len(sorted)
|
|
if n%2 == 0 {
|
|
return float32(sorted[n/2-1]+sorted[n/2]) / 2.0
|
|
}
|
|
return float32(sorted[n/2])
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) IncrementGamesPlayed(count int64) error {
|
|
if count < 0 {
|
|
return errors.ErrInvalidGameCount
|
|
}
|
|
ls.totalGamesPlayed += count
|
|
ls.lastUpdated = time.Now()
|
|
return nil
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) SetUniquePlayers(count int32) error {
|
|
if count < 0 {
|
|
return errors.ErrInvalidPlayerCount
|
|
}
|
|
ls.uniquePlayers = count
|
|
ls.lastUpdated = time.Now()
|
|
return nil
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) IncrementResetCount() {
|
|
ls.resetCount++
|
|
ls.lastResetAt = time.Now()
|
|
ls.lastUpdated = time.Now()
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) UpdateTopScoreStreak(playerID string, streak int32) error {
|
|
if playerID == "" {
|
|
return errors.ErrInvalidPlayerID
|
|
}
|
|
if streak < 0 {
|
|
return errors.ErrInvalidStreak
|
|
}
|
|
|
|
if streak == 0 {
|
|
delete(ls.topScoreStreaks, playerID)
|
|
} else {
|
|
ls.topScoreStreaks[playerID] = streak
|
|
}
|
|
|
|
ls.lastUpdated = time.Now()
|
|
return nil
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) CalculateCompetitiveness() {
|
|
if ls.totalEntries <= 1 {
|
|
ls.competitiveness = 0.0
|
|
return
|
|
}
|
|
|
|
if ls.highestScore == ls.lowestScore {
|
|
ls.competitiveness = 0.0
|
|
return
|
|
}
|
|
|
|
scoreRange := float32(ls.highestScore - ls.lowestScore)
|
|
normalizedRange := scoreRange / float32(ls.highestScore)
|
|
|
|
participationFactor := float32(ls.activeEntries) / float32(ls.totalEntries)
|
|
|
|
ls.competitiveness = normalizedRange * participationFactor
|
|
if ls.competitiveness > 1.0 {
|
|
ls.competitiveness = 1.0
|
|
}
|
|
|
|
ls.lastUpdated = time.Now()
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) CalculateActivityLevel(recentGames int64, timeWindow time.Duration) {
|
|
if timeWindow <= 0 {
|
|
ls.activityLevel = 0.0
|
|
return
|
|
}
|
|
|
|
hoursInWindow := float64(timeWindow) / float64(time.Hour)
|
|
if hoursInWindow == 0 {
|
|
ls.activityLevel = 0.0
|
|
return
|
|
}
|
|
|
|
ls.activityLevel = float32(float64(recentGames) / hoursInWindow)
|
|
ls.lastUpdated = time.Now()
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) CalculateGrowthRate(previousPeriodPlayers int32, currentPeriodPlayers int32) {
|
|
if previousPeriodPlayers <= 0 {
|
|
if currentPeriodPlayers > 0 {
|
|
ls.growthRate = 1.0
|
|
} else {
|
|
ls.growthRate = 0.0
|
|
}
|
|
return
|
|
}
|
|
|
|
ls.growthRate = float32(currentPeriodPlayers-previousPeriodPlayers) / float32(previousPeriodPlayers)
|
|
ls.lastUpdated = time.Now()
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) CalculateRetentionRate(returningPlayers int32, totalPlayers int32) {
|
|
if totalPlayers <= 0 {
|
|
ls.retentionRate = 0.0
|
|
return
|
|
}
|
|
|
|
ls.retentionRate = float32(returningPlayers) / float32(totalPlayers)
|
|
if ls.retentionRate > 1.0 {
|
|
ls.retentionRate = 1.0
|
|
}
|
|
|
|
ls.lastUpdated = time.Now()
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) SetMetadata(key string, value interface{}) error {
|
|
if key == "" {
|
|
return errors.ErrInvalidMetadata
|
|
}
|
|
ls.metadata[key] = value
|
|
ls.lastUpdated = time.Now()
|
|
return nil
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) RemoveMetadata(key string) {
|
|
delete(ls.metadata, key)
|
|
ls.lastUpdated = time.Now()
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) Reset() {
|
|
ls.totalEntries = 0
|
|
ls.activeEntries = 0
|
|
ls.totalScoreSum = 0
|
|
ls.averageScore = 0.0
|
|
ls.highestScore = 0
|
|
ls.lowestScore = 0
|
|
ls.medianScore = 0.0
|
|
ls.totalGamesPlayed = 0
|
|
ls.uniquePlayers = 0
|
|
ls.topScoreStreaks = make(map[string]int32)
|
|
ls.competitiveness = 0.0
|
|
ls.activityLevel = 0.0
|
|
ls.growthRate = 0.0
|
|
ls.retentionRate = 0.0
|
|
ls.IncrementResetCount()
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) GetAge() time.Duration {
|
|
return time.Since(ls.createdAt)
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) GetTimeSinceLastUpdate() time.Duration {
|
|
return time.Since(ls.lastUpdated)
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) GetTimeSinceLastReset() time.Duration {
|
|
if ls.lastResetAt.IsZero() {
|
|
return ls.GetAge()
|
|
}
|
|
return time.Since(ls.lastResetAt)
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) IsHealthy() bool {
|
|
timeSinceUpdate := ls.GetTimeSinceLastUpdate()
|
|
return timeSinceUpdate < time.Hour*24
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) String() string {
|
|
return fmt.Sprintf("LeaderboardStatistics{TotalEntries: %d, AvgScore: %.2f, Highest: %d, Lowest: %d, Players: %d}",
|
|
ls.totalEntries, ls.averageScore, ls.highestScore, ls.lowestScore, ls.uniquePlayers)
|
|
}
|
|
|
|
type LeaderboardStatisticsSnapshot struct {
|
|
TotalEntries int32 `json:"total_entries"`
|
|
ActiveEntries int32 `json:"active_entries"`
|
|
TotalScoreSum int64 `json:"total_score_sum"`
|
|
AverageScore float32 `json:"average_score"`
|
|
HighestScore int32 `json:"highest_score"`
|
|
LowestScore int32 `json:"lowest_score"`
|
|
MedianScore float32 `json:"median_score"`
|
|
TotalGamesPlayed int64 `json:"total_games_played"`
|
|
UniquePlayers int32 `json:"unique_players"`
|
|
LastUpdated time.Time `json:"last_updated"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
ResetCount int32 `json:"reset_count"`
|
|
LastResetAt time.Time `json:"last_reset_at"`
|
|
TopScoreStreaks map[string]int32 `json:"top_score_streaks"`
|
|
Competitiveness float32 `json:"competitiveness"`
|
|
ActivityLevel float32 `json:"activity_level"`
|
|
GrowthRate float32 `json:"growth_rate"`
|
|
RetentionRate float32 `json:"retention_rate"`
|
|
Metadata map[string]interface{} `json:"metadata"`
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) ToSnapshot() LeaderboardStatisticsSnapshot {
|
|
return LeaderboardStatisticsSnapshot{
|
|
TotalEntries: ls.totalEntries,
|
|
ActiveEntries: ls.activeEntries,
|
|
TotalScoreSum: ls.totalScoreSum,
|
|
AverageScore: ls.averageScore,
|
|
HighestScore: ls.highestScore,
|
|
LowestScore: ls.lowestScore,
|
|
MedianScore: ls.medianScore,
|
|
TotalGamesPlayed: ls.totalGamesPlayed,
|
|
UniquePlayers: ls.uniquePlayers,
|
|
LastUpdated: ls.lastUpdated,
|
|
CreatedAt: ls.createdAt,
|
|
ResetCount: ls.resetCount,
|
|
LastResetAt: ls.lastResetAt,
|
|
TopScoreStreaks: ls.TopScoreStreaks(),
|
|
Competitiveness: ls.competitiveness,
|
|
ActivityLevel: ls.activityLevel,
|
|
GrowthRate: ls.growthRate,
|
|
RetentionRate: ls.retentionRate,
|
|
Metadata: ls.Metadata(),
|
|
}
|
|
}
|
|
|
|
func (ls *LeaderboardStatistics) FromSnapshot(snapshot LeaderboardStatisticsSnapshot) error {
|
|
ls.totalEntries = snapshot.TotalEntries
|
|
ls.activeEntries = snapshot.ActiveEntries
|
|
ls.totalScoreSum = snapshot.TotalScoreSum
|
|
ls.averageScore = snapshot.AverageScore
|
|
ls.highestScore = snapshot.HighestScore
|
|
ls.lowestScore = snapshot.LowestScore
|
|
ls.medianScore = snapshot.MedianScore
|
|
ls.totalGamesPlayed = snapshot.TotalGamesPlayed
|
|
ls.uniquePlayers = snapshot.UniquePlayers
|
|
ls.lastUpdated = snapshot.LastUpdated
|
|
ls.createdAt = snapshot.CreatedAt
|
|
ls.resetCount = snapshot.ResetCount
|
|
ls.lastResetAt = snapshot.LastResetAt
|
|
ls.competitiveness = snapshot.Competitiveness
|
|
ls.activityLevel = snapshot.ActivityLevel
|
|
ls.growthRate = snapshot.GrowthRate
|
|
ls.retentionRate = snapshot.RetentionRate
|
|
|
|
ls.topScoreStreaks = make(map[string]int32)
|
|
for k, v := range snapshot.TopScoreStreaks {
|
|
ls.topScoreStreaks[k] = v
|
|
}
|
|
|
|
ls.metadata = make(map[string]interface{})
|
|
for k, v := range snapshot.Metadata {
|
|
ls.metadata[k] = v
|
|
}
|
|
|
|
return nil
|
|
} |