From 7882c776cc750c317c4208e153212d2cdfe2fe32 Mon Sep 17 00:00:00 2001 From: Marc Harper Date: Thu, 22 Oct 2015 20:45:34 -0700 Subject: [PATCH 01/13] Strategy Transformers --- axelrod/mock_player.py | 2 +- axelrod/strategies/__init__.py | 2 + axelrod/strategies/_strategies.py | 2 +- axelrod/strategies/strategy_transformers.py | 272 ++++++++++++++++++ .../tests/unit/test_strategy_transformers.py | 97 +++++++ 5 files changed, 373 insertions(+), 2 deletions(-) create mode 100644 axelrod/strategies/strategy_transformers.py create mode 100644 axelrod/tests/unit/test_strategy_transformers.py diff --git a/axelrod/mock_player.py b/axelrod/mock_player.py index 0dbadee92..f639f3eb6 100644 --- a/axelrod/mock_player.py +++ b/axelrod/mock_player.py @@ -14,7 +14,7 @@ class MockPlayer(Player): def __init__(self, player, move): # Need to retain history for opponents that examine opponents history # Do a deep copy just to be safe - axelrod.Player.__init__(self) + Player.__init__(self) self.history = copy.deepcopy(player.history) self.cooperations = player.cooperations self.defections = player.defections diff --git a/axelrod/strategies/__init__.py b/axelrod/strategies/__init__.py index b7acd82a6..c3c298345 100644 --- a/axelrod/strategies/__init__.py +++ b/axelrod/strategies/__init__.py @@ -1,6 +1,8 @@ from ..player import is_basic, obey_axelrod from ._strategies import * +import strategy_transformers + # `from ._strategies import *` import the collection `strategies` # Now import the Meta strategies. This cannot be done in _strategies # because it creates circular dependencies diff --git a/axelrod/strategies/_strategies.py b/axelrod/strategies/_strategies.py index 6a2fe797b..390a38cec 100644 --- a/axelrod/strategies/_strategies.py +++ b/axelrod/strategies/_strategies.py @@ -8,7 +8,7 @@ from .backstabber import BackStabber, DoubleCrosser from .calculator import Calculator from .cooperator import Cooperator, TrickyCooperator -from .cycler import AntiCycler, CyclerCCD, CyclerCCCD, CyclerCCCCCD +from .cycler import AntiCycler, Cycler, CyclerCCD, CyclerCCCD, CyclerCCCCCD from .darwin import Darwin from .defector import Defector, TrickyDefector from .forgiver import Forgiver, ForgivingTitForTat diff --git a/axelrod/strategies/strategy_transformers.py b/axelrod/strategies/strategy_transformers.py new file mode 100644 index 000000000..e1f70dd59 --- /dev/null +++ b/axelrod/strategies/strategy_transformers.py @@ -0,0 +1,272 @@ + +""" +Strategy Transformers -- class decorators that transform the behavior of any +strategy. + +Run Axelrod one tournament + +Flip Action +Forgiver +Initial Sequence +Final Sequence + +TFT -- force a repayment, return to other strategy + +Noisy +RetaliateUntilApology + +Compose. + +Memory-depth inference + +As Decorators + +Meta Strategies + +""" + +import random +from types import FunctionType + +import axelrod +from axelrod import flip_action, random_choice, simulate_play, Actions +C, D = Actions.C, Actions.D + +# Note: the history is overwritten with the modified history +# Just like in the Noisy case +# This can lead to unexpected behavior, such as when +# Flip transform is applied to Alternator + + +def generic_strategy_wrapper(player, opponent, proposed_action, *args, **kwargs): + """ + Strategy wrapper functions should be of the following form. + + Parameters + ---------- + player: Player object or subclass (self) + opponent: Player object or subclass + proposed_action: an axelrod.Action, C or D + The proposed action by the wrapped strategy + proposed_action = Player.strategy(...) + args, kwargs: + Any additional arguments that you need. + + Returns + ------- + action: an axelrod.Action, C or D + + """ + + # This example just passes through the proposed_action + return proposed_action + +def StrategyTransformerFactory(strategy_wrapper, wrapper_args=(), wrapper_kwargs={}, + name_prefix="Transformed "): + """Modify an existing strategy dynamically by wrapping the strategy + method with the argument `strategy_wrapper`. + + Parameters + ---------- + strategy_wrapper: function + A function of the form `strategy_wrapper(player, opponent, proposed_action, *args, **kwargs)` + wrapper_args: tuple + Any arguments to pass to the wrapper + wrapper_kwargs: dict + Any keyword arguments to pass to the wrapper + name_prefix: string, "Transformed " + A string to prepend to the strategy and class name + """ + + # Create a function that applies a wrapper function to the strategy method + # of a given class + def decorate(PlayerClass): + """ + Parameters + ---------- + PlayerClass: A subclass of axelrod.Player, e.g. Cooperator + + Returns + ------- + new_class, class object + A class object that can create instances of the modified PlayerClass + """ + + # Define the new strategy method, wrapping the existing method + # with `strategy_wrapper` + def strategy(self, opponent): + # Is the original strategy method a static method? + if isinstance(PlayerClass.strategy, FunctionType): + proposed_action = PlayerClass.strategy(opponent) + else: + proposed_action = PlayerClass.strategy(self, opponent) + # Apply the wrapper + return strategy_wrapper(self, opponent, proposed_action, + *wrapper_args, **wrapper_kwargs) + + # Define a new class and wrap the strategy method + # Modify the PlayerClass name + new_class_name = name_prefix + PlayerClass.__name__ + # Modify the Player name (class variable inherited from Player) + name = name_prefix + PlayerClass.name + # Dynamically create the new class + new_class = type(new_class_name, (PlayerClass,), + {"name": name, "strategy": strategy}) + return new_class + return decorate + +def flip_wrapper(player, opponent, action): + """Applies flip_action at the class level.""" + return flip_action(action) + +FlipTransformer = StrategyTransformerFactory(flip_wrapper, name_prefix="Flipped ") + +def forgiver_wrapper(player, opponent, action, p): + """If a strategy wants to defect, flip to cooperate with the given + probability.""" + if action == D: + return random_choice(p) + return C + +def ForgiverTransformer(p): + return StrategyTransformerFactory(forgiver_wrapper, wrapper_args=(p,)) + +def initial_sequence(player, opponent, action, initial_seq): + """Play the moves in `seq` first (must be a list), ignoring the strategy's + moves until the list is exhausted.""" + index = len(player.history) + if index < len(initial_seq): + return initial_seq[index] + return action + +## Defection initially three times +def InitialTransformer(seq=None): + if not seq: + seq = [D] * 3 + transformer = StrategyTransformerFactory(initial_sequence, wrapper_args=(seq,), + name_prefix="Initial ") + return transformer + +def final_sequence(player, opponent, action, seq): + """Play the moves in `seq` first, ignoring the strategy's + moves until the list is exhausted.""" + try: + length = player.tournament_attributes["length"] + except KeyError: + return action + finally: + if length < 0: # default is -1 + return action + + index = length - len(player.history) + if index <= len(seq): + return seq[-index] + return action + +# Defect on last N actions +def FinalTransformer(seq=None): + if not seq: + seq = [D] * 3 + transformer = StrategyTransformerFactory(final_sequence, wrapper_args=(seq,)) + return transformer + + +# Strategy wrapper as a class example +class RetaliationWrapper(object): + def __init__(self): + self.is_retaliating = False + + def __call__(self, player, opponent, action): + if len(player.history) == 0: + return action + if opponent.history[-1]: + self.is_retaliating = True + return D + if self.is_retaliating: + if opponent.history[-1] == C: + self.is_retaliating = False + return C + return D + return action + +def RetailiateUntilApologyTransformer(): + strategy_wrapper = RetaliationWrapper() + return StrategyTransformerFactory(strategy_wrapper, name_prefix="RUA ") + + + +#if __name__ == "__main__": + ## Cooperator to Defector + #p1 = axelrod.Cooperator() + #p2 = FlipTransformer(axelrod.Cooperator)() # Defector + #print p1, p2 + #print simulate_play(p1, p2) + #print simulate_play(p1, p2) + + ## Test that cloning preserves transform + #p3 = p2.clone() + #print simulate_play(p1, p3) + #print simulate_play(p1, p3) + + ## Forgiving example + #p1 = ForgiverTransformer(axelrod.Defector)() + #print simulate_play(p1, p2) + #print simulate_play(p1, p2) + #print simulate_play(p1, p2) + #print simulate_play(p1, p2) + #print simulate_play(p1, p2) + #print simulate_play(p1, p2) + + ## Difference between Alternator and CyclerCD + #p1 = axelrod.Cycler(cycle="CD") + #p2 = FlipTransformer(axelrod.Cycler)(cycle="CD") + #for _ in range(5): + #p1.play(p2) + #print p1.history, p2.history + + ## Initial play transformer + #p1 = axelrod.Cooperator() + #p2 = InitialTransformer()(axelrod.Cooperator)() + + #for _ in range(6): + #p1.play(p2) + #print p1.history, p2.history + + ## Final Play transformer + #p1 = FinalTransformer()(axelrod.Cooperator)() + #p2 = axelrod.Cooperator() + #p1.tournament_attributes["length"] = 6 + + #for _ in range(6): + #p1.play(p2) + #print p1.history, p2.history + + + ## Composition + #cls1 = InitialTransformer()(axelrod.Cooperator) + #cls2 = FinalTransformer()(cls1) + #p1 = cls2() + + #p2 = axelrod.Cooperator() + #p1.tournament_attributes["length"] = 8 + + #for _ in range(8): + #p1.play(p2) + #print p1.history + + ## Composition + ##cls2 = FinalTransformer()(InitialTransformer(axelrod.Cooperator)) + ##p1 = cls2() + + #cls1 = InitialTransformer([D, D, D])(axelrod.Cooperator) + #cls2 = FinalTransformer([D, D])(cls1) + #p1 = cls2() + + #p2 = axelrod.Cooperator() + #p1.tournament_attributes["length"] = 8 + + #for _ in range(8): + #p1.play(p2) + #print p1.history + + diff --git a/axelrod/tests/unit/test_strategy_transformers.py b/axelrod/tests/unit/test_strategy_transformers.py new file mode 100644 index 000000000..acd227420 --- /dev/null +++ b/axelrod/tests/unit/test_strategy_transformers.py @@ -0,0 +1,97 @@ + +import random +import unittest + +import axelrod +from axelrod.strategies.strategy_transformers import * + +C, D = axelrod.Actions.C, axelrod.Actions.D + + +class TestTransformers(unittest.TestCase): + + def test_naming(self): + pass + + def test_flip_transformer(self): + # Cooperator to Defector + p1 = axelrod.Cooperator() + p2 = FlipTransformer(axelrod.Cooperator)() # Defector + self.assertEqual(simulate_play(p1, p2), (C, D)) + self.assertEqual(simulate_play(p1, p2), (C, D)) + self.assertEqual(simulate_play(p1, p2), (C, D)) + + def test_cloning(self): + # Test that cloning preserves transform + p1 = axelrod.Cooperator() + p2 = FlipTransformer(axelrod.Cooperator)() # Defector + p3 = p2.clone() + self.assertEqual(simulate_play(p1, p3), (C, D)) + self.assertEqual(simulate_play(p1, p3), (C, D)) + + def test_forgiving(self): + random.seed(10) + p1 = ForgiverTransformer(0.5)(axelrod.Defector)() + p2 = axelrod.Defector() + for _ in range(10): + p1.play(p2) + self.assertEqual(p1.history, [D, C, D, C, D, D, D, C, D, C]) + + def test_cycler(self): + # Difference between Alternator and CyclerCD + p1 = axelrod.Cycler(cycle="CD") + p2 = FlipTransformer(axelrod.Cycler)(cycle="CD") + for _ in range(5): + p1.play(p2) + self.assertEqual(p1.history, [C, D, C, D, C]) + self.assertEqual(p2.history, [D, C, D, C, D]) + + def test_initial_transformer(self): + # Initial play transformer + p1 = axelrod.Cooperator() + p2 = InitialTransformer([D, D])(axelrod.Cooperator)() + for _ in range(5): + p1.play(p2) + self.assertEqual(p2.history, [D, D, C, C, C]) + + p1 = axelrod.Cooperator() + p2 = InitialTransformer([D, D, C, D])(axelrod.Cooperator)() + for _ in range(5): + p1.play(p2) + self.assertEqual(p2.history, [D, D, C, D, C]) + + def test_final_transformer(self): + # Final play transformer + p1 = axelrod.Cooperator() + p2 = FinalTransformer([D, D, D])(axelrod.Cooperator)() + p2.tournament_attributes["length"] = 6 + for _ in range(6): + p1.play(p2) + self.assertEqual(p2.history, [C, C, C, D, D, D]) + + def test_final_transformer2(self): + # Final play transformer (no tournament length) + p1 = axelrod.Cooperator() + p2 = FinalTransformer()(axelrod.Cooperator)() + for _ in range(6): + p1.play(p2) + self.assertEqual(p2.history, [C, C, C, C, C, C]) + + def test_composition(self): + cls1 = InitialTransformer()(axelrod.Cooperator) + cls2 = FinalTransformer()(cls1) + p1 = cls2() + p2 = axelrod.Cooperator() + p1.tournament_attributes["length"] = 8 + for _ in range(8): + p1.play(p2) + self.assertEqual(p1.history, [D, D, D, C, C, D, D, D]) + + cls1 = FinalTransformer()(InitialTransformer()(axelrod.Cooperator)) + p1 = cls1() + p2 = axelrod.Cooperator() + p1.tournament_attributes["length"] = 8 + for _ in range(8): + p1.play(p2) + self.assertEqual(p1.history, [D, D, D, C, C, D, D, D]) + From 7d822fdea6bb8f31370a93ca75d8ab32c14b0078 Mon Sep 17 00:00:00 2001 From: Marc Harper Date: Thu, 22 Oct 2015 20:59:46 -0700 Subject: [PATCH 02/13] More tests --- axelrod/strategies/strategy_transformers.py | 14 ++++++------ .../tests/unit/test_strategy_transformers.py | 22 ++++++++++++++++++- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/axelrod/strategies/strategy_transformers.py b/axelrod/strategies/strategy_transformers.py index e1f70dd59..e0929909c 100644 --- a/axelrod/strategies/strategy_transformers.py +++ b/axelrod/strategies/strategy_transformers.py @@ -62,7 +62,7 @@ def generic_strategy_wrapper(player, opponent, proposed_action, *args, **kwargs) return proposed_action def StrategyTransformerFactory(strategy_wrapper, wrapper_args=(), wrapper_kwargs={}, - name_prefix="Transformed "): + name_prefix="Transformed"): """Modify an existing strategy dynamically by wrapping the strategy method with the argument `strategy_wrapper`. @@ -108,7 +108,7 @@ def strategy(self, opponent): # Modify the PlayerClass name new_class_name = name_prefix + PlayerClass.__name__ # Modify the Player name (class variable inherited from Player) - name = name_prefix + PlayerClass.name + name = name_prefix + ' ' + PlayerClass.name # Dynamically create the new class new_class = type(new_class_name, (PlayerClass,), {"name": name, "strategy": strategy}) @@ -119,7 +119,7 @@ def flip_wrapper(player, opponent, action): """Applies flip_action at the class level.""" return flip_action(action) -FlipTransformer = StrategyTransformerFactory(flip_wrapper, name_prefix="Flipped ") +FlipTransformer = StrategyTransformerFactory(flip_wrapper, name_prefix="Flipped") def forgiver_wrapper(player, opponent, action, p): """If a strategy wants to defect, flip to cooperate with the given @@ -144,7 +144,7 @@ def InitialTransformer(seq=None): if not seq: seq = [D] * 3 transformer = StrategyTransformerFactory(initial_sequence, wrapper_args=(seq,), - name_prefix="Initial ") + name_prefix="Initial") return transformer def final_sequence(player, opponent, action, seq): @@ -167,7 +167,8 @@ def final_sequence(player, opponent, action, seq): def FinalTransformer(seq=None): if not seq: seq = [D] * 3 - transformer = StrategyTransformerFactory(final_sequence, wrapper_args=(seq,)) + transformer = StrategyTransformerFactory(final_sequence, wrapper_args=(seq,), + name_prefix="Final") return transformer @@ -181,7 +182,6 @@ def __call__(self, player, opponent, action): return action if opponent.history[-1]: self.is_retaliating = True - return D if self.is_retaliating: if opponent.history[-1] == C: self.is_retaliating = False @@ -191,7 +191,7 @@ def __call__(self, player, opponent, action): def RetailiateUntilApologyTransformer(): strategy_wrapper = RetaliationWrapper() - return StrategyTransformerFactory(strategy_wrapper, name_prefix="RUA ") + return StrategyTransformerFactory(strategy_wrapper, name_prefix="RUA") diff --git a/axelrod/tests/unit/test_strategy_transformers.py b/axelrod/tests/unit/test_strategy_transformers.py index acd227420..5768961e0 100644 --- a/axelrod/tests/unit/test_strategy_transformers.py +++ b/axelrod/tests/unit/test_strategy_transformers.py @@ -4,6 +4,8 @@ import axelrod from axelrod.strategies.strategy_transformers import * +from .test_titfortat import TestTitForTat + C, D = axelrod.Actions.C, axelrod.Actions.D @@ -11,7 +13,10 @@ class TestTransformers(unittest.TestCase): def test_naming(self): - pass + cls = FlipTransformer(axelrod.Cooperator) + p1 = cls() + self.assertEqual(cls.__name__, "FlippedCooperator") + self.assertEqual(p1.name, "Flipped Cooperator") def test_flip_transformer(self): # Cooperator to Defector @@ -95,3 +100,18 @@ def test_composition(self): p1.play(p2) self.assertEqual(p1.history, [D, D, D, C, C, D, D, D]) + +# Test that RUA(Cooperator) is the same as TitForTat +# Reusing the TFT tests +RUA = RetailiateUntilApologyTransformer() +TFT = RUA(axelrod.Cooperator) + +class TestRUA(TestTitForTat): + @classmethod + def setUpClass(cls): + player = TFT + name = "RUA Cooperator" + # Skips testing this, which is zero but should be 1 + #expected_classifier["memory_depth"] = 1 + + From 753f6239e708966ad5e2e1468cb373d533a85c66 Mon Sep 17 00:00:00 2001 From: Marc Harper Date: Thu, 22 Oct 2015 21:04:14 -0700 Subject: [PATCH 03/13] Noisy Transformer --- axelrod/strategies/strategy_transformers.py | 99 +++---------------- .../tests/unit/test_strategy_transformers.py | 21 ++-- 2 files changed, 27 insertions(+), 93 deletions(-) diff --git a/axelrod/strategies/strategy_transformers.py b/axelrod/strategies/strategy_transformers.py index e0929909c..883261fab 100644 --- a/axelrod/strategies/strategy_transformers.py +++ b/axelrod/strategies/strategy_transformers.py @@ -4,18 +4,7 @@ strategy. Run Axelrod one tournament - -Flip Action -Forgiver -Initial Sequence -Final Sequence - -TFT -- force a repayment, return to other strategy - Noisy -RetaliateUntilApology - -Compose. Memory-depth inference @@ -121,6 +110,18 @@ def flip_wrapper(player, opponent, action): FlipTransformer = StrategyTransformerFactory(flip_wrapper, name_prefix="Flipped") +def noisy_wrapper(player, opponent, action, noise=0.05): + """Applies flip_action at the class level.""" + r = random.random() + if r < noise: + return flip_action(action) + return action + +def NoisyTransformer(noise): + return StrategyTransformerFactory(noisy_wrapper, + wrapper_args=(noise,), + name_prefix="Noisy") + def forgiver_wrapper(player, opponent, action, p): """If a strategy wants to defect, flip to cooperate with the given probability.""" @@ -171,7 +172,6 @@ def FinalTransformer(seq=None): name_prefix="Final") return transformer - # Strategy wrapper as a class example class RetaliationWrapper(object): def __init__(self): @@ -195,78 +195,3 @@ def RetailiateUntilApologyTransformer(): -#if __name__ == "__main__": - ## Cooperator to Defector - #p1 = axelrod.Cooperator() - #p2 = FlipTransformer(axelrod.Cooperator)() # Defector - #print p1, p2 - #print simulate_play(p1, p2) - #print simulate_play(p1, p2) - - ## Test that cloning preserves transform - #p3 = p2.clone() - #print simulate_play(p1, p3) - #print simulate_play(p1, p3) - - ## Forgiving example - #p1 = ForgiverTransformer(axelrod.Defector)() - #print simulate_play(p1, p2) - #print simulate_play(p1, p2) - #print simulate_play(p1, p2) - #print simulate_play(p1, p2) - #print simulate_play(p1, p2) - #print simulate_play(p1, p2) - - ## Difference between Alternator and CyclerCD - #p1 = axelrod.Cycler(cycle="CD") - #p2 = FlipTransformer(axelrod.Cycler)(cycle="CD") - #for _ in range(5): - #p1.play(p2) - #print p1.history, p2.history - - ## Initial play transformer - #p1 = axelrod.Cooperator() - #p2 = InitialTransformer()(axelrod.Cooperator)() - - #for _ in range(6): - #p1.play(p2) - #print p1.history, p2.history - - ## Final Play transformer - #p1 = FinalTransformer()(axelrod.Cooperator)() - #p2 = axelrod.Cooperator() - #p1.tournament_attributes["length"] = 6 - - #for _ in range(6): - #p1.play(p2) - #print p1.history, p2.history - - - ## Composition - #cls1 = InitialTransformer()(axelrod.Cooperator) - #cls2 = FinalTransformer()(cls1) - #p1 = cls2() - - #p2 = axelrod.Cooperator() - #p1.tournament_attributes["length"] = 8 - - #for _ in range(8): - #p1.play(p2) - #print p1.history - - ## Composition - ##cls2 = FinalTransformer()(InitialTransformer(axelrod.Cooperator)) - ##p1 = cls2() - - #cls1 = InitialTransformer([D, D, D])(axelrod.Cooperator) - #cls2 = FinalTransformer([D, D])(cls1) - #p1 = cls2() - - #p2 = axelrod.Cooperator() - #p1.tournament_attributes["length"] = 8 - - #for _ in range(8): - #p1.play(p2) - #print p1.history - - diff --git a/axelrod/tests/unit/test_strategy_transformers.py b/axelrod/tests/unit/test_strategy_transformers.py index 5768961e0..eb254c590 100644 --- a/axelrod/tests/unit/test_strategy_transformers.py +++ b/axelrod/tests/unit/test_strategy_transformers.py @@ -18,6 +18,14 @@ def test_naming(self): self.assertEqual(cls.__name__, "FlippedCooperator") self.assertEqual(p1.name, "Flipped Cooperator") + def test_cloning(self): + # Test that cloning preserves transform + p1 = axelrod.Cooperator() + p2 = FlipTransformer(axelrod.Cooperator)() # Defector + p3 = p2.clone() + self.assertEqual(simulate_play(p1, p3), (C, D)) + self.assertEqual(simulate_play(p1, p3), (C, D)) + def test_flip_transformer(self): # Cooperator to Defector p1 = axelrod.Cooperator() @@ -26,13 +34,14 @@ def test_flip_transformer(self): self.assertEqual(simulate_play(p1, p2), (C, D)) self.assertEqual(simulate_play(p1, p2), (C, D)) - def test_cloning(self): - # Test that cloning preserves transform + def test_noisy_transformer(self): + random.seed(5) + # Cooperator to Defector p1 = axelrod.Cooperator() - p2 = FlipTransformer(axelrod.Cooperator)() # Defector - p3 = p2.clone() - self.assertEqual(simulate_play(p1, p3), (C, D)) - self.assertEqual(simulate_play(p1, p3), (C, D)) + p2 = NoisyTransformer(0.5)(axelrod.Cooperator)() # Defector + for _ in range(10): + p1.play(p2) + self.assertEqual(p2.history, [C, C, C, C, C, C, D, D, C, C]) def test_forgiving(self): random.seed(10) From e87c7a017e9b81fa29210fde9769241c5503fa59 Mon Sep 17 00:00:00 2001 From: Marc Harper Date: Thu, 22 Oct 2015 21:44:18 -0700 Subject: [PATCH 04/13] History Tracking Wrapper --- axelrod/strategies/strategy_transformers.py | 24 +++++++------ .../tests/unit/test_strategy_transformers.py | 34 ++++++++++++++----- 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/axelrod/strategies/strategy_transformers.py b/axelrod/strategies/strategy_transformers.py index 883261fab..bb8078d84 100644 --- a/axelrod/strategies/strategy_transformers.py +++ b/axelrod/strategies/strategy_transformers.py @@ -3,15 +3,7 @@ Strategy Transformers -- class decorators that transform the behavior of any strategy. -Run Axelrod one tournament -Noisy - -Memory-depth inference - -As Decorators - -Meta Strategies - +See the various Meta strategies for another type of transformation. """ import random @@ -59,6 +51,8 @@ def StrategyTransformerFactory(strategy_wrapper, wrapper_args=(), wrapper_kwargs ---------- strategy_wrapper: function A function of the form `strategy_wrapper(player, opponent, proposed_action, *args, **kwargs)` + Can also use a class that implements + def __call__(self, player, opponent, action) wrapper_args: tuple Any arguments to pass to the wrapper wrapper_kwargs: dict @@ -174,6 +168,8 @@ def FinalTransformer(seq=None): # Strategy wrapper as a class example class RetaliationWrapper(object): + """Enforces the TFT rule that the opponent pay back a defection with a + cooperation for the player to stop defecting.""" def __init__(self): self.is_retaliating = False @@ -193,5 +189,13 @@ def RetailiateUntilApologyTransformer(): strategy_wrapper = RetaliationWrapper() return StrategyTransformerFactory(strategy_wrapper, name_prefix="RUA") +def history_track_wrapper(player, opponent, action): + # Record action + try: + player._recorded_history.append(action) + except AttributeError: + player._recorded_history = [action] + return action - +TrackHistoryTransformer = StrategyTransformerFactory(history_track_wrapper, + name_prefix="HistoryTracking") diff --git a/axelrod/tests/unit/test_strategy_transformers.py b/axelrod/tests/unit/test_strategy_transformers.py index eb254c590..ca9296f29 100644 --- a/axelrod/tests/unit/test_strategy_transformers.py +++ b/axelrod/tests/unit/test_strategy_transformers.py @@ -4,7 +4,8 @@ import axelrod from axelrod.strategies.strategy_transformers import * -from .test_titfortat import TestTitForTat +import test_titfortat +import test_cooperator C, D = axelrod.Actions.C, axelrod.Actions.D @@ -91,6 +92,13 @@ def test_final_transformer2(self): p1.play(p2) self.assertEqual(p2.history, [C, C, C, C, C, C]) + def test_history_track(self): + p1 = axelrod.Cooperator() + p2 = TrackHistoryTransformer(axelrod.Random)() + for _ in range(6): + p1.play(p2) + self.assertEqual(p2.history, p2._recorded_history) + def test_composition(self): cls1 = InitialTransformer()(axelrod.Cooperator) cls2 = FinalTransformer()(cls1) @@ -111,16 +119,24 @@ def test_composition(self): # Test that RUA(Cooperator) is the same as TitForTat -# Reusing the TFT tests +# reusing the TFT tests. Since TFT is completely specified by its tests, +# this is actually a proof that they are equal! RUA = RetailiateUntilApologyTransformer() TFT = RUA(axelrod.Cooperator) +TFT.name = "Tit For Tat" +TFT.classifier["memory_depth"] = 1 + +class TestRUAisTFT(test_titfortat.TestTitForTat): + # This runs the 7 TFT tests when unittest is invoked + player = TFT + + +# Test that FlipTransformer(Defector) == Cooperator +Cooperator = FlipTransformer(axelrod.Defector) +Cooperator.name = "Cooperator" -class TestRUA(TestTitForTat): - @classmethod - def setUpClass(cls): - player = TFT - name = "RUA Cooperator" - # Skips testing this, which is zero but should be 1 - #expected_classifier["memory_depth"] = 1 +class TestFlipDefector(test_cooperator.TestCooperator): + # This runs the 7 TFT tests when unittest is invoked + player = Cooperator From 23ce4f9f5faef1e5255e70456f9c0430ce312be9 Mon Sep 17 00:00:00 2001 From: Marc Harper Date: Thu, 22 Oct 2015 21:52:44 -0700 Subject: [PATCH 05/13] Modify BackStabber to use FinalTransformer Decorator --- axelrod/strategies/axelrod_tournaments.py | 1 - axelrod/strategies/backstabber.py | 5 +++-- axelrod/strategies/strategy_transformers.py | 12 ++++++++---- axelrod/tests/unit/test_strategy_transformers.py | 3 --- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/axelrod/strategies/axelrod_tournaments.py b/axelrod/strategies/axelrod_tournaments.py index 59752d480..be8aa0c47 100644 --- a/axelrod/strategies/axelrod_tournaments.py +++ b/axelrod/strategies/axelrod_tournaments.py @@ -10,7 +10,6 @@ C, D = Actions.C, Actions.D - ## First Tournament class Davis(Player): diff --git a/axelrod/strategies/backstabber.py b/axelrod/strategies/backstabber.py index 3094c9178..60f94091a 100644 --- a/axelrod/strategies/backstabber.py +++ b/axelrod/strategies/backstabber.py @@ -1,7 +1,10 @@ from axelrod import Player, Actions +from strategy_transformers import FinalTransformer + C, D = Actions.C, Actions.D +@FinalTransformer([D, D, D]) # End with three defections class BackStabber(Player): """ Forgives the first 3 defections but on the fourth @@ -20,8 +23,6 @@ class BackStabber(Player): def strategy(self, opponent): if not opponent.history: return C - if len(opponent.history) > (self.tournament_attributes['length'] - 3): - return D if opponent.defections > 3: return D return C diff --git a/axelrod/strategies/strategy_transformers.py b/axelrod/strategies/strategy_transformers.py index bb8078d84..dabdf087e 100644 --- a/axelrod/strategies/strategy_transformers.py +++ b/axelrod/strategies/strategy_transformers.py @@ -89,9 +89,13 @@ def strategy(self, opponent): # Define a new class and wrap the strategy method # Modify the PlayerClass name - new_class_name = name_prefix + PlayerClass.__name__ - # Modify the Player name (class variable inherited from Player) - name = name_prefix + ' ' + PlayerClass.name + new_class_name = PlayerClass.__name__ + name = PlayerClass.name + if name_prefix: + # Modify the Player name (class variable inherited from Player) + new_class_name = name_prefix + PlayerClass.__name__ + # Modify the Player name (class variable inherited from Player) + name = name_prefix + ' ' + PlayerClass.name # Dynamically create the new class new_class = type(new_class_name, (PlayerClass,), {"name": name, "strategy": strategy}) @@ -163,7 +167,7 @@ def FinalTransformer(seq=None): if not seq: seq = [D] * 3 transformer = StrategyTransformerFactory(final_sequence, wrapper_args=(seq,), - name_prefix="Final") + name_prefix="") return transformer # Strategy wrapper as a class example diff --git a/axelrod/tests/unit/test_strategy_transformers.py b/axelrod/tests/unit/test_strategy_transformers.py index ca9296f29..a544d45df 100644 --- a/axelrod/tests/unit/test_strategy_transformers.py +++ b/axelrod/tests/unit/test_strategy_transformers.py @@ -130,7 +130,6 @@ class TestRUAisTFT(test_titfortat.TestTitForTat): # This runs the 7 TFT tests when unittest is invoked player = TFT - # Test that FlipTransformer(Defector) == Cooperator Cooperator = FlipTransformer(axelrod.Defector) Cooperator.name = "Cooperator" @@ -138,5 +137,3 @@ class TestRUAisTFT(test_titfortat.TestTitForTat): class TestFlipDefector(test_cooperator.TestCooperator): # This runs the 7 TFT tests when unittest is invoked player = Cooperator - - From 7c6bad82c64d59a668cb962ff3de315437c1c552 Mon Sep 17 00:00:00 2001 From: Marc Harper Date: Thu, 22 Oct 2015 22:28:12 -0700 Subject: [PATCH 06/13] Update tests --- axelrod/strategies/__init__.py | 2 +- axelrod/strategies/averagecopier.py | 2 - axelrod/strategies/backstabber.py | 2 +- axelrod/strategies/strategy_transformers.py | 9 ++-- .../tests/unit/test_strategy_transformers.py | 41 ++++++++++--------- 5 files changed, 29 insertions(+), 27 deletions(-) diff --git a/axelrod/strategies/__init__.py b/axelrod/strategies/__init__.py index c3c298345..42c1af0c3 100644 --- a/axelrod/strategies/__init__.py +++ b/axelrod/strategies/__init__.py @@ -1,7 +1,7 @@ from ..player import is_basic, obey_axelrod from ._strategies import * -import strategy_transformers +from . import strategy_transformers # `from ._strategies import *` import the collection `strategies` # Now import the Meta strategies. This cannot be done in _strategies diff --git a/axelrod/strategies/averagecopier.py b/axelrod/strategies/averagecopier.py index a77072d1e..944e240e0 100644 --- a/axelrod/strategies/averagecopier.py +++ b/axelrod/strategies/averagecopier.py @@ -1,9 +1,7 @@ - from axelrod import Player, random_choice, Actions import random - class AverageCopier(Player): """ The player will cooperate with probability p if the opponent's cooperation ratio is p. diff --git a/axelrod/strategies/backstabber.py b/axelrod/strategies/backstabber.py index 60f94091a..d034c78fd 100644 --- a/axelrod/strategies/backstabber.py +++ b/axelrod/strategies/backstabber.py @@ -1,6 +1,6 @@ from axelrod import Player, Actions -from strategy_transformers import FinalTransformer +from .strategy_transformers import FinalTransformer C, D = Actions.C, Actions.D diff --git a/axelrod/strategies/strategy_transformers.py b/axelrod/strategies/strategy_transformers.py index dabdf087e..9a75a8949 100644 --- a/axelrod/strategies/strategy_transformers.py +++ b/axelrod/strategies/strategy_transformers.py @@ -13,10 +13,11 @@ from axelrod import flip_action, random_choice, simulate_play, Actions C, D = Actions.C, Actions.D -# Note: the history is overwritten with the modified history -# Just like in the Noisy case +# Note: After a transformation is applied, +# the player's history is overwritten with the modified history +# just like in the noisy tournament case # This can lead to unexpected behavior, such as when -# Flip transform is applied to Alternator +# FlipTransform is applied to Alternator def generic_strategy_wrapper(player, opponent, proposed_action, *args, **kwargs): @@ -79,7 +80,7 @@ def decorate(PlayerClass): # with `strategy_wrapper` def strategy(self, opponent): # Is the original strategy method a static method? - if isinstance(PlayerClass.strategy, FunctionType): + if isinstance(PlayerClass.__dict__["strategy"], staticmethod): proposed_action = PlayerClass.strategy(opponent) else: proposed_action = PlayerClass.strategy(self, opponent) diff --git a/axelrod/tests/unit/test_strategy_transformers.py b/axelrod/tests/unit/test_strategy_transformers.py index a544d45df..5d6b20f34 100644 --- a/axelrod/tests/unit/test_strategy_transformers.py +++ b/axelrod/tests/unit/test_strategy_transformers.py @@ -1,11 +1,8 @@ - import random import unittest import axelrod from axelrod.strategies.strategy_transformers import * -import test_titfortat -import test_cooperator C, D = axelrod.Actions.C, axelrod.Actions.D @@ -118,22 +115,28 @@ def test_composition(self): self.assertEqual(p1.history, [D, D, D, C, C, D, D, D]) -# Test that RUA(Cooperator) is the same as TitForTat -# reusing the TFT tests. Since TFT is completely specified by its tests, -# this is actually a proof that they are equal! -RUA = RetailiateUntilApologyTransformer() -TFT = RUA(axelrod.Cooperator) -TFT.name = "Tit For Tat" -TFT.classifier["memory_depth"] = 1 +## Test that RUA(Cooperator) is the same as TitForTat +## reusing the TFT tests. Since TFT is completely specified by its tests, +## this is actually a proof that they are equal! +## However because classifier is a class variable until after instantiation +## this alters Cooperator's class variable, and causes its test to fail +## So for now this is commented out. + +#RUA = RetailiateUntilApologyTransformer() +#TFT = RUA(axelrod.Cooperator) +#TFT.name = "Tit For Tat" +#TFT.classifier["memory_depth"] = 1 + +#class TestRUAisTFT(test_titfortat.TestTitForTat): + ## This runs the 7 TFT tests when unittest is invoked + #player = TFT -class TestRUAisTFT(test_titfortat.TestTitForTat): - # This runs the 7 TFT tests when unittest is invoked - player = TFT +## Test that FlipTransformer(Defector) == Cooperator +#Cooperator2 = FlipTransformer(axelrod.Defector) +#Cooperator2.name = "Cooperator" +#Cooperator2.classifier["memory_depth"] = 0 -# Test that FlipTransformer(Defector) == Cooperator -Cooperator = FlipTransformer(axelrod.Defector) -Cooperator.name = "Cooperator" -class TestFlipDefector(test_cooperator.TestCooperator): - # This runs the 7 TFT tests when unittest is invoked - player = Cooperator +#class TestFlipDefector(test_cooperator.TestCooperator): + ## This runs the 7 TFT tests when unittest is invoked + #player = Cooperator2 From b2e988bf5f0bf27f38160d5e3de87be6ea1ff2ea Mon Sep 17 00:00:00 2001 From: Marc Harper Date: Thu, 22 Oct 2015 23:25:24 -0700 Subject: [PATCH 07/13] Add in test for Retaliation transformer --- axelrod/strategies/strategy_transformers.py | 2 +- axelrod/tests/unit/test_strategy_transformers.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/axelrod/strategies/strategy_transformers.py b/axelrod/strategies/strategy_transformers.py index 9a75a8949..1d0ebaadb 100644 --- a/axelrod/strategies/strategy_transformers.py +++ b/axelrod/strategies/strategy_transformers.py @@ -181,7 +181,7 @@ def __init__(self): def __call__(self, player, opponent, action): if len(player.history) == 0: return action - if opponent.history[-1]: + if opponent.history[-1] == D: self.is_retaliating = True if self.is_retaliating: if opponent.history[-1] == C: diff --git a/axelrod/tests/unit/test_strategy_transformers.py b/axelrod/tests/unit/test_strategy_transformers.py index 5d6b20f34..2459bc881 100644 --- a/axelrod/tests/unit/test_strategy_transformers.py +++ b/axelrod/tests/unit/test_strategy_transformers.py @@ -114,6 +114,21 @@ def test_composition(self): p1.play(p2) self.assertEqual(p1.history, [D, D, D, C, C, D, D, D]) + def test_retailiation(self): + RUA = RetailiateUntilApologyTransformer() + TFT = RUA(axelrod.Cooperator) + p1 = TFT() + p2 = axelrod.Cooperator() + p1.play(p2) + p1.play(p2) + self.assertEqual(p1.history, [C, C]) + + p1 = TFT() + p2 = axelrod.Defector() + p1.play(p2) + p1.play(p2) + self.assertEqual(p1.history, [C, D]) + ## Test that RUA(Cooperator) is the same as TitForTat ## reusing the TFT tests. Since TFT is completely specified by its tests, From 486424f983c17bd4fd40153ba434283dcf0fcbb1 Mon Sep 17 00:00:00 2001 From: Marc Harper Date: Fri, 23 Oct 2015 08:35:32 -0700 Subject: [PATCH 08/13] Improve coverage --- axelrod/strategies/strategy_transformers.py | 12 +++---- .../tests/unit/test_strategy_transformers.py | 33 +++++++++++++++++-- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/axelrod/strategies/strategy_transformers.py b/axelrod/strategies/strategy_transformers.py index 1d0ebaadb..c7fa3a6dd 100644 --- a/axelrod/strategies/strategy_transformers.py +++ b/axelrod/strategies/strategy_transformers.py @@ -9,8 +9,7 @@ import random from types import FunctionType -import axelrod -from axelrod import flip_action, random_choice, simulate_play, Actions +from axelrod import flip_action, random_choice, Actions C, D = Actions.C, Actions.D # Note: After a transformation is applied, @@ -150,13 +149,10 @@ def InitialTransformer(seq=None): def final_sequence(player, opponent, action, seq): """Play the moves in `seq` first, ignoring the strategy's moves until the list is exhausted.""" - try: - length = player.tournament_attributes["length"] - except KeyError: + length = player.tournament_attributes["length"] + + if length < 0: # default is -1 return action - finally: - if length < 0: # default is -1 - return action index = length - len(player.history) if index <= len(seq): diff --git a/axelrod/tests/unit/test_strategy_transformers.py b/axelrod/tests/unit/test_strategy_transformers.py index 2459bc881..a21d8140c 100644 --- a/axelrod/tests/unit/test_strategy_transformers.py +++ b/axelrod/tests/unit/test_strategy_transformers.py @@ -2,9 +2,9 @@ import unittest import axelrod +from axelrod import simulate_play from axelrod.strategies.strategy_transformers import * - C, D = axelrod.Actions.C, axelrod.Actions.D @@ -24,6 +24,15 @@ def test_cloning(self): self.assertEqual(simulate_play(p1, p3), (C, D)) self.assertEqual(simulate_play(p1, p3), (C, D)) + def test_generic(self): + # Test that the generic wrapper does nothing + transformer = StrategyTransformerFactory(generic_strategy_wrapper) + Cooperator2 = transformer(axelrod.Cooperator) + p1 = Cooperator2() + p2 = axelrod.Cooperator() + self.assertEqual(simulate_play(p1, p2), (C, C)) + self.assertEqual(simulate_play(p1, p2), (C, C)) + def test_flip_transformer(self): # Cooperator to Defector p1 = axelrod.Cooperator() @@ -33,6 +42,7 @@ def test_flip_transformer(self): self.assertEqual(simulate_play(p1, p2), (C, D)) def test_noisy_transformer(self): + # Test Noisy transformer random.seed(5) # Cooperator to Defector p1 = axelrod.Cooperator() @@ -42,12 +52,13 @@ def test_noisy_transformer(self): self.assertEqual(p2.history, [C, C, C, C, C, C, D, D, C, C]) def test_forgiving(self): + # Test Forgiving transformer random.seed(10) - p1 = ForgiverTransformer(0.5)(axelrod.Defector)() + p1 = ForgiverTransformer(0.5)(axelrod.Alternator)() p2 = axelrod.Defector() for _ in range(10): p1.play(p2) - self.assertEqual(p1.history, [D, C, D, C, D, D, D, C, D, C]) + self.assertEqual(p1.history, [C, D, C, C, D, C, C, D, C, D]) def test_cycler(self): # Difference between Alternator and CyclerCD @@ -58,6 +69,14 @@ def test_cycler(self): self.assertEqual(p1.history, [C, D, C, D, C]) self.assertEqual(p2.history, [D, C, D, C, D]) + p1 = axelrod.Alternator() + p2 = FlipTransformer(axelrod.Alternator)() + for _ in range(5): + p1.play(p2) + self.assertEqual(p1.history, [C, D, C, D, C]) + self.assertEqual(p2.history, [D, D, D, D, D]) + + def test_initial_transformer(self): # Initial play transformer p1 = axelrod.Cooperator() @@ -129,6 +148,14 @@ def test_retailiation(self): p1.play(p2) self.assertEqual(p1.history, [C, D]) + random.seed(12) + p1 = TFT() + p2 = axelrod.Random() + for _ in range(5): + p1.play(p2) + self.assertEqual(p1.history, [C, C, D, D, C]) + + ## Test that RUA(Cooperator) is the same as TitForTat ## reusing the TFT tests. Since TFT is completely specified by its tests, From 6bc70041c32ac6540f97a2f8239c2535258dd53c Mon Sep 17 00:00:00 2001 From: Marc Harper Date: Sat, 24 Oct 2015 11:06:23 -0700 Subject: [PATCH 09/13] Backstabber should defect twice at the end if the tournament is known, plus a test to catch this --- axelrod/strategies/backstabber.py | 2 +- axelrod/tests/unit/test_backstabber.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/axelrod/strategies/backstabber.py b/axelrod/strategies/backstabber.py index d034c78fd..68e6add04 100644 --- a/axelrod/strategies/backstabber.py +++ b/axelrod/strategies/backstabber.py @@ -4,7 +4,7 @@ C, D = Actions.C, Actions.D -@FinalTransformer([D, D, D]) # End with three defections +@FinalTransformer([D, D]) # End with three defections class BackStabber(Player): """ Forgives the first 3 defections but on the fourth diff --git a/axelrod/tests/unit/test_backstabber.py b/axelrod/tests/unit/test_backstabber.py index c285da3d5..0c854d9b6 100644 --- a/axelrod/tests/unit/test_backstabber.py +++ b/axelrod/tests/unit/test_backstabber.py @@ -32,8 +32,13 @@ def test_strategy(self): tournament_length=200) # Defects on rounds 199, and 200 no matter what + self.responses_test([C] * 197 , [C] * 197, [C, D, D], + tournament_length=200) self.responses_test([C] * 198 , [C] * 198, [D, D, D], tournament_length=200) + # But only if the tournament is known + self.responses_test([C] * 198 , [C] * 198, [C, C, C], + tournament_length=-1) class TestDoubleCrosser(TestPlayer): From 3e3cb263930d2e3c2c27a61190647a4ade71441d Mon Sep 17 00:00:00 2001 From: Marc Harper Date: Sun, 25 Oct 2015 10:43:05 -0700 Subject: [PATCH 10/13] Improved docstrings for transformers and tests --- axelrod/strategies/strategy_transformers.py | 31 ++++++++++++++++--- .../tests/unit/test_strategy_transformers.py | 25 +++++++++------ 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/axelrod/strategies/strategy_transformers.py b/axelrod/strategies/strategy_transformers.py index c7fa3a6dd..de1a45aff 100644 --- a/axelrod/strategies/strategy_transformers.py +++ b/axelrod/strategies/strategy_transformers.py @@ -116,6 +116,10 @@ def noisy_wrapper(player, opponent, action, noise=0.05): return action def NoisyTransformer(noise): + """Creates a function that takes an axelrod.Player class as an argument + and alters the play of the Player in the following way. The player's + intended action is flipped with probability noise.""" + return StrategyTransformerFactory(noisy_wrapper, wrapper_args=(noise,), name_prefix="Noisy") @@ -128,11 +132,16 @@ def forgiver_wrapper(player, opponent, action, p): return C def ForgiverTransformer(p): + """Creates a function that takes an axelrod.Player class as an argument + and alters the play of the Player in the following way. The player's + defections are flipped with probability p.""" + return StrategyTransformerFactory(forgiver_wrapper, wrapper_args=(p,)) def initial_sequence(player, opponent, action, initial_seq): """Play the moves in `seq` first (must be a list), ignoring the strategy's moves until the list is exhausted.""" + index = len(player.history) if index < len(initial_seq): return initial_seq[index] @@ -140,6 +149,10 @@ def initial_sequence(player, opponent, action, initial_seq): ## Defection initially three times def InitialTransformer(seq=None): + """Creates a function that takes an axelrod.Player class as an argument + and alters the play of the Player in the following way. The player starts + with the actions in the argument seq and then proceeds to play normally.""" + if not seq: seq = [D] * 3 transformer = StrategyTransformerFactory(initial_sequence, wrapper_args=(seq,), @@ -147,8 +160,9 @@ def InitialTransformer(seq=None): return transformer def final_sequence(player, opponent, action, seq): - """Play the moves in `seq` first, ignoring the strategy's - moves until the list is exhausted.""" + """Play the moves in `seq` first, ignoring the strategy's moves until the + list is exhausted.""" + length = player.tournament_attributes["length"] if length < 0: # default is -1 @@ -159,8 +173,12 @@ def final_sequence(player, opponent, action, seq): return seq[-index] return action -# Defect on last N actions def FinalTransformer(seq=None): + """Creates a function that takes an axelrod.Player class as an argument + and alters the play of the Player in the following way. If the tournament + length is known, the play ends with the actions in the argument seq. + Otherwise the player's actions are unaltered. """ + if not seq: seq = [D] * 3 transformer = StrategyTransformerFactory(final_sequence, wrapper_args=(seq,), @@ -187,11 +205,16 @@ def __call__(self, player, opponent, action): return action def RetailiateUntilApologyTransformer(): + """Creates a function that takes an axelrod.Player class as an argument + and alters the play of the Player in the following way. If the opponent + defects, the player will retaliate with defections until the opponent + cooperates. Otherwise the player's actions are unaltered.""" + strategy_wrapper = RetaliationWrapper() return StrategyTransformerFactory(strategy_wrapper, name_prefix="RUA") def history_track_wrapper(player, opponent, action): - # Record action + """Wrapper to track a player's history in a variable `._recorded_history`.""" try: player._recorded_history.append(action) except AttributeError: diff --git a/axelrod/tests/unit/test_strategy_transformers.py b/axelrod/tests/unit/test_strategy_transformers.py index a21d8140c..37c20333d 100644 --- a/axelrod/tests/unit/test_strategy_transformers.py +++ b/axelrod/tests/unit/test_strategy_transformers.py @@ -11,13 +11,15 @@ class TestTransformers(unittest.TestCase): def test_naming(self): + """Tests that the player and class names are properly modified.""" cls = FlipTransformer(axelrod.Cooperator) p1 = cls() self.assertEqual(cls.__name__, "FlippedCooperator") self.assertEqual(p1.name, "Flipped Cooperator") def test_cloning(self): - # Test that cloning preserves transform + """Tests that Player.clone preserves the application of transformations. + """ p1 = axelrod.Cooperator() p2 = FlipTransformer(axelrod.Cooperator)() # Defector p3 = p2.clone() @@ -25,7 +27,7 @@ def test_cloning(self): self.assertEqual(simulate_play(p1, p3), (C, D)) def test_generic(self): - # Test that the generic wrapper does nothing + """Test that the generic wrapper does nothing.""" transformer = StrategyTransformerFactory(generic_strategy_wrapper) Cooperator2 = transformer(axelrod.Cooperator) p1 = Cooperator2() @@ -34,7 +36,7 @@ def test_generic(self): self.assertEqual(simulate_play(p1, p2), (C, C)) def test_flip_transformer(self): - # Cooperator to Defector + """Tests that FlipTransformer(Cooperator) == Defector.""" p1 = axelrod.Cooperator() p2 = FlipTransformer(axelrod.Cooperator)() # Defector self.assertEqual(simulate_play(p1, p2), (C, D)) @@ -42,7 +44,7 @@ def test_flip_transformer(self): self.assertEqual(simulate_play(p1, p2), (C, D)) def test_noisy_transformer(self): - # Test Noisy transformer + """Tests that the noisy transformed does flip some moves.""" random.seed(5) # Cooperator to Defector p1 = axelrod.Cooperator() @@ -52,7 +54,7 @@ def test_noisy_transformer(self): self.assertEqual(p2.history, [C, C, C, C, C, C, D, D, C, C]) def test_forgiving(self): - # Test Forgiving transformer + """Tests that the forgiving transformer flips some defections.""" random.seed(10) p1 = ForgiverTransformer(0.5)(axelrod.Alternator)() p2 = axelrod.Defector() @@ -61,6 +63,8 @@ def test_forgiving(self): self.assertEqual(p1.history, [C, D, C, C, D, C, C, D, C, D]) def test_cycler(self): + """A test that demonstrates the difference in outcomes if + FlipTransformer is applied to Alternator and CyclerCD.""" # Difference between Alternator and CyclerCD p1 = axelrod.Cycler(cycle="CD") p2 = FlipTransformer(axelrod.Cycler)(cycle="CD") @@ -76,9 +80,8 @@ def test_cycler(self): self.assertEqual(p1.history, [C, D, C, D, C]) self.assertEqual(p2.history, [D, D, D, D, D]) - def test_initial_transformer(self): - # Initial play transformer + """Tests the InitialTransformer.""" p1 = axelrod.Cooperator() p2 = InitialTransformer([D, D])(axelrod.Cooperator)() for _ in range(5): @@ -91,7 +94,8 @@ def test_initial_transformer(self): p1.play(p2) self.assertEqual(p2.history, [D, D, C, D, C]) - def test_final_transformer(self): + def test_final_transformer(self):\ + """Tests the FinalTransformer when tournament length is known.""" # Final play transformer p1 = axelrod.Cooperator() p2 = FinalTransformer([D, D, D])(axelrod.Cooperator)() @@ -101,7 +105,7 @@ def test_final_transformer(self): self.assertEqual(p2.history, [C, C, C, D, D, D]) def test_final_transformer2(self): - # Final play transformer (no tournament length) + """Tests the FinalTransformer when tournament length is not known.""" p1 = axelrod.Cooperator() p2 = FinalTransformer()(axelrod.Cooperator)() for _ in range(6): @@ -109,6 +113,7 @@ def test_final_transformer2(self): self.assertEqual(p2.history, [C, C, C, C, C, C]) def test_history_track(self): + """Tests the history tracking transformer.""" p1 = axelrod.Cooperator() p2 = TrackHistoryTransformer(axelrod.Random)() for _ in range(6): @@ -116,6 +121,7 @@ def test_history_track(self): self.assertEqual(p2.history, p2._recorded_history) def test_composition(self): + """Tests that transformations can be chained or composed.""" cls1 = InitialTransformer()(axelrod.Cooperator) cls2 = FinalTransformer()(cls1) p1 = cls2() @@ -134,6 +140,7 @@ def test_composition(self): self.assertEqual(p1.history, [D, D, D, C, C, D, D, D]) def test_retailiation(self): + """Tests the RetailiateUntilApologyTransformer.""" RUA = RetailiateUntilApologyTransformer() TFT = RUA(axelrod.Cooperator) p1 = TFT() From a2221fc35c6d12a14fbfb90c96aece9b79259a76 Mon Sep 17 00:00:00 2001 From: Marc Harper Date: Sun, 25 Oct 2015 11:08:08 -0700 Subject: [PATCH 11/13] Strategy transformers advanced tutorial --- axelrod/strategies/__init__.py | 2 - axelrod/strategies/backstabber.py | 2 +- .../{strategies => }/strategy_transformers.py | 35 ++-- .../tests/unit/test_strategy_transformers.py | 6 +- docs/tutorials/advanced/index.rst | 2 + .../advanced/strategy_transformers.rst | 187 ++++++++++++++++++ 6 files changed, 213 insertions(+), 21 deletions(-) rename axelrod/{strategies => }/strategy_transformers.py (89%) create mode 100644 docs/tutorials/advanced/strategy_transformers.rst diff --git a/axelrod/strategies/__init__.py b/axelrod/strategies/__init__.py index 42c1af0c3..b7acd82a6 100644 --- a/axelrod/strategies/__init__.py +++ b/axelrod/strategies/__init__.py @@ -1,8 +1,6 @@ from ..player import is_basic, obey_axelrod from ._strategies import * -from . import strategy_transformers - # `from ._strategies import *` import the collection `strategies` # Now import the Meta strategies. This cannot be done in _strategies # because it creates circular dependencies diff --git a/axelrod/strategies/backstabber.py b/axelrod/strategies/backstabber.py index 68e6add04..60de076f9 100644 --- a/axelrod/strategies/backstabber.py +++ b/axelrod/strategies/backstabber.py @@ -1,6 +1,6 @@ from axelrod import Player, Actions -from .strategy_transformers import FinalTransformer +from axelrod.strategy_transformers import FinalTransformer C, D = Actions.C, Actions.D diff --git a/axelrod/strategies/strategy_transformers.py b/axelrod/strategy_transformers.py similarity index 89% rename from axelrod/strategies/strategy_transformers.py rename to axelrod/strategy_transformers.py index de1a45aff..6a9191518 100644 --- a/axelrod/strategies/strategy_transformers.py +++ b/axelrod/strategy_transformers.py @@ -9,7 +9,9 @@ import random from types import FunctionType -from axelrod import flip_action, random_choice, Actions +from .actions import Actions, flip_action +from .random_ import random_choice + C, D = Actions.C, Actions.D # Note: After a transformation is applied, @@ -18,7 +20,6 @@ # This can lead to unexpected behavior, such as when # FlipTransform is applied to Alternator - def generic_strategy_wrapper(player, opponent, proposed_action, *args, **kwargs): """ Strategy wrapper functions should be of the following form. @@ -42,8 +43,8 @@ def generic_strategy_wrapper(player, opponent, proposed_action, *args, **kwargs) # This example just passes through the proposed_action return proposed_action -def StrategyTransformerFactory(strategy_wrapper, wrapper_args=(), wrapper_kwargs={}, - name_prefix="Transformed"): +def StrategyTransformerFactory(strategy_wrapper, wrapper_args=(), + wrapper_kwargs={}, name_prefix=None): """Modify an existing strategy dynamically by wrapping the strategy method with the argument `strategy_wrapper`. @@ -63,11 +64,14 @@ def __call__(self, player, opponent, action) # Create a function that applies a wrapper function to the strategy method # of a given class - def decorate(PlayerClass): + def decorate(PlayerClass, name_prefix=name_prefix): """ Parameters ---------- PlayerClass: A subclass of axelrod.Player, e.g. Cooperator + The Player Class to modify + name_prefix: str + A string to prepend to the Player and Class name Returns ------- @@ -115,14 +119,14 @@ def noisy_wrapper(player, opponent, action, noise=0.05): return flip_action(action) return action -def NoisyTransformer(noise): +def NoisyTransformer(noise, name_prefix="Noisy"): """Creates a function that takes an axelrod.Player class as an argument and alters the play of the Player in the following way. The player's intended action is flipped with probability noise.""" return StrategyTransformerFactory(noisy_wrapper, wrapper_args=(noise,), - name_prefix="Noisy") + name_prefix=name_prefix) def forgiver_wrapper(player, opponent, action, p): """If a strategy wants to defect, flip to cooperate with the given @@ -131,12 +135,13 @@ def forgiver_wrapper(player, opponent, action, p): return random_choice(p) return C -def ForgiverTransformer(p): +def ForgiverTransformer(p, name_prefix="Forgiving"): """Creates a function that takes an axelrod.Player class as an argument and alters the play of the Player in the following way. The player's defections are flipped with probability p.""" - return StrategyTransformerFactory(forgiver_wrapper, wrapper_args=(p,)) + return StrategyTransformerFactory(forgiver_wrapper, wrapper_args=(p,), + name_prefix=name_prefix) def initial_sequence(player, opponent, action, initial_seq): """Play the moves in `seq` first (must be a list), ignoring the strategy's @@ -155,8 +160,8 @@ def InitialTransformer(seq=None): if not seq: seq = [D] * 3 - transformer = StrategyTransformerFactory(initial_sequence, wrapper_args=(seq,), - name_prefix="Initial") + transformer = StrategyTransformerFactory(initial_sequence, + wrapper_args=(seq,)) return transformer def final_sequence(player, opponent, action, seq): @@ -181,8 +186,8 @@ def FinalTransformer(seq=None): if not seq: seq = [D] * 3 - transformer = StrategyTransformerFactory(final_sequence, wrapper_args=(seq,), - name_prefix="") + transformer = StrategyTransformerFactory(final_sequence, + wrapper_args=(seq,)) return transformer # Strategy wrapper as a class example @@ -204,14 +209,14 @@ def __call__(self, player, opponent, action): return D return action -def RetailiateUntilApologyTransformer(): +def RetailiateUntilApologyTransformer(name_prefix="RUA"): """Creates a function that takes an axelrod.Player class as an argument and alters the play of the Player in the following way. If the opponent defects, the player will retaliate with defections until the opponent cooperates. Otherwise the player's actions are unaltered.""" strategy_wrapper = RetaliationWrapper() - return StrategyTransformerFactory(strategy_wrapper, name_prefix="RUA") + return StrategyTransformerFactory(strategy_wrapper, name_prefix=name_prefix) def history_track_wrapper(player, opponent, action): """Wrapper to track a player's history in a variable `._recorded_history`.""" diff --git a/axelrod/tests/unit/test_strategy_transformers.py b/axelrod/tests/unit/test_strategy_transformers.py index 37c20333d..5a184e9d8 100644 --- a/axelrod/tests/unit/test_strategy_transformers.py +++ b/axelrod/tests/unit/test_strategy_transformers.py @@ -3,7 +3,7 @@ import axelrod from axelrod import simulate_play -from axelrod.strategies.strategy_transformers import * +from axelrod.strategy_transformers import * C, D = axelrod.Actions.C, axelrod.Actions.D @@ -48,7 +48,7 @@ def test_noisy_transformer(self): random.seed(5) # Cooperator to Defector p1 = axelrod.Cooperator() - p2 = NoisyTransformer(0.5)(axelrod.Cooperator)() # Defector + p2 = NoisyTransformer(0.5)(axelrod.Cooperator)() for _ in range(10): p1.play(p2) self.assertEqual(p2.history, [C, C, C, C, C, C, D, D, C, C]) @@ -94,7 +94,7 @@ def test_initial_transformer(self): p1.play(p2) self.assertEqual(p2.history, [D, D, C, D, C]) - def test_final_transformer(self):\ + def test_final_transformer(self): """Tests the FinalTransformer when tournament length is known.""" # Final play transformer p1 = axelrod.Cooperator() diff --git a/docs/tutorials/advanced/index.rst b/docs/tutorials/advanced/index.rst index ddfd52de6..127501408 100644 --- a/docs/tutorials/advanced/index.rst +++ b/docs/tutorials/advanced/index.rst @@ -8,3 +8,5 @@ Contents: .. toctree:: :maxdepth: 2 + + strategy_transformers.rst \ No newline at end of file diff --git a/docs/tutorials/advanced/strategy_transformers.rst b/docs/tutorials/advanced/strategy_transformers.rst new file mode 100644 index 000000000..8a4fd450c --- /dev/null +++ b/docs/tutorials/advanced/strategy_transformers.rst @@ -0,0 +1,187 @@ +.. _strategy_transformers: + +Strategy Transformers +===================== + +What is a Strategy Transfomer? +------------------------------ + +A strategy transformer is a function that modifies an existing strategy. For +example, :code:`FlipTransformer` takes a strategy and flips the actions from +C to D and D to C:: + + >>> import axelrod + >>> from axelrod.strategy_transformers import FlipTransformer + >>> FlippedCooperator = FlipTransformer(axelrod.Cooperator) + >>> player = FlippedCooperator() + >>> opponent = axelrod.Cooperator() + >>> player.strategy(opponent) + 'D' + >>> opponent.strategy(player) + 'C' + +Our player was switched from a :code:`Cooperator` to a :code:`Defector` when +we applied the transformer. The transformer also changed the name of the +class and player:: + + >>> player.name + 'Flipped Cooperator' + >>> FlippedCooperator.name + 'Flipped Cooperator' + +This behavor can be supressed by setting the :code:`name_prefix` argument:: + FlipTransformer = StrategyTransformerFactory(flip_wrapper, name_prefix="") + +Note carefully that the transformer returns a class, not an instance of a class. +This means that you need to use the Transformed class as you would normally to +create a new instance:: + + >>> import axelrod + >>> from axelrod.strategy_transformers import NoisyTransformer + >>> player = NoisyTransformer(0.5)(axelrod.Cooperator)() + +rather than :code:`NoisyTransformer(0.5)(axelrod.Cooperator())` or just :code:`NoisyTransformer(0.5)(axelrod.Cooperator)`. + +You can also chain together multiple transformers:: + + cls1 = FinalTransformer([D,D])(InitialTransformer([D,D])(axelrod.Cooperator)) + p1 = cls1() + +This defines a strategy that cooperates except on the first two and last two rounds. + + +Included Transformers +--------------------- + +The library includes the following transformers: + +* :code:`FlipTransformer`: Flips all actions:: + >>> import axelrod + >>> from axelrod.strategy_transformers import FlipTransformer + >>> FlippedCooperator = FlipTransformer(axelrod.Cooperator) + >>> player = FlippedCooperator() + +* :code:`NoisyTransformer(noise)`: Flips actions with probability :code:`noise`:: + >>> import axelrod + >>> from axelrod.strategy_transformers import NoisyTransformer + >>> player = NoisyTransformer(0.5)(axelrod.Cooperator)() + +* :code:`ForgiverTransformer(p)`: Flips defections with probability :code:`p`:: + >>> import axelrod + >>> from axelrod.strategy_transformers import ForgiverTransformer + >>> player = ForgiverTransformer(0.1)(axelrod.Defector)() + +* :code:`InitialTransformer(seq=None)`: First plays the moves in the sequence :code:`seq`, then plays as usual. For example, to obtain a defector that cooperates on the first two rounds:: + >>> import axelrod + >>> from axelrod.strategy_transformers import InitialTransformer + >>> player = InitialTransformer([C, C])(axelrod.Defector)() + +* :code:`FinalTransformer(seq=None)`: Ends the tournament with the moves in the sequence :code:`seq`, if the tournament_length is known. For example, to obtain a cooperator that defects on the last two rounds:: + >>> import axelrod + >>> from axelrod.strategy_transformers import FinalTransformer + >>> player = FinalTransformer([D, D])(axelrod.Cooperator)() + +* :code:`RetailiateUntilApologyTransformer()`: adds TitForTat-style retaliation:: + >>> import axelrod + >>> from axelrod.strategy_transformers import RetailiateUntilApologyTransformer + >>> RUA = RetailiateUntilApologyTransformer() + >>> TFT = RUA(axelrod.Cooperator) + +* :code:`TrackHistoryTransformer`: Tracks History internally in the :code:`Player` instance in a variable :code:`_recorded_history`. This allows a player to e.g. detect noise.:: + >>> import axelrod + >>> from axelrod.strategy_transformers import TrackHistoryTransformer + >>> player = TrackHistoryTransformer(axelrod.Random)() + +Usage as Class Decorators +------------------------- + +Transformers can also be used to decorate existing strategies. For example, +the strategy :code:`BackStabber` defects on the last two rounds. We can encode this +behavior with a transformer as a class decorator:: + + @FinalTransformer([D, D]) # End with three defections + class BackStabber(Player): + """ + Forgives the first 3 defections but on the fourth + will defect forever. Defects on the last 2 rounds unconditionally. + """ + + name = 'BackStabber' + classifier = { + 'memory_depth': float('inf'), + 'stochastic': False, + 'inspects_source': False, + 'manipulates_source': False, + 'manipulates_state': False + } + + def strategy(self, opponent): + if not opponent.history: + return C + if opponent.defections > 3: + return D + return C + + +Writing New Transformers +------------------------ + +To make a new transformer, you need to define a strategy wrapping function with +the following signature:: + + def strategy_wrapper(player, opponent, proposed_action, *args, **kwargs): + """ + Strategy wrapper functions should be of the following form. + + Parameters + ---------- + player: Player object or subclass (self) + opponent: Player object or subclass + proposed_action: an axelrod.Action, C or D + The proposed action by the wrapped strategy + proposed_action = Player.strategy(...) + args, kwargs: + Any additional arguments that you need. + + Returns + ------- + action: an axelrod.Action, C or D + + """ + + # This example just passes through the proposed_action + return proposed_action + +The proposed action will be the outcome of:: + self.strategy(player) +in the underlying class (the one that is transformed). The strategy_wrapper still +has full access to the player and the opponent objects and can have arguments. + +To make a transformer from the :code:`strategy_wrapper` function, use +:code:`StrategyTransformerFactory`, which has signature:: + def StrategyTransformerFactory(strategy_wrapper, wrapper_args=(), + wrapper_kwargs={}, name_prefix=""): + """Modify an existing strategy dynamically by wrapping the strategy + method with the argument `strategy_wrapper`. + + Parameters + ---------- + strategy_wrapper: function + A function of the form `strategy_wrapper(player, opponent, proposed_action, *args, **kwargs)` + Can also use a class that implements + def __call__(self, player, opponent, action) + wrapper_args: tuple + Any arguments to pass to the wrapper + wrapper_kwargs: dict + Any keyword arguments to pass to the wrapper + name_prefix: string, "Transformed " + A string to prepend to the strategy and class name + """ + +So we use :code:`StrategyTransformerFactory` with :code:`strategy_wrapper`:: + + TransformedClass = StrategyTransformerFactory(generic_strategy_wrapper) + Cooperator2 = TransformedClass(axelrod.Cooperator) + +For more examples, see :code:`axelrod/strategy_transformers.py`. + From 9e92a19e06e337ca75fb781b44ce984def36e6dc Mon Sep 17 00:00:00 2001 From: Marc Harper Date: Sun, 25 Oct 2015 14:57:27 -0700 Subject: [PATCH 12/13] Remove comment to prevent merge conflict with #378 --- axelrod/strategies/axelrod_tournaments.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/axelrod/strategies/axelrod_tournaments.py b/axelrod/strategies/axelrod_tournaments.py index be8aa0c47..c0d6d7bee 100644 --- a/axelrod/strategies/axelrod_tournaments.py +++ b/axelrod/strategies/axelrod_tournaments.py @@ -10,8 +10,6 @@ C, D = Actions.C, Actions.D -## First Tournament - class Davis(Player): """A player starts by cooperating for 10 rounds then plays Grudger, defecting if at any point the opponent has defected.""" From 62f684d8bd9029722b17e56e77a15f0945969e61 Mon Sep 17 00:00:00 2001 From: Marc Harper Date: Mon, 26 Oct 2015 11:13:30 -0700 Subject: [PATCH 13/13] Blank lines for tutorial --- docs/tutorials/advanced/strategy_transformers.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/tutorials/advanced/strategy_transformers.rst b/docs/tutorials/advanced/strategy_transformers.rst index 8a4fd450c..0105d7aa3 100644 --- a/docs/tutorials/advanced/strategy_transformers.rst +++ b/docs/tutorials/advanced/strategy_transformers.rst @@ -30,6 +30,7 @@ class and player:: 'Flipped Cooperator' This behavor can be supressed by setting the :code:`name_prefix` argument:: + FlipTransformer = StrategyTransformerFactory(flip_wrapper, name_prefix="") Note carefully that the transformer returns a class, not an instance of a class. @@ -56,38 +57,45 @@ Included Transformers The library includes the following transformers: * :code:`FlipTransformer`: Flips all actions:: + >>> import axelrod >>> from axelrod.strategy_transformers import FlipTransformer >>> FlippedCooperator = FlipTransformer(axelrod.Cooperator) >>> player = FlippedCooperator() * :code:`NoisyTransformer(noise)`: Flips actions with probability :code:`noise`:: + >>> import axelrod >>> from axelrod.strategy_transformers import NoisyTransformer >>> player = NoisyTransformer(0.5)(axelrod.Cooperator)() * :code:`ForgiverTransformer(p)`: Flips defections with probability :code:`p`:: + >>> import axelrod >>> from axelrod.strategy_transformers import ForgiverTransformer >>> player = ForgiverTransformer(0.1)(axelrod.Defector)() * :code:`InitialTransformer(seq=None)`: First plays the moves in the sequence :code:`seq`, then plays as usual. For example, to obtain a defector that cooperates on the first two rounds:: + >>> import axelrod >>> from axelrod.strategy_transformers import InitialTransformer >>> player = InitialTransformer([C, C])(axelrod.Defector)() * :code:`FinalTransformer(seq=None)`: Ends the tournament with the moves in the sequence :code:`seq`, if the tournament_length is known. For example, to obtain a cooperator that defects on the last two rounds:: + >>> import axelrod >>> from axelrod.strategy_transformers import FinalTransformer >>> player = FinalTransformer([D, D])(axelrod.Cooperator)() * :code:`RetailiateUntilApologyTransformer()`: adds TitForTat-style retaliation:: + >>> import axelrod >>> from axelrod.strategy_transformers import RetailiateUntilApologyTransformer >>> RUA = RetailiateUntilApologyTransformer() >>> TFT = RUA(axelrod.Cooperator) * :code:`TrackHistoryTransformer`: Tracks History internally in the :code:`Player` instance in a variable :code:`_recorded_history`. This allows a player to e.g. detect noise.:: + >>> import axelrod >>> from axelrod.strategy_transformers import TrackHistoryTransformer >>> player = TrackHistoryTransformer(axelrod.Random)() @@ -159,6 +167,7 @@ has full access to the player and the opponent objects and can have arguments. To make a transformer from the :code:`strategy_wrapper` function, use :code:`StrategyTransformerFactory`, which has signature:: + def StrategyTransformerFactory(strategy_wrapper, wrapper_args=(), wrapper_kwargs={}, name_prefix=""): """Modify an existing strategy dynamically by wrapping the strategy @@ -184,4 +193,3 @@ So we use :code:`StrategyTransformerFactory` with :code:`strategy_wrapper`:: Cooperator2 = TransformedClass(axelrod.Cooperator) For more examples, see :code:`axelrod/strategy_transformers.py`. -