summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTom Smeding <tom.smeding@gmail.com>2018-07-02 21:39:12 +0200
committerTom Smeding <tom.smeding@gmail.com>2018-07-02 21:39:12 +0200
commit303aac14a89cb6b6dc312b3bbadfb6c0051694d9 (patch)
treeabe190da9311e939878d15fdb471f5fbfd4215df
parent3c18dd46b52b1e45341df1f9423c1654462fbb09 (diff)
Add competition manager
-rw-r--r--competition/.gitignore4
-rw-r--r--competition/Makefile21
-rw-r--r--competition/main.cpp493
3 files changed, 518 insertions, 0 deletions
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 <iostream>
+#include <fstream>
+#include <sstream>
+#include <iomanip>
+#include <vector>
+#include <string>
+#include <stdexcept>
+#include <utility>
+#include <algorithm>
+#include <optional>
+#include <string_view>
+#include <cstdint>
+#include <cstring>
+#include <cstdlib>
+#include <cctype>
+#include <cassert>
+#include <errno.h>
+#include <unistd.h>
+#include <signal.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/fcntl.h>
+#include <sys/wait.h>
+#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<MatchResult> 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<string> 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<string> 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<MatchResult> 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<MatchResult> 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<string> 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<Move> 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<Player> &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] << " <players...>" << endl;
+ return 1;
+ }
+
+ vector<Player> players;
+ for (int i = 0; i < argc - 1; i++) {
+ players.emplace_back(argv[i+1]);
+ }
+
+ mkdirp(matchcachedir);
+ mkdirp(playerlogdir);
+ mkdirp(gamelogdir);
+
+ fullCompetition(players);
+
+ vector<pair<string, int>> 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<string, int> &a, const pair<string, int> &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;
+ }
+}