From 7a2f4d051aa0357854c50368e3dcdad04d8ad376 Mon Sep 17 00:00:00 2001 From: drdkad Date: Mon, 29 Jun 2026 17:33:59 +0100 Subject: [PATCH 1/4] Remove debug prints from Sequence --- src/pygambit/strategy.pxi | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/pygambit/strategy.pxi b/src/pygambit/strategy.pxi index 048a9ef52..f46602b8b 100644 --- a/src/pygambit/strategy.pxi +++ b/src/pygambit/strategy.pxi @@ -150,11 +150,6 @@ class Sequence: return f"Sequence(player={self.player}, actions={self.actions})" def __eq__(self, other: typing.Any) -> bool: - print("__eq__") - print(isinstance(other, Sequence)) - print(type(other)) - if isinstance(other, Sequence): - print(self.sequence.deref() == cython.cast(Sequence, other).sequence.deref()) return ( isinstance(other, Sequence) and self.sequence.deref() == cython.cast(Sequence, other).sequence.deref() @@ -176,7 +171,6 @@ class Sequence: @property def parent(self) -> Sequence | None: """The parent (predecessor) of the sequence.""" - print(self) if self.sequence.deref().GetParent() == cython.cast(c_GameSequence, NULL): return None return Sequence.wrap(self.sequence.deref().GetParent()) @@ -185,10 +179,7 @@ class Sequence: def children(self) -> list[Sequence]: """The immediate children (successors) of the sequence.""" ret: list[Sequence] = [] - print("Looking for children of", self) for seq in self.player.sequences: - print("Sequence", seq) - print("Parent", seq.parent) if seq.parent == self: ret.append(seq) return ret From 2b9c36fdf6be08a76a6ed8c83c7730ab02f13f3c Mon Sep 17 00:00:00 2001 From: drdkad Date: Tue, 30 Jun 2026 22:21:06 +0100 Subject: [PATCH 2/4] 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. --- src/games/file.cc | 36 ++++++++++++--------------------- src/games/game.h | 14 ++++--------- src/games/gameagg.h | 10 +-------- src/games/gamebagg.h | 10 +-------- src/pygambit/gambit.pxd | 10 ++++----- src/pygambit/game.pxi | 45 +++++++++++------------------------------ src/pygambit/util.h | 18 +++++++---------- tests/test_io.py | 12 +---------- tests/test_mixed.py | 7 ------- tests/test_node.py | 2 +- 10 files changed, 45 insertions(+), 119 deletions(-) diff --git a/src/games/file.cc b/src/games/file.cc index ea033eebf..855da167c 100644 --- a/src/games/file.cc +++ b/src/games/file.cc @@ -841,7 +841,7 @@ void NormalizeGameLabels(const Game &p_game) } } -Game ReadEfgFile(std::istream &p_stream, bool p_normalizeLabels /* = false */) +Game ReadEfgFile(std::istream &p_stream) { GameFileLexer parser(p_stream); @@ -869,42 +869,34 @@ Game ReadEfgFile(std::istream &p_stream, bool p_normalizeLabels /* = false */) parser.GetNextToken(); } ParseNode(parser, game, game->GetRoot(), treeData); - if (p_normalizeLabels) { - NormalizeGameLabels(game); - } + NormalizeGameLabels(game); return game; } -Game ReadNfgFile(std::istream &p_stream, bool p_normalizeLabels /* = false */) +Game ReadNfgFile(std::istream &p_stream) { GameFileLexer parser(p_stream); TableFileGame data; ParseNfgHeader(parser, data); auto game = BuildNfg(parser, data); - if (p_normalizeLabels) { - NormalizeGameLabels(game); - } + NormalizeGameLabels(game); return game; } -Game ReadGbtFile(std::istream &p_stream, bool p_normalizeLabels /* = false */) +Game ReadGbtFile(std::istream &p_stream) { std::stringstream buffer; buffer << p_stream.rdbuf(); auto game = GameXMLSavefile(buffer.str()).GetGame(); - if (p_normalizeLabels) { - NormalizeGameLabels(game); - } + NormalizeGameLabels(game); return game; } -Game ReadAggFile(std::istream &p_stream, bool p_normalizeLabels /* = false */) +Game ReadAggFile(std::istream &p_stream) { try { auto game = std::make_shared(agg::AGG::makeAGG(p_stream)); - if (p_normalizeLabels) { - NormalizeGameLabels(game); - } + NormalizeGameLabels(game); return game; } catch (std::runtime_error &ex) { @@ -912,13 +904,11 @@ Game ReadAggFile(std::istream &p_stream, bool p_normalizeLabels /* = false */) } } -Game ReadBaggFile(std::istream &p_stream, bool p_normalizeLabels /* = false */) +Game ReadBaggFile(std::istream &p_stream) { try { auto game = std::make_shared(agg::BAGG::makeBAGG(p_stream)); - if (p_normalizeLabels) { - NormalizeGameLabels(game); - } + NormalizeGameLabels(game); return game; } catch (std::runtime_error &ex) { @@ -926,7 +916,7 @@ Game ReadBaggFile(std::istream &p_stream, bool p_normalizeLabels /* = false */) } } -Game ReadGame(std::istream &p_file, bool p_normalizeLabels /* = false */) +Game ReadGame(std::istream &p_file) { std::stringstream buffer; buffer << p_file.rdbuf(); @@ -947,10 +937,10 @@ Game ReadGame(std::istream &p_file, bool p_normalizeLabels /* = false */) } buffer.seekg(0, std::ios::beg); if (parser.GetLastText() == "NFG") { - return ReadNfgFile(buffer, p_normalizeLabels); + return ReadNfgFile(buffer); } if (parser.GetLastText() == "EFG") { - return ReadEfgFile(buffer, p_normalizeLabels); + return ReadEfgFile(buffer); } if (parser.GetLastText() == "#AGG") { return ReadAggFile(buffer); diff --git a/src/games/game.h b/src/games/game.h index a6a82f03a..717b948c1 100644 --- a/src/games/game.h +++ b/src/games/game.h @@ -1395,38 +1395,32 @@ Game NewTable(const std::vector &p_dim, bool p_sparseOutcomes = false); /// @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 -/// @param[in] p_normalizeLabels Require element labels to be nonempty and unique within -/// their scope /// @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, bool p_normalizeLabels = false); +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 -/// @param[in] p_normalizeLabels Require element labels to be nonempty and unique within -/// their scope /// @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, bool p_normalizeLabels = false); +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 -/// @param[in] p_normalizeLabels Require element labels to be nonempty and unique within -/// their scope /// @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, bool p_normalizeLabels = false); +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, bool p_normalizeLabels = false); +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.h b/src/games/gameagg.h index bf000019e..3b0bb979e 100644 --- a/src/games/gameagg.h +++ b/src/games/gameagg.h @@ -113,15 +113,7 @@ class GameAGGRep : public GameRep { /// @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 &p_stream) -{ - try { - return std::make_shared(agg::AGG::makeAGG(p_stream)); - } - catch (std::runtime_error &ex) { - throw InvalidFileException(ex.what()); - } -} +Game ReadAggFile(std::istream &p_stream); } // namespace Gambit diff --git a/src/games/gamebagg.h b/src/games/gamebagg.h index b4a28cb43..641a13735 100644 --- a/src/games/gamebagg.h +++ b/src/games/gamebagg.h @@ -121,15 +121,7 @@ class GameBAGGRep : public GameRep { /// @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 std::make_shared(agg::BAGG::makeBAGG(in)); - } - catch (std::runtime_error &ex) { - throw InvalidFileException(ex.what()); - } -} +Game ReadBaggFile(std::istream &in); } // end namespace Gambit diff --git a/src/pygambit/gambit.pxd b/src/pygambit/gambit.pxd index 099e4aa2a..4d6781edb 100644 --- a/src/pygambit/gambit.pxd +++ b/src/pygambit/gambit.pxd @@ -488,11 +488,11 @@ cdef extern from "games/layout.h": cdef extern from "util.h": - c_Game ParseGbtGame(string, bint) except +IOError - c_Game ParseEfgGame(string, bint) except +IOError - c_Game ParseNfgGame(string, bint) except +IOError - c_Game ParseAggGame(string, bint) except +IOError - c_Game ParseBaggGame(string, bint) except +IOError + c_Game ParseGbtGame(string) except +IOError + c_Game ParseEfgGame(string) except +IOError + c_Game ParseNfgGame(string) except +IOError + c_Game ParseAggGame(string) except +IOError + c_Game ParseBaggGame(string) except +IOError string WriteEfgFile(c_Game) string WriteNfgFile(c_Game) string WriteNfgFileSupport(c_StrategySupportProfile) except +IOError diff --git a/src/pygambit/game.pxi b/src/pygambit/game.pxi index 39cf0acbd..403d095d8 100644 --- a/src/pygambit/game.pxi +++ b/src/pygambit/game.pxi @@ -31,12 +31,11 @@ import scipy.stats import pygambit.gameiter ctypedef string (*GameWriter)(const c_Game &) except +IOError -ctypedef c_Game (*GameParser)(const string &, bool) except +IOError +ctypedef c_Game (*GameParser)(const string &) except +IOError @cython.cfunc def read_game(filepath_or_buffer: str | pathlib.Path | io.IOBase, - normalize_labels: bool, parser: GameParser): g = cython.declare(Game) @@ -48,23 +47,19 @@ def read_game(filepath_or_buffer: str | pathlib.Path | io.IOBase, with open(filepath_or_buffer, "rb") as f: data = f.read() try: - g = Game.wrap(parser(data, normalize_labels)) + g = Game.wrap(parser(data)) except Exception as exc: raise ValueError(f"Parse error in game file: {exc}") from None return g -def read_gbt(filepath_or_buffer: str | pathlib.Path | io.IOBase, - normalize_labels: bool = False) -> Game: +def read_gbt(filepath_or_buffer: str | pathlib.Path | io.IOBase) -> Game: """Construct a game from its serialised representation in a GBT file. Parameters ---------- filepath_or_buffer : str, pathlib.Path or io.IOBase The path to the file containing the game representation or file-like object - normalize_labels : bool (default False) - Ensure all labels are nonempty and unique within their scopes. - This will be enforced in a future version of Gambit. Returns ------- @@ -82,20 +77,16 @@ def read_gbt(filepath_or_buffer: str | pathlib.Path | io.IOBase, -------- read_efg, read_nfg, read_agg, read_bagg """ - return read_game(filepath_or_buffer, normalize_labels, parser=ParseGbtGame) + return read_game(filepath_or_buffer, parser=ParseGbtGame) -def read_efg(filepath_or_buffer: str | pathlib.Path | io.IOBase, - normalize_labels: bool = False) -> Game: +def read_efg(filepath_or_buffer: str | pathlib.Path | io.IOBase) -> Game: """Construct a game from its serialised representation in an EFG file. Parameters ---------- filepath_or_buffer : str, pathlib.Path or io.IOBase The path to the file containing the game representation or file-like object - normalize_labels : bool (default False) - Ensure all labels are nonempty and unique within their scopes. - This will be enforced in a future version of Gambit. Returns ------- @@ -113,20 +104,16 @@ def read_efg(filepath_or_buffer: str | pathlib.Path | io.IOBase, -------- read_gbt, read_nfg, read_agg, read_bagg """ - return read_game(filepath_or_buffer, normalize_labels, parser=ParseEfgGame) + return read_game(filepath_or_buffer, parser=ParseEfgGame) -def read_nfg(filepath_or_buffer: str | pathlib.Path | io.IOBase, - normalize_labels: bool = False) -> Game: +def read_nfg(filepath_or_buffer: str | pathlib.Path | io.IOBase) -> Game: """Construct a game from its serialised representation in a NFG file. Parameters ---------- filepath_or_buffer : str, pathlib.Path or io.IOBase The path to the file containing the game representation or file-like object - normalize_labels : bool (default False) - Ensure all labels are nonempty and unique within their scopes. - This will be enforced in a future version of Gambit. Returns ------- @@ -144,20 +131,16 @@ def read_nfg(filepath_or_buffer: str | pathlib.Path | io.IOBase, -------- read_gbt, read_efg, read_agg, read_bagg """ - return read_game(filepath_or_buffer, normalize_labels, parser=ParseNfgGame) + return read_game(filepath_or_buffer, parser=ParseNfgGame) -def read_agg(filepath_or_buffer: str | pathlib.Path | io.IOBase, - normalize_labels: bool = False) -> Game: +def read_agg(filepath_or_buffer: str | pathlib.Path | io.IOBase) -> Game: """Construct a game from its serialised representation in an AGG file. Parameters ---------- filepath_or_buffer : str, pathlib.Path or io.IOBase The path to the file containing the game representation or file-like object - normalize_labels : bool (default False) - Ensure all labels are nonempty and unique within their scopes. - This will be enforced in a future version of Gambit. Returns ------- @@ -175,20 +158,16 @@ def read_agg(filepath_or_buffer: str | pathlib.Path | io.IOBase, -------- read_gbt, read_efg, read_nfg, read_bagg """ - return read_game(filepath_or_buffer, normalize_labels, parser=ParseAggGame) + return read_game(filepath_or_buffer, parser=ParseAggGame) -def read_bagg(filepath_or_buffer: str | pathlib.Path | io.IOBase, - normalize_labels: bool = False) -> Game: +def read_bagg(filepath_or_buffer: str | pathlib.Path | io.IOBase) -> Game: """Construct a game from its serialised representation in a BAGG file. Parameters ---------- filepath_or_buffer : str, pathlib.Path or io.IOBase The path to the file containing the game representation or file-like object - normalize_labels : bool (default False) - Ensure all labels are nonempty and unique within their scopes. - This will be enforced in a future version of Gambit. Returns ------- @@ -206,7 +185,7 @@ def read_bagg(filepath_or_buffer: str | pathlib.Path | io.IOBase, -------- read_gbt, read_efg, read_nfg, read_agg """ - return read_game(filepath_or_buffer, normalize_labels, parser=ParseBaggGame) + return read_game(filepath_or_buffer, parser=ParseBaggGame) @cython.cclass diff --git a/src/pygambit/util.h b/src/pygambit/util.h index 89d5d9dc8..fca1c5353 100644 --- a/src/pygambit/util.h +++ b/src/pygambit/util.h @@ -38,31 +38,27 @@ using namespace std; using namespace Gambit; using namespace Gambit::Nash; -Game ParseGbtGame(std::string const &s, bool p_normalizeLabels) +Game ParseGbtGame(std::string const &s) { std::istringstream f(s); return ReadGbtFile(f); } - -Game ParseEfgGame(std::string const &s, bool p_normalizeLabels) +Game ParseEfgGame(std::string const &s) { std::istringstream f(s); - return ReadEfgFile(f, p_normalizeLabels); + return ReadEfgFile(f); } - -Game ParseNfgGame(std::string const &s, bool p_normalizeLabels) +Game ParseNfgGame(std::string const &s) { std::istringstream f(s); - return ReadNfgFile(f, p_normalizeLabels); + return ReadNfgFile(f); } - -Game ParseAggGame(std::string const &s, bool p_normalizeLabels) +Game ParseAggGame(std::string const &s) { std::istringstream f(s); return ReadAggFile(f); } - -Game ParseBaggGame(std::string const &s, bool p_normalizeLabels) +Game ParseBaggGame(std::string const &s) { std::istringstream f(s); return ReadBaggFile(f); diff --git a/tests/test_io.py b/tests/test_io.py index 826e7fc14..080d00472 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -157,17 +157,7 @@ def test_read_write_nfg(): nfg_game = games.read_from_file("2x2_bimatrix_all_zero_payoffs.nfg") serialized_nfg_game = nfg_game.to_nfg() deserialized_nfg_game = gbt.read_nfg( - io.BytesIO(serialized_nfg_game.encode()), normalize_labels=False + io.BytesIO(serialized_nfg_game.encode()) ) double_serialized_nfg_game = deserialized_nfg_game.to_nfg() assert serialized_nfg_game == double_serialized_nfg_game - - -def test_read_write_nfg_normalize(): - nfg_game = games.read_from_file("2x2_bimatrix_all_zero_payoffs.nfg") - serialized_nfg_game = nfg_game.to_nfg() - deserialized_nfg_game = gbt.read_nfg( - io.BytesIO(serialized_nfg_game.encode()), normalize_labels=True - ) - double_serialized_nfg_game = deserialized_nfg_game.to_nfg() - assert serialized_nfg_game != double_serialized_nfg_game diff --git a/tests/test_mixed.py b/tests/test_mixed.py index 0de526f84..047574c2f 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -309,13 +309,6 @@ def test_profile_indexing_by_invalid_strategy_label( game.mixed_strategy_profile(rational=rational_flag)[strategy_label] -def test_profile_indexing_by_player_and_duplicate_strategy_label(): - game = games.read_from_file("2x2_bimatrix_all_zero_payoffs.nfg") - profile = game.mixed_strategy_profile() - with pytest.raises(ValueError): - profile["Dan"]["defect"] - - @pytest.mark.parametrize( "game,strategy_label,prob,rational_flag", [ diff --git a/tests/test_node.py b/tests/test_node.py index ecf002edc..867077193 100644 --- a/tests/test_node.py +++ b/tests/test_node.py @@ -238,7 +238,7 @@ class SubgameRootsTestCase: factory=functools.partial( games.read_from_file, "subgame_roots_finder_overplapping_infosets_with_Nature.efg"), - expected_paths=[[], ["1"], ["1", "1"], ["1", "1", "1"]] + expected_paths=[[], ["1_2"], ["1_2", "1_3", "1_2"], ["1_3", "1_2"]] ), id="overlapping_infosets_inside_subgames_and_Nature_move" ), From c4cabb5ae49de596d49d3af16b0c1f971ae4b2d5 Mon Sep 17 00:00:00 2001 From: drdkad Date: Wed, 1 Jul 2026 12:56:25 +0100 Subject: [PATCH 3/4] Make label normalization collision-aware --- src/games/file.cc | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/games/file.cc b/src/games/file.cc index 855da167c..13f0c7f9a 100644 --- a/src/games/file.cc +++ b/src/games/file.cc @@ -23,6 +23,7 @@ #include #include #include +#include #include #include "gambit.h" @@ -804,9 +805,11 @@ template void NormalizeLabels(C &&p_container) { // NOLINTBEGIN(misc-const-correctness) std::map counts; + std::set used; // NOLINTEND(misc-const-correctness) for (const auto &element : p_container) { counts[element->GetLabel()] += 1; + used.insert(element->GetLabel()); } // NOLINTBEGIN(misc-const-correctness) std::map visited; @@ -818,8 +821,15 @@ template void NormalizeLabels(C &&p_container) if (counts[label] == 1 && label != "") { continue; } - const auto index = ++visited[label]; - element->SetLabel(label + "_" + std::to_string(index)); + // Generate the next "label_n" that is not already used in this scope, so + // that e.g. {"x", "x", "x_1"} does not renumber to a duplicate "x_1". + std::string candidate; + do { + const auto index = ++visited[label]; + candidate = label + "_" + std::to_string(index); + } while (used.count(candidate) > 0); + used.insert(candidate); + element->SetLabel(candidate); } } From 4fd50f2b68204ba7ce45a5288c6fc18d8b009a96 Mon Sep 17 00:00:00 2001 From: drdkad Date: Wed, 1 Jul 2026 14:05:30 +0100 Subject: [PATCH 4/4] Normalize player and strategy labels on raw lists before construction. Actions and outcomes are deliberately *not* retimed here. This commit is self-contained and can be reverted independently of the mandatory and collision-safe normalization it builds on. --- src/games/file.cc | 119 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 82 insertions(+), 37 deletions(-) diff --git a/src/games/file.cc b/src/games/file.cc index 13f0c7f9a..08961017f 100644 --- a/src/games/file.cc +++ b/src/games/file.cc @@ -319,6 +319,53 @@ class TableFileGame { } }; +/// Normalizes labels in place so the resulting set is distinct and nonempty: +/// empty labels are given a suffix and repeated labels are de-duplicated by +/// appending "_n", choosing the next n not already present in the scope. +/// `p_get(element)` reads an element's label and `p_set(element, label)` +/// writes it, so this works both on a container of game objects (via GetLabel/SetLabel) +/// and on a container of raw label strings (read/write the string directly). +template +void NormalizeLabels(Container &&p_container, Getter p_get, Setter p_set) +{ + // NOLINTBEGIN(misc-const-correctness) + std::map counts; + std::set used; + // NOLINTEND(misc-const-correctness) + for (auto &&element : p_container) { + counts[p_get(element)] += 1; + used.insert(p_get(element)); + } + // NOLINTBEGIN(misc-const-correctness) + std::map visited; + // NOLINTEND(misc-const-correctness) + for (auto &&element : p_container) { + const auto label = p_get(element); + // A special case: If only one label is the empty string we still want to + // convert it to "_1" + if (counts[label] == 1 && label != "") { + continue; + } + // Generate the next "label_n" that is not already used in this scope, so + // that e.g. {"x", "x", "x_1"} does not renumber to a duplicate "x_1". + std::string candidate; + do { + const auto index = ++visited[label]; + candidate = label + "_" + std::to_string(index); + } while (used.count(candidate) > 0); + used.insert(candidate); + p_set(element, candidate); + } +} + +/// Normalizes a list of raw label strings. +template void NormalizeLabelStrings(Container &p_labels) +{ + NormalizeLabels( + p_labels, [](const std::string &s) { return s; }, + [](std::string &s, const std::string &v) { s = v; }); +} + void ReadPlayers(GameFileLexer &p_state, TableFileGame &p_data) { p_state.ExpectNextToken(TOKEN_LBRACE, "'{'"); @@ -473,10 +520,19 @@ class TreeData { void ReadPlayers(GameFileLexer &p_state, Game &p_game, TreeData &p_treeData) { p_state.ExpectNextToken(TOKEN_LBRACE, "'{'"); + // Buffer the raw player labels so they can be normalized (made unique and nonempty) + // before the player objects are created. + // NOLINTBEGIN(misc-const-correctness) + std::vector player_labels; + // NOLINTEND(misc-const-correctness) while (p_state.GetNextToken() == TOKEN_TEXT) { - p_game->NewPlayer()->SetLabel(p_state.GetLastText()); + player_labels.push_back(p_state.GetLastText()); } p_state.ExpectCurrentToken(TOKEN_RBRACE, "'}'"); + NormalizeLabelStrings(player_labels); + for (const auto &label : player_labels) { + p_game->NewPlayer()->SetLabel(label); + } } void CheckOutcomeDefinition(const GameFileLexer &p_state, int p_outcomeId, @@ -801,52 +857,22 @@ Game GameXMLSavefile::GetGame() const throw InvalidFileException("No game representation found in document"); } -template void NormalizeLabels(C &&p_container) -{ - // NOLINTBEGIN(misc-const-correctness) - std::map counts; - std::set used; - // NOLINTEND(misc-const-correctness) - for (const auto &element : p_container) { - counts[element->GetLabel()] += 1; - used.insert(element->GetLabel()); - } - // NOLINTBEGIN(misc-const-correctness) - std::map visited; - // NOLINTEND(misc-const-correctness) - for (auto element : p_container) { - const auto label = element->GetLabel(); - // A special case: If only one label is the empty string we still want to - // convert it to "_1" - if (counts[label] == 1 && label != "") { - continue; - } - // Generate the next "label_n" that is not already used in this scope, so - // that e.g. {"x", "x", "x_1"} does not renumber to a duplicate "x_1". - std::string candidate; - do { - const auto index = ++visited[label]; - candidate = label + "_" + std::to_string(index); - } while (used.count(candidate) > 0); - used.insert(candidate); - element->SetLabel(candidate); - } -} - void NormalizeGameLabels(const Game &p_game) { - NormalizeLabels(p_game->GetPlayers()); - NormalizeLabels(p_game->GetOutcomes()); + const auto get_label = [](const auto &e) { return e->GetLabel(); }; + const auto set_label = [](const auto &e, const std::string &s) { e->SetLabel(s); }; + NormalizeLabels(p_game->GetPlayers(), get_label, set_label); + NormalizeLabels(p_game->GetOutcomes(), get_label, set_label); if (p_game->IsTree()) { for (const auto &player : p_game->GetPlayersWithChance()) { for (const auto &infoset : player->GetInfosets()) { - NormalizeLabels(infoset->GetActions()); + NormalizeLabels(infoset->GetActions(), get_label, set_label); } } } else { for (const auto &player : p_game->GetPlayers()) { - NormalizeLabels(player->GetStrategies()); + NormalizeLabels(player->GetStrategies(), get_label, set_label); } } } @@ -888,6 +914,25 @@ Game ReadNfgFile(std::istream &p_stream) GameFileLexer parser(p_stream); TableFileGame data; ParseNfgHeader(parser, data); + // Normalize player and strategy labels on the raw lists before the game is + // built, so labels are unique and nonempty at construction. + for (auto &player : data.m_players) { + NormalizeLabelStrings(player.m_strategies); + } + { + // NOLINTBEGIN(misc-const-correctness) + std::vector player_labels; + // NOLINTEND(misc-const-correctness) + for (const auto &player : data.m_players) { + player_labels.push_back(player.m_name); + } + NormalizeLabelStrings(player_labels); + auto label_it = player_labels.begin(); + for (auto &player : data.m_players) { + player.m_name = *label_it; + ++label_it; + } + } auto game = BuildNfg(parser, data); NormalizeGameLabels(game); return game;