2323#include < iostream>
2424#include < fstream>
2525#include < map>
26+ #include < set>
2627#include < algorithm>
2728
2829#include " gambit.h"
@@ -318,6 +319,53 @@ class TableFileGame {
318319 }
319320};
320321
322+ // / Normalizes labels in place so the resulting set is distinct and nonempty:
323+ // / empty labels are given a suffix and repeated labels are de-duplicated by
324+ // / appending "_n", choosing the next n not already present in the scope.
325+ // / `p_get(element)` reads an element's label and `p_set(element, label)`
326+ // / writes it, so this works both on a container of game objects (via GetLabel/SetLabel)
327+ // / and on a container of raw label strings (read/write the string directly).
328+ template <class Container , class Getter , class Setter >
329+ void NormalizeLabels (Container &&p_container, Getter p_get, Setter p_set)
330+ {
331+ // NOLINTBEGIN(misc-const-correctness)
332+ std::map<std::string, std::size_t > counts;
333+ std::set<std::string> used;
334+ // NOLINTEND(misc-const-correctness)
335+ for (auto &&element : p_container) {
336+ counts[p_get (element)] += 1 ;
337+ used.insert (p_get (element));
338+ }
339+ // NOLINTBEGIN(misc-const-correctness)
340+ std::map<std::string, std::size_t > visited;
341+ // NOLINTEND(misc-const-correctness)
342+ for (auto &&element : p_container) {
343+ const auto label = p_get (element);
344+ // A special case: If only one label is the empty string we still want to
345+ // convert it to "_1"
346+ if (counts[label] == 1 && label != " " ) {
347+ continue ;
348+ }
349+ // Generate the next "label_n" that is not already used in this scope, so
350+ // that e.g. {"x", "x", "x_1"} does not renumber to a duplicate "x_1".
351+ std::string candidate;
352+ do {
353+ const auto index = ++visited[label];
354+ candidate = label + " _" + std::to_string (index);
355+ } while (used.count (candidate) > 0 );
356+ used.insert (candidate);
357+ p_set (element, candidate);
358+ }
359+ }
360+
361+ // / Normalizes a list of raw label strings.
362+ template <class Container > void NormalizeLabelStrings (Container &p_labels)
363+ {
364+ NormalizeLabels (
365+ p_labels, [](const std::string &s) { return s; },
366+ [](std::string &s, const std::string &v) { s = v; });
367+ }
368+
321369void ReadPlayers (GameFileLexer &p_state, TableFileGame &p_data)
322370{
323371 p_state.ExpectNextToken (TOKEN_LBRACE , " '{'" );
@@ -472,10 +520,19 @@ class TreeData {
472520void ReadPlayers (GameFileLexer &p_state, Game &p_game, TreeData &p_treeData)
473521{
474522 p_state.ExpectNextToken (TOKEN_LBRACE , " '{'" );
523+ // Buffer the raw player labels so they can be normalized (made unique and nonempty)
524+ // before the player objects are created.
525+ // NOLINTBEGIN(misc-const-correctness)
526+ std::vector<std::string> player_labels;
527+ // NOLINTEND(misc-const-correctness)
475528 while (p_state.GetNextToken () == TOKEN_TEXT ) {
476- p_game-> NewPlayer ()-> SetLabel (p_state.GetLastText ());
529+ player_labels. push_back (p_state.GetLastText ());
477530 }
478531 p_state.ExpectCurrentToken (TOKEN_RBRACE , " '}'" );
532+ NormalizeLabelStrings (player_labels);
533+ for (const auto &label : player_labels) {
534+ p_game->NewPlayer ()->SetLabel (label);
535+ }
479536}
480537
481538void CheckOutcomeDefinition (const GameFileLexer &p_state, int p_outcomeId,
@@ -800,48 +857,27 @@ Game GameXMLSavefile::GetGame() const
800857 throw InvalidFileException (" No game representation found in document" );
801858}
802859
803- template <class C > void NormalizeLabels (C &&p_container)
804- {
805- // NOLINTBEGIN(misc-const-correctness)
806- std::map<std::string, std::size_t > counts;
807- // NOLINTEND(misc-const-correctness)
808- for (const auto &element : p_container) {
809- counts[element->GetLabel ()] += 1 ;
810- }
811- // NOLINTBEGIN(misc-const-correctness)
812- std::map<std::string, std::size_t > visited;
813- // NOLINTEND(misc-const-correctness)
814- for (auto element : p_container) {
815- const auto label = element->GetLabel ();
816- // A special case: If only one label is the empty string we still want to
817- // convert it to "_1"
818- if (counts[label] == 1 && label != " " ) {
819- continue ;
820- }
821- const auto index = ++visited[label];
822- element->SetLabel (label + " _" + std::to_string (index));
823- }
824- }
825-
826860void NormalizeGameLabels (const Game &p_game)
827861{
828- NormalizeLabels (p_game->GetPlayers ());
829- NormalizeLabels (p_game->GetOutcomes ());
862+ const auto get_label = [](const auto &e) { return e->GetLabel (); };
863+ const auto set_label = [](const auto &e, const std::string &s) { e->SetLabel (s); };
864+ NormalizeLabels (p_game->GetPlayers (), get_label, set_label);
865+ NormalizeLabels (p_game->GetOutcomes (), get_label, set_label);
830866 if (p_game->IsTree ()) {
831867 for (const auto &player : p_game->GetPlayersWithChance ()) {
832868 for (const auto &infoset : player->GetInfosets ()) {
833- NormalizeLabels (infoset->GetActions ());
869+ NormalizeLabels (infoset->GetActions (), get_label, set_label );
834870 }
835871 }
836872 }
837873 else {
838874 for (const auto &player : p_game->GetPlayers ()) {
839- NormalizeLabels (player->GetStrategies ());
875+ NormalizeLabels (player->GetStrategies (), get_label, set_label );
840876 }
841877 }
842878}
843879
844- Game ReadEfgFile (std::istream &p_stream, bool p_normalizeLabels /* = false */ )
880+ Game ReadEfgFile (std::istream &p_stream)
845881{
846882 GameFileLexer parser (p_stream);
847883
@@ -869,64 +905,73 @@ Game ReadEfgFile(std::istream &p_stream, bool p_normalizeLabels /* = false */)
869905 parser.GetNextToken ();
870906 }
871907 ParseNode (parser, game, game->GetRoot (), treeData);
872- if (p_normalizeLabels) {
873- NormalizeGameLabels (game);
874- }
908+ NormalizeGameLabels (game);
875909 return game;
876910}
877911
878- Game ReadNfgFile (std::istream &p_stream, bool p_normalizeLabels /* = false */ )
912+ Game ReadNfgFile (std::istream &p_stream)
879913{
880914 GameFileLexer parser (p_stream);
881915 TableFileGame data;
882916 ParseNfgHeader (parser, data);
883- auto game = BuildNfg (parser, data);
884- if (p_normalizeLabels) {
885- NormalizeGameLabels (game);
917+ // Normalize player and strategy labels on the raw lists before the game is
918+ // built, so labels are unique and nonempty at construction.
919+ for (auto &player : data.m_players ) {
920+ NormalizeLabelStrings (player.m_strategies );
886921 }
922+ {
923+ // NOLINTBEGIN(misc-const-correctness)
924+ std::vector<std::string> player_labels;
925+ // NOLINTEND(misc-const-correctness)
926+ for (const auto &player : data.m_players ) {
927+ player_labels.push_back (player.m_name );
928+ }
929+ NormalizeLabelStrings (player_labels);
930+ auto label_it = player_labels.begin ();
931+ for (auto &player : data.m_players ) {
932+ player.m_name = *label_it;
933+ ++label_it;
934+ }
935+ }
936+ auto game = BuildNfg (parser, data);
937+ NormalizeGameLabels (game);
887938 return game;
888939}
889940
890- Game ReadGbtFile (std::istream &p_stream, bool p_normalizeLabels /* = false */ )
941+ Game ReadGbtFile (std::istream &p_stream)
891942{
892943 std::stringstream buffer;
893944 buffer << p_stream.rdbuf ();
894945 auto game = GameXMLSavefile (buffer.str ()).GetGame ();
895- if (p_normalizeLabels) {
896- NormalizeGameLabels (game);
897- }
946+ NormalizeGameLabels (game);
898947 return game;
899948}
900949
901- Game ReadAggFile (std::istream &p_stream, bool p_normalizeLabels /* = false */ )
950+ Game ReadAggFile (std::istream &p_stream)
902951{
903952 try {
904953 auto game = std::make_shared<GameAGGRep>(agg::AGG::makeAGG (p_stream));
905- if (p_normalizeLabels) {
906- NormalizeGameLabels (game);
907- }
954+ NormalizeGameLabels (game);
908955 return game;
909956 }
910957 catch (std::runtime_error &ex) {
911958 throw InvalidFileException (ex.what ());
912959 }
913960}
914961
915- Game ReadBaggFile (std::istream &p_stream, bool p_normalizeLabels /* = false */ )
962+ Game ReadBaggFile (std::istream &p_stream)
916963{
917964 try {
918965 auto game = std::make_shared<GameBAGGRep>(agg::BAGG::makeBAGG (p_stream));
919- if (p_normalizeLabels) {
920- NormalizeGameLabels (game);
921- }
966+ NormalizeGameLabels (game);
922967 return game;
923968 }
924969 catch (std::runtime_error &ex) {
925970 throw InvalidFileException (ex.what ());
926971 }
927972}
928973
929- Game ReadGame (std::istream &p_file, bool p_normalizeLabels /* = false */ )
974+ Game ReadGame (std::istream &p_file)
930975{
931976 std::stringstream buffer;
932977 buffer << p_file.rdbuf ();
@@ -947,10 +992,10 @@ Game ReadGame(std::istream &p_file, bool p_normalizeLabels /* = false */)
947992 }
948993 buffer.seekg (0 , std::ios::beg);
949994 if (parser.GetLastText () == " NFG" ) {
950- return ReadNfgFile (buffer, p_normalizeLabels );
995+ return ReadNfgFile (buffer);
951996 }
952997 if (parser.GetLastText () == " EFG" ) {
953- return ReadEfgFile (buffer, p_normalizeLabels );
998+ return ReadEfgFile (buffer);
954999 }
9551000 if (parser.GetLastText () == " #AGG" ) {
9561001 return ReadAggFile (buffer);
0 commit comments