#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "scheduler.h" #include "process.h" #include "error.h" #include "multilog.h" #include "referee.h" using namespace std; const char *const matchcachedir = "comp_cache"; const char *const playerlogdir = "comp_playerlogs"; const char *const gamelogdir = "comp_gamelogs"; 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); } #ifdef __APPLE__ return st.st_mtimespec.tv_sec * 1000000LL + st.st_mtimespec.tv_nsec / 1000; #else return st.st_mtim.tv_sec * 1000000LL + st.st_mtim.tv_nsec / 1000; #endif } 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 { mutex lock; string fname, safename; vector results; int64_t lastModified; Player(const string &fname) : fname(fname), safename(makeSafe(fname)), lastModified(fileLastModified(fname)) {} // Do not call this in a threaded setting (wouldn't make sense anyway) Player(Player &&other) : fname{move(other.fname)} , safename{move(other.safename)} , results{move(other.results)} , lastModified{other.lastModified} {} }; 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; } enum class Mode { competition, single, }; struct Params { Mode mode; bool referee_verbose = false; string refereePath; size_t timeout_msec = 10000; }; 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) { 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.sc1 >> mres.sc2 >> mres.ms1 >> mres.ms2; if (!f) { 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) { { lock_guard guard{p1.lock}; p1.results.push_back(result); } { lock_guard guard{p2.lock}; p2.results.push_back(result.inverted()); } } static MatchResult playMatch(const Player &p1, const Player &p2, const Params ¶ms, const string &gamecode, ostream &gamelog, const string &p1logpath, const string &p2logpath) { MatchResult mres; Referee referee(params.referee_verbose, 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(p1logpath); procs[1].redirectStderr(p2logpath); procs[0].run(); procs[1].run(); while (true) { auto event = referee.nextEvent(); if (auto readEvent = get_if(&event)) { Process &proc = procs[readEvent->player]; proc.unStop(); int64_t start = gettimestamp(); optional oline = proc.readLine(); if (!oline) { cout << "ERROR reading move from player " << readEvent->player + 1 << " (game " << gamecode << ")" << endl; cout << "(process exit code: " << proc.waitPoll() << ")" << endl; throw StopCompetitionError(); } (readEvent->player == 0 ? mres.ms1 : mres.ms2) += gettimestamp() - start; proc.stop(); readEvent->callback(*oline); if (mres.ms1 / 1000 > params.timeout_msec || mres.ms2 / 1000 > params.timeout_msec) { mres.status = MatchResult::Status::timeout; mres.sc1 = mres.sc2 = 0; referee.terminate(); break; } } else if (auto writeEvent = get_if(&event)) { if (!procs[writeEvent->player].writeLine(writeEvent->line, writeEvent->allowBrokenPipe)) { cout << "ERROR writing move to player " << writeEvent->player + 1 << " (game " << gamecode << ")" << endl; throw StopCompetitionError(); } } else if (auto gamelogEvent = get_if(&event)) { gamelog << "P" << gamelogEvent->player + 1 << ": " << gamelogEvent->line << endl; } else if (auto endEvent = get_if(&event)) { mres.status = MatchResult::Status::ok; assert(endEvent->scores.size() == 2); mres.sc1 = endEvent->scores[0]; mres.sc2 = endEvent->scores[1]; break; } else if (auto errorEvent = get_if(&event)) { cout << "ERROR in player " << errorEvent->player + 1 << ": " << errorEvent->message << " (game " << gamecode << ")" << endl; gamelog << endl << "ERROR in P" << errorEvent->player + 1 << ": " << errorEvent->message << endl; throw StopCompetitionError(); } else { assert(false); } } for (int i = 0; i < 2; i++) { procs[i].unStop(); if (procs[i].waitPoll() == -1) usleep(10000); int ret = procs[i].terminate(); if (ret != 0 && ret != 1009) { cout << "Player " << i+1 << " exited with code " << ret << 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; return mres; } static void playMatchInCompetition(MultiLog &multiLog, Player &p1, Player &p2, int index, const Params ¶ms) { const int logId = multiLog.add(p1.fname + " - " + p2.fname + ": "); if (optional optres = readMatchCache(p1, p2, index)) { multiLog.append(logId, optres->describe(p1, p2) + " (cached)"); multiLog.complete(logId); recordResult(p1, p2, *optres); return; } const string gamelog_path = gameLogPath(p1, p2, index); ofstream gamelog(gamelog_path); if (!gamelog) { cout << "ERROR opening game log file " << gamelog_path << endl; throw StopCompetitionError(); } const MatchResult mres = playMatch( p1, p2, params, gameCodeName(p1, p2, index), gamelog, playerLogPath(p1, p2, index, 1), playerLogPath(p1, p2, index, 2) ); multiLog.append(logId, mres.describe(p1, p2)); multiLog.complete(logId); writeMatchCache(p1, p2, index, mres); recordResult(p1, p2, mres); } struct CompParams { Scheduler *scheduler; MultiLog *multiLog; int num_matches = 5; }; static void playerPit(CompParams &compParams, Player &p1, Player &p2, const Params ¶ms) { for (int i = 0; i < compParams.num_matches; i++) { compParams.scheduler->submit([i, &p1, &p2, ¶ms, &compParams]() { playMatchInCompetition(*compParams.multiLog, p1, p2, i + 1, params); }); } } static void fullCompetition(CompParams compParams, vector &players, const Params ¶ms) { for (size_t p1i = 0; p1i < players.size(); p1i++) { for (size_t p2i = p1i + 1; p2i < players.size(); p2i++) { playerPit(compParams, players[p1i], players[p2i], params); playerPit(compParams, players[p2i], players[p1i], params); } } } static void usage(const char *argv0) { cerr << "Usage: " << argv0 << " comp [OPTIONS] \n" "Run a full competition. Gamelogs in " << gamelogdir << ", playerlogs in\n" << playerlogdir << ". Caches in " << matchcachedir << "; already-played games\n" "as determined by caches will not be run again.\n" " --noansi Don't use fancy back-updating log output\n" " --vs Verbose scheduler output (use with --noansi)\n" " -j Set number of parallel games (default ncores/2)\n" " -n Set number of matches per player pairing (default " << CompParams{}.num_matches << ")\n" "\n" "Usage: " << argv0 << " single [OPTIONS] \n" "Run only a single game. Gamelog from referee is sent to stdout.\n" " --log1 Playerlog file for player 1\n" " --log2 Playerlog file for player 2\n" "\n" "Options that always apply:\n" " --vr Verbose referee-handling output (use with --noansi if comp mode)\n" " -T Set player timeout in milliseconds (default " << Params{}.timeout_msec << ")\n" ; } static Params parseCommonOptions(vector &argv) { Params params; if (argv.size() <= 1) { usage(argv[0]); exit(1); } if (strcmp(argv[1], "comp") == 0) params.mode = Mode::competition; else if (strcmp(argv[1], "single") == 0) params.mode = Mode::single; else { usage(argv[0]); exit(1); } argv.erase(argv.begin() + 1); for (size_t i = 1; i < argv.size(); i++) { if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { usage(argv[0]); exit(0); } else if (strcmp(argv[i], "--vr") == 0) { params.referee_verbose = true; argv.erase(argv.begin() + i); i--; } else if (strcmp(argv[i], "-T") == 0 && i + 1 < argv.size()) { char *endp; params.timeout_msec = strtoull(argv[i+1], &endp, 10); if (!argv[i][0] || *endp || params.timeout_msec <= 0) { cerr << "Invalid number to -T" << endl; exit(1); } argv.erase(argv.begin() + i, argv.begin() + i + 2); i--; } else if (strcmp(argv[i], "-j") == 0 || strcmp(argv[i], "-n") == 0 || (strlen(argv[i]) >= strlen("--log1") && memcmp(argv[i], "--log", strlen("--log")) == 0)) { i++; // skip argument of option } else if (params.refereePath.empty()) { params.refereePath = argv[i]; argv.erase(argv.begin() + i); i--; } } return params; } static int mainCompetition(const vector &argv, const Params ¶ms) { vector players; bool multilog_fancy = true; bool scheduler_verbose = false; int num_threads = std::thread::hardware_concurrency() / 2; CompParams compParams; for (size_t i = 1; i < argv.size(); i++) { if (strcmp(argv[i], "--noansi") == 0) { multilog_fancy = false; } else if (strcmp(argv[i], "--vs") == 0) { scheduler_verbose = true; } else if (strcmp(argv[i], "-j") == 0 && i + 1 < argv.size()) { char *endp; num_threads = strtol(argv[i+1], &endp, 10); if (!argv[i][0] || *endp || num_threads <= 0) { cerr << "Invalid number to -j" << endl; return 1; } i++; } else if (strcmp(argv[i], "-n") == 0 && i + 1 < argv.size()) { char *endp; compParams.num_matches = strtol(argv[i+1], &endp, 10); if (!argv[i][0] || *endp || compParams.num_matches <= 0) { cerr << "Invalid number to -n" << endl; return 1; } i++; } else if (argv[i][0] == '-') { cerr << "Unknown option/flag '" << argv[i] << "'" << endl; return 1; } else { players.emplace_back(argv[i]); } } if (players.empty()) { usage(argv[0]); return 1; } mkdirp(matchcachedir); mkdirp(playerlogdir); mkdirp(gamelogdir); signal(SIGPIPE, [](int){}); MultiLog multiLog{multilog_fancy}; compParams.multiLog = &multiLog; Scheduler scheduler(scheduler_verbose, num_threads); compParams.scheduler = &scheduler; fullCompetition(compParams, players, params); scheduler.finish(); 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; } return 0; } static int mainSingle(const vector &argv, const Params ¶ms) { vector players; unordered_map logfnames; for (size_t i = 1; i < argv.size(); i++) { if (strlen(argv[i]) >= strlen("--log1") && memcmp(argv[i], "--log", strlen("--log")) == 0 && i + 1 < argv.size()) { const char *idxstring = argv[i] + strlen("--log"); char *endp; int playeridx = strtol(idxstring, &endp, 10); if (!idxstring[0] || *endp || playeridx <= 0) { cerr << "--logN index invalid" << endl; return 1; } logfnames[playeridx] = argv[i+1]; i++; } else if (argv[i][0] == '-') { cerr << "Unknown option/flag '" << argv[i] << "'" << endl; return 1; } else { players.emplace_back(argv[i]); } } if (players.size() != 2) { usage(argv[0]); return 1; } vector playerlogs(players.size()); for (size_t i = 0; i < players.size(); i++) { auto it = logfnames.find(i + 1); if (it == logfnames.end()) playerlogs[i] = "/dev/null"; else playerlogs[i] = it->second; } signal(SIGPIPE, [](int){}); playMatch(players[0], players[1], params, "single", cout, playerlogs[0], playerlogs[1]); return 0; } int main(int argc, char **argv) { if (argc == 0) { usage("competition"); return 1; } vector arguments{argv, argv + argc}; const Params params = parseCommonOptions(arguments); switch (params.mode) { case Mode::competition: return mainCompetition(arguments, params); case Mode::single: return mainSingle(arguments, params); } }