From 48d97da82f50f9447219a2b5ba674bd4432e6f05 Mon Sep 17 00:00:00 2001 From: StephenPasteris Date: Tue, 16 Dec 2025 14:09:12 +0000 Subject: [PATCH 1/4] Fixed LP --- src/solvers/lp/lp.cc | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/solvers/lp/lp.cc b/src/solvers/lp/lp.cc index 9e0a5e467..52b3f8971 100644 --- a/src/solvers/lp/lp.cc +++ b/src/solvers/lp/lp.cc @@ -34,7 +34,7 @@ template class GameData { explicit GameData(const Game &); - void FillTableau(Matrix &A, const GameNode &n, const T &prob, int s1, int s2); + void FillTableau(Matrix &A, const GameNode &n, const T &prob, int s1, int s2, T payoff); void GetBehavior(MixedBehaviorProfile &v, const Array &, const Array &, const GameNode &, int, int); @@ -57,21 +57,22 @@ template GameData::GameData(const Game &p_game) : minpay(p_game->Ge // Recursively fills the constraint matrix A for the subtree rooted at 'n'. // template -void GameData::FillTableau(Matrix &A, const GameNode &n, const T &prob, int s1, int s2) +void GameData::FillTableau(Matrix &A, const GameNode &n, const T &prob, int s1, int s2, + T payoff) { const GameOutcome outcome = n->GetOutcome(); if (outcome) { - A(s1, s2) += - Rational(prob) * (outcome->GetPayoff(n->GetGame()->GetPlayer(1)) - minpay); + payoff += outcome->GetPayoff(n->GetGame()->GetPlayer(1)); } if (n->IsTerminal()) { + A(s1, s2) += Rational(prob) * (payoff - minpay); return; } const GameInfoset infoset = n->GetInfoset(); if (n->GetPlayer()->IsChance()) { for (const auto &action : infoset->GetActions()) { FillTableau(A, n->GetChild(action), prob * static_cast(infoset->GetActionProb(action)), - s1, s2); + s1, s2, payoff); } } else if (n->GetPlayer()->GetNumber() == 1) { @@ -80,7 +81,7 @@ void GameData::FillTableau(Matrix &A, const GameNode &n, const T &prob, in A(s1, col) = static_cast(1); for (const auto &child : n->GetChildren()) { A(++snew, col) = static_cast(-1); - FillTableau(A, child, prob, snew, s2); + FillTableau(A, child, prob, snew, s2, payoff); } } else { @@ -89,7 +90,7 @@ void GameData::FillTableau(Matrix &A, const GameNode &n, const T &prob, in A(row, s2) = static_cast(-1); for (const auto &child : n->GetChildren()) { A(row, ++snew) = static_cast(1); - FillTableau(A, child, prob, s1, snew); + FillTableau(A, child, prob, s1, snew, payoff); } } } @@ -187,7 +188,7 @@ std::list> LpBehaviorSolve(const Game &p_game, b = static_cast(0); c = static_cast(0); - data.FillTableau(A, p_game->GetRoot(), static_cast(1), 1, 1); + data.FillTableau(A, p_game->GetRoot(), static_cast(1), 1, 1, static_cast(0)); A(1, data.ns2 + 1) = static_cast(-1); A(data.ns1 + 1, 1) = static_cast(1); From fa890da771057228c065d353b9303bfe426a5cb4 Mon Sep 17 00:00:00 2001 From: StephenPasteris Date: Tue, 16 Dec 2025 14:27:20 +0000 Subject: [PATCH 2/4] Fixed LCP --- src/solvers/lcp/efglcp.cc | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/solvers/lcp/efglcp.cc b/src/solvers/lcp/efglcp.cc index 9b44b08e9..8e1679801 100644 --- a/src/solvers/lcp/efglcp.cc +++ b/src/solvers/lcp/efglcp.cc @@ -44,7 +44,7 @@ template class NashLcpBehaviorSolver { class Solution; - void FillTableau(Matrix &, const GameNode &, T, int, int, Solution &) const; + void FillTableau(Matrix &, const GameNode &, T, int, int, T, T, Solution &) const; void AllLemke(const Game &, int dup, linalg::LemkeTableau &B, int depth, Matrix &, Solution &) const; void GetProfile(const linalg::LemkeTableau &tab, MixedBehaviorProfile &, const Vector &, @@ -126,7 +126,8 @@ std::list> NashLcpBehaviorSolver::Solve(const Game &p const int ntot = solution.ns1 + solution.ns2 + solution.ni1 + solution.ni2; Matrix A(1, ntot, 0, ntot); A = static_cast(0); - FillTableau(A, p_game->GetRoot(), static_cast(1), 1, 1, solution); + FillTableau(A, p_game->GetRoot(), static_cast(1), 1, 1, static_cast(0), static_cast(0), + solution); for (int i = A.MinRow(); i <= A.MaxRow(); i++) { A(i, 0) = static_cast(-1); } @@ -238,7 +239,7 @@ void NashLcpBehaviorSolver::AllLemke(const Game &p_game, int j, linalg::Lemke template void NashLcpBehaviorSolver::FillTableau(Matrix &A, const GameNode &n, T prob, int s1, int s2, - Solution &p_solution) const + T payoff1, T payoff2, Solution &p_solution) const { const int ns1 = p_solution.ns1; const int ns2 = p_solution.ns2; @@ -246,12 +247,12 @@ void NashLcpBehaviorSolver::FillTableau(Matrix &A, const GameNode &n, T pr const GameOutcome outcome = n->GetOutcome(); if (outcome) { - A(s1, ns1 + s2) += Rational(prob) * (outcome->GetPayoff(n->GetGame()->GetPlayer(1)) - - p_solution.maxpay); - A(ns1 + s2, s1) += Rational(prob) * (outcome->GetPayoff(n->GetGame()->GetPlayer(2)) - - p_solution.maxpay); + payoff1 += outcome->GetPayoff(n->GetGame()->GetPlayer(1)); + payoff2 += outcome->GetPayoff(n->GetGame()->GetPlayer(2)); } if (n->IsTerminal()) { + A(s1, ns1 + s2) += Rational(prob) * (payoff1 - p_solution.maxpay); + A(ns1 + s2, s1) += Rational(prob) * (payoff2 - p_solution.maxpay); return; } const GameInfoset infoset = n->GetInfoset(); @@ -259,7 +260,7 @@ void NashLcpBehaviorSolver::FillTableau(Matrix &A, const GameNode &n, T pr for (const auto &action : infoset->GetActions()) { FillTableau(A, n->GetChild(action), Rational(prob) * static_cast(infoset->GetActionProb(action)), s1, s2, - p_solution); + payoff1, payoff2, p_solution); } } else if (n->GetPlayer()->GetNumber() == 1) { @@ -271,7 +272,7 @@ void NashLcpBehaviorSolver::FillTableau(Matrix &A, const GameNode &n, T pr snew++; A(snew, infoset_idx) = static_cast(1); A(infoset_idx, snew) = static_cast(-1); - FillTableau(A, child, prob, snew, s2, p_solution); + FillTableau(A, child, prob, snew, s2, payoff1, payoff2, p_solution); } } else { @@ -283,7 +284,7 @@ void NashLcpBehaviorSolver::FillTableau(Matrix &A, const GameNode &n, T pr snew++; A(ns1 + snew, infoset_idx) = static_cast(1); A(infoset_idx, ns1 + snew) = static_cast(-1); - FillTableau(A, child, prob, s1, snew, p_solution); + FillTableau(A, child, prob, s1, snew, payoff1, payoff2, p_solution); } } } From 3061252a7b37b049a6308a31b28711aa688be0fe Mon Sep 17 00:00:00 2001 From: StephenPasteris Date: Fri, 19 Dec 2025 11:46:31 +0000 Subject: [PATCH 3/4] removed xfail markers --- tests/test_nash.py | 45 +++++++++++++++------------------------------ 1 file changed, 15 insertions(+), 30 deletions(-) diff --git a/tests/test_nash.py b/tests/test_nash.py index adf5552df..6725868ba 100644 --- a/tests/test_nash.py +++ b/tests/test_nash.py @@ -418,23 +418,20 @@ def test_lcp_behavior_double(): games.create_2x2_zero_sum_efg(), [[["1/2", "1/2"]], [["1/2", "1/2"]]] ), - pytest.param( + ( games.create_2x2_zero_sum_efg(missing_term_outcome=True), [[["1/2", "1/2"]], [["1/2", "1/2"]]], - marks=pytest.mark.xfail(reason="Problem with non-standard outcomes") ), (games.create_matching_pennies_efg(), [[["1/2", "1/2"]], [["1/2", "1/2"]]]), - pytest.param( + ( games.create_matching_pennies_efg(with_neutral_outcome=True), [[["1/2", "1/2"]], [["1/2", "1/2"]]], - marks=pytest.mark.xfail(reason="Problem with non-standard outcomes") ), (games.create_stripped_down_poker_efg(), [[[1, 0], ["1/3", "2/3"]], [["2/3", "1/3"]]]), - pytest.param( + ( games.create_stripped_down_poker_efg(nonterm_outcomes=True), [[[1, 0], ["1/3", "2/3"]], [["2/3", "1/3"]]], - marks=pytest.mark.xfail(reason="Problem with non-standard outcomes") ), ( games.create_kuhn_poker_efg(), @@ -450,7 +447,7 @@ def test_lcp_behavior_double(): [[1, 0], ["2/3", "1/3"], [0, 1], [0, 1], ["2/3", "1/3"], [1, 0]], ], ), - pytest.param( + ( games.create_kuhn_poker_efg(nonterm_outcomes=True), [ [ @@ -463,7 +460,6 @@ def test_lcp_behavior_double(): ], [[1, 0], ["2/3", "1/3"], [0, 1], [0, 1], ["2/3", "1/3"], [1, 0]], ], - marks=pytest.mark.xfail(reason="Problem with non-standard outcomes") ), # In the next test case: # 1/2-1/2 for l/r is determined by MixedBehaviorProfile.UndefinedToCentroid() @@ -486,13 +482,12 @@ def test_lcp_behavior_double(): [["2/3", "1/3"], ["1/3", "2/3"], ["1/3", "2/3"]], ] ), - pytest.param( + ( games.create_three_action_internal_outcomes_efg(nonterm_outcomes=True), [ [["1/3", 0, "2/3"], ["2/3", 0, "1/3"]], [["2/3", "1/3"], ["1/3", "2/3"], ["1/3", "2/3"]], ], - marks=pytest.mark.xfail(reason="Problem with non-standard outcomes") ), ( games.create_large_payoff_game_efg(), @@ -509,13 +504,12 @@ def test_lcp_behavior_double(): [[1, 0], ["6/11", "5/11"]] ] ), - pytest.param( + ( games.create_chance_in_middle_efg(nonterm_outcomes=True), [ [["3/11", "8/11"], [1, 0], [1, 0], [1, 0], [1, 0]], [[1, 0], ["6/11", "5/11"]] ], - marks=pytest.mark.xfail(reason="Problem with non-standard outcomes") ), # Non-zero-sum games ( @@ -538,19 +532,17 @@ def test_lcp_behavior_double(): games.create_entry_accomodation_efg(), [[["2/3", "1/3"], [1, 0], [1, 0]], [["2/3", "1/3"]]] ), - pytest.param( + ( games.create_entry_accomodation_efg(nonterm_outcomes=True), [[["2/3", "1/3"], [1, 0], [1, 0]], [["2/3", "1/3"]]], - marks=pytest.mark.xfail(reason="Problem with non-standard outcomes") ), ( games.create_non_zero_sum_lacking_outcome_efg(), [[["1/3", "2/3"]], [["1/2", "1/2"]]] ), - pytest.param( + ( games.create_non_zero_sum_lacking_outcome_efg(missing_term_outcome=True), [[["1/3", "2/3"]], [["1/2", "1/2"]]], - marks=pytest.mark.xfail(reason="Problem with non-standard outcomes") ), ], ) @@ -628,35 +620,31 @@ def test_lp_behavior_double(): games.create_two_player_perfect_info_win_lose_efg(), [[[0, 1], [1, 0]], [[1, 0], [1, 0]]], ), - pytest.param( + ( games.create_two_player_perfect_info_win_lose_efg(nonterm_outcomes=True), [[[0, 1], [1, 0]], [[1, 0], [1, 0]]], - marks=pytest.mark.xfail(reason="Problem with non-standard outcomes") ), ( games.create_2x2_zero_sum_efg(missing_term_outcome=False), [[["1/2", "1/2"]], [["1/2", "1/2"]]] ), - pytest.param( + ( games.create_2x2_zero_sum_efg(missing_term_outcome=True), [[["1/2", "1/2"]], [["1/2", "1/2"]]], - marks=pytest.mark.xfail(reason="Problem with non-standard outcomes") ), (games.create_matching_pennies_efg(with_neutral_outcome=False), [[["1/2", "1/2"]], [["1/2", "1/2"]]]), - pytest.param( + ( games.create_matching_pennies_efg(with_neutral_outcome=True), [[["1/2", "1/2"]], [["1/2", "1/2"]]], - marks=pytest.mark.xfail(reason="Problem with non-standard outcomes") ), ( games.create_stripped_down_poker_efg(), [[[1, 0], ["1/3", "2/3"]], [["2/3", "1/3"]]], ), - pytest.param( + ( games.create_stripped_down_poker_efg(nonterm_outcomes=True), [[[1, 0], ["1/3", "2/3"]], [["2/3", "1/3"]]], - marks=pytest.mark.xfail(reason="Problem with non-standard outcomes") ), ( games.create_kuhn_poker_efg(), @@ -665,7 +653,7 @@ def test_lp_behavior_double(): [[1, 0], ["2/3", "1/3"], [0, 1], [0, 1], ["2/3", "1/3"], [1, 0]], ], ), - pytest.param( + ( games.create_kuhn_poker_efg(nonterm_outcomes=True), [ [ @@ -678,7 +666,6 @@ def test_lp_behavior_double(): ], [[1, 0], ["2/3", "1/3"], [0, 1], [0, 1], ["2/3", "1/3"], [1, 0]], ], - marks=pytest.mark.xfail(reason="Problem with non-standard outcomes") ), ( games.create_seq_form_STOC_paper_zero_sum_2_player_efg(), @@ -698,13 +685,12 @@ def test_lp_behavior_double(): [["2/3", "1/3"], ["2/3", "1/3"], ["1/3", "2/3"]], ] ), - pytest.param( + ( games.create_three_action_internal_outcomes_efg(nonterm_outcomes=True), [ [["1/3", 0, "2/3"], ["2/3", 0, "1/3"]], [["2/3", "1/3"], ["2/3", "1/3"], ["1/3", "2/3"]], ], - marks=pytest.mark.xfail(reason="Problem with non-standard outcomes") ), ( games.create_large_payoff_game_efg(), @@ -721,13 +707,12 @@ def test_lp_behavior_double(): [[1, 0], ["6/11", "5/11"]] ], ), - pytest.param( + ( games.create_chance_in_middle_efg(nonterm_outcomes=True), [ [["3/11", "8/11"], [1, 0], [1, 0], [1, 0], [1, 0]], [[1, 0], ["6/11", "5/11"]] ], - marks=pytest.mark.xfail(reason="Problem with non-standard outcomes") ), ], ) From 068a98e1690a2b7ce811386ccde4cfbbcf45f595 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Fri, 19 Dec 2025 12:25:05 +0000 Subject: [PATCH 4/4] Updated ChangeLog --- ChangeLog | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/ChangeLog b/ChangeLog index 5b014fd15..ff25118c9 100644 --- a/ChangeLog +++ b/ChangeLog @@ -2,9 +2,19 @@ ## [16.5.0] - unreleased +### Fixed +- Sequence-form based equilibrium-finding methods returned incorrect output on games with + outcomes at non-terminal nodes. (#654) ### Added - Implement `IsAbsentMinded()` on information sets (C++) and `Infoset.is_absent_minded` (Python) to detect if an information is absent-minded. +- Tests for EFG Nash solvers -- `enumpoly_solve`, `lp_solve`, `lcp_solve` -- in behavior stratgegies +- In `pygambit`, `Node` objects now have a read-only property `own_prior_action` and `Infoset` objects + have a read-only property `own_prior_actions` to retrieve the last action or the set of last actions + taken by the player before reaching the node or information set, respectively. (#582) +- In `pygambit`, `Node` objects now have a read-only property `is_strategy_reachable` to determine + if the node is reachable by at least one pure strategy profile. This proves useful for identifying + unreachable parts of the game tree in games with absent-mindedness. (#629) ### Changed - In the graphical interface, removed option to configure information set link drawing; information sets @@ -14,15 +24,6 @@ - In `pygambit`, `min_payoff` and `max_payoff` (for both games and players) now refers to payoffs in any play of the game; previously this referred only to the set of outcomes. (#498) -### Added -- Tests for EFG Nash solvers -- `enumpoly_solve`, `lp_solve`, `lcp_solve` -- in behavior stratgegies -- In `pygambit`, `Node` objects now have a read-only property `own_prior_action` and `Infoset` objects - have a read-only property `own_prior_actions` to retrieve the last action or the set of last actions - taken by the player before reaching the node or information set, respectively. (#582) -- In `pygambit`, `Node` objects now have a read-only property `is_strategy_reachable` to determine - if the node is reachable by at least one pure strategy profile. This proves useful for identifying - unreachable parts of the game tree in games with absent-mindedness. (#629) - ### Removed - Eliminating dominated actions has been removed from the GUI as it was implementing a non-standard formulation of dominance. (#612)