a geicko-2 based round robin ranking system designed to test c++ battleship submissions battleship.dunkirk.sh
at main 370 lines 10 kB view raw
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 <chrono> 23#include <vector> 24#include <unistd.h> 25#include <sys/wait.h> 26#include <signal.h> 27#include <fcntl.h> 28#include <poll.h> 29#include <errno.h> 30#include <cstring> 31 32using namespace std; 33 34const int TIMEOUT_MS = 5000; 35const int MAX_INVALID_MOVES = 10; 36 37struct PlayerProcess { 38 pid_t pid; 39 int stdinFd; 40 int stdoutFd; 41 string name; 42 bool alive; 43 44 PlayerProcess() : pid(-1), stdinFd(-1), stdoutFd(-1), alive(false) {} 45}; 46 47bool sendLine(PlayerProcess &p, const string &line) { 48 if (!p.alive) return false; 49 string msg = line + "\n"; 50 ssize_t written = write(p.stdinFd, msg.c_str(), msg.size()); 51 return written == (ssize_t)msg.size(); 52} 53 54bool readLine(PlayerProcess &p, string &line, int timeoutMs) { 55 if (!p.alive) return false; 56 57 line.clear(); 58 char buf[1]; 59 60 struct pollfd pfd; 61 pfd.fd = p.stdoutFd; 62 pfd.events = POLLIN; 63 64 auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs); 65 66 while (true) { 67 auto remaining = std::chrono::duration_cast<std::chrono::milliseconds>( 68 deadline - std::chrono::steady_clock::now()).count(); 69 if (remaining <= 0) return false; 70 71 int ret = poll(&pfd, 1, remaining); 72 if (ret <= 0) return false; 73 74 if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) { 75 p.alive = false; 76 return false; 77 } 78 79 ssize_t n = read(p.stdoutFd, buf, 1); 80 if (n <= 0) { 81 p.alive = false; 82 return false; 83 } 84 85 if (buf[0] == '\n') { 86 return true; 87 } 88 line += buf[0]; 89 90 if (line.size() > 1000) return false; 91 } 92} 93 94PlayerProcess spawnPlayer(const string &binaryPath) { 95 PlayerProcess p; 96 p.name = binaryPath; 97 98 int stdinPipe[2]; 99 int stdoutPipe[2]; 100 101 if (pipe(stdinPipe) < 0 || pipe(stdoutPipe) < 0) { 102 cerr << "Failed to create pipes" << endl; 103 return p; 104 } 105 106 pid_t pid = fork(); 107 if (pid < 0) { 108 cerr << "Failed to fork" << endl; 109 close(stdinPipe[0]); close(stdinPipe[1]); 110 close(stdoutPipe[0]); close(stdoutPipe[1]); 111 return p; 112 } 113 114 if (pid == 0) { 115 // Child process 116 close(stdinPipe[1]); 117 close(stdoutPipe[0]); 118 119 dup2(stdinPipe[0], STDIN_FILENO); 120 dup2(stdoutPipe[1], STDOUT_FILENO); 121 122 close(stdinPipe[0]); 123 close(stdoutPipe[1]); 124 125 execl(binaryPath.c_str(), binaryPath.c_str(), nullptr); 126 cerr << "Failed to exec " << binaryPath << ": " << strerror(errno) << endl; 127 _exit(1); 128 } 129 130 // Parent process 131 close(stdinPipe[0]); 132 close(stdoutPipe[1]); 133 134 p.pid = pid; 135 p.stdinFd = stdinPipe[1]; 136 p.stdoutFd = stdoutPipe[0]; 137 p.alive = true; 138 139 // Set stdout non-blocking 140 int flags = fcntl(p.stdoutFd, F_GETFL, 0); 141 fcntl(p.stdoutFd, F_SETFL, flags | O_NONBLOCK); 142 143 return p; 144} 145 146void killPlayer(PlayerProcess &p) { 147 if (p.pid > 0) { 148 kill(p.pid, SIGTERM); 149 usleep(10000); 150 kill(p.pid, SIGKILL); 151 waitpid(p.pid, nullptr, 0); 152 } 153 if (p.stdinFd >= 0) close(p.stdinFd); 154 if (p.stdoutFd >= 0) close(p.stdoutFd); 155 p.alive = false; 156} 157 158bool handshake(PlayerProcess &p) { 159 if (!sendLine(p, "HELLO 1")) return false; 160 161 string response; 162 if (!readLine(p, response, TIMEOUT_MS)) return false; 163 164 return response == "HELLO OK"; 165} 166 167bool initPlayer(PlayerProcess &p) { 168 if (!sendLine(p, "INIT")) return false; 169 170 string response; 171 if (!readLine(p, response, TIMEOUT_MS)) return false; 172 173 return response == "OK"; 174} 175 176string getMove(PlayerProcess &p) { 177 if (!sendLine(p, "GET_MOVE")) return ""; 178 179 string response; 180 if (!readLine(p, response, TIMEOUT_MS)) return ""; 181 182 if (response.substr(0, 5) == "MOVE ") { 183 return response.substr(5); 184 } 185 return ""; 186} 187 188bool updatePlayer(PlayerProcess &p, int row, int col, int result) { 189 ostringstream oss; 190 oss << "UPDATE " << row << " " << col << " " << result; 191 if (!sendLine(p, oss.str())) return false; 192 193 string response; 194 if (!readLine(p, response, TIMEOUT_MS)) return false; 195 196 return response == "OK"; 197} 198 199void quitPlayer(PlayerProcess &p) { 200 sendLine(p, "QUIT"); 201 string response; 202 readLine(p, response, 100); 203} 204 205struct MatchResult { 206 int player1Wins = 0; 207 int player2Wins = 0; 208 int ties = 0; 209 int totalMoves = 0; 210}; 211 212int main(int argc, char* argv[]) { 213 if (argc < 4) { 214 cerr << "Usage: " << argv[0] << " <num_games> <player1_binary> <player2_binary>" << endl; 215 return 1; 216 } 217 218 int numGames = atoi(argv[1]); 219 if (numGames <= 0) numGames = 10; 220 221 string player1Path = argv[2]; 222 string player2Path = argv[3]; 223 224 setDebugMode(false); 225 srand(time(nullptr)); 226 227 // Spawn player processes 228 PlayerProcess p1 = spawnPlayer(player1Path); 229 PlayerProcess p2 = spawnPlayer(player2Path); 230 231 if (!p1.alive || !p2.alive) { 232 cerr << "Failed to spawn player processes" << endl; 233 killPlayer(p1); 234 killPlayer(p2); 235 return 1; 236 } 237 238 // Handshake 239 if (!handshake(p1)) { 240 cerr << "Handshake failed with player 1" << endl; 241 killPlayer(p1); 242 killPlayer(p2); 243 return 1; 244 } 245 246 if (!handshake(p2)) { 247 cerr << "Handshake failed with player 2" << endl; 248 killPlayer(p1); 249 killPlayer(p2); 250 return 1; 251 } 252 253 MatchResult result; 254 255 for (int game = 0; game < numGames; game++) { 256 // Check if players are still alive 257 if (!p1.alive || !p2.alive) { 258 cerr << "Player process died during match" << endl; 259 break; 260 } 261 262 // Initialize boards (arena owns the authoritative state) 263 Board board1, board2; 264 initializeBoard(board1); 265 initializeBoard(board2); 266 267 // Initialize players for new game 268 if (!initPlayer(p1) || !initPlayer(p2)) { 269 cerr << "Failed to initialize players for game " << game << endl; 270 break; 271 } 272 273 int shipsSunk1 = 0; 274 int shipsSunk2 = 0; 275 int moveCount = 0; 276 int invalidMoves1 = 0; 277 int invalidMoves2 = 0; 278 279 while (true) { 280 moveCount++; 281 282 // Get move from player 1 (attacking board2) 283 string move1 = getMove(p1); 284 int row1, col1; 285 int check1 = move1.empty() ? ILLEGAL_FORMAT : checkMove(move1, board2, row1, col1); 286 287 while (check1 != VALID_MOVE) { 288 invalidMoves1++; 289 if (invalidMoves1 > MAX_INVALID_MOVES) { 290 move1 = randomMove(); 291 check1 = checkMove(move1, board2, row1, col1); 292 } else { 293 move1 = randomMove(); 294 check1 = checkMove(move1, board2, row1, col1); 295 } 296 } 297 298 // Get move from player 2 (attacking board1) 299 string move2 = getMove(p2); 300 int row2, col2; 301 int check2 = move2.empty() ? ILLEGAL_FORMAT : checkMove(move2, board1, row2, col2); 302 303 while (check2 != VALID_MOVE) { 304 invalidMoves2++; 305 if (invalidMoves2 > MAX_INVALID_MOVES) { 306 move2 = randomMove(); 307 check2 = checkMove(move2, board1, row2, col2); 308 } else { 309 move2 = randomMove(); 310 check2 = checkMove(move2, board1, row2, col2); 311 } 312 } 313 314 // Execute moves on the authoritative boards 315 int result1 = playMove(row1, col1, board2); 316 int result2 = playMove(row2, col2, board1); 317 318 // Update players with results 319 if (!updatePlayer(p1, row1, col1, result1)) { 320 p1.alive = false; 321 break; 322 } 323 if (!updatePlayer(p2, row2, col2, result2)) { 324 p2.alive = false; 325 break; 326 } 327 328 if (isASunk(result1)) shipsSunk1++; 329 if (isASunk(result2)) shipsSunk2++; 330 331 if (shipsSunk1 == 5 || shipsSunk2 == 5) { 332 break; 333 } 334 335 // Safety: prevent infinite games 336 if (moveCount > 200) { 337 break; 338 } 339 } 340 341 result.totalMoves += moveCount; 342 343 if (shipsSunk1 == 5 && shipsSunk2 == 5) { 344 result.ties++; 345 } else if (shipsSunk1 == 5) { 346 result.player1Wins++; 347 } else if (shipsSunk2 == 5) { 348 result.player2Wins++; 349 } else { 350 // Game ended abnormally (player died or max moves) 351 // Count as a tie or handle as you prefer 352 result.ties++; 353 } 354 } 355 356 // Clean up 357 quitPlayer(p1); 358 quitPlayer(p2); 359 killPlayer(p1); 360 killPlayer(p2); 361 362 // Output results in format Go expects 363 cout << "PLAYER1_WINS=" << result.player1Wins << endl; 364 cout << "PLAYER2_WINS=" << result.player2Wins << endl; 365 cout << "TIES=" << result.ties << endl; 366 cout << "TOTAL_MOVES=" << result.totalMoves << endl; 367 cout << "AVG_MOVES=" << (numGames > 0 ? result.totalMoves / numGames : 0) << endl; 368 369 return 0; 370}