summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTom Smeding <tom.smeding@gmail.com>2018-08-20 23:51:16 +0200
committerTom Smeding <tom.smeding@gmail.com>2018-08-20 23:52:03 +0200
commit57a1091dfb2c6403cbe63c7ddca8acc7e3d0f940 (patch)
treea997ed523a853d9d5fff7d5b90b12a180b00a231
parentf0b6d1b9c46578183b427bcec4bbe99ab10c7b97 (diff)
Switch to https://git.tomsmeding.com/competitionHEADmaster
-rw-r--r--.gitignore1
-rw-r--r--competition/.gitignore4
-rw-r--r--competition/Makefile21
-rw-r--r--competition/error.cpp9
-rw-r--r--competition/error.h12
-rw-r--r--competition/job.cpp51
-rw-r--r--competition/job.h39
-rw-r--r--competition/main.cpp368
-rw-r--r--competition/multilog.cpp57
-rw-r--r--competition/multilog.h32
-rw-r--r--competition/process.cpp136
-rw-r--r--competition/process.h32
-rw-r--r--referee/.gitignore1
-rw-r--r--referee/Makefile13
-rw-r--r--referee/referee.cpp74
15 files changed, 89 insertions, 761 deletions
diff --git a/.gitignore b/.gitignore
index 87e54c2..7efa90e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
main
*.o
+competition/
diff --git a/competition/.gitignore b/competition/.gitignore
deleted file mode 100644
index 95f3130..0000000
--- a/competition/.gitignore
+++ /dev/null
@@ -1,4 +0,0 @@
-comp_cache/
-comp_gamelogs/
-comp_playerlogs/
-competition
diff --git a/competition/Makefile b/competition/Makefile
deleted file mode 100644
index 8b2641c..0000000
--- a/competition/Makefile
+++ /dev/null
@@ -1,21 +0,0 @@
-CXX = g++
-CXXFLAGS = -Wall -Wextra -O3 -std=c++17 -fwrapv -flto -pthread
-
-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/error.cpp b/competition/error.cpp
deleted file mode 100644
index 4c737c2..0000000
--- a/competition/error.cpp
+++ /dev/null
@@ -1,9 +0,0 @@
-#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/competition/error.h b/competition/error.h
deleted file mode 100644
index 403f81b..0000000
--- a/competition/error.h
+++ /dev/null
@@ -1,12 +0,0 @@
-#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/competition/job.cpp b/competition/job.cpp
deleted file mode 100644
index 043074e..0000000
--- a/competition/job.cpp
+++ /dev/null
@@ -1,51 +0,0 @@
-#include <chrono>
-#include <cassert>
-#include "job.h"
-
-void Scheduler::workerEntry() {
- while (true) {
- Job *job = nullptr;
-
- {
- lock_guard commMutGuard(commMut);
- if (terminateFlag) break;
- if (jobs.size() > 0) {
- job = jobs.front();
- jobs.pop();
- }
- }
-
- if (job) {
- job->callback();
- } 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() {
- for (int i = 0; i < nthreads; i++) {
- workers[i].join();
- }
-}
diff --git a/competition/job.h b/competition/job.h
deleted file mode 100644
index 9dba159..0000000
--- a/competition/job.h
+++ /dev/null
@@ -1,39 +0,0 @@
-#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 terminateFlag = false;
- mutex commMut;
-
- vector<thread> workers;
-
- void workerEntry();
-
-public:
- const int nthreads;
-
- Scheduler(int nthreads);
- ~Scheduler();
-
- // func is run in child thread
- // doneCallback is run in host thread
- void submit(const function<void()> &func);
-
- void finish();
-};
diff --git a/competition/main.cpp b/competition/main.cpp
deleted file mode 100644
index 604263c..0000000
--- a/competition/main.cpp
+++ /dev/null
@@ -1,368 +0,0 @@
-#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 "../board.h"
-#include "job.h"
-#include "process.h"
-#include "error.h"
-#include "multilog.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;
-
-
-// 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;
-}
-
-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;
- int64_t lastModified;
-
- Player(const string &fname)
- : fname(fname), safename(makeSafe(fname)),
- lastModified(fileLastModified(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;
-}
-
-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);
- ifstream f(path);
- if (!f) return nullopt;
-
- int64_t cacheStamp = fileLastModified(path);
- if (p1.lastModified > cacheStamp || p2.lastModified > cacheStamp) 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) {
- 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();
- }
-
- 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 << "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.win = MatchResult::Win::timeout;
- goto match_done;
- }
-
- optional<Move> omv = Move::parse(lastMove);
- if (!omv) {
- cout << "ERROR in player " << i+1 << ": unreadable move '" << lastMove << "'" << endl;
- throw StopCompetitionError();
- }
- if (!board.isValid(*omv, i == 0 ? -1 : 1)) {
- cout << "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();
- }
-
- 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) {
- for (int i = 0; i < num_matches; i++) {
- scheduler.submit([&p1, &p2, i]() { playMatch(p1, p2, i + 1); });
- }
-}
-
-static void fullCompetition(Scheduler &scheduler, vector<Player> &players) {
- 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]);
- playerPit(scheduler, 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);
-
- Scheduler scheduler(2);
- fullCompetition(scheduler, players);
- 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/competition/multilog.cpp b/competition/multilog.cpp
deleted file mode 100644
index bf10ec6..0000000
--- a/competition/multilog.cpp
+++ /dev/null
@@ -1,57 +0,0 @@
-#include <iostream>
-#include <string>
-#include <cassert>
-#include "multilog.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);
-
- items.emplace_back();
- items.back().line = prefix;
- cout << 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;
- redrawLine(idx);
-}
-
-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/competition/multilog.h b/competition/multilog.h
deleted file mode 100644
index ba0ff4d..0000000
--- a/competition/multilog.h
+++ /dev/null
@@ -1,32 +0,0 @@
-#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/competition/process.cpp b/competition/process.cpp
deleted file mode 100644
index a728401..0000000
--- a/competition/process.cpp
+++ /dev/null
@@ -1,136 +0,0 @@
-#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;
- perror("waitpid");
- break;
- }
- if (WIFEXITED(status)) 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;
- }
- 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() {
- if (pid != -1) kill(pid, SIGTERM);
-}
diff --git a/competition/process.h b/competition/process.h
deleted file mode 100644
index ddd80b9..0000000
--- a/competition/process.h
+++ /dev/null
@@ -1,32 +0,0 @@
-#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);
-
- void redirectStderr(const string_view fname);
-
- void run();
- void wait();
- void stop();
- void unStop();
- void terminate();
-
- bool writeLine(const string_view line);
- optional<string> readLine();
-};
diff --git a/referee/.gitignore b/referee/.gitignore
new file mode 100644
index 0000000..1c8d92c
--- /dev/null
+++ b/referee/.gitignore
@@ -0,0 +1 @@
+referee
diff --git a/referee/Makefile b/referee/Makefile
new file mode 100644
index 0000000..67317b2
--- /dev/null
+++ b/referee/Makefile
@@ -0,0 +1,13 @@
+CXX = g++
+CXXFLAGS = -Wall -Wextra -std=c++17 -g -O2
+
+.PHONY: all clean
+
+all: referee
+
+clean:
+ rm -f referee
+
+
+referee: referee.cpp ../board.h ../board.o
+ $(CXX) $(CXXFLAGS) -o $@ $< ../board.o
diff --git a/referee/referee.cpp b/referee/referee.cpp
new file mode 100644
index 0000000..78c4193
--- /dev/null
+++ b/referee/referee.cpp
@@ -0,0 +1,74 @@
+#include <iostream>
+#include <sstream>
+#include <string>
+#include <cassert>
+#include "../board.h"
+
+
+struct MoveLine {
+ int pindex;
+ string line;
+};
+
+MoveLine readMoveLine() {
+ while (true) {
+ if (!isspace(cin.peek())) break;
+ cin.get();
+ }
+
+ MoveLine ml;
+ cin >> ml.pindex;
+ if (!cin) exit(0);
+
+ assert(cin.get() == ' ');
+ getline(cin, ml.line);
+
+ return ml;
+}
+
+int8_t playerToClr(int player) {
+ return 2 * player - 1;
+}
+
+int main() {
+ Board bd = Board::makeInitial();
+
+ int nplayers;
+ cin >> nplayers;
+ assert(nplayers == 2);
+ cin.ignore(1000, '\n');
+
+ string p1, p2;
+ getline(cin, p1);
+ getline(cin, p2);
+
+ int onturn = 0;
+
+ while (true) {
+ MoveLine ml = readMoveLine();
+ if (ml.pindex != onturn) {
+ cout << "invalid" << endl;
+ continue;
+ }
+
+ optional<Move> mv = Move::parse(ml.line);
+
+ if (mv && !bd.isValid(*mv, playerToClr(onturn))) {
+ cout << "invalid" << endl;
+ continue;
+ }
+
+ int win = bd.applyCW(*mv);
+
+ if (win != 0) {
+ cout << "valid end" << endl;
+ if (win == -1) cout << "3 1" << endl;
+ else if (win == 1) cout << "1 3" << endl;
+ break;
+ } else {
+ cout << "valid" << endl;
+ }
+
+ onturn = !onturn;
+ }
+}