diff --git a/pyproject.toml b/pyproject.toml index 13ba2fe..8b90bb4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ [project.optional-dependencies] gui = [ - "pygame>=2.6.1", + "pygame-ce>=2.5.3", "pygame-gui>=0.6.14", ] wandb = [ diff --git a/src/coolrl_lost_cities/games/classic/jax_ppo_policy.py b/src/coolrl_lost_cities/games/classic/jax_ppo_policy.py new file mode 100644 index 0000000..83283b6 --- /dev/null +++ b/src/coolrl_lost_cities/games/classic/jax_ppo_policy.py @@ -0,0 +1,179 @@ +"""Adapter from the classic two-phase GUI to a JAX PPO checkpoint.""" + +from __future__ import annotations + +import logging +from pathlib import Path +from typing import Any + +import jax.numpy as jnp +import numpy as np + +from lost_cities_jax.human_play import ( + evaluate_agent_policy, + infer_config_path, + load_agent, +) +from lost_cities_jax.ppo import load_config +from lost_cities_jax.types import ( + LOC_DECK, + LOC_DISCARD, + LOC_P0_BOARD, + LOC_P0_HAND, + MAX_PILE_SIZE, + N_CARDS, + N_COLORS, + NO_CARD, + State, +) + +from .game import Card, GameState +from .snapshots import Snapshot, snapshot_from_state + + +def _jax_card_id(card: Card, handshake_copy: int = 0) -> int: + slot = handshake_copy if card.is_handshake else card.numeric_value(2) + 1 + return card.color * 12 + slot + + +def snapshot_to_jax_state(snapshot: Snapshot) -> tuple[State, list[int]]: + """Convert a classic card-phase snapshot into the equivalent JAX state. + + The returned list maps each JAX sorted hand slot back to the classic hand + slot. Handshake copies are interchangeable to both rules engines and the + policy observation, so they are assigned stable per-color identities here. + """ + + if ( + snapshot.config.n_colors != N_COLORS + or snapshot.config.n_ranks != 9 + or snapshot.config.n_handshakes != 3 + or snapshot.config.hand_size != 8 + or snapshot.config.min_rank != 2 + ): + raise ValueError("JAX PPO checkpoints require the classic 5-color, 60-card rules") + if snapshot.phase != "card": + raise ValueError("JAX PPO policy conversion must start during the card phase") + + handshake_next = [0] * N_COLORS + + def allocate(card: Card) -> int: + if not card.is_handshake: + return _jax_card_id(card) + copy = handshake_next[card.color] + if copy >= 3: + raise ValueError(f"too many handshake copies for color {card.color}") + handshake_next[card.color] += 1 + return _jax_card_id(card, copy) + + card_loc = np.full(N_CARDS, LOC_DECK, dtype=np.int8) + zone_ids: list[int] = [] + hand_pairs: list[tuple[int, int]] = [] + + deck_ids = [allocate(card) for card in snapshot.deck] + zone_ids.extend(deck_ids) + for player, hand in enumerate(snapshot.hands): + for classic_slot, card in enumerate(hand): + card_id = allocate(card) + card_loc[card_id] = LOC_P0_HAND + player + zone_ids.append(card_id) + if player == snapshot.current_player: + hand_pairs.append((card_id, classic_slot)) + + col_top = np.zeros((2, N_COLORS), dtype=np.int8) + col_hs = np.zeros((2, N_COLORS), dtype=np.int8) + col_len = np.zeros((2, N_COLORS), dtype=np.int8) + for player, expeditions in enumerate(snapshot.expeditions): + for color, expedition in enumerate(expeditions): + for card in expedition: + card_id = allocate(card) + card_loc[card_id] = LOC_P0_BOARD + player + zone_ids.append(card_id) + col_len[player, color] += 1 + if card.is_handshake: + col_hs[player, color] += 1 + else: + col_top[player, color] = card.numeric_value(2) + + pile = np.full((N_COLORS, MAX_PILE_SIZE), NO_CARD, dtype=np.int8) + pile_len = np.zeros(N_COLORS, dtype=np.int8) + for color, discard in enumerate(snapshot.discards): + for index, card in enumerate(discard): + card_id = allocate(card) + card_loc[card_id] = LOC_DISCARD + pile[color, index] = card_id + pile_len[color] += 1 + zone_ids.append(card_id) + + if len(zone_ids) != N_CARDS or len(set(zone_ids)) != N_CARDS: + raise ValueError("classic snapshot does not contain one complete 60-card deck") + + # Classic draws from the end of its deck list. JAX draws forward from + # draw_ptr, so its remaining suffix is the reversed classic deck. + remaining = list(reversed(deck_ids)) + deck_id_set = set(deck_ids) + used = [card_id for card_id in range(N_CARDS) if card_id not in deck_id_set] + deck_order = np.asarray([*used, *remaining], dtype=np.int8) + draw_ptr = len(used) + hand_slot_map = [slot for _, slot in sorted(hand_pairs)] + state = State( + deck_order=jnp.asarray(deck_order), + draw_ptr=jnp.asarray(draw_ptr, dtype=jnp.int32), + card_loc=jnp.asarray(card_loc), + hand_public=jnp.zeros((N_CARDS,), dtype=jnp.bool_), + col_top=jnp.asarray(col_top), + col_hs=jnp.asarray(col_hs), + col_len=jnp.asarray(col_len), + pile=jnp.asarray(pile), + pile_len=jnp.asarray(pile_len), + to_move=jnp.asarray(snapshot.current_player, dtype=jnp.int8), + just_discarded=jnp.asarray(NO_CARD, dtype=jnp.int8), + step_count=jnp.asarray(snapshot.turn_count, dtype=jnp.int32), + done=jnp.asarray(snapshot.terminal), + ) + return state, hand_slot_map + + +class JaxPPOPolicy: + """Expose an atomic-action JAX policy through the classic policy protocol.""" + + def __init__(self, checkpoint: str | Path, *, config: str | Path | None = None): + # The classic GUI enables root DEBUG logging for its own interaction + # trace. Keep JAX/Orbax internals from flooding the launcher terminal. + for logger_name in ("jax", "absl", "orbax"): + logging.getLogger(logger_name).setLevel(logging.WARNING) + cfg_path = infer_config_path(checkpoint) if config is None else Path(config) + self.cfg = load_config(cfg_path) + self.params, self.model = load_agent(self.cfg, checkpoint) + self.checkpoint = Path(checkpoint) + self.pending_draw: int | None = None + self.last_evaluation: Any | None = None + + def act(self, obs_or_state: Any) -> int: + if not isinstance(obs_or_state, GameState): + raise TypeError("JAX PPO GUI policy requires a GameState") + if obs_or_state.phase == "draw": + if self.pending_draw is None: + raise RuntimeError("JAX PPO draw requested without a pending atomic action") + draw = self.pending_draw + self.pending_draw = None + return draw + + snapshot = snapshot_from_state(obs_or_state) + jax_state, hand_slot_map = snapshot_to_jax_state(snapshot) + result = evaluate_agent_policy( + self.cfg, + self.params, + self.model, + jax_state, + snapshot.current_player, + ) + self.last_evaluation = result + jax_hand_slot = result.action // 12 + remainder = result.action % 12 + place = remainder // 6 + self.pending_draw = remainder % 6 + return 2 * hand_slot_map[jax_hand_slot] + place + + +__all__ = ["JaxPPOPolicy", "snapshot_to_jax_state"] diff --git a/src/coolrl_lost_cities/games/classic/pygame_pvp.py b/src/coolrl_lost_cities/games/classic/pygame_pvp.py index 86240e3..c5c95e6 100644 --- a/src/coolrl_lost_cities/games/classic/pygame_pvp.py +++ b/src/coolrl_lost_cities/games/classic/pygame_pvp.py @@ -158,6 +158,8 @@ def build_argparser() -> argparse.ArgumentParser: parser.add_argument("--width", type=int, default=1536) parser.add_argument("--height", type=int, default=964) parser.add_argument("--screenshot-on-start", action="store_true") + parser.add_argument("--jax-checkpoint", help="Play PVC against a JAX PPO checkpoint") + parser.add_argument("--jax-config", help="Optional JAX PPO config (normally inferred)") return parser @@ -171,6 +173,8 @@ class LostCitiesGuiApp: width: int = 1536, height: int = 964, screenshot_on_start: bool = False, + jax_checkpoint: str | None = None, + jax_config: str | None = None, ): import pygame import pygame_gui @@ -198,6 +202,8 @@ class LostCitiesGuiApp: self.computer_player = 1 self.next_computer_action_at_ms = 0 self.config = classic_config() + self.jax_checkpoint = jax_checkpoint + self.jax_config = jax_config self.computer_bot, self.computer_bot_label = self._build_computer_bot() self.state = GameState.new_game(self.config, seed=self.seed) self.history: list[GameState] = [] @@ -240,6 +246,11 @@ class LostCitiesGuiApp: return self.computer_bot_label def _build_computer_bot(self) -> tuple[LostCitiesPolicy, str]: + if self.jax_checkpoint: + from .jax_ppo_policy import JaxPPOPolicy + + policy = JaxPPOPolicy(self.jax_checkpoint, config=self.jax_config) + return policy, f"jax-ppo:{Path(self.jax_checkpoint).name}" return build_bot(self.bot_name, seed=self._bot_seed()), self.bot_name def snapshot(self) -> Snapshot: @@ -614,28 +625,38 @@ class LostCitiesGuiApp: for element in self.ui_elements: element.kill() self.ui_elements = [] - - self.mode_dropdown = pygame_gui.elements.UIDropDownMenu( - options_list=["pvp", "pvc"], - starting_option=self.mode, - relative_rect=pygame.Rect(92, 17, 108, 46), - manager=self.manager, - ) - self.bot_dropdown = pygame_gui.elements.UIDropDownMenu( - options_list=available_bot_names(), - starting_option=self.bot_name, - relative_rect=pygame.Rect(288, 17, 138, 46), - manager=self.manager, - ) - if self.mode != "pvc": - self.bot_dropdown.disable() - game_label = pygame_gui.elements.UILabel( - relative_rect=pygame.Rect(448, 17, 190, 46), - text="CLASSIC / PYTHON", - manager=self.manager, - ) + self.mode_dropdown = None + self.bot_dropdown = None + if self.jax_checkpoint: + game_label = pygame_gui.elements.UILabel( + relative_rect=pygame.Rect(30, 17, 390, 46), + text="COOLRL LOST CITIES / FINAL JAX PPO", + manager=self.manager, + ) + new_game_x = 448 + else: + self.mode_dropdown = pygame_gui.elements.UIDropDownMenu( + options_list=["pvp", "pvc"], + starting_option=self.mode, + relative_rect=pygame.Rect(92, 17, 108, 46), + manager=self.manager, + ) + self.bot_dropdown = pygame_gui.elements.UIDropDownMenu( + options_list=available_bot_names(), + starting_option=self.bot_name, + relative_rect=pygame.Rect(288, 17, 138, 46), + manager=self.manager, + ) + if self.mode != "pvc": + self.bot_dropdown.disable() + game_label = pygame_gui.elements.UILabel( + relative_rect=pygame.Rect(448, 17, 190, 46), + text="CLASSIC / PYTHON", + manager=self.manager, + ) + new_game_x = 660 self.new_game_button = pygame_gui.elements.UIButton( - relative_rect=pygame.Rect(660, 17, 148, 46), + relative_rect=pygame.Rect(new_game_x, 17, 148, 46), text="NEW GAME", manager=self.manager, ) @@ -654,14 +675,16 @@ class LostCitiesGuiApp: if not self.snapshot().terminal: self.export_button.disable() self.ui_elements.extend( - [ + element + for element in ( self.mode_dropdown, self.bot_dropdown, game_label, self.new_game_button, self.undo_button, self.export_button, - ] + ) + if element is not None ) def draw(self) -> None: @@ -689,9 +712,10 @@ class LostCitiesGuiApp: pygame.draw.line(self.screen, LINE, (0, 0), (width, 0), 1) pygame.draw.line(self.screen, LINE, (0, 90), (width, 90), 1) pygame.draw.line(self.screen, LINE, (1080, 0), (1080, 90), 1) - self._draw_text("MODE", (31, 31), MUTED, 18) - self._draw_text("BOT", (220, 31), MUTED, 18) - self._draw_text("GAME", (448, 31), MUTED, 18) + if not self.jax_checkpoint: + self._draw_text("MODE", (31, 31), MUTED, 18) + self._draw_text("BOT", (220, 31), MUTED, 18) + self._draw_text("GAME", (448, 31), MUTED, 18) if snapshot.terminal: diff = snapshot.score_diff(0) if diff > 0: @@ -1283,6 +1307,8 @@ def main(argv: list[str] | None = None) -> None: width=args.width, height=args.height, screenshot_on_start=args.screenshot_on_start, + jax_checkpoint=args.jax_checkpoint, + jax_config=args.jax_config, ) app.run() diff --git a/tests/games/classic/test_jax_ppo_policy.py b/tests/games/classic/test_jax_ppo_policy.py new file mode 100644 index 0000000..264671b --- /dev/null +++ b/tests/games/classic/test_jax_ppo_policy.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +import numpy as np +from coolrl_lost_cities.games.classic.game import GameState, classic_config + +from coolrl_lost_cities.games.classic.jax_ppo_policy import ( + JaxPPOPolicy, + snapshot_to_jax_state, +) +from coolrl_lost_cities.games.classic.snapshots import snapshot_from_state +from lost_cities_jax.engine import board_score, current_hand_sorted, legal_action_mask +from lost_cities_jax.human_play import PolicyEval + + +def test_snapshot_to_jax_state_preserves_initial_public_state() -> None: + state = GameState.new_game(classic_config(), seed=7) + snapshot = snapshot_from_state(state) + + converted, hand_slot_map = snapshot_to_jax_state(snapshot) + + assert int(converted.to_move) == snapshot.current_player + assert int(converted.draw_ptr) == 16 + assert hand_slot_map == sorted( + hand_slot_map, + key=lambda slot: ( + snapshot.hands[0][slot].color, + snapshot.hands[0][slot].rank, + ), + ) + assert np.asarray(current_hand_sorted(converted)).shape == (8,) + assert np.asarray(legal_action_mask(converted)).any() + assert np.asarray(board_score(converted)).tolist() == [0.0, 0.0] + + +def test_jax_policy_splits_atomic_action_across_classic_phases(monkeypatch) -> None: + state = GameState.new_game(classic_config(), seed=11) + converted, hand_slot_map = snapshot_to_jax_state(snapshot_from_state(state)) + legal = np.flatnonzero(np.asarray(legal_action_mask(converted), dtype=bool)) + atomic_action = int(legal[0]) + expected_slot = hand_slot_map[atomic_action // 12] + expected_place = (atomic_action % 12) // 6 + expected_draw = atomic_action % 6 + + monkeypatch.setattr( + "coolrl_lost_cities.games.classic.jax_ppo_policy.evaluate_agent_policy", + lambda *_args: PolicyEval(action=atomic_action, top3=[], value=0.0), + ) + policy = object.__new__(JaxPPOPolicy) + policy.cfg = object() + policy.params = object() + policy.model = object() + policy.pending_draw = None + policy.last_evaluation = None + + card_action = policy.act(state) + assert card_action == 2 * expected_slot + expected_place + state.apply_action(card_action) + assert state.phase == "draw" + assert policy.act(state) == expected_draw + assert policy.pending_draw is None diff --git a/tests/games/classic/test_pygame_pvp.py b/tests/games/classic/test_pygame_pvp.py index 4bc6e3f..93cd4b7 100644 --- a/tests/games/classic/test_pygame_pvp.py +++ b/tests/games/classic/test_pygame_pvp.py @@ -18,6 +18,8 @@ def test_gui_argparser_accepts_classic_options() -> None: "1024", "--height", "768", + "--jax-checkpoint", + "/tmp/final_candidate", ] ) @@ -26,6 +28,7 @@ def test_gui_argparser_accepts_classic_options() -> None: assert args.seed == 7 assert args.width == 1024 assert args.height == 768 + assert args.jax_checkpoint == "/tmp/final_candidate" def test_gui_argparser_rejects_removed_backend_option() -> None: diff --git a/uv.lock b/uv.lock index 3af1da5..aac24ea 100644 --- a/uv.lock +++ b/uv.lock @@ -280,7 +280,7 @@ dependencies = [ [package.optional-dependencies] gui = [ - { name = "pygame" }, + { name = "pygame-ce" }, { name = "pygame-gui" }, ] wandb = [ @@ -305,7 +305,7 @@ requires-dist = [ { name = "optax", specifier = ">=0.2.8" }, { name = "orbax-checkpoint", specifier = ">=0.12.1" }, { name = "pydantic", specifier = ">=2.7" }, - { name = "pygame", marker = "extra == 'gui'", specifier = ">=2.6.1" }, + { name = "pygame-ce", marker = "extra == 'gui'", specifier = ">=2.5.3" }, { name = "pygame-gui", marker = "extra == 'gui'", specifier = ">=0.6.14" }, { name = "pyyaml", specifier = ">=6.0" }, { name = "torch", specifier = ">=2.3.0" }, @@ -359,37 +359,37 @@ wheels = [ [package.optional-dependencies] cublas = [ - { name = "nvidia-cublas", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cublas" }, ] cudart = [ - { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cuda-runtime" }, ] cufft = [ - { name = "nvidia-cufft", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cufft" }, ] cufile = [ - { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cufile" }, ] cupti = [ - { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cuda-cupti" }, ] curand = [ - { name = "nvidia-curand", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-curand" }, ] cusolver = [ - { name = "nvidia-cusolver", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cusolver" }, ] cusparse = [ - { name = "nvidia-cusparse", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cusparse" }, ] nvjitlink = [ - { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-nvjitlink" }, ] nvrtc = [ - { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cuda-nvrtc" }, ] nvtx = [ - { name = "nvidia-nvtx", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-nvtx" }, ] [[package]] @@ -1658,35 +1658,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, ] -[[package]] -name = "pygame" -version = "2.6.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/49/cc/08bba60f00541f62aaa252ce0cfbd60aebd04616c0b9574f755b583e45ae/pygame-2.6.1.tar.gz", hash = "sha256:56fb02ead529cee00d415c3e007f75e0780c655909aaa8e8bf616ee09c9feb1f", size = 14808125, upload-time = "2024-09-29T13:41:34.698Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/ca/8f367cb9fe734c4f6f6400e045593beea2635cd736158f9fabf58ee14e3c/pygame-2.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:20349195326a5e82a16e351ed93465a7845a7e2a9af55b7bc1b2110ea3e344e1", size = 13113753, upload-time = "2024-09-29T14:26:13.751Z" }, - { url = "https://files.pythonhosted.org/packages/83/47/6edf2f890139616b3219be9cfcc8f0cb8f42eb15efd59597927e390538cb/pygame-2.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f3935459109da4bb0b3901da9904f0a3e52028a3332a355d298b1673a334cf21", size = 12378146, upload-time = "2024-09-29T14:26:22.456Z" }, - { url = "https://files.pythonhosted.org/packages/00/9e/0d8aa8cf93db2d2ee38ebaf1c7b61d0df36ded27eb726221719c150c673d/pygame-2.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c31dbdb5d0217f32764797d21c2752e258e5fb7e895326538d82b5f75a0cd856", size = 13611760, upload-time = "2024-09-29T11:10:47.317Z" }, - { url = "https://files.pythonhosted.org/packages/d7/9e/d06adaa5cc65876bcd7a24f59f67e07f7e4194e6298130024ed3fb22c456/pygame-2.6.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:173badf82fa198e6888017bea40f511cb28e69ecdd5a72b214e81e4dcd66c3b1", size = 14298054, upload-time = "2024-09-29T11:39:53.891Z" }, - { url = "https://files.pythonhosted.org/packages/7a/a1/9ae2852ebd3a7cc7d9ae7ff7919ab983e4a5c1b7a14e840732f23b2b48f6/pygame-2.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce8cc108b92de9b149b344ad2e25eedbe773af0dc41dfb24d1f07f679b558c60", size = 13977107, upload-time = "2024-09-29T11:39:56.831Z" }, - { url = "https://files.pythonhosted.org/packages/31/df/6788fd2e9a864d0496a77670e44a7c012184b7a5382866ab0e60c55c0f28/pygame-2.6.1-cp311-cp311-win32.whl", hash = "sha256:811e7b925146d8149d79193652cbb83e0eca0aae66476b1cb310f0f4226b8b5c", size = 10250863, upload-time = "2024-09-29T11:44:48.199Z" }, - { url = "https://files.pythonhosted.org/packages/d2/55/ca3eb851aeef4f6f2e98a360c201f0d00bd1ba2eb98e2c7850d80aabc526/pygame-2.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:91476902426facd4bb0dad4dc3b2573bc82c95c71b135e0daaea072ed528d299", size = 10622016, upload-time = "2024-09-29T12:17:01.545Z" }, - { url = "https://files.pythonhosted.org/packages/92/16/2c602c332f45ff9526d61f6bd764db5096ff9035433e2172e2d2cadae8db/pygame-2.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4ee7f2771f588c966fa2fa8b829be26698c9b4836f82ede5e4edc1a68594942e", size = 13118279, upload-time = "2024-09-29T14:26:30.427Z" }, - { url = "https://files.pythonhosted.org/packages/cd/53/77ccbc384b251c6e34bfd2e734c638233922449a7844e3c7a11ef91cee39/pygame-2.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c8040ea2ab18c6b255af706ec01355c8a6b08dc48d77fd4ee783f8fc46a843bf", size = 12384524, upload-time = "2024-09-29T14:26:49.996Z" }, - { url = "https://files.pythonhosted.org/packages/06/be/3ed337583f010696c3b3435e89a74fb29d0c74d0931e8f33c0a4246307a9/pygame-2.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47a6938de93fa610accd4969e638c2aebcb29b2fca518a84c3a39d91ab47116", size = 13587123, upload-time = "2024-09-29T11:10:50.072Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ca/b015586a450db59313535662991b34d24c1f0c0dc149cc5f496573900f4e/pygame-2.6.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33006f784e1c7d7e466fcb61d5489da59cc5f7eb098712f792a225df1d4e229d", size = 14275532, upload-time = "2024-09-29T11:39:59.356Z" }, - { url = "https://files.pythonhosted.org/packages/b9/f2/d31e6ad42d657af07be2ffd779190353f759a07b51232b9e1d724f2cda46/pygame-2.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1206125f14cae22c44565c9d333607f1d9f59487b1f1432945dfc809aeaa3e88", size = 13952653, upload-time = "2024-09-29T11:40:01.781Z" }, - { url = "https://files.pythonhosted.org/packages/f3/42/8ea2a6979e6fa971702fece1747e862e2256d4a8558fe0da6364dd946c53/pygame-2.6.1-cp312-cp312-win32.whl", hash = "sha256:84fc4054e25262140d09d39e094f6880d730199710829902f0d8ceae0213379e", size = 10252421, upload-time = "2024-09-29T11:14:26.877Z" }, - { url = "https://files.pythonhosted.org/packages/5f/90/7d766d54bb95939725e9a9361f9c06b0cfbe3fe100aa35400f0a461a278a/pygame-2.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:3a9e7396be0d9633831c3f8d5d82dd63ba373ad65599628294b7a4f8a5a01a65", size = 10624591, upload-time = "2024-09-29T11:52:54.489Z" }, - { url = "https://files.pythonhosted.org/packages/e1/91/718acf3e2a9d08a6ddcc96bd02a6f63c99ee7ba14afeaff2a51c987df0b9/pygame-2.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae6039f3a55d800db80e8010f387557b528d34d534435e0871326804df2a62f2", size = 13090765, upload-time = "2024-09-29T14:27:02.377Z" }, - { url = "https://files.pythonhosted.org/packages/0e/c6/9cb315de851a7682d9c7568a41ea042ee98d668cb8deadc1dafcab6116f0/pygame-2.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2a3a1288e2e9b1e5834e425bedd5ba01a3cd4902b5c2bff8ed4a740ccfe98171", size = 12381704, upload-time = "2024-09-29T14:27:10.228Z" }, - { url = "https://files.pythonhosted.org/packages/9f/8f/617a1196e31ae3b46be6949fbaa95b8c93ce15e0544266198c2266cc1b4d/pygame-2.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27eb17e3dc9640e4b4683074f1890e2e879827447770470c2aba9f125f74510b", size = 13581091, upload-time = "2024-09-29T11:30:27.653Z" }, - { url = "https://files.pythonhosted.org/packages/3b/87/2851a564e40a2dad353f1c6e143465d445dab18a95281f9ea458b94f3608/pygame-2.6.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c1623180e70a03c4a734deb9bac50fc9c82942ae84a3a220779062128e75f3b", size = 14273844, upload-time = "2024-09-29T11:40:04.138Z" }, - { url = "https://files.pythonhosted.org/packages/85/b5/aa23aa2e70bcba42c989c02e7228273c30f3b44b9b264abb93eaeff43ad7/pygame-2.6.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef07c0103d79492c21fced9ad68c11c32efa6801ca1920ebfd0f15fb46c78b1c", size = 13951197, upload-time = "2024-09-29T11:40:06.785Z" }, - { url = "https://files.pythonhosted.org/packages/a6/06/29e939b34d3f1354738c7d201c51c250ad7abefefaf6f8332d962ff67c4b/pygame-2.6.1-cp313-cp313-win32.whl", hash = "sha256:3acd8c009317190c2bfd81db681ecef47d5eb108c2151d09596d9c7ea9df5c0e", size = 10249309, upload-time = "2024-09-29T11:10:23.329Z" }, - { url = "https://files.pythonhosted.org/packages/7e/11/17f7f319ca91824b86557e9303e3b7a71991ef17fd45286bf47d7f0a38e6/pygame-2.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:813af4fba5d0b2cb8e58f5d95f7910295c34067dcc290d34f1be59c48bd1ea6a", size = 10620084, upload-time = "2024-09-29T11:48:51.587Z" }, -] - [[package]] name = "pygame-ce" version = "2.5.7" @@ -1923,7 +1894,7 @@ resolution-markers = [ "python_full_version < '3.12'", ] dependencies = [ - { name = "numpy", marker = "python_full_version < '3.12'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } wheels = [ @@ -1998,7 +1969,7 @@ resolution-markers = [ "python_full_version == '3.12.*'", ] dependencies = [ - { name = "numpy", marker = "python_full_version >= '3.12'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" } wheels = [