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}