From e51a31041d7ac280d1997ae0ec79f4f451f55bab Mon Sep 17 00:00:00 2001 From: alexhroom Date: Tue, 2 May 2023 14:17:01 +0100 Subject: [PATCH 01/24] added attributes to mock player --- axelrod/mock_player.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/axelrod/mock_player.py b/axelrod/mock_player.py index 41ee0de2a..5b9d656e4 100644 --- a/axelrod/mock_player.py +++ b/axelrod/mock_player.py @@ -9,15 +9,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. + no actions are given, or it runs out of actions, + 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" + attributes = {} - def __init__(self, actions: List[Action] = None) -> None: + def __init__(self, actions: List[Action] = None, attributes: dict = None) -> None: super().__init__() if not actions: actions = [] + if attributes: + self.attributes = attributes self.actions = cycle(actions) def strategy(self, opponent: Player) -> Action: From 5e4f35004474b24f87858cbecb5259ce5214dd26 Mon Sep 17 00:00:00 2001 From: alexhroom Date: Tue, 2 May 2023 18:21:49 +0100 Subject: [PATCH 02/24] fixed mock player addition --- axelrod/mock_player.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/axelrod/mock_player.py b/axelrod/mock_player.py index 5b9d656e4..d044002f3 100644 --- a/axelrod/mock_player.py +++ b/axelrod/mock_player.py @@ -21,14 +21,13 @@ class MockPlayer(Player): """ name = "Mock Player" - attributes = {} - def __init__(self, actions: List[Action] = None, attributes: dict = None) -> None: + def __init__(self, actions: List[Action] = None, classifier: dict = None) -> None: super().__init__() if not actions: actions = [] - if attributes: - self.attributes = attributes + if classifier: + self.classifier = classifier self.actions = cycle(actions) def strategy(self, opponent: Player) -> Action: From 81d252411c80b19b6e71491271239ea5c4fef48d Mon Sep 17 00:00:00 2001 From: alexhroom Date: Tue, 2 May 2023 18:31:19 +0100 Subject: [PATCH 03/24] added test for size checking --- axelrod/tests/unit/test_match.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/axelrod/tests/unit/test_match.py b/axelrod/tests/unit/test_match.py index 6012ce32f..0d5486e30 100644 --- a/axelrod/tests/unit/test_match.py +++ b/axelrod/tests/unit/test_match.py @@ -3,8 +3,9 @@ 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 axelrod.tests.property import games, asymmetric_games from hypothesis import example, given from hypothesis.strategies import floats, integers @@ -354,6 +355,20 @@ 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)) + def test_game_size_checking(self, game, n1, n2): + """Tests warnings, errors or normal flow agrees with player action size.""" + player1 = MockPlayer(classifier={'actions_size': n1}) + player2 = MockPlayer(classifier={'actions_size': n2}) + + if (n1 > game.A.shape[0] or n2 > game.B.shape[0]): + with self.assertRaises(IndexError): + match = axl.Match((player1, player2), game=game) + elif (n1 < game.A.shape[0] or n2 < game.B.shape[0]): + with self.assertWarns(UserWarning): + match = axl.Match((player1, player2), game=game) + else: + match = axl.Match((player1, player2), game=game) class TestSampleLength(unittest.TestCase): def test_sample_length(self): From d0351a53302826aebc7b85bc5d1b4338aa7e3743 Mon Sep 17 00:00:00 2001 From: alexhroom Date: Wed, 3 May 2023 11:16:13 +0100 Subject: [PATCH 04/24] added actions_size to Classifier --- axelrod/classifier.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/axelrod/classifier.py b/axelrod/classifier.py index 44edf510c..eeee73217 100644 --- a/axelrod/classifier.py +++ b/axelrod/classifier.py @@ -72,6 +72,7 @@ def classify_player(self, player: Type[Player]) -> T: manipulates_state = Classifier[Optional[bool]]( "manipulates_state", lambda _: None ) +actions_size = Classifier[int]("actions_size", lambda _: 2) # Should list all known classifiers. all_classifiers = [ @@ -82,6 +83,7 @@ def classify_player(self, player: Type[Player]) -> T: inspects_source, manipulates_source, manipulates_state, + actions_size, ] all_classifiers_map = {c.name: c.classify_player for c in all_classifiers} From ce7d798ee5375b36249225b2f8780c729b9821fd Mon Sep 17 00:00:00 2001 From: alexhroom Date: Wed, 3 May 2023 11:19:06 +0100 Subject: [PATCH 05/24] added actions_size to tutorial --- docs/tutorials/implement_new_games/index.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/tutorials/implement_new_games/index.rst b/docs/tutorials/implement_new_games/index.rst index a9649defb..56b1a0362 100644 --- a/docs/tutorials/implement_new_games/index.rst +++ b/docs/tutorials/implement_new_games/index.rst @@ -124,6 +124,7 @@ an initialisation parameter for which move they start with:: ... "inspects_source": False, ... "manipulates_source": False, ... "manipulates_state": False, + ... "actions_size": 3, ... } ... ... def __init__(self, starting_move=S): @@ -153,6 +154,7 @@ an initialisation parameter for which move they start with:: ... "inspects_source": False, ... "manipulates_source": False, ... "manipulates_state": False, + ... "actions_size": 3, ... } ... ... def __init__(self, starting_move=S): @@ -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, +because if we tried to use these strategies on the IPD and they played 'scissors' (action 3), +the game wouldn't know what to do with that! + 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:: From d03791590f3f465cc27143b456c490d016f77b51 Mon Sep 17 00:00:00 2001 From: alexhroom Date: Wed, 3 May 2023 11:41:02 +0100 Subject: [PATCH 06/24] re-added the actual implementation (git shenanigans) --- axelrod/match.py | 4 ++++ axelrod/player.py | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/axelrod/match.py b/axelrod/match.py index 19c83abd9..f99833642 100644 --- a/axelrod/match.py +++ b/axelrod/match.py @@ -111,6 +111,10 @@ def players(self): def players(self, players): """Ensure that players are passed the match attributes""" newplayers = [] + # ensure players have the correct size action sets for the game + player[0].check_actions_size(self.game.A.shape[0]) + player[1].check_actions_size(self.game.B.shape[0]) + for player in players: player.set_match_attributes(**self.match_attributes) newplayers.append(player) diff --git a/axelrod/player.py b/axelrod/player.py index 87c08ddb8..9e72f4d5c 100644 --- a/axelrod/player.py +++ b/axelrod/player.py @@ -258,6 +258,28 @@ def reset(self): def update_history(self, play, coplay): self.history.append(play, coplay) + def check_actions_size(self, game_size: int): + """ + Compares the actions set size of the player to a given game size. + If the game is too small, throws an error; if the game is too big, + creates a warning that the player may not use certain actions. + + Parameters + ---------- + game_size: int + The size of the game to compare player action size with. + """ + + actions_size = self.classifier.get('actions_size', 2) + if actions_size > game_size: + raise IndexError("The action set of player {} is larger " + "than the number of possible actions " + "in the game.".format(self.name)) + if actions_size < game_size: + warnings.warn("The action set of player {} is smaller " + "than the size of the game; they may " + "not play certain actions.") + @property def history(self): return self._history From c1f6ab0c79cf09af36a253bd4bb7a9eebe0b2ca3 Mon Sep 17 00:00:00 2001 From: alexhroom Date: Wed, 3 May 2023 11:46:09 +0100 Subject: [PATCH 07/24] fixed typo --- axelrod/match.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/axelrod/match.py b/axelrod/match.py index f99833642..32de8b45b 100644 --- a/axelrod/match.py +++ b/axelrod/match.py @@ -112,8 +112,8 @@ def players(self, players): """Ensure that players are passed the match attributes""" newplayers = [] # ensure players have the correct size action sets for the game - player[0].check_actions_size(self.game.A.shape[0]) - player[1].check_actions_size(self.game.B.shape[0]) + players[0].check_actions_size(self.game.A.shape[0]) + players[1].check_actions_size(self.game.B.shape[0]) for player in players: player.set_match_attributes(**self.match_attributes) From cdb72d82e7aae2374244cd2aeea036078d7e94c0 Mon Sep 17 00:00:00 2001 From: Alex Room <69592136+alexhroom@users.noreply.github.com> Date: Fri, 5 May 2023 17:09:17 +0000 Subject: [PATCH 08/24] fixed docstring --- axelrod/mock_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/axelrod/mock_player.py b/axelrod/mock_player.py index d044002f3..d4216cd50 100644 --- a/axelrod/mock_player.py +++ b/axelrod/mock_player.py @@ -8,8 +8,8 @@ class MockPlayer(Player): - """Creates a mock player that plays a given sequence of actions. If - no actions are given, or it runs out of actions, + """Creates a mock player that cycles through a given + sequence of actions. If no actions are given, plays like Cooperator. Used for testing. Parameters From 0737227e7056dc1cf1f11f9a7b52a84c1cf851e5 Mon Sep 17 00:00:00 2001 From: alexhroom Date: Mon, 8 May 2023 13:35:19 +0100 Subject: [PATCH 09/24] changed size checking to a general assumptions model --- axelrod/classifier.py | 2 +- axelrod/game.py | 40 ++++++++++++++++++++++++++++++++---- axelrod/match.py | 8 +++++--- axelrod/player.py | 47 ++++++++++++++++++++++++++----------------- 4 files changed, 70 insertions(+), 27 deletions(-) diff --git a/axelrod/classifier.py b/axelrod/classifier.py index eeee73217..dd2131fa7 100644 --- a/axelrod/classifier.py +++ b/axelrod/classifier.py @@ -72,7 +72,7 @@ def classify_player(self, player: Type[Player]) -> T: manipulates_state = Classifier[Optional[bool]]( "manipulates_state", lambda _: None ) -actions_size = Classifier[int]("actions_size", lambda _: 2) +assumptions = Classifier[Optional[dict]]("assumptions", lambda _: {'actions_size': 2}) # Should list all known classifiers. all_classifiers = [ diff --git a/axelrod/game.py b/axelrod/game.py index 6b95bbbf3..226bdf237 100644 --- a/axelrod/game.py +++ b/axelrod/game.py @@ -20,7 +20,7 @@ class AsymmetricGame(object): """ # 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. @@ -30,6 +30,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: @@ -41,6 +44,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)) } @@ -75,6 +80,24 @@ 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. + """ + self._attributes = {**self._attributes, **attributes} + def __repr__(self) -> str: return "Axelrod game with matrices: {}".format((self.A, self.B)) @@ -97,7 +120,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 @@ -110,10 +133,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 + } + 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.""" @@ -132,4 +164,4 @@ def __eq__(self, other): return self.RPST() == other.RPST() -DefaultGame = Game() +DefaultGame = Game(3, 0, 5, 1, game_type='prisoners_dilemma') diff --git a/axelrod/match.py b/axelrod/match.py index 32de8b45b..90f312d26 100644 --- a/axelrod/match.py +++ b/axelrod/match.py @@ -111,9 +111,11 @@ def players(self): def players(self, players): """Ensure that players are passed the match attributes""" newplayers = [] - # ensure players have the correct size action sets for the game - players[0].check_actions_size(self.game.A.shape[0]) - players[1].check_actions_size(self.game.B.shape[0]) + # 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]}) + players[1].check_assumptions({**self.game.attributes, 'actions_size': self.game.B.shape[0]}) for player in players: player.set_match_attributes(**self.match_attributes) diff --git a/axelrod/player.py b/axelrod/player.py index 9e72f4d5c..b7e786aa9 100644 --- a/axelrod/player.py +++ b/axelrod/player.py @@ -258,27 +258,36 @@ def reset(self): def update_history(self, play, coplay): self.history.append(play, coplay) - def check_actions_size(self, game_size: int): + def check_assumptions(self, attributes, raise_error=True): """ - Compares the actions set size of the player to a given game size. - If the game is too small, throws an error; if the game is too big, - creates a warning that the player may not use certain actions. - - Parameters - ---------- - game_size: int - The size of the game to compare player action size with. + Compares the player assumptions to a dictionary of attributes. + Generates a warning or error if an assumption is not fulfilled. + + Parameters: + ----------- + attributes: dict + 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. """ - - actions_size = self.classifier.get('actions_size', 2) - if actions_size > game_size: - raise IndexError("The action set of player {} is larger " - "than the number of possible actions " - "in the game.".format(self.name)) - if actions_size < game_size: - warnings.warn("The action set of player {} is smaller " - "than the size of the game; they may " - "not play certain actions.") + + for key, value in self.classifier.get('assumptions', {}): + 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): From 76f93b6d3fdb7aa9b3169f58d61e4be70987875f Mon Sep 17 00:00:00 2001 From: alexhroom Date: Mon, 8 May 2023 13:47:50 +0100 Subject: [PATCH 10/24] fixed some bugs --- axelrod/classifier.py | 2 +- axelrod/game.py | 8 +++++++- axelrod/player.py | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/axelrod/classifier.py b/axelrod/classifier.py index dd2131fa7..2afc34b2d 100644 --- a/axelrod/classifier.py +++ b/axelrod/classifier.py @@ -83,7 +83,7 @@ def classify_player(self, player: Type[Player]) -> T: inspects_source, manipulates_source, manipulates_state, - actions_size, + assumptions, ] all_classifiers_map = {c.name: c.classify_player for c in all_classifiers} diff --git a/axelrod/game.py b/axelrod/game.py index 226bdf237..bc93899a2 100644 --- a/axelrod/game.py +++ b/axelrod/game.py @@ -17,6 +17,9 @@ 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 @@ -96,7 +99,10 @@ def attributes(self, attributes): attribute already exists, it will overwrite the previous value. """ - self._attributes = {**self._attributes, **attributes} + 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)) diff --git a/axelrod/player.py b/axelrod/player.py index b7e786aa9..b216833c0 100644 --- a/axelrod/player.py +++ b/axelrod/player.py @@ -272,7 +272,7 @@ def check_assumptions(self, attributes, raise_error=True): just generate a warning. """ - for key, value in self.classifier.get('assumptions', {}): + for key, value in self.classifier.get('assumptions', {}).items(): msg = None if key not in attributes.keys(): msg = ("Player {} assumes that " From 0cffa9b99b50ca45f80237dedcfa0fd1fc94de74 Mon Sep 17 00:00:00 2001 From: alexhroom Date: Mon, 8 May 2023 14:00:47 +0100 Subject: [PATCH 11/24] added some tests --- axelrod/tests/strategies/test_player.py | 22 ++++++++++++++++++++++ axelrod/tests/unit/test_match.py | 12 +++++------- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/axelrod/tests/strategies/test_player.py b/axelrod/tests/strategies/test_player.py index de23ffa9d..6ccb9e80c 100644 --- a/axelrod/tests/strategies/test_player.py +++ b/axelrod/tests/strategies/test_player.py @@ -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""" diff --git a/axelrod/tests/unit/test_match.py b/axelrod/tests/unit/test_match.py index 0d5486e30..6e5fe7a19 100644 --- a/axelrod/tests/unit/test_match.py +++ b/axelrod/tests/unit/test_match.py @@ -356,16 +356,14 @@ def test_sparklines(self): 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={'actions_size': n1}) - player2 = MockPlayer(classifier={'actions_size': n2}) + 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(IndexError): - match = axl.Match((player1, player2), game=game) - elif (n1 < game.A.shape[0] or n2 < game.B.shape[0]): - with self.assertWarns(UserWarning): + 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) From 44992be6b07424c6ff7602200079a8301df969e8 Mon Sep 17 00:00:00 2001 From: alexhroom Date: Mon, 8 May 2023 14:08:41 +0100 Subject: [PATCH 12/24] added user option for strictness of assumption checking --- axelrod/match.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/axelrod/match.py b/axelrod/match.py index 90f312d26..2f72ebad5 100644 --- a/axelrod/match.py +++ b/axelrod/match.py @@ -30,6 +30,7 @@ def __init__( match_attributes=None, reset=True, seed=None, + strict_player_compatibility=True ): """ Parameters @@ -54,6 +55,9 @@ def __init__( Whether to reset players or not seed : int Random seed for reproducibility + strict_player_compatibility: 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 = { @@ -89,6 +93,7 @@ def __init__( else: self.match_attributes = match_attributes + self.strict_player_compatibility = strict_player_compatibility self.players = list(players) self.reset = reset @@ -114,8 +119,10 @@ def players(self, players): # 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]}) - players[1].check_assumptions({**self.game.attributes, 'actions_size': self.game.B.shape[0]}) + players[0].check_assumptions({**self.game.attributes, 'actions_size': self.game.A.shape[0]}, + raise_error=strict_player_compatibility) + players[1].check_assumptions({**self.game.attributes, 'actions_size': self.game.B.shape[0]}, + raise_error=strict_player_compatibility) for player in players: player.set_match_attributes(**self.match_attributes) From a443887c67ee5d137f884fa657261894cff8c769 Mon Sep 17 00:00:00 2001 From: alexhroom Date: Mon, 8 May 2023 17:32:53 +0100 Subject: [PATCH 13/24] fixed tests --- axelrod/match.py | 10 +++++----- axelrod/tests/unit/test_match.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/axelrod/match.py b/axelrod/match.py index 2f72ebad5..76bc2027c 100644 --- a/axelrod/match.py +++ b/axelrod/match.py @@ -30,7 +30,7 @@ def __init__( match_attributes=None, reset=True, seed=None, - strict_player_compatibility=True + strict_player_checking=True ): """ Parameters @@ -55,7 +55,7 @@ def __init__( Whether to reset players or not seed : int Random seed for reproducibility - strict_player_compatibility: bool, default True + 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. """ @@ -93,7 +93,7 @@ def __init__( else: self.match_attributes = match_attributes - self.strict_player_compatibility = strict_player_compatibility + self.strict_player_checking = strict_player_checking self.players = list(players) self.reset = reset @@ -120,9 +120,9 @@ def players(self, players): # 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=strict_player_compatibility) + raise_error=self.strict_player_checking) players[1].check_assumptions({**self.game.attributes, 'actions_size': self.game.B.shape[0]}, - raise_error=strict_player_compatibility) + raise_error=self.strict_player_checking) for player in players: player.set_match_attributes(**self.match_attributes) diff --git a/axelrod/tests/unit/test_match.py b/axelrod/tests/unit/test_match.py index 6e5fe7a19..0ece38717 100644 --- a/axelrod/tests/unit/test_match.py +++ b/axelrod/tests/unit/test_match.py @@ -6,7 +6,7 @@ from axelrod.mock_player import MockPlayer from axelrod.random_ import RandomGenerator from axelrod.tests.property import games, asymmetric_games -from hypothesis import example, given +from hypothesis import example, given, settings from hypothesis.strategies import floats, integers C, D = axl.Action.C, axl.Action.D From 89c438c4df4a2522b0edf6675d58e22357d75f1a Mon Sep 17 00:00:00 2001 From: alexhroom Date: Mon, 8 May 2023 17:37:04 +0100 Subject: [PATCH 14/24] updated docs --- docs/tutorials/implement_new_games/index.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/tutorials/implement_new_games/index.rst b/docs/tutorials/implement_new_games/index.rst index 56b1a0362..a69c1a7be 100644 --- a/docs/tutorials/implement_new_games/index.rst +++ b/docs/tutorials/implement_new_games/index.rst @@ -124,7 +124,7 @@ an initialisation parameter for which move they start with:: ... "inspects_source": False, ... "manipulates_source": False, ... "manipulates_state": False, - ... "actions_size": 3, + ... "assumptions": {"actions_size": 3}, ... } ... ... def __init__(self, starting_move=S): @@ -154,7 +154,7 @@ an initialisation parameter for which move they start with:: ... "inspects_source": False, ... "manipulates_source": False, ... "manipulates_state": False, - ... "actions_size": 3, + ... "assumptions": {"actions_size": 3}, ... } ... ... def __init__(self, starting_move=S): @@ -169,8 +169,8 @@ an initialisation parameter for which move they start with:: 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, -because if we tried to use these strategies on the IPD and they played 'scissors' (action 3), -the game wouldn't know what to do with that! +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:: From 8b4aa6caedf3cb6458bb3afc93033395d2be0cda Mon Sep 17 00:00:00 2001 From: alexhroom Date: Thu, 11 May 2023 09:21:34 +0100 Subject: [PATCH 15/24] added assumptions_satisfy helper method --- axelrod/player.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/axelrod/player.py b/axelrod/player.py index b216833c0..0586d8d1e 100644 --- a/axelrod/player.py +++ b/axelrod/player.py @@ -258,7 +258,7 @@ def reset(self): def update_history(self, play, coplay): self.history.append(play, coplay) - def check_assumptions(self, attributes, raise_error=True): + def check_assumptions(self, attributes: dict, raise_error: bool=True): """ Compares the player assumptions to a dictionary of attributes. Generates a warning or error if an assumption is not fulfilled. @@ -289,6 +289,33 @@ def check_assumptions(self, attributes, raise_error=True): raise RuntimeError(msg) warnings.warn(msg + " The strategy may not behave as expected.") + def assumptions_satisfy(self, attributes: dict) -> bool: + """ + Compares the player assumptions to a dictionary of attributes. + Returns True if the player assumptions are all satisfied by + these attributes, and False otherwise. + + Parameters: + ----------- + attributes: dict + The dictionary of attributes to compare the player's assumptions to. + + Returns + ------- + bool + A boolean of whether or not the attributes satisfy the player's + assumptions. + """ + + # we use check_assumptions as our 'base' rather than the other way + # around as check_assumptions needs finer grained understanding of + # the assumptions to produce useful error messages + try: + self.check_assumptions(attributes, raise_error=True) + except RuntimeError: + return False + return True + @property def history(self): return self._history From c4359bcb767a15a2da4879ba40be4dff05dbb457 Mon Sep 17 00:00:00 2001 From: alexhroom Date: Thu, 11 May 2023 15:52:20 +0100 Subject: [PATCH 16/24] removed unnecessary if statement --- axelrod/mock_player.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/axelrod/mock_player.py b/axelrod/mock_player.py index d4216cd50..1be274c82 100644 --- a/axelrod/mock_player.py +++ b/axelrod/mock_player.py @@ -26,8 +26,7 @@ def __init__(self, actions: List[Action] = None, classifier: dict = None) -> Non super().__init__() if not actions: actions = [] - if classifier: - self.classifier = classifier + self.classifier = classifier self.actions = cycle(actions) def strategy(self, opponent: Player) -> Action: From 46cbf0281a560ea0bfa6e5d9cb905ab56ec42fc6 Mon Sep 17 00:00:00 2001 From: alexhroom Date: Thu, 11 May 2023 19:21:57 +0100 Subject: [PATCH 17/24] Revert "removed unnecessary if statement" This reverts commit c4359bcb767a15a2da4879ba40be4dff05dbb457. --- axelrod/mock_player.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/axelrod/mock_player.py b/axelrod/mock_player.py index 1be274c82..d4216cd50 100644 --- a/axelrod/mock_player.py +++ b/axelrod/mock_player.py @@ -26,7 +26,8 @@ def __init__(self, actions: List[Action] = None, classifier: dict = None) -> Non super().__init__() if not actions: actions = [] - self.classifier = classifier + if classifier: + self.classifier = classifier self.actions = cycle(actions) def strategy(self, opponent: Player) -> Action: From f705ffdcf1c47c91847fcbbd0ecec043c40d0978 Mon Sep 17 00:00:00 2001 From: alexhroom Date: Fri, 12 May 2023 18:30:31 +0100 Subject: [PATCH 18/24] added test for assumptions_satisfy --- axelrod/tests/strategies/test_player.py | 26 +++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/axelrod/tests/strategies/test_player.py b/axelrod/tests/strategies/test_player.py index 6ccb9e80c..aed2b1e03 100644 --- a/axelrod/tests/strategies/test_player.py +++ b/axelrod/tests/strategies/test_player.py @@ -355,18 +355,32 @@ def test_assumption_checking(self): 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': []}) + player.check_assumptions({'foo': True, 'bar': 3}) # correct characteristics + player.check_assumptions({'foo': True, 'bar': 3, 'baz': []}) # extraneous characteristic with self.assertRaises(RuntimeError): - player.check_assumptions({'foo': True}) + player.check_assumptions({'foo': True}) # missing characteristic with self.assertRaises(RuntimeError): - player.check_assumptions({'foo': True, 'bar': 5}) + player.check_assumptions({'foo': True, 'bar': 5}) # invalid charateristic value with self.assertWarns(UserWarning): - player.check_assumptions({'foo': True}, raise_error=False) + player.check_assumptions({'foo': True}, raise_error=False) # missing characteristic with self.assertWarns(UserWarning): - player.check_assumptions({'foo': True, 'bar': 5}, raise_error=False) + player.check_assumptions({'foo': True, 'bar': 5}, raise_error=False) # invalid charateristic value + + def test_assumptions_satisfy(self): + """ + Tests that the assumptions_satisfy() method works as intended. + It is a wrapper around check_assumptions() so the actual assumption + testing logic is checked more thoroughly there. + """ + player = axl.MockPlayer(classifier={'assumptions': {'foo': True, 'bar': 3}}) + + self.assertEqual(player.assumptions_satisfy({'foo': True, 'bar': 3}), True) # correct characteristics + self.assertEqual(player.assumptions_satisfy({'foo': True, 'bar': 3, 'baz': []}), True) # extraneous characteristic + self.assertEqual(player.assumptions_satisfy({'foo': True}), False) # missing characteristic + self.assertEqual(player.assumptions_satisfy({'foo': True, 'bar': 5}), False) # invalid charateristic value + From 58a7218ed533bf2b06fb0fb7e540e87cc9a53c98 Mon Sep 17 00:00:00 2001 From: alexhroom Date: Fri, 12 May 2023 18:30:45 +0100 Subject: [PATCH 19/24] changed attributes -> characteristics --- axelrod/player.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/axelrod/player.py b/axelrod/player.py index 0586d8d1e..4bb01f720 100644 --- a/axelrod/player.py +++ b/axelrod/player.py @@ -258,15 +258,15 @@ def reset(self): def update_history(self, play, coplay): self.history.append(play, coplay) - def check_assumptions(self, attributes: dict, raise_error: bool=True): + def check_assumptions(self, characteristics: dict, raise_error: bool=True): """ - Compares the player assumptions to a dictionary of attributes. + Compares the player assumptions to a dictionary of game characteristics. Generates a warning or error if an assumption is not fulfilled. Parameters: ----------- - attributes: dict - The dictionary of attributes to compare the player's assumptions to. + characteristics: dict + The dictionary of game characteristics 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. @@ -274,36 +274,36 @@ def check_assumptions(self, attributes: dict, raise_error: bool=True): for key, value in self.classifier.get('assumptions', {}).items(): msg = None - if key not in attributes.keys(): + if key not in characteristics.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]: + elif value != characteristics[key]: msg = ("Player {} assumes that the game attribute " "{} is set to {}, but it is actually set to {}." - "".format(self.name, key, value, attributes[key])) + "".format(self.name, key, value, characteristics[key])) if msg is not None: if raise_error: raise RuntimeError(msg) warnings.warn(msg + " The strategy may not behave as expected.") - def assumptions_satisfy(self, attributes: dict) -> bool: + def assumptions_satisfy(self, characteristics: dict) -> bool: """ - Compares the player assumptions to a dictionary of attributes. + Compares the player assumptions to a dictionary of game characteristics. Returns True if the player assumptions are all satisfied by - these attributes, and False otherwise. + these characteristics, and False otherwise. Parameters: ----------- - attributes: dict - The dictionary of attributes to compare the player's assumptions to. + characteristics: dict + The dictionary of game characteristics to compare the player's assumptions to. Returns ------- bool - A boolean of whether or not the attributes satisfy the player's + A boolean of whether or not the game characteristics satisfy the player's assumptions. """ @@ -311,7 +311,7 @@ def assumptions_satisfy(self, attributes: dict) -> bool: # around as check_assumptions needs finer grained understanding of # the assumptions to produce useful error messages try: - self.check_assumptions(attributes, raise_error=True) + self.check_assumptions(characteristics, raise_error=True) except RuntimeError: return False return True From 24eccc5046b80ea569b291972aa943a50b72784b Mon Sep 17 00:00:00 2001 From: alexhroom Date: Fri, 12 May 2023 18:40:02 +0100 Subject: [PATCH 20/24] more attributes -> characteristics --- axelrod/game.py | 40 ++++++++++++++++++++-------------------- axelrod/match.py | 6 +++--- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/axelrod/game.py b/axelrod/game.py index bc93899a2..c41f771de 100644 --- a/axelrod/game.py +++ b/axelrod/game.py @@ -23,7 +23,7 @@ class AsymmetricGame(object): """ # pylint: disable=invalid-name - def __init__(self, A: np.array, B: np.array, **attributes) -> None: + def __init__(self, A: np.array, B: np.array, **characteristics) -> None: """ Creates an asymmetric game from two matrices. @@ -33,8 +33,8 @@ def __init__(self, A: np.array, B: np.array, **attributes) -> None: the payoff matrix for player A. B: np.array the payoff matrix for player B. - **attributes - optional attributes for the game. Used + **characteristics + optional characteristics detailing features of the game. Used to ensure strategies used with the game are valid. """ @@ -47,7 +47,7 @@ def __init__(self, A: np.array, B: np.array, **attributes) -> None: self.A = A self.B = B - self.attributes = attributes + self.characteristics = characteristics self.scores = { pair: self.score(pair) for pair in ((C, C), (D, D), (C, D), (D, C)) @@ -84,25 +84,25 @@ def get_value(x): return (self.A[row][col], self.B[row][col]) @property - def attributes(self): - return self._attributes + def characteristics(self): + return self._characteristics - @attributes.setter - def attributes(self, attributes): + @characteristics.setter + def characteristics(self, characteristics): """ - Adds or changes game attributes. + Adds or changes game characteristics. Parameters ---------- - attributes: dict - Attributes to add to the game. If the added - attribute already exists, it will overwrite the + characteristics: dict + characteristics to add to the game. If the added + characteristic already exists, it will overwrite the previous value. """ try: - self._attributes = {**self._attributes, **attributes} + self._characteristics = {**self._characteristics, **characteristics} except AttributeError: - self._attributes = attributes + self._characteristics = characteristics def __repr__(self) -> str: return "Axelrod game with matrices: {}".format((self.A, self.B)) @@ -126,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, **attributes) -> None: + def __init__(self, r: Score = 3, s: Score = 0, t: Score = 5, p: Score = 1, **characteristics) -> None: """Create a new game object. Parameters @@ -146,12 +146,12 @@ def __init__(self, r: Score = 3, s: Score = 0, t: Score = 5, p: Score = 1, **att """ A = np.array([[r, s], [t, p]]) - default_attributes = { - 'has_RPST': True + default_characteristics = { + 'has_RPST': hasattr(self, 'RPST') } - attributes = {**default_attributes, **attributes} + characteristics = {**default_characteristics, **characteristics} - super().__init__(A, A.transpose(), **attributes) + super().__init__(A, A.transpose(), **characteristics) def RPST(self) -> Tuple[Score, Score, Score, Score]: """Returns game matrix values in Press and Dyson notation.""" @@ -170,4 +170,4 @@ def __eq__(self, other): return self.RPST() == other.RPST() -DefaultGame = Game(3, 0, 5, 1, game_type='prisoners_dilemma') +DefaultGame = Game(3, 0, 5, 1, game_type='prisoners_dilemma') \ No newline at end of file diff --git a/axelrod/match.py b/axelrod/match.py index 76bc2027c..a94a7cbb0 100644 --- a/axelrod/match.py +++ b/axelrod/match.py @@ -117,11 +117,11 @@ 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 + # note the game size characteristic 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]}, + players[0].check_assumptions({**self.game.characteristics, '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]}, + players[1].check_assumptions({**self.game.characteristics, 'actions_size': self.game.B.shape[0]}, raise_error=self.strict_player_checking) for player in players: From 88d65d4070b9b161604a4316e533ea6423a1cb8b Mon Sep 17 00:00:00 2001 From: alexhroom Date: Tue, 16 May 2023 09:56:56 +0100 Subject: [PATCH 21/24] changed name to game_characteristics when not directly referencing a Game --- axelrod/player.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/axelrod/player.py b/axelrod/player.py index 4bb01f720..6e2a68d72 100644 --- a/axelrod/player.py +++ b/axelrod/player.py @@ -258,14 +258,14 @@ def reset(self): def update_history(self, play, coplay): self.history.append(play, coplay) - def check_assumptions(self, characteristics: dict, raise_error: bool=True): + def check_assumptions(self, game_characteristics: dict, raise_error: bool=True): """ Compares the player assumptions to a dictionary of game characteristics. Generates a warning or error if an assumption is not fulfilled. Parameters: ----------- - characteristics: dict + game_characteristics: dict The dictionary of game characteristics to compare the player's assumptions to. raise_error: bool, default True If True, raises an error if the assumption is violated. Else, @@ -274,22 +274,22 @@ def check_assumptions(self, characteristics: dict, raise_error: bool=True): for key, value in self.classifier.get('assumptions', {}).items(): msg = None - if key not in characteristics.keys(): + if key not in game_characteristics.keys(): msg = ("Player {} assumes that " "the game has the attribute {}, " "but the game does not declare this attribute." "".format(self.name, key)) - elif value != characteristics[key]: + elif value != game_characteristics[key]: msg = ("Player {} assumes that the game attribute " "{} is set to {}, but it is actually set to {}." - "".format(self.name, key, value, characteristics[key])) + "".format(self.name, key, value, game_characteristics[key])) if msg is not None: if raise_error: raise RuntimeError(msg) warnings.warn(msg + " The strategy may not behave as expected.") - def assumptions_satisfy(self, characteristics: dict) -> bool: + def assumptions_satisfy(self, game_characteristics: dict) -> bool: """ Compares the player assumptions to a dictionary of game characteristics. Returns True if the player assumptions are all satisfied by @@ -297,7 +297,7 @@ def assumptions_satisfy(self, characteristics: dict) -> bool: Parameters: ----------- - characteristics: dict + game_characteristics: dict The dictionary of game characteristics to compare the player's assumptions to. Returns @@ -311,7 +311,7 @@ def assumptions_satisfy(self, characteristics: dict) -> bool: # around as check_assumptions needs finer grained understanding of # the assumptions to produce useful error messages try: - self.check_assumptions(characteristics, raise_error=True) + self.check_assumptions(game_characteristics, raise_error=True) except RuntimeError: return False return True From 16048c1da2a3e124c99dff5be7e471cdaa2bfbb8 Mon Sep 17 00:00:00 2001 From: Alex Room <69592136+alexhroom@users.noreply.github.com> Date: Wed, 17 May 2023 10:23:09 +0000 Subject: [PATCH 22/24] Adds a read the docs config file (#1423) * added a read the docs config file * moved yaml to root directory --- .readthedocs.yaml | 13 +++++++++++++ docs/requirements.txt | 1 + 2 files changed, 14 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 000000000..f334b70c2 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,13 @@ +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +sphinx: + configuration: docs/conf.py + +python: + install: + - requirements: docs/requirements.txt diff --git a/docs/requirements.txt b/docs/requirements.txt index f49ed6d8a..e876846d0 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1 +1,2 @@ docutils <= 0.17 # Added this for a problem with sphinx https://github.com/sphinx-doc/sphinx/commit/13803a79e7179f40a27f46d5a5a05f1eebbcbb63 +numpy==1.24.3 # numpy isn't mocked due to complex use in doctests From d59cab9246361c22a57903c06d5ff0c581719e5c Mon Sep 17 00:00:00 2001 From: alexhroom Date: Mon, 22 May 2023 09:11:16 +0100 Subject: [PATCH 23/24] removed default characteristic --- axelrod/game.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/axelrod/game.py b/axelrod/game.py index c41f771de..726422523 100644 --- a/axelrod/game.py +++ b/axelrod/game.py @@ -146,11 +146,6 @@ def __init__(self, r: Score = 3, s: Score = 0, t: Score = 5, p: Score = 1, **cha """ A = np.array([[r, s], [t, p]]) - default_characteristics = { - 'has_RPST': hasattr(self, 'RPST') - } - characteristics = {**default_characteristics, **characteristics} - super().__init__(A, A.transpose(), **characteristics) def RPST(self) -> Tuple[Score, Score, Score, Score]: From 22b61cf16d9214479b2d34215dd16baf204a9001 Mon Sep 17 00:00:00 2001 From: alexhroom Date: Mon, 22 May 2023 09:26:42 +0100 Subject: [PATCH 24/24] made mock player safer --- axelrod/mock_player.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/axelrod/mock_player.py b/axelrod/mock_player.py index d4216cd50..3757bdfea 100644 --- a/axelrod/mock_player.py +++ b/axelrod/mock_player.py @@ -26,10 +26,13 @@ def __init__(self, actions: List[Action] = None, classifier: dict = None) -> Non super().__init__() if not actions: actions = [] - if classifier: - self.classifier = classifier self.actions = cycle(actions) + if not classifier: + self.classifier = {} + else: + self.classifier = classifier + def strategy(self, opponent: Player) -> Action: # Return the next saved action, if present. try: