Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 12 additions & 10 deletions axelrod/fingerprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
]
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -525,9 +527,9 @@ 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)
Expand Down
5 changes: 4 additions & 1 deletion axelrod/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
1 change: 1 addition & 0 deletions axelrod/moran.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions axelrod/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
22 changes: 13 additions & 9 deletions axelrod/result_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
]
Expand All @@ -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
Expand Down Expand Up @@ -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)]

Expand Down Expand Up @@ -756,7 +760,7 @@ def summarise(self):
]

if len(counts) > 0:
rate = np.mean(counts)
rate = float(np.mean(counts))
else:
rate = 0

Expand Down
2 changes: 1 addition & 1 deletion axelrod/strategies/ann.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
22 changes: 22 additions & 0 deletions axelrod/strategies/cycler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion axelrod/strategies/memoryone.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion axelrod/strategies/qlearner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions axelrod/strategies/zero_determinant.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand Down
8 changes: 4 additions & 4 deletions axelrod/tests/property.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion axelrod/tests/strategies/test_memoryone.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion axelrod/tests/strategies/test_qlearner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
21 changes: 18 additions & 3 deletions axelrod/tests/unit/test_game.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -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."""
Expand Down
6 changes: 5 additions & 1 deletion axelrod/tests/unit/test_pickling.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -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
35 changes: 17 additions & 18 deletions docs/tutorials/running_axelrods_first_tournament/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
<BLANKLINE>
Submitted to Axelrod's first tournament by a graduate student whose name was
withheld.
The description written in [Axelrod1980]_ is:
<BLANKLINE>
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."
<BLANKLINE>
> "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.
<BLANKLINE>
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.
<BLANKLINE>
Names:
<BLANKLINE>
- (Name withheld): [Axelrod1980]_
Names:
<BLANKLINE>
- (Name withheld): [Axelrod1980]_

Other outcomes
--------------
Expand Down
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,16 @@ 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 = [
"cloudpickle>=0.2.2",
"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",
Expand Down
Loading
Loading