diff --git a/ChangeLog b/ChangeLog index fcb6c900cd..ba26b7d643 100644 --- a/ChangeLog +++ b/ChangeLog @@ -3,6 +3,7 @@ ## [16.7.0] - unreleased ### Added +- Added `Player.sequences` as the collection of sequences available to a player. - Implement `GameSubgameRep` (C++) and `Subgame` (Python), a first-class object representing a subgame. (#585) - Games can be materialised directly from OpenSpiel games if `pyspiel` is installed. (#917) - Magnify events are now supported in GUI for zooming in/out on trees. diff --git a/doc/pygambit.api.rst b/doc/pygambit.api.rst index fd64dbd2f5..b9fe15e1a0 100644 --- a/doc/pygambit.api.rst +++ b/doc/pygambit.api.rst @@ -126,7 +126,7 @@ Information about the game Player.is_chance Player.min_payoff Player.max_payoff - Player.strategies + Player.sequences .. autosummary:: :toctree: api/ @@ -198,6 +198,13 @@ Information about the game Strategy.number Strategy.action +.. autosummary:: + + :toctree: api/ + + Sequence.player + Sequence.parent + Sequence.actions Player behavior ............... diff --git a/src/games/behavspt.cc b/src/games/behavspt.cc index 84e73363ab..7f73c093a2 100644 --- a/src/games/behavspt.cc +++ b/src/games/behavspt.cc @@ -158,7 +158,7 @@ std::shared_ptr BehaviorSupportProfile::GetSequenceForm() cons BehaviorSupportProfile::Sequences BehaviorSupportProfile::GetSequences() const { return {this}; } BehaviorSupportProfile::PlayerSequences -BehaviorSupportProfile::GetSequences(GamePlayer &p_player) const +BehaviorSupportProfile::GetSequences(const GamePlayer &p_player) const { return {this, p_player}; } @@ -187,15 +187,15 @@ BehaviorSupportProfile::ToMixedBehaviorProfile(const std::map b(*this); for (auto sequence : GetSequences()) { - if (sequence->action == nullptr) { + if (sequence->m_action == nullptr) { continue; } - const double parent_prob = x.at(sequence->parent.lock()); + const double parent_prob = x.at(sequence->m_parent.lock()); if (parent_prob > 0) { - b[sequence->action] = x.at(sequence) / parent_prob; + b[sequence->m_action->shared_from_this()] = x.at(sequence) / parent_prob; } else { - b[sequence->action] = 0; + b[sequence->m_action->shared_from_this()] = 0; } } return b; diff --git a/src/games/behavspt.h b/src/games/behavspt.h index 2b4a6d08eb..9f72af66c8 100644 --- a/src/games/behavspt.h +++ b/src/games/behavspt.h @@ -231,7 +231,7 @@ class BehaviorSupportProfile { std::shared_ptr GetSequenceForm() const; Sequences GetSequences() const; - PlayerSequences GetSequences(GamePlayer &p_player) const; + PlayerSequences GetSequences(const GamePlayer &p_player) const; int GetConstraintEntry(const GameInfoset &p_infoset, const GameAction &p_action) const; const Rational &GetPayoff(const std::map &p_profile, const GamePlayer &p_player) const; diff --git a/src/games/game.cc b/src/games/game.cc index f75c742b50..a441bb0fb5 100644 --- a/src/games/game.cc +++ b/src/games/game.cc @@ -77,12 +77,15 @@ GamePlayerRep::GamePlayerRep(GameRep *p_game, int p_id, int p_strats) GamePlayerRep::~GamePlayerRep() { - for (auto infoset : m_infosets) { + for (const auto &infoset : m_infosets) { infoset->Invalidate(); } - for (auto strategy : m_strategies) { + for (const auto &strategy : m_strategies) { strategy->Invalidate(); } + for (const auto &sequence : m_sequences) { + sequence->Invalidate(); + } } void GamePlayerRep::MakeStrategy(const std::map &behav) @@ -158,16 +161,6 @@ void GamePlayerRep::MakeReducedStrats(GameNodeRep *n, GameNodeRep *nn, } } -size_t GamePlayerRep::NumSequences() const -{ - if (!m_game->IsTree()) { - throw UndefinedException(); - } - return std::transform_reduce( - m_infosets.cbegin(), m_infosets.cend(), 1, std::plus<>(), - [](const std::shared_ptr &s) { return s->m_actions.size(); }); -} - //======================================================================== // class GameRep //======================================================================== diff --git a/src/games/game.h b/src/games/game.h index 5fa619e4a6..6d77077818 100644 --- a/src/games/game.h +++ b/src/games/game.h @@ -323,16 +323,19 @@ class GameStrategyRep : public std::enable_shared_from_this { }; class GameSequenceRep : public std::enable_shared_from_this { -public: + friend class GameTreeRep; + friend class BehaviorSupportProfile; + bool m_valid{true}; - GamePlayer player; - GameAction action; - size_t number; - std::weak_ptr parent; - - explicit GameSequenceRep(const GamePlayer &p_player, const GameAction &p_action, size_t p_number, - std::weak_ptr p_parent) - : player(p_player), action(p_action), number(p_number), parent(p_parent) + GamePlayerRep *m_player; + GameActionRep *m_action; + size_t m_number; + std::weak_ptr m_parent; + +public: + explicit GameSequenceRep(GamePlayerRep *p_player, GameActionRep *p_action, size_t p_number, + const std::weak_ptr &p_parent) + : m_player(p_player), m_action(p_action), m_number(p_number), m_parent(p_parent) { } @@ -340,15 +343,18 @@ class GameSequenceRep : public std::enable_shared_from_this { void Invalidate() { m_valid = false; } Game GetGame() const; - GameInfoset GetInfoset() const { return (action) ? action->GetInfoset() : nullptr; } + GamePlayer GetPlayer() const; + GameInfoset GetInfoset() const { return (m_action) ? m_action->GetInfoset() : nullptr; } + GameAction GetAction() const { return (m_action) ? m_action->shared_from_this() : nullptr; } + GameSequence GetParent() const { return m_parent.lock(); } bool operator<(const GameSequenceRep &other) const { - return player < other.player || (player == other.player && action < other.action); + return m_player < other.m_player || (m_player == other.m_player && m_action < other.m_action); } bool operator==(const GameSequenceRep &other) const { - return player == other.player && action == other.action; + return m_player == other.m_player && m_action == other.m_action; } }; @@ -382,10 +388,12 @@ class GamePlayerRep : public std::enable_shared_from_this { std::string m_label; std::vector> m_infosets; std::vector> m_strategies; + std::vector> m_sequences; public: using Infosets = ElementCollection; using Strategies = ElementCollection; + using Sequences = ElementCollection; GamePlayerRep(GameRep *p_game, int p_id) : m_game(p_game), m_number(p_id) {} GamePlayerRep(GameRep *p_game, int p_id, int m_strats); @@ -413,14 +421,14 @@ class GamePlayerRep : public std::enable_shared_from_this { //@{ /// Returns the st'th strategy for the player GameStrategy GetStrategy(int st) const; - /// Returns the array of strategies available to the player + /// Returns the collection of strategies available to the player Strategies GetStrategies() const; //@} /// @name Sequences //@{ - /// Returns the number of sequences available to the player - size_t NumSequences() const; + /// Returns the collection of sequences available to the player + Sequences GetSequences() const; //@} }; @@ -1185,6 +1193,8 @@ class GameRep : public std::enable_shared_from_this { /// Build any computed values anew virtual void BuildComputedValues() const {} + /// Ensure sequences have been computed + virtual void EnsureSequences() const { throw UndefinedException(); } }; //======================================================================= @@ -1228,7 +1238,8 @@ inline void GameOutcomeRep::SetPayoff(const GamePlayer &p_player, const Number & inline GamePlayer GameStrategyRep::GetPlayer() const { return m_player->shared_from_this(); } inline Game GameStrategyRep::GetGame() const { return m_player->GetGame(); } -inline Game GameSequenceRep::GetGame() const { return player->GetGame(); } +inline Game GameSequenceRep::GetGame() const { return m_player->GetGame(); } +inline GamePlayer GameSequenceRep::GetPlayer() const { return m_player->shared_from_this(); } inline Game GameActionRep::GetGame() const { return m_infoset->GetGame(); } @@ -1247,6 +1258,11 @@ inline GamePlayerRep::Strategies GamePlayerRep::GetStrategies() const m_game->BuildComputedValues(); return Strategies(std::const_pointer_cast(shared_from_this()), &m_strategies); } +inline GamePlayerRep::Sequences GamePlayerRep::GetSequences() const +{ + m_game->EnsureSequences(); + return Sequences(std::const_pointer_cast(shared_from_this()), &m_sequences); +} inline Game GameNodeRep::GetGame() const { return m_game->shared_from_this(); } inline int GameNodeRep::GetNumber() const diff --git a/src/games/gameseq.cc b/src/games/gameseq.cc index 0dba377ea5..dc149fb146 100644 --- a/src/games/gameseq.cc +++ b/src/games/gameseq.cc @@ -27,43 +27,18 @@ using namespace Gambit; namespace Gambit { -void GameSequenceForm::BuildSequences(const GameNode &n, - std::map &p_currentSequences) +void GameSequenceForm::BuildSequences() { - if (!n->GetInfoset()) { - return; - } - if (n->GetPlayer()->IsChance()) { - for (auto child : n->GetChildren()) { - BuildSequences(child, p_currentSequences); - } - } - else { - m_infosets.insert(n->GetInfoset()); - auto tmp_sequence = p_currentSequences.at(n->GetPlayer()); - for (auto action : m_support.GetActions(n->GetInfoset())) { - if (m_correspondence.find(action) == m_correspondence.end()) { - m_sequences[n->GetPlayer()].emplace_back(std::make_shared( - n->GetPlayer(), action, m_sequences[n->GetPlayer()].size() + 1, - tmp_sequence.get_shared())); - m_correspondence[action] = m_sequences[n->GetPlayer()].back(); + for (const auto &player : GetPlayers()) { + for (const auto &sequence : player->GetSequences()) { + if (!sequence->GetAction() || m_support.Contains(sequence->GetAction())) { + m_sequences[player].emplace_back(sequence); + if (sequence->GetAction()) { + m_correspondence[sequence->GetAction()] = sequence; + } } - p_currentSequences[n->GetPlayer()] = m_correspondence[action]; - BuildSequences(n->GetChild(action), p_currentSequences); } - p_currentSequences[n->GetPlayer()] = tmp_sequence; - } -} - -void GameSequenceForm::BuildSequences() -{ - std::map currentSequences; - for (auto player : GetPlayers()) { - m_sequences[player] = { - std::make_shared(player, nullptr, 1, std::weak_ptr())}; - currentSequences[player] = m_sequences[player].front(); } - BuildSequences(m_support.GetGame()->GetRoot(), currentSequences); } void GameSequenceForm::FillTableau(const GameNode &n, const Rational &prob, @@ -87,7 +62,7 @@ void GameSequenceForm::FillTableau(const GameNode &n, const Rational &prob, } else { auto tmp_sequence = p_currentSequences.at(n->GetPlayer()); - m_constraints[{n->GetInfoset(), p_currentSequences.at(n->GetPlayer())->action}] = 1; + m_constraints[{n->GetInfoset(), p_currentSequences.at(n->GetPlayer())->GetAction()}] = 1; for (auto action : m_support.GetActions(n->GetInfoset())) { m_constraints[{n->GetInfoset(), action}] = -1; p_currentSequences[n->GetPlayer()] = m_correspondence.at(action); diff --git a/src/games/gameseq.h b/src/games/gameseq.h index dfb8fa9cd4..80678d1128 100644 --- a/src/games/gameseq.h +++ b/src/games/gameseq.h @@ -39,7 +39,6 @@ class GameSequenceForm { std::map m_correspondence; void BuildSequences(); - void BuildSequences(const GameNode &, std::map &); void FillTableau(); void FillTableau(const GameNode &, const Rational &, std::map &); @@ -47,7 +46,9 @@ class GameSequenceForm { { Array index(p_profile.size()); for (auto player : GetPlayers()) { - index[player->GetNumber()] = p_profile.at(player)->number; + const auto &seqs = m_sequences.at(player); + auto loc = std::find(seqs.begin(), seqs.end(), p_profile.at(player)); + index[player->GetNumber()] = loc - seqs.begin() + 1; } return index; } diff --git a/src/games/gametree.cc b/src/games/gametree.cc index 149968d3ef..bcefbf45ce 100644 --- a/src/games/gametree.cc +++ b/src/games/gametree.cc @@ -951,7 +951,12 @@ void GameTreeRep::ClearComputedValues() const strategy->Invalidate(); } player->m_strategies.clear(); + for (const auto &sequence : player->m_sequences) { + sequence->Invalidate(); + } + player->m_sequences.clear(); } + m_hasSequences = false; const_cast(this)->m_nodePlays.clear(); m_ownPriorActionInfo = nullptr; const_cast(this)->m_unreachableNodes = nullptr; @@ -975,6 +980,56 @@ void GameTreeRep::BuildComputedValues() const m_computedValues = true; } +void GameTreeRep::BuildSequences(const GameNode &n, + std::map &p_currentSequences) const +{ + if (!n->GetInfoset()) { + return; + } + if (n->GetPlayer()->IsChance()) { + for (auto child : n->GetChildren()) { + BuildSequences(child, p_currentSequences); + } + } + else { + auto *player = n->m_infoset->m_player; + const auto tmp_sequence = p_currentSequences.at(n->GetPlayer()); + for (const auto &action : n->m_infoset->m_actions) { + auto seq_it = + std::find_if(player->m_sequences.begin(), player->m_sequences.end(), + [&action](const auto seq) { return seq->m_action == action.get(); }); + std::shared_ptr sequence; + if (seq_it == player->m_sequences.end()) { + player->m_sequences.emplace_back(std::make_shared( + n->m_infoset->m_player, action.get(), player->m_sequences.size() + 1, + tmp_sequence.get_shared())); + sequence = player->m_sequences.back(); + } + else { + sequence = *seq_it; + } + p_currentSequences[n->GetPlayer()] = sequence; + BuildSequences(n->GetChild(action), p_currentSequences); + } + p_currentSequences[n->GetPlayer()] = tmp_sequence; + } +} + +void GameTreeRep::EnsureSequences() const +{ + if (m_hasSequences) { + return; + } + std::map currentSequences; + for (const auto &player : m_players) { + player->m_sequences = {std::make_shared(player.get(), nullptr, 1, + std::weak_ptr())}; + currentSequences[player] = player->m_sequences.front(); + } + BuildSequences(m_root, currentSequences); + m_hasSequences = true; +} + void GameTreeRep::BuildConsistentPlays() { m_nodePlays.clear(); diff --git a/src/games/gametree.h b/src/games/gametree.h index c94404f898..3e36af3988 100644 --- a/src/games/gametree.h +++ b/src/games/gametree.h @@ -32,6 +32,7 @@ class GameTreeRep final : public GameExplicitRep { friend class GameNodeRep; friend class GameInfosetRep; friend class GameActionRep; + friend class GamePlayerRep; struct OwnPriorActionInfo { std::map node_map; @@ -39,7 +40,8 @@ class GameTreeRep final : public GameExplicitRep { }; protected: - mutable bool m_computedValues{false}, m_nodesOrdered{false}, m_infosetsOrdered{false}; + mutable bool m_computedValues{false}, m_nodesOrdered{false}, m_infosetsOrdered{false}, + m_hasSequences{false}; std::shared_ptr m_root; std::shared_ptr m_chance; std::size_t m_numNodes = 1; @@ -94,6 +96,10 @@ class GameTreeRep final : public GameExplicitRep { void BuildConsistentPlays(); void ClearComputedValues() const; + void EnsureSequences() const override; + void BuildSequences(const GameNode &n, + std::map &p_currentSequences) const; + /// Removes the node from the information set, invalidating if emptied void RemoveMember(GameInfosetRep *, GameNodeRep *); diff --git a/src/pygambit/gambit.pxd b/src/pygambit/gambit.pxd index b3afbefc4c..62e83e8312 100644 --- a/src/pygambit/gambit.pxd +++ b/src/pygambit/gambit.pxd @@ -46,6 +46,7 @@ cdef extern from "core/array.h": cdef extern from "games/game.h": cdef cppclass c_GameRep "GameRep" cdef cppclass c_GameStrategyRep "GameStrategyRep" + cdef cppclass c_GameSequenceRep "GameSequenceRep" cdef cppclass c_GameActionRep "GameActionRep" cdef cppclass c_GameInfosetRep "GameInfosetRep" cdef cppclass c_GamePlayerRep "GamePlayerRep" @@ -79,8 +80,15 @@ cdef extern from "games/game.h": c_GameInfosetRep *deref "get"() except +RuntimeError cdef cppclass c_GameStrategy "GameObjectPtr": + bool operator ==(c_GameStrategy) except + + bool operator !=(c_GameStrategy) except + c_GameStrategyRep *deref "get"() except +RuntimeError + cdef cppclass c_GameSequence "GameObjectPtr": + bool operator ==(c_GameSequence) except + + bool operator !=(c_GameSequence) except + + c_GameSequenceRep *deref "get"() except +RuntimeError + cdef cppclass c_GameSubgame "GameObjectPtr": bool operator ==(c_GameSubgame) except + bool operator !=(c_GameSubgame) except + @@ -101,6 +109,11 @@ cdef extern from "games/game.h": void SetLabel(string) except + c_GameAction GetAction(c_GameInfoset) except + + cdef cppclass c_GameSequenceRep "GameSequenceRep": + c_GamePlayer GetPlayer() except + + c_GameAction GetAction() except + + c_GameSequence GetParent() except + + cdef cppclass c_GameActionRep "GameActionRep": int GetNumber() except + c_GameInfoset GetInfoset() except + @@ -169,6 +182,16 @@ cdef extern from "games/game.h": iterator begin() except + iterator end() except + + cppclass Sequences: + cppclass iterator: + c_GameSequence operator *() + iterator operator++() + bint operator ==(iterator) + bint operator !=(iterator) + int size() except + + iterator begin() except + + iterator end() except + + c_Game GetGame() except + int GetNumber() except + int IsChance() except + @@ -179,6 +202,8 @@ cdef extern from "games/game.h": c_GameStrategy GetStrategy(int) except +IndexError Strategies GetStrategies() except + + Sequences GetSequences() except + + c_GameInfoset GetInfoset(int) except +IndexError Infosets GetInfosets() except + diff --git a/src/pygambit/player.pxi b/src/pygambit/player.pxi index 4da57be4e2..d58de6dc46 100644 --- a/src/pygambit/player.pxi +++ b/src/pygambit/player.pxi @@ -125,7 +125,7 @@ class PlayerStrategies: def __repr__(self) -> str: return f"PlayerStrategies(player={Player.wrap(self.player)})" - def __len__(self): + def __len__(self) -> int: """The number of strategies for the player in the game.""" return self.player.deref().GetStrategies().size() @@ -148,6 +148,33 @@ class PlayerStrategies: raise TypeError(f"Strategy index must be int or str, not {index.__class__.__name__}") +@cython.cclass +class PlayerSequences: + """The collection of sequences available to a player.""" + player = cython.declare(c_GamePlayer) + + def __init__(self, *args, **kwargs) -> None: + raise ValueError("Cannot create PlayerSequences outside a Game.") + + @staticmethod + @cython.cfunc + def wrap(player: c_GamePlayer) -> PlayerSequences: + obj: PlayerSequences = PlayerSequences.__new__(PlayerSequences) + obj.player = player + return obj + + def __repr__(self) -> str: + return f"PlayerSequences(player={Player.wrap(self.player)})" + + def __len__(self) -> int: + """The number of sequences for the player in the game.""" + return self.player.deref().GetSequences().size() + + def __iter__(self) -> typing.Iterator[Sequence]: + for sequence in self.player.deref().GetSequences(): + yield Sequence.wrap(sequence) + + @cython.cclass class Player: """A player in a ``Game``.""" @@ -213,9 +240,14 @@ class Player: @property def strategies(self) -> PlayerStrategies: - """Returns the set of strategies belonging to the player.""" + """Returns the collection of strategies belonging to the player.""" return PlayerStrategies.wrap(self.player) + @property + def sequences(self) -> PlayerSequences: + """Returns the collection of sequences belonging to the player.""" + return PlayerSequences.wrap(self.player) + @property def infosets(self) -> PlayerInfosets: """Returns the set of information sets at which the player has the decision. diff --git a/src/pygambit/strategy.pxi b/src/pygambit/strategy.pxi index 2bc75adb03..fcad92db41 100644 --- a/src/pygambit/strategy.pxi +++ b/src/pygambit/strategy.pxi @@ -121,3 +121,82 @@ class Strategy: if not action: return None return Action.wrap(action) + + +@cython.cclass +class Sequence: + """A sequence ``Player`` in a ``Game``. + + .. versionadded:: 16.7.0 + """ + sequence = cython.declare(c_GameSequence) + + def __init__(self, *args, **kwargs) -> None: + raise ValueError("Cannot create a Sequence outside a Game.") + + @staticmethod + @cython.cfunc + def wrap(sequence: c_GameSequence) -> Sequence: + obj: Sequence = Sequence.__new__(Sequence) + obj.sequence = sequence + return obj + + def __repr__(self) -> str: + 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() + ) + + def __hash__(self) -> int: + return cython.cast(cython.long, self.sequence.deref()) + + @property + def game(self) -> Game: + """The game to which the sequence belongs.""" + return Game.wrap(self.sequence.deref().GetPlayer().deref().GetGame()) + + @property + def player(self) -> Player: + """The player to which the sequence belongs.""" + return Player.wrap(self.sequence.deref().GetPlayer()) + + @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()) + + @property + 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 + + @property + def actions(self) -> list[Action]: + """Get the collection of actions defining this sequence. + + Returns the empty list for the root sequence of the player. + """ + actions: list[Action] = [] + seq = self.sequence + while seq.deref().GetAction() != cython.cast(c_GameAction, NULL): + actions.insert(0, Action.wrap(seq.deref().GetAction())) + seq = seq.deref().GetParent() + return actions diff --git a/src/solvers/enumpoly/efgpoly.cc b/src/solvers/enumpoly/efgpoly.cc index 4956b372f3..bc82ac051b 100644 --- a/src/solvers/enumpoly/efgpoly.cc +++ b/src/solvers/enumpoly/efgpoly.cc @@ -59,20 +59,20 @@ class ProblemData { Polynomial BuildSequenceVariable(ProblemData &p_data, const GameSequence &p_sequence, const std::map &var) { - if (!p_sequence->action) { + if (!p_sequence->GetAction()) { return Polynomial(p_data.space, 1); } - if (p_sequence->action != p_data.m_support.GetActions(p_sequence->GetInfoset()).back()) { + if (p_sequence->GetAction() != p_data.m_support.GetActions(p_sequence->GetInfoset()).back()) { return Polynomial(p_data.space, var.at(p_sequence), 1); } Polynomial equation(p_data.space); - for (auto seq : p_data.m_support.GetSequences(p_sequence->player)) { + for (auto seq : p_data.m_support.GetSequences(p_sequence->GetPlayer())) { if (seq == p_sequence) { continue; } if (const int constraint_coef = - p_data.m_support.GetConstraintEntry(p_sequence->GetInfoset(), seq->action)) { + p_data.m_support.GetConstraintEntry(p_sequence->GetInfoset(), seq->GetAction())) { equation += BuildSequenceVariable(p_data, seq, var) * double(constraint_coef); } } @@ -85,8 +85,8 @@ ProblemData::ProblemData(const BehaviorSupportProfile &p_support) m_support.GetPlayers().size())) { for (auto sequence : m_support.GetSequences()) { - if (sequence->action && - (sequence->action != p_support.GetActions(sequence->GetInfoset()).back())) { + if (sequence->GetAction() && + (sequence->GetAction() != p_support.GetActions(sequence->GetInfoset()).back())) { var[sequence] = var.size() + 1; } } @@ -132,11 +132,11 @@ void IndifferenceEquations(ProblemData &p_data, PolynomialSystem &p_equa void LastActionProbPositiveInequalities(ProblemData &p_data, PolynomialSystem &p_equations) { for (auto sequence : p_data.m_support.GetSequences()) { - if (!sequence->action) { + if (!sequence->GetAction()) { continue; } - const auto &actions = p_data.m_support.GetActions(sequence->action->GetInfoset()); - if (actions.size() > 1 && sequence->action == actions.back()) { + const auto &actions = p_data.m_support.GetActions(sequence->GetAction()->GetInfoset()); + if (actions.size() > 1 && sequence->GetAction() == actions.back()) { p_equations.push_back(p_data.variables.at(sequence)); } } diff --git a/src/solvers/lcp/efglcp.cc b/src/solvers/lcp/efglcp.cc index bc3f1f34a7..8207c0eede 100644 --- a/src/solvers/lcp/efglcp.cc +++ b/src/solvers/lcp/efglcp.cc @@ -67,7 +67,8 @@ template class NashLcpBehaviorSolver::Solution { template NashLcpBehaviorSolver::Solution::Solution(const Game &p_game) - : ns1(p_game->GetPlayer(1)->NumSequences()), ns2(p_game->GetPlayer(2)->NumSequences()), + : ns1(p_game->GetPlayer(1)->GetSequences().size()), + ns2(p_game->GetPlayer(2)->GetSequences().size()), ni1(p_game->GetPlayer(1)->GetInfosets().size() + 1), ni2(p_game->GetPlayer(2)->GetInfosets().size() + 1), maxpay(p_game->GetMaxPayoff() + Rational(1)) diff --git a/src/solvers/lp/lp.cc b/src/solvers/lp/lp.cc index 065b2e58e3..278e9d521c 100644 --- a/src/solvers/lp/lp.cc +++ b/src/solvers/lp/lp.cc @@ -42,8 +42,8 @@ template class GameData { template GameData::GameData(const Game &p_game) : minpay(p_game->GetMinPayoff()) { - ns1 = p_game->GetPlayer(1)->NumSequences(); - ns2 = p_game->GetPlayer(2)->NumSequences(); + ns1 = p_game->GetPlayer(1)->GetSequences().size(); + ns2 = p_game->GetPlayer(2)->GetSequences().size(); for (const auto &player : p_game->GetPlayers()) { int offset = 1; for (const auto &infoset : player->GetInfosets()) { diff --git a/tests/test_players.py b/tests/test_players.py index 339ae1de2b..57dc30e1e0 100644 --- a/tests/test_players.py +++ b/tests/test_players.py @@ -147,6 +147,34 @@ def test_player_strategy_bad_type(): _ = game.players[0].strategies[1.3] +def test_player_sequence_count(): + """Test the identity that the number of sequences is the number of actions plus one.""" + game = gbt.catalog.load("books/myerson1991/fig2_1") + for player in game.players: + action_count = sum(len(infoset.actions) for infoset in player.infosets) + assert len(player.sequences) == action_count + 1 + + +def test_player_sequence_actions(): + game = gbt.catalog.load("books/myerson1991/fig2_1") + player = game.players["Alice"] + sequences = set(tuple(seq.actions) for seq in player.sequences) + reference = ( + set((action, ) for infoset in player.infosets for action in infoset.actions) | + {tuple()} + ) + assert sequences == reference + + +def test_player_sequence_tree(): + game = gbt.catalog.load("books/myerson1991/fig2_1") + player = game.players["Alice"] + for seq in player.sequences: + if not seq.parent: + continue + assert seq in seq.parent.children + + @pytest.mark.parametrize( "game,exp_min_payoffs,exp_max_payoffs", [