Skip to content

Commit 1947fb0

Browse files
authored
Merge pull request #1052 from eric-s-s/enum_actions
Enum actions #816
2 parents 7adefca + 742ce8e commit 1947fb0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+436
-295
lines changed

axelrod/actions.py

+52-18
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,65 @@
99
C, D = Actions.C, Actions.D
1010
"""
1111

12-
# Type alias for actions.
13-
Action = str
12+
from enum import Enum
13+
from typing import Iterable
14+
15+
16+
class UnknownActionError(ValueError):
17+
def __init__(self, *args):
18+
super(UnknownActionError, self).__init__(*args)
19+
20+
21+
class Actions(Enum):
22+
23+
C = 1
24+
D = 0
25+
26+
def __bool__(self):
27+
return bool(self.value)
1428

29+
def __repr__(self):
30+
return '{}'.format(self.name)
1531

16-
class Actions(object):
17-
C = 'C' # type: Action
18-
D = 'D' # type: Action
32+
def __str__(self):
33+
return '{}'.format(self.name)
34+
35+
def flip(self):
36+
"""Returns the opposite Action. """
37+
if self == Actions.C:
38+
return Actions.D
39+
if self == Actions.D:
40+
return Actions.C
41+
42+
@classmethod
43+
def from_char(cls, character):
44+
"""Converts a single character into an Action. `Action.from_char('C')`
45+
returns `Action.C`. `Action.from_char('CC')` raises an error. Use
46+
`str_to_actions` instead."""
47+
if character == 'C':
48+
return cls.C
49+
elif character == 'D':
50+
return cls.D
51+
else:
52+
raise UnknownActionError('Character must be "C" or "D".')
53+
54+
# Type alias for actions.
55+
Action = Actions
1956

2057

2158
def flip_action(action: Action) -> Action:
22-
if action == Actions.C:
23-
return Actions.D
24-
elif action == Actions.D:
25-
return Actions.C
26-
else:
27-
raise ValueError("Encountered a invalid action.")
59+
if not isinstance(action, Action):
60+
raise UnknownActionError('Not an Action')
61+
return action.flip()
2862

2963

3064
def str_to_actions(actions: str) -> tuple:
3165
"""Takes a string like 'CCDD' and returns a tuple of the appropriate
3266
actions."""
33-
action_dict = {'C': Actions.C,
34-
'D': Actions.D}
35-
try:
36-
return tuple(action_dict[action] for action in actions)
37-
except KeyError:
38-
raise ValueError(
39-
'The characters of "actions" str may only be "C" or "D"')
67+
return tuple(Actions.from_char(element) for element in actions)
68+
69+
70+
def actions_to_str(actions: Iterable[Action]) -> str:
71+
"""Takes any iterable of Action and returns a string of 'C's
72+
and 'D's. ex: (D, D, C) -> 'DDC' """
73+
return "".join(map(repr, actions))

axelrod/deterministic_cache.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class DeterministicCache(UserDict):
2525
resulting interactions. e.g. for a 3 turn Match between Cooperator and
2626
Alternator, the dictionary entry would be:
2727
28-
(axelrod.Cooperator, axelrod.Alternator): [('C', 'C'), ('C', 'D'), ('C', 'C')]
28+
(axelrod.Cooperator, axelrod.Alternator): [(C, C), (C, D), (C, C)]
2929
3030
Most of the functionality is provided by the UserDict class (which uses an
3131
instance of dict as the 'data' attribute to hold the dictionary entries).

axelrod/interaction_utils.py

+7-5
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import csv
1212
import tqdm
1313

14-
from axelrod.actions import Actions
14+
from axelrod.actions import Actions, str_to_actions
1515
from .game import Game
1616

1717

@@ -255,7 +255,9 @@ def read_interactions_from_file(filename, progress_bar=True,
255255
with open(filename, 'r') as f:
256256
for row in csv.reader(f):
257257
index_pair = (int(row[0]), int(row[1]))
258-
interaction = list(zip(row[4], row[5]))
258+
p1_actions = str_to_actions(row[4])
259+
p2_actions = str_to_actions(row[5])
260+
interaction = list(zip(p1_actions, p2_actions))
259261

260262
try:
261263
pairs_to_interactions[index_pair].append(interaction)
@@ -275,12 +277,12 @@ def string_to_interactions(string):
275277
Converts a compact string representation of an interaction to an
276278
interaction:
277279
278-
'CDCDDD' -> [('C', 'D'), ('C', 'D'), ('D', 'D')]
280+
'CDCDDD' -> [(C, D), (C, D), (D, D)]
279281
"""
280282
interactions = []
281283
interactions_list = list(string)
282284
while interactions_list:
283-
p1action = interactions_list.pop(0)
284-
p2action = interactions_list.pop(0)
285+
p1action = Actions.from_char(interactions_list.pop(0))
286+
p2action = Actions.from_char(interactions_list.pop(0))
285287
interactions.append((p1action, p2action))
286288
return interactions

axelrod/random_.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55

66
def random_choice(p: float = 0.5) -> Action:
77
"""
8-
Return 'C' with probability `p`, else return 'D'
8+
Return C with probability `p`, else return D
99
1010
No random sample is carried out if p is 0 or 1.
1111
1212
Parameters
1313
----------
1414
p : float
15-
The probability of picking 'C'
15+
The probability of picking C
1616
1717
Returns
1818
-------

axelrod/result_set.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from numpy import mean, nanmedian, std
55
import tqdm
66

7-
from axelrod.actions import Actions
7+
from axelrod.actions import Actions, str_to_actions
88
import axelrod.interaction_utils as iu
99
from . import eigen
1010
from .game import Game
@@ -848,8 +848,8 @@ def summarise(self):
848848
for player in self.normalised_state_to_action_distribution:
849849
rates = []
850850
for state in states:
851-
counts = [counter[(state, 'C')] for counter in player
852-
if counter[(state, 'C')] > 0]
851+
counts = [counter[(state, C)] for counter in player
852+
if counter[(state, C)] > 0]
853853

854854
if len(counts) > 0:
855855
rate = mean(counts)
@@ -1075,7 +1075,9 @@ def read_match_chunks(self, progress_bar=False):
10751075
count = 0
10761076
for row in csv_reader:
10771077
index_and_names = row[:4]
1078-
interactions = list(zip(row[4], row[5]))
1078+
p1_actions = str_to_actions(row[4])
1079+
p2_actions = str_to_actions(row[5])
1080+
interactions = list(zip(p1_actions, p2_actions))
10791081
repetitions.append(index_and_names + interactions)
10801082
count += 1
10811083
if progress_bar:

axelrod/strategies/appeaser.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
class Appeaser(Player):
88
"""A player who tries to guess what the opponent wants.
99
10-
Switch the classifier every time the opponent plays 'D'.
11-
Start with 'C', switch between 'C' and 'D' when opponent plays 'D'.
10+
Switch the classifier every time the opponent plays D.
11+
Start with C, switch between C and D when opponent plays D.
1212
1313
Names:
1414

axelrod/strategies/human.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def _get_human_input(self) -> Action: # pragma: no cover
136136
get_bottom_toolbar_tokens=self.status_messages['toolbar'],
137137
style=toolbar_style)
138138

139-
return action.upper()
139+
return Actions.from_char(action.upper())
140140

141141
def strategy(self, opponent: Player, input_function=None):
142142
"""

axelrod/strategies/lookerup.py

+9-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from collections import namedtuple
22
from itertools import product
33

4-
from axelrod.actions import Action, Actions, str_to_actions
4+
from axelrod.actions import Action, Actions, str_to_actions, actions_to_str
55
from axelrod.player import Player
66

77
from typing import Any, TypeVar
@@ -139,7 +139,8 @@ def display(self,
139139
:param sort_by: only_elements='self_plays', 'op_plays', 'op_openings'
140140
"""
141141
def sorter(plays):
142-
return tuple(getattr(plays, field) for field in sort_by)
142+
return tuple(actions_to_str(
143+
getattr(plays, field) for field in sort_by))
143144

144145
col_width = 11
145146
sorted_keys = sorted(self._dict, key=sorter)
@@ -148,9 +149,12 @@ def sorter(plays):
148149
'{str_list[2]:^{width}}')
149150
display_line = header_line.replace('|', ',') + ': {str_list[3]},'
150151

151-
line_elements = [(', '.join(getattr(key, sort_by[0])),
152-
', '.join(getattr(key, sort_by[1])),
153-
', '.join(getattr(key, sort_by[2])),
152+
def make_commaed_str(action_tuple):
153+
return ', '.join(str(action) for action in action_tuple)
154+
155+
line_elements = [(make_commaed_str(getattr(key, sort_by[0])),
156+
make_commaed_str(getattr(key, sort_by[1])),
157+
make_commaed_str(getattr(key, sort_by[2])),
154158
self._dict[key])
155159
for key in sorted_keys]
156160
header = header_line.format(str_list=sort_by, width=col_width) + '\n'

axelrod/strategies/meta.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def strategy(self, opponent):
8282
def meta_strategy(self, results, opponent):
8383
"""Determine the meta result based on results of all players.
8484
Override this function in child classes."""
85-
return 'C'
85+
return C
8686

8787
def reset(self):
8888
super().reset()

axelrod/strategies/qlearner.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from collections import OrderedDict
22
import random
33

4-
from axelrod.actions import Actions, Action
4+
from axelrod.actions import Actions, Action, actions_to_str
55
from axelrod.player import Player
66
from axelrod.random_ import random_choice
77

@@ -92,7 +92,9 @@ def find_state(self, opponent: Player) -> str:
9292
its previous proportion of playing C) as a hashable state
9393
"""
9494
prob = '{:.1f}'.format(opponent.cooperations)
95-
return ''.join(opponent.history[-self.memory_length:]) + prob
95+
action_str = actions_to_str(
96+
opponent.history[-self.memory_length:])
97+
return action_str + prob
9698

9799
def perform_q_learning(self, prev_state: str, state: str, action: Action, reward):
98100
"""

axelrod/strategies/shortmem.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ def strategy(opponent: Player) -> Action:
3737
return C
3838

3939
array = opponent.history[-10:]
40-
C_counts = array.count('C')
41-
D_counts = array.count('D')
40+
C_counts = array.count(C)
41+
D_counts = array.count(D)
4242

4343
if C_counts - D_counts >= 3:
4444
return C

axelrod/strategies/stalker.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
C, D = Actions.C, Actions.D
77

88

9-
@FinalTransformer((D), name_prefix=None) # End with defection
9+
@FinalTransformer((D,), name_prefix=None) # End with defection
1010
class Stalker(Player):
1111
"""
1212

axelrod/strategies/titfortat.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from axelrod.actions import Actions, Action
1+
from axelrod.actions import Actions, Action, actions_to_str
22
from axelrod.player import Player
33
from axelrod.random_ import random_choice
44
from axelrod.strategy_transformers import (
@@ -182,11 +182,11 @@ class SneakyTitForTat(Player):
182182

183183
def strategy(self, opponent: Player) -> Action:
184184
if len(self.history) < 2:
185-
return "C"
185+
return C
186186
if D not in opponent.history:
187187
return D
188188
if opponent.history[-1] == D and self.history[-2] == D:
189-
return "C"
189+
return C
190190
return opponent.history[-1]
191191

192192

@@ -297,7 +297,7 @@ def strategy(opponent: Player) -> Action:
297297
if not opponent.history:
298298
return C
299299
# Defects if two consecutive D in the opponent's last three moves
300-
history_string = "".join(opponent.history[-3:])
300+
history_string = actions_to_str(opponent.history[-3:])
301301
if 'DD' in history_string:
302302
return D
303303
# Otherwise cooperates

axelrod/strategy_transformers.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,13 @@ def __repr__(self):
130130
prefix = ': '
131131
for arg in args:
132132
try:
133-
arg = [player.name for player in arg]
134-
except TypeError:
135-
pass
133+
# Action has .name but should not be made into a list
134+
if not any(isinstance(el, Actions) for el in arg):
135+
arg = [player.name for player in arg]
136136
except AttributeError:
137137
pass
138+
except TypeError:
139+
pass
138140
name = ''.join([name, prefix, str(arg)])
139141
prefix = ', '
140142
return name

axelrod/tests/integration/test_matches.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,10 @@ def test_matches_with_det_player_for_stochastic_classes(self):
5252
p3 = axelrod.MemoryOnePlayer(four_vector=(1, 1, 1, 0))
5353

5454
m = axelrod.Match((p1, p2), turns=3)
55-
self.assertEqual(m.play(), [('C', 'C'), ('D', 'C'), ('D', 'D')])
55+
self.assertEqual(m.play(), [(C, C), (D, C), (D, D)])
5656

5757
m = axelrod.Match((p2, p3), turns=3)
58-
self.assertEqual(m.play(), [('C', 'C'), ('C', 'C'), ('C', 'C')])
58+
self.assertEqual(m.play(), [(C, C), (C, C), (C, C)])
5959

6060
m = axelrod.Match((p1, p3), turns=3)
61-
self.assertEqual(m.play(), [('C', 'C'), ('D', 'C'), ('D', 'C')])
61+
self.assertEqual(m.play(), [(C, C), (D, C), (D, C)])

axelrod/tests/strategies/test_axelrod_first.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ def test_strategy(self):
367367

368368
class TestSteinAndRapoport(TestPlayer):
369369

370-
name = "Stein and Rapoport: 0.05: ('D', 'D')"
370+
name = "Stein and Rapoport: 0.05: (D, D)"
371371
player = axelrod.SteinAndRapoport
372372
expected_classifier = {
373373
'memory_depth': 15,

axelrod/tests/strategies/test_backstabber.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
class TestBackStabber(TestPlayer):
1313

14-
name = "BackStabber: ('D', 'D')"
14+
name = "BackStabber: (D, D)"
1515
player = axelrod.BackStabber
1616
expected_classifier = {
1717
'memory_depth': float('inf'),
@@ -56,7 +56,7 @@ class TestDoubleCrosser(TestBackStabber):
5656
The alternate strategy is triggered when opponent did not defect in the
5757
first 7 rounds, and 8 <= the current round <= 180.
5858
"""
59-
name = "DoubleCrosser: ('D', 'D')"
59+
name = "DoubleCrosser: (D, D)"
6060
player = axelrod.DoubleCrosser
6161
expected_classifier = {
6262
'memory_depth': float('inf'),

axelrod/tests/strategies/test_cycler.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ def test_memory_depth_is_len_cycle_minus_one(self):
6868

6969
def test_cycler_works_as_expected(self):
7070
expected = [(C, D), (D, D), (D, D), (C, D)] * 2
71-
self.versus_test(axelrod.Defector(), expected_actions=expected, init_kwargs={'cycle': 'CDDC'})
71+
self.versus_test(axelrod.Defector(), expected_actions=expected,
72+
init_kwargs={'cycle': 'CDDC'})
7273

7374
def test_cycle_raises_value_error_on_bad_cycle_str(self):
7475
self.assertRaises(ValueError, Cycler, cycle='CdDC')

axelrod/tests/strategies/test_finite_state_machines.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ class TestSampleFSMPlayer(TestPlayer):
8181
"""Test a few sample tables to make sure that the finite state machines are
8282
working as intended."""
8383

84-
name = "FSM Player: ((1, 'C', 1, 'C'), (1, 'D', 1, 'D')), 1, C"
84+
name = "FSM Player: ((1, C, 1, C), (1, D, 1, D)), 1, C"
8585
player = axelrod.FSMPlayer
8686

8787
expected_classifier = {
@@ -137,7 +137,7 @@ def test_wsls(self):
137137

138138

139139
class TestFSMPlayer(TestPlayer):
140-
name = "FSM Player: ((1, 'C', 1, 'C'), (1, 'D', 1, 'D')), 1, C"
140+
name = "FSM Player: ((1, C, 1, C), (1, D, 1, D)), 1, C"
141141
player = axelrod.FSMPlayer
142142

143143
expected_classifier = {

0 commit comments

Comments
 (0)