Skip to content

Commit c202f7f

Browse files
committed
Merge pull request #344 from marcharper/hunters
New Strategies -- CycleHunter and multiple Meta strategies
2 parents 9f07fd7 + 38da27a commit c202f7f

File tree

10 files changed

+415
-97
lines changed

10 files changed

+415
-97
lines changed

axelrod/strategies/__init__.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,16 @@
55
# Now import the Meta strategies. This cannot be done in _strategies
66
# because it creates circular dependencies
77

8-
from .meta import MetaMajority, MetaMinority, MetaWinner, MetaHunter
9-
strategies.extend((MetaHunter, MetaMajority, MetaMinority, MetaWinner))
8+
from .meta import (
9+
MetaPlayer, MetaMajority, MetaMinority, MetaWinner, MetaHunter,
10+
MetaMajorityMemoryOne, MetaWinnerMemoryOne, MetaMajorityFiniteMemory,
11+
MetaWinnerFiniteMemory, MetaMajorityLongMemory, MetaWinnerLongMemory
12+
)
13+
14+
strategies.extend((MetaHunter, MetaMajority, MetaMinority, MetaWinner,
15+
MetaMajorityMemoryOne, MetaWinnerMemoryOne,
16+
MetaMajorityFiniteMemory, MetaWinnerFiniteMemory,
17+
MetaMajorityLongMemory, MetaWinnerLongMemory))
1018

1119
# Distinguished strategy collections in addition to
1220
# `strategies` from _strategies.py

axelrod/strategies/_strategies.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
from .grudger import Grudger, ForgetfulGrudger, OppositeGrudger, Aggravater
2020
from .grumpy import Grumpy
2121
from .hunter import (
22-
DefectorHunter, CooperatorHunter, AlternatorHunter, MathConstantHunter,
23-
RandomHunter)
22+
DefectorHunter, CooperatorHunter, CycleHunter, AlternatorHunter,
23+
MathConstantHunter, RandomHunter, EventualCycleHunter)
2424
from .inverse import Inverse
2525
from .mathematicalconstants import Golden, Pi, e
2626
from .memoryone import (
@@ -59,6 +59,7 @@
5959
Champion,
6060
Cooperator,
6161
CooperatorHunter,
62+
CycleHunter,
6263
CyclerCCCCCD,
6364
CyclerCCCD,
6465
CyclerCCD,
@@ -68,6 +69,7 @@
6869
DefectorHunter,
6970
DoubleCrosser,
7071
Eatherley,
72+
EventualCycleHunter,
7173
Feld,
7274
FoolMeForever,
7375
FoolMeOnce,

axelrod/strategies/calculator.py

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from axelrod import Player
44
from .axelrod_tournaments import Joss
5-
5+
from .hunter import detect_cycle
66

77
class Calculator(Player):
88
"""
@@ -23,20 +23,10 @@ def __init__(self):
2323
Player.__init__(self)
2424
self.joss_instance = Joss()
2525

26-
@staticmethod
27-
def detect_cycle(history):
28-
"""Detects if there is a cycle in the opponent's history."""
29-
for i in range(len(history) // 2):
30-
cycle = itertools.cycle(history[0: i + 1])
31-
cycle_list = list(itertools.islice(cycle, 0, len(history)))
32-
if list(history) == cycle_list:
33-
return True
34-
return False
35-
3626
def strategy(self, opponent):
3727
turn = len(self.history)
3828
if turn == 20:
39-
self.cycle = self.detect_cycle(opponent.history)
29+
self.cycle = detect_cycle(opponent.history)
4030
return self.extended_strategy(opponent)
4131
if turn > 20:
4232
return self.extended_strategy(opponent)

axelrod/strategies/hunter.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,36 @@
11
from __future__ import absolute_import
22

3+
import itertools
4+
35
from axelrod import Player
46

57

8+
def detect_cycle(history, min_size=1, offset=0):
9+
"""Detects cycles in the sequence history.
10+
11+
Parameters
12+
----------
13+
history: sequence of C and D
14+
The sequence to look for cycles within
15+
min_size: int, 1
16+
The minimum length of the cycle
17+
offset: int, 0
18+
The amount of history to skip initially
19+
"""
20+
history_tail = history[-offset:]
21+
for i in range(min_size, len(history_tail) // 2):
22+
cycle = tuple(history_tail[:i])
23+
for j in range(len(history_tail)):
24+
if history_tail[j] != cycle[j % len(cycle)]:
25+
break
26+
if j == len(history_tail) - 1:
27+
# We made it to the end, is the cycle itself a cycle?
28+
# I.E. CCC is not ok as cycle if min_size is really 2
29+
# Since this is the same as C
30+
return cycle
31+
return None
32+
33+
634
class DefectorHunter(Player):
735
"""A player who hunts for defectors."""
836

@@ -58,6 +86,51 @@ def strategy(self, opponent):
5886
return 'C'
5987

6088

89+
class CycleHunter(Player):
90+
"""Hunts strategies that play cyclically, like any of the Cyclers,
91+
Alternator, etc."""
92+
93+
name = 'Cycle Hunter'
94+
classifier = {
95+
'memory_depth': float('inf'), # Long memory
96+
'stochastic': False,
97+
'inspects_source': False,
98+
'manipulates_source': False,
99+
'manipulates_state': False
100+
}
101+
102+
@staticmethod
103+
def strategy(opponent):
104+
cycle = detect_cycle(opponent.history, min_size=2)
105+
if cycle:
106+
if len(set(cycle)) > 1:
107+
return 'D'
108+
return 'C'
109+
110+
111+
class EventualCycleHunter(Player):
112+
"""Hunts strategies that eventually play cyclically"""
113+
114+
name = 'Eventual Cycle Hunter'
115+
classifier = {
116+
'memory_depth': float('inf'), # Long memory
117+
'stochastic': False,
118+
'inspects_source': False,
119+
'manipulates_source': False,
120+
'manipulates_state': False
121+
}
122+
123+
def strategy(self, opponent):
124+
if len(opponent.history) < 10:
125+
return 'C'
126+
if len(opponent.history) == opponent.cooperations:
127+
return 'C'
128+
if detect_cycle(opponent.history, offset=15):
129+
return 'D'
130+
else:
131+
return 'C'
132+
133+
61134
class MathConstantHunter(Player):
62135
"""A player who hunts for mathematical constant players."""
63136

axelrod/strategies/meta.py

Lines changed: 96 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from axelrod import Player, obey_axelrod
22
from ._strategies import strategies
3-
from .hunter import DefectorHunter, AlternatorHunter, RandomHunter, MathConstantHunter
4-
3+
from .hunter import DefectorHunter, AlternatorHunter, RandomHunter, MathConstantHunter, CycleHunter, EventualCycleHunter
4+
from .cooperator import Cooperator
55

66
# Needs to be computed manually to prevent circular dependency
77
ordinary_strategies = [s for s in strategies if obey_axelrod(s)]
@@ -10,7 +10,9 @@
1010
class MetaPlayer(Player):
1111
"""A generic player that has its own team of players."""
1212

13-
team = []
13+
name = "Meta Player"
14+
15+
team = [Cooperator]
1416
classifier = {
1517
'memory_depth': float('inf'), # Long memory
1618
'stochastic': False,
@@ -30,9 +32,12 @@ def __init__(self):
3032
# Initiate all the player in out team.
3133
self.team = [t() for t in self.team]
3234

33-
# If the team will have stochastic players, this meta is also stochastic.
34-
self.classifier['stochastic'] = (
35-
any([t.classifier['stochastic'] for t in self.team]))
35+
# This player inherits the classifiers of its team.
36+
for key in ['stochastic', 'inspects_source', 'manipulates_source',
37+
'manipulates_state']:
38+
self.classifier[key] = (any([t.classifier[key] for t in self.team]))
39+
self.classifier['memory_depth'] = max([t.classifier['memory_depth'] for
40+
t in self.team])
3641

3742
def strategy(self, opponent):
3843

@@ -62,9 +67,14 @@ class MetaMajority(MetaPlayer):
6267

6368
name = "Meta Majority"
6469

65-
def __init__(self):
66-
self.team = ordinary_strategies
70+
def __init__(self, team=None):
71+
if team:
72+
self.team = team
73+
else:
74+
# Needs to be computed manually to prevent circular dependency
75+
self.team = ordinary_strategies
6776
super(MetaMajority, self).__init__()
77+
self.init_args = (team,)
6878

6979
def meta_strategy(self, results, opponent):
7080
if results.count('D') > results.count('C'):
@@ -77,9 +87,14 @@ class MetaMinority(MetaPlayer):
7787

7888
name = "Meta Minority"
7989

80-
def __init__(self):
81-
self.team = ordinary_strategies
90+
def __init__(self, team=None):
91+
if team:
92+
self.team = team
93+
else:
94+
# Needs to be computed manually to prevent circular dependency
95+
self.team = ordinary_strategies
8296
super(MetaMinority, self).__init__()
97+
self.init_args = (team,)
8398

8499
def meta_strategy(self, results, opponent):
85100
if results.count('D') < results.count('C'):
@@ -116,8 +131,6 @@ def strategy(self, opponent):
116131
# Update the running score for each player, before determining the next move.
117132
if len(self.history):
118133
for player in self.team:
119-
pl_C = player.proposed_history[-1] == "C"
120-
opp_C = opponent.history[-1] == "C"
121134
game = self.tournament_attributes["game"]
122135
s = game.scores[(player.proposed_history[-1], opponent.history[-1])][0]
123136
player.score += s
@@ -136,6 +149,10 @@ def meta_strategy(self, results, opponent):
136149
for r, t in zip(results, self.team):
137150
t.proposed_history.append(r)
138151

152+
if opponent.defections == 0:
153+
# Don't poke the bear
154+
return 'C'
155+
139156
return bestresult
140157

141158

@@ -157,7 +174,7 @@ def __init__(self):
157174
# to hunters that use defections as cues. However, a really tangible benefit comes from
158175
# combining Random Hunter and Math Constant Hunter, since together they catch strategies
159176
# that are lightly randomized but still quite constant (the tricky/suspecious ones).
160-
self.team = [DefectorHunter, AlternatorHunter, RandomHunter, MathConstantHunter]
177+
self.team = [DefectorHunter, AlternatorHunter, RandomHunter, MathConstantHunter, CycleHunter, EventualCycleHunter]
161178

162179
super(MetaHunter, self).__init__()
163180

@@ -175,3 +192,69 @@ def meta_strategy(results, opponent):
175192
return 'D' if opponent.history[-1:] == ['D'] else 'C'
176193
else:
177194
return 'C'
195+
196+
197+
class MetaMajorityMemoryOne(MetaMajority):
198+
"""MetaMajority with the team of Memory One players"""
199+
200+
name = "Meta Majority Memory One"
201+
202+
def __init__(self):
203+
team = [s for s in ordinary_strategies if s().classifier['memory_depth'] <= 1]
204+
super(MetaMajorityMemoryOne, self).__init__(team=team)
205+
self.init_args = ()
206+
207+
208+
class MetaWinnerMemoryOne(MetaWinner):
209+
"""MetaWinner with the team of Memory One players"""
210+
211+
name = "Meta Winner Memory One"
212+
213+
def __init__(self):
214+
team = [s for s in ordinary_strategies if s().classifier['memory_depth'] <= 1]
215+
super(MetaWinnerMemoryOne, self).__init__(team=team)
216+
self.init_args = ()
217+
218+
219+
class MetaMajorityFiniteMemory(MetaMajority):
220+
"""MetaMajority with the team of Finite Memory Players"""
221+
222+
name = "Meta Majority Finite Memory"
223+
def __init__(self):
224+
team = [s for s in ordinary_strategies if s().classifier['memory_depth']
225+
< float('inf')]
226+
super(MetaMajorityFiniteMemory, self).__init__(team=team)
227+
self.init_args = ()
228+
229+
230+
class MetaWinnerFiniteMemory(MetaWinner):
231+
"""MetaWinner with the team of Finite Memory Players"""
232+
233+
name = "Meta Winner Finite Memory"
234+
def __init__(self):
235+
team = [s for s in ordinary_strategies if s().classifier['memory_depth']
236+
< float('inf')]
237+
super(MetaWinnerFiniteMemory, self).__init__(team=team)
238+
self.init_args = ()
239+
240+
241+
class MetaMajorityLongMemory(MetaMajority):
242+
"""MetaMajority with the team of Long (infinite) Memory Players"""
243+
244+
name = "Meta Majority Long Memory"
245+
def __init__(self):
246+
team = [s for s in ordinary_strategies if s().classifier['memory_depth']
247+
== float('inf')]
248+
super(MetaMajorityLongMemory, self).__init__(team=team)
249+
self.init_args = ()
250+
251+
252+
class MetaWinnerLongMemory(MetaWinner):
253+
"""MetaWinner with the team of Long (infinite) Memory Players"""
254+
255+
name = "Meta Winner Long Memory"
256+
def __init__(self):
257+
team = [s for s in ordinary_strategies if s().classifier['memory_depth']
258+
== float('inf')]
259+
super(MetaWinnerLongMemory, self).__init__(team=team)
260+
self.init_args = ()

axelrod/tests/integration/test_tournament.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,17 @@ def setUpClass(cls):
1818
cls.test_repetitions = 5
1919

2020
cls.expected_outcome = [
21-
('Cooperator', [1800, 1800, 1800, 1800, 1800]),
22-
('Defector', [1612, 1612, 1612, 1612, 1612]),
23-
('Grudger', [1999, 1999, 1999, 1999, 1999]),
24-
('Soft Go By Majority', [1999, 1999, 1999, 1999, 1999]),
25-
('Tit For Tat', [1999, 1999, 1999, 1999, 1999])]
21+
('Cooperator', [180, 180, 180, 180, 180]),
22+
('Defector', [172, 172, 172, 172, 172]),
23+
('Grudger', [199, 199, 199, 199, 199]),
24+
('Soft Go By Majority', [199, 199, 199, 199, 199]),
25+
('Tit For Tat', [199, 199, 199, 199, 199])]
2626
cls.expected_outcome.sort()
2727

2828
def test_full_tournament(self):
2929
"""A test to check that tournament runs with all non cheating strategies."""
3030
strategies = [strategy() for strategy in axelrod.ordinary_strategies]
31-
tournament = axelrod.Tournament(name='test', players=strategies, game=self.game, turns=500, repetitions=2)
31+
tournament = axelrod.Tournament(name='test', players=strategies, game=self.game, turns=20, repetitions=2)
3232
output_of_tournament = tournament.play().results
3333
self.assertEqual(type(output_of_tournament), dict)
3434
self.assertEqual(len(output_of_tournament['payoff']), len(strategies))
@@ -38,7 +38,7 @@ def test_serial_play(self):
3838
name=self.test_name,
3939
players=self.players,
4040
game=self.game,
41-
turns=200,
41+
turns=20,
4242
repetitions=self.test_repetitions)
4343
scores = tournament.play().scores
4444
actual_outcome = sorted(zip(self.player_names, scores))
@@ -49,7 +49,7 @@ def test_parallel_play(self):
4949
name=self.test_name,
5050
players=self.players,
5151
game=self.game,
52-
turns=200,
52+
turns=20,
5353
repetitions=self.test_repetitions,
5454
processes=2)
5555
scores = tournament.play().scores

0 commit comments

Comments
 (0)