From 712712ec21344e7d4bb5f276104f306bc2c6e026 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Fri, 16 Oct 2026 21:26:10 +0200 Subject: [PATCH 01/12] Make recommended "some" equilibria on 2-player games run Lemke-Howson "Some" equilibria was still recommending Lemke for 2-player games in extensive form, even though we previously confirmed in c76441f18 that Lemke only supports a single equilibrium in its current implementation. --- src/gui/dlnash.cc | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/gui/dlnash.cc b/src/gui/dlnash.cc index 91ccd32d18..27cbccfa39 100644 --- a/src/gui/dlnash.cc +++ b/src/gui/dlnash.cc @@ -139,6 +139,12 @@ bool RequiresStrategicRepresentation(const NashMethodSpec &p_method) 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; }, @@ -426,7 +432,8 @@ void NashChoiceDialog::UpdateRepresentationChoice() } const auto method = ResolveMethod(m_methodChoice->GetStringSelection(), GetTarget(m_countChoice->GetSelection()), m_doc->GetGame()); - if (RequiresStrategicRepresentation(method)) { + if (RequiresStrategicRepresentation(method, + m_methodChoice->GetStringSelection() == s_recommended)) { m_repChoice->SetSelection(1); m_repChoice->Enable(false); } @@ -444,12 +451,12 @@ NashComputationSpec NashChoiceDialog::GetComputation() const { const auto target = GetTarget(m_countChoice->GetSelection()); auto method = ResolveMethod(m_methodChoice->GetStringSelection(), target, m_doc->GetGame()); - const auto representation = - !m_repChoice || m_repChoice->GetSelection() == 1 || RequiresStrategicRepresentation(method) - ? NashRepresentation::Strategic - : NashRepresentation::Behavior; - return {representation, target, std::move(method), - m_methodChoice->GetStringSelection() == s_recommended}; + 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}; } std::shared_ptr NashChoiceDialog::GetCommand() const From 1f4291091defab31ff7767bc5f78eeb70c8a2fb2 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Mon, 19 Oct 2026 11:32:34 +0100 Subject: [PATCH 02/12] Add inventory of equilibrium-finding variant algorithms as a data structure. --- Makefile.am | 2 + src/gui/dlnash.cc | 271 ++++++++++++++++++++++------------------ src/gui/dlnash.h | 9 ++ src/gui/nashvariants.cc | 167 +++++++++++++++++++++++++ src/gui/nashvariants.h | 93 ++++++++++++++ 5 files changed, 420 insertions(+), 122 deletions(-) create mode 100644 src/gui/nashvariants.cc create mode 100644 src/gui/nashvariants.h diff --git a/Makefile.am b/Makefile.am index e843c4df99..b4761162f0 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/src/gui/dlnash.cc b/src/gui/dlnash.cc index 27cbccfa39..7fc4851044 100644 --- a/src/gui/dlnash.cc +++ b/src/gui/dlnash.cc @@ -24,6 +24,7 @@ #include #include #include +#include #ifndef WX_PRECOMP #include @@ -37,84 +38,121 @@ 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) +NashGoal GoalFromSelection(int p_selection) { if (p_selection == 0) { - return NashEquilibriumTarget::One; + return NashGoal::One; } if (p_selection == 1) { + return NashGoal::Several; + } + return NashGoal::All; +} + +NashEquilibriumTarget GetTarget(NashGoal p_goal) +{ + switch (p_goal) { + case NashGoal::One: + return NashEquilibriumTarget::One; + 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) +wxString MethodLabel(NashMethodId p_method) { - if (p_method == s_recommended) { - if (p_target == NashEquilibriumTarget::One) { - if (p_game->NumPlayers() == 2 && p_game->IsConstSum()) { - return LPNashSpec{}; - } - return LogitNashSpec{}; - } - if (p_target == NashEquilibriumTarget::Some) { - if (p_game->NumPlayers() == 2) { - return LCPNashSpec{}; - } - return SimpdivNashSpec{}; - } - return EnumMixedNashSpec{}; + switch (p_method) { + case NashMethodId::EnumMixed: + return wxT("by enumerating extreme points"); + case NashMethodId::EnumPoly: + return wxT("by solving systems of polynomial equations"); + case NashMethodId::EnumPure: + return wxT("by looking for pure strategy equilibria"); + case NashMethodId::GNM: + return wxT("by global Newton tracing"); + case NashMethodId::HP: + return wxT("by the Herings-Peeters homotopy"); + case NashMethodId::IPA: + return wxT("by iterated polymatrix approximation"); + case NashMethodId::LCP: + return wxT("by solving a linear complementarity program"); + case NashMethodId::Liap: + return wxT("by minimizing the Lyapunov function"); + case NashMethodId::Logit: + return wxT("by tracing logit equilibria"); + case NashMethodId::LP: + return wxT("by solving a linear program"); + case NashMethodId::Simpdiv: + return wxT("by simplicial subdivision"); } - if (p_method == s_enumpure) { - return EnumPureNashSpec{}; + throw std::logic_error("Unknown Nash equilibrium method"); +} + +// The methods the dialog lists under each goal. +std::vector ListedMethods(NashGoal p_goal) +{ + using enum NashMethodId; + switch (p_goal) { + case NashGoal::One: + return {LP, Simpdiv, Logit, EnumPoly}; + case NashGoal::Several: + return {LCP, EnumPure, Liap, GNM, IPA, HP, EnumPoly}; + default: + return {EnumMixed}; } - if (p_method == s_enummixed) { - return EnumMixedNashSpec{}; +} + +// Agent-form liap and enumpure are not yet offered. +bool IsOffered(const NashVariant &p_variant) +{ + return p_variant.representation == VariantRepresentation::Strategic || + (p_variant.method != NashMethodId::Liap && p_variant.method != NashMethodId::EnumPure); +} + +std::vector RunnableVariants(NashMethodId p_method, const Game &p_game) +{ + std::vector result; + for (const auto *variant : NashVariantsOf(p_method)) { + if (IsOffered(*variant) && !UnavailableReason(*variant, p_game)) { + result.push_back(variant); + } } - if (p_method == s_enumpoly) { - if (p_target == NashEquilibriumTarget::One) { + return result; +} + +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 +160,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; }, @@ -349,17 +369,7 @@ NashChoiceDialog::NashChoiceDialog(wxWindow *p_parent, const std::shared_ptrAdd(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); + m_methodChoice = new wxChoice(this, wxID_ANY); topSizer->Add(m_methodChoice, 0, wxALL | wxEXPAND, S); if (As(m_doc->GetGame())) { @@ -377,7 +387,7 @@ NashChoiceDialog::NashChoiceDialog(wxWindow *p_parent, const std::shared_ptrAdd(buttonSizer, 0, wxALL | wxEXPAND, S); @@ -390,51 +400,59 @@ NashChoiceDialog::NashChoiceDialog(wxWindow *p_parent, const std::shared_ptrGetSelection()); +} + +void NashChoiceDialog::OnCount(wxCommandEvent &) { UpdateMethodChoice(); } + +void NashChoiceDialog::OnMethod(wxCommandEvent &) { UpdateRepresentationChoice(); } + +void NashChoiceDialog::UpdateMethodChoice() { + const auto game = m_doc->GetGame(); m_methodChoice->Clear(); - m_methodChoice->Append(s_recommended); + m_methods.clear(); - if (p_event.GetSelection() == 0) { - if (m_doc->GetGame()->NumPlayers() == 2 && m_doc->GetGame()->IsConstSum()) { - m_methodChoice->Append(s_lp); - } - 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); - } - 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); + m_methodChoice->Append(s_recommended); + m_methods.emplace_back(std::nullopt); + for (const auto method : ListedMethods(GetGoal())) { + if (!RunnableVariants(method, game).empty()) { + m_methodChoice->Append(MethodLabel(method)); + m_methods.emplace_back(method); } } m_methodChoice->SetSelection(0); UpdateRepresentationChoice(); } -void NashChoiceDialog::OnMethod(wxCommandEvent &) { UpdateRepresentationChoice(); } +std::vector NashChoiceDialog::Candidates() const +{ + const auto game = m_doc->GetGame(); + const auto goal = GetGoal(); + const auto &method = m_methods.at(m_methodChoice->GetSelection()); + if (!method) { + std::vector result; + for (const auto *variant : RunnableVariants(RecommendedMethod(goal, game), game)) { + if (MeetsGoal(*variant, goal)) { + result.push_back(variant); + } + } + return result; + } + return RunnableVariants(*method, game); +} void NashChoiceDialog::UpdateRepresentationChoice() { if (!m_repChoice) { return; } - 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); + const auto candidates = Candidates(); + if (candidates.size() == 1) { + m_repChoice->SetSelection( + candidates.front()->representation == VariantRepresentation::Strategic ? 1 : 0); m_repChoice->Enable(false); } else { @@ -449,14 +467,23 @@ 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 &chosen = m_methods.at(m_methodChoice->GetSelection()); + const bool recommended = !chosen.has_value(); + const auto candidates = Candidates(); + const auto methodId = recommended ? RecommendedMethod(goal, m_doc->GetGame()) : *chosen; + + VariantRepresentation representation; + if (candidates.size() == 1) { + representation = candidates.front()->representation; + } + else { + representation = m_repChoice->GetSelection() == 0 ? VariantRepresentation::Behavior + : VariantRepresentation::Strategic; + } + return {representation == VariantRepresentation::Strategic ? NashRepresentation::Strategic + : NashRepresentation::Behavior, + GetTarget(goal), ResolveMethod(methodId, goal), recommended}; } std::shared_ptr NashChoiceDialog::GetCommand() const diff --git a/src/gui/dlnash.h b/src/gui/dlnash.h index 71e99c6559..485d590d34 100644 --- a/src/gui/dlnash.h +++ b/src/gui/dlnash.h @@ -23,18 +23,27 @@ #ifndef GAMBIT_GUI_DLNASH_H #define GAMBIT_GUI_DLNASH_H +#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; + std::vector> m_methods; // Event handlers void OnCount(wxCommandEvent &); void OnMethod(wxCommandEvent &); + void UpdateMethodChoice(); void UpdateRepresentationChoice(); + NashGoal GetGoal() const; + std::vector Candidates() const; + NashComputationSpec GetComputation() const; public: diff --git a/src/gui/nashvariants.cc b/src/gui/nashvariants.cc new file mode 100644 index 0000000000..a0bf14008f --- /dev/null +++ b/src/gui/nashvariants.cc @@ -0,0 +1,167 @@ +// +// 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, + "Returns every extreme equilibrium; connected components of equilibria are described by " + "their extreme points."}, + {EnumPoly, "enumpoly", "sequence form", Behavior, Some, NumericalFailurePossible, Nash, + FloatingPoint, AnyGame, + "Finds isolated equilibria only; supports it cannot resolve are skipped. May find " + "equilibria that the strategic variant does not."}, + {EnumPoly, "enumpoly", "strategic", Strategic, Some, NumericalFailurePossible, Nash, + FloatingPoint, AnyGame, + "Finds isolated equilibria only; supports it cannot resolve are skipped. May find " + "equilibria that the sequence-form variant does not."}, + {EnumPure, "enumpure", "agent form", Behavior, AllPure, Always, AgentNash, Exact, AnyGame, + "Returns all pure-strategy agent Nash equilibria, which need not be Nash equilibria and " + "may not exist."}, + {EnumPure, "enumpure", "strategic", Strategic, AllPure, Always, Nash, Exact, AnyGame, + "Returns all pure-strategy equilibria, which may not exist."}, + {GNM, "gnm", "strategic", Strategic, Some, NumericalFailurePossible, Nash, FloatingPoint, + AnyGame, "Less thoroughly tested than simplicial subdivision and can fail numerically."}, + {HP, "hp", "strategic", Strategic, Some, NumericalFailurePossible, Nash, FloatingPoint, + AnyGame, "Returns at most one equilibrium per random prior."}, + {IPA, "ipa", "strategic", Strategic, Some, NoGuarantee, Nash, FloatingPoint, AnyGame, + "Convergence is not known to be guaranteed."}, + {LCP, "lcp", "Lemke (sequence form)", Behavior, One, Always, Nash, Exact, TwoPlayer, + "Returns a single equilibrium."}, + {LCP, "lcp", "Lemke-Howson (strategic)", Strategic, Some, Always, Nash, Exact, TwoPlayer, + "Finds only the accessible equilibria (those reachable by the Lemke-Howson path)."}, + {Liap, "liap", "agent form", Behavior, Some, NoGuarantee, AgentNash, FloatingPoint, AnyGame, + "Local descent from random starting points. The result is an agent Nash equilibrium, " + "which need not be a Nash equilibrium."}, + {Liap, "liap", "strategic", Strategic, Some, NoGuarantee, Nash, FloatingPoint, AnyGame, + "Local descent from random starting points; may converge to a non-equilibrium."}, + {Logit, "logit", "agent form", Behavior, One, NumericalFailurePossible, Nash, FloatingPoint, + AnyGame, "Traces the agent quantal response equilibrium; its limit is a Nash equilibrium."}, + {Logit, "logit", "strategic", Strategic, One, NumericalFailurePossible, Nash, FloatingPoint, + AnyGame, "Traces the quantal response equilibrium; its limit is a Nash equilibrium."}, + {LP, "lp", "sequence form", Behavior, One, Always, Nash, Exact, TwoPlayerConstSum, + "May return a different equilibrium from the strategic variant."}, + {LP, "lp", "strategic", Strategic, One, Always, Nash, Exact, TwoPlayerConstSum, + "May return a different equilibrium from the sequence-form variant."}, + {Simpdiv, "simpdiv", "strategic", Strategic, Some, Always, Nash, Exact, AnyGame, + "Returns an approximate equilibrium to within the stated regret."}, +}}; + +} // 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 0000000000..23e97c711e --- /dev/null +++ b/src/gui/nashvariants.h @@ -0,0 +1,93 @@ +// +// 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, NumericalFailurePossible, 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; + std::string_view caveat; +}; + +/// 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 From 3325ed958d71768ec6f3b1056e9632061108252c Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Tue, 20 Oct 2026 15:24:18 +0100 Subject: [PATCH 03/12] Replace equilibrium dialog drop-downs with a goal control and variant list --- src/gui/dlnash.cc | 411 +++++++++++++++++++++++++++++++--------------- src/gui/dlnash.h | 31 +++- 2 files changed, 304 insertions(+), 138 deletions(-) diff --git a/src/gui/dlnash.cc b/src/gui/dlnash.cc index 7fc4851044..22600614fc 100644 --- a/src/gui/dlnash.cc +++ b/src/gui/dlnash.cc @@ -20,15 +20,22 @@ // 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 "games.h" @@ -37,21 +44,9 @@ #include "dlnash.h" namespace Gambit::GUI { -static wxString s_recommended(wxT("with Gambit's recommended method")); namespace { -NashGoal GoalFromSelection(int p_selection) -{ - if (p_selection == 0) { - return NashGoal::One; - } - if (p_selection == 1) { - return NashGoal::Several; - } - return NashGoal::All; -} - NashEquilibriumTarget GetTarget(NashGoal p_goal) { switch (p_goal) { @@ -64,67 +59,129 @@ NashEquilibriumTarget GetTarget(NashGoal p_goal) } } -wxString MethodLabel(NashMethodId p_method) +// Agent-form liap and enumpure are not yet offered. +bool IsOffered(const NashVariant &p_variant) { - switch (p_method) { + return p_variant.representation == VariantRepresentation::Strategic || + (p_variant.method != NashMethodId::Liap && p_variant.method != NashMethodId::EnumPure); +} + +std::vector OfferedVariants(const Game &p_game) +{ + std::vector result; + for (const auto &variant : NashVariants()) { + if (IsOffered(variant) && + (variant.representation == VariantRepresentation::Strategic || As(p_game))) { + result.push_back(&variant); + } + } + 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("by enumerating extreme points"); + return wxT("Enumerate extreme points") + form; case NashMethodId::EnumPoly: - return wxT("by solving systems of polynomial equations"); + return wxT("Solve systems of polynomial equations") + form; case NashMethodId::EnumPure: - return wxT("by looking for pure strategy equilibria"); + return wxT("Look for pure strategy equilibria") + + (strategic ? form : wxString(wxT(" on the agent form"))); case NashMethodId::GNM: - return wxT("by global Newton tracing"); + return wxT("Global Newton tracing") + form; case NashMethodId::HP: - return wxT("by the Herings-Peeters homotopy"); + return wxT("Herings-Peeters homotopy") + form; case NashMethodId::IPA: - return wxT("by iterated polymatrix approximation"); + return wxT("Iterated polymatrix approximation") + form; case NashMethodId::LCP: - return wxT("by solving a linear complementarity program"); + return strategic ? wxString(wxT("Lemke-Howson linear complementarity") + form) + : wxString(wxT("Lemke linear complementarity") + form); case NashMethodId::Liap: - return wxT("by minimizing the Lyapunov function"); + return wxT("Minimize the Lyapunov function") + + (strategic ? form : wxString(wxT(" on the agent form"))); case NashMethodId::Logit: - return wxT("by tracing logit equilibria"); + return strategic ? wxString(wxT("Trace logit quantal response equilibria")) + : wxString(wxT("Trace agent logit quantal response equilibria")); case NashMethodId::LP: - return wxT("by solving a linear program"); + return wxT("Solve a linear program") + form; case NashMethodId::Simpdiv: - return wxT("by simplicial subdivision"); + return wxT("Simplicial subdivision") + form; } throw std::logic_error("Unknown Nash equilibrium method"); } -// The methods the dialog lists under each goal. -std::vector ListedMethods(NashGoal p_goal) +wxString FromStd(std::string_view p_text) +{ + return wxString::FromUTF8(p_text.data(), p_text.size()); +} + +wxString GoalName(NashGoal p_goal) { - using enum NashMethodId; switch (p_goal) { case NashGoal::One: - return {LP, Simpdiv, Logit, EnumPoly}; + return wxT("One"); case NashGoal::Several: - return {LCP, EnumPure, Liap, GNM, IPA, HP, EnumPoly}; + return wxT("Several"); default: - return {EnumMixed}; + return wxT("All"); } } -// Agent-form liap and enumpure are not yet offered. -bool IsOffered(const NashVariant &p_variant) +wxString CoverageLabel(VariantCoverage p_coverage) { - return p_variant.representation == VariantRepresentation::Strategic || - (p_variant.method != NashMethodId::Liap && p_variant.method != NashMethodId::EnumPure); + 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"); + } } -std::vector RunnableVariants(NashMethodId p_method, const Game &p_game) +wxString ReliabilityLabel(VariantReliability p_reliability) { - std::vector result; - for (const auto *variant : NashVariantsOf(p_method)) { - if (IsOffered(*variant) && !UnavailableReason(*variant, p_game)) { - result.push_back(variant); - } + switch (p_reliability) { + case VariantReliability::Always: + return wxT("Always"); + case VariantReliability::NumericalFailurePossible: + return wxT("Numerical failure possible"); + default: + return wxT("No guarantee"); } - return result; } +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; + } + 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) { @@ -341,58 +398,79 @@ wxString ParameterDescription(const NashMethodSpec &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, 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); + + 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); }); } - m_countChoice->SetSelection(0); - Connect(m_countChoice->GetId(), wxEVT_COMMAND_CHOICE_SELECTED, - wxCommandEventHandler(NashChoiceDialog::OnCount)); - topSizer->Add(m_countChoice, 0, wxALL | wxEXPAND, S); + 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); - m_methodChoice = new wxChoice(this, wxID_ANY); - topSizer->Add(m_methodChoice, 0, wxALL | 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); - 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; - } - - UpdateMethodChoice(); + m_warning = new wxStaticText(this, wxID_ANY, wxT(" ")); + topSizer->Add(m_warning, 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); @@ -402,62 +480,148 @@ NashChoiceDialog::NashChoiceDialog(wxWindow *p_parent, const std::shared_ptrGetSelection()); + if (m_goalButtons[2]->GetValue()) { + return NashGoal::All; + } + return m_goalButtons[1]->GetValue() ? NashGoal::Several : NashGoal::One; } -void NashChoiceDialog::OnCount(wxCommandEvent &) { UpdateMethodChoice(); } +const NashVariant &NashChoiceDialog::GetSelectedVariant() const +{ + return m_userChoice ? *m_userChoice : RecommendedVariant(GetGoal(), m_doc->GetGame()); +} -void NashChoiceDialog::OnMethod(wxCommandEvent &) { UpdateRepresentationChoice(); } +void NashChoiceDialog::OnGoal(wxCommandEvent &) { UpdateRecommendation(); } -void NashChoiceDialog::UpdateMethodChoice() +void NashChoiceDialog::OnItemSelected(wxListEvent &p_event) { - const auto game = m_doc->GetGame(); - m_methodChoice->Clear(); - m_methods.clear(); - - m_methodChoice->Append(s_recommended); - m_methods.emplace_back(std::nullopt); - for (const auto method : ListedMethods(GetGoal())) { - if (!RunnableVariants(method, game).empty()) { - m_methodChoice->Append(MethodLabel(method)); - m_methods.emplace_back(method); + if (m_refreshing) { + return; + } + const auto *variant = m_rows.at(p_event.GetIndex()); + if (UnavailableReason(*variant, m_doc->GetGame())) { + SelectRow(GetSelectedVariant()); + m_warning->SetLabel(VariantLabel(*variant) + wxT(" is unavailable: ") + + FromStd(*UnavailableReason(*variant, m_doc->GetGame())) + wxT(".")); + return; + } + m_userChoice = variant; + UpdateWarning(); +} + +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); + } + } + if (tip != m_tip) { + m_tip = tip; + p_window->UnsetToolTip(); + if (!tip.empty()) { + p_window->SetToolTip(tip); } } - m_methodChoice->SetSelection(0); - UpdateRepresentationChoice(); } -std::vector NashChoiceDialog::Candidates() const +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_refreshing = wasRefreshing; +} + +void NashChoiceDialog::RefreshList() { const auto game = m_doc->GetGame(); const auto goal = GetGoal(); - const auto &method = m_methods.at(m_methodChoice->GetSelection()); - if (!method) { - std::vector result; - for (const auto *variant : RunnableVariants(RecommendedMethod(goal, game), game)) { - if (MeetsGoal(*variant, goal)) { - result.push_back(variant); + 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); + }); + } + + 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); } - return result; } - return RunnableVariants(*method, game); + m_refreshing = false; + SelectRow(m_userChoice ? *m_userChoice : *recommended); + UpdateWarning(); } -void NashChoiceDialog::UpdateRepresentationChoice() +void NashChoiceDialog::UpdateRecommendation() { - if (!m_repChoice) { - return; - } - const auto candidates = Candidates(); - if (candidates.size() == 1) { - m_repChoice->SetSelection( - candidates.front()->representation == VariantRepresentation::Strategic ? 1 : 0); - m_repChoice->Enable(false); - } - else { - m_repChoice->Enable(true); + 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{}); } + SelectRow(m_userChoice ? *m_userChoice : *recommended); + UpdateWarning(); +} + +void NashChoiceDialog::UpdateWarning() +{ + const auto goal = GetGoal(); + const auto &variant = GetSelectedVariant(); + m_warning->SetLabel(MeetsGoal(variant, goal) ? wxString(wxT(" ")) + : GoalMissWarning(variant, goal)); } bool NashChoiceDialog::UseStrategic() const @@ -468,22 +632,11 @@ bool NashChoiceDialog::UseStrategic() const NashComputationSpec NashChoiceDialog::GetComputation() const { const auto goal = GetGoal(); - const auto &chosen = m_methods.at(m_methodChoice->GetSelection()); - const bool recommended = !chosen.has_value(); - const auto candidates = Candidates(); - const auto methodId = recommended ? RecommendedMethod(goal, m_doc->GetGame()) : *chosen; - - VariantRepresentation representation; - if (candidates.size() == 1) { - representation = candidates.front()->representation; - } - else { - representation = m_repChoice->GetSelection() == 0 ? VariantRepresentation::Behavior - : VariantRepresentation::Strategic; - } - return {representation == VariantRepresentation::Strategic ? NashRepresentation::Strategic - : NashRepresentation::Behavior, - GetTarget(goal), ResolveMethod(methodId, goal), recommended}; + const auto &variant = GetSelectedVariant(); + return {variant.representation == VariantRepresentation::Strategic + ? NashRepresentation::Strategic + : NashRepresentation::Behavior, + GetTarget(goal), ResolveMethod(variant.method, goal), m_userChoice == nullptr}; } std::shared_ptr NashChoiceDialog::GetCommand() const diff --git a/src/gui/dlnash.h b/src/gui/dlnash.h index 485d590d34..a26bd59706 100644 --- a/src/gui/dlnash.h +++ b/src/gui/dlnash.h @@ -23,26 +23,39 @@ #ifndef GAMBIT_GUI_DLNASH_H #define GAMBIT_GUI_DLNASH_H -#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; - std::vector> m_methods; + wxRadioButton *m_goalButtons[3]; + wxStaticText *m_goalNote, *m_warning; + 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 UpdateMethodChoice(); - void UpdateRepresentationChoice(); + void OnGoal(wxCommandEvent &); + void OnItemSelected(wxListEvent &); + void OnColumnClick(wxListEvent &); + void OnListMotion(wxWindow *, wxMouseEvent &); + void RefreshList(); + void UpdateRecommendation(); + void SelectRow(const NashVariant &); + void UpdateWarning(); NashGoal GetGoal() const; - std::vector Candidates() const; + const NashVariant &GetSelectedVariant() const; NashComputationSpec GetComputation() const; From 14ab413cb77ba180655afce8710828244b9cbe58 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Tue, 20 Oct 2026 21:51:34 +0100 Subject: [PATCH 04/12] Add detail pane to the equilibrium dialog --- src/gui/dlnash.cc | 43 +++++++++++++++++++++++++++++++------------ src/gui/dlnash.h | 8 ++++++-- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/gui/dlnash.cc b/src/gui/dlnash.cc index 22600614fc..1b413bf82c 100644 --- a/src/gui/dlnash.cc +++ b/src/gui/dlnash.cc @@ -459,8 +459,10 @@ NashChoiceDialog::NashChoiceDialog(wxWindow *p_parent, const std::shared_ptrAdd(legend, 0, wxLEFT | wxRIGHT | wxTOP | wxEXPAND, S); - m_warning = new wxStaticText(this, wxID_ANY, wxT(" ")); - topSizer->Add(m_warning, 0, wxALL | wxEXPAND, S); + m_detail = + new wxTextCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(-1, FromDIP(130)), + wxTE_MULTILINE | wxTE_READONLY | wxTE_BESTWRAP); + topSizer->Add(m_detail, 0, wxALL | wxEXPAND, S); if (auto *buttonSizer = CreateStdDialogButtonSizer(wxOK | wxCANCEL)) { topSizer->Add(buttonSizer, 0, wxALL | wxEXPAND, S); @@ -501,12 +503,10 @@ void NashChoiceDialog::OnItemSelected(wxListEvent &p_event) const auto *variant = m_rows.at(p_event.GetIndex()); if (UnavailableReason(*variant, m_doc->GetGame())) { SelectRow(GetSelectedVariant()); - m_warning->SetLabel(VariantLabel(*variant) + wxT(" is unavailable: ") + - FromStd(*UnavailableReason(*variant, m_doc->GetGame())) + wxT(".")); return; } m_userChoice = variant; - UpdateWarning(); + UpdateDetail(); } void NashChoiceDialog::OnListMotion(wxWindow *p_window, wxMouseEvent &p_event) @@ -602,7 +602,7 @@ void NashChoiceDialog::RefreshList() } m_refreshing = false; SelectRow(m_userChoice ? *m_userChoice : *recommended); - UpdateWarning(); + UpdateDetail(); } void NashChoiceDialog::UpdateRecommendation() @@ -613,15 +613,34 @@ void NashChoiceDialog::UpdateRecommendation() m_rows[row] == recommended ? wxString::FromUTF8("\xE2\x98\x85") : wxString{}); } SelectRow(m_userChoice ? *m_userChoice : *recommended); - UpdateWarning(); + UpdateDetail(); } -void NashChoiceDialog::UpdateWarning() +wxString NashChoiceDialog::DescribeVariant(const NashVariant &p_variant) { - const auto goal = GetGoal(); - const auto &variant = GetSelectedVariant(); - m_warning->SetLabel(MeetsGoal(variant, goal) ? wxString(wxT(" ")) - : GoalMissWarning(variant, goal)); + const auto game = m_doc->GetGame(); + wxString text = VariantLabel(p_variant) + wxT("\n"); + text += + wxT("Method: ") + FromStd(p_variant.methodName) + wxT(". Arithmetic: ") + + (p_variant.arithmetic == VariantArithmetic::Exact ? wxT("exact.") : wxT("floating point.")) + + wxT(" Solves for: ") + ConceptLabel(p_variant.solves) + wxT(" equilibria.\n"); + if (const auto goal = GetGoal(); !MeetsGoal(p_variant, goal)) { + text += GoalMissWarning(p_variant, goal) + wxT("\n"); + } + text += FromStd(p_variant.caveat) + wxT("\n"); + if (p_variant.representation == VariantRepresentation::Strategic && As(game)) { + if (!m_contingencies) { + m_contingencies = game->GetStrategies().extent_product(); + } + text += wxString::Format(wxT("The reduced strategic form has %llu contingencies.\n"), + static_cast(*m_contingencies)); + } + return text; +} + +void NashChoiceDialog::UpdateDetail() +{ + m_detail->ChangeValue(DescribeVariant(GetSelectedVariant())); } bool NashChoiceDialog::UseStrategic() const diff --git a/src/gui/dlnash.h b/src/gui/dlnash.h index a26bd59706..66da2f977a 100644 --- a/src/gui/dlnash.h +++ b/src/gui/dlnash.h @@ -23,6 +23,7 @@ #ifndef GAMBIT_GUI_DLNASH_H #define GAMBIT_GUI_DLNASH_H +#include #include #include @@ -35,11 +36,13 @@ namespace Gambit::GUI { class NashChoiceDialog final : public wxDialog { std::shared_ptr m_doc; wxRadioButton *m_goalButtons[3]; - wxStaticText *m_goalNote, *m_warning; + wxStaticText *m_goalNote; + wxTextCtrl *m_detail; wxListCtrl *m_list; std::vector m_variants; std::vector m_rows; const NashVariant *m_userChoice{nullptr}; + std::optional m_contingencies; int m_sortColumn{-1}; bool m_sortAscending{true}; bool m_refreshing{false}; @@ -52,7 +55,8 @@ class NashChoiceDialog final : public wxDialog { void RefreshList(); void UpdateRecommendation(); void SelectRow(const NashVariant &); - void UpdateWarning(); + void UpdateDetail(); + wxString DescribeVariant(const NashVariant &); NashGoal GetGoal() const; const NashVariant &GetSelectedVariant() const; From 5836bd8677bdb03390856989becda7cab8f301ef Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Tue, 20 Oct 2026 21:59:24 +0100 Subject: [PATCH 05/12] Add read-only parameters section to the equilibrium dialog --- src/gui/dlnash.cc | 39 +++++++++++++++++++++++++++++++++++++++ src/gui/dlnash.h | 7 +++++++ 2 files changed, 46 insertions(+) diff --git a/src/gui/dlnash.cc b/src/gui/dlnash.cc index 1b413bf82c..6d16600553 100644 --- a/src/gui/dlnash.cc +++ b/src/gui/dlnash.cc @@ -33,6 +33,7 @@ #ifndef WX_PRECOMP #include #endif // WX_PRECOMP +#include #include #include #include @@ -464,6 +465,18 @@ NashChoiceDialog::NashChoiceDialog(wxWindow *p_parent, const std::shared_ptrAdd(m_detail, 0, wxALL | wxEXPAND, S); + m_parameters = new wxCollapsiblePane(this, wxID_ANY, wxT("Parameters")); + m_parameters->Bind(wxEVT_COLLAPSIBLEPANE_CHANGED, &NashChoiceDialog::OnParametersToggled, this); + m_parametersText = new wxStaticText(m_parameters->GetPane(), wxID_ANY, wxEmptyString); + auto *paneSizer = new wxBoxSizer(wxVERTICAL); + paneSizer->Add(m_parametersText, 0, wxALL | wxEXPAND, S); + m_parameters->GetPane()->SetSizer(paneSizer); + m_parameters->GetPane()->Bind(wxEVT_SIZE, [this](wxSizeEvent &p_event) { + p_event.Skip(); + WrapParameters(); + }); + topSizer->Add(m_parameters, 0, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, S); + if (auto *buttonSizer = CreateStdDialogButtonSizer(wxOK | wxCANCEL)) { topSizer->Add(buttonSizer, 0, wxALL | wxEXPAND, S); } @@ -477,6 +490,7 @@ NashChoiceDialog::NashChoiceDialog(wxWindow *p_parent, const std::shared_ptrFit(this); topSizer->SetSizeHints(this); wxTopLevelWindowBase::Layout(); + m_bestHeight = ClientToWindowSize(topSizer->GetMinSize()).y; CenterOnParent(); } @@ -641,6 +655,31 @@ wxString NashChoiceDialog::DescribeVariant(const NashVariant &p_variant) void NashChoiceDialog::UpdateDetail() { m_detail->ChangeValue(DescribeVariant(GetSelectedVariant())); + wxString parameters = ParameterDescription(GetComputation().method); + parameters.Trim(false); + m_parametersLabel = + parameters.empty() ? wxString(wxT("This variant has no parameters.")) : parameters; + WrapParameters(); +} + +void NashChoiceDialog::WrapParameters() +{ + const int width = m_parameters->GetPane()->GetClientSize().GetWidth() - 2 * FromDIP(5); + m_parametersText->SetLabel(m_parametersLabel); + if (width > 0) { + m_parametersText->Wrap(width); + } + m_parameters->GetPane()->Layout(); +} + +void NashChoiceDialog::OnParametersToggled(wxCollapsiblePaneEvent &) +{ + Layout(); + const wxSize best = ClientToWindowSize(GetSizer()->GetMinSize()); + const wxSize size = GetSize(); + SetMinSize(best); + SetSize(size.x, std::max(best.y, size.y + best.y - m_bestHeight)); + m_bestHeight = best.y; } bool NashChoiceDialog::UseStrategic() const diff --git a/src/gui/dlnash.h b/src/gui/dlnash.h index 66da2f977a..55c236f4c0 100644 --- a/src/gui/dlnash.h +++ b/src/gui/dlnash.h @@ -26,6 +26,7 @@ #include #include +#include #include #include @@ -38,6 +39,10 @@ class NashChoiceDialog final : public wxDialog { wxRadioButton *m_goalButtons[3]; wxStaticText *m_goalNote; wxTextCtrl *m_detail; + wxCollapsiblePane *m_parameters; + wxStaticText *m_parametersText; + wxString m_parametersLabel; + int m_bestHeight{0}; wxListCtrl *m_list; std::vector m_variants; std::vector m_rows; @@ -56,6 +61,8 @@ class NashChoiceDialog final : public wxDialog { void UpdateRecommendation(); void SelectRow(const NashVariant &); void UpdateDetail(); + void WrapParameters(); + void OnParametersToggled(wxCollapsiblePaneEvent &); wxString DescribeVariant(const NashVariant &); NashGoal GetGoal() const; From 90cbf4ac85a3cfe679e4ae2054c172b1087330d6 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 21 Oct 2026 06:21:20 +0100 Subject: [PATCH 06/12] Remember the last equilibrium-dialog choices per document --- src/gui/dlnash.cc | 14 ++++++++++++++ src/gui/dlnash.h | 4 ++++ src/gui/gamedoc.h | 6 ++++++ src/gui/gameframe.cc | 1 + src/gui/nashvariants.h | 7 +++++++ 5 files changed, 32 insertions(+) diff --git a/src/gui/dlnash.cc b/src/gui/dlnash.cc index 6d16600553..4cfd9d6c75 100644 --- a/src/gui/dlnash.cc +++ b/src/gui/dlnash.cc @@ -435,6 +435,15 @@ NashChoiceDialog::NashChoiceDialog(wxWindow *p_parent, const std::shared_ptrSetForegroundColour(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_list = new wxListCtrl(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_SINGLE_SEL); m_list->AppendColumn(wxT(""), wxLIST_FORMAT_CENTER, FromDIP(24)); @@ -502,6 +511,11 @@ NashGoal NashChoiceDialog::GetGoal() const return m_goalButtons[1]->GetValue() ? NashGoal::Several : NashGoal::One; } +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()); diff --git a/src/gui/dlnash.h b/src/gui/dlnash.h index 55c236f4c0..1bb7338e68 100644 --- a/src/gui/dlnash.h +++ b/src/gui/dlnash.h @@ -66,6 +66,7 @@ class NashChoiceDialog final : public wxDialog { wxString DescribeVariant(const NashVariant &); NashGoal GetGoal() const; + void SetGoal(NashGoal); const NashVariant &GetSelectedVariant() const; NashComputationSpec GetComputation() const; @@ -76,6 +77,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/gamedoc.h b/src/gui/gamedoc.h index 511396343a..de4a9b1516 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 0144a8a221..f4b434f178 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/nashvariants.h b/src/gui/nashvariants.h index 23e97c711e..7a396441d6 100644 --- a/src/gui/nashvariants.h +++ b/src/gui/nashvariants.h @@ -69,6 +69,13 @@ struct NashVariant { std::string_view caveat; }; +/// 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(); From 37f4fe2279365765add768d78169607b3112ca5c Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 21 Oct 2026 13:22:41 +0100 Subject: [PATCH 07/12] Add agent-form enumpure to GUI offerings --- src/gui/dlnash.cc | 18 ++++++++++-------- src/gui/dlnashmon.cc | 4 ++-- src/gui/nashspec.cc | 10 +++++++++- src/gui/nashspec.h | 2 ++ 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/gui/dlnash.cc b/src/gui/dlnash.cc index 4cfd9d6c75..b4c8dfee4f 100644 --- a/src/gui/dlnash.cc +++ b/src/gui/dlnash.cc @@ -60,11 +60,11 @@ NashEquilibriumTarget GetTarget(NashGoal p_goal) } } -// Agent-form liap and enumpure are not yet offered. +// Agent-form liap is not yet offered. bool IsOffered(const NashVariant &p_variant) { return p_variant.representation == VariantRepresentation::Strategic || - (p_variant.method != NashMethodId::Liap && p_variant.method != NashMethodId::EnumPure); + p_variant.method != NashMethodId::Liap; } std::vector OfferedVariants(const Game &p_game) @@ -242,7 +242,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"); @@ -404,7 +404,7 @@ enum ListColumn { COL_STAR, COL_METHOD, COL_COVERAGE, COL_RELIABILITY, COL_SOLVE } // 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_variants(OfferedVariants(p_doc->GetGame())) { @@ -708,7 +708,8 @@ NashComputationSpec NashChoiceDialog::GetComputation() const return {variant.representation == VariantRepresentation::Strategic ? NashRepresentation::Strategic : NashRepresentation::Behavior, - GetTarget(goal), ResolveMethod(variant.method, goal), m_userChoice == nullptr}; + GetTarget(goal), ResolveMethod(variant.method, goal), m_userChoice == nullptr, + variant.solves == VariantConcept::AgentNash}; } std::shared_ptr NashChoiceDialog::GetCommand() const @@ -723,16 +724,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/dlnashmon.cc b/src/gui/dlnashmon.cc index 091439fbd7..2a3699f69d 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/nashspec.cc b/src/gui/nashspec.cc index 54be81996f..ce0780811d 100644 --- a/src/gui/nashspec.cc +++ b/src/gui/nashspec.cc @@ -36,8 +36,16 @@ 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 &) { + Nash::EnumPureAgentSolve(p_game, [&p_callback](const MixedBehaviorProfile &p) { + p_callback(ComputedProfile(p)); + }); + }; + } return [](const Game &p_game, const ProfileFoundCallback &p_callback, const CancelToken &p_cancel) { Nash::EnumPureStrategySolve( diff --git a/src/gui/nashspec.h b/src/gui/nashspec.h index d138cf9f8b..0f9ff80047 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 From 2ced59d99545143e00ec8699eaac3a89c4222774 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 21 Oct 2026 13:30:12 +0100 Subject: [PATCH 08/12] Make EnumPureAgentSolve cancellable Add a CancelToken parameter to Nash::EnumPureAgentSolve, parallel to the other Nash algorithms, and check it once per behavior contingency. --- src/solvers/enumpure/enumpure.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/solvers/enumpure/enumpure.h b/src/solvers/enumpure/enumpure.h index 744fc474e4..f6ba6e0ac8 100644 --- a/src/solvers/enumpure/enumpure.h +++ b/src/solvers/enumpure/enumpure.h @@ -104,11 +104,13 @@ struct EnumPureAgentResult { /// inline EnumPureAgentResult EnumPureAgentSolve(const Game &p_game, - BehaviorCallbackType p_onEquilibrium = NullBehaviorCallback) + BehaviorCallbackType p_onEquilibrium = NullBehaviorCallback, + const CancelToken &p_cancel = CancelToken()) { EnumPureAgentResult result; const BehaviorSupportProfile support(p_game); for (auto citer : BehaviorContingencies(BehaviorSupportProfile(p_game))) { + p_cancel.Check(); if (IsAgentNash(citer)) { result.equilibria.push_back(citer.ToMixedBehaviorProfile()); p_onEquilibrium(result.equilibria.back()); From d21ebb67895c0a481bca106aa20504e645e66a7b Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 21 Oct 2026 13:34:48 +0100 Subject: [PATCH 09/12] Wire up cancellation of agent-form enumpure in GUI. --- src/gui/nashspec.cc | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/gui/nashspec.cc b/src/gui/nashspec.cc index ce0780811d..b31898841e 100644 --- a/src/gui/nashspec.cc +++ b/src/gui/nashspec.cc @@ -40,10 +40,14 @@ std::optional EnumPureNashSpec::MakeSolver(NashRepresentation p_representation) const { if (p_representation == NashRepresentation::Behavior) { - return [](const Game &p_game, const ProfileFoundCallback &p_callback, const CancelToken &) { - Nash::EnumPureAgentSolve(p_game, [&p_callback](const MixedBehaviorProfile &p) { - p_callback(ComputedProfile(p)); - }); + 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, From 16ae9d2c05e3d431012768f27160d28f73808e74 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 21 Oct 2026 13:57:03 +0100 Subject: [PATCH 10/12] Offer agent-form liap in GUI --- src/gui/dlnash.cc | 16 ++++------------ src/gui/nashspec.cc | 16 +++++++++++++++- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/gui/dlnash.cc b/src/gui/dlnash.cc index b4c8dfee4f..21a343d594 100644 --- a/src/gui/dlnash.cc +++ b/src/gui/dlnash.cc @@ -60,19 +60,11 @@ NashEquilibriumTarget GetTarget(NashGoal p_goal) } } -// Agent-form liap is not yet offered. -bool IsOffered(const NashVariant &p_variant) -{ - return p_variant.representation == VariantRepresentation::Strategic || - p_variant.method != NashMethodId::Liap; -} - std::vector OfferedVariants(const Game &p_game) { std::vector result; for (const auto &variant : NashVariants()) { - if (IsOffered(variant) && - (variant.representation == VariantRepresentation::Strategic || As(p_game))) { + if (variant.representation == VariantRepresentation::Strategic || As(p_game)) { result.push_back(&variant); } } @@ -280,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 + diff --git a/src/gui/nashspec.cc b/src/gui/nashspec.cc index b31898841e..9e5b497409 100644 --- a/src/gui/nashspec.cc +++ b/src/gui/nashspec.cc @@ -183,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)) { From b7dd79cbccae425c9da09d468342351a0f70add8 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 21 Oct 2026 14:06:39 +0100 Subject: [PATCH 11/12] Draft documentation update for new selection dialog --- doc/gui.nash.rst | 307 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 232 insertions(+), 75 deletions(-) diff --git a/doc/gui.nash.rst b/doc/gui.nash.rst index e139f46985..c02593468d 100644 --- a/doc/gui.nash.rst +++ b/doc/gui.nash.rst @@ -10,92 +10,249 @@ 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:`Numerical failure + possible` means it converges in theory but can fail in floating-point + arithmetic. :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`). + +Selecting a row shows a description in the detail pane: the method name, +whether the arithmetic is exact or floating point, the solution concept, +any warning about your Goal, and notes on the variant. For variants +that work on the reduced strategic form of an extensive game, the pane +also shows the number of contingencies in that form, which gives an idea +of how large the computation will be. + +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 is reliable, whereas ``gnm`` is less +thoroughly tested and can fail numerically. 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 +~~~~~~~~~~ + +Below the detail pane, a collapsible :guilabel:`Parameters` 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 From 713b9c5c5249049c00163130da14609772e3af47 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Mon, 26 Oct 2026 10:51:51 +0000 Subject: [PATCH 12/12] Tidy up the new solver dialog. --- doc/gui.nash.rst | 25 +++---- src/gui/dlnash.cc | 150 +++++++++++++++++++++++----------------- src/gui/dlnash.h | 12 +--- src/gui/nashvariants.cc | 58 +++++----------- src/gui/nashvariants.h | 3 +- 5 files changed, 119 insertions(+), 129 deletions(-) diff --git a/doc/gui.nash.rst b/doc/gui.nash.rst index c02593468d..117c2ec2f6 100644 --- a/doc/gui.nash.rst +++ b/doc/gui.nash.rst @@ -87,21 +87,19 @@ be sorted by clicking a column heading. The columns are: :guilabel:`Reliability` Whether the variant is guaranteed to return a result on a given game. - :guilabel:`Always` means it is. :guilabel:`Numerical failure - possible` means it converges in theory but can fail in floating-point - arithmetic. :guilabel:`No guarantee` means it is a local or - heuristic search that is not known to converge on every 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`). -Selecting a row shows a description in the detail pane: the method name, -whether the arithmetic is exact or floating point, the solution concept, -any warning about your Goal, and notes on the variant. For variants -that work on the reduced strategic form of an extensive game, the pane -also shows the number of contingencies in that form, which gives an idea -of how large the computation will be. +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 ~~~~~~~~~~~~~~~~~~ @@ -147,8 +145,8 @@ 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 is reliable, whereas ``gnm`` is less -thoroughly tested and can fail numerically. For strategic games every +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 @@ -225,8 +223,7 @@ check the profiles returned. Parameters ~~~~~~~~~~ -Below the detail pane, a collapsible :guilabel:`Parameters` section shows -the parameters the selected variant will use. They are shown for +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``. diff --git a/src/gui/dlnash.cc b/src/gui/dlnash.cc index 21a343d594..96b756c7bb 100644 --- a/src/gui/dlnash.cc +++ b/src/gui/dlnash.cc @@ -142,8 +142,8 @@ wxString ReliabilityLabel(VariantReliability p_reliability) switch (p_reliability) { case VariantReliability::Always: return wxT("Always"); - case VariantReliability::NumericalFailurePossible: - return wxT("Numerical failure possible"); + case VariantReliability::Usually: + return wxT("Usually"); default: return wxT("No guarantee"); } @@ -389,6 +389,67 @@ 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 { @@ -461,22 +522,16 @@ NashChoiceDialog::NashChoiceDialog(wxWindow *p_parent, const std::shared_ptrAdd(legend, 0, wxLEFT | wxRIGHT | wxTOP | wxEXPAND, S); - m_detail = - new wxTextCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(-1, FromDIP(130)), - wxTE_MULTILINE | wxTE_READONLY | wxTE_BESTWRAP); - topSizer->Add(m_detail, 0, wxALL | 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_parameters = new wxCollapsiblePane(this, wxID_ANY, wxT("Parameters")); - m_parameters->Bind(wxEVT_COLLAPSIBLEPANE_CHANGED, &NashChoiceDialog::OnParametersToggled, this); - m_parametersText = new wxStaticText(m_parameters->GetPane(), wxID_ANY, wxEmptyString); + 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_parametersText, 0, wxALL | wxEXPAND, S); - m_parameters->GetPane()->SetSizer(paneSizer); - m_parameters->GetPane()->Bind(wxEVT_SIZE, [this](wxSizeEvent &p_event) { - p_event.Skip(); - WrapParameters(); - }); - topSizer->Add(m_parameters, 0, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, S); + 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); @@ -489,9 +544,8 @@ NashChoiceDialog::NashChoiceDialog(wxWindow *p_parent, const std::shared_ptrFit(this); - topSizer->SetSizeHints(this); + SetMinSize(wxSize(GetSize().x, GetSize().y - FromDIP(140))); wxTopLevelWindowBase::Layout(); - m_bestHeight = ClientToWindowSize(topSizer->GetMinSize()).y; CenterOnParent(); } @@ -636,56 +690,26 @@ void NashChoiceDialog::UpdateRecommendation() UpdateDetail(); } -wxString NashChoiceDialog::DescribeVariant(const NashVariant &p_variant) -{ - const auto game = m_doc->GetGame(); - wxString text = VariantLabel(p_variant) + wxT("\n"); - text += - wxT("Method: ") + FromStd(p_variant.methodName) + wxT(". Arithmetic: ") + - (p_variant.arithmetic == VariantArithmetic::Exact ? wxT("exact.") : wxT("floating point.")) + - wxT(" Solves for: ") + ConceptLabel(p_variant.solves) + wxT(" equilibria.\n"); - if (const auto goal = GetGoal(); !MeetsGoal(p_variant, goal)) { - text += GoalMissWarning(p_variant, goal) + wxT("\n"); - } - text += FromStd(p_variant.caveat) + wxT("\n"); - if (p_variant.representation == VariantRepresentation::Strategic && As(game)) { - if (!m_contingencies) { - m_contingencies = game->GetStrategies().extent_product(); - } - text += wxString::Format(wxT("The reduced strategic form has %llu contingencies.\n"), - static_cast(*m_contingencies)); - } - return text; -} - void NashChoiceDialog::UpdateDetail() { - m_detail->ChangeValue(DescribeVariant(GetSelectedVariant())); - wxString parameters = ParameterDescription(GetComputation().method); - parameters.Trim(false); - m_parametersLabel = - parameters.empty() ? wxString(wxT("This variant has no parameters.")) : parameters; - WrapParameters(); -} + const auto &variant = GetSelectedVariant(); + const auto goal = GetGoal(); + m_warning->SetLabel(MeetsGoal(variant, goal) ? wxString{} : GoalMissWarning(variant, goal)); -void NashChoiceDialog::WrapParameters() -{ - const int width = m_parameters->GetPane()->GetClientSize().GetWidth() - 2 * FromDIP(5); - m_parametersText->SetLabel(m_parametersLabel); - if (width > 0) { - m_parametersText->Wrap(width); - } - m_parameters->GetPane()->Layout(); -} + const auto rows = ParameterRows(GetComputation().method); -void NashChoiceDialog::OnParametersToggled(wxCollapsiblePaneEvent &) -{ + 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)); + } + 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(); - const wxSize best = ClientToWindowSize(GetSizer()->GetMinSize()); - const wxSize size = GetSize(); - SetMinSize(best); - SetSize(size.x, std::max(best.y, size.y + best.y - m_bestHeight)); - m_bestHeight = best.y; } bool NashChoiceDialog::UseStrategic() const diff --git a/src/gui/dlnash.h b/src/gui/dlnash.h index 1bb7338e68..e06e25e33a 100644 --- a/src/gui/dlnash.h +++ b/src/gui/dlnash.h @@ -38,16 +38,13 @@ class NashChoiceDialog final : public wxDialog { std::shared_ptr m_doc; wxRadioButton *m_goalButtons[3]; wxStaticText *m_goalNote; - wxTextCtrl *m_detail; - wxCollapsiblePane *m_parameters; - wxStaticText *m_parametersText; - wxString m_parametersLabel; - int m_bestHeight{0}; + 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}; - std::optional m_contingencies; int m_sortColumn{-1}; bool m_sortAscending{true}; bool m_refreshing{false}; @@ -61,9 +58,6 @@ class NashChoiceDialog final : public wxDialog { void UpdateRecommendation(); void SelectRow(const NashVariant &); void UpdateDetail(); - void WrapParameters(); - void OnParametersToggled(wxCollapsiblePaneEvent &); - wxString DescribeVariant(const NashVariant &); NashGoal GetGoal() const; void SetGoal(NashGoal); diff --git a/src/gui/nashvariants.cc b/src/gui/nashvariants.cc index a0bf14008f..93c2576f88 100644 --- a/src/gui/nashvariants.cc +++ b/src/gui/nashvariants.cc @@ -40,47 +40,23 @@ using enum VariantArithmetic; using enum VariantScope; constexpr std::array s_variants{{ - {EnumMixed, "enummixed", "strategic", Strategic, All, Always, Nash, Exact, TwoPlayer, - "Returns every extreme equilibrium; connected components of equilibria are described by " - "their extreme points."}, - {EnumPoly, "enumpoly", "sequence form", Behavior, Some, NumericalFailurePossible, Nash, - FloatingPoint, AnyGame, - "Finds isolated equilibria only; supports it cannot resolve are skipped. May find " - "equilibria that the strategic variant does not."}, - {EnumPoly, "enumpoly", "strategic", Strategic, Some, NumericalFailurePossible, Nash, - FloatingPoint, AnyGame, - "Finds isolated equilibria only; supports it cannot resolve are skipped. May find " - "equilibria that the sequence-form variant does not."}, - {EnumPure, "enumpure", "agent form", Behavior, AllPure, Always, AgentNash, Exact, AnyGame, - "Returns all pure-strategy agent Nash equilibria, which need not be Nash equilibria and " - "may not exist."}, - {EnumPure, "enumpure", "strategic", Strategic, AllPure, Always, Nash, Exact, AnyGame, - "Returns all pure-strategy equilibria, which may not exist."}, - {GNM, "gnm", "strategic", Strategic, Some, NumericalFailurePossible, Nash, FloatingPoint, - AnyGame, "Less thoroughly tested than simplicial subdivision and can fail numerically."}, - {HP, "hp", "strategic", Strategic, Some, NumericalFailurePossible, Nash, FloatingPoint, - AnyGame, "Returns at most one equilibrium per random prior."}, - {IPA, "ipa", "strategic", Strategic, Some, NoGuarantee, Nash, FloatingPoint, AnyGame, - "Convergence is not known to be guaranteed."}, - {LCP, "lcp", "Lemke (sequence form)", Behavior, One, Always, Nash, Exact, TwoPlayer, - "Returns a single equilibrium."}, - {LCP, "lcp", "Lemke-Howson (strategic)", Strategic, Some, Always, Nash, Exact, TwoPlayer, - "Finds only the accessible equilibria (those reachable by the Lemke-Howson path)."}, - {Liap, "liap", "agent form", Behavior, Some, NoGuarantee, AgentNash, FloatingPoint, AnyGame, - "Local descent from random starting points. The result is an agent Nash equilibrium, " - "which need not be a Nash equilibrium."}, - {Liap, "liap", "strategic", Strategic, Some, NoGuarantee, Nash, FloatingPoint, AnyGame, - "Local descent from random starting points; may converge to a non-equilibrium."}, - {Logit, "logit", "agent form", Behavior, One, NumericalFailurePossible, Nash, FloatingPoint, - AnyGame, "Traces the agent quantal response equilibrium; its limit is a Nash equilibrium."}, - {Logit, "logit", "strategic", Strategic, One, NumericalFailurePossible, Nash, FloatingPoint, - AnyGame, "Traces the quantal response equilibrium; its limit is a Nash equilibrium."}, - {LP, "lp", "sequence form", Behavior, One, Always, Nash, Exact, TwoPlayerConstSum, - "May return a different equilibrium from the strategic variant."}, - {LP, "lp", "strategic", Strategic, One, Always, Nash, Exact, TwoPlayerConstSum, - "May return a different equilibrium from the sequence-form variant."}, - {Simpdiv, "simpdiv", "strategic", Strategic, Some, Always, Nash, Exact, AnyGame, - "Returns an approximate equilibrium to within the stated regret."}, + {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 diff --git a/src/gui/nashvariants.h b/src/gui/nashvariants.h index 7a396441d6..df104072a1 100644 --- a/src/gui/nashvariants.h +++ b/src/gui/nashvariants.h @@ -50,7 +50,7 @@ enum class NashMethodId { enum class VariantRepresentation { Strategic, Behavior }; enum class VariantCoverage { One, Some, AllPure, All }; -enum class VariantReliability { Always, NumericalFailurePossible, NoGuarantee }; +enum class VariantReliability { Always, Usually, NoGuarantee }; enum class VariantConcept { Nash, AgentNash }; enum class VariantArithmetic { Exact, FloatingPoint }; enum class VariantScope { AnyGame, TwoPlayer, TwoPlayerConstSum }; @@ -66,7 +66,6 @@ struct NashVariant { VariantConcept solves; VariantArithmetic arithmetic; VariantScope scope; - std::string_view caveat; }; /// The user's last choices in the equilibrium dialog.