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

feat: handle pending and failed states

dunkirk.sh 20d9604e 797b9b5e

verified
+64 -8
+1 -1
cmd/battleship-arena/main.go
··· 80 80 81 81 workerCtx, workerCancel := context.WithCancel(context.Background()) 82 82 defer workerCancel() 83 - go runner.StartWorker(workerCtx, cfg.UploadDir, server.BroadcastProgress, server.NotifyLeaderboardUpdate, server.BroadcastProgressComplete) 83 + go runner.StartWorker(workerCtx, cfg.UploadDir, server.BroadcastProgress, server.NotifyLeaderboardUpdate, server.BroadcastProgressComplete, server.BroadcastStatusUpdate) 84 84 85 85 toClient, fromClient := server.NewSCPHandlers(cfg.UploadDir) 86 86 sshServer, err := wish.NewServer(
+10 -6
internal/runner/worker.go
··· 11 11 12 12 var workerMutex sync.Mutex 13 13 14 - func StartWorker(ctx context.Context, uploadDir string, broadcastFunc func(string, int, int, time.Time, []string), notifyFunc func(), completeFunc func()) { 14 + func StartWorker(ctx context.Context, uploadDir string, broadcastFunc func(string, int, int, time.Time, []string), notifyFunc func(), completeFunc func(), statusFunc func(string, string, string)) { 15 15 ticker := time.NewTicker(10 * time.Second) 16 16 defer ticker.Stop() 17 17 18 18 go func() { 19 - if err := processSubmissionsWithLock(uploadDir, broadcastFunc, notifyFunc, completeFunc); err != nil { 19 + if err := processSubmissionsWithLock(uploadDir, broadcastFunc, notifyFunc, completeFunc, statusFunc); err != nil { 20 20 log.Printf("Worker error (submissions): %v", err) 21 21 } 22 22 }() ··· 27 27 return 28 28 case <-ticker.C: 29 29 go func() { 30 - if err := processSubmissionsWithLock(uploadDir, broadcastFunc, notifyFunc, completeFunc); err != nil { 30 + if err := processSubmissionsWithLock(uploadDir, broadcastFunc, notifyFunc, completeFunc, statusFunc); err != nil { 31 31 log.Printf("Worker error (submissions): %v", err) 32 32 } 33 33 }() ··· 35 35 } 36 36 } 37 37 38 - func processSubmissionsWithLock(uploadDir string, broadcastFunc func(string, int, int, time.Time, []string), notifyFunc func(), completeFunc func()) error { 38 + func processSubmissionsWithLock(uploadDir string, broadcastFunc func(string, int, int, time.Time, []string), notifyFunc func(), completeFunc func(), statusFunc func(string, string, string)) error { 39 39 if !workerMutex.TryLock() { 40 40 // Silently skip if worker is already running 41 41 return nil 42 42 } 43 43 defer workerMutex.Unlock() 44 44 45 - return ProcessSubmissions(uploadDir, broadcastFunc, notifyFunc, completeFunc) 45 + return ProcessSubmissions(uploadDir, broadcastFunc, notifyFunc, completeFunc, statusFunc) 46 46 } 47 47 48 - func ProcessSubmissions(uploadDir string, broadcastFunc func(string, int, int, time.Time, []string), notifyFunc func(), completeFunc func()) error { 48 + func ProcessSubmissions(uploadDir string, broadcastFunc func(string, int, int, time.Time, []string), notifyFunc func(), completeFunc func(), statusFunc func(string, string, string)) error { 49 49 submissions, err := storage.GetPendingSubmissions() 50 50 if err != nil { 51 51 return err ··· 58 58 59 59 for _, sub := range submissions { 60 60 log.Printf("⚙️ Compiling %s (%s)", sub.Username, sub.Filename) 61 + statusFunc(sub.Username, "compiling", "") 61 62 62 63 if err := CompileSubmission(sub, uploadDir); err != nil { 63 64 log.Printf("❌ Compilation failed for %s: %v", sub.Username, err) 64 65 storage.UpdateSubmissionStatusWithMessage(sub.ID, "compilation_failed", err.Error()) 66 + statusFunc(sub.Username, "compilation_failed", err.Error()) 65 67 notifyFunc() 66 68 continue 67 69 } 68 70 69 71 log.Printf("✓ Compiled %s", sub.Username) 70 72 storage.UpdateSubmissionStatus(sub.ID, "completed") 73 + statusFunc(sub.Username, "running_matches", "") 71 74 72 75 RunRoundRobinMatches(sub, uploadDir, broadcastFunc) 76 + statusFunc(sub.Username, "completed", "") 73 77 notifyFunc() 74 78 } 75 79
+19
internal/server/sse.go
··· 22 22 EstimatedTimeLeft string `json:"estimated_time_left,omitempty"` 23 23 PercentComplete float64 `json:"percent_complete,omitempty"` 24 24 QueuedPlayers []string `json:"queued_players,omitempty"` 25 + Status string `json:"status,omitempty"` 26 + FailureMessage string `json:"failure_message,omitempty"` 25 27 } 26 28 27 29 func InitSSE() { ··· 114 116 // Silent - no log needed for routine completion 115 117 SSEServer.SendMessage("/events/updates", sse.SimpleMessage(string(data))) 116 118 } 119 + 120 + func BroadcastStatusUpdate(player, status, failureMessage string) { 121 + update := ProgressUpdate{ 122 + Type: "status", 123 + Player: player, 124 + Status: status, 125 + FailureMessage: failureMessage, 126 + } 127 + 128 + data, err := json.Marshal(update) 129 + if err != nil { 130 + log.Printf("Failed to marshal status update: %v", err) 131 + return 132 + } 133 + 134 + SSEServer.SendMessage("/events/updates", sse.SimpleMessage(string(data))) 135 + }
+34 -1
internal/server/web.go
··· 613 613 const data = JSON.parse(event.data); 614 614 console.log('SSE message received:', data); 615 615 616 - // Check if it's a progress update or leaderboard update 616 + // Check message type 617 617 if (data.type === 'progress') { 618 618 console.log('Progress update:', data); 619 619 updateProgress(data); 620 620 } else if (data.type === 'complete') { 621 621 console.log('Progress complete'); 622 622 hideProgress(); 623 + } else if (data.type === 'status') { 624 + console.log('Status update:', data); 625 + updatePlayerStatus(data); 623 626 } else if (Array.isArray(data)) { 624 627 // Leaderboard update 625 628 console.log('Updating leaderboard with', data.length, 'entries'); ··· 734 737 const indicator = document.getElementById('progress-indicator'); 735 738 if (indicator) { 736 739 indicator.classList.add('hidden'); 740 + } 741 + } 742 + 743 + function updatePlayerStatus(data) { 744 + // Find the player's row in the leaderboard 745 + const rows = document.querySelectorAll('tbody tr'); 746 + for (const row of rows) { 747 + const playerLink = row.querySelector('.player-name a'); 748 + if (!playerLink) continue; 749 + 750 + const username = playerLink.getAttribute('href').split('/').pop(); 751 + if (username === data.player) { 752 + const lastPlayedCell = row.cells[7]; // Last cell (Last Active) 753 + 754 + if (data.status === 'compiling') { 755 + lastPlayedCell.innerHTML = '<span style="color: #3b82f6;">⚙️ Compiling...</span>'; 756 + row.classList.add('pending'); 757 + } else if (data.status === 'compilation_failed') { 758 + lastPlayedCell.innerHTML = '<span style="color: #ef4444;" title="' + 759 + (data.failure_message || 'Compilation failed') + '">❌ Failed</span>'; 760 + row.classList.remove('pending'); 761 + } else if (data.status === 'running_matches') { 762 + lastPlayedCell.innerHTML = '<span style="color: #10b981;">▶️ Running matches...</span>'; 763 + row.classList.add('pending'); 764 + } else if (data.status === 'completed') { 765 + // Will be updated by leaderboard refresh 766 + row.classList.remove('pending'); 767 + } 768 + break; 769 + } 737 770 } 738 771 } 739 772