diff --git a/build_support/catalog/catalog.am b/build_support/catalog/catalog.am index 9c461ec62e..cde07fd95a 100644 --- a/build_support/catalog/catalog.am +++ b/build_support/catalog/catalog.am @@ -30,6 +30,7 @@ CATALOG_FILES = \ catalog/conf/itcs/jakobsen2016/fig1c.efg \ catalog/conf/itcs/jakobsen2016/fig3.efg \ catalog/journals/dcg/vonstengel1999/6x6_game_with_75_eq.nfg \ + catalog/journals/dcg/vonstengel1999/6x6_game_with_75_eq_small_payoffs.nfg \ catalog/journals/geb/bagwell1995.efg \ catalog/journals/geb/gilboa1997/fig1.efg \ catalog/journals/geb/gilboa1997/fig2.efg \ diff --git a/doc/references.bib b/doc/references.bib index 9c5e45b883..67ca841e70 100644 --- a/doc/references.bib +++ b/doc/references.bib @@ -443,3 +443,14 @@ @techreport{vS97 note = {Technical Report 264, Departement Informatik}, category = {articles_equilibria} } + +@article{SvS06, + year = {2006}, + author = {Rahul Savani and Bernhard von Stengel}, + title = {Hard-to-Solve Bimatrix Games}, + journal = {Econometrica}, + volume = {74}, + number = {2}, + pages = {397--429}, + category = {articles_equilibria} +} diff --git a/src/games/behavspt.cc b/src/games/behavspt.cc index c7f2874b75..9d047b2b64 100644 --- a/src/games/behavspt.cc +++ b/src/games/behavspt.cc @@ -223,39 +223,39 @@ size_t BehaviorSupportProfile::Sequences::size() const BehaviorSupportProfile::Sequences::iterator BehaviorSupportProfile::Sequences::begin() const { - return {m_support->GetSequenceMap(), false}; + return {m_support->GetSequenceMap(), m_support->GetPlayers(), false}; } BehaviorSupportProfile::Sequences::iterator BehaviorSupportProfile::Sequences::end() const { - return {m_support->GetSequenceMap(), true}; + return {m_support->GetSequenceMap(), m_support->GetPlayers(), true}; } BehaviorSupportProfile::Sequences::iterator::iterator( - const std::shared_ptr p_sequences, bool p_end) - : m_sequences(p_sequences) + const std::shared_ptr p_sequences, const GameRep::Players &p_players, bool p_end) + : m_sequences(p_sequences), m_players(p_players) { if (p_end) { - m_currentPlayer = m_sequences->cend(); + m_currentPlayer = m_players.end(); } else { - m_currentPlayer = m_sequences->cbegin(); - m_currentSequence = m_currentPlayer->second.cbegin(); + m_currentPlayer = m_players.begin(); + m_currentSequence = m_sequences->at(*m_currentPlayer).cbegin(); } } BehaviorSupportProfile::Sequences::iterator & BehaviorSupportProfile::Sequences::iterator::operator++() { - if (m_currentPlayer == m_sequences->cend()) { + if (m_currentPlayer == m_players.end()) { return *this; } m_currentSequence++; - if (m_currentSequence != m_currentPlayer->second.cend()) { + if (m_currentSequence != m_sequences->at(*m_currentPlayer).cend()) { return *this; } - m_currentPlayer++; - if (m_currentPlayer != m_sequences->cend()) { - m_currentSequence = m_currentPlayer->second.cbegin(); + ++m_currentPlayer; + if (m_currentPlayer != m_players.end()) { + m_currentSequence = m_sequences->at(*m_currentPlayer).cbegin(); } return *this; } @@ -265,7 +265,7 @@ bool BehaviorSupportProfile::Sequences::iterator::operator==(const iterator &it) if (m_sequences != it.m_sequences || m_currentPlayer != it.m_currentPlayer) { return false; } - if (m_currentPlayer == m_sequences->end()) { + if (m_currentPlayer == m_players.end()) { return true; } return (m_currentSequence == it.m_currentSequence); diff --git a/src/games/behavspt.h b/src/games/behavspt.h index 12869a9104..857c60c4a4 100644 --- a/src/games/behavspt.h +++ b/src/games/behavspt.h @@ -156,12 +156,20 @@ class BehaviorSupportProfile { public: class iterator { + // Sequences are visited grouped by player, in the game's canonical player order + // (GameRep::Players), rather than in SequenceMap's own key order. SequenceMap is + // keyed by GamePlayer, whose ordering is based on object addresses; iterating the + // map directly would therefore make the visitation order (and hence, downstream, + // any polynomial variable numbering derived from it) depend on heap layout, which + // can differ from run to run. const std::shared_ptr m_sequences; - SequenceMap::const_iterator m_currentPlayer; + GameRep::Players m_players; + GameRep::Players::iterator m_currentPlayer; std::vector::const_iterator m_currentSequence; public: - iterator(const std::shared_ptr p_sequences, bool p_end); + iterator(const std::shared_ptr p_sequences, const GameRep::Players &p_players, + bool p_end); GameSequence operator*() const { return *m_currentSequence; } GameSequence operator->() const { return *m_currentSequence; } diff --git a/src/solvers/enumpoly/polysolver.cc b/src/solvers/enumpoly/polysolver.cc index 9c51fff6c7..181c7cb3d3 100644 --- a/src/solvers/enumpoly/polysolver.cc +++ b/src/solvers/enumpoly/polysolver.cc @@ -72,6 +72,7 @@ bool PolynomialSystemSolver::NewtonRootInRectangle(const Rectangle &r, newpoint = NewtonStep(point); } catch (SingularMatrixException &) { + bool perturbed = false; for (int i = 1; i <= point.size(); i++) { Vector perturbed_point(point); if (r.Side(i).UpperBound() > point[i]) { @@ -82,11 +83,21 @@ bool PolynomialSystemSolver::NewtonRootInRectangle(const Rectangle &r, } try { newpoint = point + (NewtonStep(perturbed_point) - perturbed_point); + perturbed = true; break; } catch (SingularMatrixException &) { } } + if (!perturbed) { + // The Jacobian is singular at this point, and remains singular under every + // single-coordinate perturbation tried. This is a local degeneracy of the + // search at this point/rectangle, not evidence that the system of equations + // for the support as a whole is degenerate -- so we treat it the same as any + // other failure to confirm a root here, leaving it to the caller to subdivide + // the rectangle further. + return false; + } } if (!r.SameCenterDoubleLengths().Contains(newpoint)) { diff --git a/tests/games.py b/tests/games.py index 1a2dda3ed6..403577ab92 100644 --- a/tests/games.py +++ b/tests/games.py @@ -25,8 +25,20 @@ UNICODE_LABELS = ["é", "naïve", "日本語", "😀"] VALID_LABELS = ["x", "a b", "a b", "a b c", *UNICODE_LABELS] INVALID_LABELS = [ - " x", "x ", " ", "a b", "a\tb", "a\nb", "a\x01b", "a\x7fb", "a…b", - " x", "x ", " ", "a  b", "a
b", + " x", + "x ", + " ", + "a b", + "a\tb", + "a\nb", + "a\x01b", + "a\x7fb", + "a…b", + " x", + "x ", + " ", + "a  b", + "a
b", ] @@ -43,7 +55,7 @@ def read_from_file(fn: str) -> gbt.Game: raise ValueError(f"Unknown file extension in {fn}") -def create_efg_corresponding_to_bimatrix_game( +def create_efg_corresponding_to_bimatrix_game_as_ndarrays( A: np.ndarray, B: np.ndarray, title: str ) -> gbt.Game: """ @@ -61,11 +73,27 @@ def create_efg_corresponding_to_bimatrix_game( for i, j in itertools.product(range(m), range(n)): g.set_outcome( g.root.children[str(i)].children[str(j)], - g.add_outcome(f"({i},{j})", [A[i, j], B[i, j]]) + g.add_outcome(f"({i},{j})", [A[i, j], B[i, j]]), ) return g +def create_efg_corresponding_to_bimatrix_game(g: gbt.Game) -> gbt.Game: + """ + There is no direct pygambit method to create an EFG from a stategic-form game. + Here we create an EFG corresponding to a bimatrix game, given as a Gambit game. + """ + assert len(g.players) == 2 + A, B = g.to_arrays() + title = f"EFG for: {g.title}" + return create_efg_corresponding_to_bimatrix_game_as_ndarrays(A, B, title) + + +def create_efg_corresponding_to_catalog_bimatrix_game(slug: str) -> gbt.Game: + g = gbt.catalog.load(slug) + return create_efg_corresponding_to_bimatrix_game(g) + + ################################################################################################ # Extensive-form games (efg) @@ -85,7 +113,7 @@ def create_2x2_zero_sum_efg(variant: None | str = None) -> gbt.Game: A = np.eye(2) B = -A - g = create_efg_corresponding_to_bimatrix_game(A, B, title) + g = create_efg_corresponding_to_bimatrix_game_as_ndarrays(A, B, title) if variant == "missing term outcome": g.delete_outcome(g.root.children["0"].children["1"].outcome) @@ -406,24 +434,16 @@ def create_one_shot_trust_efg(unique_NE_variant: bool = False) -> gbt.Game: ) g.append_move(g.root, "Buyer", ["Trust", "Not trust"]) g.append_move(g.root.children["Trust"], "Seller", ["Honor", "Abuse"]) - g.set_outcome( - g.root.children["Trust"].children["Honor"], - g.add_outcome("Trustworthy", [1, 1]) - ) + g.set_outcome(g.root.children["Trust"].children["Honor"], g.add_outcome("Trustworthy", [1, 1])) if unique_NE_variant: g.set_outcome( - g.root.children["Trust"].children["Abuse"], - g.add_outcome("Untrustworthy", ["1/2", 2]) + g.root.children["Trust"].children["Abuse"], g.add_outcome("Untrustworthy", ["1/2", 2]) ) else: g.set_outcome( - g.root.children["Trust"].children["Abuse"], - g.add_outcome("Untrustworthy", [-1, 2]) - ) - g.set_outcome( - g.root.children["Not trust"], - g.add_outcome("Opt-out", [0, 0]) + g.root.children["Trust"].children["Abuse"], g.add_outcome("Untrustworthy", [-1, 2]) ) + g.set_outcome(g.root.children["Not trust"], g.add_outcome("Opt-out", [0, 0])) return g @@ -431,7 +451,7 @@ def create_EFG_for_nxn_bimatrix_coordination_game(n: int) -> gbt.Game: A = np.eye(n, dtype=int) B = A title = f"{n}x{n} coordination game, {2**n - 1} equilibria" - return create_efg_corresponding_to_bimatrix_game(A, B, title) + return create_efg_corresponding_to_bimatrix_game_as_ndarrays(A, B, title) def create_EFG_for_6x6_bimatrix_with_long_LH_paths_and_unique_eq() -> gbt.Game: @@ -456,7 +476,7 @@ def create_EFG_for_6x6_bimatrix_with_long_LH_paths_and_unique_eq() -> gbt.Game: A = np.array(A) B = np.array(B) title = "6x6 Long Lemke-Howson Paths, unique eq" - return create_efg_corresponding_to_bimatrix_game(A, B, title) + return create_efg_corresponding_to_bimatrix_game_as_ndarrays(A, B, title) class EfgFamilyForReducedStrategicFormTests(ABC): @@ -686,9 +706,11 @@ def gbt_game(self): for n in g.nodes: if not n.is_terminal and not n.children["L"].is_terminal: left = n.children["L"] - g.make_infoset(list(left.infoset.members) + [n.children["R"]], - left.infoset.player.label, - left.infoset.label or None) + g.make_infoset( + list(left.infoset.members) + [n.children["R"]], + left.infoset.player.label, + left.infoset.label or None, + ) return g def reduced_strategic_form(self): diff --git a/tests/test_nash.py b/tests/test_nash.py index 3a213beafb..a619b19da4 100644 --- a/tests/test_nash.py +++ b/tests/test_nash.py @@ -852,17 +852,20 @@ class QREquilibriumTestCase: ENUMPOLY_STRATEGY_CASES = [ - # 2x2x2 strategic form game based on local max cut -- 2 pure and 1 mixed + # 2-player games + # 3x3 coordination game with 3 pure and 4 mixed equilibria pytest.param( EquilibriumTestCase( - factory=functools.partial( - games.read_from_file, "2x2x2_nfg_from_local_max_cut_2_pure_1_mixed_eq.nfg" - ), - solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=None), + factory=functools.partial(games.create_EFG_for_nxn_bimatrix_coordination_game, n=3), + solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=None, use_strategic=True), expected=[ - [d(1, 0), d(0, 1), d(1, 0)], - [d(0, 1), d(1, 0), d(0, 1)], - [d("1/2", "1/2"), d("1/2", "1/2"), d("1/2", "1/2")], + [d(1, 0, 0), d(1, 0, 0)], + [d(0, 1, 0), d(0, 1, 0)], + [d(0, 0, 1), d(0, 0, 1)], + [d("1/2", "1/2", 0), d("1/2", "1/2", 0)], + [d("1/2", 0, "1/2"), d("1/2", 0, "1/2")], + [d(0, "1/2", "1/2"), d(0, "1/2", "1/2")], + [d("1/3", "1/3", "1/3"), d("1/3", "1/3", "1/3")], ], prob_tol=TOL, regret_tol=TOL, @@ -870,26 +873,88 @@ class QREquilibriumTestCase: marks=pytest.mark.nash_enumpoly_strategy, id="test_enumpoly_strategy_1", ), - # coordination game with 3 pure and 4 mixed equilibria + # 4x4 coordination game pytest.param( EquilibriumTestCase( - factory=functools.partial(games.create_EFG_for_nxn_bimatrix_coordination_game, n=3), + factory=functools.partial(games.create_EFG_for_nxn_bimatrix_coordination_game, n=4), solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=None, use_strategic=True), expected=[ - [d(1, 0, 0), d(1, 0, 0)], - [d(0, 1, 0), d(0, 1, 0)], - [d(0, 0, 1), d(0, 0, 1)], - [d("1/2", "1/2", 0), d("1/2", "1/2", 0)], - [d("1/2", 0, "1/2"), d("1/2", 0, "1/2")], - [d(0, "1/2", "1/2"), d(0, "1/2", "1/2")], - [d("1/3", "1/3", "1/3"), d("1/3", "1/3", "1/3")], + [d(1, 0, 0, 0), d(1, 0, 0, 0)], + [d(0, 1, 0, 0), d(0, 1, 0, 0)], + [d(0, 0, 1, 0), d(0, 0, 1, 0)], + [d(0, 0, 0, 1), d(0, 0, 0, 1)], + [d("1/2", "1/2", 0, 0), d("1/2", "1/2", 0, 0)], + [d("1/2", 0, "1/2", 0), d("1/2", 0, "1/2", 0)], + [d("1/2", 0, 0, "1/2"), d("1/2", 0, 0, "1/2")], + [d(0, "1/2", "1/2", 0), d(0, "1/2", "1/2", 0)], + [d(0, "1/2", 0, "1/2"), d(0, "1/2", 0, "1/2")], + [d(0, 0, "1/2", "1/2"), d(0, 0, "1/2", "1/2")], + [d("1/3", "1/3", "1/3", 0), d("1/3", "1/3", "1/3", 0)], + [d("1/3", "1/3", 0, "1/3"), d("1/3", "1/3", 0, "1/3")], + [d("1/3", 0, "1/3", "1/3"), d("1/3", 0, "1/3", "1/3")], + [d(0, "1/3", "1/3", "1/3"), d(0, "1/3", "1/3", "1/3")], + [d("1/4", "1/4", "1/4", "1/4"), d("1/4", "1/4", "1/4", "1/4")], ], - prob_tol=TOL, regret_tol=TOL, + prob_tol=TOL, ), marks=pytest.mark.nash_enumpoly_strategy, id="test_enumpoly_strategy_2", ), + # Figure 10.5 efg from Game Theory Basics + pytest.param( + EquilibriumTestCase( + factory=functools.partial(gbt.catalog.load, "books/vonstengel2022/fig10.5"), + solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=None, use_strategic=True), + expected=[ + [d(1, 0), d(1, 0, 0, 0)], + [d("1/2", "1/2"), d(0, "2/5", "3/5", 0)], + [d("1/4", "3/4"), d(0, 0, "1/3", "2/3")], + ], + regret_tol=TOL, + prob_tol=TOL, + ), + marks=pytest.mark.nash_enumpoly_strategy, + id="test_enumpoly_strategy_3", + ), + pytest.param( + EquilibriumTestCase( + # Could just use the strategic form from the catalog directly + # Currently like this to compare with use_strategic=False, which is "hanging" + # i.e., not even producing 1 eq in a reasonable amount of time + factory=functools.partial( + games.create_efg_corresponding_to_catalog_bimatrix_game, + "journals/dcg/vonstengel1999/6x6_game_with_75_eq", + ), + solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=1, use_strategic=True), + expected=[ + [d(0, 1, 0, 0, 0, 0), d(0, 0, 0, 0, 0, 1)], + ], + regret_tol=TOL, + prob_tol=TOL, + ), + marks=pytest.mark.nash_enumpoly_behavior, + id="test_enumpoly_strategy_4", + ), + # 3-player games + # 2x2x2 strategic form game based on local max cut -- 2 pure and 1 mixed + pytest.param( + EquilibriumTestCase( + factory=functools.partial( + games.read_from_file, "2x2x2_nfg_from_local_max_cut_2_pure_1_mixed_eq.nfg" + ), + solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=None), + expected=[ + [d(1, 0), d(0, 1), d(1, 0)], + [d(0, 1), d(1, 0), d(0, 1)], + [d("1/2", "1/2"), d("1/2", "1/2"), d("1/2", "1/2")], + ], + prob_tol=TOL, + regret_tol=TOL, + ), + marks=pytest.mark.nash_enumpoly_strategy, + id="test_enumpoly_strategy_5", + ), # A three-player game with a unique Nash equilibrium in irrational mixed strategies # (nau2004 sec4 catalog game) pytest.param( @@ -907,7 +972,7 @@ class QREquilibriumTestCase: regret_tol=TOL, ), marks=pytest.mark.nash_enumpoly_strategy, - id="test_enumpoly_strategy_3", + id="test_enumpoly_strategy_6", ), # A three-player 2x2x2 game with 3 pure, 2 incompletely mixed, and a # continuum of completely mixed Nash equilibria (nau2004 sec5 catalog game) @@ -924,7 +989,7 @@ class QREquilibriumTestCase: regret_tol=TOL, ), marks=pytest.mark.nash_enumpoly_strategy, - id="test_enumpoly_strategy_4", + id="test_enumpoly_strategy_7", ), ] @@ -1177,6 +1242,7 @@ class QREquilibriumTestCase: marks=pytest.mark.nash_lcp_strategy, id="test_lcp_strategy_rational_9", ), + # 4x4 coordination game pytest.param( EquilibriumTestCase( factory=functools.partial(games.create_EFG_for_nxn_bimatrix_coordination_game, n=4), @@ -1339,6 +1405,7 @@ class QREquilibriumTestCase: marks=pytest.mark.nash_lcp_strategy, id="test_lcp_strategy_double_9", ), + # 4x4 coordination game pytest.param( EquilibriumTestCase( factory=functools.partial(games.create_EFG_for_nxn_bimatrix_coordination_game, n=4), @@ -2684,9 +2751,6 @@ def test_nash_strategy_solver_w_start(test_case: EquilibriumTestCaseWithStart, s ENUMPOLY_BEHAVIOR_CASES = [ - # ############################################################# - # Examples where Nash pure behaviors and agent-form pure equillibrium behaviors coincide - # ############################################################# # 2-player zero-sum games pytest.param( EquilibriumTestCase( @@ -2699,7 +2763,7 @@ def test_nash_strategy_solver_w_start(test_case: EquilibriumTestCaseWithStart, s prob_tol=TOL, ), marks=pytest.mark.nash_enumpoly_behavior, - id="test_enumpoly_behavior_0", + id="test_enumpoly_behavior_1", ), # 2-player non-zero-sum games pytest.param( @@ -2713,7 +2777,7 @@ def test_nash_strategy_solver_w_start(test_case: EquilibriumTestCaseWithStart, s prob_tol=TOL, ), marks=pytest.mark.nash_enumpoly_behavior, - id="test_enumpoly_behavior_1a", + id="test_enumpoly_behavior_2", ), pytest.param( EquilibriumTestCase( @@ -2726,7 +2790,7 @@ def test_nash_strategy_solver_w_start(test_case: EquilibriumTestCaseWithStart, s prob_tol=TOL, ), marks=pytest.mark.nash_enumpoly_behavior, - id="test_enumpoly_behavior_1b", + id="test_enumpoly_behavior_3", ), pytest.param( EquilibriumTestCase( @@ -2739,7 +2803,7 @@ def test_nash_strategy_solver_w_start(test_case: EquilibriumTestCaseWithStart, s prob_tol=TOL, ), marks=pytest.mark.nash_enumpoly_behavior, - id="test_enumpoly_behavior_2", + id="test_enumpoly_behavior_4", ), pytest.param( EquilibriumTestCase( @@ -2754,7 +2818,7 @@ def test_nash_strategy_solver_w_start(test_case: EquilibriumTestCaseWithStart, s prob_tol=TOL, ), marks=pytest.mark.nash_enumpoly_behavior, - id="test_enumpoly_behavior_3", + id="test_enumpoly_behavior_5", ), pytest.param( EquilibriumTestCase( @@ -2772,7 +2836,7 @@ def test_nash_strategy_solver_w_start(test_case: EquilibriumTestCaseWithStart, s prob_tol=TOL, ), marks=pytest.mark.nash_enumpoly_behavior, - id="test_enumpoly_behavior_4", + id="test_enumpoly_behavior_6", ), pytest.param( EquilibriumTestCase( @@ -2792,9 +2856,9 @@ def test_nash_strategy_solver_w_start(test_case: EquilibriumTestCaseWithStart, s prob_tol=TOL, ), marks=pytest.mark.nash_enumpoly_behavior, - id="test_enumpoly_behavior_5", + id="test_enumpoly_behavior_7", ), - # 3-player games + # 3x3 coordination game pytest.param( EquilibriumTestCase( factory=functools.partial(games.create_EFG_for_nxn_bimatrix_coordination_game, n=3), @@ -2812,24 +2876,71 @@ def test_nash_strategy_solver_w_start(test_case: EquilibriumTestCaseWithStart, s prob_tol=TOL, ), marks=pytest.mark.nash_enumpoly_behavior, - id="test_enumpoly_behavior_6", + id="test_enumpoly_behavior_8", ), + # 4x4 coordination game pytest.param( EquilibriumTestCase( - factory=functools.partial(games.read_from_file, "3_player.efg"), + factory=functools.partial(games.create_EFG_for_nxn_bimatrix_coordination_game, n=4), solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=None), expected=[ - [[d(1, 0), d(1, 0)], [d(1, 0), d(1, 0)], [d(1, 0), d(1, 0)]], + [[d("1/4", "1/4", "1/4", "1/4")], [d("1/4", "1/4", "1/4", "1/4")]], + [[d("1/3", "1/3", "1/3", 0)], [d("1/3", "1/3", "1/3", 0)]], + [[d("1/3", "1/3", 0, "1/3")], [d("1/3", "1/3", 0, "1/3")]], + [[d("1/2", "1/2", 0, 0)], [d("1/2", "1/2", 0, 0)]], + [[d("1/3", 0, "1/3", "1/3")], [d("1/3", 0, "1/3", "1/3")]], + [[d("1/2", 0, "1/2", 0)], [d("1/2", 0, "1/2", 0)]], + [[d("1/2", 0, 0, "1/2")], [d("1/2", 0, 0, "1/2")]], + [[d(1, 0, 0, 0)], [d(1, 0, 0, 0)]], + [[d(0, "1/3", "1/3", "1/3")], [d(0, "1/3", "1/3", "1/3")]], + [[d(0, "1/2", "1/2", 0)], [d(0, "1/2", "1/2", 0)]], + [[d(0, "1/2", 0, "1/2")], [d(0, "1/2", 0, "1/2")]], + [[d(0, 1, 0, 0)], [d(0, 1, 0, 0)]], + [[d(0, 0, "1/2", "1/2")], [d(0, 0, "1/2", "1/2")]], + [[d(0, 0, 1, 0)], [d(0, 0, 1, 0)]], + [[d(0, 0, 0, 1)], [d(0, 0, 0, 1)]], ], regret_tol=TOL, prob_tol=TOL, ), marks=pytest.mark.nash_enumpoly_behavior, - id="test_enumpoly_behavior_7", + id="test_enumpoly_behavior_9", ), + # Figure 10.5 efg from Game Theory Basics pytest.param( EquilibriumTestCase( - factory=functools.partial(games.read_from_file, "3_player_with_nonterm_outcomes.efg"), + factory=functools.partial(gbt.catalog.load, "books/vonstengel2022/fig10.5"), + solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=None), + expected=[ + [[d("1/2", "1/2")], [d("2/5", "3/5"), d(0, 1), d(1, 0)]], + [[d("1/4", "3/4")], [d(0, 1), d(1, 0), d("1/3", "2/3")]], + [[d(1, 0)], [d(1, 0), d(1, 0), d(1, 0)]], + ], + regret_tol=TOL, + prob_tol=TOL, + ), + marks=pytest.mark.nash_enumpoly_behavior, + id="test_enumpoly_behavior_10", + ), + # 75 eq + # pytest.param( + # EquilibriumTestCase( + # factory=functools.partial(games.create_efg_corresponding_to_catalog_bimatrix_game, + # "journals/dcg/vonstengel1999/6x6_game_with_75_eq"), + # solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=1, use_strategic=True), + # expected=[ + # [[d(0, 1, 0, 0, 0, 0)], [d(0, 0, 0, 0, 0, 1)]], + # ], + # regret_tol=TOL, + # prob_tol=TOL, + # ), + # marks=pytest.mark.nash_enumpoly_behavior, + # id="test_enumpoly_behavior_11", + # ), + # 3-player games + pytest.param( + EquilibriumTestCase( + factory=functools.partial(games.read_from_file, "3_player.efg"), solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=None), expected=[ [[d(1, 0), d(1, 0)], [d(1, 0), d(1, 0)], [d(1, 0), d(1, 0)]], @@ -2838,21 +2949,20 @@ def test_nash_strategy_solver_w_start(test_case: EquilibriumTestCaseWithStart, s prob_tol=TOL, ), marks=pytest.mark.nash_enumpoly_behavior, - id="test_enumpoly_behavior_8", + id="test_enumpoly_behavior_12", ), - # 4-player game pytest.param( EquilibriumTestCase( - factory=functools.partial(games.create_EFG_for_nxn_bimatrix_coordination_game, n=4), - solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=1), + factory=functools.partial(games.read_from_file, "3_player_with_nonterm_outcomes.efg"), + solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=None), expected=[ - [[d("1/4", "1/4", "1/4", "1/4")], [d("1/4", "1/4", "1/4", "1/4")]], + [[d(1, 0), d(1, 0)], [d(1, 0), d(1, 0)], [d(1, 0), d(1, 0)]], ], regret_tol=TOL, prob_tol=TOL, ), marks=pytest.mark.nash_enumpoly_behavior, - id="test_enumpoly_behavior_9", + id="test_enumpoly_behavior_13", ), # 3-player perfect info game to test behavior two off equilibrium path pytest.param( @@ -2869,7 +2979,7 @@ def test_nash_strategy_solver_w_start(test_case: EquilibriumTestCaseWithStart, s prob_tol=TOL, ), marks=pytest.mark.nash_enumpoly_behavior, - id="test_enumpoly_behavior_10", + id="test_enumpoly_behavior_14", ), pytest.param( EquilibriumTestCase( @@ -2887,7 +2997,7 @@ def test_nash_strategy_solver_w_start(test_case: EquilibriumTestCaseWithStart, s prob_tol=TOL, ), marks=pytest.mark.nash_enumpoly_behavior, - id="test_enumpoly_behavior_11", + id="test_enumpoly_behavior_15", ), pytest.param( EquilibriumTestCase( @@ -2907,7 +3017,7 @@ def test_nash_strategy_solver_w_start(test_case: EquilibriumTestCaseWithStart, s prob_tol=TOL, ), marks=pytest.mark.nash_enumpoly_behavior, - id="test_enumpoly_behavior_12", + id="test_enumpoly_behavior_16", ), pytest.param( EquilibriumTestCase( @@ -2923,20 +3033,33 @@ def test_nash_strategy_solver_w_start(test_case: EquilibriumTestCaseWithStart, s prob_tol=TOL, ), marks=pytest.mark.nash_enumpoly_behavior, - id="test_enumpoly_behavior_13", + id="test_enumpoly_behavior_17", + ), + # 3-player game equivalent to a simultaneous-move 2x2x2 game (nine equilibria total, + # including two totally-mixed ones); this previously required an unordered check due + # to run-to-run variation in the order equilibria were found (see #589) + pytest.param( + EquilibriumTestCase( + factory=functools.partial(games.read_from_file, "mixed_behavior_game.efg"), + solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=None), + expected=[ + [[d("1/2", "1/2")], [d("2/5", "3/5")], [d("1/4", "3/4")]], + [[d("2/5", "3/5")], [d("1/2", "1/2")], [d("1/3", "2/3")]], + [[d("1/2", "1/2")], [d("1/2", "1/2")], [d(1, 0)]], + [[d("1/3", "2/3")], [d(1, 0)], [d("1/4", "3/4")]], + [[d(1, 0)], [d(1, 0)], [d(1, 0)]], + [[d(1, 0)], [d(0, 1)], [d(0, 1)]], + [[d(0, 1)], [d("1/4", "3/4")], [d("1/3", "2/3")]], + [[d(0, 1)], [d(1, 0)], [d(0, 1)]], + [[d(0, 1)], [d(0, 1)], [d(1, 0)]], + ], + regret_tol=TOL, + prob_tol=TOL, + ), + marks=pytest.mark.nash_enumpoly_behavior, + id="test_enumpoly_behavior_18", ), ] -# ############################################################################## -# 3-player game -# ( -# games.read_from_file("mixed_behavior_game.efg"), -# [ -# [[["1/2", "1/2"]], [["2/5", "3/5"]], [["1/4", "3/4"]]], -# [[["2/5", "3/5"]], [["1/2", "1/2"]], [["1/3", "2/3"]]], -# ], -# 2, # 9 in total found by enumpoly (see unordered test) -# ), -# ############################################################################## LOGIT_BEHAVIOR_CASES = [ @@ -3007,75 +3130,6 @@ def test_nash_behavior_solver(test_case: EquilibriumTestCase, subtests) -> None: assert abs(eq[action] - expected[action]) <= test_case.prob_tol -################################################################################################## -# BEHVAIOR SOLVER -- UNORDERED -################################################################################################## - -ENUMPOLY_BEHAVIOR_UNORDERED_CASES = [ - pytest.param( - EquilibriumTestCase( - factory=functools.partial(games.read_from_file, "mixed_behavior_game.efg"), - solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=9), - expected=[ - [[["2/5", "3/5"]], [["1/2", "1/2"]], [["1/3", "2/3"]]], - [[["1/2", "1/2"]], [["2/5", "3/5"]], [["1/4", "3/4"]]], - [[["1/2", "1/2"]], [["1/2", "1/2"]], [[1, 0]]], - [[["1/3", "2/3"]], [[1, 0]], [["1/4", "3/4"]]], - [[[1, 0]], [[1, 0]], [[1, 0]]], - [[[1, 0]], [[0, 1]], [[0, 1]]], - [[[0, 1]], [["1/4", "3/4"]], [["1/3", "2/3"]]], - [[[0, 1]], [[1, 0]], [[0, 1]]], - [[[0, 1]], [[0, 1]], [[1, 0]]], - ], - regret_tol=TOL, - prob_tol=TOL, - ), - marks=pytest.mark.nash_enumpoly_behavior, - id="test_enumpoly_behavior_unordered_1", - ), -] - - -@pytest.mark.nash -@pytest.mark.parametrize("test_case", ENUMPOLY_BEHAVIOR_UNORDERED_CASES, ids=lambda c: c.label) -def test_nash_behavior_solver_unordered(test_case: EquilibriumTestCase, subtests) -> None: - """Test calls of Nash solvers in EFGs in mixed behaviors -- UNORDERED - - Subtests: - - Agent max regret no more than `test_case.regret_tol` - - Agent max regret no more than max regret (+ `test_case.regret_tol`) - - Equilibria that are output are distinct and all appear in the expected set - Equilibria are deemed to match if the maximum difference in probabilities is no more - than `test_case.prob_tol` - """ - - def are_the_same(game, found, candidate): - for p in game.players: - for a in p.actions: - if not abs(found[a] - candidate[a]) <= TOL: - return False - return True - - game = test_case.factory() - result = test_case.solver(game) - with subtests.test("number of equilibria found"): - assert len(result.equilibria) == len(test_case.expected) - for i, eq in enumerate(result.equilibria): - with subtests.test(eq=i, check="agent_max_regret"): - assert eq.max_regret() <= test_case.regret_tol - with subtests.test(eq=i, check="max_regret"): - assert eq.agent_max_regret() <= eq.max_regret() + test_case.regret_tol - with subtests.test(eq=i, check="strategy_profile"): - found = False - for exp in test_case.expected[:]: - expected = game.mixed_behavior_profile(rational=True, data=exp) - if are_the_same(game, eq, expected): - test_case.expected.remove(exp) - found = True - break - assert found - - ################################################################################################## # AGENTS NASH SOLVERS (IN MIXED BEHAVIORS) ##################################################################################################