a geicko-2 based round robin ranking system designed to test c++ battleship submissions
battleship.dunkirk.sh
1package server
2
3import (
4 "encoding/json"
5 "fmt"
6 "log"
7 "time"
8
9 "github.com/alexandrevicenzi/go-sse"
10
11 "battleship-arena/internal/storage"
12)
13
14var SSEServer *sse.Server
15
16type ProgressUpdate struct {
17 Type string `json:"type"`
18 Player string `json:"player,omitempty"`
19 Opponent string `json:"opponent,omitempty"`
20 CurrentMatch int `json:"current_match,omitempty"`
21 TotalMatches int `json:"total_matches,omitempty"`
22 EstimatedTimeLeft string `json:"estimated_time_left,omitempty"`
23 PercentComplete float64 `json:"percent_complete,omitempty"`
24 QueuedPlayers []string `json:"queued_players,omitempty"`
25 Status string `json:"status,omitempty"`
26 FailureMessage string `json:"failure_message,omitempty"`
27}
28
29func InitSSE() {
30 // Disable verbose SSE library logging
31 SSEServer = sse.NewServer(nil)
32}
33
34func NotifyLeaderboardUpdate() {
35 entries, err := storage.GetLeaderboard(50)
36 if err != nil {
37 log.Printf("SSE: failed to get leaderboard: %v", err)
38 return
39 }
40
41 data, err := json.Marshal(entries)
42 if err != nil {
43 log.Printf("SSE: failed to marshal leaderboard: %v", err)
44 return
45 }
46
47 SSEServer.SendMessage("/events/updates", sse.SimpleMessage(string(data)))
48}
49
50func BroadcastProgress(player string, currentMatch, totalMatches int, startTime time.Time, queuedPlayers []string) {
51 elapsed := time.Since(startTime)
52 avgTimePerMatch := elapsed / time.Duration(currentMatch)
53 remainingMatches := totalMatches - currentMatch
54 estimatedTimeLeft := avgTimePerMatch * time.Duration(remainingMatches)
55
56 percentComplete := float64(currentMatch) / float64(totalMatches) * 100.0
57 timeLeftStr := formatDuration(estimatedTimeLeft)
58
59 filteredQueue := make([]string, 0)
60 for _, p := range queuedPlayers {
61 if p != player {
62 filteredQueue = append(filteredQueue, p)
63 }
64 }
65
66 progress := ProgressUpdate{
67 Type: "progress",
68 Player: player,
69 CurrentMatch: currentMatch,
70 TotalMatches: totalMatches,
71 EstimatedTimeLeft: timeLeftStr,
72 PercentComplete: percentComplete,
73 QueuedPlayers: filteredQueue,
74 }
75
76 data, err := json.Marshal(progress)
77 if err != nil {
78 log.Printf("Failed to marshal progress: %v", err)
79 return
80 }
81
82 // Only log every 10th match to reduce noise
83 if currentMatch%10 == 0 || currentMatch == totalMatches {
84 log.Printf("Progress: %s [%d/%d] %.0f%%", player, currentMatch, totalMatches, percentComplete)
85 }
86
87 SSEServer.SendMessage("/events/updates", sse.SimpleMessage(string(data)))
88}
89
90func formatDuration(d time.Duration) string {
91 if d < time.Minute {
92 return "< 1 min"
93 }
94 minutes := int(d.Minutes())
95 if minutes < 60 {
96 return fmt.Sprintf("%d min", minutes)
97 }
98 hours := minutes / 60
99 mins := minutes % 60
100 if mins > 0 {
101 return fmt.Sprintf("%dh %dm", hours, mins)
102 }
103 return fmt.Sprintf("%dh", hours)
104}
105
106func BroadcastProgressComplete() {
107 complete := ProgressUpdate{
108 Type: "complete",
109 }
110
111 data, err := json.Marshal(complete)
112 if err != nil {
113 return
114 }
115
116 // Silent - no log needed for routine completion
117 SSEServer.SendMessage("/events/updates", sse.SimpleMessage(string(data)))
118}
119
120func 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}