From 303aac14a89cb6b6dc312b3bbadfb6c0051694d9 Mon Sep 17 00:00:00 2001 From: Tom Smeding Date: Mon, 2 Jul 2018 21:39:12 +0200 Subject: Add competition manager --- competition/.gitignore | 4 + competition/Makefile | 21 +++ competition/main.cpp | 493 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 518 insertions(+) create mode 100644 competition/.gitignore create mode 100644 competition/Makefile create mode 100644 competition/main.cpp diff --git a/competition/.gitignore b/competition/.gitignore new file mode 100644 index 0000000..95f3130 --- /dev/null +++ b/competition/.gitignore @@ -0,0 +1,4 @@ +comp_cache/ +comp_gamelogs/ +comp_playerlogs/ +competition diff --git a/competition/Makefile b/competition/Makefile new file mode 100644 index 0000000..a39c58d --- /dev/null +++ b/competition/Makefile @@ -0,0 +1,21 @@ +CXX = g++ +CXXFLAGS = -Wall -Wextra -O3 -std=c++17 -fwrapv -flto + +TARGET = competition + +SOURCES := $(wildcard *.cpp) ../board.cpp +HEADERS := $(wildcard *.h) ../board.h + +.PHONY: all clean + +all: $(TARGET) + +clean: + rm -f $(TARGET) *.o + + +competition: $(SOURCES:.cpp=.o) + $(CXX) $(CXXFLAGS) $^ -o $@ + +%.o: %.cpp $(HEADERS) + $(CXX) $(CXXFLAGS) -c -o $@ $< diff --git a/competition/main.cpp b/competition/main.cpp new file mode 100644 index 0000000..90579db --- /dev/null +++ b/competition/main.cpp @@ -0,0 +1,493 @@ +#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; + } +} -- cgit v1.2.3