From 1fb49a2322149d7094ecdff6412eee0d1e04d178 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Mon, 8 Dec 2025 13:36:40 +0000 Subject: [PATCH 01/10] Template nested element collection, use for game infosets --- src/games/game.h | 85 +++----------------------------------- src/games/gameobject.h | 92 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 79 deletions(-) diff --git a/src/games/game.h b/src/games/game.h index 8a702800a..15e943d97 100644 --- a/src/games/game.h +++ b/src/games/game.h @@ -901,12 +901,16 @@ class GameRep : public std::enable_shared_from_this { /// @name Information sets //@{ - class Infosets; + using Infosets = + NestedElementCollection; /// Returns the iset'th information set in the game (numbered globally) virtual GameInfoset GetInfoset(int iset) const { throw UndefinedException(); } /// Returns the set of information sets in the game - virtual Infosets GetInfosets() const; + virtual Infosets GetInfosets() const + { + return Infosets(std::const_pointer_cast(this->shared_from_this())); + } /// Sort the information sets for each player in a canonical order virtual void SortInfosets() {} /// Returns the set of actions taken by the infoset's owner before reaching this infoset @@ -958,83 +962,6 @@ class GameRep : public std::enable_shared_from_this { virtual void BuildComputedValues() const {} }; -class GameRep::Infosets { - Players m_players; - -public: - explicit Infosets(const Players &outer) : m_players(outer) {} - - class iterator { - using OuterIter = Players::iterator; - using InnerIter = GamePlayerRep::Infosets::iterator; - - OuterIter m_playerIterator, m_playerEnd; - InnerIter m_infosetIterator, m_infosetEnd; - - void next() - { - while (m_playerIterator != m_playerEnd && m_infosetIterator == m_infosetEnd) { - ++m_playerIterator; - if (m_playerIterator != m_playerEnd) { - auto infosets = (*m_playerIterator)->GetInfosets(); - m_infosetIterator = infosets.begin(); - m_infosetEnd = infosets.end(); - } - } - } - - public: - using iterator_category = std::forward_iterator_tag; - using value_type = GameInfoset; - using reference = GameInfoset; - using pointer = GameInfoset; - using difference_type = std::ptrdiff_t; - - iterator() = default; - - iterator(const OuterIter &p_playerIterator, const OuterIter &p_playerEnd) - : m_playerIterator(p_playerIterator), m_playerEnd(p_playerEnd) - { - if (m_playerIterator != m_playerEnd) { - const auto infosets = (*m_playerIterator)->GetInfosets(); - m_infosetIterator = infosets.begin(); - m_infosetEnd = infosets.end(); - } - next(); - } - - reference operator*() const { return *m_infosetIterator; } - pointer operator->() const { return *m_infosetIterator; } - - iterator &operator++() - { - ++m_infosetIterator; - next(); - return *this; - } - - iterator operator++(int) - { - iterator tmp = *this; - ++(*this); - return tmp; - } - - friend bool operator==(const iterator &a, const iterator &b) - { - return a.m_playerIterator == b.m_playerIterator && - (a.m_playerIterator == a.m_playerEnd || a.m_infosetIterator == b.m_infosetIterator); - } - - friend bool operator!=(const iterator &a, const iterator &b) { return !(a == b); } - }; - - iterator begin() const { return {m_players.begin(), m_players.end()}; } - iterator end() const { return {m_players.end(), m_players.end()}; } -}; - -inline GameRep::Infosets GameRep::GetInfosets() const { return Infosets(GetPlayers()); } - //======================================================================= // Inline members of game representation classes //======================================================================= diff --git a/src/games/gameobject.h b/src/games/gameobject.h index a8328fe3c..e946df739 100644 --- a/src/games/gameobject.h +++ b/src/games/gameobject.h @@ -214,6 +214,98 @@ template class ElementCollection { iterator cend() const { return {m_owner, m_container, (m_owner) ? m_container->size() : 0}; } }; +/// @brief A view on a nested collection of objects (e.g. infosets of players or strategies of +/// players) +template class NestedElementCollection { + T m_owner; + +public: + class iterator { + using OuterRange = decltype((m_owner.get()->*OuterMemFn)()); + using OuterIter = decltype(std::begin(std::declval())); + + using OuterElem = decltype(*std::declval()); + using InnerRange = decltype((std::declval().get()->*InnerMemFn)()); + using InnerIter = decltype(std::begin(std::declval())); + + T m_owner; + OuterRange m_outerRange; + OuterIter m_outerIt, m_outerEnd; + InnerRange m_innerRange; + InnerIter m_innerIt, m_innerEnd; + + void update_inner() + { + if (m_outerIt != m_outerEnd) { + m_innerRange = ((*m_outerIt).get()->*InnerMemFn)(); + m_innerIt = m_innerRange.begin(); + m_innerEnd = m_innerRange.end(); + } + } + + void skip_empty() + { + while (m_outerIt != m_outerEnd && m_innerIt == m_innerEnd) { + ++m_outerIt; + if (m_outerIt != m_outerEnd) { + update_inner(); + } + } + } + + public: + using iterator_category = std::forward_iterator_tag; + using reference = decltype(*std::declval()); + using pointer = std::add_pointer_t; + using value_type = std::remove_reference_t; + using difference_type = std::ptrdiff_t; + + iterator() = default; + + iterator(T p_owner, const bool p_isEnd) + : m_owner(p_owner), m_outerRange((m_owner.get()->*OuterMemFn)()), + m_outerIt(std::begin(m_outerRange)), m_outerEnd(std::end(m_outerRange)) + { + if (p_isEnd) { + m_outerIt = m_outerEnd; + return; + } + if (m_outerIt != m_outerEnd) { + update_inner(); + } + skip_empty(); + } + + reference operator*() const { return *m_innerIt; } + pointer operator->() const { return *m_innerIt; } + + iterator &operator++() + { + ++m_innerIt; + skip_empty(); + return *this; + } + iterator operator++(int) + { + iterator tmp = *this; + ++(*this); + return tmp; + } + + bool operator==(const iterator &p_other) const + { + return m_outerIt == p_other.m_outerIt && + (m_outerIt == m_outerEnd || m_innerIt == p_other.m_innerIt); + } + bool operator!=(const iterator &p_other) const { return !(*this == p_other); } + }; + + explicit NestedElementCollection(T owner) : m_owner(owner) {} + + iterator begin() const { return {m_owner, false}; } + iterator end() const { return {m_owner, true}; } +}; + } // end namespace Gambit #endif // GAMBIT_GAMES_GAMEOBJECT_H From 0e8e6d183cb056a449a87ef4dffd7a11c16e0185 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Mon, 8 Dec 2025 13:47:55 +0000 Subject: [PATCH 02/10] Add size and shape methods for nestedelementcollection --- src/games/gameobject.h | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/games/gameobject.h b/src/games/gameobject.h index e946df739..78ab5a4c2 100644 --- a/src/games/gameobject.h +++ b/src/games/gameobject.h @@ -302,6 +302,26 @@ template class NestedElementCollecti explicit NestedElementCollection(T owner) : m_owner(owner) {} + /// @brief Return the total number of elements summed across all inner ranges + std::size_t size() const + { + auto outer = (m_owner->*OuterMemFn)(); + return std::accumulate( + outer.begin(), outer.end(), static_cast(0), + [](std::size_t acc, auto &elem) { return acc + (elem.*InnerMemFn)().size(); }); + } + + /// @brief Returns the shape, a vector of the sizes of the inner ranges + std::vector shape() const + { + std::vector result; + auto outer = (m_owner->*OuterMemFn)(); + result.reserve(outer.size()); + std::transform(outer.begin(), outer.end(), std::back_inserter(result), + [](const auto &element) { return (element.*InnerMemFn)().size(); }); + return result; + } + iterator begin() const { return {m_owner, false}; } iterator end() const { return {m_owner, true}; } }; From 30f0a1f0f8f61c619f1cff71e432ba374c73edac Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Mon, 8 Dec 2025 13:55:34 +0000 Subject: [PATCH 03/10] Define strategies concept --- src/games/game.h | 74 +++++++++++++++++++++++++++--------------------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/src/games/game.h b/src/games/game.h index 15e943d97..989d58e65 100644 --- a/src/games/game.h +++ b/src/games/game.h @@ -818,39 +818,6 @@ class GameRep : public std::enable_shared_from_this { throw UndefinedException(); } - /// @name Dimensions of the game - //@{ - /// The number of strategies for each player - virtual Array NumStrategies() const = 0; - /// Gets the i'th strategy in the game, numbered globally - virtual GameStrategy GetStrategy(int p_index) const = 0; - /// Creates a new strategy for the player - virtual GameStrategy NewStrategy(const GamePlayer &p_player, const std::string &p_label) - { - throw UndefinedException(); - } - /// Remove the strategy from the game - virtual void DeleteStrategy(const GameStrategy &p_strategy) { throw UndefinedException(); } - /// Returns the number of strategy contingencies in the game - int NumStrategyContingencies() const - { - BuildComputedValues(); - return std::transform_reduce( - m_players.begin(), m_players.end(), 0, std::multiplies<>(), - [](const std::shared_ptr &p) { return p->m_strategies.size(); }); - } - /// Returns the total number of actions in the game - virtual int BehavProfileLength() const = 0; - /// Returns the total number of strategies in the game - int MixedProfileLength() const - { - BuildComputedValues(); - return std::transform_reduce( - m_players.begin(), m_players.end(), 0, std::plus<>(), - [](const std::shared_ptr &p) { return p->m_strategies.size(); }); - } - //@} - virtual PureStrategyProfile NewPureStrategyProfile() const = 0; virtual MixedStrategyProfile NewMixedStrategyProfile(double) const = 0; virtual MixedStrategyProfile NewMixedStrategyProfile(const Rational &) const = 0; @@ -899,6 +866,47 @@ class GameRep : public std::enable_shared_from_this { virtual GamePlayer NewPlayer() = 0; //@} + /// @name Dimensions of the game + //@{ + using Strategies = + NestedElementCollection; + /// Returns the set of strategies in the game + virtual Strategies GetStrategies() const + { + BuildComputedValues(); + return Strategies(std::const_pointer_cast(this->shared_from_this())); + } + /// The number of strategies for each player + virtual Array NumStrategies() const = 0; + /// Gets the i'th strategy in the game, numbered globally + virtual GameStrategy GetStrategy(int p_index) const = 0; + /// Creates a new strategy for the player + virtual GameStrategy NewStrategy(const GamePlayer &p_player, const std::string &p_label) + { + throw UndefinedException(); + } + /// Remove the strategy from the game + virtual void DeleteStrategy(const GameStrategy &p_strategy) { throw UndefinedException(); } + /// Returns the number of strategy contingencies in the game + int NumStrategyContingencies() const + { + BuildComputedValues(); + return std::transform_reduce( + m_players.begin(), m_players.end(), 0, std::multiplies<>(), + [](const std::shared_ptr &p) { return p->m_strategies.size(); }); + } + /// Returns the total number of actions in the game + virtual int BehavProfileLength() const = 0; + /// Returns the total number of strategies in the game + int MixedProfileLength() const + { + BuildComputedValues(); + return std::transform_reduce( + m_players.begin(), m_players.end(), 0, std::plus<>(), + [](const std::shared_ptr &p) { return p->m_strategies.size(); }); + } + //@} + /// @name Information sets //@{ using Infosets = From d9f9851788a842b47a3a108c2273849559794e64 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Mon, 8 Dec 2025 14:15:57 +0000 Subject: [PATCH 04/10] Replace NumStrategies() vector with shape from nested collection. --- src/core/array.h | 1 + src/core/function.cc | 2 +- src/core/function.h | 2 +- src/games/game.h | 4 +--- src/games/gameagg.cc | 9 --------- src/games/gameagg.h | 2 -- src/games/gamebagg.cc | 13 ++----------- src/games/gamebagg.h | 2 -- src/games/gameexpl.cc | 10 ---------- src/games/gameexpl.h | 2 -- src/games/gameobject.h | 20 ++++++++++++++++---- src/solvers/liap/efgliap.cc | 2 +- src/solvers/liap/nfgliap.cc | 2 +- src/solvers/nashsupport/nfgsupport.cc | 21 ++++++++++----------- src/solvers/simpdiv/simpdiv.cc | 10 +++++----- 15 files changed, 39 insertions(+), 63 deletions(-) diff --git a/src/core/array.h b/src/core/array.h index a7e2e294a..82836db5b 100644 --- a/src/core/array.h +++ b/src/core/array.h @@ -94,6 +94,7 @@ template class Array { void erase(iterator pos) { m_data.erase(pos); } void push_back(const T &value) { m_data.push_back(value); } void pop_back() { m_data.pop_back(); } + void reserve(size_t len) { m_data.reserve(len); } }; /// Convenience function to erase the element at `p_index` diff --git a/src/core/function.cc b/src/core/function.cc index 680bfa234..66ee8ebf8 100644 --- a/src/core/function.cc +++ b/src/core/function.cc @@ -43,7 +43,7 @@ using namespace Gambit; // vector perpendicular to the plane, then subtracting to compute the // component parallel to the plane.) // -void FunctionOnSimplices::Project(Vector &x, const Array &lengths) const +void FunctionOnSimplices::Project(Vector &x, const Array &lengths) const { int index = 1; for (size_t part = 1; part <= lengths.size(); part++) { diff --git a/src/core/function.h b/src/core/function.h index a3bc68302..fa2c73248 100644 --- a/src/core/function.h +++ b/src/core/function.h @@ -42,7 +42,7 @@ class FunctionOnSimplices : public Function { protected: // Project the gradient 'x' onto the plane of the product of simplices. - void Project(Vector &x, const Array &lengths) const; + void Project(Vector &x, const Array &lengths) const; }; class FunctionMinimizerError : public std::runtime_error { diff --git a/src/games/game.h b/src/games/game.h index 989d58e65..c0ea1ca34 100644 --- a/src/games/game.h +++ b/src/games/game.h @@ -871,13 +871,11 @@ class GameRep : public std::enable_shared_from_this { using Strategies = NestedElementCollection; /// Returns the set of strategies in the game - virtual Strategies GetStrategies() const + Strategies GetStrategies() const { BuildComputedValues(); return Strategies(std::const_pointer_cast(this->shared_from_this())); } - /// The number of strategies for each player - virtual Array NumStrategies() const = 0; /// Gets the i'th strategy in the game, numbered globally virtual GameStrategy GetStrategy(int p_index) const = 0; /// Creates a new strategy for the player diff --git a/src/games/gameagg.cc b/src/games/gameagg.cc index 678a7b241..633614204 100644 --- a/src/games/gameagg.cc +++ b/src/games/gameagg.cc @@ -200,15 +200,6 @@ Game GameAGGRep::Copy() const // GameAGGRep: Dimensions of the game //------------------------------------------------------------------------ -Array GameAGGRep::NumStrategies() const -{ - Array ns; - for (const auto &player : m_players) { - ns.push_back(player->GetStrategies().size()); - } - return ns; -} - GameStrategy GameAGGRep::GetStrategy(int p_index) const { for (const auto &player : m_players) { diff --git a/src/games/gameagg.h b/src/games/gameagg.h index 6ae344161..31a0dfcef 100644 --- a/src/games/gameagg.h +++ b/src/games/gameagg.h @@ -46,8 +46,6 @@ class GameAGGRep : public GameRep { std::shared_ptr GetUnderlyingAGG() const { return aggPtr; } /// @name Dimensions of the game //@{ - /// The number of strategies for each player - Array NumStrategies() const override; /// Gets the i'th strategy in the game, numbered globally GameStrategy GetStrategy(int p_index) const override; /// Returns the total number of actions in the game diff --git a/src/games/gamebagg.cc b/src/games/gamebagg.cc index 3155e69d8..03d8e735a 100644 --- a/src/games/gamebagg.cc +++ b/src/games/gamebagg.cc @@ -100,7 +100,7 @@ template T BAGGMixedStrategyProfileRep::GetPayoff(int pl) const { auto &g = dynamic_cast(*(this->m_support.GetGame())); std::vector s(g.MixedProfileLength()); - Array ns = g.NumStrategies(); + const auto ns = g.GetStrategies().shape(); int bplayer = -1, btype = -1; for (int i = 0, offs = 0; i < g.baggPtr->getNumPlayers(); ++i) { for (int tp = 0; tp < g.baggPtr->getNumTypes(i); ++tp) { @@ -108,7 +108,7 @@ template T BAGGMixedStrategyProfileRep::GetPayoff(int pl) const bplayer = i; btype = tp; } - for (int j = 0; j < ns[g.baggPtr->typeOffset[i] + tp + 1]; ++j, ++offs) { + for (int j = 0; j < ns[g.baggPtr->typeOffset[i] + tp]; ++j, ++offs) { const GameStrategy strategy = this->m_support.GetGame() ->GetPlayer(g.baggPtr->typeOffset[i] + tp + 1) ->GetStrategy(j + 1); @@ -236,15 +236,6 @@ Game GameBAGGRep::Copy() const // GameBAGGRep: Dimensions of the game //------------------------------------------------------------------------ -Array GameBAGGRep::NumStrategies() const -{ - Array ns; - for (const auto &player : m_players) { - ns.push_back(player->m_strategies.size()); - } - return ns; -} - PureStrategyProfile GameBAGGRep::NewPureStrategyProfile() const { return PureStrategyProfile(std::make_shared( diff --git a/src/games/gamebagg.h b/src/games/gamebagg.h index 1ea6eb722..8603b0191 100644 --- a/src/games/gamebagg.h +++ b/src/games/gamebagg.h @@ -53,8 +53,6 @@ class GameBAGGRep : public GameRep { /// @name Dimensions of the game //@{ - /// The number of strategies for each player - Array NumStrategies() const override; /// Gets the i'th strategy in the game, numbered globally GameStrategy GetStrategy(int p_index) const override { throw UndefinedException(); } /// Returns the total number of actions in the game diff --git a/src/games/gameexpl.cc b/src/games/gameexpl.cc index 7b1f6b6a1..7515959f9 100644 --- a/src/games/gameexpl.cc +++ b/src/games/gameexpl.cc @@ -58,16 +58,6 @@ Rational GameExplicitRep::GetMaxPayoff() const // GameExplicitRep: Dimensions of the game //------------------------------------------------------------------------ -Array GameExplicitRep::NumStrategies() const -{ - BuildComputedValues(); - Array dim; - for (const auto &player : m_players) { - dim.push_back(player->m_strategies.size()); - } - return dim; -} - GameStrategy GameExplicitRep::GetStrategy(int p_index) const { BuildComputedValues(); diff --git a/src/games/gameexpl.h b/src/games/gameexpl.h index 808ab50bb..6a9c9229b 100644 --- a/src/games/gameexpl.h +++ b/src/games/gameexpl.h @@ -41,8 +41,6 @@ class GameExplicitRep : public GameRep { /// @name Dimensions of the game //@{ - /// The number of strategies for each player - Array NumStrategies() const override; /// Gets the i'th strategy in the game, numbered globally GameStrategy GetStrategy(int p_index) const override; //@} diff --git a/src/games/gameobject.h b/src/games/gameobject.h index 78ab5a4c2..5750fe246 100644 --- a/src/games/gameobject.h +++ b/src/games/gameobject.h @@ -305,20 +305,32 @@ template class NestedElementCollecti /// @brief Return the total number of elements summed across all inner ranges std::size_t size() const { - auto outer = (m_owner->*OuterMemFn)(); + auto outer = (m_owner.get()->*OuterMemFn)(); return std::accumulate( outer.begin(), outer.end(), static_cast(0), - [](std::size_t acc, auto &elem) { return acc + (elem.*InnerMemFn)().size(); }); + [](std::size_t acc, auto &elem) { return acc + (elem.get()->*InnerMemFn)().size(); }); } /// @brief Returns the shape, a vector of the sizes of the inner ranges std::vector shape() const { std::vector result; - auto outer = (m_owner->*OuterMemFn)(); + auto outer = (m_owner.get()->*OuterMemFn)(); result.reserve(outer.size()); std::transform(outer.begin(), outer.end(), std::back_inserter(result), - [](const auto &element) { return (element.*InnerMemFn)().size(); }); + [](const auto &element) { return (element.get()->*InnerMemFn)().size(); }); + return result; + } + + /// @brief Returns the shape, a Gambit Array of the sizes of the inner ranges + /// @deprecated This is for backwards compatibility; uses should be migrated to shape(). + Array shape_array() const + { + Array result; + auto outer = (m_owner.get()->*OuterMemFn)(); + result.reserve(outer.size()); + std::transform(outer.begin(), outer.end(), std::back_inserter(result), + [](const auto &element) { return (element.get()->*InnerMemFn)().size(); }); return result; } diff --git a/src/solvers/liap/efgliap.cc b/src/solvers/liap/efgliap.cc index 4eb888a33..a7ce04cd2 100644 --- a/src/solvers/liap/efgliap.cc +++ b/src/solvers/liap/efgliap.cc @@ -51,7 +51,7 @@ class AgentLyapunovFunction : public FunctionOnSimplices { private: Game m_game; mutable MixedBehaviorProfile m_profile; - Array m_shape; + Array m_shape; double m_scale, m_penalty{100.0}; double Value(const Vector &x) const override; diff --git a/src/solvers/liap/nfgliap.cc b/src/solvers/liap/nfgliap.cc index f1fe4a11a..a830123da 100644 --- a/src/solvers/liap/nfgliap.cc +++ b/src/solvers/liap/nfgliap.cc @@ -128,7 +128,7 @@ bool StrategicLyapunovFunction::Gradient(const Vector &v, Vector ++element; } } - Project(d, m_game->NumStrategies()); + Project(d, m_game->GetStrategies().shape_array()); return true; } diff --git a/src/solvers/nashsupport/nfgsupport.cc b/src/solvers/nashsupport/nfgsupport.cc index 9f76fffd7..fa2d4eff1 100644 --- a/src/solvers/nashsupport/nfgsupport.cc +++ b/src/solvers/nashsupport/nfgsupport.cc @@ -36,30 +36,29 @@ using StrategySupport = std::vector; using CallbackFunction = std::function &)>; class CartesianRange { -private: - Array m_sizes; + Array m_sizes; public: - CartesianRange(const Array &p_sizes) : m_sizes(p_sizes) {} + CartesianRange(const Array &p_sizes) : m_sizes(p_sizes) {} class iterator { private: - Array m_sizes; - Array m_indices; + Array m_sizes; + Array m_indices; bool m_end; public: using iterator_category = std::forward_iterator_tag; - iterator(const Array &p_sizes, bool p_end = false) + iterator(const Array &p_sizes, bool p_end = false) : m_sizes(p_sizes), m_indices(m_sizes.size()), m_end(p_end) { std::fill(m_indices.begin(), m_indices.end(), 1); } - const Array &operator*() const { return m_indices; } + const Array &operator*() const { return m_indices; } - const Array &operator->() const { return m_indices; } + const Array &operator->() const { return m_indices; } iterator &operator++() { @@ -205,7 +204,7 @@ void GenerateSupportProfiles(const Game &game, void GenerateSizeDiff(const Game &game, int size, int diff, CallbackFunction callback) { auto players = game->GetPlayers(); - CartesianRange range(game->NumStrategies()); + CartesianRange range(game->GetStrategies().shape_array()); for (auto size_profile : range) { if (*std::max_element(size_profile.cbegin(), size_profile.cend()) - *std::min_element(size_profile.cbegin(), size_profile.cend()) != @@ -243,14 +242,14 @@ std::shared_ptr PossibleNashStrategySupports(const Game &p_game) { auto result = std::make_shared(); - auto numActions = p_game->NumStrategies(); + auto numActions = p_game->GetStrategies().shape_array(); const int maxsize = std::accumulate(numActions.begin(), numActions.end(), 0) - p_game->NumPlayers() + 1; const int maxdiff = *std::max_element(numActions.cbegin(), numActions.cend()); const bool preferBalance = p_game->NumPlayers() == 2; - Array dim(2); + Array dim(2); dim[1] = (preferBalance) ? maxsize : maxdiff; dim[2] = (preferBalance) ? maxdiff : maxsize; diff --git a/src/solvers/simpdiv/simpdiv.cc b/src/solvers/simpdiv/simpdiv.cc index 6049924b9..b3710ffa5 100644 --- a/src/solvers/simpdiv/simpdiv.cc +++ b/src/solvers/simpdiv/simpdiv.cc @@ -29,11 +29,11 @@ namespace Gambit::Nash { template class PVector { private: Vector m_values; - Array m_offsets; - Array m_shape; + Array m_offsets; + Array m_shape; public: - explicit PVector(const Array &p_shape) + explicit PVector(const Array &p_shape) : m_values(std::accumulate(p_shape.begin(), p_shape.end(), 0)), m_offsets(p_shape.size()), m_shape(p_shape) { @@ -62,7 +62,7 @@ template class PVector { return *this; } - const Array &GetShape() const { return m_shape; } + const Array &GetShape() const { return m_shape; } explicit operator const Vector &() const { return m_values; } }; @@ -137,7 +137,7 @@ Rational NashSimpdivStrategySolver::Simplex(MixedStrategyProfile &y, const Game game = y.GetGame(); State state(m_leashLength); state.d = d; - Array nstrats(game->NumStrategies()); + Array nstrats(game->GetStrategies().shape_array()); Array ylabel(2); RectArray labels(y.MixedProfileLength(), 2), pi(y.MixedProfileLength(), 2); PVector U(nstrats), TT(nstrats); From 6f29743ab7df9b574231e9415920e6aa703eb86f Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Mon, 8 Dec 2025 14:24:57 +0000 Subject: [PATCH 05/10] Remove code duplication for global indexing of strategies --- src/games/game.h | 11 +++++++++-- src/games/gameagg.cc | 17 ----------------- src/games/gameagg.h | 2 -- src/games/gamebagg.h | 2 -- src/games/gameexpl.cc | 18 ------------------ src/games/gameexpl.h | 6 ------ src/games/gameobject.h | 7 ++++--- 7 files changed, 13 insertions(+), 50 deletions(-) diff --git a/src/games/game.h b/src/games/game.h index c0ea1ca34..3d879f14e 100644 --- a/src/games/game.h +++ b/src/games/game.h @@ -876,8 +876,15 @@ class GameRep : public std::enable_shared_from_this { BuildComputedValues(); return Strategies(std::const_pointer_cast(this->shared_from_this())); } - /// Gets the i'th strategy in the game, numbered globally - virtual GameStrategy GetStrategy(int p_index) const = 0; + /// Gets the i'th strategy in the game, numbered globally starting from 1 + GameStrategy GetStrategy(const int p_index) const + { + const auto strategies = GetStrategies(); + if (p_index < 1 || p_index > strategies.size()) { + throw std::out_of_range("Strategy index out of range"); + } + return *std::next(strategies.begin(), p_index - 1); + } /// Creates a new strategy for the player virtual GameStrategy NewStrategy(const GamePlayer &p_player, const std::string &p_label) { diff --git a/src/games/gameagg.cc b/src/games/gameagg.cc index 633614204..c92a7ef53 100644 --- a/src/games/gameagg.cc +++ b/src/games/gameagg.cc @@ -196,23 +196,6 @@ Game GameAGGRep::Copy() const return ReadAggFile(is); } -//------------------------------------------------------------------------ -// GameAGGRep: Dimensions of the game -//------------------------------------------------------------------------ - -GameStrategy GameAGGRep::GetStrategy(int p_index) const -{ - for (const auto &player : m_players) { - if (static_cast(player->GetStrategies().size()) >= p_index) { - return player->GetStrategy(p_index); - } - else { - p_index -= player->GetStrategies().size(); - } - } - throw std::out_of_range("Strategy index out of range"); -} - //------------------------------------------------------------------------ // GameAGGRep: Factory functions //------------------------------------------------------------------------ diff --git a/src/games/gameagg.h b/src/games/gameagg.h index 31a0dfcef..bd12f7da7 100644 --- a/src/games/gameagg.h +++ b/src/games/gameagg.h @@ -46,8 +46,6 @@ class GameAGGRep : public GameRep { std::shared_ptr GetUnderlyingAGG() const { return aggPtr; } /// @name Dimensions of the game //@{ - /// Gets the i'th strategy in the game, numbered globally - GameStrategy GetStrategy(int p_index) const override; /// Returns the total number of actions in the game int BehavProfileLength() const override { throw UndefinedException(); } //@} diff --git a/src/games/gamebagg.h b/src/games/gamebagg.h index 8603b0191..3918a58d2 100644 --- a/src/games/gamebagg.h +++ b/src/games/gamebagg.h @@ -53,8 +53,6 @@ class GameBAGGRep : public GameRep { /// @name Dimensions of the game //@{ - /// Gets the i'th strategy in the game, numbered globally - GameStrategy GetStrategy(int p_index) const override { throw UndefinedException(); } /// Returns the total number of actions in the game int BehavProfileLength() const override { throw UndefinedException(); } //@} diff --git a/src/games/gameexpl.cc b/src/games/gameexpl.cc index 7515959f9..b8b5ce8a3 100644 --- a/src/games/gameexpl.cc +++ b/src/games/gameexpl.cc @@ -54,24 +54,6 @@ Rational GameExplicitRep::GetMaxPayoff() const }); } -//------------------------------------------------------------------------ -// GameExplicitRep: Dimensions of the game -//------------------------------------------------------------------------ - -GameStrategy GameExplicitRep::GetStrategy(int p_index) const -{ - BuildComputedValues(); - int i = 1; - for (const auto &player : m_players) { - for (const auto &strategy : player->m_strategies) { - if (p_index == i++) { - return strategy; - } - } - } - throw std::out_of_range("Strategy index out of range"); -} - //------------------------------------------------------------------------ // GameExplicitRep: Outcomes //------------------------------------------------------------------------ diff --git a/src/games/gameexpl.h b/src/games/gameexpl.h index 6a9c9229b..52ac543fc 100644 --- a/src/games/gameexpl.h +++ b/src/games/gameexpl.h @@ -39,12 +39,6 @@ class GameExplicitRep : public GameRep { Rational GetMaxPayoff() const override; //@} - /// @name Dimensions of the game - //@{ - /// Gets the i'th strategy in the game, numbered globally - GameStrategy GetStrategy(int p_index) const override; - //@} - /// @name Outcomes //@{ /// Creates a new outcome in the game diff --git a/src/games/gameobject.h b/src/games/gameobject.h index 5750fe246..7580eeb62 100644 --- a/src/games/gameobject.h +++ b/src/games/gameobject.h @@ -306,9 +306,10 @@ template class NestedElementCollecti std::size_t size() const { auto outer = (m_owner.get()->*OuterMemFn)(); - return std::accumulate( - outer.begin(), outer.end(), static_cast(0), - [](std::size_t acc, auto &elem) { return acc + (elem.get()->*InnerMemFn)().size(); }); + return std::accumulate(outer.begin(), outer.end(), static_cast(0), + [](std::size_t acc, const auto &element) { + return acc + (element.get()->*InnerMemFn)().size(); + }); } /// @brief Returns the shape, a vector of the sizes of the inner ranges From 1b0b73625f865f1504502aee2e95f98395c806f6 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Mon, 8 Dec 2025 14:30:35 +0000 Subject: [PATCH 06/10] Remove code duplication on mixed profile length. --- src/games/game.h | 8 -------- src/games/gamebagg.cc | 6 +++--- src/gui/dlnfglogit.cc | 8 ++++---- src/gui/nfgprofile.cc | 2 +- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/src/games/game.h b/src/games/game.h index 3d879f14e..3af53b051 100644 --- a/src/games/game.h +++ b/src/games/game.h @@ -902,14 +902,6 @@ class GameRep : public std::enable_shared_from_this { } /// Returns the total number of actions in the game virtual int BehavProfileLength() const = 0; - /// Returns the total number of strategies in the game - int MixedProfileLength() const - { - BuildComputedValues(); - return std::transform_reduce( - m_players.begin(), m_players.end(), 0, std::plus<>(), - [](const std::shared_ptr &p) { return p->m_strategies.size(); }); - } //@} /// @name Information sets diff --git a/src/games/gamebagg.cc b/src/games/gamebagg.cc index 03d8e735a..5f63b4391 100644 --- a/src/games/gamebagg.cc +++ b/src/games/gamebagg.cc @@ -99,7 +99,7 @@ template class BAGGMixedStrategyProfileRep : public MixedStrategyProfi template T BAGGMixedStrategyProfileRep::GetPayoff(int pl) const { auto &g = dynamic_cast(*(this->m_support.GetGame())); - std::vector s(g.MixedProfileLength()); + std::vector s(g.GetStrategies().size()); const auto ns = g.GetStrategies().shape(); int bplayer = -1, btype = -1; for (int i = 0, offs = 0; i < g.baggPtr->getNumPlayers(); ++i) { @@ -124,7 +124,7 @@ template T BAGGMixedStrategyProfileRep::GetPayoffDeriv(int pl, const GameStrategy &ps) const { auto &g = dynamic_cast(*(this->m_support.GetGame())); - std::vector s(g.MixedProfileLength()); + std::vector s(g.GetStrategies().size()); int bplayer = -1, btype = -1; for (int i = 0; i < g.baggPtr->getNumPlayers(); ++i) { for (int tp = 0; tp < g.baggPtr->getNumTypes(i); ++tp) { @@ -163,7 +163,7 @@ T BAGGMixedStrategyProfileRep::GetPayoffDeriv(int pl, const GameStrategy &ps1 } auto &g = dynamic_cast(*(this->m_support.GetGame())); - std::vector s(g.MixedProfileLength()); + std::vector s(g.GetStrategies().size()); int bplayer = -1, btype = -1; for (int i = 0; i < g.baggPtr->getNumPlayers(); ++i) { for (int tp = 0; tp < g.baggPtr->getNumTypes(i); ++tp) { diff --git a/src/gui/dlnfglogit.cc b/src/gui/dlnfglogit.cc index 7cc9c2a04..66eab8fda 100644 --- a/src/gui/dlnfglogit.cc +++ b/src/gui/dlnfglogit.cc @@ -117,7 +117,7 @@ LogitMixedSheet::LogitMixedSheet(wxWindow *p_parent, GameDocument *p_doc, : wxSheet(p_parent, wxID_ANY, wxDefaultPosition, wxSize(800, 600)), m_doc(p_doc), m_branch(p_branch) { - CreateGrid(p_branch.NumPoints(), p_doc->GetGame()->MixedProfileLength() + 1); + CreateGrid(p_branch.NumPoints(), p_doc->GetGame()->GetStrategies().size() + 1); SetRowLabelWidth(40); SetColLabelHeight(25); } @@ -359,13 +359,13 @@ class LogitPlotStrategyList final : public wxSheet { LogitPlotStrategyList::LogitPlotStrategyList(wxWindow *p_parent, GameDocument *p_doc) : wxSheet(p_parent, wxID_ANY), m_doc(p_doc) { - CreateGrid(m_doc->GetGame()->MixedProfileLength(), 3); + CreateGrid(m_doc->GetGame()->GetStrategies().size(), 3); SetRowLabelWidth(0); SetColLabelHeight(0); SetGridLineColour(*wxWHITE); - for (int st = 1; st <= m_doc->GetGame()->MixedProfileLength(); st++) { + for (int st = 1; st <= m_doc->GetGame()->GetStrategies().size(); st++) { const GameStrategy strategy = m_doc->GetGame()->GetStrategy(st); const GamePlayer player = strategy->GetPlayer(); const wxColour color = m_doc->GetStyle().GetPlayerColor(player); @@ -476,7 +476,7 @@ void LogitPlotPanel::Plot() m_plotCtrl->DeleteCurve(-1); - for (int st = 1; st <= m_doc->GetGame()->MixedProfileLength(); st++) { + for (int st = 1; st <= m_doc->GetGame()->GetStrategies().size(); st++) { if (!m_plotStrategies->IsStrategyShown(st)) { continue; } diff --git a/src/gui/nfgprofile.cc b/src/gui/nfgprofile.cc index 705f45c40..3034849c4 100644 --- a/src/gui/nfgprofile.cc +++ b/src/gui/nfgprofile.cc @@ -188,7 +188,7 @@ void MixedProfileList::OnUpdate() DeleteRows(0, GetNumberRows()); InsertRows(0, newRows); - const int profileLength = m_doc->GetGame()->MixedProfileLength(); + const int profileLength = m_doc->GetGame()->GetStrategies().size(); const int newCols = profileLength; DeleteCols(0, GetNumberCols()); InsertCols(0, newCols); From 2f1793271ab941fadfb59c0e16f2d09fb9b63c06 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Mon, 8 Dec 2025 14:46:31 +0000 Subject: [PATCH 07/10] Replace strategy contingency calculation with generic extent_product --- src/games/game.h | 8 -------- src/games/gameobject.h | 10 ++++++++++ src/gui/gameframe.cc | 36 +++++++++++++++++++----------------- 3 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/games/game.h b/src/games/game.h index 3af53b051..c56a90f9a 100644 --- a/src/games/game.h +++ b/src/games/game.h @@ -892,14 +892,6 @@ class GameRep : public std::enable_shared_from_this { } /// Remove the strategy from the game virtual void DeleteStrategy(const GameStrategy &p_strategy) { throw UndefinedException(); } - /// Returns the number of strategy contingencies in the game - int NumStrategyContingencies() const - { - BuildComputedValues(); - return std::transform_reduce( - m_players.begin(), m_players.end(), 0, std::multiplies<>(), - [](const std::shared_ptr &p) { return p->m_strategies.size(); }); - } /// Returns the total number of actions in the game virtual int BehavProfileLength() const = 0; //@} diff --git a/src/games/gameobject.h b/src/games/gameobject.h index 7580eeb62..28311fce9 100644 --- a/src/games/gameobject.h +++ b/src/games/gameobject.h @@ -335,6 +335,16 @@ template class NestedElementCollecti return result; } + /// @brief Returns the Cartesian product of the sizes of the inner ranges + std::size_t extent_product() const + { + auto outer = (m_owner.get()->*OuterMemFn)(); + return std::accumulate(outer.begin(), outer.end(), static_cast(1), + [](std::size_t acc, const auto &element) { + return acc * (element.get()->*InnerMemFn)().size(); + }); + } + iterator begin() const { return {m_owner, false}; } iterator end() const { return {m_owner, true}; } }; diff --git a/src/gui/gameframe.cc b/src/gui/gameframe.cc index f6d356416..e66aa63f2 100644 --- a/src/gui/gameframe.cc +++ b/src/gui/gameframe.cc @@ -1111,14 +1111,15 @@ void GameFrame::OnViewStrategic(wxCommandEvent &p_event) return; } - const int ncont = m_doc->GetGame()->NumStrategyContingencies(); - if (!m_nfgPanel && ncont >= 50000) { - if (wxMessageBox( - wxString::Format(wxT("This game has %d contingencies in strategic form.\n"), ncont) + - wxT("Performance in browsing strategic form will be poor,\n") + - wxT("and may render the program nonresponsive.\n") + - wxT("Do you wish to continue?"), - _("Large strategic game warning"), wxOK | wxCANCEL | wxALIGN_CENTER, this) != wxOK) { + if (const size_t contingencies = m_doc->GetGame()->GetStrategies().extent_product(); + !m_nfgPanel && contingencies >= 50000) { + if (wxMessageBox(wxString::Format(wxT("This game has %d contingencies in strategic form.\n"), + contingencies) + + wxT("Performance in browsing strategic form will be poor,\n") + + wxT("and may render the program nonresponsive.\n") + + wxT("Do you wish to continue?"), + _("Large strategic game warning"), wxOK | wxCANCEL | wxALIGN_CENTER, + this) != wxOK) { return; } } @@ -1242,15 +1243,16 @@ void GameFrame::OnToolsEquilibrium(wxCommandEvent &) if (dialog.ShowModal() == wxID_OK) { if (dialog.UseStrategic()) { - const int ncont = m_doc->GetGame()->NumStrategyContingencies(); - if (ncont >= 50000) { - if (wxMessageBox(wxString::Format( - wxT("This game has %d contingencies in strategic form.\n"), ncont) + - wxT("Performance in solving strategic form will be poor,\n") + - wxT("and may render the program nonresponsive.\n") + - wxT("Do you wish to continue?"), - _("Large strategic game warning"), wxOK | wxCANCEL | wxALIGN_CENTER, - this) != wxOK) { + if (const int contingencies = m_doc->GetGame()->GetStrategies().extent_product(); + contingencies >= 50000) { + if (wxMessageBox( + wxString::Format(wxT("This game has %d contingencies in strategic form.\n"), + contingencies) + + wxT("Performance in solving strategic form will be poor,\n") + + wxT("and may render the program nonresponsive.\n") + + wxT("Do you wish to continue?"), + _("Large strategic game warning"), wxOK | wxCANCEL | wxALIGN_CENTER, + this) != wxOK) { return; } } From 1e70bdc221e971da20b5a24f813d348646646d2e Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Mon, 8 Dec 2025 14:59:53 +0000 Subject: [PATCH 08/10] Fix ambiguity in definition of iterator --- src/games/game.h | 2 +- src/games/gameobject.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/games/game.h b/src/games/game.h index c56a90f9a..f98827b57 100644 --- a/src/games/game.h +++ b/src/games/game.h @@ -877,7 +877,7 @@ class GameRep : public std::enable_shared_from_this { return Strategies(std::const_pointer_cast(this->shared_from_this())); } /// Gets the i'th strategy in the game, numbered globally starting from 1 - GameStrategy GetStrategy(const int p_index) const + GameStrategy GetStrategy(const std::size_t p_index) const { const auto strategies = GetStrategies(); if (p_index < 1 || p_index > strategies.size()) { diff --git a/src/games/gameobject.h b/src/games/gameobject.h index 28311fce9..2f2f39214 100644 --- a/src/games/gameobject.h +++ b/src/games/gameobject.h @@ -228,7 +228,7 @@ template class NestedElementCollecti using InnerRange = decltype((std::declval().get()->*InnerMemFn)()); using InnerIter = decltype(std::begin(std::declval())); - T m_owner; + T m_iterOwner; OuterRange m_outerRange; OuterIter m_outerIt, m_outerEnd; InnerRange m_innerRange; @@ -263,7 +263,7 @@ template class NestedElementCollecti iterator() = default; iterator(T p_owner, const bool p_isEnd) - : m_owner(p_owner), m_outerRange((m_owner.get()->*OuterMemFn)()), + : m_iterOwner(p_owner), m_outerRange((m_iterOwner.get()->*OuterMemFn)()), m_outerIt(std::begin(m_outerRange)), m_outerEnd(std::end(m_outerRange)) { if (p_isEnd) { From 91613401fa0eb30ddb6bc93428f51f5648d2ad1d Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Mon, 8 Dec 2025 15:24:48 +0000 Subject: [PATCH 09/10] Improve normal form liap --- src/solvers/liap/nfgliap.cc | 61 ++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 35 deletions(-) diff --git a/src/solvers/liap/nfgliap.cc b/src/solvers/liap/nfgliap.cc index a830123da..7e07a4459 100644 --- a/src/solvers/liap/nfgliap.cc +++ b/src/solvers/liap/nfgliap.cc @@ -63,34 +63,31 @@ class StrategicLyapunovFunction : public FunctionOnSimplices { inline double sum_player_probs(const MixedStrategyProfile &p_profile, const GamePlayer &p_player) { - auto strategies = p_player->GetStrategies(); - return std::accumulate( - strategies.cbegin(), strategies.cend(), 0.0, - [p_profile](double t, const GameStrategy &s) { return t + p_profile[s]; }); + return sum_function(p_player->GetStrategies(), + [&](const auto &strategy) -> double { return p_profile[strategy]; }); } double StrategicLyapunovFunction::Value(const Vector &v) const { m_profile = v; double value = 0; - // Liapunov function proper - should be replaced with call to profile once - // the penalty is removed from that implementation. - for (auto player : m_profile.GetGame()->GetPlayers()) { - for (auto strategy : player->GetStrategies()) { - value += sqr( - std::max(m_scale * (m_profile.GetPayoff(strategy) - m_profile.GetPayoff(player)), 0.0)); - } - } - // Penalty function for non-negativity constraint for each strategy - for (auto player : m_profile.GetGame()->GetPlayers()) { - for (auto strategy : player->GetStrategies()) { - value += m_penalty * sqr(std::min(m_profile[strategy], 0.0)); - } - } - // Penalty function for sum-to-one constraint for each player - for (auto player : m_profile.GetGame()->GetPlayers()) { - value += m_penalty * sqr(sum_player_probs(m_profile, player) - 1.0); + // Liapunov function proper + for (const auto &player : m_profile.GetGame()->GetPlayers()) { + const double payoff = m_profile.GetPayoff(player); + value += sum_function(player->GetStrategies(), [&](const auto &strategy) -> double { + return sqr(std::max(m_scale * (m_profile.GetPayoff(strategy) - payoff), 0.0)); + }); } + // Penalty for non-negativity constraint for each strategy + value += m_penalty * + sum_function(m_profile.GetGame()->GetStrategies(), [&](const auto &strategy) -> double { + return sqr(std::min(m_profile[strategy], 0.0)); + }); + // Penalty for sum-to-one constraint for each player + value += m_penalty * + sum_function(m_profile.GetGame()->GetPlayers(), [&](const auto &player) -> double { + return sqr(sum_player_probs(m_profile, player) - 1.0); + }); return value; } @@ -98,10 +95,10 @@ double StrategicLyapunovFunction::LiapDerivValue(const MixedStrategyProfileGetPlayers()) { + for (const auto &player : m_game->GetPlayers()) { + const double payoff = p_profile.GetPayoff(player); for (auto strategy : player->GetStrategies()) { - const double loss = - sqr(m_scale) * (p_profile.GetPayoff(strategy) - p_profile.GetPayoff(player)); + const double loss = sqr(m_scale) * (p_profile.GetPayoff(strategy) - payoff); if (loss <= 0.0) { continue; } @@ -121,13 +118,9 @@ double StrategicLyapunovFunction::LiapDerivValue(const MixedStrategyProfile &v, Vector &d) const { m_profile = v; - auto element = d.begin(); - for (auto player : m_game->GetPlayers()) { - for (auto strategy : player->GetStrategies()) { - *element = LiapDerivValue(m_profile, strategy); - ++element; - } - } + std::transform( + m_game->GetStrategies().begin(), m_game->GetStrategies().end(), d.begin(), + [&](const auto &strategy) -> double { return LiapDerivValue(m_profile, strategy); }); Project(d, m_game->GetStrategies().shape_array()); return true; } @@ -137,10 +130,8 @@ namespace { MixedStrategyProfile EnforceNonnegativity(const MixedStrategyProfile &p_profile) { auto profile = p_profile; - for (auto player : p_profile.GetGame()->GetPlayers()) { - for (auto strategy : player->GetStrategies()) { - profile[strategy] = std::max(profile[strategy], 0.0); - } + for (const auto &strategy : p_profile.GetGame()->GetStrategies()) { + profile[strategy] = std::max(profile[strategy], 0.0); } return profile.Normalize(); } From 462c8d7642ca1e8fdb1f0e807aef053460636f27 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Mon, 8 Dec 2025 15:29:03 +0000 Subject: [PATCH 10/10] Improve normal form liap --- src/solvers/liap/nfgliap.cc | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/solvers/liap/nfgliap.cc b/src/solvers/liap/nfgliap.cc index 7e07a4459..58c4b73a1 100644 --- a/src/solvers/liap/nfgliap.cc +++ b/src/solvers/liap/nfgliap.cc @@ -38,12 +38,7 @@ class StrategicLyapunovFunction : public FunctionOnSimplices { : m_game(p_start.GetGame()), m_profile(p_start) { m_scale = m_game->GetMaxPayoff() - m_game->GetMinPayoff(); - if (m_scale == 0.0) { - m_scale = 1.0; - } - else { - m_scale = 1.0 / m_scale; - } + m_scale = (m_scale == 0.0) ? 1.0 : 1.0 / m_scale; } ~StrategicLyapunovFunction() override = default;