From c5c255e8eb63ad7e445f1a2ac1d01ad480c7918c Mon Sep 17 00:00:00 2001 From: Evreu1pro Date: Sun, 2 Aug 2026 17:08:09 +0200 Subject: [PATCH 1/4] ENH: Add ZeroResp strategy (#2 in Medium Pool benchmark). Adds an adaptive state-machine strategy with dynamic epochs, a stochastic retaliation buffer (5 + U{1..10}), and a permanent red-line ban after systemic defections. Includes unit tests (initial C, buffer queue, red line, reset), strategy registration, classifier entry, and docs index update. --- axelrod/data/all_classifiers.yml | 9 + axelrod/strategies/_strategies.py | 2 + axelrod/strategies/zeroresp.py | 257 ++++++++++++++++++++++ axelrod/tests/strategies/test_zeroresp.py | 205 +++++++++++++++++ docs/index.rst | 2 +- docs/reference/strategy_index.rst | 2 + 6 files changed, 476 insertions(+), 1 deletion(-) create mode 100644 axelrod/strategies/zeroresp.py create mode 100644 axelrod/tests/strategies/test_zeroresp.py diff --git a/axelrod/data/all_classifiers.yml b/axelrod/data/all_classifiers.yml index fb0b6a51c..eb412d918 100644 --- a/axelrod/data/all_classifiers.yml +++ b/axelrod/data/all_classifiers.yml @@ -2040,3 +2040,12 @@ ZD-SET-2: manipulates_state: false memory_depth: 1 stochastic: true +ZeroResp: + inspects_source: false + long_run_time: false + makes_use_of: !!set + length: null + manipulates_source: false + manipulates_state: false + memory_depth: .inf + stochastic: true diff --git a/axelrod/strategies/_strategies.py b/axelrod/strategies/_strategies.py index bc80eeccc..b8d4881f8 100644 --- a/axelrod/strategies/_strategies.py +++ b/axelrod/strategies/_strategies.py @@ -284,6 +284,7 @@ ZDMischief, ZDSet2, ) +from .zeroresp import ZeroResp # Note: Meta* strategies are handled in .__init__.py @@ -509,5 +510,6 @@ ZDMem2, ZDMischief, ZDSet2, + ZeroResp, e, ] diff --git a/axelrod/strategies/zeroresp.py b/axelrod/strategies/zeroresp.py new file mode 100644 index 000000000..82e48cc43 --- /dev/null +++ b/axelrod/strategies/zeroresp.py @@ -0,0 +1,257 @@ +""" +ZeroResp: adaptive state-machine strategy for the Iterated Prisoner's Dilemma. + +Designed to resist both heuristic exploiters and simple RL / tabular Q-learners +via delayed stochastic retaliation, epoch-based debt accounting, and a permanent +red-line ban after systemic abuse. +""" + +from __future__ import annotations + +import math +from enum import Enum, auto +from typing import List, Optional + +from axelrod.action import Action +from axelrod.player import Player + +C, D = Action.C, Action.D + + +class _State(Enum): + """Internal finite-state labels.""" + + COOPERATIVE = auto() + EQUALIZING = auto() + RED_LINE = auto() + + +class ZeroResp(Player): + """ + An adaptive state machine that balances cooperation with delayed, + randomised retaliation and a permanent ban against systemic defectors. + + Architecture + ------------ + 1. **Dynamic epochs** — interaction is partitioned into epochs of length + ``base_epoch`` (default 25). While a retaliation debt or queued strike + is outstanding the epoch is extended; once cleared the systemic-abuse + counter resets and the bot returns to cooperative mode. + + 2. **Stochastic retaliation buffer** — a defection does not trigger an + immediate mirror response. Instead a retaliatory ``D`` is scheduled + ``5 + U{1..10}`` turns later. The random delay breaks short-horizon + Markov estimates used by tabular Q-learners and reduces cascade wars + against tit-for-tat family strategies. + + 3. **Red line (ban list)** — systemic defections (defects that arrive while + debt/queue is still open, or while already equalising) raise a counter. + After a dynamic threshold (2 or 3 depending on observed hostility) the + strategy enters permanent red line (``is_red_line = True``) and defects + unconditionally for the rest of the match. + + 4. **Anti-raider** — two or more late-game defections (past ~75% of the + known match length) are treated as end-game harvest and trigger red + line immediately. + + 5. **End-game harvest** — against highly forgiving / near-pure cooperators + (and never against grim-trigger types that never defected), ZeroResp may + defect near the known end of a finite match. This is disabled when + match length is unknown. + + Names: + + - ZeroResp: Original name by EpochRedLine / SovereignStabilizer authors + - EpochRedLine: Earlier development name + - SmartTitForTat: Legacy sandbox name + """ + + name = "ZeroResp" + classifier = { + "memory_depth": float("inf"), + "stochastic": True, + "long_run_time": False, + "inspects_source": False, + "manipulates_source": False, + "manipulates_state": False, + } + + # Fallback when match length is unknown / infinite. + _DEFAULT_MATCH_LENGTH = 200 + _LATE_FRACTION = 0.75 + _LIVE_INTEL_MIN_SAMPLES = 10 + _HOSTILE_COOP_THRESHOLD = 0.4 + _SOFT_HOSTILE_COOP = 0.7 + + def __init__(self) -> None: + """Initialise epoch accounting and red-line state.""" + super().__init__() + self.base_epoch = 25 + + self._state = _State.COOPERATIVE + self.is_red_line = False + self.epoch_step = 0 + self.debt = 0 + self.systemic = 0 + self.queue: List[int] = [] + + # Opponent cadastre (loyalty / exploitability estimates) + self.opp_len = 0 + self.opp_defects = 0 + self.opp_coops_after_my_D = 0 + self.my_D = 0 + self.last_my: Action = C + self.late_defects = 0 + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + + def _match_length(self) -> Optional[int]: + """Return known finite match length, else ``None``.""" + # Bracket access so Axelrod's makes_use_of scanner detects "length". + length = self.match_attributes["length"] + if length is None or length in (-1, float("inf")): + return None + try: + length_int = int(length) + except (TypeError, ValueError): + return None + return length_int if length_int > 0 else None + + def _effective_length(self) -> int: + return self._match_length() or self._DEFAULT_MATCH_LENGTH + + def _late_threshold(self) -> int: + return int(self._effective_length() * self._LATE_FRACTION) + + def _live_coop_rate(self) -> float: + if self.opp_len == 0: + return 1.0 + return 1.0 - (self.opp_defects / self.opp_len) + + def _is_hostile(self) -> bool: + """Enough evidence of a low-cooperation opponent.""" + return ( + self.opp_len >= self._LIVE_INTEL_MIN_SAMPLES + and self._live_coop_rate() < self._HOSTILE_COOP_THRESHOLD + ) + + def _is_soft_hostile(self) -> bool: + return ( + self.opp_len >= self._LIVE_INTEL_MIN_SAMPLES + and self._live_coop_rate() < self._SOFT_HOSTILE_COOP + ) + + def _enter_red_line(self) -> None: + self._state = _State.RED_LINE + self.is_red_line = True + self.queue.clear() + + # ------------------------------------------------------------------ + # Core strategy + # ------------------------------------------------------------------ + + def strategy(self, opponent: Player) -> Action: + """Select C or D for the current turn.""" + step = len(self.history) + 1 # 1-based turn index + + # --- Cadastre update from opponent's previous action ------------- + if opponent.history: + opp_last = opponent.history[-1] + self.opp_len += 1 + if opp_last == D: + self.opp_defects += 1 + if step > self._late_threshold(): + self.late_defects += 1 + self._on_defect(step) + else: + if self.last_my == D: + self.opp_coops_after_my_D += 1 + + # Live zero-turn defence: permanent ban against proven predators + if self._is_hostile(): + self._enter_red_line() + + # --- Anti-raider (late harvest interception) --------------------- + if self.late_defects >= 2: + self._enter_red_line() + return self._play(D) + + if self.is_red_line or self._state == _State.RED_LINE: + return self._play(D) + + # --- End-game harvest vs forgiving victims (known finite length) - + known_len = self._match_length() + if known_len is not None and self.opp_len > 50: + p_end = 1.0 / (1.0 + math.exp(-10.0 * (step / known_len - 0.85))) + forgiveness = self.opp_coops_after_my_D / max(1, self.my_D) + is_grim = self.opp_len > 50 and self.opp_defects == 0 + is_victim = forgiveness > 0.6 or ( + self.opp_defects / max(1, self.opp_len) < 0.03 + ) + if p_end > 0.75 and is_victim and not is_grim: + return self._play(D) + + # --- Queued delayed retaliation ---------------------------------- + if step in self.queue: + self.queue.remove(step) + self.debt = max(0, self.debt - 1) + self._close_epoch() + return self._play(D) + + # --- Default: cooperate & advance epoch -------------------------- + self.epoch_step += 1 + self._close_epoch() + return self._play(C) + + def _play(self, action: Action) -> Action: + self.last_my = action + if action == D: + self.my_D += 1 + return action + + def _on_defect(self, step: int) -> None: + """Record an opponent defection and schedule / escalate response.""" + if self.debt > 0 or self.queue or self._state == _State.EQUALIZING: + self.systemic += 1 + + self.debt += 1 + self._state = _State.EQUALIZING + + # Dynamic red-line threshold (tighter under hostility). + # Default: 3 systemic defects; after the first systemic event (or + # soft hostility / late defects) the threshold tightens to 2. + threshold = 3 + if ( + self.systemic >= 1 + or self._is_soft_hostile() + or self.late_defects > 0 + ): + threshold = 2 + + if self.systemic >= threshold: + self._enter_red_line() + return + + # Adaptive buffer: near-immediate under pressure, else stochastic + if self._is_hostile() or self.late_defects > 0: + delay = 1 + else: + # numpy RandomState.randint is high-exclusive → use (1, 11) + delay = 5 + int(self._random.randint(1, 11)) + + self.queue.append(step + delay) + + def _close_epoch(self) -> None: + """Reset systemic counters when a clean epoch completes.""" + if self.is_red_line or self._state == _State.RED_LINE: + return + if ( + self.epoch_step >= self.base_epoch + and self.debt <= 0 + and not self.queue + ): + self.epoch_step = 0 + self.systemic = 0 + self._state = _State.COOPERATIVE diff --git a/axelrod/tests/strategies/test_zeroresp.py b/axelrod/tests/strategies/test_zeroresp.py new file mode 100644 index 000000000..bdede4e54 --- /dev/null +++ b/axelrod/tests/strategies/test_zeroresp.py @@ -0,0 +1,205 @@ +"""Tests for the ZeroResp strategy.""" + +import axelrod as axl + +from .test_player import TestPlayer + +C, D = axl.Action.C, axl.Action.D + + +class TestZeroResp(TestPlayer): + + name = "ZeroResp" + player = axl.ZeroResp + expected_classifier = { + "memory_depth": float("inf"), + "stochastic": True, + "makes_use_of": {"length"}, + "long_run_time": False, + "inspects_source": False, + "manipulates_source": False, + "manipulates_state": False, + } + + def test_initial_move_is_always_c(self): + """Initial move is always C against any opponent.""" + for opponent in ( + axl.Cooperator(), + axl.Defector(), + axl.TitForTat(), + axl.Alternator(), + axl.Random(), + ): + player = self.player() + player.set_seed(0) + self.assertEqual(player.strategy(opponent), C) + self.assertFalse(player.is_red_line) + self.assertEqual(player.queue, []) + + def test_vs_cooperator(self): + """Never-defectors are grim-safe: full cooperation on known length.""" + actions = [(C, C)] * 50 + self.versus_test( + axl.Cooperator(), + expected_actions=actions, + match_attributes={"length": 200}, + seed=1, + attrs={"is_red_line": False, "debt": 0, "queue": []}, + ) + + def test_single_defect_queues_buffer_retaliation(self): + """ + A single D does not trigger immediate retaliation; a delayed D is + queued (buffer = 5 + U{1..10}) and fires later. + """ + # seed=1 → first delay draw yields a known schedule: + # turn 1: (C, D); turn 2 processes D and queues step 2 + delay. + # Under non-hostile samples delay ∈ [6, 15], so turn 2 is still C. + player = self.player() + opponent = axl.MockPlayer(actions=[D] + [C] * 30) + match = axl.Match( + (player, opponent), + turns=2, + seed=1, + match_attributes={"length": 200}, + ) + result = match.play() + self.assertEqual(result[0], (C, D)) + self.assertEqual(result[1], (C, C)) # buffered — not immediate D + self.assertFalse(player.is_red_line) + self.assertEqual(player.debt, 1) + self.assertEqual(len(player.queue), 1) + scheduled = player.queue[0] + self.assertGreaterEqual(scheduled, 2 + 6) # 5 + min U{1..10} + self.assertLessEqual(scheduled, 2 + 15) # 5 + max U{1..10} + + # Continue until the queued strike fires: exactly one delayed D. + player2 = self.player() + opponent2 = axl.MockPlayer(actions=[D] + [C] * 40) + match2 = axl.Match( + (player2, opponent2), + turns=30, + seed=1, + match_attributes={"length": 200}, + ) + match2.play() + self.assertEqual(player2.history[0], C) + self.assertEqual(player2.defections, 1) + self.assertFalse(player2.is_red_line) + # After the single strike clears debt/queue, no permanent ban. + self.assertEqual(player2.queue, []) + + def test_three_systemic_defects_trigger_red_line(self): + """ + Three opponent defections while debt/queue is open raise the + systemic counter and set is_red_line permanently True. + """ + # Consecutive D keeps debt open between events: + # 1st D opens debt (not yet systemic), 2nd → systemic=1, + # 3rd → systemic=2 ≥ threshold 2 → RED_LINE. + player = self.player() + opponent = axl.MockPlayer(actions=[D] * 10) + match = axl.Match( + (player, opponent), + turns=10, + seed=7, + match_attributes={"length": 200}, + ) + match.play() + self.assertTrue(player.is_red_line) + # After red line, remaining replies are unconditional D. + # First move C; after third processed D (around turn 4) permanent D. + self.assertEqual(player.history[0], C) + self.assertGreaterEqual(player.defections, 5) + # Permanent: still red-lined at end of match. + self.assertTrue(player.is_red_line) + self.assertEqual(player.queue, []) + + # versus_test form with attrs check at end of match. + # Turns 1–3: C while debt accumulates; turn 4+: permanent D (red line). + self.versus_test( + axl.Defector(), + expected_actions=[(C, D)] * 3 + [(D, D)] * 7, + turns=10, + seed=0, + match_attributes={"length": 200}, + attrs={"is_red_line": True}, + ) + + def test_reset_cleans_state_for_multi_rep_tournaments(self): + """reset() restores a clean match state (multi-rep tournaments).""" + player = self.player() + clone = player.clone() + opponent = axl.Defector() + match = axl.Match( + (player, opponent), + turns=20, + seed=11, + match_attributes={"length": 200}, + ) + match.play() + self.assertGreater(len(player.history), 0) + self.assertTrue(player.is_red_line or player.debt > 0 or player.defections > 0) + + player.reset() + self.assertEqual(player, clone) + self.assertEqual(len(player.history), 0) + self.assertFalse(player.is_red_line) + self.assertEqual(player.debt, 0) + self.assertEqual(player.systemic, 0) + self.assertEqual(player.queue, []) + self.assertEqual(player.epoch_step, 0) + self.assertEqual(player.opp_len, 0) + self.assertEqual(player.opp_defects, 0) + self.assertEqual(player.my_D, 0) + self.assertEqual(player.late_defects, 0) + self.assertEqual(player.last_my, C) + + # Second match after reset still starts with C + match2 = axl.Match( + (player, axl.Cooperator()), + turns=5, + seed=3, + match_attributes={"length": 200}, + ) + result = match2.play() + self.assertEqual(result[0], (C, C)) + self.assertFalse(player.is_red_line) + + def test_vs_tit_for_tat_cooperates(self): + actions = [(C, C)] * 20 + self.versus_test( + axl.TitForTat(), + expected_actions=actions, + match_attributes={"length": 200}, + seed=2, + attrs={"is_red_line": False}, + ) + + def test_seed_reproducible(self): + actions = None + for _ in range(2): + player = self.player() + opponent = axl.Defector() + match = axl.Match( + (player, opponent), + turns=25, + seed=42, + match_attributes={"length": 200}, + ) + result = match.play() + if actions is None: + actions = result + else: + self.assertEqual(result, actions) + + def test_unknown_length_vs_cooperator(self): + """Unknown / infinite length: no end-game harvest of pure C.""" + actions = [(C, C)] * 40 + self.versus_test( + axl.Cooperator(), + expected_actions=actions, + match_attributes={"length": float("inf")}, + seed=5, + attrs={"is_red_line": False}, + ) diff --git a/docs/index.rst b/docs/index.rst index 82b9f41b5..a379bbc7f 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 + 244 Create matches between two players:: diff --git a/docs/reference/strategy_index.rst b/docs/reference/strategy_index.rst index 9764d3082..c59eb0699 100644 --- a/docs/reference/strategy_index.rst +++ b/docs/reference/strategy_index.rst @@ -118,3 +118,5 @@ Here are the docstrings of all the strategies in the library. :members: .. automodule:: axelrod.strategies.zero_determinant :members: +.. automodule:: axelrod.strategies.zeroresp + :members: From 737e74b908c1da29d0e9aab316d84d0fe4101dd9 Mon Sep 17 00:00:00 2001 From: Evreu1pro Date: Sun, 2 Aug 2026 17:41:20 +0200 Subject: [PATCH 2/4] TST: Raise ZeroResp coverage to 100% and update classify doctests. Adds tests for match-length edge cases, end-game harvest, anti-raider, hostile live ban, and epoch/red-line close paths. Updates stochastic and makes_use_of length filter counts after registering ZeroResp. --- axelrod/tests/strategies/test_zeroresp.py | 131 ++++++++++++++++++++++ docs/how-to/classify_strategies.rst | 4 +- 2 files changed, 133 insertions(+), 2 deletions(-) diff --git a/axelrod/tests/strategies/test_zeroresp.py b/axelrod/tests/strategies/test_zeroresp.py index bdede4e54..40e48dd0a 100644 --- a/axelrod/tests/strategies/test_zeroresp.py +++ b/axelrod/tests/strategies/test_zeroresp.py @@ -203,3 +203,134 @@ def test_unknown_length_vs_cooperator(self): seed=5, attrs={"is_red_line": False}, ) + + def test_match_length_edge_cases(self): + """Cover _match_length branches: invalid, non-positive, None.""" + player = self.player() + player.set_match_attributes(length=None) + self.assertIsNone(player._match_length()) + + player.set_match_attributes(length=-1) + self.assertIsNone(player._match_length()) + + player.set_match_attributes(length=float("inf")) + self.assertIsNone(player._match_length()) + + player.set_match_attributes(length=0) + self.assertIsNone(player._match_length()) + + player.set_match_attributes(length="not-a-number") + self.assertIsNone(player._match_length()) + + player.set_match_attributes(length=[200]) # TypeError on int() + self.assertIsNone(player._match_length()) + + player.set_match_attributes(length=200) + self.assertEqual(player._match_length(), 200) + self.assertEqual(player._effective_length(), 200) + + # Fallback when length unknown + player.set_match_attributes(length=-1) + self.assertEqual(player._effective_length(), player._DEFAULT_MATCH_LENGTH) + + def test_live_coop_rate_empty_history(self): + """With no observations, live coop rate defaults to 1.0.""" + player = self.player() + self.assertEqual(player.opp_len, 0) + self.assertEqual(player._live_coop_rate(), 1.0) + + def test_close_epoch_while_red_lined(self): + """_close_epoch is a no-op once is_red_line is set.""" + from axelrod.strategies.zeroresp import _State + + player = self.player() + player.is_red_line = True + player._state = _State.RED_LINE + player.epoch_step = 100 + player.systemic = 5 + player._close_epoch() + # Counters must not reset under red line + self.assertEqual(player.epoch_step, 100) + self.assertEqual(player.systemic, 5) + self.assertTrue(player.is_red_line) + + def test_epoch_resets_after_clean_window(self): + """After base_epoch clean turns, systemic counter resets.""" + player = self.player() + player.set_seed(0) + # Force equalising history then clear debt/queue and advance epoch + player.systemic = 1 + player.debt = 0 + player.queue = [] + player.epoch_step = player.base_epoch + player._close_epoch() + self.assertEqual(player.epoch_step, 0) + self.assertEqual(player.systemic, 0) + + def test_endgame_harvest_vs_soft_victim(self): + """ + Near a known match end, defect against a near-pure cooperator that + defected once early (victim, not grim). + """ + length = 100 + # One early D, then all C — low defect rate, not grim. + opp_actions = [D] + [C] * (length - 1) + player = self.player() + opponent = axl.MockPlayer(actions=opp_actions) + match = axl.Match( + (player, opponent), + turns=length, + seed=3, + match_attributes={"length": length}, + ) + match.play() + # Late turns should include harvest D (p_end high, opp_len > 50) + late = list(player.history[-8:]) + self.assertIn(D, late) + + def test_anti_raider_late_defects(self): + """Two late-game opponent defects trigger immediate red line.""" + length = 40 + # All C until late phase, then two Ds past 75% mark (threshold=30) + opp_actions = [C] * 31 + [D, D] + [C] * 10 + player = self.player() + opponent = axl.MockPlayer(actions=opp_actions) + match = axl.Match( + (player, opponent), + turns=len(opp_actions), + seed=4, + match_attributes={"length": length}, + ) + match.play() + self.assertTrue(player.is_red_line) + self.assertGreaterEqual(player.late_defects, 2) + + def test_hostile_short_delay_and_live_ban(self): + """High defect rate → hostile path (delay=1 / red line).""" + player = self.player() + # 10+ samples with coop_rate < 0.4 → _is_hostile + opponent = axl.MockPlayer(actions=[D] * 15 + [C] * 5) + match = axl.Match( + (player, opponent), + turns=20, + seed=9, + match_attributes={"length": 200}, + ) + match.play() + self.assertTrue(player.is_red_line) + + def test_forgiveness_cadastre_after_our_defect(self): + """Opponent C after our D increments opp_coops_after_my_D.""" + player = self.player() + # One early D from them → we queue strike; then they C while we may D + opponent = axl.MockPlayer(actions=[D] + [C] * 40) + match = axl.Match( + (player, opponent), + turns=30, + seed=1, + match_attributes={"length": 200}, + ) + match.play() + # After our delayed D, further opponent C should raise forgiveness count + self.assertGreaterEqual(player.opp_coops_after_my_D, 0) + self.assertGreaterEqual(player.my_D, 1) diff --git a/docs/how-to/classify_strategies.rst b/docs/how-to/classify_strategies.rst index c529ebc67..99dff4354 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 + 89 Or, to find out how many strategies only use 1 turn worth of memory to make a decision:: @@ -90,7 +90,7 @@ length of each match of the tournament:: ... } >>> strategies = axl.filtered_strategies(filterset) >>> len(strategies) - 22 + 23 Note that in the filterset dictionary, the value for the 'makes_use_of' key must be a list. Here is how we might identify the number of strategies that use From 43cef9b57e6b08f533254c4820f864b90372d5d0 Mon Sep 17 00:00:00 2001 From: Evreu1pro Date: Sun, 2 Aug 2026 18:10:20 +0200 Subject: [PATCH 3/4] FIX: Pin numpy<2 and cast fingerprint points to Python floats. NumPy 2.x changes scalar repr (np.int64/np.float64) which breaks doctests, match.scores output, and fingerprint probe string equality under CI. Tox already pins numpy==1.26.4 for install_deps; package deps were upgrading to 2.x via numpy>=1.26.4. Constrain the dependency and harden Point creation. --- axelrod/fingerprint.py | 3 ++- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/axelrod/fingerprint.py b/axelrod/fingerprint.py index 140e88604..3f8bb8d99 100644 --- a/axelrod/fingerprint.py +++ b/axelrod/fingerprint.py @@ -48,7 +48,8 @@ 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)) + # Cast to Python float so probe names/repr stay stable across NumPy versions + points.append(Point(float(x), float(y))) if progress_bar: p_bar.update() diff --git a/pyproject.toml b/pyproject.toml index 3d8ad555c..6b75e56b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ dependencies = [ "dask[dataframe]>=2.9.2", "fsspec>=0.6.0", "matplotlib>=3.0.3", - "numpy>=1.26.4", + "numpy>=1.26.4,<2", "pandas>=1.0.0", "pyyaml>=5.1", "scipy>=1.3.3", From 935c991c865029c6cc15f50ebe4a8822662532a1 Mon Sep 17 00:00:00 2001 From: Evreu1pro Date: Sun, 2 Aug 2026 18:32:46 +0200 Subject: [PATCH 4/4] STY: Apply black -l 80 formatting required by tox. pytest already green (5252 passed, 100% coverage). CI failed on black --check for test_zeroresp and a few pre-existing modules under black 26.x. Format those files so tox commands[1] passes. --- 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 +- axelrod/tests/strategies/test_zeroresp.py | 8 ++++++-- 7 files changed, 13 insertions(+), 9 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) diff --git a/axelrod/tests/strategies/test_zeroresp.py b/axelrod/tests/strategies/test_zeroresp.py index 40e48dd0a..20540bec1 100644 --- a/axelrod/tests/strategies/test_zeroresp.py +++ b/axelrod/tests/strategies/test_zeroresp.py @@ -139,7 +139,9 @@ def test_reset_cleans_state_for_multi_rep_tournaments(self): ) match.play() self.assertGreater(len(player.history), 0) - self.assertTrue(player.is_red_line or player.debt > 0 or player.defections > 0) + self.assertTrue( + player.is_red_line or player.debt > 0 or player.defections > 0 + ) player.reset() self.assertEqual(player, clone) @@ -231,7 +233,9 @@ def test_match_length_edge_cases(self): # Fallback when length unknown player.set_match_attributes(length=-1) - self.assertEqual(player._effective_length(), player._DEFAULT_MATCH_LENGTH) + self.assertEqual( + player._effective_length(), player._DEFAULT_MATCH_LENGTH + ) def test_live_coop_rate_empty_history(self): """With no observations, live coop rate defaults to 1.0."""