Skip to content
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

Automatically set strategy classes (and transformers) __repr__ to include parameters. #802 #953

Merged
merged 26 commits into from
Apr 2, 2017
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5dfd22b
include transformers parameters (#802)
Chadys Mar 27, 2017
8ebd43f
Automatically add parameters in player.__repr__ (#802)
Chadys Mar 29, 2017
b189a93
Put Transformers' args after transformed player's args in name (#802)
Chadys Mar 29, 2017
08477d9
Updated tests with new players' name (#802)
Chadys Mar 30, 2017
239492e
Merge remote-tracking branch 'upstream/master' into 802
Chadys Mar 30, 2017
86ed15d
update .gitignore to include Jetbrain editor settings
Chadys Mar 30, 2017
c150911
added test_init_params for Player (#802)
Chadys Mar 30, 2017
bf83ba6
init_param *args management (#802)
Chadys Mar 30, 2017
8296a89
added more test for init_params and init_kwargs (#802)
Chadys Mar 30, 2017
a40ecc0
remove *init_args use (#802)
Chadys Mar 31, 2017
6d2dfb4
better strategy transformers arg repr (#802)
Chadys Mar 31, 2017
6bd8b04
Fix bug in testing human with new repr (#802)
Chadys Mar 31, 2017
eff83ec
add comment explaining init_params (#802)
Chadys Mar 31, 2017
0c9519b
better organisation of ParameterisedTestPlayer (#802)
Chadys Mar 31, 2017
f5f76be
added common classifier for TestOpponent and ParameterisedTestPlayer …
Chadys Mar 31, 2017
a10e2f0
Resolve bad ordering of parameters for python3.5 (#802)
Chadys Mar 31, 2017
e14dd08
Merge branch '802' of https://github.com/Chadys/Axelrod into Chadys-802
drvinceknight Mar 31, 2017
d70a2ac
Reduce quantity/size of property based tests.
drvinceknight Mar 31, 2017
b7b8ac5
Merge pull request #1 from Axelrod-Python/Chadys-802-vk
Chadys Mar 31, 2017
e744914
ParameterisedTestPlayer as class (#802)
Chadys Mar 31, 2017
fa898a4
round to 4 digits (#802)
Chadys Apr 1, 2017
180e28c
remove __repr__ override in GoByMajority (#802)
Chadys Apr 1, 2017
f603acb
correct __repr__ bug for human (#802)
Chadys Apr 1, 2017
c06dca4
Merge remote-tracking branch 'upstream/master' into 802
Chadys Apr 1, 2017
66ac41f
no round in __repr__ (#802)
Chadys Apr 1, 2017
1151f2c
Revert "remove __repr__ override in GoByMajority (#802)"
Chadys Apr 1, 2017
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
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,12 @@ ENV/
# Rope project settings
.ropeproject

# Sublime Text Settings
# Sublime Text settings
*.sublime-*

# Jetbrain editor settings
.idea/

# Docker files
Dockerfile
docker-compose.yml
docker-compose.yml
8 changes: 4 additions & 4 deletions axelrod/fingerprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,16 +91,16 @@ def create_jossann(point, probe):
x, y = point

if isinstance(probe, axl.Player):
init_args = probe.init_args
init_kwargs = probe.init_kwargs
probe = probe.__class__
else:
init_args = ()
init_kwargs = {}

if x + y >= 1:
joss_ann = DualTransformer()(
JossAnnTransformer((1 - x, 1 - y))(probe))(*init_args)
JossAnnTransformer((1 - x, 1 - y))(probe))(**init_kwargs)
else:
joss_ann = JossAnnTransformer((x, y))(probe)(*init_args)
joss_ann = JossAnnTransformer((x, y))(probe)(**init_kwargs)
return joss_ann

@staticmethod
Expand Down
37 changes: 31 additions & 6 deletions axelrod/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from functools import wraps
import random
import copy
import inspect

from axelrod.actions import Actions, flip_action, Action
from .game import DefaultGame
Expand Down Expand Up @@ -73,7 +74,7 @@ class Player(object):
"""

name = "Player"
classifier = {} # type: Dict[str, Any]
classifier = {} # type: Dict[str, Any]
default_classifier = {
'stochastic': False,
'memory_depth': float('inf'),
Expand All @@ -87,10 +88,27 @@ class Player(object):
def __new__(cls, *args, **kwargs):
"""Caches arguments for Player cloning."""
obj = super().__new__(cls)
obj.init_args = args
obj.init_kwargs = kwargs
obj.init_kwargs = cls.init_params(*args, **kwargs)
return obj

@classmethod
def init_params(cls, *args, **kwargs):
"""
Return a dictionary containing the init parameters of a strategy (without 'self').
Use *args and *kwargs as value if specified
and complete the rest with the default values.
"""
sig = inspect.signature(cls.__init__)
# the 'self' parameter needs to be removed or the first *args will be assigned to it
self_param = sig.parameters.get('self', None)
if self_param is not None:
Copy link
Member

Choose a reason for hiding this comment

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

Does this conditional need to be present?

Copy link
Member Author

Choose a reason for hiding this comment

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

There should always be a 'self' parameters to get rid of, but since I'm calling list.remove which would fail if I call it with None, better be safe than sorry I guess. But I agree that there is no reason an __init__ would ever have no 'self' parameter so I guess I could remove this test.

new_params = list(sig.parameters.values())
new_params.remove(self_param)
sig = sig.replace(parameters=new_params)
boundargs = sig.bind_partial(*args, **kwargs)
boundargs.apply_defaults()
return boundargs.arguments

def __init__(self):
"""Initiates an empty history and 0 score for a player."""
self.history = []
Expand Down Expand Up @@ -122,8 +140,15 @@ def set_match_attributes(self, length=-1, game=None, noise=0):
self.receive_match_attributes()

def __repr__(self):
"""The string method for the strategy."""
return self.name
"""The string method for the strategy.
Appends the `__init__` parameters to the strategy's name."""
name = self.name
prefix = ': '
gen = (value for value in self.init_kwargs.values() if value is not None)
for value in gen:
name = ''.join([name, prefix, str(value if not isinstance(value, float) else round(value, 2))])
Copy link
Member

Choose a reason for hiding this comment

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

Technically strategies won't have unique names since Random(0.12) and Random(0.121) will get the same name. Are we all ok with that?

Copy link
Member

Choose a reason for hiding this comment

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

I would prefer it if we didn't round. Does that make things messy in some cases?

Copy link
Member Author

Choose a reason for hiding this comment

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

Originally, the __repr__ of Random was doing a round. When I print all strategies __repr__ with default values I get srings a bit long for those :

ZD-Extort-2: 0.1111111111111111, 0.5
ZD-Extort-2 v2: 0.125, 0.5, 1
ZD-Extort-4: 0.23529411764705882, 0.25, 1

So maybe use a bigger value for round or, if you prefer to be really precise and you don't mind __repr__ like my example above, I can suppress it

Copy link
Member

Choose a reason for hiding this comment

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

I suppose ultimately the floats in the __repr__ are being truncated anyway. I'm relatively indifferent between:

  • Leaving without any rounding (or in essence using the default rounding of a python float);
  • Perhaps rounding to 4 digits?

Copy link
Member

Choose a reason for hiding this comment

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

I prefer less aesthetically pleasing in favor of more uniqueness in this case

Copy link
Member

Choose a reason for hiding this comment

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

👍

prefix = ', '
return name

@staticmethod
def _add_noise(noise, s1, s2):
Expand Down Expand Up @@ -158,7 +183,7 @@ def clone(self):
# be significant changes required throughout the library.
# Override in special cases only if absolutely necessary
cls = self.__class__
new_player = cls(*self.init_args, **self.init_kwargs)
new_player = cls(**self.init_kwargs)
new_player.match_attributes = copy.copy(self.match_attributes)
return new_player

Expand Down
3 changes: 0 additions & 3 deletions axelrod/strategies/axelrod_first.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,9 +259,6 @@ def __init__(self, p: float = 0.9) -> None:
self.p = p
super().__init__(four_vector)

def __repr__(self) -> str:
return "%s: %s" % (self.name, round(self.p, 2))


class Nydegger(Player):
"""
Expand Down
25 changes: 12 additions & 13 deletions axelrod/strategies/cycler.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ def __init__(self, cycle: str = "CCD") -> None:
super().__init__()
self.cycle_str = cycle
self.cycle = self.get_new_itertools_cycle()
self.name = "Cycler {}".format(cycle)
self.classifier['memory_depth'] = len(cycle) - 1

def get_new_itertools_cycle(self):
Expand All @@ -99,8 +98,8 @@ class CyclerDC(Cycler):
classifier = copy.copy(Cycler.classifier)
classifier['memory_depth'] = 1

def __init__(self, cycle="DC") -> None:
super().__init__(cycle=cycle)
def __init__(self) -> None:
super().__init__(cycle="DC")


class CyclerCCD(Cycler):
Expand All @@ -109,8 +108,8 @@ class CyclerCCD(Cycler):
classifier = copy.copy(Cycler.classifier)
classifier['memory_depth'] = 2

def __init__(self, cycle="CCD") -> None:
super().__init__(cycle=cycle)
def __init__(self) -> None:
super().__init__(cycle="CCD")


class CyclerDDC(Cycler):
Expand All @@ -119,8 +118,8 @@ class CyclerDDC(Cycler):
classifier = copy.copy(Cycler.classifier)
classifier['memory_depth'] = 2

def __init__(self, cycle="DDC") -> None:
super().__init__(cycle=cycle)
def __init__(self) -> None:
super().__init__(cycle="DDC")


class CyclerCCCD(Cycler):
Expand All @@ -129,8 +128,8 @@ class CyclerCCCD(Cycler):
classifier = copy.copy(Cycler.classifier)
classifier['memory_depth'] = 3

def __init__(self, cycle="CCCD") -> None:
super().__init__(cycle=cycle)
def __init__(self) -> None:
super().__init__(cycle="CCCD")


class CyclerCCCCCD(Cycler):
Expand All @@ -139,8 +138,8 @@ class CyclerCCCCCD(Cycler):
classifier = copy.copy(Cycler.classifier)
classifier['memory_depth'] = 5

def __init__(self, cycle="CCCCCD") -> None:
super().__init__(cycle=cycle)
def __init__(self) -> None:
super().__init__(cycle="CCCCCD")


class CyclerCCCDCD(Cycler):
Expand All @@ -149,5 +148,5 @@ class CyclerCCCDCD(Cycler):
classifier = copy.copy(Cycler.classifier)
classifier['memory_depth'] = 5

def __init__(self, cycle="CCCDCD") -> None:
super().__init__(cycle=cycle)
def __init__(self) -> None:
super().__init__(cycle="CCCDCD")
4 changes: 3 additions & 1 deletion axelrod/strategies/gobymajority.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,16 @@ def __init__(self, memory_depth: int = float('inf'), soft: bool = True) -> None:
self.memory = self.classifier['memory_depth']
else:
self.memory = 0

self.name = (
'Go By Majority' + (self.memory > 0) * (": %i" % self.memory))
if self.soft:
self.name = "Soft " + self.name
else:
self.name = "Hard " + self.name

def __repr__(self):
Copy link
Member

Choose a reason for hiding this comment

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

This is the default __repr__ from Player, yes? If so I think we don't need to add it again explicitly.

Copy link
Member Author

Choose a reason for hiding this comment

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

When I don't override __repr__ here I get that :

Soft Go By Majority: inf, True
Soft Go By Majority: 10
Soft Go By Majority: 20
Soft Go By Majority: 40
Soft Go By Majority: 5
...
Hard Go By Majority: inf
Hard Go By Majority: 10
Hard Go By Majority: 20
Hard Go By Majority: 40
Hard Go By Majority: 5

The 'True' is redundant with the 'Soft' name, so I guess I should effectively get rid of this override but also get rid of this lines (gobymajority.py L54-59) :

        self.name = (
            'Go By Majority' + (self.memory > 0) * (": %i" % self.memory))
        if self.soft:
            self.name = "Soft " + self.name
        else:
            self.name = "Hard " + self.name

and add the 'Soft' manually in the names of the GoByMajority_number ? But that means the GoByMajority main class will no longer be call 'Soft Go By Majority' or 'Hard Go By Majority' but 'Go By Majority: True' or 'Go By Majority: False', are we okay with that ?

Copy link
Member

Choose a reason for hiding this comment

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

'Go By Majority: True' or 'Go By Majority: False', are we okay with that ?

I'm fine with that.

Copy link
Member

Choose a reason for hiding this comment

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

I think we should prefer "Soft" since it's the name in literature

Copy link
Member Author

@Chadys Chadys Apr 1, 2017

Choose a reason for hiding this comment

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

Knowing that it would just apply for the main GoByMajority, the SoftGoByMajority40 would still be called 'Soft Go By Majority 40', the same for all the other versions, and it would avoid an override of __repr__

Copy link
Member Author

Choose a reason for hiding this comment

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

For now I did the 'Go By Majority: True' option but if everyone agree on the other one, I will put back the override

Copy link
Member

Choose a reason for hiding this comment

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

On reflection I agree with @marcharper, keeping in line with the literature is good.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok !

Copy link
Member Author

Choose a reason for hiding this comment

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

do I still keep the ': inf' though ?

Copy link
Member

Choose a reason for hiding this comment

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

Perhaps it's not needed for the base one.

return self.name

def strategy(self, opponent: Player) -> Action:
"""This is affected by the history of the opponent.
Expand Down
6 changes: 4 additions & 2 deletions axelrod/strategies/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ def __init__(self, team=None):
for t in self.team:
self.classifier['makes_use_of'].update(t.classifier['makes_use_of'])

def __repr__(self):
team_size = len(self.team)
return '{}: {} player{}'.format(self.name, team_size, 's' if team_size > 1 else '')

def strategy(self, opponent):
# Get the results of all our players.
results = []
Expand Down Expand Up @@ -148,7 +152,6 @@ def reset(self):


NiceMetaWinner = NiceTransformer()(MetaWinner)
NiceMetaWinner.name = "Nice Meta Winner"


class MetaWinnerEnsemble(MetaWinner):
Expand All @@ -175,7 +178,6 @@ def meta_strategy(self, results, opponent):


NiceMetaWinnerEnsemble = NiceTransformer()(MetaWinnerEnsemble)
NiceMetaWinnerEnsemble.name = "Nice Meta Winner Ensemble"


class MetaHunter(MetaPlayer):
Expand Down
2 changes: 1 addition & 1 deletion axelrod/strategies/negation.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
class Negation(Player):
"""
A player starts by cooperating or defecting randomly if it's their first move,
then simply doing the opposite of the opponents last move thereafter.
then simply doing the opposite of the opponents last move thereafter.
Names:
Expand Down
3 changes: 0 additions & 3 deletions axelrod/strategies/prober.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,9 +281,6 @@ def strategy(self, opponent: Player) -> Action:
choice = random_choice(1 - self.p)
return choice

def __repr__(self) -> str:
return "%s: %s" % (self.name, round(self.p, 2))


class RemorsefulProber(NaiveProber):
"""
Expand Down
3 changes: 0 additions & 3 deletions axelrod/strategies/rand.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,3 @@ def __init__(self, p: float=0.5) -> None:

def strategy(self, opponent: Player) -> Action:
return random_choice(self.p)

def __repr__(self) -> str:
return "%s: %s" % (self.name, round(self.p, 2))
8 changes: 0 additions & 8 deletions axelrod/strategies/retaliate.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,6 @@ def __init__(self, retaliation_threshold: float = 0.1) -> None:
"""
super().__init__()
self.retaliation_threshold = retaliation_threshold
self.name = (
'Retaliate (' +
str(self.retaliation_threshold) + ')')
self.play_counts = defaultdict(int) # type: defaultdict

def strategy(self, opponent: Player) -> Action:
Expand Down Expand Up @@ -130,11 +127,6 @@ def __init__(self, retaliation_threshold: float = 0.1, retaliation_limit: int =
self.retaliation_limit = retaliation_limit
self.play_counts = defaultdict(int) # type: defaultdict

self.name = (
'Limited Retaliate (' +
str(self.retaliation_threshold) +
'/' + str(self.retaliation_limit) + ')')

def strategy(self, opponent: Player) -> Action:
"""
If the opponent has played D to my C more often than x% of the time
Expand Down
4 changes: 0 additions & 4 deletions axelrod/strategies/titfortat.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,10 +552,6 @@ def reset(self):
self.world = 0.5
self.rate = self.starting_rate

def __repr__(self) -> str:

return "%s: %s" % (self.name, round(self.rate, 2))


class SpitefulTitForTat(Player):
"""
Expand Down
28 changes: 23 additions & 5 deletions axelrod/strategy_transformers.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,23 +99,42 @@ def strategy(self, opponent):
return strategy_wrapper(self, opponent, proposed_action,
*args, **kwargs)

# Define a new class and wrap the strategy method
# Modify the PlayerClass name
new_class_name = PlayerClass.__name__
name = PlayerClass.name
name_prefix = self.name_prefix
if name_prefix:
# Modify the Player name (class variable inherited from Player)
new_class_name = name_prefix + PlayerClass.__name__
new_class_name = ''.join([name_prefix, PlayerClass.__name__])
# Modify the Player name (class variable inherited from Player)
name = name_prefix + ' ' + PlayerClass.name
name = ' '.join([name_prefix, PlayerClass.name])

# Define the new __repr__ method to add the wrapper arguments
# at the end of the name
def __repr__(self):
name = PlayerClass.__repr__(self)
# add eventual transformers' arguments in name
prefix = ': '
for arg in args:
try:
arg = [player.name for player in arg]
except TypeError:
pass
except AttributeError:
pass
name = ''.join([name, prefix, str(arg)])
prefix = ', '
return name

# Define a new class and wrap the strategy method
# Dynamically create the new class
new_class = type(
new_class_name, (PlayerClass,),
{
"name": name,
"original_class": PlayerClass,
"strategy": strategy,
"__repr__": __repr__,
"__module__": PlayerClass.__module__,
})
return new_class
Expand Down Expand Up @@ -193,8 +212,7 @@ def dual_wrapper(player, opponent, proposed_action):
action: an axelrod.Action, C or D
"""
if not player.history:
player.original_player = player.original_class(*player.init_args,
**player.init_kwargs)
player.original_player = player.original_class(**player.init_kwargs)

action = player.original_player.strategy(opponent)
player.original_player.history.append(action)
Expand Down
9 changes: 4 additions & 5 deletions axelrod/tests/strategies/test_axelrod_first.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

class TestDavis(TestPlayer):

name = "Davis"
name = "Davis: 10"
player = axelrod.Davis
expected_classifier = {
'memory_depth': float('inf'),
Expand Down Expand Up @@ -45,10 +45,9 @@ def test_strategy(self):
self.versus_test(opponent, expected_actions=actions)



class TestRevisedDowning(TestPlayer):

name = "Revised Downing"
name = "Revised Downing: True"
player = axelrod.RevisedDowning
expected_classifier = {
'memory_depth': float('inf'),
Expand Down Expand Up @@ -95,7 +94,7 @@ def test_not_revised(self):

class TestFeld(TestPlayer):

name = "Feld"
name = "Feld: 1.0, 0.5, 200"
player = axelrod.Feld
expected_classifier = {
'memory_depth': 200,
Expand Down Expand Up @@ -321,7 +320,7 @@ def test_strategy(self):

class TestTullock(TestPlayer):

name = "Tullock"
name = "Tullock: 11"
player = axelrod.Tullock
expected_classifier = {
'memory_depth': 11,
Expand Down
Loading