a geicko-2 based round robin ranking system designed to test c++ battleship submissions
battleship.dunkirk.sh
1package main
2
3import (
4 "fmt"
5 "strings"
6 "time"
7
8 tea "github.com/charmbracelet/bubbletea"
9 "github.com/charmbracelet/lipgloss"
10)
11
12type model struct {
13 username string
14 width int
15 height int
16 submissions []Submission
17 leaderboard []LeaderboardEntry
18 matches []MatchResult
19}
20
21func initialModel(username string, width, height int) model {
22 return model{
23 username: username,
24 width: width,
25 height: height,
26 submissions: []Submission{},
27 leaderboard: []LeaderboardEntry{},
28 }
29}
30
31func (m model) Init() tea.Cmd {
32 return tea.Batch(loadLeaderboard, loadSubmissions(m.username), tickCmd())
33}
34
35func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
36 switch msg := msg.(type) {
37 case tea.KeyMsg:
38 switch msg.String() {
39 case "ctrl+c", "q":
40 return m, tea.Quit
41 }
42 case tea.WindowSizeMsg:
43 m.width = msg.Width
44 m.height = msg.Height
45 case leaderboardMsg:
46 m.leaderboard = msg.entries
47 case submissionsMsg:
48 m.submissions = msg.submissions
49 case tickMsg:
50 return m, tea.Batch(loadLeaderboard, loadSubmissions(m.username), tickCmd())
51 }
52 return m, nil
53}
54
55
56
57func (m model) View() string {
58 var b strings.Builder
59
60 title := titleStyle.Render("🚢 Battleship Arena")
61 b.WriteString(title + "\n\n")
62
63 b.WriteString(fmt.Sprintf("User: %s\n\n", m.username))
64
65 // Upload instructions
66 infoStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("86"))
67 b.WriteString(infoStyle.Render(fmt.Sprintf("Upload via: scp -P %s memory_functions_yourname.cpp %s@%s:~/", sshPort, m.username, host)))
68 b.WriteString("\n\n")
69
70 // Show submissions
71 if len(m.submissions) > 0 {
72 b.WriteString(renderSubmissions(m.submissions))
73 b.WriteString("\n")
74 }
75
76 // Show leaderboard if loaded
77 if len(m.leaderboard) > 0 {
78 b.WriteString(renderLeaderboard(m.leaderboard))
79 }
80
81 b.WriteString("\n\nPress q to quit")
82
83 return b.String()
84}
85
86
87
88type leaderboardMsg struct {
89 entries []LeaderboardEntry
90}
91
92func loadLeaderboard() tea.Msg {
93 entries, err := getLeaderboard(20)
94 if err != nil {
95 return leaderboardMsg{entries: nil}
96 }
97 return leaderboardMsg{entries: entries}
98}
99
100type submissionsMsg struct {
101 submissions []Submission
102}
103
104func loadSubmissions(username string) tea.Cmd {
105 return func() tea.Msg {
106 submissions, err := getUserSubmissions(username)
107 if err != nil {
108 return submissionsMsg{submissions: nil}
109 }
110 return submissionsMsg{submissions: submissions}
111 }
112}
113
114type matchesMsg struct {
115 matches []MatchResult
116}
117
118func loadMatches() tea.Msg {
119 matches, err := getAllMatches()
120 if err != nil {
121 return matchesMsg{matches: nil}
122 }
123 return matchesMsg{matches: matches}
124}
125
126type tickMsg time.Time
127
128func tickCmd() tea.Cmd {
129 return tea.Tick(time.Second*5, func(t time.Time) tea.Msg {
130 return tickMsg(t)
131 })
132}
133
134func renderSubmissions(submissions []Submission) string {
135 var b strings.Builder
136 b.WriteString(lipgloss.NewStyle().Bold(true).Render("📤 Your Submissions") + "\n\n")
137
138 headerStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("240"))
139 b.WriteString(headerStyle.Render(fmt.Sprintf("%-35s %-15s %s\n",
140 "Filename", "Uploaded", "Status")))
141
142 for _, sub := range submissions {
143 var statusColor string
144 switch sub.Status {
145 case "pending":
146 statusColor = "yellow"
147 case "testing":
148 statusColor = "blue"
149 case "completed":
150 statusColor = "green"
151 case "failed":
152 statusColor = "red"
153 default:
154 statusColor = "white"
155 }
156
157 relTime := formatRelativeTime(sub.UploadTime)
158
159 // Format the line without styles first for proper alignment
160 statusStyled := lipgloss.NewStyle().Foreground(lipgloss.Color(statusColor)).Render(sub.Status)
161 b.WriteString(fmt.Sprintf("%-35s %-15s %s\n",
162 sub.Filename, relTime, statusStyled))
163 }
164
165 return b.String()
166}
167
168func formatRelativeTime(t time.Time) string {
169 duration := time.Since(t)
170 if duration < time.Minute {
171 return "just now"
172 } else if duration < time.Hour {
173 mins := int(duration.Minutes())
174 return fmt.Sprintf("%dm ago", mins)
175 } else if duration < 24*time.Hour {
176 hours := int(duration.Hours())
177 return fmt.Sprintf("%dh ago", hours)
178 }
179 days := int(duration.Hours() / 24)
180 return fmt.Sprintf("%dd ago", days)
181}
182
183func renderLeaderboard(entries []LeaderboardEntry) string {
184 if len(entries) == 0 {
185 return "No entries yet"
186 }
187
188 var b strings.Builder
189 b.WriteString(lipgloss.NewStyle().Bold(true).Render("🏆 Leaderboard") + "\n\n")
190
191 // Header without styling on the whole line
192 b.WriteString(fmt.Sprintf("%-4s %-20s %8s %8s %10s %10s\n",
193 "Rank", "User", "Wins", "Losses", "Win Rate", "Avg Moves"))
194
195 for i, entry := range entries {
196 winRate := 0.0
197 total := entry.Wins + entry.Losses
198 if total > 0 {
199 winRate = float64(entry.Wins) / float64(total) * 100
200 }
201
202 rank := fmt.Sprintf("#%d", i+1)
203
204 // Apply color only to the rank
205 var coloredRank string
206 if i == 0 {
207 coloredRank = lipgloss.NewStyle().Foreground(lipgloss.Color("220")).Render(rank) // Gold
208 } else if i == 1 {
209 coloredRank = lipgloss.NewStyle().Foreground(lipgloss.Color("250")).Render(rank) // Silver
210 } else if i == 2 {
211 coloredRank = lipgloss.NewStyle().Foreground(lipgloss.Color("208")).Render(rank) // Bronze
212 } else {
213 coloredRank = rank
214 }
215
216 // Format line with proper spacing
217 b.WriteString(fmt.Sprintf("%-4s %-20s %8d %8d %9.2f%% %9.1f\n",
218 coloredRank, entry.Username, entry.Wins, entry.Losses, winRate, entry.AvgMoves))
219 }
220
221 return b.String()
222}
223
224func renderBracket(matches []MatchResult) string {
225 var b strings.Builder
226 b.WriteString(lipgloss.NewStyle().Bold(true).Render("⚔️ Tournament Bracket") + "\n\n")
227
228 if len(matches) == 0 {
229 return b.String()
230 }
231
232 // Group matches by matchup pairs
233 matchups := make(map[string]MatchResult)
234 for _, match := range matches {
235 // Create a consistent key regardless of order
236 key := match.Player1Username + " vs " + match.Player2Username
237 reverseKey := match.Player2Username + " vs " + match.Player1Username
238
239 // Check if we already have this matchup
240 if _, exists := matchups[reverseKey]; !exists {
241 matchups[key] = match
242 }
243 }
244
245 // Display up to 8 matchups in bracket format
246 count := 0
247 for _, match := range matchups {
248 if count >= 8 {
249 break
250 }
251
252 // Determine winner styling
253 player1Style := lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
254 player2Style := lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
255 winnerBox := lipgloss.NewStyle().
256 Foreground(lipgloss.Color("green")).
257 Bold(true).
258 Border(lipgloss.RoundedBorder()).
259 Padding(0, 1)
260
261 var winner string
262 if match.WinnerUsername == match.Player1Username {
263 player1Style = player1Style.Foreground(lipgloss.Color("green")).Bold(true)
264 winner = match.Player1Username
265 } else {
266 player2Style = player2Style.Foreground(lipgloss.Color("green")).Bold(true)
267 winner = match.Player2Username
268 }
269
270 // Format bracket style
271 // Player1 ┐
272 // ├── Winner
273 // Player2 ┘
274
275 player1Box := lipgloss.NewStyle().
276 Border(lipgloss.RoundedBorder()).
277 Padding(0, 1).
278 Width(15)
279
280 player2Box := lipgloss.NewStyle().
281 Border(lipgloss.RoundedBorder()).
282 Padding(0, 1).
283 Width(15)
284
285 p1 := player1Box.Render(player1Style.Render(match.Player1Username))
286 connector1 := " ┐"
287 middle := " ├──"
288 connector2 := " ┘"
289 p2 := player2Box.Render(player2Style.Render(match.Player2Username))
290 winnerStr := winnerBox.Render(fmt.Sprintf("%s wins", winner))
291
292 b.WriteString(p1 + connector1 + "\n")
293 b.WriteString(strings.Repeat(" ", 17) + middle + " " + winnerStr + "\n")
294 b.WriteString(p2 + connector2 + "\n")
295 b.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Render(
296 fmt.Sprintf(" (avg %d moves)\n", match.AvgMoves)))
297 b.WriteString("\n")
298
299 count++
300 }
301
302 if len(matchups) > 8 {
303 b.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Render(
304 fmt.Sprintf("... and %d more matches\n", len(matchups)-8)))
305 }
306
307 return b.String()
308}