a geicko-2 based round robin ranking system designed to test c++ battleship submissions battleship.dunkirk.sh
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add nice ssh tui

+1330 -76
+2
AGENTS.md
··· 1 1 # Development Notes 2 2 3 + **⚠️ AI Instructions: Do NOT create summary documents. User prefers to see actual code changes without additional documentation files.** 4 + 3 5 ## Architecture 4 6 5 7 - **main.go** - SSH/HTTP server initialization with Wish and Bubble Tea
+167
AUTH_IMPLEMENTATION.md
··· 1 + # SSH Public Key Authentication - Implementation Summary 2 + 3 + ## ✅ What Was Implemented 4 + 5 + ### 1. Database Schema 6 + Added `users` table to track authenticated users: 7 + - `username` - SSH username (unique) 8 + - `name` - Full name (required during onboarding) 9 + - `bio` - Optional description 10 + - `link` - Optional website/social link 11 + - `public_key` - SSH public key (unique, used for auth) 12 + - `created_at` - Registration timestamp 13 + - `last_login_at` - Last successful login 14 + 15 + **Location:** `internal/storage/database.go` + new `internal/storage/users.go` 16 + 17 + ### 2. SSH Authentication Handler 18 + Implements public key authentication flow: 19 + - Checks if public key is registered 20 + - If registered: verifies username matches and allows access 21 + - If new: checks if username is available 22 + - If username taken: rejects (prevents key reuse) 23 + - If available: flags user for onboarding 24 + 25 + **Location:** `internal/server/auth.go` 26 + 27 + ### 3. Onboarding Flow 28 + Interactive terminal prompt for first-time users: 29 + - Prompts for full name (required) 30 + - Prompts for bio (optional, skip with Enter) 31 + - Prompts for link (optional, skip with Enter) 32 + - Creates user record with their public key 33 + - Subsequent logins skip onboarding 34 + 35 + **Location:** `internal/server/auth.go` + `internal/tui/onboarding.go` 36 + 37 + ### 4. User Profile Pages 38 + Web interface to view user information: 39 + - `/users` - List all registered users 40 + - `/user/{username}` - Individual user profile showing: 41 + - Name, bio, and link 42 + - SSH public key fingerprint 43 + - Game statistics (rating, wins, losses) 44 + - Join date and last login 45 + 46 + **Location:** `internal/server/users.go` 47 + 48 + ### 5. Leaderboard Integration 49 + Updated leaderboard to link usernames to profiles: 50 + - Clicking a username takes you to their profile 51 + - Shows authentication info alongside game stats 52 + 53 + **Location:** `internal/server/web.go` (updated player name links) 54 + 55 + ## 🔐 Security Features 56 + 57 + 1. **Public key only** - No password authentication accepted 58 + 2. **Username ownership** - One public key per username, cannot be changed 59 + 3. **Key uniqueness** - One public key cannot register multiple usernames 60 + 4. **Automatic verification** - Every connection validates the key 61 + 62 + ## 📝 User Experience 63 + 64 + ### First Connection 65 + ```bash 66 + ssh -p 2222 -i ~/.ssh/id_ed25519 alice@localhost 67 + ``` 68 + 69 + **Prompts:** 70 + ``` 71 + 🚢 Welcome to Battleship Arena! 72 + Setting up account for: alice 73 + 74 + What's your full name? (required): Alice Johnson 75 + Bio (optional, press Enter to skip): CS student and battleship enthusiast 76 + Link (optional, press Enter to skip): https://github.com/alice 77 + 78 + ✅ Account created successfully! 79 + You can now upload your battleship AI and compete! 80 + ``` 81 + 82 + ### Subsequent Connections 83 + ```bash 84 + ssh -p 2222 alice@localhost 85 + # → Immediately shows TUI dashboard (no prompts) 86 + ``` 87 + 88 + ### Uploading Files 89 + ```bash 90 + scp -P 2222 memory_functions_alice.cpp alice@localhost:~/ 91 + # → Works with same key authentication 92 + ``` 93 + 94 + ## 🌐 Web Interface 95 + 96 + ### User List (`/users`) 97 + - Grid view of all registered users 98 + - Shows name, username, bio 99 + - Click to view full profile 100 + 101 + ### User Profile (`/user/alice`) 102 + - Full name and username 103 + - Bio and external link (if provided) 104 + - SSH public key fingerprint (SHA256) 105 + - Game statistics (if they've competed) 106 + - Registration and last login timestamps 107 + 108 + ### Leaderboard (`/`) 109 + - Usernames are now clickable links 110 + - Lead to user profile pages 111 + - Shows rating, wins, losses, etc. 112 + 113 + ## 📂 Files Modified/Created 114 + 115 + ### New Files 116 + - `internal/storage/users.go` - User CRUD operations 117 + - `internal/server/auth.go` - SSH authentication handlers 118 + - `internal/server/users.go` - User profile web handlers 119 + - `internal/tui/onboarding.go` - Onboarding TUI (Bubble Tea model) 120 + - `SSH_AUTH.md` - User-facing documentation 121 + 122 + ### Modified Files 123 + - `internal/storage/database.go` - Added users table to schema 124 + - `cmd/battleship-arena/main.go` - Added auth handlers and user routes 125 + - `internal/server/web.go` - Updated player name links to /user/ 126 + 127 + ## 🚀 Testing 128 + 129 + 1. **Start server:** 130 + ```bash 131 + make run 132 + ``` 133 + 134 + 2. **Connect with new user:** 135 + ```bash 136 + ssh -p 2222 newuser@localhost 137 + ``` 138 + 139 + 3. **View users:** 140 + ``` 141 + http://localhost:8081/users 142 + ``` 143 + 144 + 4. **View profile:** 145 + ``` 146 + http://localhost:8081/user/newuser 147 + ``` 148 + 149 + 5. **Try duplicate username:** 150 + ```bash 151 + # With different SSH key, same username → should be rejected 152 + ``` 153 + 154 + ## 💡 Design Decisions 155 + 156 + 1. **Onboarding in terminal** - Users are already in SSH, so keep it simple 157 + 2. **Public key as primary key** - Ensures one key = one account 158 + 3. **Optional bio/link** - Don't force users to provide info they don't want to share 159 + 4. **SHA256 fingerprint display** - More readable than full public key 160 + 5. **Separate /user/ route** - Distinguishes from game stats at /player/ 161 + 162 + ## 🔄 Migration Path 163 + 164 + Existing deployments will need to: 165 + 1. Run migration to add users table (happens automatically on next startup) 166 + 2. Existing SSH users will be prompted for onboarding on next login 167 + 3. No data loss - submission history remains intact
+60
SSH_AUTH.md
··· 1 + # SSH Public Key Authentication 2 + 3 + The Battleship Arena now uses SSH public key authentication for secure, passwordless access. 4 + 5 + ## First-Time Setup 6 + 7 + 1. **Generate an SSH key** (if you don't have one): 8 + ```bash 9 + ssh-keygen -t ed25519 -f ~/.ssh/battleship_arena 10 + ``` 11 + 12 + 2. **Connect for the first time**: 13 + ```bash 14 + ssh -p 2222 -i ~/.ssh/battleship_arena yourname@localhost 15 + ``` 16 + 17 + 3. **Complete onboarding**: 18 + - Enter your full name (required) 19 + - Enter a bio (optional) 20 + - Enter a website/link (optional) 21 + 22 + 4. **Your public key is now registered!** Only you can access this username. 23 + 24 + ## Uploading Your AI 25 + 26 + After registration, upload your battleship AI: 27 + 28 + ```bash 29 + scp -P 2222 -i ~/.ssh/battleship_arena memory_functions_yourname.cpp yourname@localhost:~/ 30 + ``` 31 + 32 + ## User Profiles 33 + 34 + - View your profile: `https://arena.example.com/user/yourname` 35 + - View all users: `https://arena.example.com/users` 36 + - Profiles display: 37 + - Name, bio, and link 38 + - SSH public key fingerprint 39 + - Game statistics (if you've competed) 40 + 41 + ## Security Features 42 + 43 + - ✅ Public key authentication only (no passwords) 44 + - ✅ Username ownership tied to SSH key 45 + - ✅ Keys cannot be reused for different usernames 46 + - ✅ Automatic key verification on every connection 47 + 48 + ## SSH Config 49 + 50 + Add to `~/.ssh/config` for easy access: 51 + 52 + ``` 53 + Host battleship 54 + HostName localhost 55 + Port 2222 56 + User yourname 57 + IdentityFile ~/.ssh/battleship_arena 58 + ``` 59 + 60 + Then simply: `ssh battleship`
+7 -1
internal/runner/worker.go
··· 37 37 38 38 func processSubmissionsWithLock(uploadDir string, broadcastFunc func(string, int, int, time.Time, []string), notifyFunc func(), completeFunc func()) error { 39 39 if !workerMutex.TryLock() { 40 - log.Printf("Worker already running, skipping this cycle") 40 + // Silently skip if worker is already running 41 41 return nil 42 42 } 43 43 defer workerMutex.Unlock() ··· 49 49 submissions, err := storage.GetPendingSubmissions() 50 50 if err != nil { 51 51 return err 52 + } 53 + 54 + // Only do work if there are pending submissions 55 + if len(submissions) == 0 { 56 + return nil 52 57 } 53 58 54 59 for _, sub := range submissions { ··· 67 72 notifyFunc() 68 73 } 69 74 75 + // Check if queue is now empty 70 76 queuedPlayers := storage.GetQueuedPlayerNames() 71 77 if len(queuedPlayers) == 0 { 72 78 completeFunc()
+224
internal/server/auth.go
··· 1 + package server 2 + 3 + import ( 4 + "errors" 5 + "fmt" 6 + "log" 7 + "os" 8 + "strings" 9 + 10 + "github.com/charmbracelet/ssh" 11 + "github.com/charmbracelet/wish" 12 + gossh "golang.org/x/crypto/ssh" 13 + 14 + "battleship-arena/internal/storage" 15 + ) 16 + 17 + var adminPasscode string 18 + 19 + func init() { 20 + // Load admin passcode from environment variable 21 + adminPasscode = os.Getenv("BATTLESHIP_ADMIN_PASSCODE") 22 + if adminPasscode == "" { 23 + adminPasscode = "battleship-admin-override" // Default fallback 24 + log.Printf("⚠️ Using default admin passcode. Set BATTLESHIP_ADMIN_PASSCODE env var for security.") 25 + } 26 + } 27 + 28 + func PublicKeyAuthHandler(ctx ssh.Context, key ssh.PublicKey) bool { 29 + publicKeyStr := strings.TrimSpace(string(gossh.MarshalAuthorizedKey(key))) 30 + 31 + log.Printf("Auth attempt: user=%s, key_fingerprint=%s", ctx.User(), gossh.FingerprintSHA256(key)) 32 + 33 + // Try to find user by public key 34 + user, err := storage.GetUserByPublicKey(publicKeyStr) 35 + if err != nil { 36 + log.Printf("Error looking up user by public key: %v", err) 37 + return false 38 + } 39 + 40 + if user != nil { 41 + // Existing user - verify username matches 42 + log.Printf("Found existing user: %s (trying to login as: %s)", user.Username, ctx.User()) 43 + if user.Username == ctx.User() { 44 + ctx.SetValue("user_id", user.ID) 45 + ctx.SetValue("needs_onboarding", false) 46 + storage.UpdateUserLastLogin(user.Username) 47 + log.Printf("✓ Authenticated %s", user.Username) 48 + return true 49 + } 50 + // Public key registered to different username 51 + log.Printf("❌ Public key registered to %s, but trying to auth as %s", user.Username, ctx.User()) 52 + return false 53 + } 54 + 55 + log.Printf("New user detected: %s", ctx.User()) 56 + 57 + // New user - check if username is taken 58 + existingUser, err := storage.GetUserByUsername(ctx.User()) 59 + if err != nil { 60 + log.Printf("Error looking up username: %v", err) 61 + return false 62 + } 63 + 64 + if existingUser != nil { 65 + // Username taken by someone else 66 + log.Printf("❌ Username %s already taken", ctx.User()) 67 + return false 68 + } 69 + 70 + // New user with available username - allow and mark for onboarding 71 + log.Printf("✓ New user %s allowed for onboarding", ctx.User()) 72 + ctx.SetValue("public_key", publicKeyStr) 73 + ctx.SetValue("needs_onboarding", true) 74 + return true 75 + } 76 + 77 + func PasswordAuthHandler(ctx ssh.Context, password string) bool { 78 + // Check for admin passcode override 79 + if password == adminPasscode { 80 + log.Printf("🔑 Admin passcode used for user: %s", ctx.User()) 81 + 82 + // Check if user exists 83 + user, err := storage.GetUserByUsername(ctx.User()) 84 + if err != nil { 85 + log.Printf("Error looking up username: %v", err) 86 + return false 87 + } 88 + 89 + if user != nil { 90 + // Existing user - allow login 91 + ctx.SetValue("user_id", user.ID) 92 + ctx.SetValue("needs_onboarding", false) 93 + ctx.SetValue("admin_override", true) 94 + log.Printf("✓ Admin authenticated as %s", user.Username) 95 + return true 96 + } 97 + 98 + // New user - create with dummy key 99 + log.Printf("✓ Admin creating new user: %s", ctx.User()) 100 + dummyKey := fmt.Sprintf("admin-override-%s", ctx.User()) 101 + newUser, err := storage.CreateUser(ctx.User(), ctx.User(), "Admin created user", "", dummyKey) 102 + if err != nil { 103 + log.Printf("Error creating user: %v", err) 104 + return false 105 + } 106 + 107 + ctx.SetValue("user_id", newUser.ID) 108 + ctx.SetValue("needs_onboarding", false) 109 + ctx.SetValue("admin_override", true) 110 + log.Printf("✓ Admin created and authenticated as %s", ctx.User()) 111 + return true 112 + } 113 + 114 + // Regular password auth disabled 115 + return false 116 + } 117 + 118 + func SessionHandler(s ssh.Session) { 119 + needsOnboarding := false 120 + if val := s.Context().Value("needs_onboarding"); val != nil { 121 + needsOnboarding = val.(bool) 122 + } 123 + 124 + if needsOnboarding { 125 + // Run onboarding flow 126 + if err := runOnboarding(s); err != nil { 127 + wish.Errorln(s, fmt.Sprintf("Onboarding failed: %v", err)) 128 + return 129 + } 130 + } 131 + 132 + // Normal session continues 133 + wish.Println(s, "Welcome to Battleship Arena!") 134 + } 135 + 136 + func runOnboarding(s ssh.Session) error { 137 + username := s.User() 138 + publicKeyStr := "" 139 + if val := s.Context().Value("public_key"); val != nil { 140 + publicKeyStr = val.(string) 141 + } 142 + 143 + if publicKeyStr == "" { 144 + return errors.New("no public key found") 145 + } 146 + 147 + wish.Println(s, "\n🚢 Welcome to Battleship Arena!") 148 + wish.Println(s, fmt.Sprintf("Setting up account for: %s\n", username)) 149 + 150 + // Get name 151 + wish.Print(s, "What's your full name? (required): ") 152 + name, err := readLine(s) 153 + if err != nil { 154 + return err 155 + } 156 + if name == "" { 157 + return errors.New("name is required") 158 + } 159 + 160 + // Get bio 161 + wish.Print(s, "Bio (optional, press Enter to skip): ") 162 + bio, err := readLine(s) 163 + if err != nil { 164 + return err 165 + } 166 + 167 + // Get link 168 + wish.Print(s, "Link (optional, press Enter to skip): ") 169 + link, err := readLine(s) 170 + if err != nil { 171 + return err 172 + } 173 + 174 + // Create user 175 + _, err = storage.CreateUser(username, name, bio, link, publicKeyStr) 176 + if err != nil { 177 + return fmt.Errorf("failed to create user: %v", err) 178 + } 179 + 180 + wish.Println(s, "\n✅ Account created successfully!") 181 + wish.Println(s, "You can now upload your battleship AI and compete!\n") 182 + 183 + // Update context 184 + s.Context().SetValue("needs_onboarding", false) 185 + 186 + return nil 187 + } 188 + 189 + func readLine(s ssh.Session) (string, error) { 190 + var line []byte 191 + buf := make([]byte, 1) 192 + 193 + for { 194 + n, err := s.Read(buf) 195 + if err != nil { 196 + return "", err 197 + } 198 + if n == 0 { 199 + continue 200 + } 201 + 202 + b := buf[0] 203 + 204 + // Handle newline 205 + if b == '\n' || b == '\r' { 206 + return string(line), nil 207 + } 208 + 209 + // Handle backspace 210 + if b == 127 || b == 8 { 211 + if len(line) > 0 { 212 + line = line[:len(line)-1] 213 + s.Write([]byte("\b \b")) 214 + } 215 + continue 216 + } 217 + 218 + // Handle printable characters 219 + if b >= 32 && b < 127 { 220 + line = append(line, b) 221 + s.Write(buf[:1]) 222 + } 223 + } 224 + }
+17 -10
internal/server/scp.go
··· 33 33 filename := filepath.Base(entry.Name) 34 34 log.Printf("SCP Write called: entry.Name=%s, filename=%s, size=%d", entry.Name, filename, entry.Size) 35 35 36 - // Skip validation for directory markers (SCP protocol negotiation) 36 + // Skip validation for directory markers 37 37 if filename == "~" || filename == "." || filename == ".." { 38 38 log.Printf("Skipping directory marker: %s", filename) 39 39 return 0, nil 40 40 } 41 41 42 - // Validate filename (must be memory_functions_*.cpp) 42 + // Validate filename 43 43 if !strings.HasPrefix(filename, "memory_functions_") || !strings.HasSuffix(filename, ".cpp") { 44 44 log.Printf("Invalid filename from %s: %s", s.User(), filename) 45 45 return 0, fmt.Errorf("only memory_functions_*.cpp files are accepted") 46 46 } 47 + 48 + // Check if this is an admin override session 49 + isAdmin := false 50 + if val := s.Context().Value("admin_override"); val != nil { 51 + isAdmin = val.(bool) 52 + } 53 + 54 + targetUser := s.User() 55 + if isAdmin { 56 + log.Printf("🔑 Admin override: uploading as %s", targetUser) 57 + } 47 58 48 - userDir := filepath.Join(h.uploadDir, s.User()) 59 + userDir := filepath.Join(h.uploadDir, targetUser) 49 60 if err := os.MkdirAll(userDir, 0755); err != nil { 50 61 log.Printf("Failed to create user directory: %v", err) 51 62 return 0, err 52 63 } 53 64 54 - // Remove old file if it exists to ensure clean overwrite 55 65 targetPath := filepath.Join(userDir, filename) 56 66 if _, err := os.Stat(targetPath); err == nil { 57 67 log.Printf("Removing old file: %s", targetPath) 58 68 os.Remove(targetPath) 59 69 } 60 70 61 - // Modify the entry to write to user's subdirectory 62 71 userEntry := &scp.FileEntry{ 63 - Name: filepath.Join(s.User(), filename), 72 + Name: filepath.Join(targetUser, filename), 64 73 Mode: entry.Mode, 65 74 Size: entry.Size, 66 75 Reader: entry.Reader, ··· 74 83 return n, err 75 84 } 76 85 77 - log.Printf("Uploaded %s from %s (%d bytes)", filename, s.User(), n) 86 + log.Printf("Uploaded %s from %s (%d bytes)", filename, targetUser, n) 78 87 79 - // Add submission and trigger testing 80 - submissionID, err := storage.AddSubmission(s.User(), filename) 88 + submissionID, err := storage.AddSubmission(targetUser, filename) 81 89 if err != nil { 82 90 log.Printf("Failed to add submission: %v", err) 83 91 } else { 84 92 log.Printf("Queued submission %d for testing", submissionID) 85 - // The worker will pick it up automatically 86 93 } 87 94 88 95 return n, nil
+7 -6
internal/server/sse.go
··· 25 25 } 26 26 27 27 func InitSSE() { 28 - SSEServer = sse.NewServer(&sse.Options{ 29 - Logger: log.New(log.Writer(), "go-sse: ", log.Ldate|log.Ltime), 30 - }) 28 + // Disable verbose SSE library logging 29 + SSEServer = sse.NewServer(nil) 31 30 } 32 31 33 32 func NotifyLeaderboardUpdate() { ··· 78 77 return 79 78 } 80 79 81 - log.Printf("Broadcasting progress: %s [%d/%d] %.1f%% (queue: %d)", player, currentMatch, totalMatches, percentComplete, len(filteredQueue)) 80 + // Only log every 10th match to reduce noise 81 + if currentMatch%10 == 0 || currentMatch == totalMatches { 82 + log.Printf("Progress: %s [%d/%d] %.0f%%", player, currentMatch, totalMatches, percentComplete) 83 + } 82 84 83 85 SSEServer.SendMessage("/events/updates", sse.SimpleMessage(string(data))) 84 86 } ··· 109 111 return 110 112 } 111 113 112 - log.Printf("Broadcasting progress complete") 113 - 114 + // Silent - no log needed for routine completion 114 115 SSEServer.SendMessage("/events/updates", sse.SimpleMessage(string(data))) 115 116 }
+380
internal/server/users.go
··· 1 + package server 2 + 3 + import ( 4 + "fmt" 5 + "html/template" 6 + "net/http" 7 + "strings" 8 + 9 + "github.com/go-chi/chi/v5" 10 + gossh "golang.org/x/crypto/ssh" 11 + 12 + "battleship-arena/internal/storage" 13 + ) 14 + 15 + func HandleUserProfile(w http.ResponseWriter, r *http.Request) { 16 + username := chi.URLParam(r, "username") 17 + if username == "" { 18 + http.Redirect(w, r, "/", http.StatusSeeOther) 19 + return 20 + } 21 + 22 + user, err := storage.GetUserByUsername(username) 23 + if err != nil { 24 + http.Error(w, "Error loading user", http.StatusInternalServerError) 25 + return 26 + } 27 + if user == nil { 28 + http.Error(w, "User not found", http.StatusNotFound) 29 + return 30 + } 31 + 32 + // Get user's submission stats 33 + entries, _ := storage.GetLeaderboard(100) 34 + var userEntry *storage.LeaderboardEntry 35 + for _, e := range entries { 36 + if e.Username == username { 37 + userEntry = &e 38 + break 39 + } 40 + } 41 + 42 + // Parse public key for display 43 + publicKeyDisplay := formatPublicKey(user.PublicKey) 44 + 45 + tmpl := template.Must(template.New("user").Parse(userProfileHTML)) 46 + data := struct { 47 + User *storage.User 48 + Entry *storage.LeaderboardEntry 49 + PublicKeyDisplay string 50 + }{ 51 + User: user, 52 + Entry: userEntry, 53 + PublicKeyDisplay: publicKeyDisplay, 54 + } 55 + tmpl.Execute(w, data) 56 + } 57 + 58 + func HandleUsers(w http.ResponseWriter, r *http.Request) { 59 + users, err := storage.GetAllUsers() 60 + if err != nil { 61 + http.Error(w, "Error loading users", http.StatusInternalServerError) 62 + return 63 + } 64 + 65 + tmpl := template.Must(template.New("users").Parse(usersListHTML)) 66 + tmpl.Execute(w, users) 67 + } 68 + 69 + func formatPublicKey(key string) string { 70 + key = strings.TrimSpace(key) 71 + parts := strings.Fields(key) 72 + if len(parts) < 2 { 73 + return key 74 + } 75 + 76 + // Parse the key to get fingerprint 77 + pubKey, _, _, _, err := gossh.ParseAuthorizedKey([]byte(key)) 78 + if err != nil { 79 + return key 80 + } 81 + 82 + fingerprint := gossh.FingerprintSHA256(pubKey) 83 + return fmt.Sprintf("%s %s", parts[0], fingerprint) 84 + } 85 + 86 + const userProfileHTML = ` 87 + <!DOCTYPE html> 88 + <html lang="en"> 89 + <head> 90 + <title>{{.User.Name}} (@{{.User.Username}}) - Battleship Arena</title> 91 + <meta charset="UTF-8"> 92 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 93 + <style> 94 + * { 95 + margin: 0; 96 + padding: 0; 97 + box-sizing: border-box; 98 + } 99 + 100 + body { 101 + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; 102 + background: #0f172a; 103 + color: #e2e8f0; 104 + min-height: 100vh; 105 + padding: 2rem 1rem; 106 + } 107 + 108 + .container { 109 + max-width: 900px; 110 + margin: 0 auto; 111 + } 112 + 113 + .back-link { 114 + display: inline-block; 115 + margin-bottom: 2rem; 116 + color: #60a5fa; 117 + text-decoration: none; 118 + font-size: 0.9rem; 119 + } 120 + 121 + .back-link:hover { 122 + text-decoration: underline; 123 + } 124 + 125 + .profile-header { 126 + background: #1e293b; 127 + border: 1px solid #334155; 128 + border-radius: 12px; 129 + padding: 2rem; 130 + margin-bottom: 2rem; 131 + } 132 + 133 + .username { 134 + font-size: 2rem; 135 + font-weight: 700; 136 + color: #e2e8f0; 137 + margin-bottom: 0.5rem; 138 + } 139 + 140 + .handle { 141 + font-size: 1.2rem; 142 + color: #94a3b8; 143 + margin-bottom: 1rem; 144 + } 145 + 146 + .bio { 147 + color: #cbd5e1; 148 + margin-bottom: 1rem; 149 + line-height: 1.6; 150 + } 151 + 152 + .link { 153 + color: #60a5fa; 154 + text-decoration: none; 155 + } 156 + 157 + .link:hover { 158 + text-decoration: underline; 159 + } 160 + 161 + .stats-grid { 162 + display: grid; 163 + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 164 + gap: 1rem; 165 + margin-bottom: 2rem; 166 + } 167 + 168 + .stat-card { 169 + background: #1e293b; 170 + border: 1px solid #334155; 171 + border-radius: 12px; 172 + padding: 1.5rem; 173 + } 174 + 175 + .stat-label { 176 + font-size: 0.875rem; 177 + color: #94a3b8; 178 + margin-bottom: 0.5rem; 179 + } 180 + 181 + .stat-value { 182 + font-size: 2rem; 183 + font-weight: 700; 184 + color: #60a5fa; 185 + } 186 + 187 + .key-section { 188 + background: #1e293b; 189 + border: 1px solid #334155; 190 + border-radius: 12px; 191 + padding: 2rem; 192 + } 193 + 194 + .section-title { 195 + font-size: 1.25rem; 196 + font-weight: 600; 197 + margin-bottom: 1rem; 198 + color: #e2e8f0; 199 + } 200 + 201 + .key-display { 202 + background: #0f172a; 203 + padding: 1rem; 204 + border-radius: 8px; 205 + font-family: 'Monaco', 'Courier New', monospace; 206 + font-size: 0.875rem; 207 + color: #94a3b8; 208 + word-break: break-all; 209 + } 210 + 211 + .metadata { 212 + display: grid; 213 + grid-template-columns: repeat(2, 1fr); 214 + gap: 1rem; 215 + margin-top: 1rem; 216 + font-size: 0.875rem; 217 + color: #64748b; 218 + } 219 + </style> 220 + </head> 221 + <body> 222 + <div class="container"> 223 + <a href="/" class="back-link">← Back to Leaderboard</a> 224 + 225 + <div class="profile-header"> 226 + <div class="username">{{.User.Name}}</div> 227 + <div class="handle">@{{.User.Username}}</div> 228 + {{if .User.Bio}} 229 + <div class="bio">{{.User.Bio}}</div> 230 + {{end}} 231 + {{if .User.Link}} 232 + <a href="{{.User.Link}}" class="link" target="_blank">🔗 {{.User.Link}}</a> 233 + {{end}} 234 + </div> 235 + 236 + {{if .Entry}} 237 + <div class="stats-grid"> 238 + <div class="stat-card"> 239 + <div class="stat-label">Rating</div> 240 + <div class="stat-value">{{.Entry.Rating}}</div> 241 + </div> 242 + <div class="stat-card"> 243 + <div class="stat-label">Wins</div> 244 + <div class="stat-value">{{.Entry.Wins}}</div> 245 + </div> 246 + <div class="stat-card"> 247 + <div class="stat-label">Losses</div> 248 + <div class="stat-value">{{.Entry.Losses}}</div> 249 + </div> 250 + <div class="stat-card"> 251 + <div class="stat-label">Win Rate</div> 252 + <div class="stat-value">{{printf "%.1f" .Entry.WinPct}}%</div> 253 + </div> 254 + </div> 255 + {{end}} 256 + 257 + <div class="key-section"> 258 + <h2 class="section-title">SSH Public Key</h2> 259 + <div class="key-display">{{.PublicKeyDisplay}}</div> 260 + <div class="metadata"> 261 + <div>Member since: {{.User.CreatedAt.Format "Jan 2, 2006"}}</div> 262 + <div>Last login: {{.User.LastLoginAt.Format "Jan 2, 3:04 PM"}}</div> 263 + </div> 264 + </div> 265 + </div> 266 + </body> 267 + </html> 268 + ` 269 + 270 + const usersListHTML = ` 271 + <!DOCTYPE html> 272 + <html lang="en"> 273 + <head> 274 + <title>Users - Battleship Arena</title> 275 + <meta charset="UTF-8"> 276 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 277 + <style> 278 + * { 279 + margin: 0; 280 + padding: 0; 281 + box-sizing: border-box; 282 + } 283 + 284 + body { 285 + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; 286 + background: #0f172a; 287 + color: #e2e8f0; 288 + min-height: 100vh; 289 + padding: 2rem 1rem; 290 + } 291 + 292 + .container { 293 + max-width: 1200px; 294 + margin: 0 auto; 295 + } 296 + 297 + h1 { 298 + font-size: 2.5rem; 299 + font-weight: 700; 300 + margin-bottom: 0.5rem; 301 + background: linear-gradient(135deg, #60a5fa 0%, #a78bfa 100%); 302 + -webkit-background-clip: text; 303 + -webkit-text-fill-color: transparent; 304 + } 305 + 306 + .back-link { 307 + display: inline-block; 308 + margin-bottom: 2rem; 309 + color: #60a5fa; 310 + text-decoration: none; 311 + font-size: 0.9rem; 312 + } 313 + 314 + .back-link:hover { 315 + text-decoration: underline; 316 + } 317 + 318 + .users-grid { 319 + display: grid; 320 + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); 321 + gap: 1.5rem; 322 + } 323 + 324 + .user-card { 325 + background: #1e293b; 326 + border: 1px solid #334155; 327 + border-radius: 12px; 328 + padding: 1.5rem; 329 + transition: transform 0.2s, border-color 0.2s; 330 + text-decoration: none; 331 + color: inherit; 332 + display: block; 333 + } 334 + 335 + .user-card:hover { 336 + transform: translateY(-2px); 337 + border-color: #60a5fa; 338 + } 339 + 340 + .user-name { 341 + font-size: 1.25rem; 342 + font-weight: 600; 343 + color: #e2e8f0; 344 + margin-bottom: 0.25rem; 345 + } 346 + 347 + .user-handle { 348 + font-size: 0.9rem; 349 + color: #94a3b8; 350 + margin-bottom: 0.75rem; 351 + } 352 + 353 + .user-bio { 354 + font-size: 0.875rem; 355 + color: #cbd5e1; 356 + line-height: 1.5; 357 + } 358 + </style> 359 + </head> 360 + <body> 361 + <div class="container"> 362 + <a href="/" class="back-link">← Back to Leaderboard</a> 363 + <h1>Players</h1> 364 + <p style="color: #94a3b8; margin-bottom: 2rem;">{{len .}} registered users</p> 365 + 366 + <div class="users-grid"> 367 + {{range .}} 368 + <a href="/user/{{.Username}}" class="user-card"> 369 + <div class="user-name">{{.Name}}</div> 370 + <div class="user-handle">@{{.Username}}</div> 371 + {{if .Bio}} 372 + <div class="user-bio">{{.Bio}}</div> 373 + {{end}} 374 + </a> 375 + {{end}} 376 + </div> 377 + </div> 378 + </body> 379 + </html> 380 + `
+11 -4
internal/server/web.go
··· 448 448 449 449 return '<tr>' + 450 450 '<td class="rank rank-' + rank + '">' + medal + '</td>' + 451 - '<td class="player-name"><a href="/player/' + e.Username + '" style="color: inherit; text-decoration: none;">' + e.Username + '</a></td>' + 451 + '<td class="player-name"><a href="/user/' + e.Username + '" style="color: inherit; text-decoration: none;">' + e.Username + '</a></td>' + 452 452 '<td><strong>' + e.Rating + '</strong> <span style="color: #94a3b8; font-size: 0.85em;">±' + e.RD + '</span></td>' + 453 453 '<td>' + e.Wins.toLocaleString() + '</td>' + 454 454 '<td>' + e.Losses.toLocaleString() + '</td>' + ··· 555 555 {{range $i, $e := .Entries}} 556 556 <tr> 557 557 <td class="rank rank-{{add $i 1}}">{{if lt $i 3}}{{medal $i}}{{else}}{{add $i 1}}{{end}}</td> 558 - <td class="player-name"><a href="/player/{{$e.Username}}" style="color: inherit; text-decoration: none;">{{$e.Username}}</a></td> 558 + <td class="player-name"><a href="/user/{{$e.Username}}" style="color: inherit; text-decoration: none;">{{$e.Username}}</a></td> 559 559 <td><strong>{{$e.Rating}}</strong> <span style="color: #94a3b8; font-size: 0.85em;">±{{$e.RD}}</span></td> 560 560 <td>{{$e.Wins}}</td> 561 561 <td>{{$e.Losses}}</td> ··· 580 580 581 581 <div class="info-card"> 582 582 <h3>📤 How to Submit</h3> 583 - <p>Connect via SSH to submit your battleship AI:</p> 583 + <p><strong>First time?</strong> Connect via SSH to create your account:</p> 584 584 <p><code>ssh -p 2222 username@localhost</code></p> 585 - <p style="margin-top: 1rem;">Upload your <code>memory_functions_*.cpp</code> file and compete in the arena!</p> 585 + <p style="margin-top: 0.5rem; color: #94a3b8;">You'll be prompted for your name, bio, and link. Your SSH key will be registered.</p> 586 + 587 + <p style="margin-top: 1rem;"><strong>Upload your AI:</strong></p> 588 + <p><code>scp -P 2222 memory_functions_yourname.cpp username@localhost:~/</code></p> 589 + 590 + <p style="margin-top: 1rem; color: #94a3b8;"> 591 + <a href="/users" style="color: #60a5fa;">View all players →</a> 592 + </p> 586 593 </div> 587 594 </div> 588 595
+11
internal/storage/database.go
··· 77 77 } 78 78 79 79 schema := ` 80 + CREATE TABLE IF NOT EXISTS users ( 81 + id INTEGER PRIMARY KEY AUTOINCREMENT, 82 + username TEXT UNIQUE NOT NULL, 83 + name TEXT NOT NULL, 84 + bio TEXT, 85 + link TEXT, 86 + public_key TEXT UNIQUE NOT NULL, 87 + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 88 + last_login_at TIMESTAMP 89 + ); 90 + 80 91 CREATE TABLE IF NOT EXISTS submissions ( 81 92 id INTEGER PRIMARY KEY AUTOINCREMENT, 82 93 username TEXT NOT NULL,
+124
internal/storage/users.go
··· 1 + package storage 2 + 3 + import ( 4 + "database/sql" 5 + "strings" 6 + "time" 7 + ) 8 + 9 + type User struct { 10 + ID int 11 + Username string 12 + Name string 13 + Bio string 14 + Link string 15 + PublicKey string 16 + CreatedAt time.Time 17 + LastLoginAt time.Time 18 + } 19 + 20 + func GetUserByUsername(username string) (*User, error) { 21 + var u User 22 + var lastLogin sql.NullTime 23 + err := DB.QueryRow( 24 + `SELECT id, username, name, bio, link, public_key, created_at, last_login_at 25 + FROM users WHERE username = ?`, 26 + username, 27 + ).Scan(&u.ID, &u.Username, &u.Name, &u.Bio, &u.Link, &u.PublicKey, &u.CreatedAt, &lastLogin) 28 + 29 + if err == sql.ErrNoRows { 30 + return nil, nil 31 + } 32 + if err != nil { 33 + return nil, err 34 + } 35 + 36 + if lastLogin.Valid { 37 + u.LastLoginAt = lastLogin.Time 38 + } 39 + 40 + return &u, nil 41 + } 42 + 43 + func GetUserByPublicKey(publicKey string) (*User, error) { 44 + publicKey = strings.TrimSpace(publicKey) 45 + 46 + var u User 47 + var lastLogin sql.NullTime 48 + err := DB.QueryRow( 49 + `SELECT id, username, name, bio, link, public_key, created_at, last_login_at 50 + FROM users WHERE TRIM(public_key) = ?`, 51 + publicKey, 52 + ).Scan(&u.ID, &u.Username, &u.Name, &u.Bio, &u.Link, &u.PublicKey, &u.CreatedAt, &lastLogin) 53 + 54 + if err == sql.ErrNoRows { 55 + return nil, nil 56 + } 57 + if err != nil { 58 + return nil, err 59 + } 60 + 61 + if lastLogin.Valid { 62 + u.LastLoginAt = lastLogin.Time 63 + } 64 + 65 + return &u, nil 66 + } 67 + 68 + func CreateUser(username, name, bio, link, publicKey string) (*User, error) { 69 + result, err := DB.Exec( 70 + `INSERT INTO users (username, name, bio, link, public_key, created_at, last_login_at) 71 + VALUES (?, ?, ?, ?, ?, ?, ?)`, 72 + username, name, bio, link, publicKey, time.Now(), time.Now(), 73 + ) 74 + if err != nil { 75 + return nil, err 76 + } 77 + 78 + id, _ := result.LastInsertId() 79 + return &User{ 80 + ID: int(id), 81 + Username: username, 82 + Name: name, 83 + Bio: bio, 84 + Link: link, 85 + PublicKey: publicKey, 86 + CreatedAt: time.Now(), 87 + LastLoginAt: time.Now(), 88 + }, nil 89 + } 90 + 91 + func UpdateUserLastLogin(username string) error { 92 + _, err := DB.Exec( 93 + "UPDATE users SET last_login_at = ? WHERE username = ?", 94 + time.Now(), username, 95 + ) 96 + return err 97 + } 98 + 99 + func GetAllUsers() ([]User, error) { 100 + rows, err := DB.Query( 101 + `SELECT id, username, name, bio, link, public_key, created_at, last_login_at 102 + FROM users ORDER BY created_at DESC`, 103 + ) 104 + if err != nil { 105 + return nil, err 106 + } 107 + defer rows.Close() 108 + 109 + var users []User 110 + for rows.Next() { 111 + var u User 112 + var lastLogin sql.NullTime 113 + err := rows.Scan(&u.ID, &u.Username, &u.Name, &u.Bio, &u.Link, &u.PublicKey, &u.CreatedAt, &lastLogin) 114 + if err != nil { 115 + return nil, err 116 + } 117 + if lastLogin.Valid { 118 + u.LastLoginAt = lastLogin.Time 119 + } 120 + users = append(users, u) 121 + } 122 + 123 + return users, rows.Err() 124 + }
+160
internal/tui/onboarding.go
··· 1 + package tui 2 + 3 + import ( 4 + "fmt" 5 + "log" 6 + "strings" 7 + 8 + tea "github.com/charmbracelet/bubbletea" 9 + "github.com/charmbracelet/lipgloss" 10 + 11 + "battleship-arena/internal/storage" 12 + ) 13 + 14 + type OnboardingModel struct { 15 + username string 16 + publicKey string 17 + step int // 0=name, 1=bio, 2=link, 3=done 18 + name string 19 + bio string 20 + link string 21 + input string 22 + err error 23 + width int 24 + height int 25 + completed bool 26 + } 27 + 28 + type onboardingCompleteMsg struct { 29 + username string 30 + } 31 + 32 + func NewOnboardingModel(username, publicKey string, width, height int) OnboardingModel { 33 + return OnboardingModel{ 34 + username: username, 35 + publicKey: publicKey, 36 + step: 0, 37 + width: width, 38 + height: height, 39 + completed: false, 40 + } 41 + } 42 + 43 + func (m OnboardingModel) Init() tea.Cmd { 44 + return nil 45 + } 46 + 47 + func (m OnboardingModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 48 + switch msg := msg.(type) { 49 + case tea.KeyMsg: 50 + switch msg.String() { 51 + case "ctrl+c": 52 + return m, tea.Quit 53 + case "enter": 54 + switch m.step { 55 + case 0: // Name 56 + if strings.TrimSpace(m.input) == "" { 57 + m.err = fmt.Errorf("name is required") 58 + return m, nil 59 + } 60 + m.name = strings.TrimSpace(m.input) 61 + m.input = "" 62 + m.err = nil 63 + m.step = 1 64 + case 1: // Bio 65 + m.bio = strings.TrimSpace(m.input) 66 + m.input = "" 67 + m.err = nil 68 + m.step = 2 69 + case 2: // Link 70 + m.link = strings.TrimSpace(m.input) 71 + m.step = 3 72 + 73 + // Create user in database 74 + _, err := storage.CreateUser(m.username, m.name, m.bio, m.link, m.publicKey) 75 + if err != nil { 76 + log.Printf("Failed to create user: %v", err) 77 + m.err = fmt.Errorf("failed to create account") 78 + m.step = 2 79 + return m, nil 80 + } 81 + 82 + m.completed = true 83 + return m, func() tea.Msg { 84 + return onboardingCompleteMsg{username: m.username} 85 + } 86 + } 87 + case "backspace": 88 + if len(m.input) > 0 { 89 + m.input = m.input[:len(m.input)-1] 90 + } 91 + default: 92 + if len(msg.String()) == 1 { 93 + m.input += msg.String() 94 + } 95 + } 96 + case tea.WindowSizeMsg: 97 + m.width = msg.Width 98 + m.height = msg.Height 99 + case onboardingCompleteMsg: 100 + // Transition to main model 101 + mainModel := InitialModel(m.username, m.width, m.height) 102 + return mainModel, mainModel.Init() 103 + } 104 + return m, nil 105 + } 106 + 107 + func (m OnboardingModel) View() string { 108 + if m.completed { 109 + successStyle := lipgloss.NewStyle(). 110 + Foreground(lipgloss.Color("green")). 111 + Bold(true) 112 + return successStyle.Render("\n✅ Account created successfully!\n\nLoading dashboard...\n") 113 + } 114 + 115 + var b strings.Builder 116 + 117 + titleStyle := lipgloss.NewStyle(). 118 + Bold(true). 119 + Foreground(lipgloss.Color("205")). 120 + MarginTop(1). 121 + MarginBottom(1) 122 + 123 + promptStyle := lipgloss.NewStyle(). 124 + Foreground(lipgloss.Color("86")) 125 + 126 + inputStyle := lipgloss.NewStyle(). 127 + Foreground(lipgloss.Color("212")). 128 + Bold(true) 129 + 130 + errorStyle := lipgloss.NewStyle(). 131 + Foreground(lipgloss.Color("196")) 132 + 133 + helpStyle := lipgloss.NewStyle(). 134 + Foreground(lipgloss.Color("240")) 135 + 136 + b.WriteString(titleStyle.Render("🚢 Welcome to Battleship Arena!")) 137 + b.WriteString("\n\n") 138 + b.WriteString(fmt.Sprintf("Setting up account for: %s\n\n", m.username)) 139 + 140 + if m.err != nil { 141 + b.WriteString(errorStyle.Render(fmt.Sprintf("❌ %s\n\n", m.err.Error()))) 142 + } 143 + 144 + switch m.step { 145 + case 0: 146 + b.WriteString(promptStyle.Render("What's your full name?") + " (required)\n") 147 + b.WriteString(inputStyle.Render(m.input + "█") + "\n\n") 148 + b.WriteString(helpStyle.Render("Press Enter to continue")) 149 + case 1: 150 + b.WriteString(promptStyle.Render("Bio:") + " (optional, press Enter to skip)\n") 151 + b.WriteString(inputStyle.Render(m.input + "█") + "\n\n") 152 + b.WriteString(helpStyle.Render("A short description about yourself")) 153 + case 2: 154 + b.WriteString(promptStyle.Render("Link:") + " (optional, press Enter to skip)\n") 155 + b.WriteString(inputStyle.Render(m.input + "█") + "\n\n") 156 + b.WriteString(helpStyle.Render("Website, GitHub, or social media link")) 157 + } 158 + 159 + return b.String() 160 + }
+66
scripts/README.md
··· 1 + # Scripts 2 + 3 + Helper scripts for testing and development. 4 + 5 + ## Batch Upload 6 + 7 + ### `batch-upload.sh` 8 + Uploads all test AIs using admin passcode authentication. 9 + 10 + ```bash 11 + ./scripts/batch-upload.sh 12 + ``` 13 + 14 + **What it does:** 15 + - Uses admin passcode to authenticate as different users 16 + - Auto-creates users if they don't exist 17 + - Uploads each test AI file 18 + - Queues all submissions for testing 19 + 20 + **Admin passcode:** 21 + - Default: `battleship-admin-override` 22 + - Override via: `BATTLESHIP_ADMIN_PASSCODE` env var 23 + 24 + ## Test Submissions 25 + 26 + The `test-submissions/` directory contains sample AI implementations for testing: 27 + 28 + - `memory_functions_random.cpp` - Random shooting 29 + - `memory_functions_hunter.cpp` - Hunt mode after first hit 30 + - `memory_functions_diagonal.cpp` - Diagonal scanning pattern 31 + - `memory_functions_parity.cpp` - Checkerboard pattern 32 + - `memory_functions_probability.cpp` - Probability density 33 + - `memory_functions_cluster.cpp` - Clustered targeting 34 + - `memory_functions_edge.cpp` - Edge-first strategy 35 + - `memory_functions_spiral.cpp` - Spiral scanning 36 + - `memory_functions_snake.cpp` - Snake pattern 37 + - `memory_functions_klukas.cpp` - Advanced algorithm 38 + 39 + ## Benchmark Script 40 + 41 + ### `benchmark_random` 42 + Runs pure random shooting baseline (compiled C++ binary). 43 + 44 + ```bash 45 + ./scripts/benchmark_random 1000 46 + ``` 47 + 48 + Outputs average moves over N games for comparison. 49 + 50 + ## Quick Start 51 + 52 + **Upload test AIs:** 53 + ```bash 54 + # Run batch upload with admin passcode 55 + ./scripts/batch-upload.sh 56 + ``` 57 + 58 + **Manual upload (with SSH key):** 59 + ```bash 60 + # Upload as yourself 61 + scp -P 2222 memory_functions_yourname.cpp username@localhost:~/ 62 + ``` 63 + 64 + **View results:** 65 + - Web UI: http://localhost:8081 66 + - All users: http://localhost:8081/users
+94
scripts/batch-upload.sh
··· 1 + #!/bin/bash 2 + 3 + # Batch upload script - uploads all test submissions using admin passcode 4 + # This bypasses normal SSH key authentication for testing/setup 5 + 6 + HOST="localhost" 7 + PORT="2222" 8 + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 9 + 10 + # Admin passcode (set via environment variable or use default) 11 + ADMIN_PASSCODE="${BATTLESHIP_ADMIN_PASSCODE:-battleship-admin-override}" 12 + 13 + echo "🚢 Battleship Arena - Batch Upload Script (Admin Mode)" 14 + echo "=======================================================" 15 + echo "" 16 + echo "This script uses admin passcode authentication to:" 17 + echo " 1. Auto-create users if they don't exist" 18 + echo " 2. Upload AI files for each user" 19 + echo " 3. Queue submissions for testing" 20 + echo "" 21 + echo "⚠️ Admin passcode: ${ADMIN_PASSCODE:0:10}..." 22 + echo "" 23 + echo "Press Enter to continue or Ctrl+C to cancel..." 24 + read 25 + 26 + # Define all submissions: username, filename 27 + declare -a SUBMISSIONS=( 28 + "alice:memory_functions_random.cpp" 29 + "bob:memory_functions_hunter.cpp" 30 + "charlie:memory_functions_klukas.cpp" 31 + "dave:memory_functions_diagonal.cpp" 32 + "eve:memory_functions_edge.cpp" 33 + "frank:memory_functions_spiral.cpp" 34 + "grace:memory_functions_parity.cpp" 35 + "henry:memory_functions_probability.cpp" 36 + "iris:memory_functions_cluster.cpp" 37 + "jack:memory_functions_snake.cpp" 38 + ) 39 + 40 + # Upload each submission using admin passcode 41 + success_count=0 42 + fail_count=0 43 + 44 + for submission in "${SUBMISSIONS[@]}"; do 45 + IFS=':' read -r username filename <<< "$submission" 46 + 47 + echo "📤 Uploading for user: $username" 48 + echo " File: test-submissions/$filename" 49 + 50 + if [ ! -f "$SCRIPT_DIR/test-submissions/$filename" ]; then 51 + echo "❌ Error: File not found" 52 + ((fail_count++)) 53 + echo "" 54 + continue 55 + fi 56 + 57 + # Use sshpass to provide password authentication 58 + # If sshpass not available, use expect or manual password entry 59 + if command -v sshpass &> /dev/null; then 60 + sshpass -p "$ADMIN_PASSCODE" scp -P $PORT "$SCRIPT_DIR/test-submissions/$filename" "$username@$HOST:~/$filename" 2>&1 | grep -q "100%" 61 + result=$? 62 + else 63 + echo " Using manual password authentication (enter passcode when prompted)" 64 + echo " Password: $ADMIN_PASSCODE" 65 + scp -P $PORT "$SCRIPT_DIR/test-submissions/$filename" "$username@$HOST:~/$filename" 2>&1 | grep -q "100%" 66 + result=$? 67 + fi 68 + 69 + if [ $result -eq 0 ]; then 70 + echo "✅ Upload successful for $username" 71 + ((success_count++)) 72 + else 73 + echo "❌ Upload failed for $username" 74 + ((fail_count++)) 75 + fi 76 + echo "" 77 + 78 + # Small delay to avoid overwhelming the server 79 + sleep 0.5 80 + done 81 + 82 + echo "=======================================================" 83 + echo "✨ Batch upload complete!" 84 + echo "" 85 + echo "Results:" 86 + echo " ✅ Successful: $success_count" 87 + echo " ❌ Failed: $fail_count" 88 + echo "" 89 + echo "Next steps:" 90 + echo " - View web leaderboard: http://$HOST:8081" 91 + echo " - View all users: http://$HOST:8081/users" 92 + echo " - Check compilation logs in server output" 93 + echo "" 94 + echo "Note: Compilation and testing will happen automatically in the background."
-55
scripts/test-upload.sh
··· 1 - #!/bin/bash 2 - 3 - # Test script to upload all AI submissions 4 - 5 - HOST="0.0.0.0" 6 - PORT="2222" 7 - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 8 - 9 - echo "🚢 Battleship Arena - Test Submission Script" 10 - echo "==============================================" 11 - echo "" 12 - 13 - # Define all submissions: username, filename 14 - declare -a SUBMISSIONS=( 15 - "alice:memory_functions_random.cpp" 16 - "bob:memory_functions_hunter.cpp" 17 - "charlie:memory_functions_klukas.cpp" 18 - "dave:memory_functions_diagonal.cpp" 19 - "eve:memory_functions_edge.cpp" 20 - "frank:memory_functions_spiral.cpp" 21 - "grace:memory_functions_parity.cpp" 22 - "henry:memory_functions_probability.cpp" 23 - "iris:memory_functions_cluster.cpp" 24 - "jack:memory_functions_snake.cpp" 25 - ) 26 - 27 - # Upload each submission 28 - for submission in "${SUBMISSIONS[@]}"; do 29 - IFS=':' read -r username filename <<< "$submission" 30 - 31 - echo "📤 Uploading for user: $username" 32 - echo " File: test-submissions/$filename" 33 - 34 - if [ -f "$SCRIPT_DIR/test-submissions/$filename" ]; then 35 - # Capture SCP output to check for 100% completion 36 - scp_output=$(scp -P $PORT "$SCRIPT_DIR/test-submissions/$filename" "$username@$HOST:~/$filename" 2>&1) 37 - echo "$scp_output" | grep -q "100%" 38 - if [ $? -eq 0 ]; then 39 - echo "✅ Upload successful for $username" 40 - else 41 - echo "❌ Upload failed for $username" 42 - echo "$scp_output" 43 - fi 44 - else 45 - echo "❌ Error: File not found" 46 - fi 47 - echo "" 48 - done 49 - 50 - echo "==============================================" 51 - echo "✨ All submissions uploaded!" 52 - echo "" 53 - echo "You can now:" 54 - echo " - SSH to view the TUI: ssh -p $PORT alice@$HOST" 55 - echo " - Check the web leaderboard: http://$HOST:8080"