Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

issue#844 backstabber.py #924

Merged
merged 8 commits into from
Mar 23, 2017
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 53 additions & 24 deletions axelrod/strategies/backstabber.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,52 +17,81 @@ class BackStabber(Player):
classifier = {
'memory_depth': float('inf'),
'stochastic': False,
'makes_use_of': set(['length']),
'makes_use_of': {'length'},
'long_run_time': False,
'inspects_source': False,
'manipulates_source': False,
'manipulates_state': False
}

@staticmethod
def strategy(opponent: Player) -> Action:
if not opponent.history:
return C
if opponent.defections > 3:
return D
return C
def strategy(self, opponent: Player) -> Action:
return _backstabber_strategy(opponent)


@FinalTransformer((D, D), name_prefix=None) # End with two defections
@FinalTransformer((D, D), name_prefix=None) # End with two defections
class DoubleCrosser(Player):
"""
Forgives the first 3 defections but on the fourth
will defect forever. If the opponent did not defect
in the first 6 rounds the player will cooperate until
the 180th round. Defects on the last 2 rounds unconditionally.
will defect forever. Defects on the last 2 rounds unconditionally.

If 8 <= current round <= 180,
if the opponent did not defect in the first 7 rounds,
the player will only defect after the opponent has defected twice in-a-row.
"""

name = 'DoubleCrosser'
classifier = {
'memory_depth': float('inf'),
'stochastic': False,
'makes_use_of': set(['length']),
'makes_use_of': {'length'},
'long_run_time': False,
'inspects_source': False,
'manipulates_source': False,
'manipulates_state': False
}

def strategy(self, opponent: Player) -> Action:
cutoff = 6

if not opponent.history:
return C
if len(opponent.history) < 180:
if len(opponent.history) > cutoff:
if D not in opponent.history[:cutoff + 1]:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here's the off-by-one. It should only check for defection in the first 6, but opponent.history[:cutoff +1] is the first 7.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, looks like you are right to me. Would be good to have @uglyfruitcake's thoughts on this PR too :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes the code doesn't match the documentation, however, It's the documentation that's wrong. I spent ages optimises the numbers in Doublecrosser to get it to perform optimally and its current state will be it's optimal performance. When I wrote the description I will have put the wrong number of turns and so it should say "the first 7 rounds". It is a mistake but its wrong in the documentation not the code. Sorry!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rewriting that now. in a moment, comments and tests should follow the specs.

if opponent.history[-2:] != [D, D]: # Fail safe
return C
if opponent.defections > 3:
return D
if _opponent_triggers_alt_strategy(opponent):
return _alt_strategy(opponent)
return _backstabber_strategy(opponent)


def _backstabber_strategy(opponent: Player) -> Action:
"""
Cooperates until opponent defects a total of four times, then always defects.
"""
if not opponent.history:
return C
if opponent.defections > 3:
return D
return C


def _alt_strategy(opponent: Player) -> Action:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you write specific tests for these module functions please.

The don't need to be extensive but just enough so that they'd fail if something got refactored in the strategy methods removing the need for them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually. this brings up a problem i had, to which module methods was the most expedient solution.

as i see it, these should all be private methods of their classes and DoubleCrosser should inherit from BackStabber. The tests, as they are, should fully cover all those methods (i don't have a coverage tool, but i think i got it).

I originally wrote class DoubleCrosser(BackStabber): , but strange things would happen with @FinalTransformer((D, D), name_prefix=None) If only BackStabber had that decorator, DoubleCrosser would not inherit that behavior. If both had that decorator I got a maximum recursion depth error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that aside, will write tests for module as is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

errrr. i hope complete tests is ok. trying to write non-complete tests is, for me, like not completing a

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha, this is way more than I was expecting but it looks good to me :)

"""
If opponent's last two plays were defect, then defects on next round. Otherwise, cooperates.
"""
final_two_plays = opponent.history[-2:]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we rename this variable? Given that a major feature of these strategies is to defect unconditionally for the final two rounds of the match, it's confusing to call this variable final_two_plays

How about previous_two_plays instead?

if final_two_plays == [D, D]:
return D
return C


def _opponent_triggers_alt_strategy(opponent: Player) -> bool:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@uglyfruitcake is this correct?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that looks good to me.

"""
If opponent did not defect in first 7 rounds and the current round is from 8 to 180, return True.
Else, return False.
"""
before_alt_strategy = first_n_rounds = 7
last_round_of_alt_strategy = 180
if _opponent_defected_in_first_n_rounds(opponent, first_n_rounds):
return False
current_round = len(opponent.history) + 1
return before_alt_strategy < current_round <= last_round_of_alt_strategy


def _opponent_defected_in_first_n_rounds(opponent: Player, first_n_rounds: int) -> bool:
"""
If opponent defected in the first N rounds, return True. Else return False.
"""
return D in opponent.history[:first_n_rounds]
109 changes: 68 additions & 41 deletions axelrod/tests/strategies/test_backstabber.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,70 +12,97 @@ class TestBackStabber(TestPlayer):
expected_classifier = {
'memory_depth': float('inf'),
'stochastic': False,
'makes_use_of': set(['length']),
'makes_use_of': {'length'},
'long_run_time': False,
'inspects_source': False,
'manipulates_source': False,
'manipulates_state': False
}

def test_strategy(self):
"""
Forgives the first 3 defections but on the fourth
will defect forever. Defects after the 198th round unconditionally.
"""

def test_defects_after_four_defections(self):
self.first_play_test(C)

# Forgives three defections
self.responses_test([C], [C], [D], length=200)
self.responses_test([C], [C, C], [D, D], length=200)
self.responses_test([C], [C, C, C], [D, D, D], length=200)
self.responses_test([D], [C, C, C, C], [D, D, D, D], length=200)
defector_actions = [(C, D), (C, D), (C, D), (C, D), (D, D), (D, D)]
self.versus_test(axelrod.Defector(), expected_actions=defector_actions, match_attributes={"length": 200})
alternator_actions = [(C, C), (C, D)] * 4 + [(D, C), (D, D)] * 2
self.versus_test(axelrod.Alternator(), expected_actions=alternator_actions, match_attributes={"length": 200})

def test_defects_on_last_two_rounds_by_match_len(self):
actions = [(C, C)] * 198 + [(D, C), (D, C)]
self.versus_test(axelrod.Cooperator(), expected_actions=actions, match_attributes={"length": 200})

actions = [(C, C)] * 10 + [(D, C), (D, C)]
self.versus_test(axelrod.Cooperator(), expected_actions=actions, match_attributes={"length": 12})

# Defects on rounds 199, and 200 no matter what
self.responses_test([C, D, D], [C] * 197, [C] * 197, length=200)
# Test that exceeds tournament length
self.responses_test([D, D, C], [C] * 198, [C] * 198, length=200)
actions = [(C, C)] * 198 + [(D, C), (D, C), (C, C), (C, C)]
self.versus_test(axelrod.Cooperator(), expected_actions=actions, match_attributes={"length": 200})
# But only if the tournament is known
self.responses_test([C, C, C], [C] * 198, [C] * 198, length=-1)
actions = [(C, C)] * 202
self.versus_test(axelrod.Cooperator(), expected_actions=actions, match_attributes={"length": -1})


class TestDoubleCrosser(TestPlayer):

class TestDoubleCrosser(TestBackStabber):
"""
Behaves like BackStabber except when its alternate strategy is triggered.
The alternate strategy is triggered when opponent did not defect in the first 7 rounds, and
8 <= the current round <= 180.
"""
name = "DoubleCrosser"
player = axelrod.DoubleCrosser
expected_classifier = {
'memory_depth': float('inf'),
'stochastic': False,
'makes_use_of': set(['length']),
'makes_use_of': {'length'},
'long_run_time': False,
'inspects_source': False,
'manipulates_source': False,
'manipulates_state': False
}

def test_strategy(self):

def test_when_alt_strategy_is_triggered(self):
"""
Forgives the first 3 defections but on the fourth will defect forever.
If the opponent did not defect in the first 6 rounds the player will
cooperate until the 180th round. Defects after the 198th round
unconditionally.
The alternate strategy is if opponent's last two plays were defect, then defect. Otherwise, cooperate.
"""
self.first_play_test(C)

# Forgives three defections
self.responses_test([C], [C], [D], length=200)
self.responses_test([C], [C, C], [D, D], length=200)
self.responses_test([C], [C, C, C], [D, D, D], length=200)
self.responses_test([D], [C, C, C, C], [D, D, D, D], length=200)

# If opponent did not defect in the first six rounds, cooperate until
# round 180
self.responses_test([C] * 174, [C] * 6, [C] * 6, length=200)
self.responses_test([C] * 160, [C] * 12, [C] * 6 + [D] + [C] * 5,
length=200)

# Defects on rounds 199, and 200 no matter what
self.responses_test([C, D, D], [C] * 197, [C] * 197, length=200)
starting_cooperation = [C] * 7
starting_rounds = [(C, C)] * 7

opponent_actions = starting_cooperation + [D, D, C, D]
expected_actions = starting_rounds + [(C, D), (C, D), (D, C), (C, D)]
self.versus_test(axelrod.MockPlayer(opponent_actions), expected_actions=expected_actions,
match_attributes={"length": 200})

opponent_actions = starting_cooperation + [D, D, D, D, C, D]
expected_actions = starting_rounds + [(C, D), (C, D), (D, D), (D, D), (D, C), (C, D)]
self.versus_test(axelrod.MockPlayer(opponent_actions), expected_actions=expected_actions,
match_attributes={"length": 200})

def test_starting_defect_keeps_alt_strategy_from_triggering(self):
opponent_actions_suffix = [C, D, C, D, D] + 3 * [C]
expected_actions_suffix = [(C, C), (C, D), (C, C), (C, D), (C, D)] + 3 * [(D, C)]

defects_on_first = [D] + [C] * 6
defects_on_first_actions = [(C, D)] + [(C, C)] * 6
self.versus_test(axelrod.MockPlayer(defects_on_first + opponent_actions_suffix),
expected_actions=defects_on_first_actions + expected_actions_suffix,
match_attributes={"length": 200})

defects_in_middle = [C, C, C, D, C, C, C]
defects_in_middle_actions = [(C, C), (C, C), (C, C), (C, D), (C, C), (C, C), (C, C)]
self.versus_test(axelrod.MockPlayer(defects_in_middle + opponent_actions_suffix),
expected_actions=defects_in_middle_actions + expected_actions_suffix,
match_attributes={"length": 200})

defects_on_last = [C] * 6 + [D]
defects_on_last_actions = [(C, C)] * 6 + [(C, D)]
self.versus_test(axelrod.MockPlayer(defects_on_last + opponent_actions_suffix),
expected_actions=defects_on_last_actions + expected_actions_suffix,
match_attributes={"length": 200})

def test_alt_strategy_stops_after_round_180(self):
one_eighty_opponent_actions = [C] * 8 + [C, D] * 86
one_eighty_expected_actions = [(C, C)] * 8 + [(C, C), (C, D)] * 86
opponent_actions = one_eighty_opponent_actions + [C] * 6
expected_actions = one_eighty_expected_actions + [(D, C)] * 6
self.versus_test(axelrod.MockPlayer(opponent_actions), expected_actions=expected_actions,
match_attributes={"length": 200})