diff --git a/axelrod/strategies/_strategies.py b/axelrod/strategies/_strategies.py index b1f147993..284cca1ee 100644 --- a/axelrod/strategies/_strategies.py +++ b/axelrod/strategies/_strategies.py @@ -238,6 +238,7 @@ ) from .shortmem import ShortMem from .stalker import Stalker +from .frequency_analyzer import FrequencyAnalyzer from .titfortat import ( AdaptiveTitForTat, Alexei, @@ -366,6 +367,7 @@ ForgivingTitForTat, Fortress3, Fortress4, + FrequencyAnalyzer, GTFT, GeneralSoftGrudger, GoByMajority, diff --git a/axelrod/strategies/frequency_analyzer.py b/axelrod/strategies/frequency_analyzer.py new file mode 100644 index 000000000..9545bdc83 --- /dev/null +++ b/axelrod/strategies/frequency_analyzer.py @@ -0,0 +1,109 @@ +from axelrod.action import Action, actions_to_str +from axelrod.player import Player +from axelrod.strategy_transformers import ( + FinalTransformer, + TrackHistoryTransformer, +) + +C, D = Action.C, Action.D + + +class FrequencyAnalyzer(Player): + """ + A player starts by playing TitForTat for the first 30 turns (dataset generation phase). + + Take the matrix of last 2 moves by both Player and Opponent. + + While in dataset generation phase, construct a dictionary d, where keys are each 4 move sequence + and the corresponding value for each key is a list of the subsequent Opponent move. The 4 move sequence + starts with the Opponent move. + + For example, if a game at turn 5 looks like this: + + Opp: C, C, D, C, D + Player: C, C, C, D, C + + d should look like this: + + { [CCCC]: [D], + [CCDC]: [C], + [DCCD]: [D] } + + During dataset generation phase, Player will play TitForTat. After end of dataset generation phase, + Player will switch strategies. Upon encountering a particular 4-move sequence in the game, Player will look up history + of subsequent Opponent move. If ratio of defections to total moves exceeds p, Player will defect. Otherwise, + Player will cooperate. + + Could fall under "Hunter" class of strategies. + More likely falls under LookerUp class of strategies. + + Names: + + - FrequencyAnalyzer (FREQ): Original by Ian Miller + """ + + # These are various properties for the strategy + name = "FrequencyAnalyzer" + classifier = { + "memory_depth": float("inf"), + "stochastic": False, + "long_run_time": False, + "inspects_source": False, + "manipulates_source": False, + "manipulates_state": False, + } + + def __init__(self) -> None: + """ + Parameters + ---------- + p, float + The probability to cooperate + """ + super().__init__() + self.minimum_cooperation_ratio = 0.25 + self.frequency_table = dict() + self.last_sequence = "" + self.current_sequence = "" + + def strategy(self, opponent: Player) -> Action: + """This is the actual strategy""" + if len(self.history) > 5: + self.last_sequence = ( + str(opponent.history[-3]) + + str(self.history[-3]) + + str(opponent.history[-2]) + + str(self.history[-2]) + ) + self.current_sequence = ( + str(opponent.history[-2]) + + str(self.history[-2]) + + str(opponent.history[-1]) + + str(self.history[-1]) + ) + self.update_table(opponent) + + # dataset generation phase + if (len(self.history) < 30) or ( + self.current_sequence not in self.frequency_table + ): + if not self.history: + return C + if opponent.history[-1] == D: + return D + return C + + # post-dataset generation phase + results = self.frequency_table[self.current_sequence] + cooperates = results.count(C) + if (cooperates / len(self.history)) > self.minimum_cooperation_ratio: + return C + return D + + def update_table(self, opponent: Player): + if self.last_sequence in self.frequency_table.keys(): + results = self.frequency_table[self.last_sequence] + results.append(opponent.history[-1]) + self.frequency_table[self.last_sequence] = results + else: + self.frequency_table[self.last_sequence] = [opponent.history[-1]] diff --git a/axelrod/tests/strategies/test_frequency_analyzer.py b/axelrod/tests/strategies/test_frequency_analyzer.py new file mode 100644 index 000000000..51e04fb33 --- /dev/null +++ b/axelrod/tests/strategies/test_frequency_analyzer.py @@ -0,0 +1,159 @@ +"""Tests for the FrequencyAnalyzer strategy.""" + +import axelrod as axl + +from .test_player import TestPlayer + +C, D = axl.Action.C, axl.Action.D + + +class Test(TestPlayer): + + name = "FrequencyAnalyzer" + player = axl.FrequencyAnalyzer + expected_classifier = { + "memory_depth": float("inf"), + "stochastic": False, + "long_run_time": False, + "makes_use_of": set(), + "inspects_source": False, + "manipulates_source": False, + "manipulates_state": False, + } + + def test_strategy_early(self): + # Test games that end while still in dataset generation phase (<30 turns) + opponent_actions = [C, C, D, C, D] + expected = [(C, C), (C, C), (C, D), (D, C), (C, D)] + self.versus_test( + axl.MockPlayer(opponent_actions), expected_actions=expected, seed=4 + ) + + def test_strategy_defector(self): + # Test against all defections + opponent_actions = [D] * 30 + expected = [(C, D)] + [(D, D)] * 29 + self.versus_test( + axl.MockPlayer(opponent_actions), expected_actions=expected, seed=4 + ) + + def test_strategy_cooperator(self): + # Test games that end while still in dataset generation phase (<30 turns) + opponent_actions = [C] * 30 + expected = [(C, C)] * 30 + self.versus_test( + axl.MockPlayer(opponent_actions), expected_actions=expected, seed=4 + ) + + def test_strategy_random(self): + # Test of 50 turns against random strategy + opponent_actions = [ + C, + D, + D, + D, + D, + D, + D, + C, + D, + C, + D, + C, + D, + C, + D, + D, + C, + D, + C, + D, + D, + C, + D, + D, + D, + D, + D, + C, + C, + D, + D, + C, + C, + C, + D, + D, + C, + D, + C, + C, + C, + D, + D, + C, + C, + C, + D, + C, + D, + D, + ] + expected = [ + (C, C), + (C, D), + (D, D), + (D, D), + (D, D), + (D, D), + (D, D), + (D, C), + (C, D), + (D, C), + (C, D), + (D, C), + (C, D), + (D, C), + (C, D), + (D, D), + (D, C), + (C, D), + (D, C), + (C, D), + (D, D), + (D, C), + (C, D), + (D, D), + (D, D), + (D, D), + (D, D), + (D, C), + (C, C), + (C, D), # rd 30 (end of dataset generation phase) + (D, D), + (D, C), + ( + D, + C, + ), # example of non TFT (by this point, FrequencyAnalyzer is generally distrustful of opponent) + (C, C), + (D, D), + (D, D), + (D, C), + (D, D), + (D, C), + (D, C), + (D, C), + (D, D), + (D, D), + (D, C), + (D, C), + (D, C), + (D, D), + (D, C), + (D, D), + (D, D), + ] + self.versus_test( + axl.MockPlayer(opponent_actions), expected_actions=expected, seed=4 + ) diff --git a/docs/index.rst b/docs/index.rst index f7fe5e0b0..b8a865f4c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -53,7 +53,7 @@ Count the number of available players:: >>> import axelrod as axl >>> len(axl.strategies) - 240 + 241 Create matches between two players:: diff --git a/docs/reference/strategy_index.rst b/docs/reference/strategy_index.rst index 6732be88c..2bb232506 100644 --- a/docs/reference/strategy_index.rst +++ b/docs/reference/strategy_index.rst @@ -48,6 +48,8 @@ Here are the docstrings of all the strategies in the library. :members: .. automodule:: axelrod.strategies.forgiver :members: +.. automodule:: axelrod.strategies.frequency_analyzer + :members: .. automodule:: axelrod.strategies.gambler :members: .. automodule:: axelrod.strategies.gobymajority diff --git a/run_mypy.py b/run_mypy.py index 94e3d669e..6e3ab6775 100755 --- a/run_mypy.py +++ b/run_mypy.py @@ -31,6 +31,7 @@ "axelrod/strategies/darwin.py", "axelrod/strategies/defector.py", "axelrod/strategies/forgiver.py", + "axelrod/strategies/frequency_analyzer.py", "axelrod/strategies/gradualkiller.py", "axelrod/strategies/grudger.py", "axelrod/strategies/grumpy.py",