diff --git a/Makefile.am b/Makefile.am index e843c4df9..b4761162f 100644 --- a/Makefile.am +++ b/Makefile.am @@ -411,6 +411,8 @@ gambit_SOURCES = \ src/gui/dlnashmon.cc \ src/gui/nashspec.cc \ src/gui/nashspec.h \ + src/gui/nashvariants.cc \ + src/gui/nashvariants.h \ src/gui/dlnewtable.cc \ src/gui/dlnewtable.h \ src/gui/dllogit.cc \ diff --git a/doc/gui.nash.rst b/doc/gui.nash.rst index e139f4698..117c2ec2f 100644 --- a/doc/gui.nash.rst +++ b/doc/gui.nash.rst @@ -10,92 +10,246 @@ or click on the calculate icon on the toolbar. -Selecting the method of computing equilibria --------------------------------------------- - -The process of computing Nash equilibria in extensive and strategic -games is similar. This section focuses on the case of extensive games; -the process for strategic games is analogous, except the extensive -game-specific features, such as displaying the profiles on the game -tree, are not applicable. - -Gambit provides guidance on the options for computing Nash equilibria -in a dialog. -The methods applicable to a particular game depend on three criteria: -the number of equilibria to compute, whether the computation is to be -done on the extensive or strategic games, and on details of the game, -such as whether the game has two players or more, and whether the game -is constant-sum. - -The first step in finding equilibria is to specify how many equilibria -are to be found. Some algorithms for computing equilibria are adapted -to finding a single equilibrium, while others attempt to compute the -whole equilibrium set. The first drop-down in the dialog specifies how -many equilibria to compute. In this drop-down there are options for -:guilabel:`Compute one Nash equilibrium`, :guilabel:`Compute some Nash equilibria`, -and, for two-player games, :guilabel:`Compute all Nash equilibria`. -For some games, there exist algorithms which will -compute many equilibria (relatively) efficiently, but are not -guaranteed to find all equilibria. - -To simplify this process of choosing the method to compute equilibria, -the second drop-down offers :guilabel:`Gambit's recommended method`. -The recommendation is determined by the requested number of equilibria -and the structure of the game as follows. - -======================= ============================================= ================== -Requested output Game Method -======================= ============================================= ================== -One equilibrium Two players and constant sum :ref:`lp` -One equilibrium Any other supported game :ref:`logit` -Some equilibria Two players :ref:`lcp` -Some equilibria Three or more players :ref:`simpdiv` -All equilibria Two players :ref:`enummixed` -======================= ============================================= ================== - -The :guilabel:`all equilibria` choice is therefore offered only for -two-player games. The recommendation selects an algorithm; it does not -infer properties such as uniqueness. In particular, "some" means that -the selected method searches for multiple equilibria where possible, not -that a particular number of distinct equilibria is guaranteed. - -For more control, the user can select one of the other methods shown in -the second drop-down. The list is filtered using the requested output -category and the methods' applicability. Methods which only operate on -strategic games automatically select and lock the strategic -representation. Otherwise an extensive game may be solved using either -its behavior representation or its reduced strategic representation. -See :ref:`algorithms` for descriptions and limitations of the methods. - -The current dialog uses the following solver parameters. They are -recorded with each computation so that the interface can expose them for -editing in a future revision. +Choosing how to compute equilibria +---------------------------------- + +The :guilabel:`Compute equilibria` dialog helps you choose a method for +computing Nash equilibria. The choice is made in two steps: you state a +**Goal**, and Gambit recommends a **variant** of one of its **methods** +that suits the Goal and the game. You can accept the recommendation or +choose any other variant in the list. The process is the same for +extensive and strategic games. For strategic games the variants that +need a game tree are not listed, and the features specific to extensive +games, such as displaying the profiles on the game tree, do not apply. + +The terms are used as follows. + +Goal + What you want from the computation, in terms of how many equilibria + are returned: :guilabel:`One equilibrium`, :guilabel:`Several + equilibria`, or :guilabel:`All equilibria`. The Goal determines the + recommended variant and the default parameters. It never removes + variants from the list. + +Method + An algorithm family, named as in :ref:`algorithms` (for example + ``lcp`` or ``simpdiv``). + +Variant + A method applied in a particular representation of the game. Each + variant has its own coverage, reliability and solution concept. + +The Goal +~~~~~~~~ + +Some algorithms are adapted to finding a single equilibrium, while others +attempt to compute the whole equilibrium set, and some compute many +equilibria (relatively) efficiently without guaranteeing all of them. +:guilabel:`All equilibria` is available only for games with two players, +because Gambit has no method that guarantees all equilibria for games +with more players; for such games the option is disabled, and a note +beside it gives the reason. + +The variant list +~~~~~~~~~~~~~~~~ + +The list shows every variant that can be applied to the game's +representations: variants that need a game tree are listed only for +extensive games. A variant that cannot be run on the game, for example +because it is restricted to two-player games, is greyed out and cannot +be selected; hovering over it shows the reason. A variant that can be +run but does not meet your Goal can still be selected, and the detail +pane under the list then shows a warning saying what to expect. A star +marks the variant Gambit recommends for the current Goal. The list can +be sorted by clicking a column heading. The columns are: + +:guilabel:`Method` + A description of the variant and the representation it works on. + +:guilabel:`Coverage` + Which equilibria the variant can be relied on to return. + :guilabel:`One` means a single equilibrium; :guilabel:`Some` means + possibly several equilibria, with no promise of finding all of them; + :guilabel:`All pure` means all equilibria in pure strategies, of + which there may be none; and :guilabel:`All` means every equilibrium. + :guilabel:`All` is claimed only where it is delivered in practice, + and only ``enummixed`` qualifies: it reports every extreme + equilibrium and describes each connected component of equilibria by + its extreme points. Two methods that might be expected to qualify do + not: + + * ``enumpoly`` can characterise all equilibria in theory, but it finds + isolated equilibria only. It cannot represent a continuum of + equilibria on a support, and supports it cannot resolve are skipped. + * ``lcp`` finds only the equilibria reachable by the Lemke-Howson path + on the strategic form (the accessible equilibria), and returns just + one on the sequence form. + +:guilabel:`Reliability` + Whether the variant is guaranteed to return a result on a given game. + :guilabel:`Always` means it is. :guilabel:`Usually` means it + converges in theory but may have problems with games that have some types + of degeneracy, which are common in practice. :guilabel:`No guarantee` + means it is a local or heuristic search that is not known to converge + on every game. + +:guilabel:`Solves for` + The solution concept: :guilabel:`Nash`, or :guilabel:`Agent Nash` + (see :ref:`agent-nash-equilibria`). + +If the selected variant does not meet your Goal, a warning appears +below the list. A collapsible :guilabel:`Details` section shows the +parameters the variant will run with. + +The recommendation +~~~~~~~~~~~~~~~~~~ + +The recommendation depends on the Goal and the structure of the game. +It selects a method; it does not infer properties such as uniqueness. +In particular, "several" means that the selected method searches for +multiple equilibria where possible, not that a particular number of +distinct equilibria is guaranteed. + +.. list-table:: + :header-rows: 1 + :widths: 20 30 15 35 + + * - Goal + - Game + - Method + - Variant for an extensive game + * - One equilibrium + - Two players and constant sum + - :ref:`lp` + - Sequence form + * - One equilibrium + - Any other game + - :ref:`logit` + - Agent form + * - Several equilibria + - Two players + - :ref:`lcp` + - Strategic form (Lemke-Howson) + * - Several equilibria + - Three or more players + - :ref:`simpdiv` + - Strategic form + * - All equilibria + - Two players + - :ref:`enummixed` + - Strategic form + +The recommended variant is the one of the recommended method that is +applicable to the game and meets the Goal, preferring the variant that +works on the extensive game. Hence for :guilabel:`Several equilibria` +on a two-player extensive game the recommendation is Lemke-Howson on the +strategic form, because the sequence-form variant of ``lcp`` returns +only one equilibrium. ``simpdiv`` is recommended over ``gnm`` for three +or more players because it always returns a result, whereas ``gnm`` +may have problems with some types of degenerate games. For strategic games every +recommendation uses the strategic form. + +The dialog remembers your Goal and variant for each game while it is +open: when you open the dialog again on the same game it starts with +the choices you last made, provided that variant can still be run. This +memory is not saved with the game file. + +.. _method-variants: + +Why the representation is part of the choice +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +An extensive game can be analysed directly, through its behavior +strategies and sequence form, or through its reduced strategic form. It +is tempting to regard the choice of representation as a setting that can +be applied to any method. It is not: for many methods, applying the +method to a different representation produces a different algorithm +with different guarantees, so each combination is treated as a +separate variant. The differences are as follows. + +.. list-table:: + :header-rows: 1 + :widths: 20 80 + + * - Method + - What changes between representations + * - ``lp`` + - The algorithms are theoretically equivalent, but the single + equilibrium returned can differ. + * - ``lcp`` + - Theoretically different algorithms: Lemke on the sequence form + and Lemke-Howson on the strategic form. The sequence-form + implementation returns one equilibrium; the strategic-form one + returns the accessible equilibria. + * - ``enumpoly`` + - In theory the sequence form suffices. In practice the sequence + form does not yet use dominance elimination, and the per-support + solvers mean some equilibria may be found in one representation + and not the other. + * - ``liap``, ``enumpure`` + - Different solution concept: on the extensive game they compute + agent Nash equilibria, not Nash equilibria. + * - ``logit`` + - Traces the agent quantal response equilibrium on the extensive + game, but its limit is a Nash equilibrium. + +For an extensive game, computation on the extensive representation is +generally preferred when it is available, since it is often a +significantly more compact representation of the strategic +characteristics of the game than the reduced strategic game. See +:ref:`algorithms` for descriptions and limitations of the methods. + +.. _agent-nash-equilibria: + +Agent Nash equilibria +~~~~~~~~~~~~~~~~~~~~~ + +An *agent Nash equilibrium* is a behavior profile of an extensive game at +which no agent can gain by deviating at its own information set alone. +It is not necessarily a Nash equilibrium of the game: a profile may be +stable against deviations at one information set at a time while a +player could gain by changing play at several information sets +together. + +The agent-form variants of ``enumpure`` and ``liap`` compute agent Nash +equilibria, and their :guilabel:`Solves for` entry in the variant list +reads :guilabel:`Agent Nash`. The profiles they return are labelled as +agent Nash equilibria in the description across the top of the profiles +panel (for example, "One agent Nash equilibrium in pure strategies in +extensive game"), so that they are not mistaken for Nash equilibria. If +Nash equilibria are what you need, use the strategic-form variants, or +check the profiles returned. + +Parameters +~~~~~~~~~~ + +The :guilabel:`Details` section shows the parameters the selected variant will use. They are shown for +information and cannot be edited in the dialog. They depend on the Goal +only for ``enumpoly`` and ``simpdiv``. ================ ============================================================================ Method Parameters used by the graphical interface ================ ============================================================================ -``enumpure`` Enumerate all pure-strategy equilibria. -``enummixed`` Enumerate all mixed-strategy extreme equilibria. -``enumpoly`` Stop after the first equilibrium when "one" is requested; otherwise search all - supports; maximum regret ``1e-4``. +``enumpure`` No method-specific parameters. +``enummixed`` No method-specific parameters. +``enumpoly`` Stop after the first equilibrium when :guilabel:`One equilibrium` is the Goal; + otherwise search all supports; maximum regret ``1e-4``. ``lp`` No method-specific parameters. -``lcp`` Search all accessible equilibria; unlimited recursion depth. -``liap`` 10 random starting points; 1,000 iterations per point; maximum regret ``1e-4``. +``lcp`` All accessible equilibria; unlimited recursion depth. +``liap`` 10 random starting points; at most 1,000 iterations per point; maximum + regret ``1e-4``. ``logit`` Maximum regret ``1e-8``; initial step ``0.03``; maximum acceleration ``1.1``. -``simpdiv`` One random starting point when "one" is requested, otherwise 20; denominator - 100; grid resize factor 2; maximum regret ``1/10000000``. +``simpdiv`` One random starting point when :guilabel:`One equilibrium` is the Goal, + otherwise 20; denominator 100; grid resize factor 2; maximum regret + ``1/10000000``. ``ipa`` One random perturbation. ``gnm`` One random perturbation; ending lambda ``-10``; 100 steps per support cell; local Newton refinement every 3 steps, with at most 10 iterations. -``hp`` No method-specific parameters. +``hp`` 10 random priors; maximum regret ``1e-8``. ================ ============================================================================ -For extensive games, there is an option of whether to use the -extensive or strategic game for computation when the selected method -supports both. In general, computation -using the extensive game is preferred, since it is often a -significantly more compact representation of the strategic -characteristics of the game than the reduced strategic game is. +Running the computation +~~~~~~~~~~~~~~~~~~~~~~~ For even moderate sized games, computation of equilibrium can be a time-intensive process. Gambit runs all computations in the diff --git a/src/gui/dlnash.cc b/src/gui/dlnash.cc index 27cbccfa3..96b756c7b 100644 --- a/src/gui/dlnash.cc +++ b/src/gui/dlnash.cc @@ -20,14 +20,23 @@ // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. // +#include #include #include #include +#include +#include +#include #include +#include #ifndef WX_PRECOMP #include #endif // WX_PRECOMP +#include +#include +#include +#include #include #include "games.h" @@ -36,85 +45,164 @@ #include "dlnash.h" namespace Gambit::GUI { -static wxString s_recommended(wxT("with Gambit's recommended method")); -static wxString s_enumpure(wxT("by looking for pure strategy equilibria")); -static wxString s_enummixed(wxT("by enumerating extreme points")); -static wxString s_enumpoly(wxT("by solving systems of polynomial equations")); -static wxString s_gnm(wxT("by global Newton tracing")); -static wxString s_hp(wxT("by the Herings-Peeters homotopy")); -static wxString s_ipa(wxT("by iterated polymatrix approximation")); -static wxString s_lp(wxT("by solving a linear program")); -static wxString s_lcp(wxT("by solving a linear complementarity program")); -static wxString s_liap(wxT("by minimizing the Lyapunov function")); -static wxString s_logit(wxT("by tracing logit equilibria")); -static wxString s_simpdiv(wxT("by simplicial subdivision")); namespace { -NashEquilibriumTarget GetTarget(int p_selection) +NashEquilibriumTarget GetTarget(NashGoal p_goal) { - if (p_selection == 0) { + switch (p_goal) { + case NashGoal::One: return NashEquilibriumTarget::One; - } - if (p_selection == 1) { + case NashGoal::Several: return NashEquilibriumTarget::Some; + default: + return NashEquilibriumTarget::All; } - return NashEquilibriumTarget::All; } -NashMethodSpec ResolveMethod(const wxString &p_method, NashEquilibriumTarget p_target, - const Game &p_game) +std::vector OfferedVariants(const Game &p_game) { - if (p_method == s_recommended) { - if (p_target == NashEquilibriumTarget::One) { - if (p_game->NumPlayers() == 2 && p_game->IsConstSum()) { - return LPNashSpec{}; - } - return LogitNashSpec{}; + std::vector result; + for (const auto &variant : NashVariants()) { + if (variant.representation == VariantRepresentation::Strategic || As(p_game)) { + result.push_back(&variant); } - if (p_target == NashEquilibriumTarget::Some) { - if (p_game->NumPlayers() == 2) { - return LCPNashSpec{}; - } - return SimpdivNashSpec{}; - } - return EnumMixedNashSpec{}; } - if (p_method == s_enumpure) { - return EnumPureNashSpec{}; + return result; +} + +wxString VariantLabel(const NashVariant &p_variant) +{ + const bool strategic = p_variant.representation == VariantRepresentation::Strategic; + const wxString form = strategic ? wxT(" on the strategic form") : wxT(" on the sequence form"); + switch (p_variant.method) { + case NashMethodId::EnumMixed: + return wxT("Enumerate extreme points") + form; + case NashMethodId::EnumPoly: + return wxT("Solve systems of polynomial equations") + form; + case NashMethodId::EnumPure: + return wxT("Look for pure strategy equilibria") + + (strategic ? form : wxString(wxT(" on the agent form"))); + case NashMethodId::GNM: + return wxT("Global Newton tracing") + form; + case NashMethodId::HP: + return wxT("Herings-Peeters homotopy") + form; + case NashMethodId::IPA: + return wxT("Iterated polymatrix approximation") + form; + case NashMethodId::LCP: + return strategic ? wxString(wxT("Lemke-Howson linear complementarity") + form) + : wxString(wxT("Lemke linear complementarity") + form); + case NashMethodId::Liap: + return wxT("Minimize the Lyapunov function") + + (strategic ? form : wxString(wxT(" on the agent form"))); + case NashMethodId::Logit: + return strategic ? wxString(wxT("Trace logit quantal response equilibria")) + : wxString(wxT("Trace agent logit quantal response equilibria")); + case NashMethodId::LP: + return wxT("Solve a linear program") + form; + case NashMethodId::Simpdiv: + return wxT("Simplicial subdivision") + form; } - if (p_method == s_enummixed) { - return EnumMixedNashSpec{}; + throw std::logic_error("Unknown Nash equilibrium method"); +} + +wxString FromStd(std::string_view p_text) +{ + return wxString::FromUTF8(p_text.data(), p_text.size()); +} + +wxString GoalName(NashGoal p_goal) +{ + switch (p_goal) { + case NashGoal::One: + return wxT("One"); + case NashGoal::Several: + return wxT("Several"); + default: + return wxT("All"); + } +} + +wxString CoverageLabel(VariantCoverage p_coverage) +{ + switch (p_coverage) { + case VariantCoverage::One: + return wxT("One"); + case VariantCoverage::Some: + return wxT("Some"); + case VariantCoverage::AllPure: + return wxT("All pure"); + default: + return wxT("All"); + } +} + +wxString ReliabilityLabel(VariantReliability p_reliability) +{ + switch (p_reliability) { + case VariantReliability::Always: + return wxT("Always"); + case VariantReliability::Usually: + return wxT("Usually"); + default: + return wxT("No guarantee"); + } +} + +wxString ConceptLabel(VariantConcept p_concept) +{ + return p_concept == VariantConcept::Nash ? wxT("Nash") : wxT("Agent Nash"); +} + +wxString GoalMissWarning(const NashVariant &p_variant, NashGoal p_goal) +{ + wxString what; + switch (p_goal) { + case NashGoal::One: + what = wxT("may find no equilibrium"); + break; + case NashGoal::Several: + what = wxT("returns only one equilibrium"); + break; + default: + what = wxT("does not guarantee to return all equilibria"); + break; } - if (p_method == s_enumpoly) { - if (p_target == NashEquilibriumTarget::One) { + return wxString::Format(wxT("Warning: this variant %s; your Goal is %s."), what, + GoalName(p_goal)); +} + +const wxString s_allUnavailable(wxT("Gambit has no method that guarantees all equilibria for " + "games with more than two players.")); + +NashMethodSpec ResolveMethod(NashMethodId p_method, NashGoal p_goal) +{ + switch (p_method) { + case NashMethodId::EnumMixed: + return EnumMixedNashSpec{}; + case NashMethodId::EnumPoly: + if (p_goal == NashGoal::One) { return EnumPolyNashSpec{.stopAfter = 1}; } return EnumPolyNashSpec{}; - } - if (p_method == s_gnm) { + case NashMethodId::EnumPure: + return EnumPureNashSpec{}; + case NashMethodId::GNM: return GNMNashSpec{}; - } - if (p_method == s_hp) { + case NashMethodId::HP: return HPNashSpec{}; - } - if (p_method == s_ipa) { + case NashMethodId::IPA: return IPANashSpec{}; - } - if (p_method == s_lp) { - return LPNashSpec{}; - } - if (p_method == s_lcp) { + case NashMethodId::LCP: return LCPNashSpec{}; - } - if (p_method == s_liap) { + case NashMethodId::Liap: return LiapNashSpec{}; - } - if (p_method == s_logit) { + case NashMethodId::Logit: return LogitNashSpec{}; - } - if (p_method == s_simpdiv) { - if (p_target == NashEquilibriumTarget::One) { + case NashMethodId::LP: + return LPNashSpec{}; + case NashMethodId::Simpdiv: + if (p_goal == NashGoal::One) { return SimpdivNashSpec{.startingPoints = 1}; } return SimpdivNashSpec{}; @@ -122,29 +210,11 @@ NashMethodSpec ResolveMethod(const wxString &p_method, NashEquilibriumTarget p_t throw std::logic_error("Unknown Nash equilibrium method"); } -template -concept StrategicMethod = - std::same_as || std::same_as || - std::same_as || std::same_as || std::same_as || - std::same_as || std::same_as; - template concept RationalOutputMethod = std::same_as || std::same_as || std::same_as || std::same_as; -bool RequiresStrategicRepresentation(const NashMethodSpec &p_method) -{ - return std::visit([](const Method &) { return StrategicMethod; }, - p_method); -} - -bool RequiresStrategicRepresentation(const NashMethodSpec &p_method, bool p_recommended) -{ - return RequiresStrategicRepresentation(p_method) || - (p_recommended && std::holds_alternative(p_method)); -} - bool UsesRationalOutput(const NashMethodSpec &p_method) { return std::visit([](const Method &) { return RationalOutputMethod; }, @@ -164,7 +234,7 @@ wxString ExternalCommand(const NashComputationSpec &p_spec) return std::visit( [&](const Method &method) { if constexpr (std::is_same_v) { - return prefix + wxT("enumpure") + strategic; + return prefix + wxT("enumpure") + (p_spec.agentNash ? wxT(" -A") : strategic); } else if constexpr (std::is_same_v) { return prefix + wxT("enummixed"); @@ -202,9 +272,9 @@ wxString ExternalCommand(const NashComputationSpec &p_spec) return prefix + command + strategic; } else if constexpr (std::is_same_v) { - return prefix + wxString::Format("liap -d 10 -n %d -i %d -m %.17g", - method.startingPoints, method.maxIterations, - method.maxRegret); + return prefix + wxT("liap") + (p_spec.agentNash ? wxT(" -A") : wxString{}) + + wxString::Format(" -d 10 -n %d -i %d -m %.17g", method.startingPoints, + method.maxIterations, method.maxRegret); } else if constexpr (std::is_same_v) { return prefix + @@ -319,127 +389,327 @@ wxString ParameterDescription(const NashMethodSpec &p_method) p_method); } +using ParameterRow = std::pair; + +std::vector ParameterRows(const NashMethodSpec &p_method) +{ + return std::visit( + [](const Method &method) -> std::vector { + const auto regret = [](double p_value) { return wxString::Format("%.4g", p_value); }; + if constexpr (std::is_same_v) { + return {{wxT("Equilibria sought"), + method.stopAfter.has_value() && method.stopAfter.value() == 1 + ? wxT("One") + : wxT("All supports")}, + {wxT("Maximum regret"), regret(method.maxRegret)}}; + } + else if constexpr (std::is_same_v) { + return { + {wxT("Perturbations"), wxString::Format("%d", method.perturbations)}, + {wxT("Ending lambda"), regret(method.lambdaEnd)}, + {wxT("Steps per support cell"), wxString::Format("%d", method.steps)}, + {wxT("Local Newton interval"), wxString::Format("%d", method.localNewtonInterval)}, + {wxT("Local Newton maximum iterations"), + wxString::Format("%d", method.localNewtonMaxIterations)}}; + } + else if constexpr (std::is_same_v) { + return {{wxT("Random priors"), wxString::Format("%d", method.priors)}, + {wxT("Maximum regret"), regret(method.maxRegret)}}; + } + else if constexpr (std::is_same_v) { + return {{wxT("Perturbations"), wxString::Format("%d", method.perturbations)}}; + } + else if constexpr (std::is_same_v) { + return {{wxT("Equilibria sought"), wxT("All accessible")}, + {wxT("Recursion depth"), method.maxDepth == 0 + ? wxString(wxT("Unlimited")) + : wxString::Format("%d", method.maxDepth)}}; + } + else if constexpr (std::is_same_v) { + return {{wxT("Random starting points"), wxString::Format("%d", method.startingPoints)}, + {wxT("Maximum iterations"), wxString::Format("%d", method.maxIterations)}, + {wxT("Maximum regret"), regret(method.maxRegret)}}; + } + else if constexpr (std::is_same_v) { + return {{wxT("Maximum regret"), regret(method.maxRegret)}, + {wxT("Initial step"), regret(method.firstStep)}, + {wxT("Maximum acceleration"), regret(method.maxAcceleration)}}; + } + else if constexpr (std::is_same_v) { + std::ostringstream maxRegret; + maxRegret << method.maxRegret; + return {{wxT("Random starting points"), wxString::Format("%d", method.startingPoints)}, + {wxT("Starting denominator"), wxString::Format("%d", method.randomDenominator)}, + {wxT("Grid resize factor"), wxString::Format("%d", method.gridResize)}, + {wxT("Maximum regret"), wxString(maxRegret.str())}}; + } + else { + return {}; + } + }, + p_method); +} + +} // namespace + +namespace { +enum ListColumn { COL_STAR, COL_METHOD, COL_COVERAGE, COL_RELIABILITY, COL_SOLVES }; } // namespace NashChoiceDialog::NashChoiceDialog(wxWindow *p_parent, const std::shared_ptr &p_doc) - : wxDialog(p_parent, wxID_ANY, wxT("Compute Nash equilibria"), wxDefaultPosition, wxDefaultSize, + : wxDialog(p_parent, wxID_ANY, wxT("Compute equilibria"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), - m_doc(p_doc) + m_doc(p_doc), m_variants(OfferedVariants(p_doc->GetGame())) { const int S = FromDIP(5); auto *topSizer = new wxBoxSizer(wxVERTICAL); - if (m_doc->GetGame()->NumPlayers() == 2) { - wxString countChoices[] = {wxT("Compute one Nash equilibrium"), - wxT("Compute some Nash equilibria"), - wxT("Compute all Nash equilibria")}; - m_countChoice = - new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 3, countChoices); + auto *goalSizer = new wxBoxSizer(wxHORIZONTAL); + goalSizer->Add(new wxStaticText(this, wxID_ANY, wxT("Goal:")), 0, + wxALIGN_CENTER_VERTICAL | wxRIGHT, S * 2); + const wxString goalLabels[] = {wxT("One equilibrium"), wxT("Several equilibria"), + wxT("All equilibria")}; + for (int i = 0; i < 3; i++) { + m_goalButtons[i] = new wxRadioButton(this, wxID_ANY, goalLabels[i], wxDefaultPosition, + wxDefaultSize, i == 0 ? wxRB_GROUP : 0); + goalSizer->Add(m_goalButtons[i], 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, S * 2); + m_goalButtons[i]->Bind(wxEVT_RADIOBUTTON, &NashChoiceDialog::OnGoal, this); } - else { - wxString countChoices[] = {wxT("Compute one Nash equilibrium"), - wxT("Compute some Nash equilibria")}; - m_countChoice = - new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 2, countChoices); + m_goalButtons[0]->SetValue(true); + topSizer->Add(goalSizer, 0, wxALL | wxEXPAND, S); + + const auto game = m_doc->GetGame(); + const bool allAvailable = std::ranges::any_of(m_variants, [&](const NashVariant *v) { + return !UnavailableReason(*v, game) && MeetsGoal(*v, NashGoal::All); + }); + m_goalButtons[2]->Enable(allAvailable); + m_goalNote = new wxStaticText(this, wxID_ANY, allAvailable ? wxString{} : s_allUnavailable); + m_goalNote->SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT)); + topSizer->Add(m_goalNote, 0, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, S); + + const NashChoice &remembered = m_doc->GetNashChoice(); + if (remembered.goal != NashGoal::All || allAvailable) { + SetGoal(remembered.goal); + if (std::ranges::find(m_variants, remembered.variant) != m_variants.end() && + !UnavailableReason(*remembered.variant, game)) { + m_userChoice = remembered.variant; + } } - m_countChoice->SetSelection(0); - - Connect(m_countChoice->GetId(), wxEVT_COMMAND_CHOICE_SELECTED, - wxCommandEventHandler(NashChoiceDialog::OnCount)); - topSizer->Add(m_countChoice, 0, wxALL | wxEXPAND, S); - if (p_doc->GetGame()->NumPlayers() == 2 && m_doc->GetGame()->IsConstSum()) { - wxString methodChoices[] = {s_recommended, s_lp, s_simpdiv, s_logit, s_enumpoly}; - m_methodChoice = - new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 5, methodChoices); - } - else { - wxString methodChoices[] = {s_recommended, s_simpdiv, s_logit, s_enumpoly}; - m_methodChoice = - new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 4, methodChoices); - } - m_methodChoice->SetSelection(0); - topSizer->Add(m_methodChoice, 0, wxALL | wxEXPAND, S); - - if (As(m_doc->GetGame())) { - wxString repChoices[] = {wxT("using the extensive game"), wxT("using the strategic game")}; - m_repChoice = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 2, repChoices); - m_repChoice->SetSelection(0); - topSizer->Add(m_repChoice, 0, wxALL | wxEXPAND, S); - - // We only need to respond to changes in method when we have an - // extensive game - Connect(m_methodChoice->GetId(), wxEVT_COMMAND_CHOICE_SELECTED, - wxCommandEventHandler(NashChoiceDialog::OnMethod)); - } - else { - m_repChoice = nullptr; + m_list = new wxListCtrl(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, + wxLC_REPORT | wxLC_SINGLE_SEL); + m_list->AppendColumn(wxT(""), wxLIST_FORMAT_CENTER, FromDIP(24)); + m_list->AppendColumn(wxT("Method")); + m_list->AppendColumn(wxT("Coverage")); + m_list->AppendColumn(wxT("Reliability")); + m_list->AppendColumn(wxT("Solves for")); + m_list->Bind(wxEVT_LIST_ITEM_SELECTED, &NashChoiceDialog::OnItemSelected, this); + m_list->Bind(wxEVT_LIST_COL_CLICK, &NashChoiceDialog::OnColumnClick, this); + for (wxWindow *child : m_list->GetChildren()) { + child->Bind(wxEVT_MOTION, + [this, child](wxMouseEvent &p_event) { OnListMotion(child, p_event); }); } - UpdateRepresentationChoice(); + int displayIndex = wxDisplay::GetFromWindow(p_parent ? p_parent : this); + const wxRect area = wxDisplay(displayIndex == wxNOT_FOUND ? 0 : displayIndex).GetClientArea(); + m_list->SetMinSize(wxSize(std::min(FromDIP(660), area.width * 8 / 10), + std::min(FromDIP(300), area.height * 4 / 10))); + topSizer->Add(m_list, 1, wxLEFT | wxRIGHT | wxEXPAND, S); + + auto *legend = new wxStaticText(this, wxID_ANY, + wxString::FromUTF8("\xE2\x98\x85") + + wxT(" marks the variant Gambit recommends for the Goal.")); + topSizer->Add(legend, 0, wxLEFT | wxRIGHT | wxTOP | wxEXPAND, S); + + m_warning = new wxStaticText(this, wxID_ANY, wxEmptyString); + m_warning->SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT)); + topSizer->Add(m_warning, 0, wxLEFT | wxRIGHT | wxTOP | wxEXPAND, S); + + m_details = new wxCollapsiblePane(this, wxID_ANY, wxT("Details")); + m_detailsGrid = new wxFlexGridSizer(2, FromDIP(4), FromDIP(16)); + auto *paneSizer = new wxBoxSizer(wxVERTICAL); + paneSizer->Add(m_detailsGrid, 0, wxALL, S); + m_details->GetPane()->SetSizer(paneSizer); + topSizer->Add(m_details, 0, wxALL | wxEXPAND, S); if (auto *buttonSizer = CreateStdDialogButtonSizer(wxOK | wxCANCEL)) { topSizer->Add(buttonSizer, 0, wxALL | wxEXPAND, S); } + RefreshList(); + for (const int column : {COL_METHOD, COL_COVERAGE, COL_RELIABILITY, COL_SOLVES}) { + m_list->SetColumnWidth(column, wxLIST_AUTOSIZE_USEHEADER); + } + SetSizer(topSizer); topSizer->Fit(this); - topSizer->SetSizeHints(this); + SetMinSize(wxSize(GetSize().x, GetSize().y - FromDIP(140))); wxTopLevelWindowBase::Layout(); CenterOnParent(); } -void NashChoiceDialog::OnCount(wxCommandEvent &p_event) +NashGoal NashChoiceDialog::GetGoal() const { - m_methodChoice->Clear(); - m_methodChoice->Append(s_recommended); + if (m_goalButtons[2]->GetValue()) { + return NashGoal::All; + } + return m_goalButtons[1]->GetValue() ? NashGoal::Several : NashGoal::One; +} - if (p_event.GetSelection() == 0) { - if (m_doc->GetGame()->NumPlayers() == 2 && m_doc->GetGame()->IsConstSum()) { - m_methodChoice->Append(s_lp); +void NashChoiceDialog::SetGoal(NashGoal p_goal) +{ + m_goalButtons[static_cast(p_goal)]->SetValue(true); +} + +const NashVariant &NashChoiceDialog::GetSelectedVariant() const +{ + return m_userChoice ? *m_userChoice : RecommendedVariant(GetGoal(), m_doc->GetGame()); +} + +void NashChoiceDialog::OnGoal(wxCommandEvent &) { UpdateRecommendation(); } + +void NashChoiceDialog::OnItemSelected(wxListEvent &p_event) +{ + if (m_refreshing) { + return; + } + const auto *variant = m_rows.at(p_event.GetIndex()); + if (UnavailableReason(*variant, m_doc->GetGame())) { + SelectRow(GetSelectedVariant()); + return; + } + m_userChoice = variant; + UpdateDetail(); +} + +void NashChoiceDialog::OnListMotion(wxWindow *p_window, wxMouseEvent &p_event) +{ + p_event.Skip(); + int flags = 0; + const wxPoint position = m_list->ScreenToClient(p_window->ClientToScreen(p_event.GetPosition())); + const long row = m_list->HitTest(position, flags); + wxString tip; + if (row != wxNOT_FOUND && row < static_cast(m_rows.size())) { + if (const auto reason = UnavailableReason(*m_rows[row], m_doc->GetGame())) { + tip = FromStd(*reason); } - m_methodChoice->Append(s_simpdiv); - m_methodChoice->Append(s_logit); - m_methodChoice->Append(s_enumpoly); } - else if (p_event.GetSelection() == 1) { - if (m_doc->GetGame()->NumPlayers() == 2) { - m_methodChoice->Append(s_lcp); + if (tip != m_tip) { + m_tip = tip; + p_window->UnsetToolTip(); + if (!tip.empty()) { + p_window->SetToolTip(tip); } - m_methodChoice->Append(s_enumpure); - m_methodChoice->Append(s_liap); - m_methodChoice->Append(s_gnm); - m_methodChoice->Append(s_ipa); - m_methodChoice->Append(s_hp); - m_methodChoice->Append(s_enumpoly); } - else { - if (m_doc->GetGame()->NumPlayers() == 2) { - m_methodChoice->Append(s_enummixed); +} + +void NashChoiceDialog::OnColumnClick(wxListEvent &p_event) +{ + const int column = p_event.GetColumn(); + if (column == COL_STAR) { + return; + } + m_sortAscending = (column != m_sortColumn) || !m_sortAscending; + m_sortColumn = column; + RefreshList(); +} + +void NashChoiceDialog::SelectRow(const NashVariant &p_variant) +{ + const bool wasRefreshing = m_refreshing; + m_refreshing = true; + for (int row = 0; row < static_cast(m_rows.size()); row++) { + if (m_rows[row] == &p_variant) { + m_list->SetItemState(row, wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED, + wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED); + m_list->EnsureVisible(row); + } + else { + m_list->SetItemState(row, 0, wxLIST_STATE_SELECTED); } } - m_methodChoice->SetSelection(0); - UpdateRepresentationChoice(); + m_refreshing = wasRefreshing; } -void NashChoiceDialog::OnMethod(wxCommandEvent &) { UpdateRepresentationChoice(); } +void NashChoiceDialog::RefreshList() +{ + const auto game = m_doc->GetGame(); + const auto goal = GetGoal(); + const auto *recommended = &RecommendedVariant(goal, game); + + m_rows = m_variants; + if (m_sortColumn != -1) { + const auto key = [&](const NashVariant *v) -> std::pair { + switch (m_sortColumn) { + case COL_METHOD: + return {0, std::string(v->methodName)}; + case COL_COVERAGE: + return {static_cast(v->coverage), {}}; + case COL_RELIABILITY: + return {static_cast(v->reliability), {}}; + default: + return {static_cast(v->solves), {}}; + } + }; + std::ranges::stable_sort(m_rows, [&](const NashVariant *a, const NashVariant *b) { + return m_sortAscending ? key(a) < key(b) : key(b) < key(a); + }); + } -void NashChoiceDialog::UpdateRepresentationChoice() + m_refreshing = true; + m_list->DeleteAllItems(); + const wxColour grey = wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT); + for (int row = 0; row < static_cast(m_rows.size()); row++) { + const auto &variant = *m_rows[row]; + const auto reason = UnavailableReason(variant, game); + const wxString label = VariantLabel(variant); + m_list->InsertItem(row, + &variant == recommended ? wxString::FromUTF8("\xE2\x98\x85") : wxString{}); + m_list->SetItem(row, COL_METHOD, label); + m_list->SetItem(row, COL_COVERAGE, CoverageLabel(variant.coverage)); + m_list->SetItem(row, COL_RELIABILITY, ReliabilityLabel(variant.reliability)); + m_list->SetItem(row, COL_SOLVES, ConceptLabel(variant.solves)); + if (reason) { + m_list->SetItemTextColour(row, grey); + } + } + m_refreshing = false; + SelectRow(m_userChoice ? *m_userChoice : *recommended); + UpdateDetail(); +} + +void NashChoiceDialog::UpdateRecommendation() { - if (!m_repChoice) { - return; + const auto *recommended = &RecommendedVariant(GetGoal(), m_doc->GetGame()); + for (int row = 0; row < static_cast(m_rows.size()); row++) { + m_list->SetItem(row, COL_STAR, + m_rows[row] == recommended ? wxString::FromUTF8("\xE2\x98\x85") : wxString{}); } - const auto method = ResolveMethod(m_methodChoice->GetStringSelection(), - GetTarget(m_countChoice->GetSelection()), m_doc->GetGame()); - if (RequiresStrategicRepresentation(method, - m_methodChoice->GetStringSelection() == s_recommended)) { - m_repChoice->SetSelection(1); - m_repChoice->Enable(false); + SelectRow(m_userChoice ? *m_userChoice : *recommended); + UpdateDetail(); +} + +void NashChoiceDialog::UpdateDetail() +{ + const auto &variant = GetSelectedVariant(); + const auto goal = GetGoal(); + m_warning->SetLabel(MeetsGoal(variant, goal) ? wxString{} : GoalMissWarning(variant, goal)); + + const auto rows = ParameterRows(GetComputation().method); + + wxWindow *pane = m_details->GetPane(); + m_detailsGrid->Clear(true); + if (rows.empty()) { + m_detailsGrid->Add(new wxStaticText(pane, wxID_ANY, wxT("No parameters"))); + m_detailsGrid->Add(new wxStaticText(pane, wxID_ANY, wxEmptyString)); } - else { - m_repChoice->Enable(true); + for (const auto &[name, value] : rows) { + m_detailsGrid->Add(new wxStaticText(pane, wxID_ANY, name)); + m_detailsGrid->Add(new wxStaticText(pane, wxID_ANY, value)); } + pane->Layout(); + Layout(); } bool NashChoiceDialog::UseStrategic() const @@ -449,14 +719,13 @@ bool NashChoiceDialog::UseStrategic() const NashComputationSpec NashChoiceDialog::GetComputation() const { - const auto target = GetTarget(m_countChoice->GetSelection()); - auto method = ResolveMethod(m_methodChoice->GetStringSelection(), target, m_doc->GetGame()); - const bool recommended = m_methodChoice->GetStringSelection() == s_recommended; - const auto representation = !m_repChoice || m_repChoice->GetSelection() == 1 || - RequiresStrategicRepresentation(method, recommended) - ? NashRepresentation::Strategic - : NashRepresentation::Behavior; - return {representation, target, std::move(method), recommended}; + const auto goal = GetGoal(); + const auto &variant = GetSelectedVariant(); + return {variant.representation == VariantRepresentation::Strategic + ? NashRepresentation::Strategic + : NashRepresentation::Behavior, + GetTarget(goal), ResolveMethod(variant.method, goal), m_userChoice == nullptr, + variant.solves == VariantConcept::AgentNash}; } std::shared_ptr NashChoiceDialog::GetCommand() const @@ -471,16 +740,17 @@ std::shared_ptr NashChoiceDialog::GetCommand() const output = std::make_shared>(m_doc.get(), useBehavior); } + const wxString kind = computation.agentNash ? wxT("agent Nash ") : wxString{}; wxString count; switch (computation.target) { case NashEquilibriumTarget::One: - count = wxT("One equilibrium "); + count = wxT("One ") + kind + wxT("equilibrium "); break; case NashEquilibriumTarget::Some: - count = wxT("Some equilibria "); + count = wxT("Some ") + kind + wxT("equilibria "); break; case NashEquilibriumTarget::All: - count = wxT("All equilibria "); + count = wxT("All ") + kind + wxT("equilibria "); break; } const wxString representation = diff --git a/src/gui/dlnash.h b/src/gui/dlnash.h index 71e99c655..e06e25e33 100644 --- a/src/gui/dlnash.h +++ b/src/gui/dlnash.h @@ -23,17 +23,45 @@ #ifndef GAMBIT_GUI_DLNASH_H #define GAMBIT_GUI_DLNASH_H +#include +#include + +#include +#include +#include + #include "gamedoc.h" +#include "nashvariants.h" namespace Gambit::GUI { class NashChoiceDialog final : public wxDialog { std::shared_ptr m_doc; - wxChoice *m_countChoice, *m_methodChoice, *m_repChoice; + wxRadioButton *m_goalButtons[3]; + wxStaticText *m_goalNote; + wxStaticText *m_warning; + wxCollapsiblePane *m_details; + wxFlexGridSizer *m_detailsGrid; + wxListCtrl *m_list; + std::vector m_variants; + std::vector m_rows; + const NashVariant *m_userChoice{nullptr}; + int m_sortColumn{-1}; + bool m_sortAscending{true}; + bool m_refreshing{false}; + wxString m_tip; - // Event handlers - void OnCount(wxCommandEvent &); - void OnMethod(wxCommandEvent &); - void UpdateRepresentationChoice(); + void OnGoal(wxCommandEvent &); + void OnItemSelected(wxListEvent &); + void OnColumnClick(wxListEvent &); + void OnListMotion(wxWindow *, wxMouseEvent &); + void RefreshList(); + void UpdateRecommendation(); + void SelectRow(const NashVariant &); + void UpdateDetail(); + + NashGoal GetGoal() const; + void SetGoal(NashGoal); + const NashVariant &GetSelectedVariant() const; NashComputationSpec GetComputation() const; @@ -43,6 +71,9 @@ class NashChoiceDialog final : public wxDialog { std::shared_ptr GetCommand() const; bool UseStrategic() const; + + /// The goal and explicitly picked variant, for the document to remember. + NashChoice GetChoice() const { return {GetGoal(), m_userChoice}; } }; } // namespace Gambit::GUI diff --git a/src/gui/dlnashmon.cc b/src/gui/dlnashmon.cc index 091439fbd..2a3699f69 100644 --- a/src/gui/dlnashmon.cc +++ b/src/gui/dlnashmon.cc @@ -361,8 +361,8 @@ class NashMonitorDialog final : public wxDialog { NashMonitorDialog::NashMonitorDialog(wxWindow *p_parent, const std::shared_ptr &p_doc, const std::shared_ptr &p_command) - : wxDialog(p_parent, wxID_ANY, wxT("Computing Nash equilibria"), wxDefaultPosition, - wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), + : wxDialog(p_parent, wxID_ANY, wxT("Computing equilibria"), wxDefaultPosition, wxDefaultSize, + wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), m_doc(p_doc), m_output(p_command) { const int S = FromDIP(5); diff --git a/src/gui/gamedoc.h b/src/gui/gamedoc.h index 511396343..de4a9b151 100644 --- a/src/gui/gamedoc.h +++ b/src/gui/gamedoc.h @@ -29,6 +29,7 @@ #include "games/stratspt.h" #include "style.h" #include "analysis.h" +#include "nashvariants.h" namespace Gambit::GUI { @@ -229,6 +230,7 @@ class GameDocument { bool m_gameModified, m_workspaceModified; AnalysisWorkspace m_workspace; + NashChoice m_nashChoice; std::list m_undoList, m_redoList; @@ -277,6 +279,10 @@ class GameDocument { const AnalysisWorkspace &GetWorkspace() const { return m_workspace; } + /// The last choices made in the equilibrium dialog; held for the session only. + const NashChoice &GetNashChoice() const { return m_nashChoice; } + void SetNashChoice(const NashChoice &p_choice) { m_nashChoice = p_choice; } + //! //! @name Undo/redo //! diff --git a/src/gui/gameframe.cc b/src/gui/gameframe.cc index 0144a8a22..f4b434f17 100644 --- a/src/gui/gameframe.cc +++ b/src/gui/gameframe.cc @@ -1082,6 +1082,7 @@ void GameFrame::OnToolsEquilibrium(wxCommandEvent &) } } + m_doc->SetNashChoice(dialog.GetChoice()); ShowNashMonitorDialog(this, m_doc, dialog.GetCommand()); if (!m_splitter->IsSplit()) { diff --git a/src/gui/nashspec.cc b/src/gui/nashspec.cc index 54be81996..9e5b49740 100644 --- a/src/gui/nashspec.cc +++ b/src/gui/nashspec.cc @@ -36,8 +36,20 @@ namespace Gambit::GUI { -std::optional EnumPureNashSpec::MakeSolver(NashRepresentation) const +std::optional +EnumPureNashSpec::MakeSolver(NashRepresentation p_representation) const { + if (p_representation == NashRepresentation::Behavior) { + return [](const Game &p_game, const ProfileFoundCallback &p_callback, + const CancelToken &p_cancel) { + Nash::EnumPureAgentSolve( + p_game, + [&p_callback](const MixedBehaviorProfile &p) { + p_callback(ComputedProfile(p)); + }, + p_cancel); + }; + } return [](const Game &p_game, const ProfileFoundCallback &p_callback, const CancelToken &p_cancel) { Nash::EnumPureStrategySolve( @@ -171,9 +183,23 @@ std::optional LCPNashSpec::MakeSolver(NashRepresentation p_repre }; } -std::optional LiapNashSpec::MakeSolver(NashRepresentation) const +std::optional LiapNashSpec::MakeSolver(NashRepresentation p_representation) const { const LiapNashSpec spec = *this; + if (p_representation == NashRepresentation::Behavior) { + return [spec](const Game &p_game, const ProfileFoundCallback &p_callback, + const CancelToken &p_cancel) { + for (const auto &start : NewRandomBehaviorProfiles(p_game, spec.startingPoints)) { + p_cancel.Check(); + Nash::LiapAgentSolve( + start, spec.maxRegret, spec.maxIterations, + [&p_callback](const MixedBehaviorProfile &p) { + p_callback(ComputedProfile(p)); + }, + Nash::NullLiapEventCallback>, p_cancel); + } + }; + } return [spec](const Game &p_game, const ProfileFoundCallback &p_callback, const CancelToken &p_cancel) { for (const auto &start : NewRandomStrategyProfiles(p_game, spec.startingPoints)) { diff --git a/src/gui/nashspec.h b/src/gui/nashspec.h index d138cf9f8..0f9ff8004 100644 --- a/src/gui/nashspec.h +++ b/src/gui/nashspec.h @@ -154,6 +154,8 @@ struct NashComputationSpec { NashEquilibriumTarget target; NashMethodSpec method; bool selectedByRecommendation{false}; + /// Whether the computation solves for agent Nash rather than Nash equilibria. + bool agentNash{false}; /// Build a function that runs this computation in-process on a worker /// thread, or std::nullopt if this method/representation combination diff --git a/src/gui/nashvariants.cc b/src/gui/nashvariants.cc new file mode 100644 index 000000000..93c2576f8 --- /dev/null +++ b/src/gui/nashvariants.cc @@ -0,0 +1,143 @@ +// +// This file is part of Gambit +// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org) +// +// FILE: src/gui/nashvariants.cc +// Catalog of equilibrium-computation variants +// +// 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 +#include +#include + +#include "games/gametree.h" + +#include "nashvariants.h" + +namespace Gambit::GUI { + +namespace { + +using enum NashMethodId; +using enum VariantRepresentation; +using enum VariantCoverage; +using enum VariantReliability; +using enum VariantConcept; +using enum VariantArithmetic; +using enum VariantScope; + +constexpr std::array s_variants{{ + {EnumMixed, "enummixed", "strategic", Strategic, All, Always, Nash, Exact, TwoPlayer}, + {EnumPoly, "enumpoly", "sequence form", Behavior, Some, Usually, Nash, FloatingPoint, AnyGame}, + {EnumPoly, "enumpoly", "strategic", Strategic, Some, Usually, Nash, FloatingPoint, AnyGame}, + {EnumPure, "enumpure", "agent form", Behavior, AllPure, Always, AgentNash, Exact, AnyGame}, + {EnumPure, "enumpure", "strategic", Strategic, AllPure, Always, Nash, Exact, AnyGame}, + {GNM, "gnm", "strategic", Strategic, Some, Usually, Nash, FloatingPoint, AnyGame}, + {HP, "hp", "strategic", Strategic, Some, Usually, Nash, FloatingPoint, AnyGame}, + {IPA, "ipa", "strategic", Strategic, Some, NoGuarantee, Nash, FloatingPoint, AnyGame}, + {LCP, "lcp", "Lemke (sequence form)", Behavior, One, Always, Nash, Exact, TwoPlayer}, + {LCP, "lcp", "Lemke-Howson (strategic)", Strategic, Some, Always, Nash, Exact, TwoPlayer}, + {Liap, "liap", "agent form", Behavior, Some, NoGuarantee, AgentNash, FloatingPoint, AnyGame}, + {Liap, "liap", "strategic", Strategic, Some, NoGuarantee, Nash, FloatingPoint, AnyGame}, + {Logit, "logit", "agent form", Behavior, One, Usually, Nash, FloatingPoint, AnyGame}, + {Logit, "logit", "strategic", Strategic, One, Usually, Nash, FloatingPoint, AnyGame}, + {LP, "lp", "sequence form", Behavior, One, Always, Nash, Exact, TwoPlayerConstSum}, + {LP, "lp", "strategic", Strategic, One, Always, Nash, Exact, TwoPlayerConstSum}, + {Simpdiv, "simpdiv", "strategic", Strategic, Some, Always, Nash, Exact, AnyGame}, +}}; + +} // namespace + +std::span NashVariants() { return s_variants; } + +std::vector NashVariantsOf(NashMethodId p_method) +{ + std::vector result; + for (const auto &variant : s_variants) { + if (variant.method == p_method) { + result.push_back(&variant); + } + } + return result; +} + +bool MeetsGoal(const NashVariant &p_variant, NashGoal p_goal) +{ + switch (p_goal) { + case NashGoal::One: + return p_variant.coverage != AllPure; + case NashGoal::Several: + return p_variant.coverage != One; + case NashGoal::All: + return p_variant.coverage == All; + } + throw std::logic_error("Unknown goal"); +} + +std::optional UnavailableReason(const NashVariant &p_variant, const Game &p_game) +{ + if (p_variant.representation == Behavior && !As(p_game)) { + return "needs a game tree"; + } + switch (p_variant.scope) { + case TwoPlayer: + if (p_game->NumPlayers() != 2) { + return "two players only"; + } + break; + case TwoPlayerConstSum: + if (p_game->NumPlayers() != 2 || !p_game->IsConstSum()) { + return "two-player constant-sum games only"; + } + break; + case AnyGame: + break; + } + return std::nullopt; +} + +NashMethodId RecommendedMethod(NashGoal p_goal, const Game &p_game) +{ + switch (p_goal) { + case NashGoal::One: + return p_game->NumPlayers() == 2 && p_game->IsConstSum() ? LP : Logit; + case NashGoal::Several: + return p_game->NumPlayers() == 2 ? LCP : Simpdiv; + case NashGoal::All: + return EnumMixed; + } + throw std::logic_error("Unknown goal"); +} + +const NashVariant &RecommendedVariant(NashGoal p_goal, const Game &p_game) +{ + const auto candidates = NashVariantsOf(RecommendedMethod(p_goal, p_game)); + const NashVariant *best = nullptr; + for (const auto *variant : candidates) { + if (UnavailableReason(*variant, p_game) || !MeetsGoal(*variant, p_goal)) { + continue; + } + if (!best || variant->representation == Behavior) { + best = variant; + } + } + if (!best) { + throw std::logic_error("No variant of the recommended method meets the goal"); + } + return *best; +} + +} // namespace Gambit::GUI diff --git a/src/gui/nashvariants.h b/src/gui/nashvariants.h new file mode 100644 index 000000000..df104072a --- /dev/null +++ b/src/gui/nashvariants.h @@ -0,0 +1,99 @@ +// +// This file is part of Gambit +// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org) +// +// FILE: src/gui/nashvariants.h +// Catalog of equilibrium-computation variants +// +// 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 GAMBIT_GUI_NASHVARIANTS_H +#define GAMBIT_GUI_NASHVARIANTS_H + +#include +#include +#include +#include +#include + +#include "games.h" + +namespace Gambit::GUI { + +enum class NashGoal { One, Several, All }; + +enum class NashMethodId { + EnumMixed, + EnumPoly, + EnumPure, + GNM, + HP, + IPA, + LCP, + Liap, + Logit, + LP, + Simpdiv +}; + +enum class VariantRepresentation { Strategic, Behavior }; +enum class VariantCoverage { One, Some, AllPure, All }; +enum class VariantReliability { Always, Usually, NoGuarantee }; +enum class VariantConcept { Nash, AgentNash }; +enum class VariantArithmetic { Exact, FloatingPoint }; +enum class VariantScope { AnyGame, TwoPlayer, TwoPlayerConstSum }; + +/// A method applied in one representation of the game. +struct NashVariant { + NashMethodId method; + std::string_view methodName; + std::string_view name; + VariantRepresentation representation; + VariantCoverage coverage; + VariantReliability reliability; + VariantConcept solves; + VariantArithmetic arithmetic; + VariantScope scope; +}; + +/// The user's last choices in the equilibrium dialog. +struct NashChoice { + NashGoal goal{NashGoal::One}; + /// The variant explicitly picked; null if the selection followed the recommendation. + const NashVariant *variant{nullptr}; +}; + +/// Every variant, in alphabetical order by method. +std::span NashVariants(); + +/// The variants of one method, in catalog order. +std::vector NashVariantsOf(NashMethodId p_method); + +/// Whether the variant is expected to return what the goal asks for. +bool MeetsGoal(const NashVariant &p_variant, NashGoal p_goal); + +/// The reason the variant cannot run on the game, if it cannot. +std::optional UnavailableReason(const NashVariant &p_variant, const Game &p_game); + +/// The method Gambit recommends for the goal on the game. +NashMethodId RecommendedMethod(NashGoal p_goal, const Game &p_game); + +/// The recommended method's variant that runs on the game and meets the goal, +/// preferring the extensive-game variant. +const NashVariant &RecommendedVariant(NashGoal p_goal, const Game &p_game); + +} // namespace Gambit::GUI + +#endif // GAMBIT_GUI_NASHVARIANTS_H