From bc4ab2470ad993c8475448a00868b60a9cd5138b Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 19 Aug 2026 22:21:55 +0100 Subject: [PATCH] Preserve decimal payoffs in strategic forms when possible in GUI. This makes an adjustment to the display of strategic forms to preserve decimal payoffs whenever possible: * In a strategic game, if the payoff is recorded as a decimal number (rather than a fraction), that is displayed. * In an extensive game, if the contingency corresponds to exactly one path through the tree, and that path has one outcome on it, then the textual representation of that payoff is displayed. For extensive games, if arithmetic is required (because of chance events or nonterminal outcomes), the resulting rational payoff is displayed, irrespective of whether it has an exact decimal representation. Closes #867. --- ChangeLog | 2 ++ src/games/behavpure.cc | 23 +++++++++++++++++++++++ src/games/behavpure.h | 7 +++++++ src/games/gametree.cc | 35 +++++++++++++++++++++++------------ src/gui/nfgtable.cc | 12 ++++++++++-- 5 files changed, 65 insertions(+), 14 deletions(-) diff --git a/ChangeLog b/ChangeLog index 890bc5a63b..fe2989fded 100644 --- a/ChangeLog +++ b/ChangeLog @@ -18,6 +18,8 @@ ### Changed - All long-running calculations are now done in a thread in the GUI process, rather than calling the command-line executables as external processes. (#990) +- When displaying a strategic form, the graphical interface preserves decimal payoffs when those are + what are specified in the game, whenever possible. (#867) ### Removed - Assigning to `Strategy.label` has been removed; use `Game.relabel_strategies`, which enforces diff --git a/src/games/behavpure.cc b/src/games/behavpure.cc index f486f179ee..e013dc0d42 100644 --- a/src/games/behavpure.cc +++ b/src/games/behavpure.cc @@ -73,6 +73,29 @@ T PureBehaviorProfile::GetPayoff(const GameNode &p_node, const GamePlayer &p_pla template double PureBehaviorProfile::GetPayoff(const GameNode &, const GamePlayer &) const; template Rational PureBehaviorProfile::GetPayoff(const GameNode &, const GamePlayer &) const; +GameOutcome PureBehaviorProfile::GetOutcome(const GameNode &p_start) const +{ + GameOutcome outcome; + GameNode node = p_start; + while (true) { + if (node->GetOutcome()) { + if (outcome) { + // More than one outcome-bearing node along this path; the combined + // payoff cannot be attributed to a single outcome's preserved text. + throw UndefinedException(); + } + outcome = node->GetOutcome(); + } + if (node->IsTerminal()) { + return outcome; + } + if (node->GetInfoset()->IsChanceInfoset()) { + throw UndefinedException(); + } + node = node->GetChild(m_profile.at(node->GetInfoset())); + } +} + MixedBehaviorProfile PureBehaviorProfile::ToMixedBehaviorProfile() const { MixedBehaviorProfile temp(m_efg); diff --git a/src/games/behavpure.h b/src/games/behavpure.h index 83dda90920..fbbb706a6a 100644 --- a/src/games/behavpure.h +++ b/src/games/behavpure.h @@ -62,6 +62,13 @@ class PureBehaviorProfile { /// Get the payoff to the player, conditional on reaching a node template T GetPayoff(const GameNode &, const GamePlayer &) const; + /// Get the outcome that results from the profile, provided this can be + /// determined without resolving a chance move and without combining more + /// than one outcome along the induced path; throws UndefinedException otherwise. + GameOutcome GetOutcome() const { return GetOutcome(m_efg->GetRoot()); } + /// Get the outcome that results from the profile, conditional on reaching a node + GameOutcome GetOutcome(const GameNode &) const; + /// Convert to a mixed behavior representation MixedBehaviorProfile ToMixedBehaviorProfile() const; //@} diff --git a/src/games/gametree.cc b/src/games/gametree.cc index 419ed9c495..4ad5813de3 100644 --- a/src/games/gametree.cc +++ b/src/games/gametree.cc @@ -1921,9 +1921,25 @@ class TreePureStrategyProfileRep : public PureStrategyProfileRep { return std::make_shared(*this); } + /// Build the behavior profile induced by this pure strategy profile + PureBehaviorProfile GetBehaviorProfile() const + { + PureBehaviorProfile behav(m_game); + for (const auto &player : m_game->GetPlayers()) { + for (const auto &infoset : player->GetInfosets()) { + try { + behav.SetAction(infoset->GetAction(GetStrategy(player)->m_behav.at(infoset.get()))); + } + catch (std::out_of_range &) { + } + } + } + return behav; + } + public: TreePureStrategyProfileRep(const Game &p_game) : PureStrategyProfileRep(p_game) {} - GameOutcome GetOutcome() const override { throw UndefinedException(); } + GameOutcome GetOutcome() const override; void SetOutcome(GameOutcome p_outcome) override { throw UndefinedException(); } Rational GetPayoff(const GamePlayer &) const override; Rational GetStrategyValue(const GameStrategy &) const override; @@ -1944,19 +1960,14 @@ PureStrategyProfile GameTreeRep::NewPureStrategyProfile() const // TreePureStrategyProfileRep: Data access and manipulation //------------------------------------------------------------------------ +GameOutcome TreePureStrategyProfileRep::GetOutcome() const +{ + return GetBehaviorProfile().GetOutcome(); +} + Rational TreePureStrategyProfileRep::GetPayoff(const GamePlayer &p_player) const { - PureBehaviorProfile behav(m_game); - for (const auto &player : m_game->GetPlayers()) { - for (const auto &infoset : player->GetInfosets()) { - try { - behav.SetAction(infoset->GetAction(GetStrategy(player)->m_behav.at(infoset.get()))); - } - catch (std::out_of_range &) { - } - } - } - return behav.GetPayoff(p_player); + return GetBehaviorProfile().GetPayoff(p_player); } Rational TreePureStrategyProfileRep::GetStrategyValue(const GameStrategy &p_strategy) const diff --git a/src/gui/nfgtable.cc b/src/gui/nfgtable.cc index 5c4502c829..8533927d62 100644 --- a/src/gui/nfgtable.cc +++ b/src/gui/nfgtable.cc @@ -1010,8 +1010,16 @@ class PayoffTable final : public wxGridTableBase { wxString PayoffTable::GetValue(int row, int col) { const PureStrategyProfile profile = m_table->GetPayoffProfile(row, col); - auto player = m_table->GetPayoffPlayer(col); - return wxString::FromUTF8(lexical_cast(profile->GetPayoff(player))); + const auto player = m_table->GetPayoffPlayer(col); + try { + if (const auto outcome = profile->GetOutcome()) { + return wxString::FromUTF8(outcome->GetPayoff(player)); + } + return wxString::FromUTF8("0"); + } + catch (const UndefinedException &) { + return wxString::FromUTF8(lexical_cast(profile->GetPayoff(player))); + } } void PayoffTable::SetValue(int row, int col, const wxString &value)