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
chore: add the cmd
dunkirk.sh
3 months ago
30ced41a
5ce1ddf5
verified
This commit was signed with the committer's
known signature
.
dunkirk.sh
SSH Key Fingerprint:
SHA256:DqcG0RXYExE26KiWo3VxJnsxswN1QNfTBvB+bdSpk80=
+179
-1
2 changed files
expand all
collapse all
unified
split
.gitignore
cmd
battleship-arena
main.go
+1
-1
.gitignore
···
1
1
.ssh/
2
2
submissions/
3
3
*.db
4
4
-
battleship-arena
4
4
+
bin
5
5
*.log
6
6
build/
7
7
+178
cmd/battleship-arena/main.go
···
1
1
+
package main
2
2
+
3
3
+
import (
4
4
+
"context"
5
5
+
"errors"
6
6
+
"log"
7
7
+
"net/http"
8
8
+
"os"
9
9
+
"os/signal"
10
10
+
"syscall"
11
11
+
"time"
12
12
+
13
13
+
tea "github.com/charmbracelet/bubbletea"
14
14
+
"github.com/charmbracelet/lipgloss"
15
15
+
"github.com/charmbracelet/ssh"
16
16
+
"github.com/charmbracelet/wish"
17
17
+
"github.com/charmbracelet/wish/bubbletea"
18
18
+
"github.com/charmbracelet/wish/logging"
19
19
+
"github.com/charmbracelet/wish/scp"
20
20
+
"github.com/go-chi/chi/v5"
21
21
+
"github.com/go-chi/chi/v5/middleware"
22
22
+
23
23
+
"battleship-arena/internal/runner"
24
24
+
"battleship-arena/internal/server"
25
25
+
"battleship-arena/internal/storage"
26
26
+
"battleship-arena/internal/tui"
27
27
+
)
28
28
+
29
29
+
type Config struct {
30
30
+
Host string
31
31
+
SSHPort string
32
32
+
WebPort string
33
33
+
UploadDir string
34
34
+
ResultsDB string
35
35
+
AdminPasscode string
36
36
+
ExternalURL string
37
37
+
}
38
38
+
39
39
+
func loadConfig() Config {
40
40
+
cfg := Config{
41
41
+
Host: getEnv("BATTLESHIP_HOST", "0.0.0.0"),
42
42
+
SSHPort: getEnv("BATTLESHIP_SSH_PORT", "2222"),
43
43
+
WebPort: getEnv("BATTLESHIP_WEB_PORT", "8081"),
44
44
+
UploadDir: getEnv("BATTLESHIP_UPLOAD_DIR", "./submissions"),
45
45
+
ResultsDB: getEnv("BATTLESHIP_RESULTS_DB", "./results.db"),
46
46
+
AdminPasscode: getEnv("BATTLESHIP_ADMIN_PASSCODE", "battleship-admin-override"),
47
47
+
ExternalURL: getEnv("BATTLESHIP_EXTERNAL_URL", "http://localhost:8081"),
48
48
+
}
49
49
+
return cfg
50
50
+
}
51
51
+
52
52
+
func getEnv(key, defaultValue string) string {
53
53
+
if value := os.Getenv(key); value != "" {
54
54
+
return value
55
55
+
}
56
56
+
return defaultValue
57
57
+
}
58
58
+
59
59
+
func main() {
60
60
+
cfg := loadConfig()
61
61
+
62
62
+
if err := initStorage(cfg); err != nil {
63
63
+
log.Fatal(err)
64
64
+
}
65
65
+
66
66
+
server.InitSSE()
67
67
+
server.SetConfig(cfg.AdminPasscode, cfg.ExternalURL)
68
68
+
69
69
+
workerCtx, workerCancel := context.WithCancel(context.Background())
70
70
+
defer workerCancel()
71
71
+
go runner.StartWorker(workerCtx, cfg.UploadDir, server.BroadcastProgress, server.NotifyLeaderboardUpdate, server.BroadcastProgressComplete)
72
72
+
73
73
+
toClient, fromClient := server.NewSCPHandlers(cfg.UploadDir)
74
74
+
sshServer, err := wish.NewServer(
75
75
+
wish.WithAddress(cfg.Host + ":" + cfg.SSHPort),
76
76
+
wish.WithHostKeyPath(".ssh/battleship_arena"),
77
77
+
wish.WithPublicKeyAuth(server.PublicKeyAuthHandler),
78
78
+
wish.WithPasswordAuth(server.PasswordAuthHandler),
79
79
+
wish.WithSubsystem("sftp", server.SFTPHandler(cfg.UploadDir)),
80
80
+
wish.WithMiddleware(
81
81
+
scp.Middleware(toClient, fromClient),
82
82
+
bubbletea.Middleware(teaHandler),
83
83
+
logging.Middleware(),
84
84
+
),
85
85
+
)
86
86
+
if err != nil {
87
87
+
log.Fatal(err)
88
88
+
}
89
89
+
90
90
+
done := make(chan os.Signal, 1)
91
91
+
signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
92
92
+
93
93
+
log.Printf("SSH server listening on %s:%s", cfg.Host, cfg.SSHPort)
94
94
+
log.Printf("Web leaderboard at %s", cfg.ExternalURL)
95
95
+
96
96
+
go func() {
97
97
+
if err := sshServer.ListenAndServe(); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
98
98
+
log.Fatal(err)
99
99
+
}
100
100
+
}()
101
101
+
102
102
+
go func() {
103
103
+
<-done
104
104
+
log.Println("Shutting down servers...")
105
105
+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
106
106
+
defer cancel()
107
107
+
if err := sshServer.Shutdown(ctx); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
108
108
+
log.Fatal(err)
109
109
+
}
110
110
+
os.Exit(0)
111
111
+
}()
112
112
+
113
113
+
r := chi.NewRouter()
114
114
+
r.Use(middleware.Logger)
115
115
+
r.Use(middleware.Recoverer)
116
116
+
r.Mount("/events/", server.SSEServer)
117
117
+
r.Get("/api/leaderboard", server.HandleAPILeaderboard)
118
118
+
r.Get("/api/rating-history/{player}", server.HandleRatingHistory)
119
119
+
r.Get("/player/{player}", server.HandlePlayerPage)
120
120
+
r.Get("/user/{username}", server.HandleUserProfile)
121
121
+
r.Get("/users", server.HandleUsers)
122
122
+
r.Get("/", server.HandleLeaderboard)
123
123
+
124
124
+
log.Println("Server running at " + cfg.ExternalURL)
125
125
+
http.ListenAndServe(":"+cfg.WebPort, r)
126
126
+
}
127
127
+
128
128
+
func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) {
129
129
+
if len(s.Command()) > 0 {
130
130
+
return nil, nil
131
131
+
}
132
132
+
133
133
+
// Check if user needs onboarding
134
134
+
needsOnboarding := false
135
135
+
if val := s.Context().Value("needs_onboarding"); val != nil {
136
136
+
needsOnboarding = val.(bool)
137
137
+
}
138
138
+
139
139
+
pty, _, active := s.Pty()
140
140
+
if !active {
141
141
+
wish.Fatalln(s, "no active terminal")
142
142
+
return nil, nil
143
143
+
}
144
144
+
145
145
+
if needsOnboarding {
146
146
+
// Run onboarding first
147
147
+
publicKey := ""
148
148
+
if val := s.Context().Value("public_key"); val != nil {
149
149
+
publicKey = val.(string)
150
150
+
}
151
151
+
152
152
+
m := tui.NewOnboardingModel(s.User(), publicKey, pty.Window.Width, pty.Window.Height)
153
153
+
return m, []tea.ProgramOption{tea.WithAltScreen()}
154
154
+
}
155
155
+
156
156
+
m := tui.InitialModel(s.User(), pty.Window.Width, pty.Window.Height)
157
157
+
return m, []tea.ProgramOption{tea.WithAltScreen()}
158
158
+
}
159
159
+
160
160
+
func initStorage(cfg Config) error {
161
161
+
if err := os.MkdirAll(cfg.UploadDir, 0755); err != nil {
162
162
+
return err
163
163
+
}
164
164
+
165
165
+
db, err := storage.InitDB(cfg.ResultsDB)
166
166
+
if err != nil {
167
167
+
return err
168
168
+
}
169
169
+
storage.DB = db
170
170
+
171
171
+
return nil
172
172
+
}
173
173
+
174
174
+
var titleStyle = lipgloss.NewStyle().
175
175
+
Bold(true).
176
176
+
Foreground(lipgloss.Color("205")).
177
177
+
MarginTop(1).
178
178
+
MarginBottom(1)