diff --git a/axelrod/strategies/_strategies.py b/axelrod/strategies/_strategies.py index bc80eeccc..4203fdf87 100644 --- a/axelrod/strategies/_strategies.py +++ b/axelrod/strategies/_strategies.py @@ -76,6 +76,7 @@ from .better_and_better import BetterAndBetter from .bush_mosteller import BushMosteller from .calculator import Calculator +from .cooperate_iso import LongtermTfT, ISO, CooperateISO from .cooperator import Cooperator, TrickyCooperator from .cycler import ( AntiCycler, @@ -316,6 +317,7 @@ CautiousQLearner, CollectiveStrategy, ContriteTitForTat, + CooperateISO, Cooperator, CooperatorHunter, CycleHunter, @@ -397,11 +399,13 @@ Hopeless, Inverse, InversePunisher, + ISO, KnowledgeableWorseAndWorse, LevelPunisher, LimitedRetaliate, LimitedRetaliate2, LimitedRetaliate3, + LongtermTfT, MEM2, MathConstantHunter, Michaelos, diff --git a/axelrod/strategies/cooperate_iso.py b/axelrod/strategies/cooperate_iso.py new file mode 100644 index 000000000..bacd7a7cb --- /dev/null +++ b/axelrod/strategies/cooperate_iso.py @@ -0,0 +1,367 @@ +import numpy as np +from scipy.optimize import minimize + +from axelrod.action import Action +from axelrod.player import Player + +C, D = Action.C, Action.D + + +class LongtermTfT(Player): + """Noise-tolerant Tit-for-Tat. + + Cooperates by default and mirrors the opponent, but distinguishes + noise-corrupted cooperation from genuine defection using a statistical + test: it compares the opponent's observed defection count against the + binomial null expected from the noise rate (via a z-statistic) and + forgives defections that are consistent with noise. The number of + forgiven defections grows like O(sqrt(N_C)), so the forgiven *rate* + tends to zero — tolerating noise while staying unexploitable in the + long run. Retaliates only when the defection rate is significantly + above what noise alone would explain. + + Names: + - Longterm TFT: [Hutter2023]_ + """ + + name = "LongtermTfT" + classifier = { + "memory_depth": float("inf"), + "stochastic": False, + "makes_use_of": {"noise"}, + "long_run_time": False, + "inspects_source": False, + "manipulates_source": False, + "manipulates_state": False, + } + + def __init__(self): + super().__init__() + self.n_tft_would_c = 0 + self.n_d_when_tft_would_c = 0 + self.z = 0.0 + + def receive_match_attributes(self): + self.noise = self.match_attributes.get("noise", 0.0) + + def strategy(self, opponent: Player) -> Action: + if not self.history: + return C + if len(self.history) == 1: + return opponent.history[-1] + if self.history[-2] == C: + self.n_tft_would_c += 1 + if opponent.history[-1] == D: + self.n_d_when_tft_would_c += 1 + n_expected_ds = self.n_tft_would_c * self.noise + std_expected_ds = np.sqrt( + self.noise * (1 - self.noise) * self.n_tft_would_c + ) + # This becomes n_d_when_tft_would_c for noise->0 + self.z = (self.n_d_when_tft_would_c - n_expected_ds) / max( + 1.0, std_expected_ds + ) + if self.n_tft_would_c >= 5 and self.z < 2: + return C + else: + # TfT + return opponent.history[-1] + + +# We describe memory-1 strategies as length-4 arrays, quantifying the probability of cooperation in the states [CC, CD, DC, DD]. + + +def get_reward( + my_strategy: np.ndarray, + opp_strategy: np.ndarray, + init_state: np.ndarray, + p_end: float, + p_noise: float, + RPST: tuple[float, float, float, float], +) -> float: + """ + Calculates the expected average reward per step for a given policy + against a specific opponent strategy (including the effect of noise), + utilizing Markov transition matrices. + """ + # Apply p_noise only to own strategy, not to opponent + # (the opponent strategy already includes noise effects). + own = my_strategy + p_noise * (1.0 - 2.0 * my_strategy) + + # Flip CD/DC for opponent. + opp = opp_strategy[[0, 2, 1, 3]] + + # Build the transition matrix. + trans_mat = np.array( + [ + own * opp, + own * (1.0 - opp), + (1.0 - own) * opp, + (1.0 - own) * (1.0 - opp), + ] + ).T + + R, P, S, T = RPST + rewards = np.array([R, S, T, P], dtype=float) + + # Don't include init state in summed rewards. + inv = np.linalg.inv(np.eye(4) - (1.0 - p_end) * trans_mat) + + # Calculate expected reward, + reward = init_state @ (inv @ rewards - rewards) + + # Avg. reward per step + return p_end * float(reward) / (1.0 - p_end) + + +def optimize_against( + opponent: np.ndarray, + init_state_idx: int, + p_end: float, + p_noise: float, + RPST: tuple[float, float, float, float], +) -> tuple[float, np.ndarray]: + """ + Discovers the optimal response strategy (policy) against a fixed opponent + model by maximizing the expected reward from a given starting state. + """ + assert p_noise < 0.5 + + # Clamp to possible values, given noise + opp = np.clip(opponent, p_noise, 1.0 - p_noise) + + # Setup initial state + init_state = np.zeros(4, dtype=np.float32) + init_state[init_state_idx] = 1.0 + + # Define the objective function to minimize (negative reward) + def objective(params: np.ndarray) -> float: + return -get_reward(params, opp, init_state, p_end, p_noise, RPST) + + x0 = np.array([0.5, 0.5, 0.5, 0.5]) + bounds = [(0.0, 1.0), (0.0, 1.0), (0.0, 1.0), (0.0, 1.0)] + result = minimize( + objective, x0, method="L-BFGS-B", bounds=bounds, options={"maxiter": 50} + ) + + # result.fun is the minimum loss (-reward), result.x are the optimal parameters + return -result.fun, result.x + + +class ISO(Player): + """Optimal response against a memory-1 opponent model. + + Estimates the opponent's memory-1 (order-1) conditional cooperation + probabilities, which together with its own memory-1 strategy induce a + Markov chain over outcome pairs. Computes the exact expected discounted + long-term payoff in closed form via the chain's stationary/resolvent + solution, then optimizes its own memory-1 policy to maximize it. A + simplification and refinement of DBS: it replaces bounded-depth tree + search with the exact infinite-horizon value, yielding stronger play + against exploitable opponents at lower complexity. Adaptive only w.r.t. + memory-1 opponents (the model is misspecified for higher-memory play). + + Names: + - ISO: [Hutter2023]_ + """ + + name = "ISO" + classifier = { + "memory_depth": float("inf"), + "stochastic": True, + "makes_use_of": {"noise", "game"}, + "long_run_time": True, + "inspects_source": False, + "manipulates_source": False, + "manipulates_state": False, + } + + def __init__(self): + super().__init__() + self.discount_factor = 0.99 + + # Track the opponent's rate of cooperation (numerator, denominator) for each state. + # Assume we have seen the opponent play following TfT once in each state, + # to make the opponent-model well-defined from the start. + self.ewma_CC = [1.0, 1.0] + self.ewma_CD = [1.0, 1.0] + self.ewma_DC = [0.0, 1.0] + self.ewma_DD = [0.0, 1.0] + + # Initial cooperation probabilities (num / den) + self.opp_model = [1.0, 0.0, 1.0, 0.0] + self.my_policy = [1.0, 0.0, 1.0, 0.0] + + def receive_match_attributes(self): + self.noise = self.match_attributes.get("noise", 0.0) + self.RPST = self.match_attributes["game"].RPST() + + def _update_single_ewma( + self, state_ewma: list[float], action_val: float + ) -> float: + """Updates the (numerator, denominator) pair in-place and returns the new average.""" + state_ewma[0] = self.discount_factor * state_ewma[0] + action_val + state_ewma[1] = self.discount_factor * state_ewma[1] + 1.0 + return state_ewma[0] / state_ewma[1] + + def _update_opponent_model(self, opponent: Player): + if len(self.history) < 2: + return + + prev_state = (self.history[-2], opponent.history[-2]) + opp_act = 1.0 if opponent.history[-1] == C else 0.0 + + if prev_state == (C, C): + pr_c = self._update_single_ewma(self.ewma_CC, opp_act) + self.opp_model[0] = pr_c + elif prev_state == (C, D): + pr_c = self._update_single_ewma(self.ewma_CD, opp_act) + self.opp_model[2] = pr_c + elif prev_state == (D, C): + pr_c = self._update_single_ewma(self.ewma_DC, opp_act) + self.opp_model[1] = pr_c + elif prev_state == (D, D): + pr_c = self._update_single_ewma(self.ewma_DD, opp_act) + self.opp_model[3] = pr_c + + def _get_state_idx(self, opponent) -> int: + if not self.history: + # Pretend we started with CC + return 0 + state = (self.history[-1], opponent.history[-1]) + if state == (C, C): + return 0 + elif state == (C, D): + return 1 + elif state == (D, C): + return 2 + elif state == (D, D): + return 3 + return -1 + + def update(self, opponent: Player) -> float: + """Updates the opponent model and our policy. + + Returns our expected reward per step. + """ + self._update_opponent_model(opponent) + state_idx = self._get_state_idx(opponent) + expected, my_policy = optimize_against( + self.opp_model, + init_state_idx=state_idx, + p_noise=self.noise, + RPST=self.RPST, + p_end=1e-2, + ) + self.my_policy = my_policy + return expected + + def act(self, opponent: Player) -> Action: + state_idx = self._get_state_idx(opponent) + pr_c = self.my_policy[state_idx] + return self._random.random_choice(pr_c) + + def strategy(self, opponent: Player) -> Action: + _ = self.update(opponent) + return self.act(opponent) + + +class CooperateISO(Player): + """Forgiving cooperation combined with optimal exploitation. + + Seeks to establish and sustain mutual cooperation using LongtermTFT's + noise-robust forgiveness, while switching to ISO to respond optimally + to opponents that can be exploited. In effect: cooperate with + retaliators, exploit the exploitable. This combination is the paper's + tournament-strong strategy, outperforming prior champions against the + Axelrod library across noise levels of 0–10%. + + Names: + - CooperateISO: [Hutter2023]_ + """ + + name = "CooperateISO" + classifier = { + "memory_depth": float("inf"), + "stochastic": True, + "makes_use_of": {"noise", "game"}, + "long_run_time": True, + "inspects_source": False, + "manipulates_source": False, + "manipulates_state": False, + } + + def __init__(self): + self.iso_instance = ISO() + super().__init__() + self.n_tft_would_c = 0 + self.n_d_when_tft_would_c = 0 + self.z = 0.0 + # Estimate of the opponent's rate of playing D after C, taking noise + # into account. + self.opp_pr_d_after_c = 0.0 + self.playing_iso = False + self.reward_history = [] + + def set_seed(self, seed: int = None): + super().set_seed(seed) + self.iso_instance.set_seed(seed) + + def receive_match_attributes(self): + super().receive_match_attributes() + self.RPST = self.match_attributes["game"].RPST() + self.noise = self.match_attributes["noise"] + self.iso_instance.noise = self.noise + + def _update_reward_history(self, opponent): + R, P, S, T = self.RPST + state = (self.history[-1], opponent.history[-1]) + if state == (C, C): + self.reward_history.append(R) + elif state == (C, D): + self.reward_history.append(S) + elif state == (D, C): + self.reward_history.append(T) + elif state == (D, D): + self.reward_history.append(P) + + def strategy(self, opponent: Player) -> Action: + if not self.history: + return C + self.iso_instance.history.append(self.history[-1], opponent.history[-1]) + if self.playing_iso: + return self.iso_instance.strategy(opponent) + self._update_reward_history(opponent) + expected = self.iso_instance.update(opponent) + if len(self.history) == 1: + return opponent.history[-1] + if self.history[-2] == C: + self.n_tft_would_c += 1 + if opponent.history[-1] == D: + self.n_d_when_tft_would_c += 1 + n_expected_ds = self.n_tft_would_c * self.noise + std_expected_ds = np.sqrt( + self.noise * (1 - self.noise) * self.n_tft_would_c + ) + # This becomes n_d_when_tft_would_c for noise->0 + self.z = (self.n_d_when_tft_would_c - n_expected_ds) / max( + 1.0, std_expected_ds + ) + # Should we start playing ISO? + R, P, _, _ = self.RPST + expected_gain = expected - np.mean(self.reward_history) + if ( + len(self.reward_history) >= 10 + and expected_gain + > 2.0 + * np.std(self.reward_history) + / np.sqrt(len(self.reward_history)) + and expected_gain > 0.05 * (R - P) + ): + self.playing_iso = True + return self.iso_instance.act(opponent) + if self.n_tft_would_c >= 5 and self.z < 2: + return C + else: + # TfT + return opponent.history[-1] diff --git a/axelrod/tests/strategies/test_cooperate_iso.py b/axelrod/tests/strategies/test_cooperate_iso.py new file mode 100644 index 000000000..c1e0ea933 --- /dev/null +++ b/axelrod/tests/strategies/test_cooperate_iso.py @@ -0,0 +1,325 @@ +import random +from unittest.mock import MagicMock, patch + +import numpy as np + +import axelrod as axl +from axelrod.action import Action +from axelrod.strategies.cooperate_iso import ISO, CooperateISO, LongtermTfT +from axelrod.tests.strategies.test_player import TestPlayer + +C, D = Action.C, Action.D + + +class TestLongtermTfT(TestPlayer): + name = "LongtermTfT" + player = LongtermTfT + + expected_classifier = { + "memory_depth": float("inf"), + "stochastic": False, + "makes_use_of": {"noise"}, + "long_run_time": False, + "inspects_source": False, + "manipulates_source": False, + "manipulates_state": False, + } + + def test_early_rounds_tit_for_tat(self): + """ + Tests that the strategy strictly defaults to Tit-for-Tat + when the threshold conditions (n_tft_would_c < 5) are active. + """ + # (Player Action, Opponent Action) + expected = [ + (C, C), # T1: No history, defaults to C + (C, D), # T2: Mirrors Opponent's T1 (C) + (D, D), # T3: Mirrors Opponent's T2 (D) + (D, C), # T4: Mirrors Opponent's T3 (D) + (C, C), # T5: Mirrors Opponent's T4 (C) + ] + + self.versus_test( + opponent=axl.MockPlayer(actions=[C, D, D, C, C]), + expected_actions=expected, + match_attributes={"noise": 0.1}, + ) + + def test_forgiveness_and_z_score_retaliation(self): + """ + Tests the transition from TfT to the forgiving Z-score phase, + and verifies that it retaliates when Z >= 2. + """ + # (Player Action, Opponent Action) + expected = [ + (C, C), # T1: History len 0 + (C, C), # T2: History len 1 + (C, C), # T3: n_c=1, n_d=0 -> TfT (plays C) + (C, C), # T4: n_c=2, n_d=0 -> TfT (plays C) + (C, C), # T5: n_c=3, n_d=0 -> TfT (plays C) + # --- Z-Score Phase Begins (n_c reaches 4, about to be 5) --- + (C, D), # T6: n_c=4, n_d=0 -> TfT (plays C). Opp defects. + # Opponent defected, but Z-score is low (Z=0.5), so player forgives. + ( + C, + D, + ), # T7: n_c=5, n_d=1 -> Forgives (plays C). Opp defects again. + # Z-score climbs (Z=1.4) but stays < 2. + ( + C, + D, + ), # T8: n_c=6, n_d=2 -> Forgives (plays C). Opp defects 3rd time. + # Z-score hits Z=2.3 (>= 2). Strategy falls back to TfT and retaliates. + (D, C), # T9: n_c=7, n_d=3 -> Retaliates (plays D). Opp plays C. + # Mirroring Opponent's C from T9 (Z=2.2, TfT mode). + (C, C), # T10: n_c=8, n_d=3 -> TfT (plays C). + ] + + self.versus_test( + opponent=axl.MockPlayer(actions=[C, C, C, C, C, D, D, D, C, C]), + expected_actions=expected, + match_attributes={"noise": 0.1}, + ) + + +class TestISO(TestPlayer): + name = "ISO" + player = ISO + + expected_classifier = { + "memory_depth": float("inf"), + "stochastic": True, + "makes_use_of": {"noise", "game"}, + "long_run_time": True, + "inspects_source": False, + "manipulates_source": False, + "manipulates_state": False, + } + + def test_get_state_idx(self): + """Unit test for the state indexing logic mapping history to 0,1,2,3.""" + player = self.player() + opponent = axl.MockPlayer(actions=[C, D, C, D]) + + # No history -> Defaults to 0 (CC) + self.assertEqual(player._get_state_idx(opponent), 0) + + # CC + # History.append(play, coplay) + player.history.append(C, C) + opponent.history.append(C, C) + self.assertEqual(player._get_state_idx(opponent), 0) + + # CD + player.history.append(C, D) + opponent.history.append(D, C) + self.assertEqual(player._get_state_idx(opponent), 1) + + # DC + player.history.append(D, C) + opponent.history.append(C, D) + self.assertEqual(player._get_state_idx(opponent), 2) + + # DD + player.history.append(D, D) + opponent.history.append(D, D) + self.assertEqual(player._get_state_idx(opponent), 3) + + # Invalid values + player.history.append("C", "C") + opponent.history.append("C", "C") + self.assertEqual(player._get_state_idx(opponent), -1) + + def test_update_opponent_model(self): + """Unit test for the discounted moving average calculation.""" + player = self.player() + opponent = axl.MockPlayer() + + # Turn 1: Both played C + # history.append(action, coplay) + player.history.append(C, C) + opponent.history.append(C, C) + + # Turn 2: Player played C, Opponent played D + player.history.append(C, D) + opponent.history.append(D, C) + + player._update_opponent_model(opponent) + + # Check EWMA accumulator state [numerator, denominator] for CC + # Initial state was [1.0, 1.0]; after seeing D (0.0): + # num = 0.99 * 1.0 + 0.0 = 0.99 + # den = 0.99 * 1.0 + 1.0 = 1.99 + self.assertAlmostEqual(player.ewma_CC[0], 0.99, places=6) + self.assertAlmostEqual(player.ewma_CC[1], 1.99, places=6) + + # Check discount logic: mean = num / den + expected_mean = 0.99 / 1.99 + self.assertAlmostEqual(player.opp_model[0], expected_mean, places=4) + + def test_vs_random_defects(self): + """ISO should learn to defect against a random player.""" + player = self.player() + opponent = axl.Random() + + match = axl.Match([player, opponent], turns=200, noise=0.05, seed=42) + match.play() + + for pr_c in player.my_policy: + self.assertLess(pr_c, 0.1), player.my_policy + + self.assertEqual(player.history[-1], D) + + def test_vs_tit_for_tat_with_noise_cooperates(self): + """Against TitForTat under noise, ISO should learn that cooperation avoids retaliation.""" + player = self.player() + opponent = axl.TitForTat() + + match = axl.Match([player, opponent], turns=200, noise=0.05, seed=42) + match.play() + + for pr_c in player.my_policy: + self.assertGreater(pr_c, 0.9), player.my_policy + + self.assertEqual(player.history[-1], C) + + +class TestCooperateISO(TestPlayer): + name = "CooperateISO" + player = CooperateISO + + expected_classifier = { + "memory_depth": float("inf"), + "stochastic": True, + "makes_use_of": {"noise", "game"}, + "long_run_time": True, + "inspects_source": False, + "manipulates_source": False, + "manipulates_state": False, + } + + def test_update_reward_history(self): + """Unit test for ensuring the reward history accurately maps RPST to match states.""" + player = self.player() + player.RPST = (3, 1, 0, 5) + opponent = axl.MockPlayer() + + # Turn 1: Mutual Cooperation (C, C) -> Should append R (3) + player.history.append(C, C) + opponent.history.append(C, C) + player._update_reward_history(opponent) + self.assertEqual(player.reward_history, [3]) + + # Turn 2: Sucker's payoff (C, D) -> Should append S (0) + player.history.append(C, D) + opponent.history.append(D, C) + player._update_reward_history(opponent) + self.assertEqual(player.reward_history, [3, 0]) + + # Turn 3: Temptation (D, C) -> Should append T (5) + player.history.append(D, C) + opponent.history.append(C, D) + player._update_reward_history(opponent) + self.assertEqual(player.reward_history, [3, 0, 5]) + + # Turn 4: Punishment (D, D) -> Should append P (1) + player.history.append(D, D) + opponent.history.append(D, D) + player._update_reward_history(opponent) + self.assertEqual(player.reward_history, [3, 0, 5, 1]) + + @patch("axelrod.strategies.cooperate_iso.ISO.update") + @patch("axelrod.strategies.cooperate_iso.ISO.act") + def test_maintains_tft_when_iso_not_profitable(self, mock_act, mock_update): + """ + Tests that if ISO's expected reward does not beat the historical average, + the strategy maintains LongtermTfT behavior. + """ + # ISO update always returns 0.0 (highly unprofitable) + mock_update.return_value = 0.0 + + expected = [ + (C, C), # T1: No history, defaults to C + (C, D), # T2: Mirrors Opponent's T1 (C) + (D, D), # T3: Mirrors Opponent's T2 (D) + (D, C), # T4: Mirrors Opponent's T3 (D) + (C, C), # T5: Mirrors Opponent's T4 (C) + ] + + self.versus_test( + opponent=axl.MockPlayer(actions=[C, D, D, C, C]), + expected_actions=expected, + match_attributes={"noise": 0.0, "game": axl.DefaultGame}, + ) + # Because we never switched to ISO, act() should never have been called + mock_act.assert_not_called() + + @patch("axelrod.strategies.cooperate_iso.ISO.update") + @patch("axelrod.strategies.cooperate_iso.ISO.act") + def test_switches_to_iso_when_profitable(self, mock_act, mock_update): + """ + Tests the switch condition: if we have 10 rounds of history and ISO predicts + a sufficiently high expected gain, the strategy flips to playing ISO. + """ + # We will mock ISO to return D whenever it acts + mock_act.return_value = D + + # We play 11 rounds. + # Turns 1-10: ISO predicts 3.0 (same as average for mutual cooperation, so expected_gain = 0) + # Turn 11: ISO suddenly predicts 5.0. expected_gain (2.0) crosses the threshold. + mock_update.side_effect = [3.0] * 9 + [5.0] + + # T1 to T10: Mutual cooperation (LongtermTfT mirroring) + expected = [(C, C)] * 10 + + # T11: The threshold is crossed, we switch to ISO, which our mock says will return D + expected.append((D, C)) + + self.versus_test( + opponent=axl.MockPlayer(actions=[C] * 11), + expected_actions=expected, + match_attributes={"noise": 0.0, "game": axl.DefaultGame}, + ) + + # Verify ISO took over on the final turn + mock_act.assert_called_once() + + @patch("axelrod.strategies.cooperate_iso.ISO.update") + @patch("axelrod.strategies.cooperate_iso.ISO.act") + @patch("axelrod.strategies.cooperate_iso.ISO.strategy") + def test_continues_playing_iso_on_subsequent_turns( + self, mock_strategy, mock_act, mock_update + ): + """ + Tests that once playing_iso is True, strategy() delegates directly + to self.iso_instance.strategy(opponent) on following turns. + """ + mock_act.return_value = D + mock_strategy.return_value = D + + # 9 turns of 3.0, then 5.0 for turns 10 and 11 + mock_update.side_effect = [3.0] * 9 + [5.0, 5.0] + + # T1 to T10: Mutual cooperation + # T11: Switches to ISO (calls act()) + # T12: Already playing ISO (calls strategy()) + expected = [(C, C)] * 10 + [(D, C), (D, C)] + + self.versus_test( + opponent=axl.MockPlayer(actions=[C] * 12), + expected_actions=expected, + match_attributes={"noise": 0.0, "game": axl.DefaultGame}, + ) + + mock_act.assert_called_once() + mock_strategy.assert_called_once() + + def test_set_seed(self): + """Ensures random seeds are passed down to the inner ISO instance.""" + player = self.player() + + # Mock the internal ISO instance's set_seed method + player.iso_instance.set_seed = MagicMock() + + player.set_seed(42) + player.iso_instance.set_seed.assert_called_once_with(42) diff --git a/axelrod/tests/unit/test_classification.py b/axelrod/tests/unit/test_classification.py index 758cfb4c4..7da99b389 100644 --- a/axelrod/tests/unit/test_classification.py +++ b/axelrod/tests/unit/test_classification.py @@ -302,8 +302,10 @@ def test_inclusion_of_strategy_lists(self): def test_long_run_strategies(self): long_run_time_strategies = [ + axl.CooperateISO, axl.DBS, axl.EvolvedAttention, + axl.ISO, axl.MetaMajority, axl.MetaMajorityFiniteMemory, axl.MetaMajorityLongMemory, diff --git a/docs/conf.py b/docs/conf.py index c632251b3..fc0b203d0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -17,6 +17,7 @@ import sys import mock +import sphinx_rtd_theme MOCK_MODULES = [ "dask", @@ -33,7 +34,6 @@ "prompt_toolkit.styles", "prompt_toolkit.token", "prompt_toolkit.validation", - "scipy", "scipy.stats", "tqdm", "yaml", @@ -126,13 +126,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -on_rtd = os.environ.get("READTHEDOCS", None) == "True" - -if not on_rtd: # only import and set the theme if we're building docs locally - import sphinx_rtd_theme - - html_theme = "sphinx_rtd_theme" - html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the diff --git a/docs/how-to/classify_strategies.rst b/docs/how-to/classify_strategies.rst index c529ebc67..3fe28e57a 100644 --- a/docs/how-to/classify_strategies.rst +++ b/docs/how-to/classify_strategies.rst @@ -57,7 +57,7 @@ strategies:: ... } >>> strategies = axl.filtered_strategies(filterset) >>> len(strategies) - 88 + 90 Or, to find out how many strategies only use 1 turn worth of memory to make a decision:: @@ -110,7 +110,7 @@ Some strategies have been classified as having a particularly long run time:: ... } >>> strategies = axl.filtered_strategies(filterset) >>> len(strategies) - 19 + 21 Strategies that :code:`manipulate_source`, :code:`manipulate_state` and/or :code:`inspect_source` return :code:`False` for the diff --git a/docs/index.rst b/docs/index.rst index 82b9f41b5..fc35607ac 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) - 243 + 246 Create matches between two players:: diff --git a/docs/reference/bibliography.rst b/docs/reference/bibliography.rst index c6f1a3a9b..41c4cb690 100644 --- a/docs/reference/bibliography.rst +++ b/docs/reference/bibliography.rst @@ -33,6 +33,7 @@ documentation. .. [Hauert2002] Hauert, Christoph, and Olaf Stenull. "Simple adaptive strategy wins the prisoner's dilemma." Journal of Theoretical Biology 218.3 (2002): 261-272. .. [Hilbe2013] Hilbe, C., Nowak, M.A. and Traulsen, A. (2013). Adaptive dynamics of extortion and compliance, PLoS ONE, 8(11), p. e77886. doi: 10.1371/journal.pone.0077886. .. [Hilbe2017] Hilbe, C., Martinez-Vaquero, L. A., Chatterjee K., Nowak M. A. (2017). Memory-n strategies of direct reciprocity, Proceedings of the National Academy of Sciences May 2017, 114 (18) 4715-4720; doi: 10.1073/pnas.1621239114. +.. [Hutter2023] Hutter, A. (2023). "Balancing Cooperativeness and Adaptiveness in the (Noisy) Iterated Prisoner's Dilemma." Available at: https://arxiv.org/abs/2303.03519 .. [Kuhn2017] Kuhn, Steven, "Prisoner's Dilemma", The Stanford Encyclopedia of Philosophy (Spring 2017 Edition), Edward N. Zalta (ed.), https://plato.stanford.edu/archives/spr2017/entries/prisoner-dilemma/ .. [Kraines1989] Kraines, David, and Vivian Kraines. "Pavlov and the prisoner's dilemma." Theory and decision 26.1 (1989): 47-79. doi:10.1007/BF00134056 .. [Krapohl2020] Krapohl, S., Ocelík, V. & Walentek, D.M. The instability of globalization: applying evolutionary game theory to global trade cooperation. Public Choice 188, 31–51 (2021). https://doi.org/10.1007/s11127-020-00799-1 diff --git a/docs/reference/strategy_index.rst b/docs/reference/strategy_index.rst index 9764d3082..d1dc70806 100644 --- a/docs/reference/strategy_index.rst +++ b/docs/reference/strategy_index.rst @@ -34,6 +34,8 @@ Here are the docstrings of all the strategies in the library. :members: .. automodule:: axelrod.strategies.calculator :members: +.. automodule:: axelrod.strategies.cooperate_iso + :members: .. automodule:: axelrod.strategies.cooperator :members: .. automodule:: axelrod.strategies.cycler diff --git a/docs/requirements.txt b/docs/requirements.txt index 0f4be075a..10f0bbf57 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,7 @@ +sphinx>=7.0.0,<9.0.0 +sphinx-rtd-theme>=2.0.0 docutils>=0.18.1 numpy==1.24.3 # numpy isn't mocked due to complex use in doctests mock>=5.1.0 +scipy>=1.3.3 torch>=2.6.0 \ No newline at end of file