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: use a wrapper to prevent stack snooping

+567 -193
+369
battleship-engine/src/arena.cpp
··· 1 + // Arena - orchestrates battleship matches between two isolated player processes 2 + // Each player runs in its own process, communicating via stdin/stdout pipes 3 + // This prevents any cross-player memory access exploits 4 + // 5 + // Usage: arena <num_games> <player1_binary> <player2_binary> 6 + // 7 + // Output format (for Go runner to parse): 8 + // PLAYER1_WINS=<n> 9 + // PLAYER2_WINS=<n> 10 + // TIES=<n> 11 + // TOTAL_MOVES=<n> 12 + // AVG_MOVES=<n> 13 + 14 + #include "battleship.h" 15 + #include "memory.h" 16 + 17 + #include <iostream> 18 + #include <string> 19 + #include <sstream> 20 + #include <cstdlib> 21 + #include <ctime> 22 + #include <vector> 23 + #include <unistd.h> 24 + #include <sys/wait.h> 25 + #include <signal.h> 26 + #include <fcntl.h> 27 + #include <poll.h> 28 + #include <errno.h> 29 + #include <cstring> 30 + 31 + using namespace std; 32 + 33 + const int TIMEOUT_MS = 5000; 34 + const int MAX_INVALID_MOVES = 10; 35 + 36 + struct PlayerProcess { 37 + pid_t pid; 38 + int stdinFd; 39 + int stdoutFd; 40 + string name; 41 + bool alive; 42 + 43 + PlayerProcess() : pid(-1), stdinFd(-1), stdoutFd(-1), alive(false) {} 44 + }; 45 + 46 + bool sendLine(PlayerProcess &p, const string &line) { 47 + if (!p.alive) return false; 48 + string msg = line + "\n"; 49 + ssize_t written = write(p.stdinFd, msg.c_str(), msg.size()); 50 + return written == (ssize_t)msg.size(); 51 + } 52 + 53 + bool readLine(PlayerProcess &p, string &line, int timeoutMs) { 54 + if (!p.alive) return false; 55 + 56 + line.clear(); 57 + char buf[1]; 58 + 59 + struct pollfd pfd; 60 + pfd.fd = p.stdoutFd; 61 + pfd.events = POLLIN; 62 + 63 + auto deadline = chrono::steady_clock::now() + chrono::milliseconds(timeoutMs); 64 + 65 + while (true) { 66 + auto remaining = chrono::duration_cast<chrono::milliseconds>( 67 + deadline - chrono::steady_clock::now()).count(); 68 + if (remaining <= 0) return false; 69 + 70 + int ret = poll(&pfd, 1, remaining); 71 + if (ret <= 0) return false; 72 + 73 + if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) { 74 + p.alive = false; 75 + return false; 76 + } 77 + 78 + ssize_t n = read(p.stdoutFd, buf, 1); 79 + if (n <= 0) { 80 + p.alive = false; 81 + return false; 82 + } 83 + 84 + if (buf[0] == '\n') { 85 + return true; 86 + } 87 + line += buf[0]; 88 + 89 + if (line.size() > 1000) return false; 90 + } 91 + } 92 + 93 + PlayerProcess spawnPlayer(const string &binaryPath) { 94 + PlayerProcess p; 95 + p.name = binaryPath; 96 + 97 + int stdinPipe[2]; 98 + int stdoutPipe[2]; 99 + 100 + if (pipe(stdinPipe) < 0 || pipe(stdoutPipe) < 0) { 101 + cerr << "Failed to create pipes" << endl; 102 + return p; 103 + } 104 + 105 + pid_t pid = fork(); 106 + if (pid < 0) { 107 + cerr << "Failed to fork" << endl; 108 + close(stdinPipe[0]); close(stdinPipe[1]); 109 + close(stdoutPipe[0]); close(stdoutPipe[1]); 110 + return p; 111 + } 112 + 113 + if (pid == 0) { 114 + // Child process 115 + close(stdinPipe[1]); 116 + close(stdoutPipe[0]); 117 + 118 + dup2(stdinPipe[0], STDIN_FILENO); 119 + dup2(stdoutPipe[1], STDOUT_FILENO); 120 + 121 + close(stdinPipe[0]); 122 + close(stdoutPipe[1]); 123 + 124 + execl(binaryPath.c_str(), binaryPath.c_str(), nullptr); 125 + cerr << "Failed to exec " << binaryPath << ": " << strerror(errno) << endl; 126 + _exit(1); 127 + } 128 + 129 + // Parent process 130 + close(stdinPipe[0]); 131 + close(stdoutPipe[1]); 132 + 133 + p.pid = pid; 134 + p.stdinFd = stdinPipe[1]; 135 + p.stdoutFd = stdoutPipe[0]; 136 + p.alive = true; 137 + 138 + // Set stdout non-blocking 139 + int flags = fcntl(p.stdoutFd, F_GETFL, 0); 140 + fcntl(p.stdoutFd, F_SETFL, flags | O_NONBLOCK); 141 + 142 + return p; 143 + } 144 + 145 + void killPlayer(PlayerProcess &p) { 146 + if (p.pid > 0) { 147 + kill(p.pid, SIGTERM); 148 + usleep(10000); 149 + kill(p.pid, SIGKILL); 150 + waitpid(p.pid, nullptr, 0); 151 + } 152 + if (p.stdinFd >= 0) close(p.stdinFd); 153 + if (p.stdoutFd >= 0) close(p.stdoutFd); 154 + p.alive = false; 155 + } 156 + 157 + bool handshake(PlayerProcess &p) { 158 + if (!sendLine(p, "HELLO 1")) return false; 159 + 160 + string response; 161 + if (!readLine(p, response, TIMEOUT_MS)) return false; 162 + 163 + return response == "HELLO OK"; 164 + } 165 + 166 + bool initPlayer(PlayerProcess &p) { 167 + if (!sendLine(p, "INIT")) return false; 168 + 169 + string response; 170 + if (!readLine(p, response, TIMEOUT_MS)) return false; 171 + 172 + return response == "OK"; 173 + } 174 + 175 + string getMove(PlayerProcess &p) { 176 + if (!sendLine(p, "GET_MOVE")) return ""; 177 + 178 + string response; 179 + if (!readLine(p, response, TIMEOUT_MS)) return ""; 180 + 181 + if (response.substr(0, 5) == "MOVE ") { 182 + return response.substr(5); 183 + } 184 + return ""; 185 + } 186 + 187 + bool updatePlayer(PlayerProcess &p, int row, int col, int result) { 188 + ostringstream oss; 189 + oss << "UPDATE " << row << " " << col << " " << result; 190 + if (!sendLine(p, oss.str())) return false; 191 + 192 + string response; 193 + if (!readLine(p, response, TIMEOUT_MS)) return false; 194 + 195 + return response == "OK"; 196 + } 197 + 198 + void quitPlayer(PlayerProcess &p) { 199 + sendLine(p, "QUIT"); 200 + string response; 201 + readLine(p, response, 100); 202 + } 203 + 204 + struct MatchResult { 205 + int player1Wins = 0; 206 + int player2Wins = 0; 207 + int ties = 0; 208 + int totalMoves = 0; 209 + }; 210 + 211 + int main(int argc, char* argv[]) { 212 + if (argc < 4) { 213 + cerr << "Usage: " << argv[0] << " <num_games> <player1_binary> <player2_binary>" << endl; 214 + return 1; 215 + } 216 + 217 + int numGames = atoi(argv[1]); 218 + if (numGames <= 0) numGames = 10; 219 + 220 + string player1Path = argv[2]; 221 + string player2Path = argv[3]; 222 + 223 + setDebugMode(false); 224 + srand(time(nullptr)); 225 + 226 + // Spawn player processes 227 + PlayerProcess p1 = spawnPlayer(player1Path); 228 + PlayerProcess p2 = spawnPlayer(player2Path); 229 + 230 + if (!p1.alive || !p2.alive) { 231 + cerr << "Failed to spawn player processes" << endl; 232 + killPlayer(p1); 233 + killPlayer(p2); 234 + return 1; 235 + } 236 + 237 + // Handshake 238 + if (!handshake(p1)) { 239 + cerr << "Handshake failed with player 1" << endl; 240 + killPlayer(p1); 241 + killPlayer(p2); 242 + return 1; 243 + } 244 + 245 + if (!handshake(p2)) { 246 + cerr << "Handshake failed with player 2" << endl; 247 + killPlayer(p1); 248 + killPlayer(p2); 249 + return 1; 250 + } 251 + 252 + MatchResult result; 253 + 254 + for (int game = 0; game < numGames; game++) { 255 + // Check if players are still alive 256 + if (!p1.alive || !p2.alive) { 257 + cerr << "Player process died during match" << endl; 258 + break; 259 + } 260 + 261 + // Initialize boards (arena owns the authoritative state) 262 + Board board1, board2; 263 + initializeBoard(board1); 264 + initializeBoard(board2); 265 + 266 + // Initialize players for new game 267 + if (!initPlayer(p1) || !initPlayer(p2)) { 268 + cerr << "Failed to initialize players for game " << game << endl; 269 + break; 270 + } 271 + 272 + int shipsSunk1 = 0; 273 + int shipsSunk2 = 0; 274 + int moveCount = 0; 275 + int invalidMoves1 = 0; 276 + int invalidMoves2 = 0; 277 + 278 + while (true) { 279 + moveCount++; 280 + 281 + // Get move from player 1 (attacking board2) 282 + string move1 = getMove(p1); 283 + int row1, col1; 284 + int check1 = move1.empty() ? ILLEGAL_FORMAT : checkMove(move1, board2, row1, col1); 285 + 286 + while (check1 != VALID_MOVE) { 287 + invalidMoves1++; 288 + if (invalidMoves1 > MAX_INVALID_MOVES) { 289 + move1 = randomMove(); 290 + check1 = checkMove(move1, board2, row1, col1); 291 + } else { 292 + move1 = randomMove(); 293 + check1 = checkMove(move1, board2, row1, col1); 294 + } 295 + } 296 + 297 + // Get move from player 2 (attacking board1) 298 + string move2 = getMove(p2); 299 + int row2, col2; 300 + int check2 = move2.empty() ? ILLEGAL_FORMAT : checkMove(move2, board1, row2, col2); 301 + 302 + while (check2 != VALID_MOVE) { 303 + invalidMoves2++; 304 + if (invalidMoves2 > MAX_INVALID_MOVES) { 305 + move2 = randomMove(); 306 + check2 = checkMove(move2, board1, row2, col2); 307 + } else { 308 + move2 = randomMove(); 309 + check2 = checkMove(move2, board1, row2, col2); 310 + } 311 + } 312 + 313 + // Execute moves on the authoritative boards 314 + int result1 = playMove(row1, col1, board2); 315 + int result2 = playMove(row2, col2, board1); 316 + 317 + // Update players with results 318 + if (!updatePlayer(p1, row1, col1, result1)) { 319 + p1.alive = false; 320 + break; 321 + } 322 + if (!updatePlayer(p2, row2, col2, result2)) { 323 + p2.alive = false; 324 + break; 325 + } 326 + 327 + if (isASunk(result1)) shipsSunk1++; 328 + if (isASunk(result2)) shipsSunk2++; 329 + 330 + if (shipsSunk1 == 5 || shipsSunk2 == 5) { 331 + break; 332 + } 333 + 334 + // Safety: prevent infinite games 335 + if (moveCount > 200) { 336 + break; 337 + } 338 + } 339 + 340 + result.totalMoves += moveCount; 341 + 342 + if (shipsSunk1 == 5 && shipsSunk2 == 5) { 343 + result.ties++; 344 + } else if (shipsSunk1 == 5) { 345 + result.player1Wins++; 346 + } else if (shipsSunk2 == 5) { 347 + result.player2Wins++; 348 + } else { 349 + // Game ended abnormally (player died or max moves) 350 + // Count as a tie or handle as you prefer 351 + result.ties++; 352 + } 353 + } 354 + 355 + // Clean up 356 + quitPlayer(p1); 357 + quitPlayer(p2); 358 + killPlayer(p1); 359 + killPlayer(p2); 360 + 361 + // Output results in format Go expects 362 + cout << "PLAYER1_WINS=" << result.player1Wins << endl; 363 + cout << "PLAYER2_WINS=" << result.player2Wins << endl; 364 + cout << "TIES=" << result.ties << endl; 365 + cout << "TOTAL_MOVES=" << result.totalMoves << endl; 366 + cout << "AVG_MOVES=" << (numGames > 0 ? result.totalMoves / numGames : 0) << endl; 367 + 368 + return 0; 369 + }
+118
battleship-engine/src/player_wrapper.cpp
··· 1 + // Player wrapper - isolates student AI code in its own process 2 + // Communicates with arena via stdin/stdout line-based protocol 3 + // 4 + // Protocol: 5 + // Arena -> Player: HELLO 1 6 + // Player -> Arena: HELLO OK 7 + // Arena -> Player: INIT 8 + // Player -> Arena: OK 9 + // Arena -> Player: GET_MOVE 10 + // Player -> Arena: MOVE <cell> (e.g., MOVE A5) 11 + // Arena -> Player: UPDATE <row> <col> <result> 12 + // Player -> Arena: OK 13 + // Arena -> Player: QUIT 14 + // Player -> Arena: OK 15 + 16 + #include "battleship.h" 17 + #include "memory.h" 18 + #include PLAYER_HEADER 19 + 20 + #include <iostream> 21 + #include <sstream> 22 + #include <string> 23 + 24 + #define CONCAT_INNER(a, b) a##b 25 + #define CONCAT(a, b) CONCAT_INNER(a, b) 26 + 27 + #define initMemoryFunc CONCAT(initMemory, PLAYER_SUFFIX) 28 + #define smartMoveFunc CONCAT(smartMove, PLAYER_SUFFIX) 29 + #define updateMemoryFunc CONCAT(updateMemory, PLAYER_SUFFIX) 30 + 31 + static void doInit(ComputerMemory &mem) { 32 + initMemoryFunc(mem); 33 + } 34 + 35 + static std::string doSmartMove(const ComputerMemory &mem) { 36 + return smartMoveFunc(mem); 37 + } 38 + 39 + static void doUpdate(int row, int col, int result, ComputerMemory &mem) { 40 + updateMemoryFunc(row, col, result, mem); 41 + } 42 + 43 + int main() { 44 + std::ios::sync_with_stdio(false); 45 + std::cin.tie(nullptr); 46 + 47 + ComputerMemory memory{}; 48 + bool initialized = false; 49 + 50 + std::string line; 51 + 52 + auto sendLine = [](const std::string &s) { 53 + std::cout << s << "\n"; 54 + std::cout.flush(); 55 + }; 56 + 57 + // Handshake 58 + if (!std::getline(std::cin, line)) { 59 + return 0; 60 + } 61 + 62 + { 63 + std::istringstream iss(line); 64 + std::string cmd; 65 + int version; 66 + iss >> cmd >> version; 67 + if (cmd != "HELLO" || version != 1) { 68 + sendLine("ERROR bad_hello"); 69 + return 1; 70 + } 71 + sendLine("HELLO OK"); 72 + } 73 + 74 + while (std::getline(std::cin, line)) { 75 + if (line.empty()) continue; 76 + 77 + std::istringstream iss(line); 78 + std::string cmd; 79 + iss >> cmd; 80 + 81 + if (cmd == "INIT") { 82 + memory = ComputerMemory{}; 83 + doInit(memory); 84 + initialized = true; 85 + sendLine("OK"); 86 + } else if (cmd == "GET_MOVE") { 87 + if (!initialized) { 88 + sendLine("ERROR not_initialized"); 89 + continue; 90 + } 91 + std::string move = doSmartMove(memory); 92 + if (move.empty()) { 93 + sendLine("ERROR empty_move"); 94 + } else { 95 + sendLine("MOVE " + move); 96 + } 97 + } else if (cmd == "UPDATE") { 98 + int row, col, result; 99 + if (!(iss >> row >> col >> result)) { 100 + sendLine("ERROR bad_update_args"); 101 + continue; 102 + } 103 + if (!initialized) { 104 + sendLine("ERROR not_initialized"); 105 + continue; 106 + } 107 + doUpdate(row, col, result, memory); 108 + sendLine("OK"); 109 + } else if (cmd == "QUIT") { 110 + sendLine("OK"); 111 + break; 112 + } else { 113 + sendLine("ERROR unknown_command"); 114 + } 115 + } 116 + 117 + return 0; 118 + }
+80 -193
internal/runner/runner.go
··· 122 122 return output, err 123 123 } 124 124 125 + // ensureArenaBuilt compiles the arena binary if it doesn't exist 126 + func ensureArenaBuilt() error { 127 + buildDir := filepath.Join(enginePath, "build") 128 + arenaBinary := filepath.Join(buildDir, "arena") 129 + 130 + // Check if arena binary exists 131 + if _, err := os.Stat(arenaBinary); err == nil { 132 + return nil 133 + } 134 + 135 + os.MkdirAll(buildDir, 0755) 136 + 137 + srcDir := filepath.Join(enginePath, "src") 138 + 139 + compileArgs := []string{ 140 + "g++", "-std=c++11", "-O3", 141 + "-I", srcDir, 142 + "-o", arenaBinary, 143 + filepath.Join(srcDir, "arena.cpp"), 144 + filepath.Join(srcDir, "battleship.cpp"), 145 + } 146 + 147 + log.Printf("Building arena binary...") 148 + output, err := runSandboxed(context.Background(), "build-arena", compileArgs, 120) 149 + if err != nil { 150 + return fmt.Errorf("failed to build arena: %s", output) 151 + } 152 + 153 + log.Printf("Arena binary built successfully") 154 + return nil 155 + } 156 + 125 157 func CompileSubmission(sub storage.Submission, uploadDir string) error { 126 158 storage.UpdateSubmissionStatus(sub.ID, "testing") 127 159 ··· 146 178 if err != nil { 147 179 return err 148 180 } 149 - 150 - // Students can now use #include "battleship.h" directly 151 - // No need to remove it 152 181 153 182 if err := os.WriteFile(dstPath, input, 0644); err != nil { 154 183 return err ··· 168 197 return err 169 198 } 170 199 171 - log.Printf("Compiling submission %d for %s", sub.ID, prefix) 200 + // Compile player wrapper binary (isolated process for this AI) 201 + playerBinary := filepath.Join(buildDir, "ai_"+prefix) 202 + 203 + log.Printf("Compiling isolated player binary for %s", prefix) 172 204 173 - // Compile in sandbox with 60 second timeout 174 205 compileArgs := []string{ 175 - "g++", "-std=c++11", "-c", "-O3", 176 - "-I", filepath.Join(enginePath, "src"), 177 - "-o", filepath.Join(buildDir, "ai_"+prefix+".o"), 178 - filepath.Join(enginePath, "src", sub.Filename), 206 + "g++", "-std=c++11", "-O3", 207 + "-I", srcDir, 208 + fmt.Sprintf("-DPLAYER_SUFFIX=%s", functionSuffix), 209 + fmt.Sprintf(`-DPLAYER_HEADER="memory_functions_%s.h"`, prefix), 210 + "-o", playerBinary, 211 + filepath.Join(srcDir, "player_wrapper.cpp"), 212 + filepath.Join(srcDir, "battleship.cpp"), 213 + filepath.Join(srcDir, fmt.Sprintf("memory_functions_%s.cpp", prefix)), 179 214 } 180 215 181 - output, err := runSandboxed(context.Background(), "compile-"+prefix, compileArgs, 60) 216 + output, err := runSandboxed(context.Background(), "compile-"+prefix, compileArgs, 120) 182 217 if err != nil { 183 218 return fmt.Errorf("compilation failed: %s", output) 184 219 } 185 220 221 + log.Printf("Player binary compiled: %s", playerBinary) 186 222 return nil 187 223 } 188 224 189 225 func RunHeadToHead(player1, player2 storage.Submission, numGames int) (int, int, int, string) { 226 + // Ensure arena is built 227 + if err := ensureArenaBuilt(); err != nil { 228 + return 0, 0, 0, fmt.Sprintf("Failed to build arena: %v", err) 229 + } 230 + 190 231 re := regexp.MustCompile(`memory_functions_(\w+)\.cpp`) 191 232 matches1 := re.FindStringSubmatch(player1.Filename) 192 233 matches2 := re.FindStringSubmatch(player2.Filename) ··· 198 239 prefix1 := matches1[1] 199 240 prefix2 := matches2[1] 200 241 201 - cpp1Path := filepath.Join(enginePath, "src", player1.Filename) 202 - cpp2Path := filepath.Join(enginePath, "src", player2.Filename) 203 - 204 - // Ensure both files exist in engine/src (copy from uploads if missing) 205 - if _, err := os.Stat(cpp1Path); os.IsNotExist(err) { 206 - log.Printf("Player1 file missing in engine/src, skipping: %s", cpp1Path) 207 - return 0, 0, 0, fmt.Sprintf("File missing: %s", cpp1Path) 208 - } 209 - 210 - if _, err := os.Stat(cpp2Path); os.IsNotExist(err) { 211 - log.Printf("Player2 file missing in engine/src, skipping: %s", cpp2Path) 212 - return 0, 0, 0, fmt.Sprintf("Opponent file missing: %s", cpp2Path) 213 - } 214 - 215 - cpp1Content, err := os.ReadFile(cpp1Path) 216 - if err != nil { 217 - log.Printf("Failed to read %s: %v", cpp1Path, err) 218 - return 0, 0, 0, fmt.Sprintf("Failed to read file: %v", err) 219 - } 220 - 221 - cpp2Content, err := os.ReadFile(cpp2Path) 222 - if err != nil { 223 - log.Printf("Failed to read %s: %v", cpp2Path, err) 224 - return 0, 0, 0, fmt.Sprintf("Failed to read opponent file: %v", err) 225 - } 226 - 227 - suffix1, err := parseFunctionNames(string(cpp1Content)) 228 - if err != nil { 229 - log.Printf("Failed to parse function names for %s: %v", player1.Filename, err) 230 - return 0, 0, 0, fmt.Sprintf("Could not find required function signatures (initMemory, smartMove, updateMemory)") 231 - } 232 - 233 - suffix2, err := parseFunctionNames(string(cpp2Content)) 234 - if err != nil { 235 - log.Printf("Failed to parse function names for %s: %v", player2.Filename, err) 236 - return 0, 0, 0, fmt.Sprintf("Opponent file parse error: %v", err) 237 - } 238 - 239 242 buildDir := filepath.Join(enginePath, "build") 240 - combinedBinary := filepath.Join(buildDir, fmt.Sprintf("match_%s_vs_%s", prefix1, prefix2)) 243 + arenaBinary := filepath.Join(buildDir, "arena") 244 + player1Binary := filepath.Join(buildDir, "ai_"+prefix1) 245 + player2Binary := filepath.Join(buildDir, "ai_"+prefix2) 241 246 242 - mainContent := generateMatchMain(prefix1, prefix2, suffix1, suffix2) 243 - mainPath := filepath.Join(enginePath, "src", fmt.Sprintf("match_%s_vs_%s.cpp", prefix1, prefix2)) 244 - if err := os.WriteFile(mainPath, []byte(mainContent), 0644); err != nil { 245 - log.Printf("Failed to write match main: %v", err) 246 - return 0, 0, 0, fmt.Sprintf("Failed to write match file: %v", err) 247 + // Check binaries exist 248 + if _, err := os.Stat(player1Binary); os.IsNotExist(err) { 249 + return 0, 0, 0, fmt.Sprintf("Player 1 binary missing: %s", player1Binary) 247 250 } 248 251 249 - // Compile match binary in sandbox with 120 second timeout 250 - compileArgs := []string{"g++"} 251 - compileArgs = append(compileArgs, "-std=c++11", "-O3", 252 - "-o", combinedBinary, 253 - mainPath, 254 - filepath.Join(enginePath, "src", "battleship.cpp"), 255 - ) 256 - 257 - if prefix1 == prefix2 { 258 - compileArgs = append(compileArgs, filepath.Join(enginePath, "src", fmt.Sprintf("memory_functions_%s.cpp", prefix1))) 259 - } else { 260 - compileArgs = append(compileArgs, 261 - filepath.Join(enginePath, "src", fmt.Sprintf("memory_functions_%s.cpp", prefix1)), 262 - filepath.Join(enginePath, "src", fmt.Sprintf("memory_functions_%s.cpp", prefix2)), 263 - ) 252 + if _, err := os.Stat(player2Binary); os.IsNotExist(err) { 253 + return 0, 0, 0, fmt.Sprintf("Player 2 binary missing: %s", player2Binary) 264 254 } 265 255 266 - output, err := runSandboxed(context.Background(), "compile-match", compileArgs, 120) 267 - if err != nil { 268 - log.Printf("Failed to compile match binary (err=%v): %s", err, output) 269 - return 0, 0, 0, fmt.Sprintf("Compilation error: %s", string(output)) 270 - } 256 + // Run arena with both player binaries 257 + // Arena spawns each player in its own isolated process 258 + runArgs := []string{arenaBinary, strconv.Itoa(numGames), player1Binary, player2Binary} 271 259 272 - log.Printf("Match compilation output: %s", output) 260 + log.Printf("Running isolated match: %s vs %s (%d games)", prefix1, prefix2, numGames) 273 261 274 - // Check if binary was actually created 275 - if _, err := os.Stat(combinedBinary); os.IsNotExist(err) { 276 - log.Printf("Match binary was not created at %s, compilation succeeded but no binary found", combinedBinary) 277 - return 0, 0, 0, "Match binary not created after compilation" 278 - } 279 - 280 - // Run match in sandbox with 300 second timeout (1000 games should be ~60s, give headroom) 281 - runArgs := []string{combinedBinary, strconv.Itoa(numGames)} 282 - output, err = runSandboxed(context.Background(), "run-match", runArgs, 300) 262 + output, err := runSandboxed(context.Background(), "run-match", runArgs, 600) 283 263 if err != nil { 284 264 log.Printf("Match execution failed: %v\n%s", err, output) 285 265 errMsg := strings.TrimSpace(string(output)) 286 266 if errMsg != "" { 287 - // If there's output, show it along with the exit status 288 267 return 0, 0, 0, fmt.Sprintf("Runtime error: %s (%s)", errMsg, err.Error()) 289 268 } 290 - // If no output, just show the error 291 269 return 0, 0, 0, fmt.Sprintf("Runtime error: %s", err.Error()) 292 270 } 293 271 ··· 315 293 } 316 294 317 295 if !hasMatch { 318 - // Ensure opponent file exists in engine/src 296 + // Ensure opponent file exists in engine/src and is compiled 319 297 opponentSrcPath := filepath.Join(uploadDir, opponent.Username, opponent.Filename) 320 298 opponentDstPath := filepath.Join(enginePath, "src", opponent.Filename) 321 299 ··· 342 320 headerPath := filepath.Join(enginePath, "src", headerFilename) 343 321 headerContent := generateHeader(headerFilename, functionSuffix) 344 322 os.WriteFile(headerPath, []byte(headerContent), 0644) 323 + } 324 + } 325 + } 326 + 327 + // Ensure opponent binary is compiled 328 + re := regexp.MustCompile(`memory_functions_(\w+)\.cpp`) 329 + matches := re.FindStringSubmatch(opponent.Filename) 330 + if len(matches) >= 2 { 331 + prefix := matches[1] 332 + playerBinary := filepath.Join(enginePath, "build", "ai_"+prefix) 333 + if _, err := os.Stat(playerBinary); os.IsNotExist(err) { 334 + // Compile opponent 335 + if err := CompileSubmission(opponent, uploadDir); err != nil { 336 + log.Printf("Failed to compile opponent %s: %v", opponent.Username, err) 337 + continue 345 338 } 346 339 } 347 340 } ··· 448 441 449 442 #endif 450 443 `, guard, guard, prefix, prefix, prefix) 451 - } 452 - 453 - func generateMatchMain(prefix1, prefix2, suffix1, suffix2 string) string { 454 - return fmt.Sprintf(`#include "battleship.h" 455 - #include "memory.h" 456 - #include "memory_functions_%s.h" 457 - #include "memory_functions_%s.h" 458 - #include <iostream> 459 - #include <cstdlib> 460 - #include <ctime> 461 - 462 - using namespace std; 463 - 464 - struct MatchResult { 465 - int player1Wins = 0; 466 - int player2Wins = 0; 467 - int ties = 0; 468 - int totalMoves = 0; 469 - }; 470 - 471 - MatchResult runMatch(int numGames) { 472 - MatchResult result; 473 - srand(time(NULL)); 474 - 475 - for (int game = 0; game < numGames; game++) { 476 - Board board1, board2; 477 - ComputerMemory memory1, memory2; 478 - 479 - initializeBoard(board1); 480 - initializeBoard(board2); 481 - initMemory%s(memory1); 482 - initMemory%s(memory2); 483 - 484 - int shipsSunk1 = 0; 485 - int shipsSunk2 = 0; 486 - int moveCount = 0; 487 - 488 - while (true) { 489 - moveCount++; 490 - 491 - string move1 = smartMove%s(memory1); 492 - int row1, col1; 493 - int check1 = checkMove(move1, board2, row1, col1); 494 - while (check1 != VALID_MOVE) { 495 - move1 = randomMove(); 496 - check1 = checkMove(move1, board2, row1, col1); 497 - } 498 - 499 - string move2 = smartMove%s(memory2); 500 - int row2, col2; 501 - int check2 = checkMove(move2, board1, row2, col2); 502 - while (check2 != VALID_MOVE) { 503 - move2 = randomMove(); 504 - check2 = checkMove(move2, board1, row2, col2); 505 - } 506 - 507 - int result1 = playMove(row1, col1, board2); 508 - int result2 = playMove(row2, col2, board1); 509 - 510 - updateMemory%s(row1, col1, result1, memory1); 511 - updateMemory%s(row2, col2, result2, memory2); 512 - 513 - if (isASunk(result1)) shipsSunk1++; 514 - if (isASunk(result2)) shipsSunk2++; 515 - 516 - if (shipsSunk1 == 5 || shipsSunk2 == 5) { 517 - break; 518 - } 519 - } 520 - 521 - result.totalMoves += moveCount; 522 - 523 - if (shipsSunk1 == 5 && shipsSunk2 == 5) { 524 - result.ties++; 525 - } else if (shipsSunk1 == 5) { 526 - result.player1Wins++; 527 - } else { 528 - result.player2Wins++; 529 - } 530 - } 531 - 532 - return result; 533 - } 534 - 535 - int main(int argc, char* argv[]) { 536 - if (argc < 2) { 537 - cerr << "Usage: " << argv[0] << " <num_games>" << endl; 538 - return 1; 539 - } 540 - 541 - int numGames = atoi(argv[1]); 542 - if (numGames <= 0) numGames = 10; 543 - 544 - setDebugMode(false); 545 - 546 - MatchResult result = runMatch(numGames); 547 - 548 - cout << "PLAYER1_WINS=" << result.player1Wins << endl; 549 - cout << "PLAYER2_WINS=" << result.player2Wins << endl; 550 - cout << "TIES=" << result.ties << endl; 551 - cout << "TOTAL_MOVES=" << result.totalMoves << endl; 552 - cout << "AVG_MOVES=" << (result.totalMoves / numGames) << endl; 553 - 554 - return 0; 555 - } 556 - `, prefix1, prefix2, suffix1, suffix2, suffix1, suffix2, suffix1, suffix2) 557 444 } 558 445 559 446 func parseMatchOutput(output string) (int, int, int) {