From 28d7b7eea9b122b4db3c0f0caf6087b1c8572e19 Mon Sep 17 00:00:00 2001 From: Vince Knight Date: Mon, 3 Aug 2026 15:26:30 +0100 Subject: [PATCH 1/6] Fix numpy problem. --- axelrod/fingerprint.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/axelrod/fingerprint.py b/axelrod/fingerprint.py index 140e88604..f406b2a4e 100644 --- a/axelrod/fingerprint.py +++ b/axelrod/fingerprint.py @@ -48,7 +48,7 @@ def _create_points(step: float, progress_bar: bool = True) -> List[Point]: points = [] for x in np.linspace(0, 1, num): for y in np.linspace(0, 1, num): - points.append(Point(x, y)) + points.append(Point(float(x), float(y))) if progress_bar: p_bar.update() @@ -525,9 +525,8 @@ def analyse_cooperation_ratio(filename): opponent in each turn. The ith row corresponds to the ith opponent and the jth column the jth turn. """ - did_c = np.vectorize( - lambda actions: [int(action == "C") for action in actions] - ) + def did_c(actions): + return [int(action == "C") for action in actions] cooperation_rates = {} df = dd.read_csv(filename) From 583431f1facefc1fade2c0677a2cd38e196c74aa Mon Sep 17 00:00:00 2001 From: Vince Knight Date: Tue, 4 Aug 2026 10:54:25 +0100 Subject: [PATCH 2/6] Bump numpy version and modify outputs. This makes all outputs be python floats. This somewhat fits with the original (perhaps mistake) approach of the library where outputs are all python objects (for example the results_set.payoff_matrix is a list of lists although arguably should be a numpy array). --- axelrod/fingerprint.py | 15 +++++++++------ axelrod/game.py | 5 ++++- axelrod/moran.py | 1 + axelrod/plot.py | 4 ++-- axelrod/result_set.py | 22 +++++++++++++--------- axelrod/strategies/cycler.py | 22 ++++++++++++++++++++++ axelrod/tests/property.py | 8 ++++---- axelrod/tests/unit/test_game.py | 21 ++++++++++++++++++--- axelrod/tests/unit/test_pickling.py | 6 +++++- pyproject.toml | 4 +++- 10 files changed, 81 insertions(+), 27 deletions(-) diff --git a/axelrod/fingerprint.py b/axelrod/fingerprint.py index f406b2a4e..aa58609f8 100644 --- a/axelrod/fingerprint.py +++ b/axelrod/fingerprint.py @@ -177,11 +177,13 @@ def _generate_data(interactions: dict, points: list, edges: list) -> dict: the values are the mean score for the corresponding interactions. """ edge_scores = [ - np.mean( - [ - compute_final_score_per_turn(scores)[0] - for scores in interactions[edge] - ] + float( + np.mean( + [ + compute_final_score_per_turn(scores)[0] + for scores in interactions[edge] + ] + ) ) for edge in edges ] @@ -212,7 +214,7 @@ def _reshape_data(data: dict, points: list, size: int) -> np.ndarray: the standard origin. """ ordered_data = [data[point] for point in points] - shaped_data = np.reshape(ordered_data, (size, size), order="F") + shaped_data: np.ndarray = np.reshape(ordered_data, (size, size), order="F") plotting_data = np.flipud(shaped_data) return plotting_data @@ -525,6 +527,7 @@ def analyse_cooperation_ratio(filename): opponent in each turn. The ith row corresponds to the ith opponent and the jth column the jth turn. """ + def did_c(actions): return [int(action == "C") for action in actions] diff --git a/axelrod/game.py b/axelrod/game.py index c2b4ffc44..143e89202 100644 --- a/axelrod/game.py +++ b/axelrod/game.py @@ -75,7 +75,10 @@ def get_value(x): row, col = map(get_value, pair) - return (self.A[row][col], self.B[row][col]) + # Use `.item()` to return native Python scalars rather than NumPy + # scalars, whose repr (`np.int64(3)`) would otherwise leak into the + # string representation of scores. + return (self.A[row][col].item(), self.B[row][col].item()) def __repr__(self) -> str: return "Axelrod game with matrices: {}".format((self.A, self.B)) diff --git a/axelrod/moran.py b/axelrod/moran.py index 539d3d5db..0b7ea7320 100644 --- a/axelrod/moran.py +++ b/axelrod/moran.py @@ -189,6 +189,7 @@ def fitness_proportionate_selection( An index of the above list selected at random proportionally to the list element divided by the total. """ + csums: np.ndarray if fitness_transformation is None: csums = np.cumsum(scores) else: diff --git a/axelrod/plot.py b/axelrod/plot.py index 23f5b7c9c..8e5647a6d 100644 --- a/axelrod/plot.py +++ b/axelrod/plot.py @@ -5,7 +5,7 @@ import matplotlib.pyplot as plt import matplotlib.transforms as transforms import tqdm -from numpy import arange, median, nan_to_num +from numpy import arange, median, nan_to_num, ndarray from .load_data_ import axl_filename from .result_set import ResultSet @@ -46,7 +46,7 @@ def _violinplot( width = max(self.num_players / 3, 12) height = width / 2 spacing = 4 - positions = spacing * arange(1, self.num_players + 1, 1) + positions: ndarray = spacing * arange(1, self.num_players + 1, 1) figure.set_size_inches(width, height) ax.violinplot( data, diff --git a/axelrod/result_set.py b/axelrod/result_set.py index 3d0934a94..4143cb395 100644 --- a/axelrod/result_set.py +++ b/axelrod/result_set.py @@ -297,14 +297,15 @@ def _build_summary_matrix(self, attribute, func=np.mean): for player_index, opponent_index in pairs: utilities = attribute[player_index][opponent_index] if utilities: - matrix[player_index][opponent_index] = func(utilities) + matrix[player_index][opponent_index] = float(func(utilities)) return matrix @update_progress_bar def _build_payoff_diffs_means(self): payoff_diffs_means = [ - [np.mean(diff) for diff in player] for player in self.score_diffs + [float(np.mean(diff)) for diff in player] + for player in self.score_diffs ] return payoff_diffs_means @@ -432,7 +433,7 @@ def _build_normalised_cooperation(self): with warnings.catch_warnings(): warnings.simplefilter("ignore") normalised_cooperation = [ - list(np.nan_to_num(row)) + [float(value) for value in np.nan_to_num(row)] for row in np.array(self.cooperation) / sum(map(np.array, self.match_lengths)) ] @@ -448,12 +449,13 @@ def _build_initial_cooperation_rate(self, interactions_series): ) with warnings.catch_warnings(): warnings.simplefilter("ignore") - initial_cooperation_rate = list( - np.nan_to_num( + initial_cooperation_rate = [ + float(rate) + for rate in np.nan_to_num( np.array(self.initial_cooperation_count) / interactions_array ) - ) + ] return initial_cooperation_rate @update_progress_bar @@ -706,8 +708,10 @@ def summarise(self): """ - median_scores = map(np.nanmedian, self.normalised_scores) - median_wins = map(np.nanmedian, self.wins) + median_scores = [ + float(np.nanmedian(scores)) for scores in self.normalised_scores + ] + median_wins = [float(np.nanmedian(wins)) for wins in self.wins] original_index = [index for index, _player in enumerate(self.players)] @@ -756,7 +760,7 @@ def summarise(self): ] if len(counts) > 0: - rate = np.mean(counts) + rate = float(np.mean(counts)) else: rate = 0 diff --git a/axelrod/strategies/cycler.py b/axelrod/strategies/cycler.py index 2eb27f206..9e7ad7823 100644 --- a/axelrod/strategies/cycler.py +++ b/axelrod/strategies/cycler.py @@ -95,6 +95,28 @@ def strategy(self, opponent: Player) -> Action: """Actual strategy definition that determines player's action.""" return next(self.cycle_iter) + def __getstate__(self): + """Used for pickling. + + The `cycle_iter` attribute is an `itertools.cycle` object, which + cannot be pickled from Python 3.14 onwards. We drop it here and + rebuild it in `__setstate__`.""" + state = self.__dict__.copy() + del state["cycle_iter"] + return state + + def __setstate__(self, state): + """Used for unpickling, rebuilding the dropped `cycle_iter`. + + A fresh `itertools.cycle` starts at the beginning of the cycle, so we + advance it to the position reached before pickling. The strategy + consumes one action per turn, hence the position is the number of + turns played modulo the cycle length.""" + self.__dict__.update(state) + self.set_cycle(cycle=self.cycle) + for _ in range(len(self.history) % len(self.cycle)): + next(self.cycle_iter) + def set_cycle(self, cycle: str): """Set or change the cycle.""" self.cycle = cycle diff --git a/axelrod/tests/property.py b/axelrod/tests/property.py index 81f960706..5ae8740f3 100644 --- a/axelrod/tests/property.py +++ b/axelrod/tests/property.py @@ -377,10 +377,10 @@ def games(draw, prisoners_dilemma=True, max_value=100): p = draw(integers(min_value=p_lower_bound, max_value=p_upper_bound)) else: - s = draw(integers(max_value=max_value)) - t = draw(integers(max_value=max_value)) - r = draw(integers(max_value=max_value)) - p = draw(integers(max_value=max_value)) + s = draw(integers(min_value=-max_value, max_value=max_value)) + t = draw(integers(min_value=-max_value, max_value=max_value)) + r = draw(integers(min_value=-max_value, max_value=max_value)) + p = draw(integers(min_value=-max_value, max_value=max_value)) game = axl.Game(r=r, s=s, t=t, p=p) return game diff --git a/axelrod/tests/unit/test_game.py b/axelrod/tests/unit/test_game.py index e124c9e18..a32cae237 100644 --- a/axelrod/tests/unit/test_game.py +++ b/axelrod/tests/unit/test_game.py @@ -43,7 +43,12 @@ def test_not_default_equality(self): def test_wrong_class_equality(self): self.assertNotEqual(axl.Game(), "wrong class") - @given(r=integers(), p=integers(), s=integers(), t=integers()) + @given( + r=integers(min_value=-1000, max_value=1000), + p=integers(min_value=-1000, max_value=1000), + s=integers(min_value=-1000, max_value=1000), + t=integers(min_value=-1000, max_value=1000), + ) @settings(max_examples=5) def test_random_init(self, r, p, s, t): """Test init with random scores using the hypothesis library.""" @@ -56,14 +61,24 @@ def test_random_init(self, r, p, s, t): game = axl.Game(r, s, t, p) self.assertEqual(game.scores, expected_scores) - @given(r=integers(), p=integers(), s=integers(), t=integers()) + @given( + r=integers(min_value=-1000, max_value=1000), + p=integers(min_value=-1000, max_value=1000), + s=integers(min_value=-1000, max_value=1000), + t=integers(min_value=-1000, max_value=1000), + ) @settings(max_examples=5) def test_random_RPST(self, r, p, s, t): """Test RPST method with random scores using the hypothesis library.""" game = axl.Game(r, s, t, p) self.assertEqual(game.RPST(), (r, p, s, t)) - @given(r=integers(), p=integers(), s=integers(), t=integers()) + @given( + r=integers(min_value=-1000, max_value=1000), + p=integers(min_value=-1000, max_value=1000), + s=integers(min_value=-1000, max_value=1000), + t=integers(min_value=-1000, max_value=1000), + ) @settings(max_examples=5) def test_random_score(self, r, p, s, t): """Test score method with random scores using the hypothesis library.""" diff --git a/axelrod/tests/unit/test_pickling.py b/axelrod/tests/unit/test_pickling.py index dff35ba96..c22f1c2ae 100644 --- a/axelrod/tests/unit/test_pickling.py +++ b/axelrod/tests/unit/test_pickling.py @@ -392,7 +392,11 @@ class LocalCooperator(axl.Cooperator): un_transformed = LocalCooperator() - self.assertRaises(AttributeError, pickle.dumps, un_transformed) + # Pickling a local class raises AttributeError up to Python 3.12 and + # PicklingError from Python 3.13 onwards. + self.assertRaises( + (AttributeError, pickle.PicklingError), pickle.dumps, un_transformed + ) player = axl.strategy_transformers.FlipTransformer()(LocalCooperator)() pickled = pickle.dumps(player) diff --git a/pyproject.toml b/pyproject.toml index 3d8ad555c..5c7bd475b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,8 @@ classifiers = [ "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "License :: OSI Approved :: MIT License", ] dependencies = [ @@ -26,7 +28,7 @@ dependencies = [ "dask[dataframe]>=2.9.2", "fsspec>=0.6.0", "matplotlib>=3.0.3", - "numpy>=1.26.4", + "numpy>=2.1", "pandas>=1.0.0", "pyyaml>=5.1", "scipy>=1.3.3", From 5c28863ebfe1b66b45ee037f5c96a968cfaff8db Mon Sep 17 00:00:00 2001 From: Vince Knight Date: Tue, 4 Aug 2026 10:56:05 +0100 Subject: [PATCH 3/6] Add support for py 3.13 + 3.14 --- .github/workflows/tests.yml | 2 +- .../index.rst | 35 +++++++++---------- tox.ini | 8 +++-- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 92c740362..a86c3bd0d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,7 +9,7 @@ jobs: max-parallel: 4 matrix: os: [ubuntu-latest, macOS-latest, windows-latest] - python-version: ["3.11", "3.12"] + python-version: ["3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v2 diff --git a/docs/tutorials/running_axelrods_first_tournament/index.rst b/docs/tutorials/running_axelrods_first_tournament/index.rst index 7f49e30cd..f2bee2409 100644 --- a/docs/tutorials/running_axelrods_first_tournament/index.rst +++ b/docs/tutorials/running_axelrods_first_tournament/index.rst @@ -123,29 +123,28 @@ given in [Axelrod1980]_ and there is no source code to base this on. This leads to some strategies being ambiguous. These are all clearly explained in the strategy docstrings. For example:: - >>> print(axl.FirstByAnonymous.__doc__) + >>> import inspect + >>> print(inspect.getdoc(axl.FirstByAnonymous)) + Submitted to Axelrod's first tournament by a graduate student whose name was + withheld. - Submitted to Axelrod's first tournament by a graduate student whose name was - withheld. + The description written in [Axelrod1980]_ is: - The description written in [Axelrod1980]_ is: + > "This rule has a probability of cooperating, P, which is initially 30% and + > is updated every 10 moves. P is adjusted if the other player seems random, + > very cooperative, or very uncooperative. P is also adjusted after move 130 + > if the rule has a lower score than the other player. Unfortunately, the + > complex process of adjustment frequently left the probability of cooperation + > in the 30% to 70% range, and therefore the rule appeared random to many + > other players." - > "This rule has a probability of cooperating, P, which is initially 30% and - > is updated every 10 moves. P is adjusted if the other player seems random, - > very cooperative, or very uncooperative. P is also adjusted after move 130 - > if the rule has a lower score than the other player. Unfortunately, the - > complex process of adjustment frequently left the probability of cooperation - > in the 30% to 70% range, and therefore the rule appeared random to many - > other players." + Given the lack of detail this strategy is implemented based on the final + sentence of the description which is to have a cooperation probability that + is uniformly random in the 30 to 70% range. - Given the lack of detail this strategy is implemented based on the final - sentence of the description which is to have a cooperation probability that - is uniformly random in the 30 to 70% range. - - Names: - - - (Name withheld): [Axelrod1980]_ + Names: + - (Name withheld): [Axelrod1980]_ Other outcomes -------------- diff --git a/tox.ini b/tox.ini index 5df968b98..5deae00d2 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,13 @@ [tox] isolated_build = True -envlist = py311, py312 +envlist = py311, py312, py313, py314 [gh-actions] python = 3.11: py311 3.12: py312 + 3.13: py313 + 3.14: py314 [flake8] per-file-ignores = @@ -27,8 +29,8 @@ deps = pytest-sugar isort black - numpy==1.26.4 - torch==2.6.0 + numpy>=2.1 + torch>=2.6.0 mypy types-setuptools commands = From d02003cb99f3cccd188b18621e36cfc9811d9822 Mon Sep 17 00:00:00 2001 From: Vince Knight Date: Tue, 4 Aug 2026 10:56:28 +0100 Subject: [PATCH 4/6] Bump numpy version. --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 0f4be075a..31f3a0022 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ docutils>=0.18.1 -numpy==1.24.3 # numpy isn't mocked due to complex use in doctests +numpy>=2.1 # numpy isn't mocked due to complex use in doctests mock>=5.1.0 torch>=2.6.0 \ No newline at end of file From 831661aa50347bd214ee0095e01108e8f346782e Mon Sep 17 00:00:00 2001 From: Vince Knight Date: Tue, 4 Aug 2026 11:20:50 +0100 Subject: [PATCH 5/6] Format with black. --- axelrod/strategies/ann.py | 2 +- axelrod/strategies/memoryone.py | 2 +- axelrod/strategies/qlearner.py | 2 +- axelrod/strategies/zero_determinant.py | 4 ++-- axelrod/tests/strategies/test_memoryone.py | 2 +- axelrod/tests/strategies/test_qlearner.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/axelrod/strategies/ann.py b/axelrod/strategies/ann.py index 4fd764c67..5b93b2fc5 100644 --- a/axelrod/strategies/ann.py +++ b/axelrod/strategies/ann.py @@ -210,7 +210,7 @@ def __init__( def _process_weights(self, weights, num_features, num_hidden): self.weights = list(weights) - (i2h, h2o, bias) = split_weights(weights, num_features, num_hidden) + i2h, h2o, bias = split_weights(weights, num_features, num_hidden) self.input_to_hidden_layer_weights = np.array(i2h) self.hidden_to_output_layer_weights = np.array(h2o) self.bias_weights = np.array(bias) diff --git a/axelrod/strategies/memoryone.py b/axelrod/strategies/memoryone.py index 6fe03c151..17f29a1a0 100644 --- a/axelrod/strategies/memoryone.py +++ b/axelrod/strategies/memoryone.py @@ -197,7 +197,7 @@ def set_initial_four_vector(self, four_vector): pass def receive_match_attributes(self): - (R, P, S, T) = self.match_attributes["game"].RPST() + R, P, S, T = self.match_attributes["game"].RPST() if self.p is None: self.p = min(1 - (T - R) / (R - S), (R - P) / (T - P)) four_vector = [1, self.p, 1, self.p] diff --git a/axelrod/strategies/qlearner.py b/axelrod/strategies/qlearner.py index 1a81ae227..50719654b 100644 --- a/axelrod/strategies/qlearner.py +++ b/axelrod/strategies/qlearner.py @@ -52,7 +52,7 @@ def __init__(self) -> None: self.prev_state = "" def receive_match_attributes(self): - (R, P, S, T) = self.match_attributes["game"].RPST() + R, P, S, T = self.match_attributes["game"].RPST() self.payoff_matrix = {C: {C: R, D: S}, D: {C: T, D: P}} def strategy(self, opponent: Player) -> Action: diff --git a/axelrod/strategies/zero_determinant.py b/axelrod/strategies/zero_determinant.py index 88e6cd22e..1bcef8158 100644 --- a/axelrod/strategies/zero_determinant.py +++ b/axelrod/strategies/zero_determinant.py @@ -134,7 +134,7 @@ def __init__(self, phi: float = 1 / 9, s: float = 0.5) -> None: super().__init__(phi, s, None) def receive_match_attributes(self): - (R, P, S, T) = self.match_attributes["game"].RPST() + R, P, S, T = self.match_attributes["game"].RPST() self.l = P super().receive_match_attributes() @@ -228,7 +228,7 @@ def __init__(self, phi: float = 0.25, s: float = 0.5) -> None: super().__init__(phi, s, None) def receive_match_attributes(self): - (R, P, S, T) = self.match_attributes["game"].RPST() + R, P, S, T = self.match_attributes["game"].RPST() self.l = R super().receive_match_attributes() diff --git a/axelrod/tests/strategies/test_memoryone.py b/axelrod/tests/strategies/test_memoryone.py index 4ae9d9340..6d6338b8b 100644 --- a/axelrod/tests/strategies/test_memoryone.py +++ b/axelrod/tests/strategies/test_memoryone.py @@ -81,7 +81,7 @@ def test_strategy2(self): ) def test_four_vector(self): - (R, P, S, T) = axl.Game().RPST() + R, P, S, T = axl.Game().RPST() p = min(1 - (T - R) / (R - S), (R - P) / (T - P)) expected_dictionary = {(C, C): 1.0, (C, D): p, (D, C): 1.0, (D, D): p} test_four_vector(self, expected_dictionary) diff --git a/axelrod/tests/strategies/test_qlearner.py b/axelrod/tests/strategies/test_qlearner.py index 07f3eb30e..a5354c9a9 100644 --- a/axelrod/tests/strategies/test_qlearner.py +++ b/axelrod/tests/strategies/test_qlearner.py @@ -22,7 +22,7 @@ class TestRiskyQLearner(TestPlayer): } def test_payoff_matrix(self): - (R, P, S, T) = axl.Game().RPST() + R, P, S, T = axl.Game().RPST() payoff_matrix = {C: {C: R, D: S}, D: {C: T, D: P}} player = self.player() self.assertEqual(player.payoff_matrix, payoff_matrix) From 13d81c686c24479fc2c1b6a5027492638071d36d Mon Sep 17 00:00:00 2001 From: Vince Knight Date: Tue, 4 Aug 2026 11:21:00 +0100 Subject: [PATCH 6/6] Reorder tox so black runs first. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 5deae00d2..ab46d7c7d 100644 --- a/tox.ini +++ b/tox.ini @@ -34,9 +34,9 @@ deps = mypy types-setuptools commands = - python -m pytest --cov-report term-missing --cov=axelrod --cov-fail-under=100 . --doctest-glob="*.md" --doctest-glob="*.rst" python -m black -l 80 . --check python -m isort --check-only axelrod/. python run_mypy.py python run_strategy_indexer.py + python -m pytest --cov-report term-missing --cov=axelrod --cov-fail-under=100 . --doctest-glob="*.md" --doctest-glob="*.rst" \ No newline at end of file