Skip to content

Update cycle detection to be more efficient #809

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 10, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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