Skip to content

Commit ae68c65

Browse files
authored
Add tests for EFG Nash solvers enumpoly, lp, lcp in behavior stratgegies (#586)
1 parent 4cd1697 commit ae68c65

6 files changed

Lines changed: 491 additions & 44 deletions

File tree

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: 281 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""A utility module to create/load games for the test suite."""
22

3+
import itertools
34
import pathlib
45
from abc import ABC, abstractmethod
5-
from itertools import product
66

77
import numpy as np
88

@@ -18,6 +18,26 @@ 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 no direct pygambit method to create an EFG from a stategic-form game.
26+
Here we create an EFG corresponding to a bimatrix game, 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+
g.append_move(g.root.children, "2", actions2)
36+
for i, j in itertools.product(range(m), range(n)):
37+
g.set_outcome(g.root.children[i].children[j], g.add_outcome([A[i, j], B[i, j]]))
38+
return g
39+
40+
2141
################################################################################################
2242
# Normal-form (aka strategic-form) games (nfg)
2343

@@ -87,10 +107,22 @@ def create_mixed_behav_game_efg() -> gbt.Game:
87107
Game
88108
Three-player extensive form game: binary tree with 3 infomation sets, one per player,
89109
with 1, 2, and 4 nodes respectively
110+
111+
Since no information is revealed this is directly equivalent to a simultaneous move game
90112
"""
91113
return read_from_file("mixed_behavior_game.efg")
92114

93115

116+
def create_1_card_poker_efg() -> gbt.Game:
117+
"""
118+
Returns
119+
-------
120+
Game
121+
One-card two-player poker game, as used in the user guide
122+
"""
123+
return read_from_file("poker.efg")
124+
125+
94126
def create_myerson_2_card_poker_efg() -> gbt.Game:
95127
"""
96128
Returns
@@ -104,6 +136,119 @@ def create_myerson_2_card_poker_efg() -> gbt.Game:
104136
return read_from_file("myerson_2_card_poker.efg")
105137

106138

139+
def create_kuhn_poker_efg() -> gbt.Game:
140+
"""
141+
Returns
142+
-------
143+
Game
144+
Kuhn poker with 3 cards and 2 players
145+
"""
146+
g = gbt.Game.new_tree(
147+
players=["Alice", "Bob"], title="Three-card poker (J, Q, K), two-player"
148+
)
149+
deals = ["JQ", "JK", "QJ", "QK", "KJ", "KQ"]
150+
g.append_move(g.root, g.players.chance, deals)
151+
g.set_chance_probs(g.root.infoset, [gbt.Rational(1, 6)]*6)
152+
# group the children of the root (indices of `deals`) by each player's dealt card
153+
alice_grouping = [[0, 1], [2, 3], [4, 5]] # J, Q, K
154+
bob_grouping = [[0, 5], [1, 3], [2, 4]] # Q, K, J
155+
156+
# Alice's first move
157+
for ij in alice_grouping:
158+
term_nodes = [g.root.children[k] for k in ij]
159+
g.append_move(term_nodes, "Alice", ["Check", "Bet"])
160+
# Bob's move after Alice checks
161+
for ij in bob_grouping:
162+
term_nodes = [g.root.children[k].children[0] for k in ij]
163+
g.append_move(term_nodes, "Bob", ["Check", "Bet"])
164+
# Alice's move if Bob's second action is bet
165+
for ij in alice_grouping:
166+
term_nodes = [g.root.children[k].children[0].children[1] for k in ij]
167+
g.append_move(term_nodes, "Alice", ["Fold", "Call"])
168+
# Bob's move after Alice bets initially
169+
for ij in bob_grouping:
170+
term_nodes = [g.root.children[k].children[1] for k in ij]
171+
g.append_move(term_nodes, "Bob", ["Fold", "Call"])
172+
173+
def calculate_payoffs(term_node):
174+
175+
def get_path(node):
176+
path = []
177+
while node.parent:
178+
path.append(node.prior_action.label)
179+
node = node.parent
180+
return path
181+
182+
def showdown_winner(deal):
183+
# deal is an element of deals = ["JQ", "JK", "QJ", "QK", "KJ", "KQ"]
184+
card_values = dict(J=0, Q=1, K=2)
185+
a, b = deal
186+
return "Alice" if card_values[a] > card_values[b] else "Bob"
187+
188+
def showdown(deal, payoffs, pot):
189+
payoffs[showdown_winner(deal)] += pot
190+
return payoffs
191+
192+
def bet(player, payoffs, pot):
193+
payoffs[player] += -1
194+
pot += 1
195+
return payoffs, pot
196+
197+
path = get_path(term_node)
198+
deal = path.pop() # needed if there is a showdown
199+
payoffs = dict(Alice=-1, Bob=-1) # ante of 1 for both players
200+
pot = 2
201+
if path.pop() == "Check": # Alice checks
202+
if path.pop() == "Check": # Bob checks
203+
payoffs = showdown(deal, payoffs, pot)
204+
else: # Bob bets
205+
payoffs, pot = bet("Bob", payoffs, pot)
206+
if path.pop() == "Fold": # Alice folds
207+
payoffs["Bob"] += pot
208+
else: # Alice calls
209+
payoffs, pot = bet("Alice", payoffs, pot)
210+
payoffs = showdown(deal, payoffs, pot)
211+
else: # Alice bets
212+
payoffs, pot = bet("Alice", payoffs, pot)
213+
if path.pop() == "Fold": # Bob
214+
payoffs["Alice"] += pot
215+
else: # Bob calls
216+
payoffs, pot = bet("Bob", payoffs, pot)
217+
payoffs = showdown(deal, payoffs, pot)
218+
219+
return tuple(payoffs.values())
220+
221+
# create 4 possible outcomes just once
222+
payoffs_to_outcomes = {(1, -1): g.add_outcome([1, -1], label="Alice wins 1"),
223+
(2, -2): g.add_outcome([2, -2], label="Alice wins 2"),
224+
(-1, 1): g.add_outcome([-1, 1], label="Bob wins 1"),
225+
(-2, 2): g.add_outcome([-2, 2], label="Bob wins 2")}
226+
227+
for term_node in [n for n in g.nodes if n.is_terminal]:
228+
outcome = payoffs_to_outcomes[calculate_payoffs(term_node)]
229+
g.set_outcome(term_node, outcome)
230+
231+
# Ensure infosets are in the same order as if game was written to efg and read back in
232+
g.sort_infosets()
233+
return g
234+
235+
236+
def create_one_shot_trust_efg() -> gbt.Game:
237+
g = gbt.Game.new_tree(
238+
players=["Buyer", "Seller"], title="One-shot trust game, after Kreps (1990)"
239+
)
240+
g.append_move(g.root, "Buyer", ["Trust", "Not trust"])
241+
g.append_move(g.root.children[0], "Seller", ["Honor", "Abuse"])
242+
g.set_outcome(
243+
g.root.children[0].children[0], g.add_outcome([1, 1], label="Trustworthy")
244+
)
245+
g.set_outcome(
246+
g.root.children[0].children[1], g.add_outcome([-1, 2], label="Untrustworthy")
247+
)
248+
g.set_outcome(g.root.children[1], g.add_outcome([0, 0], label="Opt-out"))
249+
return g
250+
251+
107252
def create_centipede_game_with_chance_efg() -> gbt.Game:
108253
"""
109254
Returns
@@ -198,7 +343,6 @@ def create_reduction_generic_payoffs_efg() -> gbt.Game:
198343
)
199344

200345
g.set_outcome(g.root.children[3], g.add_outcome([12, -12], label="d"))
201-
202346
return g
203347

204348

@@ -236,6 +380,138 @@ def create_reduction_both_players_payoff_ties_efg() -> gbt.Game:
236380
return g
237381

238382

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

@@ -515,17 +791,17 @@ def _redu_strats(self, player, level):
515791
first_half = tmp[:n_half]
516792
second_half = tmp[n_half:]
517793
# create first half suffix
518-
first_half = product(first_half, first_half)
794+
first_half = itertools.product(first_half, first_half)
519795
first_half = ["".join(t) for t in first_half]
520796
first_half = ["1" + t for t in first_half] # add 1 to front
521797
# create second half suffix
522-
second_half = product(second_half, second_half)
798+
second_half = itertools.product(second_half, second_half)
523799
second_half = ["".join(t) for t in second_half]
524800
second_half = ["2" + t for t in second_half] # add 2 to front
525801
return first_half + second_half # glue halves together
526802
else: # player == 3:
527803
tmp = self._redu_strats(player=2, level=level - 1)
528-
tmp = product(tmp, tmp)
804+
tmp = itertools.product(tmp, tmp)
529805
tmp = ["".join(t) for t in tmp]
530806
return tmp
531807
else:

0 commit comments

Comments
 (0)