diff --git a/src/games/file.cc b/src/games/file.cc index 6f19fce27..149c00344 100644 --- a/src/games/file.cc +++ b/src/games/file.cc @@ -25,6 +25,7 @@ #include #include #include +#include #include "gambit.h" // for explicit access to turning off canonicalization @@ -46,12 +47,8 @@ using GameFileToken = enum { TOKEN_NONE = 7 }; -//! -//! This parser class implements the semantics of Gambit savefiles, -//! including the nonsignificance of whitespace and the possibility of -//! escaped-quotes within text labels. -//! -class GameParserState { +/// @brief Class implementing conversion of classic Gambit savefiles into lexical tokens. +class GameFileLexer { private: std::istream &m_file; @@ -65,23 +62,47 @@ class GameParserState { void IncreaseLine(); public: - explicit GameParserState(std::istream &p_file) : m_file(p_file) {} + explicit GameFileLexer(std::istream &p_file) : m_file(p_file) {} GameFileToken GetNextToken(); GameFileToken GetCurrentToken() const { return m_lastToken; } - int GetCurrentLine() const { return m_currentLine; } - int GetCurrentColumn() const { return m_currentColumn; } - std::string CreateLineMsg(const std::string &msg) const; + /// Throw an InvalidFileException with the specified message + void OnParseError(const std::string &p_message) const; const std::string &GetLastText() const { return m_lastText; } + + void ExpectCurrentToken(GameFileToken p_tokenType, const std::string &p_message) + { + if (GetCurrentToken() != p_tokenType) { + OnParseError("Expected " + p_message); + } + } + void ExpectNextToken(GameFileToken p_tokenType, const std::string &p_message) + { + if (GetNextToken() != p_tokenType) { + OnParseError("Expected " + p_message); + } + } + void AcceptNextToken(GameFileToken p_tokenType) + { + if (GetNextToken() == p_tokenType) { + GetNextToken(); + } + } }; -void GameParserState::ReadChar(char &c) +void GameFileLexer::OnParseError(const std::string &p_message) const +{ + throw InvalidFileException("line " + std::to_string(m_currentLine) + ":" + + std::to_string(m_currentColumn) + ": " + p_message); +} + +void GameFileLexer::ReadChar(char &c) { m_file.get(c); m_currentColumn++; } -void GameParserState::UnreadChar() +void GameFileLexer::UnreadChar() { if (!m_file.eof()) { m_file.unget(); @@ -89,13 +110,13 @@ void GameParserState::UnreadChar() } } -void GameParserState::IncreaseLine() +void GameFileLexer::IncreaseLine() { m_currentLine++; m_currentColumn = 1; } -GameFileToken GameParserState::GetNextToken() +GameFileToken GameFileLexer::GetNextToken() { char c = ' '; if (m_file.eof()) { @@ -148,7 +169,7 @@ GameFileToken GameParserState::GetNextToken() buf += c; ReadChar(c); if (c != '+' && c != '-' && !isdigit(c)) { - throw InvalidFileException(CreateLineMsg("Invalid Token +/-")); + OnParseError("Invalid Token +/-"); } buf += c; ReadChar(c); @@ -177,7 +198,7 @@ GameFileToken GameParserState::GetNextToken() buf += c; ReadChar(c); if (c != '+' && c != '-' && !isdigit(c)) { - throw InvalidFileException(CreateLineMsg("Invalid Token +/-")); + OnParseError("Invalid Token +/-"); } buf += c; ReadChar(c); @@ -230,8 +251,7 @@ GameFileToken GameParserState::GetNextToken() ReadChar(a); while (a != '\"' || lastslash) { if (m_file.eof() || !m_file.good()) { - throw InvalidFileException( - CreateLineMsg("End of file encountered when reading string label")); + OnParseError("End of file encountered when reading string label"); } if (lastslash && a == '"') { m_lastText += '"'; @@ -253,8 +273,7 @@ GameFileToken GameParserState::GetNextToken() m_lastText += a; ReadChar(a); if (m_file.eof()) { - throw InvalidFileException( - CreateLineMsg("End of file encountered when reading string label")); + OnParseError("End of file encountered when reading string label"); } if (a == '\n') { IncreaseLine(); @@ -273,219 +292,88 @@ GameFileToken GameParserState::GetNextToken() return (m_lastToken = TOKEN_SYMBOL); } -std::string GameParserState::CreateLineMsg(const std::string &msg) const -{ - std::stringstream stream; - stream << "line " << m_currentLine << ":" << m_currentColumn << ": " << msg; - return stream.str(); -} - class TableFilePlayer { public: std::string m_name; Array m_strategies; - TableFilePlayer *m_next{nullptr}; - TableFilePlayer() = default; + TableFilePlayer(const std::string &p_name) : m_name(p_name) {} }; class TableFileGame { public: std::string m_title, m_comment; - TableFilePlayer *m_firstPlayer{nullptr}, *m_lastPlayer{nullptr}; - int m_numPlayers{0}; - - TableFileGame() = default; - ~TableFileGame(); - - void AddPlayer(const std::string &); - int NumPlayers() const { return m_numPlayers; } - int NumStrategies(int p_player) const; - std::string GetPlayer(int p_player) const; - std::string GetStrategy(int p_player, int p_strategy) const; -}; - -TableFileGame::~TableFileGame() -{ - if (m_firstPlayer) { - TableFilePlayer *player = m_firstPlayer; - while (player) { - TableFilePlayer *nextPlayer = player->m_next; - delete player; - player = nextPlayer; - } - } -} - -void TableFileGame::AddPlayer(const std::string &p_name) -{ - auto *player = new TableFilePlayer; - player->m_name = p_name; - - if (m_firstPlayer) { - m_lastPlayer->m_next = player; - m_lastPlayer = player; - } - else { - m_firstPlayer = player; - m_lastPlayer = player; - } - m_numPlayers++; -} - -int TableFileGame::NumStrategies(int p_player) const -{ - TableFilePlayer *player = m_firstPlayer; - int pl = 1; - - while (player && pl < p_player) { - player = player->m_next; - pl++; - } + std::vector m_players; - if (!player) { - return 0; - } - else { - return player->m_strategies.Length(); + size_t NumPlayers() const { return m_players.size(); } + Array NumStrategies() const + { + Array ret(m_players.size()); + std::transform(m_players.begin(), m_players.end(), ret.begin(), + [](const TableFilePlayer &player) { return player.m_strategies.size(); }); + return ret; } -} - -std::string TableFileGame::GetPlayer(int p_player) const -{ - TableFilePlayer *player = m_firstPlayer; - int pl = 1; - while (player && pl < p_player) { - player = player->m_next; - pl++; + std::string GetPlayer(int p_player) const { return m_players[p_player - 1].m_name; } + std::string GetStrategy(int p_player, int p_strategy) const + { + return m_players[p_player - 1].m_strategies[p_strategy]; } +}; - if (!player) { - return ""; - } - else { - return player->m_name; - } -} - -std::string TableFileGame::GetStrategy(int p_player, int p_strategy) const -{ - TableFilePlayer *player = m_firstPlayer; - int pl = 1; - - while (player && pl < p_player) { - player = player->m_next; - pl++; - } - - if (!player) { - return ""; - } - else { - return player->m_strategies[p_strategy]; - } -} - -void ReadPlayers(GameParserState &p_state, TableFileGame &p_data) +void ReadPlayers(GameFileLexer &p_state, TableFileGame &p_data) { - if (p_state.GetNextToken() != TOKEN_LBRACE) { - throw InvalidFileException(p_state.CreateLineMsg("Expecting '{' before players")); - } - + p_state.ExpectNextToken(TOKEN_LBRACE, "'{'"); while (p_state.GetNextToken() == TOKEN_TEXT) { - p_data.AddPlayer(p_state.GetLastText()); + p_data.m_players.emplace_back(p_state.GetLastText()); } - - if (p_state.GetCurrentToken() != TOKEN_RBRACE) { - throw InvalidFileException(p_state.CreateLineMsg("Expecting '}' after players")); - } - - p_state.GetNextToken(); + p_state.ExpectCurrentToken(TOKEN_RBRACE, "'}'"); } -void ReadStrategies(GameParserState &p_state, TableFileGame &p_data) +void ReadStrategies(GameFileLexer &p_state, TableFileGame &p_data) { - if (p_state.GetCurrentToken() != TOKEN_LBRACE) { - throw InvalidFileException(p_state.CreateLineMsg("Expecting '{' before strategies")); - } - p_state.GetNextToken(); - - if (p_state.GetCurrentToken() == TOKEN_LBRACE) { - TableFilePlayer *player = p_data.m_firstPlayer; - - while (p_state.GetCurrentToken() == TOKEN_LBRACE) { - if (!player) { - throw InvalidFileException( - p_state.CreateLineMsg("Not enough players for number of strategy entries")); - } + p_state.ExpectNextToken(TOKEN_LBRACE, "'{'"); + auto token = p_state.GetNextToken(); + if (token == TOKEN_LBRACE) { + // Nested list of strategy labels + for (auto &player : p_data.m_players) { + p_state.ExpectCurrentToken(TOKEN_LBRACE, "'{'"); while (p_state.GetNextToken() == TOKEN_TEXT) { - player->m_strategies.push_back(p_state.GetLastText()); - } - - if (p_state.GetCurrentToken() != TOKEN_RBRACE) { - throw InvalidFileException(p_state.CreateLineMsg("Expecting '}' after player strategy")); + player.m_strategies.push_back(p_state.GetLastText()); } - + p_state.ExpectCurrentToken(TOKEN_RBRACE, "'}'"); p_state.GetNextToken(); - player = player->m_next; } - - if (player) { - throw InvalidFileException(p_state.CreateLineMsg("Players with undefined strategies")); - } - - if (p_state.GetCurrentToken() != TOKEN_RBRACE) { - throw InvalidFileException(p_state.CreateLineMsg("Expecting '}' after strategies")); - } - - p_state.GetNextToken(); } - else if (p_state.GetCurrentToken() == TOKEN_NUMBER) { - TableFilePlayer *player = p_data.m_firstPlayer; - - while (p_state.GetCurrentToken() == TOKEN_NUMBER) { - if (!player) { - throw InvalidFileException( - p_state.CreateLineMsg("Not enough players for number of strategy entries")); - } - - for (int st = 1; st <= atoi(p_state.GetLastText().c_str()); st++) { - player->m_strategies.push_back(lexical_cast(st)); + else { + // List of number of strategies for each player, no labels + for (auto &player : p_data.m_players) { + p_state.ExpectCurrentToken(TOKEN_NUMBER, "number"); + for (int st = 1; st <= std::stoi(p_state.GetLastText()); st++) { + player.m_strategies.push_back(std::to_string(st)); } - p_state.GetNextToken(); - player = player->m_next; - } - - if (p_state.GetCurrentToken() != TOKEN_RBRACE) { - throw InvalidFileException(p_state.CreateLineMsg("Expecting '}' after strategies")); - } - - if (player) { - throw InvalidFileException(p_state.CreateLineMsg("Players with strategies undefined")); } - - p_state.GetNextToken(); - } - else { - throw InvalidFileException(p_state.CreateLineMsg("Unrecognizable strategies format")); } + p_state.ExpectCurrentToken(TOKEN_RBRACE, "'}'"); + p_state.GetNextToken(); } -void ParseNfgHeader(GameParserState &p_state, TableFileGame &p_data) +void ParseNfgHeader(GameFileLexer &p_state, TableFileGame &p_data) { + if (p_state.GetNextToken() != TOKEN_SYMBOL || p_state.GetLastText() != "NFG") { + p_state.OnParseError("Expecting NFG file type indicator"); + } if (p_state.GetNextToken() != TOKEN_NUMBER || p_state.GetLastText() != "1") { - throw InvalidFileException(p_state.CreateLineMsg("Accepting only NFG version 1")); + p_state.OnParseError("Accepting only NFG version 1"); } - if (p_state.GetNextToken() != TOKEN_SYMBOL || (p_state.GetLastText() != "D" && p_state.GetLastText() != "R")) { - throw InvalidFileException(p_state.CreateLineMsg("Accepting only NFG D or R data type")); + p_state.OnParseError("Accepting only NFG D or R data type"); } if (p_state.GetNextToken() != TOKEN_TEXT) { - throw InvalidFileException(p_state.CreateLineMsg("Game title missing")); + p_state.OnParseError("Game title missing"); } p_data.m_title = p_state.GetLastText(); @@ -499,156 +387,80 @@ void ParseNfgHeader(GameParserState &p_state, TableFileGame &p_data) } } -void ReadOutcomeList(GameParserState &p_parser, Game &p_nfg) +void ReadOutcomeList(GameFileLexer &p_parser, Game &p_nfg) { - if (p_parser.GetNextToken() == TOKEN_RBRACE) { - // Special case: empty outcome list - p_parser.GetNextToken(); - return; - } - - if (p_parser.GetCurrentToken() != TOKEN_LBRACE) { - throw InvalidFileException(p_parser.CreateLineMsg("Expecting '{' before outcome")); - } - - int nOutcomes = 0; + auto players = p_nfg->GetPlayers(); + p_parser.GetNextToken(); while (p_parser.GetCurrentToken() == TOKEN_LBRACE) { - nOutcomes++; - int pl = 1; - - if (p_parser.GetNextToken() != TOKEN_TEXT) { - throw InvalidFileException(p_parser.CreateLineMsg("Expecting string for outcome")); - } - - GameOutcome outcome; - try { - outcome = p_nfg->GetOutcome(nOutcomes); - } - catch (IndexException &) { - // It might happen that the file contains more outcomes than - // contingencies. If so, just create them on the fly. - outcome = p_nfg->NewOutcome(); - } + p_parser.ExpectNextToken(TOKEN_TEXT, "outcome name"); + auto outcome = p_nfg->NewOutcome(); outcome->SetLabel(p_parser.GetLastText()); p_parser.GetNextToken(); - try { - while (p_parser.GetCurrentToken() == TOKEN_NUMBER) { - outcome->SetPayoff(pl++, Number(p_parser.GetLastText())); - if (p_parser.GetNextToken() == TOKEN_COMMA) { - p_parser.GetNextToken(); - } - } + for (auto player : players) { + p_parser.ExpectCurrentToken(TOKEN_NUMBER, "numerical payoff"); + outcome->SetPayoff(player, Number(p_parser.GetLastText())); + p_parser.AcceptNextToken(TOKEN_COMMA); } - catch (IndexException &) { - // This would be triggered by too many payoffs - throw InvalidFileException(p_parser.CreateLineMsg("Exceeded number of players in outcome")); - } - - if (pl <= p_nfg->NumPlayers() || p_parser.GetCurrentToken() != TOKEN_RBRACE) { - throw InvalidFileException( - p_parser.CreateLineMsg("Insufficient number of players in outcome")); - } - + p_parser.ExpectCurrentToken(TOKEN_RBRACE, "'}'"); p_parser.GetNextToken(); } - if (p_parser.GetCurrentToken() != TOKEN_RBRACE) { - throw InvalidFileException(p_parser.CreateLineMsg("Expecting '}' after outcome")); - } + p_parser.ExpectCurrentToken(TOKEN_RBRACE, "'}'"); p_parser.GetNextToken(); } -void ParseOutcomeBody(GameParserState &p_parser, Game &p_nfg) +void ParseOutcomeBody(GameFileLexer &p_parser, Game &p_nfg) { ReadOutcomeList(p_parser, p_nfg); - StrategySupportProfile profile(p_nfg); - StrategyProfileIterator iter(profile); - - while (p_parser.GetCurrentToken() != TOKEN_EOF) { - if (p_parser.GetCurrentToken() != TOKEN_NUMBER) { - throw InvalidFileException(p_parser.CreateLineMsg("Expecting outcome index")); - } - else if (iter.AtEnd()) { - throw InvalidFileException( - p_parser.CreateLineMsg("More outcomes listed than entries in game table")); - } - int outcomeId = atoi(p_parser.GetLastText().c_str()); - if (outcomeId > 0) { + for (StrategyProfileIterator iter(profile); !iter.AtEnd(); iter++) { + p_parser.ExpectCurrentToken(TOKEN_NUMBER, "outcome index"); + if (int outcomeId = std::stoi(p_parser.GetLastText())) { (*iter)->SetOutcome(p_nfg->GetOutcome(outcomeId)); } - else { - (*iter)->SetOutcome(nullptr); - } p_parser.GetNextToken(); - iter++; - } - if (!iter.AtEnd()) { - throw InvalidFileException( - p_parser.CreateLineMsg("Not enough outcomes listed to fill game table")); } } -void ParsePayoffBody(GameParserState &p_parser, Game &p_nfg) +void ParsePayoffBody(GameFileLexer &p_parser, Game &p_nfg) { StrategySupportProfile profile(p_nfg); - StrategyProfileIterator iter(profile); - int pl = 1; - - while (p_parser.GetCurrentToken() != TOKEN_EOF) { - if (p_parser.GetCurrentToken() != TOKEN_NUMBER) { - throw InvalidFileException(p_parser.CreateLineMsg("Expecting payoff")); - } - else if (iter.AtEnd()) { - throw InvalidFileException( - p_parser.CreateLineMsg("More payoffs listed than entries in game table")); - } - else { - (*iter)->GetOutcome()->SetPayoff(pl, Number(p_parser.GetLastText())); + for (StrategyProfileIterator iter(profile); !iter.AtEnd(); iter++) { + for (auto player : p_nfg->GetPlayers()) { + p_parser.ExpectCurrentToken(TOKEN_NUMBER, "numerical payoff"); + (*iter)->GetOutcome()->SetPayoff(player, Number(p_parser.GetLastText())); + p_parser.GetNextToken(); } - - if (++pl > p_nfg->NumPlayers()) { - iter++; - pl = 1; - } - p_parser.GetNextToken(); - } - if (!iter.AtEnd()) { - throw InvalidFileException( - p_parser.CreateLineMsg("Not enough payoffs listed to fill game table")); } } -Game BuildNfg(GameParserState &p_parser, TableFileGame &p_data) +Game BuildNfg(GameFileLexer &p_parser, TableFileGame &p_data) { - Array dim(p_data.NumPlayers()); - for (int pl = 1; pl <= dim.Length(); pl++) { - dim[pl] = p_data.NumStrategies(pl); + if (p_parser.GetCurrentToken() != TOKEN_LBRACE && p_parser.GetCurrentToken() != TOKEN_NUMBER) { + p_parser.OnParseError("Expecting outcome or payoff"); } - Game nfg = NewTable(dim); + // If this looks lke an outcome-based format, then don't create outcomes in advance + Game nfg = NewTable(p_data.NumStrategies(), p_parser.GetCurrentToken() == TOKEN_LBRACE); nfg->SetTitle(p_data.m_title); nfg->SetComment(p_data.m_comment); - for (int pl = 1; pl <= dim.Length(); pl++) { - nfg->GetPlayer(pl)->SetLabel(p_data.GetPlayer(pl)); - for (int st = 1; st <= dim[pl]; st++) { - nfg->GetPlayer(pl)->GetStrategy(st)->SetLabel(p_data.GetStrategy(pl, st)); + for (auto player : nfg->GetPlayers()) { + player->SetLabel(p_data.GetPlayer(player->GetNumber())); + for (auto strategy : player->GetStrategies()) { + strategy->SetLabel(p_data.GetStrategy(player->GetNumber(), strategy->GetNumber())); } } if (p_parser.GetCurrentToken() == TOKEN_LBRACE) { ParseOutcomeBody(p_parser, nfg); } - else if (p_parser.GetCurrentToken() == TOKEN_NUMBER) { - ParsePayoffBody(p_parser, nfg); - } else { - throw InvalidFileException(p_parser.CreateLineMsg("Expecting outcome or payoff")); + ParsePayoffBody(p_parser, nfg); } - + p_parser.ExpectCurrentToken(TOKEN_EOF, "end-of-file"); return nfg; } @@ -659,169 +471,100 @@ Game BuildNfg(GameParserState &p_parser, TableFileGame &p_data) class TreeData { public: std::map m_outcomeMap; - std::map m_chanceInfosetMap; - List> m_infosetMap; - - TreeData() = default; - ~TreeData() = default; + std::map> m_infosetMap; }; -void ReadPlayers(GameParserState &p_state, Game &p_game, TreeData &p_treeData) +void ReadPlayers(GameFileLexer &p_state, Game &p_game, TreeData &p_treeData) { - if (p_state.GetNextToken() != TOKEN_LBRACE) { - throw InvalidFileException(p_state.CreateLineMsg("Expecting '{' before players")); - } - + p_state.ExpectNextToken(TOKEN_LBRACE, "'{'"); while (p_state.GetNextToken() == TOKEN_TEXT) { p_game->NewPlayer()->SetLabel(p_state.GetLastText()); - p_treeData.m_infosetMap.push_back(std::map()); - } - - if (p_state.GetCurrentToken() != TOKEN_RBRACE) { - throw InvalidFileException(p_state.CreateLineMsg("Expecting '}' after players")); } + p_state.ExpectCurrentToken(TOKEN_RBRACE, "'}'"); } -// -// Precondition: Parser state should be expecting the integer index -// of the outcome in a node entry -// -// Postcondition: Parser state is past the outcome entry and should be -// pointing to the 'c', 'p', or 't' token starting the -// next node declaration. -// -void ParseOutcome(GameParserState &p_state, Game &p_game, TreeData &p_treeData, GameNode &p_node) +void ParseOutcome(GameFileLexer &p_state, Game &p_game, TreeData &p_treeData, GameNode &p_node) { - if (p_state.GetCurrentToken() != TOKEN_NUMBER) { - throw InvalidFileException(p_state.CreateLineMsg("Expecting index of outcome")); - } - - int outcomeId = atoi(p_state.GetLastText().c_str()); + p_state.ExpectCurrentToken(TOKEN_NUMBER, "index of outcome"); + int outcomeId = std::stoi(p_state.GetLastText()); p_state.GetNextToken(); if (p_state.GetCurrentToken() == TOKEN_TEXT) { // This node entry contains information about the outcome GameOutcome outcome; - if (p_treeData.m_outcomeMap.count(outcomeId)) { - outcome = p_treeData.m_outcomeMap[outcomeId]; + try { + outcome = p_treeData.m_outcomeMap.at(outcomeId); } - else { + catch (std::out_of_range &) { outcome = p_game->NewOutcome(); p_treeData.m_outcomeMap[outcomeId] = outcome; } - outcome->SetLabel(p_state.GetLastText()); p_node->SetOutcome(outcome); - if (p_state.GetNextToken() != TOKEN_LBRACE) { - throw InvalidFileException(p_state.CreateLineMsg("Expecting '{' before outcome")); - } + p_state.ExpectNextToken(TOKEN_LBRACE, "'{'"); p_state.GetNextToken(); - - for (int pl = 1; pl <= p_game->NumPlayers(); pl++) { - if (p_state.GetCurrentToken() == TOKEN_NUMBER) { - outcome->SetPayoff(pl, Number(p_state.GetLastText())); - } - else { - throw InvalidFileException(p_state.CreateLineMsg("Payoffs should be numbers")); - } - - // Commas are optional between payoffs - if (p_state.GetNextToken() == TOKEN_COMMA) { - p_state.GetNextToken(); - } - } - - if (p_state.GetCurrentToken() != TOKEN_RBRACE) { - throw InvalidFileException(p_state.CreateLineMsg("Expecting '}' after outcome")); + for (auto player : p_game->GetPlayers()) { + p_state.ExpectCurrentToken(TOKEN_NUMBER, "numerical payoff"); + outcome->SetPayoff(player, Number(p_state.GetLastText())); + p_state.AcceptNextToken(TOKEN_COMMA); } + p_state.ExpectCurrentToken(TOKEN_RBRACE, "'}'"); p_state.GetNextToken(); } else if (outcomeId != 0) { // The node entry does not contain information about the outcome. - // This means the outcome better have been defined already; - // if not, raise an error. - if (p_treeData.m_outcomeMap.count(outcomeId)) { - p_node->SetOutcome(p_treeData.m_outcomeMap[outcomeId]); + // This means the outcome should have been defined already. + try { + p_node->SetOutcome(p_treeData.m_outcomeMap.at(outcomeId)); } - else { - throw InvalidFileException(p_state.CreateLineMsg("Outcome not defined")); + catch (std::out_of_range) { + p_state.OnParseError("Outcome not defined"); } } } -void ParseNode(GameParserState &p_state, Game p_game, GameNode p_node, TreeData &p_treeData); +void ParseNode(GameFileLexer &p_state, Game p_game, GameNode p_node, TreeData &p_treeData); -// -// Precondition: parser state is expecting the node label -// -// Postcondition: parser state is pointing at the 'c', 'p', or 't' -// beginning the next node entry -// -void ParseChanceNode(GameParserState &p_state, Game &p_game, GameNode &p_node, - TreeData &p_treeData) +void ParseChanceNode(GameFileLexer &p_state, Game &p_game, GameNode &p_node, TreeData &p_treeData) { - if (p_state.GetNextToken() != TOKEN_TEXT) { - throw InvalidFileException(p_state.CreateLineMsg("Expecting label")); - } + p_state.ExpectNextToken(TOKEN_TEXT, "node label"); p_node->SetLabel(p_state.GetLastText()); - if (p_state.GetNextToken() != TOKEN_NUMBER) { - throw InvalidFileException(p_state.CreateLineMsg("Expecting infoset id")); - } - + p_state.ExpectNextToken(TOKEN_NUMBER, "infoset id"); int infosetId = atoi(p_state.GetLastText().c_str()); - GameInfoset infoset; - if (p_treeData.m_chanceInfosetMap.count(infosetId)) { - infoset = p_treeData.m_chanceInfosetMap[infosetId]; - } + GameInfoset infoset = p_treeData.m_infosetMap[0][infosetId]; - p_state.GetNextToken(); - - if (p_state.GetCurrentToken() == TOKEN_TEXT) { + if (p_state.GetNextToken() == TOKEN_TEXT) { // Information set data is specified - List actions, probs; - std::string label = p_state.GetLastText(); + std::list action_labels; + Array probs; - if (p_state.GetNextToken() != TOKEN_LBRACE) { - throw InvalidFileException( - p_state.CreateLineMsg("Expecting '{' before information set data")); - } + std::string label = p_state.GetLastText(); + p_state.ExpectNextToken(TOKEN_LBRACE, "'{'"); p_state.GetNextToken(); do { - if (p_state.GetCurrentToken() != TOKEN_TEXT) { - throw InvalidFileException(p_state.CreateLineMsg("Expecting action")); - } - actions.push_back(p_state.GetLastText()); - - p_state.GetNextToken(); - - if (p_state.GetCurrentToken() == TOKEN_NUMBER) { - probs.push_back(p_state.GetLastText()); - } - else { - throw InvalidFileException(p_state.CreateLineMsg("Expecting probability")); - } - + p_state.ExpectCurrentToken(TOKEN_TEXT, "action label"); + action_labels.push_back(p_state.GetLastText()); + p_state.ExpectNextToken(TOKEN_NUMBER, "action probability"); + probs.push_back(Number(p_state.GetLastText())); p_state.GetNextToken(); } while (p_state.GetCurrentToken() != TOKEN_RBRACE); p_state.GetNextToken(); if (!infoset) { - infoset = p_node->AppendMove(p_game->GetChance(), actions.Length()); - p_treeData.m_chanceInfosetMap[infosetId] = infoset; + infoset = p_node->AppendMove(p_game->GetChance(), action_labels.size()); + p_treeData.m_infosetMap[0][infosetId] = infoset; infoset->SetLabel(label); - for (int act = 1; act <= actions.Length(); act++) { - infoset->GetAction(act)->SetLabel(actions[act]); - } - Array prob_numbers(probs.size()); - for (int act = 1; act <= actions.Length(); act++) { - prob_numbers[act] = Number(probs[act]); + auto action_label = action_labels.begin(); + for (auto action : infoset->GetActions()) { + action->SetLabel(*action_label); + ++action_label; } - p_game->SetChanceProbs(infoset, prob_numbers); + p_game->SetChanceProbs(infoset, probs); } else { - // TODO: Verify actions match up to previous specifications + // TODO: Verify actions match up to any previous specifications p_node->AppendMove(infoset); } } @@ -829,70 +572,50 @@ void ParseChanceNode(GameParserState &p_state, Game &p_game, GameNode &p_node, p_node->AppendMove(infoset); } else { - // Referencing an undefined infoset is an error - throw InvalidFileException(p_state.CreateLineMsg("Referencing an undefined infoset")); + p_state.OnParseError("Referencing an undefined infoset"); } ParseOutcome(p_state, p_game, p_treeData, p_node); - - for (int i = 1; i <= p_node->NumChildren(); i++) { - ParseNode(p_state, p_game, p_node->GetChild(i), p_treeData); + for (auto child : p_node->GetChildren()) { + ParseNode(p_state, p_game, child, p_treeData); } } -void ParsePersonalNode(GameParserState &p_state, Game p_game, GameNode p_node, - TreeData &p_treeData) +void ParsePersonalNode(GameFileLexer &p_state, Game p_game, GameNode p_node, TreeData &p_treeData) { - if (p_state.GetNextToken() != TOKEN_TEXT) { - throw InvalidFileException(p_state.CreateLineMsg("Expecting label")); - } + p_state.ExpectNextToken(TOKEN_TEXT, "node label"); p_node->SetLabel(p_state.GetLastText()); - if (p_state.GetNextToken() != TOKEN_NUMBER) { - throw InvalidFileException(p_state.CreateLineMsg("Expecting player id")); - } - int playerId = atoi(p_state.GetLastText().c_str()); - // This will throw an exception if the player ID is not valid + p_state.ExpectNextToken(TOKEN_NUMBER, "player id"); + int playerId = std::stoi(p_state.GetLastText()); GamePlayer player = p_game->GetPlayer(playerId); - std::map &infosetMap = p_treeData.m_infosetMap[playerId]; - if (p_state.GetNextToken() != TOKEN_NUMBER) { - throw InvalidFileException(p_state.CreateLineMsg("Expecting infoset id")); - } - int infosetId = atoi(p_state.GetLastText().c_str()); - GameInfoset infoset; - if (infosetMap.count(infosetId)) { - infoset = infosetMap[infosetId]; - } + p_state.ExpectNextToken(TOKEN_NUMBER, "infoset id"); + int infosetId = std::stoi(p_state.GetLastText()); + GameInfoset infoset = p_treeData.m_infosetMap[playerId][infosetId]; - p_state.GetNextToken(); - - if (p_state.GetCurrentToken() == TOKEN_TEXT) { + if (p_state.GetNextToken() == TOKEN_TEXT) { // Information set data is specified - List actions; + std::list action_labels; std::string label = p_state.GetLastText(); - if (p_state.GetNextToken() != TOKEN_LBRACE) { - throw InvalidFileException( - p_state.CreateLineMsg("Expecting '{' before information set data")); - } + p_state.ExpectNextToken(TOKEN_LBRACE, "'{'"); p_state.GetNextToken(); do { - if (p_state.GetCurrentToken() != TOKEN_TEXT) { - throw InvalidFileException(p_state.CreateLineMsg("Expecting action")); - } - actions.push_back(p_state.GetLastText()); - + p_state.ExpectCurrentToken(TOKEN_TEXT, "action label"); + action_labels.push_back(p_state.GetLastText()); p_state.GetNextToken(); } while (p_state.GetCurrentToken() != TOKEN_RBRACE); p_state.GetNextToken(); if (!infoset) { - infoset = p_node->AppendMove(player, actions.Length()); - infosetMap[infosetId] = infoset; + infoset = p_node->AppendMove(player, action_labels.size()); + p_treeData.m_infosetMap[playerId][infosetId] = infoset; infoset->SetLabel(label); - for (int act = 1; act <= actions.Length(); act++) { - infoset->GetAction(act)->SetLabel(actions[act]); + auto action_label = action_labels.begin(); + for (auto action : infoset->GetActions()) { + action->SetLabel(*action_label); + ++action_label; } } else { @@ -904,31 +627,24 @@ void ParsePersonalNode(GameParserState &p_state, Game p_game, GameNode p_node, p_node->AppendMove(infoset); } else { - // Referencing an undefined infoset is an error - throw InvalidFileException(p_state.CreateLineMsg("Referencing an undefined infoset")); + p_state.OnParseError("Referencing an undefined infoset"); } ParseOutcome(p_state, p_game, p_treeData, p_node); - - for (int i = 1; i <= p_node->NumChildren(); i++) { - ParseNode(p_state, p_game, p_node->GetChild(i), p_treeData); + for (auto child : p_node->GetChildren()) { + ParseNode(p_state, p_game, child, p_treeData); } } -void ParseTerminalNode(GameParserState &p_state, Game p_game, GameNode p_node, - TreeData &p_treeData) +void ParseTerminalNode(GameFileLexer &p_state, Game p_game, GameNode p_node, TreeData &p_treeData) { - if (p_state.GetNextToken() != TOKEN_TEXT) { - throw InvalidFileException(p_state.CreateLineMsg("Expecting label")); - } - + p_state.ExpectNextToken(TOKEN_TEXT, "node label"); p_node->SetLabel(p_state.GetLastText()); - p_state.GetNextToken(); ParseOutcome(p_state, p_game, p_treeData, p_node); } -void ParseNode(GameParserState &p_state, Game p_game, GameNode p_node, TreeData &p_treeData) +void ParseNode(GameFileLexer &p_state, Game p_game, GameNode p_node, TreeData &p_treeData) { if (p_state.GetLastText() == "c") { ParseChanceNode(p_state, p_game, p_node, p_treeData); @@ -940,34 +656,8 @@ void ParseNode(GameParserState &p_state, Game p_game, GameNode p_node, TreeData ParseTerminalNode(p_state, p_game, p_node, p_treeData); } else { - throw InvalidFileException(p_state.CreateLineMsg("Invalid type of node")); - } -} - -void ParseEfg(GameParserState &p_state, Game p_game, TreeData &p_treeData) -{ - if (p_state.GetNextToken() != TOKEN_NUMBER || p_state.GetLastText() != "2") { - throw InvalidFileException(p_state.CreateLineMsg("Accepting only EFG version 2")); - } - - if (p_state.GetNextToken() != TOKEN_SYMBOL || - (p_state.GetLastText() != "D" && p_state.GetLastText() != "R")) { - throw InvalidFileException(p_state.CreateLineMsg("Accepting only EFG R or D data type")); - } - if (p_state.GetNextToken() != TOKEN_TEXT) { - throw InvalidFileException(p_state.CreateLineMsg("Game title missing")); - } - p_game->SetTitle(p_state.GetLastText()); - - ReadPlayers(p_state, p_game, p_treeData); - - if (p_state.GetNextToken() == TOKEN_TEXT) { - // Read optional comment - p_game->SetComment(p_state.GetLastText()); - p_state.GetNextToken(); + p_state.OnParseError("Invalid type of node"); } - - ParseNode(p_state, p_game, p_game->GetRoot(), p_treeData); } } // end of anonymous namespace @@ -1022,9 +712,53 @@ Game GameXMLSavefile::GetGame() const throw InvalidFileException("No game representation found in document"); } -//========================================================================= -// ReadGame: Global visible function to read an .efg or .nfg file -//========================================================================= +Game ReadEfgFile(std::istream &p_stream) +{ + GameFileLexer parser(p_stream); + + if (parser.GetNextToken() != TOKEN_SYMBOL || parser.GetLastText() != "EFG") { + parser.OnParseError("Expecting EFG file type indicator"); + } + if (parser.GetNextToken() != TOKEN_NUMBER || parser.GetLastText() != "2") { + parser.OnParseError("Accepting only EFG version 2"); + } + if (parser.GetNextToken() != TOKEN_SYMBOL || + (parser.GetLastText() != "D" && parser.GetLastText() != "R")) { + parser.OnParseError("Accepting only EFG R or D data type"); + } + if (parser.GetNextToken() != TOKEN_TEXT) { + parser.OnParseError("Game title missing"); + } + + TreeData treeData; + Game game = NewTree(); + dynamic_cast(*game).SetCanonicalization(false); + game->SetTitle(parser.GetLastText()); + ReadPlayers(parser, game, treeData); + if (parser.GetNextToken() == TOKEN_TEXT) { + // Read optional comment + game->SetComment(parser.GetLastText()); + parser.GetNextToken(); + } + ParseNode(parser, game, game->GetRoot(), treeData); + dynamic_cast(*game).SetCanonicalization(true); + return game; +} + +Game ReadNfgFile(std::istream &p_stream) +{ + GameFileLexer parser(p_stream); + TableFileGame data; + ParseNfgHeader(parser, data); + return BuildNfg(parser, data); +} + +Game ReadGbtFile(std::istream &p_stream) +{ + std::stringstream buffer; + buffer << p_stream.rdbuf(); + return GameXMLSavefile(buffer.str()).GetGame(); +} Game ReadGame(std::istream &p_file) { @@ -1034,41 +768,32 @@ Game ReadGame(std::istream &p_file) throw InvalidFileException("Empty file or string provided"); } try { - GameXMLSavefile doc(buffer.str()); - return doc.GetGame(); + return ReadGbtFile(buffer); } catch (InvalidFileException &) { buffer.seekg(0, std::ios::beg); } - GameParserState parser(buffer); + GameFileLexer parser(buffer); try { if (parser.GetNextToken() != TOKEN_SYMBOL) { - throw InvalidFileException(parser.CreateLineMsg("Expecting file type")); + parser.OnParseError("Expecting file type"); } - + buffer.seekg(0, std::ios::beg); if (parser.GetLastText() == "NFG") { - TableFileGame data; - ParseNfgHeader(parser, data); - return BuildNfg(parser, data); + return ReadNfgFile(buffer); } else if (parser.GetLastText() == "EFG") { - TreeData treeData; - Game game = NewTree(); - dynamic_cast(*game).SetCanonicalization(false); - ParseEfg(parser, game, treeData); - dynamic_cast(*game).SetCanonicalization(true); - return game; + return ReadEfgFile(buffer); } else if (parser.GetLastText() == "#AGG") { - return GameAGGRep::ReadAggFile(buffer); + return ReadAggFile(buffer); } else if (parser.GetLastText() == "#BAGG") { - return GameBAGGRep::ReadBaggFile(buffer); + return ReadBaggFile(buffer); } else { - throw InvalidFileException( - "Tokens 'EFG' or 'NFG' or '#AGG' or '#BAGG' expected at start of file"); + throw InvalidFileException("Unrecognized file format"); } } catch (std::exception &ex) { diff --git a/src/games/game.cc b/src/games/game.cc index 1bd60a4e5..8dadcb94b 100644 --- a/src/games/game.cc +++ b/src/games/game.cc @@ -26,6 +26,7 @@ #include #include "gambit.h" +#include "writer.h" // The references to the table and tree representations violate the logic // of separating implementation types. This will be fixed when we move @@ -265,24 +266,6 @@ Array GameRep::GetStrategies() const // GameRep: Writing data files //------------------------------------------------------------------------ -namespace { - -std::string EscapeQuotes(const std::string &s) -{ - std::string ret; - - for (char c : s) { - if (c == '"') { - ret += '\\'; - } - ret += c; - } - - return ret; -} - -} // end anonymous namespace - /// /// Write the game to a savefile in .nfg payoff format. /// @@ -297,31 +280,29 @@ std::string EscapeQuotes(const std::string &s) void GameRep::WriteNfgFile(std::ostream &p_file) const { auto players = GetPlayers(); - p_file << "NFG 1 R"; - p_file << " \"" << EscapeQuotes(GetTitle()) << "\" { "; - for (auto player : players) { - p_file << '"' << EscapeQuotes(player->GetLabel()) << "\" "; - } - p_file << "}\n\n{ "; - + p_file << "NFG 1 R " << std::quoted(GetTitle()) << ' ' + << FormatList(players, [](const GamePlayer &p) { return QuoteString(p->GetLabel()); }) + << std::endl + << std::endl; + p_file << "{ "; for (auto player : players) { - p_file << "{ "; - for (auto strategy : player->GetStrategies()) { - p_file << '"' << EscapeQuotes(strategy->GetLabel()) << "\" "; - } - p_file << "}\n"; + p_file << FormatList(player->GetStrategies(), [](const GameStrategy &s) { + return QuoteString(s->GetLabel()); + }) << std::endl; } - p_file << "}\n"; - p_file << "\"" << EscapeQuotes(m_comment) << "\"\n\n"; + p_file << "}" << std::endl; + p_file << std::quoted(GetComment()) << std::endl << std::endl; for (StrategyProfileIterator iter(StrategySupportProfile(Game(const_cast(this)))); !iter.AtEnd(); iter++) { - for (auto player : players) { - p_file << (*iter)->GetPayoff(player) << " "; - } - p_file << "\n"; - } - p_file << '\n'; + p_file << FormatList( + players, + [&iter](const GamePlayer &p) { + return lexical_cast((*iter)->GetPayoff(p)); + }, + false, false) + << std::endl; + }; } //======================================================================== diff --git a/src/games/game.h b/src/games/game.h index c89af8ed5..1fb664e5f 100644 --- a/src/games/game.h +++ b/src/games/game.h @@ -478,7 +478,10 @@ class GameRep : public BaseGameRep { throw UndefinedException(); } /// Write the game in .efg format to the specified stream - virtual void WriteEfgFile(std::ostream &) const { throw UndefinedException(); } + virtual void WriteEfgFile(std::ostream &, const GameNode &subtree = 0) const + { + throw UndefinedException(); + } /// Write the game to a file in .nfg payoff format. virtual void WriteNfgFile(std::ostream &p_stream) const; //@} @@ -638,8 +641,36 @@ inline GameStrategy GamePlayerRep::GetStrategy(int st) const Game NewTree(); /// Factory function to create new game table Game NewTable(const Array &p_dim, bool p_sparseOutcomes = false); -/// Reads a game in .efg or .nfg format from the input stream -Game ReadGame(std::istream &); + +/// @brief Reads a game representation in .efg format +/// +/// @param[in] p_stream An input stream, positioned at the start of the text in .efg format +/// @return A handle to the game representation constructed +/// @throw InvalidFileException If the stream does not contain a valid serialisation +/// of a game in .efg format. +/// @sa Game::WriteEfgFile, ReadNfgFile, ReadAggFile, ReadBaggFile +Game ReadEfgFile(std::istream &p_stream); + +/// @brief Reads a game representation in .nfg format +/// @param[in] p_stream An input stream, positioned at the start of the text in .nfg format +/// @return A handle to the game representation constructed +/// @throw InvalidFileException If the stream does not contain a valid serialisation +/// of a game in .nfg format. +/// @sa Game::WriteNfgFile, ReadEfgFile, ReadAggFile, ReadBaggFile +Game ReadNfgFile(std::istream &p_stream); + +/// @brief Reads a game representation from a graphical interface XML saveflie +/// @param[in] p_stream An input stream, positioned at the start of the text +/// @return A handle to the game representation constructed +/// @throw InvalidFileException If the stream does not contain a valid serialisation +/// of a game in an XML savefile +/// @sa ReadEfgFile, ReadNfgFile, ReadAggFile, ReadBaggFile +Game ReadGbtFile(std::istream &p_stream); + +/// @brief Reads a game from the input stream, attempting to autodetect file format +/// @deprecated Deprecated in favour of the various ReadXXXGame functions. +/// @sa ReadEfgFile, ReadNfgFile, ReadGbtFile, ReadAggFile, ReadBaggFile +Game ReadGame(std::istream &p_stream); /// @brief Generate a distribution over a simplex restricted to rational numbers of given /// denominator diff --git a/src/games/gameagg.cc b/src/games/gameagg.cc index 089dcdc9c..fa99371a2 100644 --- a/src/games/gameagg.cc +++ b/src/games/gameagg.cc @@ -331,6 +331,4 @@ void GameAGGRep::WriteAggFile(std::ostream &s) const } } -Game GameAGGRep::ReadAggFile(std::istream &in) { return new GameAGGRep(agg::AGG::makeAGG(in)); } - } // end namespace Gambit diff --git a/src/games/gameagg.h b/src/games/gameagg.h index 69d7ac83b..b1b10204d 100644 --- a/src/games/gameagg.h +++ b/src/games/gameagg.h @@ -35,14 +35,11 @@ class GameAGGRep : public GameRep { std::shared_ptr aggPtr; Array m_players; - /// Constructor - explicit GameAGGRep(std::shared_ptr); - public: /// @name Lifecycle //@{ - /// Create a game from a serialized file in AGG format - static Game ReadAggFile(std::istream &); + /// Constructor + explicit GameAGGRep(std::shared_ptr); /// Destructor ~GameAGGRep() override { @@ -149,10 +146,25 @@ class GameAGGRep : public GameRep { //@{ /// Write the game to a savefile in the specified format. void Write(std::ostream &p_stream, const std::string &p_format = "native") const override; - virtual void WriteAggFile(std::ostream &) const; + void WriteAggFile(std::ostream &) const; //@} }; +/// @brief Reads a game representation in .agg format +/// @param[in] p_stream An input stream, positioned at the start of the text in .agg format +/// @return A handle to the game representation constructed +/// @throw InvalidFileException If the stream does not contain a valid serialisation +/// of a game in .agg format. +inline Game ReadAggFile(std::istream &in) +{ + try { + return new GameAGGRep(agg::AGG::makeAGG(in)); + } + catch (std::runtime_error &ex) { + throw InvalidFileException(ex.what()); + } +} + } // namespace Gambit #endif // GAMEAGG_H diff --git a/src/games/gamebagg.cc b/src/games/gamebagg.cc index 5bf096517..31a32b307 100644 --- a/src/games/gamebagg.cc +++ b/src/games/gamebagg.cc @@ -307,9 +307,4 @@ void GameBAGGRep::Write(std::ostream &p_stream, const std::string &p_format /*=" void GameBAGGRep::WriteBaggFile(std::ostream &s) const { s << (*baggPtr); } -Game GameBAGGRep::ReadBaggFile(std::istream &in) -{ - return new GameBAGGRep(agg::BAGG::makeBAGG(in)); -} - } // end namespace Gambit diff --git a/src/games/gamebagg.h b/src/games/gamebagg.h index 83bd38c98..aa6cbf475 100644 --- a/src/games/gamebagg.h +++ b/src/games/gamebagg.h @@ -36,14 +36,11 @@ class GameBAGGRep : public GameRep { Array agent2baggPlayer; Array m_players; - /// Constructor - explicit GameBAGGRep(std::shared_ptr _baggPtr); - public: /// @name Lifecycle //@{ - /// Create a game from a serialized file in BAGG format - static Game ReadBaggFile(std::istream &); + /// Constructor + explicit GameBAGGRep(std::shared_ptr _baggPtr); /// Destructor ~GameBAGGRep() override { @@ -154,6 +151,21 @@ class GameBAGGRep : public GameRep { //@} }; +/// @brief Reads a game representation in .bagg format +/// @param[in] p_stream An input stream, positioned at the start of the text in .bagg format +/// @return A handle to the game representation constructed +/// @throw InvalidFileException If the stream does not contain a valid serialisation +/// of a game in .bagg format. +inline Game ReadBaggFile(std::istream &in) +{ + try { + return new GameBAGGRep(agg::BAGG::makeBAGG(in)); + } + catch (std::runtime_error &ex) { + throw InvalidFileException(ex.what()); + } +} + } // end namespace Gambit #endif // GAMEBAGG_H diff --git a/src/games/gametable.cc b/src/games/gametable.cc index 86e4e92cf..9ca347972 100644 --- a/src/games/gametable.cc +++ b/src/games/gametable.cc @@ -24,6 +24,7 @@ #include "gambit.h" #include "gametable.h" +#include "writer.h" namespace Gambit { @@ -343,24 +344,6 @@ bool GameTableRep::IsConstSum() const // GameTableRep: Writing data files //------------------------------------------------------------------------ -namespace { - -std::string EscapeQuotes(const std::string &s) -{ - std::string ret; - - for (char c : s) { - if (c == '"') { - ret += '\\'; - } - ret += c; - } - - return ret; -} - -} // end anonymous namespace - /// /// Write the game to a savefile in .nfg outcome format. /// @@ -372,58 +355,35 @@ std::string EscapeQuotes(const std::string &s) /// void GameTableRep::WriteNfgFile(std::ostream &p_file) const { - p_file << "NFG 1 R"; - p_file << " \"" << EscapeQuotes(GetTitle()) << "\" { "; - - for (int i = 1; i <= NumPlayers(); i++) { - p_file << '"' << EscapeQuotes(GetPlayer(i)->GetLabel()) << "\" "; - } - - p_file << "}\n\n{ "; - - for (int i = 1; i <= NumPlayers(); i++) { - GamePlayerRep *player = GetPlayer(i); - p_file << "{ "; - for (int j = 1; j <= player->NumStrategies(); j++) { - p_file << '"' << EscapeQuotes(player->GetStrategy(j)->GetLabel()) << "\" "; - } - p_file << "}\n"; - } - - p_file << "}\n"; - - p_file << "\"" << EscapeQuotes(m_comment) << "\"\n\n"; - - int ncont = 1; - for (int i = 1; i <= NumPlayers(); i++) { - ncont *= m_players[i]->m_strategies.Length(); - } - - p_file << "{\n"; + auto players = GetPlayers(); + p_file << "NFG 1 R " << std::quoted(GetTitle()) << ' ' + << FormatList(players, [](const GamePlayer &p) { return QuoteString(p->GetLabel()); }) + << std::endl + << std::endl; + p_file << "{ "; + for (auto player : players) { + p_file << FormatList(player->GetStrategies(), [](const GameStrategy &s) { + return QuoteString(s->GetLabel()); + }) << std::endl; + } + p_file << "}" << std::endl; + p_file << std::quoted(GetComment()) << std::endl << std::endl; + + p_file << "{" << std::endl; for (auto outcome : m_outcomes) { - p_file << "{ \"" << EscapeQuotes(outcome->m_label) << "\" "; - for (int pl = 1; pl <= m_players.Length(); pl++) { - p_file << (const std::string &)outcome->m_payoffs[pl]; - if (pl < m_players.Length()) { - p_file << ", "; - } - else { - p_file << " }\n"; - } - } + p_file << "{ " + QuoteString(outcome->GetLabel()) << ' ' + << FormatList( + players, + [outcome](const GamePlayer &p) { return std::string(outcome->GetPayoff(p)); }, + true, false) + << " }" << std::endl; } - p_file << "}\n"; + p_file << "}" << std::endl; - for (int cont = 1; cont <= ncont; cont++) { - if (m_results[cont] != 0) { - p_file << m_results[cont]->m_number << ' '; - } - else { - p_file << "0 "; - } + for (auto result : m_results) { + p_file << ((result) ? result->m_number : 0) << ' '; } - - p_file << '\n'; + p_file << std::endl; } //------------------------------------------------------------------------ diff --git a/src/games/gametree.cc b/src/games/gametree.cc index 2cefa4bfb..a54b1bf5f 100644 --- a/src/games/gametree.cc +++ b/src/games/gametree.cc @@ -26,6 +26,7 @@ #include "gambit.h" #include "gametree.h" +#include "writer.h" namespace Gambit { @@ -921,121 +922,59 @@ void GameTreeRep::BuildComputedValues() namespace { -std::string EscapeQuotes(const std::string &s) +void WriteEfgFile(std::ostream &f, const GameNode &n) { - std::string ret; - - for (char c : s) { - if (c == '"') { - ret += '\\'; - } - ret += c; + if (n->IsTerminal()) { + f << "t "; } - - return ret; -} - -void PrintActions(std::ostream &p_stream, GameTreeInfosetRep *p_infoset) -{ - p_stream << "{ "; - for (int act = 1; act <= p_infoset->NumActions(); act++) { - p_stream << '"' << EscapeQuotes(p_infoset->GetAction(act)->GetLabel()) << "\" "; - if (p_infoset->IsChanceInfoset()) { - p_stream << static_cast(p_infoset->GetActionProb(act)) << ' '; - } + else if (n->GetInfoset()->IsChanceInfoset()) { + f << "c "; } - p_stream << "}"; -} - -void WriteEfgFile(std::ostream &f, GameTreeNodeRep *n) -{ - if (n->NumChildren() == 0) { - f << "t \"" << EscapeQuotes(n->GetLabel()) << "\" "; - if (n->GetOutcome()) { - f << n->GetOutcome()->GetNumber() << " \"" << EscapeQuotes(n->GetOutcome()->GetLabel()) - << "\" "; - f << "{ "; - for (int pl = 1; pl <= n->GetGame()->NumPlayers(); pl++) { - f << static_cast(n->GetOutcome()->GetPayoff(pl)); - - if (pl < n->GetGame()->NumPlayers()) { - f << ", "; - } - else { - f << " }\n"; - } - } + else { + f << "p "; + } + f << QuoteString(n->GetLabel()) << ' '; + if (!n->IsTerminal()) { + if (!n->GetInfoset()->IsChanceInfoset()) { + f << n->GetInfoset()->GetPlayer()->GetNumber() << ' '; + } + f << n->GetInfoset()->GetNumber() << " " << QuoteString(n->GetInfoset()->GetLabel()) << ' '; + if (n->GetInfoset()->IsChanceInfoset()) { + f << FormatList(n->GetInfoset()->GetActions(), [n](const GameAction &a) { + return QuoteString(a->GetLabel()) + " " + std::string(a->GetInfoset()->GetActionProb(a)); + }); } else { - f << "0\n"; + f << FormatList(n->GetInfoset()->GetActions(), + [n](const GameAction &a) { return QuoteString(a->GetLabel()); }); } - return; + f << ' '; } - - if (n->GetInfoset()->IsChanceInfoset()) { - f << "c \""; - } - else { - f << "p \""; - } - - f << EscapeQuotes(n->GetLabel()) << "\" "; - if (!n->GetInfoset()->IsChanceInfoset()) { - f << n->GetInfoset()->GetPlayer()->GetNumber() << ' '; - } - f << n->GetInfoset()->GetNumber() << " \"" << EscapeQuotes(n->GetInfoset()->GetLabel()) << "\" "; - PrintActions(f, dynamic_cast(n->GetInfoset().operator->())); - f << " "; if (n->GetOutcome()) { - f << n->GetOutcome()->GetNumber() << " \"" << EscapeQuotes(n->GetOutcome()->GetLabel()) - << "\" "; - f << "{ "; - for (int pl = 1; pl <= n->GetGame()->NumPlayers(); pl++) { - f << static_cast(n->GetOutcome()->GetPayoff(pl)); - - if (pl < n->GetGame()->NumPlayers()) { - f << ", "; - } - else { - f << " }\n"; - } - } + f << n->GetOutcome()->GetNumber() << " " << QuoteString(n->GetOutcome()->GetLabel()) << ' ' + << FormatList( + n->GetGame()->GetPlayers(), + [n](const GamePlayer &p) { return std::string(n->GetOutcome()->GetPayoff(p)); }, true) + << std::endl; } else { - f << "0\n"; + f << "0" << std::endl; + } + for (auto child : n->GetChildren()) { + WriteEfgFile(f, child); } - - for (int i = 1; i <= n->NumChildren(); - WriteEfgFile(f, dynamic_cast(n->GetChild(i++).operator->()))) - ; } } // end anonymous namespace -void GameTreeRep::WriteEfgFile(std::ostream &p_file) const -{ - p_file << "EFG 2 R"; - p_file << " \"" << EscapeQuotes(GetTitle()) << "\" { "; - for (int i = 1; i <= m_players.Length(); i++) { - p_file << '"' << EscapeQuotes(m_players[i]->m_label) << "\" "; - } - p_file << "}\n"; - p_file << "\"" << EscapeQuotes(GetComment()) << "\"\n\n"; - - Gambit::WriteEfgFile(p_file, m_root); -} - -void GameTreeRep::WriteEfgFile(std::ostream &p_file, const GameNode &p_root) const +void GameTreeRep::WriteEfgFile(std::ostream &p_file, const GameNode &p_subtree /* =0 */) const { - p_file << "EFG 2 R"; - p_file << " \"" << EscapeQuotes(GetTitle()) << "\" { "; - for (int i = 1; i <= m_players.Length(); i++) { - p_file << '"' << EscapeQuotes(m_players[i]->m_label) << "\" "; - } - p_file << "}\n"; - p_file << "\"" << EscapeQuotes(GetComment()) << "\"\n\n"; - - Gambit::WriteEfgFile(p_file, dynamic_cast(p_root.operator->())); + p_file << "EFG 2 R " << std::quoted(GetTitle()) << ' ' + << FormatList(GetPlayers(), + [](const GamePlayer &p) { return QuoteString(p->GetLabel()); }) + << std::endl; + p_file << std::quoted(GetComment()) << std::endl << std::endl; + Gambit::WriteEfgFile(p_file, (p_subtree) ? p_subtree : GetRoot()); } void GameTreeRep::WriteNfgFile(std::ostream &p_file) const diff --git a/src/games/gametree.h b/src/games/gametree.h index 39ef5a909..5d0e66f63 100644 --- a/src/games/gametree.h +++ b/src/games/gametree.h @@ -271,8 +271,7 @@ class GameTreeRep : public GameExplicitRep { /// @name Writing data files //@{ - void WriteEfgFile(std::ostream &) const override; - virtual void WriteEfgFile(std::ostream &, const GameNode &p_node) const; + void WriteEfgFile(std::ostream &, const GameNode &p_node = 0) const override; void WriteNfgFile(std::ostream &) const override; //@} diff --git a/src/games/stratspt.cc b/src/games/stratspt.cc index 633c66a2b..7835a04bc 100644 --- a/src/games/stratspt.cc +++ b/src/games/stratspt.cc @@ -97,60 +97,31 @@ bool StrategySupportProfile::IsSubsetOf(const StrategySupportProfile &p_support) return true; } -namespace { - -std::string EscapeQuotes(const std::string &s) -{ - std::string ret; - - for (char c : s) { - if (c == '"') { - ret += '\\'; - } - ret += c; - } - - return ret; -} - -} // end anonymous namespace - void StrategySupportProfile::WriteNfgFile(std::ostream &p_file) const { - p_file << "NFG 1 R"; - p_file << " \"" << EscapeQuotes(m_nfg->GetTitle()) << "\" { "; - - for (auto player : m_nfg->GetPlayers()) { - p_file << '"' << EscapeQuotes(player->GetLabel()) << "\" "; + auto players = m_nfg->GetPlayers(); + p_file << "NFG 1 R " << std::quoted(m_nfg->GetTitle()) << ' ' + << FormatList(players, [](const GamePlayer &p) { return QuoteString(p->GetLabel()); }) + << std::endl + << std::endl; + p_file << "{ "; + for (auto player : players) { + p_file << FormatList(GetStrategies(player), [](const GameStrategy &s) { + return QuoteString(s->GetLabel()); + }) << std::endl; } + p_file << "}" << std::endl; + p_file << std::quoted(m_nfg->GetComment()) << std::endl << std::endl; - p_file << "}\n\n{ "; - - for (auto player : m_nfg->GetPlayers()) { - p_file << "{ "; - for (auto strategy : GetStrategies(player)) { - p_file << '"' << EscapeQuotes(strategy->GetLabel()) << "\" "; - } - p_file << "}\n"; - } - - p_file << "}\n"; - - p_file << "\"" << EscapeQuotes(m_nfg->GetComment()) << "\"\n\n"; - - // For trees, we write the payoff version, since there need not be - // a one-to-one correspondence between outcomes and entries, when there - // are chance moves. - StrategyProfileIterator iter(*this); - - for (; !iter.AtEnd(); iter++) { - for (int pl = 1; pl <= m_nfg->NumPlayers(); pl++) { - p_file << (*iter)->GetPayoff(pl) << " "; - } - p_file << "\n"; - } - - p_file << '\n'; + for (StrategyProfileIterator iter(*this); !iter.AtEnd(); iter++) { + p_file << FormatList( + players, + [&iter](const GamePlayer &p) { + return lexical_cast((*iter)->GetPayoff(p)); + }, + false, false) + << std::endl; + }; } //--------------------------------------------------------------------------- diff --git a/src/games/writer.h b/src/games/writer.h index 546d50c0b..a502eb2a2 100644 --- a/src/games/writer.h +++ b/src/games/writer.h @@ -27,6 +27,46 @@ namespace Gambit { +/// @brief Render text as an explicit double-quoted string, escaping any double-quotes +/// in the text with a backslash +inline std::string QuoteString(const std::string &s) +{ + std::ostringstream ss; + ss << std::quoted(s); + return ss.str(); +} + +/// @brief Render a list of objects as a space-separated list of elements, delimited +/// by curly braces on either side +/// @param[in] p_container The container over which to iterate +/// @param[in] p_renderer A callable which returns a string representing each item +/// @param[in] p_commas Use comma as delimiter between items (default is no) +/// @param[in] p_braces Whether to include curly braces on either side of the list +/// @returns The formatted list as a string following the conventions of Gambit savefiles. +template +std::string FormatList(const C &p_container, T p_renderer, bool p_commas = false, + bool p_braces = true) +{ + std::string s, delim; + if (p_braces) { + s = "{"; + delim = " "; + } + for (auto element : p_container) { + s += delim + p_renderer(element); + if (p_commas) { + delim = ", "; + } + else { + delim = " "; + } + } + if (p_braces) { + s += " }"; + } + return s; +} + /// /// Abstract base class for objects that write games to various formats /// diff --git a/src/tools/convert/convert.cc b/src/tools/convert/convert.cc index c5bc084f7..2bdf6144b 100644 --- a/src/tools/convert/convert.cc +++ b/src/tools/convert/convert.cc @@ -126,6 +126,9 @@ int main(int argc, char *argv[]) try { Gambit::Game game = Gambit::ReadGame(*input_stream); + game->WriteNfgFile(std::cout); + exit(0); + if (rowPlayer < 1 || rowPlayer > game->NumPlayers()) { std::cerr << argv[0] << ": Player " << rowPlayer << " does not exist.\n"; return 1; diff --git a/tests/test_file.py b/tests/test_file.py index 11432939c..8a0d0e910 100644 --- a/tests/test_file.py +++ b/tests/test_file.py @@ -70,7 +70,7 @@ def test_parse_string_removed_player(self): pygambit.Game.parse_game(ft) self.assertEqual( str(e.exception), - "Parse error in game file: line 5:26: Expecting '}' after outcome" + "Parse error in game file: line 5:26: Expected '}'" ) def test_parse_string_extra_payoff(self): @@ -79,7 +79,7 @@ def test_parse_string_extra_payoff(self): pygambit.Game.parse_game(ft) self.assertEqual( str(e.exception), - "Parse error in game file: line 5:29: Expecting '}' after outcome" + "Parse error in game file: line 5:29: Expected '}'" ) def test_write_game_gte_sanity(self): @@ -112,8 +112,7 @@ def test_parse_string_removed_player(self): pygambit.Game.parse_game(ft) self.assertEqual( str(e.exception), - "Parse error in game file: line 1:73: " - "Not enough players for number of strategy entries" + "Parse error in game file: line 1:73: Expected '}'" ) @@ -122,7 +121,7 @@ def test_nfg_payoffs_not_enough(): NFG 1 R "Selten (IJGT, 75), Figure 2, normal form" { "Player 1" "Player 2" } { 3 2 } 1 1 0 2 0 2 1 1 0 3 """ - with pytest.raises(ValueError, match="Not enough payoffs"): + with pytest.raises(ValueError, match="Expected numerical payoff"): pygambit.Game.parse_game(data) @@ -131,7 +130,7 @@ def test_nfg_payoffs_too_many(): NFG 1 R "Selten (IJGT, 75), Figure 2, normal form" { "Player 1" "Player 2" } { 3 2 } 1 1 0 2 0 2 1 1 0 3 2 0 5 1 """ - with pytest.raises(ValueError, match="More payoffs listed"): + with pytest.raises(ValueError, match="end-of-file"): pygambit.Game.parse_game(data) @@ -152,7 +151,7 @@ def test_nfg_outcomes_not_enough(): } 1 2 3 """ - with pytest.raises(ValueError, match="Not enough outcomes"): + with pytest.raises(ValueError, match="Expected outcome index"): pygambit.Game.parse_game(data) @@ -173,5 +172,5 @@ def test_nfg_outcomes_too_many(): } 1 2 3 4 2 """ - with pytest.raises(ValueError, match="More outcomes listed"): + with pytest.raises(ValueError, match="end-of-file"): pygambit.Game.parse_game(data)