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), th:nth-child(4) { width: 100px; } 156 th:nth-child(5) { width: 120px; } 157 th:nth-child(6) { width: 120px; } 158 th:last-child { width: 150px; } 159 160 tbody tr { 161 border-bottom: 1px solid #334155; 162 transition: background 0.2s; 163 } 164 165 tbody tr:hover { 166 background: rgba(59, 130, 246, 0.05); 167 } 168 169 tbody tr:last-child { 170 border-bottom: none; 171 } 172 173 td { 174 padding: 1.25rem 1.5rem; 175 font-size: 0.9375rem; 176 } 177 178 .rank { 179 font-size: 1.25rem; 180 font-weight: 700; 181 } 182 183 .rank-1 { color: #fbbf24; } 184 .rank-2 { color: #d1d5db; } 185 .rank-3 { color: #f59e0b; } 186 187 .player-name { 188 font-weight: 600; 189 color: #e2e8f0; 190 } 191 192 .win-rate { 193 font-weight: 600; 194 padding: 0.25rem 0.75rem; 195 border-radius: 0.375rem; 196 display: inline-block; 197 } 198 199 .win-rate-high { 200 background: rgba(16, 185, 129, 0.1); 201 color: #10b981; 202 } 203 204 .win-rate-med { 205 background: rgba(245, 158, 11, 0.1); 206 color: #f59e0b; 207 } 208 209 .win-rate-low { 210 background: rgba(239, 68, 68, 0.1); 211 color: #ef4444; 212 } 213 214 .info-card { 215 background: #1e293b; 216 border: 1px solid #334155; 217 border-radius: 0.75rem; 218 padding: 2rem; 219 } 220 221 .info-card h3 { 222 font-size: 1.25rem; 223 margin-bottom: 1rem; 224 color: #e2e8f0; 225 } 226 227 .info-card p { 228 color: #94a3b8; 229 line-height: 1.6; 230 margin-bottom: 0.75rem; 231 } 232 233 code { 234 background: #0f172a; 235 padding: 0.375rem 0.75rem; 236 border-radius: 0.375rem; 237 font-family: 'Monaco', 'Courier New', monospace; 238 font-size: 0.875rem; 239 color: #3b82f6; 240 } 241 242 .empty-state { 243 text-align: center; 244 padding: 4rem 2rem; 245 color: #64748b; 246 } 247 248 .empty-state-icon { 249 font-size: 3rem; 250 margin-bottom: 1rem; 251 } 252 253 @media (max-width: 768px) { 254 h1 { font-size: 2rem; } 255 .subtitle { font-size: 1rem; } 256 th, td { padding: 0.75rem 1rem; font-size: 0.875rem; } 257 .stat-value { font-size: 2rem; } 258 } 259 </style> 260 <script> 261 let eventSource; 262 263 function connectSSE() { 264 console.log('Connecting to SSE...'); 265 eventSource = new EventSource('http://localhost:8081'); 266 267 eventSource.onopen = () => { 268 console.log('SSE connection established'); 269 document.querySelector('.status-bar').style.borderColor = 'rgba(16, 185, 129, 0.4)'; 270 }; 271 272 eventSource.onmessage = (event) => { 273 try { 274 const entries = JSON.parse(event.data); 275 console.log('Updating leaderboard with', entries.length, 'entries'); 276 updateLeaderboard(entries); 277 } catch (error) { 278 console.error('Failed to parse SSE data:', error); 279 } 280 }; 281 282 eventSource.onerror = (error) => { 283 console.error('SSE error, reconnecting...', error); 284 document.querySelector('.status-bar').style.borderColor = 'rgba(239, 68, 68, 0.4)'; 285 eventSource.close(); 286 setTimeout(connectSSE, 5000); 287 }; 288 } 289 290 function updateLeaderboard(entries) { 291 const tbody = document.querySelector('tbody'); 292 if (!tbody) return; 293 294 if (entries.length === 0) { 295 tbody.innerHTML = '<tr><td colspan="7"><div class="empty-state"><div class="empty-state-icon">🎯</div><div>No submissions yet. Be the first to compete!</div></div></td></tr>'; 296 return; 297 } 298 299 tbody.innerHTML = entries.map((e, i) => { 300 const rank = i + 1; 301 const total = e.Wins + e.Losses; 302 const winRate = total === 0 ? 0 : ((e.Wins / total) * 100).toFixed(1); 303 const winRateClass = winRate >= 60 ? 'win-rate-high' : winRate >= 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>' + e.Wins.toLocaleString() + '</td>' + 317 '<td>' + e.Losses.toLocaleString() + '</td>' + 318 '<td><span class="win-rate ' + winRateClass + '">' + winRate + '%</span></td>' + 319 '<td>' + e.AvgMoves.toFixed(1) + '</td>' + 320 '<td style="color: #64748b;">' + lastPlayed + '</td>' + 321 '</tr>'; 322 }).join(''); 323 324 // Update stats 325 const statValues = document.querySelectorAll('.stat-value'); 326 statValues[0].textContent = entries.length; 327 const totalGames = entries.reduce((sum, e) => sum + e.Wins + e.Losses, 0); 328 statValues[1].textContent = totalGames.toLocaleString(); 329 } 330 331 window.addEventListener('DOMContentLoaded', () => { 332 connectSSE(); 333 }); 334 </script> 335</head> 336<body> 337 <div class="container"> 338 <header> 339 <h1>⚓ BATTLESHIP ARENA</h1> 340 <p class="subtitle">AI Strategy Competition</p> 341 </header> 342 343 <div class="status-bar"> 344 <div class="live-dot"></div> 345 <span>Live Updates</span> 346 </div> 347 348 <div class="stats-grid"> 349 <div class="stat-card"> 350 <div class="stat-value">{{.TotalPlayers}}</div> 351 <div class="stat-label">Active Players</div> 352 </div> 353 <div class="stat-card"> 354 <div class="stat-value">{{.TotalGames}}</div> 355 <div class="stat-label">Games Played</div> 356 </div> 357 </div> 358 359 <div class="leaderboard"> 360 <div class="leaderboard-header"> 361 <h2>🏆 Leaderboard</h2> 362 </div> 363 <table> 364 <thead> 365 <tr> 366 <th>Rank</th> 367 <th>Player</th> 368 <th>Wins</th> 369 <th>Losses</th> 370 <th>Win Rate</th> 371 <th>Avg Moves</th> 372 <th>Last Active</th> 373 </tr> 374 </thead> 375 <tbody> 376 {{if .Entries}} 377 {{range $i, $e := .Entries}} 378 <tr> 379 <td class="rank rank-{{add $i 1}}">{{if lt $i 3}}{{medal $i}}{{else}}{{add $i 1}}{{end}}</td> 380 <td class="player-name">{{$e.Username}}</td> 381 <td>{{$e.Wins}}</td> 382 <td>{{$e.Losses}}</td> 383 <td><span class="win-rate {{winRateClass $e}}">{{winRate $e}}%</span></td> 384 <td>{{printf "%.1f" $e.AvgMoves}}</td> 385 <td style="color: #64748b;">{{$e.LastPlayed.Format "Jan 2, 3:04 PM"}}</td> 386 </tr> 387 {{end}} 388 {{else}} 389 <tr> 390 <td colspan="7"> 391 <div class="empty-state"> 392 <div class="empty-state-icon">🎯</div> 393 <div>No submissions yet. Be the first to compete!</div> 394 </div> 395 </td> 396 </tr> 397 {{end}} 398 </tbody> 399 </table> 400 </div> 401 402 <div class="info-card"> 403 <h3>📤 How to Submit</h3> 404 <p>Connect via SSH to submit your battleship AI:</p> 405 <p><code>ssh -p 2222 username@localhost</code></p> 406 <p style="margin-top: 1rem;">Upload your <code>memory_functions_*.cpp</code> file and compete in the arena!</p> 407 </div> 408 </div> 409</body> 410</html> 411` 412 413var tmpl = template.Must(template.New("leaderboard").Funcs(template.FuncMap{ 414 "add": func(a, b int) int { 415 return a + b 416 }, 417 "medal": func(i int) string { 418 medals := []string{"🥇", "🥈", "🥉"} 419 if i < len(medals) { 420 return medals[i] 421 } 422 return "" 423 }, 424 "winRate": func(e LeaderboardEntry) string { 425 total := e.Wins + e.Losses 426 if total == 0 { 427 return "0.0" 428 } 429 rate := float64(e.Wins) / float64(total) * 100 430 return formatFloat(rate, 1) 431 }, 432 "winRateClass": func(e LeaderboardEntry) string { 433 total := e.Wins + e.Losses 434 if total == 0 { 435 return "win-rate-low" 436 } 437 rate := float64(e.Wins) / float64(total) * 100 438 if rate >= 60 { 439 return "win-rate-high" 440 } else if rate >= 40 { 441 return "win-rate-med" 442 } 443 return "win-rate-low" 444 }, 445}).Parse(leaderboardHTML)) 446 447func formatFloat(f float64, decimals int) string { 448 return fmt.Sprintf("%.1f", f) 449} 450 451func handleLeaderboard(w http.ResponseWriter, r *http.Request) { 452 entries, err := getLeaderboard(50) 453 if err != nil { 454 http.Error(w, fmt.Sprintf("Failed to load leaderboard: %v", err), http.StatusInternalServerError) 455 return 456 } 457 458 // Empty leaderboard is fine 459 if entries == nil { 460 entries = []LeaderboardEntry{} 461 } 462 463 // Get matches for bracket 464 matches, err := getAllMatches() 465 if err != nil { 466 matches = []MatchResult{} 467 } 468 469 data := struct { 470 Entries []LeaderboardEntry 471 Matches []MatchResult 472 TotalPlayers int 473 TotalGames int 474 }{ 475 Entries: entries, 476 Matches: matches, 477 TotalPlayers: len(entries), 478 TotalGames: calculateTotalGames(entries), 479 } 480 481 if err := tmpl.Execute(w, data); err != nil { 482 http.Error(w, fmt.Sprintf("Template error: %v", err), http.StatusInternalServerError) 483 } 484} 485 486func handleAPILeaderboard(w http.ResponseWriter, r *http.Request) { 487 entries, err := getLeaderboard(50) 488 if err != nil { 489 http.Error(w, fmt.Sprintf("Failed to load leaderboard: %v", err), http.StatusInternalServerError) 490 return 491 } 492 493 // Empty leaderboard is fine 494 if entries == nil { 495 entries = []LeaderboardEntry{} 496 } 497 498 w.Header().Set("Content-Type", "application/json") 499 json.NewEncoder(w).Encode(entries) 500} 501 502 503 504func calculateTotalGames(entries []LeaderboardEntry) int { 505 total := 0 506 for _, e := range entries { 507 total += e.Wins + e.Losses 508 } 509 return total / 2 // Each game counted twice (win+loss) 510}