Skip to content

Commit afac86a

Browse files
authored
Refactored games to generalise & add asymmetric games (#1413)
* Added asymmetric games and made regular games a subclass * small improvements to code style * fixing docs mock... * Revert "fixing docs mock..." This reverts commit 09fb251. * used IntEnum to simplify * small improvements & a fix * added asymmetric games to docs * added werror if invalid payoff matrices are given * removed .item() * changed dbs.action_to_int to use IntEnum behaviour * Revert "changed dbs.action_to_int to use IntEnum behaviour" This reverts commit bb6171c. * made library code more robust wrt integer actions * all strategies now work with integer actions * added tests and fixed __eq__ * improved coverage * changed Action back to Enum and added casting to score * added casting test * removed numpy mocking * changed doc due to doctests being picky * re-fixed doctest examples * review changes * Added tutorial for implementing new games * fixed formatting * made doctests work
1 parent c27ad09 commit afac86a

File tree

10 files changed

+378
-34
lines changed

10 files changed

+378
-34
lines changed

.github/workflows/config.yml

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ jobs:
3838
python -m pip install sphinx
3939
python -m pip install sphinx_rtd_theme
4040
python -m pip install mock
41+
python -m pip install numpy
4142
cd docs; make clean; make html; cd ..;
4243
- name: Run doctests
4344
run: |

axelrod/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from axelrod.load_data_ import load_pso_tables, load_weights
1717
from axelrod import graph
1818
from axelrod.plot import Plot
19-
from axelrod.game import DefaultGame, Game
19+
from axelrod.game import DefaultGame, AsymmetricGame, Game
2020
from axelrod.history import History, LimitedHistory
2121
from axelrod.player import Player
2222
from axelrod.classifier import Classifiers

axelrod/game.py

+86-25
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
from typing import Tuple, Union
2+
from enum import Enum
3+
4+
import numpy as np
25

36
from axelrod import Action
47

@@ -7,7 +10,7 @@
710
Score = Union[int, float]
811

912

10-
class Game(object):
13+
class AsymmetricGame(object):
1114
"""Container for the game matrix and scoring logic.
1215
1316
Attributes
@@ -16,9 +19,85 @@ class Game(object):
1619
The numerical score attribute to all combinations of action pairs.
1720
"""
1821

19-
def __init__(
20-
self, r: Score = 3, s: Score = 0, t: Score = 5, p: Score = 1
21-
) -> None:
22+
# pylint: disable=invalid-name
23+
def __init__(self, A: np.array, B: np.array) -> None:
24+
"""
25+
Creates an asymmetric game from two matrices.
26+
27+
Parameters
28+
----------
29+
A: np.array
30+
the payoff matrix for player A.
31+
B: np.array
32+
the payoff matrix for player B.
33+
"""
34+
35+
if A.shape != B.transpose().shape:
36+
raise ValueError(
37+
"AsymmetricGame was given invalid payoff matrices; the shape "
38+
"of matrix A should be the shape of B's transpose matrix."
39+
)
40+
41+
self.A = A
42+
self.B = B
43+
44+
self.scores = {
45+
pair: self.score(pair) for pair in ((C, C), (D, D), (C, D), (D, C))
46+
}
47+
48+
def score(
49+
self, pair: Union[Tuple[Action, Action], Tuple[int, int]]
50+
) -> Tuple[Score, Score]:
51+
"""Returns the appropriate score for a decision pair.
52+
Parameters
53+
----------
54+
pair: tuple(int, int) or tuple(Action, Action)
55+
A pair of actions for two players, for example (0, 1) corresponds
56+
to the row player choosing their first action and the column
57+
player choosing their second action; in the prisoners' dilemma,
58+
this is equivalent to player 1 cooperating and player 2 defecting.
59+
Can also be a pair of Actions, where C corresponds to '0'
60+
and D to '1'.
61+
62+
Returns
63+
-------
64+
tuple of int or float
65+
Scores for two player resulting from their actions.
66+
"""
67+
68+
# if an Action has been passed to the method,
69+
# get which integer the Action corresponds to
70+
def get_value(x):
71+
if isinstance(x, Enum):
72+
return x.value
73+
return x
74+
row, col = map(get_value, pair)
75+
76+
return (self.A[row][col], self.B[row][col])
77+
78+
def __repr__(self) -> str:
79+
return "Axelrod game with matrices: {}".format((self.A, self.B))
80+
81+
def __eq__(self, other):
82+
if not isinstance(other, AsymmetricGame):
83+
return False
84+
return self.A.all() == other.A.all() and self.B.all() == other.B.all()
85+
86+
87+
class Game(AsymmetricGame):
88+
"""
89+
Simplification of the AsymmetricGame class for symmetric games.
90+
Takes advantage of Press and Dyson notation.
91+
92+
Can currently only be 2x2.
93+
94+
Attributes
95+
----------
96+
scores: dict
97+
The numerical score attribute to all combinations of action pairs.
98+
"""
99+
100+
def __init__(self, r: Score = 3, s: Score = 0, t: Score = 5, p: Score = 1) -> None:
22101
"""Create a new game object.
23102
24103
Parameters
@@ -32,12 +111,9 @@ def __init__(
32111
p: int or float
33112
Score obtained by both player for mutual defection.
34113
"""
35-
self.scores = {
36-
(C, C): (r, r),
37-
(D, D): (p, p),
38-
(C, D): (s, t),
39-
(D, C): (t, s),
40-
}
114+
A = np.array([[r, s], [t, p]])
115+
116+
super().__init__(A, A.transpose())
41117

42118
def RPST(self) -> Tuple[Score, Score, Score, Score]:
43119
"""Returns game matrix values in Press and Dyson notation."""
@@ -47,21 +123,6 @@ def RPST(self) -> Tuple[Score, Score, Score, Score]:
47123
T = self.scores[(D, C)][0]
48124
return R, P, S, T
49125

50-
def score(self, pair: Tuple[Action, Action]) -> Tuple[Score, Score]:
51-
"""Returns the appropriate score for a decision pair.
52-
53-
Parameters
54-
----------
55-
pair: tuple(Action, Action)
56-
A pair actions for two players, for example (C, C).
57-
58-
Returns
59-
-------
60-
tuple of int or float
61-
Scores for two player resulting from their actions.
62-
"""
63-
return self.scores[pair]
64-
65126
def __repr__(self) -> str:
66127
return "Axelrod game: (R,P,S,T) = {}".format(self.RPST())
67128

axelrod/history.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,10 @@ def flip_plays(self):
130130
def append(self, play, coplay):
131131
"""Appends a new (play, coplay) pair an updates metadata for
132132
number of cooperations and defections, and the state distribution."""
133-
134133
self._plays.append(play)
135134
self._actions[play] += 1
136-
if coplay:
137-
self._coplays.append(coplay)
138-
self._state_distribution[(play, coplay)] += 1
135+
self._coplays.append(coplay)
136+
self._state_distribution[(play, coplay)] += 1
139137
if len(self._plays) > self.memory_depth:
140138
first_play, first_coplay = self._plays.pop(0), self._coplays.pop(0)
141139
self._actions[first_play] -= 1

axelrod/tests/property.py

+14
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
lists,
1212
sampled_from,
1313
)
14+
from hypothesis.extra.numpy import arrays
1415

1516

1617
@composite
@@ -381,3 +382,16 @@ def games(draw, prisoners_dilemma=True, max_value=100):
381382

382383
game = axl.Game(r=r, s=s, t=t, p=p)
383384
return game
385+
386+
387+
@composite
388+
def asymmetric_games(draw, valid=True):
389+
"""Hypothesis decorator to draw a random asymmetric game."""
390+
391+
rows = draw(integers(min_value=2, max_value=255))
392+
cols = draw(integers(min_value=2, max_value=255))
393+
394+
A = draw(arrays(int, (rows, cols)))
395+
B = draw(arrays(int, (cols, rows)))
396+
397+
return axl.AsymmetricGame(A, B)

axelrod/tests/unit/test_game.py

+52-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import unittest
22

3+
import numpy as np
4+
35
import axelrod as axl
4-
from axelrod.tests.property import games
6+
from axelrod.tests.property import games, asymmetric_games
57
from hypothesis import given, settings
68
from hypothesis.strategies import integers
9+
from hypothesis.extra.numpy import arrays, array_shapes
10+
711

812
C, D = axl.Action.C, axl.Action.D
913

@@ -77,3 +81,50 @@ def test_random_repr(self, game):
7781
expected_repr = "Axelrod game: (R,P,S,T) = {}".format(game.RPST())
7882
self.assertEqual(expected_repr, game.__repr__())
7983
self.assertEqual(expected_repr, str(game))
84+
85+
@given(game=games())
86+
def test_integer_actions(self, game):
87+
"""Test Actions and integers are treated equivalently."""
88+
pair_ints = {
89+
(C, C): (0 ,0),
90+
(C, D): (0, 1),
91+
(D, C): (1, 0),
92+
(D, D): (1, 1)
93+
}
94+
for key, value in pair_ints.items():
95+
self.assertEqual(game.score(key), game.score(value))
96+
97+
class TestAsymmetricGame(unittest.TestCase):
98+
@given(A=arrays(int, array_shapes(min_dims=2, max_dims=2, min_side=2)),
99+
B=arrays(int, array_shapes(min_dims=2, max_dims=2, min_side=2)))
100+
@settings(max_examples=5)
101+
def test_invalid_matrices(self, A, B):
102+
"""Test that an error is raised when the matrices aren't the right size."""
103+
# ensures that an error is raised when the shapes are invalid,
104+
# and not raised otherwise
105+
error_raised = False
106+
try:
107+
game = axl.AsymmetricGame(A, B)
108+
except ValueError:
109+
error_raised = True
110+
111+
self.assertEqual(error_raised, (A.shape != B.transpose().shape))
112+
113+
@given(asymgame=asymmetric_games())
114+
@settings(max_examples=5)
115+
def test_random_repr(self, asymgame):
116+
"""Test repr with random scores."""
117+
expected_repr = "Axelrod game with matrices: {}".format((asymgame.A, asymgame.B))
118+
self.assertEqual(expected_repr, asymgame.__repr__())
119+
self.assertEqual(expected_repr, str(asymgame))
120+
121+
@given(asymgame1=asymmetric_games(),
122+
asymgame2=asymmetric_games())
123+
@settings(max_examples=5)
124+
def test_equality(self, asymgame1, asymgame2):
125+
"""Tests equality of AsymmetricGames based on their matrices."""
126+
self.assertFalse(asymgame1=='foo')
127+
self.assertEqual(asymgame1, asymgame1)
128+
self.assertEqual(asymgame2, asymgame2)
129+
self.assertEqual((asymgame1==asymgame2), (asymgame1.A.all() == asymgame2.A.all()
130+
and asymgame1.B.all() == asymgame2.B.all()))

docs/conf.py

-3
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,6 @@
2626
"matplotlib.transforms",
2727
"mpl_toolkits.axes_grid1",
2828
"multiprocess",
29-
"numpy",
30-
"numpy.linalg",
31-
"numpy.random",
3229
"pandas",
3330
"pandas.util",
3431
"pandas.util.decorators",

docs/how-to/use_different_stage_games.rst

+37
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
.. _use_different_stage_games:
2+
13
Use different stage games
24
=========================
35

@@ -47,3 +49,38 @@ The default Prisoner's dilemma has different results::
4749
>>> results = tournament.play()
4850
>>> results.ranked_names
4951
['Defector', 'Tit For Tat', 'Cooperator']
52+
53+
Asymmetric games can also be implemented via the AsymmetricGame class
54+
with two Numpy arrays for payoff matrices::
55+
56+
>>> import numpy as np
57+
>>> A = np.array([[3, 1], [1, 3]])
58+
>>> B = np.array([[1, 3], [2, 1]])
59+
>>> asymmetric_game = axl.AsymmetricGame(A, B)
60+
>>> asymmetric_game # doctest: +NORMALIZE_WHITESPACE
61+
Axelrod game with matrices: (array([[3, 1],
62+
[1, 3]]),
63+
array([[1, 3],
64+
[2, 1]]))
65+
66+
Asymmetric games can also be different sizes (even if symmetric; regular games
67+
can currently only be 2x2), such as Rock Paper Scissors::
68+
69+
>>> A = np.array([[0, -1, 1], [1, 0, -1], [-1, 1, 0]])
70+
>>> rock_paper_scissors = axl.AsymmetricGame(A, -A)
71+
>>> rock_paper_scissors # doctest: +NORMALIZE_WHITESPACE
72+
Axelrod game with matrices: (array([[ 0, -1, 1],
73+
[ 1, 0, -1],
74+
[-1, 1, 0]]),
75+
array([[ 0, 1, -1],
76+
[-1, 0, 1],
77+
[ 1, -1, 0]]))
78+
79+
**NB: Some features of Axelrod, such as strategy transformers, are specifically created for
80+
use with the iterated Prisoner's Dilemma; they may break with games of other sizes.**
81+
Note also that most strategies in Axelrod are Prisoners' Dilemma strategies, so behave
82+
as though they are playing the Prisoners' Dilemma; in the rock-paper-scissors example above,
83+
they will certainly never choose scissors (because their strategy action set is two actions!)
84+
85+
For a more detailed tutorial on how to implement another game into Axelrod, :ref:`here is a
86+
tutorial using rock paper scissors as an example. <implement-new-games>`

0 commit comments

Comments
 (0)