diff --git a/cmd/kicker-cli/cmd/event.go b/cmd/kicker-cli/cmd/event.go index 97b32b0..879f5aa 100644 --- a/cmd/kicker-cli/cmd/event.go +++ b/cmd/kicker-cli/cmd/event.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" "path/filepath" + "strings" "github.com/pterm/pterm" "github.com/spf13/cobra" @@ -73,7 +74,7 @@ func nameTypeIncluded(input string) bool { func initEventInfoHeader() [][]string { var table [][]string - header := []string{"ID", "Name", "Date Time", "Points", "Players", "Games", "Name Type", "Mode", "URL"} + header := []string{"ID", "Name", "Date Time", "Level", "Players", "Games", "Name Type", "Mode", "URL"} if !globalNoHeaders { table = append(table, header) } @@ -113,11 +114,21 @@ func showEvent(table *[][]string, e *entity.Event, t *model.Tournament, r *entit } func showInfo(table *[][]string, e *entity.Event, t *model.Tournament, r *entity.Record) { + var levels []string + if len(e.ITSFLevel) > 0 { + levels = append(levels, e.ITSFLevel) + } + if len(e.ATSALevel) > 0 { + levels = append(levels, e.ATSALevel) + } + if len(e.KickerLevel) > 0 { + levels = append(levels, e.KickerLevel) + } *table = append(*table, []string{ e.ID, e.Name, t.Created.Format("2006-01-02 15:04"), - fmt.Sprintf("%d", e.Points), + fmt.Sprintf("%s", strings.Join(levels, "|")), fmt.Sprintf("%d", len(r.Players)), fmt.Sprintf("%d", len(r.AllGames)), t.NameType, diff --git a/cmd/kicker-cli/cmd/event_rank.go b/cmd/kicker-cli/cmd/event_rank.go index b895e9a..26af166 100644 --- a/cmd/kicker-cli/cmd/event_rank.go +++ b/cmd/kicker-cli/cmd/event_rank.go @@ -20,13 +20,13 @@ var ( rankMinPlayed int rankHead int rankTail int - rankOrderBy string + rankSortBy string rankWithGoals bool ) func init() { rankCmd.Flags().StringVarP(&rankGameMode, "mode", "m", "", "rank mode") - rankCmd.Flags().StringVarP(&rankOrderBy, "order-by", "o", "wr", "order by (wr/elo)") + rankCmd.Flags().StringVarP(&rankSortBy, "sort-by", "o", "krs", "sort by (krs/itsf/atsa/elo/wr)") rankCmd.Flags().IntVarP(&rankMinPlayed, "minimum-played", "p", 0, "minimum matches played") rankCmd.Flags().BoolVarP(&rankWithGoals, "with-goals", "", false, "rank with goals") rankCmd.Flags().IntVarP(&rankHead, "head", "", 0, "display the head part of rank") @@ -80,6 +80,7 @@ var rankCmd = &cobra.Command{ // load tournaments var tournaments []model.Tournament + var filteredEvents []entity.Event for _, e := range events { t, err := parser.ParseFile(filepath.Join(instance.DataPath(), e.Path)) if err != nil { @@ -97,6 +98,7 @@ var rankCmd = &cobra.Command{ continue } tournaments = append(tournaments, *t) + filteredEvents = append(filteredEvents, e) } if len(tournaments) == 0 { pterm.Warning.Println("No matched tournament(s)") @@ -104,17 +106,21 @@ var rankCmd = &cobra.Command{ } var eTournaments []entity.Tournament - for _, t := range tournaments { + for i, t := range tournaments { c := converter.NewConverter() trn, err := c.Normalize(instance.Conf.Players, t) if err != nil { errorMessageAndExit(err) } - eTournaments = append(eTournaments, entity.Tournament{Raw: t, Converted: *trn}) + eTournaments = append(eTournaments, entity.Tournament{ + Event: filteredEvents[i], + Raw: t, + Converted: *trn, + }) } options := operator.Option{ - OrderBy: rankOrderBy, + OrderBy: rankSortBy, MinimumPlayed: rankMinPlayed, Head: rankHead, Tail: rankTail, diff --git a/cmd/kicker-cli/cmd/import.go b/cmd/kicker-cli/cmd/import.go index ba98713..25174e8 100644 --- a/cmd/kicker-cli/cmd/import.go +++ b/cmd/kicker-cli/cmd/import.go @@ -11,17 +11,18 @@ import ( "github.com/crispgm/kicker-cli/internal/entity" "github.com/crispgm/kicker-cli/internal/util" "github.com/crispgm/kicker-cli/pkg/ktool/parser" + "github.com/crispgm/kicker-cli/pkg/rating" ) var ( importEventName string - importEventPoints int + importEventLevel string importEventCreateUnknownPlayers bool ) func init() { importCmd.Flags().StringVarP(&importEventName, "name", "n", "", "event name") - importCmd.Flags().IntVarP(&importEventPoints, "points", "", entity.DefaultPoints, "points for the event") + importCmd.Flags().StringVarP(&importEventLevel, "points", "", rating.KLocal, "points for the event") importCmd.Flags().BoolVarP(&importEventCreateUnknownPlayers, "create-unknown-players", "c", false, "create unknown players during importing") rootCmd.AddCommand(importCmd) } @@ -73,7 +74,7 @@ var importCmd = &cobra.Command{ } pterm.Printfln("Importing \"%s\" `%s` ...", importEventName, importPath) - event := *entity.NewEvent("temp", importEventName, importEventPoints) + event := *entity.NewEvent("temp", importEventName, importEventLevel) fn := fmt.Sprintf("%s.ktool", event.ID) event.Path = fn md5, err := util.MD5CheckSum(importPath) diff --git a/docs/ranking_system.md b/docs/ranking_system.md index 9f022ce..94237fb 100644 --- a/docs/ranking_system.md +++ b/docs/ranking_system.md @@ -8,20 +8,20 @@ KCS provides both traditional points-based ranking (KRP) and ELO-based (KELO) ra KRP follows ITSF points but with our own interpretation of event class: -| Place | World | Continental | Domestic | Local | -| ----- | ----- | ----------- | -------- | ----- | -| 1 | 200 | 150 | 100 | 50 | -| 2 | 180 | 135 | 90 | 45 | -| 3 | 160 | 120 | 80 | 40 | -| 4 | 140 | 105 | 70 | 35 | -| 5 | 120 | 90 | 60 | 30 | -| 9 | 100 | 75 | 50 | 25 | -| 17 | 80 | 60 | 40 | 20 | -| 33 | 60 | 45 | 30 | 15 | -| 65 | 40 | 30 | 20 | 10 | -| 129 | 20 | 15 | 10 | 5 | -| 257 | 12 | 9 | 6 | 3 | -| 513 | 4 | 3 | 2 | 1 | +| Place | World | Continental | Domestic | Local | Casual | +| ----- | ----- | ----------- | -------- | ----- | ------ | +| 1 | 200 | 150 | 100 | 50 | 25 | +| 2 | 180 | 135 | 90 | 45 | 22 | +| 3 | 160 | 120 | 80 | 40 | 19 | +| 4 | 140 | 105 | 70 | 35 | 16 | +| 5 | 120 | 90 | 60 | 30 | 13 | +| 9 | 100 | 75 | 50 | 25 | 10 | +| 17 | 80 | 60 | 40 | 20 | 7 | +| 33 | 60 | 45 | 30 | 15 | 4 | +| 65 | 40 | 30 | 20 | 10 | 2 | +| 129 | 20 | 15 | 10 | 5 | 1 | +| 257 | 12 | 9 | 6 | 3 | 0 | +| 513 | 4 | 3 | 2 | 1 | 0 | ### Kicker ELO Scores (KELO) diff --git a/internal/entity/event.go b/internal/entity/event.go index d9f94c0..0802559 100644 --- a/internal/entity/event.go +++ b/internal/entity/event.go @@ -9,22 +9,22 @@ const DefaultPoints = 50 // Event . type Event struct { - ID string `yaml:"id"` - Name string `yaml:"name"` - Path string `yaml:"path"` - MD5 string `yaml:"md5"` - Points int `yaml:"points"` - ITSFPoints int `yaml:"itsf_points"` - ATSAPoints int `yaml:"atsa_points"` - URL string `yaml:"url"` + ID string `yaml:"id"` + Name string `yaml:"name"` + Path string `yaml:"path"` + MD5 string `yaml:"md5"` + KickerLevel string `yaml:"kicker_level"` + ITSFLevel string `yaml:"itsf_level"` + ATSALevel string `yaml:"atsa_level"` + URL string `yaml:"url"` } // NewEvent creates an event -func NewEvent(path, name string, points int) *Event { +func NewEvent(path, name string, level string) *Event { return &Event{ - ID: util.UUID(), - Name: name, - Path: path, - Points: points, + ID: util.UUID(), + Name: name, + Path: path, + KickerLevel: level, } } diff --git a/internal/entity/event_test.go b/internal/entity/event_test.go index ba84d34..e7cc40d 100644 --- a/internal/entity/event_test.go +++ b/internal/entity/event_test.go @@ -11,5 +11,5 @@ func TestNewEvent(t *testing.T) { assert.NotEmpty(t, p.ID) assert.Equal(t, p.Name, "test") - assert.Equal(t, p.Points, 50) + assert.Equal(t, p.KickerLevel, 50) } diff --git a/internal/entity/player.go b/internal/entity/player.go index 4f64e23..6fa14e7 100644 --- a/internal/entity/player.go +++ b/internal/entity/player.go @@ -15,13 +15,14 @@ type Player struct { ITSFID string `yaml:"itsf_id,omitempty"` // statistics data, not write - Played int `yaml:"-"` + EventsPlayed int `yaml:"-"` + GamesPlayed int `yaml:"-"` Win int `yaml:"-"` Loss int `yaml:"-"` Draw int `yaml:"-"` WinRate float32 `yaml:"-"` - Points int `yaml:"-"` // kicker ranking points EloRating float64 `yaml:"-"` // kicker ELO scores + KickerPoints int `yaml:"-"` // kicker ranking points ATSAPoints int `yaml:"-"` // ATSA points ITSFPoints int `yaml:"-"` // ITSF points TimePlayed int `yaml:"-"` diff --git a/internal/entity/tournament.go b/internal/entity/tournament.go index 6202d01..cd55d9a 100644 --- a/internal/entity/tournament.go +++ b/internal/entity/tournament.go @@ -4,6 +4,7 @@ import "github.com/crispgm/kicker-cli/pkg/ktool/model" // Tournament . type Tournament struct { + Event Event Raw model.Tournament Converted Record } diff --git a/internal/operator/double/player_rank.go b/internal/operator/double/player_rank.go index 673f61c..e1dd2a5 100644 --- a/internal/operator/double/player_rank.go +++ b/internal/operator/double/player_rank.go @@ -56,10 +56,10 @@ func (p *PlayerRank) Output() [][]string { t1p2Data.Name = g.Team1[1] t2p1Data.Name = g.Team2[0] t2p2Data.Name = g.Team2[1] - t1p1Data.Played++ - t1p2Data.Played++ - t2p1Data.Played++ - t2p2Data.Played++ + t1p1Data.GamesPlayed++ + t1p2Data.GamesPlayed++ + t2p1Data.GamesPlayed++ + t2p2Data.GamesPlayed++ t1p1Data.TimePlayed += g.TimePlayed t1p2Data.TimePlayed += g.TimePlayed t2p1Data.TimePlayed += g.TimePlayed @@ -134,32 +134,62 @@ func (p *PlayerRank) Output() [][]string { } team1elo := (t1p1Elo + t1p2Elo) / 2 team2elo := (t2p1Elo + t2p2Elo) / 2 - t1p1Data.EloRating = p.calculateELO(t1p1Data.Played, t1p1Elo, team2elo, sa) - t1p2Data.EloRating = p.calculateELO(t1p2Data.Played, t1p2Elo, team2elo, sa) - t2p1Data.EloRating = p.calculateELO(t2p1Data.Played, t2p1Elo, team1elo, sb) - t2p2Data.EloRating = p.calculateELO(t2p2Data.Played, t2p2Elo, team1elo, sb) + t1p1Data.EloRating = p.calculateELO(t1p1Data.GamesPlayed, t1p1Elo, team2elo, sa) + t1p2Data.EloRating = p.calculateELO(t1p2Data.GamesPlayed, t1p2Elo, team2elo, sa) + t2p1Data.EloRating = p.calculateELO(t2p1Data.GamesPlayed, t2p1Elo, team1elo, sb) + t2p2Data.EloRating = p.calculateELO(t2p2Data.GamesPlayed, t2p2Elo, team1elo, sb) data[g.Team1[0]] = t1p1Data data[g.Team1[1]] = t1p2Data data[g.Team2[0]] = t2p1Data data[g.Team2[1]] = t2p2Data } + // ranking points + curRank := 0 + for i := len(t.Converted.Ranks) - 1; i >= 0; i-- { + rank := t.Converted.Ranks[i] + curRank += len(rank) / 2 + factors := rating.Factor{ + Place: curRank, + } + for _, r := range rank { + ranker := rating.Rank{} + d := data[r.Name] + if len(t.Event.KickerLevel) > 0 { + factors.PlayerScore = float64(d.KickerPoints) + factors.Level = t.Event.KickerLevel + d.KickerPoints = int(ranker.Calculate(factors)) + } + if len(t.Event.ATSALevel) > 0 { + factors.Level = t.Event.ATSALevel + factors.PlayerScore = float64(d.ATSAPoints) + d.ATSAPoints = int(ranker.Calculate(factors)) + } + if len(t.Event.ITSFLevel) > 0 { + factors.PlayerScore = float64(d.ITSFPoints) + factors.Level = t.Event.ITSFLevel + d.ITSFPoints = int(ranker.Calculate(factors)) + } + d.EventsPlayed++ + data[r.Name] = d + } + } } var sliceData []entity.Player for _, d := range data { d.GoalDiff = d.Goals - d.GoalsIn - if d.Played != 0 { - d.WinRate = float32(d.Win) / float32(d.Played) * 100.0 + if d.GamesPlayed != 0 { + d.WinRate = float32(d.Win) / float32(d.GamesPlayed) * 100.0 if d.HomeWin+d.HomeLoss > 0 { d.HomeWinRate = float32(d.HomeWin) / float32(d.HomeWin+d.HomeLoss) * 100.0 } if d.AwayWin+d.AwayLoss > 0 { d.AwayWinRate = float32(d.AwayWin) / float32(d.AwayWin+d.AwayLoss) * 100.0 } - d.PointsPerGame = float32(d.Goals) / float32(d.Played) - d.PointsInPerGame = float32(d.GoalsIn) / float32(d.Played) - d.TimePerGame = d.TimePlayed / d.Played / 1000 + d.PointsPerGame = float32(d.Goals) / float32(d.GamesPlayed) + d.PointsInPerGame = float32(d.GoalsIn) / float32(d.GamesPlayed) + d.TimePerGame = d.TimePlayed / d.GamesPlayed / 1000 d.LongestGameTime /= 1000 d.ShortestGameTime /= 1000 if d.Win > 0 { @@ -173,14 +203,28 @@ func (p *PlayerRank) Output() [][]string { } p.players = sliceData sort.SliceStable(sliceData, func(i, j int) bool { - if sliceData[i].Played >= p.options.MinimumPlayed && sliceData[j].Played < p.options.MinimumPlayed { - return true - } - if sliceData[i].Played < p.options.MinimumPlayed && sliceData[j].Played >= p.options.MinimumPlayed { - return false + if p.options.OrderBy == "wr" || p.options.OrderBy == "elo" { + if sliceData[i].GamesPlayed >= p.options.MinimumPlayed && sliceData[j].GamesPlayed < p.options.MinimumPlayed { + return true + } + if sliceData[i].GamesPlayed < p.options.MinimumPlayed && sliceData[j].GamesPlayed >= p.options.MinimumPlayed { + return false + } } - if p.options.OrderBy == "elo" { + if p.options.OrderBy == "krs" { + if sliceData[i].KickerPoints > sliceData[j].KickerPoints { + return true + } + } else if p.options.OrderBy == "atsa" { + if sliceData[i].ATSAPoints > sliceData[j].ATSAPoints { + return true + } + } else if p.options.OrderBy == "itsf" { + if sliceData[i].ITSFPoints > sliceData[j].ITSFPoints { + return true + } + } else if p.options.OrderBy == "elo" { if sliceData[i].EloRating > sliceData[j].EloRating { return true } @@ -198,7 +242,7 @@ func (p *PlayerRank) Output() [][]string { sliceData = sliceData[len(sliceData)-p.options.Tail:] } - header := []string{"#", "Name", "Num", "Win", "Loss", "Draw", "WR%", "Elo"} + header := []string{"#", "Name", "Events", "Games", "Win", "Loss", "Draw", "WR%", "Elo", "KRS", "ATSA", "ITSF"} pointHeader := []string{"G+", "G-", "G±", "PPG", "LPG", "DPW", "DPL"} if p.options.WithGoals { header = append(header, pointHeader...) @@ -211,12 +255,16 @@ func (p *PlayerRank) Output() [][]string { item := []string{ fmt.Sprintf("%d", i+1), d.Name, - fmt.Sprintf("%d", d.Played), + fmt.Sprintf("%d", d.EventsPlayed), + fmt.Sprintf("%d", d.GamesPlayed), fmt.Sprintf("%d", d.Win), fmt.Sprintf("%d", d.Loss), fmt.Sprintf("%d", d.Draw), fmt.Sprintf("%.0f%%", d.WinRate), fmt.Sprintf("%.0f", d.EloRating), + fmt.Sprintf("%d", d.KickerPoints), + fmt.Sprintf("%d", d.ATSAPoints), + fmt.Sprintf("%d", d.ITSFPoints), } if p.options.WithGoals { item = append(item, []string{ diff --git a/internal/operator/single/player_rank.go b/internal/operator/single/player_rank.go index bb98e0f..fd2c235 100644 --- a/internal/operator/single/player_rank.go +++ b/internal/operator/single/player_rank.go @@ -52,8 +52,8 @@ func (p *PlayerRank) Output() [][]string { p2Data := data[g.Team2[0]] p1Data.Name = g.Team1[0] p2Data.Name = g.Team2[0] - p1Data.Played++ - p2Data.Played++ + p1Data.GamesPlayed++ + p2Data.GamesPlayed++ p1Data.TimePlayed += g.TimePlayed p2Data.TimePlayed += g.TimePlayed if g.Point1 > g.Point2 { @@ -97,27 +97,57 @@ func (p *PlayerRank) Output() [][]string { sa = rating.Loss sb = rating.Win } - p1Data.EloRating = p.calculateELO(p1Data.Played, p1Elo, p2Elo, sa) - p2Data.EloRating = p.calculateELO(p2Data.Played, p2Elo, p1Elo, sb) + p1Data.EloRating = p.calculateELO(p1Data.GamesPlayed, p1Elo, p2Elo, sa) + p2Data.EloRating = p.calculateELO(p2Data.GamesPlayed, p2Elo, p1Elo, sb) data[g.Team1[0]] = p1Data data[g.Team2[0]] = p2Data } + // ranking points + curRank := 0 + for i := len(t.Converted.Ranks) - 1; i >= 0; i-- { + rank := t.Converted.Ranks[i] + curRank += len(rank) + factors := rating.Factor{ + Place: curRank, + } + for _, r := range rank { + ranker := rating.Rank{} + d := data[r.Name] + if len(t.Event.KickerLevel) > 0 { + factors.PlayerScore = float64(d.KickerPoints) + factors.Level = t.Event.KickerLevel + d.KickerPoints = int(ranker.Calculate(factors)) + } + if len(t.Event.ATSALevel) > 0 { + factors.Level = t.Event.ATSALevel + factors.PlayerScore = float64(d.ATSAPoints) + d.ATSAPoints = int(ranker.Calculate(factors)) + } + if len(t.Event.ITSFLevel) > 0 { + factors.PlayerScore = float64(d.ITSFPoints) + factors.Level = t.Event.ITSFLevel + d.ITSFPoints = int(ranker.Calculate(factors)) + } + d.EventsPlayed++ + data[r.Name] = d + } + } } var sliceData []entity.Player for _, d := range data { - if d.Played != 0 { + if d.GamesPlayed != 0 { d.GoalDiff = d.Goals - d.GoalsIn - d.WinRate = float32(d.Win) / float32(d.Played) * 100.0 + d.WinRate = float32(d.Win) / float32(d.GamesPlayed) * 100.0 if d.HomeWin+d.HomeLoss > 0 { d.HomeWinRate = float32(d.HomeWin) / float32(d.HomeWin+d.HomeLoss) * 100.0 } if d.AwayWin+d.AwayLoss > 0 { d.AwayWinRate = float32(d.AwayWin) / float32(d.AwayWin+d.AwayLoss) * 100.0 } - d.PointsPerGame = float32(d.Goals) / float32(d.Played) - d.PointsInPerGame = float32(d.GoalsIn) / float32(d.Played) - d.TimePerGame = d.TimePlayed / d.Played / 1000 + d.PointsPerGame = float32(d.Goals) / float32(d.GamesPlayed) + d.PointsInPerGame = float32(d.GoalsIn) / float32(d.GamesPlayed) + d.TimePerGame = d.TimePlayed / d.GamesPlayed / 1000 d.LongestGameTime /= 1000 d.ShortestGameTime /= 1000 if d.Win > 0 { @@ -131,14 +161,28 @@ func (p *PlayerRank) Output() [][]string { } p.players = sliceData sort.SliceStable(sliceData, func(i, j int) bool { - if sliceData[i].Played >= p.options.MinimumPlayed && sliceData[j].Played < p.options.MinimumPlayed { - return true - } - if sliceData[i].Played < p.options.MinimumPlayed && sliceData[j].Played >= p.options.MinimumPlayed { - return false + if p.options.OrderBy == "wr" || p.options.OrderBy == "elo" { + if sliceData[i].GamesPlayed >= p.options.MinimumPlayed && sliceData[j].GamesPlayed < p.options.MinimumPlayed { + return true + } + if sliceData[i].GamesPlayed < p.options.MinimumPlayed && sliceData[j].GamesPlayed >= p.options.MinimumPlayed { + return false + } } - if p.options.OrderBy == "elo" { + if p.options.OrderBy == "krs" { + if sliceData[i].KickerPoints > sliceData[j].KickerPoints { + return true + } + } else if p.options.OrderBy == "atsa" { + if sliceData[i].ATSAPoints > sliceData[j].ATSAPoints { + return true + } + } else if p.options.OrderBy == "itsf" { + if sliceData[i].ITSFPoints > sliceData[j].ITSFPoints { + return true + } + } else if p.options.OrderBy == "elo" { if sliceData[i].EloRating > sliceData[j].EloRating { return true } @@ -156,7 +200,7 @@ func (p *PlayerRank) Output() [][]string { sliceData = sliceData[len(sliceData)-p.options.Tail:] } - header := []string{"#", "Name", "Num", "Win", "Loss", "Draw", "WR%", "Elo"} + header := []string{"#", "Name", "Events", "Games", "Win", "Loss", "Draw", "WR%", "Elo", "KRS", "ATSA", "ITSF"} table := [][]string{} if p.options.WithHeader { table = append(table, header) @@ -165,12 +209,16 @@ func (p *PlayerRank) Output() [][]string { item := []string{ fmt.Sprintf("%d", i+1), d.Name, - fmt.Sprintf("%d", d.Played), + fmt.Sprintf("%d", d.EventsPlayed), + fmt.Sprintf("%d", d.GamesPlayed), fmt.Sprintf("%d", d.Win), fmt.Sprintf("%d", d.Loss), fmt.Sprintf("%d", d.Draw), fmt.Sprintf("%.0f%%", d.WinRate), fmt.Sprintf("%.0f", d.EloRating), + fmt.Sprintf("%d", d.KickerPoints), + fmt.Sprintf("%d", d.ATSAPoints), + fmt.Sprintf("%d", d.ITSFPoints), } table = append(table, item) } diff --git a/pkg/rating/rank.go b/pkg/rating/rank.go new file mode 100644 index 0000000..eebdb25 --- /dev/null +++ b/pkg/rating/rank.go @@ -0,0 +1,70 @@ +package rating + +import ( + "sort" +) + +var _ Rating = (*Rank)(nil) + +var kickerRankTable = map[string]map[int]int{ + KWorld: {1: 200, 2: 180, 3: 160, 4: 140, 5: 120, 9: 100, 17: 80, 33: 60, 65: 40, 129: 20, 257: 12, 513: 4}, + KContinental: {1: 150, 2: 135, 3: 120, 4: 105, 5: 90, 9: 75, 17: 60, 33: 45, 65: 30, 129: 15, 257: 9, 513: 3}, + KDomestic: {1: 100, 2: 90, 3: 80, 4: 70, 5: 60, 9: 50, 17: 40, 33: 30, 65: 20, 129: 10, 257: 6, 513: 2}, + KLocal: {1: 50, 2: 45, 3: 40, 4: 35, 5: 30, 9: 25, 17: 20, 33: 15, 65: 10, 129: 5, 257: 3, 513: 1}, + KCasual: {1: 25, 2: 22, 3: 19, 4: 16, 5: 13, 9: 10, 17: 7, 33: 4, 65: 2, 129: 1}, +} + +var atsaRankTable = map[string]map[int]int{ + ATSA2000: {1: 2000, 2: 1200, 3: 720, 5: 360, 9: 180, 17: 90, 33: 45}, + ATSA1000: {1: 1000, 2: 600, 3: 360, 4: 240, 5: 180, 7: 120, 9: 90, 17: 45, 33: 25}, + ATSA500: {1: 500, 2: 300, 3: 180, 4: 120, 5: 90, 7: 60, 9: 45, 13: 30, 17: 20, 33: 10}, + ATSA50: {1: 50, 2: 30, 3: 20, 4: 17, 5: 15, 7: 12, 9: 9, 17: 4, 33: 1}, +} + +var itsfRankTable = map[string]map[int]int{ + ITSFWorldSeries: {1: 200, 2: 180, 3: 160, 4: 140, 5: 120, 9: 100, 17: 80, 33: 60, 65: 40, 129: 20, 257: 12, 513: 4}, + ITSFInternational: {1: 150, 2: 135, 3: 120, 4: 105, 5: 90, 9: 75, 17: 60, 33: 45, 65: 30, 129: 15, 257: 9, 513: 3}, + ITSFMasterSeries: {1: 100, 2: 90, 3: 80, 4: 70, 5: 60, 9: 50, 17: 40, 33: 30, 65: 20, 129: 10, 257: 6, 513: 2}, + ITSFProTour: {1: 25, 2: 22, 3: 19, 4: 16, 5: 13, 9: 10, 17: 7, 33: 4, 65: 2, 129: 1}, +} + +// Rank calculates rank based rating +type Rank struct { +} + +// InitialScore . +func (r Rank) InitialScore() float64 { + return 0 +} + +// Calculate . +func (r Rank) Calculate(factors Factor) float64 { + var rankTable map[string]map[int]int + if factors.IsITSF() { + rankTable = itsfRankTable + } else if factors.IsATSA() { + rankTable = atsaRankTable + } else { + rankTable = kickerRankTable + } + curScore := factors.PlayerScore + incr := 0.0 + if table, ok := rankTable[factors.Level]; ok { + var sortPts [][]int + for pos, pts := range table { + sortPts = append(sortPts, []int{pos, pts}) + } + sort.SliceStable(sortPts, func(i, j int) bool { + return sortPts[i][0] < sortPts[j][0] + }) + incr = float64(sortPts[0][1]) + for _, pts := range sortPts { + if factors.Place >= pts[0] { + incr = float64(pts[1]) + } else { + return curScore + incr + } + } + } + return 0 +} diff --git a/pkg/rating/rank_test.go b/pkg/rating/rank_test.go new file mode 100644 index 0000000..0f4c5c4 --- /dev/null +++ b/pkg/rating/rank_test.go @@ -0,0 +1,34 @@ +package rating + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRank(t *testing.T) { + r := Rank{} + f := Factor{ + Level: ATSA50, + Place: 1, + PlayerScore: 0.0, + } + + assert.Zero(t, r.InitialScore()) + assert.Equal(t, 50.0, r.Calculate(f)) + + f.Place = 2 + assert.Equal(t, 30.0, r.Calculate(f)) + f.Place = 3 + assert.Equal(t, 20.0, r.Calculate(f)) + f.Place = 4 + assert.Equal(t, 17.0, r.Calculate(f)) + f.Place = 5 + assert.Equal(t, 15.0, r.Calculate(f)) + f.Place = 6 + assert.Equal(t, 15.0, r.Calculate(f)) + f.Place = 7 + assert.Equal(t, 12.0, r.Calculate(f)) + f.Place = 9 + assert.Equal(t, 9.0, r.Calculate(f)) +} diff --git a/pkg/rating/rating.go b/pkg/rating/rating.go index c45e22e..fb7c2b2 100644 --- a/pkg/rating/rating.go +++ b/pkg/rating/rating.go @@ -1,14 +1,32 @@ // Package rating multiple algorithms for rating package rating +import "strings" + +// ranking system +const ( + RSysITSF = "ITSF" + RSysATSA = "ATSA" + RSysKicker = "KRS" +) + // NotSanctioned represents event that not sanctioned by this organization const NotSanctioned = "NS" +// Kicker Points +const ( + KWorld = "KWorld" + KContinental = "KContinental" + KDomestic = "KDomestic" + KLocal = "KLocal" + KCasual = "KCasual" +) + // ITSF Points const ( ITSFWorldSeries = "ITSFWorldSeries" ITSFInternational = "ITSFInternational" - ITSFMasterSeries = "ITSFWorldSeries" + ITSFMasterSeries = "ITSFMasterSeries" ITSFProTour = "ITSFProTour" ) @@ -18,7 +36,6 @@ const ( ATSA1000 = "ATSA1000" ATSA500 = "ATSA500" ATSA50 = "ATSA50" - ATSA25 = "ATSA25" ) // literally win/draw/loss @@ -38,6 +55,27 @@ type Factor struct { Played int // game played } +// IsATSA . +func (f Factor) IsATSA() bool { + return strings.HasPrefix(f.Level, "ATSA") +} + +// IsITSF . +func (f Factor) IsITSF() bool { + return strings.HasPrefix(f.Level, "ITSF") +} + +// GetRankSystem . +func (f Factor) GetRankSystem() string { + if f.IsITSF() { + return RSysITSF + } else if f.IsATSA() { + return RSysATSA + } + + return RSysKicker +} + // Rating interface of rating system type Rating interface { InitialScore() float64 diff --git a/pkg/rating/rating_test.go b/pkg/rating/rating_test.go new file mode 100644 index 0000000..f0460a9 --- /dev/null +++ b/pkg/rating/rating_test.go @@ -0,0 +1,14 @@ +package rating + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRatingIsMethods(t *testing.T) { + f := Factor{Level: ATSA1000} + assert.Equal(t, RSysATSA, f.GetRankSystem()) + assert.True(t, f.IsATSA()) + assert.False(t, f.IsITSF()) +}