Skip to content

Commit

Permalink
feat: implement kicker/atsa/itsf ranking system
Browse files Browse the repository at this point in the history
  • Loading branch information
crispgm committed Sep 21, 2023
1 parent 59b09e1 commit aec2156
Show file tree
Hide file tree
Showing 14 changed files with 352 additions and 80 deletions.
15 changes: 13 additions & 2 deletions cmd/kicker-cli/cmd/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cmd
import (
"fmt"
"path/filepath"
"strings"

"github.com/pterm/pterm"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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,
Expand Down
16 changes: 11 additions & 5 deletions cmd/kicker-cli/cmd/event_rank.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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 {
Expand All @@ -97,24 +98,29 @@ var rankCmd = &cobra.Command{
continue
}
tournaments = append(tournaments, *t)
filteredEvents = append(filteredEvents, e)
}
if len(tournaments) == 0 {
pterm.Warning.Println("No matched tournament(s)")
return
}

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,
Expand Down
7 changes: 4 additions & 3 deletions cmd/kicker-cli/cmd/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
Expand Down
28 changes: 14 additions & 14 deletions docs/ranking_system.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
26 changes: 13 additions & 13 deletions internal/entity/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
2 changes: 1 addition & 1 deletion internal/entity/event_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
5 changes: 3 additions & 2 deletions internal/entity/player.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:"-"`
Expand Down
1 change: 1 addition & 0 deletions internal/entity/tournament.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import "github.com/crispgm/kicker-cli/pkg/ktool/model"

// Tournament .
type Tournament struct {
Event Event
Raw model.Tournament
Converted Record
}
Expand Down
90 changes: 69 additions & 21 deletions internal/operator/double/player_rank.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
Expand All @@ -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...)
Expand All @@ -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{
Expand Down
Loading

0 comments on commit aec2156

Please sign in to comment.