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

feat: seriously revamp the ui

dunkirk.sh 00a56754 0edeb889

verified
+286 -173
+286 -173
web.go
··· 9 9 10 10 const leaderboardHTML = ` 11 11 <!DOCTYPE html> 12 - <html> 12 + <html lang="en"> 13 13 <head> 14 - <title>Battleship Arena - Leaderboard</title> 14 + <title>Battleship Arena</title> 15 15 <meta charset="UTF-8"> 16 16 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 17 17 <style> 18 + * { 19 + margin: 0; 20 + padding: 0; 21 + box-sizing: border-box; 22 + } 23 + 18 24 body { 19 - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; 20 - max-width: 1200px; 21 - margin: 0 auto; 22 - padding: 20px; 23 - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 25 + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; 26 + background: #0f172a; 27 + color: #e2e8f0; 24 28 min-height: 100vh; 29 + padding: 2rem 1rem; 25 30 } 31 + 26 32 .container { 27 - background: white; 28 - border-radius: 12px; 29 - box-shadow: 0 20px 60px rgba(0,0,0,0.3); 30 - padding: 40px; 33 + max-width: 1400px; 34 + margin: 0 auto; 31 35 } 32 - h1 { 33 - color: #333; 36 + 37 + header { 34 38 text-align: center; 35 - margin-bottom: 10px; 36 - font-size: 2.5em; 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; 37 50 } 51 + 38 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; 39 96 text-align: center; 40 - color: #666; 41 - margin-bottom: 40px; 42 - font-size: 1.1em; 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; 43 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 + 44 135 table { 45 136 width: 100%; 46 137 border-collapse: collapse; 47 - margin-top: 20px; 138 + } 139 + 140 + thead { 141 + background: #0f172a; 48 142 } 143 + 49 144 th { 50 - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 51 - color: white; 52 - padding: 15px; 145 + padding: 1rem 1.5rem; 53 146 text-align: left; 147 + font-size: 0.75rem; 54 148 font-weight: 600; 149 + color: #94a3b8; 150 + text-transform: uppercase; 151 + letter-spacing: 0.05em; 55 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 + 56 173 td { 57 - padding: 12px 15px; 58 - border-bottom: 1px solid #eee; 174 + padding: 1.25rem 1.5rem; 175 + font-size: 0.9375rem; 59 176 } 60 - tr:hover { 61 - background: #f8f9fa; 177 + 178 + .rank { 179 + font-size: 1.25rem; 180 + font-weight: 700; 62 181 } 63 - .rank { 64 - font-weight: bold; 65 - font-size: 1.1em; 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; 66 190 } 67 - .rank-1 { color: #FFD700; } 68 - .rank-2 { color: #C0C0C0; } 69 - .rank-3 { color: #CD7F32; } 191 + 70 192 .win-rate { 71 193 font-weight: 600; 72 - } 73 - .win-rate-high { color: #10b981; } 74 - .win-rate-med { color: #f59e0b; } 75 - .win-rate-low { color: #ef4444; } 76 - .stage { 194 + padding: 0.25rem 0.75rem; 195 + border-radius: 0.375rem; 77 196 display: inline-block; 78 - padding: 4px 12px; 79 - border-radius: 12px; 80 - font-size: 0.85em; 81 - font-weight: 600; 82 197 } 83 - .stage-Expert { 84 - background: #10b981; 85 - color: white; 198 + 199 + .win-rate-high { 200 + background: rgba(16, 185, 129, 0.1); 201 + color: #10b981; 86 202 } 87 - .stage-Advanced { 88 - background: #3b82f6; 89 - color: white; 203 + 204 + .win-rate-med { 205 + background: rgba(245, 158, 11, 0.1); 206 + color: #f59e0b; 90 207 } 91 - .stage-Intermediate { 92 - background: #f59e0b; 93 - color: white; 208 + 209 + .win-rate-low { 210 + background: rgba(239, 68, 68, 0.1); 211 + color: #ef4444; 94 212 } 95 - .stage-Beginner { 96 - background: #6b7280; 97 - color: white; 213 + 214 + .info-card { 215 + background: #1e293b; 216 + border: 1px solid #334155; 217 + border-radius: 0.75rem; 218 + padding: 2rem; 98 219 } 99 - .stats { 100 - display: flex; 101 - justify-content: space-around; 102 - margin-top: 40px; 103 - padding-top: 30px; 104 - border-top: 2px solid #eee; 220 + 221 + .info-card h3 { 222 + font-size: 1.25rem; 223 + margin-bottom: 1rem; 224 + color: #e2e8f0; 105 225 } 106 - .stat { 107 - text-align: center; 108 - } 109 - .stat-value { 110 - font-size: 2em; 111 - font-weight: bold; 112 - color: #667eea; 113 - } 114 - .stat-label { 115 - color: #666; 116 - margin-top: 5px; 117 - } 118 - .instructions { 119 - background: #f8f9fa; 120 - padding: 20px; 121 - border-radius: 8px; 122 - margin-top: 30px; 123 - } 124 - .instructions h3 { 125 - margin-top: 0; 126 - color: #333; 226 + 227 + .info-card p { 228 + color: #94a3b8; 229 + line-height: 1.6; 230 + margin-bottom: 0.75rem; 127 231 } 128 - .instructions code { 129 - background: #e9ecef; 130 - padding: 3px 8px; 131 - border-radius: 4px; 232 + 233 + code { 234 + background: #0f172a; 235 + padding: 0.375rem 0.75rem; 236 + border-radius: 0.375rem; 132 237 font-family: 'Monaco', 'Courier New', monospace; 238 + font-size: 0.875rem; 239 + color: #3b82f6; 133 240 } 134 - .refresh-note { 241 + 242 + .empty-state { 135 243 text-align: center; 136 - color: #999; 137 - font-size: 0.9em; 138 - margin-top: 20px; 244 + padding: 4rem 2rem; 245 + color: #64748b; 139 246 } 140 - .live-indicator { 141 - display: inline-block; 142 - width: 10px; 143 - height: 10px; 144 - background: #10b981; 145 - border-radius: 50%; 146 - animation: pulse 2s infinite; 147 - margin-right: 8px; 247 + 248 + .empty-state-icon { 249 + font-size: 3rem; 250 + margin-bottom: 1rem; 148 251 } 149 - @keyframes pulse { 150 - 0%, 100% { opacity: 1; } 151 - 50% { opacity: 0.5; } 152 - } 153 - .status-bar { 154 - text-align: center; 155 - color: #10b981; 156 - margin-bottom: 20px; 157 - font-size: 0.9em; 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; } 158 258 } 159 259 </style> 160 260 <script> 161 - // Server-Sent Events for live updates 162 261 let eventSource; 163 262 164 263 function connectSSE() { 165 264 console.log('Connecting to SSE...'); 166 265 eventSource = new EventSource('http://localhost:8081'); 167 266 168 - eventSource.onopen = function() { 267 + eventSource.onopen = () => { 169 268 console.log('SSE connection established'); 269 + document.querySelector('.status-bar').style.borderColor = 'rgba(16, 185, 129, 0.4)'; 170 270 }; 171 271 172 - eventSource.onmessage = function(event) { 173 - console.log('SSE message received:', event.data.substring(0, 100) + '...'); 272 + eventSource.onmessage = (event) => { 174 273 try { 175 274 const entries = JSON.parse(event.data); 176 275 console.log('Updating leaderboard with', entries.length, 'entries'); ··· 180 279 } 181 280 }; 182 281 183 - eventSource.onerror = function(error) { 282 + eventSource.onerror = (error) => { 184 283 console.error('SSE error, reconnecting...', error); 284 + document.querySelector('.status-bar').style.borderColor = 'rgba(239, 68, 68, 0.4)'; 185 285 eventSource.close(); 186 286 setTimeout(connectSSE, 5000); 187 287 }; ··· 192 292 if (!tbody) return; 193 293 194 294 if (entries.length === 0) { 195 - tbody.innerHTML = '<tr><td colspan="8" style="text-align: center; padding: 40px; color: #999;">No submissions yet. Be the first to compete!</td></tr>'; 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>'; 196 296 return; 197 297 } 198 298 ··· 200 300 const rank = i + 1; 201 301 const total = e.Wins + e.Losses; 202 302 const winRate = total === 0 ? 0 : ((e.Wins / total) * 100).toFixed(1); 203 - const winRateClass = winRate >= 80 ? 'win-rate-high' : winRate >= 50 ? 'win-rate-med' : 'win-rate-low'; 303 + const winRateClass = winRate >= 60 ? 'win-rate-high' : winRate >= 40 ? 'win-rate-med' : 'win-rate-low'; 204 304 const medals = ['🥇', '🥈', '🥉']; 205 - const medal = medals[i] || '#' + rank; 206 - const lastPlayed = new Date(e.LastPlayed).toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }); 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 + }); 207 312 208 313 return '<tr>' + 209 314 '<td class="rank rank-' + rank + '">' + medal + '</td>' + 210 - '<td><strong>' + e.Username + '</strong></td>' + 211 - '<td>' + e.Wins + '</td>' + 212 - '<td>' + e.Losses + '</td>' + 213 - '<td class="win-rate ' + winRateClass + '">' + winRate + '%</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>' + 214 319 '<td>' + e.AvgMoves.toFixed(1) + '</td>' + 215 - '<td>' + lastPlayed + '</td>' + 320 + '<td style="color: #64748b;">' + lastPlayed + '</td>' + 216 321 '</tr>'; 217 322 }).join(''); 218 323 219 324 // Update stats 220 325 const statValues = document.querySelectorAll('.stat-value'); 221 326 statValues[0].textContent = entries.length; 222 - const totalGames = entries.reduce((sum, e) => sum + e.Wins + e.Losses, 0) / 2; 223 - statValues[1].textContent = Math.floor(totalGames); 327 + const totalGames = entries.reduce((sum, e) => sum + e.Wins + e.Losses, 0); 328 + statValues[1].textContent = totalGames.toLocaleString(); 224 329 } 225 330 226 331 window.addEventListener('DOMContentLoaded', () => { ··· 230 335 </head> 231 336 <body> 232 337 <div class="container"> 233 - <h1>🚢 Battleship Arena</h1> 234 - <p class="subtitle">Smart AI Competition</p> 338 + <header> 339 + <h1>⚓ BATTLESHIP ARENA</h1> 340 + <p class="subtitle">AI Strategy Competition</p> 341 + </header> 235 342 236 343 <div class="status-bar"> 237 - <span class="live-indicator"></span>Live Updates Active 344 + <div class="live-dot"></div> 345 + <span>Live Updates</span> 238 346 </div> 239 347 240 - <h2 style="text-align: center; color: #333;">📊 Rankings</h2> 241 - <table> 242 - <thead> 243 - <tr> 244 - <th>Rank</th> 245 - <th>Player</th> 246 - <th>Wins</th> 247 - <th>Losses</th> 248 - <th>Win Rate</th> 249 - <th>Avg Moves</th> 250 - <th>Last Played</th> 251 - </tr> 252 - </thead> 253 - <tbody> 254 - {{if .Entries}} 255 - {{range $i, $e := .Entries}} 256 - <tr> 257 - <td class="rank rank-{{add $i 1}}">{{if lt $i 3}}{{medal $i}}{{else}}#{{add $i 1}}{{end}}</td> 258 - <td><strong>{{$e.Username}}</strong></td> 259 - <td>{{$e.Wins}}</td> 260 - <td>{{$e.Losses}}</td> 261 - <td class="win-rate {{winRateClass $e}}">{{winRate $e}}%</td> 262 - <td>{{printf "%.1f" $e.AvgMoves}}</td> 263 - <td>{{$e.LastPlayed.Format "Jan 2, 3:04 PM"}}</td> 264 - </tr> 265 - {{end}} 266 - {{else}} 267 - <tr> 268 - <td colspan="8" style="text-align: center; padding: 40px; color: #999;"> 269 - No submissions yet. Be the first to compete! 270 - </td> 271 - </tr> 272 - {{end}} 273 - </tbody> 274 - </table> 275 - 276 - <div class="stats"> 277 - <div class="stat"> 348 + <div class="stats-grid"> 349 + <div class="stat-card"> 278 350 <div class="stat-value">{{.TotalPlayers}}</div> 279 - <div class="stat-label">Players</div> 351 + <div class="stat-label">Active Players</div> 280 352 </div> 281 - <div class="stat"> 353 + <div class="stat-card"> 282 354 <div class="stat-value">{{.TotalGames}}</div> 283 355 <div class="stat-label">Games Played</div> 284 356 </div> 285 357 </div> 286 - 287 - <div class="instructions"> 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"> 288 403 <h3>📤 How to Submit</h3> 289 - <p>Upload your battleship AI implementation via SSH:</p> 290 - <code>ssh -p 2222 username@localhost</code> 291 - <p style="margin-top: 10px;">Then navigate to upload your <code>memory_functions_*.cpp</code> file.</p> 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> 292 407 </div> 293 - 294 - <p class="refresh-note">Updates in real-time via Server-Sent Events</p> 295 408 </div> 296 409 </body> 297 410 </html> ··· 322 435 return "win-rate-low" 323 436 } 324 437 rate := float64(e.Wins) / float64(total) * 100 325 - if rate >= 80 { 438 + if rate >= 60 { 326 439 return "win-rate-high" 327 - } else if rate >= 50 { 440 + } else if rate >= 40 { 328 441 return "win-rate-med" 329 442 } 330 443 return "win-rate-low"