From c01700906812f949c9088709b6c34307fbf63338 Mon Sep 17 00:00:00 2001 From: tomsmeding Date: Thu, 8 Aug 2019 22:06:03 +0200 Subject: BREAKING New referee protocol --- Makefile | 2 +- main.cpp | 81 ++++++++++------------- referee.cpp | 217 ++++++++++++++++++++++++++++++++---------------------------- referee.h | 95 +++++++++++++------------- 4 files changed, 203 insertions(+), 192 deletions(-) diff --git a/Makefile b/Makefile index 3d4f9b3..52d8e7e 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ ifeq ($(shell uname),Darwin) - CXX = g++-8 + CXX = g++-9 else CXX = g++ endif diff --git a/main.cpp b/main.cpp index fa07cf4..24edd5b 100644 --- a/main.cpp +++ b/main.cpp @@ -257,78 +257,69 @@ static void playMatch(Player &p1, Player &p2, int index, const Params ¶ms) { procs[0].run(); procs[1].run(); - const auto handleWriteLines = [&referee, &procs]() { - for (const auto &p : referee.playerWriteLines()) { - assert(0 <= p.first && p.first < 2); - procs[p.first].writeLine(p.second); - } - }; - - // Received upon initialisation of referee - handleWriteLines(); - - string lastMove = "Start"; - while (true) { - for (int i = 0; i < 2; i++) { - if (!referee.getFeatures().noLastMove) { - if (!procs[i].writeLine(lastMove)) { - cout << "ERROR writing move to player " << i+1 - << " (game " << gameCodeName(p1, p2, index) << ")" << endl; - throw StopCompetitionError(); - } - } + auto event = referee.nextEvent(); + + if (auto readEvent = get_if(&event)) { + Process &proc = procs[readEvent->player]; - procs[i].unStop(); + proc.unStop(); int64_t start = gettimestamp(); - optional oline = procs[i].readLine(); + + optional oline = proc.readLine(); if (!oline) { - cout << "ERROR reading move from player " << i+1 + cout << "ERROR reading move from player " << readEvent->player + 1 << " (game " << gameCodeName(p1, p2, index) << ")" << endl; - cout << "(process exit code: " << procs[i].waitPoll() << ")" << endl; + cout << "(process exit code: " << proc.waitPoll() << ")" << endl; throw StopCompetitionError(); } - lastMove = *oline; - (i == 0 ? mres.ms1 : mres.ms2) += gettimestamp() - start; - procs[i].stop(); + (readEvent->player == 0 ? mres.ms1 : mres.ms2) += gettimestamp() - start; + proc.stop(); - gamelog << "P" << i+1 << ": " << lastMove << endl; + readEvent->callback(*oline); if (mres.ms1 / 1000 > timeout_msec || mres.ms2 / 1000 > timeout_msec) { mres.status = MatchResult::Status::timeout; mres.sc1 = mres.sc2 = 0; referee.terminate(); - goto match_done; + break; } - if (!referee.moveValid(i, lastMove)) { - cout << "ERROR in player " << i+1 << ": invalid move '" << lastMove << "'" - << " (game " << gameCodeName(p1, p2, index) << ")" << endl; + } else if (auto writeEvent = get_if(&event)) { + if (!procs[writeEvent->player].writeLine(writeEvent->line)) { + cout << "ERROR writing move to player " << writeEvent->player + 1 + << " (game " << gameCodeName(p1, p2, index) << ")" << endl; throw StopCompetitionError(); } - handleWriteLines(); + } else if (auto gamelogEvent = get_if(&event)) { + gamelog << "P" << gamelogEvent->player + 1 << ": " + << gamelogEvent->line << endl; - if (referee.gameEnded()) { - mres.status = MatchResult::Status::ok; + } else if (auto endEvent = get_if(&event)) { + mres.status = MatchResult::Status::ok; - optional> oscores = referee.getScores(); - assert(oscores); - assert(oscores->size() == 2); - mres.sc1 = oscores->at(0); - mres.sc2 = oscores->at(1); + assert(endEvent->scores.size() == 2); + mres.sc1 = endEvent->scores[0]; + mres.sc2 = endEvent->scores[1]; + break; - goto match_done; - } + } else if (auto errorEvent = get_if(&event)) { + cout << "ERROR in player " << errorEvent->player + 1 << ": " << errorEvent->message + << " (game " << gameCodeName(p1, p2, index) << ")" << endl; + gamelog << endl + << "ERROR in P" << errorEvent->player + 1 << ": " << errorEvent->message << endl; + throw StopCompetitionError(); + + } else { + assert(false); } } -match_done: for (int i = 0; i < 2; i++) { - bool success = procs[i].writeLine("Quit"); procs[i].unStop(); - if (success) usleep(10000); + 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; diff --git a/referee.cpp b/referee.cpp index ffcbd14..2701e78 100644 --- a/referee.cpp +++ b/referee.cpp @@ -1,90 +1,160 @@ #include +#include #include #include "referee.h" #include "params.h" // Timeout in milliseconds for the feature negotiation at the start of the // protocol. If the referee hasn't sent anything after this timeout, we assume -// the referee isn't capable of sending feature lines and will be treated as -// such for the duration of the competition. +// the referee isn't capable of sending feature lines. #define FEATURE_TIMEOUT 200 Referee::Referee(const string_view execname, const vector &players) - : proc(execname), refereeExecname(execname) { + : proc(execname), refereeExecname(execname), numPlayers(players.size()) { proc.run(); + reftag = "REF(" + to_string(proc.getPid()) + ") "; + if (referee_verbose) { - cout << "REF(" << proc.getPid() << ") starting for:"; + cout << reftag << "starting for:"; for (const string &p : players) cout << " " << p; cout << endl; } queryFeatures(); + if (!features.version2) { + cout << reftag << "does not support protocol version 2!" << endl; + exit(1); + } + proc.writeLine(to_string(players.size())); for (const string &p : players) { proc.writeLine(p); } - - readWriteLines(); } Referee::~Referee() { if (referee_verbose) { - cout << "REF(" << proc.getPid() << ") stopping" << endl; + cout << reftag << "stopping" << endl; } proc.wait(); proc.terminate(); } -bool Referee::moveValid(int player, const string_view line) { - string s = to_string(player) + " "; - s.append(line); +Referee::Event Referee::nextEvent() { + optional ocommand = proc.readLine(); + if (!ocommand) { + cout << reftag << "Unexpected EOF before 'end' command!" << endl; + exit(1); + } + + string &command = *ocommand; if (referee_verbose) { - cout << "REF(" << proc.getPid() << ") write <" << s << ">" << endl; + cout << reftag << "read <" << command << ">" << endl; } - proc.writeLine(s); + auto getPlayer = [this](const string &command, const char *commandName, int prefixLen) { + int player; + try { + player = stoi(command.substr(prefixLen)); + } catch (...) { + cout << reftag << "Protocol violation in '" << commandName << "' command!" << endl; + exit(1); + } - optional response = proc.readLine(); - if (response) { - if (referee_verbose) { - cout << "REF(" << proc.getPid() << ") read <" << *response << ">" << endl; + if (player < 0 || player >= numPlayers) { + cout << reftag << "Invalid player index " << player << " mentioned in '" << commandName << "' command!" << endl; + exit(1); } - bool result; - if (*response == "valid end") { - isEnd = true; - result = true; - } else if (*response == "valid") { - result = true; - } else if (*response == "invalid") { - result = false; - } else { - cout << "REF(" << proc.getPid() << ") Invalid validity judgement '" << *response << "'!" << endl; + size_t afterIndex = command.find(' ', prefixLen); + if (afterIndex != string::npos) afterIndex++; + + return make_pair(player, afterIndex); + }; + + if (command.substr(0, 5) == "read ") { + int player = getPlayer(command, "read", 5).first; + return ReadEvent{ + player, + [this](const string &line) { + if (referee_verbose) { + cout << reftag << "write <" << line << ">" << endl; + } + + if (!proc.writeLine(line)) { + cout << reftag << "Referee exited unexpectedly after 'read' command!" << endl; + exit(1); + } + } + }; + } else if (command.substr(0, 6) == "write ") { + int player; + size_t afterIndex; + tie(player, afterIndex) = getPlayer(command, "write", 6); + + if (afterIndex == string::npos) { + cout << reftag << "Protocol violation in 'write' command: no line!" << endl; exit(1); } - readWriteLines(); + return WriteEvent{player, command.substr(afterIndex)}; + } else if (command.substr(0, 8) == "gamelog ") { + int player; + size_t afterIndex; + tie(player, afterIndex) = getPlayer(command, "gamelog", 8); - if (isEnd) readScores(); + if (afterIndex == string::npos) { + cout << reftag << "Protocol violation in 'gamelog' command: no line!" << endl; + exit(1); + } - return result; - } else { - cout << "REF(" << proc.getPid() << ") EOF!" << endl; - return false; - } -} + return GamelogEvent{player, command.substr(afterIndex)}; + } else if (command.substr(0, 4) == "end ") { + size_t cursor = 4; -vector> Referee::playerWriteLines() { - return writeLinesList; -} + vector scores; + while (cursor < command.size()) { + size_t idx = command.find(' ', cursor); + if (idx == string::npos) idx = command.size(); + + string substr = command.substr(cursor, idx - cursor); + + int score; + try { + score = stoi(substr); + } catch (...) { + cout << reftag << "Protocol violation in 'end' command: invalid scores!" << endl; + exit(1); + } + + scores.push_back(score); + + cursor = idx + 1; + } + + proc.terminate(); + return EndEvent{move(scores)}; + } else if (command.substr(0, 6) == "error ") { + int player; + size_t afterIndex; + tie(player, afterIndex) = getPlayer(command, "error", 6); + + if (afterIndex == string::npos) { + cout << reftag << "Protocol violation in 'error' command: no message!" << endl; + exit(1); + } -bool Referee::gameEnded() { - return isEnd; + proc.terminate(); + return ErrorEvent{player, command.substr(6)}; + } else { + cout << reftag << "Protocol violation: unknown command '" << command << "'!" << endl; + exit(1); + } } void Referee::queryFeatures() { @@ -95,8 +165,8 @@ void Referee::queryFeatures() { if (first) { line = proc.readLineTimeout(FEATURE_TIMEOUT); if (!line) { - cout << "Referee '" << refereeExecname << "' seems to follow the old protocol!" << endl; - return; + cout << "Referee '" << refereeExecname << "' does not support feature detection!" << endl; + exit(1); } first = false; @@ -105,85 +175,30 @@ void Referee::queryFeatures() { } if (!line) { - cout << "REF(" << proc.getPid() << ") EOF!" << endl; + cout << reftag << "EOF!" << endl; exit(1); } if (referee_verbose) { - cout << "REF(" << proc.getPid() << ") read <" << *line << ">" << endl; + cout << reftag << "read <" << *line << ">" << endl; } if (*line == "feature_end") { return; } else if (line->substr(0, 8) == "feature ") { string featname = line->substr(8); - if (featname == "write_lines") features.writeLines = true; - else if (featname == "no_last_move") features.noLastMove = true; + if (featname == "version_2") features.version2 = true; else { - cerr << "REF(" << proc.getPid() << ") Referee requested unsupported feature '" << featname << "'" << endl; + cerr << reftag << "Referee requested unsupported feature '" << featname << "'" << endl; exit(1); } } else { - cerr << "REF(" << proc.getPid() << ") Referee protocol violation in feature negotiation" << endl; - exit(1); - } - } -} - -void Referee::readWriteLines() { - writeLinesList.clear(); - - if (!features.writeLines) return; - - while (true) { - optional line = proc.readLine(); - if (!line) { - cout << "REF(" << proc.getPid() << ") EOF!" << endl; - break; - } - - if (referee_verbose) { - cout << "REF(" << proc.getPid() << ") read <" << *line << ">" << endl; - } - - if (line == "write_end") break; - - size_t idx = line->find(' '); - if (idx == string::npos) { - cerr << "REF(" << proc.getPid() << ") Referee feature_write_lines protocol violation!" << endl; + cerr << reftag << "Referee protocol violation in feature negotiation" << endl; exit(1); } - - int player = stoi(line->substr(0, idx)); - writeLinesList.emplace_back(player, line->substr(idx + 1)); } } -void Referee::readScores() { - optional line = proc.readLine(); - if (!line) { - cerr << "Referee stopped before providing scores" << endl; - exit(1); - } - - 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> Referee::getScores() { - if (isEnd) return scores; - else return nullopt; -} - void Referee::terminate() { proc.terminate(); } - -const Features& Referee::getFeatures() const { - return features; -} diff --git a/referee.h b/referee.h index 82b8b02..1b9d507 100644 --- a/referee.h +++ b/referee.h @@ -5,49 +5,58 @@ #include #include #include +#include +#include #include "process.h" using namespace std; -struct Features { - // If true, players should not receive the last move on stdin. - // Used by the match runner. - bool noLastMove = false; - - // If true, the referee program can write additional lines to the players. - // Used by the Referee class to produce playerWriteLines() output. - bool writeLines = false; -}; - class Referee { Process proc; string refereeExecname; - bool isEnd = false; - vector scores; + int numPlayers; + + string reftag; + struct Features { + // Referee supports current version of the protocol. + bool version2; + }; Features features; - vector> writeLinesList; void queryFeatures(); - void readScores(); - void readWriteLines(); public: + struct ReadEvent { + int player; + // call with the line read (no newline) + function callback; + }; + struct WriteEvent { + int player; + string line; + }; + struct GamelogEvent { + int player; + string line; + }; + struct EndEvent { + vector scores; + }; + struct ErrorEvent { + int player; + string message; + }; + using Event = variant; + Referee(const string_view execname, const vector &players); ~Referee(); - // player: 0 = first player, 1 = second player - bool moveValid(int player, const string_view line); - vector> playerWriteLines(); - - bool gameEnded(); - optional> getScores(); + Event nextEvent(); // Kills the referee process void terminate(); - - const Features& getFeatures() const; }; /* @@ -56,32 +65,28 @@ Referee protocol: At startup, the referee should write a number of lines to stdout of the form 'feature ', indicating that the referee supports the feature given by . The list of features should be ended with a line containing -'feature_end'. For supported features and their effect, see below. +'feature_end'. To use the version of the protocol described here, it is +mandatory to request 'feature version_2'. For supported features and their +effect, see below. The referee then 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 ' ', -where is the index of the player in the game (0 is first, 1 is second, -etc.) and 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. +Then, the referee has the following commands available: +- 'read ': read a line from the player with index ; this line + is written directly to the referee afterwards. The first player is 0, the + second player is 1, etc. +- 'write ': write a line to the player with index . +- 'gamelog ': write a line in the game log indicating that the + player with index made the move given by . +- 'end ...': signify that the game has ended. The final scores + of the players are given after 'end'. These scores should be given as a + space-separated list of integers, as many as there are players. +- 'error ': indicate that the player with index has + produced erroneous output. This ends the game. The will be + presented to the user and written to the gamelog. FEATURES -The following features are supported: -- 'write_lines': After having received the list of players at the start of the - protocol, and after a move validity judgement, the referee should write lines - of the form ' ' to stdout, where is the index of the - player on whose stdin to write . The referee should signal the end of - this list by writing a line 'write_end' to stdout. This allows the referee to - write additional data to players' inputs after they have written their moves. - Note that 'valid end' is also a move validity judgement, and the player lines - should be written *before* writing the scores. -- 'no_last_move': Prevent the competition manager from writing the last player's - last move to stdin of the next player. Should probably be used in conjunction - with 'write_lines'. +Currently, the following features are supported: +- 'version_2': must be used by all referees. */ -- cgit v1.2.3