a geicko-2 based round robin ranking system designed to test c++ battleship submissions battleship.dunkirk.sh
1package main 2 3import ( 4 "encoding/json" 5 "fmt" 6 "html/template" 7 "net/http" 8) 9 10const leaderboardHTML = ` 11<!DOCTYPE html> 12<html lang="en"> 13<head> 14 <title>Battleship Arena</title> 15 <meta charset="UTF-8"> 16 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 17 <style> 18 * { 19 margin: 0; 20 padding: 0; 21 box-sizing: border-box; 22 } 23 24 body { 25 font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; 26 background: #0f172a; 27 color: #e2e8f0; 28 min-height: 100vh; 29 padding: 2rem 1rem; 30 } 31 32 .container { 33 max-width: 1400px; 34 margin: 0 auto; 35 } 36 37 header { 38 text-align: center; 39 margin-bottom: 3rem; 40 } 41 42 h1 { 43 font-size: 3rem; 44 font-weight: 800; 45 background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 50%, #ec4899 100%); 46 -webkit-background-clip: text; 47 -webkit-text-fill-color: transparent; 48 background-clip: text; 49 margin-bottom: 0.5rem; 50 } 51 52 .subtitle { 53 font-size: 1.125rem; 54 color: #94a3b8; 55 } 56 57 .status-bar { 58 display: flex; 59 align-items: center; 60 justify-content: center; 61 gap: 0.5rem; 62 margin: 1.5rem 0; 63 padding: 0.75rem; 64 background: rgba(16, 185, 129, 0.1); 65 border: 1px solid rgba(16, 185, 129, 0.2); 66 border-radius: 0.5rem; 67 font-size: 0.875rem; 68 color: #10b981; 69 } 70 71 .live-dot { 72 width: 8px; 73 height: 8px; 74 background: #10b981; 75 border-radius: 50%; 76 animation: pulse 2s ease-in-out infinite; 77 } 78 79 @keyframes pulse { 80 0%, 100% { opacity: 1; transform: scale(1); } 81 50% { opacity: 0.5; transform: scale(1.1); } 82 } 83 84 .stats-grid { 85 display: grid; 86 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 87 gap: 1.5rem; 88 margin-bottom: 2rem; 89 } 90 91 .stat-card { 92 background: #1e293b; 93 border: 1px solid #334155; 94 border-radius: 0.75rem; 95 padding: 1.5rem; 96 text-align: center; 97 } 98 99 .stat-value { 100 font-size: 2.5rem; 101 font-weight: 700; 102 background: linear-gradient(135deg, #3b82f6, #8b5cf6); 103 -webkit-background-clip: text; 104 -webkit-text-fill-color: transparent; 105 background-clip: text; 106 } 107 108 .stat-label { 109 font-size: 0.875rem; 110 color: #94a3b8; 111 margin-top: 0.5rem; 112 text-transform: uppercase; 113 letter-spacing: 0.05em; 114 } 115 116 .leaderboard { 117 background: #1e293b; 118 border: 1px solid #334155; 119 border-radius: 0.75rem; 120 overflow: hidden; 121 margin-bottom: 2rem; 122 } 123 124 .leaderboard-header { 125 padding: 1.5rem; 126 background: linear-gradient(135deg, #1e293b 0%, #334155 100%); 127 border-bottom: 1px solid #334155; 128 } 129 130 .leaderboard-header h2 { 131 font-size: 1.5rem; 132 font-weight: 700; 133 } 134 135 table { 136 width: 100%; 137 border-collapse: collapse; 138 } 139 140 thead { 141 background: #0f172a; 142 } 143 144 th { 145 padding: 1rem 1.5rem; 146 text-align: left; 147 font-size: 0.75rem; 148 font-weight: 600; 149 color: #94a3b8; 150 text-transform: uppercase; 151 letter-spacing: 0.05em; 152 } 153 154 th:first-child { width: 80px; } 155 th:nth-child(3) { width: 90px; } /* ELO */ 156 th:nth-child(4), th:nth-child(5) { width: 100px; } /* Wins, Losses */ 157 th:nth-child(6) { width: 120px; } /* Win Rate */ 158 th:nth-child(7) { width: 120px; } /* Avg Moves */ 159 th:last-child { width: 150px; } /* Last Active */ 160 161 tbody tr { 162 border-bottom: 1px solid #334155; 163 transition: background 0.2s; 164 } 165 166 tbody tr:hover { 167 background: rgba(59, 130, 246, 0.05); 168 } 169 170 tbody tr:last-child { 171 border-bottom: none; 172 } 173 174 td { 175 padding: 1.25rem 1.5rem; 176 font-size: 0.9375rem; 177 } 178 179 .rank { 180 font-size: 1.25rem; 181 font-weight: 700; 182 } 183 184 .rank-1 { color: #fbbf24; } 185 .rank-2 { color: #d1d5db; } 186 .rank-3 { color: #f59e0b; } 187 188 .player-name { 189 font-weight: 600; 190 color: #e2e8f0; 191 } 192 193 .win-rate { 194 font-weight: 600; 195 padding: 0.25rem 0.75rem; 196 border-radius: 0.375rem; 197 display: inline-block; 198 } 199 200 .win-rate-high { 201 background: rgba(16, 185, 129, 0.1); 202 color: #10b981; 203 } 204 205 .win-rate-med { 206 background: rgba(245, 158, 11, 0.1); 207 color: #f59e0b; 208 } 209 210 .win-rate-low { 211 background: rgba(239, 68, 68, 0.1); 212 color: #ef4444; 213 } 214 215 .info-card { 216 background: #1e293b; 217 border: 1px solid #334155; 218 border-radius: 0.75rem; 219 padding: 2rem; 220 } 221 222 .info-card h3 { 223 font-size: 1.25rem; 224 margin-bottom: 1rem; 225 color: #e2e8f0; 226 } 227 228 .info-card p { 229 color: #94a3b8; 230 line-height: 1.6; 231 margin-bottom: 0.75rem; 232 } 233 234 code { 235 background: #0f172a; 236 padding: 0.375rem 0.75rem; 237 border-radius: 0.375rem; 238 font-family: 'Monaco', 'Courier New', monospace; 239 font-size: 0.875rem; 240 color: #3b82f6; 241 } 242 243 .empty-state { 244 text-align: center; 245 padding: 4rem 2rem; 246 color: #64748b; 247 } 248 249 .empty-state-icon { 250 font-size: 3rem; 251 margin-bottom: 1rem; 252 } 253 254 @media (max-width: 768px) { 255 h1 { font-size: 2rem; } 256 .subtitle { font-size: 1rem; } 257 th, td { padding: 0.75rem 1rem; font-size: 0.875rem; } 258 .stat-value { font-size: 2rem; } 259 } 260 </style> 261 <script> 262 let eventSource; 263 264 function connectSSE() { 265 console.log('Connecting to SSE...'); 266 eventSource = new EventSource('http://localhost:8081'); 267 268 eventSource.onopen = () => { 269 console.log('SSE connection established'); 270 document.querySelector('.status-bar').style.borderColor = 'rgba(16, 185, 129, 0.4)'; 271 }; 272 273 eventSource.onmessage = (event) => { 274 try { 275 const entries = JSON.parse(event.data); 276 console.log('Updating leaderboard with', entries.length, 'entries'); 277 updateLeaderboard(entries); 278 } catch (error) { 279 console.error('Failed to parse SSE data:', error); 280 } 281 }; 282 283 eventSource.onerror = (error) => { 284 console.error('SSE error, reconnecting...', error); 285 document.querySelector('.status-bar').style.borderColor = 'rgba(239, 68, 68, 0.4)'; 286 eventSource.close(); 287 setTimeout(connectSSE, 5000); 288 }; 289 } 290 291 function updateLeaderboard(entries) { 292 const tbody = document.querySelector('tbody'); 293 if (!tbody) return; 294 295 if (entries.length === 0) { 296 tbody.innerHTML = '<tr><td colspan="8"><div class="empty-state"><div class="empty-state-icon">🎯</div><div>No submissions yet. Be the first to compete!</div></div></td></tr>'; 297 return; 298 } 299 300 tbody.innerHTML = entries.map((e, i) => { 301 const rank = i + 1; 302 const winRate = e.WinPct.toFixed(1); 303 const winRateClass = e.WinPct >= 60 ? 'win-rate-high' : e.WinPct >= 40 ? 'win-rate-med' : 'win-rate-low'; 304 const medals = ['🥇', '🥈', '🥉']; 305 const medal = medals[i] || rank; 306 const lastPlayed = new Date(e.LastPlayed).toLocaleString('en-US', { 307 month: 'short', 308 day: 'numeric', 309 hour: 'numeric', 310 minute: '2-digit' 311 }); 312 313 return '<tr>' + 314 '<td class="rank rank-' + rank + '">' + medal + '</td>' + 315 '<td class="player-name">' + e.Username + '</td>' + 316 '<td><strong>' + e.Rating + '</strong> <span style="color: #94a3b8; font-size: 0.85em;">±' + e.RD + '</span></td>' + 317 '<td>' + e.Wins.toLocaleString() + '</td>' + 318 '<td>' + e.Losses.toLocaleString() + '</td>' + 319 '<td><span class="win-rate ' + winRateClass + '">' + winRate + '%</span></td>' + 320 '<td>' + e.AvgMoves.toFixed(1) + '</td>' + 321 '<td style="color: #64748b;">' + lastPlayed + '</td>' + 322 '</tr>'; 323 }).join(''); 324 325 // Update stats 326 const statValues = document.querySelectorAll('.stat-value'); 327 statValues[0].textContent = entries.length; 328 const totalGames = entries.reduce((sum, e) => sum + e.Wins + e.Losses, 0); 329 statValues[1].textContent = totalGames.toLocaleString(); 330 } 331 332 window.addEventListener('DOMContentLoaded', () => { 333 connectSSE(); 334 }); 335 </script> 336</head> 337<body> 338 <div class="container"> 339 <header> 340 <h1>⚓ BATTLESHIP ARENA</h1> 341 <p class="subtitle">AI Strategy Competition</p> 342 </header> 343 344 <div class="status-bar"> 345 <div class="live-dot"></div> 346 <span>Live Updates</span> 347 </div> 348 349 <div class="stats-grid"> 350 <div class="stat-card"> 351 <div class="stat-value">{{.TotalPlayers}}</div> 352 <div class="stat-label">Active Players</div> 353 </div> 354 <div class="stat-card"> 355 <div class="stat-value">{{.TotalGames}}</div> 356 <div class="stat-label">Games Played</div> 357 </div> 358 </div> 359 360 <div class="leaderboard"> 361 <div class="leaderboard-header"> 362 <h2>🏆 Leaderboard</h2> 363 </div> 364 <table> 365 <thead> 366 <tr> 367 <th>Rank</th> 368 <th>Player</th> 369 <th>Rating</th> 370 <th>Wins</th> 371 <th>Losses</th> 372 <th>Win Rate</th> 373 <th>Avg Moves</th> 374 <th>Last Active</th> 375 </tr> 376 </thead> 377 <tbody> 378 {{if .Entries}} 379 {{range $i, $e := .Entries}} 380 <tr> 381 <td class="rank rank-{{add $i 1}}">{{if lt $i 3}}{{medal $i}}{{else}}{{add $i 1}}{{end}}</td> 382 <td class="player-name">{{$e.Username}}</td> 383 <td><strong>{{$e.Rating}}</strong> <span style="color: #94a3b8; font-size: 0.85em;">±{{$e.RD}}</span></td> 384 <td>{{$e.Wins}}</td> 385 <td>{{$e.Losses}}</td> 386 <td><span class="win-rate {{winRateClass $e}}">{{winRate $e}}%</span></td> 387 <td>{{printf "%.1f" $e.AvgMoves}}</td> 388 <td style="color: #64748b;">{{$e.LastPlayed.Format "Jan 2, 3:04 PM"}}</td> 389 </tr> 390 {{end}} 391 {{else}} 392 <tr> 393 <td colspan="8"> 394 <div class="empty-state"> 395 <div class="empty-state-icon">🎯</div> 396 <div>No submissions yet. Be the first to compete!</div> 397 </div> 398 </td> 399 </tr> 400 {{end}} 401 </tbody> 402 </table> 403 </div> 404 405 <div class="info-card"> 406 <h3>📤 How to Submit</h3> 407 <p>Connect via SSH to submit your battleship AI:</p> 408 <p><code>ssh -p 2222 username@localhost</code></p> 409 <p style="margin-top: 1rem;">Upload your <code>memory_functions_*.cpp</code> file and compete in the arena!</p> 410 </div> 411 </div> 412</body> 413</html> 414` 415 416var tmpl = template.Must(template.New("leaderboard").Funcs(template.FuncMap{ 417 "add": func(a, b int) int { 418 return a + b 419 }, 420 "medal": func(i int) string { 421 medals := []string{"🥇", "🥈", "🥉"} 422 if i < len(medals) { 423 return medals[i] 424 } 425 return "" 426 }, 427 "winRate": func(e LeaderboardEntry) string { 428 return formatFloat(e.WinPct, 1) 429 }, 430 "winRateClass": func(e LeaderboardEntry) string { 431 if e.WinPct >= 60 { 432 return "win-rate-high" 433 } else if e.WinPct >= 40 { 434 return "win-rate-med" 435 } 436 return "win-rate-low" 437 }, 438}).Parse(leaderboardHTML)) 439 440func formatFloat(f float64, decimals int) string { 441 return fmt.Sprintf("%.1f", f) 442} 443 444func handleLeaderboard(w http.ResponseWriter, r *http.Request) { 445 entries, err := getLeaderboard(50) 446 if err != nil { 447 http.Error(w, fmt.Sprintf("Failed to load leaderboard: %v", err), http.StatusInternalServerError) 448 return 449 } 450 451 // Empty leaderboard is fine 452 if entries == nil { 453 entries = []LeaderboardEntry{} 454 } 455 456 // Get matches for bracket 457 matches, err := getAllMatches() 458 if err != nil { 459 matches = []MatchResult{} 460 } 461 462 data := struct { 463 Entries []LeaderboardEntry 464 Matches []MatchResult 465 TotalPlayers int 466 TotalGames int 467 }{ 468 Entries: entries, 469 Matches: matches, 470 TotalPlayers: len(entries), 471 TotalGames: calculateTotalGames(entries), 472 } 473 474 if err := tmpl.Execute(w, data); err != nil { 475 http.Error(w, fmt.Sprintf("Template error: %v", err), http.StatusInternalServerError) 476 } 477} 478 479func handleAPILeaderboard(w http.ResponseWriter, r *http.Request) { 480 entries, err := getLeaderboard(50) 481 if err != nil { 482 http.Error(w, fmt.Sprintf("Failed to load leaderboard: %v", err), http.StatusInternalServerError) 483 return 484 } 485 486 // Empty leaderboard is fine 487 if entries == nil { 488 entries = []LeaderboardEntry{} 489 } 490 491 w.Header().Set("Content-Type", "application/json") 492 json.NewEncoder(w).Encode(entries) 493} 494 495 496 497func calculateTotalGames(entries []LeaderboardEntry) int { 498 total := 0 499 for _, e := range entries { 500 total += e.Wins + e.Losses 501 } 502 return total / 2 // Each game counted twice (win+loss) 503}