a geicko-2 based round robin ranking system designed to test c++ battleship submissions battleship.dunkirk.sh
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add tabbed navigation to SSH UI (home, leaderboard, profile)

+122 -7
+122 -7
internal/tui/model.go
··· 12 12 "battleship-arena/internal/storage" 13 13 ) 14 14 15 + type viewMode int 16 + 17 + const ( 18 + viewHome viewMode = iota 19 + viewLeaderboard 20 + viewProfile 21 + ) 22 + 15 23 var titleStyle = lipgloss.NewStyle(). 16 24 Bold(true). 17 25 Foreground(lipgloss.Color("205")). ··· 27 35 matches []storage.MatchResult 28 36 externalURL string 29 37 sshPort string 38 + currentView viewMode 30 39 } 31 40 32 41 func InitialModel(username string, width, height int) model { ··· 55 64 leaderboard: []storage.LeaderboardEntry{}, 56 65 externalURL: externalURL, 57 66 sshPort: sshPort, 67 + currentView: viewHome, 58 68 } 59 69 } 60 70 ··· 68 78 switch msg.String() { 69 79 case "ctrl+c", "q": 70 80 return m, tea.Quit 81 + case "h", "1": 82 + m.currentView = viewHome 83 + case "l", "2": 84 + m.currentView = viewLeaderboard 85 + case "p", "3": 86 + m.currentView = viewProfile 71 87 } 72 88 case tea.WindowSizeMsg: 73 89 m.width = msg.Width ··· 76 92 m.leaderboard = msg.entries 77 93 case submissionsMsg: 78 94 m.submissions = msg.submissions 95 + case matchesMsg: 96 + m.matches = msg.matches 79 97 case tickMsg: 80 - return m, tea.Batch(loadLeaderboard, loadSubmissions(m.username), tickCmd()) 98 + return m, tea.Batch(loadLeaderboard, loadSubmissions(m.username), loadMatches, tickCmd()) 81 99 } 82 100 return m, nil 83 101 } ··· 88 106 var b strings.Builder 89 107 90 108 title := titleStyle.Render("🚢 Battleship Arena") 91 - b.WriteString(title + "\n\n") 109 + b.WriteString(title + "\n") 110 + 111 + // Navigation tabs 112 + tabStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")) 113 + activeTabStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("86")).Bold(true) 114 + 115 + tabs := []string{"[h] Home", "[l] Leaderboard", "[p] Profile"} 116 + for i, tab := range tabs { 117 + if viewMode(i) == m.currentView { 118 + b.WriteString(activeTabStyle.Render(tab)) 119 + } else { 120 + b.WriteString(tabStyle.Render(tab)) 121 + } 122 + if i < len(tabs)-1 { 123 + b.WriteString(" ") 124 + } 125 + } 126 + b.WriteString("\n\n") 127 + 128 + // Render content based on current view 129 + switch m.currentView { 130 + case viewHome: 131 + b.WriteString(m.renderHome()) 132 + case viewLeaderboard: 133 + b.WriteString(m.renderLeaderboardView()) 134 + case viewProfile: 135 + b.WriteString(m.renderProfile()) 136 + } 137 + 138 + b.WriteString("\n\nPress q to quit") 139 + 140 + return b.String() 141 + } 142 + 143 + func (m model) renderHome() string { 144 + var b strings.Builder 92 145 93 146 b.WriteString(fmt.Sprintf("User: %s\n\n", m.username)) 94 147 ··· 100 153 // Show submissions 101 154 if len(m.submissions) > 0 { 102 155 b.WriteString(renderSubmissions(m.submissions)) 103 - b.WriteString("\n") 156 + } else { 157 + b.WriteString("No submissions yet. Upload your first AI!\n") 104 158 } 105 159 106 - // Show leaderboard if loaded 160 + return b.String() 161 + } 162 + 163 + func (m model) renderLeaderboardView() string { 107 164 if len(m.leaderboard) > 0 { 108 - b.WriteString(renderLeaderboard(m.leaderboard)) 165 + return renderLeaderboard(m.leaderboard) 109 166 } 167 + return "Loading leaderboard..." 168 + } 110 169 111 - b.WriteString("\n\nPress q to quit") 112 - 170 + func (m model) renderProfile() string { 171 + var b strings.Builder 172 + 173 + b.WriteString(fmt.Sprintf("Profile: %s\n\n", m.username)) 174 + 175 + // Show user stats from submissions 176 + if len(m.submissions) > 0 { 177 + b.WriteString(renderSubmissions(m.submissions)) 178 + b.WriteString("\n") 179 + } 180 + 181 + // Show recent matches involving this user 182 + if len(m.matches) > 0 { 183 + b.WriteString("\nRecent Matches:\n") 184 + b.WriteString(renderMatches(m.matches, m.username)) 185 + } 186 + 113 187 return b.String() 114 188 } 115 189 ··· 243 317 displayRank, entry.Username, ratingStr, entry.Wins, entry.Losses, entry.WinPct, entry.AvgMoves)) 244 318 } 245 319 320 + return b.String() 321 + } 322 + 323 + func renderMatches(matches []storage.MatchResult, username string) string { 324 + var b strings.Builder 325 + 326 + // Filter to show only matches involving this user 327 + userMatches := []storage.MatchResult{} 328 + for _, match := range matches { 329 + if match.Player1Username == username || match.Player2Username == username { 330 + userMatches = append(userMatches, match) 331 + } 332 + } 333 + 334 + if len(userMatches) == 0 { 335 + return "No matches yet" 336 + } 337 + 338 + // Limit to last 10 matches 339 + limit := 10 340 + if len(userMatches) > limit { 341 + userMatches = userMatches[:limit] 342 + } 343 + 344 + headerStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("240")) 345 + b.WriteString(headerStyle.Render(fmt.Sprintf("%-20s %-20s %-20s %10s\n", 346 + "Player 1", "Player 2", "Winner", "Avg Moves"))) 347 + b.WriteString("\n") 348 + 349 + for _, match := range userMatches { 350 + line := fmt.Sprintf("%-20s vs %-20s → %-20s (%d moves)\n", 351 + match.Player1Username, match.Player2Username, match.WinnerUsername, match.AvgMoves) 352 + 353 + // Highlight wins in green, losses in red 354 + if match.WinnerUsername == username { 355 + b.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("green")).Render(line)) 356 + } else { 357 + b.WriteString(line) 358 + } 359 + } 360 + 246 361 return b.String() 247 362 } 248 363