Skip to content

Commit 26d2fc4

Browse files
committed
Merge pull request #461 from Axelrod-Python/37
37 - Adding probabilistic ending to the match class
2 parents 0435c33 + f8c8999 commit 26d2fc4

File tree

5 files changed

+110
-12
lines changed

5 files changed

+110
-12
lines changed

.hypothesis/examples.db

0 Bytes
Binary file not shown.

axelrod/match.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# -*- coding: utf-8 -*-
2+
import random
3+
from math import log, ceil
24

35

46
def sparkline(actions, c_symbol=u'█', d_symbol=u' '):
@@ -9,7 +11,7 @@ def sparkline(actions, c_symbol=u'█', d_symbol=u' '):
911
class Match(object):
1012

1113
def __init__(self, players, turns, deterministic_cache=None,
12-
cache_mutable=True, noise=0):
14+
cache_mutable=True, noise=0, prob_end=None):
1315
"""
1416
Parameters
1517
----------
@@ -29,6 +31,7 @@ def __init__(self, players, turns, deterministic_cache=None,
2931
self._player2 = players[1]
3032
self._classes = (players[0].__class__, players[1].__class__)
3133
self._turns = turns
34+
self._prob_end = prob_end
3235
if deterministic_cache is None:
3336
self._cache = {}
3437
else:
@@ -43,16 +46,18 @@ def _stochastic(self):
4346
stochastic
4447
"""
4548
return (
49+
self._prob_end or
4650
self._noise or
4751
self._player1.classifier['stochastic'] or
4852
self._player2.classifier['stochastic'])
4953

5054
@property
5155
def _cache_update_required(self):
5256
"""
53-
A boolean to show whether the determinstic cache should be updated
57+
A boolean to show whether the deterministic cache should be updated
5458
"""
5559
return (
60+
not self._prob_end and
5661
not self._noise and
5762
self._cache_mutable and not (
5863
self._player1.classifier['stochastic'] or
@@ -77,11 +82,29 @@ def play(self):
7782
7883
i.e. One entry per turn containing a pair of actions.
7984
"""
85+
if self._prob_end is not None:
86+
# If using a probabilistic end: sample the length of the game.
87+
# This is using inverse random sample on a pdf given by:
88+
# f(n) = p_end * (1 - p_end) ^ (n - 1)
89+
# Which gives cdf:
90+
# F(n) = 1 - (1 - p) ^ n
91+
# Which gives for given x = F(n) (ie the random sample) gives n:
92+
# n = ceil((ln(1-x)/ln(1-p)))
93+
try:
94+
x = random.random()
95+
end_turn = ceil(log(1 - x) / log(1 - self._prob_end))
96+
except ZeroDivisionError:
97+
end_turn = float("inf")
98+
except ValueError:
99+
end_turn = 1
100+
else:
101+
end_turn = float("inf")
102+
80103
if (self._stochastic or self._classes not in self._cache):
81104
turn = 0
82105
self._player1.reset()
83106
self._player2.reset()
84-
while turn < self._turns:
107+
while turn < min(self._turns, end_turn):
85108
turn += 1
86109
self._player1.play(self._player2, self._noise)
87110
result = list(zip(self._player1.history, self._player2.history))

axelrod/tests/unit/test_match.py

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,39 +4,75 @@
44
import axelrod
55
from axelrod import Actions
66

7+
from hypothesis import given, example
8+
from hypothesis.strategies import integers, floats, random_module, assume
9+
710
C, D = Actions.C, Actions.D
811

912

1013
class TestMatch(unittest.TestCase):
1114

12-
def test_init(self):
15+
@given(turns=integers(min_value=1, max_value=200),
16+
prob_end=floats(min_value=0, max_value=1))
17+
@example(turns=5, prob_end=None)
18+
def test_init(self, turns, prob_end):
1319
p1, p2 = axelrod.Cooperator(), axelrod.Cooperator()
14-
match = axelrod.Match((p1, p2), 5)
20+
match = axelrod.Match((p1, p2), turns, prob_end=prob_end)
21+
self.assertEqual(match.result, [])
22+
self.assertEqual(match._player1, p1)
23+
self.assertEqual(match._player2, p2)
24+
self.assertEqual(
25+
match._classes, (axelrod.Cooperator, axelrod.Cooperator))
26+
self.assertEqual(match._turns, turns)
27+
self.assertEqual(match._prob_end, prob_end)
28+
self.assertEqual(match._cache, {})
29+
self.assertEqual(match._cache_mutable, True)
30+
self.assertEqual(match._noise, 0)
31+
32+
# Checking that prob_end has default None
33+
match = axelrod.Match((p1, p2), turns)
1534
self.assertEqual(match.result, [])
1635
self.assertEqual(match._player1, p1)
1736
self.assertEqual(match._player2, p2)
1837
self.assertEqual(
1938
match._classes, (axelrod.Cooperator, axelrod.Cooperator))
20-
self.assertEqual(match._turns, 5)
39+
self.assertEqual(match._turns, turns)
40+
self.assertEqual(match._prob_end, None)
2141
self.assertEqual(match._cache, {})
2242
self.assertEqual(match._cache_mutable, True)
2343
self.assertEqual(match._noise, 0)
2444

25-
def test_stochastic(self):
45+
@given(p=floats(min_value=0, max_value=1),
46+
rm=random_module())
47+
def test_stochastic(self, p, rm):
48+
49+
assume(0 < p < 1)
50+
2651
p1, p2 = axelrod.Cooperator(), axelrod.Cooperator()
2752
match = axelrod.Match((p1, p2), 5)
2853
self.assertFalse(match._stochastic)
2954

30-
match = axelrod.Match((p1, p2), 5, noise=0.2)
55+
match = axelrod.Match((p1, p2), 5, noise=p)
56+
self.assertTrue(match._stochastic)
57+
58+
match = axelrod.Match((p1, p2), 5, prob_end=p)
3159
self.assertTrue(match._stochastic)
3260

3361
p1 = axelrod.Random()
3462
match = axelrod.Match((p1, p2), 5)
3563
self.assertTrue(match._stochastic)
3664

37-
def test_cache_update_required(self):
65+
@given(p=floats(min_value=0, max_value=1),
66+
rm=random_module())
67+
def test_cache_update_required(self, p, rm):
68+
69+
assume(0 < p < 1)
70+
3871
p1, p2 = axelrod.Cooperator(), axelrod.Cooperator()
39-
match = axelrod.Match((p1, p2), 5, noise=0.2)
72+
match = axelrod.Match((p1, p2), 5, noise=p)
73+
self.assertFalse(match._cache_update_required)
74+
75+
match = axelrod.Match((p1, p2), 5, prob_end=p)
4076
self.assertFalse(match._cache_update_required)
4177

4278
match = axelrod.Match((p1, p2), 5, cache_mutable=False)
@@ -64,6 +100,30 @@ def test_play(self):
64100
match = axelrod.Match(players, 3, cache)
65101
self.assertEqual(match.play(), expected_result)
66102

103+
@given(turns=integers(min_value=1, max_value=200),
104+
prob_end=floats(min_value=0, max_value=1),
105+
rm=random_module())
106+
def test_prob_end_play(self, turns, prob_end, rm):
107+
108+
players = (axelrod.Cooperator(), axelrod.Defector())
109+
match = axelrod.Match(players, turns, prob_end=prob_end)
110+
self.assertTrue(0 <= len(match.play()))
111+
112+
# If game has no ending the length will be turns
113+
match = axelrod.Match(players, turns, prob_end=0)
114+
self.assertEqual(len(match.play()), turns)
115+
116+
# If game has 1 prob of ending it lasts only one turn
117+
match = axelrod.Match(players, turns, prob_end=1)
118+
self.assertEqual(len(match.play()), 1)
119+
120+
@given(prob_end=floats(min_value=0.25, max_value=0.75),
121+
rm=random_module())
122+
def test_prob_end_play_with_no_turns(self, prob_end, rm):
123+
players = (axelrod.Cooperator(), axelrod.Defector())
124+
match = axelrod.Match(players, float("inf"), prob_end=prob_end)
125+
self.assertTrue(0 <= len(match.play()))
126+
67127
def test_sparklines(self):
68128
players = (axelrod.Cooperator(), axelrod.Alternator())
69129
match = axelrod.Match(players, 4)
@@ -72,4 +132,3 @@ def test_sparklines(self):
72132
self.assertEqual(match.sparklines(), expected_sparklines)
73133
expected_sparklines = u'XXXX\nXYXY'
74134
self.assertEqual(match.sparklines('X', 'Y'), expected_sparklines)
75-

docs/tutorials/further_topics/creating_matches.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,19 @@ but you can use any characters you like::
6464
>>> print(match.sparklines(c_symbol='|', d_symbol='-'))
6565
|||||||||||||||||||||||||
6666
|-|-|-|-|-|-|-|-|-|-|-|-|
67+
68+
It is also possible to create matches that that have a given probability of
69+
ending after certain number of turns::
70+
71+
>>> import axelrod as axl
72+
>>> players = (axl.Cooperator(), axl.Alternator())
73+
>>> match = axl.Match(players, 25, prob_end=.4)
74+
>>> match.play() # doctest: +SKIP
75+
[('C', 'C'), ('C', 'D'), ('C', 'C'), ('C', 'D'), ('C', 'C')]
76+
77+
In that particular instance the probability of any turn being the last is .4 so the mean length of a match would in fact be :math:`1/0.6\approx 1.667`. Note that you can also pass an infinite amount of turns when passing an ending probability::
78+
79+
>>> players = (axl.Cooperator(), axl.Alternator())
80+
>>> match = axl.Match(players, turns=float("inf"), prob_end=.4)
81+
>>> match.play() # doctest: +SKIP
82+
[('C', 'C'), ('C', 'D')]

test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
#!/usr/bin/env bash
22
python -m unittest discover axelrod/tests/
3-
./doctest
3+
python doctests.py

0 commit comments

Comments
 (0)