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.

chore: fix ui

+175 -126
+30 -2
database.go
··· 26 26 } 27 27 28 28 func initDB(path string) (*sql.DB, error) { 29 - db, err := sql.Open("sqlite3", path) 29 + db, err := sql.Open("sqlite3", path+"?parseTime=true") 30 30 if err != nil { 31 31 return nil, err 32 32 } ··· 83 83 var entries []LeaderboardEntry 84 84 for rows.Next() { 85 85 var e LeaderboardEntry 86 - err := rows.Scan(&e.Username, &e.Wins, &e.Losses, &e.AvgMoves, &e.LastPlayed) 86 + var lastPlayed string 87 + err := rows.Scan(&e.Username, &e.Wins, &e.Losses, &e.AvgMoves, &lastPlayed) 87 88 if err != nil { 88 89 return nil, err 89 90 } 91 + 92 + // Parse the timestamp string 93 + e.LastPlayed, _ = time.Parse("2006-01-02 15:04:05", lastPlayed) 94 + 90 95 entries = append(entries, e) 91 96 } 92 97 ··· 138 143 139 144 return submissions, rows.Err() 140 145 } 146 + 147 + func getUserSubmissions(username string) ([]Submission, error) { 148 + rows, err := globalDB.Query( 149 + "SELECT id, username, filename, upload_time, status FROM submissions WHERE username = ? ORDER BY upload_time DESC LIMIT 10", 150 + username, 151 + ) 152 + if err != nil { 153 + return nil, err 154 + } 155 + defer rows.Close() 156 + 157 + var submissions []Submission 158 + for rows.Next() { 159 + var s Submission 160 + err := rows.Scan(&s.ID, &s.Username, &s.Filename, &s.UploadTime, &s.Status) 161 + if err != nil { 162 + return nil, err 163 + } 164 + submissions = append(submissions, s) 165 + } 166 + 167 + return submissions, rows.Err() 168 + }
+87 -103
model.go
··· 3 3 import ( 4 4 "fmt" 5 5 "strings" 6 + "time" 6 7 7 8 tea "github.com/charmbracelet/bubbletea" 8 9 "github.com/charmbracelet/lipgloss" 9 10 ) 10 11 11 - type menuChoice int 12 - 13 - const ( 14 - menuUpload menuChoice = iota 15 - menuLeaderboard 16 - menuSubmit 17 - menuHelp 18 - menuQuit 19 - ) 20 - 21 12 type model struct { 22 13 username string 23 14 width int 24 15 height int 25 - choice menuChoice 26 - submitting bool 27 - filename string 28 - fileContent []byte 29 - message string 16 + submissions []Submission 30 17 leaderboard []LeaderboardEntry 31 18 } 32 19 33 20 func initialModel(username string, width, height int) model { 34 21 return model{ 35 - username: username, 36 - width: width, 37 - height: height, 38 - choice: menuUpload, 22 + username: username, 23 + width: width, 24 + height: height, 25 + submissions: []Submission{}, 26 + leaderboard: []LeaderboardEntry{}, 39 27 } 40 28 } 41 29 42 30 func (m model) Init() tea.Cmd { 43 - return loadLeaderboard 31 + return tea.Batch(loadLeaderboard, loadSubmissions(m.username), tickCmd()) 44 32 } 45 33 46 34 func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { ··· 49 37 switch msg.String() { 50 38 case "ctrl+c", "q": 51 39 return m, tea.Quit 52 - case "up", "k": 53 - if m.choice > 0 { 54 - m.choice-- 55 - } 56 - case "down", "j": 57 - if m.choice < menuQuit { 58 - m.choice++ 59 - } 60 - case "enter": 61 - return m.handleSelection() 62 40 } 63 41 case tea.WindowSizeMsg: 64 42 m.width = msg.Width 65 43 m.height = msg.Height 66 44 case leaderboardMsg: 67 45 m.leaderboard = msg.entries 46 + case submissionsMsg: 47 + m.submissions = msg.submissions 48 + case tickMsg: 49 + return m, tea.Batch(loadLeaderboard, loadSubmissions(m.username), tickCmd()) 68 50 } 69 51 return m, nil 70 52 } 71 53 72 - func (m model) handleSelection() (tea.Model, tea.Cmd) { 73 - switch m.choice { 74 - case menuUpload: 75 - m.message = fmt.Sprintf("Upload via SCP:\nscp -P %s memory_functions_yourname.cpp %s@%s:~/", sshPort, m.username, host) 76 - return m, nil 77 - case menuLeaderboard: 78 - return m, loadLeaderboard 79 - case menuSubmit: 80 - m.message = "Submission queued for testing..." 81 - return m, submitForTesting(m.username) 82 - case menuHelp: 83 - helpText := `Battleship Arena - How to Compete 84 54 85 - 1. Create your AI implementation (memory_functions_*.cpp) 86 - 2. Upload via SCP from your terminal: 87 - scp -P ` + sshPort + ` memory_functions_yourname.cpp ` + m.username + `@` + host + `:~/ 88 - 3. Select "Test Submission" to queue your AI for testing 89 - 4. Check the leaderboard to see your ranking! 90 - 91 - Your AI will be tested against the random AI baseline. 92 - Win rate and average moves determine your ranking.` 93 - m.message = helpText 94 - return m, nil 95 - case menuQuit: 96 - return m, tea.Quit 97 - } 98 - return m, nil 99 - } 100 55 101 56 func (m model) View() string { 102 57 var b strings.Builder ··· 106 61 107 62 b.WriteString(fmt.Sprintf("User: %s\n\n", m.username)) 108 63 109 - // Menu 110 - menuStyle := lipgloss.NewStyle().PaddingLeft(2) 111 - selectedStyle := lipgloss.NewStyle(). 112 - Foreground(lipgloss.Color("170")). 113 - Bold(true). 114 - PaddingLeft(1) 115 - 116 - for i := menuChoice(0); i <= menuQuit; i++ { 117 - cursor := " " 118 - style := menuStyle 119 - if i == m.choice { 120 - cursor = ">" 121 - style = selectedStyle 122 - } 123 - b.WriteString(style.Render(fmt.Sprintf("%s %s\n", cursor, menuText(i)))) 124 - } 64 + // Upload instructions 65 + infoStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("86")) 66 + b.WriteString(infoStyle.Render(fmt.Sprintf("Upload via: scp -P %s memory_functions_yourname.cpp %s@%s:~/", sshPort, m.username, host))) 67 + b.WriteString("\n\n") 125 68 126 - if m.message != "" { 127 - b.WriteString("\n" + lipgloss.NewStyle(). 128 - Foreground(lipgloss.Color("86")). 129 - Render(m.message) + "\n") 69 + // Show submissions 70 + if len(m.submissions) > 0 { 71 + b.WriteString(renderSubmissions(m.submissions)) 72 + b.WriteString("\n") 130 73 } 131 74 132 75 // Show leaderboard if loaded 133 76 if len(m.leaderboard) > 0 { 134 - b.WriteString("\n" + renderLeaderboard(m.leaderboard)) 77 + b.WriteString(renderLeaderboard(m.leaderboard)) 135 78 } 136 79 137 - b.WriteString("\n\nPress q to quit, ↑/↓ to navigate, enter to select") 80 + b.WriteString("\n\nPress q to quit") 138 81 139 82 return b.String() 140 83 } 141 84 142 - func menuText(c menuChoice) string { 143 - switch c { 144 - case menuUpload: 145 - return "Upload Submission" 146 - case menuLeaderboard: 147 - return "View Leaderboard" 148 - case menuSubmit: 149 - return "Test Submission" 150 - case menuHelp: 151 - return "Help" 152 - case menuQuit: 153 - return "Quit" 154 - default: 155 - return "Unknown" 156 - } 157 - } 85 + 158 86 159 87 type leaderboardMsg struct { 160 88 entries []LeaderboardEntry ··· 168 96 return leaderboardMsg{entries: entries} 169 97 } 170 98 171 - type submitMsg struct { 172 - success bool 173 - message string 99 + type submissionsMsg struct { 100 + submissions []Submission 174 101 } 175 102 176 - func submitForTesting(username string) tea.Cmd { 103 + func loadSubmissions(username string) tea.Cmd { 177 104 return func() tea.Msg { 178 - // Queue submission for testing 179 - if err := queueSubmission(username); err != nil { 180 - return submitMsg{success: false, message: err.Error()} 105 + submissions, err := getUserSubmissions(username) 106 + if err != nil { 107 + return submissionsMsg{submissions: nil} 108 + } 109 + return submissionsMsg{submissions: submissions} 110 + } 111 + } 112 + 113 + type tickMsg time.Time 114 + 115 + func tickCmd() tea.Cmd { 116 + return tea.Tick(time.Second*5, func(t time.Time) tea.Msg { 117 + return tickMsg(t) 118 + }) 119 + } 120 + 121 + func renderSubmissions(submissions []Submission) string { 122 + var b strings.Builder 123 + b.WriteString(lipgloss.NewStyle().Bold(true).Render("📤 Your Submissions") + "\n\n") 124 + 125 + headerStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("240")) 126 + b.WriteString(headerStyle.Render(fmt.Sprintf("%-35s %-15s %s\n", 127 + "Filename", "Uploaded", "Status"))) 128 + 129 + for _, sub := range submissions { 130 + var statusColor string 131 + switch sub.Status { 132 + case "pending": 133 + statusColor = "yellow" 134 + case "testing": 135 + statusColor = "blue" 136 + case "completed": 137 + statusColor = "green" 138 + case "failed": 139 + statusColor = "red" 140 + default: 141 + statusColor = "white" 181 142 } 182 - return submitMsg{success: true, message: "Submitted successfully!"} 143 + 144 + relTime := formatRelativeTime(sub.UploadTime) 145 + 146 + // Format the line without styles first for proper alignment 147 + statusStyled := lipgloss.NewStyle().Foreground(lipgloss.Color(statusColor)).Render(sub.Status) 148 + b.WriteString(fmt.Sprintf("%-35s %-15s %s\n", 149 + sub.Filename, relTime, statusStyled)) 150 + } 151 + 152 + return b.String() 153 + } 154 + 155 + func formatRelativeTime(t time.Time) string { 156 + duration := time.Since(t) 157 + if duration < time.Minute { 158 + return "just now" 159 + } else if duration < time.Hour { 160 + mins := int(duration.Minutes()) 161 + return fmt.Sprintf("%dm ago", mins) 162 + } else if duration < 24*time.Hour { 163 + hours := int(duration.Hours()) 164 + return fmt.Sprintf("%dh ago", hours) 183 165 } 166 + days := int(duration.Hours() / 24) 167 + return fmt.Sprintf("%dd ago", days) 184 168 } 185 169 186 170 func renderLeaderboard(entries []LeaderboardEntry) string {
+11 -15
runner.go
··· 2 2 3 3 import ( 4 4 "fmt" 5 + "log" 5 6 "os" 6 7 "os/exec" 7 8 "path/filepath" ··· 11 12 12 13 const battleshipRepoPath = "/Users/kierank/code/school/cs1210-battleship" 13 14 14 - func queueSubmission(username string) error { 15 - // Find the user's submission file 16 - files, err := filepath.Glob(filepath.Join(uploadDir, username, "memory_functions_*.cpp")) 17 - if err != nil { 18 - return err 19 - } 20 - if len(files) == 0 { 21 - return fmt.Errorf("no submission file found") 22 - } 23 - 24 - filename := filepath.Base(files[0]) 25 - _, err = addSubmission(username, filename) 26 - return err 27 - } 28 - 29 15 func processSubmissions() error { 30 16 submissions, err := getPendingSubmissions() 31 17 if err != nil { ··· 33 19 } 34 20 35 21 for _, sub := range submissions { 22 + log.Printf("Starting test for submission %d: %s by %s", sub.ID, sub.Filename, sub.Username) 23 + 36 24 if err := testSubmission(sub); err != nil { 25 + log.Printf("Submission %d failed: %v", sub.ID, err) 37 26 updateSubmissionStatus(sub.ID, "failed") 38 27 continue 39 28 } 29 + 30 + log.Printf("Submission %d completed successfully: %s by %s", sub.ID, sub.Filename, sub.Username) 40 31 updateSubmissionStatus(sub.ID, "completed") 41 32 } 42 33 ··· 44 35 } 45 36 46 37 func testSubmission(sub Submission) error { 38 + log.Printf("Setting submission %d to testing status", sub.ID) 47 39 updateSubmissionStatus(sub.ID, "testing") 48 40 49 41 // Copy submission to battleship repo 50 42 srcPath := filepath.Join(uploadDir, sub.Username, sub.Filename) 51 43 dstPath := filepath.Join(battleshipRepoPath, "src", sub.Filename) 52 44 45 + log.Printf("Copying %s to %s", srcPath, dstPath) 53 46 input, err := os.ReadFile(srcPath) 54 47 if err != nil { 55 48 return err ··· 70 63 buildDir := filepath.Join(battleshipRepoPath, "build") 71 64 os.MkdirAll(buildDir, 0755) 72 65 66 + log.Printf("Compiling submission %d for student %s", sub.ID, studentID) 73 67 // Compile using the light version for testing 74 68 cmd := exec.Command("g++", "-std=c++11", "-O3", 75 69 "-o", filepath.Join(buildDir, "battle_"+studentID), ··· 82 76 return fmt.Errorf("compilation failed: %s", output) 83 77 } 84 78 79 + log.Printf("Running benchmark for submission %d (100 games)", sub.ID) 85 80 // Run benchmark tests (100 games) 86 81 cmd = exec.Command(filepath.Join(buildDir, "battle_"+studentID), "--benchmark", "100") 87 82 output, err = cmd.CombinedOutput() ··· 90 85 } 91 86 92 87 // Parse results and store in database 88 + log.Printf("Parsing results for submission %d", sub.ID) 93 89 results := parseResults(string(output)) 94 90 for opponent, result := range results { 95 91 addResult(sub.ID, opponent, result.Result, result.Moves)
+9 -1
scp.go
··· 68 68 } 69 69 70 70 log.Printf("Uploaded %s from %s (%d bytes)", filename, s.User(), n) 71 - addSubmission(s.User(), filename) 71 + 72 + // Add submission and trigger testing 73 + submissionID, err := addSubmission(s.User(), filename) 74 + if err != nil { 75 + log.Printf("Failed to add submission: %v", err) 76 + } else { 77 + log.Printf("Queued submission %d for testing", submissionID) 78 + // The worker will pick it up automatically 79 + } 72 80 73 81 return n, nil 74 82 }
+9 -1
sftp.go
··· 157 157 err := f.file.Close() 158 158 if err == nil { 159 159 log.Printf("SFTP: Uploaded %s from %s", f.filename, f.username) 160 - addSubmission(f.username, f.filename) 160 + 161 + // Add submission and trigger testing 162 + submissionID, err := addSubmission(f.username, f.filename) 163 + if err != nil { 164 + log.Printf("Failed to add submission: %v", err) 165 + } else { 166 + log.Printf("Queued submission %d for testing", submissionID) 167 + // The worker will pick it up automatically 168 + } 161 169 } 162 170 return err 163 171 }
+23 -3
web.go
··· 138 138 </tr> 139 139 </thead> 140 140 <tbody> 141 + {{if .Entries}} 141 142 {{range $i, $e := .Entries}} 142 143 <tr> 143 144 <td class="rank rank-{{add $i 1}}">{{if lt $i 3}}{{medal $i}}{{else}}#{{add $i 1}}{{end}}</td> ··· 147 148 <td class="win-rate {{winRateClass $e}}">{{winRate $e}}%</td> 148 149 <td>{{printf "%.1f" $e.AvgMoves}}</td> 149 150 <td>{{$e.LastPlayed.Format "Jan 2, 3:04 PM"}}</td> 151 + </tr> 152 + {{end}} 153 + {{else}} 154 + <tr> 155 + <td colspan="7" style="text-align: center; padding: 40px; color: #999;"> 156 + No submissions yet. Be the first to compete! 157 + </td> 150 158 </tr> 151 159 {{end}} 152 160 </tbody> ··· 217 225 func handleLeaderboard(w http.ResponseWriter, r *http.Request) { 218 226 entries, err := getLeaderboard(50) 219 227 if err != nil { 220 - http.Error(w, "Failed to load leaderboard", http.StatusInternalServerError) 228 + http.Error(w, fmt.Sprintf("Failed to load leaderboard: %v", err), http.StatusInternalServerError) 221 229 return 230 + } 231 + 232 + // Empty leaderboard is fine 233 + if entries == nil { 234 + entries = []LeaderboardEntry{} 222 235 } 223 236 224 237 data := struct { ··· 231 244 TotalGames: calculateTotalGames(entries), 232 245 } 233 246 234 - tmpl.Execute(w, data) 247 + if err := tmpl.Execute(w, data); err != nil { 248 + http.Error(w, fmt.Sprintf("Template error: %v", err), http.StatusInternalServerError) 249 + } 235 250 } 236 251 237 252 func handleAPILeaderboard(w http.ResponseWriter, r *http.Request) { 238 253 entries, err := getLeaderboard(50) 239 254 if err != nil { 240 - http.Error(w, "Failed to load leaderboard", http.StatusInternalServerError) 255 + http.Error(w, fmt.Sprintf("Failed to load leaderboard: %v", err), http.StatusInternalServerError) 241 256 return 257 + } 258 + 259 + // Empty leaderboard is fine 260 + if entries == nil { 261 + entries = []LeaderboardEntry{} 242 262 } 243 263 244 264 w.Header().Set("Content-Type", "application/json")
+6 -1
worker.go
··· 8 8 9 9 // Background worker that processes pending submissions 10 10 func startWorker(ctx context.Context) { 11 - ticker := time.NewTicker(30 * time.Second) 11 + ticker := time.NewTicker(10 * time.Second) 12 12 defer ticker.Stop() 13 + 14 + // Process immediately on start 15 + if err := processSubmissions(); err != nil { 16 + log.Printf("Worker error: %v", err) 17 + } 13 18 14 19 for { 15 20 select {