diff --git a/src/games/behavspt.cc b/src/games/behavspt.cc index 81cd29c9f..555016215 100644 --- a/src/games/behavspt.cc +++ b/src/games/behavspt.cc @@ -174,20 +174,30 @@ std::shared_ptr BehaviorSupportProfile::GetSequenceForm() cons return m_sequenceForm; } -BehaviorSupportProfile::Sequences BehaviorSupportProfile::GetSequences() const { return {this}; } +Sequences BehaviorSupportProfile::GetSequences() const { return {this}; } -BehaviorSupportProfile::PlayerSequences -BehaviorSupportProfile::GetSequences(GamePlayer &p_player) const +GameSequence BehaviorSupportProfile::GetCorrespondingSequence(const GameAction &p_action) const { - return {this, p_player}; + return GetSequenceForm()->m_correspondence.at(p_action); } -int BehaviorSupportProfile::GetConstraintEntry(const GameInfoset &p_infoset, - const GameAction &p_action) const +int BehaviorSupportProfile::GetSequenceConstraintEntry(const GameInfoset &p_infoset, + const GameAction &p_action) const { return GetSequenceForm()->GetConstraintEntry(p_infoset, p_action); } +PlayerSequences BehaviorSupportProfile::GetSequences(const GamePlayer &p_player) const +{ + return {this, p_player}; +} + +const Rational & +BehaviorSupportProfile::GetTerminalProb(const std::map &p_profile) const +{ + return GetSequenceForm()->GetTerminalProb(p_profile); +} + const Rational & BehaviorSupportProfile::GetPayoff(const std::map &p_profile, const GamePlayer &p_player) const @@ -220,80 +230,6 @@ BehaviorSupportProfile::ToMixedBehaviorProfile(const std::mapGetSequenceForm()->m_sequences.cbegin(), - m_support->GetSequenceForm()->m_sequences.cend(), 0, - [](int acc, const std::pair> &seq) { - return acc + seq.second.size(); - }); -} - -BehaviorSupportProfile::Sequences::iterator BehaviorSupportProfile::Sequences::begin() const -{ - return {m_support->GetSequenceForm(), false}; -} -BehaviorSupportProfile::Sequences::iterator BehaviorSupportProfile::Sequences::end() const -{ - return {m_support->GetSequenceForm(), true}; -} - -BehaviorSupportProfile::Sequences::iterator::iterator( - const std::shared_ptr p_sfg, bool p_end) - : m_sfg(p_sfg) -{ - if (p_end) { - m_currentPlayer = m_sfg->m_sequences.cend(); - } - else { - m_currentPlayer = m_sfg->m_sequences.cbegin(); - m_currentSequence = m_currentPlayer->second.cbegin(); - } -} - -BehaviorSupportProfile::Sequences::iterator & -BehaviorSupportProfile::Sequences::iterator::operator++() -{ - if (m_currentPlayer == m_sfg->m_sequences.cend()) { - return *this; - } - m_currentSequence++; - if (m_currentSequence != m_currentPlayer->second.cend()) { - return *this; - } - m_currentPlayer++; - if (m_currentPlayer != m_sfg->m_sequences.cend()) { - m_currentSequence = m_currentPlayer->second.cbegin(); - } - return *this; -} - -bool BehaviorSupportProfile::Sequences::iterator::operator==(const iterator &it) const -{ - if (m_sfg != it.m_sfg || m_currentPlayer != it.m_currentPlayer) { - return false; - } - if (m_currentPlayer == m_sfg->m_sequences.end()) { - return true; - } - return (m_currentSequence == it.m_currentSequence); -} - -std::vector::const_iterator BehaviorSupportProfile::PlayerSequences::begin() const -{ - return m_support->GetSequenceForm()->m_sequences.at(m_player).begin(); -} - -std::vector::const_iterator BehaviorSupportProfile::PlayerSequences::end() const -{ - return m_support->GetSequenceForm()->m_sequences.at(m_player).end(); -} - -size_t BehaviorSupportProfile::PlayerSequences::size() const -{ - return m_support->GetSequenceForm()->m_sequences.at(m_player).size(); -} - BehaviorSupportProfile::SequenceContingencies::iterator::iterator( const std::shared_ptr p_sfg, bool p_end) : m_sfg(p_sfg), m_end(p_end) diff --git a/src/games/behavspt.h b/src/games/behavspt.h index 59e89fbbc..52c6a0da5 100644 --- a/src/games/behavspt.h +++ b/src/games/behavspt.h @@ -30,10 +30,6 @@ namespace Gambit { class GameSequenceForm; -class SequencesWrapper; -class PlayerSequencesWrapper; -class InfosetsWrapper; -class ContingenciesWrapper; /// This class represents a subset of the actions in an extensive game. /// It is enforced that each player has at least one action at each @@ -165,50 +161,6 @@ class BehaviorSupportProfile { } }; - class Sequences { - const BehaviorSupportProfile *m_support; - - public: - class iterator { - const std::shared_ptr m_sfg; - std::map>::const_iterator m_currentPlayer; - std::vector::const_iterator m_currentSequence; - - public: - iterator(const std::shared_ptr p_sfg, bool p_end); - - GameSequence operator*() const { return *m_currentSequence; } - GameSequence operator->() const { return *m_currentSequence; } - - iterator &operator++(); - - bool operator==(const iterator &it) const; - bool operator!=(const iterator &it) const { return !(*this == it); } - }; - - Sequences(const BehaviorSupportProfile *p_support) : m_support(p_support) {} - - size_t size() const; - - iterator begin() const; - iterator end() const; - }; - - class PlayerSequences { - const BehaviorSupportProfile *m_support; - GamePlayer m_player; - - public: - PlayerSequences(const BehaviorSupportProfile *p_support, const GamePlayer &p_player) - : m_support(p_support), m_player(p_player) - { - } - - size_t size() const; - std::vector::const_iterator begin() const; - std::vector::const_iterator end() const; - }; - class SequenceContingencies { const BehaviorSupportProfile *m_support; @@ -245,15 +197,17 @@ class BehaviorSupportProfile { std::shared_ptr GetSequenceForm() const; Sequences GetSequences() const; - PlayerSequences GetSequences(GamePlayer &p_player) const; - int GetConstraintEntry(const GameInfoset &p_infoset, const GameAction &p_action) const; + PlayerSequences GetSequences(const GamePlayer &p_player) const; const Rational &GetPayoff(const std::map &p_profile, const GamePlayer &p_player) const; + const Rational &GetTerminalProb(const std::map &p_profile) const; GameRep::Players GetPlayers() const { return GetGame()->GetPlayers(); } MixedBehaviorProfile ToMixedBehaviorProfile(const std::map &) const; Infosets GetInfosets() const { return {this}; }; SequenceContingencies GetSequenceContingencies() const; + GameSequence GetCorrespondingSequence(const GameAction &p_action) const; + int GetSequenceConstraintEntry(const GameInfoset &p_infoset, const GameAction &p_action) const; void FindReachableInfosets(GameNode p_node) const; std::shared_ptr> GetReachableInfosets() const; diff --git a/src/games/game.cc b/src/games/game.cc index 37619653a..eff072df6 100644 --- a/src/games/game.cc +++ b/src/games/game.cc @@ -32,6 +32,7 @@ // editing operations into the game itself instead of in the member-object // classes. #include "gametree.h" +#include "gameseq.h" namespace Gambit { @@ -409,4 +410,74 @@ template class MixedStrategyProfileRep; template class MixedStrategyProfile; template class MixedStrategyProfile; +//======================================================================== +// Sequences +//======================================================================== + +size_t Sequences::size() const +{ + return std::accumulate(m_support->GetSequenceForm()->m_sequences.cbegin(), + m_support->GetSequenceForm()->m_sequences.cend(), 0, + [](int acc, const std::pair> &seq) { + return acc + seq.second.size(); + }); +} + +Sequences::iterator Sequences::begin() const { return {m_support->GetSequenceForm(), false}; } +Sequences::iterator Sequences::end() const { return {m_support->GetSequenceForm(), true}; } + +Sequences::iterator::iterator(const std::shared_ptr p_sfg, bool p_end) + : m_sfg(p_sfg) +{ + if (p_end) { + m_currentPlayer = m_sfg->m_sequences.cend(); + } + else { + m_currentPlayer = m_sfg->m_sequences.cbegin(); + m_currentSequence = m_currentPlayer->second.cbegin(); + } +} + +Sequences::iterator &Sequences::iterator::operator++() +{ + if (m_currentPlayer == m_sfg->m_sequences.cend()) { + return *this; + } + m_currentSequence++; + if (m_currentSequence != m_currentPlayer->second.cend()) { + return *this; + } + m_currentPlayer++; + if (m_currentPlayer != m_sfg->m_sequences.cend()) { + m_currentSequence = m_currentPlayer->second.cbegin(); + } + return *this; +} + +bool Sequences::iterator::operator==(const iterator &it) const +{ + if (m_sfg != it.m_sfg || m_currentPlayer != it.m_currentPlayer) { + return false; + } + if (m_currentPlayer == m_sfg->m_sequences.end()) { + return true; + } + return (m_currentSequence == it.m_currentSequence); +} + +std::vector::const_iterator PlayerSequences::begin() const +{ + return m_support->GetSequenceForm()->m_sequences.at(m_player).begin(); +} + +std::vector::const_iterator PlayerSequences::end() const +{ + return m_support->GetSequenceForm()->m_sequences.at(m_player).end(); +} + +size_t PlayerSequences::size() const +{ + return m_support->GetSequenceForm()->m_sequences.at(m_player).size(); +} + } // end namespace Gambit diff --git a/src/games/game.h b/src/games/game.h index f98827b57..4d52df73f 100644 --- a/src/games/game.h +++ b/src/games/game.h @@ -66,6 +66,8 @@ using Game = std::shared_ptr; class PureStrategyProfile; template class MixedStrategyProfile; class StrategySupportProfile; +class BehaviorSupportProfile; +class GameSequenceForm; template class MixedBehaviorProfile; @@ -597,6 +599,50 @@ inline GameNodeRep::Actions::iterator::iterator(GameInfosetRep::Actions::iterato inline GameNode GameNodeRep::Actions::iterator::GetOwner() const { return m_child_it.GetOwner(); } +class Sequences { + const BehaviorSupportProfile *m_support; + +public: + class iterator { + const std::shared_ptr m_sfg; + std::map>::const_iterator m_currentPlayer; + std::vector::const_iterator m_currentSequence; + + public: + iterator(const std::shared_ptr p_sfg, bool p_end); + + GameSequence operator*() const { return *m_currentSequence; } + GameSequence operator->() const { return *m_currentSequence; } + + iterator &operator++(); + + bool operator==(const iterator &it) const; + bool operator!=(const iterator &it) const { return !(*this == it); } + }; + + Sequences(const BehaviorSupportProfile *p_support) : m_support(p_support) {} + + size_t size() const; + + iterator begin() const; + iterator end() const; +}; + +class PlayerSequences { + const BehaviorSupportProfile *m_support; + GamePlayer m_player; + +public: + PlayerSequences(const BehaviorSupportProfile *p_support, const GamePlayer &p_player) + : m_support(p_support), m_player(p_player) + { + } + + size_t size() const; + std::vector::const_iterator begin() const; + std::vector::const_iterator end() const; +}; + /// This is the class for representing an arbitrary finite game. class GameRep : public std::enable_shared_from_this { friend class GameOutcomeRep; @@ -957,6 +1003,49 @@ class GameRep : public std::enable_shared_from_this { /// Build any computed values anew virtual void BuildComputedValues() const {} + + // Sequence form methods + + virtual std::shared_ptr GetFullSupport() + { + throw std::runtime_error("Sequence form can only be generated for extensive form games"); + }; + + virtual const Sequences GetSequences() + { + throw std::runtime_error("Sequence form can only be generated for extensive form games"); + } + + virtual const Rational &GetTerminalProb(const std::map &p_profile) + { + throw std::runtime_error("Sequence form can only be generated for extensive form games"); + } + + virtual const PlayerSequences GetSequences(const GamePlayer &p_player) + { + throw std::runtime_error("Sequence form can only be generated for extensive form games"); + } + + virtual const Rational &GetPayoff(const std::map &p_profile, + const GamePlayer &p_player) + { + throw std::runtime_error("Sequence form can only be generated for extensive form games"); + } + + virtual const GameSequence GetCorrespondingSequence(const GameAction &p_action) + { + throw std::runtime_error("Sequence form can only be generated for extensive form games"); + } + + virtual int GetSequenceConstraintEntry(const GameInfoset &p_infoset, const GameAction &p_action) + { + throw std::runtime_error("Sequence form can only be generated for extensive form games"); + } + + virtual const GameSequence GetEmptySequence(const GamePlayer &p_player) + { + throw std::runtime_error("Sequence form can only be generated for extensive form games"); + } }; //======================================================================= @@ -1081,6 +1170,48 @@ std::list UniformOnSimplex(int p_denom, size_t p_dim, Generator &gener return output; } +//======================================================================= +// Mixed sequence profile class +//======================================================================= + +template class MixedSequenceProfile { + +private: + std::map probs; + Game m_game; + +public: + MixedSequenceProfile(const Game &p_game) : m_game(p_game) {} + + const T &operator[](const GameSequence &p_key) const { return probs.at(p_key); } + + T &operator[](const GameSequence &p_key) { return probs[p_key]; } + + MixedBehaviorProfile GetMixedBehaviorProfile() const + { + MixedBehaviorProfile mbp(m_game); + for (const auto &[seq, prob] : probs) { + auto parent_seq = seq->parent.lock(); + if (parent_seq) { + T parent_prob = probs.at(parent_seq); + auto action = seq->action; + mbp[action] = (parent_prob > static_cast(0)) ? prob / parent_prob : static_cast(0); + } + } + return mbp; + } + + std::map GetPayoffs() const + { + std::map payoffs; + auto mbp = GetMixedBehaviorProfile(); + for (auto player : m_game->GetPlayers()) { + payoffs[player] = mbp.GetPayoff(player); + } + return payoffs; + } +}; + } // namespace Gambit #endif // LIBGAMBIT_GAME_H diff --git a/src/games/gameseq.cc b/src/games/gameseq.cc index 367a7ec7c..24b33abd7 100644 --- a/src/games/gameseq.cc +++ b/src/games/gameseq.cc @@ -67,22 +67,26 @@ void GameSequenceForm::BuildSequences() } void GameSequenceForm::FillTableau(const GameNode &n, const Rational &prob, - std::map &p_currentSequences) + std::map &p_currentSequences, + std::map p_cumPayoff) { if (n->GetOutcome()) { for (auto player : m_support.GetGame()->GetPlayers()) { - GetPayoffEntry(p_currentSequences, player) += - prob * n->GetOutcome()->GetPayoff(player); + p_cumPayoff[player] += n->GetOutcome()->GetPayoff(player); } } if (!n->GetInfoset()) { + GetTerminalProb(p_currentSequences) += prob; + for (auto player : m_support.GetGame()->GetPlayers()) { + GetPayoffEntry(p_currentSequences, player) += prob * p_cumPayoff[player]; + } return; } if (n->GetPlayer()->IsChance()) { for (auto action : n->GetInfoset()->GetActions()) { FillTableau(n->GetChild(action), prob * static_cast(n->GetInfoset()->GetActionProb(action)), - p_currentSequences); + p_currentSequences, p_cumPayoff); } } else { @@ -91,7 +95,7 @@ void GameSequenceForm::FillTableau(const GameNode &n, const Rational &prob, for (auto action : m_support.GetActions(n->GetInfoset())) { m_constraints[{n->GetInfoset(), action}] = -1; p_currentSequences[n->GetPlayer()] = m_correspondence.at(action); - FillTableau(n->GetChild(action), prob, p_currentSequences); + FillTableau(n->GetChild(action), prob, p_currentSequences, p_cumPayoff); } p_currentSequences[n->GetPlayer()] = tmp_sequence; } @@ -104,12 +108,15 @@ void GameSequenceForm::FillTableau() dim[player->GetNumber()] = m_sequences.at(player).size(); } m_payoffs = NDArray(dim, dim.size()); + m_terminalProb = NDArray(dim, dim.size()); std::map currentSequence; + std::map cumPayoff; for (auto player : GetPlayers()) { currentSequence[player] = m_sequences[player].front(); + cumPayoff[player] = Rational(0); } - FillTableau(m_support.GetGame()->GetRoot(), Rational(1), currentSequence); + FillTableau(m_support.GetGame()->GetRoot(), Rational(1), currentSequence, cumPayoff); } } // end namespace Gambit diff --git a/src/games/gameseq.h b/src/games/gameseq.h index bcf4fec70..8815c3571 100644 --- a/src/games/gameseq.h +++ b/src/games/gameseq.h @@ -30,10 +30,13 @@ namespace Gambit { class GameSequenceForm { friend class BehaviorSupportProfile; + friend class Sequences; + friend class PlayerSequences; BehaviorSupportProfile m_support; std::map> m_sequences; NDArray m_payoffs; + NDArray m_terminalProb; std::map, int> m_constraints; // (sparse) constraint matrices std::set m_infosets; // infosets actually reachable given support std::map m_correspondence; @@ -41,7 +44,8 @@ class GameSequenceForm { void BuildSequences(); void BuildSequences(const GameNode &, std::map &); void FillTableau(); - void FillTableau(const GameNode &, const Rational &, std::map &); + void FillTableau(const GameNode &, const Rational &, std::map &, + std::map); Array ProfileToIndex(const std::map &p_profile) const { @@ -52,6 +56,11 @@ class GameSequenceForm { return index; } + Rational &GetTerminalProb(const std::map &p_profile) + { + return m_terminalProb.at(ProfileToIndex(p_profile), 1); + } + Rational &GetPayoffEntry(const std::map &p_profile, const GamePlayer &p_player) { diff --git a/src/games/gametree.cc b/src/games/gametree.cc index 33509228e..bfd43f6b2 100644 --- a/src/games/gametree.cc +++ b/src/games/gametree.cc @@ -871,6 +871,7 @@ void GameTreeRep::ClearComputedValues() const m_ownPriorActionInfo = nullptr; const_cast(this)->m_unreachableNodes = nullptr; m_computedValues = false; + m_fullSupport = nullptr; } void GameTreeRep::BuildComputedValues() const diff --git a/src/games/gametree.h b/src/games/gametree.h index e77500964..26778498c 100644 --- a/src/games/gametree.h +++ b/src/games/gametree.h @@ -47,6 +47,48 @@ class GameTreeRep : public GameExplicitRep { std::map> m_nodePlays; mutable std::shared_ptr m_ownPriorActionInfo; mutable std::unique_ptr> m_unreachableNodes; + mutable std::shared_ptr m_fullSupport; + + std::shared_ptr GetFullSupport() override + { + if (!m_fullSupport) { + m_fullSupport = std::make_shared(shared_from_this()); + } + return m_fullSupport; + } + + const Sequences GetSequences() override { return GetFullSupport()->GetSequences(); } + + const PlayerSequences GetSequences(const GamePlayer &p_player) override + { + return GetFullSupport()->GetSequences(p_player); + } + + const Rational &GetTerminalProb(const std::map &p_profile) override + { + return GetFullSupport()->GetTerminalProb(p_profile); + } + + const Rational &GetPayoff(const std::map &p_profile, + const GamePlayer &p_player) override + { + return GetFullSupport()->GetPayoff(p_profile, p_player); + } + + const GameSequence GetCorrespondingSequence(const GameAction &p_action) override + { + return GetFullSupport()->GetCorrespondingSequence(p_action); + } + + int GetSequenceConstraintEntry(const GameInfoset &p_infoset, const GameAction &p_action) override + { + return GetFullSupport()->GetSequenceConstraintEntry(p_infoset, p_action); + } + + const GameSequence GetEmptySequence(const GamePlayer &p_player) override + { + return *(GetFullSupport()->GetSequences(p_player).begin()); + } /// @name Private auxiliary functions //@{ diff --git a/src/pygambit/gambit.pxd b/src/pygambit/gambit.pxd index 50e56d7e9..a968abd5a 100644 --- a/src/pygambit/gambit.pxd +++ b/src/pygambit/gambit.pxd @@ -1,6 +1,6 @@ from libcpp cimport bool from libcpp.string cimport string -from libcpp.memory cimport shared_ptr, unique_ptr +from libcpp.memory cimport shared_ptr, unique_ptr, weak_ptr from libcpp.list cimport list as stdlist from libcpp.vector cimport vector as stdvector from libcpp.set cimport set as stdset @@ -93,6 +93,11 @@ cdef extern from "games/game.h": cdef cppclass c_PureBehaviorProfile "PureBehaviorProfile": c_PureBehaviorProfile(c_Game) except + + cdef cppclass c_GameSequenceRep "GameSequenceRep": + c_GamePlayer player + c_GameAction action + weak_ptr[c_GameSequenceRep] parent + cdef cppclass c_GameStrategyRep "GameStrategyRep": int GetNumber() except + int GetId() except + diff --git a/src/pygambit/gambit.pyx b/src/pygambit/gambit.pyx index ee31dde27..dc51547a6 100644 --- a/src/pygambit/gambit.pyx +++ b/src/pygambit/gambit.pyx @@ -50,7 +50,10 @@ class Rational(fractions.Fraction): @cython.cfunc def rat_to_py(r: c_Rational): """Convert a C++ Rational number to a Python Rational.""" - return Rational(to_string(r).decode("ascii")) + s = to_string(r).decode("ascii") + if not s: + return Rational(0) + return Rational(s) @cython.cfunc diff --git a/src/solvers/enumpoly/efgpoly.cc b/src/solvers/enumpoly/efgpoly.cc index e88cca353..02bd2fa92 100644 --- a/src/solvers/enumpoly/efgpoly.cc +++ b/src/solvers/enumpoly/efgpoly.cc @@ -72,7 +72,7 @@ Polynomial BuildSequenceVariable(ProblemData &p_data, const GameSequence continue; } if (const int constraint_coef = - p_data.m_support.GetConstraintEntry(p_sequence->GetInfoset(), seq->action)) { + p_data.m_support.GetSequenceConstraintEntry(p_sequence->GetInfoset(), seq->action)) { equation += BuildSequenceVariable(p_data, seq, var) * double(constraint_coef); } } diff --git a/src/solvers/lcp/efglcp.cc b/src/solvers/lcp/efglcp.cc index 9b44b08e9..e85347b72 100644 --- a/src/solvers/lcp/efglcp.cc +++ b/src/solvers/lcp/efglcp.cc @@ -24,6 +24,7 @@ #include "gambit.h" #include "solvers/linalg/lemketab.h" #include "solvers/lcp/lcp.h" +#include "games/gameseq.h" namespace Gambit::Nash { @@ -44,11 +45,11 @@ template class NashLcpBehaviorSolver { class Solution; - void FillTableau(Matrix &, const GameNode &, T, int, int, Solution &) const; + void FillTableau(Matrix &A, const Game &p_game, Solution &p_solution) const; void AllLemke(const Game &, int dup, linalg::LemkeTableau &B, int depth, Matrix &, Solution &) const; - void GetProfile(const linalg::LemkeTableau &tab, MixedBehaviorProfile &, const Vector &, - const GameNode &n, int, int, Solution &) const; + MixedBehaviorProfile GetProfile(const linalg::LemkeTableau &tab, const Vector &sol, + const Game &p_game, Solution &p_solution) const; }; template class NashLcpBehaviorSolver::Solution { @@ -126,7 +127,8 @@ std::list> NashLcpBehaviorSolver::Solve(const Game &p const int ntot = solution.ns1 + solution.ns2 + solution.ni1 + solution.ni2; Matrix A(1, ntot, 0, ntot); A = static_cast(0); - FillTableau(A, p_game->GetRoot(), static_cast(1), 1, 1, solution); + + FillTableau(A, p_game, solution); for (int i = A.MinRow(); i <= A.MaxRow(); i++) { A(i, 0) = static_cast(-1); } @@ -158,8 +160,7 @@ std::list> NashLcpBehaviorSolver::Solve(const Game &p solution.AddBFS(tab); Vector sol(tab.MinRow(), tab.MaxRow()); tab.BasisVector(sol); - MixedBehaviorProfile profile(p_game); - GetProfile(tab, profile, sol, p_game->GetRoot(), 1, 1, solution); + auto profile = GetProfile(tab, sol, p_game, solution); profile.UndefinedToCentroid(); solution.m_equilibria.push_back(profile); this->m_onEquilibrium(profile, "NE"); @@ -187,7 +188,6 @@ void NashLcpBehaviorSolver::AllLemke(const Game &p_game, int j, linalg::Lemke } Vector sol(B.MinRow(), B.MaxRow()); - MixedBehaviorProfile profile(p_game); bool newsol = false; for (int i = B.MinRow(); i <= B.MaxRow() && !newsol; i++) { @@ -214,7 +214,7 @@ void NashLcpBehaviorSolver::AllLemke(const Game &p_game, int j, linalg::Lemke if (BCopy.SF_LCPPath(-missing) == 1) { newsol = p_solution.AddBFS(BCopy); BCopy.BasisVector(sol); - GetProfile(BCopy, profile, sol, p_game->GetRoot(), 1, 1, p_solution); + auto profile = GetProfile(BCopy, sol, p_game, p_solution); profile.UndefinedToCentroid(); if (newsol) { m_onEquilibrium(profile, "NE"); @@ -237,107 +237,87 @@ void NashLcpBehaviorSolver::AllLemke(const Game &p_game, int j, linalg::Lemke } template -void NashLcpBehaviorSolver::FillTableau(Matrix &A, const GameNode &n, T prob, int s1, int s2, +void NashLcpBehaviorSolver::FillTableau(Matrix &A, const Game &p_game, Solution &p_solution) const { const int ns1 = p_solution.ns1; const int ns2 = p_solution.ns2; const int ni1 = p_solution.ni1; - - const GameOutcome outcome = n->GetOutcome(); - if (outcome) { - A(s1, ns1 + s2) += Rational(prob) * (outcome->GetPayoff(n->GetGame()->GetPlayer(1)) - - p_solution.maxpay); - A(ns1 + s2, s1) += Rational(prob) * (outcome->GetPayoff(n->GetGame()->GetPlayer(2)) - - p_solution.maxpay); - } - if (n->IsTerminal()) { - return; - } - const GameInfoset infoset = n->GetInfoset(); - if (n->GetPlayer()->IsChance()) { - for (const auto &action : infoset->GetActions()) { - FillTableau(A, n->GetChild(action), - Rational(prob) * static_cast(infoset->GetActionProb(action)), s1, s2, - p_solution); + auto player1 = p_game->GetPlayer(1); + auto player2 = p_game->GetPlayer(2); + auto sequences1 = p_game->GetSequences(player1); + auto sequences2 = p_game->GetSequences(player2); + for (auto seq : sequences1) { + auto parentSeq = seq->parent.lock(); + if (parentSeq) { + const int infoset_idx = ns1 + ns2 + seq->GetInfoset()->GetNumber() + 1; + const int seq_idx = seq->number; + const int parent_idx = parentSeq->number; + A(parent_idx, infoset_idx) = static_cast(-1); + A(infoset_idx, parent_idx) = static_cast(1); + A(seq_idx, infoset_idx) = static_cast(1); + A(infoset_idx, seq_idx) = static_cast(-1); } } - else if (n->GetPlayer()->GetNumber() == 1) { - const int infoset_idx = ns1 + ns2 + infoset->GetNumber() + 1; - A(s1, infoset_idx) = static_cast(-1); - A(infoset_idx, s1) = static_cast(1); - int snew = p_solution.infosetOffset.at(infoset); - for (const auto &child : n->GetChildren()) { - snew++; - A(snew, infoset_idx) = static_cast(1); - A(infoset_idx, snew) = static_cast(-1); - FillTableau(A, child, prob, snew, s2, p_solution); + for (auto seq : sequences2) { + auto parentSeq = seq->parent.lock(); + if (parentSeq) { + const int infoset_idx = ns1 + ns2 + ni1 + seq->GetInfoset()->GetNumber() + 1; + const int seq_idx = seq->number; + const int parent_idx = parentSeq->number; + A(ns1 + parent_idx, infoset_idx) = static_cast(-1); + A(infoset_idx, ns1 + parent_idx) = static_cast(1); + A(ns1 + seq_idx, infoset_idx) = static_cast(1); + A(infoset_idx, ns1 + seq_idx) = static_cast(-1); } } - else { - const int infoset_idx = ns1 + ns2 + ni1 + n->GetInfoset()->GetNumber() + 1; - A(ns1 + s2, infoset_idx) = static_cast(-1); - A(infoset_idx, ns1 + s2) = static_cast(1); - int snew = p_solution.infosetOffset.at(n->GetInfoset()); - for (const auto &child : n->GetChildren()) { - snew++; - A(ns1 + snew, infoset_idx) = static_cast(1); - A(infoset_idx, ns1 + snew) = static_cast(-1); - FillTableau(A, child, prob, s1, snew, p_solution); + for (auto seq1 : sequences1) { + for (auto seq2 : sequences2) { + const int s1 = seq1->number; + const int s2 = seq2->number; + std::map profile; + profile[player1] = seq1; + profile[player2] = seq2; + A(s1, ns1 + s2) = p_game->GetPayoff(profile, player1) - + (p_solution.maxpay * p_game->GetTerminalProb(profile)); + A(ns1 + s2, s1) = p_game->GetPayoff(profile, player2) - + (p_solution.maxpay * p_game->GetTerminalProb(profile)); } } } template -void NashLcpBehaviorSolver::GetProfile(const linalg::LemkeTableau &tab, - MixedBehaviorProfile &v, const Vector &sol, - const GameNode &n, int s1, int s2, - Solution &p_solution) const +MixedBehaviorProfile +NashLcpBehaviorSolver::GetProfile(const linalg::LemkeTableau &tab, const Vector &sol, + const Game &p_game, Solution &p_solution) const { const int ns1 = p_solution.ns1; - - if (n->IsTerminal()) { - return; - } - if (n->GetPlayer()->IsChance()) { - for (const auto &child : n->GetChildren()) { - GetProfile(tab, v, sol, child, s1, s2, p_solution); + auto player1 = p_game->GetPlayer(1); + auto player2 = p_game->GetPlayer(2); + auto sequences1 = p_game->GetSequences(player1); + auto sequences2 = p_game->GetSequences(player2); + Gambit::MixedSequenceProfile msp(p_game); + for (auto seq : sequences1) { + const int seq_num = seq->number; + if (tab.Member(seq_num)) { + const int index = tab.Find(seq_num); + msp[seq] = (sol[index] > p_solution.eps) ? sol[index] : static_cast(0); } - } - else if (n->GetPlayer()->GetNumber() == 1) { - int snew = p_solution.infosetOffset.at(n->GetInfoset()); - for (const auto &action : n->GetInfoset()->GetActions()) { - snew++; - v[action] = static_cast(0); - if (tab.Member(s1)) { - const int ind = tab.Find(s1); - if (sol[ind] > p_solution.eps && tab.Member(snew)) { - const int ind2 = tab.Find(snew); - if (sol[ind2] > p_solution.eps) { - v[action] = sol[ind2] / sol[ind]; - } - } - } - GetProfile(tab, v, sol, n->GetChild(action), snew, s2, p_solution); + else { + msp[seq] = static_cast(0); } } - else { - int snew = p_solution.infosetOffset.at(n->GetInfoset()); - for (const auto &action : n->GetInfoset()->GetActions()) { - snew++; - v[action] = static_cast(0); - if (tab.Member(ns1 + s2)) { - const int ind = tab.Find(ns1 + s2); - if (sol[ind] > p_solution.eps && tab.Member(ns1 + snew)) { - const int ind2 = tab.Find(ns1 + snew); - if (sol[ind2] > p_solution.eps) { - v[action] = sol[ind2] / sol[ind]; - } - } - } - GetProfile(tab, v, sol, n->GetChild(action), s1, snew, p_solution); + for (auto seq : sequences2) { + const int seq_num = seq->number; + if (tab.Member(ns1 + seq_num)) { + const int index = tab.Find(ns1 + seq_num); + msp[seq] = (sol[index] > p_solution.eps) ? sol[index] : static_cast(0); + } + else { + msp[seq] = static_cast(0); } } + return msp.GetMixedBehaviorProfile(); } template diff --git a/src/solvers/lp/lp.cc b/src/solvers/lp/lp.cc index 9e0a5e467..3ade68f02 100644 --- a/src/solvers/lp/lp.cc +++ b/src/solvers/lp/lp.cc @@ -23,6 +23,7 @@ #include "gambit.h" #include "solvers/lp/lp.h" #include "solvers/linalg/lpsolve.h" +#include "games/gameseq.h" namespace Gambit::Nash { @@ -34,10 +35,10 @@ template class GameData { explicit GameData(const Game &); - void FillTableau(Matrix &A, const GameNode &n, const T &prob, int s1, int s2); + void FillTableau(Matrix &A, const Game &p_game); - void GetBehavior(MixedBehaviorProfile &v, const Array &, const Array &, - const GameNode &, int, int); + MixedBehaviorProfile GetBehavior(const Array &p_primal, const Array &p_dual, + const Game &p_game); }; template GameData::GameData(const Game &p_game) : minpay(p_game->GetMinPayoff()) @@ -53,84 +54,63 @@ template GameData::GameData(const Game &p_game) : minpay(p_game->Ge } } -// -// Recursively fills the constraint matrix A for the subtree rooted at 'n'. -// -template -void GameData::FillTableau(Matrix &A, const GameNode &n, const T &prob, int s1, int s2) +template void GameData::FillTableau(Matrix &A, const Game &p_game) { - const GameOutcome outcome = n->GetOutcome(); - if (outcome) { - A(s1, s2) += - Rational(prob) * (outcome->GetPayoff(n->GetGame()->GetPlayer(1)) - minpay); - } - if (n->IsTerminal()) { - return; - } - const GameInfoset infoset = n->GetInfoset(); - if (n->GetPlayer()->IsChance()) { - for (const auto &action : infoset->GetActions()) { - FillTableau(A, n->GetChild(action), prob * static_cast(infoset->GetActionProb(action)), - s1, s2); + auto player1 = p_game->GetPlayer(1); + auto player2 = p_game->GetPlayer(2); + auto sequences1 = p_game->GetSequences(player1); + auto sequences2 = p_game->GetSequences(player2); + for (auto seq : sequences1) { + auto parentSeq = seq->parent.lock(); + if (parentSeq) { + const int col = ns2 + seq->GetInfoset()->GetNumber() + 1; + const int row = seq->number; + const int parentRow = parentSeq->number; + A(parentRow, col) = static_cast(1); + A(row, col) = static_cast(-1); } } - else if (n->GetPlayer()->GetNumber() == 1) { - const int col = ns2 + infoset->GetNumber() + 1; - int snew = infosetOffset.at(infoset); - A(s1, col) = static_cast(1); - for (const auto &child : n->GetChildren()) { - A(++snew, col) = static_cast(-1); - FillTableau(A, child, prob, snew, s2); + for (auto seq : sequences2) { + auto parentSeq = seq->parent.lock(); + if (parentSeq) { + const int row = ns1 + seq->GetInfoset()->GetNumber() + 1; + const int col = seq->number; + const int parentCol = parentSeq->number; + A(row, parentCol) = static_cast(-1); + A(row, col) = static_cast(1); } } - else { - const int row = ns1 + infoset->GetNumber() + 1; - int snew = infosetOffset.at(infoset); - A(row, s2) = static_cast(-1); - for (const auto &child : n->GetChildren()) { - A(row, ++snew) = static_cast(1); - FillTableau(A, child, prob, s1, snew); + for (auto seq1 : sequences1) { + for (auto seq2 : sequences2) { + const int row = seq1->number; + const int col = seq2->number; + std::map profile; + profile[player1] = seq1; + profile[player2] = seq2; + A(row, col) = + p_game->GetPayoff(profile, player1) - (minpay * p_game->GetTerminalProb(profile)); } } } -// -// Recursively construct the behavior profile from the sequence form -// solution represented by 'p_primal' (containing player 2's -// sequences) and 'p_dual' (containing player 1's sequences). -// -// Any information sets not reached with positive probability have -// their action probabilities set to zero. -// template -void GameData::GetBehavior(MixedBehaviorProfile &v, const Array &p_primal, - const Array &p_dual, const GameNode &n, int s1, int s2) +MixedBehaviorProfile GameData::GetBehavior(const Array &p_primal, const Array &p_dual, + const Game &p_game) { - if (n->IsTerminal()) { - return; - } - if (n->GetPlayer()->IsChance()) { - for (const auto &child : n->GetChildren()) { - GetBehavior(v, p_primal, p_dual, child, s1, s2); - } + auto player1 = p_game->GetPlayer(1); + auto player2 = p_game->GetPlayer(2); + auto sequences1 = p_game->GetSequences(player1); + auto sequences2 = p_game->GetSequences(player2); + Gambit::MixedSequenceProfile msp(p_game); + for (auto seq : sequences1) { + const int index = seq->number; + msp[seq] = p_dual[index]; } - else if (n->GetPlayer()->GetNumber() == 2) { - int snew = infosetOffset.at(n->GetInfoset()); - for (const auto &action : n->GetInfoset()->GetActions()) { - snew++; - v[action] = - (p_primal[s1] > static_cast(0)) ? p_primal[snew] / p_primal[s1] : static_cast(0); - GetBehavior(v, p_primal, p_dual, n->GetChild(action), snew, s2); - } - } - else { - int snew = infosetOffset.at(n->GetInfoset()); - for (const auto &action : n->GetInfoset()->GetActions()) { - snew++; - v[action] = (p_dual[s2] > static_cast(0)) ? p_dual[snew] / p_dual[s2] : static_cast(0); - GetBehavior(v, p_primal, p_dual, n->GetChild(action), s1, snew); - } + for (auto seq : sequences2) { + const int index = seq->number; + msp[seq] = p_primal[index]; } + return msp.GetMixedBehaviorProfile(); } // @@ -187,7 +167,7 @@ std::list> LpBehaviorSolve(const Game &p_game, b = static_cast(0); c = static_cast(0); - data.FillTableau(A, p_game->GetRoot(), static_cast(1), 1, 1); + data.FillTableau(A, p_game); A(1, data.ns2 + 1) = static_cast(-1); A(data.ns1 + 1, 1) = static_cast(1); @@ -197,8 +177,7 @@ std::list> LpBehaviorSolve(const Game &p_game, Array primal(A.NumColumns()), dual(A.NumRows()); std::list> solution; SolveLP(A, b, c, p_game->GetPlayer(2)->GetInfosets().size() + 1, primal, dual); - MixedBehaviorProfile profile(p_game); - data.GetBehavior(profile, primal, dual, p_game->GetRoot(), 1, 1); + MixedBehaviorProfile profile = data.GetBehavior(primal, dual, p_game); profile.UndefinedToCentroid(); p_onEquilibrium(profile, "NE"); solution.push_back(profile); diff --git a/tests/games.py b/tests/games.py index e34591d4d..7dfc4b8df 100644 --- a/tests/games.py +++ b/tests/games.py @@ -123,6 +123,230 @@ def create_2x2_zero_sum_efg(missing_term_outcome: bool = False) -> gbt.Game: return g +def create_perfect_info_with_chance_efg() -> gbt.Game: + # Tests case in which sequence profile probabilities don't sum to 1 + g = gbt.Game.new_tree(players=["1", "2"], title="2 player perfect info with chance") + g.append_move(g.root, "1", ["a", "b"]) + g.append_move(g.root.children[0], g.players.chance, ["L", "R"]) + g.append_move(g.root.children[0].children[0], "2", ["A", "B"]) + g.append_move(g.root.children[0].children[1], "2", ["C", "D"]) + g.set_outcome( + g.root.children[0].children[0].children[0], g.add_outcome([-2, 2], label="aLA") + ) + g.set_outcome( + g.root.children[0].children[0].children[1], g.add_outcome([-2, 2], label="aLB") + ) + g.set_outcome( + g.root.children[0].children[1].children[0], g.add_outcome([-2, 2], label="aRC") + ) + g.set_outcome( + g.root.children[0].children[1].children[1], g.add_outcome([-2, 2], label="aRD") + ) + g.set_outcome(g.root.children[1], g.add_outcome([-1, 1], label="b")) + return g + + +def create_one_card_poker_lacking_outcome_efg() -> gbt.Game: + g = gbt.Game.new_tree(players=["Bob", "Alice"], + title="One card poker game, after Myerson (1991)") + g.append_move(g.root, g.players.chance, ["King", "Queen"]) + for node in g.root.children: + g.append_move(node, "Alice", ["Raise", "Fold"]) + g.append_move(g.root.children[0].children[0], "Bob", ["Meet", "Pass"]) + g.append_infoset(g.root.children[1].children[0], + g.root.children[0].children[0].infoset) + alice_winsbig = g.add_outcome([-1, 1], label="Alice wins big") + bob_winsbig = g.add_outcome([3, -3], label="Bob wins big") + bob_wins = g.add_outcome([2, -2], label="Bob wins") + g.set_outcome(g.root.children[0].children[0].children[0], alice_winsbig) + g.set_outcome(g.root.children[0].children[1], bob_wins) + g.set_outcome(g.root.children[1].children[0].children[0], bob_winsbig) + g.set_outcome(g.root.children[1].children[1], bob_wins) + return g + + +def create_perfect_info_internal_outcomes_efg() -> gbt.Game: + g = gbt.Game.new_tree(players=["1", "2"], title="2 player perfect info win lose") + g.append_move(g.root, "2", ["a", "b"]) + g.append_move(g.root.children[0], "1", ["L", "R"]) + g.append_move(g.root.children[1], "1", ["L", "R"]) + g.append_move(g.root.children[0].children[0], "2", ["l", "r"]) + g.set_outcome(g.root.children[0], g.add_outcome([-100, 50], label="a")) + g.set_outcome( + g.root.children[0].children[0].children[0], g.add_outcome([101, -51], label="aLl") + ) + g.set_outcome( + g.root.children[0].children[0].children[1], g.add_outcome([99, -49], label="aLr") + ) + g.set_outcome(g.root.children[0].children[1], g.add_outcome([101, -51], label="aR")) + g.set_outcome(g.root.children[1].children[0], g.add_outcome([1, -1], label="bL")) + g.set_outcome(g.root.children[1].children[1], g.add_outcome([-1, 1], label="bR")) + return g + + +def create_three_action_internal_outcomes_efg() -> gbt.Game: + # Test 3 actions at infoset, internal outcomes and missing some outcomes at leaves + g = gbt.Game.new_tree(players=["1", "2"], + title="3 action, internal outcomes, lacking terminal outcomes") + g.append_move(g.root, g.players.chance, ["H", "L"]) + for i in range(2): + g.append_move(g.root.children[i], "1", ["A", "B", "C"]) + for i in range(3): + g.append_move(g.root.children[0].children[i], "2", ["X", "Y"]) + g.append_infoset(g.root.children[1].children[i], g.root.children[0].children[i].infoset) + o_1 = g.add_outcome([1, -1], label="1") + o_m1 = g.add_outcome([-1, 1], label="-1") + o_2 = g.add_outcome([2, -2], label="2") + o_m2 = g.add_outcome([-2, 2], label="-2") + g.set_outcome(g.root.children[0].children[0], o_1) + g.set_outcome(g.root.children[1].children[2], o_m1) + g.set_outcome(g.root.children[0].children[0].children[1], o_m2) + g.set_outcome(g.root.children[0].children[1].children[0], o_m1) + g.set_outcome(g.root.children[0].children[1].children[1], o_1) + g.set_outcome(g.root.children[0].children[2].children[0], o_1) + g.set_outcome(g.root.children[1].children[0].children[1], o_1) + g.set_outcome(g.root.children[1].children[1].children[0], o_1) + g.set_outcome(g.root.children[1].children[1].children[1], o_m1) + g.set_outcome(g.root.children[1].children[2].children[1], o_2) + return g + + +def create_entry_accomodation_efg() -> gbt.Game: + g = gbt.Game.new_tree(players=["1", "2"], + title="Entry-accomodation game with internal outcomes") + g.append_move(g.root, "1", ["S", "T"]) + g.append_move(g.root.children[0], "2", ["E", "O"]) + g.append_infoset(g.root.children[1], g.root.children[0].infoset) + g.append_move(g.root.children[0].children[0], "1", ["A", "F"]) + g.append_move(g.root.children[1].children[0], "1", ["A", "F"]) + g.set_outcome(g.root.children[0], g.add_outcome([3, 2])) + g.set_outcome(g.root.children[0].children[0].children[1], g.add_outcome([-3, -1])) + g.set_outcome(g.root.children[0].children[1], g.add_outcome([-2, 1])) + g.set_outcome(g.root.children[1].children[0].children[0], g.add_outcome([2, 3])) + g.set_outcome(g.root.children[1].children[0].children[1], g.add_outcome([1, 0])) + g.set_outcome(g.root.children[1].children[1], g.add_outcome([3, 1])) + return g + + +def create_non_zero_sum_lacking_outcome_efg() -> gbt.Game: + g = gbt.Game.new_tree(players=["1", "2"], title="Non constant-sum game lacking outcome") + g.append_move(g.root, g.players.chance, ["H", "T"]) + g.set_chance_probs(g.root.infoset, ["1/2", "1/2"]) + g.append_move(g.root.children[0], "1", ["A", "B"]) + g.append_infoset(g.root.children[1], g.root.children[0].infoset) + g.append_move(g.root.children[0].children[0], "2", ["X", "Y"]) + g.append_infoset(g.root.children[0].children[1], g.root.children[0].children[0].infoset) + g.append_infoset(g.root.children[1].children[0], g.root.children[0].children[0].infoset) + g.append_infoset(g.root.children[1].children[1], g.root.children[0].children[0].infoset) + g.set_outcome(g.root.children[0].children[0].children[0], g.add_outcome([2, 1])) + g.set_outcome(g.root.children[0].children[0].children[1], g.add_outcome([-1, 2])) + g.set_outcome(g.root.children[0].children[1].children[0], g.add_outcome([1, -1])) + g.set_outcome(g.root.children[1].children[0].children[0], g.add_outcome([1, 0])) + g.set_outcome(g.root.children[1].children[0].children[1], g.add_outcome([0, 1])) + g.set_outcome(g.root.children[1].children[1].children[0], g.add_outcome([-1, 1])) + g.set_outcome(g.root.children[1].children[1].children[1], g.add_outcome([2, -1])) + return g + + +def create_chance_in_middle_efg() -> gbt.Game: + g = gbt.Game.new_tree(players=["1", "2"], + title="Chance in middle game") + g.append_move(g.root, "1", ["A", "B"]) + g.append_move(g.root.children[0], g.players.chance, ["H", "L"]) + g.set_chance_probs(g.root.children[0].infoset, ["1/5", "4/5"]) + g.append_move(g.root.children[1], g.players.chance, ["H", "L"]) + g.set_chance_probs(g.root.children[1].infoset, ["7/10", "3/10"]) + g.set_outcome(g.root.children[0].children[0], g.add_outcome([-1, 1], label="a")) + for i in range(2): + g.append_move(g.root.children[0].children[i], "2", ["X", "Y"]) + ist = g.root.children[0].children[i].infoset + g.append_infoset(g.root.children[1].children[i], ist) + for i in range(2): + for j in range(2): + g.append_move(g.root.children[i].children[0].children[j], "1", ["C", "D"]) + ist = g.root.children[i].children[0].children[j].infoset + g.append_infoset(g.root.children[i].children[1].children[j], ist) + o_1 = g.add_outcome([1, -1], label="1") + o_m1 = g.add_outcome([-1, 1], label="-1") + o_h = g.add_outcome(["1/2", "-1/2"], label="0.5") + o_mh = g.add_outcome(["-1/2", "1/2"], label="-0.5") + g.set_outcome(g.root.children[0].children[0].children[0].children[0], o_1) + g.set_outcome(g.root.children[0].children[0].children[0].children[1], o_m1) + g.set_outcome(g.root.children[0].children[0].children[1].children[0], o_h) + g.set_outcome(g.root.children[0].children[0].children[1].children[1], o_mh) + g.set_outcome(g.root.children[0].children[1].children[0].children[0], o_h) + g.set_outcome(g.root.children[0].children[1].children[0].children[1], o_mh) + g.set_outcome(g.root.children[0].children[1].children[1].children[0], o_1) + g.set_outcome(g.root.children[0].children[1].children[1].children[1], o_m1) + g.set_outcome(g.root.children[1].children[0].children[0].children[0], o_h) + g.set_outcome(g.root.children[1].children[0].children[0].children[1], o_mh) + g.set_outcome(g.root.children[1].children[0].children[1].children[0], o_1) + g.set_outcome(g.root.children[1].children[0].children[1].children[1], o_m1) + g.set_outcome(g.root.children[1].children[1].children[0].children[0], o_1) + g.set_outcome(g.root.children[1].children[1].children[0].children[1], o_m1) + g.set_outcome(g.root.children[1].children[1].children[1].children[0], o_h) + g.set_outcome(g.root.children[1].children[1].children[1].children[1], o_mh) + return g + + +def create_large_payoff_game_efg() -> gbt.Game: + g = gbt.Game.new_tree(players=["1", "2"], title="Large payoff game") + g.append_move(g.root, g.players.chance, ["L", "R"]) + for i in range(2): + g.append_move(g.root.children[i], "1", ["A", "B"]) + for i in range(2): + g.append_move(g.root.children[0].children[i], "2", ["X", "Y"]) + g.append_infoset(g.root.children[1].children[i], g.root.children[0].children[i].infoset) + o_large = g.add_outcome([10000000000000000000, -10000000000000000000], label="large payoff") + o_1 = g.add_outcome([1, -1], label="1") + o_m1 = g.add_outcome([-1, 1], label="-1") + o_zero = g.add_outcome([0, 0], label="0") + g.set_outcome(g.root.children[0].children[0].children[0], o_large) + g.set_outcome(g.root.children[0].children[0].children[1], o_1) + g.set_outcome(g.root.children[0].children[1].children[0], o_m1) + g.set_outcome(g.root.children[0].children[1].children[1], o_zero) + g.set_outcome(g.root.children[1].children[0].children[0], o_m1) + g.set_outcome(g.root.children[1].children[0].children[1], o_1) + g.set_outcome(g.root.children[1].children[1].children[0], o_zero) + g.set_outcome(g.root.children[1].children[1].children[1], o_large) + return g + + +def create_3_player_with_internal_outcomes_efg() -> gbt.Game: + g = gbt.Game.new_tree(players=["1", "2", "3"], title="3 player game with internal outcomes") + g.append_move(g.root, g.players.chance, ["H", "T"]) + g.set_chance_probs(g.root.infoset, ["1/2", "1/2"]) + g.append_move(g.root.children[0], "1", ["a", "b"]) + g.append_move(g.root.children[1], "1", ["c", "d"]) + g.append_move(g.root.children[0].children[0], "2", ["A", "B"]) + g.append_infoset(g.root.children[1].children[0], g.root.children[0].children[0].infoset) + g.append_move(g.root.children[0].children[1], "3", ["W", "X"]) + g.append_infoset(g.root.children[1].children[1], g.root.children[0].children[1].infoset) + g.append_move(g.root.children[0].children[0].children[0], "3", ["Y", "Z"]) + iset = g.root.children[0].children[0].children[0].infoset + g.append_infoset(g.root.children[0].children[0].children[1], iset) + g.append_move(g.root.children[0].children[1].children[1], "2", ["C", "D"]) + o = g.add_outcome([1, 2, 3]) + g.set_outcome(g.root.children[1], o) + o = g.add_outcome([3, 1, 4]) + g.set_outcome(g.root.children[0].children[0].children[0].children[0], o) + o = g.add_outcome([4, 0, 1]) + g.set_outcome(g.root.children[0].children[0].children[0].children[1], o) + o = g.add_outcome([1, 0, 1]) + g.set_outcome(g.root.children[1].children[0].children[0], o) + o = g.add_outcome([2, -1, -2]) + g.set_outcome(g.root.children[1].children[0].children[1], o) + o = g.add_outcome([1, 3, 2]) + g.set_outcome(g.root.children[0].children[1].children[0], o) + o = g.add_outcome([2, 4, 1]) + g.set_outcome(g.root.children[0].children[1].children[1].children[0], o) + o = g.add_outcome([4, 1, 3]) + g.set_outcome(g.root.children[0].children[1].children[1].children[1], o) + o = g.add_outcome([-1, 2, -1]) + g.set_outcome(g.root.children[1].children[1].children[0], o) + return g + + def create_matching_pennies_efg(with_neutral_outcome: bool = False) -> gbt.Game: """ The version with_neutral_outcome adds a (0,0) payoff outcomes at a non-terminal node. diff --git a/tests/test_nash.py b/tests/test_nash.py index b436b79a9..4143f3f17 100644 --- a/tests/test_nash.py +++ b/tests/test_nash.py @@ -135,6 +135,29 @@ def test_enummixed_rational(game: gbt.Game, mixed_strategy_prof_data: list): # ], # 2, # 9 in total found by enumpoly (see unordered test) # ), + ( + games.create_3_player_with_internal_outcomes_efg(), + [ + [[[1.0, 0.0], [1.0, 0.0]], [[1.0, 0.0], [0.5, 0.5]], [[0.0, 1.0], [1.0, 0.0]]], + [[[1.0, 0.0], [1.0, 0.0]], [[1.0, 0.0], [0.0, 1.0]], + [[0.3333333333333333, 0.6666666666666667], [1.0, 0.0]]]], + 2, + ), + ( + games.create_entry_accomodation_efg(), + [ + [[[0.6666666666666666, 0.33333333333333337], [1.0, 0.0], [1.0, 0.0]], + [[0.6666666666666666, 0.33333333333333337]]], + [[[0.0, 1.0], [0.0, 0.0], [0.3333333333333333, 0.6666666666666667]], [[0.0, 1.0]]], + [[[0.0, 1.0], [0.0, 0.0], [1.0, 0.0]], [[1.0, 0.0]]], + [[[0.0, 1.0], [0.0, 0.0], [0.0, 0.0]], [[0.0, 1.0]]]], + 4, + ), + ( + games.create_non_zero_sum_lacking_outcome_efg(), + [[[[0.33333333333333337, 0.6666666666666666]], [[0.5, 0.5]]]], + 1, + ), ], ) def test_enumpoly_ordered_behavior( @@ -350,21 +373,18 @@ def test_lcp_behavior_double(): ), pytest.param( games.create_2x2_zero_sum_efg(missing_term_outcome=True), - [[["1/2", "1/2"]], [["1/2", "1/2"]]], - marks=pytest.mark.xfail(reason="Problem with missing terminal outcome in LP/LCP") + [[["1/2", "1/2"]], [["1/2", "1/2"]]] ), (games.create_matching_pennies_efg(), [[["1/2", "1/2"]], [["1/2", "1/2"]]]), pytest.param( games.create_matching_pennies_efg(with_neutral_outcome=True), - [[["1/2", "1/2"]], [["1/2", "1/2"]]], - marks=pytest.mark.xfail(reason="Problem with nonterminal nodes in LP/LCP") + [[["1/2", "1/2"]], [["1/2", "1/2"]]] ), (games.create_stripped_down_poker_efg(), [[[1, 0], ["1/3", "2/3"]], [["2/3", "1/3"]]]), pytest.param( games.create_stripped_down_poker_efg(nonterm_outcomes=True), - [[[1, 0], ["1/3", "2/3"]], [["2/3", "1/3"]]], - marks=pytest.mark.xfail(reason="Problem with missing terminal outcome in LP/LCP") + [[[1, 0], ["1/3", "2/3"]], [["2/3", "1/3"]]] ), ( games.create_kuhn_poker_efg(), @@ -392,8 +412,7 @@ def test_lcp_behavior_double(): ["1/2", "1/2"], ], [[1, 0], ["2/3", "1/3"], [0, 1], [0, 1], ["2/3", "1/3"], [1, 0]], - ], - marks=pytest.mark.xfail(reason="Problem with missing terminal outcome in LP/LCP") + ] ), # In the next test case: # 1/2-1/2 for l/r is determined by MixedBehaviorProfile.UndefinedToCentroid() @@ -401,6 +420,39 @@ def test_lcp_behavior_double(): games.create_two_player_perfect_info_win_lose_efg(), [[[0, 1], [1, 0]], [[0, 1], ["1/2", "1/2"]]], ), + ( + games.create_perfect_info_with_chance_efg(), + [[[0, 1]], [[0, 1], [0, 1]]], + ), + ( + games.create_one_card_poker_lacking_outcome_efg(), + [[["2/3", "1/3"]], [[1, 0], ["1/3", "2/3"]]], + ), + ( + games.create_perfect_info_internal_outcomes_efg(), + [[[0, 1], [1, 0]], [[0, 1], ["1/2", "1/2"]]], + ), + ( + games.create_three_action_internal_outcomes_efg(), + [ + [["1/3", 0, "2/3"], ["2/3", 0, "1/3"]], + [["2/3", "1/3"], ["1/3", "2/3"], ["1/3", "2/3"]], + ], + ), + ( + games.create_large_payoff_game_efg(), + [ + [[1, 0], [1, 0]], + [[0, 1], ["9999999999999999999/10000000000000000000", "1/10000000000000000000"]], + ], + ), + ( + games.create_chance_in_middle_efg(), + [ + [["3/11", "8/11"], [1, 0], [1, 0], [1, 0], [1, 0]], + [[1, 0], ["6/11", "5/11"]] + ], + ), # Non-zero-sum games ( games.create_reduction_both_players_payoff_ties_efg(), @@ -418,6 +470,14 @@ def test_lcp_behavior_double(): games.create_EFG_for_nxn_bimatrix_coordination_game(4), [[[0, 0, 0, 1]], [[0, 0, 0, 1]]], ), + ( + games.create_entry_accomodation_efg(), + [[["2/3", "1/3"], [1, 0], [1, 0]], [["2/3", "1/3"]]], + ), + ( + games.create_non_zero_sum_lacking_outcome_efg(), + [[["1/3", "2/3"]], [["1/2", "1/2"]]], + ), ], ) def test_lcp_behavior_rational(game: gbt.Game, mixed_behav_prof_data: list): @@ -500,15 +560,13 @@ def test_lp_behavior_double(): ), pytest.param( games.create_2x2_zero_sum_efg(missing_term_outcome=True), - [[["1/2", "1/2"]], [["1/2", "1/2"]]], - marks=pytest.mark.xfail(reason="Problem with missing terminal outcome in LP/LCP") + [[["1/2", "1/2"]], [["1/2", "1/2"]]] ), (games.create_matching_pennies_efg(with_neutral_outcome=False), [[["1/2", "1/2"]], [["1/2", "1/2"]]]), pytest.param( games.create_matching_pennies_efg(with_neutral_outcome=True), - [[["1/2", "1/2"]], [["1/2", "1/2"]]], - marks=pytest.mark.xfail(reason="Problem with nonterminal nodes in LP/LCP") + [[["1/2", "1/2"]], [["1/2", "1/2"]]] ), ( games.create_stripped_down_poker_efg(), @@ -516,8 +574,7 @@ def test_lp_behavior_double(): ), pytest.param( games.create_stripped_down_poker_efg(nonterm_outcomes=True), - [[[1, 0], ["1/3", "2/3"]], [["2/3", "1/3"]]], - marks=pytest.mark.xfail(reason="Problem with nonterminal nodes in LP/LCP") + [[[1, 0], ["1/3", "2/3"]], [["2/3", "1/3"]]] ), ( games.create_kuhn_poker_efg(), @@ -530,16 +587,15 @@ def test_lp_behavior_double(): games.create_kuhn_poker_efg(nonterm_outcomes=True), [ [ - ["2/3", "1/3"], [1, 0], [1, 0], - ["1/3", "2/3"], + [1, 0], + ["2/3", "1/3"], + [1, 0], [0, 1], - ["1/2", "1/2"], ], [[1, 0], ["2/3", "1/3"], [0, 1], [0, 1], ["2/3", "1/3"], [1, 0]], - ], - marks=pytest.mark.xfail(reason="Problem with nonterminal nodes in LP/LCP") + ] ), ( games.create_seq_form_STOC_paper_zero_sum_2_player_efg(), @@ -548,6 +604,39 @@ def test_lp_behavior_double(): [["5/6", "1/6"], ["5/9", "4/9"]], ], ), + ( + games.create_perfect_info_with_chance_efg(), + [[[0, 1]], [[1, 0], [1, 0]]], + ), + ( + games.create_one_card_poker_lacking_outcome_efg(), + [[["2/3", "1/3"]], [[1, 0], ["1/3", "2/3"]]], + ), + ( + games.create_perfect_info_internal_outcomes_efg(), + [[[0, 1], [1, 0]], [[1, 0], [1, 0]]], + ), + ( + games.create_three_action_internal_outcomes_efg(), + [ + [["1/3", 0, "2/3"], ["2/3", 0, "1/3"]], + [["2/3", "1/3"], ["2/3", "1/3"], ["1/3", "2/3"]], + ], + ), + ( + games.create_large_payoff_game_efg(), + [ + [[1, 0], [1, 0]], + [[0, 1], ["9999999999999999999/10000000000000000000", "1/10000000000000000000"]], + ], + ), + ( + games.create_chance_in_middle_efg(), + [ + [["3/11", "8/11"], [1, 0], [1, 0], [1, 0], [1, 0]], + [[1, 0], ["6/11", "5/11"]] + ], + ), ], ) def test_lp_behavior_rational(game: gbt.Game, mixed_behav_prof_data: list):