From 8de2ba711db7b18a56d5be5bbb3b4a81d0949784 Mon Sep 17 00:00:00 2001 From: alexhroom Date: Thu, 16 Mar 2023 17:08:50 +0000 Subject: [PATCH 01/24] Added asymmetric games and made regular games a subclass --- axelrod/game.py | 110 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 85 insertions(+), 25 deletions(-) diff --git a/axelrod/game.py b/axelrod/game.py index 180a9f110..3fd282736 100644 --- a/axelrod/game.py +++ b/axelrod/game.py @@ -1,5 +1,7 @@ from typing import Tuple, Union +import numpy as np + from axelrod import Action C, D = Action.C, Action.D @@ -7,7 +9,7 @@ Score = Union[int, float] -class Game(object): +class AsymmetricGame(object): """Container for the game matrix and scoring logic. Attributes @@ -16,9 +18,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. + """ + + self.scores = { + (C, C): (A[0][0], B[0][0]), + (D, D): (A[1][1], B[1][1]), + (C, D): (A[0][1], B[0][1]), + (D, C): (A[1][0], B[1][0]), + } + + self.A = A + self.B = B + + 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. + """ + # handle being passed Actions, or a mix of Actions and ints + actions_to_ints = {C: 0, D: 1} + + def convert_action(x): + if isinstance(x, Action): + return actions_to_ints[x] + return x + + r, c = map(convert_action, pair) + + # the '.item()' method converts the values from Numpy datatypes + # to native Python ones for compatibility + return (self.A[r][c].item(), self.B[r][c].item()) + + def __repr__(self) -> str: + return "Axelrod game with matrices = {}".format((self.A, self.B)) + + def __eq__(self, other): + if not isinstance(other, Game): + 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 @@ -32,12 +110,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.""" @@ -47,21 +122,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()) From f1207b0a1f6fa1c4488fecec91ae03c4c3f0c6b7 Mon Sep 17 00:00:00 2001 From: alexhroom Date: Sun, 19 Mar 2023 13:59:52 +0000 Subject: [PATCH 02/24] small improvements to code style --- axelrod/__init__.py | 2 +- axelrod/game.py | 37 ++++++++++++++++++++++++------------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/axelrod/__init__.py b/axelrod/__init__.py index 573af1bb9..dc2eefa81 100644 --- a/axelrod/__init__.py +++ b/axelrod/__init__.py @@ -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 diff --git a/axelrod/game.py b/axelrod/game.py index 3fd282736..e78a4978c 100644 --- a/axelrod/game.py +++ b/axelrod/game.py @@ -31,16 +31,13 @@ def __init__(self, A: np.array, B: np.array) -> None: the payoff matrix for player B. """ - self.scores = { - (C, C): (A[0][0], B[0][0]), - (D, D): (A[1][1], B[1][1]), - (C, D): (A[0][1], B[0][1]), - (D, C): (A[1][0], B[1][0]), - } - self.A = A 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]: @@ -60,19 +57,33 @@ def score( tuple of int or float Scores for two player resulting from their actions. """ - # handle being passed Actions, or a mix of Actions and ints - actions_to_ints = {C: 0, D: 1} - def convert_action(x): + def _convert_action(x: Union[Action, int]) -> int: + """ + A helper function to handle Actions as Ints. + + Parameters + ---------- + x: Action or Int + The potential Action or Hint to handle. + + Returns + ------- + int + If given an action, returns the action as an integer (C = 0, D = 1), + otherwise just returns the integer. + """ if isinstance(x, Action): - return actions_to_ints[x] + return {C: 0, D: 1}[x] return x - r, c = map(convert_action, pair) + # handle being potentially passed actions or ints, and unpack + # the decision pair into the row and column players' respectively + row, col = map(_convert_action, pair) # the '.item()' method converts the values from Numpy datatypes # to native Python ones for compatibility - return (self.A[r][c].item(), self.B[r][c].item()) + return (self.A[row][col].item(), self.B[row][col].item()) def __repr__(self) -> str: return "Axelrod game with matrices = {}".format((self.A, self.B)) From 09fb25104efd0d90efbc9752a068d638886a7161 Mon Sep 17 00:00:00 2001 From: alexhroom Date: Sun, 19 Mar 2023 14:06:11 +0000 Subject: [PATCH 03/24] fixing docs mock... --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 7c2df0fa9..325bbf51d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -43,7 +43,7 @@ ] for mod_name in MOCK_MODULES: - sys.modules[mod_name] = mock.Mock() + sys.modules[mod_name] = mock.MagicMock() # Adds absolute path to axelrod module sys.path.insert(0, os.path.abspath("../")) # Adding path to module From ea4c6607ede863281e7bf3a6f9a7c336a90f33ba Mon Sep 17 00:00:00 2001 From: alexhroom Date: Sun, 19 Mar 2023 14:10:00 +0000 Subject: [PATCH 04/24] Revert "fixing docs mock..." This reverts commit 09fb25104efd0d90efbc9752a068d638886a7161. --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 325bbf51d..7c2df0fa9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -43,7 +43,7 @@ ] for mod_name in MOCK_MODULES: - sys.modules[mod_name] = mock.MagicMock() + sys.modules[mod_name] = mock.Mock() # Adds absolute path to axelrod module sys.path.insert(0, os.path.abspath("../")) # Adding path to module From c593dd486489ad4e1cc91930b7137e07b2d50a99 Mon Sep 17 00:00:00 2001 From: alexhroom Date: Mon, 20 Mar 2023 13:35:14 +0000 Subject: [PATCH 05/24] used IntEnum to simplify --- axelrod/action.py | 4 ++-- axelrod/game.py | 25 ++----------------------- 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/axelrod/action.py b/axelrod/action.py index a8ba85445..455c94915 100644 --- a/axelrod/action.py +++ b/axelrod/action.py @@ -6,7 +6,7 @@ C, D = Action.C, Action.D """ -from enum import Enum +from enum import IntEnum from functools import total_ordering from typing import Iterable, Tuple @@ -19,7 +19,7 @@ def __init__(self, *args): @total_ordering -class Action(Enum): +class Action(IntEnum): """Core actions in the Prisoner's Dilemma. There are only two possible actions, namely Cooperate or Defect, diff --git a/axelrod/game.py b/axelrod/game.py index e78a4978c..2a301d4be 100644 --- a/axelrod/game.py +++ b/axelrod/game.py @@ -58,31 +58,10 @@ def score( Scores for two player resulting from their actions. """ - def _convert_action(x: Union[Action, int]) -> int: - """ - A helper function to handle Actions as Ints. - - Parameters - ---------- - x: Action or Int - The potential Action or Hint to handle. - - Returns - ------- - int - If given an action, returns the action as an integer (C = 0, D = 1), - otherwise just returns the integer. - """ - if isinstance(x, Action): - return {C: 0, D: 1}[x] - return x - - # handle being potentially passed actions or ints, and unpack - # the decision pair into the row and column players' respectively - row, col = map(_convert_action, pair) - # the '.item()' method converts the values from Numpy datatypes # to native Python ones for compatibility + # actions can be interpreted as indices as they are + # an IntEnum class return (self.A[row][col].item(), self.B[row][col].item()) def __repr__(self) -> str: From 22e619700098200347d8d6177514b2dfb379ca9d Mon Sep 17 00:00:00 2001 From: alexhroom Date: Mon, 20 Mar 2023 15:17:49 +0000 Subject: [PATCH 06/24] small improvements & a fix --- axelrod/game.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/axelrod/game.py b/axelrod/game.py index 2a301d4be..aca2210f5 100644 --- a/axelrod/game.py +++ b/axelrod/game.py @@ -58,6 +58,8 @@ def score( Scores for two player resulting from their actions. """ + row, col = pair + # the '.item()' method converts the values from Numpy datatypes # to native Python ones for compatibility # actions can be interpreted as indices as they are @@ -65,7 +67,7 @@ def score( return (self.A[row][col].item(), self.B[row][col].item()) def __repr__(self) -> str: - return "Axelrod game with matrices = {}".format((self.A, self.B)) + return "Axelrod game with matrices: {}".format((self.A, self.B)) def __eq__(self, other): if not isinstance(other, Game): From 95b95179e68699a52d0618187f48537ffb684e62 Mon Sep 17 00:00:00 2001 From: alexhroom Date: Mon, 20 Mar 2023 15:18:07 +0000 Subject: [PATCH 07/24] added asymmetric games to docs --- docs/how-to/use_different_stage_games.rst | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/how-to/use_different_stage_games.rst b/docs/how-to/use_different_stage_games.rst index c86b536ee..9bb48be6a 100644 --- a/docs/how-to/use_different_stage_games.rst +++ b/docs/how-to/use_different_stage_games.rst @@ -47,3 +47,25 @@ 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 + 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:: + + >>> A = np.array([[0, -1, 1], [1, 0, -1], [-1, 1, 0]]) + >>> rock_paper_scissors = axl.AsymmetricGame(A, -A) + >>> rock_paper_scissors + Axelrod game with matrices: (array([[ 0, -1, 1], + [ 1, 0, -1], + [-1, 1, 0]]), + array([[ 0, 1, -1], + [-1, 0, 1], + [ 1, -1, 0]])) \ No newline at end of file From 3a8cde29cae964e12208c031815e7e58662c2828 Mon Sep 17 00:00:00 2001 From: alexhroom Date: Thu, 23 Mar 2023 08:39:01 +0000 Subject: [PATCH 08/24] added werror if invalid payoff matrices are given --- axelrod/game.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/axelrod/game.py b/axelrod/game.py index aca2210f5..f995f7230 100644 --- a/axelrod/game.py +++ b/axelrod/game.py @@ -31,6 +31,12 @@ def __init__(self, A: np.array, B: np.array) -> None: 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 self.B = B From c8439bc729aca046a92a9f73d7cb1b29309cb0fb Mon Sep 17 00:00:00 2001 From: alexhroom Date: Mon, 27 Mar 2023 11:40:22 +0100 Subject: [PATCH 09/24] removed .item() --- axelrod/game.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/axelrod/game.py b/axelrod/game.py index f995f7230..fca6fb2d1 100644 --- a/axelrod/game.py +++ b/axelrod/game.py @@ -66,11 +66,9 @@ def score( row, col = pair - # the '.item()' method converts the values from Numpy datatypes - # to native Python ones for compatibility # actions can be interpreted as indices as they are # an IntEnum class - return (self.A[row][col].item(), self.B[row][col].item()) + return (self.A[row][col], self.B[row][col]) def __repr__(self) -> str: return "Axelrod game with matrices: {}".format((self.A, self.B)) From bb6171ca0d24412a07dcf6e3746cc2f312901ade Mon Sep 17 00:00:00 2001 From: alexhroom Date: Mon, 27 Mar 2023 11:45:37 +0100 Subject: [PATCH 10/24] changed dbs.action_to_int to use IntEnum behaviour --- axelrod/strategies/dbs.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/axelrod/strategies/dbs.py b/axelrod/strategies/dbs.py index 720c7fdb2..4b88d0259 100644 --- a/axelrod/strategies/dbs.py +++ b/axelrod/strategies/dbs.py @@ -229,7 +229,7 @@ def strategy(self, opponent: Player) -> Action: # rule defined in the should_promote function). if r_plus[0] not in self.Rc.keys(): if self.should_promote(r_plus, self.promotion_threshold): - self.Rc[r_plus[0]] = action_to_int(r_plus[1]) + self.Rc[r_plus[0]] = int(r_plus[1]) self.violation_counts[r_plus[0]] = 0 self.violation_counts[r_plus[0]] = 0 @@ -257,11 +257,11 @@ def strategy(self, opponent: Player) -> Action: # r+ in Rc. r_plus_in_Rc = r_plus[0] in self.Rc.keys() and self.Rc[ r_plus[0] - ] == action_to_int(r_plus[1]) + ] == int(r_plus[1]) # r- in Rd r_minus_in_Rd = r_minus[0] in self.Rd.keys() and self.Rd[ r_minus[0] - ] == action_to_int(r_minus[1]) + ] == int(r_minus[1]) # Increment number of violations of Rd rules. if r_minus_in_Rd: @@ -394,12 +394,6 @@ def create_policy(pCC, pCD, pDC, pDD): return {(C, C): pCC, (C, D): pCD, (D, C): pDC, (D, D): pDD} -def action_to_int(action): - if action == C: - return 1 - return 0 - - def minimax_tree_search(begin_node, policy, max_depth): """ Tree search function (minimax search procedure) for the tree (built by From 9cb0f8ca445c86e073f85d051d13fb7740995590 Mon Sep 17 00:00:00 2001 From: alexhroom Date: Mon, 27 Mar 2023 12:07:31 +0100 Subject: [PATCH 11/24] Revert "changed dbs.action_to_int to use IntEnum behaviour" This reverts commit bb6171ca0d24412a07dcf6e3746cc2f312901ade. --- axelrod/strategies/dbs.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/axelrod/strategies/dbs.py b/axelrod/strategies/dbs.py index 4b88d0259..720c7fdb2 100644 --- a/axelrod/strategies/dbs.py +++ b/axelrod/strategies/dbs.py @@ -229,7 +229,7 @@ def strategy(self, opponent: Player) -> Action: # rule defined in the should_promote function). if r_plus[0] not in self.Rc.keys(): if self.should_promote(r_plus, self.promotion_threshold): - self.Rc[r_plus[0]] = int(r_plus[1]) + self.Rc[r_plus[0]] = action_to_int(r_plus[1]) self.violation_counts[r_plus[0]] = 0 self.violation_counts[r_plus[0]] = 0 @@ -257,11 +257,11 @@ def strategy(self, opponent: Player) -> Action: # r+ in Rc. r_plus_in_Rc = r_plus[0] in self.Rc.keys() and self.Rc[ r_plus[0] - ] == int(r_plus[1]) + ] == action_to_int(r_plus[1]) # r- in Rd r_minus_in_Rd = r_minus[0] in self.Rd.keys() and self.Rd[ r_minus[0] - ] == int(r_minus[1]) + ] == action_to_int(r_minus[1]) # Increment number of violations of Rd rules. if r_minus_in_Rd: @@ -394,6 +394,12 @@ def create_policy(pCC, pCD, pDC, pDD): return {(C, C): pCC, (C, D): pCD, (D, C): pDC, (D, D): pDD} +def action_to_int(action): + if action == C: + return 1 + return 0 + + def minimax_tree_search(begin_node, policy, max_depth): """ Tree search function (minimax search procedure) for the tree (built by From d88c4d3280a49a1c918c76a5da71e3e4ddf9e896 Mon Sep 17 00:00:00 2001 From: alexhroom Date: Sun, 9 Apr 2023 10:56:29 +0100 Subject: [PATCH 12/24] made library code more robust wrt integer actions --- axelrod/action.py | 4 ++-- axelrod/history.py | 11 ++++++++--- axelrod/strategies/cycler.py | 2 +- axelrod/strategies/finite_state_machines.py | 4 ++-- axelrod/strategy_transformers.py | 5 ++++- 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/axelrod/action.py b/axelrod/action.py index 455c94915..ade4dc367 100644 --- a/axelrod/action.py +++ b/axelrod/action.py @@ -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": return cls.C - if character == "D": + if character == "D" or character == "1": return cls.D raise UnknownActionError('Character must be "C" or "D".') diff --git a/axelrod/history.py b/axelrod/history.py index aea7b1f64..36fa374ac 100644 --- a/axelrod/history.py +++ b/axelrod/history.py @@ -33,6 +33,8 @@ 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 + play, coplay = Action(play), Action(coplay) self._plays.append(play) self._actions[play] += 1 self._coplays.append(coplay) @@ -44,6 +46,8 @@ def copy(self): def flip_plays(self): """Creates a flipped plays history for use with DualTransformer.""" + # sometimes the 'action' under a transformer is cast to an integer; + # the Action(...) call ensures it is cast back so it can be flipped flipped_plays = [action.flip() for action in self._plays] return self.__class__(plays=flipped_plays, coplays=self._coplays) @@ -130,12 +134,13 @@ 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 + play, coplay = Action(play), Action(coplay) 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 diff --git a/axelrod/strategies/cycler.py b/axelrod/strategies/cycler.py index ac2be686a..87483af31 100644 --- a/axelrod/strategies/cycler.py +++ b/axelrod/strategies/cycler.py @@ -141,7 +141,7 @@ def _generate_random_cycle(self, cycle_length: int) -> str: Generate a sequence of random moves """ return actions_to_str( - self._random.choice(actions) for _ in range(cycle_length) + Action(self._random.choice(actions)) for _ in range(cycle_length) ) def mutate(self) -> EvolvablePlayer: diff --git a/axelrod/strategies/finite_state_machines.py b/axelrod/strategies/finite_state_machines.py index a20e408db..86d5bf26a 100644 --- a/axelrod/strategies/finite_state_machines.py +++ b/axelrod/strategies/finite_state_machines.py @@ -33,7 +33,7 @@ def __init__(self, transitions: tuple, initial_state: int) -> None: """ self._state = initial_state self._state_transitions = { - (current_state, input_action): (next_state, output_action) + (current_state, Action(input_action)): (next_state, Action(output_action)) for current_state, input_action, next_state, output_action in transitions } # type: dict @@ -118,7 +118,7 @@ def __init__( ) -> None: Player.__init__(self) self.initial_state = initial_state - self.initial_action = initial_action + self.initial_action = Action(initial_action) self.fsm = SimpleFSM(transitions, initial_state) def strategy(self, opponent: Player) -> Action: diff --git a/axelrod/strategy_transformers.py b/axelrod/strategy_transformers.py index 70773ff60..29d736056 100644 --- a/axelrod/strategy_transformers.py +++ b/axelrod/strategy_transformers.py @@ -385,7 +385,10 @@ def dual_wrapper(player, opponent: Player, proposed_action: Action) -> Action: # is done in the strategy of the new PlayerClass created by DualTransformer. # The DualTransformer is dynamically created in StrategyTransformerFactory. - return proposed_action.flip() + # sometimes the action is an integer - this is a bug. the + # Action(...) call around proposed_action ensures it is an action. + + return Action(proposed_action).flip() DualTransformer = StrategyTransformerFactory(dual_wrapper, name_prefix="Dual") From eaa1603a6a0c33447f987fd95e472084732c7c9d Mon Sep 17 00:00:00 2001 From: alexhroom Date: Sun, 9 Apr 2023 11:22:58 +0100 Subject: [PATCH 13/24] all strategies now work with integer actions --- axelrod/strategies/lookerup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/axelrod/strategies/lookerup.py b/axelrod/strategies/lookerup.py index 20024cdb8..98bc9fb31 100644 --- a/axelrod/strategies/lookerup.py +++ b/axelrod/strategies/lookerup.py @@ -517,7 +517,7 @@ def random_params(self, plays, op_plays, op_start_plays): @classmethod def mutate_value(cls, value): - return value.flip() + return Action(value).flip() def mutate_table(self, table, mutation_probability): randoms = self._random.random(len(table.keys())) @@ -536,7 +536,7 @@ def mutate(self): for i in range(len(initial_actions)): r = self._random.random() if r < self.mutation_probability: - initial_actions[i] = initial_actions[i].flip() + initial_actions[i] = Action(initial_actions[i]).flip() return self.create_new( lookup_dict=lookup_dict, initial_actions=tuple(initial_actions), From 8f195613b5f62c9f79f7cdc0a52a89f451963c02 Mon Sep 17 00:00:00 2001 From: alexhroom Date: Sun, 9 Apr 2023 14:02:35 +0100 Subject: [PATCH 14/24] added tests and fixed __eq__ --- axelrod/game.py | 2 +- axelrod/tests/property.py | 14 ++++++++++++ axelrod/tests/unit/test_game.py | 40 ++++++++++++++++++++++++++++++++- 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/axelrod/game.py b/axelrod/game.py index fca6fb2d1..8e3bc8ee3 100644 --- a/axelrod/game.py +++ b/axelrod/game.py @@ -74,7 +74,7 @@ def __repr__(self) -> str: return "Axelrod game with matrices: {}".format((self.A, self.B)) def __eq__(self, other): - if not isinstance(other, Game): + if not isinstance(other, AsymmetricGame): return False return self.A.all() == other.A.all() and self.B.all() == other.B.all() diff --git a/axelrod/tests/property.py b/axelrod/tests/property.py index 282079a38..21648fac8 100644 --- a/axelrod/tests/property.py +++ b/axelrod/tests/property.py @@ -11,6 +11,7 @@ lists, sampled_from, ) +from hypothesis.extra.numpy import arrays @composite @@ -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): + """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) \ No newline at end of file diff --git a/axelrod/tests/unit/test_game.py b/axelrod/tests/unit/test_game.py index e14eb0fb4..bb7bee300 100644 --- a/axelrod/tests/unit/test_game.py +++ b/axelrod/tests/unit/test_game.py @@ -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 @@ -77,3 +81,37 @@ 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)) + +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.assertEqual(asymgame1, asymgame1) + self.assertEqual(asymgame2, asymgame2) + self.assertEqual((asymgame1==asymgame2), (asymgame1.A.all() == asymgame2.A.all() + and asymgame1.B.all() == asymgame2.B.all())) \ No newline at end of file From 524b27864905fa001189e984e10b0dfa64e7b482 Mon Sep 17 00:00:00 2001 From: alexhroom Date: Sun, 9 Apr 2023 14:24:18 +0100 Subject: [PATCH 15/24] improved coverage --- axelrod/tests/unit/test_game.py | 1 + 1 file changed, 1 insertion(+) diff --git a/axelrod/tests/unit/test_game.py b/axelrod/tests/unit/test_game.py index bb7bee300..75c9b8df4 100644 --- a/axelrod/tests/unit/test_game.py +++ b/axelrod/tests/unit/test_game.py @@ -111,6 +111,7 @@ def test_random_repr(self, asymgame): @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() From 29506f3e3d6cd24866ad37a4fe786ea0b449c466 Mon Sep 17 00:00:00 2001 From: alexhroom Date: Sat, 15 Apr 2023 15:41:40 +0100 Subject: [PATCH 16/24] changed Action back to Enum and added casting to score --- axelrod/action.py | 4 ++-- axelrod/game.py | 11 ++++++++--- axelrod/history.py | 4 ---- axelrod/strategies/cycler.py | 2 +- axelrod/strategies/finite_state_machines.py | 4 ++-- axelrod/strategies/lookerup.py | 4 ++-- axelrod/strategy_transformers.py | 5 +---- 7 files changed, 16 insertions(+), 18 deletions(-) diff --git a/axelrod/action.py b/axelrod/action.py index ade4dc367..46e1d8d3d 100644 --- a/axelrod/action.py +++ b/axelrod/action.py @@ -6,7 +6,7 @@ C, D = Action.C, Action.D """ -from enum import IntEnum +from enum import Enum from functools import total_ordering from typing import Iterable, Tuple @@ -19,7 +19,7 @@ def __init__(self, *args): @total_ordering -class Action(IntEnum): +class Action(Enum): """Core actions in the Prisoner's Dilemma. There are only two possible actions, namely Cooperate or Defect, diff --git a/axelrod/game.py b/axelrod/game.py index 8e3bc8ee3..6b95bbbf3 100644 --- a/axelrod/game.py +++ b/axelrod/game.py @@ -1,4 +1,5 @@ from typing import Tuple, Union +from enum import Enum import numpy as np @@ -64,10 +65,14 @@ def score( Scores for two player resulting from their actions. """ - row, col = pair + # 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) - # actions can be interpreted as indices as they are - # an IntEnum class return (self.A[row][col], self.B[row][col]) def __repr__(self) -> str: diff --git a/axelrod/history.py b/axelrod/history.py index 36fa374ac..b1ad3650d 100644 --- a/axelrod/history.py +++ b/axelrod/history.py @@ -34,7 +34,6 @@ 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 - play, coplay = Action(play), Action(coplay) self._plays.append(play) self._actions[play] += 1 self._coplays.append(coplay) @@ -46,8 +45,6 @@ def copy(self): def flip_plays(self): """Creates a flipped plays history for use with DualTransformer.""" - # sometimes the 'action' under a transformer is cast to an integer; - # the Action(...) call ensures it is cast back so it can be flipped flipped_plays = [action.flip() for action in self._plays] return self.__class__(plays=flipped_plays, coplays=self._coplays) @@ -135,7 +132,6 @@ 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 - play, coplay = Action(play), Action(coplay) self._plays.append(play) self._actions[play] += 1 diff --git a/axelrod/strategies/cycler.py b/axelrod/strategies/cycler.py index 87483af31..ac2be686a 100644 --- a/axelrod/strategies/cycler.py +++ b/axelrod/strategies/cycler.py @@ -141,7 +141,7 @@ def _generate_random_cycle(self, cycle_length: int) -> str: Generate a sequence of random moves """ return actions_to_str( - Action(self._random.choice(actions)) for _ in range(cycle_length) + self._random.choice(actions) for _ in range(cycle_length) ) def mutate(self) -> EvolvablePlayer: diff --git a/axelrod/strategies/finite_state_machines.py b/axelrod/strategies/finite_state_machines.py index 86d5bf26a..a20e408db 100644 --- a/axelrod/strategies/finite_state_machines.py +++ b/axelrod/strategies/finite_state_machines.py @@ -33,7 +33,7 @@ def __init__(self, transitions: tuple, initial_state: int) -> None: """ self._state = initial_state self._state_transitions = { - (current_state, Action(input_action)): (next_state, Action(output_action)) + (current_state, input_action): (next_state, output_action) for current_state, input_action, next_state, output_action in transitions } # type: dict @@ -118,7 +118,7 @@ def __init__( ) -> None: Player.__init__(self) self.initial_state = initial_state - self.initial_action = Action(initial_action) + self.initial_action = initial_action self.fsm = SimpleFSM(transitions, initial_state) def strategy(self, opponent: Player) -> Action: diff --git a/axelrod/strategies/lookerup.py b/axelrod/strategies/lookerup.py index 98bc9fb31..20024cdb8 100644 --- a/axelrod/strategies/lookerup.py +++ b/axelrod/strategies/lookerup.py @@ -517,7 +517,7 @@ def random_params(self, plays, op_plays, op_start_plays): @classmethod def mutate_value(cls, value): - return Action(value).flip() + return value.flip() def mutate_table(self, table, mutation_probability): randoms = self._random.random(len(table.keys())) @@ -536,7 +536,7 @@ def mutate(self): for i in range(len(initial_actions)): r = self._random.random() if r < self.mutation_probability: - initial_actions[i] = Action(initial_actions[i]).flip() + initial_actions[i] = initial_actions[i].flip() return self.create_new( lookup_dict=lookup_dict, initial_actions=tuple(initial_actions), diff --git a/axelrod/strategy_transformers.py b/axelrod/strategy_transformers.py index 29d736056..70773ff60 100644 --- a/axelrod/strategy_transformers.py +++ b/axelrod/strategy_transformers.py @@ -385,10 +385,7 @@ def dual_wrapper(player, opponent: Player, proposed_action: Action) -> Action: # is done in the strategy of the new PlayerClass created by DualTransformer. # The DualTransformer is dynamically created in StrategyTransformerFactory. - # sometimes the action is an integer - this is a bug. the - # Action(...) call around proposed_action ensures it is an action. - - return Action(proposed_action).flip() + return proposed_action.flip() DualTransformer = StrategyTransformerFactory(dual_wrapper, name_prefix="Dual") From 43bb3a5ed76b90c003526a35cd36112a5a102bf1 Mon Sep 17 00:00:00 2001 From: alexhroom Date: Sat, 15 Apr 2023 16:12:01 +0100 Subject: [PATCH 17/24] added casting test --- axelrod/tests/unit/test_game.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/axelrod/tests/unit/test_game.py b/axelrod/tests/unit/test_game.py index 75c9b8df4..d97fa7b0a 100644 --- a/axelrod/tests/unit/test_game.py +++ b/axelrod/tests/unit/test_game.py @@ -82,6 +82,18 @@ def test_random_repr(self, game): 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))) From a333d626fb61b57f625d4fa2f1eb83b1e36ae00f Mon Sep 17 00:00:00 2001 From: alexhroom Date: Sat, 15 Apr 2023 16:28:28 +0100 Subject: [PATCH 18/24] removed numpy mocking --- .github/workflows/config.yml | 1 + docs/conf.py | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index cd19ca861..5826a7784 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -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: | diff --git a/docs/conf.py b/docs/conf.py index 7c2df0fa9..c632251b3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -26,9 +26,6 @@ "matplotlib.transforms", "mpl_toolkits.axes_grid1", "multiprocess", - "numpy", - "numpy.linalg", - "numpy.random", "pandas", "pandas.util", "pandas.util.decorators", From 7495e3b553ba99ca1dba55c50fcbb04cd47994a0 Mon Sep 17 00:00:00 2001 From: alexhroom Date: Sat, 15 Apr 2023 17:01:33 +0100 Subject: [PATCH 19/24] changed doc due to doctests being picky --- docs/how-to/use_different_stage_games.rst | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/how-to/use_different_stage_games.rst b/docs/how-to/use_different_stage_games.rst index 9bb48be6a..153dc5e41 100644 --- a/docs/how-to/use_different_stage_games.rst +++ b/docs/how-to/use_different_stage_games.rst @@ -56,7 +56,9 @@ with two Numpy arrays for payoff matrices:: >>> B = np.array([[1, 3], [2, 1]]) >>> asymmetric_game = axl.AsymmetricGame(A, B) >>> asymmetric_game - Axelrod game with matrices: (array([[3, 1], [1, 3]]), array([[1, 3], [2, 1]])) + 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:: @@ -64,8 +66,7 @@ Asymmetric games can also be different sizes, such as Rock Paper Scissors:: >>> rock_paper_scissors = axl.AsymmetricGame(A, -A) >>> rock_paper_scissors Axelrod game with matrices: (array([[ 0, -1, 1], - [ 1, 0, -1], - [-1, 1, 0]]), - array([[ 0, 1, -1], - [-1, 0, 1], - [ 1, -1, 0]])) \ No newline at end of file + [ 1, 0, -1], + [-1, 1, 0]]), array([[ 0, 1, -1], + [-1, 0, 1], + [ 1, -1, 0]])) \ No newline at end of file From b9e78eaa9cf40974762639bc4292445f35d95e3e Mon Sep 17 00:00:00 2001 From: alexhroom Date: Sun, 16 Apr 2023 14:14:14 +0100 Subject: [PATCH 20/24] re-fixed doctest examples --- docs/how-to/use_different_stage_games.rst | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/how-to/use_different_stage_games.rst b/docs/how-to/use_different_stage_games.rst index 153dc5e41..5710ecf63 100644 --- a/docs/how-to/use_different_stage_games.rst +++ b/docs/how-to/use_different_stage_games.rst @@ -55,18 +55,20 @@ with two Numpy arrays for payoff matrices:: >>> A = np.array([[3, 1], [1, 3]]) >>> B = np.array([[1, 3], [2, 1]]) >>> asymmetric_game = axl.AsymmetricGame(A, B) - >>> asymmetric_game + >>> asymmetric_game # doctest: +NORMALIZE_WHITESPACE Axelrod game with matrices: (array([[3, 1], - [1, 3]]), array([[1, 3], - [2, 1]])) + [1, 3]]), + array([[1, 3], + [2, 1]])) Asymmetric games can also be different sizes, such as Rock Paper Scissors:: >>> A = np.array([[0, -1, 1], [1, 0, -1], [-1, 1, 0]]) >>> rock_paper_scissors = axl.AsymmetricGame(A, -A) - >>> rock_paper_scissors + >>> 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]])) \ No newline at end of file + [ 1, 0, -1], + [-1, 1, 0]]), + array([[ 0, 1, -1], + [-1, 0, 1], + [ 1, -1, 0]])) \ No newline at end of file From 90b0a4446ccd82d89098a61b89d1af007bde8a47 Mon Sep 17 00:00:00 2001 From: alexhroom Date: Wed, 19 Apr 2023 19:35:08 +0100 Subject: [PATCH 21/24] review changes --- axelrod/action.py | 4 ++-- axelrod/history.py | 3 --- docs/how-to/use_different_stage_games.rst | 8 +++++++- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/axelrod/action.py b/axelrod/action.py index 46e1d8d3d..a8ba85445 100644 --- a/axelrod/action.py +++ b/axelrod/action.py @@ -63,9 +63,9 @@ def from_char(cls, character): UnknownActionError If the input string is not 'C' or 'D' """ - if character == "C" or character == "0": + if character == "C": return cls.C - if character == "D" or character == "1": + if character == "D": return cls.D raise UnknownActionError('Character must be "C" or "D".') diff --git a/axelrod/history.py b/axelrod/history.py index b1ad3650d..12114399b 100644 --- a/axelrod/history.py +++ b/axelrod/history.py @@ -33,7 +33,6 @@ 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) @@ -131,8 +130,6 @@ 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 - self._plays.append(play) self._actions[play] += 1 self._coplays.append(coplay) diff --git a/docs/how-to/use_different_stage_games.rst b/docs/how-to/use_different_stage_games.rst index 5710ecf63..c25a0590f 100644 --- a/docs/how-to/use_different_stage_games.rst +++ b/docs/how-to/use_different_stage_games.rst @@ -71,4 +71,10 @@ Asymmetric games can also be different sizes, such as Rock Paper Scissors:: [-1, 1, 0]]), array([[ 0, 1, -1], [-1, 0, 1], - [ 1, -1, 0]])) \ No newline at end of file + [ 1, -1, 0]])) + +**NB: Some features of Axelrod, such as strategy transformers, are specifically created for +use with the iterated Prisoner's Dilemma; they may break with games of other sizes.** +Note also that most strategies in Axelrod are Prisoners' Dilemma strategies, so behave +as though they are playing the Prisoners' Dilemma; in the rock-paper-scissors example above, +they will certainly never choose scissors (because their strategy action set is two actions!) \ No newline at end of file From 0afa186664ae293eb2231d493ae22918895ddbb3 Mon Sep 17 00:00:00 2001 From: alexhroom Date: Thu, 20 Apr 2023 09:34:51 +0100 Subject: [PATCH 22/24] Added tutorial for implementing new games --- docs/how-to/use_different_stage_games.rst | 10 +- docs/tutorials/implement_new_games/index.rst | 187 +++++++++++++++++++ docs/tutorials/index.rst | 1 + 3 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 docs/tutorials/implement_new_games/index.rst diff --git a/docs/how-to/use_different_stage_games.rst b/docs/how-to/use_different_stage_games.rst index c25a0590f..517c98555 100644 --- a/docs/how-to/use_different_stage_games.rst +++ b/docs/how-to/use_different_stage_games.rst @@ -1,3 +1,5 @@ +.. _use_different_stage_games: + Use different stage games ========================= @@ -61,7 +63,8 @@ with two Numpy arrays for payoff matrices:: array([[1, 3], [2, 1]])) -Asymmetric games can also be different sizes, such as Rock Paper Scissors:: +Asymmetric games can also be different sizes (even if symmetric; regular games +can currently only be 2x2), such as Rock Paper Scissors:: >>> A = np.array([[0, -1, 1], [1, 0, -1], [-1, 1, 0]]) >>> rock_paper_scissors = axl.AsymmetricGame(A, -A) @@ -77,4 +80,7 @@ Asymmetric games can also be different sizes, such as Rock Paper Scissors:: use with the iterated Prisoner's Dilemma; they may break with games of other sizes.** Note also that most strategies in Axelrod are Prisoners' Dilemma strategies, so behave as though they are playing the Prisoners' Dilemma; in the rock-paper-scissors example above, -they will certainly never choose scissors (because their strategy action set is two actions!) \ No newline at end of file +they will certainly never choose scissors (because their strategy action set is two actions!) + +For a more detailed tutorial on how to implement another game into Axelrod, :ref:`here is a +tutorial using rock paper scissors as an example. ` \ No newline at end of file diff --git a/docs/tutorials/implement_new_games/index.rst b/docs/tutorials/implement_new_games/index.rst new file mode 100644 index 000000000..d17ea1414 --- /dev/null +++ b/docs/tutorials/implement_new_games/index.rst @@ -0,0 +1,187 @@ +.. _implement-new-games: +.. highlight:: python + +Implement new games +=================== + +Currently, the default :code:`Strategy`, :code:`Action` and :code:`Game` +implementations in Axelrod are centred around the Iterated Prisoners' Dilemma. +The stage game can be changed as shown in :ref:`use_different_stage_games`. + +However, just changing the stage game may not be sufficient. Take, for example, the +game rock-paper-scissors:: + + >>> import axelrod as axl + >>> 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]])) + +If we tried to run a rock-paper-scissors match with the :code:`Tit-For-Tat` strategy, +it wouldn't work properly. :code:`Tit-For-Tat` only knows of two actions (cooperate and defect, +corresponding to rows 1 and 2 respectively). If we tried to use it on rock-paper-scissors, it would +interpret the game in the following way: + +1. On the first turn, choose rock (option 1, cooperate) +2. If the opponent's last move is the Python object + :code:`axl.Action.D` (which it may never be unless the opponent also thinks it's playing IPD!), + then choose paper (option 2, defection) + +and so as we see, :code:`Tit-For-Tat` would simply play Rock every turn, unless it +were playing against another Prisoners' Dilemma strategy (then it +plays rock unless the opponent last played paper, in which case it plays paper). In +particular, it would never play scissors - it does not know that Scissors is something +it can even do. This is not a bug, or an issue with the strategy itself; +simply that :code:`Tit-For-Tat` *thinks* it is playing the Iterated Prisoners' Dilemma +and its :code:`Action` set, regardless of what the actual game is. + +Thus, if we wanted to implement new games we should also implement a new Action set, +and some new strategies. + +The Actions are relatively simple; they're an `Enum class `_, +with each action corresponding to a row/column (recall that Python starts counting from 0, +rather than 1). We can also implement some methods that we think might be useful for viewing +our actions and making strategies. (The Prisoners' Dilemma :code:`Action` class, for example, +has :code:`flip`, which flips a :code:`C` to a :code:`D` and vice versa!) + +A simple rock-paper-scissors action class would look like so:: + + class RPSAction(Enum): + """Actions for Rock-Paper-Scissors.""" + + R = 0 + P = 1 + S = 2 + + def __repr__(self): + return self.name + + def __str__(self): + return self.name + + def rotate(self): + """ + Cycles one step through the actions. + + Maps: + R -> P + P -> S + S -> R + """ + rotations = { + RPSAction.R: RPSAction.P, + RPSAction.P: RPSAction.S, + RPSAction.S: RPSAction.R + } + + return rotations[self] + +We can then implement some strategies. Below we have the implementation of an +Axelrod strategy into Python. These follow the same format; + +* A subclass of the :code:`Player` class, with three parts: + + * A :code:`name` and a :code:`classifier` dictionary. + This is used for indexing strategies. + + * (Optionally) an :code:`__init__` method, which allows the setting + of initialisation variables (like probabilities of doing certain + actions, or starting moves) + + * A :code:`strategy` method, which takes the parameters :code:`self` + and `opponent`, representing both players in the match, and provides + the algorithm for determining the player's next move. + +If we want, we can also initialise some shorthand for the actions to +avoid having to evoke their full names:: + + R = RPSAction.R + P = RPSAction.P + S = RPSAction.S + +Here are a couple of examples. One is a strategy which copies the opponent's +previous move, and the other simply cycles through the moves. Both have +an initialisation parameter for which move they start with:: + + class Copycat(Player): + """ + Starts with a chosen move, + and then copies their opponent's previous move. + + Parameters + ---------- + starting_move: RPSAction, default S + What move to play on the first round. + """ + + name = "Copycat" + classifier = { + "memory_depth": 1, + "stochastic": False, + "long_run_time": False, + "inspects_source": False, + "manipulates_source": False, + "manipulates_state": False, + } + + def __init__(self, starting_move=S): + self.starting_move = starting_move + super().__init__() + + def strategy(self, opponent: Player) -> RPSAction: + """Actual strategy definition that determines player's action.""" + if not self.history: + return self.starting_move + return opponent.history[-1] + + + class Rotator(Player): + """ + Cycles through the moves from a chosen starting move. + + Parameters + ---------- + starting_move: RPSAction, default S + What move to play on the first round. + """ + + name = "Rotator" + classifier = { + "memory_depth": 1, + "stochastic": False, + "long_run_time": False, + "inspects_source": False, + "manipulates_source": False, + "manipulates_state": False, + } + + def __init__(self, starting_move=S): + self.starting_move = starting_move + super().__init__() + + def strategy(self, opponent: Player) -> RPSAction: + """Actual strategy definition that determines player's action.""" + if not self.history: + return self.starting_move + return self.history[-1].rotate() + +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:: + + >>> match = axl.Match(players=(Copycat(), Rotator()), turns=5, game=rock_paper_scissors) + >>> match.play() # doctest: +SKIP + [(S, S), (S, R), (R, P), (P, S), (S, R)] + +and as with the Prisoners' Dilemma, we can run a tournament in the same way. Just +make sure you specify the game when creating the tournament!:: + + tournament = axl.Tournament(players, game=rock_paper_scissors) + tournament.play() + +where :code:`players` is set to a list of Rock-Paper-Scissors strategies; hopefully +more than two, else it isn't a very interesting tournament! \ No newline at end of file diff --git a/docs/tutorials/index.rst b/docs/tutorials/index.rst index e4d8de34a..cf503b7dc 100644 --- a/docs/tutorials/index.rst +++ b/docs/tutorials/index.rst @@ -13,3 +13,4 @@ Contents: new_to_game_theory_and_or_python/index.rst running_axelrods_first_tournament/index.rst creating_heterogenous_player_moran_process/index.rst + implement_new_games/index.rst From 0489728546c32db6ba4e4c9f9b84024b77abdd88 Mon Sep 17 00:00:00 2001 From: Alex Room <69592136+alexhroom@users.noreply.github.com> Date: Thu, 20 Apr 2023 08:51:21 +0000 Subject: [PATCH 23/24] fixed formatting --- docs/tutorials/implement_new_games/index.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/implement_new_games/index.rst b/docs/tutorials/implement_new_games/index.rst index d17ea1414..e99e2ba38 100644 --- a/docs/tutorials/implement_new_games/index.rst +++ b/docs/tutorials/implement_new_games/index.rst @@ -94,7 +94,7 @@ Axelrod strategy into Python. These follow the same format; actions, or starting moves) * A :code:`strategy` method, which takes the parameters :code:`self` - and `opponent`, representing both players in the match, and provides + and :code:`opponent`, representing both players in the match, and provides the algorithm for determining the player's next move. If we want, we can also initialise some shorthand for the actions to @@ -184,4 +184,4 @@ make sure you specify the game when creating the tournament!:: tournament.play() where :code:`players` is set to a list of Rock-Paper-Scissors strategies; hopefully -more than two, else it isn't a very interesting tournament! \ No newline at end of file +more than two, else it isn't a very interesting tournament! From c79d4c5654c6c85468ae9871ad86fc7440481661 Mon Sep 17 00:00:00 2001 From: alexhroom Date: Thu, 20 Apr 2023 12:35:48 +0100 Subject: [PATCH 24/24] made doctests work --- docs/tutorials/implement_new_games/index.rst | 193 +++++++++---------- 1 file changed, 95 insertions(+), 98 deletions(-) diff --git a/docs/tutorials/implement_new_games/index.rst b/docs/tutorials/implement_new_games/index.rst index d17ea1414..0cc68993e 100644 --- a/docs/tutorials/implement_new_games/index.rst +++ b/docs/tutorials/implement_new_games/index.rst @@ -12,6 +12,7 @@ However, just changing the stage game may not be sufficient. Take, for example, game rock-paper-scissors:: >>> import axelrod as axl + >>> import numpy as np >>> 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 @@ -51,35 +52,31 @@ has :code:`flip`, which flips a :code:`C` to a :code:`D` and vice versa!) A simple rock-paper-scissors action class would look like so:: - class RPSAction(Enum): - """Actions for Rock-Paper-Scissors.""" - - R = 0 - P = 1 - S = 2 - - def __repr__(self): - return self.name - - def __str__(self): - return self.name - - def rotate(self): - """ - Cycles one step through the actions. - - Maps: - R -> P - P -> S - S -> R - """ - rotations = { - RPSAction.R: RPSAction.P, - RPSAction.P: RPSAction.S, - RPSAction.S: RPSAction.R - } - - return rotations[self] + >>> from enum import Enum + >>> class RPSAction(Enum): + ... """Actions for Rock-Paper-Scissors.""" + ... R = 0 # rock + ... P = 1 # paper + ... S = 2 # scissors + ... + ... def __repr__(self): + ... return self.name + ... + ... def __str__(self): + ... return self.name + ... + ... def rotate(self): + ... """ + ... Cycles one step through the actions. + ... Maps R->P, P->S, S->R + ... """ + ... rotations = { + ... RPSAction.R: RPSAction.P, + ... RPSAction.P: RPSAction.S, + ... RPSAction.S: RPSAction.R + ... } + ... + ... return rotations[self] We can then implement some strategies. Below we have the implementation of an Axelrod strategy into Python. These follow the same format; @@ -100,88 +97,88 @@ Axelrod strategy into Python. These follow the same format; If we want, we can also initialise some shorthand for the actions to avoid having to evoke their full names:: - R = RPSAction.R - P = RPSAction.P - S = RPSAction.S + >>> R = RPSAction.R + >>> P = RPSAction.P + >>> S = RPSAction.S Here are a couple of examples. One is a strategy which copies the opponent's previous move, and the other simply cycles through the moves. Both have an initialisation parameter for which move they start with:: - class Copycat(Player): - """ - Starts with a chosen move, - and then copies their opponent's previous move. - - Parameters - ---------- - starting_move: RPSAction, default S - What move to play on the first round. - """ - - name = "Copycat" - classifier = { - "memory_depth": 1, - "stochastic": False, - "long_run_time": False, - "inspects_source": False, - "manipulates_source": False, - "manipulates_state": False, - } - - def __init__(self, starting_move=S): - self.starting_move = starting_move - super().__init__() - - def strategy(self, opponent: Player) -> RPSAction: - """Actual strategy definition that determines player's action.""" - if not self.history: - return self.starting_move - return opponent.history[-1] - - - class Rotator(Player): - """ - Cycles through the moves from a chosen starting move. - - Parameters - ---------- - starting_move: RPSAction, default S - What move to play on the first round. - """ - - name = "Rotator" - classifier = { - "memory_depth": 1, - "stochastic": False, - "long_run_time": False, - "inspects_source": False, - "manipulates_source": False, - "manipulates_state": False, - } - - def __init__(self, starting_move=S): - self.starting_move = starting_move - super().__init__() - - def strategy(self, opponent: Player) -> RPSAction: - """Actual strategy definition that determines player's action.""" - if not self.history: - return self.starting_move - return self.history[-1].rotate() + >>> from axelrod.player import Player + >>> class Copycat(Player): + ... """ + ... Starts with a chosen move, + ... and then copies their opponent's previous move. + ... + ... Parameters + ... ---------- + ... starting_move: RPSAction, default S + ... What move to play on the first round. + ... """ + ... name = "Copycat" + ... classifier = { + ... "memory_depth": 1, + ... "stochastic": False, + ... "long_run_time": False, + ... "inspects_source": False, + ... "manipulates_source": False, + ... "manipulates_state": False, + ... } + ... + ... def __init__(self, starting_move=S): + ... self.starting_move = starting_move + ... super().__init__() + ... + ... def strategy(self, opponent: Player) -> RPSAction: + ... """Actual strategy definition that determines player's action.""" + ... if not self.history: + ... return self.starting_move + ... return opponent.history[-1] + + >>> class Rotator(Player): + ... """ + ... Cycles through the moves from a chosen starting move. + ... + ... Parameters + ... ---------- + ... starting_move: RPSAction, default S + ... What move to play on the first round. + ... """ + ... name = "Rotator" + ... classifier = { + ... "memory_depth": 1, + ... "stochastic": False, + ... "long_run_time": False, + ... "inspects_source": False, + ... "manipulates_source": False, + ... "manipulates_state": False, + ... } + ... + ... def __init__(self, starting_move=S): + ... self.starting_move = starting_move + ... super().__init__() + ... + ... def strategy(self, opponent: Player) -> RPSAction: + ... """Actual strategy definition that determines player's action.""" + ... if not self.history: + ... return self.starting_move + ... return self.history[-1].rotate() 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:: - >>> match = axl.Match(players=(Copycat(), Rotator()), turns=5, game=rock_paper_scissors) - >>> match.play() # doctest: +SKIP - [(S, S), (S, R), (R, P), (P, S), (S, R)] + >>> match = axl.Match(players=(Copycat(starting_move=P), Rotator()), + ... turns=5, + ... game=rock_paper_scissors) + >>> match.play() + [(P, S), (S, R), (R, P), (P, S), (S, R)] and as with the Prisoners' Dilemma, we can run a tournament in the same way. Just make sure you specify the game when creating the tournament!:: - tournament = axl.Tournament(players, game=rock_paper_scissors) - tournament.play() + >>> tournament = axl.Tournament(players, game=rock_paper_scissors) # doctest: +SKIP + >>> tournament.play() # doctest: +SKIP where :code:`players` is set to a list of Rock-Paper-Scissors strategies; hopefully more than two, else it isn't a very interesting tournament! \ No newline at end of file