Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
41 changes: 41 additions & 0 deletions axelrod/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import random
import copy
import inspect
import types
import numpy as np
import itertools

from axelrod.actions import Actions, flip_action, Action
from .game import DefaultGame
Expand Down Expand Up @@ -122,6 +125,44 @@ def __init__(self):
self.state_distribution = defaultdict(int)
self.set_match_attributes()


def __eq__(self, other):
"""
Test if two players are equal.
"""
if self.__repr__() != other.__repr__():
return False
for attribute in set(list(self.__dict__.keys()) +
list(other.__dict__.keys())):

value = getattr(self, attribute, None)
other_value = getattr(other, attribute, None)

if isinstance(value, np.ndarray):
if not (np.array_equal(value, other_value)):
return False

elif isinstance(value, types.GeneratorType) or \
isinstance(value, itertools.cycle):

# Split the original generator so it is not touched
generator, original_value = itertools.tee(value)
other_generator, original_other_value = itertools.tee(other_value)

setattr(self, attribute,
(ele for ele in original_value))
setattr(other, attribute,
(ele for ele in original_other_value))

if not (all(next(generator) == next(other_generator)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could cause unintended behavior if used during a match or tournament.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see what you mean?

I've done this in such a way that the generator gets copied and there are tests that show that using __eq__ does not alter the generator.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lines 199 and 215:

187     def test_equality_for_generator(self):                                                                                                        
188         # Check generator attribute (a special case)                                                                                              
189         p1 = axelrod.Cooperator()                                                                                                                 
190         p2 = axelrod.Cooperator()                                                                                                                 
191         # Check that the generator is still the same                                                                                              
192         p1.generator = (i for i in range(10))                                                                                                     
193         p2.generator = (i for i in range(10))                                                                                                     
194         self.assertEqual(p1, p2)                                                                                                                  
195                                                                                                                                                   
196         _ = next(p2.generator)                                                                                                                    
197         self.assertNotEqual(p1, p2)                                                                                                               
198                                                                                                                                                   
199         # Check that internal generator object has not been changed                                                                               
200         self.assertEqual(list(p1.generator), list(range(10)))                                                                                     
201         self.assertEqual(list(p2.generator), list(range(1, 10)))                                                                                  
202                                                                                                                                                   
203     def test_equality_for_cycle(self):                                                                                                            
204         # Check cycle attribute (a special case)                                                                                                  
205         p1 = axelrod.Cooperator()                                                                                                                 
206         p2 = axelrod.Cooperator()                                                                                                                 
207         # Check that the cycle is still the same                                                                                                  
208         p1.cycle = itertools.cycle(range(10))                                                                                                     
209         p2.cycle = itertools.cycle(range(10))                                                                                                     
210         self.assertEqual(p1, p2)                                                                                                                  
211                                                                                                                                                   
212         _ = next(p2.cycle)                                                                                                                        
213         self.assertNotEqual(p1, p2)                                                                                                               
214                                                                                                                                                   
215         # Check that internal generator object has not been changed                                                                               
216         self.assertEqual(next(p1.cycle), 0)                                                                                                       
217         self.assertEqual(next(p2.cycle), 1)  

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So when the generator is copied it's state is also reset?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup that's right. What itertools.tee does is essentially fork a generator in to two versions of itself from the state it currently is in.

That is also being tests:

  1. Create two Cooperators and give them two cycle generators and tests that they are equal (at this stage their generator are in the same state:

    205         p1 = axelrod.Cooperator()                                                                                                                 
    206         p2 = axelrod.Cooperator()                                                                                                                 
    207         # Check that the cycle is still the same                                                                                                
    208         p1.cycle = itertools.cycle(range(10))                                                                                                    
    209         p2.cycle = itertools.cycle(range(10))                                                                                                    
    210         self.assertEqual(p1, p2)   
    
  2. Bump p2s cycle and now the players are not the same:

    212         _ = next(p2.cycle)                                                                                                                       
    213         self.assertNotEqual(p1, p2)  
    
  3. Check what the next values in the cycle are for each player, for p1 it should be 0 because the player has not iterated through any values and for p2 it should be 1 because it has iterated through the first value:

    215         # Check that internal generator object has not been changed                                                                              
    216         self.assertEqual(next(p1.cycle), 0)                                                                                                       
    217         self.assertEqual(next(p2.cycle), 1)  
    

Let me know if you think I could clarify that in the code and/or add other tests.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI test_equality_on_init is the test that fails in this case.

This should clarify why:

>>> import itertools
>>> import types
>>> cycle = itertools.cycle('ABCD')
>>> new_cycle, backup = itertools.tee(cycle)
>>> type(cycle), type(new_cycle), type(backup)
(itertools.cycle, itertools._tee, itertools._tee)
>>> isinstance(cycle, itertools.cycle), isinstance(cycle, types.GeneratorType)
(True, False)
>>> isinstance(new_cycle, itertools.cycle), isinstance(backup, types.GeneratorType)
(False, False)

So the second time around the generator/cycle is not compared in the correct way. I did have a special case for a cycle but found it more elegant to just treat them all the same way. :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could add isinstance(value, collections.Iterable) to the first check but that would also catch lists etc which we don't want :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did have a special case for a cycle but found it more elegant to just treat them all the same way.

This might actually be a better way as it ensures we don't change anything at all (even the type of the attributes). I've also realised something else that's not right in this test. Going to tweak and push.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In c5e95d6 I've added a third equality test (theoretically things could break on the second one). I've also added test that ensure the type of a generator/cycle stays the same (which implies I've changed the way I've dealt with the output of itertools.tee). 👍 Let me know what you think :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanations!

for _ in range(200))):
return False
else:
if value != other_value:
return False
return True


def receive_match_attributes(self):
# Overwrite this function if your strategy needs
# to make use of match_attributes such as
Expand Down
10 changes: 7 additions & 3 deletions axelrod/strategies/qlearner.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ def __init__(self) -> None:
# for any subclasses that do not override methods using random calls.
self.classifier['stochastic'] = True

self.prev_action = random_choice()
self.original_prev_action = self.prev_action
self.prev_action = None # type: Action
self.original_prev_action = None # type: Action
self.history = [] # type: List[Action]
self.score = 0
self.Qs = OrderedDict({'': OrderedDict(zip([C, D], [0, 0]))})
Expand All @@ -62,6 +62,9 @@ def receive_match_attributes(self):

def strategy(self, opponent: Player) -> Action:
"""Runs a qlearn algorithm while the tournament is running."""
if len(self.history) == 0:
self.prev_action = random_choice()
self.original_prev_action = self.prev_action
state = self.find_state(opponent)
reward = self.find_reward(opponent)
if state not in self.Qs:
Expand Down Expand Up @@ -118,7 +121,8 @@ def reset(self):
self.Qs = {'': {C: 0, D: 0}}
self.Vs = {'': 0}
self.prev_state = ''
self.prev_action = self.original_prev_action
self.prev_action = None
self.original_prev_action = None


class ArrogantQLearner(RiskyQLearner):
Expand Down
6 changes: 5 additions & 1 deletion axelrod/strategies/titfortat.py
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,11 @@ class ContriteTitForTat(Player):
'manipulates_source': False,
'manipulates_state': False
}
contrite = False

def __init__(self):
super().__init__()
self.contrite = False
self._recorded_history = []

def strategy(self, opponent: Player) -> Action:

Expand Down
5 changes: 0 additions & 5 deletions axelrod/tests/strategies/test_calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,6 @@ def test_edge_case_calculator_ignores_cycles_gt_len_ten(self):
axelrod.MockPlayer(actions=opponent_actions),
expected_actions=uses_tit_for_tat_after_twenty_rounds, seed=seed)

def attribute_equality_test(self, player, clone):
"""Overwrite the default test to check Joss instance"""
self.assertIsInstance(player.joss_instance, axelrod.Joss)
self.assertIsInstance(clone.joss_instance, axelrod.Joss)

def test_get_joss_strategy_actions(self):
opponent = [C, D, D, C, C]

Expand Down
13 changes: 0 additions & 13 deletions axelrod/tests/strategies/test_memorytwo.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,3 @@ def test_strategy(self):
# ALLD forever if all D twice
self.responses_test([D] * 10, [C, D, D, D, D, D], [D, D, D, D, D, D])
self.responses_test([D] * 9, [C] + [D] * 5 + [C] * 4, [D] * 6 + [C] * 4)

def attribute_equality_test(self, player, clone):
"""Overwrite specific test to be able to test self.players"""
for p in [player, clone]:
self.assertEqual(p.play_as, "TFT")
self.assertEqual(p.shift_counter, 3)
self.assertEqual(p.alld_counter, 0)

for key, value in [("TFT", axelrod.TitForTat),
("TFTT", axelrod.TitFor2Tats),
("ALLD", axelrod.Defector)]:
self.assertEqual(p.players[key].history, [])
self.assertIsInstance(p.players[key], value)
10 changes: 0 additions & 10 deletions axelrod/tests/strategies/test_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,6 @@ def classifier_test(self, expected_class_classifier=None):
msg="%s - Behaviour: %s != Expected Behaviour: %s" %
(key, player.classifier[key], classifier[key]))

def attribute_equality_test(self, player, clone):
"""Overwriting this specific method to check team."""
for p1, p2 in zip(player.team, clone.team):
self.assertEqual(len(p1.history), 0)
self.assertEqual(len(p2.history), 0)

team_player_names = [p.__repr__() for p in player.team]
team_clone_names = [p.__repr__() for p in clone.team]
self.assertEqual(team_player_names, team_clone_names)

def test_repr(self):
player = self.player()
team_size = len(player.team)
Expand Down
120 changes: 91 additions & 29 deletions axelrod/tests/strategies/test_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from axelrod import DefaultGame, MockPlayer, Player, simulate_play
from axelrod.player import get_state_distribution_from_history

from hypothesis import given
from axelrod.tests.property import strategy_lists

C, D = axelrod.Actions.C, axelrod.Actions.D

Expand Down Expand Up @@ -150,6 +152,93 @@ def test_clone(self):
self.assertEqual(len(player1.history), turns)
self.assertEqual(player1.history, player2.history)

def test_equality(self):
"""Test the equality method for some bespoke cases"""
# Check repr
p1 = axelrod.Cooperator()
p2 = axelrod.Cooperator()
self.assertEqual(p1, p2)
p1.__repr__ = lambda: "John Nash"
self.assertNotEqual(p1, p2)

# Check attributes
p1 = axelrod.Cooperator()
p2 = axelrod.Cooperator()
p1.test = "29"
self.assertNotEqual(p1, p2)

p1 = axelrod.Cooperator()
p2 = axelrod.Cooperator()
p2.test = "29"
self.assertNotEqual(p1, p2)

def test_equality_for_numpy_array(self):
# Check numpy array attribute (a special case)
p1 = axelrod.Cooperator()
p2 = axelrod.Cooperator()

p1.array = np.array([0, 1])
p2.array = np.array([0, 1])
self.assertEqual(p1, p2)

p2.array = np.array([1, 0])
self.assertNotEqual(p1, p2)

def test_equality_for_generator(self):
"""Test equality works with generator attribute and that the generator
attribute is not altered during checking of equality"""
p1 = axelrod.Cooperator()
p2 = axelrod.Cooperator()

# Check that players are equal with generator
p1.generator = (i for i in range(10))
p2.generator = (i for i in range(10))
self.assertEqual(p1, p2)

# Check state of one generator (ensure it hasn't changed)
n = next(p2.generator)
self.assertEqual(n, 0)

# Players are no longer equal (one generator has changed)
self.assertNotEqual(p1, p2)

# Check that internal generator object has not been changed for either
# player after latest equal check.
self.assertEqual(list(p1.generator), list(range(10)))
self.assertEqual(list(p2.generator), list(range(1, 10)))

def test_equality_for_cycle(self):
"""Test equality works with cycle attribute and that the cycle attribute
is not altered during checking of equality"""
# Check cycle attribute (a special case)
p1 = axelrod.Cooperator()
p2 = axelrod.Cooperator()

# Check that players are equal with cycle
p1.cycle = itertools.cycle(range(10))
p2.cycle = itertools.cycle(range(10))
self.assertEqual(p1, p2)

# Check state of one generator (ensure it hasn't changed)
n = next(p2.cycle)
self.assertEqual(n, 0)

# Players are no longer equal (one generator has changed)
self.assertNotEqual(p1, p2)

# Check that internal cycle object has not been changed for either
# player after latest not equal check.
self.assertEqual(next(p1.cycle), 0)
self.assertEqual(next(p2.cycle), 1)

def test_equaity_on_init(self):
"""Test all instances of a strategy are equal on init"""
for s in axelrod.strategies:
p1, p2 = s(), s()
# Check twice (so testing equality doesn't change anything)
self.assertEqual(p1, p2)
self.assertEqual(p1, p2)

def test_init_params(self):
"""Tests player correct parameters signature detection."""
self.assertEqual(self.player.init_params(), {})
Expand Down Expand Up @@ -285,41 +374,14 @@ def test_reset_history_and_attributes(self):
player.play(opponent)

player.reset()
self.assertEqual(len(player.history), 0)
self.assertEqual(player.cooperations, 0)
self.assertEqual(player.defections, 0)
self.assertEqual(player.state_distribution, dict())

self.attribute_equality_test(player, clone)
self.assertEqual(player, clone)

def test_reset_clone(self):
"""Make sure history resetting with cloning works correctly, regardless
if self.test_reset() is overwritten."""
player = self.player()
clone = player.clone()
self.attribute_equality_test(player, clone)

def attribute_equality_test(self, player, clone):
"""A separate method to test equality of attributes. This method can be
overwritten in certain cases.

This method checks that all the attributes of `player` and `clone` are
the same which is used in the test of the cloning and the resetting.
"""

for attribute, reset_value in player.__dict__.items():
original_value = getattr(clone, attribute)

if isinstance(reset_value, np.ndarray):
self.assertTrue(np.array_equal(reset_value, original_value),
msg=attribute)

elif isinstance(reset_value, types.GeneratorType) or isinstance(reset_value, itertools.cycle):
for _ in range(10):
self.assertEqual(next(reset_value),
next(original_value), msg=attribute)
else:
self.assertEqual(reset_value, original_value, msg=attribute)
self.assertEqual(player, clone)

def test_clone(self):
# Test that the cloned player produces identical play
Expand Down
12 changes: 0 additions & 12 deletions axelrod/tests/strategies/test_qlearner.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,18 +70,6 @@ def test_strategy(self):
p2 = axelrod.Cooperator()
test_responses(self, p1, p2, [C, D, C, C, D, C, C])

def test_reset_method(self):
"""Test the reset method."""
P1 = axelrod.RiskyQLearner()
P1.Qs = {'': {C: 0, D: -0.9}, '0.0': {C: 0, D: 0}}
P1.Vs = {'': 0, '0.0': 0}
P1.history = [C, D, D, D]
P1.prev_state = C
P1.reset()
self.assertEqual(P1.prev_state, '')
self.assertEqual(P1.Vs, {'': 0})
self.assertEqual(P1.Qs, {'': {C: 0, D: 0}})


class TestArrogantQLearner(TestPlayer):

Expand Down
12 changes: 5 additions & 7 deletions axelrod/tests/strategies/test_titfortat.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,11 @@ class TestContriteTitForTat(TestPlayer):
deterministic_strategies = [s for s in axelrod.strategies
if not s().classifier['stochastic']]

def test_init(self):
ctft = self.player()
self.assertFalse(ctft.contrite, False)
self.assertEqual(ctft._recorded_history, [])

@given(strategies=strategy_lists(strategies=deterministic_strategies,
max_size=1),
turns=integers(min_value=1, max_value=20))
Expand Down Expand Up @@ -453,13 +458,6 @@ def test_strategy_with_noise(self):
self.assertEqual(opponent.history, [C, D, D, D])
self.assertFalse(ctft.contrite)

def test_reset_history_and_attributes(self):
"""Overwrite reset test because of decorator"""
p = self.player()
p.contrite = True
p.reset()
self.assertFalse(p.contrite)


class TestSlowTitForTwoTats(TestPlayer):

Expand Down
6 changes: 2 additions & 4 deletions axelrod/tests/unit/test_match_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,12 @@ def setUpClass(cls):
def test_init_with_clone(self):
tt = axelrod.MatchGenerator(
self.players, test_turns, test_game, test_repetitions)
self.assertEqual(tt.players, self.players)
self.assertEqual(tt.turns, test_turns)
player = tt.players[0]
opponent = tt.opponents[0]
self.assertEqual(player.name, opponent.name)
self.assertNotEqual(player, opponent)
self.assertEqual(player, opponent)
# Check that the two player instances are wholly independent
opponent.name = 'Test'
opponent.name = 'John Nash'
self.assertNotEqual(player.name, opponent.name)

def test_len(self):
Expand Down
1 change: 1 addition & 0 deletions docs/tutorials/advanced/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ Contents:
parallel_processing.rst
using_the_cache.rst
setting_a_seed.rst
player_equality.rst
27 changes: 27 additions & 0 deletions docs/tutorials/advanced/player_equality.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Player equality
===============

It is possible to test for player equality using :code:`==`::

>>> import axelrod as axl
>>> p1, p2, p3 = axl.Alternator(), axl.Alternator(), axl.TitForTat()
>>> p1 == p2
True
>>> p1 == p3
False

Note that this checks all the attributes of an instance::

>>> p1.name = "John Nash"
>>> p1 == p2
False

This however does not check if the players will behave in the same way. For
example here are two equivalent players::

>>> p1 = axl.Alternator()
>>> p2 = axl.Cycler((axl.Actions.C, axl.Actions.D))
>>> p1 == p2
False

To check if player strategies are equivalent you can use :ref:`fingerprinting`.