diff --git a/ChangeLog b/ChangeLog index 8d4b036f2..45ac13b37 100644 --- a/ChangeLog +++ b/ChangeLog @@ -16,6 +16,8 @@ - In the GUI, tab-traversal on tables (such as the strategic form payoff table) now works as expected (TAB moves one cell to the right, wrapping if appropriate; SHIFT-TAB moves to the left, wrapping if appropriate). +- Max regret is calculated correctly for strategy and behavior profiles on games with zero player + strategies or actions (is defined to be 0 trivially) (#904) ### Removed - Built-in plotting of logit QRE for strategic games has been removed in the GUI (#809) diff --git a/src/games/behavmixed.cc b/src/games/behavmixed.cc index 42498ad87..0887d1988 100644 --- a/src/games/behavmixed.cc +++ b/src/games/behavmixed.cc @@ -383,7 +383,11 @@ template T MixedBehaviorProfile::GetMaxRegret() const template T MixedBehaviorProfile::GetAgentMaxRegret() const { - return maximize_function(m_support.GetGame()->GetInfosets(), + auto infosets = m_support.GetGame()->GetInfosets(); + if (infosets.size() == 0) { + return T{0}; + } + return maximize_function(infosets, [this](const auto &infoset) -> T { return this->GetRegret(infoset); }); } diff --git a/src/games/game.cc b/src/games/game.cc index 7362028d5..f75c742b5 100644 --- a/src/games/game.cc +++ b/src/games/game.cc @@ -410,29 +410,37 @@ template T MixedStrategyProfile::GetRegret(const GameStrategy &p_st ComputePayoffs(); auto player = p_strategy->GetPlayer(); + if (player->m_strategies.size() == 1) { + return T{0}; + } T best_other_payoff = maximize_function( filter_if(player->GetStrategies(), [&](const auto &s) { return s != p_strategy; }), [this, &player](const auto &strategy) -> T { return m_cache.m_strategyValues.at(player).at(strategy); }); - return std::max(best_other_payoff - m_cache.m_strategyValues.at(player).at(p_strategy), - static_cast(0)); + return std::max(best_other_payoff - m_cache.m_strategyValues.at(player).at(p_strategy), T{0}); } template T MixedStrategyProfile::GetRegret(const GamePlayer &p_player) const { CheckVersion(); ComputePayoffs(); - auto br_payoff = - maximize_function(p_player->GetStrategies(), [this, p_player](const auto &strategy) -> T { - return m_cache.m_strategyValues.at(p_player).at(strategy); - }); + auto strategies = p_player->GetStrategies(); + if (strategies.size() == 0) { + return T{0}; + } + auto br_payoff = maximize_function(strategies, [this, p_player](const auto &strategy) -> T { + return m_cache.m_strategyValues.at(p_player).at(strategy); + }); return br_payoff - m_cache.m_payoffs.at(p_player); } template T MixedStrategyProfile::GetMaxRegret() const { CheckVersion(); + if (GetGame()->GetPlayers().size() == 0) { + return T{0}; + } return maximize_function(GetGame()->GetPlayers(), [this](const auto &player) -> T { return this->GetRegret(player); }); } diff --git a/tests/test_nash.py b/tests/test_nash.py index abfaa607d..706d8ffee 100644 --- a/tests/test_nash.py +++ b/tests/test_nash.py @@ -2259,6 +2259,20 @@ def test_nash_strategy_solver_w_start(test_case: EquilibriumTestCaseWithStart, s marks=pytest.mark.nash_logit_behavior, id="test_logit_behavior_01", ), + pytest.param( + EquilibriumTestCase( + factory=functools.partial(games.read_from_file, + "chance_root_5_moves_no_nonterm_player_nodes.efg"), + solver=gbt.nash.logit_solve, + expected=[ + [[]] # Zero-dimension edge case (two players) + ], + regret_tol=TOL_LARGE, + prob_tol=TOL_LARGE, + ), + marks=pytest.mark.nash_logit_behavior, + id="test_logit_behavior_degenerate", + ), ]