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: move to bracket style

+1991 -103
+8
.gitignore
··· 4 4 battleship-arena 5 5 *.log 6 6 build/ 7 + 8 + # Generated files in engine 9 + battleship-engine/src/memory_functions_*.cpp 10 + battleship-engine/src/memory_functions_*.h 11 + battleship-engine/src/match_*.cpp 12 + battleship-engine/build/ 13 + 14 + scripts/test-submissions/memory_functions_klukas.cpp
+68
TOURNAMENT.md
··· 1 + # Battleship Arena - Tournament System 2 + 3 + ## Overview 4 + Tournament-style battleship AI competition with automatic header generation and local compilation. 5 + 6 + ## Architecture 7 + 8 + ### Battleship Engine 9 + - Located in `./battleship-engine/` 10 + - Contains the lightweight battleship game engine 11 + - No external dependencies on the school repo 12 + - Auto-generates header files for submissions 13 + 14 + ### Submission Flow 15 + 1. User uploads `memory_functions_<name>.cpp` via SCP/SFTP 16 + 2. System auto-generates `memory_functions_<name>.h` header 17 + 3. Compiles submission with the battleship engine 18 + 4. If successful, runs tournament matches against all active submissions 19 + 5. Updates leaderboard with results 20 + 21 + ### Tournament Matching 22 + - Each match compiles both AIs into a single binary 23 + - Runs 10 games per match 24 + - Winner determined by total wins 25 + - All results stored in database 26 + 27 + ## Test Submissions 28 + 29 + Three AI implementations for testing: 30 + 31 + 1. **random** - Pure random shooter (baseline) 32 + 2. **hunter** - Checkerboard hunt + adjacent targeting 33 + 3. **klukas** - Advanced probability-based AI 34 + 35 + ## Testing 36 + 37 + ```bash 38 + # Upload test submissions 39 + ./scripts/test-upload.sh 40 + 41 + # This uploads: 42 + # - alice with random AI 43 + # - bob with hunter AI 44 + # - charlie with klukas AI 45 + ``` 46 + 47 + ## Requirements 48 + 49 + Submissions must: 50 + - Be named `memory_functions_<name>.cpp` 51 + - Implement three functions: 52 + - `void initMemory<Name>(ComputerMemory &memory)` 53 + - `std::string smartMove<Name>(const ComputerMemory &memory)` 54 + - `void updateMemory<Name>(int row, int col, int result, ComputerMemory &memory)` 55 + - Use only standard includes and provided headers (battleship.h, kasbs.h, memory.h) 56 + 57 + Headers are auto-generated - users only need to upload the `.cpp` file! 58 + 59 + ## Usage 60 + 61 + Start server: 62 + ```bash 63 + ./battleship-arena 64 + ``` 65 + 66 + View results: 67 + - SSH TUI: `ssh -p 2222 username@0.0.0.0` 68 + - Web: http://0.0.0.0:8080
+452
battleship-engine/src/battle_light.cpp
··· 1 + // Lightweight cross-platform battleship implementation 2 + // Author: Kieran Klukas 3 + // Date: November 2025 4 + // Purpose: Test smart battleship AI with benchmarking on non-Linux systems 5 + 6 + #include "battleship_light.h" 7 + #include "memory.h" 8 + #include "memory_functions_klukas.h" 9 + #include <iostream> 10 + #include <chrono> 11 + #include <thread> 12 + #include <vector> 13 + #include <mutex> 14 + #include <atomic> 15 + 16 + using namespace std; 17 + 18 + struct BenchmarkStats { 19 + atomic<int> wins{0}; 20 + atomic<int> losses{0}; 21 + atomic<int> ties{0}; 22 + atomic<long long> totalMoves{0}; 23 + atomic<long long> totalTimeNs{0}; 24 + atomic<int> minMovesWin{999999}; 25 + atomic<int> maxMovesWin{0}; 26 + atomic<int> minMovesLoss{999999}; 27 + atomic<int> maxMovesLoss{0}; 28 + 29 + mutex updateMutex; // For min/max updates 30 + }; 31 + 32 + void printStats(const BenchmarkStats &stats, int gamesPlayed) { 33 + const int MAX_MOVES = 200; // Theoretical max (both players shoot all 100 squares) 34 + double avgMoves = (double)stats.totalMoves.load() / gamesPlayed; 35 + double movesPercent = (avgMoves / MAX_MOVES) * 100.0; 36 + 37 + cout << "\n========== BENCHMARK RESULTS ==========" << endl; 38 + cout << "Games played: " << gamesPlayed << endl; 39 + cout << "Smart AI wins: " << stats.wins.load() << " (" 40 + << (100.0 * stats.wins.load() / gamesPlayed) << "%)" << endl; 41 + cout << "Dumb AI wins: " << stats.losses.load() << " (" 42 + << (100.0 * stats.losses.load() / gamesPlayed) << "%)" << endl; 43 + cout << "Ties: " << stats.ties.load() << endl; 44 + cout << "Avg moves per game: " << (int)avgMoves 45 + << " (" << movesPercent << "% of max)" << endl; 46 + 47 + if (stats.wins.load() > 0) { 48 + cout << "Win move range: " << stats.minMovesWin.load() << "-" << stats.maxMovesWin.load() << endl; 49 + } 50 + if (stats.losses.load() > 0) { 51 + cout << "Loss move range: " << stats.minMovesLoss.load() << "-" << stats.maxMovesLoss.load() << endl; 52 + } 53 + 54 + double avgTimeMs = (double)stats.totalTimeNs.load() / gamesPlayed / 1000000.0; 55 + cout << "Avg time per game: " << avgTimeMs << "ms" << endl; 56 + cout << "========================================\n" << endl; 57 + } 58 + 59 + // Thread-safe game runner function 60 + void runGames(int startGame, int endGame, BenchmarkStats &stats, bool logLosses, 61 + atomic<int> &gamesCompleted, int totalGames, unsigned int threadSeed) { 62 + // Each thread gets its own random seed 63 + srand(threadSeed); 64 + 65 + for (int game = startGame; game < endGame; game++) { 66 + auto startTime = chrono::high_resolution_clock::now(); 67 + 68 + Board dumbComputerBoard, smartComputerBoard; 69 + ComputerMemory smartComputerMemory; 70 + 71 + string dumbComputerMove, smartComputerMove; 72 + int numDumbComputerShipsSunk = 0; 73 + int numSmartComputerShipsSunk = 0; 74 + int dumbComputerRow, dumbComputerColumn; 75 + int smartComputerRow, smartComputerColumn; 76 + int checkValue, dumbComputerResult, smartComputerResult; 77 + int moveCount = 0; 78 + 79 + initializeBoard(dumbComputerBoard); 80 + initializeBoard(smartComputerBoard); 81 + initMemoryKlukas(smartComputerMemory); 82 + 83 + while (true) { 84 + moveCount++; 85 + 86 + // Dumb computer move 87 + dumbComputerMove = randomMove(); 88 + checkValue = checkMove(dumbComputerMove, smartComputerBoard, 89 + dumbComputerRow, dumbComputerColumn); 90 + 91 + while (checkValue != VALID_MOVE) { 92 + dumbComputerMove = randomMove(); 93 + checkValue = checkMove(dumbComputerMove, smartComputerBoard, 94 + dumbComputerRow, dumbComputerColumn); 95 + } 96 + 97 + // Smart computer move 98 + smartComputerMove = smartMoveKlukas(smartComputerMemory); 99 + int checkResult = checkMove(smartComputerMove, dumbComputerBoard, 100 + smartComputerRow, smartComputerColumn); 101 + 102 + while (checkResult != VALID_MOVE) { 103 + smartComputerMove = randomMove(); 104 + checkResult = checkMove(smartComputerMove, dumbComputerBoard, 105 + smartComputerRow, smartComputerColumn); 106 + } 107 + 108 + // Execute moves 109 + dumbComputerResult = playMove(dumbComputerRow, dumbComputerColumn, 110 + smartComputerBoard); 111 + smartComputerResult = playMove(smartComputerRow, smartComputerColumn, 112 + dumbComputerBoard); 113 + updateMemoryKlukas(smartComputerRow, smartComputerColumn, 114 + smartComputerResult, smartComputerMemory); 115 + 116 + if (isASunk(dumbComputerResult)) { 117 + numDumbComputerShipsSunk++; 118 + } 119 + if (isASunk(smartComputerResult)) { 120 + numSmartComputerShipsSunk++; 121 + } 122 + 123 + if (numDumbComputerShipsSunk == 5 || numSmartComputerShipsSunk == 5) { 124 + break; 125 + } 126 + } 127 + 128 + auto endTime = chrono::high_resolution_clock::now(); 129 + auto duration = chrono::duration_cast<chrono::nanoseconds>(endTime - startTime); 130 + 131 + // Update stats atomically 132 + stats.totalMoves += moveCount; 133 + stats.totalTimeNs += duration.count(); 134 + 135 + if (numDumbComputerShipsSunk == 5 && numSmartComputerShipsSunk == 5) { 136 + stats.ties++; 137 + } else if (numSmartComputerShipsSunk == 5) { 138 + stats.wins++; 139 + 140 + // Update min/max with mutex protection 141 + lock_guard<mutex> lock(stats.updateMutex); 142 + int currentMin = stats.minMovesWin.load(); 143 + if (moveCount < currentMin) stats.minMovesWin = moveCount; 144 + int currentMax = stats.maxMovesWin.load(); 145 + if (moveCount > currentMax) stats.maxMovesWin = moveCount; 146 + } else { 147 + stats.losses++; 148 + 149 + lock_guard<mutex> lock(stats.updateMutex); 150 + int currentMin = stats.minMovesLoss.load(); 151 + if (moveCount < currentMin) stats.minMovesLoss = moveCount; 152 + int currentMax = stats.maxMovesLoss.load(); 153 + if (moveCount > currentMax) stats.maxMovesLoss = moveCount; 154 + } 155 + 156 + // Update progress counter 157 + gamesCompleted++; 158 + } 159 + } 160 + 161 + int main(int argc, char* argv[]) { 162 + bool benchmark = false; 163 + bool verbose = false; 164 + bool logLosses = false; 165 + bool catchGuards = false; 166 + int numGames = 1; 167 + 168 + // Parse command line args 169 + for (int i = 1; i < argc; i++) { 170 + string arg = argv[i]; 171 + if (arg == "--benchmark" || arg == "-b") { 172 + benchmark = true; 173 + if (i + 1 < argc) { 174 + numGames = atoi(argv[++i]); 175 + if (numGames <= 0) numGames = 100; 176 + } else { 177 + numGames = 100; 178 + } 179 + } else if (arg == "--verbose" || arg == "-v") { 180 + verbose = true; 181 + } else if (arg == "--log-losses" || arg == "-l") { 182 + logLosses = true; 183 + } else if (arg == "--catch-guards" || arg == "-g") { 184 + catchGuards = true; 185 + } 186 + } 187 + 188 + BenchmarkStats stats; 189 + srand(time(NULL)); 190 + 191 + // Catch-guards mode: run games until we hit a guard 192 + if (catchGuards) { 193 + cout << "Running games until guard is tripped..." << endl; 194 + int gamesRun = 0; 195 + 196 + while (true) { 197 + gamesRun++; 198 + resetGuardTripped(); 199 + 200 + Board dumbComputerBoard, smartComputerBoard; 201 + ComputerMemory smartComputerMemory; 202 + string dumbComputerMove, smartComputerMove; 203 + int numDumbComputerShipsSunk = 0; 204 + int numSmartComputerShipsSunk = 0; 205 + int dumbComputerRow, dumbComputerColumn; 206 + int smartComputerRow, smartComputerColumn; 207 + int checkValue, dumbComputerResult, smartComputerResult; 208 + int moveCount = 0; 209 + 210 + initializeBoard(dumbComputerBoard); 211 + initializeBoard(smartComputerBoard); 212 + initMemoryKlukas(smartComputerMemory); 213 + 214 + bool guardTripped = false; 215 + while (true) { 216 + moveCount++; 217 + 218 + // Dumb computer move 219 + dumbComputerMove = randomMove(); 220 + checkValue = checkMove(dumbComputerMove, smartComputerBoard, 221 + dumbComputerRow, dumbComputerColumn); 222 + while (checkValue != VALID_MOVE) { 223 + dumbComputerMove = randomMove(); 224 + checkValue = checkMove(dumbComputerMove, smartComputerBoard, 225 + dumbComputerRow, dumbComputerColumn); 226 + } 227 + 228 + // Smart computer move 229 + smartComputerMove = smartMoveKlukas(smartComputerMemory); 230 + 231 + // Check if guard was tripped 232 + if (getGuardTripped()) { 233 + guardTripped = true; 234 + break; 235 + } 236 + 237 + int checkResult = checkMove(smartComputerMove, dumbComputerBoard, 238 + smartComputerRow, smartComputerColumn); 239 + while (checkResult != VALID_MOVE) { 240 + smartComputerMove = randomMove(); 241 + checkResult = checkMove(smartComputerMove, dumbComputerBoard, 242 + smartComputerRow, smartComputerColumn); 243 + } 244 + 245 + // Execute moves 246 + dumbComputerResult = playMove(dumbComputerRow, dumbComputerColumn, 247 + smartComputerBoard); 248 + smartComputerResult = playMove(smartComputerRow, smartComputerColumn, 249 + dumbComputerBoard); 250 + updateMemoryKlukas(smartComputerRow, smartComputerColumn, 251 + smartComputerResult, smartComputerMemory); 252 + 253 + if (isASunk(dumbComputerResult)) { 254 + numDumbComputerShipsSunk++; 255 + } 256 + if (isASunk(smartComputerResult)) { 257 + numSmartComputerShipsSunk++; 258 + } 259 + 260 + if (numDumbComputerShipsSunk == 5 || numSmartComputerShipsSunk == 5) { 261 + break; 262 + } 263 + } 264 + 265 + if (guardTripped) { 266 + cout << "\n==================================" << endl; 267 + cout << "GUARD TRIPPED after " << gamesRun << " games!" << endl; 268 + cout << "==================================" << endl; 269 + cout << "\nDebug log (last 50 entries):" << endl; 270 + cout << "----------------------------------" << endl; 271 + 272 + vector<string> log = getDebugLog(); 273 + int start = max(0, (int)log.size() - 50); 274 + for (int i = start; i < (int)log.size(); i++) { 275 + cout << log[i] << endl; 276 + } 277 + 278 + return 0; 279 + } 280 + 281 + if (gamesRun % 100 == 0) { 282 + cout << "Completed " << gamesRun << " games..." << endl; 283 + } 284 + } 285 + } 286 + 287 + if (!benchmark) { 288 + setDebugMode(true); 289 + welcome(true); 290 + verbose = true; // Always show moves in interactive mode 291 + } else { 292 + setDebugMode(false); 293 + 294 + // Determine number of threads (use hardware concurrency) 295 + unsigned int numThreads = thread::hardware_concurrency(); 296 + if (numThreads == 0) numThreads = 4; // Fallback if detection fails 297 + 298 + cout << "Running " << numGames << " games on " << numThreads << " threads..." << endl; 299 + 300 + // Progress tracking 301 + atomic<int> gamesCompleted{0}; 302 + 303 + // Launch threads 304 + vector<thread> threads; 305 + int gamesPerThread = numGames / numThreads; 306 + int remainder = numGames % numThreads; 307 + 308 + int startGame = 0; 309 + for (unsigned int t = 0; t < numThreads; t++) { 310 + int endGame = startGame + gamesPerThread + (t < (unsigned)remainder ? 1 : 0); 311 + unsigned int threadSeed = time(NULL) + t; // Unique seed per thread 312 + 313 + threads.emplace_back(runGames, startGame, endGame, ref(stats), 314 + logLosses, ref(gamesCompleted), numGames, threadSeed); 315 + startGame = endGame; 316 + } 317 + 318 + // Progress monitoring thread 319 + thread progressThread([&]() { 320 + int lastReported = 0; 321 + int interval; 322 + if (numGames >= 10000) interval = 1000; 323 + else if (numGames >= 1000) interval = 100; 324 + else if (numGames >= 100) interval = 10; 325 + else interval = numGames / 5; 326 + 327 + while (gamesCompleted.load() < numGames) { 328 + this_thread::sleep_for(chrono::milliseconds(100)); 329 + int completed = gamesCompleted.load(); 330 + if (interval > 0 && completed >= lastReported + interval) { 331 + cout << "Completed " << completed << " games..." << endl; 332 + lastReported = (completed / interval) * interval; 333 + } 334 + } 335 + }); 336 + 337 + // Wait for all game threads to complete 338 + for (auto &t : threads) { 339 + t.join(); 340 + } 341 + 342 + // Stop progress thread 343 + progressThread.join(); 344 + } 345 + 346 + if (!benchmark) { 347 + // Single game mode (existing code) 348 + Board dumbComputerBoard, smartComputerBoard; 349 + ComputerMemory smartComputerMemory; 350 + 351 + string dumbComputerMove, smartComputerMove; 352 + int numDumbComputerShipsSunk = 0; 353 + int numSmartComputerShipsSunk = 0; 354 + int dumbComputerRow, dumbComputerColumn; 355 + int smartComputerRow, smartComputerColumn; 356 + int checkValue, dumbComputerResult, smartComputerResult; 357 + 358 + initializeBoard(dumbComputerBoard); 359 + initializeBoard(smartComputerBoard); 360 + initMemoryKlukas(smartComputerMemory); 361 + 362 + while (true) { 363 + if (verbose) { 364 + clearTheScreen(); 365 + cout << "Dumb Computer Board:" << endl; 366 + displayBoard(1, 5, HUMAN, dumbComputerBoard); 367 + cout << "Smart Computer Board:" << endl; 368 + displayBoard(1, 40, HUMAN, smartComputerBoard); 369 + } 370 + 371 + // Dumb computer move 372 + dumbComputerMove = randomMove(); 373 + checkValue = checkMove(dumbComputerMove, smartComputerBoard, 374 + dumbComputerRow, dumbComputerColumn); 375 + 376 + while (checkValue != VALID_MOVE) { 377 + dumbComputerMove = randomMove(); 378 + checkValue = checkMove(dumbComputerMove, smartComputerBoard, 379 + dumbComputerRow, dumbComputerColumn); 380 + } 381 + 382 + // Smart computer move 383 + smartComputerMove = smartMoveKlukas(smartComputerMemory); 384 + int checkResult = checkMove(smartComputerMove, dumbComputerBoard, 385 + smartComputerRow, smartComputerColumn); 386 + 387 + while (checkResult != VALID_MOVE) { 388 + if (verbose) { 389 + debug("INVALID! Using random instead", 0, 0); 390 + } 391 + smartComputerMove = randomMove(); 392 + checkResult = checkMove(smartComputerMove, dumbComputerBoard, 393 + smartComputerRow, smartComputerColumn); 394 + } 395 + 396 + // Execute moves 397 + dumbComputerResult = playMove(dumbComputerRow, dumbComputerColumn, 398 + smartComputerBoard); 399 + smartComputerResult = playMove(smartComputerRow, smartComputerColumn, 400 + dumbComputerBoard); 401 + updateMemoryKlukas(smartComputerRow, smartComputerColumn, 402 + smartComputerResult, smartComputerMemory); 403 + 404 + if (verbose) { 405 + clearTheScreen(); 406 + cout << "Dumb Computer Board:" << endl; 407 + displayBoard(1, 5, HUMAN, dumbComputerBoard); 408 + cout << "Smart Computer Board:" << endl; 409 + displayBoard(1, 40, HUMAN, smartComputerBoard); 410 + 411 + writeMessage(15, 0, "The dumb computer chooses: " + dumbComputerMove); 412 + writeMessage(16, 0, "The smart computer chooses: " + smartComputerMove); 413 + 414 + writeResult(18, 0, dumbComputerResult, COMPUTER); 415 + writeResult(19, 0, smartComputerResult, HUMAN); 416 + 417 + // Delay so the game is watchable 418 + this_thread::sleep_for(chrono::milliseconds(50)); 419 + } 420 + 421 + if (isASunk(dumbComputerResult)) { 422 + numDumbComputerShipsSunk++; 423 + } 424 + if (isASunk(smartComputerResult)) { 425 + numSmartComputerShipsSunk++; 426 + } 427 + 428 + if (numDumbComputerShipsSunk == 5 || numSmartComputerShipsSunk == 5) { 429 + break; 430 + } 431 + } 432 + 433 + cout << "\nFinal Dumb Computer Board:" << endl; 434 + displayBoard(1, 5, HUMAN, dumbComputerBoard); 435 + cout << "Final Smart Computer Board:" << endl; 436 + displayBoard(1, 40, HUMAN, smartComputerBoard); 437 + 438 + if (numDumbComputerShipsSunk == 5 && numSmartComputerShipsSunk == 5) { 439 + writeMessage(21, 1, "The game is a tie."); 440 + } else if (numDumbComputerShipsSunk == 5) { 441 + writeMessage(21, 1, "Amazing, the dumb computer won."); 442 + } else { 443 + writeMessage(21, 1, "Smart AI won! As it should."); 444 + } 445 + } 446 + 447 + if (benchmark) { 448 + printStats(stats, numGames); 449 + } 450 + 451 + return 0; 452 + }
+97
battleship-engine/src/battleship.h
··· 1 + #ifndef BATTLESHIP_H 2 + #define BATTLESHIP_H 3 + 4 + // Author: Keith Shomper 5 + // Date: 24 Oct 03 6 + // Purpose: Header file for implementing a text-based battleship game 7 + // Updated: 14 Nov 08 to align class types with Cedarville standard 8 + // Updated: 3 Apr 14 to use struct rather than class for the defined types 9 + // Updated: 5 Nov 15 to introduce new functions to replace the ISAxxx macros 10 + // and also the SHIPNUM and DEBUGOUT macros 11 + 12 + // include files for implementing battleship 13 + #include <iostream> 14 + #include <cstdlib> 15 + #include <time.h> 16 + #include <curses.h> 17 + #include "kasbs.h" 18 + 19 + using namespace std; 20 + 21 + // use these constants to indicate if the player is a human or a computer 22 + // battleship board size is 10x10 grid (defined in kasbs.h) 23 + 24 + // data structure for position 25 + struct Position { 26 + int startRow; // ship's initial row 27 + int startCol; // ship's initial column 28 + int orient; // indicates whether the ship is running across 29 + // or up and down 30 + }; 31 + 32 + // data structure for ship 33 + struct Ship { 34 + Position pos; // where the ship is on the board 35 + int size; // number of hits required to sink the ship 36 + int hitsToSink; // number of hits remaining before the ship is sunk 37 + char marker; // the ASCII marker used to denote the ship on the 38 + // board 39 + }; 40 + 41 + // a game board is made up of a 10x10 playing grid and the ships 42 + struct Board { 43 + char grid[BOARDSIZE][BOARDSIZE]; 44 + Ship s[6]; // NOTE: the first (zeroth) position is left empty 45 + }; 46 + 47 + // use these constants for designating to which player we are referring 48 + const int HUMAN = 0; 49 + const int COMPUTER = 1; 50 + 51 + // use these constants for deciding whether or not the user gave a proper move 52 + const int VALID_MOVE = 0; 53 + const int ILLEGAL_FORMAT = 1; 54 + const int REUSED_MOVE = 2; 55 + 56 + // functions for screen control and I/O 57 + void welcome(bool debug = false, bool pf = false); 58 + void clearTheLine(int x); 59 + void clearTheScreen(void); 60 + void pauseForEnter(void); 61 + string getResponse(int x, int y, string prompt); 62 + void writeMessage(int x, int y, string message); 63 + void writeResult(int x, int y, int result, int playerType); 64 + void displayBoard(int x, int y, int playerType, const Board &gameBoard); 65 + 66 + // functions to control the board situation 67 + void initializeBoard(Board &gameBoard, bool file = false); 68 + int playMove(int row, int col, Board &gameBoard); 69 + 70 + // function to tell what happened in the last play_move() command 71 + bool isAMiss(int playMoveResult); 72 + bool isAHit (int playMoveResult); 73 + bool isASunk(int playMoveResult); // formerly named isItSunk() 74 + int isShip (int playMoveResult); 75 + 76 + // misc game functions 77 + string randomMove(void); 78 + int checkMove(string move, const Board &gameBoard, int &row, int &col); 79 + void debug(string s, int x = 22, int y = 1); 80 + string numToString(int x); 81 + 82 + #ifdef BATTLESHIP_BACKWARD_COMPATIBILITY 83 + 84 + // former function signatures 85 + void debug(int x, int y, string s); 86 + bool isItSunk(int playMoveResult); 87 + 88 + // a debug macro 89 + #ifdef DEBUG 90 + #define DEBUGOUT(str) debug (22, 1, (str)); 91 + #else 92 + #define DEBUGOUT(str) 93 + #endif // DEBUG 94 + 95 + #endif // BATTLESHIP_BACKWARD_COMPATABILITY 96 + 97 + #endif // BATTLESHIP_H
+317
battleship-engine/src/battleship_light.cpp
··· 1 + #include "battleship_light.h" 2 + #include <sstream> 3 + #include <cctype> 4 + #include <vector> 5 + #include <mutex> 6 + 7 + // Global flag for debug output 8 + static bool g_debugEnabled = false; 9 + static bool g_guardTripped = false; 10 + static vector<string> g_debugLog; 11 + static mutex g_debugLogMutex; 12 + static const size_t MAX_DEBUG_LOG_SIZE = 1000; 13 + 14 + void setDebugMode(bool enabled) { 15 + g_debugEnabled = enabled; 16 + } 17 + 18 + bool getGuardTripped() { 19 + return g_guardTripped; 20 + } 21 + 22 + void resetGuardTripped() { 23 + g_guardTripped = false; 24 + lock_guard<mutex> lock(g_debugLogMutex); 25 + g_debugLog.clear(); 26 + } 27 + 28 + vector<string> getDebugLog() { 29 + lock_guard<mutex> lock(g_debugLogMutex); 30 + return g_debugLog; 31 + } 32 + 33 + void welcome(bool debug) { 34 + clearTheScreen(); 35 + cout << "========================================" << endl; 36 + cout << " BATTLESHIP - Lightweight" << endl; 37 + cout << "========================================" << endl; 38 + if (debug) { 39 + cout << "Debug mode enabled" << endl; 40 + } 41 + cout << endl; 42 + } 43 + 44 + void clearTheScreen() { 45 + // Simple cross-platform clear 46 + cout << "\033[2J\033[1;1H"; 47 + } 48 + 49 + void pauseForEnter() { 50 + cout << "Press Enter to continue..."; 51 + cin.ignore(); 52 + cin.get(); 53 + } 54 + 55 + void writeMessage(int x, int y, string message) { 56 + cout << message << endl; 57 + } 58 + 59 + void writeResult(int x, int y, int result, int playerType) { 60 + string player = (playerType == HUMAN) ? "Player" : "Computer"; 61 + 62 + if (isASunk(result)) { 63 + int shipNum = isShip(result); 64 + char shipName; 65 + switch(shipNum) { 66 + case AC: shipName = 'A'; break; 67 + case BS: shipName = 'B'; break; 68 + case CR: shipName = 'C'; break; 69 + case SB: shipName = 'S'; break; 70 + case DS: shipName = 'D'; break; 71 + default: shipName = '?'; break; 72 + } 73 + cout << player << " SUNK a ship (" << shipName << ")!" << endl; 74 + } else if (isAHit(result)) { 75 + cout << player << " HIT!" << endl; 76 + } else { 77 + cout << player << " MISS" << endl; 78 + } 79 + } 80 + 81 + void displayBoard(int x, int y, int playerType, const Board &gameBoard) { 82 + cout << " "; 83 + for (int col = 0; col < BOARDSIZE; col++) { 84 + cout << (col + 1) << " "; 85 + } 86 + cout << endl; 87 + 88 + for (int row = 0; row < BOARDSIZE; row++) { 89 + cout << char('A' + row) << " "; 90 + for (int col = 0; col < BOARDSIZE; col++) { 91 + char cell = gameBoard.grid[row][col]; 92 + 93 + // Hide ships if showing to computer player 94 + if (playerType == COMPUTER) { 95 + if (cell != HIT_MARKER && cell != MISS_MARKER && cell != SUNK_MARKER) { 96 + cell = EMPTY_MARKER; 97 + } 98 + } 99 + 100 + cout << cell << " "; 101 + } 102 + cout << endl; 103 + } 104 + cout << endl; 105 + } 106 + 107 + bool placeShip(Board &gameBoard, int shipNum, int row, int col, int orient) { 108 + Ship &ship = gameBoard.s[shipNum]; 109 + 110 + // Check bounds 111 + if (orient == HORZ) { 112 + if (col + ship.size > BOARDSIZE) return false; 113 + } else { 114 + if (row + ship.size > BOARDSIZE) return false; 115 + } 116 + 117 + // Check for collisions 118 + for (int i = 0; i < ship.size; i++) { 119 + int r = (orient == VERT) ? row + i : row; 120 + int c = (orient == HORZ) ? col + i : col; 121 + if (gameBoard.grid[r][c] != EMPTY_MARKER) { 122 + return false; 123 + } 124 + } 125 + 126 + // Place the ship 127 + ship.pos.startRow = row; 128 + ship.pos.startCol = col; 129 + ship.pos.orient = orient; 130 + 131 + for (int i = 0; i < ship.size; i++) { 132 + int r = (orient == VERT) ? row + i : row; 133 + int c = (orient == HORZ) ? col + i : col; 134 + gameBoard.grid[r][c] = ship.marker; 135 + } 136 + 137 + return true; 138 + } 139 + 140 + void initializeBoard(Board &gameBoard, bool file) { 141 + // Initialize grid 142 + for (int i = 0; i < BOARDSIZE; i++) { 143 + for (int j = 0; j < BOARDSIZE; j++) { 144 + gameBoard.grid[i][j] = EMPTY_MARKER; 145 + } 146 + } 147 + 148 + // Initialize ships 149 + gameBoard.s[AC].size = AC_SIZE; 150 + gameBoard.s[AC].hitsToSink = AC_SIZE; 151 + gameBoard.s[AC].marker = AC_MARKER; 152 + 153 + gameBoard.s[BS].size = BS_SIZE; 154 + gameBoard.s[BS].hitsToSink = BS_SIZE; 155 + gameBoard.s[BS].marker = BS_MARKER; 156 + 157 + gameBoard.s[CR].size = CR_SIZE; 158 + gameBoard.s[CR].hitsToSink = CR_SIZE; 159 + gameBoard.s[CR].marker = CR_MARKER; 160 + 161 + gameBoard.s[SB].size = SB_SIZE; 162 + gameBoard.s[SB].hitsToSink = SB_SIZE; 163 + gameBoard.s[SB].marker = SB_MARKER; 164 + 165 + gameBoard.s[DS].size = DS_SIZE; 166 + gameBoard.s[DS].hitsToSink = DS_SIZE; 167 + gameBoard.s[DS].marker = DS_MARKER; 168 + 169 + // Place ships randomly 170 + for (int shipNum = AC; shipNum <= DS; shipNum++) { 171 + bool placed = false; 172 + while (!placed) { 173 + int row = rand() % BOARDSIZE; 174 + int col = rand() % BOARDSIZE; 175 + int orient = rand() % 2; 176 + placed = placeShip(gameBoard, shipNum, row, col, orient); 177 + } 178 + } 179 + } 180 + 181 + int playMove(int row, int col, Board &gameBoard) { 182 + char cell = gameBoard.grid[row][col]; 183 + 184 + // Already hit 185 + if (cell == HIT_MARKER || cell == MISS_MARKER || cell == SUNK_MARKER) { 186 + return MISS; 187 + } 188 + 189 + // Miss 190 + if (cell == EMPTY_MARKER) { 191 + gameBoard.grid[row][col] = MISS_MARKER; 192 + return MISS; 193 + } 194 + 195 + // Hit a ship 196 + int shipNum = 0; 197 + if (cell == AC_MARKER) shipNum = AC; 198 + else if (cell == BS_MARKER) shipNum = BS; 199 + else if (cell == CR_MARKER) shipNum = CR; 200 + else if (cell == SB_MARKER) shipNum = SB; 201 + else if (cell == DS_MARKER) shipNum = DS; 202 + 203 + if (shipNum == 0) { 204 + gameBoard.grid[row][col] = MISS_MARKER; 205 + return MISS; 206 + } 207 + 208 + Ship &ship = gameBoard.s[shipNum]; 209 + ship.hitsToSink--; 210 + 211 + gameBoard.grid[row][col] = HIT_MARKER; 212 + 213 + if (ship.hitsToSink == 0) { 214 + // Mark all parts as sunk 215 + for (int i = 0; i < ship.size; i++) { 216 + int r = (ship.pos.orient == VERT) ? ship.pos.startRow + i : ship.pos.startRow; 217 + int c = (ship.pos.orient == HORZ) ? ship.pos.startCol + i : ship.pos.startCol; 218 + gameBoard.grid[r][c] = SUNK_MARKER; 219 + } 220 + return SUNK | shipNum; 221 + } 222 + 223 + return HIT | shipNum; 224 + } 225 + 226 + bool isAMiss(int playMoveResult) { 227 + return !(playMoveResult & HIT); 228 + } 229 + 230 + bool isAHit(int playMoveResult) { 231 + return (playMoveResult & HIT) != 0; 232 + } 233 + 234 + bool isASunk(int playMoveResult) { 235 + return (playMoveResult & SUNK) != 0; 236 + } 237 + 238 + int isShip(int playMoveResult) { 239 + return playMoveResult & SHIP; 240 + } 241 + 242 + string randomMove() { 243 + int row = rand() % BOARDSIZE; 244 + int col = rand() % BOARDSIZE; 245 + 246 + char letter = 'A' + row; 247 + return string(1, letter) + " " + to_string(col + 1); 248 + } 249 + 250 + int checkMove(string move, const Board &gameBoard, int &row, int &col) { 251 + // Trim whitespace 252 + move.erase(0, move.find_first_not_of(" \t\n\r")); 253 + move.erase(move.find_last_not_of(" \t\n\r") + 1); 254 + 255 + if (move.empty()) { 256 + return ILLEGAL_FORMAT; 257 + } 258 + 259 + // Parse format: "A 5" or "A5" 260 + char letter = toupper(move[0]); 261 + if (letter < 'A' || letter > 'J') { 262 + return ILLEGAL_FORMAT; 263 + } 264 + 265 + row = letter - 'A'; 266 + 267 + // Extract number 268 + string numStr = move.substr(1); 269 + numStr.erase(0, numStr.find_first_not_of(" \t")); 270 + 271 + if (numStr.empty()) { 272 + return ILLEGAL_FORMAT; 273 + } 274 + 275 + try { 276 + col = stoi(numStr) - 1; 277 + } catch (...) { 278 + return ILLEGAL_FORMAT; 279 + } 280 + 281 + if (col < 0 || col >= BOARDSIZE) { 282 + return ILLEGAL_FORMAT; 283 + } 284 + 285 + // Check if already used 286 + char cell = gameBoard.grid[row][col]; 287 + if (cell == HIT_MARKER || cell == MISS_MARKER || cell == SUNK_MARKER) { 288 + return REUSED_MOVE; 289 + } 290 + 291 + return VALID_MOVE; 292 + } 293 + 294 + void debug(string s, int x, int y) { 295 + // Only accumulate logs if debug mode is enabled or we need to track guards 296 + // This prevents memory bloat during benchmarks 297 + if (g_debugEnabled || s.find("*** GUARD TRIPPED ***") != string::npos) { 298 + lock_guard<mutex> lock(g_debugLogMutex); 299 + g_debugLog.push_back(s); 300 + 301 + // Limit log size to prevent unbounded growth 302 + if (g_debugLog.size() > MAX_DEBUG_LOG_SIZE) { 303 + g_debugLog.erase(g_debugLog.begin(), 304 + g_debugLog.begin() + (g_debugLog.size() - MAX_DEBUG_LOG_SIZE)); 305 + } 306 + } 307 + 308 + if (g_debugEnabled) { 309 + cout << "[DEBUG] " << s << endl; 310 + } 311 + } 312 + 313 + string numToString(int x) { 314 + stringstream ss; 315 + ss << x; 316 + return ss.str(); 317 + }
+65
battleship-engine/src/battleship_light.h
··· 1 + #ifndef BATTLESHIP_LIGHT_H 2 + #define BATTLESHIP_LIGHT_H 3 + 4 + #include <iostream> 5 + #include <cstdlib> 6 + #include <ctime> 7 + #include <string> 8 + #include <vector> 9 + 10 + // Use the same constants and types as the normal version 11 + #include "kasbs.h" 12 + 13 + using namespace std; 14 + 15 + // Player types (not in kasbs.h) 16 + const int HUMAN = 0; 17 + const int COMPUTER = 1; 18 + 19 + // Move validation (not in kasbs.h) 20 + const int VALID_MOVE = 0; 21 + const int ILLEGAL_FORMAT = 1; 22 + const int REUSED_MOVE = 2; 23 + 24 + // Position, Ship, and Board are compatible with normal version 25 + struct Position { 26 + int startRow; 27 + int startCol; 28 + int orient; 29 + }; 30 + 31 + struct Ship { 32 + Position pos; 33 + int size; 34 + int hitsToSink; 35 + char marker; 36 + }; 37 + 38 + struct Board { 39 + char grid[BOARDSIZE][BOARDSIZE]; 40 + Ship s[6]; // index 0 unused 41 + }; 42 + 43 + // Core functions - lightweight implementations 44 + void setDebugMode(bool enabled); 45 + bool getGuardTripped(); 46 + void resetGuardTripped(); 47 + vector<string> getDebugLog(); 48 + void welcome(bool debug = false); 49 + void clearTheScreen(); 50 + void pauseForEnter(); 51 + void writeMessage(int x, int y, string message); 52 + void writeResult(int x, int y, int result, int playerType); 53 + void displayBoard(int x, int y, int playerType, const Board &gameBoard); 54 + void initializeBoard(Board &gameBoard, bool file = false); 55 + int playMove(int row, int col, Board &gameBoard); 56 + bool isAMiss(int playMoveResult); 57 + bool isAHit(int playMoveResult); 58 + bool isASunk(int playMoveResult); 59 + int isShip(int playMoveResult); 60 + string randomMove(); 61 + int checkMove(string move, const Board &gameBoard, int &row, int &col); 62 + void debug(string s, int x = 22, int y = 1); 63 + string numToString(int x); 64 + 65 + #endif // BATTLESHIP_LIGHT_H
+78
battleship-engine/src/kasbs.h
··· 1 + // Author: Keith Shomper 2 + // Date: 24 Oct 03 3 + // Purpose: Type definitions for implementing a text-based battleship game 4 + // Modified:15 Nov 2005 - Moved the class definitions into the file battleship.h 5 + // to make the data structure more visible. 6 + // Modified: 5 Nov 2013 - Moved the ISA macros higher in the file to make them 7 + // more visible. Added ISAMISS macro 8 + // Modified: 5 Nov 2015 - Moved the ISA macros back to a lower section and made 9 + // them conditionally compilable. The purpose of this 10 + // change was to promote their replacement by like-named 11 + // functions, while maintaining backward compatibility. 12 + 13 + // use these constants to indicate whether a ship goes across the grid or up 14 + // and down 15 + 16 + #ifndef BATTLESHIP_TYPE_DEFINITIONS_H 17 + #define BATTLESHIP_TYPE_DEFINITIONS_H 18 + 19 + // Board size 20 + const int BOARDSIZE = 10; 21 + 22 + // use these constants to indicate whether a ship goes across the grid or up 23 + // and down 24 + const int HORZ = 0; 25 + const int VERT = 1; 26 + 27 + // these constants allow use to refer to the ships by numerical values 28 + const int AC = 1; 29 + const int BS = 2; 30 + const int CR = 3; 31 + const int SB = 4; 32 + const int DS = 5; 33 + 34 + // constants for the ship size 35 + const int AC_SIZE = 5; 36 + const int BS_SIZE = 4; 37 + const int CR_SIZE = 3; 38 + const int SB_SIZE = 3; 39 + const int DS_SIZE = 2; 40 + 41 + // markers for keeping track of game play 42 + const char HIT_MARKER = 'H'; 43 + const char MISS_MARKER = '*'; 44 + const char SUNK_MARKER = 'X'; 45 + const char EMPTY_MARKER = ' '; 46 + const char AC_MARKER = 'A'; 47 + const char BS_MARKER = 'B'; 48 + const char CR_MARKER = 'C'; 49 + const char SB_MARKER = 'S'; 50 + const char DS_MARKER = 'D'; 51 + 52 + // TO STUDENTS: It should not be necessary in your assignment for you to use 53 + // the information appearing below this comment 54 + 55 + // color constants for diferent game situations 56 + const int BATTLE_WHITE = 1; 57 + const int BATTLE_GREEN = 2; 58 + const int BATTLE_YELLOW = 3; 59 + const int BATTLE_RED = 4; 60 + 61 + // use these constants to set the return value in playMove(), they allow us 62 + // to send back mutiple pieces of information about the result of the move in 63 + // a single integer variable 64 + const int MISS = 0; 65 + const int SHIP = 7; 66 + const int HIT = 8; 67 + const int SUNK = 16; 68 + 69 + #ifdef BATTLESHIP_BACKWARD_COMPATIBILITY 70 + // these macros use the MISS, HIT, etc. constants below to determine the 71 + // result of the move 72 + #define ISAMISS(parm) (!((parm) & HIT)) 73 + #define ISAHIT(parm) ((parm) & HIT) 74 + #define ISASUNK(parm) ((parm) & SUNK) 75 + #define SHIPNUM(parm) ((parm) & SHIP) 76 + #endif // BATTLESHIP_BACKWARD_COMPATIBILITY 77 + 78 + #endif // BATTLESHIP_TYPE_DEFINITIONS_H
+38
battleship-engine/src/memory.h
··· 1 + #ifndef MEMORY_H 2 + #define MEMORY_H 3 + 4 + #include "kasbs.h" 5 + 6 + using namespace std; 7 + 8 + #define RANDOM 1 9 + #define SEARCH 2 10 + #define DESTROY 3 11 + 12 + #define NONE 0 13 + #define NORTH 1 14 + #define SOUTH 2 15 + #define EAST 3 16 + #define WEST 4 17 + 18 + struct ComputerMemory { 19 + int hitRow, hitCol; 20 + int hitShip; 21 + int fireDir; 22 + int fireDist; 23 + int lastResult; 24 + int mode; 25 + char grid[BOARDSIZE][BOARDSIZE]; 26 + 27 + // optional attributes for students wanting to keep track of hits on 28 + // multiple ships 29 + int depth; 30 + int hitRows[5], hitCols[5]; 31 + int hitShips[5]; 32 + int fireDirs[5]; 33 + int fireDists[5]; 34 + int lastResults[5]; 35 + int modes[5]; 36 + }; 37 + 38 + #endif // MEMORY_H
+116
battleship-engine/src/tournament_battle.cpp
··· 1 + // Tournament battle runner - runs matches between two AI implementations 2 + // Outputs results in parseable format 3 + 4 + #include "battleship_light.h" 5 + #include "memory.h" 6 + #include <iostream> 7 + #include <cstdlib> 8 + #include <ctime> 9 + 10 + using namespace std; 11 + 12 + // Function pointers for the two AIs 13 + void (*initMemory1)(ComputerMemory&) = nullptr; 14 + string (*smartMove1)(const ComputerMemory&) = nullptr; 15 + void (*updateMemory1)(int, int, int, ComputerMemory&) = nullptr; 16 + 17 + void (*initMemory2)(ComputerMemory&) = nullptr; 18 + string (*smartMove2)(const ComputerMemory&) = nullptr; 19 + void (*updateMemory2)(int, int, int, ComputerMemory&) = nullptr; 20 + 21 + struct MatchResult { 22 + int player1Wins = 0; 23 + int player2Wins = 0; 24 + int ties = 0; 25 + int totalMoves = 0; 26 + }; 27 + 28 + MatchResult runMatch(int numGames) { 29 + MatchResult result; 30 + srand(time(NULL)); 31 + 32 + for (int game = 0; game < numGames; game++) { 33 + Board board1, board2; 34 + ComputerMemory memory1, memory2; 35 + 36 + initializeBoard(board1); 37 + initializeBoard(board2); 38 + initMemory1(memory1); 39 + initMemory2(memory2); 40 + 41 + int shipsSunk1 = 0; 42 + int shipsSunk2 = 0; 43 + int moveCount = 0; 44 + 45 + while (true) { 46 + moveCount++; 47 + 48 + // Player 1 move 49 + string move1 = smartMove1(memory1); 50 + int row1, col1; 51 + int check1 = checkMove(move1, board2, row1, col1); 52 + while (check1 != VALID_MOVE) { 53 + move1 = randomMove(); 54 + check1 = checkMove(move1, board2, row1, col1); 55 + } 56 + 57 + // Player 2 move 58 + string move2 = smartMove2(memory2); 59 + int row2, col2; 60 + int check2 = checkMove(move2, board1, row2, col2); 61 + while (check2 != VALID_MOVE) { 62 + move2 = randomMove(); 63 + check2 = checkMove(move2, board1, row2, col2); 64 + } 65 + 66 + // Execute moves 67 + int result1 = playMove(row1, col1, board2); 68 + int result2 = playMove(row2, col2, board1); 69 + 70 + updateMemory1(row1, col1, result1, memory1); 71 + updateMemory2(row2, col2, result2, memory2); 72 + 73 + if (isASunk(result1)) shipsSunk1++; 74 + if (isASunk(result2)) shipsSunk2++; 75 + 76 + if (shipsSunk1 == 5 || shipsSunk2 == 5) { 77 + break; 78 + } 79 + } 80 + 81 + result.totalMoves += moveCount; 82 + 83 + if (shipsSunk1 == 5 && shipsSunk2 == 5) { 84 + result.ties++; 85 + } else if (shipsSunk1 == 5) { 86 + result.player1Wins++; 87 + } else { 88 + result.player2Wins++; 89 + } 90 + } 91 + 92 + return result; 93 + } 94 + 95 + int main(int argc, char* argv[]) { 96 + if (argc < 2) { 97 + cerr << "Usage: " << argv[0] << " <num_games>" << endl; 98 + return 1; 99 + } 100 + 101 + int numGames = atoi(argv[1]); 102 + if (numGames <= 0) numGames = 10; 103 + 104 + setDebugMode(false); 105 + 106 + MatchResult result = runMatch(numGames); 107 + 108 + // Output in parseable format 109 + cout << "PLAYER1_WINS=" << result.player1Wins << endl; 110 + cout << "PLAYER2_WINS=" << result.player2Wins << endl; 111 + cout << "TIES=" << result.ties << endl; 112 + cout << "TOTAL_MOVES=" << result.totalMoves << endl; 113 + cout << "AVG_MOVES=" << (result.totalMoves / numGames) << endl; 114 + 115 + return 0; 116 + }
+100 -18
database.go
··· 37 37 username TEXT NOT NULL, 38 38 filename TEXT NOT NULL, 39 39 upload_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 40 - status TEXT DEFAULT 'pending' 40 + status TEXT DEFAULT 'pending', 41 + is_active BOOLEAN DEFAULT 1 41 42 ); 42 43 43 - CREATE TABLE IF NOT EXISTS results ( 44 + CREATE TABLE IF NOT EXISTS matches ( 44 45 id INTEGER PRIMARY KEY AUTOINCREMENT, 45 - submission_id INTEGER, 46 - opponent TEXT, 47 - result TEXT, -- win, loss, tie 48 - moves INTEGER, 46 + player1_id INTEGER, 47 + player2_id INTEGER, 48 + winner_id INTEGER, 49 + player1_moves INTEGER, 50 + player2_moves INTEGER, 49 51 timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 50 - FOREIGN KEY (submission_id) REFERENCES submissions(id) 52 + FOREIGN KEY (player1_id) REFERENCES submissions(id), 53 + FOREIGN KEY (player2_id) REFERENCES submissions(id), 54 + FOREIGN KEY (winner_id) REFERENCES submissions(id) 51 55 ); 52 56 53 - CREATE INDEX IF NOT EXISTS idx_results_submission ON results(submission_id); 57 + CREATE INDEX IF NOT EXISTS idx_matches_player1 ON matches(player1_id); 58 + CREATE INDEX IF NOT EXISTS idx_matches_player2 ON matches(player2_id); 54 59 CREATE INDEX IF NOT EXISTS idx_submissions_username ON submissions(username); 55 60 CREATE INDEX IF NOT EXISTS idx_submissions_status ON submissions(status); 61 + CREATE INDEX IF NOT EXISTS idx_submissions_active ON submissions(is_active); 56 62 ` 57 63 58 64 _, err = db.Exec(schema) ··· 63 69 query := ` 64 70 SELECT 65 71 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 72 + COUNT(CASE WHEN m.winner_id = s.id THEN 1 END) as wins, 73 + COUNT(CASE WHEN (m.player1_id = s.id OR m.player2_id = s.id) AND m.winner_id != s.id THEN 1 END) as losses, 74 + AVG(CASE WHEN m.player1_id = s.id THEN m.player1_moves ELSE m.player2_moves END) as avg_moves, 75 + MAX(m.timestamp) as last_played 70 76 FROM submissions s 71 - JOIN results r ON s.id = r.submission_id 77 + LEFT JOIN matches m ON (m.player1_id = s.id OR m.player2_id = s.id) 78 + WHERE s.is_active = 1 72 79 GROUP BY s.username 80 + HAVING COUNT(m.id) > 0 73 81 ORDER BY wins DESC, losses ASC, avg_moves ASC 74 82 LIMIT ? 75 83 ` ··· 99 107 } 100 108 101 109 func addSubmission(username, filename string) (int64, error) { 110 + // Mark old submission as inactive 111 + _, err := globalDB.Exec( 112 + "UPDATE submissions SET is_active = 0 WHERE username = ?", 113 + username, 114 + ) 115 + if err != nil { 116 + return 0, err 117 + } 118 + 119 + // Insert new submission 102 120 result, err := globalDB.Exec( 103 - "INSERT INTO submissions (username, filename) VALUES (?, ?)", 121 + "INSERT INTO submissions (username, filename, is_active) VALUES (?, ?, 1)", 104 122 username, filename, 105 123 ) 106 124 if err != nil { ··· 109 127 return result.LastInsertId() 110 128 } 111 129 112 - func addResult(submissionID int, opponent, result string, moves int) error { 130 + func addMatch(player1ID, player2ID, winnerID, player1Moves, player2Moves int) error { 113 131 _, err := globalDB.Exec( 114 - "INSERT INTO results (submission_id, opponent, result, moves) VALUES (?, ?, ?, ?)", 115 - submissionID, opponent, result, moves, 132 + "INSERT INTO matches (player1_id, player2_id, winner_id, player1_moves, player2_moves) VALUES (?, ?, ?, ?, ?)", 133 + player1ID, player2ID, winnerID, player1Moves, player2Moves, 116 134 ) 117 135 return err 118 136 } ··· 124 142 125 143 func getPendingSubmissions() ([]Submission, error) { 126 144 rows, err := globalDB.Query( 127 - "SELECT id, username, filename, upload_time, status FROM submissions WHERE status = 'pending' ORDER BY upload_time", 145 + "SELECT id, username, filename, upload_time, status FROM submissions WHERE status = 'pending' AND is_active = 1 ORDER BY upload_time", 146 + ) 147 + if err != nil { 148 + return nil, err 149 + } 150 + defer rows.Close() 151 + 152 + var submissions []Submission 153 + for rows.Next() { 154 + var s Submission 155 + err := rows.Scan(&s.ID, &s.Username, &s.Filename, &s.UploadTime, &s.Status) 156 + if err != nil { 157 + return nil, err 158 + } 159 + submissions = append(submissions, s) 160 + } 161 + 162 + return submissions, rows.Err() 163 + } 164 + 165 + func getActiveSubmissions() ([]Submission, error) { 166 + rows, err := globalDB.Query( 167 + "SELECT id, username, filename, upload_time, status FROM submissions WHERE is_active = 1 AND status = 'completed' ORDER BY username", 128 168 ) 129 169 if err != nil { 130 170 return nil, err ··· 166 206 167 207 return submissions, rows.Err() 168 208 } 209 + 210 + 211 + type MatchResult struct { 212 + Player1Username string 213 + Player2Username string 214 + WinnerUsername string 215 + AvgMoves int 216 + } 217 + 218 + func getAllMatches() ([]MatchResult, error) { 219 + query := ` 220 + SELECT 221 + s1.username as player1, 222 + s2.username as player2, 223 + sw.username as winner, 224 + m.player1_moves as avg_moves 225 + FROM matches m 226 + JOIN submissions s1 ON m.player1_id = s1.id 227 + JOIN submissions s2 ON m.player2_id = s2.id 228 + JOIN submissions sw ON m.winner_id = sw.id 229 + WHERE s1.is_active = 1 AND s2.is_active = 1 230 + ORDER BY m.timestamp DESC 231 + ` 232 + 233 + rows, err := globalDB.Query(query) 234 + if err != nil { 235 + return nil, err 236 + } 237 + defer rows.Close() 238 + 239 + var matches []MatchResult 240 + for rows.Next() { 241 + var m MatchResult 242 + err := rows.Scan(&m.Player1Username, &m.Player2Username, &m.WinnerUsername, &m.AvgMoves) 243 + if err != nil { 244 + return nil, err 245 + } 246 + matches = append(matches, m) 247 + } 248 + 249 + return matches, rows.Err() 250 + }
+67 -2
model.go
··· 15 15 height int 16 16 submissions []Submission 17 17 leaderboard []LeaderboardEntry 18 + matches []MatchResult 18 19 } 19 20 20 21 func initialModel(username string, width, height int) model { ··· 28 29 } 29 30 30 31 func (m model) Init() tea.Cmd { 31 - return tea.Batch(loadLeaderboard, loadSubmissions(m.username), tickCmd()) 32 + return tea.Batch(loadLeaderboard, loadSubmissions(m.username), loadMatches, tickCmd()) 32 33 } 33 34 34 35 func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { ··· 45 46 m.leaderboard = msg.entries 46 47 case submissionsMsg: 47 48 m.submissions = msg.submissions 49 + case matchesMsg: 50 + m.matches = msg.matches 48 51 case tickMsg: 49 - return m, tea.Batch(loadLeaderboard, loadSubmissions(m.username), tickCmd()) 52 + return m, tea.Batch(loadLeaderboard, loadSubmissions(m.username), loadMatches, tickCmd()) 50 53 } 51 54 return m, nil 52 55 } ··· 72 75 b.WriteString("\n") 73 76 } 74 77 78 + // Show bracket-style matches 79 + if len(m.matches) > 0 { 80 + b.WriteString(renderBracket(m.matches)) 81 + b.WriteString("\n") 82 + } 83 + 75 84 // Show leaderboard if loaded 76 85 if len(m.leaderboard) > 0 { 77 86 b.WriteString(renderLeaderboard(m.leaderboard)) ··· 110 119 } 111 120 } 112 121 122 + type matchesMsg struct { 123 + matches []MatchResult 124 + } 125 + 126 + func loadMatches() tea.Msg { 127 + matches, err := getAllMatches() 128 + if err != nil { 129 + return matchesMsg{matches: nil} 130 + } 131 + return matchesMsg{matches: matches} 132 + } 133 + 113 134 type tickMsg time.Time 114 135 115 136 func tickCmd() tea.Cmd { ··· 204 225 205 226 return b.String() 206 227 } 228 + 229 + func renderBracket(matches []MatchResult) string { 230 + var b strings.Builder 231 + b.WriteString(lipgloss.NewStyle().Bold(true).Render("⚔️ Recent Matches") + "\n\n") 232 + 233 + if len(matches) == 0 { 234 + return b.String() 235 + } 236 + 237 + // Show most recent matches (up to 10) 238 + displayCount := len(matches) 239 + if displayCount > 10 { 240 + displayCount = 10 241 + } 242 + 243 + for i := 0; i < displayCount; i++ { 244 + match := matches[i] 245 + 246 + // Determine styling based on winner 247 + player1Style := lipgloss.NewStyle() 248 + player2Style := lipgloss.NewStyle() 249 + 250 + if match.WinnerUsername == match.Player1Username { 251 + player1Style = player1Style.Foreground(lipgloss.Color("green")).Bold(true) 252 + player2Style = player2Style.Foreground(lipgloss.Color("240")) 253 + } else { 254 + player2Style = player2Style.Foreground(lipgloss.Color("green")).Bold(true) 255 + player1Style = player1Style.Foreground(lipgloss.Color("240")) 256 + } 257 + 258 + // Format: [Player1] ──vs── [Player2] → Winner (avg moves) 259 + player1Str := player1Style.Render(fmt.Sprintf("%-15s", match.Player1Username)) 260 + vsStr := lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Render(" ──vs── ") 261 + player2Str := player2Style.Render(fmt.Sprintf("%-15s", match.Player2Username)) 262 + 263 + winnerMark := "→" 264 + winnerStr := lipgloss.NewStyle().Foreground(lipgloss.Color("green")).Render( 265 + fmt.Sprintf("%s %s wins (avg %d moves)", winnerMark, match.WinnerUsername, match.AvgMoves)) 266 + 267 + b.WriteString(fmt.Sprintf("%s%s%s %s\n", player1Str, vsStr, player2Str, winnerStr)) 268 + } 269 + 270 + return b.String() 271 + }
+315 -53
runner.go
··· 7 7 "os/exec" 8 8 "path/filepath" 9 9 "regexp" 10 + "strconv" 10 11 "strings" 11 12 ) 12 13 13 - const battleshipRepoPath = "/Users/kierank/code/school/cs1210-battleship" 14 + const enginePath = "./battleship-engine" 14 15 15 16 func processSubmissions() error { 16 17 submissions, err := getPendingSubmissions() ··· 19 20 } 20 21 21 22 for _, sub := range submissions { 22 - log.Printf("Starting test for submission %d: %s by %s", sub.ID, sub.Filename, sub.Username) 23 + log.Printf("Starting compilation for submission %d: %s by %s", sub.ID, sub.Filename, sub.Username) 23 24 24 - if err := testSubmission(sub); err != nil { 25 - log.Printf("Submission %d failed: %v", sub.ID, err) 25 + if err := compileSubmission(sub); err != nil { 26 + log.Printf("Submission %d failed compilation: %v", sub.ID, err) 26 27 updateSubmissionStatus(sub.ID, "failed") 27 28 continue 28 29 } 29 30 30 - log.Printf("Submission %d completed successfully: %s by %s", sub.ID, sub.Filename, sub.Username) 31 + log.Printf("Submission %d compiled successfully: %s by %s", sub.ID, sub.Filename, sub.Username) 31 32 updateSubmissionStatus(sub.ID, "completed") 33 + 34 + // Run tournament matches against all active submissions 35 + go runTournamentMatches(sub) 32 36 } 33 37 34 38 return nil 35 39 } 36 40 37 - func testSubmission(sub Submission) error { 38 - log.Printf("Setting submission %d to testing status", sub.ID) 41 + func generateHeader(filename, prefix string) string { 42 + guard := strings.ToUpper(strings.Replace(filename, ".", "_", -1)) 43 + 44 + // Capitalize first letter of prefix for function names 45 + functionSuffix := strings.ToUpper(prefix[0:1]) + prefix[1:] 46 + 47 + return fmt.Sprintf(`#ifndef %s 48 + #define %s 49 + 50 + #include "memory.h" 51 + #include <string> 52 + 53 + void initMemory%s(ComputerMemory &memory); 54 + std::string smartMove%s(const ComputerMemory &memory); 55 + void updateMemory%s(int row, int col, int result, ComputerMemory &memory); 56 + 57 + #endif 58 + `, guard, guard, functionSuffix, functionSuffix, functionSuffix) 59 + } 60 + 61 + func parseFunctionNames(cppContent string) (string, error) { 62 + // Look for the initMemory function to extract the suffix 63 + re := regexp.MustCompile(`void\s+initMemory(\w+)\s*\(`) 64 + matches := re.FindStringSubmatch(cppContent) 65 + if len(matches) < 2 { 66 + return "", fmt.Errorf("could not find initMemory function") 67 + } 68 + return matches[1], nil 69 + } 70 + 71 + func compileSubmission(sub Submission) error { 39 72 updateSubmissionStatus(sub.ID, "testing") 40 73 41 - // Copy submission to battleship repo 74 + // Extract prefix from filename (memory_functions_XXXXX.cpp -> XXXXX) 75 + re := regexp.MustCompile(`memory_functions_(\w+)\.cpp`) 76 + matches := re.FindStringSubmatch(sub.Filename) 77 + if len(matches) < 2 { 78 + return fmt.Errorf("invalid filename format") 79 + } 80 + prefix := matches[1] 81 + 82 + // Create temporary build directory 83 + buildDir := filepath.Join(enginePath, "build") 84 + os.MkdirAll(buildDir, 0755) 85 + 86 + // Copy submission to engine 42 87 srcPath := filepath.Join(uploadDir, sub.Username, sub.Filename) 43 - dstPath := filepath.Join(battleshipRepoPath, "src", sub.Filename) 44 - 88 + dstPath := filepath.Join(enginePath, "src", sub.Filename) 89 + 45 90 log.Printf("Copying %s to %s", srcPath, dstPath) 46 91 input, err := os.ReadFile(srcPath) 47 92 if err != nil { ··· 51 96 return err 52 97 } 53 98 54 - // Extract student ID from filename (memory_functions_NNNN.cpp) 55 - re := regexp.MustCompile(`memory_functions_(\w+)\.cpp`) 56 - matches := re.FindStringSubmatch(sub.Filename) 57 - if len(matches) < 2 { 58 - return fmt.Errorf("invalid filename format") 99 + // Parse function names from the cpp file 100 + functionSuffix, err := parseFunctionNames(string(input)) 101 + if err != nil { 102 + return fmt.Errorf("failed to parse function names: %v", err) 59 103 } 60 - studentID := matches[1] 104 + 105 + log.Printf("Detected function suffix: %s", functionSuffix) 61 106 62 - // Build the battleship program 63 - buildDir := filepath.Join(battleshipRepoPath, "build") 64 - os.MkdirAll(buildDir, 0755) 107 + // Generate header file with parsed function names 108 + headerFilename := fmt.Sprintf("memory_functions_%s.h", prefix) 109 + headerPath := filepath.Join(enginePath, "src", headerFilename) 110 + headerContent := generateHeader(headerFilename, functionSuffix) 111 + if err := os.WriteFile(headerPath, []byte(headerContent), 0644); err != nil { 112 + return err 113 + } 65 114 66 - log.Printf("Compiling submission %d for student %s", sub.ID, studentID) 67 - // Compile using the light version for testing 68 - cmd := exec.Command("g++", "-std=c++11", "-O3", 69 - "-o", filepath.Join(buildDir, "battle_"+studentID), 70 - filepath.Join(battleshipRepoPath, "src", "battle_light.cpp"), 71 - filepath.Join(battleshipRepoPath, "src", "battleship_light.cpp"), 72 - filepath.Join(battleshipRepoPath, "src", sub.Filename), 115 + log.Printf("Compiling submission %d for %s", sub.ID, prefix) 116 + 117 + // Compile check only (no linking) to validate syntax 118 + cmd := exec.Command("g++", "-std=c++11", "-c", "-O3", 119 + "-I", filepath.Join(enginePath, "src"), 120 + "-o", filepath.Join(buildDir, "ai_"+prefix+".o"), 121 + filepath.Join(enginePath, "src", sub.Filename), 73 122 ) 74 123 output, err := cmd.CombinedOutput() 75 124 if err != nil { 76 125 return fmt.Errorf("compilation failed: %s", output) 77 126 } 78 127 79 - log.Printf("Running benchmark for submission %d (100 games)", sub.ID) 80 - // Run benchmark tests (100 games) 81 - cmd = exec.Command(filepath.Join(buildDir, "battle_"+studentID), "--benchmark", "100") 82 - output, err = cmd.CombinedOutput() 128 + return nil 129 + } 130 + 131 + func runTournamentMatches(newSub Submission) { 132 + // Get all active submissions 133 + activeSubmissions, err := getActiveSubmissions() 83 134 if err != nil { 84 - return fmt.Errorf("benchmark failed: %s", output) 135 + log.Printf("Failed to get active submissions: %v", err) 136 + return 85 137 } 86 138 87 - // Parse results and store in database 88 - log.Printf("Parsing results for submission %d", sub.ID) 89 - results := parseResults(string(output)) 90 - for opponent, result := range results { 91 - addResult(sub.ID, opponent, result.Result, result.Moves) 139 + // Run matches against all other submissions 140 + for _, opponent := range activeSubmissions { 141 + if opponent.ID == newSub.ID { 142 + continue 143 + } 144 + 145 + log.Printf("Running match: %s vs %s (1000 games)", newSub.Username, opponent.Username) 146 + 147 + // Run match (1000 games total) 148 + player1Wins, player2Wins, totalMoves := runHeadToHead(newSub, opponent, 1000) 149 + 150 + // Determine winner 151 + var winnerID int 152 + avgMoves := totalMoves / 1000 153 + 154 + if player1Wins > player2Wins { 155 + winnerID = newSub.ID 156 + log.Printf("Match result: %s wins (%d-%d, avg %d moves)", newSub.Username, player1Wins, player2Wins, avgMoves) 157 + } else if player2Wins > player1Wins { 158 + winnerID = opponent.ID 159 + log.Printf("Match result: %s wins (%d-%d, avg %d moves)", opponent.Username, player2Wins, player1Wins, avgMoves) 160 + } else { 161 + // Tie - coin flip 162 + if totalMoves%2 == 0 { 163 + winnerID = newSub.ID 164 + } else { 165 + winnerID = opponent.ID 166 + } 167 + log.Printf("Match result: Tie %d-%d, winner by coin flip: %d", player1Wins, player2Wins, winnerID) 168 + } 169 + 170 + // Store match result 171 + if err := addMatch(newSub.ID, opponent.ID, winnerID, avgMoves, avgMoves); err != nil { 172 + log.Printf("Failed to store match result: %v", err) 173 + } 92 174 } 175 + } 93 176 94 - return nil 177 + func runHeadToHead(player1, player2 Submission, numGames int) (int, int, int) { 178 + re := regexp.MustCompile(`memory_functions_(\w+)\.cpp`) 179 + matches1 := re.FindStringSubmatch(player1.Filename) 180 + matches2 := re.FindStringSubmatch(player2.Filename) 181 + 182 + if len(matches1) < 2 || len(matches2) < 2 { 183 + return 0, 0, 0 184 + } 185 + 186 + prefix1 := matches1[1] 187 + prefix2 := matches2[1] 188 + 189 + // Read both cpp files to extract function suffixes 190 + cpp1Path := filepath.Join(enginePath, "src", player1.Filename) 191 + cpp2Path := filepath.Join(enginePath, "src", player2.Filename) 192 + 193 + cpp1Content, err := os.ReadFile(cpp1Path) 194 + if err != nil { 195 + log.Printf("Failed to read %s: %v", cpp1Path, err) 196 + return 0, 0, 0 197 + } 198 + 199 + cpp2Content, err := os.ReadFile(cpp2Path) 200 + if err != nil { 201 + log.Printf("Failed to read %s: %v", cpp2Path, err) 202 + return 0, 0, 0 203 + } 204 + 205 + suffix1, err := parseFunctionNames(string(cpp1Content)) 206 + if err != nil { 207 + log.Printf("Failed to parse function names for %s: %v", player1.Filename, err) 208 + return 0, 0, 0 209 + } 210 + 211 + suffix2, err := parseFunctionNames(string(cpp2Content)) 212 + if err != nil { 213 + log.Printf("Failed to parse function names for %s: %v", player2.Filename, err) 214 + return 0, 0, 0 215 + } 216 + 217 + buildDir := filepath.Join(enginePath, "build") 218 + 219 + // Create a combined binary with both AIs 220 + combinedBinary := filepath.Join(buildDir, fmt.Sprintf("match_%s_vs_%s", prefix1, prefix2)) 221 + 222 + // Generate main file that uses both AIs with correct function suffixes 223 + mainContent := generateMatchMain(prefix1, prefix2, suffix1, suffix2) 224 + mainPath := filepath.Join(enginePath, "src", fmt.Sprintf("match_%s_vs_%s.cpp", prefix1, prefix2)) 225 + if err := os.WriteFile(mainPath, []byte(mainContent), 0644); err != nil { 226 + log.Printf("Failed to write match main: %v", err) 227 + return 0, 0, 0 228 + } 229 + 230 + // Compile combined binary 231 + cmd := exec.Command("g++", "-std=c++11", "-O3", 232 + "-o", combinedBinary, 233 + mainPath, 234 + filepath.Join(enginePath, "src", "battleship_light.cpp"), 235 + filepath.Join(enginePath, "src", fmt.Sprintf("memory_functions_%s.cpp", prefix1)), 236 + filepath.Join(enginePath, "src", fmt.Sprintf("memory_functions_%s.cpp", prefix2)), 237 + ) 238 + output, err := cmd.CombinedOutput() 239 + if err != nil { 240 + log.Printf("Failed to compile match binary: %s", output) 241 + return 0, 0, 0 242 + } 243 + 244 + // Run the match 245 + cmd = exec.Command(combinedBinary, strconv.Itoa(numGames)) 246 + output, err = cmd.CombinedOutput() 247 + if err != nil { 248 + log.Printf("Match execution failed: %v", err) 249 + return 0, 0, 0 250 + } 251 + 252 + // Parse results 253 + return parseMatchOutput(string(output)) 95 254 } 96 255 97 - type GameResult struct { 98 - Result string 99 - Moves int 256 + func generateMatchMain(prefix1, prefix2, suffix1, suffix2 string) string { 257 + return fmt.Sprintf(`#include "battleship_light.h" 258 + #include "memory.h" 259 + #include "memory_functions_%s.h" 260 + #include "memory_functions_%s.h" 261 + #include <iostream> 262 + #include <cstdlib> 263 + #include <ctime> 264 + 265 + using namespace std; 266 + 267 + struct MatchResult { 268 + int player1Wins = 0; 269 + int player2Wins = 0; 270 + int ties = 0; 271 + int totalMoves = 0; 272 + }; 273 + 274 + MatchResult runMatch(int numGames) { 275 + MatchResult result; 276 + srand(time(NULL)); 277 + 278 + for (int game = 0; game < numGames; game++) { 279 + Board board1, board2; 280 + ComputerMemory memory1, memory2; 281 + 282 + initializeBoard(board1); 283 + initializeBoard(board2); 284 + initMemory%s(memory1); 285 + initMemory%s(memory2); 286 + 287 + int shipsSunk1 = 0; 288 + int shipsSunk2 = 0; 289 + int moveCount = 0; 290 + 291 + while (true) { 292 + moveCount++; 293 + 294 + // Player 1 move 295 + string move1 = smartMove%s(memory1); 296 + int row1, col1; 297 + int check1 = checkMove(move1, board2, row1, col1); 298 + while (check1 != VALID_MOVE) { 299 + move1 = randomMove(); 300 + check1 = checkMove(move1, board2, row1, col1); 301 + } 302 + 303 + // Player 2 move 304 + string move2 = smartMove%s(memory2); 305 + int row2, col2; 306 + int check2 = checkMove(move2, board1, row2, col2); 307 + while (check2 != VALID_MOVE) { 308 + move2 = randomMove(); 309 + check2 = checkMove(move2, board1, row2, col2); 310 + } 311 + 312 + // Execute moves 313 + int result1 = playMove(row1, col1, board2); 314 + int result2 = playMove(row2, col2, board1); 315 + 316 + updateMemory%s(row1, col1, result1, memory1); 317 + updateMemory%s(row2, col2, result2, memory2); 318 + 319 + if (isASunk(result1)) shipsSunk1++; 320 + if (isASunk(result2)) shipsSunk2++; 321 + 322 + if (shipsSunk1 == 5 || shipsSunk2 == 5) { 323 + break; 324 + } 325 + } 326 + 327 + result.totalMoves += moveCount; 328 + 329 + if (shipsSunk1 == 5 && shipsSunk2 == 5) { 330 + result.ties++; 331 + } else if (shipsSunk1 == 5) { 332 + result.player1Wins++; 333 + } else { 334 + result.player2Wins++; 335 + } 336 + } 337 + 338 + return result; 100 339 } 101 340 102 - func parseResults(output string) map[string]GameResult { 103 - results := make(map[string]GameResult) 341 + int main(int argc, char* argv[]) { 342 + if (argc < 2) { 343 + cerr << "Usage: " << argv[0] << " <num_games>" << endl; 344 + return 1; 345 + } 346 + 347 + int numGames = atoi(argv[1]); 348 + if (numGames <= 0) numGames = 10; 349 + 350 + setDebugMode(false); 351 + 352 + MatchResult result = runMatch(numGames); 353 + 354 + // Output in parseable format 355 + cout << "PLAYER1_WINS=" << result.player1Wins << endl; 356 + cout << "PLAYER2_WINS=" << result.player2Wins << endl; 357 + cout << "TIES=" << result.ties << endl; 358 + cout << "TOTAL_MOVES=" << result.totalMoves << endl; 359 + cout << "AVG_MOVES=" << (result.totalMoves / numGames) << endl; 360 + 361 + return 0; 362 + } 363 + `, prefix1, prefix2, suffix1, suffix2, suffix1, suffix2, suffix1, suffix2) 364 + } 365 + 366 + func parseMatchOutput(output string) (int, int, int) { 367 + player1Wins := 0 368 + player2Wins := 0 369 + totalMoves := 0 104 370 105 - // Parse win/loss stats from benchmark output 106 - // Example: "Smart AI wins: 95 (95.0%)" 107 371 lines := strings.Split(output, "\n") 108 372 for _, line := range lines { 109 - if strings.Contains(line, "Smart AI wins:") { 110 - // Extract win count 111 - re := regexp.MustCompile(`Smart AI wins: (\d+)`) 112 - matches := re.FindStringSubmatch(line) 113 - if len(matches) >= 2 { 114 - // For now, just record as wins against "random" 115 - results["random"] = GameResult{Result: "win", Moves: 50} 116 - } 373 + if strings.HasPrefix(line, "PLAYER1_WINS=") { 374 + fmt.Sscanf(line, "PLAYER1_WINS=%d", &player1Wins) 375 + } else if strings.HasPrefix(line, "PLAYER2_WINS=") { 376 + fmt.Sscanf(line, "PLAYER2_WINS=%d", &player2Wins) 377 + } else if strings.HasPrefix(line, "TOTAL_MOVES=") { 378 + fmt.Sscanf(line, "TOTAL_MOVES=%d", &totalMoves) 117 379 } 118 380 } 119 381 120 - return results 382 + return player1Wins, player2Wins, totalMoves 121 383 }
+7
scp.go
··· 51 51 return 0, err 52 52 } 53 53 54 + // Remove old file if it exists to ensure clean overwrite 55 + targetPath := filepath.Join(userDir, filename) 56 + if _, err := os.Stat(targetPath); err == nil { 57 + log.Printf("Removing old file: %s", targetPath) 58 + os.Remove(targetPath) 59 + } 60 + 54 61 // Modify the entry to write to user's subdirectory 55 62 userEntry := &scp.FileEntry{ 56 63 Name: filepath.Join(s.User(), filename),
-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"
+128
scripts/test-submissions/memory_functions_hunter.cpp
··· 1 + #include "battleship.h" 2 + #include "kasbs.h" 3 + #include "memory.h" 4 + #include <string> 5 + 6 + using namespace std; 7 + 8 + // Hunter AI - uses hunt/target mode (simpler than klukas) 9 + inline bool onBoard(int row, int col) { 10 + return row >= 0 && row < BOARDSIZE && col >= 0 && col < BOARDSIZE; 11 + } 12 + 13 + void initMemoryHunter(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 + string smartMoveHunter(const ComputerMemory &memory) { 30 + if (memory.mode == RANDOM) { 31 + // Use checkerboard pattern for hunting 32 + for (int i = 0; i < BOARDSIZE; i++) { 33 + for (int j = 0; j < BOARDSIZE; j++) { 34 + if ((i + j) % 2 == 0 && memory.grid[i][j] == EMPTY_MARKER) { 35 + char letter = static_cast<char>('A' + i); 36 + return string(1, letter) + to_string(j + 1); 37 + } 38 + } 39 + } 40 + 41 + // If no checkerboard cells left, use any empty cell 42 + for (int i = 0; i < BOARDSIZE; i++) { 43 + for (int j = 0; j < BOARDSIZE; j++) { 44 + if (memory.grid[i][j] == EMPTY_MARKER) { 45 + char letter = static_cast<char>('A' + i); 46 + return string(1, letter) + to_string(j + 1); 47 + } 48 + } 49 + } 50 + } 51 + 52 + // Target mode - try adjacent cells 53 + int directions[4][2] = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}}; // N, E, S, W 54 + int dirIdx = memory.fireDir; 55 + 56 + if (dirIdx == NONE) dirIdx = 0; 57 + 58 + // Try current direction 59 + for (int tries = 0; tries < 4; tries++) { 60 + int idx = (dirIdx + tries) % 4; 61 + int dr = directions[idx][0]; 62 + int dc = directions[idx][1]; 63 + int newRow = memory.hitRow + dr; 64 + int newCol = memory.hitCol + dc; 65 + 66 + if (onBoard(newRow, newCol) && memory.grid[newRow][newCol] == EMPTY_MARKER) { 67 + char letter = static_cast<char>('A' + newRow); 68 + return string(1, letter) + to_string(newCol + 1); 69 + } 70 + } 71 + 72 + // Fallback to random 73 + for (int i = 0; i < BOARDSIZE; i++) { 74 + for (int j = 0; j < BOARDSIZE; j++) { 75 + if (memory.grid[i][j] == EMPTY_MARKER) { 76 + char letter = static_cast<char>('A' + i); 77 + return string(1, letter) + to_string(j + 1); 78 + } 79 + } 80 + } 81 + 82 + return "A1"; 83 + } 84 + 85 + void updateMemoryHunter(int row, int col, int result, ComputerMemory &memory) { 86 + memory.lastResult = result; 87 + char marker; 88 + if (isAMiss(result)) { 89 + marker = MISS_MARKER; 90 + } else { 91 + marker = HIT_MARKER; 92 + } 93 + memory.grid[row][col] = marker; 94 + 95 + if (memory.mode == RANDOM) { 96 + if (!isAMiss(result)) { 97 + // Got a hit, switch to target mode 98 + memory.mode = SEARCH; 99 + memory.hitRow = row; 100 + memory.hitCol = col; 101 + memory.fireDir = NORTH; // Start trying north 102 + } 103 + } else { 104 + // In target mode 105 + if (isASunk(result)) { 106 + // Sunk the ship, back to hunt mode 107 + memory.mode = RANDOM; 108 + memory.hitRow = -1; 109 + memory.hitCol = -1; 110 + memory.fireDir = NONE; 111 + } else if (!isAMiss(result)) { 112 + // Another hit, keep current direction 113 + // (fireDir stays the same) 114 + } else { 115 + // Miss in target mode, try next direction 116 + if (memory.fireDir == NORTH) memory.fireDir = EAST; 117 + else if (memory.fireDir == EAST) memory.fireDir = SOUTH; 118 + else if (memory.fireDir == SOUTH) memory.fireDir = WEST; 119 + else { 120 + // Tried all directions, back to hunt 121 + memory.mode = RANDOM; 122 + memory.hitRow = -1; 123 + memory.hitCol = -1; 124 + memory.fireDir = NONE; 125 + } 126 + } 127 + } 128 + }
+59
scripts/test-submissions/memory_functions_random.cpp
··· 1 + #include "battleship.h" 2 + #include "kasbs.h" 3 + #include "memory.h" 4 + #include <string> 5 + 6 + using namespace std; 7 + 8 + // Random AI - just picks random valid moves 9 + void initMemoryRandom(ComputerMemory &memory) { 10 + memory.mode = RANDOM; 11 + memory.hitRow = -1; 12 + memory.hitCol = -1; 13 + memory.hitShip = NONE; 14 + memory.fireDir = NONE; 15 + memory.fireDist = 1; 16 + memory.lastResult = NONE; 17 + 18 + for (int i = 0; i < BOARDSIZE; i++) { 19 + for (int j = 0; j < BOARDSIZE; j++) { 20 + memory.grid[i][j] = EMPTY_MARKER; 21 + } 22 + } 23 + } 24 + 25 + string smartMoveRandom(const ComputerMemory &memory) { 26 + // Find all empty cells 27 + for (int attempts = 0; attempts < 100; attempts++) { 28 + int row = rand() % BOARDSIZE; 29 + int col = rand() % BOARDSIZE; 30 + 31 + if (memory.grid[row][col] == EMPTY_MARKER) { 32 + char letter = static_cast<char>('A' + row); 33 + return string(1, letter) + to_string(col + 1); 34 + } 35 + } 36 + 37 + // Fallback: find first empty cell 38 + for (int i = 0; i < BOARDSIZE; i++) { 39 + for (int j = 0; j < BOARDSIZE; j++) { 40 + if (memory.grid[i][j] == EMPTY_MARKER) { 41 + char letter = static_cast<char>('A' + i); 42 + return string(1, letter) + to_string(j + 1); 43 + } 44 + } 45 + } 46 + 47 + return "A1"; // Should never reach here 48 + } 49 + 50 + void updateMemoryRandom(int row, int col, int result, ComputerMemory &memory) { 51 + memory.lastResult = result; 52 + char marker; 53 + if (isAMiss(result)) { 54 + marker = MISS_MARKER; 55 + } else { 56 + marker = HIT_MARKER; 57 + } 58 + memory.grid[row][col] = marker; 59 + }
+70
scripts/test-upload.sh
··· 1 + #!/bin/bash 2 + 3 + # Test script to upload three different 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 + # Copy klukas submission to test-submissions if it doesn't exist 14 + if [ ! -f "$SCRIPT_DIR/test-submissions/memory_functions_klukas.cpp" ]; then 15 + echo "Copying klukas submission..." 16 + mkdir -p "$SCRIPT_DIR/test-submissions" 17 + cp /Users/kierank/code/school/cs1210-battleship/src/memory_functions_klukas.cpp "$SCRIPT_DIR/test-submissions/" 18 + fi 19 + 20 + # Upload for alice (random AI) 21 + echo "📤 Uploading for user: alice" 22 + echo " File: test-submissions/memory_functions_random.cpp" 23 + if [ -f "$SCRIPT_DIR/test-submissions/memory_functions_random.cpp" ]; then 24 + scp -P $PORT "$SCRIPT_DIR/test-submissions/memory_functions_random.cpp" "alice@$HOST:~/memory_functions_random.cpp" 25 + if [ $? -eq 0 ]; then 26 + echo "✅ Upload successful for alice" 27 + else 28 + echo "❌ Upload failed for alice" 29 + fi 30 + else 31 + echo "❌ Error: File not found" 32 + fi 33 + echo "" 34 + 35 + # Upload for bob (hunter AI) 36 + echo "📤 Uploading for user: bob" 37 + echo " File: test-submissions/memory_functions_hunter.cpp" 38 + if [ -f "$SCRIPT_DIR/test-submissions/memory_functions_hunter.cpp" ]; then 39 + scp -P $PORT "$SCRIPT_DIR/test-submissions/memory_functions_hunter.cpp" "bob@$HOST:~/memory_functions_hunter.cpp" 40 + if [ $? -eq 0 ]; then 41 + echo "✅ Upload successful for bob" 42 + else 43 + echo "❌ Upload failed for bob" 44 + fi 45 + else 46 + echo "❌ Error: File not found" 47 + fi 48 + echo "" 49 + 50 + # Upload for charlie (klukas AI) 51 + echo "📤 Uploading for user: charlie" 52 + echo " File: test-submissions/memory_functions_klukas.cpp" 53 + if [ -f "$SCRIPT_DIR/test-submissions/memory_functions_klukas.cpp" ]; then 54 + scp -P $PORT "$SCRIPT_DIR/test-submissions/memory_functions_klukas.cpp" "charlie@$HOST:~/memory_functions_klukas.cpp" 55 + if [ $? -eq 0 ]; then 56 + echo "✅ Upload successful for charlie" 57 + else 58 + echo "❌ Upload failed for charlie" 59 + fi 60 + else 61 + echo "❌ Error: File not found" 62 + fi 63 + echo "" 64 + 65 + echo "==============================================" 66 + echo "✨ All submissions uploaded!" 67 + echo "" 68 + echo "You can now:" 69 + echo " - SSH to view the TUI: ssh -p $PORT alice@$HOST" 70 + echo " - Check the web leaderboard: http://$HOST:8080"
+6
sftp.go
··· 66 66 dstPath := filepath.Join(h.baseDir, filename) 67 67 log.Printf("SFTP: Creating file %s for user %s", dstPath, h.username) 68 68 69 + // Remove old file if it exists to ensure clean overwrite 70 + if _, err := os.Stat(dstPath); err == nil { 71 + log.Printf("SFTP: Removing old file: %s", dstPath) 72 + os.Remove(dstPath) 73 + } 74 + 69 75 flags := r.Pflags() 70 76 var osFlags int 71 77 if flags.Creat {