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

Adaptive Zero Determinant Strategy #1282

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions axelrod/strategies/_strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@
ZDGen2,
ZDMischief,
ZDSet2,
AdaptiveZeroDet,
)

# Note: Meta* strategies are handled in .__init__.py
Expand All @@ -242,6 +243,7 @@
all_strategies = [
Adaptive,
AdaptiveTitForTat,
AdaptiveZeroDet,
AdaptorBrief,
AdaptorLong,
Aggravater,
Expand Down
2 changes: 1 addition & 1 deletion axelrod/strategies/grudger.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ class ForgetfulGrudger(Player):

def __init__(self) -> None:
"""Initialised the player."""
super().__init__()
self.mem_length = 10
super().__init__()
self.grudged = False
self.grudge_memory = 0

Expand Down
76 changes: 76 additions & 0 deletions axelrod/strategies/zero_determinant.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import random
from axelrod.action import Action
from axelrod.player import Player

from .memoryone import MemoryOnePlayer

Expand Down Expand Up @@ -225,3 +227,77 @@ class ZDSet2(LRPlayer):

def __init__(self, phi: float = 1 / 4, s: float = 0.0, l: float = 2) -> None:
super().__init__(phi, s, l)


class AdaptiveZeroDet(LRPlayer):
"""A strategy that uses a zero determinant structure that updates
its parameters after each round of play.
Copy link
Member

Choose a reason for hiding this comment

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

It would be helpful to have some more information here, perhaps an explanation of the update procedure? It's ok to go quite in depth and include some mathematics which we can then use to check that the _adjust_parameters method is working as expected.


Names:
- AdaptiveZeroDet by Emmanuel Estrada and Dashiell Fryer
"""
name = 'AdaptiveZeroDet'
classifier = {
'memory_depth': float('inf'), # Long memory
'stochastic': True,
'makes_use_of': set(["game"]),
'long_run_time': False,
'inspects_source': False,
'manipulates_source': False,
'manipulates_state': False
}

def __init__(self, phi: float = 0.125, s: float = 0.5, l: float = 3,
initial: Action = C) -> None:
# This Keeps track of the parameter values (phi,s,l) as well as the
# four vector which makes final decisions.
super().__init__(phi=phi, s=s, l=l)
self._scores = {C: 0, D: 0}
self._initial = initial

def score_last_round(self, opponent: Player):
"""This gives the strategy the game attributes and allows the strategy
to score itself properly."""
game = self.match_attributes["game"]
if len(self.history):
last_round = (self.history[-1], opponent.history[-1])
scores = game.score(last_round)
self._scores[last_round[0]] += scores[0]

def _adjust_parameters(self):
d = random.randint(0, 9) / 1000 # Selects random value to adjust s and l
R, P, S, T = self.match_attributes["game"].RPST()
l = self.l
s = self.s
if self._scores[C] > self._scores[D]:
# This checks scores to determine how to adjust s and l either
# up or down by d, making sure not to exceed bounds.
if l + d > R:
l = (l + R) / 2
else:
l += d
s_min = - min((T - l) / (l - S), (l - S) / (T - l))
if s - d < s_min:
s = (s + s_min) / 2
else:
s = s - d
else:
# This adjusts s and l in the opposite direction, also checking distance
if l - d < P:
l = (l + P) / 2
else:
l -= d
if s + d > 1:
s = (s + 1) / 2
else:
s += d
# Update the four vector for the new l and s values
self.l = l
self.s = s
self.receive_match_attributes()

def strategy(self, opponent: Player) -> Action:
if len(self.history) > 0:
self.score_last_round(opponent)
self._adjust_parameters()
return super().strategy(opponent)
43 changes: 43 additions & 0 deletions axelrod/tests/strategies/test_zero_determinant.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,3 +317,46 @@ def test_strategy(self):

actions = [(C, D), (D, C), (D, D), (D, C), (D, D), (D, C)]
self.versus_test(opponent=axelrod.CyclerDC(), expected_actions=actions, seed=5)


class TestAdaptiveZeroDet(TestPlayer):
"""
Test the AdaptiveZeroDet strategy.
"""

name = "AdaptiveZeroDet: 0.125, 0.5, 3, C"
player = axelrod.AdaptiveZeroDet
expected_classifier = {
"memory_depth": float('inf'),
"stochastic": True,
"makes_use_of": set(['game']),
"long_run_time": False,
"inspects_source": False,
"manipulates_source": False,
"manipulates_state": False,
}

Copy link
Member

Choose a reason for hiding this comment

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

Could we add few tests for the two specific methods: _adjust_parameters and score_last_round (just running the methods and checking that the attributes have changed accordingly?

def test_strategy(self):
R, P, S, T = axelrod.Game().RPST()

player = self.player(l=R)
axelrod.seed(0)
match = axelrod.Match((player, axelrod.Alternator()), turns=200)
match.play()
Comment on lines +342 to +345
Copy link
Member

Choose a reason for hiding this comment

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

This tests need to be tweaked so that we can check expected behaviours. There's information on writing tests here: https://axelrod.readthedocs.io/en/stable/tutorials/contributing/strategy/writing_test_for_the_new_strategy.html

For example this specific paragraph would be something like (although I am not sure what the expected behaviour is):

Suggested change
player = self.player(l=R)
axelrod.seed(0)
match = axelrod.Match((player, axelrod.Alternator()), turns=200)
match.play()
expected_actions = [(C,D), (D, C)] * 100
self.versus_test(opponent=axelrod.Alternator),
expected_actions=expected_actions, seed=0, init_kwargs={"l": R})

Note that we could also check that the other attributes have changed accordingly using the attrs keyword.

Let me know if I can assist with that (happy to PR the tests to your branch).

Copy link
Member

Choose a reason for hiding this comment

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

I agree but I didn't want to search for seeds just to have to do so again once #1288 goes in. The current tests make sure that at least each branch is run (so that the limits are bounced into).

Copy link
Member

Choose a reason for hiding this comment

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

I understand. Are you suggesting this are place holders and we merge #1288 and then fix these?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, after we merge this I'll update all the tests that require seeds in #1288 (there are many and most will have to change).

Copy link
Member

Choose a reason for hiding this comment

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

Sounds good. Happy to take a share of that work if helpful.


player = self.player(l=P)
axelrod.seed(0)
match = axelrod.Match((player, axelrod.Alternator()), turns=200)
match.play()

player = self.player(s=1)
axelrod.seed(0)
match = axelrod.Match((player, axelrod.Alternator()), turns=200)
match.play()

l = 2
s_min = - min((T - l) / (l - S), (l - S) / (T - l))
player = self.player(s=s_min, l=2)
axelrod.seed(0)
match = axelrod.Match((player, axelrod.Alternator()), turns=200)
match.play()
2 changes: 1 addition & 1 deletion docs/tutorials/advanced/classification_of_strategies.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ strategies::
... }
>>> strategies = axl.filtered_strategies(filterset)
>>> len(strategies)
88
89

Or, to find out how many strategies only use 1 turn worth of memory to
make a decision::
Expand Down