tangled
alpha
login
or
join now
dunkirk.sh
/
battleship-arena
1
fork
atom
a geicko-2 based round robin ranking system designed to test c++ battleship submissions
battleship.dunkirk.sh
1
fork
atom
overview
issues
pulls
pipelines
feat: seriously revamp the ui
dunkirk.sh
3 months ago
00a56754
0edeb889
verified
This commit was signed with the committer's
known signature
.
dunkirk.sh
SSH Key Fingerprint:
SHA256:DqcG0RXYExE26KiWo3VxJnsxswN1QNfTBvB+bdSpk80=
+286
-173
1 changed file
expand all
collapse all
unified
split
web.go
+286
-173
web.go
···
9
9
10
10
const leaderboardHTML = `
11
11
<!DOCTYPE html>
12
12
-
<html>
12
12
+
<html lang="en">
13
13
<head>
14
14
-
<title>Battleship Arena - Leaderboard</title>
14
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
18
+
* {
19
19
+
margin: 0;
20
20
+
padding: 0;
21
21
+
box-sizing: border-box;
22
22
+
}
23
23
+
18
24
body {
19
19
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
20
20
-
max-width: 1200px;
21
21
-
margin: 0 auto;
22
22
-
padding: 20px;
23
23
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
25
25
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
26
26
+
background: #0f172a;
27
27
+
color: #e2e8f0;
24
28
min-height: 100vh;
29
29
+
padding: 2rem 1rem;
25
30
}
31
31
+
26
32
.container {
27
27
-
background: white;
28
28
-
border-radius: 12px;
29
29
-
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
30
30
-
padding: 40px;
33
33
+
max-width: 1400px;
34
34
+
margin: 0 auto;
31
35
}
32
32
-
h1 {
33
33
-
color: #333;
36
36
+
37
37
+
header {
34
38
text-align: center;
35
35
-
margin-bottom: 10px;
36
36
-
font-size: 2.5em;
39
39
+
margin-bottom: 3rem;
40
40
+
}
41
41
+
42
42
+
h1 {
43
43
+
font-size: 3rem;
44
44
+
font-weight: 800;
45
45
+
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 50%, #ec4899 100%);
46
46
+
-webkit-background-clip: text;
47
47
+
-webkit-text-fill-color: transparent;
48
48
+
background-clip: text;
49
49
+
margin-bottom: 0.5rem;
37
50
}
51
51
+
38
52
.subtitle {
53
53
+
font-size: 1.125rem;
54
54
+
color: #94a3b8;
55
55
+
}
56
56
+
57
57
+
.status-bar {
58
58
+
display: flex;
59
59
+
align-items: center;
60
60
+
justify-content: center;
61
61
+
gap: 0.5rem;
62
62
+
margin: 1.5rem 0;
63
63
+
padding: 0.75rem;
64
64
+
background: rgba(16, 185, 129, 0.1);
65
65
+
border: 1px solid rgba(16, 185, 129, 0.2);
66
66
+
border-radius: 0.5rem;
67
67
+
font-size: 0.875rem;
68
68
+
color: #10b981;
69
69
+
}
70
70
+
71
71
+
.live-dot {
72
72
+
width: 8px;
73
73
+
height: 8px;
74
74
+
background: #10b981;
75
75
+
border-radius: 50%;
76
76
+
animation: pulse 2s ease-in-out infinite;
77
77
+
}
78
78
+
79
79
+
@keyframes pulse {
80
80
+
0%, 100% { opacity: 1; transform: scale(1); }
81
81
+
50% { opacity: 0.5; transform: scale(1.1); }
82
82
+
}
83
83
+
84
84
+
.stats-grid {
85
85
+
display: grid;
86
86
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
87
87
+
gap: 1.5rem;
88
88
+
margin-bottom: 2rem;
89
89
+
}
90
90
+
91
91
+
.stat-card {
92
92
+
background: #1e293b;
93
93
+
border: 1px solid #334155;
94
94
+
border-radius: 0.75rem;
95
95
+
padding: 1.5rem;
39
96
text-align: center;
40
40
-
color: #666;
41
41
-
margin-bottom: 40px;
42
42
-
font-size: 1.1em;
97
97
+
}
98
98
+
99
99
+
.stat-value {
100
100
+
font-size: 2.5rem;
101
101
+
font-weight: 700;
102
102
+
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
103
103
+
-webkit-background-clip: text;
104
104
+
-webkit-text-fill-color: transparent;
105
105
+
background-clip: text;
106
106
+
}
107
107
+
108
108
+
.stat-label {
109
109
+
font-size: 0.875rem;
110
110
+
color: #94a3b8;
111
111
+
margin-top: 0.5rem;
112
112
+
text-transform: uppercase;
113
113
+
letter-spacing: 0.05em;
114
114
+
}
115
115
+
116
116
+
.leaderboard {
117
117
+
background: #1e293b;
118
118
+
border: 1px solid #334155;
119
119
+
border-radius: 0.75rem;
120
120
+
overflow: hidden;
121
121
+
margin-bottom: 2rem;
43
122
}
123
123
+
124
124
+
.leaderboard-header {
125
125
+
padding: 1.5rem;
126
126
+
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
127
127
+
border-bottom: 1px solid #334155;
128
128
+
}
129
129
+
130
130
+
.leaderboard-header h2 {
131
131
+
font-size: 1.5rem;
132
132
+
font-weight: 700;
133
133
+
}
134
134
+
44
135
table {
45
136
width: 100%;
46
137
border-collapse: collapse;
47
47
-
margin-top: 20px;
138
138
+
}
139
139
+
140
140
+
thead {
141
141
+
background: #0f172a;
48
142
}
143
143
+
49
144
th {
50
50
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
51
51
-
color: white;
52
52
-
padding: 15px;
145
145
+
padding: 1rem 1.5rem;
53
146
text-align: left;
147
147
+
font-size: 0.75rem;
54
148
font-weight: 600;
149
149
+
color: #94a3b8;
150
150
+
text-transform: uppercase;
151
151
+
letter-spacing: 0.05em;
55
152
}
153
153
+
154
154
+
th:first-child { width: 80px; }
155
155
+
th:nth-child(3), th:nth-child(4) { width: 100px; }
156
156
+
th:nth-child(5) { width: 120px; }
157
157
+
th:nth-child(6) { width: 120px; }
158
158
+
th:last-child { width: 150px; }
159
159
+
160
160
+
tbody tr {
161
161
+
border-bottom: 1px solid #334155;
162
162
+
transition: background 0.2s;
163
163
+
}
164
164
+
165
165
+
tbody tr:hover {
166
166
+
background: rgba(59, 130, 246, 0.05);
167
167
+
}
168
168
+
169
169
+
tbody tr:last-child {
170
170
+
border-bottom: none;
171
171
+
}
172
172
+
56
173
td {
57
57
-
padding: 12px 15px;
58
58
-
border-bottom: 1px solid #eee;
174
174
+
padding: 1.25rem 1.5rem;
175
175
+
font-size: 0.9375rem;
59
176
}
60
60
-
tr:hover {
61
61
-
background: #f8f9fa;
177
177
+
178
178
+
.rank {
179
179
+
font-size: 1.25rem;
180
180
+
font-weight: 700;
62
181
}
63
63
-
.rank {
64
64
-
font-weight: bold;
65
65
-
font-size: 1.1em;
182
182
+
183
183
+
.rank-1 { color: #fbbf24; }
184
184
+
.rank-2 { color: #d1d5db; }
185
185
+
.rank-3 { color: #f59e0b; }
186
186
+
187
187
+
.player-name {
188
188
+
font-weight: 600;
189
189
+
color: #e2e8f0;
66
190
}
67
67
-
.rank-1 { color: #FFD700; }
68
68
-
.rank-2 { color: #C0C0C0; }
69
69
-
.rank-3 { color: #CD7F32; }
191
191
+
70
192
.win-rate {
71
193
font-weight: 600;
72
72
-
}
73
73
-
.win-rate-high { color: #10b981; }
74
74
-
.win-rate-med { color: #f59e0b; }
75
75
-
.win-rate-low { color: #ef4444; }
76
76
-
.stage {
194
194
+
padding: 0.25rem 0.75rem;
195
195
+
border-radius: 0.375rem;
77
196
display: inline-block;
78
78
-
padding: 4px 12px;
79
79
-
border-radius: 12px;
80
80
-
font-size: 0.85em;
81
81
-
font-weight: 600;
82
197
}
83
83
-
.stage-Expert {
84
84
-
background: #10b981;
85
85
-
color: white;
198
198
+
199
199
+
.win-rate-high {
200
200
+
background: rgba(16, 185, 129, 0.1);
201
201
+
color: #10b981;
86
202
}
87
87
-
.stage-Advanced {
88
88
-
background: #3b82f6;
89
89
-
color: white;
203
203
+
204
204
+
.win-rate-med {
205
205
+
background: rgba(245, 158, 11, 0.1);
206
206
+
color: #f59e0b;
90
207
}
91
91
-
.stage-Intermediate {
92
92
-
background: #f59e0b;
93
93
-
color: white;
208
208
+
209
209
+
.win-rate-low {
210
210
+
background: rgba(239, 68, 68, 0.1);
211
211
+
color: #ef4444;
94
212
}
95
95
-
.stage-Beginner {
96
96
-
background: #6b7280;
97
97
-
color: white;
213
213
+
214
214
+
.info-card {
215
215
+
background: #1e293b;
216
216
+
border: 1px solid #334155;
217
217
+
border-radius: 0.75rem;
218
218
+
padding: 2rem;
98
219
}
99
99
-
.stats {
100
100
-
display: flex;
101
101
-
justify-content: space-around;
102
102
-
margin-top: 40px;
103
103
-
padding-top: 30px;
104
104
-
border-top: 2px solid #eee;
220
220
+
221
221
+
.info-card h3 {
222
222
+
font-size: 1.25rem;
223
223
+
margin-bottom: 1rem;
224
224
+
color: #e2e8f0;
105
225
}
106
106
-
.stat {
107
107
-
text-align: center;
108
108
-
}
109
109
-
.stat-value {
110
110
-
font-size: 2em;
111
111
-
font-weight: bold;
112
112
-
color: #667eea;
113
113
-
}
114
114
-
.stat-label {
115
115
-
color: #666;
116
116
-
margin-top: 5px;
117
117
-
}
118
118
-
.instructions {
119
119
-
background: #f8f9fa;
120
120
-
padding: 20px;
121
121
-
border-radius: 8px;
122
122
-
margin-top: 30px;
123
123
-
}
124
124
-
.instructions h3 {
125
125
-
margin-top: 0;
126
126
-
color: #333;
226
226
+
227
227
+
.info-card p {
228
228
+
color: #94a3b8;
229
229
+
line-height: 1.6;
230
230
+
margin-bottom: 0.75rem;
127
231
}
128
128
-
.instructions code {
129
129
-
background: #e9ecef;
130
130
-
padding: 3px 8px;
131
131
-
border-radius: 4px;
232
232
+
233
233
+
code {
234
234
+
background: #0f172a;
235
235
+
padding: 0.375rem 0.75rem;
236
236
+
border-radius: 0.375rem;
132
237
font-family: 'Monaco', 'Courier New', monospace;
238
238
+
font-size: 0.875rem;
239
239
+
color: #3b82f6;
133
240
}
134
134
-
.refresh-note {
241
241
+
242
242
+
.empty-state {
135
243
text-align: center;
136
136
-
color: #999;
137
137
-
font-size: 0.9em;
138
138
-
margin-top: 20px;
244
244
+
padding: 4rem 2rem;
245
245
+
color: #64748b;
139
246
}
140
140
-
.live-indicator {
141
141
-
display: inline-block;
142
142
-
width: 10px;
143
143
-
height: 10px;
144
144
-
background: #10b981;
145
145
-
border-radius: 50%;
146
146
-
animation: pulse 2s infinite;
147
147
-
margin-right: 8px;
247
247
+
248
248
+
.empty-state-icon {
249
249
+
font-size: 3rem;
250
250
+
margin-bottom: 1rem;
148
251
}
149
149
-
@keyframes pulse {
150
150
-
0%, 100% { opacity: 1; }
151
151
-
50% { opacity: 0.5; }
152
152
-
}
153
153
-
.status-bar {
154
154
-
text-align: center;
155
155
-
color: #10b981;
156
156
-
margin-bottom: 20px;
157
157
-
font-size: 0.9em;
252
252
+
253
253
+
@media (max-width: 768px) {
254
254
+
h1 { font-size: 2rem; }
255
255
+
.subtitle { font-size: 1rem; }
256
256
+
th, td { padding: 0.75rem 1rem; font-size: 0.875rem; }
257
257
+
.stat-value { font-size: 2rem; }
158
258
}
159
259
</style>
160
260
<script>
161
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
168
-
eventSource.onopen = function() {
267
267
+
eventSource.onopen = () => {
169
268
console.log('SSE connection established');
269
269
+
document.querySelector('.status-bar').style.borderColor = 'rgba(16, 185, 129, 0.4)';
170
270
};
171
271
172
172
-
eventSource.onmessage = function(event) {
173
173
-
console.log('SSE message received:', event.data.substring(0, 100) + '...');
272
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
183
-
eventSource.onerror = function(error) {
282
282
+
eventSource.onerror = (error) => {
184
283
console.error('SSE error, reconnecting...', error);
284
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
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
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
203
-
const winRateClass = winRate >= 80 ? 'win-rate-high' : winRate >= 50 ? 'win-rate-med' : 'win-rate-low';
303
303
+
const winRateClass = winRate >= 60 ? 'win-rate-high' : winRate >= 40 ? 'win-rate-med' : 'win-rate-low';
204
304
const medals = ['🥇', '🥈', '🥉'];
205
205
-
const medal = medals[i] || '#' + rank;
206
206
-
const lastPlayed = new Date(e.LastPlayed).toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
305
305
+
const medal = medals[i] || rank;
306
306
+
const lastPlayed = new Date(e.LastPlayed).toLocaleString('en-US', {
307
307
+
month: 'short',
308
308
+
day: 'numeric',
309
309
+
hour: 'numeric',
310
310
+
minute: '2-digit'
311
311
+
});
207
312
208
313
return '<tr>' +
209
314
'<td class="rank rank-' + rank + '">' + medal + '</td>' +
210
210
-
'<td><strong>' + e.Username + '</strong></td>' +
211
211
-
'<td>' + e.Wins + '</td>' +
212
212
-
'<td>' + e.Losses + '</td>' +
213
213
-
'<td class="win-rate ' + winRateClass + '">' + winRate + '%</td>' +
315
315
+
'<td class="player-name">' + e.Username + '</td>' +
316
316
+
'<td>' + e.Wins.toLocaleString() + '</td>' +
317
317
+
'<td>' + e.Losses.toLocaleString() + '</td>' +
318
318
+
'<td><span class="win-rate ' + winRateClass + '">' + winRate + '%</span></td>' +
214
319
'<td>' + e.AvgMoves.toFixed(1) + '</td>' +
215
215
-
'<td>' + lastPlayed + '</td>' +
320
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
222
-
const totalGames = entries.reduce((sum, e) => sum + e.Wins + e.Losses, 0) / 2;
223
223
-
statValues[1].textContent = Math.floor(totalGames);
327
327
+
const totalGames = entries.reduce((sum, e) => sum + e.Wins + e.Losses, 0);
328
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
233
-
<h1>🚢 Battleship Arena</h1>
234
234
-
<p class="subtitle">Smart AI Competition</p>
338
338
+
<header>
339
339
+
<h1>⚓ BATTLESHIP ARENA</h1>
340
340
+
<p class="subtitle">AI Strategy Competition</p>
341
341
+
</header>
235
342
236
343
<div class="status-bar">
237
237
-
<span class="live-indicator"></span>Live Updates Active
344
344
+
<div class="live-dot"></div>
345
345
+
<span>Live Updates</span>
238
346
</div>
239
347
240
240
-
<h2 style="text-align: center; color: #333;">📊 Rankings</h2>
241
241
-
<table>
242
242
-
<thead>
243
243
-
<tr>
244
244
-
<th>Rank</th>
245
245
-
<th>Player</th>
246
246
-
<th>Wins</th>
247
247
-
<th>Losses</th>
248
248
-
<th>Win Rate</th>
249
249
-
<th>Avg Moves</th>
250
250
-
<th>Last Played</th>
251
251
-
</tr>
252
252
-
</thead>
253
253
-
<tbody>
254
254
-
{{if .Entries}}
255
255
-
{{range $i, $e := .Entries}}
256
256
-
<tr>
257
257
-
<td class="rank rank-{{add $i 1}}">{{if lt $i 3}}{{medal $i}}{{else}}#{{add $i 1}}{{end}}</td>
258
258
-
<td><strong>{{$e.Username}}</strong></td>
259
259
-
<td>{{$e.Wins}}</td>
260
260
-
<td>{{$e.Losses}}</td>
261
261
-
<td class="win-rate {{winRateClass $e}}">{{winRate $e}}%</td>
262
262
-
<td>{{printf "%.1f" $e.AvgMoves}}</td>
263
263
-
<td>{{$e.LastPlayed.Format "Jan 2, 3:04 PM"}}</td>
264
264
-
</tr>
265
265
-
{{end}}
266
266
-
{{else}}
267
267
-
<tr>
268
268
-
<td colspan="8" style="text-align: center; padding: 40px; color: #999;">
269
269
-
No submissions yet. Be the first to compete!
270
270
-
</td>
271
271
-
</tr>
272
272
-
{{end}}
273
273
-
</tbody>
274
274
-
</table>
275
275
-
276
276
-
<div class="stats">
277
277
-
<div class="stat">
348
348
+
<div class="stats-grid">
349
349
+
<div class="stat-card">
278
350
<div class="stat-value">{{.TotalPlayers}}</div>
279
279
-
<div class="stat-label">Players</div>
351
351
+
<div class="stat-label">Active Players</div>
280
352
</div>
281
281
-
<div class="stat">
353
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
286
-
287
287
-
<div class="instructions">
358
358
+
359
359
+
<div class="leaderboard">
360
360
+
<div class="leaderboard-header">
361
361
+
<h2>🏆 Leaderboard</h2>
362
362
+
</div>
363
363
+
<table>
364
364
+
<thead>
365
365
+
<tr>
366
366
+
<th>Rank</th>
367
367
+
<th>Player</th>
368
368
+
<th>Wins</th>
369
369
+
<th>Losses</th>
370
370
+
<th>Win Rate</th>
371
371
+
<th>Avg Moves</th>
372
372
+
<th>Last Active</th>
373
373
+
</tr>
374
374
+
</thead>
375
375
+
<tbody>
376
376
+
{{if .Entries}}
377
377
+
{{range $i, $e := .Entries}}
378
378
+
<tr>
379
379
+
<td class="rank rank-{{add $i 1}}">{{if lt $i 3}}{{medal $i}}{{else}}{{add $i 1}}{{end}}</td>
380
380
+
<td class="player-name">{{$e.Username}}</td>
381
381
+
<td>{{$e.Wins}}</td>
382
382
+
<td>{{$e.Losses}}</td>
383
383
+
<td><span class="win-rate {{winRateClass $e}}">{{winRate $e}}%</span></td>
384
384
+
<td>{{printf "%.1f" $e.AvgMoves}}</td>
385
385
+
<td style="color: #64748b;">{{$e.LastPlayed.Format "Jan 2, 3:04 PM"}}</td>
386
386
+
</tr>
387
387
+
{{end}}
388
388
+
{{else}}
389
389
+
<tr>
390
390
+
<td colspan="7">
391
391
+
<div class="empty-state">
392
392
+
<div class="empty-state-icon">🎯</div>
393
393
+
<div>No submissions yet. Be the first to compete!</div>
394
394
+
</div>
395
395
+
</td>
396
396
+
</tr>
397
397
+
{{end}}
398
398
+
</tbody>
399
399
+
</table>
400
400
+
</div>
401
401
+
402
402
+
<div class="info-card">
288
403
<h3>📤 How to Submit</h3>
289
289
-
<p>Upload your battleship AI implementation via SSH:</p>
290
290
-
<code>ssh -p 2222 username@localhost</code>
291
291
-
<p style="margin-top: 10px;">Then navigate to upload your <code>memory_functions_*.cpp</code> file.</p>
404
404
+
<p>Connect via SSH to submit your battleship AI:</p>
405
405
+
<p><code>ssh -p 2222 username@localhost</code></p>
406
406
+
<p style="margin-top: 1rem;">Upload your <code>memory_functions_*.cpp</code> file and compete in the arena!</p>
292
407
</div>
293
293
-
294
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
325
-
if rate >= 80 {
438
438
+
if rate >= 60 {
326
439
return "win-rate-high"
327
327
-
} else if rate >= 50 {
440
440
+
} else if rate >= 40 {
328
441
return "win-rate-med"
329
442
}
330
443
return "win-rate-low"