]> git.xonotic.org Git - xonotic/xonstat.git/blob - xonstat/util/xs_glicko.go
Add a KReducer struct.
[xonotic/xonstat.git] / xonstat / util / xs_glicko.go
1 package main
2
3 import (
4         "encoding/json"
5         "flag"
6         "fmt"
7         "log"
8         "os"
9         "time"
10
11         "github.com/jmoiron/sqlx"
12         _ "github.com/lib/pq"
13 )
14
15 const DefaultStartGameID = 0
16 const DefaultEndGameID = -1
17 const DefaultRankingWindowDays = 7
18
19 type Config struct {
20         // database connection string
21         ConnStr string
22
23         // the starting game_id in the games table
24         StartGameID int
25
26         // the ending game_id in the games table
27         EndGameID int
28
29         // the number of days constituting the ranking window
30         RankingWindowDays int
31 }
32
33 func loadConfig(path string) (*Config, error) {
34         config := new(Config)
35
36         // defaults
37         config.ConnStr = "user=xonstat host=localhost dbname=xonstatdb sslmode=disable"
38         config.StartGameID = DefaultStartGameID
39         config.EndGameID = DefaultEndGameID
40         config.RankingWindowDays = DefaultRankingWindowDays
41
42         file, err := os.Open(path)
43         if err != nil {
44                 fmt.Println("Failed opening the file.")
45                 return config, err
46         }
47
48         decoder := json.NewDecoder(file)
49
50         // overwrite in-mem config with new values
51         err = decoder.Decode(config)
52         if err != nil {
53                 fmt.Println("Failed to decode the JSON.")
54                 return config, err
55         }
56
57         return config, nil
58 }
59
60 type Game struct {
61         GameID   int       `db:"game_id"`
62         GameType string    `db:"game_type_cd"`
63         ServerID int       `db:"server_id"`
64         Duration int       `db:"duration"`
65         CreateDt time.Time `db:"create_dt"`
66 }
67
68 type PlayerGameStat struct {
69         PlayerGameStatID int    `db:"player_game_stat_id"`
70         PlayerID         int    `db:"player_id"`
71         GameID           int    `db:"game_id"`
72         Nick             string `db:"stripped_nick"`
73         AliveTime        int    `db:"alivetime"`
74         Score            int    `db:"score"`
75 }
76
77 type KReducer struct {
78         // Time in seconds required for full points
79         FullTime int
80
81         // The minimum time a player must play in the game
82         MinTime int
83
84         // The minimum ratio of time played in the game
85         MinRatio float64
86 }
87
88 func (kr *KReducer) Evaluate(pgstat PlayerGameStat, game Game) float64 {
89         k := 1.0
90
91         if pgstat.AliveTime < kr.FullTime {
92                 k = float64(pgstat.AliveTime) / float64(kr.FullTime)
93         }
94
95         if pgstat.AliveTime < kr.MinTime || game.Duration < kr.MinTime {
96                 k = 0
97         }
98
99         if (float64(pgstat.AliveTime) / float64(game.Duration)) < kr.MinRatio {
100                 k = 0
101         }
102
103         return k
104 }
105
106 type GameProcessor struct {
107         config *Config
108         db     *sqlx.DB
109 }
110
111 func NewGameProcessor(config *Config) *GameProcessor {
112         processor := new(GameProcessor)
113
114         processor.config = config
115
116         db, err := sqlx.Connect("postgres", config.ConnStr)
117         if err != nil {
118                 log.Fatal(err)
119         }
120         processor.db = db
121
122         return processor
123 }
124
125 func (gp *GameProcessor) GamesInRange() []Game {
126         games := []Game{}
127
128         sql := `select game_id, game_type_cd, server_id, EXTRACT(EPOCH FROM duration) duration, 
129         create_dt from games where game_id between $1 and $2 order by game_id`
130
131         err := gp.db.Select(&games, sql, gp.config.StartGameID, gp.config.EndGameID)
132         if err != nil {
133                 log.Fatalf("Unable to select games: %s.\n", err)
134         }
135
136         return games
137 }
138
139 func (gp *GameProcessor) PlayerGameStats(gameID int) []PlayerGameStat {
140         pgstats := []PlayerGameStat{}
141
142         sql := `select player_game_stat_id, player_id, game_id, stripped_nick, 
143         EXTRACT(EPOCH from alivetime) alivetime, score from player_game_stats 
144         where game_id = $1 and player_id > 2 order by player_game_stat_id`
145
146         err := gp.db.Select(&pgstats, sql, gameID)
147         if err != nil {
148                 log.Fatalf("Unable to select player_game_stats for game %d: %s.\n", gameID, err)
149         }
150
151         return pgstats
152 }
153
154 func main() {
155         path := flag.String("config", "xs_glicko.json", "configuration file path")
156         start := flag.Int("start", DefaultStartGameID, "starting game_id")
157         end := flag.Int("end", DefaultEndGameID, "ending game_id")
158         days := flag.Int("days", DefaultRankingWindowDays, "number of days in the ranking window")
159         flag.Parse()
160
161         config, err := loadConfig(*path)
162         if err != nil {
163                 log.Fatalf("Unable to load config file: %s.\n", err)
164         }
165
166         if *start != DefaultStartGameID {
167                 config.StartGameID = *start
168         }
169
170         if *end != DefaultEndGameID {
171                 config.EndGameID = *end
172         }
173
174         if *days != DefaultRankingWindowDays {
175                 config.RankingWindowDays = *days
176         }
177
178         processor := NewGameProcessor(config)
179         for _, game := range processor.GamesInRange() {
180                 pgstats := processor.PlayerGameStats(game.GameID)
181                 fmt.Println(pgstats)
182         }
183 }