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: use brackets.js

+213 -43
+1
main.go
··· 112 112 mux := http.NewServeMux() 113 113 mux.HandleFunc("/", handleLeaderboard) 114 114 mux.HandleFunc("/api/leaderboard", handleAPILeaderboard) 115 + mux.HandleFunc("/api/bracket", handleBracketData) 115 116 116 117 log.Printf("Web server starting on :%s", webPort) 117 118 if err := http.ListenAndServe(":"+webPort, mux); err != nil {
+79 -42
model.go
··· 29 29 } 30 30 31 31 func (m model) Init() tea.Cmd { 32 - return tea.Batch(loadLeaderboard, loadSubmissions(m.username), loadMatches, tickCmd()) 32 + return tea.Batch(loadLeaderboard, loadSubmissions(m.username), tickCmd()) 33 33 } 34 34 35 35 func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { ··· 46 46 m.leaderboard = msg.entries 47 47 case submissionsMsg: 48 48 m.submissions = msg.submissions 49 - case matchesMsg: 50 - m.matches = msg.matches 51 49 case tickMsg: 52 - return m, tea.Batch(loadLeaderboard, loadSubmissions(m.username), loadMatches, tickCmd()) 50 + return m, tea.Batch(loadLeaderboard, loadSubmissions(m.username), tickCmd()) 53 51 } 54 52 return m, nil 55 53 } ··· 72 70 // Show submissions 73 71 if len(m.submissions) > 0 { 74 72 b.WriteString(renderSubmissions(m.submissions)) 75 - b.WriteString("\n") 76 - } 77 - 78 - // Show bracket-style matches 79 - if len(m.matches) > 0 { 80 - b.WriteString(renderBracket(m.matches)) 81 73 b.WriteString("\n") 82 74 } 83 75 ··· 196 188 var b strings.Builder 197 189 b.WriteString(lipgloss.NewStyle().Bold(true).Render("🏆 Leaderboard") + "\n\n") 198 190 199 - headerStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("240")) 200 - b.WriteString(headerStyle.Render(fmt.Sprintf("%-4s %-20s %8s %8s %10s\n", 201 - "Rank", "User", "Wins", "Losses", "Win Rate"))) 191 + // Header without styling on the whole line 192 + b.WriteString(fmt.Sprintf("%-4s %-20s %8s %8s %10s\n", 193 + "Rank", "User", "Wins", "Losses", "Win Rate")) 202 194 203 195 for i, entry := range entries { 204 196 winRate := 0.0 ··· 208 200 } 209 201 210 202 rank := fmt.Sprintf("#%d", i+1) 211 - line := fmt.Sprintf("%-4s %-20s %8d %8d %9.2f%%\n", 212 - rank, entry.Username, entry.Wins, entry.Losses, winRate) 213 - 214 - style := lipgloss.NewStyle() 203 + 204 + // Apply color only to the rank 205 + var coloredRank string 215 206 if i == 0 { 216 - style = style.Foreground(lipgloss.Color("220")) // Gold 207 + coloredRank = lipgloss.NewStyle().Foreground(lipgloss.Color("220")).Render(rank) // Gold 217 208 } else if i == 1 { 218 - style = style.Foreground(lipgloss.Color("250")) // Silver 209 + coloredRank = lipgloss.NewStyle().Foreground(lipgloss.Color("250")).Render(rank) // Silver 219 210 } else if i == 2 { 220 - style = style.Foreground(lipgloss.Color("208")) // Bronze 211 + coloredRank = lipgloss.NewStyle().Foreground(lipgloss.Color("208")).Render(rank) // Bronze 212 + } else { 213 + coloredRank = rank 221 214 } 222 - 223 - b.WriteString(style.Render(line)) 215 + 216 + // Format line with proper spacing, only rank is colored 217 + b.WriteString(fmt.Sprintf("%-4s %-20s %8d %8d %9.2f%%\n", 218 + coloredRank, entry.Username, entry.Wins, entry.Losses, winRate)) 224 219 } 225 220 226 221 return b.String() ··· 228 223 229 224 func renderBracket(matches []MatchResult) string { 230 225 var b strings.Builder 231 - b.WriteString(lipgloss.NewStyle().Bold(true).Render("⚔️ Recent Matches") + "\n\n") 226 + b.WriteString(lipgloss.NewStyle().Bold(true).Render("⚔️ Tournament Bracket") + "\n\n") 232 227 233 228 if len(matches) == 0 { 234 229 return b.String() 235 230 } 236 231 237 - // Show most recent matches (up to 10) 238 - displayCount := len(matches) 239 - if displayCount > 10 { 240 - displayCount = 10 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 + } 241 243 } 242 244 243 - for i := 0; i < displayCount; i++ { 244 - match := matches[i] 245 + // Display up to 8 matchups in bracket format 246 + count := 0 247 + for _, match := range matchups { 248 + if count >= 8 { 249 + break 250 + } 245 251 246 - // Determine styling based on winner 247 - player1Style := lipgloss.NewStyle() 248 - player2Style := lipgloss.NewStyle() 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) 249 260 261 + var winner string 250 262 if match.WinnerUsername == match.Player1Username { 251 263 player1Style = player1Style.Foreground(lipgloss.Color("green")).Bold(true) 252 - player2Style = player2Style.Foreground(lipgloss.Color("240")) 264 + winner = match.Player1Username 253 265 } else { 254 266 player2Style = player2Style.Foreground(lipgloss.Color("green")).Bold(true) 255 - player1Style = player1Style.Foreground(lipgloss.Color("240")) 267 + winner = match.Player2Username 256 268 } 257 269 258 - // Format: [Player1] ──vs── [Player2] → Winner (avg moves) 259 - player1Str := player1Style.Render(fmt.Sprintf("%-15s", match.Player1Username)) 260 - vsStr := lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Render(" ──vs── ") 261 - player2Str := player2Style.Render(fmt.Sprintf("%-15s", match.Player2Username)) 270 + // Format bracket style 271 + // Player1 ┐ 272 + // ├── Winner 273 + // Player2 ┘ 262 274 263 - winnerMark := "→" 264 - winnerStr := lipgloss.NewStyle().Foreground(lipgloss.Color("green")).Render( 265 - fmt.Sprintf("%s %s wins (avg %d moves)", winnerMark, match.WinnerUsername, match.AvgMoves)) 275 + player1Box := lipgloss.NewStyle(). 276 + Border(lipgloss.RoundedBorder()). 277 + Padding(0, 1). 278 + Width(15) 266 279 267 - b.WriteString(fmt.Sprintf("%s%s%s %s\n", player1Str, vsStr, player2Str, winnerStr)) 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))) 268 305 } 269 306 270 307 return b.String()
+133 -1
web.go
··· 14 14 <title>Battleship Arena - Leaderboard</title> 15 15 <meta charset="UTF-8"> 16 16 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 17 + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/brackets-viewer@latest/dist/brackets-viewer.min.css" /> 17 18 <style> 18 19 body { 19 20 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; ··· 114 115 font-size: 0.9em; 115 116 margin-top: 20px; 116 117 } 118 + .bracket-section { 119 + margin: 40px 0; 120 + background: white; 121 + padding: 20px; 122 + border-radius: 12px; 123 + } 124 + .bracket-section h2 { 125 + text-align: center; 126 + color: #333; 127 + margin-bottom: 30px; 128 + } 117 129 </style> 130 + <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/brackets-viewer@latest/dist/brackets-viewer.min.js"></script> 118 131 <script> 119 132 // Auto-refresh every 30 seconds 120 133 setTimeout(() => location.reload(), 30000); 134 + 135 + // Load and render bracket data 136 + window.addEventListener('DOMContentLoaded', async () => { 137 + try { 138 + const response = await fetch('/api/bracket'); 139 + const data = await response.json(); 140 + 141 + if (data.matches && data.matches.length > 0) { 142 + window.bracketsViewer.render({ 143 + stages: data.stages, 144 + matches: data.matches, 145 + matchGames: data.matchGames, 146 + participants: data.participants, 147 + }); 148 + } 149 + } catch (error) { 150 + console.error('Failed to load bracket data:', error); 151 + } 152 + }); 121 153 </script> 122 154 </head> 123 155 <body> 124 156 <div class="container"> 125 157 <h1>🚢 Battleship Arena</h1> 126 - <p class="subtitle">Smart AI Competition Leaderboard</p> 158 + <p class="subtitle">Smart AI Competition</p> 159 + 160 + <div class="bracket-section"> 161 + <h2>⚔️ Tournament Bracket</h2> 162 + <div class="brackets-viewer"></div> 163 + </div> 127 164 165 + <h2 style="text-align: center; color: #333; margin-top: 60px;">📊 Rankings</h2> 128 166 <table> 129 167 <thead> 130 168 <tr> ··· 233 271 if entries == nil { 234 272 entries = []LeaderboardEntry{} 235 273 } 274 + 275 + // Get matches for bracket 276 + matches, err := getAllMatches() 277 + if err != nil { 278 + matches = []MatchResult{} 279 + } 236 280 237 281 data := struct { 238 282 Entries []LeaderboardEntry 283 + Matches []MatchResult 239 284 TotalPlayers int 240 285 TotalGames int 241 286 }{ 242 287 Entries: entries, 288 + Matches: matches, 243 289 TotalPlayers: len(entries), 244 290 TotalGames: calculateTotalGames(entries), 245 291 } ··· 263 309 264 310 w.Header().Set("Content-Type", "application/json") 265 311 json.NewEncoder(w).Encode(entries) 312 + } 313 + 314 + func handleBracketData(w http.ResponseWriter, r *http.Request) { 315 + matches, err := getAllMatches() 316 + if err != nil { 317 + http.Error(w, fmt.Sprintf("Failed to load matches: %v", err), http.StatusInternalServerError) 318 + return 319 + } 320 + 321 + if matches == nil { 322 + matches = []MatchResult{} 323 + } 324 + 325 + // Get unique participants 326 + participantMap := make(map[string]int) 327 + participants := []map[string]interface{}{} 328 + participantID := 1 329 + 330 + for _, match := range matches { 331 + if _, exists := participantMap[match.Player1Username]; !exists { 332 + participantMap[match.Player1Username] = participantID 333 + participants = append(participants, map[string]interface{}{ 334 + "id": participantID, 335 + "name": match.Player1Username, 336 + }) 337 + participantID++ 338 + } 339 + if _, exists := participantMap[match.Player2Username]; !exists { 340 + participantMap[match.Player2Username] = participantID 341 + participants = append(participants, map[string]interface{}{ 342 + "id": participantID, 343 + "name": match.Player2Username, 344 + }) 345 + participantID++ 346 + } 347 + } 348 + 349 + // Create match data in brackets-viewer format 350 + bracketMatches := []map[string]interface{}{} 351 + for i, match := range matches { 352 + opponent1 := map[string]interface{}{ 353 + "id": participantMap[match.Player1Username], 354 + "result": "loss", 355 + } 356 + opponent2 := map[string]interface{}{ 357 + "id": participantMap[match.Player2Username], 358 + "result": "loss", 359 + } 360 + 361 + if match.WinnerUsername == match.Player1Username { 362 + opponent1["result"] = "win" 363 + } else { 364 + opponent2["result"] = "win" 365 + } 366 + 367 + bracketMatches = append(bracketMatches, map[string]interface{}{ 368 + "id": i + 1, 369 + "stage_id": 1, 370 + "group_id": 1, 371 + "round_id": 1, 372 + "number": i + 1, 373 + "opponent1": opponent1, 374 + "opponent2": opponent2, 375 + "status": "completed", 376 + }) 377 + } 378 + 379 + // Create stage data 380 + stages := []map[string]interface{}{ 381 + { 382 + "id": 1, 383 + "name": "Round Robin", 384 + "type": "round_robin", 385 + "number": 1, 386 + }, 387 + } 388 + 389 + data := map[string]interface{}{ 390 + "stages": stages, 391 + "matches": bracketMatches, 392 + "matchGames": []map[string]interface{}{}, 393 + "participants": participants, 394 + } 395 + 396 + w.Header().Set("Content-Type", "application/json") 397 + json.NewEncoder(w).Encode(data) 266 398 } 267 399 268 400 func calculateTotalGames(entries []LeaderboardEntry) int {