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: handle pending and failed states
dunkirk.sh
3 months ago
20d9604e
797b9b5e
verified
This commit was signed with the committer's
known signature
.
dunkirk.sh
SSH Key Fingerprint:
SHA256:DqcG0RXYExE26KiWo3VxJnsxswN1QNfTBvB+bdSpk80=
+64
-8
4 changed files
expand all
collapse all
unified
split
cmd
battleship-arena
main.go
internal
runner
worker.go
server
sse.go
web.go
+1
-1
cmd/battleship-arena/main.go
···
80
80
81
81
workerCtx, workerCancel := context.WithCancel(context.Background())
82
82
defer workerCancel()
83
83
-
go runner.StartWorker(workerCtx, cfg.UploadDir, server.BroadcastProgress, server.NotifyLeaderboardUpdate, server.BroadcastProgressComplete)
83
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
14
-
func StartWorker(ctx context.Context, uploadDir string, broadcastFunc func(string, int, int, time.Time, []string), notifyFunc func(), completeFunc func()) {
14
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
19
-
if err := processSubmissionsWithLock(uploadDir, broadcastFunc, notifyFunc, completeFunc); err != nil {
19
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
30
-
if err := processSubmissionsWithLock(uploadDir, broadcastFunc, notifyFunc, completeFunc); err != nil {
30
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
38
-
func processSubmissionsWithLock(uploadDir string, broadcastFunc func(string, int, int, time.Time, []string), notifyFunc func(), completeFunc func()) error {
38
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
45
-
return ProcessSubmissions(uploadDir, broadcastFunc, notifyFunc, completeFunc)
45
45
+
return ProcessSubmissions(uploadDir, broadcastFunc, notifyFunc, completeFunc, statusFunc)
46
46
}
47
47
48
48
-
func ProcessSubmissions(uploadDir string, broadcastFunc func(string, int, int, time.Time, []string), notifyFunc func(), completeFunc func()) error {
48
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
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
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
73
+
statusFunc(sub.Username, "running_matches", "")
71
74
72
75
RunRoundRobinMatches(sub, uploadDir, broadcastFunc)
76
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
25
+
Status string `json:"status,omitempty"`
26
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
119
+
120
120
+
func BroadcastStatusUpdate(player, status, failureMessage string) {
121
121
+
update := ProgressUpdate{
122
122
+
Type: "status",
123
123
+
Player: player,
124
124
+
Status: status,
125
125
+
FailureMessage: failureMessage,
126
126
+
}
127
127
+
128
128
+
data, err := json.Marshal(update)
129
129
+
if err != nil {
130
130
+
log.Printf("Failed to marshal status update: %v", err)
131
131
+
return
132
132
+
}
133
133
+
134
134
+
SSEServer.SendMessage("/events/updates", sse.SimpleMessage(string(data)))
135
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
616
-
// Check if it's a progress update or leaderboard update
616
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
623
+
} else if (data.type === 'status') {
624
624
+
console.log('Status update:', data);
625
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
740
+
}
741
741
+
}
742
742
+
743
743
+
function updatePlayerStatus(data) {
744
744
+
// Find the player's row in the leaderboard
745
745
+
const rows = document.querySelectorAll('tbody tr');
746
746
+
for (const row of rows) {
747
747
+
const playerLink = row.querySelector('.player-name a');
748
748
+
if (!playerLink) continue;
749
749
+
750
750
+
const username = playerLink.getAttribute('href').split('/').pop();
751
751
+
if (username === data.player) {
752
752
+
const lastPlayedCell = row.cells[7]; // Last cell (Last Active)
753
753
+
754
754
+
if (data.status === 'compiling') {
755
755
+
lastPlayedCell.innerHTML = '<span style="color: #3b82f6;">⚙️ Compiling...</span>';
756
756
+
row.classList.add('pending');
757
757
+
} else if (data.status === 'compilation_failed') {
758
758
+
lastPlayedCell.innerHTML = '<span style="color: #ef4444;" title="' +
759
759
+
(data.failure_message || 'Compilation failed') + '">❌ Failed</span>';
760
760
+
row.classList.remove('pending');
761
761
+
} else if (data.status === 'running_matches') {
762
762
+
lastPlayedCell.innerHTML = '<span style="color: #10b981;">▶️ Running matches...</span>';
763
763
+
row.classList.add('pending');
764
764
+
} else if (data.status === 'completed') {
765
765
+
// Will be updated by leaderboard refresh
766
766
+
row.classList.remove('pending');
767
767
+
}
768
768
+
break;
769
769
+
}
737
770
}
738
771
}
739
772