summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTom Smeding <tom.smeding@gmail.com>2018-08-20 21:53:38 +0200
committerTom Smeding <tom.smeding@gmail.com>2018-08-20 21:53:38 +0200
commit1c42efb1b116287949e59370198cad2df465835d (patch)
tree6c191f0a629b23566763827e73071c523f0d3e2b
Initial
-rw-r--r--.gitignore5
-rw-r--r--Makefile21
-rw-r--r--error.cpp9
-rw-r--r--error.h12
-rw-r--r--job.cpp62
-rw-r--r--job.h40
-rw-r--r--main.cpp386
-rw-r--r--multilog.cpp68
-rw-r--r--multilog.h32
-rw-r--r--params.h15
-rw-r--r--process.cpp152
-rw-r--r--process.h36
-rw-r--r--referee.cpp92
-rw-r--r--referee.h45
14 files changed, 975 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4791239
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+comp_cache/
+comp_gamelogs/
+comp_playerlogs/
+competition
+*.o
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..3edc3f0
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,21 @@
+CXX = g++
+CXXFLAGS = -Wall -Wextra -O2 -std=c++17 -fwrapv -pthread
+
+TARGET = competition
+
+SOURCES := $(wildcard *.cpp)
+HEADERS := $(wildcard *.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/error.cpp b/error.cpp
new file mode 100644
index 0000000..4c737c2
--- /dev/null
+++ b/error.cpp
@@ -0,0 +1,9 @@
+#include "error.h"
+
+
+StopCompetitionError::StopCompetitionError()
+ : runtime_error("StopCompetitionError") {}
+StopCompetitionError::StopCompetitionError(const string &what_arg)
+ : runtime_error(what_arg) {}
+StopCompetitionError::StopCompetitionError(const char *what_arg)
+ : runtime_error(what_arg) {}
diff --git a/error.h b/error.h
new file mode 100644
index 0000000..403f81b
--- /dev/null
+++ b/error.h
@@ -0,0 +1,12 @@
+#include <stdexcept>
+#include <string>
+
+using namespace std;
+
+
+class StopCompetitionError : public runtime_error {
+public:
+ StopCompetitionError();
+ explicit StopCompetitionError(const string &what_arg);
+ explicit StopCompetitionError(const char *what_arg);
+};
diff --git a/job.cpp b/job.cpp
new file mode 100644
index 0000000..90fede2
--- /dev/null
+++ b/job.cpp
@@ -0,0 +1,62 @@
+#include <iostream>
+#include <chrono>
+#include <cassert>
+#include "job.h"
+
+void Scheduler::workerEntry() {
+ while (true) {
+ Job *job = nullptr;
+
+ {
+ lock_guard commMutGuard(commMut);
+ if (jobs.size() > 0) {
+ job = jobs.front();
+ jobs.pop();
+ }
+ }
+
+ if (job) {
+ job->callback();
+ } else if (finishFlag) {
+ break;
+ } else {
+ this_thread::sleep_for(chrono::milliseconds(100));
+ }
+ }
+}
+
+Scheduler::Scheduler(int nthreads)
+ : nthreads(nthreads) {
+
+ assert(nthreads > 0);
+
+ workers.reserve(nthreads);
+ for (int i = 0; i < nthreads; i++) {
+ workers.emplace_back([this]() { workerEntry(); });
+ }
+}
+
+Scheduler::~Scheduler() {
+ finish();
+}
+
+void Scheduler::submit(const function<void()> &func) {
+ Job *job = new Job(func);
+ lock_guard commMutGuard(commMut);
+ jobs.push(job);
+}
+
+void Scheduler::finish() {
+ if (hasJoined) return;
+
+ {
+ lock_guard commMutGuard(commMut);
+ finishFlag = true;
+ }
+
+ for (int i = 0; i < nthreads; i++) {
+ workers[i].join();
+ }
+
+ hasJoined = true;
+}
diff --git a/job.h b/job.h
new file mode 100644
index 0000000..95da7c7
--- /dev/null
+++ b/job.h
@@ -0,0 +1,40 @@
+#pragma once
+
+#include <functional>
+#include <queue>
+#include <vector>
+#include <thread>
+#include <mutex>
+
+using namespace std;
+
+
+class Scheduler {
+ struct Job {
+ function<void()> callback;
+
+ Job(const function<void()> callback)
+ : callback(callback) {}
+ };
+
+ queue<Job*> jobs;
+ bool finishFlag = false;
+ mutex commMut;
+
+ bool hasJoined = false;
+
+ vector<thread> workers;
+
+ void workerEntry();
+
+public:
+ const int nthreads;
+
+ Scheduler(int nthreads);
+ ~Scheduler();
+
+ // func is run in child thread
+ void submit(const function<void()> &func);
+
+ void finish();
+};
diff --git a/main.cpp b/main.cpp
new file mode 100644
index 0000000..6e363e7
--- /dev/null
+++ b/main.cpp
@@ -0,0 +1,386 @@
+#include <iostream>
+#include <fstream>
+#include <sstream>
+#include <iomanip>
+#include <vector>
+#include <string>
+#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 <sys/stat.h>
+#include <sys/time.h>
+#include "job.h"
+#include "process.h"
+#include "error.h"
+#include "multilog.h"
+#include "referee.h"
+#include "params.h"
+
+using namespace std;
+
+
+// TODO no globals
+static MultiLog gMultiLog;
+
+
+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 fileLastModified(const string_view fname) {
+ struct stat st;
+ if (stat(fname.data(), &st) < 0) {
+ if (errno == EEXIST) return 0;
+ perror("stat");
+ exit(1);
+ }
+ return st.st_mtim.tv_sec * 1000000LL + st.st_mtim.tv_nsec / 1000;
+}
+
+static int64_t gettimestamp() {
+ struct timeval tv;
+ gettimeofday(&tv, nullptr);
+ return tv.tv_sec * 1000000LL + tv.tv_usec;
+}
+
+struct Player;
+
+// Methods represent results for player 1, unless inverted
+struct MatchResult {
+ enum class Win {
+ win, loss, tie, timeout, crash
+ };
+
+ enum class Status { // crash is not used yet
+ ok, timeout, crash
+ };
+
+ Status status;
+ int sc1, sc2;
+ size_t ms1 = 0, ms2 = 0;
+
+ int score() const;
+ Win getWin() const;
+ string describe(const Player &p1, const Player &p2) const;
+ MatchResult inverted() const;
+};
+
+struct Player {
+ string fname, safename;
+ vector<MatchResult> results;
+ int64_t lastModified;
+
+ Player(const string &fname)
+ : fname(fname), safename(makeSafe(fname)),
+ lastModified(fileLastModified(fname)) {}
+};
+
+int MatchResult::score() const {
+ return sc1;
+}
+
+MatchResult::Win MatchResult::getWin() const {
+ switch (status) {
+ case Status::ok:
+ if (sc1 > sc2) return Win::win;
+ else if (sc1 == sc2) return Win::tie;
+ else return Win::loss;
+
+ case Status::timeout:
+ return Win::timeout;
+
+ case Status::crash:
+ return Win::crash;
+
+ default:
+ assert(false);
+ }
+}
+
+string MatchResult::describe(const Player &p1, const Player &p2) const {
+ string prefix;
+ switch (getWin()) {
+ 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;
+ case Win::crash: prefix = "crash: "; break;
+ }
+ return prefix + to_string(sc1) + "-" + to_string(sc2);
+}
+
+MatchResult MatchResult::inverted() const {
+ MatchResult r;
+ r.status = status;
+ r.sc2 = sc1; r.sc1 = sc2;
+ r.ms2 = ms1; r.ms1 = ms2;
+ return r;
+}
+
+struct Params {
+ string refereePath;
+};
+
+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) {
+ string path = matchCachePath(p1, p2, index);
+ int64_t cacheStamp = fileLastModified(path);
+
+ ifstream f(path);
+ if (!f) return nullopt;
+
+ if (p1.lastModified > cacheStamp || p2.lastModified > cacheStamp) {
+ cerr << "(cache ignored because players newer) " << cacheStamp << " " << p1.lastModified << " " << p2.lastModified << endl;
+ return nullopt;
+ }
+
+ MatchResult mres;
+ string word;
+ f >> word >> mres.sc1 >> mres.sc2 >> mres.ms1 >> mres.ms2;
+ if (!f) {
+ cerr << "(cache ignored because error reading file)" << endl;
+ return nullopt;
+ }
+
+ if (word == "ok") mres.status = MatchResult::Status::ok;
+ else if (word == "timeout") mres.status = MatchResult::Status::timeout;
+ else if (word == "crash") mres.status = MatchResult::Status::crash;
+ 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.status) {
+ case MatchResult::Status::ok: f << "ok"; break;
+ case MatchResult::Status::timeout: f << "timeout"; break;
+ case MatchResult::Status::crash: f << "crash"; break;
+ }
+
+ f << ' ' << mres.sc1 << ' ' << mres.sc2;
+ 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, const Params &params) {
+ int logId = gMultiLog.add(p1.fname + " - " + p2.fname + ": ");
+
+ if (optional<MatchResult> optres = readMatchCache(p1, p2, index)) {
+ gMultiLog.append(logId, optres->describe(p1, p2) + " (cached)");
+ gMultiLog.complete(logId);
+ recordResult(p1, p2, *optres);
+ return;
+ }
+
+ MatchResult mres;
+
+
+ string gamelog_path = gameLogPath(p1, p2, index);
+ ofstream gamelog(gamelog_path);
+ if (!gamelog) {
+ cout << "ERROR opening game log file " << gamelog_path << endl;
+ throw StopCompetitionError();
+ }
+
+ Referee referee(params.refereePath, {p1.fname, p2.fname});
+
+ 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();
+
+ string lastMove = "Start";
+
+ while (true) {
+ for (int i = 0; i < 2; i++) {
+ if (!procs[i].writeLine(lastMove)) {
+ cout << "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 << "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.status = MatchResult::Status::timeout;
+ mres.sc1 = mres.sc2 = 0;
+ goto match_done;
+ }
+
+ if (!referee.moveValid(i, lastMove)) {
+ cout << "ERROR in player " << i+1 << ": invalid move '" << lastMove << "'" << endl;
+ throw StopCompetitionError();
+ }
+
+ if (referee.gameEnded()) {
+ goto match_done;
+ }
+ }
+ }
+
+match_done:
+ optional<vector<int>> oscores = referee.getScores();
+ assert(oscores);
+ mres.status = MatchResult::Status::ok;
+ mres.sc1 = oscores->at(0);
+ mres.sc2 = oscores->at(1);
+
+ 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();
+ }
+
+ gMultiLog.append(logId, mres.describe(p1, p2));
+ gMultiLog.complete(logId);
+
+ 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(Scheduler &scheduler, Player &p1, Player &p2, const Params &params) {
+ for (int i = 0; i < num_matches; i++) {
+ scheduler.submit([&p1, &p2, i, params]() { playMatch(p1, p2, i + 1, params); });
+ }
+}
+
+static void fullCompetition(Scheduler &scheduler, vector<Player> &players, const Params &params) {
+ for (size_t p1i = 0; p1i < players.size(); p1i++) {
+ for (size_t p2i = p1i + 1; p2i < players.size(); p2i++) {
+ playerPit(scheduler, players[p1i], players[p2i], params);
+ playerPit(scheduler, players[p2i], players[p1i], params);
+ }
+ }
+}
+
+int main(int argc, char **argv) {
+ if (argc < 3) {
+ cerr << "Usage: " << argv[0] << " <referee> <players...>" << endl;
+ return 1;
+ }
+
+ Params params;
+ params.refereePath = argv[1];
+
+ vector<Player> players;
+ for (int i = 2; i < argc; i++) {
+ players.emplace_back(argv[i]);
+ }
+
+
+ mkdirp(matchcachedir);
+ mkdirp(playerlogdir);
+ mkdirp(gamelogdir);
+
+ Scheduler scheduler(num_threads);
+ fullCompetition(scheduler, players, params);
+ scheduler.finish();
+
+ 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;
+ }
+}
diff --git a/multilog.cpp b/multilog.cpp
new file mode 100644
index 0000000..d2dce10
--- /dev/null
+++ b/multilog.cpp
@@ -0,0 +1,68 @@
+#include <iostream>
+#include <string>
+#include <cassert>
+#include "multilog.h"
+#include "params.h"
+
+
+static int uniqid() {
+ static int i = 0;
+ return i++;
+}
+
+
+MultiLog::Item::Item()
+ : id(uniqid()) {}
+
+int MultiLog::add(const string_view prefix) {
+ lock_guard guard(mut);
+
+ assert(prefix.find('\n') == string::npos);
+
+ items.emplace_back();
+ items.back().line = prefix;
+ if (multilog_fancy) {
+ cout << prefix << endl;
+ } else {
+ cout << "[" << items.back().id << "] " << prefix << endl;
+ }
+ return items.back().id;
+}
+
+void MultiLog::append(int id, const string_view text) {
+ lock_guard guard(mut);
+
+ assert(text.find('\n') == string::npos);
+
+ size_t idx = findId(id);
+ items[idx].line += text;
+ if (multilog_fancy) {
+ redrawLine(idx);
+ } else {
+ cout << "[" << items[idx].id << "]... " << text << endl;
+ }
+}
+
+void MultiLog::complete(int id) {
+ lock_guard guard(mut);
+
+ size_t idx = findId(id);
+ items[idx].complete = true;
+ size_t nextIdx;
+ for (nextIdx = 0; nextIdx < items.size(); nextIdx++) {
+ if (!items[nextIdx].complete) break;
+ }
+ if (nextIdx > 0) items.erase(items.begin(), items.begin() + (nextIdx - 1));
+}
+
+size_t MultiLog::findId(int id) {
+ for (int i = 0; i < (int)items.size(); i++) {
+ if (items[i].id == id) return i;
+ }
+ assert(false);
+}
+
+void MultiLog::redrawLine(size_t idx) {
+ size_t offset = items.size() - idx;
+ cout << "\r\x1B[" << offset << "A\x1B[K" << items[idx].line << "\r\x1B[" << offset << "B" << flush;
+}
diff --git a/multilog.h b/multilog.h
new file mode 100644
index 0000000..ba0ff4d
--- /dev/null
+++ b/multilog.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include <vector>
+#include <string_view>
+#include <mutex>
+
+using namespace std;
+
+
+// Access is thread-safe
+class MultiLog {
+ struct Item {
+ int id;
+ string line;
+ bool complete = false;
+
+ Item();
+ };
+
+ mutex mut;
+ vector<Item> items;
+
+ // Methods assume mutex is taken.
+ size_t findId(int id);
+ void redrawLine(size_t idx);
+
+public:
+ // Returns id of new item
+ int add(const string_view prefix);
+ void append(int id, const string_view text);
+ void complete(int id);
+};
diff --git a/params.h b/params.h
new file mode 100644
index 0000000..c3e6d5e
--- /dev/null
+++ b/params.h
@@ -0,0 +1,15 @@
+#pragma once
+
+
+// Actual variable needs to be const as well to enable C++ default-internal linkage
+const char *const matchcachedir = "comp_cache";
+const char *const playerlogdir = "comp_playerlogs";
+const char *const gamelogdir = "comp_gamelogs";
+
+const int num_threads = 2;
+
+const int num_matches = 5;
+const int timeout_msec = 60000;
+
+const bool multilog_fancy = true;
+const bool referee_verbose = false;
diff --git a/process.cpp b/process.cpp
new file mode 100644
index 0000000..00a2e08
--- /dev/null
+++ b/process.cpp
@@ -0,0 +1,152 @@
+#include <iostream>
+#include <cstdlib>
+#include <fcntl.h>
+#include <sys/wait.h>
+#include "process.h"
+#include "error.h"
+
+
+Process::Process(const string_view execname)
+ : execname(execname) {}
+
+void Process::redirectStderr(const string_view fname) {
+ stderrRedirect = fname;
+}
+
+void Process::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 Process::wait() {
+ while (true) {
+ int status;
+ if (waitpid(pid, &status, 0) < 0) {
+ if (errno == EINTR) continue;
+ if (errno == ECHILD) break;
+ perror("waitpid");
+ break;
+ }
+ if (WIFEXITED(status)) {
+ pid = -1;
+ break;
+ }
+ }
+}
+
+void Process::stop() {
+ if (pid != -1) kill(pid, SIGSTOP);
+}
+
+void Process::unStop() {
+ if (pid != -1) kill(pid, SIGCONT);
+}
+
+bool Process::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> Process::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;
+ }
+ if (nr == 0) { // EOF
+ 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 Process::terminate() {
+ // TODO: send only SIGTERM, then wait a little while, then send
+ // SIGKILL if necessary
+ if (pid != -1) {
+ kill(pid, SIGKILL); // force kill
+ pid = -1;
+ }
+}
+
+pid_t Process::getPid() const {
+ return pid;
+}
diff --git a/process.h b/process.h
new file mode 100644
index 0000000..900a0b6
--- /dev/null
+++ b/process.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include <string>
+#include <optional>
+#include <string_view>
+#include <unistd.h>
+
+using namespace std;
+
+
+class Process {
+ string execname;
+ optional<string> stderrRedirect;
+ pid_t pid = -1;
+ int infd = -1, outfd = -1;
+
+ string readBuf;
+
+public:
+ Process(const string_view execname);
+ Process(const Process&) = delete;
+ Process(Process&&) = default;
+
+ void redirectStderr(const string_view fname);
+
+ void run();
+ void wait();
+ void stop();
+ void unStop();
+ void terminate();
+
+ pid_t getPid() const;
+
+ bool writeLine(const string_view line);
+ optional<string> readLine();
+};
diff --git a/referee.cpp b/referee.cpp
new file mode 100644
index 0000000..3f15b6b
--- /dev/null
+++ b/referee.cpp
@@ -0,0 +1,92 @@
+#include <iostream>
+#include <cstdlib>
+#include "referee.h"
+#include "params.h"
+
+
+Referee::Referee(const string_view execname, const vector<string> &players)
+ : proc(execname) {
+
+ proc.run();
+
+ if (referee_verbose) {
+ cout << "REF(" << proc.getPid() << ") starting for:";
+ for (const string &p : players) cout << " " << p;
+ cout << endl;
+ }
+
+ proc.writeLine(to_string(players.size()));
+ for (const string &p : players) {
+ proc.writeLine(p);
+ }
+}
+
+Referee::~Referee() {
+ if (referee_verbose) {
+ cout << "REF(" << proc.getPid() << ") stopping" << endl;
+ }
+ proc.wait();
+ proc.terminate();
+}
+
+bool Referee::moveValid(int player, const string_view line) {
+ string s = to_string(player) + " ";
+ s.append(line);
+
+ if (referee_verbose) {
+ cout << "REF(" << proc.getPid() << ") write <" << s << ">" << endl;
+ }
+
+ proc.writeLine(s);
+
+ optional<string> response = proc.readLine();
+ if (response) {
+ if (referee_verbose) {
+ cout << "REF(" << proc.getPid() << ") read <" << *response << ">" << endl;
+ }
+
+ if (*response == "valid end") {
+ isEnd = true;
+ readScores();
+ return true;
+ } else if (*response == "valid") {
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ if (referee_verbose) {
+ cout << "REF(" << proc.getPid() << ") EOF!" << endl;
+ }
+ return false;
+ }
+}
+
+bool Referee::gameEnded() {
+ return isEnd;
+}
+
+void Referee::readScores() {
+ optional<string> line = proc.readLine();
+ if (!line) {
+ cerr << "Referee stopped before providing scores" << endl;
+ exit(1);
+ }
+
+ if (referee_verbose) {
+ cout << "REF(" << proc.getPid() << ") readScores <" << *line << ">" << endl;
+ }
+
+ size_t cursor = 0;
+ while (cursor < line->size()) {
+ size_t idx = line->find(' ', cursor);
+ if (idx == string::npos) idx = line->size();
+ scores.push_back(stoi(line->substr(cursor, idx - cursor)));
+ cursor = idx + 1;
+ }
+}
+
+optional<vector<int>> Referee::getScores() {
+ if (isEnd) return scores;
+ else return nullopt;
+}
diff --git a/referee.h b/referee.h
new file mode 100644
index 0000000..5f278c5
--- /dev/null
+++ b/referee.h
@@ -0,0 +1,45 @@
+#pragma once
+
+#include <vector>
+#include <string>
+#include <string_view>
+#include <optional>
+#include "process.h"
+
+using namespace std;
+
+
+class Referee {
+ Process proc;
+ bool isEnd = false;
+ vector<int> scores;
+
+ void readScores();
+
+public:
+ Referee(const string_view execname, const vector<string> &players);
+ ~Referee();
+
+ // player: 0 = first player, 1 = second player
+ bool moveValid(int player, const string_view line);
+
+ bool gameEnded();
+ optional<vector<int>> getScores();
+};
+
+/*
+Referee protocol:
+
+At startup, the referee receives a line containing the number of players in the
+game, followed by that number of lines containing the players' names.
+
+Then, it repeatedly receives a line on stdin of the form '<player> <line>',
+where <player> is the index of the player in the game (0 is first, 1 is second,
+etc.) and <line> is the line as written out by the player. The referee should
+then write a line to stdout that is equal to either 'valid', 'invalid', or
+'valid end'. 'valid' and 'invalid' indicate a valid and invalid move, while
+'valid end' indicates a valid move that ends the game. After writing the line
+'valid end', the referee should write a line containing as many integers
+(separated by spaces) as there are players, giving their scores at the end of
+the game.
+*/