diff --git a/.travis.yml b/.travis.yml index a125bb9..74ba544 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: python python: - - 3.5 - 3.6 + - 3.7 dist: xenial sudo: required services: @@ -19,5 +19,6 @@ before_install: install: - pip install -r requirements.txt - pip install pytest + - python setup.py install script: - - pytest + - pytest tests/ diff --git a/requirements.txt b/requirements.txt index 11803af..661761b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -axelrod>=4.8.0 +axelrod>=4.10.0 diff --git a/src/axelrod_fortran/player.py b/src/axelrod_fortran/player.py index 19198a7..e6c4afb 100644 --- a/src/axelrod_fortran/player.py +++ b/src/axelrod_fortran/player.py @@ -1,5 +1,4 @@ from ctypes import byref, c_float, c_int, POINTER -import random import warnings import axelrod as axl @@ -42,16 +41,19 @@ def __init__(self, original_name): A instance of an axelrod Game """ super().__init__() + # The order of the next 4 lines is important. We must first check that + # the player name is valid, then grab a copy of the shared library, + # and then setup the actual strategy function. + self.original_name = original_name self.index, self.shared_library_filename = \ - shared_library_manager.get_filename_for_player(original_name) + shared_library_manager.get_filename_for_player(self.original_name) self.shared_library = load_library(self.shared_library_filename) - self.original_name = original_name self.original_function = self.original_name + is_stochastic = characteristics[self.original_name]['stochastic'] if is_stochastic is not None: self.classifier['stochastic'] = is_stochastic - def __enter__(self): return self @@ -63,11 +65,11 @@ def original_name(self): return self.__original_name @original_name.setter - def original_name(self, value): - if value in characteristics: - self.__original_name = value + def original_name(self, key): + if key in characteristics: + self.__original_name = key else: - raise ValueError('{} is not a valid Fortran function'.format(value)) + raise ValueError('{} is not a valid Fortran function'.format(key)) @property def original_function(self): @@ -105,7 +107,7 @@ def strategy(self, opponent): my_last_move = original_actions[self.history[-1]] move_number = len(self.history) + 1 if self.classifier["stochastic"]: - random_value = random.random() + random_value = self._random.random() else: random_value = 0 original_action = self.original_strategy( @@ -119,7 +121,16 @@ def _release_shared_library(self): # thread closes before the player class is garbage collected, which # tends to happen at the end of a script. try: - shared_library_manager.release(self.original_name, self.index) + name = self.original_name + index = self.index + except AttributeError: + # If the Player does finish __init__, because the name of a + # non-existent strategy is supplied, a copy of the shared library + # won't be loaded, nor will self.original_name or self.index + # exist. In that case there's nothing to do. + return + try: + shared_library_manager.release(name, index) except FileNotFoundError: pass diff --git a/src/axelrod_fortran/shared_library_manager.py b/src/axelrod_fortran/shared_library_manager.py index 8fadf6b..6a004f1 100644 --- a/src/axelrod_fortran/shared_library_manager.py +++ b/src/axelrod_fortran/shared_library_manager.py @@ -59,7 +59,7 @@ def create_library_copy(self): # Copy the library file to a new (temp) location. temp_directory = tempfile.gettempdir() copy_number = len(self.filenames) - filename = "{}-{}-{}".format( + filename = "{}-{}-{}".format( self.prefix, str(copy_number), self.shared_library_name) diff --git a/src/axelrod_fortran/strategies.py b/src/axelrod_fortran/strategies.py index 8d5817d..c4e0264 100644 --- a/src/axelrod_fortran/strategies.py +++ b/src/axelrod_fortran/strategies.py @@ -348,7 +348,7 @@ 'original_rank': None}, } -all_strategies = characteristics.keys() +all_strategies = list(characteristics.keys()) # Players from Axelrod's second tournament. second_tournament_strategies = [ diff --git a/tests/test_player.py b/tests/test_player.py index 8425697..55f96f3 100644 --- a/tests/test_player.py +++ b/tests/test_player.py @@ -1,11 +1,11 @@ -from ctypes import c_int, c_float, POINTER, CDLL +from ctypes import c_int, c_float, POINTER import itertools import pytest from axelrod_fortran import Player, characteristics, all_strategies from axelrod import (Alternator, Cooperator, Defector, Match, MoranProcess, - Game, basic_strategies, seed) + Game, RandomGenerator, Tournament, basic_strategies) from axelrod.action import Action @@ -122,32 +122,33 @@ def test_implemented_strategies(): """ for strategy, dictionary in characteristics.items(): axelrod_class = dictionary["axelrod-python_class"] - player = Player(strategy) - if (axelrod_class is not None and - player.classifier["stochastic"] is False): - axl_player = axelrod_class() + stochastic = Player(strategy).classifier["stochastic"] + if axelrod_class is not None and not stochastic: for opponent_strategy in basic_strategies: + player = Player(strategy) opponent = opponent_strategy() match = Match((player, opponent)) interactions = match.play() + + axl_player = axelrod_class() + opponent = opponent_strategy() axl_match = Match((axl_player, opponent)) - assert interactions == axl_match.play(), (player, opponent) + axl_interactions = axl_match.play() + assert interactions == axl_interactions def test_champion_v_alternator(): """ - Specific regression test for a bug. + Specific regression test for a bug. See: + https://github.com/Axelrod-Python/axelrod-fortran/issues/62 """ player = Player("k61r") opponent = Alternator() - - match = Match((player, opponent)) - - seed(0) + seed = 3 + match = Match((player, opponent), seed=seed) interactions = match.play() assert interactions[25:30] == [(C, D), (C, C), (C, D), (D, C), (C, D)] - - seed(0) + match = Match((player, opponent), seed=seed) assert interactions == match.play() @@ -157,9 +158,7 @@ def test_warning_for_self_interaction(recwarn): """ player = Player("k42r") opponent = player - match = Match((player, opponent)) - interactions = match.play() assert len(recwarn) == 1 @@ -168,13 +167,10 @@ def test_no_warning_for_normal_interaction(recwarn): """ Test that a warning is not given for a normal interaction """ - player = Player("k42r") - opponent = Alternator() for players in [(Player("k42r"), Alternator()), (Player("k42r"), Player("k41r"))]: match = Match(players) - interactions = match.play() assert len(recwarn) == 0 @@ -185,3 +181,35 @@ def test_multiple_copies(recwarn): mp = MoranProcess(players) mp.play() mp.populations_plot() + + +def test_match_reproducibility(): + for _ in range(100): + rng = RandomGenerator() + seed = rng.random_seed_int() + strategies = rng.choice(all_strategies, size=2) + players1 = [Player(strategy) for strategy in strategies] + match1 = Match(players1, turns=200, noise=0.1, seed=seed) + results1 = match1.play() + + players2 = [Player(strategy) for strategy in strategies] + match2 = Match(players2, turns=200, noise=0.1, seed=seed) + results2 = match2.play() + + assert (results1 == results2) + + +def test_tournament_reproducibility(): + rng = RandomGenerator() + seed = rng.random_seed_int() + strategies = rng.choice(all_strategies, size=10) + players1 = [Player(strategy) for strategy in strategies] + tournament1 = Tournament(players1, seed=seed, repetitions=2) + results1 = tournament1.play(processes=2) + + players2 = [Player(strategy) for strategy in strategies] + tournament2 = Tournament(players2, seed=seed, repetitions=2) + results2 = tournament2.play(processes=2) + + assert (results1.ranked_names == results2.ranked_names) +