From 5a2898af420b55147f5471d207c5f0cde101335e Mon Sep 17 00:00:00 2001 From: Rahul Savani Date: Mon, 3 Nov 2025 16:24:28 +0000 Subject: [PATCH] tests for EFG Nash solvers -- enumpoly, lp, lcp -- in behavior stratgegies --- ChangeLog | 3 + pyproject.toml | 12 +- tests/games.py | 286 +++++++++++++++++++++++++++++++++++++++- tests/test_behav.py | 4 +- tests/test_extensive.py | 2 +- tests/test_nash.py | 228 +++++++++++++++++++++++++++----- 6 files changed, 491 insertions(+), 44 deletions(-) diff --git a/ChangeLog b/ChangeLog index c98fe0f2e..c1066f9fa 100644 --- a/ChangeLog +++ b/ChangeLog @@ -6,6 +6,9 @@ - In the graphical interface, removed option to configure information set link drawing; information sets are always drawn and indicators are always drawn if an information set spans multiple levels. +### Added +- Tests for EFG Nash solvers -- `enumpoly_solve`, `lp_solve`, `lcp_solve` -- in behavior stratgegies + ## [16.4.1] - unreleased diff --git a/pyproject.toml b/pyproject.toml index 1206c9a1b..860e21e9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ select = [ "SIM", # flake8-simplify "I", # isort "Q", # prefer double quotes + "W291", # trailing-whitespace ] ignore = [] @@ -75,11 +76,12 @@ max-line-length = 99 [tool.pytest.ini_options] addopts = "--strict-markers" markers = [ - "nash_enummixed_strategy: tests of enummixed_solve in strategies", - "nash_lcp_strategy: tests of lcp_solve in strategies", - "nash_lcp_behavior: tests of lcp_solve in behaviors", - "nash_lp_strategy: tests of lp_solve in strategies", - "nash_lp_behavior: tests of lp_solve in behaviors", + "nash_enummixed_strategy: tests of enummixed_solve in mixed strategies", + "nash_enumpoly_behavior: tests of enumpoly_solve in behavior strategies", + "nash_lcp_strategy: tests of lcp_solve in mixed strategies", + "nash_lcp_behavior: tests of lcp_solve in behavior strategies", + "nash_lp_strategy: tests of lp_solve in mixed strategies", + "nash_lp_behavior: tests of lp_solve in behavior strategies", "nash: all tests of Nash equilibrium solvers", "slow: all time-consuming tests", ] diff --git a/tests/games.py b/tests/games.py index 5ce33b629..5997924bd 100644 --- a/tests/games.py +++ b/tests/games.py @@ -1,8 +1,8 @@ """A utility module to create/load games for the test suite.""" +import itertools import pathlib from abc import ABC, abstractmethod -from itertools import product import numpy as np @@ -18,6 +18,26 @@ def read_from_file(fn: str) -> gbt.Game: raise ValueError(f"Unknown file extension in {fn}") +def create_efg_corresponding_to_bimatrix_game( + A: np.ndarray, B: np.ndarray, title: str +) -> 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 by two numpy arrays. + Player 1 moves first. + """ + assert A.shape == B.shape + m, n = A.shape + g = gbt.Game.new_tree(players=["1", "2"], title=title) + actions1 = [str(i) for i in range(m)] + actions2 = [str(i) for i in range(n)] + g.append_move(g.root, "1", actions1) + g.append_move(g.root.children, "2", actions2) + for i, j in itertools.product(range(m), range(n)): + g.set_outcome(g.root.children[i].children[j], g.add_outcome([A[i, j], B[i, j]])) + return g + + ################################################################################################ # Normal-form (aka strategic-form) games (nfg) @@ -87,10 +107,22 @@ def create_mixed_behav_game_efg() -> gbt.Game: Game Three-player extensive form game: binary tree with 3 infomation sets, one per player, with 1, 2, and 4 nodes respectively + + Since no information is revealed this is directly equivalent to a simultaneous move game """ return read_from_file("mixed_behavior_game.efg") +def create_1_card_poker_efg() -> gbt.Game: + """ + Returns + ------- + Game + One-card two-player poker game, as used in the user guide + """ + return read_from_file("poker.efg") + + def create_myerson_2_card_poker_efg() -> gbt.Game: """ Returns @@ -104,6 +136,119 @@ def create_myerson_2_card_poker_efg() -> gbt.Game: return read_from_file("myerson_2_card_poker.efg") +def create_kuhn_poker_efg() -> gbt.Game: + """ + Returns + ------- + Game + Kuhn poker with 3 cards and 2 players + """ + g = gbt.Game.new_tree( + players=["Alice", "Bob"], title="Three-card poker (J, Q, K), two-player" + ) + deals = ["JQ", "JK", "QJ", "QK", "KJ", "KQ"] + g.append_move(g.root, g.players.chance, deals) + g.set_chance_probs(g.root.infoset, [gbt.Rational(1, 6)]*6) + # group the children of the root (indices of `deals`) by each player's dealt card + alice_grouping = [[0, 1], [2, 3], [4, 5]] # J, Q, K + bob_grouping = [[0, 5], [1, 3], [2, 4]] # Q, K, J + + # Alice's first move + for ij in alice_grouping: + term_nodes = [g.root.children[k] for k in ij] + g.append_move(term_nodes, "Alice", ["Check", "Bet"]) + # Bob's move after Alice checks + for ij in bob_grouping: + term_nodes = [g.root.children[k].children[0] for k in ij] + g.append_move(term_nodes, "Bob", ["Check", "Bet"]) + # Alice's move if Bob's second action is bet + for ij in alice_grouping: + term_nodes = [g.root.children[k].children[0].children[1] for k in ij] + g.append_move(term_nodes, "Alice", ["Fold", "Call"]) + # Bob's move after Alice bets initially + for ij in bob_grouping: + term_nodes = [g.root.children[k].children[1] for k in ij] + g.append_move(term_nodes, "Bob", ["Fold", "Call"]) + + def calculate_payoffs(term_node): + + def get_path(node): + path = [] + while node.parent: + path.append(node.prior_action.label) + node = node.parent + return path + + def showdown_winner(deal): + # deal is an element of deals = ["JQ", "JK", "QJ", "QK", "KJ", "KQ"] + card_values = dict(J=0, Q=1, K=2) + a, b = deal + return "Alice" if card_values[a] > card_values[b] else "Bob" + + def showdown(deal, payoffs, pot): + payoffs[showdown_winner(deal)] += pot + return payoffs + + def bet(player, payoffs, pot): + payoffs[player] += -1 + pot += 1 + return payoffs, pot + + path = get_path(term_node) + deal = path.pop() # needed if there is a showdown + payoffs = dict(Alice=-1, Bob=-1) # ante of 1 for both players + pot = 2 + if path.pop() == "Check": # Alice checks + if path.pop() == "Check": # Bob checks + payoffs = showdown(deal, payoffs, pot) + else: # Bob bets + payoffs, pot = bet("Bob", payoffs, pot) + if path.pop() == "Fold": # Alice folds + payoffs["Bob"] += pot + else: # Alice calls + payoffs, pot = bet("Alice", payoffs, pot) + payoffs = showdown(deal, payoffs, pot) + else: # Alice bets + payoffs, pot = bet("Alice", payoffs, pot) + if path.pop() == "Fold": # Bob + payoffs["Alice"] += pot + else: # Bob calls + payoffs, pot = bet("Bob", payoffs, pot) + payoffs = showdown(deal, payoffs, pot) + + return tuple(payoffs.values()) + + # create 4 possible outcomes just once + payoffs_to_outcomes = {(1, -1): g.add_outcome([1, -1], label="Alice wins 1"), + (2, -2): g.add_outcome([2, -2], label="Alice wins 2"), + (-1, 1): g.add_outcome([-1, 1], label="Bob wins 1"), + (-2, 2): g.add_outcome([-2, 2], label="Bob wins 2")} + + for term_node in [n for n in g.nodes if n.is_terminal]: + outcome = payoffs_to_outcomes[calculate_payoffs(term_node)] + g.set_outcome(term_node, outcome) + + # Ensure infosets are in the same order as if game was written to efg and read back in + g.sort_infosets() + return g + + +def create_one_shot_trust_efg() -> gbt.Game: + g = gbt.Game.new_tree( + players=["Buyer", "Seller"], title="One-shot trust game, after Kreps (1990)" + ) + g.append_move(g.root, "Buyer", ["Trust", "Not trust"]) + g.append_move(g.root.children[0], "Seller", ["Honor", "Abuse"]) + g.set_outcome( + g.root.children[0].children[0], g.add_outcome([1, 1], label="Trustworthy") + ) + g.set_outcome( + g.root.children[0].children[1], g.add_outcome([-1, 2], label="Untrustworthy") + ) + g.set_outcome(g.root.children[1], g.add_outcome([0, 0], label="Opt-out")) + return g + + def create_centipede_game_with_chance_efg() -> gbt.Game: """ Returns @@ -198,7 +343,6 @@ def create_reduction_generic_payoffs_efg() -> gbt.Game: ) g.set_outcome(g.root.children[3], g.add_outcome([12, -12], label="d")) - return g @@ -236,6 +380,138 @@ def create_reduction_both_players_payoff_ties_efg() -> gbt.Game: return g +def create_seq_form_STOC_paper_zero_sum_2_player_efg() -> gbt.Game: + """ + Example from + + Fast Algorithms for Finding Randomized Strategies in Game Trees (1994) + Koller, Megiddo, von Stengel + """ + g = gbt.Game.new_tree(players=["1", "2"], title="From STOC'94 paper") + g.append_move(g.root, g.players.chance, actions=["1", "2", "3", "4"]) + g.set_chance_probs(g.root.infoset, [0.2, 0.2, 0.2, 0.4]) + g.append_move(g.root.children[0], player="1", actions=["l", "r"]) + g.append_move(g.root.children[1], player="1", actions=["c", "d"]) + g.append_infoset(g.root.children[2], g.root.children[1].infoset) + g.append_move(g.root.children[0].children[1], player="2", actions=["p", "q"]) + g.append_move( + g.root.children[0].children[1].children[0], player="1", actions=["L", "R"] + ) + g.append_infoset( + g.root.children[0].children[1].children[1], + g.root.children[0].children[1].children[0].infoset, + ) + g.append_move(g.root.children[2].children[0], player="2", actions=["s", "t"]) + g.append_infoset( + g.root.children[2].children[1], g.root.children[2].children[0].infoset + ) + + g.set_outcome( + g.root.children[0].children[0], + outcome=g.add_outcome(payoffs=[5, -5], label="l"), + ) + g.set_outcome( + g.root.children[0].children[1].children[0].children[0], + outcome=g.add_outcome(payoffs=[10, -10], label="rpL"), + ) + g.set_outcome( + g.root.children[0].children[1].children[0].children[1], + outcome=g.add_outcome(payoffs=[15, -15], label="rpR"), + ) + g.set_outcome( + g.root.children[0].children[1].children[1].children[0], + outcome=g.add_outcome(payoffs=[20, -20], label="rqL"), + ) + g.set_outcome( + g.root.children[0].children[1].children[1].children[1], + outcome=g.add_outcome(payoffs=[-5, 5], label="rqR"), + ) + g.set_outcome( + g.root.children[1].children[0], + outcome=g.add_outcome(payoffs=[10, -10], label="c"), + ) + g.set_outcome( + g.root.children[1].children[1], + outcome=g.add_outcome(payoffs=[20, -20], label="d"), + ) + g.set_outcome( + g.root.children[2].children[0].children[0], + outcome=g.add_outcome(payoffs=[20, -20], label="cs"), + ) + g.set_outcome( + g.root.children[2].children[0].children[1], + outcome=g.add_outcome(payoffs=[50, -50], label="ct"), + ) + g.set_outcome( + g.root.children[2].children[1].children[0], + outcome=g.add_outcome(payoffs=[30, -30], label="ds"), + ) + g.set_outcome( + g.root.children[2].children[1].children[1], + outcome=g.add_outcome(payoffs=[15, -15], label="dt"), + ) + g.set_outcome( + g.root.children[3], outcome=g.add_outcome(payoffs=[5, -5], label="nothing") + ) + g.root.children[0].infoset.label = "0" + g.root.children[1].infoset.label = "1" + g.root.children[0].children[1].infoset.label = "01" + g.root.children[2].children[0].infoset.label = "20" + g.root.children[0].children[1].children[0].infoset.label = "010" + + return g + + +def create_two_player_perfect_info_win_lose_efg() -> gbt.Game: + g = gbt.Game.new_tree(players=["1", "2"], title="2 player perfect info win lose") + g.append_move(g.root, "2", ["a", "b"]) + g.append_move(g.root.children[0], "1", ["L", "R"]) + g.append_move(g.root.children[1], "1", ["L", "R"]) + g.append_move(g.root.children[0].children[0], "2", ["l", "r"]) + g.set_outcome( + g.root.children[0].children[0].children[0], g.add_outcome([1, -1], label="aLl") + ) + g.set_outcome( + g.root.children[0].children[0].children[1], g.add_outcome([-1, 1], label="aLr") + ) + g.set_outcome(g.root.children[0].children[1], g.add_outcome([1, -1], label="aR")) + g.set_outcome(g.root.children[1].children[0], g.add_outcome([1, -1], label="bL")) + g.set_outcome(g.root.children[1].children[1], g.add_outcome([-1, 1], label="bR")) + return g + + +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) + + +def create_EFG_for_6x6_bimatrix_with_long_LH_paths_and_unique_eq() -> gbt.Game: + # 6 x 6 Payoff matrix A: + A = [ + [-180, 72, -333, 297, -153, 270], + [-30, 17, -33, 42, -3, 20], + [-81, 36, -126, 126, -36, 90], + [90, -36, 126, -126, 36, -81], + [20, -3, 42, -33, 17, -30], + [270, -153, 297, -333, 72, -180], + ] + # 6 x 6 Payoff matrix B: + B = [ + [72, 36, 17, -3, -36, -153], + [-180, -81, -30, 20, 90, 270], + [297, 126, 42, -33, -126, -333], + [-333, -126, -33, 42, 126, 297], + [270, 90, 20, -30, -81, -180], + [-153, -36, -3, 17, 36, 72], + ] + 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) + + class EfgFamilyForReducedStrategicFormTests(ABC): """ """ @@ -515,17 +791,17 @@ def _redu_strats(self, player, level): first_half = tmp[:n_half] second_half = tmp[n_half:] # create first half suffix - first_half = product(first_half, first_half) + first_half = itertools.product(first_half, first_half) first_half = ["".join(t) for t in first_half] first_half = ["1" + t for t in first_half] # add 1 to front # create second half suffix - second_half = product(second_half, second_half) + second_half = itertools.product(second_half, second_half) second_half = ["".join(t) for t in second_half] second_half = ["2" + t for t in second_half] # add 2 to front return first_half + second_half # glue halves together else: # player == 3: tmp = self._redu_strats(player=2, level=level - 1) - tmp = product(tmp, tmp) + tmp = itertools.product(tmp, tmp) tmp = ["".join(t) for t in tmp] return tmp else: diff --git a/tests/test_behav.py b/tests/test_behav.py index fd7e93e85..4514997be 100644 --- a/tests/test_behav.py +++ b/tests/test_behav.py @@ -229,7 +229,7 @@ def test_profile_indexing_by_infoset_and_action_labels_reference(game: gbt.Game, action_label: str, prob: typing.Union[str, float], rational_flag: bool): - """Here we use the infoset label and action label, with some exampels where the action label + """Here we use the infoset label and action label, with some examples where the action label alone throws a ValueError (checked in a separate test) """ profile = game.mixed_behavior_profile(rational=rational_flag) @@ -259,7 +259,7 @@ def test_profile_indexing_by_player_infoset_action_labels_reference(game: gbt.Ga action_label: str, prob: typing.Union[str, float], rational_flag: bool): - """Here we use the infoset label and action label, with some exampels where the action label + """Here we use the infoset label and action label, with some examples where the action label alone throws a ValueError (checked in a separate test) """ profile = game.mixed_behavior_profile(rational=rational_flag) diff --git a/tests/test_extensive.py b/tests/test_extensive.py index b30c15398..25a47b8eb 100644 --- a/tests/test_extensive.py +++ b/tests/test_extensive.py @@ -377,7 +377,7 @@ def test_outcome_index_exception_label(): ], ) def test_reduced_strategic_form( - game: gbt.Game, strategy_labels: list, np_arrays_of_rsf: list + game: gbt.Game, strategy_labels: list, np_arrays_of_rsf: typing.Union[list, None] ): """ We test two things: diff --git a/tests/test_nash.py b/tests/test_nash.py index 1cfde948a..93f94c039 100644 --- a/tests/test_nash.py +++ b/tests/test_nash.py @@ -1,9 +1,12 @@ """Test of calls to Nash equilibrium solvers. -These tests primarily ensure that calling the solvers works and returns -expected results on a very simple game. This is not intended to be a -rigorous test suite for the algorithms across all games. +For many solvers the tests simply ensure that calling the solver works, and then for some cases, +checking the expected results on a very simple game. + +There is better test coverage for +lp_solve, lcp_solve, and enumpoly_solve, all in mixed behaviors. """ +import typing import pytest @@ -11,6 +14,8 @@ from . import games +TOL = 1e-13 # tolerance for floating point assertions + def test_enumpure_strategy(): """Test calls of enumeration of pure strategies.""" @@ -24,8 +29,9 @@ def test_enumpure_agent(): assert len(gbt.nash.enumpure_solve(game, use_strategic=False).equilibria) == 0 -def test_enummixed_strategy_double(): - """Test calls of enumeration of mixed strategy equilibria, floating-point.""" +def test_enummixed_double(): + """Test calls of enumeration of mixed strategy equilibria for 2-player games, floating-point. + """ game = games.read_from_file("poker.efg") result = gbt.nash.enummixed_solve(game, rational=False) assert len(result.equilibria) == 1 @@ -34,19 +40,99 @@ def test_enummixed_strategy_double(): @pytest.mark.nash @pytest.mark.nash_enummixed_strategy -def test_enummixed_strategy_rational(): +def test_enummixed_rational(): """Test calls of enumeration of mixed strategy equilibria, rational precision.""" game = games.read_from_file("poker.efg") result = gbt.nash.enummixed_solve(game, rational=True) assert len(result.equilibria) == 1 expected = game.mixed_strategy_profile( rational=True, - data=[[gbt.Rational(1, 3), gbt.Rational(2, 3), gbt.Rational(0), gbt.Rational(0)], - [gbt.Rational(2, 3), gbt.Rational(1, 3)]] + data=[ + [gbt.Rational(1, 3), gbt.Rational(2, 3), gbt.Rational(0), gbt.Rational(0)], + [gbt.Rational(2, 3), gbt.Rational(1, 3)], + ], ) assert result.equilibria[0] == expected +@pytest.mark.nash +@pytest.mark.nash_enumpoly_behavior +@pytest.mark.parametrize( + "game,mixed_behav_prof_data,stop_after", + [ + # 2-player zero-sum games + ( + games.create_1_card_poker_efg(), + [[[[1, 0], ["1/3", "2/3"]], [["2/3", "1/3"]]]], + None, + ), + ( + games.create_myerson_2_card_poker_efg(), + [[[[1, 0], ["1/3", "2/3"]], [["2/3", "1/3"]]]], + None, + ), + # 2-player non-zero-sum games + ( + games.create_one_shot_trust_efg(), + [[[[0, 1]], [["1/2", "1/2"]]], [[[0, 1]], [[0, 0]]]], + 2, + ), # Note all zero probs at iset + ( + games.create_EFG_for_nxn_bimatrix_coordination_game(3), + [ + [[["1/3", "1/3", "1/3"]], [["1/3", "1/3", "1/3"]]], + [[["1/2", "1/2", 0]], [["1/2", "1/2", 0]]], + [[["1/2", 0, "1/2"]], [["1/2", 0, "1/2"]]], + [[[1, 0, 0]], [[1, 0, 0]]], + [[[0, "1/2", "1/2"]], [[0, "1/2", "1/2"]]], + [[[0, 1, 0]], [[0, 1, 0]]], + [[[0, 0, 1]], [[0, 0, 1]]], + ], + None, + ), + ( + games.create_EFG_for_nxn_bimatrix_coordination_game(4), + [[[["1/4", "1/4", "1/4", "1/4"]], [["1/4", "1/4", "1/4", "1/4"]]]], + 1, + ), + # 3-player game + ( + games.create_mixed_behav_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 + ), + ], +) +def test_enumpoly_behavior_rational( + game: gbt.Game, mixed_behav_prof_data: list, stop_after: typing.Union[None, int] +): + """Test calls of enumpoly for mixed behavior equilibria, rational precision, + using max_regret (internal consistency); and comparison to a set of previously + computed equilibria using this function (regression test). + This set will be the full set of all computed equilibria if stop_after is None, + else the first stop_after-many equilibria. + """ + if stop_after: + result = gbt.nash.enumpoly_solve( + game, use_strategic=False, stop_after=stop_after + ) + assert len(result.equilibria) == stop_after + else: + # compute all + result = gbt.nash.enumpoly_solve(game, use_strategic=False) + assert len(result.equilibria) == len(mixed_behav_prof_data) + for eq, exp in zip(result.equilibria, mixed_behav_prof_data): + assert abs(eq.max_regret()) <= TOL + expected = game.mixed_behavior_profile(rational=True, data=exp) + for p in game.players: + for i in p.infosets: + for a in i.actions: + assert abs(eq[p][i][a] - expected[p][i][a]) <= TOL + + def test_lcp_strategy_double(): """Test calls of LCP for mixed strategy equilibria, floating-point.""" game = games.read_from_file("poker.efg") @@ -64,8 +150,10 @@ def test_lcp_strategy_rational(): assert len(result.equilibria) == 1 expected = game.mixed_strategy_profile( rational=True, - data=[[gbt.Rational(1, 3), gbt.Rational(2, 3), gbt.Rational(0), gbt.Rational(0)], - [gbt.Rational(2, 3), gbt.Rational(1, 3)]] + data=[ + [gbt.Rational(1, 3), gbt.Rational(2, 3), gbt.Rational(0), gbt.Rational(0)], + [gbt.Rational(2, 3), gbt.Rational(1, 3)], + ], ) assert result.equilibria[0] == expected @@ -80,16 +168,63 @@ def test_lcp_behavior_double(): @pytest.mark.nash @pytest.mark.nash_lcp_behavior -def test_lcp_behavior_rational(): - """Test calls of LCP for mixed behavior equilibria, rational precision.""" - game = games.read_from_file("poker.efg") +@pytest.mark.parametrize( + "game,mixed_behav_prof_data", + [ + # Zero-sum games (also tested with lp solve) + (games.create_1_card_poker_efg(), [[[1, 0], ["1/3", "2/3"]], [["2/3", "1/3"]]]), + (games.create_myerson_2_card_poker_efg(), [[[1, 0], ["1/3", "2/3"]], [["2/3", "1/3"]]]), + ( + games.create_kuhn_poker_efg(), + [ + [ + ["2/3", "1/3"], + [1, 0], + [1, 0], + ["1/3", "2/3"], + [0, 1], + ["1/2", "1/2"], + ], + [[1, 0], ["2/3", "1/3"], [0, 1], [0, 1], ["2/3", "1/3"], [1, 0]], + ], + ), + # In the next test case: + # 1/2-1/2 for l/r is determined by MixedBehaviorProfile.UndefinedToCentroid() + ( + games.create_two_player_perfect_info_win_lose_efg(), + [[[0, 1], [1, 0]], [[0, 1], ["1/2", "1/2"]]], + ), + # Non-zero-sum games + ( + games.create_reduction_both_players_payoff_ties_efg(), + [[[0, 0, 1, 0], [1, 0]], [[0, 1], [0, 1], [0, 1], [0, 1]]], + ), + ( + games.create_EFG_for_6x6_bimatrix_with_long_LH_paths_and_unique_eq(), + [ + [["1/30", "1/6", "3/10", "3/10", "1/6", "1/30"]], + [["1/6", "1/30", "3/10", "3/10", "1/30", "1/6"]], + ], + ), + (games.create_EFG_for_nxn_bimatrix_coordination_game(3), [[[0, 0, 1]], [[0, 0, 1]]]), + ( + games.create_EFG_for_nxn_bimatrix_coordination_game(4), + [[[0, 0, 0, 1]], [[0, 0, 0, 1]]], + ), + ], +) +def test_lcp_behavior_rational(game: gbt.Game, mixed_behav_prof_data: list): + """Test calls of LCP for mixed behavior equilibria, rational precision. + + using max_regret (internal consistency); and comparison to a previously + computed equilibrium using this function (regression test) + """ result = gbt.nash.lcp_solve(game, use_strategic=False, rational=True) assert len(result.equilibria) == 1 - expected = game.mixed_behavior_profile(rational=True, - data=[[[1, 0], - [gbt.Rational("1/3"), gbt.Rational("2/3")]], - [[gbt.Rational("2/3"), gbt.Rational("1/3")]]]) - assert result.equilibria[0] == expected + eq = result.equilibria[0] + assert eq.max_regret() == 0 + expected = game.mixed_behavior_profile(rational=True, data=mixed_behav_prof_data) + assert eq == expected def test_lp_strategy_double(): @@ -109,8 +244,10 @@ def test_lp_strategy_rational(): assert len(result.equilibria) == 1 expected = game.mixed_strategy_profile( rational=True, - data=[[gbt.Rational(1, 3), gbt.Rational(2, 3), gbt.Rational(0), gbt.Rational(0)], - [gbt.Rational(2, 3), gbt.Rational(1, 3)]] + data=[ + [gbt.Rational(1, 3), gbt.Rational(2, 3), gbt.Rational(0), gbt.Rational(0)], + [gbt.Rational(2, 3), gbt.Rational(1, 3)], + ], ) assert result.equilibria[0] == expected @@ -126,16 +263,45 @@ def test_lp_behavior_double(): @pytest.mark.nash @pytest.mark.slow @pytest.mark.nash_lp_behavior -def test_lp_behavior_rational(): - """Test calls of LP for mixed behavior equilibria, rational precision.""" - game = games.read_from_file("poker.efg") +@pytest.mark.parametrize( + "game,mixed_behav_prof_data", + [ + ( + games.create_two_player_perfect_info_win_lose_efg(), + [[[0, 1], [1, 0]], [[1, 0], [1, 0]]], + ), + (games.create_1_card_poker_efg(), [[[1, 0], ["1/3", "2/3"]], [["2/3", "1/3"]]]), + ( + games.create_myerson_2_card_poker_efg(), + [[[1, 0], ["1/3", "2/3"]], [["2/3", "1/3"]]], + ), + ( + games.create_kuhn_poker_efg(), + [ + [[1, 0], [1, 0], [1, 0], ["2/3", "1/3"], [1, 0], [0, 1]], + [[1, 0], ["2/3", "1/3"], [0, 1], [0, 1], ["2/3", "1/3"], [1, 0]], + ], + ), + ( + games.create_seq_form_STOC_paper_zero_sum_2_player_efg(), + [ + [[0, 1], ["1/3", "2/3"], ["2/3", "1/3"]], + [["5/6", "1/6"], ["5/9", "4/9"]], + ], + ), + ], +) +def test_lp_behavior_rational(game: gbt.Game, mixed_behav_prof_data: list): + """Test calls of LP for mixed behavior equilibria, rational precision, + using max_regret (internal consistency); and comparison to a previously + computed equilibrium using this function (regression test) + """ result = gbt.nash.lp_solve(game, use_strategic=False, rational=True) assert len(result.equilibria) == 1 - expected = game.mixed_behavior_profile(rational=True, - data=[[[1, 0], - [gbt.Rational("1/3"), gbt.Rational("2/3")]], - [[gbt.Rational("2/3"), gbt.Rational("1/3")]]]) - assert result.equilibria[0] == expected + eq = result.equilibria[0] + assert eq.max_regret() == 0 + expected = game.mixed_behavior_profile(rational=True, data=mixed_behav_prof_data) + assert eq == expected def test_liap_strategy(): @@ -179,7 +345,7 @@ def test_logit_strategy(): def test_logit_behavior(): - """Test calls of logit for mixed behavior equilibria.""" + """Test calls of logit for behavior equilibria.""" game = games.read_from_file("poker.efg") result = gbt.nash.logit_solve(game, use_strategic=False) assert len(result.equilibria) == 1 @@ -212,7 +378,7 @@ def test_logit_solve_branch_error_with_invalid_max_accel(): def test_logit_solve_branch(): game = games.read_from_file("const_sum_game.nfg") assert len(gbt.qre.logit_solve_branch( - game=game, maxregret=0.2, first_step=0.2, max_accel=1)) > 0 + game=game, maxregret=0.2, first_step=0.2, max_accel=1)) > 0 def test_logit_solve_lambda_error_with_invalid_first_step(): @@ -234,4 +400,4 @@ def test_logit_solve_lambda_error_with_invalid_max_accel(): def test_logit_solve_lambda(): game = games.read_from_file("const_sum_game.nfg") assert len(gbt.qre.logit_solve_lambda( - game=game, lam=[1, 2, 3], first_step=0.2, max_accel=1)) > 0 + game=game, lam=[1, 2, 3], first_step=0.2, max_accel=1)) > 0