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

bug: handle rendering over ssh properly

dunkirk.sh 9a95d7f7 0cd98e60

verified
+65 -591
+1
.gitignore
··· 6 build/ 7 8 ./battleship-arena 9 10 # Generated files in engine 11 battleship-engine/src/memory_functions_*.cpp
··· 6 build/ 7 8 ./battleship-arena 9 + ./remote-submissions 10 11 # Generated files in engine 12 battleship-engine/src/memory_functions_*.cpp
battleship-arena

This is a binary file and will not be displayed.

+6 -8
cmd/battleship-arena/main.go
··· 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" ··· 145 // Get proper terminal options for color support 146 opts := bubbletea.MakeOptions(s) 147 148 if needsOnboarding { 149 // Run onboarding first 150 publicKey := "" ··· 152 publicKey = val.(string) 153 } 154 155 - m := tui.NewOnboardingModel(s.User(), publicKey, pty.Window.Width, pty.Window.Height) 156 return m, opts 157 } 158 159 - m := tui.InitialModel(s.User(), pty.Window.Width, pty.Window.Height) 160 return m, opts 161 } 162 ··· 174 return nil 175 } 176 177 - var titleStyle = lipgloss.NewStyle(). 178 - Bold(true). 179 - Foreground(lipgloss.Color("205")). 180 - MarginTop(1). 181 - MarginBottom(1)
··· 11 "time" 12 13 tea "github.com/charmbracelet/bubbletea" 14 "github.com/charmbracelet/ssh" 15 "github.com/charmbracelet/wish" 16 "github.com/charmbracelet/wish/bubbletea" ··· 144 // Get proper terminal options for color support 145 opts := bubbletea.MakeOptions(s) 146 147 + // Create renderer for this session 148 + renderer := bubbletea.MakeRenderer(s) 149 + 150 if needsOnboarding { 151 // Run onboarding first 152 publicKey := "" ··· 154 publicKey = val.(string) 155 } 156 157 + m := tui.NewOnboardingModel(s.User(), publicKey, pty.Window.Width, pty.Window.Height, renderer) 158 return m, opts 159 } 160 161 + m := tui.InitialModel(s.User(), pty.Window.Width, pty.Window.Height, renderer) 162 return m, opts 163 } 164 ··· 176 return nil 177 } 178 179 +
-266
comito@0.0.0.0
··· 1 - #include "memory_functions_comito.h" 2 - 3 - using namespace std; 4 - 5 - void createMove(int row, int col, string &move); 6 - int moveCheck(int row, int col, const ComputerMemory &memory); 7 - void directionChecks(int &nextRow, int &nextCol, int dir, int offset, const ComputerMemory &memory); 8 - int DirFlip(int currDir); 9 - 10 - // initMemory initializes the memory; at the outset of the game the grid of 11 - // shots taken is empty, we've not hit any ships, and our player can only apply 12 - // a general, somewhat random firing strategy until we get a hit on some ship 13 - void initMemorycomito(ComputerMemory &memory) { 14 - memory.mode = RANDOM; 15 - memory.hitRow = -1; 16 - memory.hitCol = -1; 17 - memory.hitShip = NONE; 18 - memory.fireDir = NONE; 19 - memory.fireDist = 1; 20 - memory.lastResult = NONE; 21 - 22 - for (int i = 0; i < BOARDSIZE; i++) { 23 - for (int j = 0; j < BOARDSIZE; j++) { 24 - memory.grid[i][j] = EMPTY_MARKER; 25 - } 26 - } 27 - } 28 - 29 - // complete this function so it produces a "smart" move based on the information 30 - // which appears in the computer's memory 31 - string smartMovecomito(const ComputerMemory &memory) { 32 - string move; 33 - int checkResult = -1; 34 - int nextRow = -1; 35 - int nextCol = -1; 36 - if (memory.mode == SEARCH) 37 - { 38 - int i = 0; 39 - while (i <= 4 && checkResult != 1) 40 - { 41 - 42 - switch (memory.fireDir) 43 - { 44 - case 1: 45 - nextRow = memory.hitRow - 1; 46 - nextCol = memory.hitCol; 47 - break; 48 - case 2: 49 - nextRow = memory.hitRow + 1; 50 - nextCol = memory.hitCol; 51 - break; 52 - case 3: 53 - nextCol = memory.hitCol - 1; 54 - nextRow = memory.hitRow; 55 - break; 56 - case 4: 57 - nextCol = memory.hitCol + 1; 58 - nextRow = memory.hitRow; 59 - break; 60 - } 61 - i++; 62 - checkResult = moveCheck(nextRow, nextCol, memory); 63 - } 64 - } 65 - if (memory.mode == DESTROY) 66 - { 67 - switch (memory.fireDir) 68 - { 69 - case 1: 70 - nextRow = memory.hitRow - memory.fireDist; 71 - nextCol = memory.hitCol; 72 - break; 73 - case 2: 74 - nextRow = memory.hitRow + memory.fireDist; 75 - nextCol = memory.hitCol; 76 - break; 77 - case 3: 78 - nextCol = memory.hitCol - memory.fireDist; 79 - nextRow = memory.hitRow; 80 - break; 81 - case 4: 82 - nextCol = memory.hitCol + memory.fireDist; 83 - nextRow = memory.hitRow; 84 - break; 85 - } 86 - } 87 - createMove(nextRow, nextCol, move); 88 - debug(move); 89 - return move; 90 - } 91 - 92 - void updateMemorycomito(int row, int col, int result, ComputerMemory &memory) { 93 - 94 - int moveRes = result / 10; 95 - int hitShipId = result % 10; 96 - 97 - if (memory.mode == RANDOM) //if random 98 - { 99 - if (result == 0) //if missed 100 - { 101 - memory.fireDir = 0; 102 - } 103 - else //if hit 104 - { 105 - memory.hitShip = isShip(result); 106 - memory.hitRow = row; 107 - memory.hitCol = col; 108 - memory.mode = SEARCH; 109 - memory.fireDir++; 110 - } 111 - } 112 - else if (memory.mode == SEARCH) //if search 113 - { 114 - if (result == 0) //if missed 115 - { 116 - memory.fireDir++; 117 - if (memory.fireDir > 4) 118 - { 119 - memory.mode = RANDOM; 120 - memory.fireDist = 1; 121 - } 122 - } 123 - else 124 - { 125 - memory.mode = DESTROY; 126 - memory.fireDist++; 127 - } 128 - }else //if destroy 129 - { 130 - int i = BOARDSIZE; 131 - int nextRow = -1; 132 - int nextCol = -1; 133 - int checkResult = -1; 134 - while (checkResult != 1 && i > 0) 135 - { 136 - switch (memory.fireDir) 137 - { 138 - case 1: 139 - nextRow = memory.hitRow - memory.fireDist; 140 - nextCol = memory.hitCol; 141 - break; 142 - case 2: 143 - nextRow = memory.hitRow + memory.fireDist; 144 - nextCol = memory.hitCol; 145 - break; 146 - case 3: 147 - nextCol = memory.hitCol - memory.fireDist; 148 - nextRow = memory.hitRow; 149 - break; 150 - case 4: 151 - nextCol = memory.hitCol + memory.fireDist; 152 - nextRow = memory.hitRow; 153 - break; 154 - } 155 - checkResult = moveCheck(nextRow, nextCol, memory); 156 - 157 - switch (checkResult) 158 - { 159 - case 0: 160 - memory.fireDir = DirFlip(memory.fireDir); 161 - memory.fireDist = 1; 162 - break; 163 - case 1: 164 - //check if should fire here and if yes fire 165 - if (moveRes != 0) 166 - { 167 - 168 - } 169 - break; 170 - case 2: 171 - memory.fireDist += 1; 172 - break; 173 - case 3: 174 - memory.fireDir = DirFlip(memory.fireDir); 175 - memory.fireDist = 1; 176 - break; 177 - } 178 - i--; 179 - } 180 - if (i < 1) 181 - { 182 - memory.mode = RANDOM; 183 - } 184 - } 185 - //update memory grid 186 - if (result == 0) 187 - { 188 - memory.grid[row][col] = MISS_MARKER; 189 - } 190 - if (result == 1) 191 - { 192 - memory.grid[row][col] = HIT_MARKER; 193 - } 194 - } 195 - 196 - //I added this function to call within the move so that 197 - //I can input the row number and just get the letter out 198 - //I didn't know how best to do this so I did whatever this is :/ 199 - void createMove(int row, int col, string &move) 200 - { 201 - char letter = 'A'; 202 - letter += row; 203 - move.push_back(letter); 204 - string number = to_string(col + 1); 205 - move.append(number); 206 - } 207 - 208 - int moveCheck(int row, int col, const ComputerMemory &memory) 209 - { 210 - int success = 0; 211 - bool isMovePlayedYet = false; 212 - 213 - if (row < 0 || row >= BOARDSIZE || col < 0 || col >= BOARDSIZE) 214 - { 215 - success = false; 216 - } 217 - else 218 - { 219 - if(memory.grid[row][col] == EMPTY_MARKER) 220 - { 221 - success = 1; 222 - }else if (memory.grid[row][col] == HIT_MARKER) 223 - { 224 - success = 2; 225 - }else 226 - { 227 - success = 3; 228 - } 229 - } 230 - return success; 231 - } 232 - int DirFlip(int currDir) 233 - { 234 - if (currDir == 1) 235 - { 236 - return 2; 237 - } 238 - if (currDir == 3) 239 - { 240 - return 4; 241 - } 242 - return currDir; 243 - } 244 - /* attempted function graveyard 245 - { 246 - switch (dir) 247 - { 248 - case 1: 249 - nextRow = memory.hitRow - offset; 250 - nextCol = memory.hitCol; 251 - break; 252 - case 2: 253 - nextRow = memory.hitRow + offset; 254 - nextCol = memory.hitCol; 255 - break; 256 - case 3: 257 - nextCol = memory.hitCol - offset; 258 - nextRow = memory.hitRow; 259 - break; 260 - case 4: 261 - nextCol = memory.hitCol + offset; 262 - nextRow = memory.hitRow; 263 - break; 264 - } 265 - } 266 - */
···
-266
comito@localhost
··· 1 - #include "memory_functions_comito.h" 2 - 3 - using namespace std; 4 - 5 - void createMove(int row, int col, string &move); 6 - int moveCheck(int row, int col, const ComputerMemory &memory); 7 - void directionChecks(int &nextRow, int &nextCol, int dir, int offset, const ComputerMemory &memory); 8 - int DirFlip(int currDir); 9 - 10 - // initMemory initializes the memory; at the outset of the game the grid of 11 - // shots taken is empty, we've not hit any ships, and our player can only apply 12 - // a general, somewhat random firing strategy until we get a hit on some ship 13 - void initMemorycomito(ComputerMemory &memory) { 14 - memory.mode = RANDOM; 15 - memory.hitRow = -1; 16 - memory.hitCol = -1; 17 - memory.hitShip = NONE; 18 - memory.fireDir = NONE; 19 - memory.fireDist = 1; 20 - memory.lastResult = NONE; 21 - 22 - for (int i = 0; i < BOARDSIZE; i++) { 23 - for (int j = 0; j < BOARDSIZE; j++) { 24 - memory.grid[i][j] = EMPTY_MARKER; 25 - } 26 - } 27 - } 28 - 29 - // complete this function so it produces a "smart" move based on the information 30 - // which appears in the computer's memory 31 - string smartMovecomito(const ComputerMemory &memory) { 32 - string move; 33 - int checkResult = -1; 34 - int nextRow = -1; 35 - int nextCol = -1; 36 - if (memory.mode == SEARCH) 37 - { 38 - int i = 0; 39 - while (i <= 4 && checkResult != 1) 40 - { 41 - 42 - switch (memory.fireDir) 43 - { 44 - case 1: 45 - nextRow = memory.hitRow - 1; 46 - nextCol = memory.hitCol; 47 - break; 48 - case 2: 49 - nextRow = memory.hitRow + 1; 50 - nextCol = memory.hitCol; 51 - break; 52 - case 3: 53 - nextCol = memory.hitCol - 1; 54 - nextRow = memory.hitRow; 55 - break; 56 - case 4: 57 - nextCol = memory.hitCol + 1; 58 - nextRow = memory.hitRow; 59 - break; 60 - } 61 - i++; 62 - checkResult = moveCheck(nextRow, nextCol, memory); 63 - } 64 - } 65 - if (memory.mode == DESTROY) 66 - { 67 - switch (memory.fireDir) 68 - { 69 - case 1: 70 - nextRow = memory.hitRow - memory.fireDist; 71 - nextCol = memory.hitCol; 72 - break; 73 - case 2: 74 - nextRow = memory.hitRow + memory.fireDist; 75 - nextCol = memory.hitCol; 76 - break; 77 - case 3: 78 - nextCol = memory.hitCol - memory.fireDist; 79 - nextRow = memory.hitRow; 80 - break; 81 - case 4: 82 - nextCol = memory.hitCol + memory.fireDist; 83 - nextRow = memory.hitRow; 84 - break; 85 - } 86 - } 87 - createMove(nextRow, nextCol, move); 88 - debug(move); 89 - return move; 90 - } 91 - 92 - void updateMemorycomito(int row, int col, int result, ComputerMemory &memory) { 93 - 94 - int moveRes = result / 10; 95 - int hitShipId = result % 10; 96 - 97 - if (memory.mode == RANDOM) //if random 98 - { 99 - if (result == 0) //if missed 100 - { 101 - memory.fireDir = 0; 102 - } 103 - else //if hit 104 - { 105 - memory.hitShip = isShip(result); 106 - memory.hitRow = row; 107 - memory.hitCol = col; 108 - memory.mode = SEARCH; 109 - memory.fireDir++; 110 - } 111 - } 112 - else if (memory.mode == SEARCH) //if search 113 - { 114 - if (result == 0) //if missed 115 - { 116 - memory.fireDir++; 117 - if (memory.fireDir > 4) 118 - { 119 - memory.mode = RANDOM; 120 - memory.fireDist = 1; 121 - } 122 - } 123 - else 124 - { 125 - memory.mode = DESTROY; 126 - memory.fireDist++; 127 - } 128 - }else //if destroy 129 - { 130 - int i = BOARDSIZE; 131 - int nextRow = -1; 132 - int nextCol = -1; 133 - int checkResult = -1; 134 - while (checkResult != 1 && i > 0) 135 - { 136 - switch (memory.fireDir) 137 - { 138 - case 1: 139 - nextRow = memory.hitRow - memory.fireDist; 140 - nextCol = memory.hitCol; 141 - break; 142 - case 2: 143 - nextRow = memory.hitRow + memory.fireDist; 144 - nextCol = memory.hitCol; 145 - break; 146 - case 3: 147 - nextCol = memory.hitCol - memory.fireDist; 148 - nextRow = memory.hitRow; 149 - break; 150 - case 4: 151 - nextCol = memory.hitCol + memory.fireDist; 152 - nextRow = memory.hitRow; 153 - break; 154 - } 155 - checkResult = moveCheck(nextRow, nextCol, memory); 156 - 157 - switch (checkResult) 158 - { 159 - case 0: 160 - memory.fireDir = DirFlip(memory.fireDir); 161 - memory.fireDist = 1; 162 - break; 163 - case 1: 164 - //check if should fire here and if yes fire 165 - if (moveRes != 0) 166 - { 167 - 168 - } 169 - break; 170 - case 2: 171 - memory.fireDist += 1; 172 - break; 173 - case 3: 174 - memory.fireDir = DirFlip(memory.fireDir); 175 - memory.fireDist = 1; 176 - break; 177 - } 178 - i--; 179 - } 180 - if (i < 1) 181 - { 182 - memory.mode = RANDOM; 183 - } 184 - } 185 - //update memory grid 186 - if (result == 0) 187 - { 188 - memory.grid[row][col] = MISS_MARKER; 189 - } 190 - if (result == 1) 191 - { 192 - memory.grid[row][col] = HIT_MARKER; 193 - } 194 - } 195 - 196 - //I added this function to call within the move so that 197 - //I can input the row number and just get the letter out 198 - //I didn't know how best to do this so I did whatever this is :/ 199 - void createMove(int row, int col, string &move) 200 - { 201 - char letter = 'A'; 202 - letter += row; 203 - move.push_back(letter); 204 - string number = to_string(col + 1); 205 - move.append(number); 206 - } 207 - 208 - int moveCheck(int row, int col, const ComputerMemory &memory) 209 - { 210 - int success = 0; 211 - bool isMovePlayedYet = false; 212 - 213 - if (row < 0 || row >= BOARDSIZE || col < 0 || col >= BOARDSIZE) 214 - { 215 - success = false; 216 - } 217 - else 218 - { 219 - if(memory.grid[row][col] == EMPTY_MARKER) 220 - { 221 - success = 1; 222 - }else if (memory.grid[row][col] == HIT_MARKER) 223 - { 224 - success = 2; 225 - }else 226 - { 227 - success = 3; 228 - } 229 - } 230 - return success; 231 - } 232 - int DirFlip(int currDir) 233 - { 234 - if (currDir == 1) 235 - { 236 - return 2; 237 - } 238 - if (currDir == 3) 239 - { 240 - return 4; 241 - } 242 - return currDir; 243 - } 244 - /* attempted function graveyard 245 - { 246 - switch (dir) 247 - { 248 - case 1: 249 - nextRow = memory.hitRow - offset; 250 - nextCol = memory.hitCol; 251 - break; 252 - case 2: 253 - nextRow = memory.hitRow + offset; 254 - nextCol = memory.hitCol; 255 - break; 256 - case 3: 257 - nextCol = memory.hitCol - offset; 258 - nextRow = memory.hitRow; 259 - break; 260 - case 4: 261 - nextCol = memory.hitCol + offset; 262 - nextRow = memory.hitRow; 263 - break; 264 - } 265 - } 266 - */
···
+48 -43
internal/tui/model.go
··· 29 fieldLink 30 ) 31 32 - var titleStyle = lipgloss.NewStyle(). 33 - Bold(true). 34 - Foreground(lipgloss.Color("205")). 35 - MarginTop(1). 36 - MarginBottom(1) 37 - 38 type model struct { 39 username string 40 width int 41 height int ··· 53 saveMessage string 54 } 55 56 - func InitialModel(username string, width, height int) model { 57 externalURL := os.Getenv("BATTLESHIP_EXTERNAL_URL") 58 if externalURL == "" { 59 externalURL = "localhost" ··· 74 // Load user profile 75 user, _ := storage.GetUserByUsername(username) 76 77 return model{ 78 username: username, 79 width: width, 80 height: height, ··· 200 func (m model) View() string { 201 var b strings.Builder 202 203 - title := titleStyle.Render("🚢 Battleship Arena") 204 b.WriteString(title + "\n") 205 206 // Skip tabs if in edit mode 207 if m.currentView != viewEditProfile { 208 // Navigation tabs 209 - tabStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")) 210 - activeTabStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("86")).Bold(true) 211 212 tabs := []string{"[h] Home", "[l] Leaderboard", "[p] Profile"} 213 for i, tab := range tabs { ··· 248 b.WriteString(fmt.Sprintf("User: %s\n\n", m.username)) 249 250 // Upload instructions 251 - infoStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("86")) 252 b.WriteString(infoStyle.Render(fmt.Sprintf("Upload via: scp -P %s memory_functions_yourname.cpp %s@%s:~/", m.sshPort, m.username, m.externalURL))) 253 b.WriteString("\n\n") 254 255 // Show submissions 256 if len(m.submissions) > 0 { 257 - b.WriteString(renderSubmissions(m.submissions)) 258 } else { 259 b.WriteString("No submissions yet. Upload your first AI!\n") 260 } ··· 264 265 func (m model) renderLeaderboardView() string { 266 if len(m.leaderboard) > 0 { 267 - return renderLeaderboard(m.leaderboard) 268 } 269 return "Loading leaderboard..." 270 } ··· 276 return "Loading profile..." 277 } 278 279 - b.WriteString(lipgloss.NewStyle().Bold(true).Render("👤 Profile") + "\n\n") 280 281 // Show user info 282 - labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")) 283 b.WriteString(labelStyle.Render("Username: ") + m.user.Username + "\n") 284 b.WriteString(labelStyle.Render("Name: ") + m.user.Name + "\n") 285 b.WriteString(labelStyle.Render("Bio: ") + m.user.Bio + "\n") 286 b.WriteString(labelStyle.Render("Link: ") + m.user.Link + "\n\n") 287 288 - hintStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("86")) 289 b.WriteString(hintStyle.Render("Press 'e' to edit profile") + "\n\n") 290 291 // Show user stats from submissions 292 if len(m.submissions) > 0 { 293 - b.WriteString(renderSubmissions(m.submissions)) 294 b.WriteString("\n") 295 } 296 ··· 300 func (m model) renderEditProfile() string { 301 var b strings.Builder 302 303 - b.WriteString(lipgloss.NewStyle().Bold(true).Render("✏️ Edit Profile") + "\n\n") 304 305 - activeStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("86")).Bold(true) 306 - inactiveStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")) 307 308 // Name field 309 if m.editingField == fieldName { ··· 329 b.WriteString("\n") 330 331 if m.saveMessage != "" { 332 - msgStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("green")) 333 b.WriteString(msgStyle.Render(m.saveMessage) + "\n\n") 334 } 335 336 - hintStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")) 337 b.WriteString(hintStyle.Render("Tab/↑↓: Navigate fields | Enter: Save | Esc: Cancel")) 338 339 return b.String() ··· 387 }) 388 } 389 390 - func renderSubmissions(submissions []storage.Submission) string { 391 var b strings.Builder 392 - b.WriteString(lipgloss.NewStyle().Bold(true).Render("📤 Your Submissions") + "\n\n") 393 394 - headerStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("240")) 395 b.WriteString(headerStyle.Render(fmt.Sprintf("%-35s %-15s %s", 396 "Filename", "Uploaded", "Status")) + "\n") 397 ··· 414 415 // Build the line manually to avoid formatting issues with ANSI codes 416 line := fmt.Sprintf("%-35s %-15s ", sub.Filename, relTime) 417 - statusStyled := lipgloss.NewStyle().Foreground(lipgloss.Color(statusColor)).Render(sub.Status) 418 b.WriteString(line + statusStyled + "\n") 419 } 420 ··· 436 return fmt.Sprintf("%dd ago", days) 437 } 438 439 - func renderLeaderboard(entries []storage.LeaderboardEntry) string { 440 if len(entries) == 0 { 441 return "No entries yet" 442 } 443 444 var b strings.Builder 445 - b.WriteString(lipgloss.NewStyle().Bold(true).Render("🏆 Leaderboard") + "\n\n") 446 447 // Header without styling on the whole line 448 b.WriteString(fmt.Sprintf("%-4s %-20s %11s %8s %8s %10s %10s\n", ··· 454 // Apply color only to the rank and pad manually 455 var displayRank string 456 if i == 0 { 457 - displayRank = lipgloss.NewStyle().Foreground(lipgloss.Color("220")).Render(rank) + " " // Gold 458 } else if i == 1 { 459 - displayRank = lipgloss.NewStyle().Foreground(lipgloss.Color("250")).Render(rank) + " " // Silver 460 } else if i == 2 { 461 - displayRank = lipgloss.NewStyle().Foreground(lipgloss.Color("208")).Render(rank) + " " // Bronze 462 } else { 463 displayRank = fmt.Sprintf("%-4s", rank) 464 } ··· 472 return b.String() 473 } 474 475 - func renderMatches(matches []storage.MatchResult, username string) string { 476 var b strings.Builder 477 478 // Filter to show only matches involving this user ··· 493 userMatches = userMatches[:limit] 494 } 495 496 - headerStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("240")) 497 b.WriteString(headerStyle.Render(fmt.Sprintf("%-20s %-20s %-20s %10s\n", 498 "Player 1", "Player 2", "Winner", "Avg Moves"))) 499 b.WriteString("\n") ··· 504 505 // Highlight wins in green, losses in red 506 if match.WinnerUsername == username { 507 - b.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("green")).Render(line)) 508 } else { 509 b.WriteString(line) 510 } ··· 513 return b.String() 514 } 515 516 - func renderBracket(matches []storage.MatchResult) string { 517 var b strings.Builder 518 - b.WriteString(lipgloss.NewStyle().Bold(true).Render("⚔️ Tournament Bracket") + "\n\n") 519 520 if len(matches) == 0 { 521 return b.String() ··· 542 } 543 544 // Determine winner styling 545 - player1Style := lipgloss.NewStyle().Foreground(lipgloss.Color("240")) 546 - player2Style := lipgloss.NewStyle().Foreground(lipgloss.Color("240")) 547 - winnerBox := lipgloss.NewStyle(). 548 Foreground(lipgloss.Color("green")). 549 Bold(true). 550 Border(lipgloss.RoundedBorder()). ··· 564 // ├── Winner 565 // Player2 ┘ 566 567 - player1Box := lipgloss.NewStyle(). 568 Border(lipgloss.RoundedBorder()). 569 Padding(0, 1). 570 Width(15) 571 572 - player2Box := lipgloss.NewStyle(). 573 Border(lipgloss.RoundedBorder()). 574 Padding(0, 1). 575 Width(15) ··· 584 b.WriteString(p1 + connector1 + "\n") 585 b.WriteString(strings.Repeat(" ", 17) + middle + " " + winnerStr + "\n") 586 b.WriteString(p2 + connector2 + "\n") 587 - b.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Render( 588 fmt.Sprintf(" (avg %d moves)\n", match.AvgMoves))) 589 b.WriteString("\n") 590 ··· 592 } 593 594 if len(matchups) > 8 { 595 - b.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Render( 596 fmt.Sprintf("... and %d more matches\n", len(matchups)-8))) 597 } 598
··· 29 fieldLink 30 ) 31 32 type model struct { 33 + renderer *lipgloss.Renderer 34 + titleStyle lipgloss.Style 35 username string 36 width int 37 height int ··· 49 saveMessage string 50 } 51 52 + func InitialModel(username string, width, height int, renderer *lipgloss.Renderer) model { 53 externalURL := os.Getenv("BATTLESHIP_EXTERNAL_URL") 54 if externalURL == "" { 55 externalURL = "localhost" ··· 70 // Load user profile 71 user, _ := storage.GetUserByUsername(username) 72 73 + // Create styles using session renderer 74 + titleStyle := renderer.NewStyle(). 75 + Bold(true). 76 + Foreground(lipgloss.Color("205")). 77 + MarginTop(1). 78 + MarginBottom(1) 79 + 80 return model{ 81 + renderer: renderer, 82 + titleStyle: titleStyle, 83 username: username, 84 width: width, 85 height: height, ··· 205 func (m model) View() string { 206 var b strings.Builder 207 208 + title := m.titleStyle.Render("🚢 Battleship Arena") 209 b.WriteString(title + "\n") 210 211 // Skip tabs if in edit mode 212 if m.currentView != viewEditProfile { 213 // Navigation tabs 214 + tabStyle := m.renderer.NewStyle().Foreground(lipgloss.Color("240")) 215 + activeTabStyle := m.renderer.NewStyle().Foreground(lipgloss.Color("86")).Bold(true) 216 217 tabs := []string{"[h] Home", "[l] Leaderboard", "[p] Profile"} 218 for i, tab := range tabs { ··· 253 b.WriteString(fmt.Sprintf("User: %s\n\n", m.username)) 254 255 // Upload instructions 256 + infoStyle := m.renderer.NewStyle().Foreground(lipgloss.Color("86")) 257 b.WriteString(infoStyle.Render(fmt.Sprintf("Upload via: scp -P %s memory_functions_yourname.cpp %s@%s:~/", m.sshPort, m.username, m.externalURL))) 258 b.WriteString("\n\n") 259 260 // Show submissions 261 if len(m.submissions) > 0 { 262 + b.WriteString(m.renderSubmissions(m.submissions)) 263 } else { 264 b.WriteString("No submissions yet. Upload your first AI!\n") 265 } ··· 269 270 func (m model) renderLeaderboardView() string { 271 if len(m.leaderboard) > 0 { 272 + return m.renderLeaderboard(m.leaderboard) 273 } 274 return "Loading leaderboard..." 275 } ··· 281 return "Loading profile..." 282 } 283 284 + b.WriteString(m.renderer.NewStyle().Bold(true).Render("👤 Profile") + "\n\n") 285 286 // Show user info 287 + labelStyle := m.renderer.NewStyle().Foreground(lipgloss.Color("240")) 288 b.WriteString(labelStyle.Render("Username: ") + m.user.Username + "\n") 289 b.WriteString(labelStyle.Render("Name: ") + m.user.Name + "\n") 290 b.WriteString(labelStyle.Render("Bio: ") + m.user.Bio + "\n") 291 b.WriteString(labelStyle.Render("Link: ") + m.user.Link + "\n\n") 292 293 + hintStyle := m.renderer.NewStyle().Foreground(lipgloss.Color("86")) 294 b.WriteString(hintStyle.Render("Press 'e' to edit profile") + "\n\n") 295 296 // Show user stats from submissions 297 if len(m.submissions) > 0 { 298 + b.WriteString(m.renderSubmissions(m.submissions)) 299 b.WriteString("\n") 300 } 301 ··· 305 func (m model) renderEditProfile() string { 306 var b strings.Builder 307 308 + b.WriteString(m.renderer.NewStyle().Bold(true).Render("✏️ Edit Profile") + "\n\n") 309 310 + activeStyle := m.renderer.NewStyle().Foreground(lipgloss.Color("86")).Bold(true) 311 + inactiveStyle := m.renderer.NewStyle().Foreground(lipgloss.Color("240")) 312 313 // Name field 314 if m.editingField == fieldName { ··· 334 b.WriteString("\n") 335 336 if m.saveMessage != "" { 337 + msgStyle := m.renderer.NewStyle().Foreground(lipgloss.Color("green")) 338 b.WriteString(msgStyle.Render(m.saveMessage) + "\n\n") 339 } 340 341 + hintStyle := m.renderer.NewStyle().Foreground(lipgloss.Color("240")) 342 b.WriteString(hintStyle.Render("Tab/↑↓: Navigate fields | Enter: Save | Esc: Cancel")) 343 344 return b.String() ··· 392 }) 393 } 394 395 + func (m model) renderSubmissions(submissions []storage.Submission) string { 396 var b strings.Builder 397 + b.WriteString(m.renderer.NewStyle().Bold(true).Render("📤 Your Submissions") + "\n\n") 398 399 + headerStyle := m.renderer.NewStyle().Bold(true).Foreground(lipgloss.Color("240")) 400 b.WriteString(headerStyle.Render(fmt.Sprintf("%-35s %-15s %s", 401 "Filename", "Uploaded", "Status")) + "\n") 402 ··· 419 420 // Build the line manually to avoid formatting issues with ANSI codes 421 line := fmt.Sprintf("%-35s %-15s ", sub.Filename, relTime) 422 + statusStyled := m.renderer.NewStyle().Foreground(lipgloss.Color(statusColor)).Render(sub.Status) 423 b.WriteString(line + statusStyled + "\n") 424 } 425 ··· 441 return fmt.Sprintf("%dd ago", days) 442 } 443 444 + func (m model) renderLeaderboard(entries []storage.LeaderboardEntry) string { 445 if len(entries) == 0 { 446 return "No entries yet" 447 } 448 449 var b strings.Builder 450 + b.WriteString(m.renderer.NewStyle().Bold(true).Render("🏆 Leaderboard") + "\n\n") 451 452 // Header without styling on the whole line 453 b.WriteString(fmt.Sprintf("%-4s %-20s %11s %8s %8s %10s %10s\n", ··· 459 // Apply color only to the rank and pad manually 460 var displayRank string 461 if i == 0 { 462 + displayRank = m.renderer.NewStyle().Foreground(lipgloss.Color("220")).Render(rank) + " " // Gold 463 } else if i == 1 { 464 + displayRank = m.renderer.NewStyle().Foreground(lipgloss.Color("250")).Render(rank) + " " // Silver 465 } else if i == 2 { 466 + displayRank = m.renderer.NewStyle().Foreground(lipgloss.Color("208")).Render(rank) + " " // Bronze 467 } else { 468 displayRank = fmt.Sprintf("%-4s", rank) 469 } ··· 477 return b.String() 478 } 479 480 + func (m model) renderMatches(matches []storage.MatchResult, username string) string { 481 var b strings.Builder 482 483 // Filter to show only matches involving this user ··· 498 userMatches = userMatches[:limit] 499 } 500 501 + headerStyle := m.renderer.NewStyle().Bold(true).Foreground(lipgloss.Color("240")) 502 b.WriteString(headerStyle.Render(fmt.Sprintf("%-20s %-20s %-20s %10s\n", 503 "Player 1", "Player 2", "Winner", "Avg Moves"))) 504 b.WriteString("\n") ··· 509 510 // Highlight wins in green, losses in red 511 if match.WinnerUsername == username { 512 + b.WriteString(m.renderer.NewStyle().Foreground(lipgloss.Color("green")).Render(line)) 513 } else { 514 b.WriteString(line) 515 } ··· 518 return b.String() 519 } 520 521 + func (m model) renderBracket(matches []storage.MatchResult) string { 522 var b strings.Builder 523 + b.WriteString(m.renderer.NewStyle().Bold(true).Render("⚔️ Tournament Bracket") + "\n\n") 524 525 if len(matches) == 0 { 526 return b.String() ··· 547 } 548 549 // Determine winner styling 550 + player1Style := m.renderer.NewStyle().Foreground(lipgloss.Color("240")) 551 + player2Style := m.renderer.NewStyle().Foreground(lipgloss.Color("240")) 552 + winnerBox := m.renderer.NewStyle(). 553 Foreground(lipgloss.Color("green")). 554 Bold(true). 555 Border(lipgloss.RoundedBorder()). ··· 569 // ├── Winner 570 // Player2 ┘ 571 572 + player1Box := m.renderer.NewStyle(). 573 Border(lipgloss.RoundedBorder()). 574 Padding(0, 1). 575 Width(15) 576 577 + player2Box := m.renderer.NewStyle(). 578 Border(lipgloss.RoundedBorder()). 579 Padding(0, 1). 580 Width(15) ··· 589 b.WriteString(p1 + connector1 + "\n") 590 b.WriteString(strings.Repeat(" ", 17) + middle + " " + winnerStr + "\n") 591 b.WriteString(p2 + connector2 + "\n") 592 + b.WriteString(m.renderer.NewStyle().Foreground(lipgloss.Color("240")).Render( 593 fmt.Sprintf(" (avg %d moves)\n", match.AvgMoves))) 594 b.WriteString("\n") 595 ··· 597 } 598 599 if len(matchups) > 8 { 600 + b.WriteString(m.renderer.NewStyle().Foreground(lipgloss.Color("240")).Render( 601 fmt.Sprintf("... and %d more matches\n", len(matchups)-8))) 602 } 603
+10 -8
internal/tui/onboarding.go
··· 12 ) 13 14 type OnboardingModel struct { 15 username string 16 publicKey string 17 step int // 0=name, 1=bio, 2=link, 3=done ··· 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, ··· 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 ··· 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") ··· 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!"))
··· 12 ) 13 14 type OnboardingModel struct { 15 + renderer *lipgloss.Renderer 16 username string 17 publicKey string 18 step int // 0=name, 1=bio, 2=link, 3=done ··· 30 username string 31 } 32 33 + func NewOnboardingModel(username, publicKey string, width, height int, renderer *lipgloss.Renderer) OnboardingModel { 34 return OnboardingModel{ 35 + renderer: renderer, 36 username: username, 37 publicKey: publicKey, 38 step: 0, ··· 100 m.height = msg.Height 101 case onboardingCompleteMsg: 102 // Transition to main model 103 + mainModel := InitialModel(m.username, m.width, m.height, m.renderer) 104 return mainModel, mainModel.Init() 105 } 106 return m, nil ··· 108 109 func (m OnboardingModel) View() string { 110 if m.completed { 111 + successStyle := m.renderer.NewStyle(). 112 Foreground(lipgloss.Color("green")). 113 Bold(true) 114 return successStyle.Render("\n✅ Account created successfully!\n\nLoading dashboard...\n") ··· 116 117 var b strings.Builder 118 119 + titleStyle := m.renderer.NewStyle(). 120 Bold(true). 121 Foreground(lipgloss.Color("205")). 122 MarginTop(1). 123 MarginBottom(1) 124 125 + promptStyle := m.renderer.NewStyle(). 126 Foreground(lipgloss.Color("86")) 127 128 + inputStyle := m.renderer.NewStyle(). 129 Foreground(lipgloss.Color("212")). 130 Bold(true) 131 132 + errorStyle := m.renderer.NewStyle(). 133 Foreground(lipgloss.Color("196")) 134 135 + helpStyle := m.renderer.NewStyle(). 136 Foreground(lipgloss.Color("240")) 137 138 b.WriteString(titleStyle.Render("🚢 Welcome to Battleship Arena!"))