Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactored games to generalise & add asymmetric games #1413

Merged
merged 27 commits into from
Apr 24, 2023
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8de2ba7
Added asymmetric games and made regular games a subclass
alexhroom Mar 16, 2023
60482f9
Merge branch 'Axelrod-Python:dev' into dev
alexhroom Mar 19, 2023
f1207b0
small improvements to code style
alexhroom Mar 19, 2023
09fb251
fixing docs mock...
alexhroom Mar 19, 2023
ea4c660
Revert "fixing docs mock..."
alexhroom Mar 19, 2023
c593dd4
used IntEnum to simplify
alexhroom Mar 20, 2023
22e6197
small improvements & a fix
alexhroom Mar 20, 2023
95b9517
added asymmetric games to docs
alexhroom Mar 20, 2023
3a8cde2
added werror if invalid payoff matrices are given
alexhroom Mar 23, 2023
c8439bc
removed .item()
alexhroom Mar 27, 2023
bb6171c
changed dbs.action_to_int to use IntEnum behaviour
alexhroom Mar 27, 2023
9cb0f8c
Revert "changed dbs.action_to_int to use IntEnum behaviour"
alexhroom Mar 27, 2023
d88c4d3
made library code more robust wrt integer actions
alexhroom Apr 9, 2023
eaa1603
all strategies now work with integer actions
alexhroom Apr 9, 2023
8f19561
added tests and fixed __eq__
alexhroom Apr 9, 2023
524b278
improved coverage
alexhroom Apr 9, 2023
29506f3
changed Action back to Enum and added casting to score
alexhroom Apr 15, 2023
43bb3a5
added casting test
alexhroom Apr 15, 2023
add9a26
Merge branch 'Axelrod-Python:dev' into dev
alexhroom Apr 15, 2023
a333d62
removed numpy mocking
alexhroom Apr 15, 2023
7495e3b
changed doc due to doctests being picky
alexhroom Apr 15, 2023
b9e78ea
re-fixed doctest examples
alexhroom Apr 16, 2023
90b0a44
review changes
alexhroom Apr 19, 2023
0afa186
Added tutorial for implementing new games
alexhroom Apr 20, 2023
0489728
fixed formatting
alexhroom Apr 20, 2023
c79d4c5
made doctests work
alexhroom Apr 20, 2023
f2452ee
Merge branch 'dev' of https://github.com/alexhroom/axelrod into dev
alexhroom Apr 20, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ jobs:
python -m pip install sphinx
python -m pip install sphinx_rtd_theme
python -m pip install mock
python -m pip install numpy
cd docs; make clean; make html; cd ..;
- name: Run doctests
run: |
Expand Down
2 changes: 1 addition & 1 deletion axelrod/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from axelrod.load_data_ import load_pso_tables, load_weights
from axelrod import graph
from axelrod.plot import Plot
from axelrod.game import DefaultGame, Game
from axelrod.game import DefaultGame, AsymmetricGame, Game
from axelrod.history import History, LimitedHistory
from axelrod.player import Player
from axelrod.classifier import Classifiers
Expand Down
4 changes: 2 additions & 2 deletions axelrod/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ def from_char(cls, character):
UnknownActionError
If the input string is not 'C' or 'D'
"""
if character == "C":
if character == "C" or character == "0":
alexhroom marked this conversation as resolved.
Show resolved Hide resolved
return cls.C
if character == "D":
if character == "D" or character == "1":
return cls.D
raise UnknownActionError('Character must be "C" or "D".')
alexhroom marked this conversation as resolved.
Show resolved Hide resolved

Expand Down
111 changes: 86 additions & 25 deletions axelrod/game.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from typing import Tuple, Union
from enum import Enum

import numpy as np

from axelrod import Action

Expand All @@ -7,7 +10,7 @@
Score = Union[int, float]


class Game(object):
class AsymmetricGame(object):
"""Container for the game matrix and scoring logic.

Attributes
Expand All @@ -16,9 +19,85 @@ class Game(object):
The numerical score attribute to all combinations of action pairs.
"""

def __init__(
self, r: Score = 3, s: Score = 0, t: Score = 5, p: Score = 1
) -> None:
# pylint: disable=invalid-name
def __init__(self, A: np.array, B: np.array) -> None:
"""
Creates an asymmetric game from two matrices.

Parameters
----------
A: np.array
the payoff matrix for player A.
B: np.array
the payoff matrix for player B.
"""

if A.shape != B.transpose().shape:
raise ValueError(
"AsymmetricGame was given invalid payoff matrices; the shape "
"of matrix A should be the shape of B's transpose matrix."
)

self.A = A
alexhroom marked this conversation as resolved.
Show resolved Hide resolved
self.B = B

self.scores = {
pair: self.score(pair) for pair in ((C, C), (D, D), (C, D), (D, C))
}

def score(
self, pair: Union[Tuple[Action, Action], Tuple[int, int]]
) -> Tuple[Score, Score]:
"""Returns the appropriate score for a decision pair.
Parameters
----------
pair: tuple(int, int) or tuple(Action, Action)
A pair of actions for two players, for example (0, 1) corresponds
to the row player choosing their first action and the column
player choosing their second action; in the prisoners' dilemma,
this is equivalent to player 1 cooperating and player 2 defecting.
Can also be a pair of Actions, where C corresponds to '0'
and D to '1'.

Returns
-------
tuple of int or float
Scores for two player resulting from their actions.
"""

# if an Action has been passed to the method,
# get which integer the Action corresponds to
def get_value(x):
if isinstance(x, Enum):
return x.value
return x
row, col = map(get_value, pair)

return (self.A[row][col], self.B[row][col])

def __repr__(self) -> str:
return "Axelrod game with matrices: {}".format((self.A, self.B))

def __eq__(self, other):
if not isinstance(other, AsymmetricGame):
return False
return self.A.all() == other.A.all() and self.B.all() == other.B.all()


class Game(AsymmetricGame):
"""
Simplification of the AsymmetricGame class for symmetric games.
Takes advantage of Press and Dyson notation.

Can currently only be 2x2.

Attributes
----------
scores: dict
The numerical score attribute to all combinations of action pairs.
"""

def __init__(self, r: Score = 3, s: Score = 0, t: Score = 5, p: Score = 1) -> None:
"""Create a new game object.

Parameters
Expand All @@ -32,12 +111,9 @@ def __init__(
p: int or float
Score obtained by both player for mutual defection.
"""
self.scores = {
(C, C): (r, r),
(D, D): (p, p),
(C, D): (s, t),
(D, C): (t, s),
}
A = np.array([[r, s], [t, p]])

super().__init__(A, A.transpose())

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

def score(self, pair: Tuple[Action, Action]) -> Tuple[Score, Score]:
"""Returns the appropriate score for a decision pair.

Parameters
----------
pair: tuple(Action, Action)
A pair actions for two players, for example (C, C).

Returns
-------
tuple of int or float
Scores for two player resulting from their actions.
"""
return self.scores[pair]

def __repr__(self) -> str:
return "Axelrod game: (R,P,S,T) = {}".format(self.RPST())

Expand Down
7 changes: 4 additions & 3 deletions axelrod/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def __init__(self, plays=None, coplays=None):
def append(self, play, coplay):
"""Appends a new (play, coplay) pair an updates metadata for
number of cooperations and defections, and the state distribution."""
# casts plays sent as integers into Action objects
self._plays.append(play)
self._actions[play] += 1
self._coplays.append(coplay)
Expand Down Expand Up @@ -130,12 +131,12 @@ def flip_plays(self):
def append(self, play, coplay):
"""Appends a new (play, coplay) pair an updates metadata for
number of cooperations and defections, and the state distribution."""
# casts plays sent as integers into Action objects
alexhroom marked this conversation as resolved.
Show resolved Hide resolved

self._plays.append(play)
self._actions[play] += 1
if coplay:
self._coplays.append(coplay)
self._state_distribution[(play, coplay)] += 1
self._coplays.append(coplay)
self._state_distribution[(play, coplay)] += 1
if len(self._plays) > self.memory_depth:
first_play, first_coplay = self._plays.pop(0), self._coplays.pop(0)
self._actions[first_play] -= 1
Expand Down
14 changes: 14 additions & 0 deletions axelrod/tests/property.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
lists,
sampled_from,
)
from hypothesis.extra.numpy import arrays


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

game = axl.Game(r=r, s=s, t=t, p=p)
return game


@composite
def asymmetric_games(draw, valid=True):
alexhroom marked this conversation as resolved.
Show resolved Hide resolved
"""Hypothesis decorator to draw a random asymmetric game."""

rows = draw(integers(min_value=2, max_value=255))
cols = draw(integers(min_value=2, max_value=255))

A = draw(arrays(int, (rows, cols)))
B = draw(arrays(int, (cols, rows)))

return axl.AsymmetricGame(A, B)
53 changes: 52 additions & 1 deletion axelrod/tests/unit/test_game.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import unittest

import numpy as np

import axelrod as axl
from axelrod.tests.property import games
from axelrod.tests.property import games, asymmetric_games
from hypothesis import given, settings
from hypothesis.strategies import integers
from hypothesis.extra.numpy import arrays, array_shapes


C, D = axl.Action.C, axl.Action.D

Expand Down Expand Up @@ -77,3 +81,50 @@ def test_random_repr(self, game):
expected_repr = "Axelrod game: (R,P,S,T) = {}".format(game.RPST())
self.assertEqual(expected_repr, game.__repr__())
self.assertEqual(expected_repr, str(game))

@given(game=games())
def test_integer_actions(self, game):
"""Test Actions and integers are treated equivalently."""
pair_ints = {
(C, C): (0 ,0),
(C, D): (0, 1),
(D, C): (1, 0),
(D, D): (1, 1)
}
for key, value in pair_ints.items():
self.assertEqual(game.score(key), game.score(value))

class TestAsymmetricGame(unittest.TestCase):
@given(A=arrays(int, array_shapes(min_dims=2, max_dims=2, min_side=2)),
B=arrays(int, array_shapes(min_dims=2, max_dims=2, min_side=2)))
@settings(max_examples=5)
def test_invalid_matrices(self, A, B):
"""Test that an error is raised when the matrices aren't the right size."""
# ensures that an error is raised when the shapes are invalid,
# and not raised otherwise
error_raised = False
try:
game = axl.AsymmetricGame(A, B)
except ValueError:
error_raised = True

self.assertEqual(error_raised, (A.shape != B.transpose().shape))

@given(asymgame=asymmetric_games())
@settings(max_examples=5)
def test_random_repr(self, asymgame):
"""Test repr with random scores."""
expected_repr = "Axelrod game with matrices: {}".format((asymgame.A, asymgame.B))
self.assertEqual(expected_repr, asymgame.__repr__())
self.assertEqual(expected_repr, str(asymgame))

@given(asymgame1=asymmetric_games(),
asymgame2=asymmetric_games())
@settings(max_examples=5)
def test_equality(self, asymgame1, asymgame2):
"""Tests equality of AsymmetricGames based on their matrices."""
self.assertFalse(asymgame1=='foo')
self.assertEqual(asymgame1, asymgame1)
self.assertEqual(asymgame2, asymgame2)
self.assertEqual((asymgame1==asymgame2), (asymgame1.A.all() == asymgame2.A.all()
and asymgame1.B.all() == asymgame2.B.all()))
3 changes: 0 additions & 3 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@
"matplotlib.transforms",
"mpl_toolkits.axes_grid1",
"multiprocess",
"numpy",
"numpy.linalg",
"numpy.random",
"pandas",
"pandas.util",
"pandas.util.decorators",
Expand Down
25 changes: 25 additions & 0 deletions docs/how-to/use_different_stage_games.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,28 @@ The default Prisoner's dilemma has different results::
>>> results = tournament.play()
>>> results.ranked_names
['Defector', 'Tit For Tat', 'Cooperator']

Asymmetric games can also be implemented via the AsymmetricGame class
with two Numpy arrays for payoff matrices::

>>> import numpy as np
>>> A = np.array([[3, 1], [1, 3]])
>>> B = np.array([[1, 3], [2, 1]])
>>> asymmetric_game = axl.AsymmetricGame(A, B)
>>> asymmetric_game # doctest: +NORMALIZE_WHITESPACE
alexhroom marked this conversation as resolved.
Show resolved Hide resolved
Axelrod game with matrices: (array([[3, 1],
[1, 3]]),
array([[1, 3],
[2, 1]]))

Asymmetric games can also be different sizes, such as Rock Paper Scissors::
alexhroom marked this conversation as resolved.
Show resolved Hide resolved

>>> A = np.array([[0, -1, 1], [1, 0, -1], [-1, 1, 0]])
>>> rock_paper_scissors = axl.AsymmetricGame(A, -A)
>>> rock_paper_scissors # doctest: +NORMALIZE_WHITESPACE
Axelrod game with matrices: (array([[ 0, -1, 1],
[ 1, 0, -1],
[-1, 1, 0]]),
array([[ 0, 1, -1],
[-1, 0, 1],
[ 1, -1, 0]]))