Skip to content

Commit

Permalink
Added some tests
Browse files Browse the repository at this point in the history
  • Loading branch information
gaffney2010 committed Mar 27, 2020
1 parent b38a016 commit acf2672
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 49 deletions.
3 changes: 2 additions & 1 deletion axelrod/__init__.py
Expand Up @@ -9,7 +9,8 @@
from axelrod.plot import Plot
from axelrod.game import DefaultGame, Game
from axelrod.history import History, LimitedHistory
from axelrod.player import is_basic, obey_axelrod, Player
from axelrod.player import Player
from axelrod.classifier import Classifiers, is_basic, obey_axelrod
from axelrod.evolvable_player import EvolvablePlayer
from axelrod.mock_player import MockPlayer
from axelrod.match import Match
Expand Down
111 changes: 109 additions & 2 deletions axelrod/classifier.py
@@ -1,6 +1,7 @@
from typing import Any, Callable, Generic, List, Optional, Set, Text, Type, \
TypeVar, Union

import os
import yaml

from axelrod.player import Player
Expand All @@ -11,11 +12,34 @@


class Classifier(Generic[T]):
"""Describes a Player (strategy).
User sets a name and function, f, at initialization. Through
calc_for_player, looks for the classifier to be set in the passed Player
class. If not set, then passes to f for calculation.
f must operate on the class, and not an instance. If necessary, f may
initialize an instance, but this shouldn't depend on runtime states, because
the result gets stored in a file. If a strategy's classifier depends on
runtime states, such as those created by transformers, then it can set the
field in its classifier dict, and that will take precedent over saved
values.
Attributes
----------
name: An identifier for the classifier, used as a dict key in storage and in
'classifier' dicts of Player classes.
f: A function that takes in a Player class (not an instance) and returns a
value.
"""

def __init__(self, name: Text, f: Callable[[Type[Player]], T]):
self.name = name
self.f = f

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

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

# Should list all known classifiers.
all_classifiers = [
stochastic,
memory_depth,
Expand All @@ -47,37 +72,88 @@ def calc_for_player(self, player: Type[Player]) -> T:
def rebuild_classifier_table(classifiers: List[Classifier],
players: List[Type[Player]],
path: Text = ALL_CLASSIFIERS_PATH) -> None:
"""Builds the classifier table in data.
Parameters
----------
classifiers: A list of classifiers to calculate on the strategies
players: A list of strategies (classes, not instances) to compute the
classifiers for.
path: Where to save the resulting yaml file.
"""
# Get absolute path
dirname = os.path.dirname(__file__)
filename = os.path.join(dirname, path)

all_player_dicts = dict()
for p in players:
new_player_dict = dict()
for c in classifiers:
new_player_dict[c.name] = c.calc_for_player(p)
all_player_dicts[p.name] = new_player_dict

with open(path, 'w') as f:
with open(filename, 'w') as f:
yaml.dump(all_player_dicts, f)


class Classifiers(object):
"""A singleton used to calculate any known classifier.
Attributes
----------
all_player_dicts: A local copy of the dict saved in the classifier table.
The keys are player names, and the values are 'classifier' dicts (keyed
by classifier name).
"""
_instance = None
all_player_dicts = dict()

# Make this a singleton
def __new__(cls):
if cls._instance is None:
cls._instance = super(Classifiers, cls).__new__(cls)
with open(ALL_CLASSIFIERS_PATH, 'r') as f:
# When this is first created, read from the classifier table file.
# Get absolute path
dirname = os.path.dirname(__file__)
filename = os.path.join(dirname, ALL_CLASSIFIERS_PATH)
with open(filename, 'r') as f:
cls.all_player_dicts = yaml.load(f, Loader=yaml.FullLoader)

return cls._instance

@classmethod
def known_classifier(cls, classifier_name: Text) -> bool:
"""Returns True if the passed classifier_name is known."""
global all_classifiers
return classifier_name in (c.name for c in all_classifiers)

@classmethod
def get(cls, classifier: Union[Classifier, Text],
player: Player) -> Any:
"""Looks up the classifier for the player.
If the classifier is found in the 'classifier' dict on the player, then
return that. Otherwise look for the classifier for the player in the
all_player_dicts. Returns None if the classifier is not found in either
of those.
Parameters
----------
classifier: A classifier or classifier name that we want to calculate
for the player.
player: The player (instance) for which we compute the classifier.
Returns
-------
The classifier value for the player, or None if unknown.
"""
# Classifier may be the name or an instance. Convert to name.
if not isinstance(classifier, str):
classifier = classifier.name

if not cls.known_classifier(classifier):
raise KeyError("Unknown classifier")

# Factory-generated players won't exist in the table. As well, some
# players, like Random, may change classifiers at construction time;
# this get() function takes a player instance, while the saved-values
Expand All @@ -102,3 +178,34 @@ def return_missing() -> None:
if classifier not in player_classifiers:
return return_missing()
return player_classifiers[classifier]


# Strategy classifiers

def is_basic(s):
"""
Defines criteria for a strategy to be considered 'basic'
"""
stochastic = Classifiers().get("stochastic", s)
depth = Classifiers().get("memory_depth", s)
inspects_source = Classifiers().get("inspects_source", s)
manipulates_source = Classifiers().get("manipulates_source", s)
manipulates_state = Classifiers().get("manipulates_state", s)
return (
not stochastic
and not inspects_source
and not manipulates_source
and not manipulates_state
and depth in (0, 1)
)


def obey_axelrod(s):
"""
A function to check if a strategy obeys Axelrod's original tournament
rules.
"""
for c in ["inspects_source", "manipulates_source", "manipulates_state"]:
if Classifiers().get(c, s):
return False
return True
33 changes: 0 additions & 33 deletions axelrod/player.py
Expand Up @@ -7,46 +7,13 @@
import numpy as np

from axelrod.action import Action
from axelrod.classifier import Classifiers
from axelrod.game import DefaultGame
from axelrod.history import History
from axelrod.random_ import random_flip

C, D = Action.C, Action.D


# Strategy classifiers


def is_basic(s):
"""
Defines criteria for a strategy to be considered 'basic'
"""
stochastic = Classifiers().get("stochastic", s)
depth = Classifiers().get("depth", s)
inspects_source = Classifiers().get("inspects_source", s)
manipulates_source = Classifiers().get("manipulates_source", s)
manipulates_state = Classifiers().get("manipulates_state", s)
return (
not stochastic
and not inspects_source
and not manipulates_source
and not manipulates_state
and depth in (0, 1)
)


def obey_axelrod(s):
"""
A function to check if a strategy obeys Axelrod's original tournament
rules.
"""
for c in ["inspects_source", "manipulates_source", "manipulates_state"]:
if Classifiers().get(c, s):
return False
return True


def simultaneous_play(player, coplayer, noise=0):
"""This pits two players against each other."""
s1, s2 = player.strategy(coplayer), coplayer.strategy(player)
Expand Down
7 changes: 3 additions & 4 deletions axelrod/strategies/__init__.py
@@ -1,5 +1,4 @@
from ..classifier import Classifiers
from ..player import is_basic, obey_axelrod
from ..classifier import Classifiers, is_basic, obey_axelrod
from ._strategies import *
from ._filters import passes_filterset

Expand Down Expand Up @@ -85,10 +84,10 @@
strategies = [s for s in all_strategies if obey_axelrod(s())]

long_run_time_strategies = [
s for s in all_strategies if Classifiers().get("long_run_time", s)
s for s in all_strategies if Classifiers().get("long_run_time", s())
]
short_run_time_strategies = [
s for s in strategies if not Classifiers().get("long_run_time", s)
s for s in strategies if not Classifiers().get("long_run_time", s())
]
cheating_strategies = [s for s in all_strategies if not obey_axelrod(s())]

Expand Down
4 changes: 2 additions & 2 deletions axelrod/strategies/meta.py
Expand Up @@ -4,8 +4,8 @@
from numpy.random import choice

from axelrod.action import Action
from axelrod.classifier import Classifiers
from axelrod.player import Player, obey_axelrod
from axelrod.classifier import Classifiers, obey_axelrod
from axelrod.player import Player
from axelrod.strategies import TitForTat
from axelrod.strategy_transformers import NiceTransformer
from ._strategies import all_strategies
Expand Down
33 changes: 30 additions & 3 deletions axelrod/tests/unit/test_classification.py
@@ -1,12 +1,39 @@
"""Tests for the classification."""

import os
import unittest
from typing import Text

import yaml

import axelrod as axl
from axelrod.classifier import Classifiers
from axelrod.classifier import Classifier, Classifiers, rebuild_classifier_table


class TestClassification(unittest.TestCase):
def test_classifier_build(self):
test_path = "../test_outputs/classifier_test.yaml"

# Just returns the name of the player. For testing.
name_classifier = Classifier[Text]("name", lambda player: player.name)
rebuild_classifier_table(classifiers=[name_classifier],
players=[axl.Cooperator, axl.Defector],
path=test_path)

filename = os.path.join("../..", test_path)
with open(filename, 'r') as f:
all_player_dicts = yaml.load(f, Loader=yaml.FullLoader)

self.assertDictEqual(all_player_dicts,
{"Cooperator": {"name": "Cooperator"},
"Defector": {"name": "Defector"}})

def test_singletonity_of_classifiers_class(self):
classifiers_1 = Classifiers()
classifiers_2 = Classifiers()

self.assertIs(classifiers_1, classifiers_2)

def test_known_classifiers(self):
# A set of dimensions that are known to have been fully applied
known_keys = [
Expand Down Expand Up @@ -223,7 +250,7 @@ def test_long_run_strategies(self):
str_reps(axl.long_run_time_strategies)
)
self.assertTrue(
all(Classifiers().get("long_run_time", s) for s in
all(Classifiers().get("long_run_time", s()) for s in
axl.long_run_time_strategies)
)

Expand All @@ -237,7 +264,7 @@ def test_short_run_strategies(self):
str_reps(axl.short_run_time_strategies)
)
self.assertFalse(
any(Classifiers().get("long_run_time", s) for s in
any(Classifiers().get("long_run_time", s()) for s in
axl.short_run_time_strategies)
)

Expand Down
6 changes: 2 additions & 4 deletions rebuild_classifier_table.py
@@ -1,10 +1,8 @@
import os

from axelrod import all_strategies
from axelrod.classifier import ALL_CLASSIFIERS_PATH, all_classifiers, \
rebuild_classifier_table
from axelrod.classifier import all_classifiers, rebuild_classifier_table

if __name__ == "__main__":
# Change to relative path inside axelrod folder
axelrod_path = os.path.join("axelrod", ALL_CLASSIFIERS_PATH)
rebuild_classifier_table(all_classifiers, all_strategies, path=axelrod_path)
rebuild_classifier_table(all_classifiers, all_strategies)

0 comments on commit acf2672

Please sign in to comment.