Add JAX PPO opponent to classic GUI
This commit is contained in:
+1
-1
@@ -22,7 +22,7 @@ dependencies = [
|
||||
|
||||
[project.optional-dependencies]
|
||||
gui = [
|
||||
"pygame>=2.6.1",
|
||||
"pygame-ce>=2.5.3",
|
||||
"pygame-gui>=0.6.14",
|
||||
]
|
||||
wandb = [
|
||||
|
||||
@@ -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"]
|
||||
@@ -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,7 +625,16 @@ class LostCitiesGuiApp:
|
||||
for element in self.ui_elements:
|
||||
element.kill()
|
||||
self.ui_elements = []
|
||||
|
||||
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,
|
||||
@@ -634,8 +654,9 @@ class LostCitiesGuiApp:
|
||||
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,6 +712,7 @@ 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)
|
||||
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)
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user