diff --git a/store/daily.go b/store/daily.go new file mode 100644 index 0000000..9d7f466 --- /dev/null +++ b/store/daily.go @@ -0,0 +1,81 @@ +package store + +import ( + "encoding/json" + "sort" + "time" + + bolt "go.etcd.io/bbolt" +) + +var bucketDailyRuns = []byte("daily_runs") + +type DailyRecord struct { + Date string `json:"date"` + Player string `json:"player"` + PlayerName string `json:"player_name"` + FloorReached int `json:"floor_reached"` + GoldEarned int `json:"gold_earned"` +} + +func (d *DB) SaveDaily(record DailyRecord) error { + return d.db.Update(func(tx *bolt.Tx) error { + b := tx.Bucket(bucketDailyRuns) + key := []byte(record.Date + ":" + record.Player) + data, err := json.Marshal(record) + if err != nil { + return err + } + return b.Put(key, data) + }) +} + +func (d *DB) GetDailyLeaderboard(date string, limit int) ([]DailyRecord, error) { + var records []DailyRecord + prefix := []byte(date + ":") + err := d.db.View(func(tx *bolt.Tx) error { + b := tx.Bucket(bucketDailyRuns) + c := b.Cursor() + for k, v := c.Seek(prefix); k != nil && len(k) >= len(prefix) && string(k[:len(prefix)]) == string(prefix); k, v = c.Next() { + var r DailyRecord + if json.Unmarshal(v, &r) == nil { + records = append(records, r) + } + } + return nil + }) + if err != nil { + return nil, err + } + sort.Slice(records, func(i, j int) bool { + if records[i].FloorReached != records[j].FloorReached { + return records[i].FloorReached > records[j].FloorReached + } + return records[i].GoldEarned > records[j].GoldEarned + }) + if len(records) > limit { + records = records[:limit] + } + return records, nil +} + +func (d *DB) GetStreak(fingerprint, currentDate string) (int, error) { + streak := 0 + date, err := time.Parse("2006-01-02", currentDate) + if err != nil { + return 0, err + } + err = d.db.View(func(tx *bolt.Tx) error { + b := tx.Bucket(bucketDailyRuns) + for { + key := []byte(date.Format("2006-01-02") + ":" + fingerprint) + if b.Get(key) == nil { + break + } + streak++ + date = date.AddDate(0, 0, -1) + } + return nil + }) + return streak, err +} diff --git a/store/daily_test.go b/store/daily_test.go new file mode 100644 index 0000000..00658bd --- /dev/null +++ b/store/daily_test.go @@ -0,0 +1,66 @@ +package store + +import ( + "testing" +) + +func TestSaveAndGetDaily(t *testing.T) { + dir := t.TempDir() + db, err := Open(dir + "/test_daily.db") + if err != nil { + t.Fatal(err) + } + defer db.Close() + + db.SaveDaily(DailyRecord{Date: "2026-03-25", Player: "fp1", PlayerName: "Alice", FloorReached: 10, GoldEarned: 200}) + db.SaveDaily(DailyRecord{Date: "2026-03-25", Player: "fp2", PlayerName: "Bob", FloorReached: 15, GoldEarned: 100}) + db.SaveDaily(DailyRecord{Date: "2026-03-25", Player: "fp3", PlayerName: "Charlie", FloorReached: 15, GoldEarned: 300}) + + records, err := db.GetDailyLeaderboard("2026-03-25", 10) + if err != nil { + t.Fatal(err) + } + if len(records) != 3 { + t.Fatalf("expected 3 records, got %d", len(records)) + } + // Charlie and Bob both floor 15, Charlie has more gold so first + if records[0].PlayerName != "Charlie" { + t.Errorf("expected Charlie first, got %s", records[0].PlayerName) + } + if records[1].PlayerName != "Bob" { + t.Errorf("expected Bob second, got %s", records[1].PlayerName) + } + if records[2].PlayerName != "Alice" { + t.Errorf("expected Alice third, got %s", records[2].PlayerName) + } +} + +func TestDailyStreak(t *testing.T) { + dir := t.TempDir() + db, err := Open(dir + "/test_streak.db") + if err != nil { + t.Fatal(err) + } + defer db.Close() + + db.SaveDaily(DailyRecord{Date: "2026-03-23", Player: "fp1", PlayerName: "Alice", FloorReached: 5, GoldEarned: 50}) + db.SaveDaily(DailyRecord{Date: "2026-03-24", Player: "fp1", PlayerName: "Alice", FloorReached: 8, GoldEarned: 100}) + db.SaveDaily(DailyRecord{Date: "2026-03-25", Player: "fp1", PlayerName: "Alice", FloorReached: 10, GoldEarned: 200}) + + streak, err := db.GetStreak("fp1", "2026-03-25") + if err != nil { + t.Fatal(err) + } + if streak != 3 { + t.Errorf("expected streak 3, got %d", streak) + } + + // Gap in streak + streak2, err := db.GetStreak("fp1", "2026-03-27") + if err != nil { + t.Fatal(err) + } + if streak2 != 0 { + t.Errorf("expected streak 0 after gap, got %d", streak2) + } +} diff --git a/store/db.go b/store/db.go index fb2a178..9c08847 100644 --- a/store/db.go +++ b/store/db.go @@ -39,6 +39,18 @@ func Open(path string) (*DB, error) { if _, err := tx.CreateBucketIfNotExists(bucketAchievements); err != nil { return err } + if _, err := tx.CreateBucketIfNotExists(bucketDailyRuns); err != nil { + return err + } + if _, err := tx.CreateBucketIfNotExists(bucketUnlocks); err != nil { + return err + } + if _, err := tx.CreateBucketIfNotExists(bucketTitles); err != nil { + return err + } + if _, err := tx.CreateBucketIfNotExists(bucketCodex); err != nil { + return err + } return nil }) return &DB{db: db}, err