Skip to content

Commit 469afe3

Browse files
authored
Make label normalization mandatory and collision-safe, and retime it for players and strategies (#965)
Make NormalizeGameLabels run unconditionally in every reader and remove the normalize_labels / p_normalizeLabels parameter throughout the read path: ReadEfgFile, ReadNfgFile, ReadGbtFile, ReadAggFile, ReadBaggFile, ReadGame, and the pygambit read_* wrappers.
1 parent 1bf645e commit 469afe3

11 files changed

Lines changed: 128 additions & 156 deletions

File tree

src/games/file.cc

Lines changed: 96 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
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+
321369
void ReadPlayers(GameFileLexer &p_state, TableFileGame &p_data)
322370
{
323371
p_state.ExpectNextToken(TOKEN_LBRACE, "'{'");
@@ -472,10 +520,19 @@ class TreeData {
472520
void 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

481538
void 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-
826860
void 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);

src/games/game.h

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1395,38 +1395,32 @@ Game NewTable(const std::vector<int> &p_dim, bool p_sparseOutcomes = false);
13951395
/// @brief Reads a game representation in .efg format
13961396
///
13971397
/// @param[in] p_stream An input stream, positioned at the start of the text in .efg format
1398-
/// @param[in] p_normalizeLabels Require element labels to be nonempty and unique within
1399-
/// their scope
14001398
/// @return A handle to the game representation constructed
14011399
/// @throw InvalidFileException If the stream does not contain a valid serialisation
14021400
/// of a game in .efg format.
14031401
/// @sa Game::WriteEfgFile, ReadNfgFile, ReadAggFile, ReadBaggFile
1404-
Game ReadEfgFile(std::istream &p_stream, bool p_normalizeLabels = false);
1402+
Game ReadEfgFile(std::istream &p_stream);
14051403

14061404
/// @brief Reads a game representation in .nfg format
14071405
/// @param[in] p_stream An input stream, positioned at the start of the text in .nfg format
1408-
/// @param[in] p_normalizeLabels Require element labels to be nonempty and unique within
1409-
/// their scope
14101406
/// @return A handle to the game representation constructed
14111407
/// @throw InvalidFileException If the stream does not contain a valid serialisation
14121408
/// of a game in .nfg format.
14131409
/// @sa Game::WriteNfgFile, ReadEfgFile, ReadAggFile, ReadBaggFile
1414-
Game ReadNfgFile(std::istream &p_stream, bool p_normalizeLabels = false);
1410+
Game ReadNfgFile(std::istream &p_stream);
14151411

14161412
/// @brief Reads a game representation from a graphical interface XML saveflie
14171413
/// @param[in] p_stream An input stream, positioned at the start of the text
1418-
/// @param[in] p_normalizeLabels Require element labels to be nonempty and unique within
1419-
/// their scope
14201414
/// @return A handle to the game representation constructed
14211415
/// @throw InvalidFileException If the stream does not contain a valid serialisation
14221416
/// of a game in an XML savefile
14231417
/// @sa ReadEfgFile, ReadNfgFile, ReadAggFile, ReadBaggFile
1424-
Game ReadGbtFile(std::istream &p_stream, bool p_normalizeLabels = false);
1418+
Game ReadGbtFile(std::istream &p_stream);
14251419

14261420
/// @brief Reads a game from the input stream, attempting to autodetect file format
14271421
/// @deprecated Deprecated in favour of the various ReadXXXGame functions.
14281422
/// @sa ReadEfgFile, ReadNfgFile, ReadGbtFile, ReadAggFile, ReadBaggFile
1429-
Game ReadGame(std::istream &p_stream, bool p_normalizeLabels = false);
1423+
Game ReadGame(std::istream &p_stream);
14301424

14311425
/// @brief Generate a distribution over a simplex restricted to rational numbers of given
14321426
/// denominator

src/games/gameagg.h

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -113,15 +113,7 @@ class GameAGGRep : public GameRep {
113113
/// @return A handle to the game representation constructed
114114
/// @throw InvalidFileException If the stream does not contain a valid serialisation
115115
/// of a game in .agg format.
116-
inline Game ReadAggFile(std::istream &p_stream)
117-
{
118-
try {
119-
return std::make_shared<GameAGGRep>(agg::AGG::makeAGG(p_stream));
120-
}
121-
catch (std::runtime_error &ex) {
122-
throw InvalidFileException(ex.what());
123-
}
124-
}
116+
Game ReadAggFile(std::istream &p_stream);
125117

126118
} // namespace Gambit
127119

src/games/gamebagg.h

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -121,15 +121,7 @@ class GameBAGGRep : public GameRep {
121121
/// @return A handle to the game representation constructed
122122
/// @throw InvalidFileException If the stream does not contain a valid serialisation
123123
/// of a game in .bagg format.
124-
inline Game ReadBaggFile(std::istream &in)
125-
{
126-
try {
127-
return std::make_shared<GameBAGGRep>(agg::BAGG::makeBAGG(in));
128-
}
129-
catch (std::runtime_error &ex) {
130-
throw InvalidFileException(ex.what());
131-
}
132-
}
124+
Game ReadBaggFile(std::istream &in);
133125

134126
} // end namespace Gambit
135127

src/pygambit/gambit.pxd

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -488,11 +488,11 @@ cdef extern from "games/layout.h":
488488

489489

490490
cdef extern from "util.h":
491-
c_Game ParseGbtGame(string, bint) except +IOError
492-
c_Game ParseEfgGame(string, bint) except +IOError
493-
c_Game ParseNfgGame(string, bint) except +IOError
494-
c_Game ParseAggGame(string, bint) except +IOError
495-
c_Game ParseBaggGame(string, bint) except +IOError
491+
c_Game ParseGbtGame(string) except +IOError
492+
c_Game ParseEfgGame(string) except +IOError
493+
c_Game ParseNfgGame(string) except +IOError
494+
c_Game ParseAggGame(string) except +IOError
495+
c_Game ParseBaggGame(string) except +IOError
496496
string WriteEfgFile(c_Game)
497497
string WriteNfgFile(c_Game)
498498
string WriteNfgFileSupport(c_StrategySupportProfile) except +IOError

0 commit comments

Comments
 (0)