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

feat: add pending submissions

dunkirk.sh 8dc2bbe2 cb3204e8

verified
+75 -23
battleship-arena

This is a binary file and will not be displayed.

+49 -20
internal/server/web.go
··· 168 168 transition: background 0.2s; 169 169 } 170 170 171 + tbody tr.pending { 172 + opacity: 0.5; 173 + color: #64748b; 174 + } 175 + 176 + tbody tr.pending .player-name { 177 + color: #64748b; 178 + } 179 + 180 + tbody tr.pending .rank { 181 + color: #64748b !important; 182 + } 183 + 171 184 tbody tr:hover { 172 185 background: rgba(59, 130, 246, 0.05); 173 186 } ··· 436 449 437 450 tbody.innerHTML = entries.map((e, i) => { 438 451 const rank = i + 1; 452 + const isPending = e.IsPending || false; 453 + const rowClass = isPending ? ' class="pending"' : ''; 454 + 455 + let rankDisplay; 456 + if (isPending) { 457 + rankDisplay = '⏳'; 458 + } else { 459 + const medals = ['🥇', '🥈', '🥉']; 460 + rankDisplay = medals[i] || rank; 461 + } 462 + 439 463 const winRate = e.WinPct.toFixed(1); 440 464 const winRateClass = e.WinPct >= 60 ? 'win-rate-high' : e.WinPct >= 40 ? 'win-rate-med' : 'win-rate-low'; 441 - const medals = ['🥇', '🥈', '🥉']; 442 - const medal = medals[i] || rank; 443 - const lastPlayed = new Date(e.LastPlayed).toLocaleString('en-US', { 465 + const lastPlayed = isPending ? 'Waiting...' : new Date(e.LastPlayed).toLocaleString('en-US', { 444 466 month: 'short', 445 467 day: 'numeric', 446 468 hour: 'numeric', 447 469 minute: '2-digit' 448 470 }); 449 471 450 - return '<tr>' + 451 - '<td class="rank rank-' + rank + '">' + medal + '</td>' + 452 - '<td class="player-name"><a href="/user/' + e.Username + '" style="color: inherit; text-decoration: none;">' + e.Username + '</a></td>' + 453 - '<td><strong>' + e.Rating + '</strong> <span style="color: #94a3b8; font-size: 0.85em;">±' + e.RD + '</span></td>' + 454 - '<td>' + e.Wins.toLocaleString() + '</td>' + 455 - '<td>' + e.Losses.toLocaleString() + '</td>' + 456 - '<td><span class="win-rate ' + winRateClass + '">' + winRate + '%</span></td>' + 457 - '<td>' + e.AvgMoves.toFixed(1) + '</td>' + 472 + const nameDisplay = e.Username + (isPending ? ' <span style="font-size: 0.8em;">(pending)</span>' : ''); 473 + const ratingDisplay = isPending ? '-' : '<strong>' + e.Rating + '</strong> <span style="color: #94a3b8; font-size: 0.85em;">±' + e.RD + '</span>'; 474 + const winsDisplay = isPending ? '-' : e.Wins.toLocaleString(); 475 + const lossesDisplay = isPending ? '-' : e.Losses.toLocaleString(); 476 + const winRateDisplay = isPending ? '-' : '<span class="win-rate ' + winRateClass + '">' + winRate + '%</span>'; 477 + const avgMovesDisplay = isPending ? '-' : e.AvgMoves.toFixed(1); 478 + 479 + return '<tr' + rowClass + '>' + 480 + '<td class="rank rank-' + rank + '">' + rankDisplay + '</td>' + 481 + '<td class="player-name"><a href="/user/' + e.Username + '" style="color: inherit; text-decoration: none;">' + nameDisplay + '</a></td>' + 482 + '<td>' + ratingDisplay + '</td>' + 483 + '<td>' + winsDisplay + '</td>' + 484 + '<td>' + lossesDisplay + '</td>' + 485 + '<td>' + winRateDisplay + '</td>' + 486 + '<td>' + avgMovesDisplay + '</td>' + 458 487 '<td style="color: #64748b;">' + lastPlayed + '</td>' + 459 488 '</tr>'; 460 489 }).join(''); ··· 554 583 <tbody> 555 584 {{if .Entries}} 556 585 {{range $i, $e := .Entries}} 557 - <tr> 558 - <td class="rank rank-{{add $i 1}}">{{if lt $i 3}}{{medal $i}}{{else}}{{add $i 1}}{{end}}</td> 559 - <td class="player-name"><a href="/user/{{$e.Username}}" style="color: inherit; text-decoration: none;">{{$e.Username}}</a></td> 560 - <td><strong>{{$e.Rating}}</strong> <span style="color: #94a3b8; font-size: 0.85em;">±{{$e.RD}}</span></td> 561 - <td>{{$e.Wins}}</td> 562 - <td>{{$e.Losses}}</td> 563 - <td><span class="win-rate {{winRateClass $e}}">{{winRate $e}}%</span></td> 564 - <td>{{printf "%.1f" $e.AvgMoves}}</td> 565 - <td style="color: #64748b;">{{$e.LastPlayed.Format "Jan 2, 3:04 PM"}}</td> 586 + <tr{{if $e.IsPending}} class="pending"{{end}}> 587 + <td class="rank rank-{{add $i 1}}">{{if $e.IsPending}}⏳{{else if lt $i 3}}{{medal $i}}{{else}}{{add $i 1}}{{end}}</td> 588 + <td class="player-name"><a href="/user/{{$e.Username}}" style="color: inherit; text-decoration: none;">{{$e.Username}}{{if $e.IsPending}} <span style="font-size: 0.8em;">(pending)</span>{{end}}</a></td> 589 + <td>{{if $e.IsPending}}-{{else}}<strong>{{$e.Rating}}</strong> <span style="color: #94a3b8; font-size: 0.85em;">±{{$e.RD}}</span>{{end}}</td> 590 + <td>{{if $e.IsPending}}-{{else}}{{$e.Wins}}{{end}}</td> 591 + <td>{{if $e.IsPending}}-{{else}}{{$e.Losses}}{{end}}</td> 592 + <td>{{if $e.IsPending}}-{{else}}<span class="win-rate {{winRateClass $e}}">{{winRate $e}}%</span>{{end}}</td> 593 + <td>{{if $e.IsPending}}-{{else}}{{printf "%.1f" $e.AvgMoves}}{{end}}</td> 594 + <td style="color: #64748b;">{{if $e.IsPending}}Waiting...{{else}}{{$e.LastPlayed.Format "Jan 2, 3:04 PM"}}{{end}}</td> 566 595 </tr> 567 596 {{end}} 568 597 {{else}}
+26 -3
internal/storage/database.go
··· 20 20 AvgMoves float64 21 21 Stage string 22 22 LastPlayed time.Time 23 + IsPending bool 23 24 } 24 25 25 26 type Submission struct { ··· 175 176 } 176 177 177 178 func GetLeaderboard(limit int) ([]LeaderboardEntry, error) { 179 + // Get submissions with matches 178 180 query := ` 179 181 SELECT 180 182 s.username, ··· 183 185 SUM(CASE WHEN m.player1_id = s.id THEN m.player1_wins WHEN m.player2_id = s.id THEN m.player2_wins ELSE 0 END) as total_wins, 184 186 SUM(CASE WHEN m.player1_id = s.id THEN m.player2_wins WHEN m.player2_id = s.id THEN m.player1_wins ELSE 0 END) as total_losses, 185 187 AVG(CASE WHEN m.player1_id = s.id THEN m.player1_moves ELSE m.player2_moves END) as avg_moves, 186 - MAX(m.timestamp) as last_played 188 + MAX(m.timestamp) as last_played, 189 + 0 as is_pending 187 190 FROM submissions s 188 191 LEFT JOIN matches m ON (m.player1_id = s.id OR m.player2_id = s.id) AND m.is_valid = 1 189 192 WHERE s.is_active = 1 190 193 GROUP BY s.username, s.glicko_rating, s.glicko_rd 191 194 HAVING COUNT(m.id) > 0 192 - ORDER BY rating DESC, total_wins DESC 195 + 196 + UNION ALL 197 + 198 + SELECT 199 + s.username, 200 + 1500.0 as rating, 201 + 350.0 as rd, 202 + 0 as total_wins, 203 + 0 as total_losses, 204 + 0.0 as avg_moves, 205 + s.upload_time as last_played, 206 + 1 as is_pending 207 + FROM submissions s 208 + LEFT JOIN matches m ON (m.player1_id = s.id OR m.player2_id = s.id) AND m.is_valid = 1 209 + WHERE s.is_active = 1 AND s.status IN ('pending', 'testing') 210 + GROUP BY s.username, s.upload_time 211 + HAVING COUNT(m.id) = 0 212 + 213 + ORDER BY is_pending ASC, rating DESC, total_wins DESC 193 214 LIMIT ? 194 215 ` 195 216 ··· 204 225 var e LeaderboardEntry 205 226 var lastPlayed string 206 227 var rating, rd float64 207 - err := rows.Scan(&e.Username, &rating, &rd, &e.Wins, &e.Losses, &e.AvgMoves, &lastPlayed) 228 + var isPending int 229 + err := rows.Scan(&e.Username, &rating, &rd, &e.Wins, &e.Losses, &e.AvgMoves, &lastPlayed, &isPending) 208 230 if err != nil { 209 231 return nil, err 210 232 } 211 233 212 234 e.Rating = int(rating) 213 235 e.RD = int(rd) 236 + e.IsPending = isPending == 1 214 237 215 238 totalGames := e.Wins + e.Losses 216 239 if totalGames > 0 {