Skip to content

Commit

Permalink
Update cycle detection to be more efficient (#809)
Browse files Browse the repository at this point in the history
* Update cycle detection to be more efficient
* PEP8 spacing fix
  • Loading branch information
marcharper authored Jan 10, 2017
1 parent deaba6b commit ce9c4ed
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 33 deletions.
14 changes: 9 additions & 5 deletions axelrod/_strategy_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
61 changes: 42 additions & 19 deletions axelrod/strategies/hunter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand All @@ -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,
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions axelrod/strategies/memoryone.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
38 changes: 30 additions & 8 deletions axelrod/tests/unit/test_hunter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -122,18 +130,25 @@ 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()
for i in range(30):
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,
Expand All @@ -155,13 +170,20 @@ 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()
for i in range(50):
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):

Expand Down
2 changes: 1 addition & 1 deletion axelrod/tests/unit/test_prober.py
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down

0 comments on commit ce9c4ed

Please sign in to comment.