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}