From 33a850b4954ba1929984c7850cb4a9f3d660de53 Mon Sep 17 00:00:00 2001 From: Trevor Slocum Date: Fri, 29 Dec 2023 23:28:29 -0800 Subject: [PATCH] Add leaderboard --- pkg/server/database.go | 51 +++++++++++++++++++ pkg/server/database_common.go | 9 ++++ pkg/server/database_disabled.go | 4 ++ pkg/server/server.go | 89 +++++++++++++++++++++++++++++++++ 4 files changed, 153 insertions(+) diff --git a/pkg/server/database.go b/pkg/server/database.go index 3ac4bb9..d574f69 100644 --- a/pkg/server/database.go +++ b/pkg/server/database.go @@ -592,6 +592,57 @@ func matchHistory(username string) ([]*bgammon.HistoryMatch, error) { return matches, nil } +func getLeaderboard(matchType int, multiPoint bool) (*leaderboardResult, error) { + dbLock.Lock() + defer dbLock.Unlock() + + tx, err := begin() + if err != nil { + return nil, err + } + defer tx.Commit(context.Background()) + + var columnName string + switch matchType { + case matchTypeCasual: + if !multiPoint { + columnName = "casualsingle" + } else { + columnName = "casualmulti" + } + case matchTypeRated: + if !multiPoint { + columnName = "ratedsingle" + } else { + columnName = "ratedmulti" + } + default: + log.Panicf("unknown match type: %d", matchType) + } + + result := &leaderboardResult{} + rows, err := tx.Query(context.Background(), "SELECT username, "+columnName+" FROM account ORDER BY "+columnName+" DESC LIMIT 100") + if err != nil { + return nil, err + } + for rows.Next() { + if err != nil { + continue + } + entry := &leaderboardEntry{} + err = rows.Scan(&entry.User, &entry.Rating) + if err != nil { + continue + } + entry.Rating /= 100 + result.Leaderboard = append(result.Leaderboard, entry) + } + if err != nil { + return nil, err + } + return result, nil +} + func dailyStats(tz *time.Location) (*serverStatsResult, error) { dbLock.Lock() defer dbLock.Unlock() diff --git a/pkg/server/database_common.go b/pkg/server/database_common.go index 542bf5e..868e806 100644 --- a/pkg/server/database_common.go +++ b/pkg/server/database_common.go @@ -5,6 +5,15 @@ const ( matchTypeRated ) +type leaderboardEntry struct { + User string + Rating int +} + +type leaderboardResult struct { + Leaderboard []*leaderboardEntry +} + type serverStatsEntry struct { Date string Games int diff --git a/pkg/server/database_disabled.go b/pkg/server/database_disabled.go index df98849..68eda0b 100644 --- a/pkg/server/database_disabled.go +++ b/pkg/server/database_disabled.go @@ -63,6 +63,10 @@ func matchHistory(username string) ([]*bgammon.HistoryMatch, error) { return nil, nil } +func getLeaderboard(matchType int, multiPoint bool) (*leaderboardResult, error) { + return nil, nil +} + func dailyStats(tz *time.Location) (*serverStatsResult, error) { return &serverStatsResult{}, nil } diff --git a/pkg/server/server.go b/pkg/server/server.go index 2c4f413..46f63ab 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -50,6 +50,10 @@ type server struct { gamesCacheTime time.Time gamesCacheLock sync.Mutex + leaderboardCache [4][]byte + leaderboardCacheTime time.Time + leaderboardCacheLock sync.Mutex + mailServer string passwordSalt string resetSalt string @@ -142,6 +146,67 @@ func (s *server) cachedMatches() []byte { return s.gamesCache } +func (s *server) cachedLeaderboard(matchType int, multiPoint bool) []byte { + s.leaderboardCacheLock.Lock() + defer s.leaderboardCacheLock.Unlock() + + var i int + switch matchType { + case matchTypeCasual: + if multiPoint { + i = 1 + } + case matchTypeRated: + if !multiPoint { + i = 2 + } else { + i = 3 + } + } + + if time.Since(s.leaderboardCacheTime) < 5*time.Minute { + return s.leaderboardCache[i] + } + s.leaderboardCacheTime = time.Now() + + result, err := getLeaderboard(matchTypeCasual, false) + if err != nil { + log.Fatalf("failed to get leaderboard: %s", err) + } + s.leaderboardCache[0], err = json.Marshal(result) + if err != nil { + log.Fatalf("failed to marshal %+v: %s", result, err) + } + + result, err = getLeaderboard(matchTypeCasual, true) + if err != nil { + log.Fatalf("failed to get leaderboard: %s", err) + } + s.leaderboardCache[1], err = json.Marshal(result) + if err != nil { + log.Fatalf("failed to marshal %+v: %s", result, err) + } + + result, err = getLeaderboard(matchTypeRated, false) + if err != nil { + log.Fatalf("failed to get leaderboard: %s", err) + } + s.leaderboardCache[2], err = json.Marshal(result) + if err != nil { + log.Fatalf("failed to marshal %+v: %s", result, err) + } + + result, err = getLeaderboard(matchTypeRated, true) + if err != nil { + log.Fatalf("failed to get leaderboard: %s", err) + } + s.leaderboardCache[3], err = json.Marshal(result) + if err != nil { + log.Fatalf("failed to marshal %+v: %s", result, err) + } + return s.leaderboardCache[i] +} + func (s *server) handleResetPassword(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) @@ -186,6 +251,26 @@ func (s *server) handleListMatches(w http.ResponseWriter, r *http.Request) { w.Write(s.cachedMatches()) } +func (s *server) handleLeaderboardCasualSingle(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write(s.cachedLeaderboard(matchTypeCasual, false)) +} + +func (s *server) handleLeaderboardCasualMulti(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write(s.cachedLeaderboard(matchTypeCasual, true)) +} + +func (s *server) handleLeaderboardRatedSingle(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write(s.cachedLeaderboard(matchTypeRated, false)) +} + +func (s *server) handleLeaderboardRatedMulti(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write(s.cachedLeaderboard(matchTypeRated, true)) +} + func (s *server) handlePrintDailyStats(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -272,6 +357,10 @@ func (s *server) listenWebSocket(address string) { m.HandleFunc("/reset/{id:[0-9]+}/{key:[A-Za-z0-9]+}", s.handleResetPassword) m.HandleFunc("/match/{id:[0-9]+}", s.handleMatch) m.HandleFunc("/matches", s.handleListMatches) + m.HandleFunc("/leaderboard-casual-single", s.handleLeaderboardCasualSingle) + m.HandleFunc("/leaderboard-casual-multi", s.handleLeaderboardCasualMulti) + m.HandleFunc("/leaderboard-rated-single", s.handleLeaderboardRatedSingle) + m.HandleFunc("/leaderboard-rated-multi", s.handleLeaderboardRatedMulti) m.HandleFunc("/stats", s.handlePrintDailyStats) m.HandleFunc("/stats-total", s.handlePrintCumulativeStats) m.HandleFunc("/stats-tabula", s.handlePrintTabulaStats)