Skip to content

Commit ecad2a1

Browse files
Merge pull request #1300 from gaffney2010/classifiers
Move classifiers outside of Players
2 parents d4735cb + 53a6371 commit ecad2a1

32 files changed

+2796
-561
lines changed

axelrod/__init__.py

Lines changed: 2 additions & 1 deletion
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
1314
from axelrod.evolvable_player import EvolvablePlayer
1415
from axelrod.mock_player import MockPlayer
1516
from axelrod.match import Match

axelrod/classifier.py

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
import os
2+
from typing import (
3+
Any,
4+
Callable,
5+
Generic,
6+
List,
7+
Optional,
8+
Set,
9+
Text,
10+
Type,
11+
TypeVar,
12+
Union,
13+
)
14+
import warnings
15+
import yaml
16+
17+
from axelrod.player import Player
18+
19+
ALL_CLASSIFIERS_PATH = "data/all_classifiers.yml"
20+
21+
T = TypeVar("T")
22+
23+
24+
class Classifier(Generic[T]):
25+
"""Describes a Player (strategy).
26+
27+
User sets a name and function, f, at initialization. Through
28+
classify_player, looks for the classifier to be set in the passed Player
29+
class. If not set, then passes to f for calculation.
30+
31+
f must operate on the class, and not an instance. If necessary, f may
32+
initialize an instance, but this shouldn't depend on runtime states, because
33+
the result gets stored in a file. If a strategy's classifier depends on
34+
runtime states, such as those created by transformers, then it can set the
35+
field in its classifier dict, and that will take precedent over saved
36+
values.
37+
38+
Attributes
39+
----------
40+
name: An identifier for the classifier, used as a dict key in storage and in
41+
'classifier' dicts of Player classes.
42+
player_class_classifier: A function that takes in a Player class (not an
43+
instance) and returns a value.
44+
"""
45+
46+
def __init__(
47+
self, name: Text, player_class_classifier: Callable[[Type[Player]], T]
48+
):
49+
self.name = name
50+
self.player_class_classifier = player_class_classifier
51+
52+
def classify_player(self, player: Type[Player]) -> T:
53+
"""Look for this classifier in the passed player's 'classifier' dict,
54+
otherwise pass to the player to f."""
55+
try:
56+
return player.classifier[self.name]
57+
except:
58+
return self.player_class_classifier(player)
59+
60+
61+
stochastic = Classifier[bool]("stochastic", lambda _: False)
62+
memory_depth = Classifier[Union[float, int]]("memory_depth", lambda _: float("inf"))
63+
makes_use_of = Classifier[Optional[Set[Text]]]("makes_use_of", lambda _: None)
64+
long_run_time = Classifier[bool]("long_run_time", lambda _: False)
65+
inspects_source = Classifier[Optional[bool]]("inspects_source", lambda _: None)
66+
manipulates_source = Classifier[Optional[bool]]("manipulates_source", lambda _: None)
67+
manipulates_state = Classifier[Optional[bool]]("manipulates_state", lambda _: None)
68+
69+
# Should list all known classifiers.
70+
all_classifiers = [
71+
stochastic,
72+
memory_depth,
73+
makes_use_of,
74+
long_run_time,
75+
inspects_source,
76+
manipulates_source,
77+
manipulates_state,
78+
]
79+
80+
81+
def rebuild_classifier_table(
82+
classifiers: List[Classifier],
83+
players: List[Type[Player]],
84+
path: Text = ALL_CLASSIFIERS_PATH,
85+
) -> None:
86+
"""Builds the classifier table in data.
87+
88+
Parameters
89+
----------
90+
classifiers: A list of classifiers to calculate on the strategies
91+
players: A list of strategies (classes, not instances) to compute the
92+
classifiers for.
93+
path: Where to save the resulting yaml file.
94+
"""
95+
# Get absolute path
96+
dirname = os.path.dirname(__file__)
97+
filename = os.path.join(dirname, path)
98+
99+
all_player_dicts = dict()
100+
for p in players:
101+
new_player_dict = dict()
102+
for c in classifiers:
103+
new_player_dict[c.name] = c.classify_player(p)
104+
all_player_dicts[p.name] = new_player_dict
105+
106+
with open(filename, "w") as f:
107+
yaml.dump(all_player_dicts, f)
108+
109+
110+
class _Classifiers(object):
111+
"""A singleton used to calculate any known classifier.
112+
113+
Attributes
114+
----------
115+
all_player_dicts: A local copy of the dict saved in the classifier table.
116+
The keys are player names, and the values are 'classifier' dicts (keyed
117+
by classifier name).
118+
"""
119+
120+
_instance = None
121+
all_player_dicts = dict()
122+
123+
# Make this a singleton
124+
def __new__(cls):
125+
if cls._instance is None:
126+
cls._instance = super(_Classifiers, cls).__new__(cls)
127+
# When this is first created, read from the classifier table file.
128+
# Get absolute path
129+
dirname = os.path.dirname(__file__)
130+
filename = os.path.join(dirname, ALL_CLASSIFIERS_PATH)
131+
with open(filename, "r") as f:
132+
cls.all_player_dicts = yaml.load(f, Loader=yaml.FullLoader)
133+
134+
return cls._instance
135+
136+
@classmethod
137+
def known_classifier(cls, classifier_name: Text) -> bool:
138+
"""Returns True if the passed classifier_name is known."""
139+
global all_classifiers
140+
return classifier_name in (c.name for c in all_classifiers)
141+
142+
@classmethod
143+
def __getitem__(
144+
cls, key: Union[Classifier, Text]
145+
) -> Callable[[Union[Player, Type[Player]]], Any]:
146+
"""Looks up the classifier for the player.
147+
148+
Given a passed classifier key, return a function that:
149+
150+
Takes a player. If the classifier is found in the 'classifier' dict on
151+
the player, then return that. Otherwise look for the classifier for the
152+
player in the all_player_dicts. Returns None if the classifier is not
153+
found in either of those.
154+
155+
The returned function expects Player instances, but if a Player class is
156+
passed, then it will create an instance by calling an argument-less
157+
initializer. If no such initializer exists on the class, then an error
158+
will result.
159+
160+
Parameters
161+
----------
162+
key: A classifier or classifier name that we want to calculate for the
163+
player.
164+
165+
Returns
166+
-------
167+
A function that will map Player (or Player instances) to their value for
168+
this classification.
169+
"""
170+
# Key may be the name or an instance. Convert to name.
171+
if not isinstance(key, str):
172+
key = key.name
173+
174+
if not cls.known_classifier(key):
175+
raise KeyError("Unknown classifier")
176+
177+
def classify_player_for_this_classifier(
178+
player: Union[Player, Type[Player]]
179+
) -> Any:
180+
def try_lookup() -> Any:
181+
try:
182+
player_classifiers = cls.all_player_dicts[player.name]
183+
except:
184+
return None
185+
186+
return player_classifiers.get(key, None)
187+
188+
# If the passed player is not an instance, then try to initialize an
189+
# instance without arguments.
190+
if not isinstance(player, Player):
191+
try:
192+
player = player()
193+
warnings.warn(
194+
"Classifiers are intended to run on player instances. "
195+
"Passed player {} was initialized with default "
196+
"arguments.".format(player.name)
197+
)
198+
except:
199+
# All strategies must have trivial initializers.
200+
raise Exception(
201+
"Passed player class doesn't have a trivial initializer."
202+
)
203+
204+
# Factory-generated players won't exist in the table. As well, some
205+
# players, like Random, may change classifiers at construction time;
206+
# this get() function takes a player instance, while the saved-values
207+
# are from operations on the player object itself.
208+
if key in player.classifier:
209+
return player.classifier[key]
210+
211+
# Try to find the name in the all_player_dicts, read from disk.
212+
return try_lookup()
213+
214+
return classify_player_for_this_classifier
215+
216+
@classmethod
217+
def is_basic(cls, s: Union[Player, Type[Player]]):
218+
"""
219+
Defines criteria for a strategy to be considered 'basic'
220+
"""
221+
stochastic = cls.__getitem__("stochastic")(s)
222+
depth = cls.__getitem__("memory_depth")(s)
223+
inspects_source = cls.__getitem__("inspects_source")(s)
224+
manipulates_source = cls.__getitem__("manipulates_source")(s)
225+
manipulates_state = cls.__getitem__("manipulates_state")(s)
226+
return (
227+
not stochastic
228+
and not inspects_source
229+
and not manipulates_source
230+
and not manipulates_state
231+
and depth in (0, 1)
232+
)
233+
234+
@classmethod
235+
def obey_axelrod(cls, s: Union[Player, Type[Player]]):
236+
"""
237+
A function to check if a strategy obeys Axelrod's original tournament
238+
rules.
239+
"""
240+
for c in ["inspects_source", "manipulates_source", "manipulates_state"]:
241+
if cls.__getitem__(c)(s):
242+
return False
243+
return True
244+
245+
246+
Classifiers = _Classifiers()

0 commit comments

Comments
 (0)