11 "github.com/jmoiron/sqlx"
15 const DefaultStartGameID = 0
16 const DefaultEndGameID = -1
17 const DefaultRankingWindowDays = 7
20 // database connection string
23 // the starting game_id in the games table
26 // the ending game_id in the games table
29 // the number of days constituting the ranking window
33 func loadConfig(path string) (*Config, error) {
37 config.ConnStr = "user=xonstat host=localhost dbname=xonstatdb sslmode=disable"
38 config.StartGameID = DefaultStartGameID
39 config.EndGameID = DefaultEndGameID
40 config.RankingWindowDays = DefaultRankingWindowDays
42 file, err := os.Open(path)
44 fmt.Println("Failed opening the file.")
48 decoder := json.NewDecoder(file)
50 // overwrite in-mem config with new values
51 err = decoder.Decode(config)
53 fmt.Println("Failed to decode the JSON.")
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"`
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"`
77 type KReducer struct {
78 // Time in seconds required for full points
81 // The minimum time a player must play in the game
84 // The minimum ratio of time played in the game
88 func (kr *KReducer) Evaluate(pgstat PlayerGameStat, game Game) float64 {
91 if pgstat.AliveTime < kr.FullTime {
92 k = float64(pgstat.AliveTime) / float64(kr.FullTime)
95 if pgstat.AliveTime < kr.MinTime || game.Duration < kr.MinTime {
99 if (float64(pgstat.AliveTime) / float64(game.Duration)) < kr.MinRatio {
106 type GameProcessor struct {
111 func NewGameProcessor(config *Config) *GameProcessor {
112 processor := new(GameProcessor)
114 processor.config = config
116 db, err := sqlx.Connect("postgres", config.ConnStr)
125 func (gp *GameProcessor) GamesInRange() []Game {
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`
131 err := gp.db.Select(&games, sql, gp.config.StartGameID, gp.config.EndGameID)
133 log.Fatalf("Unable to select games: %s.\n", err)
139 func (gp *GameProcessor) PlayerGameStats(gameID int) []PlayerGameStat {
140 pgstats := []PlayerGameStat{}
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`
146 err := gp.db.Select(&pgstats, sql, gameID)
148 log.Fatalf("Unable to select player_game_stats for game %d: %s.\n", gameID, err)
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")
161 config, err := loadConfig(*path)
163 log.Fatalf("Unable to load config file: %s.\n", err)
166 if *start != DefaultStartGameID {
167 config.StartGameID = *start
170 if *end != DefaultEndGameID {
171 config.EndGameID = *end
174 if *days != DefaultRankingWindowDays {
175 config.RankingWindowDays = *days
178 processor := NewGameProcessor(config)
179 for _, game := range processor.GamesInRange() {
180 pgstats := processor.PlayerGameStats(game.GameID)