diff --git a/axelrod/__init__.py b/axelrod/__init__.py index 7cb2ebda6..43bfa6729 100644 --- a/axelrod/__init__.py +++ b/axelrod/__init__.py @@ -12,6 +12,6 @@ from .strategies import * from .deterministic_cache import DeterministicCache from .match_generator import * -from .tournament import Tournament, ProbEndTournament +from .tournament import Tournament, ProbEndTournament, SpatialTournament from .result_set import ResultSet, ResultSetFromFile from .ecosystem import Ecosystem diff --git a/axelrod/match_generator.py b/axelrod/match_generator.py index 75611fac0..11c1061f0 100644 --- a/axelrod/match_generator.py +++ b/axelrod/match_generator.py @@ -167,3 +167,43 @@ def estimated_size(self): """Rough estimate of the number of matches that will be generated.""" size = self.__len__() * (1. / self.prob_end) * self.repetitions return size + +class SpatialMatches(RoundRobinMatches): + """ + A class that generates spatially-structured matches. + In these matches, players interact only with their neighbors rather than the + entire population. This reduces to a well-mixed population when the spatial + graph is a complete graph. + + Parameters + ---------- + players : list + A list of axelrod.Player objects + turns : integer + The number of turns per match + game : axelrod.Game + The game object used to score the match + repetitions : int + The number of repetitions of a given match + edges : list + A list of tuples containing the existing edges + """ + + def __init__(self, players, turns, game, repetitions, edges): + + player_indices = list(range(len(players))) + node_indices = sorted(set([node for edge in edges for node in edge])) + if player_indices != node_indices: + raise ValueError("The graph edges do not include all players.") + + self.edges = edges + super(SpatialMatches, self).__init__(players, turns, game, repetitions) + + def build_match_chunks(self): + for edge in self.edges: + match_params = self.build_single_match_params() + index_pair = edge + yield (index_pair, match_params, self.repetitions) + + def __len__(self): + return len(self.edges) diff --git a/axelrod/result_set.py b/axelrod/result_set.py index cac7bc2b4..ad250897e 100644 --- a/axelrod/result_set.py +++ b/axelrod/result_set.py @@ -309,7 +309,7 @@ def build_payoffs(self): if (player, opponent) == index_pair: for interaction in repetitions: utilities.append(iu.compute_final_score_per_turn(interaction)[0]) - if (opponent, player) == index_pair: + elif (opponent, player) == index_pair: for interaction in repetitions: utilities.append(iu.compute_final_score_per_turn(interaction)[1]) @@ -460,7 +460,7 @@ def build_payoff_diffs_means(self): for interaction in repetitions: scores = iu.compute_final_score_per_turn(interaction) diffs.append(scores[0] - scores[1]) - if (opponent, player) == index_pair: + elif (opponent, player) == index_pair: for interaction in repetitions: scores = iu.compute_final_score_per_turn(interaction) diffs.append(scores[1] - scores[0]) @@ -501,7 +501,7 @@ def build_cooperation(self): if (player, opponent) == index_pair: for interaction in repetitions: coop_count += iu.compute_cooperations(interaction)[0] - if (opponent, player) == index_pair: + elif (opponent, player) == index_pair: for interaction in repetitions: coop_count += iu.compute_cooperations(interaction)[1] @@ -625,7 +625,7 @@ def build_good_partner_matrix(self): if coops[0] >= coops[1]: good_partner_matrix[player][opponent] += 1 - if (opponent, player) == index_pair: + elif (opponent, player) == index_pair: for interaction in repetitions: coops = iu.compute_cooperations(interaction) if coops[0] <= coops[1]: diff --git a/axelrod/tests/unit/test_match_generator.py b/axelrod/tests/unit/test_match_generator.py index 4f49666ca..739db5e4d 100644 --- a/axelrod/tests/unit/test_match_generator.py +++ b/axelrod/tests/unit/test_match_generator.py @@ -207,3 +207,32 @@ def test_len(self, prob_end): self.players, prob_end, game=None, repetitions=repetitions) self.assertEqual(len(rr), len(list(rr.build_match_chunks()))) self.assertAlmostEqual(rr.estimated_size(), len(rr) * 1. / prob_end * repetitions) + + +class TestSpatialMatches(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.players = [s() for s in test_strategies] + + @given(repetitions=integers(min_value=1, max_value=test_repetitions), + turns=integers(min_value=1, max_value=test_turns)) + @example(repetitions=test_repetitions, turns=test_turns) + def test_build_match_chunks(self, repetitions, turns): + edges = [(0, 1), (1, 2), (3, 4)] + sp = axelrod.SpatialMatches( + self.players, turns, test_game, repetitions, edges) + chunks = list(sp.build_match_chunks()) + match_definitions = [tuple(list(index_pair) + [repetitions]) + for (index_pair, match_params, repetitions) in chunks] + expected_match_definitions = [(edge[0], edge[1], repetitions) + for edge in edges] + + self.assertEqual(sorted(match_definitions), sorted(expected_match_definitions)) + + def test_len(self): + edges = [(0, 1), (1, 2), (3, 4)] + sp = axelrod.SpatialMatches( + self.players, test_turns, test_game, test_repetitions, edges) + self.assertEqual(len(sp), len(list(sp.build_match_chunks()))) + self.assertEqual(len(sp), len(edges)) diff --git a/axelrod/tests/unit/test_property.py b/axelrod/tests/unit/test_property.py index 1b1af4ddc..4497efc13 100644 --- a/axelrod/tests/unit/test_property.py +++ b/axelrod/tests/unit/test_property.py @@ -159,6 +159,7 @@ def test_decorator_with_stochastic_strategies(self, tournament): for p in tournament.players: self.assertIn(str(p), stochastic_player_names) + class TestGame(unittest.TestCase): def test_call(self): diff --git a/axelrod/tests/unit/test_resultset.py b/axelrod/tests/unit/test_resultset.py index 4adbf513c..125e0a00c 100644 --- a/axelrod/tests/unit/test_resultset.py +++ b/axelrod/tests/unit/test_resultset.py @@ -38,8 +38,6 @@ def setUpClass(cls): 1: cls.matches[(0, 1)] + cls.matches[(1, 2)], 2: cls.matches[(1, 2)] + cls.matches[(0, 2)]} - - cls.expected_match_lengths =[ [[0, 5, 5], [5, 0, 5], [5, 5, 0]] for _ in range(3) @@ -144,7 +142,6 @@ def setUpClass(cls): 0 ] - cls.expected_eigenjesus_rating = [ 0.5547001962252291, 0.8320502943378436, @@ -164,7 +161,8 @@ def test_init(self): self.assertEqual(rs.players, self.players) self.assertEqual(rs.nplayers, len(self.players)) self.assertEqual(rs.interactions, self.interactions) - self.assertEqual(rs.nrepetitions, len(self.interactions)) + for inter in self.interactions.values(): + self.assertEqual(rs.nrepetitions, len(inter)) # Test structure of matches # This is really a test of the test @@ -394,3 +392,499 @@ class TestDecorator(unittest.TestCase): def test_update_progress_bar(self): method = lambda x: None self.assertEqual(axelrod.result_set.update_progress_bar(method)(1), None) + + +class TestResultSetSpatialStructure(TestResultSet): + """ + Specific test for some spatial tournament. + """ + @classmethod + def setUpClass(cls): + + cls.players = (axelrod.Alternator(), axelrod.TitForTat(), axelrod.Defector()) + cls.turns = 5 + cls.edges = [(0, 1), (0, 2)] + cls.matches = { (0,1): [axelrod.Match((cls.players[0], cls.players[1]), + turns=cls.turns) for _ in range(3)], + (0,2): [axelrod.Match((cls.players[0], cls.players[2]), + turns=cls.turns) for _ in range(3)]} + + cls.interactions = {} + for index_pair, matches in cls.matches.items(): + for match in matches: + match.play() + try: + cls.interactions[index_pair].append(match.result) + except KeyError: + cls.interactions[index_pair] = [match.result] + + cls.expected_players_to_match_dicts = {0: cls.matches[(0, 1)] + cls.matches[(0, 2)], + 1: cls.matches[(0, 1)] , + 2: cls.matches[(0, 2)]} + + cls.expected_match_lengths =[ + [[0, 5, 5], [5, 0, 0], [5, 0, 0]] + for _ in range(3) + ] + + cls.expected_scores =[ + [15, 15, 15], + [13, 13, 13], + [17, 17, 17] + ] + + cls.expected_wins =[ + [0, 0, 0], + [0, 0, 0], + [1, 1, 1] + ] + + cls.expected_normalised_scores =[ + [3.0 / 2 for _ in range(3)], + [(13.0 / 5 ) for _ in range(3)], + [(17.0 / 5 ) for _ in range(3)], + ] + + cls.expected_ranking = [2, 1, 0] + + cls.expected_ranked_names = ['Defector', 'Tit For Tat', 'Alternator'] + + cls.expected_null_results_matrix = [ + [[0, 0, 0], [0, 0, 0], [0, 0, 0]], + [[0, 0, 0], [0, 0, 0], [0, 0, 0]], + [[0, 0, 0], [0, 0, 0], [0, 0, 0]], + ] + + cls.expected_payoffs = [ + [[], [13/5.0 for _ in range(3)], [2/5.0 for _ in range(3)]], + [[13/5.0 for _ in range(3)], [], []], + [[17/5.0 for _ in range(3)], [], []] + ] + + norm_scores = cls.expected_normalised_scores + cls.expected_score_diffs = [ + [[0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [-3.0, -3.0, -3.0]], + [[0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0]], + [[3.0, 3.0, 3.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0]], + ] + + cls.expected_payoff_diffs_means = [ + [0.0, 0.0, -3.0], + [0.0, 0.0, 0.0], + [3.0, 0.0, 0.0] + ] + + # Recalculating to deal with numeric imprecision + cls.expected_payoff_matrix = [ + [0, mean([13/5.0 for _ in range(3)]), mean([2/5.0 for _ in range(3)])], + [mean([13/5.0 for _ in range(3)]), 0, 0 ], + [mean([17/5.0 for _ in range(3)]), 0 , 0] + ] + + cls.expected_payoff_stddevs = [ + [0, std([13/5.0 for _ in range(3)]), std([2/5.0 for _ in range(3)])], + [std([13/5.0 for _ in range(3)]), 0, 0 ], + [std([17/5.0 for _ in range(3)]), 0, 0 ] + ] + + cls.expected_cooperation = [ + [0, 9, 9], + [9, 0, 0], + [0, 0, 0], + ] + + cls.expected_normalised_cooperation = [ + [0, mean([3 / 5.0 for _ in range(3)]), mean([3 / 5.0 for _ in range(3)])], + [mean([3 / 5.0 for _ in range(3)]), 0, 0 ], + [0, 0, 0], + ] + + cls.expected_vengeful_cooperation = [[2 * element - 1 for element in row] + for row in cls.expected_normalised_cooperation] + + cls.expected_cooperating_rating = [ + 18.0 / 30, + 9.0 / 15, + 0 + ] + + cls.expected_good_partner_matrix = [ + [0, 3, 3], + [3, 0, 0], + [0, 0, 0] + ] + + cls.expected_good_partner_rating = [ + 1.0, + 1.0, + 0.0 + ] + + cls.expected_eigenjesus_rating = [ + 0.447213595499958, + 0.894427190999916, + 0.0 + ] + + cls.expected_eigenmoses_rating = [ + -0.32929277996907086, + 0.7683498199278325, + 0.5488212999484519 + ] + + cls.expected_csv = ( + 'Defector,Tit For Tat,Alternator\n3.4,2.6,1.5\n3.4,2.6,1.5\n3.4,2.6,1.5\n') + + def test_match_lengths(self): + """ + Overwriting match lengths test. This method, among other things, checks + that if two players interacted the length of that interaction equals the + number of turns. + + Implementing this for the round robin tournament meant checking the + interactions between each strategy and the rest strategies of the + tournament. + + In a spatial tournament we need to check that: The length of interaction + of players-nodes that are end vertices of an edge is equal to the + number of turns. Otherwise it is 0. + """ + rs = axelrod.ResultSet(self.players, self.interactions, + progress_bar=False) + self.assertIsInstance(rs.match_lengths, list) + self.assertEqual(len(rs.match_lengths), rs.nrepetitions) + self.assertEqual(rs.match_lengths, self.expected_match_lengths) + + for rep in rs.match_lengths: + self.assertIsInstance(rep, list) + self.assertEqual(len(rep), len(self.players)) + + for i, opp in enumerate(rep): + self.assertIsInstance(opp, list) + self.assertEqual(len(opp), len(self.players)) + + for j, length in enumerate(opp): + edge = (i, j) + # Specific test for example match setup + if edge in self.edges or edge[::-1] in self.edges : + self.assertEqual(length, self.turns) + else: + self.assertEqual(length, 0) + + +class TestResultSetSpatialStructureTwo(TestResultSetSpatialStructure): + + @classmethod + def setUpClass(cls): + + cls.players = (axelrod.Alternator(), axelrod.TitForTat(), + axelrod.Defector(), axelrod.Cooperator()) + cls.turns = 5 + cls.edges = [(0, 1), (2, 3)] + cls.matches = { (0,1): [axelrod.Match((cls.players[0], cls.players[1]), + turns=cls.turns) for _ in range(3)], + (2,3): [axelrod.Match((cls.players[2], cls.players[3]), + turns=cls.turns) for _ in range(3)]} + + cls.interactions = {} + for index_pair, matches in cls.matches.items(): + for match in matches: + match.play() + try: + cls.interactions[index_pair].append(match.result) + except KeyError: + cls.interactions[index_pair] = [match.result] + + cls.expected_players_to_match_dicts = {0: cls.matches[(0, 1)] , + 1: cls.matches[(0, 1)] , + 2: cls.matches[(2, 3)], + 3: cls.matches[(2, 3)]} + + cls.expected_match_lengths =[ + [[0, 5, 0, 0], [5, 0, 0, 0], [0, 0, 0, 5], [0, 0, 5, 0]] + for _ in range(3) + ] + + cls.expected_scores =[ + [ 13.0 for _ in range(3)], + [ 13.0 for _ in range(3)], + [ 25.0 for _ in range(3)], + [ 0 for _ in range(3)] + ] + + cls.expected_wins =[ + [0, 0, 0], + [0, 0, 0], + [1, 1, 1], + [0, 0, 0] + ] + + cls.expected_normalised_scores =[ + [(13.0 / 5 ) for _ in range(3)], + [(13.0 / 5 ) for _ in range(3)], + [(25.0 / 5 ) for _ in range(3)], + [0 for _ in range(3)] + ] + + cls.expected_ranking = [2, 0, 1, 3] + + cls.expected_ranked_names = ['Defector','Alternator', + 'Tit For Tat','Cooperator'] + + cls.expected_null_results_matrix = [ + [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], + [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], + [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], + ] + + cls.expected_payoffs = [ + [[], [13/5.0 for _ in range(3)], [], []], + [[13/5.0 for _ in range(3)], [], [], []], + [[], [], [], [25/5.0 for _ in range(3)]], + [[], [], [0 for _ in range(3)], []] + ] + + norm_scores = cls.expected_normalised_scores + cls.expected_score_diffs = [ + [[0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0]], + [[0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0]], + [[0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [5.0, 5.0, 5.0]], + [[0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [-5.0, -5.0, -5.0], + [0.0, 0.0, 0.0]] + ] + + cls.expected_payoff_diffs_means = [ + [0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 5.0], + [0.0, 0.0, -5.0, 0.0] + ] + + # Recalculating to deal with numeric imprecision + cls.expected_payoff_matrix = [ + [0, mean([13/5.0 for _ in range(3)]), 0, 0], + [mean([13/5.0 for _ in range(3)]), 0, 0, 0], + [0, 0, 0, mean([25/5.0 for _ in range(3)])], + [0, 0, 0, 0] + ] + + cls.expected_payoff_stddevs = [ + [0, std([13/5.0 for _ in range(3)]), 0, 0], + [std([13/5.0 for _ in range(3)]), 0, 0, 0], + [0, 0, 0, std([25/5.0 for _ in range(3)])], + [0, 0, 0, 0] + ] + + cls.expected_cooperation = [ + [0, 9, 0, 0], + [9, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 15, 0] + ] + + cls.expected_normalised_cooperation = [ + [0.0, mean([3 / 5.0 for _ in range(3)]), 0.0, 0.0], + [mean([3 / 5.0 for _ in range(3)]), 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, mean([5 / 5.0 for _ in range(3)]), 0.0] + ] + + cls.expected_vengeful_cooperation = [[2 * element - 1 for element in row] + for row in cls.expected_normalised_cooperation] + + cls.expected_cooperating_rating = [ + 18.0 / 30, + 18.0 / 30, + 0.0, + 30 / 30 + ] + + cls.expected_good_partner_matrix = [ + [0, 3, 0, 0], + [3, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 3, 0] + ] + + cls.expected_good_partner_rating = [ + 1.0, + 1.0, + 0.0, + 1.0 + ] + + cls.expected_eigenjesus_rating = [ + 0.7071067811865476, + 0.7071067811865476, + 0.0, + 0.0, + ] + + cls.expected_eigenmoses_rating = [ + 0.48505781033492573, + 0.48505781033492573, + 0.7090603855860735, + 0.1633132292825755 + ] + + cls.expected_csv = ( + "Defector,Alternator,Tit For Tat,Cooperator\n5.0,2.6,2.6,0.0\n5.0,2.6,2.6,0.0\n5.0,2.6,2.6,0.0\n") + + +class TestResultSetSpatialStructureThree(TestResultSetSpatialStructure): + + @classmethod + def setUpClass(cls): + + cls.players = (axelrod.Alternator(), axelrod.TitForTat(), + axelrod.Defector(), axelrod.Cooperator()) + cls.turns = 5 + cls.edges = [(0, 0), (1, 1), (2, 2), (3, 3)] + cls.matches = {(i, i): [axelrod.Match((cls.players[i], + cls.players[i].clone()), + turns=cls.turns) + for _ in range(3)] for i in range(4)} + + cls.interactions = {} + for index_pair, matches in cls.matches.items(): + for match in matches: + match.play() + + try: + cls.interactions[index_pair].append(match.result) + except KeyError: + cls.interactions[index_pair] = [match.result] + + cls.expected_players_to_match_dicts = {0: cls.matches[(0, 0)], + 1: cls.matches[(1, 1)], + 2: cls.matches[(2, 2)], + 3: cls.matches[(3, 3)]} + + cls.expected_match_lengths =[ + [[5, 0, 0, 0], [0, 5, 0, 0], [0, 0, 5, 0], [0, 0, 0, 5]] + for _ in range(3) + ] + + cls.expected_scores =[ + [0 for _ in range(3)] for _ in range(4) + ] + + cls.expected_wins =[ + [0 for _ in range(3)] for _ in range(4) + ] + + cls.expected_normalised_scores =[ + ["nan" for _ in range(3)] for i in range(4) + ] + + cls.expected_ranking = [0, 1, 2, 3] + + cls.expected_ranked_names = ['Alternator','Tit For Tat','Defector','Cooperator'] + + cls.expected_null_results_matrix = [ + [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], + [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], + [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], + ] + + cls.expected_payoffs = [ + [[11 /5.0 for _ in range(3)], [], [], []], + [[], [15 /5.0 for _ in range(3)], [], []], + [[], [], [5 /5.0 for _ in range(3)], []], + [[], [], [], [15 /5.0 for _ in range(3)]] + ] + + norm_scores = cls.expected_normalised_scores + cls.expected_score_diffs = [ + [[0.0 for _ in range(3)] for _ in range(4) ] for _ in range(4) + ] + + cls.expected_payoff_diffs_means = [ + [0.0 for _ in range(4)] for _ in range(4) + ] + + # Recalculating to deal with numeric imprecision + cls.expected_payoff_matrix = [ + [mean([11/5.0 for _ in range(3)]),0, 0, 0], + [0, mean([15/5.0 for _ in range(3)]), 0, 0], + [0, 0, mean([5/5.0 for _ in range(3)]), 0], + [0, 0, 0, mean([15/5.0 for _ in range(3)])] + ] + + cls.expected_payoff_stddevs = [ + [std([11/5.0 for _ in range(3)]),0, 0, 0], + [0, std([15/5.0 for _ in range(3)]), 0, 0], + [0, 0, std([5/5.0 for _ in range(3)]), 0], + [0, 0, 0, std([15/5.0 for _ in range(3)])] + ] + + cls.expected_cooperation = [ + [0.0 for _ in range(4)] for _ in range(4) + ] + + cls.expected_normalised_cooperation = [ + [mean([3 / 5.0 for _ in range(3)]), 0.0, 0.0, 0.0], + [0.0, mean([5 / 5.0 for _ in range(3)]), 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, mean([5 / 5.0 for _ in range(3)])] + ] + + cls.expected_vengeful_cooperation = [[2 * element - 1 for element in row] + for row in cls.expected_normalised_cooperation] + + cls.expected_cooperating_rating = [ + 0.0 for _ in range(4) + ] + + cls.expected_good_partner_matrix = [ + [0.0 for _ in range(4)] for _ in range(4) + ] + + cls.expected_good_partner_rating = [ + 0.0 for _ in range(4) + ] + + cls.expected_eigenjesus_rating = [ + 0.0009235301367282831, + 0.7071064796379986, + 0.0, + 0.7071064796379986, + ] + + cls.expected_eigenmoses_rating = [ + 0.4765940316018446, + 0.3985944056208427, + 0.6746133178770147, + 0.3985944056208427 + ] + + cls.expected_csv = ( + 'Alternator,Tit For Tat,Defector,Cooperator\nnan,nan,nan,nan\nnan,nan,nan,nan\nnan,nan,nan,nan\n') + + def test_normalised_scores(self): + """ + Need to test string representation because of nan + """ + rs = axelrod.ResultSet(self.players, self.interactions, + progress_bar=False) + self.assertIsInstance(rs.normalised_scores, list) + self.assertEqual(len(rs.normalised_scores), rs.nplayers) + self.assertEqual([[str(s) for s in player] for player in rs.normalised_scores] + , self.expected_normalised_scores) diff --git a/axelrod/tests/unit/test_tournament.py b/axelrod/tests/unit/test_tournament.py index 3d433cd7f..62fb4eb6e 100644 --- a/axelrod/tests/unit/test_tournament.py +++ b/axelrod/tests/unit/test_tournament.py @@ -9,7 +9,9 @@ from hypothesis import given, example, settings from hypothesis.strategies import integers -from axelrod.tests.property import tournaments, prob_end_tournaments +from axelrod.tests.property import (tournaments, + prob_end_tournaments, + strategy_lists) import axelrod @@ -31,6 +33,10 @@ test_prob_end = .5 +test_edges = [(0, 1), (1, 2), (3, 4)] + +deterministic_strategies = [s for s in axelrod.ordinary_strategies + if not s().classifier['stochastic']] class TestTournament(unittest.TestCase): @@ -548,3 +554,71 @@ def test_property_serial_play(self, tournament): self.assertEqual(results.players, [str(p) for p in tournament.players]) for rep in results.interactions.values(): self.assertEqual(len(rep), tournament.repetitions) + +class TestSpatialTournament(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.game = axelrod.Game() + cls.players = [s() for s in test_strategies] + cls.test_name = 'test' + cls.test_repetitions = test_repetitions + cls.test_turns = test_turns + cls.test_edges = test_edges + + def test_init(self): + tournament = axelrod.SpatialTournament( + name=self.test_name, + players=self.players, + game=self.game, + turns=self.test_turns, + edges=self.test_edges, + noise=0.2) + self.assertEqual(tournament.match_generator.edges, tournament.edges) + self.assertEqual(len(tournament.players), len(test_strategies)) + self.assertEqual(tournament.game.score(('C', 'C')), (3, 3)) + self.assertEqual(tournament.turns, 100) + self.assertEqual(tournament.repetitions, 10) + self.assertEqual(tournament.name, 'test') + self.assertTrue(tournament._with_morality) + self.assertIsInstance(tournament._logger, logging.Logger) + self.assertEqual(tournament.noise, 0.2) + anonymous_tournament = axelrod.Tournament(players=self.players) + self.assertEqual(anonymous_tournament.name, 'axelrod') + + @given(strategies=strategy_lists(strategies=deterministic_strategies, + min_size=2, max_size=2), + turns=integers(min_value=1, max_value=20)) + + def test_complete_tournament(self, strategies, turns): + """ + A test to check that a spatial tournament on the complete multigraph + gives the same results as the round robin. + """ + + players = [s() for s in strategies] + # edges + edges=[] + for i in range(0, len(players)) : + for j in range(i, len(players)) : + edges.append((i, j)) + # create a round robin tournament + tournament = axelrod.Tournament(players, turns=turns) + results = tournament.play() + # create a complete spatial tournament + spatial_tournament = axelrod.SpatialTournament(players, turns=turns, + edges=edges) + spatial_results = spatial_tournament.play() + self.assertEqual(results.ranked_names, spatial_results.ranked_names) + self.assertEqual(results.nplayers, spatial_results.nplayers) + self.assertEqual(results.nrepetitions, spatial_results.nrepetitions) + self.assertEqual(results.payoff_diffs_means, spatial_results.payoff_diffs_means) + self.assertEqual(results.payoff_matrix, spatial_results.payoff_matrix) + self.assertEqual(results.payoff_stddevs, spatial_results.payoff_stddevs) + self.assertEqual(results.payoffs, spatial_results.payoffs) + self.assertEqual(results.cooperating_rating, spatial_results.cooperating_rating) + self.assertEqual(results.cooperation, spatial_results.cooperation) + self.assertEqual(results.normalised_cooperation, spatial_results.normalised_cooperation) + self.assertEqual(results.normalised_scores, spatial_results.normalised_scores) + self.assertEqual(results.good_partner_matrix, spatial_results.good_partner_matrix) + self.assertEqual(results.good_partner_rating, spatial_results.good_partner_rating) diff --git a/axelrod/tournament.py b/axelrod/tournament.py index 15716adbf..0c7afbd59 100644 --- a/axelrod/tournament.py +++ b/axelrod/tournament.py @@ -11,7 +11,7 @@ from .game import Game from .match import Match -from .match_generator import RoundRobinMatches, ProbEndRoundRobinMatches +from .match_generator import RoundRobinMatches, ProbEndRoundRobinMatches, SpatialMatches from .result_set import ResultSetFromFile @@ -335,3 +335,43 @@ def __init__(self, players, match_generator=ProbEndRoundRobinMatches, self.prob_end = prob_end self.match_generator = ProbEndRoundRobinMatches( players, prob_end, self.game, repetitions) + + +class SpatialTournament(Tournament): + """ + A tournament in which the players are allocated in a graph as nodes + and they players only play the others that are connected to with an edge. + """ + def __init__(self, players, edges, match_generator=SpatialMatches, + name='axelrod', game=None, turns=200, repetitions=10, + noise=0, + with_morality=True): + """ + Parameters + ---------- + players : list + A list of axelrod.Player objects + match_generator : class + A class that must be descended from axelrod.MatchGenerator + name : string + A name for the tournament + game : axelrod.Game + The game object used to score the tournament + edges : list + A list of tuples containing the existing edges + repetitions : integer + The number of times the round robin should be repeated + processes : integer + The number of processes to be used for parallel processing + noise : float + The probability that a player's intended action should be flipped + with_morality : boolean + Whether morality metrics should be calculated + """ + super(SpatialTournament, self).__init__( + players, name=name, game=game, turns=turns, + repetitions=repetitions, noise=noise, with_morality=with_morality) + + self.edges = edges + self.match_generator = SpatialMatches( + players, turns, self.game, repetitions, edges) diff --git a/docs/tutorials/further_topics/_static/spatial_tournaments/spatial.png b/docs/tutorials/further_topics/_static/spatial_tournaments/spatial.png new file mode 100644 index 000000000..1d6985e75 Binary files /dev/null and b/docs/tutorials/further_topics/_static/spatial_tournaments/spatial.png differ diff --git a/docs/tutorials/further_topics/_static/spatial_tournaments/spatial_results.png b/docs/tutorials/further_topics/_static/spatial_tournaments/spatial_results.png new file mode 100644 index 000000000..ff64c7444 Binary files /dev/null and b/docs/tutorials/further_topics/_static/spatial_tournaments/spatial_results.png differ diff --git a/docs/tutorials/further_topics/index.rst b/docs/tutorials/further_topics/index.rst index 2eb9e817f..e404fe4ed 100644 --- a/docs/tutorials/further_topics/index.rst +++ b/docs/tutorials/further_topics/index.rst @@ -11,5 +11,6 @@ Contents: noisy_tournaments.rst probabilistict_end_tournaments.rst + spatial_tournaments.rst morality_metrics.rst ecological_variant.rst diff --git a/docs/tutorials/further_topics/spatial_tournaments.rst b/docs/tutorials/further_topics/spatial_tournaments.rst new file mode 100644 index 000000000..bc35c3352 --- /dev/null +++ b/docs/tutorials/further_topics/spatial_tournaments.rst @@ -0,0 +1,71 @@ +Spatial tournaments +=================== + +In a spatial topology tournament the connectivity between players is given by +a graph where the nodes represent players, and the edges, connecting the nodes, +refer to the connection between the corresponding players. + +The initial work was done by Nowak and May in a 1992 paper, "Evolutionary games +and spatial chaos", introducing spatial topology as a square lattice. (The +paper can be found here: http://www.nature.com/nature/journal/v359/n6398/abs/359826a0.html). + +Additionally, Szabó and Fáth in their 2007 paper consider a variety of graphs, +such as lattices, small world, scale-free graphs and evolving networks. (Their +paper can be found here: https://arxiv.org/abs/cond-mat/0607344). + +Even so, here it is possible to create a tournament where the players are +allocated to any given graph and they only interact with players to which they +have a connection - edge. + +Let's create a tournament where :code:`Cooperator` and :code:`Defector` do not +play each other and neither do :code:`TitForTat` and :code:`Grudger` : + +.. image:: _static/spatial_tournaments/spatial.png + :width: 80% + :align: center + +Note that the edges have to be given as a list of tuples of player +indices:: + + >>> import axelrod as axl + >>> players = [axl.Cooperator(), axl.Defector(), + ... axl.TitForTat(), axl.Grudger()] + >>> edges = [(0, 2), (0, 3), (1, 2), (1, 3)] + +To create a spatial tournament you call the :code:`SpatialTournamnent` class:: + + >>> spatial_tournament = axl.SpatialTournament(players, edges=edges) + >>> results = spatial_tournament.play() + +We can plot the results:: + + >>> plot = axl.Plot(results) + >>> p = plot.boxplot() + >>> p.show() + + +.. image:: _static/spatial_tournaments/spatial_results.png + :width: 50% + :align: center + +We can, like any other tournament, obtain the ranks for our players:: + + >>> results.ranked_names + ['Cooperator', 'Tit For Tat', 'Grudger', 'Defector'] + +Let's run small tournament of 2 :code:`turns` and 5 :code:`repetitions` +and obtain the interactions:: + + >>> spatial_tournament = axl.SpatialTournament(players ,turns=2, repetitions=2, edges=edges) + >>> results = spatial_tournament.play() + >>> for index_pair, interaction in results.interactions.items(): + ... player1 = spatial_tournament.players[index_pair[0]] + ... player2 = spatial_tournament.players[index_pair[1]] + ... print('%s vs %s: %s' % (player1, player2, interaction)) + Defector vs Tit For Tat: [[('D', 'C'), ('D', 'D')], [('D', 'C'), ('D', 'D')]] + Cooperator vs Grudger: [[('C', 'C'), ('C', 'C')], [('C', 'C'), ('C', 'C')]] + Defector vs Grudger: [[('D', 'C'), ('D', 'D')], [('D', 'C'), ('D', 'D')]] + Cooperator vs Tit For Tat: [[('C', 'C'), ('C', 'C')], [('C', 'C'), ('C', 'C')]] + +As anticipated :code:`Cooperator` does not interact with :code:`Defector` neither +:code:`TitForTat` with :code:`Grudger`.