From 21f7e74bb3d8d649d6bd3b54a2d7b59a6803cd33 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Thu, 10 Mar 2022 17:23:01 +0100 Subject: [PATCH 01/67] Initial work on adding commands for recalculating statistics --- cmd/cricket.go | 29 ++ cmd/recalculate.go | 30 ++ cmd/root.go | 27 ++ cmd/serve.go | 143 +++++++ cmd/shootout.go | 29 ++ cmd/x01.go | 26 ++ data/leg.go | 45 +++ data/statistics_cricket.go | 40 ++ data/statistics_shootout.go | 33 +- data/statistics_x01.go | 21 - go.mod | 10 +- go.sum | 753 ++++++++++++++++++++++++++++++++++++ kcapp-api.go | 129 ------ main.go | 11 + 14 files changed, 1165 insertions(+), 161 deletions(-) create mode 100644 cmd/cricket.go create mode 100644 cmd/recalculate.go create mode 100644 cmd/root.go create mode 100644 cmd/serve.go create mode 100644 cmd/shootout.go create mode 100644 cmd/x01.go delete mode 100644 kcapp-api.go create mode 100644 main.go diff --git a/cmd/cricket.go b/cmd/cricket.go new file mode 100644 index 00000000..db1a35a1 --- /dev/null +++ b/cmd/cricket.go @@ -0,0 +1,29 @@ +package cmd + +import ( + "log" + + "github.com/kcapp/api/data" + "github.com/spf13/cobra" +) + +// cricketCmd represents the cricket command +var cricketCmd = &cobra.Command{ + Use: "cricket", + Short: "Recalculate Cricket statistics", + Long: `Recalculate Cricket statistics`, + Run: func(cmd *cobra.Command, args []string) { + since, _ := cmd.Flags().GetString("since") + dryRun, _ := cmd.Flags().GetBool("dry-run") + + log.Printf("Recalculating Cricket Statistics since=%s, dryRun=%t", since, dryRun) + err := data.ReCalculateCricketStatistics(since, dryRun) + if err != nil { + panic(err) + } + }, +} + +func init() { + recalculateCmd.AddCommand(cricketCmd) +} diff --git a/cmd/recalculate.go b/cmd/recalculate.go new file mode 100644 index 00000000..f434194d --- /dev/null +++ b/cmd/recalculate.go @@ -0,0 +1,30 @@ +package cmd + +import ( + "github.com/kcapp/api/models" + "github.com/spf13/cobra" +) + +// recalculateCmd represents the recalculate command +var recalculateCmd = &cobra.Command{ + Use: "recalculate", + Short: "Recalculate statistics", + Long: `Recalculate statistics for the given match type`, + PersistentPreRun: func(cmd *cobra.Command, args []string) { + configFileParam, err := cmd.Flags().GetString("config") + if err != nil { + panic(err) + } + config, err := models.GetConfig(configFileParam) + if err != nil { + panic(err) + } + models.InitDB(config.GetMysqlConnectionString()) + }, +} + +func init() { + rootCmd.AddCommand(recalculateCmd) + recalculateCmd.PersistentFlags().Bool("dry-run", true, "Print queries instead of executing") + recalculateCmd.PersistentFlags().StringP("since", "s", "", "Recalculate statistics newer than the given date") +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 00000000..48d07389 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,27 @@ +package cmd + +import ( + "os" + + "github.com/spf13/cobra" +) + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "api", + Short: "Backend API for kcapp frontend", + Long: `kcapp-api is the backend API for kcapp dart scoring application frontend`, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + rootCmd.PersistentFlags().StringP("config", "c", "config/config.yaml", "Config file") +} diff --git a/cmd/serve.go b/cmd/serve.go new file mode 100644 index 00000000..1af787b1 --- /dev/null +++ b/cmd/serve.go @@ -0,0 +1,143 @@ +package cmd + +import ( + "fmt" + "log" + "net/http" + + "github.com/gorilla/mux" + "github.com/kcapp/api/controllers" + controllers_v2 "github.com/kcapp/api/controllers/v2" + "github.com/kcapp/api/models" + "github.com/spf13/cobra" +) + +// serveCmd represents the serve command +var serveCmd = &cobra.Command{ + Use: "serve", + Short: "Start serving the API", + Long: `Start serving the API`, + Run: func(cmd *cobra.Command, args []string) { + configFileParam, _ := cmd.Flags().GetString("config") + config, err := models.GetConfig(configFileParam) + if err != nil { + panic(err) + } + models.InitDB(config.GetMysqlConnectionString()) + + router := mux.NewRouter() + router.HandleFunc("/health", controllers.Healthcheck).Methods("HEAD") + + router.HandleFunc("/match", controllers.NewMatch).Methods("POST") + router.HandleFunc("/match/active", controllers.GetActiveMatches).Methods("GET") + router.HandleFunc("/match/types", controllers.GetMatchesTypes).Methods("GET") + router.HandleFunc("/match/modes", controllers.GetMatchesModes).Methods("GET") + router.HandleFunc("/match/outshot", controllers.GetOutshotTypes).Methods("GET") + router.HandleFunc("/match", controllers.GetMatches).Methods("GET") + router.HandleFunc("/match/{id}", controllers.GetMatch).Methods("GET") + router.HandleFunc("/match/{id}/metadata", controllers.GetMatchMetadata).Methods("GET") + router.HandleFunc("/match/{id}/rematch", controllers.ReMatch).Methods("POST") + router.HandleFunc("/match/{id}/statistics", controllers.GetStatisticsForMatch).Methods("GET") + router.HandleFunc("/match/{id}/legs", controllers.GetLegsForMatch).Methods("GET") + router.HandleFunc("/match/{start}/{limit}", controllers.GetMatchesLimit).Methods("GET") + + router.HandleFunc("/leg/active", controllers.GetActiveLegs).Methods("GET") + router.HandleFunc("/leg/{id}", controllers.GetLeg).Methods("GET") + router.HandleFunc("/leg/{id}", controllers.DeleteLeg).Methods("DELETE") + router.HandleFunc("/leg/{id}/statistics", controllers.GetStatisticsForLeg).Methods("GET") + router.HandleFunc("/leg/{id}/players", controllers.GetLegPlayers).Methods("GET") + router.HandleFunc("/leg/{id}/order", controllers.ChangePlayerOrder).Methods("PUT") + router.HandleFunc("/leg/{id}/warmup", controllers.StartWarmup).Methods("PUT") + router.HandleFunc("/leg/{id}/undo", controllers.UndoFinishLeg).Methods("PUT") + + router.HandleFunc("/visit", controllers.AddVisit).Methods("POST") + router.HandleFunc("/visit/{id}/modify", controllers.ModifyVisit).Methods("PUT") + router.HandleFunc("/visit/{id}", controllers.DeleteVisit).Methods("DELETE") + router.HandleFunc("/visit/{leg_id}/last", controllers.DeleteLastVisit).Methods("DELETE") + + router.HandleFunc("/player", controllers.GetPlayers).Methods("GET") + router.HandleFunc("/player/active", controllers.GetActivePlayers).Methods("GET") + router.HandleFunc("/player/compare", controllers.GetPlayersX01Statistics).Methods("GET") + router.HandleFunc("/player/{id}", controllers.GetPlayer).Methods("GET") + router.HandleFunc("/player/{id}", controllers.UpdatePlayer).Methods("PUT") + router.HandleFunc("/player/{id}/statistics", controllers.GetPlayerStatistics).Methods("GET") + router.HandleFunc("/player/{id}/statistics/previous", controllers.GetPlayerX01PreviousStatistics).Methods("GET") + router.HandleFunc("/player/{id}/progression", controllers.GetPlayerProgression).Methods("GET") + router.HandleFunc("/player/{id}/checkouts", controllers.GetPlayerCheckouts).Methods("GET") + router.HandleFunc("/player/{id}/tournament", controllers.GetPlayerTournamentStandings).Methods("GET") + router.HandleFunc("/player/{id}/elo/{start}/{limit}", controllers.GetPlayerEloChangelog).Methods("GET") + router.HandleFunc("/player/{player_1}/vs/{player_2}", controllers.GetPlayerHeadToHead).Methods("GET") + router.HandleFunc("/player/{player_1}/vs/{player_2}/simulate", controllers.SimulateMatch).Methods("PUT") + router.HandleFunc("/player", controllers.AddPlayer).Methods("POST") + router.HandleFunc("/player/{id}/calendar", controllers.GetPlayerCalendar).Methods("GET") + router.HandleFunc("/player/{id}/random/{starting_score}", controllers.GetRandomLegForPlayer).Methods("GET") + router.HandleFunc("/player/{id}/statistics/{match_type}", controllers.GetPlayerMatchTypeStatistics).Methods("GET") + router.HandleFunc("/player/{id}/statistics/{match_type}/history/{limit}", controllers.GetPlayerMatchTypeHistory).Methods("GET") + + // v2 + router.HandleFunc("/players", controllers_v2.GetPlayers).Methods("GET") + + router.HandleFunc("/preset", controllers.AddPreset).Methods("POST") + router.HandleFunc("/preset", controllers.GetPresets).Methods("GET") + router.HandleFunc("/preset/{id}", controllers.GetPreset).Methods("GET") + router.HandleFunc("/preset/{id}", controllers.UpdatePreset).Methods("PUT") + router.HandleFunc("/preset/{id}", controllers.DeletePreset).Methods("DELETE") + + router.HandleFunc("/statistics/global", controllers.GetGlobalStatistics).Methods("GET") + router.HandleFunc("/statistics/global/fnc", controllers.GetGlobalStatisticsFnc).Methods("GET") + router.HandleFunc("/statistics/office/{from}/{to}", controllers.GetOfficeStatistics).Methods("GET") + router.HandleFunc("/statistics/office/{office_id}/{from}/{to}", controllers.GetOfficeStatistics).Methods("GET") + router.HandleFunc("/statistics/{dart}/hits", controllers.GetDartStatistics).Methods("GET") + router.HandleFunc("/statistics/{match_type}/{from}/{to}", controllers.GetStatistics).Methods("GET") + + router.HandleFunc("/owe", controllers.GetOwes).Methods("GET") + router.HandleFunc("/owe/payback", controllers.RegisterPayback).Methods("PUT") + + router.HandleFunc("/owetype", controllers.GetOweTypes).Methods("GET") + + router.HandleFunc("/office", controllers.AddOffice).Methods("POST") + router.HandleFunc("/office/{id}", controllers.UpdateOffice).Methods("PUT") + router.HandleFunc("/office", controllers.GetOffices).Methods("GET") + + router.HandleFunc("/venue", controllers.AddVenue).Methods("POST") + router.HandleFunc("/venue/{id}", controllers.UpdateVenue).Methods("PUT") + router.HandleFunc("/venue", controllers.GetVenues).Methods("GET") + router.HandleFunc("/venue/{id}", controllers.GetVenue).Methods("GET") + router.HandleFunc("/venue/{id}/config", controllers.GetVenueConfiguration).Methods("GET") + router.HandleFunc("/venue/{id}/spectate", controllers.SpectateVenue).Methods("GET") + router.HandleFunc("/venue/{id}/players", controllers.GetRecentPlayers).Methods("GET") + router.HandleFunc("/venue/{id}/matches", controllers.GetActiveVenueMatches).Methods("GET") + + router.HandleFunc("/tournament", controllers.NewTournament).Methods("POST") + router.HandleFunc("/tournament", controllers.GetTournaments).Methods("GET") + router.HandleFunc("/tournament/current", controllers.GetCurrentTournament).Methods("GET") + router.HandleFunc("/tournament/current/{office_id}", controllers.GetCurrentTournamentForOffice).Methods("GET") + router.HandleFunc("/tournament/groups", controllers.AddTournamentGroup).Methods("POST") + router.HandleFunc("/tournament/groups", controllers.GetTournamentGroups).Methods("GET") + router.HandleFunc("/tournament/standings", controllers.GetTournamentStandings).Methods("GET") + router.HandleFunc("/tournament/{id}", controllers.GetTournament).Methods("GET") + router.HandleFunc("/tournament/{id}/player/{player_id}", controllers.GetTournamentPlayerMatches).Methods("GET") + router.HandleFunc("/tournament/{id}/matches", controllers.GetTournamentMatches).Methods("GET") + router.HandleFunc("/tournament/{id}/metadata", controllers.GetMatchMetadataForTournament).Methods("GET") + router.HandleFunc("/tournament/{id}/overview", controllers.GetTournamentOverview).Methods("GET") + router.HandleFunc("/tournament/{id}/statistics", controllers.GetTournamentStatistics).Methods("GET") + router.HandleFunc("/tournament/match/{id}/next", controllers.GetNextTournamentMatch).Methods("GET") + + log.Printf("Listening on port %d", config.APIConfig.Port) + log.Println(http.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", config.APIConfig.Port), router)) + }, +} + +func init() { + rootCmd.AddCommand(serveCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // serveCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // serveCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/shootout.go b/cmd/shootout.go new file mode 100644 index 00000000..03fdfa08 --- /dev/null +++ b/cmd/shootout.go @@ -0,0 +1,29 @@ +package cmd + +import ( + "log" + + "github.com/kcapp/api/data" + "github.com/spf13/cobra" +) + +// shootoutCmd represents the shootout command +var shootoutCmd = &cobra.Command{ + Use: "shootout", + Short: "Recalculate 9 Dart Shootout statistics", + Long: `Recalculate 9 Dart Shootout statistics`, + Run: func(cmd *cobra.Command, args []string) { + since, _ := cmd.Flags().GetString("since") + dryRun, _ := cmd.Flags().GetBool("dry-run") + + log.Printf("Recalculating 9 Dart Shootout Statistics since=%s, dryRun=%t", since, dryRun) + err := data.ReCalculateShootoutStatistics(since, dryRun) + if err != nil { + panic(err) + } + }, +} + +func init() { + recalculateCmd.AddCommand(shootoutCmd) +} diff --git a/cmd/x01.go b/cmd/x01.go new file mode 100644 index 00000000..97263d79 --- /dev/null +++ b/cmd/x01.go @@ -0,0 +1,26 @@ +package cmd + +import ( + "log" + + "github.com/kcapp/api/data" + "github.com/spf13/cobra" +) + +// x01Cmd represents the x01 command +var x01Cmd = &cobra.Command{ + Use: "x01", + Short: "Recalculate x01 statistics", + Long: `Recalculate x01 statistics`, + Run: func(cmd *cobra.Command, args []string) { + since, _ := cmd.Flags().GetString("since") + dryRun, _ := cmd.Flags().GetBool("dry-run") + + log.Printf("Recalculating x01 since=%s, dryRun=%t", since, dryRun) + data.RecalculateX01Statistics() + }, +} + +func init() { + recalculateCmd.AddCommand(x01Cmd) +} diff --git a/data/leg.go b/data/leg.go index e6c90d24..2837eb0b 100644 --- a/data/leg.go +++ b/data/leg.go @@ -737,6 +737,51 @@ func GetLegsOfType(matchType int, loadVisits bool) ([]*models.Leg, error) { return legs, nil } +// GetLegsToRecalculate returns all legs since the given date which can be recalculated +func GetLegsToRecalculate(matchType int, since string) ([]*models.Leg, error) { + rows, err := models.DB.Query(` + SELECT + l.id, l.end_time, l.starting_score, l.is_finished, + l.current_player_id, l.winner_id, l.created_at, l.updated_at, + l.match_id, l.has_scores, GROUP_CONCAT(p2l.player_id ORDER BY p2l.order ASC) + FROM leg l + JOIN matches m on m.id = l.match_id + JOIN player2leg p2l ON p2l.leg_id = l.id + WHERE l.has_scores = 1 AND (m.match_type_id = ? OR l.leg_type_id = ?) + AND l.updated_at >= ? + AND m.is_abandoned = 0 AND l.is_finished = 1 AND l.has_scores = 1 + GROUP BY l.id + ORDER BY l.id DESC`, matchType, matchType, since) + if err != nil { + return nil, err + } + defer rows.Close() + + legs := make([]*models.Leg, 0) + for rows.Next() { + leg := new(models.Leg) + var players string + err := rows.Scan(&leg.ID, &leg.Endtime, &leg.StartingScore, &leg.IsFinished, &leg.CurrentPlayerID, + &leg.WinnerPlayerID, &leg.CreatedAt, &leg.UpdatedAt, &leg.MatchID, &leg.HasScores, &players) + if err != nil { + return nil, err + } + leg.Players = util.StringToIntArray(players) + if matchType == models.TICTACTOE || matchType == models.KNOCKOUT { + leg.Parameters, err = GetLegParameters(leg.ID) + if err != nil { + return nil, err + } + } + legs = append(legs, leg) + } + if err = rows.Err(); err != nil { + return nil, err + } + + return legs, nil +} + // GetActiveLegs returns all legs which are currently live func GetActiveLegs() ([]*models.Leg, error) { rows, err := models.DB.Query(` diff --git a/data/statistics_cricket.go b/data/statistics_cricket.go index 3e00fb79..17eefd3c 100644 --- a/data/statistics_cricket.go +++ b/data/statistics_cricket.go @@ -2,6 +2,8 @@ package data import ( "database/sql" + "fmt" + "log" "github.com/guregu/null" @@ -288,3 +290,41 @@ func CalculateCricketStatistics(legID int) (map[int]*models.StatisticsCricket, e } return statisticsMap, nil } + +// ReCalculateCricketStatistics will recaulcate statistics for Cricket matches +func ReCalculateCricketStatistics(since string, dryRun bool) error { + legs, err := GetLegsToRecalculate(models.CRICKET, since) + if err != nil { + return err + } + + queries := make([]string, 0) + for _, leg := range legs { + stats, err := CalculateCricketStatistics(leg.ID) + if err != nil { + return err + } + for playerID, stat := range stats { + + queries = append(queries, fmt.Sprintf(`UPDATE statistics_cricket SET total_marks = %d, rounds = %d, score = %d, first_nine_marks = %d mpr = %f, first_nine_mpr = %f, marks5 = %d, marks6 = %d, marks7 = %d, marks8 = %d, marks9 = %d, WHERE leg_id = %d AND player_id = %d;`, + stat.TotalMarks, stat.Rounds, stat.Score.Int64, stat.FirstNineMarks, stat.MPR, stat.FirstNineMPR, stat.Marks5, stat.Marks6, stat.Marks7, stat.Marks8, stat.Marks9, leg.ID, playerID)) + } + } + if dryRun { + for _, query := range queries { + log.Print(query) + } + } else { + log.Printf("Executing %d UPDATE queries", len(queries)) + tx, err := models.DB.Begin() + if err != nil { + return err + } + for _, query := range queries { + tx.Exec(query) + } + tx.Commit() + } + + return nil +} diff --git a/data/statistics_shootout.go b/data/statistics_shootout.go index 3c0ea4aa..38b194ec 100644 --- a/data/statistics_shootout.go +++ b/data/statistics_shootout.go @@ -2,6 +2,7 @@ package data import ( "database/sql" + "fmt" "log" "github.com/kcapp/api/models" @@ -249,24 +250,38 @@ func CalculateShootoutStatistics(legID int) (map[int]*models.StatisticsShootout, } // ReCalculateShootoutStatistics will recaulcate statistics for Shootout matches -func ReCalculateShootoutStatistics() (map[int]map[int]*models.StatisticsShootout, error) { - legs, err := GetLegsOfType(models.SHOOTOUT, true) +func ReCalculateShootoutStatistics(since string, dryRun bool) error { + legs, err := GetLegsToRecalculate(models.SHOOTOUT, since) if err != nil { - return nil, err + return err } - s := make(map[int]map[int]*models.StatisticsShootout) + queries := make([]string, 0) for _, leg := range legs { stats, err := CalculateShootoutStatistics(leg.ID) if err != nil { - return nil, err + return err } for playerID, stat := range stats { - log.Printf(`UPDATE kcapp.statistics_shootout SET score = %d, ppd = %f, 60s_plus = %d, 100s_plus = %d, 140s_plus = %d, 180s = %d WHERE leg_id = %d AND player_id = %d;`, - stat.Score, stat.PPD, stat.Score60sPlus, stat.Score100sPlus, stat.Score140sPlus, stat.Score180s, leg.ID, playerID) + queries = append(queries, fmt.Sprintf(`UPDATE statistics_shootout SET score = %d, ppd = %f, 60s_plus = %d, 100s_plus = %d, 140s_plus = %d, 180s = %d WHERE leg_id = %d AND player_id = %d;`, + stat.Score, stat.PPD, stat.Score60sPlus, stat.Score100sPlus, stat.Score140sPlus, stat.Score180s, leg.ID, playerID)) + } + } + if dryRun { + for _, query := range queries { + log.Print(query) + } + } else { + log.Printf("Executing %d UPDATE queries", len(queries)) + tx, err := models.DB.Begin() + if err != nil { + return err + } + for _, query := range queries { + tx.Exec(query) } - s[leg.ID] = stats + tx.Commit() } - return s, err + return nil } diff --git a/data/statistics_x01.go b/data/statistics_x01.go index f7de1a4a..72709802 100644 --- a/data/statistics_x01.go +++ b/data/statistics_x01.go @@ -925,26 +925,5 @@ func RecalculateX01Statistics() (map[int]map[int]*models.StatisticsX01, error) { } m[leg.ID] = stats } - - /*s := make([]*models.CheckoutStatistics, 0) - for _, leg := range legs { - log.Printf("Getting statistics for %d", leg.ID) - stats, err := getCheckoutStatistics(leg.ID, leg.StartingScore) - if err != nil { - return nil, err - } - s = append(s, stats) - } - - all := make(map[int]int) - for _, stats := range s { - log.Printf("Checkout: %d, Total: %d, Attempts: %d", stats.Checkout, stats.Count, stats.CheckoutAttempts) - - for checkout, count := range stats.CheckoutAttempts { - all[checkout] += count - } - } - log.Printf("All: %v", all)*/ - return m, err } diff --git a/go.mod b/go.mod index 31447896..884a6201 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/kcapp/api go 1.17 require ( - github.com/davecgh/go-spew v1.1.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-sql-driver/mysql v1.6.0 github.com/gorilla/mux v1.8.0 github.com/guregu/null v4.0.0+incompatible @@ -12,5 +12,11 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/testify v1.7.0 // indirect gopkg.in/yaml.v2 v2.4.0 - gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect +) + +require ( + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/spf13/cobra v1.3.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect ) diff --git a/go.sum b/go.sum index 46c1acad..88ac5eed 100644 --- a/go.sum +++ b/go.sum @@ -1,26 +1,779 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/guregu/null v4.0.0+incompatible h1:4zw0ckM7ECd6FNNddc3Fu4aty9nTlpkkzH7dPn4/4Gw= github.com/guregu/null v4.0.0+incompatible/go.mod h1:ePGpQaN9cw0tj45IR5E5ehMvsFlLlQZAkkOXZurJ3NM= +github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= +github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= +github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= +github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= github.com/jmoiron/sqlx v1.3.4 h1:wv+0IJZfL5z0uZoUjlpKgHkgaFSYD+r9CfrXjEXsO7w= github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= github.com/jordic/goics v0.0.0-20210404174824-5a0337b716a0 h1:p+k2RozdR141dIkAbOuZafkZjrcjT/YvwYYH7qCSG+c= github.com/jordic/goics v0.0.0-20210404174824-5a0337b716a0/go.mod h1:YHaw6sOIeFRob8Y9q/blEAMfVcLpeE9+vdhrwyEMxoI= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.3.0 h1:R7cSvGu+Vv+qX0gW5R/85dx2kmmJT5z5NM8ifdYjdn0= +github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/kcapp-api.go b/kcapp-api.go deleted file mode 100644 index 64ce0d8b..00000000 --- a/kcapp-api.go +++ /dev/null @@ -1,129 +0,0 @@ -package main - -import ( - "fmt" - "log" - "net/http" - "os" - - "github.com/gorilla/mux" - "github.com/kcapp/api/controllers" - controllers_v2 "github.com/kcapp/api/controllers/v2" - "github.com/kcapp/api/models" -) - -// our main function -func main() { - var configFileParam string - - if len(os.Args) > 1 { - configFileParam = os.Args[1] - } - - config, err := models.GetConfig(configFileParam) - if err != nil { - panic(err) - } - models.InitDB(config.GetMysqlConnectionString()) - - router := mux.NewRouter() - router.HandleFunc("/health", controllers.Healthcheck).Methods("HEAD") - - router.HandleFunc("/match", controllers.NewMatch).Methods("POST") - router.HandleFunc("/match/active", controllers.GetActiveMatches).Methods("GET") - router.HandleFunc("/match/types", controllers.GetMatchesTypes).Methods("GET") - router.HandleFunc("/match/modes", controllers.GetMatchesModes).Methods("GET") - router.HandleFunc("/match/outshot", controllers.GetOutshotTypes).Methods("GET") - router.HandleFunc("/match", controllers.GetMatches).Methods("GET") - router.HandleFunc("/match/{id}", controllers.GetMatch).Methods("GET") - router.HandleFunc("/match/{id}/metadata", controllers.GetMatchMetadata).Methods("GET") - router.HandleFunc("/match/{id}/rematch", controllers.ReMatch).Methods("POST") - router.HandleFunc("/match/{id}/statistics", controllers.GetStatisticsForMatch).Methods("GET") - router.HandleFunc("/match/{id}/legs", controllers.GetLegsForMatch).Methods("GET") - router.HandleFunc("/match/{start}/{limit}", controllers.GetMatchesLimit).Methods("GET") - - router.HandleFunc("/leg/active", controllers.GetActiveLegs).Methods("GET") - router.HandleFunc("/leg/{id}", controllers.GetLeg).Methods("GET") - router.HandleFunc("/leg/{id}", controllers.DeleteLeg).Methods("DELETE") - router.HandleFunc("/leg/{id}/statistics", controllers.GetStatisticsForLeg).Methods("GET") - router.HandleFunc("/leg/{id}/players", controllers.GetLegPlayers).Methods("GET") - router.HandleFunc("/leg/{id}/order", controllers.ChangePlayerOrder).Methods("PUT") - router.HandleFunc("/leg/{id}/warmup", controllers.StartWarmup).Methods("PUT") - router.HandleFunc("/leg/{id}/undo", controllers.UndoFinishLeg).Methods("PUT") - - router.HandleFunc("/visit", controllers.AddVisit).Methods("POST") - router.HandleFunc("/visit/{id}/modify", controllers.ModifyVisit).Methods("PUT") - router.HandleFunc("/visit/{id}", controllers.DeleteVisit).Methods("DELETE") - router.HandleFunc("/visit/{leg_id}/last", controllers.DeleteLastVisit).Methods("DELETE") - - router.HandleFunc("/player", controllers.GetPlayers).Methods("GET") - router.HandleFunc("/player/active", controllers.GetActivePlayers).Methods("GET") - router.HandleFunc("/player/compare", controllers.GetPlayersX01Statistics).Methods("GET") - router.HandleFunc("/player/{id}", controllers.GetPlayer).Methods("GET") - router.HandleFunc("/player/{id}", controllers.UpdatePlayer).Methods("PUT") - router.HandleFunc("/player/{id}/statistics", controllers.GetPlayerStatistics).Methods("GET") - router.HandleFunc("/player/{id}/statistics/previous", controllers.GetPlayerX01PreviousStatistics).Methods("GET") - router.HandleFunc("/player/{id}/progression", controllers.GetPlayerProgression).Methods("GET") - router.HandleFunc("/player/{id}/checkouts", controllers.GetPlayerCheckouts).Methods("GET") - router.HandleFunc("/player/{id}/tournament", controllers.GetPlayerTournamentStandings).Methods("GET") - router.HandleFunc("/player/{id}/elo/{start}/{limit}", controllers.GetPlayerEloChangelog).Methods("GET") - router.HandleFunc("/player/{player_1}/vs/{player_2}", controllers.GetPlayerHeadToHead).Methods("GET") - router.HandleFunc("/player/{player_1}/vs/{player_2}/simulate", controllers.SimulateMatch).Methods("PUT") - router.HandleFunc("/player", controllers.AddPlayer).Methods("POST") - router.HandleFunc("/player/{id}/calendar", controllers.GetPlayerCalendar).Methods("GET") - router.HandleFunc("/player/{id}/random/{starting_score}", controllers.GetRandomLegForPlayer).Methods("GET") - router.HandleFunc("/player/{id}/statistics/{match_type}", controllers.GetPlayerMatchTypeStatistics).Methods("GET") - router.HandleFunc("/player/{id}/statistics/{match_type}/history/{limit}", controllers.GetPlayerMatchTypeHistory).Methods("GET") - - // v2 - router.HandleFunc("/players", controllers_v2.GetPlayers).Methods("GET") - - router.HandleFunc("/preset", controllers.AddPreset).Methods("POST") - router.HandleFunc("/preset", controllers.GetPresets).Methods("GET") - router.HandleFunc("/preset/{id}", controllers.GetPreset).Methods("GET") - router.HandleFunc("/preset/{id}", controllers.UpdatePreset).Methods("PUT") - router.HandleFunc("/preset/{id}", controllers.DeletePreset).Methods("DELETE") - - router.HandleFunc("/statistics/global", controllers.GetGlobalStatistics).Methods("GET") - router.HandleFunc("/statistics/global/fnc", controllers.GetGlobalStatisticsFnc).Methods("GET") - router.HandleFunc("/statistics/office/{from}/{to}", controllers.GetOfficeStatistics).Methods("GET") - router.HandleFunc("/statistics/office/{office_id}/{from}/{to}", controllers.GetOfficeStatistics).Methods("GET") - router.HandleFunc("/statistics/{dart}/hits", controllers.GetDartStatistics).Methods("GET") - router.HandleFunc("/statistics/{match_type}/{from}/{to}", controllers.GetStatistics).Methods("GET") - - router.HandleFunc("/owe", controllers.GetOwes).Methods("GET") - router.HandleFunc("/owe/payback", controllers.RegisterPayback).Methods("PUT") - - router.HandleFunc("/owetype", controllers.GetOweTypes).Methods("GET") - - router.HandleFunc("/office", controllers.AddOffice).Methods("POST") - router.HandleFunc("/office/{id}", controllers.UpdateOffice).Methods("PUT") - router.HandleFunc("/office", controllers.GetOffices).Methods("GET") - - router.HandleFunc("/venue", controllers.AddVenue).Methods("POST") - router.HandleFunc("/venue/{id}", controllers.UpdateVenue).Methods("PUT") - router.HandleFunc("/venue", controllers.GetVenues).Methods("GET") - router.HandleFunc("/venue/{id}", controllers.GetVenue).Methods("GET") - router.HandleFunc("/venue/{id}/config", controllers.GetVenueConfiguration).Methods("GET") - router.HandleFunc("/venue/{id}/spectate", controllers.SpectateVenue).Methods("GET") - router.HandleFunc("/venue/{id}/players", controllers.GetRecentPlayers).Methods("GET") - router.HandleFunc("/venue/{id}/matches", controllers.GetActiveVenueMatches).Methods("GET") - - router.HandleFunc("/tournament", controllers.NewTournament).Methods("POST") - router.HandleFunc("/tournament", controllers.GetTournaments).Methods("GET") - router.HandleFunc("/tournament/current", controllers.GetCurrentTournament).Methods("GET") - router.HandleFunc("/tournament/current/{office_id}", controllers.GetCurrentTournamentForOffice).Methods("GET") - router.HandleFunc("/tournament/groups", controllers.AddTournamentGroup).Methods("POST") - router.HandleFunc("/tournament/groups", controllers.GetTournamentGroups).Methods("GET") - router.HandleFunc("/tournament/standings", controllers.GetTournamentStandings).Methods("GET") - router.HandleFunc("/tournament/{id}", controllers.GetTournament).Methods("GET") - router.HandleFunc("/tournament/{id}/player/{player_id}", controllers.GetTournamentPlayerMatches).Methods("GET") - router.HandleFunc("/tournament/{id}/matches", controllers.GetTournamentMatches).Methods("GET") - router.HandleFunc("/tournament/{id}/metadata", controllers.GetMatchMetadataForTournament).Methods("GET") - router.HandleFunc("/tournament/{id}/overview", controllers.GetTournamentOverview).Methods("GET") - router.HandleFunc("/tournament/{id}/statistics", controllers.GetTournamentStatistics).Methods("GET") - router.HandleFunc("/tournament/match/{id}/next", controllers.GetNextTournamentMatch).Methods("GET") - - log.Printf("Listening on port %d", config.APIConfig.Port) - log.Println(http.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", config.APIConfig.Port), router)) -} diff --git a/main.go b/main.go new file mode 100644 index 00000000..ee53ed04 --- /dev/null +++ b/main.go @@ -0,0 +1,11 @@ +/* +Copyright © 2022 Thord Setsaas thordendre@gmail.com + +*/ +package main + +import "github.com/kcapp/api/cmd" + +func main() { + cmd.Execute() +} From eaf7680e24a27bc22d059def6a80427f2e0872c4 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Sun, 13 Mar 2022 13:09:10 +0100 Subject: [PATCH 02/67] Added commands for recalculating all statistics --- cmd/aroundtheclock.go | 25 ++++++++++ cmd/aroundtheworld.go | 25 ++++++++++ cmd/bermudatriangle.go | 25 ++++++++++ cmd/cricket.go | 3 +- cmd/dartsatx.go | 25 ++++++++++ cmd/fourtwenty.go | 25 ++++++++++ cmd/gotcha.go | 25 ++++++++++ cmd/jdcpractice.go | 25 ++++++++++ cmd/killbull.go | 25 ++++++++++ cmd/knockout.go | 25 ++++++++++ cmd/recalculate.go | 6 +++ cmd/shanghai.go | 25 ++++++++++ cmd/shootout.go | 6 +-- cmd/tictactoe.go | 25 ++++++++++ cmd/x01.go | 9 ++-- data/leg.go | 4 +- data/statistics_420.go | 35 +++++++++---- data/statistics_around_the_clock.go | 36 ++++++++++---- data/statistics_around_the_world.go | 77 +++++++++++++++++++---------- data/statistics_bermuda_triangle.go | 36 ++++++++++---- data/statistics_cricket.go | 4 +- data/statistics_darts_at_x.go | 40 +++++++++++++++ data/statistics_gotcha.go | 36 ++++++++++---- data/statistics_jdc_practice.go | 35 +++++++++---- data/statistics_kill_bull.go | 36 ++++++++++---- data/statistics_knockout.go | 35 +++++++++---- data/statistics_shootout.go | 4 +- data/statistics_tic_tac_toe.go | 38 ++++++++++++++ data/statistics_x01.go | 76 +++++++++++++--------------- 29 files changed, 635 insertions(+), 156 deletions(-) create mode 100644 cmd/aroundtheclock.go create mode 100644 cmd/aroundtheworld.go create mode 100644 cmd/bermudatriangle.go create mode 100644 cmd/dartsatx.go create mode 100644 cmd/fourtwenty.go create mode 100644 cmd/gotcha.go create mode 100644 cmd/jdcpractice.go create mode 100644 cmd/killbull.go create mode 100644 cmd/knockout.go create mode 100644 cmd/shanghai.go create mode 100644 cmd/tictactoe.go diff --git a/cmd/aroundtheclock.go b/cmd/aroundtheclock.go new file mode 100644 index 00000000..b3b80ccc --- /dev/null +++ b/cmd/aroundtheclock.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "log" + + "github.com/kcapp/api/data" + "github.com/spf13/cobra" +) + +// aroundtheclockCmd represents the aroundtheclock command +var aroundtheclockCmd = &cobra.Command{ + Use: "aroundtheclock", + Short: "Recalculate Around the Clock statistics", + Run: func(cmd *cobra.Command, args []string) { + log.Printf("Recalculating Around the Clock Statistics since=%s, dryRun=%t", since, dryRun) + err := data.RecalculateAroundTheClockStatistics(since, dryRun) + if err != nil { + panic(err) + } + }, +} + +func init() { + recalculateCmd.AddCommand(aroundtheclockCmd) +} diff --git a/cmd/aroundtheworld.go b/cmd/aroundtheworld.go new file mode 100644 index 00000000..7d4bfa3b --- /dev/null +++ b/cmd/aroundtheworld.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "log" + + "github.com/kcapp/api/data" + "github.com/spf13/cobra" +) + +// aroundtheworldCmd represents the aroundtheworld command +var aroundtheworldCmd = &cobra.Command{ + Use: "aroundtheworld", + Short: "Recalculate Around the World statistics", + Run: func(cmd *cobra.Command, args []string) { + log.Printf("Recalculating Around the World Statistics since=%s, dryRun=%t", since, dryRun) + err := data.RecalculateAroundTheWorldStatistics(since, dryRun) + if err != nil { + panic(err) + } + }, +} + +func init() { + recalculateCmd.AddCommand(aroundtheworldCmd) +} diff --git a/cmd/bermudatriangle.go b/cmd/bermudatriangle.go new file mode 100644 index 00000000..af2364d9 --- /dev/null +++ b/cmd/bermudatriangle.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "log" + + "github.com/kcapp/api/data" + "github.com/spf13/cobra" +) + +// bermudatriangleCmd represents the bermudatriangle command +var bermudatriangleCmd = &cobra.Command{ + Use: "bermudatriangle", + Short: "Recalculate Bermuda Triangle statistics", + Run: func(cmd *cobra.Command, args []string) { + log.Printf("Recalculating Bermuda Triangle Statistics since=%s, dryRun=%t", since, dryRun) + err := data.RecalculateBermudaTriangleStatistics(since, dryRun) + if err != nil { + panic(err) + } + }, +} + +func init() { + recalculateCmd.AddCommand(bermudatriangleCmd) +} diff --git a/cmd/cricket.go b/cmd/cricket.go index db1a35a1..d192f8d1 100644 --- a/cmd/cricket.go +++ b/cmd/cricket.go @@ -11,13 +11,12 @@ import ( var cricketCmd = &cobra.Command{ Use: "cricket", Short: "Recalculate Cricket statistics", - Long: `Recalculate Cricket statistics`, Run: func(cmd *cobra.Command, args []string) { since, _ := cmd.Flags().GetString("since") dryRun, _ := cmd.Flags().GetBool("dry-run") log.Printf("Recalculating Cricket Statistics since=%s, dryRun=%t", since, dryRun) - err := data.ReCalculateCricketStatistics(since, dryRun) + err := data.RecalculateCricketStatistics(since, dryRun) if err != nil { panic(err) } diff --git a/cmd/dartsatx.go b/cmd/dartsatx.go new file mode 100644 index 00000000..4baf19fd --- /dev/null +++ b/cmd/dartsatx.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "log" + + "github.com/kcapp/api/data" + "github.com/spf13/cobra" +) + +// dartsatxCmd represents the dartsatx command +var dartsatxCmd = &cobra.Command{ + Use: "dartsatx", + Short: "Recalculate Darts at X statistics", + Run: func(cmd *cobra.Command, args []string) { + log.Printf("Recalculating Darts at X Statistics since=%s, dryRun=%t", since, dryRun) + err := data.RecalculateDartsAtXStatistics(since, dryRun) + if err != nil { + panic(err) + } + }, +} + +func init() { + recalculateCmd.AddCommand(dartsatxCmd) +} diff --git a/cmd/fourtwenty.go b/cmd/fourtwenty.go new file mode 100644 index 00000000..520b48ad --- /dev/null +++ b/cmd/fourtwenty.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "log" + + "github.com/kcapp/api/data" + "github.com/spf13/cobra" +) + +// fourtwentyCmd represents the fourtwenty command +var fourtwentyCmd = &cobra.Command{ + Use: "fourtwenty", + Short: "Recalculate 420 statistics", + Run: func(cmd *cobra.Command, args []string) { + log.Printf("Recalculating 420 Statistics since=%s, dryRun=%t", since, dryRun) + err := data.Recalculate420Statistics(since, dryRun) + if err != nil { + panic(err) + } + }, +} + +func init() { + recalculateCmd.AddCommand(fourtwentyCmd) +} diff --git a/cmd/gotcha.go b/cmd/gotcha.go new file mode 100644 index 00000000..4880e775 --- /dev/null +++ b/cmd/gotcha.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "log" + + "github.com/kcapp/api/data" + "github.com/spf13/cobra" +) + +// gotchaCmd represents the gotcha command +var gotchaCmd = &cobra.Command{ + Use: "gotcha", + Short: "Recalculate Gotcha statistics", + Run: func(cmd *cobra.Command, args []string) { + log.Printf("Recalculating Gotcha Statistics since=%s, dryRun=%t", since, dryRun) + err := data.RecalculateGotchaStatistics(since, dryRun) + if err != nil { + panic(err) + } + }, +} + +func init() { + recalculateCmd.AddCommand(gotchaCmd) +} diff --git a/cmd/jdcpractice.go b/cmd/jdcpractice.go new file mode 100644 index 00000000..724dad63 --- /dev/null +++ b/cmd/jdcpractice.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "log" + + "github.com/kcapp/api/data" + "github.com/spf13/cobra" +) + +// jdcpracticeCmd represents the jdcpractice command +var jdcpracticeCmd = &cobra.Command{ + Use: "jdcpractice", + Short: "Recalculate JDC Practice statistics", + Run: func(cmd *cobra.Command, args []string) { + log.Printf("Recalculating JDC Practice Statistics since=%s, dryRun=%t", since, dryRun) + err := data.RecalculateJDCPracticeStatistics(since, dryRun) + if err != nil { + panic(err) + } + }, +} + +func init() { + recalculateCmd.AddCommand(jdcpracticeCmd) +} diff --git a/cmd/killbull.go b/cmd/killbull.go new file mode 100644 index 00000000..4c9dad2b --- /dev/null +++ b/cmd/killbull.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "log" + + "github.com/kcapp/api/data" + "github.com/spf13/cobra" +) + +// killbullCmd represents the killbull command +var killbullCmd = &cobra.Command{ + Use: "killbull", + Short: "Recalculate Kill Bull statistics", + Run: func(cmd *cobra.Command, args []string) { + log.Printf("Recalculating Kill Bull Statistics since=%s, dryRun=%t", since, dryRun) + err := data.RecalculateKillBullStatistics(since, dryRun) + if err != nil { + panic(err) + } + }, +} + +func init() { + recalculateCmd.AddCommand(killbullCmd) +} diff --git a/cmd/knockout.go b/cmd/knockout.go new file mode 100644 index 00000000..79496b96 --- /dev/null +++ b/cmd/knockout.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "log" + + "github.com/kcapp/api/data" + "github.com/spf13/cobra" +) + +// knockoutCmd represents the knockout command +var knockoutCmd = &cobra.Command{ + Use: "knockout", + Short: "Recalculate Knockout statistics", + Run: func(cmd *cobra.Command, args []string) { + log.Printf("Recalculating Knockout Statistics since=%s, dryRun=%t", since, dryRun) + err := data.RecalculateKnockoutStatistics(since, dryRun) + if err != nil { + panic(err) + } + }, +} + +func init() { + recalculateCmd.AddCommand(knockoutCmd) +} diff --git a/cmd/recalculate.go b/cmd/recalculate.go index f434194d..cc067e56 100644 --- a/cmd/recalculate.go +++ b/cmd/recalculate.go @@ -5,6 +5,9 @@ import ( "github.com/spf13/cobra" ) +var since string +var dryRun bool + // recalculateCmd represents the recalculate command var recalculateCmd = &cobra.Command{ Use: "recalculate", @@ -20,6 +23,9 @@ var recalculateCmd = &cobra.Command{ panic(err) } models.InitDB(config.GetMysqlConnectionString()) + + since, _ = cmd.Flags().GetString("since") + dryRun, _ = cmd.Flags().GetBool("dry-run") }, } diff --git a/cmd/shanghai.go b/cmd/shanghai.go new file mode 100644 index 00000000..daca6af8 --- /dev/null +++ b/cmd/shanghai.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "log" + + "github.com/kcapp/api/data" + "github.com/spf13/cobra" +) + +// shanghaiCmd represents the shanghai command +var shanghaiCmd = &cobra.Command{ + Use: "shanghai", + Short: "Recalculate Shanghai statistics", + Run: func(cmd *cobra.Command, args []string) { + log.Printf("Recalculating Shanghai since=%s, dryRun=%t", since, dryRun) + err := data.RecalculateShanghaiStatistics(since, dryRun) + if err != nil { + panic(err) + } + }, +} + +func init() { + recalculateCmd.AddCommand(shanghaiCmd) +} diff --git a/cmd/shootout.go b/cmd/shootout.go index 03fdfa08..d0ae5e30 100644 --- a/cmd/shootout.go +++ b/cmd/shootout.go @@ -11,13 +11,9 @@ import ( var shootoutCmd = &cobra.Command{ Use: "shootout", Short: "Recalculate 9 Dart Shootout statistics", - Long: `Recalculate 9 Dart Shootout statistics`, Run: func(cmd *cobra.Command, args []string) { - since, _ := cmd.Flags().GetString("since") - dryRun, _ := cmd.Flags().GetBool("dry-run") - log.Printf("Recalculating 9 Dart Shootout Statistics since=%s, dryRun=%t", since, dryRun) - err := data.ReCalculateShootoutStatistics(since, dryRun) + err := data.RecalculateShootoutStatistics(since, dryRun) if err != nil { panic(err) } diff --git a/cmd/tictactoe.go b/cmd/tictactoe.go new file mode 100644 index 00000000..188b16f1 --- /dev/null +++ b/cmd/tictactoe.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "log" + + "github.com/kcapp/api/data" + "github.com/spf13/cobra" +) + +// tictactoeCmd represents the tictactoe command +var tictactoeCmd = &cobra.Command{ + Use: "tictactoe", + Short: "Recalculate Tic-Tac-Toe statistics", + Run: func(cmd *cobra.Command, args []string) { + log.Printf("Recalculating Tic-Tac-Toe since=%s, dryRun=%t", since, dryRun) + err := data.RecalculateTicTacToeStatistics(since, dryRun) + if err != nil { + panic(err) + } + }, +} + +func init() { + recalculateCmd.AddCommand(tictactoeCmd) +} diff --git a/cmd/x01.go b/cmd/x01.go index 97263d79..9fd9eee6 100644 --- a/cmd/x01.go +++ b/cmd/x01.go @@ -11,13 +11,12 @@ import ( var x01Cmd = &cobra.Command{ Use: "x01", Short: "Recalculate x01 statistics", - Long: `Recalculate x01 statistics`, Run: func(cmd *cobra.Command, args []string) { - since, _ := cmd.Flags().GetString("since") - dryRun, _ := cmd.Flags().GetBool("dry-run") - log.Printf("Recalculating x01 since=%s, dryRun=%t", since, dryRun) - data.RecalculateX01Statistics() + err := data.RecalculateX01Statistics(since, dryRun) + if err != nil { + panic(err) + } }, } diff --git a/data/leg.go b/data/leg.go index 2837eb0b..6a13b8bf 100644 --- a/data/leg.go +++ b/data/leg.go @@ -748,10 +748,10 @@ func GetLegsToRecalculate(matchType int, since string) ([]*models.Leg, error) { JOIN matches m on m.id = l.match_id JOIN player2leg p2l ON p2l.leg_id = l.id WHERE l.has_scores = 1 AND (m.match_type_id = ? OR l.leg_type_id = ?) - AND l.updated_at >= ? + AND l.updated_at >= DATE_FORMAT(STR_TO_DATE(?, '%Y-%m-%d %T'), "%Y-%m-%d %T") AND m.is_abandoned = 0 AND l.is_finished = 1 AND l.has_scores = 1 GROUP BY l.id - ORDER BY l.id DESC`, matchType, matchType, since) + ORDER BY l.id ASC`, matchType, matchType, since) if err != nil { return nil, err } diff --git a/data/statistics_420.go b/data/statistics_420.go index 993055e3..9064bdea 100644 --- a/data/statistics_420.go +++ b/data/statistics_420.go @@ -2,6 +2,7 @@ package data import ( "database/sql" + "fmt" "log" "github.com/kcapp/api/models" @@ -392,28 +393,42 @@ func Calculate420Statistics(legID int) (map[int]*models.Statistics420, error) { return statisticsMap, nil } -// ReCalculate420Statistics will recaulcate statistics for 420 legs -func ReCalculate420Statistics() (map[int]map[int]*models.Statistics420, error) { - legs, err := GetLegsOfType(models.FOURTWENTY, true) +// Recalculate420Statistics will recaulcate statistics for 420 legs +func Recalculate420Statistics(since string, dryRun bool) error { + legs, err := GetLegsToRecalculate(models.FOURTWENTY, since) if err != nil { - return nil, err + return err } - s := make(map[int]map[int]*models.Statistics420) + queries := make([]string, 0) for _, leg := range legs { stats, err := Calculate420Statistics(leg.ID) if err != nil { - return nil, err + return err } for playerID, stat := range stats { - log.Printf(`UPDATE statistics_420 SET score = %d, total_hit_rate = %f, hit_rate_1 = %f, hit_rate_2 = %f, hit_rate_3 = %f, hit_rate_4 = %f, hit_rate_5 = %f, hit_rate_6 = %f, hit_rate_7 = %f, hit_rate_8 = %f, + queries = append(queries, fmt.Sprintf(`UPDATE statistics_420 SET score = %d, total_hit_rate = %f, hit_rate_1 = %f, hit_rate_2 = %f, hit_rate_3 = %f, hit_rate_4 = %f, hit_rate_5 = %f, hit_rate_6 = %f, hit_rate_7 = %f, hit_rate_8 = %f, hit_rate_9 = %f, hit_rate_10 = %f, hit_rate_11 = %f, hit_rate_12 = %f, hit_rate_13 = %f, hit_rate_14 = %f, hit_rate_15 = %f, hit_rate_16 = %f, hit_rate_17 = %f, hit_rate_18 = %f, hit_rate_19 = %f, hit_rate_20 = %f, hit_rate_bull = %f WHERE leg_id = %d AND player_id = %d;`, stat.Score, stat.TotalHitRate, stat.Hitrates[1], stat.Hitrates[2], stat.Hitrates[3], stat.Hitrates[4], stat.Hitrates[5], stat.Hitrates[6], stat.Hitrates[7], stat.Hitrates[8], stat.Hitrates[9], stat.Hitrates[10], - stat.Hitrates[11], stat.Hitrates[12], stat.Hitrates[13], stat.Hitrates[14], stat.Hitrates[15], stat.Hitrates[16], stat.Hitrates[17], stat.Hitrates[18], stat.Hitrates[19], stat.Hitrates[20], stat.Hitrates[25], leg.ID, playerID) + stat.Hitrates[11], stat.Hitrates[12], stat.Hitrates[13], stat.Hitrates[14], stat.Hitrates[15], stat.Hitrates[16], stat.Hitrates[17], stat.Hitrates[18], stat.Hitrates[19], stat.Hitrates[20], stat.Hitrates[25], leg.ID, playerID)) + } + } + if dryRun { + for _, query := range queries { + log.Print(query) + } + } else { + log.Printf("Executing %d UPDATE queries", len(queries)) + tx, err := models.DB.Begin() + if err != nil { + return err + } + for _, query := range queries { + tx.Exec(query) } - s[leg.ID] = stats + tx.Commit() } - return s, err + return nil } diff --git a/data/statistics_around_the_clock.go b/data/statistics_around_the_clock.go index e2e598d6..b6967cd0 100644 --- a/data/statistics_around_the_clock.go +++ b/data/statistics_around_the_clock.go @@ -2,6 +2,7 @@ package data import ( "database/sql" + "fmt" "log" "github.com/guregu/null" @@ -425,28 +426,43 @@ func isHit(stats *models.StatisticsAroundThe, currentScore int, dart *models.Dar return target } -// ReCalculateAroundTheClockStatistics will recaulcate statistics for Around the Clock legs -func ReCalculateAroundTheClockStatistics() (map[int]map[int]*models.StatisticsAroundThe, error) { - legs, err := GetLegsOfType(models.AROUNDTHECLOCK, true) +// RecalculateAroundTheClockStatistics will recaulcate statistics for Around the Clock legs +func RecalculateAroundTheClockStatistics(since string, dryRun bool) error { + legs, err := GetLegsToRecalculate(models.AROUNDTHECLOCK, since) if err != nil { - return nil, err + return err } - s := make(map[int]map[int]*models.StatisticsAroundThe) + queries := make([]string, 0) for _, leg := range legs { stats, err := CalculateAroundTheClockStatistics(leg.ID) if err != nil { - return nil, err + return err } for playerID, stat := range stats { - log.Printf(`UPDATE statistics_around_the SET darts_thrown = %d, score = %d, longest_streak = %d, total_hit_rate = %f, hit_rate_1 = %f, hit_rate_2 = %f, hit_rate_3 = %f, hit_rate_4 = %f, hit_rate_5 = %f, hit_rate_6 = %f, hit_rate_7 = %f, hit_rate_8 = %f, hit_rate_9 = %f, hit_rate_10 = %f, hit_rate_11 = %f, hit_rate_12 = %f, hit_rate_13 = %f, hit_rate_14 = %f, hit_rate_15 = %f, hit_rate_16 = %f, hit_rate_17 = %f, hit_rate_18 = %f, hit_rate_19 = %f, hit_rate_20 = %f, hit_rate_bull = %f WHERE leg_id = %d AND player_id = %d;`, + queries = append(queries, fmt.Sprintf(`UPDATE statistics_around_the SET darts_thrown = %d, score = %d, longest_streak = %d, total_hit_rate = %f, hit_rate_1 = %f, hit_rate_2 = %f, hit_rate_3 = %f, hit_rate_4 = %f, hit_rate_5 = %f, hit_rate_6 = %f, hit_rate_7 = %f, hit_rate_8 = %f, hit_rate_9 = %f, hit_rate_10 = %f, hit_rate_11 = %f, hit_rate_12 = %f, hit_rate_13 = %f, hit_rate_14 = %f, hit_rate_15 = %f, hit_rate_16 = %f, hit_rate_17 = %f, hit_rate_18 = %f, hit_rate_19 = %f, hit_rate_20 = %f, hit_rate_bull = %f WHERE leg_id = %d AND player_id = %d;`, stat.DartsThrown, stat.Score, stat.LongestStreak.Int64, stat.TotalHitRate, stat.Hitrates[1], stat.Hitrates[2], stat.Hitrates[3], stat.Hitrates[4], stat.Hitrates[5], stat.Hitrates[6], stat.Hitrates[7], stat.Hitrates[8], stat.Hitrates[9], stat.Hitrates[10], stat.Hitrates[11], stat.Hitrates[12], stat.Hitrates[13], stat.Hitrates[14], stat.Hitrates[15], stat.Hitrates[16], stat.Hitrates[17], stat.Hitrates[18], stat.Hitrates[19], stat.Hitrates[20], stat.Hitrates[25], - leg.ID, playerID) + leg.ID, playerID)) + } + } + + if dryRun { + for _, query := range queries { + log.Print(query) + } + } else { + log.Printf("Executing %d UPDATE queries", len(queries)) + tx, err := models.DB.Begin() + if err != nil { + return err + } + for _, query := range queries { + tx.Exec(query) } - s[leg.ID] = stats + tx.Commit() } - return s, err + return nil } diff --git a/data/statistics_around_the_world.go b/data/statistics_around_the_world.go index 72e7c95a..16678b17 100644 --- a/data/statistics_around_the_world.go +++ b/data/statistics_around_the_world.go @@ -2,6 +2,7 @@ package data import ( "database/sql" + "fmt" "log" "github.com/guregu/null" @@ -773,74 +774,98 @@ func CalculateAroundTheWorldStatistics(legID int, matchType int) (map[int]*model return statisticsMap, nil } -// ReCalculateAroundTheWorldStatistics will recaulcate statistics for Around the World legs -func ReCalculateAroundTheWorldStatistics() (map[int]map[int]*models.StatisticsAroundThe, error) { - legs, err := GetLegsOfType(models.AROUNDTHEWORLD, true) +// RecalculateAroundTheWorldStatistics will recaulcate statistics for Around the World legs +func RecalculateAroundTheWorldStatistics(since string, dryRun bool) error { + legs, err := GetLegsToRecalculate(models.AROUNDTHEWORLD, since) if err != nil { - return nil, err + return err } - s := make(map[int]map[int]*models.StatisticsAroundThe) + queries := make([]string, 0) for _, leg := range legs { stats, err := CalculateAroundTheWorldStatistics(leg.ID, models.AROUNDTHEWORLD) if err != nil { - return nil, err + return err } for playerID, stat := range stats { - log.Printf(`UPDATE statistics_around_the SET darts_thrown = %d, score = %d, mpr = %f, total_hit_rate = %f, hit_rate_1 = %f, + queries = append(queries, fmt.Sprintf(`UPDATE statistics_around_the SET darts_thrown = %d, score = %d, mpr = %f, total_hit_rate = %f, hit_rate_1 = %f, hit_rate_2 = %f, hit_rate_3 = %f, hit_rate_4 = %f, hit_rate_5 = %f, hit_rate_6 = %f, hit_rate_7 = %f, hit_rate_8 = %f, hit_rate_9 = %f, hit_rate_10 = %f, hit_rate_11 = %f, hit_rate_12 = %f, hit_rate_13 = %f, hit_rate_14 = %f, hit_rate_15 = %f, hit_rate_16 = %f, hit_rate_17 = %f, hit_rate_18 = %f, hit_rate_19 = %f, hit_rate_20 = %f, hit_rate_bull = %f WHERE leg_id = %d AND player_id = %d;`, stat.DartsThrown, stat.Score, stat.MPR.Float64, stat.TotalHitRate, stat.Hitrates[1], stat.Hitrates[2], stat.Hitrates[3], stat.Hitrates[4], stat.Hitrates[5], stat.Hitrates[6], stat.Hitrates[7], stat.Hitrates[8], stat.Hitrates[9], stat.Hitrates[10], stat.Hitrates[11], stat.Hitrates[12], stat.Hitrates[13], stat.Hitrates[14], stat.Hitrates[15], stat.Hitrates[16], stat.Hitrates[17], stat.Hitrates[18], stat.Hitrates[19], stat.Hitrates[20], stat.Hitrates[25], - leg.ID, playerID) + leg.ID, playerID)) } - s[leg.ID] = stats } - return s, err + if dryRun { + for _, query := range queries { + log.Print(query) + } + } else { + log.Printf("Executing %d UPDATE queries", len(queries)) + tx, err := models.DB.Begin() + if err != nil { + return err + } + for _, query := range queries { + tx.Exec(query) + } + tx.Commit() + } + return nil } -// ReCalculateShanghaiStatistics will recaulcate statistics for Shanghai legs -func ReCalculateShanghaiStatistics() (map[int]map[int]*models.StatisticsAroundThe, error) { - legs, err := GetLegsOfType(models.SHANGHAI, true) +// RecalculateShanghaiStatistics will recaulcate statistics for Shanghai legs +func RecalculateShanghaiStatistics(since string, dryRun bool) error { + legs, err := GetLegsToRecalculate(models.SHANGHAI, since) if err != nil { - return nil, err + return err } - s := make(map[int]map[int]*models.StatisticsAroundThe) + queries := make([]string, 0) for _, leg := range legs { - if leg.ID != 19 { - continue - } stats, err := CalculateAroundTheWorldStatistics(leg.ID, models.SHANGHAI) if err != nil { - return nil, err + return err } for playerID, stat := range stats { if stat.Shanghai.Valid { - log.Printf(`UPDATE statistics_around_the SET darts_thrown = %d, score = %d, shanghai = %d, mpr = %f, total_hit_rate = %f, hit_rate_1 = %f, + queries = append(queries, fmt.Sprintf(`UPDATE statistics_around_the SET darts_thrown = %d, score = %d, shanghai = %d, mpr = %f, total_hit_rate = %f, hit_rate_1 = %f, hit_rate_2 = %f, hit_rate_3 = %f, hit_rate_4 = %f, hit_rate_5 = %f, hit_rate_6 = %f, hit_rate_7 = %f, hit_rate_8 = %f, hit_rate_9 = %f, hit_rate_10 = %f, hit_rate_11 = %f, hit_rate_12 = %f, hit_rate_13 = %f, hit_rate_14 = %f, hit_rate_15 = %f, hit_rate_16 = %f, hit_rate_17 = %f, hit_rate_18 = %f, hit_rate_19 = %f, hit_rate_20 = %f WHERE leg_id = %d AND player_id = %d;`, stat.DartsThrown, stat.Score, stat.Shanghai.Int64, stat.MPR.Float64, stat.TotalHitRate, stat.Hitrates[1], stat.Hitrates[2], stat.Hitrates[3], stat.Hitrates[4], stat.Hitrates[5], stat.Hitrates[6], stat.Hitrates[7], stat.Hitrates[8], stat.Hitrates[9], stat.Hitrates[10], stat.Hitrates[11], stat.Hitrates[12], stat.Hitrates[13], stat.Hitrates[14], stat.Hitrates[15], stat.Hitrates[16], stat.Hitrates[17], stat.Hitrates[18], stat.Hitrates[19], stat.Hitrates[20], - leg.ID, playerID) + leg.ID, playerID)) } else { - log.Printf(`UPDATE statistics_around_the SET darts_thrown = %d, score = %d, shanghai = null, mpr = %f, total_hit_rate = %f, hit_rate_1 = %f, + queries = append(queries, fmt.Sprintf(`UPDATE statistics_around_the SET darts_thrown = %d, score = %d, shanghai = null, mpr = %f, total_hit_rate = %f, hit_rate_1 = %f, hit_rate_2 = %f, hit_rate_3 = %f, hit_rate_4 = %f, hit_rate_5 = %f, hit_rate_6 = %f, hit_rate_7 = %f, hit_rate_8 = %f, hit_rate_9 = %f, hit_rate_10 = %f, hit_rate_11 = %f, hit_rate_12 = %f, hit_rate_13 = %f, hit_rate_14 = %f, hit_rate_15 = %f, hit_rate_16 = %f, hit_rate_17 = %f, hit_rate_18 = %f, hit_rate_19 = %f, hit_rate_20 = %f WHERE leg_id = %d AND player_id = %d;`, stat.DartsThrown, stat.Score, stat.MPR.Float64, stat.TotalHitRate, stat.Hitrates[1], stat.Hitrates[2], stat.Hitrates[3], stat.Hitrates[4], stat.Hitrates[5], stat.Hitrates[6], stat.Hitrates[7], stat.Hitrates[8], stat.Hitrates[9], stat.Hitrates[10], stat.Hitrates[11], stat.Hitrates[12], stat.Hitrates[13], stat.Hitrates[14], stat.Hitrates[15], stat.Hitrates[16], stat.Hitrates[17], stat.Hitrates[18], stat.Hitrates[19], stat.Hitrates[20], - leg.ID, playerID) + leg.ID, playerID)) } } - s[leg.ID] = stats } - - return s, err + if dryRun { + for _, query := range queries { + log.Print(query) + } + } else { + log.Printf("Executing %d UPDATE queries", len(queries)) + tx, err := models.DB.Begin() + if err != nil { + return err + } + for _, query := range queries { + tx.Exec(query) + } + tx.Commit() + } + return nil } diff --git a/data/statistics_bermuda_triangle.go b/data/statistics_bermuda_triangle.go index 0ef19d31..1585f209 100644 --- a/data/statistics_bermuda_triangle.go +++ b/data/statistics_bermuda_triangle.go @@ -2,6 +2,7 @@ package data import ( "database/sql" + "fmt" "log" "github.com/kcapp/api/models" @@ -381,28 +382,43 @@ func CalculateBermudaTriangleStatistics(legID int) (map[int]*models.StatisticsBe return statisticsMap, nil } -// ReCalculateBermudaTriangleStatistics will recaulcate statistics for Bermuda Triangle legs -func ReCalculateBermudaTriangleStatistics() (map[int]map[int]*models.StatisticsBermudaTriangle, error) { - legs, err := GetLegsOfType(models.BERMUDATRIANGLE, true) +// RecalculateBermudaTriangleStatistics will recaulcate statistics for Bermuda Triangle legs +func RecalculateBermudaTriangleStatistics(since string, dryRun bool) error { + legs, err := GetLegsToRecalculate(models.BERMUDATRIANGLE, since) if err != nil { - return nil, err + return err } - s := make(map[int]map[int]*models.StatisticsBermudaTriangle) + queries := make([]string, 0) for _, leg := range legs { stats, err := CalculateBermudaTriangleStatistics(leg.ID) if err != nil { - return nil, err + return err } for playerID, stat := range stats { - log.Printf(`UPDATE statistics_bermuda_triangle SET darts_thrown = %d, score = %d, mpr = %f, total_marks = %d, highest_score_reached = 22%d4, total_hit_rate = %f, hit_rate_1 = %f, hit_rate_2 = %f, + queries = append(queries, fmt.Sprintf(`UPDATE statistics_bermuda_triangle SET darts_thrown = %d, score = %d, mpr = %f, total_marks = %d, highest_score_reached = 22%d4, total_hit_rate = %f, hit_rate_1 = %f, hit_rate_2 = %f, hit_rate_3 = %f, hit_rate_4 = %f, hit_rate_5 = %f, hit_rate_6 = %f, hit_rate_7 = %f, hit_rate_8 = %f, hit_rate_9 = %f, hit_rate_10 = %f, hit_rate_11 = %f, hit_rate_12 = %f, hit_rate_13 = %f, hit_count = %d WHERE leg_id = %d AND player_id = %d;`, stat.DartsThrown, stat.Score, stat.MPR, stat.TotalMarks, stat.HighestScoreReached, stat.TotalHitRate, stat.Hitrates[0], stat.Hitrates[1], stat.Hitrates[2], stat.Hitrates[3], stat.Hitrates[4], - stat.Hitrates[5], stat.Hitrates[6], stat.Hitrates[7], stat.Hitrates[8], stat.Hitrates[9], stat.Hitrates[10], stat.Hitrates[11], stat.Hitrates[12], stat.HitCount, leg.ID, playerID) + stat.Hitrates[5], stat.Hitrates[6], stat.Hitrates[7], stat.Hitrates[8], stat.Hitrates[9], stat.Hitrates[10], stat.Hitrates[11], stat.Hitrates[12], stat.HitCount, leg.ID, playerID)) + } + } + + if dryRun { + for _, query := range queries { + log.Print(query) + } + } else { + log.Printf("Executing %d UPDATE queries", len(queries)) + tx, err := models.DB.Begin() + if err != nil { + return err + } + for _, query := range queries { + tx.Exec(query) } - s[leg.ID] = stats + tx.Commit() } - return s, err + return nil } diff --git a/data/statistics_cricket.go b/data/statistics_cricket.go index 17eefd3c..215cd668 100644 --- a/data/statistics_cricket.go +++ b/data/statistics_cricket.go @@ -291,8 +291,8 @@ func CalculateCricketStatistics(legID int) (map[int]*models.StatisticsCricket, e return statisticsMap, nil } -// ReCalculateCricketStatistics will recaulcate statistics for Cricket matches -func ReCalculateCricketStatistics(since string, dryRun bool) error { +// RecalculateCricketStatistics will recaulcate statistics for Cricket matches +func RecalculateCricketStatistics(since string, dryRun bool) error { legs, err := GetLegsToRecalculate(models.CRICKET, since) if err != nil { return err diff --git a/data/statistics_darts_at_x.go b/data/statistics_darts_at_x.go index 787cdce5..1b061390 100644 --- a/data/statistics_darts_at_x.go +++ b/data/statistics_darts_at_x.go @@ -2,6 +2,8 @@ package data import ( "database/sql" + "fmt" + "log" "github.com/guregu/null" "github.com/kcapp/api/models" @@ -312,6 +314,44 @@ func CalculateDartsAtXStatistics(legID int) (map[int]*models.StatisticsDartsAtX, return statisticsMap, nil } +// RecalculateDartsAtXStatistics will recaulcate statistics for Darts at X legs +func RecalculateDartsAtXStatistics(since string, dryRun bool) error { + legs, err := GetLegsToRecalculate(models.DARTSATX, since) + if err != nil { + return err + } + + queries := make([]string, 0) + for _, leg := range legs { + stats, err := CalculateDartsAtXStatistics(leg.ID) + if err != nil { + return err + } + for playerID, stat := range stats { + queries = append(queries, fmt.Sprintf(`UPDATE statistics_darts_at_x SET score = %d, singles = %d, doubles = %d, triples = %d, hit_rate = %f, hits5 = %d, hits6 = %d, hits7 = %d, hits8 = %d, hits9 = %d WHERE leg_id = %d AND player_id = %d;`, + stat.Score.Int64, stat.Singles, stat.Doubles, stat.Triples, stat.HitRate, stat.Hits5, stat.Hits6, stat.Hits7, stat.Hits8, stat.Hits9, leg.ID, playerID)) + } + } + + if dryRun { + for _, query := range queries { + log.Print(query) + } + } else { + log.Printf("Executing %d UPDATE queries", len(queries)) + tx, err := models.DB.Begin() + if err != nil { + return err + } + for _, query := range queries { + tx.Exec(query) + } + tx.Commit() + } + + return nil +} + func addDart(number int, dart *models.Dart, stats *models.StatisticsDartsAtX) int { if dart.ValueRaw() == number { if dart.IsTriple() { diff --git a/data/statistics_gotcha.go b/data/statistics_gotcha.go index 3d58418d..4fdfb89e 100644 --- a/data/statistics_gotcha.go +++ b/data/statistics_gotcha.go @@ -2,6 +2,7 @@ package data import ( "database/sql" + "fmt" "log" "github.com/kcapp/api/models" @@ -275,25 +276,40 @@ func getPlayersReset(visit *models.Visit, players map[int]*models.Player2Leg) in return resets } -// ReCalculateGotchaStatistics will recaulcate statistics for Gotcha legs -func ReCalculateGotchaStatistics() (map[int]map[int]*models.StatisticsGotcha, error) { - legs, err := GetLegsOfType(models.GOTCHA, true) +// RecalculateGotchaStatistics will recaulcate statistics for Gotcha legs +func RecalculateGotchaStatistics(since string, dryRun bool) error { + legs, err := GetLegsToRecalculate(models.GOTCHA, since) if err != nil { - return nil, err + return err } - s := make(map[int]map[int]*models.StatisticsGotcha) + queries := make([]string, 0) for _, leg := range legs { stats, err := CalculateGotchaStatistics(leg.ID) if err != nil { - return nil, err + return err } for playerID, stat := range stats { - log.Printf(`UPDATE statistics_gotcha SET darts_thrown = %d, highest_score = %d, times_reset = %d, others_reset = %d, score = %d WHERE leg_id = %d AND player_id = %d;`, - stat.DartsThrown, stat.HighestScore, stat.TimesReset, stat.OthersReset, stat.Score, leg.ID, playerID) + queries = append(queries, fmt.Sprintf(`UPDATE statistics_gotcha SET darts_thrown = %d, highest_score = %d, times_reset = %d, others_reset = %d, score = %d WHERE leg_id = %d AND player_id = %d;`, + stat.DartsThrown, stat.HighestScore, stat.TimesReset, stat.OthersReset, stat.Score, leg.ID, playerID)) + } + } + + if dryRun { + for _, query := range queries { + log.Print(query) + } + } else { + log.Printf("Executing %d UPDATE queries", len(queries)) + tx, err := models.DB.Begin() + if err != nil { + return err + } + for _, query := range queries { + tx.Exec(query) } - s[leg.ID] = stats + tx.Commit() } - return s, err + return nil } diff --git a/data/statistics_jdc_practice.go b/data/statistics_jdc_practice.go index fda11a16..84678a4c 100644 --- a/data/statistics_jdc_practice.go +++ b/data/statistics_jdc_practice.go @@ -2,6 +2,7 @@ package data import ( "database/sql" + "fmt" "log" "github.com/guregu/null" @@ -273,26 +274,40 @@ func CalculateJDCPracticeStatistics(legID int) (map[int]*models.StatisticsJDCPra return statisticsMap, nil } -// ReCalculateJDCPracticeStatistics will recaulcate statistics for JDC Practice legs -func ReCalculateJDCPracticeStatistics() (map[int]map[int]*models.StatisticsJDCPractice, error) { - legs, err := GetLegsOfType(models.JDCPRACTICE, true) +// RecalculateJDCPracticeStatistics will recaulcate statistics for JDC Practice legs +func RecalculateJDCPracticeStatistics(since string, dryRun bool) error { + legs, err := GetLegsToRecalculate(models.JDCPRACTICE, since) if err != nil { - return nil, err + return err } - s := make(map[int]map[int]*models.StatisticsJDCPractice) + queries := make([]string, 0) for _, leg := range legs { stats, err := CalculateJDCPracticeStatistics(leg.ID) if err != nil { - return nil, err + return err } for playerID, stat := range stats { - log.Printf(`UPDATE statistics_jdc_practice SET darts_thrown = %d, score = %d, mpr = %f, + queries = append(queries, fmt.Sprintf(`UPDATE statistics_jdc_practice SET darts_thrown = %d, score = %d, mpr = %f, shanghai_count = %d, doubles_hitrate = %f WHERE leg_id = %d AND player_id = %d;`, - stat.DartsThrown, stat.Score, stat.MPR.Float64, stat.ShanghaiCount, stat.DoublesHitrate, leg.ID, playerID) + stat.DartsThrown, stat.Score, stat.MPR.Float64, stat.ShanghaiCount, stat.DoublesHitrate, leg.ID, playerID)) } - s[leg.ID] = stats } - return s, err + if dryRun { + for _, query := range queries { + log.Print(query) + } + } else { + log.Printf("Executing %d UPDATE queries", len(queries)) + tx, err := models.DB.Begin() + if err != nil { + return err + } + for _, query := range queries { + tx.Exec(query) + } + tx.Commit() + } + return nil } diff --git a/data/statistics_kill_bull.go b/data/statistics_kill_bull.go index 12141539..ac8a40a4 100644 --- a/data/statistics_kill_bull.go +++ b/data/statistics_kill_bull.go @@ -2,6 +2,7 @@ package data import ( "database/sql" + "fmt" "log" "github.com/kcapp/api/models" @@ -307,25 +308,40 @@ func CalculateKillBullStatistics(legID int) (map[int]*models.StatisticsKillBull, return statisticsMap, nil } -// ReCalculateKillBullStatistics will recaulcate statistics for Kill Bull legs -func ReCalculateKillBullStatistics() (map[int]map[int]*models.StatisticsKillBull, error) { - legs, err := GetLegsOfType(models.KILLBULL, true) +// RecalculateKillBullStatistics will recaulcate statistics for Kill Bull legs +func RecalculateKillBullStatistics(since string, dryRun bool) error { + legs, err := GetLegsToRecalculate(models.KILLBULL, since) if err != nil { - return nil, err + return err } - s := make(map[int]map[int]*models.StatisticsKillBull) + queries := make([]string, 0) for _, leg := range legs { stats, err := CalculateKillBullStatistics(leg.ID) if err != nil { - return nil, err + return err } for playerID, stat := range stats { - log.Printf(`UPDATE statistics_kill_bull SET darts_thrown = %d, score = %d, marks3 = %d, marks4 = %d, marks5 = %d, marks6 = %d, longest_streak = %d, times_busted = %d, total_hit_rate = %f - WHERE leg_id = %d AND player_id = %d;`, stat.DartsThrown, stat.Score, stat.Marks3, stat.Marks4, stat.Marks5, stat.Marks6, stat.LongestStreak, stat.TimesBusted, stat.TotalHitRate, leg.ID, playerID) + queries = append(queries, fmt.Sprintf(`UPDATE statistics_kill_bull SET darts_thrown = %d, score = %d, marks3 = %d, marks4 = %d, marks5 = %d, marks6 = %d, longest_streak = %d, times_busted = %d, total_hit_rate = %f + WHERE leg_id = %d AND player_id = %d;`, stat.DartsThrown, stat.Score, stat.Marks3, stat.Marks4, stat.Marks5, stat.Marks6, stat.LongestStreak, stat.TimesBusted, stat.TotalHitRate, leg.ID, playerID)) + } + } + + if dryRun { + for _, query := range queries { + log.Print(query) + } + } else { + log.Printf("Executing %d UPDATE queries", len(queries)) + tx, err := models.DB.Begin() + if err != nil { + return err + } + for _, query := range queries { + tx.Exec(query) } - s[leg.ID] = stats + tx.Commit() } - return s, err + return nil } diff --git a/data/statistics_knockout.go b/data/statistics_knockout.go index 24f245d1..433c16f3 100644 --- a/data/statistics_knockout.go +++ b/data/statistics_knockout.go @@ -2,6 +2,7 @@ package data import ( "database/sql" + "fmt" "log" "github.com/kcapp/api/models" @@ -252,26 +253,40 @@ func CalculateKnockoutStatistics(legID int) (map[int]*models.StatisticsKnockout, return statisticsMap, nil } -// ReCalculateKnockoutStatistics will recaulcate statistics for Knockout legs -func ReCalculateKnockoutStatistics() (map[int]map[int]*models.StatisticsKnockout, error) { - legs, err := GetLegsOfType(models.KNOCKOUT, true) +// RecalculateKnockoutStatistics will recaulcate statistics for Knockout legs +func RecalculateKnockoutStatistics(since string, dryRun bool) error { + legs, err := GetLegsToRecalculate(models.KNOCKOUT, since) if err != nil { - return nil, err + return err } - s := make(map[int]map[int]*models.StatisticsKnockout) + queries := make([]string, 0) for _, leg := range legs { stats, err := CalculateKnockoutStatistics(leg.ID) if err != nil { - return nil, err + return err } for playerID, stat := range stats { - log.Printf(`UPDATE statistics_knockout SET darts_thrown = %d, avg_score = %f, lives_lost = %d, lives_taken = %d, + queries = append(queries, fmt.Sprintf(`UPDATE statistics_knockout SET darts_thrown = %d, avg_score = %f, lives_lost = %d, lives_taken = %d, final_position = %d WHERE leg_id = %d AND player_id = %d;`, - stat.DartsThrown, stat.AvgScore, stat.LivesLost, stat.LivesTaken, stat.FinalPosition, leg.ID, playerID) + stat.DartsThrown, stat.AvgScore, stat.LivesLost, stat.LivesTaken, stat.FinalPosition, leg.ID, playerID)) } - s[leg.ID] = stats } - return s, err + if dryRun { + for _, query := range queries { + log.Print(query) + } + } else { + log.Printf("Executing %d UPDATE queries", len(queries)) + tx, err := models.DB.Begin() + if err != nil { + return err + } + for _, query := range queries { + tx.Exec(query) + } + tx.Commit() + } + return nil } diff --git a/data/statistics_shootout.go b/data/statistics_shootout.go index 38b194ec..3ea42da6 100644 --- a/data/statistics_shootout.go +++ b/data/statistics_shootout.go @@ -249,8 +249,8 @@ func CalculateShootoutStatistics(legID int) (map[int]*models.StatisticsShootout, return statisticsMap, nil } -// ReCalculateShootoutStatistics will recaulcate statistics for Shootout matches -func ReCalculateShootoutStatistics(since string, dryRun bool) error { +// RecalculateShootoutStatistics will recaulcate statistics for Shootout matches +func RecalculateShootoutStatistics(since string, dryRun bool) error { legs, err := GetLegsToRecalculate(models.SHOOTOUT, since) if err != nil { return err diff --git a/data/statistics_tic_tac_toe.go b/data/statistics_tic_tac_toe.go index 10df435c..74c94b53 100644 --- a/data/statistics_tic_tac_toe.go +++ b/data/statistics_tic_tac_toe.go @@ -2,6 +2,8 @@ package data import ( "database/sql" + "fmt" + "log" "github.com/kcapp/api/models" ) @@ -237,3 +239,39 @@ func CalculateTicTacToeStatistics(legID int) (map[int]*models.StatisticsTicTacTo } return statisticsMap, nil } + +// RecalculateTicTacToeStatistics will recaulcate statistics for Tic Tac Toe legs +func RecalculateTicTacToeStatistics(since string, dryRun bool) error { + legs, err := GetLegsToRecalculate(models.TICTACTOE, since) + if err != nil { + return err + } + + queries := make([]string, 0) + for _, leg := range legs { + stats, err := CalculateTicTacToeStatistics(leg.ID) + if err != nil { + return err + } + for playerID, stat := range stats { + queries = append(queries, fmt.Sprintf(`UPDATE statistics_tic_tac_toe SET darts_thrown = %d WHERE leg_id = %d AND player_id = %d;`, + stat.DartsThrown, leg.ID, playerID)) + } + } + if dryRun { + for _, query := range queries { + log.Print(query) + } + } else { + log.Printf("Executing %d UPDATE queries", len(queries)) + tx, err := models.DB.Begin() + if err != nil { + return err + } + for _, query := range queries { + tx.Exec(query) + } + tx.Commit() + } + return nil +} diff --git a/data/statistics_x01.go b/data/statistics_x01.go index 72709802..d01dcc6d 100644 --- a/data/statistics_x01.go +++ b/data/statistics_x01.go @@ -2,12 +2,12 @@ package data import ( "database/sql" + "fmt" "log" "github.com/guregu/null" "github.com/jmoiron/sqlx" "github.com/kcapp/api/models" - "github.com/kcapp/api/util" ) // GetX01Statistics will return statistics for all players active duing the given period @@ -872,58 +872,50 @@ func GetOfficeStatisticsForOffice(officeID int, from string, to string) ([]*mode } // RecalculateX01Statistics will recalculate x01 statistics for all legs -func RecalculateX01Statistics() (map[int]map[int]*models.StatisticsX01, error) { - rows, err := models.DB.Query(` - SELECT - l.id, l.end_time, l.starting_score, l.is_finished, - l.current_player_id, l.winner_id, l.created_at, l.updated_at, - l.match_id, GROUP_CONCAT(p2l.player_id ORDER BY p2l.order ASC) - FROM leg l - JOIN matches m on m.id = l.match_id - JOIN player2leg p2l ON p2l.leg_id = l.id - WHERE - l.has_scores = 1 - AND m.match_type_id = 1 - GROUP BY l.id - ORDER BY l.id`) +func RecalculateX01Statistics(since string, dryRun bool) error { + legs, err := GetLegsToRecalculate(models.X01, since) if err != nil { - return nil, err - } - defer rows.Close() - - legs := make([]*models.Leg, 0) - for rows.Next() { - m := new(models.Leg) - var players string - err := rows.Scan(&m.ID, &m.Endtime, &m.StartingScore, &m.IsFinished, &m.CurrentPlayerID, &m.WinnerPlayerID, &m.CreatedAt, &m.UpdatedAt, - &m.MatchID, &players) - if err != nil { - return nil, err - } - m.Players = util.StringToIntArray(players) - legs = append(legs, m) - } - if err = rows.Err(); err != nil { - return nil, err + return err } - m := make(map[int]map[int]*models.StatisticsX01) + queries := make([]string, 0) for _, leg := range legs { stats, err := CalculateX01Statistics(leg.ID, int(leg.WinnerPlayerID.Int64), leg.StartingScore) if err != nil { - return nil, err + return err } for playerID, stat := range stats { + query := fmt.Sprintf("UPDATE statistics_x01 SET ppd = %f, ppd_score = %d, first_nine_ppd = %f, first_nine_ppd_score = %d, checkout_attempts = %d, darts_thrown = %d, `60s_plus` = %d, `100s_plus` = %d, `140s_plus` = %d, `180s` = %d, overall_accuracy = %f", + stat.PPD, stat.PPDScore, stat.FirstNinePPD, stat.FirstNinePPDScore, stat.CheckoutAttempts, stat.DartsThrown, stat.Score60sPlus, stat.Score100sPlus, stat.Score140sPlus, stat.Score180s, stat.AccuracyStatistics.AccuracyOverall.Float64) + if stat.CheckoutPercentage.Valid { - log.Printf("UPDATE statistics_x01 SET checkout_attempts = %d, checkout_percentage = %f WHERE leg_id = %d AND player_id = %d;", - stat.CheckoutAttempts, stat.CheckoutPercentage.Float64, leg.ID, playerID) - } else { - log.Printf("UPDATE statistics_x01 SET checkout_attempts = %d, checkout_percentage = NULL WHERE leg_id = %d AND player_id = %d;", - stat.CheckoutAttempts, leg.ID, playerID) + query += fmt.Sprintf(", checkout_percentage = %f", stat.CheckoutPercentage.Float64) } + if stat.AccuracyStatistics.Accuracy19.Valid { + query += fmt.Sprintf(", accuracy_19 = %f", stat.AccuracyStatistics.Accuracy19.Float64) + } + if stat.AccuracyStatistics.Accuracy20.Valid { + query += fmt.Sprintf(", accuracy_20 = %f", stat.AccuracyStatistics.Accuracy20.Float64) + } + query += fmt.Sprintf(" WHERE leg_id = %d AND player_id = %d;", leg.ID, playerID) + queries = append(queries, query) + } + } + if dryRun { + for _, query := range queries { + log.Print(query) + } + } else { + log.Printf("Executing %d UPDATE queries", len(queries)) + tx, err := models.DB.Begin() + if err != nil { + return err + } + for _, query := range queries { + tx.Exec(query) } - m[leg.ID] = stats + tx.Commit() } - return m, err + return nil } From 8b897cb1de958de42508958e12c95f2895d17927 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Sun, 13 Mar 2022 14:04:25 +0100 Subject: [PATCH 03/67] Simplified recalculate statistics commands --- cmd/aroundtheclock.go | 6 +- cmd/aroundtheworld.go | 6 +- cmd/bermudatriangle.go | 6 +- cmd/cricket.go | 9 +-- cmd/dartsatx.go | 6 +- cmd/fourtwenty.go | 6 +- cmd/gotcha.go | 6 +- cmd/jdcpractice.go | 6 +- cmd/killbull.go | 6 +- cmd/knockout.go | 6 +- cmd/recalculate.go | 5 +- cmd/serve.go | 3 +- cmd/shanghai.go | 6 +- cmd/shootout.go | 6 +- cmd/tictactoe.go | 6 +- cmd/x01.go | 6 +- data/leg.go | 27 +++------ data/recalculate.go | 84 ++++++++++++++++++++++++++ data/statistics_420.go | 38 +++--------- data/statistics_around_the_clock.go | 35 ++--------- data/statistics_around_the_world.go | 94 ++++++----------------------- data/statistics_bermuda_triangle.go | 39 +++--------- data/statistics_cricket.go | 34 ++--------- data/statistics_darts_at_x.go | 35 ++--------- data/statistics_gotcha.go | 35 ++--------- data/statistics_jdc_practice.go | 37 +++--------- data/statistics_kill_bull.go | 37 +++--------- data/statistics_knockout.go | 37 +++--------- data/statistics_shootout.go | 34 ++--------- data/statistics_tic_tac_toe.go | 37 +++--------- data/statistics_x01.go | 39 +++--------- models/match.go | 17 ++++++ 32 files changed, 243 insertions(+), 511 deletions(-) create mode 100644 data/recalculate.go diff --git a/cmd/aroundtheclock.go b/cmd/aroundtheclock.go index b3b80ccc..4ecb4ae1 100644 --- a/cmd/aroundtheclock.go +++ b/cmd/aroundtheclock.go @@ -1,9 +1,8 @@ package cmd import ( - "log" - "github.com/kcapp/api/data" + "github.com/kcapp/api/models" "github.com/spf13/cobra" ) @@ -12,8 +11,7 @@ var aroundtheclockCmd = &cobra.Command{ Use: "aroundtheclock", Short: "Recalculate Around the Clock statistics", Run: func(cmd *cobra.Command, args []string) { - log.Printf("Recalculating Around the Clock Statistics since=%s, dryRun=%t", since, dryRun) - err := data.RecalculateAroundTheClockStatistics(since, dryRun) + err := data.RecalculateStatistics(models.AROUNDTHECLOCK, legID, since, dryRun) if err != nil { panic(err) } diff --git a/cmd/aroundtheworld.go b/cmd/aroundtheworld.go index 7d4bfa3b..8db05f9f 100644 --- a/cmd/aroundtheworld.go +++ b/cmd/aroundtheworld.go @@ -1,9 +1,8 @@ package cmd import ( - "log" - "github.com/kcapp/api/data" + "github.com/kcapp/api/models" "github.com/spf13/cobra" ) @@ -12,8 +11,7 @@ var aroundtheworldCmd = &cobra.Command{ Use: "aroundtheworld", Short: "Recalculate Around the World statistics", Run: func(cmd *cobra.Command, args []string) { - log.Printf("Recalculating Around the World Statistics since=%s, dryRun=%t", since, dryRun) - err := data.RecalculateAroundTheWorldStatistics(since, dryRun) + err := data.RecalculateStatistics(models.AROUNDTHEWORLD, legID, since, dryRun) if err != nil { panic(err) } diff --git a/cmd/bermudatriangle.go b/cmd/bermudatriangle.go index af2364d9..45685b85 100644 --- a/cmd/bermudatriangle.go +++ b/cmd/bermudatriangle.go @@ -1,9 +1,8 @@ package cmd import ( - "log" - "github.com/kcapp/api/data" + "github.com/kcapp/api/models" "github.com/spf13/cobra" ) @@ -12,8 +11,7 @@ var bermudatriangleCmd = &cobra.Command{ Use: "bermudatriangle", Short: "Recalculate Bermuda Triangle statistics", Run: func(cmd *cobra.Command, args []string) { - log.Printf("Recalculating Bermuda Triangle Statistics since=%s, dryRun=%t", since, dryRun) - err := data.RecalculateBermudaTriangleStatistics(since, dryRun) + err := data.RecalculateStatistics(models.BERMUDATRIANGLE, legID, since, dryRun) if err != nil { panic(err) } diff --git a/cmd/cricket.go b/cmd/cricket.go index d192f8d1..a02d0aec 100644 --- a/cmd/cricket.go +++ b/cmd/cricket.go @@ -1,9 +1,8 @@ package cmd import ( - "log" - "github.com/kcapp/api/data" + "github.com/kcapp/api/models" "github.com/spf13/cobra" ) @@ -12,11 +11,7 @@ var cricketCmd = &cobra.Command{ Use: "cricket", Short: "Recalculate Cricket statistics", Run: func(cmd *cobra.Command, args []string) { - since, _ := cmd.Flags().GetString("since") - dryRun, _ := cmd.Flags().GetBool("dry-run") - - log.Printf("Recalculating Cricket Statistics since=%s, dryRun=%t", since, dryRun) - err := data.RecalculateCricketStatistics(since, dryRun) + err := data.RecalculateStatistics(models.CRICKET, legID, since, dryRun) if err != nil { panic(err) } diff --git a/cmd/dartsatx.go b/cmd/dartsatx.go index 4baf19fd..7bc5abb3 100644 --- a/cmd/dartsatx.go +++ b/cmd/dartsatx.go @@ -1,9 +1,8 @@ package cmd import ( - "log" - "github.com/kcapp/api/data" + "github.com/kcapp/api/models" "github.com/spf13/cobra" ) @@ -12,8 +11,7 @@ var dartsatxCmd = &cobra.Command{ Use: "dartsatx", Short: "Recalculate Darts at X statistics", Run: func(cmd *cobra.Command, args []string) { - log.Printf("Recalculating Darts at X Statistics since=%s, dryRun=%t", since, dryRun) - err := data.RecalculateDartsAtXStatistics(since, dryRun) + err := data.RecalculateStatistics(models.DARTSATX, legID, since, dryRun) if err != nil { panic(err) } diff --git a/cmd/fourtwenty.go b/cmd/fourtwenty.go index 520b48ad..5f1c81d5 100644 --- a/cmd/fourtwenty.go +++ b/cmd/fourtwenty.go @@ -1,9 +1,8 @@ package cmd import ( - "log" - "github.com/kcapp/api/data" + "github.com/kcapp/api/models" "github.com/spf13/cobra" ) @@ -12,8 +11,7 @@ var fourtwentyCmd = &cobra.Command{ Use: "fourtwenty", Short: "Recalculate 420 statistics", Run: func(cmd *cobra.Command, args []string) { - log.Printf("Recalculating 420 Statistics since=%s, dryRun=%t", since, dryRun) - err := data.Recalculate420Statistics(since, dryRun) + err := data.RecalculateStatistics(models.FOURTWENTY, legID, since, dryRun) if err != nil { panic(err) } diff --git a/cmd/gotcha.go b/cmd/gotcha.go index 4880e775..2e5ed278 100644 --- a/cmd/gotcha.go +++ b/cmd/gotcha.go @@ -1,9 +1,8 @@ package cmd import ( - "log" - "github.com/kcapp/api/data" + "github.com/kcapp/api/models" "github.com/spf13/cobra" ) @@ -12,8 +11,7 @@ var gotchaCmd = &cobra.Command{ Use: "gotcha", Short: "Recalculate Gotcha statistics", Run: func(cmd *cobra.Command, args []string) { - log.Printf("Recalculating Gotcha Statistics since=%s, dryRun=%t", since, dryRun) - err := data.RecalculateGotchaStatistics(since, dryRun) + err := data.RecalculateStatistics(models.GOTCHA, legID, since, dryRun) if err != nil { panic(err) } diff --git a/cmd/jdcpractice.go b/cmd/jdcpractice.go index 724dad63..a8fa6673 100644 --- a/cmd/jdcpractice.go +++ b/cmd/jdcpractice.go @@ -1,9 +1,8 @@ package cmd import ( - "log" - "github.com/kcapp/api/data" + "github.com/kcapp/api/models" "github.com/spf13/cobra" ) @@ -12,8 +11,7 @@ var jdcpracticeCmd = &cobra.Command{ Use: "jdcpractice", Short: "Recalculate JDC Practice statistics", Run: func(cmd *cobra.Command, args []string) { - log.Printf("Recalculating JDC Practice Statistics since=%s, dryRun=%t", since, dryRun) - err := data.RecalculateJDCPracticeStatistics(since, dryRun) + err := data.RecalculateStatistics(models.JDCPRACTICE, legID, since, dryRun) if err != nil { panic(err) } diff --git a/cmd/killbull.go b/cmd/killbull.go index 4c9dad2b..1950e1e6 100644 --- a/cmd/killbull.go +++ b/cmd/killbull.go @@ -1,9 +1,8 @@ package cmd import ( - "log" - "github.com/kcapp/api/data" + "github.com/kcapp/api/models" "github.com/spf13/cobra" ) @@ -12,8 +11,7 @@ var killbullCmd = &cobra.Command{ Use: "killbull", Short: "Recalculate Kill Bull statistics", Run: func(cmd *cobra.Command, args []string) { - log.Printf("Recalculating Kill Bull Statistics since=%s, dryRun=%t", since, dryRun) - err := data.RecalculateKillBullStatistics(since, dryRun) + err := data.RecalculateStatistics(models.KILLBULL, legID, since, dryRun) if err != nil { panic(err) } diff --git a/cmd/knockout.go b/cmd/knockout.go index 79496b96..24360fbc 100644 --- a/cmd/knockout.go +++ b/cmd/knockout.go @@ -1,9 +1,8 @@ package cmd import ( - "log" - "github.com/kcapp/api/data" + "github.com/kcapp/api/models" "github.com/spf13/cobra" ) @@ -12,8 +11,7 @@ var knockoutCmd = &cobra.Command{ Use: "knockout", Short: "Recalculate Knockout statistics", Run: func(cmd *cobra.Command, args []string) { - log.Printf("Recalculating Knockout Statistics since=%s, dryRun=%t", since, dryRun) - err := data.RecalculateKnockoutStatistics(since, dryRun) + err := data.RecalculateStatistics(models.KNOCKOUT, legID, since, dryRun) if err != nil { panic(err) } diff --git a/cmd/recalculate.go b/cmd/recalculate.go index cc067e56..ad70ee8b 100644 --- a/cmd/recalculate.go +++ b/cmd/recalculate.go @@ -7,6 +7,7 @@ import ( var since string var dryRun bool +var legID int // recalculateCmd represents the recalculate command var recalculateCmd = &cobra.Command{ @@ -26,11 +27,13 @@ var recalculateCmd = &cobra.Command{ since, _ = cmd.Flags().GetString("since") dryRun, _ = cmd.Flags().GetBool("dry-run") + legID, _ = cmd.Flags().GetInt("leg") }, } func init() { rootCmd.AddCommand(recalculateCmd) recalculateCmd.PersistentFlags().Bool("dry-run", true, "Print queries instead of executing") - recalculateCmd.PersistentFlags().StringP("since", "s", "", "Recalculate statistics newer than the given date") + recalculateCmd.PersistentFlags().StringP("since", "s", "", "Only recalculate statistics newer than the given date") + recalculateCmd.PersistentFlags().IntP("leg", "l", 0, "Recalculate statistics for the given leg id") } diff --git a/cmd/serve.go b/cmd/serve.go index 1af787b1..21ba3a7f 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -15,8 +15,7 @@ import ( // serveCmd represents the serve command var serveCmd = &cobra.Command{ Use: "serve", - Short: "Start serving the API", - Long: `Start serving the API`, + Short: "Start the API", Run: func(cmd *cobra.Command, args []string) { configFileParam, _ := cmd.Flags().GetString("config") config, err := models.GetConfig(configFileParam) diff --git a/cmd/shanghai.go b/cmd/shanghai.go index daca6af8..bca91b57 100644 --- a/cmd/shanghai.go +++ b/cmd/shanghai.go @@ -1,9 +1,8 @@ package cmd import ( - "log" - "github.com/kcapp/api/data" + "github.com/kcapp/api/models" "github.com/spf13/cobra" ) @@ -12,8 +11,7 @@ var shanghaiCmd = &cobra.Command{ Use: "shanghai", Short: "Recalculate Shanghai statistics", Run: func(cmd *cobra.Command, args []string) { - log.Printf("Recalculating Shanghai since=%s, dryRun=%t", since, dryRun) - err := data.RecalculateShanghaiStatistics(since, dryRun) + err := data.RecalculateStatistics(models.SHANGHAI, legID, since, dryRun) if err != nil { panic(err) } diff --git a/cmd/shootout.go b/cmd/shootout.go index d0ae5e30..8e16eb22 100644 --- a/cmd/shootout.go +++ b/cmd/shootout.go @@ -1,9 +1,8 @@ package cmd import ( - "log" - "github.com/kcapp/api/data" + "github.com/kcapp/api/models" "github.com/spf13/cobra" ) @@ -12,8 +11,7 @@ var shootoutCmd = &cobra.Command{ Use: "shootout", Short: "Recalculate 9 Dart Shootout statistics", Run: func(cmd *cobra.Command, args []string) { - log.Printf("Recalculating 9 Dart Shootout Statistics since=%s, dryRun=%t", since, dryRun) - err := data.RecalculateShootoutStatistics(since, dryRun) + err := data.RecalculateStatistics(models.SHOOTOUT, legID, since, dryRun) if err != nil { panic(err) } diff --git a/cmd/tictactoe.go b/cmd/tictactoe.go index 188b16f1..986b79fc 100644 --- a/cmd/tictactoe.go +++ b/cmd/tictactoe.go @@ -1,9 +1,8 @@ package cmd import ( - "log" - "github.com/kcapp/api/data" + "github.com/kcapp/api/models" "github.com/spf13/cobra" ) @@ -12,8 +11,7 @@ var tictactoeCmd = &cobra.Command{ Use: "tictactoe", Short: "Recalculate Tic-Tac-Toe statistics", Run: func(cmd *cobra.Command, args []string) { - log.Printf("Recalculating Tic-Tac-Toe since=%s, dryRun=%t", since, dryRun) - err := data.RecalculateTicTacToeStatistics(since, dryRun) + err := data.RecalculateStatistics(models.TICTACTOE, legID, since, dryRun) if err != nil { panic(err) } diff --git a/cmd/x01.go b/cmd/x01.go index 9fd9eee6..70981ae2 100644 --- a/cmd/x01.go +++ b/cmd/x01.go @@ -1,9 +1,8 @@ package cmd import ( - "log" - "github.com/kcapp/api/data" + "github.com/kcapp/api/models" "github.com/spf13/cobra" ) @@ -12,8 +11,7 @@ var x01Cmd = &cobra.Command{ Use: "x01", Short: "Recalculate x01 statistics", Run: func(cmd *cobra.Command, args []string) { - log.Printf("Recalculating x01 since=%s, dryRun=%t", since, dryRun) - err := data.RecalculateX01Statistics(since, dryRun) + err := data.RecalculateStatistics(models.X01, legID, since, dryRun) if err != nil { panic(err) } diff --git a/data/leg.go b/data/leg.go index 6a13b8bf..129efc13 100644 --- a/data/leg.go +++ b/data/leg.go @@ -385,7 +385,7 @@ func FinishLeg(visit models.Visit) error { log.Printf("[%d] Inserting Knockout statistics for player %d", visit.LegID, playerID) } } else { - statisticsMap, err := CalculateX01Statistics(visit.LegID, visit.PlayerID, leg.StartingScore) + statisticsMap, err := CalculateX01Statistics(visit.LegID) if err != nil { tx.Rollback() return err @@ -738,15 +738,11 @@ func GetLegsOfType(matchType int, loadVisits bool) ([]*models.Leg, error) { } // GetLegsToRecalculate returns all legs since the given date which can be recalculated -func GetLegsToRecalculate(matchType int, since string) ([]*models.Leg, error) { +func GetLegsToRecalculate(matchType int, since string) ([]int, error) { rows, err := models.DB.Query(` - SELECT - l.id, l.end_time, l.starting_score, l.is_finished, - l.current_player_id, l.winner_id, l.created_at, l.updated_at, - l.match_id, l.has_scores, GROUP_CONCAT(p2l.player_id ORDER BY p2l.order ASC) + SELECT l.id FROM leg l JOIN matches m on m.id = l.match_id - JOIN player2leg p2l ON p2l.leg_id = l.id WHERE l.has_scores = 1 AND (m.match_type_id = ? OR l.leg_type_id = ?) AND l.updated_at >= DATE_FORMAT(STR_TO_DATE(?, '%Y-%m-%d %T'), "%Y-%m-%d %T") AND m.is_abandoned = 0 AND l.is_finished = 1 AND l.has_scores = 1 @@ -757,23 +753,14 @@ func GetLegsToRecalculate(matchType int, since string) ([]*models.Leg, error) { } defer rows.Close() - legs := make([]*models.Leg, 0) + legs := make([]int, 0) for rows.Next() { - leg := new(models.Leg) - var players string - err := rows.Scan(&leg.ID, &leg.Endtime, &leg.StartingScore, &leg.IsFinished, &leg.CurrentPlayerID, - &leg.WinnerPlayerID, &leg.CreatedAt, &leg.UpdatedAt, &leg.MatchID, &leg.HasScores, &players) + var legId int + err := rows.Scan(&legId) if err != nil { return nil, err } - leg.Players = util.StringToIntArray(players) - if matchType == models.TICTACTOE || matchType == models.KNOCKOUT { - leg.Parameters, err = GetLegParameters(leg.ID) - if err != nil { - return nil, err - } - } - legs = append(legs, leg) + legs = append(legs, legId) } if err = rows.Err(); err != nil { return nil, err diff --git a/data/recalculate.go b/data/recalculate.go new file mode 100644 index 00000000..9b2c74a0 --- /dev/null +++ b/data/recalculate.go @@ -0,0 +1,84 @@ +package data + +import ( + "fmt" + "log" + + "github.com/kcapp/api/models" +) + +// RecalculateTicTacToeStatistics will recaulcate statistics for Tic Tac Toe legs +func RecalculateStatistics(matchType int, legID int, since string, dryRun bool) error { + legs := make([]int, 0) + if legID != 0 { + log.Printf("Recalculating statistics for leg %d", legID) + legs = append(legs, legID) + } else { + log.Printf("Recalculating %s statistics since %s", models.MatchTypes[matchType], since) + ids, err := GetLegsToRecalculate(matchType, since) + if err != nil { + return err + } + legs = append(legs, ids...) + } + + var queries []string + var err error + switch matchType { + case models.X01: + queries, err = RecalculateX01Statistics(legs) + case models.SHOOTOUT: + queries, err = RecalculateShootoutStatistics(legs) + case models.X01HANDICAP: + case models.CRICKET: + queries, err = RecalculateCricketStatistics(legs) + case models.DARTSATX: + queries, err = RecalculateDartsAtXStatistics(legs) + case models.AROUNDTHEWORLD: + queries, err = RecalculateAroundTheWorldStatistics(legs) + case models.SHANGHAI: + queries, err = RecalculateShanghaiStatistics(legs) + case models.AROUNDTHECLOCK: + queries, err = RecalculateAroundTheClockStatistics(legs) + case models.TICTACTOE: + queries, err = RecalculateTicTacToeStatistics(legs) + case models.BERMUDATRIANGLE: + queries, err = RecalculateBermudaTriangleStatistics(legs) + case models.FOURTWENTY: + queries, err = Recalculate420Statistics(legs) + case models.KILLBULL: + queries, err = RecalculateKillBullStatistics(legs) + case models.GOTCHA: + queries, err = RecalculateGotchaStatistics(legs) + case models.JDCPRACTICE: + queries, err = RecalculateJDCPracticeStatistics(legs) + case models.KNOCKOUT: + queries, err = RecalculateKnockoutStatistics(legs) + default: + return fmt.Errorf("cannot recalculate statistics for type %d", matchType) + } + if err != nil { + return err + } + + if len(queries) == 0 { + log.Print("No legs to recalculate") + } else { + if dryRun { + for _, query := range queries { + log.Print(query) + } + } else { + log.Printf("Executing %d UPDATE queries", len(queries)) + tx, err := models.DB.Begin() + if err != nil { + return err + } + for _, query := range queries { + tx.Exec(query) + } + tx.Commit() + } + } + return nil +} diff --git a/data/statistics_420.go b/data/statistics_420.go index 9064bdea..8d07855e 100644 --- a/data/statistics_420.go +++ b/data/statistics_420.go @@ -3,7 +3,6 @@ package data import ( "database/sql" "fmt" - "log" "github.com/kcapp/api/models" ) @@ -394,41 +393,18 @@ func Calculate420Statistics(legID int) (map[int]*models.Statistics420, error) { } // Recalculate420Statistics will recaulcate statistics for 420 legs -func Recalculate420Statistics(since string, dryRun bool) error { - legs, err := GetLegsToRecalculate(models.FOURTWENTY, since) - if err != nil { - return err - } - +func Recalculate420Statistics(legs []int) ([]string, error) { queries := make([]string, 0) - for _, leg := range legs { - stats, err := Calculate420Statistics(leg.ID) + for _, legID := range legs { + stats, err := Calculate420Statistics(legID) if err != nil { - return err + return nil, err } for playerID, stat := range stats { - queries = append(queries, fmt.Sprintf(`UPDATE statistics_420 SET score = %d, total_hit_rate = %f, hit_rate_1 = %f, hit_rate_2 = %f, hit_rate_3 = %f, hit_rate_4 = %f, hit_rate_5 = %f, hit_rate_6 = %f, hit_rate_7 = %f, hit_rate_8 = %f, - hit_rate_9 = %f, hit_rate_10 = %f, hit_rate_11 = %f, hit_rate_12 = %f, hit_rate_13 = %f, hit_rate_14 = %f, hit_rate_15 = %f, hit_rate_16 = %f, hit_rate_17 = %f, hit_rate_18 = %f, hit_rate_19 = %f, hit_rate_20 = %f, - hit_rate_bull = %f WHERE leg_id = %d AND player_id = %d;`, + queries = append(queries, fmt.Sprintf(`UPDATE statistics_420 SET score = %d, total_hit_rate = %f, hit_rate_1 = %f, hit_rate_2 = %f, hit_rate_3 = %f, hit_rate_4 = %f, hit_rate_5 = %f, hit_rate_6 = %f, hit_rate_7 = %f, hit_rate_8 = %f, hit_rate_9 = %f, hit_rate_10 = %f, hit_rate_11 = %f, hit_rate_12 = %f, hit_rate_13 = %f, hit_rate_14 = %f, hit_rate_15 = %f, hit_rate_16 = %f, hit_rate_17 = %f, hit_rate_18 = %f, hit_rate_19 = %f, hit_rate_20 = %f, hit_rate_bull = %f WHERE leg_id = %d AND player_id = %d;`, stat.Score, stat.TotalHitRate, stat.Hitrates[1], stat.Hitrates[2], stat.Hitrates[3], stat.Hitrates[4], stat.Hitrates[5], stat.Hitrates[6], stat.Hitrates[7], stat.Hitrates[8], stat.Hitrates[9], stat.Hitrates[10], - stat.Hitrates[11], stat.Hitrates[12], stat.Hitrates[13], stat.Hitrates[14], stat.Hitrates[15], stat.Hitrates[16], stat.Hitrates[17], stat.Hitrates[18], stat.Hitrates[19], stat.Hitrates[20], stat.Hitrates[25], leg.ID, playerID)) + stat.Hitrates[11], stat.Hitrates[12], stat.Hitrates[13], stat.Hitrates[14], stat.Hitrates[15], stat.Hitrates[16], stat.Hitrates[17], stat.Hitrates[18], stat.Hitrates[19], stat.Hitrates[20], stat.Hitrates[25], legID, playerID)) } } - if dryRun { - for _, query := range queries { - log.Print(query) - } - } else { - log.Printf("Executing %d UPDATE queries", len(queries)) - tx, err := models.DB.Begin() - if err != nil { - return err - } - for _, query := range queries { - tx.Exec(query) - } - tx.Commit() - } - - return nil + return queries, nil } diff --git a/data/statistics_around_the_clock.go b/data/statistics_around_the_clock.go index b6967cd0..a58ebc11 100644 --- a/data/statistics_around_the_clock.go +++ b/data/statistics_around_the_clock.go @@ -3,7 +3,6 @@ package data import ( "database/sql" "fmt" - "log" "github.com/guregu/null" "github.com/kcapp/api/models" @@ -427,42 +426,20 @@ func isHit(stats *models.StatisticsAroundThe, currentScore int, dart *models.Dar } // RecalculateAroundTheClockStatistics will recaulcate statistics for Around the Clock legs -func RecalculateAroundTheClockStatistics(since string, dryRun bool) error { - legs, err := GetLegsToRecalculate(models.AROUNDTHECLOCK, since) - if err != nil { - return err - } - +func RecalculateAroundTheClockStatistics(legs []int) ([]string, error) { queries := make([]string, 0) - for _, leg := range legs { - stats, err := CalculateAroundTheClockStatistics(leg.ID) + for _, legID := range legs { + stats, err := CalculateAroundTheClockStatistics(legID) if err != nil { - return err + return nil, err } for playerID, stat := range stats { queries = append(queries, fmt.Sprintf(`UPDATE statistics_around_the SET darts_thrown = %d, score = %d, longest_streak = %d, total_hit_rate = %f, hit_rate_1 = %f, hit_rate_2 = %f, hit_rate_3 = %f, hit_rate_4 = %f, hit_rate_5 = %f, hit_rate_6 = %f, hit_rate_7 = %f, hit_rate_8 = %f, hit_rate_9 = %f, hit_rate_10 = %f, hit_rate_11 = %f, hit_rate_12 = %f, hit_rate_13 = %f, hit_rate_14 = %f, hit_rate_15 = %f, hit_rate_16 = %f, hit_rate_17 = %f, hit_rate_18 = %f, hit_rate_19 = %f, hit_rate_20 = %f, hit_rate_bull = %f WHERE leg_id = %d AND player_id = %d;`, stat.DartsThrown, stat.Score, stat.LongestStreak.Int64, stat.TotalHitRate, stat.Hitrates[1], stat.Hitrates[2], stat.Hitrates[3], stat.Hitrates[4], stat.Hitrates[5], stat.Hitrates[6], stat.Hitrates[7], stat.Hitrates[8], stat.Hitrates[9], stat.Hitrates[10], stat.Hitrates[11], stat.Hitrates[12], stat.Hitrates[13], stat.Hitrates[14], stat.Hitrates[15], stat.Hitrates[16], stat.Hitrates[17], stat.Hitrates[18], stat.Hitrates[19], stat.Hitrates[20], stat.Hitrates[25], - leg.ID, playerID)) + legID, playerID)) } } - - if dryRun { - for _, query := range queries { - log.Print(query) - } - } else { - log.Printf("Executing %d UPDATE queries", len(queries)) - tx, err := models.DB.Begin() - if err != nil { - return err - } - for _, query := range queries { - tx.Exec(query) - } - tx.Commit() - } - - return nil + return queries, nil } diff --git a/data/statistics_around_the_world.go b/data/statistics_around_the_world.go index 16678b17..67b88dba 100644 --- a/data/statistics_around_the_world.go +++ b/data/statistics_around_the_world.go @@ -3,7 +3,6 @@ package data import ( "database/sql" "fmt" - "log" "github.com/guregu/null" "github.com/kcapp/api/models" @@ -775,97 +774,44 @@ func CalculateAroundTheWorldStatistics(legID int, matchType int) (map[int]*model } // RecalculateAroundTheWorldStatistics will recaulcate statistics for Around the World legs -func RecalculateAroundTheWorldStatistics(since string, dryRun bool) error { - legs, err := GetLegsToRecalculate(models.AROUNDTHEWORLD, since) - if err != nil { - return err - } - +func RecalculateAroundTheWorldStatistics(legs []int) ([]string, error) { queries := make([]string, 0) - for _, leg := range legs { - stats, err := CalculateAroundTheWorldStatistics(leg.ID, models.AROUNDTHEWORLD) + for _, legID := range legs { + stats, err := CalculateAroundTheWorldStatistics(legID, models.AROUNDTHEWORLD) if err != nil { - return err + return nil, err } for playerID, stat := range stats { - queries = append(queries, fmt.Sprintf(`UPDATE statistics_around_the SET darts_thrown = %d, score = %d, mpr = %f, total_hit_rate = %f, hit_rate_1 = %f, - hit_rate_2 = %f, hit_rate_3 = %f, hit_rate_4 = %f, hit_rate_5 = %f, hit_rate_6 = %f, hit_rate_7 = %f, hit_rate_8 = %f, hit_rate_9 = %f, - hit_rate_10 = %f, hit_rate_11 = %f, hit_rate_12 = %f, hit_rate_13 = %f, hit_rate_14 = %f, hit_rate_15 = %f, hit_rate_16 = %f, hit_rate_17 = %f, - hit_rate_18 = %f, hit_rate_19 = %f, hit_rate_20 = %f, hit_rate_bull = %f WHERE leg_id = %d AND player_id = %d;`, + queries = append(queries, fmt.Sprintf(`UPDATE statistics_around_the SET darts_thrown = %d, score = %d, mpr = %f, total_hit_rate = %f, hit_rate_1 = %f, hit_rate_2 = %f, hit_rate_3 = %f, hit_rate_4 = %f, hit_rate_5 = %f, hit_rate_6 = %f, hit_rate_7 = %f, hit_rate_8 = %f, hit_rate_9 = %f, hit_rate_10 = %f, hit_rate_11 = %f, hit_rate_12 = %f, hit_rate_13 = %f, hit_rate_14 = %f, hit_rate_15 = %f, hit_rate_16 = %f, hit_rate_17 = %f, hit_rate_18 = %f, hit_rate_19 = %f, hit_rate_20 = %f, hit_rate_bull = %f WHERE leg_id = %d AND player_id = %d;`, stat.DartsThrown, stat.Score, stat.MPR.Float64, stat.TotalHitRate, stat.Hitrates[1], stat.Hitrates[2], stat.Hitrates[3], stat.Hitrates[4], stat.Hitrates[5], stat.Hitrates[6], stat.Hitrates[7], stat.Hitrates[8], stat.Hitrates[9], stat.Hitrates[10], stat.Hitrates[11], stat.Hitrates[12], stat.Hitrates[13], stat.Hitrates[14], stat.Hitrates[15], stat.Hitrates[16], stat.Hitrates[17], stat.Hitrates[18], stat.Hitrates[19], stat.Hitrates[20], stat.Hitrates[25], - leg.ID, playerID)) + legID, playerID)) } } - - if dryRun { - for _, query := range queries { - log.Print(query) - } - } else { - log.Printf("Executing %d UPDATE queries", len(queries)) - tx, err := models.DB.Begin() - if err != nil { - return err - } - for _, query := range queries { - tx.Exec(query) - } - tx.Commit() - } - return nil + return queries, nil } // RecalculateShanghaiStatistics will recaulcate statistics for Shanghai legs -func RecalculateShanghaiStatistics(since string, dryRun bool) error { - legs, err := GetLegsToRecalculate(models.SHANGHAI, since) - if err != nil { - return err - } - +func RecalculateShanghaiStatistics(legs []int) ([]string, error) { queries := make([]string, 0) - for _, leg := range legs { - stats, err := CalculateAroundTheWorldStatistics(leg.ID, models.SHANGHAI) + for _, legID := range legs { + stats, err := CalculateAroundTheWorldStatistics(legID, models.SHANGHAI) if err != nil { - return err + return nil, err } for playerID, stat := range stats { + query := fmt.Sprintf(`UPDATE statistics_around_the SET darts_thrown = %d, score = %d, mpr = %f, total_hit_rate = %f, hit_rate_1 = %f, hit_rate_2 = %f, hit_rate_3 = %f, hit_rate_4 = %f, hit_rate_5 = %f, hit_rate_6 = %f, hit_rate_7 = %f, hit_rate_8 = %f, hit_rate_9 = %f, hit_rate_10 = %f, hit_rate_11 = %f, hit_rate_12 = %f, hit_rate_13 = %f, hit_rate_14 = %f, hit_rate_15 = %f, hit_rate_16 = %f, hit_rate_17 = %f, hit_rate_18 = %f, hit_rate_19 = %f, hit_rate_20 = %f`, + stat.DartsThrown, stat.Score, stat.MPR.Float64, stat.TotalHitRate, stat.Hitrates[1], stat.Hitrates[2], stat.Hitrates[3], stat.Hitrates[4], stat.Hitrates[5], + stat.Hitrates[6], stat.Hitrates[7], stat.Hitrates[8], stat.Hitrates[9], stat.Hitrates[10], stat.Hitrates[11], stat.Hitrates[12], stat.Hitrates[13], + stat.Hitrates[14], stat.Hitrates[15], stat.Hitrates[16], stat.Hitrates[17], stat.Hitrates[18], stat.Hitrates[19], stat.Hitrates[20]) + if stat.Shanghai.Valid { - queries = append(queries, fmt.Sprintf(`UPDATE statistics_around_the SET darts_thrown = %d, score = %d, shanghai = %d, mpr = %f, total_hit_rate = %f, hit_rate_1 = %f, - hit_rate_2 = %f, hit_rate_3 = %f, hit_rate_4 = %f, hit_rate_5 = %f, hit_rate_6 = %f, hit_rate_7 = %f, hit_rate_8 = %f, hit_rate_9 = %f, - hit_rate_10 = %f, hit_rate_11 = %f, hit_rate_12 = %f, hit_rate_13 = %f, hit_rate_14 = %f, hit_rate_15 = %f, hit_rate_16 = %f, hit_rate_17 = %f, - hit_rate_18 = %f, hit_rate_19 = %f, hit_rate_20 = %f WHERE leg_id = %d AND player_id = %d;`, - stat.DartsThrown, stat.Score, stat.Shanghai.Int64, stat.MPR.Float64, stat.TotalHitRate, stat.Hitrates[1], stat.Hitrates[2], stat.Hitrates[3], stat.Hitrates[4], stat.Hitrates[5], - stat.Hitrates[6], stat.Hitrates[7], stat.Hitrates[8], stat.Hitrates[9], stat.Hitrates[10], stat.Hitrates[11], stat.Hitrates[12], stat.Hitrates[13], - stat.Hitrates[14], stat.Hitrates[15], stat.Hitrates[16], stat.Hitrates[17], stat.Hitrates[18], stat.Hitrates[19], stat.Hitrates[20], - leg.ID, playerID)) - } else { - queries = append(queries, fmt.Sprintf(`UPDATE statistics_around_the SET darts_thrown = %d, score = %d, shanghai = null, mpr = %f, total_hit_rate = %f, hit_rate_1 = %f, - hit_rate_2 = %f, hit_rate_3 = %f, hit_rate_4 = %f, hit_rate_5 = %f, hit_rate_6 = %f, hit_rate_7 = %f, hit_rate_8 = %f, hit_rate_9 = %f, - hit_rate_10 = %f, hit_rate_11 = %f, hit_rate_12 = %f, hit_rate_13 = %f, hit_rate_14 = %f, hit_rate_15 = %f, hit_rate_16 = %f, hit_rate_17 = %f, - hit_rate_18 = %f, hit_rate_19 = %f, hit_rate_20 = %f WHERE leg_id = %d AND player_id = %d;`, - stat.DartsThrown, stat.Score, stat.MPR.Float64, stat.TotalHitRate, stat.Hitrates[1], stat.Hitrates[2], stat.Hitrates[3], stat.Hitrates[4], stat.Hitrates[5], - stat.Hitrates[6], stat.Hitrates[7], stat.Hitrates[8], stat.Hitrates[9], stat.Hitrates[10], stat.Hitrates[11], stat.Hitrates[12], stat.Hitrates[13], - stat.Hitrates[14], stat.Hitrates[15], stat.Hitrates[16], stat.Hitrates[17], stat.Hitrates[18], stat.Hitrates[19], stat.Hitrates[20], - leg.ID, playerID)) + query += fmt.Sprintf(`, shanghai = %d`, stat.Shanghai.Int64) } + query += fmt.Sprintf(" WHERE leg_id = %d AND player_id = %d;", legID, playerID) + queries = append(queries, query) } } - if dryRun { - for _, query := range queries { - log.Print(query) - } - } else { - log.Printf("Executing %d UPDATE queries", len(queries)) - tx, err := models.DB.Begin() - if err != nil { - return err - } - for _, query := range queries { - tx.Exec(query) - } - tx.Commit() - } - return nil + return queries, nil } diff --git a/data/statistics_bermuda_triangle.go b/data/statistics_bermuda_triangle.go index 1585f209..1437f3ed 100644 --- a/data/statistics_bermuda_triangle.go +++ b/data/statistics_bermuda_triangle.go @@ -3,7 +3,6 @@ package data import ( "database/sql" "fmt" - "log" "github.com/kcapp/api/models" ) @@ -383,42 +382,18 @@ func CalculateBermudaTriangleStatistics(legID int) (map[int]*models.StatisticsBe } // RecalculateBermudaTriangleStatistics will recaulcate statistics for Bermuda Triangle legs -func RecalculateBermudaTriangleStatistics(since string, dryRun bool) error { - legs, err := GetLegsToRecalculate(models.BERMUDATRIANGLE, since) - if err != nil { - return err - } - +func RecalculateBermudaTriangleStatistics(legs []int) ([]string, error) { queries := make([]string, 0) - for _, leg := range legs { - stats, err := CalculateBermudaTriangleStatistics(leg.ID) + for _, legID := range legs { + stats, err := CalculateBermudaTriangleStatistics(legID) if err != nil { - return err + return nil, err } for playerID, stat := range stats { - queries = append(queries, fmt.Sprintf(`UPDATE statistics_bermuda_triangle SET darts_thrown = %d, score = %d, mpr = %f, total_marks = %d, highest_score_reached = 22%d4, total_hit_rate = %f, hit_rate_1 = %f, hit_rate_2 = %f, - hit_rate_3 = %f, hit_rate_4 = %f, hit_rate_5 = %f, hit_rate_6 = %f, hit_rate_7 = %f, hit_rate_8 = %f, hit_rate_9 = %f, hit_rate_10 = %f, hit_rate_11 = %f, hit_rate_12 = %f, - hit_rate_13 = %f, hit_count = %d WHERE leg_id = %d AND player_id = %d;`, + queries = append(queries, fmt.Sprintf(`UPDATE statistics_bermuda_triangle SET darts_thrown = %d, score = %d, mpr = %f, total_marks = %d, highest_score_reached = 22%d4, total_hit_rate = %f, hit_rate_1 = %f, hit_rate_2 = %f, hit_rate_3 = %f, hit_rate_4 = %f, hit_rate_5 = %f, hit_rate_6 = %f, hit_rate_7 = %f, hit_rate_8 = %f, hit_rate_9 = %f, hit_rate_10 = %f, hit_rate_11 = %f, hit_rate_12 = %f, hit_rate_13 = %f, hit_count = %d WHERE leg_id = %d AND player_id = %d;`, stat.DartsThrown, stat.Score, stat.MPR, stat.TotalMarks, stat.HighestScoreReached, stat.TotalHitRate, stat.Hitrates[0], stat.Hitrates[1], stat.Hitrates[2], stat.Hitrates[3], stat.Hitrates[4], - stat.Hitrates[5], stat.Hitrates[6], stat.Hitrates[7], stat.Hitrates[8], stat.Hitrates[9], stat.Hitrates[10], stat.Hitrates[11], stat.Hitrates[12], stat.HitCount, leg.ID, playerID)) - } - } - - if dryRun { - for _, query := range queries { - log.Print(query) - } - } else { - log.Printf("Executing %d UPDATE queries", len(queries)) - tx, err := models.DB.Begin() - if err != nil { - return err - } - for _, query := range queries { - tx.Exec(query) + stat.Hitrates[5], stat.Hitrates[6], stat.Hitrates[7], stat.Hitrates[8], stat.Hitrates[9], stat.Hitrates[10], stat.Hitrates[11], stat.Hitrates[12], stat.HitCount, legID, playerID)) } - tx.Commit() } - - return nil + return queries, nil } diff --git a/data/statistics_cricket.go b/data/statistics_cricket.go index 215cd668..70cf8a60 100644 --- a/data/statistics_cricket.go +++ b/data/statistics_cricket.go @@ -3,7 +3,6 @@ package data import ( "database/sql" "fmt" - "log" "github.com/guregu/null" @@ -292,39 +291,18 @@ func CalculateCricketStatistics(legID int) (map[int]*models.StatisticsCricket, e } // RecalculateCricketStatistics will recaulcate statistics for Cricket matches -func RecalculateCricketStatistics(since string, dryRun bool) error { - legs, err := GetLegsToRecalculate(models.CRICKET, since) - if err != nil { - return err - } - +func RecalculateCricketStatistics(legs []int) ([]string, error) { queries := make([]string, 0) - for _, leg := range legs { - stats, err := CalculateCricketStatistics(leg.ID) + for _, legID := range legs { + stats, err := CalculateCricketStatistics(legID) if err != nil { - return err + return nil, err } for playerID, stat := range stats { queries = append(queries, fmt.Sprintf(`UPDATE statistics_cricket SET total_marks = %d, rounds = %d, score = %d, first_nine_marks = %d mpr = %f, first_nine_mpr = %f, marks5 = %d, marks6 = %d, marks7 = %d, marks8 = %d, marks9 = %d, WHERE leg_id = %d AND player_id = %d;`, - stat.TotalMarks, stat.Rounds, stat.Score.Int64, stat.FirstNineMarks, stat.MPR, stat.FirstNineMPR, stat.Marks5, stat.Marks6, stat.Marks7, stat.Marks8, stat.Marks9, leg.ID, playerID)) + stat.TotalMarks, stat.Rounds, stat.Score.Int64, stat.FirstNineMarks, stat.MPR, stat.FirstNineMPR, stat.Marks5, stat.Marks6, stat.Marks7, stat.Marks8, stat.Marks9, legID, playerID)) } } - if dryRun { - for _, query := range queries { - log.Print(query) - } - } else { - log.Printf("Executing %d UPDATE queries", len(queries)) - tx, err := models.DB.Begin() - if err != nil { - return err - } - for _, query := range queries { - tx.Exec(query) - } - tx.Commit() - } - - return nil + return queries, nil } diff --git a/data/statistics_darts_at_x.go b/data/statistics_darts_at_x.go index 1b061390..bf67a36e 100644 --- a/data/statistics_darts_at_x.go +++ b/data/statistics_darts_at_x.go @@ -3,7 +3,6 @@ package data import ( "database/sql" "fmt" - "log" "github.com/guregu/null" "github.com/kcapp/api/models" @@ -315,41 +314,19 @@ func CalculateDartsAtXStatistics(legID int) (map[int]*models.StatisticsDartsAtX, } // RecalculateDartsAtXStatistics will recaulcate statistics for Darts at X legs -func RecalculateDartsAtXStatistics(since string, dryRun bool) error { - legs, err := GetLegsToRecalculate(models.DARTSATX, since) - if err != nil { - return err - } - +func RecalculateDartsAtXStatistics(legs []int) ([]string, error) { queries := make([]string, 0) - for _, leg := range legs { - stats, err := CalculateDartsAtXStatistics(leg.ID) + for _, legID := range legs { + stats, err := CalculateDartsAtXStatistics(legID) if err != nil { - return err + return nil, err } for playerID, stat := range stats { queries = append(queries, fmt.Sprintf(`UPDATE statistics_darts_at_x SET score = %d, singles = %d, doubles = %d, triples = %d, hit_rate = %f, hits5 = %d, hits6 = %d, hits7 = %d, hits8 = %d, hits9 = %d WHERE leg_id = %d AND player_id = %d;`, - stat.Score.Int64, stat.Singles, stat.Doubles, stat.Triples, stat.HitRate, stat.Hits5, stat.Hits6, stat.Hits7, stat.Hits8, stat.Hits9, leg.ID, playerID)) - } - } - - if dryRun { - for _, query := range queries { - log.Print(query) - } - } else { - log.Printf("Executing %d UPDATE queries", len(queries)) - tx, err := models.DB.Begin() - if err != nil { - return err - } - for _, query := range queries { - tx.Exec(query) + stat.Score.Int64, stat.Singles, stat.Doubles, stat.Triples, stat.HitRate, stat.Hits5, stat.Hits6, stat.Hits7, stat.Hits8, stat.Hits9, legID, playerID)) } - tx.Commit() } - - return nil + return queries, nil } func addDart(number int, dart *models.Dart, stats *models.StatisticsDartsAtX) int { diff --git a/data/statistics_gotcha.go b/data/statistics_gotcha.go index 4fdfb89e..29273ce3 100644 --- a/data/statistics_gotcha.go +++ b/data/statistics_gotcha.go @@ -3,7 +3,6 @@ package data import ( "database/sql" "fmt" - "log" "github.com/kcapp/api/models" ) @@ -277,39 +276,17 @@ func getPlayersReset(visit *models.Visit, players map[int]*models.Player2Leg) in } // RecalculateGotchaStatistics will recaulcate statistics for Gotcha legs -func RecalculateGotchaStatistics(since string, dryRun bool) error { - legs, err := GetLegsToRecalculate(models.GOTCHA, since) - if err != nil { - return err - } - +func RecalculateGotchaStatistics(legs []int) ([]string, error) { queries := make([]string, 0) - for _, leg := range legs { - stats, err := CalculateGotchaStatistics(leg.ID) + for _, legID := range legs { + stats, err := CalculateGotchaStatistics(legID) if err != nil { - return err + return nil, err } for playerID, stat := range stats { queries = append(queries, fmt.Sprintf(`UPDATE statistics_gotcha SET darts_thrown = %d, highest_score = %d, times_reset = %d, others_reset = %d, score = %d WHERE leg_id = %d AND player_id = %d;`, - stat.DartsThrown, stat.HighestScore, stat.TimesReset, stat.OthersReset, stat.Score, leg.ID, playerID)) - } - } - - if dryRun { - for _, query := range queries { - log.Print(query) - } - } else { - log.Printf("Executing %d UPDATE queries", len(queries)) - tx, err := models.DB.Begin() - if err != nil { - return err - } - for _, query := range queries { - tx.Exec(query) + stat.DartsThrown, stat.HighestScore, stat.TimesReset, stat.OthersReset, stat.Score, legID, playerID)) } - tx.Commit() } - - return nil + return queries, nil } diff --git a/data/statistics_jdc_practice.go b/data/statistics_jdc_practice.go index 84678a4c..d857b17e 100644 --- a/data/statistics_jdc_practice.go +++ b/data/statistics_jdc_practice.go @@ -3,7 +3,6 @@ package data import ( "database/sql" "fmt" - "log" "github.com/guregu/null" "github.com/kcapp/api/models" @@ -275,39 +274,17 @@ func CalculateJDCPracticeStatistics(legID int) (map[int]*models.StatisticsJDCPra } // RecalculateJDCPracticeStatistics will recaulcate statistics for JDC Practice legs -func RecalculateJDCPracticeStatistics(since string, dryRun bool) error { - legs, err := GetLegsToRecalculate(models.JDCPRACTICE, since) - if err != nil { - return err - } - +func RecalculateJDCPracticeStatistics(legs []int) ([]string, error) { queries := make([]string, 0) - for _, leg := range legs { - stats, err := CalculateJDCPracticeStatistics(leg.ID) + for _, legID := range legs { + stats, err := CalculateJDCPracticeStatistics(legID) if err != nil { - return err + return nil, err } for playerID, stat := range stats { - queries = append(queries, fmt.Sprintf(`UPDATE statistics_jdc_practice SET darts_thrown = %d, score = %d, mpr = %f, - shanghai_count = %d, doubles_hitrate = %f WHERE leg_id = %d AND player_id = %d;`, - stat.DartsThrown, stat.Score, stat.MPR.Float64, stat.ShanghaiCount, stat.DoublesHitrate, leg.ID, playerID)) - } - } - - if dryRun { - for _, query := range queries { - log.Print(query) - } - } else { - log.Printf("Executing %d UPDATE queries", len(queries)) - tx, err := models.DB.Begin() - if err != nil { - return err - } - for _, query := range queries { - tx.Exec(query) + queries = append(queries, fmt.Sprintf(`UPDATE statistics_jdc_practice SET darts_thrown = %d, score = %d, mpr = %f, shanghai_count = %d, doubles_hitrate = %f WHERE leg_id = %d AND player_id = %d;`, + stat.DartsThrown, stat.Score, stat.MPR.Float64, stat.ShanghaiCount, stat.DoublesHitrate, legID, playerID)) } - tx.Commit() } - return nil + return queries, nil } diff --git a/data/statistics_kill_bull.go b/data/statistics_kill_bull.go index ac8a40a4..af81a50a 100644 --- a/data/statistics_kill_bull.go +++ b/data/statistics_kill_bull.go @@ -3,7 +3,6 @@ package data import ( "database/sql" "fmt" - "log" "github.com/kcapp/api/models" ) @@ -309,39 +308,17 @@ func CalculateKillBullStatistics(legID int) (map[int]*models.StatisticsKillBull, } // RecalculateKillBullStatistics will recaulcate statistics for Kill Bull legs -func RecalculateKillBullStatistics(since string, dryRun bool) error { - legs, err := GetLegsToRecalculate(models.KILLBULL, since) - if err != nil { - return err - } - +func RecalculateKillBullStatistics(legs []int) ([]string, error) { queries := make([]string, 0) - for _, leg := range legs { - stats, err := CalculateKillBullStatistics(leg.ID) + for _, legID := range legs { + stats, err := CalculateKillBullStatistics(legID) if err != nil { - return err + return nil, err } for playerID, stat := range stats { - queries = append(queries, fmt.Sprintf(`UPDATE statistics_kill_bull SET darts_thrown = %d, score = %d, marks3 = %d, marks4 = %d, marks5 = %d, marks6 = %d, longest_streak = %d, times_busted = %d, total_hit_rate = %f - WHERE leg_id = %d AND player_id = %d;`, stat.DartsThrown, stat.Score, stat.Marks3, stat.Marks4, stat.Marks5, stat.Marks6, stat.LongestStreak, stat.TimesBusted, stat.TotalHitRate, leg.ID, playerID)) - } - } - - if dryRun { - for _, query := range queries { - log.Print(query) - } - } else { - log.Printf("Executing %d UPDATE queries", len(queries)) - tx, err := models.DB.Begin() - if err != nil { - return err - } - for _, query := range queries { - tx.Exec(query) + queries = append(queries, fmt.Sprintf(`UPDATE statistics_kill_bull SET darts_thrown = %d, score = %d, marks3 = %d, marks4 = %d, marks5 = %d, marks6 = %d, longest_streak = %d, times_busted = %d, total_hit_rate = %f WHERE leg_id = %d AND player_id = %d;`, + stat.DartsThrown, stat.Score, stat.Marks3, stat.Marks4, stat.Marks5, stat.Marks6, stat.LongestStreak, stat.TimesBusted, stat.TotalHitRate, legID, playerID)) } - tx.Commit() } - - return nil + return queries, nil } diff --git a/data/statistics_knockout.go b/data/statistics_knockout.go index 433c16f3..56dc1544 100644 --- a/data/statistics_knockout.go +++ b/data/statistics_knockout.go @@ -3,7 +3,6 @@ package data import ( "database/sql" "fmt" - "log" "github.com/kcapp/api/models" ) @@ -254,39 +253,17 @@ func CalculateKnockoutStatistics(legID int) (map[int]*models.StatisticsKnockout, } // RecalculateKnockoutStatistics will recaulcate statistics for Knockout legs -func RecalculateKnockoutStatistics(since string, dryRun bool) error { - legs, err := GetLegsToRecalculate(models.KNOCKOUT, since) - if err != nil { - return err - } - +func RecalculateKnockoutStatistics(legs []int) ([]string, error) { queries := make([]string, 0) - for _, leg := range legs { - stats, err := CalculateKnockoutStatistics(leg.ID) + for _, legID := range legs { + stats, err := CalculateKnockoutStatistics(legID) if err != nil { - return err + return nil, err } for playerID, stat := range stats { - queries = append(queries, fmt.Sprintf(`UPDATE statistics_knockout SET darts_thrown = %d, avg_score = %f, lives_lost = %d, lives_taken = %d, - final_position = %d WHERE leg_id = %d AND player_id = %d;`, - stat.DartsThrown, stat.AvgScore, stat.LivesLost, stat.LivesTaken, stat.FinalPosition, leg.ID, playerID)) - } - } - - if dryRun { - for _, query := range queries { - log.Print(query) - } - } else { - log.Printf("Executing %d UPDATE queries", len(queries)) - tx, err := models.DB.Begin() - if err != nil { - return err - } - for _, query := range queries { - tx.Exec(query) + queries = append(queries, fmt.Sprintf(`UPDATE statistics_knockout SET darts_thrown = %d, avg_score = %f, lives_lost = %d, lives_taken = %d, final_position = %d WHERE leg_id = %d AND player_id = %d;`, + stat.DartsThrown, stat.AvgScore, stat.LivesLost, stat.LivesTaken, stat.FinalPosition, legID, playerID)) } - tx.Commit() } - return nil + return queries, nil } diff --git a/data/statistics_shootout.go b/data/statistics_shootout.go index 3ea42da6..4cb5724b 100644 --- a/data/statistics_shootout.go +++ b/data/statistics_shootout.go @@ -3,7 +3,6 @@ package data import ( "database/sql" "fmt" - "log" "github.com/kcapp/api/models" ) @@ -250,38 +249,17 @@ func CalculateShootoutStatistics(legID int) (map[int]*models.StatisticsShootout, } // RecalculateShootoutStatistics will recaulcate statistics for Shootout matches -func RecalculateShootoutStatistics(since string, dryRun bool) error { - legs, err := GetLegsToRecalculate(models.SHOOTOUT, since) - if err != nil { - return err - } - +func RecalculateShootoutStatistics(legs []int) ([]string, error) { queries := make([]string, 0) - for _, leg := range legs { - stats, err := CalculateShootoutStatistics(leg.ID) + for _, legID := range legs { + stats, err := CalculateShootoutStatistics(legID) if err != nil { - return err + return nil, err } for playerID, stat := range stats { queries = append(queries, fmt.Sprintf(`UPDATE statistics_shootout SET score = %d, ppd = %f, 60s_plus = %d, 100s_plus = %d, 140s_plus = %d, 180s = %d WHERE leg_id = %d AND player_id = %d;`, - stat.Score, stat.PPD, stat.Score60sPlus, stat.Score100sPlus, stat.Score140sPlus, stat.Score180s, leg.ID, playerID)) + stat.Score, stat.PPD, stat.Score60sPlus, stat.Score100sPlus, stat.Score140sPlus, stat.Score180s, legID, playerID)) } } - if dryRun { - for _, query := range queries { - log.Print(query) - } - } else { - log.Printf("Executing %d UPDATE queries", len(queries)) - tx, err := models.DB.Begin() - if err != nil { - return err - } - for _, query := range queries { - tx.Exec(query) - } - tx.Commit() - } - - return nil + return queries, nil } diff --git a/data/statistics_tic_tac_toe.go b/data/statistics_tic_tac_toe.go index 74c94b53..6285f8cb 100644 --- a/data/statistics_tic_tac_toe.go +++ b/data/statistics_tic_tac_toe.go @@ -3,7 +3,6 @@ package data import ( "database/sql" "fmt" - "log" "github.com/kcapp/api/models" ) @@ -240,38 +239,18 @@ func CalculateTicTacToeStatistics(legID int) (map[int]*models.StatisticsTicTacTo return statisticsMap, nil } -// RecalculateTicTacToeStatistics will recaulcate statistics for Tic Tac Toe legs -func RecalculateTicTacToeStatistics(since string, dryRun bool) error { - legs, err := GetLegsToRecalculate(models.TICTACTOE, since) - if err != nil { - return err - } - +// RecalculateTicTacToeStatistics will recaulcate statistics for the given Tic Tac Toe legs +func RecalculateTicTacToeStatistics(legs []int) ([]string, error) { queries := make([]string, 0) - for _, leg := range legs { - stats, err := CalculateTicTacToeStatistics(leg.ID) + for _, legID := range legs { + stats, err := CalculateTicTacToeStatistics(legID) if err != nil { - return err + return nil, err } for playerID, stat := range stats { - queries = append(queries, fmt.Sprintf(`UPDATE statistics_tic_tac_toe SET darts_thrown = %d WHERE leg_id = %d AND player_id = %d;`, - stat.DartsThrown, leg.ID, playerID)) - } - } - if dryRun { - for _, query := range queries { - log.Print(query) - } - } else { - log.Printf("Executing %d UPDATE queries", len(queries)) - tx, err := models.DB.Begin() - if err != nil { - return err - } - for _, query := range queries { - tx.Exec(query) + queries = append(queries, fmt.Sprintf(`UPDATE statistics_tic_tac_toe SET darts_thrown = %d, score = %d, numbers_closed = %d, highest_closed = %d WHERE leg_id = %d AND player_id = %d;`, + stat.DartsThrown, stat.Score, stat.NumbersClosed, stat.HighestClosed, legID, playerID)) } - tx.Commit() } - return nil + return queries, nil } diff --git a/data/statistics_x01.go b/data/statistics_x01.go index d01dcc6d..3a28ad28 100644 --- a/data/statistics_x01.go +++ b/data/statistics_x01.go @@ -3,7 +3,6 @@ package data import ( "database/sql" "fmt" - "log" "github.com/guregu/null" "github.com/jmoiron/sqlx" @@ -515,11 +514,12 @@ func GetX01HistoryForPlayer(id int, limit int, matchType int) ([]*models.Leg, er } // CalculateX01Statistics will calculate x01 statistics for the given leg -func CalculateX01Statistics(legID int, winnerID int, startingScore int) (map[int]*models.StatisticsX01, error) { +func CalculateX01Statistics(legID int) (map[int]*models.StatisticsX01, error) { visits, err := GetLegVisits(legID) if err != nil { return nil, err } + winnerID := visits[len(visits)-1].PlayerID players, err := GetPlayersScore(legID) if err != nil { @@ -533,7 +533,7 @@ func CalculateX01Statistics(legID int, winnerID int, startingScore int) (map[int statisticsMap[player.PlayerID] = stats playersMap[player.PlayerID] = player - player.CurrentScore = startingScore + player.CurrentScore = player.StartingScore if player.Handicap.Valid { player.CurrentScore += int(player.Handicap.Int64) } @@ -872,17 +872,12 @@ func GetOfficeStatisticsForOffice(officeID int, from string, to string) ([]*mode } // RecalculateX01Statistics will recalculate x01 statistics for all legs -func RecalculateX01Statistics(since string, dryRun bool) error { - legs, err := GetLegsToRecalculate(models.X01, since) - if err != nil { - return err - } - +func RecalculateX01Statistics(legs []int) ([]string, error) { queries := make([]string, 0) - for _, leg := range legs { - stats, err := CalculateX01Statistics(leg.ID, int(leg.WinnerPlayerID.Int64), leg.StartingScore) + for _, legID := range legs { + stats, err := CalculateX01Statistics(legID) if err != nil { - return err + return nil, err } for playerID, stat := range stats { query := fmt.Sprintf("UPDATE statistics_x01 SET ppd = %f, ppd_score = %d, first_nine_ppd = %f, first_nine_ppd_score = %d, checkout_attempts = %d, darts_thrown = %d, `60s_plus` = %d, `100s_plus` = %d, `140s_plus` = %d, `180s` = %d, overall_accuracy = %f", @@ -897,25 +892,9 @@ func RecalculateX01Statistics(since string, dryRun bool) error { if stat.AccuracyStatistics.Accuracy20.Valid { query += fmt.Sprintf(", accuracy_20 = %f", stat.AccuracyStatistics.Accuracy20.Float64) } - query += fmt.Sprintf(" WHERE leg_id = %d AND player_id = %d;", leg.ID, playerID) + query += fmt.Sprintf(" WHERE leg_id = %d AND player_id = %d;", legID, playerID) queries = append(queries, query) } } - - if dryRun { - for _, query := range queries { - log.Print(query) - } - } else { - log.Printf("Executing %d UPDATE queries", len(queries)) - tx, err := models.DB.Begin() - if err != nil { - return err - } - for _, query := range queries { - tx.Exec(query) - } - tx.Commit() - } - return nil + return queries, nil } diff --git a/models/match.go b/models/match.go index be5c05fe..8df659c0 100644 --- a/models/match.go +++ b/models/match.go @@ -49,6 +49,23 @@ const ( KNOCKOUT = 15 ) +var MatchTypes = map[int]string{ + X01: "X01", + SHOOTOUT: "9 Dart Shootout", + X01HANDICAP: "X01 Handicap", + CRICKET: "Cricket", + DARTSATX: "Darts At X", + AROUNDTHEWORLD: "Around the World", + SHANGHAI: "Shanghai", + AROUNDTHECLOCK: "Around the Clock", + TICTACTOE: "Tic-Tac-Toe", + BERMUDATRIANGLE: "Bermuda Triangle", + FOURTWENTY: "420", + KILLBULL: "Kill Bull", + GOTCHA: "Gotcha", + JDCPRACTICE: "JDC Practice", + KNOCKOUT: "Knockout"} + // TargetsBermudaTriangle contains the target for each round of Bermuda Triangle var TargetsBermudaTriangle = [13]Target{ {Value: 12, multipliers: []int64{1, 2, 3}}, From f6f67f7cd5b6cd5521086450e0230b83fd537a62 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Sun, 13 Mar 2022 15:06:10 +0100 Subject: [PATCH 04/67] New command for recreating elo --- cmd/aroundtheclock.go | 2 +- cmd/aroundtheworld.go | 2 +- cmd/bermudatriangle.go | 2 +- cmd/cricket.go | 2 +- cmd/dartsatx.go | 2 +- cmd/elo.go | 15 +++++ cmd/fourtwenty.go | 2 +- cmd/gotcha.go | 2 +- cmd/jdcpractice.go | 2 +- cmd/killbull.go | 2 +- cmd/knockout.go | 2 +- cmd/recalculate_elo.go | 39 ++++++++++++ ...calculate.go => recalculate_statistics.go} | 12 ++-- cmd/shanghai.go | 2 +- cmd/shootout.go | 2 +- cmd/statistics.go | 15 +++++ cmd/tictactoe.go | 2 +- cmd/x01.go | 2 +- data/player.go | 31 ---------- data/recalculate.go | 59 ++++++++++++++++++- 20 files changed, 146 insertions(+), 53 deletions(-) create mode 100644 cmd/elo.go create mode 100644 cmd/recalculate_elo.go rename cmd/{recalculate.go => recalculate_statistics.go} (57%) create mode 100644 cmd/statistics.go diff --git a/cmd/aroundtheclock.go b/cmd/aroundtheclock.go index 4ecb4ae1..7e08a389 100644 --- a/cmd/aroundtheclock.go +++ b/cmd/aroundtheclock.go @@ -19,5 +19,5 @@ var aroundtheclockCmd = &cobra.Command{ } func init() { - recalculateCmd.AddCommand(aroundtheclockCmd) + recalculateStatisticsCmd.AddCommand(aroundtheclockCmd) } diff --git a/cmd/aroundtheworld.go b/cmd/aroundtheworld.go index 8db05f9f..1b3bc69f 100644 --- a/cmd/aroundtheworld.go +++ b/cmd/aroundtheworld.go @@ -19,5 +19,5 @@ var aroundtheworldCmd = &cobra.Command{ } func init() { - recalculateCmd.AddCommand(aroundtheworldCmd) + recalculateStatisticsCmd.AddCommand(aroundtheworldCmd) } diff --git a/cmd/bermudatriangle.go b/cmd/bermudatriangle.go index 45685b85..88d6d6cd 100644 --- a/cmd/bermudatriangle.go +++ b/cmd/bermudatriangle.go @@ -19,5 +19,5 @@ var bermudatriangleCmd = &cobra.Command{ } func init() { - recalculateCmd.AddCommand(bermudatriangleCmd) + recalculateStatisticsCmd.AddCommand(bermudatriangleCmd) } diff --git a/cmd/cricket.go b/cmd/cricket.go index a02d0aec..d35b5296 100644 --- a/cmd/cricket.go +++ b/cmd/cricket.go @@ -19,5 +19,5 @@ var cricketCmd = &cobra.Command{ } func init() { - recalculateCmd.AddCommand(cricketCmd) + recalculateStatisticsCmd.AddCommand(cricketCmd) } diff --git a/cmd/dartsatx.go b/cmd/dartsatx.go index 7bc5abb3..98a8bb00 100644 --- a/cmd/dartsatx.go +++ b/cmd/dartsatx.go @@ -19,5 +19,5 @@ var dartsatxCmd = &cobra.Command{ } func init() { - recalculateCmd.AddCommand(dartsatxCmd) + recalculateStatisticsCmd.AddCommand(dartsatxCmd) } diff --git a/cmd/elo.go b/cmd/elo.go new file mode 100644 index 00000000..7cad5f1b --- /dev/null +++ b/cmd/elo.go @@ -0,0 +1,15 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +// eloCmd represents the elo command +var eloCmd = &cobra.Command{ + Use: "elo", + Short: "Modify Elo", +} + +func init() { + rootCmd.AddCommand(eloCmd) +} diff --git a/cmd/fourtwenty.go b/cmd/fourtwenty.go index 5f1c81d5..74889d7d 100644 --- a/cmd/fourtwenty.go +++ b/cmd/fourtwenty.go @@ -19,5 +19,5 @@ var fourtwentyCmd = &cobra.Command{ } func init() { - recalculateCmd.AddCommand(fourtwentyCmd) + recalculateStatisticsCmd.AddCommand(fourtwentyCmd) } diff --git a/cmd/gotcha.go b/cmd/gotcha.go index 2e5ed278..081a7581 100644 --- a/cmd/gotcha.go +++ b/cmd/gotcha.go @@ -19,5 +19,5 @@ var gotchaCmd = &cobra.Command{ } func init() { - recalculateCmd.AddCommand(gotchaCmd) + recalculateStatisticsCmd.AddCommand(gotchaCmd) } diff --git a/cmd/jdcpractice.go b/cmd/jdcpractice.go index a8fa6673..71c3b0f0 100644 --- a/cmd/jdcpractice.go +++ b/cmd/jdcpractice.go @@ -19,5 +19,5 @@ var jdcpracticeCmd = &cobra.Command{ } func init() { - recalculateCmd.AddCommand(jdcpracticeCmd) + recalculateStatisticsCmd.AddCommand(jdcpracticeCmd) } diff --git a/cmd/killbull.go b/cmd/killbull.go index 1950e1e6..525c1ec1 100644 --- a/cmd/killbull.go +++ b/cmd/killbull.go @@ -19,5 +19,5 @@ var killbullCmd = &cobra.Command{ } func init() { - recalculateCmd.AddCommand(killbullCmd) + recalculateStatisticsCmd.AddCommand(killbullCmd) } diff --git a/cmd/knockout.go b/cmd/knockout.go index 24360fbc..d0efdc52 100644 --- a/cmd/knockout.go +++ b/cmd/knockout.go @@ -19,5 +19,5 @@ var knockoutCmd = &cobra.Command{ } func init() { - recalculateCmd.AddCommand(knockoutCmd) + recalculateStatisticsCmd.AddCommand(knockoutCmd) } diff --git a/cmd/recalculate_elo.go b/cmd/recalculate_elo.go new file mode 100644 index 00000000..6703b61e --- /dev/null +++ b/cmd/recalculate_elo.go @@ -0,0 +1,39 @@ +package cmd + +import ( + "github.com/kcapp/api/data" + "github.com/kcapp/api/models" + "github.com/spf13/cobra" +) + +// recalculateEloCmd represents the recalculate command +var recalculateEloCmd = &cobra.Command{ + Use: "recalculate", + Short: "Recalculate elo", + Long: `Recalculate elo for all matches played. + + This will reset the elo for all players, and regenerate the elo changelog + Elo will be recalculated based on 'updated_at' timestamp of each match`, + Run: func(cmd *cobra.Command, args []string) { + configFileParam, err := cmd.Flags().GetString("config") + if err != nil { + panic(err) + } + config, err := models.GetConfig(configFileParam) + if err != nil { + panic(err) + } + models.InitDB(config.GetMysqlConnectionString()) + + dryRun, _ := cmd.Flags().GetBool("dry-run") + err = data.RecalculateElo(dryRun) + if err != nil { + panic(err) + } + }, +} + +func init() { + eloCmd.AddCommand(recalculateEloCmd) + recalculateEloCmd.PersistentFlags().Bool("dry-run", true, "Print queries instead of executing") +} diff --git a/cmd/recalculate.go b/cmd/recalculate_statistics.go similarity index 57% rename from cmd/recalculate.go rename to cmd/recalculate_statistics.go index ad70ee8b..31b8ced5 100644 --- a/cmd/recalculate.go +++ b/cmd/recalculate_statistics.go @@ -9,8 +9,8 @@ var since string var dryRun bool var legID int -// recalculateCmd represents the recalculate command -var recalculateCmd = &cobra.Command{ +// recalculateStatisticsCmd represents the recalculate command +var recalculateStatisticsCmd = &cobra.Command{ Use: "recalculate", Short: "Recalculate statistics", Long: `Recalculate statistics for the given match type`, @@ -32,8 +32,8 @@ var recalculateCmd = &cobra.Command{ } func init() { - rootCmd.AddCommand(recalculateCmd) - recalculateCmd.PersistentFlags().Bool("dry-run", true, "Print queries instead of executing") - recalculateCmd.PersistentFlags().StringP("since", "s", "", "Only recalculate statistics newer than the given date") - recalculateCmd.PersistentFlags().IntP("leg", "l", 0, "Recalculate statistics for the given leg id") + statisticsCmd.AddCommand(recalculateStatisticsCmd) + recalculateStatisticsCmd.PersistentFlags().Bool("dry-run", true, "Print queries instead of executing") + recalculateStatisticsCmd.PersistentFlags().StringP("since", "s", "", "Only recalculate statistics newer than the given date") + recalculateStatisticsCmd.PersistentFlags().IntP("leg", "l", 0, "Recalculate statistics for the given leg id") } diff --git a/cmd/shanghai.go b/cmd/shanghai.go index bca91b57..b9401ec3 100644 --- a/cmd/shanghai.go +++ b/cmd/shanghai.go @@ -19,5 +19,5 @@ var shanghaiCmd = &cobra.Command{ } func init() { - recalculateCmd.AddCommand(shanghaiCmd) + recalculateStatisticsCmd.AddCommand(shanghaiCmd) } diff --git a/cmd/shootout.go b/cmd/shootout.go index 8e16eb22..9e737ec0 100644 --- a/cmd/shootout.go +++ b/cmd/shootout.go @@ -19,5 +19,5 @@ var shootoutCmd = &cobra.Command{ } func init() { - recalculateCmd.AddCommand(shootoutCmd) + recalculateStatisticsCmd.AddCommand(shootoutCmd) } diff --git a/cmd/statistics.go b/cmd/statistics.go new file mode 100644 index 00000000..cf7153e9 --- /dev/null +++ b/cmd/statistics.go @@ -0,0 +1,15 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +// statisticsCmd represents the statistics command +var statisticsCmd = &cobra.Command{ + Use: "statistics", + Short: "Modify statistics", +} + +func init() { + rootCmd.AddCommand(statisticsCmd) +} diff --git a/cmd/tictactoe.go b/cmd/tictactoe.go index 986b79fc..c4844380 100644 --- a/cmd/tictactoe.go +++ b/cmd/tictactoe.go @@ -19,5 +19,5 @@ var tictactoeCmd = &cobra.Command{ } func init() { - recalculateCmd.AddCommand(tictactoeCmd) + recalculateStatisticsCmd.AddCommand(tictactoeCmd) } diff --git a/cmd/x01.go b/cmd/x01.go index 70981ae2..d6394fe7 100644 --- a/cmd/x01.go +++ b/cmd/x01.go @@ -19,5 +19,5 @@ var x01Cmd = &cobra.Command{ } func init() { - recalculateCmd.AddCommand(x01Cmd) + recalculateStatisticsCmd.AddCommand(x01Cmd) } diff --git a/data/player.go b/data/player.go index 63b68a72..df4dc8d2 100644 --- a/data/player.go +++ b/data/player.go @@ -942,7 +942,6 @@ func UpdateEloForMatch(matchID int) error { // matches which were walkovers return nil } - //log.Printf("Updating Elo for players %v in match %d", match.Players, matchID) elos, err := GetPlayersElo(match.Players...) if err != nil { @@ -1071,36 +1070,6 @@ func updateElo(matchID int, player1 *models.PlayerElo, player2 *models.PlayerElo return nil } -// RecalculateElo will recalculate Elo for all players -func RecalculateElo() error { - rows, err := models.DB.Query(`SELECT id FROM matches ORDER BY updated_at`) - if err != nil { - return err - } - defer rows.Close() - - matches := make([]int, 0) - for rows.Next() { - var id int - err := rows.Scan(&id) - if err != nil { - return err - } - matches = append(matches, id) - } - if err = rows.Err(); err != nil { - return err - } - - for _, id := range matches { - err = UpdateEloForMatch(id) - if err != nil { - return err - } - } - return nil -} - // CalculateElo will calculate the Elo for each player based on the given information. Returned value is new Elo for player1 and player2 respectively func CalculateElo(player1Elo int, player1Matches int, player1Score int, player2Elo int, player2Matches int, player2Score int) (int, int) { if player1Matches == 0 { diff --git a/data/recalculate.go b/data/recalculate.go index 9b2c74a0..e7e6af7b 100644 --- a/data/recalculate.go +++ b/data/recalculate.go @@ -14,7 +14,11 @@ func RecalculateStatistics(matchType int, legID int, since string, dryRun bool) log.Printf("Recalculating statistics for leg %d", legID) legs = append(legs, legID) } else { - log.Printf("Recalculating %s statistics since %s", models.MatchTypes[matchType], since) + s := since + if s == "" { + s = "(All Time)" + } + log.Printf("Recalculating %s statistics since=%s", models.MatchTypes[matchType], s) ids, err := GetLegsToRecalculate(matchType, since) if err != nil { return err @@ -75,10 +79,61 @@ func RecalculateStatistics(matchType int, legID int, since string, dryRun bool) return err } for _, query := range queries { - tx.Exec(query) + _, err = tx.Exec(query) + if err != nil { + tx.Rollback() + return err + } } tx.Commit() } } return nil } + +// RecalculateElo will recalculate Elo for all players +func RecalculateElo(dryRun bool) error { + rows, err := models.DB.Query(` + SELECT id FROM matches + WHERE is_finished = 1 AND is_practice = 0 AND is_abandoned = 0 AND match_type_id = 1 + ORDER BY updated_at`) + if err != nil { + return err + } + defer rows.Close() + + matches := make([]int, 0) + for rows.Next() { + var id int + err := rows.Scan(&id) + if err != nil { + return err + } + matches = append(matches, id) + } + if err = rows.Err(); err != nil { + return err + } + if dryRun { + log.Print("Elo not reset because dry-run is enabled") + } else { + log.Printf("Recalculating elo for %d matches", len(matches)) + tx, err := models.DB.Begin() + if err != nil { + return err + } + // Reset the Elo for all players back to initial values + tx.Exec(`UPDATE player_elo SET current_elo = 1500, current_elo_matches = 0, tournament_elo = 1500, tournament_elo_matches = 0;`) + tx.Exec(`DELETE FROM player_elo_changelog;`) + tx.Commit() + + for _, id := range matches { + err = UpdateEloForMatch(id) + if err != nil { + return err + } + } + } + + return nil +} From a6028ca887e1c6605b22f432b57766d8966d03c0 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Sun, 13 Mar 2022 15:43:41 +0100 Subject: [PATCH 05/67] Initial work on commands for recalculating elo --- cmd/recalculate_elo.go | 17 ++++++-- data/recalculate.go | 88 ++++++++++++++++++++++++++++++++++++++++++ data/tournament.go | 31 +++++++++++++++ 3 files changed, 132 insertions(+), 4 deletions(-) diff --git a/cmd/recalculate_elo.go b/cmd/recalculate_elo.go index 6703b61e..4dddb0ea 100644 --- a/cmd/recalculate_elo.go +++ b/cmd/recalculate_elo.go @@ -26,14 +26,23 @@ var recalculateEloCmd = &cobra.Command{ models.InitDB(config.GetMysqlConnectionString()) dryRun, _ := cmd.Flags().GetBool("dry-run") - err = data.RecalculateElo(dryRun) - if err != nil { - panic(err) + tournament, _ := cmd.Flags().GetInt("tournament") + if tournament != 0 { + err = data.CalculateEloForTournament(tournament) + if err != nil { + panic(err) + } + } else { + err = data.RecalculateElo(dryRun) + if err != nil { + panic(err) + } } }, } func init() { eloCmd.AddCommand(recalculateEloCmd) - recalculateEloCmd.PersistentFlags().Bool("dry-run", true, "Print queries instead of executing") + recalculateEloCmd.Flags().Bool("dry-run", true, "Print queries instead of executing") + recalculateEloCmd.Flags().IntP("tournament", "t", 0, "Calculate elo for the given tournament") } diff --git a/data/recalculate.go b/data/recalculate.go index e7e6af7b..6ecab2df 100644 --- a/data/recalculate.go +++ b/data/recalculate.go @@ -3,7 +3,11 @@ package data import ( "fmt" "log" + "os" + "sort" + "text/tabwriter" + "github.com/guregu/null" "github.com/kcapp/api/models" ) @@ -137,3 +141,87 @@ func RecalculateElo(dryRun bool) error { return nil } + +// CalculateEloForTournament will calculate a local elo for a given tournament +func CalculateEloForTournament(tournamentID int) error { + players, err := GetPlayers() + if err != nil { + return err + } + tournamentPlayers, err := GetTournamentPlayers(tournamentID) + if err != nil { + return err + } + + rows, err := models.DB.Query(` + SELECT id FROM matches + WHERE tournament_id = ? AND is_finished = 1 AND is_practice = 0 AND is_abandoned = 0 AND match_type_id = 1 + ORDER BY updated_at`, tournamentID) + if err != nil { + return err + } + defer rows.Close() + + matches := make([]int, 0) + for rows.Next() { + var id int + err := rows.Scan(&id) + if err != nil { + return err + } + matches = append(matches, id) + } + if err = rows.Err(); err != nil { + return err + } + + elos := make(map[int]*models.PlayerElo) + for _, player := range tournamentPlayers { + elo := new(models.PlayerElo) + elo.TournamentElo = null.IntFrom(1500) + elo.TournamentEloMatches = 0 + elo.PlayerID = player.ID + + elos[player.ID] = elo + } + + for _, matchID := range matches { + match, err := GetMatch(matchID) + if err != nil { + return err + } + wins, err := GetWinsPerPlayer(matchID) + if err != nil { + return err + } + + p1 := elos[match.Players[0]] + p2 := elos[match.Players[1]] + + // Calculate elo for winner and looser + one, two := CalculateElo(int(p1.TournamentElo.Int64), p1.TournamentEloMatches, wins[p1.PlayerID], + int(p2.TournamentElo.Int64), p2.TournamentEloMatches, wins[p2.PlayerID]) + p1.TournamentElo = null.IntFrom(int64(one)) + p2.TournamentElo = null.IntFrom(int64(two)) + p1.TournamentEloMatches++ + p2.TournamentEloMatches++ + } + + values := make([]*models.PlayerElo, 0, len(elos)) + for _, value := range elos { + values = append(values, value) + } + sort.SliceStable(values, func(i, j int) bool { + return values[i].TournamentElo.Int64 > values[j].TournamentElo.Int64 + }) + log.Printf("Calculated the following elo for tournament %d:", tournamentID) + w := tabwriter.NewWriter(os.Stdout, 1, 1, 1, ' ', 0) + fmt.Fprintln(w, "ID\tPlayer\tElo\tMatches") + for _, elo := range values { + player := players[elo.PlayerID] + fmt.Fprintf(w, "%d\t%s %s\t%d\t%d\n", player.ID, player.FirstName, player.LastName.String, elo.TournamentElo.Int64, elo.TournamentEloMatches) + } + w.Flush() + + return nil +} diff --git a/data/tournament.go b/data/tournament.go index 5326476e..e900ff1a 100644 --- a/data/tournament.go +++ b/data/tournament.go @@ -225,6 +225,37 @@ func GetTournamentMatches(id int) (map[int][]*models.Match, error) { return matches, nil } +// GetTournamentPlayers will return all players for the given tournament +func GetTournamentPlayers(id int) ([]*models.Player, error) { + rows, err := models.DB.Query(` + SELECT + p.id, p.first_name, p.last_name, p.vocal_name, p.nickname, p.slack_handle, p.color, p.profile_pic_url, p.smartcard_uid, + p.board_stream_url, p.board_stream_css, p.active, p.office_id, p.is_bot, p.created_at + FROM player p + LEFT JOIN player2tournament p2t ON p2t.player_id = p.id + WHERE p2t.tournament_id = ?`, id) + if err != nil { + return nil, err + } + defer rows.Close() + + players := make([]*models.Player, 0) + for rows.Next() { + p := new(models.Player) + err := rows.Scan(&p.ID, &p.FirstName, &p.LastName, &p.VocalName, &p.Nickname, &p.SlackHandle, &p.Color, &p.ProfilePicURL, + &p.SmartcardUID, &p.BoardStreamURL, &p.BoardStreamCSS, &p.IsActive, &p.OfficeID, &p.IsBot, &p.CreatedAt) + if err != nil { + return nil, err + } + players = append(players, p) + } + if err = rows.Err(); err != nil { + return nil, err + } + + return players, nil +} + // GetTournamentOverview will return an overview for a given tournament func GetTournamentOverview(id int) (map[int][]*models.TournamentOverview, error) { rows, err := models.DB.Query(` From 799c4cd9dacbdec57d2ea05d6ac1a356716519db Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Thu, 31 Mar 2022 21:01:41 +0200 Subject: [PATCH 06/67] Initial work on import command --- .gitignore | 3 + cmd/import.go | 202 ++++++++++++++++++++++++++++++++++++++++++++++ cmd/match.go | 27 +++++++ data/v2/office.go | 27 +++++++ 4 files changed, 259 insertions(+) create mode 100644 cmd/import.go create mode 100644 cmd/match.go create mode 100644 data/v2/office.go diff --git a/.gitignore b/.gitignore index 67febac7..fbf241dc 100644 --- a/.gitignore +++ b/.gitignore @@ -28,5 +28,8 @@ config/config*.yaml log/ logs/ +# Import/export files +*.csv + api kcapp-api diff --git a/cmd/import.go b/cmd/import.go new file mode 100644 index 00000000..c6b9ad63 --- /dev/null +++ b/cmd/import.go @@ -0,0 +1,202 @@ +package cmd + +import ( + "database/sql" + "encoding/csv" + "fmt" + "log" + "os" + "strconv" + "strings" + + "github.com/kcapp/api/data" + data_v2 "github.com/kcapp/api/data/v2" + "github.com/kcapp/api/models" + "github.com/spf13/cobra" +) + +// importCmd represents the import command +var importCmd = &cobra.Command{ + Use: "import", + Short: "Import matches from the given file", + Long: `Import all the matches from a specific file + + File should be a CSV with the following format + , office name, venue name, starting score, type, `, + Run: func(cmd *cobra.Command, args []string) { + filename, _ := cmd.Flags().GetString("filename") + fmt.Printf("import called %s", filename) + records, err := readFile(filename) + if err != nil { + panic(err) + } + + for _, record := range records { + startingScore, err := strconv.Atoi(record[3]) + if err != nil { + panic(err) + } + + homeWins, err := strconv.Atoi(record[7]) + if err != nil { + panic(err) + } + awayWins, err := strconv.Atoi(record[9]) + if err != nil { + panic(err) + } + + log.Printf("Need to create %d legs", homeWins+awayWins) + + // Check if players exist + players, err := data_v2.GetPlayers() + if err != nil { + panic(err) + } + home := findPlayerByName(record[6], players) + away := findPlayerByName(record[8], players) + log.Printf("Found player: %s (%d)", home.FirstName, home.ID) + log.Printf("Found player: %s (%d)", away.FirstName, away.ID) + + winnerId := 0 + if homeWins == awayWins { + winnerId = 0 + } else if homeWins > awayWins { + winnerId = home.ID + } else { + winnerId = away.ID + } + + // Check if office exist + offices, err := data_v2.GetOffices() + if err != nil { + panic(err) + } + office := findOfficeByName(record[1], offices) + log.Printf("Found office: %s (%d)", office.Name, office.ID) + + venues, err := data.GetVenues() + if err != nil { + panic(err) + } + venue := findVenueByName(record[2], venues) + log.Printf("Found venue: %s (%d)", venue.Name.String, venue.ID.Int64) + + // TODO Add param to create office/venue if they don't exist + + tx, err := models.DB.Begin() + if err != nil { + panic(err) + } + matchTypeID := record[4] + createdAt := record[0] + log.Printf("Creating match %s", createdAt) + res, err := tx.Exec(`INSERT INTO matches (is_finished, match_mode_id, winner_id, office_id, created_at, match_type_id, venue_id) + VALUES (?, ?, ?, ?, ?, ?, ?)`, 1, record[5], winnerId, office.ID, createdAt, matchTypeID, venue.ID) + if err != nil { + tx.Rollback() + panic(err) + } + matchID, err := res.LastInsertId() + if err != nil { + tx.Rollback() + panic(err) + } + + // Create all the legs + _, err = createLegs(tx, homeWins, matchID, createdAt, startingScore, home.ID, away.ID) + if err != nil { + panic(err) + } + legID, err := createLegs(tx, awayWins, matchID, createdAt, startingScore, away.ID, home.ID) + if err != nil { + panic(err) + } + + _, err = tx.Exec("UPDATE matches SET current_leg_id = ?, updated_at = NOW() WHERE id = ?", legID, matchID) + if err != nil { + tx.Rollback() + panic(err) + } + tx.Commit() + + log.Printf("Created match %d", matchID) + } + }, +} + +func init() { + matchCmd.AddCommand(importCmd) + importCmd.PersistentFlags().StringP("filename", "f", "", "File to import from") + importCmd.MarkPersistentFlagRequired("filename") +} + +func readFile(path string) ([][]string, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + + csvReader := csv.NewReader(f) + records, err := csvReader.ReadAll() + if err != nil { + return nil, err + } + + return records, nil +} + +func findPlayerByName(playerName string, players []*models.Player) *models.Player { + for _, player := range players { + name := strings.Trim(player.FirstName+" "+player.LastName.String, " ") + if name == playerName { + return player + } + } + return nil +} + +func findOfficeByName(officeName string, offices []*models.Office) *models.Office { + for _, office := range offices { + if office.Name == officeName { + return office + } + } + return nil +} + +func findVenueByName(venueName string, venues []*models.Venue) *models.Venue { + for _, venue := range venues { + if venue.Name.String == venueName { + return venue + } + } + return nil +} + +func createLegs(tx *sql.Tx, numToCreate int, matchID int64, createdAt string, startingScore int, winnerPlayerID int, looserPlayerID int) (*int64, error) { + var legID int64 + for i := 0; i < numToCreate; i++ { + log.Printf("Creating leg %d with winner %d and looser %d", i+1, winnerPlayerID, looserPlayerID) + res, err := tx.Exec(`INSERT INTO leg (end_time, starting_score, is_finished, current_player_id, winner_id, created_at, match_id, has_scores) VALUES + (?, ?, 1, ?, ?, ?, ?, 0)`, createdAt, startingScore, winnerPlayerID, winnerPlayerID, createdAt, matchID) + if err != nil { + tx.Rollback() + return nil, err + } + legID, err = res.LastInsertId() + if err != nil { + tx.Rollback() + return nil, err + } + + _, err = tx.Exec("INSERT INTO player2leg (player_id, leg_id, `order`, match_id) VALUES (?, ?, ?, ?), (?, ?, ?, ?)", + winnerPlayerID, legID, 1, matchID, looserPlayerID, legID, 2, matchID) + if err != nil { + tx.Rollback() + return nil, err + } + } + return &legID, nil +} diff --git a/cmd/match.go b/cmd/match.go new file mode 100644 index 00000000..bf6ddc54 --- /dev/null +++ b/cmd/match.go @@ -0,0 +1,27 @@ +package cmd + +import ( + "github.com/kcapp/api/models" + "github.com/spf13/cobra" +) + +// matchCmd represents the match command +var matchCmd = &cobra.Command{ + Use: "match", + Short: "Import/Export matches", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + configFileParam, err := cmd.Flags().GetString("config") + if err != nil { + panic(err) + } + config, err := models.GetConfig(configFileParam) + if err != nil { + panic(err) + } + models.InitDB(config.GetMysqlConnectionString()) + }, +} + +func init() { + rootCmd.AddCommand(matchCmd) +} diff --git a/data/v2/office.go b/data/v2/office.go new file mode 100644 index 00000000..ec473b50 --- /dev/null +++ b/data/v2/office.go @@ -0,0 +1,27 @@ +package data_v2 + +import "github.com/kcapp/api/models" + +// GetOffices will return all offices +func GetOffices() ([]*models.Office, error) { + rows, err := models.DB.Query("SELECT id, name, is_global, is_active FROM office") + if err != nil { + return nil, err + } + defer rows.Close() + + offices := make([]*models.Office, 0) + for rows.Next() { + office := new(models.Office) + err := rows.Scan(&office.ID, &office.Name, &office.IsGlobal, &office.IsActive) + if err != nil { + return nil, err + } + offices = append(offices, office) + } + if err = rows.Err(); err != nil { + return nil, err + } + + return offices, nil +} From bc147fe51682e9b4a519b17361edbe2a2a798e23 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Thu, 26 May 2022 13:31:08 +0200 Subject: [PATCH 07/67] Introducing a lower kfactor to Elo calculation --- cmd/root.go | 13 ++++- cmd/serve.go | 14 ++---- data/player.go | 12 +++-- kcapp-api.go | 133 ------------------------------------------------- 4 files changed, 23 insertions(+), 149 deletions(-) delete mode 100644 kcapp-api.go diff --git a/cmd/root.go b/cmd/root.go index 48d07389..4585d097 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,9 +1,11 @@ package cmd import ( + "fmt" "os" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) // rootCmd represents the base command when called without any subcommands @@ -16,8 +18,15 @@ var rootCmd = &cobra.Command{ // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { - err := rootCmd.Execute() - if err != nil { + cmd, _, err := rootCmd.Find(os.Args[1:]) + // default cmd if no cmd is given + if err == nil && cmd.Use == rootCmd.Use && cmd.Flags().Parse(os.Args[1:]) != pflag.ErrHelp { + args := append([]string{serveCmd.Use}, os.Args[1:]...) + rootCmd.SetArgs(args) + } + + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) os.Exit(1) } } diff --git a/cmd/serve.go b/cmd/serve.go index 21ba3a7f..93ca4acf 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -111,16 +111,20 @@ var serveCmd = &cobra.Command{ router.HandleFunc("/tournament", controllers.GetTournaments).Methods("GET") router.HandleFunc("/tournament/current", controllers.GetCurrentTournament).Methods("GET") router.HandleFunc("/tournament/current/{office_id}", controllers.GetCurrentTournamentForOffice).Methods("GET") + router.HandleFunc("/tournament/office/{office_id}", controllers.GetTournamentsForOffice).Methods("GET") router.HandleFunc("/tournament/groups", controllers.AddTournamentGroup).Methods("POST") router.HandleFunc("/tournament/groups", controllers.GetTournamentGroups).Methods("GET") router.HandleFunc("/tournament/standings", controllers.GetTournamentStandings).Methods("GET") router.HandleFunc("/tournament/{id}", controllers.GetTournament).Methods("GET") router.HandleFunc("/tournament/{id}/player/{player_id}", controllers.GetTournamentPlayerMatches).Methods("GET") router.HandleFunc("/tournament/{id}/matches", controllers.GetTournamentMatches).Methods("GET") + router.HandleFunc("/tournament/{id}/matches/result", controllers.GetTournamentMatchResults).Methods("GET") router.HandleFunc("/tournament/{id}/metadata", controllers.GetMatchMetadataForTournament).Methods("GET") router.HandleFunc("/tournament/{id}/overview", controllers.GetTournamentOverview).Methods("GET") router.HandleFunc("/tournament/{id}/statistics", controllers.GetTournamentStatistics).Methods("GET") router.HandleFunc("/tournament/match/{id}/next", controllers.GetNextTournamentMatch).Methods("GET") + router.HandleFunc("/tournament/{id}/probabilities", controllers.GetTournamentProbabilities).Methods("GET") + router.HandleFunc("/tournament/match/{id}/probabilities", controllers.GetMatchProbabilities).Methods("GET") log.Printf("Listening on port %d", config.APIConfig.Port) log.Println(http.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", config.APIConfig.Port), router)) @@ -129,14 +133,4 @@ var serveCmd = &cobra.Command{ func init() { rootCmd.AddCommand(serveCmd) - - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // serveCmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - // serveCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } diff --git a/data/player.go b/data/player.go index 12ae757c..dbab306b 100644 --- a/data/player.go +++ b/data/player.go @@ -1084,8 +1084,9 @@ func CalculateElo(player1Elo int, player1Matches int, player1Score int, player2E // P2 = Looser // PD = Points Difference // Multiplier = ln(abs(PD) + 1) * (2.2 / ((P1(old)-P2(old)) * 0.001 + 2.2)) - // Elo Winner = P1(old) + 800/num_matches * (1 - 1/(1 + 10 ^ (P2(old) - P1(old) / 400) ) ) - // Elo Looser = P2(old) + 800/num_matches * (0 - 1/(1 + 10 ^ (P2(old) - P1(old) / 400) ) ) + // k-factor = max(800/num_matches, k_factor_min) + // Elo Winner = P1(old) + k-factor * (1 - 1/(1 + 10 ^ (P2(old) - P1(old) / 400) ) ) + // Elo Looser = P2(old) + k-factor * (0 - 1/(1 + 10 ^ (P2(old) - P1(old) / 400) ) ) if player1Score > player2Score { multiplier := math.Log(math.Abs(float64(player1Score-player2Score))+1) * (2.2 / ((float64(player1Elo-player2Elo))*0.001 + 2.2)) @@ -1108,6 +1109,9 @@ func CalculateElo(player1Elo int, player1Matches int, player1Score int, player2E func calculateElo(winnerElo int, winnerMatches int, looserElo int, looserMatches int, multiplier float64, isDraw bool) (int, int) { constant := 800.0 + // k-factor indicates the strength of a player, which we set as "800 / matchesPlayed", + // but to avoid it going to low, we set a cap of the kFactor here, to avoid it getting to low, and elo changes not being reflected + kFactor := 20.0 Wwinner := 1.0 Wlooser := 0.0 @@ -1115,10 +1119,10 @@ func calculateElo(winnerElo int, winnerMatches int, looserElo int, looserMatches Wwinner = 0.5 Wlooser = 0.5 } - changeWinner := int((constant / float64(winnerMatches) * (Wwinner - (1 / (1 + math.Pow(10, float64(looserElo-winnerElo)/400))))) * multiplier) + changeWinner := int((math.Max(constant/float64(winnerMatches), kFactor) * (Wwinner - (1 / (1 + math.Pow(10, float64(looserElo-winnerElo)/400))))) * multiplier) calculatedWinner := winnerElo + changeWinner - changeLooser := int((constant / float64(looserMatches) * (Wlooser - (1 / (1 + math.Pow(10, float64(winnerElo-looserElo)/400))))) * multiplier) + changeLooser := int((math.Max(constant/float64(looserMatches), kFactor) * (Wlooser - (1 / (1 + math.Pow(10, float64(winnerElo-looserElo)/400))))) * multiplier) calculatedLooser := looserElo + changeLooser return calculatedWinner, calculatedLooser diff --git a/kcapp-api.go b/kcapp-api.go deleted file mode 100644 index dbf34bad..00000000 --- a/kcapp-api.go +++ /dev/null @@ -1,133 +0,0 @@ -package main - -import ( - "fmt" - "log" - "net/http" - "os" - - "github.com/gorilla/mux" - "github.com/kcapp/api/controllers" - controllers_v2 "github.com/kcapp/api/controllers/v2" - "github.com/kcapp/api/models" -) - -// our main function -func main() { - var configFileParam string - - if len(os.Args) > 1 { - configFileParam = os.Args[1] - } - - config, err := models.GetConfig(configFileParam) - if err != nil { - panic(err) - } - models.InitDB(config.GetMysqlConnectionString()) - - router := mux.NewRouter() - router.HandleFunc("/health", controllers.Healthcheck).Methods("HEAD") - - router.HandleFunc("/match", controllers.NewMatch).Methods("POST") - router.HandleFunc("/match/active", controllers.GetActiveMatches).Methods("GET") - router.HandleFunc("/match/types", controllers.GetMatchesTypes).Methods("GET") - router.HandleFunc("/match/modes", controllers.GetMatchesModes).Methods("GET") - router.HandleFunc("/match/outshot", controllers.GetOutshotTypes).Methods("GET") - router.HandleFunc("/match", controllers.GetMatches).Methods("GET") - router.HandleFunc("/match/{id}", controllers.GetMatch).Methods("GET") - router.HandleFunc("/match/{id}/metadata", controllers.GetMatchMetadata).Methods("GET") - router.HandleFunc("/match/{id}/rematch", controllers.ReMatch).Methods("POST") - router.HandleFunc("/match/{id}/statistics", controllers.GetStatisticsForMatch).Methods("GET") - router.HandleFunc("/match/{id}/legs", controllers.GetLegsForMatch).Methods("GET") - router.HandleFunc("/match/{start}/{limit}", controllers.GetMatchesLimit).Methods("GET") - - router.HandleFunc("/leg/active", controllers.GetActiveLegs).Methods("GET") - router.HandleFunc("/leg/{id}", controllers.GetLeg).Methods("GET") - router.HandleFunc("/leg/{id}", controllers.DeleteLeg).Methods("DELETE") - router.HandleFunc("/leg/{id}/statistics", controllers.GetStatisticsForLeg).Methods("GET") - router.HandleFunc("/leg/{id}/players", controllers.GetLegPlayers).Methods("GET") - router.HandleFunc("/leg/{id}/order", controllers.ChangePlayerOrder).Methods("PUT") - router.HandleFunc("/leg/{id}/warmup", controllers.StartWarmup).Methods("PUT") - router.HandleFunc("/leg/{id}/undo", controllers.UndoFinishLeg).Methods("PUT") - - router.HandleFunc("/visit", controllers.AddVisit).Methods("POST") - router.HandleFunc("/visit/{id}/modify", controllers.ModifyVisit).Methods("PUT") - router.HandleFunc("/visit/{id}", controllers.DeleteVisit).Methods("DELETE") - router.HandleFunc("/visit/{leg_id}/last", controllers.DeleteLastVisit).Methods("DELETE") - - router.HandleFunc("/player", controllers.GetPlayers).Methods("GET") - router.HandleFunc("/player/active", controllers.GetActivePlayers).Methods("GET") - router.HandleFunc("/player/compare", controllers.GetPlayersX01Statistics).Methods("GET") - router.HandleFunc("/player/{id}", controllers.GetPlayer).Methods("GET") - router.HandleFunc("/player/{id}", controllers.UpdatePlayer).Methods("PUT") - router.HandleFunc("/player/{id}/statistics", controllers.GetPlayerStatistics).Methods("GET") - router.HandleFunc("/player/{id}/statistics/previous", controllers.GetPlayerX01PreviousStatistics).Methods("GET") - router.HandleFunc("/player/{id}/progression", controllers.GetPlayerProgression).Methods("GET") - router.HandleFunc("/player/{id}/checkouts", controllers.GetPlayerCheckouts).Methods("GET") - router.HandleFunc("/player/{id}/tournament", controllers.GetPlayerTournamentStandings).Methods("GET") - router.HandleFunc("/player/{id}/elo/{start}/{limit}", controllers.GetPlayerEloChangelog).Methods("GET") - router.HandleFunc("/player/{player_1}/vs/{player_2}", controllers.GetPlayerHeadToHead).Methods("GET") - router.HandleFunc("/player/{player_1}/vs/{player_2}/simulate", controllers.SimulateMatch).Methods("PUT") - router.HandleFunc("/player", controllers.AddPlayer).Methods("POST") - router.HandleFunc("/player/{id}/calendar", controllers.GetPlayerCalendar).Methods("GET") - router.HandleFunc("/player/{id}/random/{starting_score}", controllers.GetRandomLegForPlayer).Methods("GET") - router.HandleFunc("/player/{id}/statistics/{match_type}", controllers.GetPlayerMatchTypeStatistics).Methods("GET") - router.HandleFunc("/player/{id}/statistics/{match_type}/history/{limit}", controllers.GetPlayerMatchTypeHistory).Methods("GET") - - // v2 - router.HandleFunc("/players", controllers_v2.GetPlayers).Methods("GET") - - router.HandleFunc("/preset", controllers.AddPreset).Methods("POST") - router.HandleFunc("/preset", controllers.GetPresets).Methods("GET") - router.HandleFunc("/preset/{id}", controllers.GetPreset).Methods("GET") - router.HandleFunc("/preset/{id}", controllers.UpdatePreset).Methods("PUT") - router.HandleFunc("/preset/{id}", controllers.DeletePreset).Methods("DELETE") - - router.HandleFunc("/statistics/global", controllers.GetGlobalStatistics).Methods("GET") - router.HandleFunc("/statistics/global/fnc", controllers.GetGlobalStatisticsFnc).Methods("GET") - router.HandleFunc("/statistics/office/{from}/{to}", controllers.GetOfficeStatistics).Methods("GET") - router.HandleFunc("/statistics/office/{office_id}/{from}/{to}", controllers.GetOfficeStatistics).Methods("GET") - router.HandleFunc("/statistics/{dart}/hits", controllers.GetDartStatistics).Methods("GET") - router.HandleFunc("/statistics/{match_type}/{from}/{to}", controllers.GetStatistics).Methods("GET") - - router.HandleFunc("/owe", controllers.GetOwes).Methods("GET") - router.HandleFunc("/owe/payback", controllers.RegisterPayback).Methods("PUT") - - router.HandleFunc("/owetype", controllers.GetOweTypes).Methods("GET") - - router.HandleFunc("/office", controllers.AddOffice).Methods("POST") - router.HandleFunc("/office/{id}", controllers.UpdateOffice).Methods("PUT") - router.HandleFunc("/office", controllers.GetOffices).Methods("GET") - - router.HandleFunc("/venue", controllers.AddVenue).Methods("POST") - router.HandleFunc("/venue/{id}", controllers.UpdateVenue).Methods("PUT") - router.HandleFunc("/venue", controllers.GetVenues).Methods("GET") - router.HandleFunc("/venue/{id}", controllers.GetVenue).Methods("GET") - router.HandleFunc("/venue/{id}/config", controllers.GetVenueConfiguration).Methods("GET") - router.HandleFunc("/venue/{id}/spectate", controllers.SpectateVenue).Methods("GET") - router.HandleFunc("/venue/{id}/players", controllers.GetRecentPlayers).Methods("GET") - router.HandleFunc("/venue/{id}/matches", controllers.GetActiveVenueMatches).Methods("GET") - - router.HandleFunc("/tournament", controllers.NewTournament).Methods("POST") - router.HandleFunc("/tournament", controllers.GetTournaments).Methods("GET") - router.HandleFunc("/tournament/current", controllers.GetCurrentTournament).Methods("GET") - router.HandleFunc("/tournament/current/{office_id}", controllers.GetCurrentTournamentForOffice).Methods("GET") - router.HandleFunc("/tournament/office/{office_id}", controllers.GetTournamentsForOffice).Methods("GET") - router.HandleFunc("/tournament/groups", controllers.AddTournamentGroup).Methods("POST") - router.HandleFunc("/tournament/groups", controllers.GetTournamentGroups).Methods("GET") - router.HandleFunc("/tournament/standings", controllers.GetTournamentStandings).Methods("GET") - router.HandleFunc("/tournament/{id}", controllers.GetTournament).Methods("GET") - router.HandleFunc("/tournament/{id}/player/{player_id}", controllers.GetTournamentPlayerMatches).Methods("GET") - router.HandleFunc("/tournament/{id}/matches", controllers.GetTournamentMatches).Methods("GET") - router.HandleFunc("/tournament/{id}/matches/result", controllers.GetTournamentMatchResults).Methods("GET") - router.HandleFunc("/tournament/{id}/metadata", controllers.GetMatchMetadataForTournament).Methods("GET") - router.HandleFunc("/tournament/{id}/overview", controllers.GetTournamentOverview).Methods("GET") - router.HandleFunc("/tournament/{id}/statistics", controllers.GetTournamentStatistics).Methods("GET") - router.HandleFunc("/tournament/match/{id}/next", controllers.GetNextTournamentMatch).Methods("GET") - router.HandleFunc("/tournament/{id}/probabilities", controllers.GetTournamentProbabilities).Methods("GET") - router.HandleFunc("/tournament/match/{id}/probabilities", controllers.GetMatchProbabilities).Methods("GET") - - log.Printf("Listening on port %d", config.APIConfig.Port) - log.Println(http.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", config.APIConfig.Port), router)) -} From 62759cedb82a18a0e08c9b7b818e4625e0ad95c4 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Sun, 5 Mar 2023 15:21:13 +0100 Subject: [PATCH 08/67] Initial work on tournament preset --- controllers/match_controller.go | 28 ++ controllers/tournament_controller.go | 42 +++ controllers/tournament_preset_controller.go | 89 +++++ data/match.go | 143 ++++++- data/player.go | 8 +- data/statistics_420.go | 4 +- data/statistics_around_the_clock.go | 4 +- data/statistics_around_the_world.go | 8 +- data/statistics_bermuda_triangle.go | 4 +- data/statistics_cricket.go | 4 +- data/statistics_darts_at_x.go | 4 +- data/statistics_global.go | 4 +- data/statistics_gotcha.go | 4 +- data/statistics_jdc_practice.go | 4 +- data/statistics_kill_bull.go | 4 +- data/statistics_knockout.go | 4 +- data/statistics_scam.go | 4 +- data/statistics_shootout.go | 4 +- data/statistics_tic_tac_toe.go | 4 +- data/statistics_x01.go | 8 +- data/tournament.go | 390 +++++++++++++++++++- data/tournament_preset.go | 135 +++++++ data/v2/player.go | 4 +- kcapp-api.go | 7 + models/match.go | 10 + models/tournament.go | 36 +- 26 files changed, 894 insertions(+), 66 deletions(-) create mode 100644 controllers/tournament_preset_controller.go create mode 100644 data/tournament_preset.go diff --git a/controllers/match_controller.go b/controllers/match_controller.go index 9b1faabd..940f398b 100644 --- a/controllers/match_controller.go +++ b/controllers/match_controller.go @@ -176,6 +176,34 @@ func GetMatch(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(match) } +// SetScore will set the score of a given match +func SetScore(w http.ResponseWriter, r *http.Request) { + SetHeaders(w) + params := mux.Vars(r) + id, err := strconv.Atoi(params["id"]) + if err != nil { + log.Println("Invalid id parameter") + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + var input models.MatchResult + err = json.NewDecoder(r.Body).Decode(&input) + if err != nil { + log.Println("Unable to deserialize body", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + match, err := data.SetScore(id, input) + if err != nil { + log.Println("Unable to set score for match: ", err) + http.Error(w, "Unable to set score for match", http.StatusBadRequest) + return + } + json.NewEncoder(w).Encode(match) +} + // GetMatchMetadata will return metadata for the given match func GetMatchMetadata(w http.ResponseWriter, r *http.Request) { SetHeaders(w) diff --git a/controllers/tournament_controller.go b/controllers/tournament_controller.go index 3cc1c734..827db15f 100644 --- a/controllers/tournament_controller.go +++ b/controllers/tournament_controller.go @@ -360,6 +360,48 @@ func NewTournament(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(tournament) } +// GenerateTournament will generate a new tournament +func GenerateTournament(w http.ResponseWriter, r *http.Request) { + SetHeaders(w) + var input models.Tournament + err := json.NewDecoder(r.Body).Decode(&input) + if err != nil { + log.Println("Unable to deserialize body", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + tournament, err := data.GenerateTournament(input) + if err != nil { + log.Println("Unable to create new tournament", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(tournament) +} + +// GeneratePlayoffsTournament will generate a new playoffs tournament +func GeneratePlayoffsTournament(w http.ResponseWriter, r *http.Request) { + SetHeaders(w) + params := mux.Vars(r) + id, err := strconv.Atoi(params["id"]) + if err != nil { + log.Println("Invalid id parameter") + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + tournament, err := data.GeneratePlayoffsTournament(id) + if err != nil { + log.Println("Unable to create new tournament", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(tournament) +} + // GetTournamentPlayerMatches will return all matches for the given tournament and player func GetTournamentPlayerMatches(w http.ResponseWriter, r *http.Request) { SetHeaders(w) diff --git a/controllers/tournament_preset_controller.go b/controllers/tournament_preset_controller.go new file mode 100644 index 00000000..6daee48b --- /dev/null +++ b/controllers/tournament_preset_controller.go @@ -0,0 +1,89 @@ +package controllers + +import ( + "encoding/json" + "log" + "net/http" + "strconv" + + "github.com/gorilla/mux" + "github.com/kcapp/api/data" +) + +/* +// AddTournamentPreset will create a new tournament preset +func AddTournamentPreset(w http.ResponseWriter, r *http.Request) { + var preset models.TournamentPreset + err := json.NewDecoder(r.Body).Decode(&preset) + if err != nil { + log.Println("Unable to deserialize preset json", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + err = data.AddTournamentPreset(preset) + if err != nil { + log.Println("Unable to add preset", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +}*/ + +// GetTournamentPresets will return a list of all presets +func GetTournamentPresets(w http.ResponseWriter, r *http.Request) { + SetHeaders(w) + players, err := data.GetTournamentPresets() + if err != nil { + log.Println("Unable to get presets", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + json.NewEncoder(w).Encode(players) +} + +// GetTournamentPreset will return a preset with the given ID +func GetTournamentPreset(w http.ResponseWriter, r *http.Request) { + SetHeaders(w) + params := mux.Vars(r) + id, err := strconv.Atoi(params["id"]) + if err != nil { + log.Println("Invalid id parameter") + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + preset, err := data.GetTournamentPreset(id) + if err != nil { + log.Println("Unable to get preset", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + json.NewEncoder(w).Encode(preset) +} + +/* +// UpdateTournamentPreset will update the given preset +func UpdateTournamentPreset(w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + id, err := strconv.Atoi(params["id"]) + if err != nil { + log.Println("Invalid id parameter") + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + var preset models.TournamentPreset + err = json.NewDecoder(r.Body).Decode(&preset) + if err != nil { + log.Println("Unable to deserialize preset json", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + err = data.UpdateTournamentPreset(id, preset) + if err != nil { + log.Println("Unable to update preset", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} +*/ diff --git a/data/match.go b/data/match.go index 6bd790cd..1d8ecdc5 100644 --- a/data/match.go +++ b/data/match.go @@ -100,7 +100,7 @@ func NewMatch(match models.Match) (*models.Match, error) { func GetMatches() ([]*models.Match, error) { rows, err := models.DB.Query(` SELECT - m.id, m.is_finished, m.is_abandoned, m.is_walkover, m.current_leg_id, m.winner_id, m.office_id, m.is_practice, + m.id, m.is_finished, m.is_abandoned, m.is_walkover, m.is_bye, m.current_leg_id, m.winner_id, m.office_id, m.is_practice, m.created_at, m.updated_at, m.owe_type_id, m.venue_id, mt.id, mt.name, mt.description, mm.id, mm.name, mm.short_name, mm.wins_required, mm.legs_required, ot.id, ot.item, v.id, v.name, v.description, l.updated_at as 'last_throw', GROUP_CONCAT(DISTINCT p2l.player_id ORDER BY p2l.order) AS 'players' @@ -126,7 +126,7 @@ func GetMatches() ([]*models.Match, error) { ot := new(models.OweType) venue := new(models.Venue) var players string - err := rows.Scan(&m.ID, &m.IsFinished, &m.IsAbandoned, &m.IsWalkover, &m.CurrentLegID, &m.WinnerID, &m.OfficeID, &m.IsPractice, &m.CreatedAt, &m.UpdatedAt, + err := rows.Scan(&m.ID, &m.IsFinished, &m.IsAbandoned, &m.IsWalkover, &m.IsBye, &m.CurrentLegID, &m.WinnerID, &m.OfficeID, &m.IsPractice, &m.CreatedAt, &m.UpdatedAt, &m.OweTypeID, &m.VenueID, &m.MatchType.ID, &m.MatchType.Name, &m.MatchType.Description, &m.MatchMode.ID, &m.MatchMode.Name, &m.MatchMode.ShortName, &m.MatchMode.WinsRequired, &m.MatchMode.LegsRequired, &ot.ID, &ot.Item, &venue.ID, &venue.Name, &venue.Description, &m.LastThrow, &players) @@ -164,7 +164,7 @@ func GetMatchesCount() (int, error) { func GetActiveMatches() ([]*models.Match, error) { rows, err := models.DB.Query(` SELECT - m.id, m.is_finished, m.is_abandoned, m.is_walkover, m.current_leg_id, m.winner_id, m.office_id, m.is_practice, + m.id, m.is_finished, m.is_abandoned, m.is_walkover, m.is_bye, m.current_leg_id, m.winner_id, m.office_id, m.is_practice, m.created_at, m.updated_at, m.owe_type_id, m.venue_id, mt.id, mt.name, mt.description, mm.id, mm.name, mm.short_name, mm.wins_required, mm.legs_required, ot.id, ot.item, v.id, v.name, v.description, l.updated_at as 'last_throw', GROUP_CONCAT(DISTINCT p2l.player_id ORDER BY p2l.order) AS 'players' @@ -192,7 +192,7 @@ func GetActiveMatches() ([]*models.Match, error) { ot := new(models.OweType) venue := new(models.Venue) var players string - err := rows.Scan(&m.ID, &m.IsFinished, &m.IsAbandoned, &m.IsWalkover, &m.CurrentLegID, &m.WinnerID, &m.OfficeID, &m.IsPractice, + err := rows.Scan(&m.ID, &m.IsFinished, &m.IsAbandoned, &m.IsWalkover, &m.IsBye, &m.CurrentLegID, &m.WinnerID, &m.OfficeID, &m.IsPractice, &m.CreatedAt, &m.UpdatedAt, &m.OweTypeID, &m.VenueID, &m.MatchType.ID, &m.MatchType.Name, &m.MatchType.Description, &m.MatchMode.ID, &m.MatchMode.Name, &m.MatchMode.ShortName, &m.MatchMode.WinsRequired, &m.MatchMode.LegsRequired, &ot.ID, &ot.Item, &venue.ID, &venue.Name, &venue.Description, &m.LastThrow, &players) @@ -294,7 +294,7 @@ func GetMatchProbabilities(id int) (*models.Probability, error) { func GetMatchesLimit(start int, limit int) ([]*models.Match, error) { rows, err := models.DB.Query(` SELECT - m.id, m.is_finished, m.is_abandoned, m.is_walkover, m.current_leg_id, m.winner_id, m.office_id, m.is_practice, + m.id, m.is_finished, m.is_abandoned, m.is_walkover, m.is_bye, m.current_leg_id, m.winner_id, m.office_id, m.is_practice, m.created_at, m.updated_at, m.owe_type_id, m.venue_id, mt.id, mt.name, mt.description, mm.id, mm.name, mm.short_name, mm.wins_required, mm.legs_required, ot.id, ot.item, v.id, v.name, v.description, l.updated_at as 'last_throw', GROUP_CONCAT(DISTINCT p2l.player_id ORDER BY p2l.order) AS 'players', @@ -329,7 +329,7 @@ func GetMatchesLimit(start int, limit int) ([]*models.Match, error) { tournament := new(models.MatchTournament) var players string var legsWon null.String - err := rows.Scan(&m.ID, &m.IsFinished, &m.IsAbandoned, &m.IsWalkover, &m.CurrentLegID, &m.WinnerID, &m.OfficeID, &m.IsPractice, + err := rows.Scan(&m.ID, &m.IsFinished, &m.IsAbandoned, &m.IsWalkover, &m.IsBye, &m.CurrentLegID, &m.WinnerID, &m.OfficeID, &m.IsPractice, &m.CreatedAt, &m.UpdatedAt, &m.OweTypeID, &m.VenueID, &m.MatchType.ID, &m.MatchType.Name, &m.MatchType.Description, &m.MatchMode.ID, &m.MatchMode.Name, &m.MatchMode.ShortName, &m.MatchMode.WinsRequired, &m.MatchMode.LegsRequired, &ot.ID, &ot.Item, &venue.ID, &venue.Name, &venue.Description, &m.LastThrow, &players, &m.TournamentID, &tournament.TournamentID, @@ -370,7 +370,7 @@ func GetMatch(id int) (*models.Match, error) { var players string err := models.DB.QueryRow(` SELECT - m.id, m.is_finished, m.is_abandoned, m.is_walkover, m.current_leg_id, m.winner_id, m.office_id, m.is_practice, m.created_at, m.updated_at, + m.id, m.is_finished, m.is_abandoned, m.is_walkover, m.is_bye, m.current_leg_id, m.winner_id, m.office_id, m.is_practice, m.created_at, m.updated_at, m.owe_type_id, m.venue_id, mt.id, mt.name, mt.description, mm.id, mm.name, mm.short_name, mm.wins_required, mm.legs_required, mm.tiebreak_match_type_id, ot.id, ot.item, v.id, v.name, v.description, MAX(l.updated_at) AS 'last_throw', @@ -388,7 +388,7 @@ func GetMatch(id int) (*models.Match, error) { LEFT JOIN player2tournament p2t ON p2t.tournament_id = m.tournament_id AND p2t.player_id = p2l.player_id LEFT JOIN tournament t ON t.id = p2t.tournament_id LEFT JOIN tournament_group tg ON tg.id = p2t.tournament_group_id - WHERE m.id = ?`, id).Scan(&m.ID, &m.IsFinished, &m.IsAbandoned, &m.IsWalkover, &m.CurrentLegID, &m.WinnerID, &m.OfficeID, &m.IsPractice, + WHERE m.id = ?`, id).Scan(&m.ID, &m.IsFinished, &m.IsAbandoned, &m.IsWalkover, &m.IsBye, &m.CurrentLegID, &m.WinnerID, &m.OfficeID, &m.IsPractice, &m.CreatedAt, &m.UpdatedAt, &m.OweTypeID, &m.VenueID, &m.MatchType.ID, &m.MatchType.Name, &m.MatchType.Description, &m.MatchMode.ID, &m.MatchMode.Name, &m.MatchMode.ShortName, &m.MatchMode.WinsRequired, &m.MatchMode.LegsRequired, &m.MatchMode.TieBreakMatchTypeID, &ot.ID, &ot.Item, &venue.ID, &venue.Name, &venue.Description, &m.LastThrow, &m.FirstThrow, &players, &m.TournamentID, &tournament.TournamentID, @@ -413,7 +413,7 @@ func GetMatch(id int) (*models.Match, error) { if err != nil { return nil, err } - if m.IsFinished { + if m.IsFinished && len(m.Legs) > 0 { m.EndTime = *m.Legs[len(m.Legs)-1].Endtime.Ptr() } @@ -424,6 +424,123 @@ func GetMatch(id int) (*models.Match, error) { return m, nil } +// SetScore will set the score of a given match +func SetScore(matchID int, result models.MatchResult) (*models.Match, error) { + tx, err := models.DB.Begin() + if err != nil { + return nil, err + } + + match, err := GetMatch(matchID) + if err != nil { + return nil, err + } + + _, err = tx.Exec(`DELETE FROM leg WHERE match_id = ?`, matchID) + if err != nil { + tx.Rollback() + return nil, err + } + + // TODO Improve by only inserting legs where there is no score? + + for i := 0; i < result.LooserScore; i++ { + res, err := tx.Exec(`INSERT INTO leg (end_time, starting_score, current_player_id, match_id, created_at, is_finished, winner_id, has_scores) VALUES + (NOW(), ?, ?, ?, NOW(), 1, ?, 0)`, match.Legs[0].StartingScore, result.LooserID, matchID, result.LooserID) + if err != nil { + tx.Rollback() + return nil, err + } + legID, err := res.LastInsertId() + if err != nil { + tx.Rollback() + return nil, err + } + for idx, playerID := range match.Players { + order := idx + 1 + _, err = tx.Exec("INSERT INTO player2leg (player_id, leg_id, `order`, match_id, handicap) VALUES (?, ?, ?, ?, ?)", + playerID, legID, order, matchID, match.PlayerHandicaps[playerID]) + if err != nil { + tx.Rollback() + return nil, err + } + } + } + var legID int64 + for i := 0; i < result.WinnerScore; i++ { + res, err := tx.Exec(`INSERT INTO leg (end_time, starting_score, current_player_id, match_id, created_at, is_finished, winner_id, has_scores) VALUES + (NOW(), ?, ?, ?, NOW(), 1, ?, 0)`, match.Legs[0].StartingScore, result.WinnerID, matchID, result.WinnerID) + if err != nil { + tx.Rollback() + return nil, err + } + legID, err = res.LastInsertId() + if err != nil { + tx.Rollback() + return nil, err + } + for idx, playerID := range match.Players { + order := idx + 1 + _, err = tx.Exec("INSERT INTO player2leg (player_id, leg_id, `order`, match_id, handicap) VALUES (?, ?, ?, ?, ?)", + playerID, legID, order, matchID, match.PlayerHandicaps[playerID]) + if err != nil { + tx.Rollback() + return nil, err + } + } + } + _, err = tx.Exec("UPDATE matches SET is_finished = 1, winner_id = ?, current_leg_id = ? WHERE id = ?", result.WinnerID, legID, matchID) + if err != nil { + tx.Rollback() + return nil, err + } + tx.Commit() + + // Update Elo for players if match is finished + err = UpdateEloForMatch(matchID) + if err != nil { + return nil, err + } + + if match.TournamentID.Valid { + metadata, err := GetMatchMetadata(matchID) + if err != nil { + return nil, err + } + + if metadata.WinnerOutcomeMatchID.Valid { + winnerMatch, err := GetMatch(int(metadata.WinnerOutcomeMatchID.Int64)) + if err != nil { + return nil, err + } + idx := 0 + if !metadata.IsWinnerOutcomeHome { + idx = 1 + } + err = SwapPlayers(winnerMatch.ID, result.WinnerID, winnerMatch.Players[idx]) + if err != nil { + return nil, err + } + } + if metadata.LooserOutcomeMatchID.Valid { + looserMatch, err := GetMatch(int(metadata.LooserOutcomeMatchID.Int64)) + if err != nil { + return nil, err + } + idx := 0 + if !metadata.IsLooserOutcomeHome { + idx = 1 + } + err = SwapPlayers(looserMatch.ID, result.LooserID, looserMatch.Players[idx]) + if err != nil { + return nil, err + } + } + } + + return GetMatch(int(matchID)) +} + // GetMatchMetadata returns a metadata about the given match func GetMatchMetadata(id int) (*models.MatchMetadata, error) { m := new(models.MatchMetadata) @@ -613,7 +730,7 @@ func GetWinsPerPlayer(id int) (map[int]int, error) { func GetHeadToHeadMatches(player1 int, player2 int) ([]*models.Match, error) { rows, err := models.DB.Query(` SELECT - m.id, m.is_finished, m.is_abandoned, m.is_walkover, m.current_leg_id, m.winner_id, m.created_at, m.updated_at, + m.id, m.is_finished, m.is_abandoned, m.is_walkover, m.is_bye, m.current_leg_id, m.winner_id, m.created_at, m.updated_at, m.owe_type_id, mt.id, mt.name, mt.description, mm.id, mm.name, mm.short_name, mm.wins_required, mm.legs_required FROM matches m @@ -637,7 +754,7 @@ func GetHeadToHeadMatches(player1 int, player2 int) ([]*models.Match, error) { m := new(models.Match) m.MatchType = new(models.MatchType) m.MatchMode = new(models.MatchMode) - err := rows.Scan(&m.ID, &m.IsFinished, &m.IsAbandoned, &m.IsWalkover, &m.CurrentLegID, &m.WinnerID, &m.CreatedAt, &m.UpdatedAt, + err := rows.Scan(&m.ID, &m.IsFinished, &m.IsAbandoned, &m.IsWalkover, &m.IsBye, &m.CurrentLegID, &m.WinnerID, &m.CreatedAt, &m.UpdatedAt, &m.OweTypeID, &m.MatchType.ID, &m.MatchType.Name, &m.MatchType.Description, &m.MatchMode.ID, &m.MatchMode.Name, &m.MatchMode.ShortName, &m.MatchMode.WinsRequired, &m.MatchMode.LegsRequired) if err != nil { @@ -655,7 +772,7 @@ func GetHeadToHeadMatches(player1 int, player2 int) ([]*models.Match, error) { func GetPlayerLastMatches(playerID int, limit int) ([]*models.Match, error) { rows, err := models.DB.Query(` SELECT - m.id, m.is_finished, m.is_abandoned, m.is_walkover, m.current_leg_id, m.winner_id, m.created_at, m.updated_at, + m.id, m.is_finished, m.is_abandoned, m.is_walkover, m.is_bye, m.current_leg_id, m.winner_id, m.created_at, m.updated_at, m.owe_type_id, mt.id, mt.name, mt.description, mm.id, mm.name, mm.short_name, mm.wins_required, mm.legs_required FROM matches m @@ -677,7 +794,7 @@ func GetPlayerLastMatches(playerID int, limit int) ([]*models.Match, error) { m := new(models.Match) m.MatchType = new(models.MatchType) m.MatchMode = new(models.MatchMode) - err := rows.Scan(&m.ID, &m.IsFinished, &m.IsAbandoned, &m.IsWalkover, &m.CurrentLegID, &m.WinnerID, &m.CreatedAt, &m.UpdatedAt, &m.OweTypeID, + err := rows.Scan(&m.ID, &m.IsFinished, &m.IsAbandoned, &m.IsWalkover, &m.IsBye, &m.CurrentLegID, &m.WinnerID, &m.CreatedAt, &m.UpdatedAt, &m.OweTypeID, &m.MatchType.ID, &m.MatchType.Name, &m.MatchType.Description, &m.MatchMode.ID, &m.MatchMode.Name, &m.MatchMode.ShortName, &m.MatchMode.WinsRequired, &m.MatchMode.LegsRequired) if err != nil { diff --git a/data/player.go b/data/player.go index 749b2619..d84888d8 100644 --- a/data/player.go +++ b/data/player.go @@ -678,7 +678,7 @@ func GetMatchesPlayedPerPlayer() (map[int]*models.Player, error) { FROM player2leg p2l JOIN leg l ON l.id = p2l.leg_id JOIN matches m ON m.id = p2l.match_id - WHERE l.is_finished = 1 AND m.is_abandoned = 0 + WHERE l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 GROUP BY p2l.player_id UNION ALL SELECT @@ -690,7 +690,7 @@ func GetMatchesPlayedPerPlayer() (map[int]*models.Player, error) { FROM matches m JOIN leg l ON l.match_id = m.id JOIN player2leg p2l ON p2l.player_id = m.winner_id AND p2l.match_id = m.id - WHERE l.is_finished = 1 AND m.is_abandoned = 0 + WHERE l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 GROUP BY m.winner_id ) matches GROUP BY player_id`) @@ -870,7 +870,7 @@ func GetPlayerTournamentStandings(playerID int) ([]*models.PlayerTournamentStand func GetPlayerOfficialMatches(playerID int) ([]*models.Match, error) { rows, err := models.DB.Query(` SELECT - m.id, m.is_finished, m.is_abandoned, m.is_walkover, m.current_leg_id, m.winner_id, m.office_id, m.is_practice, + m.id, m.is_finished, m.is_abandoned, m.is_walkover, m.is_bye, m.current_leg_id, m.winner_id, m.office_id, m.is_practice, m.created_at, m.updated_at, m.owe_type_id, m.venue_id, mt.id, mt.name, mt.description, mm.id, mm.name, mm.short_name, mm.wins_required, mm.legs_required, ot.id, ot.item, v.id, v.name, v.description, l.updated_at as 'last_throw', GROUP_CONCAT(DISTINCT p2l.player_id ORDER BY p2l.order) AS 'players' @@ -899,7 +899,7 @@ func GetPlayerOfficialMatches(playerID int) ([]*models.Match, error) { ot := new(models.OweType) venue := new(models.Venue) var players string - err := rows.Scan(&m.ID, &m.IsFinished, &m.IsAbandoned, &m.IsWalkover, &m.CurrentLegID, &m.WinnerID, &m.OfficeID, &m.IsPractice, &m.CreatedAt, &m.UpdatedAt, + err := rows.Scan(&m.ID, &m.IsFinished, &m.IsAbandoned, &m.IsWalkover, &m.IsBye, &m.CurrentLegID, &m.WinnerID, &m.OfficeID, &m.IsPractice, &m.CreatedAt, &m.UpdatedAt, &m.OweTypeID, &m.VenueID, &m.MatchType.ID, &m.MatchType.Name, &m.MatchType.Description, &m.MatchMode.ID, &m.MatchMode.Name, &m.MatchMode.ShortName, &m.MatchMode.WinsRequired, &m.MatchMode.LegsRequired, &ot.ID, &ot.Item, &venue.ID, &venue.Name, &venue.Description, &m.LastThrow, &players) diff --git a/data/statistics_420.go b/data/statistics_420.go index 993055e3..12d66306 100644 --- a/data/statistics_420.go +++ b/data/statistics_420.go @@ -47,7 +47,7 @@ func Get420Statistics(from string, to string) ([]*models.Statistics420, error) { LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE m.updated_at >= ? AND m.updated_at < ? - AND l.is_finished = 1 AND m.is_abandoned = 0 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 AND m.match_type_id = 11 GROUP BY p.id, m.office_id ORDER BY(COUNT(DISTINCT m2.id) / COUNT(DISTINCT m.id)) DESC, matches_played DESC`, from, to) @@ -238,7 +238,7 @@ func Get420StatisticsForPlayer(id int) (*models.Statistics420, error) { LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE s.player_id = ? - AND l.is_finished = 1 AND m.is_abandoned = 0 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 AND m.match_type_id = 11 GROUP BY p.id`, id).Scan(&s.PlayerID, &s.MatchesPlayed, &s.MatchesWon, &s.LegsPlayed, &s.LegsWon, &s.Score, &s.TotalHitRate, &h[1], &h[2], &h[3], &h[4], &h[5], &h[6], &h[7], &h[8], &h[9], &h[10], &h[11], &h[12], &h[13], diff --git a/data/statistics_around_the_clock.go b/data/statistics_around_the_clock.go index e2e598d6..cc93fa22 100644 --- a/data/statistics_around_the_clock.go +++ b/data/statistics_around_the_clock.go @@ -50,7 +50,7 @@ func GetAroundTheClockStatistics(from string, to string) ([]*models.StatisticsAr LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE m.updated_at >= ? AND m.updated_at < ? - AND l.is_finished = 1 AND m.is_abandoned = 0 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 AND m.match_type_id = 8 GROUP BY p.id, m.office_id ORDER BY(COUNT(DISTINCT m2.id) / COUNT(DISTINCT m.id)) DESC, matches_played DESC`, from, to) @@ -250,7 +250,7 @@ func GetAroundTheClockStatisticsForPlayer(id int) (*models.StatisticsAroundThe, LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE s.player_id = ? - AND l.is_finished = 1 AND m.is_abandoned = 0 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 AND m.match_type_id = 8 GROUP BY p.id`, id).Scan(&s.PlayerID, &s.MatchesPlayed, &s.MatchesWon, &s.LegsPlayed, &s.LegsWon, &s.DartsThrown, &s.Score, &s.LongestStreak, &s.TotalHitRate, diff --git a/data/statistics_around_the_world.go b/data/statistics_around_the_world.go index 72e7c95a..dac2b734 100644 --- a/data/statistics_around_the_world.go +++ b/data/statistics_around_the_world.go @@ -50,7 +50,7 @@ func GetAroundTheWorldStatistics(from string, to string) ([]*models.StatisticsAr LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE m.updated_at >= ? AND m.updated_at < ? - AND l.is_finished = 1 AND m.is_abandoned = 0 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 AND m.match_type_id = 6 GROUP BY p.id, m.office_id ORDER BY(COUNT(DISTINCT m2.id) / COUNT(DISTINCT m.id)) DESC, matches_played DESC`, from, to) @@ -249,7 +249,7 @@ func GetAroundTheWorldStatisticsForPlayer(id int) (*models.StatisticsAroundThe, LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE s.player_id = ? - AND l.is_finished = 1 AND m.is_abandoned = 0 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 AND m.match_type_id = 6 GROUP BY p.id`, id).Scan(&s.PlayerID, &s.MatchesPlayed, &s.MatchesWon, &s.LegsPlayed, &s.LegsWon, &s.DartsThrown, &s.Score, &s.MPR, &s.TotalHitRate, &h[1], &h[2], &h[3], &h[4], &h[5], &h[6], &h[7], &h[8], &h[9], &h[10], @@ -388,7 +388,7 @@ func GetShanghaiStatistics(from string, to string) ([]*models.StatisticsAroundTh LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE m.updated_at >= ? AND m.updated_at < ? - AND l.is_finished = 1 AND m.is_abandoned = 0 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 AND m.match_type_id = 7 GROUP BY p.id, m.office_id ORDER BY(COUNT(DISTINCT m2.id) / COUNT(DISTINCT m.id)) DESC, matches_played DESC`, from, to) @@ -593,7 +593,7 @@ func GetShanghaiStatisticsForPlayer(id int) (*models.StatisticsAroundThe, error) LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE s.player_id = ? - AND l.is_finished = 1 AND m.is_abandoned = 0 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 AND m.match_type_id = 7 GROUP BY p.id`, id).Scan(&s.PlayerID, &s.MatchesPlayed, &s.MatchesWon, &s.LegsPlayed, &s.LegsWon, &s.DartsThrown, &s.Score, &s.MPR, &s.TotalHitRate, &h[1], &h[2], &h[3], &h[4], &h[5], &h[6], &h[7], &h[8], &h[9], &h[10], diff --git a/data/statistics_bermuda_triangle.go b/data/statistics_bermuda_triangle.go index 0ef19d31..700c496c 100644 --- a/data/statistics_bermuda_triangle.go +++ b/data/statistics_bermuda_triangle.go @@ -43,7 +43,7 @@ func GetBermudaTriangleStatistics(from string, to string) ([]*models.StatisticsB LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE m.updated_at >= ? AND m.updated_at < ? - AND l.is_finished = 1 AND m.is_abandoned = 0 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 AND m.match_type_id = 10 GROUP BY p.id, m.office_id ORDER BY(COUNT(DISTINCT m2.id) / COUNT(DISTINCT m.id)) DESC, matches_played DESC`, from, to) @@ -219,7 +219,7 @@ func GetBermudaTriangleStatisticsForPlayer(id int) (*models.StatisticsBermudaTri LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE s.player_id = ? - AND l.is_finished = 1 AND m.is_abandoned = 0 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 AND m.match_type_id = 10 GROUP BY p.id`, id).Scan(&s.PlayerID, &s.MatchesPlayed, &s.MatchesWon, &s.LegsPlayed, &s.LegsWon, &s.DartsThrown, &s.Score, &s.MPR, &s.HighestScoreReached, &s.TotalHitRate, &h[1], &h[2], &h[3], &h[4], &h[5], &h[6], &h[7], &h[8], diff --git a/data/statistics_cricket.go b/data/statistics_cricket.go index 3e00fb79..894bb300 100644 --- a/data/statistics_cricket.go +++ b/data/statistics_cricket.go @@ -34,7 +34,7 @@ func GetCricketStatistics(from string, to string) ([]*models.StatisticsCricket, LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE m.updated_at >= ? AND m.updated_at < ? - AND l.is_finished = 1 AND m.is_abandoned = 0 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 AND m.match_type_id = 4 GROUP BY p.id, m.office_id ORDER BY(COUNT(DISTINCT m2.id) / COUNT(DISTINCT m.id)) DESC, matches_played DESC`, from, to) @@ -162,7 +162,7 @@ func GetCricketStatisticsForPlayer(id int) (*models.StatisticsCricket, error) { LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE s.player_id = ? - AND l.is_finished = 1 AND m.is_abandoned = 0 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 AND m.match_type_id = 4 GROUP BY p.id`, id).Scan(&s.PlayerID, &s.MatchesPlayed, &s.MatchesWon, &s.LegsPlayed, &s.LegsWon, &s.TotalMarks, &s.FirstNineMarks, &s.MPR, &s.FirstNineMPR, &s.Marks5, &s.Marks6, &s.Marks7, &s.Marks8, &s.Marks9) diff --git a/data/statistics_darts_at_x.go b/data/statistics_darts_at_x.go index 787cdce5..bec9979c 100644 --- a/data/statistics_darts_at_x.go +++ b/data/statistics_darts_at_x.go @@ -34,7 +34,7 @@ func GetDartsAtXStatistics(from string, to string) ([]*models.StatisticsDartsAtX LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE m.updated_at >= ? AND m.updated_at < ? - AND l.is_finished = 1 AND m.is_abandoned = 0 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 AND m.match_type_id = 5 GROUP BY p.id, m.office_id ORDER BY(COUNT(DISTINCT m2.id) / COUNT(DISTINCT m.id)) DESC, matches_played DESC, avg_score DESC`, from, to) @@ -161,7 +161,7 @@ func GetDartsAtXStatisticsForPlayer(id int) (*models.StatisticsDartsAtX, error) LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE s.player_id = ? - AND l.is_finished = 1 AND m.is_abandoned = 0 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 AND m.match_type_id = 5 GROUP BY p.id`, id).Scan(&s.PlayerID, &s.MatchesPlayed, &s.MatchesWon, &s.LegsPlayed, &s.LegsWon, &s.AvgScore, &s.Singles, &s.Doubles, &s.Triples, &s.HitRate, &s.Hits5, &s.Hits6, &s.Hits7, &s.Hits8, &s.Hits9) diff --git a/data/statistics_global.go b/data/statistics_global.go index 19f8e72b..9bac24fb 100644 --- a/data/statistics_global.go +++ b/data/statistics_global.go @@ -14,7 +14,7 @@ func GetGlobalStatistics() (map[int]*models.GlobalStatistics, error) { COUNT(DISTINCT l.id) AS 'legs', COUNT(DISTINCT s.id) AS 'visits', COUNT(first_dart) + SUM(IF(second_dart is null, 0, 1)) + SUM(IF(third_dart is null, 0, 1)) as darts, - SUM(s.first_dart * s.first_dart_multiplier + IFNULL(s.second_dart, 0) * s.second_dart_multiplier + IFNULL(s.third_dart, 0) * s.third_dart_multiplier) - SUM(IF(s.is_bust = 1, s.first_dart * s.first_dart_multiplier + IFNULL(s.second_dart, 0) * s.second_dart_multiplier + IFNULL(s.third_dart, 0) * s.third_dart_multiplier, 0)) as 'points', + IFNULL(SUM(s.first_dart * s.first_dart_multiplier + IFNULL(s.second_dart, 0) * s.second_dart_multiplier + IFNULL(s.third_dart, 0) * s.third_dart_multiplier) - SUM(IF(s.is_bust = 1, s.first_dart * s.first_dart_multiplier + IFNULL(s.second_dart, 0) * s.second_dart_multiplier + IFNULL(s.third_dart, 0) * s.third_dart_multiplier, 0)), 0) as 'points', SUM(IF(s.is_bust = 1, s.first_dart * s.first_dart_multiplier + IFNULL(s.second_dart, 0) * s.second_dart_multiplier + IFNULL(s.third_dart, 0) * s.third_dart_multiplier, 0)) as 'points_busted', SUM(IF(s.is_bust, 0, IF(first_dart = 20 AND first_dart_multiplier = 3 AND second_dart = 20 AND second_dart_multiplier = 3 AND third_dart = 20 AND third_dart_multiplier = 3, 1, 0))) as '180s', SUM(IF(s.is_bust, 0, IF((first_dart = 25 AND first_dart_multiplier = 2) OR (second_dart = 25 AND second_dart_multiplier = 2) OR (third_dart = 25 AND third_dart_multiplier = 2), 1, 0))) as 'bullseyes' @@ -22,7 +22,7 @@ func GetGlobalStatistics() (map[int]*models.GlobalStatistics, error) { LEFT JOIN leg l on l.match_id = m.id LEFT JOIN score s on s.leg_id = l.id LEFT JOIN player p on p.id = s.player_id - WHERE m.is_finished = 1 AND m.is_abandoned = 0 AND p.is_bot = 0 + WHERE m.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 AND (p.id is null OR p.is_bot = 0) GROUP BY m.office_id`) if err != nil { return nil, err diff --git a/data/statistics_gotcha.go b/data/statistics_gotcha.go index 0bd83981..fd23bfc4 100644 --- a/data/statistics_gotcha.go +++ b/data/statistics_gotcha.go @@ -29,7 +29,7 @@ func GetGotchaStatistics(from string, to string) ([]*models.StatisticsGotcha, er LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE m.updated_at >= ? AND m.updated_at < ? - AND l.is_finished = 1 AND m.is_abandoned = 0 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 AND m.match_type_id = 13 GROUP BY p.id, m.office_id ORDER BY(COUNT(DISTINCT m2.id) / COUNT(DISTINCT m.id)) DESC, matches_played DESC`, from, to) @@ -141,7 +141,7 @@ func GetGotchaStatisticsForPlayer(id int) (*models.StatisticsGotcha, error) { LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE s.player_id = ? - AND l.is_finished = 1 AND m.is_abandoned = 0 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 AND m.match_type_id = 13 GROUP BY p.id`, id).Scan(&s.PlayerID, &s.MatchesPlayed, &s.MatchesWon, &s.LegsPlayed, &s.LegsWon, &s.DartsThrown, &s.HighestScore, &s.TimesReset, &s.OthersReset, &s.Score) diff --git a/data/statistics_jdc_practice.go b/data/statistics_jdc_practice.go index 323440c9..51b1fc4e 100644 --- a/data/statistics_jdc_practice.go +++ b/data/statistics_jdc_practice.go @@ -30,7 +30,7 @@ func GetJDCPracticeStatistics(from string, to string) ([]*models.StatisticsJDCPr LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE m.updated_at >= ? AND m.updated_at < ? - AND l.is_finished = 1 AND m.is_abandoned = 0 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 AND m.match_type_id = 14 GROUP BY p.id, m.office_id ORDER BY(COUNT(DISTINCT m2.id) / COUNT(DISTINCT m.id)) DESC, matches_played DESC`, from, to) @@ -143,7 +143,7 @@ func GetJDCPracticeStatisticsForPlayer(id int) (*models.StatisticsJDCPractice, e LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE s.player_id = ? - AND l.is_finished = 1 AND m.is_abandoned = 0 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 AND m.match_type_id = 14 GROUP BY p.id`, id).Scan(&s.PlayerID, &s.MatchesPlayed, &s.MatchesWon, &s.LegsPlayed, &s.LegsWon, &s.DartsThrown, &s.Score, &s.MPR, &s.ShanghaiCount, &s.DoublesHitrate, &s.HighestScore) diff --git a/data/statistics_kill_bull.go b/data/statistics_kill_bull.go index 12141539..a29f651e 100644 --- a/data/statistics_kill_bull.go +++ b/data/statistics_kill_bull.go @@ -33,7 +33,7 @@ func GetKillBullStatistics(from string, to string) ([]*models.StatisticsKillBull LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE m.updated_at >= ? AND m.updated_at < ? - AND l.is_finished = 1 AND m.is_abandoned = 0 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 AND m.match_type_id = 12 GROUP BY p.id, m.office_id ORDER BY(COUNT(DISTINCT m2.id) / COUNT(DISTINCT m.id)) DESC, matches_played DESC`, from, to) @@ -156,7 +156,7 @@ func GetKillBullStatisticsForPlayer(id int) (*models.StatisticsKillBull, error) LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE s.player_id = ? - AND l.is_finished = 1 AND m.is_abandoned = 0 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 AND m.match_type_id = 12 GROUP BY p.id`, id).Scan(&s.PlayerID, &s.MatchesPlayed, &s.MatchesWon, &s.LegsPlayed, &s.LegsWon, &s.DartsThrown, &s.Score, &s.Marks3, &s.Marks4, &s.Marks5, &s.Marks6, &s.LongestStreak, &s.TimesBusted, &s.TotalHitRate) diff --git a/data/statistics_knockout.go b/data/statistics_knockout.go index 24f245d1..acf6d20f 100644 --- a/data/statistics_knockout.go +++ b/data/statistics_knockout.go @@ -29,7 +29,7 @@ func GetKnockoutStatistics(from string, to string) ([]*models.StatisticsKnockout LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE m.updated_at >= ? AND m.updated_at < ? - AND l.is_finished = 1 AND m.is_abandoned = 0 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 AND m.match_type_id = 15 GROUP BY p.id, m.office_id ORDER BY(COUNT(DISTINCT m2.id) / COUNT(DISTINCT m.id)) DESC, matches_played DESC`, from, to) @@ -141,7 +141,7 @@ func GetKnockoutStatisticsForPlayer(id int) (*models.StatisticsKnockout, error) LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE s.player_id = ? - AND l.is_finished = 1 AND m.is_abandoned = 0 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 AND m.match_type_id = 15 GROUP BY p.id`, id).Scan(&s.PlayerID, &s.MatchesPlayed, &s.MatchesWon, &s.LegsPlayed, &s.LegsWon, &s.DartsThrown, &s.AvgScore, &s.LivesLost, &s.LivesTaken, &s.FinalPosition) diff --git a/data/statistics_scam.go b/data/statistics_scam.go index d1bfb1dd..12b33842 100644 --- a/data/statistics_scam.go +++ b/data/statistics_scam.go @@ -30,7 +30,7 @@ func GetScamStatistics(from string, to string) ([]*models.StatisticsScam, error) LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE m.updated_at >= ? AND m.updated_at < ? - AND l.is_finished = 1 AND m.is_abandoned = 0 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 AND m.match_type_id = 16 GROUP BY p.id, m.office_id ORDER BY(COUNT(DISTINCT m2.id) / COUNT(DISTINCT m.id)) DESC, matches_played DESC`, from, to) @@ -145,7 +145,7 @@ func GetScamStatisticsForPlayer(id int) (*models.StatisticsScam, error) { LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE s.player_id = ? - AND l.is_finished = 1 AND m.is_abandoned = 0 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 AND m.match_type_id = 16 GROUP BY p.id`, id).Scan(&s.PlayerID, &s.MatchesPlayed, &s.MatchesWon, &s.LegsPlayed, &s.LegsWon, &s.DartsThrownScorer, &s.DartsThrownStopper, &s.Score, &s.MPR, &s.PPD, &s.ThreeDartAvg) diff --git a/data/statistics_shootout.go b/data/statistics_shootout.go index 31c0b007..a4d654d8 100644 --- a/data/statistics_shootout.go +++ b/data/statistics_shootout.go @@ -30,7 +30,7 @@ func GetShootoutStatistics(from string, to string) ([]*models.StatisticsShootout LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE m.updated_at >= ? AND m.updated_at < ? - AND m.is_finished = 1 AND m.is_abandoned = 0 + AND m.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 AND m.match_type_id = 2 GROUP BY p.id, m.office_id ORDER BY(COUNT(DISTINCT m2.id) / COUNT(DISTINCT m.id)) DESC, matches_played DESC, ppd DESC`, from, to) @@ -146,7 +146,7 @@ func GetShootoutStatisticsForPlayer(id int) (*models.StatisticsShootout, error) LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE s.player_id = ? - AND l.is_finished = 1 AND m.is_abandoned = 0 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 AND m.match_type_id = 2 GROUP BY p.id`, id).Scan(&s.PlayerID, &s.MatchesPlayed, &s.MatchesWon, &s.LegsPlayed, &s.LegsWon, &s.Score, &s.PPD, &s.Score60sPlus, &s.Score100sPlus, &s.Score140sPlus, &s.Score180s, &s.HighestScore) if err != nil { diff --git a/data/statistics_tic_tac_toe.go b/data/statistics_tic_tac_toe.go index 10df435c..6e92f791 100644 --- a/data/statistics_tic_tac_toe.go +++ b/data/statistics_tic_tac_toe.go @@ -27,7 +27,7 @@ func GetTicTacToeStatistics(from string, to string) ([]*models.StatisticsTicTacT LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE m.updated_at >= ? AND m.updated_at < ? - AND l.is_finished = 1 AND m.is_abandoned = 0 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 AND m.match_type_id = 9 GROUP BY p.id, m.office_id ORDER BY(COUNT(DISTINCT m2.id) / COUNT(DISTINCT m.id)) DESC, matches_played DESC`, from, to) @@ -136,7 +136,7 @@ func GetTicTacToeStatisticsForPlayer(id int) (*models.StatisticsTicTacToe, error LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE s.player_id = ? - AND l.is_finished = 1 AND m.is_abandoned = 0 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 AND m.match_type_id = 9 GROUP BY p.id`, id).Scan(&s.PlayerID, &s.MatchesPlayed, &s.MatchesWon, &s.LegsPlayed, &s.LegsWon, &s.Score, &s.DartsThrown, &s.NumbersClosed, &s.HighestClosed) if err != nil { diff --git a/data/statistics_x01.go b/data/statistics_x01.go index 634e3bba..65d334a1 100644 --- a/data/statistics_x01.go +++ b/data/statistics_x01.go @@ -40,7 +40,7 @@ func GetX01Statistics(from string, to string, matchType int, startingScores ...i LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE m.updated_at >= ? AND m.updated_at < ? AND l.starting_score IN (?) - AND l.is_finished = 1 AND m.is_abandoned = 0 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 AND m.match_type_id = ? GROUP BY p.id, m.office_id ORDER BY(COUNT(DISTINCT m2.id) / COUNT(DISTINCT m.id)) DESC, matches_played DESC, @@ -227,7 +227,7 @@ func GetPlayersX01Statistics(ids []int, startingScores ...int) ([]*models.Statis LEFT JOIN matches m2 ON m2.id = l2.match_id AND l2.winner_id = p.id WHERE s.player_id IN (?) AND l.starting_score IN (?) - AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_practice = 0 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_practice = 0 AND m.is_walkover = 0 AND IFNULL(l.leg_type_id, m.match_type_id) = 1 GROUP BY s.player_id ORDER BY p.id`, ids, startingScores) @@ -314,7 +314,7 @@ func GetPlayersX01PreviousStatistics(ids []int, startingScores ...int) ([]*model LEFT JOIN matches m2 ON m2.id = l2.match_id AND l2.winner_id = p.id WHERE s.player_id IN (?) AND l.starting_score IN (?) - AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_practice = 0 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_practice = 0 AND m.is_walkover = 0 AND m.match_type_id = 1 -- Exclude all matches played this week AND m.updated_at < (CURRENT_DATE - INTERVAL WEEKDAY(CURRENT_DATE) DAY) @@ -441,7 +441,7 @@ func GetX01StatisticsForPlayer(id int, matchType int) (*models.StatisticsX01, er LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE s.player_id = ? - AND l.is_finished = 1 AND m.is_abandoned = 0 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 AND m.match_type_id = ? GROUP BY p.id`, id, matchType).Scan(&s.PlayerID, &s.MatchesPlayed, &s.MatchesWon, &s.LegsPlayed, &s.LegsWon, &s.PPD, &s.FirstNinePPD, &s.ThreeDartAvg, &s.FirstNineThreeDartAvg, &s.Score60sPlus, &s.Score100sPlus, &s.Score140sPlus, &s.Score180s, &s.Accuracy20, &s.Accuracy19, diff --git a/data/tournament.go b/data/tournament.go index d272dc79..626e9f07 100644 --- a/data/tournament.go +++ b/data/tournament.go @@ -4,6 +4,7 @@ import ( "database/sql" "log" "math" + "time" "github.com/guregu/null" "github.com/kcapp/api/models" @@ -92,9 +93,9 @@ func GetTournament(id int) (*models.Tournament, error) { tournament := new(models.Tournament) err := models.DB.QueryRow(` SELECT - id, name, short_name, is_finished, is_playoffs, playoffs_tournament_id, office_id, start_time, end_time + id, name, short_name, is_finished, is_playoffs, playoffs_tournament_id, preset_id, office_id, start_time, end_time FROM tournament t WHERE t.id = ?`, id).Scan(&tournament.ID, &tournament.Name, &tournament.ShortName, &tournament.IsFinished, &tournament.IsPlayoffs, - &tournament.PlayoffsTournamentID, &tournament.OfficeID, &tournament.StartTime, &tournament.EndTime) + &tournament.PlayoffsTournamentID, &tournament.PresetID, &tournament.OfficeID, &tournament.StartTime, &tournament.EndTime) if err != nil { return nil, err } @@ -132,7 +133,13 @@ func GetTournament(id int) (*models.Tournament, error) { } tournament.Standings = standings } - + if tournament.PresetID.Valid { + preset, err := GetTournamentPreset(int(tournament.PresetID.Int64)) + if err != nil { + return nil, err + } + tournament.Preset = preset + } return tournament, nil } @@ -197,7 +204,7 @@ func GetTournamentsForOffice(officeID int) ([]*models.Tournament, error) { func GetTournamentMatches(id int) (map[int][]*models.Match, error) { rows, err := models.DB.Query(` SELECT - m.id, m.is_finished, m.current_leg_id, m.winner_id, m.is_walkover, IF(TIMEDIFF(MAX(l.updated_at), NOW() - INTERVAL 15 MINUTE) > 0, 1, 0) AS 'is_started', + m.id, m.is_finished, m.current_leg_id, m.winner_id, m.is_walkover, m.is_bye, IF(TIMEDIFF(MAX(l.updated_at), NOW() - INTERVAL 15 MINUTE) > 0, 1, 0) AS 'is_started', m.created_at, m.updated_at, m.owe_type_id, m.venue_id, mt.id, mt.name, mt.description, mm.id, mm.name, mm.short_name, mm.wins_required, mm.legs_required, v.id, v.name, v.description, l.updated_at as 'last_throw', GROUP_CONCAT(DISTINCT p2l.player_id ORDER BY p2l.order) AS 'players', @@ -231,7 +238,7 @@ func GetTournamentMatches(id int) (map[int][]*models.Match, error) { var players string var legsWon null.String var ot null.String - err := rows.Scan(&m.ID, &m.IsFinished, &m.CurrentLegID, &m.WinnerID, &m.IsWalkover, &m.IsStarted, &m.CreatedAt, &m.UpdatedAt, + err := rows.Scan(&m.ID, &m.IsFinished, &m.CurrentLegID, &m.WinnerID, &m.IsWalkover, &m.IsBye, &m.IsStarted, &m.CreatedAt, &m.UpdatedAt, &m.OweTypeID, &m.VenueID, &m.MatchType.ID, &m.MatchType.Name, &m.MatchType.Description, &m.MatchMode.ID, &m.MatchMode.Name, &m.MatchMode.ShortName, &m.MatchMode.WinsRequired, &m.MatchMode.LegsRequired, &venue.ID, &venue.Name, &venue.Description, &m.LastThrow, &players, &m.TournamentID, &groupID, &legsWon, &ot) @@ -350,9 +357,9 @@ func GetTournamentOverview(id int) (map[int][]*models.TournamentOverview, error) (COUNT(DISTINCT legs_for.id) - COUNT(DISTINCT legs_against.id)) AS 'diff', COUNT(DISTINCT won.id) * 2 + COUNT(DISTINCT draw.id) AS 'pts', IFNULL(SUM(s.ppd_score) / SUM(s.darts_thrown), -1) AS 'ppd', - IFNULL(SUM(s.first_nine_ppd_score) / (9 * (COUNT(DISTINCT legs_for.id) + COUNT(DISTINCT legs_against.id))), -1) AS 'first_nine_ppd', + IFNULL(SUM(s.first_nine_ppd_score) / (9 * (COUNT(DISTINCT s.leg_id))), -1) AS 'first_nine_ppd', IFNULL(SUM(s.ppd_score) / SUM(s.darts_thrown) * 3, -1) AS 'three_dart_avg', - IFNULL(SUM(s.first_nine_ppd_score) * 3 / (9 * (COUNT(DISTINCT legs_for.id) + COUNT(DISTINCT legs_against.id))), -1) AS 'first_nine_three_dart_avg', + IFNULL(SUM(s.first_nine_ppd_score) * 3 / (9 * (COUNT(DISTINCT s.leg_id))), -1) AS 'first_nine_three_dart_avg', IFNULL(SUM(60s_plus), 0) AS '60s_plus', IFNULL(SUM(100s_plus), 0) AS '100s_plus', IFNULL(SUM(140s_plus), 0) AS '140s_plus', @@ -462,7 +469,7 @@ func GetNextTournamentMatch(matchID int) (*models.Match, error) { return GetMatch(int(nextMatchID.Int64)) } -// GetTournamentStandings will return statistics for the given tournament +// GetTournamentStandings will return elo standings for all players func GetTournamentStandings() ([]*models.TournamentStanding, error) { rows, err := models.DB.Query(` SELECT player_id, first_name, tournament_elo, tournament_elo_matches, current_elo, current_elo_matches, @@ -720,8 +727,8 @@ func NewTournament(tournament models.Tournament) (*models.Tournament, error) { } res, err := tx.Exec(` - INSERT INTO tournament (name, short_name, is_finished, is_playoffs, playoffs_tournament_id, office_id, start_time, end_time) VALUES - (?, ?, ?, ?, ?, ?, ?, ?)`, tournament.Name, tournament.ShortName, 0, tournament.IsPlayoffs, tournament.PlayoffsTournamentID, + INSERT INTO tournament (name, short_name, is_finished, is_playoffs, playoffs_tournament_id, preset_id, office_id, start_time, end_time) VALUES + (?, ?, ?, ?, ?, ?, ?, ?, ?)`, tournament.Name, tournament.ShortName, 0, tournament.IsPlayoffs, tournament.PresetID, tournament.PlayoffsTournamentID, tournament.OfficeID, tournament.StartTime, tournament.EndTime) if err != nil { tx.Rollback() @@ -749,11 +756,362 @@ func NewTournament(tournament models.Tournament) (*models.Tournament, error) { return GetTournament(int(tournamentID)) } +// GenerateTournament will generate a new tournament +func GenerateTournament(input models.Tournament) (*models.Tournament, error) { + officeID := input.OfficeID + tournament, err := NewTournament(models.Tournament{ + Name: input.Name, + ShortName: input.ShortName, + IsPlayoffs: false, + OfficeID: officeID, + Players: input.Players, + StartTime: null.TimeFrom(time.Now()), + EndTime: null.TimeFrom(time.Now()), + }) + if err != nil { + return nil, err + } + + players := input.Players + mt := models.MatchType{ID: 1} + mo := models.MatchMode{ID: 2} + for i := 0; i < len(players); i++ { + for j := i + 1; j < len(players); j++ { + if players[i].TournamentGroupID != players[j].TournamentGroupID { + // Only generate matches for people in the same group + continue + } + match, err := NewMatch(models.Match{ + MatchType: &mt, + MatchMode: &mo, + //VenueID: 1, + OfficeID: null.IntFrom(int64(officeID)), + IsPractice: false, + TournamentID: null.IntFrom(int64(tournament.ID)), + Players: []int{players[i].PlayerID, players[j].PlayerID}, + Legs: []*models.Leg{{StartingScore: 501}}, + }) + if err != nil { + return nil, err + } + log.Printf("Generated Match %d for %d vs %d", match.ID, players[i].PlayerID, players[j].PlayerID) + } + } + + return tournament, nil +} + +// GeneratePlayoffsTournament will generate playoffs matches for the given tournament +func GeneratePlayoffsTournament(tournamentID int) (*models.Tournament, error) { + tournament, err := GetTournament(tournamentID) + if err != nil { + return nil, err + } + + overview, err := GetTournamentOverview(tournamentID) + if err != nil { + return nil, err + } + + keys := make([]int, 0) + for key := range overview { + keys = append(keys, key) + } + + group1 := overview[keys[0]] + group2 := overview[keys[1]] + + // TODO Make this configurable + preset := tournament.Preset + playoffsGroupID := preset.PlayoffsTournamentGroup.ID + mt := preset.MatchType + walkoverPlayerID := preset.PlayerIDWalkover + placeholderHomeID := preset.PlayerIDPlaceholderHome + placeholderAwayID := preset.PlayerIDPlaceholderAway + startingScore := preset.StartingScore + + // Generate Player to tournament + players := make([]*models.Player2Tournament, 0) + for i := 0; i < len(group1); i++ { + player := group1[i] + players = append(players, &models.Player2Tournament{ + PlayerID: player.PlayerID, + TournamentGroupID: playoffsGroupID, + }) + } + for i := 0; i < len(group2); i++ { + player := group2[i] + players = append(players, &models.Player2Tournament{ + PlayerID: player.PlayerID, + TournamentGroupID: playoffsGroupID, + }) + } + players = append(players, &models.Player2Tournament{ + PlayerID: walkoverPlayerID, + TournamentGroupID: playoffsGroupID, + }) + players = append(players, &models.Player2Tournament{ + PlayerID: placeholderHomeID, + TournamentGroupID: playoffsGroupID, + }) + players = append(players, &models.Player2Tournament{ + PlayerID: placeholderAwayID, + TournamentGroupID: playoffsGroupID, + }) + + playoffs, err := NewTournament(models.Tournament{ + Name: tournament.Name + " Playoffs", + ShortName: tournament.ShortName + "P", + IsPlayoffs: true, + OfficeID: tournament.OfficeID, + Players: players, + StartTime: null.TimeFrom(time.Now()), + EndTime: null.TimeFrom(time.Now()), + }) + if err != nil { + return nil, err + } + // Update Playoffs Tournament ID + _, err = models.DB.Exec(`UPDATE tournament SET playoffs_tournament_id = ? WHERE id = ?`, playoffs.ID, tournament.ID) + if err != nil { + return nil, err + } + + // Create initial 8 matches + for i := 0; i < 8; i++ { + temp := models.TournamentTemplateLast16[i] + home := getCompetitor(group1, temp.Home) + if home == -1 { + // Walkover, so use placeholder + home = walkoverPlayerID + } + away := getCompetitor(group2, temp.Away) + if away == -1 { + // Walkover, so use placeholder + away = walkoverPlayerID + } + // TODO set is_bye + match, err := NewMatch(models.Match{ + MatchType: mt, + MatchMode: preset.MatchModeLast16, + //VenueID: 1, + OfficeID: null.IntFrom(int64(tournament.OfficeID)), + IsPractice: false, + TournamentID: null.IntFrom(int64(playoffs.ID)), + Players: []int{home, away}, + Legs: []*models.Leg{{StartingScore: startingScore}}, + }) + if err != nil { + return nil, err + } + log.Printf("Generated Match %d for %d vs %d", match.ID, home, away) + } + + // Create Quarter Final Matches + for i := 0; i < 4; i++ { + match, err := NewMatch(models.Match{ + MatchType: mt, + MatchMode: preset.MatchModeQuarterFinal, + //VenueID: 1, + OfficeID: null.IntFrom(int64(tournament.OfficeID)), + IsPractice: false, + TournamentID: null.IntFrom(int64(playoffs.ID)), + Players: []int{placeholderHomeID, placeholderAwayID}, + Legs: []*models.Leg{{StartingScore: startingScore}}, + }) + if err != nil { + return nil, err + } + log.Printf("Generated Match %d for %d vs %d", match.ID, placeholderHomeID, placeholderAwayID) + } + + // Create Semi Final Matches + for i := 0; i < 2; i++ { + match, err := NewMatch(models.Match{ + MatchType: mt, + MatchMode: preset.MatchModeSemiFinal, + //VenueID: 1, + OfficeID: null.IntFrom(int64(tournament.OfficeID)), + IsPractice: false, + TournamentID: null.IntFrom(int64(playoffs.ID)), + Players: []int{placeholderHomeID, placeholderAwayID}, + Legs: []*models.Leg{{StartingScore: startingScore}}, + }) + if err != nil { + return nil, err + } + log.Printf("Generated Match %d for %d vs %d", match.ID, placeholderHomeID, placeholderAwayID) + } + + // Create Grand Final + match, err := NewMatch(models.Match{ + MatchType: mt, + MatchMode: preset.MatchModeGrandFinal, + //VenueID: 1, + OfficeID: null.IntFrom(int64(tournament.OfficeID)), + IsPractice: false, + TournamentID: null.IntFrom(int64(playoffs.ID)), + Players: []int{placeholderHomeID, placeholderAwayID}, + Legs: []*models.Leg{{StartingScore: startingScore}}, + }) + if err != nil { + return nil, err + } + log.Printf("Generated Match %d for %d vs %d", match.ID, placeholderHomeID, placeholderAwayID) + + // Insert Match Metadata for each match created + matches, err := GetTournamentMatches(playoffs.ID) + if err != nil { + return nil, err + } + gf := matches[playoffsGroupID][0] + sf1 := matches[playoffsGroupID][1] + sf2 := matches[playoffsGroupID][2] + qf1 := matches[playoffsGroupID][3] + qf2 := matches[playoffsGroupID][4] + qf3 := matches[playoffsGroupID][5] + qf4 := matches[playoffsGroupID][6] + m1 := matches[playoffsGroupID][14] + m2 := matches[playoffsGroupID][13] + m3 := matches[playoffsGroupID][12] + m4 := matches[playoffsGroupID][11] + m5 := matches[playoffsGroupID][10] + m6 := matches[playoffsGroupID][9] + m7 := matches[playoffsGroupID][8] + m8 := matches[playoffsGroupID][7] + + tx, err := models.DB.Begin() + if err != nil { + return nil, err + } + stmt, err := tx.Prepare(`INSERT INTO match_metadata (match_id, order_of_play, tournament_group_id, match_displayname, elimination, trophy, semi_final, grand_final, winner_outcome_match_id, is_winner_outcome_home) VALUES (?, ?, ?, ?, 1, 0, 0, 1, ?, ?)`) + if err != nil { + tx.Rollback() + return nil, err + } + _, err = stmt.Exec(gf.ID, 15, playoffsGroupID, "Grand Final", nil, 0) + if err != nil { + tx.Rollback() + return nil, err + } + _, err = stmt.Exec(sf1.ID, 14, playoffsGroupID, "Semi Final 1", gf.ID, 1) + if err != nil { + tx.Rollback() + return nil, err + } + _, err = stmt.Exec(sf2.ID, 13, playoffsGroupID, "Semi Final 2", gf.ID, 0) + if err != nil { + tx.Rollback() + return nil, err + } + _, err = stmt.Exec(qf1.ID, 12, playoffsGroupID, "Quarter Final 1", sf1.ID, 1) + if err != nil { + tx.Rollback() + return nil, err + } + _, err = stmt.Exec(qf2.ID, 11, playoffsGroupID, "Quarter Final 2", sf1.ID, 0) + if err != nil { + tx.Rollback() + return nil, err + } + _, err = stmt.Exec(qf3.ID, 10, playoffsGroupID, "Quarter Final 3", sf2.ID, 1) + if err != nil { + tx.Rollback() + return nil, err + } + _, err = stmt.Exec(qf4.ID, 9, playoffsGroupID, "Quarter Final 4", sf2.ID, 0) + if err != nil { + tx.Rollback() + return nil, err + } + _, err = stmt.Exec(m1.ID, 1, playoffsGroupID, "Match 1", qf1.ID, 1) + if err != nil { + tx.Rollback() + return nil, err + } + _, err = stmt.Exec(m2.ID, 2, playoffsGroupID, "Match 2", qf1.ID, 0) + if err != nil { + tx.Rollback() + return nil, err + } + _, err = stmt.Exec(m3.ID, 3, playoffsGroupID, "Match 3", qf2.ID, 1) + if err != nil { + tx.Rollback() + return nil, err + } + _, err = stmt.Exec(m4.ID, 4, playoffsGroupID, "Match 4", qf2.ID, 0) + if err != nil { + tx.Rollback() + return nil, err + } + _, err = stmt.Exec(m5.ID, 5, playoffsGroupID, "Match 5", qf3.ID, 1) + if err != nil { + tx.Rollback() + return nil, err + } + _, err = stmt.Exec(m6.ID, 6, playoffsGroupID, "Match 6", qf3.ID, 0) + if err != nil { + tx.Rollback() + return nil, err + } + _, err = stmt.Exec(m7.ID, 7, playoffsGroupID, "Match 7", qf4.ID, 1) + if err != nil { + tx.Rollback() + return nil, err + } + _, err = stmt.Exec(m8.ID, 8, playoffsGroupID, "Match 8", qf4.ID, 0) + tx.Commit() + if err != nil { + return nil, err + } + + for _, match := range matches[playoffsGroupID] { + if match.Players[0] == walkoverPlayerID || match.Players[1] == walkoverPlayerID { + // This is a Bye, so we need to finish it + winnerID := match.Players[0] + if match.Players[0] == walkoverPlayerID { + winnerID = match.Players[1] + } + + if match.TournamentID.Valid { + metadata, err := GetMatchMetadata(match.ID) + if err != nil { + return nil, err + } + + if metadata.WinnerOutcomeMatchID.Valid { + winnerMatch, err := GetMatch(int(metadata.WinnerOutcomeMatchID.Int64)) + if err != nil { + return nil, err + } + idx := 0 + if !metadata.IsWinnerOutcomeHome { + idx = 1 + } + err = SwapPlayers(winnerMatch.ID, winnerID, winnerMatch.Players[idx]) + if err != nil { + return nil, err + } + } + } + _, err = models.DB.Exec(`UPDATE leg SET is_finished = 1, end_time = NOW() WHERE match_id = ?`, match.ID) + if err != nil { + return nil, err + } + _, err = models.DB.Exec(`UPDATE matches SET is_finished = 1, is_bye = 1, is_walkover = 1 WHERE id = ?`, match.ID) + if err != nil { + return nil, err + } + } + } + return nil, nil +} + // GetTournamentMatchesForPlayer will return all tournament matches for the given player and tournament func GetTournamentMatchesForPlayer(tournamentID int, playerID int) ([]*models.Match, error) { rows, err := models.DB.Query(` SELECT - m.id, m.is_finished, m.is_abandoned, m.is_walkover, m.current_leg_id, m.winner_id, m.office_id, m.is_practice, + m.id, m.is_finished, m.is_abandoned, m.is_walkover, m.is_bye, m.current_leg_id, m.winner_id, m.office_id, m.is_practice, m.created_at, m.updated_at, m.owe_type_id, m.venue_id, mt.id, mt.name, mt.description, mm.id, mm.name, mm.short_name, mm.wins_required, mm.legs_required, ot.id, ot.item, v.id, v.name, v.description, l.updated_at as 'last_throw', GROUP_CONCAT(DISTINCT p2l.player_id ORDER BY p2l.order) AS 'players', m.tournament_id, m.tournament_id, t.id, t.name, @@ -789,7 +1147,7 @@ func GetTournamentMatchesForPlayer(tournamentID int, playerID int) ([]*models.Ma venue := new(models.Venue) var players string var legsWon null.String - err := rows.Scan(&m.ID, &m.IsFinished, &m.IsAbandoned, &m.IsWalkover, &m.CurrentLegID, &m.WinnerID, &m.OfficeID, &m.IsPractice, &m.CreatedAt, &m.UpdatedAt, + err := rows.Scan(&m.ID, &m.IsFinished, &m.IsAbandoned, &m.IsWalkover, &m.IsBye, &m.CurrentLegID, &m.WinnerID, &m.OfficeID, &m.IsPractice, &m.CreatedAt, &m.UpdatedAt, &m.OweTypeID, &m.VenueID, &m.MatchType.ID, &m.MatchType.Name, &m.MatchType.Description, &m.MatchMode.ID, &m.MatchMode.Name, &m.MatchMode.ShortName, &m.MatchMode.WinsRequired, &m.MatchMode.LegsRequired, &ot.ID, &ot.Item, &venue.ID, &venue.Name, &venue.Description, &m.LastThrow, &players, &m.TournamentID, &m.TournamentID, &m.Tournament.TournamentID, @@ -817,3 +1175,11 @@ func GetTournamentMatchesForPlayer(tournamentID int, playerID int) ([]*models.Ma return matches, nil } + +func getCompetitor(group []*models.TournamentOverview, num int) int { + if num >= len(group) { + // Not enough players, walkover + return -1 + } + return group[num].PlayerID +} diff --git a/data/tournament_preset.go b/data/tournament_preset.go new file mode 100644 index 00000000..77cc0642 --- /dev/null +++ b/data/tournament_preset.go @@ -0,0 +1,135 @@ +package data + +import ( + "github.com/kcapp/api/models" +) + +// GetTournamentPresets returns all tournament presets +func GetTournamentPresets() ([]*models.TournamentPreset, error) { + rows, err := models.DB.Query(` + SELECT + tp.id, tp.name, tp.starting_score, tp.description, + tp.match_type_id, mt.name, + mml16.id, mml16.name, mml16.short_name, + mmqf.id, mmqf.name, mmqf.short_name, + mmsf.id, mmsf.name, mmsf.short_name, + mmgf.id, mmgf.name, mmgf.short_name, + tg.id, tg.name, + tp.player_id_walkover, tp.player_id_placeholder_home, tp.player_id_placeholder_away + FROM tournament_preset tp + JOIN match_type mt ON mt.id = tp.match_type_id + JOIN match_mode mml16 ON mml16.id = tp.match_mode_id_last_16 + JOIN match_mode mmqf ON mmqf.id = tp.match_mode_id_quarter_final + JOIN match_mode mmsf ON mmsf.id = tp.match_mode_id_semi_final + JOIN match_mode mmgf ON mmgf.id = tp.match_mode_id_grand_final + JOIN tournament_group tg ON tg.id = tp.playoffs_tournament_group_id`) + if err != nil { + return nil, err + } + defer rows.Close() + + presets := make([]*models.TournamentPreset, 0) + for rows.Next() { + tp := new(models.TournamentPreset) + tp.MatchModeLast16 = new(models.MatchMode) + tp.MatchModeQuarterFinal = new(models.MatchMode) + tp.MatchModeSemiFinal = new(models.MatchMode) + tp.MatchModeGrandFinal = new(models.MatchMode) + tp.MatchType = new(models.MatchType) + tp.PlayoffsTournamentGroup = new(models.TournamentGroup) + + err := rows.Scan(&tp.ID, &tp.Name, &tp.StartingScore, &tp.Description, &tp.MatchType.ID, &tp.MatchType.Name, + &tp.MatchModeLast16.ID, &tp.MatchModeLast16.Name, &tp.MatchModeLast16.ShortName, + &tp.MatchModeQuarterFinal.ID, &tp.MatchModeQuarterFinal.Name, &tp.MatchModeQuarterFinal.ShortName, + &tp.MatchModeSemiFinal.ID, &tp.MatchModeSemiFinal.Name, &tp.MatchModeSemiFinal.ShortName, + &tp.MatchModeGrandFinal.ID, &tp.MatchModeGrandFinal.Name, &tp.MatchModeGrandFinal.ShortName, + &tp.PlayoffsTournamentGroup.ID, &tp.PlayoffsTournamentGroup.Name, + &tp.PlayerIDWalkover, &tp.PlayerIDPlaceholderHome, &tp.PlayerIDPlaceholderAway) + if err != nil { + return nil, err + } + presets = append(presets, tp) + } + if err = rows.Err(); err != nil { + return nil, err + } + + return presets, nil +} + +// GetPreset returns the preset for the given ID +func GetTournamentPreset(id int) (*models.TournamentPreset, error) { + tp := new(models.TournamentPreset) + tp.MatchModeLast16 = new(models.MatchMode) + tp.MatchModeQuarterFinal = new(models.MatchMode) + tp.MatchModeSemiFinal = new(models.MatchMode) + tp.MatchModeGrandFinal = new(models.MatchMode) + tp.MatchType = new(models.MatchType) + tp.PlayoffsTournamentGroup = new(models.TournamentGroup) + err := models.DB.QueryRow(` + SELECT + tp.id, tp.name, tp.starting_score, tp.description, + tp.match_type_id, mt.name, + mml16.id, mml16.name, mml16.short_name, + mmqf.id, mmqf.name, mmqf.short_name, + mmsf.id, mmsf.name, mmsf.short_name, + mmgf.id, mmgf.name, mmgf.short_name, + tg.id, tg.name, + tp.player_id_walkover, tp.player_id_placeholder_home, tp.player_id_placeholder_away + FROM tournament_preset tp + JOIN match_type mt ON mt.id = tp.match_type_id + JOIN match_mode mml16 ON mml16.id = tp.match_mode_id_last_16 + JOIN match_mode mmqf ON mmqf.id = tp.match_mode_id_quarter_final + JOIN match_mode mmsf ON mmsf.id = tp.match_mode_id_semi_final + JOIN match_mode mmgf ON mmgf.id = tp.match_mode_id_grand_final + JOIN tournament_group tg ON tg.id = tp.playoffs_tournament_group_id + WHERE tp.id = ?`, id). + Scan(&tp.ID, &tp.Name, &tp.StartingScore, &tp.Description, &tp.MatchType.ID, &tp.MatchType.Name, + &tp.MatchModeLast16.ID, &tp.MatchModeLast16.Name, &tp.MatchModeLast16.ShortName, + &tp.MatchModeQuarterFinal.ID, &tp.MatchModeQuarterFinal.Name, &tp.MatchModeQuarterFinal.ShortName, + &tp.MatchModeSemiFinal.ID, &tp.MatchModeSemiFinal.Name, &tp.MatchModeSemiFinal.ShortName, + &tp.MatchModeGrandFinal.ID, &tp.MatchModeGrandFinal.Name, &tp.MatchModeGrandFinal.ShortName, + &tp.PlayoffsTournamentGroup.ID, &tp.PlayoffsTournamentGroup.Name, + &tp.PlayerIDWalkover, &tp.PlayerIDPlaceholderHome, &tp.PlayerIDPlaceholderAway) + if err != nil { + return nil, err + } + return tp, nil +} + +// AddTournamentPreset will add a new preset to the database +/*func AddTournamentPreset(preset models.TournamentPreset) error { + stmt, err := models.DB.Prepare(` + INSERT INTO match_preset(name, match_type_id, match_mode_id, starting_score, smartcard_uid, description) VALUES(?, ?, ?, ?, ?, ?)`) + if err != nil { + return err + } + defer stmt.Close() + + _, err = stmt.Exec(preset.Name, preset.MatchType.ID, preset.MatchMode.ID, preset.StartingScore, preset.SmartcardUID, preset.Description) + if err != nil { + return err + } + log.Printf("Created preset %s (%v)", preset.Name, preset) + return nil +} + +// UpdateTournamentPreset will update the given preset +func UpdateTournamentPreset(id int, preset models.TournamentPreset) error { + stmt, err := models.DB.Prepare(` + UPDATE match_preset SET + name = ?, match_type_id = ?, match_mode_id = ?, starting_score = ?, smartcard_uid = ?, description = ? + WHERE id = ?`) + if err != nil { + return err + } + defer stmt.Close() + + _, err = stmt.Exec(preset.Name, preset.MatchType.ID, preset.MatchMode.ID, preset.StartingScore, preset.SmartcardUID, preset.Description, id) + if err != nil { + return err + } + log.Printf("Updated preset %s (%v)", preset.Name, preset) + return nil +} +*/ diff --git a/data/v2/player.go b/data/v2/player.go index b0039de6..b0f1a7fe 100644 --- a/data/v2/player.go +++ b/data/v2/player.go @@ -64,7 +64,7 @@ func GetMatchesPlayedPerPlayer() (map[int]*models.Player, error) { FROM player2leg p2l JOIN leg l ON l.id = p2l.leg_id JOIN matches m ON m.id = p2l.match_id - WHERE l.is_finished = 1 AND m.is_abandoned = 0 + WHERE l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 GROUP BY p2l.player_id UNION ALL SELECT @@ -76,7 +76,7 @@ func GetMatchesPlayedPerPlayer() (map[int]*models.Player, error) { FROM matches m JOIN leg l ON l.match_id = m.id JOIN player2leg p2l ON p2l.player_id = m.winner_id AND p2l.match_id = m.id - WHERE l.is_finished = 1 AND m.is_abandoned = 0 + WHERE l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 GROUP BY m.winner_id ) matches GROUP BY player_id`) diff --git a/kcapp-api.go b/kcapp-api.go index dbf34bad..ab0e027b 100644 --- a/kcapp-api.go +++ b/kcapp-api.go @@ -36,6 +36,7 @@ func main() { router.HandleFunc("/match/outshot", controllers.GetOutshotTypes).Methods("GET") router.HandleFunc("/match", controllers.GetMatches).Methods("GET") router.HandleFunc("/match/{id}", controllers.GetMatch).Methods("GET") + router.HandleFunc("/match/{id}/finish", controllers.SetScore).Methods("PUT") router.HandleFunc("/match/{id}/metadata", controllers.GetMatchMetadata).Methods("GET") router.HandleFunc("/match/{id}/rematch", controllers.ReMatch).Methods("POST") router.HandleFunc("/match/{id}/statistics", controllers.GetStatisticsForMatch).Methods("GET") @@ -110,6 +111,8 @@ func main() { router.HandleFunc("/venue/{id}/matches", controllers.GetActiveVenueMatches).Methods("GET") router.HandleFunc("/tournament", controllers.NewTournament).Methods("POST") + router.HandleFunc("/tournament/generate", controllers.GenerateTournament).Methods("POST") + router.HandleFunc("/tournament/generate/playoffs/{id}", controllers.GeneratePlayoffsTournament).Methods("POST") router.HandleFunc("/tournament", controllers.GetTournaments).Methods("GET") router.HandleFunc("/tournament/current", controllers.GetCurrentTournament).Methods("GET") router.HandleFunc("/tournament/current/{office_id}", controllers.GetCurrentTournamentForOffice).Methods("GET") @@ -117,6 +120,10 @@ func main() { router.HandleFunc("/tournament/groups", controllers.AddTournamentGroup).Methods("POST") router.HandleFunc("/tournament/groups", controllers.GetTournamentGroups).Methods("GET") router.HandleFunc("/tournament/standings", controllers.GetTournamentStandings).Methods("GET") + //router.HandleFunc("/tournament/preset", controllers.AddTournamentPreset).Methods("POST") + router.HandleFunc("/tournament/preset", controllers.GetTournamentPresets).Methods("GET") + router.HandleFunc("/tournament/preset/{id}", controllers.GetTournamentPreset).Methods("GET") + //router.HandleFunc("/tournament/preset/{id}", controllers.UpdateTournamentPreset).Methods("PUT") router.HandleFunc("/tournament/{id}", controllers.GetTournament).Methods("GET") router.HandleFunc("/tournament/{id}/player/{player_id}", controllers.GetTournamentPlayerMatches).Methods("GET") router.HandleFunc("/tournament/{id}/matches", controllers.GetTournamentMatches).Methods("GET") diff --git a/models/match.go b/models/match.go index 7a3a97fb..9b0d4f32 100644 --- a/models/match.go +++ b/models/match.go @@ -127,6 +127,7 @@ type Match struct { IsFinished bool `json:"is_finished"` IsAbandoned bool `json:"is_abandoned"` IsWalkover bool `json:"is_walkover"` + IsBye bool `json:"is_bye"` IsStarted bool `json:"is_started"` OfficeID null.Int `json:"office_id,omitempty"` OweTypeID null.Int `json:"owe_type_id"` @@ -161,6 +162,7 @@ func (match Match) MarshalJSON() ([]byte, error) { IsFinished bool `json:"is_finished"` IsAbandoned bool `json:"is_abandoned"` IsWalkover bool `json:"is_walkover"` + IsBye bool `json:"is_bye"` OfficeID null.Int `json:"office_id,omitempty"` OweTypeID null.Int `json:"owe_type_id"` VenueID null.Int `json:"venue_id"` @@ -200,6 +202,7 @@ func (match Match) MarshalJSON() ([]byte, error) { IsFinished: match.IsFinished, IsAbandoned: match.IsAbandoned, IsWalkover: match.IsWalkover, + IsBye: match.IsBye, OfficeID: match.OfficeID, OweTypeID: match.OweTypeID, VenueID: match.VenueID, @@ -220,6 +223,13 @@ func (match Match) MarshalJSON() ([]byte, error) { }) } +type MatchResult struct { + WinnerID int `json:"winner_id"` + WinnerScore int `json:"winner_score"` + LooserID int `json:"looser_id"` + LooserScore int `json:"looser_score"` +} + // MatchType struct used for storing match types type MatchType struct { ID int `json:"id"` diff --git a/models/tournament.go b/models/tournament.go index 31a843ee..748387de 100644 --- a/models/tournament.go +++ b/models/tournament.go @@ -2,6 +2,22 @@ package models import "github.com/guregu/null" +type TournamentMatchTemplate struct { + Home int + Away int +} + +var TournamentTemplateLast16 = [8]TournamentMatchTemplate{ + {Home: 0, Away: 7}, + {Home: 4, Away: 2}, + {Home: 3, Away: 5}, + {Home: 6, Away: 1}, + {Home: 1, Away: 6}, + {Home: 5, Away: 3}, + {Home: 2, Away: 4}, + {Home: 7, Away: 0}, +} + // Tournament struct for storing tournaments type Tournament struct { ID int `json:"id"` @@ -11,6 +27,8 @@ type Tournament struct { IsPlayoffs bool `json:"is_playoffs"` PlayoffsTournamentID null.Int `json:"playoffs_tournament_id,omitempty"` PlayoffsTournament *Tournament `json:"playoffs,omitempty"` + PresetID null.Int `json:"preset_id,omitempty"` + Preset *TournamentPreset `json:"preset,omitempty"` OfficeID int `json:"office_id"` StartTime null.Time `json:"start_time"` EndTime null.Time `json:"end_time"` @@ -60,4 +78,20 @@ type PlayerTournamentStanding struct { } type TournamentProbabilities struct { -} \ No newline at end of file +} + +type TournamentPreset struct { + ID int `json:"id"` + Name string `json:"name"` + MatchType *MatchType `json:"match_type_id"` + StartingScore int `json:"starting_score"` + MatchModeLast16 *MatchMode `json:"match_mode_id_last_16"` + MatchModeQuarterFinal *MatchMode `json:"match_mode_id_quarter_final"` + MatchModeSemiFinal *MatchMode `json:"match_mode_id_semi_final"` + MatchModeGrandFinal *MatchMode `json:"match_mode_id_grand_final"` + PlayoffsTournamentGroup *TournamentGroup `json:"playoffs_tournament_group_id"` + PlayerIDWalkover int `json:"player_id_walkover"` + PlayerIDPlaceholderHome int `json:"player_id_placeholder_home"` + PlayerIDPlaceholderAway int `json:"player_id_placeholder_away"` + Description string `json:"description"` +} From 3eeb23522befa217fa37e710b86ed17ec1367bac Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Fri, 10 Mar 2023 14:24:36 +0100 Subject: [PATCH 09/67] Initial files for next version --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30f747ef..f2a3bf16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## [2.6.0] - TBD + + ## [2.5.0] - 2023-03-10 #### Feature - Insert BotConfig on next Leg @@ -120,6 +123,7 @@ #### Feature - Intial version of API for [kcapp-frontend](https://github.com/kcapp/frontend) +[2.6.0]: https://github.com/kcapp/api/compare/v2.5.0...develop [2.5.0]: https://github.com/kcapp/api/compare/v2.4.0...v2.5.0 [2.4.0]: https://github.com/kcapp/api/compare/v2.3.0...v2.4.0 [2.3.0]: https://github.com/kcapp/api/compare/v2.2.0...v2.3.0 From 5a84c63ac2ba61812b4d94a064cc3fc5a7186bcb Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Fri, 10 Mar 2023 14:37:58 +0100 Subject: [PATCH 10/67] Added command for recalculating scam --- cmd/scam.go | 23 +++++++++++++++++++++++ data/recalculate.go | 2 ++ data/statistics_scam.go | 22 ++++++++-------------- main.go | 5 +---- 4 files changed, 34 insertions(+), 18 deletions(-) create mode 100644 cmd/scam.go diff --git a/cmd/scam.go b/cmd/scam.go new file mode 100644 index 00000000..8faf78f1 --- /dev/null +++ b/cmd/scam.go @@ -0,0 +1,23 @@ +package cmd + +import ( + "github.com/kcapp/api/data" + "github.com/kcapp/api/models" + "github.com/spf13/cobra" +) + +// scamCmd represents the scam command +var scamCmd = &cobra.Command{ + Use: "scam", + Short: "Recalculate Scam statistics", + Run: func(cmd *cobra.Command, args []string) { + err := data.RecalculateStatistics(models.SCAM, legID, since, dryRun) + if err != nil { + panic(err) + } + }, +} + +func init() { + recalculateStatisticsCmd.AddCommand(scamCmd) +} diff --git a/data/recalculate.go b/data/recalculate.go index 6ecab2df..f5ee7b2d 100644 --- a/data/recalculate.go +++ b/data/recalculate.go @@ -62,6 +62,8 @@ func RecalculateStatistics(matchType int, legID int, since string, dryRun bool) queries, err = RecalculateJDCPracticeStatistics(legs) case models.KNOCKOUT: queries, err = RecalculateKnockoutStatistics(legs) + case models.SCAM: + queries, err = ReCalculateScamStatistics(legs) default: return fmt.Errorf("cannot recalculate statistics for type %d", matchType) } diff --git a/data/statistics_scam.go b/data/statistics_scam.go index d1bfb1dd..14f1c938 100644 --- a/data/statistics_scam.go +++ b/data/statistics_scam.go @@ -2,7 +2,7 @@ package data import ( "database/sql" - "log" + "fmt" "github.com/kcapp/api/models" ) @@ -247,24 +247,18 @@ func CalculateScamStatistics(legID int) (map[int]*models.StatisticsScam, error) } // ReCalculateScamStatistics will recaulcate statistics for Scam legs -func ReCalculateScamStatistics() (map[int]map[int]*models.StatisticsScam, error) { - legs, err := GetLegsOfType(models.SCAM, true) - if err != nil { - return nil, err - } - - s := make(map[int]map[int]*models.StatisticsScam) - for _, leg := range legs { - stats, err := CalculateScamStatistics(leg.ID) +func ReCalculateScamStatistics(legs []int) ([]string, error) { + queries := make([]string, 0) + for _, legID := range legs { + stats, err := CalculateScamStatistics(legID) if err != nil { return nil, err } for playerID, stat := range stats { - log.Printf(`UPDATE statistics_scam SET darts_thrown_stopper = %d, darts_thrown_scorer = %d, mpr = %f, score = %d, WHERE leg_id = %d AND player_id = %d;`, - stat.DartsThrownStopper, stat.DartsThrownScorer, stat.MPR, stat.Score, leg.ID, playerID) + queries = append(queries, fmt.Sprintf(`UPDATE statistics_scam SET darts_thrown_stopper = %d, darts_thrown_scorer = %d, mpr = %f, score = %d, WHERE leg_id = %d AND player_id = %d;`, + stat.DartsThrownStopper, stat.DartsThrownScorer, stat.MPR, stat.Score, legID, playerID)) } - s[leg.ID] = stats } - return s, err + return queries, nil } diff --git a/main.go b/main.go index ee53ed04..58a1d4de 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,4 @@ -/* -Copyright © 2022 Thord Setsaas thordendre@gmail.com - -*/ +/* Copyright © 2022 Thord Setsaas thordendre@gmail.com */ package main import "github.com/kcapp/api/cmd" From 4024706625a8d80158b692423d5c269ffc37caf9 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Wed, 5 Apr 2023 11:58:54 +0200 Subject: [PATCH 11/67] Updated changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2a3bf16..a0fdc45e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ # Changelog ## [2.6.0] - TBD - +#### Feature +- Added commands for recalculating statistics, resetting elo etc ## [2.5.0] - 2023-03-10 #### Feature From 23aef6d09835b53af314a4b2f4f539419d6533ee Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Thu, 11 May 2023 21:17:58 +0200 Subject: [PATCH 12/67] Initial files for next version --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30f747ef..f2a3bf16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## [2.6.0] - TBD + + ## [2.5.0] - 2023-03-10 #### Feature - Insert BotConfig on next Leg @@ -120,6 +123,7 @@ #### Feature - Intial version of API for [kcapp-frontend](https://github.com/kcapp/frontend) +[2.6.0]: https://github.com/kcapp/api/compare/v2.5.0...develop [2.5.0]: https://github.com/kcapp/api/compare/v2.4.0...v2.5.0 [2.4.0]: https://github.com/kcapp/api/compare/v2.3.0...v2.4.0 [2.3.0]: https://github.com/kcapp/api/compare/v2.2.0...v2.3.0 From 28b06047ed295249e10d1355974ce49536ac2c11 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Thu, 11 May 2023 21:29:03 +0200 Subject: [PATCH 13/67] Correctly set First 9 Avg if leg was won in <9 darts --- CHANGELOG.md | 3 ++- data/statistics_x01.go | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2a3bf16..de83fe5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ # Changelog ## [2.6.0] - TBD - +#### Fixed +- Correctly set First 9 Avg. if leg was won in <9 darts ## [2.5.0] - 2023-03-10 #### Feature diff --git a/data/statistics_x01.go b/data/statistics_x01.go index 634e3bba..45b748a0 100644 --- a/data/statistics_x01.go +++ b/data/statistics_x01.go @@ -610,7 +610,12 @@ func CalculateX01Statistics(legID int, winnerID int, startingScore int) (map[int // Set PPD and First 9 PPD stats.PPD = float32(stats.PPDScore) / float32(stats.DartsThrown) - stats.FirstNinePPD = float32(stats.FirstNinePPDScore) / float32(9) + if stats.DartsThrown < 9 { + // In 301, we could win in less than 9 darts, so use DartsThrown instead of 9 + stats.FirstNinePPD = float32(stats.FirstNinePPDScore) / float32(stats.DartsThrown) + } else { + stats.FirstNinePPD = float32(stats.FirstNinePPDScore) / float32(9) + } } return statisticsMap, nil From 1364959547681269eb37e56b9b22f7dace0227c3 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Wed, 7 Jun 2023 16:02:57 +0200 Subject: [PATCH 14/67] Return all 9 dart shootout legs when getting statistics --- CHANGELOG.md | 1 + data/statistics_shootout.go | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de83fe5e..83c16f7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [2.6.0] - TBD #### Fixed - Correctly set First 9 Avg. if leg was won in <9 darts +- Return statistics about all `9 Dart Shootout` legs on player statistics ## [2.5.0] - 2023-03-10 #### Feature diff --git a/data/statistics_shootout.go b/data/statistics_shootout.go index 31c0b007..e7926e49 100644 --- a/data/statistics_shootout.go +++ b/data/statistics_shootout.go @@ -31,7 +31,7 @@ func GetShootoutStatistics(from string, to string) ([]*models.StatisticsShootout LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE m.updated_at >= ? AND m.updated_at < ? AND m.is_finished = 1 AND m.is_abandoned = 0 - AND m.match_type_id = 2 + AND (m.match_type_id = 2 OR l.leg_type_id = 2) GROUP BY p.id, m.office_id ORDER BY(COUNT(DISTINCT m2.id) / COUNT(DISTINCT m.id)) DESC, matches_played DESC, ppd DESC`, from, to) if err != nil { @@ -70,7 +70,7 @@ func GetShootoutStatisticsForMatch(matchID int) ([]*models.StatisticsShootout, e JOIN matches m ON m.id = l.match_id WHERE m.id = ? AND m.is_finished = 1 AND m.is_abandoned = 0 - AND m.match_type_id = 2 + AND (m.match_type_id = 2 OR l.leg_type_id = 2) GROUP BY p.id`, matchID) if err != nil { return nil, err @@ -147,7 +147,7 @@ func GetShootoutStatisticsForPlayer(id int) (*models.StatisticsShootout, error) LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE s.player_id = ? AND l.is_finished = 1 AND m.is_abandoned = 0 - AND m.match_type_id = 2 + AND (m.match_type_id = 2 OR l.leg_type_id = 2) GROUP BY p.id`, id).Scan(&s.PlayerID, &s.MatchesPlayed, &s.MatchesWon, &s.LegsPlayed, &s.LegsWon, &s.Score, &s.PPD, &s.Score60sPlus, &s.Score100sPlus, &s.Score140sPlus, &s.Score180s, &s.HighestScore) if err != nil { if err == sql.ErrNoRows { @@ -185,7 +185,7 @@ func GetShootoutHistoryForPlayer(id int, limit int) ([]*models.Leg, error) { LEFT JOIN matches m ON m.id = l.match_id WHERE s.player_id = ? AND l.is_finished = 1 AND m.is_abandoned = 0 - AND m.match_type_id = 2 + AND (m.match_type_id = 2 OR l.leg_type_id = 2) ORDER BY l.id DESC LIMIT ?`, id, limit) if err != nil { From a5b86b7ee3d455e0d5ce3a9e04323c6139db3f91 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Fri, 30 Jun 2023 11:24:45 +0200 Subject: [PATCH 15/67] Prepare for release of v2.6.0 --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83c16f7b..98fb6948 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [2.6.0] - TBD +## [2.6.0] - 2023-06-30 #### Fixed - Correctly set First 9 Avg. if leg was won in <9 darts - Return statistics about all `9 Dart Shootout` legs on player statistics @@ -125,7 +125,7 @@ #### Feature - Intial version of API for [kcapp-frontend](https://github.com/kcapp/frontend) -[2.6.0]: https://github.com/kcapp/api/compare/v2.5.0...develop +[2.6.0]: https://github.com/kcapp/api/compare/v2.5.0...v2.6.0 [2.5.0]: https://github.com/kcapp/api/compare/v2.4.0...v2.5.0 [2.4.0]: https://github.com/kcapp/api/compare/v2.3.0...v2.4.0 [2.3.0]: https://github.com/kcapp/api/compare/v2.2.0...v2.3.0 From 3a902a611c6045d7f7fb04e34f5476d9191af235 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Fri, 30 Jun 2023 11:28:49 +0200 Subject: [PATCH 16/67] Initial files for next version --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98fb6948..a14e4029 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## [2.7.0] - TBD + + ## [2.6.0] - 2023-06-30 #### Fixed - Correctly set First 9 Avg. if leg was won in <9 darts @@ -125,6 +128,7 @@ #### Feature - Intial version of API for [kcapp-frontend](https://github.com/kcapp/frontend) +[2.7.0]: https://github.com/kcapp/api/compare/v2.6.0...develop [2.6.0]: https://github.com/kcapp/api/compare/v2.5.0...v2.6.0 [2.5.0]: https://github.com/kcapp/api/compare/v2.4.0...v2.5.0 [2.4.0]: https://github.com/kcapp/api/compare/v2.3.0...v2.4.0 From 270d6ba9a1f8e7a9cda06265dc0a34646ceb74d7 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Fri, 30 Jun 2023 11:54:45 +0200 Subject: [PATCH 17/67] Correctly handle recalculation of statistics for all time with MySQL8 --- data/recalculate.go | 1 + 1 file changed, 1 insertion(+) diff --git a/data/recalculate.go b/data/recalculate.go index f5ee7b2d..4c55a3ed 100644 --- a/data/recalculate.go +++ b/data/recalculate.go @@ -21,6 +21,7 @@ func RecalculateStatistics(matchType int, legID int, since string, dryRun bool) s := since if s == "" { s = "(All Time)" + since = "1970-01-01" } log.Printf("Recalculating %s statistics since=%s", models.MatchTypes[matchType], s) ids, err := GetLegsToRecalculate(matchType, since) From 238c6f7c42c8686b628489ae087f2ed446111ed4 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Sat, 1 Jul 2023 17:29:49 +0200 Subject: [PATCH 18/67] Explicitly write checkout value for x01, and improved a lot of queries --- CHANGELOG.md | 4 ++++ data/leg.go | 10 ++++---- data/player.go | 52 ++++++++++++++++++++-------------------- data/statistics_x01.go | 40 +++++++++++++++---------------- data/v2/player.go | 32 ++++++++++++------------- models/statistics_x01.go | 1 + 6 files changed, 72 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 073c5c9b..d16798bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ #### Feature - Added commands for recalculating statistics, resetting elo etc +#### Changed +- Store `checkout` value for each `x01` leg explicitly +- Improved a lot of queries when loading player statistics to reduce load times + ## [2.6.0] - 2023-06-30 #### Fixed - Correctly set First 9 Avg. if leg was won in <9 darts diff --git a/data/leg.go b/data/leg.go index 2e9e31c3..1243d31a 100644 --- a/data/leg.go +++ b/data/leg.go @@ -423,10 +423,10 @@ func FinishLeg(visit models.Visit) error { for playerID, stats := range statisticsMap { _, err = tx.Exec(` INSERT INTO statistics_x01 - (leg_id, player_id, ppd, ppd_score, first_nine_ppd, first_nine_ppd_score, checkout_percentage, checkout_attempts, darts_thrown, 60s_plus, + (leg_id, player_id, ppd, ppd_score, first_nine_ppd, first_nine_ppd_score, checkout_percentage, checkout_attempts, checkout, darts_thrown, 60s_plus, 100s_plus, 140s_plus, 180s, accuracy_20, accuracy_19, overall_accuracy) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, visit.LegID, playerID, stats.PPD, stats.PPDScore, stats.FirstNinePPD, stats.FirstNinePPDScore, - stats.CheckoutPercentage, stats.CheckoutAttempts, stats.DartsThrown, stats.Score60sPlus, stats.Score100sPlus, stats.Score140sPlus, + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, visit.LegID, playerID, stats.PPD, stats.PPDScore, stats.FirstNinePPD, stats.FirstNinePPDScore, + stats.CheckoutPercentage, stats.CheckoutAttempts, stats.Checkout, stats.DartsThrown, stats.Score60sPlus, stats.Score100sPlus, stats.Score140sPlus, stats.Score180s, stats.AccuracyStatistics.Accuracy20, stats.AccuracyStatistics.Accuracy19, stats.AccuracyStatistics.AccuracyOverall) if err != nil { tx.Rollback() @@ -779,9 +779,9 @@ func GetLegsToRecalculate(matchType int, since string) ([]int, error) { SELECT l.id FROM leg l JOIN matches m on m.id = l.match_id - WHERE l.has_scores = 1 AND (m.match_type_id = ? OR l.leg_type_id = ?) + WHERE l.has_scores = 1 AND (m.match_type_id = ? AND l.leg_type_id IS NULL OR l.leg_type_id = ?) AND l.updated_at >= DATE_FORMAT(STR_TO_DATE(?, '%Y-%m-%d %T'), "%Y-%m-%d %T") - AND m.is_abandoned = 0 AND l.is_finished = 1 AND l.has_scores = 1 + AND m.is_abandoned = 0 AND l.is_finished = 1 GROUP BY l.id ORDER BY l.id ASC`, matchType, matchType, since) if err != nil { diff --git a/data/player.go b/data/player.go index 62812fb7..50edc541 100644 --- a/data/player.go +++ b/data/player.go @@ -664,35 +664,34 @@ func GetMatchesPlayedPerPlayer() (map[int]*models.Player, error) { rows, err := models.DB.Query(` SELECT player_id, - MAX(matches_played) AS 'matches_played', - MAX(matches_won) AS 'matches_won', - MAX(legs_played) AS 'legs_played', - MAX(legs_won) AS 'legs_won' + MAX(matches_played) AS matches_played, + MAX(matches_won) AS matches_won, + MAX(legs_played) AS legs_played, + MAX(legs_won) AS legs_won FROM ( SELECT p2l.player_id, - COUNT(DISTINCT p2l.match_id) AS 'matches_played', - 0 AS 'matches_won', - COUNT(m.id) AS 'legs_played', - SUM(CASE WHEN p2l.player_id = m.winner_id THEN 1 ELSE 0 END) AS 'legs_won' + COUNT(DISTINCT p2l.match_id) AS matches_played, + 0 AS matches_won, + COUNT(p2l.leg_id) AS legs_played, + SUM(CASE WHEN p2l.player_id = m.winner_id THEN 1 ELSE 0 END) AS legs_won FROM player2leg p2l - JOIN leg l ON l.id = p2l.leg_id JOIN matches m ON m.id = p2l.match_id + JOIN leg l ON l.id = p2l.leg_id AND l.match_id = m.id WHERE l.is_finished = 1 AND m.is_abandoned = 0 GROUP BY p2l.player_id UNION ALL SELECT - p2l.player_id, - 0 AS 'matches_played', - COUNT(DISTINCT m.id) AS 'matches_won', - 0 AS 'legs_played', - 0 AS 'legs_won' + m.winner_id AS player_id, + 0 AS matches_played, + COUNT(DISTINCT m.id) AS matches_won, + 0 AS legs_played, + 0 AS legs_won FROM matches m - JOIN leg l ON l.match_id = m.id - JOIN player2leg p2l ON p2l.player_id = m.winner_id AND p2l.match_id = m.id - WHERE l.is_finished = 1 AND m.is_abandoned = 0 + WHERE m.is_abandoned = 0 GROUP BY m.winner_id - ) matches + ) AS subquery + WHERE player_id IS NOT NULL GROUP BY player_id`) if err != nil { return nil, err @@ -718,18 +717,19 @@ func GetMatchesPlayedPerPlayer() (map[int]*models.Player, error) { func GetPlayerCheckouts(playerID int) ([]*models.CheckoutStatistics, error) { rows, err := models.DB.Query(` SELECT - s.player_id, + x.player_id, s.first_dart, s.first_dart_multiplier, s.second_dart, s.second_dart_multiplier, s.third_dart, s.third_dart_multiplier, - (IFNULL(s.first_dart, 0) * s.first_dart_multiplier + + x.checkout, + COUNT(*) as 'count' + FROM statistics_x01 x + LEFT JOIN leg l on x.leg_id = l.id + LEFT JOIN score s on l.id = s.leg_id + WHERE x.player_id = ? AND x.checkout IS NOT NULL AND + (IFNULL(s.first_dart, 0) * s.first_dart_multiplier + IFNULL(s.second_dart, 0) * s.second_dart_multiplier + - IFNULL(s.third_dart, 0) * s.third_dart_multiplier) AS 'checkout', - COUNT(*) - FROM score s - WHERE s.id IN (SELECT MAX(id) FROM score WHERE leg_id IN ( - SELECT l.id FROM leg l JOIN matches m ON m.id = l.match_id - WHERE m.match_type_id = 1 AND l.winner_id = ?) GROUP BY leg_id) + IFNULL(s.third_dart, 0) * s.third_dart_multiplier) = x.checkout GROUP BY s.first_dart, s.first_dart_multiplier, s.second_dart, s.second_dart_multiplier, s.third_dart, s.third_dart_multiplier diff --git a/data/statistics_x01.go b/data/statistics_x01.go index b3b450af..15059ef7 100644 --- a/data/statistics_x01.go +++ b/data/statistics_x01.go @@ -544,6 +544,9 @@ func CalculateX01Statistics(legID int) (map[int]*models.StatisticsX01, error) { stats := statisticsMap[visit.PlayerID] currentScore := player.CurrentScore + if visit.IsCheckout(currentScore) { + stats.Checkout = null.IntFrom(int64(currentScore)) + } if visit.FirstDart.IsCheckoutAttempt(currentScore, 1) { stats.CheckoutAttempts++ } @@ -716,26 +719,20 @@ func getBestStatistics(ids []int, statisticsMap map[int]*models.StatisticsX01, s func getHighestCheckout(ids []int, statisticsMap map[int]*models.StatisticsX01, startingScores ...int) error { q, args, err := sqlx.In(` SELECT - player_id, - leg_id, - MAX(checkout) - FROM (SELECT - s.player_id, - s.leg_id, - IFNULL(s.first_dart * s.first_dart_multiplier, 0) + - IFNULL(s.second_dart * s.second_dart_multiplier, 0) + - IFNULL(s.third_dart * s.third_dart_multiplier, 0) AS 'checkout' - FROM score s - JOIN leg l ON l.id = s.leg_id - JOIN matches m ON m.id = l.match_id - WHERE l.winner_id = s.player_id - AND s.player_id IN (?) - AND s.id IN (SELECT MAX(s.id) FROM score s JOIN leg l ON l.id = s.leg_id WHERE l.winner_id = s.player_id GROUP BY leg_id) - AND l.starting_score IN (?) - AND IFNULL(l.leg_type_id, m.match_type_id) = 1 - GROUP BY s.player_id, s.id - ORDER BY checkout DESC) checkouts - GROUP BY player_id`, ids, startingScores) + max.player_id, + l.id, + max.checkout + FROM ( + SELECT player_id, MAX(checkout) AS checkout + FROM ( + SELECT s.player_id, checkout + FROM statistics_x01 s LEFT JOIN leg l ON l.id = s.leg_id + WHERE s.player_id IN (?) AND l.starting_score IN (?) + ) AS max_checkout + GROUP BY player_id + ) AS max + JOIN statistics_x01 s2 ON s2.player_id = max.player_id AND s2.checkout = max.checkout + LEFT JOIN leg l ON l.id = s2.leg_id`, ids, startingScores) if err != nil { return err } @@ -902,6 +899,9 @@ func RecalculateX01Statistics(legs []int) ([]string, error) { if stat.AccuracyStatistics.Accuracy20.Valid { query += fmt.Sprintf(", accuracy_20 = %f", stat.AccuracyStatistics.Accuracy20.Float64) } + if stat.Checkout.Valid { + query += fmt.Sprintf(", checkout = %d", stat.Checkout.Int64) + } query += fmt.Sprintf(" WHERE leg_id = %d AND player_id = %d;", legID, playerID) queries = append(queries, query) } diff --git a/data/v2/player.go b/data/v2/player.go index b0039de6..b1545b38 100644 --- a/data/v2/player.go +++ b/data/v2/player.go @@ -50,35 +50,35 @@ func GetMatchesPlayedPerPlayer() (map[int]*models.Player, error) { rows, err := models.DB.Query(` SELECT player_id, - MAX(matches_played) AS 'matches_played', - MAX(matches_won) AS 'matches_won', - MAX(legs_played) AS 'legs_played', - MAX(legs_won) AS 'legs_won' + MAX(matches_played) AS matches_played, + MAX(matches_won) AS matches_won, + MAX(legs_played) AS legs_played, + MAX(legs_won) AS legs_won FROM ( SELECT p2l.player_id, - COUNT(DISTINCT p2l.match_id) AS 'matches_played', - 0 AS 'matches_won', - COUNT(m.id) AS 'legs_played', - SUM(CASE WHEN p2l.player_id = m.winner_id THEN 1 ELSE 0 END) AS 'legs_won' + COUNT(DISTINCT p2l.match_id) AS matches_played, + SUM(CASE WHEN p2l.player_id = m.winner_id THEN 1 ELSE 0 END) AS matches_won, + COUNT(p2l.leg_id) AS legs_played, + SUM(CASE WHEN p2l.player_id = m.winner_id THEN 1 ELSE 0 END) AS legs_won FROM player2leg p2l - JOIN leg l ON l.id = p2l.leg_id JOIN matches m ON m.id = p2l.match_id + JOIN leg l ON l.id = p2l.leg_id AND l.match_id = m.id WHERE l.is_finished = 1 AND m.is_abandoned = 0 GROUP BY p2l.player_id UNION ALL SELECT - p2l.player_id, - 0 AS 'matches_played', - COUNT(DISTINCT m.id) AS 'matches_won', - 0 AS 'legs_played', - 0 AS 'legs_won' + m.winner_id AS player_id, + 0 AS matches_played, + COUNT(DISTINCT m.id) AS matches_won, + 0 AS legs_played, + 0 AS legs_won FROM matches m JOIN leg l ON l.match_id = m.id - JOIN player2leg p2l ON p2l.player_id = m.winner_id AND p2l.match_id = m.id WHERE l.is_finished = 1 AND m.is_abandoned = 0 GROUP BY m.winner_id - ) matches + ) AS subquery + WHERE player_id IS NOT NULL GROUP BY player_id`) if err != nil { return nil, err diff --git a/models/statistics_x01.go b/models/statistics_x01.go index 6d4a170a..8b54fba0 100644 --- a/models/statistics_x01.go +++ b/models/statistics_x01.go @@ -32,6 +32,7 @@ type StatisticsX01 struct { FirstNineThreeDartAvg float32 `json:"first_nine_three_dart_avg"` CheckoutPercentage null.Float `json:"checkout_percentage"` CheckoutAttempts int `json:"checkout_attempts,omitempty"` + Checkout null.Int `json:"checkout,omitempty"` DartsThrown int `json:"darts_thrown,omitempty"` TotalVisits int `json:"total_visits,omitempty"` Score60sPlus int `json:"scores_60s_plus"` From 4fb0af5b0953595f17f6677ef28d7cc204907791 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Sat, 1 Jul 2023 22:24:13 +0200 Subject: [PATCH 19/67] Updated changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d16798bc..bf77aea8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ #### Changed - Store `checkout` value for each `x01` leg explicitly -- Improved a lot of queries when loading player statistics to reduce load times +- Improved a lot of queries when loading player statistics to reduce load times by ~70% for players with a lot of data ## [2.6.0] - 2023-06-30 #### Fixed From 37c34c50ae3a42d9ea52777f09dc498c89537649 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Mon, 3 Jul 2023 15:57:29 +0200 Subject: [PATCH 20/67] Only return x01 legs when requesting random legs for players --- CHANGELOG.md | 3 +++ data/score.go | 2 ++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf77aea8..a56e2fd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ - Store `checkout` value for each `x01` leg explicitly - Improved a lot of queries when loading player statistics to reduce load times by ~70% for players with a lot of data +#### Fixed +- Only return `x01` legs when requesting a random leg for a player + ## [2.6.0] - 2023-06-30 #### Fixed - Correctly set First 9 Avg. if leg was won in <9 darts diff --git a/data/score.go b/data/score.go index f3ba2b3b..c894cbec 100644 --- a/data/score.go +++ b/data/score.go @@ -546,7 +546,9 @@ func GetRandomLegForPlayer(playerID int, startingScore int) ([]*models.Visit, er l.id FROM leg l JOIN player2leg p2l ON p2l.leg_id = l.id + JOIN matches m on m.id = l.match_id WHERE l.is_finished = 1 AND l.winner_id = ? AND l.starting_score = ? AND l.has_scores = 1 + AND IFNULL(l.leg_type_id, m.match_type_id) = 1 -- X01 GROUP BY l.id HAVING COUNT(DISTINCT p2l.player_id) = 2 ORDER BY RAND() From 01c0f766b05d9d7376eaa4d4dd84266d84d7b2c6 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Tue, 11 Jul 2023 08:42:05 +0200 Subject: [PATCH 21/67] New endpoint for getting player hits --- CHANGELOG.md | 2 +- cmd/serve.go | 9 ++++++ controllers/player_controller.go | 30 +++++++++++++++++++ data/player.go | 49 +++++++++++++++++++++++++++++++- 4 files changed, 88 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a56e2fd7..1e05296b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,9 @@ # Changelog ## [2.7.0] - TBD - #### Feature - Added commands for recalculating statistics, resetting elo etc +- New endpoint for getting player hits #### Changed - Store `checkout` value for each `x01` leg explicitly diff --git a/cmd/serve.go b/cmd/serve.go index 93ca4acf..166d5645 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -25,6 +25,14 @@ var serveCmd = &cobra.Command{ models.InitDB(config.GetMysqlConnectionString()) router := mux.NewRouter() + router.Methods("OPTIONS").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") + w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, Access-Control-Request-Headers, Access-Control-Request-Method, Connection, Host, Origin, User-Agent, Referer, Cache-Control, X-header") + w.WriteHeader(http.StatusNoContent) + return + }) + router.HandleFunc("/health", controllers.Healthcheck).Methods("HEAD") router.HandleFunc("/match", controllers.NewMatch).Methods("POST") @@ -60,6 +68,7 @@ var serveCmd = &cobra.Command{ router.HandleFunc("/player/{id}", controllers.GetPlayer).Methods("GET") router.HandleFunc("/player/{id}", controllers.UpdatePlayer).Methods("PUT") router.HandleFunc("/player/{id}/statistics", controllers.GetPlayerStatistics).Methods("GET") + router.HandleFunc("/player/{id}/hits", controllers.GetPlayerHits).Methods("PUT") router.HandleFunc("/player/{id}/statistics/previous", controllers.GetPlayerX01PreviousStatistics).Methods("GET") router.HandleFunc("/player/{id}/progression", controllers.GetPlayerProgression).Methods("GET") router.HandleFunc("/player/{id}/checkouts", controllers.GetPlayerCheckouts).Methods("GET") diff --git a/controllers/player_controller.go b/controllers/player_controller.go index 305387e0..801bd849 100644 --- a/controllers/player_controller.go +++ b/controllers/player_controller.go @@ -144,6 +144,36 @@ func GetPlayerStatistics(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(statistics) } +// GetPlayerHits will return dart hits for the given player +func GetPlayerHits(w http.ResponseWriter, r *http.Request) { + SetHeaders(w) + params := mux.Vars(r) + id, err := strconv.Atoi(params["id"]) + if err != nil { + log.Println("Invalid id parameter") + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + var visit models.Visit + err = json.NewDecoder(r.Body).Decode(&visit) + if err != nil { + log.Println("Unable to deserialize body", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if visit.SecondDart == nil { + visit.SecondDart = new(models.Dart) + } + + hits, err := data.GetPlayerHits(id, visit) + if err != nil { + log.Println("Unable to get player hits") + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + json.NewEncoder(w).Encode(hits) +} + // GetPlayerMatchTypeStatistics will return statistics for the given player func GetPlayerMatchTypeStatistics(w http.ResponseWriter, r *http.Request) { SetHeaders(w) diff --git a/data/player.go b/data/player.go index 50edc541..4ebca993 100644 --- a/data/player.go +++ b/data/player.go @@ -730,10 +730,11 @@ func GetPlayerCheckouts(playerID int) ([]*models.CheckoutStatistics, error) { (IFNULL(s.first_dart, 0) * s.first_dart_multiplier + IFNULL(s.second_dart, 0) * s.second_dart_multiplier + IFNULL(s.third_dart, 0) * s.third_dart_multiplier) = x.checkout + AND s.id IN (SELECT MAX(s.id) FROM score s JOIN leg l ON l.id = s.leg_id WHERE l.winner_id = s.player_id AND s.player_id = ? GROUP BY leg_id) GROUP BY s.first_dart, s.first_dart_multiplier, s.second_dart, s.second_dart_multiplier, s.third_dart, s.third_dart_multiplier - ORDER BY checkout DESC`, playerID) + ORDER BY checkout DESC`, playerID, playerID) if err != nil { return nil, err } @@ -1232,3 +1233,49 @@ func GetPlayerDrawProbability(player1Elo int, player2Elo int) float64 { pDraw := 1 / (1 + math.Exp(-(-0.001215*eloDiff - float64(0.0000005372533)*eloDiff*eloDiff))) return pDraw } + +// GetPlayerHits will return a map containing which darts the player hits after hitting the first and second provided +func GetPlayerHits(playerID int, visit models.Visit) ([]*models.Visit, error) { + rows, err := models.DB.Query(` + SELECT + first_dart, + first_dart_multiplier, + second_dart, + second_dart_multiplier, + third_dart, + third_dart_multiplier, + COUNT(s.id) AS 'count' + FROM score s + JOIN leg l ON l.id = s.leg_id + JOIN matches m ON m.id = l.match_id + WHERE player_id = ? AND IFNULL(l.leg_type_id, m.match_type_id) = 1 + AND first_dart = ? AND first_dart_multiplier = ? + AND ((? = 0) OR (second_dart = ? AND second_dart_multiplier = ?)) + GROUP BY first_dart, first_dart_multiplier, second_dart, second_dart_multiplier, third_dart, third_dart_multiplier + ORDER BY count DESC`, playerID, visit.FirstDart.Value, visit.FirstDart.Multiplier, visit.SecondDart.Multiplier, visit.SecondDart.ValueRaw(), visit.SecondDart.Multiplier) + if err != nil { + return nil, err + } + defer rows.Close() + + hits := make([]*models.Visit, 0) + for rows.Next() { + v := new(models.Visit) + v.FirstDart = new(models.Dart) + v.SecondDart = new(models.Dart) + v.ThirdDart = new(models.Dart) + err := rows.Scan( + &v.FirstDart.Value, &v.FirstDart.Multiplier, + &v.SecondDart.Value, &v.SecondDart.Multiplier, + &v.ThirdDart.Value, &v.ThirdDart.Multiplier, + &v.Count) + if err != nil { + return nil, err + } + hits = append(hits, v) + } + if err = rows.Err(); err != nil { + return nil, err + } + return hits, nil +} From 70595edea7354a018911225b16e90fa70c3b8b7b Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Mon, 17 Jul 2023 13:54:52 +0200 Subject: [PATCH 22/67] Initial work to support ANY/MASTER out for X01 --- CHANGELOG.md | 1 + data/leg.go | 43 ++++++++++++++++++----------- data/match.go | 11 ++++++-- data/player.go | 2 ++ data/score.go | 4 +-- data/statistics_x01.go | 58 ++++++++++++++++++++++++--------------- models/dart.go | 37 ++++++++++++++++++++----- models/dart_test.go | 61 +++++++++++++++++++++++++++++++++++------- models/visit.go | 23 ++++++++++++---- models/visit_test.go | 27 +++++++++++++++++++ 10 files changed, 204 insertions(+), 63 deletions(-) create mode 100644 models/visit_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e05296b..7229d19f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ #### Feature - Added commands for recalculating statistics, resetting elo etc - New endpoint for getting player hits +- Support for `ANY` and `MASTER` out for `x01` legs #### Changed - Store `checkout` value for each `x01` leg explicitly diff --git a/data/leg.go b/data/leg.go index 1243d31a..9ed0a94b 100644 --- a/data/leg.go +++ b/data/leg.go @@ -60,6 +60,16 @@ func NewLeg(matchID int, startingScore int, players []int, matchType *int) (*mod for _, player := range scores { handicaps[player.PlayerID] = player.Handicap } + } + + // Insert leg parameters + if match.MatchType.ID == models.X01 || match.MatchType.ID == models.X01HANDICAP { + params := match.Legs[0].Parameters + _, err = tx.Exec("INSERT INTO leg_parameters (leg_id, outshot_type_id) VALUES (?, ?)", legID, params.OutshotType.ID) + if err != nil { + tx.Rollback() + return nil, err + } } else if *matchType == models.TICTACTOE { params := match.Legs[0].Parameters params.GenerateTicTacToeNumbers(startingScore) @@ -708,7 +718,7 @@ func GetLegsForMatch(matchID int) ([]*models.Leg, error) { leg.Visits = visits matchType := leg.LegType.ID - if matchType == models.TICTACTOE || matchType == models.KNOCKOUT { + if matchType == models.X01 || matchType == models.TICTACTOE || matchType == models.KNOCKOUT { leg.Parameters, err = GetLegParameters(leg.ID) if err != nil { return nil, err @@ -758,7 +768,7 @@ func GetLegsOfType(matchType int, loadVisits bool) ([]*models.Leg, error) { } leg.Visits = visits } - if matchType == models.TICTACTOE || matchType == models.KNOCKOUT { + if matchType == models.X01 || matchType == models.TICTACTOE || matchType == models.KNOCKOUT { leg.Parameters, err = GetLegParameters(leg.ID) if err != nil { return nil, err @@ -874,7 +884,7 @@ func GetLeg(id int) (*models.Leg, error) { } matchType := leg.LegType.ID - if matchType == models.TICTACTOE || matchType == models.KNOCKOUT { + if matchType == models.X01 || matchType == models.X01HANDICAP || matchType == models.TICTACTOE || matchType == models.KNOCKOUT { leg.Parameters, err = GetLegParameters(id) if err != nil { return nil, err @@ -1085,7 +1095,7 @@ func GetLeg(id int) (*models.Leg, error) { leg.Visits = visits leg.Hits, leg.DartsThrown = models.GetHitsMap(visits) if matchType == models.X01 || matchType == models.X01HANDICAP { - leg.CheckoutStatistics, err = getCheckoutStatistics(leg.ID, leg.StartingScore) + leg.CheckoutStatistics, err = getCheckoutStatistics(leg) } if err != nil { return nil, err @@ -1262,6 +1272,9 @@ func GetLegParameters(legID int) (*models.LegParameters, error) { SELECT outshot_type_id, number_1, number_2, number_3, number_4, number_5, number_6, number_7, number_8, number_9, starting_lives FROM leg_parameters WHERE leg_id = ?`, legID).Scan(&ost, &n[0], &n[1], &n[2], &n[3], &n[4], &n[5], &n[6], &n[7], &n[8], ¶ms.StartingLives) if err != nil { + if err == sql.ErrNoRows { + return new(models.LegParameters), nil + } return nil, err } if ost.Valid { @@ -1298,13 +1311,9 @@ func GetLegMatchType(legID int) (*int, error) { } // getCheckoutStatistics will get all checkout attempts for the given leg -func getCheckoutStatistics(legID int, startingScore int) (*models.CheckoutStatistics, error) { - visits, err := GetLegVisits(legID) - if err != nil { - return nil, err - } - - players, err := GetPlayersScore(legID) +func getCheckoutStatistics(leg *models.Leg) (*models.CheckoutStatistics, error) { + visits := leg.Visits + players, err := GetPlayersScore(leg.ID) if err != nil { return nil, err } @@ -1312,31 +1321,35 @@ func getCheckoutStatistics(legID int, startingScore int) (*models.CheckoutStatis playersMap := make(map[int]*models.Player2Leg) for _, player := range players { playersMap[player.PlayerID] = player - player.CurrentScore = startingScore + player.CurrentScore = leg.StartingScore if player.Handicap.Valid { player.CurrentScore += int(player.Handicap.Int64) } } + outshotTypeId := models.OUTSHOTDOUBLE + if leg.Parameters.OutshotType != nil { + outshotTypeId = leg.Parameters.OutshotType.ID + } totalAttempts := 0 checkoutAttempts := make(map[int]int) for _, visit := range visits { player := playersMap[visit.PlayerID] currentScore := player.CurrentScore - if visit.FirstDart.IsCheckoutAttempt(currentScore, 1) { + if visit.FirstDart.IsCheckoutAttempt(currentScore, 1, outshotTypeId) { totalAttempts++ checkoutAttempts[currentScore]++ } currentScore -= visit.FirstDart.GetScore() - if visit.SecondDart.IsCheckoutAttempt(currentScore, 2) { + if visit.SecondDart.IsCheckoutAttempt(currentScore, 2, outshotTypeId) { totalAttempts++ checkoutAttempts[currentScore]++ } currentScore -= visit.SecondDart.GetScore() - if visit.ThirdDart.IsCheckoutAttempt(currentScore, 3) { + if visit.ThirdDart.IsCheckoutAttempt(currentScore, 3, outshotTypeId) { totalAttempts++ checkoutAttempts[currentScore]++ } diff --git a/data/match.go b/data/match.go index 6bd790cd..9b194d17 100644 --- a/data/match.go +++ b/data/match.go @@ -43,7 +43,14 @@ func NewMatch(match models.Match) (*models.Match, error) { tx.Rollback() return nil, err } - if match.MatchType.ID == models.TICTACTOE { + if match.MatchType.ID == models.X01 || match.MatchType.ID == models.X01HANDICAP { + params := match.Legs[0].Parameters + _, err = tx.Exec("INSERT INTO leg_parameters (leg_id, outshot_type_id) VALUES (?, ?)", legID, params.OutshotType.ID) + if err != nil { + tx.Rollback() + return nil, err + } + } else if match.MatchType.ID == models.TICTACTOE { params := match.Legs[0].Parameters params.GenerateTicTacToeNumbers(startingScore) _, err = tx.Exec("INSERT INTO leg_parameters (leg_id, outshot_type_id, number_1, number_2, number_3, number_4, number_5, number_6, number_7, number_8, number_9) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", @@ -551,7 +558,7 @@ func GetMatchTypes() ([]*models.MatchType, error) { // GetOutshotTypes will return all outshot types func GetOutshotTypes() ([]*models.OutshotType, error) { - rows, err := models.DB.Query("SELECT id, `name`, short_name FROM outshot_type") + rows, err := models.DB.Query("SELECT id, `name`, short_name FROM outshot_type ORDER BY FIELD(id, 3, 1, 2)") if err != nil { return nil, err } diff --git a/data/player.go b/data/player.go index 4ebca993..5c7b894c 100644 --- a/data/player.go +++ b/data/player.go @@ -725,12 +725,14 @@ func GetPlayerCheckouts(playerID int) ([]*models.CheckoutStatistics, error) { COUNT(*) as 'count' FROM statistics_x01 x LEFT JOIN leg l on x.leg_id = l.id + LEFT JOIN leg_parameters lp on l.id = lp.leg_id LEFT JOIN score s on l.id = s.leg_id WHERE x.player_id = ? AND x.checkout IS NOT NULL AND (IFNULL(s.first_dart, 0) * s.first_dart_multiplier + IFNULL(s.second_dart, 0) * s.second_dart_multiplier + IFNULL(s.third_dart, 0) * s.third_dart_multiplier) = x.checkout AND s.id IN (SELECT MAX(s.id) FROM score s JOIN leg l ON l.id = s.leg_id WHERE l.winner_id = s.player_id AND s.player_id = ? GROUP BY leg_id) + AND (lp.outshot_type_id = 2 OR lp.outshot_type_id IS NULL) -- Only count DOUBLEOUT legs GROUP BY s.first_dart, s.first_dart_multiplier, s.second_dart, s.second_dart_multiplier, s.third_dart, s.third_dart_multiplier diff --git a/data/score.go b/data/score.go index c894cbec..5662cb2e 100644 --- a/data/score.go +++ b/data/score.go @@ -48,8 +48,8 @@ func AddVisit(visit models.Visit) (*models.Visit, error) { isFinished := false // Invalidate extra darts not thrown, and check if leg is finished if matchType == models.X01 || matchType == models.X01HANDICAP { - visit.SetIsBust(players[visit.PlayerID].CurrentScore) - isFinished = !visit.IsBust && visit.IsCheckout(players[visit.PlayerID].CurrentScore) + visit.SetIsBust(players[visit.PlayerID].CurrentScore, leg.Parameters.OutshotType.ID) + isFinished = !visit.IsBust && visit.IsCheckout(players[visit.PlayerID].CurrentScore, leg.Parameters.OutshotType.ID) } else if matchType == models.SHOOTOUT { isFinished = ((len(leg.Visits) + 1) * 3) >= (9 * len(leg.Players)) if isFinished { diff --git a/data/statistics_x01.go b/data/statistics_x01.go index 15059ef7..6066891d 100644 --- a/data/statistics_x01.go +++ b/data/statistics_x01.go @@ -30,7 +30,8 @@ func GetX01Statistics(from string, to string, matchType int, startingScores ...i SUM(accuracy_20) / COUNT(accuracy_20) AS 'accuracy_20s', SUM(accuracy_19) / COUNT(accuracy_19) AS 'accuracy_19s', SUM(overall_accuracy) / COUNT(overall_accuracy) AS 'accuracy_overall', - COUNT(s.checkout_percentage) / SUM(s.checkout_attempts) * 100 AS 'checkout_percentage' + COUNT(s.checkout_percentage) / SUM(s.checkout_attempts) * 100 AS 'checkout_percentage', + MAX(s.checkout) AS 'checkout' FROM statistics_x01 s JOIN player p ON p.id = s.player_id JOIN leg l ON l.id = s.leg_id @@ -58,7 +59,7 @@ func GetX01Statistics(from string, to string, matchType int, startingScores ...i s := new(models.StatisticsX01) err := rows.Scan(&s.PlayerID, &s.MatchesPlayed, &s.MatchesWon, &s.LegsPlayed, &s.LegsWon, &s.OfficeID, &s.PPD, &s.FirstNinePPD, &s.ThreeDartAvg, &s.FirstNineThreeDartAvg, &s.Score60sPlus, &s.Score100sPlus, &s.Score140sPlus, - &s.Score180s, &s.Accuracy20, &s.Accuracy19, &s.AccuracyOverall, &s.CheckoutPercentage) + &s.Score180s, &s.Accuracy20, &s.Accuracy19, &s.AccuracyOverall, &s.CheckoutPercentage, &s.Checkout) if err != nil { return nil, err } @@ -86,7 +87,8 @@ func GetX01StatisticsForLeg(id int) ([]*models.StatisticsX01, error) { s.overall_accuracy, s.darts_thrown, s.checkout_attempts, - IFNULL(s.checkout_percentage, 0) AS 'checkout_percentage' + IFNULL(s.checkout_percentage, 0) AS 'checkout_percentage', + s.checkout FROM statistics_x01 s JOIN player p ON p.id = s.player_id JOIN leg l ON l.id = s.leg_id @@ -106,7 +108,7 @@ func GetX01StatisticsForLeg(id int) ([]*models.StatisticsX01, error) { s := new(models.StatisticsX01) err := rows.Scan(&s.LegID, &s.PlayerID, &s.PPD, &s.FirstNinePPD, &s.ThreeDartAvg, &s.FirstNineThreeDartAvg, &s.Score60sPlus, &s.Score100sPlus, &s.Score140sPlus, &s.Score180s, &s.Accuracy20, &s.Accuracy19, &s.AccuracyOverall, &s.DartsThrown, - &s.CheckoutAttempts, &s.CheckoutPercentage) + &s.CheckoutAttempts, &s.CheckoutPercentage, &s.Checkout) if err != nil { return nil, err } @@ -132,7 +134,8 @@ func GetX01StatisticsForMatch(id int) ([]*models.StatisticsX01, error) { SUM(s.accuracy_19) / COUNT(s.accuracy_19) AS 'accuracy_19s', SUM(s.overall_accuracy) / COUNT(s.overall_accuracy) AS 'accuracy_overall', SUM(s.checkout_attempts) AS 'checkout_attempts', - COUNT(s.checkout_percentage) / SUM(s.checkout_attempts) * 100 AS 'checkout_percentage' + COUNT(s.checkout_percentage) / SUM(s.checkout_attempts) * 100 AS 'checkout_percentage', + MAX(s.checkout) AS 'checkout' FROM statistics_x01 s JOIN player p ON p.id = s.player_id JOIN leg l ON l.id = s.leg_id @@ -152,7 +155,7 @@ func GetX01StatisticsForMatch(id int) ([]*models.StatisticsX01, error) { s := new(models.StatisticsX01) err := rows.Scan(&s.PlayerID, &s.PPD, &s.FirstNinePPD, &s.ThreeDartAvg, &s.FirstNineThreeDartAvg, &s.Score60sPlus, &s.Score100sPlus, &s.Score140sPlus, &s.Score180s, &s.Accuracy20, &s.Accuracy19, &s.AccuracyOverall, &s.CheckoutAttempts, - &s.CheckoutPercentage) + &s.CheckoutPercentage, &s.Checkout) if err != nil { return nil, err } @@ -217,7 +220,8 @@ func GetPlayersX01Statistics(ids []int, startingScores ...int) ([]*models.Statis SUM(s.accuracy_20) / COUNT(s.accuracy_20) AS 'accuracy_20s', SUM(s.accuracy_19) / COUNT(s.accuracy_19) AS 'accuracy_19s', SUM(s.overall_accuracy) / COUNT(s.overall_accuracy) AS 'accuracy_overall', - COUNT(s.checkout_percentage) / SUM(s.checkout_attempts) * 100 AS 'checkout_percentage' + COUNT(s.checkout_percentage) / SUM(s.checkout_attempts) * 100 AS 'checkout_percentage', + MAX(s.checkout) AS 'checkout' FROM statistics_x01 s JOIN player p ON p.id = s.player_id JOIN leg l ON l.id = s.leg_id @@ -245,7 +249,7 @@ func GetPlayersX01Statistics(ids []int, startingScores ...int) ([]*models.Statis s := new(models.StatisticsX01) err := rows.Scan(&s.PlayerID, &s.MatchesPlayed, &s.MatchesWon, &s.LegsPlayed, &s.LegsWon, &s.PPD, &s.FirstNinePPD, &s.ThreeDartAvg, &s.FirstNineThreeDartAvg, &s.Score60sPlus, &s.Score100sPlus, &s.Score140sPlus, &s.Score180s, - &s.Accuracy20, &s.Accuracy19, &s.AccuracyOverall, &s.CheckoutPercentage) + &s.Accuracy20, &s.Accuracy19, &s.AccuracyOverall, &s.CheckoutPercentage, &s.Checkout) if err != nil { return nil, err } @@ -304,7 +308,8 @@ func GetPlayersX01PreviousStatistics(ids []int, startingScores ...int) ([]*model SUM(s.accuracy_20) / COUNT(s.accuracy_20) AS 'accuracy_20s', SUM(s.accuracy_19) / COUNT(s.accuracy_19) AS 'accuracy_19s', SUM(s.overall_accuracy) / COUNT(s.overall_accuracy) AS 'accuracy_overall', - COUNT(s.checkout_percentage) / SUM(s.checkout_attempts) * 100 AS 'checkout_percentage' + COUNT(s.checkout_percentage) / SUM(s.checkout_attempts) * 100 AS 'checkout_percentage', + MAX(s.checkout) AS 'checkout' FROM statistics_x01 s JOIN player p ON p.id = s.player_id JOIN leg l ON l.id = s.leg_id @@ -334,7 +339,7 @@ func GetPlayersX01PreviousStatistics(ids []int, startingScores ...int) ([]*model s := new(models.StatisticsX01) err := rows.Scan(&s.PlayerID, &s.MatchesPlayed, &s.MatchesWon, &s.LegsPlayed, &s.LegsWon, &s.PPD, &s.FirstNinePPD, &s.ThreeDartAvg, &s.FirstNineThreeDartAvg, &s.Score60sPlus, &s.Score100sPlus, &s.Score140sPlus, &s.Score180s, - &s.Accuracy20, &s.Accuracy19, &s.AccuracyOverall, &s.CheckoutPercentage) + &s.Accuracy20, &s.Accuracy19, &s.AccuracyOverall, &s.CheckoutPercentage, &s.Checkout) if err != nil { return nil, err } @@ -379,6 +384,7 @@ func GetPlayerProgression(id int) (map[string]*models.StatisticsX01, error) { SUM(s.accuracy_19) / COUNT(s.accuracy_19) AS 'accuracy_19s', SUM(s.overall_accuracy) / COUNT(s.overall_accuracy) AS 'accuracy_overall', COUNT(s.checkout_percentage) / SUM(s.checkout_attempts) * 100 AS 'checkout_percentage', + MAX(s.checkout) AS 'checkout', DATE(m.updated_at) AS 'date' FROM statistics_x01 s JOIN leg l ON l.id = s.leg_id @@ -386,7 +392,7 @@ func GetPlayerProgression(id int) (map[string]*models.StatisticsX01, error) { WHERE s.player_id = ? AND m.match_type_id = 1 AND m.is_finished = 1 AND m.is_abandoned = 0 AND m.is_practice = 0 - GROUP BY YEAR(m.updateD_at), WEEK(m.updated_at) + GROUP BY YEAR(m.updated_at), WEEK(m.updated_at) ORDER BY date DESC`, id) if err != nil { return nil, err @@ -398,7 +404,8 @@ func GetPlayerProgression(id int) (map[string]*models.StatisticsX01, error) { var date string s := new(models.StatisticsX01) err := rows.Scan(&s.PlayerID, &s.PPD, &s.FirstNinePPD, &s.ThreeDartAvg, &s.FirstNineThreeDartAvg, &s.Score60sPlus, - &s.Score100sPlus, &s.Score140sPlus, &s.Score180s, &s.Accuracy20, &s.Accuracy19, &s.AccuracyOverall, &s.CheckoutPercentage, &date) + &s.Score100sPlus, &s.Score140sPlus, &s.Score180s, &s.Accuracy20, &s.Accuracy19, &s.AccuracyOverall, &s.CheckoutPercentage, + &s.Checkout, &date) if err != nil { return nil, err } @@ -432,7 +439,8 @@ func GetX01StatisticsForPlayer(id int, matchType int) (*models.StatisticsX01, er SUM(accuracy_20) / COUNT(accuracy_20) AS 'accuracy_20s', SUM(accuracy_19) / COUNT(accuracy_19) AS 'accuracy_19s', SUM(overall_accuracy) / COUNT(overall_accuracy) AS 'accuracy_overall', - COUNT(s.checkout_percentage) / SUM(s.checkout_attempts) * 100 AS 'checkout_percentage' + COUNT(s.checkout_percentage) / SUM(s.checkout_attempts) * 100 AS 'checkout_percentage', + MAX(s.checkout) AS 'checkout' FROM statistics_x01 s JOIN player p ON p.id = s.player_id JOIN leg l ON l.id = s.leg_id @@ -444,7 +452,7 @@ func GetX01StatisticsForPlayer(id int, matchType int) (*models.StatisticsX01, er AND m.match_type_id = ? GROUP BY p.id`, id, matchType).Scan(&s.PlayerID, &s.MatchesPlayed, &s.MatchesWon, &s.LegsPlayed, &s.LegsWon, &s.PPD, &s.FirstNinePPD, &s.ThreeDartAvg, &s.FirstNineThreeDartAvg, &s.Score60sPlus, &s.Score100sPlus, &s.Score140sPlus, &s.Score180s, &s.Accuracy20, &s.Accuracy19, - &s.AccuracyOverall, &s.CheckoutPercentage) + &s.AccuracyOverall, &s.CheckoutPercentage, &s.Checkout) if err != nil { if err == sql.ErrNoRows { return new(models.StatisticsX01), nil @@ -482,7 +490,8 @@ func GetX01HistoryForPlayer(id int, limit int, matchType int) ([]*models.Leg, er s.overall_accuracy, s.darts_thrown, s.checkout_attempts, - IFNULL(s.checkout_percentage, 0) AS 'checkout_percentage' + IFNULL(s.checkout_percentage, 0) AS 'checkout_percentage', + s.checkout AS 'checkout' FROM statistics_x01 s LEFT JOIN player p ON p.id = s.player_id LEFT JOIN leg l ON l.id = s.leg_id @@ -502,7 +511,7 @@ func GetX01HistoryForPlayer(id int, limit int, matchType int) ([]*models.Leg, er s := new(models.StatisticsX01) err := rows.Scan(&s.LegID, &s.PlayerID, &s.PPD, &s.FirstNinePPD, &s.ThreeDartAvg, &s.FirstNineThreeDartAvg, &s.Score60sPlus, &s.Score100sPlus, &s.Score140sPlus, &s.Score180s, &s.Accuracy20, &s.Accuracy19, &s.AccuracyOverall, &s.DartsThrown, - &s.CheckoutAttempts, &s.CheckoutPercentage) + &s.CheckoutAttempts, &s.CheckoutPercentage, &s.Checkout) if err != nil { return nil, err } @@ -515,10 +524,12 @@ func GetX01HistoryForPlayer(id int, limit int, matchType int) ([]*models.Leg, er // CalculateX01Statistics will calculate x01 statistics for the given leg func CalculateX01Statistics(legID int) (map[int]*models.StatisticsX01, error) { - visits, err := GetLegVisits(legID) + leg, err := GetLeg(legID) if err != nil { return nil, err } + + visits := leg.Visits winnerID := visits[len(visits)-1].PlayerID players, err := GetPlayersScore(legID) @@ -544,18 +555,18 @@ func CalculateX01Statistics(legID int) (map[int]*models.StatisticsX01, error) { stats := statisticsMap[visit.PlayerID] currentScore := player.CurrentScore - if visit.IsCheckout(currentScore) { + if visit.IsCheckout(currentScore, leg.Parameters.OutshotType.ID) { stats.Checkout = null.IntFrom(int64(currentScore)) } - if visit.FirstDart.IsCheckoutAttempt(currentScore, 1) { + if visit.FirstDart.IsCheckoutAttempt(currentScore, 1, leg.Parameters.OutshotType.ID) { stats.CheckoutAttempts++ } currentScore -= visit.FirstDart.GetScore() - if visit.SecondDart.IsCheckoutAttempt(currentScore, 2) { + if visit.SecondDart.IsCheckoutAttempt(currentScore, 2, leg.Parameters.OutshotType.ID) { stats.CheckoutAttempts++ } currentScore -= visit.SecondDart.GetScore() - if visit.ThirdDart.IsCheckoutAttempt(currentScore, 3) { + if visit.ThirdDart.IsCheckoutAttempt(currentScore, 3, leg.Parameters.OutshotType.ID) { stats.CheckoutAttempts++ } currentScore -= visit.ThirdDart.GetScore() @@ -726,8 +737,11 @@ func getHighestCheckout(ids []int, statisticsMap map[int]*models.StatisticsX01, SELECT player_id, MAX(checkout) AS checkout FROM ( SELECT s.player_id, checkout - FROM statistics_x01 s LEFT JOIN leg l ON l.id = s.leg_id + FROM statistics_x01 s + LEFT JOIN leg l ON l.id = s.leg_id + LEFT JOIN leg_parameters lp on l.id = lp.leg_id WHERE s.player_id IN (?) AND l.starting_score IN (?) + AND (lp.outshot_type_id = 2 OR lp.outshot_type_id IS NULL) ) AS max_checkout GROUP BY player_id ) AS max diff --git a/models/dart.go b/models/dart.go index 485dc1cc..c5856dd6 100644 --- a/models/dart.go +++ b/models/dart.go @@ -28,12 +28,23 @@ type Dart struct { } // IsBust will check if the given dart is a bust -func (dart *Dart) IsBust(currentScore int) bool { +func (dart *Dart) IsBust(currentScore int, outshotTypeId int) bool { scoreAfterThrow := currentScore - dart.GetScore() - if scoreAfterThrow == 0 && dart.IsDouble() { - return false - } else if scoreAfterThrow < 2 { - return true + if scoreAfterThrow == 0 { + if outshotTypeId == OUTSHOTANY || + (outshotTypeId == OUTSHOTDOUBLE && dart.IsDouble()) || + (outshotTypeId == OUTSHOTMASTER && dart.IsTriple()) { + return false + } + } + if outshotTypeId == OUTSHOTANY { + if scoreAfterThrow < 1 { + return true + } + } else { + if scoreAfterThrow < 2 { + return true + } } // If the throw is not a bust, make sure the dart is valid @@ -107,15 +118,27 @@ func (dart Dart) GetJDCPracticeScore(target Target) int { } // IsCheckoutAttempt checks if this dart was a checkout attempt -func (dart Dart) IsCheckoutAttempt(currentScore int, num int) bool { +func (dart Dart) IsCheckoutAttempt(currentScore int, dartNum int, outshotTypeId int) bool { if !dart.Value.Valid { // Dart was not actually thrown, player busted/checked out already return false } + + if outshotTypeId == OUTSHOTANY { + if ((currentScore <= 20 || currentScore == 25) || + ((currentScore == 50 || currentScore <= 40) && currentScore%2 == 0) || + (currentScore <= 60 && currentScore%3 == 0)) && currentScore > 0 { + return true + } + } else if outshotTypeId == OUTSHOTMASTER { + if currentScore <= 60 && currentScore%3 == 0 && currentScore > 1 { + return true + } + } if currentScore-dart.GetScore() == 0 && dart.IsDouble() { // Actual checkout return true - } else if (num == 3 && currentScore == 50) || (currentScore <= 40 && currentScore%2 == 0 && currentScore > 1) { + } else if (dartNum == 3 && currentScore == 50) || (currentScore <= 40 && currentScore%2 == 0 && currentScore > 1) { // Checkout attempt (bull only counts if it was on the third dart) return true } diff --git a/models/dart_test.go b/models/dart_test.go index a54c1499..9666ead8 100644 --- a/models/dart_test.go +++ b/models/dart_test.go @@ -11,15 +11,32 @@ import ( // TestIsBust will check that the given dart is bust func TestIsBust(t *testing.T) { dart := Dart{Value: null.IntFrom(20), Multiplier: 1} - assert.Equal(t, dart.IsBust(20), true, "should be bust") + assert.Equal(t, dart.IsBust(20, OUTSHOTDOUBLE), true, "should be bust") dart = Dart{Value: null.IntFrom(10), Multiplier: 2} - assert.Equal(t, dart.IsBust(20), false, "should not be bust") + assert.Equal(t, dart.IsBust(20, OUTSHOTDOUBLE), false, "should not be bust") dart = Dart{Value: null.NewInt(-1, false), Multiplier: 1} - assert.Equal(t, dart.IsBust(301), false, "should be bust") + assert.Equal(t, dart.IsBust(301, OUTSHOTDOUBLE), false, "should be bust") assert.Equal(t, dart.Value.Valid, true, "should be valid") assert.Equal(t, dart.Value.Int64, int64(0), "should be 0") + + dart = Dart{Value: null.IntFrom(3), Multiplier: 1} + assert.Equal(t, dart.IsBust(4, OUTSHOTDOUBLE), true, "should be bust") + + dart = Dart{Value: null.IntFrom(3), Multiplier: 1} + assert.Equal(t, dart.IsBust(4, OUTSHOTANY), false, "should not be bust") +} + +func TestIsBust_ScoreIsZero(t *testing.T) { + dart := Dart{Value: null.IntFrom(3), Multiplier: 1} + assert.Equal(t, dart.IsBust(3, OUTSHOTANY), false, "should not be bust") + + dart = Dart{Value: null.IntFrom(3), Multiplier: 2} + assert.Equal(t, dart.IsBust(6, OUTSHOTDOUBLE), false, "should not be bust") + + dart = Dart{Value: null.IntFrom(3), Multiplier: 3} + assert.Equal(t, dart.IsBust(9, OUTSHOTMASTER), false, "should not be bust") } // TestIsBustAbove will check that the given dart is bust above @@ -132,27 +149,51 @@ func TestGetJDCPracticeScore(t *testing.T) { assert.Equal(t, score, 0, "score should be 0") } -// TestIsCheckoutAttempt will check if the given dart is a checkout attempt -func TestIsCheckoutAttempt(t *testing.T) { +// TestIsCheckoutAttempt_Doubles will check if the given dart is a checkout attempt +func TestIsCheckoutAttempt_Doubles(t *testing.T) { // Invalid dart dart := Dart{Value: null.NewInt(0, false), Multiplier: 2} - assert.Equal(t, dart.IsCheckoutAttempt(301, 1), false, "should be false") + assert.Equal(t, dart.IsCheckoutAttempt(301, 1, OUTSHOTDOUBLE), false, "should be false") // Not checkout dart = Dart{Value: null.IntFrom(20), Multiplier: 3} - assert.Equal(t, dart.IsCheckoutAttempt(301, 1), false, "should be false") + assert.Equal(t, dart.IsCheckoutAttempt(301, 1, OUTSHOTDOUBLE), false, "should be false") // Successful checkout dart = Dart{Value: null.IntFrom(20), Multiplier: 2} - assert.Equal(t, dart.IsCheckoutAttempt(40, 1), true, "should be true") + assert.Equal(t, dart.IsCheckoutAttempt(40, 1, OUTSHOTDOUBLE), true, "should be true") // Checkout attempt dart = Dart{Value: null.IntFrom(8), Multiplier: 1} - assert.Equal(t, dart.IsCheckoutAttempt(32, 1), true, "should be true") + assert.Equal(t, dart.IsCheckoutAttempt(32, 1, OUTSHOTDOUBLE), true, "should be true") // Checkout attempt bull dart = Dart{Value: null.IntFrom(18), Multiplier: 1} - assert.Equal(t, dart.IsCheckoutAttempt(50, 3), true, "should be true") + assert.Equal(t, dart.IsCheckoutAttempt(50, 3, OUTSHOTDOUBLE), true, "should be true") +} + +// TestIsCheckoutAttempt_Any will check if the given dart is a checkout attempt +func TestIsCheckoutAttempt_Any(t *testing.T) { + dart := Dart{Value: null.IntFrom(20), Multiplier: 3} + assert.Equal(t, dart.IsCheckoutAttempt(20, 1, OUTSHOTANY), true, "should be true") + + dart = Dart{Value: null.IntFrom(20), Multiplier: 2} + assert.Equal(t, dart.IsCheckoutAttempt(40, 1, OUTSHOTANY), true, "should be true") + + dart = Dart{Value: null.IntFrom(20), Multiplier: 1} + assert.Equal(t, dart.IsCheckoutAttempt(60, 1, OUTSHOTANY), true, "should be true") +} + +// TestIsCheckoutAttempt_Master will check if the given dart is a checkout attempt +func TestIsCheckoutAttempt_Master(t *testing.T) { + dart := Dart{Value: null.IntFrom(20), Multiplier: 1} + assert.Equal(t, dart.IsCheckoutAttempt(23, 1, OUTSHOTMASTER), false, "should not be true") + + dart = Dart{Value: null.IntFrom(20), Multiplier: 1} + assert.Equal(t, dart.IsCheckoutAttempt(40, 1, OUTSHOTMASTER), true, "should be true") + + dart = Dart{Value: null.IntFrom(20), Multiplier: 1} + assert.Equal(t, dart.IsCheckoutAttempt(60, 1, OUTSHOTMASTER), true, "should be true") } // TestGetString will check that dart string is created correctly diff --git a/models/visit.go b/models/visit.go index f3d56447..3130c29f 100644 --- a/models/visit.go +++ b/models/visit.go @@ -69,15 +69,15 @@ func (visit Visit) ValidateInput() error { } // SetIsBust will set IsBust for the given visit -func (visit *Visit) SetIsBust(currentScore int) { +func (visit *Visit) SetIsBust(currentScore int, outshotTypeId int) { isBust := false - isBust = visit.FirstDart.IsBust(currentScore) + isBust = visit.FirstDart.IsBust(currentScore, outshotTypeId) currentScore = currentScore - visit.FirstDart.GetScore() if !isBust && currentScore > 0 { - isBust = visit.SecondDart.IsBust(currentScore) + isBust = visit.SecondDart.IsBust(currentScore, outshotTypeId) currentScore = currentScore - visit.SecondDart.GetScore() if !isBust && currentScore > 0 { - isBust = visit.ThirdDart.IsBust(currentScore) + isBust = visit.ThirdDart.IsBust(currentScore, outshotTypeId) } else { // Invalidate third dart if second was bust visit.ThirdDart.Value = null.IntFromPtr(nil) @@ -144,9 +144,22 @@ func (visit *Visit) SetIsBustAbove(currentScore int, targetScore int) { } // IsCheckout will check if the given visit is a checkout (remaining score is 0 and last dart thrown is a double) -func (visit Visit) IsCheckout(currentScore int) bool { +func (visit Visit) IsCheckout(currentScore int, outshotTypeId int) bool { remaining := currentScore - visit.GetScore() if remaining == 0 { + if outshotTypeId == OUTSHOTANY { + // With ANY outshot, as long as score is 0 we are good + return true + } else if outshotTypeId == OUTSHOTMASTER { + if visit.ThirdDart.Value.Valid { + return visit.ThirdDart.IsTriple() + } else if visit.SecondDart.Value.Valid { + return visit.SecondDart.IsTriple() + } else { + return visit.FirstDart.IsTriple() + } + } + // OUTSHOTMASTER can also be double, so we need to check outside the if if visit.ThirdDart.Value.Valid { return visit.ThirdDart.IsDouble() } else if visit.SecondDart.Value.Valid { diff --git a/models/visit_test.go b/models/visit_test.go new file mode 100644 index 00000000..4896025d --- /dev/null +++ b/models/visit_test.go @@ -0,0 +1,27 @@ +package models + +import ( + "testing" + + "github.com/guregu/null" + "github.com/stretchr/testify/assert" +) + +// TestIsCheckout will check that the given visit is checkout +func TestIsCheckout(t *testing.T) { + visit := Visit{FirstDart: &Dart{Value: null.IntFrom(20), Multiplier: 1}, + SecondDart: &Dart{Value: null.IntFrom(20), Multiplier: 1}, + ThirdDart: &Dart{Value: null.IntFrom(20), Multiplier: 1}} + assert.Equal(t, visit.IsCheckout(60, OUTSHOTANY), true, "should be checkout") + + visit = Visit{FirstDart: &Dart{Value: null.IntFrom(20), Multiplier: 1}, + SecondDart: &Dart{Value: null.IntFrom(20), Multiplier: 1}, + ThirdDart: &Dart{Value: null.IntFrom(20), Multiplier: 1}} + assert.Equal(t, visit.IsCheckout(60, OUTSHOTDOUBLE), false, "should not be checkout") + + visit = Visit{FirstDart: &Dart{Value: null.IntFrom(20), Multiplier: 3}, SecondDart: &Dart{}, ThirdDart: &Dart{}} + assert.Equal(t, visit.IsCheckout(60, OUTSHOTMASTER), true, "should be checkout") + + visit = Visit{FirstDart: &Dart{}, SecondDart: &Dart{Value: null.IntFrom(20), Multiplier: 2}, ThirdDart: &Dart{}} + assert.Equal(t, visit.IsCheckout(40, OUTSHOTDOUBLE), true, "should be checkout") +} From c6560ecdcc3fb196c3c6ffb6239a4fcf3574559b Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Mon, 17 Jul 2023 19:50:49 +0200 Subject: [PATCH 23/67] Correctly handle Master outshot --- models/dart.go | 2 +- models/dart_test.go | 3 +++ models/visit.go | 6 +++--- models/visit_test.go | 14 ++++++++++++++ 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/models/dart.go b/models/dart.go index c5856dd6..3d9f542a 100644 --- a/models/dart.go +++ b/models/dart.go @@ -33,7 +33,7 @@ func (dart *Dart) IsBust(currentScore int, outshotTypeId int) bool { if scoreAfterThrow == 0 { if outshotTypeId == OUTSHOTANY || (outshotTypeId == OUTSHOTDOUBLE && dart.IsDouble()) || - (outshotTypeId == OUTSHOTMASTER && dart.IsTriple()) { + (outshotTypeId == OUTSHOTMASTER && (dart.IsDouble() || dart.IsTriple())) { return false } } diff --git a/models/dart_test.go b/models/dart_test.go index 9666ead8..b2902e59 100644 --- a/models/dart_test.go +++ b/models/dart_test.go @@ -37,6 +37,9 @@ func TestIsBust_ScoreIsZero(t *testing.T) { dart = Dart{Value: null.IntFrom(3), Multiplier: 3} assert.Equal(t, dart.IsBust(9, OUTSHOTMASTER), false, "should not be bust") + + dart = Dart{Value: null.IntFrom(3), Multiplier: 2} + assert.Equal(t, dart.IsBust(6, OUTSHOTMASTER), false, "should not be bust") } // TestIsBustAbove will check that the given dart is bust above diff --git a/models/visit.go b/models/visit.go index 3130c29f..325621d3 100644 --- a/models/visit.go +++ b/models/visit.go @@ -152,11 +152,11 @@ func (visit Visit) IsCheckout(currentScore int, outshotTypeId int) bool { return true } else if outshotTypeId == OUTSHOTMASTER { if visit.ThirdDart.Value.Valid { - return visit.ThirdDart.IsTriple() + return visit.ThirdDart.IsDouble() || visit.ThirdDart.IsTriple() } else if visit.SecondDart.Value.Valid { - return visit.SecondDart.IsTriple() + return visit.SecondDart.IsDouble() || visit.SecondDart.IsTriple() } else { - return visit.FirstDart.IsTriple() + return visit.FirstDart.IsDouble() || visit.FirstDart.IsTriple() } } // OUTSHOTMASTER can also be double, so we need to check outside the if diff --git a/models/visit_test.go b/models/visit_test.go index 4896025d..497bfcdc 100644 --- a/models/visit_test.go +++ b/models/visit_test.go @@ -25,3 +25,17 @@ func TestIsCheckout(t *testing.T) { visit = Visit{FirstDart: &Dart{}, SecondDart: &Dart{Value: null.IntFrom(20), Multiplier: 2}, ThirdDart: &Dart{}} assert.Equal(t, visit.IsCheckout(40, OUTSHOTDOUBLE), true, "should be checkout") } + +// TestIsCheckout_Master will check that the given visit is checkout +func TestIsCheckout_Master(t *testing.T) { + visit := Visit{FirstDart: &Dart{Value: null.IntFrom(20), Multiplier: 2}, + SecondDart: &Dart{Value: null.IntFrom(10), Multiplier: 1}, + ThirdDart: &Dart{Value: null.IntFrom(5), Multiplier: 2}} + assert.Equal(t, visit.IsCheckout(60, OUTSHOTMASTER), true, "should be checkout") + + visit = Visit{FirstDart: &Dart{Value: null.IntFrom(14), Multiplier: 1}, SecondDart: &Dart{Value: null.IntFrom(7), Multiplier: 3}, ThirdDart: &Dart{}} + assert.Equal(t, visit.IsCheckout(35, OUTSHOTMASTER), true, "should be checkout") + + visit = Visit{FirstDart: &Dart{Value: null.IntFrom(3), Multiplier: 3}, SecondDart: &Dart{}, ThirdDart: &Dart{}} + assert.Equal(t, visit.IsCheckout(9, OUTSHOTMASTER), true, "should be checkout") +} From a30ec2c4bcc18f236da8e7466f3be38a3e2b0982 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Wed, 2 Aug 2023 21:21:32 +0200 Subject: [PATCH 24/67] Updated tournament generation --- cmd/serve.go | 2 + data/tournament.go | 335 +++++++++++++++++++-------------------------- 2 files changed, 141 insertions(+), 196 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index bb93d094..12b27b3b 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -117,6 +117,8 @@ var serveCmd = &cobra.Command{ router.HandleFunc("/venue/{id}/matches", controllers.GetActiveVenueMatches).Methods("GET") router.HandleFunc("/tournament", controllers.NewTournament).Methods("POST") + router.HandleFunc("/tournament/generate", controllers.GenerateTournament).Methods("POST") + router.HandleFunc("/tournament/generate/playoffs/{id}", controllers.GeneratePlayoffsTournament).Methods("POST") router.HandleFunc("/tournament", controllers.GetTournaments).Methods("GET") router.HandleFunc("/tournament/current", controllers.GetCurrentTournament).Methods("GET") router.HandleFunc("/tournament/current/{office_id}", controllers.GetCurrentTournamentForOffice).Methods("GET") diff --git a/data/tournament.go b/data/tournament.go index 2240cfd3..48b0683a 100644 --- a/data/tournament.go +++ b/data/tournament.go @@ -787,7 +787,7 @@ func NewTournament(tournament models.Tournament) (*models.Tournament, error) { return GetTournament(int(tournamentID)) } -// GenerateTournament will generate a new tournament +// GenerateTournament generates a new tournament func GenerateTournament(input models.Tournament) (*models.Tournament, error) { officeID := input.OfficeID tournament, err := NewTournament(models.Tournament{ @@ -804,7 +804,7 @@ func GenerateTournament(input models.Tournament) (*models.Tournament, error) { } players := input.Players - mt := models.MatchType{ID: 1} + mt := models.MatchType{ID: models.X01} mo := models.MatchMode{ID: 2} for i := 0; i < len(players); i++ { for j := i + 1; j < len(players); j++ { @@ -812,6 +812,7 @@ func GenerateTournament(input models.Tournament) (*models.Tournament, error) { // Only generate matches for people in the same group continue } + match, err := NewMatch(models.Match{ MatchType: &mt, MatchMode: &mo, @@ -820,7 +821,9 @@ func GenerateTournament(input models.Tournament) (*models.Tournament, error) { IsPractice: false, TournamentID: null.IntFrom(int64(tournament.ID)), Players: []int{players[i].PlayerID, players[j].PlayerID}, - Legs: []*models.Leg{{StartingScore: 501}}, + Legs: []*models.Leg{{ + StartingScore: 501, + Parameters: &models.LegParameters{OutshotType: &models.OutshotType{ID: models.OUTSHOTDOUBLE}}}}, }) if err != nil { return nil, err @@ -832,7 +835,7 @@ func GenerateTournament(input models.Tournament) (*models.Tournament, error) { return tournament, nil } -// GeneratePlayoffsTournament will generate playoffs matches for the given tournament +// GeneratePlayoffsTournament generates playoffs matches for the given tournament func GeneratePlayoffsTournament(tournamentID int) (*models.Tournament, error) { tournament, err := GetTournament(tournamentID) if err != nil { @@ -844,6 +847,7 @@ func GeneratePlayoffsTournament(tournamentID int) (*models.Tournament, error) { return nil, err } + // Get all players for the tournament keys := make([]int, 0) for key := range overview { keys = append(keys, key) @@ -852,7 +856,6 @@ func GeneratePlayoffsTournament(tournamentID int) (*models.Tournament, error) { group1 := overview[keys[0]] group2 := overview[keys[1]] - // TODO Make this configurable preset := tournament.Preset playoffsGroupID := preset.PlayoffsTournamentGroup.ID mt := preset.MatchType @@ -861,34 +864,15 @@ func GeneratePlayoffsTournament(tournamentID int) (*models.Tournament, error) { placeholderAwayID := preset.PlayerIDPlaceholderAway startingScore := preset.StartingScore - // Generate Player to tournament players := make([]*models.Player2Tournament, 0) - for i := 0; i < len(group1); i++ { - player := group1[i] - players = append(players, &models.Player2Tournament{ - PlayerID: player.PlayerID, - TournamentGroupID: playoffsGroupID, - }) - } - for i := 0; i < len(group2); i++ { - player := group2[i] - players = append(players, &models.Player2Tournament{ - PlayerID: player.PlayerID, - TournamentGroupID: playoffsGroupID, - }) - } - players = append(players, &models.Player2Tournament{ - PlayerID: walkoverPlayerID, - TournamentGroupID: playoffsGroupID, - }) - players = append(players, &models.Player2Tournament{ - PlayerID: placeholderHomeID, - TournamentGroupID: playoffsGroupID, - }) - players = append(players, &models.Player2Tournament{ - PlayerID: placeholderAwayID, - TournamentGroupID: playoffsGroupID, - }) + for _, groupPlayer := range append(group1, group2...) { + players = append(players, &models.Player2Tournament{PlayerID: groupPlayer.PlayerID, TournamentGroupID: playoffsGroupID}) + } + // Add all the placeholder players + players = append(players, + &models.Player2Tournament{PlayerID: walkoverPlayerID, TournamentGroupID: playoffsGroupID}, + &models.Player2Tournament{PlayerID: placeholderHomeID, TournamentGroupID: playoffsGroupID}, + &models.Player2Tournament{PlayerID: placeholderAwayID, TournamentGroupID: playoffsGroupID}) playoffs, err := NewTournament(models.Tournament{ Name: tournament.Name + " Playoffs", @@ -908,6 +892,7 @@ func GeneratePlayoffsTournament(tournamentID int) (*models.Tournament, error) { return nil, err } + matches := make([]*models.Match, 15) // Create initial 8 matches for i := 0; i < 8; i++ { temp := models.TournamentTemplateLast16[i] @@ -922,220 +907,178 @@ func GeneratePlayoffsTournament(tournamentID int) (*models.Tournament, error) { away = walkoverPlayerID } // TODO set is_bye - match, err := NewMatch(models.Match{ - MatchType: mt, - MatchMode: preset.MatchModeLast16, - //VenueID: 1, - OfficeID: null.IntFrom(int64(tournament.OfficeID)), - IsPractice: false, - TournamentID: null.IntFrom(int64(playoffs.ID)), - Players: []int{home, away}, - Legs: []*models.Leg{{StartingScore: startingScore}}, - }) + match, err := createTournamentMatch(playoffs.ID, []int{home, away}, startingScore, 1, + tournament.OfficeID, mt, preset.MatchModeLast16) if err != nil { return nil, err } - log.Printf("Generated Match %d for %d vs %d", match.ID, home, away) + matches = append(matches, match) } // Create Quarter Final Matches for i := 0; i < 4; i++ { - match, err := NewMatch(models.Match{ - MatchType: mt, - MatchMode: preset.MatchModeQuarterFinal, - //VenueID: 1, - OfficeID: null.IntFrom(int64(tournament.OfficeID)), - IsPractice: false, - TournamentID: null.IntFrom(int64(playoffs.ID)), - Players: []int{placeholderHomeID, placeholderAwayID}, - Legs: []*models.Leg{{StartingScore: startingScore}}, - }) + match, err := createTournamentMatch(playoffs.ID, []int{placeholderHomeID, placeholderAwayID}, startingScore, 1, + tournament.OfficeID, mt, preset.MatchModeQuarterFinal) if err != nil { return nil, err } - log.Printf("Generated Match %d for %d vs %d", match.ID, placeholderHomeID, placeholderAwayID) + matches = append(matches, match) } // Create Semi Final Matches for i := 0; i < 2; i++ { - match, err := NewMatch(models.Match{ - MatchType: mt, - MatchMode: preset.MatchModeSemiFinal, - //VenueID: 1, - OfficeID: null.IntFrom(int64(tournament.OfficeID)), - IsPractice: false, - TournamentID: null.IntFrom(int64(playoffs.ID)), - Players: []int{placeholderHomeID, placeholderAwayID}, - Legs: []*models.Leg{{StartingScore: startingScore}}, - }) + match, err := createTournamentMatch(playoffs.ID, []int{placeholderHomeID, placeholderAwayID}, startingScore, 1, + tournament.OfficeID, mt, preset.MatchModeSemiFinal) if err != nil { return nil, err } - log.Printf("Generated Match %d for %d vs %d", match.ID, placeholderHomeID, placeholderAwayID) + matches = append(matches, match) } // Create Grand Final - match, err := NewMatch(models.Match{ - MatchType: mt, - MatchMode: preset.MatchModeGrandFinal, - //VenueID: 1, - OfficeID: null.IntFrom(int64(tournament.OfficeID)), - IsPractice: false, - TournamentID: null.IntFrom(int64(playoffs.ID)), - Players: []int{placeholderHomeID, placeholderAwayID}, - Legs: []*models.Leg{{StartingScore: startingScore}}, - }) + match, err := createTournamentMatch(playoffs.ID, []int{placeholderHomeID, placeholderAwayID}, startingScore, 1, + tournament.OfficeID, mt, preset.MatchModeGrandFinal) if err != nil { return nil, err } - log.Printf("Generated Match %d for %d vs %d", match.ID, placeholderHomeID, placeholderAwayID) + matches = append(matches, match) // Insert Match Metadata for each match created - matches, err := GetTournamentMatches(playoffs.ID) - if err != nil { - return nil, err + gf := matches[14] + sf1 := matches[13] + sf2 := matches[12] + qf1 := matches[11] + qf2 := matches[10] + qf3 := matches[9] + qf4 := matches[8] + m8 := matches[7] + m7 := matches[6] + m6 := matches[5] + m5 := matches[4] + m4 := matches[3] + m3 := matches[2] + m2 := matches[1] + m1 := matches[0] + + playoffMatches := []struct { + MatchID int + OrderOfPlay int + GroupID int + DisplayName string + WinnerMatchID *int + IsWinnerHome bool + IsGrandFinal null.Bool + IsSemiFinal null.Bool + }{ + {MatchID: gf.ID, OrderOfPlay: 15, GroupID: playoffsGroupID, DisplayName: "Grand Final", WinnerMatchID: nil, IsWinnerHome: false, IsGrandFinal: null.BoolFrom(true)}, + {MatchID: sf1.ID, OrderOfPlay: 14, GroupID: playoffsGroupID, DisplayName: "Semi Final 1", WinnerMatchID: &gf.ID, IsWinnerHome: true, IsSemiFinal: null.BoolFrom(true)}, + {MatchID: sf2.ID, OrderOfPlay: 13, GroupID: playoffsGroupID, DisplayName: "Semi Final 2", WinnerMatchID: &gf.ID, IsWinnerHome: false, IsSemiFinal: null.BoolFrom(true)}, + {MatchID: qf1.ID, OrderOfPlay: 12, GroupID: playoffsGroupID, DisplayName: "Quarter Final 1", WinnerMatchID: &sf1.ID, IsWinnerHome: true}, + {MatchID: qf2.ID, OrderOfPlay: 11, GroupID: playoffsGroupID, DisplayName: "Quarter Final 2", WinnerMatchID: &sf1.ID, IsWinnerHome: false}, + {MatchID: qf3.ID, OrderOfPlay: 10, GroupID: playoffsGroupID, DisplayName: "Quarter Final 3", WinnerMatchID: &sf2.ID, IsWinnerHome: true}, + {MatchID: qf4.ID, OrderOfPlay: 9, GroupID: playoffsGroupID, DisplayName: "Quarter Final 4", WinnerMatchID: &sf2.ID, IsWinnerHome: false}, + {MatchID: m1.ID, OrderOfPlay: 1, GroupID: playoffsGroupID, DisplayName: "Match 1", WinnerMatchID: &qf1.ID, IsWinnerHome: true}, + {MatchID: m2.ID, OrderOfPlay: 2, GroupID: playoffsGroupID, DisplayName: "Match 2", WinnerMatchID: &qf1.ID, IsWinnerHome: false}, + {MatchID: m3.ID, OrderOfPlay: 3, GroupID: playoffsGroupID, DisplayName: "Match 3", WinnerMatchID: &qf2.ID, IsWinnerHome: true}, + {MatchID: m4.ID, OrderOfPlay: 4, GroupID: playoffsGroupID, DisplayName: "Match 4", WinnerMatchID: &qf2.ID, IsWinnerHome: false}, + {MatchID: m5.ID, OrderOfPlay: 5, GroupID: playoffsGroupID, DisplayName: "Match 5", WinnerMatchID: &qf3.ID, IsWinnerHome: true}, + {MatchID: m6.ID, OrderOfPlay: 6, GroupID: playoffsGroupID, DisplayName: "Match 6", WinnerMatchID: &qf3.ID, IsWinnerHome: false}, + {MatchID: m7.ID, OrderOfPlay: 7, GroupID: playoffsGroupID, DisplayName: "Match 7", WinnerMatchID: &qf4.ID, IsWinnerHome: true}, + {MatchID: m8.ID, OrderOfPlay: 8, GroupID: playoffsGroupID, DisplayName: "Match 8", WinnerMatchID: &qf4.ID, IsWinnerHome: false}, } - gf := matches[playoffsGroupID][0] - sf1 := matches[playoffsGroupID][1] - sf2 := matches[playoffsGroupID][2] - qf1 := matches[playoffsGroupID][3] - qf2 := matches[playoffsGroupID][4] - qf3 := matches[playoffsGroupID][5] - qf4 := matches[playoffsGroupID][6] - m1 := matches[playoffsGroupID][14] - m2 := matches[playoffsGroupID][13] - m3 := matches[playoffsGroupID][12] - m4 := matches[playoffsGroupID][11] - m5 := matches[playoffsGroupID][10] - m6 := matches[playoffsGroupID][9] - m7 := matches[playoffsGroupID][8] - m8 := matches[playoffsGroupID][7] - tx, err := models.DB.Begin() if err != nil { return nil, err } - stmt, err := tx.Prepare(`INSERT INTO match_metadata (match_id, order_of_play, tournament_group_id, match_displayname, elimination, trophy, semi_final, grand_final, winner_outcome_match_id, is_winner_outcome_home) VALUES (?, ?, ?, ?, 1, 0, 0, 1, ?, ?)`) - if err != nil { - tx.Rollback() - return nil, err - } - _, err = stmt.Exec(gf.ID, 15, playoffsGroupID, "Grand Final", nil, 0) - if err != nil { - tx.Rollback() - return nil, err - } - _, err = stmt.Exec(sf1.ID, 14, playoffsGroupID, "Semi Final 1", gf.ID, 1) - if err != nil { - tx.Rollback() - return nil, err - } - _, err = stmt.Exec(sf2.ID, 13, playoffsGroupID, "Semi Final 2", gf.ID, 0) - if err != nil { - tx.Rollback() - return nil, err - } - _, err = stmt.Exec(qf1.ID, 12, playoffsGroupID, "Quarter Final 1", sf1.ID, 1) - if err != nil { - tx.Rollback() - return nil, err - } - _, err = stmt.Exec(qf2.ID, 11, playoffsGroupID, "Quarter Final 2", sf1.ID, 0) - if err != nil { - tx.Rollback() - return nil, err - } - _, err = stmt.Exec(qf3.ID, 10, playoffsGroupID, "Quarter Final 3", sf2.ID, 1) - if err != nil { - tx.Rollback() - return nil, err - } - _, err = stmt.Exec(qf4.ID, 9, playoffsGroupID, "Quarter Final 4", sf2.ID, 0) - if err != nil { - tx.Rollback() - return nil, err - } - _, err = stmt.Exec(m1.ID, 1, playoffsGroupID, "Match 1", qf1.ID, 1) - if err != nil { - tx.Rollback() - return nil, err - } - _, err = stmt.Exec(m2.ID, 2, playoffsGroupID, "Match 2", qf1.ID, 0) - if err != nil { - tx.Rollback() - return nil, err - } - _, err = stmt.Exec(m3.ID, 3, playoffsGroupID, "Match 3", qf2.ID, 1) - if err != nil { - tx.Rollback() - return nil, err - } - _, err = stmt.Exec(m4.ID, 4, playoffsGroupID, "Match 4", qf2.ID, 0) + stmt, err := tx.Prepare(`INSERT INTO match_metadata (match_id, order_of_play, tournament_group_id, match_displayname, elimination, trophy, semi_final, + grand_final, winner_outcome_match_id, is_winner_outcome_home) VALUES (?, ?, ?, ?, 1, 0, ?, ?, ?, ?)`) if err != nil { tx.Rollback() return nil, err } - _, err = stmt.Exec(m5.ID, 5, playoffsGroupID, "Match 5", qf3.ID, 1) - if err != nil { - tx.Rollback() - return nil, err - } - _, err = stmt.Exec(m6.ID, 6, playoffsGroupID, "Match 6", qf3.ID, 0) - if err != nil { - tx.Rollback() - return nil, err - } - _, err = stmt.Exec(m7.ID, 7, playoffsGroupID, "Match 7", qf4.ID, 1) - if err != nil { - tx.Rollback() - return nil, err + for _, metadata := range playoffMatches { + var winnerOutcomeMatchID *int + if metadata.WinnerMatchID != nil { + winnerOutcomeMatchID = metadata.WinnerMatchID + } + _, err = stmt.Exec(metadata.MatchID, metadata.OrderOfPlay, metadata.GroupID, metadata.DisplayName, metadata.IsSemiFinal, metadata.IsGrandFinal, + winnerOutcomeMatchID, metadata.IsWinnerHome) + if err != nil { + tx.Rollback() + return nil, err + } } - _, err = stmt.Exec(m8.ID, 8, playoffsGroupID, "Match 8", qf4.ID, 0) tx.Commit() - if err != nil { - return nil, err - } - for _, match := range matches[playoffsGroupID] { + for _, match := range matches { if match.Players[0] == walkoverPlayerID || match.Players[1] == walkoverPlayerID { - // This is a Bye, so we need to finish it winnerID := match.Players[0] if match.Players[0] == walkoverPlayerID { winnerID = match.Players[1] } + // This is a Bye, so we need to finish it + err = FinishByeMatch(match, winnerID) + if err != nil { + return nil, err + } + } + } + return nil, nil +} - if match.TournamentID.Valid { - metadata, err := GetMatchMetadata(match.ID) - if err != nil { - return nil, err - } +func createTournamentMatch(tournamentID int, players []int, startingScore int, venueID int, officeID int, matchType *models.MatchType, matchMode *models.MatchMode) (*models.Match, error) { + match, err := NewMatch(models.Match{ + MatchType: matchType, + MatchMode: matchMode, + //VenueID: null.IntFrom(int64(venueID)), + OfficeID: null.IntFrom(int64(officeID)), + IsPractice: false, + TournamentID: null.IntFrom(int64(tournamentID)), + Players: players, + Legs: []*models.Leg{{StartingScore: startingScore}}, + }) + if err != nil { + return nil, err + } + log.Printf("Generated Match %d for %d vs %d", match.ID, players[0], players[1]) - if metadata.WinnerOutcomeMatchID.Valid { - winnerMatch, err := GetMatch(int(metadata.WinnerOutcomeMatchID.Int64)) - if err != nil { - return nil, err - } - idx := 0 - if !metadata.IsWinnerOutcomeHome { - idx = 1 - } - err = SwapPlayers(winnerMatch.ID, winnerID, winnerMatch.Players[idx]) - if err != nil { - return nil, err - } - } - } - _, err = models.DB.Exec(`UPDATE leg SET is_finished = 1, end_time = NOW() WHERE match_id = ?`, match.ID) + return match, nil +} + +// FinishByeMatch will finish a given match, and mov +func FinishByeMatch(match *models.Match, winnerID int) error { + if match.TournamentID.Valid { + metadata, err := GetMatchMetadata(match.ID) + if err != nil { + return err + } + + if metadata.WinnerOutcomeMatchID.Valid { + winnerMatch, err := GetMatch(int(metadata.WinnerOutcomeMatchID.Int64)) if err != nil { - return nil, err + return err } - _, err = models.DB.Exec(`UPDATE matches SET is_finished = 1, is_bye = 1, is_walkover = 1 WHERE id = ?`, match.ID) + idx := 0 + if !metadata.IsWinnerOutcomeHome { + idx = 1 + } + err = SwapPlayers(winnerMatch.ID, winnerID, winnerMatch.Players[idx]) if err != nil { - return nil, err + return err } } } - return nil, nil + + // Finish the Bye match + _, err := models.DB.Exec(`UPDATE leg SET is_finished = 1, end_time = NOW(), has_score = 0 WHERE match_id = ?`, match.ID) + if err != nil { + return err + } + _, err = models.DB.Exec(`UPDATE matches SET is_finished = 1, is_bye = 1, is_walkover = 1 WHERE id = ?`, match.ID) + if err != nil { + return err + } + return nil } // GetTournamentMatchesForPlayer will return all tournament matches for the given player and tournament From 0ff93da63f4e37a94fc6cf362b548986fc949a02 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Sun, 6 Aug 2023 11:42:59 +0200 Subject: [PATCH 25/67] Initial backend support for badges --- cmd/serve.go | 79 ++++++++++++++++++++++++++++++++ controllers/badge_controller.go | 21 +++++++++ controllers/player_controller.go | 21 +++++++++ data/badge.go | 33 +++++++++++++ data/player.go | 42 +++++++++++++++++ models/badge.go | 23 ++++++++++ 6 files changed, 219 insertions(+) create mode 100644 controllers/badge_controller.go create mode 100644 data/badge.go create mode 100644 models/badge.go diff --git a/cmd/serve.go b/cmd/serve.go index 166d5645..40da2a95 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -1,9 +1,11 @@ package cmd import ( + "database/sql" "fmt" "log" "net/http" + "time" "github.com/gorilla/mux" "github.com/kcapp/api/controllers" @@ -23,6 +25,7 @@ var serveCmd = &cobra.Command{ panic(err) } models.InitDB(config.GetMysqlConnectionString()) + GetLongestWinStreak() router := mux.NewRouter() router.Methods("OPTIONS").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -73,6 +76,7 @@ var serveCmd = &cobra.Command{ router.HandleFunc("/player/{id}/progression", controllers.GetPlayerProgression).Methods("GET") router.HandleFunc("/player/{id}/checkouts", controllers.GetPlayerCheckouts).Methods("GET") router.HandleFunc("/player/{id}/tournament", controllers.GetPlayerTournamentStandings).Methods("GET") + router.HandleFunc("/player/{id}/badges", controllers.GetPlayerBadges).Methods("GET") router.HandleFunc("/player/{id}/elo/{start}/{limit}", controllers.GetPlayerEloChangelog).Methods("GET") router.HandleFunc("/player/{player_1}/vs/{player_2}", controllers.GetPlayerHeadToHead).Methods("GET") router.HandleFunc("/player/{player_1}/vs/{player_2}/simulate", controllers.SimulateMatch).Methods("PUT") @@ -135,11 +139,86 @@ var serveCmd = &cobra.Command{ router.HandleFunc("/tournament/{id}/probabilities", controllers.GetTournamentProbabilities).Methods("GET") router.HandleFunc("/tournament/match/{id}/probabilities", controllers.GetMatchProbabilities).Methods("GET") + router.HandleFunc("/badge", controllers.GetBadges).Methods("GET") + log.Printf("Listening on port %d", config.APIConfig.Port) log.Println(http.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", config.APIConfig.Port), router)) }, } +func GetLongestWinStreak() { + // Establish a connection to the MySQL database + db, err := sql.Open("mysql", "developer:abcd1234@tcp(localhost:3306)/kcapp") + if err != nil { + log.Fatal(err) + } + defer db.Close() + + // Set maximum number of open and idle connections + db.SetMaxOpenConns(10) // Adjust the value based on your MySQL server configuration + db.SetMaxIdleConns(5) // Adjust the value based on your MySQL server configuration + + // Set a maximum connection lifetime + db.SetConnMaxLifetime(5 * time.Minute) // Adjust the value based on your application requirements + + // Get a list of all players + players := []int{1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 35, 36, 37, 38, 39, 40, 42, 44, 46, 52, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 66, 67, 68, 69, 70, 76, 79, 82, 83, 84, 90, 97, 99, 102, 103, 106, 107, 108, 109, 110, 111, 112, 113, 114, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 135, 139, 141, 142, 143, 144, 148, 151, 152, 157, 159, 160, 163, 165, 168, 169, 175, 176, 177, 178, 179, 180, 181, 183, 184, 185, 187, 188, 191, 192, 194, 196, 198, 199, 200, 202, 206, 207, 208, 209, 210, 213, 217, 219, 221, 222, 223, 224, 225, 226, 229, 236, 237, 238, 239, 240, 250, 252, 253, 254, 255, 256, 257, 258, 260, 261, 262, 264, 276, 277, 278, 281, 286, 287, 290, 292, 296, 297, 298, 303, 304, 306, 307, 308, 311, 313, 314, 318, 322, 331, 332, 333, 334, 343, 344, 345, 347, 350, 361, 363, 364, 365, 366, 368, 371, 376, 380, 385, 387, 389, 401, 402, 409, 411, 412, 413, 414, 417, 418, 420, 422, 428, 429, 432, 435} // Replace with your actual list of player IDs + + // Initialize variables to track the longest win streak + longestStreak := 0 + playerWithLongestStreak := 0 + + // Iterate over each player + for _, playerID := range players { + // Dynamically update the query + query := fmt.Sprintf(` + SELECT + winner_id, + MAX(streak) AS longest_streak + FROM ( + SELECT + winner_id, + @streak := IF(@prev_winner = winner_id, @streak + 1, 1) AS streak, + @prev_winner := winner_id + FROM + (SELECT @streak := 0, @prev_winner := NULL) AS vars, + (SELECT * FROM matches m WHERE id IN (SELECT DISTINCT match_id FROM player2leg WHERE player_id = %d) AND is_finished = 1 AND match_type_id = 1 ORDER BY updated_at) AS m + ) AS streaks + WHERE winner_id = %d + GROUP BY winner_id + ORDER BY longest_streak DESC + `, playerID, playerID) + + // Execute the query + rows, err := db.Query(query) + if err != nil { + log.Fatal(err) + } + defer rows.Close() + + // Fetch the result + var winnerID, streak int + if rows.Next() { + err := rows.Scan(&winnerID, &streak) + if err != nil { + log.Fatal(err) + } + + // Check if the player has a longer streak than the current longest + if streak > longestStreak { + longestStreak = streak + playerWithLongestStreak = winnerID + } + } + rows.Close() + if err := rows.Err(); err != nil { + log.Fatal(err) + } + fmt.Printf("Player ID = %d, Streak = %d\n", winnerID, streak) + } + fmt.Printf("Player with the longest win streak: Player ID = %d, Streak = %d\n", playerWithLongestStreak, longestStreak) +} + func init() { rootCmd.AddCommand(serveCmd) } diff --git a/controllers/badge_controller.go b/controllers/badge_controller.go new file mode 100644 index 00000000..4e7dbaf1 --- /dev/null +++ b/controllers/badge_controller.go @@ -0,0 +1,21 @@ +package controllers + +import ( + "encoding/json" + "log" + "net/http" + + "github.com/kcapp/api/data" +) + +func GetBadges(w http.ResponseWriter, r *http.Request) { + SetHeaders(w) + badges, err := data.GetBadges() + if err != nil { + log.Println("Unable to get badges") + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(badges) +} diff --git a/controllers/player_controller.go b/controllers/player_controller.go index 801bd849..74cc3e32 100644 --- a/controllers/player_controller.go +++ b/controllers/player_controller.go @@ -700,6 +700,27 @@ func GetPlayerTournamentStandings(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(standings) } +// GetPlayerBadges returns all badges for a given player +func GetPlayerBadges(w http.ResponseWriter, r *http.Request) { + SetHeaders(w) + params := mux.Vars(r) + id, err := strconv.Atoi(params["id"]) + if err != nil { + log.Println("Invalid id parameter") + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + badges, err := data.GetPlayerBadges(id) + if err != nil { + log.Println("Unable to get player badges") + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(badges) +} + // GetPlayerHeadToHead will return head to head statistics between the given players func GetPlayerHeadToHead(w http.ResponseWriter, r *http.Request) { SetHeaders(w) diff --git a/data/badge.go b/data/badge.go new file mode 100644 index 00000000..03398677 --- /dev/null +++ b/data/badge.go @@ -0,0 +1,33 @@ +package data + +import "github.com/kcapp/api/models" + +func GetBadges() ([]*models.Badge, error) { + rows, err := models.DB.Query(` + SELECT + b.id, + b.name, + b.description, + b.filename + FROM badge b`) + if err != nil { + return nil, err + } + defer rows.Close() + + badges := make([]*models.Badge, 0) + for rows.Next() { + badge := new(models.Badge) + err := rows.Scan( + &badge.ID, + &badge.Name, + &badge.Description, + &badge.Filename, + ) + if err != nil { + return nil, err + } + badges = append(badges, badge) + } + return badges, nil +} diff --git a/data/player.go b/data/player.go index 5c7b894c..aa3cd20c 100644 --- a/data/player.go +++ b/data/player.go @@ -869,6 +869,48 @@ func GetPlayerTournamentStandings(playerID int) ([]*models.PlayerTournamentStand return standings, nil } +func GetPlayerBadges(playerID int) ([]*models.PlayerBadge, error) { + rows, err := models.DB.Query(` + SELECT + b.id, + b.name, + b.description, + b.filename, + p2b.player_id, + p2b.leg_id, + p2b.created_at + FROM player2badge p2b + LEFT JOIN badge b ON b.id = p2b.badge_id + WHERE p2b.player_id = ?`, playerID) + if err != nil { + return nil, err + } + defer rows.Close() + + badges := make([]*models.PlayerBadge, 0) + for rows.Next() { + badge := new(models.PlayerBadge) + badge.Badge = new(models.Badge) + err := rows.Scan( + &badge.Badge.ID, + &badge.Badge.Name, + &badge.Badge.Description, + &badge.Badge.Filename, + &badge.PlayerID, + &badge.LegID, + &badge.CreatedAt, + ) + if err != nil { + return nil, err + } + badges = append(badges, badge) + } + if err = rows.Err(); err != nil { + return nil, err + } + return badges, nil +} + // GetPlayerOfficialMatches will return an overview of all official matches for the given player func GetPlayerOfficialMatches(playerID int) ([]*models.Match, error) { rows, err := models.DB.Query(` diff --git a/models/badge.go b/models/badge.go new file mode 100644 index 00000000..c4d5ecec --- /dev/null +++ b/models/badge.go @@ -0,0 +1,23 @@ +package models + +import ( + "time" + + "github.com/guregu/null" +) + +// Badge represents a badge model. +type Badge struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Filename string `json:"filename"` +} + +// PlayerBadge represents a Player2Badge model. +type PlayerBadge struct { + Badge *Badge `json:"badge"` + PlayerID int `json:"player_id"` + LegID null.Int `json:"leg_id,omitempty"` + CreatedAt time.Time `json:"created_at"` +} From 51a1c4ff6cbfcc8afba35a47bed248c35cbba1a8 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Sun, 6 Aug 2023 14:11:27 +0200 Subject: [PATCH 26/67] Added tournament groups to tournament preset --- data/tournament.go | 3 ++- data/tournament_preset.go | 20 +++++++++++++++----- models/tournament.go | 2 ++ 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/data/tournament.go b/data/tournament.go index 48b0683a..07587c65 100644 --- a/data/tournament.go +++ b/data/tournament.go @@ -759,7 +759,7 @@ func NewTournament(tournament models.Tournament) (*models.Tournament, error) { res, err := tx.Exec(` INSERT INTO tournament (name, short_name, is_finished, is_playoffs, playoffs_tournament_id, preset_id, office_id, start_time, end_time) VALUES - (?, ?, ?, ?, ?, ?, ?, ?, ?)`, tournament.Name, tournament.ShortName, 0, tournament.IsPlayoffs, tournament.PresetID, tournament.PlayoffsTournamentID, + (?, ?, ?, ?, ?, ?, ?, ?, ?)`, tournament.Name, tournament.ShortName, 0, tournament.IsPlayoffs, tournament.PlayoffsTournamentID, tournament.PresetID, tournament.OfficeID, tournament.StartTime, tournament.EndTime) if err != nil { tx.Rollback() @@ -795,6 +795,7 @@ func GenerateTournament(input models.Tournament) (*models.Tournament, error) { ShortName: input.ShortName, IsPlayoffs: false, OfficeID: officeID, + PresetID: input.PresetID, Players: input.Players, StartTime: null.TimeFrom(time.Now()), EndTime: null.TimeFrom(time.Now()), diff --git a/data/tournament_preset.go b/data/tournament_preset.go index 77cc0642..44ede358 100644 --- a/data/tournament_preset.go +++ b/data/tournament_preset.go @@ -14,7 +14,7 @@ func GetTournamentPresets() ([]*models.TournamentPreset, error) { mmqf.id, mmqf.name, mmqf.short_name, mmsf.id, mmsf.name, mmsf.short_name, mmgf.id, mmgf.name, mmgf.short_name, - tg.id, tg.name, + tg.id, tg.name, tg1.id, tg1.name, tg2.id, tg2.name, tp.player_id_walkover, tp.player_id_placeholder_home, tp.player_id_placeholder_away FROM tournament_preset tp JOIN match_type mt ON mt.id = tp.match_type_id @@ -22,7 +22,9 @@ func GetTournamentPresets() ([]*models.TournamentPreset, error) { JOIN match_mode mmqf ON mmqf.id = tp.match_mode_id_quarter_final JOIN match_mode mmsf ON mmsf.id = tp.match_mode_id_semi_final JOIN match_mode mmgf ON mmgf.id = tp.match_mode_id_grand_final - JOIN tournament_group tg ON tg.id = tp.playoffs_tournament_group_id`) + JOIN tournament_group tg ON tg.id = tp.playoffs_tournament_group_id + JOIN tournament_group tg1 ON tg1.id = tp.group1_tournament_group_id + JOIN tournament_group tg2 ON tg2.id = tp.group2_tournament_group_id`) if err != nil { return nil, err } @@ -37,13 +39,16 @@ func GetTournamentPresets() ([]*models.TournamentPreset, error) { tp.MatchModeGrandFinal = new(models.MatchMode) tp.MatchType = new(models.MatchType) tp.PlayoffsTournamentGroup = new(models.TournamentGroup) + tp.Group1TournamentGroup = new(models.TournamentGroup) + tp.Group2TournamentGroup = new(models.TournamentGroup) err := rows.Scan(&tp.ID, &tp.Name, &tp.StartingScore, &tp.Description, &tp.MatchType.ID, &tp.MatchType.Name, &tp.MatchModeLast16.ID, &tp.MatchModeLast16.Name, &tp.MatchModeLast16.ShortName, &tp.MatchModeQuarterFinal.ID, &tp.MatchModeQuarterFinal.Name, &tp.MatchModeQuarterFinal.ShortName, &tp.MatchModeSemiFinal.ID, &tp.MatchModeSemiFinal.Name, &tp.MatchModeSemiFinal.ShortName, &tp.MatchModeGrandFinal.ID, &tp.MatchModeGrandFinal.Name, &tp.MatchModeGrandFinal.ShortName, - &tp.PlayoffsTournamentGroup.ID, &tp.PlayoffsTournamentGroup.Name, + &tp.PlayoffsTournamentGroup.ID, &tp.PlayoffsTournamentGroup.Name, &tp.Group1TournamentGroup.ID, + &tp.Group1TournamentGroup.Name, &tp.Group2TournamentGroup.ID, &tp.Group2TournamentGroup.Name, &tp.PlayerIDWalkover, &tp.PlayerIDPlaceholderHome, &tp.PlayerIDPlaceholderAway) if err != nil { return nil, err @@ -66,6 +71,8 @@ func GetTournamentPreset(id int) (*models.TournamentPreset, error) { tp.MatchModeGrandFinal = new(models.MatchMode) tp.MatchType = new(models.MatchType) tp.PlayoffsTournamentGroup = new(models.TournamentGroup) + tp.Group1TournamentGroup = new(models.TournamentGroup) + tp.Group2TournamentGroup = new(models.TournamentGroup) err := models.DB.QueryRow(` SELECT tp.id, tp.name, tp.starting_score, tp.description, @@ -74,7 +81,7 @@ func GetTournamentPreset(id int) (*models.TournamentPreset, error) { mmqf.id, mmqf.name, mmqf.short_name, mmsf.id, mmsf.name, mmsf.short_name, mmgf.id, mmgf.name, mmgf.short_name, - tg.id, tg.name, + tg.id, tg.name, tg1.id, tg1.name, tg2.id, tg2.name, tp.player_id_walkover, tp.player_id_placeholder_home, tp.player_id_placeholder_away FROM tournament_preset tp JOIN match_type mt ON mt.id = tp.match_type_id @@ -83,13 +90,16 @@ func GetTournamentPreset(id int) (*models.TournamentPreset, error) { JOIN match_mode mmsf ON mmsf.id = tp.match_mode_id_semi_final JOIN match_mode mmgf ON mmgf.id = tp.match_mode_id_grand_final JOIN tournament_group tg ON tg.id = tp.playoffs_tournament_group_id + JOIN tournament_group tg1 ON tg1.id = tp.group1_tournament_group_id + JOIN tournament_group tg2 ON tg2.id = tp.group2_tournament_group_id WHERE tp.id = ?`, id). Scan(&tp.ID, &tp.Name, &tp.StartingScore, &tp.Description, &tp.MatchType.ID, &tp.MatchType.Name, &tp.MatchModeLast16.ID, &tp.MatchModeLast16.Name, &tp.MatchModeLast16.ShortName, &tp.MatchModeQuarterFinal.ID, &tp.MatchModeQuarterFinal.Name, &tp.MatchModeQuarterFinal.ShortName, &tp.MatchModeSemiFinal.ID, &tp.MatchModeSemiFinal.Name, &tp.MatchModeSemiFinal.ShortName, &tp.MatchModeGrandFinal.ID, &tp.MatchModeGrandFinal.Name, &tp.MatchModeGrandFinal.ShortName, - &tp.PlayoffsTournamentGroup.ID, &tp.PlayoffsTournamentGroup.Name, + &tp.PlayoffsTournamentGroup.ID, &tp.PlayoffsTournamentGroup.Name, &tp.Group1TournamentGroup.ID, + &tp.Group1TournamentGroup.Name, &tp.Group2TournamentGroup.ID, &tp.Group2TournamentGroup.Name, &tp.PlayerIDWalkover, &tp.PlayerIDPlaceholderHome, &tp.PlayerIDPlaceholderAway) if err != nil { return nil, err diff --git a/models/tournament.go b/models/tournament.go index 748387de..235d3a1c 100644 --- a/models/tournament.go +++ b/models/tournament.go @@ -90,6 +90,8 @@ type TournamentPreset struct { MatchModeSemiFinal *MatchMode `json:"match_mode_id_semi_final"` MatchModeGrandFinal *MatchMode `json:"match_mode_id_grand_final"` PlayoffsTournamentGroup *TournamentGroup `json:"playoffs_tournament_group_id"` + Group1TournamentGroup *TournamentGroup `json:"group1_tournament_group_id"` + Group2TournamentGroup *TournamentGroup `json:"group2_tournament_group_id"` PlayerIDWalkover int `json:"player_id_walkover"` PlayerIDPlaceholderHome int `json:"player_id_placeholder_home"` PlayerIDPlaceholderAway int `json:"player_id_placeholder_away"` From 1363339f39f373b60f92f2636d1df1494f96b3b3 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Mon, 7 Aug 2023 15:49:00 +0200 Subject: [PATCH 27/67] Continued work on tournament presets --- cmd/serve.go | 1 + data/match.go | 9 ++- data/tournament.go | 141 ++++++++++++++++++++++++--------------------- 3 files changed, 82 insertions(+), 69 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index 12b27b3b..8a11742e 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -42,6 +42,7 @@ var serveCmd = &cobra.Command{ router.HandleFunc("/match/outshot", controllers.GetOutshotTypes).Methods("GET") router.HandleFunc("/match", controllers.GetMatches).Methods("GET") router.HandleFunc("/match/{id}", controllers.GetMatch).Methods("GET") + router.HandleFunc("/match/{id}", controllers.SetScore).Methods("PUT") router.HandleFunc("/match/{id}/metadata", controllers.GetMatchMetadata).Methods("GET") router.HandleFunc("/match/{id}/rematch", controllers.ReMatch).Methods("POST") router.HandleFunc("/match/{id}/statistics", controllers.GetStatisticsForMatch).Methods("GET") diff --git a/data/match.go b/data/match.go index 3c446d31..526f2bbc 100644 --- a/data/match.go +++ b/data/match.go @@ -614,8 +614,13 @@ func GetMatchMetadataForTournament(tournamentID int) ([]*models.MatchMetadata, e return nil, err } players := util.StringToIntArray(playersStr) - m.HomePlayer = players[0] - m.AwayPlayer = players[1] + if len(players) == 1 { + m.HomePlayer = players[0] + m.AwayPlayer = players[0] + } else { + m.HomePlayer = players[0] + m.AwayPlayer = players[1] + } metadata = append(metadata, m) } diff --git a/data/tournament.go b/data/tournament.go index 07587c65..76e77a5f 100644 --- a/data/tournament.go +++ b/data/tournament.go @@ -876,8 +876,9 @@ func GeneratePlayoffsTournament(tournamentID int) (*models.Tournament, error) { &models.Player2Tournament{PlayerID: placeholderAwayID, TournamentGroupID: playoffsGroupID}) playoffs, err := NewTournament(models.Tournament{ - Name: tournament.Name + " Playoffs", - ShortName: tournament.ShortName + "P", + Name: tournament.Name + " Playoffs", + //ShortName: tournament.ShortName + "P", + ShortName: tournament.ShortName, IsPlayoffs: true, OfficeID: tournament.OfficeID, Players: players, @@ -893,23 +894,19 @@ func GeneratePlayoffsTournament(tournamentID int) (*models.Tournament, error) { return nil, err } - matches := make([]*models.Match, 15) - // Create initial 8 matches - for i := 0; i < 8; i++ { - temp := models.TournamentTemplateLast16[i] - home := getCompetitor(group1, temp.Home) - if home == -1 { - // Walkover, so use placeholder - home = walkoverPlayerID - } - away := getCompetitor(group2, temp.Away) - if away == -1 { - // Walkover, so use placeholder - away = walkoverPlayerID - } - // TODO set is_bye - match, err := createTournamentMatch(playoffs.ID, []int{home, away}, startingScore, 1, - tournament.OfficeID, mt, preset.MatchModeLast16) + matches := make([]*models.Match, 0) + // Create Grand Final + match, err := createTournamentMatch(playoffs.ID, []int{placeholderHomeID, placeholderAwayID}, startingScore, 1, + tournament.OfficeID, mt, preset.MatchModeGrandFinal) + if err != nil { + return nil, err + } + matches = append(matches, match) + + // Create Semi Final Matches + for i := 0; i < 2; i++ { + match, err := createTournamentMatch(playoffs.ID, []int{placeholderHomeID, placeholderAwayID}, startingScore, 1, + tournament.OfficeID, mt, preset.MatchModeSemiFinal) if err != nil { return nil, err } @@ -926,40 +923,44 @@ func GeneratePlayoffsTournament(tournamentID int) (*models.Tournament, error) { matches = append(matches, match) } - // Create Semi Final Matches - for i := 0; i < 2; i++ { - match, err := createTournamentMatch(playoffs.ID, []int{placeholderHomeID, placeholderAwayID}, startingScore, 1, - tournament.OfficeID, mt, preset.MatchModeSemiFinal) + // Create initial 8 matches + for i := 0; i < 8; i++ { + temp := models.TournamentTemplateLast16[i] + home := getCompetitor(group1, temp.Home) + if home == -1 { + // Walkover, so use placeholder + home = walkoverPlayerID + } + away := getCompetitor(group2, temp.Away) + if away == -1 { + // Walkover, so use placeholder + away = walkoverPlayerID + } + // TODO set is_bye + match, err := createTournamentMatch(playoffs.ID, []int{home, away}, startingScore, 1, + tournament.OfficeID, mt, preset.MatchModeLast16) if err != nil { return nil, err } matches = append(matches, match) } - // Create Grand Final - match, err := createTournamentMatch(playoffs.ID, []int{placeholderHomeID, placeholderAwayID}, startingScore, 1, - tournament.OfficeID, mt, preset.MatchModeGrandFinal) - if err != nil { - return nil, err - } - matches = append(matches, match) - // Insert Match Metadata for each match created - gf := matches[14] - sf1 := matches[13] - sf2 := matches[12] - qf1 := matches[11] - qf2 := matches[10] - qf3 := matches[9] - qf4 := matches[8] - m8 := matches[7] - m7 := matches[6] - m6 := matches[5] - m5 := matches[4] - m4 := matches[3] - m3 := matches[2] - m2 := matches[1] - m1 := matches[0] + gf := matches[0] + sf1 := matches[1] + sf2 := matches[2] + qf1 := matches[3] + qf2 := matches[4] + qf3 := matches[5] + qf4 := matches[6] + m4 := matches[10] + m1 := matches[7] + m2 := matches[8] + m3 := matches[9] + m5 := matches[11] + m6 := matches[12] + m7 := matches[13] + m8 := matches[14] playoffMatches := []struct { MatchID int @@ -971,28 +972,28 @@ func GeneratePlayoffsTournament(tournamentID int) (*models.Tournament, error) { IsGrandFinal null.Bool IsSemiFinal null.Bool }{ - {MatchID: gf.ID, OrderOfPlay: 15, GroupID: playoffsGroupID, DisplayName: "Grand Final", WinnerMatchID: nil, IsWinnerHome: false, IsGrandFinal: null.BoolFrom(true)}, - {MatchID: sf1.ID, OrderOfPlay: 14, GroupID: playoffsGroupID, DisplayName: "Semi Final 1", WinnerMatchID: &gf.ID, IsWinnerHome: true, IsSemiFinal: null.BoolFrom(true)}, - {MatchID: sf2.ID, OrderOfPlay: 13, GroupID: playoffsGroupID, DisplayName: "Semi Final 2", WinnerMatchID: &gf.ID, IsWinnerHome: false, IsSemiFinal: null.BoolFrom(true)}, - {MatchID: qf1.ID, OrderOfPlay: 12, GroupID: playoffsGroupID, DisplayName: "Quarter Final 1", WinnerMatchID: &sf1.ID, IsWinnerHome: true}, - {MatchID: qf2.ID, OrderOfPlay: 11, GroupID: playoffsGroupID, DisplayName: "Quarter Final 2", WinnerMatchID: &sf1.ID, IsWinnerHome: false}, - {MatchID: qf3.ID, OrderOfPlay: 10, GroupID: playoffsGroupID, DisplayName: "Quarter Final 3", WinnerMatchID: &sf2.ID, IsWinnerHome: true}, - {MatchID: qf4.ID, OrderOfPlay: 9, GroupID: playoffsGroupID, DisplayName: "Quarter Final 4", WinnerMatchID: &sf2.ID, IsWinnerHome: false}, - {MatchID: m1.ID, OrderOfPlay: 1, GroupID: playoffsGroupID, DisplayName: "Match 1", WinnerMatchID: &qf1.ID, IsWinnerHome: true}, - {MatchID: m2.ID, OrderOfPlay: 2, GroupID: playoffsGroupID, DisplayName: "Match 2", WinnerMatchID: &qf1.ID, IsWinnerHome: false}, - {MatchID: m3.ID, OrderOfPlay: 3, GroupID: playoffsGroupID, DisplayName: "Match 3", WinnerMatchID: &qf2.ID, IsWinnerHome: true}, - {MatchID: m4.ID, OrderOfPlay: 4, GroupID: playoffsGroupID, DisplayName: "Match 4", WinnerMatchID: &qf2.ID, IsWinnerHome: false}, - {MatchID: m5.ID, OrderOfPlay: 5, GroupID: playoffsGroupID, DisplayName: "Match 5", WinnerMatchID: &qf3.ID, IsWinnerHome: true}, - {MatchID: m6.ID, OrderOfPlay: 6, GroupID: playoffsGroupID, DisplayName: "Match 6", WinnerMatchID: &qf3.ID, IsWinnerHome: false}, - {MatchID: m7.ID, OrderOfPlay: 7, GroupID: playoffsGroupID, DisplayName: "Match 7", WinnerMatchID: &qf4.ID, IsWinnerHome: true}, - {MatchID: m8.ID, OrderOfPlay: 8, GroupID: playoffsGroupID, DisplayName: "Match 8", WinnerMatchID: &qf4.ID, IsWinnerHome: false}, + {MatchID: gf.ID, OrderOfPlay: 15, GroupID: playoffsGroupID, DisplayName: "Grand Final", WinnerMatchID: nil, IsWinnerHome: false, IsGrandFinal: null.BoolFrom(true), IsSemiFinal: null.BoolFrom(false)}, + {MatchID: sf1.ID, OrderOfPlay: 14, GroupID: playoffsGroupID, DisplayName: "Semi Final 1", WinnerMatchID: &gf.ID, IsWinnerHome: true, IsGrandFinal: null.BoolFrom(false), IsSemiFinal: null.BoolFrom(true)}, + {MatchID: sf2.ID, OrderOfPlay: 13, GroupID: playoffsGroupID, DisplayName: "Semi Final 2", WinnerMatchID: &gf.ID, IsWinnerHome: false, IsGrandFinal: null.BoolFrom(false), IsSemiFinal: null.BoolFrom(true)}, + {MatchID: qf1.ID, OrderOfPlay: 12, GroupID: playoffsGroupID, DisplayName: "Quarter Final 1", WinnerMatchID: &sf1.ID, IsWinnerHome: true, IsGrandFinal: null.BoolFrom(false), IsSemiFinal: null.BoolFrom(false)}, + {MatchID: qf2.ID, OrderOfPlay: 11, GroupID: playoffsGroupID, DisplayName: "Quarter Final 2", WinnerMatchID: &sf1.ID, IsWinnerHome: false, IsGrandFinal: null.BoolFrom(false), IsSemiFinal: null.BoolFrom(false)}, + {MatchID: qf3.ID, OrderOfPlay: 10, GroupID: playoffsGroupID, DisplayName: "Quarter Final 3", WinnerMatchID: &sf2.ID, IsWinnerHome: true, IsGrandFinal: null.BoolFrom(false), IsSemiFinal: null.BoolFrom(false)}, + {MatchID: qf4.ID, OrderOfPlay: 9, GroupID: playoffsGroupID, DisplayName: "Quarter Final 4", WinnerMatchID: &sf2.ID, IsWinnerHome: false, IsGrandFinal: null.BoolFrom(false), IsSemiFinal: null.BoolFrom(false)}, + {MatchID: m1.ID, OrderOfPlay: 1, GroupID: playoffsGroupID, DisplayName: "Match 1", WinnerMatchID: &qf1.ID, IsWinnerHome: true, IsGrandFinal: null.BoolFrom(false), IsSemiFinal: null.BoolFrom(false)}, + {MatchID: m2.ID, OrderOfPlay: 2, GroupID: playoffsGroupID, DisplayName: "Match 2", WinnerMatchID: &qf1.ID, IsWinnerHome: false, IsGrandFinal: null.BoolFrom(false), IsSemiFinal: null.BoolFrom(false)}, + {MatchID: m3.ID, OrderOfPlay: 3, GroupID: playoffsGroupID, DisplayName: "Match 3", WinnerMatchID: &qf2.ID, IsWinnerHome: true, IsGrandFinal: null.BoolFrom(false), IsSemiFinal: null.BoolFrom(false)}, + {MatchID: m4.ID, OrderOfPlay: 4, GroupID: playoffsGroupID, DisplayName: "Match 4", WinnerMatchID: &qf2.ID, IsWinnerHome: false, IsGrandFinal: null.BoolFrom(false), IsSemiFinal: null.BoolFrom(false)}, + {MatchID: m5.ID, OrderOfPlay: 5, GroupID: playoffsGroupID, DisplayName: "Match 5", WinnerMatchID: &qf3.ID, IsWinnerHome: true, IsGrandFinal: null.BoolFrom(false), IsSemiFinal: null.BoolFrom(false)}, + {MatchID: m6.ID, OrderOfPlay: 6, GroupID: playoffsGroupID, DisplayName: "Match 6", WinnerMatchID: &qf3.ID, IsWinnerHome: false, IsGrandFinal: null.BoolFrom(false), IsSemiFinal: null.BoolFrom(false)}, + {MatchID: m7.ID, OrderOfPlay: 7, GroupID: playoffsGroupID, DisplayName: "Match 7", WinnerMatchID: &qf4.ID, IsWinnerHome: true, IsGrandFinal: null.BoolFrom(false), IsSemiFinal: null.BoolFrom(false)}, + {MatchID: m8.ID, OrderOfPlay: 8, GroupID: playoffsGroupID, DisplayName: "Match 8", WinnerMatchID: &qf4.ID, IsWinnerHome: false, IsGrandFinal: null.BoolFrom(false), IsSemiFinal: null.BoolFrom(false)}, } tx, err := models.DB.Begin() if err != nil { return nil, err } - stmt, err := tx.Prepare(`INSERT INTO match_metadata (match_id, order_of_play, tournament_group_id, match_displayname, elimination, trophy, semi_final, - grand_final, winner_outcome_match_id, is_winner_outcome_home) VALUES (?, ?, ?, ?, 1, 0, ?, ?, ?, ?)`) + stmt, err := tx.Prepare(`INSERT INTO match_metadata (match_id, order_of_play, tournament_group_id, match_displayname, elimination, promotion, trophy, semi_final, + grand_final, winner_outcome_match_id, is_winner_outcome_home) VALUES (?, ?, ?, ?, 1, 0, 0, ?, ?, ?, ?)`) if err != nil { tx.Rollback() return nil, err @@ -1015,7 +1016,11 @@ func GeneratePlayoffsTournament(tournamentID int) (*models.Tournament, error) { if match.Players[0] == walkoverPlayerID || match.Players[1] == walkoverPlayerID { winnerID := match.Players[0] if match.Players[0] == walkoverPlayerID { - winnerID = match.Players[1] + if len(match.Players) == 1 { + winnerID = match.Players[0] + } else { + winnerID = match.Players[1] + } } // This is a Bye, so we need to finish it err = FinishByeMatch(match, winnerID) @@ -1036,7 +1041,9 @@ func createTournamentMatch(tournamentID int, players []int, startingScore int, v IsPractice: false, TournamentID: null.IntFrom(int64(tournamentID)), Players: players, - Legs: []*models.Leg{{StartingScore: startingScore}}, + Legs: []*models.Leg{{ + StartingScore: 501, + Parameters: &models.LegParameters{OutshotType: &models.OutshotType{ID: models.OUTSHOTDOUBLE}}}}, }) if err != nil { return nil, err @@ -1046,7 +1053,7 @@ func createTournamentMatch(tournamentID int, players []int, startingScore int, v return match, nil } -// FinishByeMatch will finish a given match, and mov +// FinishByeMatch will finish a given match, and move winner to the next match func FinishByeMatch(match *models.Match, winnerID int) error { if match.TournamentID.Valid { metadata, err := GetMatchMetadata(match.ID) @@ -1071,7 +1078,7 @@ func FinishByeMatch(match *models.Match, winnerID int) error { } // Finish the Bye match - _, err := models.DB.Exec(`UPDATE leg SET is_finished = 1, end_time = NOW(), has_score = 0 WHERE match_id = ?`, match.ID) + _, err := models.DB.Exec(`UPDATE leg SET is_finished = 1, end_time = NOW(), has_scores = 0 WHERE match_id = ?`, match.ID) if err != nil { return err } From a670537bdb8f7bcf593a78725be4e832089a6493 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Fri, 11 Aug 2023 13:54:09 +0200 Subject: [PATCH 28/67] Continued work on tournament presets --- cmd/serve.go | 6 -- controllers/preset_controller.go | 106 ------------------------------- data/match.go | 8 ++- data/tournament.go | 6 +- models/match.go | 63 +++++++++--------- 5 files changed, 45 insertions(+), 144 deletions(-) delete mode 100644 controllers/preset_controller.go diff --git a/cmd/serve.go b/cmd/serve.go index 8a11742e..7b8e39b6 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -86,12 +86,6 @@ var serveCmd = &cobra.Command{ // v2 router.HandleFunc("/players", controllers_v2.GetPlayers).Methods("GET") - router.HandleFunc("/preset", controllers.AddPreset).Methods("POST") - router.HandleFunc("/preset", controllers.GetPresets).Methods("GET") - router.HandleFunc("/preset/{id}", controllers.GetPreset).Methods("GET") - router.HandleFunc("/preset/{id}", controllers.UpdatePreset).Methods("PUT") - router.HandleFunc("/preset/{id}", controllers.DeletePreset).Methods("DELETE") - router.HandleFunc("/statistics/global", controllers.GetGlobalStatistics).Methods("GET") router.HandleFunc("/statistics/global/fnc", controllers.GetGlobalStatisticsFnc).Methods("GET") router.HandleFunc("/statistics/office/{from}/{to}", controllers.GetOfficeStatistics).Methods("GET") diff --git a/controllers/preset_controller.go b/controllers/preset_controller.go deleted file mode 100644 index db5fd714..00000000 --- a/controllers/preset_controller.go +++ /dev/null @@ -1,106 +0,0 @@ -package controllers - -import ( - "encoding/json" - "log" - "net/http" - "strconv" - - "github.com/kcapp/api/data" - "github.com/kcapp/api/models" - - "github.com/gorilla/mux" -) - -// AddPreset will create a new preset -func AddPreset(w http.ResponseWriter, r *http.Request) { - var preset models.MatchPreset - err := json.NewDecoder(r.Body).Decode(&preset) - if err != nil { - log.Println("Unable to deserialize preset json", err) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - err = data.AddPreset(preset) - if err != nil { - log.Println("Unable to add preset", err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } -} - -// GetPresets will return a list of all presets -func GetPresets(w http.ResponseWriter, r *http.Request) { - SetHeaders(w) - players, err := data.GetPresets() - if err != nil { - log.Println("Unable to get presets", err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - json.NewEncoder(w).Encode(players) -} - -// GetPreset will return a preset with the given ID -func GetPreset(w http.ResponseWriter, r *http.Request) { - SetHeaders(w) - params := mux.Vars(r) - id, err := strconv.Atoi(params["id"]) - if err != nil { - log.Println("Invalid id parameter") - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - preset, err := data.GetPreset(id) - if err != nil { - log.Println("Unable to get preset", err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - json.NewEncoder(w).Encode(preset) -} - -// UpdatePreset will update the given preset -func UpdatePreset(w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - id, err := strconv.Atoi(params["id"]) - if err != nil { - log.Println("Invalid id parameter") - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - var preset models.MatchPreset - err = json.NewDecoder(r.Body).Decode(&preset) - if err != nil { - log.Println("Unable to deserialize preset json", err) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - err = data.UpdatePreset(id, preset) - if err != nil { - log.Println("Unable to update preset", err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } -} - -// DeletePreset will delete a preset with the given ID -func DeletePreset(w http.ResponseWriter, r *http.Request) { - SetHeaders(w) - params := mux.Vars(r) - id, err := strconv.Atoi(params["id"]) - if err != nil { - log.Println("Invalid id parameter") - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - err = data.DeletePreset(id) - if err != nil { - log.Println("Unable to delete preset", err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } -} diff --git a/data/match.go b/data/match.go index 526f2bbc..7087baf2 100644 --- a/data/match.go +++ b/data/match.go @@ -449,6 +449,12 @@ func SetScore(matchID int, result models.MatchResult) (*models.Match, error) { return nil, err } + _, err = tx.Exec(`DELETE FROM player_elo_changelog WHERE match_id = ?`, matchID) + if err != nil { + tx.Rollback() + return nil, err + } + // TODO Improve by only inserting legs where there is no score? for i := 0; i < result.LooserScore; i++ { @@ -496,7 +502,7 @@ func SetScore(matchID int, result models.MatchResult) (*models.Match, error) { } } } - _, err = tx.Exec("UPDATE matches SET is_finished = 1, winner_id = ?, current_leg_id = ? WHERE id = ?", result.WinnerID, legID, matchID) + _, err = tx.Exec("UPDATE matches SET is_finished = 1, winner_id = ?, current_leg_id = ?, updated_at = NOW() WHERE id = ?", result.WinnerID, legID, matchID) if err != nil { tx.Rollback() return nil, err diff --git a/data/tournament.go b/data/tournament.go index 76e77a5f..ecaf2011 100644 --- a/data/tournament.go +++ b/data/tournament.go @@ -208,7 +208,8 @@ func GetTournamentMatches(id int) (map[int][]*models.Match, error) { m.created_at, m.updated_at, m.owe_type_id, m.venue_id, mt.id, mt.name, mt.description, mm.id, mm.name, mm.short_name, mm.wins_required, mm.legs_required, v.id, v.name, v.description, l.updated_at as 'last_throw', GROUP_CONCAT(DISTINCT p2l.player_id ORDER BY p2l.order) AS 'players', - m.tournament_id, tg.id, GROUP_CONCAT(legs.winner_id ORDER BY legs.id) AS 'legs_won', ot.item + m.tournament_id, tg.id, GROUP_CONCAT(legs.winner_id ORDER BY legs.id) AS 'legs_won', ot.item, + IF(SUM(p.is_placeholder) > 0, 1, 0) as 'is_players_decided' FROM matches m JOIN match_type mt ON mt.id = m.match_type_id JOIN match_mode mm ON mm.id = m.match_mode_id @@ -220,6 +221,7 @@ func GetTournamentMatches(id int) (map[int][]*models.Match, error) { LEFT JOIN player2tournament p2t ON p2t.tournament_id = m.tournament_id AND p2t.player_id = p2l.player_id LEFT JOIN tournament t ON t.id = p2t.tournament_id LEFT JOIN tournament_group tg ON tg.id = p2t.tournament_group_id + LEFT JOIN player p on p.id = p2l.player_id WHERE t.id = ? GROUP BY m.id ORDER BY m.id DESC`, id) @@ -241,7 +243,7 @@ func GetTournamentMatches(id int) (map[int][]*models.Match, error) { err := rows.Scan(&m.ID, &m.IsFinished, &m.CurrentLegID, &m.WinnerID, &m.IsWalkover, &m.IsBye, &m.IsStarted, &m.CreatedAt, &m.UpdatedAt, &m.OweTypeID, &m.VenueID, &m.MatchType.ID, &m.MatchType.Name, &m.MatchType.Description, &m.MatchMode.ID, &m.MatchMode.Name, &m.MatchMode.ShortName, &m.MatchMode.WinsRequired, &m.MatchMode.LegsRequired, - &venue.ID, &venue.Name, &venue.Description, &m.LastThrow, &players, &m.TournamentID, &groupID, &legsWon, &ot) + &venue.ID, &venue.Name, &venue.Description, &m.LastThrow, &players, &m.TournamentID, &groupID, &legsWon, &ot, &m.IsPlayersDecided) if err != nil { return nil, err } diff --git a/models/match.go b/models/match.go index ea92ba06..a3e0b4a3 100644 --- a/models/match.go +++ b/models/match.go @@ -133,35 +133,36 @@ var TargetsJDCPractice = [19]Target{ // Match struct used for storing matches type Match struct { - ID int `json:"id"` - CurrentLegID null.Int `json:"current_leg_id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - EndTime time.Time `json:"end_time,omitempty"` - MatchType *MatchType `json:"match_type"` - MatchMode *MatchMode `json:"match_mode"` - WinnerID null.Int `json:"winner_id"` - IsFinished bool `json:"is_finished"` - IsAbandoned bool `json:"is_abandoned"` - IsWalkover bool `json:"is_walkover"` - IsBye bool `json:"is_bye"` - IsStarted bool `json:"is_started"` - OfficeID null.Int `json:"office_id,omitempty"` - OweTypeID null.Int `json:"owe_type_id"` - VenueID null.Int `json:"venue_id"` - IsPractice bool `json:"is_practice"` - Venue *Venue `json:"venue"` - OweType *OweType `json:"owe_type,omitempty"` - TournamentID null.Int `json:"tournament_id,omitempty"` - Tournament *MatchTournament `json:"tournament,omitempty"` - Players []int `json:"players"` - Legs []*Leg `json:"legs,omitempty"` - PlayerHandicaps map[int]int `json:"player_handicaps,omitempty"` - BotPlayerConfig map[int]*BotConfig `json:"bot_player_config,omitempty"` - FirstThrow null.Time `json:"first_throw_time,omitempty"` - LastThrow null.Time `json:"last_throw_time,omitempty"` - EloChange map[int]*PlayerElo `json:"elo_change,omitempty"` - LegsWon []int `json:"legs_won,omitempty"` + ID int `json:"id"` + CurrentLegID null.Int `json:"current_leg_id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + EndTime time.Time `json:"end_time,omitempty"` + MatchType *MatchType `json:"match_type"` + MatchMode *MatchMode `json:"match_mode"` + WinnerID null.Int `json:"winner_id"` + IsFinished bool `json:"is_finished"` + IsAbandoned bool `json:"is_abandoned"` + IsWalkover bool `json:"is_walkover"` + IsBye bool `json:"is_bye"` + IsStarted bool `json:"is_started"` + IsPlayersDecided bool `json:"is_players_decided"` + OfficeID null.Int `json:"office_id,omitempty"` + OweTypeID null.Int `json:"owe_type_id"` + VenueID null.Int `json:"venue_id"` + IsPractice bool `json:"is_practice"` + Venue *Venue `json:"venue"` + OweType *OweType `json:"owe_type,omitempty"` + TournamentID null.Int `json:"tournament_id,omitempty"` + Tournament *MatchTournament `json:"tournament,omitempty"` + Players []int `json:"players"` + Legs []*Leg `json:"legs,omitempty"` + PlayerHandicaps map[int]int `json:"player_handicaps,omitempty"` + BotPlayerConfig map[int]*BotConfig `json:"bot_player_config,omitempty"` + FirstThrow null.Time `json:"first_throw_time,omitempty"` + LastThrow null.Time `json:"last_throw_time,omitempty"` + EloChange map[int]*PlayerElo `json:"elo_change,omitempty"` + LegsWon []int `json:"legs_won,omitempty"` } // MarshalJSON will marshall the given object to JSON @@ -180,6 +181,8 @@ func (match Match) MarshalJSON() ([]byte, error) { IsAbandoned bool `json:"is_abandoned"` IsWalkover bool `json:"is_walkover"` IsBye bool `json:"is_bye"` + IsStarted bool `json:"is_started"` + IsPlayersDecided bool `json:"is_players_decided"` OfficeID null.Int `json:"office_id,omitempty"` OweTypeID null.Int `json:"owe_type_id"` VenueID null.Int `json:"venue_id"` @@ -220,6 +223,8 @@ func (match Match) MarshalJSON() ([]byte, error) { IsAbandoned: match.IsAbandoned, IsWalkover: match.IsWalkover, IsBye: match.IsBye, + IsStarted: match.IsStarted, + IsPlayersDecided: match.IsPlayersDecided, OfficeID: match.OfficeID, OweTypeID: match.OweTypeID, VenueID: match.VenueID, From 409b72527346167e5f9b013e7a7b1c3ac6164813 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Sun, 13 Aug 2023 13:52:52 +0200 Subject: [PATCH 29/67] Added support for tournament presets --- CHANGELOG.md | 1 + data/tournament.go | 281 +++++++++++++++++++++++++++---------------- models/match.go | 3 + models/tournament.go | 11 ++ 4 files changed, 190 insertions(+), 106 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7229d19f..ba6e72d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [2.7.0] - TBD #### Feature +- Support for Tournament Presets - Added commands for recalculating statistics, resetting elo etc - New endpoint for getting player hits - Support for `ANY` and `MASTER` out for `x01` legs diff --git a/data/tournament.go b/data/tournament.go index ecaf2011..1a36a55c 100644 --- a/data/tournament.go +++ b/data/tournament.go @@ -15,7 +15,7 @@ import ( func GetTournaments() ([]*models.Tournament, error) { rows, err := models.DB.Query(` SELECT - id, name, short_name, is_finished, is_playoffs, playoffs_tournament_id, office_id, start_time, end_time + id, name, short_name, is_finished, is_playoffs, playoffs_tournament_id, preset_id, manual_admin, office_id, start_time, end_time FROM tournament WHERE is_playoffs = 0 ORDER BY id DESC`) @@ -28,7 +28,8 @@ func GetTournaments() ([]*models.Tournament, error) { for rows.Next() { tournament := new(models.Tournament) err := rows.Scan(&tournament.ID, &tournament.Name, &tournament.ShortName, &tournament.IsFinished, &tournament.IsPlayoffs, - &tournament.PlayoffsTournamentID, &tournament.OfficeID, &tournament.StartTime, &tournament.EndTime) + &tournament.PlayoffsTournamentID, &tournament.PresetID, &tournament.ManualAdmin, &tournament.OfficeID, &tournament.StartTime, + &tournament.EndTime) if err != nil { return nil, err } @@ -93,9 +94,9 @@ func GetTournament(id int) (*models.Tournament, error) { tournament := new(models.Tournament) err := models.DB.QueryRow(` SELECT - id, name, short_name, is_finished, is_playoffs, playoffs_tournament_id, preset_id, office_id, start_time, end_time + id, name, short_name, is_finished, is_playoffs, playoffs_tournament_id, preset_id, manual_admin, office_id, start_time, end_time FROM tournament t WHERE t.id = ?`, id).Scan(&tournament.ID, &tournament.Name, &tournament.ShortName, &tournament.IsFinished, &tournament.IsPlayoffs, - &tournament.PlayoffsTournamentID, &tournament.PresetID, &tournament.OfficeID, &tournament.StartTime, &tournament.EndTime) + &tournament.PlayoffsTournamentID, &tournament.PresetID, &tournament.ManualAdmin, &tournament.OfficeID, &tournament.StartTime, &tournament.EndTime) if err != nil { return nil, err } @@ -207,9 +208,10 @@ func GetTournamentMatches(id int) (map[int][]*models.Match, error) { m.id, m.is_finished, m.current_leg_id, m.winner_id, m.is_walkover, m.is_bye, IF(TIMEDIFF(MAX(l.updated_at), NOW() - INTERVAL 15 MINUTE) > 0, 1, 0) AS 'is_started', m.created_at, m.updated_at, m.owe_type_id, m.venue_id, mt.id, mt.name, mt.description, mm.id, mm.name, mm.short_name, mm.wins_required, mm.legs_required, - v.id, v.name, v.description, l.updated_at as 'last_throw', GROUP_CONCAT(DISTINCT p2l.player_id ORDER BY p2l.order) AS 'players', + v.id, v.name, v.description, l.updated_at as 'last_throw', if(l.is_finished AND l.has_scores, 1, 0) as 'has_scores', + GROUP_CONCAT(DISTINCT p2l.player_id ORDER BY p2l.order) AS 'players', m.tournament_id, tg.id, GROUP_CONCAT(legs.winner_id ORDER BY legs.id) AS 'legs_won', ot.item, - IF(SUM(p.is_placeholder) > 0, 1, 0) as 'is_players_decided' + IF(SUM(p.is_placeholder) > 0, 0, 1) as 'is_players_decided' FROM matches m JOIN match_type mt ON mt.id = m.match_type_id JOIN match_mode mm ON mm.id = m.match_mode_id @@ -243,7 +245,7 @@ func GetTournamentMatches(id int) (map[int][]*models.Match, error) { err := rows.Scan(&m.ID, &m.IsFinished, &m.CurrentLegID, &m.WinnerID, &m.IsWalkover, &m.IsBye, &m.IsStarted, &m.CreatedAt, &m.UpdatedAt, &m.OweTypeID, &m.VenueID, &m.MatchType.ID, &m.MatchType.Name, &m.MatchType.Description, &m.MatchMode.ID, &m.MatchMode.Name, &m.MatchMode.ShortName, &m.MatchMode.WinsRequired, &m.MatchMode.LegsRequired, - &venue.ID, &venue.Name, &venue.Description, &m.LastThrow, &players, &m.TournamentID, &groupID, &legsWon, &ot, &m.IsPlayersDecided) + &venue.ID, &venue.Name, &venue.Description, &m.LastThrow, &m.HasScores, &players, &m.TournamentID, &groupID, &legsWon, &ot, &m.IsPlayersDecided) if err != nil { return nil, err } @@ -760,9 +762,9 @@ func NewTournament(tournament models.Tournament) (*models.Tournament, error) { } res, err := tx.Exec(` - INSERT INTO tournament (name, short_name, is_finished, is_playoffs, playoffs_tournament_id, preset_id, office_id, start_time, end_time) VALUES - (?, ?, ?, ?, ?, ?, ?, ?, ?)`, tournament.Name, tournament.ShortName, 0, tournament.IsPlayoffs, tournament.PlayoffsTournamentID, tournament.PresetID, - tournament.OfficeID, tournament.StartTime, tournament.EndTime) + INSERT INTO tournament (name, short_name, is_finished, is_playoffs, playoffs_tournament_id, preset_id, manual_admin, office_id, start_time, end_time) VALUES + (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, tournament.Name, tournament.ShortName, 0, tournament.IsPlayoffs, tournament.PlayoffsTournamentID, tournament.PresetID, + tournament.ManualAdmin, tournament.OfficeID, tournament.StartTime, tournament.EndTime) if err != nil { tx.Rollback() return nil, err @@ -793,14 +795,15 @@ func NewTournament(tournament models.Tournament) (*models.Tournament, error) { func GenerateTournament(input models.Tournament) (*models.Tournament, error) { officeID := input.OfficeID tournament, err := NewTournament(models.Tournament{ - Name: input.Name, - ShortName: input.ShortName, - IsPlayoffs: false, - OfficeID: officeID, - PresetID: input.PresetID, - Players: input.Players, - StartTime: null.TimeFrom(time.Now()), - EndTime: null.TimeFrom(time.Now()), + Name: input.Name, + ShortName: input.ShortName, + IsPlayoffs: false, + OfficeID: officeID, + PresetID: input.PresetID, + ManualAdmin: input.ManualAdmin, + Players: input.Players, + StartTime: null.TimeFrom(time.Now()), + EndTime: null.TimeFrom(time.Now()), }) if err != nil { return nil, err @@ -871,6 +874,7 @@ func GeneratePlayoffsTournament(tournamentID int) (*models.Tournament, error) { for _, groupPlayer := range append(group1, group2...) { players = append(players, &models.Player2Tournament{PlayerID: groupPlayer.PlayerID, TournamentGroupID: playoffsGroupID}) } + numPlayers := len(players) // Add all the placeholder players players = append(players, &models.Player2Tournament{PlayerID: walkoverPlayerID, TournamentGroupID: playoffsGroupID}, @@ -880,12 +884,13 @@ func GeneratePlayoffsTournament(tournamentID int) (*models.Tournament, error) { playoffs, err := NewTournament(models.Tournament{ Name: tournament.Name + " Playoffs", //ShortName: tournament.ShortName + "P", - ShortName: tournament.ShortName, - IsPlayoffs: true, - OfficeID: tournament.OfficeID, - Players: players, - StartTime: null.TimeFrom(time.Now()), - EndTime: null.TimeFrom(time.Now()), + ShortName: tournament.ShortName, + IsPlayoffs: true, + OfficeID: tournament.OfficeID, + Players: players, + StartTime: null.TimeFrom(time.Now()), + EndTime: null.TimeFrom(time.Now()), + ManualAdmin: tournament.ManualAdmin, }) if err != nil { return nil, err @@ -906,45 +911,90 @@ func GeneratePlayoffsTournament(tournamentID int) (*models.Tournament, error) { matches = append(matches, match) // Create Semi Final Matches - for i := 0; i < 2; i++ { - match, err := createTournamentMatch(playoffs.ID, []int{placeholderHomeID, placeholderAwayID}, startingScore, 1, - tournament.OfficeID, mt, preset.MatchModeSemiFinal) - if err != nil { - return nil, err + if numPlayers > 4 { + for i := 0; i < 2; i++ { + match, err := createTournamentMatch(playoffs.ID, []int{placeholderHomeID, placeholderAwayID}, startingScore, 1, + tournament.OfficeID, mt, preset.MatchModeSemiFinal) + if err != nil { + return nil, err + } + matches = append(matches, match) } - matches = append(matches, match) - } - - // Create Quarter Final Matches - for i := 0; i < 4; i++ { - match, err := createTournamentMatch(playoffs.ID, []int{placeholderHomeID, placeholderAwayID}, startingScore, 1, - tournament.OfficeID, mt, preset.MatchModeQuarterFinal) - if err != nil { - return nil, err + } else { + // Create Semi Final Matches + for i := 0; i < 2; i++ { + temp := models.TournamentTemplateSemiFinals[i] + home := getCompetitor(group1, temp.Home) + if home == -1 { + // Walkover, so use placeholder + home = walkoverPlayerID + } + away := getCompetitor(group2, temp.Away) + if away == -1 { + // Walkover, so use placeholder + away = walkoverPlayerID + } + match, err := createTournamentMatch(playoffs.ID, []int{home, away}, startingScore, 1, + tournament.OfficeID, mt, preset.MatchModeSemiFinal) + if err != nil { + return nil, err + } + matches = append(matches, match) } - matches = append(matches, match) } - // Create initial 8 matches - for i := 0; i < 8; i++ { - temp := models.TournamentTemplateLast16[i] - home := getCompetitor(group1, temp.Home) - if home == -1 { - // Walkover, so use placeholder - home = walkoverPlayerID + if numPlayers > 8 { + // Create Quarter Final Matches + for i := 0; i < 4; i++ { + match, err := createTournamentMatch(playoffs.ID, []int{placeholderHomeID, placeholderAwayID}, startingScore, 1, + tournament.OfficeID, mt, preset.MatchModeQuarterFinal) + if err != nil { + return nil, err + } + matches = append(matches, match) } - away := getCompetitor(group2, temp.Away) - if away == -1 { - // Walkover, so use placeholder - away = walkoverPlayerID + + // Create initial 8 matches + for i := 0; i < 8; i++ { + temp := models.TournamentTemplateLast16[i] + home := getCompetitor(group1, temp.Home) + if home == -1 { + // Walkover, so use placeholder + home = walkoverPlayerID + } + away := getCompetitor(group2, temp.Away) + if away == -1 { + // Walkover, so use placeholder + away = walkoverPlayerID + } + match, err := createTournamentMatch(playoffs.ID, []int{home, away}, startingScore, 1, + tournament.OfficeID, mt, preset.MatchModeLast16) + if err != nil { + return nil, err + } + matches = append(matches, match) } - // TODO set is_bye - match, err := createTournamentMatch(playoffs.ID, []int{home, away}, startingScore, 1, - tournament.OfficeID, mt, preset.MatchModeLast16) - if err != nil { - return nil, err + } else { + // Create Quarter Final Matches + for i := 0; i < 4; i++ { + temp := models.TournamentTemplateQuarterFinals[i] + home := getCompetitor(group1, temp.Home) + if home == -1 { + // Walkover, so use placeholder + home = walkoverPlayerID + } + away := getCompetitor(group2, temp.Away) + if away == -1 { + // Walkover, so use placeholder + away = walkoverPlayerID + } + match, err := createTournamentMatch(playoffs.ID, []int{home, away}, startingScore, 1, + tournament.OfficeID, mt, preset.MatchModeQuarterFinal) + if err != nil { + return nil, err + } + matches = append(matches, match) } - matches = append(matches, match) } // Insert Match Metadata for each match created @@ -955,64 +1005,56 @@ func GeneratePlayoffsTournament(tournamentID int) (*models.Tournament, error) { qf2 := matches[4] qf3 := matches[5] qf4 := matches[6] - m4 := matches[10] - m1 := matches[7] - m2 := matches[8] - m3 := matches[9] - m5 := matches[11] - m6 := matches[12] - m7 := matches[13] - m8 := matches[14] - - playoffMatches := []struct { - MatchID int - OrderOfPlay int - GroupID int - DisplayName string - WinnerMatchID *int - IsWinnerHome bool - IsGrandFinal null.Bool - IsSemiFinal null.Bool - }{ - {MatchID: gf.ID, OrderOfPlay: 15, GroupID: playoffsGroupID, DisplayName: "Grand Final", WinnerMatchID: nil, IsWinnerHome: false, IsGrandFinal: null.BoolFrom(true), IsSemiFinal: null.BoolFrom(false)}, - {MatchID: sf1.ID, OrderOfPlay: 14, GroupID: playoffsGroupID, DisplayName: "Semi Final 1", WinnerMatchID: &gf.ID, IsWinnerHome: true, IsGrandFinal: null.BoolFrom(false), IsSemiFinal: null.BoolFrom(true)}, - {MatchID: sf2.ID, OrderOfPlay: 13, GroupID: playoffsGroupID, DisplayName: "Semi Final 2", WinnerMatchID: &gf.ID, IsWinnerHome: false, IsGrandFinal: null.BoolFrom(false), IsSemiFinal: null.BoolFrom(true)}, - {MatchID: qf1.ID, OrderOfPlay: 12, GroupID: playoffsGroupID, DisplayName: "Quarter Final 1", WinnerMatchID: &sf1.ID, IsWinnerHome: true, IsGrandFinal: null.BoolFrom(false), IsSemiFinal: null.BoolFrom(false)}, - {MatchID: qf2.ID, OrderOfPlay: 11, GroupID: playoffsGroupID, DisplayName: "Quarter Final 2", WinnerMatchID: &sf1.ID, IsWinnerHome: false, IsGrandFinal: null.BoolFrom(false), IsSemiFinal: null.BoolFrom(false)}, - {MatchID: qf3.ID, OrderOfPlay: 10, GroupID: playoffsGroupID, DisplayName: "Quarter Final 3", WinnerMatchID: &sf2.ID, IsWinnerHome: true, IsGrandFinal: null.BoolFrom(false), IsSemiFinal: null.BoolFrom(false)}, - {MatchID: qf4.ID, OrderOfPlay: 9, GroupID: playoffsGroupID, DisplayName: "Quarter Final 4", WinnerMatchID: &sf2.ID, IsWinnerHome: false, IsGrandFinal: null.BoolFrom(false), IsSemiFinal: null.BoolFrom(false)}, - {MatchID: m1.ID, OrderOfPlay: 1, GroupID: playoffsGroupID, DisplayName: "Match 1", WinnerMatchID: &qf1.ID, IsWinnerHome: true, IsGrandFinal: null.BoolFrom(false), IsSemiFinal: null.BoolFrom(false)}, - {MatchID: m2.ID, OrderOfPlay: 2, GroupID: playoffsGroupID, DisplayName: "Match 2", WinnerMatchID: &qf1.ID, IsWinnerHome: false, IsGrandFinal: null.BoolFrom(false), IsSemiFinal: null.BoolFrom(false)}, - {MatchID: m3.ID, OrderOfPlay: 3, GroupID: playoffsGroupID, DisplayName: "Match 3", WinnerMatchID: &qf2.ID, IsWinnerHome: true, IsGrandFinal: null.BoolFrom(false), IsSemiFinal: null.BoolFrom(false)}, - {MatchID: m4.ID, OrderOfPlay: 4, GroupID: playoffsGroupID, DisplayName: "Match 4", WinnerMatchID: &qf2.ID, IsWinnerHome: false, IsGrandFinal: null.BoolFrom(false), IsSemiFinal: null.BoolFrom(false)}, - {MatchID: m5.ID, OrderOfPlay: 5, GroupID: playoffsGroupID, DisplayName: "Match 5", WinnerMatchID: &qf3.ID, IsWinnerHome: true, IsGrandFinal: null.BoolFrom(false), IsSemiFinal: null.BoolFrom(false)}, - {MatchID: m6.ID, OrderOfPlay: 6, GroupID: playoffsGroupID, DisplayName: "Match 6", WinnerMatchID: &qf3.ID, IsWinnerHome: false, IsGrandFinal: null.BoolFrom(false), IsSemiFinal: null.BoolFrom(false)}, - {MatchID: m7.ID, OrderOfPlay: 7, GroupID: playoffsGroupID, DisplayName: "Match 7", WinnerMatchID: &qf4.ID, IsWinnerHome: true, IsGrandFinal: null.BoolFrom(false), IsSemiFinal: null.BoolFrom(false)}, - {MatchID: m8.ID, OrderOfPlay: 8, GroupID: playoffsGroupID, DisplayName: "Match 8", WinnerMatchID: &qf4.ID, IsWinnerHome: false, IsGrandFinal: null.BoolFrom(false), IsSemiFinal: null.BoolFrom(false)}, + + tg := &models.TournamentGroup{ID: playoffsGroupID} + playoffsMatches := []*models.MatchMetadata{ + {MatchID: gf.ID, OrderOfPlay: 15, TournamentGroup: tg, MatchDisplayname: "Grand Final", WinnerOutcomeMatchID: null.IntFromPtr(nil), IsWinnerOutcomeHome: false, GrandFinal: true, SemiFinal: false}, + {MatchID: sf1.ID, OrderOfPlay: 14, TournamentGroup: tg, MatchDisplayname: "Semi Final 1", WinnerOutcomeMatchID: null.IntFrom(int64(gf.ID)), IsWinnerOutcomeHome: true, GrandFinal: false, SemiFinal: true}, + {MatchID: sf2.ID, OrderOfPlay: 13, TournamentGroup: tg, MatchDisplayname: "Semi Final 2", WinnerOutcomeMatchID: null.IntFrom(int64(gf.ID)), IsWinnerOutcomeHome: false, GrandFinal: false, SemiFinal: true}, } - tx, err := models.DB.Begin() + err = insertMetadata(playoffsMatches) if err != nil { return nil, err } - stmt, err := tx.Prepare(`INSERT INTO match_metadata (match_id, order_of_play, tournament_group_id, match_displayname, elimination, promotion, trophy, semi_final, - grand_final, winner_outcome_match_id, is_winner_outcome_home) VALUES (?, ?, ?, ?, 1, 0, 0, ?, ?, ?, ?)`) - if err != nil { - tx.Rollback() - return nil, err + + if numPlayers > 4 { + playoffsMatches := []*models.MatchMetadata{ + {MatchID: qf1.ID, OrderOfPlay: 12, TournamentGroup: tg, MatchDisplayname: "Quarter Final 1", WinnerOutcomeMatchID: null.IntFrom(int64(sf1.ID)), IsWinnerOutcomeHome: true, GrandFinal: false, SemiFinal: false}, + {MatchID: qf2.ID, OrderOfPlay: 11, TournamentGroup: tg, MatchDisplayname: "Quarter Final 2", WinnerOutcomeMatchID: null.IntFrom(int64(sf1.ID)), IsWinnerOutcomeHome: false, GrandFinal: false, SemiFinal: false}, + {MatchID: qf3.ID, OrderOfPlay: 10, TournamentGroup: tg, MatchDisplayname: "Quarter Final 3", WinnerOutcomeMatchID: null.IntFrom(int64(sf2.ID)), IsWinnerOutcomeHome: true, GrandFinal: false, SemiFinal: false}, + {MatchID: qf4.ID, OrderOfPlay: 9, TournamentGroup: tg, MatchDisplayname: "Quarter Final 4", WinnerOutcomeMatchID: null.IntFrom(int64(sf2.ID)), IsWinnerOutcomeHome: false, GrandFinal: false, SemiFinal: false}, + } + err = insertMetadata(playoffsMatches) + if err != nil { + return nil, err + } } - for _, metadata := range playoffMatches { - var winnerOutcomeMatchID *int - if metadata.WinnerMatchID != nil { - winnerOutcomeMatchID = metadata.WinnerMatchID + + if numPlayers > 8 { + m1 := matches[7] + m2 := matches[8] + m3 := matches[9] + m4 := matches[10] + m5 := matches[11] + m6 := matches[12] + m7 := matches[13] + m8 := matches[14] + + playoffsMatches := []*models.MatchMetadata{ + {MatchID: m1.ID, OrderOfPlay: 1, TournamentGroup: tg, MatchDisplayname: "Match 1", WinnerOutcomeMatchID: null.IntFrom(int64(qf1.ID)), IsWinnerOutcomeHome: true, GrandFinal: false, SemiFinal: false}, + {MatchID: m2.ID, OrderOfPlay: 2, TournamentGroup: tg, MatchDisplayname: "Match 2", WinnerOutcomeMatchID: null.IntFrom(int64(qf1.ID)), IsWinnerOutcomeHome: false, GrandFinal: false, SemiFinal: false}, + {MatchID: m3.ID, OrderOfPlay: 3, TournamentGroup: tg, MatchDisplayname: "Match 3", WinnerOutcomeMatchID: null.IntFrom(int64(qf2.ID)), IsWinnerOutcomeHome: true, GrandFinal: false, SemiFinal: false}, + {MatchID: m4.ID, OrderOfPlay: 4, TournamentGroup: tg, MatchDisplayname: "Match 4", WinnerOutcomeMatchID: null.IntFrom(int64(qf2.ID)), IsWinnerOutcomeHome: false, GrandFinal: false, SemiFinal: false}, + {MatchID: m5.ID, OrderOfPlay: 5, TournamentGroup: tg, MatchDisplayname: "Match 1", WinnerOutcomeMatchID: null.IntFrom(int64(qf3.ID)), IsWinnerOutcomeHome: true, GrandFinal: false, SemiFinal: false}, + {MatchID: m6.ID, OrderOfPlay: 6, TournamentGroup: tg, MatchDisplayname: "Match 2", WinnerOutcomeMatchID: null.IntFrom(int64(qf3.ID)), IsWinnerOutcomeHome: false, GrandFinal: false, SemiFinal: false}, + {MatchID: m7.ID, OrderOfPlay: 7, TournamentGroup: tg, MatchDisplayname: "Match 3", WinnerOutcomeMatchID: null.IntFrom(int64(qf4.ID)), IsWinnerOutcomeHome: true, GrandFinal: false, SemiFinal: false}, + {MatchID: m8.ID, OrderOfPlay: 8, TournamentGroup: tg, MatchDisplayname: "Match 4", WinnerOutcomeMatchID: null.IntFrom(int64(qf4.ID)), IsWinnerOutcomeHome: false, GrandFinal: false, SemiFinal: false}, } - _, err = stmt.Exec(metadata.MatchID, metadata.OrderOfPlay, metadata.GroupID, metadata.DisplayName, metadata.IsSemiFinal, metadata.IsGrandFinal, - winnerOutcomeMatchID, metadata.IsWinnerHome) + err = insertMetadata(playoffsMatches) if err != nil { - tx.Rollback() return nil, err } } - tx.Commit() for _, match := range matches { if match.Players[0] == walkoverPlayerID || match.Players[1] == walkoverPlayerID { @@ -1031,7 +1073,7 @@ func GeneratePlayoffsTournament(tournamentID int) (*models.Tournament, error) { } } } - return nil, nil + return GetTournament(playoffs.ID) } func createTournamentMatch(tournamentID int, players []int, startingScore int, venueID int, officeID int, matchType *models.MatchType, matchMode *models.MatchMode) (*models.Match, error) { @@ -1044,7 +1086,7 @@ func createTournamentMatch(tournamentID int, players []int, startingScore int, v TournamentID: null.IntFrom(int64(tournamentID)), Players: players, Legs: []*models.Leg{{ - StartingScore: 501, + StartingScore: startingScore, Parameters: &models.LegParameters{OutshotType: &models.OutshotType{ID: models.OUTSHOTDOUBLE}}}}, }) if err != nil { @@ -1167,3 +1209,30 @@ func getCompetitor(group []*models.TournamentOverview, num int) int { } return group[num].PlayerID } + +func insertMetadata(matches []*models.MatchMetadata) error { + tx, err := models.DB.Begin() + if err != nil { + return err + } + stmt, err := tx.Prepare(`INSERT INTO match_metadata (match_id, order_of_play, tournament_group_id, match_displayname, elimination, promotion, + trophy, semi_final, grand_final, winner_outcome_match_id, is_winner_outcome_home) VALUES (?, ?, ?, ?, 1, 0, 0, ?, ?, ?, ?)`) + if err != nil { + tx.Rollback() + return err + } + for _, metadata := range matches { + var winnerOutcomeMatchID *int64 + if metadata.WinnerOutcomeMatchID.Valid { + winnerOutcomeMatchID = &metadata.WinnerOutcomeMatchID.Int64 + } + _, err = stmt.Exec(metadata.MatchID, metadata.OrderOfPlay, metadata.TournamentGroup.ID, metadata.MatchDisplayname, metadata.SemiFinal, + metadata.GrandFinal, winnerOutcomeMatchID, metadata.IsWinnerOutcomeHome) + if err != nil { + tx.Rollback() + return err + } + } + tx.Commit() + return nil +} diff --git a/models/match.go b/models/match.go index a3e0b4a3..ca3bc097 100644 --- a/models/match.go +++ b/models/match.go @@ -147,6 +147,7 @@ type Match struct { IsBye bool `json:"is_bye"` IsStarted bool `json:"is_started"` IsPlayersDecided bool `json:"is_players_decided"` + HasScores bool `json:"has_scores"` OfficeID null.Int `json:"office_id,omitempty"` OweTypeID null.Int `json:"owe_type_id"` VenueID null.Int `json:"venue_id"` @@ -183,6 +184,7 @@ func (match Match) MarshalJSON() ([]byte, error) { IsBye bool `json:"is_bye"` IsStarted bool `json:"is_started"` IsPlayersDecided bool `json:"is_players_decided"` + HasScores bool `json:"has_scores"` OfficeID null.Int `json:"office_id,omitempty"` OweTypeID null.Int `json:"owe_type_id"` VenueID null.Int `json:"venue_id"` @@ -225,6 +227,7 @@ func (match Match) MarshalJSON() ([]byte, error) { IsBye: match.IsBye, IsStarted: match.IsStarted, IsPlayersDecided: match.IsPlayersDecided, + HasScores: match.HasScores, OfficeID: match.OfficeID, OweTypeID: match.OweTypeID, VenueID: match.VenueID, diff --git a/models/tournament.go b/models/tournament.go index 235d3a1c..1301d3fb 100644 --- a/models/tournament.go +++ b/models/tournament.go @@ -7,6 +7,16 @@ type TournamentMatchTemplate struct { Away int } +var TournamentTemplateSemiFinals = [2]TournamentMatchTemplate{ + {Home: 0, Away: 1}, + {Home: 1, Away: 0}, +} +var TournamentTemplateQuarterFinals = [4]TournamentMatchTemplate{ + {Home: 0, Away: 3}, + {Home: 1, Away: 2}, + {Home: 2, Away: 1}, + {Home: 3, Away: 0}, +} var TournamentTemplateLast16 = [8]TournamentMatchTemplate{ {Home: 0, Away: 7}, {Home: 4, Away: 2}, @@ -29,6 +39,7 @@ type Tournament struct { PlayoffsTournament *Tournament `json:"playoffs,omitempty"` PresetID null.Int `json:"preset_id,omitempty"` Preset *TournamentPreset `json:"preset,omitempty"` + ManualAdmin bool `json:"manual_admin"` OfficeID int `json:"office_id"` StartTime null.Time `json:"start_time"` EndTime null.Time `json:"end_time"` From 92400404c52d6641a1e2b3fc82a0f2caa9f9c195 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Sun, 13 Aug 2023 15:37:29 +0200 Subject: [PATCH 30/67] Added back match_presets deleted by accident --- cmd/serve.go | 6 ++ controllers/match_preset_controller.go | 106 +++++++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 controllers/match_preset_controller.go diff --git a/cmd/serve.go b/cmd/serve.go index 7b8e39b6..8a11742e 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -86,6 +86,12 @@ var serveCmd = &cobra.Command{ // v2 router.HandleFunc("/players", controllers_v2.GetPlayers).Methods("GET") + router.HandleFunc("/preset", controllers.AddPreset).Methods("POST") + router.HandleFunc("/preset", controllers.GetPresets).Methods("GET") + router.HandleFunc("/preset/{id}", controllers.GetPreset).Methods("GET") + router.HandleFunc("/preset/{id}", controllers.UpdatePreset).Methods("PUT") + router.HandleFunc("/preset/{id}", controllers.DeletePreset).Methods("DELETE") + router.HandleFunc("/statistics/global", controllers.GetGlobalStatistics).Methods("GET") router.HandleFunc("/statistics/global/fnc", controllers.GetGlobalStatisticsFnc).Methods("GET") router.HandleFunc("/statistics/office/{from}/{to}", controllers.GetOfficeStatistics).Methods("GET") diff --git a/controllers/match_preset_controller.go b/controllers/match_preset_controller.go new file mode 100644 index 00000000..db5fd714 --- /dev/null +++ b/controllers/match_preset_controller.go @@ -0,0 +1,106 @@ +package controllers + +import ( + "encoding/json" + "log" + "net/http" + "strconv" + + "github.com/kcapp/api/data" + "github.com/kcapp/api/models" + + "github.com/gorilla/mux" +) + +// AddPreset will create a new preset +func AddPreset(w http.ResponseWriter, r *http.Request) { + var preset models.MatchPreset + err := json.NewDecoder(r.Body).Decode(&preset) + if err != nil { + log.Println("Unable to deserialize preset json", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + err = data.AddPreset(preset) + if err != nil { + log.Println("Unable to add preset", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// GetPresets will return a list of all presets +func GetPresets(w http.ResponseWriter, r *http.Request) { + SetHeaders(w) + players, err := data.GetPresets() + if err != nil { + log.Println("Unable to get presets", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + json.NewEncoder(w).Encode(players) +} + +// GetPreset will return a preset with the given ID +func GetPreset(w http.ResponseWriter, r *http.Request) { + SetHeaders(w) + params := mux.Vars(r) + id, err := strconv.Atoi(params["id"]) + if err != nil { + log.Println("Invalid id parameter") + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + preset, err := data.GetPreset(id) + if err != nil { + log.Println("Unable to get preset", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + json.NewEncoder(w).Encode(preset) +} + +// UpdatePreset will update the given preset +func UpdatePreset(w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + id, err := strconv.Atoi(params["id"]) + if err != nil { + log.Println("Invalid id parameter") + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + var preset models.MatchPreset + err = json.NewDecoder(r.Body).Decode(&preset) + if err != nil { + log.Println("Unable to deserialize preset json", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + err = data.UpdatePreset(id, preset) + if err != nil { + log.Println("Unable to update preset", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// DeletePreset will delete a preset with the given ID +func DeletePreset(w http.ResponseWriter, r *http.Request) { + SetHeaders(w) + params := mux.Vars(r) + id, err := strconv.Atoi(params["id"]) + if err != nil { + log.Println("Invalid id parameter") + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + err = data.DeletePreset(id) + if err != nil { + log.Println("Unable to delete preset", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} From 74cff2526e774083a2f6a08a4e552b8772a60139 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Thu, 17 Aug 2023 08:23:22 +0200 Subject: [PATCH 31/67] Correctly supporting single group in tournament preset --- data/tournament.go | 81 +++++++++++++++++++++++++++++--------------- models/tournament.go | 18 +++++++--- 2 files changed, 67 insertions(+), 32 deletions(-) diff --git a/data/tournament.go b/data/tournament.go index 1a36a55c..0f576495 100644 --- a/data/tournament.go +++ b/data/tournament.go @@ -418,6 +418,7 @@ func GetTournamentOverview(id int) (map[int][]*models.TournamentOverview, error) JOIN player2tournament p2t ON p2t.player_id = p.id AND p2t.tournament_id = t.id JOIN tournament_group tg ON tg.id = p2t.tournament_group_id WHERE m.tournament_id = ? AND m.match_type_id = 1 + AND m.is_bye <> 1 GROUP BY p2l.player_id, tg.id ORDER BY tg.division, pts DESC, diff DESC, is_relegated, manual_order`, id) if err != nil { @@ -860,7 +861,10 @@ func GeneratePlayoffsTournament(tournamentID int) (*models.Tournament, error) { } group1 := overview[keys[0]] - group2 := overview[keys[1]] + var group2 []*models.TournamentOverview + if len(keys) > 1 { + group2 = overview[keys[1]] + } preset := tournament.Preset playoffsGroupID := preset.PlayoffsTournamentGroup.ID @@ -888,6 +892,7 @@ func GeneratePlayoffsTournament(tournamentID int) (*models.Tournament, error) { IsPlayoffs: true, OfficeID: tournament.OfficeID, Players: players, + PresetID: tournament.PresetID, StartTime: null.TimeFrom(time.Now()), EndTime: null.TimeFrom(time.Now()), ManualAdmin: tournament.ManualAdmin, @@ -903,7 +908,7 @@ func GeneratePlayoffsTournament(tournamentID int) (*models.Tournament, error) { matches := make([]*models.Match, 0) // Create Grand Final - match, err := createTournamentMatch(playoffs.ID, []int{placeholderHomeID, placeholderAwayID}, startingScore, 1, + match, err := createTournamentMatch(playoffs.ID, []int{placeholderHomeID, placeholderAwayID}, startingScore, models.X01, tournament.OfficeID, mt, preset.MatchModeGrandFinal) if err != nil { return nil, err @@ -912,29 +917,33 @@ func GeneratePlayoffsTournament(tournamentID int) (*models.Tournament, error) { // Create Semi Final Matches if numPlayers > 4 { - for i := 0; i < 2; i++ { - match, err := createTournamentMatch(playoffs.ID, []int{placeholderHomeID, placeholderAwayID}, startingScore, 1, - tournament.OfficeID, mt, preset.MatchModeSemiFinal) - if err != nil { - return nil, err - } - matches = append(matches, match) + semis, err := createTournamentMatches(2, playoffs.ID, []int{placeholderHomeID, placeholderAwayID}, startingScore, models.X01, + tournament.OfficeID, mt, preset.MatchModeSemiFinal) + if err != nil { + return nil, err } + matches = append(matches, semis...) } else { - // Create Semi Final Matches for i := 0; i < 2; i++ { temp := models.TournamentTemplateSemiFinals[i] + g2 := group2 + if len(g2) == 0 { + // Single group + g2 = group1 + temp = models.TournamentTemplateSemiFinalsSingle[i] + } + home := getCompetitor(group1, temp.Home) if home == -1 { // Walkover, so use placeholder home = walkoverPlayerID } - away := getCompetitor(group2, temp.Away) + away := getCompetitor(g2, temp.Away) if away == -1 { // Walkover, so use placeholder away = walkoverPlayerID } - match, err := createTournamentMatch(playoffs.ID, []int{home, away}, startingScore, 1, + match, err := createTournamentMatch(playoffs.ID, []int{home, away}, startingScore, models.X01, tournament.OfficeID, mt, preset.MatchModeSemiFinal) if err != nil { return nil, err @@ -943,16 +952,14 @@ func GeneratePlayoffsTournament(tournamentID int) (*models.Tournament, error) { } } + // Create Quarter Final Matches if numPlayers > 8 { - // Create Quarter Final Matches - for i := 0; i < 4; i++ { - match, err := createTournamentMatch(playoffs.ID, []int{placeholderHomeID, placeholderAwayID}, startingScore, 1, - tournament.OfficeID, mt, preset.MatchModeQuarterFinal) - if err != nil { - return nil, err - } - matches = append(matches, match) + quarters, err := createTournamentMatches(4, playoffs.ID, []int{placeholderHomeID, placeholderAwayID}, startingScore, models.X01, + tournament.OfficeID, mt, preset.MatchModeQuarterFinal) + if err != nil { + return nil, err } + matches = append(matches, quarters...) // Create initial 8 matches for i := 0; i < 8; i++ { @@ -967,7 +974,7 @@ func GeneratePlayoffsTournament(tournamentID int) (*models.Tournament, error) { // Walkover, so use placeholder away = walkoverPlayerID } - match, err := createTournamentMatch(playoffs.ID, []int{home, away}, startingScore, 1, + match, err := createTournamentMatch(playoffs.ID, []int{home, away}, startingScore, models.X01, tournament.OfficeID, mt, preset.MatchModeLast16) if err != nil { return nil, err @@ -975,20 +982,26 @@ func GeneratePlayoffsTournament(tournamentID int) (*models.Tournament, error) { matches = append(matches, match) } } else { - // Create Quarter Final Matches for i := 0; i < 4; i++ { + log.Printf("Quarter Final %d", i) temp := models.TournamentTemplateQuarterFinals[i] + g2 := group2 + if len(g2) == 0 { + // Single group + g2 = group1 + temp = models.TournamentTemplateQuarterFinalsSingle[i] + } home := getCompetitor(group1, temp.Home) if home == -1 { // Walkover, so use placeholder home = walkoverPlayerID } - away := getCompetitor(group2, temp.Away) + away := getCompetitor(g2, temp.Away) if away == -1 { // Walkover, so use placeholder away = walkoverPlayerID } - match, err := createTournamentMatch(playoffs.ID, []int{home, away}, startingScore, 1, + match, err := createTournamentMatch(playoffs.ID, []int{home, away}, startingScore, models.X01, tournament.OfficeID, mt, preset.MatchModeQuarterFinal) if err != nil { return nil, err @@ -1045,10 +1058,10 @@ func GeneratePlayoffsTournament(tournamentID int) (*models.Tournament, error) { {MatchID: m2.ID, OrderOfPlay: 2, TournamentGroup: tg, MatchDisplayname: "Match 2", WinnerOutcomeMatchID: null.IntFrom(int64(qf1.ID)), IsWinnerOutcomeHome: false, GrandFinal: false, SemiFinal: false}, {MatchID: m3.ID, OrderOfPlay: 3, TournamentGroup: tg, MatchDisplayname: "Match 3", WinnerOutcomeMatchID: null.IntFrom(int64(qf2.ID)), IsWinnerOutcomeHome: true, GrandFinal: false, SemiFinal: false}, {MatchID: m4.ID, OrderOfPlay: 4, TournamentGroup: tg, MatchDisplayname: "Match 4", WinnerOutcomeMatchID: null.IntFrom(int64(qf2.ID)), IsWinnerOutcomeHome: false, GrandFinal: false, SemiFinal: false}, - {MatchID: m5.ID, OrderOfPlay: 5, TournamentGroup: tg, MatchDisplayname: "Match 1", WinnerOutcomeMatchID: null.IntFrom(int64(qf3.ID)), IsWinnerOutcomeHome: true, GrandFinal: false, SemiFinal: false}, - {MatchID: m6.ID, OrderOfPlay: 6, TournamentGroup: tg, MatchDisplayname: "Match 2", WinnerOutcomeMatchID: null.IntFrom(int64(qf3.ID)), IsWinnerOutcomeHome: false, GrandFinal: false, SemiFinal: false}, - {MatchID: m7.ID, OrderOfPlay: 7, TournamentGroup: tg, MatchDisplayname: "Match 3", WinnerOutcomeMatchID: null.IntFrom(int64(qf4.ID)), IsWinnerOutcomeHome: true, GrandFinal: false, SemiFinal: false}, - {MatchID: m8.ID, OrderOfPlay: 8, TournamentGroup: tg, MatchDisplayname: "Match 4", WinnerOutcomeMatchID: null.IntFrom(int64(qf4.ID)), IsWinnerOutcomeHome: false, GrandFinal: false, SemiFinal: false}, + {MatchID: m5.ID, OrderOfPlay: 5, TournamentGroup: tg, MatchDisplayname: "Match 5", WinnerOutcomeMatchID: null.IntFrom(int64(qf3.ID)), IsWinnerOutcomeHome: true, GrandFinal: false, SemiFinal: false}, + {MatchID: m6.ID, OrderOfPlay: 6, TournamentGroup: tg, MatchDisplayname: "Match 6", WinnerOutcomeMatchID: null.IntFrom(int64(qf3.ID)), IsWinnerOutcomeHome: false, GrandFinal: false, SemiFinal: false}, + {MatchID: m7.ID, OrderOfPlay: 7, TournamentGroup: tg, MatchDisplayname: "Match 7", WinnerOutcomeMatchID: null.IntFrom(int64(qf4.ID)), IsWinnerOutcomeHome: true, GrandFinal: false, SemiFinal: false}, + {MatchID: m8.ID, OrderOfPlay: 8, TournamentGroup: tg, MatchDisplayname: "Match 8", WinnerOutcomeMatchID: null.IntFrom(int64(qf4.ID)), IsWinnerOutcomeHome: false, GrandFinal: false, SemiFinal: false}, } err = insertMetadata(playoffsMatches) if err != nil { @@ -1076,6 +1089,18 @@ func GeneratePlayoffsTournament(tournamentID int) (*models.Tournament, error) { return GetTournament(playoffs.ID) } +func createTournamentMatches(num int, tournamentID int, players []int, startingScore int, venueID int, officeID int, matchType *models.MatchType, matchMode *models.MatchMode) ([]*models.Match, error) { + matches := make([]*models.Match, 0) + for i := 0; i < num; i++ { + match, err := createTournamentMatch(tournamentID, players, startingScore, venueID, officeID, matchType, matchMode) + if err != nil { + return nil, err + } + matches = append(matches, match) + } + return matches, nil +} + func createTournamentMatch(tournamentID int, players []int, startingScore int, venueID int, officeID int, matchType *models.MatchType, matchMode *models.MatchMode) (*models.Match, error) { match, err := NewMatch(models.Match{ MatchType: matchType, diff --git a/models/tournament.go b/models/tournament.go index 1301d3fb..41173bfb 100644 --- a/models/tournament.go +++ b/models/tournament.go @@ -11,12 +11,22 @@ var TournamentTemplateSemiFinals = [2]TournamentMatchTemplate{ {Home: 0, Away: 1}, {Home: 1, Away: 0}, } -var TournamentTemplateQuarterFinals = [4]TournamentMatchTemplate{ +var TournamentTemplateSemiFinalsSingle = [2]TournamentMatchTemplate{ {Home: 0, Away: 3}, {Home: 1, Away: 2}, +} +var TournamentTemplateQuarterFinals = [4]TournamentMatchTemplate{ + {Home: 0, Away: 3}, {Home: 2, Away: 1}, + {Home: 1, Away: 2}, {Home: 3, Away: 0}, } +var TournamentTemplateQuarterFinalsSingle = [4]TournamentMatchTemplate{ + {Home: 0, Away: 7}, + {Home: 3, Away: 4}, + {Home: 1, Away: 6}, + {Home: 2, Away: 5}, +} var TournamentTemplateLast16 = [8]TournamentMatchTemplate{ {Home: 0, Away: 7}, {Home: 4, Away: 2}, @@ -100,9 +110,9 @@ type TournamentPreset struct { MatchModeQuarterFinal *MatchMode `json:"match_mode_id_quarter_final"` MatchModeSemiFinal *MatchMode `json:"match_mode_id_semi_final"` MatchModeGrandFinal *MatchMode `json:"match_mode_id_grand_final"` - PlayoffsTournamentGroup *TournamentGroup `json:"playoffs_tournament_group_id"` - Group1TournamentGroup *TournamentGroup `json:"group1_tournament_group_id"` - Group2TournamentGroup *TournamentGroup `json:"group2_tournament_group_id"` + PlayoffsTournamentGroup *TournamentGroup `json:"playoffs_tournament_group"` + Group1TournamentGroup *TournamentGroup `json:"group1_tournament_group"` + Group2TournamentGroup *TournamentGroup `json:"group2_tournament_group"` PlayerIDWalkover int `json:"player_id_walkover"` PlayerIDPlaceholderHome int `json:"player_id_placeholder_home"` PlayerIDPlaceholderAway int `json:"player_id_placeholder_away"` From d3f5f66be7cbac9851a38cfcdf5ce5fbf4041dab Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Sat, 26 Aug 2023 20:55:15 +0200 Subject: [PATCH 32/67] Initial work on badges --- cmd/badge.go | 15 +++++++ cmd/recalculate_badge.go | 33 ++++++++++++++ data/badge.go | 46 ++++++++++++++++++- data/leg.go | 6 +++ models/badge.go | 96 ++++++++++++++++++++++++++++++++++++++++ models/dart.go | 2 + models/leg.go | 5 +++ 7 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 cmd/badge.go create mode 100644 cmd/recalculate_badge.go diff --git a/cmd/badge.go b/cmd/badge.go new file mode 100644 index 00000000..a50dbc23 --- /dev/null +++ b/cmd/badge.go @@ -0,0 +1,15 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +// badgeCmd represents the badge command +var badgeCmd = &cobra.Command{ + Use: "badge", + Short: "Modify Badges", +} + +func init() { + rootCmd.AddCommand(badgeCmd) +} diff --git a/cmd/recalculate_badge.go b/cmd/recalculate_badge.go new file mode 100644 index 00000000..9754ef6a --- /dev/null +++ b/cmd/recalculate_badge.go @@ -0,0 +1,33 @@ +package cmd + +import ( + "log" + + "github.com/kcapp/api/models" + "github.com/spf13/cobra" +) + +// recalculateBadgeCmd represents the recalculate command +var recalculateBadgeCmd = &cobra.Command{ + Use: "recalculate", + Short: "Recalculate badge", + Long: `Recalculate badges earned by each player`, + PersistentPreRun: func(cmd *cobra.Command, args []string) { + configFileParam, err := cmd.Flags().GetString("config") + if err != nil { + panic(err) + } + config, err := models.GetConfig(configFileParam) + if err != nil { + panic(err) + } + models.InitDB(config.GetMysqlConnectionString()) + }, + Run: func(cmd *cobra.Command, args []string) { + log.Printf("Hello") + }, +} + +func init() { + badgeCmd.AddCommand(recalculateBadgeCmd) +} diff --git a/data/badge.go b/data/badge.go index 03398677..1868df4c 100644 --- a/data/badge.go +++ b/data/badge.go @@ -1,6 +1,11 @@ package data -import "github.com/kcapp/api/models" +import ( + "database/sql" + "log" + + "github.com/kcapp/api/models" +) func GetBadges() ([]*models.Badge, error) { rows, err := models.DB.Query(` @@ -31,3 +36,42 @@ func GetBadges() ([]*models.Badge, error) { } return badges, nil } + +func CheckLegForBadges(leg *models.Leg) error { + tx, err := models.DB.Begin() + if err != nil { + return err + } + for _, badge := range models.LegBadges { + valid, playerID := badge.Validate(leg) + if valid { + if playerID != nil { + addLegBadge(tx, *playerID, leg.ID, badge) + if err != nil { + return err + } + } else { + for _, playerID := range leg.Players { + addLegBadge(tx, playerID, leg.ID, badge) + if err != nil { + return err + } + } + } + } + } + tx.Commit() + + return nil +} + +func addLegBadge(tx *sql.Tx, playerID int, legID int, badge models.LegBadge) error { + _, err := tx.Exec("INSERT INTO player2badge (player_id, badge_id, leg_id) VALUES (?, ?, ?)", + playerID, badge.GetID(), legID) + if err != nil { + tx.Rollback() + return err + } + log.Printf("Added badge %d to player %d on leg %d", badge.GetID(), playerID, legID) + return nil +} diff --git a/data/leg.go b/data/leg.go index 9ed0a94b..53b56390 100644 --- a/data/leg.go +++ b/data/leg.go @@ -548,6 +548,12 @@ func FinishLeg(visit models.Visit) error { } } } + + // Calculate badges + err = CheckLegForBadges(leg) + if err != nil { + return err + } } else { log.Printf("Match %d is not finished, creating next leg", match.ID) var matchType *int diff --git a/models/badge.go b/models/badge.go index c4d5ecec..a72eb5e0 100644 --- a/models/badge.go +++ b/models/badge.go @@ -11,6 +11,8 @@ type Badge struct { ID int `json:"id"` Name string `json:"name"` Description string `json:"description"` + Hidden bool `json:"hidden"` + Secret bool `json:"secret"` Filename string `json:"filename"` } @@ -18,6 +20,100 @@ type Badge struct { type PlayerBadge struct { Badge *Badge `json:"badge"` PlayerID int `json:"player_id"` + Level null.Int `json:"level,omitempty"` LegID null.Int `json:"leg_id,omitempty"` CreatedAt time.Time `json:"created_at"` } + +var LegBadges = []LegBadge{ + BadgeDoubleDouble{ID: 8}, + BadgeTripleDouble{ID: 9}, + BadgeMadHouse{ID: 10}, + BadgeMerryChristmas{ID: 11}, + BadgeBigFish{ID: 14}, + BadgeGettingCrowded{ID: 17}, + BadgeBullseye{ID: 20}, +} + +type LegBadge interface { + GetID() int + Validate(*Leg) (bool, *int) +} + +type BadgeMerryChristmas struct{ ID int } +type BadgeGettingCrowded struct{ ID int } +type BadgeBullseye struct{ ID int } +type BadgeDoubleDouble struct{ ID int } +type BadgeTripleDouble struct{ ID int } +type BadgeMadHouse struct{ ID int } +type BadgeBigFish struct{ ID int } + +type BadgeHighScore struct{ ID int } +type BadgeHigherScore struct{ ID int } +type BadgeTheMaximum struct{ ID int } +type BadgeGlobetrotter struct{ ID int } +type BadgePartyOfTwo struct{ ID int } +type BadgeWorkFromHome struct{ ID int } +type BadgeHardlyWorking struct{ ID int } +type BadgeWeeklyPlayer struct{ ID int } + +func (b BadgeGettingCrowded) GetID() int { + return b.ID +} +func (b BadgeGettingCrowded) Validate(leg *Leg) (bool, *int) { + return len(leg.Players) > 4, nil +} +func (b BadgeMerryChristmas) GetID() int { + return b.ID +} +func (b BadgeMerryChristmas) Validate(leg *Leg) (bool, *int) { + d := leg.Endtime.Time + return d.Day() == 25 && d.Month() == 12, nil +} +func (b BadgeBullseye) GetID() int { + return b.ID +} +func (b BadgeBullseye) Validate(leg *Leg) (bool, *int) { + visit := leg.GetLastVisit() + last := visit.GetLastDart() + return last.ValueRaw() == BULLSEYE && last.Multiplier == DOUBLE, &visit.PlayerID +} + +func (b BadgeDoubleDouble) GetID() int { + return b.ID +} + +func (b BadgeDoubleDouble) Validate(leg *Leg) (bool, *int) { + visit := leg.GetLastVisit() + return visit.SecondDart.IsDouble(), &visit.PlayerID +} + +func (b BadgeTripleDouble) GetID() int { + return b.ID +} + +func (b BadgeTripleDouble) Validate(leg *Leg) (bool, *int) { + visit := leg.GetLastVisit() + return visit.FirstDart.IsDouble() && visit.SecondDart.IsDouble(), &visit.PlayerID +} + +func (b BadgeMadHouse) GetID() int { + return b.ID +} + +func (b BadgeMadHouse) Validate(leg *Leg) (bool, *int) { + visit := leg.GetLastVisit() + last := visit.GetLastDart() + return last.IsDouble() && last.ValueRaw() == 1, &visit.PlayerID +} + +func (b BadgeBigFish) GetID() int { + return b.ID +} + +func (b BadgeBigFish) Validate(leg *Leg) (bool, *int) { + visit := leg.GetLastVisit() + return visit.FirstDart.IsTriple() && visit.FirstDart.ValueRaw() == 20 && + visit.SecondDart.IsTriple() && visit.SecondDart.ValueRaw() == 20 && + visit.ThirdDart.IsDouble() && visit.ThirdDart.IsBull(), &visit.PlayerID +} diff --git a/models/dart.go b/models/dart.go index 3d9f542a..fc8d323f 100644 --- a/models/dart.go +++ b/models/dart.go @@ -16,6 +16,8 @@ const ( TRIPLE = 3 ) +const BULLSEYE = 25 + var ( // CRICKETDARTS var holding darts aimed at in a game of Cricket CRICKETDARTS = []int{15, 16, 17, 18, 19, 20, 25} diff --git a/models/leg.go b/models/leg.go index 31af761a..1cf80937 100644 --- a/models/leg.go +++ b/models/leg.go @@ -388,3 +388,8 @@ func DecorateVisitsScam(players map[int]*Player2Leg, visits []*Visit) { } } } + +// GetLastVisit returns the last visit of the leg. +func (leg Leg) GetLastVisit() *Visit { + return leg.Visits[len(leg.Visits)-1] +} From 8c987927d7bf96c9d4ebd2403e669a9ceab144e8 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Tue, 29 Aug 2023 21:42:36 +0200 Subject: [PATCH 33/67] Added option to add player to tournament --- cmd/serve.go | 1 + controllers/tournament_controller.go | 29 ++++++++++ data/match.go | 6 +- data/tournament.go | 87 ++++++++++++++++++++++++++-- data/tournament_preset.go | 8 +++ models/tournament.go | 9 +-- 6 files changed, 130 insertions(+), 10 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index 8a11742e..463ae77c 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -132,6 +132,7 @@ var serveCmd = &cobra.Command{ router.HandleFunc("/tournament/preset/{id}", controllers.GetTournamentPreset).Methods("GET") //router.HandleFunc("/tournament/preset/{id}", controllers.UpdateTournamentPreset).Methods("PUT") router.HandleFunc("/tournament/{id}", controllers.GetTournament).Methods("GET") + router.HandleFunc("/tournament/{id}/player", controllers.AddPlayerToTournament).Methods("POST") router.HandleFunc("/tournament/{id}/player/{player_id}", controllers.GetTournamentPlayerMatches).Methods("GET") router.HandleFunc("/tournament/{id}/matches", controllers.GetTournamentMatches).Methods("GET") router.HandleFunc("/tournament/{id}/matches/result", controllers.GetTournamentMatchResults).Methods("GET") diff --git a/controllers/tournament_controller.go b/controllers/tournament_controller.go index 827db15f..ac51bffa 100644 --- a/controllers/tournament_controller.go +++ b/controllers/tournament_controller.go @@ -427,3 +427,32 @@ func GetTournamentPlayerMatches(w http.ResponseWriter, r *http.Request) { } json.NewEncoder(w).Encode(matches) } + +// AddPlayerToTournament will add the given player to the tournament +func AddPlayerToTournament(w http.ResponseWriter, r *http.Request) { + SetHeaders(w) + var input models.Player2Tournament + err := json.NewDecoder(r.Body).Decode(&input) + if err != nil { + log.Println("Unable to deserialize body", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + params := mux.Vars(r) + id, err := strconv.Atoi(params["id"]) + if err != nil { + log.Println("Invalid id parameter") + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + matches, err := data.AddPlayerToTournament(input.PlayerID, input.TournamentGroupID, id) + if err != nil { + log.Println("Unable to add player to tournament", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + json.NewEncoder(w).Encode(matches) + +} diff --git a/data/match.go b/data/match.go index 7087baf2..866ab642 100644 --- a/data/match.go +++ b/data/match.go @@ -45,7 +45,11 @@ func NewMatch(match models.Match) (*models.Match, error) { } if match.MatchType.ID == models.X01 || match.MatchType.ID == models.X01HANDICAP { params := match.Legs[0].Parameters - _, err = tx.Exec("INSERT INTO leg_parameters (leg_id, outshot_type_id) VALUES (?, ?)", legID, params.OutshotType.ID) + outshotType := models.OUTSHOTDOUBLE + if params != nil { + outshotType = params.OutshotType.ID + } + _, err = tx.Exec("INSERT INTO leg_parameters (leg_id, outshot_type_id) VALUES (?, ?)", legID, outshotType) if err != nil { tx.Rollback() return nil, err diff --git a/data/tournament.go b/data/tournament.go index 0f576495..58ccc26a 100644 --- a/data/tournament.go +++ b/data/tournament.go @@ -794,6 +794,11 @@ func NewTournament(tournament models.Tournament) (*models.Tournament, error) { // GenerateTournament generates a new tournament func GenerateTournament(input models.Tournament) (*models.Tournament, error) { + preset, err := GetTournamentPreset(int(input.PresetID.Int64)) + if err != nil { + return nil, err + } + officeID := input.OfficeID tournament, err := NewTournament(models.Tournament{ Name: input.Name, @@ -811,8 +816,6 @@ func GenerateTournament(input models.Tournament) (*models.Tournament, error) { } players := input.Players - mt := models.MatchType{ID: models.X01} - mo := models.MatchMode{ID: 2} for i := 0; i < len(players); i++ { for j := i + 1; j < len(players); j++ { if players[i].TournamentGroupID != players[j].TournamentGroupID { @@ -821,15 +824,15 @@ func GenerateTournament(input models.Tournament) (*models.Tournament, error) { } match, err := NewMatch(models.Match{ - MatchType: &mt, - MatchMode: &mo, + MatchType: preset.MatchType, + MatchMode: preset.MatchMode, //VenueID: 1, OfficeID: null.IntFrom(int64(officeID)), IsPractice: false, TournamentID: null.IntFrom(int64(tournament.ID)), Players: []int{players[i].PlayerID, players[j].PlayerID}, Legs: []*models.Leg{{ - StartingScore: 501, + StartingScore: preset.StartingScore, Parameters: &models.LegParameters{OutshotType: &models.OutshotType{ID: models.OUTSHOTDOUBLE}}}}, }) if err != nil { @@ -1227,6 +1230,80 @@ func GetTournamentMatchesForPlayer(tournamentID int, playerID int) ([]*models.Ma return matches, nil } +// GetTournamentGroupPlayers will return all players in the given group in the given tournament +func GetTournamentGroupPlayers(tournamentID int, groupID int) ([]*models.Player2Tournament, error) { + rows, err := models.DB.Query(` + SELECT p2t.player_id, p2t.tournament_id, p2t.tournament_group_id FROM player2tournament p2t + WHERE p2t.tournament_id = ? AND p2t.tournament_group_id = ?`, tournamentID, groupID) + if err != nil { + return nil, err + } + defer rows.Close() + + players := make([]*models.Player2Tournament, 0) + for rows.Next() { + p2t := new(models.Player2Tournament) + err := rows.Scan(&p2t.PlayerID, &p2t.TournamentID, &p2t.TournamentGroupID) + if err != nil { + return nil, err + } + players = append(players, p2t) + } + if err = rows.Err(); err != nil { + return nil, err + } + return players, nil +} + +func AddPlayerToTournament(playerID int, tournamentGroupID int, tournamentID int) ([]*models.Match, error) { + tournament, err := GetTournament(tournamentID) + if err != nil { + return nil, err + } + + players, err := GetTournamentGroupPlayers(tournamentID, tournamentGroupID) + if err != nil { + return nil, err + } + + tx, err := models.DB.Begin() + if err != nil { + return nil, err + } + + // Add player to tournament + _, err = tx.Exec(`INSERT INTO player2tournament (player_id, tournament_id, tournament_group_id) VALUES (?, ?, ?)`, + playerID, tournamentID, tournamentGroupID) + if err != nil { + tx.Rollback() + return nil, err + } + tx.Commit() + + // Create matches for new player + matches := make([]*models.Match, 0) + for i := 0; i < len(players); i++ { + match, err := NewMatch(models.Match{ + MatchType: tournament.Preset.MatchType, + MatchMode: tournament.Preset.MatchMode, + //VenueID: 1, + OfficeID: null.IntFrom(int64(tournament.OfficeID)), + IsPractice: false, + TournamentID: null.IntFrom(int64(tournament.ID)), + Players: []int{players[i].PlayerID, playerID}, + Legs: []*models.Leg{{ + StartingScore: tournament.Preset.StartingScore, + Parameters: &models.LegParameters{OutshotType: &models.OutshotType{ID: models.OUTSHOTDOUBLE}}}}, + }) + if err != nil { + return nil, err + } + log.Printf("Generated Match %d for %d vs %d", match.ID, players[i].PlayerID, playerID) + matches = append(matches, match) + } + return matches, nil +} + func getCompetitor(group []*models.TournamentOverview, num int) int { if num >= len(group) { // Not enough players, walkover diff --git a/data/tournament_preset.go b/data/tournament_preset.go index 44ede358..07079d21 100644 --- a/data/tournament_preset.go +++ b/data/tournament_preset.go @@ -10,6 +10,7 @@ func GetTournamentPresets() ([]*models.TournamentPreset, error) { SELECT tp.id, tp.name, tp.starting_score, tp.description, tp.match_type_id, mt.name, + mm.id, mm.name, mm.short_name, mml16.id, mml16.name, mml16.short_name, mmqf.id, mmqf.name, mmqf.short_name, mmsf.id, mmsf.name, mmsf.short_name, @@ -18,6 +19,7 @@ func GetTournamentPresets() ([]*models.TournamentPreset, error) { tp.player_id_walkover, tp.player_id_placeholder_home, tp.player_id_placeholder_away FROM tournament_preset tp JOIN match_type mt ON mt.id = tp.match_type_id + JOIN match_mode mm ON mm.id = tp.match_mode_id JOIN match_mode mml16 ON mml16.id = tp.match_mode_id_last_16 JOIN match_mode mmqf ON mmqf.id = tp.match_mode_id_quarter_final JOIN match_mode mmsf ON mmsf.id = tp.match_mode_id_semi_final @@ -33,6 +35,7 @@ func GetTournamentPresets() ([]*models.TournamentPreset, error) { presets := make([]*models.TournamentPreset, 0) for rows.Next() { tp := new(models.TournamentPreset) + tp.MatchMode = new(models.MatchMode) tp.MatchModeLast16 = new(models.MatchMode) tp.MatchModeQuarterFinal = new(models.MatchMode) tp.MatchModeSemiFinal = new(models.MatchMode) @@ -43,6 +46,7 @@ func GetTournamentPresets() ([]*models.TournamentPreset, error) { tp.Group2TournamentGroup = new(models.TournamentGroup) err := rows.Scan(&tp.ID, &tp.Name, &tp.StartingScore, &tp.Description, &tp.MatchType.ID, &tp.MatchType.Name, + &tp.MatchMode.ID, &tp.MatchMode.Name, &tp.MatchMode.ShortName, &tp.MatchModeLast16.ID, &tp.MatchModeLast16.Name, &tp.MatchModeLast16.ShortName, &tp.MatchModeQuarterFinal.ID, &tp.MatchModeQuarterFinal.Name, &tp.MatchModeQuarterFinal.ShortName, &tp.MatchModeSemiFinal.ID, &tp.MatchModeSemiFinal.Name, &tp.MatchModeSemiFinal.ShortName, @@ -65,6 +69,7 @@ func GetTournamentPresets() ([]*models.TournamentPreset, error) { // GetPreset returns the preset for the given ID func GetTournamentPreset(id int) (*models.TournamentPreset, error) { tp := new(models.TournamentPreset) + tp.MatchMode = new(models.MatchMode) tp.MatchModeLast16 = new(models.MatchMode) tp.MatchModeQuarterFinal = new(models.MatchMode) tp.MatchModeSemiFinal = new(models.MatchMode) @@ -77,6 +82,7 @@ func GetTournamentPreset(id int) (*models.TournamentPreset, error) { SELECT tp.id, tp.name, tp.starting_score, tp.description, tp.match_type_id, mt.name, + mm.id, mm.name, mm.short_name, mml16.id, mml16.name, mml16.short_name, mmqf.id, mmqf.name, mmqf.short_name, mmsf.id, mmsf.name, mmsf.short_name, @@ -85,6 +91,7 @@ func GetTournamentPreset(id int) (*models.TournamentPreset, error) { tp.player_id_walkover, tp.player_id_placeholder_home, tp.player_id_placeholder_away FROM tournament_preset tp JOIN match_type mt ON mt.id = tp.match_type_id + JOIN match_mode mm ON mm.id = tp.match_mode_id JOIN match_mode mml16 ON mml16.id = tp.match_mode_id_last_16 JOIN match_mode mmqf ON mmqf.id = tp.match_mode_id_quarter_final JOIN match_mode mmsf ON mmsf.id = tp.match_mode_id_semi_final @@ -94,6 +101,7 @@ func GetTournamentPreset(id int) (*models.TournamentPreset, error) { JOIN tournament_group tg2 ON tg2.id = tp.group2_tournament_group_id WHERE tp.id = ?`, id). Scan(&tp.ID, &tp.Name, &tp.StartingScore, &tp.Description, &tp.MatchType.ID, &tp.MatchType.Name, + &tp.MatchMode.ID, &tp.MatchMode.Name, &tp.MatchMode.ShortName, &tp.MatchModeLast16.ID, &tp.MatchModeLast16.Name, &tp.MatchModeLast16.ShortName, &tp.MatchModeQuarterFinal.ID, &tp.MatchModeQuarterFinal.Name, &tp.MatchModeQuarterFinal.ShortName, &tp.MatchModeSemiFinal.ID, &tp.MatchModeSemiFinal.Name, &tp.MatchModeSemiFinal.ShortName, diff --git a/models/tournament.go b/models/tournament.go index 41173bfb..05291355 100644 --- a/models/tournament.go +++ b/models/tournament.go @@ -106,10 +106,11 @@ type TournamentPreset struct { Name string `json:"name"` MatchType *MatchType `json:"match_type_id"` StartingScore int `json:"starting_score"` - MatchModeLast16 *MatchMode `json:"match_mode_id_last_16"` - MatchModeQuarterFinal *MatchMode `json:"match_mode_id_quarter_final"` - MatchModeSemiFinal *MatchMode `json:"match_mode_id_semi_final"` - MatchModeGrandFinal *MatchMode `json:"match_mode_id_grand_final"` + MatchMode *MatchMode `json:"match_mode"` + MatchModeLast16 *MatchMode `json:"match_mode_last_16"` + MatchModeQuarterFinal *MatchMode `json:"match_mode_quarter_final"` + MatchModeSemiFinal *MatchMode `json:"match_mode_semi_final"` + MatchModeGrandFinal *MatchMode `json:"match_mode_grand_final"` PlayoffsTournamentGroup *TournamentGroup `json:"playoffs_tournament_group"` Group1TournamentGroup *TournamentGroup `json:"group1_tournament_group"` Group2TournamentGroup *TournamentGroup `json:"group2_tournament_group"` From b8ce9a32442a152bf46b0d977e3e9e40110edefa Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Tue, 5 Sep 2023 20:09:02 +0200 Subject: [PATCH 34/67] Don't return bye matches to match list --- data/match.go | 3 ++- data/tournament.go | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/match.go b/data/match.go index 866ab642..56f18b86 100644 --- a/data/match.go +++ b/data/match.go @@ -188,6 +188,7 @@ func GetActiveMatches() ([]*models.Match, error) { LEFT JOIN player2leg p2l ON p2l.match_id = m.id WHERE m.is_finished = 0 AND l.updated_at > NOW() - INTERVAL 2 MINUTE + AND m.is_bye <> 1 GROUP BY m.id ORDER BY m.id DESC`) if err != nil { @@ -321,7 +322,7 @@ func GetMatchesLimit(start int, limit int) ([]*models.Match, error) { LEFT JOIN player2tournament p2t ON p2t.tournament_id = m.tournament_id AND p2t.player_id = p2l.player_id LEFT JOIN tournament t ON t.id = p2t.tournament_id LEFT JOIN tournament_group tg ON tg.id = p2t.tournament_group_id - WHERE m.created_at <= NOW() + WHERE m.created_at <= NOW() AND m.is_bye <> 1 GROUP BY m.id ORDER BY m.created_at DESC, m.id DESC LIMIT ?, ?`, start, limit) diff --git a/data/tournament.go b/data/tournament.go index 58ccc26a..a8e979a2 100644 --- a/data/tournament.go +++ b/data/tournament.go @@ -841,7 +841,6 @@ func GenerateTournament(input models.Tournament) (*models.Tournament, error) { log.Printf("Generated Match %d for %d vs %d", match.ID, players[i].PlayerID, players[j].PlayerID) } } - return tournament, nil } From 2fdb70afadf5a51b6545ad23361f64532a02152a Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Tue, 12 Sep 2023 17:57:54 +0200 Subject: [PATCH 35/67] Initial version of badges --- cmd/recalculate_badge.go | 14 +- cmd/serve.go | 78 --------- data/badge.go | 90 +++++++++- data/leg.go | 55 +++++- data/match.go | 6 +- data/player.go | 24 ++- data/recalculate.go | 111 +++++++++++- data/statistics_x01.go | 43 +++++ data/tournament.go | 38 +++++ models/badge.go | 358 ++++++++++++++++++++++++++++++++++----- models/bot.go | 12 ++ models/player.go | 1 + models/tournament.go | 2 +- 13 files changed, 683 insertions(+), 149 deletions(-) create mode 100644 models/bot.go diff --git a/cmd/recalculate_badge.go b/cmd/recalculate_badge.go index 9754ef6a..7176d6d3 100644 --- a/cmd/recalculate_badge.go +++ b/cmd/recalculate_badge.go @@ -1,8 +1,7 @@ package cmd import ( - "log" - + "github.com/kcapp/api/data" "github.com/kcapp/api/models" "github.com/spf13/cobra" ) @@ -24,7 +23,16 @@ var recalculateBadgeCmd = &cobra.Command{ models.InitDB(config.GetMysqlConnectionString()) }, Run: func(cmd *cobra.Command, args []string) { - log.Printf("Hello") + err := data.RecalculateGlobalBadges() + if err != nil { + panic(err) + } + + err = data.RecalculateLegBadges() + if err != nil { + panic(err) + } + }, } diff --git a/cmd/serve.go b/cmd/serve.go index 97d55659..a293af41 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -1,11 +1,9 @@ package cmd import ( - "database/sql" "fmt" "log" "net/http" - "time" "github.com/gorilla/mux" "github.com/kcapp/api/controllers" @@ -25,7 +23,6 @@ var serveCmd = &cobra.Command{ panic(err) } models.InitDB(config.GetMysqlConnectionString()) - GetLongestWinStreak() router := mux.NewRouter() router.Methods("OPTIONS").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -131,10 +128,8 @@ var serveCmd = &cobra.Command{ router.HandleFunc("/tournament/groups", controllers.AddTournamentGroup).Methods("POST") router.HandleFunc("/tournament/groups", controllers.GetTournamentGroups).Methods("GET") router.HandleFunc("/tournament/standings", controllers.GetTournamentStandings).Methods("GET") - //router.HandleFunc("/tournament/preset", controllers.AddTournamentPreset).Methods("POST") router.HandleFunc("/tournament/preset", controllers.GetTournamentPresets).Methods("GET") router.HandleFunc("/tournament/preset/{id}", controllers.GetTournamentPreset).Methods("GET") - //router.HandleFunc("/tournament/preset/{id}", controllers.UpdateTournamentPreset).Methods("PUT") router.HandleFunc("/tournament/{id}", controllers.GetTournament).Methods("GET") router.HandleFunc("/tournament/{id}/player", controllers.AddPlayerToTournament).Methods("POST") router.HandleFunc("/tournament/{id}/player/{player_id}", controllers.GetTournamentPlayerMatches).Methods("GET") @@ -154,79 +149,6 @@ var serveCmd = &cobra.Command{ }, } -func GetLongestWinStreak() { - // Establish a connection to the MySQL database - db, err := sql.Open("mysql", "developer:abcd1234@tcp(localhost:3306)/kcapp") - if err != nil { - log.Fatal(err) - } - defer db.Close() - - // Set maximum number of open and idle connections - db.SetMaxOpenConns(10) // Adjust the value based on your MySQL server configuration - db.SetMaxIdleConns(5) // Adjust the value based on your MySQL server configuration - - // Set a maximum connection lifetime - db.SetConnMaxLifetime(5 * time.Minute) // Adjust the value based on your application requirements - - // Get a list of all players - players := []int{1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 35, 36, 37, 38, 39, 40, 42, 44, 46, 52, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 66, 67, 68, 69, 70, 76, 79, 82, 83, 84, 90, 97, 99, 102, 103, 106, 107, 108, 109, 110, 111, 112, 113, 114, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 135, 139, 141, 142, 143, 144, 148, 151, 152, 157, 159, 160, 163, 165, 168, 169, 175, 176, 177, 178, 179, 180, 181, 183, 184, 185, 187, 188, 191, 192, 194, 196, 198, 199, 200, 202, 206, 207, 208, 209, 210, 213, 217, 219, 221, 222, 223, 224, 225, 226, 229, 236, 237, 238, 239, 240, 250, 252, 253, 254, 255, 256, 257, 258, 260, 261, 262, 264, 276, 277, 278, 281, 286, 287, 290, 292, 296, 297, 298, 303, 304, 306, 307, 308, 311, 313, 314, 318, 322, 331, 332, 333, 334, 343, 344, 345, 347, 350, 361, 363, 364, 365, 366, 368, 371, 376, 380, 385, 387, 389, 401, 402, 409, 411, 412, 413, 414, 417, 418, 420, 422, 428, 429, 432, 435} // Replace with your actual list of player IDs - - // Initialize variables to track the longest win streak - longestStreak := 0 - playerWithLongestStreak := 0 - - // Iterate over each player - for _, playerID := range players { - // Dynamically update the query - query := fmt.Sprintf(` - SELECT - winner_id, - MAX(streak) AS longest_streak - FROM ( - SELECT - winner_id, - @streak := IF(@prev_winner = winner_id, @streak + 1, 1) AS streak, - @prev_winner := winner_id - FROM - (SELECT @streak := 0, @prev_winner := NULL) AS vars, - (SELECT * FROM matches m WHERE id IN (SELECT DISTINCT match_id FROM player2leg WHERE player_id = %d) AND is_finished = 1 AND match_type_id = 1 ORDER BY updated_at) AS m - ) AS streaks - WHERE winner_id = %d - GROUP BY winner_id - ORDER BY longest_streak DESC - `, playerID, playerID) - - // Execute the query - rows, err := db.Query(query) - if err != nil { - log.Fatal(err) - } - defer rows.Close() - - // Fetch the result - var winnerID, streak int - if rows.Next() { - err := rows.Scan(&winnerID, &streak) - if err != nil { - log.Fatal(err) - } - - // Check if the player has a longer streak than the current longest - if streak > longestStreak { - longestStreak = streak - playerWithLongestStreak = winnerID - } - } - rows.Close() - if err := rows.Err(); err != nil { - log.Fatal(err) - } - fmt.Printf("Player ID = %d, Streak = %d\n", winnerID, streak) - } - fmt.Printf("Player with the longest win streak: Player ID = %d, Streak = %d\n", playerWithLongestStreak, longestStreak) -} - func init() { rootCmd.AddCommand(serveCmd) } diff --git a/data/badge.go b/data/badge.go index 1868df4c..ac8c249c 100644 --- a/data/badge.go +++ b/data/badge.go @@ -3,6 +3,7 @@ package data import ( "database/sql" "log" + "time" "github.com/kcapp/api/models" ) @@ -37,22 +38,31 @@ func GetBadges() ([]*models.Badge, error) { return badges, nil } -func CheckLegForBadges(leg *models.Leg) error { +func CheckLegForBadges(leg *models.Leg, statistics map[int]*models.BadgeStatistics) error { tx, err := models.DB.Begin() if err != nil { return err } + playersMap, err := GetPlayersScore(leg.ID) + if err != nil { + return err + } + players := make([]*models.Player2Leg, 0, len(playersMap)) + for _, value := range playersMap { + players = append(players, value) + } + for _, badge := range models.LegBadges { valid, playerID := badge.Validate(leg) if valid { if playerID != nil { - addLegBadge(tx, *playerID, leg.ID, badge) + err = addLegBadge(tx, *playerID, leg.ID, badge, leg.UpdatedAt) if err != nil { return err } } else { for _, playerID := range leg.Players { - addLegBadge(tx, playerID, leg.ID, badge) + err = addLegBadge(tx, playerID, leg.ID, badge, leg.UpdatedAt) if err != nil { return err } @@ -60,18 +70,84 @@ func CheckLegForBadges(leg *models.Leg) error { } } } + + for _, badge := range models.LegPlayerBadges { + valid, playerID := badge.Validate(leg, players) + if valid { + err = addLegPlayerBadge(tx, *playerID, leg.ID, badge, leg.UpdatedAt) + if err != nil { + return err + } + } + } + + for _, badge := range models.VisitBadges { + for _, playerID := range leg.Players { + stats := statistics[playerID] + valid, level := badge.Validate(stats, leg.Visits) + if valid { + err = addVisitBadge(tx, playerID, *level, leg.ID, badge, leg.UpdatedAt) + if err != nil { + return err + } + } + } + } tx.Commit() return nil } -func addLegBadge(tx *sql.Tx, playerID int, legID int, badge models.LegBadge) error { - _, err := tx.Exec("INSERT INTO player2badge (player_id, badge_id, leg_id) VALUES (?, ?, ?)", - playerID, badge.GetID(), legID) +func AddBadge(playerID int, badge models.GlobalBadge) error { + _, err := models.DB.Exec("INSERT IGNORE INTO player2badge (player_id, badge_id, created_at) VALUES (?, ?, ?)", + playerID, badge.GetID(), time.Now()) + if err != nil { + return err + } + log.Printf("Added global badge %d to player %d", badge.GetID(), playerID) + return nil +} + +func AddTournamentBadge(playerID int, tournamentID int, badge models.GlobalBadge, when time.Time) error { + _, err := models.DB.Exec("INSERT IGNORE INTO player2badge (player_id, badge_id, tournament_id, created_at) VALUES (?, ?, ?, ?)", + playerID, badge.GetID(), tournamentID, when) + if err != nil { + return err + } + log.Printf("Added tournament badge %d to player %d", badge.GetID(), playerID) + return nil +} + +func addLegBadge(tx *sql.Tx, playerID int, legID int, badge models.LegBadge, when time.Time) error { + _, err := tx.Exec("INSERT IGNORE INTO player2badge (player_id, badge_id, leg_id, created_at) VALUES (?, ?, ?, ?)", + playerID, badge.GetID(), legID, when) + if err != nil { + tx.Rollback() + return err + } + log.Printf("Added leg badge %d to player %d on leg %d", badge.GetID(), playerID, legID) + return nil +} + +func addLegPlayerBadge(tx *sql.Tx, playerID int, legID int, badge models.LegPlayerBadge, when time.Time) error { + _, err := tx.Exec("INSERT IGNORE INTO player2badge (player_id, badge_id, leg_id, created_at) VALUES (?, ?, ?, ?)", + playerID, badge.GetID(), legID, when) + if err != nil { + tx.Rollback() + return err + } + log.Printf("Added leg player badge %d to player %d on leg %d", badge.GetID(), playerID, legID) + return nil +} + +func addVisitBadge(tx *sql.Tx, playerID int, level int, legID int, badge models.VisitBadge, when time.Time) error { + _, err := tx.Exec(`INSERT INTO player2badge (player_id, badge_id, level, value, leg_id, created_at) VALUES (?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE leg_id=IF(?>level,?,leg_id), created_at=IF(?>level,?,created_at), value=IF(?>level,?,value),level=?`, + playerID, badge.GetID(), level, badge.Levels()[level-1], legID, when, level, legID, level, when, level, badge.Levels()[level-1], level) if err != nil { tx.Rollback() return err } - log.Printf("Added badge %d to player %d on leg %d", badge.GetID(), playerID, legID) + log.Printf("Added visit badge %d (level %d) to player %d on leg %d", badge.GetID(), level, playerID, legID) return nil } diff --git a/data/leg.go b/data/leg.go index 53b56390..3665af6d 100644 --- a/data/leg.go +++ b/data/leg.go @@ -26,8 +26,8 @@ func NewLeg(matchID int, startingScore int, players []int, matchType *int) (*mod players[i], players[j] = players[j], players[i] } } - res, err := tx.Exec("INSERT INTO leg (starting_score, current_player_id, leg_type_id, match_id, created_at) VALUES (?, ?, ?, ?, NOW()) ", - startingScore, players[0], matchType, matchID) + res, err := tx.Exec("INSERT INTO leg (starting_score, current_player_id, leg_type_id, match_id, num_players, created_at) VALUES (?, ?, ?, ?, ?, NOW()) ", + startingScore, players[0], matchType, matchID, len(players)) if err != nil { tx.Rollback() return nil, err @@ -548,12 +548,6 @@ func FinishLeg(visit models.Visit) error { } } } - - // Calculate badges - err = CheckLegForBadges(leg) - if err != nil { - return err - } } else { log.Printf("Match %d is not finished, creating next leg", match.ID) var matchType *int @@ -566,6 +560,17 @@ func FinishLeg(visit models.Visit) error { return err } } + + // Calculate badges earned in this leg + statistics, err := GetPlayerBadgeStatistics(leg.Players, nil) + if err != nil { + return err + } + err = CheckLegForBadges(leg, statistics) + if err != nil { + return err + } + return nil } @@ -821,6 +826,38 @@ func GetLegsToRecalculate(matchType int, since string) ([]int, error) { return legs, nil } +// GetBadgeLegsToRecalculate returns all legs which can generate badges which can be recalculated +func GetBadgeLegsToRecalculate() ([]int, error) { + rows, err := models.DB.Query(` + SELECT l.id + FROM leg l + JOIN matches m on m.id = l.match_id + WHERE l.has_scores = 1 AND COALESCE(l.leg_type_id, m.match_type_id) = 1 -- X01 + AND m.is_abandoned = 0 AND m.is_bye = 0 AND m.is_walkover = 0 + AND l.is_finished = 1 + GROUP BY l.id + ORDER BY l.id ASC`) + if err != nil { + return nil, err + } + defer rows.Close() + + legs := make([]int, 0) + for rows.Next() { + var legId int + err := rows.Scan(&legId) + if err != nil { + return nil, err + } + legs = append(legs, legId) + } + if err = rows.Err(); err != nil { + return nil, err + } + + return legs, nil +} + // GetActiveLegs returns all legs which are currently live func GetActiveLegs() ([]*models.Leg, error) { rows, err := models.DB.Query(` @@ -1110,7 +1147,7 @@ func GetLeg(id int) (*models.Leg, error) { return leg, nil } -// GetLegPlayers returns information about current score for players in a leg +// GetLegPlayers returns information about all players in a leg func GetLegPlayers(id int) ([]*models.Player2Leg, error) { leg, err := GetLeg(id) if err != nil { diff --git a/data/match.go b/data/match.go index 56f18b86..f72f00fb 100644 --- a/data/match.go +++ b/data/match.go @@ -21,7 +21,8 @@ func NewMatch(match models.Match) (*models.Match, error) { if match.CreatedAt.IsZero() { match.CreatedAt = time.Now().UTC() } - res, err := tx.Exec("INSERT INTO matches (match_type_id, match_mode_id, owe_type_id, venue_id, office_id, is_practice, tournament_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + res, err := tx.Exec(`INSERT INTO matches (match_type_id, match_mode_id, owe_type_id, venue_id, office_id, is_practice, tournament_id, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, match.MatchType.ID, match.MatchMode.ID, match.OweTypeID, match.VenueID, match.OfficeID, match.IsPractice, match.TournamentID, match.CreatedAt) if err != nil { tx.Rollback() @@ -33,7 +34,8 @@ func NewMatch(match models.Match) (*models.Match, error) { return nil, err } startingScore := match.Legs[0].StartingScore - res, err = tx.Exec("INSERT INTO leg (starting_score, current_player_id, match_id, created_at) VALUES (?, ?, ?, ?) ", match.Legs[0].StartingScore, match.Players[0], matchID, match.CreatedAt) + res, err = tx.Exec("INSERT INTO leg (starting_score, current_player_id, match_id, num_players, created_at) VALUES (?, ?, ?, ?, ?)", + match.Legs[0].StartingScore, match.Players[0], matchID, len(match.Players), match.CreatedAt) if err != nil { tx.Rollback() return nil, err diff --git a/data/player.go b/data/player.go index 417a64ab..1fd97cad 100644 --- a/data/player.go +++ b/data/player.go @@ -22,7 +22,8 @@ func GetPlayers() (map[int]*models.Player, error) { rows, err := models.DB.Query(` SELECT p.id, p.first_name, p.last_name, p.vocal_name, p.nickname, p.slack_handle, p.color, p.profile_pic_url, p.smartcard_uid, - p.board_stream_url, p.board_stream_css, p.active, p.office_id, p.is_bot, p.is_placeholder, p.created_at, p.updated_at + p.board_stream_url, p.board_stream_css, p.active, p.office_id, p.is_bot, p.is_placeholder, p.is_supporter, p.created_at, + p.updated_at FROM player p`) if err != nil { return nil, err @@ -33,7 +34,8 @@ func GetPlayers() (map[int]*models.Player, error) { for rows.Next() { p := new(models.Player) err := rows.Scan(&p.ID, &p.FirstName, &p.LastName, &p.VocalName, &p.Nickname, &p.SlackHandle, &p.Color, &p.ProfilePicURL, - &p.SmartcardUID, &p.BoardStreamURL, &p.BoardStreamCSS, &p.IsActive, &p.OfficeID, &p.IsBot, &p.IsPlaceholder, &p.CreatedAt, &p.UpdatedAt) + &p.SmartcardUID, &p.BoardStreamURL, &p.BoardStreamCSS, &p.IsActive, &p.OfficeID, &p.IsBot, &p.IsPlaceholder, &p.IsSupporter, + &p.CreatedAt, &p.UpdatedAt) if err != nil { return nil, err } @@ -835,7 +837,8 @@ func GetPlayerTournamentStandings(playerID int) ([]*models.PlayerTournamentStand tg.division AS 'tournament_group_division', ts.rank AS 'final_standing', MAX(ts2.rank) AS 'total_players', - ts.elo as 'elo' + ts.elo as 'elo', + t.end_time FROM tournament_standings ts JOIN tournament t ON t.id = ts.tournament_id JOIN player p ON p.id = ts.player_id @@ -858,7 +861,7 @@ func GetPlayerTournamentStandings(playerID int) ([]*models.PlayerTournamentStand err := rows.Scan(&standing.PlayerID, &standing.Tournament.ID, &standing.Tournament.Name, &standing.TournamentGroup.ID, &standing.TournamentGroup.Name, &standing.TournamentGroup.Division, &standing.FinalStanding, &standing.TotalPlayers, - &standing.Elo) + &standing.Elo, &standing.Tournament.EndTime) if err != nil { return nil, err } @@ -870,6 +873,7 @@ func GetPlayerTournamentStandings(playerID int) ([]*models.PlayerTournamentStand return standings, nil } +// GetPlayerBadges will return all badges for a given player func GetPlayerBadges(playerID int) ([]*models.PlayerBadge, error) { rows, err := models.DB.Query(` SELECT @@ -878,7 +882,13 @@ func GetPlayerBadges(playerID int) ([]*models.PlayerBadge, error) { b.description, b.filename, p2b.player_id, + p2b.level, + p2b.value, p2b.leg_id, + p2b.match_id, + p2b.tournament_id, + p2b.opponent_player_id, + p2b.darts, p2b.created_at FROM player2badge p2b LEFT JOIN badge b ON b.id = p2b.badge_id @@ -898,7 +908,13 @@ func GetPlayerBadges(playerID int) ([]*models.PlayerBadge, error) { &badge.Badge.Description, &badge.Badge.Filename, &badge.PlayerID, + &badge.Level, + &badge.Value, &badge.LegID, + &badge.MatchID, + &badge.TournamentID, + &badge.OpponentPlayerID, + &badge.Darts, &badge.CreatedAt, ) if err != nil { diff --git a/data/recalculate.go b/data/recalculate.go index 4c55a3ed..1626e261 100644 --- a/data/recalculate.go +++ b/data/recalculate.go @@ -5,13 +5,14 @@ import ( "log" "os" "sort" + "strings" "text/tabwriter" "github.com/guregu/null" "github.com/kcapp/api/models" ) -// RecalculateTicTacToeStatistics will recaulcate statistics for Tic Tac Toe legs +// RecalculateStatistics will recaulcate statistics based on the given parameters func RecalculateStatistics(matchType int, legID int, since string, dryRun bool) error { legs := make([]int, 0) if legID != 0 { @@ -228,3 +229,111 @@ func CalculateEloForTournament(tournamentID int) error { return nil } + +func RecalculateLegBadges() error { + ids, err := GetBadgeLegsToRecalculate() + if err != nil { + return err + } + + for _, legID := range ids { + log.Printf("Checking Leg %d for badges", legID) + leg, err := GetLeg(legID) + if err != nil { + return err + } + + statistics, err := GetPlayerBadgeStatistics(leg.Players, &legID) + if err != nil { + return err + } + + // Calculate badges + err = CheckLegForBadges(leg, statistics) + if err != nil { + return err + } + } + + return nil +} + +func RecalculateGlobalBadges() error { + players, err := GetPlayers() + if err != nil { + return err + } + for _, player := range players { + if player.IsSupporter { + // Add supporter badge + err = AddBadge(player.ID, new(models.BadgeKcappSupporter)) + if err != nil { + return err + } + } + if player.VocalName.Valid && strings.HasSuffix(player.VocalName.String, ".wav") { + // Add vocal name badge + err = AddBadge(player.ID, new(models.BadgeSayMyName)) + if err != nil { + return err + } + } + + standings, err := GetPlayerTournamentStandings(player.ID) + if err != nil { + return err + } + if len(standings) > 0 { + standing := standings[len(standings)-1] + err = AddTournamentBadge(player.ID, standing.Tournament.ID, new(models.BadgeItsOfficial), standing.Tournament.EndTime.Time) + if err != nil { + return err + } + } + + standing := getPlayerTournamentStanding(1, standings) + if standing != nil { + err = AddTournamentBadge(player.ID, standing.Tournament.ID, new(models.BadgeTournament1st), standing.Tournament.EndTime.Time) + if err != nil { + return err + } + } + standing = getPlayerTournamentStanding(2, standings) + if standing != nil { + err = AddTournamentBadge(player.ID, standing.Tournament.ID, new(models.BadgeTournament2nd), standing.Tournament.EndTime.Time) + if err != nil { + return err + } + } + standing = getPlayerTournamentStanding(3, standings) + if standing != nil { + err = AddTournamentBadge(player.ID, standing.Tournament.ID, new(models.BadgeTournament3rd), standing.Tournament.EndTime.Time) + if err != nil { + return err + } + } + } + + undefeated, err := GetUndefeatedPlayers() + if err != nil { + return err + } + for playerID, tournament := range undefeated { + err := AddTournamentBadge(playerID, tournament.ID, new(models.BadgeUntouchable), tournament.EndTime.Time) + if err != nil { + return err + } + } + + return nil +} + +func getPlayerTournamentStanding(pos int, standings []*models.PlayerTournamentStanding) *models.PlayerTournamentStanding { + for i := len(standings) - 1; i >= 0; i-- { + standing := standings[i] + if standing.FinalStanding == pos { + return standing + } + } + return nil +} diff --git a/data/statistics_x01.go b/data/statistics_x01.go index 754bf4fa..e57f1748 100644 --- a/data/statistics_x01.go +++ b/data/statistics_x01.go @@ -922,3 +922,46 @@ func RecalculateX01Statistics(legs []int) ([]string, error) { } return queries, nil } + +// GetPlayerBadgeStatistics will return statistics calculate badges for the given players +func GetPlayerBadgeStatistics(ids []int, legID *int) (map[int]*models.BadgeStatistics, error) { + q, args, err := sqlx.In(` + SELECT + player_id, + SUM(s.100s_plus), + SUM(s.140s_plus), + SUM(s.180s) + FROM statistics_x01 s + LEFT JOIN leg l ON s.leg_id = l.id + LEFT JOIN matches m ON l.match_id = m.id + WHERE player_id IN (?) AND l.num_players = 2 AND m.is_practice = 0 + AND m.is_bye = 0 AND m.is_walkover = 0 AND leg_id <= COALESCE(?, ~0) -- BIGINT hack + GROUP BY player_id + ORDER BY player_id DESC`, ids, legID) + if err != nil { + return nil, err + } + rows, err := models.DB.Query(q, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + statistics := make(map[int]*models.BadgeStatistics) + for _, playerID := range ids { + statistics[playerID] = new(models.BadgeStatistics) + } + for rows.Next() { + s := new(models.BadgeStatistics) + err := rows.Scan(&s.PlayerID, &s.Score100sPlus, &s.Score140sPlus, &s.Score180s) + if err != nil { + return nil, err + } + statistics[s.PlayerID] = s + } + if err = rows.Err(); err != nil { + return nil, err + } + + return statistics, nil +} diff --git a/data/tournament.go b/data/tournament.go index a8e979a2..30deca4a 100644 --- a/data/tournament.go +++ b/data/tournament.go @@ -1337,3 +1337,41 @@ func insertMetadata(matches []*models.MatchMetadata) error { tx.Commit() return nil } + +// GetUndefeatedPlayers will return all players undefeated in a tournament +func GetUndefeatedPlayers() (map[int]*models.Tournament, error) { + rows, err := models.DB.Query(` + SELECT + p.id AS 'player_id', + t.id as 'tournament_id', + t.end_time + FROM player2leg p2l + JOIN matches m ON m.id = p2l.match_id + JOIN player p ON p.id = p2l.player_id + LEFT JOIN matches won ON won.id = p2l.match_id AND won.winner_id = p.id + LEFT JOIN matches finished ON m.id = finished.id AND finished.is_finished = 1 + JOIN tournament t ON t.id = m.tournament_id + WHERE m.is_bye <> 1 AND t.is_finished = 1 AND t.is_playoffs = 0 + GROUP BY p2l.player_id, t.id + HAVING IF(COUNT(DISTINCT finished.id) = COUNT(DISTINCT won.id), 1, 0) = 1 AND COUNT(DISTINCT finished.id) >= 5 + ORDER BY t.id DESC`) + if err != nil { + return nil, err + } + defer rows.Close() + + undefeated := make(map[int]*models.Tournament) + for rows.Next() { + var playerID int + tournament := new(models.Tournament) + err := rows.Scan(&playerID, &tournament.ID, &tournament.EndTime) + if err != nil { + return nil, err + } + undefeated[playerID] = tournament + } + if err = rows.Err(); err != nil { + return nil, err + } + return undefeated, nil +} diff --git a/models/badge.go b/models/badge.go index a72eb5e0..4cd9876d 100644 --- a/models/badge.go +++ b/models/badge.go @@ -18,21 +18,84 @@ type Badge struct { // PlayerBadge represents a Player2Badge model. type PlayerBadge struct { - Badge *Badge `json:"badge"` - PlayerID int `json:"player_id"` - Level null.Int `json:"level,omitempty"` - LegID null.Int `json:"leg_id,omitempty"` - CreatedAt time.Time `json:"created_at"` + Badge *Badge `json:"badge"` + PlayerID int `json:"player_id"` + Level null.Int `json:"level,omitempty"` + LegID null.Int `json:"leg_id,omitempty"` + Value null.Int `json:"value,omitempty"` + MatchID null.Int `json:"match_id,omitempty"` + OpponentPlayerID null.Int `json:"opponent_player_id,omitempty"` + TournamentID null.Int `json:"tournament_id,omitempty"` + Darts null.String `json:"darts,omitempty"` + CreatedAt time.Time `json:"created_at"` +} + +// BadgeStatistics struct used for storing badge statistics +type BadgeStatistics struct { + PlayerID int + Score100sPlus int + Score140sPlus int + Score180s int +} + +type GlobalBadge interface { + GetID() int +} + +type BadgeKcappSupporter struct{ ID int } +type BadgeSayMyName struct{ ID int } +type BadgeItsOfficial struct{ ID int } +type BadgeTournament1st struct{ ID int } +type BadgeTournament2nd struct{ ID int } +type BadgeTournament3rd struct{ ID int } +type BadgeUntouchable struct{ ID int } + +func (b BadgeKcappSupporter) GetID() int { + return 4 +} + +func (b BadgeSayMyName) GetID() int { + return 12 +} + +func (b BadgeItsOfficial) GetID() int { + return 17 +} + +func (b BadgeTournament1st) GetID() int { + return 18 +} + +func (b BadgeTournament2nd) GetID() int { + return 19 +} + +func (b BadgeTournament3rd) GetID() int { + return 20 +} + +func (b BadgeUntouchable) GetID() int { + return 26 +} + +var MatchBadges = []MatchBadge{} + +type MatchBadge interface { + GetID() int + Validate(*Match) (bool, *int) } var LegBadges = []LegBadge{ - BadgeDoubleDouble{ID: 8}, - BadgeTripleDouble{ID: 9}, - BadgeMadHouse{ID: 10}, - BadgeMerryChristmas{ID: 11}, - BadgeBigFish{ID: 14}, - BadgeGettingCrowded{ID: 17}, - BadgeBullseye{ID: 20}, + BadgeDoubleDouble{ID: 6}, + BadgeTripleDouble{ID: 7}, + BadgeMadHouse{ID: 8}, + BadgeMerryChristmas{ID: 9}, + BadgeHappyNewYear{ID: 10}, + BadgeBigFish{ID: 11}, + BadgeGettingCrowded{ID: 13}, + BadgeBullseye{ID: 14}, + BadgeEasyAs123{ID: 15}, + BadgeCloseToPerfect{ID: 16}, } type LegBadge interface { @@ -40,29 +103,52 @@ type LegBadge interface { Validate(*Leg) (bool, *int) } -type BadgeMerryChristmas struct{ ID int } -type BadgeGettingCrowded struct{ ID int } -type BadgeBullseye struct{ ID int } type BadgeDoubleDouble struct{ ID int } type BadgeTripleDouble struct{ ID int } type BadgeMadHouse struct{ ID int } +type BadgeMerryChristmas struct{ ID int } +type BadgeHappyNewYear struct{ ID int } type BadgeBigFish struct{ ID int } +type BadgeGettingCrowded struct{ ID int } +type BadgeBullseye struct{ ID int } +type BadgeEasyAs123 struct{ ID int } +type BadgeCloseToPerfect struct{ ID int } -type BadgeHighScore struct{ ID int } -type BadgeHigherScore struct{ ID int } -type BadgeTheMaximum struct{ ID int } -type BadgeGlobetrotter struct{ ID int } -type BadgePartyOfTwo struct{ ID int } -type BadgeWorkFromHome struct{ ID int } -type BadgeHardlyWorking struct{ ID int } -type BadgeWeeklyPlayer struct{ ID int } +func (b BadgeDoubleDouble) GetID() int { + return b.ID +} +func (b BadgeDoubleDouble) Validate(leg *Leg) (bool, *int) { + visit := leg.GetLastVisit() + doubles := 0 + if visit.ThirdDart.IsDouble() { + doubles++ + } + if visit.SecondDart.IsDouble() { + doubles++ + } + if visit.FirstDart.IsDouble() { + doubles++ + } + return doubles == 2, &visit.PlayerID +} -func (b BadgeGettingCrowded) GetID() int { +func (b BadgeTripleDouble) GetID() int { return b.ID } -func (b BadgeGettingCrowded) Validate(leg *Leg) (bool, *int) { - return len(leg.Players) > 4, nil +func (b BadgeTripleDouble) Validate(leg *Leg) (bool, *int) { + visit := leg.GetLastVisit() + return visit.FirstDart.IsDouble() && visit.SecondDart.IsDouble() && visit.ThirdDart.IsDouble(), &visit.PlayerID +} + +func (b BadgeMadHouse) GetID() int { + return b.ID } +func (b BadgeMadHouse) Validate(leg *Leg) (bool, *int) { + visit := leg.GetLastVisit() + last := visit.GetLastDart() + return last.IsDouble() && last.ValueRaw() == 1, &visit.PlayerID +} + func (b BadgeMerryChristmas) GetID() int { return b.ID } @@ -70,6 +156,32 @@ func (b BadgeMerryChristmas) Validate(leg *Leg) (bool, *int) { d := leg.Endtime.Time return d.Day() == 25 && d.Month() == 12, nil } + +func (b BadgeHappyNewYear) GetID() int { + return b.ID +} +func (b BadgeHappyNewYear) Validate(leg *Leg) (bool, *int) { + d := leg.Endtime.Time + return d.Day() == 31 && d.Month() == 12, nil +} + +func (b BadgeBigFish) GetID() int { + return b.ID +} +func (b BadgeBigFish) Validate(leg *Leg) (bool, *int) { + visit := leg.GetLastVisit() + return visit.FirstDart.IsTriple() && visit.FirstDart.ValueRaw() == 20 && + visit.SecondDart.IsTriple() && visit.SecondDart.ValueRaw() == 20 && + visit.ThirdDart.IsDouble() && visit.ThirdDart.IsBull(), &visit.PlayerID +} + +func (b BadgeGettingCrowded) GetID() int { + return b.ID +} +func (b BadgeGettingCrowded) Validate(leg *Leg) (bool, *int) { + return len(leg.Players) > 4, nil +} + func (b BadgeBullseye) GetID() int { return b.ID } @@ -79,41 +191,199 @@ func (b BadgeBullseye) Validate(leg *Leg) (bool, *int) { return last.ValueRaw() == BULLSEYE && last.Multiplier == DOUBLE, &visit.PlayerID } -func (b BadgeDoubleDouble) GetID() int { +func (b BadgeEasyAs123) GetID() int { return b.ID } +func (b BadgeEasyAs123) Validate(leg *Leg) (bool, *int) { + visit := leg.GetLastVisit() + last := visit.GetLastDart() + return visit.GetScore() == 123 && last.IsDouble(), &visit.PlayerID +} -func (b BadgeDoubleDouble) Validate(leg *Leg) (bool, *int) { +func (b BadgeCloseToPerfect) GetID() int { + return b.ID +} +func (b BadgeCloseToPerfect) Validate(leg *Leg) (bool, *int) { visit := leg.GetLastVisit() - return visit.SecondDart.IsDouble(), &visit.PlayerID + return leg.StartingScore == 501 && visit.DartsThrown < 15 && visit.DartsThrown > 9, &visit.PlayerID } -func (b BadgeTripleDouble) GetID() int { +var LegPlayerBadges = []LegPlayerBadge{ + BadgeImpersonator{ID: 22}, + BadgeBotBeaterEasy{ID: 22}, + BadgeBotBeaterMedium{ID: 23}, + BadgeBotBeaterHard{ID: 24}, +} + +type LegPlayerBadge interface { + GetID() int + Validate(*Leg, []*Player2Leg) (bool, *int) +} + +type BadgeImpersonator struct{ ID int } +type BadgeBotBeaterEasy struct{ ID int } +type BadgeBotBeaterMedium struct{ ID int } +type BadgeBotBeaterHard struct{ ID int } + +func (b BadgeImpersonator) GetID() int { return b.ID } +func (b BadgeImpersonator) Validate(leg *Leg, players []*Player2Leg) (bool, *int) { + var bot *Player2Leg + for _, p2l := range players { + if p2l.Player.IsBot && p2l.BotConfig.PlayerID.Valid { + bot = p2l + } + } + winner := int(leg.WinnerPlayerID.Int64) + if bot != nil && bot.PlayerID != winner { + return true, &winner + } + return false, nil +} -func (b BadgeTripleDouble) Validate(leg *Leg) (bool, *int) { - visit := leg.GetLastVisit() - return visit.FirstDart.IsDouble() && visit.SecondDart.IsDouble(), &visit.PlayerID +func (b BadgeBotBeaterEasy) GetID() int { + return b.ID +} +func (b BadgeBotBeaterEasy) Validate(leg *Leg, players []*Player2Leg) (bool, *int) { + bot := getBot(BOT_EASY, players) + winner := int(leg.WinnerPlayerID.Int64) + if bot != nil && bot.PlayerID != winner { + return true, &winner + } + return false, nil } -func (b BadgeMadHouse) GetID() int { +func (b BadgeBotBeaterMedium) GetID() int { return b.ID } +func (b BadgeBotBeaterMedium) Validate(leg *Leg, players []*Player2Leg) (bool, *int) { + bot := getBot(BOT_MEDIUM, players) + winner := int(leg.WinnerPlayerID.Int64) + if bot != nil && bot.PlayerID != winner { + return true, &winner + } + return false, nil +} -func (b BadgeMadHouse) Validate(leg *Leg) (bool, *int) { - visit := leg.GetLastVisit() - last := visit.GetLastDart() - return last.IsDouble() && last.ValueRaw() == 1, &visit.PlayerID +func (b BadgeBotBeaterHard) GetID() int { + return b.ID +} +func (b BadgeBotBeaterHard) Validate(leg *Leg, players []*Player2Leg) (bool, *int) { + bot := getBot(BOT_HARD, players) + winner := int(leg.WinnerPlayerID.Int64) + if bot != nil && bot.PlayerID != winner { + return true, &winner + } + return false, nil } -func (b BadgeBigFish) GetID() int { +var VisitBadges = []VisitBadge{ + BadgeHighScore{ID: 1}, + BadgeHigherScore{ID: 2}, + BadgeTheMaximum{ID: 3}, +} + +type VisitBadge interface { + GetID() int + Validate(stats *BadgeStatistics, visits []*Visit) (bool, *int) + Levels() []int +} + +type BadgeHighScore struct{ ID int } +type BadgeHigherScore struct{ ID int } +type BadgeTheMaximum struct{ ID int } + +func (b BadgeHighScore) GetID() int { return b.ID } -func (b BadgeBigFish) Validate(leg *Leg) (bool, *int) { - visit := leg.GetLastVisit() - return visit.FirstDart.IsTriple() && visit.FirstDart.ValueRaw() == 20 && - visit.SecondDart.IsTriple() && visit.SecondDart.ValueRaw() == 20 && - visit.ThirdDart.IsDouble() && visit.ThirdDart.IsBull(), &visit.PlayerID +func (b BadgeHighScore) Levels() []int { + return []int{1, 10, 100, 1000} +} + +func (b BadgeHighScore) Validate(stats *BadgeStatistics, visits []*Visit) (bool, *int) { + count := 0 + for _, visit := range visits { + if visit.PlayerID != stats.PlayerID { + continue + } + if visit.Score == 100 { + count++ + } + } + if count > 0 { + level := getLevel(stats.Score100sPlus+count, b.Levels()) + return true, &level + } + return false, nil +} + +func (b BadgeHigherScore) GetID() int { + return b.ID +} + +func (b BadgeHigherScore) Levels() []int { + return []int{1, 10, 100, 1000} +} + +func (b BadgeHigherScore) Validate(stats *BadgeStatistics, visits []*Visit) (bool, *int) { + count := 0 + for _, visit := range visits { + if visit.PlayerID != stats.PlayerID { + continue + } + if visit.Score == 140 { + count++ + } + } + if count > 0 { + level := getLevel(stats.Score140sPlus+count, b.Levels()) + return true, &level + } + return false, nil +} + +func (b BadgeTheMaximum) GetID() int { + return b.ID +} + +func (b BadgeTheMaximum) Levels() []int { + return []int{1, 10, 50, 100} +} + +func (b BadgeTheMaximum) Validate(stats *BadgeStatistics, visits []*Visit) (bool, *int) { + count := 0 + for _, visit := range visits { + if visit.PlayerID != stats.PlayerID { + continue + } + if visit.Score == 180 { + count++ + } + } + if count > 0 { + level := getLevel(stats.Score180s+count, b.Levels()) + return true, &level + } + return false, nil +} + +func getLevel(value int, levels []int) int { + level := 1 + for i, treshold := range levels { + if value > treshold { + level = i + 1 + } + } + return level +} + +func getBot(skill int64, players []*Player2Leg) *Player2Leg { + for _, p2l := range players { + if p2l.Player.IsBot && p2l.BotConfig.Skill.Int64 == skill { + return p2l + } + } + return nil } diff --git a/models/bot.go b/models/bot.go new file mode 100644 index 00000000..4ae12509 --- /dev/null +++ b/models/bot.go @@ -0,0 +1,12 @@ +package models + +const ( + BOT_FIRSTTIME = 5 + BOT_VERYEASY = 6 + BOT_EASY = 1 + BOT_MEDIUM = 2 + BOT_CHALLENGING = 8 + BOT_HARD = 3 + BOT_MVG = 7 + BOT_PERFECT = 4 +) diff --git a/models/player.go b/models/player.go index 330715e8..2652069a 100644 --- a/models/player.go +++ b/models/player.go @@ -29,6 +29,7 @@ type Player struct { IsActive bool `json:"is_active"` IsBot bool `json:"is_bot"` IsPlaceholder bool `json:"is_placeholder"` + IsSupporter bool `json:"is_supporter"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at,omitempty"` TournamentElo int `json:"tournament_elo,omitempty"` diff --git a/models/tournament.go b/models/tournament.go index 05291355..94f56864 100644 --- a/models/tournament.go +++ b/models/tournament.go @@ -117,5 +117,5 @@ type TournamentPreset struct { PlayerIDWalkover int `json:"player_id_walkover"` PlayerIDPlaceholderHome int `json:"player_id_placeholder_home"` PlayerIDPlaceholderAway int `json:"player_id_placeholder_away"` - Description string `json:"description"` + Description null.String `json:"description"` } From 644d0445e930954e0f6d0f6cc17f88390ab9e50d Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Tue, 12 Sep 2023 18:04:17 +0200 Subject: [PATCH 36/67] Separated commands for recalculating leg and global badges --- cmd/badge_global.go | 22 ++++++++++++++++++++++ cmd/badge_leg.go | 22 ++++++++++++++++++++++ cmd/recalculate_badge.go | 13 ------------- 3 files changed, 44 insertions(+), 13 deletions(-) create mode 100644 cmd/badge_global.go create mode 100644 cmd/badge_leg.go diff --git a/cmd/badge_global.go b/cmd/badge_global.go new file mode 100644 index 00000000..c887d8df --- /dev/null +++ b/cmd/badge_global.go @@ -0,0 +1,22 @@ +package cmd + +import ( + "github.com/kcapp/api/data" + "github.com/spf13/cobra" +) + +// recalculateGlobalBadgeCmd represents the global command +var recalculateGlobalBadgeCmd = &cobra.Command{ + Use: "global", + Short: "Recalculate Global Badges", + Run: func(cmd *cobra.Command, args []string) { + err := data.RecalculateGlobalBadges() + if err != nil { + panic(err) + } + }, +} + +func init() { + recalculateBadgeCmd.AddCommand(recalculateGlobalBadgeCmd) +} diff --git a/cmd/badge_leg.go b/cmd/badge_leg.go new file mode 100644 index 00000000..70870d3f --- /dev/null +++ b/cmd/badge_leg.go @@ -0,0 +1,22 @@ +package cmd + +import ( + "github.com/kcapp/api/data" + "github.com/spf13/cobra" +) + +// recalculateLegBadgesCmd represents the leg command +var recalculateLegBadgesCmd = &cobra.Command{ + Use: "leg", + Short: "Recalculate Leg Badges", + Run: func(cmd *cobra.Command, args []string) { + err := data.RecalculateLegBadges() + if err != nil { + panic(err) + } + }, +} + +func init() { + recalculateBadgeCmd.AddCommand(recalculateLegBadgesCmd) +} diff --git a/cmd/recalculate_badge.go b/cmd/recalculate_badge.go index 7176d6d3..f5d58ed4 100644 --- a/cmd/recalculate_badge.go +++ b/cmd/recalculate_badge.go @@ -1,7 +1,6 @@ package cmd import ( - "github.com/kcapp/api/data" "github.com/kcapp/api/models" "github.com/spf13/cobra" ) @@ -22,18 +21,6 @@ var recalculateBadgeCmd = &cobra.Command{ } models.InitDB(config.GetMysqlConnectionString()) }, - Run: func(cmd *cobra.Command, args []string) { - err := data.RecalculateGlobalBadges() - if err != nil { - panic(err) - } - - err = data.RecalculateLegBadges() - if err != nil { - panic(err) - } - - }, } func init() { From 554c6131d2fbbc3e2996fb4ecddd68ee867b09fd Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Tue, 12 Sep 2023 18:30:28 +0200 Subject: [PATCH 37/67] Prepare for release of v2.7.0 --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba6e72d7..9bd3be9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ # Changelog -## [2.7.0] - TBD +## [2.7.0] - 2023-09-12 #### Feature +- Player Badges! - Support for Tournament Presets - Added commands for recalculating statistics, resetting elo etc - New endpoint for getting player hits @@ -139,7 +140,7 @@ #### Feature - Intial version of API for [kcapp-frontend](https://github.com/kcapp/frontend) -[2.7.0]: https://github.com/kcapp/api/compare/v2.6.0...develop +[2.7.0]: https://github.com/kcapp/api/compare/v2.6.0...v2.7.0 [2.6.0]: https://github.com/kcapp/api/compare/v2.5.0...v2.6.0 [2.5.0]: https://github.com/kcapp/api/compare/v2.4.0...v2.5.0 [2.4.0]: https://github.com/kcapp/api/compare/v2.3.0...v2.4.0 From 92c58fca05bd238d37045345a68d22750823d625 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Tue, 12 Sep 2023 22:20:27 +0200 Subject: [PATCH 38/67] Correctly grant badges based on winner ID --- data/leg.go | 1 + 1 file changed, 1 insertion(+) diff --git a/data/leg.go b/data/leg.go index 3665af6d..7d6b16b7 100644 --- a/data/leg.go +++ b/data/leg.go @@ -193,6 +193,7 @@ func FinishLeg(visit models.Visit) error { tx.Rollback() return err } + leg.WinnerPlayerID = winnerID log.Printf("[%d] Finished with player %d winning", visit.LegID, winnerID.ValueOrZero()) if matchType == models.SHOOTOUT { From 51ae83ed0022b99c38f0b0a7a66daca7fd74da81 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Wed, 13 Sep 2023 15:18:47 +0200 Subject: [PATCH 39/67] Calculate high scores and badge high scores in the same way --- data/statistics_x01.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/data/statistics_x01.go b/data/statistics_x01.go index e57f1748..cd31c120 100644 --- a/data/statistics_x01.go +++ b/data/statistics_x01.go @@ -230,8 +230,8 @@ func GetPlayersX01Statistics(ids []int, startingScores ...int) ([]*models.Statis LEFT JOIN matches m2 ON m2.id = l2.match_id AND l2.winner_id = p.id WHERE s.player_id IN (?) AND l.starting_score IN (?) - AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_practice = 0 AND m.is_walkover = 0 - AND IFNULL(l.leg_type_id, m.match_type_id) = 1 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 + AND COALESCE(l.leg_type_id, m.match_type_id) = 1 GROUP BY s.player_id ORDER BY p.id`, ids, startingScores) if err != nil { @@ -934,7 +934,9 @@ func GetPlayerBadgeStatistics(ids []int, legID *int) (map[int]*models.BadgeStati FROM statistics_x01 s LEFT JOIN leg l ON s.leg_id = l.id LEFT JOIN matches m ON l.match_id = m.id - WHERE player_id IN (?) AND l.num_players = 2 AND m.is_practice = 0 + WHERE player_id IN (?) + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 + AND COALESCE(l.leg_type_id, m.match_type_id) = 1 AND m.is_bye = 0 AND m.is_walkover = 0 AND leg_id <= COALESCE(?, ~0) -- BIGINT hack GROUP BY player_id ORDER BY player_id DESC`, ids, legID) From 3af2d3a787e961c6f464f43a3c8e34e26c7e88f9 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Thu, 14 Sep 2023 10:26:15 +0200 Subject: [PATCH 40/67] Initial files for next version --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bd3be9a..b35aa98e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## [2.8.0] - TBD + + ## [2.7.0] - 2023-09-12 #### Feature - Player Badges! @@ -140,6 +143,7 @@ #### Feature - Intial version of API for [kcapp-frontend](https://github.com/kcapp/frontend) +[2.8.0]: https://github.com/kcapp/api/compare/v2.7.0...develop [2.7.0]: https://github.com/kcapp/api/compare/v2.6.0...v2.7.0 [2.6.0]: https://github.com/kcapp/api/compare/v2.5.0...v2.6.0 [2.5.0]: https://github.com/kcapp/api/compare/v2.4.0...v2.5.0 From 132529a13510eb2e9b66bec37e8196027c4aa757 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Wed, 4 Oct 2023 17:47:55 +0200 Subject: [PATCH 41/67] Endpoints for getting badge statistics --- CHANGELOG.md | 4 + cmd/serve.go | 2 + controllers/badge_controller.go | 33 +++++++ data/badge.go | 138 +++++++++++++++++++++++++--- data/leg.go | 2 +- data/player.go | 63 +++++++------ data/statistics_x01.go | 12 +-- models/badge.go | 153 ++++++++++++++++++++++---------- models/dart.go | 12 +++ models/player.go | 7 ++ 10 files changed, 325 insertions(+), 101 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b35aa98e..1b0fec52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,11 @@ # Changelog ## [2.8.0] - TBD +#### Feature +- Endpoint for getting badge statistics +#### Fixed +- Return `outshot_type` for `X01 Handicap` ## [2.7.0] - 2023-09-12 #### Feature diff --git a/cmd/serve.go b/cmd/serve.go index a293af41..40a4afa8 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -143,6 +143,8 @@ var serveCmd = &cobra.Command{ router.HandleFunc("/tournament/match/{id}/probabilities", controllers.GetMatchProbabilities).Methods("GET") router.HandleFunc("/badge", controllers.GetBadges).Methods("GET") + router.HandleFunc("/badge/statistics", controllers.GetBadgesStatistics).Methods("GET") + router.HandleFunc("/badge/{id}/statistics", controllers.GetBadgeStatistics).Methods("GET") log.Printf("Listening on port %d", config.APIConfig.Port) log.Println(http.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", config.APIConfig.Port), router)) diff --git a/controllers/badge_controller.go b/controllers/badge_controller.go index 4e7dbaf1..b08c3920 100644 --- a/controllers/badge_controller.go +++ b/controllers/badge_controller.go @@ -4,7 +4,9 @@ import ( "encoding/json" "log" "net/http" + "strconv" + "github.com/gorilla/mux" "github.com/kcapp/api/data" ) @@ -19,3 +21,34 @@ func GetBadges(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(badges) } + +func GetBadgesStatistics(w http.ResponseWriter, r *http.Request) { + SetHeaders(w) + badges, err := data.GetBadgesStatistics() + if err != nil { + log.Println("Unable to get badge statistics") + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(badges) +} + +func GetBadgeStatistics(w http.ResponseWriter, r *http.Request) { + SetHeaders(w) + params := mux.Vars(r) + id, err := strconv.Atoi(params["id"]) + if err != nil { + log.Println("Invalid id parameter") + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + badges, err := data.GetBadgeStatistics(id) + if err != nil { + log.Println("Unable to get badge statistics") + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(badges) +} diff --git a/data/badge.go b/data/badge.go index ac8c249c..38f9c198 100644 --- a/data/badge.go +++ b/data/badge.go @@ -3,6 +3,8 @@ package data import ( "database/sql" "log" + "strconv" + "strings" "time" "github.com/kcapp/api/models" @@ -14,7 +16,8 @@ func GetBadges() ([]*models.Badge, error) { b.id, b.name, b.description, - b.filename + b.filename, + b.levels FROM badge b`) if err != nil { return nil, err @@ -24,21 +27,130 @@ func GetBadges() ([]*models.Badge, error) { badges := make([]*models.Badge, 0) for rows.Next() { badge := new(models.Badge) + err := rows.Scan(&badge.ID, &badge.Name, &badge.Description, &badge.Filename, &badge.Levels) + if err != nil { + return nil, err + } + badges = append(badges, badge) + } + return badges, nil +} + +func GetBadgesStatistics() ([]*models.BadgeStatistics, error) { + players, err := GetPlayers() + if err != nil { + return nil, err + } + numPlayers := 0 + for _, player := range players { + if !player.IsBot && !player.IsPlaceholder { + numPlayers++ + } + } + rows, err := models.DB.Query(` + SELECT + b.id, p2b.level, p2b.value, + COUNT(DISTINCT p2b.player_id) AS 'players_unlocked', + MIN(p2b.created_at) AS 'first_unlock', + GROUP_CONCAT(DISTINCT p2b.player_id ORDER BY p2b.created_at) AS 'players' + FROM badge b + LEFT JOIN player2badge p2b ON b.id = p2b.badge_id + GROUP BY b.id, p2b.level`) + if err != nil { + return nil, err + } + defer rows.Close() + + badges := make([]*models.BadgeStatistics, 0) + for rows.Next() { + var players []uint8 + statistic := new(models.BadgeStatistics) + err := rows.Scan(&statistic.BadgeID, &statistic.Level, &statistic.Value, &statistic.UnlockedPlayers, &statistic.FirstUnlock, &players) + if err != nil { + return nil, err + } + playerStr := string(players) + if playerStr != "" { + playerIDs := strings.Split(playerStr, ",") + for _, playerID := range playerIDs { + id, err := strconv.Atoi(playerID) + if err != nil { + return nil, err + } + statistic.Players = append(statistic.Players, id) + } + } + + if statistic.UnlockedPlayers > 0 { + statistic.UnlockedPercent = float32(statistic.UnlockedPlayers) / float32(numPlayers) + } + badges = append(badges, statistic) + } + return badges, nil +} + +func GetBadgeStatistics(badgeID int) ([]*models.PlayerBadge, error) { + rows, err := models.DB.Query(` + SELECT + b.id, b.name, b.description, b.filename, + p2b.player_id, p2b.level, p2b.value, p2b.leg_id, + p2b.match_id, p2b.tournament_id, p2b.opponent_player_id, + p2b.visit_id, + s.first_dart, IFNULL(s.first_dart_multiplier, 1), + s.second_dart, IFNULL(s.second_dart_multiplier, 1), + s.third_dart, IFNULL(s.third_dart_multiplier, 1), + p2b.created_at + FROM player2badge p2b + LEFT JOIN badge b ON b.id = p2b.badge_id + LEFT JOIN score s on s.id = p2b.visit_id + WHERE b.id = ? + ORDER BY level, created_at`, badgeID) + if err != nil { + return nil, err + } + defer rows.Close() + + badges := make([]*models.PlayerBadge, 0) + for rows.Next() { + badge := new(models.PlayerBadge) + badge.Badge = new(models.Badge) + darts := make([]*models.Dart, 3) + darts[0] = new(models.Dart) + darts[1] = new(models.Dart) + darts[2] = new(models.Dart) + err := rows.Scan( - &badge.ID, - &badge.Name, - &badge.Description, - &badge.Filename, + &badge.Badge.ID, + &badge.Badge.Name, + &badge.Badge.Description, + &badge.Badge.Filename, + &badge.PlayerID, + &badge.Level, + &badge.Value, + &badge.LegID, + &badge.MatchID, + &badge.TournamentID, + &badge.OpponentPlayerID, + &badge.VisitID, + &darts[0].Value, &darts[0].Multiplier, + &darts[1].Value, &darts[1].Multiplier, + &darts[2].Value, &darts[2].Multiplier, + &badge.CreatedAt, ) if err != nil { return nil, err } + if badge.VisitID.Valid { + badge.Darts = darts + } badges = append(badges, badge) } + if err = rows.Err(); err != nil { + return nil, err + } return badges, nil } - -func CheckLegForBadges(leg *models.Leg, statistics map[int]*models.BadgeStatistics) error { +func CheckLegForBadges(leg *models.Leg, statistics map[int]*models.PlayerBadgeStatistics) error { tx, err := models.DB.Begin() if err != nil { return err @@ -53,16 +165,16 @@ func CheckLegForBadges(leg *models.Leg, statistics map[int]*models.BadgeStatisti } for _, badge := range models.LegBadges { - valid, playerID := badge.Validate(leg) + valid, playerID, visitID := badge.Validate(leg) if valid { if playerID != nil { - err = addLegBadge(tx, *playerID, leg.ID, badge, leg.UpdatedAt) + err = addLegBadge(tx, *playerID, leg.ID, visitID, badge, leg.UpdatedAt) if err != nil { return err } } else { for _, playerID := range leg.Players { - err = addLegBadge(tx, playerID, leg.ID, badge, leg.UpdatedAt) + err = addLegBadge(tx, playerID, leg.ID, visitID, badge, leg.UpdatedAt) if err != nil { return err } @@ -118,9 +230,9 @@ func AddTournamentBadge(playerID int, tournamentID int, badge models.GlobalBadge return nil } -func addLegBadge(tx *sql.Tx, playerID int, legID int, badge models.LegBadge, when time.Time) error { - _, err := tx.Exec("INSERT IGNORE INTO player2badge (player_id, badge_id, leg_id, created_at) VALUES (?, ?, ?, ?)", - playerID, badge.GetID(), legID, when) +func addLegBadge(tx *sql.Tx, playerID int, legID int, visitID *int, badge models.LegBadge, when time.Time) error { + _, err := tx.Exec("INSERT IGNORE INTO player2badge (player_id, badge_id, leg_id, visit_id, created_at) VALUES (?, ?, ?, ?, ?)", + playerID, badge.GetID(), legID, visitID, when) if err != nil { tx.Rollback() return err diff --git a/data/leg.go b/data/leg.go index 7d6b16b7..2e96947a 100644 --- a/data/leg.go +++ b/data/leg.go @@ -730,7 +730,7 @@ func GetLegsForMatch(matchID int) ([]*models.Leg, error) { leg.Visits = visits matchType := leg.LegType.ID - if matchType == models.X01 || matchType == models.TICTACTOE || matchType == models.KNOCKOUT { + if matchType == models.X01 || matchType == models.X01HANDICAP || matchType == models.TICTACTOE || matchType == models.KNOCKOUT { leg.Parameters, err = GetLegParameters(leg.ID) if err != nil { return nil, err diff --git a/data/player.go b/data/player.go index 1fd97cad..5118b89d 100644 --- a/data/player.go +++ b/data/player.go @@ -665,37 +665,19 @@ func GetPlayersInLeg(legID int) (map[int]*models.Player, error) { func GetMatchesPlayedPerPlayer() (map[int]*models.Player, error) { rows, err := models.DB.Query(` SELECT - player_id, - MAX(matches_played) AS matches_played, - MAX(matches_won) AS matches_won, - MAX(legs_played) AS legs_played, - MAX(legs_won) AS legs_won - FROM ( - SELECT - p2l.player_id, - COUNT(DISTINCT p2l.match_id) AS matches_played, - SUM(CASE WHEN p2l.player_id = m.winner_id THEN 1 ELSE 0 END) AS matches_won, - COUNT(p2l.leg_id) AS legs_played, - SUM(CASE WHEN p2l.player_id = m.winner_id THEN 1 ELSE 0 END) AS legs_won - FROM player2leg p2l - JOIN matches m ON m.id = p2l.match_id - JOIN leg l ON l.id = p2l.leg_id AND l.match_id = m.id - WHERE l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 - GROUP BY p2l.player_id - UNION ALL - SELECT - m.winner_id AS player_id, - 0 AS matches_played, - COUNT(DISTINCT m.id) AS matches_won, - 0 AS legs_played, - 0 AS legs_won - FROM matches m - JOIN leg l ON l.match_id = m.id - WHERE l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 - GROUP BY m.winner_id - ) AS subquery - WHERE player_id IS NOT NULL - GROUP BY player_id`) + p.id AS 'player_id', + COUNT(DISTINCT m.id) AS 'matches_played', + COUNT(DISTINCT m2.id) AS 'matches_won', + COUNT(DISTINCT l.id) AS 'legs_played', + COUNT(DISTINCT l2.id) AS 'legs_won' + FROM statistics_x01 s + JOIN player p ON p.id = s.player_id + JOIN leg l ON l.id = s.leg_id + JOIN matches m ON m.id = l.match_id + LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id + LEFT JOIN matches m2 ON m2.id = l2.match_id AND l2.winner_id = p.id + WHERE l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 + GROUP BY s.player_id`) if err != nil { return nil, err } @@ -888,10 +870,14 @@ func GetPlayerBadges(playerID int) ([]*models.PlayerBadge, error) { p2b.match_id, p2b.tournament_id, p2b.opponent_player_id, - p2b.darts, + p2b.visit_id, + s.first_dart, IFNULL(s.first_dart_multiplier, 1), + s.second_dart, IFNULL(s.second_dart_multiplier, 1), + s.third_dart, IFNULL(s.third_dart_multiplier, 1), p2b.created_at FROM player2badge p2b LEFT JOIN badge b ON b.id = p2b.badge_id + LEFT JOIN score s on s.id = p2b.visit_id WHERE p2b.player_id = ?`, playerID) if err != nil { return nil, err @@ -902,6 +888,11 @@ func GetPlayerBadges(playerID int) ([]*models.PlayerBadge, error) { for rows.Next() { badge := new(models.PlayerBadge) badge.Badge = new(models.Badge) + darts := make([]*models.Dart, 3) + darts[0] = new(models.Dart) + darts[1] = new(models.Dart) + darts[2] = new(models.Dart) + err := rows.Scan( &badge.Badge.ID, &badge.Badge.Name, @@ -914,12 +905,18 @@ func GetPlayerBadges(playerID int) ([]*models.PlayerBadge, error) { &badge.MatchID, &badge.TournamentID, &badge.OpponentPlayerID, - &badge.Darts, + &badge.VisitID, + &darts[0].Value, &darts[0].Multiplier, + &darts[1].Value, &darts[1].Multiplier, + &darts[2].Value, &darts[2].Multiplier, &badge.CreatedAt, ) if err != nil { return nil, err } + if badge.VisitID.Valid { + badge.Darts = darts + } badges = append(badges, badge) } if err = rows.Err(); err != nil { diff --git a/data/statistics_x01.go b/data/statistics_x01.go index cd31c120..48bfd4f8 100644 --- a/data/statistics_x01.go +++ b/data/statistics_x01.go @@ -318,8 +318,8 @@ func GetPlayersX01PreviousStatistics(ids []int, startingScores ...int) ([]*model LEFT JOIN matches m2 ON m2.id = l2.match_id AND l2.winner_id = p.id WHERE s.player_id IN (?) AND l.starting_score IN (?) - AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_practice = 0 AND m.is_walkover = 0 - AND m.match_type_id = 1 + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 + AND COALESCE(l.leg_type_id, m.match_type_id) = 1 -- Exclude all matches played this week AND m.updated_at < (CURRENT_DATE - INTERVAL WEEKDAY(CURRENT_DATE) DAY) GROUP BY s.player_id @@ -924,7 +924,7 @@ func RecalculateX01Statistics(legs []int) ([]string, error) { } // GetPlayerBadgeStatistics will return statistics calculate badges for the given players -func GetPlayerBadgeStatistics(ids []int, legID *int) (map[int]*models.BadgeStatistics, error) { +func GetPlayerBadgeStatistics(ids []int, legID *int) (map[int]*models.PlayerBadgeStatistics, error) { q, args, err := sqlx.In(` SELECT player_id, @@ -949,12 +949,12 @@ func GetPlayerBadgeStatistics(ids []int, legID *int) (map[int]*models.BadgeStati } defer rows.Close() - statistics := make(map[int]*models.BadgeStatistics) + statistics := make(map[int]*models.PlayerBadgeStatistics) for _, playerID := range ids { - statistics[playerID] = new(models.BadgeStatistics) + statistics[playerID] = new(models.PlayerBadgeStatistics) } for rows.Next() { - s := new(models.BadgeStatistics) + s := new(models.PlayerBadgeStatistics) err := rows.Scan(&s.PlayerID, &s.Score100sPlus, &s.Score140sPlus, &s.Score180s) if err != nil { return nil, err diff --git a/models/badge.go b/models/badge.go index 4cd9876d..86e319a3 100644 --- a/models/badge.go +++ b/models/badge.go @@ -1,6 +1,7 @@ package models import ( + "encoding/json" "time" "github.com/guregu/null" @@ -8,30 +9,86 @@ import ( // Badge represents a badge model. type Badge struct { - ID int `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Hidden bool `json:"hidden"` - Secret bool `json:"secret"` - Filename string `json:"filename"` + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Hidden bool `json:"hidden"` + Secret bool `json:"secret"` + Filename string `json:"filename"` + Levels null.Int `json:"levels,omitempty"` +} + +// BadgeStatistics represents badge statistics. +type BadgeStatistics struct { + BadgeID int `json:"badge_id"` + Level null.Int `json:"level,omitempty"` + Value null.Int `json:"value,omitempty"` + UnlockedPlayers int `json:"unlocked_players"` + UnlockedPercent float32 `json:"unlocked_percent"` + FirstUnlock null.Time `json:"first_unlock"` + Players []int `json:"players"` } // PlayerBadge represents a Player2Badge model. type PlayerBadge struct { - Badge *Badge `json:"badge"` - PlayerID int `json:"player_id"` - Level null.Int `json:"level,omitempty"` - LegID null.Int `json:"leg_id,omitempty"` - Value null.Int `json:"value,omitempty"` - MatchID null.Int `json:"match_id,omitempty"` - OpponentPlayerID null.Int `json:"opponent_player_id,omitempty"` - TournamentID null.Int `json:"tournament_id,omitempty"` - Darts null.String `json:"darts,omitempty"` - CreatedAt time.Time `json:"created_at"` -} - -// BadgeStatistics struct used for storing badge statistics -type BadgeStatistics struct { + Badge *Badge `json:"badge"` + PlayerID int `json:"player_id"` + Level null.Int `json:"level,omitempty"` + LegID null.Int `json:"leg_id,omitempty"` + Value null.Int `json:"value,omitempty"` + MatchID null.Int `json:"match_id,omitempty"` + OpponentPlayerID null.Int `json:"opponent_player_id,omitempty"` + TournamentID null.Int `json:"tournament_id,omitempty"` + VisitID null.Int `json:"visit_id,omitempty"` + Darts []*Dart `json:"darts,omitempty"` + CreatedAt time.Time `json:"created_at"` +} + +// MarshalJSON will marshall the given object to JSON +func (pb PlayerBadge) MarshalJSON() ([]byte, error) { + // Use a type to get consistent order of JSON key-value pairs. + type playerBadgeJSON struct { + Badge *Badge `json:"badge"` + PlayerID int `json:"player_id"` + Level null.Int `json:"level,omitempty"` + LegID null.Int `json:"leg_id,omitempty"` + Value null.Int `json:"value,omitempty"` + MatchID null.Int `json:"match_id,omitempty"` + OpponentPlayerID null.Int `json:"opponent_player_id,omitempty"` + TournamentID null.Int `json:"tournament_id,omitempty"` + VisitID null.Int `json:"visit_id,omitempty"` + Darts []*Dart `json:"darts,omitempty"` + DartsString string `json:"darts_string,omitempty"` + CreatedAt time.Time `json:"created_at"` + } + var dartsString string + if pb.Darts != nil { + dartsString = pb.Darts[0].String() + if pb.Darts[1].Value.Valid { + dartsString += " " + pb.Darts[1].String() + } + if pb.Darts[2].Value.Valid { + dartsString += " " + pb.Darts[2].String() + } + } + return json.Marshal(playerBadgeJSON{ + Badge: pb.Badge, + PlayerID: pb.PlayerID, + Level: pb.Level, + LegID: pb.LegID, + Value: pb.Value, + MatchID: pb.MatchID, + OpponentPlayerID: pb.OpponentPlayerID, + TournamentID: pb.TournamentID, + VisitID: pb.VisitID, + Darts: pb.Darts, + DartsString: dartsString, + CreatedAt: pb.CreatedAt, + }) +} + +// PlayerBadgeStatistics struct used for storing badge statistics +type PlayerBadgeStatistics struct { PlayerID int Score100sPlus int Score140sPlus int @@ -100,7 +157,7 @@ var LegBadges = []LegBadge{ type LegBadge interface { GetID() int - Validate(*Leg) (bool, *int) + Validate(*Leg) (bool, *int, *int) } type BadgeDoubleDouble struct{ ID int } @@ -117,7 +174,7 @@ type BadgeCloseToPerfect struct{ ID int } func (b BadgeDoubleDouble) GetID() int { return b.ID } -func (b BadgeDoubleDouble) Validate(leg *Leg) (bool, *int) { +func (b BadgeDoubleDouble) Validate(leg *Leg) (bool, *int, *int) { visit := leg.GetLastVisit() doubles := 0 if visit.ThirdDart.IsDouble() { @@ -129,87 +186,87 @@ func (b BadgeDoubleDouble) Validate(leg *Leg) (bool, *int) { if visit.FirstDart.IsDouble() { doubles++ } - return doubles == 2, &visit.PlayerID + return doubles == 2, &visit.PlayerID, &visit.ID } func (b BadgeTripleDouble) GetID() int { return b.ID } -func (b BadgeTripleDouble) Validate(leg *Leg) (bool, *int) { +func (b BadgeTripleDouble) Validate(leg *Leg) (bool, *int, *int) { visit := leg.GetLastVisit() - return visit.FirstDart.IsDouble() && visit.SecondDart.IsDouble() && visit.ThirdDart.IsDouble(), &visit.PlayerID + return visit.FirstDart.IsDouble() && visit.SecondDart.IsDouble() && visit.ThirdDart.IsDouble(), &visit.PlayerID, &visit.ID } func (b BadgeMadHouse) GetID() int { return b.ID } -func (b BadgeMadHouse) Validate(leg *Leg) (bool, *int) { +func (b BadgeMadHouse) Validate(leg *Leg) (bool, *int, *int) { visit := leg.GetLastVisit() last := visit.GetLastDart() - return last.IsDouble() && last.ValueRaw() == 1, &visit.PlayerID + return last.IsDouble() && last.ValueRaw() == 1, &visit.PlayerID, &visit.ID } func (b BadgeMerryChristmas) GetID() int { return b.ID } -func (b BadgeMerryChristmas) Validate(leg *Leg) (bool, *int) { +func (b BadgeMerryChristmas) Validate(leg *Leg) (bool, *int, *int) { d := leg.Endtime.Time - return d.Day() == 25 && d.Month() == 12, nil + return d.Day() == 25 && d.Month() == 12, nil, nil } func (b BadgeHappyNewYear) GetID() int { return b.ID } -func (b BadgeHappyNewYear) Validate(leg *Leg) (bool, *int) { +func (b BadgeHappyNewYear) Validate(leg *Leg) (bool, *int, *int) { d := leg.Endtime.Time - return d.Day() == 31 && d.Month() == 12, nil + return d.Day() == 31 && d.Month() == 12, nil, nil } func (b BadgeBigFish) GetID() int { return b.ID } -func (b BadgeBigFish) Validate(leg *Leg) (bool, *int) { +func (b BadgeBigFish) Validate(leg *Leg) (bool, *int, *int) { visit := leg.GetLastVisit() return visit.FirstDart.IsTriple() && visit.FirstDart.ValueRaw() == 20 && visit.SecondDart.IsTriple() && visit.SecondDart.ValueRaw() == 20 && - visit.ThirdDart.IsDouble() && visit.ThirdDart.IsBull(), &visit.PlayerID + visit.ThirdDart.IsDouble() && visit.ThirdDart.IsBull(), &visit.PlayerID, &visit.ID } func (b BadgeGettingCrowded) GetID() int { return b.ID } -func (b BadgeGettingCrowded) Validate(leg *Leg) (bool, *int) { - return len(leg.Players) > 4, nil +func (b BadgeGettingCrowded) Validate(leg *Leg) (bool, *int, *int) { + return len(leg.Players) > 4, nil, nil } func (b BadgeBullseye) GetID() int { return b.ID } -func (b BadgeBullseye) Validate(leg *Leg) (bool, *int) { +func (b BadgeBullseye) Validate(leg *Leg) (bool, *int, *int) { visit := leg.GetLastVisit() last := visit.GetLastDart() - return last.ValueRaw() == BULLSEYE && last.Multiplier == DOUBLE, &visit.PlayerID + return last.ValueRaw() == BULLSEYE && last.Multiplier == DOUBLE, &visit.PlayerID, &visit.ID } func (b BadgeEasyAs123) GetID() int { return b.ID } -func (b BadgeEasyAs123) Validate(leg *Leg) (bool, *int) { +func (b BadgeEasyAs123) Validate(leg *Leg) (bool, *int, *int) { visit := leg.GetLastVisit() last := visit.GetLastDart() - return visit.GetScore() == 123 && last.IsDouble(), &visit.PlayerID + return visit.GetScore() == 123 && last.IsDouble(), &visit.PlayerID, &visit.ID } func (b BadgeCloseToPerfect) GetID() int { return b.ID } -func (b BadgeCloseToPerfect) Validate(leg *Leg) (bool, *int) { +func (b BadgeCloseToPerfect) Validate(leg *Leg) (bool, *int, *int) { visit := leg.GetLastVisit() - return leg.StartingScore == 501 && visit.DartsThrown < 15 && visit.DartsThrown > 9, &visit.PlayerID + return leg.StartingScore == 501 && visit.DartsThrown < 15 && visit.DartsThrown > 9, &visit.PlayerID, nil } var LegPlayerBadges = []LegPlayerBadge{ - BadgeImpersonator{ID: 22}, + BadgeImpersonator{ID: 21}, BadgeBotBeaterEasy{ID: 22}, BadgeBotBeaterMedium{ID: 23}, BadgeBotBeaterHard{ID: 24}, @@ -286,7 +343,7 @@ var VisitBadges = []VisitBadge{ type VisitBadge interface { GetID() int - Validate(stats *BadgeStatistics, visits []*Visit) (bool, *int) + Validate(stats *PlayerBadgeStatistics, visits []*Visit) (bool, *int) Levels() []int } @@ -302,13 +359,13 @@ func (b BadgeHighScore) Levels() []int { return []int{1, 10, 100, 1000} } -func (b BadgeHighScore) Validate(stats *BadgeStatistics, visits []*Visit) (bool, *int) { +func (b BadgeHighScore) Validate(stats *PlayerBadgeStatistics, visits []*Visit) (bool, *int) { count := 0 for _, visit := range visits { if visit.PlayerID != stats.PlayerID { continue } - if visit.Score == 100 { + if visit.Score >= 100 && visit.Score < 140 { count++ } } @@ -327,13 +384,13 @@ func (b BadgeHigherScore) Levels() []int { return []int{1, 10, 100, 1000} } -func (b BadgeHigherScore) Validate(stats *BadgeStatistics, visits []*Visit) (bool, *int) { +func (b BadgeHigherScore) Validate(stats *PlayerBadgeStatistics, visits []*Visit) (bool, *int) { count := 0 for _, visit := range visits { if visit.PlayerID != stats.PlayerID { continue } - if visit.Score == 140 { + if visit.Score >= 140 && visit.Score < 180 { count++ } } @@ -352,7 +409,7 @@ func (b BadgeTheMaximum) Levels() []int { return []int{1, 10, 50, 100} } -func (b BadgeTheMaximum) Validate(stats *BadgeStatistics, visits []*Visit) (bool, *int) { +func (b BadgeTheMaximum) Validate(stats *PlayerBadgeStatistics, visits []*Visit) (bool, *int) { count := 0 for _, visit := range visits { if visit.PlayerID != stats.PlayerID { diff --git a/models/dart.go b/models/dart.go index fc8d323f..6050dc71 100644 --- a/models/dart.go +++ b/models/dart.go @@ -155,6 +155,18 @@ func (dart Dart) GetString() string { return fmt.Sprintf("%d-NULL", dart.Multiplier) } +func (dart Dart) String() string { + if !dart.Value.Valid { + return "" + } + if dart.Multiplier == TRIPLE { + return fmt.Sprintf("T%d", dart.ValueRaw()) + } else if dart.Multiplier == DOUBLE { + return fmt.Sprintf("D%d", dart.ValueRaw()) + } + return fmt.Sprintf("%d", dart.ValueRaw()) +} + // NewDart will return a new dart with the given settings func NewDart(value null.Int, multipler int64) *Dart { return &Dart{Value: value, Multiplier: multipler} diff --git a/models/player.go b/models/player.go index 2652069a..d9662030 100644 --- a/models/player.go +++ b/models/player.go @@ -59,6 +59,7 @@ func (player Player) MarshalJSON() ([]byte, error) { Name string `json:"name"` FirstName string `json:"first_name"` LastName null.String `json:"last_name"` + DisplayName string `json:"display_name"` VocalName null.String `json:"vocal_name,omitempty"` Nickname null.String `json:"nickname,omitempty"` SlackHandle null.String `json:"slack_handle,omitempty"` @@ -85,6 +86,11 @@ func (player Player) MarshalJSON() ([]byte, error) { if player.PlayerOptions != nil && !player.PlayerOptions.SubtractPerDart.Valid { player.PlayerOptions = nil } + displayName := player.FirstName + if player.Nickname.Valid { + displayName += " '" + player.Nickname.String + "' " + } + displayName += " " + player.LastName.String return json.Marshal(playerJSON{ ID: player.ID, @@ -92,6 +98,7 @@ func (player Player) MarshalJSON() ([]byte, error) { LastName: player.LastName, VocalName: player.VocalName, Nickname: player.Nickname, + DisplayName: displayName, SlackHandle: player.SlackHandle, MatchesPlayed: player.MatchesPlayed, MatchesWon: player.MatchesWon, From 8f43dee801a04f5c6a3491ed0f32ca43a8268729 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Mon, 9 Oct 2023 23:14:11 +0200 Subject: [PATCH 42/67] Initial work to add additional badges --- CHANGELOG.md | 1 + cmd/badge_match.go | 22 ++++ data/badge.go | 120 ++++++++++++----- data/leg.go | 5 + data/match.go | 30 +++++ data/player.go | 31 +++++ data/recalculate.go | 60 ++++++++- data/tournament.go | 2 +- models/badge.go | 306 +++++++++++++++++++++++++++++++++++++++----- models/dart.go | 9 ++ models/leg.go | 11 +- 11 files changed, 530 insertions(+), 67 deletions(-) create mode 100644 cmd/badge_match.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b0fec52..5ef05d63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [2.8.0] - TBD #### Feature - Endpoint for getting badge statistics +- Lots of new badges #### Fixed - Return `outshot_type` for `X01 Handicap` diff --git a/cmd/badge_match.go b/cmd/badge_match.go new file mode 100644 index 00000000..384a7284 --- /dev/null +++ b/cmd/badge_match.go @@ -0,0 +1,22 @@ +package cmd + +import ( + "github.com/kcapp/api/data" + "github.com/spf13/cobra" +) + +// recalculateMatchBadgeCmd represents the match command +var recalculateMatchBadgeCmd = &cobra.Command{ + Use: "match", + Short: "Recalculate Match Badges", + Run: func(cmd *cobra.Command, args []string) { + err := data.RecalculateMatchBadges() + if err != nil { + panic(err) + } + }, +} + +func init() { + recalculateBadgeCmd.AddCommand(recalculateMatchBadgeCmd) +} diff --git a/data/badge.go b/data/badge.go index 38f9c198..10b9aaa6 100644 --- a/data/badge.go +++ b/data/badge.go @@ -103,8 +103,9 @@ func GetBadgeStatistics(badgeID int) ([]*models.PlayerBadge, error) { FROM player2badge p2b LEFT JOIN badge b ON b.id = p2b.badge_id LEFT JOIN score s on s.id = p2b.visit_id + LEFT JOIN player p on p.id = p2b.player_id WHERE b.id = ? - ORDER BY level, created_at`, badgeID) + ORDER BY level, created_at, p.first_name`, badgeID) if err != nil { return nil, err } @@ -168,13 +169,13 @@ func CheckLegForBadges(leg *models.Leg, statistics map[int]*models.PlayerBadgeSt valid, playerID, visitID := badge.Validate(leg) if valid { if playerID != nil { - err = addLegBadge(tx, *playerID, leg.ID, visitID, badge, leg.UpdatedAt) + err = addBadge(tx, badge.GetID(), nil, nil, *playerID, nil, &leg.ID, visitID, leg.UpdatedAt) if err != nil { return err } } else { for _, playerID := range leg.Players { - err = addLegBadge(tx, playerID, leg.ID, visitID, badge, leg.UpdatedAt) + err = addBadge(tx, badge.GetID(), nil, nil, playerID, nil, &leg.ID, visitID, leg.UpdatedAt) if err != nil { return err } @@ -186,19 +187,30 @@ func CheckLegForBadges(leg *models.Leg, statistics map[int]*models.PlayerBadgeSt for _, badge := range models.LegPlayerBadges { valid, playerID := badge.Validate(leg, players) if valid { - err = addLegPlayerBadge(tx, *playerID, leg.ID, badge, leg.UpdatedAt) + err = addBadge(tx, badge.GetID(), nil, nil, *playerID, nil, &leg.ID, nil, leg.UpdatedAt) if err != nil { return err } } } - for _, badge := range models.VisitBadges { + for _, badge := range models.VisitBadgesLevel { for _, playerID := range leg.Players { stats := statistics[playerID] - valid, level := badge.Validate(stats, leg.Visits) + valid, level, visitID := badge.Validate(stats, leg.Visits) if valid { - err = addVisitBadge(tx, playerID, *level, leg.ID, badge, leg.UpdatedAt) + err = addBadge(tx, badge.GetID(), level, badge.Levels(), playerID, nil, &leg.ID, visitID, leg.UpdatedAt) + if err != nil { + return err + } + } + } + } + for _, badge := range models.VisitBadges { + for _, playerID := range leg.Players { + valid, visitID := badge.Validate(playerID, leg.Visits) + if valid { + err = addBadge(tx, badge.GetID(), nil, nil, playerID, nil, &leg.ID, visitID, leg.UpdatedAt) if err != nil { return err } @@ -210,56 +222,98 @@ func CheckLegForBadges(leg *models.Leg, statistics map[int]*models.PlayerBadgeSt return nil } -func AddBadge(playerID int, badge models.GlobalBadge) error { - _, err := models.DB.Exec("INSERT IGNORE INTO player2badge (player_id, badge_id, created_at) VALUES (?, ?, ?)", - playerID, badge.GetID(), time.Now()) +func CheckMatchForBadges(match *models.Match) error { + tx, err := models.DB.Begin() if err != nil { return err } - log.Printf("Added global badge %d to player %d", badge.GetID(), playerID) + + for _, badge := range models.MatchBadges { + valid, playerIDs := badge.Validate(match) + if valid { + if playerIDs != nil { + for _, playerID := range playerIDs { + err = addBadge(tx, badge.GetID(), nil, nil, playerID, &match.ID, nil, nil, match.EndTime) + if err != nil { + return err + } + } + } else { + for _, playerID := range match.Players { + err = addBadge(tx, badge.GetID(), nil, nil, playerID, &match.ID, nil, nil, match.EndTime) + if err != nil { + return err + } + } + } + + } + } + tx.Commit() + return nil } -func AddTournamentBadge(playerID int, tournamentID int, badge models.GlobalBadge, when time.Time) error { - _, err := models.DB.Exec("INSERT IGNORE INTO player2badge (player_id, badge_id, tournament_id, created_at) VALUES (?, ?, ?, ?)", - playerID, badge.GetID(), tournamentID, when) +func AddGlobalBadge(playerID int, badge models.GlobalBadge) error { + return AddGlobalBadgeWithTime(playerID, badge, time.Now()) +} + +func AddGlobalBadgeWithTime(playerID int, badge models.GlobalBadge, when time.Time) error { + _, err := models.DB.Exec("INSERT IGNORE INTO player2badge (player_id, badge_id, created_at) VALUES (?, ?, ?)", + playerID, badge.GetID(), when) if err != nil { return err } - log.Printf("Added tournament badge %d to player %d", badge.GetID(), playerID) + log.Printf("Added global badge %d to player %d", badge.GetID(), playerID) return nil } -func addLegBadge(tx *sql.Tx, playerID int, legID int, visitID *int, badge models.LegBadge, when time.Time) error { - _, err := tx.Exec("INSERT IGNORE INTO player2badge (player_id, badge_id, leg_id, visit_id, created_at) VALUES (?, ?, ?, ?, ?)", - playerID, badge.GetID(), legID, visitID, when) +func AddGlobalLevelBadge(playerID int, level int, badge models.GlobalLevelBadge) error { + _, err := models.DB.Exec(`INSERT INTO player2badge (player_id, badge_id, level, value, created_at) VALUES (?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE value=IF(?>level,?,value),level=?`, + playerID, badge.GetID(), level, badge.Levels()[level-1], time.Now(), level, badge.Levels()[level-1], level) if err != nil { - tx.Rollback() return err } - log.Printf("Added leg badge %d to player %d on leg %d", badge.GetID(), playerID, legID) + log.Printf("Added global badge %d to player %d", badge.GetID(), playerID) return nil } -func addLegPlayerBadge(tx *sql.Tx, playerID int, legID int, badge models.LegPlayerBadge, when time.Time) error { - _, err := tx.Exec("INSERT IGNORE INTO player2badge (player_id, badge_id, leg_id, created_at) VALUES (?, ?, ?, ?)", - playerID, badge.GetID(), legID, when) +func AddTournamentBadge(playerID int, tournamentID int, badge models.GlobalBadge, when time.Time) error { + _, err := models.DB.Exec("INSERT IGNORE INTO player2badge (player_id, badge_id, tournament_id, created_at) VALUES (?, ?, ?, ?)", + playerID, badge.GetID(), tournamentID, when) if err != nil { - tx.Rollback() return err } - log.Printf("Added leg player badge %d to player %d on leg %d", badge.GetID(), playerID, legID) + log.Printf("Added tournament badge %d to player %d", badge.GetID(), playerID) return nil } -func addVisitBadge(tx *sql.Tx, playerID int, level int, legID int, badge models.VisitBadge, when time.Time) error { - _, err := tx.Exec(`INSERT INTO player2badge (player_id, badge_id, level, value, leg_id, created_at) VALUES (?, ?, ?, ?, ?, ?) - ON DUPLICATE KEY UPDATE leg_id=IF(?>level,?,leg_id), created_at=IF(?>level,?,created_at), value=IF(?>level,?,value),level=?`, - playerID, badge.GetID(), level, badge.Levels()[level-1], legID, when, level, legID, level, when, level, badge.Levels()[level-1], level) - if err != nil { - tx.Rollback() - return err +func addBadge(tx *sql.Tx, badgeID int, level *int, levels []int, playerID int, matchID *int, legID *int, visitID *int, when time.Time) error { + if level != nil { + _, err := tx.Exec(`INSERT INTO player2badge(player_id, badge_id, level, value, leg_id, visit_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE leg_id=IF(?>level,?,leg_id), visit_id=IF(?>level,?,visit_id), created_at=IF(?>level,?,created_at), value=IF(?>level,?,value),level=?`, + playerID, badgeID, level, levels[*level-1], legID, visitID, when, level, legID, level, visitID, level, when, level, levels[*level-1], level) + if err != nil { + tx.Rollback() + return err + } + log.Printf("Added badge %d (level %d) to player %d on leg %d", badgeID, *level, playerID, *legID) + } else { + _, err := tx.Exec("INSERT IGNORE INTO player2badge (player_id, badge_id, match_id, leg_id, visit_id, created_at) VALUES (?, ?, ?, ?, ?, ?)", + playerID, badgeID, matchID, legID, visitID, when) + if err != nil { + tx.Rollback() + return err + } + if legID != nil { + log.Printf("Added badge %d to player %d on leg %d", badgeID, playerID, *legID) + } else if matchID != nil { + log.Printf("Added badge %d to player %d on match %d", badgeID, playerID, *matchID) + } else { + log.Printf("Added badge %d to player %d", badgeID, playerID) + + } } - log.Printf("Added visit badge %d (level %d) to player %d on leg %d", badge.GetID(), level, playerID, legID) return nil } diff --git a/data/leg.go b/data/leg.go index 2e96947a..7ded1f40 100644 --- a/data/leg.go +++ b/data/leg.go @@ -549,6 +549,11 @@ func FinishLeg(visit models.Visit) error { } } } + + err = CheckMatchForBadges(match) + if err != nil { + return err + } } else { log.Printf("Match %d is not finished, creating next leg", match.ID) var matchType *int diff --git a/data/match.go b/data/match.go index f72f00fb..e389b479 100644 --- a/data/match.go +++ b/data/match.go @@ -889,3 +889,33 @@ func SwapPlayers(matchID int, newPlayerID int, oldPlayerID int) error { log.Printf("Swapped player %d with %d for match %d", oldPlayerID, newPlayerID, matchID) return nil } + +// GetBadgeMatchesToRecalculate returns all matches which can generate badges which can be recalculated +func GetBadgeMatchesToRecalculate() ([]int, error) { + rows, err := models.DB.Query(` + SELECT m.id FROM matches m + LEFT JOIN leg l ON l.match_id = m.id + WHERE m.is_abandoned = 0 AND m.is_bye = 0 AND m.is_walkover = 0 + AND m.is_finished = 1 AND l.has_scores = 1 AND m.match_type_id = 1 + GROUP BY m.id + ORDER BY m.id ASC`) + if err != nil { + return nil, err + } + defer rows.Close() + + matches := make([]int, 0) + for rows.Next() { + var matchID int + err := rows.Scan(&matchID) + if err != nil { + return nil, err + } + matches = append(matches, matchID) + } + if err = rows.Err(); err != nil { + return nil, err + } + + return matches, nil +} diff --git a/data/player.go b/data/player.go index 5118b89d..a44443f5 100644 --- a/data/player.go +++ b/data/player.go @@ -1337,3 +1337,34 @@ func GetPlayerHits(playerID int, visit models.Visit) ([]*models.Visit, error) { } return hits, nil } + +func GetPlayersMatchTypes() (map[int]int, error) { + rows, err := models.DB.Query(` + SELECT + p2l.player_id, + COUNT(DISTINCT COALESCE(l.leg_type_id, m.match_type_id)) AS 'match_types' + FROM kcapp.player2leg p2l + LEFT JOIN leg l ON l.id = p2l.leg_id + LEFT JOIN matches m ON l.match_id = m.id + WHERE l.is_finished = 1 AND m.is_finished = 1 + GROUP BY p2l.player_id`) + if err != nil { + return nil, err + } + defer rows.Close() + + m := make(map[int]int) + for rows.Next() { + var playerID int + var matchTypes int + err := rows.Scan(&playerID, &matchTypes) + if err != nil { + return nil, err + } + m[playerID] = matchTypes + } + if err = rows.Err(); err != nil { + return nil, err + } + return m, nil +} diff --git a/data/recalculate.go b/data/recalculate.go index 1626e261..1423ea62 100644 --- a/data/recalculate.go +++ b/data/recalculate.go @@ -7,6 +7,7 @@ import ( "sort" "strings" "text/tabwriter" + "time" "github.com/guregu/null" "github.com/kcapp/api/models" @@ -258,22 +259,77 @@ func RecalculateLegBadges() error { return nil } +func RecalculateMatchBadges() error { + ids, err := GetBadgeMatchesToRecalculate() + if err != nil { + return err + } + + for _, matchID := range ids { + log.Printf("Checking match %d for badges", matchID) + match, err := GetMatch(matchID) + if err != nil { + return err + } + + // Calculate badges + err = CheckMatchForBadges(match) + if err != nil { + return err + } + } + + return nil +} + func RecalculateGlobalBadges() error { players, err := GetPlayers() if err != nil { return err } + + matchTypes, err := GetPlayersMatchTypes() + if err != nil { + return err + } for _, player := range players { if player.IsSupporter { // Add supporter badge - err = AddBadge(player.ID, new(models.BadgeKcappSupporter)) + err = AddGlobalBadge(player.ID, new(models.BadgeKcappSupporter)) if err != nil { return err } } if player.VocalName.Valid && strings.HasSuffix(player.VocalName.String, ".wav") { // Add vocal name badge - err = AddBadge(player.ID, new(models.BadgeSayMyName)) + err = AddGlobalBadge(player.ID, new(models.BadgeSayMyName)) + if err != nil { + return err + } + } + + // Bye for Now + if !player.IsActive { + err = AddGlobalBadge(player.ID, new(models.BadgeByeForNow)) + if err != nil { + return err + } + } + + // Old Timer + threeYearsAgo := time.Now().AddDate(-3, 0, 0) + if player.CreatedAt.Before(threeYearsAgo) { + err = AddGlobalBadgeWithTime(player.ID, new(models.BadgeOldTimer), player.CreatedAt.AddDate(3, 0, 0)) + if err != nil { + return err + } + } + + // Versatile Player + types := matchTypes[player.ID] + if types >= 5 { + b := new(models.BadgeVersatilePlayer) + err = AddGlobalLevelBadge(player.ID, models.GetLevel(types, b.Levels()), b) if err != nil { return err } diff --git a/data/tournament.go b/data/tournament.go index 30deca4a..bfe678c7 100644 --- a/data/tournament.go +++ b/data/tournament.go @@ -420,7 +420,7 @@ func GetTournamentOverview(id int) (map[int][]*models.TournamentOverview, error) WHERE m.tournament_id = ? AND m.match_type_id = 1 AND m.is_bye <> 1 GROUP BY p2l.player_id, tg.id - ORDER BY tg.division, pts DESC, diff DESC, is_relegated, manual_order`, id) + ORDER BY tg.division, pts DESC, diff DESC, three_dart_avg DESC, is_relegated, manual_order`, id) if err != nil { return nil, err } diff --git a/models/badge.go b/models/badge.go index 86e319a3..b00934e1 100644 --- a/models/badge.go +++ b/models/badge.go @@ -106,6 +106,8 @@ type BadgeTournament1st struct{ ID int } type BadgeTournament2nd struct{ ID int } type BadgeTournament3rd struct{ ID int } type BadgeUntouchable struct{ ID int } +type BadgeByeForNow struct{ ID int } +type BadgeOldTimer struct{ ID int } func (b BadgeKcappSupporter) GetID() int { return 4 @@ -135,11 +137,101 @@ func (b BadgeUntouchable) GetID() int { return 26 } -var MatchBadges = []MatchBadge{} +func (b BadgeByeForNow) GetID() int { + return 27 +} + +func (b BadgeOldTimer) GetID() int { + return 28 +} + +type GlobalLevelBadge interface { + GetID() int + Levels() []int +} + +type BadgeVersatilePlayer struct{ ID int } + +func (b BadgeVersatilePlayer) GetID() int { + return 29 +} + +func (b BadgeVersatilePlayer) Levels() []int { + return []int{5, 10, 15, 20} +} + +var MatchBadges = []MatchBadge{ + BadgeJustAQuickie{ID: 37}, + BadgeAroundTheWorld{ID: 38}, +} type MatchBadge interface { GetID() int - Validate(*Match) (bool, *int) + // Validate returns bool, player.ID + Validate(match *Match) (bool, []int) +} + +type BadgeJustAQuickie struct{ ID int } +type BadgeAroundTheWorld struct{ ID int } + +func (b BadgeJustAQuickie) GetID() int { + return b.ID +} +func (b BadgeJustAQuickie) Validate(match *Match) (bool, []int) { + if len(match.Legs) == 3 && len(match.Players) > 1 { + first := match.Legs[0] + second := match.Legs[1] + third := match.Legs[2] + if first.GetLastVisit().CreatedAt.Sub(first.Visits[0].CreatedAt).Minutes() <= 2 && + second.GetLastVisit().CreatedAt.Sub(second.Visits[0].CreatedAt).Minutes() <= 2 && + third.GetLastVisit().CreatedAt.Sub(third.Visits[0].CreatedAt).Minutes() <= 2 { + return true, nil + } + } + return false, nil +} + +func (b BadgeAroundTheWorld) GetID() int { + return b.ID +} + +func (b BadgeAroundTheWorld) Validate(match *Match) (bool, []int) { + playerHits := make(map[int][]int) + for playerID := range match.Players { + playerHits[playerID] = make([]int, 0) + } + + for _, leg := range match.Legs { + for _, visit := range leg.Visits { + if !visit.IsBust { + playerHits[visit.PlayerID] = append(playerHits[visit.PlayerID], visit.FirstDart.ValueRaw()) + playerHits[visit.PlayerID] = append(playerHits[visit.PlayerID], visit.SecondDart.ValueRaw()) + playerHits[visit.PlayerID] = append(playerHits[visit.PlayerID], visit.ThirdDart.ValueRaw()) + } + } + } + + players := make([]int, 0) + for playerID, hits := range playerHits { + allHit := true + for i := 1; i <= 20; i++ { + if !containsInt(hits, i) { + allHit = false + break + } + } + if !containsInt(hits, 25) { + allHit = false + break + } + if allHit { + players = append(players, playerID) + } + } + if len(players) > 0 { + return true, players + } + return false, nil } var LegBadges = []LegBadge{ @@ -153,13 +245,14 @@ var LegBadges = []LegBadge{ BadgeBullseye{ID: 14}, BadgeEasyAs123{ID: 15}, BadgeCloseToPerfect{ID: 16}, + BadgeLittleFish{ID: 33}, + BadgeShanghai{ID: 36}, } type LegBadge interface { GetID() int Validate(*Leg) (bool, *int, *int) } - type BadgeDoubleDouble struct{ ID int } type BadgeTripleDouble struct{ ID int } type BadgeMadHouse struct{ ID int } @@ -170,6 +263,13 @@ type BadgeGettingCrowded struct{ ID int } type BadgeBullseye struct{ ID int } type BadgeEasyAs123 struct{ ID int } type BadgeCloseToPerfect struct{ ID int } +type BadgeLittleFish struct{ ID int } +type BadgeShanghai struct{ ID int } + +type BadgeTripleThreat struct{ ID int } +type BadgeBabyTon struct{ ID int } +type BadgeBullBullBull struct{ ID int } +type BadgeSoClose struct{ ID int } func (b BadgeDoubleDouble) GetID() int { return b.ID @@ -265,6 +365,24 @@ func (b BadgeCloseToPerfect) Validate(leg *Leg) (bool, *int, *int) { return leg.StartingScore == 501 && visit.DartsThrown < 15 && visit.DartsThrown > 9, &visit.PlayerID, nil } +func (b BadgeLittleFish) GetID() int { + return b.ID +} +func (b BadgeLittleFish) Validate(leg *Leg) (bool, *int, *int) { + visit := leg.GetLastVisit() + return visit.FirstDart.ValueRaw() == 20 && visit.FirstDart.IsTriple() && + visit.SecondDart.ValueRaw() == 20 && visit.SecondDart.IsSingle() && + visit.ThirdDart.IsBull() && visit.ThirdDart.IsDouble(), &visit.PlayerID, &visit.ID +} + +func (b BadgeShanghai) GetID() int { + return b.ID +} +func (b BadgeShanghai) Validate(leg *Leg) (bool, *int, *int) { + visit := leg.GetLastVisit() + return visit.IsShanghai(), &visit.PlayerID, &visit.ID +} + var LegPlayerBadges = []LegPlayerBadge{ BadgeImpersonator{ID: 21}, BadgeBotBeaterEasy{ID: 22}, @@ -274,6 +392,7 @@ var LegPlayerBadges = []LegPlayerBadge{ type LegPlayerBadge interface { GetID() int + // Validate returns bool, player.ID Validate(*Leg, []*Player2Leg) (bool, *int) } @@ -335,58 +454,56 @@ func (b BadgeBotBeaterHard) Validate(leg *Leg, players []*Player2Leg) (bool, *in return false, nil } -var VisitBadges = []VisitBadge{ +var VisitBadgesLevel = []VisitBadgeLevel{ BadgeHighScore{ID: 1}, BadgeHigherScore{ID: 2}, BadgeTheMaximum{ID: 3}, + BadgeMonotonous{ID: 30}, } -type VisitBadge interface { +type VisitBadgeLevel interface { GetID() int - Validate(stats *PlayerBadgeStatistics, visits []*Visit) (bool, *int) + // Validate returns bool, level, visit.ID + Validate(stats *PlayerBadgeStatistics, visits []*Visit) (bool, *int, *int) Levels() []int } type BadgeHighScore struct{ ID int } type BadgeHigherScore struct{ ID int } type BadgeTheMaximum struct{ ID int } +type BadgeMonotonous struct{ ID int } func (b BadgeHighScore) GetID() int { return b.ID } - func (b BadgeHighScore) Levels() []int { return []int{1, 10, 100, 1000} } - -func (b BadgeHighScore) Validate(stats *PlayerBadgeStatistics, visits []*Visit) (bool, *int) { +func (b BadgeHighScore) Validate(stats *PlayerBadgeStatistics, visits []*Visit) (bool, *int, *int) { count := 0 - for _, visit := range visits { - if visit.PlayerID != stats.PlayerID { - continue - } + playerVisits := getVisitsForPlayer(visits, stats.PlayerID) + for _, visit := range playerVisits { if visit.Score >= 100 && visit.Score < 140 { count++ } } if count > 0 { - level := getLevel(stats.Score100sPlus+count, b.Levels()) - return true, &level + level := GetLevel(stats.Score100sPlus+count, b.Levels()) + return true, &level, nil } - return false, nil + return false, nil, nil } func (b BadgeHigherScore) GetID() int { return b.ID } - func (b BadgeHigherScore) Levels() []int { return []int{1, 10, 100, 1000} } - -func (b BadgeHigherScore) Validate(stats *PlayerBadgeStatistics, visits []*Visit) (bool, *int) { +func (b BadgeHigherScore) Validate(stats *PlayerBadgeStatistics, visits []*Visit) (bool, *int, *int) { count := 0 - for _, visit := range visits { + playerVisits := getVisitsForPlayer(visits, stats.PlayerID) + for _, visit := range playerVisits { if visit.PlayerID != stats.PlayerID { continue } @@ -395,23 +512,22 @@ func (b BadgeHigherScore) Validate(stats *PlayerBadgeStatistics, visits []*Visit } } if count > 0 { - level := getLevel(stats.Score140sPlus+count, b.Levels()) - return true, &level + level := GetLevel(stats.Score140sPlus+count, b.Levels()) + return true, &level, nil } - return false, nil + return false, nil, nil } func (b BadgeTheMaximum) GetID() int { return b.ID } - func (b BadgeTheMaximum) Levels() []int { return []int{1, 10, 50, 100} } - -func (b BadgeTheMaximum) Validate(stats *PlayerBadgeStatistics, visits []*Visit) (bool, *int) { +func (b BadgeTheMaximum) Validate(stats *PlayerBadgeStatistics, visits []*Visit) (bool, *int, *int) { count := 0 - for _, visit := range visits { + playerVisits := getVisitsForPlayer(visits, stats.PlayerID) + for _, visit := range playerVisits { if visit.PlayerID != stats.PlayerID { continue } @@ -420,13 +536,107 @@ func (b BadgeTheMaximum) Validate(stats *PlayerBadgeStatistics, visits []*Visit) } } if count > 0 { - level := getLevel(stats.Score180s+count, b.Levels()) - return true, &level + level := GetLevel(stats.Score180s+count, b.Levels()) + return true, &level, nil + } + return false, nil, nil +} + +func (b BadgeMonotonous) GetID() int { + return b.ID +} +func (b BadgeMonotonous) Levels() []int { + return []int{3, 4, 5, 6} +} +func (b BadgeMonotonous) Validate(stats *PlayerBadgeStatistics, visits []*Visit) (bool, *int, *int) { + playerVisits := getVisitsForPlayer(visits, stats.PlayerID) + for i := len(b.Levels()) - 1; i >= 0; i-- { + monotonous, visit := hasSameVisitsInARow(playerVisits, b.Levels()[i]) + if monotonous { + level := i + 1 + return true, &level, &visit.ID + } + } + return false, nil, nil +} + +var VisitBadges = []VisitBadge{ + BadgeTripleThreat{ID: 31}, + BadgeBabyTon{ID: 32}, + BadgeBullBullBull{ID: 34}, + BadgeSoClose{ID: 35}, +} + +type VisitBadge interface { + GetID() int + // Validate returns bool, visit.ID + Validate(playerID int, visits []*Visit) (bool, *int) +} + +func (b BadgeTripleThreat) GetID() int { + return b.ID +} +func (b BadgeTripleThreat) Validate(playerID int, visits []*Visit) (bool, *int) { + values := []int{20, 19, 18} + playerVisits := getVisitsForPlayer(visits, playerID) + for _, visit := range playerVisits { + if visit.GetScore() == 168 && + visit.FirstDart.IsTriple() && visit.SecondDart.IsTriple() && visit.ThirdDart.IsTriple() && + visit.FirstDart.IsValue(values) && visit.SecondDart.IsValue(values) && visit.ThirdDart.IsValue(values) { + return true, &visit.ID + } + } + return false, nil +} + +func (b BadgeBabyTon) GetID() int { + return b.ID +} +func (b BadgeBabyTon) Validate(playerID int, visits []*Visit) (bool, *int) { + value := []int{19} + playerVisits := getVisitsForPlayer(visits, playerID) + for _, visit := range playerVisits { + if visit.GetScore() == 95 && visit.FirstDart.IsValue(value) && visit.SecondDart.IsValue(value) && visit.ThirdDart.IsValue(value) && + // Only allow a Baby Ton to be T19, 19, 19 + !visit.FirstDart.IsDouble() && !visit.SecondDart.IsDouble() && !visit.ThirdDart.IsDouble() { + return true, &visit.ID + } + } + return false, nil +} + +func (b BadgeBullBullBull) GetID() int { + return b.ID +} +func (b BadgeBullBullBull) Validate(playerID int, visits []*Visit) (bool, *int) { + playerVisits := getVisitsForPlayer(visits, playerID) + for _, visit := range playerVisits { + if visit.FirstDart.IsBull() && visit.FirstDart.IsDouble() && + visit.SecondDart.IsBull() && visit.SecondDart.IsDouble() && + visit.ThirdDart.IsBull() && visit.ThirdDart.IsDouble() { + return true, &visit.ID + } + } + return false, nil +} + +func (b BadgeSoClose) GetID() int { + return b.ID +} +func (b BadgeSoClose) Validate(playerID int, visits []*Visit) (bool, *int) { + value := []int{1} + playerVisits := getVisitsForPlayer(visits, playerID) + for _, visit := range playerVisits { + if visit.FirstDart.IsTriple() && visit.FirstDart.IsValue(value) && + visit.SecondDart.IsTriple() && visit.SecondDart.IsValue(value) && + visit.ThirdDart.IsTriple() && visit.ThirdDart.IsValue(value) { + return true, &visit.ID + } } return false, nil } -func getLevel(value int, levels []int) int { +func GetLevel(value int, levels []int) int { level := 1 for i, treshold := range levels { if value > treshold { @@ -444,3 +654,39 @@ func getBot(skill int64, players []*Player2Leg) *Player2Leg { } return nil } + +func hasSameVisitsInARow(visits []*Visit, numVisits int) (bool, *Visit) { + if len(visits) < numVisits { + return false, nil + } + + for i := numVisits - 1; i < len(visits); i++ { + sameVisits := true + var visit *Visit + for j := 0; j < numVisits-1; j++ { + visit = visits[i-j] + if visit.FirstDart.IsMiss() || visit.SecondDart.IsMiss() || visit.ThirdDart.IsMiss() { + sameVisits = false + break + } + if !visits[i-j].isEqualTo(*visits[i-j-1]) { + sameVisits = false + break + } + } + if sameVisits { + return true, visit + } + } + return false, nil +} + +func getVisitsForPlayer(visits []*Visit, playerID int) []*Visit { + playerVisits := make([]*Visit, 0) + for _, visit := range visits { + if visit.PlayerID == playerID { + playerVisits = append(playerVisits, visit) + } + } + return playerVisits +} diff --git a/models/dart.go b/models/dart.go index 6050dc71..2714399d 100644 --- a/models/dart.go +++ b/models/dart.go @@ -215,6 +215,15 @@ func (dart Dart) ValueRaw() int { return 0 } +func (dart Dart) IsValue(values []int) bool { + for _, value := range values { + if dart.ValueRaw() == value { + return true + } + } + return false +} + // GetMarksHit will return the number of marks hit by the given darts, accounting for numbers requiring less than 3 hits to close // If the number is still open by other players hits = multiplier, otherwise hits = multiplier - prev_hits func (dart *Dart) GetMarksHit(hits map[int]int64, open bool) int64 { diff --git a/models/leg.go b/models/leg.go index 1cf80937..87a59bb3 100644 --- a/models/leg.go +++ b/models/leg.go @@ -153,6 +153,7 @@ func (leg Leg) MarshalJSON() ([]byte, error) { // Use a type to get consistent order of JSON key-value pairs. type legJSON struct { ID int `json:"id"` + StartTime null.Time `json:"start_time,omitempty"` Endtime null.Time `json:"end_time"` StartingScore int `json:"starting_score"` IsFinished bool `json:"is_finished"` @@ -175,9 +176,17 @@ func (leg Leg) MarshalJSON() ([]byte, error) { } round := int(math.Floor(float64(len(leg.Visits))/float64(len(leg.Players))) + 1) + startTime := null.TimeFrom(leg.CreatedAt) + endTime := leg.Endtime + if leg.Visits != nil && len(leg.Visits) > 0 { + startTime = null.TimeFrom(leg.Visits[0].CreatedAt) + endTime = null.TimeFrom(leg.GetLastVisit().CreatedAt) + } + return json.Marshal(legJSON{ ID: leg.ID, - Endtime: leg.Endtime, + StartTime: startTime, + Endtime: endTime, StartingScore: leg.StartingScore, IsFinished: leg.IsFinished, CurrentPlayerID: leg.CurrentPlayerID, From e85921a1301a701fbef333abcf13d414ecd68975 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Tue, 10 Oct 2023 23:13:55 +0200 Subject: [PATCH 43/67] Added new DPL statistic --- CHANGELOG.md | 4 +++ data/tournament.go | 54 +++++++++++++++++++++++++++------ models/badge.go | 8 ++--- models/tournament_statistics.go | 1 + 4 files changed, 53 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ef05d63..27c689a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,12 @@ ## [2.8.0] - TBD #### Feature - Endpoint for getting badge statistics +- Added Darts Per Leg `DPL` metric to tournament - Lots of new badges +#### Changed +- Sort tournament best statistics by leg id for equal values + #### Fixed - Return `outshot_type` for `X01 Handicap` diff --git a/data/tournament.go b/data/tournament.go index bfe678c7..017ff1c3 100644 --- a/data/tournament.go +++ b/data/tournament.go @@ -4,6 +4,7 @@ import ( "database/sql" "log" "math" + "sort" "time" "github.com/guregu/null" @@ -395,15 +396,16 @@ func GetTournamentOverview(id int) (map[int][]*models.TournamentOverview, error) IFNULL(SUM(s.first_nine_ppd_score) / (9 * (COUNT(DISTINCT s.leg_id))), -1) AS 'first_nine_ppd', IFNULL(SUM(s.ppd_score) / SUM(s.darts_thrown) * 3, -1) AS 'three_dart_avg', IFNULL(SUM(s.first_nine_ppd_score) * 3 / (9 * (COUNT(DISTINCT s.leg_id))), -1) AS 'first_nine_three_dart_avg', - IFNULL(SUM(60s_plus), 0) AS '60s_plus', - IFNULL(SUM(100s_plus), 0) AS '100s_plus', - IFNULL(SUM(140s_plus), 0) AS '140s_plus', - IFNULL(SUM(180s), 0) AS '180s', - IFNULL(SUM(accuracy_20) / COUNT(accuracy_20), -1) AS 'accuracy_20s', - IFNULL(SUM(accuracy_19) / COUNT(accuracy_19), -1) AS 'accuracy_19s', - IFNULL(SUM(overall_accuracy) / COUNT(overall_accuracy), -1) AS 'accuracy_overall', + IFNULL(SUM(s.60s_plus), 0) AS '60s_plus', + IFNULL(SUM(s.100s_plus), 0) AS '100s_plus', + IFNULL(SUM(s.140s_plus), 0) AS '140s_plus', + IFNULL(SUM(s.180s), 0) AS '180s', + IFNULL(SUM(s.accuracy_20) / COUNT(s.accuracy_20), -1) AS 'accuracy_20s', + IFNULL(SUM(s.accuracy_19) / COUNT(s.accuracy_19), -1) AS 'accuracy_19s', + IFNULL(SUM(s.overall_accuracy) / COUNT(s.overall_accuracy), -1) AS 'accuracy_overall', IFNULL(SUM(s.checkout_attempts), -1) AS 'checkout_attempts', - IFNULL(COUNT(s.checkout_percentage) / SUM(s.checkout_attempts) * 100, -1) AS 'checkout_percentage' + IFNULL(COUNT(s.checkout_percentage) / SUM(s.checkout_attempts) * 100, -1) AS 'checkout_percentage', + IFNULL((SUM(s_won.darts_thrown)/(COUNT(DISTINCT legs_for.id))), -1) AS 'darts_per_leg' FROM player2leg p2l JOIN matches m ON m.id = p2l.match_id JOIN player p ON p.id = p2l.player_id @@ -413,6 +415,7 @@ func GetTournamentOverview(id int) (map[int][]*models.TournamentOverview, error) LEFT JOIN matches draw ON draw.id = p2l.match_id AND draw.is_finished AND draw.winner_id IS NULL LEFT JOIN leg legs_for ON legs_for.id = p2l.leg_id AND legs_for.winner_id = p.id LEFT JOIN leg legs_against ON legs_against.id = p2l.leg_id AND legs_against.winner_id <> p.id + LEFT JOIN statistics_x01 s_won on s_won.leg_id = legs_for.id AND s.player_id = p.id and s_won.checkout is not null LEFT JOIN matches finished ON m.id = finished.id AND finished.is_finished = 1 JOIN tournament t ON t.id = m.tournament_id JOIN player2tournament p2t ON p2t.player_id = p.id AND p2t.tournament_id = t.id @@ -434,7 +437,8 @@ func GetTournamentOverview(id int) (map[int][]*models.TournamentOverview, error) &group.Name, &group.Division, &stats.PlayerID, &stats.IsPromoted, &stats.IsRelegated, &stats.IsWinner, &stats.ManualOrder, &stats.Played, &stats.MatchesWon, &stats.MatchesDraw, &stats.MatchesLost, &stats.LegsFor, &stats.LegsAgainst, &stats.LegsDifference, &stats.Points, &stats.PPD, &stats.FirstNinePPD, &stats.ThreeDartAvg, &stats.FirstNineThreeDartAvg, &stats.Score60sPlus, &stats.Score100sPlus, &stats.Score140sPlus, - &stats.Score180s, &stats.Accuracy20, &stats.Accuracy19, &stats.AccuracyOverall, &stats.CheckoutAttempts, &stats.CheckoutPercentage) + &stats.Score180s, &stats.Accuracy20, &stats.Accuracy19, &stats.AccuracyOverall, &stats.CheckoutAttempts, &stats.CheckoutPercentage, + &stats.DartsPerLeg) if err != nil { return nil, err } @@ -478,6 +482,37 @@ func GetTournamentStatistics(tournamentID int) (*models.TournamentStatistics, er statistics.Best701DartsThrown = append(statistics.Best701DartsThrown, val.Best701) } } + if statistics.Best301DartsThrown != nil { + sort.Slice(statistics.Best301DartsThrown, func(i, j int) bool { + val1 := statistics.Best301DartsThrown[i] + val2 := statistics.Best301DartsThrown[j] + if val1.Value == val2.Value { + return val1.LegID < val2.LegID + } + return val1.Value < val2.Value + }) + } + if statistics.Best501DartsThrown != nil { + sort.Slice(statistics.Best501DartsThrown, func(i, j int) bool { + val1 := statistics.Best501DartsThrown[i] + val2 := statistics.Best501DartsThrown[j] + if val1.Value == val2.Value { + return val1.LegID < val2.LegID + } + return val1.Value < val2.Value + }) + } + if statistics.Best301DartsThrown != nil { + sort.Slice(statistics.Best701DartsThrown, func(i, j int) bool { + val1 := statistics.Best701DartsThrown[i] + val2 := statistics.Best701DartsThrown[j] + if val1.Value == val2.Value { + return val1.LegID < val2.LegID + } + return val1.Value < val2.Value + }) + } + generalStatistics, err := getTournamentGeneralStatistics(tournamentID) if err != nil { return nil, err @@ -684,7 +719,6 @@ func getTournamentBestStatistics(tournamentID int) ([]*models.StatisticsX01, err for _, val := range bestStatistics { s = append(s, val) } - return s, nil } diff --git a/models/badge.go b/models/badge.go index b00934e1..5f920490 100644 --- a/models/badge.go +++ b/models/badge.go @@ -182,10 +182,10 @@ func (b BadgeJustAQuickie) Validate(match *Match) (bool, []int) { first := match.Legs[0] second := match.Legs[1] third := match.Legs[2] - if first.GetLastVisit().CreatedAt.Sub(first.Visits[0].CreatedAt).Minutes() <= 2 && - second.GetLastVisit().CreatedAt.Sub(second.Visits[0].CreatedAt).Minutes() <= 2 && - third.GetLastVisit().CreatedAt.Sub(third.Visits[0].CreatedAt).Minutes() <= 2 { - return true, nil + if first.GetLastVisit().CreatedAt.Sub(first.Visits[0].CreatedAt).Minutes() <= 3 && + second.GetLastVisit().CreatedAt.Sub(second.Visits[0].CreatedAt).Minutes() <= 3 && + third.GetLastVisit().CreatedAt.Sub(third.Visits[0].CreatedAt).Minutes() <= 3 { + return true, []int{int(match.WinnerID.Int64)} } } return false, nil diff --git a/models/tournament_statistics.go b/models/tournament_statistics.go index 5f330226..eaa32d0b 100644 --- a/models/tournament_statistics.go +++ b/models/tournament_statistics.go @@ -44,6 +44,7 @@ type TournamentOverview struct { FirstNineThreeDartAvg float32 `json:"first_nine_three_dart_avg"` CheckoutAttempts int `json:"checkout_attempts"` CheckoutPercentage float32 `json:"checkout_percentage"` + DartsPerLeg float32 `json:"darts_per_leg"` Score60sPlus int `json:"scores_60s_plus"` Score100sPlus int `json:"scores_100s_plus"` Score140sPlus int `json:"scores_140s_plus"` From f23d20f8d316292098a66e77c1fae92eae1b5fe9 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Wed, 11 Oct 2023 00:36:15 +0200 Subject: [PATCH 44/67] Fixed Little Fish badge validation --- models/badge.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/models/badge.go b/models/badge.go index 5f920490..588fb08f 100644 --- a/models/badge.go +++ b/models/badge.go @@ -370,8 +370,9 @@ func (b BadgeLittleFish) GetID() int { } func (b BadgeLittleFish) Validate(leg *Leg) (bool, *int, *int) { visit := leg.GetLastVisit() - return visit.FirstDart.ValueRaw() == 20 && visit.FirstDart.IsTriple() && - visit.SecondDart.ValueRaw() == 20 && visit.SecondDart.IsSingle() && + return visit.GetScore() == 130 && + visit.FirstDart.ValueRaw() == 20 && (visit.FirstDart.IsSingle() || visit.FirstDart.IsTriple()) && + visit.SecondDart.ValueRaw() == 20 && (visit.SecondDart.IsSingle() || visit.SecondDart.IsTriple()) && visit.ThirdDart.IsBull() && visit.ThirdDart.IsDouble(), &visit.PlayerID, &visit.ID } From b698d9459ec10756b0b264a71fd357a66d92b7d6 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Wed, 11 Oct 2023 00:52:47 +0200 Subject: [PATCH 45/67] Fixed DPL statistics with walkover legs --- data/tournament.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/tournament.go b/data/tournament.go index 017ff1c3..69e9705c 100644 --- a/data/tournament.go +++ b/data/tournament.go @@ -405,7 +405,7 @@ func GetTournamentOverview(id int) (map[int][]*models.TournamentOverview, error) IFNULL(SUM(s.overall_accuracy) / COUNT(s.overall_accuracy), -1) AS 'accuracy_overall', IFNULL(SUM(s.checkout_attempts), -1) AS 'checkout_attempts', IFNULL(COUNT(s.checkout_percentage) / SUM(s.checkout_attempts) * 100, -1) AS 'checkout_percentage', - IFNULL((SUM(s_won.darts_thrown)/(COUNT(DISTINCT legs_for.id))), -1) AS 'darts_per_leg' + IFNULL((SUM(s_won.darts_thrown)/(COUNT(DISTINCT s_won.id))), -1) AS 'darts_per_leg' FROM player2leg p2l JOIN matches m ON m.id = p2l.match_id JOIN player p ON p.id = p2l.player_id From 8ad1ed7b1241d3ccf90355a426487a04c9e10495 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Sat, 14 Oct 2023 15:06:55 +0200 Subject: [PATCH 46/67] New endpoints for getting a single badge --- cmd/serve.go | 1 + controllers/badge_controller.go | 19 +++++++++++++++++++ data/badge.go | 16 ++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/cmd/serve.go b/cmd/serve.go index 40a4afa8..cd67de24 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -144,6 +144,7 @@ var serveCmd = &cobra.Command{ router.HandleFunc("/badge", controllers.GetBadges).Methods("GET") router.HandleFunc("/badge/statistics", controllers.GetBadgesStatistics).Methods("GET") + router.HandleFunc("/badge/{id}", controllers.GetBadge).Methods("GET") router.HandleFunc("/badge/{id}/statistics", controllers.GetBadgeStatistics).Methods("GET") log.Printf("Listening on port %d", config.APIConfig.Port) diff --git a/controllers/badge_controller.go b/controllers/badge_controller.go index b08c3920..fb315365 100644 --- a/controllers/badge_controller.go +++ b/controllers/badge_controller.go @@ -22,6 +22,25 @@ func GetBadges(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(badges) } +func GetBadge(w http.ResponseWriter, r *http.Request) { + SetHeaders(w) + params := mux.Vars(r) + id, err := strconv.Atoi(params["id"]) + if err != nil { + log.Println("Invalid id parameter") + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + badge, err := data.GetBadge(id) + if err != nil { + log.Println("Unable to get badge") + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(badge) +} + func GetBadgesStatistics(w http.ResponseWriter, r *http.Request) { SetHeaders(w) badges, err := data.GetBadgesStatistics() diff --git a/data/badge.go b/data/badge.go index 10b9aaa6..04d7be9f 100644 --- a/data/badge.go +++ b/data/badge.go @@ -36,6 +36,22 @@ func GetBadges() ([]*models.Badge, error) { return badges, nil } +func GetBadge(badgeID int) (*models.Badge, error) { + badge := new(models.Badge) + err := models.DB.QueryRow(` + SELECT + b.id, + b.name, + b.description, + b.filename, + b.levels + FROM badge b WHERE b.id = ?`, badgeID).Scan(&badge.ID, &badge.Name, &badge.Description, &badge.Filename, &badge.Levels) + if err != nil { + return nil, err + } + return badge, nil +} + func GetBadgesStatistics() ([]*models.BadgeStatistics, error) { players, err := GetPlayers() if err != nil { From 11444d7f810a645f9eec1a27f9601fe49467d9e6 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Fri, 27 Oct 2023 12:37:42 +0200 Subject: [PATCH 47/67] Initial work on 170 type --- controllers/leg_controller.go | 8 + controllers/match_controller.go | 8 + controllers/statistics_controller.go | 10 +- data/leg.go | 48 +++- data/match.go | 7 + data/player.go | 34 +++ data/score.go | 19 ++ data/statistics_170.go | 324 +++++++++++++++++++++++++++ models/badge.go | 1 - models/leg.go | 13 +- models/match.go | 2 + models/statistics_170.go | 23 ++ 12 files changed, 489 insertions(+), 8 deletions(-) create mode 100644 data/statistics_170.go create mode 100644 models/statistics_170.go diff --git a/controllers/leg_controller.go b/controllers/leg_controller.go index 4bc7ab5b..ef4202d5 100644 --- a/controllers/leg_controller.go +++ b/controllers/leg_controller.go @@ -224,6 +224,14 @@ func GetStatisticsForLeg(w http.ResponseWriter, r *http.Request) { return } json.NewEncoder(w).Encode(stats) + } else if matchType == models.ONESEVENTY { + stats, err := data.Get170StatisticsForLeg(legID) + if err != nil { + log.Println("Unable to get 170 statistics", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + json.NewEncoder(w).Encode(stats) } else { stats, err := data.GetX01StatisticsForLeg(legID) if err != nil { diff --git a/controllers/match_controller.go b/controllers/match_controller.go index 940f398b..3908acf0 100644 --- a/controllers/match_controller.go +++ b/controllers/match_controller.go @@ -372,6 +372,14 @@ func GetStatisticsForMatch(w http.ResponseWriter, r *http.Request) { return } json.NewEncoder(w).Encode(stats) + } else if match.MatchType.ID == models.ONESEVENTY { + stats, err := data.Get170StatisticsForMatch(matchID) + if err != nil { + log.Printf("Unable to get 170 statistics for match %d: %s", matchID, err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + json.NewEncoder(w).Encode(stats) } else { stats, err := data.GetX01StatisticsForMatch(matchID) if err != nil { diff --git a/controllers/statistics_controller.go b/controllers/statistics_controller.go index e3c63429..20e3f2ee 100644 --- a/controllers/statistics_controller.go +++ b/controllers/statistics_controller.go @@ -182,7 +182,15 @@ func GetStatistics(w http.ResponseWriter, r *http.Request) { } json.NewEncoder(w).Encode(stats) return - + case models.ONESEVENTY: + stats, err := data.Get170Statistics(params["from"], params["to"]) + if err != nil { + log.Println("Unable to get 170 Statistics", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + json.NewEncoder(w).Encode(stats) + return default: log.Println("Unknown match type parameter") http.Error(w, err.Error(), http.StatusBadRequest) diff --git a/data/leg.go b/data/leg.go index 7ded1f40..f9d3e8d0 100644 --- a/data/leg.go +++ b/data/leg.go @@ -186,6 +186,22 @@ func FinishLeg(visit models.Visit) error { winnerID = null.IntFrom(int64(player.PlayerID)) } } + } else if matchType == models.ONESEVENTY { + scores, err := GetPlayersScore(visit.LegID) + if err != nil { + return err + } + + mostPoints := int64(0) + for playerID, player := range scores { + if player.CurrentPoints.Int64 == mostPoints { + winnerID = null.IntFromPtr(nil) + } + if player.CurrentPoints.Int64 > mostPoints { + mostPoints = player.CurrentPoints.Int64 + winnerID = null.IntFrom(int64(playerID)) + } + } } _, err = tx.Exec(`UPDATE leg SET current_player_id = ?, winner_id = ?, is_finished = 1, end_time = NOW() WHERE id = ?`, visit.PlayerID, winnerID, visit.LegID) @@ -425,6 +441,26 @@ func FinishLeg(visit models.Visit) error { } log.Printf("[%d] Inserting Scam statistics for player %d", visit.LegID, playerID) } + } else if matchType == models.ONESEVENTY { + statisticsMap, err := Calculate170Statistics(visit.LegID) + if err != nil { + tx.Rollback() + return err + } + for playerID, stats := range statisticsMap { + _, err = tx.Exec(` + INSERT INTO statistics_170 + (leg_id, player_id, points, ppd, ppd_score, rounds, checkout_percentage, checkout_attempts, checkout_completed, highest_checkout, darts_thrown, + checkout_9_darts, checkout_8_darts, checkout_7_darts, checkout_6_darts, checkout_5_darts, checkout_4_darts, checkout_3_darts) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, visit.LegID, playerID, stats.Points, stats.PPD, stats.PPDScore, stats.Rounds, + stats.CheckoutPercentage, stats.CheckoutAttempts, stats.CheckoutCompleted, stats.HighestCheckout, stats.DartsThrown, stats.CheckoutDarts[9], + stats.CheckoutDarts[8], stats.CheckoutDarts[7], stats.CheckoutDarts[6], stats.CheckoutDarts[5], stats.CheckoutDarts[4], stats.CheckoutDarts[3]) + if err != nil { + tx.Rollback() + return err + } + log.Printf("[%d] Inserting 170 statistics for player %d", visit.LegID, playerID) + } } else { statisticsMap, err := CalculateX01Statistics(visit.LegID) if err != nil { @@ -735,7 +771,8 @@ func GetLegsForMatch(matchID int) ([]*models.Leg, error) { leg.Visits = visits matchType := leg.LegType.ID - if matchType == models.X01 || matchType == models.X01HANDICAP || matchType == models.TICTACTOE || matchType == models.KNOCKOUT { + if matchType == models.X01 || matchType == models.X01HANDICAP || matchType == models.TICTACTOE || matchType == models.KNOCKOUT || + matchType == models.ONESEVENTY { leg.Parameters, err = GetLegParameters(leg.ID) if err != nil { return nil, err @@ -933,7 +970,8 @@ func GetLeg(id int) (*models.Leg, error) { } matchType := leg.LegType.ID - if matchType == models.X01 || matchType == models.X01HANDICAP || matchType == models.TICTACTOE || matchType == models.KNOCKOUT { + if matchType == models.X01 || matchType == models.X01HANDICAP || matchType == models.TICTACTOE || + matchType == models.KNOCKOUT || matchType == models.ONESEVENTY { leg.Parameters, err = GetLegParameters(id) if err != nil { return nil, err @@ -1318,8 +1356,10 @@ func GetLegParameters(legID int) (*models.LegParameters, error) { n := make([]null.Int, 9) var ost null.Int err := models.DB.QueryRow(` - SELECT outshot_type_id, number_1, number_2, number_3, number_4, number_5, number_6, number_7, number_8, number_9, starting_lives - FROM leg_parameters WHERE leg_id = ?`, legID).Scan(&ost, &n[0], &n[1], &n[2], &n[3], &n[4], &n[5], &n[6], &n[7], &n[8], ¶ms.StartingLives) + SELECT outshot_type_id, number_1, number_2, number_3, number_4, number_5, number_6, number_7, number_8, number_9, starting_lives, + points_to_win, max_rounds + FROM leg_parameters WHERE leg_id = ?`, legID).Scan(&ost, &n[0], &n[1], &n[2], &n[3], &n[4], &n[5], &n[6], &n[7], &n[8], + ¶ms.StartingLives, ¶ms.PointsToWin, ¶ms.MaxRounds) if err != nil { if err == sql.ErrNoRows { return new(models.LegParameters), nil diff --git a/data/match.go b/data/match.go index e389b479..a11126d8 100644 --- a/data/match.go +++ b/data/match.go @@ -72,6 +72,13 @@ func NewMatch(match models.Match) (*models.Match, error) { tx.Rollback() return nil, err } + } else if match.MatchType.ID == models.ONESEVENTY { + params := match.Legs[0].Parameters + _, err = tx.Exec("INSERT INTO leg_parameters (leg_id, points_to_win, max_rounds) VALUES (?, ?, ?)", legID, params.PointsToWin, params.MaxRounds) + if err != nil { + tx.Rollback() + return nil, err + } } tx.Exec("UPDATE matches SET current_leg_id = ? WHERE id = ?", legID, matchID) diff --git a/data/player.go b/data/player.go index a44443f5..968f5204 100644 --- a/data/player.go +++ b/data/player.go @@ -609,6 +609,40 @@ func GetPlayersScore(legID int) (map[int]*models.Player2Leg, error) { } } } + } else if matchType == models.ONESEVENTY { + visits, err := GetLegVisits(legID) + if err != nil { + return nil, err + } + for _, player := range scores { + player.CurrentScore = 170 + player.DartsThrown = 0 + player.CurrentPoints = null.IntFrom(0) + } + + round := 1 + for i, visit := range visits { + if i > 0 && i%len(players) == 0 { + round++ + } + player := scores[visit.PlayerID] + + player.DartsThrown += 3 + if !visit.IsBust { + player.CurrentScore -= visit.GetScore() + } + + if player.CurrentScore == 0 && visit.GetLastDart().IsDouble() { + // We hit a checkout, reset + player.CurrentScore = 170 + player.CurrentPoints.Int64++ + player.DartsThrown = 0 + } else if round != 1 && player.DartsThrown%9 == 0 { + // 9 Darts have been thrown, reset + player.CurrentScore = 170 + player.DartsThrown = 0 + } + } } return scores, nil } diff --git a/data/score.go b/data/score.go index 5662cb2e..14c9f4c4 100644 --- a/data/score.go +++ b/data/score.go @@ -196,6 +196,25 @@ func AddVisit(visit models.Visit) (*models.Visit, error) { isFinished = true } } + } else if matchType == models.ONESEVENTY { + visit.SetIsBust(players[visit.PlayerID].CurrentScore, models.OUTSHOTDOUBLE) + if visit.IsCheckout(players[visit.PlayerID].CurrentScore, models.OUTSHOTDOUBLE) { + players[visit.PlayerID].CurrentPoints.Int64++ + } + + for _, player := range players { + if player.CurrentPoints.Int64 >= leg.Parameters.PointsToWin.Int64 { + // One player hit required number of points + isFinished = true + break + } + } + + round := (len(leg.Visits)+1)/len(leg.Players)/3 + 1 + if leg.Parameters.MaxRounds.Valid && round > int(leg.Parameters.MaxRounds.Int64) { + // We hit max number of rounds, so we are finished + isFinished = true + } } // Determine who will be the next player diff --git a/data/statistics_170.go b/data/statistics_170.go new file mode 100644 index 00000000..ba6dfe6a --- /dev/null +++ b/data/statistics_170.go @@ -0,0 +1,324 @@ +package data + +import ( + "database/sql" + "fmt" + + "github.com/guregu/null" + "github.com/kcapp/api/models" +) + +// Get170Statistics will return statistics for all players active during the given period +func Get170Statistics(from string, to string) ([]*models.StatisticsScam, error) { + rows, err := models.DB.Query(` + SELECT + p.id, + COUNT(DISTINCT m.id) AS 'matches_played', + COUNT(DISTINCT m2.id) AS 'matches_won', + COUNT(DISTINCT l.id) AS 'legs_played', + COUNT(DISTINCT l2.id) AS 'legs_won', + m.office_id AS 'office_id', + SUM(s.darts_thrown_stopper) as 'darts_thrown_stopper', + SUM(s.darts_thrown_scorer) as 'darts_thrown_scorer', + SUM(s.score) / SUM(s.darts_thrown_scorer) as 'ppd', + SUM(s.score) / SUM(s.darts_thrown_scorer) * 3 as 'three_dart_avg', + CAST(SUM(s.score) / COUNT(DISTINCT l.id) AS SIGNED) as 'avg_score', + (20 * COUNT(DISTINCT l.id)) / SUM(darts_thrown_stopper) * 3 as 'mpr' + FROM statistics_scam s + JOIN player p ON p.id = s.player_id + JOIN leg l ON l.id = s.leg_id + JOIN matches m ON m.id = l.match_id + LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id + LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id + WHERE m.updated_at >= ? AND m.updated_at < ? + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 + AND m.match_type_id = 17 + GROUP BY p.id, m.office_id + ORDER BY(COUNT(DISTINCT m2.id) / COUNT(DISTINCT m.id)) DESC, matches_played DESC`, from, to) + if err != nil { + return nil, err + } + defer rows.Close() + + stats := make([]*models.StatisticsScam, 0) + for rows.Next() { + s := new(models.StatisticsScam) + err := rows.Scan(&s.PlayerID, &s.MatchesPlayed, &s.MatchesWon, &s.LegsPlayed, &s.LegsWon, &s.OfficeID, &s.DartsThrownStopper, &s.DartsThrownScorer, + &s.PPD, &s.ThreeDartAvg, &s.Score, &s.MPR) + if err != nil { + return nil, err + } + stats = append(stats, s) + } + return stats, nil +} + +// Get170StatisticsForLeg will return statistics for all players in the given leg +func Get170StatisticsForLeg(id int) ([]*models.StatisticsScam, error) { + rows, err := models.DB.Query(` + SELECT + l.id, + p.id, + s.darts_thrown_scorer, + s.darts_thrown_stopper, + s.score, + s.mpr, + s.ppd, + s.ppd / 3 as 'three_dart_avg' + FROM statistics_scam s + JOIN player p ON p.id = s.player_id + JOIN leg l ON l.id = s.leg_id + JOIN player2leg p2l on l.id = p2l.leg_id AND p.id = p2l.player_id + WHERE l.id = ? GROUP BY p.id ORDER BY p2l.order`, id) + if err != nil { + return nil, err + } + defer rows.Close() + + stats := make([]*models.StatisticsScam, 0) + for rows.Next() { + s := new(models.StatisticsScam) + err := rows.Scan(&s.LegID, &s.PlayerID, &s.DartsThrownScorer, &s.DartsThrownStopper, &s.Score, &s.MPR, &s.PPD, &s.ThreeDartAvg) + if err != nil { + return nil, err + } + stats = append(stats, s) + } + return stats, nil +} + +// Get170StatisticsForMatch will return statistics for all players in the given match +func Get170StatisticsForMatch(id int) ([]*models.StatisticsScam, error) { + rows, err := models.DB.Query(` + SELECT + p.id, + SUM(s.darts_thrown_scorer) as 'darts_thrown_scorer', + SUM(s.darts_thrown_stopper) as 'darts_thrown_stopper', + CAST(SUM(s.score) / COUNT(DISTINCT l.id) AS SIGNED) as 'avg_score', + SUM(s.score) / SUM(s.darts_thrown_scorer) as 'ppd', + SUM(s.score) / SUM(s.darts_thrown_scorer) * 3 as 'three_dart_avg', + (20 * COUNT(DISTINCT l.id)) / SUM(darts_thrown_stopper) * 3 as 'mpr' + FROM statistics_scam s + JOIN player p ON p.id = s.player_id + JOIN leg l ON l.id = s.leg_id + JOIN matches m ON m.id = l.match_id + JOIN player2leg p2l ON p2l.leg_id = l.id AND p2l.player_id = s.player_id + WHERE m.id = ? + GROUP BY p.id + ORDER BY p2l.order`, id) + if err != nil { + return nil, err + } + defer rows.Close() + + stats := make([]*models.StatisticsScam, 0) + for rows.Next() { + s := new(models.StatisticsScam) + err := rows.Scan(&s.PlayerID, &s.DartsThrownScorer, &s.DartsThrownStopper, &s.Score, &s.PPD, &s.ThreeDartAvg, &s.MPR) + if err != nil { + return nil, err + } + stats = append(stats, s) + } + return stats, nil +} + +// Get170StatisticsForPlayer will return Scam statistics for the given player +func Get170StatisticsForPlayer(id int) (*models.StatisticsScam, error) { + s := new(models.StatisticsScam) + err := models.DB.QueryRow(` + SELECT + p.id, + COUNT(DISTINCT m.id) AS 'matches_played', + COUNT(DISTINCT m2.id) AS 'matches_won', + COUNT(DISTINCT l.id) AS 'legs_played', + COUNT(DISTINCT l2.id) AS 'legs_won', + SUM(s.darts_thrown_scorer) as 'darts_thrown_scorer', + SUM(s.darts_thrown_stopper) as 'darts_thrown_stopper', + CAST(SUM(s.score) / COUNT(DISTINCT l.id) AS SIGNED) as 'avg_score', + SUM(darts_thrown_stopper) / 20 * COUNT(DISTINCT l.id) * 3 as 'mpr', + SUM(s.score) / SUM(s.darts_thrown_scorer) as 'ppd', + SUM(s.score) / SUM(s.darts_thrown_scorer) * 3 as 'three_dart_avg' + FROM statistics_scam s + JOIN player p ON p.id = s.player_id + JOIN leg l ON l.id = s.leg_id + JOIN matches m ON m.id = l.match_id + LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id + LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id + WHERE s.player_id = ? + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 + AND m.match_type_id = 16 + GROUP BY p.id`, id).Scan(&s.PlayerID, &s.MatchesPlayed, &s.MatchesWon, &s.LegsPlayed, &s.LegsWon, &s.DartsThrownScorer, &s.DartsThrownStopper, + &s.Score, &s.MPR, &s.PPD, &s.ThreeDartAvg) + if err != nil { + if err == sql.ErrNoRows { + return new(models.StatisticsScam), nil + } + return nil, err + } + return s, nil +} + +// Get170HistoryForPlayer will return history of Scam statistics for the given player +func Get170HistoryForPlayer(id int, limit int) ([]*models.Leg, error) { + legs, err := GetLegsOfType(models.ONESEVENTY, false) + if err != nil { + return nil, err + } + m := make(map[int]*models.Leg) + for _, leg := range legs { + m[leg.ID] = leg + } + + rows, err := models.DB.Query(` + SELECT + l.id, + p.id, + s.points, + s.ppd, + s.ppd_score, + s.rounds, + s.checkout_percentage, + s.checkout_attempts, + s.checkout_completed, + s.highest_checkout, + s.checkout_9_darts, + s.checkout_8_darts, + s.checkout_7_darts, + s.checkout_6_darts, + s.checkout_5_darts, + s.checkout_4_darts, + s.checkout_3_darts + FROM kcapp.statistics_170 s + LEFT JOIN player p ON p.id = s.player_id + LEFT JOIN leg l ON l.id = s.leg_id + LEFT JOIN matches m ON m.id = l.match_id + WHERE s.player_id = ? + AND l.is_finished = 1 AND m.is_abandoned = 0 + AND m.match_type_id = 17 + ORDER BY l.id DESC + LIMIT ?`, id, limit) + if err != nil { + return nil, err + } + defer rows.Close() + + legs = make([]*models.Leg, 0) + for rows.Next() { + s := new(models.Statistics170) + s.CheckoutDarts = make(map[int]int) + err := rows.Scan(&s.LegID, &s.PlayerID, &s.Points, &s.PPD, &s.PPDScore, &s.Rounds, &s.CheckoutPercentage, + &s.CheckoutAttempts, &s.CheckoutCompleted, &s.HighestCheckout, s.CheckoutDarts[9], s.CheckoutDarts[8], + s.CheckoutDarts[7], s.CheckoutDarts[6], s.CheckoutDarts[5], s.CheckoutDarts[4], s.CheckoutDarts[3]) + if err != nil { + return nil, err + } + leg := m[s.LegID] + leg.Statistics = s + legs = append(legs, leg) + } + return legs, nil +} + +// Calculate170Statistics will generate 170 statistics for the given leg +func Calculate170Statistics(legID int) (map[int]*models.Statistics170, error) { + leg, err := GetLeg(legID) + if err != nil { + return nil, err + } + + players, err := GetPlayersScore(legID) + if err != nil { + return nil, err + } + + statisticsMap := make(map[int]*models.Statistics170) + for _, player := range players { + stats := new(models.Statistics170) + stats.PlayerID = player.PlayerID + stats.Points = int(player.CurrentPoints.Int64) + stats.Rounds = len(leg.Visits)/len(leg.Players)/3 + 1 + stats.HighestCheckout = null.IntFrom(0) + stats.CheckoutDarts = make(map[int]int) + for i := 3; i <= 0; i++ { + stats.CheckoutDarts[i] = 0 + } + + player.CurrentScore = 170 + player.DartsThrown = 0 + player.CurrentPoints = null.IntFrom(0) + statisticsMap[player.PlayerID] = stats + } + round := 1 + for i, visit := range leg.Visits { + if i > 0 && i%len(players) == 0 { + round++ + } + stats := statisticsMap[visit.PlayerID] + player := players[visit.PlayerID] + + if !visit.IsBust { + stats.PPDScore += visit.GetScore() + } + stats.DartsThrown = visit.DartsThrown + + stats.DartsThrown += 3 + if !visit.IsBust { + player.CurrentScore -= visit.GetScore() + } + + // TODO checkout attempts + currentScore := player.CurrentScore + if visit.FirstDart.IsCheckoutAttempt(currentScore, 1, models.OUTSHOTDOUBLE) { + stats.CheckoutAttempts++ + } + currentScore -= visit.FirstDart.GetScore() + if visit.SecondDart.IsCheckoutAttempt(currentScore, 2, models.OUTSHOTDOUBLE) { + stats.CheckoutAttempts++ + } + currentScore -= visit.SecondDart.GetScore() + if visit.ThirdDart.IsCheckoutAttempt(currentScore, 3, models.OUTSHOTDOUBLE) { + stats.CheckoutAttempts++ + } + + if player.CurrentScore == 0 && visit.GetLastDart().IsDouble() { + stats.CheckoutCompleted++ + stats.CheckoutDarts[player.DartsThrown]++ + if int(stats.HighestCheckout.Int64) < visit.GetScore() { + stats.HighestCheckout = null.IntFrom(int64(visit.GetScore())) + } + player.CurrentScore = 170 + player.DartsThrown = 0 + } else if round != 1 && player.DartsThrown%9 == 0 { + // 9 Darts have been thrown, reset + player.CurrentScore = 170 + player.DartsThrown = 0 + } + } + + for _, stats := range statisticsMap { + stats.PPD = float32(stats.PPDScore) / float32(stats.DartsThrown) + if stats.CheckoutAttempts > 0 { + stats.CheckoutPercentage = null.FloatFrom(100 / float64(stats.CheckoutAttempts)) + } + stats.ThreeDartAvg = stats.PPD * 3 + } + return statisticsMap, nil +} + +// ReCalculate170Statistics will recaulcate statistics for 170 legs +func ReCalculate170Statistics(legs []int) ([]string, error) { + queries := make([]string, 0) + for _, legID := range legs { + stats, err := CalculateScamStatistics(legID) + if err != nil { + return nil, err + } + for playerID, stat := range stats { + queries = append(queries, fmt.Sprintf(`UPDATE statistics_170 SET darts_thrown_stopper = %d, darts_thrown_scorer = %d, mpr = %f, score = %d, WHERE leg_id = %d AND player_id = %d;`, + stat.DartsThrownStopper, stat.DartsThrownScorer, stat.MPR, stat.Score, legID, playerID)) + } + } + + return queries, nil +} diff --git a/models/badge.go b/models/badge.go index 588fb08f..fce7ca9b 100644 --- a/models/badge.go +++ b/models/badge.go @@ -222,7 +222,6 @@ func (b BadgeAroundTheWorld) Validate(match *Match) (bool, []int) { } if !containsInt(hits, 25) { allHit = false - break } if allHit { players = append(players, playerID) diff --git a/models/leg.go b/models/leg.go index 87a59bb3..a175527d 100644 --- a/models/leg.go +++ b/models/leg.go @@ -39,6 +39,8 @@ type LegParameters struct { Numbers []int `json:"numbers"` Hits map[int]int `json:"hits"` StartingLives null.Int `json:"starting_lives,omitempty"` + PointsToWin null.Int `json:"points_to_win,omitempty"` + MaxRounds null.Int `json:"max_rounds,omitempty"` } // IsTicTacToeWinner will check if the given player has won a game of Tic Tac Toe @@ -174,7 +176,11 @@ func (leg Leg) MarshalJSON() ([]byte, error) { Statistics interface{} `json:"statistics,omitempty"` Parameters *LegParameters `json:"parameters,omitempty"` } + round := int(math.Floor(float64(len(leg.Visits))/float64(len(leg.Players))) + 1) + if leg.LegType.ID == ONESEVENTY { + round = len(leg.Visits)/len(leg.Players)/3 + 1 + } startTime := null.TimeFrom(leg.CreatedAt) endTime := leg.Endtime @@ -225,9 +231,10 @@ type Player2Leg struct { Player *Player `json:"player,omitempty"` BotConfig *BotConfig `json:"bot_config,omitempty"` Hits HitsMap `json:"hits"` - DartsThrown int `json:"darts_thrown,omitempty"` + DartsThrown int `json:"darts_thrown"` IsStopper null.Bool `json:"is_stopper,omitempty"` IsScorer null.Bool `json:"is_scorer,omitempty"` + CurrentPoints null.Int `json:"current_points"` } type HitsMap map[int]*Hits @@ -333,7 +340,9 @@ func (p2l *Player2Leg) AddVisitStatistics(leg Leg) { } else if visit.IsScore180() { p2l.VisitStatistics.Score180Counter++ } - p2l.DartsThrown = visit.DartsThrown + if leg.LegType.ID != ONESEVENTY { + p2l.DartsThrown = visit.DartsThrown + } } } } diff --git a/models/match.go b/models/match.go index ca3bc097..3963c7ef 100644 --- a/models/match.go +++ b/models/match.go @@ -50,6 +50,8 @@ const ( KNOCKOUT = 15 // SCAM constant representing type 16 SCAM = 16 + // ONESEVENTY contenst representing type 17 + ONESEVENTY = 17 ) var MatchTypes = map[int]string{ diff --git a/models/statistics_170.go b/models/statistics_170.go new file mode 100644 index 00000000..db62e465 --- /dev/null +++ b/models/statistics_170.go @@ -0,0 +1,23 @@ +package models + +import ( + "github.com/guregu/null" +) + +// Statistics170 struct used for storing statistics +type Statistics170 struct { + ID int `json:"id"` + LegID int `json:"leg_id"` + PlayerID int `json:"player_id"` + Points int `json:"points"` + PPD float32 `json:"ppd"` + PPDScore int `json:"-"` + ThreeDartAvg float32 `json:"three_dart_avg"` + Rounds int `json:"rounds"` + CheckoutPercentage null.Float `json:"checkout_percentage"` + CheckoutAttempts int `json:"checkout_attempts"` + CheckoutCompleted int `json:"checkout_completed"` + HighestCheckout null.Int `json:"highest_checkout,omitempty"` + DartsThrown int `json:"darts_thrown"` + CheckoutDarts map[int]int `json:"checkout_darts"` +} From 77b35b8bd8ba33f221879ed3971f03fc7bff9b02 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Tue, 31 Oct 2023 14:19:44 +0100 Subject: [PATCH 48/67] Use tournament elo if available for odds --- CHANGELOG.md | 1 + data/match.go | 2 +- data/tournament.go | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27c689a6..3e1209e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ #### Changed - Sort tournament best statistics by leg id for equal values +- Use tournament elo if available for odds #### Fixed - Return `outshot_type` for `X01 Handicap` diff --git a/data/match.go b/data/match.go index e389b479..d8c3409a 100644 --- a/data/match.go +++ b/data/match.go @@ -237,7 +237,7 @@ func GetMatchProbabilities(id int) (*models.Probability, error) { m.id, m.created_at, m.updated_at, IF(TIMEDIFF(MAX(l.updated_at), NOW() - INTERVAL 15 MINUTE) > 0, 1, 0) AS 'is_started', m.is_finished, m.is_abandoned, m.is_walkover, m.winner_id, GROUP_CONCAT(DISTINCT p2l.player_id ORDER BY p2l.order) AS 'players', - GROUP_CONCAT(DISTINCT pe.current_elo ORDER BY p2l.order) AS 'elos', + GROUP_CONCAT(DISTINCT if(pe.tournament_elo_matches<6, pe.current_elo, pe.tournament_elo) ORDER BY p2l.order) AS 'elos', mm.is_draw_possible FROM matches m JOIN player2leg p2l ON p2l.match_id = m.id diff --git a/data/tournament.go b/data/tournament.go index 69e9705c..511491cc 100644 --- a/data/tournament.go +++ b/data/tournament.go @@ -307,7 +307,7 @@ func GetTournamentProbabilities(id int) ([]*models.Probability, error) { m.id, m.created_at, m.updated_at, IF(TIMEDIFF(MAX(l.updated_at), NOW() - INTERVAL 15 MINUTE) > 0, 1, 0) AS 'is_started', m.is_finished, m.is_abandoned, m.is_walkover, m.winner_id, GROUP_CONCAT(DISTINCT p2l.player_id ORDER BY p2l.order) AS 'players', - GROUP_CONCAT(DISTINCT pe.current_elo ORDER BY p2l.order) AS 'elos', + GROUP_CONCAT(DISTINCT if(pe.tournament_elo_matches<6, pe.current_elo, pe.tournament_elo) ORDER BY p2l.order) AS 'elos', (MAX(p.is_placeholder) - 1) * -1 AS 'is_players_decided', mm.is_draw_possible FROM matches m From db3bc8d3b4cb6c66ae0c820ea7ed9fa118fd44a6 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Wed, 14 Feb 2024 15:16:55 +0100 Subject: [PATCH 49/67] Fixed getting X01 History --- data/leg.go | 61 ++++++++++++++++++++++++++++++++++++++++++ data/statistics_x01.go | 25 +++++++++-------- 2 files changed, 73 insertions(+), 13 deletions(-) diff --git a/data/leg.go b/data/leg.go index 7ded1f40..cf655a5a 100644 --- a/data/leg.go +++ b/data/leg.go @@ -6,6 +6,7 @@ import ( "sort" "github.com/guregu/null" + "github.com/jmoiron/sqlx" "github.com/kcapp/api/models" "github.com/kcapp/api/util" ) @@ -750,6 +751,65 @@ func GetLegsForMatch(matchID int) ([]*models.Leg, error) { return legs, nil } +// GetLegs returns all legs with the given IDs +func GetLegs(ids []int) ([]*models.Leg, error) { + q, args, err := sqlx.In(` + SELECT + l.id, l.end_time, l.starting_score, l.is_finished, + l.current_player_id, l.winner_id, l.created_at, l.updated_at, + l.match_id, l.has_scores, GROUP_CONCAT(p2l.player_id ORDER BY p2l.order ASC) as "players", + mt.id as 'match_type_id', mt.name, mt.description + FROM leg l + LEFT JOIN player2leg p2l ON p2l.leg_id = l.id + LEFT JOIN matches m ON m.id = l.match_id + LEFT JOIN match_type mt on mt.id = IFNULL(l.leg_type_id, m.match_type_id) + WHERE l.id IN(?) + GROUP BY l.id + ORDER BY l.id ASC`, ids) + if err != nil { + return nil, err + } + rows, err := models.DB.Query(q, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + legs := make([]*models.Leg, 0) + for rows.Next() { + leg := new(models.Leg) + leg.LegType = new(models.MatchType) + var players string + err := rows.Scan(&leg.ID, &leg.Endtime, &leg.StartingScore, &leg.IsFinished, &leg.CurrentPlayerID, + &leg.WinnerPlayerID, &leg.CreatedAt, &leg.UpdatedAt, &leg.MatchID, &leg.HasScores, &players, &leg.LegType.ID, + &leg.LegType.Name, &leg.LegType.Description) + if err != nil { + return nil, err + } + leg.Players = util.StringToIntArray(players) + visits, err := GetLegVisits(leg.ID) + if err != nil { + return nil, err + } + leg.Visits = visits + + matchType := leg.LegType.ID + if matchType == models.X01 || matchType == models.X01HANDICAP || matchType == models.TICTACTOE || matchType == models.KNOCKOUT || + matchType == models.ONESEVENTY { + leg.Parameters, err = GetLegParameters(leg.ID) + if err != nil { + return nil, err + } + } + legs = append(legs, leg) + } + if err = rows.Err(); err != nil { + return nil, err + } + + return legs, nil +} + // GetLegsOfType returns all legs with scores for the given match type func GetLegsOfType(matchType int, loadVisits bool) ([]*models.Leg, error) { rows, err := models.DB.Query(` @@ -791,6 +851,7 @@ func GetLegsOfType(matchType int, loadVisits bool) ([]*models.Leg, error) { return nil, err } } + leg.LegType = &models.MatchType{ID: matchType} legs = append(legs, leg) } if err = rows.Err(); err != nil { diff --git a/data/statistics_x01.go b/data/statistics_x01.go index 48bfd4f8..ac5089b3 100644 --- a/data/statistics_x01.go +++ b/data/statistics_x01.go @@ -464,15 +464,6 @@ func GetX01StatisticsForPlayer(id int, matchType int) (*models.StatisticsX01, er // GetX01HistoryForPlayer will return history of X01 statistics for the given player func GetX01HistoryForPlayer(id int, limit int, matchType int) ([]*models.Leg, error) { - legs, err := GetLegsOfType(matchType, false) - if err != nil { - return nil, err - } - m := make(map[int]*models.Leg) - for _, leg := range legs { - m[leg.ID] = leg - } - rows, err := models.DB.Query(` SELECT l.id AS 'leg_id', @@ -506,7 +497,8 @@ func GetX01HistoryForPlayer(id int, limit int, matchType int) ([]*models.Leg, er } defer rows.Close() - legs = make([]*models.Leg, 0) + statistics := make(map[int]*models.StatisticsX01, 0) + legIDs := make([]int, 0) for rows.Next() { s := new(models.StatisticsX01) err := rows.Scan(&s.LegID, &s.PlayerID, &s.PPD, &s.FirstNinePPD, &s.ThreeDartAvg, &s.FirstNineThreeDartAvg, &s.Score60sPlus, &s.Score100sPlus, @@ -515,9 +507,16 @@ func GetX01HistoryForPlayer(id int, limit int, matchType int) ([]*models.Leg, er if err != nil { return nil, err } - leg := m[s.LegID] - leg.Statistics = s - legs = append(legs, leg) + statistics[s.LegID] = s + legIDs = append(legIDs, s.LegID) + } + + legs, err := GetLegs(legIDs) + if err != nil { + return nil, err + } + for _, leg := range legs { + leg.Statistics = statistics[leg.ID] } return legs, nil } From 29c437cba1083c5ab37a64efb24d8e5ba5f67221 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Wed, 14 Feb 2024 15:19:29 +0100 Subject: [PATCH 50/67] Removed accidental match type --- data/leg.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/data/leg.go b/data/leg.go index cf655a5a..6f877859 100644 --- a/data/leg.go +++ b/data/leg.go @@ -794,8 +794,7 @@ func GetLegs(ids []int) ([]*models.Leg, error) { leg.Visits = visits matchType := leg.LegType.ID - if matchType == models.X01 || matchType == models.X01HANDICAP || matchType == models.TICTACTOE || matchType == models.KNOCKOUT || - matchType == models.ONESEVENTY { + if matchType == models.X01 || matchType == models.X01HANDICAP || matchType == models.TICTACTOE || matchType == models.KNOCKOUT { leg.Parameters, err = GetLegParameters(leg.ID) if err != nil { return nil, err From 84d7e1dbdcf681df08570e7e1288643450ec7c32 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Tue, 5 Mar 2024 20:35:38 +0100 Subject: [PATCH 51/67] Added additional badges --- data/badge.go | 17 +++ data/statistics_x01.go | 44 +++++++ models/badge.go | 265 ++++++++++++++++++++++++++++++++++++----- models/dart.go | 5 + 4 files changed, 298 insertions(+), 33 deletions(-) diff --git a/data/badge.go b/data/badge.go index 04d7be9f..582f3c20 100644 --- a/data/badge.go +++ b/data/badge.go @@ -127,6 +127,7 @@ func GetBadgeStatistics(badgeID int) ([]*models.PlayerBadge, error) { } defer rows.Close() + playerIds := make([]int, 0) badges := make([]*models.PlayerBadge, 0) for rows.Next() { badge := new(models.PlayerBadge) @@ -161,12 +162,28 @@ func GetBadgeStatistics(badgeID int) ([]*models.PlayerBadge, error) { badge.Darts = darts } badges = append(badges, badge) + playerIds = append(playerIds, badge.PlayerID) } if err = rows.Err(); err != nil { return nil, err } + + if len(badges) > 0 { + badge := badges[0] + if badge.Level.Valid { + stats, err := GetPlayerBadgeStatistics(playerIds, nil) + if err != nil { + return nil, err + } + + for _, badge := range badges { + badge.Statistics = stats[badge.PlayerID] + } + } + } return badges, nil } + func CheckLegForBadges(leg *models.Leg, statistics map[int]*models.PlayerBadgeStatistics) error { tx, err := models.DB.Begin() if err != nil { diff --git a/data/statistics_x01.go b/data/statistics_x01.go index ac5089b3..15351740 100644 --- a/data/statistics_x01.go +++ b/data/statistics_x01.go @@ -954,6 +954,7 @@ func GetPlayerBadgeStatistics(ids []int, legID *int) (map[int]*models.PlayerBadg } for rows.Next() { s := new(models.PlayerBadgeStatistics) + s.BadgeMap = make(map[int]interface{}, 0) err := rows.Scan(&s.PlayerID, &s.Score100sPlus, &s.Score140sPlus, &s.Score180s) if err != nil { return nil, err @@ -964,5 +965,48 @@ func GetPlayerBadgeStatistics(ids []int, legID *int) (map[int]*models.PlayerBadg return nil, err } + q, args, err = sqlx.In(` + SELECT + player_id, first_dart + FROM score s + WHERE + first_dart = second_dart AND first_dart = third_dart + and ((first_dart_multiplier = 1 and second_dart_multiplier = 2 and third_dart_multiplier = 3) + or (first_dart_multiplier = 2 and second_dart_multiplier = 3 and third_dart_multiplier = 1) + or (first_dart_multiplier = 3 and second_dart_multiplier = 1 and third_dart_multiplier = 2) + or (first_dart_multiplier = 3 and second_dart_multiplier = 2 and third_dart_multiplier = 1) + or (first_dart_multiplier = 1 and second_dart_multiplier = 3 and third_dart_multiplier = 2) + or (first_dart_multiplier = 2 and second_dart_multiplier = 1 and third_dart_multiplier = 3)) + AND is_bust = 0 AND s.player_id IN (?) AND leg_id <= COALESCE(?, ~0) -- BIGINT hack + GROUP BY first_dart, player_id`, ids, legID) + if err != nil { + return nil, err + } + rows, err = models.DB.Query(q, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + shanghai := make(map[int][]int, 0) + for _, playerID := range ids { + shanghai[playerID] = make([]int, 0) + } + for rows.Next() { + var playerID int + var number int + err := rows.Scan(&playerID, &number) + if err != nil { + return nil, err + } + shanghai[playerID] = append(shanghai[playerID], number) + } + if err = rows.Err(); err != nil { + return nil, err + } + + for playerID, stats := range statistics { + stats.Shanghais = shanghai[playerID] + } return statistics, nil } diff --git a/models/badge.go b/models/badge.go index 588fb08f..d41abff1 100644 --- a/models/badge.go +++ b/models/badge.go @@ -2,6 +2,7 @@ package models import ( "encoding/json" + "sort" "time" "github.com/guregu/null" @@ -31,35 +32,37 @@ type BadgeStatistics struct { // PlayerBadge represents a Player2Badge model. type PlayerBadge struct { - Badge *Badge `json:"badge"` - PlayerID int `json:"player_id"` - Level null.Int `json:"level,omitempty"` - LegID null.Int `json:"leg_id,omitempty"` - Value null.Int `json:"value,omitempty"` - MatchID null.Int `json:"match_id,omitempty"` - OpponentPlayerID null.Int `json:"opponent_player_id,omitempty"` - TournamentID null.Int `json:"tournament_id,omitempty"` - VisitID null.Int `json:"visit_id,omitempty"` - Darts []*Dart `json:"darts,omitempty"` - CreatedAt time.Time `json:"created_at"` + Badge *Badge `json:"badge"` + PlayerID int `json:"player_id"` + Level null.Int `json:"level,omitempty"` + LegID null.Int `json:"leg_id,omitempty"` + Value null.Int `json:"value,omitempty"` + MatchID null.Int `json:"match_id,omitempty"` + OpponentPlayerID null.Int `json:"opponent_player_id,omitempty"` + TournamentID null.Int `json:"tournament_id,omitempty"` + VisitID null.Int `json:"visit_id,omitempty"` + Darts []*Dart `json:"darts,omitempty"` + CreatedAt time.Time `json:"created_at"` + Statistics *PlayerBadgeStatistics `json:"statistics,omitempty"` } // MarshalJSON will marshall the given object to JSON func (pb PlayerBadge) MarshalJSON() ([]byte, error) { // Use a type to get consistent order of JSON key-value pairs. type playerBadgeJSON struct { - Badge *Badge `json:"badge"` - PlayerID int `json:"player_id"` - Level null.Int `json:"level,omitempty"` - LegID null.Int `json:"leg_id,omitempty"` - Value null.Int `json:"value,omitempty"` - MatchID null.Int `json:"match_id,omitempty"` - OpponentPlayerID null.Int `json:"opponent_player_id,omitempty"` - TournamentID null.Int `json:"tournament_id,omitempty"` - VisitID null.Int `json:"visit_id,omitempty"` - Darts []*Dart `json:"darts,omitempty"` - DartsString string `json:"darts_string,omitempty"` - CreatedAt time.Time `json:"created_at"` + Badge *Badge `json:"badge"` + PlayerID int `json:"player_id"` + Level null.Int `json:"level,omitempty"` + LegID null.Int `json:"leg_id,omitempty"` + Value null.Int `json:"value,omitempty"` + MatchID null.Int `json:"match_id,omitempty"` + OpponentPlayerID null.Int `json:"opponent_player_id,omitempty"` + TournamentID null.Int `json:"tournament_id,omitempty"` + VisitID null.Int `json:"visit_id,omitempty"` + Darts []*Dart `json:"darts,omitempty"` + DartsString string `json:"darts_string,omitempty"` + CreatedAt time.Time `json:"created_at"` + Statistics *PlayerBadgeStatistics `json:"statistics,omitempty"` } var dartsString string if pb.Darts != nil { @@ -71,6 +74,7 @@ func (pb PlayerBadge) MarshalJSON() ([]byte, error) { dartsString += " " + pb.Darts[2].String() } } + return json.Marshal(playerBadgeJSON{ Badge: pb.Badge, PlayerID: pb.PlayerID, @@ -84,15 +88,47 @@ func (pb PlayerBadge) MarshalJSON() ([]byte, error) { Darts: pb.Darts, DartsString: dartsString, CreatedAt: pb.CreatedAt, + Statistics: pb.Statistics, }) } // PlayerBadgeStatistics struct used for storing badge statistics type PlayerBadgeStatistics struct { - PlayerID int - Score100sPlus int - Score140sPlus int - Score180s int + PlayerID int `json:"player_id"` + Score100sPlus int `json:"score_100_plus"` + Score140sPlus int `json:"score_140_plus"` + Score180s int `json:"score_180s"` + Shanghais []int `json:"shanghais"` + BadgeMap map[int]interface{} `json:"values"` +} + +// MarshalJSON will marshall the given object to JSON +func (pbs PlayerBadgeStatistics) MarshalJSON() ([]byte, error) { + // Use a type to get consistent order of JSON key-value pairs. + type playerBadgeStatisticsJSON struct { + PlayerID int `json:"player_id"` + Score100sPlus int `json:"score_100_plus"` + Score140sPlus int `json:"score_140_plus"` + Score180s int `json:"score_180s"` + Shanghais []int `json:"shanghais"` + BadgeMap map[int]interface{} `json:"values"` + } + + pbs.BadgeMap = make(map[int]interface{}, 0) + pbs.BadgeMap[1] = pbs.Score100sPlus + pbs.BadgeMap[2] = pbs.Score140sPlus + pbs.BadgeMap[3] = pbs.Score180s + sort.Ints(pbs.Shanghais) + pbs.BadgeMap[46] = pbs.Shanghais + + return json.Marshal(playerBadgeStatisticsJSON{ + PlayerID: pbs.PlayerID, + Score100sPlus: pbs.Score100sPlus, + Score140sPlus: pbs.Score140sPlus, + Score180s: pbs.Score180s, + Shanghais: pbs.Shanghais, + BadgeMap: pbs.BadgeMap, + }) } type GlobalBadge interface { @@ -163,6 +199,7 @@ func (b BadgeVersatilePlayer) Levels() []int { var MatchBadges = []MatchBadge{ BadgeJustAQuickie{ID: 37}, BadgeAroundTheWorld{ID: 38}, + BadgeOfficiallyGood{ID: 41}, } type MatchBadge interface { @@ -173,6 +210,7 @@ type MatchBadge interface { type BadgeJustAQuickie struct{ ID int } type BadgeAroundTheWorld struct{ ID int } +type BadgeOfficiallyGood struct{ ID int } func (b BadgeJustAQuickie) GetID() int { return b.ID @@ -194,7 +232,6 @@ func (b BadgeJustAQuickie) Validate(match *Match) (bool, []int) { func (b BadgeAroundTheWorld) GetID() int { return b.ID } - func (b BadgeAroundTheWorld) Validate(match *Match) (bool, []int) { playerHits := make(map[int][]int) for playerID := range match.Players { @@ -234,6 +271,26 @@ func (b BadgeAroundTheWorld) Validate(match *Match) (bool, []int) { return false, nil } +func (b BadgeOfficiallyGood) GetID() int { + return b.ID +} +func (b BadgeOfficiallyGood) Validate(match *Match) (bool, []int) { + playerIDs := make([]int, 0) + if match.TournamentID.Valid { + for _, leg := range match.Legs { + for _, visit := range leg.Visits { + if visit.GetScore() == 180 { + playerIDs = append(playerIDs, visit.PlayerID) + } + } + } + } + if len(playerIDs) > 0 { + return true, playerIDs + } + return false, nil +} + var LegBadges = []LegBadge{ BadgeDoubleDouble{ID: 6}, BadgeTripleDouble{ID: 7}, @@ -246,7 +303,12 @@ var LegBadges = []LegBadge{ BadgeEasyAs123{ID: 15}, BadgeCloseToPerfect{ID: 16}, BadgeLittleFish{ID: 33}, - BadgeShanghai{ID: 36}, + BadgeShanghaiCheckout{ID: 36}, + BadgeTripleTrouble{ID: 39}, + BadgePerfection{ID: 40}, + BadgeChampagneShot{ID: 42}, + BadgeYin{ID: 44}, + BadgeYang{ID: 45}, } type LegBadge interface { @@ -264,12 +326,16 @@ type BadgeBullseye struct{ ID int } type BadgeEasyAs123 struct{ ID int } type BadgeCloseToPerfect struct{ ID int } type BadgeLittleFish struct{ ID int } -type BadgeShanghai struct{ ID int } - +type BadgeShanghaiCheckout struct{ ID int } type BadgeTripleThreat struct{ ID int } type BadgeBabyTon struct{ ID int } type BadgeBullBullBull struct{ ID int } type BadgeSoClose struct{ ID int } +type BadgeTripleTrouble struct{ ID int } +type BadgePerfection struct{ ID int } +type BadgeChampagneShot struct{ ID int } +type BadgeYin struct{ ID int } +type BadgeYang struct{ ID int } func (b BadgeDoubleDouble) GetID() int { return b.ID @@ -376,19 +442,111 @@ func (b BadgeLittleFish) Validate(leg *Leg) (bool, *int, *int) { visit.ThirdDart.IsBull() && visit.ThirdDart.IsDouble(), &visit.PlayerID, &visit.ID } -func (b BadgeShanghai) GetID() int { +func (b BadgeShanghaiCheckout) GetID() int { return b.ID } -func (b BadgeShanghai) Validate(leg *Leg) (bool, *int, *int) { +func (b BadgeShanghaiCheckout) Validate(leg *Leg) (bool, *int, *int) { visit := leg.GetLastVisit() return visit.IsShanghai(), &visit.PlayerID, &visit.ID } +func (b BadgeTripleTrouble) GetID() int { + return b.ID +} +func (b BadgeTripleTrouble) Validate(leg *Leg) (bool, *int, *int) { + visit := leg.GetLastVisit() + + if visit.FirstDart.IsDouble() && visit.SecondDart.IsDouble() && visit.ThirdDart.IsDouble() && + visit.FirstDart.ValueRaw() == visit.SecondDart.ValueRaw() && visit.FirstDart.ValueRaw() == visit.ThirdDart.ValueRaw() { + return true, &visit.PlayerID, &visit.ID + + } + return false, nil, nil +} + +func (b BadgePerfection) GetID() int { + return b.ID +} +func (b BadgePerfection) Validate(leg *Leg) (bool, *int, *int) { + visit := leg.GetLastVisit() + if leg.StartingScore == 501 && visit.DartsThrown == 9 { + return true, &visit.PlayerID, nil + } + return false, nil, nil +} + +func (b BadgeChampagneShot) GetID() int { + return b.ID +} +func (b BadgeChampagneShot) Validate(leg *Leg) (bool, *int, *int) { + visit := leg.GetLastVisit() + + if visit.FirstDart.IsBull() && visit.FirstDart.IsDouble() && + visit.SecondDart.IsBull() && visit.SecondDart.IsDouble() && + visit.ThirdDart.ValueRaw() == 16 && visit.ThirdDart.IsDouble() { + return true, &visit.PlayerID, &visit.ID + + } + return false, nil, nil +} + +func (b BadgeYin) GetID() int { + return b.ID +} +func (b BadgeYin) Validate(leg *Leg) (bool, *int, *int) { + black := []int{0, 20, 18, 13, 10, 2, 3, 7, 8, 14, 12} + + completed := true + winner := leg.GetLastVisit().PlayerID + for _, visit := range leg.Visits { + if visit.PlayerID != winner { + continue + } + if !containsInt(black, visit.FirstDart.ValueRaw()) || + !containsInt(black, visit.SecondDart.ValueRaw()) || + !containsInt(black, visit.ThirdDart.ValueRaw()) { + completed = false + break + + } + } + if completed { + return true, &winner, nil + } + return false, nil, nil +} + +func (b BadgeYang) GetID() int { + return b.ID +} +func (b BadgeYang) Validate(leg *Leg) (bool, *int, *int) { + white := []int{0, 1, 4, 6, 15, 17, 19, 16, 11, 9, 5} + completed := true + winner := leg.GetLastVisit().PlayerID + for _, visit := range leg.Visits { + if visit.PlayerID != winner { + continue + } + if !containsInt(white, visit.FirstDart.ValueRaw()) || + !containsInt(white, visit.SecondDart.ValueRaw()) || + !containsInt(white, visit.ThirdDart.ValueRaw()) { + completed = false + break + + } + } + if completed { + return true, &winner, nil + } + return false, nil, nil +} + var LegPlayerBadges = []LegPlayerBadge{ BadgeImpersonator{ID: 21}, BadgeBotBeaterEasy{ID: 22}, BadgeBotBeaterMedium{ID: 23}, BadgeBotBeaterHard{ID: 24}, + BadgeBeerGame{ID: 43}, } type LegPlayerBadge interface { @@ -401,6 +559,7 @@ type BadgeImpersonator struct{ ID int } type BadgeBotBeaterEasy struct{ ID int } type BadgeBotBeaterMedium struct{ ID int } type BadgeBotBeaterHard struct{ ID int } +type BadgeBeerGame struct{ ID int } func (b BadgeImpersonator) GetID() int { return b.ID @@ -455,11 +614,29 @@ func (b BadgeBotBeaterHard) Validate(leg *Leg, players []*Player2Leg) (bool, *in return false, nil } +func (b BadgeBeerGame) GetID() int { + return b.ID +} +func (b BadgeBeerGame) Validate(leg *Leg, players []*Player2Leg) (bool, *int) { + visit := leg.GetLastVisit() + + for _, player := range players { + if player.PlayerID == visit.PlayerID { + continue + } + if player.CurrentScore >= 200 { + return true, &visit.PlayerID + } + } + return false, nil +} + var VisitBadgesLevel = []VisitBadgeLevel{ BadgeHighScore{ID: 1}, BadgeHigherScore{ID: 2}, BadgeTheMaximum{ID: 3}, BadgeMonotonous{ID: 30}, + BadgeShanghai{ID: 46}, } type VisitBadgeLevel interface { @@ -473,6 +650,7 @@ type BadgeHighScore struct{ ID int } type BadgeHigherScore struct{ ID int } type BadgeTheMaximum struct{ ID int } type BadgeMonotonous struct{ ID int } +type BadgeShanghai struct{ ID int } func (b BadgeHighScore) GetID() int { return b.ID @@ -561,6 +739,27 @@ func (b BadgeMonotonous) Validate(stats *PlayerBadgeStatistics, visits []*Visit) return false, nil, nil } +func (b BadgeShanghai) GetID() int { + return b.ID +} +func (b BadgeShanghai) Levels() []int { + return []int{1, 5, 10, 15, 20} +} +func (b BadgeShanghai) Validate(stats *PlayerBadgeStatistics, visits []*Visit) (bool, *int, *int) { + count := 0 + playerVisits := getVisitsForPlayer(visits, stats.PlayerID) + for _, visit := range playerVisits { + if visit.IsShanghai() && containsInt(stats.Shanghais, visit.FirstDart.ValueRaw()) { + count++ + } + } + if count > 0 { + level := GetLevel(len(stats.Shanghais)+count, b.Levels()) + return true, &level, nil + } + return false, nil, nil +} + var VisitBadges = []VisitBadge{ BadgeTripleThreat{ID: 31}, BadgeBabyTon{ID: 32}, diff --git a/models/dart.go b/models/dart.go index 2714399d..4514a21f 100644 --- a/models/dart.go +++ b/models/dart.go @@ -313,3 +313,8 @@ func containsInt(s []int, e int) bool { } return false } + +func removeInt(s []int, i int) []int { + s[i] = s[len(s)-1] + return s[:len(s)-1] +} From c6d080fbd1c6ad048a7ad8ca876935b248cdd372 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Tue, 5 Mar 2024 20:44:47 +0100 Subject: [PATCH 52/67] Fixed build of goose in Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 58dae96d..6afbdf62 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ RUN apk update && apk add --no-cache git gcc WORKDIR $GOPATH/src/github.com/pressly/goose RUN git clone https://github.com/pressly/goose . RUN go get -d -v -RUN CGO_ENABLED=0 go build -tags='no_postgres no_sqlite3' -o $GOPATH/bin/goose -a -ldflags '-extldflags "-static"' ./cmd/goose +RUN CGO_ENABLED=0 go build -tags='no_postgres no_sqlite3 no_ydb no_duckdb' -o $GOPATH/bin/goose -a -ldflags '-extldflags "-static"' ./cmd/goose # Add script to run migrations RUN mkdir -p /usr/local/scripts From 25da81284ac68cc55a79602ad16349a950535f54 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Fri, 8 Mar 2024 11:21:04 +0100 Subject: [PATCH 53/67] Correctly only calculate shanghai during X01 matches for Badges --- data/statistics_x01.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/data/statistics_x01.go b/data/statistics_x01.go index 15351740..ecb8d2ad 100644 --- a/data/statistics_x01.go +++ b/data/statistics_x01.go @@ -935,7 +935,7 @@ func GetPlayerBadgeStatistics(ids []int, legID *int) (map[int]*models.PlayerBadg LEFT JOIN matches m ON l.match_id = m.id WHERE player_id IN (?) AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 - AND COALESCE(l.leg_type_id, m.match_type_id) = 1 + AND COALESCE(l.leg_type_id, m.match_type_id) = 1 -- X01 AND m.is_bye = 0 AND m.is_walkover = 0 AND leg_id <= COALESCE(?, ~0) -- BIGINT hack GROUP BY player_id ORDER BY player_id DESC`, ids, legID) @@ -969,6 +969,8 @@ func GetPlayerBadgeStatistics(ids []int, legID *int) (map[int]*models.PlayerBadg SELECT player_id, first_dart FROM score s + LEFT JOIN leg l ON s.leg_id = l.id + LEFT JOIN matches m ON l.match_id = m.id WHERE first_dart = second_dart AND first_dart = third_dart and ((first_dart_multiplier = 1 and second_dart_multiplier = 2 and third_dart_multiplier = 3) @@ -978,6 +980,9 @@ func GetPlayerBadgeStatistics(ids []int, legID *int) (map[int]*models.PlayerBadg or (first_dart_multiplier = 1 and second_dart_multiplier = 3 and third_dart_multiplier = 2) or (first_dart_multiplier = 2 and second_dart_multiplier = 1 and third_dart_multiplier = 3)) AND is_bust = 0 AND s.player_id IN (?) AND leg_id <= COALESCE(?, ~0) -- BIGINT hack + AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 + AND COALESCE(l.leg_type_id, m.match_type_id) = 1 -- X01 + AND m.is_bye = 0 AND m.is_walkover = 0 GROUP BY first_dart, player_id`, ids, legID) if err != nil { return nil, err From 46132804b1e629d25ae75ee690c99763a6957469 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Fri, 8 Mar 2024 13:33:03 +0100 Subject: [PATCH 54/67] Fixed bug preventing statistics to load when no legs exist --- data/statistics_x01.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data/statistics_x01.go b/data/statistics_x01.go index ecb8d2ad..aa467430 100644 --- a/data/statistics_x01.go +++ b/data/statistics_x01.go @@ -511,6 +511,9 @@ func GetX01HistoryForPlayer(id int, limit int, matchType int) ([]*models.Leg, er legIDs = append(legIDs, s.LegID) } + if len(legIDs) == 0 { + return []*models.Leg{}, nil + } legs, err := GetLegs(legIDs) if err != nil { return nil, err From 76f32f0932576539bf588e1e8af171fce0108f6c Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Fri, 8 Mar 2024 17:16:56 +0100 Subject: [PATCH 55/67] Update all users without office when creating first office --- data/office.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/data/office.go b/data/office.go index 320ccadc..22896c6b 100644 --- a/data/office.go +++ b/data/office.go @@ -13,7 +13,6 @@ func AddOffice(office models.Office) error { return err } - // Prepare statement for inserting data res, err := tx.Exec("INSERT INTO office (name, is_active, is_global) VALUES (?, ?, ?)", office.Name, office.IsActive, office.IsGlobal) if err != nil { tx.Rollback() @@ -24,6 +23,12 @@ func AddOffice(office models.Office) error { tx.Rollback() return err } + // Add all existing players to this office if there are any not connected to a office + _, err = tx.Exec("UPDATE player SET office_id = ? WHERE office_id IS NULL", officeID) + if err != nil { + tx.Rollback() + return err + } log.Printf("Created new office (%d) %s", officeID, office.Name) tx.Commit() return nil From bcbbdfbc1b633d94716e483ae7ec3fddf122d9f4 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Sat, 16 Mar 2024 10:49:27 +0100 Subject: [PATCH 56/67] Added new 170 game type --- CHANGELOG.md | 1 + data/leg.go | 21 +++- data/match.go | 3 - data/player.go | 24 +---- data/statistics_170.go | 220 +++++++++++++++++++++++++++++----------- data/statistics_scam.go | 2 +- data/statistics_x01.go | 7 +- models/visit.go | 22 ++++ 8 files changed, 205 insertions(+), 95 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e1209e6..8fbcd834 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [2.8.0] - TBD #### Feature +- New game type `170` - Endpoint for getting badge statistics - Added Darts Per Leg `DPL` metric to tournament - Lots of new badges diff --git a/data/leg.go b/data/leg.go index 47ef6311..ae380dd6 100644 --- a/data/leg.go +++ b/data/leg.go @@ -87,6 +87,13 @@ func NewLeg(matchID int, startingScore int, players []int, matchType *int) (*mod tx.Rollback() return nil, err } + } else if *matchType == models.ONESEVENTY { + params := match.Legs[0].Parameters + _, err = tx.Exec("INSERT INTO leg_parameters (leg_id, max_rounds, points_to_win) VALUES (?, ?, ?)", legID, params.MaxRounds, params.PointsToWin) + if err != nil { + tx.Rollback() + return nil, err + } } for idx, playerID := range players { @@ -702,6 +709,11 @@ func UndoLegFinish(legID int) error { tx.Rollback() return err } + _, err = tx.Exec("DELETE FROM statistics_170 WHERE leg_id = ?", legID) + if err != nil { + tx.Rollback() + return err + } // Remove the last score _, err = tx.Exec("DELETE FROM score WHERE leg_id = ? ORDER BY id DESC LIMIT 1", legID) @@ -831,7 +843,8 @@ func GetLegs(ids []int) ([]*models.Leg, error) { leg.Visits = visits matchType := leg.LegType.ID - if matchType == models.X01 || matchType == models.X01HANDICAP || matchType == models.TICTACTOE || matchType == models.KNOCKOUT { + if matchType == models.X01 || matchType == models.X01HANDICAP || matchType == models.TICTACTOE || matchType == models.KNOCKOUT || + matchType == models.ONESEVENTY { leg.Parameters, err = GetLegParameters(leg.ID) if err != nil { return nil, err @@ -881,7 +894,8 @@ func GetLegsOfType(matchType int, loadVisits bool) ([]*models.Leg, error) { } leg.Visits = visits } - if matchType == models.X01 || matchType == models.TICTACTOE || matchType == models.KNOCKOUT { + if matchType == models.X01 || matchType == models.TICTACTOE || matchType == models.KNOCKOUT || + matchType == models.ONESEVENTY { leg.Parameters, err = GetLegParameters(leg.ID) if err != nil { return nil, err @@ -1188,6 +1202,9 @@ func GetLeg(id int) (*models.Leg, error) { score = visit.CalculateScamScore(scores) scores[visit.PlayerID].CurrentScore += score } + } else if matchType == models.ONESEVENTY { + player := scores[visit.PlayerID] + score = visit.Calculate170Score(round, player) } else { scores[visit.PlayerID].CurrentScore -= score } diff --git a/data/match.go b/data/match.go index 86cfcb8d..624d849f 100644 --- a/data/match.go +++ b/data/match.go @@ -644,9 +644,6 @@ func GetMatchMetadataForTournament(tournamentID int) ([]*models.MatchMetadata, e metadata = append(metadata, m) } - if err != nil { - return nil, err - } return metadata, nil } diff --git a/data/player.go b/data/player.go index 968f5204..b32ff9e0 100644 --- a/data/player.go +++ b/data/player.go @@ -167,9 +167,6 @@ func GetPlayerEloChangelog(id int, start int, limit int) (*models.PlayerEloChang } defer rows.Close() - if err != nil { - return nil, err - } changelogs := new(models.PlayerEloChangelogs) changelogs.Total = total changelogs.Changelog = make([]*models.PlayerEloChangelog, 0) @@ -311,10 +308,10 @@ func GetPlayersScore(legID int) (map[int]*models.Player2Leg, error) { FROM player2leg p2l LEFT JOIN player p on p.id = p2l.player_id LEFT JOIN leg l ON l.id = p2l.leg_id - LEFT JOIN score s ON s.leg_id = p2l.leg_id AND s.player_id = p2l.player_id + LEFT JOIN score s ON s.leg_id = p2l.leg_id AND s.player_id = p2l.player_id AND s.is_bust = 0 LEFT JOIN matches m on m.id = l.match_id LEFT JOIN bot2player2leg b ON b.player2leg_id = p2l.id - WHERE p2l.leg_id = ? AND (s.is_bust IS NULL OR is_bust = 0) + WHERE p2l.leg_id = ? GROUP BY p2l.player_id ORDER BY p2l.order ASC`, legID) if err != nil { @@ -626,22 +623,7 @@ func GetPlayersScore(legID int) (map[int]*models.Player2Leg, error) { round++ } player := scores[visit.PlayerID] - - player.DartsThrown += 3 - if !visit.IsBust { - player.CurrentScore -= visit.GetScore() - } - - if player.CurrentScore == 0 && visit.GetLastDart().IsDouble() { - // We hit a checkout, reset - player.CurrentScore = 170 - player.CurrentPoints.Int64++ - player.DartsThrown = 0 - } else if round != 1 && player.DartsThrown%9 == 0 { - // 9 Darts have been thrown, reset - player.CurrentScore = 170 - player.DartsThrown = 0 - } + visit.Calculate170Score(round, player) } } return scores, nil diff --git a/data/statistics_170.go b/data/statistics_170.go index ba6dfe6a..813cd795 100644 --- a/data/statistics_170.go +++ b/data/statistics_170.go @@ -9,7 +9,7 @@ import ( ) // Get170Statistics will return statistics for all players active during the given period -func Get170Statistics(from string, to string) ([]*models.StatisticsScam, error) { +func Get170Statistics(from string, to string) ([]*models.Statistics170, error) { rows, err := models.DB.Query(` SELECT p.id, @@ -18,13 +18,23 @@ func Get170Statistics(from string, to string) ([]*models.StatisticsScam, error) COUNT(DISTINCT l.id) AS 'legs_played', COUNT(DISTINCT l2.id) AS 'legs_won', m.office_id AS 'office_id', - SUM(s.darts_thrown_stopper) as 'darts_thrown_stopper', - SUM(s.darts_thrown_scorer) as 'darts_thrown_scorer', - SUM(s.score) / SUM(s.darts_thrown_scorer) as 'ppd', - SUM(s.score) / SUM(s.darts_thrown_scorer) * 3 as 'three_dart_avg', - CAST(SUM(s.score) / COUNT(DISTINCT l.id) AS SIGNED) as 'avg_score', - (20 * COUNT(DISTINCT l.id)) / SUM(darts_thrown_stopper) * 3 as 'mpr' - FROM statistics_scam s + SUM(s.points), + IF(s.darts_thrown = 0, 0, SUM(s.ppd_score) / SUM(s.darts_thrown)), + IF(s.darts_thrown = 0, 0, SUM(s.ppd_score) / SUM(s.darts_thrown) * 3), + SUM(s.rounds), + COUNT(s.checkout_percentage) / SUM(s.checkout_attempts) * 100, + SUM(s.checkout_completed), + SUM(s.checkout_attempts), + MAX(s.highest_checkout), + SUM(s.darts_thrown), + SUM(s.checkout_9_darts), + SUM(s.checkout_8_darts), + SUM(s.checkout_7_darts), + SUM(s.checkout_6_darts), + SUM(s.checkout_5_darts), + SUM(s.checkout_4_darts), + SUM(s.checkout_3_darts) + FROM statistics_170 s JOIN player p ON p.id = s.player_id JOIN leg l ON l.id = s.leg_id JOIN matches m ON m.id = l.match_id @@ -40,32 +50,53 @@ func Get170Statistics(from string, to string) ([]*models.StatisticsScam, error) } defer rows.Close() - stats := make([]*models.StatisticsScam, 0) + stats := make([]*models.Statistics170, 0) for rows.Next() { - s := new(models.StatisticsScam) - err := rows.Scan(&s.PlayerID, &s.MatchesPlayed, &s.MatchesWon, &s.LegsPlayed, &s.LegsWon, &s.OfficeID, &s.DartsThrownStopper, &s.DartsThrownScorer, - &s.PPD, &s.ThreeDartAvg, &s.Score, &s.MPR) + s := new(models.Statistics170) + var darts9, darts8, darts7, darts6, darts5, darts4, darts3 int + + checkoutDarts := make(map[int]int, 0) + err := rows.Scan(&s.LegID, &s.PlayerID, &s.Points, &s.PPD, &s.ThreeDartAvg, &s.Rounds, &s.CheckoutPercentage, &s.CheckoutCompleted, + &s.CheckoutAttempts, &s.HighestCheckout, &s.DartsThrown, &darts9, &darts8, &darts7, &darts6, &darts5, &darts4, &darts3) if err != nil { return nil, err } + checkoutDarts[9] = darts9 + checkoutDarts[8] = darts8 + checkoutDarts[7] = darts7 + checkoutDarts[6] = darts6 + checkoutDarts[5] = darts5 + checkoutDarts[4] = darts4 + checkoutDarts[3] = darts3 + s.CheckoutDarts = checkoutDarts stats = append(stats, s) } return stats, nil } // Get170StatisticsForLeg will return statistics for all players in the given leg -func Get170StatisticsForLeg(id int) ([]*models.StatisticsScam, error) { +func Get170StatisticsForLeg(id int) ([]*models.Statistics170, error) { rows, err := models.DB.Query(` SELECT l.id, p.id, - s.darts_thrown_scorer, - s.darts_thrown_stopper, - s.score, - s.mpr, + s.points, s.ppd, - s.ppd / 3 as 'three_dart_avg' - FROM statistics_scam s + s.ppd_score / s.darts_thrown * 3, + s.rounds, + s.checkout_percentage, + s.checkout_completed, + s.checkout_attempts, + s.highest_checkout, + s.darts_thrown, + s.checkout_9_darts, + s.checkout_8_darts, + s.checkout_7_darts, + s.checkout_6_darts, + s.checkout_5_darts, + s.checkout_4_darts, + s.checkout_3_darts + FROM statistics_170 s JOIN player p ON p.id = s.player_id JOIN leg l ON l.id = s.leg_id JOIN player2leg p2l on l.id = p2l.leg_id AND p.id = p2l.player_id @@ -75,30 +106,52 @@ func Get170StatisticsForLeg(id int) ([]*models.StatisticsScam, error) { } defer rows.Close() - stats := make([]*models.StatisticsScam, 0) + stats := make([]*models.Statistics170, 0) for rows.Next() { - s := new(models.StatisticsScam) - err := rows.Scan(&s.LegID, &s.PlayerID, &s.DartsThrownScorer, &s.DartsThrownStopper, &s.Score, &s.MPR, &s.PPD, &s.ThreeDartAvg) + s := new(models.Statistics170) + var darts9, darts8, darts7, darts6, darts5, darts4, darts3 int + + checkoutDarts := make(map[int]int, 0) + err := rows.Scan(&s.LegID, &s.PlayerID, &s.Points, &s.PPD, &s.ThreeDartAvg, &s.Rounds, &s.CheckoutPercentage, &s.CheckoutCompleted, + &s.CheckoutAttempts, &s.HighestCheckout, &s.DartsThrown, &darts9, &darts8, &darts7, &darts6, &darts5, &darts4, &darts3) if err != nil { return nil, err } + checkoutDarts[9] = darts9 + checkoutDarts[8] = darts8 + checkoutDarts[7] = darts7 + checkoutDarts[6] = darts6 + checkoutDarts[5] = darts5 + checkoutDarts[4] = darts4 + checkoutDarts[3] = darts3 + s.CheckoutDarts = checkoutDarts stats = append(stats, s) } return stats, nil } // Get170StatisticsForMatch will return statistics for all players in the given match -func Get170StatisticsForMatch(id int) ([]*models.StatisticsScam, error) { +func Get170StatisticsForMatch(id int) ([]*models.Statistics170, error) { rows, err := models.DB.Query(` SELECT p.id, - SUM(s.darts_thrown_scorer) as 'darts_thrown_scorer', - SUM(s.darts_thrown_stopper) as 'darts_thrown_stopper', - CAST(SUM(s.score) / COUNT(DISTINCT l.id) AS SIGNED) as 'avg_score', - SUM(s.score) / SUM(s.darts_thrown_scorer) as 'ppd', - SUM(s.score) / SUM(s.darts_thrown_scorer) * 3 as 'three_dart_avg', - (20 * COUNT(DISTINCT l.id)) / SUM(darts_thrown_stopper) * 3 as 'mpr' - FROM statistics_scam s + SUM(s.points), + IF(s.darts_thrown = 0, 0, SUM(s.ppd_score) / SUM(s.darts_thrown)), + IF(s.darts_thrown = 0, 0, SUM(s.ppd_score) / SUM(s.darts_thrown) * 3), + SUM(s.rounds), + SUM(s.checkout_completed) / SUM(s.checkout_attempts) * 100, + SUM(s.checkout_completed), + SUM(s.checkout_attempts), + MAX(s.highest_checkout), + SUM(s.darts_thrown), + SUM(s.checkout_9_darts), + SUM(s.checkout_8_darts), + SUM(s.checkout_7_darts), + SUM(s.checkout_6_darts), + SUM(s.checkout_5_darts), + SUM(s.checkout_4_darts), + SUM(s.checkout_3_darts) + FROM statistics_170 s JOIN player p ON p.id = s.player_id JOIN leg l ON l.id = s.leg_id JOIN matches m ON m.id = l.match_id @@ -111,35 +164,52 @@ func Get170StatisticsForMatch(id int) ([]*models.StatisticsScam, error) { } defer rows.Close() - stats := make([]*models.StatisticsScam, 0) + stats := make([]*models.Statistics170, 0) for rows.Next() { - s := new(models.StatisticsScam) - err := rows.Scan(&s.PlayerID, &s.DartsThrownScorer, &s.DartsThrownStopper, &s.Score, &s.PPD, &s.ThreeDartAvg, &s.MPR) + s := new(models.Statistics170) + var darts9, darts8, darts7, darts6, darts5, darts4, darts3 int + + checkoutDarts := make(map[int]int, 0) + err := rows.Scan(&s.PlayerID, &s.Points, &s.PPD, &s.ThreeDartAvg, &s.Rounds, &s.CheckoutPercentage, &s.CheckoutCompleted, + &s.CheckoutAttempts, &s.HighestCheckout, &s.DartsThrown, &darts9, &darts8, &darts7, &darts6, &darts5, &darts4, &darts3) if err != nil { return nil, err } + checkoutDarts[9] = darts9 + checkoutDarts[8] = darts8 + checkoutDarts[7] = darts7 + checkoutDarts[6] = darts6 + checkoutDarts[5] = darts5 + checkoutDarts[4] = darts4 + checkoutDarts[3] = darts3 + s.CheckoutDarts = checkoutDarts stats = append(stats, s) } return stats, nil } // Get170StatisticsForPlayer will return Scam statistics for the given player -func Get170StatisticsForPlayer(id int) (*models.StatisticsScam, error) { - s := new(models.StatisticsScam) +func Get170StatisticsForPlayer(id int) (*models.Statistics170, error) { + s := new(models.Statistics170) + var darts9, darts8, darts7, darts6, darts5, darts4, darts3 int err := models.DB.QueryRow(` SELECT p.id, - COUNT(DISTINCT m.id) AS 'matches_played', - COUNT(DISTINCT m2.id) AS 'matches_won', - COUNT(DISTINCT l.id) AS 'legs_played', - COUNT(DISTINCT l2.id) AS 'legs_won', - SUM(s.darts_thrown_scorer) as 'darts_thrown_scorer', - SUM(s.darts_thrown_stopper) as 'darts_thrown_stopper', - CAST(SUM(s.score) / COUNT(DISTINCT l.id) AS SIGNED) as 'avg_score', - SUM(darts_thrown_stopper) / 20 * COUNT(DISTINCT l.id) * 3 as 'mpr', - SUM(s.score) / SUM(s.darts_thrown_scorer) as 'ppd', - SUM(s.score) / SUM(s.darts_thrown_scorer) * 3 as 'three_dart_avg' - FROM statistics_scam s + SUM(s.points), + SUM(s.ppd_score) / SUM(s.darts_thrown), + SUM(s.rounds), + SUM(s.checkout_attempts) / COUNT(DISTINCT l2.id), + SUM(s.checkout_attempts), + MAX(s.highest_checkout), + SUM(s.darts_thrown), + SUM(s.checkout_9_darts), + SUM(s.checkout_8_darts), + SUM(s.checkout_7_darts), + SUM(s.checkout_6_darts), + SUM(s.checkout_5_darts), + SUM(s.checkout_4_darts), + SUM(s.checkout_3_darts) + FROM statistics_170 s JOIN player p ON p.id = s.player_id JOIN leg l ON l.id = s.leg_id JOIN matches m ON m.id = l.match_id @@ -147,15 +217,24 @@ func Get170StatisticsForPlayer(id int) (*models.StatisticsScam, error) { LEFT JOIN matches m2 ON m2.id = l.match_id AND m2.winner_id = p.id WHERE s.player_id = ? AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 - AND m.match_type_id = 16 - GROUP BY p.id`, id).Scan(&s.PlayerID, &s.MatchesPlayed, &s.MatchesWon, &s.LegsPlayed, &s.LegsWon, &s.DartsThrownScorer, &s.DartsThrownStopper, - &s.Score, &s.MPR, &s.PPD, &s.ThreeDartAvg) + AND m.match_type_id = 17 + GROUP BY p.id`, id).Scan(&s.LegID, &s.PlayerID, &s.Points, &s.PPD, &s.Rounds, &s.CheckoutPercentage, &s.CheckoutAttempts, + &s.HighestCheckout, &s.DartsThrown, &darts9, &darts8, &darts7, &darts6, &darts5, &darts4, &darts3) if err != nil { if err == sql.ErrNoRows { - return new(models.StatisticsScam), nil + return new(models.Statistics170), nil } return nil, err } + checkoutDarts := make(map[int]int, 0) + checkoutDarts[9] = darts9 + checkoutDarts[8] = darts8 + checkoutDarts[7] = darts7 + checkoutDarts[6] = darts6 + checkoutDarts[5] = darts5 + checkoutDarts[4] = darts4 + checkoutDarts[3] = darts3 + s.CheckoutDarts = checkoutDarts return s, nil } @@ -261,13 +340,8 @@ func Calculate170Statistics(legID int) (map[int]*models.Statistics170, error) { stats.PPDScore += visit.GetScore() } stats.DartsThrown = visit.DartsThrown + player.DartsThrown += 3 - stats.DartsThrown += 3 - if !visit.IsBust { - player.CurrentScore -= visit.GetScore() - } - - // TODO checkout attempts currentScore := player.CurrentScore if visit.FirstDart.IsCheckoutAttempt(currentScore, 1, models.OUTSHOTDOUBLE) { stats.CheckoutAttempts++ @@ -281,6 +355,10 @@ func Calculate170Statistics(legID int) (map[int]*models.Statistics170, error) { stats.CheckoutAttempts++ } + if !visit.IsBust { + player.CurrentScore -= visit.GetScore() + } + if player.CurrentScore == 0 && visit.GetLastDart().IsDouble() { stats.CheckoutCompleted++ stats.CheckoutDarts[player.DartsThrown]++ @@ -297,11 +375,16 @@ func Calculate170Statistics(legID int) (map[int]*models.Statistics170, error) { } for _, stats := range statisticsMap { - stats.PPD = float32(stats.PPDScore) / float32(stats.DartsThrown) - if stats.CheckoutAttempts > 0 { - stats.CheckoutPercentage = null.FloatFrom(100 / float64(stats.CheckoutAttempts)) + if stats.PPDScore == 0 { + stats.PPD = 0 + } else { + stats.PPD = float32(stats.PPDScore) / float32(stats.DartsThrown) + } + if stats.CheckoutCompleted > 0 { + stats.CheckoutPercentage = null.FloatFrom(float64(stats.CheckoutCompleted) / float64(stats.CheckoutAttempts) * 100.0) } stats.ThreeDartAvg = stats.PPD * 3 + stats.Rounds -= 1 } return statisticsMap, nil } @@ -310,13 +393,26 @@ func Calculate170Statistics(legID int) (map[int]*models.Statistics170, error) { func ReCalculate170Statistics(legs []int) ([]string, error) { queries := make([]string, 0) for _, legID := range legs { - stats, err := CalculateScamStatistics(legID) + stats, err := Calculate170Statistics(legID) if err != nil { return nil, err } for playerID, stat := range stats { - queries = append(queries, fmt.Sprintf(`UPDATE statistics_170 SET darts_thrown_stopper = %d, darts_thrown_scorer = %d, mpr = %f, score = %d, WHERE leg_id = %d AND player_id = %d;`, - stat.DartsThrownStopper, stat.DartsThrownScorer, stat.MPR, stat.Score, legID, playerID)) + query := fmt.Sprintf(`UPDATE statistics_170 SET points = %d, ppd = %f, ppd_score = %d, rounds = %d, checkout_attempts = %d, + checkout_completed = %d, darts_thrown = %d, checkout_9_darts = %d, checkout_8_darts = %d, + checkout_7_darts = %d, checkout_6_darts = %d, checkout_5_darts = %d, checkout_4_darts = %d, checkout_3_darts = %d`, + stat.Points, stat.PPD, stat.PPDScore, stat.Rounds, stat.CheckoutAttempts, stat.CheckoutCompleted, + stat.DartsThrown, stat.CheckoutDarts[9], stat.CheckoutDarts[8], stat.CheckoutDarts[7], + stat.CheckoutDarts[6], stat.CheckoutDarts[5], stat.CheckoutDarts[4], stat.CheckoutDarts[3]) + + if stat.CheckoutPercentage.Valid { + query += fmt.Sprintf(", checkout_percentage = %f", stat.CheckoutPercentage.Float64) + } + if stat.HighestCheckout.Valid { + query += fmt.Sprintf(", highest_checkout = %d", stat.HighestCheckout.Int64) + } + query += fmt.Sprintf(" WHERE leg_id = %d AND player_id = %d;", legID, playerID) + queries = append(queries, query) } } diff --git a/data/statistics_scam.go b/data/statistics_scam.go index 25fca92f..0fda6c92 100644 --- a/data/statistics_scam.go +++ b/data/statistics_scam.go @@ -63,7 +63,7 @@ func GetScamStatisticsForLeg(id int) ([]*models.StatisticsScam, error) { s.score, s.mpr, s.ppd, - s.ppd / 3 as 'three_dart_avg' + s.ppd * 3 as 'three_dart_avg' FROM statistics_scam s JOIN player p ON p.id = s.player_id JOIN leg l ON l.id = s.leg_id diff --git a/data/statistics_x01.go b/data/statistics_x01.go index ac5089b3..597a94a7 100644 --- a/data/statistics_x01.go +++ b/data/statistics_x01.go @@ -187,12 +187,7 @@ func GetPlayerX01PreviousStatistics(id int) (*models.StatisticsX01, error) { return nil, err } if len(statistics) > 0 { - stats := statistics[0] - if err != nil { - return nil, err - } - - return stats, nil + return statistics[0], nil } return new(models.StatisticsX01), nil } diff --git a/models/visit.go b/models/visit.go index 325621d3..8e004a92 100644 --- a/models/visit.go +++ b/models/visit.go @@ -601,6 +601,28 @@ func (visit *Visit) CalculateScamMarks(scores map[int]*Player2Leg) int { return marks } +func (visit *Visit) Calculate170Score(round int, player *Player2Leg) int { + player.DartsThrown += 3 + score := 0 + if !visit.IsBust { + player.CurrentScore -= visit.GetScore() + score = visit.GetScore() + } + + if player.CurrentScore == 0 && visit.GetLastDart().IsDouble() { + // We hit a checkout, reset + player.CurrentScore = 170 + player.CurrentPoints.Int64++ + player.DartsThrown = 0 + } else if round != 1 && player.DartsThrown%9 == 0 { + // 9 Darts have been thrown, reset + player.CurrentScore = 170 + player.DartsThrown = 0 + score = 0 + } + return score +} + // IsShanghai will check if the given visit is a "Shanghai". A Shanghai visit is one where a single, double and triple multipler is hit with each dart func (visit *Visit) IsShanghai() bool { first := visit.FirstDart From 759ba1e8a24ca4c59bb585911706a74b8b3fab82 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Mon, 18 Mar 2024 20:53:38 +0100 Subject: [PATCH 57/67] Changed Around The World and Shanghai to use multiplier as points --- CHANGELOG.md | 1 + cmd/fourtwenty.go | 2 +- cmd/onesecenety.go | 23 +++++++++++++++++++++++ controllers/match_controller.go | 6 +++++- data/match.go | 6 +++--- data/recalculate.go | 2 ++ data/statistics_around_the_world.go | 3 ++- models/visit.go | 6 +++--- 8 files changed, 40 insertions(+), 9 deletions(-) create mode 100644 cmd/onesecenety.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fbcd834..cd44f7bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ #### Changed - Sort tournament best statistics by leg id for equal values - Use tournament elo if available for odds +- Switched `Around the World` and `Shanghai` to count multiplers as points instead of value #### Fixed - Return `outshot_type` for `X01 Handicap` diff --git a/cmd/fourtwenty.go b/cmd/fourtwenty.go index 74889d7d..344b4c11 100644 --- a/cmd/fourtwenty.go +++ b/cmd/fourtwenty.go @@ -8,7 +8,7 @@ import ( // fourtwentyCmd represents the fourtwenty command var fourtwentyCmd = &cobra.Command{ - Use: "fourtwenty", + Use: "420", Short: "Recalculate 420 statistics", Run: func(cmd *cobra.Command, args []string) { err := data.RecalculateStatistics(models.FOURTWENTY, legID, since, dryRun) diff --git a/cmd/onesecenety.go b/cmd/onesecenety.go new file mode 100644 index 00000000..f9f93903 --- /dev/null +++ b/cmd/onesecenety.go @@ -0,0 +1,23 @@ +package cmd + +import ( + "github.com/kcapp/api/data" + "github.com/kcapp/api/models" + "github.com/spf13/cobra" +) + +// oneseventyCmd represents the oneseventy command +var oneseventyCmd = &cobra.Command{ + Use: "170", + Short: "Recalculate 170 statistics", + Run: func(cmd *cobra.Command, args []string) { + err := data.RecalculateStatistics(models.ONESEVENTY, legID, since, dryRun) + if err != nil { + panic(err) + } + }, +} + +func init() { + recalculateStatisticsCmd.AddCommand(oneseventyCmd) +} diff --git a/controllers/match_controller.go b/controllers/match_controller.go index 3908acf0..eb7759cb 100644 --- a/controllers/match_controller.go +++ b/controllers/match_controller.go @@ -98,7 +98,11 @@ func GetMatches(w http.ResponseWriter, r *http.Request) { // GetActiveMatches will return a list of active matches func GetActiveMatches(w http.ResponseWriter, r *http.Request) { SetHeaders(w) - matches, err := data.GetActiveMatches() + since, err := strconv.Atoi(r.URL.Query().Get("since")) + if err != nil { + since = 2 + } + matches, err := data.GetActiveMatches(since) if err != nil { log.Println("Unable to get active matches", err) http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/data/match.go b/data/match.go index 624d849f..0086d1c4 100644 --- a/data/match.go +++ b/data/match.go @@ -181,7 +181,7 @@ func GetMatchesCount() (int, error) { } // GetActiveMatches returns all active matches -func GetActiveMatches() ([]*models.Match, error) { +func GetActiveMatches(since int) ([]*models.Match, error) { rows, err := models.DB.Query(` SELECT m.id, m.is_finished, m.is_abandoned, m.is_walkover, m.is_bye, m.current_leg_id, m.winner_id, m.office_id, m.is_practice, @@ -196,10 +196,10 @@ func GetActiveMatches() ([]*models.Match, error) { LEFT JOIN venue v on v.id = m.venue_id LEFT JOIN player2leg p2l ON p2l.match_id = m.id WHERE m.is_finished = 0 - AND l.updated_at > NOW() - INTERVAL 2 MINUTE + AND l.updated_at > NOW() - INTERVAL ? MINUTE AND m.is_bye <> 1 GROUP BY m.id - ORDER BY m.id DESC`) + ORDER BY m.id DESC`, since) if err != nil { return nil, err } diff --git a/data/recalculate.go b/data/recalculate.go index 1423ea62..ac47f5e9 100644 --- a/data/recalculate.go +++ b/data/recalculate.go @@ -67,6 +67,8 @@ func RecalculateStatistics(matchType int, legID int, since string, dryRun bool) queries, err = RecalculateKnockoutStatistics(legs) case models.SCAM: queries, err = ReCalculateScamStatistics(legs) + case models.ONESEVENTY: + queries, err = ReCalculate170Statistics(legs) default: return fmt.Errorf("cannot recalculate statistics for type %d", matchType) } diff --git a/data/statistics_around_the_world.go b/data/statistics_around_the_world.go index 5284a8c4..d3869698 100644 --- a/data/statistics_around_the_world.go +++ b/data/statistics_around_the_world.go @@ -493,6 +493,7 @@ func GetShanghaiStatisticsForMatch(id int) ([]*models.StatisticsAroundThe, error CAST(SUM(s.score) / COUNT(DISTINCT l.id) AS SIGNED) as 'avg_score', SUM(s.mpr) / COUNT(DISTINCT l.id) as 'mpr', SUM(s.total_hit_rate) / COUNT(l.id) as 'total_hit_rate', + MAX(shanghai) as 'shanghai', IFNULL(SUM(s.hit_rate_1) / SUM(IF(shanghai < 1, 0, 1)), 0) as 'hit_rate_1', IFNULL(SUM(s.hit_rate_2) / SUM(IF(shanghai < 2, 0, 1)), 0) as 'hit_rate_2', IFNULL(SUM(s.hit_rate_3) / SUM(IF(shanghai < 3, 0, 1)), 0) as 'hit_rate_3', @@ -530,7 +531,7 @@ func GetShanghaiStatisticsForMatch(id int) ([]*models.StatisticsAroundThe, error for rows.Next() { s := new(models.StatisticsAroundThe) h := make([]*null.Float, 21) - err := rows.Scan(&s.PlayerID, &s.DartsThrown, &s.Score, &s.MPR, &s.TotalHitRate, + err := rows.Scan(&s.PlayerID, &s.DartsThrown, &s.Score, &s.MPR, &s.TotalHitRate, &s.Shanghai, &h[1], &h[2], &h[3], &h[4], &h[5], &h[6], &h[7], &h[8], &h[9], &h[10], &h[11], &h[12], &h[13], &h[14], &h[15], &h[16], &h[17], &h[18], &h[19], &h[20]) if err != nil { diff --git a/models/visit.go b/models/visit.go index 8e004a92..1b48e02c 100644 --- a/models/visit.go +++ b/models/visit.go @@ -441,13 +441,13 @@ func (visit *Visit) CalculateAroundTheClockScore(currentScore int) int { func (visit *Visit) CalculateAroundTheWorldScore(round int) int { score := 0 if round == visit.FirstDart.ValueRaw() || (round == 21 && visit.FirstDart.IsBull()) { - score += visit.FirstDart.GetScore() + score += int(visit.FirstDart.Multiplier) } if round == visit.SecondDart.ValueRaw() || (round == 21 && visit.SecondDart.IsBull()) { - score += visit.SecondDart.GetScore() + score += int(visit.SecondDart.Multiplier) } if round == visit.ThirdDart.ValueRaw() || (round == 21 && visit.ThirdDart.IsBull()) { - score += visit.ThirdDart.GetScore() + score += int(visit.ThirdDart.Multiplier) } return score } From 409e4f8e993793595ee878430e33a688898f42a3 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Thu, 28 Mar 2024 17:32:41 +0100 Subject: [PATCH 58/67] Count badges for all leg types --- CHANGELOG.md | 1 + data/badge.go | 5 +- data/leg.go | 2 +- data/statistics_x01.go | 2 - models/badge.go | 119 +++++++++++++++++++++++++++++++++++++---- models/leg.go | 20 +++++++ models/match.go | 5 ++ models/visit.go | 3 ++ 8 files changed, 143 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd44f7bc..d8b97c7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - New game type `170` - Endpoint for getting badge statistics - Added Darts Per Leg `DPL` metric to tournament +- Count badges for all leg types - Lots of new badges #### Changed diff --git a/data/badge.go b/data/badge.go index 582f3c20..fea34d13 100644 --- a/data/badge.go +++ b/data/badge.go @@ -17,6 +17,8 @@ func GetBadges() ([]*models.Badge, error) { b.name, b.description, b.filename, + b.secret, + b.hidden, b.levels FROM badge b`) if err != nil { @@ -27,7 +29,7 @@ func GetBadges() ([]*models.Badge, error) { badges := make([]*models.Badge, 0) for rows.Next() { badge := new(models.Badge) - err := rows.Scan(&badge.ID, &badge.Name, &badge.Description, &badge.Filename, &badge.Levels) + err := rows.Scan(&badge.ID, &badge.Name, &badge.Description, &badge.Filename, &badge.Secret, &badge.Hidden, &badge.Levels) if err != nil { return nil, err } @@ -239,6 +241,7 @@ func CheckLegForBadges(leg *models.Leg, statistics map[int]*models.PlayerBadgeSt } } } + for _, badge := range models.VisitBadges { for _, playerID := range leg.Players { valid, visitID := badge.Validate(playerID, leg.Visits) diff --git a/data/leg.go b/data/leg.go index ae380dd6..368d07ab 100644 --- a/data/leg.go +++ b/data/leg.go @@ -949,7 +949,7 @@ func GetBadgeLegsToRecalculate() ([]int, error) { SELECT l.id FROM leg l JOIN matches m on m.id = l.match_id - WHERE l.has_scores = 1 AND COALESCE(l.leg_type_id, m.match_type_id) = 1 -- X01 + WHERE l.has_scores = 1 AND m.is_abandoned = 0 AND m.is_bye = 0 AND m.is_walkover = 0 AND l.is_finished = 1 GROUP BY l.id diff --git a/data/statistics_x01.go b/data/statistics_x01.go index b65d9386..7246f910 100644 --- a/data/statistics_x01.go +++ b/data/statistics_x01.go @@ -933,7 +933,6 @@ func GetPlayerBadgeStatistics(ids []int, legID *int) (map[int]*models.PlayerBadg LEFT JOIN matches m ON l.match_id = m.id WHERE player_id IN (?) AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 - AND COALESCE(l.leg_type_id, m.match_type_id) = 1 -- X01 AND m.is_bye = 0 AND m.is_walkover = 0 AND leg_id <= COALESCE(?, ~0) -- BIGINT hack GROUP BY player_id ORDER BY player_id DESC`, ids, legID) @@ -979,7 +978,6 @@ func GetPlayerBadgeStatistics(ids []int, legID *int) (map[int]*models.PlayerBadg or (first_dart_multiplier = 2 and second_dart_multiplier = 1 and third_dart_multiplier = 3)) AND is_bust = 0 AND s.player_id IN (?) AND leg_id <= COALESCE(?, ~0) -- BIGINT hack AND l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 - AND COALESCE(l.leg_type_id, m.match_type_id) = 1 -- X01 AND m.is_bye = 0 AND m.is_walkover = 0 GROUP BY first_dart, player_id`, ids, legID) if err != nil { diff --git a/models/badge.go b/models/badge.go index 9807f861..e45b0c7d 100644 --- a/models/badge.go +++ b/models/badge.go @@ -2,6 +2,7 @@ package models import ( "encoding/json" + "log" "sort" "time" @@ -216,6 +217,9 @@ func (b BadgeJustAQuickie) GetID() int { return b.ID } func (b BadgeJustAQuickie) Validate(match *Match) (bool, []int) { + if !match.IsX01() { + return false, nil + } if len(match.Legs) == 3 && len(match.Players) > 1 { first := match.Legs[0] second := match.Legs[1] @@ -226,6 +230,7 @@ func (b BadgeJustAQuickie) Validate(match *Match) (bool, []int) { return true, []int{int(match.WinnerID.Int64)} } } + return false, nil } @@ -233,6 +238,10 @@ func (b BadgeAroundTheWorld) GetID() int { return b.ID } func (b BadgeAroundTheWorld) Validate(match *Match) (bool, []int) { + if !match.IsX01() { + return false, nil + } + playerHits := make(map[int][]int) for playerID := range match.Players { playerHits[playerID] = make([]int, 0) @@ -308,6 +317,7 @@ var LegBadges = []LegBadge{ BadgeChampagneShot{ID: 42}, BadgeYin{ID: 44}, BadgeYang{ID: 45}, + BadgeZebra{ID: 47}, } type LegBadge interface { @@ -335,11 +345,15 @@ type BadgePerfection struct{ ID int } type BadgeChampagneShot struct{ ID int } type BadgeYin struct{ ID int } type BadgeYang struct{ ID int } +type BadgeZebra struct{ ID int } func (b BadgeDoubleDouble) GetID() int { return b.ID } func (b BadgeDoubleDouble) Validate(leg *Leg) (bool, *int, *int) { + if !leg.IsX01() { + return false, nil, nil + } visit := leg.GetLastVisit() doubles := 0 if visit.ThirdDart.IsDouble() { @@ -358,6 +372,9 @@ func (b BadgeTripleDouble) GetID() int { return b.ID } func (b BadgeTripleDouble) Validate(leg *Leg) (bool, *int, *int) { + if !leg.IsX01() { + return false, nil, nil + } visit := leg.GetLastVisit() return visit.FirstDart.IsDouble() && visit.SecondDart.IsDouble() && visit.ThirdDart.IsDouble(), &visit.PlayerID, &visit.ID } @@ -366,6 +383,9 @@ func (b BadgeMadHouse) GetID() int { return b.ID } func (b BadgeMadHouse) Validate(leg *Leg) (bool, *int, *int) { + if !leg.IsX01() { + return false, nil, nil + } visit := leg.GetLastVisit() last := visit.GetLastDart() return last.IsDouble() && last.ValueRaw() == 1, &visit.PlayerID, &visit.ID @@ -408,6 +428,9 @@ func (b BadgeBullseye) GetID() int { return b.ID } func (b BadgeBullseye) Validate(leg *Leg) (bool, *int, *int) { + if !leg.IsX01() { + return false, nil, nil + } visit := leg.GetLastVisit() last := visit.GetLastDart() return last.ValueRaw() == BULLSEYE && last.Multiplier == DOUBLE, &visit.PlayerID, &visit.ID @@ -417,6 +440,9 @@ func (b BadgeEasyAs123) GetID() int { return b.ID } func (b BadgeEasyAs123) Validate(leg *Leg) (bool, *int, *int) { + if !leg.IsX01() { + return false, nil, nil + } visit := leg.GetLastVisit() last := visit.GetLastDart() return visit.GetScore() == 123 && last.IsDouble(), &visit.PlayerID, &visit.ID @@ -426,6 +452,9 @@ func (b BadgeCloseToPerfect) GetID() int { return b.ID } func (b BadgeCloseToPerfect) Validate(leg *Leg) (bool, *int, *int) { + if !leg.IsX01() { + return false, nil, nil + } visit := leg.GetLastVisit() return leg.StartingScore == 501 && visit.DartsThrown < 15 && visit.DartsThrown > 9, &visit.PlayerID, nil } @@ -445,6 +474,9 @@ func (b BadgeShanghaiCheckout) GetID() int { return b.ID } func (b BadgeShanghaiCheckout) Validate(leg *Leg) (bool, *int, *int) { + if !leg.IsX01() { + return false, nil, nil + } visit := leg.GetLastVisit() return visit.IsShanghai(), &visit.PlayerID, &visit.ID } @@ -453,8 +485,11 @@ func (b BadgeTripleTrouble) GetID() int { return b.ID } func (b BadgeTripleTrouble) Validate(leg *Leg) (bool, *int, *int) { - visit := leg.GetLastVisit() + if !leg.IsX01() { + return false, nil, nil + } + visit := leg.GetLastVisit() if visit.FirstDart.IsDouble() && visit.SecondDart.IsDouble() && visit.ThirdDart.IsDouble() && visit.FirstDart.ValueRaw() == visit.SecondDart.ValueRaw() && visit.FirstDart.ValueRaw() == visit.ThirdDart.ValueRaw() { return true, &visit.PlayerID, &visit.ID @@ -467,6 +502,10 @@ func (b BadgePerfection) GetID() int { return b.ID } func (b BadgePerfection) Validate(leg *Leg) (bool, *int, *int) { + if !leg.IsX01() { + return false, nil, nil + } + visit := leg.GetLastVisit() if leg.StartingScore == 501 && visit.DartsThrown == 9 { return true, &visit.PlayerID, nil @@ -478,6 +517,10 @@ func (b BadgeChampagneShot) GetID() int { return b.ID } func (b BadgeChampagneShot) Validate(leg *Leg) (bool, *int, *int) { + if !leg.IsX01() { + return false, nil, nil + } + visit := leg.GetLastVisit() if visit.FirstDart.IsBull() && visit.FirstDart.IsDouble() && @@ -493,7 +536,9 @@ func (b BadgeYin) GetID() int { return b.ID } func (b BadgeYin) Validate(leg *Leg) (bool, *int, *int) { - black := []int{0, 20, 18, 13, 10, 2, 3, 7, 8, 14, 12} + if !leg.IsX01() { + return false, nil, nil + } completed := true winner := leg.GetLastVisit().PlayerID @@ -501,9 +546,9 @@ func (b BadgeYin) Validate(leg *Leg) (bool, *int, *int) { if visit.PlayerID != winner { continue } - if !containsInt(black, visit.FirstDart.ValueRaw()) || - !containsInt(black, visit.SecondDart.ValueRaw()) || - !containsInt(black, visit.ThirdDart.ValueRaw()) { + if !containsInt(NUMS_BLACK, visit.FirstDart.ValueRaw()) || + !containsInt(NUMS_BLACK, visit.SecondDart.ValueRaw()) || + !containsInt(NUMS_BLACK, visit.ThirdDart.ValueRaw()) { completed = false break @@ -519,16 +564,19 @@ func (b BadgeYang) GetID() int { return b.ID } func (b BadgeYang) Validate(leg *Leg) (bool, *int, *int) { - white := []int{0, 1, 4, 6, 15, 17, 19, 16, 11, 9, 5} + if !leg.IsX01() { + return false, nil, nil + } + completed := true winner := leg.GetLastVisit().PlayerID for _, visit := range leg.Visits { if visit.PlayerID != winner { continue } - if !containsInt(white, visit.FirstDart.ValueRaw()) || - !containsInt(white, visit.SecondDart.ValueRaw()) || - !containsInt(white, visit.ThirdDart.ValueRaw()) { + if !containsInt(NUMS_WHITE, visit.FirstDart.ValueRaw()) || + !containsInt(NUMS_WHITE, visit.SecondDart.ValueRaw()) || + !containsInt(NUMS_WHITE, visit.ThirdDart.ValueRaw()) { completed = false break @@ -540,6 +588,57 @@ func (b BadgeYang) Validate(leg *Leg) (bool, *int, *int) { return false, nil, nil } +func (b BadgeZebra) GetID() int { + return b.ID +} +func (b BadgeZebra) Validate(leg *Leg) (bool, *int, *int) { + if !leg.IsX01() { + return false, nil, nil + } + winner := leg.GetLastVisit().PlayerID + + COLORS := map[int][]int{ + 0: NUMS_BLACK, + 1: NUMS_WHITE, + } + completed := true + + first := leg.GetFirstHitDart(winner) + var color int + if containsInt(COLORS[0], first.ValueRaw()) { + color = 0 + } else { + color = 1 + } + for _, visit := range leg.Visits { + if visit.PlayerID != winner { + continue + } + + for _, dart := range visit.GetDarts() { + if dart.IsMiss() { + log.Printf("Skipping Missed dart") + continue + } + colors := COLORS[color%2] + log.Printf("Checking if %d is %d in %v", dart.ValueRaw(), color, colors) + if !containsInt(colors, dart.ValueRaw()) { + completed = false + break + } + color++ + } + + if !completed { + break + } + } + if completed { + return true, &winner, nil + } + return false, nil, nil +} + var LegPlayerBadges = []LegPlayerBadge{ BadgeImpersonator{ID: 21}, BadgeBotBeaterEasy{ID: 22}, @@ -838,7 +937,7 @@ func (b BadgeSoClose) Validate(playerID int, visits []*Visit) (bool, *int) { func GetLevel(value int, levels []int) int { level := 1 for i, treshold := range levels { - if value > treshold { + if value >= treshold { level = i + 1 } } diff --git a/models/leg.go b/models/leg.go index a175527d..b0a2c393 100644 --- a/models/leg.go +++ b/models/leg.go @@ -411,3 +411,23 @@ func DecorateVisitsScam(players map[int]*Player2Leg, visits []*Visit) { func (leg Leg) GetLastVisit() *Visit { return leg.Visits[len(leg.Visits)-1] } + +// IsX01 returns true if this leg is a X01 leg +func (leg Leg) IsX01() bool { + return leg.LegType.ID == X01 +} + +// GetFirstHitDart will return the first (non-Miss) dart for the given player +func (leg Leg) GetFirstHitDart(playerID int) *Dart { + for _, visit := range leg.Visits { + if visit.PlayerID != playerID { + continue + } + for _, dart := range visit.GetDarts() { + if !dart.IsMiss() { + return &dart + } + } + } + return nil +} diff --git a/models/match.go b/models/match.go index 3963c7ef..1cb2219e 100644 --- a/models/match.go +++ b/models/match.go @@ -329,3 +329,8 @@ type MatchConfigError struct { func (e *MatchConfigError) Error() string { return e.Err.Error() } + +// IsX01 returns true if this match is a X01 match +func (m Match) IsX01() bool { + return m.MatchType.ID == X01 +} diff --git a/models/visit.go b/models/visit.go index 1b48e02c..acb81cbb 100644 --- a/models/visit.go +++ b/models/visit.go @@ -9,6 +9,9 @@ import ( "github.com/guregu/null" ) +var NUMS_WHITE = []int{0, 1, 4, 6, 15, 17, 19, 16, 11, 9, 5} +var NUMS_BLACK = []int{0, 20, 18, 13, 10, 2, 3, 7, 8, 14, 12} + // Visit struct used for storing legs type Visit struct { ID int `json:"id"` From 2fb2d5167f2115a878ca14a3aa6289b851380bec Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Thu, 28 Mar 2024 17:34:28 +0100 Subject: [PATCH 59/67] Removed debug logging from Zebra badge --- models/badge.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/models/badge.go b/models/badge.go index e45b0c7d..a1a57573 100644 --- a/models/badge.go +++ b/models/badge.go @@ -2,7 +2,6 @@ package models import ( "encoding/json" - "log" "sort" "time" @@ -617,11 +616,9 @@ func (b BadgeZebra) Validate(leg *Leg) (bool, *int, *int) { for _, dart := range visit.GetDarts() { if dart.IsMiss() { - log.Printf("Skipping Missed dart") continue } colors := COLORS[color%2] - log.Printf("Checking if %d is %d in %v", dart.ValueRaw(), color, colors) if !containsInt(colors, dart.ValueRaw()) { completed = false break From f5fcd6d1d3cde2b127a718ec2b3d8458e7b3d807 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Thu, 16 May 2024 21:32:32 +0200 Subject: [PATCH 60/67] Correctly calculate match badges on match finish --- CHANGELOG.md | 1 + data/leg.go | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8b97c7c..dc8b0b7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ #### Fixed - Return `outshot_type` for `X01 Handicap` +- Correctly calculate match badges on match finish ## [2.7.0] - 2023-09-12 #### Feature diff --git a/data/leg.go b/data/leg.go index 368d07ab..08c1407b 100644 --- a/data/leg.go +++ b/data/leg.go @@ -535,6 +535,7 @@ func FinishLeg(visit models.Visit) error { log.Printf("Added owes of %s from player %d to player %d", match.OweType.Item.String, playerID, visit.PlayerID) } } + match.WinnerID = winnerID log.Printf("Match %d finished with player %d winning", match.ID, winnerID.ValueOrZero()) } else if match.MatchMode.LegsRequired.Valid && playedLegs == int(match.MatchMode.LegsRequired.Int64) { // Match finished, draw From 7c7845ee94f5c997a350a86a42459949509307ea Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Thu, 13 Jun 2024 10:53:36 +0200 Subject: [PATCH 61/67] Added WLED support to venues --- CHANGELOG.md | 1 + data/venue.go | 26 +++++++++++++++++--------- models/venue.go | 1 + 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc8b0b7b..9580f969 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Endpoint for getting badge statistics - Added Darts Per Leg `DPL` metric to tournament - Count badges for all leg types +- WLED support for venues - Lots of new badges #### Changed diff --git a/data/venue.go b/data/venue.go index 748e7995..de163c8c 100644 --- a/data/venue.go +++ b/data/venue.go @@ -26,9 +26,9 @@ func AddVenue(venue models.Venue) error { return err } - _, err = tx.Exec(`INSERT INTO venue_configuration (venue_id, has_dual_monitor, has_led_lights, has_smartboard, smartboard_uuid, smartboard_button_number) - VALUES (?, ?, ?, ?, ?, ?)`, venueID, venue.Config.HasDualMonitor, venue.Config.HasLEDLights, venue.Config.HasSmartboard, - venue.Config.SmartboardUUID, venue.Config.SmartboardButtonNumber) + _, err = tx.Exec(`INSERT INTO venue_configuration (venue_id, has_dual_monitor, has_led_lights, has_wled_lights, has_smartboard, \ + smartboard_uuid, smartboard_button_number) VALUES (?, ?, ?, ?, ?, ?)`, venueID, venue.Config.HasDualMonitor, venue.Config.HasLEDLights, + venue.Config.HasWLEDLights, venue.Config.HasSmartboard, venue.Config.SmartboardUUID, venue.Config.SmartboardButtonNumber) if err != nil { tx.Rollback() return err @@ -51,8 +51,9 @@ func UpdateVenue(venueID int, venue models.Venue) error { tx.Rollback() return err } - _, err = tx.Exec(`UPDATE venue_configuration SET has_dual_monitor = ?, has_led_lights = ?, has_smartboard = ?, smartboard_uuid = ?, smartboard_button_number = ? WHERE venue_id = ?`, - venue.Config.HasDualMonitor, venue.Config.HasLEDLights, venue.Config.HasSmartboard, + _, err = tx.Exec(`UPDATE venue_configuration SET has_dual_monitor = ?, has_led_lights = ?, has_wled_lights = ?, + has_smartboard = ?, smartboard_uuid = ?, smartboard_button_number = ? WHERE venue_id = ?`, + venue.Config.HasDualMonitor, venue.Config.HasLEDLights, &venue.Config.HasWLEDLights, venue.Config.HasSmartboard, venue.Config.SmartboardUUID, venue.Config.SmartboardButtonNumber, venueID) if err != nil { tx.Rollback() @@ -85,14 +86,17 @@ func GetVenues() ([]*models.Venue, error) { return nil, err } - rows, err = models.DB.Query("SELECT venue_id, has_dual_monitor, has_led_lights, has_smartboard, smartboard_uuid, smartboard_button_number FROM venue_configuration") + rows, err = models.DB.Query(` + SELECT venue_id, has_dual_monitor, has_led_lights, has_wled_lights, has_smartboard, smartboard_uuid, smartboard_button_number + FROM venue_configuration`) if err != nil { return nil, err } configs := make(map[int]*models.VenueConfig) for rows.Next() { config := new(models.VenueConfig) - err := rows.Scan(&config.VenueID, &config.HasDualMonitor, &config.HasLEDLights, &config.HasSmartboard, &config.SmartboardUUID, &config.SmartboardButtonNumber) + err := rows.Scan(&config.VenueID, &config.HasDualMonitor, &config.HasLEDLights, &config.HasWLEDLights, &config.HasSmartboard, + &config.SmartboardUUID, &config.SmartboardButtonNumber) if err != nil { return nil, err } @@ -116,6 +120,7 @@ func GetVenue(id int) (*models.Venue, error) { venue.Config, err = GetVenueConfiguration(id) if err != nil { log.Printf("Unable to get venue configuration for %d", id) + return nil, err } return venue, nil } @@ -123,8 +128,11 @@ func GetVenue(id int) (*models.Venue, error) { // GetVenueConfiguration will return the configuration for a venue with the given id func GetVenueConfiguration(id int) (*models.VenueConfig, error) { config := new(models.VenueConfig) - err := models.DB.QueryRow("SELECT venue_id, has_dual_monitor, has_led_lights, has_smartboard, smartboard_uuid, smartboard_button_number FROM venue_configuration WHERE venue_id = ?", - id).Scan(&config.VenueID, &config.HasDualMonitor, &config.HasLEDLights, &config.HasSmartboard, &config.SmartboardUUID, &config.SmartboardButtonNumber) + err := models.DB.QueryRow(` + SELECT venue_id, has_dual_monitor, has_led_lights, has_wled_lights, has_smartboard, smartboard_uuid, smartboard_button_number + FROM venue_configuration WHERE venue_id = ?`, + id).Scan(&config.VenueID, &config.HasDualMonitor, &config.HasLEDLights, &config.HasWLEDLights, &config.HasSmartboard, &config.SmartboardUUID, + &config.SmartboardButtonNumber) if err != nil { return nil, err } diff --git a/models/venue.go b/models/venue.go index c699862c..d6f0317b 100644 --- a/models/venue.go +++ b/models/venue.go @@ -16,6 +16,7 @@ type VenueConfig struct { VenueID int `json:"id"` HasDualMonitor bool `json:"has_dual_monitor"` HasLEDLights bool `json:"has_led_lights"` + HasWLEDLights bool `json:"has_wled_lights"` HasSmartboard bool `json:"has_smartboard"` SmartboardUUID null.String `json:"smartboard_uuid,omitempty"` SmartboardButtonNumber null.Int `json:"smartboard_button_number,omitempty"` From 3c74a5f1b8c3a853810c38695bcbbfe71d36495e Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Fri, 16 Aug 2024 10:02:25 +0200 Subject: [PATCH 62/67] Add option to select TTS voice per venue --- CHANGELOG.md | 1 + data/venue.go | 24 ++++++++++++------------ models/venue.go | 1 + 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9580f969..cc42de95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Added Darts Per Leg `DPL` metric to tournament - Count badges for all leg types - WLED support for venues +- TTS voice selection per venue - Lots of new badges #### Changed diff --git a/data/venue.go b/data/venue.go index de163c8c..8b43de2a 100644 --- a/data/venue.go +++ b/data/venue.go @@ -26,9 +26,9 @@ func AddVenue(venue models.Venue) error { return err } - _, err = tx.Exec(`INSERT INTO venue_configuration (venue_id, has_dual_monitor, has_led_lights, has_wled_lights, has_smartboard, \ + _, err = tx.Exec(`INSERT INTO venue_configuration (venue_id, has_dual_monitor, has_led_lights, has_wled_lights, tts_voice, has_smartboard, smartboard_uuid, smartboard_button_number) VALUES (?, ?, ?, ?, ?, ?)`, venueID, venue.Config.HasDualMonitor, venue.Config.HasLEDLights, - venue.Config.HasWLEDLights, venue.Config.HasSmartboard, venue.Config.SmartboardUUID, venue.Config.SmartboardButtonNumber) + venue.Config.HasWLEDLights, venue.Config.TTSVoice, venue.Config.HasSmartboard, venue.Config.SmartboardUUID, venue.Config.SmartboardButtonNumber) if err != nil { tx.Rollback() return err @@ -52,9 +52,9 @@ func UpdateVenue(venueID int, venue models.Venue) error { return err } _, err = tx.Exec(`UPDATE venue_configuration SET has_dual_monitor = ?, has_led_lights = ?, has_wled_lights = ?, - has_smartboard = ?, smartboard_uuid = ?, smartboard_button_number = ? WHERE venue_id = ?`, - venue.Config.HasDualMonitor, venue.Config.HasLEDLights, &venue.Config.HasWLEDLights, venue.Config.HasSmartboard, - venue.Config.SmartboardUUID, venue.Config.SmartboardButtonNumber, venueID) + tts_voice = ?, has_smartboard = ?, smartboard_uuid = ?, smartboard_button_number = ? WHERE venue_id = ?`, + venue.Config.HasDualMonitor, venue.Config.HasLEDLights, venue.Config.HasWLEDLights, venue.Config.TTSVoice, + venue.Config.HasSmartboard, venue.Config.SmartboardUUID, venue.Config.SmartboardButtonNumber, venueID) if err != nil { tx.Rollback() return err @@ -87,7 +87,7 @@ func GetVenues() ([]*models.Venue, error) { } rows, err = models.DB.Query(` - SELECT venue_id, has_dual_monitor, has_led_lights, has_wled_lights, has_smartboard, smartboard_uuid, smartboard_button_number + SELECT venue_id, has_dual_monitor, has_led_lights, has_wled_lights, tts_voice, has_smartboard, smartboard_uuid, smartboard_button_number FROM venue_configuration`) if err != nil { return nil, err @@ -95,8 +95,8 @@ func GetVenues() ([]*models.Venue, error) { configs := make(map[int]*models.VenueConfig) for rows.Next() { config := new(models.VenueConfig) - err := rows.Scan(&config.VenueID, &config.HasDualMonitor, &config.HasLEDLights, &config.HasWLEDLights, &config.HasSmartboard, - &config.SmartboardUUID, &config.SmartboardButtonNumber) + err := rows.Scan(&config.VenueID, &config.HasDualMonitor, &config.HasLEDLights, &config.HasWLEDLights, &config.TTSVoice, + &config.HasSmartboard, &config.SmartboardUUID, &config.SmartboardButtonNumber) if err != nil { return nil, err } @@ -129,10 +129,10 @@ func GetVenue(id int) (*models.Venue, error) { func GetVenueConfiguration(id int) (*models.VenueConfig, error) { config := new(models.VenueConfig) err := models.DB.QueryRow(` - SELECT venue_id, has_dual_monitor, has_led_lights, has_wled_lights, has_smartboard, smartboard_uuid, smartboard_button_number - FROM venue_configuration WHERE venue_id = ?`, - id).Scan(&config.VenueID, &config.HasDualMonitor, &config.HasLEDLights, &config.HasWLEDLights, &config.HasSmartboard, &config.SmartboardUUID, - &config.SmartboardButtonNumber) + SELECT venue_id, has_dual_monitor, has_led_lights, has_wled_lights, tts_voice, has_smartboard, smartboard_uuid, + smartboard_button_number FROM venue_configuration WHERE venue_id = ?`, + id).Scan(&config.VenueID, &config.HasDualMonitor, &config.HasLEDLights, &config.HasWLEDLights, &config.TTSVoice, + &config.HasSmartboard, &config.SmartboardUUID, &config.SmartboardButtonNumber) if err != nil { return nil, err } diff --git a/models/venue.go b/models/venue.go index d6f0317b..b678d155 100644 --- a/models/venue.go +++ b/models/venue.go @@ -17,6 +17,7 @@ type VenueConfig struct { HasDualMonitor bool `json:"has_dual_monitor"` HasLEDLights bool `json:"has_led_lights"` HasWLEDLights bool `json:"has_wled_lights"` + TTSVoice null.String `json:"tts_voice,omitempty"` HasSmartboard bool `json:"has_smartboard"` SmartboardUUID null.String `json:"smartboard_uuid,omitempty"` SmartboardButtonNumber null.Int `json:"smartboard_button_number,omitempty"` From c62967b381f15bdcb2e86b4b1e04a3d947ca5d9f Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Wed, 13 Nov 2024 12:37:49 +0100 Subject: [PATCH 63/67] Updated players without office when first one is created --- data/office.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/data/office.go b/data/office.go index 22896c6b..41a73f79 100644 --- a/data/office.go +++ b/data/office.go @@ -30,6 +30,13 @@ func AddOffice(office models.Office) error { return err } log.Printf("Created new office (%d) %s", officeID, office.Name) + + // Update any players without office + _, err = tx.Exec("UPDATE player SET office_id = ? WHERE office_id IS NULL", officeID) + if err != nil { + tx.Rollback() + return err + } tx.Commit() return nil } From 0242f5186b51b4824d2e9a2ecfa384e347f57ede Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Wed, 13 Nov 2024 12:37:58 +0100 Subject: [PATCH 64/67] Correctly calculate scores for 170 --- data/leg.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/data/leg.go b/data/leg.go index 08c1407b..b9b8de9a 100644 --- a/data/leg.go +++ b/data/leg.go @@ -1204,13 +1204,16 @@ func GetLeg(id int) (*models.Leg, error) { scores[visit.PlayerID].CurrentScore += score } } else if matchType == models.ONESEVENTY { - player := scores[visit.PlayerID] - score = visit.Calculate170Score(round, player) + // This is done below regardless of if the visit was a bust } else { scores[visit.PlayerID].CurrentScore -= score } visit.Score = score } + if matchType == models.ONESEVENTY { + player := scores[visit.PlayerID] + visit.Score = visit.Calculate170Score(round, player) + } visit.Scores = make(map[int]int) visit.Scores[visit.PlayerID] = scores[visit.PlayerID].CurrentScore From e9f89819b33e2e030c92449185de562c787967b9 Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Wed, 13 Nov 2024 21:42:10 +0100 Subject: [PATCH 65/67] Fixed broken venue create --- data/venue.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/venue.go b/data/venue.go index 8b43de2a..49cb4ca9 100644 --- a/data/venue.go +++ b/data/venue.go @@ -27,7 +27,7 @@ func AddVenue(venue models.Venue) error { } _, err = tx.Exec(`INSERT INTO venue_configuration (venue_id, has_dual_monitor, has_led_lights, has_wled_lights, tts_voice, has_smartboard, - smartboard_uuid, smartboard_button_number) VALUES (?, ?, ?, ?, ?, ?)`, venueID, venue.Config.HasDualMonitor, venue.Config.HasLEDLights, + smartboard_uuid, smartboard_button_number) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, venueID, venue.Config.HasDualMonitor, venue.Config.HasLEDLights, venue.Config.HasWLEDLights, venue.Config.TTSVoice, venue.Config.HasSmartboard, venue.Config.SmartboardUUID, venue.Config.SmartboardButtonNumber) if err != nil { tx.Rollback() From fe7a58dc2357b607f28f293e41167b30f2bc4eac Mon Sep 17 00:00:00 2001 From: Thord Setsaas Date: Mon, 18 Nov 2024 12:30:04 +0100 Subject: [PATCH 66/67] Fixed query for calculating highest checkout --- data/statistics_x01.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/statistics_x01.go b/data/statistics_x01.go index 7246f910..2e1461a7 100644 --- a/data/statistics_x01.go +++ b/data/statistics_x01.go @@ -738,7 +738,7 @@ func getHighestCheckout(ids []int, statisticsMap map[int]*models.StatisticsX01, LEFT JOIN leg l ON l.id = s.leg_id LEFT JOIN leg_parameters lp on l.id = lp.leg_id WHERE s.player_id IN (?) AND l.starting_score IN (?) - AND (lp.outshot_type_id = 2 OR lp.outshot_type_id IS NULL) + AND (lp.outshot_type_id = 1 OR lp.outshot_type_id IS NULL) ) AS max_checkout GROUP BY player_id ) AS max From e0dd71750fe81ccfe3b5bd35571a83fcc6b03693 Mon Sep 17 00:00:00 2001 From: Ove Aursand Date: Tue, 19 Nov 2024 00:04:35 +0100 Subject: [PATCH 67/67] Fix calculation of matches_won --- data/player.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/player.go b/data/player.go index b32ff9e0..681fe4cd 100644 --- a/data/player.go +++ b/data/player.go @@ -691,7 +691,7 @@ func GetMatchesPlayedPerPlayer() (map[int]*models.Player, error) { JOIN leg l ON l.id = s.leg_id JOIN matches m ON m.id = l.match_id LEFT JOIN leg l2 ON l2.id = s.leg_id AND l2.winner_id = p.id - LEFT JOIN matches m2 ON m2.id = l2.match_id AND l2.winner_id = p.id + LEFT JOIN matches m2 ON m2.id = l2.match_id AND m2.winner_id = p.id WHERE l.is_finished = 1 AND m.is_abandoned = 0 AND m.is_walkover = 0 GROUP BY s.player_id`) if err != nil {