From ce9c4ed957c9b7fc65a9d6352d30faac40427408 Mon Sep 17 00:00:00 2001 From: Marc Harper Date: Mon, 9 Jan 2017 22:01:37 -0800 Subject: [PATCH] Update cycle detection to be more efficient (#809) * Update cycle detection to be more efficient * PEP8 spacing fix --- axelrod/_strategy_utils.py | 14 ++++--- axelrod/strategies/hunter.py | 61 +++++++++++++++++++++---------- axelrod/strategies/memoryone.py | 1 + axelrod/tests/unit/test_hunter.py | 38 +++++++++++++++---- axelrod/tests/unit/test_prober.py | 2 +- 5 files changed, 83 insertions(+), 33 deletions(-) diff --git a/axelrod/_strategy_utils.py b/axelrod/_strategy_utils.py index 3ee876924..72dcaab69 100644 --- a/axelrod/_strategy_utils.py +++ b/axelrod/_strategy_utils.py @@ -10,7 +10,7 @@ C, D = Actions.C, Actions.D -def detect_cycle(history, min_size=1, offset=0): +def detect_cycle(history, min_size=1, max_size=12, offset=0): """Detects cycles in the sequence history. Mainly used by hunter strategies. @@ -21,18 +21,22 @@ def detect_cycle(history, min_size=1, offset=0): The sequence to look for cycles within min_size: int, 1 The minimum length of the cycle + max_size: int, 12 offset: int, 0 The amount of history to skip initially """ - history_tail = history[-offset:] - for i in range(min_size, len(history_tail) // 2): + history_tail = history[offset:] + max_ = min(len(history_tail) // 2, max_size) + for i in range(min_size, max_): + has_cycle = True cycle = tuple(history_tail[:i]) for j, elem in enumerate(history_tail): if elem != cycle[j % len(cycle)]: + has_cycle = False break - if j == len(history_tail) - 1: + if has_cycle and (j == len(history_tail) - 1): # We made it to the end, is the cycle itself a cycle? - # I.E. CCC is not ok as cycle if min_size is really 2 + # E.G. CCC is not ok as cycle if min_size is really 2 # Since this is the same as C return cycle return None diff --git a/axelrod/strategies/hunter.py b/axelrod/strategies/hunter.py index 7376fb52f..7de9741d8 100644 --- a/axelrod/strategies/hunter.py +++ b/axelrod/strategies/hunter.py @@ -44,6 +44,13 @@ def strategy(self, opponent): return C +def is_alternator(history): + for i in range(len(history) - 1): + if history[i] == history[i + 1]: + return False + return True + + class AlternatorHunter(Player): """A player who hunts for alternators.""" @@ -58,12 +65,24 @@ class AlternatorHunter(Player): 'manipulates_state': False } + def __init__(self): + Player.__init__(self) + self.is_alt = False + def strategy(self, opponent): - oh = opponent.history - if len(self.history) >= 6 and all([oh[i] != oh[i+1] for i in range(len(oh)-1)]): + if len(opponent.history) < 6: + return C + if len(self.history) == 6: + if is_alternator(opponent.history): + self.is_alt = True + if self.is_alt: return D return C + def reset(self): + Player.reset(self) + self.is_alt = False + class CycleHunter(Player): """Hunts strategies that play cyclically, like any of the Cyclers, @@ -80,36 +99,40 @@ class CycleHunter(Player): 'manipulates_state': False } - @staticmethod - def strategy(opponent): - cycle = detect_cycle(opponent.history, min_size=2) + def __init__(self): + Player.__init__(self) + self.cycle = None + + def strategy(self, opponent): + if self.cycle: + return D + cycle = detect_cycle(opponent.history, min_size=3) if cycle: if len(set(cycle)) > 1: + self.cycle = cycle return D return C + def reset(self): + Player.reset(self) + self.cycle = None -class EventualCycleHunter(Player): - """Hunts strategies that eventually play cyclically""" + +class EventualCycleHunter(CycleHunter): + """Hunts strategies that eventually play cyclically.""" name = 'Eventual Cycle Hunter' - classifier = { - 'memory_depth': float('inf'), # Long memory - 'stochastic': False, - 'makes_use_of': set(), - 'long_run_time': False, - 'inspects_source': False, - 'manipulates_source': False, - 'manipulates_state': False - } - @staticmethod - def strategy(opponent): + def strategy(self, opponent): if len(opponent.history) < 10: return C if len(opponent.history) == opponent.cooperations: return C - if detect_cycle(opponent.history, offset=15): + if len(opponent.history) % 10 == 0: + # recheck + self.cycle = detect_cycle(opponent.history, offset=10, + min_size=3) + if self.cycle: return D else: return C diff --git a/axelrod/strategies/memoryone.py b/axelrod/strategies/memoryone.py index 3df1b9ffd..6f3c568fe 100644 --- a/axelrod/strategies/memoryone.py +++ b/axelrod/strategies/memoryone.py @@ -3,6 +3,7 @@ C, D = Actions.C, Actions.D + class MemoryOnePlayer(Player): """Uses a four-vector for strategies based on the last round of play, (P(C|CC), P(C|CD), P(C|DC), P(C|DD)), defaults to Win-Stay Lose-Shift. diff --git a/axelrod/tests/unit/test_hunter.py b/axelrod/tests/unit/test_hunter.py index 44105709f..5bb16f048 100644 --- a/axelrod/tests/unit/test_hunter.py +++ b/axelrod/tests/unit/test_hunter.py @@ -89,12 +89,20 @@ class TestAlternatorHunter(TestPlayer): def test_strategy(self): self.first_play_test(C) - self.responses_test([C] * 2, [C, D], [C]) - self.responses_test([C] * 3, [C, D, C], [C]) - self.responses_test([C] * 4, [C, D] * 2, [C]) - self.responses_test([C] * 5, [C, D] * 2 + [C], [C]) - self.responses_test([C] * 6, [C, D] * 3, [D]) - self.responses_test([C] * 7, [C, D] * 3 + [C], [D]) + self.responses_test([C] * 2, [C, D], [C], attrs={'is_alt': False}) + self.responses_test([C] * 3, [C, D, C], [C], attrs={'is_alt': False}) + self.responses_test([C] * 4, [C, D] * 2, [C], attrs={'is_alt': False}) + self.responses_test([C] * 5, [C, D] * 2 + [C], [C], + attrs={'is_alt': False}) + self.responses_test([C] * 6, [C, D] * 3, [D], attrs={'is_alt': True}) + self.responses_test([C] * 7, [C, D] * 3 + [C], [D], + attrs={'is_alt': True}) + + def test_reset_attr(self): + p = self.player() + p.is_alt = True + p.reset() + self.assertFalse(p.is_alt) class TestCycleHunter(TestPlayer): @@ -122,6 +130,7 @@ def test_strategy(self): player.play(opponent) self.assertEqual(player.history[-1], D) # Test against non-cyclers + axelrod.seed(40) for opponent in [axelrod.Random(), axelrod.AntiCycler(), axelrod.Cooperator(), axelrod.Defector()]: player.reset() @@ -129,11 +138,17 @@ def test_strategy(self): player.play(opponent) self.assertEqual(player.history[-1], C) + def test_reset_attr(self): + p = self.player() + p.cycle = "CCDDCD" + p.reset() + self.assertEqual(p.cycle, None) + class TestEventualCycleHunter(TestPlayer): - name = "Cycle Hunter" - player = axelrod.CycleHunter + name = "Eventual Cycle Hunter" + player = axelrod.EventualCycleHunter expected_classifier = { 'memory_depth': float('inf'), # Long memory 'stochastic': False, @@ -155,6 +170,7 @@ def test_strategy(self): player.play(opponent) self.assertEqual(player.history[-1], D) # Test against non-cyclers and cooperators + axelrod.seed(43) for opponent in [axelrod.Random(), axelrod.AntiCycler(), axelrod.DoubleCrosser(), axelrod.Cooperator()]: player.reset() @@ -162,6 +178,12 @@ def test_strategy(self): player.play(opponent) self.assertEqual(player.history[-1], C) + def test_reset_attr(self): + p = self.player() + p.cycle = "CCDDCD" + p.reset() + self.assertEqual(p.cycle, None) + class TestMathConstantHunter(TestPlayer): diff --git a/axelrod/tests/unit/test_prober.py b/axelrod/tests/unit/test_prober.py index f7e7fc9e4..dc2864cd5 100644 --- a/axelrod/tests/unit/test_prober.py +++ b/axelrod/tests/unit/test_prober.py @@ -299,7 +299,7 @@ def test_remorse(self): opponent = axelrod.Cooperator() test_responses(self, player, opponent, [C], [C], [C], - random_seed=0, attrs={'probing': False}) + random_seed=3, attrs={'probing': False}) test_responses(self, player, opponent, [C], [C], [D], random_seed=1, attrs={'probing': True})