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)