feat: BoltDB store — player profiles and run rankings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1
go.mod
1
go.mod
@@ -36,6 +36,7 @@ require (
|
|||||||
github.com/muesli/termenv v0.16.0 // indirect
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
|
go.etcd.io/bbolt v1.4.3 // indirect
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
|
||||||
golang.org/x/sys v0.36.0 // indirect
|
golang.org/x/sys v0.36.0 // indirect
|
||||||
golang.org/x/text v0.23.0 // indirect
|
golang.org/x/text v0.23.0 // indirect
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -63,6 +63,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
|
|||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
|
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||||
|
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||||
|
|||||||
105
store/db.go
Normal file
105
store/db.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
bolt "go.etcd.io/bbolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
bucketProfiles = []byte("profiles")
|
||||||
|
bucketRankings = []byte("rankings")
|
||||||
|
)
|
||||||
|
|
||||||
|
type DB struct {
|
||||||
|
db *bolt.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
type RunRecord struct {
|
||||||
|
Player string `json:"player"`
|
||||||
|
Floor int `json:"floor"`
|
||||||
|
Score int `json:"score"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Open(path string) (*DB, error) {
|
||||||
|
db, err := bolt.Open(path, 0600, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = db.Update(func(tx *bolt.Tx) error {
|
||||||
|
if _, err := tx.CreateBucketIfNotExists(bucketProfiles); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := tx.CreateBucketIfNotExists(bucketRankings); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return &DB{db: db}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) Close() error {
|
||||||
|
return d.db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) SaveProfile(fingerprint, name string) error {
|
||||||
|
return d.db.Update(func(tx *bolt.Tx) error {
|
||||||
|
return tx.Bucket(bucketProfiles).Put([]byte(fingerprint), []byte(name))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) GetProfile(fingerprint string) (string, error) {
|
||||||
|
var name string
|
||||||
|
err := d.db.View(func(tx *bolt.Tx) error {
|
||||||
|
v := tx.Bucket(bucketProfiles).Get([]byte(fingerprint))
|
||||||
|
if v == nil {
|
||||||
|
return fmt.Errorf("profile not found")
|
||||||
|
}
|
||||||
|
name = string(v)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return name, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) SaveRun(player string, floor, score int) error {
|
||||||
|
return d.db.Update(func(tx *bolt.Tx) error {
|
||||||
|
b := tx.Bucket(bucketRankings)
|
||||||
|
id, _ := b.NextSequence()
|
||||||
|
record := RunRecord{Player: player, Floor: floor, Score: score}
|
||||||
|
data, err := json.Marshal(record)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return b.Put([]byte(fmt.Sprintf("%010d", id)), data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) TopRuns(limit int) ([]RunRecord, error) {
|
||||||
|
var runs []RunRecord
|
||||||
|
err := d.db.View(func(tx *bolt.Tx) error {
|
||||||
|
b := tx.Bucket(bucketRankings)
|
||||||
|
return b.ForEach(func(k, v []byte) error {
|
||||||
|
var r RunRecord
|
||||||
|
if err := json.Unmarshal(v, &r); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
runs = append(runs, r)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sort.Slice(runs, func(i, j int) bool {
|
||||||
|
if runs[i].Floor != runs[j].Floor {
|
||||||
|
return runs[i].Floor > runs[j].Floor
|
||||||
|
}
|
||||||
|
return runs[i].Score > runs[j].Score
|
||||||
|
})
|
||||||
|
if len(runs) > limit {
|
||||||
|
runs = runs[:limit]
|
||||||
|
}
|
||||||
|
return runs, nil
|
||||||
|
}
|
||||||
55
store/db_test.go
Normal file
55
store/db_test.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPlayerProfile(t *testing.T) {
|
||||||
|
db, err := Open("test.db")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
db.Close()
|
||||||
|
os.Remove("test.db")
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = db.SaveProfile("SHA256:abc123", "TestPlayer")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
name, err := db.GetProfile("SHA256:abc123")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if name != "TestPlayer" {
|
||||||
|
t.Errorf("Name: got %q, want %q", name, "TestPlayer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRanking(t *testing.T) {
|
||||||
|
db, err := Open("test_rank.db")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
db.Close()
|
||||||
|
os.Remove("test_rank.db")
|
||||||
|
}()
|
||||||
|
|
||||||
|
db.SaveRun("Alice", 20, 1500)
|
||||||
|
db.SaveRun("Bob", 15, 1000)
|
||||||
|
db.SaveRun("Charlie", 20, 2000)
|
||||||
|
|
||||||
|
rankings, err := db.TopRuns(10)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(rankings) != 3 {
|
||||||
|
t.Errorf("Rankings: got %d, want 3", len(rankings))
|
||||||
|
}
|
||||||
|
if rankings[0].Player != "Charlie" {
|
||||||
|
t.Errorf("Top player: got %q, want Charlie", rankings[0].Player)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user