Skip to content

Commit aac1038

Browse files
committed
New tests for creation of reduced strategic form for centipede and 1/2/3-player binary games with an exponentially-large reduced strategic forms
1 parent 89cbddc commit aac1038

2 files changed

Lines changed: 339 additions & 0 deletions

File tree

tests/games.py

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""A utility module to create/load games for the test suite."""
22

33
import pathlib
4+
from abc import ABC, abstractmethod
5+
from itertools import product
46

57
import numpy as np
68

@@ -234,6 +236,312 @@ def create_reduction_both_players_payoff_ties_efg() -> gbt.Game:
234236
return g
235237

236238

239+
class EfgFamilyForReducedStrategicFormTests(ABC):
240+
""" """
241+
242+
@abstractmethod
243+
def __init__(params):
244+
pass
245+
246+
@abstractmethod
247+
def gbt_game(self):
248+
pass
249+
250+
@abstractmethod
251+
def reduced_strategies(self):
252+
pass
253+
254+
@abstractmethod
255+
def reduced_strategic_form(self):
256+
pass
257+
258+
def set_size_of_rsf(self, reduced_strategies):
259+
self.size_of_rsf = [len(r) for r in reduced_strategies]
260+
261+
@classmethod
262+
def get_test_data(cls, **params):
263+
"""
264+
given the provided parameters, return a tuple with:
265+
- the game as a gbt.Game object
266+
- the expected list of players reduced strategies in this game
267+
- the expected reduced strategic form (i.e. payoff tensors) for this game
268+
the tuple is used directly in test_reduced_strategic_form in test_extensive.py
269+
"""
270+
271+
game = cls(params)
272+
273+
return (
274+
game.gbt_game(),
275+
game.reduced_strategies(),
276+
game.reduced_strategic_form(),
277+
)
278+
279+
280+
class Centipede(EfgFamilyForReducedStrategicFormTests):
281+
"""
282+
Two-player Centipede game
283+
284+
Params: number of rounds (N); two payoff parameters (m0, m1)
285+
"""
286+
287+
def __init__(self, params):
288+
self.N = params["N"]
289+
self.m0 = params["m0"]
290+
self.m1 = params["m1"]
291+
292+
def gbt_game(self):
293+
g = gbt.Game.new_tree(
294+
players=["1", "2"], title=f"Centipede Game with {self.N} rounds"
295+
)
296+
current_node = g.root
297+
current_player = "1"
298+
for t in range(self.N):
299+
g.append_move(current_node, current_player, ["Take", "Push"])
300+
payoffs = [2**t * self.m0, 2**t * self.m1] # take payoffs
301+
if current_player == "2":
302+
payoffs.reverse()
303+
g.set_outcome(current_node.children[0], g.add_outcome(payoffs))
304+
if t == self.N - 1: # for last round, push payoffs
305+
payoffs = [2 ** (t + 1) * self.m1, 2 ** (t + 1) * self.m0]
306+
if current_player == "2":
307+
payoffs.reverse()
308+
g.set_outcome(current_node.children[1], g.add_outcome(payoffs))
309+
current_node = current_node.children[1]
310+
current_player = "2" if current_player == "1" else "1"
311+
return g
312+
313+
def reduced_strategies(self):
314+
315+
if self.N % 2 == 0:
316+
n_moves = [int(self.N / 2)] * 2
317+
else:
318+
n_moves = [int((self.N + 1) / 2), int((self.N - 1) / 2)]
319+
320+
def get_rss(n):
321+
# Given n number of times a player moves, their reduced strategies are such that
322+
# they Have n positions; have all *s after any 1; have prefixes 1, 21, 221, 2221, etc.
323+
# and finally the last strategyt is all 2s
324+
ret = ["2" * (i) + "1" * 1 + "*" * (n - i - 1) for i in range(n)]
325+
ret.append("2" * n)
326+
return ret
327+
328+
rs = [get_rss(n) for n in n_moves]
329+
self.set_size_of_rsf(rs)
330+
return rs
331+
332+
def reduced_strategic_form(self):
333+
m, n = self.size_of_rsf
334+
p1_payoffs = np.zeros((m, n), dtype=int)
335+
p2_payoffs = np.zeros((m, n), dtype=int)
336+
row1_1 = [self.m0] * n
337+
row1_2 = [self.m1] * n
338+
p1_payoffs[0, :] = row1_1
339+
p2_payoffs[0, :] = row1_2
340+
341+
for j in range(n - 1 if self.N % 2 == 0 else n):
342+
max_in_col_p1 = 2 ** (2 * j + 1) * self.m1
343+
max_in_col_p2 = 2 ** (2 * j + 1) * self.m0
344+
base1 = [max_in_col_p1] * (m - 1)
345+
base2 = [max_in_col_p2] * (m - 1)
346+
for i in range(1, (j + 1)):
347+
base1[i - 1] = 2 ** (2 * i) * self.m0
348+
base2[i - 1] = 2 ** (2 * i) * self.m1
349+
p1_payoffs[1:, j] = base1
350+
p2_payoffs[1:, j] = base2
351+
if self.N % 2 == 0:
352+
# final col
353+
p1_payoffs[:, n - 1] = p1_payoffs[:, n - 2]
354+
p2_payoffs[:, n - 1] = p2_payoffs[:, n - 2]
355+
p1_extra_pay = 2 ** (2 * (n - 1)) * self.m0
356+
p2_extra_pay = 2 ** (2 * (n - 1)) * self.m1
357+
else:
358+
# final row
359+
p1_payoffs[m - 1, :] = p1_payoffs[m - 2, :]
360+
p2_payoffs[m - 1, :] = p2_payoffs[m - 2, :]
361+
p1_extra_pay = 2 ** (2 * (n) - 1) * self.m1
362+
p2_extra_pay = 2 ** (2 * (n) - 1) * self.m0
363+
p1_payoffs[m - 1, n - 1] = p1_extra_pay
364+
p2_payoffs[m - 1, n - 1] = p2_extra_pay
365+
return p1_payoffs, p2_payoffs
366+
367+
368+
class BinaryTreeGames(EfgFamilyForReducedStrategicFormTests):
369+
"""
370+
Params:
371+
- single positive integer, namely the number of "level"s
372+
- number of players (currently the 1, 2, and 3-player versions are used in tests)
373+
374+
These games:
375+
- are all binary trees with imperfect information
376+
- after every L/R choice the subsequent, have the two subsequent nodes (unless terminal)
377+
together and alone in a single infoset (so all infosets except the root are pairs)
378+
- the purpose of these games is to test the reduced strategy lists
379+
- payoff matrices are all zero for simplicity
380+
381+
These games have rougly 2^root(level) many reduced strategies
382+
383+
The 2-player versions appear in:
384+
385+
B. von Stengel, A. van den Elzen, and A. J. J. Talman (2002)
386+
Computing normal form perfect equilibria for extensive two-person games
387+
Econometrica 70(2), 693-715
388+
389+
The 1-player versions have Imperfect Recall
390+
"""
391+
392+
def __init__(self, n_players, params):
393+
self.level = params["level"]
394+
self.players = list(range(1, n_players + 1))
395+
self.n_players = n_players
396+
397+
def get_n_infosets(self, level):
398+
399+
if self.n_players == 1:
400+
return {1: 2 ** (level - 1)}
401+
402+
players = list(range(1, self.n_players + 1))
403+
n_isets = [1] + [0] * (self.n_players - 1)
404+
whose_turn = 1 # start from player 2 and level 2
405+
for lev in range(2, level + 1):
406+
n_isets[whose_turn] += 2 ** (lev - 2)
407+
whose_turn = (whose_turn + 1) % self.n_players
408+
return {p: n_isets[p - 1] for p in players}
409+
410+
def _redu_strategies_level_1(self, player):
411+
return ["1", "2"] if player == 1 else ["*"]
412+
413+
def player_with_changes(self, level):
414+
return ((level - 1) % self.n_players) + 1
415+
416+
def last_player_with_changes(self, level):
417+
return ((level - 2) % self.n_players) + 1
418+
419+
@abstractmethod
420+
def _redu_strats(self, player, level):
421+
pass
422+
423+
def reduced_strategies(self):
424+
rs = [self._redu_strats(player, self.level) for player in self.players]
425+
self.set_size_of_rsf(rs)
426+
return rs
427+
428+
def create_binary_tree(self, g, node, whose_turn, depth, max_depth):
429+
# whose_turn cycles through 0,1,n_players-1; current player is str(whose_turn + 1)
430+
if depth == max_depth:
431+
g.set_outcome(node, g.add_outcome([0] * self.n_players))
432+
else:
433+
current_player = str(whose_turn + 1)
434+
g.append_move(node, current_player, ["L", "R"])
435+
436+
whose_turn = (whose_turn + 1) % self.n_players
437+
for child in node.children:
438+
self.create_binary_tree(g, child, whose_turn, depth + 1, max_depth)
439+
440+
def gbt_game(self):
441+
g = gbt.Game.new_tree(
442+
players=[str(p) for p in self.players],
443+
title=f"Binary Tree Game (L={self.level})",
444+
)
445+
self.create_binary_tree(g, g.root, 0, 0, self.level)
446+
for n in g.nodes:
447+
if not n.is_terminal and not n.children[0].is_terminal:
448+
g.set_infoset(n.children[1], n.children[0].infoset)
449+
return g
450+
451+
def reduced_strategic_form(self):
452+
# special case for 1 player
453+
dims = (
454+
(self.size_of_rsf[0], 1) if len(self.size_of_rsf) == 1 else self.size_of_rsf
455+
)
456+
457+
zeros = np.zeros(dims, dtype=int)
458+
return [zeros] * len(self.players)
459+
460+
461+
class BinEfgOnePlayerIR(BinaryTreeGames):
462+
463+
def __init__(self, params):
464+
super().__init__(n_players=1, params=params)
465+
466+
def _redu_strats(self, player, level):
467+
if level == 1:
468+
return self._redu_strategies_level_1(player)
469+
else:
470+
tmp = self._redu_strats(1, level - 1)
471+
tmp = [
472+
t[1:] for t in tmp
473+
] # remove first action (1 from 1st half; 2 from 2nd half)
474+
n_half = int(len(tmp) / 2)
475+
first_half = tmp[:n_half]
476+
second_half = tmp[n_half:]
477+
n_stars = (
478+
self.get_n_infosets(level)[1] - self.get_n_infosets(level - 1)[1] - 1
479+
)
480+
stars = "*" * n_stars
481+
return (
482+
["11" + t + stars for t in first_half]
483+
+ ["12" + t + stars for t in second_half]
484+
+ ["21" + stars + t for t in first_half]
485+
+ ["22" + stars + t for t in second_half]
486+
)
487+
488+
489+
class BinEfgTwoOrThreePlayers(BinaryTreeGames):
490+
491+
def _redu_strats(self, player, level):
492+
if level == 1:
493+
return self._redu_strategies_level_1(player)
494+
elif player == self.player_with_changes(level):
495+
if player == 1:
496+
last_player = self.last_player_with_changes(level)
497+
tmp1 = self.get_n_infosets(level)
498+
tmp2 = self.get_n_infosets(level - 1)
499+
n_stars = tmp1[player] - tmp2[last_player] - 1
500+
stars = "*" * n_stars
501+
return [
502+
"1" + t + stars
503+
for t in self._redu_strats(player=last_player, level=level - 1)
504+
] + [
505+
"2" + stars + t
506+
for t in self._redu_strats(player=last_player, level=level - 1)
507+
]
508+
elif player == 2:
509+
tmp = self._redu_strats(player=1, level=level - 1)
510+
tmp = [
511+
t[1:] for t in tmp
512+
] # remove first action (1 from 1st half; 2 from 2nd half)
513+
# split into two halves
514+
n_half = int(len(tmp) / 2)
515+
first_half = tmp[:n_half]
516+
second_half = tmp[n_half:]
517+
# create first half suffix
518+
first_half = product(first_half, first_half)
519+
first_half = ["".join(t) for t in first_half]
520+
first_half = ["1" + t for t in first_half] # add 1 to front
521+
# create second half suffix
522+
second_half = product(second_half, second_half)
523+
second_half = ["".join(t) for t in second_half]
524+
second_half = ["2" + t for t in second_half] # add 2 to front
525+
return first_half + second_half # glue halves together
526+
else: # player == 3:
527+
tmp = self._redu_strats(player=2, level=level - 1)
528+
tmp = product(tmp, tmp)
529+
tmp = ["".join(t) for t in tmp]
530+
return tmp
531+
else:
532+
return self._redu_strats(player, level - 1)
533+
534+
535+
class BinEfgTwoPlayer(BinEfgTwoOrThreePlayers):
536+
def __init__(self, params):
537+
super().__init__(n_players=2, params=params)
538+
539+
540+
class BinEfgThreePlayer(BinEfgTwoOrThreePlayers):
541+
def __init__(self, params):
542+
super().__init__(n_players=3, params=params)
543+
544+
237545
def make_rational(input: str):
238546
return gbt.Rational(input)
239547

tests/test_extensive.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,37 @@ def test_outcome_index_exception_label():
293293
),
294294
],
295295
),
296+
# Centipede
297+
(games.Centipede.get_test_data(N=3, m0=2, m1=7)),
298+
(games.Centipede.get_test_data(N=4, m0=2, m1=7)),
299+
(games.Centipede.get_test_data(N=5, m0=2, m1=7)),
300+
(games.Centipede.get_test_data(N=3, m0=1, m1=3)),
301+
(games.Centipede.get_test_data(N=4, m0=1, m1=3)),
302+
(games.Centipede.get_test_data(N=5, m0=1, m1=3)),
303+
(games.Centipede.get_test_data(N=9, m0=3, m1=11)),
304+
# Two player binary tree
305+
(games.BinEfgTwoPlayer.get_test_data(level=1)),
306+
(games.BinEfgTwoPlayer.get_test_data(level=2)),
307+
(games.BinEfgTwoPlayer.get_test_data(level=3)),
308+
(games.BinEfgTwoPlayer.get_test_data(level=4)),
309+
(games.BinEfgTwoPlayer.get_test_data(level=5)),
310+
(games.BinEfgTwoPlayer.get_test_data(level=6)),
311+
(games.BinEfgTwoPlayer.get_test_data(level=7)),
312+
# Three player binary tree
313+
(games.BinEfgThreePlayer.get_test_data(level=1)),
314+
(games.BinEfgThreePlayer.get_test_data(level=2)),
315+
(games.BinEfgThreePlayer.get_test_data(level=3)),
316+
(games.BinEfgThreePlayer.get_test_data(level=4)),
317+
(games.BinEfgThreePlayer.get_test_data(level=5)),
318+
(games.BinEfgThreePlayer.get_test_data(level=6)),
319+
# One player IR binary tree
320+
(games.BinEfgOnePlayerIR.get_test_data(level=1)),
321+
(games.BinEfgOnePlayerIR.get_test_data(level=2)),
322+
(games.BinEfgOnePlayerIR.get_test_data(level=3)),
323+
(games.BinEfgOnePlayerIR.get_test_data(level=4)),
324+
(games.BinEfgOnePlayerIR.get_test_data(level=5)),
325+
(games.BinEfgOnePlayerIR.get_test_data(level=6)),
326+
#
296327
# I M P E R F E C T R E C A L L --- commented out in the test suite
297328
# Wichardt (2008): binary tree of height 3; 2 players; the root player forgets the action
298329
# (

0 commit comments

Comments
 (0)