Skip to content

Commit 588b5a0

Browse files
committed
Merge pull request #411 from Axelrod-Python/mojones-master
Mojones-master this adds to #398
2 parents dee4255 + fd80dfe commit 588b5a0

File tree

4 files changed

+311
-1
lines changed

4 files changed

+311
-1
lines changed

axelrod/strategies/_strategies.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
DefectorHunter, CooperatorHunter, CycleHunter, AlternatorHunter,
2525
MathConstantHunter, RandomHunter, EventualCycleHunter)
2626
from .inverse import Inverse
27+
from .lookerup import LookerUp, EvolvedLookerUp
2728
from .mathematicalconstants import Golden, Pi, e
2829
from .memoryone import (ALLCorALLD,
2930
MemoryOnePlayer, GTFT, SoftJoss, StochasticCooperator, StochasticWSLS,
@@ -106,6 +107,7 @@
106107
LimitedRetaliate,
107108
LimitedRetaliate2,
108109
LimitedRetaliate3,
110+
EvolvedLookerUp,
109111
MathConstantHunter,
110112
MindBender,
111113
MindController,

axelrod/strategies/lookerup.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
from axelrod import Player, Actions
2+
from itertools import product
3+
4+
C, D = Actions.C, Actions.D
5+
6+
class LookerUp(Player):
7+
"""
8+
A strategy that uses a lookup table to decide what to do based on a
9+
combination of the last m turns and the opponent's opening n actions. If
10+
there isn't enough history to do this (i.e. for the first m turns) then
11+
cooperate.
12+
13+
The lookup table is implemented as a dict. The keys are 3-tuples giving the
14+
opponents first n actions, self's last m actions, and opponents last m
15+
actions, all as strings. The values are the actions to play on this round.
16+
For example, in the case of m=n=1, if
17+
- the opponent started by playing C
18+
- my last action was a C the opponents
19+
- last action was a D
20+
then the corresponding key would be
21+
('C', 'C', 'D')
22+
and the value would contain the action to play on this turn.
23+
24+
Some well-known strategies can be expressed as special cases; for example
25+
Cooperator is given by the dict:
26+
{('', '', '') : C}
27+
where m and n are both zero. Tit-For-Tat is given by:
28+
29+
{
30+
('', 'C', 'D') : D,
31+
('', 'D', 'D') : D,
32+
('', 'C', 'C') : C,
33+
('', 'D', 'C') : C,
34+
}
35+
36+
where m=1 and n=0.
37+
38+
Lookup tables where the action depends on the opponent's first actions (as
39+
opposed to most recent actions) will have a non-empty first string in the
40+
tuple. For example, this fragment of a dict:
41+
42+
{
43+
...
44+
('C', 'C', 'C') : C.
45+
('D', 'C', 'C') : D,
46+
...
47+
}
48+
49+
states that if self and opponent both cooperated on the previous turn, we
50+
should cooperate this turn unless the opponent started by defecting, in
51+
which case we should defect.
52+
53+
To denote lookup tables where the action depends on sequences of actions
54+
(so m or n are greater than 1), simply concatenate the strings together.
55+
Below is an incomplete example where m=3 and n=2.
56+
57+
{
58+
...
59+
('CC', 'CDD', 'CCC') : C.
60+
('CD', 'CCD', 'CCC') : D,
61+
...
62+
}
63+
"""
64+
65+
name = 'LookerUp'
66+
classifier = {
67+
'memory_depth': float('inf'),
68+
'stochastic': False,
69+
'inspects_source': False,
70+
'manipulates_source': False,
71+
'manipulates_state': False
72+
}
73+
74+
def __init__(self, lookup_table=None):
75+
"""
76+
If no lookup table is provided to the constructor, then use the TFT one.
77+
"""
78+
Player.__init__(self)
79+
80+
if not lookup_table:
81+
lookup_table = {
82+
('', 'C', 'D') : D,
83+
('', 'D', 'D') : D,
84+
('', 'C', 'C') : C,
85+
('', 'D', 'C') : C,
86+
}
87+
88+
self.lookup_table = lookup_table
89+
# Rather than pass the number of previous turns (m) to consider in as a
90+
# separate variable, figure it out. The number of turns is the length
91+
# of the second element of any given key in the dict.
92+
self.plays = len(list(self.lookup_table.keys())[0][1])
93+
# The number of opponent starting actions is the length of the first
94+
# element of any given key in the dict.
95+
self.opponent_start_plays = len(list(self.lookup_table.keys())[0][0])
96+
# If the table dictates to ignore the opening actions of the opponent
97+
# then the memory classification is adjusted
98+
if self.opponent_start_plays == 0:
99+
self.classifier['memory_depth'] = self.plays
100+
self.init_args = (lookup_table,)
101+
102+
# Ensure that table is well-formed
103+
for k, v in lookup_table.items():
104+
if (len(k[1]) != self.plays) or (len(k[0]) != self.opponent_start_plays):
105+
raise ValueError("All table elements must have the same size")
106+
if len(v) > 1:
107+
raise ValueError("Table values should be of length one, C or D")
108+
109+
def strategy(self, opponent):
110+
# If there isn't enough history to lookup an action, cooperate.
111+
if len(self.history) < max(self.plays, self.opponent_start_plays):
112+
return C
113+
# Count backward m turns to get my own recent history.
114+
history_start = -1 * self.plays
115+
my_history = ''.join(self.history[history_start:])
116+
# Do the same for the opponent.
117+
opponent_history = ''.join(opponent.history[history_start:])
118+
# Get the opponents first n actions.
119+
opponent_start = ''.join(opponent.history[:self.opponent_start_plays])
120+
# Put these three strings together in a tuple.
121+
key = (opponent_start, my_history, opponent_history)
122+
# Look up the action associated with that tuple in the lookup table.
123+
action = self.lookup_table[key]
124+
return action
125+
126+
127+
class EvolvedLookerUp(LookerUp):
128+
"""
129+
A LookerUp strategy that uses a lookup table generated using an evolutionary
130+
algorithm.
131+
"""
132+
133+
name = "EvolvedLookerUp"
134+
135+
def __init__(self, lookup_table=None):
136+
plays = 2
137+
opponent_start_plays = 2
138+
139+
# Generate the list of possible tuples, i.e. all possible combinations
140+
# of m actions for me, m actions for opponent, and n starting actions
141+
# for opponent.
142+
self_histories = [''.join(x) for x in product('CD', repeat=plays)]
143+
other_histories = [''.join(x) for x in product('CD', repeat=plays)]
144+
opponent_starts = [''.join(x) for x in
145+
product('CD', repeat=opponent_start_plays)]
146+
lookup_table_keys = list(product(opponent_starts, self_histories,
147+
other_histories))
148+
149+
# Pattern of values determed previously with an evolutionary algorithm.
150+
pattern='CDCCDCCCDCDDDDDCCDCCCDDDCDDDDDDCDDDDCDDDDCCDDCDDCDDDCCCDCDCDDDDD'
151+
# Zip together the keys and the action pattern to get the lookup table.
152+
lookup_table = dict(zip(lookup_table_keys, pattern))
153+
LookerUp.__init__(self, lookup_table=lookup_table)
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
"""Test for the Looker Up strategy."""
2+
3+
import axelrod
4+
from .test_player import TestPlayer, TestHeadsUp
5+
6+
C, D = axelrod.Actions.C, axelrod.Actions.D
7+
8+
9+
class TestLookerUp(TestPlayer):
10+
11+
name = "LookerUp"
12+
player = axelrod.LookerUp
13+
14+
expected_classifier = {
15+
'memory_depth': 1, # Default TFT table
16+
'stochastic': False,
17+
'inspects_source': False,
18+
'manipulates_source': False,
19+
'manipulates_state': False
20+
}
21+
22+
def test_init(self):
23+
# Test empty table
24+
player = self.player(dict())
25+
opponent = axelrod.Cooperator()
26+
self.assertEqual(player.strategy(opponent), C)
27+
# Test default table
28+
player = self.player()
29+
expected_lookup_table = {
30+
('', 'C', 'D') : D,
31+
('', 'D', 'D') : D,
32+
('', 'C', 'C') : C,
33+
('', 'D', 'C') : C,
34+
}
35+
self.assertEqual(player.lookup_table, expected_lookup_table)
36+
# Test malformed tables
37+
table = {(C, C): C, ('DD', 'DD'): C}
38+
with self.assertRaises(ValueError):
39+
player = self.player(table)
40+
table = {(C, C): C, (C, D): 'CD'}
41+
with self.assertRaises(ValueError):
42+
player = self.player(table)
43+
44+
def test_strategy(self):
45+
self.markov_test([C, D, C, D]) # TFT
46+
self.responses_test([C] * 4, [C, C, C, C], [C])
47+
self.responses_test([C] * 5, [C, C, C, C, D], [D])
48+
49+
def test_defector_table(self):
50+
"""
51+
Testing a lookup table that always defects if there is enough history.
52+
In order for the testing framework to be able to construct new player
53+
objects for the test, self.player needs to be callable with no
54+
arguments, thus we use a lambda expression which will call the
55+
constructor with the lookup table we want.
56+
"""
57+
defector_table = {
58+
('', C, D) : D,
59+
('', D, D) : D,
60+
('', C, C) : D,
61+
('', D, C) : D,
62+
}
63+
self.player = lambda : axelrod.LookerUp(defector_table)
64+
self.responses_test([C, C], [C, C], [D])
65+
self.responses_test([C, D], [D, C], [D])
66+
self.responses_test([D, D], [D, D], [D])
67+
68+
def test_starting_move(self):
69+
"""A lookup table that always repeats the opponent's first move."""
70+
71+
first_move_table = {
72+
# If oppponent starts by cooperating:
73+
(C, C, D) : C,
74+
(C, D, D) : C,
75+
(C, C, C) : C,
76+
(C, D, C) : C,
77+
# If opponent starts by defecting:
78+
(D, C, D) : D,
79+
(D, D, D) : D,
80+
(D, C, C) : D,
81+
(D, D, C) : D,
82+
}
83+
84+
self.player = lambda : axelrod.LookerUp(first_move_table)
85+
86+
# if the opponent started by cooperating, we should always cooperate
87+
self.responses_test([C, C, C], [C, C, C], [C])
88+
self.responses_test([D, D, D], [C, C, C], [C])
89+
self.responses_test([C, C, C], [C, D, C], [C])
90+
self.responses_test([C, C, D], [C, D, C], [C])
91+
92+
# if the opponent started by defecting, we should always defect
93+
self.responses_test([C, C, C], [D, C, C], [D])
94+
self.responses_test([D, D, D], [D, C, C], [D])
95+
self.responses_test([C, C, C], [D, D, C], [D])
96+
self.responses_test([C, C, D], [D, D, C], [D])
97+
98+
99+
class TestEvolvedLookerUp(TestPlayer):
100+
101+
name = "EvolvedLookerUp"
102+
player = axelrod.EvolvedLookerUp
103+
104+
expected_classifier = {
105+
'memory_depth': float('inf'),
106+
'stochastic': False,
107+
'inspects_source': False,
108+
'manipulates_source': False,
109+
'manipulates_state': False
110+
}
111+
112+
def test_init(self):
113+
# Check for a few known keys
114+
known_pairs = {('DD', 'CC', 'CD'): 'D', ('DC', 'CD', 'CD'): 'D',
115+
('DD', 'CD', 'CD'): 'C', ('DC', 'DC', 'DC'): 'C',
116+
('DD', 'DD', 'CC'): 'D', ('CD', 'CC', 'DC'): 'C'}
117+
player = self.player()
118+
for k, v in known_pairs.items():
119+
self.assertEqual(player.lookup_table[k], v)
120+
121+
def test_strategy(self):
122+
"""Starts by cooperating."""
123+
self.first_play_test(C)
124+
125+
126+
# Some heads up tests for EvolvedLookerUp
127+
class EvolvedLookerUpvsDefector(TestHeadsUp):
128+
def test_vs(self):
129+
outcomes = zip([C, C, D], [D, D, D])
130+
self.versus_test(axelrod.EvolvedLookerUp, axelrod.Defector, outcomes)
131+
132+
133+
class EvolvedLookerUpvsCooperator(TestHeadsUp):
134+
def test_vs(self):
135+
outcomes = zip([C] * 10, [C] * 10)
136+
self.versus_test(axelrod.EvolvedLookerUp, axelrod.Cooperator, outcomes)
137+
138+
139+
class EvolvedLookerUpvsTFT(TestHeadsUp):
140+
def test_vs(self):
141+
outcomes = zip([C] * 10, [C] * 10)
142+
self.versus_test(axelrod.EvolvedLookerUp, axelrod.TitForTat, outcomes)
143+
144+
145+
class EvolvedLookerUpvsAlternator(TestHeadsUp):
146+
def test_vs(self):
147+
outcomes = zip([C, C, D, D, D, D], [C, D, C, D, C, D])
148+
self.versus_test(axelrod.EvolvedLookerUp, axelrod.Alternator, outcomes)

docs/reference/overview_of_strategies.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1092,7 +1092,6 @@ AntiCycler plays a sequence that contains no cycles::
10921092

10931093
C CD CCD CCCD CCCCD CCCCCD ...
10941094

1095-
10961095
APavlov2006
10971096
^^^^^^^^^^^
10981097

@@ -1115,3 +1114,11 @@ counters of how often the opponent changes actions. When the counter
11151114
exceeds a threshold, OmegaTFT defects for the rest of the rounds. OmegaTFT
11161115
also keeps a counter to break deadlocks (C D to D C cycles) against
11171116
strategies like SuspiciousTitForTat.
1117+
=======
1118+
=======
1119+
1120+
LookerUp
1121+
^^^^^^^^
1122+
1123+
LookerUp uses a lookup table to decide what to play based on the last few rounds plus
1124+
the first few plays made by the opponent at the start of the match.

0 commit comments

Comments
 (0)