#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../board.h" using namespace std; static const char *matchcachedir = "comp_cache"; static const char *playerlogdir = "comp_playerlogs"; static const char *gamelogdir = "comp_gamelogs"; static const int num_matches = 5; static const int timeout_msec = 60000; static char hexchar(int n) { return "0123456789ABCDEF"[n]; } static string makeSafe(const string_view str) { string res; res.reserve(str.size() + 3); for (char c : str) { if (isalnum(c)) { res += c; } else { res += '_'; res += hexchar((c >> 4) & 0xf); res += hexchar(c & 0xf); } } return res; } // Only creates a single component static void mkdirp(const string_view name) { if (mkdir(name.data(), 0755) < 0) { if (errno != EEXIST) { perror("mkdir"); exit(1); } } } static int64_t gettimestamp() { struct timeval tv; gettimeofday(&tv, nullptr); return tv.tv_sec * 1000000LL + tv.tv_usec; } struct Player; // 'win' is for player 1 struct MatchResult { enum class Win { win, loss, tie, timeout }; Win win; size_t ms1 = 0, ms2 = 0; int score() const; string describe(const Player &p1, const Player &p2) const; MatchResult inverted() const; }; struct Player { string fname, safename; vector results; Player(const string &fname) : fname(fname), safename(makeSafe(fname)) {} }; int MatchResult::score() const { switch (win) { case Win::win: return 3; case Win::loss: return 1; case Win::tie: return 2; case Win::timeout: return 0; default: assert(false); } } string MatchResult::describe(const Player &p1, const Player &p2) const { string prefix; switch (win) { case Win::win: prefix = p1.fname + " won: "; break; case Win::loss: prefix = p2.fname + " won: "; break; case Win::tie: prefix = "tie: "; break; case Win::timeout: prefix = "timeout: "; break; } return prefix + to_string(score()) + "-" + to_string(inverted().score()); } MatchResult MatchResult::inverted() const { MatchResult r; switch (win) { case Win::win: r.win = Win::loss; break; case Win::loss: r.win = Win::win; break; case Win::tie: r.win = Win::tie; break; case Win::timeout: r.win = Win::timeout; break; } r.ms2 = ms1; r.ms1 = ms2; return r; } class StopCompetitionError : public runtime_error { public: StopCompetitionError() : runtime_error("StopCompetitionError") {} explicit StopCompetitionError(const string &what_arg) : runtime_error(what_arg) {} explicit StopCompetitionError(const char *what_arg) : runtime_error(what_arg) {} }; class Process { string execname; optional stderrRedirect; pid_t pid = -1; int infd = -1, outfd = -1; string readBuf; public: Process(const string_view execname) : execname(execname) {} void redirectStderr(const string_view fname) { stderrRedirect = fname; } void run() { int stderrfd = -1; if (stderrRedirect) { stderrfd = open(stderrRedirect->data(), O_WRONLY|O_CREAT|O_TRUNC, 0644); if (stderrfd < 0) { perror("open"); cout << endl << "ERROR: Cannot open player log file '" << *stderrRedirect << "'" << endl; throw StopCompetitionError(); } } int pipefds[2]; if (pipe(pipefds) < 0) { perror("pipe"); exit(1); } infd = pipefds[1]; int child_in = pipefds[0]; if (pipe(pipefds) < 0) { perror("pipe"); exit(1); } outfd = pipefds[0]; int child_out = pipefds[1]; pid = fork(); if (pid < 0) { perror("fork"); exit(1); } if (pid == 0) { if (stderrRedirect) dup2(stderrfd, STDERR_FILENO); dup2(child_in, STDIN_FILENO); dup2(child_out, STDOUT_FILENO); close(infd); close(outfd); execlp(execname.data(), execname.data(), NULL); cerr << endl << "ERROR: Error executing player file '" << execname << "'" << endl; exit(255); } if (stderrfd >= 0) close(stderrfd); close(child_in); close(child_out); } void wait() { while (true) { int status; if (waitpid(pid, &status, 0) < 0) { if (errno == EINTR) continue; perror("waitpid"); break; } if (WIFEXITED(status)) break; } } void stop() { if (pid != -1) kill(pid, SIGSTOP); } void unStop() { if (pid != -1) kill(pid, SIGCONT); } bool writeLine(const string_view line) { string str; str.reserve(line.size() + 1); str += line; str += '\n'; size_t cursor = 0; while (cursor < str.size()) { ssize_t nw = write(infd, str.data() + cursor, str.size() - cursor); if (nw < 0) { if (errno == EINTR) continue; perror("write"); return false; } cursor += nw; } return true; } optional readLine() { size_t idx = readBuf.find('\n'); if (idx != string::npos) { string res = readBuf.substr(0, idx); readBuf = readBuf.substr(idx + 1); return res; } while (true) { string s(1024, '\0'); ssize_t nr = read(outfd, &s[0], s.size()); if (nr < 0) { if (errno == EINTR) continue; perror("read"); return nullopt; } s.resize(nr); idx = s.find('\n'); if (idx != string::npos) { string res = readBuf + s.substr(0, idx); readBuf = s.substr(idx + 1); return res; } readBuf += s; } } void terminate() { if (pid != -1) kill(pid, SIGTERM); } }; static string gameCodeName(const Player &p1, const Player &p2, int index) { return p1.safename + "-" + p2.safename + "-" + to_string(index); } static string playerLogPath(const Player &p1, const Player &p2, int index, int who) { return string(playerlogdir) + "/" + gameCodeName(p1, p2, index) + "-" + to_string(who) + ".txt"; } static string matchCachePath(const Player &p1, const Player &p2, int index) { return string(matchcachedir) + "/" + gameCodeName(p1, p2, index) + ".txt"; } static string gameLogPath(const Player &p1, const Player &p2, int index) { return string(gamelogdir) + "/" + gameCodeName(p1, p2, index) + ".txt"; } static optional readMatchCache(const Player &p1, const Player &p2, int index) { ifstream f(matchCachePath(p1, p2, index)); if (!f) return nullopt; MatchResult mres; string word; f >> word >> mres.ms1 >> mres.ms2; if (!f) return nullopt; if (word == "win") mres.win = MatchResult::Win::win; else if (word == "loss") mres.win = MatchResult::Win::loss; else if (word == "tie") mres.win = MatchResult::Win::tie; else if (word == "timeout") mres.win = MatchResult::Win::timeout; else return nullopt; return mres; } static void writeMatchCache(const Player &p1, const Player &p2, int index, const MatchResult &mres) { string path = matchCachePath(p1, p2, index); ofstream f(path); if (!f) { cout << endl << "ERROR: Cannot open match cache file '" << path << "'" << endl; throw StopCompetitionError(); } switch (mres.win) { case MatchResult::Win::win: f << "win"; break; case MatchResult::Win::loss: f << "loss"; break; case MatchResult::Win::tie: f << "tie"; break; case MatchResult::Win::timeout: f << "timeout"; break; } f << ' ' << mres.ms1 << ' ' << mres.ms2 << endl; } static void recordResult(Player &p1, Player &p2, const MatchResult &result) { p1.results.push_back(result); p2.results.push_back(result.inverted()); } static void playMatch(Player &p1, Player &p2, int index) { cout << p1.fname << " - " << p2.fname << ": " << flush; if (optional optres = readMatchCache(p1, p2, index)) { cout << optres->describe(p1, p2) << " (cached)" << endl; recordResult(p1, p2, *optres); return; } MatchResult mres; string gamelog_path = gameLogPath(p1, p2, index); ofstream gamelog(gamelog_path); if (!gamelog) { cout << endl << "ERROR opening game log file " << gamelog_path << endl; throw StopCompetitionError(); } gamelog << "Player 1: " << p1.fname << "\n" << "Player 2: " << p2.fname << "\n\n" << flush; Process procs[2] = {Process(p1.fname), Process(p2.fname)}; procs[0].redirectStderr(playerLogPath(p1, p2, index, 1)); procs[1].redirectStderr(playerLogPath(p1, p2, index, 2)); procs[0].run(); procs[1].run(); Board board = Board::makeInitial(); string lastMove = "Start"; while (true) { for (int i = 0; i < 2; i++) { if (!procs[i].writeLine(lastMove)) { cout << endl << "ERROR writing move to player " << i+1 << endl; throw StopCompetitionError(); } procs[i].unStop(); int64_t start = gettimestamp(); optional oline = procs[i].readLine(); if (!oline) { cout << endl << "ERROR reading move from player " << i+1 << endl; throw StopCompetitionError(); } lastMove = *oline; (i == 0 ? mres.ms1 : mres.ms2) += gettimestamp() - start; procs[i].stop(); gamelog << "P" << i+1 << ": " << lastMove << endl; if (mres.ms1 / 1000 > timeout_msec || mres.ms2 / 1000 > timeout_msec) { mres.win = MatchResult::Win::timeout; goto match_done; } optional omv = Move::parse(lastMove); if (!omv) { cout << endl << "ERROR in player " << i+1 << ": unreadable move '" << lastMove << "'" << endl; throw StopCompetitionError(); } if (!board.isValid(*omv, i == 0 ? -1 : 1)) { cout << endl << "ERROR in player " << i+1 << ": invalid move " << *omv << endl; throw StopCompetitionError(); } int outcome = board.applyCW(*omv); if (outcome != 0) { if (outcome == 1) mres.win = MatchResult::Win::loss; else if (outcome == -1) mres.win = MatchResult::Win::win; else assert(false); goto match_done; } stringstream ss; ss << *omv; lastMove = ss.str(); } } match_done: for (int i = 0; i < 2; i++) { bool success = procs[i].writeLine("Stop"); procs[i].unStop(); if (success) usleep(10000); procs[i].terminate(); } for (int i = 0; i < 2; i++) { procs[i].wait(); } cout << mres.describe(p1, p2) << endl; gamelog << "\nResult: " << mres.describe(p1, p2) << "\n" << "P1 took " << mres.ms1 / 1000000.0 << " seconds\n" << "P2 took " << mres.ms2 / 1000000.0 << " seconds\n" << flush; writeMatchCache(p1, p2, index, mres); recordResult(p1, p2, mres); } static void playerPit(Player &p1, Player &p2) { for (int i = 0; i < num_matches; i++) { playMatch(p1, p2, i + 1); } } static void fullCompetition(vector &players) { for (size_t p1i = 0; p1i < players.size(); p1i++) { for (size_t p2i = p1i + 1; p2i < players.size(); p2i++) { playerPit(players[p1i], players[p2i]); playerPit(players[p2i], players[p1i]); } } } int main(int argc, char **argv) { if (argc <= 1) { cerr << "Usage: " << argv[0] << " " << endl; return 1; } vector players; for (int i = 0; i < argc - 1; i++) { players.emplace_back(argv[i+1]); } mkdirp(matchcachedir); mkdirp(playerlogdir); mkdirp(gamelogdir); fullCompetition(players); vector> scores; for (const Player &player : players) { int score = 0; for (const MatchResult &result : player.results) { score += result.score(); } scores.emplace_back(player.fname, score); } sort(scores.begin(), scores.end(), [](const pair &a, const pair &b) { return a.second > b.second; }); size_t maxlen = strlen("Player"); for (const Player &player : players) { maxlen = max(maxlen, player.fname.size()); } cout << endl << setw(maxlen) << "Player" << " | " << "Score" << endl; cout << string(maxlen, '-') << "-+------" << endl; for (const auto &p : scores) { cout << setw(maxlen) << p.first << " | " << p.second << endl; } }