Skip to content

Commit 9309eb5

Browse files
committed
Merge pull request #449 from Axelrod-Python/447
447 - Adding a Mixed decorator **and** metaplayer
2 parents 62ee9d5 + 4025c3d commit 9309eb5

File tree

7 files changed

+202
-7
lines changed

7 files changed

+202
-7
lines changed

axelrod/strategies/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@
88
from .meta import (
99
MetaPlayer, MetaMajority, MetaMinority, MetaWinner, MetaHunter,
1010
MetaMajorityMemoryOne, MetaWinnerMemoryOne, MetaMajorityFiniteMemory,
11-
MetaWinnerFiniteMemory, MetaMajorityLongMemory, MetaWinnerLongMemory
11+
MetaWinnerFiniteMemory, MetaMajorityLongMemory, MetaWinnerLongMemory,
12+
MetaMixer
1213
)
1314

1415
strategies.extend((MetaHunter, MetaMajority, MetaMinority, MetaWinner,
1516
MetaMajorityMemoryOne, MetaWinnerMemoryOne,
1617
MetaMajorityFiniteMemory, MetaWinnerFiniteMemory,
17-
MetaMajorityLongMemory, MetaWinnerLongMemory))
18+
MetaMajorityLongMemory, MetaWinnerLongMemory, MetaMixer))
1819

1920
# Distinguished strategy collections in addition to
2021
# `strategies` from _strategies.py

axelrod/strategies/meta.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from ._strategies import strategies
33
from .hunter import DefectorHunter, AlternatorHunter, RandomHunter, MathConstantHunter, CycleHunter, EventualCycleHunter
44
from .cooperator import Cooperator
5+
from numpy.random import choice
56

67
# Needs to be computed manually to prevent circular dependency
78
ordinary_strategies = [s for s in strategies if obey_axelrod(s)]
@@ -266,3 +267,42 @@ def __init__(self):
266267
== float('inf')]
267268
super(MetaWinnerLongMemory, self).__init__(team=team)
268269
self.init_args = ()
270+
271+
272+
class MetaMixer(MetaPlayer):
273+
"""A player who randomly switches between a team of players.
274+
If no distribution is passed then the player will uniformly choose between
275+
sub players.
276+
277+
In essence this is creating a Mixed strategy.
278+
279+
Parameters
280+
----------
281+
team : list of strategy classes, optional
282+
Team of strategies that are to be randomly played
283+
If none is passed will select the ordinary strategies.
284+
distribution : list representing a probability distribution, optional
285+
This gives the distribution from which to select the players.
286+
If none is passed will select uniformly.
287+
"""
288+
289+
name = "Meta Mixer"
290+
291+
def __init__(self, team=None, distribution=None):
292+
293+
# The default is to use all strategies available, but we need to import the list
294+
# at runtime, since _strategies import also _this_ module before defining the list.
295+
if team:
296+
self.team = team
297+
else:
298+
# Needs to be computed manually to prevent circular dependency
299+
self.team = ordinary_strategies
300+
301+
self.distribution = distribution
302+
303+
super(MetaMixer, self).__init__()
304+
self.init_args = (team, distribution)
305+
306+
def meta_strategy(self, results, opponent):
307+
"""Using the numpy.random choice function to sample with weights"""
308+
return choice(results, p=self.distribution)

axelrod/strategy_transformers.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88

99
import inspect
1010
import random
11-
from types import FunctionType
11+
import collections
12+
from numpy.random import choice
1213

1314
from .actions import Actions, flip_action
1415
from .random_ import random_choice
@@ -239,6 +240,46 @@ def apology_wrapper(player, opponent, action, myseq, opseq):
239240
ApologyTransformer = StrategyTransformerFactory(apology_wrapper,
240241
name_prefix="Apologizing")
241242

243+
244+
def mixed_wrapper(player, opponent, action, probability, m_player):
245+
"""Randomly picks a strategy to play, either from a distribution on a list
246+
of players or a single player.
247+
248+
In essence creating a mixed strategy.
249+
250+
Parameters
251+
----------
252+
253+
probability: a float (or integer: 0 or 1) OR an iterable representing a
254+
an incomplete probability distribution (entries to do not have to sum to
255+
1). Eg: 0, 1, [.5,.5], (.5,.3)
256+
m_players: a single player class or iterable representing set of player
257+
classes to mix from.
258+
Eg: axelrod.TitForTat, [axelod.Cooperator, axelrod.Defector]
259+
"""
260+
261+
# If a single probability, player is passed
262+
if isinstance(probability, float) or isinstance(probability, int):
263+
m_player = [m_player]
264+
probability = [probability]
265+
266+
# If a probability distribution, players is passed
267+
if isinstance(probability, collections.Iterable) and \
268+
isinstance(m_player, collections.Iterable):
269+
mutate_prob = sum(probability) # Prob of mutation
270+
if mutate_prob > 0:
271+
# Distribution of choice of mutation:
272+
normalised_prob = [prob / float(mutate_prob)
273+
for prob in probability]
274+
if random.random() < mutate_prob:
275+
p = choice(list(m_player), p=normalised_prob)()
276+
p.history = player.history
277+
return p.strategy(opponent)
278+
279+
return action
280+
281+
MixedTransformer = StrategyTransformerFactory(mixed_wrapper, name_prefix="Mutated")
282+
242283
# Strategy wrappers as classes
243284

244285
class RetaliationWrapper(object):
@@ -260,6 +301,7 @@ def __call__(self, player, opponent, action, retaliations):
260301
RetaliationTransformer = StrategyTransformerFactory(
261302
RetaliationWrapper(), name_prefix="Retaliating")
262303

304+
263305
class RetaliationUntilApologyWrapper(object):
264306
"""Enforces the TFT rule that the opponent pay back a defection with a
265307
cooperation for the player to stop defecting."""

axelrod/tests/unit/test_meta.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,3 +284,51 @@ class TestMetaWinnerLongMemory(TestMetaPlayer):
284284

285285
def test_strategy(self):
286286
self.first_play_test(C)
287+
288+
289+
class TestMetaMixer(TestMetaPlayer):
290+
291+
name = "Meta Mixer"
292+
player = axelrod.MetaMixer
293+
expected_classifier = {
294+
'memory_depth': float('inf'), # Long memory
295+
'stochastic': True,
296+
'manipulates_source': False,
297+
'inspects_source': False,
298+
'manipulates_state': False
299+
}
300+
301+
def test_strategy(self):
302+
303+
team = [axelrod.TitForTat, axelrod.Cooperator, axelrod.Grudger]
304+
distribution = [.2, .5, .3]
305+
306+
P1 = axelrod.MetaMixer(team, distribution)
307+
P2 = axelrod.Cooperator()
308+
309+
for k in range(100):
310+
self.assertEqual(P1.strategy(P2), C)
311+
312+
team.append(axelrod.Defector)
313+
distribution = [.2, .5, .3, 0] # If add a defector but does not occur
314+
315+
P1 = axelrod.MetaMixer(team, distribution)
316+
317+
for k in range(100):
318+
self.assertEqual(P1.strategy(P2), C)
319+
320+
distribution = [0, 0, 0, 1] # If defector is only one that is played
321+
322+
P1 = axelrod.MetaMixer(team, distribution)
323+
324+
for k in range(100):
325+
self.assertEqual(P1.strategy(P2), D)
326+
327+
def test_raise_error_in_distribution(self):
328+
team = [axelrod.TitForTat, axelrod.Cooperator, axelrod.Grudger]
329+
distribution = [.2, .5, .5] # Not a valid probability distribution
330+
331+
P1 = axelrod.MetaMixer(team, distribution)
332+
P2 = axelrod.Cooperator()
333+
334+
self.assertRaises(ValueError, P1.strategy, P2)

axelrod/tests/unit/test_strategy_transformers.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
class TestTransformers(unittest.TestCase):
1414

1515
def test_all_strategies(self):
16-
# Attempt to transform each strategy to ensure that implemenation
16+
# Attempt to transform each strategy to ensure that implementation
1717
# choices (like use of super) do not cause issues
1818
for s in axelrod.ordinary_strategies:
1919
opponent = axelrod.Cooperator()
@@ -212,6 +212,53 @@ def test_apology(self):
212212
p1.play(p2)
213213
self.assertEqual(p1.history, [D, D, C, D, D, C])
214214

215+
def test_mixed(self):
216+
"""Tests the MixedTransformer."""
217+
probability = 1
218+
MD = MixedTransformer(probability, axelrod.Cooperator)(axelrod.Defector)
219+
220+
p1 = MD()
221+
p2 = axelrod.Cooperator()
222+
for _ in range(5):
223+
p1.play(p2)
224+
self.assertEqual(p1.history, [C, C, C, C, C])
225+
226+
probability = 0
227+
MD = MixedTransformer(probability, axelrod.Cooperator)(axelrod.Defector)
228+
229+
p1 = MD()
230+
p2 = axelrod.Cooperator()
231+
for _ in range(5):
232+
p1.play(p2)
233+
self.assertEqual(p1.history, [D, D, D, D, D])
234+
235+
# Decorating with list and distribution
236+
237+
# Decorate a cooperator putting all weight on other strategies that are
238+
# 'nice'
239+
probability = [.3, .2, 0]
240+
strategies = [axelrod.TitForTat, axelrod.Grudger, axelrod.Defector]
241+
MD = MixedTransformer(probability, strategies)(axelrod.Cooperator)
242+
243+
p1 = MD()
244+
# Against a cooperator we see that we only cooperate
245+
p2 = axelrod.Cooperator()
246+
for _ in range(5):
247+
p1.play(p2)
248+
self.assertEqual(p1.history, [C, C, C, C, C])
249+
250+
# Decorate a cooperator putting all weight on Defector
251+
probability = (0, 0, 1) # Note can also pass tuple
252+
strategies = [axelrod.TitForTat, axelrod.Grudger, axelrod.Defector]
253+
MD = MixedTransformer(probability, strategies)(axelrod.Cooperator)
254+
255+
p1 = MD()
256+
# Against a cooperator we see that we only cooperate
257+
p2 = axelrod.Cooperator()
258+
for _ in range(5):
259+
p1.play(p2)
260+
self.assertEqual(p1.history, [D, D, D, D, D])
261+
215262
def test_deadlock(self):
216263
"""Test the DeadlockBreakingTransformer."""
217264
# We can induce a deadlock by alterting TFT to defect first

docs/tutorials/advanced/strategy_transformers.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,23 @@ after two consequtive rounds of `(D, C)`::
132132
>>> from axelrod.strategy_transformers import TrackHistoryTransformer
133133
>>> player = TrackHistoryTransformer(axelrod.Random)()
134134

135+
* :code:`MixedTransformer`: Randomly plays a mutation to another strategy (or
136+
set of strategies. Here is the syntax to do this with a set of strategies::
137+
138+
>>> import axelrod
139+
>>> from axelrod.strategy_transformers import MixedTransformer
140+
>>> strategies = [axelrod.Grudger, axelrod.TitForTat]
141+
>>> probability = [.2, .3] # .5 chance of mutated to one of above
142+
>>> player = MixedTransformer(probability, strategies)(axelrod.Cooperator)
143+
144+
Here is the syntax when passing a single strategy::
145+
146+
>>> import axelrod
147+
>>> from axelrod.strategy_transformers import MixedTransformer
148+
>>> strategy = axelrod.Grudger
149+
>>> probability = .2
150+
>>> player = MixedTransformer(probability, strategy)(axelrod.Cooperator)
151+
135152

136153
Composing Transformers
137154
----------------------

docs/tutorials/further_topics/classification_of_strategies.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ This allows us to, for example, quickly identify all the stochastic
2424
strategies::
2525

2626
>>> len([s for s in axl.strategies if s().classifier['stochastic']])
27-
36
27+
37
2828

2929
Or indeed find out how many strategy only use 1 turn worth of memory to
3030
make a decision::
@@ -37,13 +37,13 @@ tournament. For example, here is the number of strategies that make use of the
3737
length of each match of the tournament::
3838

3939
>>> len([s() for s in axl.strategies if 'length' in s().classifier['makes_use_of']])
40-
8
40+
9
4141

4242
Here are how many of the strategies that make use of the particular game being
4343
played (whether or not it's the default Prisoner's dilemma)::
4444

4545
>>> len([s() for s in axl.strategies if 'game' in s().classifier['makes_use_of']])
46-
20
46+
21
4747

4848
Similarly, strategies that :code:`manipulate_source`, :code:`manipulate_state`
4949
and/or :code:`inspect_source` return :code:`False` for the :code:`obey_axelrod`

0 commit comments

Comments
 (0)