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

Added player assumption classification #1418

Merged
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e51a310
added attributes to mock player
alexhroom May 2, 2023
5e4f350
fixed mock player addition
alexhroom May 2, 2023
81d2524
added test for size checking
alexhroom May 2, 2023
d0351a5
added actions_size to Classifier
alexhroom May 3, 2023
ce7d798
added actions_size to tutorial
alexhroom May 3, 2023
d037915
re-added the actual implementation (git shenanigans)
alexhroom May 3, 2023
c1f6ab0
fixed typo
alexhroom May 3, 2023
cdb72d8
fixed docstring
alexhroom May 5, 2023
0737227
changed size checking to a general assumptions model
alexhroom May 8, 2023
76f93b6
fixed some bugs
alexhroom May 8, 2023
0cffa9b
added some tests
alexhroom May 8, 2023
44992be
added user option for strictness of assumption checking
alexhroom May 8, 2023
a443887
fixed tests
alexhroom May 8, 2023
89c438c
updated docs
alexhroom May 8, 2023
8b4aa6c
added assumptions_satisfy helper method
alexhroom May 11, 2023
c4359bc
removed unnecessary if statement
alexhroom May 11, 2023
46cbf02
Revert "removed unnecessary if statement"
alexhroom May 11, 2023
f705ffd
added test for assumptions_satisfy
alexhroom May 12, 2023
58a7218
changed attributes -> characteristics
alexhroom May 12, 2023
24eccc5
more attributes -> characteristics
alexhroom May 12, 2023
88d65d4
changed name to game_characteristics when not directly referencing a …
alexhroom May 16, 2023
16048c1
Adds a read the docs config file (#1423)
alexhroom May 17, 2023
fc80c19
Merge branch 'dev' into action-size-classification
alexhroom May 18, 2023
d59cab9
removed default characteristic
alexhroom May 22, 2023
22b61cf
made mock player safer
alexhroom May 22, 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
2 changes: 2 additions & 0 deletions axelrod/classifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ def classify_player(self, player: Type[Player]) -> T:
manipulates_state = Classifier[Optional[bool]](
"manipulates_state", lambda _: None
)
assumptions = Classifier[Optional[dict]]("assumptions", lambda _: {'actions_size': 2})

# Should list all known classifiers.
all_classifiers = [
Expand All @@ -82,6 +83,7 @@ def classify_player(self, player: Type[Player]) -> T:
inspects_source,
manipulates_source,
manipulates_state,
assumptions,
]

all_classifiers_map = {c.name: c.classify_player for c in all_classifiers}
Expand Down
46 changes: 42 additions & 4 deletions axelrod/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@ class AsymmetricGame(object):
----------
scores: dict
The numerical score attribute to all combinations of action pairs.
attributes: dict
A dictionary of attributes of the game. Used to ensure strategies
used with the game are valid.
"""

# pylint: disable=invalid-name
def __init__(self, A: np.array, B: np.array) -> None:
def __init__(self, A: np.array, B: np.array, **attributes) -> None:
"""
Creates an asymmetric game from two matrices.

Expand All @@ -30,6 +33,9 @@ def __init__(self, A: np.array, B: np.array) -> None:
the payoff matrix for player A.
B: np.array
the payoff matrix for player B.
**attributes
optional attributes for the game. Used
to ensure strategies used with the game are valid.
"""

if A.shape != B.transpose().shape:
Expand All @@ -41,6 +47,8 @@ def __init__(self, A: np.array, B: np.array) -> None:
self.A = A
self.B = B

self.attributes = attributes

self.scores = {
pair: self.score(pair) for pair in ((C, C), (D, D), (C, D), (D, C))
}
Expand Down Expand Up @@ -75,6 +83,27 @@ def get_value(x):

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

@property
def attributes(self):
return self._attributes

@attributes.setter
def attributes(self, attributes):
"""
Adds or changes game attributes.

Parameters
----------
attributes: dict
Attributes to add to the game. If the added
attribute already exists, it will overwrite the
previous value.
"""
try:
self._attributes = {**self._attributes, **attributes}
except AttributeError:
self._attributes = attributes

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

Expand All @@ -97,7 +126,7 @@ class Game(AsymmetricGame):
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:
def __init__(self, r: Score = 3, s: Score = 0, t: Score = 5, p: Score = 1, **attributes) -> None:
"""Create a new game object.

Parameters
Expand All @@ -110,10 +139,19 @@ def __init__(self, r: Score = 3, s: Score = 0, t: Score = 5, p: Score = 1) -> No
Score obtained by a player for defecting against a cooperator.
p: int or float
Score obtained by both player for mutual defection.
**attributes
optional attributes for the game. Used
to ensure strategies used with the game are valid.

"""
A = np.array([[r, s], [t, p]])

super().__init__(A, A.transpose())
default_attributes = {
'has_RPST': True
alexhroom marked this conversation as resolved.
Show resolved Hide resolved
}
attributes = {**default_attributes, **attributes}

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

def RPST(self) -> Tuple[Score, Score, Score, Score]:
"""Returns game matrix values in Press and Dyson notation."""
Expand All @@ -132,4 +170,4 @@ def __eq__(self, other):
return self.RPST() == other.RPST()


DefaultGame = Game()
DefaultGame = Game(3, 0, 5, 1, game_type='prisoners_dilemma')
13 changes: 13 additions & 0 deletions axelrod/match.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def __init__(
match_attributes=None,
reset=True,
seed=None,
strict_player_checking=True
):
"""
Parameters
Expand All @@ -54,6 +55,9 @@ def __init__(
Whether to reset players or not
seed : int
Random seed for reproducibility
strict_player_checking: bool, default True
If True, throws an error if strategies make assumptions which aren't
compatible with the game. if False, just produces warnings instead.
"""

defaults = {
Expand Down Expand Up @@ -89,6 +93,7 @@ def __init__(
else:
self.match_attributes = match_attributes

self.strict_player_checking = strict_player_checking
self.players = list(players)
self.reset = reset

Expand All @@ -111,6 +116,14 @@ def players(self):
def players(self, players):
"""Ensure that players are passed the match attributes"""
newplayers = []
# ensure the game satisfies the player assumptions
# note the game size attribute is added here because the player
# and coplayer may have different game sizes if the game is asymmetric!
players[0].check_assumptions({**self.game.attributes, 'actions_size': self.game.A.shape[0]},
raise_error=self.strict_player_checking)
players[1].check_assumptions({**self.game.attributes, 'actions_size': self.game.B.shape[0]},
raise_error=self.strict_player_checking)

for player in players:
player.set_match_attributes(**self.match_attributes)
newplayers.append(player)
Expand Down
16 changes: 13 additions & 3 deletions axelrod/mock_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,26 @@


class MockPlayer(Player):
"""Creates a mock player that plays a given sequence of actions. If
no actions are given, plays like Cooperator. Used for testing.
"""Creates a mock player that cycles through a given
sequence of actions. If no actions are given,
plays like Cooperator. Used for testing.

Parameters
----------
actions: List[Action], default []
The sequence of actions played by the mock player.
attributes: dict, default {}
A dictionary of player attributes.
"""

name = "Mock Player"

def __init__(self, actions: List[Action] = None) -> None:
def __init__(self, actions: List[Action] = None, classifier: dict = None) -> None:
super().__init__()
if not actions:
actions = []
if classifier:
alexhroom marked this conversation as resolved.
Show resolved Hide resolved
self.classifier = classifier
self.actions = cycle(actions)

def strategy(self, opponent: Player) -> Action:
Expand Down
31 changes: 31 additions & 0 deletions axelrod/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,37 @@ def reset(self):
def update_history(self, play, coplay):
self.history.append(play, coplay)

def check_assumptions(self, attributes, raise_error=True):
"""
Compares the player assumptions to a dictionary of attributes.
Generates a warning or error if an assumption is not fulfilled.

Parameters:
-----------
attributes: dict
alexhroom marked this conversation as resolved.
Show resolved Hide resolved
The dictionary of attributes to compare the player's assumptions to.
raise_error: bool, default True
If True, raises an error if the assumption is violated. Else,
just generate a warning.
"""

for key, value in self.classifier.get('assumptions', {}).items():
msg = None
if key not in attributes.keys():
msg = ("Player {} assumes that "
"the game has the attribute {}, "
"but the game does not declare this attribute."
"".format(self.name, key))
elif value != attributes[key]:
msg = ("Player {} assumes that the game attribute "
"{} is set to {}, but it is actually set to {}."
"".format(self.name, key, value, attributes[key]))

if msg is not None:
if raise_error:
raise RuntimeError(msg)
warnings.warn(msg + " The strategy may not behave as expected.")

@property
def history(self):
return self._history
Expand Down
22 changes: 22 additions & 0 deletions axelrod/tests/strategies/test_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,28 @@ def test_init_kwargs(self):
TypeError, ParameterisedTestPlayer, "other", "other", "other"
)

def test_assumption_checking(self):
"""
Checks that assumptions are checked, and warnings
or errors are raised when they're unfulfilled.
"""
player = axl.MockPlayer(classifier={'assumptions': {'foo': True, 'bar': 3}})

# these should pass without errors/warnings
player.check_assumptions({'foo': True, 'bar': 3})
player.check_assumptions({'foo': True, 'bar': 3, 'baz': []})

with self.assertRaises(RuntimeError):
player.check_assumptions({'foo': True})
with self.assertRaises(RuntimeError):
player.check_assumptions({'foo': True, 'bar': 5})

with self.assertWarns(UserWarning):
player.check_assumptions({'foo': True}, raise_error=False)
with self.assertWarns(UserWarning):
player.check_assumptions({'foo': True, 'bar': 5}, raise_error=False)



class TestOpponent(axl.Player):
"""A player who only exists so we have something to test against"""
Expand Down
17 changes: 15 additions & 2 deletions axelrod/tests/unit/test_match.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@

import axelrod as axl
from axelrod.deterministic_cache import DeterministicCache
from axelrod.mock_player import MockPlayer
from axelrod.random_ import RandomGenerator
from axelrod.tests.property import games
from hypothesis import example, given
from axelrod.tests.property import games, asymmetric_games
from hypothesis import example, given, settings
from hypothesis.strategies import floats, integers

C, D = axl.Action.C, axl.Action.D
Expand Down Expand Up @@ -354,6 +355,18 @@ def test_sparklines(self):
expected_sparklines = "XXXX\nXYXY"
self.assertEqual(match.sparklines("X", "Y"), expected_sparklines)

@given(game=asymmetric_games(), n1=integers(min_value=2), n2=integers(min_value=2))
@settings(max_examples=5)
def test_game_size_checking(self, game, n1, n2):
"""Tests warnings, errors or normal flow agrees with player action size."""
player1 = MockPlayer(classifier={'assumptions': {'actions_size': n1}})
player2 = MockPlayer(classifier={'assumptions': {'actions_size': n2}})

if (n1 != game.A.shape[0] or n2 != game.B.shape[0]):
with self.assertRaises(RuntimeError):
match = axl.Match((player1, player2), game=game)
else:
match = axl.Match((player1, player2), game=game)

class TestSampleLength(unittest.TestCase):
def test_sample_length(self):
Expand Down
7 changes: 7 additions & 0 deletions docs/tutorials/implement_new_games/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ an initialisation parameter for which move they start with::
... "inspects_source": False,
... "manipulates_source": False,
... "manipulates_state": False,
... "assumptions": {"actions_size": 3},
... }
...
... def __init__(self, starting_move=S):
Expand Down Expand Up @@ -153,6 +154,7 @@ an initialisation parameter for which move they start with::
... "inspects_source": False,
... "manipulates_source": False,
... "manipulates_state": False,
... "assumptions": {"actions_size": 3},
... }
...
... def __init__(self, starting_move=S):
Expand All @@ -165,6 +167,11 @@ an initialisation parameter for which move they start with::
... return self.starting_move
... return self.history[-1].rotate()

Note that in the classifier for each strategy we set 'actions_size' to `3`. This
is how we let Axelrod know that this strategy is expecting to have 3 possible actions,
and when a match is created, it will check to make sure that this assumption is
satisfied by the game.

We are now all set to run some matches and tournaments in our new game!
Let's start with a match between our two new players::

Expand Down