-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmain.go
More file actions
178 lines (150 loc) · 4.12 KB
/
main.go
File metadata and controls
178 lines (150 loc) · 4.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
package main
import (
"encoding/csv"
"flag"
"fmt"
"io"
"log"
"math"
"os"
"sort"
"strings"
elogo "github.com/kortemy/elo-go"
)
// The default starting score of players in our analyzer.
const defaultStartingScore = 1500
type finalScore struct {
player string
eloScore int
}
func main() {
path := flag.String("path", "./mtgscores.csv", "path to analyze with tracker")
useTUI := flag.Bool("tui", false, "use terminal UI for displaying rankings")
flag.Parse()
// Suppress logs during TUI mode to prevent interference with display
if *useTUI {
log.SetOutput(io.Discard)
}
log.Printf("Analyzing scores for %s", *path)
elo := initializeElo()
scores := make(map[string]int)
if err := processScores(*path, elo, scores); err != nil {
if *useTUI {
log.SetOutput(os.Stderr) // Restore for error display
}
log.Fatalf("Error processing scores: %v", err)
}
finalScores := calculateFinalScores(scores)
if *useTUI {
// Use the TUI to display rankings
if err := DisplayRankingsTUI(finalScores); err != nil {
log.SetOutput(os.Stderr) // Restore log output to show errors
log.Fatalf("Error in TUI: %v", err)
}
} else {
// Use the original console output
for i, v := range finalScores {
fmt.Printf("%d --- %s --- %d\n", i+1, v.player, v.eloScore)
}
}
}
func initializeElo() *elogo.Elo {
elo := elogo.NewElo()
elo.D = 800
elo.K = 40
return elo
}
func processScores(path string, elo *elogo.Elo, scores map[string]int) error {
file, err := os.Open(path)
if err != nil {
return fmt.Errorf("failed to open scores file: %w", err)
}
defer file.Close()
reader := csv.NewReader(file)
for {
record, err := reader.Read()
if err != nil {
if err == io.EOF {
break
}
return fmt.Errorf("error reading record: %w", err)
}
if len(record) < 3 {
continue
}
game := parseGame(record[2:])
if len(game) >= 2 {
if err := scoreGame(elo, scores, game); err != nil {
return fmt.Errorf("failed to score game: %w", err)
}
}
}
return nil
}
func parseGame(players []string) []string {
game := make([]string, 0, len(players))
for _, player := range players {
player = strings.TrimSpace(player)
if player == "" {
break
}
game = append(game, player)
}
return game
}
func scoreGame(elo *elogo.Elo, scores map[string]int, game []string) error {
numPlayers := len(game)
if numPlayers < 2 {
return fmt.Errorf("invalid game: need at least 2 players, got %d", numPlayers)
}
// Ensure all players have a starting score and capture snapshot ratings.
ratings := make([]float64, numPlayers)
for i := 0; i < numPlayers; i++ {
name := game[i]
if _, exists := scores[name]; !exists {
scores[name] = defaultStartingScore
}
ratings[i] = float64(scores[name])
}
// Accumulate Elo deltas based on all pairwise outcomes from the snapshot.
deltas := make([]float64, numPlayers)
K := float64(elo.K)
D := float64(elo.D)
for i := range numPlayers {
for j := i + 1; j < numPlayers; j++ {
// Player at index i placed higher than player at index j, so i wins vs j.
RA := ratings[i]
RB := ratings[j]
// Expected score of A vs B
EA := 1.0 / (1.0 + math.Pow(10, (RB-RA)/D))
// Single-game deltas
deltaA := K * (1.0 - EA) // A wins
deltaB := -deltaA // B loses
// Apply deltas
deltas[i] += deltaA
deltas[j] += deltaB
}
}
// Apply accumulated deltas.
for i := range numPlayers {
name := game[i]
newRating := int(math.Round(ratings[i] + deltas[i]))
scores[name] = newRating
}
return nil
}
func calculateFinalScores(scores map[string]int) []finalScore {
finalScores := make([]finalScore, 0, len(scores))
for player, eloScore := range scores {
finalScores = append(finalScores, finalScore{player: player, eloScore: eloScore})
}
// Deterministic ordering: stable sort by Elo desc, then player name asc as tiebreaker.
sort.SliceStable(finalScores, func(i, j int) bool {
if finalScores[i].eloScore == finalScores[j].eloScore {
return finalScores[i].player < finalScores[j].player
}
return finalScores[i].eloScore > finalScores[j].eloScore
})
return finalScores
}
// displayScores was previously used for console output; removed to avoid unused warnings.