Skip to content

Commit a509e01

Browse files
committed
tests for EFG Nash solvers -- enumpoly, lp, lcp -- in behavior stratgegies
1 parent a311874 commit a509e01

File tree

6 files changed

+515
-53
lines changed

6 files changed

+515
-53
lines changed

ChangeLog

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
- In the graphical interface, removed option to configure information set link drawing; information sets
77
are always drawn and indicators are always drawn if an information set spans multiple levels.
88

9+
### Added
10+
- Tests for EFG Nash solvers -- `enumpoly_solve`, `lp_solve`, `lcp_solve` -- in behavior stratgegies
11+
912

1013
## [16.4.1] - unreleased
1114

pyproject.toml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ select = [
5454
"SIM", # flake8-simplify
5555
"I", # isort
5656
"Q", # prefer double quotes
57+
"W291", # trailing-whitespace
5758
]
5859
ignore = []
5960

@@ -75,11 +76,12 @@ max-line-length = 99
7576
[tool.pytest.ini_options]
7677
addopts = "--strict-markers"
7778
markers = [
78-
"nash_enummixed_strategy: tests of enummixed_solve in strategies",
79-
"nash_lcp_strategy: tests of lcp_solve in strategies",
80-
"nash_lcp_behavior: tests of lcp_solve in behaviors",
81-
"nash_lp_strategy: tests of lp_solve in strategies",
82-
"nash_lp_behavior: tests of lp_solve in behaviors",
79+
"nash_enummixed_strategy: tests of enummixed_solve in mixed strategies",
80+
"nash_enumpoly_behavior: tests of enumpoly_solve in behavior strategies",
81+
"nash_lcp_strategy: tests of lcp_solve in mixed strategies",
82+
"nash_lcp_behavior: tests of lcp_solve in behavior strategies",
83+
"nash_lp_strategy: tests of lp_solve in mixed strategies",
84+
"nash_lp_behavior: tests of lp_solve in behavior strategies",
8385
"nash: all tests of Nash equilibrium solvers",
8486
"slow: all time-consuming tests",
8587
]

tests/games.py

Lines changed: 292 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,32 @@ def read_from_file(fn: str) -> gbt.Game:
1818
raise ValueError(f"Unknown file extension in {fn}")
1919

2020

21+
def create_efg_corresponding_to_bimatrix_game(
22+
A: np.ndarray, B: np.ndarray, title: str
23+
) -> gbt.Game:
24+
"""
25+
There is not direct pygambit method to create an EFG from a stategic-form game.
26+
Here we create an EFG corresponding to a bimatrix game, which is given by two numpy arrays.
27+
Player 1 moves first.
28+
"""
29+
assert A.shape == B.shape
30+
m, n = A.shape
31+
g = gbt.Game.new_tree(players=["1", "2"], title=title)
32+
actions1 = [str(i) for i in range(m)]
33+
actions2 = [str(i) for i in range(n)]
34+
g.append_move(g.root, "1", actions1)
35+
for node in g.root.children:
36+
g.append_move(node, "2", actions2)
37+
iset = g.root.children[0].infoset
38+
for c in g.root.children:
39+
g.set_infoset(c, iset)
40+
from itertools import product
41+
42+
for i, j in product(range(m), range(n)):
43+
g.set_outcome(g.root.children[i].children[j], g.add_outcome([A[i, j], B[i, j]]))
44+
return g
45+
46+
2147
################################################################################################
2248
# Normal-form (aka strategic-form) games (nfg)
2349

@@ -87,10 +113,22 @@ def create_mixed_behav_game_efg() -> gbt.Game:
87113
Game
88114
Three-player extensive form game: binary tree with 3 infomation sets, one per player,
89115
with 1, 2, and 4 nodes respectively
116+
117+
Since no information is revealed this is directly equivalent to a simultaneous move game
90118
"""
91119
return read_from_file("mixed_behavior_game.efg")
92120

93121

122+
def create_1_card_poker_efg() -> gbt.Game:
123+
"""
124+
Returns
125+
-------
126+
Game
127+
One-card two-player poker game, as used in the user guide
128+
"""
129+
return read_from_file("poker.efg")
130+
131+
94132
def create_myerson_2_card_poker_efg() -> gbt.Game:
95133
"""
96134
Returns
@@ -104,6 +142,128 @@ def create_myerson_2_card_poker_efg() -> gbt.Game:
104142
return read_from_file("myerson_2_card_poker.efg")
105143

106144

145+
def create_kuhn_poker_efg() -> gbt.Game:
146+
"""
147+
Returns
148+
-------
149+
Game
150+
Kuhn poker with 3 cards and 2 players
151+
"""
152+
g = gbt.Game.new_tree(
153+
players=["Alice", "Bob"], title="Three-card poker (J, Q, K), two-player"
154+
)
155+
g.append_move(g.root, g.players.chance, ["JQ", "JK", "QJ", "QK", "KJ", "KQ"])
156+
g.set_chance_probs(g.root.infoset, [gbt.Rational(1, 6)]*6)
157+
# For each chance outcome node, append Alice's first decision: Check or Bet.
158+
for i in range(6):
159+
g.append_move(g.root.children[i], "Alice", ["Check", "Bet"])
160+
# After Alice checks, Bob moves: Check or Bet.
161+
for i in range(6):
162+
g.append_move(g.root.children[i].children[0], "Bob", ["Check", "Bet"])
163+
# If Bob bets after Alice checked (Bob's second action), then Alice can Fold or Call.
164+
# Append Alice's response nodes for the check-then-bet branch
165+
for i in range(6):
166+
g.append_move(g.root.children[i].children[0].children[1], "Alice", ["Fold", "Call"])
167+
# If Alice bets initially, Bob can Fold or Call.
168+
for i in range(6):
169+
g.append_move(g.root.children[i].children[1], "Bob", ["Fold", "Call"])
170+
# Set up information sets to represent imperfect information.
171+
# Alice dealt J
172+
g.set_infoset(g.root.children[0], g.root.children[1].infoset)
173+
# Alice dealt Q
174+
g.set_infoset(g.root.children[2], g.root.children[3].infoset)
175+
# Alice dealt K
176+
g.set_infoset(g.root.children[4], g.root.children[5].infoset)
177+
# Bob's decision after Alice checks: Bob knows his own card but not Alice's
178+
# Bob dealt Q
179+
g.set_infoset(g.root.children[0].children[0], g.root.children[5].children[0].infoset)
180+
# Bob dealt K
181+
g.set_infoset(g.root.children[1].children[0], g.root.children[3].children[0].infoset)
182+
# Bob dealt J
183+
g.set_infoset(g.root.children[2].children[0], g.root.children[4].children[0].infoset)
184+
# Bob's decision after Alice bets:
185+
# Bob dealt Q
186+
g.set_infoset(g.root.children[0].children[1], g.root.children[5].children[1].infoset)
187+
# Bob dealt K
188+
g.set_infoset(g.root.children[1].children[1], g.root.children[3].children[1].infoset)
189+
# Bob dealt J
190+
g.set_infoset(g.root.children[2].children[1], g.root.children[4].children[1].infoset)
191+
# Alice's decision after she checked and Bob then bet:
192+
# Alice dealt J
193+
g.set_infoset(g.root.children[0].children[0].children[1],
194+
g.root.children[1].children[0].children[1].infoset)
195+
# Alice dealt Q
196+
g.set_infoset(g.root.children[2].children[0].children[1],
197+
g.root.children[3].children[0].children[1].infoset)
198+
# Alice dealt K
199+
g.set_infoset(g.root.children[4].children[0].children[1],
200+
g.root.children[5].children[0].children[1].infoset)
201+
# Add outcomes at terminal nodes (net payoffs); first define four outcomes:
202+
alice_wins1 = g.add_outcome([1, -1], label="Alice wins 1")
203+
alice_wins2 = g.add_outcome([2, -2], label="Alice wins 2")
204+
bob_wins1 = g.add_outcome([-1, 1], label="Bob wins 1")
205+
bob_wins2 = g.add_outcome([-2, 2], label="Bob wins 2")
206+
# Check-Check -> Bob wins 1
207+
g.set_outcome(g.root.children[0].children[0].children[0], bob_wins1)
208+
# Check-Bet-Fold -> Bob wins 1
209+
g.set_outcome(g.root.children[0].children[0].children[1].children[0], bob_wins1)
210+
# Check-Bet-Call -> showdown -> Bob wins 2
211+
g.set_outcome(g.root.children[0].children[0].children[1].children[1], bob_wins2)
212+
# Bet-Fold -> Alice bet, Bob folds -> Alice wins pot -> Alice wins 1
213+
g.set_outcome(g.root.children[0].children[1].children[0], alice_wins1)
214+
# Bet-Call -> showdown -> Bob wins 2
215+
g.set_outcome(g.root.children[0].children[1].children[1], bob_wins2)
216+
# JK -> Bob wins showdown
217+
g.set_outcome(g.root.children[1].children[0].children[0], bob_wins1)
218+
g.set_outcome(g.root.children[1].children[0].children[1].children[0], bob_wins1)
219+
g.set_outcome(g.root.children[1].children[0].children[1].children[1], bob_wins2)
220+
g.set_outcome(g.root.children[1].children[1].children[0], alice_wins1)
221+
g.set_outcome(g.root.children[1].children[1].children[1], bob_wins2)
222+
# QJ -> Alice wins showdown
223+
g.set_outcome(g.root.children[2].children[0].children[0], alice_wins1)
224+
g.set_outcome(g.root.children[2].children[0].children[1].children[0], bob_wins1)
225+
g.set_outcome(g.root.children[2].children[0].children[1].children[1], alice_wins2)
226+
g.set_outcome(g.root.children[2].children[1].children[0], alice_wins1)
227+
g.set_outcome(g.root.children[2].children[1].children[1], alice_wins2)
228+
# QK -> Bob wins showdown
229+
g.set_outcome(g.root.children[3].children[0].children[0], bob_wins1)
230+
g.set_outcome(g.root.children[3].children[0].children[1].children[0], bob_wins1)
231+
g.set_outcome(g.root.children[3].children[0].children[1].children[1], bob_wins2)
232+
g.set_outcome(g.root.children[3].children[1].children[0], alice_wins1)
233+
g.set_outcome(g.root.children[3].children[1].children[1], bob_wins2)
234+
# KJ -> Alice wins showdown
235+
g.set_outcome(g.root.children[4].children[0].children[0], alice_wins1)
236+
g.set_outcome(g.root.children[4].children[0].children[1].children[0], bob_wins1)
237+
g.set_outcome(g.root.children[4].children[0].children[1].children[1], alice_wins2)
238+
g.set_outcome(g.root.children[4].children[1].children[0], alice_wins1)
239+
g.set_outcome(g.root.children[4].children[1].children[1], alice_wins2)
240+
# KQ -> Alice wins showdown
241+
g.set_outcome(g.root.children[5].children[0].children[0], alice_wins1)
242+
g.set_outcome(g.root.children[5].children[0].children[1].children[0], bob_wins1)
243+
g.set_outcome(g.root.children[5].children[0].children[1].children[1], alice_wins2)
244+
g.set_outcome(g.root.children[5].children[1].children[0], alice_wins1)
245+
g.set_outcome(g.root.children[5].children[1].children[1], alice_wins2)
246+
# Ensure infosets are in the same order as if game was written to efg and read back in
247+
g.sort_infosets()
248+
return g
249+
250+
251+
def create_one_shot_trust_efg() -> gbt.Game:
252+
g = gbt.Game.new_tree(
253+
players=["Buyer", "Seller"], title="One-shot trust game, after Kreps (1990)"
254+
)
255+
g.append_move(g.root, "Buyer", ["Trust", "Not trust"])
256+
g.append_move(g.root.children[0], "Seller", ["Honor", "Abuse"])
257+
g.set_outcome(
258+
g.root.children[0].children[0], g.add_outcome([1, 1], label="Trustworthy")
259+
)
260+
g.set_outcome(
261+
g.root.children[0].children[1], g.add_outcome([-1, 2], label="Untrustworthy")
262+
)
263+
g.set_outcome(g.root.children[1], g.add_outcome([0, 0], label="Opt-out"))
264+
return g
265+
266+
107267
def create_centipede_game_with_chance_efg() -> gbt.Game:
108268
"""
109269
Returns
@@ -198,7 +358,6 @@ def create_reduction_generic_payoffs_efg() -> gbt.Game:
198358
)
199359

200360
g.set_outcome(g.root.children[3], g.add_outcome([12, -12], label="d"))
201-
202361
return g
203362

204363

@@ -236,6 +395,138 @@ def create_reduction_both_players_payoff_ties_efg() -> gbt.Game:
236395
return g
237396

238397

398+
def create_seq_form_STOC_paper_zero_sum_2_player_efg() -> gbt.Game:
399+
"""
400+
Example from
401+
402+
Fast Algorithms for Finding Randomized Strategies in Game Trees (1994)
403+
Koller, Megiddo, von Stengel
404+
"""
405+
g = gbt.Game.new_tree(players=["1", "2"], title="From STOC'94 paper")
406+
g.append_move(g.root, g.players.chance, actions=["1", "2", "3", "4"])
407+
g.set_chance_probs(g.root.infoset, [0.2, 0.2, 0.2, 0.4])
408+
g.append_move(g.root.children[0], player="1", actions=["l", "r"])
409+
g.append_move(g.root.children[1], player="1", actions=["c", "d"])
410+
g.append_infoset(g.root.children[2], g.root.children[1].infoset)
411+
g.append_move(g.root.children[0].children[1], player="2", actions=["p", "q"])
412+
g.append_move(
413+
g.root.children[0].children[1].children[0], player="1", actions=["L", "R"]
414+
)
415+
g.append_infoset(
416+
g.root.children[0].children[1].children[1],
417+
g.root.children[0].children[1].children[0].infoset,
418+
)
419+
g.append_move(g.root.children[2].children[0], player="2", actions=["s", "t"])
420+
g.append_infoset(
421+
g.root.children[2].children[1], g.root.children[2].children[0].infoset
422+
)
423+
424+
g.set_outcome(
425+
g.root.children[0].children[0],
426+
outcome=g.add_outcome(payoffs=[5, -5], label="l"),
427+
)
428+
g.set_outcome(
429+
g.root.children[0].children[1].children[0].children[0],
430+
outcome=g.add_outcome(payoffs=[10, -10], label="rpL"),
431+
)
432+
g.set_outcome(
433+
g.root.children[0].children[1].children[0].children[1],
434+
outcome=g.add_outcome(payoffs=[15, -15], label="rpR"),
435+
)
436+
g.set_outcome(
437+
g.root.children[0].children[1].children[1].children[0],
438+
outcome=g.add_outcome(payoffs=[20, -20], label="rqL"),
439+
)
440+
g.set_outcome(
441+
g.root.children[0].children[1].children[1].children[1],
442+
outcome=g.add_outcome(payoffs=[-5, 5], label="rqR"),
443+
)
444+
g.set_outcome(
445+
g.root.children[1].children[0],
446+
outcome=g.add_outcome(payoffs=[10, -10], label="c"),
447+
)
448+
g.set_outcome(
449+
g.root.children[1].children[1],
450+
outcome=g.add_outcome(payoffs=[20, -20], label="d"),
451+
)
452+
g.set_outcome(
453+
g.root.children[2].children[0].children[0],
454+
outcome=g.add_outcome(payoffs=[20, -20], label="cs"),
455+
)
456+
g.set_outcome(
457+
g.root.children[2].children[0].children[1],
458+
outcome=g.add_outcome(payoffs=[50, -50], label="ct"),
459+
)
460+
g.set_outcome(
461+
g.root.children[2].children[1].children[0],
462+
outcome=g.add_outcome(payoffs=[30, -30], label="ds"),
463+
)
464+
g.set_outcome(
465+
g.root.children[2].children[1].children[1],
466+
outcome=g.add_outcome(payoffs=[15, -15], label="dt"),
467+
)
468+
g.set_outcome(
469+
g.root.children[3], outcome=g.add_outcome(payoffs=[5, -5], label="nothing")
470+
)
471+
g.root.children[0].infoset.label = "0"
472+
g.root.children[1].infoset.label = "1"
473+
g.root.children[0].children[1].infoset.label = "01"
474+
g.root.children[2].children[0].infoset.label = "20"
475+
g.root.children[0].children[1].children[0].infoset.label = "010"
476+
477+
return g
478+
479+
480+
def create_two_player_perfect_info_win_lose_efg() -> gbt.Game:
481+
g = gbt.Game.new_tree(players=["1", "2"], title="2 player perfect info win lose")
482+
g.append_move(g.root, "2", ["a", "b"])
483+
g.append_move(g.root.children[0], "1", ["L", "R"])
484+
g.append_move(g.root.children[1], "1", ["L", "R"])
485+
g.append_move(g.root.children[0].children[0], "2", ["l", "r"])
486+
g.set_outcome(
487+
g.root.children[0].children[0].children[0], g.add_outcome([1, -1], label="aLl")
488+
)
489+
g.set_outcome(
490+
g.root.children[0].children[0].children[1], g.add_outcome([-1, 1], label="aLr")
491+
)
492+
g.set_outcome(g.root.children[0].children[1], g.add_outcome([1, -1], label="aR"))
493+
g.set_outcome(g.root.children[1].children[0], g.add_outcome([1, -1], label="bL"))
494+
g.set_outcome(g.root.children[1].children[1], g.add_outcome([-1, 1], label="bR"))
495+
return g
496+
497+
498+
def create_EFG_for_nxn_bimatrix_coordination_game(n: int) -> gbt.Game:
499+
A = np.eye(n, dtype=int)
500+
B = A
501+
title = f"{n}x{n} coordination game, {2**n - 1} equilibria"
502+
return create_efg_corresponding_to_bimatrix_game(A, B, title)
503+
504+
505+
def create_EFG_for_6x6_bimatrix_with_long_LH_paths_and_unique_eq() -> gbt.Game:
506+
# 6 x 6 Payoff matrix A:
507+
A = [
508+
[-180, 72, -333, 297, -153, 270],
509+
[-30, 17, -33, 42, -3, 20],
510+
[-81, 36, -126, 126, -36, 90],
511+
[90, -36, 126, -126, 36, -81],
512+
[20, -3, 42, -33, 17, -30],
513+
[270, -153, 297, -333, 72, -180],
514+
]
515+
# 6 x 6 Payoff matrix B:
516+
B = [
517+
[72, 36, 17, -3, -36, -153],
518+
[-180, -81, -30, 20, 90, 270],
519+
[297, 126, 42, -33, -126, -333],
520+
[-333, -126, -33, 42, 126, 297],
521+
[270, 90, 20, -30, -81, -180],
522+
[-153, -36, -3, 17, 36, 72],
523+
]
524+
A = np.array(A)
525+
B = np.array(B)
526+
title = "6x6 Long Lemke-Howson Paths, unique eq"
527+
return create_efg_corresponding_to_bimatrix_game(A, B, title)
528+
529+
239530
class EfgFamilyForReducedStrategicFormTests(ABC):
240531
""" """
241532

tests/test_behav.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ def test_profile_indexing_by_infoset_and_action_labels_reference(game: gbt.Game,
229229
action_label: str,
230230
prob: typing.Union[str, float],
231231
rational_flag: bool):
232-
"""Here we use the infoset label and action label, with some exampels where the action label
232+
"""Here we use the infoset label and action label, with some examples where the action label
233233
alone throws a ValueError (checked in a separate test)
234234
"""
235235
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
259259
action_label: str,
260260
prob: typing.Union[str, float],
261261
rational_flag: bool):
262-
"""Here we use the infoset label and action label, with some exampels where the action label
262+
"""Here we use the infoset label and action label, with some examples where the action label
263263
alone throws a ValueError (checked in a separate test)
264264
"""
265265
profile = game.mixed_behavior_profile(rational=rational_flag)

tests/test_extensive.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ def test_outcome_index_exception_label():
377377
],
378378
)
379379
def test_reduced_strategic_form(
380-
game: gbt.Game, strategy_labels: list, np_arrays_of_rsf: list
380+
game: gbt.Game, strategy_labels: list, np_arrays_of_rsf: typing.Union[list, None]
381381
):
382382
"""
383383
We test two things:

0 commit comments

Comments
 (0)