From cb61be5d81d6b224b92edc9be5b3e5b93af7c675 Mon Sep 17 00:00:00 2001 From: Marc Harper Date: Mon, 16 Jan 2017 08:50:37 -0800 Subject: [PATCH] Update docs, rework simulate play and some test internals, add warnings for mismatched histories --- axelrod/mock_player.py | 58 ++++++---- axelrod/strategies/appeaser.py | 1 + axelrod/tests/unit/test_alternator.py | 6 +- axelrod/tests/unit/test_cooperator.py | 2 +- axelrod/tests/unit/test_mock_player.py | 47 ++++---- axelrod/tests/unit/test_player.py | 21 ++-- .../writing_test_for_the_new_strategy.rst | 106 +++++++++++------- doctests.py | 3 + 8 files changed, 146 insertions(+), 98 deletions(-) diff --git a/axelrod/mock_player.py b/axelrod/mock_player.py index ed9e3e140..3cfba6e8c 100644 --- a/axelrod/mock_player.py +++ b/axelrod/mock_player.py @@ -1,55 +1,69 @@ from collections import defaultdict -import copy -from axelrod import (Actions, Player, get_state_distribution_from_history, - update_history, update_state_distribution) +import warnings +from axelrod import (Actions, Player, update_history, update_state_distribution) C, D = Actions.C, Actions.D class MockPlayer(Player): - """Creates a mock player that enforces a particular next move for a given - player.""" + """Creates a mock player that copies a history and state distribution to + simulate a history of play, and then plays a given sequence of actions. If + no actions are given, plays like Cooperator. + """ + + name = "Mock Player" - def __init__(self, player, move): + def __init__(self, actions=None, history=None, state_dist=None): # Need to retain history for opponents that examine opponents history # Do a deep copy just to be safe super().__init__() - self.history = copy.deepcopy(player.history) - self.cooperations = player.cooperations - self.defections = player.defections - self.move = move + if history: + # Make sure we both copy the history and get the right counts + # for cooperations and defections. + for action in history: + update_history(self, action) + if state_dist: + self.state_distribution = dict(state_dist) + if actions: + self.actions = list(actions) + else: + self.actions = [] def strategy(self, opponent): - # Just return the saved move - return self.move + # Return the next saved action, if present. + try: + action = self.actions.pop(0) + return action + except IndexError: + return C def simulate_play(P1, P2, h1=None, h2=None): """ Simulates play with or without forced history. If h1 and h2 are given, these - moves are enforced in the players strategy. This generally should not be + actions are enforced in the players strategy. This generally should not be necessary, but various tests may force impossible or unlikely histories. """ if h1 and h2: - # Simulate Players - mock_P1 = MockPlayer(P1, h1) - mock_P2 = MockPlayer(P2, h1) - mock_P1.state_distribution = defaultdict( - int, zip(P1.history, P2.history)) - mock_P2.state_distribution = defaultdict( - int, zip(P2.history, P1.history)) + mock_P1 = MockPlayer(actions=[h1], history=P1.history) + mock_P2 = MockPlayer(actions=[h2], history=P2.history) # Force plays - s1 = P1.strategy(mock_P2) s2 = P2.strategy(mock_P1) + if (s1 != h1) or (s2 != h2): + warnings.warn( + "Simulated play mismatch with expected history: Round was " + "({}, {}) but ({}, {}) was expected for player: {}".format( + s1, s2, h1, h2, str(P1)) + ) # Record intended history # Update Cooperation / Defection counts update_history(P1, h1) update_history(P2, h2) update_state_distribution(P1, h1, h2) update_state_distribution(P2, h2, h1) - return (h1, h2) + return (s1, s2) else: s1 = P1.strategy(P2) s2 = P2.strategy(P1) diff --git a/axelrod/strategies/appeaser.py b/axelrod/strategies/appeaser.py index a9f64907a..e3c991019 100644 --- a/axelrod/strategies/appeaser.py +++ b/axelrod/strategies/appeaser.py @@ -2,6 +2,7 @@ C, D = Actions.C, Actions.D + class Appeaser(Player): """A player who tries to guess what the opponent wants. diff --git a/axelrod/tests/unit/test_alternator.py b/axelrod/tests/unit/test_alternator.py index 1e1013d78..242c45e46 100644 --- a/axelrod/tests/unit/test_alternator.py +++ b/axelrod/tests/unit/test_alternator.py @@ -23,9 +23,7 @@ class TestAlternator(TestPlayer): def test_strategy(self): # Starts by cooperating. self.first_play_test(C) - # Simply does the opposite to what the strategy did last time. - self.second_play_test(D, D, C, C) for i in range(10): self.responses_test([C, D] * i) - self.responses_test([C], [C, D, D, D], [C, C, C, C]) - self.responses_test([D], [C, C, D, D, C], [C, D, C, C, C]) + self.responses_test([C], [C, D, C, D], [C, C, C, C]) + self.responses_test([D], [C, D, C, D, C], [C, C, C, C, C]) diff --git a/axelrod/tests/unit/test_cooperator.py b/axelrod/tests/unit/test_cooperator.py index ccf09c223..daa331add 100644 --- a/axelrod/tests/unit/test_cooperator.py +++ b/axelrod/tests/unit/test_cooperator.py @@ -1,4 +1,4 @@ -"""Test for the Cooperator strategy.""" +"""Tests for the Cooperator strategy.""" import axelrod from .test_player import TestPlayer diff --git a/axelrod/tests/unit/test_mock_player.py b/axelrod/tests/unit/test_mock_player.py index 86a02102f..10b1896b8 100644 --- a/axelrod/tests/unit/test_mock_player.py +++ b/axelrod/tests/unit/test_mock_player.py @@ -9,27 +9,31 @@ class TestMockPlayer(unittest.TestCase): def test_strategy(self): - for move in [C, D]: - m = MockPlayer(axelrod.Player(), move) + for action in [C, D]: + m = MockPlayer( [action]) p2 = axelrod.Player() - self.assertEqual(move, m.strategy(p2)) - - def test_cloning(self): - p1 = axelrod.Cooperator() - p2 = axelrod.Defector() - moves = 10 - for i in range(moves): - p1.play(p2) - m1 = MockPlayer(p1, C) - m2 = MockPlayer(p2, D) - self.assertEqual(m1.move, C) - self.assertEqual(m1.history, p1.history) - self.assertEqual(m1.cooperations, p1.cooperations) - self.assertEqual(m1.defections, p1.defections) - self.assertEqual(m2.move, D) - self.assertEqual(m2.history, p2.history) - self.assertEqual(m2.cooperations, p2.cooperations) - self.assertEqual(m2.defections, p2.defections) + self.assertEqual(action, m.strategy(p2)) + + actions = [C, C, D, D, C, C] + m = MockPlayer(actions) + p2 = axelrod.Player() + for action in actions: + self.assertEqual(action, m.strategy(p2)) + + def test_history(self): + t = TestOpponent() + m1 = MockPlayer([C], history=[C]*10) + self.assertEqual(m1.actions[0], C) + self.assertEqual(m1.history, [C] * 10) + self.assertEqual(m1.cooperations, 10) + self.assertEqual(m1.defections, 0) + self.assertEqual(m1.strategy(t), C) + m2 = MockPlayer([D], history=[D]*10) + self.assertEqual(m2.actions[0], D) + self.assertEqual(m2.history, [D] * 10) + self.assertEqual(m2.cooperations, 0) + self.assertEqual(m2.defections, 10) + self.assertEqual(m2.strategy(t), D) class TestUpdateHistories(unittest.TestCase): @@ -67,9 +71,10 @@ def test_various(self): self.assertEqual(p1.defections, 0) self.assertEqual(p2.defections, 0) + # TestOpponent always returns C for h1 in [C, D]: for h2 in [C, D]: - self.assertEqual(simulate_play(p1, p2, h1, h2), (h1, h2)) + self.assertEqual(simulate_play(p1, p2, h1, h2), (C, C)) self.assertEqual(p1.cooperations, 3) self.assertEqual(p2.cooperations, 3) self.assertEqual(p1.defections, 2) diff --git a/axelrod/tests/unit/test_player.py b/axelrod/tests/unit/test_player.py index 673c356ad..fc7f29756 100644 --- a/axelrod/tests/unit/test_player.py +++ b/axelrod/tests/unit/test_player.py @@ -1,8 +1,9 @@ import random +import warnings import unittest import axelrod -from axelrod import DefaultGame, Player, simulate_play +from axelrod import DefaultGame, MockPlayer, Player, simulate_play C, D = axelrod.Actions.C, axelrod.Actions.D @@ -131,7 +132,7 @@ def test_responses(test_class, player1, player2, responses, history1=None, # method. Still need to append history manually. if history1 and history2: for h1, h2 in zip(history1, history2): - simulate_play(player1, player2, h1, h2) + s1, s2 = simulate_play(player1, player2, h1, h2) # Run the tests for response in responses: s1, s2 = simulate_play(player1, player2) @@ -265,10 +266,14 @@ def first_play_test(self, play, seed=None): def second_play_test(self, rCC, rCD, rDC, rDD, seed=None): """Test responses to the four possible one round histories. Input responses is simply the four responses to CC, CD, DC, and DD.""" - self.responses_test(rCC, [C], [C], seed=seed) - self.responses_test(rCD, [C], [D], seed=seed) - self.responses_test(rDC, [D], [C], seed=seed) - self.responses_test(rDD, [D], [D], seed=seed) + test_responses(self, self.player(), axelrod.Cooperator(), + rCC, [C], [C], seed=seed) + test_responses(self, self.player(), axelrod.Defector(), + rCD, [C], [D], seed=seed) + test_responses(self, self.player(), axelrod.Cooperator(), + rDC, [D], [C], seed=seed) + test_responses(self, self.player(), axelrod.Defector(), + rDD, [D], [D], seed=seed) def responses_test(self, responses, history1=None, history2=None, seed=None, tournament_length=200, attrs=None, @@ -285,10 +290,8 @@ def responses_test(self, responses, history1=None, history2=None, player1 = self.player(*init_args, **init_kwargs) player1.set_match_attributes(length=tournament_length) - # player1.match_attributes['length'] = tournament_length - player2 = TestOpponent() + player2 = MockPlayer() player2.set_match_attributes(length=tournament_length) - # player2.match_attributes['length'] = tournament_length test_responses(self, player1, player2, responses, history1, history2, seed=seed, attrs=attrs) diff --git a/docs/tutorials/contributing/strategy/writing_test_for_the_new_strategy.rst b/docs/tutorials/contributing/strategy/writing_test_for_the_new_strategy.rst index 6a433754d..814df69bd 100644 --- a/docs/tutorials/contributing/strategy/writing_test_for_the_new_strategy.rst +++ b/docs/tutorials/contributing/strategy/writing_test_for_the_new_strategy.rst @@ -6,10 +6,42 @@ where :code:`.py` is the name of the file you have created or similarly add tests to the test file that is already present in the :code:`axelrod/tests/unit/` directory. +Typically we want to test the following: + +* That the strategy behaves as intended on the first move and subsequent moves, +triggering any expected actions + +* That the strategy initializes correctly + +* That the strategy resets and clones correctly + +If the strategy does not use any internal variables then there are generic tests +that are automatically invoked to verify proper initialization, resetting, and +cloning. + +There are several convenience functions to help write tests efficiently for +how a strategy plays. + +* `first_play_test(action, seed=None)` tests the strategy's first action, taking +an optional random seed in case the strategy is stochastic. If so, please +include cases where both outcomes are observed, e.g.:: + + def test_strategy(self): + self.first_play_test(C, seed=11) + self.first_play_test(D, seed=23) + +* `second_play_test(rCC, rCD, rDC, rDD, seed=None)` tests the strategies actions +in the four possible second rounds of play, depending on the move the the +strategy and the opponent in the first round. + +* `responses_test(responses, history1, history2, ...)` is a powerful test that +can handle a variety of situations, testing the first X actions, the actions +played in response to given player histories, and can also check that internal +attributes are set. + As an example, the tests for Tit-For-Tat are as follows:: import axelrod - from test_player import TestPlayer C, D = axelrod.Actions.C, axelrod.Actions.D @@ -32,31 +64,29 @@ As an example, the tests for Tit-For-Tat are as follows:: } def test_strategy(self): - """Starts by cooperating.""" + # Starts by cooperating. self.first_play_test(C) - - def test_effect_of_strategy(self): - """Repeats last action of opponent history.""" + # Repeats last action of opponent history. self.second_play_test([C, D, C, D]) - self.responses_test([C] * 4, [C, C, C, C], [C]) - self.responses_test([C] * 5, [C, C, C, C, D], [D]) + self.responses_test([C], [C] * 4, [C, C, C, C]) + self.responses_test([D], [C] * 5, [C, C, C, C, D]) -The :code:`test_effect_of_strategy` method mainly checks that the +The :code:`test_strategy` method mainly checks that the :code:`strategy` method in the :code:`TitForTat` class works as expected: 1. If the opponent's last strategy was :code:`C`: then :code:`TitForTat` should cooperate:: - self.responses_test([C] * 4, [C, C, C, C], [C]) + self.responses_test([C], [C] * 4, [C, C, C, C]) 2. If the opponent's last strategy was :code:`D`: then :code:`TitForTat` should defect:: - self.responses_test([C] * 5, [C, C, C, C, D], [D]) + self.responses_test([D], [C] * 5, [C, C, C, C, D]) We have added some convenience member functions to the :code:`TestPlayer` class. All three of these functions can take an optional keyword argument -:code:`random_seed` (useful for stochastic strategies). +:code:`seed` (useful for stochastic strategies). 1. The member function :code:`first_play_test` tests the first strategy, e.g.:: @@ -65,53 +95,44 @@ All three of these functions can take an optional keyword argument This is equivalent to:: - def test_effect_of_strategy(self): - P1 = axelrod.TitForTat() # Or whatever player is in your test class - P2 = axelrod.Player() - P2.history = [] - P2.history = [] - self.assertEqual(P1.strategy(P2), 'C') + P1 = axelrod.TitForTat() # Or whatever player is in your test class + P2 = axelrod.Player() + self.assertEqual(P1.strategy(P2), 'C') 2. The member function :code:`second_play_test` takes a list of four plays, each following one round of CC, CD, DC, and DD respectively:: - def test_effect_of_strategy(self): - self.second_play_test(['C', 'D', 'D', 'C']) - - This is equivalent to:: + self.second_play_test('C', 'D', 'D', 'C') - def test_effect_of_strategy(self): - P1 = axelrod.TitForTat() # Or whatever player is in your test class - P2 = axelrod.Player() - P2.history = ['C'] - P2.history = ['C'] - self.assertEqual(P1.strategy(P2), 'C') - P2.history = ['C'] - P2.history = ['D'] - self.assertEqual(P1.strategy(P2), 'D') - P2.history = ['D'] - P2.history = ['C'] - self.assertEqual(P1.strategy(P2), 'D') - P2.history = ['D'] - P2.history = ['D'] - self.assertEqual(P1.strategy(P2), 'C') + This is equivalent to choosing an opponent will play C or D as needed and + checking the next move. This function can also take an optional random seed + argument `seed`. 3. The member function :code:`responses_test` takes arbitrary histories for each player and tests a list of expected next responses:: def test_effect_of_strategy(self): - self.responses_test([C], [C], [D, C, C, C], random_seed=15) + self.responses_test([D, C, C, C], [C], [C], random_seed=15) + + In this case each player has their history simulated to be :code:`[C]` and + the expected responses are D, C, C, C. Note that the histories will elongate + as the responses accumulated, with the opponent accruing cooperations. - In this case each player has their history set to :code:`[C]` and the - expected responses are D, C, C, C. Note that the histories will elongate as - the responses accumulated. + If the given histories are not possible for the strategy then the test will + not be meaningful. For example, setting the history of Defector to have + cooperations is not a possible history of play since Defector always defects, + and so will not actually test the strategy correctly. The test suite will + warn you if it detects a mismatch in simulated history and actual history. + + Note also that in general it is not a good idea to manually set the history + of any player. The function :code:`responses_test` also accepts a dictionary parameter of attributes to check at the end of the checks. For example this test checks if the player's internal variable :code:`opponent_class` is set to :code:`"Cooperative"`:: - self.responses_test([C] * 6, [C] * 6, [C], + self.responses_test([C], [C] * 6, [C] * 6, attrs={"opponent_class": "Cooperative"}) Finally, there is a :code:`TestHeadsUp` class that streamlines the testing of @@ -125,6 +146,9 @@ example, to test several rounds of play of :code:`TitForTwoTats` versus outcomes = [[C, D], [C, D], [D, D], [D, C], [C, C], [C, D], [C, D], [D, D]] self.versus_test(axelrod.TitFor2Tats, axelrod.Bully, outcomes) +Using `TestHeadsUp` is essentially equivalent to playing a short `Match` between +the players and checking the outcome. + The function :code:`versus_test` also accepts a :code:`random_seed` keyword, and like :code:`responses_test` the history is accumulated. diff --git a/doctests.py b/doctests.py index 832bf4023..6a466c1d9 100644 --- a/doctests.py +++ b/doctests.py @@ -1,8 +1,10 @@ import doctest import os import unittest +import warnings +# Note loader and ignore are required arguments for unittest even if unused. def load_tests(loader, tests, ignore): for root, dirs, files in os.walk("."): for f in files: @@ -15,4 +17,5 @@ def load_tests(loader, tests, ignore): if __name__ == '__main__': + warnings.simplefilter("ignore") unittest.main()