237 lines
5.2 KiB
Go
237 lines
5.2 KiB
Go
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 "✅"
|
|
}
|
|
}
|