Skip to content

Commit acf2672

Browse files
committed
Added some tests
1 parent b38a016 commit acf2672

File tree

7 files changed

+148
-49
lines changed

7 files changed

+148
-49
lines changed

axelrod/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
from axelrod.plot import Plot
1010
from axelrod.game import DefaultGame, Game
1111
from axelrod.history import History, LimitedHistory
12-
from axelrod.player import is_basic, obey_axelrod, Player
12+
from axelrod.player import Player
13+
from axelrod.classifier import Classifiers, is_basic, obey_axelrod
1314
from axelrod.evolvable_player import EvolvablePlayer
1415
from axelrod.mock_player import MockPlayer
1516
from axelrod.match import Match

axelrod/classifier.py

+109-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Any, Callable, Generic, List, Optional, Set, Text, Type, \
22
TypeVar, Union
33

4+
import os
45
import yaml
56

67
from axelrod.player import Player
@@ -11,11 +12,34 @@
1112

1213

1314
class Classifier(Generic[T]):
15+
"""Describes a Player (strategy).
16+
17+
User sets a name and function, f, at initialization. Through
18+
calc_for_player, looks for the classifier to be set in the passed Player
19+
class. If not set, then passes to f for calculation.
20+
21+
f must operate on the class, and not an instance. If necessary, f may
22+
initialize an instance, but this shouldn't depend on runtime states, because
23+
the result gets stored in a file. If a strategy's classifier depends on
24+
runtime states, such as those created by transformers, then it can set the
25+
field in its classifier dict, and that will take precedent over saved
26+
values.
27+
28+
Attributes
29+
----------
30+
name: An identifier for the classifier, used as a dict key in storage and in
31+
'classifier' dicts of Player classes.
32+
f: A function that takes in a Player class (not an instance) and returns a
33+
value.
34+
"""
35+
1436
def __init__(self, name: Text, f: Callable[[Type[Player]], T]):
1537
self.name = name
1638
self.f = f
1739

1840
def calc_for_player(self, player: Type[Player]) -> T:
41+
"""Look for this classifier in the passed player's 'classifier' dict,
42+
otherwise pass to the player to f."""
1943
if self.name in player.classifier:
2044
return player.classifier[self.name]
2145

@@ -33,6 +57,7 @@ def calc_for_player(self, player: Type[Player]) -> T:
3357
manipulates_state = Classifier[Optional[bool]]("manipulates_state",
3458
lambda _: None)
3559

60+
# Should list all known classifiers.
3661
all_classifiers = [
3762
stochastic,
3863
memory_depth,
@@ -47,37 +72,88 @@ def calc_for_player(self, player: Type[Player]) -> T:
4772
def rebuild_classifier_table(classifiers: List[Classifier],
4873
players: List[Type[Player]],
4974
path: Text = ALL_CLASSIFIERS_PATH) -> None:
75+
"""Builds the classifier table in data.
76+
77+
Parameters
78+
----------
79+
classifiers: A list of classifiers to calculate on the strategies
80+
players: A list of strategies (classes, not instances) to compute the
81+
classifiers for.
82+
path: Where to save the resulting yaml file.
83+
"""
84+
# Get absolute path
85+
dirname = os.path.dirname(__file__)
86+
filename = os.path.join(dirname, path)
87+
5088
all_player_dicts = dict()
5189
for p in players:
5290
new_player_dict = dict()
5391
for c in classifiers:
5492
new_player_dict[c.name] = c.calc_for_player(p)
5593
all_player_dicts[p.name] = new_player_dict
5694

57-
with open(path, 'w') as f:
95+
with open(filename, 'w') as f:
5896
yaml.dump(all_player_dicts, f)
5997

6098

6199
class Classifiers(object):
100+
"""A singleton used to calculate any known classifier.
101+
102+
Attributes
103+
----------
104+
all_player_dicts: A local copy of the dict saved in the classifier table.
105+
The keys are player names, and the values are 'classifier' dicts (keyed
106+
by classifier name).
107+
"""
62108
_instance = None
63109
all_player_dicts = dict()
64110

65111
# Make this a singleton
66112
def __new__(cls):
67113
if cls._instance is None:
68114
cls._instance = super(Classifiers, cls).__new__(cls)
69-
with open(ALL_CLASSIFIERS_PATH, 'r') as f:
115+
# When this is first created, read from the classifier table file.
116+
# Get absolute path
117+
dirname = os.path.dirname(__file__)
118+
filename = os.path.join(dirname, ALL_CLASSIFIERS_PATH)
119+
with open(filename, 'r') as f:
70120
cls.all_player_dicts = yaml.load(f, Loader=yaml.FullLoader)
71121

72122
return cls._instance
73123

124+
@classmethod
125+
def known_classifier(cls, classifier_name: Text) -> bool:
126+
"""Returns True if the passed classifier_name is known."""
127+
global all_classifiers
128+
return classifier_name in (c.name for c in all_classifiers)
129+
74130
@classmethod
75131
def get(cls, classifier: Union[Classifier, Text],
76132
player: Player) -> Any:
133+
"""Looks up the classifier for the player.
134+
135+
If the classifier is found in the 'classifier' dict on the player, then
136+
return that. Otherwise look for the classifier for the player in the
137+
all_player_dicts. Returns None if the classifier is not found in either
138+
of those.
139+
140+
Parameters
141+
----------
142+
classifier: A classifier or classifier name that we want to calculate
143+
for the player.
144+
player: The player (instance) for which we compute the classifier.
145+
146+
Returns
147+
-------
148+
The classifier value for the player, or None if unknown.
149+
"""
77150
# Classifier may be the name or an instance. Convert to name.
78151
if not isinstance(classifier, str):
79152
classifier = classifier.name
80153

154+
if not cls.known_classifier(classifier):
155+
raise KeyError("Unknown classifier")
156+
81157
# Factory-generated players won't exist in the table. As well, some
82158
# players, like Random, may change classifiers at construction time;
83159
# this get() function takes a player instance, while the saved-values
@@ -102,3 +178,34 @@ def return_missing() -> None:
102178
if classifier not in player_classifiers:
103179
return return_missing()
104180
return player_classifiers[classifier]
181+
182+
183+
# Strategy classifiers
184+
185+
def is_basic(s):
186+
"""
187+
Defines criteria for a strategy to be considered 'basic'
188+
"""
189+
stochastic = Classifiers().get("stochastic", s)
190+
depth = Classifiers().get("memory_depth", s)
191+
inspects_source = Classifiers().get("inspects_source", s)
192+
manipulates_source = Classifiers().get("manipulates_source", s)
193+
manipulates_state = Classifiers().get("manipulates_state", s)
194+
return (
195+
not stochastic
196+
and not inspects_source
197+
and not manipulates_source
198+
and not manipulates_state
199+
and depth in (0, 1)
200+
)
201+
202+
203+
def obey_axelrod(s):
204+
"""
205+
A function to check if a strategy obeys Axelrod's original tournament
206+
rules.
207+
"""
208+
for c in ["inspects_source", "manipulates_source", "manipulates_state"]:
209+
if Classifiers().get(c, s):
210+
return False
211+
return True

axelrod/player.py

-33
Original file line numberDiff line numberDiff line change
@@ -7,46 +7,13 @@
77
import numpy as np
88

99
from axelrod.action import Action
10-
from axelrod.classifier import Classifiers
1110
from axelrod.game import DefaultGame
1211
from axelrod.history import History
1312
from axelrod.random_ import random_flip
1413

1514
C, D = Action.C, Action.D
1615

1716

18-
# Strategy classifiers
19-
20-
21-
def is_basic(s):
22-
"""
23-
Defines criteria for a strategy to be considered 'basic'
24-
"""
25-
stochastic = Classifiers().get("stochastic", s)
26-
depth = Classifiers().get("depth", s)
27-
inspects_source = Classifiers().get("inspects_source", s)
28-
manipulates_source = Classifiers().get("manipulates_source", s)
29-
manipulates_state = Classifiers().get("manipulates_state", s)
30-
return (
31-
not stochastic
32-
and not inspects_source
33-
and not manipulates_source
34-
and not manipulates_state
35-
and depth in (0, 1)
36-
)
37-
38-
39-
def obey_axelrod(s):
40-
"""
41-
A function to check if a strategy obeys Axelrod's original tournament
42-
rules.
43-
"""
44-
for c in ["inspects_source", "manipulates_source", "manipulates_state"]:
45-
if Classifiers().get(c, s):
46-
return False
47-
return True
48-
49-
5017
def simultaneous_play(player, coplayer, noise=0):
5118
"""This pits two players against each other."""
5219
s1, s2 = player.strategy(coplayer), coplayer.strategy(player)

axelrod/strategies/__init__.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
from ..classifier import Classifiers
2-
from ..player import is_basic, obey_axelrod
1+
from ..classifier import Classifiers, is_basic, obey_axelrod
32
from ._strategies import *
43
from ._filters import passes_filterset
54

@@ -85,10 +84,10 @@
8584
strategies = [s for s in all_strategies if obey_axelrod(s())]
8685

8786
long_run_time_strategies = [
88-
s for s in all_strategies if Classifiers().get("long_run_time", s)
87+
s for s in all_strategies if Classifiers().get("long_run_time", s())
8988
]
9089
short_run_time_strategies = [
91-
s for s in strategies if not Classifiers().get("long_run_time", s)
90+
s for s in strategies if not Classifiers().get("long_run_time", s())
9291
]
9392
cheating_strategies = [s for s in all_strategies if not obey_axelrod(s())]
9493

axelrod/strategies/meta.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
from numpy.random import choice
55

66
from axelrod.action import Action
7-
from axelrod.classifier import Classifiers
8-
from axelrod.player import Player, obey_axelrod
7+
from axelrod.classifier import Classifiers, obey_axelrod
8+
from axelrod.player import Player
99
from axelrod.strategies import TitForTat
1010
from axelrod.strategy_transformers import NiceTransformer
1111
from ._strategies import all_strategies

axelrod/tests/unit/test_classification.py

+30-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,39 @@
11
"""Tests for the classification."""
22

3+
import os
34
import unittest
5+
from typing import Text
6+
7+
import yaml
48

59
import axelrod as axl
6-
from axelrod.classifier import Classifiers
10+
from axelrod.classifier import Classifier, Classifiers, rebuild_classifier_table
711

812

913
class TestClassification(unittest.TestCase):
14+
def test_classifier_build(self):
15+
test_path = "../test_outputs/classifier_test.yaml"
16+
17+
# Just returns the name of the player. For testing.
18+
name_classifier = Classifier[Text]("name", lambda player: player.name)
19+
rebuild_classifier_table(classifiers=[name_classifier],
20+
players=[axl.Cooperator, axl.Defector],
21+
path=test_path)
22+
23+
filename = os.path.join("../..", test_path)
24+
with open(filename, 'r') as f:
25+
all_player_dicts = yaml.load(f, Loader=yaml.FullLoader)
26+
27+
self.assertDictEqual(all_player_dicts,
28+
{"Cooperator": {"name": "Cooperator"},
29+
"Defector": {"name": "Defector"}})
30+
31+
def test_singletonity_of_classifiers_class(self):
32+
classifiers_1 = Classifiers()
33+
classifiers_2 = Classifiers()
34+
35+
self.assertIs(classifiers_1, classifiers_2)
36+
1037
def test_known_classifiers(self):
1138
# A set of dimensions that are known to have been fully applied
1239
known_keys = [
@@ -223,7 +250,7 @@ def test_long_run_strategies(self):
223250
str_reps(axl.long_run_time_strategies)
224251
)
225252
self.assertTrue(
226-
all(Classifiers().get("long_run_time", s) for s in
253+
all(Classifiers().get("long_run_time", s()) for s in
227254
axl.long_run_time_strategies)
228255
)
229256

@@ -237,7 +264,7 @@ def test_short_run_strategies(self):
237264
str_reps(axl.short_run_time_strategies)
238265
)
239266
self.assertFalse(
240-
any(Classifiers().get("long_run_time", s) for s in
267+
any(Classifiers().get("long_run_time", s()) for s in
241268
axl.short_run_time_strategies)
242269
)
243270

rebuild_classifier_table.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import os
22

33
from axelrod import all_strategies
4-
from axelrod.classifier import ALL_CLASSIFIERS_PATH, all_classifiers, \
5-
rebuild_classifier_table
4+
from axelrod.classifier import all_classifiers, rebuild_classifier_table
65

76
if __name__ == "__main__":
87
# Change to relative path inside axelrod folder
9-
axelrod_path = os.path.join("axelrod", ALL_CLASSIFIERS_PATH)
10-
rebuild_classifier_table(all_classifiers, all_strategies, path=axelrod_path)
8+
rebuild_classifier_table(all_classifiers, all_strategies)

0 commit comments

Comments
 (0)