a geicko-2 based round robin ranking system designed to test c++ battleship submissions
battleship.dunkirk.sh
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}