a geicko-2 based round robin ranking system designed to test c++ battleship submissions
battleship.dunkirk.sh
1package main
2
3import (
4 "context"
5 "errors"
6 "log"
7 "net/http"
8 "os"
9 "os/signal"
10 "syscall"
11 "time"
12
13 tea "github.com/charmbracelet/bubbletea"
14 "github.com/charmbracelet/lipgloss"
15 "github.com/charmbracelet/ssh"
16 "github.com/charmbracelet/wish"
17 "github.com/charmbracelet/wish/bubbletea"
18 "github.com/charmbracelet/wish/logging"
19 "github.com/charmbracelet/wish/scp"
20)
21
22const (
23 host = "0.0.0.0"
24 sshPort = "2222"
25 webPort = "8080"
26 ssePort = "8081"
27 uploadDir = "./submissions"
28 resultsDB = "./results.db"
29)
30
31func main() {
32 // Initialize storage
33 if err := initStorage(); err != nil {
34 log.Fatal(err)
35 }
36
37 // Initialize SSE server
38 initSSE()
39
40 // Start SSE server on separate port
41 go startSSEServer()
42
43 // Start background worker
44 workerCtx, workerCancel := context.WithCancel(context.Background())
45 defer workerCancel()
46 go startWorker(workerCtx)
47
48 // Start web server
49 go startWebServer()
50
51 // Start SSH server with TUI, SCP, and SFTP
52 toClient, fromClient := newSCPHandlers()
53 s, err := wish.NewServer(
54 wish.WithAddress(host + ":" + sshPort),
55 wish.WithHostKeyPath(".ssh/battleship_arena"),
56 wish.WithSubsystem("sftp", sftpHandler),
57 wish.WithMiddleware(
58 scp.Middleware(toClient, fromClient),
59 bubbletea.Middleware(teaHandler),
60 logging.Middleware(),
61 ),
62 )
63 if err != nil {
64 log.Fatal(err)
65 }
66
67 done := make(chan os.Signal, 1)
68 signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
69
70 log.Printf("SSH server listening on %s:%s", host, sshPort)
71 log.Printf("Web leaderboard at http://%s:%s", host, webPort)
72
73 go func() {
74 if err := s.ListenAndServe(); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
75 log.Fatal(err)
76 }
77 }()
78
79 <-done
80 log.Println("Shutting down servers...")
81 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
82 defer cancel()
83 if err := s.Shutdown(ctx); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
84 log.Fatal(err)
85 }
86}
87
88func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) {
89 // Don't handle non-interactive sessions (SCP/SFTP have commands)
90 if len(s.Command()) > 0 {
91 return nil, nil
92 }
93
94 pty, _, active := s.Pty()
95 if !active {
96 wish.Fatalln(s, "no active terminal")
97 return nil, nil
98 }
99
100 m := initialModel(s.User(), pty.Window.Width, pty.Window.Height)
101 return m, []tea.ProgramOption{tea.WithAltScreen()}
102}
103
104func initStorage() error {
105 if err := os.MkdirAll(uploadDir, 0755); err != nil {
106 return err
107 }
108
109 db, err := initDB(resultsDB)
110 if err != nil {
111 return err
112 }
113 globalDB = db
114
115 return nil
116}
117
118func startWebServer() {
119 mux := http.NewServeMux()
120 mux.HandleFunc("/", handleLeaderboard)
121 mux.HandleFunc("/api/leaderboard", handleAPILeaderboard)
122
123 // Serve static files
124 fs := http.FileServer(http.Dir("./static"))
125 mux.Handle("/static/", http.StripPrefix("/static/", fs))
126
127 server := &http.Server{
128 Addr: ":" + webPort,
129 Handler: mux,
130 ReadTimeout: 0, // No timeout for SSE
131 WriteTimeout: 0, // No timeout for SSE
132 MaxHeaderBytes: 1 << 20,
133 }
134
135 log.Printf("Web server starting on :%s", webPort)
136 if err := server.ListenAndServe(); err != nil {
137 log.Fatal(err)
138 }
139}
140
141func startSSEServer() {
142 // Wrap SSE server with CORS middleware
143 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
144 w.Header().Set("Access-Control-Allow-Origin", "*")
145 w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
146 w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
147
148 if r.Method == "OPTIONS" {
149 w.WriteHeader(http.StatusOK)
150 return
151 }
152
153 sseServer.ServeHTTP(w, r)
154 })
155
156 log.Printf("SSE server starting on :%s", ssePort)
157 if err := http.ListenAndServe(":"+ssePort, handler); err != nil {
158 log.Fatal(err)
159 }
160}
161
162var titleStyle = lipgloss.NewStyle().
163 Bold(true).
164 Foreground(lipgloss.Color("205")).
165 MarginTop(1).
166 MarginBottom(1)