From db2c2ba407a831039bea47a7543e12ce545ff8ef Mon Sep 17 00:00:00 2001 From: Rahul Savani Date: Tue, 16 Dec 2025 14:04:05 +0000 Subject: [PATCH 1/2] consistency/reference tests for MixedStrategyProfile {player,max}_regret --- tests/games.py | 12 ++-- tests/test_mixed.py | 159 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 143 insertions(+), 28 deletions(-) diff --git a/tests/games.py b/tests/games.py index e34591d4d..2292d30aa 100644 --- a/tests/games.py +++ b/tests/games.py @@ -72,14 +72,14 @@ def create_2x2x2_nfg() -> gbt.Game: - The payoff to a player is the sum of their incident edges across the implied cut - Pure equilibrium iff local max cuts; in addition, uniform mixture is an equilibrium - Equilibrium analysis for pure profiles: - a a a: 0 0 0 -- Not Nash (2 can deviate and get 4) - b a a: 1 2 -1 -- Not Nash (3 can deviate and get 2) + a a a: 0 0 0 -- Not Nash (regrets: 1, 4, 1) + b a a: 1 2 -1 -- Not Nash (regrets: 0, 0, 3) a b a: 2 4 2 -- Nash (global max cut) - b b a: -1 2 1 -- Not Nash (1 can deviate and get 2) - a a b: -1 2 1 -- Not Nash (1 can deviate and get 2) + b b a: -1 2 1 -- Not Nash (regrets: 3, 0, 0) + a a b: -1 2 1 -- Not Nash (regrets: 3, 0, 0) b a b: 2 4 2 -- Nash (global max cut) - a b b: 1 2 -1 -- Not Nash (3 can deviate and get 2) - b b b: 0 0 0 -- Not Nash (2 can deviate and get 4) + a b b: 1 2 -1 -- Not Nash (regrets: 0, 0, 3) + b b b: 0 0 0 -- Not Nash (regrets: 1, 4, 1) """ return read_from_file("2x2x2_nfg_with_two_pure_one_mixed_eq.nfg") diff --git a/tests/test_mixed.py b/tests/test_mixed.py index 03d3da50e..f94ff1d28 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -501,7 +501,7 @@ def test_strategy_value_reference(game: gbt.Game, profile_data: list, rational_f @pytest.mark.parametrize( - "game,profile_data,liap_expected,tol,rational_flag", + "game,profile_data,liap_exp,tol,rational_flag", [ ############################################################################## # Zero matrix nfg, all liap_values are zero @@ -513,37 +513,110 @@ def test_strategy_value_reference(game: gbt.Game, profile_data: list, rational_f # 4x4 coordination nfg (games.create_coord_4x4_nfg(), None, 0, ZERO, True), (games.create_coord_4x4_nfg(), None, 0, TOL, False), - (games.create_coord_4x4_nfg(), - [["1/3", "1/2", "1/12", "1/12"], ["3/8", "1/8", "1/4", "1/4"]], "245/2304", ZERO, True), - (games.create_coord_4x4_nfg(), - [["1/4", "1/4", "1/4", "1/4"], ["1/4", "1/4", "1/4", "1/4"]], 0, ZERO, True), (games.create_coord_4x4_nfg(), [[1, 0, 0, 0], [1, 0, 0, 0]], 0, ZERO, True), + (games.create_coord_4x4_nfg(), [[1, 0, 0, 0], [1, 0, 0, 0]], 0, TOL, False), + (games.create_coord_4x4_nfg(), + [["1/3", "1/2", "1/12", "1/12"], ["3/8", "1/8", "1/4", "1/4"]], + "245/2304", ZERO, True), (games.create_coord_4x4_nfg(), [[1/3, 1/2, 1/12, 1/12], [3/8, 1/8, 1/4, 1/4]], 245/2304, TOL, False), + (games.create_coord_4x4_nfg(), + [["1/3", 0, 0, "2/3"], [1, 0, 0, 0]], "5/9", ZERO, True), + (games.create_coord_4x4_nfg(), + [[1/3, 0, 0, 2/3], [1, 0, 0, 0]], 5/9, TOL, False), ############################################################################## # El Farol bar game efg (games.create_el_farol_bar_game_efg(), - [["1/2", "1/2"], ["1/2", "1/2"], ["1/2", "1/2"], ["1/2", "1/2"], ["1/2", "1/2"]], "0", - ZERO, True), - (games.create_el_farol_bar_game_efg(), - [["1/1", "0/1"], ["1/1", "0/1"], ["0/1", "1/1"], ["0/1", "1/1"], ["0/1", "1/1"]], "0", - ZERO, True), + [["1/2", "1/2"], ["1/2", "1/2"], ["1/2", "1/2"], ["1/2", "1/2"], ["1/2", "1/2"]], + 0, ZERO, True), + (games.create_el_farol_bar_game_efg(), [[1, 0], [1, 0], [0, 1], [0, 1], [0, 1]], + 0, ZERO, True), ############################################################################## - # 2x2x2 nfg with 2 pure and 1 mixed eq + # # 2x2x2 nfg with 2 pure and 1 mixed eq + # Pure non-Nash eq: (games.create_2x2x2_nfg(), [[1, 0], [1, 0], [1, 0]], 18, ZERO, True), # 4^2+1+1 (games.create_2x2x2_nfg(), [[0, 1], [0, 1], [0, 1]], 18, ZERO, True), # 4^2+1+1 + (games.create_2x2x2_nfg(), [[1, 0], [0, 1], [0, 1]], 9, ZERO, True), # 3^2 + (games.create_2x2x2_nfg(), [[0, 1], [1, 0], [1, 0]], 9, ZERO, True), # 3^2 + (games.create_2x2x2_nfg(), [[1, 0], [0, 1], [0, 1]], 9, ZERO, True), # 3^2 + (games.create_2x2x2_nfg(), [[1, 1], [1, 0], [0, 0]], 9, ZERO, True), # 3^2 + # Non-pure non-Nash eq: + (games.create_2x2x2_nfg(), [["1/2", "1/2"], [1, 0], [1, 0]], "33/4", ZERO, True), + (games.create_2x2x2_nfg(), [[1, 0], ["1/2", "1/2"], [1, 0]], 4, ZERO, True), + (games.create_2x2x2_nfg(), [[1, 0], [1, 0], ["1/2", "1/2"]], "33/4", ZERO, True), + # Nash eq: (games.create_2x2x2_nfg(), [[1, 0], [0, 1], [1, 0]], 0, ZERO, True), (games.create_2x2x2_nfg(), [[0, 1], [1, 0], [0, 1]], 0, ZERO, True), (games.create_2x2x2_nfg(), None, 0, ZERO, True), # uniform is Nash ] ) -def test_liapunov_value_reference(game: gbt.Game, profile_data: list, - liap_expected: float | str, - tol: float | gbt.Rational | int, - rational_flag: bool): - liap_expected = gbt.Rational(liap_expected) if rational_flag else liap_expected +def test_liap_value_reference(game: gbt.Game, profile_data: list, liap_exp: float | str, + tol: float | gbt.Rational | int, rational_flag: bool): profile = game.mixed_strategy_profile(rational=rational_flag, data=profile_data) - assert abs(profile.liap_value() - liap_expected) <= tol + liap_exp = gbt.Rational(liap_exp) if rational_flag else liap_exp + assert abs(profile.liap_value() - liap_exp) <= tol + + +@pytest.mark.parametrize( + "game,profile_data,player_regrets_exp,tol,rational_flag", + [ + ############################################################################## + # Zero matrix nfg, all liap_values are zero + (games.create_2x2_zero_nfg(), [["3/4", "1/4"], ["2/5", "3/5"]], [0]*2, ZERO, True), + (games.create_2x2_zero_nfg(), [["1/2", "1/2"], ["1/2", "1/2"]], [0]*2, ZERO, True), + (games.create_2x2_zero_nfg(), [[1, 0], [1, 0]], [0]*2, ZERO, True), + (games.create_2x2_zero_nfg(), [[1/4, 3/4], [2/5, 3/5]], [0]*2, TOL, False), + ############################################################################## + # 4x4 coordination nfg + (games.create_coord_4x4_nfg(), None, [0]*2, ZERO, True), + (games.create_coord_4x4_nfg(), None, [0]*2, TOL, False), + (games.create_coord_4x4_nfg(), [[1, 0, 0, 0], [1, 0, 0, 0]], [0]*2, ZERO, True), + (games.create_coord_4x4_nfg(), [[1, 0, 0, 0], [1, 0, 0, 0]], [0]*2, TOL, False), + (games.create_coord_4x4_nfg(), + [["1/3", "1/2", "1/12", "1/12"], ["3/8", "1/8", "1/4", "1/4"]], + ["7/48", "13/48"], ZERO, True), + (games.create_coord_4x4_nfg(), [[1/3, 1/2, 1/12, 1/12], [3/8, 1/8, 1/4, 1/4]], + [7/48, 13/48], TOL, False), + (games.create_coord_4x4_nfg(), + [["1/3", 0, 0, "2/3"], [1, 0, 0, 0]], ["2/3", "1/3"], ZERO, True), + (games.create_coord_4x4_nfg(), + [[1/3, 0, 0, 2/3], [1, 0, 0, 0]], [2/3, 1/3], TOL, False), + ############################################################################## + # El Farol bar game efg + (games.create_el_farol_bar_game_efg(), + [["1/2", "1/2"], ["1/2", "1/2"], ["1/2", "1/2"], ["1/2", "1/2"], ["1/2", "1/2"]], + [0]*5, ZERO, True), + (games.create_el_farol_bar_game_efg(), [[1, 0], [1, 0], [0, 1], [0, 1], [0, 1]], + [0]*5, ZERO, True), + ############################################################################## + # 2x2x2 nfg with 2 pure and 1 mixed eq + # Pure non-Nash + (games.create_2x2x2_nfg(), [[1, 0], [1, 0], [1, 0]], [1, 4, 1], ZERO, True), # 111 + (games.create_2x2x2_nfg(), [[0, 1], [0, 1], [0, 1]], [1, 4, 1], ZERO, True), # 000 + (games.create_2x2x2_nfg(), [[1, 0], [0, 1], [0, 1]], [0, 0, 3], ZERO, True), # 100 + (games.create_2x2x2_nfg(), [[0, 1], [1, 0], [1, 0]], [0, 0, 3], ZERO, True), # 011 + (games.create_2x2x2_nfg(), [[0, 1], [0, 1], [1, 0]], [3, 0, 0], ZERO, True), # 001 + (games.create_2x2x2_nfg(), [[1, 0], [1, 0], [0, 1]], [3, 0, 0], ZERO, True), # 110 + # Mixed non-Nash + (games.create_2x2x2_nfg(), [["1/2", "1/2"], [1, 0], [1, 0]], ["1/2", 2, 2], ZERO, True), + (games.create_2x2x2_nfg(), [[1, 0], ["1/2", "1/2"], [1, 0]], [0, 2, 0], ZERO, True), + (games.create_2x2x2_nfg(), [[1, 0], [1, 0], ["1/2", "1/2"]], [2, 2, "1/2"], ZERO, True), + # Nash eq: + (games.create_2x2x2_nfg(), [[1, 0], [0, 1], [1, 0]], [0]*3, ZERO, True), # 101 + (games.create_2x2x2_nfg(), [[0, 1], [1, 0], [0, 1]], [0]*3, ZERO, True), # 010 + (games.create_2x2x2_nfg(), None, [0]*3, ZERO, True), # uniform is Nash + ] +) +def test_player_regret_max_regret_reference(game: gbt.Game, profile_data: list, + player_regrets_exp: list, + tol: float | gbt.Rational | int, + rational_flag: bool): + profile = game.mixed_strategy_profile(rational=rational_flag, data=profile_data) + if rational_flag: + player_regrets_exp = [gbt.Rational(r) for r in player_regrets_exp] + for p, r in zip(game.players, player_regrets_exp, strict=True): + assert abs(profile.player_regret(p) - r) <= tol + assert abs(profile.max_regret() - max(player_regrets_exp)) <= tol @pytest.mark.parametrize( @@ -596,11 +669,11 @@ def test_strategy_regret_consistency(game: gbt.Game, rational_flag: bool): ################################################################################# # Centipede with chance efg (games.create_centipede_game_with_chance_efg(), - [["1/3", "1/3", "1/3", "0/1"], ["1/10", "3/5", "3/10", "0/1"]], ZERO, True), + [["1/3", "1/3", "1/3", "0/1"], ["1/10", "3/5", "3/10", 0]], ZERO, True), (games.create_centipede_game_with_chance_efg(), [[1/3, 1/3, 1/3, 0], [.10, 3/5, .3, 0]], TOL, False), ################################################################################# - # El Faor bar game efg + # El Farol bar game efg (games.create_el_farol_bar_game_efg(), [[1, 0], ["1/2", "1/2"], ["1/3", "2/3"], ["1/5", "4/5"], ["1/8", "7/8"]], ZERO, True), (games.create_el_farol_bar_game_efg(), @@ -613,9 +686,9 @@ def test_strategy_regret_consistency(game: gbt.Game, rational_flag: bool): (games.create_2x2x2_nfg(), [[1, 0], [1, 0], [1, 0]], TOL, False), ] ) -def test_liapunov_value_consistency(game: gbt.Game, profile_data: list, - tol: float | gbt.Rational, - rational_flag: bool): +def test_liap_value_consistency(game: gbt.Game, profile_data: list, + tol: float | gbt.Rational, + rational_flag: bool): profile = game.mixed_strategy_profile(rational=rational_flag, data=profile_data) assert ( @@ -625,6 +698,48 @@ def test_liapunov_value_consistency(game: gbt.Game, profile_data: list, ) +@pytest.mark.parametrize( + "game,profile_data,tol,rational_flag", + [ + ################################################################################# + # 4x4 coordination nfg + (games.create_coord_4x4_nfg(), + [["1/5", "2/5", "0/5", "2/5"], ["3/8", "1/4", "3/8", "0/4"]], ZERO, True), + (games.create_coord_4x4_nfg(), + [[1/3, 1/3, 0/3, 1/3], [1/4, 1/4, 3/8, 1/8]], TOL, False), + ################################################################################# + # Centipede with chance efg + (games.create_centipede_game_with_chance_efg(), + [["1/3", "1/3", "1/3", "0/1"], ["1/10", "3/5", "3/10", 0]], ZERO, True), + (games.create_centipede_game_with_chance_efg(), + [[1/3, 1/3, 1/3, 0], [.10, 3/5, .3, 0]], TOL, False), + ################################################################################# + # El Farol bar game efg + (games.create_el_farol_bar_game_efg(), + [[1, 0], ["1/2", "1/2"], ["1/3", "2/3"], ["1/5", "4/5"], ["1/8", "7/8"]], ZERO, True), + (games.create_el_farol_bar_game_efg(), + [[1, 0], [1/2, 1/2], [1/3, 2/3], [1/5, 4/5], [1/8, 7/8]], TOL, False), + ################################################################################# + # 2x2x2 nfg with 2 pure and 1 mixed eq + (games.create_2x2x2_nfg(), None, ZERO, True), + (games.create_2x2x2_nfg(), [[1, 0], [1, 0], [1, 0]], ZERO, True), + (games.create_2x2x2_nfg(), None, TOL, False), + (games.create_2x2x2_nfg(), [[1, 0], [1, 0], [1, 0]], TOL, False), + ] +) +def test_player_regret_max_regret_consistency(game: gbt.Game, profile_data: list, + tol: float | gbt.Rational, + rational_flag: bool): + profile = game.mixed_strategy_profile(rational=rational_flag, data=profile_data) + player_regrets = [] + for p in game.players: + p_regret = max([max(profile.strategy_value(strategy) - profile.payoff(p), 0) + for strategy in p.strategies]) + player_regrets.append(p_regret) + assert abs(profile.player_regret(p) - p_regret) <= tol + assert abs(profile.max_regret() - max(player_regrets)) <= tol + + @pytest.mark.parametrize( "game,profile1,profile2,alpha,tol,rational_flag", [ From 61f4895fc247d88039229569e240c35506ef4505 Mon Sep 17 00:00:00 2001 From: Rahul Savani Date: Tue, 16 Dec 2025 19:00:31 +0000 Subject: [PATCH 2/2] max_regret added to test_profile_order_consistency --- tests/test_mixed.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/test_mixed.py b/tests/test_mixed.py index f94ff1d28..7ef840fcd 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -1048,6 +1048,29 @@ def _get_and_check_answers(game: gbt.Game, action_probs1: tuple, action_probs2: pytest.param(games.create_stripped_down_poker_efg(), PROBS_1B_rat, PROBS_2B_rat, True, lambda profile, y: profile.liap_value(), lambda x: [1], id="liap_value_poker_rat"), + ################################################################################# + # max_regret (of profile, hence [1] for objects_to_test, any singleton collection would do) + # 4x4 coordination nfg + pytest.param(games.create_coord_4x4_nfg(), PROBS_1A_doub, PROBS_2A_doub, False, + lambda profile, y: profile.max_regret(), lambda x: [1], + id="max_regret_coord_doub"), + pytest.param(games.create_coord_4x4_nfg(), PROBS_1A_rat, PROBS_2A_rat, True, + lambda profile, y: profile.max_regret(), lambda x: [1], + id="max_regret_coord_rat"), + # 2x2x2 nfg + pytest.param(games.create_2x2x2_nfg(), PROBS_1B_doub, PROBS_2B_doub, False, + lambda profile, y: profile.max_regret(), lambda x: [1], + id="max_regret_2x2x2_doub"), + pytest.param(games.create_2x2x2_nfg(), PROBS_1B_rat, PROBS_2B_rat, True, + lambda profile, y: profile.max_regret(), lambda x: [1], + id="max_regret_2x2x2_rat"), + # stripped-down poker + pytest.param(games.create_stripped_down_poker_efg(), PROBS_1B_doub, PROBS_2B_doub, False, + lambda profile, y: profile.max_regret(), lambda x: [1], + id="max_regret_poker_doub"), + pytest.param(games.create_stripped_down_poker_efg(), PROBS_1B_rat, PROBS_2B_rat, True, + lambda profile, y: profile.max_regret(), lambda x: [1], + id="max_regret_poker_rat"), ] ) def test_profile_order_consistency(game: gbt.Game,