Allow printing match statistics

This commit is contained in:
Trevor Slocum 2023-11-22 10:12:06 -08:00
parent 8cba945823
commit caf4b3e2d8
3 changed files with 103 additions and 2 deletions

View file

@ -3,6 +3,7 @@ package main
import (
"context"
"log"
"time"
"code.rocket9labs.com/tslocum/bgammon"
"github.com/jackc/pgx/v5"
@ -82,3 +83,71 @@ func recordGameResult(conn *pgx.Conn, g bgammon.Game) error {
_, err = tx.Exec(context.Background(), "INSERT INTO game (started, ended, winner, player1, player2) VALUES ($1, $2, $3, $4, $5)", g.Started.Unix(), g.Ended.Unix(), g.Winner, g.Player1.Name, g.Player2.Name)
return err
}
type serverStatsEntry struct {
Date string
Games int
}
type serverStatsResult struct {
History []*serverStatsEntry
}
func serverStats(conn *pgx.Conn, tz *time.Location) (*serverStatsResult, error) {
tx, err := begin(conn)
if err != nil {
return nil, err
}
defer tx.Commit(context.Background())
var earliestGame int64
rows, err := tx.Query(context.Background(), "SELECT started FROM game ORDER BY started ASC LIMIT 1")
if err != nil {
return nil, err
}
for rows.Next() {
if err != nil {
continue
}
err = rows.Scan(&earliestGame)
}
if err != nil {
return nil, err
}
result := &serverStatsResult{}
earliest := midnight(time.Unix(earliestGame, 0).In(tz))
rangeStart, rangeEnd := earliest.Unix(), earliest.AddDate(0, 0, 1).Unix()
var count int
for {
rows, err := tx.Query(context.Background(), "SELECT COUNT(*) FROM game WHERE started >= $1 AND started < $2", rangeStart, rangeEnd)
if err != nil {
return nil, err
}
for rows.Next() {
if err != nil {
continue
}
err = rows.Scan(&count)
}
if err != nil {
return nil, err
}
result.History = append(result.History, &serverStatsEntry{
Date: earliest.Format("2006-01-02"),
Games: count,
})
earliest = earliest.AddDate(0, 0, 1)
rangeStart, rangeEnd = rangeEnd, earliest.AddDate(0, 0, 1).Unix()
if rangeStart >= time.Now().Unix() {
break
}
}
return result, nil
}
func midnight(t time.Time) time.Time {
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
}

View file

@ -13,6 +13,7 @@ func main() {
var (
tcpAddress string
wsAddress string
tz string
dataSource string
debug int
debugCommands bool
@ -20,6 +21,7 @@ func main() {
)
flag.StringVar(&tcpAddress, "tcp", "localhost:1337", "TCP listen address")
flag.StringVar(&wsAddress, "ws", "localhost:1338", "WebSocket listen address")
flag.StringVar(&tz, "tz", "", "Time zone used when calculating statistics")
flag.StringVar(&dataSource, "db", "", "Database data source (postgres://username:password@localhost:5432/database_name")
flag.IntVar(&debug, "debug", 0, "print debug information and serve pprof on specified port")
flag.BoolVar(&debugCommands, "debug-commands", false, "allow players to use restricted commands")
@ -49,7 +51,7 @@ func main() {
allowDebugCommands = debugCommands
}
s := newServer(dataSource)
s := newServer(tz, dataSource)
if tcpAddress != "" {
s.listen("tcp", tcpAddress)
}

View file

@ -49,10 +49,12 @@ type server struct {
gamesCacheTime time.Time
gamesCacheLock sync.Mutex
tz *time.Location
db *pgx.Conn
}
func newServer(dataSource string) *server {
func newServer(tz string, dataSource string) *server {
const bufferSize = 10
s := &server{
newGameIDs: make(chan int),
@ -61,6 +63,16 @@ func newServer(dataSource string) *server {
welcome: []byte("hello Welcome to bgammon.org! Please log in by sending the 'login' command. You may specify a username, otherwise you will be assigned a random username. If you specify a username, you may also specify a password. Have fun!"),
}
if tz != "" {
var err error
s.tz, err = time.LoadLocation(tz)
if err != nil {
log.Fatalf("failed to parse timezone %s: %s", tz, err)
}
} else {
s.tz = time.UTC
}
if dataSource != "" {
var err error
s.db, err = connectDB(dataSource)
@ -123,6 +135,23 @@ func (s *server) handleListMatches(w http.ResponseWriter, r *http.Request) {
w.Write(s.cachedMatches())
}
func (s *server) handlePrintStats(w http.ResponseWriter, r *http.Request) {
if s.db == nil {
return
}
w.Header().Set("Content-Type", "application/json")
stats, err := serverStats(s.db, s.tz)
if err != nil {
log.Fatalf("failed to fetch server statistics: %s", err)
}
buf, err := json.Marshal(stats)
if err != nil {
log.Fatalf("failed to fetch serialize statistics: %s", err)
}
w.Write(buf)
}
func (s *server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
const bufferSize = 8
commands := make(chan []byte, bufferSize)
@ -151,6 +180,7 @@ func (s *server) listenWebSocket(address string) {
mux := http.NewServeMux()
mux.HandleFunc("/matches", s.handleListMatches)
mux.HandleFunc("/stats", s.handlePrintStats)
mux.HandleFunc("/", s.handleWebSocket)
err := http.ListenAndServe(address, mux)