diff --git a/axelrod/classifier.py b/axelrod/classifier.py index b9386099c..cc56f6000 100644 --- a/axelrod/classifier.py +++ b/axelrod/classifier.py @@ -185,7 +185,7 @@ def __getitem__( raise KeyError("Unknown classifier") def classify_player_for_this_classifier( - player: Union[Player, Type[Player]] + player: Union[Player, Type[Player]], ) -> Any: def try_lookup() -> Any: try: diff --git a/axelrod/makes_use_of.py b/axelrod/makes_use_of.py index cd6bc16d9..b397fbddd 100644 --- a/axelrod/makes_use_of.py +++ b/axelrod/makes_use_of.py @@ -36,7 +36,7 @@ def makes_use_of(player: Type[Player]) -> Set[Text]: def makes_use_of_variant( - player_or_method: Union[Callable, Type[Player]] + player_or_method: Union[Callable, Type[Player]], ) -> Set[Text]: """A version of makes_use_of that works on functions or player classes.""" try: diff --git a/axelrod/strategies/_strategies.py b/axelrod/strategies/_strategies.py index 284cca1ee..a209664c2 100644 --- a/axelrod/strategies/_strategies.py +++ b/axelrod/strategies/_strategies.py @@ -193,6 +193,7 @@ from .memorytwo import AON2, MEM2, DelayedAON1 from .memorytwo import MemoryTwoPlayer # pylint: disable=unused-import +from .momentum import Momentum from .mutual import Desperate, Hopeless, Willing from .negation import Negation from .oncebitten import FoolMeOnce, ForgetfulFoolMeOnce, OnceBitten @@ -402,6 +403,7 @@ MEM2, MathConstantHunter, Michaelos, + Momentum, NTitsForMTats, NaiveProber, Negation, diff --git a/axelrod/strategies/frequency_analyzer.py b/axelrod/strategies/frequency_analyzer.py index 9545bdc83..492e7d464 100644 --- a/axelrod/strategies/frequency_analyzer.py +++ b/axelrod/strategies/frequency_analyzer.py @@ -62,7 +62,7 @@ def __init__(self) -> None: """ super().__init__() self.minimum_cooperation_ratio = 0.25 - self.frequency_table = dict() + self.frequency_table: dict = dict() self.last_sequence = "" self.current_sequence = "" diff --git a/axelrod/strategies/memorytwo.py b/axelrod/strategies/memorytwo.py index b5edcf3c3..b351bd6d0 100644 --- a/axelrod/strategies/memorytwo.py +++ b/axelrod/strategies/memorytwo.py @@ -98,7 +98,7 @@ def set_sixteen_vector(self, sixteen_vector: Tuple[float, ...]): @staticmethod def compute_memory_depth( - sixteen_vector: Dict[Tuple[Action, Action], float] + sixteen_vector: Dict[Tuple[Action, Action], float], ) -> int: values = set(list(sixteen_vector.values())) diff --git a/axelrod/strategies/momentum.py b/axelrod/strategies/momentum.py new file mode 100644 index 000000000..7515d583e --- /dev/null +++ b/axelrod/strategies/momentum.py @@ -0,0 +1,63 @@ +from axelrod.action import Action +from axelrod.player import Player + +C, D = Action.C, Action.D + + +class Momentum(Player): + """ + This strategy is inspired by the concept of Gradual and the mathematical foundation of + the Momentum optimizer used in deep learning. + + The idea is that trust (or cooperation) evolves dynamically. A shift in trust can + create significant and rapid changes in the player's behavior, much like how momentum + responds to gradients in optimization. + + Parameters: + - alpha: Momentum decay factor that determines the rate of trust reduction. A higher value leads to slower decay, and the opponent's Defect acts as a trigger. (Optimized by Genetic Algorithm) + - threshold: The minimum momentum required to continue cooperation. If momentum falls below this value, the strategy switches to Defect as punishment. (Optimized by Genetic Algorithm) + - momentum: Represents the inertia of trust, dynamically changing based on past cooperation. + + Names: + - Momentum: Original name by Dong Won Moon + + """ + + name = "Momentum" + classifier = { + "memory_depth": float("inf"), + "stochastic": False, + "long_run_time": False, + "inspects_source": False, + "manipulates_source": False, + "manipulates_state": False, + } + + def __init__( + self, + alpha=0.9914655399877477, + threshold=0.9676595613724907, + ) -> None: + super().__init__() + self.alpha = alpha + self.threshold = threshold + self.momentum = 1.0 + + def __repr__(self): + return f"Momentum: {self.momentum}, Alpha: {self.alpha}, Threshold: {self.threshold}" + + def update_momentum(self, opponent_action): + # If the opponent defects, the momentum decreases, reflecting a loss of trust. + action_value = 1 if opponent_action == C else 0 + self.momentum = ( + self.alpha * self.momentum + (1 - self.alpha) * action_value + ) + + def strategy(self, opponent: Player) -> Action: + if len(self.history) == 0: + self.momentum = 1.0 + return C + + else: + self.update_momentum(opponent.history[-1]) + return C if self.momentum >= self.threshold else D diff --git a/axelrod/tests/strategies/test_gambler.py b/axelrod/tests/strategies/test_gambler.py index 62810b7a1..fae570990 100755 --- a/axelrod/tests/strategies/test_gambler.py +++ b/axelrod/tests/strategies/test_gambler.py @@ -1,5 +1,4 @@ -"""Test for the Gambler strategy. Most tests come from the LookerUp test suite. -""" +"""Test for the Gambler strategy. Most tests come from the LookerUp test suite.""" import copy import unittest diff --git a/axelrod/tests/strategies/test_momentum.py b/axelrod/tests/strategies/test_momentum.py new file mode 100644 index 000000000..8ff56a215 --- /dev/null +++ b/axelrod/tests/strategies/test_momentum.py @@ -0,0 +1,90 @@ +import axelrod as axl +from axelrod import Action +from axelrod.strategies.momentum import Momentum +from axelrod.tests.strategies.test_player import TestPlayer + +C, D = Action.C, Action.D + + +class TestMomentum(TestPlayer): + name = "Momentum" + player = Momentum + expected_classifier = { + "memory_depth": float("inf"), + "stochastic": False, + "long_run_time": False, + "inspects_source": False, + "manipulates_source": False, + "manipulates_state": False, + } + + def test_initialisation(self): + player = self.player(alpha=0.9, threshold=0.8) + self.assertEqual(player.alpha, 0.9) + self.assertEqual(player.threshold, 0.8) + self.assertEqual(player.momentum, 1.0) + + def test_repr(self): + player = self.player(alpha=0.9, threshold=0.8) + self.assertEqual( + repr(player), "Momentum: 1.0, Alpha: 0.9, Threshold: 0.8" + ) + + def test_strategy(self): + actions = [(C, C)] + self.versus_test( + axl.MockPlayer(actions=[C]), + expected_actions=actions, + init_kwargs={"alpha": 0.5, "threshold": 0.5}, + attrs={"momentum": 1.0}, + ) + + actions = [(C, D), (C, D), (D, D)] + self.versus_test( + axl.MockPlayer(actions=[D]), + expected_actions=actions, + init_kwargs={"alpha": 0.5, "threshold": 0.5}, + attrs={"momentum": 0.25}, + ) + + def test_vs_alternator(self): + actions = [(C, C), (C, D), (C, C), (C, D), (D, C)] + self.versus_test( + axl.Alternator(), + expected_actions=actions, + init_kwargs={"alpha": 0.5, "threshold": 0.5}, + ) + + def test_vs_cooperator(self): + actions = [(C, C), (C, C), (C, C), (C, C), (C, C)] + self.versus_test( + axl.Cooperator(), + expected_actions=actions, + init_kwargs={"alpha": 0.5, "threshold": 0.5}, + ) + + def test_vs_defector(self): + actions = [(C, D), (C, D), (D, D), (D, D), (D, D)] + self.versus_test( + axl.Defector(), + expected_actions=actions, + init_kwargs={"alpha": 0.5, "threshold": 0.5}, + ) + + def test_vs_random(self): + actions = [(C, D), (C, C), (C, C), (C, D), (D, D)] + self.versus_test( + axl.Random(), + expected_actions=actions, + seed=17, + init_kwargs={"alpha": 0.5, "threshold": 0.5}, + ) + + def test_vs_random2(self): + actions = [(C, C), (C, C), (C, C), (C, C)] + self.versus_test( + axl.Random(), + expected_actions=actions, + seed=3, + init_kwargs={"alpha": 0.5, "threshold": 0.5}, + ) diff --git a/axelrod/tests/unit/test_resultset.py b/axelrod/tests/unit/test_resultset.py index 586d38603..011f34f23 100644 --- a/axelrod/tests/unit/test_resultset.py +++ b/axelrod/tests/unit/test_resultset.py @@ -4,7 +4,6 @@ from collections import Counter import pandas as pd -from dask.dataframe.core import DataFrame from hypothesis import given, settings from numpy import mean, nanmedian, std diff --git a/docs/index.rst b/docs/index.rst index b8a865f4c..0fc7c8ff3 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) - 241 + 242 Create matches between two players:: diff --git a/docs/reference/strategy_index.rst b/docs/reference/strategy_index.rst index 2bb232506..1e570fac6 100644 --- a/docs/reference/strategy_index.rst +++ b/docs/reference/strategy_index.rst @@ -76,6 +76,8 @@ Here are the docstrings of all the strategies in the library. :members: .. automodule:: axelrod.strategies.memoryone :members: +.. automodule:: axelrod.strategies.momentum + :members: .. automodule:: axelrod.strategies.meta :members: .. automodule:: axelrod.strategies.mutual diff --git a/run_mypy.py b/run_mypy.py index b3c586c9b..9286bca9a 100755 --- a/run_mypy.py +++ b/run_mypy.py @@ -41,6 +41,7 @@ "axelrod/strategies/mathematicalconstants.py", "axelrod/strategies/memoryone.py", "axelrod/strategies/memorytwo.py", + "axelrod/strategies/momentum.py", "axelrod/strategies/mutual.py", "axelrod/strategies/negation.py", "axelrod/strategies/oncebitten.py",