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

feat: add submissions and make src dir

dunkirk.sh bc2272fb d3602e5c

verified
+151 -4
battleship-arena

This is a binary file and will not be displayed.

+3
internal/runner/runner.go
··· 35 35 36 36 buildDir := filepath.Join(enginePath, "build") 37 37 os.MkdirAll(buildDir, 0755) 38 + 39 + srcDir := filepath.Join(enginePath, "src") 40 + os.MkdirAll(srcDir, 0755) 38 41 39 42 srcPath := filepath.Join(uploadDir, sub.Username, sub.Filename) 40 43 dstPath := filepath.Join(enginePath, "src", sub.Filename)
+69 -2
internal/server/users.go
··· 3 3 import ( 4 4 "fmt" 5 5 "html/template" 6 + "log" 6 7 "net/http" 7 8 "strings" 8 9 ··· 39 40 } 40 41 } 41 42 43 + // Get user's submissions with stats 44 + submissions, err := storage.GetUserSubmissionsWithStats(username) 45 + if err != nil { 46 + log.Printf("Error getting submissions for %s: %v", username, err) 47 + submissions = []storage.SubmissionWithStats{} 48 + } 49 + if submissions == nil { 50 + submissions = []storage.SubmissionWithStats{} 51 + } 52 + log.Printf("Found %d submissions for %s", len(submissions), username) 53 + 42 54 // Parse public key for display 43 55 publicKeyDisplay := formatPublicKey(user.PublicKey) 44 56 45 57 tmpl := template.Must(template.New("user").Parse(userProfileHTML)) 46 58 data := struct { 47 - User *storage.User 48 - Entry *storage.LeaderboardEntry 59 + User *storage.User 60 + Entry *storage.LeaderboardEntry 61 + Submissions []storage.SubmissionWithStats 49 62 PublicKeyDisplay string 50 63 }{ 51 64 User: user, 52 65 Entry: userEntry, 66 + Submissions: submissions, 53 67 PublicKeyDisplay: publicKeyDisplay, 54 68 } 55 69 tmpl.Execute(w, data) ··· 251 265 <div class="stat-card"> 252 266 <div class="stat-label">Win Rate</div> 253 267 <div class="stat-value">{{printf "%.1f" .Entry.WinPct}}%</div> 268 + </div> 269 + </div> 270 + {{end}} 271 + 272 + {{if .Submissions}} 273 + <div class="key-section" style="margin-bottom: 2rem;"> 274 + <h2 class="section-title">📤 Submissions</h2> 275 + <div style="overflow-x: auto;"> 276 + <table style="width: 100%; border-collapse: collapse; font-size: 0.875rem;"> 277 + <thead> 278 + <tr style="border-bottom: 1px solid #334155;"> 279 + <th style="text-align: left; padding: 0.75rem 0.5rem; color: #94a3b8;">Filename</th> 280 + <th style="text-align: left; padding: 0.75rem 0.5rem; color: #94a3b8;">Uploaded</th> 281 + <th style="text-align: center; padding: 0.75rem 0.5rem; color: #94a3b8;">Rating</th> 282 + <th style="text-align: center; padding: 0.75rem 0.5rem; color: #94a3b8;">Wins</th> 283 + <th style="text-align: center; padding: 0.75rem 0.5rem; color: #94a3b8;">Losses</th> 284 + <th style="text-align: center; padding: 0.75rem 0.5rem; color: #94a3b8;">Win Rate</th> 285 + <th style="text-align: center; padding: 0.75rem 0.5rem; color: #94a3b8;">Avg Moves</th> 286 + <th style="text-align: center; padding: 0.75rem 0.5rem; color: #94a3b8;">Status</th> 287 + <th style="text-align: center; padding: 0.75rem 0.5rem; color: #94a3b8;">Active</th> 288 + </tr> 289 + </thead> 290 + <tbody> 291 + {{range .Submissions}} 292 + <tr style="border-bottom: 1px solid #334155;"> 293 + <td style="padding: 0.75rem 0.5rem; font-family: Monaco, monospace;">{{.Filename}}</td> 294 + <td style="padding: 0.75rem 0.5rem; color: #94a3b8;">{{.UploadTime.Format "Jan 2, 3:04 PM"}}</td> 295 + <td style="padding: 0.75rem 0.5rem; text-align: center;"> 296 + {{if .HasMatches}}{{.Rating}} <span style="color: #94a3b8; font-size: 0.8em;">±{{.RD}}</span>{{else}}-{{end}} 297 + </td> 298 + <td style="padding: 0.75rem 0.5rem; text-align: center;">{{if .HasMatches}}{{.Wins}}{{else}}-{{end}}</td> 299 + <td style="padding: 0.75rem 0.5rem; text-align: center;">{{if .HasMatches}}{{.Losses}}{{else}}-{{end}}</td> 300 + <td style="padding: 0.75rem 0.5rem; text-align: center;"> 301 + {{if .HasMatches}} 302 + {{if ge .WinPct 60.0}}<span style="color: #10b981;">{{printf "%.1f" .WinPct}}%</span>{{end}} 303 + {{if and (lt .WinPct 60.0) (ge .WinPct 40.0)}}<span style="color: #f59e0b;">{{printf "%.1f" .WinPct}}%</span>{{end}} 304 + {{if lt .WinPct 40.0}}<span style="color: #ef4444;">{{printf "%.1f" .WinPct}}%</span>{{end}} 305 + {{else}}-{{end}} 306 + </td> 307 + <td style="padding: 0.75rem 0.5rem; text-align: center;">{{if .HasMatches}}{{printf "%.1f" .AvgMoves}}{{else}}-{{end}}</td> 308 + <td style="padding: 0.75rem 0.5rem; text-align: center;"> 309 + {{if eq .Status "completed"}}<span style="color: #10b981;">✓</span>{{end}} 310 + {{if eq .Status "pending"}}<span style="color: #fbbf24;">⏳</span>{{end}} 311 + {{if eq .Status "testing"}}<span style="color: #3b82f6;">⚙️</span>{{end}} 312 + {{if eq .Status "failed"}}<span style="color: #ef4444;">✗</span>{{end}} 313 + </td> 314 + <td style="padding: 0.75rem 0.5rem; text-align: center;"> 315 + {{if .IsActive}}<span style="color: #10b981;">●</span>{{else}}<span style="color: #64748b;">○</span>{{end}} 316 + </td> 317 + </tr> 318 + {{end}} 319 + </tbody> 320 + </table> 254 321 </div> 255 322 </div> 256 323 {{end}}
+79 -2
internal/storage/database.go
··· 29 29 Filename string 30 30 UploadTime time.Time 31 31 Status string 32 + IsActive bool 33 + } 34 + 35 + type SubmissionWithStats struct { 36 + Submission 37 + Rating int 38 + RD int 39 + Wins int 40 + Losses int 41 + WinPct float64 42 + AvgMoves float64 43 + LastPlayed time.Time 44 + HasMatches bool 32 45 } 33 46 34 47 type Tournament struct { ··· 338 351 339 352 func GetUserSubmissions(username string) ([]Submission, error) { 340 353 rows, err := DB.Query( 341 - "SELECT id, username, filename, upload_time, status FROM submissions WHERE username = ? ORDER BY upload_time DESC LIMIT 10", 354 + "SELECT id, username, filename, upload_time, status, is_active FROM submissions WHERE username = ? ORDER BY upload_time DESC LIMIT 10", 342 355 username, 343 356 ) 344 357 if err != nil { ··· 349 362 var submissions []Submission 350 363 for rows.Next() { 351 364 var s Submission 352 - err := rows.Scan(&s.ID, &s.Username, &s.Filename, &s.UploadTime, &s.Status) 365 + err := rows.Scan(&s.ID, &s.Username, &s.Filename, &s.UploadTime, &s.Status, &s.IsActive) 353 366 if err != nil { 354 367 return nil, err 355 368 } 369 + submissions = append(submissions, s) 370 + } 371 + 372 + return submissions, rows.Err() 373 + } 374 + 375 + func GetUserSubmissionsWithStats(username string) ([]SubmissionWithStats, error) { 376 + query := ` 377 + SELECT 378 + s.id, 379 + s.username, 380 + s.filename, 381 + s.upload_time, 382 + s.status, 383 + s.is_active, 384 + COALESCE(s.glicko_rating, 1500.0) as rating, 385 + COALESCE(s.glicko_rd, 350.0) as rd, 386 + COALESCE(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), 0) as total_wins, 387 + COALESCE(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), 0) as total_losses, 388 + COALESCE(AVG(CASE WHEN m.player1_id = s.id THEN m.player1_moves ELSE m.player2_moves END), 0) as avg_moves, 389 + MAX(m.timestamp) as last_played, 390 + COUNT(m.id) as match_count 391 + FROM submissions s 392 + LEFT JOIN matches m ON (m.player1_id = s.id OR m.player2_id = s.id) AND m.is_valid = 1 393 + WHERE s.username = ? 394 + GROUP BY s.id, s.username, s.filename, s.upload_time, s.status, s.is_active, s.glicko_rating, s.glicko_rd 395 + ORDER BY s.upload_time DESC 396 + LIMIT 10 397 + ` 398 + 399 + rows, err := DB.Query(query, username) 400 + if err != nil { 401 + return nil, err 402 + } 403 + defer rows.Close() 404 + 405 + var submissions []SubmissionWithStats 406 + for rows.Next() { 407 + var s SubmissionWithStats 408 + var lastPlayed *string 409 + var rating, rd float64 410 + var matchCount int 411 + 412 + err := rows.Scan( 413 + &s.ID, &s.Username, &s.Filename, &s.UploadTime, &s.Status, &s.IsActive, 414 + &rating, &rd, &s.Wins, &s.Losses, &s.AvgMoves, &lastPlayed, &matchCount, 415 + ) 416 + if err != nil { 417 + return nil, err 418 + } 419 + 420 + s.Rating = int(rating) 421 + s.RD = int(rd) 422 + s.HasMatches = matchCount > 0 423 + 424 + totalGames := s.Wins + s.Losses 425 + if totalGames > 0 { 426 + s.WinPct = float64(s.Wins) / float64(totalGames) * 100.0 427 + } 428 + 429 + if lastPlayed != nil { 430 + s.LastPlayed, _ = time.Parse("2006-01-02 15:04:05", *lastPlayed) 431 + } 432 + 356 433 submissions = append(submissions, s) 357 434 } 358 435