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
Changed files
+64 -8
cmd
battleship-arena
internal
runner
server
+1 -1
cmd/battleship-arena/main.go
···
workerCtx, workerCancel := context.WithCancel(context.Background())
defer workerCancel()
-
go runner.StartWorker(workerCtx, cfg.UploadDir, server.BroadcastProgress, server.NotifyLeaderboardUpdate, server.BroadcastProgressComplete)
+
go runner.StartWorker(workerCtx, cfg.UploadDir, server.BroadcastProgress, server.NotifyLeaderboardUpdate, server.BroadcastProgressComplete, server.BroadcastStatusUpdate)
toClient, fromClient := server.NewSCPHandlers(cfg.UploadDir)
sshServer, err := wish.NewServer(
+10 -6
internal/runner/worker.go
···
var workerMutex sync.Mutex
-
func StartWorker(ctx context.Context, uploadDir string, broadcastFunc func(string, int, int, time.Time, []string), notifyFunc func(), completeFunc func()) {
+
func StartWorker(ctx context.Context, uploadDir string, broadcastFunc func(string, int, int, time.Time, []string), notifyFunc func(), completeFunc func(), statusFunc func(string, string, string)) {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
go func() {
-
if err := processSubmissionsWithLock(uploadDir, broadcastFunc, notifyFunc, completeFunc); err != nil {
+
if err := processSubmissionsWithLock(uploadDir, broadcastFunc, notifyFunc, completeFunc, statusFunc); err != nil {
log.Printf("Worker error (submissions): %v", err)
}
}()
···
return
case <-ticker.C:
go func() {
-
if err := processSubmissionsWithLock(uploadDir, broadcastFunc, notifyFunc, completeFunc); err != nil {
+
if err := processSubmissionsWithLock(uploadDir, broadcastFunc, notifyFunc, completeFunc, statusFunc); err != nil {
log.Printf("Worker error (submissions): %v", err)
}
}()
···
}
}
-
func processSubmissionsWithLock(uploadDir string, broadcastFunc func(string, int, int, time.Time, []string), notifyFunc func(), completeFunc func()) error {
+
func processSubmissionsWithLock(uploadDir string, broadcastFunc func(string, int, int, time.Time, []string), notifyFunc func(), completeFunc func(), statusFunc func(string, string, string)) error {
if !workerMutex.TryLock() {
// Silently skip if worker is already running
return nil
}
defer workerMutex.Unlock()
-
return ProcessSubmissions(uploadDir, broadcastFunc, notifyFunc, completeFunc)
+
return ProcessSubmissions(uploadDir, broadcastFunc, notifyFunc, completeFunc, statusFunc)
}
-
func ProcessSubmissions(uploadDir string, broadcastFunc func(string, int, int, time.Time, []string), notifyFunc func(), completeFunc func()) error {
+
func ProcessSubmissions(uploadDir string, broadcastFunc func(string, int, int, time.Time, []string), notifyFunc func(), completeFunc func(), statusFunc func(string, string, string)) error {
submissions, err := storage.GetPendingSubmissions()
if err != nil {
return err
···
for _, sub := range submissions {
log.Printf("⚙️ Compiling %s (%s)", sub.Username, sub.Filename)
+
statusFunc(sub.Username, "compiling", "")
if err := CompileSubmission(sub, uploadDir); err != nil {
log.Printf("❌ Compilation failed for %s: %v", sub.Username, err)
storage.UpdateSubmissionStatusWithMessage(sub.ID, "compilation_failed", err.Error())
+
statusFunc(sub.Username, "compilation_failed", err.Error())
notifyFunc()
continue
}
log.Printf("✓ Compiled %s", sub.Username)
storage.UpdateSubmissionStatus(sub.ID, "completed")
+
statusFunc(sub.Username, "running_matches", "")
RunRoundRobinMatches(sub, uploadDir, broadcastFunc)
+
statusFunc(sub.Username, "completed", "")
notifyFunc()
}
+19
internal/server/sse.go
···
EstimatedTimeLeft string `json:"estimated_time_left,omitempty"`
PercentComplete float64 `json:"percent_complete,omitempty"`
QueuedPlayers []string `json:"queued_players,omitempty"`
+
Status string `json:"status,omitempty"`
+
FailureMessage string `json:"failure_message,omitempty"`
}
func InitSSE() {
···
// Silent - no log needed for routine completion
SSEServer.SendMessage("/events/updates", sse.SimpleMessage(string(data)))
}
+
+
func BroadcastStatusUpdate(player, status, failureMessage string) {
+
update := ProgressUpdate{
+
Type: "status",
+
Player: player,
+
Status: status,
+
FailureMessage: failureMessage,
+
}
+
+
data, err := json.Marshal(update)
+
if err != nil {
+
log.Printf("Failed to marshal status update: %v", err)
+
return
+
}
+
+
SSEServer.SendMessage("/events/updates", sse.SimpleMessage(string(data)))
+
}
+34 -1
internal/server/web.go
···
const data = JSON.parse(event.data);
console.log('SSE message received:', data);
-
// Check if it's a progress update or leaderboard update
+
// Check message type
if (data.type === 'progress') {
console.log('Progress update:', data);
updateProgress(data);
} else if (data.type === 'complete') {
console.log('Progress complete');
hideProgress();
+
} else if (data.type === 'status') {
+
console.log('Status update:', data);
+
updatePlayerStatus(data);
} else if (Array.isArray(data)) {
// Leaderboard update
console.log('Updating leaderboard with', data.length, 'entries');
···
const indicator = document.getElementById('progress-indicator');
if (indicator) {
indicator.classList.add('hidden');
+
}
+
}
+
+
function updatePlayerStatus(data) {
+
// Find the player's row in the leaderboard
+
const rows = document.querySelectorAll('tbody tr');
+
for (const row of rows) {
+
const playerLink = row.querySelector('.player-name a');
+
if (!playerLink) continue;
+
+
const username = playerLink.getAttribute('href').split('/').pop();
+
if (username === data.player) {
+
const lastPlayedCell = row.cells[7]; // Last cell (Last Active)
+
+
if (data.status === 'compiling') {
+
lastPlayedCell.innerHTML = '<span style="color: #3b82f6;">⚙️ Compiling...</span>';
+
row.classList.add('pending');
+
} else if (data.status === 'compilation_failed') {
+
lastPlayedCell.innerHTML = '<span style="color: #ef4444;" title="' +
+
(data.failure_message || 'Compilation failed') + '">❌ Failed</span>';
+
row.classList.remove('pending');
+
} else if (data.status === 'running_matches') {
+
lastPlayedCell.innerHTML = '<span style="color: #10b981;">▶️ Running matches...</span>';
+
row.classList.add('pending');
+
} else if (data.status === 'completed') {
+
// Will be updated by leaderboard refresh
+
row.classList.remove('pending');
+
}
+
break;
+
}
}
}