package tracker import ( "encoding/json" "fmt" "os" "sort" "strconv" "strings" "time" ) const defaultFile = "wordle_scores.json" // Score represents a single Wordle score entry type Score struct { UserName string `json:"user_name"` PuzzleNumber int `json:"puzzle_number"` Score string `json:"score"` Timestamp time.Time `json:"timestamp"` } // DailyScores maps user IDs to their scores for a specific date type DailyScores map[string]Score // scoresDB maps dates (YYYY-MM-DD) to daily scores type scoresDB map[string]DailyScores // Tracker manages score tracking and persistence type Tracker struct { scores scoresDB filename string } // New creates a new tracker instance func New(filename string) (*Tracker, error) { if filename == "" { filename = defaultFile } tracker := &Tracker{ filename: filename, scores: make(scoresDB), } if err := tracker.load(); err != nil { if !os.IsNotExist(err) { return nil, fmt.Errorf("failed to load scores: %w", err) } } return tracker, nil } func (t *Tracker) load() error { data, err := os.ReadFile(t.filename) if err != nil { return err } return json.Unmarshal(data, &t.scores) } func (t *Tracker) save() error { data, err := json.MarshalIndent(t.scores, "", " ") if err != nil { return fmt.Errorf("failed to marshal scores: %w", err) } return os.WriteFile(t.filename, data, 0644) } // Add adds a Wordle score for a user func (t *Tracker) Add(userID, userName string, puzzleNumber int, score string, timestamp time.Time) error { date := timestamp.Format("2006-01-02") if t.scores[date] == nil { t.scores[date] = make(DailyScores) } t.scores[date][userID] = Score{ UserName: userName, PuzzleNumber: puzzleNumber, Score: score, Timestamp: timestamp, } return t.save() } // GetForDate retrieves all scores for a specific date func (t *Tracker) GetForDate(date string) DailyScores { scores, ok := t.scores[date] if !ok { return make(DailyScores) } return scores } // GetYesterday retrieves scores from yesterday func (t *Tracker) GetYesterday() DailyScores { yesterday := time.Now().AddDate(0, 0, -1).Format("2006-01-02") return t.GetForDate(yesterday) } // GetToday retrieves scores from today func (t *Tracker) GetToday() DailyScores { today := time.Now().Format("2006-01-02") return t.GetForDate(today) } // ScoreEntry is used for sorting type ScoreEntry struct { UserID string Score Score } // FormatSummary formats scores into a nice summary message func FormatSummary(scores DailyScores) string { if len(scores) == 0 { return "No Wordle scores were posted yesterday. 😢" } // Get puzzle number from first entry var puzzleNum int for _, score := range scores { puzzleNum = score.PuzzleNumber break } // Sort scores var entries []ScoreEntry for userID, score := range scores { entries = append(entries, ScoreEntry{UserID: userID, Score: score}) } sort.Slice(entries, func(i, j int) bool { return scoreValue(entries[i].Score.Score) < scoreValue(entries[j].Score.Score) }) // Group by score scoreGroups := make(map[string][]string) for _, entry := range entries { score := entry.Score.Score scoreGroups[score] = append(scoreGroups[score], entry.Score.UserName) } // Build summary var lines []string lines = append(lines, fmt.Sprintf("šŸ“Š *Wordle %d Results* šŸ“Š\n", puzzleNum)) // Get sorted score keys var scoreKeys []string for score := range scoreGroups { scoreKeys = append(scoreKeys, score) } sort.Slice(scoreKeys, func(i, j int) bool { return scoreValue(scoreKeys[i]) < scoreValue(scoreKeys[j]) }) // Display grouped results for _, score := range scoreKeys { users := scoreGroups[score] emoji := getEmoji(score) userList := strings.Join(users, ", ") lines = append(lines, fmt.Sprintf("%s *%s/6*: %s", emoji, score, userList)) } // Statistics totalPlayers := len(scores) successful := 0 totalScore := 0 for _, score := range scores { if score.Score != "X" { successful++ val, _ := strconv.Atoi(score.Score) totalScore += val } } lines = append(lines, fmt.Sprintf("\nšŸ“ˆ *Stats*: %d/%d solved", successful, totalPlayers)) if successful > 0 { avgScore := float64(totalScore) / float64(successful) lines = append(lines, fmt.Sprintf("Average score: %.2f", avgScore)) } return strings.Join(lines, "\n") } // FormatToday formats today's scores func FormatToday(scores DailyScores) string { if len(scores) == 0 { return "No scores posted yet today!" } // Get puzzle number var puzzleNum int for _, score := range scores { puzzleNum = score.PuzzleNumber break } var lines []string lines = append(lines, fmt.Sprintf("šŸ“Š *Today's Wordle %d Scores So Far* šŸ“Š\n", puzzleNum)) for _, score := range scores { lines = append(lines, fmt.Sprintf("• %s: %s/6", score.UserName, score.Score)) } return strings.Join(lines, "\n") } // scoreValue returns the numeric value for sorting (X = 7) func scoreValue(score string) int { if score == "X" { return 7 } val, _ := strconv.Atoi(score) return val } // getEmoji returns the appropriate emoji for a score func getEmoji(score string) string { switch score { case "1", "2", "3": return "šŸŽÆ" case "X": return "āŒ" default: return "āœ…" } }