diff --git a/Makefile.am b/Makefile.am index feb6664ed..e55a43a9b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -406,9 +406,6 @@ gambit_enumpoly_SOURCES = \ src/solvers/enumpoly/polypartial.imp \ src/solvers/enumpoly/polysolver.cc \ src/solvers/enumpoly/polysolver.h \ - src/solvers/enumpoly/polyfeasible.h \ - src/solvers/enumpoly/behavextend.cc \ - src/solvers/enumpoly/behavextend.h \ src/solvers/enumpoly/efgpoly.cc \ src/solvers/enumpoly/nfgpoly.cc \ src/solvers/enumpoly/enumpoly.h \ diff --git a/doc/tools.enumpoly.rst b/doc/tools.enumpoly.rst index a04362e98..2e328ec2f 100644 --- a/doc/tools.enumpoly.rst +++ b/doc/tools.enumpoly.rst @@ -24,6 +24,19 @@ supports which have the fewest strategies in total. For many classes of games, this will tend to lower the average time until finding one equilibrium, as well as finding the second equilibrium (if one exists). +For extensive games, a support of actions equates to allowing positive +probabilities over a subset of terminal nodes. The indifference conditions +used are those for the sequence form defined on the projection of the game +to that support of actions. A solution to these equations implies a probability +distribution over terminal nodes. The algorithm then searches for +a profile that is a Nash equilibrium that implements that probability +distribution. If there exists at least one such profile, a sample one is returned. +Note that for probability distributions which assign zero probability to some terminal +nodes, it is generally the case that there are (infinitely) many such profiles. +Subsequent analysis of unreached information sets can yield alternative +profiles that specify different choices at unreached information sets +while still satisfying the Nash equilibrium conditions. + When the verbose switch `-v` is used, the program outputs each support as it is considered. The supports are presented as a comma-separated list of binary strings, where each entry represents one player. The diff --git a/src/games/behavmixed.cc b/src/games/behavmixed.cc index 36b0df22c..1a787f9c9 100644 --- a/src/games/behavmixed.cc +++ b/src/games/behavmixed.cc @@ -234,12 +234,12 @@ template MixedBehaviorProfile MixedBehaviorProfile::Normalize() template MixedBehaviorProfile MixedBehaviorProfile::ToFullSupport() const { CheckVersion(); - MixedBehaviorProfile full(GetGame()); + MixedBehaviorProfile full(GetGame()); for (auto player : m_support.GetGame()->GetPlayers()) { for (auto infoset : player->GetInfosets()) { for (auto action : infoset->GetActions()) { - full[action] = (m_support.Contains(action)) ? (*this)[action] : T(0); + full[action] = (m_support.Contains(action)) ? (*this)[action] : T{0}; } } } diff --git a/src/games/behavspt.cc b/src/games/behavspt.cc index e75fdf2d5..84e73363a 100644 --- a/src/games/behavspt.cc +++ b/src/games/behavspt.cc @@ -73,8 +73,10 @@ void BehaviorSupportProfile::AddAction(const GameAction &p_action) if (pos == support.end() || *pos != p_action) { // Action is not in the support at the infoset; add at this location to keep sorted by number support.insert(pos, p_action); - for (const auto &node : GetMembers(p_action->GetInfoset())) { - ActivateSubtree(node->GetChild(p_action)); + for (const auto &node : p_action->GetInfoset()->GetMembers()) { + if (m_nonterminalReachable[node]) { + ActivateSubtree(node->GetChild(p_action)); + } } } } @@ -86,25 +88,16 @@ bool BehaviorSupportProfile::RemoveAction(const GameAction &p_action) auto pos = std::find(support.begin(), support.end(), p_action); if (pos != support.end()) { support.erase(pos); - for (const auto &node : GetMembers(p_action->GetInfoset())) { - DeactivateSubtree(node->GetChild(p_action)); + for (const auto &node : p_action->GetInfoset()->GetMembers()) { + if (m_nonterminalReachable[node]) { + DeactivateSubtree(node->GetChild(p_action)); + } } return !support.empty(); } return false; } -std::list BehaviorSupportProfile::GetInfosets(const GamePlayer &p_player) const -{ - std::list answer; - for (const auto &infoset : p_player->GetInfosets()) { - if (m_infosetReachable.at(infoset)) { - answer.push_back(infoset); - } - } - return answer; -} - bool BehaviorSupportProfile::HasReachableMembers(const GameInfoset &p_infoset) const { const auto &members = p_infoset->GetMembers(); @@ -150,17 +143,6 @@ void BehaviorSupportProfile::DeactivateSubtree(const GameNode &n) } } -std::list BehaviorSupportProfile::GetMembers(const GameInfoset &p_infoset) const -{ - std::list answer; - for (const auto &member : p_infoset->GetMembers()) { - if (m_nonterminalReachable.at(member)) { - answer.push_back(member); - } - } - return answer; -} - //======================================================================== // BehaviorSupportProfile: Sequence form //======================================================================== diff --git a/src/games/behavspt.h b/src/games/behavspt.h index b75013d86..2b4a6d08e 100644 --- a/src/games/behavspt.h +++ b/src/games/behavspt.h @@ -130,20 +130,6 @@ class BehaviorSupportProfile { //@{ /// Can the information set be reached under this support? bool IsReachable(const GameInfoset &p_infoset) const { return m_infosetReachable.at(p_infoset); } - /// Get the information sets for the player reachable under the support - std::list GetInfosets(const GamePlayer &) const; - /// Get the members of the information set reachable under the support - std::list GetMembers(const GameInfoset &) const; - //@} - - /// @name Identification of dominated actions - //@{ - /// Returns true if action 'a' is dominated by action 'b' - bool Dominates(const GameAction &a, const GameAction &b, bool p_strict) const; - /// Returns true if the action is dominated by some other action - bool IsDominated(const GameAction &a, bool p_strict) const; - /// Returns a copy of the support with dominated actions eliminated - BehaviorSupportProfile Undominated(bool p_strict) const; //@} class Infosets { diff --git a/src/solvers/enumpoly/behavextend.cc b/src/solvers/enumpoly/behavextend.cc deleted file mode 100644 index 601973ab7..000000000 --- a/src/solvers/enumpoly/behavextend.cc +++ /dev/null @@ -1,454 +0,0 @@ -// -// This file is part of Gambit -// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org) -// -// FILE: src/solver/enumpoly/behavextend.cc -// Algorithms for extending behavior profiles to Nash equilibria -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -// - -#include "behavextend.h" -#include "polysystem.h" -#include "polyfeasible.h" - -namespace { - -using namespace Gambit; - -void TerminalDescendants(const GameNode &p_node, std::list ¤t) -{ - if (p_node->IsTerminal()) { - current.push_back(p_node); - } - else { - for (auto child : p_node->GetChildren()) { - TerminalDescendants(child, current); - } - } -} - -std::list TerminalNodes(const Game &p_efg) -{ - std::list ret; - TerminalDescendants(p_efg->GetRoot(), ret); - return ret; -} - -void DeviationInfosets(Array &answer, const BehaviorSupportProfile &big_supp, - const GamePlayer &p_player, const GameNode &p_node, - const GameAction &p_action) -{ - const GameNode child = p_node->GetChild(p_action); - if (child->IsTerminal()) { - return; - } - const GameInfoset iset = child->GetInfoset(); - if (iset->GetPlayer() == p_player) { - size_t insert = 0; - bool done = false; - while (!done) { - insert++; - if (insert > answer.size() || iset->Precedes(answer[insert]->GetMember(1))) { - done = true; - } - } - answer.insert(std::next(answer.begin(), insert - 1), iset); - } - - for (auto action : iset->GetActions()) { - DeviationInfosets(answer, big_supp, p_player, child, action); - } -} - -Array DeviationInfosets(const BehaviorSupportProfile &big_supp, - const GamePlayer &p_player, const GameInfoset &p_infoset, - const GameAction &p_action) -{ - Array answer; - for (auto member : p_infoset->GetMembers()) { - DeviationInfosets(answer, big_supp, p_player, member, p_action); - } - return answer; -} - -PolynomialSystem ActionProbsSumToOneIneqs(const MixedBehaviorProfile &p_solution, - std::shared_ptr BehavStratSpace, - const BehaviorSupportProfile &big_supp, - const std::map &var_index) -{ - PolynomialSystem answer(BehavStratSpace); - - for (auto player : p_solution.GetGame()->GetPlayers()) { - for (auto infoset : player->GetInfosets()) { - if (!big_supp.HasAction(infoset)) { - const int index_base = var_index.at(infoset); - Polynomial factor(BehavStratSpace, 1.0); - for (size_t k = 1; k < infoset->GetActions().size(); k++) { - factor -= Polynomial(BehavStratSpace, index_base + k, 1); - } - answer.push_back(factor); - } - } - } - return answer; -} - -std::list DeviationSupports(const BehaviorSupportProfile &big_supp, - const Array &isetlist) -{ - std::list answer; - - Array active_act_no(isetlist.size()); - std::fill(active_act_no.begin(), active_act_no.end(), 0); - - BehaviorSupportProfile new_supp(big_supp); - - for (size_t i = 1; i <= isetlist.size(); i++) { - for (size_t j = 1; j < isetlist[i]->GetActions().size(); j++) { - new_supp.RemoveAction(isetlist[i]->GetAction(j)); - } - new_supp.AddAction(isetlist[i]->GetAction(1)); - - active_act_no[i] = 1; - for (size_t k = 1; k < i; k++) { - if (isetlist[k]->Precedes(isetlist[i]->GetMember(1))) { - if (isetlist[k]->GetAction(1)->Precedes(isetlist[i]->GetMember(1))) { - new_supp.RemoveAction(isetlist[i]->GetAction(1)); - active_act_no[i] = 0; - } - } - } - } - answer.push_back(new_supp); - - size_t iset_cursor = isetlist.size(); - while (iset_cursor > 0) { - if (active_act_no[iset_cursor] == 0 || - active_act_no[iset_cursor] == - static_cast(isetlist[iset_cursor]->GetActions().size())) { - iset_cursor--; - } - else { - new_supp.RemoveAction(isetlist[iset_cursor]->GetAction(active_act_no[iset_cursor])); - active_act_no[iset_cursor]++; - new_supp.AddAction(isetlist[iset_cursor]->GetAction(active_act_no[iset_cursor])); - for (size_t k = iset_cursor + 1; k <= isetlist.size(); k++) { - if (active_act_no[k] > 0) { - new_supp.RemoveAction(isetlist[k]->GetAction(1)); - } - size_t h = 1; - bool active = true; - while (active && h < k) { - if (isetlist[h]->Precedes(isetlist[k]->GetMember(1))) { - if (active_act_no[h] == 0 || - !isetlist[h]->GetAction(active_act_no[h])->Precedes(isetlist[k]->GetMember(1))) { - active = false; - if (active_act_no[k] > 0) { - new_supp.RemoveAction(isetlist[k]->GetAction(active_act_no[k])); - active_act_no[k] = 0; - } - } - } - h++; - } - if (active) { - new_supp.AddAction(isetlist[k]->GetAction(1)); - active_act_no[k] = 1; - } - } - answer.push_back(new_supp); - } - } - return answer; -} - -bool NashNodeProbabilityPoly(const MixedBehaviorProfile &p_solution, - Polynomial &node_prob, - std::shared_ptr BehavStratSpace, - const BehaviorSupportProfile &dsupp, - const std::map &var_index, GameNode tempnode, - const GameInfoset &iset, const GameAction &act) -{ - while (tempnode != p_solution.GetGame()->GetRoot()) { - const GameAction last_action = tempnode->GetPriorAction(); - const GameInfoset last_infoset = last_action->GetInfoset(); - - if (last_infoset->IsChanceInfoset()) { - node_prob *= static_cast(last_infoset->GetActionProb(last_action)); - } - else if (dsupp.HasAction(last_infoset)) { - if (last_infoset == iset) { - if (act != last_action) { - return false; - } - } - else if (dsupp.Contains(last_action)) { - if (last_action->GetInfoset()->GetPlayer() != act->GetInfoset()->GetPlayer() || - !act->Precedes(tempnode)) { - node_prob *= p_solution.GetActionProb(last_action); - } - } - else { - return false; - } - } - else { - const int initial_var_no = var_index.at(last_infoset); - if (last_action != last_infoset->GetActions().back()) { - const int varno = initial_var_no + last_action->GetNumber(); - node_prob *= Polynomial(BehavStratSpace, varno, 1); - } - else { - Polynomial factor(BehavStratSpace, 1.0); - for (size_t k = 1; k < last_infoset->GetActions().size(); k++) { - factor -= Polynomial(BehavStratSpace, initial_var_no + k, 1); - } - node_prob *= factor; - } - } - tempnode = tempnode->GetParent(); - } - return true; -} - -PolynomialSystem NashExpectedPayoffDiffPolys( - const MixedBehaviorProfile &p_solution, std::shared_ptr BehavStratSpace, - const BehaviorSupportProfile &little_supp, const BehaviorSupportProfile &big_supp, - const std::map &var_index) -{ - PolynomialSystem answer(BehavStratSpace); - - auto terminal_nodes = TerminalNodes(p_solution.GetGame()); - - for (auto player : p_solution.GetGame()->GetPlayers()) { - for (auto infoset : player->GetInfosets()) { - if (!little_supp.IsReachable(infoset)) { - continue; - } - for (auto action : infoset->GetActions()) { - if (little_supp.Contains(action)) { - continue; - } - auto isetlist = DeviationInfosets(big_supp, player, infoset, action); - auto dsupps = DeviationSupports(big_supp, isetlist); - for (auto support : dsupps) { - // The utility difference between the - // payoff resulting from the profile and deviation to - // the strategy for pl specified by dsupp[k] - Polynomial next_poly(BehavStratSpace); - - for (auto node : terminal_nodes) { - Polynomial node_prob(BehavStratSpace, 1.0); - if (NashNodeProbabilityPoly(p_solution, node_prob, BehavStratSpace, support, var_index, - node, infoset, action)) { - if (node->GetOutcome()) { - node_prob *= node->GetOutcome()->GetPayoff(player); - } - next_poly += node_prob; - } - } - answer.push_back(-next_poly + p_solution.GetPayoff(player)); - } - } - } - } - return answer; -} - -PolynomialSystem ExtendsToNashIneqs(const MixedBehaviorProfile &p_solution, - std::shared_ptr BehavStratSpace, - const BehaviorSupportProfile &little_supp, - const BehaviorSupportProfile &big_supp, - const std::map &var_index) -{ - PolynomialSystem answer(BehavStratSpace); - answer.push_back(ActionProbsSumToOneIneqs(p_solution, BehavStratSpace, big_supp, var_index)); - answer.push_back( - NashExpectedPayoffDiffPolys(p_solution, BehavStratSpace, little_supp, big_supp, var_index)); - return answer; -} - -} // end anonymous namespace - -namespace Gambit::Nash { - -bool ExtendsToNash(const MixedBehaviorProfile &p_solution, - const BehaviorSupportProfile &little_supp, - const BehaviorSupportProfile &big_supp) -{ - - // First we compute the number of variables, and indexing information - int num_vars = 0; - std::map var_index; - for (auto player : p_solution.GetGame()->GetPlayers()) { - for (auto infoset : player->GetInfosets()) { - var_index[infoset] = num_vars; - if (!big_supp.HasAction(infoset)) { - num_vars += infoset->GetActions().size() - 1; - } - } - } - - // We establish the space - auto BehavStratSpace = std::make_shared(num_vars); - - const PolynomialSystem inequalities = - ExtendsToNashIneqs(p_solution, BehavStratSpace, little_supp, big_supp, var_index); - // set up the rectangle of search - Vector bottoms(num_vars), tops(num_vars); - bottoms = 0; - tops = 1; - return PolynomialFeasibilitySolver(inequalities).HasSolution(Rectangle(bottoms, tops)); -} - -} // end namespace Gambit::Nash - -namespace { - -bool ANFNodeProbabilityPoly(const MixedBehaviorProfile &p_solution, - Polynomial &node_prob, - std::shared_ptr BehavStratSpace, - const BehaviorSupportProfile &big_supp, - const std::map &var_index, GameNode tempnode, int pl, - int i, int j) -{ - while (tempnode != p_solution.GetGame()->GetRoot()) { - const GameAction last_action = tempnode->GetPriorAction(); - const GameInfoset last_infoset = last_action->GetInfoset(); - - if (last_infoset->IsChanceInfoset()) { - node_prob *= static_cast(last_infoset->GetActionProb(last_action)); - } - else if (big_supp.HasAction(last_infoset)) { - if (last_infoset == p_solution.GetGame()->GetPlayer(pl)->GetInfoset(i)) { - if (j != last_action->GetNumber()) { - return false; - } - } - else if (big_supp.Contains(last_action)) { - node_prob *= p_solution.GetActionProb(last_action); - } - else { - return false; - } - } - else { - const int initial_var_no = var_index.at(last_infoset); - if (last_action != last_infoset->GetActions().back()) { - const int varno = initial_var_no + last_action->GetNumber(); - node_prob *= Polynomial(BehavStratSpace, varno, 1); - } - else { - Polynomial factor(BehavStratSpace, 1.0); - for (size_t k = 1; k < last_infoset->GetActions().size(); k++) { - factor -= Polynomial(BehavStratSpace, initial_var_no + k, 1); - } - node_prob *= factor; - } - } - tempnode = tempnode->GetParent(); - } - return true; -} - -PolynomialSystem ANFExpectedPayoffDiffPolys(const MixedBehaviorProfile &p_solution, - std::shared_ptr BehavStratSpace, - const BehaviorSupportProfile &little_supp, - const BehaviorSupportProfile &big_supp, - const std::map &var_index) -{ - PolynomialSystem answer(BehavStratSpace); - - auto terminal_nodes = TerminalNodes(p_solution.GetGame()); - - for (auto player : p_solution.GetGame()->GetPlayers()) { - for (auto infoset : player->GetInfosets()) { - if (!little_supp.IsReachable(infoset)) { - continue; - } - for (auto action : infoset->GetActions()) { - if (little_supp.Contains(action)) { - continue; - } - // This will be the utility difference between the - // payoff resulting from the profile and deviation to - // action j - Polynomial next_poly(BehavStratSpace); - for (auto terminal : terminal_nodes) { - Polynomial node_prob(BehavStratSpace, 1.0); - if (ANFNodeProbabilityPoly(p_solution, node_prob, BehavStratSpace, big_supp, var_index, - terminal, player->GetNumber(), infoset->GetNumber(), - action->GetNumber())) { - if (terminal->GetOutcome()) { - node_prob *= terminal->GetOutcome()->GetPayoff(player); - } - next_poly += node_prob; - } - } - answer.push_back(-next_poly + p_solution.GetPayoff(player)); - } - } - } - return answer; -} - -PolynomialSystem ExtendsToANFNashIneqs(const MixedBehaviorProfile &p_solution, - std::shared_ptr BehavStratSpace, - const BehaviorSupportProfile &little_supp, - const BehaviorSupportProfile &big_supp, - const std::map &var_index) -{ - PolynomialSystem answer(BehavStratSpace); - answer.push_back(ActionProbsSumToOneIneqs(p_solution, BehavStratSpace, big_supp, var_index)); - answer.push_back( - ANFExpectedPayoffDiffPolys(p_solution, BehavStratSpace, little_supp, big_supp, var_index)); - return answer; -} - -} // end anonymous namespace - -namespace Gambit::Nash { - -bool ExtendsToAgentNash(const MixedBehaviorProfile &p_solution, - const BehaviorSupportProfile &little_supp, - const BehaviorSupportProfile &big_supp) -{ - // First we compute the number of variables, and indexing information - int num_vars = 0; - std::map var_index; - for (auto player : p_solution.GetGame()->GetPlayers()) { - for (auto infoset : player->GetInfosets()) { - var_index[infoset] = num_vars; - if (!big_supp.HasAction(infoset)) { - num_vars += infoset->GetActions().size() - 1; - } - } - } - - // We establish the space - auto BehavStratSpace = std::make_shared(num_vars); - const PolynomialSystem inequalities = - ExtendsToANFNashIneqs(p_solution, BehavStratSpace, little_supp, big_supp, var_index); - - // set up the rectangle of search - Vector bottoms(num_vars), tops(num_vars); - bottoms = 0; - tops = 1; - - return PolynomialFeasibilitySolver(inequalities).HasSolution(Rectangle(bottoms, tops)); -} - -} // end namespace Gambit::Nash diff --git a/src/solvers/enumpoly/behavextend.h b/src/solvers/enumpoly/behavextend.h deleted file mode 100644 index 4ad33771d..000000000 --- a/src/solvers/enumpoly/behavextend.h +++ /dev/null @@ -1,51 +0,0 @@ -// -// This file is part of Gambit -// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org) -// -// FILE: src/solvers/enumpoly/behavextend.h -// Algorithms for extending behavior profiles to Nash equilibria -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -// - -#ifndef BEHAVEXTEND_H -#define BEHAVEXTEND_H - -#include "gambit.h" - -namespace Gambit::Nash { - -// This asks whether there is a Nash extension of the MixedBehaviorProfile to -// all information sets at which the behavioral probabilities are not -// specified. The assumption is that the support has active actions -// at infosets at which the behavioral probabilities are defined, and -// no others. Also, the BehavSol is assumed to be already a Nash -// equilibrium for the truncated game obtained by eliminating stuff -// outside little_supp. -bool ExtendsToNash(const MixedBehaviorProfile &p_solution, - const BehaviorSupportProfile &p_littleSupport, - const BehaviorSupportProfile &p_bigSupport); - -// This asks whether there is an ANF Nash extension of the MixedBehaviorProfile -// to all information sets at which the behavioral probabilities are not specified. The -// assumption is that the support has active actions at infosets at which the behavioral -// probabilities are defined, and no others. -bool ExtendsToAgentNash(const MixedBehaviorProfile &p_solution, - const BehaviorSupportProfile &p_littleSupport, - const BehaviorSupportProfile &p_bigSupport); - -} // namespace Gambit::Nash - -#endif // BEHAVEXTEND_H diff --git a/src/solvers/enumpoly/efgpoly.cc b/src/solvers/enumpoly/efgpoly.cc index ad7ada9c2..4956b372f 100644 --- a/src/solvers/enumpoly/efgpoly.cc +++ b/src/solvers/enumpoly/efgpoly.cc @@ -25,7 +25,7 @@ #include "games/gameseq.h" #include "polysystem.h" #include "polysolver.h" -#include "behavextend.h" +#include "indexproduct.h" using namespace Gambit; using namespace Gambit::Nash; @@ -151,8 +151,37 @@ std::map ToSequenceProbs(const ProblemData &p_data, const return x; } +std::optional> +FindNashExtension(const MixedBehaviorProfile &p_baseProfile, double p_maxRegret) +{ + const Game &game = p_baseProfile.GetGame(); + std::list extensionInfosets; + for (const auto &infoset : game->GetInfosets()) { + if (!p_baseProfile.IsDefinedAt(infoset)) { + extensionInfosets.push_back(infoset); + } + } + Array firstIndex(extensionInfosets.size()); + std::fill(firstIndex.begin(), firstIndex.end(), 1); + Array lastIndex(extensionInfosets.size()); + std::transform(extensionInfosets.begin(), extensionInfosets.end(), lastIndex.begin(), + [](const auto &infoset) { return infoset->GetActions().size(); }); + CartesianIndexProduct indices(firstIndex, lastIndex); + for (const auto &index : indices) { + auto extension = p_baseProfile.ToFullSupport(); + for (auto [i, infoset] : enumerate(extensionInfosets)) { + extension[infoset->GetAction(index[i + 1])] = 1.0; + } + if (extension.GetMaxRegret() < p_maxRegret) { + return extension; + } + } + return std::nullopt; +} + std::list> SolveSupport(const BehaviorSupportProfile &p_support, - bool &p_isSingular, int p_stopAfter) + bool &p_isSingular, int p_stopAfter, + double p_maxRegret) { ProblemData data(p_support); PolynomialSystem equations(data.space); @@ -174,17 +203,16 @@ std::list> SolveSupport(const BehaviorSupportProfil p_isSingular = true; } catch (const std::domain_error &) { - // std::cerr << "Assertion warning: " << e.what() << std::endl; p_isSingular = true; } std::list> solutions; - for (auto root : roots) { + for (const auto &root : roots) { const MixedBehaviorProfile sol( data.m_support.ToMixedBehaviorProfile(ToSequenceProbs(data, root))); - if (ExtendsToNash(sol, BehaviorSupportProfile(sol.GetGame()), - BehaviorSupportProfile(sol.GetGame()))) { - solutions.push_back(sol); + auto extended = FindNashExtension(sol, p_maxRegret); + if (extended.has_value()) { + solutions.push_back(extended.value()); } } return solutions; @@ -210,13 +238,11 @@ EnumPolyBehaviorSolve(const Game &p_game, int p_stopAfter, double p_maxregret, for (auto support : possible_supports->m_supports) { p_onSupport("candidate", support); bool isSingular = false; - for (auto solution : - SolveSupport(support, isSingular, std::max(p_stopAfter - int(ret.size()), 0))) { - const MixedBehaviorProfile fullProfile = solution.ToFullSupport(); - if (fullProfile.GetAgentMaxRegret() < p_maxregret) { - p_onEquilibrium(fullProfile); - ret.push_back(fullProfile); - } + for (const auto &solution : + SolveSupport(support, isSingular, std::max(p_stopAfter - static_cast(ret.size()), 0), + p_maxregret)) { + p_onEquilibrium(solution); + ret.push_back(solution); } if (isSingular) { p_onSupport("singular", support); diff --git a/src/solvers/enumpoly/polyfeasible.h b/src/solvers/enumpoly/polyfeasible.h deleted file mode 100644 index c77fe4f64..000000000 --- a/src/solvers/enumpoly/polyfeasible.h +++ /dev/null @@ -1,107 +0,0 @@ -// -// This file is part of Gambit -// Copyright (c) 1994-2026, The Gambit Project (http://www.gambit-project.org) -// -// FILE: src/solvers/enumpoly/polyfeasible.h -// Check feasibility of a system of polynomial inequalities -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -// - -#ifndef POLYFEASIBLE_H -#define POLYFEASIBLE_H - -#include "gambit.h" - -#include "rectangle.h" -#include "polysystem.h" -#include "polypartial.h" - -namespace Gambit { - -/// Determine whether a -/// system of weak inequalities has a solution (a point where all are satisfied) -/// in a given rectangle. Ir is modeled on PolynomialSystemSolver, but simpler. There is -/// no Newton search, only repeated subdivision, queries at the center, and -/// tests against whether one of the inequalities is provably everywhere -/// negative in the rectangle. -/// The constructor for this takes a list of polynomials, interpreted as -/// inequalities in the sense that, at a solution, all the polynomials -/// are required to be non-negative. -class PolynomialFeasibilitySolver { -private: - PolynomialSystem m_system; - PolynomialSystemDerivatives m_systemDerivs; - double m_epsilon{1.0e-6}; - - bool IsASolution(const Vector &v) const - { - return std::all_of(m_system.begin(), m_system.end(), - [&](const Polynomial &p) { return p.Evaluate(v) > -m_epsilon; }); - } - bool SystemHasNoSolutionIn(const Rectangle &r, Array &precedence) const - { - for (int i = 1; i <= m_system.size(); i++) { - if (m_systemDerivs[precedence[i]].PolyEverywhereNegativeIn(r)) { - if (i != 1) { // We have found a new "most likely to never be positive" - const int tmp = precedence[i]; - for (int j = 1; j <= i - 1; j++) { - precedence[i - j + 1] = precedence[i - j]; - } - precedence[1] = tmp; - } - return true; - } - } - return false; - } - - bool SolutionExists(const Rectangle &r, Array &precedence) const - { - if (IsASolution(r.Center())) { - return true; - } - if (SystemHasNoSolutionIn(r, precedence)) { - return false; - } - for (const auto &cell : r.Orthants()) { - if (SolutionExists(cell, precedence)) { - return true; - } - } - return false; - } - -public: - explicit PolynomialFeasibilitySolver(const PolynomialSystem &given) - : m_system(given), m_systemDerivs(given) - { - } - PolynomialFeasibilitySolver(const PolynomialFeasibilitySolver &) = delete; - ~PolynomialFeasibilitySolver() = default; - PolynomialFeasibilitySolver &operator=(const PolynomialFeasibilitySolver &) = delete; - - /// Does a solution exist in the specified rectangle? - bool HasSolution(const Rectangle &r) - { - Array precedence(m_system.size()); - std::iota(precedence.begin(), precedence.end(), 1); - return SolutionExists(r, precedence); - } -}; - -} // namespace Gambit - -#endif // POLYFEASIBLE_H diff --git a/tests/test_games/2_player_PI_2_dev_off_eq_path_const_sum.efg b/tests/test_games/2_player_PI_2_dev_off_eq_path_const_sum.efg new file mode 100644 index 000000000..d2daff2f3 --- /dev/null +++ b/tests/test_games/2_player_PI_2_dev_off_eq_path_const_sum.efg @@ -0,0 +1,13 @@ +EFG 2 R "Untitled Extensive Game" { "Player 1" "Player 2" } +"" + +p "" 1 1 "" { "1" "2" } 0 +p "" 2 1 "" { "1" "2" } 0 +p "" 1 2 "" { "1" "2" "3" "4" "5" } 0 +t "" 1 "" { 1, 0 } +t "" 2 "" { 0, 1 } +t "" 3 "" { 0, 1 } +t "" 4 "" { 0, 1 } +t "" 5 "" { 0, 1 } +t "" 6 "" { 0, 1 } +t "" 7 "" { 1, 0 } diff --git a/tests/test_games/3_player_PI_2_dev_off_eq_path.efg b/tests/test_games/3_player_PI_2_dev_off_eq_path.efg new file mode 100644 index 000000000..f3daeb0c8 --- /dev/null +++ b/tests/test_games/3_player_PI_2_dev_off_eq_path.efg @@ -0,0 +1,17 @@ +EFG 2 R "Untitled Extensive Game" { "Player 1" "Player 2" "Player 3" } +"" + +p "" 1 1 "" { "1" "2" } 0 +p "" 2 1 "" { "1" "2" } 0 +p "" 3 1 "" { "1" "2" "3" "4" "5" } 0 +p "" 2 2 "" { "1" "2" "3" "4" } 0 +t "" 1 "" { 1, 1, 1 } +t "" 2 "" { 0, 0, 1 } +t "" 3 "" { 0, 0, 1 } +t "" 4 "" { 0, 0, 1 } +t "" 5 "" { 0, 0, 0 } +t "" 6 "" { 0, 0, 0 } +t "" 7 "" { 0, 0, 0 } +t "" 8 "" { 0, 0, 0 } +t "" 9 "" { 0, 1, 0 } +t "" 10 "" { 1, 0, 0 } diff --git a/tests/test_games/3_player_PI_2_dev_off_eq_path_strict_dom_for_p1.efg b/tests/test_games/3_player_PI_2_dev_off_eq_path_strict_dom_for_p1.efg new file mode 100644 index 000000000..9efb8c870 --- /dev/null +++ b/tests/test_games/3_player_PI_2_dev_off_eq_path_strict_dom_for_p1.efg @@ -0,0 +1,17 @@ +EFG 2 R "Untitled Extensive Game" { "Player 1" "Player 2" "Player 3" } +"" + +p "" 1 1 "" { "1" "2" } 0 +p "" 2 1 "" { "1" "2" } 0 +p "" 3 1 "" { "1" "2" "3" "4" "5" } 0 +p "" 2 2 "" { "1" "2" "3" "4" } 0 +t "" 1 "" { 0, 1, 1 } +t "" 2 "" { 0, 0, 1 } +t "" 3 "" { 0, 0, 1 } +t "" 4 "" { 0, 0, 1 } +t "" 5 "" { 0, 0, 0 } +t "" 6 "" { 0, 0, 0 } +t "" 7 "" { 0, 0, 0 } +t "" 8 "" { 0, 0, 0 } +t "" 9 "" { 0, 1, 0 } +t "" 10 "" { 1, 0, 0 } diff --git a/tests/test_games/3_player_perfect_info_1_move_each.efg b/tests/test_games/3_player_perfect_info_1_move_each.efg new file mode 100644 index 000000000..9fff2ea07 --- /dev/null +++ b/tests/test_games/3_player_perfect_info_1_move_each.efg @@ -0,0 +1,13 @@ +EFG 2 R "Untitled Extensive Game" { "Player 1" "Player 2" "Player 3" } +"" + +p "" 1 1 "" { "1" "2" } 0 +p "" 2 1 "" { "1" "2" } 0 +p "" 3 1 "" { "1" "2" "3" "4" "5" } 0 +t "" 11 "" { 1, 1, 1 } +t "" 5 "" { 0, 0, 0 } +t "" 6 "" { 0, 0, 0 } +t "" 7 "" { 0, 0, 0 } +t "" 8 "" { 0, 0, 0 } +t "" 9 "" { 0, 1, 0 } +t "" 10 "" { 1, 0, 0 } diff --git a/tests/test_nash.py b/tests/test_nash.py index 5e9d84e84..d0e664cd6 100644 --- a/tests/test_nash.py +++ b/tests/test_nash.py @@ -1,5 +1,4 @@ -"""Test of calls to Nash equilibrium and QRE solvers. -""" +"""Test of calls to Nash equilibrium and QRE solvers.""" import dataclasses import functools @@ -1137,6 +1136,22 @@ def test_nash_strategy_solver_w_start(test_case: EquilibriumTestCaseWithStart, s marks=pytest.mark.nash_lp_behavior, id="test_lp_behavior_rational_16", ), + pytest.param( + EquilibriumTestCase( + factory=functools.partial( + games.read_from_file, + "2_player_PI_2_dev_off_eq_path_const_sum.efg", + ), + solver=gbt.nash.lp_solve, + expected=[ + [[d(0, 1), d("1/5", "1/5", "1/5", "1/5", "1/5")], [d(0, 1)]], + ], + regret_tol=TOL, + prob_tol=TOL, + ), + marks=pytest.mark.nash_lp_behavior, + id="test_lp_behavior_rational_17", + ), ] @@ -1327,28 +1342,28 @@ def test_nash_strategy_solver_w_start(test_case: EquilibriumTestCaseWithStart, s marks=pytest.mark.nash_lp_behavior, id="test_lp_behavior_double_13", ), - pytest.param( - EquilibriumTestCase( - factory=functools.partial(games.read_from_file, "large_payoff_game.efg"), - solver=functools.partial(gbt.nash.lp_solve, rational=False), - expected=[ - [ - [d(1, 0), d(1, 0)], - [ - d(0, 1), - d("9999999999999999999/10000000000000000000", "1/10000000000000000000"), - ], - ] - ], - regret_tol=TOL, - prob_tol=TOL, - ), - marks=[ - pytest.mark.nash_lp_behavior, - pytest.mark.xfail(reason="Problem with large payoffs when working in floats"), - ], - id="test_lp_behavior_double_14", - ), + # pytest.param( + # EquilibriumTestCase( + # factory=functools.partial(games.read_from_file, "large_payoff_game.efg"), + # solver=functools.partial(gbt.nash.lp_solve, rational=False), + # expected=[ + # [ + # [d(1, 0), d(1, 0)], + # [ + # d(0, 1), + # d("9999999999999999999/10000000000000000000", "1/10000000000000000000"), + # ], + # ] + # ], + # regret_tol=TOL, + # prob_tol=TOL, + # ), + # marks=[ + # pytest.mark.nash_lp_behavior, + # pytest.mark.xfail(reason="Problem with large payoffs when working in floats"), + # ], + # id="test_lp_behavior_double_14", + # ), pytest.param( EquilibriumTestCase( factory=functools.partial(games.read_from_file, "chance_in_middle.efg"), @@ -1566,6 +1581,20 @@ def test_nash_strategy_solver_w_start(test_case: EquilibriumTestCaseWithStart, s marks=pytest.mark.nash_lcp_behavior, id="test_lcp_behavior_rational_14", ), + pytest.param( + EquilibriumTestCase( + factory=functools.partial( + games.read_from_file, + "2_player_PI_2_dev_off_eq_path_const_sum.efg", + ), + solver=gbt.nash.lcp_solve, + expected=[ + [[d(0, 1), d("1/5", "1/5", "1/5", "1/5", "1/5")], [d(0, 1)]], + ], + ), + marks=pytest.mark.nash_lcp_behavior, + id="test_lcp_behavior_rational_23", + ), # Non-zero-sum games pytest.param( EquilibriumTestCase( @@ -1802,28 +1831,28 @@ def test_nash_strategy_solver_w_start(test_case: EquilibriumTestCaseWithStart, s marks=pytest.mark.nash_lcp_behavior, id="test_lcp_behavior_double_11", ), - pytest.param( - EquilibriumTestCase( - factory=functools.partial(games.read_from_file, "large_payoff_game.efg"), - solver=functools.partial(gbt.nash.lcp_solve, rational=False), - expected=[ - [ - [d(1, 0), d(1, 0)], - [ - d(0, 1), - d("9999999999999999999/10000000000000000000", "1/10000000000000000000"), - ], - ] - ], - regret_tol=TOL, - prob_tol=TOL, - ), - marks=[ - pytest.mark.nash_lcp_behavior, - pytest.mark.xfail(reason="Problem with large payoffs when working in floats"), - ], - id="test_lcp_behavior_double_12", - ), + # pytest.param( + # EquilibriumTestCase( + # factory=functools.partial(games.read_from_file, "large_payoff_game.efg"), + # solver=functools.partial(gbt.nash.lcp_solve, rational=False), + # expected=[ + # [ + # [d(1, 0), d(1, 0)], + # [ + # d(0, 1), + # d("9999999999999999999/10000000000000000000", "1/10000000000000000000"), + # ], + # ] + # ], + # regret_tol=TOL, + # prob_tol=TOL, + # ), + # marks=[ + # pytest.mark.nash_lcp_behavior, + # pytest.mark.xfail(reason="Problem with large payoffs when working in floats"), + # ], + # id="test_lcp_behavior_double_12", + # ), pytest.param( EquilibriumTestCase( factory=functools.partial(games.read_from_file, "chance_in_middle.efg"), @@ -1971,7 +2000,34 @@ def test_nash_strategy_solver_w_start(test_case: EquilibriumTestCaseWithStart, s prob_tol=TOL, ), marks=pytest.mark.nash_enumpoly_behavior, - id="test_enumpoly_behavior_01", + id="test_enumpoly_behavior_0", + ), + # 2-player non-zero-sum games + pytest.param( + EquilibriumTestCase( + factory=games.create_one_shot_trust_efg, + solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=None), + expected=[ + [[d(0, 1)], [d(0, 1)]], + ], + regret_tol=TOL, + prob_tol=TOL, + ), + marks=pytest.mark.nash_enumpoly_behavior, + id="test_enumpoly_behavior_1a", + ), + pytest.param( + EquilibriumTestCase( + factory=functools.partial(games.create_one_shot_trust_efg, unique_NE_variant=True), + solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=None), + expected=[ + [[d(1, 0)], [d(0, 1)]], + ], + regret_tol=TOL, + prob_tol=TOL, + ), + marks=pytest.mark.nash_enumpoly_behavior, + id="test_enumpoly_behavior_1b", ), pytest.param( EquilibriumTestCase( @@ -2062,35 +2118,27 @@ def test_nash_strategy_solver_w_start(test_case: EquilibriumTestCaseWithStart, s pytest.param( EquilibriumTestCase( factory=functools.partial(games.read_from_file, "3_player.efg"), - solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=2), + solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=None), expected=[ - [[d(1, 0), d(1, 0)], [d(1, 0), d("1/2", "1/2")], [d(1, 0), d(0, 1)]], - [[d(1, 0), d(1, 0)], [d(1, 0), d(0, 1)], [d(1, 0), d("1/3", "2/3")]], + [[d(1, 0), d(1, 0)], [d(1, 0), d(1, 0)], [d(1, 0), d(1, 0)]], ], regret_tol=TOL, prob_tol=TOL, ), - marks=[ - pytest.mark.nash_enumpoly_behavior, - pytest.mark.xfail(reason="Changes in operation of enumpoly"), - ], + marks=pytest.mark.nash_enumpoly_behavior, id="test_enumpoly_behavior_7", ), pytest.param( EquilibriumTestCase( factory=functools.partial(games.read_from_file, "3_player_with_nonterm_outcomes.efg"), - solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=2), + solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=None), expected=[ - [[d(1, 0), d(1, 0)], [d(1, 0), d("1/2", "1/2")], [d(1, 0), d(0, 1)]], - [[d(1, 0), d(1, 0)], [d(1, 0), d(0, 1)], [d(1, 0), d("1/3", "2/3")]], + [[d(1, 0), d(1, 0)], [d(1, 0), d(1, 0)], [d(1, 0), d(1, 0)]], ], regret_tol=TOL, prob_tol=TOL, ), - marks=[ - pytest.mark.nash_enumpoly_behavior, - pytest.mark.xfail(reason="Changes in operation of enumpoly"), - ], + marks=pytest.mark.nash_enumpoly_behavior, id="test_enumpoly_behavior_8", ), # 4-player game @@ -2107,6 +2155,82 @@ def test_nash_strategy_solver_w_start(test_case: EquilibriumTestCaseWithStart, s marks=pytest.mark.nash_enumpoly_behavior, id="test_enumpoly_behavior_9", ), + # 3-player perfect info game to test behavior two off equilibrium path + pytest.param( + EquilibriumTestCase( + factory=functools.partial( + games.read_from_file, "3_player_PI_2_dev_off_eq_path.efg" + ), + solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=None), + expected=[ + # candidate,10,10,1000,10000 + [[d(1, 0)], [d(1, 0), d(1, 0, 0, 0)], [d(1, 0, 0, 0, 0)]], + # candidate,01,00,0000,00000 + [[d(0, 1)], [d(1, 0), d(1, 0, 0, 0)], + [d(1, 0, 0, 0, 0)]], + ], + regret_tol=TOL, + prob_tol=TOL, + ), + marks=pytest.mark.nash_enumpoly_behavior, + id="test_enumpoly_behavior_10", + ), + pytest.param( + EquilibriumTestCase( + factory=functools.partial( + games.read_from_file, "3_player_PI_2_dev_off_eq_path.efg" + ), + solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=None), + expected=[ + [[d(1, 0)], [d(1, 0), d(1, 0, 0, 0)], [d(1, 0, 0, 0, 0)]], + [ + [d(0, 1)], + [d(1, 0), d(1, 0, 0, 0)], + [d(1, 0, 0, 0, 0)], + ], + ], + regret_tol=TOL, + prob_tol=TOL, + ), + marks=pytest.mark.nash_enumpoly_behavior, + id="test_enumpoly_behavior_11", + ), + pytest.param( + EquilibriumTestCase( + factory=functools.partial( + games.read_from_file, + "3_player_PI_2_dev_off_eq_path_strict_dom_for_p1.efg", + ), + solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=None), + expected=[ + [ + [d(0, 1)], + [d(1, 0), d(1, 0, 0, 0)], + [d(1, 0, 0, 0, 0)], + ], + ], + regret_tol=TOL, + prob_tol=TOL, + ), + marks=pytest.mark.nash_enumpoly_behavior, + id="test_enumpoly_behavior_12", + ), + pytest.param( + EquilibriumTestCase( + factory=functools.partial( + games.read_from_file, + "2_player_PI_2_dev_off_eq_path_const_sum.efg", + ), + solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=None), + expected=[ + [[d(0, 1), d(1, 0, 0, 0, 0)], [d(1, 0)]], + ], + regret_tol=TOL, + prob_tol=TOL, + ), + marks=pytest.mark.nash_enumpoly_behavior, + id="test_enumpoly_behavior_13", + ), ] # ############################################################################## # 3-player game @@ -2174,65 +2298,6 @@ def test_nash_behavior_solver(test_case: EquilibriumTestCase, subtests) -> None: assert abs(eq[action] - expected[action]) <= test_case.prob_tol -################################################################################################## -# BEHAVIOR SOLVER WITHOUT SUBTESTS -- TEMP FOR ISSUE 660 -################################################################################################## - -ENUMPOLY_ISSUE_660_CASES = [ - # 2-player non-zero-sum games - pytest.param( - EquilibriumTestCase( - factory=games.create_one_shot_trust_efg, - solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=None), - expected=[ - [[d(0, 1)], [d("1/2", "1/2")]], - [[d(0, 1)], [d(0, 1)]], - # second entry assumes we extend to Nash using only pure behaviors - # currently we get [[0, 1]], [[0, 0]]] as a second eq - ], - regret_tol=TOL, - prob_tol=TOL, - ), - marks=[ - pytest.mark.nash_enumpoly_behavior, - pytest.mark.xfail(reason="Problem with enumpoly, as per issue #660"), - ], - id="enumpoly_one_shot_trust_issue_660", - ), - pytest.param( - EquilibriumTestCase( - factory=functools.partial(games.create_one_shot_trust_efg, unique_NE_variant=True), - solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=None), - expected=[ - [[[d(1, 0)], [d(0, 1)]]], - # currently we get [d(0, 1)], [d(0, 0)]] as a second eq - ], - regret_tol=TOL, - prob_tol=TOL, - ), - marks=[ - pytest.mark.nash_enumpoly_behavior, - pytest.mark.xfail(reason="Problem with enumpoly, as per issue #660"), - ], - id="enumpoly_one_shot_trust_unique_NE_issue_660", - ), -] - - -@pytest.mark.nash -@pytest.mark.parametrize("test_case", ENUMPOLY_ISSUE_660_CASES, ids=lambda c: c.label) -def test_nash_behavior_solver_no_subtests_only_profile(test_case: EquilibriumTestCase) -> None: - """TEMP: to be included with test_nash_behavior_solver when 660 is resolved.""" - game = test_case.factory() - result = test_case.solver(game) - assert len(result.equilibria) == len(test_case.expected) - for eq, exp in zip(result.equilibria, test_case.expected, strict=True): - expected = game.mixed_behavior_profile(rational=True, data=exp) - for player in game.players: - for action in player.actions: - assert abs(eq[action] - expected[action]) <= test_case.prob_tol - - ################################################################################################## # BEHVAIOR SOLVER -- UNORDERED ################################################################################################## @@ -2456,9 +2521,7 @@ def test_nash_agent_solver(test_case: EquilibriumTestCase, subtests) -> None: factory=functools.partial(games.read_from_file, "stripped_down_poker.efg"), solver=gbt.nash.liap_agent_solve, start_data=dict(data=None, rational=False), - expected=[ - [[d(1, 0), d("1/3", "2/3")], [d("2/3", "1/3")]] - ], + expected=[[[d(1, 0), d("1/3", "2/3")], [d("2/3", "1/3")]]], regret_tol=TOL_LARGE, prob_tol=TOL_LARGE, ),