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

feat: init

dunkirk.sh 079b13a4

+1332
+6
.gitignore
··· 1 + .ssh/ 2 + submissions/ 3 + *.db 4 + battleship-arena 5 + *.log 6 + build/
+51
AGENTS.md
··· 1 + # Development Notes 2 + 3 + ## Architecture 4 + 5 + - **main.go** - SSH/HTTP server initialization with Wish and Bubble Tea 6 + - **model.go** - Terminal UI (TUI) for SSH sessions 7 + - **database.go** - SQLite storage for submissions and results 8 + - **web.go** - HTTP leaderboard with HTML template 9 + - **runner.go** - Compiles and tests C++ submissions against battleship library 10 + - **scp.go** - SCP upload middleware for file submissions 11 + - **worker.go** - Background processor (runs every 30s) 12 + 13 + ## File Upload 14 + 15 + Students upload via SCP: 16 + ```bash 17 + scp -P 2222 memory_functions_name.cpp username@host:~/ 18 + ``` 19 + 20 + Files must match pattern `memory_functions_*.cpp` 21 + 22 + ## Testing Flow 23 + 24 + 1. Student uploads file via SCP → saved to `./submissions/username/` 25 + 2. Student SSH in and selects "Test Submission" 26 + 3. Worker picks up pending submission 27 + 4. Compiles with battleship library: `g++ battle_light.cpp battleship_light.cpp memory_functions_*.cpp` 28 + 5. Runs benchmark: `./battle --benchmark 100` 29 + 6. Parses results and updates database 30 + 7. Leaderboard shows updated rankings 31 + 32 + ## Configuration 33 + 34 + Edit `runner.go` line 11: 35 + ```go 36 + const battleshipRepoPath = "/path/to/cs1210-battleship" 37 + ``` 38 + 39 + ## Building 40 + 41 + ```bash 42 + make build # Build binary 43 + make run # Build and run 44 + make gen-key # Generate SSH host key 45 + ``` 46 + 47 + ## Deployment 48 + 49 + See `Dockerfile`, `docker-compose.yml`, or `battleship-arena.service` for systemd. 50 + 51 + Web runs on port 8080, SSH on port 2222.
+25
LICENSE.md
··· 1 + The MIT License (MIT) 2 + ===================== 3 + 4 + Copyright © `2025` `Kieran Klukas` 5 + 6 + Permission is hereby granted, free of charge, to any person 7 + obtaining a copy of this software and associated documentation 8 + files (the “Software”), to deal in the Software without 9 + restriction, including without limitation the rights to use, 10 + copy, modify, merge, publish, distribute, sublicense, and/or sell 11 + copies of the Software, and to permit persons to whom the 12 + Software is furnished to do so, subject to the following 13 + conditions: 14 + 15 + The above copyright notice and this permission notice shall be 16 + included in all copies or substantial portions of the Software. 17 + 18 + THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 + OTHER DEALINGS IN THE SOFTWARE.
+63
Makefile
··· 1 + .PHONY: build run clean test docker-build docker-run help 2 + 3 + # Build the battleship arena server 4 + build: 5 + @echo "Building battleship-arena..." 6 + @go build -o battleship-arena 7 + 8 + # Run the server 9 + run: build 10 + @echo "Starting battleship-arena..." 11 + @./battleship-arena 12 + 13 + # Clean build artifacts 14 + clean: 15 + @echo "Cleaning..." 16 + @rm -f battleship-arena 17 + @rm -rf submissions/ .ssh/ *.db 18 + 19 + # Run tests 20 + test: 21 + @echo "Running tests..." 22 + @go test -v ./... 23 + 24 + # Generate SSH host key 25 + gen-key: 26 + @echo "Generating SSH host key..." 27 + @mkdir -p .ssh 28 + @ssh-keygen -t ed25519 -f .ssh/battleship_arena -N "" 29 + 30 + # Format code 31 + fmt: 32 + @echo "Formatting code..." 33 + @go fmt ./... 34 + 35 + # Lint code 36 + lint: 37 + @echo "Linting code..." 38 + @golangci-lint run 39 + 40 + # Update dependencies 41 + deps: 42 + @echo "Updating dependencies..." 43 + @go mod tidy 44 + @go mod download 45 + 46 + # Build for production (optimized) 47 + build-prod: 48 + @echo "Building for production..." 49 + @CGO_ENABLED=1 go build -ldflags="-s -w" -o battleship-arena 50 + 51 + # Show help 52 + help: 53 + @echo "Available targets:" 54 + @echo " build - Build the server" 55 + @echo " run - Build and run the server" 56 + @echo " clean - Clean build artifacts" 57 + @echo " test - Run tests" 58 + @echo " gen-key - Generate SSH host key" 59 + @echo " fmt - Format code" 60 + @echo " lint - Lint code" 61 + @echo " deps - Update dependencies" 62 + @echo " build-prod - Build optimized production binary" 63 + @echo " help - Show this help"
+41
README.md
··· 1 + # Battleship Arena 2 + 3 + This is a service I made to allow students in my `cs-1210` class to benchmark their battleship programs against each other. 4 + 5 + ## I just want to get on the leaderboard; How? 6 + 7 + First ssh into the battleship server and it will ask you a few questions to set up your account. Then scp your battleship file onto the server! 8 + 9 + ```bash 10 + ssh battleship.dunkirk.sh 11 + scp memory_functions_yourname.cpp battleship.dunkirk.sh 12 + ``` 13 + 14 + ## Development 15 + 16 + Built with Go using [Wish](https://github.com/charmbracelet/wish), [Bubble Tea](https://github.com/charmbracelet/bubbletea), and [Lipgloss](https://github.com/charmbracelet/lipgloss). 17 + 18 + ```bash 19 + # Build and run 20 + make build 21 + make run 22 + 23 + # Generate SSH host key 24 + make gen-key 25 + ``` 26 + 27 + See `AGENTS.md` for architecture details. 28 + 29 + The main repo is [the tangled repo](https://tangled.org/dunkirk.sh/battleship-arena) and the github is just a mirror. 30 + 31 + <p align="center"> 32 + <img src="https://raw.githubusercontent.com/taciturnaxolotl/carriage/master/.github/images/line-break.svg" /> 33 + </p> 34 + 35 + <p align="center"> 36 + &copy 2025-present <a href="https://github.com/taciturnaxolotl">Kieran Klukas</a> 37 + </p> 38 + 39 + <p align="center"> 40 + <a href="https://github.com/taciturnaxolotl/battleship-arena/blob/main/LICENSE.md"><img src="https://img.shields.io/static/v1.svg?style=for-the-badge&label=License&message=MIT&logoColor=d9e0ee&colorA=363a4f&colorB=b7bdf8"/></a> 41 + </p>
+140
database.go
··· 1 + package main 2 + 3 + import ( 4 + "database/sql" 5 + "time" 6 + 7 + _ "github.com/mattn/go-sqlite3" 8 + ) 9 + 10 + var globalDB *sql.DB 11 + 12 + type LeaderboardEntry struct { 13 + Username string 14 + Wins int 15 + Losses int 16 + AvgMoves float64 17 + LastPlayed time.Time 18 + } 19 + 20 + type Submission struct { 21 + ID int 22 + Username string 23 + Filename string 24 + UploadTime time.Time 25 + Status string // pending, testing, completed, failed 26 + } 27 + 28 + func initDB(path string) (*sql.DB, error) { 29 + db, err := sql.Open("sqlite3", path) 30 + if err != nil { 31 + return nil, err 32 + } 33 + 34 + schema := ` 35 + CREATE TABLE IF NOT EXISTS submissions ( 36 + id INTEGER PRIMARY KEY AUTOINCREMENT, 37 + username TEXT NOT NULL, 38 + filename TEXT NOT NULL, 39 + upload_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 40 + status TEXT DEFAULT 'pending' 41 + ); 42 + 43 + CREATE TABLE IF NOT EXISTS results ( 44 + id INTEGER PRIMARY KEY AUTOINCREMENT, 45 + submission_id INTEGER, 46 + opponent TEXT, 47 + result TEXT, -- win, loss, tie 48 + moves INTEGER, 49 + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 50 + FOREIGN KEY (submission_id) REFERENCES submissions(id) 51 + ); 52 + 53 + CREATE INDEX IF NOT EXISTS idx_results_submission ON results(submission_id); 54 + CREATE INDEX IF NOT EXISTS idx_submissions_username ON submissions(username); 55 + CREATE INDEX IF NOT EXISTS idx_submissions_status ON submissions(status); 56 + ` 57 + 58 + _, err = db.Exec(schema) 59 + return db, err 60 + } 61 + 62 + func getLeaderboard(limit int) ([]LeaderboardEntry, error) { 63 + query := ` 64 + SELECT 65 + s.username, 66 + SUM(CASE WHEN r.result = 'win' THEN 1 ELSE 0 END) as wins, 67 + SUM(CASE WHEN r.result = 'loss' THEN 1 ELSE 0 END) as losses, 68 + AVG(r.moves) as avg_moves, 69 + MAX(r.timestamp) as last_played 70 + FROM submissions s 71 + JOIN results r ON s.id = r.submission_id 72 + GROUP BY s.username 73 + ORDER BY wins DESC, losses ASC, avg_moves ASC 74 + LIMIT ? 75 + ` 76 + 77 + rows, err := globalDB.Query(query, limit) 78 + if err != nil { 79 + return nil, err 80 + } 81 + defer rows.Close() 82 + 83 + var entries []LeaderboardEntry 84 + for rows.Next() { 85 + var e LeaderboardEntry 86 + err := rows.Scan(&e.Username, &e.Wins, &e.Losses, &e.AvgMoves, &e.LastPlayed) 87 + if err != nil { 88 + return nil, err 89 + } 90 + entries = append(entries, e) 91 + } 92 + 93 + return entries, rows.Err() 94 + } 95 + 96 + func addSubmission(username, filename string) (int64, error) { 97 + result, err := globalDB.Exec( 98 + "INSERT INTO submissions (username, filename) VALUES (?, ?)", 99 + username, filename, 100 + ) 101 + if err != nil { 102 + return 0, err 103 + } 104 + return result.LastInsertId() 105 + } 106 + 107 + func addResult(submissionID int, opponent, result string, moves int) error { 108 + _, err := globalDB.Exec( 109 + "INSERT INTO results (submission_id, opponent, result, moves) VALUES (?, ?, ?, ?)", 110 + submissionID, opponent, result, moves, 111 + ) 112 + return err 113 + } 114 + 115 + func updateSubmissionStatus(id int, status string) error { 116 + _, err := globalDB.Exec("UPDATE submissions SET status = ? WHERE id = ?", status, id) 117 + return err 118 + } 119 + 120 + func getPendingSubmissions() ([]Submission, error) { 121 + rows, err := globalDB.Query( 122 + "SELECT id, username, filename, upload_time, status FROM submissions WHERE status = 'pending' ORDER BY upload_time", 123 + ) 124 + if err != nil { 125 + return nil, err 126 + } 127 + defer rows.Close() 128 + 129 + var submissions []Submission 130 + for rows.Next() { 131 + var s Submission 132 + err := rows.Scan(&s.ID, &s.Username, &s.Filename, &s.UploadTime, &s.Status) 133 + if err != nil { 134 + return nil, err 135 + } 136 + submissions = append(submissions, s) 137 + } 138 + 139 + return submissions, rows.Err() 140 + }
+43
go.mod
··· 1 + module battleship-arena 2 + 3 + go 1.25.4 4 + 5 + require ( 6 + github.com/charmbracelet/bubbletea v1.3.10 7 + github.com/charmbracelet/lipgloss v1.1.0 8 + github.com/charmbracelet/ssh v0.0.0-20250826160808-ebfa259c7309 9 + github.com/charmbracelet/wish v1.4.7 10 + github.com/mattn/go-sqlite3 v1.14.32 11 + ) 12 + 13 + require ( 14 + github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect 15 + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 16 + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect 17 + github.com/charmbracelet/keygen v0.5.3 // indirect 18 + github.com/charmbracelet/log v0.4.1 // indirect 19 + github.com/charmbracelet/x/ansi v0.10.1 // indirect 20 + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect 21 + github.com/charmbracelet/x/conpty v0.1.0 // indirect 22 + github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 // indirect 23 + github.com/charmbracelet/x/input v0.3.4 // indirect 24 + github.com/charmbracelet/x/term v0.2.1 // indirect 25 + github.com/charmbracelet/x/termios v0.1.0 // indirect 26 + github.com/charmbracelet/x/windows v0.2.0 // indirect 27 + github.com/creack/pty v1.1.21 // indirect 28 + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect 29 + github.com/go-logfmt/logfmt v0.6.0 // indirect 30 + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 31 + github.com/mattn/go-isatty v0.0.20 // indirect 32 + github.com/mattn/go-localereader v0.0.1 // indirect 33 + github.com/mattn/go-runewidth v0.0.16 // indirect 34 + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 35 + github.com/muesli/cancelreader v0.2.2 // indirect 36 + github.com/muesli/termenv v0.16.0 // indirect 37 + github.com/rivo/uniseg v0.4.7 // indirect 38 + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 39 + golang.org/x/crypto v0.37.0 // indirect 40 + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect 41 + golang.org/x/sys v0.36.0 // indirect 42 + golang.org/x/text v0.24.0 // indirect 43 + )
+85
go.sum
··· 1 + github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 2 + github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 3 + github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 4 + github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 5 + github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= 6 + github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= 7 + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= 8 + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= 9 + github.com/charmbracelet/keygen v0.5.3 h1:2MSDC62OUbDy6VmjIE2jM24LuXUvKywLCmaJDmr/Z/4= 10 + github.com/charmbracelet/keygen v0.5.3/go.mod h1:TcpNoMAO5GSmhx3SgcEMqCrtn8BahKhB8AlwnLjRUpk= 11 + github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= 12 + github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= 13 + github.com/charmbracelet/log v0.4.1 h1:6AYnoHKADkghm/vt4neaNEXkxcXLSV2g1rdyFDOpTyk= 14 + github.com/charmbracelet/log v0.4.1/go.mod h1:pXgyTsqsVu4N9hGdHmQ0xEA4RsXof402LX9ZgiITn2I= 15 + github.com/charmbracelet/ssh v0.0.0-20250826160808-ebfa259c7309 h1:dCVbCRRtg9+tsfiTXTp0WupDlHruAXyp+YoxGVofHHc= 16 + github.com/charmbracelet/ssh v0.0.0-20250826160808-ebfa259c7309/go.mod h1:R9cISUs5kAH4Cq/rguNbSwcR+slE5Dfm8FEs//uoIGE= 17 + github.com/charmbracelet/wish v1.4.7 h1:O+jdLac3s6GaqkOHHSwezejNK04vl6VjO1A+hl8J8Yc= 18 + github.com/charmbracelet/wish v1.4.7/go.mod h1:OBZ8vC62JC5cvbxJLh+bIWtG7Ctmct+ewziuUWK+G14= 19 + github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= 20 + github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= 21 + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= 22 + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= 23 + github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U= 24 + github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ= 25 + github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= 26 + github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= 27 + github.com/charmbracelet/x/input v0.3.4 h1:Mujmnv/4DaitU0p+kIsrlfZl/UlmeLKw1wAP3e1fMN0= 28 + github.com/charmbracelet/x/input v0.3.4/go.mod h1:JI8RcvdZWQIhn09VzeK3hdp4lTz7+yhiEdpEQtZN+2c= 29 + github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= 30 + github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= 31 + github.com/charmbracelet/x/termios v0.1.0 h1:y4rjAHeFksBAfGbkRDmVinMg7x7DELIGAFbdNvxg97k= 32 + github.com/charmbracelet/x/termios v0.1.0/go.mod h1:H/EVv/KRnrYjz+fCYa9bsKdqF3S8ouDK0AZEbG7r+/U= 33 + github.com/charmbracelet/x/windows v0.2.0 h1:ilXA1GJjTNkgOm94CLPeSz7rar54jtFatdmoiONPuEw= 34 + github.com/charmbracelet/x/windows v0.2.0/go.mod h1:ZibNFR49ZFqCXgP76sYanisxRyC+EYrBE7TTknD8s1s= 35 + github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= 36 + github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 37 + github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 38 + github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 39 + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= 40 + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= 41 + github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= 42 + github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 43 + github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 44 + github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 45 + github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 46 + github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 47 + github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= 48 + github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= 49 + github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 50 + github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 51 + github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= 52 + github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= 53 + github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 54 + github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 55 + github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= 56 + github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 57 + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= 58 + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= 59 + github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= 60 + github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 61 + github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= 62 + github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= 63 + github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 64 + github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 65 + github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 66 + github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 67 + github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 68 + github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 69 + github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 70 + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 71 + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 72 + golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 73 + golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 74 + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= 75 + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 76 + golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 77 + golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 78 + golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= 79 + golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 80 + golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= 81 + golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 82 + golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 83 + golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 84 + gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 85 + gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+118
main.go
··· 1 + package main 2 + 3 + import ( 4 + "context" 5 + "errors" 6 + "log" 7 + "net/http" 8 + "os" 9 + "os/signal" 10 + "syscall" 11 + "time" 12 + 13 + tea "github.com/charmbracelet/bubbletea" 14 + "github.com/charmbracelet/lipgloss" 15 + "github.com/charmbracelet/ssh" 16 + "github.com/charmbracelet/wish" 17 + "github.com/charmbracelet/wish/bubbletea" 18 + "github.com/charmbracelet/wish/logging" 19 + ) 20 + 21 + const ( 22 + host = "0.0.0.0" 23 + sshPort = "2222" 24 + webPort = "8080" 25 + uploadDir = "./submissions" 26 + resultsDB = "./results.db" 27 + ) 28 + 29 + func main() { 30 + // Initialize storage 31 + if err := initStorage(); err != nil { 32 + log.Fatal(err) 33 + } 34 + 35 + // Start background worker 36 + workerCtx, workerCancel := context.WithCancel(context.Background()) 37 + defer workerCancel() 38 + go startWorker(workerCtx) 39 + 40 + // Start web server 41 + go startWebServer() 42 + 43 + // Start SSH server with TUI 44 + s, err := wish.NewServer( 45 + wish.WithAddress(host + ":" + sshPort), 46 + wish.WithHostKeyPath(".ssh/battleship_arena"), 47 + wish.WithMiddleware( 48 + scpMiddleware(), 49 + bubbletea.Middleware(teaHandler), 50 + logging.Middleware(), 51 + ), 52 + ) 53 + if err != nil { 54 + log.Fatal(err) 55 + } 56 + 57 + done := make(chan os.Signal, 1) 58 + signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) 59 + 60 + log.Printf("SSH server listening on %s:%s", host, sshPort) 61 + log.Printf("Web leaderboard at http://%s:%s", host, webPort) 62 + 63 + go func() { 64 + if err := s.ListenAndServe(); err != nil && !errors.Is(err, ssh.ErrServerClosed) { 65 + log.Fatal(err) 66 + } 67 + }() 68 + 69 + <-done 70 + log.Println("Shutting down servers...") 71 + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 72 + defer cancel() 73 + if err := s.Shutdown(ctx); err != nil && !errors.Is(err, ssh.ErrServerClosed) { 74 + log.Fatal(err) 75 + } 76 + } 77 + 78 + func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) { 79 + pty, _, active := s.Pty() 80 + if !active { 81 + wish.Fatalln(s, "no active terminal") 82 + return nil, nil 83 + } 84 + 85 + m := initialModel(s.User(), pty.Window.Width, pty.Window.Height) 86 + return m, []tea.ProgramOption{tea.WithAltScreen()} 87 + } 88 + 89 + func initStorage() error { 90 + if err := os.MkdirAll(uploadDir, 0755); err != nil { 91 + return err 92 + } 93 + 94 + db, err := initDB(resultsDB) 95 + if err != nil { 96 + return err 97 + } 98 + globalDB = db 99 + 100 + return nil 101 + } 102 + 103 + func startWebServer() { 104 + mux := http.NewServeMux() 105 + mux.HandleFunc("/", handleLeaderboard) 106 + mux.HandleFunc("/api/leaderboard", handleAPILeaderboard) 107 + 108 + log.Printf("Web server starting on :%s", webPort) 109 + if err := http.ListenAndServe(":"+webPort, mux); err != nil { 110 + log.Fatal(err) 111 + } 112 + } 113 + 114 + var titleStyle = lipgloss.NewStyle(). 115 + Bold(true). 116 + Foreground(lipgloss.Color("205")). 117 + MarginTop(1). 118 + MarginBottom(1)
+222
model.go
··· 1 + package main 2 + 3 + import ( 4 + "fmt" 5 + "strings" 6 + 7 + tea "github.com/charmbracelet/bubbletea" 8 + "github.com/charmbracelet/lipgloss" 9 + ) 10 + 11 + type menuChoice int 12 + 13 + const ( 14 + menuUpload menuChoice = iota 15 + menuLeaderboard 16 + menuSubmit 17 + menuHelp 18 + menuQuit 19 + ) 20 + 21 + type model struct { 22 + username string 23 + width int 24 + height int 25 + choice menuChoice 26 + submitting bool 27 + filename string 28 + fileContent []byte 29 + message string 30 + leaderboard []LeaderboardEntry 31 + } 32 + 33 + func initialModel(username string, width, height int) model { 34 + return model{ 35 + username: username, 36 + width: width, 37 + height: height, 38 + choice: menuUpload, 39 + } 40 + } 41 + 42 + func (m model) Init() tea.Cmd { 43 + return loadLeaderboard 44 + } 45 + 46 + func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 47 + switch msg := msg.(type) { 48 + case tea.KeyMsg: 49 + switch msg.String() { 50 + case "ctrl+c", "q": 51 + 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 + } 63 + case tea.WindowSizeMsg: 64 + m.width = msg.Width 65 + m.height = msg.Height 66 + case leaderboardMsg: 67 + m.leaderboard = msg.entries 68 + } 69 + return m, nil 70 + } 71 + 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 + 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 + 101 + func (m model) View() string { 102 + var b strings.Builder 103 + 104 + title := titleStyle.Render("🚢 Battleship Arena") 105 + b.WriteString(title + "\n\n") 106 + 107 + b.WriteString(fmt.Sprintf("User: %s\n\n", m.username)) 108 + 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 + } 125 + 126 + if m.message != "" { 127 + b.WriteString("\n" + lipgloss.NewStyle(). 128 + Foreground(lipgloss.Color("86")). 129 + Render(m.message) + "\n") 130 + } 131 + 132 + // Show leaderboard if loaded 133 + if len(m.leaderboard) > 0 { 134 + b.WriteString("\n" + renderLeaderboard(m.leaderboard)) 135 + } 136 + 137 + b.WriteString("\n\nPress q to quit, ↑/↓ to navigate, enter to select") 138 + 139 + return b.String() 140 + } 141 + 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 + } 158 + 159 + type leaderboardMsg struct { 160 + entries []LeaderboardEntry 161 + } 162 + 163 + func loadLeaderboard() tea.Msg { 164 + entries, err := getLeaderboard(20) 165 + if err != nil { 166 + return leaderboardMsg{entries: nil} 167 + } 168 + return leaderboardMsg{entries: entries} 169 + } 170 + 171 + type submitMsg struct { 172 + success bool 173 + message string 174 + } 175 + 176 + func submitForTesting(username string) tea.Cmd { 177 + return func() tea.Msg { 178 + // Queue submission for testing 179 + if err := queueSubmission(username); err != nil { 180 + return submitMsg{success: false, message: err.Error()} 181 + } 182 + return submitMsg{success: true, message: "Submitted successfully!"} 183 + } 184 + } 185 + 186 + func renderLeaderboard(entries []LeaderboardEntry) string { 187 + if len(entries) == 0 { 188 + return "No entries yet" 189 + } 190 + 191 + var b strings.Builder 192 + b.WriteString(lipgloss.NewStyle().Bold(true).Render("🏆 Leaderboard") + "\n\n") 193 + 194 + headerStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("240")) 195 + b.WriteString(headerStyle.Render(fmt.Sprintf("%-4s %-20s %8s %8s %10s\n", 196 + "Rank", "User", "Wins", "Losses", "Win Rate"))) 197 + 198 + for i, entry := range entries { 199 + winRate := 0.0 200 + total := entry.Wins + entry.Losses 201 + if total > 0 { 202 + winRate = float64(entry.Wins) / float64(total) * 100 203 + } 204 + 205 + rank := fmt.Sprintf("#%d", i+1) 206 + line := fmt.Sprintf("%-4s %-20s %8d %8d %9.2f%%\n", 207 + rank, entry.Username, entry.Wins, entry.Losses, winRate) 208 + 209 + style := lipgloss.NewStyle() 210 + if i == 0 { 211 + style = style.Foreground(lipgloss.Color("220")) // Gold 212 + } else if i == 1 { 213 + style = style.Foreground(lipgloss.Color("250")) // Silver 214 + } else if i == 2 { 215 + style = style.Foreground(lipgloss.Color("208")) // Bronze 216 + } 217 + 218 + b.WriteString(style.Render(line)) 219 + } 220 + 221 + return b.String() 222 + }
+125
runner.go
··· 1 + package main 2 + 3 + import ( 4 + "fmt" 5 + "os" 6 + "os/exec" 7 + "path/filepath" 8 + "regexp" 9 + "strings" 10 + ) 11 + 12 + const battleshipRepoPath = "/Users/kierank/code/school/cs1210-battleship" 13 + 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 + func processSubmissions() error { 30 + submissions, err := getPendingSubmissions() 31 + if err != nil { 32 + return err 33 + } 34 + 35 + for _, sub := range submissions { 36 + if err := testSubmission(sub); err != nil { 37 + updateSubmissionStatus(sub.ID, "failed") 38 + continue 39 + } 40 + updateSubmissionStatus(sub.ID, "completed") 41 + } 42 + 43 + return nil 44 + } 45 + 46 + func testSubmission(sub Submission) error { 47 + updateSubmissionStatus(sub.ID, "testing") 48 + 49 + // Copy submission to battleship repo 50 + srcPath := filepath.Join(uploadDir, sub.Username, sub.Filename) 51 + dstPath := filepath.Join(battleshipRepoPath, "src", sub.Filename) 52 + 53 + input, err := os.ReadFile(srcPath) 54 + if err != nil { 55 + return err 56 + } 57 + if err := os.WriteFile(dstPath, input, 0644); err != nil { 58 + return err 59 + } 60 + 61 + // Extract student ID from filename (memory_functions_NNNN.cpp) 62 + re := regexp.MustCompile(`memory_functions_(\w+)\.cpp`) 63 + matches := re.FindStringSubmatch(sub.Filename) 64 + if len(matches) < 2 { 65 + return fmt.Errorf("invalid filename format") 66 + } 67 + studentID := matches[1] 68 + 69 + // Build the battleship program 70 + buildDir := filepath.Join(battleshipRepoPath, "build") 71 + os.MkdirAll(buildDir, 0755) 72 + 73 + // Compile using the light version for testing 74 + cmd := exec.Command("g++", "-std=c++11", "-O3", 75 + "-o", filepath.Join(buildDir, "battle_"+studentID), 76 + filepath.Join(battleshipRepoPath, "src", "battle_light.cpp"), 77 + filepath.Join(battleshipRepoPath, "src", "battleship_light.cpp"), 78 + filepath.Join(battleshipRepoPath, "src", sub.Filename), 79 + ) 80 + output, err := cmd.CombinedOutput() 81 + if err != nil { 82 + return fmt.Errorf("compilation failed: %s", output) 83 + } 84 + 85 + // Run benchmark tests (100 games) 86 + cmd = exec.Command(filepath.Join(buildDir, "battle_"+studentID), "--benchmark", "100") 87 + output, err = cmd.CombinedOutput() 88 + if err != nil { 89 + return fmt.Errorf("benchmark failed: %s", output) 90 + } 91 + 92 + // Parse results and store in database 93 + results := parseResults(string(output)) 94 + for opponent, result := range results { 95 + addResult(sub.ID, opponent, result.Result, result.Moves) 96 + } 97 + 98 + return nil 99 + } 100 + 101 + type GameResult struct { 102 + Result string 103 + Moves int 104 + } 105 + 106 + func parseResults(output string) map[string]GameResult { 107 + results := make(map[string]GameResult) 108 + 109 + // Parse win/loss stats from benchmark output 110 + // Example: "Smart AI wins: 95 (95.0%)" 111 + lines := strings.Split(output, "\n") 112 + for _, line := range lines { 113 + if strings.Contains(line, "Smart AI wins:") { 114 + // Extract win count 115 + re := regexp.MustCompile(`Smart AI wins: (\d+)`) 116 + matches := re.FindStringSubmatch(line) 117 + if len(matches) >= 2 { 118 + // For now, just record as wins against "random" 119 + results["random"] = GameResult{Result: "win", Moves: 50} 120 + } 121 + } 122 + } 123 + 124 + return results 125 + }
+105
scp.go
··· 1 + package main 2 + 3 + import ( 4 + "fmt" 5 + "io" 6 + "log" 7 + "os" 8 + "path/filepath" 9 + 10 + "github.com/charmbracelet/ssh" 11 + "github.com/charmbracelet/wish" 12 + ) 13 + 14 + // Add SCP support as a custom middleware 15 + func scpMiddleware() wish.Middleware { 16 + return func(sh ssh.Handler) ssh.Handler { 17 + return func(s ssh.Session) { 18 + cmd := s.Command() 19 + if len(cmd) > 0 && cmd[0] == "scp" { 20 + handleSCP(s, cmd) 21 + return 22 + } 23 + sh(s) 24 + } 25 + } 26 + } 27 + 28 + func handleSCP(s ssh.Session, cmd []string) { 29 + // Parse SCP command 30 + target := false 31 + filename := "" 32 + 33 + for i, arg := range cmd { 34 + if arg == "-t" { 35 + target = true 36 + } else if i == len(cmd)-1 { 37 + filename = filepath.Base(arg) 38 + } 39 + } 40 + 41 + if !target { 42 + log.Printf("SCP source mode not supported from %s", s.User()) 43 + fmt.Fprintf(s, "SCP source mode not supported\n") 44 + s.Exit(1) 45 + return 46 + } 47 + 48 + // Validate filename 49 + matched, _ := filepath.Match("memory_functions_*.cpp", filename) 50 + if !matched { 51 + log.Printf("Invalid filename from %s: %s", s.User(), filename) 52 + fmt.Fprintf(s, "Only memory_functions_*.cpp files are accepted\n") 53 + s.Exit(1) 54 + return 55 + } 56 + 57 + // Create user directory 58 + userDir := filepath.Join(uploadDir, s.User()) 59 + if err := os.MkdirAll(userDir, 0755); err != nil { 60 + log.Printf("Failed to create user directory: %v", err) 61 + s.Exit(1) 62 + return 63 + } 64 + 65 + // SCP protocol: send 0 byte to indicate ready 66 + fmt.Fprintf(s, "\x00") 67 + 68 + // Read SCP header (C0644 size filename) 69 + buf := make([]byte, 1024) 70 + n, err := s.Read(buf) 71 + if err != nil { 72 + log.Printf("Failed to read SCP header: %v", err) 73 + s.Exit(1) 74 + return 75 + } 76 + 77 + // Acknowledge header 78 + fmt.Fprintf(s, "\x00") 79 + 80 + // Save file 81 + dstPath := filepath.Join(userDir, filename) 82 + file, err := os.Create(dstPath) 83 + if err != nil { 84 + log.Printf("Failed to create file: %v", err) 85 + s.Exit(1) 86 + return 87 + } 88 + defer file.Close() 89 + 90 + // Read file content 91 + _, err = io.Copy(file, io.LimitReader(s, int64(n))) 92 + if err != nil && err != io.EOF { 93 + log.Printf("Failed to write file: %v", err) 94 + s.Exit(1) 95 + return 96 + } 97 + 98 + // Final acknowledgment 99 + fmt.Fprintf(s, "\x00") 100 + 101 + log.Printf("Uploaded %s from %s", filename, s.User()) 102 + addSubmission(s.User(), filename) 103 + 104 + s.Exit(0) 105 + }
+30
scripts/test-submission.sh
··· 1 + #!/bin/bash 2 + # Example test script for submitting and testing an AI 3 + 4 + set -e 5 + 6 + USER="testuser" 7 + HOST="localhost" 8 + PORT="2222" 9 + FILE="memory_functions_test.cpp" 10 + 11 + echo "🚢 Battleship Arena Test Script" 12 + echo "================================" 13 + 14 + # Check if submission file exists 15 + if [ ! -f "$1" ]; then 16 + echo "Usage: $0 <memory_functions_*.cpp>" 17 + exit 1 18 + fi 19 + 20 + FILE=$(basename "$1") 21 + 22 + echo "📤 Uploading $FILE..." 23 + scp -P $PORT "$1" ${USER}@${HOST}:~/ 24 + 25 + echo "✅ Upload complete!" 26 + echo "" 27 + echo "Next steps:" 28 + echo "1. SSH into the server: ssh -p $PORT ${USER}@${HOST}" 29 + echo "2. Navigate to 'Test Submission' in the menu" 30 + echo "3. View results on the leaderboard: http://localhost:8080"
+254
web.go
··· 1 + package main 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "html/template" 7 + "net/http" 8 + ) 9 + 10 + const leaderboardHTML = ` 11 + <!DOCTYPE html> 12 + <html> 13 + <head> 14 + <title>Battleship Arena - Leaderboard</title> 15 + <meta charset="UTF-8"> 16 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 17 + <style> 18 + body { 19 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; 20 + max-width: 1200px; 21 + margin: 0 auto; 22 + padding: 20px; 23 + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 24 + min-height: 100vh; 25 + } 26 + .container { 27 + background: white; 28 + border-radius: 12px; 29 + box-shadow: 0 20px 60px rgba(0,0,0,0.3); 30 + padding: 40px; 31 + } 32 + h1 { 33 + color: #333; 34 + text-align: center; 35 + margin-bottom: 10px; 36 + font-size: 2.5em; 37 + } 38 + .subtitle { 39 + text-align: center; 40 + color: #666; 41 + margin-bottom: 40px; 42 + font-size: 1.1em; 43 + } 44 + table { 45 + width: 100%; 46 + border-collapse: collapse; 47 + margin-top: 20px; 48 + } 49 + th { 50 + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 51 + color: white; 52 + padding: 15px; 53 + text-align: left; 54 + font-weight: 600; 55 + } 56 + td { 57 + padding: 12px 15px; 58 + border-bottom: 1px solid #eee; 59 + } 60 + tr:hover { 61 + background: #f8f9fa; 62 + } 63 + .rank { 64 + font-weight: bold; 65 + font-size: 1.1em; 66 + } 67 + .rank-1 { color: #FFD700; } 68 + .rank-2 { color: #C0C0C0; } 69 + .rank-3 { color: #CD7F32; } 70 + .win-rate { 71 + font-weight: 600; 72 + } 73 + .win-rate-high { color: #10b981; } 74 + .win-rate-med { color: #f59e0b; } 75 + .win-rate-low { color: #ef4444; } 76 + .stats { 77 + display: flex; 78 + justify-content: space-around; 79 + margin-top: 40px; 80 + padding-top: 30px; 81 + border-top: 2px solid #eee; 82 + } 83 + .stat { 84 + text-align: center; 85 + } 86 + .stat-value { 87 + font-size: 2em; 88 + font-weight: bold; 89 + color: #667eea; 90 + } 91 + .stat-label { 92 + color: #666; 93 + margin-top: 5px; 94 + } 95 + .instructions { 96 + background: #f8f9fa; 97 + padding: 20px; 98 + border-radius: 8px; 99 + margin-top: 30px; 100 + } 101 + .instructions h3 { 102 + margin-top: 0; 103 + color: #333; 104 + } 105 + .instructions code { 106 + background: #e9ecef; 107 + padding: 3px 8px; 108 + border-radius: 4px; 109 + font-family: 'Monaco', 'Courier New', monospace; 110 + } 111 + .refresh-note { 112 + text-align: center; 113 + color: #999; 114 + font-size: 0.9em; 115 + margin-top: 20px; 116 + } 117 + </style> 118 + <script> 119 + // Auto-refresh every 30 seconds 120 + setTimeout(() => location.reload(), 30000); 121 + </script> 122 + </head> 123 + <body> 124 + <div class="container"> 125 + <h1>🚢 Battleship Arena</h1> 126 + <p class="subtitle">Smart AI Competition Leaderboard</p> 127 + 128 + <table> 129 + <thead> 130 + <tr> 131 + <th>Rank</th> 132 + <th>Player</th> 133 + <th>Wins</th> 134 + <th>Losses</th> 135 + <th>Win Rate</th> 136 + <th>Avg Moves</th> 137 + <th>Last Played</th> 138 + </tr> 139 + </thead> 140 + <tbody> 141 + {{range $i, $e := .Entries}} 142 + <tr> 143 + <td class="rank rank-{{add $i 1}}">{{if lt $i 3}}{{medal $i}}{{else}}#{{add $i 1}}{{end}}</td> 144 + <td><strong>{{$e.Username}}</strong></td> 145 + <td>{{$e.Wins}}</td> 146 + <td>{{$e.Losses}}</td> 147 + <td class="win-rate {{winRateClass $e}}">{{winRate $e}}%</td> 148 + <td>{{printf "%.1f" $e.AvgMoves}}</td> 149 + <td>{{$e.LastPlayed.Format "Jan 2, 3:04 PM"}}</td> 150 + </tr> 151 + {{end}} 152 + </tbody> 153 + </table> 154 + 155 + <div class="stats"> 156 + <div class="stat"> 157 + <div class="stat-value">{{.TotalPlayers}}</div> 158 + <div class="stat-label">Players</div> 159 + </div> 160 + <div class="stat"> 161 + <div class="stat-value">{{.TotalGames}}</div> 162 + <div class="stat-label">Games Played</div> 163 + </div> 164 + </div> 165 + 166 + <div class="instructions"> 167 + <h3>📤 How to Submit</h3> 168 + <p>Upload your battleship AI implementation via SSH:</p> 169 + <code>ssh -p 2222 username@localhost</code> 170 + <p style="margin-top: 10px;">Then navigate to upload your <code>memory_functions_*.cpp</code> file.</p> 171 + </div> 172 + 173 + <p class="refresh-note">Page auto-refreshes every 30 seconds</p> 174 + </div> 175 + </body> 176 + </html> 177 + ` 178 + 179 + var tmpl = template.Must(template.New("leaderboard").Funcs(template.FuncMap{ 180 + "add": func(a, b int) int { 181 + return a + b 182 + }, 183 + "medal": func(i int) string { 184 + medals := []string{"🥇", "🥈", "🥉"} 185 + if i < len(medals) { 186 + return medals[i] 187 + } 188 + return "" 189 + }, 190 + "winRate": func(e LeaderboardEntry) string { 191 + total := e.Wins + e.Losses 192 + if total == 0 { 193 + return "0.0" 194 + } 195 + rate := float64(e.Wins) / float64(total) * 100 196 + return formatFloat(rate, 1) 197 + }, 198 + "winRateClass": func(e LeaderboardEntry) string { 199 + total := e.Wins + e.Losses 200 + if total == 0 { 201 + return "win-rate-low" 202 + } 203 + rate := float64(e.Wins) / float64(total) * 100 204 + if rate >= 80 { 205 + return "win-rate-high" 206 + } else if rate >= 50 { 207 + return "win-rate-med" 208 + } 209 + return "win-rate-low" 210 + }, 211 + }).Parse(leaderboardHTML)) 212 + 213 + func formatFloat(f float64, decimals int) string { 214 + return fmt.Sprintf("%.1f", f) 215 + } 216 + 217 + func handleLeaderboard(w http.ResponseWriter, r *http.Request) { 218 + entries, err := getLeaderboard(50) 219 + if err != nil { 220 + http.Error(w, "Failed to load leaderboard", http.StatusInternalServerError) 221 + return 222 + } 223 + 224 + data := struct { 225 + Entries []LeaderboardEntry 226 + TotalPlayers int 227 + TotalGames int 228 + }{ 229 + Entries: entries, 230 + TotalPlayers: len(entries), 231 + TotalGames: calculateTotalGames(entries), 232 + } 233 + 234 + tmpl.Execute(w, data) 235 + } 236 + 237 + func handleAPILeaderboard(w http.ResponseWriter, r *http.Request) { 238 + entries, err := getLeaderboard(50) 239 + if err != nil { 240 + http.Error(w, "Failed to load leaderboard", http.StatusInternalServerError) 241 + return 242 + } 243 + 244 + w.Header().Set("Content-Type", "application/json") 245 + json.NewEncoder(w).Encode(entries) 246 + } 247 + 248 + func calculateTotalGames(entries []LeaderboardEntry) int { 249 + total := 0 250 + for _, e := range entries { 251 + total += e.Wins + e.Losses 252 + } 253 + return total / 2 // Each game counted twice (win+loss) 254 + }
+24
worker.go
··· 1 + package main 2 + 3 + import ( 4 + "context" 5 + "log" 6 + "time" 7 + ) 8 + 9 + // Background worker that processes pending submissions 10 + func startWorker(ctx context.Context) { 11 + ticker := time.NewTicker(30 * time.Second) 12 + defer ticker.Stop() 13 + 14 + for { 15 + select { 16 + case <-ctx.Done(): 17 + return 18 + case <-ticker.C: 19 + if err := processSubmissions(); err != nil { 20 + log.Printf("Worker error: %v", err) 21 + } 22 + } 23 + } 24 + }