From 8c4c009813b0d7e5d894b0c8bcea5278de9b57e6 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Wed, 13 Nov 2024 17:24:59 -0600 Subject: [PATCH 01/58] WIP change entire system to component based --- src/azul/client.py | 862 ++++---- src/azul/element_list.py | 140 ++ src/azul/game.py | 1760 ++++++++--------- src/azul/mr_floppy_test.py | 490 +++++ .../Crash_at_Wed-Nov-13-15:30:42-2024.png | Bin 0 -> 4361 bytes .../Crash_at_Wed_Nov_13_16:02:46_2024.png | Bin 0 -> 3888 bytes .../Crash_at_Wed_Nov_13_16:20:10_2024.png | Bin 0 -> 3886 bytes src/azul/server.py | 870 ++++++++ src/azul/sound.py | 67 + src/azul/state.py | 461 +++++ 10 files changed, 3317 insertions(+), 1333 deletions(-) create mode 100644 src/azul/element_list.py create mode 100644 src/azul/mr_floppy_test.py create mode 100644 src/azul/screenshots/Crash_at_Wed-Nov-13-15:30:42-2024.png create mode 100644 src/azul/screenshots/Crash_at_Wed_Nov_13_16:02:46_2024.png create mode 100644 src/azul/screenshots/Crash_at_Wed_Nov_13_16:20:10_2024.png create mode 100755 src/azul/server.py create mode 100644 src/azul/sound.py create mode 100644 src/azul/state.py diff --git a/src/azul/client.py b/src/azul/client.py index 03815e5..15e41e7 100644 --- a/src/azul/client.py +++ b/src/azul/client.py @@ -1,490 +1,556 @@ -"""Azul Client.""" - -from __future__ import annotations - -import contextlib +"""Game Client.""" # Programmed by CoolCat467 -# Hide the pygame prompt -import os -import sys -from os import path -from pathlib import Path -from typing import TYPE_CHECKING, Any, Final - -import trio -from pygame.locals import K_ESCAPE, KEYUP, QUIT, RESIZABLE, WINDOWRESIZED -from pygame.rect import Rect - -from azul import conf, lang, objects, sprite -from azul.component import Component, ComponentManager, Event -from azul.statemachine import AsyncState, AsyncStateMachine -from azul.vector import Vector2 -os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "True" -if os.environ["PYGAME_HIDE_SUPPORT_PROMPT"]: - import pygame -del os +# Copyright (C) 2023-2024 CoolCat467 +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +from __future__ import annotations -if TYPE_CHECKING: - from collections.abc import Iterator, Sequence - -__title__ = "Azul Client" +__title__ = "Game Client" __author__ = "CoolCat467" -__version__ = "2.0.0" - -SCREEN_SIZE = Vector2(800, 600) -FPS = 30 -# FPS = 60 -VSYNC = True -# PORT = server.PORT - -ROOT_FOLDER: Final = Path(__file__).absolute().parent -DATA_FOLDER: Final = ROOT_FOLDER / "data" -FONT_FOLDER: Final = ROOT_FOLDER / "fonts" - -FONT = FONT_FOLDER / "RuneScape-UF-Regular.ttf" +__license__ = "GNU General Public License Version 3" +__version__ = "0.0.0" +import struct +import time +import traceback +from typing import TYPE_CHECKING -class AzulClient(sprite.GroupProcessor, AsyncStateMachine): - """Gear Runner and Layered Dirty Sprite group handler.""" +import trio - def __init__(self) -> None: - """Initialize azul client.""" - sprite.GroupProcessor.__init__(self) - AsyncStateMachine.__init__(self) +from azul import network +from azul.base_io import StructFormat +from azul.buffer import Buffer +from azul.component import Event +from azul.encrypted_event import EncryptedNetworkEventComponent +from azul.encryption import ( + deserialize_public_key, + encrypt_token_and_secret, + generate_shared_secret, +) +from azul.network_shared import ( + ADVERTISEMENT_IP, + ADVERTISEMENT_PORT, + ClientBoundEvents, + ServerBoundEvents, +) - self.add_states( - ( - HaltState(), - AzulInitialize(), - ), +if TYPE_CHECKING: + from mypy_extensions import u8 + + +async def read_advertisements( + timeout: int = 3, # noqa: ASYNC109 +) -> list[tuple[str, tuple[str, int]]]: + """Read server advertisements from network. Return tuples of (motd, (host, port)).""" + # Look up multicast group address in name server and find out IP version + addrinfo = (await trio.socket.getaddrinfo(ADVERTISEMENT_IP, None))[0] + + with trio.socket.socket( + family=trio.socket.AF_INET, # IPv4 + type=trio.socket.SOCK_DGRAM, # UDP + proto=trio.socket.IPPROTO_UDP, + ) as udp_socket: + # SO_REUSEADDR: allows binding to port potentially already in use + # Allow multiple copies of this program on one machine + # (not strictly needed) + udp_socket.setsockopt( + trio.socket.SOL_SOCKET, + trio.socket.SO_REUSEADDR, + 1, ) - @property - def running(self) -> bool: - """Boolean of if state machine is running.""" - return self.active_state is not None + await udp_socket.bind(("", ADVERTISEMENT_PORT)) + + # # Tell the kernel that we are a multicast socket + # udp_socket.setsockopt(trio.socket.IPPROTO_IP, trio.socket.IP_MULTICAST_TTL, 255) + + # socket.IPPROTO_IP works on Linux and Windows + # # IP_MULTICAST_IF: force sending network traffic over specific network adapter + # IP_ADD_MEMBERSHIP: join multicast group + # udp_socket.setsockopt( + # trio.socket.IPPROTO_IP, trio.socket.IP_MULTICAST_IF, + # trio.socket.inet_aton(network_adapter) + # ) + # udp_socket.setsockopt( + # trio.socket.IPPROTO_IP, + # trio.socket.IP_ADD_MEMBERSHIP, + # struct.pack( + # "4s4s", + # trio.socket.inet_aton(group), + # trio.socket.inet_aton(network_adapter), + # ), + # ) + group_bin = trio.socket.inet_pton(addrinfo[0], addrinfo[4][0]) + # Join group + if addrinfo[0] == trio.socket.AF_INET: # IPv4 + mreq = group_bin + struct.pack("=I", trio.socket.INADDR_ANY) + udp_socket.setsockopt( + trio.socket.IPPROTO_IP, + trio.socket.IP_ADD_MEMBERSHIP, + mreq, + ) + else: # IPv6 + mreq = group_bin + struct.pack("@I", 0) + udp_socket.setsockopt( + trio.socket.IPPROTO_IPV6, + trio.socket.IPV6_JOIN_GROUP, + mreq, + ) + + host = "" + buffer = b"" + with trio.move_on_after(timeout): + buffer, address = await udp_socket.recvfrom(512) + host, _port = address + # print(f"{buffer = }") + # print(f"{address = }") + + response: list[tuple[str, tuple[str, int]]] = [] + + start = 0 + for _ in range(1024): + ad_start = buffer.find(b"[AD]", start) + if ad_start == -1: + break + ad_end = buffer.find(b"[/AD]", ad_start) + if ad_end == -1: + break + start_block = buffer.find(b"[AZUL]", ad_end) + if start_block == -1: + break + start_end = buffer.find(b"[/AZUL]", start_block) + if start_end == -1: + break + + start = start_end + + motd = buffer[start_block + 10 : start_end].decode("utf-8") + raw_port = buffer[ad_start + 4 : ad_end].decode("utf-8") + try: + port = int(raw_port) + except ValueError: + continue + response.append((motd, (host, port))) + return response - async def raise_event(self, event: Event[Any]) -> None: - """Raise component event in all groups.""" - if self.active_state is None: - return - manager = getattr(self.active_state, "manager", None) - assert isinstance(manager, ComponentManager | None) - if manager is None: - return - await manager.raise_event(event) +class GameClient(EncryptedNetworkEventComponent): + """Game Client Network Event Component. -class AzulState(AsyncState[AzulClient]): - """Azul Client Asynchronous base class.""" + This class handles connecting to the game server, transmitting events + to the server, and reading and raising incoming events from the server. + """ - __slots__ = ("id", "manager") + __slots__ = ("connect_event_lock", "running") def __init__(self, name: str) -> None: - """Initialize azul state.""" + """Initialize GameClient.""" super().__init__(name) - self.id: int = 0 - self.manager = ComponentManager(self.name) - - -class HaltState(AzulState): - """Halt state to set state to None so running becomes False.""" - - def __init__(self) -> None: - """Initialize halt state.""" - super().__init__("Halt") - - async def check_conditions(self) -> None: - """Set active state to None.""" - await self.machine.set_state(None) - - -class ClickDestinationComponent(Component): - """Component that will use targeting to go to wherever you click on the screen.""" + # Five seconds until timeout is generous, but it gives server end wiggle + # room. + self.timeout = 5 - __slots__ = ("selected",) - outline = pygame.color.Color(255, 220, 0) - - def __init__(self) -> None: - """Initialize click destination component.""" - super().__init__("click_dest") + sbe = ServerBoundEvents + self.register_network_write_events( + { + "select_piece->server": sbe.select_piece, + "select_tile->server": sbe.select_tile, + "encryption_response->server": sbe.encryption_response, + }, + ) + cbe = ClientBoundEvents + self.register_read_network_events( + { + cbe.callback_ping: "server->callback_ping", + cbe.create_piece: "server->create_piece", + cbe.select_piece: "server->select_piece", + cbe.create_tile: "server->create_tile", + cbe.delete_tile: "server->delete_tile", + cbe.delete_piece_animation: "server->delete_piece_animation", + cbe.update_piece_animation: "server->update_piece_animation", + cbe.move_piece_animation: "server->move_piece_animation", + cbe.animation_state: "server->animation_state", + cbe.game_over: "server->game_over", + cbe.action_complete: "server->action_complete", + cbe.initial_config: "server->initial_config", + cbe.playing_as: "server->playing_as", + cbe.encryption_request: "server->encryption_request", + }, + ) - self.selected = False + self.connect_event_lock = trio.Lock() + self.running = False def bind_handlers(self) -> None: - """Register PygameMouseButtonDown and tick handlers.""" + """Register event handlers.""" + super().bind_handlers() self.register_handlers( { - "click": self.click, - "drag": self.drag, - "PygameMouseButtonDown": self.mouse_down, - "tick": self.move_towards_dest, - "init": self.cache_outline, - "test": self.test, + "server->callback_ping": self.read_callback_ping, + "gameboard_piece_clicked": self.write_piece_click, + "gameboard_tile_clicked": self.write_tile_click, + "server->create_piece": self.read_create_piece, + "server->select_piece": self.read_select_piece, + "server->create_tile": self.read_create_tile, + "server->delete_tile": self.read_delete_tile, + "server->delete_piece_animation": self.read_delete_piece_animation, + "server->update_piece_animation": self.read_update_piece_animation, + "server->move_piece_animation": self.read_move_piece_animation, + "server->animation_state": self.read_animation_state, + "server->game_over": self.read_game_over, + "server->action_complete": self.read_action_complete, + "server->initial_config": self.read_initial_config, + "server->playing_as": self.read_playing_as, + "server->encryption_request": self.read_encryption_request, + "network_stop": self.handle_network_stop, + "client_connect": self.handle_client_connect, + # f"client[{self.name}]_read_event": self.handle_read_event, }, ) - async def test(self, event: Event[object]) -> None: - """Print out event data.""" - print(f"{event = }") - - async def cache_outline(self, _: Event[None]) -> None: - """Precalculate outlined images.""" - image: sprite.ImageComponent = self.get_component("image") - outline: sprite.OutlineComponent = image.get_component("outline") - outline.precalculate_all_outlined(self.outline) - - async def update_selected(self) -> None: - """Update selected.""" - image: sprite.ImageComponent = self.get_component("image") - outline: sprite.OutlineComponent = image.get_component("outline") - - color = (None, self.outline)[int(self.selected)] - outline.set_color(color) - - if not self.selected: - movement: sprite.MovementComponent = self.get_component("movement") - movement.speed = 0 - - async def click( - self, - event: Event[sprite.PygameMouseButtonEventData], - ) -> None: - """Toggle selected.""" - if event.data["button"] == 1: - self.selected = not self.selected - - await self.update_selected() - - async def drag(self, event: Event[None]) -> None: - """Drag sprite.""" - if not self.selected: - self.selected = True - await self.update_selected() - movement: sprite.MovementComponent = self.get_component("movement") - movement.speed = 0 - - async def mouse_down( - self, - event: Event[sprite.PygameMouseButtonEventData], - ) -> None: - """Target click pos if selected.""" - if not self.selected: + async def print_callback_ping(self, event: Event[bytearray]) -> None: + """Print received `callback_ping` event from server. + + This event is used as a sort of keepalive heartbeat, because + it stops the connection from timing out. + """ + print(f"print_callback_ping {event = }") + + async def raise_disconnect(self, message: str) -> None: + """Raise client_disconnected event with given message.""" + print(f"{self.__class__.__name__}: {message}") + if not self.manager_exists: + print( + f"{self.__class__.__name__}: Manager does not exist, not raising disconnect event.", + ) + return + await self.raise_event(Event("client_disconnected", message)) + await self.close() + assert self.not_connected + + async def handle_read_event(self) -> None: + """Raise events from server. + + Can raise following exceptions: + RuntimeError - Unhandled packet id + network.NetworkStreamNotConnectedError - Network stream is not connected + OSError - Stopped responding + trio.BrokenResourceError - Something is wrong and stream is broken + + Shouldn't happen with write lock but still: + trio.BusyResourceError - Another task is already writing data + + Handled exceptions: + trio.ClosedResourceError - Stream is closed or another task closes stream + network.NetworkTimeoutError - Timeout + network.NetworkEOFError - Server closed connection + """ + # print(f"{self.__class__.__name__}[{self.name}]: handle_read_event") + if not self.manager_exists: + return + if self.not_connected: + await self.raise_disconnect("Not connected to server.") + return + try: + # print("handle_read_event start") + event = await self.read_event() + except trio.ClosedResourceError: + self.running = False + await self.close() + print(f"[{self.name}] Socket closed from another task.") return - if event.data["button"] == 1: - movement: sprite.MovementComponent = self.get_component("movement") - movement.speed = 200 - target: sprite.TargetingComponent = self.get_component("targeting") - target.destination = Vector2.from_iter(event.data["pos"]) + except network.NetworkTimeoutError as exc: + if self.running: + self.running = False + print(f"[{self.name}] NetworkTimeoutError") + await self.close() + traceback.print_exception(exc) + await self.raise_disconnect( + "Failed to read event from server.", + ) + return + except network.NetworkStreamNotConnectedError as exc: + self.running = False + print(f"[{self.name}] NetworkStreamNotConnectedError") + traceback.print_exception(exc) + await self.close() + assert self.not_connected + raise + except network.NetworkEOFError: + self.running = False + print(f"[{self.name}] NetworkEOFError") + await self.close() + await self.raise_disconnect( + "Server closed connection.", + ) + return + + await self.raise_event(event) - async def move_towards_dest( + async def handle_client_connect( self, - event: Event[sprite.TickEventData], + event: Event[tuple[str, int]], ) -> None: - """Move closer to destination.""" - target: sprite.TargetingComponent = self.get_component("targeting") - await target.move_destination_time(event.data.time_passed) - - -class MrFloppy(sprite.Sprite): - """Mr. Floppy test sprite.""" + """Have client connect to address specified in event.""" + if self.connect_event_lock.locked(): + raise RuntimeError("2nd client connect fired!") + async with self.connect_event_lock: + # Mypy does not understand that self.not_connected becomes + # false after connect call. + if not TYPE_CHECKING and not self.not_connected: + raise RuntimeError("Already connected!") + try: + await self.connect(*event.data) + except OSError as ex: + traceback.print_exception(ex) + else: + self.running = True + while not self.not_connected and self.running: + await self.handle_read_event() + self.running = False + + await self.close() + if self.manager_exists: + await self.raise_event( + Event("client_connection_closed", None), + ) + else: + print( + "manager does not exist, cannot send client connection closed event.", + ) + return + await self.raise_disconnect("Error connecting to server.") + + async def read_callback_ping(self, event: Event[bytearray]) -> None: + """Read callback_ping event from server.""" + ns = int.from_bytes(event.data) + now = int(time.time() * 1e9) + difference = now - ns + + # print(f'{difference / 1e9 = } seconds') + + await self.raise_event( + Event("callback_ping", difference), + ) - __slots__ = () + async def read_create_piece(self, event: Event[bytearray]) -> None: + """Read create_piece event from server.""" + buffer = Buffer(event.data) - def __init__(self) -> None: - """Initialize mr floppy sprite.""" - super().__init__("MrFloppy") + piece_pos = read_position(buffer) + piece_type: u8 = buffer.read_value(StructFormat.UBYTE) - self.add_components( - ( - sprite.MovementComponent(), - sprite.TargetingComponent(), - ClickDestinationComponent(), - sprite.ImageComponent(), - sprite.DragClickEventComponent(), - ), + await self.raise_event( + Event("gameboard_create_piece", (piece_pos, piece_type)), ) - movement = self.get_component("movement") - targeting = self.get_component("targeting") - image = self.get_component("image") + async def read_select_piece(self, event: Event[bytearray]) -> None: + """Read create_piece event from server.""" + buffer = Buffer(event.data) - movement.speed = 200 + piece_pos = read_position(buffer) + outline_value = buffer.read_value(StructFormat.BOOL) - # lintcheck: c-extension-no-member (I1101): Module 'pygame.surface' has no 'Surface' member, but source is unavailable. Consider adding this module to extension-pkg-allow-list if you want to perform analysis based on run-time introspection of living objects. - floppy: pygame.surface.Surface = pygame.image.load( - path.join("data", "mr_floppy.png"), + await self.raise_event( + Event("gameboard_select_piece", (piece_pos, outline_value)), ) - image.add_images( - { - 0: floppy, - # '1': pygame.transform.flip(floppy, False, True) - 1: pygame.transform.rotate(floppy, 270), - 2: pygame.transform.flip(floppy, True, True), - 3: pygame.transform.rotate(floppy, 90), - }, - ) + async def read_create_tile(self, event: Event[bytearray]) -> None: + """Read create_tile event from server.""" + buffer = Buffer(event.data) - anim = image.get_component("animation") - anim.controller = self.controller((0, 1, 2, 3)) + tile_pos = read_position(buffer) - image.set_image(0) - self.visible = True + await self.raise_event(Event("gameboard_create_tile", tile_pos)) - self.location = SCREEN_SIZE / 2 - targeting.destination = self.location + async def read_delete_tile(self, event: Event[bytearray]) -> None: + """Read delete_tile event from server.""" + buffer = Buffer(event.data) - self.register_handler("drag", self.drag) + tile_pos = read_position(buffer) - @staticmethod - def controller( - image_identifiers: Sequence[str | int], - ) -> Iterator[str | int | None]: - """Animation controller.""" - cidx = 0 - while True: - count = len(image_identifiers) - if not count: - yield None - continue - cidx = (cidx + 1) % count - yield image_identifiers[cidx] + await self.raise_event(Event("gameboard_delete_tile", tile_pos)) - async def drag(self, event: Event[sprite.DragEvent]) -> None: - """Move by relative from drag.""" - if event.data.button != 1: + async def write_piece_click(self, event: Event[tuple[Pos, int]]) -> None: + """Write piece click event to server.""" + if self.not_connected: return - self.location += event.data.rel - self.dirty = 1 + piece_position, _piece_type = event.data + buffer = Buffer() + write_position(buffer, piece_position) + # buffer.write_value(StructFormat.UINT, piece_type) -class FPSCounter(objects.Text): - """FPS counter.""" + await self.write_event(Event("select_piece->server", buffer)) - __slots__ = () + async def write_tile_click(self, event: Event[Pos]) -> None: + """Write tile click event to server.""" + if self.not_connected: + return + tile_position = event.data - def __init__(self) -> None: - """Initialize fps counter.""" - font = pygame.font.Font(FONT, 28) - super().__init__("fps", font) + buffer = Buffer() + write_position(buffer, tile_position) - async def on_tick(self, event: Event[sprite.TickEventData]) -> None: - """Update text.""" - # self.text = f'FPS: {event.data["fps"]:.2f}' - self.text = f"FPS: {event.data.fps:.0f}" + await self.write_event(Event("select_tile->server", buffer)) - async def update_loc( + async def read_delete_piece_animation( self, - event: Event[dict[str, tuple[int, int]]], + event: Event[bytearray], ) -> None: - """Move to top left corner.""" - self.location = Vector2.from_iter(event.data["size"]) / 2 + (5, 5) - - def bind_handlers(self) -> None: - """Register event handlers.""" - super().bind_handlers() - self.register_handlers( - { - "tick": self.on_tick, - "sprite_image_resized": self.update_loc, - }, - ) - - -class AzulInitialize(AzulState): - """Initialize Azul.""" - - __slots__ = () - - def __init__(self) -> None: - """Initialize state.""" - super().__init__("initialize") + """Read delete_piece_animation event from server.""" + buffer = Buffer(event.data) - def group_add(self, new_sprite: sprite.Sprite) -> None: - """Add new sprite to group.""" - group = self.machine.get_group(self.id) - assert group is not None, "Expected group from new group id" - group.add(new_sprite) - self.manager.add_component(new_sprite) + tile_pos = read_position(buffer) - async def entry_actions(self) -> None: - """Create group and add mr floppy.""" - self.id = self.machine.new_group("test") - floppy = MrFloppy() - print(floppy) - self.group_add(floppy) - self.group_add(FPSCounter()) - - await self.machine.raise_event(Event("init", None)) + await self.raise_event( + Event("gameboard_delete_piece_animation", tile_pos), + ) - async def exit_actions(self) -> None: - """Remove group and unbind components.""" - self.machine.remove_group(self.id) - self.manager.unbind_components() + async def read_update_piece_animation( + self, + event: Event[bytearray], + ) -> None: + """Read update_piece_animation event from server.""" + buffer = Buffer(event.data) + piece_pos = read_position(buffer) + piece_type: u8 = buffer.read_value(StructFormat.UBYTE) -def save_crash_img() -> None: - """Save the last frame before the game crashed.""" - surface = pygame.display.get_surface().copy() - # strTime = '-'.join(time.asctime().split(' ')) - # filename = f'Crash_at_{strTime}.png' - filename = "screenshot.png" + await self.raise_event( + Event("gameboard_update_piece_animation", (piece_pos, piece_type)), + ) - pygame.image.save(surface, path.join("screenshots", filename)) - del surface + async def read_move_piece_animation(self, event: Event[bytearray]) -> None: + """Read move_piece_animation event from server.""" + buffer = Buffer(event.data) + piece_current_pos = read_position(buffer) + piece_new_pos = read_position(buffer) -async def async_run() -> None: - """Run client.""" - global SCREEN_SIZE - # global client - config = conf.load_config(path.join("conf", "main.conf")) - lang.load_lang(config["Language"]["lang_name"]) + await self.raise_event( + Event( + "gameboard_move_piece_animation", + (piece_current_pos, piece_new_pos), + ), + ) - screen = pygame.display.set_mode( - tuple(SCREEN_SIZE), - RESIZABLE, - vsync=VSYNC, - ) - pygame.display.set_caption(f"{__title__} v{__version__}") - pygame.key.set_repeat(1000, 30) - screen.fill((0xFF, 0xFF, 0xFF)) + async def read_animation_state(self, event: Event[bytearray]) -> None: + """Read animation_state event from server.""" + buffer = Buffer(event.data) - client = AzulClient() + animation_state = buffer.read_value(StructFormat.BOOL) - background = pygame.image.load( - path.join("data", "background.png"), - ).convert() - client.clear(screen, background) + await self.raise_event( + Event("gameboard_animation_state", animation_state), + ) - client.set_timing_threshold(1000 / FPS) + async def read_game_over(self, event: Event[bytearray]) -> None: + """Read update_piece event from server.""" + buffer = Buffer(event.data) - await client.set_state("initialize") + winner: u8 = buffer.read_value(StructFormat.UBYTE) - clock = pygame.time.Clock() + await self.raise_event(Event("game_winner", winner)) + self.running = False - while client.running: - resized_window = False + async def read_action_complete(self, event: Event[bytearray]) -> None: + """Read action_complete event from server. - async with trio.open_nursery() as nursery: - for event in pygame.event.get(): - # pylint: disable=undefined-variable - if event.type == QUIT: - await client.set_state("Halt") - elif event.type == KEYUP and event.key == K_ESCAPE: - pygame.event.post(pygame.event.Event(QUIT)) - elif event.type == WINDOWRESIZED: - SCREEN_SIZE = Vector2(event.x, event.y) - resized_window = True - sprite_event = sprite.convert_pygame_event(event) - # print(sprite_event) - nursery.start_soon(client.raise_event, sprite_event) - await client.think() + Sent when last action from client is done, great for AIs. + As of writing, not used for main client. + """ + buffer = Buffer(event.data) - time_passed = clock.tick(FPS) + from_pos = read_position(buffer) + to_pos = read_position(buffer) + current_turn: u8 = buffer.read_value(StructFormat.UBYTE) - await client.raise_event( - Event( - "tick", - sprite.TickEventData( - time_passed / 1000, - clock.get_fps(), - ), - ), + await self.raise_event( + Event("game_action_complete", (from_pos, to_pos, current_turn)), ) - if resized_window: - screen.fill((0xFF, 0xFF, 0xFF)) - rects = [Rect((0, 0), tuple(SCREEN_SIZE))] - client.repaint_rect(rects[0]) - rects.extend(client.draw(screen)) - else: - rects = client.draw(screen) - pygame.display.update(rects) - client.clear_groups() - + async def read_initial_config(self, event: Event[bytearray]) -> None: + """Read initial_config event from server.""" + buffer = Buffer(event.data) -class Tracer(trio.abc.Instrument): - """Tracer instrument.""" + board_size: u8 = buffer.read_value(StructFormat.UBYTE) + current_turn: u8 = buffer.read_value(StructFormat.UBYTE) - __slots__ = ("_sleep_time",) - - def before_run(self) -> None: - """Before run.""" - print("!!! run started") - - def _print_with_task(self, msg: str, task: trio.lowlevel.Task) -> None: - """Print message with task name.""" - # repr(task) is perhaps more useful than task.name in general, - # but in context of a tutorial the extra noise is unhelpful. - print(f"{msg}: {task.name}") - - def task_spawned(self, task: trio.lowlevel.Task) -> None: - """Task spawned.""" - self._print_with_task("### new task spawned", task) - - def task_scheduled(self, task: trio.lowlevel.Task) -> None: - """Task scheduled.""" - self._print_with_task("### task scheduled", task) - - def before_task_step(self, task: trio.lowlevel.Task) -> None: - """Before task step.""" - self._print_with_task(">>> about to run one step of task", task) + await self.raise_event( + Event("game_initial_config", (board_size, current_turn)), + ) - def after_task_step(self, task: trio.lowlevel.Task) -> None: - """After task step.""" - self._print_with_task("<<< task step finished", task) + async def read_playing_as(self, event: Event[bytearray]) -> None: + """Read playing_as event from server.""" + buffer = Buffer(event.data) - def task_exited(self, task: trio.lowlevel.Task) -> None: - """Task exited.""" - self._print_with_task("### task exited", task) + playing_as: u8 = buffer.read_value(StructFormat.UBYTE) - def before_io_wait(self, timeout: float) -> None: - """Before IO wait.""" - if timeout: - print(f"### waiting for I/O for up to {timeout} seconds") - else: - print("### doing a quick check for I/O") - self._sleep_time = trio.current_time() + await self.raise_event( + Event("game_playing_as", playing_as), + ) - def after_io_wait(self, timeout: float) -> None: - """After IO wait.""" - duration = trio.current_time() - self._sleep_time - print(f"### finished I/O check (took {duration} seconds)") + async def write_encryption_response( + self, + shared_secret: bytes, + verify_token: bytes, + ) -> None: + """Write encryption response to server.""" + buffer = Buffer() + buffer.write_bytearray(shared_secret) + buffer.write_bytearray(verify_token) - def after_run(self) -> None: - """After run.""" - print("!!! run finished") + await self.write_event(Event("encryption_response->server", buffer)) + async def read_encryption_request(self, event: Event[bytearray]) -> None: + """Read and handle encryption request from server.""" + buffer = Buffer(event.data) -def run() -> None: - """Run asynchronous side of everything.""" - trio.run(async_run) # , instruments=[Tracer()]) + serialized_public_key = buffer.read_bytearray() + verify_token = buffer.read_bytearray() + public_key = deserialize_public_key(serialized_public_key) -# save_crash_img() + shared_secret = generate_shared_secret() -if __name__ == "__main__": - print(f"{__title__} v{__version__}\nProgrammed by {__author__}.\n") + encrypted_token, encrypted_secret = encrypt_token_and_secret( + public_key, + verify_token, + shared_secret, + ) - # Make sure the game will display correctly on high DPI monitors on Windows. - if sys.platform == "win32": - # Exists on windows but not on linux or macos - # Windows raises attr-defined - # others say unused-ignore - from ctypes import windll # type: ignore[attr-defined,unused-ignore] + await self.write_encryption_response(encrypted_secret, encrypted_token) - with contextlib.suppress(AttributeError): - windll.user32.SetProcessDPIAware() - del windll + # Start encrypting all future data + self.enable_encryption(shared_secret, verify_token) - try: - pygame.init() - run() - finally: - pygame.quit() + async def handle_network_stop(self, event: Event[None]) -> None: + """Send EOF if connected and close socket.""" + if self.not_connected: + return + self.running = False + try: + await self.send_eof() + finally: + await self.close() + assert self.not_connected + + def __del__(self) -> None: + """Print debug message.""" + print(f"del {self.__class__.__name__}") diff --git a/src/azul/element_list.py b/src/azul/element_list.py new file mode 100644 index 0000000..d461aec --- /dev/null +++ b/src/azul/element_list.py @@ -0,0 +1,140 @@ +"""Element List - List of element sprites.""" + +# Programmed by CoolCat467 + +from __future__ import annotations + +# Element List - List of element sprites. +# Copyright (C) 2024 CoolCat467 +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +__title__ = "Element List" +__author__ = "CoolCat467" +__version__ = "0.0.0" +__license__ = "GNU General Public License Version 3" + + +from typing import TYPE_CHECKING + +from azul import sprite +from azul.vector import Vector2 + +if TYPE_CHECKING: + from collections.abc import Generator + + +class Element(sprite.Sprite): + """Element sprite.""" + + __slots__ = () + + def self_destruct(self) -> None: + """Remove this element.""" + self.kill() + if self.manager_exists: + self.manager.remove_component(self.name) + + def __del__(self) -> None: + """Clean up this element for garbage collecting.""" + self.self_destruct() + super().__del__() + + +class ElementList(sprite.Sprite): + """Element List sprite.""" + + __slots__ = ("_order",) + + def __init__(self, name: object) -> None: + """Initialize connection list.""" + super().__init__(name) + + self._order: list[object] = [] + + def add_element(self, element: Element) -> None: + """Add element to this list.""" + group = self.groups()[-1] + group.add(element) # type: ignore[arg-type] + self.add_component(element) + self._order.append(element.name) + + def delete_element(self, element_name: object) -> None: + """Delete an element (only from component).""" + element = self.get_component(element_name) + index = self._order.index(element_name) + if element.visible: + assert element.image is not None + height = element.image.get_height() + self.offset_elements_after(index, (0, -height)) + self._order.pop(index) + assert isinstance(element, Element) + element.self_destruct() + + def yield_elements(self) -> Generator[Element, None, None]: + """Yield bound Element components in order.""" + for component_name in iter(self._order): + # Kind of strange to mutate in yield, maybe shouldn't do that? + if not self.component_exists(component_name): + self._order.remove(component_name) + continue + component = self.get_component(component_name) + assert isinstance(component, Element) + yield component + + def get_last_rendered_element(self) -> Element | None: + """Return last bound Element sprite or None.""" + for component_name in reversed(self._order): + if not self.component_exists(component_name): + self._order.remove(component_name) + continue + component = self.get_component(component_name) + assert isinstance(component, Element) + if component.visible: + assert component.image is not None + return component + return None + + def get_new_connection_position(self) -> Vector2: + """Return location for new connection.""" + last_element = self.get_last_rendered_element() + if last_element is None: + return Vector2.from_iter(self.rect.topleft) + location = Vector2.from_iter(last_element.rect.topleft) + assert last_element.image is not None + location += (0, last_element.image.get_height()) + return location + + def offset_elements(self, diff: tuple[int, int]) -> None: + """Offset all element locations by given difference.""" + for element in self.yield_elements(): + element.location += diff + + def offset_elements_after(self, index: int, diff: tuple[int, int]) -> None: + """Offset elements after index by given difference.""" + for idx, element in enumerate(self.yield_elements()): + if idx <= index: + continue + element.location += diff + + def _set_location(self, value: tuple[int, int]) -> None: + """Set rect center from tuple of integers.""" + current = self.location + super()._set_location(value) + diff = Vector2.from_iter(value) - current + self.offset_elements(diff) + + +if __name__ == "__main__": + print(f"{__title__} v{__version__}\nProgrammed by {__author__}.\n") diff --git a/src/azul/game.py b/src/azul/game.py index 84da90f..6ecb4c3 100644 --- a/src/azul/game.py +++ b/src/azul/game.py @@ -28,22 +28,27 @@ import operator import os import random +import traceback import time from collections import Counter, deque from functools import lru_cache, wraps from pathlib import Path +import trio from typing import TYPE_CHECKING, Final, NamedTuple, TypeVar, cast import pygame +from pygame.color import Color from numpy import array, int8 from pygame.locals import ( KEYDOWN, KEYUP, + K_ESCAPE, QUIT, RESIZABLE, SRCALPHA, USEREVENT, VIDEORESIZE, + WINDOWRESIZED, ) from pygame.rect import Rect @@ -56,9 +61,22 @@ sort_tiles, ) from azul.vector import Vector2 +from azul import sprite +from azul import objects +from azul.component import Component, Event, ExternalRaiseManager, ComponentManager +from azul import errorbox +from azul.async_clock import Clock +from azul.server import GameServer +from azul.sound import SoundData, play_sound as base_play_sound +from azul import element_list +from azul.client import GameClient, read_advertisements +from azul.statemachine import AsyncState +from azul.network_shared import DEFAULT_PORT, find_ip +from enum import IntEnum, auto +import sys if TYPE_CHECKING: - from collections.abc import Callable, Generator, Iterable, Sequence + from collections.abc import Callable, Generator, Iterable, Sequence, Awaitable from typing_extensions import TypeVarTuple, Unpack @@ -67,7 +85,8 @@ T = TypeVar("T") RT = TypeVar("RT") -SCREENSIZE = (650, 600) +SCREEN_SIZE = (650, 600) +VSYNC = True FPS: Final = 48 @@ -111,7 +130,20 @@ ("&", ORANGE), ("1", BLUE), ) -NUMBERONETILE = 5 +class Tiles(IntEnum): + blank = -6 + fake_cyan = -5 + fake_black = -4 + fake_red = -3 + fake_yellow = -2 + fake_blue = -1 + blue = 0 + yellow = auto() + red = auto() + black = auto() + cyan = auto() + one = auto() + TILESIZE = 15 # Colors @@ -119,15 +151,30 @@ TILEDEFAULT = ORANGE SCORECOLOR = BLACK PATSELECTCOLOR = DARKGREEN -BUTTONTEXTCOLOR = DARKCYAN +BUTTON_TEXT_COLOR = DARKCYAN +BUTTON_TEXT_OUTLINE = BLACK BUTTONBACKCOLOR = WHITE GREYSHIFT = 0.75 # 0.65 # Font -FONT: Final = FONT_FOLDER / "RuneScape-UF-Regular.ttf" +FONT: Final = FONT_FOLDER / "VeraSerif.ttf"#"RuneScape-UF-Regular.ttf" SCOREFONTSIZE = 30 BUTTONFONTSIZE = 60 +SOUND_LOOKUP: Final = { + "delete_piece": "pop.mp3", + "piece_move": "slide.mp3", + "piece_update": "ding.mp3", + "game_won": "newthingget.ogg", + "button_click": "select.mp3", + "tick": "tick.mp3", +} +SOUND_DATA: Final = { + "delete_piece": SoundData( + volume=50, + ), +} + @lru_cache def make_square_surf( @@ -148,6 +195,21 @@ def make_square_surf( return surf +def play_sound( + sound_name: str, +) -> tuple[pygame.mixer.Sound, int | float]: + """Play sound effect.""" + sound_filename = SOUND_LOOKUP.get(sound_name) + if sound_filename is None: + raise RuntimeError(f"Error: Sound with ID `{sound_name}` not found.") + sound_data = SOUND_DATA.get(sound_name, SoundData()) + + return base_play_sound( + DATA_FOLDER / sound_filename, + sound_data, + ) + + def outline_rectangle( surface: pygame.surface.Surface, color: ( @@ -264,6 +326,7 @@ def add_symbol_to_tile_surf( symbolsurf, (width * scale_factor, height * scale_factor), ) + # symbolsurf = pygame.transform.scale(symbolsurf, (tilesize, tilesize)) # sw, sh = symbolsurf.get_size() @@ -287,18 +350,17 @@ def add_symbol_to_tile_surf( @lru_cache def get_tile_image( - tile: Tile, + tile_color: int, tilesize: int, greyshift: float = GREYSHIFT, outline_size: float = 0.2, ) -> pygame.surface.Surface: """Return a surface of a given tile.""" - cid = tile.color - if cid < 5: - color = get_tile_color(cid, greyshift) + if tile_color < 5: + color = get_tile_color(tile_color, greyshift) - elif cid >= 5: - color_data = tile_colors[cid] + elif tile_color >= 5: + color_data = tile_colors[tile_color] assert len(color_data) == 2 color, outline = color_data surf = outline_rectangle( @@ -307,12 +369,12 @@ def get_tile_image( outline_size, ) # Add tile symbol - add_symbol_to_tile_surf(surf, cid, tilesize, greyshift) + add_symbol_to_tile_surf(surf, tile_color, tilesize, greyshift) return surf surf = make_square_surf(color, tilesize) # Add tile symbol - add_symbol_to_tile_surf(surf, cid, tilesize, greyshift) + add_symbol_to_tile_surf(surf, tile_color, tilesize, greyshift) return surf @@ -351,129 +413,6 @@ def get_tile_container_image( return image -class Font: - """Font object, simplify using text.""" - - def __init__( - self, - font_name: str | Path, - fontsize: int = 20, - color: tuple[int, int, int] = (0, 0, 0), - cx: bool = True, - cy: bool = True, - antialias: bool = False, - background: tuple[int, int, int] | None = None, - do_cache: bool = True, - ) -> None: - """Initialize font.""" - self.font = font_name - self.size = int(fontsize) - self.color = color - self.center = [cx, cy] - self.antialias = bool(antialias) - self.background = background - self.do_cache = bool(do_cache) - self.cache: pygame.surface.Surface | None = None - self.last_text: str | None = None - self._change_font() - - def __repr__(self) -> str: - """Return representation of self.""" - return f"{self.__class__.__name__}(%r, %i, %r, %r, %r, %r, %r, %r)" % ( - self.font, - self.size, - self.color, - self.center[0], - self.center[1], - self.antialias, - self.background, - self.do_cache, - ) - - def _change_font(self) -> None: - """Set self.pyfont to a new pygame.font.Font object from data we have.""" - self.pyfont = pygame.font.Font(self.font, self.size) - - def _cache(self, surface: pygame.surface.Surface) -> None: - """Set self.cache to surface.""" - self.cache = surface - - def get_height(self) -> int: - """Return the height of font.""" - return self.pyfont.get_height() - - def render_nosurf( - self, - text: str | None, - size: int | None = None, - color: tuple[int, int, int] | None = None, - background: tuple[int, int, int] | None = None, - force_update: bool = False, - ) -> pygame.surface.Surface: - """Render and return a surface of given text. Use stored data to render, if arguments change internal data and render.""" - update_cache = ( - self.cache is None or force_update or text != self.last_text - ) - # Update internal data if new values given - if size is not None: - self.size = int(size) - self._change_font() - update_cache = True - if color is not None: - self.color = color - update_cache = True - if self.background != background: - self.background = background - update_cache = True - - if self.do_cache: - if update_cache: - self.last_text = text - surf = self.pyfont.render( - text, - self.antialias, - self.color, - self.background, - ).convert_alpha() - self._cache(surf.copy()) - else: - assert self.cache is not None - surf = self.cache - else: - # Render the text using the pygame font - surf = self.pyfont.render( - text, - self.antialias, - self.color, - self.background, - ).convert_alpha() - return surf - - def render( - self, - surface: pygame.surface.Surface, - text: str, - xy: tuple[int, int], - size: int | None = None, - color: tuple[int, int, int] | None = None, - background: tuple[int, int, int] | None = None, - force_update: bool = False, - ) -> None: - """Render given text, use stored data to render, if arguments change internal data and render.""" - surf = self.render_nosurf(text, size, color, background, force_update) - - if True in self.center: - x, y = xy - cx, cy = self.center - w, h = surf.get_size() - if cx: - x -= w // 2 - if cy: - y -= h // 2 - xy = (int(x), int(y)) - - surface.blit(surf, xy) - class ObjectHandler: """ObjectHandler class, meant to be used for other classes.""" @@ -636,122 +575,8 @@ def __del__(self) -> None: self.rm_star() -class Object: - """Object object.""" - - __slots__ = ( - "Render_Priority", - "game", - "hidden", - "id", - "image", - "location", - "location_mode_on_resize", - "name", - "screen_size_last", - "wh", - ) - - def __init__(self, name: str) -> None: - """Set self.name to name, and other values for rendering. - - Defines the following attributes: - self.name - self.image - self.location - self.wh - self.hidden - self.location_mode_on_resize - self.id - """ - self.name = str(name) - self.image: pygame.surface.Surface | None = None - self.location = Vector2( - round(SCREENSIZE[0] / 2), - round(SCREENSIZE[1] / 2), - ) - self.wh = 0, 0 - self.hidden = False - self.location_mode_on_resize = "Scale" - self.screen_size_last = SCREENSIZE - self.id = 0 - self.game: Game - self.Render_Priority: str | int - - def __repr__(self) -> str: - """Return representation of self.""" - return f"{self.__class__.__name__}()" - - def get_image_zreo_no_fix(self) -> tuple[float, float]: - """Return the screen location of the topleft point of self.image.""" - return ( - self.location[0] - self.wh[0] / 2, - self.location[1] - self.wh[1] / 2, - ) - - def get_image_zero(self) -> tuple[int, int]: - """Return the screen location of the topleft point of self.image fixed to integer values.""" - x, y = self.get_image_zreo_no_fix() - return int(x), int(y) - - def get_rect(self) -> Rect: - """Return a Rect object representing this Object's area.""" - return Rect(self.get_image_zero(), self.wh) - - def point_intersects( - self, - screen_location: tuple[int, int] | Vector2, - ) -> bool: - """Return True if this Object intersects with a given screen location.""" - return self.get_rect().collidepoint(tuple(screen_location)) - - def to_image_surface_location( - self, - screen_location: tuple[int, int] | Vector2, - ) -> tuple[int, int]: - """Return the location a screen location would be at on the objects image. Can return invalid data.""" - # Get zero zero in image locations - zx, zy = self.get_image_zero() # Zero x and y - sx, sy = screen_location # Screen x and y - return ( - int(sx) - zx, - int(sy) - zy, - ) # Location with respect to image dimensions - - def process(self, time_passed: float) -> None: - """Process Object. Replace when calling this class.""" - - def render(self, surface: pygame.surface.Surface) -> None: - """Render self.image to surface if self.image is not None. Updates self.wh.""" - if self.image is None or self.hidden: - return - self.wh = self.image.get_size() - x, y = self.get_image_zero() - surface.blit(self.image, (int(x), int(y))) - - # pygame.draw.rect(surface, MAGENTA, self.get_rect(), 1) - - def __del__(self) -> None: - """Delete self.image.""" - del self.image - - def screen_size_update(self) -> None: - """Handle screensize is changes.""" - nx, ny = self.location - - if self.location_mode_on_resize == "Scale": - ow, oh = self.screen_size_last - nw, nh = SCREENSIZE - - x, y = self.location - nx, ny = x * (nw / ow), y * (nh / oh) - - self.location = Vector2(nx, ny) - self.screen_size_last = SCREENSIZE - - -class MultipartObject(Object, ObjectHandler): +class MultipartObject(ObjectHandler): """Thing that is both an Object and an ObjectHandler, and is meant to be an Object made up of multiple Objects.""" def __init__(self, name: str): @@ -759,7 +584,6 @@ def __init__(self, name: str): Also set self._lastloc and self._lasthidden to None """ - Object.__init__(self, name) ObjectHandler.__init__(self) self._lastloc: Vector2 | None = None @@ -810,54 +634,28 @@ def __del__(self) -> None: ObjectHandler.__del__(self) -class Tile(NamedTuple): - """Represents a Tile.""" - - color: int - - -class TileRenderer(Object): +class TileRenderer(sprite.Sprite): """Base class for all objects that need to render tiles.""" - __slots__ = ("back", "image_update", "tile_full", "tile_seperation") + __slots__ = ("background", "tile_seperation") greyshift = GREYSHIFT tile_size = TILESIZE def __init__( self, name: str, - game: Game, tile_seperation: int | None = None, background: tuple[int, int, int] | None = TILEDEFAULT, ) -> None: - """Initialize renderer. Needs a game object for its cache and optional tile separation value and background RGB color. - - Defines the following attributes during initialization and uses throughout: - self.game - self.wh - self.tile_seperation - self.tile_full - self.back - and finally, self.image_update - - The following functions are also defined: - self.clear_image - self.render_tile - self.update_image (but not implemented) - self.process - """ + """Initialize renderer.""" super().__init__(name) - self.game = game if tile_seperation is None: self.tile_seperation = self.tile_size / 3.75 else: self.tile_seperation = tile_seperation - self.tile_full = self.tile_size + self.tile_seperation - self.back = background - - self.image_update = True + self.background = background def get_rect(self) -> Rect: """Return a Rect object representing this row's area.""" @@ -869,17 +667,20 @@ def get_rect(self) -> Rect: return Rect(location, wh) def clear_image(self, tile_dimensions: tuple[int, int]) -> None: - """Reset self.image using tile_dimensions tuple and fills with self.back. Also updates self.wh.""" + """Reset self.image using tile_dimensions tuple and fills with self.background. Also updates self.wh.""" tw, th = tile_dimensions - self.wh = ( - round(tw * self.tile_full + self.tile_seperation), - round(th * self.tile_full + self.tile_seperation), + tile_full = self.tile_size + self.tile_seperation + self.image = get_tile_container_image( + ( + round(tw * tile_full + self.tile_seperation), + round(th * tile_full + self.tile_seperation), + ), + self.background ) - self.image = get_tile_container_image(self.wh, self.back) - def render_tile( + def blit_tile( self, - tile_object: Tile, + tile_color: int, tile_location: tuple[int, int], ) -> None: """Blit the surface of a given tile object onto self.image at given tile location. It is assumed that all tile locations are xy tuples.""" @@ -894,79 +695,80 @@ def render_tile( ), ) - def update_image(self) -> None: - """Process image changes, directed by self.image_update being True.""" - raise NotImplementedError + def to_image_surface_location( + self, + screen_location: tuple[int, int] | Vector2, + ) -> tuple[int, int]: + """Return the location a screen location would be at on the objects image. Can return invalid data.""" + # Get zero zero in image locations + zx, zy = self.rect.topleft + sx, sy = screen_location # Screen x and y + # Location with respect to image dimensions + return ( + int(sx) - zx, + int(sy) - zy, + ) - def process(self, time_passed: float) -> None: - """Call self.update_image() if self.image_update is True, then set self.update_image to False.""" - if self.image_update: - self.update_image() - self.image_update = False +## def screen_size_update(self) -> None: +## """Handle screensize is changes.""" +## nx, ny = self.location +## +## if self.location_mode_on_resize == "Scale": +## ow, oh = self.screen_size_last +## nw, nh = SCREEN_SIZE +## +## x, y = self.location +## nx, ny = x * (nw / ow), y * (nh / oh) +## +## self.location = Vector2(nx, ny) +## self.screen_size_last = SCREEN_SIZE class Cursor(TileRenderer): """Cursor Object.""" - __slots__ = ("holding_number_one", "tiles") + __slots__ = ("tiles",) greyshift = GREYSHIFT - Render_Priority = "last" - def __init__(self, game: Game) -> None: + def __init__(self) -> None: """Initialize cursor with a game it belongs to.""" - super().__init__("Cursor", game, background=None) + super().__init__("Cursor", background=None) - self.holding_number_one = False - self.tiles: deque[Tile] = deque() + self.tiles: list[int] = [] def update_image(self) -> None: """Update self.image.""" self.clear_image((len(self.tiles), 1)) for x in range(len(self.tiles)): - self.render_tile(self.tiles[x], (x, 0)) + self.blit_tile(self.tiles[x], (x, 0)) + self.dirty = 1 - def is_pressed(self) -> bool: - """Return True if the right mouse button is pressed.""" - return bool(pygame.mouse.get_pressed()[0]) - - def get_held_count(self, count_number_one: bool = False) -> int: - """Return the number of held tiles, can be discounting number one tile.""" - length = len(self.tiles) - if self.holding_number_one and not count_number_one: - return length - 1 - return length + def get_held_count(self) -> int: + """Return the number of held tiles.""" + return len(self.tiles) - def is_holding(self, count_number_one: bool = False) -> bool: + def is_holding(self) -> bool: """Return True if the mouse is dragging something.""" - return self.get_held_count(count_number_one) > 0 + return len(self.tiles) > 0 def get_held_info( self, - count_number_one_tile: bool = False, - ) -> tuple[Tile | None, int]: - """Return color of tiles are and number of tiles held.""" - if not self.is_holding(count_number_one_tile): - return None, 0 - return self.tiles[0], self.get_held_count(count_number_one_tile) + ) -> tuple[int, ...]: + """Return tuple of currently held tiles.""" + return tuple(self.tiles) def process(self, time_passed: float) -> None: """Process cursor.""" x, y = pygame.mouse.get_pos() - x = saturate(x, 0, SCREENSIZE[0]) - y = saturate(y, 0, SCREENSIZE[1]) - self.location = Vector2(x, y) - if self.image_update: - if len(self.tiles): - self.update_image() - else: - self.image = None - self.image_update = False + x = saturate(x, 0, SCREEN_SIZE[0]) + y = saturate(y, 0, SCREEN_SIZE[1]) + self.location = (x, y) def force_hold(self, tiles: Iterable[Tile]) -> None: """Pretty much it's drag but with no constraints.""" for tile in tiles: - if tile.color == NUMBERONETILE: + if tile.color == Tiles.one: self.holding_number_one = True self.tiles.append(tile) else: @@ -976,7 +778,7 @@ def force_hold(self, tiles: Iterable[Tile]) -> None: def drag(self, tiles: Iterable[Tile]) -> None: """Drag one or more tiles, as long as it's a list.""" for tile in tiles: - if tile is not None and tile.color == NUMBERONETILE: + if tile is not None and tile.color == Tiles.one: self.holding_number_one = True self.tiles.append(tile) else: @@ -1001,13 +803,13 @@ def drop( tiles = [] for tile in (self.tiles.popleft() for i in range(number)): - if tile.color == NUMBERONETILE and not allow_number_one_tile: + if tile.color == Tiles.one and not allow_number_one_tile: self.tiles.append(tile) continue tiles.append(tile) self.image_update = True - self.holding_number_one = NUMBERONETILE in { + self.holding_number_one = Tiles.one in { tile.color for tile in self.tiles } return tiles @@ -1087,7 +889,7 @@ def update_image(self) -> None: for y in range(self.size[1]): for x in range(self.size[0]): - self.render_tile(Tile(self.data[y, x]), (x, y)) + self.blit_tile(Tile(self.data[y, x]), (x, y)) def get_tile_point( self, @@ -1095,7 +897,7 @@ def get_tile_point( ) -> tuple[int, int] | None: """Return the xy choordinates of which tile intersects given a point. Returns None if no intersections.""" # Can't get tile if screen location doesn't intersect our hitbox! - if not self.point_intersects(screen_location): + if not self.is_selected(screen_location): return None # Otherwise, find out where screen point is in image locations # board x and y @@ -1509,13 +1311,13 @@ def update_image(self) -> None: self.clear_image((self.size, 1)) for x in range(len(self.tiles)): - self.render_tile(self.tiles[x], (x, 0)) + self.blit_tile(self.tiles[x], (x, 0)) def get_tile_point(self, screen_location: tuple[int, int]) -> int | None: """Return the xy choordinates of which tile intersects given a point. Returns None if no intersections.""" # `Grid.get_tile_point` inlined # Can't get tile if screen location doesn't intersect our hitbox! - if not self.point_intersects(screen_location): + if not self.is_selected(screen_location): return None # Otherwise, find out where screen point is in image locations # board x and y @@ -1625,7 +1427,7 @@ def wall_tile( def set_background(self, color: tuple[int, int, int] | None) -> None: """Set the background color for this row.""" - self.back = color + self.background = color self.image_update = True @@ -1712,86 +1514,11 @@ def process(self, time_passed: float) -> None: super().process(time_passed) -class Text(Object): - """Text object, used to render text with a given font.""" - - __slots__ = ("_cxy", "_last", "font") - - def __init__( - self, - font_size: int, - color: tuple[int, int, int], - background: tuple[int, int, int] | None = None, - cx: bool = True, - cy: bool = True, - name: str = "", - ) -> None: - """Initialize text.""" - super().__init__(f"Text{name}") - self.font = Font( - FONT, - font_size, - color, - cx, - cy, - True, - background, - True, - ) - self._cxy = cx, cy - self._last: str | None = None - - def get_image_zero(self) -> tuple[int, int]: - """Return the screen location of the topleft point of self.image.""" - x = int(self.location[0]) - y = int(self.location[1]) - if self._cxy[0]: - x -= self.wh[0] // 2 - if self._cxy[1]: - y -= self.wh[1] // 2 - return x, y - - def __repr__(self) -> str: - """Return representation of self.""" - return f"<{self.__class__.__name__} Object>" - - @staticmethod - def get_font_height(font: str | Path, size: int) -> int: - """Return the height of font at fontsize size.""" - return pygame.font.Font(font, size).get_height() - - def update_value( - self, - text: str | None, - size: int | None = None, - color: tuple[int, int, int] | None = None, - background: tuple[int, int, int] | None = None, - ) -> pygame.surface.Surface: - """Return a surface of given text rendered in FONT.""" - self.image = self.font.render_nosurf(text, size, color, background) - return self.image - - def get_surface(self) -> pygame.surface.Surface: - """Return self.image.""" - assert self.image is not None - return self.image - - def get_tile_point(self, location: tuple[int, int]) -> None: - """Set get_tile_point attribute so that errors are not raised.""" - return - - def process(self, time_passed: float) -> None: - """Process text.""" - if self.font.last_text != self._last: - self.update_value(self.font.last_text) - self._last = self.font.last_text - - class FloorLine(Row): """Represents a player's floor line.""" size = 7 - number_one_color = NUMBERONETILE + number_one_color = Tiles.one def __init__(self, player: Player) -> None: """Initialize floor line.""" @@ -1918,7 +1645,8 @@ def add_circle(self, surface: pygame.surface.Surface) -> None: """Add circle to self.image.""" # if f"FactoryCircle{self.radius}" not in self.game.cache: rad = math.ceil(self.radius) - surf = set_alpha(pygame.surface.Surface((2 * rad, 2 * rad)), 1) + surf = pygame.surface.Surface((2 * rad, 2 * rad), SRCALPHA) +## surf = set_alpha(, 1) pygame.draw.circle(surf, self.outline, (rad, rad), rad) pygame.draw.circle( surf, @@ -2120,7 +1848,7 @@ class TableCenter(Grid): """Object that represents the center of the table.""" size = (6, 6) - first_tile_color = NUMBERONETILE + first_tile_color = Tiles.one def __init__(self, game: Game, has_number_one_tile: bool = True) -> None: """Initialize center of table.""" @@ -2215,10 +1943,10 @@ def process(self, time_passed: float) -> None: cursor.is_pressed() and not cursor.is_holding() and not self.is_empty() - and self.point_intersects(cursor.location) + and self.is_selected(cursor.location) ): point = self.get_tile_point(cursor.location) - # Shouldn't return none anymore since we have point_intersects now. + # Shouldn't return none anymore since we have is_selected now. assert point is not None tile = self.get_info(point) assert isinstance(tile, Tile) @@ -2228,7 +1956,7 @@ def process(self, time_passed: float) -> None: super().process(time_passed) -class Bag: +class Bag(Component): """Represents the bag full of tiles.""" __slots__ = ( @@ -2241,6 +1969,7 @@ class Bag: def __init__(self, tile_count: int = 100, tile_types: int = 5) -> None: """Initialize bag of tiles.""" + super().__init__("bag") self.tile_count = int(tile_count) self.tile_types = int(tile_types) self.tile_names = [chr(65 + i) for i in range(self.tile_types)] @@ -2318,18 +2047,17 @@ def add_tiles(self, tile_objects: Iterable[Tile]) -> None: self.add_tile(tile_object) -class BoxLid(Object): +class BoxLid(Component): """BoxLid Object, represents the box lid were tiles go before being added to the bag again.""" - def __init__(self, game: Game) -> None: + def __init__(self) -> None: """Initialize box lid.""" super().__init__("BoxLid") - self.game = game self.tiles: deque[Tile] = deque() def __repr__(self) -> str: """Return representation of self.""" - return f"{self.__class__.__name__}({self.game!r})" + return f"{self.__class__.__name__}()" def add_tile(self, tile: Tile) -> None: """Add a tile to self.""" @@ -2680,132 +2408,100 @@ def process(self, time_passed: float) -> None: super().process(time_passed) -class Button(Text): - """Button Object.""" +class HaltState(AsyncState["AzulClient"]): + """Halt state to set state to None so running becomes False.""" - textcolor = BUTTONTEXTCOLOR - backcolor = BUTTONBACKCOLOR - - def __init__( - self, - state: MenuState, - name: str, - minimum_size: int = 10, - initial_value: str = "", - font_size: int = BUTTONFONTSIZE, - ) -> None: - """Initialize button.""" - super().__init__(font_size, self.textcolor, background=None) - self.name = f"Button{name}" - self.state = state + __slots__ = () - self.minsize = int(minimum_size) - self.update_value(initial_value) + def __init__(self) -> None: + """Initialize Halt State.""" + super().__init__("Halt") - self.borderWidth = math.floor(font_size / 12) # 5 + async def check_conditions(self) -> None: + """Set active state to None.""" + assert self.machine is not None + await self.machine.set_state(None) - self.delay = 0.6 - self.cur_time = 1.0 - self.action: Callable[[], None] = lambda: None +class GameState(AsyncState["AzulClient"]): + """Checkers Game Asynchronous State base class.""" - def __repr__(self) -> str: - """Return representation of self.""" - return f"Button({self.name[6:]}, {self.state}, {self.minsize}, {self.font.last_text}, {self.font.pyfont})" + __slots__ = ("id", "manager") - def get_height(self) -> int: - """Return font height.""" - return self.font.get_height() + def __init__(self, name: str) -> None: + """Initialize Game State.""" + super().__init__(name) - def bind_action(self, function: Callable[[], None]) -> None: - """When self is pressed, call given function exactly once. Function takes no arguments.""" - self.action = function + self.id: int = 0 + self.manager = ComponentManager(self.name) + + def add_actions(self) -> None: + """Add internal component manager to state machine's component manager.""" + assert self.machine is not None + self.machine.manager.add_component(self.manager) + + def group_add(self, new_sprite: sprite.Sprite) -> None: + """Add new sprite to state machine's group.""" + assert self.machine is not None + group = self.machine.get_group(self.id) + assert group is not None, "Expected group from new group id" + group.add(new_sprite) + self.manager.add_component(new_sprite) + + async def exit_actions(self) -> None: + """Remove group and unbind all components.""" + assert self.machine is not None + self.machine.remove_group(self.id) + self.manager.unbind_components() + self.id = 0 - def update_value( + def change_state( self, - text: str | None, - size: int | None = None, - color: tuple[int, int, int] | None = None, - background: tuple[int, int, int] | None = None, - ) -> pygame.surface.Surface: - """Update button text.""" - disp = str(text or "").center(self.minsize) - surface = super().update_value(f" {disp} ", size, color, background) - self.font.last_text = disp - return surface + new_state: str | None, + ) -> Callable[[Event[Any]], Awaitable[None]]: + """Return an async function that will change state to `new_state`.""" - def render(self, surface: pygame.surface.Surface) -> None: - """Render button.""" - if not self.hidden: - text_rect = self.get_rect() - # if PYGAME_VERSION < 201: - # pygame.draw.rect(surface, self.backcolor, text_rect) - # pygame.draw.rect(surface, BLACK, text_rect, self.borderWidth) - # else: - pygame.draw.rect( - surface, - self.backcolor, - text_rect, - border_radius=20, - ) - pygame.draw.rect( - surface, - BLACK, - text_rect, - width=self.borderWidth, - border_radius=20, - ) - super().render(surface) + async def set_state(*args: object, **kwargs: object) -> None: + play_sound("button_click") + await self.machine.set_state(new_state) - def is_pressed(self) -> bool: - """Return True if this button is pressed.""" - assert self.state.game is not None - cursor = self.state.game.get_object_by_name("Cursor") - assert isinstance(cursor, Cursor) - return ( - not self.hidden - and cursor.is_pressed() - and self.point_intersects(cursor.location) - ) + return set_state - def process(self, time_passed: float) -> None: - """Call self.action one time when pressed, then wait self.delay to call again.""" - if self.cur_time > 0: - self.cur_time = max(self.cur_time - time_passed, 0) - elif self.is_pressed(): - self.action() - self.cur_time = self.delay - if self.font.last_text != self._last: - self.textSize = self.font.pyfont.size(f" {self.font.last_text} ") - super().process(time_passed) +class KwargOutlineText(objects.OutlinedText): + """Outlined Text with attributes settable via keyword arguments.""" -class GameState: - """Base class for all game states.""" + __slots__ = () - __slots__ = ("game", "name") + def __init__( + self, + name: str, + font: pygame.font.Font, + **kwargs: object, + ) -> None: + """Initialize attributes via keyword arguments.""" + super().__init__(name, font) - def __init__(self, name: str) -> None: - """Initialize state with a name, set self.game to None to be overwritten later.""" - self.game: Game | None = None - self.name = name + for key, value in kwargs.items(): + setattr(self, key, value) - def __repr__(self) -> str: - """Return representation of self.""" - return f"<{self.__class__.__name__} {self.name}>" - def entry_actions(self) -> None: - """Perform entry actions for this GameState.""" +class KwargButton(objects.Button): + """Button with attributes settable via keyword arguments.""" - def do_actions(self) -> None: - """Perform actions for this GameState.""" + __slots__ = () - def check_state(self) -> str | None: - """Check state and return new state. None remains in current state.""" - return None + def __init__( + self, + name: str, + font: pygame.font.Font, + **kwargs: object, + ) -> None: + """Initialize attributes via keyword arguments.""" + super().__init__(name, font) - def exit_actions(self) -> None: - """Perform exit actions for this GameState.""" + for key, value in kwargs.items(): + setattr(self, key, value) class MenuState(GameState): @@ -2817,9 +2513,6 @@ class MenuState(GameState): def __init__(self, name: str) -> None: """Initialize GameState and set up self.bh.""" super().__init__(name) - self.bh = Text.get_font_height(FONT, self.fontsize) - - self.next_state: str | None = None def add_button( self, @@ -2830,53 +2523,50 @@ def add_button( size: int = fontsize, minlen: int = button_minimum, ) -> int: - """Add a new Button object to self.game with arguments. Return button id.""" - button = Button(self, name, minlen, value, size) - button.bind_action(action) - if location is not None: - button.location = Vector2(*location) - assert self.game is not None - self.game.add_object(button) - return button.id + """Add a new Button object to group""" + button = KwargButton( + name, + font=pygame.font.Font(FONT, size), + visible=True, + color=Color(0, 0, 0), + text=value, + location=location, + handle_click=action, + ) + self.group_add(button) def add_text( self, name: str, value: str, location: tuple[int, int], - color: tuple[int, int, int] = BUTTONTEXTCOLOR, - cx: bool = True, - cy: bool = True, + color: tuple[int, int, int] = BUTTON_TEXT_COLOR, size: int = fontsize, + outline: tuple[int, int, int] = BUTTON_TEXT_OUTLINE, ) -> int: """Add a new Text object to self.game with arguments. Return text id.""" - text = Text(size, color, None, cx, cy, name) - text.location = Vector2(*location) - text.update_value(value) - assert self.game is not None - self.game.add_object(text) - return text.id - - def entry_actions(self) -> None: - """Clear all objects, add cursor object, and set up next_state.""" - self.next_state = None - - assert self.game is not None - self.game.rm_star() - self.game.add_object(Cursor(self.game)) + text = KwargOutlineText( + name, + font=pygame.font.Font(FONT, size), + visible=True, + color=color, + text=value, + location=location, + ) + self.group_add(text) def set_var(self, attribute: str, value: object) -> None: """Set MenuState.{attribute} to {value}.""" setattr(self, attribute, value) - def to_state(self, state_name: str) -> Callable[[], None]: + def to_state(self, new_state: str) -> Callable[[], Awaitable[None]]: """Return a function that will change game state to state_name.""" - def to_state_name() -> None: - """Set MenuState.next_state to {state_name}.""" - self.next_state = state_name + async def set_state(*args: object, **kwargs: object) -> None: + play_sound("button_click") + await self.machine.set_state(new_state) - return to_state_name + return set_state def var_dependant_to_state( self, @@ -2955,94 +2645,127 @@ def toggle_value() -> None: return toggle_value - def check_state(self) -> str | None: - """Return self.next_state.""" - return self.next_state - -class InitState(GameState): +class InitializeState(AsyncState["AzulClient"]): """Initialize state.""" __slots__ = () def __init__(self) -> None: """Initialize self.""" - super().__init__("Init") + super().__init__("initialize") - def entry_actions(self) -> None: - """Register keyboard handlers.""" - assert self.game is not None - assert self.game.keyboard is not None - self.game.keyboard.add_listener("\x7f", "Delete") - self.game.keyboard.bind_action("Delete", "screenshot", 5) - - self.game.keyboard.add_listener("\x1b", "Escape") - self.game.keyboard.bind_action("Escape", "raise_close", 5) - - self.game.keyboard.add_listener("0", "Debug") - self.game.keyboard.bind_action("Debug", "debug", 5) - - def check_state(self) -> str: + async def check_conditions(self) -> str: """Go to title state.""" - return "Title" + return "title" -class TitleScreen(MenuState): +class TitleState(MenuState): """Game state when the title screen is up.""" __slots__ = () def __init__(self) -> None: """Initialize title.""" - super().__init__("Title") + super().__init__("title") - def entry_actions(self) -> None: + async def entry_actions(self) -> None: """Set up buttons.""" - super().entry_actions() - sw, sh = SCREENSIZE - self.add_button( - "ToSettings", - "New Game", - self.to_state("Settings"), - (sw // 2, sh // 2 - self.bh // 2), + assert self.machine is not None + self.id = self.machine.new_group("title") + + button_font = pygame.font.Font(FONT, 28) + title_font = pygame.font.Font(FONT, 56) + + title_text = KwargOutlineText( + "title_text", + title_font, + visible=True, + color=Color(0, 0, 0), + outline=(255, 0, 0), + border_width=4, + text=__title__.upper(), ) - self.add_button( - "ToCredits", - "Credits", - self.to_state("Credits"), - (sw // 2, sh // 2 + self.bh * 3), - int(self.fontsize / 1.5), + title_text.location = (SCREEN_SIZE[0] // 2, title_text.rect.h) + self.group_add(title_text) + + hosting_button = KwargButton( + "hosting_button", + button_font, + visible=True, + color=Color(0, 0, 0), + text="Host Networked Game", + location=[x // 2 for x in SCREEN_SIZE], + handle_click=self.change_state("play_hosting"), ) - assert self.game is not None - self.add_button( - "Quit", - "Quit", - self.game.raise_close, - (sw // 2, sh // 2 + self.bh * 4), - int(self.fontsize / 1.5), + self.group_add(hosting_button) + + join_button = KwargButton( + "join_button", + button_font, + visible=True, + color=Color(0, 0, 0), + text="Join Networked Game", + location=hosting_button.location + + Vector2( + 0, + hosting_button.rect.h + 10, + ), + handle_click=self.change_state("play_joining"), ) + self.group_add(join_button) + + internal_button = KwargButton( + "internal_hosting", + button_font, + visible=True, + color=Color(0, 0, 0), + text="Singleplayer Game", + location=hosting_button.location + - Vector2( + 0, + hosting_button.rect.h + 10, + ), + handle_click=self.change_state("play_internal_hosting"), + ) + self.group_add(internal_button) + + quit_button = KwargButton( + "quit_button", + button_font, + visible=True, + color=Color(0, 0, 0), + text="Quit", + location=join_button.location + + Vector2( + 0, + join_button.rect.h + 10, + ), + handle_click=self.change_state("Halt"), + ) + self.group_add(quit_button) -class CreditsScreen(MenuState): +class CreditsState(MenuState): """Game state when credits for original game are up.""" __slots__ = () def __init__(self) -> None: """Initialize credits.""" - super().__init__("Credits") + super().__init__("credits") def check_state(self) -> str: """Return to title.""" - return "Title" + return "title" -class SettingsScreen(MenuState): +class SettingsState(MenuState): """Game state when user is defining game type, players, etc.""" def __init__(self) -> None: """Initialize settings.""" - super().__init__("Settings") + super().__init__("settings") self.player_count = 0 # 2 self.host_mode = True @@ -3104,7 +2827,7 @@ def set_player_count() -> None: for i in range(count): add_number(i, start + i) - sw, sh = SCREENSIZE + sw, sh = SCREEN_SIZE cx = sw // 2 cy = sh // 2 @@ -3124,9 +2847,9 @@ def host_text(x: object) -> str: size=int(self.fontsize / 1.5), ) - # TEMPORARY: Hide everything to do with "Host Mode", networked games aren't done yet. - assert self.game is not None - self.game.set_attr_all("hidden", True) +## # TEMPORARY: Hide everything to do with "Host Mode", networked games aren't done yet. +## assert self.game is not None +## self.game.set_attr_all("visible", False) def varient_text(x: object) -> str: return f"Variant Play: {x}" @@ -3292,19 +3015,6 @@ def exit_actions(self) -> None: self.game.player_turn = nturn -class PhaseWallTilingNetworked(PhaseWallTiling): - """Wall tiling networked state.""" - - __slots__ = () - - def __init__(self) -> None: - """Initialize will tiling networked.""" - GameState.__init__(self, "WallTilingNetworked") - - def check_state(self) -> str: - """Go to networked next prepare.""" - return "PrepareNextNetworked" - class PhasePrepareNext(GameState): """Prepare next phase of game.""" @@ -3342,18 +3052,6 @@ def check_state(self) -> str: return "End" -class PhasePrepareNextNetworked(PhasePrepareNext): - """Prepare for next, networked.""" - - __slots__ = () - - def __init__(self) -> None: - """Initialize prepare for next stage.""" - GameState.__init__(self, "PrepareNextNetworked") - - def check_state(self) -> str: - """Go to networked end.""" - return "EndNetworked" class EndScreen(MenuState): @@ -3455,7 +3153,7 @@ def entry_actions(self) -> None: "ReturnTitle", "Return to Title", self.to_state("Title"), - (SCREENSIZE[0] // 2, SCREENSIZE[1] * 4 // 5), + (SCREEN_SIZE[0] // 2, SCREEN_SIZE[1] * 4 // 5), ) buttontitle = self.game.get_object(bid) assert isinstance(buttontitle, Button) @@ -3463,7 +3161,7 @@ def entry_actions(self) -> None: buttontitle.cur_time = 2 # Add score board - x = SCREENSIZE[0] // 2 + x = SCREEN_SIZE[0] // 2 y = 10 for idx, line in enumerate(self.wininf.split("\n")): self.add_text(f"Line{idx}", line, (x, y), cx=True, cy=False) @@ -3474,18 +3172,6 @@ def entry_actions(self) -> None: y += self.bh -class EndScreenNetworked(EndScreen): - """Networked end screen.""" - - def __init__(self) -> None: - """Initialize end screen.""" - MenuState.__init__(self, "EndNetworked") - self.ranking = {} - self.wininf = "" - - def check_state(self) -> str: - """Go to title.""" - return "Title" class Game(ObjectHandler): @@ -3504,10 +3190,10 @@ def __init__(self) -> None: self.add_states( [ - InitState(), - TitleScreen(), - CreditsScreen(), - SettingsScreen(), + InitializeState(), + TitleState(), + CreditsState(), + SettingsState(), PhaseFactoryOffer(), PhaseWallTiling(), PhasePrepareNext(), @@ -3538,92 +3224,11 @@ def __repr__(self) -> str: """Return representation of self.""" return f"{self.__class__.__name__}()" - def debug(self) -> None: - """Debug.""" - - def screenshot(self) -> None: - """Save a screenshot of this game's most recent frame.""" - surface = pygame.surface.Surface(SCREENSIZE) - self.render(surface) - str_time = "-".join(time.asctime().split(" ")) - filename = f"Screenshot_at_{str_time}.png" - - if not os.path.exists("Screenshots"): - os.mkdir("Screenshots") - - surface.unlock() - pygame.image.save( - surface, - os.path.join("Screenshots", filename), - filename, - ) - del surface - - savepath = os.path.join(os.getcwd(), "Screenshots") - - print(f'Saved screenshot as "{filename}" in "{savepath}".') - - def raise_close(self) -> None: - """Raise a window close event.""" - pygame.event.post(pygame.event.Event(QUIT)) - - def add_states(self, states: Iterable[GameState]) -> None: - """Add game states to self.""" - for state in states: - if not isinstance(state, GameState): - raise ValueError( - f'"{state}" Object is not a subclass of GameState!', - ) - state.game = self - self.states[state.name] = state - - def set_state(self, new_state_name: str) -> None: - """Change states and perform any exit / entry actions.""" - # Ensure the new state is valid. - if new_state_name not in self.states: - raise ValueError(f'State "{new_state_name}" does not exist!') - - # If we have an active state, - if self.active_state is not None: - # Perform exit actions - self.active_state.exit_actions() - - # The active state is the new state - self.active_state = self.states[new_state_name] - # Perform entry actions for new active state - self.active_state.entry_actions() - - def update_state(self) -> None: - """Perform the actions of the active state and potentially change states.""" - # Only continue if there is an active state - if self.active_state is None: - return - - # Perform the actions of the active state and check conditions - self.active_state.do_actions() - - new_state_name = self.active_state.check_state() - if new_state_name is not None: - self.set_state(new_state_name) - def add_object(self, obj: Object) -> None: """Add an object to the game.""" obj.game = self super().add_object(obj) - def render(self, surface: pygame.surface.Surface) -> None: - """Render all of self.objects to the screen.""" - surface.fill(self.background_color) - self.render_objects(surface) - - def process(self, time_passed: float) -> None: - """Process all the objects and self.""" - if not self.initialized_state and self.keyboard is not None: - self.set_state("Init") - self.initialized_state = True - self.process_objects(time_passed) - self.update_state() - def get_player(self, player_id: int) -> Player: """Get the player with player id player_id.""" if self.players: @@ -3681,7 +3286,7 @@ def start_game( else: raise NotImplementedError() - cx, cy = SCREENSIZE[0] / 2, SCREENSIZE[1] / 2 + cx, cy = SCREEN_SIZE[0] / 2, SCREEN_SIZE[1] / 2 out = math.sqrt(cx**2 + cy**2) // 3 * 2 mdeg = 360 // max_players @@ -3721,247 +3326,532 @@ def screen_size_update(self) -> None: obj.screen_size_update() -class Keyboard: - """Keyboard object, handles keyboard input.""" +class PlayHostingState(AsyncState["AzulClient"]): + """Start running server.""" - __slots__ = ("actions", "active", "delay", "keys", "target", "time") + __slots__ = ("address",) - def __init__( + internal_server = False + + def __init__(self) -> None: + """Initialize Play internal hosting / hosting State.""" + extra = "_internal" if self.internal_server else "" + super().__init__(f"play{extra}_hosting") + + async def entry_actions(self) -> None: + """Start hosting server.""" + assert self.machine is not None + self.machine.manager.add_components( + ( + GameServer(self.internal_server), + GameClient("network"), + ), + ) + + host = "localhost" if self.internal_server else await find_ip() + port = DEFAULT_PORT + + self.address = (host, port) + + await self.machine.raise_event(Event("server_start", self.address)) + + async def exit_actions(self) -> None: + """Have client connect.""" + assert self.machine is not None + await self.machine.raise_event( + Event("client_connect", self.address), + ) + + async def check_conditions(self) -> str | None: + """Return to Play state when server is up and running.""" + server: GameServer = self.machine.manager.get_component("GameServer") + return "play" if server.running else None + + +class PlayInternalHostingState(PlayHostingState): + """Host server with internal server mode.""" + + __slots__ = () + + internal_server = True + + +class ReturnElement(element_list.Element, objects.Button): + """Connection list return to title element sprite.""" + + __slots__ = () + + def __init__(self, name: str, font: pygame.font.Font) -> None: + """Initialize return element.""" + super().__init__(name, font) + + self.update_location_on_resize = False + self.border_width = 4 + self.outline = RED + self.text = "Return to Title" + self.visible = True + self.location = (SCREEN_SIZE[0] // 2, self.location.y + 10) + + async def handle_click( self, - target: Game, - **kwargs: tuple[str, str], + _: Event[sprite.PygameMouseButtonEventData], ) -> None: - """Initialize keyboard.""" - self.target = target - self.target.keyboard = self - - # Map of keyboard events to names - self.keys: dict[str, str] = {} - # Map of keyboard event names to functions - self.actions: dict[str, Callable[[], None]] = {} - # Map of names to time until function should be called again - self.time: dict[str, float] = {} - # Map of names to duration timer waits for function recalls - self.delay: dict[str, float | None] = {} - # Map of names to boolian of pressed or not - self.active: dict[str, bool] = {} - - for name in kwargs: - if not hasattr(kwargs[name], "__iter__"): - raise ValueError( - "Keyword arguments must be given as name=[key, self.target.function_name, delay]", - ) - # if len(kwargs[name]) == 2: - key, function_name = kwargs[name] - # elif len(kwargs[name]) == 3: - # key, function_name, _delay = kwargs[name] - # else: - # raise ValueError - self.add_listener(key, name) - self.bind_action(name, function_name) + """Handle Click Event.""" + await self.raise_event( + Event("return_to_title", None, 2), + ) - def __repr__(self) -> str: - """Return representation of self.""" - return f"{self.__class__.__name__}({self.target!r})" - def is_pressed(self, key: str) -> bool: - """Return True if is pressed.""" - return self.active.get(key, False) +class ConnectionElement(element_list.Element, objects.Button): + """Connection list element sprite.""" - def add_listener(self, key: str, name: str) -> None: - """Listen for key down events with event.key == key argument and when that happens set self.actions[name] to true.""" - self.keys[key] = name # key to name - self.actions[name] = lambda: None # name to function - self.time[name] = 0 # name to time until function recall - self.delay[name] = None # name to function recall delay - self.active[name] = False # name to boolian of pressed + __slots__ = () - def get_function_from_target( + def __init__( self, - function_name: str, - ) -> Callable[[], None]: - """Return function with name function_name from self.target.""" - if hasattr(self.target, function_name): - attribute = getattr(self.target, function_name) - assert callable(attribute) - return cast("Callable[[], None]", attribute) - return lambda: None - - def bind_action( + name: tuple[str, int], + font: pygame.font.Font, + motd: str, + ) -> None: + """Initialize connection element.""" + super().__init__(name, font) + + self.text = f"[{name[0]}:{name[1]}]\n{motd}" + self.visible = True + + async def handle_click( self, - name: str, - target_function_name: str, - delay: float | None = None, + _: Event[sprite.PygameMouseButtonEventData], ) -> None: - """Bind an event we are listening for to calling a function, can call multiple times if delay is not None.""" - self.actions[name] = self.get_function_from_target( - target_function_name, + """Handle Click Event.""" + details = self.name + await self.raise_event( + Event("join_server", details, 2), ) - self.delay[name] = delay - - def set_active(self, name: str, value: bool) -> None: - """Set active value for key name to .""" - if name in self.active: - self.active[name] = bool(value) - if not value: - self.time[name] = 0 - - def set_key(self, key: str, value: bool) -> None: - """Set active value for key to .""" - if key in self.keys: - self.set_active(self.keys[key], value) - - # elif isinstance(key, int) and key < 0x110000: - # self.set_key(chr(key), value) - - def read_event(self, event: pygame.event.Event) -> None: - """Handle an event.""" - if event.type == KEYDOWN: - self.set_key(event.key, True) - elif event.type == KEYUP: - self.set_key(event.key, False) - - def read_events(self, events: Iterable[pygame.event.Event]) -> None: - """Handle a list of events.""" - for event in events: - self.read_event(event) - def process(self, time_passed: float) -> None: - """Send commands to self.target based on pressed keys and time.""" - for name in self.active: - if self.active[name]: - self.time[name] = max(self.time[name] - time_passed, 0) - if self.time[name] == 0: - self.actions[name]() - delay = self.delay[name] - if delay is not None: - self.time[name] = delay - else: - self.time[name] = math.inf +class PlayJoiningState(GameState): + """Start running client.""" -def network_shutdown() -> None: - """Handle network shutdown.""" + __slots__ = ("font",) + def __init__(self) -> None: + """Initialize Joining State.""" + super().__init__("play_joining") -def run() -> None: + self.font = pygame.font.Font( + FONT, + 12, + ) + + async def entry_actions(self) -> None: + """Add game client component.""" + await super().entry_actions() + assert self.machine is not None + self.id = self.machine.new_group("join") + client = GameClient("network") + + # Add network to higher level manager + self.machine.manager.add_component(client) + + connections = element_list.ElementList("connection_list") + self.manager.add_component(connections) + group = self.machine.get_group(self.id) + assert group is not None + group.add(connections) + + return_font = pygame.font.Font( + FONT, + 30, + ) + return_button = ReturnElement("return_button", return_font) + connections.add_element(return_button) + + self.manager.register_handlers( + { + "update_listing": self.handle_update_listing, + "return_to_title": self.handle_return_to_title, + "join_server": self.handle_join_server, + }, + ) + + await self.manager.raise_event(Event("update_listing", None)) + + async def handle_update_listing(self, _: Event[None]) -> None: + """Update server listing.""" + assert self.machine is not None + + connections = self.manager.get_component("connection_list") + + old: list[tuple[str, int]] = [] + current: list[tuple[str, int]] = [] + + # print(f'{self.machine.active_state = }') + # print(f'{self.name = }') + while ( + self.machine.active_state is not None + and self.machine.active_state is self + ): + # print("handle_update_listing click") + + for motd, details in await read_advertisements(): + current.append(details) + if connections.component_exists(details): + continue + element = ConnectionElement(details, self.font, motd) + element.rect.topleft = ( + connections.get_new_connection_position() + ) + element.rect.topleft = (10, element.location.y + 3) + connections.add_element(element) + for details in old: + if details in current: + continue + connections.delete_element(details) + old, current = current, [] + + async def handle_join_server(self, event: Event[tuple[str, int]]) -> None: + """Handle join server event.""" + details = event.data + await self.machine.raise_event( + Event("client_connect", details), + ) + await self.machine.set_state("play") + + async def handle_return_to_title(self, _: Event[None]) -> None: + """Handle return to title event.""" + # Fire server stop event so server shuts down if it exists + await self.machine.raise_event_internal(Event("network_stop", None)) + + if self.machine.manager.component_exists("network"): + self.machine.manager.remove_component("network") + + await self.machine.set_state("title") + + +# async def check_conditions(self) -> str | None: +# return None + + +class PlayState(GameState): + """Game Play State.""" + + __slots__ = ("exit_data",) + + def __init__(self) -> None: + """Initialize Play State.""" + super().__init__("play") + + # (0: normal | 1: error) + self.exit_data: tuple[int, str, bool] | None = None + + def register_handlers(self) -> None: + """Register event handlers.""" + self.manager.register_handlers( + { + "client_disconnected": self.handle_client_disconnected, + "game_winner": self.handle_game_over, + }, + ) + + def add_actions(self) -> None: + """Register handlers.""" + super().add_actions() + self.register_handlers() + + async def entry_actions(self) -> None: + """Add GameBoard and raise init event.""" + self.exit_data = None + + assert self.machine is not None + if self.id == 0: + self.id = self.machine.new_group("play") + + # self.group_add(()) +## gameboard = GameBoard( +## 45, +## ) +## gameboard.location = [x // 2 for x in SCREEN_SIZE] +## self.group_add(gameboard) + + await self.machine.raise_event(Event("init", None)) + + async def check_conditions(self) -> str | None: + """Return to title if client component doesn't exist.""" + if not self.machine.manager.component_exists("network"): + return "title" + return None + + async def exit_actions(self) -> None: + """Raise network stop event and remove components.""" + # Fire server stop event so server shuts down if it exists + # await self.machine.raise_event(Event("network_stop", None)) + await self.machine.raise_event_internal(Event("network_stop", None)) + + if self.machine.manager.component_exists("network"): + self.machine.manager.remove_component("network") + if self.machine.manager.component_exists("GameServer"): + self.machine.manager.remove_component("GameServer") + + # Unbind components and remove group + await super().exit_actions() + + self.register_handlers() + + assert self.manager.has_handler("game_winner") + + async def handle_game_over(self, event: Event[int]) -> None: + """Handle game over event.""" + winner = event.data + self.exit_data = (0, f"{PLAYERS[winner]} Won", False) + + await self.machine.raise_event_internal(Event("network_stop", None)) + + async def handle_client_disconnected(self, event: Event[str]) -> None: + """Handle client disconnected error.""" + error = event.data + print(f"handle_client_disconnected {error = }") + + self.exit_data = (1, f"Client Disconnected$${error}", False) + + # await self.do_actions() + + async def do_actions(self) -> None: + """Perform actions for this State.""" + # print(f"{self.__class__.__name__} do_actions tick") + if self.exit_data is None: + return + + exit_status, message, handled = self.exit_data + + if handled: + return + self.exit_data = (exit_status, message, True) + + font = pygame.font.Font( + FONT, + 28, + ) + + error_message = "" + if exit_status == 1: + message, error_message = message.split("$$") + + if not self.manager.component_exists("continue_button"): + continue_button = KwargButton( + "continue_button", + font, + visible=True, + color=Color(0, 0, 0), + text=f"{message} - Return to Title", + location=[x // 2 for x in SCREEN_SIZE], + handle_click=self.change_state("title"), + ) + self.group_add(continue_button) + group = continue_button.groups()[0] + # LayeredDirty, not just AbstractGroup + group.move_to_front(continue_button) # type: ignore[attr-defined] + else: + continue_button = self.manager.get_component("continue_button") + + if exit_status == 1: + if not self.manager.component_exists("error_text"): + error_text = objects.OutlinedText("error_text", font) + else: + error_text = self.manager.get_component("error_text") + error_text.visible = True + error_text.color = Color(255, 0, 0) + error_text.border_width = 1 + error_text.text += error_message + "\n" + error_text.location = continue_button.location + Vector2( + 0, + continue_button.rect.h + 10, + ) + + if not self.manager.component_exists("error_text"): + self.group_add(error_text) + + +class AzulClient(sprite.GroupProcessor): + """Azul Game Client.""" + + __slots__ = ("manager",) + + def __init__(self, manager: ExternalRaiseManager) -> None: + """Initialize Checkers Client.""" + super().__init__() + self.manager = manager + + self.add_states( + ( + HaltState(), + InitializeState(), + TitleState(), + CreditsState(), + SettingsState(), + PlayHostingState(), + PlayInternalHostingState(), + PlayJoiningState(), + PlayState(), + ), + ) + + async def raise_event(self, event: Event[Any]) -> None: + """Raise component event in all groups.""" + await self.manager.raise_event(event) + + async def raise_event_internal(self, event: Event[Any]) -> None: + """Raise component event in all groups.""" + await self.manager.raise_event_internal(event) + + +async def async_run() -> None: """Run program.""" - # global game - global SCREENSIZE + # Set up globals + global SCREEN_SIZE + # Set up the screen - screen = pygame.display.set_mode(SCREENSIZE, RESIZABLE, 16) - pygame.display.set_caption(f"{__title__} {__version__}") + screen = pygame.display.set_mode(SCREEN_SIZE, RESIZABLE, 16, vsync=VSYNC) + pygame.display.set_caption(f"{__title__} v{__version__}") # pygame.display.set_icon(pygame.image.load('icon.png')) - pygame.display.set_icon(get_tile_image(Tile(5), 32)) - - # Set up the FPS clock - clock = pygame.time.Clock() - - game = Game() - keyboard = Keyboard(game) - - music_end = USEREVENT + 1 # This event is sent when a music track ends - - # Set music end event to our new event - pygame.mixer.music.set_endevent(music_end) - - # Load and start playing the music - # pygame.mixer.music.load('sound/') - # pygame.mixer.music.play() - - running = True - - # While the game is active - while running: - # Event handler - for event in pygame.event.get(): - if event.type == QUIT: - running = False - elif event.type == music_end: - # If the music ends, stop it and play it again. - pygame.mixer.music.stop() - pygame.mixer.music.play() - elif event.type == VIDEORESIZE: - SCREENSIZE = event.size - game.screen_size_update() - else: - # If it's not a quit or music end event, tell the keyboard handler about it. - keyboard.read_event(event) + pygame.display.set_icon(get_tile_image(Tiles.one, 32)) + screen.fill((0xFF, 0xFF, 0xFF)) + +## try: + async with trio.open_nursery() as main_nursery: + event_manager = ExternalRaiseManager( + "checkers", + main_nursery, # "client" + ) + client = AzulClient(event_manager) + + background = pygame.image.load( + DATA_FOLDER / "background.png", + ).convert() + client.clear(screen, background) + + client.set_timing_threshold(1000 / 80) + + await client.set_state("initialize") + + music_end = USEREVENT + 1 # This event is sent when a music track ends + + # Set music end event to our new event + pygame.mixer.music.set_endevent(music_end) + + # Load and start playing the music + # pygame.mixer.music.load('sound/') + # pygame.mixer.music.play() + + clock = Clock() + + resized_window = False + while client.running: + async with trio.open_nursery() as event_nursery: + for event in pygame.event.get(): + if event.type == QUIT: + await client.set_state("Halt") + elif event.type == KEYUP and event.key == K_ESCAPE: + pygame.event.post(pygame.event.Event(QUIT)) + elif event.type == music_end: + # If the music ends, stop it and play it again. + pygame.mixer.music.stop() + pygame.mixer.music.play() + elif event.type == WINDOWRESIZED: + SCREEN_SIZE = (event.x, event.y) + resized_window = True + sprite_event = sprite.convert_pygame_event(event) + # print(sprite_event) + event_nursery.start_soon( + event_manager.raise_event, + sprite_event, + ) + event_nursery.start_soon(client.think) + event_nursery.start_soon(clock.tick, FPS) + + await client.raise_event( + Event( + "tick", + sprite.TickEventData( + time_passed=clock.get_time() + / 1e9, # nanoseconds -> seconds + fps=clock.get_fps(), + ), + ), + ) - # Get the time passed from the FPS clock - time_passed = clock.tick(FPS) - time_passed_secconds = time_passed / 1000 + if resized_window: + resized_window = False + screen.fill((0xFF, 0xFF, 0xFF)) + rects = [Rect((0, 0), SCREEN_SIZE)] + client.repaint_rect(rects[0]) + rects.extend(client.draw(screen)) + else: + rects = client.draw(screen) + pygame.display.update(rects) + client.clear_groups() - # Process the game - game.process(time_passed_secconds) - keyboard.process(time_passed_secconds) + # Once the game has ended, stop the music + pygame.mixer.music.stop() - # Render the grid to the screen. - game.render(screen) - # Update the display - pygame.display.update() - # Once the game has ended, stop the music and de-initalize pygame. - pygame.mixer.music.stop() +def run() -> None: + """Start asynchronous run.""" + trio.run(async_run) -def save_crash_img() -> None: +def screenshot_last_frame() -> None: """Save the last frame before the game crashed.""" surface = pygame.display.get_surface().copy() - str_time = "-".join(time.asctime().split(" ")) + str_time = "_".join(time.asctime().split(" ")) filename = f"Crash_at_{str_time}.png" - if not os.path.exists("Screenshots"): - os.mkdir("Screenshots") + path = Path("screenshots").absolute() + if not path.exists(): + os.mkdir(path) - # surface.lock() - pygame.image.save(surface, os.path.join("Screenshots", filename), filename) - # surface.unlock() - del surface + fullpath = path / filename - savepath = os.path.join(os.getcwd(), "Screenshots") + pygame.image.save(surface, fullpath, filename) + del surface - print(f'Saved screenshot as "{filename}" in "{savepath}".') + print(f'Saved screenshot to "{fullpath}".') def cli_run() -> None: """Run from command line interface.""" - # Linebreak before, as pygame prints a message on import. - print(f"\n{__title__} v{__version__}\nProgrammed by {__author__}.") + print(f"{__title__} v{__version__}\nProgrammed by {__author__}.\n") + + # Make sure the game will display correctly on high DPI monitors on Windows. + if sys.platform == "win32": + from ctypes import windll + + with contextlib.suppress(AttributeError): + windll.user32.SetProcessDPIAware() + del windll + + exception: str | None = None try: # Initialize Pygame _success, fail = pygame.init() if fail > 0: print( - "Warning! Some modules of Pygame have not initialized properly!", - ) - print( - "This can occur when not all required modules of SDL, which pygame utilizes, are installed.", + "Warning! Some modules of Pygame have not initialized properly!\n", + "This can occur when not all required modules of SDL are installed.", ) run() - # except BaseException as ex: - # reraise = True#False - ## - # print('Debug: Activating Post motem.') - # import pdb - # pdb.post_mortem() - ## - # try: - # save_crash_img() - # except BaseException as svex: - # print(f'Could not save crash screenshot: {", ".join(svex.args)}') - # try: - # import errorbox - # except ImportError: - # reraise = True - # print(f'A {type(ex).__name__} Error Has Occored: {", ".join(ex.args)}') - # else: - # errorbox.errorbox('Error', f'A {type(ex).__name__} Error Has Occored: {", ".join(ex.args)}') - # if reraise: - # raise + except ExceptionGroup as exc: + print(exc) + exception = traceback.format_exception(exc) +## raise +## except BaseException as ex: +## screenshot_last_frame() +## # errorbox.errorbox('Error', f'A {type(ex).__name__} Error Has Occored: {", ".join(ex.args)}') +## raise finally: pygame.quit() - network_shutdown() + if exception is not None: + print(''.join(exception), file=sys.stderr) if __name__ == "__main__": diff --git a/src/azul/mr_floppy_test.py b/src/azul/mr_floppy_test.py new file mode 100644 index 0000000..03815e5 --- /dev/null +++ b/src/azul/mr_floppy_test.py @@ -0,0 +1,490 @@ +"""Azul Client.""" + +from __future__ import annotations + +import contextlib + +# Programmed by CoolCat467 +# Hide the pygame prompt +import os +import sys +from os import path +from pathlib import Path +from typing import TYPE_CHECKING, Any, Final + +import trio +from pygame.locals import K_ESCAPE, KEYUP, QUIT, RESIZABLE, WINDOWRESIZED +from pygame.rect import Rect + +from azul import conf, lang, objects, sprite +from azul.component import Component, ComponentManager, Event +from azul.statemachine import AsyncState, AsyncStateMachine +from azul.vector import Vector2 + +os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "True" +if os.environ["PYGAME_HIDE_SUPPORT_PROMPT"]: + import pygame +del os + + +if TYPE_CHECKING: + from collections.abc import Iterator, Sequence + +__title__ = "Azul Client" +__author__ = "CoolCat467" +__version__ = "2.0.0" + +SCREEN_SIZE = Vector2(800, 600) +FPS = 30 +# FPS = 60 +VSYNC = True +# PORT = server.PORT + +ROOT_FOLDER: Final = Path(__file__).absolute().parent +DATA_FOLDER: Final = ROOT_FOLDER / "data" +FONT_FOLDER: Final = ROOT_FOLDER / "fonts" + +FONT = FONT_FOLDER / "RuneScape-UF-Regular.ttf" + + +class AzulClient(sprite.GroupProcessor, AsyncStateMachine): + """Gear Runner and Layered Dirty Sprite group handler.""" + + def __init__(self) -> None: + """Initialize azul client.""" + sprite.GroupProcessor.__init__(self) + AsyncStateMachine.__init__(self) + + self.add_states( + ( + HaltState(), + AzulInitialize(), + ), + ) + + @property + def running(self) -> bool: + """Boolean of if state machine is running.""" + return self.active_state is not None + + async def raise_event(self, event: Event[Any]) -> None: + """Raise component event in all groups.""" + if self.active_state is None: + return + manager = getattr(self.active_state, "manager", None) + assert isinstance(manager, ComponentManager | None) + if manager is None: + return + await manager.raise_event(event) + + +class AzulState(AsyncState[AzulClient]): + """Azul Client Asynchronous base class.""" + + __slots__ = ("id", "manager") + + def __init__(self, name: str) -> None: + """Initialize azul state.""" + super().__init__(name) + + self.id: int = 0 + self.manager = ComponentManager(self.name) + + +class HaltState(AzulState): + """Halt state to set state to None so running becomes False.""" + + def __init__(self) -> None: + """Initialize halt state.""" + super().__init__("Halt") + + async def check_conditions(self) -> None: + """Set active state to None.""" + await self.machine.set_state(None) + + +class ClickDestinationComponent(Component): + """Component that will use targeting to go to wherever you click on the screen.""" + + __slots__ = ("selected",) + outline = pygame.color.Color(255, 220, 0) + + def __init__(self) -> None: + """Initialize click destination component.""" + super().__init__("click_dest") + + self.selected = False + + def bind_handlers(self) -> None: + """Register PygameMouseButtonDown and tick handlers.""" + self.register_handlers( + { + "click": self.click, + "drag": self.drag, + "PygameMouseButtonDown": self.mouse_down, + "tick": self.move_towards_dest, + "init": self.cache_outline, + "test": self.test, + }, + ) + + async def test(self, event: Event[object]) -> None: + """Print out event data.""" + print(f"{event = }") + + async def cache_outline(self, _: Event[None]) -> None: + """Precalculate outlined images.""" + image: sprite.ImageComponent = self.get_component("image") + outline: sprite.OutlineComponent = image.get_component("outline") + outline.precalculate_all_outlined(self.outline) + + async def update_selected(self) -> None: + """Update selected.""" + image: sprite.ImageComponent = self.get_component("image") + outline: sprite.OutlineComponent = image.get_component("outline") + + color = (None, self.outline)[int(self.selected)] + outline.set_color(color) + + if not self.selected: + movement: sprite.MovementComponent = self.get_component("movement") + movement.speed = 0 + + async def click( + self, + event: Event[sprite.PygameMouseButtonEventData], + ) -> None: + """Toggle selected.""" + if event.data["button"] == 1: + self.selected = not self.selected + + await self.update_selected() + + async def drag(self, event: Event[None]) -> None: + """Drag sprite.""" + if not self.selected: + self.selected = True + await self.update_selected() + movement: sprite.MovementComponent = self.get_component("movement") + movement.speed = 0 + + async def mouse_down( + self, + event: Event[sprite.PygameMouseButtonEventData], + ) -> None: + """Target click pos if selected.""" + if not self.selected: + return + if event.data["button"] == 1: + movement: sprite.MovementComponent = self.get_component("movement") + movement.speed = 200 + target: sprite.TargetingComponent = self.get_component("targeting") + target.destination = Vector2.from_iter(event.data["pos"]) + + async def move_towards_dest( + self, + event: Event[sprite.TickEventData], + ) -> None: + """Move closer to destination.""" + target: sprite.TargetingComponent = self.get_component("targeting") + await target.move_destination_time(event.data.time_passed) + + +class MrFloppy(sprite.Sprite): + """Mr. Floppy test sprite.""" + + __slots__ = () + + def __init__(self) -> None: + """Initialize mr floppy sprite.""" + super().__init__("MrFloppy") + + self.add_components( + ( + sprite.MovementComponent(), + sprite.TargetingComponent(), + ClickDestinationComponent(), + sprite.ImageComponent(), + sprite.DragClickEventComponent(), + ), + ) + + movement = self.get_component("movement") + targeting = self.get_component("targeting") + image = self.get_component("image") + + movement.speed = 200 + + # lintcheck: c-extension-no-member (I1101): Module 'pygame.surface' has no 'Surface' member, but source is unavailable. Consider adding this module to extension-pkg-allow-list if you want to perform analysis based on run-time introspection of living objects. + floppy: pygame.surface.Surface = pygame.image.load( + path.join("data", "mr_floppy.png"), + ) + + image.add_images( + { + 0: floppy, + # '1': pygame.transform.flip(floppy, False, True) + 1: pygame.transform.rotate(floppy, 270), + 2: pygame.transform.flip(floppy, True, True), + 3: pygame.transform.rotate(floppy, 90), + }, + ) + + anim = image.get_component("animation") + anim.controller = self.controller((0, 1, 2, 3)) + + image.set_image(0) + self.visible = True + + self.location = SCREEN_SIZE / 2 + targeting.destination = self.location + + self.register_handler("drag", self.drag) + + @staticmethod + def controller( + image_identifiers: Sequence[str | int], + ) -> Iterator[str | int | None]: + """Animation controller.""" + cidx = 0 + while True: + count = len(image_identifiers) + if not count: + yield None + continue + cidx = (cidx + 1) % count + yield image_identifiers[cidx] + + async def drag(self, event: Event[sprite.DragEvent]) -> None: + """Move by relative from drag.""" + if event.data.button != 1: + return + self.location += event.data.rel + self.dirty = 1 + + +class FPSCounter(objects.Text): + """FPS counter.""" + + __slots__ = () + + def __init__(self) -> None: + """Initialize fps counter.""" + font = pygame.font.Font(FONT, 28) + super().__init__("fps", font) + + async def on_tick(self, event: Event[sprite.TickEventData]) -> None: + """Update text.""" + # self.text = f'FPS: {event.data["fps"]:.2f}' + self.text = f"FPS: {event.data.fps:.0f}" + + async def update_loc( + self, + event: Event[dict[str, tuple[int, int]]], + ) -> None: + """Move to top left corner.""" + self.location = Vector2.from_iter(event.data["size"]) / 2 + (5, 5) + + def bind_handlers(self) -> None: + """Register event handlers.""" + super().bind_handlers() + self.register_handlers( + { + "tick": self.on_tick, + "sprite_image_resized": self.update_loc, + }, + ) + + +class AzulInitialize(AzulState): + """Initialize Azul.""" + + __slots__ = () + + def __init__(self) -> None: + """Initialize state.""" + super().__init__("initialize") + + def group_add(self, new_sprite: sprite.Sprite) -> None: + """Add new sprite to group.""" + group = self.machine.get_group(self.id) + assert group is not None, "Expected group from new group id" + group.add(new_sprite) + self.manager.add_component(new_sprite) + + async def entry_actions(self) -> None: + """Create group and add mr floppy.""" + self.id = self.machine.new_group("test") + floppy = MrFloppy() + print(floppy) + self.group_add(floppy) + self.group_add(FPSCounter()) + + await self.machine.raise_event(Event("init", None)) + + async def exit_actions(self) -> None: + """Remove group and unbind components.""" + self.machine.remove_group(self.id) + self.manager.unbind_components() + + +def save_crash_img() -> None: + """Save the last frame before the game crashed.""" + surface = pygame.display.get_surface().copy() + # strTime = '-'.join(time.asctime().split(' ')) + # filename = f'Crash_at_{strTime}.png' + filename = "screenshot.png" + + pygame.image.save(surface, path.join("screenshots", filename)) + del surface + + +async def async_run() -> None: + """Run client.""" + global SCREEN_SIZE + # global client + config = conf.load_config(path.join("conf", "main.conf")) + lang.load_lang(config["Language"]["lang_name"]) + + screen = pygame.display.set_mode( + tuple(SCREEN_SIZE), + RESIZABLE, + vsync=VSYNC, + ) + pygame.display.set_caption(f"{__title__} v{__version__}") + pygame.key.set_repeat(1000, 30) + screen.fill((0xFF, 0xFF, 0xFF)) + + client = AzulClient() + + background = pygame.image.load( + path.join("data", "background.png"), + ).convert() + client.clear(screen, background) + + client.set_timing_threshold(1000 / FPS) + + await client.set_state("initialize") + + clock = pygame.time.Clock() + + while client.running: + resized_window = False + + async with trio.open_nursery() as nursery: + for event in pygame.event.get(): + # pylint: disable=undefined-variable + if event.type == QUIT: + await client.set_state("Halt") + elif event.type == KEYUP and event.key == K_ESCAPE: + pygame.event.post(pygame.event.Event(QUIT)) + elif event.type == WINDOWRESIZED: + SCREEN_SIZE = Vector2(event.x, event.y) + resized_window = True + sprite_event = sprite.convert_pygame_event(event) + # print(sprite_event) + nursery.start_soon(client.raise_event, sprite_event) + await client.think() + + time_passed = clock.tick(FPS) + + await client.raise_event( + Event( + "tick", + sprite.TickEventData( + time_passed / 1000, + clock.get_fps(), + ), + ), + ) + + if resized_window: + screen.fill((0xFF, 0xFF, 0xFF)) + rects = [Rect((0, 0), tuple(SCREEN_SIZE))] + client.repaint_rect(rects[0]) + rects.extend(client.draw(screen)) + else: + rects = client.draw(screen) + pygame.display.update(rects) + client.clear_groups() + + +class Tracer(trio.abc.Instrument): + """Tracer instrument.""" + + __slots__ = ("_sleep_time",) + + def before_run(self) -> None: + """Before run.""" + print("!!! run started") + + def _print_with_task(self, msg: str, task: trio.lowlevel.Task) -> None: + """Print message with task name.""" + # repr(task) is perhaps more useful than task.name in general, + # but in context of a tutorial the extra noise is unhelpful. + print(f"{msg}: {task.name}") + + def task_spawned(self, task: trio.lowlevel.Task) -> None: + """Task spawned.""" + self._print_with_task("### new task spawned", task) + + def task_scheduled(self, task: trio.lowlevel.Task) -> None: + """Task scheduled.""" + self._print_with_task("### task scheduled", task) + + def before_task_step(self, task: trio.lowlevel.Task) -> None: + """Before task step.""" + self._print_with_task(">>> about to run one step of task", task) + + def after_task_step(self, task: trio.lowlevel.Task) -> None: + """After task step.""" + self._print_with_task("<<< task step finished", task) + + def task_exited(self, task: trio.lowlevel.Task) -> None: + """Task exited.""" + self._print_with_task("### task exited", task) + + def before_io_wait(self, timeout: float) -> None: + """Before IO wait.""" + if timeout: + print(f"### waiting for I/O for up to {timeout} seconds") + else: + print("### doing a quick check for I/O") + self._sleep_time = trio.current_time() + + def after_io_wait(self, timeout: float) -> None: + """After IO wait.""" + duration = trio.current_time() - self._sleep_time + print(f"### finished I/O check (took {duration} seconds)") + + def after_run(self) -> None: + """After run.""" + print("!!! run finished") + + +def run() -> None: + """Run asynchronous side of everything.""" + trio.run(async_run) # , instruments=[Tracer()]) + + +# save_crash_img() + +if __name__ == "__main__": + print(f"{__title__} v{__version__}\nProgrammed by {__author__}.\n") + + # Make sure the game will display correctly on high DPI monitors on Windows. + if sys.platform == "win32": + # Exists on windows but not on linux or macos + # Windows raises attr-defined + # others say unused-ignore + from ctypes import windll # type: ignore[attr-defined,unused-ignore] + + with contextlib.suppress(AttributeError): + windll.user32.SetProcessDPIAware() + del windll + + try: + pygame.init() + run() + finally: + pygame.quit() diff --git a/src/azul/screenshots/Crash_at_Wed-Nov-13-15:30:42-2024.png b/src/azul/screenshots/Crash_at_Wed-Nov-13-15:30:42-2024.png new file mode 100644 index 0000000000000000000000000000000000000000..2f5157632faef40c4f7d4a86329cbc7fb39e7a7f GIT binary patch literal 4361 zcmeHL{W}wS8y|`eqs~cXRC5x=@!D7pTOvYdq$A}doy}&W%uC)j6CzaJF620COzJcT zu_0#1TUr@qwuZ-n#D zY$@!zRLbQl1Dxh_StkJizib@ft_}~d$B`2Sge((ZnoE+DEx_H{zsL4VS_!{Hy2Uf6ngsBTpE>kUKTkb6{UkBQ#EL5+yP z9RDP2G!56%Q*ENA@<_Rq79^akv+f@~-~u zqNKBk!zH^&+VvM&^{ zJs-V2WymE-CDTH%sBv0^CE>-5T6v3J50zsLemX+L&beC6^b&{Br;UiYPZylGT834O$_?HV1xo1!u>+fHgJ-(dPmfQh$vQYKheZ1PqY^_hMsM1b zu82$s$@wX5Z7Jeo-Wc;W@XfiRZ$^*s7?R=Y1I^lLMOme&5UszYqoP*Tc!mIn)^-1WROBwWv%L8`r`Q~%`m^fi6Ef(+H5X%%DSjDgYORA=*SL?=9g}L%L(#Y zCEYcVUbNFBCUp)NN|$&xj{U!eUS0NX`XPGvTsJHS8A5sI;bF0&_ac$#0f7>+J17(u z6F?i0wa??4N2Z8b6FbJ)@`Q@o}-On-on)#qK8017E zcI`QNq75=UssG$5o4%4*!c9d`$@aX)XHjNzqc0c=bOat7U9Q=3C&ENmF$Gi5b`-%w zw93jaXkra$xKU&(He||x{0j^M&cuE_b(EX{+k3j`D~zc1*|r2*S^&T3@Rxn$-?Ntm zjSc{us$(9KRNgSQl|}o)$d=-i+X$=ofNcVdt%7nzX6z{KK1W4We^v|i?&%D2x?NK8 zd9{4wo7bV&HC_vE$bx+^@{FVKoZ@Q;+`XnB7W#|~g7;uD4{9W?C?!LTyc}qZk`Bi`Bfl+k{qjV+P;V| zyLPF@1)Z|6q5qv!yu-l^&c0iNLsuJH)pTo~mX^D`*TiJ37Y~W58<^7>%os)xC7E8N zc=ZlUme*{3GmSahhe5A*ZDxRpGrb?rn{FSE92zmfwr#jQkB|E`+<+|g3t<+ctv8;VJo<5o%Z+BW2Yr+>W8)G`jq%?yVuFmH0r_ z4Fr!L#N>{Ne!k(?=tZ{ zO|40LB1cYmDtAK6>z@T^P0$uM-ZctzRK&_O+;Kb6#fFBPKTQsXiS}{h{97UFcv5<= ze8-`LEYCmx;Y=-;fe>NzX8qc{?@Sq>vK7)EurTtrJ2w7kutAEDFTQ!f`9M5Xu;M6m zfDa{3fUps_ZRqW7IT`HEoK#pehjsAV7J+rplf&LuZd&iL+5M($H!+}psx>2S zsxG?+FN}yU7*aT17H}p3c7|S3a<*Z0Sqwtm%AM+piDGGlhuX<8=2*q|_pHc5l9Z`-@Osw(`u4SdIzoV#1m<+XA6Z?Z2AfU|?E JeYqVn{y)4=Y%u@; literal 0 HcmV?d00001 diff --git a/src/azul/screenshots/Crash_at_Wed_Nov_13_16:02:46_2024.png b/src/azul/screenshots/Crash_at_Wed_Nov_13_16:02:46_2024.png new file mode 100644 index 0000000000000000000000000000000000000000..61fe23da5f7bd80c3974a176ae8a96a0729e1ea0 GIT binary patch literal 3888 zcmeHKX;4#H8htFT5OAcSScLT0A~LNIiH&R_wyh!~1=vH6IG}(m0V0SZO9%u?G{{83 zr5Q*A(a~|G**YKz!~hcnGzlTHBq5P4fe0jFCxk$l7Ztnx)j#IP%nxtXse9|yd$+!O z?)kp+yV#EypJCG9sqRz4kB=)F8CyN(0>&GjFjGoz70q%k-uRkZRhD8 zIG)P1uaVlgZxoL0+T_2LjRROmNcWp{^rFi6|N@Rj33L`86XK7Sl@4Z4*JssD}^O!38Om-zP@z+`(aeS4=fK zUB)^{B6dzl7;0YGc|X0s4_ zx&Wg)p1Hb`vebO>n!3htQa-_}9H3{Yam8$EvobEB*UzGUHpG@UCMc|YLU(qo4^O5C zAeRNRCza{Ft&f&l^54wr`*FD&oD^M7qR`sE$wi}ckR#C9dD3s`harN+bhe{ZP4P$ ze9yz+i|3qi>DobS;b-079j|vF&jZAZxWHwNdNNa^l3q=rB<`isHZ~@nE$fMuVvaD4 zBj!SES%*ns({f>5eyM)8W#gaY@oV8!Qy^Gm#K`#K4;SDp5;e`suiN8(qh@JhD&~$( z@~scNfR^uI4XBe-{NCqG<5h+@ zx)pKEm5&F|9{9y5TtVo0tM@AUhd~Y&M&xmVtRq``Fv3$!ghKBk#D}A3kMp~L|C4vy z)VMO)FvFL^o|6zUyY}(^jV?IZ4)3z`@uW>~m5Lu?$61$y{cpLxbvn@&WZUJ0jWX&V zqd1(d_UVujPZE!qNUd)XwIcXLTWcEoJpY<+pSk$2xhR2-+5kKDNNt);JSwCxs)7bn&XO5inqi@;=>R8`ZVbL)aZ})Ig-)SA@YF2}0n;SDXZUrZP{n zLWktVHwNwo&0);S(TpHf(pS0eITx3*5olPGQtKiz%k?G#8Ll?zb@laf)d)TD_VcsP zCjTPIsB>E;J1d`2RlN}9b=8!rJIk1iHV=;FRb?=#Vp6eeYn@wUHrd&o`??}*>eaBb zHhJ;6!Q7$1N5Y@-?lgn~vhW4agQ7TFl1`y!`dpFyJ`d}XQF?j5pv`@1(0p!CX;MT& z1h0zWsWD6q8CIOs8jJRQ7ODh1ceT}p7#2C3SX5zM#)wdM7I8_l=oS8xGNqg?; z@hl$_MTS*P>i%e_ars&&!2u*>`V^zz*@L3&&25OZhIVXHUyXYs+{v-)9}l-%=bnc2 z%2b3UL$V=Ss=yFYV5!|jM)6_T-}u@md4{iJq;}LteIdROU~2dhPP^Ypo~mJ(Vel`` zOvktGG&R90FBiKp&QvG#!cx5or6cl2IBkJAw{okCkLH^{kJ-ZM;RWHIQ`bB^HvUn6@4^HW1B%CYQZqvs%BiJ zYF*9VQ^@dgA8;8QEkZCJE1s|ke}k~na_`yL+CHj4EEXq-V;*vY!r>`77*OV|iD!9L zCMGV&?2)SP2I3Wa*0p{#mk$C<344ATy7-lUI1gs-m&&WUuZ-crj)GSI{bl_nRM|*Cq?NiOdc6+=lsCyc8KUMl_I+bmL0EH5 zZNE{CHfPDkv?E4w9N05s2*;+S_4T1Go3z3N+ltm%s$Q8x8oxlVY_R^F!Es1oZb6BH zj1x)e0$Izk19?d??S;^KqJ9Hl%VG{YUz75O7le4M!=b%2)lkpqd|JjUqsT{&@lYGS zMSgqG=4;K`y}?LReMyJ`u=O!7)P}y+%X~Utlg=02W1y_5h`gtlxG&+sEh}Fv;cV=E zyh&jJ*<)hF#fdlJ`;zd}E?|;1$x~GB{obJgQJe%FW$#lgmX?44*WUY)*B9377XA^u nr|>lg|JV!uwlDl2tTo2S=;u+R-KN@qW5D~c@1cr=p_l#xcDVQH literal 0 HcmV?d00001 diff --git a/src/azul/screenshots/Crash_at_Wed_Nov_13_16:20:10_2024.png b/src/azul/screenshots/Crash_at_Wed_Nov_13_16:20:10_2024.png new file mode 100644 index 0000000000000000000000000000000000000000..0d712ac54d6b6fef95167d14f1aa4de2d93bf541 GIT binary patch literal 3886 zcmeHKYgCfi8vfKN%j2cf98)Vb7c<(K%rqKOBe6zvoD#cCDxs-aFy$pfO9j!=Nhfbq zR_16pMMkN4iBbsxl}SSpa#GO*#lkzNA%cjPftoXGwbog4{+;>bx7PFLeZRH8y`TMj z&$IWHgK#e+gY^ag02q0H>v0$WbTc#vSf#6}oDm&d0s!L~Z;w6S#}`V4z31Ns=(-;Q zmmV3(A-_AfR`kVJ83v7(9(L`fpG^-OHSg`NJ1GQp<5a98dV3q!a|M}JrRh>V;Hr$- zz6Aj4?0CRc(BFXR<`Gj-U!*^Y4;UjNy()B+kuxD?C)b2N@V#&GLJSG}0h}4)b?RRU zD8M?w{wLv0n*&kc8a?X^NSuvRsgm{x5rrCh<(Re>_49EB$K+aC&usz9Z)i1QGtB{< zi*9m?wdFdrILizHmp{STUIxD~_j(&nJQ|Lf%j8loksyi`@>xhhD`a4##k~ls=bocQ z>-AUXqoZQz$CKxq$z(Et=X&Z)>{t5yv4<-nMT}O6LhdPrC`Lz5E7cb!Hcfx(H_Xd>IqMmIJDF9Vmo4?J4dmy#-%UsR|1LBu|z_}^|Q4BA06}D5bId0>>*ldXJlBGvTX2?mFu><}Rc-e_HBsB2y zng-_%`A+cLaT|lE50PIFG$rBJc3|ml*jpXV{~m8c#Y_>L=i$N8>-u+tOFQCttcuOl zSCd4vdT!|kG+B-y#tEu2JRPYPE~m;}<%~UxK$| z|Cqa-UqeuL+sFh@Ig!N7F7RvE>hzzMc4_^)WBy?aH6@>JG=j!gcP`4*lf7+A)PaT( z3m`>o0bmY9y?&OAzAyAK&glb%zHB4GnK7(IklE8qYKPwL)m!EGR7&kbJBsW0J)*k2 z4)i)MxMMoD2=D3IyOKClwtAf0%)~jCFw`G~1{|7&+4?6cF z`2&nZJTY+Z|Ve^$U|mW z0jk~ta_YpaL)XyxS$I3dDuJQat29<-@nC)TohOC34Jmr+ETYb)<`Ih=TwKGG87zIt zx`;@G2Xbk9rfGw9ND-(1BonzaEY3W9MwFb&84_>!j`($Y9r6u|;u6sEI(UNOm2{_^ z5RU7?aPi$w%M;tZ7kM74?&o}^UCkMRt=6W(FdNDTvo?ib_GywjOwKt`#vd2j97|qW zP{NkxV+M66&s3@>Myg41!yCs1;iTgiDs^wL!Gg{Jc^-a7uB5yNvVp&J6us5Wc@IkU${N zdS-cybhlY@VqzlH`7CouB%D-xf!(vAtf1M5@98v54{aM6M!VN@$PD(+ZaZEy{{pM| zQH~|p>aw@<4nqKwMMT^m9v&7>GT?BR+2QCCr@~f^!)xlHm74kj`(p8~_~@Azu7U{N~MzT+Bf~o8kRIauZoc$tN%=FQERE0?sEy&_TW1jy0&Yqrj^L(iKyvy@WCctz0_G#Sz1(2BK=pEhkhIj!1$RMzX^dbnPZPFw>(u3$y!Vc z&baK~!LH8hujhkIF{MqBAJa8WX*Ru{QTT$Yx(nv`5(ly$wP0bFi6{3@4og!$XM>^8 z195J0`W=5dGuw=S)#}(hJdP@B+~*gaelI@P3* z;!X1}IkLQDhWe{kI^Oy;n^1DKI#B<3l|8rQM3RhSZqRnP+uvd@F`wTjVv()zXBCIP zL8HVap;^DQ+UF3y8#*QDta2L-SckKxfU&5}tRLPc?%n>=a_B7w)~8m#%*Qex|6Kzr Z*u|0+Jcw}~SaS~syglI_<$FU?eg|T+PbmNZ literal 0 HcmV?d00001 diff --git a/src/azul/server.py b/src/azul/server.py new file mode 100755 index 0000000..cfe9eab --- /dev/null +++ b/src/azul/server.py @@ -0,0 +1,870 @@ +#!/usr/bin/env python3 +# Checkers Game Server + +"""Checkers Game Server.""" + +# Programmed by CoolCat467 + +from __future__ import annotations + +# Copyright (C) 2023-2024 CoolCat467 +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +__title__ = "Server" +__author__ = "CoolCat467" +__license__ = "GNU General Public License Version 3" +__version__ = "0.0.0" + +import time +import traceback +from collections import deque +from functools import partial +from typing import TYPE_CHECKING, NoReturn, cast + +import trio + +from azul import network +from azul.base_io import StructFormat +from azul.buffer import Buffer +from azul.component import ComponentManager, Event, ExternalRaiseManager +from azul.encrypted_event import EncryptedNetworkEventComponent +from azul.encryption import ( + RSAPrivateKey, + decrypt_token_and_secret, + generate_rsa_key, + generate_verify_token, + serialize_public_key, +) +from azul.network_shared import ( + ADVERTISEMENT_IP, + ADVERTISEMENT_PORT, + DEFAULT_PORT, + ClientBoundEvents, + ServerBoundEvents, + find_ip, +) +from azul.state import State, generate_pieces + +if TYPE_CHECKING: + from collections.abc import Awaitable, Callable, Iterable + + +class ServerClient(EncryptedNetworkEventComponent): + """Server Client Network Event Component. + + When clients connect to server, this class handles the incoming + connections to the server in the way of reading and raising events + that are transferred over the network. + """ + + __slots__ = ("client_id", "rsa_key", "verify_token") + + def __init__(self, client_id: int) -> None: + """Initialize Server Client.""" + self.client_id = client_id + super().__init__(f"client_{client_id}") + + self.timeout = 3 + + cbe = ClientBoundEvents + self.register_network_write_events( + { + "server[write]->encryption_request": cbe.encryption_request, + "server[write]->callback_ping": cbe.callback_ping, + "server[write]->initial_config": cbe.initial_config, + "server[write]->game_over": cbe.game_over, + "server[write]->playing_as": cbe.playing_as, + }, + ) + sbe = ServerBoundEvents + self.register_read_network_events( + { + sbe.encryption_response: f"client[{self.client_id}]->encryption_response", + }, + ) + + self.rsa_key: RSAPrivateKey | None = None + self.verify_token: bytes | None = None + + def bind_handlers(self) -> None: + """Bind event handlers.""" + super().bind_handlers() + self.register_handlers( + { + f"callback_ping->network[{self.client_id}]": self.handle_callback_ping, + f"client[{self.client_id}]->encryption_response": self.handle_encryption_response, + "initial_config->network": self.handle_initial_config, + "game_over->network": self.handle_game_over, + f"playing_as->network[{self.client_id}]": self.handle_playing_as, + }, + ) + + async def handle_game_over(self, event: Event[int]) -> None: + """Read game over event and reraise as server[write]->game_over.""" + winner = event.data + + buffer = Buffer() + + buffer.write_value(StructFormat.UBYTE, winner) + + await self.write_event(Event("server[write]->game_over", buffer)) + + async def handle_initial_config( + self, + event: Event[tuple[Pos, int]], + ) -> None: + """Read initial config event and reraise as server[write]->initial_config.""" + board_size, player_turn = event.data + + buffer = Buffer() + +## write_position(buffer, board_size) + buffer.write_value(StructFormat.UBYTE, 0) + buffer.write_value(StructFormat.UBYTE, player_turn) + + await self.write_event(Event("server[write]->initial_config", buffer)) + + async def handle_playing_as( + self, + event: Event[int], + ) -> None: + """Read playing as event and reraise as server[write]->playing_as.""" + playing_as = event.data + + buffer = Buffer() + buffer.write_value(StructFormat.UBYTE, playing_as) + await self.write_event(Event("server[write]->playing_as", buffer)) + + async def write_callback_ping(self) -> None: + """Write callback_ping packet to client. + + Could raise the following exceptions: + trio.BrokenResourceError: if something has gone wrong, and the stream + is broken. + trio.ClosedResourceError: if stream was previously closed + + Listed as possible but probably not because of write lock: + trio.BusyResourceError: if another task is using :meth:`write` + """ + buffer = Buffer() + + # Try to be as accurate with time as possible + await self.wait_write_might_not_block() + ns = int(time.time() * 1e9) + # Use as many bits as time needs, write_buffer handles size for us. + buffer.write(ns.to_bytes(-(-ns.bit_length() // 8), "big")) + + await self.write_event(Event("server[write]->callback_ping", buffer)) + + async def handle_callback_ping( + self, + _: Event[None], + ) -> None: + """Reraise as server[write]->callback_ping.""" + await self.write_callback_ping() + + async def start_encryption_request(self) -> None: + """Start encryption request and raise as server[write]->encryption_request.""" + if self.encryption_enabled: + raise RuntimeError("Encryption is already set up!") + self.rsa_key = generate_rsa_key() + self.verify_token = generate_verify_token() + + public_key = self.rsa_key.public_key() + + serialized_public_key = serialize_public_key(public_key) + + buffer = Buffer() + buffer.write_bytearray(serialized_public_key) + buffer.write_bytearray(self.verify_token) + + await self.write_event( + Event("server[write]->encryption_request", buffer), + ) + + event = await self.read_event() + if event.name != f"client[{self.client_id}]->encryption_response": + raise RuntimeError( + f"Expected encryption response, got but {event.name!r}", + ) + await self.handle_encryption_response(event) + + async def handle_encryption_response( + self, + event: Event[bytearray], + ) -> None: + """Read encryption response.""" + if self.rsa_key is None or self.verify_token is None: + raise RuntimeError( + "Was not expecting encryption response, request start not sent!", + ) + if self.encryption_enabled: + raise RuntimeError("Encryption is already set up!") + buffer = Buffer(event.data) + + encrypted_shared_secret = buffer.read_bytearray() + encrypted_verify_token = buffer.read_bytearray() + + verify_token, shared_secret = decrypt_token_and_secret( + self.rsa_key, + encrypted_verify_token, + encrypted_shared_secret, + ) + + if verify_token != self.verify_token: + raise RuntimeError( + "Received verify token does not match sent verify token!", + ) + + # Start encrypting all future data + self.enable_encryption(shared_secret, verify_token) + + +class GameServer(network.Server): + """Checkers server. + + Handles accepting incoming connections from clients and handles + main game logic via State subclass above. + """ + + __slots__ = ( + "actions_queue", + "advertisement_scope", + "client_count", + "client_players", + "internal_singleplayer_mode", + "player_selections", + "players_can_interact", + "running", + "state", + ) + + board_size = (8, 8) + max_clients = 4 + + def __init__(self, internal_singleplayer_mode: bool = False) -> None: + """Initialize server.""" + super().__init__("GameServer") + + self.client_count: int + self.state: CheckersState = State(self.board_size, {}) + + self.client_players: dict[int, int] = {} + self.player_selections: dict[int, Pos] = {} + self.players_can_interact: bool = False + + self.internal_singleplayer_mode = internal_singleplayer_mode + self.advertisement_scope: trio.CancelScope | None = None + self.running = False + + def bind_handlers(self) -> None: + """Register start_server and stop_server.""" + self.register_handlers( + { + "server_start": self.start_server, + "network_stop": self.stop_server, + "server_send_game_start": self.handle_server_start_new_game, + "network->select_piece": self.handle_network_select_piece, + "network->select_tile": self.handle_network_select_tile, + }, + ) + + async def stop_server(self, event: Event[None] | None = None) -> None: + """Stop serving and disconnect all NetworkEventComponents.""" + self.stop_serving() + self.stop_advertising() + + close_methods: deque[Callable[[], Awaitable[object]]] = deque() + for component in self.get_all_components(): + if isinstance(component, network.NetworkEventComponent): + close_methods.append(component.close) + print(f"stop_server {component.name = }") + self.remove_component(component.name) + async with trio.open_nursery() as nursery: + while close_methods: + nursery.start_soon(close_methods.popleft()) + self.running = False + + async def post_advertisement( + self, + udp_socket: trio.socket.SocketType, + send_to_ip: str, + hosting_port: int, + ) -> None: + """Post server advertisement packet.""" + motd = "Azul Game" + advertisement = ( + f"[AD]{hosting_port}[/AD][AZUL]{motd}[/AZUL]" + ).encode() + # print("post_advertisement") + await udp_socket.sendto( + advertisement, + (send_to_ip, ADVERTISEMENT_PORT), + ) + + def stop_advertising(self) -> None: + """Cancel self.advertisement_scope.""" + if self.advertisement_scope is None: + return + self.advertisement_scope.cancel() + + async def post_advertisements(self, hosting_port: int) -> None: + """Post lan UDP packets so server can be found.""" + self.stop_advertising() + self.advertisement_scope = trio.CancelScope() + + # Look up multicast group address in name server and find out IP version + addrinfo = (await trio.socket.getaddrinfo(ADVERTISEMENT_IP, None))[0] + send_to_ip = addrinfo[4][0] + + with trio.socket.socket( + family=trio.socket.AF_INET, # IPv4 + type=trio.socket.SOCK_DGRAM, # UDP + proto=trio.socket.IPPROTO_UDP, # UDP + ) as udp_socket: + # Set Time-to-live (optional) + # ttl_bin = struct.pack('@i', MYTTL) + # if addrinfo[0] == trio.socket.AF_INET: # IPv4 + # udp_socket.setsockopt( + # trio.socket.IPPROTO_IP, trio.socket.IP_MULTICAST_TTL, ttl_bin) + # else: + # udp_socket.setsockopt( + # trio.socket.IPPROTO_IPV6, trio.socket.IPV6_MULTICAST_HOPS, ttl_bin) + with self.advertisement_scope: + print("Starting advertisement posting.") + while True: # not self.can_start(): + try: + await self.post_advertisement( + udp_socket, + send_to_ip, + hosting_port, + ) + except OSError as exc: + traceback.print_exception(exc) + print( + f"{self.__class__.__name__}: Failed to post server advertisement", + ) + break + await trio.sleep(1.5) + print("Stopped advertisement posting.") + + @staticmethod + def setup_teams_internal(client_ids: list[int]) -> dict[int, int]: + """Return teams for internal server mode given sorted client ids.""" + players: dict[int, int] = {} + for idx, client_id in enumerate(client_ids): + if idx == 0: + players[client_id] = 2 + else: + players[client_id] = 0xFF # Spectator + return players + + @staticmethod + def setup_teams(client_ids: list[int]) -> dict[int, int]: + """Return teams given sorted client ids.""" + players: dict[int, int] = {} + for idx, client_id in enumerate(client_ids): + if idx < 2: + players[client_id] = idx % 2 + else: + players[client_id] = 0xFF # Spectator + return players + + def new_game_init(self) -> None: + """Start new game.""" + self.client_players.clear() + self.player_selections.clear() + + pieces = generate_pieces(*self.board_size) + self.state = State(self.board_size, pieces) + + # Why keep track of another object just to know client ID numbers + # if we already have that with the components? No need! + client_ids: set[int] = set() + for component in self.get_all_components(): + if isinstance(component, ServerClient): + client_ids.add(component.client_id) + + sorted_client_ids = sorted(client_ids) + if self.internal_singleplayer_mode: + self.client_players = self.setup_teams_internal(sorted_client_ids) + else: + self.client_players = self.setup_teams(sorted_client_ids) + + self.players_can_interact = True + + # "Implicit return in function which does not return" + async def start_server( # type: ignore[misc] + self, + event: Event[tuple[str | None, int]], + ) -> NoReturn: + """Serve clients.""" + print(f"{self.__class__.__name__}: Closing old server clients") + await self.stop_server() + print(f"{self.__class__.__name__}: Starting Server") + self.client_count = 0 + + host, port = event.data + + self.running = True + async with trio.open_nursery() as nursery: + # Do not post advertisements when using internal singleplayer mode + if not self.internal_singleplayer_mode: + nursery.start_soon(self.post_advertisements, port) + # Serve runs forever until canceled + nursery.start_soon(partial(self.serve, port, host, backlog=0)) + + async def transmit_playing_as(self) -> None: + """Transmit playing as.""" + async with trio.open_nursery() as nursery: + for client_id, team in self.client_players.items(): + nursery.start_soon( + self.raise_event, + Event(f"playing_as->network[{client_id}]", team), + ) + + async def handle_server_start_new_game(self, event: Event[None]) -> None: + """Handle game start.""" +## # Delete all pieces from last state (shouldn't be needed but still.) +## async with trio.open_nursery() as nursery: +## for piece_pos, _piece_type in self.state.get_pieces(): +## nursery.start_soon( +## self.raise_event, +## Event("delete_piece->network", piece_pos), +## ) + + # Choose which team plays first + # Using non-cryptographically secure random because it doesn't matter + self.new_game_init() + +## # Send create_piece events for all pieces +## async with trio.open_nursery() as nursery: +## for piece_pos, piece_type in self.state.get_pieces(): +## nursery.start_soon( +## self.raise_event, +## Event("create_piece->network", (piece_pos, piece_type)), +## ) + + await self.transmit_playing_as() + + # Raise initial config event with board size and initial turn. + await self.raise_event( + Event( + "initial_config->network", + (self.board_size, self.state.turn), + ), + ) + + async def client_network_loop(self, client: ServerClient) -> None: + """Network loop for given ServerClient. + + Could raise the following exceptions: + trio.BrokenResourceError: if something has gone wrong, and the stream + is broken. + trio.ClosedResourceError: if stream was previously closed + + Probably couldn't raise because of write lock but still: + trio.BusyResourceError: More than one task is trying to write + to socket at once. + """ + while not self.can_start() and not client.not_connected: + try: + await client.write_callback_ping() + except ( + trio.BrokenResourceError, + trio.ClosedResourceError, + network.NetworkStreamNotConnectedError, + ): + print(f"{client.name} Disconnected in lobby.") + return + while not client.not_connected: + event: Event[bytearray] | None = None + try: + await client.write_callback_ping() + with trio.move_on_after(2): + event = await client.read_event() + except network.NetworkTimeoutError: + print(f"{client.name} Timeout") + break + except network.NetworkEOFError: + print(f"{client.name} EOF") + break + except ( + trio.BrokenResourceError, + trio.ClosedResourceError, + RuntimeError, + ): + break + except Exception as exc: + traceback.print_exception(exc) + break + if event is not None: + # print(f"{client.name} client_network_loop tick") + # print(f"{client.name} {event = }") + await client.raise_event(event) + + def can_start(self) -> bool: + """Return if game can start.""" + if self.internal_singleplayer_mode: + return self.client_count >= 1 + return self.client_count >= 2 + + def game_active(self) -> bool: + """Return if game is active.""" + return self.state.check_for_win() is None + + async def send_spectator_join_packets( + self, + client: ServerClient, + ) -> None: + """Send spectator start data.""" + print("send_spectator_join_packets") + + private_events_pocket = ComponentManager( + f"private_events_pocket for {client.client_id}", + ) + with self.temporary_component(private_events_pocket): + with private_events_pocket.temporary_component(client): +## # Send create_piece events for all pieces +## async with trio.open_nursery() as nursery: +## for piece_pos, piece_type in self.state.get_pieces(): +## nursery.start_soon( +## client.raise_event, +## Event( +## "create_piece->network", +## (piece_pos, piece_type), +## ), +## ) + + await client.raise_event( + Event(f"playing_as->network[{client.client_id}]", 255), + ) + + # Raise initial config event with board size and initial turn. + await client.raise_event( + Event( + "initial_config->network", + (self.state.size, self.state.turn), + ), + ) + + async def handler(self, stream: trio.SocketStream) -> None: + """Accept clients. Called by network.Server.serve.""" + if self.client_count == 0 and self.game_active(): + # Old game was running but everyone left, restart + self.state.pieces.clear() + # self.state = CheckersState(self.board_size, {}) + new_client_id = self.client_count + print( + f"{self.__class__.__name__}: client connected [client_id {new_client_id}]", + ) + self.client_count += 1 + + can_start = self.can_start() + game_active = self.game_active() + # if can_start: + # self.stop_serving() + + if self.client_count > self.max_clients: + print( + f"{self.__class__.__name__}: client disconnected, too many clients", + ) + await stream.aclose() + self.client_count -= 1 + return + + async with ServerClient.from_stream( + new_client_id, + stream=stream, + ) as client: + # Encrypt traffic + await client.start_encryption_request() + assert client.encryption_enabled + + if can_start and game_active: + await self.send_spectator_join_packets(client) + with self.temporary_component(client): + if can_start and not game_active: + await self.raise_event( + Event("server_send_game_start", None), + ) + try: + await self.client_network_loop(client) + finally: + print( + f"{self.__class__.__name__}: client disconnected [client_id {new_client_id}]", + ) + self.client_count -= 1 + # ServerClient's `with` block handles closing stream. + + async def handle_network_select_piece( + self, + event: Event[tuple[int, Pos]], + ) -> None: + """Handle piece event from client.""" + client_id, tile_pos = event.data + + player = self.client_players.get(client_id, 0xFF) + if player == 2: + player = int(self.state.turn) + + if player != self.state.turn: + print( + f"{player = } cannot select piece {tile_pos = } because it is not that player's turn", + ) + return + + if not self.players_can_interact: + print( + f"{player = } cannot select piece {tile_pos = } because players_can_interact is False", + ) + return + if not self.state.can_player_select_piece(player, tile_pos): + print(f"{player = } cannot select piece {tile_pos = }") + await self.player_select_piece(player, None) + return + if tile_pos == self.player_selections.get(player): + # print(f"{player = } toggle select -> No select") + await self.player_select_piece(player, None) + return + + await self.player_select_piece(player, tile_pos) + + async def player_select_piece( + self, + player: int, + piece_pos: Pos | None, + ) -> None: + """Update glowing tiles from new selected piece.""" + ignore: set[Pos] = set() + + if piece_pos is not None: + # Calculate actions if required + new_action_set = self.state.calculate_actions(piece_pos) + ignore = new_action_set.ends + + ignored: set[Pos] = set() + + # Remove outlined tiles from previous selection if existed + if prev_selection := self.player_selections.get(player): + action_set = self.state.calculate_actions(prev_selection) + ignored = action_set.ends & ignore + remove = action_set.ends - ignore + async with trio.open_nursery() as nursery: + for tile_position in remove: + nursery.start_soon( + self.raise_event, + Event("delete_tile->network", tile_position), + ) + if piece_pos != prev_selection: + nursery.start_soon( + self.raise_event, + Event( + "select_piece->network", + (prev_selection, False), + ), + ) + + if piece_pos is None: + if prev_selection: + del self.player_selections[player] + return + + self.player_selections[player] = piece_pos + + # For each end point + async with trio.open_nursery() as nursery: + for tile_position in new_action_set.ends - ignored: + nursery.start_soon( + self.raise_event, + Event("create_tile->network", tile_position), + ) + # Sent select piece as well + nursery.start_soon( + self.raise_event, + Event( + "select_piece->network", + (self.player_selections[player], True), + ), + ) + + async def handle_move_animation(self, from_pos: Pos, to_pos: Pos) -> None: + """Handle move animation.""" + await self.raise_event( + Event("move_piece_animation->network", (from_pos, to_pos)), + ) + + async def handle_jump_animation(self, jumped_pos: Pos) -> None: + """Handle jump animation.""" + await self.raise_event( + Event("delete_piece_animation->network", jumped_pos), + ) + + async def handle_king_animation( + self, + kinged_pos: Pos, + piece_type: int, + ) -> None: + """Handle jump animation.""" + await self.raise_event( + Event("update_piece_animation->network", (kinged_pos, piece_type)), + ) + + async def handle_action_animations( + self, + actions: deque[tuple[str, Iterable[Pos | int]]], + ) -> None: + """Handle action animations.""" + while actions: + name, params = actions.popleft() + if name == "move": + await self.handle_move_animation( + *cast("Iterable[Pos]", params), + ) + elif name == "jump": + await self.handle_jump_animation( + *cast("Iterable[Pos]", params), + ) + elif name == "king": + await self.handle_king_animation( + *cast("tuple[Pos, int]", params), + ) + else: + raise NotImplementedError(f"Animation for action {name}") + + async def handle_network_select_tile( + self, + event: Event[tuple[int, Pos]], + ) -> None: + """Handle select tile event from network.""" + client_id, tile_pos = event.data + + player = self.client_players.get(client_id, 0xFF) + if player == 2: + player = int(self.state.turn) + + if not self.players_can_interact: + print( + f"{player = } cannot select tile {tile_pos = } because players_can_interact is False", + ) + return + + if player != self.state.turn: + print( + f"{player = } cannot select tile {tile_pos = } because it is not their turn.", + ) + return + + piece_pos = self.player_selections.get(player) + if piece_pos is None: + print( + f"{player = } cannot select tile {tile_pos = } because has no selection", + ) + return + + if tile_pos not in self.state.calculate_actions(piece_pos).ends: + print( + f"{player = } cannot select tile {piece_pos!r} because not valid move", + ) + return + + self.players_can_interact = False # No one moves during animation + # Send animation state start event + await self.raise_event(Event("animation_state->network", True)) + + # Remove tile sprites and glowing effect + await self.player_select_piece(player, None) + + action = self.state.action_from_points(piece_pos, tile_pos) + # print(f"{action = }") + + # Get new state after performing valid action + new_state = self.state.preform_action(action) + # Get action queue from old state + action_queue = self.state.get_action_queue() + self.state = new_state + + # Send action animations + await self.handle_action_animations(action_queue) + + # Send action complete event + await self.raise_event( + Event( + "action_complete->network", + (piece_pos, tile_pos, self.state.turn), + ), + ) + + win_value = self.state.check_for_win() + if win_value is not None: + # If we have a winner, send game over event. + await self.raise_event(Event("game_over->network", win_value)) + return + + # If not game over, allow interactions so next player can take turn + self.players_can_interact = True + await self.raise_event(Event("animation_state->network", False)) + + def __del__(self) -> None: + """Debug print.""" + print(f"del {self.__class__.__name__}") + super().__del__() + + +async def run_server( + server_class: type[GameServer], + host: str, + port: int, +) -> None: + """Run machine client and raise tick events.""" + async with trio.open_nursery() as main_nursery: + event_manager = ExternalRaiseManager( + "azul", + main_nursery, + ) + server = server_class() + event_manager.add_component(server) + + await event_manager.raise_event(Event("server_start", (host, port))) + while not server.running: + print("Server starting...") + await trio.sleep(1) + + print("\nServer running.") + + try: + while server.running: # noqa: ASYNC110 # sleep in while loop + # Process background tasks in the main nursery + await trio.sleep(0.01) + except KeyboardInterrupt: + print("\nClosing from keyboard interrupt.") + await server.stop_server() + server.unbind_components() + + +async def cli_run_async() -> None: + """Run game server.""" + host = await find_ip() + port = DEFAULT_PORT + await run_server(GameServer, host, port) + + +def cli_run() -> None: + """Run game server.""" + trio.run(cli_run_async) + + +if __name__ == "__main__": + cli_run() diff --git a/src/azul/sound.py b/src/azul/sound.py new file mode 100644 index 0000000..1576592 --- /dev/null +++ b/src/azul/sound.py @@ -0,0 +1,67 @@ +"""Sound - Play sounds.""" + +# Programmed by CoolCat467 + +from __future__ import annotations + +# Sound - Play sounds +# Copyright (C) 2024 CoolCat467 +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +__title__ = "sound" +__author__ = "CoolCat467" +__version__ = "0.0.0" +__license__ = "GNU General Public License Version 3" + + +from typing import TYPE_CHECKING, NamedTuple + +from pygame import mixer + +if TYPE_CHECKING: + from os import PathLike + + +class SoundData(NamedTuple): + """Sound data container.""" + + loops: int = 0 + maxtime: int = 0 + fade_ms: int = 0 + volume: int = 100 + # volume_left: int = 100 + # volume_right: int = 100 + + +def play_sound( # pragma: nocover + filename: PathLike[str] | str, + sound_data: SoundData, +) -> tuple[mixer.Sound, int | float]: + """Play sound with pygame.""" + sound_object = mixer.Sound(filename) + sound_object.set_volume(sound_data.volume) + seconds: int | float = sound_object.get_length() + if sound_data.maxtime > 0: + seconds = sound_data.maxtime + _channel = sound_object.play( + loops=sound_data.loops, + maxtime=sound_data.maxtime, + fade_ms=sound_data.fade_ms, + ) + # channel.set_volume( + # sound_data.volume_left, + # sound_data.volume_right, + # ) + return sound_object, seconds diff --git a/src/azul/state.py b/src/azul/state.py new file mode 100644 index 0000000..114a6a9 --- /dev/null +++ b/src/azul/state.py @@ -0,0 +1,461 @@ +"""Checkers State.""" + +# Programmed by CoolCat467 + +from __future__ import annotations + +# Copyright (C) 2023-2024 CoolCat467 +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +__title__ = "Checkers State" +__author__ = "CoolCat467" +__license__ = "GNU General Public License Version 3" +__version__ = "0.0.0" + +import copy +import math +from dataclasses import dataclass +from typing import ( + TYPE_CHECKING, + NamedTuple, + TypeAlias, + TypeVar, + cast, +) + +from mypy_extensions import u8 + +if TYPE_CHECKING: + from collections.abc import Callable, Generator, Iterable + + from typing_extensions import Self + +MANDATORY_CAPTURE = True # If a jump is available, do you have to or not? +PAWN_JUMP_FORWARD_ONLY = True # Pawns not allowed to go backwards in jumps? + +# Note: Tile Ids are chess board tile titles, A1 to H8 +# A8 ... H8 +# ......... +# A1 ... H1 + +# Player: +# 0 = False = Red = MIN = 0, 2 +# 1 = True = Black = MAX = 1, 3 + +T = TypeVar("T") + +Pos: TypeAlias = tuple[u8, u8] + + +class Action(NamedTuple): + """Represents an action.""" + + from_pos: Pos + to_pos: Pos + + +class ActionSet(NamedTuple): + """Represents a set of actions.""" + + jumps: dict[Pos, list[Pos]] + moves: tuple[Pos, ...] + ends: set[Pos] + + +def get_sides(xy: Pos) -> tuple[Pos, Pos, Pos, Pos]: + """Return the tile xy coordinates on the top left, top right, bottom left, and bottom right sides of given xy coordinates.""" + cx, cy = xy + sides = [] + for raw_dy in range(2): + dy = raw_dy * 2 - 1 + ny = cy + dy + for raw_dx in range(2): + dx = raw_dx * 2 - 1 + nx = cx + dx + sides.append((nx, ny)) + tuple_sides = tuple(sides) + assert len(tuple_sides) == 4 + return cast(tuple[Pos, Pos, Pos, Pos], tuple_sides) + + +def pawn_modify(moves: tuple[T, ...], piece_type: u8) -> tuple[T, ...]: + """Return moves but remove invalid moves for pawns.""" + assert ( + len(moves) == 4 + ), "Tuple size MUST be four for this to return valid results!" + if ( + piece_type == 0 + ): # If it's a white pawn, it can only move to top left and top right + return moves[:2] + if ( + piece_type == 1 + ): # If it's a black pawn, it can only move to bottom left anf bottom right + return moves[2:] + return moves + + +@dataclass(slots=True) +class State: + """Represents state of checkers game.""" + + size: tuple[int, int] + pieces: dict[Pos, int] + turn: bool = True # Black moves first + + def __str__(self) -> str: + """Return text representation of game board state.""" + map_ = {None: " ", 0: "-", 1: "+", 2: "O", 3: "X"} + w, h = self.size + lines = [] + for y in range(h): + line = [] + for x in range(w): + if (x + y + 1) % 2: + # line.append("_") + line.append(" ") + continue + line.append(map_[self.pieces.get((x, y))]) + lines.append("".join(line)) + # lines.append(" | ".join(line)) + # lines.append("--+-"*(w-1)+"-") + return "\n".join(lines) + + def calculate_actions(self, position: Pos) -> ActionSet: + """Return actions the piece at given position can make.""" + if MANDATORY_CAPTURE: + exists = False + for start, _end in self.get_all_actions(self.pieces[position]): + if start == position: + exists = True + break + if not exists: + return ActionSet({}, (), set()) + jumps = self.get_jumps(position) + moves: tuple[Pos, ...] + moves = () if MANDATORY_CAPTURE and jumps else self.get_moves(position) + ends = set(jumps) + ends.update(moves) + return ActionSet(jumps, moves, ends) + + def piece_kinged(self, piece_pos: Pos, new_type: int) -> None: + """Piece kinged.""" + # print(f'piece_kinged {piece = }') + + def piece_moved(self, start_pos: Pos, end_pos: Pos) -> None: + """Piece moved from start_pos to end_pos.""" + + def piece_jumped(self, jumped_piece_pos: Pos) -> None: + """Piece has been jumped.""" + # print(f'piece_jumped {position = }') + + def preform_action(self, action: Action) -> Self: + """Return new state after performing action on self.""" + from_pos, to_pos = action + + pieces_copy = dict(self.pieces.items()) + + # Remove piece from it's start position + piece_type = pieces_copy.pop(from_pos) + + # See if it's a jump + if to_pos not in self.get_moves(from_pos): + # Jumps are more complex to calculate and we need + # to know what pieces got jumped over + cur_x, cur_y = from_pos + for jumped_pos in self.get_jumps(from_pos)[to_pos]: + from_pos = (cur_x, cur_y) + + # Remove jumped position from pieces in play + if jumped_pos in pieces_copy: + pieces_copy.pop(jumped_pos) + self.piece_jumped(jumped_pos) + # See if piece kinged + jumped_x, jumped_y = jumped_pos + # Rightshift 1 is more efficiant way to multiply by 2 + cur_x += (jumped_x - cur_x) << 1 + cur_y += (jumped_y - cur_y) << 1 + + self.piece_moved(from_pos, (cur_x, cur_y)) + + # Now that we know the current position, see if kinged + if self.does_piece_king(piece_type, (cur_x, cur_y)): + piece_type += 2 + self.piece_kinged((cur_x, cur_y), piece_type) + else: + self.piece_moved(from_pos, to_pos) + + # See if it kings and king it if so + if self.does_piece_king(piece_type, to_pos): + piece_type += 2 + self.piece_kinged(to_pos, piece_type) + + # Move piece to it's end position + pieces_copy[to_pos] = piece_type + + # Swap turn + return self.__class__( + self.size, + pieces_copy, + not self.turn, + ) + + def get_tile_name(self, x: int, y: int) -> str: + """Return name of a given tile.""" + return chr(65 + x) + str(self.size[1] - y) + + @staticmethod + def action_from_points(start: Pos, end: Pos) -> Action: + """Return action from given start and end coordinates.""" + # return Action(self.get_tile_name(*start), self.get_tile_name(*end)) + return Action(start, end) + + def get_turn(self) -> int: + """Return whose turn it is. 0 = red, 1 = black.""" + return int(self.turn) + + def valid_location(self, position: Pos) -> bool: + """Return if position is valid.""" + x, y = position + w, h = self.size + return x >= 0 and y >= 0 and x < w and y < h + + def does_piece_king(self, piece_type: int, position: Pos) -> bool: + """Return if piece needs to be kinged given it's type and position.""" + _, y = position + _, h = self.size + return (piece_type == 0 and y == 0) or (piece_type == 1 and y == h - 1) + + @staticmethod + def get_enemy(self_type: int) -> int: + """Return enemy pawn piece type.""" + # If we are kinged, get a pawn version of ourselves. + # Take that plus one mod 2 to get the pawn of the enemy + return (self_type + 1) % 2 + + @staticmethod + def get_piece_types(self_type: int) -> tuple[int, int]: + """Return piece types of given piece type.""" + # If we are kinged, get a pawn version of ourselves. + self_pawn = self_type % 2 + return (self_pawn, self_pawn + 2) + + def get_jumps( + self, + position: Pos, + piece_type: int | None = None, + _pieces: dict[Pos, int] | None = None, + _recursion: int = 0, + ) -> dict[Pos, list[Pos]]: + """Return valid jumps a piece can make. + + position is a xy coordinate tuple pointing to a board position + that may or may not have a piece on it. + piece_type is the piece type at position. If not + given, position must point to a tile with a piece on it + + Returns dictionary that maps end positions to + jumped pieces to get there + """ + if piece_type is None: + piece_type = self.pieces[position] + if _pieces is None: + _pieces = self.pieces + _pieces = copy.deepcopy(_pieces) + + enemy_pieces = self.get_piece_types(self.get_enemy(piece_type)) + + # Get the side coordinates of the tile and make them tuples so + # the scan later works properly. + sides = get_sides(position) + # Make a dictionary to find what direction a tile is in if you + # give it the tile. + # end position : jumped pieces + + # Make a dictionary for the valid jumps and the pieces they jump + valid: dict[Pos, list[Pos]] = {} + + valid_sides: tuple[tuple[int, Pos], ...] + if PAWN_JUMP_FORWARD_ONLY: + valid_sides = pawn_modify( + tuple(enumerate(sides)), + piece_type, + ) + else: + valid_sides = tuple(enumerate(sides)) + + # For each side tile in the jumpable tiles for this type of piece, + for direction, side in valid_sides: + # Make sure side exists + if not self.valid_location(side): + continue + side_piece = _pieces.get(side) + # Side piece must be one of our enemy's pieces + if side_piece not in enemy_pieces: + continue + # Get the direction from the dictionary we made earlier + # Get the coordinates of the tile on the side of the main tile's + # side in the same direction as the main tile's side + side_side = get_sides(side)[direction] + # Make sure side exists + if not self.valid_location(side_side): + continue + side_side_piece = _pieces.get(side_side) + # If the side is open, + if side_side_piece is None: + # Add it the valid jumps dictionary and add the tile + # to the list of end tiles. + valid[side_side] = [side] + + # Remove jumped piece from future calculations + _pieces.pop(side) + + # For each end point tile in the list of end point tiles, + for end_tile in tuple(valid): + # Get the dictionary from the jumps you could make + # from that end tile + w, h = self.size + if _recursion + 1 > math.ceil((w**2 + h**2) ** 0.25): + break + # If the piece has made it to the opposite side, + piece_type_copy = piece_type + if self.does_piece_king(piece_type_copy, end_tile): + # King that piece + piece_type_copy += 2 + _recursion = -1 + add_valid = self.get_jumps( + end_tile, + piece_type_copy, + _pieces=_pieces, + _recursion=_recursion + 1, + ) + # For each key in the new dictionary of valid tile's keys, + for end_pos, jumped_pieces in add_valid.items(): + # If the key is not already existent in the list of + # valid destinations, + if end_pos not in valid: + # Add that destination to the dictionary and every + # tile you have to jump to get there. + no_duplicates = [ + p for p in jumped_pieces if p not in valid[end_tile] + ] + valid[end_pos] = valid[end_tile] + no_duplicates + + return valid + + def get_moves(self, position: Pos) -> tuple[Pos, ...]: + """Return valid moves piece at position can make, not including jumps.""" + piece_type = self.pieces[position] + # Get the side xy choords of the tile's xy pos, + # then modify results for pawns + moves = pawn_modify(get_sides(position), piece_type) + return tuple( + m + for m in filter(self.valid_location, moves) + if m not in self.pieces + ) + + @classmethod + def wrap_actions( + cls, + position: Pos, + calculate_ends: Callable[[Pos], Iterable[Pos]], + ) -> Generator[Action, None, None]: + """Yield end calculation function results as Actions.""" + for end in calculate_ends(position): + yield cls.action_from_points(position, end) + + def get_actions(self, position: Pos) -> Generator[Action, None, None]: + """Yield all moves and jumps the piece at position can make.""" + ends = set(self.get_jumps(position)) + if not (ends and MANDATORY_CAPTURE): + ends.update(self.get_moves(position)) + for end in ends: + yield self.action_from_points(position, end) + + def get_all_actions(self, player: int) -> Generator[Action, None, None]: + """Yield all actions for given player.""" + player_pieces = {player, player + 2} + if not MANDATORY_CAPTURE: + for position, piece_type in self.pieces.items(): + if piece_type not in player_pieces: + continue + yield from self.get_actions(position) + return + jumps_available = False + for position, piece_type in self.pieces.items(): + if piece_type not in player_pieces: + continue + if not jumps_available: + for jump in self.wrap_actions(position, self.get_jumps): + yield jump + jumps_available = True + else: + yield from self.wrap_actions(position, self.get_jumps) + if not jumps_available: + for position, piece_type in self.pieces.items(): + if piece_type not in player_pieces: + continue + yield from self.wrap_actions(position, self.get_moves) + + def check_for_win(self) -> int | None: + """Return player number if they won else None.""" + # For each of the two players, + for player in range(2): + # For each tile in the playable tiles, + has_move = False + for _ in self.get_all_actions(player): + has_move = True + # Player has at least one move, no need to continue + break + if not has_move and self.turn == bool(player): + # Continued without break, so player either has no moves + # or no possible moves, so their opponent wins + return (player + 1) % 2 + return None + + def can_player_select_piece(self, player: int, tile_pos: Pos) -> bool: + """Return True if player can select piece on given tile position.""" + piece_at_pos = self.pieces.get(tile_pos) + if piece_at_pos is None: + return False + return (piece_at_pos % 2) == player + + def get_pieces(self) -> tuple[tuple[Pos, int], ...]: + """Return all pieces.""" + return tuple((pos, type_) for pos, type_ in self.pieces.items()) + + +def generate_pieces( + board_width: int, + board_height: int, + colors: int = 2, +) -> dict[Pos, int]: + """Generate data about each piece.""" + pieces: dict[Pos, int] = {} + # Get where pieces should be placed + z_to_1 = round(board_height / 3) # White + z_to_2 = (board_height - (z_to_1 * 2)) + z_to_1 # Black + # For each xy position in the area of where tiles should be, + for y in range(board_height): + # Reset the x pos to 0 + for x in range(board_width): + # Get the color of that spot by adding x and y mod the number of different colors + color = (x + y + 1) % colors + # If a piece should be placed on that tile and the tile is not Red, + if (not color) and ((y <= z_to_1 - 1) or (y >= z_to_2)): + # Set the piece to White Pawn or Black Pawn depending on the current y pos + piece_type = int(y <= z_to_1) + pieces[x, y] = piece_type + return pieces From 3f20f2d0fb68784e428da6327dbfb8756efe2d01 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Wed, 13 Nov 2024 19:29:41 -0600 Subject: [PATCH 02/58] Start converting to event based and state module --- .pre-commit-config.yaml | 2 +- computer_players/MiniMax_AI.py | 136 +++ computer_players/machine_client.py | 253 ++++ computer_players/minimax.py | 364 ++++++ src/azul/client.py | 139 --- src/azul/component.py | 56 + src/azul/game.py | 1791 ++++++++++++---------------- src/azul/server.py | 133 +-- src/azul/sprite.py | 22 +- src/azul/state.py | 1405 +++++++++++++++------- src/azul/tools.py | 44 +- 11 files changed, 2652 insertions(+), 1693 deletions(-) create mode 100755 computer_players/MiniMax_AI.py create mode 100644 computer_players/machine_client.py create mode 100644 computer_players/minimax.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 77f196e..5da692c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.7.3 + rev: v0.7.4 hooks: - id: ruff types: [file] diff --git a/computer_players/MiniMax_AI.py b/computer_players/MiniMax_AI.py new file mode 100755 index 0000000..6bf8909 --- /dev/null +++ b/computer_players/MiniMax_AI.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +# AI that plays checkers. + +"""Minimax Checkers AI.""" + +from __future__ import annotations + +# Programmed by CoolCat467 + +__title__ = "Minimax AI" +__author__ = "CoolCat467" +__version__ = "0.0.0" + +from typing import TYPE_CHECKING, TypeAlias, TypeVar + +##from machine_client import RemoteState, run_clients_in_local_servers_sync +from minimax import Minimax, Player + +from azul.state import ( + Phase, + SelectableDestinationTiles, + SelectableSourceTiles, + State, +) + +if TYPE_CHECKING: + from collections.abc import Iterable + +T = TypeVar("T") +Action: TypeAlias = ( + tuple[SelectableDestinationTiles, ...] + | tuple[SelectableSourceTiles, tuple[SelectableDestinationTiles, ...]] +) + +# Player: +# 0 = False = Person = MIN = 0, 2 +# 1 = True = AI (Us) = MAX = 1, 3 + + +class AzulMinimax(Minimax[State, Action]): + """Minimax Algorithm for Checkers.""" + + __slots__ = () + + @staticmethod + def value(state: State) -> int | float: + """Return value of given game state.""" + # Real + real_state, max_player = state + if AzulMinimax.terminal(state): + winner, _score = real_state.get_win_order()[0] + if winner == max_player: + return 1 + return -1 + # Heuristic + min_ = 0 + max_ = 0 + for player_id, player_data in real_state.player_data.items(): + score = player_data.get_end_of_game_score() + if player_id == max_player: + max_ += score + else: + min_ += score + # More max will make score higher, + # more min will make score lower + # Plus one in divisor makes so never / 0 + return (max_ - min_) / (max_ + min_ + 1) + + @staticmethod + def terminal(state: State) -> bool: + """Return if game state is terminal.""" + real_state, _max_player = state + return real_state.current_phase == Phase.end + + @staticmethod + def player(state: State) -> Player: + """Return Player enum from current state's turn.""" + real_state, max_player = state + return ( + Player.MAX if real_state.current_turn == max_player else Player.MIN + ) + + @staticmethod + def actions(state: State) -> Iterable[Action]: + """Return all actions that are able to be performed for the current player in the given state.""" + real_state, _max_player = state + return tuple(real_state.yield_actions()) + ## print(f'{len(actions) = }') + + @staticmethod + def result(state: State, action: Action) -> State: + """Return new state after performing given action on given current state.""" + real_state, max_player = state + return (real_state.preform_action(action), max_player) + + +##class MinimaxPlayer(RemoteState): +## """Minimax Player.""" +## +## __slots__ = () +## +## async def preform_turn(self) -> Action: +## """Perform turn.""" +## print("preform_turn") +## ##value, action = CheckersMinimax.adaptive_depth_minimax( +## ## self.state, 4, 5 +## ##) +## ##value, action = CheckersMinimax.minimax(self.state, 4) +## value, action = CheckersMinimax.alphabeta(self.state, 4) +## if action is None: +## raise ValueError("action is None") +## print(f"{value = }") +## return action + + +def run() -> None: + """Run MinimaxPlayer clients in local server.""" + import random + + random.seed(0) + + state = (State.new_game(2), 0) + + while not AzulMinimax.terminal(state): + action = AzulMinimax.alphabeta(state, 2) + print(f"{action.value = }") + state = AzulMinimax.result(state, action.action) + print(state) + + +## run_clients_in_local_servers_sync(MinimaxPlayer) + + +if __name__ == "__main__": + print(f"{__title__} v{__version__}\nProgrammed by {__author__}.\n") + run() diff --git a/computer_players/machine_client.py b/computer_players/machine_client.py new file mode 100644 index 0000000..dac52f7 --- /dev/null +++ b/computer_players/machine_client.py @@ -0,0 +1,253 @@ +"""Machine Client - Checkers game client that can be controlled mechanically.""" + +from __future__ import annotations + +__title__ = "Machine Client" +__author__ = "CoolCat467" +__version__ = "0.0.0" + +import sys +from abc import ABCMeta, abstractmethod +from contextlib import asynccontextmanager +from typing import TYPE_CHECKING + +import trio +from checkers.client import GameClient, read_advertisements +from checkers.component import ( + Component, + ComponentManager, + Event, + ExternalRaiseManager, +) +from checkers.state import Action, Pos, State + +if TYPE_CHECKING: + from collections.abc import AsyncGenerator + +if sys.version_info < (3, 11): + from exceptiongroup import BaseExceptionGroup + +# Player: +# 0 = False = Person = MIN = 0, 2 +# 1 = True = AI (Us) = MAX = 1, 3 + + +class RemoteState(Component, metaclass=ABCMeta): + """Remote State. + + Keeps track of game state and call preform_action when it's this clients + turn. + """ + + __slots__ = ("state", "pieces", "has_initial", "playing_as", "moves") + + def __init__(self) -> None: + """Initialize remote state.""" + super().__init__("remote_state") + + self.state = State((8, 8), {}) + self.has_initial = False + self.pieces: dict[Pos, int] = {} + + self.playing_as = 1 + self.moves = 0 + + def bind_handlers(self) -> None: + """Register game event handlers.""" + self.register_handlers( + { + "game_action_complete": self.handle_action_complete, + "game_winner": self.handle_game_over, + "game_initial_config": self.handle_initial_config, + "game_playing_as": self.handle_playing_as, + "gameboard_create_piece": self.handle_create_piece, + }, + ) + + async def preform_action(self, action: Action) -> None: + """Raise events to perform game action.""" + await self.raise_event( + Event( + "gameboard_piece_clicked", + ( + action.from_pos, + self.state.pieces[action.from_pos], + ), + ), + ) + await self.raise_event(Event("gameboard_tile_clicked", action.to_pos)) + + @abstractmethod + async def preform_turn(self) -> Action: + """Perform turn, return action to perform.""" + + async def base_preform_turn(self) -> None: + """Perform turn.""" + self.moves += 1 + winner = self.state.check_for_win() + if winner is not None: + print("Terminal state, not performing turn") + value = ("Lost", "Won")[winner == self.playing_as] + print(f"{value} after {self.moves}") + return + action = await self.preform_turn() + await self.preform_action(action) + + async def handle_action_complete( + self, + event: Event[tuple[Pos, Pos, int]], + ) -> None: + """Perform action on internal state and perform our turn if possible.""" + from_pos, to_pos, turn = event.data + action = self.state.action_from_points(from_pos, to_pos) + self.state = self.state.preform_action(action) + ## print(f'{turn = }') + if turn == self.playing_as: + await self.base_preform_turn() + + async def handle_create_piece(self, event: Event[tuple[Pos, int]]) -> None: + """Update internal pieces if we haven't had the initial setup event.""" + if self.has_initial: + return + pos, type_ = event.data + self.pieces[pos] = type_ + + async def handle_playing_as(self, event: Event[int]) -> None: + """Handle playing as event.""" + self.playing_as = event.data + + async def handle_initial_config( + self, + event: Event[tuple[Pos, int]], + ) -> None: + """Set up initial state and perform our turn if possible.""" + board_size, turn = event.data + self.state = State(board_size, self.pieces, bool(turn)) + self.has_initial = True + if turn == self.playing_as: + await self.base_preform_turn() + + async def handle_game_over(self, event: Event[int]) -> None: + """Raise network_stop event so we disconnect from server.""" + self.has_initial = False + await self.raise_event(Event("network_stop", None)) + + +class MachineClient(ComponentManager): + """Manager that runs until client_disconnected event fires.""" + + __slots__ = ("running",) + + def __init__(self, remote_state_class: type[RemoteState]) -> None: + """Initialize machine client.""" + super().__init__("machine_client") + + self.running = True + + self.add_component(remote_state_class()) + + @asynccontextmanager + async def client_with_block(self) -> AsyncGenerator[GameClient, None]: + """Add client temporarily with `with` block, ensuring closure.""" + async with GameClient("game_client") as client: + with self.temporary_component(client): + yield client + + def bind_handlers(self) -> None: + """Register client event handlers.""" + self.register_handlers( + { + "client_disconnected": self.handle_client_disconnected, + "client_connection_closed": self.handle_client_disconnected, + }, + ) + + ## async def raise_event(self, event: Event) -> None: + ## """Raise event but also log it if not tick.""" + ## if event.name not in {"tick"}: + ## print(f'{event = }') + ## return await super().raise_event(event) + + async def handle_client_disconnected(self, event: Event[None]) -> None: + """Set self.running to false on network disconnect.""" + self.running = False + + +async def run_client( + host: str, + port: int, + remote_state_class: type[RemoteState], + connected: set[tuple[str, int]], +) -> None: + """Run machine client and raise tick events.""" + async with trio.open_nursery() as main_nursery: + event_manager = ExternalRaiseManager( + "checkers", + main_nursery, + "client", + ) + client = MachineClient(remote_state_class) + with event_manager.temporary_component(client): + async with client.client_with_block(): + await event_manager.raise_event( + Event("client_connect", (host, port)), + ) + print(f"Connected to server {host}:{port}") + try: + while client.running: # noqa: ASYNC110 + # Wait so backlog things happen + await trio.sleep(1) + except KeyboardInterrupt: + print("Shutting down client from keyboard interrupt.") + await event_manager.raise_event( + Event("network_stop", None), + ) + print(f"Disconnected from server {host}:{port}") + client.unbind_components() + connected.remove((host, port)) + + +def run_client_sync( + host: str, + port: int, + remote_state_class: type[RemoteState], +) -> None: + """Run client and connect to server at host:port.""" + trio.run(run_client, host, port, remote_state_class, set()) + + +async def run_clients_in_local_servers( + remote_state_class: type[RemoteState], +) -> None: + """Run clients in local servers.""" + connected: set[tuple[str, int]] = set() + print("Watching for advertisements...\n(CTRL + C to quit)") + try: + async with trio.open_nursery(strict_exception_groups=True) as nursery: + while True: + advertisements = set(await read_advertisements()) + servers = {server for _motd, server in advertisements} + servers -= connected + for server in servers: + connected.add(server) + nursery.start_soon( + run_client, + *server, + remote_state_class, + connected, + ) + await trio.sleep(1) + except BaseExceptionGroup as exc: + for ex in exc.exceptions: + if isinstance(ex, KeyboardInterrupt): + print("Shutting down from keyboard interrupt.") + break + else: + raise + + +def run_clients_in_local_servers_sync( + remote_state_class: type[RemoteState], +) -> None: + """Run clients in local servers.""" + trio.run(run_clients_in_local_servers, remote_state_class) diff --git a/computer_players/minimax.py b/computer_players/minimax.py new file mode 100644 index 0000000..c3a325f --- /dev/null +++ b/computer_players/minimax.py @@ -0,0 +1,364 @@ +"""Minimax - Boilerplate code for Minimax AIs.""" + +from __future__ import annotations + +# Programmed by CoolCat467 + +__title__ = "Minimax" +__author__ = "CoolCat467" +__version__ = "0.0.0" + +import operator +import random +from abc import ABC, abstractmethod +from enum import IntEnum, auto +from math import inf as infinity +from typing import TYPE_CHECKING, Generic, NamedTuple, TypeVar + +if TYPE_CHECKING: + from collections.abc import Iterable + + +class Player(IntEnum): + """Enum for player status.""" + + __slots__ = () + MIN = auto() + MAX = auto() + CHANCE = auto() + + +State = TypeVar("State") +Action = TypeVar("Action") + + +class MinimaxResult(NamedTuple, Generic[Action]): + """Minimax Result.""" + + value: int | float + action: Action | None + + +class Minimax(ABC, Generic[State, Action]): + """Base class for Minimax AIs.""" + + __slots__ = () + + @classmethod + @abstractmethod + def value(cls, state: State) -> int | float: + """Return the value of a given game state.""" + + @classmethod + @abstractmethod + def terminal(cls, state: State) -> bool: + """Return if given game state is terminal.""" + + @classmethod + @abstractmethod + def player(cls, state: State) -> Player: + """Return player status given the state of the game. + + Must return either Player.MIN or Player.MAX + """ + + @classmethod + @abstractmethod + def actions(cls, state: State) -> Iterable[Action]: + """Return a collection of all possible actions in a given game state.""" + + @classmethod + @abstractmethod + def result(cls, state: State, action: Action) -> State: + """Return new game state after performing action on given state.""" + + @classmethod + def minimax( + cls, + state: State, + depth: int | None = 5, + ) -> MinimaxResult[Action]: + """Return minimax result best action for a given state for the current player.""" + if cls.terminal(state): + return MinimaxResult(cls.value(state), None) + if depth is not None and depth <= 0: + # Choose a random action + # No need for cryptographic secure random + return MinimaxResult( + cls.value(state), + random.choice(tuple(cls.actions(state))), # noqa: S311 + ) + next_down = None if depth is None else depth - 1 + + current_player = cls.player(state) + value: int | float + if current_player == Player.MAX: + value = -infinity + best = max + elif current_player == Player.MIN: + value = infinity + best = min + elif current_player == Player.CHANCE: + raise ValueError("CHANCE is not valid for regular minimax.") + else: + raise ValueError(f"Unexpected player type {current_player!r}") + + best_action: Action | None = None + for action in cls.actions(state): + result = cls.minimax(cls.result(state, action), next_down) + new_value = best(value, result.value) + if new_value != value: + best_action = action + value = new_value + return MinimaxResult(value, best_action) + + @classmethod + def alphabeta( + cls, + state: State, + depth: int | None = 5, + a: int | float = -infinity, + b: int | float = infinity, + ) -> MinimaxResult[Action]: + """Return minimax alphabeta pruning result best action for given current state.""" + # print(f'alphabeta {depth = } {a = } {b = }') + + if cls.terminal(state): + return MinimaxResult(cls.value(state), None) + if depth is not None and depth <= 0: + # Choose a random action + # No need for cryptographic secure random + return MinimaxResult( + cls.value(state), + random.choice(tuple(cls.actions(state))), # noqa: S311 + ) + next_down = None if depth is None else depth - 1 + + current_player = cls.player(state) + value: int | float + if current_player == Player.MAX: + value = -infinity + best = max + compare = operator.gt # greater than (>) + set_idx = 0 + elif current_player == Player.MIN: + value = infinity + best = min + compare = operator.lt # less than (<) + set_idx = 1 + elif current_player == Player.CHANCE: + raise ValueError("CHANCE is not valid for regular minimax.") + else: + raise ValueError(f"Unexpected player type {current_player!r}") + + best_action: Action | None = None + for action in cls.actions(state): + result = cls.alphabeta(cls.result(state, action), next_down, a, b) + new_value = best(value, result.value) + + if new_value != value: + best_action = action + value = new_value + + if compare(new_value, (a, b)[set_idx ^ 1]): + # print("cutoff") + break # cutoff + + alpha_beta_value = (a, b)[set_idx] + new_alpha_beta_value = best(alpha_beta_value, value) + + if new_alpha_beta_value != alpha_beta_value: + # Set new best + alpha_beta_list = [a, b] + alpha_beta_list[set_idx] = new_alpha_beta_value + a, b = alpha_beta_list + return MinimaxResult(value, best_action) + + +class Expectiminimax(ABC, Generic[State, Action]): + """Base class for Expectiminimax AIs.""" + + __slots__ = () + + LOWEST = -1 + HIGHEST = 1 + + @classmethod + @abstractmethod + def value(cls, state: State) -> int | float: + """Return the value of a given game state.""" + + @classmethod + @abstractmethod + def terminal(cls, state: State) -> bool: + """Return if given game state is terminal.""" + + @classmethod + @abstractmethod + def player(cls, state: State) -> Player: + """Return player status given the state of the game. + + Must return either Player.MIN or Player.MAX, or Player.CHANCE + if there is a random action. + """ + + @classmethod + @abstractmethod + def actions(cls, state: State) -> Iterable[Action]: + """Return a collection of all possible actions in a given game state.""" + + @classmethod + @abstractmethod + def result(cls, state: State, action: Action) -> State: + """Return new game state after performing action on given state.""" + + @classmethod + def probability(cls, action: Action) -> float: + """Return probability that given chance node action will happen. + + Should be in range [0.0, 1.0] for 0% and 100% chance respectively. + """ + return 1.0 + + @classmethod + def minimax( + cls, + state: State, + depth: int | None = 5, + ) -> MinimaxResult[Action]: + """Return minimax result best action for a given state for the current player.""" + if cls.terminal(state): + return MinimaxResult(cls.value(state), None) + if depth is not None and depth <= 0: + # Choose a random action + # No need for cryptographic secure random + return MinimaxResult( + cls.value(state), + random.choice(tuple(cls.actions(state))), # noqa: S311 + ) + next_down = None if depth is None else depth - 1 + + current_player = cls.player(state) + value: int | float + if current_player == Player.MAX: + value = -infinity + best = max + elif current_player == Player.MIN: + value = infinity + best = min + elif current_player == Player.CHANCE: + value = 0 + best = sum + else: + raise ValueError(f"Unexpected player type {current_player!r}") + + best_action: Action | None = None + for action in cls.actions(state): + result = cls.minimax(cls.result(state, action), next_down) + result_value = result.value + if current_player == Player.CHANCE: + # Probability[action] + result_value *= cls.probability(action) + new_value = best(value, result_value) + if new_value != value and current_player != Player.CHANCE: + best_action = action + value = new_value + return MinimaxResult(value, best_action) + + @classmethod + def alphabeta( + cls, + state: State, + depth: int | None = 5, + a: int | float = -infinity, + b: int | float = infinity, + ) -> MinimaxResult[Action]: + """Return minimax alphabeta pruning result best action for given current state.""" + # print(f'alphabeta {depth = } {a = } {b = }') + + if cls.terminal(state): + return MinimaxResult(cls.value(state), None) + if depth is not None and depth <= 0: + # Choose a random action + # No need for cryptographic secure random + return MinimaxResult( + cls.value(state), + random.choice(tuple(cls.actions(state))), # noqa: S311 + ) + next_down = None if depth is None else depth - 1 + + current_player = cls.player(state) + value: int | float + if current_player == Player.MAX: + value = -infinity + best = max + compare = operator.gt # greater than (>) + set_idx = 0 + elif current_player == Player.MIN: + value = infinity + best = min + compare = operator.lt # less than (<) + set_idx = 1 + elif current_player == Player.CHANCE: + value = 0 + best = sum + else: + raise ValueError(f"Unexpected player type {current_player!r}") + + actions = tuple(cls.actions(state)) + successors = len(actions) + expect_a = successors * (a - cls.HIGHEST) + cls.HIGHEST + expect_b = successors * (b - cls.LOWEST) + cls.LOWEST + + best_action: Action | None = None + for action in actions: + if current_player == Player.CHANCE: + # Limit child a, b to a valid range + ax = max(expect_a, cls.LOWEST) + bx = min(expect_b, cls.HIGHEST) + # Search the child with new cutoff values + result = cls.alphabeta( + cls.result(state, action), + next_down, + ax, + bx, + ) + score = result.value + # Check for a, b cutoff conditions + if score <= expect_a: + return MinimaxResult(a, None) + if score >= expect_b: + return MinimaxResult(b, None) + value += score + # Adjust a, b for the next child + expect_a += cls.HIGHEST - score + expect_b += cls.LOWEST - score + continue + + result = cls.alphabeta(cls.result(state, action), next_down, a, b) + new_value = best(value, result.value) + + if new_value != value: + best_action = action + value = new_value + + if compare(new_value, (a, b)[set_idx ^ 1]): + # print("cutoff") + break # cutoff + + alpha_beta_value = (a, b)[set_idx] + new_alpha_beta_value = best(alpha_beta_value, value) + + if new_alpha_beta_value != alpha_beta_value: + # Set new best + alpha_beta_list = [a, b] + alpha_beta_list[set_idx] = new_alpha_beta_value + a, b = alpha_beta_list + if current_player == Player.CHANCE: + # No cutoff occurred, return score + return MinimaxResult(value / successors, None) + return MinimaxResult(value, best_action) + + +if __name__ == "__main__": + print(f"{__title__}\nProgrammed by {__author__}.\n") diff --git a/src/azul/client.py b/src/azul/client.py index 15e41e7..697101c 100644 --- a/src/azul/client.py +++ b/src/azul/client.py @@ -202,18 +202,7 @@ def bind_handlers(self) -> None: self.register_handlers( { "server->callback_ping": self.read_callback_ping, - "gameboard_piece_clicked": self.write_piece_click, - "gameboard_tile_clicked": self.write_tile_click, - "server->create_piece": self.read_create_piece, - "server->select_piece": self.read_select_piece, - "server->create_tile": self.read_create_tile, - "server->delete_tile": self.read_delete_tile, - "server->delete_piece_animation": self.read_delete_piece_animation, - "server->update_piece_animation": self.read_update_piece_animation, - "server->move_piece_animation": self.read_move_piece_animation, - "server->animation_state": self.read_animation_state, "server->game_over": self.read_game_over, - "server->action_complete": self.read_action_complete, "server->initial_config": self.read_initial_config, "server->playing_as": self.read_playing_as, "server->encryption_request": self.read_encryption_request, @@ -348,118 +337,6 @@ async def read_callback_ping(self, event: Event[bytearray]) -> None: Event("callback_ping", difference), ) - async def read_create_piece(self, event: Event[bytearray]) -> None: - """Read create_piece event from server.""" - buffer = Buffer(event.data) - - piece_pos = read_position(buffer) - piece_type: u8 = buffer.read_value(StructFormat.UBYTE) - - await self.raise_event( - Event("gameboard_create_piece", (piece_pos, piece_type)), - ) - - async def read_select_piece(self, event: Event[bytearray]) -> None: - """Read create_piece event from server.""" - buffer = Buffer(event.data) - - piece_pos = read_position(buffer) - outline_value = buffer.read_value(StructFormat.BOOL) - - await self.raise_event( - Event("gameboard_select_piece", (piece_pos, outline_value)), - ) - - async def read_create_tile(self, event: Event[bytearray]) -> None: - """Read create_tile event from server.""" - buffer = Buffer(event.data) - - tile_pos = read_position(buffer) - - await self.raise_event(Event("gameboard_create_tile", tile_pos)) - - async def read_delete_tile(self, event: Event[bytearray]) -> None: - """Read delete_tile event from server.""" - buffer = Buffer(event.data) - - tile_pos = read_position(buffer) - - await self.raise_event(Event("gameboard_delete_tile", tile_pos)) - - async def write_piece_click(self, event: Event[tuple[Pos, int]]) -> None: - """Write piece click event to server.""" - if self.not_connected: - return - piece_position, _piece_type = event.data - - buffer = Buffer() - write_position(buffer, piece_position) - # buffer.write_value(StructFormat.UINT, piece_type) - - await self.write_event(Event("select_piece->server", buffer)) - - async def write_tile_click(self, event: Event[Pos]) -> None: - """Write tile click event to server.""" - if self.not_connected: - return - tile_position = event.data - - buffer = Buffer() - write_position(buffer, tile_position) - - await self.write_event(Event("select_tile->server", buffer)) - - async def read_delete_piece_animation( - self, - event: Event[bytearray], - ) -> None: - """Read delete_piece_animation event from server.""" - buffer = Buffer(event.data) - - tile_pos = read_position(buffer) - - await self.raise_event( - Event("gameboard_delete_piece_animation", tile_pos), - ) - - async def read_update_piece_animation( - self, - event: Event[bytearray], - ) -> None: - """Read update_piece_animation event from server.""" - buffer = Buffer(event.data) - - piece_pos = read_position(buffer) - piece_type: u8 = buffer.read_value(StructFormat.UBYTE) - - await self.raise_event( - Event("gameboard_update_piece_animation", (piece_pos, piece_type)), - ) - - async def read_move_piece_animation(self, event: Event[bytearray]) -> None: - """Read move_piece_animation event from server.""" - buffer = Buffer(event.data) - - piece_current_pos = read_position(buffer) - piece_new_pos = read_position(buffer) - - await self.raise_event( - Event( - "gameboard_move_piece_animation", - (piece_current_pos, piece_new_pos), - ), - ) - - async def read_animation_state(self, event: Event[bytearray]) -> None: - """Read animation_state event from server.""" - buffer = Buffer(event.data) - - animation_state = buffer.read_value(StructFormat.BOOL) - - await self.raise_event( - Event("gameboard_animation_state", animation_state), - ) - async def read_game_over(self, event: Event[bytearray]) -> None: """Read update_piece event from server.""" buffer = Buffer(event.data) @@ -469,22 +346,6 @@ async def read_game_over(self, event: Event[bytearray]) -> None: await self.raise_event(Event("game_winner", winner)) self.running = False - async def read_action_complete(self, event: Event[bytearray]) -> None: - """Read action_complete event from server. - - Sent when last action from client is done, great for AIs. - As of writing, not used for main client. - """ - buffer = Buffer(event.data) - - from_pos = read_position(buffer) - to_pos = read_position(buffer) - current_turn: u8 = buffer.read_value(StructFormat.UBYTE) - - await self.raise_event( - Event("game_action_complete", (from_pos, to_pos, current_turn)), - ) - async def read_initial_config(self, event: Event[bytearray]) -> None: """Read initial_config event from server.""" buffer = Buffer(event.data) diff --git a/src/azul/component.py b/src/azul/component.py index 3e6865b..487db3a 100644 --- a/src/azul/component.py +++ b/src/azul/component.py @@ -145,6 +145,25 @@ def has_handler(self, event_name: str) -> bool: """ return self.manager.has_handler(event_name) + def unregister_handler( + self, + event_name: str, + handler_coro: Callable[[Event[Any]], Awaitable[None]], + ) -> None: + """Unregister a handler function for event_name. + + Raises ValueError if no component with given name is registered. + """ + return self.manager.unregister_handler( + event_name, + handler_coro, + self.name, + ) + + def unregister_handler_type(self, event_name: str) -> bool: + """Unregister all event handlers for a given event type.""" + return self.manager.unregister_handler_type(event_name) + async def raise_event(self, event: Event[Any]) -> None: """Raise event for bound manager. @@ -247,6 +266,43 @@ def register_component_handler( self.__event_handlers[event_name] = set() self.__event_handlers[event_name].add((handler_coro, component_name)) + def unregister_handler( + self, + event_name: str, + handler_coro: Callable[[Event[Any]], Awaitable[None]], + component_name: object, + ) -> None: + """Unregister a handler function for event_name. + + Raises ValueError if no component with given name is registered. + """ + if ( + component_name != self.name + and component_name not in self.__components + ): + raise ValueError( + f"Component named {component_name!r} is not registered!", + ) + + if event_name not in self.__event_handlers: + return + + handler_tuple = (handler_coro, component_name) + if handler_tuple in self.__event_handlers[event_name]: + self.__event_handlers[event_name].remove(handler_tuple) + + # If the event_name no longer has any handlers, remove it + if not self.__event_handlers[event_name]: + del self.__event_handlers[event_name] + + def unregister_handler_type( + self, + event_name: str, + ) -> None: + """Unregister all event handlers for a given event type.""" + if event_name in self.__event_handlers: + del self.__event_handlers[event_name] + def has_handler(self, event_name: str) -> bool: """Return if there are event handlers registered for a given event.""" return bool(self.__event_handlers.get(event_name)) diff --git a/src/azul/game.py b/src/azul/game.py index 6ecb4c3..29512a5 100644 --- a/src/azul/game.py +++ b/src/azul/game.py @@ -23,62 +23,63 @@ __author__ = "CoolCat467" __version__ = "2.0.0" +import contextlib import importlib import math import operator import os import random -import traceback +import sys import time -from collections import Counter, deque +import traceback +from collections import Counter from functools import lru_cache, wraps from pathlib import Path -import trio -from typing import TYPE_CHECKING, Final, NamedTuple, TypeVar, cast +from typing import TYPE_CHECKING, Final, TypeVar import pygame -from pygame.color import Color +import trio from numpy import array, int8 +from pygame.color import Color from pygame.locals import ( - KEYDOWN, - KEYUP, K_ESCAPE, + KEYUP, QUIT, RESIZABLE, SRCALPHA, USEREVENT, - VIDEORESIZE, WINDOWRESIZED, ) from pygame.rect import Rect +from azul import element_list, objects, sprite +from azul.async_clock import Clock +from azul.client import GameClient, read_advertisements +from azul.component import ( + ComponentManager, + Event, + ExternalRaiseManager, +) +from azul.network_shared import DEFAULT_PORT, find_ip +from azul.server import GameServer +from azul.sound import SoundData, play_sound as base_play_sound +from azul.statemachine import AsyncState from azul.tools import ( - floor_line_subtract_generator, - gen_random_proper_seq, lerp_color, - randomize, saturate, - sort_tiles, ) from azul.vector import Vector2 -from azul import sprite -from azul import objects -from azul.component import Component, Event, ExternalRaiseManager, ComponentManager -from azul import errorbox -from azul.async_clock import Clock -from azul.server import GameServer -from azul.sound import SoundData, play_sound as base_play_sound -from azul import element_list -from azul.client import GameClient, read_advertisements -from azul.statemachine import AsyncState -from azul.network_shared import DEFAULT_PORT, find_ip -from enum import IntEnum, auto -import sys if TYPE_CHECKING: - from collections.abc import Callable, Generator, Iterable, Sequence, Awaitable + from collections.abc import ( + Awaitable, + Callable, + Generator, + Iterable, + Sequence, + ) - from typing_extensions import TypeVarTuple, Unpack + from typing_extensions import TypeVarTuple P = TypeVarTuple("P") @@ -130,19 +131,9 @@ ("&", ORANGE), ("1", BLUE), ) -class Tiles(IntEnum): - blank = -6 - fake_cyan = -5 - fake_black = -4 - fake_red = -3 - fake_yellow = -2 - fake_blue = -1 - blue = 0 - yellow = auto() - red = auto() - black = auto() - cyan = auto() - one = auto() + + +from azul.state import Tile TILESIZE = 15 @@ -157,7 +148,7 @@ class Tiles(IntEnum): GREYSHIFT = 0.75 # 0.65 # Font -FONT: Final = FONT_FOLDER / "VeraSerif.ttf"#"RuneScape-UF-Regular.ttf" +FONT: Final = FONT_FOLDER / "VeraSerif.ttf" # "RuneScape-UF-Regular.ttf" SCOREFONTSIZE = 30 BUTTONFONTSIZE = 60 @@ -278,7 +269,7 @@ def get_tile_color( ) -> tuple[int, int, int] | tuple[tuple[int, int, int], tuple[int, int, int]]: """Return the color a given tile should be.""" if tile_color < 0: - if tile_color == -6: + if tile_color == Tile.blank: return GREY color = tile_colors[abs(tile_color + 1)] assert len(color) == 3 @@ -296,7 +287,7 @@ def get_tile_symbol_and_color( ) -> tuple[str, tuple[int, int, int]]: """Return the color a given tile should be.""" if tile_color < 0: - if tile_color == -6: + if tile_color == Tile.blank: return " ", GREY symbol, scolor = TILESYMBOLS[abs(tile_color + 1)] r, g, b = lerp_color(scolor, GREY, greyshift) @@ -348,7 +339,6 @@ def add_symbol_to_tile_surf( # surf.blit(symbolsurf, (0, 0)) -@lru_cache def get_tile_image( tile_color: int, tilesize: int, @@ -378,22 +368,8 @@ def get_tile_image( return surf -def set_alpha( - surface: pygame.surface.Surface, - alpha: int, -) -> pygame.surface.Surface: - """Return a surface by replacing the alpha channel of it with given alpha value, preserve color.""" - surface = surface.copy().convert_alpha() - w, h = surface.get_size() - for y in range(h): - for x in range(w): - r, g, b = cast("tuple[int, int, int]", surface.get_at((x, y))[:3]) - surface.set_at((x, y), pygame.Color(r, g, b, alpha)) - return surface - - def get_tile_container_image( - wh: tuple[int, int], + width_height: tuple[int, int], back: ( pygame.color.Color | int @@ -405,7 +381,7 @@ def get_tile_container_image( ), ) -> pygame.surface.Surface: """Return a tile container image from a width and a height and a background color, and use a game's cache to help.""" - image = pygame.surface.Surface(wh, flags=SRCALPHA) + image = pygame.surface.Surface(width_height, flags=SRCALPHA) if back is not None: image.fill(back) else: @@ -413,7 +389,6 @@ def get_tile_container_image( return image - class ObjectHandler: """ObjectHandler class, meant to be used for other classes.""" @@ -575,7 +550,6 @@ def __del__(self) -> None: self.rm_star() - class MultipartObject(ObjectHandler): """Thing that is both an Object and an ObjectHandler, and is meant to be an Object made up of multiple Objects.""" @@ -637,77 +611,100 @@ def __del__(self) -> None: class TileRenderer(sprite.Sprite): """Base class for all objects that need to render tiles.""" - __slots__ = ("background", "tile_seperation") + __slots__ = ("background", "tile_separation") greyshift = GREYSHIFT tile_size = TILESIZE def __init__( self, name: str, - tile_seperation: int | None = None, + tile_separation: int | None = None, background: tuple[int, int, int] | None = TILEDEFAULT, ) -> None: """Initialize renderer.""" super().__init__(name) - if tile_seperation is None: - self.tile_seperation = self.tile_size / 3.75 + if tile_separation is None: + self.tile_separation = self.tile_size / 3.75 else: - self.tile_seperation = tile_seperation + self.tile_separation = tile_separation self.background = background - def get_rect(self) -> Rect: - """Return a Rect object representing this row's area.""" - wh = ( - self.wh[0] - self.tile_seperation * 2, - self.wh[1] - self.tile_seperation * 2, - ) - location = self.location[0] - wh[0] / 2, self.location[1] - wh[1] / 2 - return Rect(location, wh) - def clear_image(self, tile_dimensions: tuple[int, int]) -> None: - """Reset self.image using tile_dimensions tuple and fills with self.background. Also updates self.wh.""" - tw, th = tile_dimensions - tile_full = self.tile_size + self.tile_seperation + """Reset self.image using tile_dimensions tuple and fills with self.background. Also updates self.width_height.""" + tile_width, tile_height = tile_dimensions + tile_full = self.tile_size + self.tile_separation self.image = get_tile_container_image( ( - round(tw * tile_full + self.tile_seperation), - round(th * tile_full + self.tile_seperation), + round(tile_width * tile_full + self.tile_separation), + round(tile_height * tile_full + self.tile_separation), ), - self.background + self.background, ) def blit_tile( self, tile_color: int, tile_location: tuple[int, int], + offset: tuple[int, int] | None = None, ) -> None: """Blit the surface of a given tile object onto self.image at given tile location. It is assumed that all tile locations are xy tuples.""" x, y = tile_location - surf = get_tile_image(tile_object, self.tile_size, self.greyshift) + if offset is None: + ox, oy = 0, 0 + else: + ox, oy = offset + + ox += self.tile_separation + oy += self.tile_separation + + surf = get_tile_image(tile_color, self.tile_size, self.greyshift) assert self.image is not None + + tile_full = self.tile_size + self.tile_separation + self.image.blit( surf, ( - round(x * self.tile_full + self.tile_seperation), - round(y * self.tile_full + self.tile_seperation), + round(x * tile_full + ox), + round(y * tile_full + oy), ), ) def to_image_surface_location( self, screen_location: tuple[int, int] | Vector2, - ) -> tuple[int, int]: - """Return the location a screen location would be at on the objects image. Can return invalid data.""" - # Get zero zero in image locations - zx, zy = self.rect.topleft - sx, sy = screen_location # Screen x and y - # Location with respect to image dimensions - return ( - int(sx) - zx, - int(sy) - zy, - ) + ) -> Vector2: + """Return screen location with respect to top left of image.""" + return Vector2.from_points(self.rect.topleft, screen_location) + + def get_tile_point( + self, + screen_location: tuple[int, int] | Vector2, + ) -> tuple[int, int] | None: + """Return the xy choordinates of which tile intersects given a point or None.""" + # Can't get tile if screen location doesn't intersect our hitbox! + if not self.is_selected(screen_location): + return None + + # Find out where screen point is in image locations + # board x and y + surface_pos = self.to_image_surface_location(screen_location) + # Subtract separation boarder offset + surface_pos -= (self.tile_separation, self.tile_separation) + + tile_full = self.tile_size + self.tile_separation + + # Get tile position and offset into that tile + tile_position, offset = divmod(surface_pos, tile_full) + for value in offset: + # If in separation region, not selected + if value > self.tile_size: + return None + # Otherwise, not in separation region, so we should be good + return tile_position + ## def screen_size_update(self) -> None: ## """Handle screensize is changes.""" @@ -725,7 +722,14 @@ def to_image_surface_location( class Cursor(TileRenderer): - """Cursor Object.""" + """Cursor TileRenderer. + + Registers following event handlers: + - cursor_drag + - cursor_reached_destination + - cursor_set_destination + - cursor_set_location + """ __slots__ = ("tiles",) greyshift = GREYSHIFT @@ -733,16 +737,87 @@ class Cursor(TileRenderer): def __init__(self) -> None: """Initialize cursor with a game it belongs to.""" super().__init__("Cursor", background=None) + self.update_location_on_resize = True + self.add_components( + ( + sprite.MovementComponent(speed=800), + sprite.TargetingComponent("cursor_reached_destination"), + ), + ) + + # Stored in reverse render order self.tiles: list[int] = [] def update_image(self) -> None: """Update self.image.""" - self.clear_image((len(self.tiles), 1)) + tile_count = len(self.tiles) + self.clear_image((tile_count, 1)) - for x in range(len(self.tiles)): - self.blit_tile(self.tiles[x], (x, 0)) - self.dirty = 1 + # Render in reverse order so keeping number one on end is easier + for x in range(tile_count): + self.blit_tile(self.tiles[tile_count - x - 1], (x, 0)) + if tile_count: + self.dirty = 1 + self.visible = bool(tile_count) + + def bind_handlers(self): + """Register handlers.""" + self.register_handlers( + { + "cursor_drag": self.handle_cursor_drag, + "cursor_reached_destination": self.handle_cursor_reached_destination, + "cursor_set_destination": self.handle_cursor_set_destination, + "cursor_set_location": self.handle_cursor_set_location, + }, + ) + + async def handle_cursor_drag(self, event: Event[Iterable[int]]) -> None: + """Drag one or more tiles.""" + await trio.lowlevel.checkpoint() + for tile_color in event.data: + if tile_color == Tile.one: + self.tiles.insert(0, tile_color) + else: + self.tiles.append(tile_color) + self.update_image() + + async def handle_cursor_reached_destination( + self, + event: Event[None], + ) -> None: + """Stop ticking.""" + self.unregister_handler_type("tick") + await trio.lowlevel.checkpoint() + + def move_to_front(self) -> None: + """Move this sprite to front.""" + group: sprite.LayeredDirty = self.groups()[-1] + group.move_to_front(self) + + async def handle_cursor_set_destination( + self, + event: Event[tuple[int, int]], + ) -> None: + """Start moving towards new destination.""" + targeting: sprite.TargetingComponent = self.get_component("targeting") + targeting.destination = event.data + if not self.has_handler("tick"): + self.register_handler( + "tick", + targeting.move_destination_time_ticks, + ) + self.move_to_front() + await trio.lowlevel.checkpoint() + + async def handle_cursor_set_location( + self, + event: Event[tuple[int, int]], + ) -> None: + """Set location to event data.""" + self.move_to_front() + self.location = event.data + await trio.lowlevel.checkpoint() def get_held_count(self) -> int: """Return the number of held tiles.""" @@ -752,113 +827,28 @@ def is_holding(self) -> bool: """Return True if the mouse is dragging something.""" return len(self.tiles) > 0 - def get_held_info( - self, - ) -> tuple[int, ...]: + def get_held_info(self) -> tuple[int, ...]: """Return tuple of currently held tiles.""" - return tuple(self.tiles) - - def process(self, time_passed: float) -> None: - """Process cursor.""" - x, y = pygame.mouse.get_pos() - x = saturate(x, 0, SCREEN_SIZE[0]) - y = saturate(y, 0, SCREEN_SIZE[1]) - self.location = (x, y) - - def force_hold(self, tiles: Iterable[Tile]) -> None: - """Pretty much it's drag but with no constraints.""" - for tile in tiles: - if tile.color == Tiles.one: - self.holding_number_one = True - self.tiles.append(tile) - else: - self.tiles.appendleft(tile) - self.image_update = True - - def drag(self, tiles: Iterable[Tile]) -> None: - """Drag one or more tiles, as long as it's a list.""" - for tile in tiles: - if tile is not None and tile.color == Tiles.one: - self.holding_number_one = True - self.tiles.append(tile) - else: - self.tiles.appendleft(tile) - self.image_update = True + return tuple(reversed(self.tiles)) def drop( self, number: int | None = None, - allow_number_one_tile: bool = False, - ) -> list[Tile]: - """Return all of the tiles the Cursor is carrying.""" - if self.is_holding(allow_number_one_tile): - if number is None: - number = self.get_held_count(allow_number_one_tile) - else: - number = saturate( - number, - 0, - self.get_held_count(allow_number_one_tile), - ) - - tiles = [] - for tile in (self.tiles.popleft() for i in range(number)): - if tile.color == Tiles.one and not allow_number_one_tile: - self.tiles.append(tile) - continue - tiles.append(tile) - self.image_update = True - - self.holding_number_one = Tiles.one in { - tile.color for tile in self.tiles - } - return tiles - return [] - - def drop_one_tile(self) -> Tile | None: - """If holding the number one tile, drop it (returns it).""" - if self.holding_number_one: - not_number_one_tile = self.drop(None, False) - one = self.drop(1, True) - self.drag(not_number_one_tile) - self.holding_number_one = False - return one[0] - return None - - -G = TypeVar("G", bound="Grid") - - -def gsc_bound_index( - bounds_failure_return: T, -) -> Callable[ - [Callable[[G, tuple[int, int], *P], RT]], - Callable[[G, tuple[int, int], *P], RT | T], -]: - """Return a decorator for any grid or grid subclass that will keep index positions within bounds.""" - - def gsc_bounds_keeper( - function: Callable[[G, tuple[int, int], *P], RT], - ) -> Callable[[G, tuple[int, int], *P], RT | T]: - """Grid or Grid Subclass Decorator that keeps index positions within bounds, as long as index is first argument after self arg.""" - - @wraps(function) - def keep_within_bounds( - self: G, - index: tuple[int, int], - *args: Unpack[P], - ) -> RT | T: - """Ensure a index position tuple is valid.""" - x, y = index - if x < 0 or x >= self.size[0]: - return bounds_failure_return - if y < 0 or y >= self.size[1]: - return bounds_failure_return - return function(self, index, *args) - - return keep_within_bounds + ) -> tuple[int, ...]: + """Pop and return tiles the Cursor is carrying. - return gsc_bounds_keeper + If number is None, pops all tiles, otherwise only pops given count. + """ + if number is None: + tiles_copy = self.get_held_info() + self.tiles.clear() + self.update_image() + return tiles_copy + tiles: list[int] = [] + for _ in range(number): + tiles.append(self.tiles.pop()) + self.update_image() + return tuple(tiles) class Grid(TileRenderer): @@ -868,303 +858,233 @@ class Grid(TileRenderer): def __init__( self, + name: str, size: tuple[int, int], - game: Game, - tile_seperation: int | None = None, + tile_separation: int | None = None, background: tuple[int, int, int] | None = TILEDEFAULT, ) -> None: """Grid Objects require a size and game at least.""" - super().__init__("Grid", game, tile_seperation, background) + super().__init__(name, tile_separation, background) self.size = size self.data = array( - [-6 for i in range(int(self.size[0] * self.size[1]))], + [Tile.blank for i in range(int(self.size[0] * self.size[1]))], int8, ).reshape(self.size) + def get_tile(self, xy: tuple[int, int]) -> int: + """Return tile color at given index.""" + x, y = xy + return int(self.data[y, x]) + def update_image(self) -> None: """Update self.image.""" self.clear_image(self.size) - for y in range(self.size[1]): - for x in range(self.size[0]): - self.blit_tile(Tile(self.data[y, x]), (x, y)) + width, height = self.size - def get_tile_point( - self, - screen_location: tuple[int, int] | Vector2, - ) -> tuple[int, int] | None: - """Return the xy choordinates of which tile intersects given a point. Returns None if no intersections.""" - # Can't get tile if screen location doesn't intersect our hitbox! - if not self.is_selected(screen_location): - return None - # Otherwise, find out where screen point is in image locations - # board x and y - bx, by = self.to_image_surface_location(screen_location) - # Finally, return the full divides (no decimals) of xy location by self.tile_full. - return int(bx // self.tile_full), int(by // self.tile_full) + for y in range(height): + for x in range(width): + pos = (x, y) + self.blit_tile(self.get_tile(pos), pos) - @gsc_bound_index(None) - def place_tile(self, xy: tuple[int, int], tile: Tile) -> bool: - """Place a Tile Object if permitted to do so. Return True if success.""" - x, y = xy - if self.data[y, x] < 0: - self.data[y, x] = tile.color - del tile - self.image_update = True - return True - return False + def fake_tile_exists(self, xy: tuple[int, int]) -> bool: + """Return if tile at given position is a fake tile.""" + return self.get_tile(xy) < 0 - @gsc_bound_index(None) - def get_tile(self, xy: tuple[int, int], replace: int = -6) -> Tile | None: - """Return a Tile Object from a given position in the grid if permitted. Return None on failure.""" + def place_tile(self, xy: tuple[int, int], tile_color: int) -> bool: + """Place tile at given position.""" x, y = xy - tile_color = int(self.data[y, x]) - if tile_color < 0: - return None - self.data[y, x] = replace - self.image_update = True - return Tile(tile_color) + self.data[y, x] = tile_color + self.update_image() - @gsc_bound_index(None) - def get_info(self, xy: tuple[int, int]) -> Tile: - """Return the Tile Object at a given position without deleting it from the Grid.""" - x, y = xy - color = int(self.data[y, x]) - return Tile(color) + def pop_tile(self, xy: tuple[int, int], replace: int = Tile.blank) -> int: + """Return popped tile from given position in the grid.""" + tile_color = self.get_tile(xy) + self.place_tile(xy, replace) + return tile_color - def get_colors(self) -> list[int]: - """Return a list of the colors of tiles within self.""" + def get_colors(self) -> set[int]: + """Return a set of the colors of tiles within self.""" colors = set() - for y in range(self.size[1]): - for x in range(self.size[0]): - info_color = int(self.data[y, x]) - assert info_color is not None - colors.add(info_color) - return list(colors) + width, height = self.size + for y in range(height): + for x in range(width): + colors.add(self.get_tile((x, y))) + return colors - def is_empty(self, empty_color: int = -6) -> bool: + def is_empty(self, empty_color: int = Tile.blank) -> bool: """Return True if Grid is empty (all tiles are empty_color).""" colors = self.get_colors() - # Colors should only be [-6] if empty - return colors == [empty_color] - - def __del__(self) -> None: - """Delete data.""" - super().__del__() - del self.data + return len(colors) == 1 and colors.pop() == empty_color class Board(Grid): """Represents the board in the Game.""" - __slots__ = ("additions", "player", "variant_play", "wall_tiling") - bcolor = ORANGE + __slots__ = ("additions", "variant_play", "wall_tiling") - def __init__(self, player: Player, variant_play: bool = False) -> None: + def __init__(self, variant_play: bool = False) -> None: """Initialize player's board.""" - super().__init__((5, 5), player.game, background=self.bcolor) - self.name = "Board" - self.player = player + super().__init__("Board", (5, 5), background=ORANGE) self.variant_play = variant_play - self.additions: dict[int, Tile | int | None] = {} + self.additions: dict[int, int | None] = {} self.wall_tiling = False + if not variant_play: + self.set_colors() + else: + self.update_image() + self.visible = True + def __repr__(self) -> str: """Return representation of self.""" - return ( - f"{self.__class__.__name__}({self.player!r}, {self.variant_play})" - ) + return f"{self.__class__.__name__}({self.variant_play})" - def set_colors(self, keep_read: bool = True) -> None: + def set_colors(self, keep_real: bool = True) -> None: """Reset tile colors.""" - for y in range(self.size[1]): - for x in range(self.size[0]): - if not keep_read or self.data[y, x] < 0: - self.data[y, x] = -( - (self.size[1] - y + x) % REGTILECOUNT + 1 - ) - - # print(self.data[y, x], end=' ') - # print() - # print('-'*10) - - def get_row(self, index: int) -> Generator[Tile, None, None]: + width, height = self.size + for y in range(height): + for x in range(width): + if not keep_real or self.fake_tile_exists((x, y)): + color = -((height - y + x) % REGTILECOUNT + 1) + self.data[y, x] = color + self.update_image() + + def get_row(self, index: int) -> Generator[int, None, None]: """Return a row from self. Does not delete data from internal grid.""" for x in range(self.size[0]): - tile = self.get_info((x, index)) - assert tile is not None - yield tile + yield self.get_info((x, index)) - def get_column(self, index: int) -> Generator[Tile, None, None]: + def get_column(self, index: int) -> Generator[int, None, None]: """Return a column from self. Does not delete data from internal grid.""" for y in range(self.size[1]): - tile = self.get_info((index, y)) - assert tile is not None - yield tile + yield self.get_info((index, y)) def get_colors_in_row( self, index: int, exclude_negatives: bool = True, - ) -> list[int]: + ) -> set[int]: """Return the colors placed in a given row in internal grid.""" - row_colors = [tile.color for tile in self.get_row(index)] + row_colors: Iterable[int] = self.get_row(index) if exclude_negatives: - row_colors = [c for c in row_colors if c >= 0] - ccolors = Counter(row_colors) - return sorted(ccolors.keys()) + row_colors = (c for c in row_colors if c >= 0) + return set(row_colors) def get_colors_in_column( self, index: int, exclude_negatives: bool = True, - ) -> list[int]: + ) -> set[int]: """Return the colors placed in a given row in internal grid.""" - column_colors = [tile.color for tile in self.get_column(index)] + column_colors: Iterable[int] = self.get_column(index) if exclude_negatives: - column_colors = [c for c in column_colors if c >= 0] - ccolors = Counter(column_colors) - return sorted(ccolors.keys()) + column_colors = (c for c in column_colors if c >= 0) + return set(column_colors) def is_wall_tiling(self) -> bool: """Return True if in Wall Tiling Mode.""" return self.wall_tiling - def get_tile_for_cursor_by_row(self, row: int) -> Tile | None: - """Return A COPY OF tile the mouse should hold. Returns None on failure.""" - if row in self.additions: - data = self.additions[row] - if isinstance(data, Tile): - return data - return None - - @gsc_bound_index(False) def can_place_tile_color_at_point( self, position: tuple[int, int], - tile: Tile, + tile_color: int, ) -> bool: """Return True if tile's color is valid at given position.""" column, row = position - colors = set( - self.get_colors_in_column(column) + self.get_colors_in_row(row), + colors = self.get_colors_in_column(column) | self.get_colors_in_row( + row, ) - return tile.color not in colors - - def get_rows_to_tile_map(self) -> dict[int, int]: - """Return a dictionary of row numbers and row color to be wall tiled.""" - rows = {} - for row, tile in self.additions.items(): - if not isinstance(tile, Tile): - continue - rows[row] = tile.color - return rows - - def calculate_valid_locations_for_tile_row( - self, - row: int, - ) -> tuple[int, ...]: - """Return the valid drop columns of the additions tile for a given row.""" - valid = [] - # ??? Why overwriting row? - if row in self.additions: - tile = self.additions[row] - if isinstance(tile, Tile): - for column in range(self.size[0]): - if self.can_place_tile_color_at_point((column, row), tile): - valid.append(column) - return tuple(valid) - return () - - def remove_invalid_additions(self) -> None: - """Remove invalid additions that would not be placeable.""" - # In the wall-tiling phase, it may happen that you - # are not able to move the rightmost tile of a certain - # pattern line over to the wall because there is no valid - # space left for it. In this case, you must immediately - # place all tiles of that pattern line in your floor line. - for row in range(self.size[1]): - row_tile = self.additions[row] - if not isinstance(row_tile, Tile): - continue - valid = self.calculate_valid_locations_for_tile_row(row) - if not valid: - floor = self.player.get_object_by_name("floor_line") - assert isinstance(floor, FloorLine) - floor.place_tile(row_tile) - self.additions[row] = None - - @gsc_bound_index(False) - def wall_tile_from_point(self, position: tuple[int, int]) -> bool: - """Given a position, wall tile. Return success on placement. Also updates if in wall tiling mode.""" - success = False - column, row = position - at_point = self.get_info(position) - assert at_point is not None - if at_point.color <= 0 and row in self.additions: - tile = self.additions[row] - if isinstance(tile, Tile) and self.can_place_tile_color_at_point( - position, - tile, - ): - self.place_tile(position, tile) - self.additions[row] = column - # Update invalid placements after new placement - self.remove_invalid_additions() - success = True - if not self.get_rows_to_tile_map(): - self.wall_tiling = False - return success - - def wall_tiling_mode(self, moved_table: dict[int, Tile]) -> None: - """Set self into Wall Tiling Mode. Finishes automatically if not in variant play mode.""" - self.wall_tiling = True - for key, value in moved_table.items(): - key = int(key) - 1 - if key in self.additions: - raise RuntimeError( - f"Key {key!r} Already in additions dictionary!", - ) - self.additions[key] = value - if not self.variant_play: - for row in range(self.size[1]): - if row in self.additions: - rowdata = [tile.color for tile in self.get_row(row)] - tile = self.additions[row] - if not isinstance(tile, Tile): - continue - negative_tile_color = -(tile.color + 1) - if negative_tile_color in rowdata: - column = rowdata.index(negative_tile_color) - self.place_tile((column, row), tile) - # Set data to the column placed in, use for scoring - self.additions[row] = column - else: - raise RuntimeError( - f"{negative_tile_color} not in row {row}!", - ) - else: - raise RuntimeError(f"{row} not in moved_table!") - self.wall_tiling = False - else: - # Invalid additions can only happen in variant play mode. - self.remove_invalid_additions() + return tile_color not in colors + + ## def remove_invalid_additions(self) -> None: + ## """Remove invalid additions that would not be placeable.""" + ## # In the wall-tiling phase, it may happen that you + ## # are not able to move the rightmost tile of a certain + ## # pattern line over to the wall because there is no valid + ## # space left for it. In this case, you must immediately + ## # place all tiles of that pattern line in your floor line. + ## for row in range(self.size[1]): + ## row_tile = self.additions[row] + ## if not isinstance(row_tile, int): + ## continue + ## valid = self.calculate_valid_locations_for_tile_row(row) + ## if not valid: + ## floor = self.player.get_object_by_name("floor_line") + ## assert isinstance(floor, FloorLine) + ## floor.place_tile(row_tile) + ## self.additions[row] = None + + ## def wall_tile_from_point(self, position: tuple[int, int]) -> bool: + ## """Given a position, wall tile. Return success on placement. Also updates if in wall tiling mode.""" + ## success = False + ## column, row = position + ## at_point = self.get_info(position) + ## assert at_point is not None + ## if at_point.color <= 0 and row in self.additions: + ## tile = self.additions[row] + ## if isinstance(tile, int) and self.can_place_tile_color_at_point( + ## position, + ## tile, + ## ): + ## self.place_tile(position, tile) + ## self.additions[row] = column + ## # Update invalid placements after new placement + ## self.remove_invalid_additions() + ## success = True + ## if not self.get_rows_to_tile_map(): + ## self.wall_tiling = False + ## return success + + ## def wall_tiling_mode(self, moved_table: dict[int, int]) -> None: + ## """Set self into Wall Tiling Mode. Finishes automatically if not in variant play mode.""" + ## self.wall_tiling = True + ## for key, value in moved_table.items(): + ## key = int(key) - 1 + ## if key in self.additions: + ## raise RuntimeError( + ## f"Key {key!r} Already in additions dictionary!", + ## ) + ## self.additions[key] = value + ## if not self.variant_play: + ## for row in range(self.size[1]): + ## if row in self.additions: + ## rowdata = [tile.color for tile in self.get_row(row)] + ## tile = self.additions[row] + ## if not isinstance(tile, int): + ## continue + ## negative_tile_color = -(tile.color + 1) + ## if negative_tile_color in rowdata: + ## column = rowdata.index(negative_tile_color) + ## self.place_tile((column, row), tile) + ## # Set data to the column placed in, use for scoring + ## self.additions[row] = column + ## else: + ## raise RuntimeError( + ## f"{negative_tile_color} not in row {row}!", + ## ) + ## else: + ## raise RuntimeError(f"{row} not in moved_table!") + ## self.wall_tiling = False + ## else: + ## # Invalid additions can only happen in variant play mode. + ## self.remove_invalid_additions() - @gsc_bound_index(([], [])) def get_touches_continuous( self, xy: tuple[int, int], - ) -> tuple[list[Tile], list[Tile]]: + ) -> tuple[list[int], list[int]]: """Return two lists, each of which contain all the tiles that touch the tile at given x y position, including that position.""" rs, cs = self.size x, y = xy # Get row and column tile color data - row = [tile.color for tile in self.get_row(y)] - column = [tile.color for tile in self.get_column(x)] + row = list(self.get_row(y)) + column = list(self.get_column(x)) # Both def get_greater_than(v: int, size: int, data: list[int]) -> list[int]: @@ -1187,12 +1107,10 @@ def comb(one: Iterable[T], two: Iterable[RT]) -> list[tuple[T, RT]]: """Combine two lists by zipping together and returning list object.""" return list(zip(one, two, strict=False)) - def get_all(lst: list[tuple[int, int]]) -> Generator[Tile, None, None]: + def get_all(lst: list[tuple[int, int]]) -> Generator[int, None, None]: """Return all of the self.get_info points for each value in lst.""" for pos in lst: - tile = self.get_info(pos) - assert tile is not None - yield tile + yield self.get_info(pos) # Get row touches row_touches = comb(get_greater_than(x, rs, row), [y] * rs) @@ -1226,7 +1144,7 @@ def get_filled_rows(self) -> int: """Return the number of filled rows on this board.""" count = 0 for row in range(self.size[1]): - real = (t.color >= 0 for t in self.get_row(row)) + real = (t >= 0 for t in self.get_row(row)) if all(real): count += 1 return count @@ -1239,19 +1157,18 @@ def get_filled_columns(self) -> int: """Return the number of filled rows on this board.""" count = 0 for column in range(self.size[0]): - real = (t.color >= 0 for t in self.get_column(column)) + real = (t >= 0 for t in self.get_column(column)) if all(real): count += 1 return count def get_filled_colors(self) -> int: """Return the number of completed colors on this board.""" - tiles = ( + color_count = Counter( self.get_info((x, y)) for x in range(self.size[0]) for y in range(self.size[1]) ) - color_count = Counter(t.color for t in tiles if t is not None) count = 0 for fill_count in color_count.values(): if fill_count >= 5: @@ -1266,12 +1183,6 @@ def end_of_game_scoreing(self) -> int: score += self.get_filled_colors() * 10 return score - def process(self, time_passed: float) -> None: - """Process board.""" - if self.image_update and not self.variant_play: - self.set_colors(True) - super().process(time_passed) - class Row(TileRenderer): """Represents one of the five rows each player has.""" @@ -1281,30 +1192,24 @@ class Row(TileRenderer): def __init__( self, - player: Player, size: int, - tile_seperation: int | None = None, + tile_separation: int | None = None, background: tuple[int, int, int] | None = None, ) -> None: """Initialize row.""" super().__init__( "Row", - player.game, - tile_seperation, + tile_separation, background, ) - self.player = player self.size = int(size) - self.color = -6 - self.tiles = deque([Tile(self.color)] * self.size) + self.color = Tile.blank + self.tiles = list([self.color] * self.size) def __repr__(self) -> str: """Return representation of self.""" - return f"{self.__class__.__name__}(%r, %i, ...)" % ( - self.game, - self.size, - ) + return f"{self.__class__.__name__}({self.size})" def update_image(self) -> None: """Update self.image.""" @@ -1315,16 +1220,10 @@ def update_image(self) -> None: def get_tile_point(self, screen_location: tuple[int, int]) -> int | None: """Return the xy choordinates of which tile intersects given a point. Returns None if no intersections.""" - # `Grid.get_tile_point` inlined - # Can't get tile if screen location doesn't intersect our hitbox! - if not self.is_selected(screen_location): + pos = super().get_tile_point() + if pos is None: return None - # Otherwise, find out where screen point is in image locations - # board x and y - bx, _by = self.to_image_surface_location(screen_location) - # Finally, return the full divides (no decimals) of xy location by self.tile_full. - - return self.size - 1 - int(bx // self.tile_full) + return pos[0] def get_placed(self) -> int: """Return the number of tiles in self that are not fake tiles, like grey ones.""" @@ -1338,14 +1237,14 @@ def is_full(self) -> bool: """Return True if this row is full.""" return self.get_placed() == self.size - def get_info(self, location: int) -> Tile | None: + def get_info(self, location: int) -> int | None: """Return tile at location without deleting it. Return None on invalid location.""" index = self.size - 1 - location if index < 0 or index > len(self.tiles): return None return self.tiles[index] - def can_place(self, tile: Tile) -> bool: + def can_place(self, tile: int) -> bool: """Return True if permitted to place given tile object on self.""" placeable = (tile.color == self.color) or ( self.color < 0 and tile.color >= 0 @@ -1366,14 +1265,14 @@ def can_place(self, tile: Tile) -> bool: self.size - 1, ) - def get_tile(self, replace: int = -6) -> Tile: + def get_tile(self, replace: int = Tile.blank) -> int: """Return the leftmost tile while deleting it from self.""" - self.tiles.appendleft(Tile(replace)) + self.tiles.appendleft(int(replace)) self.image_update = True return self.tiles.pop() - def place_tile(self, tile: Tile) -> None: - """Place a given Tile Object on self if permitted.""" + def place_tile(self, tile: int) -> None: + """Place a given int Object on self if permitted.""" if self.can_place(tile): self.color = tile.color self.tiles.append(tile) @@ -1386,7 +1285,7 @@ def place_tile(self, tile: Tile) -> None: else: raise ValueError("Not allowed to place.") - def can_place_tiles(self, tiles: list[Tile]) -> bool: + def can_place_tiles(self, tiles: list[int]) -> bool: """Return True if permitted to place all of given tiles objects on self.""" if len(tiles) > self.get_placeable(): return False @@ -1399,7 +1298,7 @@ def can_place_tiles(self, tiles: list[Tile]) -> bool: tile_colors.append(tile.color) return not len(tile_colors) > 1 - def place_tiles(self, tiles: list[Tile]) -> None: + def place_tiles(self, tiles: list[int]) -> None: """Place multiple tile objects on self if permitted.""" if self.can_place_tiles(tiles): for tile in tiles: @@ -1407,41 +1306,41 @@ def place_tiles(self, tiles: list[Tile]) -> None: else: raise ValueError("Not allowed to place tiles.") - def wall_tile( - self, - add_to_table: dict[str, list[Tile] | Tile | None], - empty_color: int = -6, - ) -> None: - """Move tiles around and into add dictionary for the wall tiling phase of the game. Removes tiles from self.""" - if "tiles_for_box" not in add_to_table: - add_to_table["tiles_for_box"] = [] - if not self.is_full(): - add_to_table[str(self.size)] = None - return - self.color = empty_color - add_to_table[str(self.size)] = self.get_tile() - for_box = add_to_table["tiles_for_box"] - assert isinstance(for_box, list) - for _i in range(self.size - 1): - for_box.append(self.get_tile()) + ## def wall_tile( + ## self, + ## add_to_table: dict[str, list[int] | int | None], + ## empty_color: int = Tile.blank, + ## ) -> None: + ## """Move tiles around and into add dictionary for the wall tiling phase of the game. Removes tiles from self.""" + ## if "tiles_for_box" not in add_to_table: + ## add_to_table["tiles_for_box"] = [] + ## if not self.is_full(): + ## add_to_table[str(self.size)] = None + ## return + ## self.color = empty_color + ## add_to_table[str(self.size)] = self.get_tile() + ## for_box = add_to_table["tiles_for_box"] + ## assert isinstance(for_box, list) + ## for _i in range(self.size - 1): + ## for_box.append(self.get_tile()) def set_background(self, color: tuple[int, int, int] | None) -> None: """Set the background color for this row.""" self.background = color - self.image_update = True + self.update_image() class PatternLine(MultipartObject): """Represents multiple rows to make the pattern line.""" - __slots__ = ("player", "row_seperation") + __slots__ = ("player", "row_separation") size = (5, 5) - def __init__(self, player: Player, row_seperation: int = 0) -> None: + def __init__(self, player: Player, row_separation: int = 0) -> None: """Initialize pattern line.""" super().__init__("PatternLine") self.player = player - self.row_seperation = row_seperation + self.row_separation = row_separation for x, _y in zip( range(self.size[0]), @@ -1468,14 +1367,14 @@ def get_row(self, row: int) -> Row: def reset_position(self) -> None: """Reset Locations of Rows according to self.location.""" last = self.size[1] - w = self.get_row(last - 1).wh[0] + w = self.get_row(last - 1).width_height[0] if w is None: raise RuntimeError( - "Image Dimensions for Row Object (row.wh) are None!", + "Image Dimensions for Row Object (row.width_height) are None!", ) h1 = self.get_row(0).tile_full h = int(last * h1) - self.wh = w, h + self.width_height = w, h w1 = h1 / 2 x, y = self.location @@ -1500,9 +1399,9 @@ def is_full(self) -> bool: """Return True if self is full.""" return all(self.get_row(rid).is_full() for rid in range(self.size[1])) - def wall_tiling(self) -> dict[str, list[Tile] | Tile | None]: + def wall_tiling(self) -> dict[str, list[int] | int | None]: """Return a dictionary to be used with wall tiling. Removes tiles from rows.""" - values: dict[str, list[Tile] | Tile | None] = {} + values: dict[str, list[int] | int | None] = {} for rid in range(self.size[1]): self.get_row(rid).wall_tile(values) return values @@ -1518,7 +1417,7 @@ class FloorLine(Row): """Represents a player's floor line.""" size = 7 - number_one_color = Tiles.one + number_one_color = Tile.one def __init__(self, player: Player) -> None: """Initialize floor line.""" @@ -1546,13 +1445,13 @@ def render(self, surface: pygame.surface.Surface) -> None: super().render(surface) sx, sy = self.location - assert self.wh is not None, "Should be impossible." - w, h = self.wh + assert self.width_height is not None, "Should be impossible." + w, h = self.width_height for x in range(self.size): xy = round( - x * self.tile_full + self.tile_seperation + sx - w / 2, + x * self.tile_full + self.tile_separation + sx - w / 2, ), round( - self.tile_seperation + sy - h / 2, + self.tile_separation + sy - h / 2, ) self.text.update_value(str(self.numbers[x])) self.text.location = Vector2(*xy) @@ -1560,8 +1459,8 @@ def render(self, surface: pygame.surface.Surface) -> None: # self.font.render(surface, str(self.numbers[x]), xy) - def place_tile(self, tile: Tile) -> None: - """Place a given Tile Object on self if permitted.""" + def place_tile(self, tile: int) -> None: + """Place a given int Object on self if permitted.""" self.tiles.insert(self.get_placed(), tile) if tile.color == self.number_one_color: @@ -1570,7 +1469,7 @@ def place_tile(self, tile: Tile) -> None: box_lid = self.player.game.get_object_by_name("BoxLid") assert isinstance(box_lid, BoxLid) - def handle_end(end: Tile) -> None: + def handle_end(end: int) -> None: """Handle the end tile we are replacing. Ensures number one tile is not removed.""" if not end.color < 0: if end.color == self.number_one_color: @@ -1597,8 +1496,8 @@ def score_tiles(self) -> int: def get_tiles( self, - empty_color: int = -6, - ) -> tuple[list[Tile], Tile | None]: + empty_color: int = Tile.blank, + ) -> tuple[list[int], int | None]: """Return tuple of tiles gathered, and then either the number one tile or None.""" tiles = [] number_one_tile = None @@ -1610,11 +1509,11 @@ def get_tiles( tiles.append(tile) for _i in range(self.size): - self.tiles.append(Tile(empty_color)) + self.tiles.append(int(empty_color)) self.image_update = True return tiles, number_one_tile - def can_place_tiles(self, tiles: list[Tile]) -> bool: + def can_place_tiles(self, tiles: list[int]) -> bool: """Return True.""" return True @@ -1627,9 +1526,9 @@ class Factory(Grid): outline = BLUE out_size = 0.1 - def __init__(self, game: Game, factory_id: int) -> None: + def __init__(self, factory_id: int) -> None: """Initialize factory.""" - super().__init__(self.size, game, background=None) + super().__init__(self.size, background=None) self.number = factory_id self.name = f"Factory{self.number}" @@ -1639,14 +1538,12 @@ def __init__(self, game: Game, factory_id: int) -> None: def __repr__(self) -> str: """Return representation of self.""" - return f"{self.__class__.__name__}(%r, %i)" % (self.game, self.number) + return f"{self.__class__.__name__}({self.number})" def add_circle(self, surface: pygame.surface.Surface) -> None: """Add circle to self.image.""" - # if f"FactoryCircle{self.radius}" not in self.game.cache: rad = math.ceil(self.radius) surf = pygame.surface.Surface((2 * rad, 2 * rad), SRCALPHA) -## surf = set_alpha(, 1) pygame.draw.circle(surf, self.outline, (rad, rad), rad) pygame.draw.circle( surf, @@ -1654,8 +1551,7 @@ def add_circle(self, surface: pygame.surface.Surface) -> None: (rad, rad), math.ceil(rad * (1 - self.out_size)), ) - # self.game.cache[f"FactoryCircle{self.radius}"] = surf - # surf = self.game.cache[f"FactoryCircle{self.radius}"].copy() + surface.blit( surf, ( @@ -1670,7 +1566,7 @@ def render(self, surface: pygame.surface.Surface) -> None: self.add_circle(surface) super().render(surface) - def fill(self, tiles: list[Tile]) -> None: + def fill(self, tiles: list[int]) -> None: """Fill self with tiles. Will raise exception if insufficiant tiles.""" if len(tiles) < self.size[0] * self.size[1]: size = self.size[0] * self.size[1] @@ -1687,19 +1583,19 @@ def fill(self, tiles: list[Tile]) -> None: if tiles: raise RuntimeError("Too many tiles!") - def grab(self) -> list[Tile]: + def grab(self) -> list[int]: """Return all tiles on this factory.""" return [ tile for tile in ( - self.get_tile((x, y), -6) + self.get_tile((x, y), Tile.blank) for x in range(self.size[0]) for y in range(self.size[1]) ) - if tile is not None and tile.color != -6 + if tile is not None and tile.color != Tile.blank ] - def grab_color(self, color: int) -> tuple[list[Tile], list[Tile]]: + def grab_color(self, color: int) -> tuple[list[int], list[int]]: """Return all tiles of color given in the first list, and all non-matches in the second list.""" tiles = self.grab() right, wrong = [], [] @@ -1722,7 +1618,7 @@ def process(self, time_passed: float) -> None: class Factories(MultipartObject): """Factories Multipart Object, made of multiple Factory Objects.""" - teach = 4 + tiles_each = 4 def __init__( self, @@ -1760,12 +1656,13 @@ def __repr__(self) -> str: def reset_position(self) -> None: """Reset the position of all factories within.""" - degrees = 360 / self.count - for i in range(self.count): - radians = math.radians(degrees * i) - self.objects[i].location = Vector2( - math.sin(radians) * self.size + self.location[0], - math.cos(radians) * self.size + self.location[1], + for index, degrees in enumerate(range(0, 360, 360 // self.count)): + self.objects[index].location = ( + Vector2.from_degrees( + degrees, + self.size, + ) + + self.location ) def process(self, time_passed: float) -> None: @@ -1797,13 +1694,13 @@ def process(self, time_passed: float) -> None: table.add_tiles(tocenter) cursor.drag(select) - def play_tiles_from_bag(self, empty_color: int = -6) -> None: + def play_tiles_from_bag(self, empty_color: int = Tile.blank) -> None: """Divy up tiles to each factory from the bag.""" # For every factory we have, for fid in range(self.count): # Draw tiles for the factory drawn = [] - for _i in range(self.teach): + for _i in range(self.tiles_each): # If the bag is not empty, if not self.game.bag.is_empty(): # Draw a tile from the bag. @@ -1828,7 +1725,7 @@ def play_tiles_from_bag(self, empty_color: int = -6) -> None: # while there are none left in the lid, start a new # round as usual even though are not all factory # displays are properly filled." - drawn.append(Tile(empty_color)) + drawn.append(int(empty_color)) # Place drawn tiles on factory factory = self.objects[fid] assert isinstance(factory, Factory) @@ -1844,245 +1741,98 @@ def is_all_empty(self) -> bool: return True -class TableCenter(Grid): +class TableCenter(TileRenderer): """Object that represents the center of the table.""" + __slots__ = ("tiles",) size = (6, 6) - first_tile_color = Tiles.one - def __init__(self, game: Game, has_number_one_tile: bool = True) -> None: + def __init__(self) -> None: """Initialize center of table.""" - super().__init__(self.size, game, background=None) - self.game = game - self.name = "TableCenter" - - self.number_one_tile_exists = False - if has_number_one_tile: - self.add_number_one_tile() - - self.next_position = (0, 0) + super().__init__("TableCenter", background=None) - def __repr__(self) -> str: - """Return representation of self.""" - return f"{self.__class__.__name__}({self.game!r})" - - def add_number_one_tile(self) -> None: - """Add the number one tile to the internal grid.""" - if not self.number_one_tile_exists: - x, y = self.size - self.place_tile((x - 1, y - 1), Tile(self.first_tile_color)) - self.number_one_tile_exists = True - - def add_tile(self, tile: Tile) -> None: - """Add a Tile Object to the Table Center Grid.""" - self.place_tile(self.next_position, tile) - x, y = self.next_position - x += 1 - y += int(x // self.size[0]) - x %= self.size[0] - y %= self.size[1] - self.next_position = (x, y) - self.image_update = True - - def add_tiles(self, tiles: Iterable[Tile], sort: bool = True) -> None: - """Add multiple Tile Objects to the Table Center Grid.""" - for tile in tiles: - self.add_tile(tile) - if sort and tiles: - self.reorder_tiles() - - def reorder_tiles(self, replace: int = -6) -> None: - """Re-organize tiles by Color.""" - full = [] - for y in range(self.size[1]): - for x in range(self.size[0]): - if self.number_one_tile_exists: - tile = self.get_info((x, y)) - assert tile is not None - if tile.color == self.first_tile_color: - continue - at = self.get_tile((x, y), replace) - - if at is not None: - full.append(at) - sorted_tiles = sorted(full, key=sort_tiles) - self.next_position = (0, 0) - self.add_tiles(sorted_tiles, False) - - def pull_tiles(self, tile_color: int, replace: int = -6) -> list[Tile]: - """Remove all of the tiles of tile_color from the Table Center Grid.""" - to_pull: list[tuple[int, int]] = [] - for y in range(self.size[1]): - for x in range(self.size[0]): - info_tile = self.get_info((x, y)) - assert info_tile is not None - if info_tile.color == tile_color: - to_pull.append((x, y)) - elif ( - self.number_one_tile_exists - and info_tile.color == self.first_tile_color - ): - to_pull.append((x, y)) - self.number_one_tile_exists = False - tiles = [] - for pos in to_pull: - tile = self.get_tile(pos, replace) - assert tile is not None - tiles.append(tile) - self.reorder_tiles(replace) - return tiles - - def process(self, time_passed: float) -> None: - """Process factories.""" - if self.hidden: - super().process(time_passed) - return - cursor = self.game.get_object_by_name("Cursor") - assert isinstance(cursor, Cursor) - if ( - cursor.is_pressed() - and not cursor.is_holding() - and not self.is_empty() - and self.is_selected(cursor.location) - ): - point = self.get_tile_point(cursor.location) - # Shouldn't return none anymore since we have is_selected now. - assert point is not None - tile = self.get_info(point) - assert isinstance(tile, Tile) - color_at_point = tile.color - if color_at_point >= 0 and color_at_point < 5: - cursor.drag(self.pull_tiles(color_at_point)) - super().process(time_passed) - - -class Bag(Component): - """Represents the bag full of tiles.""" - - __slots__ = ( - "percent_each", - "tile_count", - "tile_names", - "tile_types", - "tiles", - ) - - def __init__(self, tile_count: int = 100, tile_types: int = 5) -> None: - """Initialize bag of tiles.""" - super().__init__("bag") - self.tile_count = int(tile_count) - self.tile_types = int(tile_types) - self.tile_names = [chr(65 + i) for i in range(self.tile_types)] - self.percent_each = (self.tile_count / self.tile_types) / 100 - self.tiles: deque[str] - self.full_reset() - - def full_reset(self) -> None: - """Reset the bag to a full, re-randomized bag.""" - self.tiles = deque( - gen_random_proper_seq( - self.tile_count, - **dict.fromkeys(self.tile_names, self.percent_each), - ), - ) - - def __repr__(self) -> str: - """Return representation of self.""" - return f"{self.__class__.__name__}(%i, %i)" % ( - self.tile_count, - self.tile_types, - ) - - def reset(self) -> None: - """Randomize all the tiles in the bag.""" - self.tiles = deque(randomize(self.tiles)) - - def get_color(self, tile_name: str) -> int: - """Return the color of a named tile.""" - if tile_name not in self.tile_names: - raise ValueError(f"Tile Name {tile_name} Not Found!") - return self.tile_names.index(tile_name) - - def get_tile(self, tile_name: str) -> Tile: - """Return a Tile Object from a tile name.""" - return Tile(self.get_color(tile_name)) - - def get_count(self) -> int: - """Return number of tiles currently held.""" - return len(self.tiles) - - def is_empty(self) -> bool: - """Return True if no tiles are currently held.""" - return self.get_count() == 0 - - def draw_tile(self) -> Tile | None: - """Return a random Tile Object from the bag. Return None if no tiles to draw.""" - if not self.is_empty(): - return self.get_tile(self.tiles.pop()) - return None - - def get_name(self, tile_color: int) -> str: - """Return the name of a tile given it's color.""" - try: - return self.tile_names[tile_color] - except IndexError as exc: - raise ValueError("Invalid Tile Color!") from exc - - def add_tile(self, tile_object: Tile) -> None: - """Add a Tile Object to the bag.""" - name = self.get_name(int(tile_object.color)) - range_ = (0, len(self.tiles) - 1) - if range_[1] - range_[0] <= 1: - index = 0 - else: - # S311 Standard pseudo-random generators are not suitable for cryptographic purposes - index = random.randint(range_[0], range_[1]) # noqa: S311 - # self.tiles.insert(random.randint(0, len(self.tiles)-1), self.get_name(int(tile_object.color))) - self.tiles.insert(index, name) - del tile_object - - def add_tiles(self, tile_objects: Iterable[Tile]) -> None: - """Add multiple Tile Objects to the bag.""" - for tile_object in tile_objects: - self.add_tile(tile_object) - - -class BoxLid(Component): - """BoxLid Object, represents the box lid were tiles go before being added to the bag again.""" - - def __init__(self) -> None: - """Initialize box lid.""" - super().__init__("BoxLid") - self.tiles: deque[Tile] = deque() + self.tiles: Counter[int] = Counter() + self.update_image() + self.visible = True def __repr__(self) -> str: """Return representation of self.""" return f"{self.__class__.__name__}()" - def add_tile(self, tile: Tile) -> None: - """Add a tile to self.""" - if tile.color >= 0 and tile.color < 5: - self.tiles.append(tile) - return - raise ValueError( - f"BoxLid.add_tile tried to add an invalid tile to self ({tile.color = }).", - ) + def iter_tiles(self) -> Generator[int, None, None]: + """Yield tile colors.""" + count = 0 + for tile_type in sorted(set(self.tiles) - {Tile.one}): + tile_count = self.tiles[tile_type] + for _ in range(tile_count): + yield tile_type + count += 1 - def add_tiles(self, tiles: Iterable[Tile]) -> None: - """Add multiple tiles to self.""" - for tile in tiles: - self.add_tile(tile) + width, height = self.size + remaining = width * height - count - def get_tiles(self) -> list[Tile]: - """Return all tiles in self while deleting them from self.""" - return [self.tiles.popleft() for i in range(len(self.tiles))] + one_count = self.tiles.get(Tile.one, 0) + remaining = max(remaining - one_count, 0) + for _ in range(remaining): + yield Tile.blank + for _ in range(one_count): + yield Tile.one - def is_empty(self) -> bool: - """Return True if self is empty (no tiles on it).""" - return len(self.tiles) == 0 + def update_image(self): + """Reset/update image.""" + self.clear_image(self.size) + width, height = self.size + tile_generator = self.iter_tiles() + for y in range(height): + for x in range(width): + tile = next(tile_generator) + # if tile == Tile.blank: + # continue + self.blit_tile(tile, (x, y)) + self.dirty = 1 -class Player(MultipartObject): + def add_tile(self, tile: int) -> None: + """Add a tile to the center of the table.""" + self.tiles.update((tile,)) + self.update_image() + + def add_tiles(self, tiles: Iterable[int]) -> None: + """Add multiple int Objects to the Table Center Grid.""" + self.tiles.update(tiles) + self.update_image() + + def pull_tiles(self, tile_color: int) -> list[int]: + """Pop all of tile_color. Raises KeyError if not exists.""" + tile_count = self.tiles.pop(tile_color) + return [tile_color] * tile_count + + +## def process(self, time_passed: float) -> None: +## """Process factories.""" +## if self.hidden: +## super().process(time_passed) +## return +## cursor = self.game.get_object_by_name("Cursor") +## assert isinstance(cursor, Cursor) +## if ( +## cursor.is_pressed() +## and not cursor.is_holding() +## and not self.is_empty() +## and self.is_selected(cursor.location) +## ): +## point = self.get_tile_point(cursor.location) +## # Shouldn't return none anymore since we have is_selected now. +## assert point is not None +## tile = self.get_info(point) +## assert isinstance(tile, int) +## color_at_point = tile.color +## if color_at_point >= 0 and color_at_point < 5: +## cursor.drag(self.pull_tiles(color_at_point)) +## super().process(time_passed) + + +class Player(sprite.Sprite): """Represents a player. Made of lots of objects.""" def __init__( @@ -2100,10 +1850,10 @@ def __init__( self.networked = networked self.varient_play = varient_play - self.add_object(Board(self, self.varient_play)) + self.add_object(Board(self.varient_play)) self.add_object(PatternLine(self)) self.add_object(FloorLine(self)) - self.add_object(Text(SCOREFONTSIZE, SCORECOLOR)) + ## self.add_object(objects.Text(SCOREFONTSIZE, SCORECOLOR)) self.score = 0 self.is_turn = False @@ -2124,11 +1874,11 @@ def __repr__(self) -> str: self.varient_play, ) - def update_score(self) -> None: - """Update the scorebox for this player.""" - score_box = self.get_object_by_name("Text") - assert isinstance(score_box, Text) - score_box.update_value(f"Player {self.player_id + 1}: {self.score}") + ## def update_score(self) -> None: + ## """Update the scorebox for this player.""" + ## score_box = self.get_object_by_name("Text") + ## assert isinstance(score_box, Text) + ## score_box.update_value(f"Player {self.player_id + 1}: {self.score}") def trigger_turn_now(self) -> None: """Handle start of turn.""" @@ -2157,17 +1907,17 @@ def end_of_turn(self) -> None: pattern_line.set_background(None) self.is_turn = False - def end_of_game_trigger(self) -> None: - """Handle end of game. - - Called by end state when game is over - Hide pattern lines and floor line. - """ - pattern = self.get_object_by_name("PatternLine") - floor = self.get_object_by_name("floor_line") - - pattern.hidden = True - floor.hidden = True + ## def end_of_game_trigger(self) -> None: + ## """Handle end of game. + ## + ## Called by end state when game is over + ## Hide pattern lines and floor line. + ## """ + ## pattern = self.get_object_by_name("PatternLine") + ## floor = self.get_object_by_name("floor_line") + ## + ## pattern.hidden = True + ## floor.hidden = True def reset_position(self) -> None: """Reset positions of all parts of self based off self.location.""" @@ -2175,17 +1925,16 @@ def reset_position(self) -> None: board = self.get_object_by_name("Board") assert isinstance(board, Board) - bw, bh = board.wh + bw, bh = board.width_height board.location = Vector2(x + bw // 2, y) pattern_line = self.get_object_by_name("PatternLine") assert isinstance(pattern_line, PatternLine) - lw = pattern_line.wh[0] // 2 + lw = pattern_line.width_height[0] // 2 pattern_line.location = Vector2(x - lw, y) floor_line = self.get_object_by_name("floor_line") assert isinstance(floor_line, FloorLine) - floor_line.wh[0] floor_line.location = Vector2( int(x - lw * (2 / 3) + TILESIZE / 3.75), int(y + bh * (2 / 3)), @@ -2195,217 +1944,218 @@ def reset_position(self) -> None: assert isinstance(text, Text) text.location = Vector2(x - (bw // 3), y - (bh * 2 // 3)) - def wall_tiling(self) -> None: - """Do the wall tiling phase of the game for this player.""" - self.is_wall_tiling = True - pattern_line = self.get_object_by_name("PatternLine") - assert isinstance(pattern_line, PatternLine) - board = self.get_object_by_name("Board") - assert isinstance(board, Board) - box_lid = self.game.get_object_by_name("BoxLid") - assert isinstance(box_lid, BoxLid) - - data = pattern_line.wall_tiling() - tiles_for_box = data["tiles_for_box"] - assert isinstance(tiles_for_box, list) - box_lid.add_tiles(tiles_for_box) - del data["tiles_for_box"] - - cleaned = {} - for key, value in data.items(): - if not isinstance(value, Tile): - continue - cleaned[int(key)] = value - - board.wall_tiling_mode(cleaned) - - def done_wall_tiling(self) -> bool: - """Return True if internal Board is done wall tiling.""" - board = self.get_object_by_name("Board") - assert isinstance(board, Board) - return not board.is_wall_tiling() - - def next_round(self) -> None: - """Handle end of wall tiling.""" - self.is_wall_tiling = False - - def score_phase(self) -> Tile | None: - """Do the scoring phase of the game for this player. Return number one tile or None.""" - board = self.get_object_by_name("Board") - floor_line = self.get_object_by_name("floor_line") - box_lid = self.game.get_object_by_name("BoxLid") - assert isinstance(board, Board) - assert isinstance(floor_line, FloorLine) - assert isinstance(box_lid, BoxLid) - - def saturatescore() -> None: - if self.score < 0: - self.score = 0 - - self.score += board.score_additions() - self.score += floor_line.score_tiles() - saturatescore() - - tiles_for_box, number_one = floor_line.get_tiles() - box_lid.add_tiles(tiles_for_box) - - self.update_score() - - return number_one - def end_of_game_scoring(self) -> None: - """Update final score with additional end of game points.""" - board = self.get_object_by_name("Board") - assert isinstance(board, Board) - - self.score += board.end_of_game_scoreing() - - self.update_score() - - def has_horzontal_line(self) -> bool: - """Return True if this player has a horizontal line on their game board filled.""" - board = self.get_object_by_name("Board") - assert isinstance(board, Board) - - return board.has_filled_row() - - def get_horizontal_lines(self) -> int: - """Return the number of filled horizontal lines this player has on their game board.""" - board = self.get_object_by_name("Board") - assert isinstance(board, Board) - - return board.get_filled_rows() - - def process(self, time_passed: float) -> None: - """Process Player.""" - if not self.is_turn: # Is our turn? - self.set_attr_all("hidden", self.hidden) - super().process(time_passed) - return - if self.hidden and self.is_wall_tiling and self.varient_play: - # If hidden, not anymore. Our turn. - self.hidden = False - if self.networked: # We are networked. - self.set_attr_all("hidden", self.hidden) - super().process(time_passed) - return +## def wall_tiling(self) -> None: +## """Do the wall tiling phase of the game for this player.""" +## self.is_wall_tiling = True +## pattern_line = self.get_object_by_name("PatternLine") +## assert isinstance(pattern_line, PatternLine) +## board = self.get_object_by_name("Board") +## assert isinstance(board, Board) +## box_lid = self.game.get_object_by_name("BoxLid") +## assert isinstance(box_lid, BoxLid) +## +## data = pattern_line.wall_tiling() +## tiles_for_box = data["tiles_for_box"] +## assert isinstance(tiles_for_box, list) +## box_lid.add_tiles(tiles_for_box) +## del data["tiles_for_box"] +## +## cleaned = {} +## for key, value in data.items(): +## if not isinstance(value, int): +## continue +## cleaned[int(key)] = value +## +## board.wall_tiling_mode(cleaned) + +## def done_wall_tiling(self) -> bool: +## """Return True if internal Board is done wall tiling.""" +## board = self.get_object_by_name("Board") +## assert isinstance(board, Board) +## return not board.is_wall_tiling() + +## def next_round(self) -> None: +## """Handle end of wall tiling.""" +## self.is_wall_tiling = False + +## def score_phase(self) -> int | None: +## """Do the scoring phase of the game for this player. Return number one tile or None.""" +## board = self.get_object_by_name("Board") +## floor_line = self.get_object_by_name("floor_line") +## box_lid = self.game.get_object_by_name("BoxLid") +## assert isinstance(board, Board) +## assert isinstance(floor_line, FloorLine) +## assert isinstance(box_lid, BoxLid) +## +## def saturatescore() -> None: +## if self.score < 0: +## self.score = 0 +## +## self.score += board.score_additions() +## self.score += floor_line.score_tiles() +## saturatescore() +## +## tiles_for_box, number_one = floor_line.get_tiles() +## box_lid.add_tiles(tiles_for_box) +## +## self.update_score() +## +## return number_one - cursor = self.game.get_object_by_name("Cursor") - assert isinstance(cursor, Cursor) - box_lid = self.game.get_object_by_name("BoxLid") - assert isinstance(box_lid, BoxLid) - pattern_line = self.get_object_by_name("PatternLine") - assert isinstance(pattern_line, PatternLine) - floor_line = self.get_object_by_name("floor_line") - assert isinstance(floor_line, FloorLine) - board = self.get_object_by_name("Board") - assert isinstance(board, Board) +## def end_of_game_scoring(self) -> None: +## """Update final score with additional end of game points.""" +## board = self.get_object_by_name("Board") +## assert isinstance(board, Board) +## +## self.score += board.end_of_game_scoreing() +## +## self.update_score() - if not cursor.is_pressed(): - # Mouse up - if self.just_held: - self.just_held = False - if self.just_dropped: - self.just_dropped = False - self.set_attr_all("hidden", self.hidden) - super().process(time_passed) - return +## def has_horzontal_line(self) -> bool: +## """Return True if this player has a horizontal line on their game board filled.""" +## board = self.get_object_by_name("Board") +## assert isinstance(board, Board) +## +## return board.has_filled_row() - # Mouse down - obj, point = self.get_intersection(cursor.location) - if obj is None or point is None: - if self.is_wall_tiling and self.done_wall_tiling(): - self.next_round() - self.game.next_turn() - self.set_attr_all("hidden", self.hidden) - super().process(time_passed) - return - # Something pressed - if cursor.is_holding(): # Cursor holding tiles - move_made = False - if not self.is_wall_tiling: # Is wall tiling: - if obj == "PatternLine": - pos, row_number = point - row = pattern_line.get_row(row_number) - if not row.is_full(): - info = row.get_info(pos) - if info is not None and info.color < 0: - _color, _held = cursor.get_held_info() - todrop = min( - pos + 1, - row.get_placeable(), - ) - tiles = cursor.drop(todrop) - if row.can_place_tiles(tiles): - row.place_tiles(tiles) - move_made = True - else: - cursor.force_hold(tiles) - elif obj == "floor_line": - tiles_to_add = cursor.drop() - if floor_line.is_full(): - # Floor is full, - # Add tiles to box instead. - box_lid.add_tiles(tiles_to_add) - elif floor_line.get_placeable() < len( - tiles_to_add, - ): - # Floor is not full but cannot fit all in floor line. - # Add tiles to floor line and then to box - while len(tiles_to_add) > 0: - if floor_line.get_placeable() > 0: - floor_line.place_tile( - tiles_to_add.pop(), - ) - else: - box_lid.add_tile( - tiles_to_add.pop(), - ) - else: - # Otherwise add to floor line for all. - floor_line.place_tiles(tiles_to_add) - move_made = True - elif not self.just_held and obj == "Board": - tile = board.get_info(point) - assert isinstance(tile, Tile) - if tile.color == -6: - # Cursor holding and wall tiling - _column, row_id = point - cursor_tile = cursor.drop(1)[0] - board_tile = board.get_tile_for_cursor_by_row( - row_id, - ) - if ( - board_tile is not None - and cursor_tile.color == board_tile.color - and board.wall_tile_from_point(point) - ): - self.just_dropped = True - pattern_line.get_row( - row_id, - ).set_background(None) - if move_made and not self.is_wall_tiling: - if cursor.holding_number_one: - one_tile = cursor.drop_one_tile() - assert one_tile is not None - floor_line.place_tile(one_tile) - if cursor.get_held_count(True) == 0: - self.game.next_turn() - elif self.is_wall_tiling and obj == "Board" and not self.just_dropped: - # Mouse down, something pressed, and not holding anything - # Wall tiling, pressed, not holding - _column_number, row_number = point - tile = board.get_tile_for_cursor_by_row( - row_number, - ) - if tile is not None: - cursor.drag([tile]) - self.just_held = True - if self.is_wall_tiling and self.done_wall_tiling(): - self.next_round() - self.game.next_turn() - self.set_attr_all("hidden", self.hidden) - super().process(time_passed) +## def get_horizontal_lines(self) -> int: +## """Return the number of filled horizontal lines this player has on their game board.""" +## board = self.get_object_by_name("Board") +## assert isinstance(board, Board) +## +## return board.get_filled_rows() + +## def process(self, time_passed: float) -> None: +## """Process Player.""" +## if not self.is_turn: # Is our turn? +## self.set_attr_all("hidden", self.hidden) +## super().process(time_passed) +## return +## if self.hidden and self.is_wall_tiling and self.varient_play: +## # If hidden, not anymore. Our turn. +## self.hidden = False +## if self.networked: # We are networked. +## self.set_attr_all("hidden", self.hidden) +## super().process(time_passed) +## return +## +## cursor = self.game.get_object_by_name("Cursor") +## assert isinstance(cursor, Cursor) +## box_lid = self.game.get_object_by_name("BoxLid") +## assert isinstance(box_lid, BoxLid) +## pattern_line = self.get_object_by_name("PatternLine") +## assert isinstance(pattern_line, PatternLine) +## floor_line = self.get_object_by_name("floor_line") +## assert isinstance(floor_line, FloorLine) +## board = self.get_object_by_name("Board") +## assert isinstance(board, Board) +## +## if not cursor.is_pressed(): +## # Mouse up +## if self.just_held: +## self.just_held = False +## if self.just_dropped: +## self.just_dropped = False +## self.set_attr_all("hidden", self.hidden) +## super().process(time_passed) +## return +## +## # Mouse down +## obj, point = self.get_intersection(cursor.location) +## if obj is None or point is None: +## if self.is_wall_tiling and self.done_wall_tiling(): +## self.next_round() +## self.game.next_turn() +## self.set_attr_all("hidden", self.hidden) +## super().process(time_passed) +## return +## # Something pressed +## if cursor.is_holding(): # Cursor holding tiles +## move_made = False +## if not self.is_wall_tiling: # Is wall tiling: +## if obj == "PatternLine": +## pos, row_number = point +## row = pattern_line.get_row(row_number) +## if not row.is_full(): +## info = row.get_info(pos) +## if info is not None and info.color < 0: +## _color, _held = cursor.get_held_info() +## todrop = min( +## pos + 1, +## row.get_placeable(), +## ) +## tiles = cursor.drop(todrop) +## if row.can_place_tiles(tiles): +## row.place_tiles(tiles) +## move_made = True +## else: +## cursor.force_hold(tiles) +## elif obj == "floor_line": +## tiles_to_add = cursor.drop() +## if floor_line.is_full(): +## # Floor is full, +## # Add tiles to box instead. +## box_lid.add_tiles(tiles_to_add) +## elif floor_line.get_placeable() < len( +## tiles_to_add, +## ): +## # Floor is not full but cannot fit all in floor line. +## # Add tiles to floor line and then to box +## while len(tiles_to_add) > 0: +## if floor_line.get_placeable() > 0: +## floor_line.place_tile( +## tiles_to_add.pop(), +## ) +## else: +## box_lid.add_tile( +## tiles_to_add.pop(), +## ) +## else: +## # Otherwise add to floor line for all. +## floor_line.place_tiles(tiles_to_add) +## move_made = True +## elif not self.just_held and obj == "Board": +## tile = board.get_info(point) +## assert isinstance(tile, int) +## if tile.color == Tile.blank: +## # Cursor holding and wall tiling +## _column, row_id = point +## cursor_tile = cursor.drop(1)[0] +## board_tile = board.get_tile_for_cursor_by_row( +## row_id, +## ) +## if ( +## board_tile is not None +## and cursor_tile.color == board_tile.color +## and board.wall_tile_from_point(point) +## ): +## self.just_dropped = True +## pattern_line.get_row( +## row_id, +## ).set_background(None) +## if move_made and not self.is_wall_tiling: +## if cursor.holding_number_one: +## one_tile = cursor.drop_one_tile() +## assert one_tile is not None +## floor_line.place_tile(one_tile) +## if cursor.get_held_count(True) == 0: +## self.game.next_turn() +## elif self.is_wall_tiling and obj == "Board" and not self.just_dropped: +## # Mouse down, something pressed, and not holding anything +## # Wall tiling, pressed, not holding +## _column_number, row_number = point +## tile = board.get_tile_for_cursor_by_row( +## row_number, +## ) +## if tile is not None: +## cursor.drag([tile]) +## self.just_held = True +## if self.is_wall_tiling and self.done_wall_tiling(): +## self.next_round() +## self.game.next_turn() +## self.set_attr_all("hidden", self.hidden) +## super().process(time_passed) class HaltState(AsyncState["AzulClient"]): @@ -2523,7 +2273,7 @@ def add_button( size: int = fontsize, minlen: int = button_minimum, ) -> int: - """Add a new Button object to group""" + """Add a new Button object to group.""" button = KwargButton( name, font=pygame.font.Font(FONT, size), @@ -2660,6 +2410,54 @@ async def check_conditions(self) -> str: return "title" +class InitializeState(GameState): + """Initialize state.""" + + __slots__ = () + + def __init__(self) -> None: + """Initialize self.""" + super().__init__("initialize") + + async def entry_actions(self) -> None: + """Set up buttons.""" + assert self.machine is not None + self.id = self.machine.new_group("initialize") + + self.group_add(Cursor()) + await self.manager.raise_event(Event("cursor_drag", [3, 5])) + self.manager.register_handler("PygameMouseMotion", self.mouse_moved) + + ## board = Board() + #### board.place_tile((2, 2), Tile.red) + ## board.location = Vector2.from_iter(SCREEN_SIZE) // 2 + ## self.group_add(board) + + center = TableCenter() + center.location = Vector2.from_iter(SCREEN_SIZE) // 2 + self.group_add(center) + center.add_tiles((0, 1, 2, 3, 5)) + + async def mouse_moved( + self, + event: Event[sprite.PygameMouseMotion], + ) -> None: + ## print(f'{event = }') + await self.manager.raise_event( + Event("cursor_set_location", event.data["pos"]), + ) + + +## await self.manager.raise_event( +## Event("cursor_set_destination", event.data["pos"]), +## ) + + +## async def check_conditions(self) -> str: +## """Go to title state.""" +## return "title" + + class TitleState(MenuState): """Game state when the title screen is up.""" @@ -2688,7 +2486,7 @@ async def entry_actions(self) -> None: ) title_text.location = (SCREEN_SIZE[0] // 2, title_text.rect.h) self.group_add(title_text) - + hosting_button = KwargButton( "hosting_button", button_font, @@ -2847,9 +2645,9 @@ def host_text(x: object) -> str: size=int(self.fontsize / 1.5), ) -## # TEMPORARY: Hide everything to do with "Host Mode", networked games aren't done yet. -## assert self.game is not None -## self.game.set_attr_all("visible", False) + ## # TEMPORARY: Hide everything to do with "Host Mode", networked games aren't done yet. + ## assert self.game is not None + ## self.game.set_attr_all("visible", False) def varient_text(x: object) -> str: return f"Variant Play: {x}" @@ -3015,7 +2813,6 @@ def exit_actions(self) -> None: self.game.player_turn = nturn - class PhasePrepareNext(GameState): """Prepare next phase of game.""" @@ -3052,8 +2849,6 @@ def check_state(self) -> str: return "End" - - class EndScreen(MenuState): """End screen state.""" @@ -3172,8 +2967,6 @@ def entry_actions(self) -> None: y += self.bh - - class Game(ObjectHandler): """Game object, contains most of what's required for Azul.""" @@ -3198,10 +2991,10 @@ def __init__(self) -> None: PhaseWallTiling(), PhasePrepareNext(), EndScreen(), - PhaseFactoryOfferNetworked(), - PhaseWallTilingNetworked(), - PhasePrepareNextNetworked(), - EndScreenNetworked(), + ## PhaseFactoryOfferNetworked(), + ## PhaseWallTilingNetworked(), + ## PhasePrepareNextNetworked(), + ## EndScreenNetworked(), ], ) self.initialized_state = False @@ -3568,11 +3361,11 @@ async def entry_actions(self) -> None: self.id = self.machine.new_group("play") # self.group_add(()) -## gameboard = GameBoard( -## 45, -## ) -## gameboard.location = [x // 2 for x in SCREEN_SIZE] -## self.group_add(gameboard) + ## gameboard = GameBoard( + ## 45, + ## ) + ## gameboard.location = [x // 2 for x in SCREEN_SIZE] + ## self.group_add(gameboard) await self.machine.raise_event(Event("init", None)) @@ -3603,7 +3396,7 @@ async def exit_actions(self) -> None: async def handle_game_over(self, event: Event[int]) -> None: """Handle game over event.""" winner = event.data - self.exit_data = (0, f"{PLAYERS[winner]} Won", False) + self.exit_data = (0, f"{winner} Won", False) await self.machine.raise_event_internal(Event("network_stop", None)) @@ -3714,10 +3507,10 @@ async def async_run() -> None: screen = pygame.display.set_mode(SCREEN_SIZE, RESIZABLE, 16, vsync=VSYNC) pygame.display.set_caption(f"{__title__} v{__version__}") # pygame.display.set_icon(pygame.image.load('icon.png')) - pygame.display.set_icon(get_tile_image(Tiles.one, 32)) + pygame.display.set_icon(get_tile_image(Tile.one, 32)) screen.fill((0xFF, 0xFF, 0xFF)) -## try: + ## try: async with trio.open_nursery() as main_nursery: event_manager = ExternalRaiseManager( "checkers", @@ -3843,15 +3636,15 @@ def cli_run() -> None: except ExceptionGroup as exc: print(exc) exception = traceback.format_exception(exc) -## raise -## except BaseException as ex: -## screenshot_last_frame() -## # errorbox.errorbox('Error', f'A {type(ex).__name__} Error Has Occored: {", ".join(ex.args)}') -## raise + ## raise + ## except BaseException as ex: + ## screenshot_last_frame() + ## # errorbox.errorbox('Error', f'A {type(ex).__name__} Error Has Occored: {", ".join(ex.args)}') + ## raise finally: pygame.quit() if exception is not None: - print(''.join(exception), file=sys.stderr) + print("".join(exception), file=sys.stderr) if __name__ == "__main__": diff --git a/src/azul/server.py b/src/azul/server.py index cfe9eab..2fc6c76 100755 --- a/src/azul/server.py +++ b/src/azul/server.py @@ -55,7 +55,7 @@ ServerBoundEvents, find_ip, ) -from azul.state import State, generate_pieces +from azul.state import State if TYPE_CHECKING: from collections.abc import Awaitable, Callable, Iterable @@ -130,7 +130,7 @@ async def handle_initial_config( buffer = Buffer() -## write_position(buffer, board_size) + ## write_position(buffer, board_size) buffer.write_value(StructFormat.UBYTE, 0) buffer.write_value(StructFormat.UBYTE, player_turn) @@ -259,7 +259,7 @@ def __init__(self, internal_singleplayer_mode: bool = False) -> None: super().__init__("GameServer") self.client_count: int - self.state: CheckersState = State(self.board_size, {}) + self.state = State.new_game(0) self.client_players: dict[int, int] = {} self.player_selections: dict[int, Pos] = {} @@ -276,8 +276,6 @@ def bind_handlers(self) -> None: "server_start": self.start_server, "network_stop": self.stop_server, "server_send_game_start": self.handle_server_start_new_game, - "network->select_piece": self.handle_network_select_piece, - "network->select_tile": self.handle_network_select_tile, }, ) @@ -387,8 +385,8 @@ def new_game_init(self) -> None: self.client_players.clear() self.player_selections.clear() - pieces = generate_pieces(*self.board_size) - self.state = State(self.board_size, pieces) + ## pieces = generate_pieces(*self.board_size) + self.state = State.new_game(self.client_count) # Why keep track of another object just to know client ID numbers # if we already have that with the components? No need! @@ -437,25 +435,25 @@ async def transmit_playing_as(self) -> None: async def handle_server_start_new_game(self, event: Event[None]) -> None: """Handle game start.""" -## # Delete all pieces from last state (shouldn't be needed but still.) -## async with trio.open_nursery() as nursery: -## for piece_pos, _piece_type in self.state.get_pieces(): -## nursery.start_soon( -## self.raise_event, -## Event("delete_piece->network", piece_pos), -## ) + ## # Delete all pieces from last state (shouldn't be needed but still.) + ## async with trio.open_nursery() as nursery: + ## for piece_pos, _piece_type in self.state.get_pieces(): + ## nursery.start_soon( + ## self.raise_event, + ## Event("delete_piece->network", piece_pos), + ## ) # Choose which team plays first # Using non-cryptographically secure random because it doesn't matter self.new_game_init() -## # Send create_piece events for all pieces -## async with trio.open_nursery() as nursery: -## for piece_pos, piece_type in self.state.get_pieces(): -## nursery.start_soon( -## self.raise_event, -## Event("create_piece->network", (piece_pos, piece_type)), -## ) + ## # Send create_piece events for all pieces + ## async with trio.open_nursery() as nursery: + ## for piece_pos, piece_type in self.state.get_pieces(): + ## nursery.start_soon( + ## self.raise_event, + ## Event("create_piece->network", (piece_pos, piece_type)), + ## ) await self.transmit_playing_as() @@ -537,16 +535,16 @@ async def send_spectator_join_packets( ) with self.temporary_component(private_events_pocket): with private_events_pocket.temporary_component(client): -## # Send create_piece events for all pieces -## async with trio.open_nursery() as nursery: -## for piece_pos, piece_type in self.state.get_pieces(): -## nursery.start_soon( -## client.raise_event, -## Event( -## "create_piece->network", -## (piece_pos, piece_type), -## ), -## ) + ## # Send create_piece events for all pieces + ## async with trio.open_nursery() as nursery: + ## for piece_pos, piece_type in self.state.get_pieces(): + ## nursery.start_soon( + ## client.raise_event, + ## Event( + ## "create_piece->network", + ## (piece_pos, piece_type), + ## ), + ## ) await client.raise_event( Event(f"playing_as->network[{client.client_id}]", 255), @@ -744,79 +742,6 @@ async def handle_action_animations( else: raise NotImplementedError(f"Animation for action {name}") - async def handle_network_select_tile( - self, - event: Event[tuple[int, Pos]], - ) -> None: - """Handle select tile event from network.""" - client_id, tile_pos = event.data - - player = self.client_players.get(client_id, 0xFF) - if player == 2: - player = int(self.state.turn) - - if not self.players_can_interact: - print( - f"{player = } cannot select tile {tile_pos = } because players_can_interact is False", - ) - return - - if player != self.state.turn: - print( - f"{player = } cannot select tile {tile_pos = } because it is not their turn.", - ) - return - - piece_pos = self.player_selections.get(player) - if piece_pos is None: - print( - f"{player = } cannot select tile {tile_pos = } because has no selection", - ) - return - - if tile_pos not in self.state.calculate_actions(piece_pos).ends: - print( - f"{player = } cannot select tile {piece_pos!r} because not valid move", - ) - return - - self.players_can_interact = False # No one moves during animation - # Send animation state start event - await self.raise_event(Event("animation_state->network", True)) - - # Remove tile sprites and glowing effect - await self.player_select_piece(player, None) - - action = self.state.action_from_points(piece_pos, tile_pos) - # print(f"{action = }") - - # Get new state after performing valid action - new_state = self.state.preform_action(action) - # Get action queue from old state - action_queue = self.state.get_action_queue() - self.state = new_state - - # Send action animations - await self.handle_action_animations(action_queue) - - # Send action complete event - await self.raise_event( - Event( - "action_complete->network", - (piece_pos, tile_pos, self.state.turn), - ), - ) - - win_value = self.state.check_for_win() - if win_value is not None: - # If we have a winner, send game over event. - await self.raise_event(Event("game_over->network", win_value)) - return - - # If not game over, allow interactions so next player can take turn - self.players_can_interact = True - await self.raise_event(Event("animation_state->network", False)) - def __del__(self) -> None: """Debug print.""" print(f"del {self.__class__.__name__}") diff --git a/src/azul/sprite.py b/src/azul/sprite.py index f98e611..1335e74 100644 --- a/src/azul/sprite.py +++ b/src/azul/sprite.py @@ -429,7 +429,7 @@ async def tick(self, tick_event: Event[TickEventData]) -> None: await trio.lowlevel.checkpoint() passed = tick_event.data.time_passed - new = None + new: int | str | None = None if self.update_every == 0: new = self.fetch_controller_new_state() else: @@ -501,6 +501,9 @@ def move_heading_time(self, time_passed: float) -> None: class TargetingComponent(Component): """Sprite that moves toward a destination and then stops. + Registered Component Name: + targeting + Requires components: Sprite MovementComponent @@ -526,6 +529,7 @@ def update_heading(self) -> None: """Update the heading of the movement component.""" movement = cast(MovementComponent, self.get_component("movement")) to_dest = self.to_destination() + # If magnitude is zero if to_dest @ to_dest == 0: movement.heading = Vector2(0, 0) return @@ -555,6 +559,7 @@ def to_destination(self) -> Vector2: async def move_destination_time(self, time_passed: float) -> None: """Move with time_passed.""" if self.__reached: + await trio.lowlevel.checkpoint() return sprite, movement = cast( @@ -566,16 +571,25 @@ async def move_destination_time(self, time_passed: float) -> None: self.__reached = True await self.raise_event(Event(self.event_raise_name, None)) return - await trio.lowlevel.checkpoint() + to_destination = self.to_destination() travel_distance = min( - self.to_destination().magnitude(), + to_destination @ to_destination, movement.speed * time_passed, ) if travel_distance > 0: movement.move_heading_distance(travel_distance) - self.update_heading() # Fix imprecision + # Fix imprecision + self.update_heading() + await trio.lowlevel.checkpoint() + + async def move_destination_time_ticks( + self, + event: Event[TickEventData], + ) -> None: + """Move with tick data.""" + await self.move_destination_time(event.data.time_passed) class DragEvent(NamedTuple): diff --git a/src/azul/state.py b/src/azul/state.py index 114a6a9..05236ff 100644 --- a/src/azul/state.py +++ b/src/azul/state.py @@ -1,10 +1,10 @@ -"""Checkers State.""" +"""Azul State.""" # Programmed by CoolCat467 from __future__ import annotations -# Copyright (C) 2023-2024 CoolCat467 +# Copyright (C) 2024 CoolCat467 # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -19,443 +19,1042 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -__title__ = "Checkers State" +__title__ = "Azul State" __author__ = "CoolCat467" __license__ = "GNU General Public License Version 3" __version__ = "0.0.0" -import copy -import math -from dataclasses import dataclass + +import random +from collections import Counter +from enum import IntEnum, auto from typing import ( TYPE_CHECKING, + Any, + Final, NamedTuple, - TypeAlias, TypeVar, - cast, ) -from mypy_extensions import u8 +from numpy import array, int8 if TYPE_CHECKING: - from collections.abc import Callable, Generator, Iterable + from collections.abc import Generator + from numpy.typing import NDArray from typing_extensions import Self -MANDATORY_CAPTURE = True # If a jump is available, do you have to or not? -PAWN_JUMP_FORWARD_ONLY = True # Pawns not allowed to go backwards in jumps? +T = TypeVar("T") -# Note: Tile Ids are chess board tile titles, A1 to H8 -# A8 ... H8 -# ......... -# A1 ... H1 +FLOOR_LINE_COUNT: Final = 7 -# Player: -# 0 = False = Red = MIN = 0, 2 -# 1 = True = Black = MAX = 1, 3 -T = TypeVar("T") +def floor_line_subtract_generator(seed: int = 1) -> Generator[int, None, None]: + """Floor Line subtraction number generator. Can continue indefinitely.""" + while True: + yield from (-seed,) * (seed + 1) + seed += 1 + + +FLOOR_LINE_DATA: Final = tuple( + value + for _, value in zip( + range(FLOOR_LINE_COUNT), + floor_line_subtract_generator(), + strict=False, + ) +) + + +class Tile(IntEnum): + """All type types.""" + + blank = -6 + fake_cyan = -5 + fake_black = -4 + fake_red = -3 + fake_yellow = -2 + fake_blue = -1 + blue = 0 + yellow = auto() + red = auto() + black = auto() + cyan = auto() + one = auto() + + +REAL_TILES: Final = {Tile.blue, Tile.yellow, Tile.red, Tile.black, Tile.cyan} + + +class Phase(IntEnum): + """Game phases.""" + + factory_offer = 0 + wall_tiling = auto() + end = auto() + + +def generate_bag_contents() -> Counter[int]: + """Generate and return unrandomized bag.""" + tile_types = 5 + tile_count = 100 + count_each = tile_count // tile_types + return Counter({type_: count_each for type_ in range(tile_types)}) -Pos: TypeAlias = tuple[u8, u8] - - -class Action(NamedTuple): - """Represents an action.""" - - from_pos: Pos - to_pos: Pos - - -class ActionSet(NamedTuple): - """Represents a set of actions.""" - - jumps: dict[Pos, list[Pos]] - moves: tuple[Pos, ...] - ends: set[Pos] - - -def get_sides(xy: Pos) -> tuple[Pos, Pos, Pos, Pos]: - """Return the tile xy coordinates on the top left, top right, bottom left, and bottom right sides of given xy coordinates.""" - cx, cy = xy - sides = [] - for raw_dy in range(2): - dy = raw_dy * 2 - 1 - ny = cy + dy - for raw_dx in range(2): - dx = raw_dx * 2 - 1 - nx = cx + dx - sides.append((nx, ny)) - tuple_sides = tuple(sides) - assert len(tuple_sides) == 4 - return cast(tuple[Pos, Pos, Pos, Pos], tuple_sides) - - -def pawn_modify(moves: tuple[T, ...], piece_type: u8) -> tuple[T, ...]: - """Return moves but remove invalid moves for pawns.""" - assert ( - len(moves) == 4 - ), "Tuple size MUST be four for this to return valid results!" - if ( - piece_type == 0 - ): # If it's a white pawn, it can only move to top left and top right - return moves[:2] - if ( - piece_type == 1 - ): # If it's a black pawn, it can only move to bottom left anf bottom right - return moves[2:] - return moves - - -@dataclass(slots=True) -class State: - """Represents state of checkers game.""" - - size: tuple[int, int] - pieces: dict[Pos, int] - turn: bool = True # Black moves first - - def __str__(self) -> str: - """Return text representation of game board state.""" - map_ = {None: " ", 0: "-", 1: "+", 2: "O", 3: "X"} - w, h = self.size - lines = [] - for y in range(h): - line = [] - for x in range(w): - if (x + y + 1) % 2: - # line.append("_") - line.append(" ") - continue - line.append(map_[self.pieces.get((x, y))]) - lines.append("".join(line)) - # lines.append(" | ".join(line)) - # lines.append("--+-"*(w-1)+"-") - return "\n".join(lines) - - def calculate_actions(self, position: Pos) -> ActionSet: - """Return actions the piece at given position can make.""" - if MANDATORY_CAPTURE: - exists = False - for start, _end in self.get_all_actions(self.pieces[position]): - if start == position: - exists = True - break - if not exists: - return ActionSet({}, (), set()) - jumps = self.get_jumps(position) - moves: tuple[Pos, ...] - moves = () if MANDATORY_CAPTURE and jumps else self.get_moves(position) - ends = set(jumps) - ends.update(moves) - return ActionSet(jumps, moves, ends) - - def piece_kinged(self, piece_pos: Pos, new_type: int) -> None: - """Piece kinged.""" - # print(f'piece_kinged {piece = }') - - def piece_moved(self, start_pos: Pos, end_pos: Pos) -> None: - """Piece moved from start_pos to end_pos.""" - - def piece_jumped(self, jumped_piece_pos: Pos) -> None: - """Piece has been jumped.""" - # print(f'piece_jumped {position = }') - - def preform_action(self, action: Action) -> Self: - """Return new state after performing action on self.""" - from_pos, to_pos = action - - pieces_copy = dict(self.pieces.items()) - - # Remove piece from it's start position - piece_type = pieces_copy.pop(from_pos) - - # See if it's a jump - if to_pos not in self.get_moves(from_pos): - # Jumps are more complex to calculate and we need - # to know what pieces got jumped over - cur_x, cur_y = from_pos - for jumped_pos in self.get_jumps(from_pos)[to_pos]: - from_pos = (cur_x, cur_y) - - # Remove jumped position from pieces in play - if jumped_pos in pieces_copy: - pieces_copy.pop(jumped_pos) - self.piece_jumped(jumped_pos) - # See if piece kinged - jumped_x, jumped_y = jumped_pos - # Rightshift 1 is more efficiant way to multiply by 2 - cur_x += (jumped_x - cur_x) << 1 - cur_y += (jumped_y - cur_y) << 1 - - self.piece_moved(from_pos, (cur_x, cur_y)) - - # Now that we know the current position, see if kinged - if self.does_piece_king(piece_type, (cur_x, cur_y)): - piece_type += 2 - self.piece_kinged((cur_x, cur_y), piece_type) - else: - self.piece_moved(from_pos, to_pos) - # See if it kings and king it if so - if self.does_piece_king(piece_type, to_pos): - piece_type += 2 - self.piece_kinged(to_pos, piece_type) +def bag_draw_tile(bag: Counter[int]) -> int: + """Return drawn tile from bag. Mutates bag.""" + tile = random.choice(tuple(bag.elements())) + bag[tile] -= 1 + return tile - # Move piece to it's end position - pieces_copy[to_pos] = piece_type - # Swap turn - return self.__class__( - self.size, - pieces_copy, - not self.turn, +def select_color(holder: Counter[int], color: int) -> int: + """Pop color tiles from bag. Returns count. Mutates holder. + + Raises KeyError if color not in holder. + """ + return holder.pop(color) + + +class PatternLine(NamedTuple): + """Player pattern line row.""" + + color: Tile + count_: int + + @classmethod + def blank(cls) -> Self: + """Return new blank pattern line.""" + return cls( + color=Tile.blank, + count_=0, + ) + + def place_tiles(self, color: Tile, place_count: int) -> Self: + """Return new pattern line after placing tiles of given color.""" + assert self.color == Tile.blank or self.color == color + assert place_count > 0 + return self._replace( + color=color, + count_=self.count_ + place_count, ) - def get_tile_name(self, x: int, y: int) -> str: - """Return name of a given tile.""" - return chr(65 + x) + str(self.size[1] - y) - @staticmethod - def action_from_points(start: Pos, end: Pos) -> Action: - """Return action from given start and end coordinates.""" - # return Action(self.get_tile_name(*start), self.get_tile_name(*end)) - return Action(start, end) - - def get_turn(self) -> int: - """Return whose turn it is. 0 = red, 1 = black.""" - return int(self.turn) - - def valid_location(self, position: Pos) -> bool: - """Return if position is valid.""" - x, y = position - w, h = self.size - return x >= 0 and y >= 0 and x < w and y < h - - def does_piece_king(self, piece_type: int, position: Pos) -> bool: - """Return if piece needs to be kinged given it's type and position.""" - _, y = position - _, h = self.size - return (piece_type == 0 and y == 0) or (piece_type == 1 and y == h - 1) +def remove_counter_zeros(counter: Counter[Any]) -> None: + """Remove any zero counts from given counter. Mutates counter.""" + for key, count in tuple(counter.items()): + if count == 0: + del counter[key] + + +def floor_fill_tile_excess( + floor: Counter[int], + tile: int, + count: int, +) -> Counter[int]: + """Fill floor with count of tile, return excess for box lid. Mutates floor.""" + excess: Counter[int] = Counter() + while floor.total() < FLOOR_LINE_COUNT and count > 0: + floor[tile] += 1 + count -= 1 + # If overflow and it's number one tile + if count and tile == Tile.one: + # Move non-one tiles from floor to excess + non_one = floor.total() - floor[Tile.one] + assert non_one > 0 + for _ in range(min(non_one, count)): + non_one_tiles = set(floor.elements()) - {Tile.one} + non_one_tile = sorted(non_one_tiles).pop() + # Move non-one tile from floor to box lid + floor[non_one_tile] -= 1 + excess[non_one_tile] += 1 + # Add one tile to floor + floor[tile] += 1 + count -= 1 + remove_counter_zeros(floor) + assert count >= 0 + if count: + # Add overflow tiles to box lid. + excess[tile] += count + + return excess + + +class PlayerData(NamedTuple): + """Player data.""" + + score: int + wall: NDArray[int8] + lines: tuple[PatternLine, ...] + floor: Counter[int] + + @classmethod + def new(cls, varient_play: bool = False) -> Self: + """Return new player data instance.""" + wall = array( + [Tile.blank for _ in range(5 * 5)], + int8, + ).reshape((5, 5)) + + if not varient_play: + for y in range(5): + for x in range(5): + color = -((5 - y + x) % len(REAL_TILES) + 1) + wall[y, x] = color + + return cls( + score=0, + wall=wall, + lines=(PatternLine.blank(),) * 5, + floor=Counter(), + ) + + def copy(self) -> Self: + """Return copy of self.""" + return self._replace( + floor=self.floor.copy(), + ) + + def line_id_valid(self, line_id: int) -> bool: + """Return if given line id is valid.""" + return line_id >= 0 and line_id < len(self.lines) @staticmethod - def get_enemy(self_type: int) -> int: - """Return enemy pawn piece type.""" - # If we are kinged, get a pawn version of ourselves. - # Take that plus one mod 2 to get the pawn of the enemy - return (self_type + 1) % 2 + def get_line_max_count(line_id: int) -> int: + """Return max count allowed in given line.""" + # Line id is keeping track of max count + return line_id + 1 + + def get_line_max_placable_count(self, line_id: int) -> int: + """Return max placable count for given line.""" + assert self.line_id_valid(line_id) + max_count = self.get_line_max_count(line_id) + return max_count - self.lines[line_id].count_ + + def get_row_colors_used(self, line_id: int) -> set[Tile]: + """Return set of tile colors used in wall for given row.""" + row = self.wall[line_id, :] + return {Tile(int(x)) for x in row[row >= 0]} + + def get_row_unused_colors(self, line_id: int) -> set[Tile]: + """Return set of tiles colors not currently used in wall for given row.""" + return REAL_TILES - self.get_row_colors_used(line_id) + + def yield_possible_placement_rows( + self, + color: int, + ) -> Generator[tuple[int, int], None, None]: + """Yield row line ids and number of placable for rows able to place color at.""" + for line_id, line in enumerate(self.lines): + # Color must match + if line.color != Tile.blank and int(line.color) != color: + # print("color mismatch") + continue + placable = self.get_line_max_placable_count(line_id) + # Must have placable spots + if not placable: + continue + # Must not already use color + if color in self.get_row_colors_used(line_id): + continue + yield (line_id, placable) + + def can_select_line( + self, + line_id: int, + color: int, + place_count: int, + ) -> bool: + """Return if can select given line with given color and place count.""" + if not self.line_id_valid(line_id): + # print("invalid line id") + return False + line = self.lines[line_id] + # Don't allow placing zero + if place_count <= 0: + # print("place count too smol") + return False + # Color must match + if line.color != Tile.blank and int(line.color) != color: + # print("color mismatch") + return False + # Must have space to place + if place_count > self.get_line_max_placable_count(line_id): + return False + # Can't place in row that uses that color already + return Tile(color) not in self.get_row_colors_used(line_id) @staticmethod - def get_piece_types(self_type: int) -> tuple[int, int]: - """Return piece types of given piece type.""" - # If we are kinged, get a pawn version of ourselves. - self_pawn = self_type % 2 - return (self_pawn, self_pawn + 2) + def replace_pattern_line( + lines: tuple[PatternLine, ...], + line_id: int, + new: PatternLine, + ) -> tuple[PatternLine, ...]: + """Return new pattern line data after replacing one of them.""" + left = lines[:line_id] + right = lines[line_id + 1 :] + return (*left, new, *right) + + def place_pattern_line_tiles( + self, + line_id: int, + color: int, + place_count: int, + ) -> Self: + """Return new player data after placing tiles in a pattern line.""" + assert self.can_select_line(line_id, color, place_count) + line = self.lines[line_id] + return self._replace( + lines=self.replace_pattern_line( + self.lines, + line_id, + line.place_tiles(Tile(color), place_count), + ), + ) + + def is_floor_line_full(self) -> bool: + """Return if floor line is full.""" + return self.floor.total() >= FLOOR_LINE_COUNT - def get_jumps( + def place_floor_line_tiles( self, - position: Pos, - piece_type: int | None = None, - _pieces: dict[Pos, int] | None = None, - _recursion: int = 0, - ) -> dict[Pos, list[Pos]]: - """Return valid jumps a piece can make. - - position is a xy coordinate tuple pointing to a board position - that may or may not have a piece on it. - piece_type is the piece type at position. If not - given, position must point to a tile with a piece on it - - Returns dictionary that maps end positions to - jumped pieces to get there - """ - if piece_type is None: - piece_type = self.pieces[position] - if _pieces is None: - _pieces = self.pieces - _pieces = copy.deepcopy(_pieces) - - enemy_pieces = self.get_piece_types(self.get_enemy(piece_type)) - - # Get the side coordinates of the tile and make them tuples so - # the scan later works properly. - sides = get_sides(position) - # Make a dictionary to find what direction a tile is in if you - # give it the tile. - # end position : jumped pieces - - # Make a dictionary for the valid jumps and the pieces they jump - valid: dict[Pos, list[Pos]] = {} - - valid_sides: tuple[tuple[int, Pos], ...] - if PAWN_JUMP_FORWARD_ONLY: - valid_sides = pawn_modify( - tuple(enumerate(sides)), - piece_type, - ) - else: - valid_sides = tuple(enumerate(sides)) + color: int, + place_count: int, + ) -> tuple[Self, Counter[int]]: + """Return new player and excess tiles for box lid.""" + floor = self.floor.copy() + for_box_lid = floor_fill_tile_excess(floor, color, place_count) + assert all(x > 0 for x in for_box_lid.values()), for_box_lid + return ( + self._replace(floor=floor), + for_box_lid, + ) - # For each side tile in the jumpable tiles for this type of piece, - for direction, side in valid_sides: - # Make sure side exists - if not self.valid_location(side): - continue - side_piece = _pieces.get(side) - # Side piece must be one of our enemy's pieces - if side_piece not in enemy_pieces: - continue - # Get the direction from the dictionary we made earlier - # Get the coordinates of the tile on the side of the main tile's - # side in the same direction as the main tile's side - side_side = get_sides(side)[direction] - # Make sure side exists - if not self.valid_location(side_side): + def get_horizontal_linked_wall_count( + self, + x: int, + y: int, + wall: NDArray[int8], + ) -> int: + """Return horizontally-linked tile count.""" + count = 0 + for range_ in (range(x - 1, -1, -1), range(x + 1, 5)): + for cx in range_: + if wall[y, cx] < 0: + break + count += 1 + return count + + def get_vertically_linked_wall_count( + self, + x: int, + y: int, + wall: NDArray[int8], + ) -> int: + """Return vertically-linked tile count.""" + count = 0 + for range_ in (range(y - 1, -1, -1), range(y + 1, 5)): + for cy in range_: + if wall[cy, x] < 0: + break + count += 1 + return count + + def get_score_from_wall_placement( + self, + color: int, + x: int, + y: int, + wall: NDArray[int8], + ) -> int: + """Return score increment value from placing tile at given coordinates.""" + # Should be blank or fake at position + assert wall[y, x] < 0 + count = 1 + count += self.get_horizontal_linked_wall_count(x, y, wall) + count += self.get_vertically_linked_wall_count(x, y, wall) + return count + + def perform_floor_line_scoring(self) -> int: + """Return score increment value from floor line.""" + total_count = self.floor.total() + assert total_count <= FLOOR_LINE_COUNT + score = 0 + for idx in range(total_count): + score += FLOOR_LINE_DATA[idx] + return score + + def perform_auto_wall_tiling(self) -> tuple[Self, Counter[int], bool]: + """Return new player data and tiles for box lid after performing automatic wall tiling.""" + for_box_lid: Counter[int] = Counter() + + score = self.score + new_lines = self.lines + new_wall = self.wall.copy() + for line_id, line in enumerate(self.lines): + if line.count_ != self.get_line_max_count(line_id): continue - side_side_piece = _pieces.get(side_side) - # If the side is open, - if side_side_piece is None: - # Add it the valid jumps dictionary and add the tile - # to the list of end tiles. - valid[side_side] = [side] - - # Remove jumped piece from future calculations - _pieces.pop(side) - - # For each end point tile in the list of end point tiles, - for end_tile in tuple(valid): - # Get the dictionary from the jumps you could make - # from that end tile - w, h = self.size - if _recursion + 1 > math.ceil((w**2 + h**2) ** 0.25): - break - # If the piece has made it to the opposite side, - piece_type_copy = piece_type - if self.does_piece_king(piece_type_copy, end_tile): - # King that piece - piece_type_copy += 2 - _recursion = -1 - add_valid = self.get_jumps( - end_tile, - piece_type_copy, - _pieces=_pieces, - _recursion=_recursion + 1, + right = max(0, line.count_ - 1) + if right: + for_box_lid[line.color] += right + x = tuple(map(int, new_wall[line_id, :])).index(-line.color - 1) + score += self.get_score_from_wall_placement( + line.color, + x, + line_id, + new_wall, ) - # For each key in the new dictionary of valid tile's keys, - for end_pos, jumped_pieces in add_valid.items(): - # If the key is not already existent in the list of - # valid destinations, - if end_pos not in valid: - # Add that destination to the dictionary and every - # tile you have to jump to get there. - no_duplicates = [ - p for p in jumped_pieces if p not in valid[end_tile] - ] - valid[end_pos] = valid[end_tile] + no_duplicates - - return valid - - def get_moves(self, position: Pos) -> tuple[Pos, ...]: - """Return valid moves piece at position can make, not including jumps.""" - piece_type = self.pieces[position] - # Get the side xy choords of the tile's xy pos, - # then modify results for pawns - moves = pawn_modify(get_sides(position), piece_type) - return tuple( - m - for m in filter(self.valid_location, moves) - if m not in self.pieces + new_wall[line_id, x] = line.color + new_lines = self.replace_pattern_line( + new_lines, + line_id, + PatternLine.blank(), + ) + + score += self.perform_floor_line_scoring() + if score < 0: + score = 0 + + # Get one tile from floor line + floor = self.floor.copy() + has_one = False + if floor[Tile.one]: + floor[Tile.one] -= 1 + remove_counter_zeros(floor) + has_one = True + for_box_lid.update(floor) + + return ( + self._replace( + lines=new_lines, + wall=new_wall, + score=score, + floor=Counter(), + ), + for_box_lid, + has_one, ) + def has_horizontal_wall_line(self) -> bool: + """Return if full horizontal line is filled anywhere.""" + return any(all(self.wall[y, :] >= 0) for y in range(5)) + + def get_filled_horizontal_line_count(self) -> int: + """Return number of filled horizontal lines.""" + count = 0 + for y in range(5): + if all(self.wall[y, :] >= 0): + count += 1 + return count + + def get_end_of_game_score(self) -> int: + """Return end of game score for this player.""" + score = self.score + score += self.get_filled_horizontal_line_count() * 2 + for x in range(5): + if all(self.wall[:, x] >= 0): + score += 7 + counts = Counter(int(x) for x in self.wall[self.wall >= 0]) + for count in counts.values(): + if count == 5: + score += 10 + return score + + def perform_end_of_game_scoring(self) -> Self: + """Return new player data after performing end of game scoring.""" + return self._replace(score=self.get_end_of_game_score()) + + +def factory_displays_deepcopy( + factory_displays: dict[int, Counter[int]], +) -> dict[int, Counter[int]]: + """Return deepcopy of factory displays.""" + return {k: v.copy() for k, v in factory_displays.items()} + + +def player_data_deepcopy( + player_data: dict[int, PlayerData], +) -> dict[int, PlayerData]: + """Return deepcopy of player data.""" + return {k: v.copy() for k, v in player_data.items()} + + +class SelectableSource(IntEnum): + """Selectable tile source.""" + + table_center = 0 + factory = auto() + + +class SelectableSourceTiles(NamedTuple): + """Selectable source tiles data.""" + + source: SelectableSource + tiles: Tile + # Factory ids + source_id: int | None = None + + +class SelectableDestination(IntEnum): + """Selectable tile destination.""" + + floor_line = 0 + pattern_line = auto() + + +class SelectableDestinationTiles(NamedTuple): + """Selectable destination tiles data.""" + + destination: SelectableDestination + place_count: int + # Pattern line ids + destination_id: int | None = None + + +class State(NamedTuple): + """Represents state of an azul game.""" + + varient_play: bool + current_phase: Phase + bag: Counter[int] + box_lid: Counter[int] + table_center: Counter[int] + factory_displays: dict[int, Counter[int]] + cursor_contents: Counter[int] + current_turn: int + player_data: dict[int, PlayerData] + @classmethod - def wrap_actions( - cls, - position: Pos, - calculate_ends: Callable[[Pos], Iterable[Pos]], - ) -> Generator[Action, None, None]: - """Yield end calculation function results as Actions.""" - for end in calculate_ends(position): - yield cls.action_from_points(position, end) - - def get_actions(self, position: Pos) -> Generator[Action, None, None]: - """Yield all moves and jumps the piece at position can make.""" - ends = set(self.get_jumps(position)) - if not (ends and MANDATORY_CAPTURE): - ends.update(self.get_moves(position)) - for end in ends: - yield self.action_from_points(position, end) - - def get_all_actions(self, player: int) -> Generator[Action, None, None]: - """Yield all actions for given player.""" - player_pieces = {player, player + 2} - if not MANDATORY_CAPTURE: - for position, piece_type in self.pieces.items(): - if piece_type not in player_pieces: - continue - yield from self.get_actions(position) - return - jumps_available = False - for position, piece_type in self.pieces.items(): - if piece_type not in player_pieces: + def new_game(cls, player_count: int, varient_play: bool = False) -> Self: + """Return state of a new game.""" + factory_count = player_count * 2 + 1 + bag = generate_bag_contents() + + factory_displays: dict[int, Counter[int]] = {} + for x in range(factory_count): + tiles: Counter[int] = Counter() + for _ in range(4): + tiles[bag_draw_tile(bag)] += 1 + factory_displays[x] = tiles + + return cls( + varient_play=varient_play, + current_phase=Phase.factory_offer, + bag=bag, + box_lid=Counter(), + table_center=Counter({Tile.one, 1}), + factory_displays=factory_displays, + cursor_contents=Counter(), + current_turn=0, + player_data={ + x: PlayerData.new(varient_play) for x in range(player_count) + }, + ) + + def is_cursor_empty(self) -> bool: + """Return if cursor is empty.""" + return self.cursor_contents.total() == 0 + + def can_cursor_select_factory(self, factory_id: int) -> bool: + """Return if cursor can select a specific factory.""" + assert self.current_phase == Phase.factory_offer + if not self.is_cursor_empty(): + return False + factory = self.factory_displays.get(factory_id, None) + if factory is None: + return False + return factory.total() > 0 + + def can_cursor_select_factory_color( + self, + factory_id: int, + color: int, + ) -> bool: + """Return if cursor can select color at factory.""" + if not self.can_cursor_select_factory(factory_id): + return False + factory = self.factory_displays[factory_id] + return factory[color] > 0 + + def cursor_selects_factory(self, factory_id: int, color: int) -> Self: + """Return new state after cursor selects factory.""" + assert self.can_cursor_select_factory_color(factory_id, color) + # Only mutate copies + factory_displays = factory_displays_deepcopy(self.factory_displays) + table_center = self.table_center.copy() + cursor_contents = self.cursor_contents.copy() + + factory = factory_displays[factory_id] + count = select_color(factory, color) + # Add to cursor + cursor_contents[color] += count + # Add all non-matching colored tiles to center of table + table_center.update(factory) + factory.clear() + + return self._replace( + table_center=table_center, + factory_displays=factory_displays, + cursor_contents=cursor_contents, + ) + + def can_cursor_select_center(self, color: int) -> bool: + """Return if cursor can select color from table center.""" + assert self.current_phase == Phase.factory_offer + if not self.is_cursor_empty(): + return False + return color != Tile.one and self.table_center[color] > 0 + + def cursor_selects_table_center(self, color: int) -> Self: + """Return new state after cursor selects from table center.""" + assert self.can_cursor_select_center(color) + table_center = self.table_center.copy() + cursor_contents = self.cursor_contents.copy() + + # Get all of color from table center and add to cursor + cursor_contents[color] += select_color(table_center, color) + # Handle number one tile + if table_center[Tile.one]: + cursor_contents[Tile.one] += select_color(table_center, Tile.one) + remove_counter_zeros(table_center) + + return self._replace( + table_center=table_center, + cursor_contents=cursor_contents, + ) + + def yield_table_center_selections( + self, + ) -> Generator[SelectableSourceTiles, None, None]: + """Yield SelectableSourceTiles objects from table center.""" + for color, count in self.table_center.items(): + if color == Tile.one or count <= 0: continue - if not jumps_available: - for jump in self.wrap_actions(position, self.get_jumps): - yield jump - jumps_available = True - else: - yield from self.wrap_actions(position, self.get_jumps) - if not jumps_available: - for position, piece_type in self.pieces.items(): - if piece_type not in player_pieces: - continue - yield from self.wrap_actions(position, self.get_moves) - - def check_for_win(self) -> int | None: - """Return player number if they won else None.""" - # For each of the two players, - for player in range(2): - # For each tile in the playable tiles, - has_move = False - for _ in self.get_all_actions(player): - has_move = True - # Player has at least one move, no need to continue - break - if not has_move and self.turn == bool(player): - # Continued without break, so player either has no moves - # or no possible moves, so their opponent wins - return (player + 1) % 2 - return None - - def can_player_select_piece(self, player: int, tile_pos: Pos) -> bool: - """Return True if player can select piece on given tile position.""" - piece_at_pos = self.pieces.get(tile_pos) - if piece_at_pos is None: + yield SelectableSourceTiles( + source=SelectableSource.table_center, + tiles=Tile(color), + ) + + def yield_selectable_tiles_factory_offer( + self, + ) -> Generator[SelectableSourceTiles, None, None]: + """Yield SelectableSourceTiles objects from all sources.""" + yield from self.yield_table_center_selections() + for factory_id, factory_display in self.factory_displays.items(): + for color in factory_display: + yield SelectableSourceTiles( + source=SelectableSource.factory, + tiles=Tile(color), + source_id=factory_id, + ) + + def apply_source_select_action_factory_offer( + self, + selection: SelectableSourceTiles, + ) -> Self: + """Return new state after applying selection action.""" + color = selection.tiles + if selection.source == SelectableSource.table_center: + return self.cursor_selects_table_center(color) + if selection.source == SelectableSource.factory: + assert selection.source_id is not None + return self.cursor_selects_factory(selection.source_id, color) + raise NotImplementedError(selection.source) + + def get_cursor_holding_color(self) -> int: + """Return color of tile cursor is holding.""" + cursor_colors = set(self.cursor_contents.elements()) + # Do not count number one tile + cursor_colors.discard(Tile.one) + assert len(cursor_colors) == 1, "Cursor should only exactly one color" + return cursor_colors.pop() + + def can_player_select_line( + self, + line_id: int, + color: int, + place_count: int, + ) -> bool: + """Return if player can select line.""" + player_data = self.player_data[self.current_turn] + + # Cannot place more than we have + # Can't be pulling tiles out of thin air now can we? + if place_count > self.cursor_contents[color]: + return False + + return player_data.can_select_line(line_id, color, place_count) + + def get_player_line_max_placable_count(self, line_id: int) -> int: + """Return max placable count for given line.""" + player_data = self.player_data[self.current_turn] + + return player_data.get_line_max_placable_count(line_id) + + def all_pullable_empty(self) -> bool: + """Return if all pullable tile locations are empty, not counting cursor.""" + if self.table_center.total(): return False - return (piece_at_pos % 2) == player - - def get_pieces(self) -> tuple[tuple[Pos, int], ...]: - """Return all pieces.""" - return tuple((pos, type_) for pos, type_ in self.pieces.items()) - - -def generate_pieces( - board_width: int, - board_height: int, - colors: int = 2, -) -> dict[Pos, int]: - """Generate data about each piece.""" - pieces: dict[Pos, int] = {} - # Get where pieces should be placed - z_to_1 = round(board_height / 3) # White - z_to_2 = (board_height - (z_to_1 * 2)) + z_to_1 # Black - # For each xy position in the area of where tiles should be, - for y in range(board_height): - # Reset the x pos to 0 - for x in range(board_width): - # Get the color of that spot by adding x and y mod the number of different colors - color = (x + y + 1) % colors - # If a piece should be placed on that tile and the tile is not Red, - if (not color) and ((y <= z_to_1 - 1) or (y >= z_to_2)): - # Set the piece to White Pawn or Black Pawn depending on the current y pos - piece_type = int(y <= z_to_1) - pieces[x, y] = piece_type - return pieces + for factory_display in self.factory_displays.values(): + if factory_display.total(): + return False + return True + + def _factory_offer_maybe_next_turn(self) -> Self: + """Return either current state or new state if player's turn is over.""" + assert self.current_phase == Phase.factory_offer + # If cursor is still holding things, turn is not over. + if not self.is_cursor_empty(): + return self + # Turn is over + # Increment who's turn it is + current_turn = (self.current_turn + 1) % len(self.player_data) + + current_phase: Phase = self.current_phase + if self.all_pullable_empty(): + # Go to wall tiling phase + current_phase = Phase.wall_tiling + + new_state = self._replace( + current_phase=current_phase, + current_turn=current_turn, + ) + if current_phase == Phase.wall_tiling: + if self.varient_play: + raise NotImplementedError() + return new_state.apply_auto_wall_tiling() + return new_state + + def player_select_floor_line(self, color: int, place_count: int) -> Self: + """Return new state after player adds tiles to floor line.""" + assert self.current_phase == Phase.factory_offer + cursor_contents = self.cursor_contents.copy() + assert place_count > 0 + assert place_count <= cursor_contents[color] + + box_lid = self.box_lid.copy() + current_player_data = self.player_data[self.current_turn] + + # Remove from cursor + cursor_contents[color] -= place_count + # Add to floor line + new_player_data, for_box_lid = ( + current_player_data.place_floor_line_tiles( + color, + place_count, + ) + ) + # Add overflow tiles to box lid + assert all(x > 0 for x in for_box_lid.values()), for_box_lid + box_lid.update(for_box_lid) + + # If has number one tile, add to floor line + if cursor_contents[Tile.one]: + # Add to floor line + new_player_data, for_box_lid = ( + new_player_data.place_floor_line_tiles( + Tile.one, + cursor_contents.pop(Tile.one), + ) + ) + # Add overflow tiles to box lid + assert all(x > 0 for x in for_box_lid.values()), for_box_lid + box_lid.update(for_box_lid) + + remove_counter_zeros(cursor_contents) + + # Update player data + player_data = player_data_deepcopy(self.player_data) + player_data[self.current_turn] = new_player_data + + return self._replace( + box_lid=box_lid, + cursor_contents=cursor_contents, + player_data=player_data, + )._factory_offer_maybe_next_turn() + + def player_selects_pattern_line( + self, + line_id: int, + place_count: int, + ) -> Self: + """Return new state after player selects line.""" + assert self.current_phase == Phase.factory_offer + assert not self.is_cursor_empty() + color = self.get_cursor_holding_color() + + assert self.can_player_select_line(line_id, color, place_count) + current_player_data = self.player_data[self.current_turn] + + new_player_data = current_player_data.place_pattern_line_tiles( + line_id, + color, + place_count, + ) + + cursor_contents = self.cursor_contents.copy() + cursor_contents[color] -= place_count + + # Might need to change box lid + box_lid = self.box_lid + + # If has number one tile, add to floor line + if cursor_contents[Tile.one]: + # Will be mutating box lid then + box_lid = self.box_lid.copy() + # Add to floor line + new_player_data, for_box_lid = ( + new_player_data.place_floor_line_tiles( + Tile.one, + cursor_contents.pop(Tile.one), + ) + ) + # Add overflow tiles to box lid + assert all(x > 0 for x in for_box_lid.values()), for_box_lid + box_lid.update(for_box_lid) + + remove_counter_zeros(cursor_contents) + + player_data = player_data_deepcopy(self.player_data) + player_data[self.current_turn] = new_player_data + + return self._replace( + box_lid=box_lid, + player_data=player_data, + cursor_contents=cursor_contents, + )._factory_offer_maybe_next_turn() + + def yield_selectable_tile_destinations_factory_offer( + self, + ) -> Generator[SelectableDestinationTiles, None, None]: + """Yield selectable tile destinations for factory offer phase.""" + assert self.current_phase == Phase.factory_offer + assert not self.is_cursor_empty() + + current_player_data = self.player_data[self.current_turn] + + color = self.get_cursor_holding_color() + count = self.cursor_contents[color] + 1 + + for ( + line_id, + placable, + ) in current_player_data.yield_possible_placement_rows(color): + for place_count in range(1, min(count, placable + 1)): + yield SelectableDestinationTiles( + destination=SelectableDestination.pattern_line, + place_count=place_count, + destination_id=line_id, + ) + # Can always place in floor line, even if full, + # because of box lid overflow + for place_count in range(1, count): + yield SelectableDestinationTiles( + destination=SelectableDestination.floor_line, + place_count=place_count, + ) + + def apply_destination_select_action_factory_offer( + self, + selection: SelectableDestinationTiles, + ) -> Self: + """Return new state after applying destination selection action.""" + assert self.current_phase == Phase.factory_offer + assert not self.is_cursor_empty() + + if selection.destination == SelectableDestination.floor_line: + color = self.get_cursor_holding_color() + return self.player_select_floor_line( + color, + selection.place_count, + ) + if selection.destination == SelectableDestination.pattern_line: + assert selection.destination_id is not None + return self.player_selects_pattern_line( + selection.destination_id, + selection.place_count, + ) + raise NotImplementedError(selection.destination) + + def apply_auto_wall_tiling(self) -> Self: + """Return new state after performing automatic wall tiling.""" + assert self.current_phase == Phase.wall_tiling + assert not self.varient_play + box_lid = self.box_lid.copy() + new_players = player_data_deepcopy(self.player_data) + + is_end = False + current_turn = self.current_turn + for player_id, player in self.player_data.items(): + new_player, for_box_lid, has_one = ( + player.perform_auto_wall_tiling() + ) + new_players[player_id] = new_player + box_lid.update(for_box_lid) + if not is_end: + is_end = new_player.has_horizontal_wall_line() + if has_one: + current_turn = player_id + + bag = self.bag.copy() + factory_displays: dict[int, Counter[int]] = {} + + if is_end: + for player_id in self.player_data: + new_players[player_id] = new_players[ + player_id + ].perform_end_of_game_scoring() + else: + out_of_tiles = False + for factory_id in self.factory_displays: + tiles: Counter[int] = Counter() + if out_of_tiles: + factory_displays[factory_id] = tiles + continue + for _ in range(4): + if bag.total() > 0: + tiles[bag_draw_tile(bag)] += 1 + else: + bag = box_lid + box_lid = Counter() + if bag.total() <= 0: + # "In the rare case that you run out of + # tiles again while there are one left in + # the lid, start the new round as usual even + # though not all Factory displays are + # properly filled." + out_of_tiles = True + break + factory_displays[factory_id] = tiles + + return self._replace( + current_phase=Phase.end if is_end else Phase.factory_offer, + current_turn=current_turn, + player_data=new_players, + bag=bag, + box_lid=box_lid, + factory_displays=factory_displays, + table_center=Counter({Tile.one: 1}), + ) + + def get_win_order(self) -> list[tuple[int, int]]: + """Return player ranking with (id, compare_score) entries.""" + counts: dict[int, int] = {} + # get_filled_horizontal_line_count can return at most 5 + for player_id, player in self.player_data.items(): + counts[player_id] = ( + player.score * 6 + player.get_filled_horizontal_line_count() + ) + return sorted(counts.items(), key=lambda x: x[1], reverse=True) + + def yield_all_factory_offer_destinations( + self, + ) -> Generator[tuple[SelectableDestinationTiles, ...]]: + """Yield all factory offer destinations.""" + if self.is_cursor_empty(): + yield () + else: + for ( + destination + ) in self.yield_selectable_tile_destinations_factory_offer(): + new = self.apply_destination_select_action_factory_offer( + destination, + ) + did_not_iter = True + for action in new.yield_all_factory_offer_destinations(): + did_not_iter = False + yield (destination, *action) + if did_not_iter: + yield (destination,) + + def yield_actions( + self, + ) -> Generator[ + tuple[SelectableDestinationTiles, ...] + | tuple[SelectableSourceTiles, tuple[SelectableDestinationTiles, ...]], + None, + None, + ]: + """Yield all possible actions able to be performed on this state.""" + if self.current_phase == Phase.factory_offer: + if not self.is_cursor_empty(): + yield from self.yield_all_factory_offer_destinations() + else: + for selection in self.yield_selectable_tiles_factory_offer(): + new = self.apply_source_select_action_factory_offer( + selection, + ) + for ( + action_chain + ) in new.yield_all_factory_offer_destinations(): + yield (selection, action_chain) + else: + raise NotImplementedError() + + def preform_action( + self, + action: ( + tuple[SelectableDestinationTiles, ...] + | tuple[ + SelectableSourceTiles, + tuple[SelectableDestinationTiles, ...], + ] + ), + ) -> Self: + """Return new state after applying an action.""" + if self.current_phase == Phase.factory_offer: + if isinstance(action[0], SelectableDestinationTiles): + new = self + for destination in action: + assert isinstance(destination, SelectableDestinationTiles) + new = new.apply_destination_select_action_factory_offer( + destination, + ) + return new + selection, destinations = action + assert isinstance(selection, SelectableSourceTiles) + new = self.apply_source_select_action_factory_offer( + selection, + ) + for destination_ in destinations: + assert isinstance(destination_, SelectableDestinationTiles) + new = new.apply_destination_select_action_factory_offer( + destination_, + ) + return new + raise NotImplementedError() + + +def run() -> None: + """Run program.""" + from market_api import pretty_print_response as pprint + + random.seed(0) + state = State.new_game(2) + ticks = 0 + try: + ## last_turn = -1 + while state.current_phase == Phase.factory_offer: + ## assert last_turn != state.current_turn + ## last_turn = state.current_turn + actions = tuple(state.yield_actions()) + print(f"{len(actions) = }") + action = random.choice(actions) + ## pprint(action) + state = state.preform_action(action) + + ticks += 1 + print(f"{state.get_win_order() = }") + except BaseException: + print(f"{ticks = }") + ## print(f'{state = }') + pprint(state) + raise + ## print(f'{destination = }') + ## pprint(state) + pprint(state) + + +if __name__ == "__main__": + run() diff --git a/src/azul/tools.py b/src/azul/tools.py index 5c8629f..ccdc9e4 100644 --- a/src/azul/tools.py +++ b/src/azul/tools.py @@ -3,14 +3,7 @@ from __future__ import annotations # Programmed by CoolCat467 -import math -import random -from typing import TYPE_CHECKING, TypeVar - -if TYPE_CHECKING: - from collections.abc import Generator, Iterable - - from azul.game import Tile +from typing import TypeVar T = TypeVar("T") Numeric = TypeVar("Numeric", int, float) @@ -37,32 +30,6 @@ def saturate(value: Numeric, low: Numeric, high: Numeric) -> Numeric: return min(max(value, low), high) -def randomize(iterable: Iterable[T]) -> list[T]: - """Randomize all values of an iterable.""" - lst = list(iterable) - random.shuffle(lst) - return lst - - -def gen_random_proper_seq(length: int, **kwargs: float) -> list[str]: - """Generate a random sequence of letters given keyword arguments of =.""" - letters = [] - if sum(list(kwargs.values())) != 1: - raise ArithmeticError( - "Sum of percentages of " - + " ".join(list(kwargs.keys())) - + " are not equal to 100 percent!", - ) - for letter in kwargs: - letters += [letter] * math.ceil(length * kwargs[letter]) - return randomize(letters) - - -def sort_tiles(tile_object: Tile) -> int: - """Key function for sorting tiles.""" - return tile_object.color - - # def getCacheSignatureTile(tile, tilesize, greyshift, outlineSize): # """Return the string a tile and it's configuration information would use to identify itself in the tile cache.""" # safeFloat = lambda x: round(x*100) @@ -70,12 +37,3 @@ def sort_tiles(tile_object: Tile) -> int: # data = tile.color, safeFloat(tilesize), safeFloat(greyshift), safeFloat(outlineSize) # # types: ^ # return ''.join((str(i) for i in data)) - - -def floor_line_subtract_generator(seed: int = 1) -> Generator[int, None, None]: - """Floor Line subtraction number generator. Can continue indefinitely.""" - num = seed - while True: - nxt = [-num] * (num + 1) - yield from nxt - num += 1 From 6f3f75b3502268abce40ae9e699cb1c95807f749 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Mon, 25 Nov 2024 01:13:38 -0600 Subject: [PATCH 03/58] Do some small things with state Update minimax module Remove some checkers code from server --- .pre-commit-config.yaml | 2 +- computer_players/MiniMax_AI.py | 21 ++-- computer_players/machine_client.py | 2 +- computer_players/minimax.py | 143 +------------------------ src/azul/game.py | 3 +- src/azul/server.py | 165 ++--------------------------- src/azul/state.py | 20 ++-- 7 files changed, 43 insertions(+), 313 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5da692c..f030e52 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.7.4 + rev: v0.8.0 hooks: - id: ruff types: [file] diff --git a/computer_players/MiniMax_AI.py b/computer_players/MiniMax_AI.py index 6bf8909..bbc0865 100755 --- a/computer_players/MiniMax_AI.py +++ b/computer_players/MiniMax_AI.py @@ -14,7 +14,7 @@ from typing import TYPE_CHECKING, TypeAlias, TypeVar ##from machine_client import RemoteState, run_clients_in_local_servers_sync -from minimax import Minimax, Player +from minimax import Minimax, MinimaxResult, Player from azul.state import ( Phase, @@ -50,13 +50,14 @@ def value(state: State) -> int | float: if AzulMinimax.terminal(state): winner, _score = real_state.get_win_order()[0] if winner == max_player: - return 1 - return -1 + return 10 + return -10 # Heuristic min_ = 0 max_ = 0 for player_id, player_data in real_state.player_data.items(): score = player_data.get_end_of_game_score() + score += player_data.get_floor_line_scoring() if player_id == max_player: max_ += score else: @@ -64,7 +65,7 @@ def value(state: State) -> int | float: # More max will make score higher, # more min will make score lower # Plus one in divisor makes so never / 0 - return (max_ - min_) / (max_ + min_ + 1) + return (max_ - min_) / (abs(max_) + abs(min_) + 1) @staticmethod def terminal(state: State) -> bool: @@ -93,6 +94,13 @@ def result(state: State, action: Action) -> State: real_state, max_player = state return (real_state.preform_action(action), max_player) + @classmethod + def adaptive_depth_minimax(cls, state: State) -> MinimaxResult[Action]: + """Adaptive depth minimax.""" + # TODO + depth = 1 + return cls.alphabeta(state, depth) + ##class MinimaxPlayer(RemoteState): ## """Minimax Player.""" @@ -122,9 +130,10 @@ def run() -> None: state = (State.new_game(2), 0) while not AzulMinimax.terminal(state): - action = AzulMinimax.alphabeta(state, 2) - print(f"{action.value = }") + action = AzulMinimax.adaptive_depth_minimax(state) + print(f"{action = }") state = AzulMinimax.result(state, action.action) + print(f"{state = }") print(state) diff --git a/computer_players/machine_client.py b/computer_players/machine_client.py index dac52f7..78254ce 100644 --- a/computer_players/machine_client.py +++ b/computer_players/machine_client.py @@ -39,7 +39,7 @@ class RemoteState(Component, metaclass=ABCMeta): turn. """ - __slots__ = ("state", "pieces", "has_initial", "playing_as", "moves") + __slots__ = ("has_initial", "moves", "pieces", "playing_as", "state") def __init__(self) -> None: """Initialize remote state.""" diff --git a/computer_players/minimax.py b/computer_players/minimax.py index c3a325f..0a4600a 100644 --- a/computer_players/minimax.py +++ b/computer_players/minimax.py @@ -44,149 +44,16 @@ class Minimax(ABC, Generic[State, Action]): __slots__ = () - @classmethod - @abstractmethod - def value(cls, state: State) -> int | float: - """Return the value of a given game state.""" - - @classmethod - @abstractmethod - def terminal(cls, state: State) -> bool: - """Return if given game state is terminal.""" - - @classmethod - @abstractmethod - def player(cls, state: State) -> Player: - """Return player status given the state of the game. - - Must return either Player.MIN or Player.MAX - """ - - @classmethod - @abstractmethod - def actions(cls, state: State) -> Iterable[Action]: - """Return a collection of all possible actions in a given game state.""" - - @classmethod - @abstractmethod - def result(cls, state: State, action: Action) -> State: - """Return new game state after performing action on given state.""" - - @classmethod - def minimax( - cls, - state: State, - depth: int | None = 5, - ) -> MinimaxResult[Action]: - """Return minimax result best action for a given state for the current player.""" - if cls.terminal(state): - return MinimaxResult(cls.value(state), None) - if depth is not None and depth <= 0: - # Choose a random action - # No need for cryptographic secure random - return MinimaxResult( - cls.value(state), - random.choice(tuple(cls.actions(state))), # noqa: S311 - ) - next_down = None if depth is None else depth - 1 - - current_player = cls.player(state) - value: int | float - if current_player == Player.MAX: - value = -infinity - best = max - elif current_player == Player.MIN: - value = infinity - best = min - elif current_player == Player.CHANCE: - raise ValueError("CHANCE is not valid for regular minimax.") - else: - raise ValueError(f"Unexpected player type {current_player!r}") - - best_action: Action | None = None - for action in cls.actions(state): - result = cls.minimax(cls.result(state, action), next_down) - new_value = best(value, result.value) - if new_value != value: - best_action = action - value = new_value - return MinimaxResult(value, best_action) - - @classmethod - def alphabeta( - cls, - state: State, - depth: int | None = 5, - a: int | float = -infinity, - b: int | float = infinity, - ) -> MinimaxResult[Action]: - """Return minimax alphabeta pruning result best action for given current state.""" - # print(f'alphabeta {depth = } {a = } {b = }') - - if cls.terminal(state): - return MinimaxResult(cls.value(state), None) - if depth is not None and depth <= 0: - # Choose a random action - # No need for cryptographic secure random - return MinimaxResult( - cls.value(state), - random.choice(tuple(cls.actions(state))), # noqa: S311 - ) - next_down = None if depth is None else depth - 1 - - current_player = cls.player(state) - value: int | float - if current_player == Player.MAX: - value = -infinity - best = max - compare = operator.gt # greater than (>) - set_idx = 0 - elif current_player == Player.MIN: - value = infinity - best = min - compare = operator.lt # less than (<) - set_idx = 1 - elif current_player == Player.CHANCE: - raise ValueError("CHANCE is not valid for regular minimax.") - else: - raise ValueError(f"Unexpected player type {current_player!r}") - - best_action: Action | None = None - for action in cls.actions(state): - result = cls.alphabeta(cls.result(state, action), next_down, a, b) - new_value = best(value, result.value) - - if new_value != value: - best_action = action - value = new_value - - if compare(new_value, (a, b)[set_idx ^ 1]): - # print("cutoff") - break # cutoff - - alpha_beta_value = (a, b)[set_idx] - new_alpha_beta_value = best(alpha_beta_value, value) - - if new_alpha_beta_value != alpha_beta_value: - # Set new best - alpha_beta_list = [a, b] - alpha_beta_list[set_idx] = new_alpha_beta_value - a, b = alpha_beta_list - return MinimaxResult(value, best_action) - - -class Expectiminimax(ABC, Generic[State, Action]): - """Base class for Expectiminimax AIs.""" - - __slots__ = () - LOWEST = -1 HIGHEST = 1 @classmethod @abstractmethod def value(cls, state: State) -> int | float: - """Return the value of a given game state.""" + """Return the value of a given game state. + + Should be in range [cls.LOWEST, cls.HIGHEST]. + """ @classmethod @abstractmethod @@ -218,7 +85,7 @@ def probability(cls, action: Action) -> float: Should be in range [0.0, 1.0] for 0% and 100% chance respectively. """ - return 1.0 + raise NotImplementedError() @classmethod def minimax( diff --git a/src/azul/game.py b/src/azul/game.py index 29512a5..4b9708f 100644 --- a/src/azul/game.py +++ b/src/azul/game.py @@ -63,6 +63,7 @@ from azul.network_shared import DEFAULT_PORT, find_ip from azul.server import GameServer from azul.sound import SoundData, play_sound as base_play_sound +from azul.state import Tile from azul.statemachine import AsyncState from azul.tools import ( lerp_color, @@ -133,8 +134,6 @@ ) -from azul.state import Tile - TILESIZE = 15 # Colors diff --git a/src/azul/server.py b/src/azul/server.py index 2fc6c76..3769b39 100755 --- a/src/azul/server.py +++ b/src/azul/server.py @@ -31,7 +31,7 @@ import traceback from collections import deque from functools import partial -from typing import TYPE_CHECKING, NoReturn, cast +from typing import TYPE_CHECKING, NoReturn import trio @@ -58,7 +58,7 @@ from azul.state import State if TYPE_CHECKING: - from collections.abc import Awaitable, Callable, Iterable + from collections.abc import Awaitable, Callable class ServerClient(EncryptedNetworkEventComponent): @@ -123,7 +123,7 @@ async def handle_game_over(self, event: Event[int]) -> None: async def handle_initial_config( self, - event: Event[tuple[Pos, int]], + event: Event[tuple[None, int]], ) -> None: """Read initial config event and reraise as server[write]->initial_config.""" board_size, player_turn = event.data @@ -251,7 +251,6 @@ class GameServer(network.Server): "state", ) - board_size = (8, 8) max_clients = 4 def __init__(self, internal_singleplayer_mode: bool = False) -> None: @@ -461,7 +460,7 @@ async def handle_server_start_new_game(self, event: Event[None]) -> None: await self.raise_event( Event( "initial_config->network", - (self.board_size, self.state.turn), + (None, self.state.turn), ), ) @@ -535,29 +534,18 @@ async def send_spectator_join_packets( ) with self.temporary_component(private_events_pocket): with private_events_pocket.temporary_component(client): - ## # Send create_piece events for all pieces - ## async with trio.open_nursery() as nursery: - ## for piece_pos, piece_type in self.state.get_pieces(): - ## nursery.start_soon( - ## client.raise_event, - ## Event( - ## "create_piece->network", - ## (piece_pos, piece_type), - ## ), - ## ) - - await client.raise_event( - Event(f"playing_as->network[{client.client_id}]", 255), - ) - # Raise initial config event with board size and initial turn. await client.raise_event( Event( "initial_config->network", - (self.state.size, self.state.turn), + (None, self.state.turn), ), ) + await client.raise_event( + Event(f"playing_as->network[{client.client_id}]", 255), + ) + async def handler(self, stream: trio.SocketStream) -> None: """Accept clients. Called by network.Server.serve.""" if self.client_count == 0 and self.game_active(): @@ -607,141 +595,6 @@ async def handler(self, stream: trio.SocketStream) -> None: self.client_count -= 1 # ServerClient's `with` block handles closing stream. - async def handle_network_select_piece( - self, - event: Event[tuple[int, Pos]], - ) -> None: - """Handle piece event from client.""" - client_id, tile_pos = event.data - - player = self.client_players.get(client_id, 0xFF) - if player == 2: - player = int(self.state.turn) - - if player != self.state.turn: - print( - f"{player = } cannot select piece {tile_pos = } because it is not that player's turn", - ) - return - - if not self.players_can_interact: - print( - f"{player = } cannot select piece {tile_pos = } because players_can_interact is False", - ) - return - if not self.state.can_player_select_piece(player, tile_pos): - print(f"{player = } cannot select piece {tile_pos = }") - await self.player_select_piece(player, None) - return - if tile_pos == self.player_selections.get(player): - # print(f"{player = } toggle select -> No select") - await self.player_select_piece(player, None) - return - - await self.player_select_piece(player, tile_pos) - - async def player_select_piece( - self, - player: int, - piece_pos: Pos | None, - ) -> None: - """Update glowing tiles from new selected piece.""" - ignore: set[Pos] = set() - - if piece_pos is not None: - # Calculate actions if required - new_action_set = self.state.calculate_actions(piece_pos) - ignore = new_action_set.ends - - ignored: set[Pos] = set() - - # Remove outlined tiles from previous selection if existed - if prev_selection := self.player_selections.get(player): - action_set = self.state.calculate_actions(prev_selection) - ignored = action_set.ends & ignore - remove = action_set.ends - ignore - async with trio.open_nursery() as nursery: - for tile_position in remove: - nursery.start_soon( - self.raise_event, - Event("delete_tile->network", tile_position), - ) - if piece_pos != prev_selection: - nursery.start_soon( - self.raise_event, - Event( - "select_piece->network", - (prev_selection, False), - ), - ) - - if piece_pos is None: - if prev_selection: - del self.player_selections[player] - return - - self.player_selections[player] = piece_pos - - # For each end point - async with trio.open_nursery() as nursery: - for tile_position in new_action_set.ends - ignored: - nursery.start_soon( - self.raise_event, - Event("create_tile->network", tile_position), - ) - # Sent select piece as well - nursery.start_soon( - self.raise_event, - Event( - "select_piece->network", - (self.player_selections[player], True), - ), - ) - - async def handle_move_animation(self, from_pos: Pos, to_pos: Pos) -> None: - """Handle move animation.""" - await self.raise_event( - Event("move_piece_animation->network", (from_pos, to_pos)), - ) - - async def handle_jump_animation(self, jumped_pos: Pos) -> None: - """Handle jump animation.""" - await self.raise_event( - Event("delete_piece_animation->network", jumped_pos), - ) - - async def handle_king_animation( - self, - kinged_pos: Pos, - piece_type: int, - ) -> None: - """Handle jump animation.""" - await self.raise_event( - Event("update_piece_animation->network", (kinged_pos, piece_type)), - ) - - async def handle_action_animations( - self, - actions: deque[tuple[str, Iterable[Pos | int]]], - ) -> None: - """Handle action animations.""" - while actions: - name, params = actions.popleft() - if name == "move": - await self.handle_move_animation( - *cast("Iterable[Pos]", params), - ) - elif name == "jump": - await self.handle_jump_animation( - *cast("Iterable[Pos]", params), - ) - elif name == "king": - await self.handle_king_animation( - *cast("tuple[Pos, int]", params), - ) - else: - raise NotImplementedError(f"Animation for action {name}") - def __del__(self) -> None: """Debug print.""" print(f"del {self.__class__.__name__}") diff --git a/src/azul/state.py b/src/azul/state.py index 05236ff..5c836dc 100644 --- a/src/azul/state.py +++ b/src/azul/state.py @@ -36,7 +36,7 @@ TypeVar, ) -from numpy import array, int8 +from numpy import full, int8 if TYPE_CHECKING: from collections.abc import Generator @@ -192,10 +192,7 @@ class PlayerData(NamedTuple): @classmethod def new(cls, varient_play: bool = False) -> Self: """Return new player data instance.""" - wall = array( - [Tile.blank for _ in range(5 * 5)], - int8, - ).reshape((5, 5)) + wall = full((5, 5), Tile.blank, int8) if not varient_play: for y in range(5): @@ -376,7 +373,7 @@ def get_score_from_wall_placement( count += self.get_vertically_linked_wall_count(x, y, wall) return count - def perform_floor_line_scoring(self) -> int: + def get_floor_line_scoring(self) -> int: """Return score increment value from floor line.""" total_count = self.floor.total() assert total_count <= FLOOR_LINE_COUNT @@ -412,7 +409,7 @@ def perform_auto_wall_tiling(self) -> tuple[Self, Counter[int], bool]: PatternLine.blank(), ) - score += self.perform_floor_line_scoring() + score += self.get_floor_line_scoring() if score < 0: score = 0 @@ -722,7 +719,7 @@ def _factory_offer_maybe_next_turn(self) -> Self: ) if current_phase == Phase.wall_tiling: if self.varient_play: - raise NotImplementedError() + return new_state.start_manual_wall_tiling() return new_state.apply_auto_wall_tiling() return new_state @@ -1025,12 +1022,17 @@ def preform_action( return new raise NotImplementedError() + def start_manual_wall_tiling(self) -> Self: + """Return new state after starting manual wall tiling.""" + raise NotImplementedError() + return self + def run() -> None: """Run program.""" from market_api import pretty_print_response as pprint - random.seed(0) + random.seed(2) state = State.new_game(2) ticks = 0 try: From 1b5ce40806efe41f26e3768c5c5376abe84b7e67 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Mon, 25 Nov 2024 01:43:00 -0600 Subject: [PATCH 04/58] Switch to use my `libcomponent` module --- LICENSE-THIRD-PARTY | 392 ------------------ pyproject.toml | 2 +- src/azul/base_io.py | 701 -------------------------------- src/azul/buffer.py | 101 ----- src/azul/client.py | 64 +-- src/azul/component.py | 485 ---------------------- src/azul/encrypted_event.py | 130 ------ src/azul/encryption.py | 140 ------- src/azul/game.py | 154 +++---- src/azul/keyboard.py | 2 +- src/azul/mr_floppy_test.py | 2 +- src/azul/network.py | 512 ----------------------- src/azul/network_shared.py | 31 -- src/azul/objects.py | 3 +- src/azul/server.py | 86 +--- src/azul/sprite.py | 2 +- src/azul/utils.py | 49 --- test-requirements.in | 2 +- tests/helpers.py | 175 -------- tests/protocol_helpers.py | 87 ---- tests/test_base_io.py | 746 ---------------------------------- tests/test_buffer.py | 97 ----- tests/test_component.py | 370 ----------------- tests/test_encrypted_event.py | 79 ---- tests/test_encryption.py | 108 ----- tests/test_network.py | 178 -------- tests/test_utils.py | 121 ------ 27 files changed, 77 insertions(+), 4742 deletions(-) delete mode 100644 src/azul/base_io.py delete mode 100644 src/azul/buffer.py delete mode 100644 src/azul/component.py delete mode 100644 src/azul/encrypted_event.py delete mode 100644 src/azul/encryption.py delete mode 100644 src/azul/network.py delete mode 100644 src/azul/utils.py delete mode 100644 tests/helpers.py delete mode 100644 tests/protocol_helpers.py delete mode 100644 tests/test_base_io.py delete mode 100644 tests/test_buffer.py delete mode 100644 tests/test_component.py delete mode 100644 tests/test_encrypted_event.py delete mode 100644 tests/test_encryption.py delete mode 100644 tests/test_network.py delete mode 100644 tests/test_utils.py diff --git a/LICENSE-THIRD-PARTY b/LICENSE-THIRD-PARTY index c7343f4..e1a17ae 100644 --- a/LICENSE-THIRD-PARTY +++ b/LICENSE-THIRD-PARTY @@ -1,395 +1,3 @@ ---------------------------------------------------------------------------------------------------- - GNU LESSER GENERAL PUBLIC LICENSE -Applies to: - - Copyright (c) 2023, ItsDrike - All rights reserved. - - src/azul/base_io.py: Entire file - - src/azul/buffer.py: Entire file - - src/azul/utils.py: Entire file - - src/azul/encryption.py: Almost entire file (see details below) - - src/azul/encrypted_event.py: Almost entire file (see details below) - - tests/helpers.py: Entire file - - tests/protocol_helpers.py: Entire file - - tests/test_base_io.py: Entire file - - tests/test_buffer.py: Entire file - - tests/test_encryption.py: Entire file - - tests/test_utils.py: Entire file ---------------------------------------------------------------------------------------------------- - GNU LESSER GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - - This version of the GNU Lesser General Public License incorporates -the terms and conditions of version 3 of the GNU General Public -License, supplemented by the additional permissions listed below. - - 0. Additional Definitions. - - As used herein, "this License" refers to version 3 of the GNU Lesser -General Public License, and the "GNU GPL" refers to version 3 of the GNU -General Public License. - - "The Library" refers to a covered work governed by this License, -other than an Application or a Combined Work as defined below. - - An "Application" is any work that makes use of an interface provided -by the Library, but which is not otherwise based on the Library. -Defining a subclass of a class defined by the Library is deemed a mode -of using an interface provided by the Library. - - A "Combined Work" is a work produced by combining or linking an -Application with the Library. The particular version of the Library -with which the Combined Work was made is also called the "Linked -Version". - - The "Minimal Corresponding Source" for a Combined Work means the -Corresponding Source for the Combined Work, excluding any source code -for portions of the Combined Work that, considered in isolation, are -based on the Application, and not on the Linked Version. - - The "Corresponding Application Code" for a Combined Work means the -object code and/or source code for the Application, including any data -and utility programs needed for reproducing the Combined Work from the -Application, but excluding the System Libraries of the Combined Work. - - 1. Exception to Section 3 of the GNU GPL. - - You may convey a covered work under sections 3 and 4 of this License -without being bound by section 3 of the GNU GPL. - - 2. Conveying Modified Versions. - - If you modify a copy of the Library, and, in your modifications, a -facility refers to a function or data to be supplied by an Application -that uses the facility (other than as an argument passed when the -facility is invoked), then you may convey a copy of the modified -version: - - a) under this License, provided that you make a good faith effort to - ensure that, in the event an Application does not supply the - function or data, the facility still operates, and performs - whatever part of its purpose remains meaningful, or - - b) under the GNU GPL, with none of the additional permissions of - this License applicable to that copy. - - 3. Object Code Incorporating Material from Library Header Files. - - The object code form of an Application may incorporate material from -a header file that is part of the Library. You may convey such object -code under terms of your choice, provided that, if the incorporated -material is not limited to numerical parameters, data structure -layouts and accessors, or small macros, inline functions and templates -(ten or fewer lines in length), you do both of the following: - - a) Give prominent notice with each copy of the object code that the - Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the object code with a copy of the GNU GPL and this license - document. - - 4. Combined Works. - - You may convey a Combined Work under terms of your choice that, -taken together, effectively do not restrict modification of the -portions of the Library contained in the Combined Work and reverse -engineering for debugging such modifications, if you also do each of -the following: - - a) Give prominent notice with each copy of the Combined Work that - the Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the Combined Work with a copy of the GNU GPL and this license - document. - - c) For a Combined Work that displays copyright notices during - execution, include the copyright notice for the Library among - these notices, as well as a reference directing the user to the - copies of the GNU GPL and this license document. - - d) Do one of the following: - - 0) Convey the Minimal Corresponding Source under the terms of this - License, and the Corresponding Application Code in a form - suitable for, and under terms that permit, the user to - recombine or relink the Application with a modified version of - the Linked Version to produce a modified Combined Work, in the - manner specified by section 6 of the GNU GPL for conveying - Corresponding Source. - - 1) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (a) uses at run time - a copy of the Library already present on the user's computer - system, and (b) will operate properly with a modified version - of the Library that is interface-compatible with the Linked - Version. - - e) Provide Installation Information, but only if you would otherwise - be required to provide such information under section 6 of the - GNU GPL, and only to the extent that such information is - necessary to install and execute a modified version of the - Combined Work produced by recombining or relinking the - Application with a modified version of the Linked Version. (If - you use option 4d0, the Installation Information must accompany - the Minimal Corresponding Source and Corresponding Application - Code. If you use option 4d1, you must provide the Installation - Information in the manner specified by section 6 of the GNU GPL - for conveying Corresponding Source.) - - 5. Combined Libraries. - - You may place library facilities that are a work based on the -Library side by side in a single library together with other library -facilities that are not Applications and are not covered by this -License, and convey such a combined library under terms of your -choice, if you do both of the following: - - a) Accompany the combined library with a copy of the same work based - on the Library, uncombined with any other library facilities, - conveyed under the terms of this License. - - b) Give prominent notice with the combined library that part of it - is a work based on the Library, and explaining where to find the - accompanying uncombined form of the same work. - - 6. Revised Versions of the GNU Lesser General Public License. - - The Free Software Foundation may publish revised and/or new versions -of the GNU Lesser General Public License from time to time. Such new -versions will be similar in spirit to the present version, but may -differ in detail to address new problems or concerns. - - Each version is given a distinguishing version number. If the -Library as you received it specifies that a certain numbered version -of the GNU Lesser General Public License "or any later version" -applies to it, you have the option of following the terms and -conditions either of that published version or of any later version -published by the Free Software Foundation. If the Library as you -received it does not specify a version number of the GNU Lesser -General Public License, you may choose any version of the GNU Lesser -General Public License ever published by the Free Software Foundation. - - If the Library as you received it specifies that a proxy can decide -whether future versions of the GNU Lesser General Public License shall -apply, that proxy's public statement of acceptance of any version is -permanent authorization for you to choose that version for the -Library. ---------------------------------------------------------------------------------------------------- - Apache License version 2.0 -Applies to: - - Copyright (c) 2012 Ammar Askar - All rights reserved. - - src/azul/encryption.py: encrypt_token_and_secret, generate_shared_secret functions - - src/azul/encrypted_event.py: read, write functions ---------------------------------------------------------------------------------------------------- - - - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --------------------------------------------------------------------------------------------------- CREATIVE COMMONS 0 Applies to: diff --git a/pyproject.toml b/pyproject.toml index 1344d9c..b8a5cce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,11 +36,11 @@ keywords = [ "ai", "multi-player", "azul", "ai-support", "networked-game" ] dependencies = [ + "libcomponent~=0.0.0", "pygame~=2.6.0", "typing_extensions>=4.12.2", "mypy_extensions>=1.0.0", "trio~=0.27.0", - "cryptography>=43.0.0", "exceptiongroup; python_version < '3.11'", "numpy~=2.1.3", ] diff --git a/src/azul/base_io.py b/src/azul/base_io.py deleted file mode 100644 index f85b31f..0000000 --- a/src/azul/base_io.py +++ /dev/null @@ -1,701 +0,0 @@ -"""Base IO classes.""" - -# This is the base_io module from https://github.com/py-mine/mcproto v0.3.0, -# which is licensed under the GNU LESSER GENERAL PUBLIC LICENSE v3.0 - -from __future__ import annotations - -__author__ = "ItsDrike" -__license__ = "LGPL-3.0-only" - -import struct -from abc import ABC, abstractmethod -from enum import Enum -from itertools import count -from typing import TYPE_CHECKING, Literal, TypeAlias, TypeVar, overload - -from .utils import from_twos_complement, to_twos_complement - -if TYPE_CHECKING: - from collections.abc import Awaitable, Callable - -__all__ = [ - "FLOAT_FORMATS_TYPE", - "INT_FORMATS_TYPE", - "BaseAsyncReader", - "BaseAsyncWriter", - "BaseSyncReader", - "BaseSyncWriter", - "StructFormat", -] - -T = TypeVar("T") -R = TypeVar("R") - - -# region: Format types - - -class StructFormat(str, Enum): - """All possible write/read struct types. - - .. seealso: - :module:`struct` module documentation. - """ - - BOOL = "?" - CHAR = "c" - BYTE = "b" - UBYTE = "B" - SHORT = "h" - USHORT = "H" - INT = "i" - UINT = "I" - LONG = "l" - ULONG = "L" - FLOAT = "f" - DOUBLE = "d" - HALFFLOAT = "e" - LONGLONG = "q" - ULONGLONG = "Q" - - -INT_FORMATS_TYPE: TypeAlias = Literal[ - StructFormat.BYTE, - StructFormat.UBYTE, - StructFormat.SHORT, - StructFormat.USHORT, - StructFormat.INT, - StructFormat.UINT, - StructFormat.LONG, - StructFormat.ULONG, - StructFormat.LONGLONG, - StructFormat.ULONGLONG, -] - -FLOAT_FORMATS_TYPE: TypeAlias = Literal[ - StructFormat.FLOAT, - StructFormat.DOUBLE, - StructFormat.HALFFLOAT, -] - -# endregion - -# region: Writer classes - - -class BaseAsyncWriter(ABC): - """Base class holding asynchronous write buffer/connection interactions.""" - - __slots__ = () - - @abstractmethod - async def write(self, data: bytes, /) -> None: - """Underlying write method, sending/storing the data. - - All the writer functions will eventually call this method. - """ - - @overload - async def write_value( - self, - fmt: INT_FORMATS_TYPE, - value: int, - /, - ) -> None: ... - - @overload - async def write_value( - self, - fmt: FLOAT_FORMATS_TYPE, - value: float, - /, - ) -> None: ... - - @overload - async def write_value( - self, - fmt: Literal[StructFormat.BOOL], - value: bool, - /, - ) -> None: ... - - @overload - async def write_value( - self, - fmt: Literal[StructFormat.CHAR], - value: str, - /, - ) -> None: ... - - async def write_value(self, fmt: StructFormat, value: object, /) -> None: - """Write a given ``value`` as given struct format (``fmt``) in big-endian mode.""" - await self.write(struct.pack(">" + fmt.value, value)) - - async def _write_varuint( - self, - value: int, - /, - *, - max_bits: int | None = None, - ) -> None: - """Write an arbitrarily big unsigned integer in a variable length format. - - This is a standard way of transmitting ints, and it allows smaller numbers to take less bytes. - - Writing will be limited up to integer values of ``max_bits`` bits, and trying to write bigger values will rase - a :exc:`ValueError`. Note that setting ``max_bits`` to for example 32 bits doesn't mean that at most 4 bytes - will be sent, in this case it would actually take at most 5 bytes, due to the variable encoding overhead. - - Varints send bytes where 7 least significant bits are value bits, and the most significant bit is continuation - flag bit. If this continuation bit is set (1), it indicates that there will be another varint byte sent after - this one. The least significant group is written first, followed by each of the more significant groups, making - varints little-endian, however in groups of 7 bits, not 8. - """ - value_max = ( - (1 << (max_bits)) - 1 if max_bits is not None else float("inf") - ) - if value < 0 or value > value_max: - raise ValueError( - f"Tried to write varint outside of the range of {max_bits}-bit int.", - ) - - remaining = value - while True: - if remaining & -0x80 == 0: # final byte (~0x7F) - await self.write_value(StructFormat.UBYTE, remaining) - return - # Write only 7 least significant bits with the first bit being 1, marking there will be another byte - await self.write_value(StructFormat.UBYTE, remaining & 0x7F | 0x80) - # Subtract the value we've already sent (7 least significant bits) - remaining >>= 7 - - async def write_varint(self, value: int, /) -> None: - """Write a 32-bit signed integer in a variable length format. - - For more information about variable length format check :meth:`._write_varuint`. - - Raises ValueError if value is outside of the range of a 32-bit signed integer. - """ - val = to_twos_complement(value, bits=32) - await self._write_varuint(val, max_bits=32) - - async def write_varlong(self, value: int, /) -> None: - """Write a 64-bit signed integer in a variable length format. - - For more information about variable length format check :meth:`._write_varuint`. - - Raises ValueError if value is outside of the range of a 64-bit signed integer. - """ - val = to_twos_complement(value, bits=64) - await self._write_varuint(val, max_bits=64) - - async def write_bytearray(self, data: bytes, /) -> None: - """Write an arbitrary sequence of bytes, prefixed with a varint of it's size. - - Raises ValueError if length is is outside of the range of a 32-bit signed integer. - """ - await self.write_varint(len(data)) - await self.write(data) - - async def write_ascii(self, value: str, /) -> None: - """Write ISO-8859-1 encoded string, with NULL (0x00) at the end to indicate string end.""" - data = bytearray(value, "ISO-8859-1") - await self.write(data) - await self.write(bytearray.fromhex("00")) - - async def write_utf(self, value: str, /) -> None: - """Write a UTF-8 encoded string, prefixed with a varint of it's size (in bytes). - - The maximum amount of UTF-8 characters is limited to 32767. - - Individual UTF-8 characters can take up to 4 bytes, however most of the common ones take up less. Assuming the - worst case of 4 bytes per every character, at most 131068 data bytes will be written + 3 additional bytes from - the varint encoding overhead. - - :raises ValueError: - If the given string ``value`` has more characters than the allowed maximum (32767). - """ - if len(value) > 32767: - raise ValueError( - "Maximum character limit for writing strings is 32767 characters.", - ) - - data = bytearray(value, "utf-8") - await self.write_varint(len(data)) - await self.write(data) - - async def write_optional( - self, - value: T | None, - /, - writer: Callable[[T], Awaitable[R]], - ) -> R | None: - """Write a bool showing if a ``value`` is present, if so, also writes this value with ``writer`` function. - - * When ``value`` is ``None``, a bool of ``False`` will be written, and ``None`` is returned. - * When ``value`` is not ``None``, a bool of ``True`` is written, after which the ``writer`` function is called, - and the return value is forwarded. - """ - if value is None: - await self.write_value(StructFormat.BOOL, False) - return None - - await self.write_value(StructFormat.BOOL, True) - return await writer(value) - - -class BaseSyncWriter(ABC): - """Base class holding synchronous write buffer/connection interactions.""" - - __slots__ = () - - @abstractmethod - def write(self, data: bytes, /) -> None: - """Write data.""" - ... - - @overload - def write_value(self, fmt: INT_FORMATS_TYPE, value: int, /) -> None: ... - - @overload - def write_value( - self, - fmt: FLOAT_FORMATS_TYPE, - value: float, - /, - ) -> None: ... - - @overload - def write_value( - self, - fmt: Literal[StructFormat.BOOL], - value: bool, - /, - ) -> None: ... - - @overload - def write_value( - self, - fmt: Literal[StructFormat.CHAR], - value: str, - /, - ) -> None: ... - - def write_value(self, fmt: StructFormat, value: object, /) -> None: - """Write a given ``value`` as given struct format (``fmt``) in big-endian mode.""" - self.write(struct.pack(">" + fmt.value, value)) - - def _write_varuint( - self, - value: int, - /, - *, - max_bits: int | None = None, - ) -> None: - """Write an arbitrarily big unsigned integer in a variable length format. - - This is a standard way of transmitting ints, and it allows smaller numbers to take less bytes. - - Writing will be limited up to integer values of ``max_bits`` bits, and trying to write bigger values will rase - a :exc:`ValueError`. Note that setting ``max_bits`` to for example 32 bits doesn't mean that at most 4 bytes - will be sent, in this case it would actually take at most 5 bytes, due to the variable encoding overhead. - - Varints send bytes where 7 least significant bits are value bits, and the most significant bit is continuation - flag bit. If this continuation bit is set (1), it indicates that there will be another varint byte sent after - this one. The least significant group is written first, followed by each of the more significant groups, making - varints little-endian, however in groups of 7 bits, not 8. - """ - value_max = ( - (1 << (max_bits)) - 1 if max_bits is not None else float("inf") - ) - if value < 0 or value > value_max: - raise ValueError( - f"Tried to write varint outside of the range of {max_bits}-bit int.", - ) - - remaining = value - while True: - if remaining & ~0x7F == 0: # final byte - self.write_value(StructFormat.UBYTE, remaining) - return - # Write only 7 least significant bits with the first bit being 1, marking there will be another byte - self.write_value(StructFormat.UBYTE, remaining & 0x7F | 0x80) - # Subtract the value we've already sent (7 least significant bits) - remaining >>= 7 - - def write_varint(self, value: int, /) -> None: - """Write a 32-bit signed integer in a variable length format. - - For more information about variable length format check :meth:`._write_varuint`. - - Raises ValueError if length is is outside of the range of a 32-bit signed integer. - """ - val = to_twos_complement(value, bits=32) - self._write_varuint(val, max_bits=32) - - def write_varlong(self, value: int, /) -> None: - """Write a 64-bit signed integer in a variable length format. - - For more information about variable length format check :meth:`._write_varuint` docstring. - - Raises ValueError if length is is outside of the range of a 64-bit signed integer. - """ - val = to_twos_complement(value, bits=64) - self._write_varuint(val, max_bits=64) - - def write_bytearray(self, data: bytes, /) -> None: - """Write an arbitrary sequence of bytes, prefixed with a varint of it's size. - - Raises ValueError if length is is outside of the range of a 32-bit signed integer. - """ - self.write_varint(len(data)) - self.write(data) - - def write_ascii(self, value: str, /) -> None: - """Write ISO-8859-1 encoded string, with NULL (0x00) at the end to indicate string end.""" - data = bytearray(value, "ISO-8859-1") - self.write(data) - self.write(bytearray.fromhex("00")) - - def write_utf(self, value: str, /) -> None: - """Write a UTF-8 encoded string, prefixed with a varint of it's size (in bytes). - - The maximum amount of UTF-8 characters is limited to 32767. - - Individual UTF-8 characters can take up to 4 bytes, however most of the common ones take up less. Assuming the - worst case of 4 bytes per every character, at most 131068 data bytes will be written + 3 additional bytes from - the varint encoding overhead. - - :raises ValueError: - If the given string ``value`` has more characters than the allowed maximum (32767). - """ - if len(value) > 32767: - raise ValueError( - "Maximum character limit for writing strings is 32767 characters.", - ) - - data = bytearray(value, "utf-8") - self.write_varint(len(data)) - self.write(data) - - def write_optional( - self, - value: T | None, - /, - writer: Callable[[T], R], - ) -> R | None: - """Write a bool showing if a ``value`` is present, if so, also writes this value with ``writer`` function. - - * When ``value`` is ``None``, a bool of ``False`` will be written, and ``None`` is returned. - * When ``value`` is not ``None``, a bool of ``True`` is written, after which the ``writer`` function is called, - and the return value is forwarded. - """ - if value is None: - self.write_value(StructFormat.BOOL, False) - return None - - self.write_value(StructFormat.BOOL, True) - return writer(value) - - -# endregion -# region: Reader classes - - -class BaseAsyncReader(ABC): - """Base class holding asynchronous read buffer/connection interactions.""" - - __slots__ = () - - @abstractmethod - async def read(self, length: int, /) -> bytearray: - """Underlying read method, obtaining the raw data. - - All of the reader functions will eventually call this method. - """ - - @overload - async def read_value(self, fmt: INT_FORMATS_TYPE, /) -> int: ... - - @overload - async def read_value(self, fmt: FLOAT_FORMATS_TYPE, /) -> float: ... - - @overload - async def read_value(self, fmt: Literal[StructFormat.BOOL], /) -> bool: ... - - @overload - async def read_value(self, fmt: Literal[StructFormat.CHAR], /) -> str: ... - - async def read_value(self, fmt: StructFormat, /) -> object: - """Read a value as given struct format (``fmt``) in big-endian mode. - - The amount of bytes to read will be determined based on the struct format automatically. - """ - length = struct.calcsize(fmt.value) - data = await self.read(length) - unpacked = struct.unpack(">" + fmt.value, data) - return unpacked[0] - - async def _read_varuint(self, *, max_bits: int | None = None) -> int: - """Read an arbitrarily big unsigned integer in a variable length format. - - This is a standard way of transmitting ints, and it allows smaller numbers to take less bytes. - - Reading will be limited up to integer values of ``max_bits`` bits, and trying to read bigger values will rase - an :exc:`OSError`. Note that setting ``max_bits`` to for example 32 bits doesn't mean that at most 4 bytes - will be read, in this case we would actually read at most 5 bytes, due to the variable encoding overhead. - - Varints send bytes where 7 least significant bits are value bits, and the most significant bit is continuation - flag bit. If this continuation bit is set (1), it indicates that there will be another varint byte sent after - this one. The least significant group is written first, followed by each of the more significant groups, making - varints little-endian, however in groups of 7 bits, not 8. - """ - value_max = ( - (1 << (max_bits)) - 1 if max_bits is not None else float("inf") - ) - - result = 0 - for i in count(): - byte = await self.read_value(StructFormat.UBYTE) - # Read 7 least significant value bits in this byte, and shift them appropriately to be in the right place - # then simply add them (OR) as additional 7 most significant bits in our result - result |= (byte & 0x7F) << (7 * i) - - # Ensure that we stop reading and raise an error if the size gets over the maximum - # (if the current amount of bits is higher than allowed size in bits) - if result > value_max: - raise OSError( - f"Received varint was outside the range of {max_bits}-bit int.", - ) - - # If the most significant bit is 0, we should stop reading - if not byte & 0x80: - break - - return result - - async def read_varint(self) -> int: - """Read a 32-bit signed integer in a variable length format. - - For more information about variable length format check :meth:`._read_varuint`. - """ - unsigned_num = await self._read_varuint(max_bits=32) - return from_twos_complement(unsigned_num, bits=32) - - async def read_varlong(self) -> int: - """Read a 64-bit signed integer in a variable length format. - - For more information about variable length format check :meth:`._read_varuint`. - """ - unsigned_num = await self._read_varuint(max_bits=64) - return from_twos_complement(unsigned_num, bits=64) - - async def read_bytearray(self, /) -> bytearray: - """Read an arbitrary sequence of bytes, prefixed with a varint of it's size.""" - length = await self.read_varint() - return await self.read(length) - - async def read_ascii(self) -> str: - """Read ISO-8859-1 encoded string, until we encounter NULL (0x00) at the end indicating string end.""" - # Keep reading bytes until we find NULL - result = bytearray() - while len(result) == 0 or result[-1] != 0: - byte = await self.read(1) - result.extend(byte) - return result[:-1].decode("ISO-8859-1") - - async def read_utf(self) -> str: - """Read a UTF-8 encoded string, prefixed with a varint of it's size (in bytes). - - The maximum amount of UTF-8 characters is limited to 32767. - - Individual UTF-8 characters can take up to 4 bytes, however most of the common ones take up less. Assuming the - worst case of 4 bytes per every character, at most 131068 data bytes will be read + 3 additional bytes from - the varint encoding overhead. - - :raises IOError: - * If the prefix varint is bigger than the maximum (131068) bytes, the string will not be read at all, - and :exc:`IOError` will be raised immediately. - * If the received string has more than the maximum amount of characters (32767). Note that in this case, - the string will still get read in it's entirety, since it fits into the maximum bytes limit (131068), - which was simply read at once. This limitation is here only to replicate the behavior of minecraft's - implementation. - """ - length = await self.read_varint() - if length > 131068: - raise OSError( - f"Maximum read limit for utf strings is 131068 bytes, got {length}.", - ) - - data = await self.read(length) - chars = data.decode("utf-8") - - if len(chars) > 32767: - raise OSError( - f"Maximum read limit for utf strings is 32767 characters, got {len(chars)}.", - ) - - return chars - - async def read_optional( - self, - reader: Callable[[], Awaitable[R]], - ) -> R | None: - """Read a bool showing if a value is present, if so, also reads this value with ``reader`` function. - - * When ``False`` is read, the function will not read anything and ``None`` is returned. - * When ``True`` is read, the ``reader`` function is called, and it's return value is forwarded. - """ - if not await self.read_value(StructFormat.BOOL): - return None - - return await reader() - - -class BaseSyncReader(ABC): - """Base class holding synchronous read buffer/connection interactions.""" - - __slots__ = () - - @abstractmethod - def read(self, length: int, /) -> bytearray: - """Read ``length`` bytes and return in a bytearray.""" - ... - - @overload - def read_value(self, fmt: INT_FORMATS_TYPE, /) -> int: ... - - @overload - def read_value(self, fmt: FLOAT_FORMATS_TYPE, /) -> float: ... - - @overload - def read_value(self, fmt: Literal[StructFormat.BOOL], /) -> bool: ... - - @overload - def read_value(self, fmt: Literal[StructFormat.CHAR], /) -> str: ... - - def read_value(self, fmt: StructFormat, /) -> object: - """Read a value into given struct format in big-endian mode. - - The amount of bytes to read will be determined based on the struct format automatically. - """ - length = struct.calcsize(fmt.value) - data = self.read(length) - unpacked = struct.unpack(">" + fmt.value, data) - return unpacked[0] - - def _read_varuint(self, *, max_bits: int | None = None) -> int: - """Read an arbitrarily big unsigned integer in a variable length format. - - This is a standard way of transmitting ints, and it allows smaller numbers to take less bytes. - - Reading will be limited up to integer values of ``max_bits`` bits, and trying to read bigger values will rase - an :exc:`IOError`. Note that setting ``max_bits`` to for example 32 bits doesn't mean that at most 4 bytes - will be read, in this case we would actually read at most 5 bytes, due to the variable encoding overhead. - - Varints send bytes where 7 least significant bits are value bits, and the most significant bit is continuation - flag bit. If this continuation bit is set (1), it indicates that there will be another varint byte sent after - this one. The least significant group is written first, followed by each of the more significant groups, making - varints little-endian, however in groups of 7 bits, not 8. - """ - value_max = ( - (1 << (max_bits)) - 1 if max_bits is not None else float("inf") - ) - - result = 0 - for i in count(): - byte = self.read_value(StructFormat.UBYTE) - # Read 7 least significant value bits in this byte, and shift them appropriately to be in the right place - # then simply add them (OR) as additional 7 most significant bits in our result - result |= (byte & 0x7F) << (7 * i) - - # Ensure that we stop reading and raise an error if the size gets over the maximum - # (if the current amount of bits is higher than allowed size in bits) - if result > value_max: - raise OSError( - f"Received varint was outside the range of {max_bits}-bit int.", - ) - - # If the most significant bit is 0, we should stop reading - if not byte & 0x80: - break - - return result - - def read_varint(self) -> int: - """Read a 32-bit signed integer in a variable length format. - - For more information about variable length format check :meth:`._read_varuint`. - """ - unsigned_num = self._read_varuint(max_bits=32) - return from_twos_complement(unsigned_num, bits=32) - - def read_varlong(self) -> int: - """Read a 64-bit signed integer in a variable length format. - - For more information about variable length format check :meth:`._read_varuint`. - """ - unsigned_num = self._read_varuint(max_bits=64) - return from_twos_complement(unsigned_num, bits=64) - - def read_bytearray(self) -> bytearray: - """Read an arbitrary sequence of bytes, prefixed with a varint of it's size.""" - length = self.read_varint() - return self.read(length) - - def read_ascii(self) -> str: - """Read ISO-8859-1 encoded string, until we encounter NULL (0x00) at the end indicating string end.""" - # Keep reading bytes until we find NULL - result = bytearray() - while len(result) == 0 or result[-1] != 0: - byte = self.read(1) - result.extend(byte) - return result[:-1].decode("ISO-8859-1") - - def read_utf(self) -> str: - """Read a UTF-8 encoded string, prefixed with a varint of it's size (in bytes). - - The maximum amount of UTF-8 characters is limited to 32767. - - Individual UTF-8 characters can take up to 4 bytes, however most of the common ones take up less. Assuming the - worst case of 4 bytes per every character, at most 131068 data bytes will be read + 3 additional bytes from - the varint encoding overhead. - - :raises IOError: - * If the prefix varint is bigger than the maximum (131068) bytes, the string will not be read at all, - and :exc:`IOError` will be raised immediately. - * If the received string has more than the maximum amount of characters (32767). Note that in this case, - the string will still get read in it's entirety, since it fits into the maximum bytes limit (131068), - which was simply read at once. This limitation is here only to replicate the behavior of minecraft's - implementation. - """ - length = self.read_varint() - if length > 131068: - raise OSError( - f"Maximum read limit for utf strings is 131068 bytes, got {length}.", - ) - - data = self.read(length) - chars = data.decode("utf-8") - - if len(chars) > 32767: - raise OSError( - f"Maximum read limit for utf strings is 32767 characters, got {len(chars)}.", - ) - - return chars - - def read_optional(self, reader: Callable[[], R]) -> R | None: - """Read a bool showing if a value is present, if so, also reads this value with ``reader`` function. - - * When ``False`` is read, the function will not read anything and ``None`` is returned. - * When ``True`` is read, the ``reader`` function is called, and it's return value is forwarded. - """ - if not self.read_value(StructFormat.BOOL): - return None - - return reader() - - -# endregion diff --git a/src/azul/buffer.py b/src/azul/buffer.py deleted file mode 100644 index 8207bc2..0000000 --- a/src/azul/buffer.py +++ /dev/null @@ -1,101 +0,0 @@ -"""Buffer module.""" - -# This is the buffer module from https://github.com/py-mine/mcproto v0.5.0, -# which is licensed under the GNU LESSER GENERAL PUBLIC LICENSE v3.0 - -from __future__ import annotations - -__author__ = "ItsDrike" -__license__ = "LGPL-3.0-only" - -from typing import Any - -from .base_io import BaseSyncReader, BaseSyncWriter - -__all__ = ["Buffer"] - - -class Buffer(BaseSyncWriter, BaseSyncReader, bytearray): - """In-memory bytearray-like buffer supporting the common read/write operations.""" - - __slots__ = ("pos",) - - def __init__(self, *args: Any, **kwargs: Any) -> None: - """Initialize starting at position zero.""" - super().__init__(*args, **kwargs) - self.pos = 0 - - def write(self, data: bytes) -> None: - """Write/Store given ``data`` into the buffer.""" - self.extend(data) - - def read(self, length: int) -> bytearray: - """Read data stored in the buffer. - - Reading data doesn't remove that data, rather that data is treated as already read, and - next read will start from the first unread byte. If freeing the data is necessary, check - the :meth:`.clear` function. - - :param length: - Amount of bytes to be read. - - If the requested amount can't be read (buffer doesn't contain that much data/buffer - doesn't contain any data), an :exc:`IOError` will be re-raised. - - If there were some data in the buffer, but it was less than requested, this remaining - data will still be depleted and the partial data that was read will be a part of the - error message in the :exc:`IOError`. This behavior is here to mimic reading from a real - socket connection. - """ - end = self.pos + length - - if end > len(self): - data = self[self.pos : len(self)] - bytes_read = len(self) - self.pos - self.pos = len(self) - raise OSError( - "Requested to read more data than available." - f" Read {bytes_read} bytes: {data}, out of {length} requested bytes.", - ) - - try: - return self[self.pos : end] - finally: - self.pos = end - - def clear(self, only_already_read: bool = False) -> None: - """Clear out the stored data and reset position. - - :param only_already_read: - When set to ``True``, only the data that was already marked as read will be cleared, - and the position will be reset (to start at the remaining data). This can be useful - for avoiding needlessly storing large amounts of data in memory, if this data is no - longer useful. - - Otherwise, if set to ``False``, all of the data is cleared, and the position is reset, - essentially resulting in a blank buffer. - """ - if only_already_read: - del self[: self.pos] - else: - super().clear() - self.pos = 0 - - def reset(self) -> None: - """Reset the position in the buffer. - - Since the buffer doesn't automatically clear the already read data, it is possible to simply - reset the position and read the data it contains again. - """ - self.pos = 0 - - def flush(self) -> bytearray: - """Read all of the remaining data in the buffer and clear it out.""" - data = self[self.pos : len(self)] - self.clear() - return data - - @property - def remaining(self) -> int: - """Get the amount of bytes that's still remaining in the buffer to be read.""" - return len(self) - self.pos diff --git a/src/azul/client.py b/src/azul/client.py index 697101c..d830f74 100644 --- a/src/azul/client.py +++ b/src/azul/client.py @@ -25,22 +25,16 @@ __version__ = "0.0.0" import struct -import time import traceback from typing import TYPE_CHECKING import trio +from libcomponent import network +from libcomponent.base_io import StructFormat +from libcomponent.buffer import Buffer +from libcomponent.component import Event +from libcomponent.network_utils import ClientNetworkEventComponent -from azul import network -from azul.base_io import StructFormat -from azul.buffer import Buffer -from azul.component import Event -from azul.encrypted_event import EncryptedNetworkEventComponent -from azul.encryption import ( - deserialize_public_key, - encrypt_token_and_secret, - generate_shared_secret, -) from azul.network_shared import ( ADVERTISEMENT_IP, ADVERTISEMENT_PORT, @@ -148,7 +142,7 @@ async def read_advertisements( return response -class GameClient(EncryptedNetworkEventComponent): +class GameClient(ClientNetworkEventComponent): """Game Client Network Event Component. This class handles connecting to the game server, transmitting events @@ -325,18 +319,6 @@ async def handle_client_connect( return await self.raise_disconnect("Error connecting to server.") - async def read_callback_ping(self, event: Event[bytearray]) -> None: - """Read callback_ping event from server.""" - ns = int.from_bytes(event.data) - now = int(time.time() * 1e9) - difference = now - ns - - # print(f'{difference / 1e9 = } seconds') - - await self.raise_event( - Event("callback_ping", difference), - ) - async def read_game_over(self, event: Event[bytearray]) -> None: """Read update_piece event from server.""" buffer = Buffer(event.data) @@ -367,40 +349,6 @@ async def read_playing_as(self, event: Event[bytearray]) -> None: Event("game_playing_as", playing_as), ) - async def write_encryption_response( - self, - shared_secret: bytes, - verify_token: bytes, - ) -> None: - """Write encryption response to server.""" - buffer = Buffer() - buffer.write_bytearray(shared_secret) - buffer.write_bytearray(verify_token) - - await self.write_event(Event("encryption_response->server", buffer)) - - async def read_encryption_request(self, event: Event[bytearray]) -> None: - """Read and handle encryption request from server.""" - buffer = Buffer(event.data) - - serialized_public_key = buffer.read_bytearray() - verify_token = buffer.read_bytearray() - - public_key = deserialize_public_key(serialized_public_key) - - shared_secret = generate_shared_secret() - - encrypted_token, encrypted_secret = encrypt_token_and_secret( - public_key, - verify_token, - shared_secret, - ) - - await self.write_encryption_response(encrypted_secret, encrypted_token) - - # Start encrypting all future data - self.enable_encryption(shared_secret, verify_token) - async def handle_network_stop(self, event: Event[None]) -> None: """Send EOF if connected and close socket.""" if self.not_connected: diff --git a/src/azul/component.py b/src/azul/component.py deleted file mode 100644 index 487db3a..0000000 --- a/src/azul/component.py +++ /dev/null @@ -1,485 +0,0 @@ -"""Component system module - Components instead of chaotic class hierarchy mess.""" - -# Programmed by CoolCat467 - -# Copyright (C) 2023-2024 CoolCat467 -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -from __future__ import annotations - -__title__ = "Component" -__author__ = "CoolCat467" -__license__ = "GNU General Public License Version 3" -__version__ = "0.0.0" - -from contextlib import contextmanager -from typing import TYPE_CHECKING, Any, Generic, TypeVar -from weakref import ref - -import trio - -if TYPE_CHECKING: - from collections.abc import Awaitable, Callable, Generator, Iterable - - from mypy_extensions import u8 - -T = TypeVar("T") - - -class Event(Generic[T]): - """Event with name, data, and re-raise levels.""" - - __slots__ = ("data", "level", "name") - - def __init__( - self, - name: str, - data: T, - levels: u8 = 0, - ) -> None: - """Initialize event.""" - self.name = name - self.data = data - self.level = levels - - def __repr__(self) -> str: - """Return representation of self.""" - return f"{self.__class__.__name__}({self.name!r}, {self.data!r}, {self.level!r})" - - def pop_level(self) -> bool: - """Travel up one level and return True if event should continue or not.""" - continue_level = self.level > 0 - self.level = max(0, self.level - 1) - return continue_level - - -class Component: - """Component base class.""" - - __slots__ = ("__manager", "name") - - def __init__(self, name: object) -> None: - """Initialise with name.""" - self.name = name - self.__manager: ref[ComponentManager] | None = None - - def __repr__(self) -> str: - """Return representation of self.""" - return f"{self.__class__.__name__}({self.name!r})" - - @property - def manager(self) -> ComponentManager: - """ComponentManager if bound to one, otherwise raise AttributeError.""" - if self.__manager is not None: - manager = self.__manager() - if manager is not None: - return manager - raise AttributeError(f"No component manager bound for {self.name}") - - def _unbind(self) -> None: - """If you use this you are evil. This is only for ComponentManagers!.""" - self.__manager = None - - @property - def manager_exists(self) -> bool: - """Return if manager is bound or not.""" - return self.__manager is not None and self.__manager() is not None - - def register_handler( - self, - event_name: str, - handler_coro: Callable[[Event[Any]], Awaitable[Any]], - ) -> None: - """Register handler with bound component manager. - - Raises AttributeError if this component is not bound. - """ - self.manager.register_component_handler( - event_name, - handler_coro, - self.name, - ) - - def register_handlers( - self, - handlers: dict[str, Callable[[Event[Any]], Awaitable[Any]]], - ) -> None: - """Register multiple handler Coroutines. - - Raises AttributeError if this component is not bound. - """ - for name, coro in handlers.items(): - self.register_handler(name, coro) - - def bind_handlers(self) -> None: - """Add handlers in subclass.""" - - def bind(self, manager: ComponentManager) -> None: - """Bind self to manager. - - Raises RuntimeError if component is already bound to a manager. - """ - if self.manager_exists: - raise RuntimeError( - f"{self.name} component is already bound to {self.manager}", - ) - self.__manager = ref(manager) - self.bind_handlers() - - def has_handler(self, event_name: str) -> bool: - """Return if manager has event handlers registered for a given event. - - Raises AttributeError if this component is not bound. - """ - return self.manager.has_handler(event_name) - - def unregister_handler( - self, - event_name: str, - handler_coro: Callable[[Event[Any]], Awaitable[None]], - ) -> None: - """Unregister a handler function for event_name. - - Raises ValueError if no component with given name is registered. - """ - return self.manager.unregister_handler( - event_name, - handler_coro, - self.name, - ) - - def unregister_handler_type(self, event_name: str) -> bool: - """Unregister all event handlers for a given event type.""" - return self.manager.unregister_handler_type(event_name) - - async def raise_event(self, event: Event[Any]) -> None: - """Raise event for bound manager. - - Raises AttributeError if this component is not bound. - """ - await self.manager.raise_event(event) - - def component_exists(self, component_name: str) -> bool: - """Return if component exists in manager. - - Raises AttributeError if this component is not bound. - """ - return self.manager.component_exists(component_name) - - def components_exist(self, component_names: Iterable[str]) -> bool: - """Return if all component names given exist in manager. - - Raises AttributeError if this component is not bound. - """ - return self.manager.components_exist(component_names) - - def get_component(self, component_name: str) -> Any: - """Get Component from manager. - - Raises AttributeError if this component is not bound. - """ - return self.manager.get_component(component_name) - - def get_components( - self, - component_names: Iterable[str], - ) -> list[Component]: - """Return Components from manager. - - Raises AttributeError if this component is not bound. - """ - return self.manager.get_components(component_names) - - -ComponentPassthrough = TypeVar("ComponentPassthrough", bound=Component) - - -class ComponentManager(Component): - """Component manager class.""" - - __slots__ = ("__components", "__event_handlers", "__weakref__") - - def __init__(self, name: object, own_name: object | None = None) -> None: - """If own_name is set, add self to list of components as specified name.""" - super().__init__(name) - self.__event_handlers: dict[ - str, - set[tuple[Callable[[Event[Any]], Awaitable[Any]], object]], - ] = {} - self.__components: dict[object, Component] = {} - - if own_name is not None: - self.__add_self_as_component(own_name) - self.bind_handlers() - - def __repr__(self) -> str: - """Return representation of self.""" - return f"<{self.__class__.__name__} Components: {self.__components}>" - - def __add_self_as_component(self, name: object) -> None: - """Add this manager as component to self without binding. - - Raises ValueError if a component with given name already exists. - """ - if self.component_exists(name): # pragma: nocover - raise ValueError(f'Component named "{name}" already exists!') - self.__components[name] = self - - def register_handler( - self, - event_name: str, - handler_coro: Callable[[Event[Any]], Awaitable[None]], - ) -> None: - """Register handler_func as handler for event_name (self component).""" - self.register_component_handler(event_name, handler_coro, self.name) - - def register_component_handler( - self, - event_name: str, - handler_coro: Callable[[Event[Any]], Awaitable[None]], - component_name: object, - ) -> None: - """Register handler_func as handler for event_name. - - Raises ValueError if no component with given name is registered. - """ - if ( - component_name != self.name - and component_name not in self.__components - ): - raise ValueError( - f"Component named {component_name!r} is not registered!", - ) - if event_name not in self.__event_handlers: - self.__event_handlers[event_name] = set() - self.__event_handlers[event_name].add((handler_coro, component_name)) - - def unregister_handler( - self, - event_name: str, - handler_coro: Callable[[Event[Any]], Awaitable[None]], - component_name: object, - ) -> None: - """Unregister a handler function for event_name. - - Raises ValueError if no component with given name is registered. - """ - if ( - component_name != self.name - and component_name not in self.__components - ): - raise ValueError( - f"Component named {component_name!r} is not registered!", - ) - - if event_name not in self.__event_handlers: - return - - handler_tuple = (handler_coro, component_name) - if handler_tuple in self.__event_handlers[event_name]: - self.__event_handlers[event_name].remove(handler_tuple) - - # If the event_name no longer has any handlers, remove it - if not self.__event_handlers[event_name]: - del self.__event_handlers[event_name] - - def unregister_handler_type( - self, - event_name: str, - ) -> None: - """Unregister all event handlers for a given event type.""" - if event_name in self.__event_handlers: - del self.__event_handlers[event_name] - - def has_handler(self, event_name: str) -> bool: - """Return if there are event handlers registered for a given event.""" - return bool(self.__event_handlers.get(event_name)) - - async def raise_event_in_nursery( - self, - event: Event[Any], - nursery: trio.Nursery, - ) -> None: - """Raise event in a particular trio nursery. - - Could raise RuntimeError if given nursery is no longer open. - """ - await trio.lowlevel.checkpoint() - - # Forward leveled events up; They'll come back to us soon enough. - if self.manager_exists and event.pop_level(): - await super().raise_event(event) - return - # Make sure events not raised twice - # if not self.manager_exists: - # while event.level > 0: - # event.pop_level() - - # if not event.name.startswith("Pygame") and event.name not in {"tick", "gameboard_create_piece", "server->create_piece", "create_piece->network"}: - # print(f'''{self.__class__.__name__}({self.name!r}):\n{event = }''') - - # Call all registered handlers for this event - if event.name in self.__event_handlers: - for handler, _name in self.__event_handlers[event.name]: - nursery.start_soon(handler, event) - - # Forward events to contained managers - for component in self.get_all_components(): - # Skip self component if exists - if component is self: - continue - if isinstance(component, ComponentManager): - nursery.start_soon(component.raise_event, event) - - async def raise_event(self, event: Event[Any]) -> None: - """Raise event for all components that have handlers registered.""" - async with trio.open_nursery() as nursery: - await self.raise_event_in_nursery(event, nursery) - - def add_component(self, component: Component) -> None: - """Add component to this manager. - - Raises ValueError if component already exists with component name. - `component` must be an instance of Component. - """ - assert isinstance(component, Component), "Must be component instance" - if self.component_exists(component.name): - raise ValueError( - f'Component named "{component.name}" already exists!', - ) - self.__components[component.name] = component - component.bind(self) - - def add_components(self, components: Iterable[Component]) -> None: - """Add multiple components to this manager. - - Raises ValueError if any component already exists with component name. - `component`s must be instances of Component. - """ - for component in components: - self.add_component(component) - - def remove_component(self, component_name: object) -> None: - """Remove a component. - - Raises ValueError if component name does not exist. - """ - if not self.component_exists(component_name): - raise ValueError(f"Component {component_name!r} does not exist!") - # Remove component from registered components - component = self.__components.pop(component_name) - # Tell component they need to unbind - component._unbind() - - # Unregister component's event handlers - # List of events that will have no handlers once we are done - empty = [] - for event_name, handlers in self.__event_handlers.items(): - for item in tuple(handlers): - _handler, handler_component = item - if handler_component == component_name: - self.__event_handlers[event_name].remove(item) - if not self.__event_handlers[event_name]: - empty.append(event_name) - # Remove event handler table keys that have no items anymore - for name in empty: - self.__event_handlers.pop(name) - - def component_exists(self, component_name: object) -> bool: - """Return if component exists in this manager.""" - return component_name in self.__components - - @contextmanager - def temporary_component( - self, - component: ComponentPassthrough, - ) -> Generator[ComponentPassthrough, None, None]: - """Temporarily add given component but then remove after exit.""" - name = component.name - self.add_component(component) - try: - yield component - finally: - if self.component_exists(name): - self.remove_component(name) - - def components_exist(self, component_names: Iterable[object]) -> bool: - """Return if all component names given exist in this manager.""" - return all(self.component_exists(name) for name in component_names) - - def get_component(self, component_name: object) -> Any: - """Return Component or raise ValueError because it doesn't exist.""" - if not self.component_exists(component_name): - raise ValueError(f'"{component_name}" component does not exist') - return self.__components[component_name] - - def get_components(self, component_names: Iterable[object]) -> list[Any]: - """Return iterable of components asked for or raise ValueError.""" - return [self.get_component(name) for name in component_names] - - def list_components(self) -> tuple[object, ...]: - """Return tuple of the names of components bound to this manager.""" - return tuple(self.__components) - - def get_all_components(self) -> tuple[Component, ...]: - """Return tuple of all components bound to this manager.""" - return tuple(self.__components.values()) - - def unbind_components(self) -> None: - """Unbind all components, allows things to get garbage collected.""" - self.__event_handlers.clear() - for component in iter(self.__components.values()): - if ( - isinstance(component, ComponentManager) - and component is not self - ): - component.unbind_components() - component._unbind() - self.__components.clear() - - def __del__(self) -> None: - """Unbind components.""" - self.unbind_components() - - -class ExternalRaiseManager(ComponentManager): - """Component Manager, but raises events in an external nursery.""" - - __slots__ = ("nursery",) - - def __init__( - self, - name: object, - nursery: trio.Nursery, - own_name: object | None = None, - ) -> None: - """Initialize with name, own component name, and nursery.""" - super().__init__(name, own_name) - self.nursery = nursery - - async def raise_event(self, event: Event[Any]) -> None: - """Raise event in nursery. - - Could raise RuntimeError if self.nursery is no longer open. - """ - await self.raise_event_in_nursery(event, self.nursery) - - async def raise_event_internal(self, event: Event[Any]) -> None: - """Raise event in internal nursery.""" - await super().raise_event(event) - - -if __name__ == "__main__": # pragma: nocover - print(f"{__title__}\nProgrammed by {__author__}.") diff --git a/src/azul/encrypted_event.py b/src/azul/encrypted_event.py deleted file mode 100644 index a34b046..0000000 --- a/src/azul/encrypted_event.py +++ /dev/null @@ -1,130 +0,0 @@ -"""Encrypted Event - Encrypt and decrypt event data.""" - -# Programmed by CoolCat467 - -from __future__ import annotations - -# Encrypted Event - Encrypt and decrypt event data. -# Copyright (C) 2024 CoolCat467 -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -__title__ = "Encrypted Event" -__author__ = "CoolCat467, ItsDrike, and Ammar Askar" -__version__ = "0.0.0" -__license__ = "GNU General Public License Version 3" - - -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.ciphers import ( - Cipher, - CipherContext, - algorithms, - modes, -) - -from azul.network import NetworkEventComponent - - -class EncryptedNetworkEventComponent(NetworkEventComponent): - """Encrypted Network Event Component.""" - - __slots__ = ( - "cipher", - "decryptor", - "encryptor", - "shared_secret", - ) - - def __init__(self, name: str) -> None: - """Initialize Encrypted Network Event Component.""" - super().__init__(name) - - self.cipher: Cipher[modes.CFB8] | None = None - self.encryptor: CipherContext - self.decryptor: CipherContext - - @property - def encryption_enabled(self) -> bool: - """Return if encryption is enabled.""" - return self.cipher is not None - - def enable_encryption( - self, - shared_secret: bytes, - initialization_vector: bytes, - ) -> None: - """Enable encryption for this connection, using the ``shared_secret``. - - After calling this method, the reading and writing process for this connection - will be altered, and any future communication will be encrypted/decrypted there. - - :param shared_secret: - This is the cipher key for the AES symmetric cipher used for the encryption. - - See :func:`azul.encryption.generate_shared_secret`. - """ - self.cipher = Cipher( - algorithms.AES256(bytes(shared_secret)), - modes.CFB8(bytes(initialization_vector)), - backend=default_backend(), - ) - self.encryptor = self.cipher.encryptor() - self.decryptor = self.cipher.decryptor() - - async def write(self, data: bytes) -> None: - """Send the given data, encrypted through the stream, blocking if necessary. - - Args: - data (bytes, bytearray, or memoryview): The data to send. - - Raises: - trio.BusyResourceError: if another task is already executing a - :meth:`send_all`, :meth:`wait_send_all_might_not_block`, or - :meth:`HalfCloseableStream.send_eof` on this stream. - trio.BrokenResourceError: if something has gone wrong, and the stream - is broken. - trio.ClosedResourceError: if you previously closed this stream - object, or if another task closes this stream object while - :meth:`send_all` is running. - - Most low-level operations in Trio provide a guarantee: if they raise - :exc:`trio.Cancelled`, this means that they had no effect, so the - system remains in a known state. This is **not true** for - :meth:`send_all`. If this operation raises :exc:`trio.Cancelled` (or - any other exception for that matter), then it may have sent some, all, - or none of the requested data, and there is no way to know which. - - Copied from Trio docs. - - """ - if self.encryption_enabled: - data = self.encryptor.update(data) - return await super().write(data) - - async def read(self, length: int) -> bytearray: - """Read `length` bytes from stream. - - Can raise following exceptions: - NetworkStreamNotConnectedError - NetworkTimeoutError - Timeout or no data - OSError - Stopped responding - trio.BusyResourceError - Another task is already writing data - trio.BrokenResourceError - Something is wrong and stream is broken - trio.ClosedResourceError - Stream is closed or another task closes stream - """ - data = await super().read(length) - if self.encryption_enabled: - return bytearray(self.decryptor.update(data)) - return data diff --git a/src/azul/encryption.py b/src/azul/encryption.py deleted file mode 100644 index e2ad0d2..0000000 --- a/src/azul/encryption.py +++ /dev/null @@ -1,140 +0,0 @@ -"""Encryption module.""" - -# This is the buffer module from https://github.com/py-mine/mcproto v0.5.0, -# which is licensed under the GNU LESSER GENERAL PUBLIC LICENSE v3.0 - -from __future__ import annotations - -__author__ = "ItsDrike" -__license__ = "LGPL-3.0-only" - -import os - -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.asymmetric.padding import MGF1, OAEP -from cryptography.hazmat.primitives.asymmetric.rsa import ( - RSAPrivateKey as RSAPrivateKey, - RSAPublicKey as RSAPublicKey, - generate_private_key, -) -from cryptography.hazmat.primitives.hashes import SHA256 -from cryptography.hazmat.primitives.serialization import ( - Encoding, - PublicFormat, - load_der_public_key, -) - - -def generate_shared_secret() -> bytes: # pragma: no cover - """Generate a random shared secret for client. - - This secret will be sent to the server in :class:`~mcproto.packets.login.login.LoginEncryptionResponse` packet, - and used to encrypt all future communication afterwards. - - This will be symmetric encryption using AES/CFB8 stream cipher. And this shared secret will be 256-bits long. - """ - return os.urandom(256 // 8) - - -def generate_verify_token() -> bytes: # pragma: no cover - """Generate a random verify token. - - This token will be sent by the server in :class:`~mcproto.packets.login.login.LoginEncryptionRequest`, to be - encrypted by the client as a form of verification. - - This token doesn't need to be cryptographically secure, it's just a sanity check that - the client has encrypted the data correctly. - """ - return os.urandom(16) - - -def generate_rsa_key() -> RSAPrivateKey: # pragma: no cover - """Generate a random RSA key pair for server. - - This key pair will be used for :class:`~mcproto.packets.login.login.LoginEncryptionRequest` packet, - where the client will be sent the public part of this key pair, which will be used to encrypt the - shared secret (and verification token) sent in :class:`~mcproto.packets.login.login.LoginEncryptionResponse` - packet. The server will then use the private part of this key pair to decrypt that. - - This will be a 2048-bit RSA key pair. - """ - return generate_private_key( - public_exponent=65537, - key_size=2048, - backend=default_backend(), - ) - - -def encrypt_with_rsa( - public_key: RSAPublicKey, - data: bytes, -) -> bytes: - """Encrypt given data with given RSA public key.""" - return public_key.encrypt( - bytes(data), - OAEP(MGF1(SHA256()), SHA256(), None), - ) - - -def encrypt_token_and_secret( - public_key: RSAPublicKey, - verification_token: bytes, - shared_secret: bytes, -) -> tuple[bytes, bytes]: - """Encrypts the verification token and shared secret with the server's public key. - - :param public_key: The RSA public key provided by the server - :param verification_token: The verification token provided by the server - :param shared_secret: The generated shared secret - :return: A tuple containing (encrypted token, encrypted secret) - """ - encrypted_token = encrypt_with_rsa(public_key, verification_token) - encrypted_secret = encrypt_with_rsa(public_key, shared_secret) - return encrypted_token, encrypted_secret - - -def decrypt_with_rsa( - private_key: RSAPrivateKey, - data: bytes, -) -> bytes: - """Decrypt given data with given RSA private key.""" - return private_key.decrypt( - bytes(data), - OAEP(MGF1(SHA256()), SHA256(), None), - ) - - -def decrypt_token_and_secret( - private_key: RSAPrivateKey, - verification_token: bytes, - shared_secret: bytes, -) -> tuple[bytes, bytes]: - """Decrypts the verification token and shared secret with the server's private key. - - :param private_key: The RSA private key generated by the server - :param verification_token: The verification token encrypted and sent by the client - :param shared_secret: The shared secret encrypted and sent by the client - :return: A tuple containing (decrypted token, decrypted secret) - """ - decrypted_token = decrypt_with_rsa(private_key, verification_token) - decrypted_secret = decrypt_with_rsa(private_key, shared_secret) - return decrypted_token, decrypted_secret - - -def serialize_public_key( - public_key: RSAPublicKey, -) -> bytes: - """Return public key serialize as bytes.""" - return public_key.public_bytes( - encoding=Encoding.DER, - format=PublicFormat.SubjectPublicKeyInfo, - ) - - -def deserialize_public_key(serialized_public_key: bytes) -> RSAPublicKey: - """Return deserialized public key.""" - # Key type is determined by the passed key itself. - # Should be be an RSA public key in this case. - key = load_der_public_key(serialized_public_key, default_backend()) - assert isinstance(key, RSAPublicKey) - return key diff --git a/src/azul/game.py b/src/azul/game.py index 4b9708f..db25d88 100644 --- a/src/azul/game.py +++ b/src/azul/game.py @@ -35,10 +35,16 @@ from collections import Counter from functools import lru_cache, wraps from pathlib import Path -from typing import TYPE_CHECKING, Final, TypeVar +from typing import TYPE_CHECKING, Any, Final, TypeVar import pygame import trio +from libcomponent.component import ( + ComponentManager, + Event, + ExternalRaiseManager, +) +from libcomponent.network_utils import find_ip from numpy import array, int8 from pygame.color import Color from pygame.locals import ( @@ -55,12 +61,7 @@ from azul import element_list, objects, sprite from azul.async_clock import Clock from azul.client import GameClient, read_advertisements -from azul.component import ( - ComponentManager, - Event, - ExternalRaiseManager, -) -from azul.network_shared import DEFAULT_PORT, find_ip +from azul.network_shared import DEFAULT_PORT from azul.server import GameServer from azul.sound import SoundData, play_sound as base_play_sound from azul.state import Tile @@ -71,6 +72,9 @@ ) from azul.vector import Vector2 +if sys.version_info < (3, 11): + from exceptiongroup import ExceptionGroup + if TYPE_CHECKING: from collections.abc import ( Awaitable, @@ -395,21 +399,21 @@ class ObjectHandler: def __init__(self) -> None: """Initialize object handler.""" - self.objects: dict[int, Object] = {} + self.objects: dict[int, sprite.Sprite] = {} self.next_id = 0 self.cache: dict[str, int] = {} self.recalculate_render = True self._render_order: tuple[int, ...] = () - def add_object(self, obj: Object) -> None: + def add_object(self, obj: sprite.Sprite) -> None: """Add an object to the game.""" obj.id = self.next_id self.objects[self.next_id] = obj self.next_id += 1 self.recalculate_render = True - def rm_object(self, obj: Object) -> None: + def rm_object(self, obj: sprite.Sprite) -> None: """Remove an object from the game.""" del self.objects[obj.id] self.recalculate_render = True @@ -420,7 +424,7 @@ def rm_star(self) -> None: self.rm_object(self.objects[oid]) self.next_id = 0 - def get_object(self, object_id: int) -> Object | None: + def get_object(self, object_id: int) -> sprite.Sprite | None: """Return the object associated with object id given. Return None if object not found.""" if object_id in self.objects: return self.objects[object_id] @@ -454,17 +458,17 @@ def reset_cache(self) -> None: """Reset the cache.""" self.cache = {} - def get_object_by_name(self, object_name: str) -> Object: + def get_object_by_name(self, object_name: str) -> sprite.Sprite: """Get object by name, with cache.""" if object_name not in self.cache: ids = self.get_object_given_name(object_name) if ids: self.cache[object_name] = min(ids) else: - raise RuntimeError(f"{object_name} Object Not Found!") + raise RuntimeError(f"{object_name} sprite.Sprite Not Found!") result = self.get_object(self.cache[object_name]) if result is None: - raise RuntimeError(f"{object_name} Object Not Found!") + raise RuntimeError(f"{object_name} sprite.Sprite Not Found!") return result def set_attr_all(self, attribute: str, value: object) -> None: @@ -550,10 +554,10 @@ def __del__(self) -> None: class MultipartObject(ObjectHandler): - """Thing that is both an Object and an ObjectHandler, and is meant to be an Object made up of multiple Objects.""" + """Thing that is both an sprite.Sprite and an ObjectHandler, and is meant to be an sprite.Sprite made up of multiple Objects.""" def __init__(self, name: str): - """Initialize Object and ObjectHandler of self. + """Initialize sprite.Sprite and ObjectHandler of self. Also set self._lastloc and self._lasthidden to None """ @@ -584,8 +588,8 @@ def get_intersection( return None, None def process(self, time_passed: float) -> None: - """Process Object self and ObjectHandler self and call self.reset_position on location change.""" - Object.process(self, time_passed) + """Process sprite.Sprite self and ObjectHandler self and call self.reset_position on location change.""" + sprite.Sprite.process(self, time_passed) ObjectHandler.process_objects(self, time_passed) if self.location != self._lastloc: @@ -598,12 +602,12 @@ def process(self, time_passed: float) -> None: def render(self, surface: pygame.surface.Surface) -> None: """Render self and all parts to the surface.""" - Object.render(self, surface) + sprite.Sprite.render(self, surface) ObjectHandler.render_objects(self, surface) def __del__(self) -> None: """Delete data.""" - Object.__del__(self) + sprite.Sprite.__del__(self) ObjectHandler.__del__(self) @@ -760,7 +764,7 @@ def update_image(self) -> None: self.dirty = 1 self.visible = bool(tile_count) - def bind_handlers(self): + def bind_handlers(self) -> None: """Register handlers.""" self.register_handlers( { @@ -1271,7 +1275,7 @@ def get_tile(self, replace: int = Tile.blank) -> int: return self.tiles.pop() def place_tile(self, tile: int) -> None: - """Place a given int Object on self if permitted.""" + """Place a given int sprite.Sprite on self if permitted.""" if self.can_place(tile): self.color = tile.color self.tiles.append(tile) @@ -1369,7 +1373,7 @@ def reset_position(self) -> None: w = self.get_row(last - 1).width_height[0] if w is None: raise RuntimeError( - "Image Dimensions for Row Object (row.width_height) are None!", + "Image Dimensions for Row sprite.Sprite (row.width_height) are None!", ) h1 = self.get_row(0).tile_full h = int(last * h1) @@ -1424,7 +1428,7 @@ def __init__(self, player: Player) -> None: self.name = "floor_line" # self.font = Font(FONT, round(self.tile_size*1.2), color=BLACK, cx=False, cy=False) - self.text = Text( + self.text = objects.Text( round(self.tile_size * 1.2), BLACK, cx=False, @@ -1432,8 +1436,7 @@ def __init__(self, player: Player) -> None: ) self.has_number_one_tile = False - gen = floor_line_subtract_generator(1) - self.numbers = [next(gen) for i in range(self.size)] + self.numbers = [-255 for _ in range(self.size)] def __repr__(self) -> str: """Return representation of self.""" @@ -1459,28 +1462,9 @@ def render(self, surface: pygame.surface.Surface) -> None: # self.font.render(surface, str(self.numbers[x]), xy) def place_tile(self, tile: int) -> None: - """Place a given int Object on self if permitted.""" + """Place a given int sprite.Sprite on self if permitted.""" self.tiles.insert(self.get_placed(), tile) - if tile.color == self.number_one_color: - self.has_number_one_tile = True - - box_lid = self.player.game.get_object_by_name("BoxLid") - assert isinstance(box_lid, BoxLid) - - def handle_end(end: int) -> None: - """Handle the end tile we are replacing. Ensures number one tile is not removed.""" - if not end.color < 0: - if end.color == self.number_one_color: - handle_end(self.tiles.pop()) - self.tiles.appendleft(end) - return - box_lid.add_tile(end) - - handle_end(self.tiles.pop()) - - self.image_update = True - def score_tiles(self) -> int: """Score self.tiles and return how to change points.""" running_total = 0 @@ -1615,7 +1599,7 @@ def process(self, time_passed: float) -> None: class Factories(MultipartObject): - """Factories Multipart Object, made of multiple Factory Objects.""" + """Factories Multipart sprite.Sprite, made of multiple Factory Objects.""" tiles_each = 4 @@ -1693,43 +1677,6 @@ def process(self, time_passed: float) -> None: table.add_tiles(tocenter) cursor.drag(select) - def play_tiles_from_bag(self, empty_color: int = Tile.blank) -> None: - """Divy up tiles to each factory from the bag.""" - # For every factory we have, - for fid in range(self.count): - # Draw tiles for the factory - drawn = [] - for _i in range(self.tiles_each): - # If the bag is not empty, - if not self.game.bag.is_empty(): - # Draw a tile from the bag. - tile = self.game.bag.draw_tile() - assert tile is not None - drawn.append(tile) - else: # Otherwise, get the box lid - box_lid = self.game.get_object_by_name("BoxLid") - assert isinstance(box_lid, BoxLid) - # If the box lid is not empty, - if not box_lid.is_empty(): - # Add all the tiles from the box lid to the bag - self.game.bag.add_tiles(box_lid.get_tiles()) - # and shake the bag to randomize everything - self.game.bag.reset() - # Then, grab a tile from the bag like usual. - tile = self.game.bag.draw_tile() - assert tile is not None - drawn.append(tile) - else: - # "In the rare case that you run out of tiles again - # while there are none left in the lid, start a new - # round as usual even though are not all factory - # displays are properly filled." - drawn.append(int(empty_color)) - # Place drawn tiles on factory - factory = self.objects[fid] - assert isinstance(factory, Factory) - factory.fill(drawn) - def is_all_empty(self) -> bool: """Return True if all factories are empty.""" for fid in range(self.count): @@ -1741,7 +1688,7 @@ def is_all_empty(self) -> bool: class TableCenter(TileRenderer): - """Object that represents the center of the table.""" + """sprite.Sprite that represents the center of the table.""" __slots__ = ("tiles",) size = (6, 6) @@ -1777,7 +1724,7 @@ def iter_tiles(self) -> Generator[int, None, None]: for _ in range(one_count): yield Tile.one - def update_image(self): + def update_image(self) -> None: """Reset/update image.""" self.clear_image(self.size) @@ -1852,7 +1799,7 @@ def __init__( self.add_object(Board(self.varient_play)) self.add_object(PatternLine(self)) self.add_object(FloorLine(self)) - ## self.add_object(objects.Text(SCOREFONTSIZE, SCORECOLOR)) + ## self.add_object(objects.objects.Text(SCOREFONTSIZE, SCORECOLOR)) self.score = 0 self.is_turn = False @@ -1875,8 +1822,8 @@ def __repr__(self) -> str: ## def update_score(self) -> None: ## """Update the scorebox for this player.""" - ## score_box = self.get_object_by_name("Text") - ## assert isinstance(score_box, Text) + ## score_box = self.get_object_by_name("objects.Text") + ## assert isinstance(score_box, objects.Text) ## score_box.update_value(f"Player {self.player_id + 1}: {self.score}") def trigger_turn_now(self) -> None: @@ -1939,8 +1886,8 @@ def reset_position(self) -> None: int(y + bh * (2 / 3)), ) - text = self.get_object_by_name("Text") - assert isinstance(text, Text) + text = self.get_object_by_name("objects.Text") + assert isinstance(text, objects.Text) text.location = Vector2(x - (bw // 3), y - (bh * 2 // 3)) @@ -2218,7 +2165,7 @@ async def set_state(*args: object, **kwargs: object) -> None: class KwargOutlineText(objects.OutlinedText): - """Outlined Text with attributes settable via keyword arguments.""" + """Outlined objects.Text with attributes settable via keyword arguments.""" __slots__ = () @@ -2236,7 +2183,7 @@ def __init__( class KwargButton(objects.Button): - """Button with attributes settable via keyword arguments.""" + """objects.Button with attributes settable via keyword arguments.""" __slots__ = () @@ -2272,7 +2219,7 @@ def add_button( size: int = fontsize, minlen: int = button_minimum, ) -> int: - """Add a new Button object to group.""" + """Add a new objects.Button object to group.""" button = KwargButton( name, font=pygame.font.Font(FONT, size), @@ -2293,7 +2240,7 @@ def add_text( size: int = fontsize, outline: tuple[int, int, int] = BUTTON_TEXT_OUTLINE, ) -> int: - """Add a new Text object to self.game with arguments. Return text id.""" + """Add a new objects.Text object to self.game with arguments. Return text id.""" text = KwargOutlineText( name, font=pygame.font.Font(FONT, size), @@ -2369,8 +2316,8 @@ def update_text( def updater() -> None: """Update text object {text_name}'s value with {value_function}.""" assert self.game is not None - text = self.game.get_object_by_name(f"Text{text_name}") - assert isinstance(text, Text) + text = self.game.get_object_by_name(f"objects.Text{text_name}") + assert isinstance(text, objects.Text) text.update_value(value_function()) return updater @@ -2441,6 +2388,7 @@ async def mouse_moved( self, event: Event[sprite.PygameMouseMotion], ) -> None: + """Handle PygameMouseMotion event.""" ## print(f'{event = }') await self.manager.raise_event( Event("cursor_set_location", event.data["pos"]), @@ -2950,7 +2898,7 @@ def entry_actions(self) -> None: (SCREEN_SIZE[0] // 2, SCREEN_SIZE[1] * 4 // 5), ) buttontitle = self.game.get_object(bid) - assert isinstance(buttontitle, Button) + assert isinstance(buttontitle, objects.Button) buttontitle.Render_Priority = "last-1" buttontitle.cur_time = 2 @@ -2961,7 +2909,7 @@ def entry_actions(self) -> None: self.add_text(f"Line{idx}", line, (x, y), cx=True, cy=False) # self.game.get_object(bid).Render_Priority = f'last{-(2+idx)}' button = self.game.get_object(bid) - assert isinstance(button, Button) + assert isinstance(button, objects.Button) button.Render_Priority = "last-2" y += self.bh @@ -2974,8 +2922,6 @@ class Game(ObjectHandler): def __init__(self) -> None: """Initialize game.""" super().__init__() - # Gets overwritten by Keyboard object - self.keyboard: Keyboard | None = None self.states: dict[str, GameState] = {} self.active_state: GameState | None = None @@ -3006,9 +2952,6 @@ def __init__(self) -> None: self.player_turn: int = 0 - # Tiles - self.bag = Bag(TILECOUNT, REGTILECOUNT) - # # Cache # self.cache: dict[int, pygame.surface.Surface] = {} @@ -3016,7 +2959,7 @@ def __repr__(self) -> str: """Return representation of self.""" return f"{self.__class__.__name__}()" - def add_object(self, obj: Object) -> None: + def add_object(self, obj: sprite.Sprite) -> None: """Add an object to the game.""" obj.game = self super().add_object(obj) @@ -3066,7 +3009,6 @@ def start_game( self.add_object(Cursor(self)) self.add_object(TableCenter(self)) - self.add_object(BoxLid(self)) if self.is_host: self.bag.reset() diff --git a/src/azul/keyboard.py b/src/azul/keyboard.py index daeda5b..a305365 100644 --- a/src/azul/keyboard.py +++ b/src/azul/keyboard.py @@ -9,7 +9,7 @@ __version__ = "0.0.0" -from azul.component import ComponentManager +from libcomponent.component import ComponentManager class Keyboard(ComponentManager): diff --git a/src/azul/mr_floppy_test.py b/src/azul/mr_floppy_test.py index 03815e5..1b8a772 100644 --- a/src/azul/mr_floppy_test.py +++ b/src/azul/mr_floppy_test.py @@ -13,11 +13,11 @@ from typing import TYPE_CHECKING, Any, Final import trio +from libcomponent.component import Component, ComponentManager, Event from pygame.locals import K_ESCAPE, KEYUP, QUIT, RESIZABLE, WINDOWRESIZED from pygame.rect import Rect from azul import conf, lang, objects, sprite -from azul.component import Component, ComponentManager, Event from azul.statemachine import AsyncState, AsyncStateMachine from azul.vector import Vector2 diff --git a/src/azul/network.py b/src/azul/network.py deleted file mode 100644 index fba2fb2..0000000 --- a/src/azul/network.py +++ /dev/null @@ -1,512 +0,0 @@ -"""Network - Module for sending events over the network.""" - -# Programmed by CoolCat467 - -from __future__ import annotations - -# Copyright (C) 2023-2024 CoolCat467 -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -__title__ = "Network" -__author__ = "CoolCat467" -__license__ = "GNU General Public License Version 3" -__version__ = "0.0.0" - - -import contextlib -from typing import ( - TYPE_CHECKING, - Any, - Literal, - NoReturn, -) - -import trio - -from azul.base_io import ( - BaseAsyncReader, - BaseAsyncWriter, - StructFormat, -) -from azul.buffer import Buffer -from azul.component import ( - Component, - ComponentManager, - Event, -) - -if TYPE_CHECKING: - from types import TracebackType - - from typing_extensions import Self - - -class NetworkTimeoutError(Exception): - """Network Timeout Error.""" - - __slots__ = () - - -class NetworkEOFError(Exception): - """Network End of File Error.""" - - __slots__ = () - - -class NetworkStreamNotConnectedError(Exception): - """Network Stream Not Connected Error.""" - - __slots__ = () - - -class NetworkComponent(Component, BaseAsyncReader, BaseAsyncWriter): - """Network Component (client).""" - - __slots__ = ("_stream", "timeout") - - def __init__(self, name: str) -> None: - """Initialize Network Component.""" - super().__init__(name) - - self.timeout: int | float = 3 - self._stream: trio.SocketStream | None = None - - @property - def not_connected(self) -> bool: - """Is stream None?.""" - return self._stream is None - - @property - def stream(self) -> trio.SocketStream: - """Trio SocketStream or raise NetworkStreamNotConnectedError.""" - if self._stream is None: - raise NetworkStreamNotConnectedError("Stream not connected!") - return self._stream - - @classmethod - def from_stream( - cls, - *args: object, - kwargs: dict[str, object] | None = None, - stream: trio.SocketStream, - ) -> Self: - """Initialize from stream.""" - if kwargs is None: - kwargs = {} - self = cls(*args, **kwargs) # type: ignore[arg-type] - self._stream = stream - return self - - async def connect(self, host: str, port: int) -> None: - """Connect to host:port on TCP. - - Raises: - OSError: if the connection fails. - RuntimeError: if stream is already connected - - """ - if not self.not_connected: - raise RuntimeError("Already connected!") - try: # pragma: nocover - self._stream = await trio.open_tcp_stream(host, port) - except OSError: # pragma: nocover - await self.close() - raise - - async def read(self, length: int) -> bytearray: - """Read `length` bytes from stream. - - Can raise following exceptions: - NetworkStreamNotConnectedError - Network stream is not connected - NetworkTimeoutError - Timeout - NetworkEOFError - End of File - OSError - Stopped responding - trio.BusyResourceError - Another task is already writing data - trio.BrokenResourceError - Something is wrong and stream is broken - trio.ClosedResourceError - Stream is closed or another task closes stream - """ - content = bytearray() - while max_read_count := length - len(content): - received = b"" - # try: - with trio.move_on_after(self.timeout) as cancel_scope: - received = await self.stream.receive_some(max_read_count) - cancel_called = cancel_scope.cancel_called - # except (trio.BrokenResourceError, trio.ClosedResourceError): - # await self.close() - # raise - if len(received) == 0: - # No information at all - if len(content) == 0: - if cancel_called: - raise NetworkTimeoutError("Read timed out.") - raise NetworkEOFError( - "Server did not respond with any information.", - ) - # Only sent a few bytes, but we requested more - raise OSError( - f"Server stopped responding (got {len(content)} bytes, " - f"but expected {length} bytes)." - f" Partial obtained packet: {content!r}", - ) - content.extend(received) - return content - - async def write(self, data: bytes | bytearray | memoryview) -> None: - """Send the given data through the stream, blocking if necessary. - - Args: - data (bytes, bytearray, or memoryview): The data to send. - - Raises: - trio.BusyResourceError: if another task is already executing a - :meth:`send_all`, :meth:`wait_send_all_might_not_block`, or - :meth:`HalfCloseableStream.send_eof` on this stream. - trio.BrokenResourceError: if something has gone wrong, and the stream - is broken. - trio.ClosedResourceError: if you previously closed this stream - object, or if another task closes this stream object while - :meth:`send_all` is running. - - Most low-level operations in Trio provide a guarantee: if they raise - :exc:`trio.Cancelled`, this means that they had no effect, so the - system remains in a known state. This is **not true** for - :meth:`send_all`. If this operation raises :exc:`trio.Cancelled` (or - any other exception for that matter), then it may have sent some, all, - or none of the requested data, and there is no way to know which. - - Copied from Trio docs. - - """ - await self.stream.send_all(data) - - # try: - # await self.stream.send_all(data) - # except (trio.BrokenResourceError, trio.ClosedResourceError): - # await self.close() - # raise - - async def close(self) -> None: - """Close the stream, possibly blocking.""" - if self._stream is None: - await trio.lowlevel.checkpoint() - return - await self._stream.aclose() - self._stream = None - - async def send_eof(self) -> None: - """Close the sending half of the stream. - - This corresponds to ``shutdown(..., SHUT_WR)`` (`man - page `__). - - If an EOF has already been sent, then this method should silently - succeed. - - Raises: - trio.BusyResourceError: if another task is already executing a - :meth:`~SendStream.send_all`, - :meth:`~SendStream.wait_send_all_might_not_block`, or - :meth:`send_eof` on this stream. - trio.BrokenResourceError: if something has gone wrong, and the stream - is broken. - - Suppresses: - trio.ClosedResourceError: if you previously closed this stream - object, or if another task closes this stream object while - :meth:`send_eof` is running. - - Copied from trio docs. - - """ - with contextlib.suppress(trio.ClosedResourceError): - await self.stream.send_eof() - - async def wait_write_might_not_block(self) -> None: - """Block until it's possible that :meth:`write` might not block. - - This method may return early: it's possible that after it returns, - :meth:`send_all` will still block. (In the worst case, if no better - implementation is available, then it might always return immediately - without blocking. It's nice to do better than that when possible, - though.) - - This method **must not** return *late*: if it's possible for - :meth:`send_all` to complete without blocking, then it must - return. When implementing it, err on the side of returning early. - - Raises: - trio.BusyResourceError: if another task is already executing a - :meth:`send_all`, :meth:`wait_send_all_might_not_block`, or - :meth:`HalfCloseableStream.send_eof` on this stream. - trio.BrokenResourceError: if something has gone wrong, and the stream - is broken. - trio.ClosedResourceError: if you previously closed this stream - object, or if another task closes this stream object while - :meth:`wait_send_all_might_not_block` is running. - - Note: - This method is intended to aid in implementing protocols that want - to delay choosing which data to send until the last moment. E.g., - suppose you're working on an implementation of a remote display server - like `VNC - `__, and - the network connection is currently backed up so that if you call - :meth:`send_all` now then it will sit for 0.5 seconds before actually - sending anything. In this case it doesn't make sense to take a - screenshot, then wait 0.5 seconds, and then send it, because the - screen will keep changing while you wait; it's better to wait 0.5 - seconds, then take the screenshot, and then send it, because this - way the data you deliver will be more - up-to-date. Using :meth:`wait_send_all_might_not_block` makes it - possible to implement the better strategy. - - If you use this method, you might also want to read up on - ``TCP_NOTSENT_LOWAT``. - - Further reading: - - * `Prioritization Only Works When There's Pending Data to Prioritize - `__ - - * WWDC 2015: Your App and Next Generation Networks: `slides - `__, - `video and transcript - `__ - - Copied from Trio docs. - - """ - return await self.stream.wait_send_all_might_not_block() - - async def __aenter__(self) -> Self: - """Async context manager enter.""" - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - """Async context manager exit. Close connection.""" - await self.close() - - -# async def send_eof_and_close(self) -> None: -# """Send EOF and close.""" -# await self.send_eof() -# await self.close() - - -class NetworkEventComponent(NetworkComponent): - """Network Event Component - Send events over the network.""" - - __slots__ = ( - "_read_packet_id_to_event_name", - "_write_event_name_to_packet_id", - "read_lock", - "write_lock", - ) - - # Max of 255 packet ids - # Next higher is USHORT with 65535 packet ids - packet_id_format: Literal[StructFormat.UBYTE] = StructFormat.UBYTE - - def __init__(self, name: str) -> None: - """Initialize Network Event Component.""" - super().__init__(name) - self._read_packet_id_to_event_name: dict[int, str] = {} - self._write_event_name_to_packet_id: dict[str, int] = {} - self.read_lock = trio.Lock() - self.write_lock = trio.Lock() - - def bind_handlers(self) -> None: - """Register serverbound event handlers.""" - self.register_handlers( - dict.fromkeys( - self._write_event_name_to_packet_id, - self.write_event, - ), - ) - - def register_network_write_event( - self, - event_name: str, - packet_id: int, - ) -> None: - """Map event name to serverbound packet id. - - Raises: - ValueError: Event name already registered or infinite network loop. - - """ - if event_name in self._write_event_name_to_packet_id: - raise ValueError(f"{event_name!r} event already registered!") - if self._read_packet_id_to_event_name.get(packet_id) == event_name: - raise ValueError( - f"{event_name!r} events are also being received " - f"from server with packet id {packet_id!r}, " - "which will would lead to infinite looping over network", - ) - self._write_event_name_to_packet_id[event_name] = packet_id - if self.manager_exists: - self.register_handler(event_name, self.write_event) - - def register_network_write_events(self, event_map: dict[str, int]) -> None: - """Map event names to serverbound packet ids.""" - for event_name, packet_id in event_map.items(): - self.register_network_write_event(event_name, packet_id) - - async def write_event(self, event: Event[bytearray]) -> None: - """Send event to network. - - Raises: - RuntimeError: if unregistered packet id received from network - trio.BusyResourceError: if another task is already executing a - :meth:`send_all`, :meth:`wait_send_all_might_not_block`, or - :meth:`HalfCloseableStream.send_eof` on this stream. - trio.BrokenResourceError: if something has gone wrong, and the stream - is broken. - trio.ClosedResourceError: if you previously closed this stream - object, or if another task closes this stream object while - :meth:`send_all` is running. - - """ - packet_id = self._write_event_name_to_packet_id.get(event.name) - if packet_id is None: - raise RuntimeError(f"Unhandled network event name {event.name!r}") - buffer = Buffer() - buffer.write_value(self.packet_id_format, packet_id) - buffer.write_bytearray(event.data) - async with self.write_lock: - await self.write(buffer) - - async def read_event(self) -> Event[bytearray]: - """Receive event from network. - - Can raise following exceptions: - RuntimeError - Unhandled packet id - NetworkStreamNotConnectedError - Network stream is not connected - NetworkTimeoutError - Timeout or no data - OSError - Stopped responding - trio.BrokenResourceError - Something is wrong and stream is broken - trio.ClosedResourceError - Stream is closed or another task closes stream - - Shouldn't happen with write lock but still: - trio.BusyResourceError - Another task is already writing data - """ - async with self.read_lock: - packet_id = await self.read_value(self.packet_id_format) - event_data = await self.read_bytearray() - event_name = self._read_packet_id_to_event_name.get(packet_id) - if event_name is None: - raise RuntimeError(f"Unhandled packet ID {packet_id!r}") - return Event(event_name, event_data) - - def register_read_network_event( - self, - packet_id: int, - event_name: str, - ) -> None: - """Map clientbound packet id to event name.""" - if packet_id in self._read_packet_id_to_event_name: - raise ValueError(f"Packet ID {packet_id!r} already registered!") - if self._write_event_name_to_packet_id.get(event_name) == packet_id: - raise ValueError( - f"Packet id {packet_id!r} packets are also being received " - f"from server with as {event_name!r} events, " - "which will would lead to infinite looping over network", - ) - self._read_packet_id_to_event_name[packet_id] = event_name - - def register_read_network_events(self, packet_map: dict[int, str]) -> None: - """Map clientbound packet ids to event names.""" - for packet_id, event_name in packet_map.items(): - self.register_read_network_event(packet_id, event_name) - - -class Server(ComponentManager): - """Asynchronous TCP Server.""" - - __slots__ = ("serve_cancel_scope",) - - def __init__(self, name: str, own_name: str | None = None) -> None: - """Initialize Server.""" - super().__init__(name, own_name) - self.serve_cancel_scope: trio.CancelScope | None = None - - def stop_serving(self) -> None: - """Cancel serve scope immediately. - - This method is idempotent, i.e., if the scope was already - cancelled then this method silently does nothing. - """ - if self.serve_cancel_scope is None: - return - self.serve_cancel_scope.cancel() - - # "Implicit return in function which does not return" - async def serve( # type: ignore[misc] # pragma: nocover - self, - port: int, - host: str | bytes | None = None, - backlog: int | None = None, - ) -> NoReturn: - """Serve over TCP. See trio.open_tcp_listeners for argument details.""" - self.serve_cancel_scope = trio.CancelScope() - async with trio.open_nursery() as nursery: - listeners = await trio.open_tcp_listeners( - port, - host=host, - backlog=backlog, - ) - - async def handle_serve( - task_status: trio.TaskStatus[Any] = trio.TASK_STATUS_IGNORED, - ) -> None: - assert self.serve_cancel_scope is not None - try: - with self.serve_cancel_scope: - await trio.serve_listeners( - self.handler, - listeners, - handler_nursery=nursery, - task_status=task_status, - ) - except trio.Cancelled: - # Close all listeners - async with trio.open_nursery() as cancel_nursery: - for listener in listeners: - cancel_nursery.start_soon(listener.aclose) - - await nursery.start(handle_serve) - - async def handler( - self, - stream: trio.SocketStream, - ) -> None: # pragma: nocover - """Handle new client streams. - - Override in a subclass - Default only closes the stream - """ - try: - await stream.send_eof() - finally: - await stream.aclose() - - -if __name__ == "__main__": # pragma: nocover - print(f"{__title__}\nProgrammed by {__author__}.\n") diff --git a/src/azul/network_shared.py b/src/azul/network_shared.py index ce56c88..aa30f5d 100644 --- a/src/azul/network_shared.py +++ b/src/azul/network_shared.py @@ -27,7 +27,6 @@ from enum import IntEnum, auto from typing import Final, NamedTuple, TypeAlias -import trio from mypy_extensions import u8 ADVERTISEMENT_IP: Final = "224.0.2.60" @@ -45,36 +44,6 @@ class TickEventData(NamedTuple): fps: float -# Stolen from WOOF (Web Offer One File), Copyright (C) 2004-2009 Simon Budig, -# available at http://www.home.unix-ag.org/simon/woof -# with modifications - -# Utility function to guess the IP (as a string) where the server can be -# reached from the outside. Quite nasty problem actually. - - -async def find_ip() -> str: # pragma: nocover - """Guess the IP where the server can be found from the network.""" - # we get a UDP-socket for the TEST-networks reserved by IANA. - # It is highly unlikely, that there is special routing used - # for these networks, hence the socket later should give us - # the IP address of the default route. - # We're doing multiple tests, to guard against the computer being - # part of a test installation. - - candidates: list[str] = [] - for test_ip in ("192.0.2.0", "198.51.100.0", "203.0.113.0"): - sock = trio.socket.socket(trio.socket.AF_INET, trio.socket.SOCK_DGRAM) - await sock.connect((test_ip, 80)) - ip_addr: str = sock.getsockname()[0] - sock.close() - if ip_addr in candidates: - return ip_addr - candidates.append(ip_addr) - - return candidates[0] - - class ClientBoundEvents(IntEnum): """Client bound event IDs.""" diff --git a/src/azul/objects.py b/src/azul/objects.py index 0f89f07..77d71a8 100644 --- a/src/azul/objects.py +++ b/src/azul/objects.py @@ -35,10 +35,9 @@ from azul import sprite if TYPE_CHECKING: + from libcomponent.component import Event from pygame.font import Font - from azul.component import Event - class Text(sprite.Sprite): """Text element. diff --git a/src/azul/server.py b/src/azul/server.py index 3769b39..f361f62 100755 --- a/src/azul/server.py +++ b/src/azul/server.py @@ -34,26 +34,25 @@ from typing import TYPE_CHECKING, NoReturn import trio - -from azul import network -from azul.base_io import StructFormat -from azul.buffer import Buffer -from azul.component import ComponentManager, Event, ExternalRaiseManager -from azul.encrypted_event import EncryptedNetworkEventComponent -from azul.encryption import ( - RSAPrivateKey, - decrypt_token_and_secret, - generate_rsa_key, - generate_verify_token, - serialize_public_key, +from libcomponent import network +from libcomponent.base_io import StructFormat +from libcomponent.buffer import Buffer +from libcomponent.component import ( + ComponentManager, + Event, + ExternalRaiseManager, +) +from libcomponent.network_utils import ( + ServerClientNetworkEventComponent, + find_ip, ) + from azul.network_shared import ( ADVERTISEMENT_IP, ADVERTISEMENT_PORT, DEFAULT_PORT, ClientBoundEvents, ServerBoundEvents, - find_ip, ) from azul.state import State @@ -61,7 +60,7 @@ from collections.abc import Awaitable, Callable -class ServerClient(EncryptedNetworkEventComponent): +class ServerClient(ServerClientNetworkEventComponent): """Server Client Network Event Component. When clients connect to server, this class handles the incoming @@ -69,7 +68,7 @@ class ServerClient(EncryptedNetworkEventComponent): that are transferred over the network. """ - __slots__ = ("client_id", "rsa_key", "verify_token") + __slots__ = ("client_id",) def __init__(self, client_id: int) -> None: """Initialize Server Client.""" @@ -95,9 +94,6 @@ def __init__(self, client_id: int) -> None: }, ) - self.rsa_key: RSAPrivateKey | None = None - self.verify_token: bytes | None = None - def bind_handlers(self) -> None: """Bind event handlers.""" super().bind_handlers() @@ -176,23 +172,8 @@ async def handle_callback_ping( await self.write_callback_ping() async def start_encryption_request(self) -> None: - """Start encryption request and raise as server[write]->encryption_request.""" - if self.encryption_enabled: - raise RuntimeError("Encryption is already set up!") - self.rsa_key = generate_rsa_key() - self.verify_token = generate_verify_token() - - public_key = self.rsa_key.public_key() - - serialized_public_key = serialize_public_key(public_key) - - buffer = Buffer() - buffer.write_bytearray(serialized_public_key) - buffer.write_bytearray(self.verify_token) - - await self.write_event( - Event("server[write]->encryption_request", buffer), - ) + """Start encryption request and raise as `server[write]->encryption_request`.""" + await super().start_encryption_request() event = await self.read_event() if event.name != f"client[{self.client_id}]->encryption_response": @@ -201,36 +182,6 @@ async def start_encryption_request(self) -> None: ) await self.handle_encryption_response(event) - async def handle_encryption_response( - self, - event: Event[bytearray], - ) -> None: - """Read encryption response.""" - if self.rsa_key is None or self.verify_token is None: - raise RuntimeError( - "Was not expecting encryption response, request start not sent!", - ) - if self.encryption_enabled: - raise RuntimeError("Encryption is already set up!") - buffer = Buffer(event.data) - - encrypted_shared_secret = buffer.read_bytearray() - encrypted_verify_token = buffer.read_bytearray() - - verify_token, shared_secret = decrypt_token_and_secret( - self.rsa_key, - encrypted_verify_token, - encrypted_shared_secret, - ) - - if verify_token != self.verify_token: - raise RuntimeError( - "Received verify token does not match sent verify token!", - ) - - # Start encrypting all future data - self.enable_encryption(shared_secret, verify_token) - class GameServer(network.Server): """Checkers server. @@ -261,7 +212,6 @@ def __init__(self, internal_singleplayer_mode: bool = False) -> None: self.state = State.new_game(0) self.client_players: dict[int, int] = {} - self.player_selections: dict[int, Pos] = {} self.players_can_interact: bool = False self.internal_singleplayer_mode = internal_singleplayer_mode @@ -382,7 +332,6 @@ def setup_teams(client_ids: list[int]) -> dict[int, int]: def new_game_init(self) -> None: """Start new game.""" self.client_players.clear() - self.player_selections.clear() ## pieces = generate_pieces(*self.board_size) self.state = State.new_game(self.client_count) @@ -550,8 +499,7 @@ async def handler(self, stream: trio.SocketStream) -> None: """Accept clients. Called by network.Server.serve.""" if self.client_count == 0 and self.game_active(): # Old game was running but everyone left, restart - self.state.pieces.clear() - # self.state = CheckersState(self.board_size, {}) + print("TODO: restart") new_client_id = self.client_count print( f"{self.__class__.__name__}: client connected [client_id {new_client_id}]", diff --git a/src/azul/sprite.py b/src/azul/sprite.py index 1335e74..fbd1915 100644 --- a/src/azul/sprite.py +++ b/src/azul/sprite.py @@ -27,6 +27,7 @@ from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple, TypedDict, cast import trio +from libcomponent.component import Component, ComponentManager, Event from pygame.color import Color from pygame.event import Event as PygameEvent, event_name from pygame.mask import Mask, from_surface as mask_from_surface @@ -34,7 +35,6 @@ from pygame.sprite import LayeredDirty, LayeredUpdates, WeakDirtySprite from pygame.surface import Surface -from azul.component import Component, ComponentManager, Event from azul.statemachine import AsyncStateMachine from azul.vector import Vector2 diff --git a/src/azul/utils.py b/src/azul/utils.py deleted file mode 100644 index 22bf7ec..0000000 --- a/src/azul/utils.py +++ /dev/null @@ -1,49 +0,0 @@ -"""Two's Complement Utilities.""" - -# This is the base_io module from https://github.com/py-mine/mcproto v0.3.0, -# which is licensed under the GNU LESSER GENERAL PUBLIC LICENSE v3.0 - -from __future__ import annotations - -__author__ = "ItsDrike" -__license__ = "LGPL-3.0-only" - -__all__ = ["from_twos_complement", "to_twos_complement"] - - -def to_twos_complement(number: int, bits: int) -> int: - """Convert a given ``number`` into twos complement format of given amount of ``bits``. - - :raises ValueError: - Given ``number`` is out of range, and can't be converted into twos complement format, since - it wouldn't fit into the given amount of ``bits``. - """ - value_max = 1 << (bits - 1) - value_min = value_max * -1 - # With two's complement, we have one more negative number than positive - # this means we can't be exactly at value_max, but we can be at exactly value_min - if number >= value_max or number < value_min: - raise ValueError( - f"Can't convert number {number} into {bits}-bit twos complement format - out of range", - ) - - return number + (1 << bits) if number < 0 else number - - -def from_twos_complement(number: int, bits: int) -> int: - """Convert a given ``number`` from twos complement format of given amount of ``bits``. - - :raises ValueError: - Given ``number`` doesn't fit into given amount of ``bits``. This likely means that you're using - the wrong number, or that the number was converted into twos complement with higher amount of ``bits``. - """ - value_max = (1 << bits) - 1 - if number < 0 or number > value_max: - raise ValueError( - f"Can't convert number {number} from {bits}-bit twos complement format - out of range", - ) - - if number & (1 << (bits - 1)) != 0: - number -= 1 << bits - - return number diff --git a/test-requirements.in b/test-requirements.in index 449c3e8..6c3b0de 100644 --- a/test-requirements.in +++ b/test-requirements.in @@ -18,8 +18,8 @@ typing-extensions # Azul's own dependencies # -cryptography>=43.0.0 exceptiongroup; python_version < '3.11' +libcomponent~=0.0.0 mypy_extensions>=1.0.0 numpy~=2.1.3 pygame~=2.6.0 diff --git a/tests/helpers.py b/tests/helpers.py deleted file mode 100644 index 24390e4..0000000 --- a/tests/helpers.py +++ /dev/null @@ -1,175 +0,0 @@ -# This is the base_io module from https://github.com/py-mine/mcproto v0.5.0, -# which is licensed under the GNU LESSER GENERAL PUBLIC LICENSE v3.0 - -from __future__ import annotations - -__author__ = "ItsDrike" -__license__ = "LGPL-3.0-only" - -import inspect -import unittest.mock -from functools import partial -from typing import TYPE_CHECKING, Any, Generic, TypeVar - -import trio -from typing_extensions import ParamSpec - -if TYPE_CHECKING: - from collections.abc import Callable, Coroutine - -T = TypeVar("T") -P = ParamSpec("P") -T_Mock = TypeVar("T_Mock", bound=unittest.mock.Mock) - - -def synchronize(f: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, T]: - """Take an asynchronous function, and return a synchronous alternative. - - This is needed because we sometimes want to test asynchronous behavior in a synchronous test function, - where we can't simply await something. This function uses `trio.run` and generates a wrapper - around the original asynchronous function, that awaits the result in a blocking synchronous way, - returning the obtained value. - """ - - def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: - return trio.run(partial(f, *args, **kwargs)) - - return wrapper - - -class SynchronizedMixin: - """Class acting as another wrapped object, with all async methods synchronized. - - This class needs :attr:`._WRAPPED_ATTRIBUTE` class variable to be set as the name of the internally - held attribute, holding the object we'll be wrapping around. - - Child classes of this mixin will have their lookup logic changed, to instead perform a lookup - on the wrapped attribute. Only if that lookup fails, we fallback to this class, meaning if both - the wrapped attribute and this class have some attribute defined, the attribute from the wrapped - object is returned. The only exceptions to this are lookup of the ``_WRAPPED_ATTRIBUTE`` variable, - and of the attribute name stored under the ``_WRAPPED_ATTRIBUTE`` (the wrapped object). - - If the attribute held by the wrapped object is an asynchronous function, instead of returning it - directly, the :func:`.synchronize` function will be called, returning a wrapped synchronous - alternative for the requested async function. - - This is useful when we need to quickly create a synchronous alternative to a class holding async methods. - However it isn't useful in production, since will cause typing issues (attributes will be accessible, but - type checkers won't know that they exist here, because of the dynamic nature of this implementation). - """ - - _WRAPPED_ATTRIBUTE: str - - def __getattribute__(self, __name: str) -> Any: - """Return attributes of the wrapped object, if the attribute is a coroutine function, synchronize it. - - The only exception to this behavior is getting the :attr:`._WRAPPED_ATTRIBUTE` variable itself, or the - attribute named as the content of the ``_WRAPPED_ATTRIBUTE`` variable. All other attribute access will - be delegated to the wrapped attribute. If the wrapped object doesn't have given attribute, the lookup - will fallback to regular lookup for variables belonging to this class. - """ - if ( - __name == "_WRAPPED_ATTRIBUTE" or __name == self._WRAPPED_ATTRIBUTE - ): # Order is important - return super().__getattribute__(__name) - - wrapped = getattr(self, self._WRAPPED_ATTRIBUTE) - - if hasattr(wrapped, __name): - obj = getattr(wrapped, __name) - if inspect.iscoroutinefunction(obj): - return synchronize(obj) - return obj - - return super().__getattribute__(__name) - - def __setattr__(self, __name: str, __value: object) -> None: - """Allow for changing attributes of the wrapped object. - - * If wrapped object isn't yet set, fall back to :meth:`~object.__setattr__` of this class. - * If wrapped object doesn't already contain the attribute we want to set, also fallback to this class. - * Otherwise, run ``__setattr__`` on it to update it. - """ - try: - wrapped = getattr(self, self._WRAPPED_ATTRIBUTE) - except AttributeError: - return super().__setattr__(__name, __value) - else: - if hasattr(wrapped, __name): - return setattr(wrapped, __name, __value) - - return super().__setattr__(__name, __value) - - -class UnpropagatingMockMixin(Generic[T_Mock]): - """Provides common functionality for our :class:`~unittest.mock.Mock` classes. - - By default, mock objects propagate themselves by returning a new instance of the same mock - class, with same initialization attributes. This is done whenever we're accessing new - attributes that mock class. - - This propagation makes sense for simple mocks without any additional restrictions, however when - dealing with limited mocks to some ``spec_set``, it doesn't usually make sense to propagate - those same ``spec_set`` restrictions, since we generally don't have attributes/methods of a - class be of/return the same class. - - This mixin class stops this propagation, and instead returns instances of specified mock class, - defined in :attr:`.child_mock_type` class variable, which is by default set to - :class:`~unittest.mock.MagicMock`, as it can safely represent most objects. - - .. note: - This propagation handling will only be done for the mock classes that inherited from this - mixin class. That means if the :attr:`.child_mock_type` is one of the regular mock classes, - and the mock is propagated, a regular mock class is returned as that new attribute. This - regular class then won't have the same overrides, and will therefore propagate itself, like - any other mock class would. - - If you wish to counteract this, you can set the :attr:`.child_mock_type` to a mock class - that also inherits from this mixin class, perhaps to your class itself, overriding any - propagation recursively. - """ - - child_mock_type: T_Mock = unittest.mock.MagicMock - - # Since this is a mixin class, we can access some attributes defined in mock classes safely. - # Define the types of these variables here, for proper static type analysis. - _mock_sealed: bool - _extract_mock_name: Callable[[], str] - - def _get_child_mock(self, **kwargs) -> T_Mock: - """Make :attr:`.child_mock_type`` instances instead of instances of the same class. - - By default, this method creates a new mock instance of the same original class, and passes - over the same initialization arguments. This overrides that behavior to instead create an - instance of :attr:`.child_mock_type` class. - """ - # Mocks can be sealed, in which case we wouldn't want to allow propagation of any kind - # and rather raise an AttributeError, informing that given attr isn't accessible - if self._mock_sealed: - mock_name = self._extract_mock_name() - obj_name = ( - f"{mock_name}.{kwargs['name']}" - if "name" in kwargs - else f"{mock_name}()" - ) - raise AttributeError(f"Can't access {obj_name}, mock is sealed.") - - # Propagate any other children as simple `unittest.mock.Mock` instances - # rather than `self.__class__` instances - return self.child_mock_type(**kwargs) - - -class CustomMockMixin(UnpropagatingMockMixin): - """Provides common functionality for our custom mock types. - - * Stops propagation of same ``spec_set`` restricted mock in child mocks - (see :class:`.UnpropagatingMockMixin` for more info) - * Allows using the ``spec_set`` attribute as class attribute - """ - - spec_set = None - - def __init__(self, **kwargs): - if "spec_set" in kwargs: - self.spec_set = kwargs.pop("spec_set") - super().__init__(spec_set=self.spec_set, **kwargs) # type: ignore # Mixin class, this __init__ is valid diff --git a/tests/protocol_helpers.py b/tests/protocol_helpers.py deleted file mode 100644 index 74a16f9..0000000 --- a/tests/protocol_helpers.py +++ /dev/null @@ -1,87 +0,0 @@ -# This is the base_io module from https://github.com/py-mine/mcproto v0.5.0, -# which is licensed under the GNU LESSER GENERAL PUBLIC LICENSE v3.0 - -from __future__ import annotations - -__author__ = "ItsDrike" -__license__ = "LGPL-3.0-only" - -from unittest.mock import AsyncMock, Mock - - -class WriteFunctionMock(Mock): - """Mock write function, storing the written data.""" - - def __init__(self, *a, **kw): - super().__init__(*a, **kw) - self.combined_data = bytearray() - - def __call__( - self, - data: bytes, - ) -> None: # pyright: ignore[reportIncompatibleMethodOverride] - """Override mock's ``__call__`` to extend our :attr:`.combined_data` bytearray. - - This allows us to keep track of exactly what data was written by the mocked write function - in total, rather than only having tools like :meth:`.assert_called_with`, which might let us - get the data from individual calls, but not the combined data, which is what we'll need. - """ - self.combined_data.extend(data) - return super().__call__(data) - - def assert_has_data( - self, - data: bytearray, - ensure_called: bool = True, - ) -> None: - """Ensure that the combined write data by the mocked function matches expected ``data``.""" - if ensure_called: - self.assert_called() - - if self.combined_data != data: - raise AssertionError( - f"Write function mock expected data {data!r}, but was {self.call_data!r}", - ) - - -class WriteFunctionAsyncMock(WriteFunctionMock, AsyncMock): - """Asynchronous mock write function, storing the written data.""" - - -class ReadFunctionMock(Mock): - """Mock read function, giving pre-defined data.""" - - def __init__(self, *a, combined_data: bytearray | None = None, **kw): - super().__init__(*a, **kw) - if combined_data is None: - combined_data = bytearray() - self.combined_data = combined_data - - def __call__( - self, - length: int, - ) -> bytearray: # pyright: ignore[reportIncompatibleMethodOverride] - """Override mock's __call__ to make it return part of our :attr:`.combined_data` bytearray. - - This allows us to make the return value always be the next requested part (length) of - the :attr:`.combined_data`. It would be difficult to replicate this with regular mocks, - because some functions can end up making multiple read calls, and each time the result - needs to be different (the next part). - """ - self.return_value = self.combined_data[:length] - del self.combined_data[:length] - return super().__call__(length) - - def assert_read_everything(self, ensure_called: bool = True) -> None: - """Ensure that the passed :attr:`.combined_data` was fully read and depleted.""" - if ensure_called: - self.assert_called() - - if len(self.combined_data) != 0: - raise AssertionError( - f"Read function didn't deplete all of it's data, remaining data: {self.combined_data!r}", - ) - - -class ReadFunctionAsyncMock(ReadFunctionMock, AsyncMock): - """Asynchronous mock read function, giving pre-defined data.""" diff --git a/tests/test_base_io.py b/tests/test_base_io.py deleted file mode 100644 index 456aad8..0000000 --- a/tests/test_base_io.py +++ /dev/null @@ -1,746 +0,0 @@ -# This is the base_io module from https://github.com/py-mine/mcproto v0.5.0, -# which is licensed under the GNU LESSER GENERAL PUBLIC LICENSE v3.0 - -from __future__ import annotations - -__author__ = "ItsDrike" -__license__ = "LGPL-3.0-only" - -import platform -import struct -from abc import ABC, abstractmethod -from typing import Any -from unittest.mock import AsyncMock, Mock - -import pytest -from helpers import SynchronizedMixin -from protocol_helpers import ( - ReadFunctionAsyncMock, - ReadFunctionMock, - WriteFunctionAsyncMock, - WriteFunctionMock, -) - -from azul.base_io import ( - INT_FORMATS_TYPE, - BaseAsyncReader, - BaseAsyncWriter, - BaseSyncReader, - BaseSyncWriter, - StructFormat, -) -from azul.utils import to_twos_complement - -# region: Initializable concrete implementations of ABC classes. - - -class SyncWriter(BaseSyncWriter): - """Initializable concrete implementation of :class:`~mcproto.protocol.base_io.BaseSyncWriter` ABC.""" - - def write(self, data: bytes) -> None: - """Concrete implementation of abstract write method. - - Since :class:`abc.ABC` classes can't be initialized if they have any abstract methods - which weren't overridden with a concrete implementations, this is a fake implementation, - without any actual logic, purely to allow the initialization of this class. - - This method is expected to be mocked using :class:`~tests.mcproto.protocol.helpers.WriteFunctionMock` - if it's supposed to get called during testing. - - If this method gets called without being mocked, it will raise :exc:`NotImplementedError`. - """ - raise NotImplementedError( - "This concrete override of abstract write method isn't intended for actual use!\n" - " - If you're writing a new test, did you forget to mock it?\n" - " - If you're seeing this in an existing test, this method got called without the test expecting it," - " this probably means you changed something in the code leading to this call, but you haven't updated" - " the tests to mock this function.", - ) - - -class SyncReader(BaseSyncReader): - """Testable concrete implementation of :class:`~mcproto.protocol.base_io.BaseSyncReader` ABC.""" - - def read(self, length: int) -> bytearray: - """Concrete implementation of abstract read method. - - Since :class:`abc.ABC` classes can't be initialized if they have any abstract methods - which weren't overridden with a concrete implementations, this is a fake implementation, - without any actual logic, purely to allow the initialization of this class. - - This method is expected to be mocked using :class:`~tests.mcproto.protocol.helpers.ReadFunctionMock` - if it's supposed to get called during testing. - - If this method gets called without being mocked, it will raise :exc:`NotImplementedError`. - """ - raise NotImplementedError( - "This concrete override of abstract read method isn't intended for actual use!\n" - " - If you're writing a new test, did you forget to mock it?\n" - " - If you're seeing this in an existing test, this method got called without the test expecting it," - " this probably means you changed something in the code leading to this call, but you haven't updated" - " the tests to mock this function.", - ) - - -class AsyncWriter(BaseAsyncWriter): - """Initializable concrete implementation of :class:`~mcproto.protocol.base_io.BaseAsyncWriter` ABC.""" - - async def write(self, data: bytes) -> None: - """Concrete implementation of abstract write method. - - Since :class:`abc.ABC` classes can't be initialized if they have any abstract methods - which weren't overridden with a concrete implementations, this is a fake implementation, - without any actual logic, purely to allow the initialization of this class. - - This method is expected to be mocked using :class:`~tests.mcproto.protocol.helpers.WriteFunctionAsyncMock` - if it's supposed to get called during testing. - - If this method gets called without being mocked, it will raise :exc:`NotImplementedError`. - """ - raise NotImplementedError( - "This concrete override of abstract write method isn't intended for actual use!\n" - " - If you're writing a new test, did you forget to mock it?\n" - " - If you're seeing this in an existing test, this method got called without the test expecting it," - " this probably means you changed something in the code leading to this call, but you haven't updated" - " the tests to mock this function.", - ) - - -class AsyncReader(BaseAsyncReader): - """Testable concrete implementation of BaseAsyncReader ABC.""" - - async def read(self, length: int) -> bytearray: - """Concrete implementation of abstract read method. - - Since :class:`abc.ABC` classes can't be initialized if they have any abstract methods - which weren't overridden with a concrete implementations, this is a fake implementation, - without any actual logic, purely to allow the initialization of this class. - - This method is expected to be mocked using :class:`~tests.mcproto.protocol.helpers.ReadFunctionAsyncMock` - if it's supposed to get called during testing. - - If this method gets called without being mocked, it will raise :exc:`NotImplementedError`. - """ - raise NotImplementedError( - "This concrete override of abstract read method isn't intended for actual use!\n" - " - If you're writing a new test, did you forget to mock it?\n" - " - If you're seeing this in an existing test, this method got called without the test expecting it," - " this probably means you changed something in the code leading to this call, but you haven't updated" - " the tests to mock this function.", - ) - - -# endregion -# region: Synchronized classes - - -class WrappedAsyncReader(SynchronizedMixin): - """Wrapped synchronous implementation of asynchronous :class:`.AsyncReader` class. - - This essentially mimics :class:`~mcproto.protocol.base_io.BaseSyncReader`. - """ - - _WRAPPED_ATTRIBUTE = "_reader" - - def __init__(self): - self._reader = AsyncReader() - - -class WrappedAsyncWriter(SynchronizedMixin): - """Wrapped synchronous implementation of asynchronous :class:`.AsyncWriter` class. - - This essentially mimics :class:`~mcproto.protocol.base_io.BaseSyncWriter`. - """ - - _WRAPPED_ATTRIBUTE = "_writer" - - def __init__(self): - self._writer = AsyncWriter() - - -# endregion -# region: Abstract test classes - - -class WriterTests(ABC): - """Collection of tests for both sync and async versions of the writer.""" - - writer: BaseSyncWriter | BaseAsyncWriter - - @classmethod - @abstractmethod - def setup_class(cls): - """Initialize writer instance to be tested.""" - ... - - @pytest.fixture - def method_mock(self) -> Mock | AsyncMock: - """Obtain the appropriate type of mock, supporting both sync and async modes.""" - if isinstance(self.writer, BaseSyncWriter): - return Mock - return AsyncMock - - @pytest.fixture - def autopatch(self, monkeypatch: pytest.MonkeyPatch): - """Create a simple function, supporting patching both sync/async writer functions with appropriate mocks. - - This returned function takes in the name of the function to patch, and returns the mock object. - This mock object will either be Mock, or AsyncMock instance, depending on whether we're in async or sync mode. - """ - if isinstance(self.writer, SyncWriter): - patch_path = "mcproto.protocol.base_io.BaseSyncWriter" - mock_type = Mock - else: - patch_path = "mcproto.protocol.base_io.BaseAsyncWriter" - mock_type = AsyncMock - - def autopatch(function_name: str) -> Mock | AsyncMock: - mock_f = mock_type() - monkeypatch.setattr(f"{patch_path}.{function_name}", mock_f) - return mock_f - - return autopatch - - @pytest.fixture - def write_mock(self, monkeypatch: pytest.MonkeyPatch): - """Monkeypatch the write function with a mock which is returned.""" - mock_f = ( - WriteFunctionMock() - if isinstance(self.writer, BaseSyncWriter) - else WriteFunctionAsyncMock() - ) - monkeypatch.setattr(self.writer.__class__, "write", mock_f) - return mock_f - - @pytest.mark.parametrize( - ("fmt", "value", "expected_bytes"), - [ - (StructFormat.UBYTE, 0, [0]), - (StructFormat.UBYTE, 15, [15]), - (StructFormat.UBYTE, 255, [255]), - (StructFormat.BYTE, 0, [0]), - (StructFormat.BYTE, 15, [15]), - (StructFormat.BYTE, 127, [127]), - (StructFormat.BYTE, -20, [to_twos_complement(-20, bits=8)]), - (StructFormat.BYTE, -128, [to_twos_complement(-128, bits=8)]), - ], - ) - def test_write_value( - self, - fmt: INT_FORMATS_TYPE, - value: Any, - expected_bytes: list[int], - write_mock: WriteFunctionMock, - ): - """Test writing values sends expected bytes.""" - self.writer.write_value(fmt, value) - write_mock.assert_has_data(bytearray(expected_bytes)) - - @pytest.mark.parametrize( - ("fmt", "value"), - [ - (StructFormat.UBYTE, -1), - (StructFormat.UBYTE, 256), - (StructFormat.BYTE, -129), - (StructFormat.BYTE, 128), - ], - ) - def test_write_value_out_of_range( - self, - fmt: INT_FORMATS_TYPE, - value: Any, - ): - """Test writing out of range values for the given format raises :exc:`struct.error`.""" - with pytest.raises(struct.error): - self.writer.write_value(fmt, value) - - @pytest.mark.parametrize( - ("number", "expected_bytes"), - [ - (0, [0]), - (1, [1]), - (2, [2]), - (15, [15]), - (127, [127]), - (128, [128, 1]), - (129, [129, 1]), - (255, [255, 1]), - (1000000, [192, 132, 61]), - (2147483647, [255, 255, 255, 255, 7]), - ], - ) - def test_write_varuint( - self, - number: int, - expected_bytes: list[int], - write_mock: WriteFunctionMock, - ): - """Test writing varuints results in correct bytes.""" - self.writer._write_varuint(number) - write_mock.assert_has_data(bytearray(expected_bytes)) - - @pytest.mark.parametrize( - ("write_value", "max_bits"), - [ - (-1, 128), - (-1, 1), - (2**16, 16), - (2**32, 32), - ], - ) - def test_write_varuint_out_of_range(self, write_value: int, max_bits: int): - """Test writing out of range varuints raises :exc:`ValueError`.""" - with pytest.raises( - ValueError, - match="^Tried to write varint outside of the range of", - ): - self.writer._write_varuint(write_value, max_bits=max_bits) - - @pytest.mark.parametrize( - ("number", "expected_bytes"), - [ - (127, [127]), - (16384, [128, 128, 1]), - (-128, [128, 255, 255, 255, 15]), - (-16383, [129, 128, 255, 255, 15]), - ], - ) - def test_write_varint( - self, - number: int, - expected_bytes: list[int], - write_mock: WriteFunctionMock, - ): - """Test writing varints results in correct bytes.""" - self.writer.write_varint(number) - write_mock.assert_has_data(bytearray(expected_bytes)) - - @pytest.mark.parametrize( - ("number", "expected_bytes"), - [ - (127, [127]), - (16384, [128, 128, 1]), - (-128, [128, 255, 255, 255, 255, 255, 255, 255, 255, 1]), - (-16383, [129, 128, 255, 255, 255, 255, 255, 255, 255, 1]), - ], - ) - def test_write_varlong( - self, - number: int, - expected_bytes: list[int], - write_mock: WriteFunctionMock, - ): - """Test writing varlongs results in correct bytes.""" - self.writer.write_varlong(number) - write_mock.assert_has_data(bytearray(expected_bytes)) - - @pytest.mark.parametrize( - ("data", "expected_bytes"), - [ - (b"", [0]), - (b"\x01", [1, 1]), - ( - b"hello\0world", - [11, 104, 101, 108, 108, 111, 0, 119, 111, 114, 108, 100], - ), - (b"\x01\x02\x03four\x05", [8, 1, 2, 3, 102, 111, 117, 114, 5]), - ], - ) - def test_write_bytearray( - self, - data: bytes, - expected_bytes: list[int], - write_mock: WriteFunctionMock, - ): - """Test writing ASCII string results in correct bytes.""" - self.writer.write_bytearray(data) - write_mock.assert_has_data(bytearray(expected_bytes)) - - @pytest.mark.parametrize( - ("string", "expected_bytes"), - [ - ("test", [*list(map(ord, "test")), 0]), - ("a" * 100, [*list(map(ord, "a" * 100)), 0]), - ("", [0]), - ], - ) - def test_write_ascii( - self, - string: str, - expected_bytes: list[int], - write_mock: WriteFunctionMock, - ): - """Test writing ASCII string results in correct bytes.""" - self.writer.write_ascii(string) - write_mock.assert_has_data(bytearray(expected_bytes)) - - @pytest.mark.parametrize( - ("string", "expected_bytes"), - [ - ("test", [len("test"), *list(map(ord, "test"))]), - ("a" * 100, [len("a" * 100), *list(map(ord, "a" * 100))]), - ("", [0]), - ("नमस्ते", [18] + [int(x) for x in "नमस्ते".encode()]), - ], - ) - def test_write_utf( - self, - string: str, - expected_bytes: list[int], - write_mock: WriteFunctionMock, - ): - """Test writing UTF string results in correct bytes.""" - self.writer.write_utf(string) - write_mock.assert_has_data(bytearray(expected_bytes)) - - @pytest.mark.skipif( - platform.system() == "Windows", - reason="environment variable limit on Windows", - ) - def test_write_utf_limit(self, write_mock: WriteFunctionMock): - """Test writing a UTF string too big raises a :exc:`ValueError`.""" - with pytest.raises( - ValueError, - match="Maximum character limit for writing strings is 32767 characters.", - ): - self.writer.write_utf("a" * (32768)) - - def test_write_optional_true( - self, - method_mock: Mock | AsyncMock, - write_mock: WriteFunctionMock, - ): - """Test writing non-``None`` value writes ``True`` and runs the writer function.""" - mock_v = Mock() - mock_f = method_mock() - self.writer.write_optional(mock_v, mock_f) - mock_f.assert_called_once_with(mock_v) - write_mock.assert_has_data(bytearray([1])) - - def test_write_optional_false( - self, - method_mock: Mock | AsyncMock, - write_mock: WriteFunctionMock, - ): - """Test writing ``None`` value should write ``False`` and skip running the writer function.""" - mock_f = method_mock() - self.writer.write_optional(None, mock_f) - mock_f.assert_not_called() - write_mock.assert_has_data(bytearray([0])) - - -class ReaderTests(ABC): - """Collection of tests for both sync and async versions of the reader.""" - - reader: BaseSyncReader | BaseAsyncReader - - @classmethod - @abstractmethod - def setup_class(cls): - """Initialize reader instance to be tested.""" - ... - - @pytest.fixture - def method_mock(self) -> Mock | AsyncMock: - """Obtain the appropriate type of mock, supporting both sync and async modes.""" - if isinstance(self.reader, BaseSyncReader): - return Mock - return AsyncMock - - @pytest.fixture - def autopatch(self, monkeypatch: pytest.MonkeyPatch): - """Create a simple function, supporting patching both sync/async reader functions with appropriate mocks. - - This returned function takes in the name of the function to patch, and returns the mock object. - This mock object will either be Mock, or AsyncMock instance, depending on whether we're in async or sync mode. - """ - if isinstance(self.reader, SyncReader): - patch_path = "mcproto.protocol.base_io.BaseSyncReader" - mock_type = Mock - else: - patch_path = "mcproto.protocol.base_io.BaseAsyncReader" - mock_type = AsyncMock - - def autopatch(function_name: str) -> Mock | AsyncMock: - mock_f = mock_type() - monkeypatch.setattr(f"{patch_path}.{function_name}", mock_f) - return mock_f - - return autopatch - - @pytest.fixture - def read_mock(self, monkeypatch: pytest.MonkeyPatch): - """Monkeypatch the read function with a mock which is returned.""" - mock_f = ( - ReadFunctionMock() - if isinstance(self.reader, SyncReader) - else ReadFunctionAsyncMock() - ) - monkeypatch.setattr(self.reader.__class__, "read", mock_f) - yield mock_f - # Run this assertion after the test, to ensure that all specified data - # to be read, actually was read - mock_f.assert_read_everything() - - @pytest.mark.parametrize( - ("fmt", "read_bytes", "expected_value"), - [ - (StructFormat.UBYTE, [0], 0), - (StructFormat.UBYTE, [10], 10), - (StructFormat.UBYTE, [255], 255), - (StructFormat.BYTE, [0], 0), - (StructFormat.BYTE, [20], 20), - (StructFormat.BYTE, [127], 127), - (StructFormat.BYTE, [to_twos_complement(-20, bits=8)], -20), - (StructFormat.BYTE, [to_twos_complement(-128, bits=8)], -128), - ], - ) - def test_read_value( - self, - fmt: INT_FORMATS_TYPE, - read_bytes: list[int], - expected_value: Any, - read_mock: ReadFunctionMock, - ): - """Test reading bytes gets expected value.""" - read_mock.combined_data = bytearray(read_bytes) - assert self.reader.read_value(fmt) == expected_value - - @pytest.mark.parametrize( - ("read_bytes", "expected_value"), - [ - ([0], 0), - ([1], 1), - ([2], 2), - ([15], 15), - ([127], 127), - ([128, 1], 128), - ([129, 1], 129), - ([255, 1], 255), - ([192, 132, 61], 1000000), - ([255, 255, 255, 255, 7], 2147483647), - ], - ) - def test_read_varuint( - self, - read_bytes: list[int], - expected_value: int, - read_mock: ReadFunctionMock, - ): - """Test reading varuint bytes results in correct values.""" - read_mock.combined_data = bytearray(read_bytes) - assert self.reader._read_varuint() == expected_value - - @pytest.mark.parametrize( - ("read_bytes", "max_bits"), - [ - ([128, 128, 4], 16), - ([128, 128, 128, 128, 16], 32), - ], - ) - def test_read_varuint_out_of_range( - self, - read_bytes: list[int], - max_bits: int, - read_mock: ReadFunctionMock, - ): - """Test reading out-of-range varuints raises :exc:`IOError`.""" - read_mock.combined_data = bytearray(read_bytes) - with pytest.raises( - IOError, - match="^Received varint was outside the range of", - ): - self.reader._read_varuint(max_bits=max_bits) - - @pytest.mark.parametrize( - ("read_bytes", "expected_value"), - [ - ([127], 127), - ([128, 128, 1], 16384), - ([128, 255, 255, 255, 15], -128), - ([129, 128, 255, 255, 15], -16383), - ], - ) - def test_read_varint( - self, - read_bytes: list[int], - expected_value: int, - read_mock: ReadFunctionMock, - ): - """Test reading varuint bytes results in correct values.""" - read_mock.combined_data = bytearray(read_bytes) - assert self.reader.read_varint() == expected_value - - @pytest.mark.parametrize( - ("read_bytes", "expected_value"), - [ - ([127], 127), - ([128, 128, 1], 16384), - ([128, 255, 255, 255, 255, 255, 255, 255, 255, 1], -128), - ([129, 128, 255, 255, 255, 255, 255, 255, 255, 1], -16383), - ], - ) - def test_read_varlong( - self, - read_bytes: list[int], - expected_value: int, - read_mock: ReadFunctionMock, - ): - """Test reading varuint bytes results in correct values.""" - read_mock.combined_data = bytearray(read_bytes) - assert self.reader.read_varlong() == expected_value - - @pytest.mark.parametrize( - ("read_bytes", "expected_bytes"), - [ - ([0], b""), - ([1, 1], b"\x01"), - ( - [11, 104, 101, 108, 108, 111, 0, 119, 111, 114, 108, 100], - b"hello\0world", - ), - ([8, 1, 2, 3, 102, 111, 117, 114, 5], b"\x01\x02\x03four\x05"), - ], - ) - def test_read_bytearray( - self, - read_bytes: list[int], - expected_bytes: bytes, - read_mock: ReadFunctionMock, - ): - """Test reading ASCII string results in correct bytes.""" - read_mock.combined_data = bytearray(read_bytes) - assert self.reader.read_bytearray() == expected_bytes - - @pytest.mark.parametrize( - ("read_bytes", "expected_string"), - [ - ([*list(map(ord, "test")), 0], "test"), - ([*list(map(ord, "a" * 100)), 0], "a" * 100), - ([0], ""), - ], - ) - def test_read_ascii( - self, - read_bytes: list[int], - expected_string: str, - read_mock: ReadFunctionMock, - ): - """Test reading ASCII string results in correct bytes.""" - read_mock.combined_data = bytearray(read_bytes) - assert self.reader.read_ascii() == expected_string - - @pytest.mark.parametrize( - ("read_bytes", "expected_string"), - [ - ([len("test"), *list(map(ord, "test"))], "test"), - ([len("a" * 100), *list(map(ord, "a" * 100))], "a" * 100), - ([0], ""), - ([18] + [int(x) for x in "नमस्ते".encode()], "नमस्ते"), - ], - ) - def test_read_utf( - self, - read_bytes: list[int], - expected_string: str, - read_mock: ReadFunctionMock, - ): - """Test reading UTF string results in correct values.""" - read_mock.combined_data = bytearray(read_bytes) - assert self.reader.read_utf() == expected_string - - @pytest.mark.skipif( - platform.system() == "Windows", - reason="environment variable limit on Windows", - ) - @pytest.mark.parametrize( - ("read_bytes"), - [ - [253, 255, 7], - [128, 128, 2, *list(map(ord, "a" * 32768))], - ], - # Temporary workaround. - # https://github.com/pytest-dev/pytest/issues/6881#issuecomment-596381626 - ids=["a", "b"], - ) - def test_read_utf_limit( - self, - read_bytes: list[int], - read_mock: ReadFunctionMock, - ): - """Test reading a UTF string too big raises an IOError.""" - read_mock.combined_data = bytearray(read_bytes) - with pytest.raises( - IOError, - match="^Maximum read limit for utf strings is ", - ): - self.reader.read_utf() - - def test_read_optional_true( - self, - method_mock: Mock | AsyncMock, - read_mock: ReadFunctionMock, - ): - """Test reading optional runs reader function when first bool is ``True``.""" - mock_f = method_mock() - read_mock.combined_data = bytearray([1]) - self.reader.read_optional(mock_f) - mock_f.assert_called_once_with() - - def test_read_optional_false( - self, - method_mock: Mock | AsyncMock, - read_mock: ReadFunctionMock, - ): - """Test reading optional doesn't run reader function when first bool is ``False``.""" - mock_f = method_mock() - read_mock.combined_data = bytearray([0]) - self.reader.read_optional(mock_f) - mock_f.assert_not_called() - - -# endregion -# region: Concrete test classes - - -class TestBaseSyncWriter(WriterTests): - """Tests for individual write methods implemented in :class:`~mcproto.protocol.base_io.BaseSyncWriter`.""" - - @classmethod - def setup_class(cls): - """Initialize writer instance to be tested.""" - cls.writer = SyncWriter() - - -class TestBaseSyncReader(ReaderTests): - """Tests for individual write methods implemented in :class:`~mcproto.protocol.base_io.BaseSyncReader`.""" - - @classmethod - def setup_class(cls): - """Initialize reader instance to be tested.""" - cls.reader = SyncReader() - - -class TestBaseAsyncWriter(WriterTests): - """Tests for individual write methods implemented in :class:`~mcproto.protocol.base_io.BaseSyncReader`.""" - - writer: WrappedAsyncWriter - - @classmethod - def setup_class(cls): - """Initialize writer instance to be tested.""" - cls.writer = WrappedAsyncWriter() - - -class TestBaseAsyncReader(ReaderTests): - """Tests for individual write methods implemented in :class:`~mcproto.protocol.base_io.BaseSyncReader`.""" - - reader: WrappedAsyncReader - - @classmethod - def setup_class(cls): - """Initialize writer instance to be tested.""" - cls.reader = WrappedAsyncReader() - - -# endregion diff --git a/tests/test_buffer.py b/tests/test_buffer.py deleted file mode 100644 index ac30774..0000000 --- a/tests/test_buffer.py +++ /dev/null @@ -1,97 +0,0 @@ -# This is the base_io module from https://github.com/py-mine/mcproto v0.5.0, -# which is licensed under the GNU LESSER GENERAL PUBLIC LICENSE v3.0 - -from __future__ import annotations - -__author__ = "ItsDrike" -__license__ = "LGPL-3.0-only" - -import pytest - -from azul.buffer import Buffer - - -def test_write() -> None: - """Writing into the buffer should store data.""" - buf = Buffer() - buf.write(b"Hello") - assert buf, bytearray(b"Hello") - - -def test_read() -> None: - """Reading from buffer should return stored data.""" - buf = Buffer(b"Reading is cool") - data = buf.read(len(buf)) - assert data == b"Reading is cool" - - -def test_read_multiple() -> None: - """Multiple reads should deplete the data.""" - buf = Buffer(b"Something random") - data = buf.read(9) - assert data == b"Something" - data = buf.read(7) - assert data == b" random" - - -def test_no_data_read() -> None: - """Reading more data than available should raise IOError.""" - buf = Buffer(b"Blip") - with pytest.raises( - IOError, - match="^Requested to read more data than available.", - ): - buf.read(len(buf) + 1) - - -def test_reset() -> None: - """Resetting should treat already read data as new unread data.""" - buf = Buffer(b"Will it reset?") - data = buf.read(len(buf)) - buf.reset() - data2 = buf.read(len(buf)) - assert data == data2 - assert data == b"Will it reset?" - - -def test_clear() -> None: - """Clearing should remove all stored data from buffer.""" - buf = Buffer(b"Will it clear?") - buf.clear() - assert buf == bytearray() - - -def test_clear_resets_position() -> None: - """Clearing should reset reading position for new data to be read.""" - buf = Buffer(b"abcdef") - buf.read(3) - buf.clear() - buf.write(b"012345") - data = buf.read(3) - assert data == b"012" - - -def test_clear_read_only() -> None: - """Clearing should allow just removing the already read data.""" - buf = Buffer(b"0123456789") - buf.read(5) - buf.clear(only_already_read=True) - assert buf == bytearray(b"56789") - - -def test_flush() -> None: - """Flushing should read all available data and clear out the buffer.""" - buf = Buffer(b"Foobar") - data = buf.flush() - assert data == b"Foobar" - assert buf == bytearray() - - -def test_remainig() -> None: - """Buffer should report correct amount of remaining bytes to be read.""" - buf = Buffer(b"012345") # 6 bytes to be read - assert buf.remaining == 6 - buf.read(2) - assert buf.remaining == 4 - buf.clear() - assert buf.remaining == 0 diff --git a/tests/test_component.py b/tests/test_component.py deleted file mode 100644 index d2ce838..0000000 --- a/tests/test_component.py +++ /dev/null @@ -1,370 +0,0 @@ -from __future__ import annotations - -import gc - -import pytest -import trio - -from azul.component import ( - Component, - ComponentManager, - Event, - ExternalRaiseManager, -) - - -def test_event_init() -> None: - event = Event("event_name", {"fish": 27}, 3) - assert event.name == "event_name" - assert event.data == {"fish": 27} - assert event.level == 3 - - -def test_event_pop_level() -> None: - event = Event("event_name", None, 3) - assert event.pop_level() - assert event.level == 2 - assert event.pop_level() - assert event.level == 1 - assert event.pop_level() - assert event.level == 0 - - assert not event.pop_level() - assert event.level == 0 - - -def test_event_repr() -> None: - assert repr(Event("cat_moved", (3, 3))) == "Event('cat_moved', (3, 3), 0)" - - -def test_component_init() -> None: - component = Component("component_name") - assert component.name == "component_name" - - -def test_component_repr() -> None: - assert repr(Component("fish")) == "Component('fish')" - - -def test_component_manager_property_error() -> None: - component = Component("waffle") - assert not component.manager_exists - with pytest.raises( - AttributeError, - match="^No component manager bound for", - ): - component.manager # noqa: B018 - - -def test_componentmanager_add_has_manager_property() -> None: - manager = ComponentManager("manager") - sound_effect = Component("sound_effect") - with pytest.raises(AttributeError): - print(sound_effect.manager) - manager.add_component(sound_effect) - assert manager.component_exists("sound_effect") - assert sound_effect.manager_exists - assert sound_effect.manager is manager - assert sound_effect.component_exists("sound_effect") - assert sound_effect.components_exist(("sound_effect",)) - assert not sound_effect.components_exist(("sound_effect", "waffle")) - assert manager.list_components() == ("sound_effect",) - assert sound_effect.get_component("sound_effect") is sound_effect - assert sound_effect.get_components(("sound_effect",)) == [sound_effect] - - -def test_componentmanager_manager_property_weakref_failure() -> None: - # Have to override __del__, unbind_components called and unbinds - # components so weakref failure branch never hit in normal - # circumstances - class EvilNoUnbindManager(ComponentManager): - def __del__(self) -> None: - return - - manager = EvilNoUnbindManager("manager") - sound_effect = Component("sound_effect") - with pytest.raises(AttributeError): - print(sound_effect.manager) - manager.add_component(sound_effect) - assert sound_effect.manager is manager - del manager - # make sure gc collects manager - for _ in range(3): - gc.collect() - with pytest.raises(AttributeError): - print(sound_effect.manager) - - -def test_double_bind_error() -> None: - manager = ComponentManager("manager") - sound_effect = Component("sound_effect") - manager.add_component(sound_effect) - manager_two = ComponentManager("manager_two") - with pytest.raises(RuntimeError, match="component is already bound to"): - manager_two.add_component(sound_effect) - - -def test_self_component() -> None: - manager = ComponentManager("manager", "cat_event") - assert manager.component_exists("cat_event") - assert manager.get_component("cat_event") is manager - - cat_event = Component("cat_event") - with pytest.raises(ValueError, match="already exists"): - manager.add_component(cat_event) - - -def test_add_multiple() -> None: - manager = ComponentManager("manager") - manager.add_components( - ( - Component("fish"), - Component("waffle"), - ), - ) - assert manager.component_exists("fish") - assert manager.component_exists("waffle") - - manager.unbind_components() - assert not manager.get_all_components() - - -def test_component_not_exist_error() -> None: - manager = ComponentManager("manager") - with pytest.raises(ValueError, match="does not exist"): - manager.remove_component("darkness") - with pytest.raises(ValueError, match="does not exist"): - manager.get_component("darkness") - - -@pytest.mark.trio -async def test_self_component_handler() -> None: - event_called = False - - async def event_call(event: Event[None]) -> None: - nonlocal event_called - assert event.name == "fish_appears_event" - event_called = True - - manager = ComponentManager("manager", "cat") - manager.register_handler("fish_appears_event", event_call) - - assert manager.has_handler("fish_appears_event") - - await manager.raise_event(Event("fish_appears_event", None)) - assert event_called - - -@pytest.mark.trio -async def test_raise_event_register_handlers_double_call() -> None: - event_called_count = 0 - - async def event_call(event: Event[int]) -> None: - nonlocal event_called_count - assert event.data == 27 - event_called_count += 1 - - manager = ComponentManager("manager") - assert not manager.has_handler("event_name") - - manager.register_component_handler("event_name", event_call, manager.name) - assert manager.has_handler("event_name") - await manager.raise_event(Event("event_name", 27)) - assert event_called_count == 1 - - event_called_count = 0 - - with pytest.raises(ValueError, match="is not registered!"): - manager.register_component_handler( - "event_name", - event_call, - "2nd name", - ) - manager.add_component(Component("2nd name")) - manager.register_component_handler("event_name", event_call, "2nd name") - - await manager.raise_event(Event("event_name", 27)) - assert event_called_count == 2 - - -@pytest.mark.trio -async def test_raise_event_register_handlers() -> None: - event_called = False - - async def event_call(event: Event[int]) -> None: - nonlocal event_called - assert event.data == 27 - event_called = True - - manager = ComponentManager("manager") - sound_effect = Component("sound_effect") - manager.add_component(sound_effect) - sound_effect.register_handlers({"event_name": event_call}) - - assert sound_effect.has_handler("event_name") - - await sound_effect.raise_event(Event("event_name", 27)) - assert event_called - - event_called = False - await manager.raise_event(Event("event_name", 27)) - assert event_called - - event_called = False - manager.remove_component("sound_effect") - with pytest.raises(AttributeError, match="No component manager bound for"): - await sound_effect.raise_event(Event("event_name", 27)) - await manager.raise_event(Event("event_name", 27)) - assert not event_called - - -@pytest.mark.trio -async def test_raise_leveled_comes_back() -> None: - event_called = False - - async def event_call(event: Event[int]) -> None: - nonlocal event_called - assert event.level == 0 - event_called = True - - event_called_two = False - - async def event_call_two(event: Event[int]) -> None: - nonlocal event_called_two - assert event.level == 0 - event_called_two = True - - super_manager = ComponentManager("super_manager") - manager = ComponentManager("manager") - - super_manager.add_component(manager) - assert super_manager.component_exists("manager") - - super_manager.register_handler("leveled_event", event_call) - manager.register_handler("leveled_event", event_call_two) - - await manager.raise_event(Event("leveled_event", None, 1)) - assert event_called - assert event_called_two - - -@pytest.mark.trio -async def test_raise_event_register_handler() -> None: - event_called = False - - async def event_call(event: Event[int]) -> None: - nonlocal event_called - assert event.data == 27 - event_called = True - - manager = ComponentManager("manager") - sound_effect = Component("sound_effect") - manager.add_component(sound_effect) - sound_effect.register_handler("event_name", event_call) - - await sound_effect.raise_event(Event("event_name", 27)) - assert event_called - - -@pytest.mark.trio -async def test_raises_event_in_nursery() -> None: - nursery_called = False - event_called = False - - async def call_bean(event: Event[None]) -> None: - nonlocal event_called - assert event.name == "bean_event" - event_called = True - - async with trio.open_nursery() as nursery: - original = nursery.start_soon - - def replacement(*args: object, **kwargs: object) -> object: - nonlocal nursery_called - nursery_called = True - return original(*args, **kwargs) - - nursery.start_soon = replacement - - manager = ExternalRaiseManager("manager", nursery) - manager.register_handler("bean_event", call_bean) - await manager.raise_event(Event("bean_event", None)) - assert nursery_called - assert event_called - - -@pytest.mark.trio -async def test_internal_does_not_raise_event_in_nursery() -> None: - nursery_called = False - event_called = False - - async def call_bean(event: Event[None]) -> None: - nonlocal event_called - assert event.name == "bean_event" - event_called = True - - async with trio.open_nursery() as nursery: - original = nursery.start_soon - - def replacement(*args: object, **kwargs: object) -> object: - nonlocal nursery_called - nursery_called = True - return original(*args, **kwargs) - - nursery.start_soon = replacement - - manager = ExternalRaiseManager("manager", nursery) - manager.register_handler("bean_event", call_bean) - await manager.raise_event_internal(Event("bean_event", None)) - assert not nursery_called - assert event_called - - -@pytest.mark.trio -async def test_temporary_component() -> None: - event_called = False - - async def event_call(event: Event[int]) -> None: - nonlocal event_called - assert event.data == 27 - event_called = True - - manager = ComponentManager("manager") - with manager.temporary_component( - Component("sound_effect"), - ) as sound_effect: - assert manager.component_exists("sound_effect") - sound_effect.register_handler("event_name", event_call) - - await sound_effect.raise_event(Event("event_name", 27)) - assert event_called - assert not manager.component_exists("sound_effect") - with manager.temporary_component( - Component("sound_effect"), - ) as sound_effect: - manager.remove_component("sound_effect") - - -@pytest.mark.trio -async def test_remove_component() -> None: - event_called = False - - async def event_call(event: Event[int]) -> None: - nonlocal event_called - assert event.data == 27 - event_called = True - - manager = ComponentManager("manager") - sound_effect = Component("sound_effect") - manager.add_component(sound_effect) - assert manager.component_exists("sound_effect") - sound_effect.register_handler("event_name", event_call) - sound_effect.register_handler("waffle_name", event_call) - - await sound_effect.raise_event(Event("event_name", 27)) - assert event_called - manager.add_component(Component("jerald")) - manager.register_handler("event_name", event_call) - manager.remove_component("jerald") - manager.remove_component("sound_effect") - assert not manager.component_exists("sound_effect") diff --git a/tests/test_encrypted_event.py b/tests/test_encrypted_event.py deleted file mode 100644 index aaa837d..0000000 --- a/tests/test_encrypted_event.py +++ /dev/null @@ -1,79 +0,0 @@ -from __future__ import annotations - -import pytest -import trio -import trio.testing - -from azul.component import Event -from azul.encrypted_event import EncryptedNetworkEventComponent - - -@pytest.mark.trio -async def test_event_transmission() -> None: - one, two = trio.testing.memory_stream_pair() - client_one = EncryptedNetworkEventComponent.from_stream("one", stream=one) - client_two = EncryptedNetworkEventComponent.from_stream("two", stream=two) - - client_one.register_network_write_event("echo_event", 0) - client_two.register_read_network_event(0, "reposted_event") - - event = Event( - "echo_event", - bytearray("I will give my cat food to bob", "utf-8"), - 3, - ) - - await client_one.write_event(event) - read_event = await client_two.read_event() - assert read_event.name == "reposted_event" - assert read_event.data == event.data - - await client_one.close() - await client_two.close() - - -@pytest.mark.trio -async def test_event_encrypted_transmission() -> None: - verification_token = bytes.fromhex("da053623dd3dcd441e105ee5ce212ac8") - shared_secret = bytes.fromhex( - "95a883358f09cd5698b3cf8a414a8a659a35c4eb877e9b0228b7f64df85b0f26", - ) - - one, two = trio.testing.memory_stream_pair() - client_one = EncryptedNetworkEventComponent.from_stream("one", stream=one) - client_two = EncryptedNetworkEventComponent.from_stream("two", stream=two) - - client_one.register_network_write_event("echo_event", 0) - client_two.register_read_network_event(0, "reposted_event") - - event = Event( - "echo_event", - bytearray("I will give my cat food to bob", "utf-8"), - 3, - ) - - await client_one.write_event(event) - read_event = await client_two.read_event() - assert read_event.name == "reposted_event" - assert read_event.data == event.data - - await client_one.write_event(event) - assert ( - await two.receive_some() == b"\x00\x1eI will give my cat food to bob" - ) - - client_one.enable_encryption(shared_secret, verification_token) - client_two.enable_encryption(shared_secret, verification_token) - - await client_one.write_event(event) - read_event = await client_two.read_event() - assert read_event.name == "reposted_event" - assert read_event.data == event.data - - await client_one.write_event(event) - assert await two.receive_some() == bytearray.fromhex( - "2bb572309dfb71d22eb5f0442c5347f2d666ed16c97093190a8101c3e59f2beb", - ) - - await client_one.close() - await client_two.close() diff --git a/tests/test_encryption.py b/tests/test_encryption.py deleted file mode 100644 index 4f588c0..0000000 --- a/tests/test_encryption.py +++ /dev/null @@ -1,108 +0,0 @@ -# This is the buffer module from https://github.com/py-mine/mcproto v0.5.0, -# which is licensed under the GNU LESSER GENERAL PUBLIC LICENSE v3.0 - -from __future__ import annotations - -__author__ = "ItsDrike" -__license__ = "LGPL-3.0-only" - -from typing import cast - -from cryptography.hazmat.primitives.asymmetric.padding import MGF1, OAEP -from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey -from cryptography.hazmat.primitives.hashes import SHA256 -from cryptography.hazmat.primitives.serialization import load_pem_private_key - -from azul.encryption import ( - decrypt_token_and_secret, - deserialize_public_key, - encrypt_token_and_secret, - serialize_public_key, -) - -_SERIALIZED_RSA_PRIVATE_KEY = b""" ------BEGIN PRIVATE KEY----- -MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAMtRUQmRHqPkdA2K -F6fM2c8ibIPHYV5KVQXNEkVx7iEKS6JsfELhX1H8t/qQ3Ob4Pr4OFjgXx9n7GvfZ -gekNoswG6lnQH/n7t2sYA 6D+WvSix1FF2J6wPmpKriHS59TDk4opjaV14S4K4XjW -Gmm8DqCzgXkPGC2dunFb+1A8mdkrAgMBAAECgYAWj2dWkGu989OMzQ3i6LAic8dm -t/Dt7YGRqzejzQiHUgUieLcxFKDnEAu6GejpGBKeNCHzB3B9l4deiRwJKCIwHqMN -LKMKoayinA8mj/Y/ O/ELDofkEyeXOhFyM642sPpaxQJoNWc9QEsYbxpG2zeB3sPf -l3eIhkYTKVdxB+o8AQJBAPiddMjU8fuHyjKT6VCL2ZQbwnrRe1AaLLE6VLwEZuZC -wlbx5Lcszi77PkMRTvltQW39VN6MEjiYFSPtRJleA+sCQQDRW2e3BX6uiil2IZ08 -tPFMnltFJpa 8YvW50N6mySd8Zg1oQJpzP2fC0n0+K4j3EiA/Zli8jBt45cJ4dMGX -km/BAkEAtkYy5j+BvolbDGP3Ti+KcRU9K/DD+QGHvNRoZYTQsIdHlpk4t7eo3zci -+ecJwMOCkhKHE7cccNPHxBRkFBGiywJAJBt2pMsu0R2FDxm3C6xNXaCGL0P7hVwv -8y9B51 QUGlFjiJJz0OKjm6c/8IQDqFEY/LZDIamsZ0qBItNIPEMGQQJALZV0GD5Y -zmnkw1hek/JcfQBlVYo3gFmWBh6Hl1Lb7p3TKUViJCA1k2f0aGv7+d9aFS0fRq6u -/sETkem8Jc1s3g== ------END PRIVATE KEY----- -""" -RSA_PRIVATE_KEY = cast( - RSAPrivateKey, - load_pem_private_key(_SERIALIZED_RSA_PRIVATE_KEY, password=None), -) -RSA_PUBLIC_KEY = RSA_PRIVATE_KEY.public_key() -SERIALIZED_RSA_PUBLIC_KEY = bytes.fromhex( - "30819f300d06092a864886f70d010101050003818d0030818902818100cb515109911ea3e4740d8a17a7ccd9cf226c83c7615e4a5505cd124571ee210a4ba26c7c42e15f51fcb7fa90dce6f83ebe0e163817c7d9fb1af7d981e90da2cc06ea59d01ff9fbb76b1803a0fe5af4a2c75145d89eb03e6a4aae21d2e7d4c3938a298da575e12e0ae178d61a69bc0ea0b381790f182d9dba715bfb503c99d92b0203010001", -) - - -def test_encrypt_token_and_secret() -> None: - """Test encryption returns properly encrypted (decryptable) values.""" - verification_token = bytes.fromhex("da053623dd3dcd441e105ee5ce212ac8") - shared_secret = bytes.fromhex( - "95a883358f09cd5698b3cf8a414a8a659a35c4eb877e9b0228b7f64df85b0f26", - ) - - encrypted_token, encrypted_secret = encrypt_token_and_secret( - RSA_PUBLIC_KEY, - verification_token, - shared_secret, - ) - - assert ( - RSA_PRIVATE_KEY.decrypt( - encrypted_token, - OAEP(MGF1(SHA256()), SHA256(), None), - ) - == verification_token - ) - assert ( - RSA_PRIVATE_KEY.decrypt( - encrypted_secret, - OAEP(MGF1(SHA256()), SHA256(), None), - ) - == shared_secret - ) - - -def test_decrypt_token_and_secret() -> None: - """Test decryption returns properly decrypted values.""" - encrypted_token = bytes.fromhex( - "5541c0c0fc99d8908ed428b20c260795bec7b4041a4f98d26fbed383e8dba077eb53fb5cf905e722e2ceb341843e875508134817bcd3a909ac279e77ed94fd98c428bbe00db630a5ad3df310380d9274ed369cc6a011e7edd45cbe44ae8ad2575ef793b23057e4b15f1b6e3e195ff0921e46370773218517922fbb8b96092d88", - ) - encrypted_secret = bytes.fromhex( - "1a43782ca17f71e87e6ef98f9be66050ecf5d185da81445d26ceb5941f95d69d61b726d27b5ca62aed4cbe27b40fd4bd6b16b5be154a7b6a24ae31c705bc47d9397589b448fb72b14572ea2a9d843c6a3c674b7454cef97e2d65be36e0d0a8cc9f1093a19a8d52a5633a5317d19779bb46146dfaea7a690a7f080fb77d59c7f9", - ) - - assert decrypt_token_and_secret( - RSA_PRIVATE_KEY, - encrypted_token, - encrypted_secret, - ) == ( - bytes.fromhex("da053623dd3dcd441e105ee5ce212ac8"), - bytes.fromhex( - "95a883358f09cd5698b3cf8a414a8a659a35c4eb877e9b0228b7f64df85b0f26", - ), - ) - - -def test_serialize_public_key() -> None: - """Test serialize_public_key.""" - assert serialize_public_key(RSA_PUBLIC_KEY) == SERIALIZED_RSA_PUBLIC_KEY - - -def test_deserialize_public_key() -> None: - """Test deserialize_public_key.""" - assert deserialize_public_key(SERIALIZED_RSA_PUBLIC_KEY) == RSA_PUBLIC_KEY diff --git a/tests/test_network.py b/tests/test_network.py deleted file mode 100644 index 0273bff..0000000 --- a/tests/test_network.py +++ /dev/null @@ -1,178 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -import pytest -import trio -import trio.testing - -from azul.component import ComponentManager, Event -from azul.network import ( - NetworkComponent, - NetworkEventComponent, - NetworkStreamNotConnectedError, - NetworkTimeoutError, - Server, -) - -if TYPE_CHECKING: - from collections.abc import Callable - - -@pytest.mark.trio -async def client_connect(port: int, stop_server: Callable[[], None]) -> None: - await trio.sleep(0.05) - # manager = ComponentManager("manager") - - client = NetworkEventComponent("client") - # manager.add_component(client) - - await client.connect("127.0.0.1", port) - - client.register_network_write_event("echo_event", 0) - client.register_read_network_event(1, "reposted_event") - - event = Event( - "echo_event", - bytearray("I will give my cat food to bob", "utf-8"), - 3, - ) - - # await client.raise_event(event) - await client.write_event(event) - print(f"{await client.read_event() = }") - - await client.close() - stop_server() - - -@pytest.mark.trio -async def run_async() -> None: - class TestServer(Server): - async def handler(self, stream: trio.SocketStream) -> None: - client = NetworkEventComponent.from_stream("client", stream=stream) - - client.register_read_network_event(0, "repost_event") - client.register_network_write_event("repost_event", 1) - - await client.write_event(await client.read_event()) - await stream.aclose() - - server = TestServer("server") - port = 3004 - async with trio.open_nursery() as nursery: - nursery.start_soon(server.serve, port) - nursery.start_soon(client_connect, port, server.stop_serving) - nursery.start_soon(client_connect, port, server.stop_serving) - - -def test_not_connected() -> None: - client = NetworkComponent("name") - assert client.not_connected - with pytest.raises(NetworkStreamNotConnectedError): - print(client.stream) - - -@pytest.mark.trio -async def test_from_stream() -> None: - stream = trio.testing.MemorySendStream() - - named = NetworkComponent.from_stream( - kwargs={"name": "name"}, - stream=stream, - ) - with pytest.raises(RuntimeError, match="Already connected!"): - await named.connect("example.com", 80) - await named.close() - - -@pytest.mark.trio -async def test_register_network_write_event() -> None: - client = NetworkEventComponent("client") - client.register_network_write_event("echo_event", 0) - with pytest.raises(ValueError, match="event already registered"): - client.register_network_write_event("echo_event", 0) - client.register_read_network_event(0, "reposted_event") - with pytest.raises(ValueError, match="events are also being received"): - client.register_network_write_events({"reposted_event": 0}) - with pytest.raises(RuntimeError, match="Unhandled network event name"): - await client.write_event(Event("jerald event", bytearray())) - client.register_network_write_events({}) - - -@pytest.mark.trio -async def test_register_network_read_event() -> None: - one, two = trio.testing.memory_stream_pair() - client_one = NetworkEventComponent.from_stream("one", stream=one) - client_two = NetworkEventComponent.from_stream("two", stream=two) - client_one.register_network_write_event("echo_event", 0) - await client_one.write_event( - Event( - "echo_event", - bytearray("I will give my cat food to bob", "utf-8"), - ), - ) - with pytest.raises(RuntimeError, match="Unhandled packet ID 0"): - await client_two.read_event() - with pytest.raises(ValueError, match="Packet id 0 packets are also"): - client_one.register_read_network_event(0, "echo_event") - client_two.register_read_network_event(0, "reposted_event") - with pytest.raises(ValueError, match="Packet ID 0 already registered!"): - client_two.register_read_network_events({0: "type_two"}) - client_two.register_read_network_events({}) - - -@pytest.mark.trio -async def test_event_transmission() -> None: - one, two = trio.testing.memory_stream_pair() - client_one = NetworkEventComponent.from_stream("one", stream=one) - manager = ComponentManager("manager") - async with NetworkEventComponent.from_stream( - "two", - stream=two, - ) as client_two: - manager.add_component(client_one) - - assert not client_one.not_connected - - client_one.register_network_write_event("echo_event", 0) - client_two.register_read_network_event(0, "reposted_event") - - event = Event( - "echo_event", - bytearray("I will give my cat food to bob", "utf-8"), - 3, - ) - - await client_one.write_event(event) - read_event = await client_two.read_event() - assert read_event.name == "reposted_event" - assert read_event.data == event.data - - await client_one.write_event(event) - assert ( - await two.receive_some() - == b"\x00\x1eI will give my cat food to bob" - ) - - await client_one.wait_write_might_not_block() - await one.send_all(b"") - client_two.timeout = 0.05 - with pytest.raises(NetworkTimeoutError): - await client_two.read_event() - await one.send_all(b"cat") - with pytest.raises(OSError, match="Server stopped responding"): - await client_two.read(4) - - await client_one.send_eof() - await client_one.send_eof() - - await client_one.close() - await client_one.close() - - -def test_server() -> None: - server = Server("name") - server.stop_serving() - server.serve_cancel_scope = trio.CancelScope() - server.stop_serving() diff --git a/tests/test_utils.py b/tests/test_utils.py deleted file mode 100644 index 37b40b4..0000000 --- a/tests/test_utils.py +++ /dev/null @@ -1,121 +0,0 @@ -# This is the base_io module from https://github.com/py-mine/mcproto v0.5.0, -# which is licensed under the GNU LESSER GENERAL PUBLIC LICENSE v3.0 - -from __future__ import annotations - -__author__ = "ItsDrike" -__license__ = "LGPL-3.0-only" - -import pytest - -from azul.utils import from_twos_complement, to_twos_complement - -# TODO: Consider adding tests for enforce_range - - -@pytest.mark.parametrize( - ("number", "bits", "expected_out"), - [ - (0, 8, 0), - (1, 8, 1), - (10, 8, 10), - (127, 8, 127), - ], -) -def test_to_twos_complement_positive( - number: int, - bits: int, - expected_out: int, -): - """Test conversion to two's complement format from positive numbers gives expected result.""" - assert to_twos_complement(number, bits) == expected_out - - -@pytest.mark.parametrize( - ("number", "bits", "expected_out"), - [ - (-1, 8, 255), - (-10, 8, 246), - (-128, 8, 128), - ], -) -def test_to_twos_complement_negative( - number: int, - bits: int, - expected_out: int, -): - """Test conversion to two's complement format of negative numbers gives expected result.""" - assert to_twos_complement(number, bits) == expected_out - - -@pytest.mark.parametrize( - ("number", "bits"), - [ - (128, 8), - (-129, 8), - (32768, 16), - (-32769, 16), - (2147483648, 32), - (-2147483649, 32), - (9223372036854775808, 64), - (-9223372036854775809, 64), - ], -) -def test_to_twos_complement_range(number: int, bits: int): - """Test conversion to two's complement format for out of range numbers raises :exc:`ValueError`.""" - with pytest.raises(ValueError, match="out of range"): - to_twos_complement(number, bits) - - -@pytest.mark.parametrize( - ("number", "bits", "expected_out"), - [ - (0, 8, 0), - (1, 8, 1), - (10, 8, 10), - (127, 8, 127), - ], -) -def test_from_twos_complement_positive( - number: int, - bits: int, - expected_out: int, -): - """Test conversion from two's complement format of positive numbers give expected result.""" - assert from_twos_complement(number, bits) == expected_out - - -@pytest.mark.parametrize( - ("number", "bits", "expected_out"), - [ - (255, 8, -1), - (246, 8, -10), - (128, 8, -128), - ], -) -def test_from_twos_complement_negative( - number: int, - bits: int, - expected_out: int, -): - """Test conversion from two's complement format of negative numbers give expected result.""" - assert from_twos_complement(number, bits) == expected_out - - -@pytest.mark.parametrize( - ("number", "bits"), - [ - (256, 8), - (-1, 8), - (65536, 16), - (-1, 16), - (4294967296, 32), - (-1, 32), - (18446744073709551616, 64), - (-1, 64), - ], -) -def test_from_twos_complement_range(number: int, bits: int): - """Test conversion from two's complement format for out of range numbers raises :exc:`ValueError`.""" - with pytest.raises(ValueError, match="out of range"): - from_twos_complement(number, bits) From 86f4d2bf6225d39b1b10a605645d6ebbfaf645b0 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Mon, 25 Nov 2024 12:49:51 -0600 Subject: [PATCH 05/58] Add more tests from catto invasion and upgrade gitignore --- .gitignore | 28 ++++- pyproject.toml | 6 + src/azul/sprite.py | 5 +- test-requirements.in | 2 +- tests/test_async_clock.py | 49 ++++++-- tests/test_objects.py | 81 ++++++++++++ tests/test_sprite.py | 255 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 411 insertions(+), 15 deletions(-) create mode 100644 tests/test_objects.py create mode 100644 tests/test_sprite.py diff --git a/.gitignore b/.gitignore index 8d514aa..a55ab68 100644 --- a/.gitignore +++ b/.gitignore @@ -86,6 +86,7 @@ instance/ docs/_build/ # PyBuilder +.pybuilder/ target/ # Jupyter Notebook @@ -96,6 +97,8 @@ profile_default/ ipython_config.py # pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: .python-version # pipenv @@ -105,6 +108,21 @@ ipython_config.py # install all needed dependencies. #Pipfile.lock +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ @@ -139,11 +157,15 @@ venv.bak/ .dmypy.json dmypy.json -# Pyre type checker +# Pyre static type analyzer .pyre/ -# Sphinx documentation -doc/_build/ +# Cython debug symbols +cython_debug/ # PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. .idea/ diff --git a/pyproject.toml b/pyproject.toml index b8a5cce..53b856d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,10 +62,16 @@ azul = ["py.typed", "data/*"] plugins = ["numpy.typing.mypy_plugin"] files = ["src/azul/",] check_untyped_defs = true +disallow_any_decorated = true disallow_any_generics = true +disallow_any_unimported = true +disallow_incomplete_defs = true +disallow_subclassing_any = true disallow_untyped_calls = true +disallow_untyped_decorators = true disallow_untyped_defs = true ignore_missing_imports = true +local_partial_types = true no_implicit_optional = true no_implicit_reexport = true show_column_numbers = true diff --git a/src/azul/sprite.py b/src/azul/sprite.py index fbd1915..4cff67c 100644 --- a/src/azul/sprite.py +++ b/src/azul/sprite.py @@ -693,13 +693,12 @@ class GroupProcessor(AsyncStateMachine): __slots__ = ("_clear", "_timing", "group_names", "groups", "new_gid") sub_renderer_class: ClassVar = LayeredDirty - groups: dict[int, sub_renderer_class] def __init__(self) -> None: """Initialize group processor.""" super().__init__() - self.groups = {} + self.groups: dict[int, LayeredDirty[Sprite]] = {} self.group_names: dict[str, int] = {} self.new_gid = 0 self._timing = 1000 / 80 @@ -751,7 +750,7 @@ def remove_group(self, gid: int) -> None: del self.group_names[name] return - def get_group(self, gid_name: str | int) -> sub_renderer_class | None: + def get_group(self, gid_name: str | int) -> LayeredDirty[Sprite] | None: """Return group from group ID or name.""" named = None if isinstance(gid_name, str): diff --git a/test-requirements.in b/test-requirements.in index 6c3b0de..0094b39 100644 --- a/test-requirements.in +++ b/test-requirements.in @@ -1,5 +1,5 @@ # For tests -pytest >= 5.0 # for faulthandler in core +pytest >= 5.0 coverage >= 7.2.5 pytest-trio pytest-cov diff --git a/tests/test_async_clock.py b/tests/test_async_clock.py index aac40bd..7c97954 100644 --- a/tests/test_async_clock.py +++ b/tests/test_async_clock.py @@ -1,27 +1,60 @@ +from __future__ import annotations + import pytest -from azul import async_clock +from azul.async_clock import Clock + + +@pytest.fixture +def clock() -> Clock: + return Clock() + + +def test_initial_values(clock: Clock) -> None: + assert clock.fps == 0.0 + assert clock.fps_count == 0 + + +def test_get_fps(clock: Clock) -> None: + assert clock.get_fps() == 0.0 + + +def test_get_rawtime(clock: Clock) -> None: + assert clock.get_rawtime() == 0 + + +def test_get_time(clock: Clock) -> None: + assert clock.get_time() == 0 @pytest.mark.trio -async def test_tick() -> None: - clock = async_clock.Clock() +async def test_tick_elasped(clock: Clock) -> None: + time_passed = await clock.tick() + assert time_passed >= 0 + + # Test with a specific framerate + time_passed = await clock.tick(60) + assert time_passed >= int(1e9 // 60) + # Test with a zero framerate + time_passed = await clock.tick(0) + assert time_passed >= 0 + + +@pytest.mark.trio +async def test_tick(clock: Clock) -> None: await clock.tick(60) result = await clock.tick(60) assert isinstance(result, int) - assert result >= 0 + assert result >= int(1e9 // 60) assert repr(clock).startswith(" None: - clock = async_clock.Clock() - +async def test_tick_fps(clock: Clock) -> None: for _ in range(20): await clock.tick(60) fps = clock.get_fps() diff --git a/tests/test_objects.py b/tests/test_objects.py new file mode 100644 index 0000000..12e2b56 --- /dev/null +++ b/tests/test_objects.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +import pytest +from pygame.surface import Surface + +from azul.objects import Button, OutlinedText, Text + + +class MockSurface(Surface): + """Mocking a pygame surface for testing.""" + + __slots__ = ("text_data",) + + def __init__(self, text_data: str = "") -> None: + super().__init__((0, 0)) + self.text_data = text_data + + +class MockFont: + """Mocking a pygame font for testing.""" + + __slots__ = () + + def render( + self, + text: str, + antialias: bool, + color: tuple[int, int, int], + ) -> str: + """Fake render method.""" + return MockSurface(text) + + +@pytest.fixture +def font() -> MockFont: + return MockFont() + + +def test_text_initialization(font: MockFont) -> None: + text = Text("TestText", font) + assert text.text == "None" + assert text.color == (255, 255, 255) + assert text.font == font + + +def test_text_rendering(font: MockFont) -> None: + text = Text("TestText", font) + assert text.image is None + + +def test_text_rendering_blank(font: MockFont) -> None: + text = Text("TestText", font) + text.text = "" + text.text = "" + assert text.image.text_data == "" + + +def test_outlined_text_initialization(font: MockFont) -> None: + outlined_text = OutlinedText("TestOutlinedText", font) + assert outlined_text.outline == (0, 0, 0) + assert outlined_text.inside == (255, 255, 255) + + +def test_outlined_text_rendering(font: MockFont) -> None: + outlined_text = OutlinedText("TestOutlinedText", font) + outlined_text.text = "Outlined Text" + assert outlined_text.text == "Outlined Text" + + +def test_outlined_text_rendering_zero_border(font: MockFont) -> None: + outlined_text = OutlinedText("TestOutlinedText", font) + outlined_text.border_width = 0 + outlined_text.text = "Outlined Text" + assert isinstance(outlined_text.image, Surface) + + +def test_button_initialization(font: MockFont) -> None: + button = Button("TestButton", font) + assert button.text == "None" + assert button.color == (0, 0, 0, 255) + assert button.border_width == 3 diff --git a/tests/test_sprite.py b/tests/test_sprite.py new file mode 100644 index 0000000..f2bda6c --- /dev/null +++ b/tests/test_sprite.py @@ -0,0 +1,255 @@ +from __future__ import annotations + +import pytest +import trio +from libcomponent.component import Event +from pygame.rect import Rect +from pygame.surface import Surface + +from azul.sprite import ( + AnimationComponent, + DragClickEventComponent, + GroupProcessor, + ImageComponent, + MovementComponent, + OutlineComponent, + Sprite, + TargetingComponent, + TickEventData, +) +from azul.vector import Vector2 + + +@pytest.fixture +def sprite() -> Sprite: + return Sprite("test_sprite") + + +@pytest.fixture +def image_component(sprite: Sprite) -> ImageComponent: + sprite.add_component(ImageComponent()) + return sprite.get_component("image") + + +@pytest.fixture +def animation_component(image_component: ImageComponent) -> AnimationComponent: + return image_component.get_component("animation") + + +@pytest.fixture +def outline_component(image_component: ImageComponent) -> OutlineComponent: + return image_component.get_component("outline") + + +@pytest.fixture +def movement_component(sprite: Sprite) -> MovementComponent: + sprite.add_component(MovementComponent()) + return sprite.get_component("movement") + + +@pytest.fixture +def targeting_component( + movement_component: MovementComponent, +) -> TargetingComponent: + sprite = movement_component.manager + sprite.add_component(TargetingComponent()) + return sprite.get_component("targeting") + + +@pytest.fixture +def drag_click_event_component() -> DragClickEventComponent: + return DragClickEventComponent() + + +@pytest.fixture +def group_processor() -> GroupProcessor: + return GroupProcessor() + + +def test_sprite_init(sprite: Sprite) -> None: + assert sprite.name == "test_sprite" + assert not sprite.visible + assert sprite.rect == Rect(0, 0, 0, 0) + + +def test_sprite_location(sprite: Sprite) -> None: + sprite.location = (10, 20) + assert sprite.rect.center == (10, 20) + + +def test_sprite_repr(sprite: Sprite) -> None: + assert repr(sprite) == "" + + +def test_sprite_image(sprite: Sprite) -> None: + sprite.dirty = 0 + assert sprite.image is None + assert not sprite.dirty + sprite.image = Surface((10, 10)) + assert isinstance(sprite.image, Surface) + assert sprite.dirty + assert sprite.rect.size == (10, 10) + + +def test_sprite_image_set_none(sprite: Sprite) -> None: + sprite.dirty = 0 + assert sprite.image is None + assert not sprite.dirty + sprite.image = None + assert sprite.dirty + + +def test_sprite_image_no_set_location_change(sprite: Sprite) -> None: + sprite.update_location_on_resize = False + sprite.location = (100, 100) + sprite.image = Surface((50, 25)) + assert sprite.location == (125, 112) + + +def test_sprite_image_set_location_change(sprite: Sprite) -> None: + sprite.update_location_on_resize = True + sprite.location = (100, 100) + sprite.image = Surface((50, 25)) + assert sprite.location == (100, 100) + + +def test_image_component_init(image_component: ImageComponent) -> None: + assert image_component.mask_threshold == 127 + + +def test_image_component_add_image(image_component: ImageComponent) -> None: + image = Surface((10, 10)) + image_component.add_image("test_image", image) + assert "test_image" in image_component.list_images() + + +def test_image_component_add_image_and_mask_invalid_image( + image_component: ImageComponent, +) -> None: + with pytest.raises( + ValueError, + match="^Expected surface to be a valid identifier$", + ): + image_component.add_image_and_mask("test_image", None, None) # type: ignore[arg-type] + with pytest.raises( + ValueError, + match="^Expected surface to be a valid identifier$", + ): + image_component.add_image_and_mask("test_image", "copy_from", None) # type: ignore[arg-type] + + +def test_image_component_add_image_and_mask_invalid_mask( + image_component: ImageComponent, +) -> None: + image = Surface((1, 1)) + with pytest.raises( + ValueError, + match="^Expected mask to be a valid identifier$", + ): + image_component.add_image_and_mask("test_image", image, None) # type: ignore[arg-type] + with pytest.raises( + ValueError, + match="^Expected mask to be a valid identifier$", + ): + image_component.add_image_and_mask("test_image", image, "copy_from") # type: ignore[arg-type] + + +def test_image_component_get_image(image_component: ImageComponent) -> None: + image = Surface((1, 1)) + image_component.add_image("test_image", image) + assert image_component.get_image("test_image") is image + + +def test_image_component_add_image_duplication( + image_component: ImageComponent, +) -> None: + image = Surface((1, 1)) + image_component.add_image("test_image", image) + image_component.add_image("duplicate", "test_image") + assert image_component.get_image("duplicate") is image + + +def test_movement_component_init( + movement_component: MovementComponent, +) -> None: + assert movement_component.heading == Vector2(0, 0) + assert movement_component.speed == 0 + + +def test_movement_component_point_toward( + movement_component: MovementComponent, +) -> None: + movement_component.point_toward((10, 20)) + assert ( + movement_component.heading + == Vector2.from_points((0, 0), (10, 20)).normalized() + ) + + +def test_movement_component_move_heading_time( + movement_component: MovementComponent, +) -> None: + movement_component.speed = 5 + movement_component.move_heading_time(1) + assert movement_component.heading * 5 == movement_component.heading + + +def test_targeting_component_init( + targeting_component: TargetingComponent, +) -> None: + assert targeting_component.destination == Vector2(0, 0) + assert targeting_component.event_raise_name == "reached_destination" + + +def test_targeting_component_update_heading( + targeting_component: TargetingComponent, +) -> None: + targeting_component.destination = Vector2(10, 20) + targeting_component.update_heading() + assert targeting_component.to_destination() == Vector2.from_points( + (0, 0), + (10, 20), + ) + + +@pytest.mark.trio +async def test_targeting_component_move_destination_time( + targeting_component: TargetingComponent, +) -> None: + movement_component = targeting_component.get_component("movement") + movement_component.speed = 1 + targeting_component.destination = Vector2(10, 20) + current_distance = targeting_component.to_destination().magnitude() + await targeting_component.move_destination_time(1) + assert targeting_component.to_destination().magnitude() < current_distance + + +def test_drag_click_event_component_init( + drag_click_event_component: DragClickEventComponent, +) -> None: + assert drag_click_event_component.pressed == {} + + +def test_group_processor_init(group_processor: GroupProcessor) -> None: + assert group_processor.groups == {} + assert group_processor.group_names == {} + assert group_processor.new_gid == 0 + + +def test_group_processor_new_group(group_processor: GroupProcessor) -> None: + gid = group_processor.new_group("test_group") + assert gid in group_processor.groups + assert "test_group" in group_processor.group_names + + +@pytest.mark.trio +async def test_animation_component_tick( + animation_component: AnimationComponent, +) -> None: + async with trio.open_nursery() as nursery: + nursery.start_soon( + animation_component.tick, + Event("tick", TickEventData(time_passed=1, fps=60)), + ) + await trio.lowlevel.checkpoint() + # Assert that the animation component has updated correctly From bd061ab5a4eb64506491959f19422ad8eaa2f2e6 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Mon, 25 Nov 2024 23:15:45 -0600 Subject: [PATCH 06/58] Work on manual wall tiling and start server side work --- src/azul/errorbox.py | 2 +- src/azul/game.py | 4 +- src/azul/network_shared.py | 41 ++++--- src/azul/server.py | 45 +++++--- src/azul/state.py | 211 +++++++++++++++++++++++++++++++++++-- 5 files changed, 258 insertions(+), 45 deletions(-) diff --git a/src/azul/errorbox.py b/src/azul/errorbox.py index 6c6b3e4..3fcb40c 100644 --- a/src/azul/errorbox.py +++ b/src/azul/errorbox.py @@ -52,7 +52,7 @@ def __wxpython(title: str, message: str) -> None: """Error with wxPython.""" from wxPython.wx import wxApp, wxICON_EXCLAMATION, wxMessageDialog, wxOK - class LameApp(wxApp): # type: ignore[misc] + class LameApp(wxApp): # type: ignore[misc,no-any-unimported] __slots__ = () def OnInit(self) -> int: # noqa: N802 diff --git a/src/azul/game.py b/src/azul/game.py index db25d88..db0bc45 100644 --- a/src/azul/game.py +++ b/src/azul/game.py @@ -2516,9 +2516,9 @@ def __init__(self) -> None: self.host_mode = True self.variant_play = False - def entry_actions(self) -> None: + async def entry_actions(self) -> None: """Add cursor object and tons of button and text objects to the game.""" - super().entry_actions() + await super().entry_actions() def add_numbers( start: int, diff --git a/src/azul/network_shared.py b/src/azul/network_shared.py index aa30f5d..862ff9f 100644 --- a/src/azul/network_shared.py +++ b/src/azul/network_shared.py @@ -24,9 +24,12 @@ __license__ = "GNU General Public License Version 3" +from collections import Counter from enum import IntEnum, auto -from typing import Final, NamedTuple, TypeAlias +from typing import Final, TypeAlias +from libcomponent.base_io import StructFormat +from libcomponent.buffer import Buffer from mypy_extensions import u8 ADVERTISEMENT_IP: Final = "224.0.2.60" @@ -37,11 +40,28 @@ Pos: TypeAlias = tuple[u8, u8] -class TickEventData(NamedTuple): - """Tick Event Data.""" +def encode_numeric_uint8_counter(counter: Counter[int]) -> Buffer: + """Return buffer from uint8 counter (both keys and values).""" + buffer = Buffer() - time_passed: float - fps: float + for key, value in counter.items(): + assert isinstance(key, int) + buffer.write_value(StructFormat.UBYTE, key) + buffer.write_value(StructFormat.UBYTE, value) + return buffer + + +def decode_numeric_uint8_counter(buffer: Buffer) -> Counter[int]: + """Return buffer from uint8 counter (both keys and values).""" + data: dict[int, int] = {} + + for _ in range(0, len(buffer), 2): + key = buffer.read_value(StructFormat.UBYTE) + value = buffer.read_value(StructFormat.UBYTE) + assert key not in data + data[key] = value + + return Counter(data) class ClientBoundEvents(IntEnum): @@ -51,15 +71,6 @@ class ClientBoundEvents(IntEnum): callback_ping = auto() initial_config = auto() playing_as = auto() - create_piece = auto() - select_piece = auto() - create_tile = auto() - delete_tile = auto() - animation_state = auto() - delete_piece_animation = auto() - update_piece_animation = auto() - move_piece_animation = auto() - action_complete = auto() game_over = auto() @@ -67,5 +78,3 @@ class ServerBoundEvents(IntEnum): """Server bound event IDs.""" encryption_response = 0 - select_piece = auto() - select_tile = auto() diff --git a/src/azul/server.py b/src/azul/server.py index f361f62..4cd2226 100755 --- a/src/azul/server.py +++ b/src/azul/server.py @@ -54,7 +54,7 @@ ClientBoundEvents, ServerBoundEvents, ) -from azul.state import State +from azul.state import Phase, State if TYPE_CHECKING: from collections.abc import Awaitable, Callable @@ -196,7 +196,6 @@ class GameServer(network.Server): "client_count", "client_players", "internal_singleplayer_mode", - "player_selections", "players_can_interact", "running", "state", @@ -208,7 +207,7 @@ def __init__(self, internal_singleplayer_mode: bool = False) -> None: """Initialize server.""" super().__init__("GameServer") - self.client_count: int + self.client_count: int = 0 self.state = State.new_game(0) self.client_players: dict[int, int] = {} @@ -323,18 +322,20 @@ def setup_teams(client_ids: list[int]) -> dict[int, int]: """Return teams given sorted client ids.""" players: dict[int, int] = {} for idx, client_id in enumerate(client_ids): - if idx < 2: - players[client_id] = idx % 2 + if idx < 4: + players[client_id] = idx % 4 else: players[client_id] = 0xFF # Spectator return players - def new_game_init(self) -> None: + def new_game_init(self, varient_play: bool = False) -> None: """Start new game.""" self.client_players.clear() - ## pieces = generate_pieces(*self.board_size) - self.state = State.new_game(self.client_count) + self.state = State.new_game( + max(2, min(4, self.client_count)), + varient_play, + ) # Why keep track of another object just to know client ID numbers # if we already have that with the components? No need! @@ -360,7 +361,6 @@ async def start_server( # type: ignore[misc] print(f"{self.__class__.__name__}: Closing old server clients") await self.stop_server() print(f"{self.__class__.__name__}: Starting Server") - self.client_count = 0 host, port = event.data @@ -381,8 +381,9 @@ async def transmit_playing_as(self) -> None: Event(f"playing_as->network[{client_id}]", team), ) - async def handle_server_start_new_game(self, event: Event[None]) -> None: + async def handle_server_start_new_game(self, event: Event[bool]) -> None: """Handle game start.""" + varient_play = event.data ## # Delete all pieces from last state (shouldn't be needed but still.) ## async with trio.open_nursery() as nursery: ## for piece_pos, _piece_type in self.state.get_pieces(): @@ -393,7 +394,7 @@ async def handle_server_start_new_game(self, event: Event[None]) -> None: # Choose which team plays first # Using non-cryptographically secure random because it doesn't matter - self.new_game_init() + self.new_game_init(varient_play) ## # Send create_piece events for all pieces ## async with trio.open_nursery() as nursery: @@ -413,7 +414,11 @@ async def handle_server_start_new_game(self, event: Event[None]) -> None: ), ) - async def client_network_loop(self, client: ServerClient) -> None: + async def client_network_loop( + self, + client: ServerClient, + controls_lobby: bool = False, + ) -> None: """Network loop for given ServerClient. Could raise the following exceptions: @@ -457,6 +462,7 @@ async def client_network_loop(self, client: ServerClient) -> None: traceback.print_exception(exc) break if event is not None: + # if controls_lobby: # print(f"{client.name} client_network_loop tick") # print(f"{client.name} {event = }") await client.raise_event(event) @@ -469,7 +475,7 @@ def can_start(self) -> bool: def game_active(self) -> bool: """Return if game is active.""" - return self.state.check_for_win() is None + return self.state.current_phase != Phase.end async def send_spectator_join_packets( self, @@ -487,7 +493,7 @@ async def send_spectator_join_packets( await client.raise_event( Event( "initial_config->network", - (None, self.state.turn), + (self.state.varient_play, self.state.current_turn), ), ) @@ -501,6 +507,10 @@ async def handler(self, stream: trio.SocketStream) -> None: # Old game was running but everyone left, restart print("TODO: restart") new_client_id = self.client_count + + # Is controlling player? + is_zee_capitan = new_client_id == 0 + print( f"{self.__class__.__name__}: client connected [client_id {new_client_id}]", ) @@ -530,12 +540,13 @@ async def handler(self, stream: trio.SocketStream) -> None: if can_start and game_active: await self.send_spectator_join_packets(client) with self.temporary_component(client): - if can_start and not game_active: + if can_start and not game_active and is_zee_capitan: + varient_play = False await self.raise_event( - Event("server_send_game_start", None), + Event("server_send_game_start", varient_play), ) try: - await self.client_network_loop(client) + await self.client_network_loop(client, is_zee_capitan) finally: print( f"{self.__class__.__name__}: client disconnected [client_id {new_client_id}]", diff --git a/src/azul/state.py b/src/azul/state.py index 5c836dc..7439967 100644 --- a/src/azul/state.py +++ b/src/azul/state.py @@ -181,6 +181,16 @@ def floor_fill_tile_excess( return excess +class UnplacableTileError(Exception): + """Unplacable Tile Exception.""" + + __slots__ = ("y",) + + def __init__(self, y: int) -> None: + """Remember Y position.""" + self.y = y + + class PlayerData(NamedTuple): """Player data.""" @@ -392,9 +402,10 @@ def perform_auto_wall_tiling(self) -> tuple[Self, Counter[int], bool]: for line_id, line in enumerate(self.lines): if line.count_ != self.get_line_max_count(line_id): continue - right = max(0, line.count_ - 1) - if right: - for_box_lid[line.color] += right + left = max(0, line.count_ - 1) + if left: + for_box_lid[line.color] += left + # placed tile is stuck in the wall now x = tuple(map(int, new_wall[line_id, :])).index(-line.color - 1) score += self.get_score_from_wall_placement( line.color, @@ -462,6 +473,122 @@ def perform_end_of_game_scoring(self) -> Self: """Return new player data after performing end of game scoring.""" return self._replace(score=self.get_end_of_game_score()) + def get_manual_wall_tile_location(self) -> tuple[int, list[int]] | None: + """Return tuple of row and placable columns for wall tiling, or None if done. + + Raises UnplacableTileError if no valid placement locations. + """ + for y, line in enumerate(self.lines): + if line.color == Tile.blank: + continue + if line.count_ != self.get_line_max_count(y): + continue + + valid_x: list[int] = [] + for x, is_open in enumerate(self.wall[y, :] >= 0): + if not is_open: + continue + if line.color in {Tile(int(v)) for v in self.wall[:, x]}: + continue + valid_x.append(x) + if not valid_x: + raise UnplacableTileError(y) + return (y, valid_x) + return None + + def handle_unplacable_wall_tiling( + self, + y: int, + ) -> tuple[Self, Counter[int]]: + """Return new player data and tiles for floor line.""" + line = self.lines[y] + assert line.color != Tile.blank + + new_lines = self.replace_pattern_line( + self.lines, + y, + PatternLine.blank(), + ) + + return self._replace( + lines=new_lines, + ).place_floor_line_tiles(line.color, line.count_) + + def manual_wall_tiling_action( + self, + line_id: int, + x_pos: int, + ) -> tuple[Self, Counter[int]]: + """Wall tile given full line to given x position in that row. + + Return new player data and any tiles to return to box lid. + """ + for_box_lid: Counter[int] = Counter() + + score = self.score + new_lines = self.lines + new_wall = self.wall.copy() + + line = self.lines[line_id] + + assert line.count_ == self.get_line_max_count(line_id) + assert line.color != Tile.blank + assert new_wall[line_id, x_pos] == Tile.blank + + left = max(0, line.count_ - 1) + if left: + for_box_lid[line.color] += left + # placed tile is stuck in wall now + score += self.get_score_from_wall_placement( + line.color, + x_pos, + line_id, + new_wall, + ) + new_wall[line_id, x_pos] = line.color + new_lines = self.replace_pattern_line( + new_lines, + line_id, + PatternLine.blank(), + ) + + return ( + self._replace( + lines=new_lines, + wall=new_wall, + score=score, + ), + for_box_lid, + ) + + def finish_manual_wall_tiling(self) -> tuple[Self, Counter[int], bool]: + """Return new player data and tiles for box lid after performing automatic wall tiling.""" + for_box_lid: Counter[int] = Counter() + + score = self.score + + score += self.get_floor_line_scoring() + if score < 0: + score = 0 + + # Get one tile from floor line + floor = self.floor.copy() + has_one = False + if floor[Tile.one]: + floor[Tile.one] -= 1 + remove_counter_zeros(floor) + has_one = True + for_box_lid.update(floor) + + return ( + self._replace( + score=score, + floor=Counter(), + ), + for_box_lid, + has_one, + ) + def factory_displays_deepcopy( factory_displays: dict[int, Counter[int]], @@ -717,9 +844,7 @@ def _factory_offer_maybe_next_turn(self) -> Self: current_phase=current_phase, current_turn=current_turn, ) - if current_phase == Phase.wall_tiling: - if self.varient_play: - return new_state.start_manual_wall_tiling() + if current_phase == Phase.wall_tiling and not self.varient_play: return new_state.apply_auto_wall_tiling() return new_state @@ -1022,17 +1147,85 @@ def preform_action( return new raise NotImplementedError() - def start_manual_wall_tiling(self) -> Self: - """Return new state after starting manual wall tiling.""" + def _manual_wall_tiling_maybe_next_turn(self) -> Self: raise NotImplementedError() return self + def get_manual_wall_tiling_locations_for_player( + self, + player_id: int, + ) -> tuple[int, list[int]] | None | Self: + """Either return player wall tiling location data or new state. + + New state when player cannot wall tile their current row. + """ + current_player_data = self.player_data[player_id] + + try: + return current_player_data.get_manual_wall_tile_location() + except UnplacableTileError as unplacable_exc: + # kind of hacky, but it works + y_position = unplacable_exc.y + + new_player_data, for_box_lid = ( + current_player_data.handle_unplacable_wall_tiling(y_position) + ) + + box_lid = self.box_lid.copy() + + # Add overflow tiles to box lid + assert all(x > 0 for x in for_box_lid.values()), for_box_lid + box_lid.update(for_box_lid) + + # Update player data + player_data = player_data_deepcopy(self.player_data) + player_data[player_id] = new_player_data + + return self._replace( + box_lid=box_lid, + player_data=player_data, + )._manual_wall_tiling_maybe_next_turn() + + def manual_wall_tiling_action( + self, + player_id: int, + line_id: int, + x_pos: int, + ) -> Self: + """Perform manual wall tiling action.""" + current_player_data = self.player_data[player_id] + + new_player_data, for_box_lid = ( + current_player_data.manual_wall_tiling_action(line_id, x_pos) + ) + box_lid = self.box_lid.copy() + + # Add overflow tiles to box lid + assert all(x > 0 for x in for_box_lid.values()), for_box_lid + box_lid.update(for_box_lid) + + # Update player data + player_data = player_data_deepcopy(self.player_data) + player_data[player_id] = new_player_data + + new_state = self._replace( + box_lid=box_lid, + player_data=player_data, + ) + + result = new_state.get_manual_wall_tiling_locations_for_player( + player_id, + ) + if isinstance(result, tuple) or result is None: + return new_state._manual_wall_tiling_maybe_next_turn() + return result._manual_wall_tiling_maybe_next_turn() + def run() -> None: """Run program.""" from market_api import pretty_print_response as pprint - random.seed(2) + random.seed(0) state = State.new_game(2) ticks = 0 try: From ec4da64f79111496399763c75efb0035326687bc Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Thu, 28 Nov 2024 20:32:38 -0600 Subject: [PATCH 07/58] Add more tests for sprite --- src/azul/sprite.py | 10 +++--- tests/test_sprite.py | 72 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/src/azul/sprite.py b/src/azul/sprite.py index 4cff67c..e55ea4e 100644 --- a/src/azul/sprite.py +++ b/src/azul/sprite.py @@ -160,7 +160,7 @@ class ImageComponent(ComponentManager): """Allow sprite to use multiple images easily. Components Supplied: - AnimationComponent + # AnimationComponent OutlineComponent Requires Component: @@ -186,7 +186,7 @@ def __init__(self) -> None: self.add_components( ( - AnimationComponent(), + # AnimationComponent(), OutlineComponent(), ), ) @@ -253,7 +253,7 @@ def get_mask(self, identifier: int | str) -> Mask: while True: if not self.image_exists(identifier): raise ValueError( - f'No image saved for identifier "{identifier}"', + f'No mask saved for identifier "{identifier}"', ) mask = self.__masks[identifier] if isinstance(mask, Mask): @@ -785,12 +785,12 @@ def clear_groups(self) -> None: for group_id in tuple(self.groups): self.remove_group(group_id) - def __del__(self) -> None: + def __del__(self) -> None: # pragma: nocover """Clear groups.""" self.clear_groups() -def convert_pygame_event(event: PygameEvent) -> Event[Any]: +def convert_pygame_event(event: PygameEvent) -> Event[Any]: # pragma: nocover """Convert Pygame Event to Component Event.""" # data = event.dict # data['type_int'] = event.type diff --git a/tests/test_sprite.py b/tests/test_sprite.py index f2bda6c..9c7ce9a 100644 --- a/tests/test_sprite.py +++ b/tests/test_sprite.py @@ -1,8 +1,11 @@ from __future__ import annotations +from typing import cast + import pytest import trio from libcomponent.component import Event +from pygame.mask import Mask from pygame.rect import Rect from pygame.surface import Surface @@ -33,6 +36,7 @@ def image_component(sprite: Sprite) -> ImageComponent: @pytest.fixture def animation_component(image_component: ImageComponent) -> AnimationComponent: + image_component.add_component(AnimationComponent()) return image_component.get_component("animation") @@ -113,6 +117,18 @@ def test_sprite_image_set_location_change(sprite: Sprite) -> None: assert sprite.location == (100, 100) +def test_sprite_selected_invisible(sprite: Sprite) -> None: + assert not sprite.visible + sprite.rect.size = (100, 100) + assert not sprite.is_selected((20, 20)) + + +def test_sprite_selected(sprite: Sprite) -> None: + sprite.visible = True + sprite.rect.size = (100, 100) + assert sprite.is_selected((20, 20)) + + def test_image_component_init(image_component: ImageComponent) -> None: assert image_component.mask_threshold == 127 @@ -123,6 +139,40 @@ def test_image_component_add_image(image_component: ImageComponent) -> None: assert "test_image" in image_component.list_images() +def test_image_component_add_images(image_component: ImageComponent) -> None: + image = Surface((10, 10)) + image_component.add_images({"test_image": image}) + assert "test_image" in image_component.list_images() + + +def test_image_component_get_image_fail( + image_component: ImageComponent, +) -> None: + with pytest.raises( + ValueError, + match='^No image saved for identifier "test_image"$', + ): + image_component.get_image("test_image") + + +def test_image_component_get_mask_fail( + image_component: ImageComponent, +) -> None: + with pytest.raises( + ValueError, + match='^No mask saved for identifier "test_mask"$', + ): + image_component.get_mask("test_mask") + + +def test_image_component_get_mask_success( + image_component: ImageComponent, +) -> None: + image = Surface((10, 10)) + image_component.add_image("test_image", image) + assert isinstance(image_component.get_mask("test_image"), Mask) + + def test_image_component_add_image_and_mask_invalid_image( image_component: ImageComponent, ) -> None: @@ -169,6 +219,28 @@ def test_image_component_add_image_duplication( assert image_component.get_image("duplicate") is image +def test_image_component_get_duplicate_mask( + image_component: ImageComponent, +) -> None: + image = Surface((1, 1)) + image_component.add_image("test_image", image) + image_component.add_image("duplicate", "test_image") + assert isinstance(image_component.get_mask("duplicate"), Mask) + + +def test_image_component_set_image_affects_sprite( + image_component: ImageComponent, +) -> None: + image = Surface((1, 1)) + sprite = cast(Sprite, image_component.manager.get_component("sprite")) + image_component.add_image("test_image", image) + assert sprite.image is None + image_component.set_image("test_image") + assert sprite.image is image + image_component.set_image("test_image") + assert sprite.image is image + + def test_movement_component_init( movement_component: MovementComponent, ) -> None: From aae563474e456cf06ece3589bad9f02fd0ac9b90 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Fri, 29 Nov 2024 01:08:33 -0600 Subject: [PATCH 08/58] Work on making GUI side work Using beta release of `libcomponent` for the moment, uses new `unregister_handler` feature --- pyproject.toml | 2 +- src/azul/client.py | 25 ++-- src/azul/game.py | 281 +++++++++++++++++++++---------------------- src/azul/server.py | 31 +++-- test-requirements.in | 2 +- 5 files changed, 178 insertions(+), 163 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 53b856d..4012fde 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ keywords = [ "ai", "multi-player", "azul", "ai-support", "networked-game" ] dependencies = [ - "libcomponent~=0.0.0", + "libcomponent @ git+https://github.com/CoolCat467/LibComponent", "pygame~=2.6.0", "typing_extensions>=4.12.2", "mypy_extensions>=1.0.0", diff --git a/src/azul/client.py b/src/azul/client.py index d830f74..35fdbf1 100644 --- a/src/azul/client.py +++ b/src/azul/client.py @@ -162,8 +162,6 @@ def __init__(self, name: str) -> None: sbe = ServerBoundEvents self.register_network_write_events( { - "select_piece->server": sbe.select_piece, - "select_tile->server": sbe.select_tile, "encryption_response->server": sbe.encryption_response, }, ) @@ -171,16 +169,7 @@ def __init__(self, name: str) -> None: self.register_read_network_events( { cbe.callback_ping: "server->callback_ping", - cbe.create_piece: "server->create_piece", - cbe.select_piece: "server->select_piece", - cbe.create_tile: "server->create_tile", - cbe.delete_tile: "server->delete_tile", - cbe.delete_piece_animation: "server->delete_piece_animation", - cbe.update_piece_animation: "server->update_piece_animation", - cbe.move_piece_animation: "server->move_piece_animation", - cbe.animation_state: "server->animation_state", cbe.game_over: "server->game_over", - cbe.action_complete: "server->action_complete", cbe.initial_config: "server->initial_config", cbe.playing_as: "server->playing_as", cbe.encryption_request: "server->encryption_request", @@ -332,11 +321,21 @@ async def read_initial_config(self, event: Event[bytearray]) -> None: """Read initial_config event from server.""" buffer = Buffer(event.data) - board_size: u8 = buffer.read_value(StructFormat.UBYTE) + varient_play: u8 = buffer.read_value(StructFormat.BOOL) + player_count: u8 = buffer.read_value(StructFormat.UBYTE) + factory_count: u8 = buffer.read_value(StructFormat.UBYTE) current_turn: u8 = buffer.read_value(StructFormat.UBYTE) await self.raise_event( - Event("game_initial_config", (board_size, current_turn)), + Event( + "game_initial_config", + ( + varient_play, + player_count, + factory_count, + current_turn, + ), + ), ) async def read_playing_as(self, event: Event[bytearray]) -> None: diff --git a/src/azul/game.py b/src/azul/game.py index db0bc45..e9c1b56 100644 --- a/src/azul/game.py +++ b/src/azul/game.py @@ -634,14 +634,26 @@ def __init__( self.background = background - def clear_image(self, tile_dimensions: tuple[int, int]) -> None: + def clear_image( + self, + tile_dimensions: tuple[int, int], + extra: tuple[int, int] | None = None, + ) -> None: """Reset self.image using tile_dimensions tuple and fills with self.background. Also updates self.width_height.""" tile_width, tile_height = tile_dimensions tile_full = self.tile_size + self.tile_separation + + ox = self.tile_separation + oy = self.tile_separation + + if extra is not None: + ox += extra[0] + oy += extra[1] + self.image = get_tile_container_image( ( - round(tile_width * tile_full + self.tile_separation), - round(tile_height * tile_full + self.tile_separation), + round(tile_width * tile_full + ox), + round(tile_height * tile_full + oy), ), self.background, ) @@ -731,7 +743,10 @@ class Cursor(TileRenderer): - cursor_drag - cursor_reached_destination - cursor_set_destination - - cursor_set_location + - cursor_set_movement_mode + + Sometimes registered: + - PygameMouseMotion """ __slots__ = ("tiles",) @@ -771,7 +786,7 @@ def bind_handlers(self) -> None: "cursor_drag": self.handle_cursor_drag, "cursor_reached_destination": self.handle_cursor_reached_destination, "cursor_set_destination": self.handle_cursor_set_destination, - "cursor_set_location": self.handle_cursor_set_location, + "cursor_set_movement_mode": self.handle_cursor_set_movement_mode, }, ) @@ -813,13 +828,28 @@ async def handle_cursor_set_destination( self.move_to_front() await trio.lowlevel.checkpoint() - async def handle_cursor_set_location( + async def handle_pygame_mouse_motion( self, - event: Event[tuple[int, int]], + event: Event[sprite.PygameMouseMotion], ) -> None: """Set location to event data.""" self.move_to_front() - self.location = event.data + self.location = event.data["pos"] + await trio.lowlevel.checkpoint() + + async def handle_cursor_set_movement_mode( + self, + event: Event[bool], + ) -> None: + """Change cursor movement mode. True if client mode, False if server mode.""" + client_mode = event.data + if client_mode: + self.register_handler( + "PygameMouseMotion", + self.handle_pygame_mouse_motion, + ) + else: + self.unregister_handler_type("PygameMouseMotion") await trio.lowlevel.checkpoint() def get_held_count(self) -> int: @@ -881,16 +911,20 @@ def get_tile(self, xy: tuple[int, int]) -> int: x, y = xy return int(self.data[y, x]) - def update_image(self) -> None: + def update_image( + self, + offset: tuple[int, int] | None = None, + extra_space: tuple[int, int] | None = None, + ) -> None: """Update self.image.""" - self.clear_image(self.size) + self.clear_image(self.size, extra_space) width, height = self.size for y in range(height): for x in range(width): pos = (x, y) - self.blit_tile(self.get_tile(pos), pos) + self.blit_tile(self.get_tile(pos), pos, offset) def fake_tile_exists(self, xy: tuple[int, int]) -> bool: """Return if tile at given position is a fake tile.""" @@ -928,9 +962,9 @@ class Board(Grid): __slots__ = ("additions", "variant_play", "wall_tiling") - def __init__(self, variant_play: bool = False) -> None: + def __init__(self, name: str, variant_play: bool = False) -> None: """Initialize player's board.""" - super().__init__("Board", (5, 5), background=ORANGE) + super().__init__(name, (5, 5), background=ORANGE) self.variant_play = variant_play self.additions: dict[int, int | None] = {} @@ -1504,98 +1538,55 @@ def can_place_tiles(self, tiles: list[int]) -> bool: class Factory(Grid): """Represents a Factory.""" - size = (2, 2) color = WHITE outline = BLUE - out_size = 0.1 def __init__(self, factory_id: int) -> None: """Initialize factory.""" - super().__init__(self.size, background=None) + super().__init__(f"Factory_{factory_id}", (2, 2), background=None) + self.number = factory_id - self.name = f"Factory{self.number}" - self.radius = math.ceil( - self.tile_full * self.size[0] * self.size[1] / 3 + 3, - ) + self.redraw() + self.visible = True def __repr__(self) -> str: """Return representation of self.""" return f"{self.__class__.__name__}({self.number})" - def add_circle(self, surface: pygame.surface.Surface) -> None: - """Add circle to self.image.""" - rad = math.ceil(self.radius) - surf = pygame.surface.Surface((2 * rad, 2 * rad), SRCALPHA) - pygame.draw.circle(surf, self.outline, (rad, rad), rad) + def clear_image( + self, + tile_size: tuple[int, int], + extra_space: tuple[int, int] | None, + ) -> None: + """Clear self.image and draw circles.""" + super().clear_image(tile_size, extra_space) + radius = 29 pygame.draw.circle( - surf, - self.color, - (rad, rad), - math.ceil(rad * (1 - self.out_size)), + self.image, + self.outline, + (radius, radius), + radius, ) - - surface.blit( - surf, - ( - round(self.location[0] - self.radius), - round(self.location[1] - self.radius), - ), + pygame.draw.circle( + self.image, + self.color, + (radius, radius), + math.ceil(radius * 0.9), ) - def render(self, surface: pygame.surface.Surface) -> None: - """Render Factory.""" - if not self.hidden: - self.add_circle(surface) - super().render(surface) - - def fill(self, tiles: list[int]) -> None: - """Fill self with tiles. Will raise exception if insufficiant tiles.""" - if len(tiles) < self.size[0] * self.size[1]: - size = self.size[0] * self.size[1] - raise RuntimeError( - f"Insufficiant quantity of tiles! Needs {size}!", - ) - for y in range(self.size[1]): - for tile, x in zip( - (tiles.pop() for i in range(self.size[0])), - range(self.size[0]), - strict=True, - ): - self.place_tile((x, y), tile) - if tiles: - raise RuntimeError("Too many tiles!") - - def grab(self) -> list[int]: - """Return all tiles on this factory.""" - return [ - tile - for tile in ( - self.get_tile((x, y), Tile.blank) - for x in range(self.size[0]) - for y in range(self.size[1]) - ) - if tile is not None and tile.color != Tile.blank - ] - - def grab_color(self, color: int) -> tuple[list[int], list[int]]: - """Return all tiles of color given in the first list, and all non-matches in the second list.""" - tiles = self.grab() - right, wrong = [], [] - for tile in tiles: - if tile.color == color: - right.append(tile) - else: - wrong.append(tile) - return right, wrong + def redraw(self) -> None: + """Redraw this factory.""" + super().update_image(offset=(8, 8), extra_space=(16, 16)) - def process(self, time_passed: float) -> None: - """Process self.""" - if self.image_update: - self.radius = int( - self.tile_full * self.size[0] * self.size[1] // 3 + 3, - ) - super().process(time_passed) + def get_tile_point( + self, + screen_location: tuple[int, int] | Vector2, + ) -> tuple[int, int] | None: + """Get tile point accounting for offset.""" + return super().get_tile_point( + Vector2.from_iter(screen_location) - (8, 8), + ) class Factories(MultipartObject): @@ -1643,7 +1634,7 @@ def reset_position(self) -> None: self.objects[index].location = ( Vector2.from_degrees( degrees, - self.size, + 29 * 5, ) + self.location ) @@ -2356,55 +2347,6 @@ async def check_conditions(self) -> str: return "title" -class InitializeState(GameState): - """Initialize state.""" - - __slots__ = () - - def __init__(self) -> None: - """Initialize self.""" - super().__init__("initialize") - - async def entry_actions(self) -> None: - """Set up buttons.""" - assert self.machine is not None - self.id = self.machine.new_group("initialize") - - self.group_add(Cursor()) - await self.manager.raise_event(Event("cursor_drag", [3, 5])) - self.manager.register_handler("PygameMouseMotion", self.mouse_moved) - - ## board = Board() - #### board.place_tile((2, 2), Tile.red) - ## board.location = Vector2.from_iter(SCREEN_SIZE) // 2 - ## self.group_add(board) - - center = TableCenter() - center.location = Vector2.from_iter(SCREEN_SIZE) // 2 - self.group_add(center) - center.add_tiles((0, 1, 2, 3, 5)) - - async def mouse_moved( - self, - event: Event[sprite.PygameMouseMotion], - ) -> None: - """Handle PygameMouseMotion event.""" - ## print(f'{event = }') - await self.manager.raise_event( - Event("cursor_set_location", event.data["pos"]), - ) - - -## await self.manager.raise_event( -## Event("cursor_set_destination", event.data["pos"]), -## ) - - -## async def check_conditions(self) -> str: -## """Go to title state.""" -## return "title" - - class TitleState(MenuState): """Game state when the title screen is up.""" @@ -3267,6 +3209,23 @@ async def handle_return_to_title(self, _: Event[None]) -> None: # return None +## async def entry_actions(self) -> None: +## """Set up buttons.""" +## assert self.machine is not None +## self.id = self.machine.new_group("initialize") +## +## self.group_add(Cursor()) +## await self.manager.raise_event(Event("cursor_drag", [3, 5])) +## self.manager.register_handler("PygameMouseMotion", self.mouse_moved) +## +## ## board = Board() +## #### board.place_tile((2, 2), Tile.red) +## ## board.location = Vector2.from_iter(SCREEN_SIZE) // 2 +## ## self.group_add(board) +## +## center.add_tiles((0, 1, 2, 3, 5)) + + class PlayState(GameState): """Game Play State.""" @@ -3283,6 +3242,7 @@ def register_handlers(self) -> None: """Register event handlers.""" self.manager.register_handlers( { + "game_initial_config": self.handle_game_initial_config, "client_disconnected": self.handle_client_disconnected, "game_winner": self.handle_game_over, }, @@ -3295,12 +3255,16 @@ def add_actions(self) -> None: async def entry_actions(self) -> None: """Add GameBoard and raise init event.""" - self.exit_data = None - assert self.machine is not None if self.id == 0: self.id = self.machine.new_group("play") + self.group_add(Cursor()) + + center = TableCenter() + center.location = Vector2.from_iter(SCREEN_SIZE) // 2 + self.group_add(center) + # self.group_add(()) ## gameboard = GameBoard( ## 45, @@ -3308,7 +3272,42 @@ async def entry_actions(self) -> None: ## gameboard.location = [x // 2 for x in SCREEN_SIZE] ## self.group_add(gameboard) - await self.machine.raise_event(Event("init", None)) + async def handle_game_initial_config( + self, + event: Event[tuple[bool, int, int, int]], + ) -> None: + """Handle `game_initial_config` event.""" + varient_play, player_count, factory_count, current_turn = event.data + + print("handle_game_initial_config") + print((varient_play, player_count, factory_count, current_turn)) + + center = Vector2.from_iter(SCREEN_SIZE) // 2 + + # Add factories + for index, degrees in enumerate(range(0, 360, 360 // factory_count)): + factory = Factory(index) + factory.location = ( + Vector2.from_degrees( + degrees - 90, + 145, + ) + + center + ) + self.group_add(factory) + + # Add players + # TODO: Do it properly + for index, degrees in enumerate(range(0, 360, 360 // player_count)): + board = Board(f"Board_{index}", varient_play) + board.location = ( + Vector2.from_degrees( + degrees - 45, + 300, + ) + + center + ) + self.group_add(board) async def check_conditions(self) -> str | None: """Return to title if client component doesn't exist.""" diff --git a/src/azul/server.py b/src/azul/server.py index 4cd2226..13a9397 100755 --- a/src/azul/server.py +++ b/src/azul/server.py @@ -60,6 +60,10 @@ from collections.abc import Awaitable, Callable +# cursor_set_movement_mode +# cursor_set_destination + + class ServerClient(ServerClientNetworkEventComponent): """Server Client Network Event Component. @@ -119,16 +123,17 @@ async def handle_game_over(self, event: Event[int]) -> None: async def handle_initial_config( self, - event: Event[tuple[None, int]], + event: Event[tuple[bool, int, int, int]], ) -> None: """Read initial config event and reraise as server[write]->initial_config.""" - board_size, player_turn = event.data + varient_play, player_count, factory_count, current_turn = event.data buffer = Buffer() - ## write_position(buffer, board_size) - buffer.write_value(StructFormat.UBYTE, 0) - buffer.write_value(StructFormat.UBYTE, player_turn) + buffer.write_value(StructFormat.BOOL, varient_play) + buffer.write_value(StructFormat.UBYTE, player_count) + buffer.write_value(StructFormat.UBYTE, factory_count) + buffer.write_value(StructFormat.UBYTE, current_turn) await self.write_event(Event("server[write]->initial_config", buffer)) @@ -330,6 +335,7 @@ def setup_teams(client_ids: list[int]) -> dict[int, int]: def new_game_init(self, varient_play: bool = False) -> None: """Start new game.""" + print("server new_game_init") self.client_players.clear() self.state = State.new_game( @@ -410,7 +416,12 @@ async def handle_server_start_new_game(self, event: Event[bool]) -> None: await self.raise_event( Event( "initial_config->network", - (None, self.state.turn), + ( + self.state.varient_play, + len(self.state.player_data), + len(self.state.factory_displays), + self.state.current_turn, + ), ), ) @@ -493,7 +504,12 @@ async def send_spectator_join_packets( await client.raise_event( Event( "initial_config->network", - (self.state.varient_play, self.state.current_turn), + ( + self.state.varient_play, + len(self.state.player_data), + len(self.state.factory_displays), + self.state.current_turn, + ), ), ) @@ -506,6 +522,7 @@ async def handler(self, stream: trio.SocketStream) -> None: if self.client_count == 0 and self.game_active(): # Old game was running but everyone left, restart print("TODO: restart") + self.new_game_init() new_client_id = self.client_count # Is controlling player? diff --git a/test-requirements.in b/test-requirements.in index 0094b39..a9dc141 100644 --- a/test-requirements.in +++ b/test-requirements.in @@ -19,7 +19,7 @@ typing-extensions # Azul's own dependencies # exceptiongroup; python_version < '3.11' -libcomponent~=0.0.0 +libcomponent @ git+https://github.com/CoolCat467/LibComponent mypy_extensions>=1.0.0 numpy~=2.1.3 pygame~=2.6.0 From 2ee1522160679df5617f1e0fcf72e37c3be1b598 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Fri, 29 Nov 2024 13:52:54 -0600 Subject: [PATCH 09/58] Send board data and get rid of old code --- src/azul/client.py | 41 +- src/azul/game.py | 1504 ++---------------------------------- src/azul/network_shared.py | 28 +- src/azul/server.py | 108 +-- src/azul/state.py | 14 + 5 files changed, 185 insertions(+), 1510 deletions(-) diff --git a/src/azul/client.py b/src/azul/client.py index 35fdbf1..b9a1d1a 100644 --- a/src/azul/client.py +++ b/src/azul/client.py @@ -40,6 +40,7 @@ ADVERTISEMENT_PORT, ClientBoundEvents, ServerBoundEvents, + decode_int8_array, ) if TYPE_CHECKING: @@ -168,11 +169,12 @@ def __init__(self, name: str) -> None: cbe = ClientBoundEvents self.register_read_network_events( { + cbe.encryption_request: "server->encryption_request", cbe.callback_ping: "server->callback_ping", - cbe.game_over: "server->game_over", cbe.initial_config: "server->initial_config", cbe.playing_as: "server->playing_as", - cbe.encryption_request: "server->encryption_request", + cbe.game_over: "server->game_over", + cbe.board_data: "server->board_data", }, ) @@ -184,14 +186,14 @@ def bind_handlers(self) -> None: super().bind_handlers() self.register_handlers( { + "server->encryption_request": self.read_encryption_request, "server->callback_ping": self.read_callback_ping, - "server->game_over": self.read_game_over, "server->initial_config": self.read_initial_config, "server->playing_as": self.read_playing_as, - "server->encryption_request": self.read_encryption_request, - "network_stop": self.handle_network_stop, + "server->game_over": self.read_game_over, + "server->board_data": self.read_board_data, "client_connect": self.handle_client_connect, - # f"client[{self.name}]_read_event": self.handle_read_event, + "network_stop": self.handle_network_stop, }, ) @@ -308,15 +310,6 @@ async def handle_client_connect( return await self.raise_disconnect("Error connecting to server.") - async def read_game_over(self, event: Event[bytearray]) -> None: - """Read update_piece event from server.""" - buffer = Buffer(event.data) - - winner: u8 = buffer.read_value(StructFormat.UBYTE) - - await self.raise_event(Event("game_winner", winner)) - self.running = False - async def read_initial_config(self, event: Event[bytearray]) -> None: """Read initial_config event from server.""" buffer = Buffer(event.data) @@ -348,6 +341,24 @@ async def read_playing_as(self, event: Event[bytearray]) -> None: Event("game_playing_as", playing_as), ) + async def read_game_over(self, event: Event[bytearray]) -> None: + """Read game_over event from server.""" + buffer = Buffer(event.data) + + winner: u8 = buffer.read_value(StructFormat.UBYTE) + + await self.raise_event(Event("game_winner", winner)) + self.running = False + + async def read_board_data(self, event: Event[bytearray]) -> None: + """Read board_data event from server.""" + buffer = Buffer(event.data) + + player_id: u8 = buffer.read_value(StructFormat.UBYTE) + array = decode_int8_array(buffer, (5, 5)) + + await self.raise_event(Event("game_board_data", (player_id, array))) + async def handle_network_stop(self, event: Event[None]) -> None: """Send EOF if connected and close socket.""" if self.not_connected: diff --git a/src/azul/game.py b/src/azul/game.py index e9c1b56..59c288f 100644 --- a/src/azul/game.py +++ b/src/azul/game.py @@ -21,14 +21,13 @@ __title__ = "Azul" __author__ = "CoolCat467" +__license__ = "GNU General Public License Version 3" __version__ = "2.0.0" import contextlib import importlib import math -import operator import os -import random import sys import time import traceback @@ -68,7 +67,6 @@ from azul.statemachine import AsyncState from azul.tools import ( lerp_color, - saturate, ) from azul.vector import Vector2 @@ -84,6 +82,7 @@ Sequence, ) + from numpy.typing import NDArray from typing_extensions import TypeVarTuple P = TypeVarTuple("P") @@ -392,225 +391,6 @@ def get_tile_container_image( return image -class ObjectHandler: - """ObjectHandler class, meant to be used for other classes.""" - - # __slots__ = ("objects", "next_id", "cache") - - def __init__(self) -> None: - """Initialize object handler.""" - self.objects: dict[int, sprite.Sprite] = {} - self.next_id = 0 - self.cache: dict[str, int] = {} - - self.recalculate_render = True - self._render_order: tuple[int, ...] = () - - def add_object(self, obj: sprite.Sprite) -> None: - """Add an object to the game.""" - obj.id = self.next_id - self.objects[self.next_id] = obj - self.next_id += 1 - self.recalculate_render = True - - def rm_object(self, obj: sprite.Sprite) -> None: - """Remove an object from the game.""" - del self.objects[obj.id] - self.recalculate_render = True - - def rm_star(self) -> None: - """Remove all objects from self.objects.""" - for oid in list(self.objects): - self.rm_object(self.objects[oid]) - self.next_id = 0 - - def get_object(self, object_id: int) -> sprite.Sprite | None: - """Return the object associated with object id given. Return None if object not found.""" - if object_id in self.objects: - return self.objects[object_id] - return None - - def get_objects_with_attr(self, attribute: str) -> tuple[int, ...]: - """Return a tuple of object ids with given attribute.""" - return tuple( - oid - for oid in self.objects - if hasattr(self.objects[oid], attribute) - ) - - def get_object_by_attr( - self, - attribute: str, - value: object, - ) -> tuple[int, ...]: - """Return a tuple of object ids with that are equal to .""" - matches = [] - for oid in self.get_objects_with_attr(attribute): - if getattr(self.objects[oid], attribute) == value: - matches.append(oid) - return tuple(matches) - - def get_object_given_name(self, name: str) -> tuple[int, ...]: - """Return a tuple of object ids with names matching .""" - return self.get_object_by_attr("name", name) - - def reset_cache(self) -> None: - """Reset the cache.""" - self.cache = {} - - def get_object_by_name(self, object_name: str) -> sprite.Sprite: - """Get object by name, with cache.""" - if object_name not in self.cache: - ids = self.get_object_given_name(object_name) - if ids: - self.cache[object_name] = min(ids) - else: - raise RuntimeError(f"{object_name} sprite.Sprite Not Found!") - result = self.get_object(self.cache[object_name]) - if result is None: - raise RuntimeError(f"{object_name} sprite.Sprite Not Found!") - return result - - def set_attr_all(self, attribute: str, value: object) -> None: - """Set given attribute in all of self.objects to given value in all objects with that attribute.""" - for oid in self.get_objects_with_attr(attribute): - setattr(self.objects[oid], attribute, value) - - def recalculate_render_order(self) -> None: - """Recalculate the order in which to render objects to the screen.""" - new: dict[int, int] = {} - cur = 0 - for oid in reversed(self.objects): - obj = self.objects[oid] - if hasattr(obj, "Render_Priority"): - prior = obj.Render_Priority - if isinstance(prior, str): - add = 0 - if prior[:4] == "last": - try: - add = int(prior[4:] or 0) - except ValueError: - add = 0 - pos = len(self.objects) + add - if prior[:5] == "first": - try: - add = int(prior[5:] or 0) - except ValueError: - add = 0 - pos = -1 + add - if pos not in new.values(): - new[oid] = pos - else: - while True: - if add < 0: - pos -= 1 - else: - pos += 1 - if pos not in new.values(): - new[oid] = pos - break - else: - try: - prior = int(prior) - except ValueError: - prior = cur - while True: - if prior in new.values(): - prior += 1 - else: - break - new[oid] = prior - else: - while True: - if cur in new.values(): - cur += 1 - else: - break - new[oid] = cur - cur += 1 - revnew = {new[k]: k for k in new} - self._render_order = tuple(revnew[key] for key in sorted(revnew)) - - def process_objects(self, time_passed: float) -> None: - """Call the process function on all objects.""" - if self.recalculate_render: - self.recalculate_render_order() - self.recalculate_render = False - for oid in iter(self.objects): - self.objects[oid].process(time_passed) - - def render_objects(self, surface: pygame.surface.Surface) -> None: - """Render all objects to surface.""" - if not self._render_order or self.recalculate_render: - self.recalculate_render_order() - self.recalculate_render = False - for oid in self._render_order: # reversed(list(self.objects.keys())): - self.objects[oid].render(surface) - - def __del__(self) -> None: - """Cleanup.""" - self.reset_cache() - self.rm_star() - - -class MultipartObject(ObjectHandler): - """Thing that is both an sprite.Sprite and an ObjectHandler, and is meant to be an sprite.Sprite made up of multiple Objects.""" - - def __init__(self, name: str): - """Initialize sprite.Sprite and ObjectHandler of self. - - Also set self._lastloc and self._lasthidden to None - """ - ObjectHandler.__init__(self) - - self._lastloc: Vector2 | None = None - self._lasthidden: bool | None = None - - def reset_position(self) -> None: - """Reset the position of all objects within.""" - raise NotImplementedError - - def get_intersection( - self, - point: tuple[int, int] | Vector2, - ) -> tuple[str, tuple[int, int]] | tuple[None, None]: - """Return where a given point touches in self. Returns (None, None) with no intersections.""" - for oid in self.objects: - obj = self.objects[oid] - if hasattr(obj, "get_tile_point"): - output = obj.get_tile_point(point) - if output is not None: - return obj.name, output - else: - raise Warning( - "Not all of self.objects have the get_tile_point attribute!", - ) - return None, None - - def process(self, time_passed: float) -> None: - """Process sprite.Sprite self and ObjectHandler self and call self.reset_position on location change.""" - sprite.Sprite.process(self, time_passed) - ObjectHandler.process_objects(self, time_passed) - - if self.location != self._lastloc: - self.reset_position() - self._lastloc = self.location - - if self.hidden != self._lasthidden: - self.set_attr_all("hidden", self.hidden) - self._lasthidden = self.hidden - - def render(self, surface: pygame.surface.Surface) -> None: - """Render self and all parts to the surface.""" - sprite.Sprite.render(self, surface) - ObjectHandler.render_objects(self, surface) - - def __del__(self) -> None: - """Delete data.""" - sprite.Sprite.__del__(self) - ObjectHandler.__del__(self) - - class TileRenderer(sprite.Sprite): """Base class for all objects that need to render tiles.""" @@ -640,21 +420,19 @@ def clear_image( extra: tuple[int, int] | None = None, ) -> None: """Reset self.image using tile_dimensions tuple and fills with self.background. Also updates self.width_height.""" - tile_width, tile_height = tile_dimensions + size = Vector2.from_iter(tile_dimensions) tile_full = self.tile_size + self.tile_separation + size *= tile_full - ox = self.tile_separation - oy = self.tile_separation + offset = Vector2(self.tile_separation, self.tile_separation) if extra is not None: - ox += extra[0] - oy += extra[1] + offset += extra + + size += offset self.image = get_tile_container_image( - ( - round(tile_width * tile_full + ox), - round(tile_height * tile_full + oy), - ), + round(size), self.background, ) @@ -721,21 +499,6 @@ def get_tile_point( return tile_position -## def screen_size_update(self) -> None: -## """Handle screensize is changes.""" -## nx, ny = self.location -## -## if self.location_mode_on_resize == "Scale": -## ow, oh = self.screen_size_last -## nw, nh = SCREEN_SIZE -## -## x, y = self.location -## nx, ny = x * (nw / ow), y * (nh / oh) -## -## self.location = Vector2(nx, ny) -## self.screen_size_last = SCREEN_SIZE - - class Cursor(TileRenderer): """Cursor TileRenderer. @@ -960,289 +723,69 @@ def is_empty(self, empty_color: int = Tile.blank) -> bool: class Board(Grid): """Represents the board in the Game.""" - __slots__ = ("additions", "variant_play", "wall_tiling") + __slots__ = ("board_id",) - def __init__(self, name: str, variant_play: bool = False) -> None: + def __init__(self, board_id: int) -> None: """Initialize player's board.""" - super().__init__(name, (5, 5), background=ORANGE) + super().__init__(f"board_{board_id}", (5, 5), background=ORANGE) - self.variant_play = variant_play - self.additions: dict[int, int | None] = {} + self.board_id = board_id - self.wall_tiling = False - - if not variant_play: - self.set_colors() - else: - self.update_image() - self.visible = True + self.update_location_on_resize = True def __repr__(self) -> str: """Return representation of self.""" return f"{self.__class__.__name__}({self.variant_play})" - def set_colors(self, keep_real: bool = True) -> None: - """Reset tile colors.""" - width, height = self.size - for y in range(height): - for x in range(width): - if not keep_real or self.fake_tile_exists((x, y)): - color = -((height - y + x) % REGTILECOUNT + 1) - self.data[y, x] = color - self.update_image() - - def get_row(self, index: int) -> Generator[int, None, None]: - """Return a row from self. Does not delete data from internal grid.""" - for x in range(self.size[0]): - yield self.get_info((x, index)) - - def get_column(self, index: int) -> Generator[int, None, None]: - """Return a column from self. Does not delete data from internal grid.""" - for y in range(self.size[1]): - yield self.get_info((index, y)) - - def get_colors_in_row( - self, - index: int, - exclude_negatives: bool = True, - ) -> set[int]: - """Return the colors placed in a given row in internal grid.""" - row_colors: Iterable[int] = self.get_row(index) - if exclude_negatives: - row_colors = (c for c in row_colors if c >= 0) - return set(row_colors) - - def get_colors_in_column( - self, - index: int, - exclude_negatives: bool = True, - ) -> set[int]: - """Return the colors placed in a given row in internal grid.""" - column_colors: Iterable[int] = self.get_column(index) - if exclude_negatives: - column_colors = (c for c in column_colors if c >= 0) - return set(column_colors) - - def is_wall_tiling(self) -> bool: - """Return True if in Wall Tiling Mode.""" - return self.wall_tiling - - def can_place_tile_color_at_point( - self, - position: tuple[int, int], - tile_color: int, - ) -> bool: - """Return True if tile's color is valid at given position.""" - column, row = position - colors = self.get_colors_in_column(column) | self.get_colors_in_row( - row, + def bind_handlers(self) -> None: + """Register event handlers.""" + self.register_handlers( + { + "game_board_data": self.handle_game_board_data, + }, ) - return tile_color not in colors - - ## def remove_invalid_additions(self) -> None: - ## """Remove invalid additions that would not be placeable.""" - ## # In the wall-tiling phase, it may happen that you - ## # are not able to move the rightmost tile of a certain - ## # pattern line over to the wall because there is no valid - ## # space left for it. In this case, you must immediately - ## # place all tiles of that pattern line in your floor line. - ## for row in range(self.size[1]): - ## row_tile = self.additions[row] - ## if not isinstance(row_tile, int): - ## continue - ## valid = self.calculate_valid_locations_for_tile_row(row) - ## if not valid: - ## floor = self.player.get_object_by_name("floor_line") - ## assert isinstance(floor, FloorLine) - ## floor.place_tile(row_tile) - ## self.additions[row] = None - - ## def wall_tile_from_point(self, position: tuple[int, int]) -> bool: - ## """Given a position, wall tile. Return success on placement. Also updates if in wall tiling mode.""" - ## success = False - ## column, row = position - ## at_point = self.get_info(position) - ## assert at_point is not None - ## if at_point.color <= 0 and row in self.additions: - ## tile = self.additions[row] - ## if isinstance(tile, int) and self.can_place_tile_color_at_point( - ## position, - ## tile, - ## ): - ## self.place_tile(position, tile) - ## self.additions[row] = column - ## # Update invalid placements after new placement - ## self.remove_invalid_additions() - ## success = True - ## if not self.get_rows_to_tile_map(): - ## self.wall_tiling = False - ## return success - - ## def wall_tiling_mode(self, moved_table: dict[int, int]) -> None: - ## """Set self into Wall Tiling Mode. Finishes automatically if not in variant play mode.""" - ## self.wall_tiling = True - ## for key, value in moved_table.items(): - ## key = int(key) - 1 - ## if key in self.additions: - ## raise RuntimeError( - ## f"Key {key!r} Already in additions dictionary!", - ## ) - ## self.additions[key] = value - ## if not self.variant_play: - ## for row in range(self.size[1]): - ## if row in self.additions: - ## rowdata = [tile.color for tile in self.get_row(row)] - ## tile = self.additions[row] - ## if not isinstance(tile, int): - ## continue - ## negative_tile_color = -(tile.color + 1) - ## if negative_tile_color in rowdata: - ## column = rowdata.index(negative_tile_color) - ## self.place_tile((column, row), tile) - ## # Set data to the column placed in, use for scoring - ## self.additions[row] = column - ## else: - ## raise RuntimeError( - ## f"{negative_tile_color} not in row {row}!", - ## ) - ## else: - ## raise RuntimeError(f"{row} not in moved_table!") - ## self.wall_tiling = False - ## else: - ## # Invalid additions can only happen in variant play mode. - ## self.remove_invalid_additions() - - def get_touches_continuous( + + async def handle_game_board_data( self, - xy: tuple[int, int], - ) -> tuple[list[int], list[int]]: - """Return two lists, each of which contain all the tiles that touch the tile at given x y position, including that position.""" - rs, cs = self.size - x, y = xy - # Get row and column tile color data - row = list(self.get_row(y)) - column = list(self.get_column(x)) - - # Both - def get_greater_than(v: int, size: int, data: list[int]) -> list[int]: - """Go through data forward and backward from point v out by size, and return all points from data with a value >= 0.""" - - def try_range(range_: Iterable[int]) -> list[int]: - """Try range. Return all of data in range up to when indexed value is < 0.""" - ret = [] - for tv in range_: - if data[tv] < 0: - break - ret.append(tv) - return ret - - nt = try_range(reversed(range(v))) - pt = try_range(range(v + 1, size)) - return nt + pt - - def comb(one: Iterable[T], two: Iterable[RT]) -> list[tuple[T, RT]]: - """Combine two lists by zipping together and returning list object.""" - return list(zip(one, two, strict=False)) - - def get_all(lst: list[tuple[int, int]]) -> Generator[int, None, None]: - """Return all of the self.get_info points for each value in lst.""" - for pos in lst: - yield self.get_info(pos) - - # Get row touches - row_touches = comb(get_greater_than(x, rs, row), [y] * rs) - # Get column touches - column_touches = comb([x] * cs, get_greater_than(y, cs, column)) - # Get real tiles from indexes and return - return list(get_all(row_touches)), list(get_all(column_touches)) - - def score_additions(self) -> int: - """Return the number of points the additions scored. - - Uses self.additions, which is set in self.wall_tiling_mode() - """ - score = 0 - for x, y in ((self.additions[y], y) for y in range(self.size[1])): - if x is not None: - assert isinstance(x, int) - rowt, colt = self.get_touches_continuous((x, y)) - horiz = len(rowt) - verti = len(colt) - if horiz > 1: - score += horiz - if verti > 1: - score += verti - if horiz <= 1 and verti <= 1: - score += 1 - del self.additions[y] - return score - - def get_filled_rows(self) -> int: - """Return the number of filled rows on this board.""" - count = 0 - for row in range(self.size[1]): - real = (t >= 0 for t in self.get_row(row)) - if all(real): - count += 1 - return count + event: Event[tuple[int, NDArray[int8]]], + ) -> None: + """Handle `game_board_data` event.""" + board_id, array = event.data - def has_filled_row(self) -> bool: - """Return True if there is at least one completely filled horizontal line.""" - return self.get_filled_rows() >= 1 + if board_id != self.board_id: + await trio.lowlevel.checkpoint() + return - def get_filled_columns(self) -> int: - """Return the number of filled rows on this board.""" - count = 0 - for column in range(self.size[0]): - real = (t >= 0 for t in self.get_column(column)) - if all(real): - count += 1 - return count - - def get_filled_colors(self) -> int: - """Return the number of completed colors on this board.""" - color_count = Counter( - self.get_info((x, y)) - for x in range(self.size[0]) - for y in range(self.size[1]) - ) - count = 0 - for fill_count in color_count.values(): - if fill_count >= 5: - count += 1 - return count + self.data = array + self.update_image() + self.visible = True - def end_of_game_scoreing(self) -> int: - """Return the additional points for this board at the end of the game.""" - score = 0 - score += self.get_filled_rows() * 2 - score += self.get_filled_columns() * 7 - score += self.get_filled_colors() * 10 - return score + await trio.lowlevel.checkpoint() class Row(TileRenderer): """Represents one of the five rows each player has.""" - __slots__ = ("color", "player", "size", "tiles") + __slots__ = ("color", "size", "tiles") greyshift = GREYSHIFT def __init__( self, + name: str, size: int, tile_separation: int | None = None, background: tuple[int, int, int] | None = None, ) -> None: """Initialize row.""" super().__init__( - "Row", + name, tile_separation, background, ) - self.size = int(size) self.color = Tile.blank - self.tiles = list([self.color] * self.size) + self.size = int(size) + self.count = 0 def __repr__(self) -> str: """Return representation of self.""" @@ -1252,19 +795,24 @@ def update_image(self) -> None: """Update self.image.""" self.clear_image((self.size, 1)) - for x in range(len(self.tiles)): - self.blit_tile(self.tiles[x], (x, 0)) + for x in range(self.count): + self.blit_tile(self.color, (x, 0)) + for x in range(self.count, self.size): + self.blit_tile(Tile.blank, (x, 0)) - def get_tile_point(self, screen_location: tuple[int, int]) -> int | None: + def get_tile_point( + self, + screen_location: tuple[int, int] | Vector2, + ) -> int | None: """Return the xy choordinates of which tile intersects given a point. Returns None if no intersections.""" - pos = super().get_tile_point() + pos = super().get_tile_point(screen_location) if pos is None: return None return pos[0] def get_placed(self) -> int: """Return the number of tiles in self that are not fake tiles, like grey ones.""" - return len([tile for tile in self.tiles if tile.color >= 0]) + return self.count def get_placeable(self) -> int: """Return the number of tiles permitted to be placed on self.""" @@ -1272,94 +820,7 @@ def get_placeable(self) -> int: def is_full(self) -> bool: """Return True if this row is full.""" - return self.get_placed() == self.size - - def get_info(self, location: int) -> int | None: - """Return tile at location without deleting it. Return None on invalid location.""" - index = self.size - 1 - location - if index < 0 or index > len(self.tiles): - return None - return self.tiles[index] - - def can_place(self, tile: int) -> bool: - """Return True if permitted to place given tile object on self.""" - placeable = (tile.color == self.color) or ( - self.color < 0 and tile.color >= 0 - ) - if not placeable: - return False - color_correct = tile.color >= 0 and tile.color < 5 - if not color_correct: - return False - number_correct = self.get_placeable() > 0 - if not number_correct: - return False - - board = self.player.get_object_by_name("Board") - assert isinstance(board, Board) - # Is color not present? - return tile.color not in board.get_colors_in_row( - self.size - 1, - ) - - def get_tile(self, replace: int = Tile.blank) -> int: - """Return the leftmost tile while deleting it from self.""" - self.tiles.appendleft(int(replace)) - self.image_update = True - return self.tiles.pop() - - def place_tile(self, tile: int) -> None: - """Place a given int sprite.Sprite on self if permitted.""" - if self.can_place(tile): - self.color = tile.color - self.tiles.append(tile) - end = self.tiles.popleft() - if not end.color < 0: - raise RuntimeError( - "Attempted deletion of real tile from Row!", - ) - self.image_update = True - else: - raise ValueError("Not allowed to place.") - - def can_place_tiles(self, tiles: list[int]) -> bool: - """Return True if permitted to place all of given tiles objects on self.""" - if len(tiles) > self.get_placeable(): - return False - for tile in tiles: - if not self.can_place(tile): - return False - tile_colors = [] - for tile in tiles: - if tile.color not in tile_colors: - tile_colors.append(tile.color) - return not len(tile_colors) > 1 - - def place_tiles(self, tiles: list[int]) -> None: - """Place multiple tile objects on self if permitted.""" - if self.can_place_tiles(tiles): - for tile in tiles: - self.place_tile(tile) - else: - raise ValueError("Not allowed to place tiles.") - - ## def wall_tile( - ## self, - ## add_to_table: dict[str, list[int] | int | None], - ## empty_color: int = Tile.blank, - ## ) -> None: - ## """Move tiles around and into add dictionary for the wall tiling phase of the game. Removes tiles from self.""" - ## if "tiles_for_box" not in add_to_table: - ## add_to_table["tiles_for_box"] = [] - ## if not self.is_full(): - ## add_to_table[str(self.size)] = None - ## return - ## self.color = empty_color - ## add_to_table[str(self.size)] = self.get_tile() - ## for_box = add_to_table["tiles_for_box"] - ## assert isinstance(for_box, list) - ## for _i in range(self.size - 1): - ## for_box.append(self.get_tile()) + return self.get_placeable() == 0 def set_background(self, color: tuple[int, int, int] | None) -> None: """Set the background color for this row.""" @@ -1367,99 +828,14 @@ def set_background(self, color: tuple[int, int, int] | None) -> None: self.update_image() -class PatternLine(MultipartObject): - """Represents multiple rows to make the pattern line.""" - - __slots__ = ("player", "row_separation") - size = (5, 5) - - def __init__(self, player: Player, row_separation: int = 0) -> None: - """Initialize pattern line.""" - super().__init__("PatternLine") - self.player = player - self.row_separation = row_separation - - for x, _y in zip( - range(self.size[0]), - range(self.size[1]), - strict=True, - ): - self.add_object(Row(self.player, x + 1)) - - self.set_background(None) - - self._lastloc = Vector2(0, 0) - - def set_background(self, color: tuple[int, int, int] | None) -> None: - """Set the background color for all rows in the pattern line.""" - self.set_attr_all("back", color) - self.set_attr_all("image_update", True) - - def get_row(self, row: int) -> Row: - """Return given row.""" - object_ = self.get_object(row) - assert isinstance(object_, Row) - return object_ - - def reset_position(self) -> None: - """Reset Locations of Rows according to self.location.""" - last = self.size[1] - w = self.get_row(last - 1).width_height[0] - if w is None: - raise RuntimeError( - "Image Dimensions for Row sprite.Sprite (row.width_height) are None!", - ) - h1 = self.get_row(0).tile_full - h = int(last * h1) - self.width_height = w, h - w1 = h1 / 2 - - x, y = self.location - y -= h / 2 - w1 - for rid in self.objects: - row = self.get_row(rid) - diff = last - row.size - row.location = Vector2(x + (diff * w1), y + rid * h1) - - def get_tile_point( - self, - screen_location: tuple[int, int], - ) -> tuple[int, int] | None: - """Return the xy choordinates of which tile intersects given a point. Returns None if no intersections.""" - for y in range(self.size[1]): - x = self.get_row(y).get_tile_point(screen_location) - if x is not None: - return x, y - return None - - def is_full(self) -> bool: - """Return True if self is full.""" - return all(self.get_row(rid).is_full() for rid in range(self.size[1])) - - def wall_tiling(self) -> dict[str, list[int] | int | None]: - """Return a dictionary to be used with wall tiling. Removes tiles from rows.""" - values: dict[str, list[int] | int | None] = {} - for rid in range(self.size[1]): - self.get_row(rid).wall_tile(values) - return values - - def process(self, time_passed: float) -> None: - """Process all the rows that make up the pattern line.""" - if self.hidden != self._lasthidden: - self.set_attr_all("image_update", True) - super().process(time_passed) - - class FloorLine(Row): """Represents a player's floor line.""" - size = 7 - number_one_color = Tile.one + __slots__ = ("floor_line_id", "numbers", "text") - def __init__(self, player: Player) -> None: + def __init__(self, floor_line_id: int) -> None: """Initialize floor line.""" - super().__init__(player, self.size, background=ORANGE) - self.name = "floor_line" + super().__init__(f"floor_line_{floor_line_id}", 7, background=ORANGE) # self.font = Font(FONT, round(self.tile_size*1.2), color=BLACK, cx=False, cy=False) self.text = objects.Text( @@ -1468,7 +844,6 @@ def __init__(self, player: Player) -> None: cx=False, cy=False, ) - self.has_number_one_tile = False self.numbers = [-255 for _ in range(self.size)] @@ -1493,47 +868,6 @@ def render(self, surface: pygame.surface.Surface) -> None: self.text.location = Vector2(*xy) self.text.render(surface) - # self.font.render(surface, str(self.numbers[x]), xy) - - def place_tile(self, tile: int) -> None: - """Place a given int sprite.Sprite on self if permitted.""" - self.tiles.insert(self.get_placed(), tile) - - def score_tiles(self) -> int: - """Score self.tiles and return how to change points.""" - running_total = 0 - for x in range(self.size): - if self.tiles[x].color >= 0: - running_total += self.numbers[x] - elif x < self.size - 1 and self.tiles[x + 1].color >= 0: - raise RuntimeError( - "Player is likely cheating! Invalid placement of floor_line tiles!", - ) - return running_total - - def get_tiles( - self, - empty_color: int = Tile.blank, - ) -> tuple[list[int], int | None]: - """Return tuple of tiles gathered, and then either the number one tile or None.""" - tiles = [] - number_one_tile = None - for tile in (self.tiles.pop() for i in range(len(self.tiles))): - if tile.color == self.number_one_color: - number_one_tile = tile - self.has_number_one_tile = False - elif tile.color >= 0: - tiles.append(tile) - - for _i in range(self.size): - self.tiles.append(int(empty_color)) - self.image_update = True - return tiles, number_one_tile - - def can_place_tiles(self, tiles: list[int]) -> bool: - """Return True.""" - return True - class Factory(Grid): """Represents a Factory.""" @@ -1589,95 +923,6 @@ def get_tile_point( ) -class Factories(MultipartObject): - """Factories Multipart sprite.Sprite, made of multiple Factory Objects.""" - - tiles_each = 4 - - def __init__( - self, - game: Game, - factories: int, - size: int | None = None, - ) -> None: - """Initialize factories.""" - super().__init__("Factories") - - self.game = game - self.count = factories - - for i in range(self.count): - self.add_object(Factory(self.game, i)) - - if size is None: - factory = self.objects[0] - assert isinstance(factory, Factory) - factory.process(0) - rad = factory.radius - self.size = rad * 5 - else: - self.size = size - self.size = math.ceil(self.size) - - self.play_tiles_from_bag() - - def __repr__(self) -> str: - """Return representation of self.""" - return f"{self.__class__.__name__}(%r, %i, ...)" % ( - self.game, - self.count, - ) - - def reset_position(self) -> None: - """Reset the position of all factories within.""" - for index, degrees in enumerate(range(0, 360, 360 // self.count)): - self.objects[index].location = ( - Vector2.from_degrees( - degrees, - 29 * 5, - ) - + self.location - ) - - def process(self, time_passed: float) -> None: - """Process factories. Does not react to cursor if hidden.""" - super().process(time_passed) - if self.hidden: - return - cursor = self.game.get_object_by_name("Cursor") - assert isinstance(cursor, Cursor) - if not cursor.is_pressed() or cursor.is_holding(): - return - obj, point = self.get_intersection(cursor.location) - if obj is None or point is None: - return - oid = int(obj[7:]) - - factory = self.objects[oid] - assert isinstance(factory, Factory) - - tile_at_point = factory.get_info(point) - if tile_at_point is None or tile_at_point.color < 0: - return - table = self.game.get_object_by_name("TableCenter") - assert isinstance(table, TableCenter) - select, tocenter = factory.grab_color( - tile_at_point.color, - ) - if tocenter: - table.add_tiles(tocenter) - cursor.drag(select) - - def is_all_empty(self) -> bool: - """Return True if all factories are empty.""" - for fid in range(self.count): - factory = self.objects[fid] - assert isinstance(factory, Factory) - if not factory.is_empty(): - return False - return True - - class TableCenter(TileRenderer): """sprite.Sprite that represents the center of the table.""" @@ -1745,356 +990,6 @@ def pull_tiles(self, tile_color: int) -> list[int]: return [tile_color] * tile_count -## def process(self, time_passed: float) -> None: -## """Process factories.""" -## if self.hidden: -## super().process(time_passed) -## return -## cursor = self.game.get_object_by_name("Cursor") -## assert isinstance(cursor, Cursor) -## if ( -## cursor.is_pressed() -## and not cursor.is_holding() -## and not self.is_empty() -## and self.is_selected(cursor.location) -## ): -## point = self.get_tile_point(cursor.location) -## # Shouldn't return none anymore since we have is_selected now. -## assert point is not None -## tile = self.get_info(point) -## assert isinstance(tile, int) -## color_at_point = tile.color -## if color_at_point >= 0 and color_at_point < 5: -## cursor.drag(self.pull_tiles(color_at_point)) -## super().process(time_passed) - - -class Player(sprite.Sprite): - """Represents a player. Made of lots of objects.""" - - def __init__( - self, - game: Game, - player_id: int, - networked: bool = False, - varient_play: bool = False, - ) -> None: - """Initialize player.""" - super().__init__(f"Player{player_id}") - - self.game = game - self.player_id = player_id - self.networked = networked - self.varient_play = varient_play - - self.add_object(Board(self.varient_play)) - self.add_object(PatternLine(self)) - self.add_object(FloorLine(self)) - ## self.add_object(objects.objects.Text(SCOREFONTSIZE, SCORECOLOR)) - - self.score = 0 - self.is_turn = False - self.is_wall_tiling = False - self.just_held = False - self.just_dropped = False - - self.update_score() - - self._lastloc = Vector2(0, 0) - - def __repr__(self) -> str: - """Return representation of self.""" - return f"{self.__class__.__name__}(%r, %i, %s, %s)" % ( - self.game, - self.player_id, - self.networked, - self.varient_play, - ) - - ## def update_score(self) -> None: - ## """Update the scorebox for this player.""" - ## score_box = self.get_object_by_name("objects.Text") - ## assert isinstance(score_box, objects.Text) - ## score_box.update_value(f"Player {self.player_id + 1}: {self.score}") - - def trigger_turn_now(self) -> None: - """Handle start of turn.""" - if not self.is_turn: - pattern_line = self.get_object_by_name("PatternLine") - assert isinstance(pattern_line, PatternLine) - if self.is_wall_tiling: - board = self.get_object_by_name("Board") - assert isinstance(board, Board) - rows = board.get_rows_to_tile_map() - for rowpos, value in rows.items(): - color = get_tile_color(value, board.greyshift) - assert isinstance(color[0], int) - pattern_line.get_row(rowpos).set_background( - color, - ) - else: - pattern_line.set_background(PATSELECTCOLOR) - self.is_turn = True - - def end_of_turn(self) -> None: - """Handle end of turn.""" - if self.is_turn: - pattern_line = self.get_object_by_name("PatternLine") - assert isinstance(pattern_line, PatternLine) - pattern_line.set_background(None) - self.is_turn = False - - ## def end_of_game_trigger(self) -> None: - ## """Handle end of game. - ## - ## Called by end state when game is over - ## Hide pattern lines and floor line. - ## """ - ## pattern = self.get_object_by_name("PatternLine") - ## floor = self.get_object_by_name("floor_line") - ## - ## pattern.hidden = True - ## floor.hidden = True - - def reset_position(self) -> None: - """Reset positions of all parts of self based off self.location.""" - x, y = self.location - - board = self.get_object_by_name("Board") - assert isinstance(board, Board) - bw, bh = board.width_height - board.location = Vector2(x + bw // 2, y) - - pattern_line = self.get_object_by_name("PatternLine") - assert isinstance(pattern_line, PatternLine) - lw = pattern_line.width_height[0] // 2 - pattern_line.location = Vector2(x - lw, y) - - floor_line = self.get_object_by_name("floor_line") - assert isinstance(floor_line, FloorLine) - floor_line.location = Vector2( - int(x - lw * (2 / 3) + TILESIZE / 3.75), - int(y + bh * (2 / 3)), - ) - - text = self.get_object_by_name("objects.Text") - assert isinstance(text, objects.Text) - text.location = Vector2(x - (bw // 3), y - (bh * 2 // 3)) - - -## def wall_tiling(self) -> None: -## """Do the wall tiling phase of the game for this player.""" -## self.is_wall_tiling = True -## pattern_line = self.get_object_by_name("PatternLine") -## assert isinstance(pattern_line, PatternLine) -## board = self.get_object_by_name("Board") -## assert isinstance(board, Board) -## box_lid = self.game.get_object_by_name("BoxLid") -## assert isinstance(box_lid, BoxLid) -## -## data = pattern_line.wall_tiling() -## tiles_for_box = data["tiles_for_box"] -## assert isinstance(tiles_for_box, list) -## box_lid.add_tiles(tiles_for_box) -## del data["tiles_for_box"] -## -## cleaned = {} -## for key, value in data.items(): -## if not isinstance(value, int): -## continue -## cleaned[int(key)] = value -## -## board.wall_tiling_mode(cleaned) - -## def done_wall_tiling(self) -> bool: -## """Return True if internal Board is done wall tiling.""" -## board = self.get_object_by_name("Board") -## assert isinstance(board, Board) -## return not board.is_wall_tiling() - -## def next_round(self) -> None: -## """Handle end of wall tiling.""" -## self.is_wall_tiling = False - -## def score_phase(self) -> int | None: -## """Do the scoring phase of the game for this player. Return number one tile or None.""" -## board = self.get_object_by_name("Board") -## floor_line = self.get_object_by_name("floor_line") -## box_lid = self.game.get_object_by_name("BoxLid") -## assert isinstance(board, Board) -## assert isinstance(floor_line, FloorLine) -## assert isinstance(box_lid, BoxLid) -## -## def saturatescore() -> None: -## if self.score < 0: -## self.score = 0 -## -## self.score += board.score_additions() -## self.score += floor_line.score_tiles() -## saturatescore() -## -## tiles_for_box, number_one = floor_line.get_tiles() -## box_lid.add_tiles(tiles_for_box) -## -## self.update_score() -## -## return number_one - -## def end_of_game_scoring(self) -> None: -## """Update final score with additional end of game points.""" -## board = self.get_object_by_name("Board") -## assert isinstance(board, Board) -## -## self.score += board.end_of_game_scoreing() -## -## self.update_score() - -## def has_horzontal_line(self) -> bool: -## """Return True if this player has a horizontal line on their game board filled.""" -## board = self.get_object_by_name("Board") -## assert isinstance(board, Board) -## -## return board.has_filled_row() - -## def get_horizontal_lines(self) -> int: -## """Return the number of filled horizontal lines this player has on their game board.""" -## board = self.get_object_by_name("Board") -## assert isinstance(board, Board) -## -## return board.get_filled_rows() - -## def process(self, time_passed: float) -> None: -## """Process Player.""" -## if not self.is_turn: # Is our turn? -## self.set_attr_all("hidden", self.hidden) -## super().process(time_passed) -## return -## if self.hidden and self.is_wall_tiling and self.varient_play: -## # If hidden, not anymore. Our turn. -## self.hidden = False -## if self.networked: # We are networked. -## self.set_attr_all("hidden", self.hidden) -## super().process(time_passed) -## return -## -## cursor = self.game.get_object_by_name("Cursor") -## assert isinstance(cursor, Cursor) -## box_lid = self.game.get_object_by_name("BoxLid") -## assert isinstance(box_lid, BoxLid) -## pattern_line = self.get_object_by_name("PatternLine") -## assert isinstance(pattern_line, PatternLine) -## floor_line = self.get_object_by_name("floor_line") -## assert isinstance(floor_line, FloorLine) -## board = self.get_object_by_name("Board") -## assert isinstance(board, Board) -## -## if not cursor.is_pressed(): -## # Mouse up -## if self.just_held: -## self.just_held = False -## if self.just_dropped: -## self.just_dropped = False -## self.set_attr_all("hidden", self.hidden) -## super().process(time_passed) -## return -## -## # Mouse down -## obj, point = self.get_intersection(cursor.location) -## if obj is None or point is None: -## if self.is_wall_tiling and self.done_wall_tiling(): -## self.next_round() -## self.game.next_turn() -## self.set_attr_all("hidden", self.hidden) -## super().process(time_passed) -## return -## # Something pressed -## if cursor.is_holding(): # Cursor holding tiles -## move_made = False -## if not self.is_wall_tiling: # Is wall tiling: -## if obj == "PatternLine": -## pos, row_number = point -## row = pattern_line.get_row(row_number) -## if not row.is_full(): -## info = row.get_info(pos) -## if info is not None and info.color < 0: -## _color, _held = cursor.get_held_info() -## todrop = min( -## pos + 1, -## row.get_placeable(), -## ) -## tiles = cursor.drop(todrop) -## if row.can_place_tiles(tiles): -## row.place_tiles(tiles) -## move_made = True -## else: -## cursor.force_hold(tiles) -## elif obj == "floor_line": -## tiles_to_add = cursor.drop() -## if floor_line.is_full(): -## # Floor is full, -## # Add tiles to box instead. -## box_lid.add_tiles(tiles_to_add) -## elif floor_line.get_placeable() < len( -## tiles_to_add, -## ): -## # Floor is not full but cannot fit all in floor line. -## # Add tiles to floor line and then to box -## while len(tiles_to_add) > 0: -## if floor_line.get_placeable() > 0: -## floor_line.place_tile( -## tiles_to_add.pop(), -## ) -## else: -## box_lid.add_tile( -## tiles_to_add.pop(), -## ) -## else: -## # Otherwise add to floor line for all. -## floor_line.place_tiles(tiles_to_add) -## move_made = True -## elif not self.just_held and obj == "Board": -## tile = board.get_info(point) -## assert isinstance(tile, int) -## if tile.color == Tile.blank: -## # Cursor holding and wall tiling -## _column, row_id = point -## cursor_tile = cursor.drop(1)[0] -## board_tile = board.get_tile_for_cursor_by_row( -## row_id, -## ) -## if ( -## board_tile is not None -## and cursor_tile.color == board_tile.color -## and board.wall_tile_from_point(point) -## ): -## self.just_dropped = True -## pattern_line.get_row( -## row_id, -## ).set_background(None) -## if move_made and not self.is_wall_tiling: -## if cursor.holding_number_one: -## one_tile = cursor.drop_one_tile() -## assert one_tile is not None -## floor_line.place_tile(one_tile) -## if cursor.get_held_count(True) == 0: -## self.game.next_turn() -## elif self.is_wall_tiling and obj == "Board" and not self.just_dropped: -## # Mouse down, something pressed, and not holding anything -## # Wall tiling, pressed, not holding -## _column_number, row_number = point -## tile = board.get_tile_for_cursor_by_row( -## row_number, -## ) -## if tile is not None: -## cursor.drag([tile]) -## self.just_held = True -## if self.is_wall_tiling and self.done_wall_tiling(): -## self.next_round() -## self.game.next_turn() -## self.set_attr_all("hidden", self.hidden) -## super().process(time_passed) - - class HaltState(AsyncState["AzulClient"]): """Halt state to set state to None so running becomes False.""" @@ -2413,7 +1308,7 @@ async def entry_actions(self) -> None: 0, hosting_button.rect.h + 10, ), - handle_click=self.change_state("play_internal_hosting"), + handle_click=self.change_state("play_hosting_internal"), ) self.group_add(internal_button) @@ -2592,28 +1487,6 @@ def __init__(self) -> None: """Initialize factory offer phase.""" super().__init__("FactoryOffer") - def entry_actions(self) -> None: - """Advance turn.""" - assert self.game is not None - self.game.next_turn() - - def check_state(self) -> str | None: - """If all tiles are gone, go to wall tiling. Otherwise keep waiting for that to happen.""" - assert self.game is not None - fact = self.game.get_object_by_name("Factories") - assert isinstance(fact, Factories) - table = self.game.get_object_by_name("TableCenter") - assert isinstance(table, TableCenter) - cursor = self.game.get_object_by_name("Cursor") - assert isinstance(cursor, Cursor) - if ( - fact.is_all_empty() - and table.is_empty() - and not cursor.is_holding(True) - ): - return "WallTiling" - return None - class PhaseFactoryOfferNetworked(PhaseFactoryOffer): """Factory offer phase but networked.""" @@ -2722,15 +1595,6 @@ def entry_actions(self) -> None: complete = (player.has_horzontal_line() for player in players) self.new_round = not any(complete) - def do_actions(self) -> None: - """Perform actions of state.""" - assert self.game is not None - if self.new_round: - fact = self.game.get_object_by_name("Factories") - assert isinstance(fact, Factories) - # This also handles bag re-filling from box lid. - fact.play_tiles_from_bag() - def check_state(self) -> str: """Go to factory offer if new round else end screen.""" if self.new_round: @@ -2741,265 +1605,11 @@ def check_state(self) -> str: class EndScreen(MenuState): """End screen state.""" + __slots__ = () + def __init__(self) -> None: """Initialize end screen.""" super().__init__("End") - self.ranking: dict[int, list[int]] = {} - self.wininf = "" - - def get_winners(self) -> None: - """Update self.ranking by player scores.""" - assert self.game is not None - self.ranking.clear() - scpid = {} - for player_id in range(self.game.players): - player = self.game.get_player(player_id) - assert isinstance(player, Player) - player.end_of_game_trigger() - if player.score not in scpid: - scpid[player.score] = [player_id] - else: - scpid[player.score] += [player_id] - # make sure no ties and establish rank - rank = 1 - for score in sorted(scpid, reverse=True): - pids = scpid[score] - if len(pids) > 1: - # If players have same score, - # most horizontal lines is tie breaker. - players = [ - self.game.get_player(player_id) for player_id in pids - ] - lines = [ - (p.get_horizontal_lines(), p.player_id) for p in players - ] - last = None - for c, player_id in sorted( - lines, - key=operator.itemgetter(0), - reverse=True, - ): - if last == c: - self.ranking[rank - 1] += [player_id + 1] - continue - last = c - self.ranking[rank] = [player_id + 1] - rank += 1 - else: - self.ranking[rank] = [pids[0] + 1] - rank += 1 - # Finally, make nice text. - text = "" - for rank in sorted(self.ranking): - line = "Player" - players_rank = self.ranking[rank] - cnt = len(players_rank) - if cnt > 1: - line += "s" - line += " " - if cnt == 1: - line += "{}" - elif cnt == 2: - line += "{} and {}" - elif cnt >= 3: - tmp = (["{}"] * (cnt - 1)) + ["and {}"] - line += ", ".join(tmp) - line += " " - if cnt == 1: - line += "got" - else: - line += "tied for" - line += " " - if rank <= 2: - line += ("1st", "2nd")[rank - 1] - else: - line += f"{rank}th" - line += " place!\n" - text += line.format(*players_rank) - self.wininf = text[:-1] - - def entry_actions(self) -> None: - """Set up end screen.""" - assert self.game is not None - # Figure out who won the game by points. - self.get_winners() - # Hide everything - table = self.game.get_object_by_name("TableCenter") - assert isinstance(table, TableCenter) - table.hidden = True - - fact = self.game.get_object_by_name("Factories") - assert isinstance(fact, Factories) - fact.set_attr_all("hidden", True) - - # Add buttons - bid = self.add_button( - "ReturnTitle", - "Return to Title", - self.to_state("Title"), - (SCREEN_SIZE[0] // 2, SCREEN_SIZE[1] * 4 // 5), - ) - buttontitle = self.game.get_object(bid) - assert isinstance(buttontitle, objects.Button) - buttontitle.Render_Priority = "last-1" - buttontitle.cur_time = 2 - - # Add score board - x = SCREEN_SIZE[0] // 2 - y = 10 - for idx, line in enumerate(self.wininf.split("\n")): - self.add_text(f"Line{idx}", line, (x, y), cx=True, cy=False) - # self.game.get_object(bid).Render_Priority = f'last{-(2+idx)}' - button = self.game.get_object(bid) - assert isinstance(button, objects.Button) - button.Render_Priority = "last-2" - y += self.bh - - -class Game(ObjectHandler): - """Game object, contains most of what's required for Azul.""" - - tile_size = 30 - - def __init__(self) -> None: - """Initialize game.""" - super().__init__() - - self.states: dict[str, GameState] = {} - self.active_state: GameState | None = None - - self.add_states( - [ - InitializeState(), - TitleState(), - CreditsState(), - SettingsState(), - PhaseFactoryOffer(), - PhaseWallTiling(), - PhasePrepareNext(), - EndScreen(), - ## PhaseFactoryOfferNetworked(), - ## PhaseWallTilingNetworked(), - ## PhasePrepareNextNetworked(), - ## EndScreenNetworked(), - ], - ) - self.initialized_state = False - - self.background_color = BACKGROUND - - self.is_host = True - self.players = 0 - self.factories = 0 - - self.player_turn: int = 0 - - # # Cache - # self.cache: dict[int, pygame.surface.Surface] = {} - - def __repr__(self) -> str: - """Return representation of self.""" - return f"{self.__class__.__name__}()" - - def add_object(self, obj: sprite.Sprite) -> None: - """Add an object to the game.""" - obj.game = self - super().add_object(obj) - - def get_player(self, player_id: int) -> Player: - """Get the player with player id player_id.""" - if self.players: - player = self.get_object_by_name(f"Player{player_id}") - assert isinstance(player, Player) - return player - raise RuntimeError("No players!") - - def player_turn_over(self) -> None: - """Call end_of_turn for current player.""" - if self.player_turn >= 0 and self.player_turn < self.players: - old_player = self.get_player(self.player_turn) - if old_player.is_turn: - old_player.end_of_turn() - - def next_turn(self) -> None: - """Tell current player it's the end of their turn, and update who's turn it is and now it's their turn.""" - if self.is_host: - self.player_turn_over() - last = self.player_turn - self.player_turn = (self.player_turn + 1) % self.players - if self.player_turn == last and self.players > 1: - self.next_turn() - return - new_player = self.get_player(self.player_turn) - new_player.trigger_turn_now() - - def start_game( - self, - players: int, - varient_play: bool = False, - host_mode: bool = True, - address: str = "", - ) -> None: - """Start a new game.""" - self.reset_cache() - max_players = 4 - self.players = saturate(players, 1, max_players) - self.is_host = host_mode - self.factories = self.players * 2 + 1 - - self.rm_star() - - self.add_object(Cursor(self)) - self.add_object(TableCenter(self)) - - if self.is_host: - self.bag.reset() - # S311 Standard pseudo-random generators are not suitable for cryptographic purposes - self.player_turn = random.randint( # noqa: S311 - -1, - self.players - 1, - ) - else: - raise NotImplementedError() - - cx, cy = SCREEN_SIZE[0] / 2, SCREEN_SIZE[1] / 2 - out = math.sqrt(cx**2 + cy**2) // 3 * 2 - - mdeg = 360 // max_players - - for player_id in range(self.players): - networked = False - newp = Player(self, player_id, networked, varient_play) - - truedeg = (self.players + 1 - player_id) * (360 / self.players) - closedeg = truedeg // mdeg * mdeg + 45 - rad = math.radians(closedeg) - - newp.location = Vector2( - round(cx + out * math.sin(rad)), - round( - cy + out * math.cos(rad), - ), - ) - self.add_object(newp) - if self.is_host: - self.next_turn() - - factory = Factories(self, self.factories) - factory.location = Vector2(cx, cy) - self.add_object(factory) - self.process_objects(0) - - if self.is_host: - self.next_turn() - - def screen_size_update(self) -> None: - """Handle screen size updates.""" - objs_with_attr = self.get_objects_with_attr("screen_size_update") - for oid in objs_with_attr: - obj = self.get_object(oid) - assert obj is not None - obj.screen_size_update() class PlayHostingState(AsyncState["AzulClient"]): @@ -3012,7 +1622,7 @@ class PlayHostingState(AsyncState["AzulClient"]): def __init__(self) -> None: """Initialize Play internal hosting / hosting State.""" extra = "_internal" if self.internal_server else "" - super().__init__(f"play{extra}_hosting") + super().__init__(f"play_hosting{extra}") async def entry_actions(self) -> None: """Start hosting server.""" @@ -3299,7 +1909,7 @@ async def handle_game_initial_config( # Add players # TODO: Do it properly for index, degrees in enumerate(range(0, 360, 360 // player_count)): - board = Board(f"Board_{index}", varient_play) + board = Board(index) board.location = ( Vector2.from_degrees( degrees - 45, diff --git a/src/azul/network_shared.py b/src/azul/network_shared.py index 862ff9f..30f41ee 100644 --- a/src/azul/network_shared.py +++ b/src/azul/network_shared.py @@ -26,11 +26,15 @@ from collections import Counter from enum import IntEnum, auto -from typing import Final, TypeAlias +from typing import TYPE_CHECKING, Final, TypeAlias from libcomponent.base_io import StructFormat from libcomponent.buffer import Buffer from mypy_extensions import u8 +from numpy import int8, zeros + +if TYPE_CHECKING: + from numpy.typing import NDArray ADVERTISEMENT_IP: Final = "224.0.2.60" ADVERTISEMENT_PORT: Final = 4445 @@ -47,6 +51,7 @@ def encode_numeric_uint8_counter(counter: Counter[int]) -> Buffer: for key, value in counter.items(): assert isinstance(key, int) buffer.write_value(StructFormat.UBYTE, key) + assert value >= 0 buffer.write_value(StructFormat.UBYTE, value) return buffer @@ -64,6 +69,26 @@ def decode_numeric_uint8_counter(buffer: Buffer) -> Counter[int]: return Counter(data) +def encode_int8_array(array: NDArray[int8]) -> Buffer: + """Return buffer from int8 array flat values.""" + buffer = Buffer() + + for value in array.flat: + buffer.write_value(StructFormat.BYTE, value) + + return buffer + + +def decode_int8_array(buffer: Buffer, size: tuple[int, ...]) -> NDArray[int8]: + """Return flattened int8 array from buffer.""" + array = zeros(size, dtype=int8) + + for index in range(array.size): + array.flat[index] = buffer.read_value(StructFormat.BYTE) + + return array + + class ClientBoundEvents(IntEnum): """Client bound event IDs.""" @@ -72,6 +97,7 @@ class ClientBoundEvents(IntEnum): initial_config = auto() playing_as = auto() game_over = auto() + board_data = auto() class ServerBoundEvents(IntEnum): diff --git a/src/azul/server.py b/src/azul/server.py index 13a9397..ab78f9a 100755 --- a/src/azul/server.py +++ b/src/azul/server.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 -# Checkers Game Server +# Azul Game Server -"""Checkers Game Server.""" +"""Azul Game Server.""" # Programmed by CoolCat467 @@ -27,7 +27,6 @@ __license__ = "GNU General Public License Version 3" __version__ = "0.0.0" -import time import traceback from collections import deque from functools import partial @@ -53,12 +52,16 @@ DEFAULT_PORT, ClientBoundEvents, ServerBoundEvents, + encode_int8_array, ) from azul.state import Phase, State if TYPE_CHECKING: from collections.abc import Awaitable, Callable + from numpy import int8 + from numpy.typing import NDArray + # cursor_set_movement_mode # cursor_set_destination @@ -87,8 +90,9 @@ def __init__(self, client_id: int) -> None: "server[write]->encryption_request": cbe.encryption_request, "server[write]->callback_ping": cbe.callback_ping, "server[write]->initial_config": cbe.initial_config, - "server[write]->game_over": cbe.game_over, "server[write]->playing_as": cbe.playing_as, + "server[write]->game_over": cbe.game_over, + "server[write]->board_data": cbe.board_data, }, ) sbe = ServerBoundEvents @@ -103,23 +107,32 @@ def bind_handlers(self) -> None: super().bind_handlers() self.register_handlers( { - f"callback_ping->network[{self.client_id}]": self.handle_callback_ping, f"client[{self.client_id}]->encryption_response": self.handle_encryption_response, + f"callback_ping->network[{self.client_id}]": self.handle_callback_ping, "initial_config->network": self.handle_initial_config, - "game_over->network": self.handle_game_over, f"playing_as->network[{self.client_id}]": self.handle_playing_as, + "game_over->network": self.handle_game_over, + "board_data->network": self.handle_board_data, }, ) - async def handle_game_over(self, event: Event[int]) -> None: - """Read game over event and reraise as server[write]->game_over.""" - winner = event.data - - buffer = Buffer() + async def start_encryption_request(self) -> None: + """Start encryption request and raise as `server[write]->encryption_request`.""" + await super().start_encryption_request() - buffer.write_value(StructFormat.UBYTE, winner) + event = await self.read_event() + if event.name != f"client[{self.client_id}]->encryption_response": + raise RuntimeError( + f"Expected encryption response, got but {event.name!r}", + ) + await self.handle_encryption_response(event) - await self.write_event(Event("server[write]->game_over", buffer)) + async def handle_callback_ping( + self, + _: Event[None], + ) -> None: + """Reraise as server[write]->callback_ping.""" + await self.write_callback_ping() async def handle_initial_config( self, @@ -148,48 +161,32 @@ async def handle_playing_as( buffer.write_value(StructFormat.UBYTE, playing_as) await self.write_event(Event("server[write]->playing_as", buffer)) - async def write_callback_ping(self) -> None: - """Write callback_ping packet to client. - - Could raise the following exceptions: - trio.BrokenResourceError: if something has gone wrong, and the stream - is broken. - trio.ClosedResourceError: if stream was previously closed + async def handle_game_over(self, event: Event[int]) -> None: + """Read game over event and reraise as server[write]->game_over.""" + winner = event.data - Listed as possible but probably not because of write lock: - trio.BusyResourceError: if another task is using :meth:`write` - """ buffer = Buffer() - # Try to be as accurate with time as possible - await self.wait_write_might_not_block() - ns = int(time.time() * 1e9) - # Use as many bits as time needs, write_buffer handles size for us. - buffer.write(ns.to_bytes(-(-ns.bit_length() // 8), "big")) + buffer.write_value(StructFormat.UBYTE, winner) - await self.write_event(Event("server[write]->callback_ping", buffer)) + await self.write_event(Event("server[write]->game_over", buffer)) - async def handle_callback_ping( + async def handle_board_data( self, - _: Event[None], + event: Event[tuple[int, NDArray[int8]]], ) -> None: - """Reraise as server[write]->callback_ping.""" - await self.write_callback_ping() + """Reraise as server[write]->board_data.""" + player_id, array = event.data - async def start_encryption_request(self) -> None: - """Start encryption request and raise as `server[write]->encryption_request`.""" - await super().start_encryption_request() + buffer = Buffer() + buffer.write_value(StructFormat.UBYTE, player_id) + buffer.extend(encode_int8_array(array)) - event = await self.read_event() - if event.name != f"client[{self.client_id}]->encryption_response": - raise RuntimeError( - f"Expected encryption response, got but {event.name!r}", - ) - await self.handle_encryption_response(event) + await self.write_event(Event("server[write]->board_data", buffer)) class GameServer(network.Server): - """Checkers server. + """Azul server. Handles accepting incoming connections from clients and handles main game logic via State subclass above. @@ -213,7 +210,7 @@ def __init__(self, internal_singleplayer_mode: bool = False) -> None: super().__init__("GameServer") self.client_count: int = 0 - self.state = State.new_game(0) + self.state = State.blank() self.client_players: dict[int, int] = {} self.players_can_interact: bool = False @@ -410,8 +407,6 @@ async def handle_server_start_new_game(self, event: Event[bool]) -> None: ## Event("create_piece->network", (piece_pos, piece_type)), ## ) - await self.transmit_playing_as() - # Raise initial config event with board size and initial turn. await self.raise_event( Event( @@ -425,6 +420,24 @@ async def handle_server_start_new_game(self, event: Event[bool]) -> None: ), ) + print(f"{self.state.player_data = }") + + # Transmit board data for all players + async with trio.open_nursery() as nursery: + for player_id, player_data in self.state.player_data.items(): + nursery.start_soon( + self.raise_event, + Event( + "board_data->network", + ( + player_id, + player_data.wall, + ), + ), + ) + + await self.transmit_playing_as() + async def client_network_loop( self, client: ServerClient, @@ -555,7 +568,8 @@ async def handler(self, stream: trio.SocketStream) -> None: assert client.encryption_enabled if can_start and game_active: - await self.send_spectator_join_packets(client) + print("TODO: Joined as spectator") + # await self.send_spectator_join_packets(client) with self.temporary_component(client): if can_start and not game_active and is_zee_capitan: varient_play = False diff --git a/src/azul/state.py b/src/azul/state.py index 7439967..755bc9b 100644 --- a/src/azul/state.py +++ b/src/azul/state.py @@ -649,6 +649,20 @@ class State(NamedTuple): current_turn: int player_data: dict[int, PlayerData] + @classmethod + def blank(cls) -> Self: + return cls( + varient_play=False, + current_phase=Phase.end, + bag=Counter(), + box_lid=Counter(), + table_center=Counter(), + factory_displays={}, + cursor_contents=Counter(), + current_turn=0, + player_data={}, + ) + @classmethod def new_game(cls, player_count: int, varient_play: bool = False) -> Self: """Return state of a new game.""" From 35bf6fdee6767662c4fd6947eb3d24b94b698ac7 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Fri, 29 Nov 2024 16:37:27 -0600 Subject: [PATCH 10/58] Send factory display tile data over the network --- src/azul/client.py | 14 ++++++++- src/azul/game.py | 61 ++++++++++++++++++++++++++------------ src/azul/network_shared.py | 36 +++++++++++++++++----- src/azul/server.py | 36 ++++++++++++++++++++-- 4 files changed, 116 insertions(+), 31 deletions(-) diff --git a/src/azul/client.py b/src/azul/client.py index b9a1d1a..64ac3f4 100644 --- a/src/azul/client.py +++ b/src/azul/client.py @@ -41,6 +41,7 @@ ClientBoundEvents, ServerBoundEvents, decode_int8_array, + decode_numeric_uint8_counter, ) if TYPE_CHECKING: @@ -175,6 +176,7 @@ def __init__(self, name: str) -> None: cbe.playing_as: "server->playing_as", cbe.game_over: "server->game_over", cbe.board_data: "server->board_data", + cbe.factory_data: "server->factory_data", }, ) @@ -192,6 +194,7 @@ def bind_handlers(self) -> None: "server->playing_as": self.read_playing_as, "server->game_over": self.read_game_over, "server->board_data": self.read_board_data, + "server->factory_data": self.read_factory_data, "client_connect": self.handle_client_connect, "network_stop": self.handle_network_stop, }, @@ -351,7 +354,7 @@ async def read_game_over(self, event: Event[bytearray]) -> None: self.running = False async def read_board_data(self, event: Event[bytearray]) -> None: - """Read board_data event from server.""" + """Read board_data event from server, reraise as `game_board_data`.""" buffer = Buffer(event.data) player_id: u8 = buffer.read_value(StructFormat.UBYTE) @@ -359,6 +362,15 @@ async def read_board_data(self, event: Event[bytearray]) -> None: await self.raise_event(Event("game_board_data", (player_id, array))) + async def read_factory_data(self, event: Event[bytearray]) -> None: + """Read factory_data event from server, reraise as `game_factory_data`.""" + buffer = Buffer(event.data) + + factory_id: u8 = buffer.read_value(StructFormat.UBYTE) + tiles = decode_numeric_uint8_counter(buffer) + + await self.raise_event(Event("game_factory_data", (factory_id, tiles))) + async def handle_network_stop(self, event: Event[None]) -> None: """Send EOF if connected and close socket.""" if self.not_connected: diff --git a/src/azul/game.py b/src/azul/game.py index 59c288f..f26a9dc 100644 --- a/src/azul/game.py +++ b/src/azul/game.py @@ -869,32 +869,55 @@ def render(self, surface: pygame.surface.Surface) -> None: self.text.render(surface) -class Factory(Grid): +class Factory(TileRenderer): """Represents a Factory.""" + __slots__ = ("factory_id", "tiles") color = WHITE outline = BLUE def __init__(self, factory_id: int) -> None: """Initialize factory.""" - super().__init__(f"Factory_{factory_id}", (2, 2), background=None) + super().__init__(f"Factory_{factory_id}", background=None) - self.number = factory_id + self.factory_id = factory_id + self.tiles: Counter[int] = Counter() - self.redraw() - self.visible = True + self.update_location_on_resize = True def __repr__(self) -> str: """Return representation of self.""" return f"{self.__class__.__name__}({self.number})" - def clear_image( + def bind_handlers(self) -> None: + """Register event handlers.""" + self.register_handlers( + { + "game_factory_data": self.handle_factory_data, + }, + ) + + async def handle_factory_data( self, - tile_size: tuple[int, int], - extra_space: tuple[int, int] | None, + event: Event[tuple[int, Counter[int]]], ) -> None: - """Clear self.image and draw circles.""" - super().clear_image(tile_size, extra_space) + """Handle `game_factory_data` event.""" + factory_id, tiles = event.data + + if factory_id != self.factory_id: + await trio.lowlevel.checkpoint() + return + + self.tiles = tiles + self.update_image() + self.visible = True + + await trio.lowlevel.checkpoint() + + def update_image(self) -> None: + """Update image.""" + self.clear_image((2, 2), extra=(16, 16)) + radius = 29 pygame.draw.circle( self.image, @@ -909,9 +932,9 @@ def clear_image( math.ceil(radius * 0.9), ) - def redraw(self) -> None: - """Redraw this factory.""" - super().update_image(offset=(8, 8), extra_space=(16, 16)) + for index, tile_color in enumerate(self.tiles.elements()): + y, x = divmod(index, 2) + self.blit_tile(tile_color, (x, y), (8, 8)) def get_tile_point( self, @@ -1839,12 +1862,14 @@ async def handle_return_to_title(self, _: Event[None]) -> None: class PlayState(GameState): """Game Play State.""" - __slots__ = ("exit_data",) + __slots__ = ("current_turn", "exit_data") def __init__(self) -> None: """Initialize Play State.""" super().__init__("play") + self.current_turn: int = 0 + # (0: normal | 1: error) self.exit_data: tuple[int, str, bool] | None = None @@ -1887,10 +1912,9 @@ async def handle_game_initial_config( event: Event[tuple[bool, int, int, int]], ) -> None: """Handle `game_initial_config` event.""" - varient_play, player_count, factory_count, current_turn = event.data - - print("handle_game_initial_config") - print((varient_play, player_count, factory_count, current_turn)) + varient_play, player_count, factory_count, self.current_turn = ( + event.data + ) center = Vector2.from_iter(SCREEN_SIZE) // 2 @@ -1907,7 +1931,6 @@ async def handle_game_initial_config( self.group_add(factory) # Add players - # TODO: Do it properly for index, degrees in enumerate(range(0, 360, 360 // player_count)): board = Board(index) board.location = ( diff --git a/src/azul/network_shared.py b/src/azul/network_shared.py index 30f41ee..92adb3e 100644 --- a/src/azul/network_shared.py +++ b/src/azul/network_shared.py @@ -44,25 +44,44 @@ Pos: TypeAlias = tuple[u8, u8] +def encode_tile_count(tile_color: int, tile_count: int) -> Buffer: + """Return buffer from tile color and count.""" + buffer = Buffer() + + buffer.write_value(StructFormat.UBYTE, tile_color) + buffer.write_value(StructFormat.UBYTE, tile_count) + + return buffer + + +def decode_tile_count(buffer: Buffer) -> tuple[int, int]: + """Read and return tile color and count from buffer.""" + tile_color = buffer.read_value(StructFormat.UBYTE) + tile_count = buffer.read_value(StructFormat.UBYTE) + + return (tile_color, tile_count) + + def encode_numeric_uint8_counter(counter: Counter[int]) -> Buffer: - """Return buffer from uint8 counter (both keys and values).""" + """Return buffer from uint8 counter.""" buffer = Buffer() + buffer.write_value(StructFormat.UBYTE, len(counter)) for key, value in counter.items(): assert isinstance(key, int) - buffer.write_value(StructFormat.UBYTE, key) assert value >= 0 - buffer.write_value(StructFormat.UBYTE, value) + buffer.extend(encode_tile_count(key, value)) + return buffer def decode_numeric_uint8_counter(buffer: Buffer) -> Counter[int]: - """Return buffer from uint8 counter (both keys and values).""" + """Read and return uint8 counter from buffer.""" data: dict[int, int] = {} - for _ in range(0, len(buffer), 2): - key = buffer.read_value(StructFormat.UBYTE) - value = buffer.read_value(StructFormat.UBYTE) + pair_count = buffer.read_value(StructFormat.UBYTE) + for _ in range(pair_count): + key, value = decode_tile_count(buffer) assert key not in data data[key] = value @@ -74,7 +93,7 @@ def encode_int8_array(array: NDArray[int8]) -> Buffer: buffer = Buffer() for value in array.flat: - buffer.write_value(StructFormat.BYTE, value) + buffer.write_value(StructFormat.BYTE, int(value)) return buffer @@ -98,6 +117,7 @@ class ClientBoundEvents(IntEnum): playing_as = auto() game_over = auto() board_data = auto() + factory_data = auto() class ServerBoundEvents(IntEnum): diff --git a/src/azul/server.py b/src/azul/server.py index ab78f9a..44adf47 100755 --- a/src/azul/server.py +++ b/src/azul/server.py @@ -53,10 +53,12 @@ ClientBoundEvents, ServerBoundEvents, encode_int8_array, + encode_numeric_uint8_counter, ) from azul.state import Phase, State if TYPE_CHECKING: + from collections import Counter from collections.abc import Awaitable, Callable from numpy import int8 @@ -93,6 +95,7 @@ def __init__(self, client_id: int) -> None: "server[write]->playing_as": cbe.playing_as, "server[write]->game_over": cbe.game_over, "server[write]->board_data": cbe.board_data, + "server[write]->factory_data": cbe.factory_data, }, ) sbe = ServerBoundEvents @@ -113,6 +116,7 @@ def bind_handlers(self) -> None: f"playing_as->network[{self.client_id}]": self.handle_playing_as, "game_over->network": self.handle_game_over, "board_data->network": self.handle_board_data, + "factory_data->network": self.handle_factory_data, }, ) @@ -184,6 +188,19 @@ async def handle_board_data( await self.write_event(Event("server[write]->board_data", buffer)) + async def handle_factory_data( + self, + event: Event[tuple[int, Counter[int]]], + ) -> None: + """Reraise as server[write]->factory_data.""" + factory_id, tiles = event.data + + buffer = Buffer() + buffer.write_value(StructFormat.UBYTE, factory_id) + buffer.extend(encode_numeric_uint8_counter(tiles)) + + await self.write_event(Event("server[write]->factory_data", buffer)) + class GameServer(network.Server): """Azul server. @@ -420,10 +437,8 @@ async def handle_server_start_new_game(self, event: Event[bool]) -> None: ), ) - print(f"{self.state.player_data = }") - - # Transmit board data for all players async with trio.open_nursery() as nursery: + # Transmit board data for player_id, player_data in self.state.player_data.items(): nursery.start_soon( self.raise_event, @@ -435,6 +450,21 @@ async def handle_server_start_new_game(self, event: Event[bool]) -> None: ), ), ) + # Transmit factory data + for ( + factory_id, + factory_tiles, + ) in self.state.factory_displays.items(): + nursery.start_soon( + self.raise_event, + Event( + "factory_data->network", + ( + factory_id, + factory_tiles, + ), + ), + ) await self.transmit_playing_as() From 086e0255dab3214b1cb76b9743e3ab1144a5c69f Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Fri, 29 Nov 2024 19:19:34 -0600 Subject: [PATCH 11/58] Add PatternRows and fix a few issues --- src/azul/crop.py | 59 +++++- src/azul/game.py | 419 ++++++++++++------------------------- src/azul/mr_floppy_test.py | 2 +- src/azul/sprite.py | 25 +-- 4 files changed, 203 insertions(+), 302 deletions(-) diff --git a/src/azul/crop.py b/src/azul/crop.py index 9aeb132..cbdbbab 100644 --- a/src/azul/crop.py +++ b/src/azul/crop.py @@ -2,14 +2,37 @@ # Programmed by CoolCat467 +from __future__ import annotations + +# Copyright (C) 2020-2024 CoolCat467 +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + __title__ = "Crop Functions" __author__ = "CoolCat467" __version__ = "0.0.0" + +from typing import TYPE_CHECKING + from pygame.color import Color from pygame.rect import Rect from pygame.surface import Surface +if TYPE_CHECKING: + from collections.abc import Callable, Generator, Iterable + def crop_color(surface: Surface, color: Color) -> Surface: """Crop out color from surface.""" @@ -46,10 +69,40 @@ def crop_color(surface: Surface, color: Color) -> Surface: return surf -def run() -> None: - """Run test of module.""" +def auto_crop_clear( + surface: Surface, + clear: Color | None = None, +) -> Surface: + """Remove unneccicary pixels from image.""" + if clear is None: + clear = Color(0, 0, 0, 0) + surface = surface.convert_alpha() + w, h = surface.get_size() + surface.lock() + + def find_end( + iterfunc: Callable[[int], Iterable[Color]], + rangeobj: Iterable[int], + ) -> int: + for x in rangeobj: + if not all(y == clear for y in iterfunc(x)): + return x + return x + + def column(x: int) -> Generator[Color, None, None]: + return (surface.get_at((x, y)) for y in range(h)) + + def row(y: int) -> Generator[Color, None, None]: + return (surface.get_at((x, y)) for x in range(w)) + + leftc = find_end(column, range(w)) + rightc = find_end(column, range(w - 1, -1, -1)) + topc = find_end(row, range(h)) + floorc = find_end(row, range(h - 1, -1, -1)) + surface.unlock() + dim = Rect(leftc, topc, rightc - leftc, floorc - topc) + return surface.subsurface(dim) if __name__ == "__main__": print(f"{__title__}\nProgrammed by {__author__}.\n") - run() diff --git a/src/azul/game.py b/src/azul/game.py index f26a9dc..58e6a00 100644 --- a/src/azul/game.py +++ b/src/azul/game.py @@ -32,7 +32,7 @@ import time import traceback from collections import Counter -from functools import lru_cache, wraps +from functools import lru_cache from pathlib import Path from typing import TYPE_CHECKING, Any, Final, TypeVar @@ -60,6 +60,7 @@ from azul import element_list, objects, sprite from azul.async_clock import Clock from azul.client import GameClient, read_advertisements +from azul.crop import auto_crop_clear from azul.network_shared import DEFAULT_PORT from azul.server import GameServer from azul.sound import SoundData, play_sound as base_play_sound @@ -83,6 +84,7 @@ ) from numpy.typing import NDArray + from pygame.sprite import LayeredDirty from typing_extensions import TypeVarTuple P = TypeVarTuple("P") @@ -169,7 +171,12 @@ } -@lru_cache +def vec2_to_location(vec: Vector2) -> tuple[int, int]: + """Return rounded location tuple from Vector2.""" + x, y = map(int, vec.rounded()) + return x, y + + def make_square_surf( color: ( pygame.color.Color @@ -229,41 +236,6 @@ def outline_rectangle( return surface -def auto_crop_clear( - surface: pygame.surface.Surface, - clear: pygame.color.Color | None = None, -) -> pygame.surface.Surface: - """Remove unneccicary pixels from image.""" - if clear is None: - clear = pygame.color.Color(0, 0, 0, 0) - surface = surface.convert_alpha() - w, h = surface.get_size() - surface.lock() - - def find_end( - iterfunc: Callable[[int], Iterable[pygame.color.Color]], - rangeobj: Iterable[int], - ) -> int: - for x in rangeobj: - if not all(y == clear for y in iterfunc(x)): - return x - return x - - def column(x: int) -> Generator[pygame.color.Color, None, None]: - return (surface.get_at((x, y)) for y in range(h)) - - def row(y: int) -> Generator[pygame.color.Color, None, None]: - return (surface.get_at((x, y)) for x in range(w)) - - leftc = find_end(column, range(w)) - rightc = find_end(column, range(w - 1, -1, -1)) - topc = find_end(row, range(h)) - floorc = find_end(row, range(h - 1, -1, -1)) - surface.unlock() - dim = pygame.rect.Rect(leftc, topc, rightc - leftc, floorc - topc) - return surface.subsurface(dim) - - @lru_cache def get_tile_color( tile_color: int, @@ -338,9 +310,6 @@ def add_symbol_to_tile_surf( surf.blit(symbolsurf, (int(x), int(y))) -# surf.blit(symbolsurf, (0, 0)) - - def get_tile_image( tile_color: int, tilesize: int, @@ -364,6 +333,7 @@ def get_tile_image( add_symbol_to_tile_surf(surf, tile_color, tilesize, greyshift) return surf + assert isinstance(color[0], int) surf = make_square_surf(color, tilesize) # Add tile symbol add_symbol_to_tile_surf(surf, tile_color, tilesize, greyshift) @@ -432,7 +402,7 @@ def clear_image( size += offset self.image = get_tile_container_image( - round(size), + vec2_to_location(size), self.background, ) @@ -573,7 +543,7 @@ async def handle_cursor_reached_destination( def move_to_front(self) -> None: """Move this sprite to front.""" - group: sprite.LayeredDirty = self.groups()[-1] + group: LayeredDirty = self.groups()[-1] group.move_to_front(self) async def handle_cursor_set_destination( @@ -733,9 +703,12 @@ def __init__(self, board_id: int) -> None: self.update_location_on_resize = True + # Clear image so rect is set + self.clear_image((5, 5)) + def __repr__(self) -> str: """Return representation of self.""" - return f"{self.__class__.__name__}({self.variant_play})" + return f"{self.__class__.__name__}({self.board_id})" def bind_handlers(self) -> None: """Register event handlers.""" @@ -766,7 +739,7 @@ async def handle_game_board_data( class Row(TileRenderer): """Represents one of the five rows each player has.""" - __slots__ = ("color", "size", "tiles") + __slots__ = ("color", "count", "size") greyshift = GREYSHIFT def __init__( @@ -795,16 +768,17 @@ def update_image(self) -> None: """Update self.image.""" self.clear_image((self.size, 1)) - for x in range(self.count): - self.blit_tile(self.color, (x, 0)) for x in range(self.count, self.size): - self.blit_tile(Tile.blank, (x, 0)) + self.blit_tile(Tile.blank, (self.size - x, 0)) + for x in range(self.count): + self.blit_tile(self.color, (self.size - x, 0)) + self.dirty = 1 def get_tile_point( self, screen_location: tuple[int, int] | Vector2, ) -> int | None: - """Return the xy choordinates of which tile intersects given a point. Returns None if no intersections.""" + """Return the x choordinate of which tile intersects given a point. Returns None if no intersections.""" pos = super().get_tile_point(screen_location) if pos is None: return None @@ -828,6 +802,79 @@ def set_background(self, color: tuple[int, int, int] | None) -> None: self.update_image() +class PatternRows(TileRenderer): + """Represents one of the five rows each player has.""" + + __slots__ = ( + "rows", + "rows_id", + ) + greyshift = GREYSHIFT + + def __init__( + self, + rows_id: int, + ) -> None: + """Initialize row.""" + super().__init__(f"Pattern_Rows_{rows_id}", background=None) + + self.add_component(sprite.DragClickEventComponent()) + + self.rows_id = rows_id + self.rows: dict[int, tuple[int, int]] = { + i: (Tile.blank, 0) for i in range(5) + } + + self.update_image() + self.visible = True + + def __repr__(self) -> str: + """Return representation of self.""" + return f"{self.__class__.__name__}({self.rows_id})" + + def update_image(self) -> None: + """Update self.image.""" + self.clear_image((5, 5)) + + for y in range(5): + tile_color, count = self.rows[y] + for x in range(count, (y + 1)): + self.blit_tile(Tile.blank, (4 - x, y)) + for x in range(count): + self.blit_tile(tile_color, (4 - x, y)) + self.dirty = 1 + + def set_row_data( + self, + row_id: int, + tile_color: Tile, + tile_count: int, + ) -> None: + """Set row data and update image.""" + assert row_id in self.rows + self.rows[row_id] = (tile_color, tile_count) + self.update_image() + + def get_tile_point( + self, + screen_location: tuple[int, int] | Vector2, + ) -> int | None: + """Return the x choordinate of which tile intersects given a point. Returns None if no intersections.""" + point = super().get_tile_point(screen_location) + if point is None: + return None + x, y = point + # If point is not valid for that row, say invalid + if (4 - x) > y: + return None + return point + + def set_background(self, color: tuple[int, int, int] | None) -> None: + """Set the background color for this row.""" + self.background = color + self.update_image() + + class FloorLine(Row): """Represents a player's floor line.""" @@ -885,9 +932,11 @@ def __init__(self, factory_id: int) -> None: self.update_location_on_resize = True + self.add_component(sprite.DragClickEventComponent()) + def __repr__(self) -> str: """Return representation of self.""" - return f"{self.__class__.__name__}({self.number})" + return f"{self.__class__.__name__}({self.factory_id})" def bind_handlers(self) -> None: """Register event handlers.""" @@ -935,6 +984,7 @@ def update_image(self) -> None: for index, tile_color in enumerate(self.tiles.elements()): y, x = divmod(index, 2) self.blit_tile(tile_color, (x, y), (8, 8)) + self.dirty = 1 def get_tile_point( self, @@ -1116,7 +1166,7 @@ class MenuState(GameState): fontsize = BUTTONFONTSIZE def __init__(self, name: str) -> None: - """Initialize GameState and set up self.bh.""" + """Initialize GameState and set up 30.""" super().__init__(name) def add_button( @@ -1127,7 +1177,7 @@ def add_button( location: tuple[int, int] | None = None, size: int = fontsize, minlen: int = button_minimum, - ) -> int: + ) -> None: """Add a new objects.Button object to group.""" button = KwargButton( name, @@ -1148,7 +1198,7 @@ def add_text( color: tuple[int, int, int] = BUTTON_TEXT_COLOR, size: int = fontsize, outline: tuple[int, int, int] = BUTTON_TEXT_OUTLINE, - ) -> int: + ) -> None: """Add a new objects.Text object to self.game with arguments. Return text id.""" text = KwargOutlineText( name, @@ -1196,60 +1246,6 @@ def to_state_by_attributes() -> None: return to_state_by_attributes - def with_update( - self, - update_function: Callable[[], None], - ) -> Callable[[Callable[[], None]], Callable[[], None]]: - """Return a wrapper for a function that will call update_function after function.""" - - def update_wrapper(function: Callable[[], None]) -> Callable[[], None]: - """Wrap anything that might require a screen update.""" - - @wraps(function) - def function_with_update() -> None: - """Call main function, then update function.""" - function() - update_function() - - return function_with_update - - return update_wrapper - - def update_text( - self, - text_name: str, - value_function: Callable[[], str], - ) -> Callable[[], None]: - """Update text object with text_name's display value.""" - - def updater() -> None: - """Update text object {text_name}'s value with {value_function}.""" - assert self.game is not None - text = self.game.get_object_by_name(f"objects.Text{text_name}") - assert isinstance(text, objects.Text) - text.update_value(value_function()) - - return updater - - def toggle_button_state( - self, - textname: str, - boolattr: str, - textfunc: Callable[[bool], str], - ) -> Callable[[], None]: - """Return function that will toggle the value of text object , toggling attribute , and setting text value with textfunc.""" - - def valfunc() -> str: - """Return the new value for the text object. Gets called AFTER value is toggled.""" - return textfunc(getattr(self, boolattr)) - - @self.with_update(self.update_text(textname, valfunc)) - def toggle_value() -> None: - """Toggle the value of boolattr.""" - self.set_var(boolattr, not getattr(self, boolattr)) - - return toggle_value - class InitializeState(AsyncState["AzulClient"]): """Initialize state.""" @@ -1442,13 +1438,13 @@ def host_text(x: object) -> str: self.add_text( "Host", host_text(self.host_mode), - (cx, cy - self.bh * 3), + (cx, cy - 30 * 3), ) self.add_button( "ToggleHost", "Toggle", self.toggle_button_state("Host", "host_mode", host_text), - (cx, cy - self.bh * 2), + (cx, cy - 30 * 2), size=int(self.fontsize / 1.5), ) @@ -1462,7 +1458,7 @@ def varient_text(x: object) -> str: self.add_text( "Variant", varient_text(self.variant_play), - (cx, cy - self.bh), + (cx, cy - 30), ) self.add_button( "ToggleVarient", @@ -1475,9 +1471,9 @@ def varient_text(x: object) -> str: self.add_text( "Players", f"Players: {self.player_count}", - (cx, cy + self.bh), + (cx, cy + 30), ) - add_numbers(2, 4, 70, cx, int(cy + self.bh * 2)) + add_numbers(2, 4, 70, cx, int(cy + 30 * 2)) var_to_state = self.var_dependant_to_state( FactoryOffer=("host_mode", True), @@ -1487,142 +1483,8 @@ def varient_text(x: object) -> str: "StartGame", "Start Game", var_to_state, - (cx, cy + self.bh * 3), - ) - - def exit_actions(self) -> None: - """Start game.""" - assert self.game is not None - self.game.start_game( - self.player_count, - self.variant_play, - self.host_mode, - ) - self.game.bag.full_reset() - - -class PhaseFactoryOffer(GameState): - """Game state when it's the Factory Offer Stage.""" - - __slots__ = () - - def __init__(self) -> None: - """Initialize factory offer phase.""" - super().__init__("FactoryOffer") - - -class PhaseFactoryOfferNetworked(PhaseFactoryOffer): - """Factory offer phase but networked.""" - - __slots__ = () - - def __init__(self) -> None: - """Initialize factory offer networked.""" - GameState.__init__(self, "FactoryOfferNetworked") - - def check_state(self) -> str: - """Go to networked wall tiling.""" - return "WallTilingNetworked" - - -class PhaseWallTiling(GameState): - """Wall tiling game phase.""" - - # __slots__ = () - def __init__(self) -> None: - """Initialize will tiling phase.""" - super().__init__("WallTiling") - - def entry_actions(self) -> None: - """Start wall tiling.""" - assert self.game is not None - self.next_starter: int = 0 - self.not_processed = [] - - self.game.player_turn_over() - - # For each player, - for player_id in range(self.game.players): - # Activate wall tiling mode. - player = self.game.get_player(player_id) - player.wall_tiling() - # Add that player's player_id to the list of not-processed players. - self.not_processed.append(player.player_id) - - # Start processing players. - self.game.next_turn() - - def do_actions(self) -> None: - """Do game actions.""" - assert self.game is not None - if self.not_processed: - if self.game.player_turn in self.not_processed: - player = self.game.get_player(self.game.player_turn) - if player.done_wall_tiling(): - # Once player is done wall tiling, score their moves. - # Also gets if they had the number one tile. - number_one = player.score_phase() - - if number_one: - # If player had the number one tile, remember that. - self.next_starter = self.game.player_turn - # Then, add the number one tile back to the table center. - table = self.game.get_object_by_name("TableCenter") - assert isinstance(table, TableCenter) - table.add_number_one_tile() - # After calculating their score, delete player from un-processed list - self.not_processed.remove(self.game.player_turn) - # and continue to the next un-processed player. - self.game.next_turn() - else: - self.game.next_turn() - - def check_state(self) -> str | None: - """Go to next state if ready.""" - assert self.game is not None - cursor = self.game.get_object_by_name("Cursor") - assert isinstance(cursor, Cursor) - if not self.not_processed and not cursor.is_holding(): - return "PrepareNext" - return None - - def exit_actions(self) -> None: - """Update who's turn it is.""" - assert self.game is not None - # Set up the player that had the number one tile to be the starting player next round. - self.game.player_turn_over() - # Goal: make (self.player_turn + 1) % self.players = self.next_starter - nturn = self.next_starter - 1 - if nturn < 0: - nturn += self.game.players - self.game.player_turn = nturn - - -class PhasePrepareNext(GameState): - """Prepare next phase of game.""" - - __slots__ = ("new_round",) - - def __init__(self) -> None: - """Initialize prepare next state.""" - super().__init__("PrepareNext") - self.new_round = False - - def entry_actions(self) -> None: - """Find out if game continues.""" - assert self.game is not None - players = ( - self.game.get_player(player_id) - for player_id in range(self.game.players) + (cx, cy + 30 * 3), ) - complete = (player.has_horzontal_line() for player in players) - self.new_round = not any(complete) - - def check_state(self) -> str: - """Go to factory offer if new round else end screen.""" - if self.new_round: - return "FactoryOffer" - return "End" class EndScreen(MenuState): @@ -1838,27 +1700,6 @@ async def handle_return_to_title(self, _: Event[None]) -> None: await self.machine.set_state("title") -# async def check_conditions(self) -> str | None: -# return None - - -## async def entry_actions(self) -> None: -## """Set up buttons.""" -## assert self.machine is not None -## self.id = self.machine.new_group("initialize") -## -## self.group_add(Cursor()) -## await self.manager.raise_event(Event("cursor_drag", [3, 5])) -## self.manager.register_handler("PygameMouseMotion", self.mouse_moved) -## -## ## board = Board() -## #### board.place_tile((2, 2), Tile.red) -## ## board.location = Vector2.from_iter(SCREEN_SIZE) // 2 -## ## self.group_add(board) -## -## center.add_tiles((0, 1, 2, 3, 5)) - - class PlayState(GameState): """Game Play State.""" @@ -1901,11 +1742,11 @@ async def entry_actions(self) -> None: self.group_add(center) # self.group_add(()) - ## gameboard = GameBoard( - ## 45, - ## ) - ## gameboard.location = [x // 2 for x in SCREEN_SIZE] - ## self.group_add(gameboard) + ##gameboard = GameBoard( + ## 45, + ##) + ##gameboard.location = [x // 2 for x in SCREEN_SIZE] + ##self.group_add(gameboard) async def handle_game_initial_config( self, @@ -1919,29 +1760,41 @@ async def handle_game_initial_config( center = Vector2.from_iter(SCREEN_SIZE) // 2 # Add factories - for index, degrees in enumerate(range(0, 360, 360 // factory_count)): + each = 360 / factory_count + degrees: float = -90 + for index in range(factory_count): factory = Factory(index) - factory.location = ( + factory.location = vec2_to_location( Vector2.from_degrees( - degrees - 90, + degrees, 145, ) - + center + + center, ) self.group_add(factory) + degrees += each + # Add players - for index, degrees in enumerate(range(0, 360, 360 // player_count)): + each = 360 / player_count + degrees = -(90 / player_count) + for index in range(player_count): board = Board(index) - board.location = ( + board.rect.midleft = vec2_to_location( Vector2.from_degrees( - degrees - 45, + degrees, 300, ) - + center + + center, ) self.group_add(board) + pattern_rows = PatternRows(index) + pattern_rows.rect.bottomright = board.rect.bottomleft + self.group_add(pattern_rows) + + degrees += each + async def check_conditions(self) -> str | None: """Return to title if client component doesn't exist.""" if not self.machine.manager.component_exists("network"): @@ -1980,8 +1833,6 @@ async def handle_client_disconnected(self, event: Event[str]) -> None: self.exit_data = (1, f"Client Disconnected$${error}", False) - # await self.do_actions() - async def do_actions(self) -> None: """Perform actions for this State.""" # print(f"{self.__class__.__name__} do_actions tick") @@ -2208,7 +2059,7 @@ def cli_run() -> None: run() except ExceptionGroup as exc: print(exc) - exception = traceback.format_exception(exc) + exception = "".join(traceback.format_exception(exc)) ## raise ## except BaseException as ex: ## screenshot_last_frame() @@ -2217,7 +2068,7 @@ def cli_run() -> None: finally: pygame.quit() if exception is not None: - print("".join(exception), file=sys.stderr) + print(exception, file=sys.stderr) if __name__ == "__main__": diff --git a/src/azul/mr_floppy_test.py b/src/azul/mr_floppy_test.py index 1b8a772..5275dda 100644 --- a/src/azul/mr_floppy_test.py +++ b/src/azul/mr_floppy_test.py @@ -257,7 +257,7 @@ def controller( async def drag(self, event: Event[sprite.DragEvent]) -> None: """Move by relative from drag.""" - if event.data.button != 1: + if not event.data.buttons[1]: return self.location += event.data.rel self.dirty = 1 diff --git a/src/azul/sprite.py b/src/azul/sprite.py index e55ea4e..ba625d4 100644 --- a/src/azul/sprite.py +++ b/src/azul/sprite.py @@ -597,7 +597,7 @@ class DragEvent(NamedTuple): pos: tuple[int, int] rel: tuple[int, int] - button: int + buttons: dict[int, bool] class DragClickEventComponent(Component): @@ -672,20 +672,17 @@ async def motion( if not self.manager_exists: return async with trio.open_nursery() as nursery: - for button, pressed in self.pressed.items(): - if not pressed: - continue - nursery.start_soon( - self.raise_event, - Event( - "drag", - DragEvent( - event.data["pos"], - event.data["rel"], - button, - ), + nursery.start_soon( + self.raise_event, + Event( + "drag", + DragEvent( + event.data["pos"], + event.data["rel"], + self.pressed, ), - ) + ), + ) class GroupProcessor(AsyncStateMachine): From db3f8c3767933e1bfb153f787bab6291c68baf34 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Fri, 29 Nov 2024 19:20:21 -0600 Subject: [PATCH 12/58] Vectorize detection --- src/azul/game.py | 31 +++++++------------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/src/azul/game.py b/src/azul/game.py index 58e6a00..968adc4 100644 --- a/src/azul/game.py +++ b/src/azul/game.py @@ -413,26 +413,19 @@ def blit_tile( offset: tuple[int, int] | None = None, ) -> None: """Blit the surface of a given tile object onto self.image at given tile location. It is assumed that all tile locations are xy tuples.""" - x, y = tile_location - if offset is None: - ox, oy = 0, 0 - else: - ox, oy = offset + tile_full = self.tile_size + self.tile_separation - ox += self.tile_separation - oy += self.tile_separation + position = Vector2.from_iter(tile_location) * tile_full + if offset is not None: + position += offset + position += (self.tile_separation, self.tile_separation) surf = get_tile_image(tile_color, self.tile_size, self.greyshift) assert self.image is not None - tile_full = self.tile_size + self.tile_separation - self.image.blit( surf, - ( - round(x * tile_full + ox), - round(y * tile_full + oy), - ), + vec2_to_location(position), ) def to_image_surface_location( @@ -774,16 +767,6 @@ def update_image(self) -> None: self.blit_tile(self.color, (self.size - x, 0)) self.dirty = 1 - def get_tile_point( - self, - screen_location: tuple[int, int] | Vector2, - ) -> int | None: - """Return the x choordinate of which tile intersects given a point. Returns None if no intersections.""" - pos = super().get_tile_point(screen_location) - if pos is None: - return None - return pos[0] - def get_placed(self) -> int: """Return the number of tiles in self that are not fake tiles, like grey ones.""" return self.count @@ -858,7 +841,7 @@ def set_row_data( def get_tile_point( self, screen_location: tuple[int, int] | Vector2, - ) -> int | None: + ) -> tuple[int, int] | None: """Return the x choordinate of which tile intersects given a point. Returns None if no intersections.""" point = super().get_tile_point(screen_location) if point is None: From 60ea0f6cf37f29500b12bca4a1eee3814a147789 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Fri, 29 Nov 2024 21:00:35 -0600 Subject: [PATCH 13/58] Fix a ton of type and lint errors --- src/azul/game.py | 166 ++++++---------------------------------------- src/azul/state.py | 11 ++- 2 files changed, 27 insertions(+), 150 deletions(-) diff --git a/src/azul/game.py b/src/azul/game.py index 968adc4..77fa66c 100644 --- a/src/azul/game.py +++ b/src/azul/game.py @@ -56,6 +56,7 @@ WINDOWRESIZED, ) from pygame.rect import Rect +from pygame.sprite import LayeredDirty from azul import element_list, objects, sprite from azul.async_clock import Clock @@ -84,7 +85,6 @@ ) from numpy.typing import NDArray - from pygame.sprite import LayeredDirty from typing_extensions import TypeVarTuple P = TypeVarTuple("P") @@ -438,9 +438,11 @@ def to_image_surface_location( def get_tile_point( self, screen_location: tuple[int, int] | Vector2, - ) -> tuple[int, int] | None: + ) -> Vector2 | None: """Return the xy choordinates of which tile intersects given a point or None.""" # Can't get tile if screen location doesn't intersect our hitbox! + if isinstance(screen_location, Vector2): + screen_location = vec2_to_location(screen_location) if not self.is_selected(screen_location): return None @@ -536,7 +538,8 @@ async def handle_cursor_reached_destination( def move_to_front(self) -> None: """Move this sprite to front.""" - group: LayeredDirty = self.groups()[-1] + group = self.groups()[-1] + assert isinstance(group, LayeredDirty) group.move_to_front(self) async def handle_cursor_set_destination( @@ -656,7 +659,7 @@ def fake_tile_exists(self, xy: tuple[int, int]) -> bool: """Return if tile at given position is a fake tile.""" return self.get_tile(xy) < 0 - def place_tile(self, xy: tuple[int, int], tile_color: int) -> bool: + def place_tile(self, xy: tuple[int, int], tile_color: int) -> None: """Place tile at given position.""" x, y = xy self.data[y, x] = tile_color @@ -841,14 +844,13 @@ def set_row_data( def get_tile_point( self, screen_location: tuple[int, int] | Vector2, - ) -> tuple[int, int] | None: + ) -> Vector2 | None: """Return the x choordinate of which tile intersects given a point. Returns None if no intersections.""" point = super().get_tile_point(screen_location) if point is None: return None - x, y = point # If point is not valid for that row, say invalid - if (4 - x) > y: + if (4 - point.x) > point.y: return None return point @@ -869,34 +871,31 @@ def __init__(self, floor_line_id: int) -> None: # self.font = Font(FONT, round(self.tile_size*1.2), color=BLACK, cx=False, cy=False) self.text = objects.Text( - round(self.tile_size * 1.2), - BLACK, - cx=False, - cy=False, + "text object", + pygame.font.Font(FONT, round(self.tile_size * 1.2)), ) + self.text.color = BLACK self.numbers = [-255 for _ in range(self.size)] def __repr__(self) -> str: """Return representation of self.""" - return f"{self.__class__.__name__}({self.player!r})" + return f"{self.__class__.__name__}(<>)" def render(self, surface: pygame.surface.Surface) -> None: """Update self.image.""" - super().render(surface) - sx, sy = self.location - assert self.width_height is not None, "Should be impossible." - w, h = self.width_height + w, h = self.rect.size + tile_full = self.tile_separation + self.tile_size for x in range(self.size): xy = round( - x * self.tile_full + self.tile_separation + sx - w / 2, + x * tile_full + self.tile_separation + sx - w / 2, ), round( self.tile_separation + sy - h / 2, ) - self.text.update_value(str(self.numbers[x])) + self.text.text = str(self.numbers[x]) self.text.location = Vector2(*xy) - self.text.render(surface) + # self.text.render(surface) class Factory(TileRenderer): @@ -972,7 +971,7 @@ def update_image(self) -> None: def get_tile_point( self, screen_location: tuple[int, int] | Vector2, - ) -> tuple[int, int] | None: + ) -> Vector2 | None: """Get tile point accounting for offset.""" return super().get_tile_point( Vector2.from_iter(screen_location) - (8, 8), @@ -1344,132 +1343,6 @@ def check_state(self) -> str: return "title" -class SettingsState(MenuState): - """Game state when user is defining game type, players, etc.""" - - def __init__(self) -> None: - """Initialize settings.""" - super().__init__("settings") - - self.player_count = 0 # 2 - self.host_mode = True - self.variant_play = False - - async def entry_actions(self) -> None: - """Add cursor object and tons of button and text objects to the game.""" - await super().entry_actions() - - def add_numbers( - start: int, - end: int, - width_each: int, - cx: int, - cy: int, - ) -> None: - """Add numbers.""" - count = end - start + 1 - evencount = count % 2 == 0 - mid = count // 2 - - def add_number( - number: int, - display: str | int, - ) -> None: - """Add number.""" - if evencount: - if number < mid: - x = number - start - 0.5 - else: - x = number - mid + 0.5 - else: - if number < mid: - x = number - start + 1 - elif number == mid: - x = 0 - else: - x = number - mid - - @self.with_update( - self.update_text( - "Players", - lambda: f"Players: {self.player_count}", - ), - ) - def set_player_count() -> None: - """Set variable player_count to {display} while updating text.""" - return self.set_var("player_count", display) - - self.add_button( - f"SetCount{number}", - str(display), - set_player_count, - (int(cx + (width_each * x)), int(cy)), - size=int(self.fontsize / 1.5), - minlen=3, - ) - - for i in range(count): - add_number(i, start + i) - - sw, sh = SCREEN_SIZE - cx = sw // 2 - cy = sh // 2 - - def host_text(x: object) -> str: - return f"Host Mode: {x}" - - self.add_text( - "Host", - host_text(self.host_mode), - (cx, cy - 30 * 3), - ) - self.add_button( - "ToggleHost", - "Toggle", - self.toggle_button_state("Host", "host_mode", host_text), - (cx, cy - 30 * 2), - size=int(self.fontsize / 1.5), - ) - - ## # TEMPORARY: Hide everything to do with "Host Mode", networked games aren't done yet. - ## assert self.game is not None - ## self.game.set_attr_all("visible", False) - - def varient_text(x: object) -> str: - return f"Variant Play: {x}" - - self.add_text( - "Variant", - varient_text(self.variant_play), - (cx, cy - 30), - ) - self.add_button( - "ToggleVarient", - "Toggle", - self.toggle_button_state("Variant", "variant_play", varient_text), - (cx, cy), - size=int(self.fontsize / 1.5), - ) - - self.add_text( - "Players", - f"Players: {self.player_count}", - (cx, cy + 30), - ) - add_numbers(2, 4, 70, cx, int(cy + 30 * 2)) - - var_to_state = self.var_dependant_to_state( - FactoryOffer=("host_mode", True), - FactoryOfferNetworked=("host_mode", False), - ) - self.add_button( - "StartGame", - "Start Game", - var_to_state, - (cx, cy + 30 * 3), - ) - - class EndScreen(MenuState): """End screen state.""" @@ -1888,7 +1761,6 @@ def __init__(self, manager: ExternalRaiseManager) -> None: InitializeState(), TitleState(), CreditsState(), - SettingsState(), PlayHostingState(), PlayInternalHostingState(), PlayJoiningState(), diff --git a/src/azul/state.py b/src/azul/state.py index 755bc9b..0041124 100644 --- a/src/azul/state.py +++ b/src/azul/state.py @@ -104,7 +104,9 @@ def generate_bag_contents() -> Counter[int]: def bag_draw_tile(bag: Counter[int]) -> int: """Return drawn tile from bag. Mutates bag.""" - tile = random.choice(tuple(bag.elements())) + # S311 Standard pseudo-random generators are not suitable for + # cryptographic purposes + tile = random.choice(tuple(bag.elements())) # noqa: S311 bag[tile] -= 1 return tile @@ -651,6 +653,7 @@ class State(NamedTuple): @classmethod def blank(cls) -> Self: + """Return new blank state.""" return cls( varient_play=False, current_phase=Phase.end, @@ -1230,7 +1233,7 @@ def manual_wall_tiling_action( result = new_state.get_manual_wall_tiling_locations_for_player( player_id, ) - if isinstance(result, tuple) or result is None: + if not isinstance(result, self.__class__): return new_state._manual_wall_tiling_maybe_next_turn() return result._manual_wall_tiling_maybe_next_turn() @@ -1249,7 +1252,9 @@ def run() -> None: ## last_turn = state.current_turn actions = tuple(state.yield_actions()) print(f"{len(actions) = }") - action = random.choice(actions) + # S311 Standard pseudo-random generators are not suitable + # for cryptographic purposes + action = random.choice(actions) # noqa: S311 ## pprint(action) state = state.preform_action(action) From 988271c7cb4b9b585bd1fc1f0f6af1f8d889bfb7 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sat, 30 Nov 2024 00:08:33 -0700 Subject: [PATCH 14/58] Implement more parts of grabbing tile logic --- src/azul/client.py | 84 +++++++++++++ src/azul/game.py | 141 +++++++++++++++++---- src/azul/network_shared.py | 6 + src/azul/server.py | 250 ++++++++++++++++++++++++++++++++++--- src/azul/state.py | 2 +- src/azul/vector.py | 6 + 6 files changed, 447 insertions(+), 42 deletions(-) diff --git a/src/azul/client.py b/src/azul/client.py index 64ac3f4..2d9be7c 100644 --- a/src/azul/client.py +++ b/src/azul/client.py @@ -47,6 +47,9 @@ if TYPE_CHECKING: from mypy_extensions import u8 + from azul.state import Tile + from azul.vector import Vector2 + async def read_advertisements( timeout: int = 3, # noqa: ASYNC109 @@ -165,6 +168,9 @@ def __init__(self, name: str) -> None: self.register_network_write_events( { "encryption_response->server": sbe.encryption_response, + "factory_clicked->server[write]": sbe.factory_clicked, + "cursor_location->server[write]": sbe.cursor_location, + "pattern_row_clicked->server[write]": sbe.pattern_row_clicked, }, ) cbe = ClientBoundEvents @@ -177,6 +183,9 @@ def __init__(self, name: str) -> None: cbe.game_over: "server->game_over", cbe.board_data: "server->board_data", cbe.factory_data: "server->factory_data", + cbe.cursor_data: "server->cursor_data", + cbe.table_data: "server->table_data", + cbe.cursor_movement_mode: "server->cursor_movement_mode", }, ) @@ -195,8 +204,14 @@ def bind_handlers(self) -> None: "server->game_over": self.read_game_over, "server->board_data": self.read_board_data, "server->factory_data": self.read_factory_data, + "server->cursor_data": self.read_cursor_data, + "server->table_data": self.read_table_data, + "server->cursor_movement_mode": self.read_cursor_movement_mode, "client_connect": self.handle_client_connect, "network_stop": self.handle_network_stop, + "game_factory_clicked": self.write_game_factory_clicked, + "game_cursor_location_transmit": self.write_game_cursor_location_transmit, + "game_pattern_row_clicked": self.write_game_pattern_row_clicked, }, ) @@ -371,6 +386,75 @@ async def read_factory_data(self, event: Event[bytearray]) -> None: await self.raise_event(Event("game_factory_data", (factory_id, tiles))) + async def read_cursor_data(self, event: Event[bytearray]) -> None: + """Read cursor_data event from server, reraise as `game_cursor_data`.""" + buffer = Buffer(event.data) + + tiles = decode_numeric_uint8_counter(buffer) + + await self.raise_event(Event("game_cursor_data", tiles)) + + async def read_table_data(self, event: Event[bytearray]) -> None: + """Read table_data event from server, reraise as `game_table_data`.""" + buffer = Buffer(event.data) + + tiles = decode_numeric_uint8_counter(buffer) + + await self.raise_event(Event("game_table_data", tiles)) + + async def read_cursor_movement_mode(self, event: Event[bytearray]) -> None: + """Read cursor_movement_mode event from server, reraise as `game_cursor_set_movement_mode`.""" + buffer = Buffer(event.data) + + client_mode = buffer.read_value(StructFormat.BOOL) + + await self.raise_event( + Event("game_cursor_set_movement_mode", client_mode), + ) + + async def write_game_factory_clicked( + self, + event: Event[tuple[int, Tile]], + ) -> None: + """Write factory_clicked event to server.""" + factory_id, tile = event.data + buffer = Buffer() + + buffer.write_value(StructFormat.UBYTE, factory_id) + buffer.write_value(StructFormat.UBYTE, tile) + + await self.raise_event(Event("factory_clicked->server[write]", buffer)) + + async def write_game_cursor_location_transmit( + self, + event: Event[Vector2], + ) -> None: + """Write cursor_location_transmit event to server.""" + scaled_location = event.data + + x, y = map(int, (scaled_location * 4096).rounded()) # 2 ** 12 + + position = (x & 0xFFF) << 3 | (y & 0xFFF) + buffer = position.to_bytes(3, "big") + + await self.raise_event(Event("cursor_location->server[write]", buffer)) + + async def write_game_pattern_row_clicked( + self, + event: Event[tuple[int, Vector2]], + ) -> None: + """Write factory_clicked event to server.""" + row_id, location = event.data + buffer = Buffer() + + buffer.write_value(StructFormat.UBYTE, row_id) + buffer.write_value(StructFormat.UBYTE, int(location.x)) + buffer.write_value(StructFormat.UBYTE, int(location.y)) + + await self.raise_event( + Event("pattern_row_clicked->server[write]", buffer), + ) + async def handle_network_stop(self, event: Event[None]) -> None: """Send EOF if connected and close socket.""" if self.not_connected: diff --git a/src/azul/game.py b/src/azul/game.py index 77fa66c..b784c8c 100644 --- a/src/azul/game.py +++ b/src/azul/game.py @@ -461,7 +461,7 @@ def get_tile_point( if value > self.tile_size: return None # Otherwise, not in separation region, so we should be good - return tile_position + return tile_position.floored() class Cursor(TileRenderer): @@ -511,22 +511,23 @@ def bind_handlers(self) -> None: """Register handlers.""" self.register_handlers( { - "cursor_drag": self.handle_cursor_drag, + "game_cursor_data": self.handle_cursor_drag, "cursor_reached_destination": self.handle_cursor_reached_destination, - "cursor_set_destination": self.handle_cursor_set_destination, - "cursor_set_movement_mode": self.handle_cursor_set_movement_mode, + "game_cursor_set_destination": self.handle_cursor_set_destination, + "game_cursor_set_movement_mode": self.handle_cursor_set_movement_mode, }, ) - async def handle_cursor_drag(self, event: Event[Iterable[int]]) -> None: + async def handle_cursor_drag(self, event: Event[Counter[int]]) -> None: """Drag one or more tiles.""" - await trio.lowlevel.checkpoint() - for tile_color in event.data: + self.tiles.clear() + for tile_color in event.data.elements(): if tile_color == Tile.one: self.tiles.insert(0, tile_color) else: self.tiles.append(tile_color) self.update_image() + await trio.lowlevel.checkpoint() async def handle_cursor_reached_destination( self, @@ -564,7 +565,20 @@ async def handle_pygame_mouse_motion( """Set location to event data.""" self.move_to_front() self.location = event.data["pos"] - await trio.lowlevel.checkpoint() + + ##transmit_location = Vector2.from_iter( + ## x / y for x, y in zip(self.location, SCREEN_SIZE, strict=False) + ##) + ## + ### Transmit to server + ### Event level to so reaches client + ##await self.raise_event( + ## Event( + ## "game_cursor_location_transmit", + ## transmit_location, + ## 2, + ## ) + ##) async def handle_cursor_set_movement_mode( self, @@ -818,6 +832,10 @@ def __repr__(self) -> str: """Return representation of self.""" return f"{self.__class__.__name__}({self.rows_id})" + def bind_handlers(self) -> None: + """Register click event handler.""" + self.register_handler("click", self.handle_click) + def update_image(self) -> None: """Update self.image.""" self.clear_image((5, 5)) @@ -859,6 +877,28 @@ def set_background(self, color: tuple[int, int, int] | None) -> None: self.background = color self.update_image() + async def handle_click( + self, + event: Event[sprite.PygameMouseButtonEventData], + ) -> None: + """Handle click event.""" + point = self.get_tile_point(event.data["pos"]) + if point is None: + await trio.lowlevel.checkpoint() + return + + # Transmit to server + await self.raise_event( + Event( + "game_pattern_row_clicked", + ( + self.rows_id, + point.floored(), + ), + 2, + ), + ) + class FloorLine(Row): """Represents a player's floor line.""" @@ -925,26 +965,10 @@ def bind_handlers(self) -> None: self.register_handlers( { "game_factory_data": self.handle_factory_data, + "click": self.handle_click, }, ) - async def handle_factory_data( - self, - event: Event[tuple[int, Counter[int]]], - ) -> None: - """Handle `game_factory_data` event.""" - factory_id, tiles = event.data - - if factory_id != self.factory_id: - await trio.lowlevel.checkpoint() - return - - self.tiles = tiles - self.update_image() - self.visible = True - - await trio.lowlevel.checkpoint() - def update_image(self) -> None: """Update image.""" self.clear_image((2, 2), extra=(16, 16)) @@ -973,9 +997,62 @@ def get_tile_point( screen_location: tuple[int, int] | Vector2, ) -> Vector2 | None: """Get tile point accounting for offset.""" - return super().get_tile_point( + point = super().get_tile_point( Vector2.from_iter(screen_location) - (8, 8), ) + if point is None: + return None + if any(x >= 2 for x in point): + return None + return point + + async def handle_factory_data( + self, + event: Event[tuple[int, Counter[int]]], + ) -> None: + """Handle `game_factory_data` event.""" + factory_id, tiles = event.data + + if factory_id != self.factory_id: + await trio.lowlevel.checkpoint() + return + + self.tiles = tiles + self.update_image() + self.visible = True + + await trio.lowlevel.checkpoint() + + async def handle_click( + self, + event: Event[sprite.PygameMouseButtonEventData], + ) -> None: + """Handle click event.""" + point = self.get_tile_point(event.data["pos"]) + if point is None: + await trio.lowlevel.checkpoint() + return + + index = int(point.y * 2 + point.x) + tile_color = tuple(self.tiles.elements())[index] + + if tile_color < 0: + # Do not send non-real tiles + await trio.lowlevel.checkpoint() + return + + # Transmit to server + # Needs level 2 to reach server client + await self.raise_event( + Event( + "game_factory_clicked", + ( + self.factory_id, + Tile(tile_color), + ), + 2, + ), + ) class TableCenter(TileRenderer): @@ -996,6 +1073,10 @@ def __repr__(self) -> str: """Return representation of self.""" return f"{self.__class__.__name__}()" + def bind_handlers(self) -> None: + """Register event handlers.""" + self.register_handler("game_table_data", self.update_board_data) + def iter_tiles(self) -> Generator[int, None, None]: """Yield tile colors.""" count = 0 @@ -1044,6 +1125,12 @@ def pull_tiles(self, tile_color: int) -> list[int]: tile_count = self.tiles.pop(tile_color) return [tile_color] * tile_count + async def update_board_data(self, event: Event[Counter[int]]) -> None: + """Update table center board data.""" + self.tiles = event.data + self.update_image() + await trio.lowlevel.checkpoint() + class HaltState(AsyncState["AzulClient"]): """Halt state to set state to None so running becomes False.""" @@ -1647,6 +1734,8 @@ async def handle_game_initial_config( pattern_rows = PatternRows(index) pattern_rows.rect.bottomright = board.rect.bottomleft + if index == self.current_turn: + pattern_rows.set_background(DARKGREEN) self.group_add(pattern_rows) degrees += each diff --git a/src/azul/network_shared.py b/src/azul/network_shared.py index 92adb3e..84168ef 100644 --- a/src/azul/network_shared.py +++ b/src/azul/network_shared.py @@ -118,9 +118,15 @@ class ClientBoundEvents(IntEnum): game_over = auto() board_data = auto() factory_data = auto() + cursor_data = auto() + table_data = auto() + cursor_movement_mode = auto() class ServerBoundEvents(IntEnum): """Server bound event IDs.""" encryption_response = 0 + factory_clicked = auto() + cursor_location = auto() + pattern_row_clicked = auto() diff --git a/src/azul/server.py b/src/azul/server.py index 44adf47..6e1f2f7 100755 --- a/src/azul/server.py +++ b/src/azul/server.py @@ -29,6 +29,7 @@ import traceback from collections import deque +from enum import IntEnum, auto from functools import partial from typing import TYPE_CHECKING, NoReturn @@ -55,7 +56,7 @@ encode_int8_array, encode_numeric_uint8_counter, ) -from azul.state import Phase, State +from azul.state import Phase, State, Tile if TYPE_CHECKING: from collections import Counter @@ -96,12 +97,18 @@ def __init__(self, client_id: int) -> None: "server[write]->game_over": cbe.game_over, "server[write]->board_data": cbe.board_data, "server[write]->factory_data": cbe.factory_data, + "server[write]->cursor_data": cbe.cursor_data, + "server[write]->table_data": cbe.table_data, + "server[write]->cursor_movement_mode": cbe.cursor_movement_mode, }, ) sbe = ServerBoundEvents self.register_read_network_events( { sbe.encryption_response: f"client[{self.client_id}]->encryption_response", + sbe.factory_clicked: f"client[{self.client_id}]->factory_clicked", + sbe.cursor_location: f"client[{self.client_id}]->cursor_location", + sbe.pattern_row_clicked: f"client[{self.client_id}]->pattern_row_clicked", }, ) @@ -111,12 +118,18 @@ def bind_handlers(self) -> None: self.register_handlers( { f"client[{self.client_id}]->encryption_response": self.handle_encryption_response, + f"client[{self.client_id}]->factory_clicked": self.read_factory_clicked, + f"client[{self.client_id}]->cursor_location": self.read_cursor_location, + f"client[{self.client_id}]->pattern_row_clicked": self.read_pattern_row_clicked, f"callback_ping->network[{self.client_id}]": self.handle_callback_ping, - "initial_config->network": self.handle_initial_config, - f"playing_as->network[{self.client_id}]": self.handle_playing_as, - "game_over->network": self.handle_game_over, - "board_data->network": self.handle_board_data, - "factory_data->network": self.handle_factory_data, + "initial_config->network": self.write_factory_clicked, + f"playing_as->network[{self.client_id}]": self.write_playing_as, + "game_over->network": self.write_game_over, + "board_data->network": self.write_board_data, + "factory_data->network": self.write_factory_data, + "cursor_data->network": self.write_cursor_data, + "table_data->network": self.write_table_data, + f"cursor_movement_mode->network[{self.client_id}]": self.write_cursor_movement_mode, }, ) @@ -131,6 +144,47 @@ async def start_encryption_request(self) -> None: ) await self.handle_encryption_response(event) + async def read_factory_clicked(self, event: Event[bytearray]) -> None: + """Read factory_clicked event from client. Raise as `factory_clicked->server`.""" + buffer = Buffer(event.data) + + factory_id = buffer.read_value(StructFormat.UBYTE) + tile_color = Tile(buffer.read_value(StructFormat.UBYTE)) + + await self.raise_event( + Event( + "factory_clicked->server", + ( + self.client_id, + factory_id, + tile_color, + ), + ), + ) + + async def read_cursor_location(self, event: Event[bytearray]) -> None: + """Read factory_clicked event from client. Raise as `factory_clicked->server`.""" + print(f"read_cursor_location {event.data = }") + + async def read_pattern_row_clicked(self, event: Event[bytearray]) -> None: + """Read pattern_row_clicked event from client. Raise as `pattern_row_clicked->server`.""" + buffer = Buffer(event.data) + + row_id = buffer.read_value(StructFormat.UBYTE) + row_pos_x = buffer.read_value(StructFormat.UBYTE) + row_pos_y = buffer.read_value(StructFormat.UBYTE) + + await self.raise_event( + Event( + "pattern_row_clicked->server", + ( + self.client_id, + row_id, + (row_pos_x, row_pos_y), + ), + ), + ) + async def handle_callback_ping( self, _: Event[None], @@ -138,7 +192,7 @@ async def handle_callback_ping( """Reraise as server[write]->callback_ping.""" await self.write_callback_ping() - async def handle_initial_config( + async def write_factory_clicked( self, event: Event[tuple[bool, int, int, int]], ) -> None: @@ -154,7 +208,7 @@ async def handle_initial_config( await self.write_event(Event("server[write]->initial_config", buffer)) - async def handle_playing_as( + async def write_playing_as( self, event: Event[int], ) -> None: @@ -165,7 +219,7 @@ async def handle_playing_as( buffer.write_value(StructFormat.UBYTE, playing_as) await self.write_event(Event("server[write]->playing_as", buffer)) - async def handle_game_over(self, event: Event[int]) -> None: + async def write_game_over(self, event: Event[int]) -> None: """Read game over event and reraise as server[write]->game_over.""" winner = event.data @@ -175,7 +229,7 @@ async def handle_game_over(self, event: Event[int]) -> None: await self.write_event(Event("server[write]->game_over", buffer)) - async def handle_board_data( + async def write_board_data( self, event: Event[tuple[int, NDArray[int8]]], ) -> None: @@ -188,7 +242,7 @@ async def handle_board_data( await self.write_event(Event("server[write]->board_data", buffer)) - async def handle_factory_data( + async def write_factory_data( self, event: Event[tuple[int, Counter[int]]], ) -> None: @@ -201,6 +255,53 @@ async def handle_factory_data( await self.write_event(Event("server[write]->factory_data", buffer)) + async def write_cursor_data( + self, + event: Event[Counter[int]], + ) -> None: + """Reraise as server[write]->cursor_data.""" + tiles = event.data + + buffer = encode_numeric_uint8_counter(tiles) + + await self.write_event(Event("server[write]->cursor_data", buffer)) + + async def write_table_data( + self, + event: Event[Counter[int]], + ) -> None: + """Reraise as server[write]->table_data.""" + tiles = event.data + + buffer = encode_numeric_uint8_counter(tiles) + + await self.write_event(Event("server[write]->table_data", buffer)) + + async def write_cursor_movement_mode( + self, + event: Event[bool], + ) -> None: + """Reraise as server[write]->table_data.""" + client_mode = event.data + + buffer = Buffer() + buffer.write_value(StructFormat.BOOL, client_mode) + + await self.write_event( + Event("server[write]->cursor_movement_mode", buffer), + ) + + +class ServerPlayer(IntEnum): + """Server Player enum.""" + + one = 0 + two = auto() + three = auto() + four = auto() + singleplayer_all = auto() + spectator = auto() + class GameServer(network.Server): """Azul server. @@ -243,6 +344,8 @@ def bind_handlers(self) -> None: "server_start": self.start_server, "network_stop": self.stop_server, "server_send_game_start": self.handle_server_start_new_game, + "factory_clicked->server": self.handle_client_factory_clicked, + "pattern_row_clicked->server": self.handle_client_pattern_row_clicked, }, ) @@ -331,9 +434,9 @@ def setup_teams_internal(client_ids: list[int]) -> dict[int, int]: players: dict[int, int] = {} for idx, client_id in enumerate(client_ids): if idx == 0: - players[client_id] = 2 + players[client_id] = ServerPlayer.singleplayer_all else: - players[client_id] = 0xFF # Spectator + players[client_id] = ServerPlayer.spectator return players @staticmethod @@ -342,9 +445,9 @@ def setup_teams(client_ids: list[int]) -> dict[int, int]: players: dict[int, int] = {} for idx, client_id in enumerate(client_ids): if idx < 4: - players[client_id] = idx % 4 + players[client_id] = ServerPlayer(idx % 4) else: - players[client_id] = 0xFF # Spectator + players[client_id] = ServerPlayer.spectator return players def new_game_init(self, varient_play: bool = False) -> None: @@ -465,6 +568,25 @@ async def handle_server_start_new_game(self, event: Event[bool]) -> None: ), ), ) + # Transmit table center data + await self.raise_event( + Event( + "table_data->network", + self.state.table_center, + ), + ) + + rev_map = {v: k for k, v in self.client_players.items()} + if self.internal_singleplayer_mode: + client_id = rev_map[ServerPlayer.singleplayer_all] + else: + client_id = rev_map[ServerPlayer(self.state.current_turn)] + await self.raise_event( + Event( + f"cursor_movement_mode->network[{client_id}]", + True, + ), + ) await self.transmit_playing_as() @@ -615,6 +737,104 @@ async def handler(self, stream: trio.SocketStream) -> None: self.client_count -= 1 # ServerClient's `with` block handles closing stream. + async def handle_client_factory_clicked( + self, + event: Event[tuple[int, int, Tile]], + ) -> None: + """Handle client clicked a factory tile.""" + if not self.players_can_interact: + print("Players are not allowed to interact.") + await trio.lowlevel.checkpoint() + return + + client_id, factory_id, tile = event.data + + server_player_id = self.client_players[client_id] + + if server_player_id == ServerPlayer.spectator: + print(f"Spectator cannot select {factory_id = } {tile}") + await trio.lowlevel.checkpoint() + return + + player_id = int(server_player_id) + if server_player_id == ServerPlayer.singleplayer_all: + player_id = self.state.current_turn + + if player_id != self.state.current_turn: + print( + "Player {player_id} (client ID {client_id}) cannot select factory tile, not their turn.", + ) + await trio.lowlevel.checkpoint() + return + + if self.state.current_phase != Phase.factory_offer: + print( + "Player {player_id} (client ID {client_id}) cannot select factory tile, not in factory offer phase.", + ) + await trio.lowlevel.checkpoint() + return + + factory_display = self.state.factory_displays.get(factory_id) + if factory_display is None: + print( + "Player {player_id} (client ID {client_id}) cannot select invalid factory {factory_id!r}.", + ) + await trio.lowlevel.checkpoint() + return + + if tile < 0 or tile not in factory_display: + print( + "Player {player_id} (client ID {client_id}) cannot select nonexistent color {tile}.", + ) + await trio.lowlevel.checkpoint() + return + + if not self.state.can_cursor_select_factory_color( + factory_id, + int(tile), + ): + print( + "Player {player_id} (client ID {client_id}) cannot select factory tile, state says no.", + ) + await trio.lowlevel.checkpoint() + return + + # Perform move + self.state = self.state.cursor_selects_factory(factory_id, int(tile)) + + # Send updates to client + # Send factory display changes + await self.raise_event( + Event( + "factory_data->network", + ( + factory_id, + self.state.factory_displays[factory_id], + ), + ), + ) + await self.raise_event( + Event( + "cursor_data->network", + self.state.cursor_contents, + ), + ) + await self.raise_event( + Event( + "table_data->network", + self.state.table_center, + ), + ) + + async def handle_client_pattern_row_clicked( + self, + event: Event[tuple[int, int, tuple[int, int]]], + ) -> None: + """Handle client clicking on pattern row.""" + client_id, row_id, row_pos = event.data + print(f"handle_client_pattern_row_clicked {event.data = }") + await trio.lowlevel.checkpoint() + def __del__(self) -> None: """Debug print.""" print(f"del {self.__class__.__name__}") diff --git a/src/azul/state.py b/src/azul/state.py index 0041124..57bbe12 100644 --- a/src/azul/state.py +++ b/src/azul/state.py @@ -684,7 +684,7 @@ def new_game(cls, player_count: int, varient_play: bool = False) -> Self: current_phase=Phase.factory_offer, bag=bag, box_lid=Counter(), - table_center=Counter({Tile.one, 1}), + table_center=Counter({Tile.one: 1}), factory_displays=factory_displays, cursor_contents=Counter(), current_turn=0, diff --git a/src/azul/vector.py b/src/azul/vector.py index 5302e6d..57d50a0 100644 --- a/src/azul/vector.py +++ b/src/azul/vector.py @@ -158,6 +158,12 @@ def __round__( """Return result of rounding self components to given number of digits.""" return self.rounded(ndigits) + def floored( + self: Self, + ) -> Self: + """Return result of rounding self components to given number of digits.""" + return self.from_iter(int(c) for c in self) + def __abs__( self: Self, ) -> Self: From 1d8fe1768ca340037366f6bfd346d25df8f1f950 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sat, 30 Nov 2024 23:28:44 -0700 Subject: [PATCH 15/58] WIP pattern line handling --- src/azul/server.py | 45 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/src/azul/server.py b/src/azul/server.py index 6e1f2f7..b8554ea 100755 --- a/src/azul/server.py +++ b/src/azul/server.py @@ -831,8 +831,51 @@ async def handle_client_pattern_row_clicked( event: Event[tuple[int, int, tuple[int, int]]], ) -> None: """Handle client clicking on pattern row.""" + if not self.players_can_interact: + print("Players are not allowed to interact.") + await trio.lowlevel.checkpoint() + return + client_id, row_id, row_pos = event.data - print(f"handle_client_pattern_row_clicked {event.data = }") + + server_player_id = self.client_players[client_id] + + if server_player_id == ServerPlayer.spectator: + print(f"Spectator cannot select {row_id = } {row_pos}") + await trio.lowlevel.checkpoint() + return + + player_id = int(server_player_id) + if server_player_id == ServerPlayer.singleplayer_all: + player_id = self.state.current_turn + + if player_id != self.state.current_turn: + print( + "Player {player_id} (client ID {client_id}) cannot select pattern row, not their turn.", + ) + await trio.lowlevel.checkpoint() + return + + if self.state.current_phase != Phase.factory_offer: + print( + "Player {player_id} (client ID {client_id}) cannot select pattern row, not in factory offer phase.", + ) + await trio.lowlevel.checkpoint() + return + + if player_id != row_id: + print( + "Player {player_id} (client ID {client_id}) cannot select pattern row {row_id} that does not belong to them.", + ) + await trio.lowlevel.checkpoint() + return + + column, line_id = row_pos + place_count = 5 - column + + print( + f"handle_client_pattern_row_clicked {line_id = } {place_count = }", + ) await trio.lowlevel.checkpoint() def __del__(self) -> None: From 06ce6bac91ed47215f0bdaa302bda6b5abfd48be Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Tue, 3 Dec 2024 01:18:29 -0600 Subject: [PATCH 16/58] Update requirements --- .pre-commit-config.yaml | 2 +- test-requirements.txt | 21 +++++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f030e52..66ef42b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.0 + rev: v0.8.1 hooks: - id: ruff types: [file] diff --git a/test-requirements.txt b/test-requirements.txt index 74f1709..adb56ab 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -18,12 +18,12 @@ colorama==0.4.6 ; (implementation_name != 'cpython' and sys_platform == 'win32') # via # click # pytest -coverage==7.6.4 +coverage==7.6.8 # via # -r test-requirements.in # pytest-cov -cryptography==43.0.3 - # via -r test-requirements.in +cryptography==44.0.0 + # via libcomponent exceptiongroup==1.2.2 ; python_full_version < '3.11' # via # -r test-requirements.in @@ -33,16 +33,19 @@ idna==3.10 # via trio iniconfig==2.0.0 # via pytest +libcomponent @ git+https://github.com/CoolCat467/LibComponent@b06eaca2f029a9fc019d68194ade260ccb00a841 + # via -r test-requirements.in mypy==1.13.0 # via -r test-requirements.in mypy-extensions==1.0.0 # via # -r test-requirements.in # black + # libcomponent # mypy numpy==2.1.3 # via -r test-requirements.in -orjson==3.10.11 ; implementation_name == 'cpython' +orjson==3.10.12 ; implementation_name == 'cpython' # via -r test-requirements.in outcome==1.3.0.post0 # via @@ -62,7 +65,7 @@ pycparser==2.22 ; (os_name != 'nt' and platform_python_implementation != 'PyPy') # via cffi pygame==2.6.1 # via -r test-requirements.in -pytest==8.3.3 +pytest==8.3.4 # via # -r test-requirements.in # pytest-cov @@ -71,13 +74,13 @@ pytest-cov==6.0.0 # via -r test-requirements.in pytest-trio==0.8.0 # via -r test-requirements.in -ruff==0.7.3 +ruff==0.8.1 # via -r test-requirements.in sniffio==1.3.1 # via trio sortedcontainers==2.4.0 # via trio -tomli==2.0.2 ; python_full_version <= '3.11' +tomli==2.2.1 ; python_full_version <= '3.11' # via # black # coverage @@ -86,11 +89,13 @@ tomli==2.0.2 ; python_full_version <= '3.11' trio==0.27.0 # via # -r test-requirements.in + # libcomponent # pytest-trio typing-extensions==4.12.2 # via # -r test-requirements.in # black + # libcomponent # mypy -uv==0.5.1 +uv==0.5.5 # via -r test-requirements.in From 03b51eb0146d9f98a039a69fb6d25411f5a6922a Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Tue, 3 Dec 2024 01:20:30 -0600 Subject: [PATCH 17/58] Fix encoding and implement decoding for cursor location --- src/azul/client.py | 7 +++---- src/azul/game.py | 2 +- src/azul/server.py | 25 ++++++++++++++++++++++++- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/azul/client.py b/src/azul/client.py index 2d9be7c..7421cdd 100644 --- a/src/azul/client.py +++ b/src/azul/client.py @@ -432,10 +432,9 @@ async def write_game_cursor_location_transmit( """Write cursor_location_transmit event to server.""" scaled_location = event.data - x, y = map(int, (scaled_location * 4096).rounded()) # 2 ** 12 - - position = (x & 0xFFF) << 3 | (y & 0xFFF) - buffer = position.to_bytes(3, "big") + x, y = map(int, (scaled_location * 0xFFF).floored()) + position = ((x & 0xFFF) << 12) | (y & 0xFFF) + buffer = (position & 0xFFFFFF).to_bytes(3) await self.raise_event(Event("cursor_location->server[write]", buffer)) diff --git a/src/azul/game.py b/src/azul/game.py index b784c8c..23b8018 100644 --- a/src/azul/game.py +++ b/src/azul/game.py @@ -567,7 +567,7 @@ async def handle_pygame_mouse_motion( self.location = event.data["pos"] ##transmit_location = Vector2.from_iter( - ## x / y for x, y in zip(self.location, SCREEN_SIZE, strict=False) + ## x / y for x, y in zip(self.location, SCREEN_SIZE, strict=True) ##) ## ### Transmit to server diff --git a/src/azul/server.py b/src/azul/server.py index b8554ea..3096bda 100755 --- a/src/azul/server.py +++ b/src/azul/server.py @@ -164,7 +164,19 @@ async def read_factory_clicked(self, event: Event[bytearray]) -> None: async def read_cursor_location(self, event: Event[bytearray]) -> None: """Read factory_clicked event from client. Raise as `factory_clicked->server`.""" - print(f"read_cursor_location {event.data = }") + buffer = int.from_bytes(event.data) & 0xFFFFFF + x = (buffer >> 12) & 0xFFF + y = buffer & 0xFFF + + await self.raise_event( + Event( + "cursor_location->server", + ( + self.client_id, + (x, y), + ), + ), + ) async def read_pattern_row_clicked(self, event: Event[bytearray]) -> None: """Read pattern_row_clicked event from client. Raise as `pattern_row_clicked->server`.""" @@ -346,6 +358,7 @@ def bind_handlers(self) -> None: "server_send_game_start": self.handle_server_start_new_game, "factory_clicked->server": self.handle_client_factory_clicked, "pattern_row_clicked->server": self.handle_client_pattern_row_clicked, + "cursor_location->server": self.handle_cursor_location, }, ) @@ -878,6 +891,16 @@ async def handle_client_pattern_row_clicked( ) await trio.lowlevel.checkpoint() + async def handle_cursor_location( + self, + event: Event[tuple[int, tuple[int, int]]], + ) -> None: + """Handle cursor location sent from client.""" + client_id, pos = event.data + + print(f"handle_cursor_location {client_id = } {pos = }") + await trio.lowlevel.checkpoint() + def __del__(self) -> None: """Debug print.""" print(f"del {self.__class__.__name__}") From 077f67d140eb7339f0f12d6118acb8b133701eac Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Fri, 13 Dec 2024 10:59:54 -0600 Subject: [PATCH 18/58] Work on starting to send cursor location --- src/azul/game.py | 7 +++++++ src/azul/server.py | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/azul/game.py b/src/azul/game.py index 23b8018..7749978 100644 --- a/src/azul/game.py +++ b/src/azul/game.py @@ -39,6 +39,7 @@ import pygame import trio from libcomponent.component import ( + Component, ComponentManager, Event, ExternalRaiseManager, @@ -464,6 +465,12 @@ def get_tile_point( return tile_position.floored() +class EventClock(Component): + """Event Clock Component.""" + + __slots__ = () + + class Cursor(TileRenderer): """Cursor TileRenderer. diff --git a/src/azul/server.py b/src/azul/server.py index 3096bda..891927a 100755 --- a/src/azul/server.py +++ b/src/azul/server.py @@ -896,8 +896,31 @@ async def handle_cursor_location( event: Event[tuple[int, tuple[int, int]]], ) -> None: """Handle cursor location sent from client.""" + if not self.players_can_interact: + print("Players are not allowed to interact.") + await trio.lowlevel.checkpoint() + return + client_id, pos = event.data + server_player_id = self.client_players[client_id] + + if server_player_id == ServerPlayer.spectator: + print(f"Spectator cannot select {pos = }") + await trio.lowlevel.checkpoint() + return + + player_id = int(server_player_id) + if server_player_id == ServerPlayer.singleplayer_all: + player_id = self.state.current_turn + + if player_id != self.state.current_turn: + print( + "Player {player_id} (client ID {client_id}) cannot move cursor, not their turn.", + ) + await trio.lowlevel.checkpoint() + return + print(f"handle_cursor_location {client_id = } {pos = }") await trio.lowlevel.checkpoint() From c7ea408f9b4df73912dac6eb16d220ca277b9ac8 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Fri, 13 Dec 2024 11:00:24 -0600 Subject: [PATCH 19/58] Add `zizmor` pre-commit hook --- .github/workflows/autodeps.yml | 2 ++ .github/workflows/ci.yml | 2 ++ .pre-commit-config.yaml | 6 +++++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/autodeps.yml b/.github/workflows/autodeps.yml index 757f6c8..5918fd9 100644 --- a/.github/workflows/autodeps.yml +++ b/.github/workflows/autodeps.yml @@ -20,6 +20,8 @@ jobs: steps: - name: Checkout + with: + persist-credentials: false uses: actions/checkout@v4 - name: Setup python uses: actions/setup-python@v5 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc9b89d..b6bbbe7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,6 +78,8 @@ jobs: }} steps: - name: Checkout + with: + persist-credentials: false uses: actions/checkout@v4 - name: Setup python uses: actions/setup-python@v5 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 66ef42b..e9fafbc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.1 + rev: v0.8.3 hooks: - id: ruff types: [file] @@ -36,6 +36,10 @@ repos: rev: v2.3.0 hooks: - id: codespell + - repo: https://github.com/woodruffw/zizmor-pre-commit + rev: v0.9.1 + hooks: + - id: zizmor - repo: local hooks: - id: project-requirements From d5a0839ba6f6ac3161a097b015c4143ea96c5016 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Mon, 16 Dec 2024 22:53:49 -0600 Subject: [PATCH 20/58] Transmit pattern data and current turn changes --- .pre-commit-config.yaml | 2 +- src/azul/client.py | 26 ++++++++ src/azul/game.py | 36 +++++++++- src/azul/network_shared.py | 2 + src/azul/server.py | 132 +++++++++++++++++++++++++++++++++++-- src/azul/state.py | 2 +- test-requirements.txt | 8 +-- 7 files changed, 196 insertions(+), 12 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e9fafbc..ff6904d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: codespell - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v0.9.1 + rev: v0.9.2 hooks: - id: zizmor - repo: local diff --git a/src/azul/client.py b/src/azul/client.py index 7421cdd..414beeb 100644 --- a/src/azul/client.py +++ b/src/azul/client.py @@ -42,6 +42,7 @@ ServerBoundEvents, decode_int8_array, decode_numeric_uint8_counter, + decode_tile_count, ) if TYPE_CHECKING: @@ -182,10 +183,12 @@ def __init__(self, name: str) -> None: cbe.playing_as: "server->playing_as", cbe.game_over: "server->game_over", cbe.board_data: "server->board_data", + cbe.pattern_data: "server->pattern_data", cbe.factory_data: "server->factory_data", cbe.cursor_data: "server->cursor_data", cbe.table_data: "server->table_data", cbe.cursor_movement_mode: "server->cursor_movement_mode", + cbe.current_turn_change: "server->current_turn_change", }, ) @@ -203,10 +206,12 @@ def bind_handlers(self) -> None: "server->playing_as": self.read_playing_as, "server->game_over": self.read_game_over, "server->board_data": self.read_board_data, + "server->pattern_data": self.read_pattern_data, "server->factory_data": self.read_factory_data, "server->cursor_data": self.read_cursor_data, "server->table_data": self.read_table_data, "server->cursor_movement_mode": self.read_cursor_movement_mode, + "server->current_turn_change": self.read_current_turn_change, "client_connect": self.handle_client_connect, "network_stop": self.handle_network_stop, "game_factory_clicked": self.write_game_factory_clicked, @@ -377,6 +382,18 @@ async def read_board_data(self, event: Event[bytearray]) -> None: await self.raise_event(Event("game_board_data", (player_id, array))) + async def read_pattern_data(self, event: Event[bytearray]) -> None: + """Read pattern_data event from server, reraise as `game_pattern_data`.""" + buffer = Buffer(event.data) + + player_id: u8 = buffer.read_value(StructFormat.UBYTE) + row_id: u8 = buffer.read_value(StructFormat.UBYTE) + tile_data = decode_tile_count(buffer) + + await self.raise_event( + Event("game_pattern_data", (player_id, row_id, tile_data)), + ) + async def read_factory_data(self, event: Event[bytearray]) -> None: """Read factory_data event from server, reraise as `game_factory_data`.""" buffer = Buffer(event.data) @@ -412,6 +429,15 @@ async def read_cursor_movement_mode(self, event: Event[bytearray]) -> None: Event("game_cursor_set_movement_mode", client_mode), ) + async def read_current_turn_change(self, event: Event[bytearray]) -> None: + """Read current_turn_change event from server, reraise as `game_pattern_current_turn_change`.""" + buffer = Buffer(event.data) + + pattern_id: u8 = buffer.read_value(StructFormat.UBYTE) + await self.raise_event( + Event("game_pattern_current_turn_change", pattern_id), + ) + async def write_game_factory_clicked( self, event: Event[tuple[int, Tile]], diff --git a/src/azul/game.py b/src/azul/game.py index 7749978..52ac281 100644 --- a/src/azul/game.py +++ b/src/azul/game.py @@ -840,8 +840,14 @@ def __repr__(self) -> str: return f"{self.__class__.__name__}({self.rows_id})" def bind_handlers(self) -> None: - """Register click event handler.""" - self.register_handler("click", self.handle_click) + """Register event handlers.""" + self.register_handlers( + { + "click": self.handle_click, + "game_pattern_current_turn_change": self.handle_game_pattern_current_turn_change, + "game_pattern_data": self.handle_game_pattern_data, + }, + ) def update_image(self) -> None: """Update self.image.""" @@ -906,6 +912,32 @@ async def handle_click( ), ) + async def handle_game_pattern_current_turn_change( + self, + event: Event[int], + ) -> None: + """Handle game_pattern_current_turn_change event.""" + player_id = event.data + + if player_id == self.rows_id: + self.set_background(DARKGREEN) + else: + self.set_background(None) + + async def handle_game_pattern_data( + self, + event: Event[tuple[int, int, tuple[int, int]]], + ) -> None: + """Handle game_pattern_data event.""" + player_id, row_id, (raw_tile_color, tile_count) = event.data + + if player_id != self.rows_id: + await trio.lowlevel.checkpoint() + return + tile_color = Tile(raw_tile_color) + self.set_row_data(row_id, tile_color, tile_count) + await trio.lowlevel.checkpoint() + class FloorLine(Row): """Represents a player's floor line.""" diff --git a/src/azul/network_shared.py b/src/azul/network_shared.py index 84168ef..149d732 100644 --- a/src/azul/network_shared.py +++ b/src/azul/network_shared.py @@ -117,10 +117,12 @@ class ClientBoundEvents(IntEnum): playing_as = auto() game_over = auto() board_data = auto() + pattern_data = auto() factory_data = auto() cursor_data = auto() table_data = auto() cursor_movement_mode = auto() + current_turn_change = auto() class ServerBoundEvents(IntEnum): diff --git a/src/azul/server.py b/src/azul/server.py index 891927a..66e526d 100755 --- a/src/azul/server.py +++ b/src/azul/server.py @@ -55,6 +55,7 @@ ServerBoundEvents, encode_int8_array, encode_numeric_uint8_counter, + encode_tile_count, ) from azul.state import Phase, State, Tile @@ -96,10 +97,12 @@ def __init__(self, client_id: int) -> None: "server[write]->playing_as": cbe.playing_as, "server[write]->game_over": cbe.game_over, "server[write]->board_data": cbe.board_data, + "server[write]->pattern_data": cbe.pattern_data, "server[write]->factory_data": cbe.factory_data, "server[write]->cursor_data": cbe.cursor_data, "server[write]->table_data": cbe.table_data, "server[write]->cursor_movement_mode": cbe.cursor_movement_mode, + "server[write]->current_turn_change": cbe.current_turn_change, }, ) sbe = ServerBoundEvents @@ -130,6 +133,8 @@ def bind_handlers(self) -> None: "cursor_data->network": self.write_cursor_data, "table_data->network": self.write_table_data, f"cursor_movement_mode->network[{self.client_id}]": self.write_cursor_movement_mode, + "current_turn_change->network": self.write_current_turn_change, + "pattern_data->network": self.write_pattern_data, }, ) @@ -293,7 +298,7 @@ async def write_cursor_movement_mode( self, event: Event[bool], ) -> None: - """Reraise as server[write]->table_data.""" + """Reraise as server[write]->cursor_movement_mode.""" client_mode = event.data buffer = Buffer() @@ -303,6 +308,37 @@ async def write_cursor_movement_mode( Event("server[write]->cursor_movement_mode", buffer), ) + async def write_current_turn_change( + self, + event: Event[int], + ) -> None: + """Reraise as server[write]->current_turn_change.""" + pattern_id = event.data + + buffer = Buffer() + buffer.write_value(StructFormat.UBYTE, pattern_id) + + await self.write_event( + Event("server[write]->current_turn_change", buffer), + ) + + async def write_pattern_data( + self, + event: Event[tuple[int, int, tuple[int, int]]], + ) -> None: + """Reraise as server[write]->board_data.""" + player_id, row_id, (tile_color, tile_count) = event.data + + buffer = Buffer() + buffer.write_value(StructFormat.UBYTE, player_id) + buffer.write_value(StructFormat.UBYTE, row_id) + assert tile_color >= 0 + buffer.extend(encode_tile_count(tile_color, tile_count)) + + await self.write_event( + Event("server[write]->pattern_data", buffer), + ) + class ServerPlayer(IntEnum): """Server Player enum.""" @@ -750,6 +786,35 @@ async def handler(self, stream: trio.SocketStream) -> None: self.client_count -= 1 # ServerClient's `with` block handles closing stream. + def find_client_id_from_server_player_id( + self, + server_player_id: ServerPlayer, + ) -> int | None: + """Return client id from server player id or None if not found.""" + for client_id, current_server_player_id in self.client_players.items(): + if current_server_player_id == server_player_id: + return client_id + # Return singleplayer client id if exists + if current_server_player_id == ServerPlayer.singleplayer_all: + return client_id + return None + + def find_server_player_id_from_state_turn( + self, + state_turn: int, + ) -> ServerPlayer: + """Return ServerPlayer id from game state turn.""" + if self.internal_singleplayer_mode: + return ServerPlayer.singleplayer_all + return ServerPlayer(state_turn) + + def find_client_id_from_state_turn(self, state_turn: int) -> int | None: + """Return client id from state turn or None if not found.""" + server_player_id = self.find_server_player_id_from_state_turn( + state_turn, + ) + return self.find_client_id_from_server_player_id(server_player_id) + async def handle_client_factory_clicked( self, event: Event[tuple[int, int, Tile]], @@ -886,10 +951,69 @@ async def handle_client_pattern_row_clicked( column, line_id = row_pos place_count = 5 - column - print( - f"handle_client_pattern_row_clicked {line_id = } {place_count = }", + color = self.state.get_cursor_holding_color() + if not self.state.can_player_select_line(line_id, color, place_count): + print( + f"Player {player_id} (client ID {client_id}) cannot select pattern line {line_id} placing {place_count} {Tile(color)} tiles.", + ) + await trio.lowlevel.checkpoint() + return + + prev_player_turn = self.state.current_turn + + self.state = self.state.player_selects_pattern_line( + line_id, + place_count, + ) + + if self.state.current_turn != player_id: + if server_player_id != ServerPlayer.singleplayer_all: + new_client_id = self.find_client_id_from_state_turn( + self.state.current_turn, + ) + assert new_client_id is not None + await self.raise_event( + Event( + f"cursor_movement_mode->network[{client_id}]", + False, + ), + ) + await self.raise_event( + Event( + f"cursor_movement_mode->network[{new_client_id}]", + True, + ), + ) + + await self.raise_event( + Event( + "current_turn_change->network", + self.state.current_turn, + ), + ) + + raw_tile_color, tile_count = self.state.player_data[ + prev_player_turn + ].lines[line_id] + # Do not send blank colors, clamp to zero + tile_color = max(0, int(raw_tile_color)) + await self.raise_event( + Event( + "pattern_data->network", + ( + prev_player_turn, + line_id, + (tile_color, tile_count), + ), + ), + ) + + await self.raise_event( + Event( + "cursor_data->network", + self.state.cursor_contents, + ), ) - await trio.lowlevel.checkpoint() async def handle_cursor_location( self, diff --git a/src/azul/state.py b/src/azul/state.py index 57bbe12..6018c0f 100644 --- a/src/azul/state.py +++ b/src/azul/state.py @@ -1165,8 +1165,8 @@ def preform_action( raise NotImplementedError() def _manual_wall_tiling_maybe_next_turn(self) -> Self: + # return self raise NotImplementedError() - return self def get_manual_wall_tiling_locations_for_player( self, diff --git a/test-requirements.txt b/test-requirements.txt index adb56ab..904fa0f 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,6 +1,6 @@ # This file was autogenerated by uv via the following command: # uv pip compile --universal --python-version=3.10 test-requirements.in -o test-requirements.txt -attrs==24.2.0 +attrs==24.3.0 # via # outcome # trio @@ -18,7 +18,7 @@ colorama==0.4.6 ; (implementation_name != 'cpython' and sys_platform == 'win32') # via # click # pytest -coverage==7.6.8 +coverage==7.6.9 # via # -r test-requirements.in # pytest-cov @@ -74,7 +74,7 @@ pytest-cov==6.0.0 # via -r test-requirements.in pytest-trio==0.8.0 # via -r test-requirements.in -ruff==0.8.1 +ruff==0.8.3 # via -r test-requirements.in sniffio==1.3.1 # via trio @@ -97,5 +97,5 @@ typing-extensions==4.12.2 # black # libcomponent # mypy -uv==0.5.5 +uv==0.5.9 # via -r test-requirements.in From 46e05f1bcc0dbe0bfb268685c013551135139885 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Tue, 17 Dec 2024 03:26:18 -0600 Subject: [PATCH 21/58] If user clicks row, probably intended to place tiles. --- src/azul/server.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/azul/server.py b/src/azul/server.py index 66e526d..2bbe106 100755 --- a/src/azul/server.py +++ b/src/azul/server.py @@ -952,6 +952,11 @@ async def handle_client_pattern_row_clicked( place_count = 5 - column color = self.state.get_cursor_holding_color() + + max_place = self.state.get_player_line_max_placable_count(line_id) + current_hold_count = self.state.cursor_contents[color] + place_count = min(place_count, current_hold_count, max_place) + if not self.state.can_player_select_line(line_id, color, place_count): print( f"Player {player_id} (client ID {client_id}) cannot select pattern line {line_id} placing {place_count} {Tile(color)} tiles.", From ea9d971887fe485f35a57a765d3d1ff26d7bf4f8 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Tue, 17 Dec 2024 22:31:54 -0600 Subject: [PATCH 22/58] Transmit cursor location --- src/azul/client.py | 18 +++++- src/azul/game.py | 124 ++++++++++++++++++++++++++++++------- src/azul/network_shared.py | 17 +++++ src/azul/server.py | 92 ++++++++++++++++++++------- src/azul/sprite.py | 16 ++--- test-requirements.txt | 10 +-- 6 files changed, 214 insertions(+), 63 deletions(-) diff --git a/src/azul/client.py b/src/azul/client.py index 414beeb..e2fe064 100644 --- a/src/azul/client.py +++ b/src/azul/client.py @@ -40,16 +40,18 @@ ADVERTISEMENT_PORT, ClientBoundEvents, ServerBoundEvents, + decode_cursor_location, decode_int8_array, decode_numeric_uint8_counter, decode_tile_count, + encode_cursor_location, ) +from azul.vector import Vector2 if TYPE_CHECKING: from mypy_extensions import u8 from azul.state import Tile - from azul.vector import Vector2 async def read_advertisements( @@ -189,6 +191,7 @@ def __init__(self, name: str) -> None: cbe.table_data: "server->table_data", cbe.cursor_movement_mode: "server->cursor_movement_mode", cbe.current_turn_change: "server->current_turn_change", + cbe.cursor_position: "server->cursor_position", }, ) @@ -212,6 +215,7 @@ def bind_handlers(self) -> None: "server->table_data": self.read_table_data, "server->cursor_movement_mode": self.read_cursor_movement_mode, "server->current_turn_change": self.read_current_turn_change, + "server->cursor_position": self.read_cursor_position, "client_connect": self.handle_client_connect, "network_stop": self.handle_network_stop, "game_factory_clicked": self.write_game_factory_clicked, @@ -438,6 +442,15 @@ async def read_current_turn_change(self, event: Event[bytearray]) -> None: Event("game_pattern_current_turn_change", pattern_id), ) + async def read_cursor_position(self, event: Event[bytearray]) -> None: + """Read current_turn_change event from server, reraise as `game_cursor_set_destination`.""" + location = decode_cursor_location(event.data) + unit_location = Vector2.from_iter(x / 0xFFF for x in location) + + await self.raise_event( + Event("game_cursor_set_destination", unit_location), + ) + async def write_game_factory_clicked( self, event: Event[tuple[int, Tile]], @@ -459,8 +472,7 @@ async def write_game_cursor_location_transmit( scaled_location = event.data x, y = map(int, (scaled_location * 0xFFF).floored()) - position = ((x & 0xFFF) << 12) | (y & 0xFFF) - buffer = (position & 0xFFFFFF).to_bytes(3) + buffer = encode_cursor_location((x, y)) await self.raise_event(Event("cursor_location->server[write]", buffer)) diff --git a/src/azul/game.py b/src/azul/game.py index 52ac281..25ae555 100644 --- a/src/azul/game.py +++ b/src/azul/game.py @@ -466,9 +466,47 @@ def get_tile_point( class EventClock(Component): - """Event Clock Component.""" + """Event Clock Component. - __slots__ = () + Will raise `self.event_to_raise` every `self.duration` seconds. + If more than duration seconds pass before ticks, will only raise + one event. + + Do not pass leveled events, event reference is maintained and + when first event is mutated with pop_level it will be same object + and only first run will be leveled event. + """ + + __slots__ = ("duration", "event_to_raise", "time_passed") + + def __init__( + self, + name: str, + duration: float, + event_to_raise: Event[Any], + ) -> None: + """Initialize with name, duration, and event to raise.""" + super().__init__(name) + + self.time_passed: float = 0.0 + self.duration = duration + self.event_to_raise = event_to_raise + + def bind_handlers(self) -> None: + """Register tick event handler.""" + self.register_handler("tick", self.handle_tick) + + async def handle_tick(self, event: Event[sprite.TickEventData]) -> None: + """Handle tick event.""" + self.time_passed += event.data.time_passed + truediv, self.time_passed = divmod(self.time_passed, self.duration) + + # Could raise multiple times, but I am deciding that we will + # only raise at most once even if we miss the train + if truediv: + # Known issue: Event to raise cannot be a leveled event, + # because event.pop_level mutates the event object in place + await self.raise_event(self.event_to_raise) class Cursor(TileRenderer): @@ -484,8 +522,14 @@ class Cursor(TileRenderer): - PygameMouseMotion """ - __slots__ = ("tiles",) + __slots__ = ( + "client_mode", + "last_transmit_pos", + "tiles", + "time_passed", + ) greyshift = GREYSHIFT + duration = 0.25 def __init__(self) -> None: """Initialize cursor with a game it belongs to.""" @@ -494,13 +538,16 @@ def __init__(self) -> None: self.add_components( ( - sprite.MovementComponent(speed=800), + sprite.MovementComponent(speed=600), sprite.TargetingComponent("cursor_reached_destination"), ), ) # Stored in reverse render order self.tiles: list[int] = [] + self.last_transmit_pos = self.location + self.time_passed = 0.0 + self.client_mode = False def update_image(self) -> None: """Update self.image.""" @@ -555,13 +602,19 @@ async def handle_cursor_set_destination( event: Event[tuple[int, int]], ) -> None: """Start moving towards new destination.""" + destination = Vector2.from_iter( + x * y for x, y in zip(event.data, SCREEN_SIZE, strict=True) + ).floored() + # print(f"handle_cursor_set_destination {destination = }") + targeting: sprite.TargetingComponent = self.get_component("targeting") - targeting.destination = event.data + targeting.destination = destination if not self.has_handler("tick"): self.register_handler( "tick", - targeting.move_destination_time_ticks, + self.handle_tick, ) + self.move_to_front() await trio.lowlevel.checkpoint() @@ -572,34 +625,57 @@ async def handle_pygame_mouse_motion( """Set location to event data.""" self.move_to_front() self.location = event.data["pos"] + await trio.lowlevel.checkpoint() - ##transmit_location = Vector2.from_iter( - ## x / y for x, y in zip(self.location, SCREEN_SIZE, strict=True) - ##) - ## - ### Transmit to server - ### Event level to so reaches client - ##await self.raise_event( - ## Event( - ## "game_cursor_location_transmit", - ## transmit_location, - ## 2, - ## ) - ##) + async def handle_tick(self, event: Event[sprite.TickEventData]) -> None: + """Handle tick event.""" + if self.client_mode: + self.time_passed += event.data.time_passed + truediv, self.time_passed = divmod(self.time_passed, self.duration) + + if self.last_transmit_pos != self.location and truediv: + self.last_transmit_pos = self.location + else: + await trio.lowlevel.checkpoint() + return + + transmit_location = Vector2.from_iter( + x / y for x, y in zip(self.location, SCREEN_SIZE, strict=True) + ) + + # Transmit to server + # Event level to so reaches client + await self.raise_event( + Event( + "game_cursor_location_transmit", + transmit_location, + 2, + ), + ) + else: + # Server mode + targeting: sprite.TargetingComponent = self.get_component( + "targeting", + ) + await targeting.move_destination_time(event.data.time_passed) async def handle_cursor_set_movement_mode( self, event: Event[bool], ) -> None: """Change cursor movement mode. True if client mode, False if server mode.""" - client_mode = event.data - if client_mode: - self.register_handler( - "PygameMouseMotion", - self.handle_pygame_mouse_motion, + self.client_mode = event.data + # print(f'handle_cursor_set_movement_mode {self.client_mode = }') + if self.client_mode: + self.register_handlers( + { + "PygameMouseMotion": self.handle_pygame_mouse_motion, + "tick": self.handle_tick, + }, ) else: self.unregister_handler_type("PygameMouseMotion") + self.unregister_handler_type("tick") await trio.lowlevel.checkpoint() def get_held_count(self) -> int: diff --git a/src/azul/network_shared.py b/src/azul/network_shared.py index 149d732..79333b5 100644 --- a/src/azul/network_shared.py +++ b/src/azul/network_shared.py @@ -36,6 +36,7 @@ if TYPE_CHECKING: from numpy.typing import NDArray + ADVERTISEMENT_IP: Final = "224.0.2.60" ADVERTISEMENT_PORT: Final = 4445 @@ -108,6 +109,21 @@ def decode_int8_array(buffer: Buffer, size: tuple[int, ...]) -> NDArray[int8]: return array +def encode_cursor_location(scaled_location: tuple[int, int]) -> bytes: + """Return buffer from cursor location.""" + x, y = scaled_location + position = ((x & 0xFFF) << 12) | (y & 0xFFF) + return (position & 0xFFFFFF).to_bytes(3) + + +def decode_cursor_location(buffer: bytes | bytearray) -> tuple[int, int]: + """Return cursor location from buffer.""" + value = int.from_bytes(buffer) & 0xFFFFFF + x = (value >> 12) & 0xFFF + y = value & 0xFFF + return (x, y) + + class ClientBoundEvents(IntEnum): """Client bound event IDs.""" @@ -123,6 +139,7 @@ class ClientBoundEvents(IntEnum): table_data = auto() cursor_movement_mode = auto() current_turn_change = auto() + cursor_position = auto() class ServerBoundEvents(IntEnum): diff --git a/src/azul/server.py b/src/azul/server.py index 2bbe106..e5f6b51 100755 --- a/src/azul/server.py +++ b/src/azul/server.py @@ -53,6 +53,8 @@ DEFAULT_PORT, ClientBoundEvents, ServerBoundEvents, + decode_cursor_location, + encode_cursor_location, encode_int8_array, encode_numeric_uint8_counter, encode_tile_count, @@ -103,6 +105,7 @@ def __init__(self, client_id: int) -> None: "server[write]->table_data": cbe.table_data, "server[write]->cursor_movement_mode": cbe.cursor_movement_mode, "server[write]->current_turn_change": cbe.current_turn_change, + "server[write]->cursor_position": cbe.cursor_position, }, ) sbe = ServerBoundEvents @@ -133,6 +136,7 @@ def bind_handlers(self) -> None: "cursor_data->network": self.write_cursor_data, "table_data->network": self.write_table_data, f"cursor_movement_mode->network[{self.client_id}]": self.write_cursor_movement_mode, + f"cursor_position->network[{self.client_id}]": self.write_cursor_position, "current_turn_change->network": self.write_current_turn_change, "pattern_data->network": self.write_pattern_data, }, @@ -169,9 +173,7 @@ async def read_factory_clicked(self, event: Event[bytearray]) -> None: async def read_cursor_location(self, event: Event[bytearray]) -> None: """Read factory_clicked event from client. Raise as `factory_clicked->server`.""" - buffer = int.from_bytes(event.data) & 0xFFFFFF - x = (buffer >> 12) & 0xFFF - y = buffer & 0xFFF + x, y = decode_cursor_location(event.data) await self.raise_event( Event( @@ -308,6 +310,17 @@ async def write_cursor_movement_mode( Event("server[write]->cursor_movement_mode", buffer), ) + async def write_cursor_position( + self, + event: Event[tuple[int, int]], + ) -> None: + """Reraise as server[write]->cursor_position.""" + buffer = encode_cursor_location(event.data) + + await self.write_event( + Event("server[write]->cursor_position", buffer), + ) + async def write_current_turn_change( self, event: Event[int], @@ -625,11 +638,16 @@ async def handle_server_start_new_game(self, event: Event[bool]) -> None: ), ) - rev_map = {v: k for k, v in self.client_players.items()} - if self.internal_singleplayer_mode: - client_id = rev_map[ServerPlayer.singleplayer_all] - else: - client_id = rev_map[ServerPlayer(self.state.current_turn)] + await self.transmit_cursor_movement_mode() + + await self.transmit_playing_as() + + async def transmit_cursor_movement_mode(self) -> None: + """Update current cursor movement mode for all clients.""" + client_id = self.find_client_id_from_state_turn( + self.state.current_turn, + ) + await self.raise_event( Event( f"cursor_movement_mode->network[{client_id}]", @@ -637,7 +655,16 @@ async def handle_server_start_new_game(self, event: Event[bool]) -> None: ), ) - await self.transmit_playing_as() + async with trio.open_nursery() as nursery: + for other_client_id in self.client_players: + if other_client_id != client_id: + nursery.start_soon( + self.raise_event, + Event( + f"cursor_movement_mode->network[{other_client_id}]", + False, + ), + ) async def client_network_loop( self, @@ -748,7 +775,9 @@ async def handler(self, stream: trio.SocketStream) -> None: self.client_count += 1 can_start = self.can_start() + print(f"[azul.server] {can_start = }") game_active = self.game_active() + print(f"[azul.server] {game_active = }") # if can_start: # self.stop_serving() @@ -772,7 +801,8 @@ async def handler(self, stream: trio.SocketStream) -> None: print("TODO: Joined as spectator") # await self.send_spectator_join_packets(client) with self.temporary_component(client): - if can_start and not game_active and is_zee_capitan: + if can_start and not game_active: # and is_zee_capitan: + print("[azul.server] game start trigger.") varient_play = False await self.raise_event( Event("server_send_game_start", varient_play), @@ -840,14 +870,14 @@ async def handle_client_factory_clicked( if player_id != self.state.current_turn: print( - "Player {player_id} (client ID {client_id}) cannot select factory tile, not their turn.", + f"Player {player_id} (client ID {client_id}) cannot select factory tile, not their turn.", ) await trio.lowlevel.checkpoint() return if self.state.current_phase != Phase.factory_offer: print( - "Player {player_id} (client ID {client_id}) cannot select factory tile, not in factory offer phase.", + f"Player {player_id} (client ID {client_id}) cannot select factory tile, not in factory offer phase.", ) await trio.lowlevel.checkpoint() return @@ -855,14 +885,14 @@ async def handle_client_factory_clicked( factory_display = self.state.factory_displays.get(factory_id) if factory_display is None: print( - "Player {player_id} (client ID {client_id}) cannot select invalid factory {factory_id!r}.", + f"Player {player_id} (client ID {client_id}) cannot select invalid factory {factory_id!r}.", ) await trio.lowlevel.checkpoint() return if tile < 0 or tile not in factory_display: print( - "Player {player_id} (client ID {client_id}) cannot select nonexistent color {tile}.", + f"Player {player_id} (client ID {client_id}) cannot select nonexistent color {tile}.", ) await trio.lowlevel.checkpoint() return @@ -872,7 +902,7 @@ async def handle_client_factory_clicked( int(tile), ): print( - "Player {player_id} (client ID {client_id}) cannot select factory tile, state says no.", + f"Player {player_id} (client ID {client_id}) cannot select factory tile, state says no.", ) await trio.lowlevel.checkpoint() return @@ -929,21 +959,21 @@ async def handle_client_pattern_row_clicked( if player_id != self.state.current_turn: print( - "Player {player_id} (client ID {client_id}) cannot select pattern row, not their turn.", + f"Player {player_id} (client ID {client_id}) cannot select pattern row, not their turn.", ) await trio.lowlevel.checkpoint() return if self.state.current_phase != Phase.factory_offer: print( - "Player {player_id} (client ID {client_id}) cannot select pattern row, not in factory offer phase.", + f"Player {player_id} (client ID {client_id}) cannot select pattern row, not in factory offer phase.", ) await trio.lowlevel.checkpoint() return if player_id != row_id: print( - "Player {player_id} (client ID {client_id}) cannot select pattern row {row_id} that does not belong to them.", + f"Player {player_id} (client ID {client_id}) cannot select pattern row {row_id} that does not belong to them.", ) await trio.lowlevel.checkpoint() return @@ -972,7 +1002,7 @@ async def handle_client_pattern_row_clicked( ) if self.state.current_turn != player_id: - if server_player_id != ServerPlayer.singleplayer_all: + if not self.internal_singleplayer_mode: new_client_id = self.find_client_id_from_state_turn( self.state.current_turn, ) @@ -1030,12 +1060,12 @@ async def handle_cursor_location( await trio.lowlevel.checkpoint() return - client_id, pos = event.data + client_id, location = event.data server_player_id = self.client_players[client_id] if server_player_id == ServerPlayer.spectator: - print(f"Spectator cannot select {pos = }") + print("Spectator cannot control cursor") await trio.lowlevel.checkpoint() return @@ -1045,13 +1075,27 @@ async def handle_cursor_location( if player_id != self.state.current_turn: print( - "Player {player_id} (client ID {client_id}) cannot move cursor, not their turn.", + f"Player {player_id} (client ID {client_id}) cannot move cursor, not their turn.", ) await trio.lowlevel.checkpoint() return - print(f"handle_cursor_location {client_id = } {pos = }") - await trio.lowlevel.checkpoint() + # print(f"handle_cursor_location {client_id = } {location = }") + + if self.internal_singleplayer_mode: + await trio.lowlevel.checkpoint() + return + + async with trio.open_nursery() as nursery: + for other_client_id in self.client_players: + if other_client_id != client_id: + nursery.start_soon( + self.raise_event, + Event( + f"cursor_position->network[{other_client_id}]", + location, + ), + ) def __del__(self) -> None: """Debug print.""" diff --git a/src/azul/sprite.py b/src/azul/sprite.py index ba625d4..b4f9fd5 100644 --- a/src/azul/sprite.py +++ b/src/azul/sprite.py @@ -573,15 +573,17 @@ async def move_destination_time(self, time_passed: float) -> None: return to_destination = self.to_destination() - travel_distance = min( - to_destination @ to_destination, - movement.speed * time_passed, - ) + dest_magnitude = to_destination.magnitude() + travel_distance = movement.speed * time_passed if travel_distance > 0: - movement.move_heading_distance(travel_distance) - # Fix imprecision - self.update_heading() + if travel_distance > dest_magnitude: + sprite.location = self.destination + else: + # Fix imprecision + self.update_heading() + if travel_distance > 0: + movement.move_heading_distance(travel_distance) await trio.lowlevel.checkpoint() async def move_destination_time_ticks( diff --git a/test-requirements.txt b/test-requirements.txt index 904fa0f..4eece78 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -6,7 +6,7 @@ attrs==24.3.0 # trio black==24.10.0 ; implementation_name == 'cpython' # via -r test-requirements.in -cffi==1.17.1 ; (os_name != 'nt' and platform_python_implementation != 'PyPy') or (implementation_name != 'pypy' and os_name == 'nt') or (implementation_name == 'pypy' and platform_python_implementation != 'PyPy') +cffi==1.17.1 ; (implementation_name != 'pypy' and os_name == 'nt') or platform_python_implementation != 'PyPy' # via # cryptography # trio @@ -14,7 +14,7 @@ click==8.1.7 ; implementation_name == 'cpython' # via black codespell==2.3.0 # via -r test-requirements.in -colorama==0.4.6 ; (implementation_name != 'cpython' and sys_platform == 'win32') or (platform_system != 'Windows' and sys_platform == 'win32') or (implementation_name == 'cpython' and platform_system == 'Windows') +colorama==0.4.6 ; (implementation_name == 'cpython' and platform_system == 'Windows') or sys_platform == 'win32' # via # click # pytest @@ -33,7 +33,7 @@ idna==3.10 # via trio iniconfig==2.0.0 # via pytest -libcomponent @ git+https://github.com/CoolCat467/LibComponent@b06eaca2f029a9fc019d68194ade260ccb00a841 +libcomponent @ git+https://github.com/CoolCat467/LibComponent@5c69fa04833560443dd3be414b5ce713793d5d42 # via -r test-requirements.in mypy==1.13.0 # via -r test-requirements.in @@ -61,7 +61,7 @@ platformdirs==4.3.6 ; implementation_name == 'cpython' # via black pluggy==1.5.0 # via pytest -pycparser==2.22 ; (os_name != 'nt' and platform_python_implementation != 'PyPy') or (implementation_name != 'pypy' and os_name == 'nt') or (implementation_name == 'pypy' and platform_python_implementation != 'PyPy') +pycparser==2.22 ; (implementation_name != 'pypy' and os_name == 'nt') or platform_python_implementation != 'PyPy' # via cffi pygame==2.6.1 # via -r test-requirements.in @@ -97,5 +97,5 @@ typing-extensions==4.12.2 # black # libcomponent # mypy -uv==0.5.9 +uv==0.5.10 # via -r test-requirements.in From c15bc1a743f7e8d873b9d7983aad2279b379626a Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Tue, 17 Dec 2024 23:41:42 -0600 Subject: [PATCH 23/58] Implement clicking table center and handle several edge cases I hadn't considered previously --- src/azul/client.py | 14 ++ src/azul/game.py | 74 +++++---- src/azul/network_shared.py | 1 + src/azul/server.py | 315 +++++++++++++++++++++++++++---------- src/azul/state.py | 19 ++- 5 files changed, 300 insertions(+), 123 deletions(-) diff --git a/src/azul/client.py b/src/azul/client.py index e2fe064..1813167 100644 --- a/src/azul/client.py +++ b/src/azul/client.py @@ -174,6 +174,7 @@ def __init__(self, name: str) -> None: "factory_clicked->server[write]": sbe.factory_clicked, "cursor_location->server[write]": sbe.cursor_location, "pattern_row_clicked->server[write]": sbe.pattern_row_clicked, + "table_clicked->server[write]": sbe.table_clicked, }, ) cbe = ClientBoundEvents @@ -221,6 +222,7 @@ def bind_handlers(self) -> None: "game_factory_clicked": self.write_game_factory_clicked, "game_cursor_location_transmit": self.write_game_cursor_location_transmit, "game_pattern_row_clicked": self.write_game_pattern_row_clicked, + "game_table_clicked": self.write_game_table_clicked, }, ) @@ -492,6 +494,18 @@ async def write_game_pattern_row_clicked( Event("pattern_row_clicked->server[write]", buffer), ) + async def write_game_table_clicked( + self, + event: Event[Tile], + ) -> None: + """Write table_clicked event to server.""" + tile = event.data + buffer = Buffer() + + buffer.write_value(StructFormat.UBYTE, tile) + + await self.raise_event(Event("table_clicked->server[write]", buffer)) + async def handle_network_stop(self, event: Event[None]) -> None: """Send EOF if connected and close socket.""" if self.not_connected: diff --git a/src/azul/game.py b/src/azul/game.py index 25ae555..e6fd2fa 100644 --- a/src/azul/game.py +++ b/src/azul/game.py @@ -678,37 +678,6 @@ async def handle_cursor_set_movement_mode( self.unregister_handler_type("tick") await trio.lowlevel.checkpoint() - def get_held_count(self) -> int: - """Return the number of held tiles.""" - return len(self.tiles) - - def is_holding(self) -> bool: - """Return True if the mouse is dragging something.""" - return len(self.tiles) > 0 - - def get_held_info(self) -> tuple[int, ...]: - """Return tuple of currently held tiles.""" - return tuple(reversed(self.tiles)) - - def drop( - self, - number: int | None = None, - ) -> tuple[int, ...]: - """Pop and return tiles the Cursor is carrying. - - If number is None, pops all tiles, otherwise only pops given count. - """ - if number is None: - tiles_copy = self.get_held_info() - self.tiles.clear() - self.update_image() - return tiles_copy - tiles: list[int] = [] - for _ in range(number): - tiles.append(self.tiles.pop()) - self.update_image() - return tuple(tiles) - class Grid(TileRenderer): """Grid object, used for boards and parts of other objects.""" @@ -1149,7 +1118,11 @@ async def handle_click( return index = int(point.y * 2 + point.x) - tile_color = tuple(self.tiles.elements())[index] + tiles = tuple(self.tiles.elements()) + if not tiles: + await trio.lowlevel.checkpoint() + return + tile_color = tiles[index] if tile_color < 0: # Do not send non-real tiles @@ -1184,13 +1157,20 @@ def __init__(self) -> None: self.update_image() self.visible = True + self.add_component(sprite.DragClickEventComponent()) + def __repr__(self) -> str: """Return representation of self.""" return f"{self.__class__.__name__}()" def bind_handlers(self) -> None: """Register event handlers.""" - self.register_handler("game_table_data", self.update_board_data) + self.register_handlers( + { + "game_table_data": self.update_board_data, + "click": self.handle_click, + }, + ) def iter_tiles(self) -> Generator[int, None, None]: """Yield tile colors.""" @@ -1246,6 +1226,34 @@ async def update_board_data(self, event: Event[Counter[int]]) -> None: self.update_image() await trio.lowlevel.checkpoint() + async def handle_click( + self, + event: Event[sprite.PygameMouseButtonEventData], + ) -> None: + """Handle click event.""" + point = self.get_tile_point(event.data["pos"]) + if point is None: + await trio.lowlevel.checkpoint() + return + + index = int(point.y * 6 + point.x) + tile_color = tuple(self.iter_tiles())[index] + + if tile_color < 0: + # Do not send non-real tiles + await trio.lowlevel.checkpoint() + return + + # Transmit to server + # Needs level 2 to reach server client + await self.raise_event( + Event( + "game_table_clicked", + Tile(tile_color), + 2, + ), + ) + class HaltState(AsyncState["AzulClient"]): """Halt state to set state to None so running becomes False.""" diff --git a/src/azul/network_shared.py b/src/azul/network_shared.py index 79333b5..d9b7d1e 100644 --- a/src/azul/network_shared.py +++ b/src/azul/network_shared.py @@ -149,3 +149,4 @@ class ServerBoundEvents(IntEnum): factory_clicked = auto() cursor_location = auto() pattern_row_clicked = auto() + table_clicked = auto() diff --git a/src/azul/server.py b/src/azul/server.py index e5f6b51..dc1f80b 100755 --- a/src/azul/server.py +++ b/src/azul/server.py @@ -115,6 +115,7 @@ def __init__(self, client_id: int) -> None: sbe.factory_clicked: f"client[{self.client_id}]->factory_clicked", sbe.cursor_location: f"client[{self.client_id}]->cursor_location", sbe.pattern_row_clicked: f"client[{self.client_id}]->pattern_row_clicked", + sbe.table_clicked: f"client[{self.client_id}]->table_clicked", }, ) @@ -127,6 +128,7 @@ def bind_handlers(self) -> None: f"client[{self.client_id}]->factory_clicked": self.read_factory_clicked, f"client[{self.client_id}]->cursor_location": self.read_cursor_location, f"client[{self.client_id}]->pattern_row_clicked": self.read_pattern_row_clicked, + f"client[{self.client_id}]->table_clicked": self.read_table_clicked, f"callback_ping->network[{self.client_id}]": self.handle_callback_ping, "initial_config->network": self.write_factory_clicked, f"playing_as->network[{self.client_id}]": self.write_playing_as, @@ -204,6 +206,22 @@ async def read_pattern_row_clicked(self, event: Event[bytearray]) -> None: ), ) + async def read_table_clicked(self, event: Event[bytearray]) -> None: + """Read table_clicked event from client. Raise as `table_clicked->server`.""" + buffer = Buffer(event.data) + + tile_color = Tile(buffer.read_value(StructFormat.UBYTE)) + + await self.raise_event( + Event( + "table_clicked->server", + ( + self.client_id, + tile_color, + ), + ), + ) + async def handle_callback_ping( self, _: Event[None], @@ -408,6 +426,7 @@ def bind_handlers(self) -> None: "factory_clicked->server": self.handle_client_factory_clicked, "pattern_row_clicked->server": self.handle_client_pattern_row_clicked, "cursor_location->server": self.handle_cursor_location, + "table_clicked->server": self.handle_client_table_clicked, }, ) @@ -557,51 +576,8 @@ async def start_server( # type: ignore[misc] # Serve runs forever until canceled nursery.start_soon(partial(self.serve, port, host, backlog=0)) - async def transmit_playing_as(self) -> None: - """Transmit playing as.""" - async with trio.open_nursery() as nursery: - for client_id, team in self.client_players.items(): - nursery.start_soon( - self.raise_event, - Event(f"playing_as->network[{client_id}]", team), - ) - - async def handle_server_start_new_game(self, event: Event[bool]) -> None: - """Handle game start.""" - varient_play = event.data - ## # Delete all pieces from last state (shouldn't be needed but still.) - ## async with trio.open_nursery() as nursery: - ## for piece_pos, _piece_type in self.state.get_pieces(): - ## nursery.start_soon( - ## self.raise_event, - ## Event("delete_piece->network", piece_pos), - ## ) - - # Choose which team plays first - # Using non-cryptographically secure random because it doesn't matter - self.new_game_init(varient_play) - - ## # Send create_piece events for all pieces - ## async with trio.open_nursery() as nursery: - ## for piece_pos, piece_type in self.state.get_pieces(): - ## nursery.start_soon( - ## self.raise_event, - ## Event("create_piece->network", (piece_pos, piece_type)), - ## ) - - # Raise initial config event with board size and initial turn. - await self.raise_event( - Event( - "initial_config->network", - ( - self.state.varient_play, - len(self.state.player_data), - len(self.state.factory_displays), - self.state.current_turn, - ), - ), - ) - + async def transmit_new_round_data(self) -> None: + """Transmit all player board data, factory data, and table center data.""" async with trio.open_nursery() as nursery: # Transmit board data for player_id, player_data in self.state.player_data.items(): @@ -638,9 +614,26 @@ async def handle_server_start_new_game(self, event: Event[bool]) -> None: ), ) - await self.transmit_cursor_movement_mode() - - await self.transmit_playing_as() + async def transmit_pattern_line_data(self) -> None: + """Transmit all pattern line data for all players.""" + async with trio.open_nursery() as nursery: + # Transmit pattern line data + for player_id, player_data in self.state.player_data.items(): + for line_id, line_data in enumerate(player_data.lines): + nursery.start_soon( + self.raise_event, + Event( + "pattern_data->network", + ( + player_id, + line_id, + ( + max(0, int(line_data.color)), + line_data.count_, + ), + ), + ), + ) async def transmit_cursor_movement_mode(self) -> None: """Update current cursor movement mode for all clients.""" @@ -666,6 +659,57 @@ async def transmit_cursor_movement_mode(self) -> None: ), ) + async def transmit_playing_as(self) -> None: + """Transmit playing as.""" + async with trio.open_nursery() as nursery: + for client_id, team in self.client_players.items(): + nursery.start_soon( + self.raise_event, + Event(f"playing_as->network[{client_id}]", team), + ) + + async def handle_server_start_new_game(self, event: Event[bool]) -> None: + """Handle game start.""" + varient_play = event.data + ## # Delete all pieces from last state (shouldn't be needed but still.) + ## async with trio.open_nursery() as nursery: + ## for piece_pos, _piece_type in self.state.get_pieces(): + ## nursery.start_soon( + ## self.raise_event, + ## Event("delete_piece->network", piece_pos), + ## ) + + # Choose which team plays first + # Using non-cryptographically secure random because it doesn't matter + self.new_game_init(varient_play) + + ## # Send create_piece events for all pieces + ## async with trio.open_nursery() as nursery: + ## for piece_pos, piece_type in self.state.get_pieces(): + ## nursery.start_soon( + ## self.raise_event, + ## Event("create_piece->network", (piece_pos, piece_type)), + ## ) + + # Raise initial config event with board size and initial turn. + await self.raise_event( + Event( + "initial_config->network", + ( + self.state.varient_play, + len(self.state.player_data), + len(self.state.factory_displays), + self.state.current_turn, + ), + ), + ) + + await self.transmit_new_round_data() + + await self.transmit_cursor_movement_mode() + + await self.transmit_playing_as() + async def client_network_loop( self, client: ServerClient, @@ -934,6 +978,69 @@ async def handle_client_factory_clicked( ), ) + async def handle_client_table_clicked( + self, + event: Event[tuple[int, Tile]], + ) -> None: + """Handle client clicked a table center tile.""" + if not self.players_can_interact: + print("Players are not allowed to interact.") + await trio.lowlevel.checkpoint() + return + + client_id, tile = event.data + + server_player_id = self.client_players[client_id] + + if server_player_id == ServerPlayer.spectator: + print(f"Spectator cannot select table center {tile}") + await trio.lowlevel.checkpoint() + return + + player_id = int(server_player_id) + if server_player_id == ServerPlayer.singleplayer_all: + player_id = self.state.current_turn + + if player_id != self.state.current_turn: + print( + f"Player {player_id} (client ID {client_id}) cannot select table center tile, not their turn.", + ) + await trio.lowlevel.checkpoint() + return + + if self.state.current_phase != Phase.factory_offer: + print( + f"Player {player_id} (client ID {client_id}) cannot select table center tile, not in factory offer phase.", + ) + await trio.lowlevel.checkpoint() + return + + if not self.state.can_cursor_select_center( + int(tile), + ): + print( + f"Player {player_id} (client ID {client_id}) cannot select table center tile, state says no.", + ) + await trio.lowlevel.checkpoint() + return + + # Perform move + self.state = self.state.cursor_selects_table_center(int(tile)) + + # Send updates to client + await self.raise_event( + Event( + "cursor_data->network", + self.state.cursor_contents, + ), + ) + await self.raise_event( + Event( + "table_data->network", + self.state.table_center, + ), + ) + async def handle_client_pattern_row_clicked( self, event: Event[tuple[int, int, tuple[int, int]]], @@ -979,13 +1086,36 @@ async def handle_client_pattern_row_clicked( return column, line_id = row_pos - place_count = 5 - column + if line_id >= 5: + print( + f"Player {player_id} (client ID {client_id}) cannot select pattern row {row_id} line {line_id} (invalid line id).", + ) + await trio.lowlevel.checkpoint() + return + if column >= 5: + print( + f"Player {player_id} (client ID {client_id}) cannot select pattern row {row_id} column {column} (invalid column).", + ) + await trio.lowlevel.checkpoint() + return + + currently_placed = self.state.get_player_line_current_place_count( + line_id, + ) + + place_count = 5 - column - currently_placed + + if self.state.is_cursor_empty(): + print( + f"Player {player_id} (client ID {client_id}) cannot select pattern row {row_id} when not holding tiles.", + ) + await trio.lowlevel.checkpoint() + return color = self.state.get_cursor_holding_color() - max_place = self.state.get_player_line_max_placable_count(line_id) current_hold_count = self.state.cursor_contents[color] - place_count = min(place_count, current_hold_count, max_place) + place_count = min(place_count, current_hold_count) if not self.state.can_player_select_line(line_id, color, place_count): print( @@ -1001,47 +1131,43 @@ async def handle_client_pattern_row_clicked( place_count, ) - if self.state.current_turn != player_id: - if not self.internal_singleplayer_mode: - new_client_id = self.find_client_id_from_state_turn( - self.state.current_turn, - ) - assert new_client_id is not None - await self.raise_event( - Event( - f"cursor_movement_mode->network[{client_id}]", - False, - ), - ) - await self.raise_event( - Event( - f"cursor_movement_mode->network[{new_client_id}]", - True, - ), - ) - + if ( + self.state.current_turn != player_id + and not self.internal_singleplayer_mode + ): + new_client_id = self.find_client_id_from_state_turn( + self.state.current_turn, + ) + assert new_client_id is not None await self.raise_event( Event( - "current_turn_change->network", - self.state.current_turn, + f"cursor_movement_mode->network[{client_id}]", + False, + ), + ) + await self.raise_event( + Event( + f"cursor_movement_mode->network[{new_client_id}]", + True, ), ) - raw_tile_color, tile_count = self.state.player_data[ - prev_player_turn - ].lines[line_id] - # Do not send blank colors, clamp to zero - tile_color = max(0, int(raw_tile_color)) - await self.raise_event( - Event( - "pattern_data->network", - ( - prev_player_turn, - line_id, - (tile_color, tile_count), + if self.state.current_phase != Phase.wall_tiling: + raw_tile_color, tile_count = self.state.player_data[ + prev_player_turn + ].lines[line_id] + # Do not send blank colors, clamp to zero + tile_color = max(0, int(raw_tile_color)) + await self.raise_event( + Event( + "pattern_data->network", + ( + prev_player_turn, + line_id, + (tile_color, tile_count), + ), ), - ), - ) + ) await self.raise_event( Event( @@ -1050,6 +1176,23 @@ async def handle_client_pattern_row_clicked( ), ) + if self.state.current_phase == Phase.end: + print("TODO: Handle end of game.") + + if self.state.current_phase == Phase.wall_tiling: + if not self.state.varient_play: + self.state = self.state.apply_auto_wall_tiling() + await self.transmit_new_round_data() + await self.transmit_pattern_line_data() + + if self.state.current_turn != player_id: + await self.raise_event( + Event( + "current_turn_change->network", + self.state.current_turn, + ), + ) + async def handle_cursor_location( self, event: Event[tuple[int, tuple[int, int]]], diff --git a/src/azul/state.py b/src/azul/state.py index 6018c0f..99bfb6b 100644 --- a/src/azul/state.py +++ b/src/azul/state.py @@ -235,6 +235,11 @@ def get_line_max_count(line_id: int) -> int: # Line id is keeping track of max count return line_id + 1 + def get_line_current_place_count(self, line_id: int) -> int: + """Return count of currently placed tiles for given line.""" + assert self.line_id_valid(line_id) + return self.lines[line_id].count_ + def get_line_max_placable_count(self, line_id: int) -> int: """Return max placable count for given line.""" assert self.line_id_valid(line_id) @@ -833,6 +838,12 @@ def get_player_line_max_placable_count(self, line_id: int) -> int: return player_data.get_line_max_placable_count(line_id) + def get_player_line_current_place_count(self, line_id: int) -> int: + """Return current place count for given line.""" + player_data = self.player_data[self.current_turn] + + return player_data.get_line_current_place_count(line_id) + def all_pullable_empty(self) -> bool: """Return if all pullable tile locations are empty, not counting cursor.""" if self.table_center.total(): @@ -857,13 +868,13 @@ def _factory_offer_maybe_next_turn(self) -> Self: # Go to wall tiling phase current_phase = Phase.wall_tiling - new_state = self._replace( + ##if current_phase == Phase.wall_tiling and not self.varient_play: + ## return new_state.apply_auto_wall_tiling() + ##return new_state + return self._replace( current_phase=current_phase, current_turn=current_turn, ) - if current_phase == Phase.wall_tiling and not self.varient_play: - return new_state.apply_auto_wall_tiling() - return new_state def player_select_floor_line(self, color: int, place_count: int) -> Self: """Return new state after player adds tiles to floor line.""" From d2b89a0110452cdf7a3d92ddf9dc8337785ff256 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Wed, 18 Dec 2024 12:18:37 -0600 Subject: [PATCH 24/58] Render floor line --- src/azul/game.py | 143 +++++++++++++++++++---------------------------- 1 file changed, 58 insertions(+), 85 deletions(-) diff --git a/src/azul/game.py b/src/azul/game.py index e6fd2fa..5998000 100644 --- a/src/azul/game.py +++ b/src/azul/game.py @@ -407,13 +407,12 @@ def clear_image( self.background, ) - def blit_tile( + def get_tile_topleft( self, - tile_color: int, tile_location: tuple[int, int], offset: tuple[int, int] | None = None, - ) -> None: - """Blit the surface of a given tile object onto self.image at given tile location. It is assumed that all tile locations are xy tuples.""" + ) -> tuple[int, int]: + """Return top left corner location of tile given its location and optional offset.""" tile_full = self.tile_size + self.tile_separation position = Vector2.from_iter(tile_location) * tile_full @@ -421,12 +420,35 @@ def blit_tile( position += offset position += (self.tile_separation, self.tile_separation) + return vec2_to_location(position) + + def get_tile_rect( + self, + tile_location: tuple[int, int], + offset: tuple[int, int] | None = None, + ) -> Rect: + """Return Rect of area given tile exists in.""" + topleft = self.get_tile_topleft(tile_location, offset) + return Rect( + topleft, + (self.tile_size, self.tile_size), + ) + + def blit_tile( + self, + tile_color: int, + tile_location: tuple[int, int], + offset: tuple[int, int] | None = None, + ) -> None: + """Blit the surface of a given tile object onto self.image at given tile location. It is assumed that all tile locations are xy tuples.""" + position = self.get_tile_topleft(tile_location, offset) + surf = get_tile_image(tile_color, self.tile_size, self.greyshift) assert self.image is not None self.image.blit( surf, - vec2_to_location(position), + position, ) def to_image_surface_location( @@ -798,62 +820,6 @@ async def handle_game_board_data( await trio.lowlevel.checkpoint() -class Row(TileRenderer): - """Represents one of the five rows each player has.""" - - __slots__ = ("color", "count", "size") - greyshift = GREYSHIFT - - def __init__( - self, - name: str, - size: int, - tile_separation: int | None = None, - background: tuple[int, int, int] | None = None, - ) -> None: - """Initialize row.""" - super().__init__( - name, - tile_separation, - background, - ) - - self.color = Tile.blank - self.size = int(size) - self.count = 0 - - def __repr__(self) -> str: - """Return representation of self.""" - return f"{self.__class__.__name__}({self.size})" - - def update_image(self) -> None: - """Update self.image.""" - self.clear_image((self.size, 1)) - - for x in range(self.count, self.size): - self.blit_tile(Tile.blank, (self.size - x, 0)) - for x in range(self.count): - self.blit_tile(self.color, (self.size - x, 0)) - self.dirty = 1 - - def get_placed(self) -> int: - """Return the number of tiles in self that are not fake tiles, like grey ones.""" - return self.count - - def get_placeable(self) -> int: - """Return the number of tiles permitted to be placed on self.""" - return self.size - self.get_placed() - - def is_full(self) -> bool: - """Return True if this row is full.""" - return self.get_placeable() == 0 - - def set_background(self, color: tuple[int, int, int] | None) -> None: - """Set the background color for this row.""" - self.background = color - self.update_image() - - class PatternRows(TileRenderer): """Represents one of the five rows each player has.""" @@ -984,42 +950,45 @@ async def handle_game_pattern_data( await trio.lowlevel.checkpoint() -class FloorLine(Row): +class FloorLine(TileRenderer): """Represents a player's floor line.""" - __slots__ = ("floor_line_id", "numbers", "text") + __slots__ = ("floor_line_id", "numbers", "size") def __init__(self, floor_line_id: int) -> None: """Initialize floor line.""" - super().__init__(f"floor_line_{floor_line_id}", 7, background=ORANGE) - - # self.font = Font(FONT, round(self.tile_size*1.2), color=BLACK, cx=False, cy=False) - self.text = objects.Text( - "text object", - pygame.font.Font(FONT, round(self.tile_size * 1.2)), + super().__init__( + f"floor_line_{floor_line_id}", + background=RED, ) - self.text.color = BLACK - self.numbers = [-255 for _ in range(self.size)] + self.size = 7 + + self.numbers = tuple(-1 for _ in range(self.size)) + self.tiles: list[Tile] = [] + + self.update_image() + self.visible = True def __repr__(self) -> str: """Return representation of self.""" - return f"{self.__class__.__name__}(<>)" + return f"{self.__class__.__name__}({self.floor_line_id})" - def render(self, surface: pygame.surface.Surface) -> None: + def update_image(self) -> None: """Update self.image.""" - sx, sy = self.location - w, h = self.rect.size - tile_full = self.tile_separation + self.tile_size - for x in range(self.size): - xy = round( - x * tile_full + self.tile_separation + sx - w / 2, - ), round( - self.tile_separation + sy - h / 2, - ) - self.text.text = str(self.numbers[x]) - self.text.location = Vector2(*xy) - # self.text.render(surface) + self.clear_image((self.size, 1)) + + font = pygame.font.Font(FONT, size=self.tile_size) + + for x, tile in enumerate(self.tiles): + self.blit_tile(tile, (x, 0)) + for x in range(len(self.tiles), self.size): + self.blit_tile(Tile.blank, (x, 0)) + # Draw number on top + number_surf = font.render(str(self.numbers[x]), False, BLACK) + tile_topleft = self.get_tile_topleft((x, 0)) + self.image.blit(number_surf, tile_topleft) + self.dirty = 1 class Factory(TileRenderer): @@ -1861,6 +1830,10 @@ async def handle_game_initial_config( pattern_rows.set_background(DARKGREEN) self.group_add(pattern_rows) + floor_line = FloorLine(index) + floor_line.rect.topleft = pattern_rows.rect.bottomleft + self.group_add(floor_line) + degrees += each async def check_conditions(self) -> str | None: From d212ff263b388b9f9cae766df26a25e0d23c77ed Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Wed, 18 Dec 2024 12:37:23 -0600 Subject: [PATCH 25/58] Use numbers length instead of keeping track of size --- src/azul/game.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/azul/game.py b/src/azul/game.py index 5998000..f830428 100644 --- a/src/azul/game.py +++ b/src/azul/game.py @@ -953,7 +953,7 @@ async def handle_game_pattern_data( class FloorLine(TileRenderer): """Represents a player's floor line.""" - __slots__ = ("floor_line_id", "numbers", "size") + __slots__ = ("floor_line_id", "numbers") def __init__(self, floor_line_id: int) -> None: """Initialize floor line.""" @@ -962,9 +962,7 @@ def __init__(self, floor_line_id: int) -> None: background=RED, ) - self.size = 7 - - self.numbers = tuple(-1 for _ in range(self.size)) + self.numbers = tuple(-1 for _ in range(7)) self.tiles: list[Tile] = [] self.update_image() @@ -976,13 +974,13 @@ def __repr__(self) -> str: def update_image(self) -> None: """Update self.image.""" - self.clear_image((self.size, 1)) + self.clear_image((len(self.numbers), 1)) font = pygame.font.Font(FONT, size=self.tile_size) for x, tile in enumerate(self.tiles): self.blit_tile(tile, (x, 0)) - for x in range(len(self.tiles), self.size): + for x in range(len(self.tiles), len(self.numbers)): self.blit_tile(Tile.blank, (x, 0)) # Draw number on top number_surf = font.render(str(self.numbers[x]), False, BLACK) From 19d7938dd77a02bd75182cfcf624df5329d4805a Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sun, 22 Dec 2024 13:28:19 -0600 Subject: [PATCH 26/58] WIP implement being able to click floor line --- src/azul/client.py | 23 +++++++++++++--- src/azul/game.py | 55 +++++++++++++++++++++++++++++++++++--- src/azul/mr_floppy_test.py | 45 ++++++++++++++++++++++++------- src/azul/network_shared.py | 1 + src/azul/server.py | 10 +++++-- test-requirements.txt | 10 +++---- 6 files changed, 119 insertions(+), 25 deletions(-) diff --git a/src/azul/client.py b/src/azul/client.py index 1813167..d4f9c3b 100644 --- a/src/azul/client.py +++ b/src/azul/client.py @@ -175,6 +175,7 @@ def __init__(self, name: str) -> None: "cursor_location->server[write]": sbe.cursor_location, "pattern_row_clicked->server[write]": sbe.pattern_row_clicked, "table_clicked->server[write]": sbe.table_clicked, + "floor_clicked->server[write]": sbe.floor_clicked, }, ) cbe = ClientBoundEvents @@ -223,6 +224,7 @@ def bind_handlers(self) -> None: "game_cursor_location_transmit": self.write_game_cursor_location_transmit, "game_pattern_row_clicked": self.write_game_pattern_row_clicked, "game_table_clicked": self.write_game_table_clicked, + "game_floor_clicked": self.write_game_floor_clicked, }, ) @@ -464,7 +466,7 @@ async def write_game_factory_clicked( buffer.write_value(StructFormat.UBYTE, factory_id) buffer.write_value(StructFormat.UBYTE, tile) - await self.raise_event(Event("factory_clicked->server[write]", buffer)) + await self.write_event(Event("factory_clicked->server[write]", buffer)) async def write_game_cursor_location_transmit( self, @@ -476,7 +478,7 @@ async def write_game_cursor_location_transmit( x, y = map(int, (scaled_location * 0xFFF).floored()) buffer = encode_cursor_location((x, y)) - await self.raise_event(Event("cursor_location->server[write]", buffer)) + await self.write_event(Event("cursor_location->server[write]", buffer)) async def write_game_pattern_row_clicked( self, @@ -490,7 +492,7 @@ async def write_game_pattern_row_clicked( buffer.write_value(StructFormat.UBYTE, int(location.x)) buffer.write_value(StructFormat.UBYTE, int(location.y)) - await self.raise_event( + await self.write_event( Event("pattern_row_clicked->server[write]", buffer), ) @@ -504,7 +506,20 @@ async def write_game_table_clicked( buffer.write_value(StructFormat.UBYTE, tile) - await self.raise_event(Event("table_clicked->server[write]", buffer)) + await self.write_event(Event("table_clicked->server[write]", buffer)) + + async def write_game_floor_clicked( + self, + event: Event[tuple[int, int]], + ) -> None: + """Write floor_clicked event to server.""" + floor_line_id, location_x = event.data + buffer = Buffer() + + buffer.write_value(StructFormat.UBYTE, floor_line_id) + buffer.write_value(StructFormat.UBYTE, location_x) + + await self.write_event(Event("floor_clicked->server[write]", buffer)) async def handle_network_stop(self, event: Event[None]) -> None: """Send EOF if connected and close socket.""" diff --git a/src/azul/game.py b/src/azul/game.py index f830428..c366249 100644 --- a/src/azul/game.py +++ b/src/azul/game.py @@ -591,6 +591,7 @@ def bind_handlers(self) -> None: "cursor_reached_destination": self.handle_cursor_reached_destination, "game_cursor_set_destination": self.handle_cursor_set_destination, "game_cursor_set_movement_mode": self.handle_cursor_set_movement_mode, + "client_disconnected": self.handle_network_stop, }, ) @@ -700,6 +701,15 @@ async def handle_cursor_set_movement_mode( self.unregister_handler_type("tick") await trio.lowlevel.checkpoint() + async def handle_network_stop( + self, + event: Event[None], + ) -> None: + """Unregister tick event handler.""" + print(f"[azul.game.Cursor] Got {event = }") + self.unregister_handler_type("tick") + await trio.lowlevel.checkpoint() + class Grid(TileRenderer): """Grid object, used for boards and parts of other objects.""" @@ -962,6 +972,10 @@ def __init__(self, floor_line_id: int) -> None: background=RED, ) + self.add_component(sprite.DragClickEventComponent()) + + self.floor_line_id = floor_line_id + self.numbers = tuple(-1 for _ in range(7)) self.tiles: list[Tile] = [] @@ -988,6 +1002,36 @@ def update_image(self) -> None: self.image.blit(number_surf, tile_topleft) self.dirty = 1 + def bind_handlers(self) -> None: + """Register event handlers.""" + self.register_handlers( + { + "click": self.handle_click, + }, + ) + + async def handle_click( + self, + event: Event[sprite.PygameMouseButtonEventData], + ) -> None: + """Handle click event.""" + point = self.get_tile_point(event.data["pos"]) + if point is None: + await trio.lowlevel.checkpoint() + return + + # Transmit to server + await self.raise_event( + Event( + "game_floor_clicked", + ( + self.floor_line_id, + int(point.floored().x), + ), + 2, + ), + ) + class Factory(TileRenderer): """Represents a Factory.""" @@ -1868,7 +1912,7 @@ async def handle_game_over(self, event: Event[int]) -> None: async def handle_client_disconnected(self, event: Event[str]) -> None: """Handle client disconnected error.""" error = event.data - print(f"handle_client_disconnected {error = }") + print(f"[azul.game.PlayState] handle_client_disconnected {error = }") self.exit_data = (1, f"Client Disconnected$${error}", False) @@ -1878,11 +1922,11 @@ async def do_actions(self) -> None: if self.exit_data is None: return - exit_status, message, handled = self.exit_data + exit_status, raw_message, handled = self.exit_data if handled: return - self.exit_data = (exit_status, message, True) + self.exit_data = (exit_status, raw_message, True) font = pygame.font.Font( FONT, @@ -1891,7 +1935,9 @@ async def do_actions(self) -> None: error_message = "" if exit_status == 1: - message, error_message = message.split("$$") + message, error_message = raw_message.split("$$", 1) + else: + message = raw_message if not self.manager.component_exists("continue_button"): continue_button = KwargButton( @@ -1913,6 +1959,7 @@ async def do_actions(self) -> None: if exit_status == 1: if not self.manager.component_exists("error_text"): error_text = objects.OutlinedText("error_text", font) + error_text.text = "" else: error_text = self.manager.get_component("error_text") error_text.visible = True diff --git a/src/azul/mr_floppy_test.py b/src/azul/mr_floppy_test.py index 5275dda..878df8c 100644 --- a/src/azul/mr_floppy_test.py +++ b/src/azul/mr_floppy_test.py @@ -47,7 +47,7 @@ FONT = FONT_FOLDER / "RuneScape-UF-Regular.ttf" -class AzulClient(sprite.GroupProcessor, AsyncStateMachine): +class GameClient(sprite.GroupProcessor, AsyncStateMachine): """Gear Runner and Layered Dirty Sprite group handler.""" def __init__(self) -> None: @@ -78,7 +78,7 @@ async def raise_event(self, event: Event[Any]) -> None: await manager.raise_event(event) -class AzulState(AsyncState[AzulClient]): +class AzulState(AsyncState[GameClient]): """Azul Client Asynchronous base class.""" __slots__ = ("id", "manager") @@ -120,7 +120,7 @@ def bind_handlers(self) -> None: self.register_handlers( { "click": self.click, - "drag": self.drag, + # "drag": self.drag, "PygameMouseButtonDown": self.mouse_down, "tick": self.move_towards_dest, "init": self.cache_outline, @@ -132,12 +132,16 @@ async def test(self, event: Event[object]) -> None: """Print out event data.""" print(f"{event = }") + await trio.lowlevel.checkpoint() + async def cache_outline(self, _: Event[None]) -> None: """Precalculate outlined images.""" image: sprite.ImageComponent = self.get_component("image") outline: sprite.OutlineComponent = image.get_component("outline") outline.precalculate_all_outlined(self.outline) + await trio.lowlevel.checkpoint() + async def update_selected(self) -> None: """Update selected.""" image: sprite.ImageComponent = self.get_component("image") @@ -150,15 +154,19 @@ async def update_selected(self) -> None: movement: sprite.MovementComponent = self.get_component("movement") movement.speed = 0 + await trio.lowlevel.checkpoint() + async def click( self, event: Event[sprite.PygameMouseButtonEventData], ) -> None: """Toggle selected.""" - if event.data["button"] == 1: - self.selected = not self.selected + if event.data["button"] != 1: + await trio.lowlevel.checkpoint() + return + self.selected = not self.selected - await self.update_selected() + await self.update_selected() async def drag(self, event: Event[None]) -> None: """Drag sprite.""" @@ -168,12 +176,15 @@ async def drag(self, event: Event[None]) -> None: movement: sprite.MovementComponent = self.get_component("movement") movement.speed = 0 + await trio.lowlevel.checkpoint() + async def mouse_down( self, event: Event[sprite.PygameMouseButtonEventData], ) -> None: """Target click pos if selected.""" if not self.selected: + await trio.lowlevel.checkpoint() return if event.data["button"] == 1: movement: sprite.MovementComponent = self.get_component("movement") @@ -181,6 +192,8 @@ async def mouse_down( target: sprite.TargetingComponent = self.get_component("targeting") target.destination = Vector2.from_iter(event.data["pos"]) + await trio.lowlevel.checkpoint() + async def move_towards_dest( self, event: Event[sprite.TickEventData], @@ -199,12 +212,14 @@ def __init__(self) -> None: """Initialize mr floppy sprite.""" super().__init__("MrFloppy") + image_component = sprite.ImageComponent() + image_component.add_component(sprite.AnimationComponent()) self.add_components( ( sprite.MovementComponent(), sprite.TargetingComponent(), ClickDestinationComponent(), - sprite.ImageComponent(), + image_component, sprite.DragClickEventComponent(), ), ) @@ -257,11 +272,14 @@ def controller( async def drag(self, event: Event[sprite.DragEvent]) -> None: """Move by relative from drag.""" - if not event.data.buttons[1]: + if not event.data.buttons.get(1): + await trio.lowlevel.checkpoint() return self.location += event.data.rel self.dirty = 1 + await trio.lowlevel.checkpoint() + class FPSCounter(objects.Text): """FPS counter.""" @@ -273,11 +291,16 @@ def __init__(self) -> None: font = pygame.font.Font(FONT, 28) super().__init__("fps", font) + self.text = "FPS: ???" + self.visible = True + async def on_tick(self, event: Event[sprite.TickEventData]) -> None: """Update text.""" # self.text = f'FPS: {event.data["fps"]:.2f}' self.text = f"FPS: {event.data.fps:.0f}" + await trio.lowlevel.checkpoint() + async def update_loc( self, event: Event[dict[str, tuple[int, int]]], @@ -316,7 +339,7 @@ async def entry_actions(self) -> None: """Create group and add mr floppy.""" self.id = self.machine.new_group("test") floppy = MrFloppy() - print(floppy) + print(f"{floppy = }") self.group_add(floppy) self.group_add(FPSCounter()) @@ -327,6 +350,8 @@ async def exit_actions(self) -> None: self.machine.remove_group(self.id) self.manager.unbind_components() + await trio.lowlevel.checkpoint() + def save_crash_img() -> None: """Save the last frame before the game crashed.""" @@ -355,7 +380,7 @@ async def async_run() -> None: pygame.key.set_repeat(1000, 30) screen.fill((0xFF, 0xFF, 0xFF)) - client = AzulClient() + client = GameClient() background = pygame.image.load( path.join("data", "background.png"), diff --git a/src/azul/network_shared.py b/src/azul/network_shared.py index d9b7d1e..76010f1 100644 --- a/src/azul/network_shared.py +++ b/src/azul/network_shared.py @@ -150,3 +150,4 @@ class ServerBoundEvents(IntEnum): cursor_location = auto() pattern_row_clicked = auto() table_clicked = auto() + floor_clicked = auto() diff --git a/src/azul/server.py b/src/azul/server.py index dc1f80b..79a60f7 100755 --- a/src/azul/server.py +++ b/src/azul/server.py @@ -748,14 +748,20 @@ async def client_network_loop( except network.NetworkEOFError: print(f"{client.name} EOF") break + except RuntimeError as exc: + traceback.print_exception(exc) + print(f"{client.name} Bad packet") + break except ( trio.BrokenResourceError, trio.ClosedResourceError, - RuntimeError, - ): + ) as exc: + traceback.print_exception(exc) + print(f"{client.name} Socket connection issue") break except Exception as exc: traceback.print_exception(exc) + print(f"{client.name} Unhandled exception") break if event is not None: # if controls_lobby: diff --git a/test-requirements.txt b/test-requirements.txt index 4eece78..71fa0ea 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -10,7 +10,7 @@ cffi==1.17.1 ; (implementation_name != 'pypy' and os_name == 'nt') or platform_p # via # cryptography # trio -click==8.1.7 ; implementation_name == 'cpython' +click==8.1.8 ; implementation_name == 'cpython' # via black codespell==2.3.0 # via -r test-requirements.in @@ -33,9 +33,9 @@ idna==3.10 # via trio iniconfig==2.0.0 # via pytest -libcomponent @ git+https://github.com/CoolCat467/LibComponent@5c69fa04833560443dd3be414b5ce713793d5d42 +libcomponent @ git+https://github.com/CoolCat467/LibComponent@91362869af19943520430fbed9edc38fc473ac9e # via -r test-requirements.in -mypy==1.13.0 +mypy==1.14.0 # via -r test-requirements.in mypy-extensions==1.0.0 # via @@ -74,7 +74,7 @@ pytest-cov==6.0.0 # via -r test-requirements.in pytest-trio==0.8.0 # via -r test-requirements.in -ruff==0.8.3 +ruff==0.8.4 # via -r test-requirements.in sniffio==1.3.1 # via trio @@ -97,5 +97,5 @@ typing-extensions==4.12.2 # black # libcomponent # mypy -uv==0.5.10 +uv==0.5.11 # via -r test-requirements.in From acb31b9287222ad5c6cd2fb94e5e53d1ea32987b Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sun, 22 Dec 2024 13:31:20 -0600 Subject: [PATCH 27/58] Add `crate-ci/typos` pre-commit hook and associated spelling fixes --- .pre-commit-config.yaml | 6 ++++++ src/azul/client.py | 4 ++-- src/azul/game.py | 2 +- src/azul/server.py | 22 +++++++++++----------- src/azul/sprite.py | 6 +++--- src/azul/state.py | 34 +++++++++++++++++----------------- src/azul/vector.py | 2 +- tests/test_async_clock.py | 2 +- 8 files changed, 42 insertions(+), 36 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ff6904d..d965914 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,6 +36,12 @@ repos: rev: v2.3.0 hooks: - id: codespell + additional_dependencies: + - tomli + - repo: https://github.com/crate-ci/typos + rev: v1.28.4 + hooks: + - id: typos - repo: https://github.com/woodruffw/zizmor-pre-commit rev: v0.9.2 hooks: diff --git a/src/azul/client.py b/src/azul/client.py index d4f9c3b..7911144 100644 --- a/src/azul/client.py +++ b/src/azul/client.py @@ -345,7 +345,7 @@ async def read_initial_config(self, event: Event[bytearray]) -> None: """Read initial_config event from server.""" buffer = Buffer(event.data) - varient_play: u8 = buffer.read_value(StructFormat.BOOL) + variant_play: u8 = buffer.read_value(StructFormat.BOOL) player_count: u8 = buffer.read_value(StructFormat.UBYTE) factory_count: u8 = buffer.read_value(StructFormat.UBYTE) current_turn: u8 = buffer.read_value(StructFormat.UBYTE) @@ -354,7 +354,7 @@ async def read_initial_config(self, event: Event[bytearray]) -> None: Event( "game_initial_config", ( - varient_play, + variant_play, player_count, factory_count, current_turn, diff --git a/src/azul/game.py b/src/azul/game.py index c366249..6081afa 100644 --- a/src/azul/game.py +++ b/src/azul/game.py @@ -1830,7 +1830,7 @@ async def handle_game_initial_config( event: Event[tuple[bool, int, int, int]], ) -> None: """Handle `game_initial_config` event.""" - varient_play, player_count, factory_count, self.current_turn = ( + variant_play, player_count, factory_count, self.current_turn = ( event.data ) diff --git a/src/azul/server.py b/src/azul/server.py index 79a60f7..ca18422 100755 --- a/src/azul/server.py +++ b/src/azul/server.py @@ -234,11 +234,11 @@ async def write_factory_clicked( event: Event[tuple[bool, int, int, int]], ) -> None: """Read initial config event and reraise as server[write]->initial_config.""" - varient_play, player_count, factory_count, current_turn = event.data + variant_play, player_count, factory_count, current_turn = event.data buffer = Buffer() - buffer.write_value(StructFormat.BOOL, varient_play) + buffer.write_value(StructFormat.BOOL, variant_play) buffer.write_value(StructFormat.UBYTE, player_count) buffer.write_value(StructFormat.UBYTE, factory_count) buffer.write_value(StructFormat.UBYTE, current_turn) @@ -531,14 +531,14 @@ def setup_teams(client_ids: list[int]) -> dict[int, int]: players[client_id] = ServerPlayer.spectator return players - def new_game_init(self, varient_play: bool = False) -> None: + def new_game_init(self, variant_play: bool = False) -> None: """Start new game.""" print("server new_game_init") self.client_players.clear() self.state = State.new_game( max(2, min(4, self.client_count)), - varient_play, + variant_play, ) # Why keep track of another object just to know client ID numbers @@ -670,7 +670,7 @@ async def transmit_playing_as(self) -> None: async def handle_server_start_new_game(self, event: Event[bool]) -> None: """Handle game start.""" - varient_play = event.data + variant_play = event.data ## # Delete all pieces from last state (shouldn't be needed but still.) ## async with trio.open_nursery() as nursery: ## for piece_pos, _piece_type in self.state.get_pieces(): @@ -681,7 +681,7 @@ async def handle_server_start_new_game(self, event: Event[bool]) -> None: # Choose which team plays first # Using non-cryptographically secure random because it doesn't matter - self.new_game_init(varient_play) + self.new_game_init(variant_play) ## # Send create_piece events for all pieces ## async with trio.open_nursery() as nursery: @@ -696,7 +696,7 @@ async def handle_server_start_new_game(self, event: Event[bool]) -> None: Event( "initial_config->network", ( - self.state.varient_play, + self.state.variant_play, len(self.state.player_data), len(self.state.factory_displays), self.state.current_turn, @@ -796,7 +796,7 @@ async def send_spectator_join_packets( Event( "initial_config->network", ( - self.state.varient_play, + self.state.variant_play, len(self.state.player_data), len(self.state.factory_displays), self.state.current_turn, @@ -853,9 +853,9 @@ async def handler(self, stream: trio.SocketStream) -> None: with self.temporary_component(client): if can_start and not game_active: # and is_zee_capitan: print("[azul.server] game start trigger.") - varient_play = False + variant_play = False await self.raise_event( - Event("server_send_game_start", varient_play), + Event("server_send_game_start", variant_play), ) try: await self.client_network_loop(client, is_zee_capitan) @@ -1186,7 +1186,7 @@ async def handle_client_pattern_row_clicked( print("TODO: Handle end of game.") if self.state.current_phase == Phase.wall_tiling: - if not self.state.varient_play: + if not self.state.variant_play: self.state = self.state.apply_auto_wall_tiling() await self.transmit_new_round_data() await self.transmit_pattern_line_data() diff --git a/src/azul/sprite.py b/src/azul/sprite.py index b4f9fd5..f753ddb 100644 --- a/src/azul/sprite.py +++ b/src/azul/sprite.py @@ -325,7 +325,7 @@ def set_color(self, color: Color | None) -> None: assert manager.set_surface is not None manager.set_image(manager.set_surface) - def get_outline_discriptor(self, identifier: str | int) -> str: + def get_outline_descriptor(self, identifier: str | int) -> str: """Return outlined identifier for given original identifier.""" color = "_".join(map(str, self.__color)) return f"{identifier}{self.mod}{color}_{self.size}" @@ -334,7 +334,7 @@ def save_outline(self, identifier: str | int) -> None: """Save outlined version of given identifier image.""" manager = cast(ImageComponent, self.manager) - outlined = self.get_outline_discriptor(identifier) + outlined = self.get_outline_descriptor(identifier) if manager.image_exists(outlined): return @@ -367,7 +367,7 @@ def save_outline(self, identifier: str | int) -> None: def get_outline(self, identifier: str | int) -> str: """Return saved outline effect identifier.""" self.save_outline(identifier) - return self.get_outline_discriptor(identifier) + return self.get_outline_descriptor(identifier) def precalculate_outline( self, diff --git a/src/azul/state.py b/src/azul/state.py index 99bfb6b..09f0176 100644 --- a/src/azul/state.py +++ b/src/azul/state.py @@ -183,8 +183,8 @@ def floor_fill_tile_excess( return excess -class UnplacableTileError(Exception): - """Unplacable Tile Exception.""" +class UnplayableTileError(Exception): + """Unplayable Tile Exception.""" __slots__ = ("y",) @@ -202,11 +202,11 @@ class PlayerData(NamedTuple): floor: Counter[int] @classmethod - def new(cls, varient_play: bool = False) -> Self: + def new(cls, variant_play: bool = False) -> Self: """Return new player data instance.""" wall = full((5, 5), Tile.blank, int8) - if not varient_play: + if not variant_play: for y in range(5): for x in range(5): color = -((5 - y + x) % len(REAL_TILES) + 1) @@ -483,7 +483,7 @@ def perform_end_of_game_scoring(self) -> Self: def get_manual_wall_tile_location(self) -> tuple[int, list[int]] | None: """Return tuple of row and placable columns for wall tiling, or None if done. - Raises UnplacableTileError if no valid placement locations. + Raises UnplayableTileError if no valid placement locations. """ for y, line in enumerate(self.lines): if line.color == Tile.blank: @@ -499,11 +499,11 @@ def get_manual_wall_tile_location(self) -> tuple[int, list[int]] | None: continue valid_x.append(x) if not valid_x: - raise UnplacableTileError(y) + raise UnplayableTileError(y) return (y, valid_x) return None - def handle_unplacable_wall_tiling( + def handle_unplayable_wall_tiling( self, y: int, ) -> tuple[Self, Counter[int]]: @@ -646,7 +646,7 @@ class SelectableDestinationTiles(NamedTuple): class State(NamedTuple): """Represents state of an azul game.""" - varient_play: bool + variant_play: bool current_phase: Phase bag: Counter[int] box_lid: Counter[int] @@ -660,7 +660,7 @@ class State(NamedTuple): def blank(cls) -> Self: """Return new blank state.""" return cls( - varient_play=False, + variant_play=False, current_phase=Phase.end, bag=Counter(), box_lid=Counter(), @@ -672,7 +672,7 @@ def blank(cls) -> Self: ) @classmethod - def new_game(cls, player_count: int, varient_play: bool = False) -> Self: + def new_game(cls, player_count: int, variant_play: bool = False) -> Self: """Return state of a new game.""" factory_count = player_count * 2 + 1 bag = generate_bag_contents() @@ -685,7 +685,7 @@ def new_game(cls, player_count: int, varient_play: bool = False) -> Self: factory_displays[x] = tiles return cls( - varient_play=varient_play, + variant_play=variant_play, current_phase=Phase.factory_offer, bag=bag, box_lid=Counter(), @@ -694,7 +694,7 @@ def new_game(cls, player_count: int, varient_play: bool = False) -> Self: cursor_contents=Counter(), current_turn=0, player_data={ - x: PlayerData.new(varient_play) for x in range(player_count) + x: PlayerData.new(variant_play) for x in range(player_count) }, ) @@ -868,7 +868,7 @@ def _factory_offer_maybe_next_turn(self) -> Self: # Go to wall tiling phase current_phase = Phase.wall_tiling - ##if current_phase == Phase.wall_tiling and not self.varient_play: + ##if current_phase == Phase.wall_tiling and not self.variant_play: ## return new_state.apply_auto_wall_tiling() ##return new_state return self._replace( @@ -1030,7 +1030,7 @@ def apply_destination_select_action_factory_offer( def apply_auto_wall_tiling(self) -> Self: """Return new state after performing automatic wall tiling.""" assert self.current_phase == Phase.wall_tiling - assert not self.varient_play + assert not self.variant_play box_lid = self.box_lid.copy() new_players = player_data_deepcopy(self.player_data) @@ -1191,12 +1191,12 @@ def get_manual_wall_tiling_locations_for_player( try: return current_player_data.get_manual_wall_tile_location() - except UnplacableTileError as unplacable_exc: + except UnplayableTileError as unplayable_exc: # kind of hacky, but it works - y_position = unplacable_exc.y + y_position = unplayable_exc.y new_player_data, for_box_lid = ( - current_player_data.handle_unplacable_wall_tiling(y_position) + current_player_data.handle_unplayable_wall_tiling(y_position) ) box_lid = self.box_lid.copy() diff --git a/src/azul/vector.py b/src/azul/vector.py index 57d50a0..f9aa830 100644 --- a/src/azul/vector.py +++ b/src/azul/vector.py @@ -41,7 +41,7 @@ # As a forward to the madness below, we are doing something incredibly sneeky. # We have BaseVector, which we want to have all of the shared functionality # of all Vector subclasses. We also want each Vector class to be a NamedTuple -# so we can let Python handle storing data in the most efficiant way and +# so we can let Python handle storing data in the most efficient way and # make Vectors immutable. # # Problem is, we can't have Vector classes be diff --git a/tests/test_async_clock.py b/tests/test_async_clock.py index 7c97954..4297787 100644 --- a/tests/test_async_clock.py +++ b/tests/test_async_clock.py @@ -28,7 +28,7 @@ def test_get_time(clock: Clock) -> None: @pytest.mark.trio -async def test_tick_elasped(clock: Clock) -> None: +async def test_tick_elapsed(clock: Clock) -> None: time_passed = await clock.tick() assert time_passed >= 0 From f24f2f29fe24fefa0a32c1b06cd2a996a1c4cc6b Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sun, 22 Dec 2024 13:52:46 -0600 Subject: [PATCH 28/58] Stop writing events after client disconnects --- src/azul/client.py | 27 +++++++++++++++++++++++++++ src/azul/game.py | 7 ++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/azul/client.py b/src/azul/client.py index 7911144..420f967 100644 --- a/src/azul/client.py +++ b/src/azul/client.py @@ -244,10 +244,37 @@ async def raise_disconnect(self, message: str) -> None: f"{self.__class__.__name__}: Manager does not exist, not raising disconnect event.", ) return + # self.unregister_all_network_write_events() await self.raise_event(Event("client_disconnected", message)) await self.close() assert self.not_connected + async def write_event( + self, + event: Event[bytes | bytearray | memoryview], + ) -> None: + """Send event to network if running, otherwise does nothing. + + Raises: + RuntimeError: if unregistered packet id received from network + trio.BusyResourceError: if another task is already executing a + :meth:`send_all`, :meth:`wait_send_all_might_not_block`, or + :meth:`HalfCloseableStream.send_eof` on this stream. + trio.BrokenResourceError: if something has gone wrong, and the stream + is broken. + trio.ClosedResourceError: if you previously closed this stream + object, or if another task closes this stream object while + :meth:`send_all` is running. + + """ + if not self.running: + await trio.lowlevel.checkpoint() + print( + f"[azul.client.write_event] Skipping writing {event.name!r}, not running.", + ) + return + await super().write_event(event) + async def handle_read_event(self) -> None: """Raise events from server. diff --git a/src/azul/game.py b/src/azul/game.py index 6081afa..b0cf55c 100644 --- a/src/azul/game.py +++ b/src/azul/game.py @@ -539,6 +539,7 @@ class Cursor(TileRenderer): - cursor_reached_destination - cursor_set_destination - cursor_set_movement_mode + - client_disconnected Sometimes registered: - PygameMouseMotion @@ -591,7 +592,7 @@ def bind_handlers(self) -> None: "cursor_reached_destination": self.handle_cursor_reached_destination, "game_cursor_set_destination": self.handle_cursor_set_destination, "game_cursor_set_movement_mode": self.handle_cursor_set_movement_mode, - "client_disconnected": self.handle_network_stop, + "client_disconnected": self.handle_client_disconnected, }, ) @@ -701,12 +702,12 @@ async def handle_cursor_set_movement_mode( self.unregister_handler_type("tick") await trio.lowlevel.checkpoint() - async def handle_network_stop( + async def handle_client_disconnected( self, event: Event[None], ) -> None: """Unregister tick event handler.""" - print(f"[azul.game.Cursor] Got {event = }") + print("[azul.game.Cursor] Got client disconnect, unregistering tick") self.unregister_handler_type("tick") await trio.lowlevel.checkpoint() From 87276051e89b7d72378d90879cc0b771c274dbc6 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sun, 22 Dec 2024 17:01:19 -0600 Subject: [PATCH 29/58] Implement server-side handling for floor line --- src/azul/client.py | 28 +++++- src/azul/game.py | 43 +++++++-- src/azul/network_shared.py | 9 +- src/azul/server.py | 183 ++++++++++++++++++++++++++++++++++--- src/azul/state.py | 2 +- 5 files changed, 236 insertions(+), 29 deletions(-) diff --git a/src/azul/client.py b/src/azul/client.py index 420f967..88d1be8 100644 --- a/src/azul/client.py +++ b/src/azul/client.py @@ -194,6 +194,7 @@ def __init__(self, name: str) -> None: cbe.cursor_movement_mode: "server->cursor_movement_mode", cbe.current_turn_change: "server->current_turn_change", cbe.cursor_position: "server->cursor_position", + cbe.floor_data: "server->floor_data", }, ) @@ -218,6 +219,7 @@ def bind_handlers(self) -> None: "server->cursor_movement_mode": self.read_cursor_movement_mode, "server->current_turn_change": self.read_current_turn_change, "server->cursor_position": self.read_cursor_position, + "server->floor_data": self.read_floor_data, "client_connect": self.handle_client_connect, "network_stop": self.handle_network_stop, "game_factory_clicked": self.write_game_factory_clicked, @@ -225,16 +227,19 @@ def bind_handlers(self) -> None: "game_pattern_row_clicked": self.write_game_pattern_row_clicked, "game_table_clicked": self.write_game_table_clicked, "game_floor_clicked": self.write_game_floor_clicked, + # "callback_ping": self.print_callback_ping, }, ) - async def print_callback_ping(self, event: Event[bytearray]) -> None: + async def print_callback_ping(self, event: Event[int]) -> None: """Print received `callback_ping` event from server. This event is used as a sort of keepalive heartbeat, because it stops the connection from timing out. """ - print(f"print_callback_ping {event = }") + difference = event.data + print(f"[azul.client] print_callback_ping {difference * 1e-06:.03f}ms") + await trio.lowlevel.checkpoint() async def raise_disconnect(self, message: str) -> None: """Raise client_disconnected event with given message.""" @@ -298,6 +303,7 @@ async def handle_read_event(self) -> None: if self.not_connected: await self.raise_disconnect("Not connected to server.") return + # event: Event[bytearray] | None = None try: # print("handle_read_event start") event = await self.read_event() @@ -307,6 +313,7 @@ async def handle_read_event(self) -> None: print(f"[{self.name}] Socket closed from another task.") return except network.NetworkTimeoutError as exc: + # print("[azul.client] Network timeout") if self.running: self.running = False print(f"[{self.name}] NetworkTimeoutError") @@ -332,6 +339,8 @@ async def handle_read_event(self) -> None: ) return + ## print(f'[azul.client] handle_read_event {event}') + await self.raise_event(event) async def handle_client_connect( @@ -376,6 +385,9 @@ async def read_initial_config(self, event: Event[bytearray]) -> None: player_count: u8 = buffer.read_value(StructFormat.UBYTE) factory_count: u8 = buffer.read_value(StructFormat.UBYTE) current_turn: u8 = buffer.read_value(StructFormat.UBYTE) + floor_line_size: u8 = buffer.read_value(StructFormat.UBYTE) + + floor_line_data = decode_int8_array(buffer, (floor_line_size, 1)) await self.raise_event( Event( @@ -385,6 +397,7 @@ async def read_initial_config(self, event: Event[bytearray]) -> None: player_count, factory_count, current_turn, + floor_line_data, ), ), ) @@ -482,6 +495,17 @@ async def read_cursor_position(self, event: Event[bytearray]) -> None: Event("game_cursor_set_destination", unit_location), ) + async def read_floor_data(self, event: Event[bytearray]) -> None: + """Read floor_data event from server, reraise as `game_floor_data`.""" + buffer = Buffer(event.data) + + floor_id: u8 = buffer.read_value(StructFormat.UBYTE) + floor_line = decode_numeric_uint8_counter(buffer) + + await self.raise_event( + Event("game_floor_data", (floor_id, floor_line)), + ) + async def write_game_factory_clicked( self, event: Event[tuple[int, Tile]], diff --git a/src/azul/game.py b/src/azul/game.py index b0cf55c..ece5b38 100644 --- a/src/azul/game.py +++ b/src/azul/game.py @@ -85,6 +85,7 @@ Sequence, ) + from mypy_extensions import u8 from numpy.typing import NDArray from typing_extensions import TypeVarTuple @@ -966,7 +967,7 @@ class FloorLine(TileRenderer): __slots__ = ("floor_line_id", "numbers") - def __init__(self, floor_line_id: int) -> None: + def __init__(self, floor_line_id: int, numbers: NDArray[int8]) -> None: """Initialize floor line.""" super().__init__( f"floor_line_{floor_line_id}", @@ -977,8 +978,8 @@ def __init__(self, floor_line_id: int) -> None: self.floor_line_id = floor_line_id - self.numbers = tuple(-1 for _ in range(7)) - self.tiles: list[Tile] = [] + self.numbers = tuple(numbers.flat) + self.tiles: Counter[Tile] = Counter() self.update_image() self.visible = True @@ -993,9 +994,9 @@ def update_image(self) -> None: font = pygame.font.Font(FONT, size=self.tile_size) - for x, tile in enumerate(self.tiles): + for x, tile in enumerate(sorted(self.tiles.elements(), reverse=True)): self.blit_tile(tile, (x, 0)) - for x in range(len(self.tiles), len(self.numbers)): + for x in range(self.tiles.total(), len(self.numbers)): self.blit_tile(Tile.blank, (x, 0)) # Draw number on top number_surf = font.render(str(self.numbers[x]), False, BLACK) @@ -1007,10 +1008,28 @@ def bind_handlers(self) -> None: """Register event handlers.""" self.register_handlers( { + "game_floor_data": self.handle_game_floor_data, "click": self.handle_click, }, ) + async def handle_game_floor_data( + self, + event: Event[tuple[int, Counter[u8]]], + ) -> None: + """Handle game_floor_data event.""" + line_id, floor_data = event.data + + if line_id != self.floor_line_id: + await trio.lowlevel.checkpoint() + return + + self.tiles.clear() + self.tiles.update({Tile(k): v for k, v in floor_data.items()}) + self.update_image() + + await trio.lowlevel.checkpoint() + async def handle_click( self, event: Event[sprite.PygameMouseButtonEventData], @@ -1828,12 +1847,16 @@ async def entry_actions(self) -> None: async def handle_game_initial_config( self, - event: Event[tuple[bool, int, int, int]], + event: Event[tuple[bool, int, int, int, NDArray[int8]]], ) -> None: """Handle `game_initial_config` event.""" - variant_play, player_count, factory_count, self.current_turn = ( - event.data - ) + ( + variant_play, + player_count, + factory_count, + self.current_turn, + floor_line_data, + ) = event.data center = Vector2.from_iter(SCREEN_SIZE) // 2 @@ -1873,7 +1896,7 @@ async def handle_game_initial_config( pattern_rows.set_background(DARKGREEN) self.group_add(pattern_rows) - floor_line = FloorLine(index) + floor_line = FloorLine(index, floor_line_data) floor_line.rect.topleft = pattern_rows.rect.bottomleft self.group_add(floor_line) diff --git a/src/azul/network_shared.py b/src/azul/network_shared.py index 76010f1..65b2ba1 100644 --- a/src/azul/network_shared.py +++ b/src/azul/network_shared.py @@ -45,7 +45,7 @@ Pos: TypeAlias = tuple[u8, u8] -def encode_tile_count(tile_color: int, tile_count: int) -> Buffer: +def encode_tile_count(tile_color: u8, tile_count: u8) -> Buffer: """Return buffer from tile color and count.""" buffer = Buffer() @@ -55,7 +55,7 @@ def encode_tile_count(tile_color: int, tile_count: int) -> Buffer: return buffer -def decode_tile_count(buffer: Buffer) -> tuple[int, int]: +def decode_tile_count(buffer: Buffer) -> tuple[u8, u8]: """Read and return tile color and count from buffer.""" tile_color = buffer.read_value(StructFormat.UBYTE) tile_count = buffer.read_value(StructFormat.UBYTE) @@ -76,9 +76,9 @@ def encode_numeric_uint8_counter(counter: Counter[int]) -> Buffer: return buffer -def decode_numeric_uint8_counter(buffer: Buffer) -> Counter[int]: +def decode_numeric_uint8_counter(buffer: Buffer) -> Counter[u8]: """Read and return uint8 counter from buffer.""" - data: dict[int, int] = {} + data: dict[u8, u8] = {} pair_count = buffer.read_value(StructFormat.UBYTE) for _ in range(pair_count): @@ -140,6 +140,7 @@ class ClientBoundEvents(IntEnum): cursor_movement_mode = auto() current_turn_change = auto() cursor_position = auto() + floor_data = auto() class ServerBoundEvents(IntEnum): diff --git a/src/azul/server.py b/src/azul/server.py index ca18422..214ad94 100755 --- a/src/azul/server.py +++ b/src/azul/server.py @@ -46,6 +46,7 @@ ServerClientNetworkEventComponent, find_ip, ) +from numpy import array, int8 from azul.network_shared import ( ADVERTISEMENT_IP, @@ -59,13 +60,12 @@ encode_numeric_uint8_counter, encode_tile_count, ) -from azul.state import Phase, State, Tile +from azul.state import FLOOR_LINE_DATA, Phase, State, Tile if TYPE_CHECKING: from collections import Counter from collections.abc import Awaitable, Callable - from numpy import int8 from numpy.typing import NDArray @@ -106,6 +106,7 @@ def __init__(self, client_id: int) -> None: "server[write]->cursor_movement_mode": cbe.cursor_movement_mode, "server[write]->current_turn_change": cbe.current_turn_change, "server[write]->cursor_position": cbe.cursor_position, + "server[write]->floor_data": cbe.floor_data, }, ) sbe = ServerBoundEvents @@ -116,6 +117,7 @@ def __init__(self, client_id: int) -> None: sbe.cursor_location: f"client[{self.client_id}]->cursor_location", sbe.pattern_row_clicked: f"client[{self.client_id}]->pattern_row_clicked", sbe.table_clicked: f"client[{self.client_id}]->table_clicked", + sbe.floor_clicked: f"client[{self.client_id}]->floor_clicked", }, ) @@ -129,8 +131,9 @@ def bind_handlers(self) -> None: f"client[{self.client_id}]->cursor_location": self.read_cursor_location, f"client[{self.client_id}]->pattern_row_clicked": self.read_pattern_row_clicked, f"client[{self.client_id}]->table_clicked": self.read_table_clicked, + f"client[{self.client_id}]->floor_clicked": self.read_floor_clicked, f"callback_ping->network[{self.client_id}]": self.handle_callback_ping, - "initial_config->network": self.write_factory_clicked, + "initial_config->network": self.write_initial_config, f"playing_as->network[{self.client_id}]": self.write_playing_as, "game_over->network": self.write_game_over, "board_data->network": self.write_board_data, @@ -141,6 +144,7 @@ def bind_handlers(self) -> None: f"cursor_position->network[{self.client_id}]": self.write_cursor_position, "current_turn_change->network": self.write_current_turn_change, "pattern_data->network": self.write_pattern_data, + "floor_data->network": self.write_floor_data, }, ) @@ -222,6 +226,24 @@ async def read_table_clicked(self, event: Event[bytearray]) -> None: ), ) + async def read_floor_clicked(self, event: Event[bytearray]) -> None: + """Read floor_clicked event from client. Raise as `floor_clicked->server`.""" + buffer = Buffer(event.data) + + floor_line_id = buffer.read_value(StructFormat.UBYTE) + floor_line_pos_x = buffer.read_value(StructFormat.UBYTE) + + await self.raise_event( + Event( + "floor_clicked->server", + ( + self.client_id, + floor_line_id, + floor_line_pos_x, + ), + ), + ) + async def handle_callback_ping( self, _: Event[None], @@ -229,12 +251,14 @@ async def handle_callback_ping( """Reraise as server[write]->callback_ping.""" await self.write_callback_ping() - async def write_factory_clicked( + async def write_initial_config( self, - event: Event[tuple[bool, int, int, int]], + event: Event[tuple[bool, int, int, int, tuple[int, ...]]], ) -> None: """Read initial config event and reraise as server[write]->initial_config.""" - variant_play, player_count, factory_count, current_turn = event.data + variant_play, player_count, factory_count, current_turn, floor_data = ( + event.data + ) buffer = Buffer() @@ -242,6 +266,8 @@ async def write_factory_clicked( buffer.write_value(StructFormat.UBYTE, player_count) buffer.write_value(StructFormat.UBYTE, factory_count) buffer.write_value(StructFormat.UBYTE, current_turn) + buffer.write_value(StructFormat.UBYTE, len(floor_data)) + buffer.extend(encode_int8_array(array(floor_data, dtype=int8))) await self.write_event(Event("server[write]->initial_config", buffer)) @@ -370,6 +396,21 @@ async def write_pattern_data( Event("server[write]->pattern_data", buffer), ) + async def write_floor_data( + self, + event: Event[tuple[int, Counter[int]]], + ) -> None: + """Reraise as server[write]->floor_data.""" + floor_id, floor_line = event.data + + buffer = Buffer() + buffer.write_value(StructFormat.UBYTE, floor_id) + buffer.extend(encode_numeric_uint8_counter(floor_line)) + + await self.write_event( + Event("server[write]->floor_data", buffer), + ) + class ServerPlayer(IntEnum): """Server Player enum.""" @@ -427,6 +468,7 @@ def bind_handlers(self) -> None: "pattern_row_clicked->server": self.handle_client_pattern_row_clicked, "cursor_location->server": self.handle_cursor_location, "table_clicked->server": self.handle_client_table_clicked, + "floor_clicked->server": self.handle_client_floor_clicked, }, ) @@ -579,8 +621,8 @@ async def start_server( # type: ignore[misc] async def transmit_new_round_data(self) -> None: """Transmit all player board data, factory data, and table center data.""" async with trio.open_nursery() as nursery: - # Transmit board data for player_id, player_data in self.state.player_data.items(): + # Transmit board data nursery.start_soon( self.raise_event, Event( @@ -591,6 +633,17 @@ async def transmit_new_round_data(self) -> None: ), ), ) + # Transmit floor line data + nursery.start_soon( + self.raise_event, + Event( + "floor_data->network", + ( + player_id, + player_data.floor, + ), + ), + ) # Transmit factory data for ( factory_id, @@ -606,6 +659,7 @@ async def transmit_new_round_data(self) -> None: ), ), ) + # Transmit table center data await self.raise_event( Event( @@ -700,6 +754,7 @@ async def handle_server_start_new_game(self, event: Event[bool]) -> None: len(self.state.player_data), len(self.state.factory_displays), self.state.current_turn, + FLOOR_LINE_DATA, ), ), ) @@ -729,6 +784,7 @@ async def client_network_loop( while not self.can_start() and not client.not_connected: try: await client.write_callback_ping() + await trio.sleep(1.5) except ( trio.BrokenResourceError, trio.ClosedResourceError, @@ -739,8 +795,8 @@ async def client_network_loop( while not client.not_connected: event: Event[bytearray] | None = None try: - await client.write_callback_ping() - with trio.move_on_after(2): + # await client.write_callback_ping() + with trio.move_on_after(1.5): event = await client.read_event() except network.NetworkTimeoutError: print(f"{client.name} Timeout") @@ -768,6 +824,7 @@ async def client_network_loop( # print(f"{client.name} client_network_loop tick") # print(f"{client.name} {event = }") await client.raise_event(event) + await client.write_callback_ping() def can_start(self) -> bool: """Return if game can start.""" @@ -800,6 +857,7 @@ async def send_spectator_join_packets( len(self.state.player_data), len(self.state.factory_displays), self.state.current_turn, + FLOOR_LINE_DATA, ), ), ) @@ -1158,6 +1216,16 @@ async def handle_client_pattern_row_clicked( ), ) + await self.raise_event( + Event( + "floor_data->network", + ( + player_id, + self.state.player_data[player_id].floor, + ), + ), + ) + if self.state.current_phase != Phase.wall_tiling: raw_tile_color, tile_count = self.state.player_data[ prev_player_turn @@ -1182,15 +1250,15 @@ async def handle_client_pattern_row_clicked( ), ) - if self.state.current_phase == Phase.end: - print("TODO: Handle end of game.") - if self.state.current_phase == Phase.wall_tiling: if not self.state.variant_play: self.state = self.state.apply_auto_wall_tiling() await self.transmit_new_round_data() await self.transmit_pattern_line_data() + if self.state.current_phase == Phase.end: + print("TODO: Handle end of game.") + if self.state.current_turn != player_id: await self.raise_event( Event( @@ -1246,6 +1314,97 @@ async def handle_cursor_location( ), ) + async def handle_client_floor_clicked( + self, + event: Event[tuple[int, int, int]], + ) -> None: + """Handle client clicking floor line.""" + if not self.players_can_interact: + print("Players are not allowed to interact.") + await trio.lowlevel.checkpoint() + return + + client_id, floor_line_id, location_x = event.data + + server_player_id = self.client_players[client_id] + + if server_player_id == ServerPlayer.spectator: + print("Spectator cannot select floor line") + await trio.lowlevel.checkpoint() + return + + player_id = int(server_player_id) + if server_player_id == ServerPlayer.singleplayer_all: + player_id = self.state.current_turn + + if player_id != self.state.current_turn: + print( + f"Player {player_id} (client ID {client_id}) cannot select floor line, not their turn.", + ) + await trio.lowlevel.checkpoint() + return + + if self.state.current_phase != Phase.factory_offer: + print( + f"Player {player_id} (client ID {client_id}) cannot select floor line, not in factory offer phase.", + ) + await trio.lowlevel.checkpoint() + return + + if player_id != floor_line_id: + print( + f"Player {player_id} (client ID {client_id}) cannot select floor line {floor_line_id} that does not belong to them.", + ) + await trio.lowlevel.checkpoint() + return + + if self.state.is_cursor_empty(): + print( + f"Player {player_id} (client ID {client_id}) cannot select floor line when not holding tiles.", + ) + await trio.lowlevel.checkpoint() + return + + color = self.state.get_cursor_holding_color() + + place_count = min(location_x + 1, self.state.cursor_contents[color]) + + self.state = self.state.player_select_floor_line(color, place_count) + + await self.raise_event( + Event( + "cursor_data->network", + self.state.cursor_contents, + ), + ) + + await self.raise_event( + Event( + "floor_data->network", + ( + player_id, + self.state.player_data[player_id].floor, + ), + ), + ) + + if self.state.current_phase == Phase.wall_tiling: + if not self.state.variant_play: + self.state = self.state.apply_auto_wall_tiling() + await self.transmit_new_round_data() + await self.transmit_pattern_line_data() + + if self.state.current_phase == Phase.end: + print("TODO: Handle end of game.") + + if self.state.current_turn != player_id: + await self.raise_event( + Event( + "current_turn_change->network", + self.state.current_turn, + ), + ) + def __del__(self) -> None: """Debug print.""" print(f"del {self.__class__.__name__}") diff --git a/src/azul/state.py b/src/azul/state.py index 09f0176..c801135 100644 --- a/src/azul/state.py +++ b/src/azul/state.py @@ -569,7 +569,7 @@ def manual_wall_tiling_action( ) def finish_manual_wall_tiling(self) -> tuple[Self, Counter[int], bool]: - """Return new player data and tiles for box lid after performing automatic wall tiling.""" + """Return new player data and tiles for box lid after performing manual wall tiling.""" for_box_lid: Counter[int] = Counter() score = self.score From 4cc0d65b9dabb90c047e4eeb1723df187e2648ba Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Mon, 23 Dec 2024 16:01:15 -0600 Subject: [PATCH 30/58] Update pre-commit hooks --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d965914..b644634 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.3 + rev: v0.8.4 hooks: - id: ruff types: [file] @@ -43,14 +43,14 @@ repos: hooks: - id: typos - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v0.9.2 + rev: v0.10.0 hooks: - id: zizmor - repo: local hooks: - id: project-requirements name: regenerate requirements.in - language: system - entry: python tools/project_requirements.py + language: python + entry: tools/project_requirements.py pass_filenames: false files: ^(test-requirements.in)|(pyproject.toml)$ From bc8cdca31716ca31b4d02d485c471aea55a995b4 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Mon, 30 Dec 2024 12:50:45 -0600 Subject: [PATCH 31/58] Update dependencies & project --- .github/workflows/autodeps.yml | 4 +- .github/workflows/ci.yml | 174 +++++++++++++++++---------------- .pre-commit-config.yaml | 2 +- pyproject.toml | 4 +- test-requirements.in | 6 +- test-requirements.txt | 14 +-- 6 files changed, 103 insertions(+), 101 deletions(-) diff --git a/.github/workflows/autodeps.yml b/.github/workflows/autodeps.yml index 5918fd9..7a08a2b 100644 --- a/.github/workflows/autodeps.yml +++ b/.github/workflows/autodeps.yml @@ -39,8 +39,8 @@ jobs: run: python -m pip install -r test-requirements.txt # apply newer versions' formatting - - name: Black - run: black src/azul + - name: Pre-commit updates + run: pre-commit run -a - name: uv run: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6bbbe7..24e4a67 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,52 +11,52 @@ concurrency: cancel-in-progress: true jobs: -## Windows: -## name: 'Windows (${{ matrix.python }}, ${{ matrix.arch }}${{ matrix.extra_name }})' -## timeout-minutes: 20 -## runs-on: 'windows-latest' -## strategy: -## fail-fast: false -## matrix: -## python: ['3.10', '3.11', '3.12'] -## arch: ['x86', 'x64'] -## continue-on-error: >- -## ${{ -## ( -## endsWith(matrix.python, '-dev') -## || endsWith(matrix.python, '-nightly') -## ) -## && true -## || false -## }} -## steps: -## - name: Checkout -## uses: actions/checkout@v4 -## - name: Setup python -## uses: actions/setup-python@v5 -## with: -## # This allows the matrix to specify just the major.minor version while still -## # expanding it to get the latest patch version including alpha releases. -## # This avoids the need to update for each new alpha, beta, release candidate, -## # and then finally an actual release version. actions/setup-python doesn't -## # support this for PyPy presently so we get no help there. -## # -## # 'CPython' -> '3.9.0-alpha - 3.9.X' -## # 'PyPy' -> 'pypy-3.9' -## python-version: ${{ fromJSON(format('["{0}", "{1}"]', format('{0}.0-alpha - {0}.X', matrix.python), matrix.python))[startsWith(matrix.python, 'pypy')] }} -## architecture: '${{ matrix.arch }}' -## cache: pip -## cache-dependency-path: test-requirements.txt -## - name: Run tests -## run: ./ci.sh -## shell: bash + Windows: + name: 'Windows (${{ matrix.python }}, ${{ matrix.arch }}${{ matrix.extra_name }})' + timeout-minutes: 20 + runs-on: 'windows-latest' + strategy: + fail-fast: false + matrix: + python: ['3.10', '3.11', '3.12'] + arch: ['x86', 'x64'] + continue-on-error: >- + ${{ + ( + endsWith(matrix.python, '-dev') + || endsWith(matrix.python, '-nightly') + ) + && true + || false + }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Setup python + uses: actions/setup-python@v5 + with: + # This allows the matrix to specify just the major.minor version while still + # expanding it to get the latest patch version including alpha releases. + # This avoids the need to update for each new alpha, beta, release candidate, + # and then finally an actual release version. actions/setup-python doesn't + # support this for PyPy presently so we get no help there. + # + # 'CPython' -> '3.9.0-alpha - 3.9.X' + # 'PyPy' -> 'pypy-3.9' + python-version: ${{ fromJSON(format('["{0}", "{1}"]', format('{0}.0-alpha - {0}.X', matrix.python), matrix.python))[startsWith(matrix.python, 'pypy')] }} + architecture: '${{ matrix.arch }}' + cache: pip + cache-dependency-path: test-requirements.txt + - name: Run tests + run: ./ci.sh + shell: bash Ubuntu: name: 'Ubuntu (${{ matrix.python }}${{ matrix.extra_name }})' timeout-minutes: 10 runs-on: 'ubuntu-latest' - # Only run for PRs or pushes to main - if: github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main') strategy: fail-fast: false matrix: @@ -98,49 +98,51 @@ jobs: env: CHECK_FORMATTING: '${{ matrix.check_formatting }}' -## macOS: -## name: 'macOS (${{ matrix.python }})' -## timeout-minutes: 15 -## runs-on: 'macos-latest' -## strategy: -## fail-fast: false -## matrix: -## python: ['3.10', '3.11', '3.12'] -## continue-on-error: >- -## ${{ -## ( -## endsWith(matrix.python, '-dev') -## || endsWith(matrix.python, '-nightly') -## ) -## && true -## || false -## }} -## steps: -## - name: Checkout -## uses: actions/checkout@v4 -## - name: Setup python -## uses: actions/setup-python@v5 -## with: -## python-version: ${{ fromJSON(format('["{0}", "{1}"]', format('{0}.0-alpha - {0}.X', matrix.python), matrix.python))[startsWith(matrix.python, 'pypy')] }} -## cache: pip -## cache-dependency-path: test-requirements.txt -## - name: Run tests -## run: ./ci.sh + macOS: + name: 'macOS (${{ matrix.python }})' + timeout-minutes: 15 + runs-on: 'macos-latest' + strategy: + fail-fast: false + matrix: + python: ['3.10', '3.11', '3.12'] + continue-on-error: >- + ${{ + ( + endsWith(matrix.python, '-dev') + || endsWith(matrix.python, '-nightly') + ) + && true + || false + }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Setup python + uses: actions/setup-python@v5 + with: + python-version: ${{ fromJSON(format('["{0}", "{1}"]', format('{0}.0-alpha - {0}.X', matrix.python), matrix.python))[startsWith(matrix.python, 'pypy')] }} + cache: pip + cache-dependency-path: test-requirements.txt + - name: Run tests + run: ./ci.sh + + # https://github.com/marketplace/actions/alls-green#why + check: # This job does nothing and is only used for the branch protection + + if: always() + + needs: + - Windows + - Ubuntu + - macOS -## # https://github.com/marketplace/actions/alls-green#why -## check: # This job does nothing and is only used for the branch protection -## -## if: always() -## -## needs: -## - Windows -## - Ubuntu -## - macOS -## -## runs-on: ubuntu-latest -## -## steps: -## - name: Decide whether the needed jobs succeeded or failed -## uses: re-actors/alls-green@release/v1 -## with: -## jobs: ${{ toJSON(needs) }} + runs-on: ubuntu-latest + + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b644634..c8d04bc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ ci: autofix_prs: true autoupdate_schedule: quarterly submodules: false - skip: [badgie, project-requirements] + skip: [badgie] repos: - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/pyproject.toml b/pyproject.toml index 4012fde..3ddaa43 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,11 +36,11 @@ keywords = [ "ai", "multi-player", "azul", "ai-support", "networked-game" ] dependencies = [ - "libcomponent @ git+https://github.com/CoolCat467/LibComponent", + "libcomponent~=0.0.1", "pygame~=2.6.0", "typing_extensions>=4.12.2", "mypy_extensions>=1.0.0", - "trio~=0.27.0", + "trio~=0.28.0", "exceptiongroup; python_version < '3.11'", "numpy~=2.1.3", ] diff --git a/test-requirements.in b/test-requirements.in index a9dc141..1e6d53e 100644 --- a/test-requirements.in +++ b/test-requirements.in @@ -6,7 +6,7 @@ pytest-cov # Tools black; implementation_name == "cpython" -mypy >= 1.13.0 # Would use mypy[faster-cache], but orjson has build issues on pypy +mypy >= 1.14.0 # Would use mypy[faster-cache], but orjson has build issues on pypy orjson; implementation_name == "cpython" ruff >= 0.6.6 uv >= 0.2.24 @@ -19,10 +19,10 @@ typing-extensions # Azul's own dependencies # exceptiongroup; python_version < '3.11' -libcomponent @ git+https://github.com/CoolCat467/LibComponent +libcomponent~=0.0.1 mypy_extensions>=1.0.0 numpy~=2.1.3 pygame~=2.6.0 -trio~=0.27.0 +trio~=0.28.0 typing_extensions>=4.12.2 # diff --git a/test-requirements.txt b/test-requirements.txt index 71fa0ea..dd75553 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -14,11 +14,11 @@ click==8.1.8 ; implementation_name == 'cpython' # via black codespell==2.3.0 # via -r test-requirements.in -colorama==0.4.6 ; (implementation_name == 'cpython' and platform_system == 'Windows') or sys_platform == 'win32' +colorama==0.4.6 ; sys_platform == 'win32' # via # click # pytest -coverage==7.6.9 +coverage==7.6.10 # via # -r test-requirements.in # pytest-cov @@ -33,9 +33,9 @@ idna==3.10 # via trio iniconfig==2.0.0 # via pytest -libcomponent @ git+https://github.com/CoolCat467/LibComponent@91362869af19943520430fbed9edc38fc473ac9e +libcomponent==0.0.1 # via -r test-requirements.in -mypy==1.14.0 +mypy==1.14.1 # via -r test-requirements.in mypy-extensions==1.0.0 # via @@ -45,7 +45,7 @@ mypy-extensions==1.0.0 # mypy numpy==2.1.3 # via -r test-requirements.in -orjson==3.10.12 ; implementation_name == 'cpython' +orjson==3.10.13 ; implementation_name == 'cpython' # via -r test-requirements.in outcome==1.3.0.post0 # via @@ -86,7 +86,7 @@ tomli==2.2.1 ; python_full_version <= '3.11' # coverage # mypy # pytest -trio==0.27.0 +trio==0.28.0 # via # -r test-requirements.in # libcomponent @@ -97,5 +97,5 @@ typing-extensions==4.12.2 # black # libcomponent # mypy -uv==0.5.11 +uv==0.5.13 # via -r test-requirements.in From 30aa3420c21bc2984817a68fc3bb7b1f0aa3680a Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sat, 4 Jan 2025 04:44:18 -0600 Subject: [PATCH 32/58] Work on getting machine client working --- computer_players/MiniMax_AI.py | 83 ++++----- computer_players/machine_client.py | 276 +++++++++++++++++++++++------ computer_players/minimax.py | 11 +- pyproject.toml | 2 +- 4 files changed, 278 insertions(+), 94 deletions(-) diff --git a/computer_players/MiniMax_AI.py b/computer_players/MiniMax_AI.py index bbc0865..3da2d9c 100755 --- a/computer_players/MiniMax_AI.py +++ b/computer_players/MiniMax_AI.py @@ -13,7 +13,7 @@ from typing import TYPE_CHECKING, TypeAlias, TypeVar -##from machine_client import RemoteState, run_clients_in_local_servers_sync +from machine_client import RemoteState, run_clients_in_local_servers_sync from minimax import Minimax, MinimaxResult, Player from azul.state import ( @@ -37,13 +37,13 @@ # 1 = True = AI (Us) = MAX = 1, 3 -class AzulMinimax(Minimax[State, Action]): +class AzulMinimax(Minimax[tuple[State, int], Action]): """Minimax Algorithm for Checkers.""" __slots__ = () @staticmethod - def value(state: State) -> int | float: + def value(state: tuple[State, int]) -> int | float: """Return value of given game state.""" # Real real_state, max_player = state @@ -68,13 +68,13 @@ def value(state: State) -> int | float: return (max_ - min_) / (abs(max_) + abs(min_) + 1) @staticmethod - def terminal(state: State) -> bool: + def terminal(state: tuple[State, int]) -> bool: """Return if game state is terminal.""" real_state, _max_player = state return real_state.current_phase == Phase.end @staticmethod - def player(state: State) -> Player: + def player(state: tuple[State, int]) -> Player: """Return Player enum from current state's turn.""" real_state, max_player = state return ( @@ -82,62 +82,65 @@ def player(state: State) -> Player: ) @staticmethod - def actions(state: State) -> Iterable[Action]: + def actions(state: tuple[State, int]) -> Iterable[Action]: """Return all actions that are able to be performed for the current player in the given state.""" real_state, _max_player = state return tuple(real_state.yield_actions()) ## print(f'{len(actions) = }') @staticmethod - def result(state: State, action: Action) -> State: + def result(state: tuple[State, int], action: Action) -> tuple[State, int]: """Return new state after performing given action on given current state.""" real_state, max_player = state return (real_state.preform_action(action), max_player) @classmethod - def adaptive_depth_minimax(cls, state: State) -> MinimaxResult[Action]: + def adaptive_depth_minimax( + cls, + state: tuple[State, int], + ) -> MinimaxResult[Action]: """Adaptive depth minimax.""" # TODO depth = 1 return cls.alphabeta(state, depth) -##class MinimaxPlayer(RemoteState): -## """Minimax Player.""" -## -## __slots__ = () -## -## async def preform_turn(self) -> Action: -## """Perform turn.""" -## print("preform_turn") -## ##value, action = CheckersMinimax.adaptive_depth_minimax( -## ## self.state, 4, 5 -## ##) -## ##value, action = CheckersMinimax.minimax(self.state, 4) -## value, action = CheckersMinimax.alphabeta(self.state, 4) -## if action is None: -## raise ValueError("action is None") -## print(f"{value = }") -## return action +class MinimaxPlayer(RemoteState): + """Minimax Player.""" + __slots__ = () -def run() -> None: - """Run MinimaxPlayer clients in local server.""" - import random - - random.seed(0) - - state = (State.new_game(2), 0) - - while not AzulMinimax.terminal(state): - action = AzulMinimax.adaptive_depth_minimax(state) - print(f"{action = }") - state = AzulMinimax.result(state, action.action) - print(f"{state = }") - print(state) + async def preform_turn(self) -> Action: + """Perform turn.""" + print("preform_turn") + ##value, action = CheckersMinimax.adaptive_depth_minimax( + ## self.state, 4, 5 + ##) + ##value, action = CheckersMinimax.minimax(self.state, 4) + value, action = AzulMinimax.alphabeta((self.state, self.playing_as), 1) + ## value, action = AzulMinimax.alphabeta((self.state, self.playing_as), 4) + if action is None: + raise ValueError("action is None") + print(f"{value = }") + return action -## run_clients_in_local_servers_sync(MinimaxPlayer) +def run() -> None: + """Run MinimaxPlayer clients in local server.""" + ## import random + ## + ## random.seed(0) + ## + ## state = (State.new_game(2), 0) + ## + ## while not AzulMinimax.terminal(state): + ## action = AzulMinimax.adaptive_depth_minimax(state) + ## print(f"{action = }") + ## state = AzulMinimax.result(state, action.action) + ## print(f"{state = }") + ## print(state) + + run_clients_in_local_servers_sync(MinimaxPlayer) if __name__ == "__main__": diff --git a/computer_players/machine_client.py b/computer_players/machine_client.py index 78254ce..8b85f39 100644 --- a/computer_players/machine_client.py +++ b/computer_players/machine_client.py @@ -9,21 +9,39 @@ import sys from abc import ABCMeta, abstractmethod from contextlib import asynccontextmanager -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, TypeAlias import trio -from checkers.client import GameClient, read_advertisements -from checkers.component import ( +from libcomponent.component import ( Component, ComponentManager, Event, ExternalRaiseManager, ) -from checkers.state import Action, Pos, State + +from azul.client import GameClient, read_advertisements +from azul.state import ( + PatternLine, + Phase, + SelectableDestination, + SelectableDestinationTiles, + SelectableSource, + SelectableSourceTiles, + State, + Tile, + factory_displays_deepcopy, + player_data_deepcopy, +) if TYPE_CHECKING: + from collections import Counter from collections.abc import AsyncGenerator + from mypy_extensions import u8 + from numpy import int8 + from numpy.typing import NDArray + + if sys.version_info < (3, 11): from exceptiongroup import BaseExceptionGroup @@ -31,6 +49,12 @@ # 0 = False = Person = MIN = 0, 2 # 1 = True = AI (Us) = MAX = 1, 3 +##Action: TypeAlias = tuple[SelectableSourceTiles, SelectableDestinationTiles] +Action: TypeAlias = ( + tuple[SelectableDestinationTiles, ...] + | tuple[SelectableSourceTiles, tuple[SelectableDestinationTiles, ...]] +) + class RemoteState(Component, metaclass=ABCMeta): """Remote State. @@ -39,43 +63,84 @@ class RemoteState(Component, metaclass=ABCMeta): turn. """ - __slots__ = ("has_initial", "moves", "pieces", "playing_as", "state") + __slots__ = ("has_initial", "moves", "playing_as", "state") def __init__(self) -> None: """Initialize remote state.""" super().__init__("remote_state") - self.state = State((8, 8), {}) + self.state = State.blank() self.has_initial = False - self.pieces: dict[Pos, int] = {} - self.playing_as = 1 + self.playing_as: u8 = 1 self.moves = 0 def bind_handlers(self) -> None: """Register game event handlers.""" self.register_handlers( { - "game_action_complete": self.handle_action_complete, "game_winner": self.handle_game_over, "game_initial_config": self.handle_initial_config, "game_playing_as": self.handle_playing_as, - "gameboard_create_piece": self.handle_create_piece, + "game_board_data": self.handle_board_data, + "game_pattern_data": self.handle_pattern_data, + "game_factory_data": self.handle_factory_data, + # "game_cursor_data": + "game_table_data": self.handle_table_data, + # "game_cursor_set_movement_mode": + "game_pattern_current_turn_change": self.handle_pattern_current_turn_change, + # "game_cursor_set_destination": + "game_floor_data": self.handle_floor_data, }, ) + async def apply_select_source( + self, + selection: SelectableSourceTiles, + ) -> None: + """Select source.""" + color = selection.tiles + raise NotImplementedError(selection.source) + if selection.source == SelectableSource.table_center: # type: ignore[unreachable] + return self.cursor_selects_table_center(color) + if selection.source == SelectableSource.factory: + assert selection.source_id is not None + return self.cursor_selects_factory(selection.source_id, color) + raise NotImplementedError(selection.source) + + async def apply_select_destination( + self, + selection: SelectableDestinationTiles, + ) -> None: + """Select destination.""" + assert self.state.current_phase == Phase.factory_offer + assert not self.state.is_cursor_empty() + + raise NotImplementedError(selection.destination) + if selection.destination == SelectableDestination.floor_line: # type: ignore[unreachable] + color = self.state.get_cursor_holding_color() + return self.player_select_floor_line( + color, + selection.place_count, + ) + if selection.destination == SelectableDestination.pattern_line: + assert selection.destination_id is not None + return self.state.player_selects_pattern_line( + selection.destination_id, + selection.place_count, + ) + raise NotImplementedError(selection.destination) + async def preform_action(self, action: Action) -> None: """Raise events to perform game action.""" - await self.raise_event( - Event( - "gameboard_piece_clicked", - ( - action.from_pos, - self.state.pieces[action.from_pos], - ), - ), - ) - await self.raise_event(Event("gameboard_tile_clicked", action.to_pos)) + source, dest = action + assert isinstance(source, SelectableSourceTiles) + + await self.apply_select_source(source) + destination = dest[0] + assert isinstance(destination, SelectableDestinationTiles) + await self.apply_select_destination(destination) + raise NotImplementedError(f"{source = } {dest = }") @abstractmethod async def preform_turn(self) -> Action: @@ -84,54 +149,165 @@ async def preform_turn(self) -> Action: async def base_preform_turn(self) -> None: """Perform turn.""" self.moves += 1 - winner = self.state.check_for_win() - if winner is not None: + ## winner = self.state.check_for_win() + ## if winner is not None: + if self.state.current_phase == Phase.end: print("Terminal state, not performing turn") - value = ("Lost", "Won")[winner == self.playing_as] + ##value = ("Lost", "Won")[winner == self.playing_as] + value = "" print(f"{value} after {self.moves}") + await trio.lowlevel.checkpoint() return + print(f"Move {self.moves}...") action = await self.preform_turn() await self.preform_action(action) - async def handle_action_complete( - self, - event: Event[tuple[Pos, Pos, int]], - ) -> None: - """Perform action on internal state and perform our turn if possible.""" - from_pos, to_pos, turn = event.data - action = self.state.action_from_points(from_pos, to_pos) - self.state = self.state.preform_action(action) - ## print(f'{turn = }') - if turn == self.playing_as: - await self.base_preform_turn() + async def handle_playing_as(self, event: Event[u8]) -> None: + """Handle client playing as specified player event.""" + ## print("handle_playing_as") + self.playing_as = event.data - async def handle_create_piece(self, event: Event[tuple[Pos, int]]) -> None: - """Update internal pieces if we haven't had the initial setup event.""" - if self.has_initial: + if self.state.current_turn == self.playing_as: + await self.base_preform_turn() return - pos, type_ = event.data - self.pieces[pos] = type_ - - async def handle_playing_as(self, event: Event[int]) -> None: - """Handle playing as event.""" - self.playing_as = event.data + await trio.lowlevel.checkpoint() async def handle_initial_config( self, - event: Event[tuple[Pos, int]], + event: Event[tuple[u8, u8, u8, u8, NDArray[int8]]], ) -> None: - """Set up initial state and perform our turn if possible.""" - board_size, turn = event.data - self.state = State(board_size, self.pieces, bool(turn)) + """Set up initial game state.""" + ## print("handle_initial_config") + ( + variant_play, + player_count, + factory_count, + current_turn, + floor_line_data, + ) = event.data + self.state = State.new_game(player_count, bool(variant_play)) + self.state = self.state._replace(current_turn=current_turn) self.has_initial = True - if turn == self.playing_as: - await self.base_preform_turn() + ##if current_turn == self.playing_as: + ## await self.base_preform_turn() - async def handle_game_over(self, event: Event[int]) -> None: + async def handle_game_over(self, event: Event[u8]) -> None: """Raise network_stop event so we disconnect from server.""" + ## print("handle_game_over") self.has_initial = False await self.raise_event(Event("network_stop", None)) + async def handle_board_data( + self, + event: Event[tuple[u8, NDArray[int8]]], + ) -> None: + """Handle player board data update.""" + ## print("handle_board_data") + player_id, board_data = event.data + + current_player_data = self.state.player_data[player_id] + + new_player_data = current_player_data._replace(wall=board_data) + + player_data = player_data_deepcopy(self.state.player_data) + player_data[player_id] = new_player_data + + self.state = self.state._replace( + player_data=player_data, + ) + await trio.lowlevel.checkpoint() + + async def handle_pattern_data( + self, + event: Event[tuple[u8, u8, tuple[u8, u8]]], + ) -> None: + """Handle player pattern line data update.""" + ## print("handle_pattern_data") + player_id, row_id, (tile_color, tile_count) = event.data + + current_player_data = self.state.player_data[player_id] + + new_player_data = current_player_data._replace( + lines=current_player_data.replace_pattern_line( + current_player_data.lines, + row_id, + PatternLine(Tile(tile_color), int(tile_count)), + ), + ) + + player_data = player_data_deepcopy(self.state.player_data) + player_data[player_id] = new_player_data + + self.state = self.state._replace( + player_data=player_data, + ) + await trio.lowlevel.checkpoint() + + async def handle_factory_data( + self, + event: Event[tuple[u8, Counter[u8]]], + ) -> None: + """Handle factory data update.""" + ## print("handle_factory_data") + factory_id, tiles = event.data + + factory_displays = factory_displays_deepcopy( + self.state.factory_displays, + ) + factory_displays[factory_id] = tiles + + self.state = self.state._replace( + factory_displays=factory_displays, + ) + await trio.lowlevel.checkpoint() + + async def handle_table_data(self, event: Event[Counter[u8]]) -> None: + """Handle table center tile data update.""" + ## print("handle_table_data") + table_center = event.data + + self.state = self.state._replace( + table_center=table_center, + ) + await trio.lowlevel.checkpoint() + + async def handle_pattern_current_turn_change( + self, + event: Event[u8], + ) -> None: + """Handle change of current turn.""" + ## print("handle_pattern_current_turn_change") + pattern_id = event.data + + self.state = self.state._replace( + current_turn=pattern_id, + ) + + if self.state.current_turn == self.playing_as: + await self.base_preform_turn() + return + await trio.lowlevel.checkpoint() + + async def handle_floor_data( + self, + event: Event[tuple[u8, Counter[u8]]], + ) -> None: + """Handle floor data event.""" + ## print("handle_floor_data") + floor_id, floor_line = event.data + + current_player_data = self.state.player_data[floor_id] + + new_player_data = current_player_data._replace(floor=floor_line) + + player_data = player_data_deepcopy(self.state.player_data) + player_data[floor_id] = new_player_data + + self.state = self.state._replace( + player_data=player_data, + ) + await trio.lowlevel.checkpoint() + class MachineClient(ComponentManager): """Manager that runs until client_disconnected event fires.""" diff --git a/computer_players/minimax.py b/computer_players/minimax.py index 0a4600a..10f9568 100644 --- a/computer_players/minimax.py +++ b/computer_players/minimax.py @@ -11,9 +11,10 @@ import operator import random from abc import ABC, abstractmethod +from collections.abc import Callable from enum import IntEnum, auto from math import inf as infinity -from typing import TYPE_CHECKING, Generic, NamedTuple, TypeVar +from typing import TYPE_CHECKING, Generic, NamedTuple, TypeVar, cast if TYPE_CHECKING: from collections.abc import Iterable @@ -107,6 +108,7 @@ def minimax( current_player = cls.player(state) value: int | float + best: Callable[[float, float], float] if current_player == Player.MAX: value = -infinity best = max @@ -115,7 +117,7 @@ def minimax( best = min elif current_player == Player.CHANCE: value = 0 - best = sum + best = cast(Callable[[float, float], float], sum) else: raise ValueError(f"Unexpected player type {current_player!r}") @@ -156,6 +158,9 @@ def alphabeta( current_player = cls.player(state) value: int | float + best: Callable[[float, float], float] + compare = operator.gt + set_idx = 0 if current_player == Player.MAX: value = -infinity best = max @@ -168,7 +173,7 @@ def alphabeta( set_idx = 1 elif current_player == Player.CHANCE: value = 0 - best = sum + best = cast(Callable[[float, float], float], sum) else: raise ValueError(f"Unexpected player type {current_player!r}") diff --git a/pyproject.toml b/pyproject.toml index 3ddaa43..8e4ece1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ azul = ["py.typed", "data/*"] [tool.mypy] plugins = ["numpy.typing.mypy_plugin"] -files = ["src/azul/",] +files = ["src/azul/", "computer_players"] check_untyped_defs = true disallow_any_decorated = true disallow_any_generics = true From 4f19a8cefa847251debc3ff969bc837e0a15f2ae Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Tue, 7 Jan 2025 16:22:51 -0600 Subject: [PATCH 33/58] More work on minimax AI --- computer_players/MiniMax_AI.py | 67 +++++++++++-- computer_players/machine_client.py | 147 ++++++++++++++++++++++------- src/azul/game.py | 6 +- src/azul/server.py | 8 +- src/azul/state.py | 27 +++--- 5 files changed, 196 insertions(+), 59 deletions(-) diff --git a/computer_players/MiniMax_AI.py b/computer_players/MiniMax_AI.py index 3da2d9c..2e290a8 100755 --- a/computer_players/MiniMax_AI.py +++ b/computer_players/MiniMax_AI.py @@ -11,10 +11,12 @@ __author__ = "CoolCat467" __version__ = "0.0.0" +from math import inf as infinity from typing import TYPE_CHECKING, TypeAlias, TypeVar from machine_client import RemoteState, run_clients_in_local_servers_sync from minimax import Minimax, MinimaxResult, Player +from mypy_extensions import u8 from azul.state import ( Phase, @@ -26,6 +28,8 @@ if TYPE_CHECKING: from collections.abc import Iterable + from typing_extensions import Self + T = TypeVar("T") Action: TypeAlias = ( tuple[SelectableDestinationTiles, ...] @@ -37,13 +41,30 @@ # 1 = True = AI (Us) = MAX = 1, 3 -class AzulMinimax(Minimax[tuple[State, int], Action]): +class AutoWallState(State): + """Azul State with automatic wall tiling in regular play mode.""" + + __slots__ = () + + def _factory_offer_maybe_next_turn(self) -> Self: + """Return either current state or new state if player's turn is over.""" + new_state = super()._factory_offer_maybe_next_turn() + + if ( + new_state.current_phase == Phase.wall_tiling + and not new_state.variant_play + ): + return new_state.apply_auto_wall_tiling() + return new_state + + +class AzulMinimax(Minimax[tuple[AutoWallState, u8], Action]): """Minimax Algorithm for Checkers.""" __slots__ = () @staticmethod - def value(state: tuple[State, int]) -> int | float: + def value(state: tuple[AutoWallState, u8]) -> int | float: """Return value of given game state.""" # Real real_state, max_player = state @@ -68,13 +89,13 @@ def value(state: tuple[State, int]) -> int | float: return (max_ - min_) / (abs(max_) + abs(min_) + 1) @staticmethod - def terminal(state: tuple[State, int]) -> bool: + def terminal(state: tuple[AutoWallState, u8]) -> bool: """Return if game state is terminal.""" real_state, _max_player = state return real_state.current_phase == Phase.end @staticmethod - def player(state: tuple[State, int]) -> Player: + def player(state: tuple[AutoWallState, u8]) -> Player: """Return Player enum from current state's turn.""" real_state, max_player = state return ( @@ -82,14 +103,17 @@ def player(state: tuple[State, int]) -> Player: ) @staticmethod - def actions(state: tuple[State, int]) -> Iterable[Action]: + def actions(state: tuple[AutoWallState, u8]) -> Iterable[Action]: """Return all actions that are able to be performed for the current player in the given state.""" real_state, _max_player = state return tuple(real_state.yield_actions()) ## print(f'{len(actions) = }') @staticmethod - def result(state: tuple[State, int], action: Action) -> tuple[State, int]: + def result( + state: tuple[AutoWallState, u8], + action: Action, + ) -> tuple[AutoWallState, u8]: """Return new state after performing given action on given current state.""" real_state, max_player = state return (real_state.preform_action(action), max_player) @@ -97,19 +121,43 @@ def result(state: tuple[State, int], action: Action) -> tuple[State, int]: @classmethod def adaptive_depth_minimax( cls, - state: tuple[State, int], + state: tuple[AutoWallState, u8], ) -> MinimaxResult[Action]: """Adaptive depth minimax.""" # TODO depth = 1 return cls.alphabeta(state, depth) + @classmethod + def alphabeta( + cls, + state: tuple[AutoWallState, u8], + depth: int | None = 5, + a: int | float = -infinity, + b: int | float = infinity, + ) -> MinimaxResult[ + tuple[SelectableDestinationTiles, ...] + | tuple[SelectableSourceTiles, tuple[SelectableDestinationTiles, ...]] + ]: + """Return minimax alphabeta pruning result best action for given current state.""" + new_state, player = state + if ( + new_state.current_phase == Phase.wall_tiling + and not new_state.variant_play + ): + new_state = new_state.apply_auto_wall_tiling() + return super().alphabeta((new_state, player), depth, a, b) + class MinimaxPlayer(RemoteState): """Minimax Player.""" __slots__ = () + def __init__(self) -> None: + """Initialize remote minmax player state.""" + super().__init__(state_class=AutoWallState) + async def preform_turn(self) -> Action: """Perform turn.""" print("preform_turn") @@ -117,7 +165,8 @@ async def preform_turn(self) -> Action: ## self.state, 4, 5 ##) ##value, action = CheckersMinimax.minimax(self.state, 4) - value, action = AzulMinimax.alphabeta((self.state, self.playing_as), 1) + assert isinstance(self.state, AutoWallState) + value, action = AzulMinimax.alphabeta((self.state, self.playing_as), 2) ## value, action = AzulMinimax.alphabeta((self.state, self.playing_as), 4) if action is None: raise ValueError("action is None") @@ -131,7 +180,7 @@ def run() -> None: ## ## random.seed(0) ## - ## state = (State.new_game(2), 0) + ## state = (AutoWallState.new_game(2), 0) ## ## while not AzulMinimax.terminal(state): ## action = AzulMinimax.adaptive_depth_minimax(state) diff --git a/computer_players/machine_client.py b/computer_players/machine_client.py index 8b85f39..5a616f1 100644 --- a/computer_players/machine_client.py +++ b/computer_players/machine_client.py @@ -9,7 +9,7 @@ import sys from abc import ABCMeta, abstractmethod from contextlib import asynccontextmanager -from typing import TYPE_CHECKING, TypeAlias +from typing import TYPE_CHECKING, TypeAlias, cast import trio from libcomponent.component import ( @@ -32,6 +32,7 @@ factory_displays_deepcopy, player_data_deepcopy, ) +from azul.vector import Vector2 if TYPE_CHECKING: from collections import Counter @@ -63,18 +64,29 @@ class RemoteState(Component, metaclass=ABCMeta): turn. """ - __slots__ = ("has_initial", "moves", "playing_as", "state") + __slots__ = ( + "can_made_play", + "has_initial", + "moves", + "playing_as", + "playing_lock", + "state", + ) - def __init__(self) -> None: + def __init__(self, state_class: type[State] = State) -> None: """Initialize remote state.""" super().__init__("remote_state") - self.state = State.blank() + ## print(f'[RemoteState] {state_class = }') + self.state = state_class.blank() self.has_initial = False self.playing_as: u8 = 1 self.moves = 0 + self.playing_lock = trio.Lock() + self.can_made_play = True + def bind_handlers(self) -> None: """Register game event handlers.""" self.register_handlers( @@ -85,7 +97,7 @@ def bind_handlers(self) -> None: "game_board_data": self.handle_board_data, "game_pattern_data": self.handle_pattern_data, "game_factory_data": self.handle_factory_data, - # "game_cursor_data": + "game_cursor_data": self.handle_cursor_data, "game_table_data": self.handle_table_data, # "game_cursor_set_movement_mode": "game_pattern_current_turn_change": self.handle_pattern_current_turn_change, @@ -99,14 +111,17 @@ async def apply_select_source( selection: SelectableSourceTiles, ) -> None: """Select source.""" + ## print(f"select {selection = }") color = selection.tiles - raise NotImplementedError(selection.source) - if selection.source == SelectableSource.table_center: # type: ignore[unreachable] - return self.cursor_selects_table_center(color) - if selection.source == SelectableSource.factory: + if selection.source == SelectableSource.table_center: + await self.raise_event(Event("game_table_clicked", color)) + elif selection.source == SelectableSource.factory: assert selection.source_id is not None - return self.cursor_selects_factory(selection.source_id, color) - raise NotImplementedError(selection.source) + await self.raise_event( + Event("game_factory_clicked", (selection.source_id, color)), + ) + else: + raise NotImplementedError(selection.source) async def apply_select_destination( self, @@ -114,33 +129,68 @@ async def apply_select_destination( ) -> None: """Select destination.""" assert self.state.current_phase == Phase.factory_offer - assert not self.state.is_cursor_empty() - - raise NotImplementedError(selection.destination) - if selection.destination == SelectableDestination.floor_line: # type: ignore[unreachable] - color = self.state.get_cursor_holding_color() - return self.player_select_floor_line( - color, - selection.place_count, + ##assert not self.state.is_cursor_empty() + ## print(f'dest {selection = }') + + if selection.destination == SelectableDestination.floor_line: + await self.raise_event( + Event( + "game_floor_clicked", + (self.playing_as, selection.place_count), + ), ) - if selection.destination == SelectableDestination.pattern_line: + elif selection.destination == SelectableDestination.pattern_line: assert selection.destination_id is not None - return self.state.player_selects_pattern_line( - selection.destination_id, - selection.place_count, + line_id = selection.destination_id + currently_placed = self.state.get_player_line_current_place_count( + line_id, ) - raise NotImplementedError(selection.destination) + await self.raise_event( + Event( + "game_pattern_row_clicked", + ( + self.playing_as, + Vector2( + 5 - selection.place_count - currently_placed, + line_id, + ), + ), + ), + ) + else: + raise NotImplementedError(selection.destination) async def preform_action(self, action: Action) -> None: """Raise events to perform game action.""" - source, dest = action - assert isinstance(source, SelectableSourceTiles) + await self.raise_event( + Event( + "game_cursor_location_transmit", + Vector2(0.5, 0.5), + ), + ) + source: SelectableSourceTiles | None = None + dest: tuple[SelectableDestinationTiles, ...] + if len(action) == 2: + raw_source, raw_dest = action + if isinstance(raw_source, SelectableSourceTiles): + source = raw_source + dest = cast(tuple[SelectableDestinationTiles, ...], raw_dest) + else: + dest = cast(tuple[SelectableDestinationTiles, ...], action) + else: + dest = action + + async with self.playing_lock: + self.can_made_play = False + if source is not None: + await self.apply_select_source(source) + for destination in dest: + ## print(f'{destination = }') + assert isinstance(destination, SelectableDestinationTiles) + await self.apply_select_destination(destination) + self.can_made_play = True - await self.apply_select_source(source) - destination = dest[0] - assert isinstance(destination, SelectableDestinationTiles) - await self.apply_select_destination(destination) - raise NotImplementedError(f"{source = } {dest = }") + ## raise NotImplementedError(f"{source = } {dest = }") @abstractmethod async def preform_turn(self) -> Action: @@ -148,6 +198,12 @@ async def preform_turn(self) -> Action: async def base_preform_turn(self) -> None: """Perform turn.""" + ## async with self.playing_lock: + if not self.can_made_play: + print("Skipping making move because of flag.") + await trio.lowlevel.checkpoint() + return + self.can_made_play = False self.moves += 1 ## winner = self.state.check_for_win() ## if winner is not None: @@ -158,13 +214,14 @@ async def base_preform_turn(self) -> None: print(f"{value} after {self.moves}") await trio.lowlevel.checkpoint() return - print(f"Move {self.moves}...") + print(f"\nMove {self.moves}...") action = await self.preform_turn() await self.preform_action(action) + print("Action complete.") async def handle_playing_as(self, event: Event[u8]) -> None: """Handle client playing as specified player event.""" - ## print("handle_playing_as") + print("handle_playing_as") self.playing_as = event.data if self.state.current_turn == self.playing_as: @@ -185,6 +242,7 @@ async def handle_initial_config( current_turn, floor_line_data, ) = event.data + ## print(f'[RemoteState] {variant_play = }') self.state = State.new_game(player_count, bool(variant_play)) self.state = self.state._replace(current_turn=current_turn) self.has_initial = True @@ -259,6 +317,27 @@ async def handle_factory_data( self.state = self.state._replace( factory_displays=factory_displays, ) + + ##if self.state.current_turn == self.playing_as: + ## await self.base_preform_turn() + ## return + await trio.lowlevel.checkpoint() + + async def handle_cursor_data( + self, + event: Event[Counter[u8]], + ) -> None: + """Handle cursor data update.""" + ## print("handle_cursor_data") + cursor_contents = event.data + + self.state = self.state._replace( + cursor_contents=cursor_contents, + ) + + ## if self.state.current_turn == self.playing_as and not self.state.is_cursor_empty(): + ## await self.base_preform_turn() + ## return await trio.lowlevel.checkpoint() async def handle_table_data(self, event: Event[Counter[u8]]) -> None: @@ -276,7 +355,7 @@ async def handle_pattern_current_turn_change( event: Event[u8], ) -> None: """Handle change of current turn.""" - ## print("handle_pattern_current_turn_change") + print("handle_pattern_current_turn_change") pattern_id = event.data self.state = self.state._replace( diff --git a/src/azul/game.py b/src/azul/game.py index ece5b38..f03ea2a 100644 --- a/src/azul/game.py +++ b/src/azul/game.py @@ -2167,8 +2167,10 @@ def cli_run() -> None: ) run() except ExceptionGroup as exc: - print(exc) - exception = "".join(traceback.format_exception(exc)) + ## print(exc) + ## exception = "".join(traceback.format_exception(exc)) + ## print(exception) + traceback.print_exception(exc) ## raise ## except BaseException as ex: ## screenshot_last_frame() diff --git a/src/azul/server.py b/src/azul/server.py index 214ad94..ef9f7f4 100755 --- a/src/azul/server.py +++ b/src/azul/server.py @@ -1250,8 +1250,10 @@ async def handle_client_pattern_row_clicked( ), ) + did_auto_wall_tile = False if self.state.current_phase == Phase.wall_tiling: if not self.state.variant_play: + did_auto_wall_tile = True self.state = self.state.apply_auto_wall_tiling() await self.transmit_new_round_data() await self.transmit_pattern_line_data() @@ -1259,7 +1261,7 @@ async def handle_client_pattern_row_clicked( if self.state.current_phase == Phase.end: print("TODO: Handle end of game.") - if self.state.current_turn != player_id: + if self.state.current_turn != player_id or did_auto_wall_tile: await self.raise_event( Event( "current_turn_change->network", @@ -1388,8 +1390,10 @@ async def handle_client_floor_clicked( ), ) + did_auto_wall_tile = False if self.state.current_phase == Phase.wall_tiling: if not self.state.variant_play: + did_auto_wall_tile = True self.state = self.state.apply_auto_wall_tiling() await self.transmit_new_round_data() await self.transmit_pattern_line_data() @@ -1397,7 +1401,7 @@ async def handle_client_floor_clicked( if self.state.current_phase == Phase.end: print("TODO: Handle end of game.") - if self.state.current_turn != player_id: + if self.state.current_turn != player_id or did_auto_wall_tile: await self.raise_event( Event( "current_turn_change->network", diff --git a/src/azul/state.py b/src/azul/state.py index c801135..1182aea 100644 --- a/src/azul/state.py +++ b/src/azul/state.py @@ -985,25 +985,26 @@ def yield_selectable_tile_destinations_factory_offer( current_player_data = self.player_data[self.current_turn] color = self.get_cursor_holding_color() - count = self.cursor_contents[color] + 1 + count = self.cursor_contents[color] for ( line_id, placable, ) in current_player_data.yield_possible_placement_rows(color): - for place_count in range(1, min(count, placable + 1)): - yield SelectableDestinationTiles( - destination=SelectableDestination.pattern_line, - place_count=place_count, - destination_id=line_id, - ) - # Can always place in floor line, even if full, - # because of box lid overflow - for place_count in range(1, count): + ## for place_count in range(1, min(count, placable + 1)): + place_count = min(count, placable) yield SelectableDestinationTiles( - destination=SelectableDestination.floor_line, + destination=SelectableDestination.pattern_line, place_count=place_count, + destination_id=line_id, ) + # Can always place in floor line, even if full, + # because of box lid overflow + ## for place_count in range(1, count): + yield SelectableDestinationTiles( + destination=SelectableDestination.floor_line, + place_count=count, + ) def apply_destination_select_action_factory_offer( self, @@ -1139,8 +1140,10 @@ def yield_actions( action_chain ) in new.yield_all_factory_offer_destinations(): yield (selection, action_chain) + elif self.current_phase == Phase.end: + pass else: - raise NotImplementedError() + raise NotImplementedError(f"{self.current_phase = }") def preform_action( self, From 3405ffe291a322deb7681eb4bc9ef0dd49a49051 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sat, 11 Jan 2025 16:45:21 -0600 Subject: [PATCH 34/58] Replace language module with `database` from MineOS-Market-Server Adding orjson as runtime dependency instead of only test dependency --- pyproject.toml | 9 +- src/azul/database.py | 329 +++++++++++++++++++++++++++++++++++++++++++ src/azul/lang.py | 30 ---- test-requirements.in | 8 +- 4 files changed, 336 insertions(+), 40 deletions(-) create mode 100644 src/azul/database.py delete mode 100644 src/azul/lang.py diff --git a/pyproject.toml b/pyproject.toml index 8e4ece1..407bade 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,13 +36,14 @@ keywords = [ "ai", "multi-player", "azul", "ai-support", "networked-game" ] dependencies = [ + "exceptiongroup; python_version < '3.11'", "libcomponent~=0.0.1", - "pygame~=2.6.0", - "typing_extensions>=4.12.2", "mypy_extensions>=1.0.0", - "trio~=0.28.0", - "exceptiongroup; python_version < '3.11'", "numpy~=2.1.3", + "orjson>=3.10,<4", + "pygame~=2.6.0", + "trio~=0.28.0", + "typing_extensions>=4.12.2", ] [tool.setuptools.dynamic] diff --git a/src/azul/database.py b/src/azul/database.py new file mode 100644 index 0000000..8d19b2a --- /dev/null +++ b/src/azul/database.py @@ -0,0 +1,329 @@ +"""Database - Read and write json files.""" + +# Programmed by CoolCat467 + +from __future__ import annotations + +# Database - Read and write json files +# Copyright (C) 2024 CoolCat467 +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +__title__ = "Database" +__author__ = "CoolCat467" + +from os import makedirs, path +from pathlib import Path +from typing import TYPE_CHECKING, Any + +import orjson +import trio + +if TYPE_CHECKING: + from collections.abc import Generator, Iterable, Iterator + from types import TracebackType + + from typing_extensions import Self + + +_LOADED: dict[str, Records] = {} + + +class Database(dict[str, Any]): + """Database dict with file read write functions.""" + + __slots__ = ("__weakref__", "file") + + def __init__( + self, + file_path: str | Path | trio.Path, + auto_load: bool = True, + ) -> None: + """Initialize and set file path. + + If auto_load is True, automatically load file contents synchronously + if file exists. + """ + super().__init__() + self.file = file_path + + if auto_load and path.exists(self.file): + self.reload_file() + + def reload_file(self) -> None: + """Reload database file. + + Will raise FileNotFoundError in the event file does not exist. + """ + self.update(orjson.loads(Path(self.file).read_bytes())) + + async def reload_async(self) -> None: + """Reload database file asynchronously. + + Does not decode json data if file is empty. + Will raise FileNotFoundError in the event file does not exist. + """ + async with await trio.open_file(self.file, "rb") as file: + data = await file.read() + if not data: + return + self.update(orjson.loads(data)) + + def serialize(self) -> bytes: + """Return this object's data serialized as bytes.""" + return orjson.dumps( + self, + option=orjson.OPT_APPEND_NEWLINE + | orjson.OPT_NON_STR_KEYS + | orjson.OPT_NAIVE_UTC, + ) + + def write_file(self) -> None: + """Write database file. + + May raise PermissionError in the event of insufficient permissions. + """ + folder = path.dirname(self.file) + if not path.exists(folder): + makedirs(folder, exist_ok=False) + Path(self.file).write_bytes(self.serialize()) + + async def write_async(self) -> None: + """Write database file asynchronously. + + May raise PermissionError in the event of insufficient permissions. + """ + folder = trio.Path(self.file).parent + if not await folder.exists(): + await folder.mkdir(parents=True, exist_ok=False) + async with await trio.open_file( + self.file, + "wb", + ) as file: + await file.write(self.serialize()) + + def __enter__(self) -> Self: + """Enter context manager.""" + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """Context manager exit.""" + self.write_file() + + async def __aenter__(self) -> Self: + """Enter async context manager. + + Automatically reloads file if it exists. + """ + if await trio.Path(self.file).exists(): + await self.reload_async() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """Async context manager exit, write file contents asynchronously.""" + await self.write_async() + + +class Table: + """Table from dictionary. + + Allows getting and setting entire columns of a database + """ + + __slots__ = ("_key_name", "_records") + + def __init__(self, records: dict[str, Any], key_name: str) -> None: + """Initialize and set records and key name.""" + self._records = records + self._key_name = key_name + + def __repr__(self) -> str: + """Get text representation of table.""" + size: dict[str, int] = {} + columns = self.keys() + for column in columns: + size[column] = len(column) + for value in self[column]: + if value is None: + continue + length = ( + len(value) + if hasattr(value, "__len__") + else len(repr(value)) + ) + size[column] = max(size[column], length) + num_pad = len(str(len(self))) + lines = [] + column_names = " ".join(c.ljust(length) for c, length in size.items()) + lines.append("".rjust(num_pad) + " " + column_names) + for index in range(len(self)): + line = [str(index).ljust(num_pad)] + for column in columns: + line.append(str(self[column][index]).ljust(size[column])) + lines.append(" ".join(line)) + return "\n".join(lines) + + def __getitem__(self, column: str) -> tuple[Any, ...]: + """Get column data.""" + if column not in self.keys(): + return tuple(None for _ in range(len(self))) + if column == self._key_name: + return tuple(self._records.keys()) + return tuple(row.get(column) for row in self._records.values()) + + def __setitem__(self, column: str, value: Iterable[Any]) -> None: + """Set column data to value.""" + if column == self._key_name: + for old, new in zip(tuple(self._records), value, strict=False): + self._records[new] = self._records.pop(old) + else: + for key, set_value in zip(self._records, value, strict=True): + if set_value is None: + continue + self._records[key][column] = set_value + + def _raw_keys(self) -> set[str]: + """Return the name of every column.""" + keys = set() + for row in self._records.values(): + keys |= set(row.keys()) + return keys + + def keys(self) -> set[str]: + """Return the name of every column.""" + return self._raw_keys() | {self._key_name} + + def __iter__(self) -> Iterator[str]: + """Return iterator for column names.""" + return iter(self.keys()) + + def values(self) -> tuple[Any, ...]: + """Return every column.""" + values = [] + for key in self.keys(): + values.append(self[key]) + return tuple(values) + + def items(self) -> tuple[tuple[str, Any], ...]: + """Return tuples of column names and columns.""" + items = [] + for key in sorted(self.keys()): + items.append((key, self[key])) + return tuple(items) + + def _rows( + self, + columns: list[str], + ) -> Generator[tuple[Any, ...], None, None]: + """Yield columns in order from each row.""" + for key, value in self._records.items(): + yield (key, *tuple(value.get(col) for col in columns)) + + def rows(self) -> Generator[tuple[Any, ...], None, None]: + """Yield each row.""" + yield from self._rows(sorted(self.keys())) + + def column_and_rows(self) -> Generator[tuple[str | Any, ...], None, None]: + """Yield tuple of column row and then rows in column order.""" + columns = sorted(self._raw_keys()) + yield (self._key_name, *columns) + yield from self._rows(columns) + + def __len__(self) -> int: + """Return number of records.""" + return len(self._records) + + def get_id(self, key: str, value: object) -> int | None: + """Return index of value in column key or None if not found.""" + try: + return self[key].index(value) + except ValueError: + return None + + +class Records(Database): + """Records dict with columns.""" + + __slots__ = () + + def table(self, element_name: str) -> Table: + """Get table object given that keys are named element name.""" + return Table(self, element_name) + + +def load(file_path: str | Path | trio.Path) -> Records: + """Load database from file path or return already loaded instance.""" + file = path.abspath(file_path) + if file not in _LOADED: + _LOADED[file] = Records(file) + return _LOADED[file] + + +async def load_async(file_path: str | Path | trio.Path) -> Records: + """Load database from file path or return already loaded instance.""" + await trio.lowlevel.checkpoint() + file = path.abspath(file_path) + if file not in _LOADED: + _LOADED[file] = Records(file, auto_load=False) + if await trio.Path(file).exists(): + await _LOADED[file].reload_async() + return _LOADED[file] + + +def get_loaded() -> set[str]: + """Return set of loaded database files.""" + return set(_LOADED) + + +def unload(file_path: str | Path | trio.Path) -> None: + """If database loaded, write file and unload.""" + file = path.abspath(file_path) + if file not in get_loaded(): + return + database = load(file) + database.write_file() + del _LOADED[file] + + +async def async_unload(file_path: str | Path | trio.Path) -> None: + """If database loaded, write file and unload.""" + file = path.abspath(file_path) + if file not in get_loaded(): + return + database = load(file) + await database.write_async() + del _LOADED[file] + + +def unload_all() -> None: + """Unload all loaded databases.""" + for file_path in get_loaded(): + unload(file_path) + + +async def async_unload_all() -> None: + """Unload all loaded databases.""" + async with trio.open_nursery() as nursery: + for file_path in get_loaded(): + nursery.start_soon(async_unload, file_path) diff --git a/src/azul/lang.py b/src/azul/lang.py deleted file mode 100644 index b97d6c0..0000000 --- a/src/azul/lang.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Language file handler.""" - -from __future__ import annotations - -# Programmed by CoolCat467 - -__title__ = "lang" -__author__ = "CoolCat467" -__version__ = "0.0.0" - -import json -from functools import cache -from os.path import exists, join - - -def load_json(filename: str) -> dict[str, str]: - """Return json data loaded from filename.""" - with open(filename, encoding="utf-8") as loaded: - data = json.load(loaded) - assert isinstance(data, dict) - return data - - -@cache -def load_lang(name: str) -> dict[str, str] | None: - """Return full data for language with given name.""" - filename = join("lang", f"{name}.json") - if not exists(filename): - return None - return load_json(filename) diff --git a/test-requirements.in b/test-requirements.in index 1e6d53e..105217b 100644 --- a/test-requirements.in +++ b/test-requirements.in @@ -6,22 +6,18 @@ pytest-cov # Tools black; implementation_name == "cpython" -mypy >= 1.14.0 # Would use mypy[faster-cache], but orjson has build issues on pypy -orjson; implementation_name == "cpython" +mypy >= 1.14.0 ruff >= 0.6.6 uv >= 0.2.24 codespell -# https://github.com/python-trio/trio/pull/654#issuecomment-420518745 -mypy-extensions -typing-extensions - # Azul's own dependencies # exceptiongroup; python_version < '3.11' libcomponent~=0.0.1 mypy_extensions>=1.0.0 numpy~=2.1.3 +orjson>=3.10,<4 pygame~=2.6.0 trio~=0.28.0 typing_extensions>=4.12.2 From 336de9809eeae98b3acc60beb8e14c7e5d9319cb Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sat, 11 Jan 2025 18:03:00 -0600 Subject: [PATCH 35/58] Use localization files for user-displayed strings not counting debug text --- pyproject.toml | 2 +- src/azul/client.py | 18 +++--- src/azul/conf.py | 32 ----------- src/azul/conf/main.conf | 6 -- src/azul/data/tiles/black.png | Bin 72 -> 0 bytes src/azul/data/tiles/blue.png | Bin 82 -> 0 bytes src/azul/data/tiles/cyan.png | Bin 82 -> 0 bytes src/azul/data/tiles/grey.png | Bin 82 -> 0 bytes src/azul/data/tiles/grey_black.png | Bin 82 -> 0 bytes src/azul/data/tiles/grey_blue.png | Bin 82 -> 0 bytes src/azul/data/tiles/grey_cyan.png | Bin 82 -> 0 bytes src/azul/data/tiles/grey_red.png | Bin 82 -> 0 bytes src/azul/data/tiles/grey_yellow.png | Bin 82 -> 0 bytes src/azul/data/tiles/number_one.png | Bin 167 -> 0 bytes src/azul/data/tiles/red.png | Bin 80 -> 0 bytes src/azul/data/tiles/yellow.png | Bin 81 -> 0 bytes src/azul/game.py | 84 +++++++++++++++++++++------- src/azul/lang/en_us.json | 37 ++++++------ src/azul/mr_floppy_test.py | 4 +- 19 files changed, 92 insertions(+), 91 deletions(-) delete mode 100644 src/azul/conf.py delete mode 100644 src/azul/conf/main.conf delete mode 100644 src/azul/data/tiles/black.png delete mode 100644 src/azul/data/tiles/blue.png delete mode 100644 src/azul/data/tiles/cyan.png delete mode 100644 src/azul/data/tiles/grey.png delete mode 100644 src/azul/data/tiles/grey_black.png delete mode 100644 src/azul/data/tiles/grey_blue.png delete mode 100644 src/azul/data/tiles/grey_cyan.png delete mode 100644 src/azul/data/tiles/grey_red.png delete mode 100644 src/azul/data/tiles/grey_yellow.png delete mode 100644 src/azul/data/tiles/number_one.png delete mode 100644 src/azul/data/tiles/red.png delete mode 100644 src/azul/data/tiles/yellow.png diff --git a/pyproject.toml b/pyproject.toml index 407bade..c790d1f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,7 @@ version = {attr = "azul.game.__version__"} azul_game = "azul.game:cli_run" [tool.setuptools.package-data] -azul = ["py.typed", "data/*"] +azul = ["py.typed", "data/*", "lang/*", "fonts/*"] [tool.mypy] plugins = ["numpy.typing.mypy_plugin"] diff --git a/src/azul/client.py b/src/azul/client.py index 88d1be8..f604ee1 100644 --- a/src/azul/client.py +++ b/src/azul/client.py @@ -241,16 +241,16 @@ async def print_callback_ping(self, event: Event[int]) -> None: print(f"[azul.client] print_callback_ping {difference * 1e-06:.03f}ms") await trio.lowlevel.checkpoint() - async def raise_disconnect(self, message: str) -> None: + async def raise_disconnect(self, message_key: str) -> None: """Raise client_disconnected event with given message.""" - print(f"{self.__class__.__name__}: {message}") + print(f"{self.__class__.__name__}: {message_key}") if not self.manager_exists: print( f"{self.__class__.__name__}: Manager does not exist, not raising disconnect event.", ) return # self.unregister_all_network_write_events() - await self.raise_event(Event("client_disconnected", message)) + await self.raise_event(Event("client_disconnected", message_key)) await self.close() assert self.not_connected @@ -301,7 +301,7 @@ async def handle_read_event(self) -> None: if not self.manager_exists: return if self.not_connected: - await self.raise_disconnect("Not connected to server.") + await self.raise_disconnect("error.not_connected") return # event: Event[bytearray] | None = None try: @@ -319,9 +319,7 @@ async def handle_read_event(self) -> None: print(f"[{self.name}] NetworkTimeoutError") await self.close() traceback.print_exception(exc) - await self.raise_disconnect( - "Failed to read event from server.", - ) + await self.raise_disconnect("error.read_event_fail") return except network.NetworkStreamNotConnectedError as exc: self.running = False @@ -334,9 +332,7 @@ async def handle_read_event(self) -> None: self.running = False print(f"[{self.name}] NetworkEOFError") await self.close() - await self.raise_disconnect( - "Server closed connection.", - ) + await self.raise_disconnect("error.socket_eof") return ## print(f'[azul.client] handle_read_event {event}') @@ -375,7 +371,7 @@ async def handle_client_connect( "manager does not exist, cannot send client connection closed event.", ) return - await self.raise_disconnect("Error connecting to server.") + await self.raise_disconnect("error.socket_connect_fail") async def read_initial_config(self, event: Event[bytearray]) -> None: """Read initial_config event from server.""" diff --git a/src/azul/conf.py b/src/azul/conf.py deleted file mode 100644 index 4f4400a..0000000 --- a/src/azul/conf.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Config module.""" - -# Programmed by CoolCat467 - -__title__ = "Conf" -__author__ = "CoolCat467" -__version__ = "0.0.0" - - -from configparser import ConfigParser - - -def load_config(config_file: str) -> dict[str, dict[str, str]]: - """Return a config object from config_file.""" - config = ConfigParser() - config.read((config_file,)) - - data: dict[str, dict[str, str]] = {} - for section, values in dict(config.items()).items(): - data[section] = dict(values) - - # config.clear() - # config.update(data) - ## - # with open(config_file, mode='w', encoding='utf-8') as conf_file: - # config.write(conf_file) - - return data - - -if __name__ == "__main__": - print(f"{__title__}\nProgrammed by {__author__}.") diff --git a/src/azul/conf/main.conf b/src/azul/conf/main.conf deleted file mode 100644 index f2f1c51..0000000 --- a/src/azul/conf/main.conf +++ /dev/null @@ -1,6 +0,0 @@ -[Font] -font_folder = data -font_file = RuneScape-UF-Regular.ttf - -[Language] -lang_name = en_us diff --git a/src/azul/data/tiles/black.png b/src/azul/data/tiles/black.png deleted file mode 100644 index d19d97136c0b5cc73746c8d148bd289d165e3126..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 72 zcmeAS@N?(olHy`uVBq!ia0vp^{2+Qh)< T$-Ht4P>R9R)z4*}Q$iB}r<4uT diff --git a/src/azul/data/tiles/blue.png b/src/azul/data/tiles/blue.png deleted file mode 100644 index 97cfd8efdeea8a1753b2aedc1dd3e74fa5bab689..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 82 zcmeAS@N?(olHy`uVBq!ia0vp^{2lAbP(AsQ2tLjr!BcQ8p%ihH@@ fRp3>VKwgICeT=cY!bP0l+XkK)29{j diff --git a/src/azul/data/tiles/cyan.png b/src/azul/data/tiles/cyan.png deleted file mode 100644 index e49b3dbdb2324e3edb5be80928cddff2cd983b1e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 82 zcmeAS@N?(olHy`uVBq!ia0vp^{2lAbP(AsQ2tQ-1uP-lAbP(AsQ2tW430AGGA=ytXR5u em3!!A7gL5dZYCL5QNFuCMGT&lAbP(AsQ2t*Q{Tkt{`O~xV|Lh f>x!&dE7%yceHdq}h%7h^RK(!v>gTe~DWM4fumTh| diff --git a/src/azul/data/tiles/grey_blue.png b/src/azul/data/tiles/grey_blue.png deleted file mode 100644 index 3e5d1a261ca574a0bb88b79c74a03decf0cac8d6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 82 zcmeAS@N?(olHy`uVBq!ia0vp^{2lAbP(AsQ2tbMo(>cQ8p%ihH@@ fRp3>VKwbtWWyaU1F8-VjRK(!v>gTe~DWM4f)u|RD diff --git a/src/azul/data/tiles/grey_cyan.png b/src/azul/data/tiles/grey_cyan.png deleted file mode 100644 index 6d4c01f7f761a5e1c67860419ac6d9def9d7aaf8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 82 zcmeAS@N?(olHy`uVBq!ia0vp^{2lAbP(AsQ2t*WBMfzcJ&0%brVt fR~N6!TqMQNEX^eSL?B`}P!WTttDnm{r-UW|{Qee{ diff --git a/src/azul/data/tiles/grey_red.png b/src/azul/data/tiles/grey_red.png deleted file mode 100644 index 7ac7401fa16a02e9cea88a68a1c22119922f1ebf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 82 zcmeAS@N?(olHy`uVBq!ia0vp^{2lAbP(AsQ2t@5INScQ8p%ihH@@ fRp3>VKwgIHJ&d=%{VSOPRK(!v>gTe~DWM4f=Ykgn diff --git a/src/azul/data/tiles/grey_yellow.png b/src/azul/data/tiles/grey_yellow.png deleted file mode 100644 index 43f00e6e63ab8eaf069521e3b9aa614473cc31a3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 82 zcmeAS@N?(olHy`uVBq!ia0vp^{2lAbP(AsQ2t@9bYYuQB6*%brVt fR~N6!TqMPioy{b)URa+MsEEPS)z4*}Q$iB}@v9ZD diff --git a/src/azul/data/tiles/number_one.png b/src/azul/data/tiles/number_one.png deleted file mode 100644 index 264020879236514c86613dba01363f991129ae74..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 167 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1SJ1Ryj={W@;qG}Lp(Z@Ljr!Bckn*o5`6h( zA|v;l(*Jg1nXayHZf(#ywyJPJ^NWAAZNI-ue)^o5#`rYhN>d=Wo36xGFr%%VRp-aj zyLTleBqZ+bJF0i?z<~|L>ZxLPcDc^7P?LC}pmgJa5{HDl^!^s0eKSPXT3)Vr4P-HR My85}Sb4q9e09g+|*#H0l diff --git a/src/azul/data/tiles/red.png b/src/azul/data/tiles/red.png deleted file mode 100644 index 478d2665dfc3a1839f5479959fb2a555adc27f9f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 80 zcmeAS@N?(olHy`uVBq!ia0vp^{2;+`&!AsQ2t|D1PVN>FnBxZ+jd dRg(Z-hMT>NvOb$lM1e{eJYD@<);T3K0RWQq6RrRN diff --git a/src/azul/data/tiles/yellow.png b/src/azul/data/tiles/yellow.png deleted file mode 100644 index 1341ce2e9fb30ccfde5a76807932042e0b4868c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 81 zcmeAS@N?(olHy`uVBq!ia0vp^{25}q!OAsQ2t|J1iPRvd6?x)peJ e@v6*)QVbfu7=M00`oaXLhQZU-&t;ucLK6V>P#3!Z diff --git a/src/azul/game.py b/src/azul/game.py index f03ea2a..9d3140d 100644 --- a/src/azul/game.py +++ b/src/azul/game.py @@ -59,7 +59,7 @@ from pygame.rect import Rect from pygame.sprite import LayeredDirty -from azul import element_list, objects, sprite +from azul import database, element_list, objects, sprite from azul.async_clock import Clock from azul.client import GameClient, read_advertisements from azul.crop import auto_crop_clear @@ -125,6 +125,9 @@ ROOT_FOLDER: Final = Path(__file__).absolute().parent DATA_FOLDER: Final = ROOT_FOLDER / "data" FONT_FOLDER: Final = ROOT_FOLDER / "fonts" +LANG_FOLDER: Final = ROOT_FOLDER / "lang" +# TODO: Way to change language +LANGUAGE: Final = "en_us" # Game stuff # Tiles @@ -154,7 +157,7 @@ GREYSHIFT = 0.75 # 0.65 # Font -FONT: Final = FONT_FOLDER / "VeraSerif.ttf" # "RuneScape-UF-Regular.ttf" +FONT: Final = FONT_FOLDER / "VeraSerif.ttf" SCOREFONTSIZE = 30 BUTTONFONTSIZE = 60 @@ -173,6 +176,45 @@ } +def decode_localization_entry(localization_string: str) -> list[str]: + """Return localization entry path.""" + return localization_string.split(".") + + +def s_(localization_string: str, **kwargs: object) -> str: + """Return localization string entry, or path to it if it doesn't exist.""" + language_filename = f"{LANGUAGE}.json" + language_file = LANG_FOLDER / language_filename + # Load keeps copy in-memory, so only performance hit first time. + language_data = database.load(language_file) + + localization_entry = decode_localization_entry(localization_string) + + current: dict[str, Any] = language_data + new: dict[str, Any] | str | None + final: str | None = None + for entry in localization_entry: + new = current.get(entry) + if new is None: + break + if isinstance(new, str): + final = new + break + assert isinstance( + new, + dict, + ), f"Unexpected value in {language_file!r} for {localization_string!r}" + current = new + if final is None: + # Key does not exist + localization_key = f"[{LANGUAGE}] {localization_string}" + if kwargs: + args = ",".join(f"{k}={v!r}" for k, v in kwargs.items()) + return f"{localization_key}<{args}>" + return localization_key + return final.format(**kwargs) + + def vec2_to_location(vec: Vector2) -> tuple[int, int]: """Return rounded location tuple from Vector2.""" x, y = map(int, vec.rounded()) @@ -708,7 +750,7 @@ async def handle_client_disconnected( event: Event[None], ) -> None: """Unregister tick event handler.""" - print("[azul.game.Cursor] Got client disconnect, unregistering tick") + # print("[azul.game.Cursor] Got client disconnect, unregistering tick") self.unregister_handler_type("tick") await trio.lowlevel.checkpoint() @@ -825,7 +867,11 @@ async def handle_game_board_data( await trio.lowlevel.checkpoint() return - self.data = array + assert array.ndim == 2 + assert len(array.shape) == 2 + w, h = array.shape + # Prove to typechecker that array is 2D + self.data = array.reshape((w, h)) self.update_image() self.visible = True @@ -1508,7 +1554,7 @@ async def entry_actions(self) -> None: color=Color(0, 0, 0), outline=(255, 0, 0), border_width=4, - text=__title__.upper(), + text=s_("title.game_title"), ) title_text.location = (SCREEN_SIZE[0] // 2, title_text.rect.h) self.group_add(title_text) @@ -1518,7 +1564,7 @@ async def entry_actions(self) -> None: button_font, visible=True, color=Color(0, 0, 0), - text="Host Networked Game", + text=s_("title.host_game"), location=[x // 2 for x in SCREEN_SIZE], handle_click=self.change_state("play_hosting"), ) @@ -1529,7 +1575,7 @@ async def entry_actions(self) -> None: button_font, visible=True, color=Color(0, 0, 0), - text="Join Networked Game", + text=s_("title.join_game"), location=hosting_button.location + Vector2( 0, @@ -1544,7 +1590,7 @@ async def entry_actions(self) -> None: button_font, visible=True, color=Color(0, 0, 0), - text="Singleplayer Game", + text=s_("title.singleplayer"), location=hosting_button.location - Vector2( 0, @@ -1559,7 +1605,7 @@ async def entry_actions(self) -> None: button_font, visible=True, color=Color(0, 0, 0), - text="Quit", + text=s_("title.quit"), location=join_button.location + Vector2( 0, @@ -1656,7 +1702,7 @@ def __init__(self, name: str, font: pygame.font.Font) -> None: self.update_location_on_resize = False self.border_width = 4 self.outline = RED - self.text = "Return to Title" + self.text = s_("connect.return_title") self.visible = True self.location = (SCREEN_SIZE[0] // 2, self.location.y + 10) @@ -1929,7 +1975,7 @@ async def exit_actions(self) -> None: async def handle_game_over(self, event: Event[int]) -> None: """Handle game over event.""" winner = event.data - self.exit_data = (0, f"{winner} Won", False) + self.exit_data = (0, s_("play.win", winner=winner), False) await self.machine.raise_event_internal(Event("network_stop", None)) @@ -1938,7 +1984,9 @@ async def handle_client_disconnected(self, event: Event[str]) -> None: error = event.data print(f"[azul.game.PlayState] handle_client_disconnected {error = }") - self.exit_data = (1, f"Client Disconnected$${error}", False) + client_disconnected = s_("error.client_disconnected") + error_text = s_(error) + self.exit_data = (1, f"{client_disconnected}$${error_text}", False) async def do_actions(self) -> None: """Perform actions for this State.""" @@ -1969,7 +2017,7 @@ async def do_actions(self) -> None: font, visible=True, color=Color(0, 0, 0), - text=f"{message} - Return to Title", + text=s_("play.return_title_msg", message=message), location=[x // 2 for x in SCREEN_SIZE], handle_click=self.change_state("title"), ) @@ -2141,7 +2189,7 @@ def screenshot_last_frame() -> None: pygame.image.save(surface, fullpath, filename) del surface - print(f'Saved screenshot to "{fullpath}".') + print(s_("screenshot_save", fullpath=fullpath)) def cli_run() -> None: @@ -2161,16 +2209,12 @@ def cli_run() -> None: # Initialize Pygame _success, fail = pygame.init() if fail > 0: - print( - "Warning! Some modules of Pygame have not initialized properly!\n", - "This can occur when not all required modules of SDL are installed.", - ) + print(s_("error.pygame_uninitialized")) run() except ExceptionGroup as exc: ## print(exc) - ## exception = "".join(traceback.format_exception(exc)) + exception = "".join(traceback.format_exception(exc)) ## print(exception) - traceback.print_exception(exc) ## raise ## except BaseException as ex: ## screenshot_last_frame() diff --git a/src/azul/lang/en_us.json b/src/azul/lang/en_us.json index 67ac85b..35b1356 100644 --- a/src/azul/lang/en_us.json +++ b/src/azul/lang/en_us.json @@ -1,24 +1,25 @@ { - "main_menu": { - "title": "Azul", - "host_server": "Host Game", - "join_server": "Join Game", - "close": "Close" - }, - "host_server_menu": { - "title": "Host Server", - "port_input": "Enter Host Port ({})", - "start_server": "Start Server", + "connect": { "return_title": "Return to Title" }, - "join_server_menu": { - "title": "Join Server", - "host_input": "Enter Host Address", - "port_input": "Enter Host Port ({})", - "join_server": "Connect to Server", - "return_title": "Return to Title" + "error": { + "client_disconnected": "Client Disconnected", + "not_connected": "Not connected to server.", + "pygame_uninitialized": "Warning! Some modules of Pygame have not initialized properly!\nThis can occur when not all required modules of SDL are installed.", + "read_event_fail": "Failed to read event from server.", + "socket_connect_fail": "Error connecting to server.", + "socket_eof": "Server closed connection." + }, + "play": { + "return_title_msg": "{message} - Return to Title", + "win": "{winner} Won" }, - "connect_server": { - "connecting": "Connecting to Server..." + "screenshot_save": "Saved screenshot to \"{fullpath}\".", + "title": { + "game_title": "Azul", + "host_game": "Host Networked Game", + "join_game": "Join Networked Game", + "quit": "Quit", + "singleplayer": "Singleplayer Game" } } diff --git a/src/azul/mr_floppy_test.py b/src/azul/mr_floppy_test.py index 878df8c..3b55725 100644 --- a/src/azul/mr_floppy_test.py +++ b/src/azul/mr_floppy_test.py @@ -17,7 +17,7 @@ from pygame.locals import K_ESCAPE, KEYUP, QUIT, RESIZABLE, WINDOWRESIZED from pygame.rect import Rect -from azul import conf, lang, objects, sprite +from azul import objects, sprite from azul.statemachine import AsyncState, AsyncStateMachine from azul.vector import Vector2 @@ -368,8 +368,6 @@ async def async_run() -> None: """Run client.""" global SCREEN_SIZE # global client - config = conf.load_config(path.join("conf", "main.conf")) - lang.load_lang(config["Language"]["lang_name"]) screen = pygame.display.set_mode( tuple(SCREEN_SIZE), From 37495e43f5573edbdffe48017a4f1d8b79c6b0ec Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sat, 11 Jan 2025 18:08:43 -0600 Subject: [PATCH 36/58] Update test-requirements --- test-requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index dd75553..6e0a404 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -45,7 +45,7 @@ mypy-extensions==1.0.0 # mypy numpy==2.1.3 # via -r test-requirements.in -orjson==3.10.13 ; implementation_name == 'cpython' +orjson==3.10.14 # via -r test-requirements.in outcome==1.3.0.post0 # via @@ -74,7 +74,7 @@ pytest-cov==6.0.0 # via -r test-requirements.in pytest-trio==0.8.0 # via -r test-requirements.in -ruff==0.8.4 +ruff==0.9.1 # via -r test-requirements.in sniffio==1.3.1 # via trio @@ -97,5 +97,5 @@ typing-extensions==4.12.2 # black # libcomponent # mypy -uv==0.5.13 +uv==0.5.18 # via -r test-requirements.in From ee541f950049be5c90a5946566f33c9283d25191 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sat, 11 Jan 2025 18:21:15 -0600 Subject: [PATCH 37/58] Try fixing windows CI issue by explicity disabling submodules --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24e4a67..ac20006 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,7 @@ jobs: uses: actions/checkout@v4 with: persist-credentials: false + submodules: false - name: Setup python uses: actions/setup-python@v5 with: From 0e74738e6b8c3d4179382c06becf917c72aa284f Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sat, 11 Jan 2025 18:36:37 -0600 Subject: [PATCH 38/58] Try using `set-safe-directory: false` to fix windows CI issue Suggested from https://github.com/actions/checkout/issues/1388#issuecomment-2229260270 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac20006..9713dc8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: uses: actions/checkout@v4 with: persist-credentials: false - submodules: false + set-safe-directory: false - name: Setup python uses: actions/setup-python@v5 with: From 935ca82f98d8b96ed0237b78f0b95f3d39af1ff5 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sat, 11 Jan 2025 18:47:50 -0600 Subject: [PATCH 39/58] Still trying to fix windows CI issues --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9713dc8..f94dac5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,11 +30,13 @@ jobs: || false }} steps: + - name: Print Git config + run: git config --list - name: Checkout uses: actions/checkout@v4 with: persist-credentials: false - set-safe-directory: false + submodules: false # Disable submodule fetching - name: Setup python uses: actions/setup-python@v5 with: From bec254f9775225d0d5effd313a6f253178ce12c0 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sat, 11 Jan 2025 18:54:50 -0600 Subject: [PATCH 40/58] Try not disabling `persist-credentials` --- .github/workflows/ci.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f94dac5..efa93a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,12 +30,10 @@ jobs: || false }} steps: - - name: Print Git config - run: git config --list - name: Checkout uses: actions/checkout@v4 with: - persist-credentials: false + persist-credentials: true # Set to `false` once git issue fixed submodules: false # Disable submodule fetching - name: Setup python uses: actions/setup-python@v5 From 0eeda4f25336b5b23eb2460d3725e009cf0f52b3 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sat, 11 Jan 2025 19:00:20 -0600 Subject: [PATCH 41/58] Rename files windows doesn't like --- .github/workflows/ci.yml | 3 +-- src/azul/game.py | 2 +- ...24.png => Crash_at_Wed-Nov-13-15_30_42-2024.png} | Bin ...24.png => Crash_at_Wed_Nov_13_16_02_46_2024.png} | Bin ...24.png => Crash_at_Wed_Nov_13_16_20_10_2024.png} | Bin ...g => Screenshot_at_Sun-Jun-13-10_46_34-2021.png} | Bin 6 files changed, 2 insertions(+), 3 deletions(-) rename src/azul/screenshots/{Crash_at_Wed-Nov-13-15:30:42-2024.png => Crash_at_Wed-Nov-13-15_30_42-2024.png} (100%) rename src/azul/screenshots/{Crash_at_Wed_Nov_13_16:02:46_2024.png => Crash_at_Wed_Nov_13_16_02_46_2024.png} (100%) rename src/azul/screenshots/{Crash_at_Wed_Nov_13_16:20:10_2024.png => Crash_at_Wed_Nov_13_16_20_10_2024.png} (100%) rename src/azul/screenshots/{Screenshot_at_Sun-Jun-13-10:46:34-2021.png => Screenshot_at_Sun-Jun-13-10_46_34-2021.png} (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index efa93a4..24e4a67 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,8 +33,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - persist-credentials: true # Set to `false` once git issue fixed - submodules: false # Disable submodule fetching + persist-credentials: false - name: Setup python uses: actions/setup-python@v5 with: diff --git a/src/azul/game.py b/src/azul/game.py index 9d3140d..df3c1bb 100644 --- a/src/azul/game.py +++ b/src/azul/game.py @@ -2177,7 +2177,7 @@ def run() -> None: def screenshot_last_frame() -> None: """Save the last frame before the game crashed.""" surface = pygame.display.get_surface().copy() - str_time = "_".join(time.asctime().split(" ")) + str_time = "_".join(time.asctime().split(" ")).replace(":", "_") filename = f"Crash_at_{str_time}.png" path = Path("screenshots").absolute() diff --git a/src/azul/screenshots/Crash_at_Wed-Nov-13-15:30:42-2024.png b/src/azul/screenshots/Crash_at_Wed-Nov-13-15_30_42-2024.png similarity index 100% rename from src/azul/screenshots/Crash_at_Wed-Nov-13-15:30:42-2024.png rename to src/azul/screenshots/Crash_at_Wed-Nov-13-15_30_42-2024.png diff --git a/src/azul/screenshots/Crash_at_Wed_Nov_13_16:02:46_2024.png b/src/azul/screenshots/Crash_at_Wed_Nov_13_16_02_46_2024.png similarity index 100% rename from src/azul/screenshots/Crash_at_Wed_Nov_13_16:02:46_2024.png rename to src/azul/screenshots/Crash_at_Wed_Nov_13_16_02_46_2024.png diff --git a/src/azul/screenshots/Crash_at_Wed_Nov_13_16:20:10_2024.png b/src/azul/screenshots/Crash_at_Wed_Nov_13_16_20_10_2024.png similarity index 100% rename from src/azul/screenshots/Crash_at_Wed_Nov_13_16:20:10_2024.png rename to src/azul/screenshots/Crash_at_Wed_Nov_13_16_20_10_2024.png diff --git a/src/azul/screenshots/Screenshot_at_Sun-Jun-13-10:46:34-2021.png b/src/azul/screenshots/Screenshot_at_Sun-Jun-13-10_46_34-2021.png similarity index 100% rename from src/azul/screenshots/Screenshot_at_Sun-Jun-13-10:46:34-2021.png rename to src/azul/screenshots/Screenshot_at_Sun-Jun-13-10_46_34-2021.png From 190d68d8b9cd7a336a752274cdbfa3dd045ce5cd Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sat, 18 Jan 2025 15:14:11 -0600 Subject: [PATCH 42/58] Mypy `--strict` already handles a lot --- .pre-commit-config.yaml | 6 +++--- pyproject.toml | 19 +++---------------- 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c8d04bc..6f7f3e3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.4 + rev: v0.9.2 hooks: - id: ruff types: [file] @@ -39,11 +39,11 @@ repos: additional_dependencies: - tomli - repo: https://github.com/crate-ci/typos - rev: v1.28.4 + rev: dictgen-v0.3.1 hooks: - id: typos - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v0.10.0 + rev: v1.2.0 hooks: - id: zizmor - repo: local diff --git a/pyproject.toml b/pyproject.toml index c790d1f..413c237 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,29 +62,16 @@ azul = ["py.typed", "data/*", "lang/*", "fonts/*"] [tool.mypy] plugins = ["numpy.typing.mypy_plugin"] files = ["src/azul/", "computer_players"] -check_untyped_defs = true +show_column_numbers = true +show_error_codes = true +show_traceback = true disallow_any_decorated = true -disallow_any_generics = true disallow_any_unimported = true -disallow_incomplete_defs = true -disallow_subclassing_any = true -disallow_untyped_calls = true -disallow_untyped_decorators = true -disallow_untyped_defs = true ignore_missing_imports = true local_partial_types = true no_implicit_optional = true -no_implicit_reexport = true -show_column_numbers = true -show_error_codes = true -show_traceback = true strict = true -strict_equality = true -warn_redundant_casts = true -warn_return_any = true warn_unreachable = true -warn_unused_configs = true -warn_unused_ignores = true [tool.ruff.lint.isort] combine-as-imports = true From 961b5182c7f1406df5450620f8615a91336494af Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sat, 18 Jan 2025 17:55:31 -0600 Subject: [PATCH 43/58] Switch to uv-based lock file --- .github/workflows/autodeps.yml | 16 +- .github/workflows/ci.yml | 2 + .pre-commit-config.yaml | 10 +- check.sh | 15 +- ci.sh | 44 +- pyproject.toml | 24 +- test-requirements.in | 24 -- test-requirements.txt | 101 ----- tests/test_async_clock.py | 2 +- tools/project_requirements.py | 106 ----- uv.lock | 762 +++++++++++++++++++++++++++++++++ 11 files changed, 836 insertions(+), 270 deletions(-) delete mode 100644 test-requirements.in delete mode 100644 test-requirements.txt delete mode 100755 tools/project_requirements.py create mode 100644 uv.lock diff --git a/.github/workflows/autodeps.yml b/.github/workflows/autodeps.yml index 7a08a2b..a41f4ff 100644 --- a/.github/workflows/autodeps.yml +++ b/.github/workflows/autodeps.yml @@ -30,21 +30,17 @@ jobs: - name: Bump dependencies run: | - python -m pip install -U pip pre-commit - python -m pip install -r test-requirements.txt - uv pip compile --universal --python-version=3.10 --upgrade test-requirements.in -o test-requirements.txt - pre-commit autoupdate --jobs 0 + python -m pip install -U uv + uv lock --upgrade + uv tool install pre-commit + uv run pre-commit autoupdate --jobs 0 - name: Install new requirements - run: python -m pip install -r test-requirements.txt + run: uv sync # apply newer versions' formatting - name: Pre-commit updates - run: pre-commit run -a - - - name: uv - run: | - uv pip compile --universal --python-version=3.10 test-requirements.in -o test-requirements.txt + run: uv run pre-commit run -a - name: Commit changes and create automerge PR env: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24e4a67..b56f28d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,7 @@ name: CI +permissions: {} + on: push: branches-ignore: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6f7f3e3..e689c73 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -43,14 +43,6 @@ repos: hooks: - id: typos - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.2.0 + rev: v1.2.1 hooks: - id: zizmor - - repo: local - hooks: - - id: project-requirements - name: regenerate requirements.in - language: python - entry: tools/project_requirements.py - pass_filenames: false - files: ^(test-requirements.in)|(pyproject.toml)$ diff --git a/check.sh b/check.sh index fb0c125..c382508 100755 --- a/check.sh +++ b/check.sh @@ -73,15 +73,15 @@ fi # Check pip compile is consistent echo "::group::Pip Compile - Tests" -uv pip compile --universal --python-version=3.10 test-requirements.in -o test-requirements.txt +uv lock echo "::endgroup::" -if git status --porcelain | grep -q "requirements.txt"; then - echo "::error::requirements.txt changed." - echo "::group::requirements.txt changed" - echo "* requirements.txt changed" >> "$GITHUB_STEP_SUMMARY" +if git status --porcelain | grep -q "uv.lock"; then + echo "::error::uv.lock changed." + echo "::group::uv.lock changed" + echo "* uv.lock changed" >> "$GITHUB_STEP_SUMMARY" git status --porcelain - git --no-pager diff --color ./*requirements.txt + git --no-pager diff --color ./*uv.lock EXIT_STATUS=1 echo "::endgroup::" fi @@ -97,9 +97,10 @@ if [ $EXIT_STATUS -ne 0 ]; then Problems were found by static analysis (listed above). To fix formatting and see remaining errors, run - uv pip install -r test-requirements.txt + uv sync --extra tools black src/$PROJECT ruff check src/$PROJECT + mypy ./check.sh in your local checkout. diff --git a/ci.sh b/ci.sh index 81cc25a..6fb887f 100755 --- a/ci.sh +++ b/ci.sh @@ -19,18 +19,36 @@ python -c "import sys, struct; print('python:', sys.version); print('version_inf echo "::endgroup::" echo "::group::Install dependencies" -python -m pip install -U pip uv -c test-requirements.txt +python -m pip install -U pip tomli python -m pip --version +UV_VERSION=$(python -c 'import tomli; from pathlib import Path; print({p["name"]:p for p in tomli.loads(Path("uv.lock").read_text())["package"]}["uv"]["version"])') +python -m pip install uv==$UV_VERSION python -m uv --version -python -m uv pip install build +UV_VENV_SEED="pip" +UV_VENV_OUTPUT="$(uv venv --seed --allow-existing 2>&1)" +echo "$UV_VENV_OUTPUT" -python -m build -wheel_package=$(ls dist/*.whl) -python -m uv pip install "$PROJECT @ $wheel_package" -c test-requirements.txt +# Extract the activation command from the output +activation_command=$(echo "$UV_VENV_OUTPUT" | grep -oP '(?<=Activate with: ).*') + +# Check if the activation command was found +if [ -n "$activation_command" ]; then + # Execute the activation command + echo "Activating virtual environment..." + eval "$activation_command" +else + echo "::error:: Activation command not found in uv venv output." + exit 1 +fi +python -m pip install uv==$UV_VERSION + +# python -m uv build +# wheel_package=$(ls dist/*.whl) +# python -m uv pip install "$PROJECT @ $wheel_package" -c test-requirements.txt if [ "$CHECK_FORMATTING" = "1" ]; then - python -m uv pip install -r test-requirements.txt exceptiongroup + python -m uv sync --extra tests --extra tools echo "::endgroup::" source check.sh else @@ -38,10 +56,12 @@ else # expands to 0 != 1 if NO_TEST_REQUIREMENTS is not set, if set the `-0` has no effect # https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_02 if [ "${NO_TEST_REQUIREMENTS-0}" == 1 ]; then - python -m uv pip install pytest coverage -c test-requirements.txt - flags="--skip-optional-imports" + # python -m uv pip install pytest coverage -c test-requirements.txt + python -m uv sync --extra tests + flags="" + #"--skip-optional-imports" else - python -m uv pip install -r test-requirements.txt + python -m uv sync --extra tests --extra tools flags="" fi @@ -71,10 +91,14 @@ else else PASSED=false fi + PREV_DIR="$PWD" + cd "$INSTALLDIR" + rm pyproject.toml + cd "$PREV_DIR" echo "::endgroup::" echo "::group::Coverage" - #coverage combine --rcfile ../pyproject.toml + coverage combine --rcfile ../pyproject.toml coverage report -m --rcfile ../pyproject.toml coverage xml --rcfile ../pyproject.toml diff --git a/pyproject.toml b/pyproject.toml index 413c237..ebdd41e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,6 @@ dependencies = [ "orjson>=3.10,<4", "pygame~=2.6.0", "trio~=0.28.0", - "typing_extensions>=4.12.2", ] [tool.setuptools.dynamic] @@ -53,12 +52,30 @@ version = {attr = "azul.game.__version__"} "Source" = "https://github.com/CoolCat467/Azul" "Bug Tracker" = "https://github.com/CoolCat467/Azul/issues" -[project.scripts] +[project.gui-scripts] azul_game = "azul.game:cli_run" +[project.optional-dependencies] +tests = [ + "pytest>=5.0", + "pytest-cov", + "pytest-trio", + "coverage>=7.2.5", + "uv>=0.5.21", + "mypy>=1.14.1", +] +tools = [ + 'black>=24.10.0; implementation_name == "cpython"', + "ruff>=0.9.2", + "codespell>=2.3.0", +] + [tool.setuptools.package-data] azul = ["py.typed", "data/*", "lang/*", "fonts/*"] +[tool.uv] +package = true + [tool.mypy] plugins = ["numpy.typing.mypy_plugin"] files = ["src/azul/", "computer_players"] @@ -152,6 +169,9 @@ source_pkgs = ["azul"] omit = [ "__init__.py", ] +parallel = true +relative_files = true +source = ["."] [tool.coverage.report] precision = 1 diff --git a/test-requirements.in b/test-requirements.in deleted file mode 100644 index 105217b..0000000 --- a/test-requirements.in +++ /dev/null @@ -1,24 +0,0 @@ -# For tests -pytest >= 5.0 -coverage >= 7.2.5 -pytest-trio -pytest-cov - -# Tools -black; implementation_name == "cpython" -mypy >= 1.14.0 -ruff >= 0.6.6 -uv >= 0.2.24 -codespell - -# Azul's own dependencies -# -exceptiongroup; python_version < '3.11' -libcomponent~=0.0.1 -mypy_extensions>=1.0.0 -numpy~=2.1.3 -orjson>=3.10,<4 -pygame~=2.6.0 -trio~=0.28.0 -typing_extensions>=4.12.2 -# diff --git a/test-requirements.txt b/test-requirements.txt deleted file mode 100644 index 6e0a404..0000000 --- a/test-requirements.txt +++ /dev/null @@ -1,101 +0,0 @@ -# This file was autogenerated by uv via the following command: -# uv pip compile --universal --python-version=3.10 test-requirements.in -o test-requirements.txt -attrs==24.3.0 - # via - # outcome - # trio -black==24.10.0 ; implementation_name == 'cpython' - # via -r test-requirements.in -cffi==1.17.1 ; (implementation_name != 'pypy' and os_name == 'nt') or platform_python_implementation != 'PyPy' - # via - # cryptography - # trio -click==8.1.8 ; implementation_name == 'cpython' - # via black -codespell==2.3.0 - # via -r test-requirements.in -colorama==0.4.6 ; sys_platform == 'win32' - # via - # click - # pytest -coverage==7.6.10 - # via - # -r test-requirements.in - # pytest-cov -cryptography==44.0.0 - # via libcomponent -exceptiongroup==1.2.2 ; python_full_version < '3.11' - # via - # -r test-requirements.in - # pytest - # trio -idna==3.10 - # via trio -iniconfig==2.0.0 - # via pytest -libcomponent==0.0.1 - # via -r test-requirements.in -mypy==1.14.1 - # via -r test-requirements.in -mypy-extensions==1.0.0 - # via - # -r test-requirements.in - # black - # libcomponent - # mypy -numpy==2.1.3 - # via -r test-requirements.in -orjson==3.10.14 - # via -r test-requirements.in -outcome==1.3.0.post0 - # via - # pytest-trio - # trio -packaging==24.2 - # via - # black - # pytest -pathspec==0.12.1 ; implementation_name == 'cpython' - # via black -platformdirs==4.3.6 ; implementation_name == 'cpython' - # via black -pluggy==1.5.0 - # via pytest -pycparser==2.22 ; (implementation_name != 'pypy' and os_name == 'nt') or platform_python_implementation != 'PyPy' - # via cffi -pygame==2.6.1 - # via -r test-requirements.in -pytest==8.3.4 - # via - # -r test-requirements.in - # pytest-cov - # pytest-trio -pytest-cov==6.0.0 - # via -r test-requirements.in -pytest-trio==0.8.0 - # via -r test-requirements.in -ruff==0.9.1 - # via -r test-requirements.in -sniffio==1.3.1 - # via trio -sortedcontainers==2.4.0 - # via trio -tomli==2.2.1 ; python_full_version <= '3.11' - # via - # black - # coverage - # mypy - # pytest -trio==0.28.0 - # via - # -r test-requirements.in - # libcomponent - # pytest-trio -typing-extensions==4.12.2 - # via - # -r test-requirements.in - # black - # libcomponent - # mypy -uv==0.5.18 - # via -r test-requirements.in diff --git a/tests/test_async_clock.py b/tests/test_async_clock.py index 4297787..fa8256d 100644 --- a/tests/test_async_clock.py +++ b/tests/test_async_clock.py @@ -56,7 +56,7 @@ async def test_tick(clock: Clock) -> None: @pytest.mark.trio async def test_tick_fps(clock: Clock) -> None: for _ in range(20): - await clock.tick(60) + await clock.tick(1024) fps = clock.get_fps() assert isinstance(fps, float) assert fps >= 0 diff --git a/tools/project_requirements.py b/tools/project_requirements.py deleted file mode 100755 index edcfbad..0000000 --- a/tools/project_requirements.py +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env python3 - -"""Project Requirements - Write test-requirements.in based on pyproject.toml.""" - -# Programmed by CoolCat467 - -from __future__ import annotations - -# Project Requirements - Write test-requirements.in based on pyproject.toml. -# Copyright (C) 2024 CoolCat467 -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -__title__ = "Project Requirements" -__author__ = "CoolCat467" -__version__ = "0.0.0" -__license__ = "GNU General Public License Version 3" - -import sys -from pathlib import Path -from typing import Final - -import tomllib - -# Key to start replacing inside of contents -KEY: Final = "TOML_DEPENDENCIES" - - -def run() -> None: - """Run program.""" - # Find root folder - this = Path(__file__).absolute() - tools = this.parent - root = tools.parent - # Make sure it's right - assert (root / "LICENSE").exists(), "Not in correct directory!" - - # Read pyproject.toml - pyproject = root / "pyproject.toml" - with pyproject.open("rb") as fp: - data = tomllib.load(fp) - - # Get dependencies list - assert isinstance(data, dict) - project = data["project"] - assert isinstance(project, dict) - dependencies = project["dependencies"] - assert isinstance(dependencies, list) - - # Read requirements file - requirements_list = root / "test-requirements.in" - assert requirements_list.exists(), f"{requirements_list} does not exist!" - requirements_data = requirements_list.read_text("utf-8") - - # Find out what start and end should be based on key. - key_start = f"<{KEY}>" - key_end = f"" - - # Try to find start and end triggers in requirements data - start_char = requirements_data.find(key_start) - end_char = requirements_data.find(key_end) - if -1 in {start_char, end_char}: - raise ValueError( - f"{key_start!r} or {key_end!r} not found in {requirements_list}", - ) - - # Create overwrite text - dependencies_text = "\n".join(sorted(dependencies)) - overwrite_text = "\n".join( - ( - key_start, - dependencies_text, - f"#{key_end}", - ), - ) - # Create new file contents - end = end_char + len(key_end) - new_text = ( - requirements_data[:start_char] - + overwrite_text - + requirements_data[end:] - ) - - # If new text differs, overwrite and alert - if new_text != requirements_data: - print("Requirements file is outdated...") - requirements_list.write_text(new_text, "utf-8") - print("Requirements file updated successfully.") - return 1 - print("Requirements file is up to date.") - return 0 - - -if __name__ == "__main__": - sys.exit(run()) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..a9fa758 --- /dev/null +++ b/uv.lock @@ -0,0 +1,762 @@ +version = 1 +requires-python = ">=3.10" + +[[package]] +name = "attrs" +version = "24.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/48/c8/6260f8ccc11f0917360fc0da435c5c9c7504e3db174d5a12a1494887b045/attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff", size = 805984 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/aa/ab0f7891a01eeb2d2e338ae8fecbe57fcebea1a24dbb64d45801bfab481d/attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308", size = 63397 }, +] + +[[package]] +name = "azul" +source = { editable = "." } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "libcomponent" }, + { name = "mypy-extensions" }, + { name = "numpy" }, + { name = "orjson" }, + { name = "pygame" }, + { name = "trio" }, +] + +[package.optional-dependencies] +tests = [ + { name = "coverage" }, + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-trio" }, + { name = "uv" }, +] +tools = [ + { name = "black", marker = "implementation_name == 'cpython'" }, + { name = "codespell" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "black", marker = "implementation_name == 'cpython' and extra == 'tools'", specifier = ">=24.10.0" }, + { name = "codespell", marker = "extra == 'tools'", specifier = ">=2.3.0" }, + { name = "coverage", marker = "extra == 'tests'", specifier = ">=7.2.5" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "libcomponent", specifier = "~=0.0.1" }, + { name = "mypy", marker = "extra == 'tests'", specifier = ">=1.14.1" }, + { name = "mypy-extensions", specifier = ">=1.0.0" }, + { name = "numpy", specifier = "~=2.1.3" }, + { name = "orjson", specifier = ">=3.10,<4" }, + { name = "pygame", specifier = "~=2.6.0" }, + { name = "pytest", marker = "extra == 'tests'", specifier = ">=5.0" }, + { name = "pytest-cov", marker = "extra == 'tests'" }, + { name = "pytest-trio", marker = "extra == 'tests'" }, + { name = "ruff", marker = "extra == 'tools'", specifier = ">=0.9.2" }, + { name = "trio", specifier = "~=0.28.0" }, + { name = "uv", marker = "extra == 'tests'", specifier = ">=0.5.21" }, +] + +[[package]] +name = "black" +version = "24.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/0d/cc2fb42b8c50d80143221515dd7e4766995bd07c56c9a3ed30baf080b6dc/black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875", size = 645813 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/f3/465c0eb5cddf7dbbfe1fecd9b875d1dcf51b88923cd2c1d7e9ab95c6336b/black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812", size = 1623211 }, + { url = "https://files.pythonhosted.org/packages/df/57/b6d2da7d200773fdfcc224ffb87052cf283cec4d7102fab450b4a05996d8/black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea", size = 1457139 }, + { url = "https://files.pythonhosted.org/packages/6e/c5/9023b7673904a5188f9be81f5e129fff69f51f5515655fbd1d5a4e80a47b/black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f", size = 1753774 }, + { url = "https://files.pythonhosted.org/packages/e1/32/df7f18bd0e724e0d9748829765455d6643ec847b3f87e77456fc99d0edab/black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e", size = 1414209 }, + { url = "https://files.pythonhosted.org/packages/c2/cc/7496bb63a9b06a954d3d0ac9fe7a73f3bf1cd92d7a58877c27f4ad1e9d41/black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad", size = 1607468 }, + { url = "https://files.pythonhosted.org/packages/2b/e3/69a738fb5ba18b5422f50b4f143544c664d7da40f09c13969b2fd52900e0/black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50", size = 1437270 }, + { url = "https://files.pythonhosted.org/packages/c9/9b/2db8045b45844665c720dcfe292fdaf2e49825810c0103e1191515fc101a/black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392", size = 1737061 }, + { url = "https://files.pythonhosted.org/packages/a3/95/17d4a09a5be5f8c65aa4a361444d95edc45def0de887810f508d3f65db7a/black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175", size = 1423293 }, + { url = "https://files.pythonhosted.org/packages/90/04/bf74c71f592bcd761610bbf67e23e6a3cff824780761f536512437f1e655/black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3", size = 1644256 }, + { url = "https://files.pythonhosted.org/packages/4c/ea/a77bab4cf1887f4b2e0bce5516ea0b3ff7d04ba96af21d65024629afedb6/black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65", size = 1448534 }, + { url = "https://files.pythonhosted.org/packages/4e/3e/443ef8bc1fbda78e61f79157f303893f3fddf19ca3c8989b163eb3469a12/black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f", size = 1761892 }, + { url = "https://files.pythonhosted.org/packages/52/93/eac95ff229049a6901bc84fec6908a5124b8a0b7c26ea766b3b8a5debd22/black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8", size = 1434796 }, + { url = "https://files.pythonhosted.org/packages/d0/a0/a993f58d4ecfba035e61fca4e9f64a2ecae838fc9f33ab798c62173ed75c/black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981", size = 1643986 }, + { url = "https://files.pythonhosted.org/packages/37/d5/602d0ef5dfcace3fb4f79c436762f130abd9ee8d950fa2abdbf8bbc555e0/black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b", size = 1448085 }, + { url = "https://files.pythonhosted.org/packages/47/6d/a3a239e938960df1a662b93d6230d4f3e9b4a22982d060fc38c42f45a56b/black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2", size = 1760928 }, + { url = "https://files.pythonhosted.org/packages/dd/cf/af018e13b0eddfb434df4d9cd1b2b7892bab119f7a20123e93f6910982e8/black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b", size = 1436875 }, + { url = "https://files.pythonhosted.org/packages/8d/a7/4b27c50537ebca8bec139b872861f9d2bf501c5ec51fcf897cb924d9e264/black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d", size = 206898 }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191 }, + { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592 }, + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 }, + { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 }, + { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 }, + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, +] + +[[package]] +name = "codespell" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a0/a9/98353dfc7afcdf18cffd2dd3e959a25eaaf2728cf450caa59af89648a8e4/codespell-2.3.0.tar.gz", hash = "sha256:360c7d10f75e65f67bad720af7007e1060a5d395670ec11a7ed1fed9dd17471f", size = 329791 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/20/b6019add11e84f821184234cea0ad91442373489ef7ccfa3d73a71b908fa/codespell-2.3.0-py3-none-any.whl", hash = "sha256:a9c7cef2501c9cfede2110fd6d4e5e62296920efe9abfb84648df866e47f58d1", size = 329167 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "coverage" +version = "7.6.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/84/ba/ac14d281f80aab516275012e8875991bb06203957aa1e19950139238d658/coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23", size = 803868 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/12/2a2a923edf4ddabdffed7ad6da50d96a5c126dae7b80a33df7310e329a1e/coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78", size = 207982 }, + { url = "https://files.pythonhosted.org/packages/ca/49/6985dbca9c7be3f3cb62a2e6e492a0c88b65bf40579e16c71ae9c33c6b23/coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c", size = 208414 }, + { url = "https://files.pythonhosted.org/packages/35/93/287e8f1d1ed2646f4e0b2605d14616c9a8a2697d0d1b453815eb5c6cebdb/coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a", size = 236860 }, + { url = "https://files.pythonhosted.org/packages/de/e1/cfdb5627a03567a10031acc629b75d45a4ca1616e54f7133ca1fa366050a/coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165", size = 234758 }, + { url = "https://files.pythonhosted.org/packages/6d/85/fc0de2bcda3f97c2ee9fe8568f7d48f7279e91068958e5b2cc19e0e5f600/coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988", size = 235920 }, + { url = "https://files.pythonhosted.org/packages/79/73/ef4ea0105531506a6f4cf4ba571a214b14a884630b567ed65b3d9c1975e1/coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5", size = 234986 }, + { url = "https://files.pythonhosted.org/packages/c6/4d/75afcfe4432e2ad0405c6f27adeb109ff8976c5e636af8604f94f29fa3fc/coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3", size = 233446 }, + { url = "https://files.pythonhosted.org/packages/86/5b/efee56a89c16171288cafff022e8af44f8f94075c2d8da563c3935212871/coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5", size = 234566 }, + { url = "https://files.pythonhosted.org/packages/f2/db/67770cceb4a64d3198bf2aa49946f411b85ec6b0a9b489e61c8467a4253b/coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244", size = 210675 }, + { url = "https://files.pythonhosted.org/packages/8d/27/e8bfc43f5345ec2c27bc8a1fa77cdc5ce9dcf954445e11f14bb70b889d14/coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e", size = 211518 }, + { url = "https://files.pythonhosted.org/packages/85/d2/5e175fcf6766cf7501a8541d81778fd2f52f4870100e791f5327fd23270b/coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3", size = 208088 }, + { url = "https://files.pythonhosted.org/packages/4b/6f/06db4dc8fca33c13b673986e20e466fd936235a6ec1f0045c3853ac1b593/coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43", size = 208536 }, + { url = "https://files.pythonhosted.org/packages/0d/62/c6a0cf80318c1c1af376d52df444da3608eafc913b82c84a4600d8349472/coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132", size = 240474 }, + { url = "https://files.pythonhosted.org/packages/a3/59/750adafc2e57786d2e8739a46b680d4fb0fbc2d57fbcb161290a9f1ecf23/coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f", size = 237880 }, + { url = "https://files.pythonhosted.org/packages/2c/f8/ef009b3b98e9f7033c19deb40d629354aab1d8b2d7f9cfec284dbedf5096/coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994", size = 239750 }, + { url = "https://files.pythonhosted.org/packages/a6/e2/6622f3b70f5f5b59f705e680dae6db64421af05a5d1e389afd24dae62e5b/coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99", size = 238642 }, + { url = "https://files.pythonhosted.org/packages/2d/10/57ac3f191a3c95c67844099514ff44e6e19b2915cd1c22269fb27f9b17b6/coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd", size = 237266 }, + { url = "https://files.pythonhosted.org/packages/ee/2d/7016f4ad9d553cabcb7333ed78ff9d27248ec4eba8dd21fa488254dff894/coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377", size = 238045 }, + { url = "https://files.pythonhosted.org/packages/a7/fe/45af5c82389a71e0cae4546413266d2195c3744849669b0bab4b5f2c75da/coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8", size = 210647 }, + { url = "https://files.pythonhosted.org/packages/db/11/3f8e803a43b79bc534c6a506674da9d614e990e37118b4506faf70d46ed6/coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609", size = 211508 }, + { url = "https://files.pythonhosted.org/packages/86/77/19d09ea06f92fdf0487499283b1b7af06bc422ea94534c8fe3a4cd023641/coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853", size = 208281 }, + { url = "https://files.pythonhosted.org/packages/b6/67/5479b9f2f99fcfb49c0d5cf61912a5255ef80b6e80a3cddba39c38146cf4/coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078", size = 208514 }, + { url = "https://files.pythonhosted.org/packages/15/d1/febf59030ce1c83b7331c3546d7317e5120c5966471727aa7ac157729c4b/coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0", size = 241537 }, + { url = "https://files.pythonhosted.org/packages/4b/7e/5ac4c90192130e7cf8b63153fe620c8bfd9068f89a6d9b5f26f1550f7a26/coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50", size = 238572 }, + { url = "https://files.pythonhosted.org/packages/dc/03/0334a79b26ecf59958f2fe9dd1f5ab3e2f88db876f5071933de39af09647/coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022", size = 240639 }, + { url = "https://files.pythonhosted.org/packages/d7/45/8a707f23c202208d7b286d78ad6233f50dcf929319b664b6cc18a03c1aae/coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b", size = 240072 }, + { url = "https://files.pythonhosted.org/packages/66/02/603ce0ac2d02bc7b393279ef618940b4a0535b0868ee791140bda9ecfa40/coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0", size = 238386 }, + { url = "https://files.pythonhosted.org/packages/04/62/4e6887e9be060f5d18f1dd58c2838b2d9646faf353232dec4e2d4b1c8644/coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852", size = 240054 }, + { url = "https://files.pythonhosted.org/packages/5c/74/83ae4151c170d8bd071924f212add22a0e62a7fe2b149edf016aeecad17c/coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359", size = 210904 }, + { url = "https://files.pythonhosted.org/packages/c3/54/de0893186a221478f5880283119fc40483bc460b27c4c71d1b8bba3474b9/coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247", size = 211692 }, + { url = "https://files.pythonhosted.org/packages/25/6d/31883d78865529257bf847df5789e2ae80e99de8a460c3453dbfbe0db069/coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9", size = 208308 }, + { url = "https://files.pythonhosted.org/packages/70/22/3f2b129cc08de00c83b0ad6252e034320946abfc3e4235c009e57cfeee05/coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b", size = 208565 }, + { url = "https://files.pythonhosted.org/packages/97/0a/d89bc2d1cc61d3a8dfe9e9d75217b2be85f6c73ebf1b9e3c2f4e797f4531/coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690", size = 241083 }, + { url = "https://files.pythonhosted.org/packages/4c/81/6d64b88a00c7a7aaed3a657b8eaa0931f37a6395fcef61e53ff742b49c97/coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18", size = 238235 }, + { url = "https://files.pythonhosted.org/packages/9a/0b/7797d4193f5adb4b837207ed87fecf5fc38f7cc612b369a8e8e12d9fa114/coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c", size = 240220 }, + { url = "https://files.pythonhosted.org/packages/65/4d/6f83ca1bddcf8e51bf8ff71572f39a1c73c34cf50e752a952c34f24d0a60/coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd", size = 239847 }, + { url = "https://files.pythonhosted.org/packages/30/9d/2470df6aa146aff4c65fee0f87f58d2164a67533c771c9cc12ffcdb865d5/coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e", size = 237922 }, + { url = "https://files.pythonhosted.org/packages/08/dd/723fef5d901e6a89f2507094db66c091449c8ba03272861eaefa773ad95c/coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694", size = 239783 }, + { url = "https://files.pythonhosted.org/packages/3d/f7/64d3298b2baf261cb35466000628706ce20a82d42faf9b771af447cd2b76/coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6", size = 210965 }, + { url = "https://files.pythonhosted.org/packages/d5/58/ec43499a7fc681212fe7742fe90b2bc361cdb72e3181ace1604247a5b24d/coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e", size = 211719 }, + { url = "https://files.pythonhosted.org/packages/ab/c9/f2857a135bcff4330c1e90e7d03446b036b2363d4ad37eb5e3a47bbac8a6/coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe", size = 209050 }, + { url = "https://files.pythonhosted.org/packages/aa/b3/f840e5bd777d8433caa9e4a1eb20503495709f697341ac1a8ee6a3c906ad/coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273", size = 209321 }, + { url = "https://files.pythonhosted.org/packages/85/7d/125a5362180fcc1c03d91850fc020f3831d5cda09319522bcfa6b2b70be7/coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8", size = 252039 }, + { url = "https://files.pythonhosted.org/packages/a9/9c/4358bf3c74baf1f9bddd2baf3756b54c07f2cfd2535f0a47f1e7757e54b3/coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098", size = 247758 }, + { url = "https://files.pythonhosted.org/packages/cf/c7/de3eb6fc5263b26fab5cda3de7a0f80e317597a4bad4781859f72885f300/coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb", size = 250119 }, + { url = "https://files.pythonhosted.org/packages/3e/e6/43de91f8ba2ec9140c6a4af1102141712949903dc732cf739167cfa7a3bc/coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0", size = 249597 }, + { url = "https://files.pythonhosted.org/packages/08/40/61158b5499aa2adf9e37bc6d0117e8f6788625b283d51e7e0c53cf340530/coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf", size = 247473 }, + { url = "https://files.pythonhosted.org/packages/50/69/b3f2416725621e9f112e74e8470793d5b5995f146f596f133678a633b77e/coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2", size = 248737 }, + { url = "https://files.pythonhosted.org/packages/3c/6e/fe899fb937657db6df31cc3e61c6968cb56d36d7326361847440a430152e/coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312", size = 211611 }, + { url = "https://files.pythonhosted.org/packages/1c/55/52f5e66142a9d7bc93a15192eba7a78513d2abf6b3558d77b4ca32f5f424/coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d", size = 212781 }, + { url = "https://files.pythonhosted.org/packages/a1/70/de81bfec9ed38a64fc44a77c7665e20ca507fc3265597c28b0d989e4082e/coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f", size = 200223 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "44.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/4c/45dfa6829acffa344e3967d6006ee4ae8be57af746ae2eba1c431949b32c/cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02", size = 710657 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/09/8cc67f9b84730ad330b3b72cf867150744bf07ff113cda21a15a1c6d2c7c/cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123", size = 6541833 }, + { url = "https://files.pythonhosted.org/packages/7e/5b/3759e30a103144e29632e7cb72aec28cedc79e514b2ea8896bb17163c19b/cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092", size = 3922710 }, + { url = "https://files.pythonhosted.org/packages/5f/58/3b14bf39f1a0cfd679e753e8647ada56cddbf5acebffe7db90e184c76168/cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f", size = 4137546 }, + { url = "https://files.pythonhosted.org/packages/98/65/13d9e76ca19b0ba5603d71ac8424b5694415b348e719db277b5edc985ff5/cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb", size = 3915420 }, + { url = "https://files.pythonhosted.org/packages/b1/07/40fe09ce96b91fc9276a9ad272832ead0fddedcba87f1190372af8e3039c/cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b", size = 4154498 }, + { url = "https://files.pythonhosted.org/packages/75/ea/af65619c800ec0a7e4034207aec543acdf248d9bffba0533342d1bd435e1/cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543", size = 3932569 }, + { url = "https://files.pythonhosted.org/packages/c7/af/d1deb0c04d59612e3d5e54203159e284d3e7a6921e565bb0eeb6269bdd8a/cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e", size = 4016721 }, + { url = "https://files.pythonhosted.org/packages/bd/69/7ca326c55698d0688db867795134bdfac87136b80ef373aaa42b225d6dd5/cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e", size = 4240915 }, + { url = "https://files.pythonhosted.org/packages/ef/d4/cae11bf68c0f981e0413906c6dd03ae7fa864347ed5fac40021df1ef467c/cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053", size = 2757925 }, + { url = "https://files.pythonhosted.org/packages/64/b1/50d7739254d2002acae64eed4fc43b24ac0cc44bf0a0d388d1ca06ec5bb1/cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd", size = 3202055 }, + { url = "https://files.pythonhosted.org/packages/11/18/61e52a3d28fc1514a43b0ac291177acd1b4de00e9301aaf7ef867076ff8a/cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591", size = 6542801 }, + { url = "https://files.pythonhosted.org/packages/1a/07/5f165b6c65696ef75601b781a280fc3b33f1e0cd6aa5a92d9fb96c410e97/cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7", size = 3922613 }, + { url = "https://files.pythonhosted.org/packages/28/34/6b3ac1d80fc174812486561cf25194338151780f27e438526f9c64e16869/cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc", size = 4137925 }, + { url = "https://files.pythonhosted.org/packages/d0/c7/c656eb08fd22255d21bc3129625ed9cd5ee305f33752ef2278711b3fa98b/cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289", size = 3915417 }, + { url = "https://files.pythonhosted.org/packages/ef/82/72403624f197af0db6bac4e58153bc9ac0e6020e57234115db9596eee85d/cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7", size = 4155160 }, + { url = "https://files.pythonhosted.org/packages/a2/cd/2f3c440913d4329ade49b146d74f2e9766422e1732613f57097fea61f344/cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c", size = 3932331 }, + { url = "https://files.pythonhosted.org/packages/7f/df/8be88797f0a1cca6e255189a57bb49237402b1880d6e8721690c5603ac23/cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64", size = 4017372 }, + { url = "https://files.pythonhosted.org/packages/af/36/5ccc376f025a834e72b8e52e18746b927f34e4520487098e283a719c205e/cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285", size = 4239657 }, + { url = "https://files.pythonhosted.org/packages/46/b0/f4f7d0d0bcfbc8dd6296c1449be326d04217c57afb8b2594f017eed95533/cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417", size = 2758672 }, + { url = "https://files.pythonhosted.org/packages/97/9b/443270b9210f13f6ef240eff73fd32e02d381e7103969dc66ce8e89ee901/cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede", size = 3202071 }, + { url = "https://files.pythonhosted.org/packages/77/d4/fea74422326388bbac0c37b7489a0fcb1681a698c3b875959430ba550daa/cryptography-44.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731", size = 3338857 }, + { url = "https://files.pythonhosted.org/packages/1a/aa/ba8a7467c206cb7b62f09b4168da541b5109838627f582843bbbe0235e8e/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4", size = 3850615 }, + { url = "https://files.pythonhosted.org/packages/89/fa/b160e10a64cc395d090105be14f399b94e617c879efd401188ce0fea39ee/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756", size = 4081622 }, + { url = "https://files.pythonhosted.org/packages/47/8f/20ff0656bb0cf7af26ec1d01f780c5cfbaa7666736063378c5f48558b515/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c", size = 3867546 }, + { url = "https://files.pythonhosted.org/packages/38/d9/28edf32ee2fcdca587146bcde90102a7319b2f2c690edfa627e46d586050/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa", size = 4090937 }, + { url = "https://files.pythonhosted.org/packages/cc/9d/37e5da7519de7b0b070a3fedd4230fe76d50d2a21403e0f2153d70ac4163/cryptography-44.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c", size = 3128774 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "libcomponent" +version = "0.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "mypy-extensions" }, + { name = "trio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/40/30e51ad19bc3420a61e60c300c2bc4a9f685ba587bdcf8e852dd5a3a9a40/libcomponent-0.0.1.tar.gz", hash = "sha256:fdebdc9b4857707511c3b2bf0a434af30910b19fa0ec1ddb3e78457d074e40f1", size = 74693 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/2f/404652eae9da1f1216c66e57d016debaec8ec927fcd2a2c3c600629ceb25/libcomponent-0.0.1-py3-none-any.whl", hash = "sha256:be0fe9fffcebe4602656d55fb1c1d8afa230552d84cf1d69300b23d44e19369a", size = 57644 }, +] + +[[package]] +name = "mypy" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/7a/87ae2adb31d68402da6da1e5f30c07ea6063e9f09b5e7cfc9dfa44075e74/mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb", size = 11211002 }, + { url = "https://files.pythonhosted.org/packages/e1/23/eada4c38608b444618a132be0d199b280049ded278b24cbb9d3fc59658e4/mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0", size = 10358400 }, + { url = "https://files.pythonhosted.org/packages/43/c9/d6785c6f66241c62fd2992b05057f404237deaad1566545e9f144ced07f5/mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d", size = 12095172 }, + { url = "https://files.pythonhosted.org/packages/c3/62/daa7e787770c83c52ce2aaf1a111eae5893de9e004743f51bfcad9e487ec/mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b", size = 12828732 }, + { url = "https://files.pythonhosted.org/packages/1b/a2/5fb18318a3637f29f16f4e41340b795da14f4751ef4f51c99ff39ab62e52/mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427", size = 13012197 }, + { url = "https://files.pythonhosted.org/packages/28/99/e153ce39105d164b5f02c06c35c7ba958aaff50a2babba7d080988b03fe7/mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f", size = 9780836 }, + { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432 }, + { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515 }, + { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791 }, + { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203 }, + { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900 }, + { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869 }, + { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668 }, + { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060 }, + { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167 }, + { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341 }, + { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991 }, + { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016 }, + { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097 }, + { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728 }, + { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965 }, + { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660 }, + { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198 }, + { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276 }, + { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, +] + +[[package]] +name = "numpy" +version = "2.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ca/1166b75c21abd1da445b97bf1fa2f14f423c6cfb4fc7c4ef31dccf9f6a94/numpy-2.1.3.tar.gz", hash = "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761", size = 20166090 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/80/d572a4737626372915bca41c3afbfec9d173561a39a0a61bacbbfd1dafd4/numpy-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c894b4305373b9c5576d7a12b473702afdf48ce5369c074ba304cc5ad8730dff", size = 21152472 }, + { url = "https://files.pythonhosted.org/packages/6f/bb/7bfba10c791ae3bb6716da77ad85a82d5fac07fc96fb0023ef0571df9d20/numpy-2.1.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b47fbb433d3260adcd51eb54f92a2ffbc90a4595f8970ee00e064c644ac788f5", size = 13747967 }, + { url = "https://files.pythonhosted.org/packages/da/d6/2df7bde35f0478455f0be5934877b3e5a505f587b00230f54a519a6b55a5/numpy-2.1.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:825656d0743699c529c5943554d223c021ff0494ff1442152ce887ef4f7561a1", size = 5354921 }, + { url = "https://files.pythonhosted.org/packages/d1/bb/75b945874f931494891eac6ca06a1764d0e8208791f3addadb2963b83527/numpy-2.1.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:6a4825252fcc430a182ac4dee5a505053d262c807f8a924603d411f6718b88fd", size = 6888603 }, + { url = "https://files.pythonhosted.org/packages/68/a7/fde73636f6498dbfa6d82fc336164635fe592f1ad0d13285fcb6267fdc1c/numpy-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e711e02f49e176a01d0349d82cb5f05ba4db7d5e7e0defd026328e5cfb3226d3", size = 13889862 }, + { url = "https://files.pythonhosted.org/packages/05/db/5d9c91b2e1e2e72be1369278f696356d44975befcae830daf2e667dcb54f/numpy-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78574ac2d1a4a02421f25da9559850d59457bac82f2b8d7a44fe83a64f770098", size = 16328151 }, + { url = "https://files.pythonhosted.org/packages/3e/6a/7eb732109b53ae64a29e25d7e68eb9d6611037f6354875497008a49e74d3/numpy-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c7662f0e3673fe4e832fe07b65c50342ea27d989f92c80355658c7f888fcc83c", size = 16704107 }, + { url = "https://files.pythonhosted.org/packages/88/cc/278113b66a1141053cbda6f80e4200c6da06b3079c2d27bda1fde41f2c1f/numpy-2.1.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fa2d1337dc61c8dc417fbccf20f6d1e139896a30721b7f1e832b2bb6ef4eb6c4", size = 14385789 }, + { url = "https://files.pythonhosted.org/packages/f5/69/eb20f5e1bfa07449bc67574d2f0f7c1e6b335fb41672e43861a7727d85f2/numpy-2.1.3-cp310-cp310-win32.whl", hash = "sha256:72dcc4a35a8515d83e76b58fdf8113a5c969ccd505c8a946759b24e3182d1f23", size = 6536706 }, + { url = "https://files.pythonhosted.org/packages/8e/8b/1c131ab5a94c1086c289c6e1da1d843de9dbd95fe5f5ee6e61904c9518e2/numpy-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:ecc76a9ba2911d8d37ac01de72834d8849e55473457558e12995f4cd53e778e0", size = 12864165 }, + { url = "https://files.pythonhosted.org/packages/ad/81/c8167192eba5247593cd9d305ac236847c2912ff39e11402e72ae28a4985/numpy-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4d1167c53b93f1f5d8a139a742b3c6f4d429b54e74e6b57d0eff40045187b15d", size = 21156252 }, + { url = "https://files.pythonhosted.org/packages/da/74/5a60003fc3d8a718d830b08b654d0eea2d2db0806bab8f3c2aca7e18e010/numpy-2.1.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c80e4a09b3d95b4e1cac08643f1152fa71a0a821a2d4277334c88d54b2219a41", size = 13784119 }, + { url = "https://files.pythonhosted.org/packages/47/7c/864cb966b96fce5e63fcf25e1e4d957fe5725a635e5f11fe03f39dd9d6b5/numpy-2.1.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:576a1c1d25e9e02ed7fa5477f30a127fe56debd53b8d2c89d5578f9857d03ca9", size = 5352978 }, + { url = "https://files.pythonhosted.org/packages/09/ac/61d07930a4993dd9691a6432de16d93bbe6aa4b1c12a5e573d468eefc1ca/numpy-2.1.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:973faafebaae4c0aaa1a1ca1ce02434554d67e628b8d805e61f874b84e136b09", size = 6892570 }, + { url = "https://files.pythonhosted.org/packages/27/2f/21b94664f23af2bb52030653697c685022119e0dc93d6097c3cb45bce5f9/numpy-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:762479be47a4863e261a840e8e01608d124ee1361e48b96916f38b119cfda04a", size = 13896715 }, + { url = "https://files.pythonhosted.org/packages/7a/f0/80811e836484262b236c684a75dfc4ba0424bc670e765afaa911468d9f39/numpy-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc6f24b3d1ecc1eebfbf5d6051faa49af40b03be1aaa781ebdadcbc090b4539b", size = 16339644 }, + { url = "https://files.pythonhosted.org/packages/fa/81/ce213159a1ed8eb7d88a2a6ef4fbdb9e4ffd0c76b866c350eb4e3c37e640/numpy-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:17ee83a1f4fef3c94d16dc1802b998668b5419362c8a4f4e8a491de1b41cc3ee", size = 16712217 }, + { url = "https://files.pythonhosted.org/packages/7d/84/4de0b87d5a72f45556b2a8ee9fc8801e8518ec867fc68260c1f5dcb3903f/numpy-2.1.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15cb89f39fa6d0bdfb600ea24b250e5f1a3df23f901f51c8debaa6a5d122b2f0", size = 14399053 }, + { url = "https://files.pythonhosted.org/packages/7e/1c/e5fabb9ad849f9d798b44458fd12a318d27592d4bc1448e269dec070ff04/numpy-2.1.3-cp311-cp311-win32.whl", hash = "sha256:d9beb777a78c331580705326d2367488d5bc473b49a9bc3036c154832520aca9", size = 6534741 }, + { url = "https://files.pythonhosted.org/packages/1e/48/a9a4b538e28f854bfb62e1dea3c8fea12e90216a276c7777ae5345ff29a7/numpy-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:d89dd2b6da69c4fff5e39c28a382199ddedc3a5be5390115608345dec660b9e2", size = 12869487 }, + { url = "https://files.pythonhosted.org/packages/8a/f0/385eb9970309643cbca4fc6eebc8bb16e560de129c91258dfaa18498da8b/numpy-2.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f55ba01150f52b1027829b50d70ef1dafd9821ea82905b63936668403c3b471e", size = 20849658 }, + { url = "https://files.pythonhosted.org/packages/54/4a/765b4607f0fecbb239638d610d04ec0a0ded9b4951c56dc68cef79026abf/numpy-2.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13138eadd4f4da03074851a698ffa7e405f41a0845a6b1ad135b81596e4e9958", size = 13492258 }, + { url = "https://files.pythonhosted.org/packages/bd/a7/2332679479c70b68dccbf4a8eb9c9b5ee383164b161bee9284ac141fbd33/numpy-2.1.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a6b46587b14b888e95e4a24d7b13ae91fa22386c199ee7b418f449032b2fa3b8", size = 5090249 }, + { url = "https://files.pythonhosted.org/packages/c1/67/4aa00316b3b981a822c7a239d3a8135be2a6945d1fd11d0efb25d361711a/numpy-2.1.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:0fa14563cc46422e99daef53d725d0c326e99e468a9320a240affffe87852564", size = 6621704 }, + { url = "https://files.pythonhosted.org/packages/5e/da/1a429ae58b3b6c364eeec93bf044c532f2ff7b48a52e41050896cf15d5b1/numpy-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8637dcd2caa676e475503d1f8fdb327bc495554e10838019651b76d17b98e512", size = 13606089 }, + { url = "https://files.pythonhosted.org/packages/9e/3e/3757f304c704f2f0294a6b8340fcf2be244038be07da4cccf390fa678a9f/numpy-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2312b2aa89e1f43ecea6da6ea9a810d06aae08321609d8dc0d0eda6d946a541b", size = 16043185 }, + { url = "https://files.pythonhosted.org/packages/43/97/75329c28fea3113d00c8d2daf9bc5828d58d78ed661d8e05e234f86f0f6d/numpy-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a38c19106902bb19351b83802531fea19dee18e5b37b36454f27f11ff956f7fc", size = 16410751 }, + { url = "https://files.pythonhosted.org/packages/ad/7a/442965e98b34e0ae9da319f075b387bcb9a1e0658276cc63adb8c9686f7b/numpy-2.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02135ade8b8a84011cbb67dc44e07c58f28575cf9ecf8ab304e51c05528c19f0", size = 14082705 }, + { url = "https://files.pythonhosted.org/packages/ac/b6/26108cf2cfa5c7e03fb969b595c93131eab4a399762b51ce9ebec2332e80/numpy-2.1.3-cp312-cp312-win32.whl", hash = "sha256:e6988e90fcf617da2b5c78902fe8e668361b43b4fe26dbf2d7b0f8034d4cafb9", size = 6239077 }, + { url = "https://files.pythonhosted.org/packages/a6/84/fa11dad3404b7634aaab50733581ce11e5350383311ea7a7010f464c0170/numpy-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:0d30c543f02e84e92c4b1f415b7c6b5326cbe45ee7882b6b77db7195fb971e3a", size = 12566858 }, + { url = "https://files.pythonhosted.org/packages/4d/0b/620591441457e25f3404c8057eb924d04f161244cb8a3680d529419aa86e/numpy-2.1.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96fe52fcdb9345b7cd82ecd34547fca4321f7656d500eca497eb7ea5a926692f", size = 20836263 }, + { url = "https://files.pythonhosted.org/packages/45/e1/210b2d8b31ce9119145433e6ea78046e30771de3fe353f313b2778142f34/numpy-2.1.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f653490b33e9c3a4c1c01d41bc2aef08f9475af51146e4a7710c450cf9761598", size = 13507771 }, + { url = "https://files.pythonhosted.org/packages/55/44/aa9ee3caee02fa5a45f2c3b95cafe59c44e4b278fbbf895a93e88b308555/numpy-2.1.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dc258a761a16daa791081d026f0ed4399b582712e6fc887a95af09df10c5ca57", size = 5075805 }, + { url = "https://files.pythonhosted.org/packages/78/d6/61de6e7e31915ba4d87bbe1ae859e83e6582ea14c6add07c8f7eefd8488f/numpy-2.1.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:016d0f6f5e77b0f0d45d77387ffa4bb89816b57c835580c3ce8e099ef830befe", size = 6608380 }, + { url = "https://files.pythonhosted.org/packages/3e/46/48bdf9b7241e317e6cf94276fe11ba673c06d1fdf115d8b4ebf616affd1a/numpy-2.1.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c181ba05ce8299c7aa3125c27b9c2167bca4a4445b7ce73d5febc411ca692e43", size = 13602451 }, + { url = "https://files.pythonhosted.org/packages/70/50/73f9a5aa0810cdccda9c1d20be3cbe4a4d6ea6bfd6931464a44c95eef731/numpy-2.1.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5641516794ca9e5f8a4d17bb45446998c6554704d888f86df9b200e66bdcce56", size = 16039822 }, + { url = "https://files.pythonhosted.org/packages/ad/cd/098bc1d5a5bc5307cfc65ee9369d0ca658ed88fbd7307b0d49fab6ca5fa5/numpy-2.1.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ea4dedd6e394a9c180b33c2c872b92f7ce0f8e7ad93e9585312b0c5a04777a4a", size = 16411822 }, + { url = "https://files.pythonhosted.org/packages/83/a2/7d4467a2a6d984549053b37945620209e702cf96a8bc658bc04bba13c9e2/numpy-2.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0df3635b9c8ef48bd3be5f862cf71b0a4716fa0e702155c45067c6b711ddcef", size = 14079598 }, + { url = "https://files.pythonhosted.org/packages/e9/6a/d64514dcecb2ee70bfdfad10c42b76cab657e7ee31944ff7a600f141d9e9/numpy-2.1.3-cp313-cp313-win32.whl", hash = "sha256:50ca6aba6e163363f132b5c101ba078b8cbd3fa92c7865fd7d4d62d9779ac29f", size = 6236021 }, + { url = "https://files.pythonhosted.org/packages/bb/f9/12297ed8d8301a401e7d8eb6b418d32547f1d700ed3c038d325a605421a4/numpy-2.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:747641635d3d44bcb380d950679462fae44f54b131be347d5ec2bce47d3df9ed", size = 12560405 }, + { url = "https://files.pythonhosted.org/packages/a7/45/7f9244cd792e163b334e3a7f02dff1239d2890b6f37ebf9e82cbe17debc0/numpy-2.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:996bb9399059c5b82f76b53ff8bb686069c05acc94656bb259b1d63d04a9506f", size = 20859062 }, + { url = "https://files.pythonhosted.org/packages/b1/b4/a084218e7e92b506d634105b13e27a3a6645312b93e1c699cc9025adb0e1/numpy-2.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:45966d859916ad02b779706bb43b954281db43e185015df6eb3323120188f9e4", size = 13515839 }, + { url = "https://files.pythonhosted.org/packages/27/45/58ed3f88028dcf80e6ea580311dc3edefdd94248f5770deb980500ef85dd/numpy-2.1.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:baed7e8d7481bfe0874b566850cb0b85243e982388b7b23348c6db2ee2b2ae8e", size = 5116031 }, + { url = "https://files.pythonhosted.org/packages/37/a8/eb689432eb977d83229094b58b0f53249d2209742f7de529c49d61a124a0/numpy-2.1.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f7f672a3388133335589cfca93ed468509cb7b93ba3105fce780d04a6576a0", size = 6629977 }, + { url = "https://files.pythonhosted.org/packages/42/a3/5355ad51ac73c23334c7caaed01adadfda49544f646fcbfbb4331deb267b/numpy-2.1.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7aac50327da5d208db2eec22eb11e491e3fe13d22653dce51b0f4109101b408", size = 13575951 }, + { url = "https://files.pythonhosted.org/packages/c4/70/ea9646d203104e647988cb7d7279f135257a6b7e3354ea6c56f8bafdb095/numpy-2.1.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4394bc0dbd074b7f9b52024832d16e019decebf86caf909d94f6b3f77a8ee3b6", size = 16022655 }, + { url = "https://files.pythonhosted.org/packages/14/ce/7fc0612903e91ff9d0b3f2eda4e18ef9904814afcae5b0f08edb7f637883/numpy-2.1.3-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:50d18c4358a0a8a53f12a8ba9d772ab2d460321e6a93d6064fc22443d189853f", size = 16399902 }, + { url = "https://files.pythonhosted.org/packages/ef/62/1d3204313357591c913c32132a28f09a26357e33ea3c4e2fe81269e0dca1/numpy-2.1.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:14e253bd43fc6b37af4921b10f6add6925878a42a0c5fe83daee390bca80bc17", size = 14067180 }, + { url = "https://files.pythonhosted.org/packages/24/d7/78a40ed1d80e23a774cb8a34ae8a9493ba1b4271dde96e56ccdbab1620ef/numpy-2.1.3-cp313-cp313t-win32.whl", hash = "sha256:08788d27a5fd867a663f6fc753fd7c3ad7e92747efc73c53bca2f19f8bc06f48", size = 6291907 }, + { url = "https://files.pythonhosted.org/packages/86/09/a5ab407bd7f5f5599e6a9261f964ace03a73e7c6928de906981c31c38082/numpy-2.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2564fbdf2b99b3f815f2107c1bbc93e2de8ee655a69c261363a1172a79a257d4", size = 12644098 }, + { url = "https://files.pythonhosted.org/packages/00/e7/8d8bb791b62586cc432ecbb70632b4f23b7b7c88df41878de7528264f6d7/numpy-2.1.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4f2015dfe437dfebbfce7c85c7b53d81ba49e71ba7eadbf1df40c915af75979f", size = 20983893 }, + { url = "https://files.pythonhosted.org/packages/5e/f3/cb8118a044b5007586245a650360c9f5915b2f4232dd7658bb7a63dd1d02/numpy-2.1.3-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:3522b0dfe983a575e6a9ab3a4a4dfe156c3e428468ff08ce582b9bb6bd1d71d4", size = 6752501 }, + { url = "https://files.pythonhosted.org/packages/53/f5/365b46439b518d2ec6ebb880cc0edf90f225145dfd4db7958334f7164530/numpy-2.1.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c006b607a865b07cd981ccb218a04fc86b600411d83d6fc261357f1c0966755d", size = 16142601 }, + { url = "https://files.pythonhosted.org/packages/03/c2/d1fee6ba999aa7cd41ca6856937f2baaf604c3eec1565eae63451ec31e5e/numpy-2.1.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e14e26956e6f1696070788252dcdff11b4aca4c3e8bd166e0df1bb8f315a67cb", size = 12771397 }, +] + +[[package]] +name = "orjson" +version = "3.10.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/5dea21763eeff8c1590076918a446ea3d6140743e0e36f58f369928ed0f4/orjson-3.10.15.tar.gz", hash = "sha256:05ca7fe452a2e9d8d9d706a2984c95b9c2ebc5db417ce0b7a49b91d50642a23e", size = 5282482 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/09/e5ff18ad009e6f97eb7edc5f67ef98b3ce0c189da9c3eaca1f9587cd4c61/orjson-3.10.15-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:552c883d03ad185f720d0c09583ebde257e41b9521b74ff40e08b7dec4559c04", size = 249532 }, + { url = "https://files.pythonhosted.org/packages/bd/b8/a75883301fe332bd433d9b0ded7d2bb706ccac679602c3516984f8814fb5/orjson-3.10.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e3e8d438d02e4854f70bfdc03a6bcdb697358dbaa6bcd19cbe24d24ece1f8", size = 125229 }, + { url = "https://files.pythonhosted.org/packages/83/4b/22f053e7a364cc9c685be203b1e40fc5f2b3f164a9b2284547504eec682e/orjson-3.10.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c2c79fa308e6edb0ffab0a31fd75a7841bf2a79a20ef08a3c6e3b26814c8ca8", size = 150148 }, + { url = "https://files.pythonhosted.org/packages/63/64/1b54fc75ca328b57dd810541a4035fe48c12a161d466e3cf5b11a8c25649/orjson-3.10.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cb85490aa6bf98abd20607ab5c8324c0acb48d6da7863a51be48505646c814", size = 139748 }, + { url = "https://files.pythonhosted.org/packages/5e/ff/ff0c5da781807bb0a5acd789d9a7fbcb57f7b0c6e1916595da1f5ce69f3c/orjson-3.10.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763dadac05e4e9d2bc14938a45a2d0560549561287d41c465d3c58aec818b164", size = 154559 }, + { url = "https://files.pythonhosted.org/packages/4e/9a/11e2974383384ace8495810d4a2ebef5f55aacfc97b333b65e789c9d362d/orjson-3.10.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a330b9b4734f09a623f74a7490db713695e13b67c959713b78369f26b3dee6bf", size = 130349 }, + { url = "https://files.pythonhosted.org/packages/2d/c4/dd9583aea6aefee1b64d3aed13f51d2aadb014028bc929fe52936ec5091f/orjson-3.10.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a61a4622b7ff861f019974f73d8165be1bd9a0855e1cad18ee167acacabeb061", size = 138514 }, + { url = "https://files.pythonhosted.org/packages/53/3e/dcf1729230654f5c5594fc752de1f43dcf67e055ac0d300c8cdb1309269a/orjson-3.10.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:acd271247691574416b3228db667b84775c497b245fa275c6ab90dc1ffbbd2b3", size = 130940 }, + { url = "https://files.pythonhosted.org/packages/e8/2b/b9759fe704789937705c8a56a03f6c03e50dff7df87d65cba9a20fec5282/orjson-3.10.15-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4759b109c37f635aa5c5cc93a1b26927bfde24b254bcc0e1149a9fada253d2d", size = 414713 }, + { url = "https://files.pythonhosted.org/packages/a7/6b/b9dfdbd4b6e20a59238319eb203ae07c3f6abf07eef909169b7a37ae3bba/orjson-3.10.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9e992fd5cfb8b9f00bfad2fd7a05a4299db2bbe92e6440d9dd2fab27655b3182", size = 141028 }, + { url = "https://files.pythonhosted.org/packages/7c/b5/40f5bbea619c7caf75eb4d652a9821875a8ed04acc45fe3d3ef054ca69fb/orjson-3.10.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f95fb363d79366af56c3f26b71df40b9a583b07bbaaf5b317407c4d58497852e", size = 129715 }, + { url = "https://files.pythonhosted.org/packages/38/60/2272514061cbdf4d672edbca6e59c7e01cd1c706e881427d88f3c3e79761/orjson-3.10.15-cp310-cp310-win32.whl", hash = "sha256:f9875f5fea7492da8ec2444839dcc439b0ef298978f311103d0b7dfd775898ab", size = 142473 }, + { url = "https://files.pythonhosted.org/packages/11/5d/be1490ff7eafe7fef890eb4527cf5bcd8cfd6117f3efe42a3249ec847b60/orjson-3.10.15-cp310-cp310-win_amd64.whl", hash = "sha256:17085a6aa91e1cd70ca8533989a18b5433e15d29c574582f76f821737c8d5806", size = 133564 }, + { url = "https://files.pythonhosted.org/packages/7a/a2/21b25ce4a2c71dbb90948ee81bd7a42b4fbfc63162e57faf83157d5540ae/orjson-3.10.15-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c4cc83960ab79a4031f3119cc4b1a1c627a3dc09df125b27c4201dff2af7eaa6", size = 249533 }, + { url = "https://files.pythonhosted.org/packages/b2/85/2076fc12d8225698a51278009726750c9c65c846eda741e77e1761cfef33/orjson-3.10.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddbeef2481d895ab8be5185f2432c334d6dec1f5d1933a9c83014d188e102cef", size = 125230 }, + { url = "https://files.pythonhosted.org/packages/06/df/a85a7955f11274191eccf559e8481b2be74a7c6d43075d0a9506aa80284d/orjson-3.10.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e590a0477b23ecd5b0ac865b1b907b01b3c5535f5e8a8f6ab0e503efb896334", size = 150148 }, + { url = "https://files.pythonhosted.org/packages/37/b3/94c55625a29b8767c0eed194cb000b3787e3c23b4cdd13be17bae6ccbb4b/orjson-3.10.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6be38bd103d2fd9bdfa31c2720b23b5d47c6796bcb1d1b598e3924441b4298d", size = 139749 }, + { url = "https://files.pythonhosted.org/packages/53/ba/c608b1e719971e8ddac2379f290404c2e914cf8e976369bae3cad88768b1/orjson-3.10.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ff4f6edb1578960ed628a3b998fa54d78d9bb3e2eb2cfc5c2a09732431c678d0", size = 154558 }, + { url = "https://files.pythonhosted.org/packages/b2/c4/c1fb835bb23ad788a39aa9ebb8821d51b1c03588d9a9e4ca7de5b354fdd5/orjson-3.10.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0482b21d0462eddd67e7fce10b89e0b6ac56570424662b685a0d6fccf581e13", size = 130349 }, + { url = "https://files.pythonhosted.org/packages/78/14/bb2b48b26ab3c570b284eb2157d98c1ef331a8397f6c8bd983b270467f5c/orjson-3.10.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bb5cc3527036ae3d98b65e37b7986a918955f85332c1ee07f9d3f82f3a6899b5", size = 138513 }, + { url = "https://files.pythonhosted.org/packages/4a/97/d5b353a5fe532e92c46467aa37e637f81af8468aa894cd77d2ec8a12f99e/orjson-3.10.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d569c1c462912acdd119ccbf719cf7102ea2c67dd03b99edcb1a3048651ac96b", size = 130942 }, + { url = "https://files.pythonhosted.org/packages/b5/5d/a067bec55293cca48fea8b9928cfa84c623be0cce8141d47690e64a6ca12/orjson-3.10.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1e6d33efab6b71d67f22bf2962895d3dc6f82a6273a965fab762e64fa90dc399", size = 414717 }, + { url = "https://files.pythonhosted.org/packages/6f/9a/1485b8b05c6b4c4db172c438cf5db5dcfd10e72a9bc23c151a1137e763e0/orjson-3.10.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c33be3795e299f565681d69852ac8c1bc5c84863c0b0030b2b3468843be90388", size = 141033 }, + { url = "https://files.pythonhosted.org/packages/f8/d2/fc67523656e43a0c7eaeae9007c8b02e86076b15d591e9be11554d3d3138/orjson-3.10.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eea80037b9fae5339b214f59308ef0589fc06dc870578b7cce6d71eb2096764c", size = 129720 }, + { url = "https://files.pythonhosted.org/packages/79/42/f58c7bd4e5b54da2ce2ef0331a39ccbbaa7699b7f70206fbf06737c9ed7d/orjson-3.10.15-cp311-cp311-win32.whl", hash = "sha256:d5ac11b659fd798228a7adba3e37c010e0152b78b1982897020a8e019a94882e", size = 142473 }, + { url = "https://files.pythonhosted.org/packages/00/f8/bb60a4644287a544ec81df1699d5b965776bc9848d9029d9f9b3402ac8bb/orjson-3.10.15-cp311-cp311-win_amd64.whl", hash = "sha256:cf45e0214c593660339ef63e875f32ddd5aa3b4adc15e662cdb80dc49e194f8e", size = 133570 }, + { url = "https://files.pythonhosted.org/packages/66/85/22fe737188905a71afcc4bf7cc4c79cd7f5bbe9ed1fe0aac4ce4c33edc30/orjson-3.10.15-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9d11c0714fc85bfcf36ada1179400862da3288fc785c30e8297844c867d7505a", size = 249504 }, + { url = "https://files.pythonhosted.org/packages/48/b7/2622b29f3afebe938a0a9037e184660379797d5fd5234e5998345d7a5b43/orjson-3.10.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dba5a1e85d554e3897fa9fe6fbcff2ed32d55008973ec9a2b992bd9a65d2352d", size = 125080 }, + { url = "https://files.pythonhosted.org/packages/ce/8f/0b72a48f4403d0b88b2a41450c535b3e8989e8a2d7800659a967efc7c115/orjson-3.10.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7723ad949a0ea502df656948ddd8b392780a5beaa4c3b5f97e525191b102fff0", size = 150121 }, + { url = "https://files.pythonhosted.org/packages/06/ec/acb1a20cd49edb2000be5a0404cd43e3c8aad219f376ac8c60b870518c03/orjson-3.10.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6fd9bc64421e9fe9bd88039e7ce8e58d4fead67ca88e3a4014b143cec7684fd4", size = 139796 }, + { url = "https://files.pythonhosted.org/packages/33/e1/f7840a2ea852114b23a52a1c0b2bea0a1ea22236efbcdb876402d799c423/orjson-3.10.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dadba0e7b6594216c214ef7894c4bd5f08d7c0135f4dd0145600be4fbcc16767", size = 154636 }, + { url = "https://files.pythonhosted.org/packages/fa/da/31543337febd043b8fa80a3b67de627669b88c7b128d9ad4cc2ece005b7a/orjson-3.10.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b48f59114fe318f33bbaee8ebeda696d8ccc94c9e90bc27dbe72153094e26f41", size = 130621 }, + { url = "https://files.pythonhosted.org/packages/ed/78/66115dc9afbc22496530d2139f2f4455698be444c7c2475cb48f657cefc9/orjson-3.10.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:035fb83585e0f15e076759b6fedaf0abb460d1765b6a36f48018a52858443514", size = 138516 }, + { url = "https://files.pythonhosted.org/packages/22/84/cd4f5fb5427ffcf823140957a47503076184cb1ce15bcc1165125c26c46c/orjson-3.10.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d13b7fe322d75bf84464b075eafd8e7dd9eae05649aa2a5354cfa32f43c59f17", size = 130762 }, + { url = "https://files.pythonhosted.org/packages/93/1f/67596b711ba9f56dd75d73b60089c5c92057f1130bb3a25a0f53fb9a583b/orjson-3.10.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7066b74f9f259849629e0d04db6609db4cf5b973248f455ba5d3bd58a4daaa5b", size = 414700 }, + { url = "https://files.pythonhosted.org/packages/7c/0c/6a3b3271b46443d90efb713c3e4fe83fa8cd71cda0d11a0f69a03f437c6e/orjson-3.10.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:88dc3f65a026bd3175eb157fea994fca6ac7c4c8579fc5a86fc2114ad05705b7", size = 141077 }, + { url = "https://files.pythonhosted.org/packages/3b/9b/33c58e0bfc788995eccd0d525ecd6b84b40d7ed182dd0751cd4c1322ac62/orjson-3.10.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b342567e5465bd99faa559507fe45e33fc76b9fb868a63f1642c6bc0735ad02a", size = 129898 }, + { url = "https://files.pythonhosted.org/packages/01/c1/d577ecd2e9fa393366a1ea0a9267f6510d86e6c4bb1cdfb9877104cac44c/orjson-3.10.15-cp312-cp312-win32.whl", hash = "sha256:0a4f27ea5617828e6b58922fdbec67b0aa4bb844e2d363b9244c47fa2180e665", size = 142566 }, + { url = "https://files.pythonhosted.org/packages/ed/eb/a85317ee1732d1034b92d56f89f1de4d7bf7904f5c8fb9dcdd5b1c83917f/orjson-3.10.15-cp312-cp312-win_amd64.whl", hash = "sha256:ef5b87e7aa9545ddadd2309efe6824bd3dd64ac101c15dae0f2f597911d46eaa", size = 133732 }, + { url = "https://files.pythonhosted.org/packages/06/10/fe7d60b8da538e8d3d3721f08c1b7bff0491e8fa4dd3bf11a17e34f4730e/orjson-3.10.15-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bae0e6ec2b7ba6895198cd981b7cca95d1487d0147c8ed751e5632ad16f031a6", size = 249399 }, + { url = "https://files.pythonhosted.org/packages/6b/83/52c356fd3a61abd829ae7e4366a6fe8e8863c825a60d7ac5156067516edf/orjson-3.10.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f93ce145b2db1252dd86af37d4165b6faa83072b46e3995ecc95d4b2301b725a", size = 125044 }, + { url = "https://files.pythonhosted.org/packages/55/b2/d06d5901408e7ded1a74c7c20d70e3a127057a6d21355f50c90c0f337913/orjson-3.10.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c203f6f969210128af3acae0ef9ea6aab9782939f45f6fe02d05958fe761ef9", size = 150066 }, + { url = "https://files.pythonhosted.org/packages/75/8c/60c3106e08dc593a861755781c7c675a566445cc39558677d505878d879f/orjson-3.10.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8918719572d662e18b8af66aef699d8c21072e54b6c82a3f8f6404c1f5ccd5e0", size = 139737 }, + { url = "https://files.pythonhosted.org/packages/6a/8c/ae00d7d0ab8a4490b1efeb01ad4ab2f1982e69cc82490bf8093407718ff5/orjson-3.10.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f71eae9651465dff70aa80db92586ad5b92df46a9373ee55252109bb6b703307", size = 154804 }, + { url = "https://files.pythonhosted.org/packages/22/86/65dc69bd88b6dd254535310e97bc518aa50a39ef9c5a2a5d518e7a223710/orjson-3.10.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e117eb299a35f2634e25ed120c37c641398826c2f5a3d3cc39f5993b96171b9e", size = 130583 }, + { url = "https://files.pythonhosted.org/packages/bb/00/6fe01ededb05d52be42fabb13d93a36e51f1fd9be173bd95707d11a8a860/orjson-3.10.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:13242f12d295e83c2955756a574ddd6741c81e5b99f2bef8ed8d53e47a01e4b7", size = 138465 }, + { url = "https://files.pythonhosted.org/packages/db/2f/4cc151c4b471b0cdc8cb29d3eadbce5007eb0475d26fa26ed123dca93b33/orjson-3.10.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7946922ada8f3e0b7b958cc3eb22cfcf6c0df83d1fe5521b4a100103e3fa84c8", size = 130742 }, + { url = "https://files.pythonhosted.org/packages/9f/13/8a6109e4b477c518498ca37963d9c0eb1508b259725553fb53d53b20e2ea/orjson-3.10.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b7155eb1623347f0f22c38c9abdd738b287e39b9982e1da227503387b81b34ca", size = 414669 }, + { url = "https://files.pythonhosted.org/packages/22/7b/1d229d6d24644ed4d0a803de1b0e2df832032d5beda7346831c78191b5b2/orjson-3.10.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:208beedfa807c922da4e81061dafa9c8489c6328934ca2a562efa707e049e561", size = 141043 }, + { url = "https://files.pythonhosted.org/packages/cc/d3/6dc91156cf12ed86bed383bcb942d84d23304a1e57b7ab030bf60ea130d6/orjson-3.10.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eca81f83b1b8c07449e1d6ff7074e82e3fd6777e588f1a6632127f286a968825", size = 129826 }, + { url = "https://files.pythonhosted.org/packages/b3/38/c47c25b86f6996f1343be721b6ea4367bc1c8bc0fc3f6bbcd995d18cb19d/orjson-3.10.15-cp313-cp313-win32.whl", hash = "sha256:c03cd6eea1bd3b949d0d007c8d57049aa2b39bd49f58b4b2af571a5d3833d890", size = 142542 }, + { url = "https://files.pythonhosted.org/packages/27/f1/1d7ec15b20f8ce9300bc850de1e059132b88990e46cd0ccac29cbf11e4f9/orjson-3.10.15-cp313-cp313-win_amd64.whl", hash = "sha256:fd56a26a04f6ba5fb2045b0acc487a63162a958ed837648c5781e1fe3316cfbf", size = 133444 }, +] + +[[package]] +name = "outcome" +version = "1.3.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/0b/334c7c50a2979e15f2a027a41d1ca78ee730d5b1c7f7f4b26d7cb899839d/pygame-2.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9beeb647e555afb5657111fa83acb74b99ad88761108eaea66472e8b8547b55b", size = 13109297 }, + { url = "https://files.pythonhosted.org/packages/dc/48/f8b1069788d1bd42e63a960d74d3355242480b750173a42b2749687578ca/pygame-2.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:10e3d2a55f001f6c0a6eb44aa79ea7607091c9352b946692acedb2ac1482f1c9", size = 12375837 }, + { url = "https://files.pythonhosted.org/packages/bc/33/a1310386b8913ce1bdb90c33fa536970e299ad57eb35785f1d71ea1e2ad3/pygame-2.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:816e85000c5d8b02a42b9834f761a5925ef3377d2924e3a7c4c143d2990ce5b8", size = 13607860 }, + { url = "https://files.pythonhosted.org/packages/88/0f/4e37b115056e43714e7550054dd3cd7f4d552da54d7fc58a2fb1407acda5/pygame-2.6.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a78fd030d98faab4a8e27878536fdff7518d3e062a72761c552f624ebba5a5f", size = 14304696 }, + { url = "https://files.pythonhosted.org/packages/11/b3/de6ed93ae483cf3bac8f950a955e83f7ffe59651fd804d100fff65d66d6c/pygame-2.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da3ad64d685f84a34ebe5daacb39fff14f1251acb34c098d760d63fee768f50c", size = 13977684 }, + { url = "https://files.pythonhosted.org/packages/d3/05/d86440aa879708c41844bafc6b3eb42c6d8cf54082482499b53139133e2a/pygame-2.6.1-cp310-cp310-win32.whl", hash = "sha256:9dd5c054d4bd875a8caf978b82672f02bec332f52a833a76899220c460bb4b58", size = 10251775 }, + { url = "https://files.pythonhosted.org/packages/38/88/8de61324775cf2c844a51d8db14a8a6d2a9092312f27678f6eaa3a460376/pygame-2.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:00827aba089355925902d533f9c41e79a799641f03746c50a374dc5c3362e43d", size = 10618801 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/31/df/6788fd2e9a864d0496a77670e44a7c012184b7a5382866ab0e60c55c0f28/pygame-2.6.1-cp311-cp311-win32.whl", hash = "sha256:811e7b925146d8149d79193652cbb83e0eca0aae66476b1cb310f0f4226b8b5c", size = 10250863 }, + { url = "https://files.pythonhosted.org/packages/d2/55/ca3eb851aeef4f6f2e98a360c201f0d00bd1ba2eb98e2c7850d80aabc526/pygame-2.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:91476902426facd4bb0dad4dc3b2573bc82c95c71b135e0daaea072ed528d299", size = 10622016 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/f3/42/8ea2a6979e6fa971702fece1747e862e2256d4a8558fe0da6364dd946c53/pygame-2.6.1-cp312-cp312-win32.whl", hash = "sha256:84fc4054e25262140d09d39e094f6880d730199710829902f0d8ceae0213379e", size = 10252421 }, + { url = "https://files.pythonhosted.org/packages/5f/90/7d766d54bb95939725e9a9361f9c06b0cfbe3fe100aa35400f0a461a278a/pygame-2.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:3a9e7396be0d9633831c3f8d5d82dd63ba373ad65599628294b7a4f8a5a01a65", size = 10624591 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/a6/06/29e939b34d3f1354738c7d201c51c250ad7abefefaf6f8332d962ff67c4b/pygame-2.6.1-cp313-cp313-win32.whl", hash = "sha256:3acd8c009317190c2bfd81db681ecef47d5eb108c2151d09596d9c7ea9df5c0e", size = 10249309 }, + { url = "https://files.pythonhosted.org/packages/7e/11/17f7f319ca91824b86557e9303e3b7a71991ef17fd45286bf47d7f0a38e6/pygame-2.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:813af4fba5d0b2cb8e58f5d95f7910295c34067dcc290d34f1be59c48bd1ea6a", size = 10620084 }, +] + +[[package]] +name = "pytest" +version = "8.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, +] + +[[package]] +name = "pytest-cov" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, +] + +[[package]] +name = "pytest-trio" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "outcome" }, + { name = "pytest" }, + { name = "trio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/08/056279526554c6c6e6ad6d4a479a338d14dc785ac30be8bdc6ca0153c1be/pytest-trio-0.8.0.tar.gz", hash = "sha256:8363db6336a79e6c53375a2123a41ddbeccc4aa93f93788651641789a56fb52e", size = 46525 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/22/71953f47e0da5852c899f58cd7a31e6100f37c632b7b9ee52d067613a844/pytest_trio-0.8.0-py3-none-any.whl", hash = "sha256:e6a7e7351ae3e8ec3f4564d30ee77d1ec66e1df611226e5618dbb32f9545c841", size = 27221 }, +] + +[[package]] +name = "ruff" +version = "0.9.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/80/63/77ecca9d21177600f551d1c58ab0e5a0b260940ea7312195bd2a4798f8a8/ruff-0.9.2.tar.gz", hash = "sha256:b5eceb334d55fae5f316f783437392642ae18e16dcf4f1858d55d3c2a0f8f5d0", size = 3553799 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/b9/0e168e4e7fb3af851f739e8f07889b91d1a33a30fca8c29fa3149d6b03ec/ruff-0.9.2-py3-none-linux_armv6l.whl", hash = "sha256:80605a039ba1454d002b32139e4970becf84b5fee3a3c3bf1c2af6f61a784347", size = 11652408 }, + { url = "https://files.pythonhosted.org/packages/2c/22/08ede5db17cf701372a461d1cb8fdde037da1d4fa622b69ac21960e6237e/ruff-0.9.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b9aab82bb20afd5f596527045c01e6ae25a718ff1784cb92947bff1f83068b00", size = 11587553 }, + { url = "https://files.pythonhosted.org/packages/42/05/dedfc70f0bf010230229e33dec6e7b2235b2a1b8cbb2a991c710743e343f/ruff-0.9.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fbd337bac1cfa96be615f6efcd4bc4d077edbc127ef30e2b8ba2a27e18c054d4", size = 11020755 }, + { url = "https://files.pythonhosted.org/packages/df/9b/65d87ad9b2e3def67342830bd1af98803af731243da1255537ddb8f22209/ruff-0.9.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b35259b0cbf8daa22a498018e300b9bb0174c2bbb7bcba593935158a78054d", size = 11826502 }, + { url = "https://files.pythonhosted.org/packages/93/02/f2239f56786479e1a89c3da9bc9391120057fc6f4a8266a5b091314e72ce/ruff-0.9.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b6a9701d1e371bf41dca22015c3f89769da7576884d2add7317ec1ec8cb9c3c", size = 11390562 }, + { url = "https://files.pythonhosted.org/packages/c9/37/d3a854dba9931f8cb1b2a19509bfe59e00875f48ade632e95aefcb7a0aee/ruff-0.9.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9cc53e68b3c5ae41e8faf83a3b89f4a5d7b2cb666dff4b366bb86ed2a85b481f", size = 12548968 }, + { url = "https://files.pythonhosted.org/packages/fa/c3/c7b812bb256c7a1d5553433e95980934ffa85396d332401f6b391d3c4569/ruff-0.9.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8efd9da7a1ee314b910da155ca7e8953094a7c10d0c0a39bfde3fcfd2a015684", size = 13187155 }, + { url = "https://files.pythonhosted.org/packages/bd/5a/3c7f9696a7875522b66aa9bba9e326e4e5894b4366bd1dc32aa6791cb1ff/ruff-0.9.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3292c5a22ea9a5f9a185e2d131dc7f98f8534a32fb6d2ee7b9944569239c648d", size = 12704674 }, + { url = "https://files.pythonhosted.org/packages/be/d6/d908762257a96ce5912187ae9ae86792e677ca4f3dc973b71e7508ff6282/ruff-0.9.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a605fdcf6e8b2d39f9436d343d1f0ff70c365a1e681546de0104bef81ce88df", size = 14529328 }, + { url = "https://files.pythonhosted.org/packages/2d/c2/049f1e6755d12d9cd8823242fa105968f34ee4c669d04cac8cea51a50407/ruff-0.9.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c547f7f256aa366834829a08375c297fa63386cbe5f1459efaf174086b564247", size = 12385955 }, + { url = "https://files.pythonhosted.org/packages/91/5a/a9bdb50e39810bd9627074e42743b00e6dc4009d42ae9f9351bc3dbc28e7/ruff-0.9.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d18bba3d3353ed916e882521bc3e0af403949dbada344c20c16ea78f47af965e", size = 11810149 }, + { url = "https://files.pythonhosted.org/packages/e5/fd/57df1a0543182f79a1236e82a79c68ce210efb00e97c30657d5bdb12b478/ruff-0.9.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b338edc4610142355ccf6b87bd356729b62bf1bc152a2fad5b0c7dc04af77bfe", size = 11479141 }, + { url = "https://files.pythonhosted.org/packages/dc/16/bc3fd1d38974f6775fc152a0554f8c210ff80f2764b43777163c3c45d61b/ruff-0.9.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:492a5e44ad9b22a0ea98cf72e40305cbdaf27fac0d927f8bc9e1df316dcc96eb", size = 12014073 }, + { url = "https://files.pythonhosted.org/packages/47/6b/e4ca048a8f2047eb652e1e8c755f384d1b7944f69ed69066a37acd4118b0/ruff-0.9.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:af1e9e9fe7b1f767264d26b1075ac4ad831c7db976911fa362d09b2d0356426a", size = 12435758 }, + { url = "https://files.pythonhosted.org/packages/c2/40/4d3d6c979c67ba24cf183d29f706051a53c36d78358036a9cd21421582ab/ruff-0.9.2-py3-none-win32.whl", hash = "sha256:71cbe22e178c5da20e1514e1e01029c73dc09288a8028a5d3446e6bba87a5145", size = 9796916 }, + { url = "https://files.pythonhosted.org/packages/c3/ef/7f548752bdb6867e6939489c87fe4da489ab36191525fadc5cede2a6e8e2/ruff-0.9.2-py3-none-win_amd64.whl", hash = "sha256:c5e1d6abc798419cf46eed03f54f2e0c3adb1ad4b801119dedf23fcaf69b55b5", size = 10773080 }, + { url = "https://files.pythonhosted.org/packages/0e/4e/33df635528292bd2d18404e4daabcd74ca8a9853b2e1df85ed3d32d24362/ruff-0.9.2-py3-none-win_arm64.whl", hash = "sha256:a1b63fa24149918f8b37cef2ee6fff81f24f0d74b6f0bdc37bc3e1f2143e41c6", size = 10001738 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575 }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + +[[package]] +name = "trio" +version = "0.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cffi", marker = "implementation_name != 'pypy' and os_name == 'nt'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "outcome" }, + { name = "sniffio" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/73/57efab729506a8d4b89814f1e356ec8f3369de0ed4fd7e7616974d09646d/trio-0.28.0.tar.gz", hash = "sha256:4e547896fe9e8a5658e54e4c7c5fa1db748cbbbaa7c965e7d40505b928c73c05", size = 580318 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/04/9954a59e1fb6732f5436225c9af963811d7b24ea62a8bf96991f2cb8c26e/trio-0.28.0-py3-none-any.whl", hash = "sha256:56d58977acc1635735a96581ec70513cc781b8b6decd299c487d3be2a721cd94", size = 486317 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "uv" +version = "0.5.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/c1/d8da0122d14a48a7895241b1c15b027d7df6f56350cae614561c0567ecb2/uv-0.5.21.tar.gz", hash = "sha256:eb33043b42111ae3fef76906422b5c4247188e1ae1233da63be82cc64bb527d0", size = 2631880 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/c7/c7a787cc2c526442b2999cbebe526e24517b8812f3d545e90811e38c213a/uv-0.5.21-py3-none-linux_armv6l.whl", hash = "sha256:8ea7309dc1891e88276e207aa389cc4524ec7a7038a75bfd7c5a09ed3701316f", size = 15181071 }, + { url = "https://files.pythonhosted.org/packages/5d/61/5a6796f31830898d0aa01e018d49bbbf39d61f2c19350663be16b6cfd1d9/uv-0.5.21-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ef4e579390a022efcbfe8720f51ad46fdff54caf982782967d5689841485ddd8", size = 15305687 }, + { url = "https://files.pythonhosted.org/packages/65/37/a5a2e0d0776063e2fe1f6dfac21dd5e707d2df9c167572c416970dd3af34/uv-0.5.21-py3-none-macosx_11_0_arm64.whl", hash = "sha256:73c9d1bdbff989114c5c37649235c569f89b65bd2e57b75d8fdb73946ade7cbd", size = 14214520 }, + { url = "https://files.pythonhosted.org/packages/15/ce/a844df3ea81c9370feed1ab0fd474776709a60f07b897c41fcdf0f260c0f/uv-0.5.21-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:6e97c68306c0583af1b14b5b801c3e18ab7bc349a4c9cdd8ab5f8f46348539c5", size = 14667101 }, + { url = "https://files.pythonhosted.org/packages/88/53/d4a0cefd1927f6047500c95967d69d045b11839c9f48e2a448372498186f/uv-0.5.21-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ecdf58adf9376f2b4f63e6538e38be0e77fcd3d5b07b3ee56a3c7cd1d9ca526", size = 14952637 }, + { url = "https://files.pythonhosted.org/packages/4d/0a/a68d9142e429b4a28cebcae21c6dba262f7905772d950d076e0b161f4e0c/uv-0.5.21-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dafa7b5bb3ae8949ba100645b7a8d804f683547586024f73ad1b2d97a1aa9976", size = 15665199 }, + { url = "https://files.pythonhosted.org/packages/18/9a/062eb481fe3661ee663751f0af9a6490014357592c9aea65d0261d385a40/uv-0.5.21-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:609299c04c00ece874b30abee9cb83753224a03e8d9191327397f33a92674a53", size = 16571172 }, + { url = "https://files.pythonhosted.org/packages/94/f0/8e36e40acb289a39ed00a49122f6c3ad36993ff11d8197885877ace30b73/uv-0.5.21-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10232d5f24a1831f7ab3967f0b56f78681c520ff3391dcf5096eface94619e8e", size = 16292510 }, + { url = "https://files.pythonhosted.org/packages/91/40/3b48d57626dcb306c9e5736d4148fb6eaf931d94dbeb810ad32e48b58ac8/uv-0.5.21-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f17d35ab4a099657ad55d3cfeaf91a35b929ae2cd2b22163710cdfec45ea3941", size = 20623325 }, + { url = "https://files.pythonhosted.org/packages/5c/6f/86ee925f5e20df3aa366538a56e0d1bd5dfa9ef9d9bea57709480d47d72c/uv-0.5.21-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a1582f4964b1249b0e82ad0e60519a73392e099541a6db587e7333139255d50", size = 15952215 }, + { url = "https://files.pythonhosted.org/packages/62/f9/094ceaf8f0380b5381918aeb65907ff1fd06150b51f3baafa879ed9fdf4a/uv-0.5.21-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:afd98237d97b92935c8d5a9bf28218b5ecb497af9a99ad0a740d0b71b51f864a", size = 14914771 }, + { url = "https://files.pythonhosted.org/packages/0c/10/a5f73f433f29922b304eb95e7d6f18632734f92753c73017a8b05ce41795/uv-0.5.21-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:b317bfb7ba61e0396be5776f723e03e818a6393322f62828b67c16b565e1c0ec", size = 14904317 }, + { url = "https://files.pythonhosted.org/packages/76/4e/b9be4fcc45a026f1e1a2975719ee5f0444dafda1b606c0871d0c24651115/uv-0.5.21-py3-none-musllinux_1_1_i686.whl", hash = "sha256:168fca3bad68f75518a168feeebfd2c0b104e9abc06a33caa710d0b2753db3aa", size = 15315311 }, + { url = "https://files.pythonhosted.org/packages/a5/2d/74df7f292a7c15269bacd451a492520e26c4ef99b19c01fe96913506dea5/uv-0.5.21-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:f5ba5076b6b69161d318f5ddeff6dd935ab29a157ff10dd8756ed6dcb5d0a497", size = 16042115 }, + { url = "https://files.pythonhosted.org/packages/4b/69/03731b38d23e7bed653f186be2ff2dfcdcef29a611f4937ff4bacff205fe/uv-0.5.21-py3-none-win32.whl", hash = "sha256:34944204a39b840fa0efb2ba27f4decce50115460c6b8e4e6ade6aae6246d0cf", size = 15262952 }, + { url = "https://files.pythonhosted.org/packages/c2/8d/f6508e3c3fbc76b945365062ffff9fa6e60ad6516b26dae23a1c761d65c0/uv-0.5.21-py3-none-win_amd64.whl", hash = "sha256:36f21534a9e00a85cc532ef9575d3785a4e434a25daa93e51ebc08b54ade4991", size = 16625459 }, +] From e783b16390d10b41ab65766aa98e1911913395ea Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sat, 18 Jan 2025 17:59:06 -0600 Subject: [PATCH 44/58] Fix activation command --- ci.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci.sh b/ci.sh index 6fb887f..f470dd3 100755 --- a/ci.sh +++ b/ci.sh @@ -30,7 +30,7 @@ UV_VENV_OUTPUT="$(uv venv --seed --allow-existing 2>&1)" echo "$UV_VENV_OUTPUT" # Extract the activation command from the output -activation_command=$(echo "$UV_VENV_OUTPUT" | grep -oP '(?<=Activate with: ).*') +activation_command=$(echo "$UV_VENV_OUTPUT" | grep 'Activate with:' | sed 's/.*Activate with: //') # Check if the activation command was found if [ -n "$activation_command" ]; then From 441a53350d947d7883f139d4ffc784ed6518da01 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sat, 18 Jan 2025 18:02:09 -0600 Subject: [PATCH 45/58] Try to fix activation script again --- ci.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci.sh b/ci.sh index f470dd3..7dd39ba 100755 --- a/ci.sh +++ b/ci.sh @@ -30,7 +30,7 @@ UV_VENV_OUTPUT="$(uv venv --seed --allow-existing 2>&1)" echo "$UV_VENV_OUTPUT" # Extract the activation command from the output -activation_command=$(echo "$UV_VENV_OUTPUT" | grep 'Activate with:' | sed 's/.*Activate with: //') +activation_command=$(echo "$UV_VENV_OUTPUT" | grep 'Activate with:' | sed 's/.*Activate with: //; s/^/"/; s/$/"/') # Check if the activation command was found if [ -n "$activation_command" ]; then From 881f454bf644055815fb457517cebdf5b9be1856 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sat, 18 Jan 2025 18:11:20 -0600 Subject: [PATCH 46/58] Run different virtual environment activate depending on platform --- ci.sh | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/ci.sh b/ci.sh index 7dd39ba..af2d284 100755 --- a/ci.sh +++ b/ci.sh @@ -26,21 +26,30 @@ python -m pip install uv==$UV_VERSION python -m uv --version UV_VENV_SEED="pip" -UV_VENV_OUTPUT="$(uv venv --seed --allow-existing 2>&1)" -echo "$UV_VENV_OUTPUT" - -# Extract the activation command from the output -activation_command=$(echo "$UV_VENV_OUTPUT" | grep 'Activate with:' | sed 's/.*Activate with: //; s/^/"/; s/$/"/') - -# Check if the activation command was found -if [ -n "$activation_command" ]; then - # Execute the activation command - echo "Activating virtual environment..." - eval "$activation_command" -else - echo "::error:: Activation command not found in uv venv output." +python -m uv venv --seed --allow-existing + +# Determine the platform and activate the virtual environment accordingly +case "$OSTYPE" in + linux-gnu*|linux-musl*) + # Linux + echo "Activating virtual environment on Linux..." + source .venv/bin/activate + ;; + darwin*) + # macOS + echo "Activating virtual environment on macOS..." + source .venv/bin/activate + ;; + cygwin*|msys*) + # Windows + echo "Activating virtual environment on Windows..." + .venv\Scripts\activate + ;; + *) + echo "::error:: Unknown OS. Please activate the virtual environment manually." exit 1 -fi + ;; +esac python -m pip install uv==$UV_VERSION # python -m uv build From c4d9d794c3af51449175e6e1dc74e4da179360e5 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sat, 18 Jan 2025 18:20:57 -0600 Subject: [PATCH 47/58] Try to fix windows activate --- ci.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci.sh b/ci.sh index af2d284..e6cfdfe 100755 --- a/ci.sh +++ b/ci.sh @@ -40,10 +40,10 @@ case "$OSTYPE" in echo "Activating virtual environment on macOS..." source .venv/bin/activate ;; - cygwin*|msys*) + cygwin*|msys*|win32) # Windows echo "Activating virtual environment on Windows..." - .venv\Scripts\activate + source .venv/Scripts/activate ;; *) echo "::error:: Unknown OS. Please activate the virtual environment manually." From 851bba748daa83547c7f3f348bdc729251f7b127 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sat, 18 Jan 2025 18:23:30 -0600 Subject: [PATCH 48/58] Remove unused case --- ci.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci.sh b/ci.sh index e6cfdfe..f31e38a 100755 --- a/ci.sh +++ b/ci.sh @@ -40,7 +40,7 @@ case "$OSTYPE" in echo "Activating virtual environment on macOS..." source .venv/bin/activate ;; - cygwin*|msys*|win32) + cygwin*|msys*) # Windows echo "Activating virtual environment on Windows..." source .venv/Scripts/activate From 48d3793357b237e0733ebbe26080a3876ad84adb Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sat, 18 Jan 2025 18:26:28 -0600 Subject: [PATCH 49/58] Cleanup --- ci.sh | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/ci.sh b/ci.sh index f31e38a..e7d4c92 100755 --- a/ci.sh +++ b/ci.sh @@ -30,31 +30,20 @@ python -m uv venv --seed --allow-existing # Determine the platform and activate the virtual environment accordingly case "$OSTYPE" in - linux-gnu*|linux-musl*) - # Linux - echo "Activating virtual environment on Linux..." - source .venv/bin/activate - ;; - darwin*) - # macOS - echo "Activating virtual environment on macOS..." + linux-gnu*|linux-musl*|darwin*) source .venv/bin/activate ;; cygwin*|msys*) - # Windows - echo "Activating virtual environment on Windows..." source .venv/Scripts/activate ;; *) - echo "::error:: Unknown OS. Please activate the virtual environment manually." + echo "::error:: Unknown OS. Please add an activation method for '$OSTYPE'." exit 1 ;; esac -python -m pip install uv==$UV_VERSION -# python -m uv build -# wheel_package=$(ls dist/*.whl) -# python -m uv pip install "$PROJECT @ $wheel_package" -c test-requirements.txt +# Install uv in virtual environment +python -m pip install uv==$UV_VERSION if [ "$CHECK_FORMATTING" = "1" ]; then python -m uv sync --extra tests --extra tools @@ -65,7 +54,6 @@ else # expands to 0 != 1 if NO_TEST_REQUIREMENTS is not set, if set the `-0` has no effect # https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_02 if [ "${NO_TEST_REQUIREMENTS-0}" == 1 ]; then - # python -m uv pip install pytest coverage -c test-requirements.txt python -m uv sync --extra tests flags="" #"--skip-optional-imports" @@ -90,9 +78,6 @@ else # get mypy tests a nice cache MYPYPATH=".." mypy --config-file= --cache-dir=./.mypy_cache -c "import $PROJECT" >/dev/null 2>/dev/null || true - # support subprocess spawning with coverage.py - # echo "import coverage; coverage.process_startup()" | tee -a "$INSTALLDIR/../sitecustomize.py" - echo "::endgroup::" echo "::group:: Run Tests" if coverage run --rcfile=../pyproject.toml -m pytest -ra --junitxml=../test-results.xml ../tests --verbose --durations=10 $flags; then From 1555747966239e4adf37bf29d24ec836fb9ce188 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Thu, 13 Feb 2025 10:46:12 -0600 Subject: [PATCH 50/58] Update depencencies & minor CI changes --- .github/workflows/autodeps.yml | 2 +- .pre-commit-config.yaml | 12 +- ci.sh | 8 +- uv.lock | 368 +++++++++++++++++---------------- 4 files changed, 199 insertions(+), 191 deletions(-) diff --git a/.github/workflows/autodeps.yml b/.github/workflows/autodeps.yml index a41f4ff..1862fb9 100644 --- a/.github/workflows/autodeps.yml +++ b/.github/workflows/autodeps.yml @@ -40,7 +40,7 @@ jobs: # apply newer versions' formatting - name: Pre-commit updates - run: uv run pre-commit run -a + run: uv run pre-commit run -a || true - name: Commit changes and create automerge PR env: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e689c73..3f9da0e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,33 +16,35 @@ repos: - id: check-merge-conflict - id: mixed-line-ending - id: check-case-conflict + - id: check-added-large-files - id: sort-simple-yaml files: .pre-commit-config.yaml - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.10.0 + rev: 25.1.0 hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.2 + rev: v0.9.6 hooks: - id: ruff types: [file] types_or: [python, pyi, toml] + args: ["--show-fixes"] - repo: https://github.com/CoolCat467/badgie rev: v0.9.6 hooks: - id: badgie - repo: https://github.com/codespell-project/codespell - rev: v2.3.0 + rev: v2.4.1 hooks: - id: codespell additional_dependencies: - tomli - repo: https://github.com/crate-ci/typos - rev: dictgen-v0.3.1 + rev: typos-dict-v0.12.5 hooks: - id: typos - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.2.1 + rev: v1.3.1 hooks: - id: zizmor diff --git a/ci.sh b/ci.sh index e7d4c92..02ea341 100755 --- a/ci.sh +++ b/ci.sh @@ -28,7 +28,7 @@ python -m uv --version UV_VENV_SEED="pip" python -m uv venv --seed --allow-existing -# Determine the platform and activate the virtual environment accordingly +# Determine platform and activate virtual environment accordingly case "$OSTYPE" in linux-gnu*|linux-musl*|darwin*) source .venv/bin/activate @@ -46,7 +46,7 @@ esac python -m pip install uv==$UV_VERSION if [ "$CHECK_FORMATTING" = "1" ]; then - python -m uv sync --extra tests --extra tools + python -m uv sync --locked --extra tests --extra tools echo "::endgroup::" source check.sh else @@ -54,11 +54,11 @@ else # expands to 0 != 1 if NO_TEST_REQUIREMENTS is not set, if set the `-0` has no effect # https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_02 if [ "${NO_TEST_REQUIREMENTS-0}" == 1 ]; then - python -m uv sync --extra tests + python -m uv sync --locked --extra tests flags="" #"--skip-optional-imports" else - python -m uv sync --extra tests --extra tools + python -m uv sync --locked --extra tests --extra tools flags="" fi diff --git a/uv.lock b/uv.lock index a9fa758..c3cebe9 100644 --- a/uv.lock +++ b/uv.lock @@ -3,11 +3,11 @@ requires-python = ">=3.10" [[package]] name = "attrs" -version = "24.3.0" +version = "25.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/48/c8/6260f8ccc11f0917360fc0da435c5c9c7504e3db174d5a12a1494887b045/attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff", size = 805984 } +sdist = { url = "https://files.pythonhosted.org/packages/49/7c/fdf464bcc51d23881d110abd74b512a42b3d5d376a55a831b44c603ae17f/attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e", size = 810562 } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/aa/ab0f7891a01eeb2d2e338ae8fecbe57fcebea1a24dbb64d45801bfab481d/attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308", size = 63397 }, + { url = "https://files.pythonhosted.org/packages/fc/30/d4986a882011f9df997a55e6becd864812ccfcd821d64aac8570ee39f719/attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a", size = 63152 }, ] [[package]] @@ -60,7 +60,7 @@ requires-dist = [ [[package]] name = "black" -version = "24.10.0" +version = "25.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -71,25 +71,25 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d8/0d/cc2fb42b8c50d80143221515dd7e4766995bd07c56c9a3ed30baf080b6dc/black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875", size = 645813 } +sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/f3/465c0eb5cddf7dbbfe1fecd9b875d1dcf51b88923cd2c1d7e9ab95c6336b/black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812", size = 1623211 }, - { url = "https://files.pythonhosted.org/packages/df/57/b6d2da7d200773fdfcc224ffb87052cf283cec4d7102fab450b4a05996d8/black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea", size = 1457139 }, - { url = "https://files.pythonhosted.org/packages/6e/c5/9023b7673904a5188f9be81f5e129fff69f51f5515655fbd1d5a4e80a47b/black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f", size = 1753774 }, - { url = "https://files.pythonhosted.org/packages/e1/32/df7f18bd0e724e0d9748829765455d6643ec847b3f87e77456fc99d0edab/black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e", size = 1414209 }, - { url = "https://files.pythonhosted.org/packages/c2/cc/7496bb63a9b06a954d3d0ac9fe7a73f3bf1cd92d7a58877c27f4ad1e9d41/black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad", size = 1607468 }, - { url = "https://files.pythonhosted.org/packages/2b/e3/69a738fb5ba18b5422f50b4f143544c664d7da40f09c13969b2fd52900e0/black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50", size = 1437270 }, - { url = "https://files.pythonhosted.org/packages/c9/9b/2db8045b45844665c720dcfe292fdaf2e49825810c0103e1191515fc101a/black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392", size = 1737061 }, - { url = "https://files.pythonhosted.org/packages/a3/95/17d4a09a5be5f8c65aa4a361444d95edc45def0de887810f508d3f65db7a/black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175", size = 1423293 }, - { url = "https://files.pythonhosted.org/packages/90/04/bf74c71f592bcd761610bbf67e23e6a3cff824780761f536512437f1e655/black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3", size = 1644256 }, - { url = "https://files.pythonhosted.org/packages/4c/ea/a77bab4cf1887f4b2e0bce5516ea0b3ff7d04ba96af21d65024629afedb6/black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65", size = 1448534 }, - { url = "https://files.pythonhosted.org/packages/4e/3e/443ef8bc1fbda78e61f79157f303893f3fddf19ca3c8989b163eb3469a12/black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f", size = 1761892 }, - { url = "https://files.pythonhosted.org/packages/52/93/eac95ff229049a6901bc84fec6908a5124b8a0b7c26ea766b3b8a5debd22/black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8", size = 1434796 }, - { url = "https://files.pythonhosted.org/packages/d0/a0/a993f58d4ecfba035e61fca4e9f64a2ecae838fc9f33ab798c62173ed75c/black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981", size = 1643986 }, - { url = "https://files.pythonhosted.org/packages/37/d5/602d0ef5dfcace3fb4f79c436762f130abd9ee8d950fa2abdbf8bbc555e0/black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b", size = 1448085 }, - { url = "https://files.pythonhosted.org/packages/47/6d/a3a239e938960df1a662b93d6230d4f3e9b4a22982d060fc38c42f45a56b/black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2", size = 1760928 }, - { url = "https://files.pythonhosted.org/packages/dd/cf/af018e13b0eddfb434df4d9cd1b2b7892bab119f7a20123e93f6910982e8/black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b", size = 1436875 }, - { url = "https://files.pythonhosted.org/packages/8d/a7/4b27c50537ebca8bec139b872861f9d2bf501c5ec51fcf897cb924d9e264/black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d", size = 206898 }, + { url = "https://files.pythonhosted.org/packages/4d/3b/4ba3f93ac8d90410423fdd31d7541ada9bcee1df32fb90d26de41ed40e1d/black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32", size = 1629419 }, + { url = "https://files.pythonhosted.org/packages/b4/02/0bde0485146a8a5e694daed47561785e8b77a0466ccc1f3e485d5ef2925e/black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da", size = 1461080 }, + { url = "https://files.pythonhosted.org/packages/52/0e/abdf75183c830eaca7589144ff96d49bce73d7ec6ad12ef62185cc0f79a2/black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7", size = 1766886 }, + { url = "https://files.pythonhosted.org/packages/dc/a6/97d8bb65b1d8a41f8a6736222ba0a334db7b7b77b8023ab4568288f23973/black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9", size = 1419404 }, + { url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372 }, + { url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865 }, + { url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699 }, + { url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028 }, + { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988 }, + { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985 }, + { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816 }, + { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860 }, + { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673 }, + { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190 }, + { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926 }, + { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613 }, + { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646 }, ] [[package]] @@ -163,11 +163,11 @@ wheels = [ [[package]] name = "codespell" -version = "2.3.0" +version = "2.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a0/a9/98353dfc7afcdf18cffd2dd3e959a25eaaf2728cf450caa59af89648a8e4/codespell-2.3.0.tar.gz", hash = "sha256:360c7d10f75e65f67bad720af7007e1060a5d395670ec11a7ed1fed9dd17471f", size = 329791 } +sdist = { url = "https://files.pythonhosted.org/packages/15/e0/709453393c0ea77d007d907dd436b3ee262e28b30995ea1aa36c6ffbccaf/codespell-2.4.1.tar.gz", hash = "sha256:299fcdcb09d23e81e35a671bbe746d5ad7e8385972e65dbb833a2eaac33c01e5", size = 344740 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/20/b6019add11e84f821184234cea0ad91442373489ef7ccfa3d73a71b908fa/codespell-2.3.0-py3-none-any.whl", hash = "sha256:a9c7cef2501c9cfede2110fd6d4e5e62296920efe9abfb84648df866e47f58d1", size = 329167 }, + { url = "https://files.pythonhosted.org/packages/20/01/b394922252051e97aab231d416c86da3d8a6d781eeadcdca1082867de64e/codespell-2.4.1-py3-none-any.whl", hash = "sha256:3dadafa67df7e4a3dbf51e0d7315061b80d265f9552ebd699b3dd6834b47e425", size = 344501 }, ] [[package]] @@ -181,61 +181,62 @@ wheels = [ [[package]] name = "coverage" -version = "7.6.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/84/ba/ac14d281f80aab516275012e8875991bb06203957aa1e19950139238d658/coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23", size = 803868 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/12/2a2a923edf4ddabdffed7ad6da50d96a5c126dae7b80a33df7310e329a1e/coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78", size = 207982 }, - { url = "https://files.pythonhosted.org/packages/ca/49/6985dbca9c7be3f3cb62a2e6e492a0c88b65bf40579e16c71ae9c33c6b23/coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c", size = 208414 }, - { url = "https://files.pythonhosted.org/packages/35/93/287e8f1d1ed2646f4e0b2605d14616c9a8a2697d0d1b453815eb5c6cebdb/coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a", size = 236860 }, - { url = "https://files.pythonhosted.org/packages/de/e1/cfdb5627a03567a10031acc629b75d45a4ca1616e54f7133ca1fa366050a/coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165", size = 234758 }, - { url = "https://files.pythonhosted.org/packages/6d/85/fc0de2bcda3f97c2ee9fe8568f7d48f7279e91068958e5b2cc19e0e5f600/coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988", size = 235920 }, - { url = "https://files.pythonhosted.org/packages/79/73/ef4ea0105531506a6f4cf4ba571a214b14a884630b567ed65b3d9c1975e1/coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5", size = 234986 }, - { url = "https://files.pythonhosted.org/packages/c6/4d/75afcfe4432e2ad0405c6f27adeb109ff8976c5e636af8604f94f29fa3fc/coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3", size = 233446 }, - { url = "https://files.pythonhosted.org/packages/86/5b/efee56a89c16171288cafff022e8af44f8f94075c2d8da563c3935212871/coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5", size = 234566 }, - { url = "https://files.pythonhosted.org/packages/f2/db/67770cceb4a64d3198bf2aa49946f411b85ec6b0a9b489e61c8467a4253b/coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244", size = 210675 }, - { url = "https://files.pythonhosted.org/packages/8d/27/e8bfc43f5345ec2c27bc8a1fa77cdc5ce9dcf954445e11f14bb70b889d14/coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e", size = 211518 }, - { url = "https://files.pythonhosted.org/packages/85/d2/5e175fcf6766cf7501a8541d81778fd2f52f4870100e791f5327fd23270b/coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3", size = 208088 }, - { url = "https://files.pythonhosted.org/packages/4b/6f/06db4dc8fca33c13b673986e20e466fd936235a6ec1f0045c3853ac1b593/coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43", size = 208536 }, - { url = "https://files.pythonhosted.org/packages/0d/62/c6a0cf80318c1c1af376d52df444da3608eafc913b82c84a4600d8349472/coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132", size = 240474 }, - { url = "https://files.pythonhosted.org/packages/a3/59/750adafc2e57786d2e8739a46b680d4fb0fbc2d57fbcb161290a9f1ecf23/coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f", size = 237880 }, - { url = "https://files.pythonhosted.org/packages/2c/f8/ef009b3b98e9f7033c19deb40d629354aab1d8b2d7f9cfec284dbedf5096/coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994", size = 239750 }, - { url = "https://files.pythonhosted.org/packages/a6/e2/6622f3b70f5f5b59f705e680dae6db64421af05a5d1e389afd24dae62e5b/coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99", size = 238642 }, - { url = "https://files.pythonhosted.org/packages/2d/10/57ac3f191a3c95c67844099514ff44e6e19b2915cd1c22269fb27f9b17b6/coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd", size = 237266 }, - { url = "https://files.pythonhosted.org/packages/ee/2d/7016f4ad9d553cabcb7333ed78ff9d27248ec4eba8dd21fa488254dff894/coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377", size = 238045 }, - { url = "https://files.pythonhosted.org/packages/a7/fe/45af5c82389a71e0cae4546413266d2195c3744849669b0bab4b5f2c75da/coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8", size = 210647 }, - { url = "https://files.pythonhosted.org/packages/db/11/3f8e803a43b79bc534c6a506674da9d614e990e37118b4506faf70d46ed6/coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609", size = 211508 }, - { url = "https://files.pythonhosted.org/packages/86/77/19d09ea06f92fdf0487499283b1b7af06bc422ea94534c8fe3a4cd023641/coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853", size = 208281 }, - { url = "https://files.pythonhosted.org/packages/b6/67/5479b9f2f99fcfb49c0d5cf61912a5255ef80b6e80a3cddba39c38146cf4/coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078", size = 208514 }, - { url = "https://files.pythonhosted.org/packages/15/d1/febf59030ce1c83b7331c3546d7317e5120c5966471727aa7ac157729c4b/coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0", size = 241537 }, - { url = "https://files.pythonhosted.org/packages/4b/7e/5ac4c90192130e7cf8b63153fe620c8bfd9068f89a6d9b5f26f1550f7a26/coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50", size = 238572 }, - { url = "https://files.pythonhosted.org/packages/dc/03/0334a79b26ecf59958f2fe9dd1f5ab3e2f88db876f5071933de39af09647/coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022", size = 240639 }, - { url = "https://files.pythonhosted.org/packages/d7/45/8a707f23c202208d7b286d78ad6233f50dcf929319b664b6cc18a03c1aae/coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b", size = 240072 }, - { url = "https://files.pythonhosted.org/packages/66/02/603ce0ac2d02bc7b393279ef618940b4a0535b0868ee791140bda9ecfa40/coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0", size = 238386 }, - { url = "https://files.pythonhosted.org/packages/04/62/4e6887e9be060f5d18f1dd58c2838b2d9646faf353232dec4e2d4b1c8644/coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852", size = 240054 }, - { url = "https://files.pythonhosted.org/packages/5c/74/83ae4151c170d8bd071924f212add22a0e62a7fe2b149edf016aeecad17c/coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359", size = 210904 }, - { url = "https://files.pythonhosted.org/packages/c3/54/de0893186a221478f5880283119fc40483bc460b27c4c71d1b8bba3474b9/coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247", size = 211692 }, - { url = "https://files.pythonhosted.org/packages/25/6d/31883d78865529257bf847df5789e2ae80e99de8a460c3453dbfbe0db069/coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9", size = 208308 }, - { url = "https://files.pythonhosted.org/packages/70/22/3f2b129cc08de00c83b0ad6252e034320946abfc3e4235c009e57cfeee05/coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b", size = 208565 }, - { url = "https://files.pythonhosted.org/packages/97/0a/d89bc2d1cc61d3a8dfe9e9d75217b2be85f6c73ebf1b9e3c2f4e797f4531/coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690", size = 241083 }, - { url = "https://files.pythonhosted.org/packages/4c/81/6d64b88a00c7a7aaed3a657b8eaa0931f37a6395fcef61e53ff742b49c97/coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18", size = 238235 }, - { url = "https://files.pythonhosted.org/packages/9a/0b/7797d4193f5adb4b837207ed87fecf5fc38f7cc612b369a8e8e12d9fa114/coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c", size = 240220 }, - { url = "https://files.pythonhosted.org/packages/65/4d/6f83ca1bddcf8e51bf8ff71572f39a1c73c34cf50e752a952c34f24d0a60/coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd", size = 239847 }, - { url = "https://files.pythonhosted.org/packages/30/9d/2470df6aa146aff4c65fee0f87f58d2164a67533c771c9cc12ffcdb865d5/coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e", size = 237922 }, - { url = "https://files.pythonhosted.org/packages/08/dd/723fef5d901e6a89f2507094db66c091449c8ba03272861eaefa773ad95c/coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694", size = 239783 }, - { url = "https://files.pythonhosted.org/packages/3d/f7/64d3298b2baf261cb35466000628706ce20a82d42faf9b771af447cd2b76/coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6", size = 210965 }, - { url = "https://files.pythonhosted.org/packages/d5/58/ec43499a7fc681212fe7742fe90b2bc361cdb72e3181ace1604247a5b24d/coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e", size = 211719 }, - { url = "https://files.pythonhosted.org/packages/ab/c9/f2857a135bcff4330c1e90e7d03446b036b2363d4ad37eb5e3a47bbac8a6/coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe", size = 209050 }, - { url = "https://files.pythonhosted.org/packages/aa/b3/f840e5bd777d8433caa9e4a1eb20503495709f697341ac1a8ee6a3c906ad/coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273", size = 209321 }, - { url = "https://files.pythonhosted.org/packages/85/7d/125a5362180fcc1c03d91850fc020f3831d5cda09319522bcfa6b2b70be7/coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8", size = 252039 }, - { url = "https://files.pythonhosted.org/packages/a9/9c/4358bf3c74baf1f9bddd2baf3756b54c07f2cfd2535f0a47f1e7757e54b3/coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098", size = 247758 }, - { url = "https://files.pythonhosted.org/packages/cf/c7/de3eb6fc5263b26fab5cda3de7a0f80e317597a4bad4781859f72885f300/coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb", size = 250119 }, - { url = "https://files.pythonhosted.org/packages/3e/e6/43de91f8ba2ec9140c6a4af1102141712949903dc732cf739167cfa7a3bc/coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0", size = 249597 }, - { url = "https://files.pythonhosted.org/packages/08/40/61158b5499aa2adf9e37bc6d0117e8f6788625b283d51e7e0c53cf340530/coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf", size = 247473 }, - { url = "https://files.pythonhosted.org/packages/50/69/b3f2416725621e9f112e74e8470793d5b5995f146f596f133678a633b77e/coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2", size = 248737 }, - { url = "https://files.pythonhosted.org/packages/3c/6e/fe899fb937657db6df31cc3e61c6968cb56d36d7326361847440a430152e/coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312", size = 211611 }, - { url = "https://files.pythonhosted.org/packages/1c/55/52f5e66142a9d7bc93a15192eba7a78513d2abf6b3558d77b4ca32f5f424/coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d", size = 212781 }, - { url = "https://files.pythonhosted.org/packages/a1/70/de81bfec9ed38a64fc44a77c7665e20ca507fc3265597c28b0d989e4082e/coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f", size = 200223 }, +version = "7.6.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/d6/2b53ab3ee99f2262e6f0b8369a43f6d66658eab45510331c0b3d5c8c4272/coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2", size = 805941 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/67/81dc41ec8f548c365d04a29f1afd492d3176b372c33e47fa2a45a01dc13a/coverage-7.6.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:704c8c8c6ce6569286ae9622e534b4f5b9759b6f2cd643f1c1a61f666d534fe8", size = 208345 }, + { url = "https://files.pythonhosted.org/packages/33/43/17f71676016c8829bde69e24c852fef6bd9ed39f774a245d9ec98f689fa0/coverage-7.6.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ad7525bf0241e5502168ae9c643a2f6c219fa0a283001cee4cf23a9b7da75879", size = 208775 }, + { url = "https://files.pythonhosted.org/packages/86/25/c6ff0775f8960e8c0840845b723eed978d22a3cd9babd2b996e4a7c502c6/coverage-7.6.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06097c7abfa611c91edb9e6920264e5be1d6ceb374efb4986f38b09eed4cb2fe", size = 237925 }, + { url = "https://files.pythonhosted.org/packages/b0/3d/5f5bd37046243cb9d15fff2c69e498c2f4fe4f9b42a96018d4579ed3506f/coverage-7.6.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:220fa6c0ad7d9caef57f2c8771918324563ef0d8272c94974717c3909664e674", size = 235835 }, + { url = "https://files.pythonhosted.org/packages/b5/f1/9e6b75531fe33490b910d251b0bf709142e73a40e4e38a3899e6986fe088/coverage-7.6.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3688b99604a24492bcfe1c106278c45586eb819bf66a654d8a9a1433022fb2eb", size = 236966 }, + { url = "https://files.pythonhosted.org/packages/4f/bc/aef5a98f9133851bd1aacf130e754063719345d2fb776a117d5a8d516971/coverage-7.6.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1a987778b9c71da2fc8948e6f2656da6ef68f59298b7e9786849634c35d2c3c", size = 236080 }, + { url = "https://files.pythonhosted.org/packages/eb/d0/56b4ab77f9b12aea4d4c11dc11cdcaa7c29130b837eb610639cf3400c9c3/coverage-7.6.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cec6b9ce3bd2b7853d4a4563801292bfee40b030c05a3d29555fd2a8ee9bd68c", size = 234393 }, + { url = "https://files.pythonhosted.org/packages/0d/77/28ef95c5d23fe3dd191a0b7d89c82fea2c2d904aef9315daf7c890e96557/coverage-7.6.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ace9048de91293e467b44bce0f0381345078389814ff6e18dbac8fdbf896360e", size = 235536 }, + { url = "https://files.pythonhosted.org/packages/29/62/18791d3632ee3ff3f95bc8599115707d05229c72db9539f208bb878a3d88/coverage-7.6.12-cp310-cp310-win32.whl", hash = "sha256:ea31689f05043d520113e0552f039603c4dd71fa4c287b64cb3606140c66f425", size = 211063 }, + { url = "https://files.pythonhosted.org/packages/fc/57/b3878006cedfd573c963e5c751b8587154eb10a61cc0f47a84f85c88a355/coverage-7.6.12-cp310-cp310-win_amd64.whl", hash = "sha256:676f92141e3c5492d2a1596d52287d0d963df21bf5e55c8b03075a60e1ddf8aa", size = 211955 }, + { url = "https://files.pythonhosted.org/packages/64/2d/da78abbfff98468c91fd63a73cccdfa0e99051676ded8dd36123e3a2d4d5/coverage-7.6.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e18aafdfb3e9ec0d261c942d35bd7c28d031c5855dadb491d2723ba54f4c3015", size = 208464 }, + { url = "https://files.pythonhosted.org/packages/31/f2/c269f46c470bdabe83a69e860c80a82e5e76840e9f4bbd7f38f8cebbee2f/coverage-7.6.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66fe626fd7aa5982cdebad23e49e78ef7dbb3e3c2a5960a2b53632f1f703ea45", size = 208893 }, + { url = "https://files.pythonhosted.org/packages/47/63/5682bf14d2ce20819998a49c0deadb81e608a59eed64d6bc2191bc8046b9/coverage-7.6.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ef01d70198431719af0b1f5dcbefc557d44a190e749004042927b2a3fed0702", size = 241545 }, + { url = "https://files.pythonhosted.org/packages/6a/b6/6b6631f1172d437e11067e1c2edfdb7238b65dff965a12bce3b6d1bf2be2/coverage-7.6.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e92ae5a289a4bc4c0aae710c0948d3c7892e20fd3588224ebe242039573bf0", size = 239230 }, + { url = "https://files.pythonhosted.org/packages/c7/01/9cd06cbb1be53e837e16f1b4309f6357e2dfcbdab0dd7cd3b1a50589e4e1/coverage-7.6.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e695df2c58ce526eeab11a2e915448d3eb76f75dffe338ea613c1201b33bab2f", size = 241013 }, + { url = "https://files.pythonhosted.org/packages/4b/26/56afefc03c30871326e3d99709a70d327ac1f33da383cba108c79bd71563/coverage-7.6.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d74c08e9aaef995f8c4ef6d202dbd219c318450fe2a76da624f2ebb9c8ec5d9f", size = 239750 }, + { url = "https://files.pythonhosted.org/packages/dd/ea/88a1ff951ed288f56aa561558ebe380107cf9132facd0b50bced63ba7238/coverage-7.6.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e995b3b76ccedc27fe4f477b349b7d64597e53a43fc2961db9d3fbace085d69d", size = 238462 }, + { url = "https://files.pythonhosted.org/packages/6e/d4/1d9404566f553728889409eff82151d515fbb46dc92cbd13b5337fa0de8c/coverage-7.6.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b1f097878d74fe51e1ddd1be62d8e3682748875b461232cf4b52ddc6e6db0bba", size = 239307 }, + { url = "https://files.pythonhosted.org/packages/12/c1/e453d3b794cde1e232ee8ac1d194fde8e2ba329c18bbf1b93f6f5eef606b/coverage-7.6.12-cp311-cp311-win32.whl", hash = "sha256:1f7ffa05da41754e20512202c866d0ebfc440bba3b0ed15133070e20bf5aeb5f", size = 211117 }, + { url = "https://files.pythonhosted.org/packages/d5/db/829185120c1686fa297294f8fcd23e0422f71070bf85ef1cc1a72ecb2930/coverage-7.6.12-cp311-cp311-win_amd64.whl", hash = "sha256:e216c5c45f89ef8971373fd1c5d8d1164b81f7f5f06bbf23c37e7908d19e8558", size = 212019 }, + { url = "https://files.pythonhosted.org/packages/e2/7f/4af2ed1d06ce6bee7eafc03b2ef748b14132b0bdae04388e451e4b2c529b/coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad", size = 208645 }, + { url = "https://files.pythonhosted.org/packages/dc/60/d19df912989117caa95123524d26fc973f56dc14aecdec5ccd7d0084e131/coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3", size = 208898 }, + { url = "https://files.pythonhosted.org/packages/bd/10/fecabcf438ba676f706bf90186ccf6ff9f6158cc494286965c76e58742fa/coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574", size = 242987 }, + { url = "https://files.pythonhosted.org/packages/4c/53/4e208440389e8ea936f5f2b0762dcd4cb03281a7722def8e2bf9dc9c3d68/coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985", size = 239881 }, + { url = "https://files.pythonhosted.org/packages/c4/47/2ba744af8d2f0caa1f17e7746147e34dfc5f811fb65fc153153722d58835/coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750", size = 242142 }, + { url = "https://files.pythonhosted.org/packages/e9/90/df726af8ee74d92ee7e3bf113bf101ea4315d71508952bd21abc3fae471e/coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea", size = 241437 }, + { url = "https://files.pythonhosted.org/packages/f6/af/995263fd04ae5f9cf12521150295bf03b6ba940d0aea97953bb4a6db3e2b/coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3", size = 239724 }, + { url = "https://files.pythonhosted.org/packages/1c/8e/5bb04f0318805e190984c6ce106b4c3968a9562a400180e549855d8211bd/coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a", size = 241329 }, + { url = "https://files.pythonhosted.org/packages/9e/9d/fa04d9e6c3f6459f4e0b231925277cfc33d72dfab7fa19c312c03e59da99/coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95", size = 211289 }, + { url = "https://files.pythonhosted.org/packages/53/40/53c7ffe3c0c3fff4d708bc99e65f3d78c129110d6629736faf2dbd60ad57/coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288", size = 212079 }, + { url = "https://files.pythonhosted.org/packages/76/89/1adf3e634753c0de3dad2f02aac1e73dba58bc5a3a914ac94a25b2ef418f/coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1", size = 208673 }, + { url = "https://files.pythonhosted.org/packages/ce/64/92a4e239d64d798535c5b45baac6b891c205a8a2e7c9cc8590ad386693dc/coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd", size = 208945 }, + { url = "https://files.pythonhosted.org/packages/b4/d0/4596a3ef3bca20a94539c9b1e10fd250225d1dec57ea78b0867a1cf9742e/coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9", size = 242484 }, + { url = "https://files.pythonhosted.org/packages/1c/ef/6fd0d344695af6718a38d0861408af48a709327335486a7ad7e85936dc6e/coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e", size = 239525 }, + { url = "https://files.pythonhosted.org/packages/0c/4b/373be2be7dd42f2bcd6964059fd8fa307d265a29d2b9bcf1d044bcc156ed/coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4", size = 241545 }, + { url = "https://files.pythonhosted.org/packages/a6/7d/0e83cc2673a7790650851ee92f72a343827ecaaea07960587c8f442b5cd3/coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6", size = 241179 }, + { url = "https://files.pythonhosted.org/packages/ff/8c/566ea92ce2bb7627b0900124e24a99f9244b6c8c92d09ff9f7633eb7c3c8/coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3", size = 239288 }, + { url = "https://files.pythonhosted.org/packages/7d/e4/869a138e50b622f796782d642c15fb5f25a5870c6d0059a663667a201638/coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc", size = 241032 }, + { url = "https://files.pythonhosted.org/packages/ae/28/a52ff5d62a9f9e9fe9c4f17759b98632edd3a3489fce70154c7d66054dd3/coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3", size = 211315 }, + { url = "https://files.pythonhosted.org/packages/bc/17/ab849b7429a639f9722fa5628364c28d675c7ff37ebc3268fe9840dda13c/coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef", size = 212099 }, + { url = "https://files.pythonhosted.org/packages/d2/1c/b9965bf23e171d98505eb5eb4fb4d05c44efd256f2e0f19ad1ba8c3f54b0/coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e", size = 209511 }, + { url = "https://files.pythonhosted.org/packages/57/b3/119c201d3b692d5e17784fee876a9a78e1b3051327de2709392962877ca8/coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703", size = 209729 }, + { url = "https://files.pythonhosted.org/packages/52/4e/a7feb5a56b266304bc59f872ea07b728e14d5a64f1ad3a2cc01a3259c965/coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0", size = 253988 }, + { url = "https://files.pythonhosted.org/packages/65/19/069fec4d6908d0dae98126aa7ad08ce5130a6decc8509da7740d36e8e8d2/coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924", size = 249697 }, + { url = "https://files.pythonhosted.org/packages/1c/da/5b19f09ba39df7c55f77820736bf17bbe2416bbf5216a3100ac019e15839/coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b", size = 252033 }, + { url = "https://files.pythonhosted.org/packages/1e/89/4c2750df7f80a7872267f7c5fe497c69d45f688f7b3afe1297e52e33f791/coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d", size = 251535 }, + { url = "https://files.pythonhosted.org/packages/78/3b/6d3ae3c1cc05f1b0460c51e6f6dcf567598cbd7c6121e5ad06643974703c/coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827", size = 249192 }, + { url = "https://files.pythonhosted.org/packages/6e/8e/c14a79f535ce41af7d436bbad0d3d90c43d9e38ec409b4770c894031422e/coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9", size = 250627 }, + { url = "https://files.pythonhosted.org/packages/cb/79/b7cee656cfb17a7f2c1b9c3cee03dd5d8000ca299ad4038ba64b61a9b044/coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3", size = 212033 }, + { url = "https://files.pythonhosted.org/packages/b6/c3/f7aaa3813f1fa9a4228175a7bd368199659d392897e184435a3b66408dd3/coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f", size = 213240 }, + { url = "https://files.pythonhosted.org/packages/7a/7f/05818c62c7afe75df11e0233bd670948d68b36cdbf2a339a095bc02624a8/coverage-7.6.12-pp39.pp310-none-any.whl", hash = "sha256:7e39e845c4d764208e7b8f6a21c541ade741e2c41afabdfa1caa28687a3c98cf", size = 200558 }, + { url = "https://files.pythonhosted.org/packages/fb/b2/f655700e1024dec98b10ebaafd0cedbc25e40e4abe62a3c8e2ceef4f8f0a/coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953", size = 200552 }, ] [package.optional-dependencies] @@ -245,39 +246,43 @@ toml = [ [[package]] name = "cryptography" -version = "44.0.0" +version = "44.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/91/4c/45dfa6829acffa344e3967d6006ee4ae8be57af746ae2eba1c431949b32c/cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02", size = 710657 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/55/09/8cc67f9b84730ad330b3b72cf867150744bf07ff113cda21a15a1c6d2c7c/cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123", size = 6541833 }, - { url = "https://files.pythonhosted.org/packages/7e/5b/3759e30a103144e29632e7cb72aec28cedc79e514b2ea8896bb17163c19b/cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092", size = 3922710 }, - { url = "https://files.pythonhosted.org/packages/5f/58/3b14bf39f1a0cfd679e753e8647ada56cddbf5acebffe7db90e184c76168/cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f", size = 4137546 }, - { url = "https://files.pythonhosted.org/packages/98/65/13d9e76ca19b0ba5603d71ac8424b5694415b348e719db277b5edc985ff5/cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb", size = 3915420 }, - { url = "https://files.pythonhosted.org/packages/b1/07/40fe09ce96b91fc9276a9ad272832ead0fddedcba87f1190372af8e3039c/cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b", size = 4154498 }, - { url = "https://files.pythonhosted.org/packages/75/ea/af65619c800ec0a7e4034207aec543acdf248d9bffba0533342d1bd435e1/cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543", size = 3932569 }, - { url = "https://files.pythonhosted.org/packages/c7/af/d1deb0c04d59612e3d5e54203159e284d3e7a6921e565bb0eeb6269bdd8a/cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e", size = 4016721 }, - { url = "https://files.pythonhosted.org/packages/bd/69/7ca326c55698d0688db867795134bdfac87136b80ef373aaa42b225d6dd5/cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e", size = 4240915 }, - { url = "https://files.pythonhosted.org/packages/ef/d4/cae11bf68c0f981e0413906c6dd03ae7fa864347ed5fac40021df1ef467c/cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053", size = 2757925 }, - { url = "https://files.pythonhosted.org/packages/64/b1/50d7739254d2002acae64eed4fc43b24ac0cc44bf0a0d388d1ca06ec5bb1/cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd", size = 3202055 }, - { url = "https://files.pythonhosted.org/packages/11/18/61e52a3d28fc1514a43b0ac291177acd1b4de00e9301aaf7ef867076ff8a/cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591", size = 6542801 }, - { url = "https://files.pythonhosted.org/packages/1a/07/5f165b6c65696ef75601b781a280fc3b33f1e0cd6aa5a92d9fb96c410e97/cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7", size = 3922613 }, - { url = "https://files.pythonhosted.org/packages/28/34/6b3ac1d80fc174812486561cf25194338151780f27e438526f9c64e16869/cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc", size = 4137925 }, - { url = "https://files.pythonhosted.org/packages/d0/c7/c656eb08fd22255d21bc3129625ed9cd5ee305f33752ef2278711b3fa98b/cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289", size = 3915417 }, - { url = "https://files.pythonhosted.org/packages/ef/82/72403624f197af0db6bac4e58153bc9ac0e6020e57234115db9596eee85d/cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7", size = 4155160 }, - { url = "https://files.pythonhosted.org/packages/a2/cd/2f3c440913d4329ade49b146d74f2e9766422e1732613f57097fea61f344/cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c", size = 3932331 }, - { url = "https://files.pythonhosted.org/packages/7f/df/8be88797f0a1cca6e255189a57bb49237402b1880d6e8721690c5603ac23/cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64", size = 4017372 }, - { url = "https://files.pythonhosted.org/packages/af/36/5ccc376f025a834e72b8e52e18746b927f34e4520487098e283a719c205e/cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285", size = 4239657 }, - { url = "https://files.pythonhosted.org/packages/46/b0/f4f7d0d0bcfbc8dd6296c1449be326d04217c57afb8b2594f017eed95533/cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417", size = 2758672 }, - { url = "https://files.pythonhosted.org/packages/97/9b/443270b9210f13f6ef240eff73fd32e02d381e7103969dc66ce8e89ee901/cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede", size = 3202071 }, - { url = "https://files.pythonhosted.org/packages/77/d4/fea74422326388bbac0c37b7489a0fcb1681a698c3b875959430ba550daa/cryptography-44.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731", size = 3338857 }, - { url = "https://files.pythonhosted.org/packages/1a/aa/ba8a7467c206cb7b62f09b4168da541b5109838627f582843bbbe0235e8e/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4", size = 3850615 }, - { url = "https://files.pythonhosted.org/packages/89/fa/b160e10a64cc395d090105be14f399b94e617c879efd401188ce0fea39ee/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756", size = 4081622 }, - { url = "https://files.pythonhosted.org/packages/47/8f/20ff0656bb0cf7af26ec1d01f780c5cfbaa7666736063378c5f48558b515/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c", size = 3867546 }, - { url = "https://files.pythonhosted.org/packages/38/d9/28edf32ee2fcdca587146bcde90102a7319b2f2c690edfa627e46d586050/cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa", size = 4090937 }, - { url = "https://files.pythonhosted.org/packages/cc/9d/37e5da7519de7b0b070a3fedd4230fe76d50d2a21403e0f2153d70ac4163/cryptography-44.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c", size = 3128774 }, +sdist = { url = "https://files.pythonhosted.org/packages/c7/67/545c79fe50f7af51dbad56d16b23fe33f63ee6a5d956b3cb68ea110cbe64/cryptography-44.0.1.tar.gz", hash = "sha256:f51f5705ab27898afda1aaa430f34ad90dc117421057782022edf0600bec5f14", size = 710819 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/27/5e3524053b4c8889da65cf7814a9d0d8514a05194a25e1e34f46852ee6eb/cryptography-44.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf688f615c29bfe9dfc44312ca470989279f0e94bb9f631f85e3459af8efc009", size = 6642022 }, + { url = "https://files.pythonhosted.org/packages/34/b9/4d1fa8d73ae6ec350012f89c3abfbff19fc95fe5420cf972e12a8d182986/cryptography-44.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd7c7e2d71d908dc0f8d2027e1604102140d84b155e658c20e8ad1304317691f", size = 3943865 }, + { url = "https://files.pythonhosted.org/packages/6e/57/371a9f3f3a4500807b5fcd29fec77f418ba27ffc629d88597d0d1049696e/cryptography-44.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887143b9ff6bad2b7570da75a7fe8bbf5f65276365ac259a5d2d5147a73775f2", size = 4162562 }, + { url = "https://files.pythonhosted.org/packages/c5/1d/5b77815e7d9cf1e3166988647f336f87d5634a5ccecec2ffbe08ef8dd481/cryptography-44.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:322eb03ecc62784536bc173f1483e76747aafeb69c8728df48537eb431cd1911", size = 3951923 }, + { url = "https://files.pythonhosted.org/packages/28/01/604508cd34a4024467cd4105887cf27da128cba3edd435b54e2395064bfb/cryptography-44.0.1-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:21377472ca4ada2906bc313168c9dc7b1d7ca417b63c1c3011d0c74b7de9ae69", size = 3685194 }, + { url = "https://files.pythonhosted.org/packages/c6/3d/d3c55d4f1d24580a236a6753902ef6d8aafd04da942a1ee9efb9dc8fd0cb/cryptography-44.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:df978682c1504fc93b3209de21aeabf2375cb1571d4e61907b3e7a2540e83026", size = 4187790 }, + { url = "https://files.pythonhosted.org/packages/ea/a6/44d63950c8588bfa8594fd234d3d46e93c3841b8e84a066649c566afb972/cryptography-44.0.1-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:eb3889330f2a4a148abead555399ec9a32b13b7c8ba969b72d8e500eb7ef84cd", size = 3951343 }, + { url = "https://files.pythonhosted.org/packages/c1/17/f5282661b57301204cbf188254c1a0267dbd8b18f76337f0a7ce1038888c/cryptography-44.0.1-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8e6a85a93d0642bd774460a86513c5d9d80b5c002ca9693e63f6e540f1815ed0", size = 4187127 }, + { url = "https://files.pythonhosted.org/packages/f3/68/abbae29ed4f9d96596687f3ceea8e233f65c9645fbbec68adb7c756bb85a/cryptography-44.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6f76fdd6fd048576a04c5210d53aa04ca34d2ed63336d4abd306d0cbe298fddf", size = 4070666 }, + { url = "https://files.pythonhosted.org/packages/0f/10/cf91691064a9e0a88ae27e31779200b1505d3aee877dbe1e4e0d73b4f155/cryptography-44.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6c8acf6f3d1f47acb2248ec3ea261171a671f3d9428e34ad0357148d492c7864", size = 4288811 }, + { url = "https://files.pythonhosted.org/packages/38/78/74ea9eb547d13c34e984e07ec8a473eb55b19c1451fe7fc8077c6a4b0548/cryptography-44.0.1-cp37-abi3-win32.whl", hash = "sha256:24979e9f2040c953a94bf3c6782e67795a4c260734e5264dceea65c8f4bae64a", size = 2771882 }, + { url = "https://files.pythonhosted.org/packages/cf/6c/3907271ee485679e15c9f5e93eac6aa318f859b0aed8d369afd636fafa87/cryptography-44.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:fd0ee90072861e276b0ff08bd627abec29e32a53b2be44e41dbcdf87cbee2b00", size = 3206989 }, + { url = "https://files.pythonhosted.org/packages/9f/f1/676e69c56a9be9fd1bffa9bc3492366901f6e1f8f4079428b05f1414e65c/cryptography-44.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a2d8a7045e1ab9b9f803f0d9531ead85f90c5f2859e653b61497228b18452008", size = 6643714 }, + { url = "https://files.pythonhosted.org/packages/ba/9f/1775600eb69e72d8f9931a104120f2667107a0ee478f6ad4fe4001559345/cryptography-44.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8272f257cf1cbd3f2e120f14c68bff2b6bdfcc157fafdee84a1b795efd72862", size = 3943269 }, + { url = "https://files.pythonhosted.org/packages/25/ba/e00d5ad6b58183829615be7f11f55a7b6baa5a06910faabdc9961527ba44/cryptography-44.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e8d181e90a777b63f3f0caa836844a1182f1f265687fac2115fcf245f5fbec3", size = 4166461 }, + { url = "https://files.pythonhosted.org/packages/b3/45/690a02c748d719a95ab08b6e4decb9d81e0ec1bac510358f61624c86e8a3/cryptography-44.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:436df4f203482f41aad60ed1813811ac4ab102765ecae7a2bbb1dbb66dcff5a7", size = 3950314 }, + { url = "https://files.pythonhosted.org/packages/e6/50/bf8d090911347f9b75adc20f6f6569ed6ca9b9bff552e6e390f53c2a1233/cryptography-44.0.1-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4f422e8c6a28cf8b7f883eb790695d6d45b0c385a2583073f3cec434cc705e1a", size = 3686675 }, + { url = "https://files.pythonhosted.org/packages/e1/e7/cfb18011821cc5f9b21efb3f94f3241e3a658d267a3bf3a0f45543858ed8/cryptography-44.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:72198e2b5925155497a5a3e8c216c7fb3e64c16ccee11f0e7da272fa93b35c4c", size = 4190429 }, + { url = "https://files.pythonhosted.org/packages/07/ef/77c74d94a8bfc1a8a47b3cafe54af3db537f081742ee7a8a9bd982b62774/cryptography-44.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a46a89ad3e6176223b632056f321bc7de36b9f9b93b2cc1cccf935a3849dc62", size = 3950039 }, + { url = "https://files.pythonhosted.org/packages/6d/b9/8be0ff57c4592382b77406269b1e15650c9f1a167f9e34941b8515b97159/cryptography-44.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:53f23339864b617a3dfc2b0ac8d5c432625c80014c25caac9082314e9de56f41", size = 4189713 }, + { url = "https://files.pythonhosted.org/packages/78/e1/4b6ac5f4100545513b0847a4d276fe3c7ce0eacfa73e3b5ebd31776816ee/cryptography-44.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:888fcc3fce0c888785a4876ca55f9f43787f4c5c1cc1e2e0da71ad481ff82c5b", size = 4071193 }, + { url = "https://files.pythonhosted.org/packages/3d/cb/afff48ceaed15531eab70445abe500f07f8f96af2bb35d98af6bfa89ebd4/cryptography-44.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00918d859aa4e57db8299607086f793fa7813ae2ff5a4637e318a25ef82730f7", size = 4289566 }, + { url = "https://files.pythonhosted.org/packages/30/6f/4eca9e2e0f13ae459acd1ca7d9f0257ab86e68f44304847610afcb813dc9/cryptography-44.0.1-cp39-abi3-win32.whl", hash = "sha256:9b336599e2cb77b1008cb2ac264b290803ec5e8e89d618a5e978ff5eb6f715d9", size = 2772371 }, + { url = "https://files.pythonhosted.org/packages/d2/05/5533d30f53f10239616a357f080892026db2d550a40c393d0a8a7af834a9/cryptography-44.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:e403f7f766ded778ecdb790da786b418a9f2394f36e8cc8b796cc056ab05f44f", size = 3207303 }, + { url = "https://files.pythonhosted.org/packages/15/06/507bfb5c7e048114a0185dd65f7814677a2ba285d15705c3d69e660c21d7/cryptography-44.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1f9a92144fa0c877117e9748c74501bea842f93d21ee00b0cf922846d9d0b183", size = 3380782 }, + { url = "https://files.pythonhosted.org/packages/e0/f1/7fb4982d59aa86e1a116c812b545e7fc045352be07738ae3fb278835a9a4/cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:610a83540765a8d8ce0f351ce42e26e53e1f774a6efb71eb1b41eb01d01c3d12", size = 3888155 }, + { url = "https://files.pythonhosted.org/packages/60/7b/cbc203838d3092203493d18b923fbbb1de64e0530b332a713ba376905b0b/cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5fed5cd6102bb4eb843e3315d2bf25fede494509bddadb81e03a859c1bc17b83", size = 4106417 }, + { url = "https://files.pythonhosted.org/packages/12/c7/2fe59fb085ab418acc82e91e040a6acaa7b1696fcc1c1055317537fbf0d3/cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f4daefc971c2d1f82f03097dc6f216744a6cd2ac0f04c68fb935ea2ba2a0d420", size = 3887540 }, + { url = "https://files.pythonhosted.org/packages/48/89/09fc7b115f60f5bd970b80e32244f8e9aeeb9244bf870b63420cec3b5cd5/cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94f99f2b943b354a5b6307d7e8d19f5c423a794462bde2bf310c770ba052b1c4", size = 4106040 }, + { url = "https://files.pythonhosted.org/packages/2e/38/3fd83c4690dc7d753a442a284b3826ea5e5c380a411443c66421cd823898/cryptography-44.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d9c5b9f698a83c8bd71e0f4d3f9f839ef244798e5ffe96febfa9714717db7af7", size = 3134657 }, ] [[package]] @@ -309,7 +314,7 @@ wheels = [ [[package]] name = "libcomponent" -version = "0.0.1" +version = "0.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, @@ -317,47 +322,47 @@ dependencies = [ { name = "trio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/40/30e51ad19bc3420a61e60c300c2bc4a9f685ba587bdcf8e852dd5a3a9a40/libcomponent-0.0.1.tar.gz", hash = "sha256:fdebdc9b4857707511c3b2bf0a434af30910b19fa0ec1ddb3e78457d074e40f1", size = 74693 } +sdist = { url = "https://files.pythonhosted.org/packages/2f/e9/7a72fba139f5d40686c2ca35bb9ce7417b0fa48d14206e5df9997636e5a9/libcomponent-0.0.2.tar.gz", hash = "sha256:cd337db0b435d69e723c8475be0df84f5fbed73756f08090e237fb51a7f197a3", size = 75171 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/2f/404652eae9da1f1216c66e57d016debaec8ec927fcd2a2c3c600629ceb25/libcomponent-0.0.1-py3-none-any.whl", hash = "sha256:be0fe9fffcebe4602656d55fb1c1d8afa230552d84cf1d69300b23d44e19369a", size = 57644 }, + { url = "https://files.pythonhosted.org/packages/81/ff/1626669a3ece34c6966750ef5623165947eb3c94f35598a309e9bac870f7/libcomponent-0.0.2-py3-none-any.whl", hash = "sha256:1bfd730d2044349005dc796c624b239a9799fcf01272a8f6a0c43372e2751047", size = 57907 }, ] [[package]] name = "mypy" -version = "1.14.1" +version = "1.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/7a/87ae2adb31d68402da6da1e5f30c07ea6063e9f09b5e7cfc9dfa44075e74/mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb", size = 11211002 }, - { url = "https://files.pythonhosted.org/packages/e1/23/eada4c38608b444618a132be0d199b280049ded278b24cbb9d3fc59658e4/mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0", size = 10358400 }, - { url = "https://files.pythonhosted.org/packages/43/c9/d6785c6f66241c62fd2992b05057f404237deaad1566545e9f144ced07f5/mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d", size = 12095172 }, - { url = "https://files.pythonhosted.org/packages/c3/62/daa7e787770c83c52ce2aaf1a111eae5893de9e004743f51bfcad9e487ec/mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b", size = 12828732 }, - { url = "https://files.pythonhosted.org/packages/1b/a2/5fb18318a3637f29f16f4e41340b795da14f4751ef4f51c99ff39ab62e52/mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427", size = 13012197 }, - { url = "https://files.pythonhosted.org/packages/28/99/e153ce39105d164b5f02c06c35c7ba958aaff50a2babba7d080988b03fe7/mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f", size = 9780836 }, - { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432 }, - { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515 }, - { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791 }, - { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203 }, - { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900 }, - { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869 }, - { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668 }, - { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060 }, - { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167 }, - { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341 }, - { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991 }, - { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016 }, - { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097 }, - { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728 }, - { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965 }, - { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660 }, - { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198 }, - { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276 }, - { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905 }, +sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/f8/65a7ce8d0e09b6329ad0c8d40330d100ea343bd4dd04c4f8ae26462d0a17/mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13", size = 10738433 }, + { url = "https://files.pythonhosted.org/packages/b4/95/9c0ecb8eacfe048583706249439ff52105b3f552ea9c4024166c03224270/mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559", size = 9861472 }, + { url = "https://files.pythonhosted.org/packages/84/09/9ec95e982e282e20c0d5407bc65031dfd0f0f8ecc66b69538296e06fcbee/mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b", size = 11611424 }, + { url = "https://files.pythonhosted.org/packages/78/13/f7d14e55865036a1e6a0a69580c240f43bc1f37407fe9235c0d4ef25ffb0/mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3", size = 12365450 }, + { url = "https://files.pythonhosted.org/packages/48/e1/301a73852d40c241e915ac6d7bcd7fedd47d519246db2d7b86b9d7e7a0cb/mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b", size = 12551765 }, + { url = "https://files.pythonhosted.org/packages/77/ba/c37bc323ae5fe7f3f15a28e06ab012cd0b7552886118943e90b15af31195/mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828", size = 9274701 }, + { url = "https://files.pythonhosted.org/packages/03/bc/f6339726c627bd7ca1ce0fa56c9ae2d0144604a319e0e339bdadafbbb599/mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f", size = 10662338 }, + { url = "https://files.pythonhosted.org/packages/e2/90/8dcf506ca1a09b0d17555cc00cd69aee402c203911410136cd716559efe7/mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5", size = 9787540 }, + { url = "https://files.pythonhosted.org/packages/05/05/a10f9479681e5da09ef2f9426f650d7b550d4bafbef683b69aad1ba87457/mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e", size = 11538051 }, + { url = "https://files.pythonhosted.org/packages/e9/9a/1f7d18b30edd57441a6411fcbc0c6869448d1a4bacbaee60656ac0fc29c8/mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c", size = 12286751 }, + { url = "https://files.pythonhosted.org/packages/72/af/19ff499b6f1dafcaf56f9881f7a965ac2f474f69f6f618b5175b044299f5/mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f", size = 12421783 }, + { url = "https://files.pythonhosted.org/packages/96/39/11b57431a1f686c1aed54bf794870efe0f6aeca11aca281a0bd87a5ad42c/mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f", size = 9265618 }, + { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981 }, + { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175 }, + { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675 }, + { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020 }, + { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582 }, + { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614 }, + { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592 }, + { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611 }, + { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443 }, + { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541 }, + { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348 }, + { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648 }, + { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 }, ] [[package]] @@ -630,27 +635,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.9.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/80/63/77ecca9d21177600f551d1c58ab0e5a0b260940ea7312195bd2a4798f8a8/ruff-0.9.2.tar.gz", hash = "sha256:b5eceb334d55fae5f316f783437392642ae18e16dcf4f1858d55d3c2a0f8f5d0", size = 3553799 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/af/b9/0e168e4e7fb3af851f739e8f07889b91d1a33a30fca8c29fa3149d6b03ec/ruff-0.9.2-py3-none-linux_armv6l.whl", hash = "sha256:80605a039ba1454d002b32139e4970becf84b5fee3a3c3bf1c2af6f61a784347", size = 11652408 }, - { url = "https://files.pythonhosted.org/packages/2c/22/08ede5db17cf701372a461d1cb8fdde037da1d4fa622b69ac21960e6237e/ruff-0.9.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b9aab82bb20afd5f596527045c01e6ae25a718ff1784cb92947bff1f83068b00", size = 11587553 }, - { url = "https://files.pythonhosted.org/packages/42/05/dedfc70f0bf010230229e33dec6e7b2235b2a1b8cbb2a991c710743e343f/ruff-0.9.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fbd337bac1cfa96be615f6efcd4bc4d077edbc127ef30e2b8ba2a27e18c054d4", size = 11020755 }, - { url = "https://files.pythonhosted.org/packages/df/9b/65d87ad9b2e3def67342830bd1af98803af731243da1255537ddb8f22209/ruff-0.9.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b35259b0cbf8daa22a498018e300b9bb0174c2bbb7bcba593935158a78054d", size = 11826502 }, - { url = "https://files.pythonhosted.org/packages/93/02/f2239f56786479e1a89c3da9bc9391120057fc6f4a8266a5b091314e72ce/ruff-0.9.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b6a9701d1e371bf41dca22015c3f89769da7576884d2add7317ec1ec8cb9c3c", size = 11390562 }, - { url = "https://files.pythonhosted.org/packages/c9/37/d3a854dba9931f8cb1b2a19509bfe59e00875f48ade632e95aefcb7a0aee/ruff-0.9.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9cc53e68b3c5ae41e8faf83a3b89f4a5d7b2cb666dff4b366bb86ed2a85b481f", size = 12548968 }, - { url = "https://files.pythonhosted.org/packages/fa/c3/c7b812bb256c7a1d5553433e95980934ffa85396d332401f6b391d3c4569/ruff-0.9.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8efd9da7a1ee314b910da155ca7e8953094a7c10d0c0a39bfde3fcfd2a015684", size = 13187155 }, - { url = "https://files.pythonhosted.org/packages/bd/5a/3c7f9696a7875522b66aa9bba9e326e4e5894b4366bd1dc32aa6791cb1ff/ruff-0.9.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3292c5a22ea9a5f9a185e2d131dc7f98f8534a32fb6d2ee7b9944569239c648d", size = 12704674 }, - { url = "https://files.pythonhosted.org/packages/be/d6/d908762257a96ce5912187ae9ae86792e677ca4f3dc973b71e7508ff6282/ruff-0.9.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a605fdcf6e8b2d39f9436d343d1f0ff70c365a1e681546de0104bef81ce88df", size = 14529328 }, - { url = "https://files.pythonhosted.org/packages/2d/c2/049f1e6755d12d9cd8823242fa105968f34ee4c669d04cac8cea51a50407/ruff-0.9.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c547f7f256aa366834829a08375c297fa63386cbe5f1459efaf174086b564247", size = 12385955 }, - { url = "https://files.pythonhosted.org/packages/91/5a/a9bdb50e39810bd9627074e42743b00e6dc4009d42ae9f9351bc3dbc28e7/ruff-0.9.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d18bba3d3353ed916e882521bc3e0af403949dbada344c20c16ea78f47af965e", size = 11810149 }, - { url = "https://files.pythonhosted.org/packages/e5/fd/57df1a0543182f79a1236e82a79c68ce210efb00e97c30657d5bdb12b478/ruff-0.9.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b338edc4610142355ccf6b87bd356729b62bf1bc152a2fad5b0c7dc04af77bfe", size = 11479141 }, - { url = "https://files.pythonhosted.org/packages/dc/16/bc3fd1d38974f6775fc152a0554f8c210ff80f2764b43777163c3c45d61b/ruff-0.9.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:492a5e44ad9b22a0ea98cf72e40305cbdaf27fac0d927f8bc9e1df316dcc96eb", size = 12014073 }, - { url = "https://files.pythonhosted.org/packages/47/6b/e4ca048a8f2047eb652e1e8c755f384d1b7944f69ed69066a37acd4118b0/ruff-0.9.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:af1e9e9fe7b1f767264d26b1075ac4ad831c7db976911fa362d09b2d0356426a", size = 12435758 }, - { url = "https://files.pythonhosted.org/packages/c2/40/4d3d6c979c67ba24cf183d29f706051a53c36d78358036a9cd21421582ab/ruff-0.9.2-py3-none-win32.whl", hash = "sha256:71cbe22e178c5da20e1514e1e01029c73dc09288a8028a5d3446e6bba87a5145", size = 9796916 }, - { url = "https://files.pythonhosted.org/packages/c3/ef/7f548752bdb6867e6939489c87fe4da489ab36191525fadc5cede2a6e8e2/ruff-0.9.2-py3-none-win_amd64.whl", hash = "sha256:c5e1d6abc798419cf46eed03f54f2e0c3adb1ad4b801119dedf23fcaf69b55b5", size = 10773080 }, - { url = "https://files.pythonhosted.org/packages/0e/4e/33df635528292bd2d18404e4daabcd74ca8a9853b2e1df85ed3d32d24362/ruff-0.9.2-py3-none-win_arm64.whl", hash = "sha256:a1b63fa24149918f8b37cef2ee6fff81f24f0d74b6f0bdc37bc3e1f2143e41c6", size = 10001738 }, +version = "0.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/e1/e265aba384343dd8ddd3083f5e33536cd17e1566c41453a5517b5dd443be/ruff-0.9.6.tar.gz", hash = "sha256:81761592f72b620ec8fa1068a6fd00e98a5ebee342a3642efd84454f3031dca9", size = 3639454 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/e3/3d2c022e687e18cf5d93d6bfa2722d46afc64eaa438c7fbbdd603b3597be/ruff-0.9.6-py3-none-linux_armv6l.whl", hash = "sha256:2f218f356dd2d995839f1941322ff021c72a492c470f0b26a34f844c29cdf5ba", size = 11714128 }, + { url = "https://files.pythonhosted.org/packages/e1/22/aff073b70f95c052e5c58153cba735748c9e70107a77d03420d7850710a0/ruff-0.9.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b908ff4df65dad7b251c9968a2e4560836d8f5487c2f0cc238321ed951ea0504", size = 11682539 }, + { url = "https://files.pythonhosted.org/packages/75/a7/f5b7390afd98a7918582a3d256cd3e78ba0a26165a467c1820084587cbf9/ruff-0.9.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b109c0ad2ececf42e75fa99dc4043ff72a357436bb171900714a9ea581ddef83", size = 11132512 }, + { url = "https://files.pythonhosted.org/packages/a6/e3/45de13ef65047fea2e33f7e573d848206e15c715e5cd56095589a7733d04/ruff-0.9.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1de4367cca3dac99bcbd15c161404e849bb0bfd543664db39232648dc00112dc", size = 11929275 }, + { url = "https://files.pythonhosted.org/packages/7d/f2/23d04cd6c43b2e641ab961ade8d0b5edb212ecebd112506188c91f2a6e6c/ruff-0.9.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3ee4d7c2c92ddfdaedf0bf31b2b176fa7aa8950efc454628d477394d35638b", size = 11466502 }, + { url = "https://files.pythonhosted.org/packages/b5/6f/3a8cf166f2d7f1627dd2201e6cbc4cb81f8b7d58099348f0c1ff7b733792/ruff-0.9.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dc1edd1775270e6aa2386119aea692039781429f0be1e0949ea5884e011aa8e", size = 12676364 }, + { url = "https://files.pythonhosted.org/packages/f5/c4/db52e2189983c70114ff2b7e3997e48c8318af44fe83e1ce9517570a50c6/ruff-0.9.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4a091729086dffa4bd070aa5dab7e39cc6b9d62eb2bef8f3d91172d30d599666", size = 13335518 }, + { url = "https://files.pythonhosted.org/packages/66/44/545f8a4d136830f08f4d24324e7db957c5374bf3a3f7a6c0bc7be4623a37/ruff-0.9.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1bbc6808bf7b15796cef0815e1dfb796fbd383e7dbd4334709642649625e7c5", size = 12823287 }, + { url = "https://files.pythonhosted.org/packages/c5/26/8208ef9ee7431032c143649a9967c3ae1aae4257d95e6f8519f07309aa66/ruff-0.9.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:589d1d9f25b5754ff230dce914a174a7c951a85a4e9270613a2b74231fdac2f5", size = 14592374 }, + { url = "https://files.pythonhosted.org/packages/31/70/e917781e55ff39c5b5208bda384fd397ffd76605e68544d71a7e40944945/ruff-0.9.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc61dd5131742e21103fbbdcad683a8813be0e3c204472d520d9a5021ca8b217", size = 12500173 }, + { url = "https://files.pythonhosted.org/packages/84/f5/e4ddee07660f5a9622a9c2b639afd8f3104988dc4f6ba0b73ffacffa9a8c/ruff-0.9.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5e2d9126161d0357e5c8f30b0bd6168d2c3872372f14481136d13de9937f79b6", size = 11906555 }, + { url = "https://files.pythonhosted.org/packages/f1/2b/6ff2fe383667075eef8656b9892e73dd9b119b5e3add51298628b87f6429/ruff-0.9.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:68660eab1a8e65babb5229a1f97b46e3120923757a68b5413d8561f8a85d4897", size = 11538958 }, + { url = "https://files.pythonhosted.org/packages/3c/db/98e59e90de45d1eb46649151c10a062d5707b5b7f76f64eb1e29edf6ebb1/ruff-0.9.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c4cae6c4cc7b9b4017c71114115db0445b00a16de3bcde0946273e8392856f08", size = 12117247 }, + { url = "https://files.pythonhosted.org/packages/ec/bc/54e38f6d219013a9204a5a2015c09e7a8c36cedcd50a4b01ac69a550b9d9/ruff-0.9.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19f505b643228b417c1111a2a536424ddde0db4ef9023b9e04a46ed8a1cb4656", size = 12554647 }, + { url = "https://files.pythonhosted.org/packages/a5/7d/7b461ab0e2404293c0627125bb70ac642c2e8d55bf590f6fce85f508f1b2/ruff-0.9.6-py3-none-win32.whl", hash = "sha256:194d8402bceef1b31164909540a597e0d913c0e4952015a5b40e28c146121b5d", size = 9949214 }, + { url = "https://files.pythonhosted.org/packages/ee/30/c3cee10f915ed75a5c29c1e57311282d1a15855551a64795c1b2bbe5cf37/ruff-0.9.6-py3-none-win_amd64.whl", hash = "sha256:03482d5c09d90d4ee3f40d97578423698ad895c87314c4de39ed2af945633caa", size = 10999914 }, + { url = "https://files.pythonhosted.org/packages/e8/a8/d71f44b93e3aa86ae232af1f2126ca7b95c0f515ec135462b3e1f351441c/ruff-0.9.6-py3-none-win_arm64.whl", hash = "sha256:0e2bb706a2be7ddfea4a4af918562fdc1bcb16df255e5fa595bbd800ce322a5a", size = 10177499 }, ] [[package]] @@ -739,24 +744,25 @@ wheels = [ [[package]] name = "uv" -version = "0.5.21" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/c1/d8da0122d14a48a7895241b1c15b027d7df6f56350cae614561c0567ecb2/uv-0.5.21.tar.gz", hash = "sha256:eb33043b42111ae3fef76906422b5c4247188e1ae1233da63be82cc64bb527d0", size = 2631880 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/c7/c7a787cc2c526442b2999cbebe526e24517b8812f3d545e90811e38c213a/uv-0.5.21-py3-none-linux_armv6l.whl", hash = "sha256:8ea7309dc1891e88276e207aa389cc4524ec7a7038a75bfd7c5a09ed3701316f", size = 15181071 }, - { url = "https://files.pythonhosted.org/packages/5d/61/5a6796f31830898d0aa01e018d49bbbf39d61f2c19350663be16b6cfd1d9/uv-0.5.21-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ef4e579390a022efcbfe8720f51ad46fdff54caf982782967d5689841485ddd8", size = 15305687 }, - { url = "https://files.pythonhosted.org/packages/65/37/a5a2e0d0776063e2fe1f6dfac21dd5e707d2df9c167572c416970dd3af34/uv-0.5.21-py3-none-macosx_11_0_arm64.whl", hash = "sha256:73c9d1bdbff989114c5c37649235c569f89b65bd2e57b75d8fdb73946ade7cbd", size = 14214520 }, - { url = "https://files.pythonhosted.org/packages/15/ce/a844df3ea81c9370feed1ab0fd474776709a60f07b897c41fcdf0f260c0f/uv-0.5.21-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:6e97c68306c0583af1b14b5b801c3e18ab7bc349a4c9cdd8ab5f8f46348539c5", size = 14667101 }, - { url = "https://files.pythonhosted.org/packages/88/53/d4a0cefd1927f6047500c95967d69d045b11839c9f48e2a448372498186f/uv-0.5.21-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ecdf58adf9376f2b4f63e6538e38be0e77fcd3d5b07b3ee56a3c7cd1d9ca526", size = 14952637 }, - { url = "https://files.pythonhosted.org/packages/4d/0a/a68d9142e429b4a28cebcae21c6dba262f7905772d950d076e0b161f4e0c/uv-0.5.21-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dafa7b5bb3ae8949ba100645b7a8d804f683547586024f73ad1b2d97a1aa9976", size = 15665199 }, - { url = "https://files.pythonhosted.org/packages/18/9a/062eb481fe3661ee663751f0af9a6490014357592c9aea65d0261d385a40/uv-0.5.21-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:609299c04c00ece874b30abee9cb83753224a03e8d9191327397f33a92674a53", size = 16571172 }, - { url = "https://files.pythonhosted.org/packages/94/f0/8e36e40acb289a39ed00a49122f6c3ad36993ff11d8197885877ace30b73/uv-0.5.21-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10232d5f24a1831f7ab3967f0b56f78681c520ff3391dcf5096eface94619e8e", size = 16292510 }, - { url = "https://files.pythonhosted.org/packages/91/40/3b48d57626dcb306c9e5736d4148fb6eaf931d94dbeb810ad32e48b58ac8/uv-0.5.21-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f17d35ab4a099657ad55d3cfeaf91a35b929ae2cd2b22163710cdfec45ea3941", size = 20623325 }, - { url = "https://files.pythonhosted.org/packages/5c/6f/86ee925f5e20df3aa366538a56e0d1bd5dfa9ef9d9bea57709480d47d72c/uv-0.5.21-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a1582f4964b1249b0e82ad0e60519a73392e099541a6db587e7333139255d50", size = 15952215 }, - { url = "https://files.pythonhosted.org/packages/62/f9/094ceaf8f0380b5381918aeb65907ff1fd06150b51f3baafa879ed9fdf4a/uv-0.5.21-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:afd98237d97b92935c8d5a9bf28218b5ecb497af9a99ad0a740d0b71b51f864a", size = 14914771 }, - { url = "https://files.pythonhosted.org/packages/0c/10/a5f73f433f29922b304eb95e7d6f18632734f92753c73017a8b05ce41795/uv-0.5.21-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:b317bfb7ba61e0396be5776f723e03e818a6393322f62828b67c16b565e1c0ec", size = 14904317 }, - { url = "https://files.pythonhosted.org/packages/76/4e/b9be4fcc45a026f1e1a2975719ee5f0444dafda1b606c0871d0c24651115/uv-0.5.21-py3-none-musllinux_1_1_i686.whl", hash = "sha256:168fca3bad68f75518a168feeebfd2c0b104e9abc06a33caa710d0b2753db3aa", size = 15315311 }, - { url = "https://files.pythonhosted.org/packages/a5/2d/74df7f292a7c15269bacd451a492520e26c4ef99b19c01fe96913506dea5/uv-0.5.21-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:f5ba5076b6b69161d318f5ddeff6dd935ab29a157ff10dd8756ed6dcb5d0a497", size = 16042115 }, - { url = "https://files.pythonhosted.org/packages/4b/69/03731b38d23e7bed653f186be2ff2dfcdcef29a611f4937ff4bacff205fe/uv-0.5.21-py3-none-win32.whl", hash = "sha256:34944204a39b840fa0efb2ba27f4decce50115460c6b8e4e6ade6aae6246d0cf", size = 15262952 }, - { url = "https://files.pythonhosted.org/packages/c2/8d/f6508e3c3fbc76b945365062ffff9fa6e60ad6516b26dae23a1c761d65c0/uv-0.5.21-py3-none-win_amd64.whl", hash = "sha256:36f21534a9e00a85cc532ef9575d3785a4e434a25daa93e51ebc08b54ade4991", size = 16625459 }, +version = "0.5.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/67/04/63265828848c2ca60e322408ed529587f670ee97c5607114df08c389398a/uv-0.5.31.tar.gz", hash = "sha256:59c4c6e3704208a8dd5e8d51b79ec995db18a64bd3ff88fd239ca433fbaf1694", size = 2875508 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/80/58942f09e0a38fdef37d54d553175e3c195da32547711c78dcc70876f2ce/uv-0.5.31-py3-none-linux_armv6l.whl", hash = "sha256:ba5707a6e363284ba1acd29ae9e70e2377ed31e272b953069798c444bae847ef", size = 15475386 }, + { url = "https://files.pythonhosted.org/packages/a2/ed/1605df7bd74eac86975a48e16a76ae04feedc9d27dc841e8d4f3c00a790f/uv-0.5.31-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3169a373d0d41571a7b9d4a442f875f6e26250693ced7779f62461f52ba1da64", size = 15608043 }, + { url = "https://files.pythonhosted.org/packages/1f/5a/1eb42f481a9f9010c8c194d70ab375a6eda96d67ca1fd011bf869d4016c8/uv-0.5.31-py3-none-macosx_11_0_arm64.whl", hash = "sha256:335c16f91b46b4f4a3b31c18cf112a0643d59d4c1708a177103621da0addbaef", size = 14523527 }, + { url = "https://files.pythonhosted.org/packages/9a/05/9817ea1f0d8e7134ed60abeefa7bdc602f4a4a6e2ccdf2760b54fb3dcef3/uv-0.5.31-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:cedceefebf2123b514464671d0544a8db126071c2d56dbc10d408b8222939e6a", size = 14940294 }, + { url = "https://files.pythonhosted.org/packages/e4/2d/aee8e68026057c6db71424e3a312d739af8838ae35321bfa1f5900e93d1c/uv-0.5.31-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7233182a2b8226011562341f05aaee19925b48730fccdb2e7ee20e31a84f12db", size = 15211133 }, + { url = "https://files.pythonhosted.org/packages/58/cf/16c3b71c903e7d8c3aeb0b85efbf2efb4694ffeab72165d7d9166bf2d497/uv-0.5.31-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ce4dc079fd5ddf1946e6085b6ece126ce7c4be23ba27e4010aa68fdec004191", size = 15943734 }, + { url = "https://files.pythonhosted.org/packages/7c/28/8421b94710581c81a9240df95f04b87cfffd5da229eb178733acb6d1a6de/uv-0.5.31-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:007576e1b62268d4a21d4a375d43ff5ae3698313a11f7702c8e7cb5bd29d7f1b", size = 16890117 }, + { url = "https://files.pythonhosted.org/packages/be/9c/a3d4318aebbc68158dc069d3f8de423d56ec3a38017401e92e9e37fe5afc/uv-0.5.31-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51d8287cdb760ea8c44b374cb96a59fae2292f1b3e18e228f7ed817d2bd96243", size = 16623168 }, + { url = "https://files.pythonhosted.org/packages/dd/b1/32a5e1239eca3915bec3825dab8c635f80c64b09ae46cf03d1bef7641892/uv-0.5.31-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27ce8f3eecd281a6ec255644a328b60eb10044e506a46be931db7bbfe8db89ab", size = 20939390 }, + { url = "https://files.pythonhosted.org/packages/ce/2e/0c3ac2f5be92492cbe735de7f66a83b2d3e22bd59554deaa0106562cba45/uv-0.5.31-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d07e9db12a55005a28bb49ecfa444a0221702158fc021f79e26d8e174f1ebdf9", size = 16293460 }, + { url = "https://files.pythonhosted.org/packages/cc/de/59e6665d9f1d4fc93c0b3383eaf31dbf7088cf8fce5c239b5eb8f0bf911b/uv-0.5.31-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:8acf6bcb0c0c27e1a157926f35dc70b1c7620c1a2e1124ffacdbf21c78265761", size = 15234496 }, + { url = "https://files.pythonhosted.org/packages/32/14/e69d04bc77f73a34d2d850d60cf21ded8cf0f3481302ea31533ad5a64733/uv-0.5.31-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:a8f27ea8441ce9de43a6af4825d2b936030a0a6864c608f1015db30e9f5f9cdb", size = 15212989 }, + { url = "https://files.pythonhosted.org/packages/99/29/1afb24345ffa6dd351170adc9b30d8a3855c47a2b85f093f28b7366c2a6d/uv-0.5.31-py3-none-musllinux_1_1_i686.whl", hash = "sha256:e6b5a29c29e774525baf982f570c53e8862f19e3f7e74bd819c7b3749f4cdfa0", size = 15554448 }, + { url = "https://files.pythonhosted.org/packages/5a/5f/784cbe68aa0c291587a3735a61372dc02521780ccd0f0058f159a451df19/uv-0.5.31-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:15109a938c56ee1e1c997b291743812af3ea1d7547b0929569494c359082a993", size = 16405791 }, + { url = "https://files.pythonhosted.org/packages/9f/80/458b8f67e41dddc3c6ca1515ea8136c217a52b92dedd8c53f9eb00287d22/uv-0.5.31-py3-none-win32.whl", hash = "sha256:f2161ef8b9a0308f05dd4a3eb2c1d104301e23c699fab5898e9fc38387690e4b", size = 15602489 }, + { url = "https://files.pythonhosted.org/packages/4c/50/f3f89c6bd27aae15ca3150b839c9d8f5d32a9a19a6eae3daa6d9aae1de4f/uv-0.5.31-py3-none-win_amd64.whl", hash = "sha256:bcc57b75883516233658ff1daee0d17347a8b872f717a1644d36e8ea2b021f45", size = 16895932 }, + { url = "https://files.pythonhosted.org/packages/12/64/af4aa07bc1c525b1fefd1686d31a43a74eac51e74046755ffdca4502784d/uv-0.5.31-py3-none-win_arm64.whl", hash = "sha256:51ceab5a128dd22bcd62489107563e10084e13ed9c15107193c2d7d1139979f4", size = 15776619 }, ] From 8a817d36b335ed949e0a01085883bc51866c9aef Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sat, 1 Mar 2025 16:55:15 -0600 Subject: [PATCH 51/58] Upgrade dependencies and actions runner Upgrade `libcomponent` to v0.0.3 --- .github/workflows/autodeps.yml | 2 +- .github/workflows/ci.yml | 4 +- .pre-commit-config.yaml | 6 +- pyproject.toml | 10 +-- src/azul/async_clock.py | 106 -------------------------------- src/azul/game.py | 2 +- tests/test_async_clock.py | 62 ------------------- uv.lock | 108 +++++++++++++++++---------------- 8 files changed, 67 insertions(+), 233 deletions(-) delete mode 100644 src/azul/async_clock.py delete mode 100644 tests/test_async_clock.py diff --git a/.github/workflows/autodeps.yml b/.github/workflows/autodeps.yml index 1862fb9..2974d9b 100644 --- a/.github/workflows/autodeps.yml +++ b/.github/workflows/autodeps.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout with: - persist-credentials: false + persist-credentials: true # credentials are needed to push commits uses: actions/checkout@v4 - name: Setup python uses: actions/setup-python@v5 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b56f28d..bdc7d48 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: strategy: fail-fast: false matrix: - python: ['3.10', '3.11', '3.12'] + python: ['3.10', '3.11', '3.12', '3.13'] arch: ['x86', 'x64'] continue-on-error: >- ${{ @@ -107,7 +107,7 @@ jobs: strategy: fail-fast: false matrix: - python: ['3.10', '3.11', '3.12'] + python: ['3.10', '3.11', '3.12', '3.13'] continue-on-error: >- ${{ ( diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3f9da0e..f002e4b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.6 + rev: v0.9.9 hooks: - id: ruff types: [file] @@ -41,10 +41,10 @@ repos: additional_dependencies: - tomli - repo: https://github.com/crate-ci/typos - rev: typos-dict-v0.12.5 + rev: v1.30.0 hooks: - id: typos - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.3.1 + rev: v1.4.1 hooks: - id: zizmor diff --git a/pyproject.toml b/pyproject.toml index ebdd41e..c3a5cad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,13 +36,13 @@ keywords = [ "ai", "multi-player", "azul", "ai-support", "networked-game" ] dependencies = [ - "exceptiongroup; python_version < '3.11'", - "libcomponent~=0.0.1", + "exceptiongroup>=1.2.2; python_version < '3.11'", + "libcomponent~=0.0.3", "mypy_extensions>=1.0.0", "numpy~=2.1.3", "orjson>=3.10,<4", "pygame~=2.6.0", - "trio~=0.28.0", + "trio~=0.29.0", ] [tool.setuptools.dynamic] @@ -58,8 +58,8 @@ azul_game = "azul.game:cli_run" [project.optional-dependencies] tests = [ "pytest>=5.0", - "pytest-cov", - "pytest-trio", + "pytest-cov>=6.0.0", + "pytest-trio>=0.8.0", "coverage>=7.2.5", "uv>=0.5.21", "mypy>=1.14.1", diff --git a/src/azul/async_clock.py b/src/azul/async_clock.py deleted file mode 100644 index 9e93365..0000000 --- a/src/azul/async_clock.py +++ /dev/null @@ -1,106 +0,0 @@ -"""Asynchronous Clock - Asynchronous version of pygame.time.Clock.""" - -# Programmed by CoolCat467 - -from __future__ import annotations - -# Copyright (C) 2023 CoolCat467 -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -__title__ = "Async Clock" -__author__ = "CoolCat467" -__license__ = "GNU General Public License Version 3" -__version__ = "0.0.0" - - -from time import perf_counter_ns -from typing import NewType - -import trio - -nanoseconds = NewType("nanoseconds", int) - - -def get_ticks() -> nanoseconds: - """Get Ticks.""" - return nanoseconds(perf_counter_ns()) - - -class Clock: - """pygame.time.Clock but with asynchronous tick.""" - - __slots__ = ( - "fps", - "fps_count", - "fps_tick", - "last_tick", - "rawpassed", - "timepassed", - ) - - def __init__(self) -> None: - """Initialize variables.""" - self.fps_tick = nanoseconds(0) - self.timepassed = nanoseconds(0) - self.rawpassed = nanoseconds(0) - self.last_tick: nanoseconds = get_ticks() - self.fps = 0.0 - self.fps_count = 0 - - def __repr__(self) -> str: - """Return representation of self.""" - return f"<{self.__class__.__name__}({self.fps:2f})>" - - def get_fps(self) -> float: - """Return the clock framerate in Frames Per Second.""" - return self.fps - - def get_rawtime(self) -> nanoseconds: - """Return the actual time used in the previous tick in nanoseconds (original was milliseconds).""" - return self.rawpassed - - def get_time(self) -> nanoseconds: - """Return time used in the previous tick (in nanoseconds, original was milliseconds).""" - return self.timepassed - - async def tick(self, framerate: int = 0) -> int: - """Tick the clock. Return time passed in nanoseconds, same as get_time (original was milliseconds).""" - endtime = 1000000000 // framerate if framerate > 0 else 0 - self.rawpassed = nanoseconds(get_ticks() - self.last_tick) - delay = endtime - self.rawpassed - if delay > 0: - await trio.sleep(delay / 1e9) # nanoseconds -> seconds - else: - await trio.lowlevel.checkpoint() - - nowtime: nanoseconds = get_ticks() - self.timepassed = nanoseconds(nowtime - self.last_tick) - self.fps_count += 1 - self.last_tick = nowtime - - if not self.fps_tick: - self.fps_count = 0 - self.fps_tick = nowtime - if self.fps_count >= 10: - self.fps = self.fps_count / ( - (nowtime - self.fps_tick) / 1e9 - ) # nanoseconds -> seconds - self.fps_count = 0 - self.fps_tick = nowtime - return self.timepassed - - -if __name__ == "__main__": # pragma: nocover - print(f"{__title__} v{__version__}\nProgrammed by {__author__}.\n") diff --git a/src/azul/game.py b/src/azul/game.py index df3c1bb..3def590 100644 --- a/src/azul/game.py +++ b/src/azul/game.py @@ -38,6 +38,7 @@ import pygame import trio +from libcomponent.async_clock import Clock from libcomponent.component import ( Component, ComponentManager, @@ -60,7 +61,6 @@ from pygame.sprite import LayeredDirty from azul import database, element_list, objects, sprite -from azul.async_clock import Clock from azul.client import GameClient, read_advertisements from azul.crop import auto_crop_clear from azul.network_shared import DEFAULT_PORT diff --git a/tests/test_async_clock.py b/tests/test_async_clock.py deleted file mode 100644 index fa8256d..0000000 --- a/tests/test_async_clock.py +++ /dev/null @@ -1,62 +0,0 @@ -from __future__ import annotations - -import pytest - -from azul.async_clock import Clock - - -@pytest.fixture -def clock() -> Clock: - return Clock() - - -def test_initial_values(clock: Clock) -> None: - assert clock.fps == 0.0 - assert clock.fps_count == 0 - - -def test_get_fps(clock: Clock) -> None: - assert clock.get_fps() == 0.0 - - -def test_get_rawtime(clock: Clock) -> None: - assert clock.get_rawtime() == 0 - - -def test_get_time(clock: Clock) -> None: - assert clock.get_time() == 0 - - -@pytest.mark.trio -async def test_tick_elapsed(clock: Clock) -> None: - time_passed = await clock.tick() - assert time_passed >= 0 - - # Test with a specific framerate - time_passed = await clock.tick(60) - assert time_passed >= int(1e9 // 60) - - # Test with a zero framerate - time_passed = await clock.tick(0) - assert time_passed >= 0 - - -@pytest.mark.trio -async def test_tick(clock: Clock) -> None: - await clock.tick(60) - result = await clock.tick(60) - assert isinstance(result, int) - assert result >= int(1e9 // 60) - assert repr(clock).startswith(" None: - for _ in range(20): - await clock.tick(1024) - fps = clock.get_fps() - assert isinstance(fps, float) - assert fps >= 0 diff --git a/uv.lock b/uv.lock index c3cebe9..22ccfe9 100644 --- a/uv.lock +++ b/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 1 requires-python = ">=3.10" [[package]] @@ -43,20 +44,21 @@ requires-dist = [ { name = "black", marker = "implementation_name == 'cpython' and extra == 'tools'", specifier = ">=24.10.0" }, { name = "codespell", marker = "extra == 'tools'", specifier = ">=2.3.0" }, { name = "coverage", marker = "extra == 'tests'", specifier = ">=7.2.5" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "libcomponent", specifier = "~=0.0.1" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'", specifier = ">=1.2.2" }, + { name = "libcomponent", specifier = "~=0.0.3" }, { name = "mypy", marker = "extra == 'tests'", specifier = ">=1.14.1" }, { name = "mypy-extensions", specifier = ">=1.0.0" }, { name = "numpy", specifier = "~=2.1.3" }, { name = "orjson", specifier = ">=3.10,<4" }, { name = "pygame", specifier = "~=2.6.0" }, { name = "pytest", marker = "extra == 'tests'", specifier = ">=5.0" }, - { name = "pytest-cov", marker = "extra == 'tests'" }, - { name = "pytest-trio", marker = "extra == 'tests'" }, + { name = "pytest-cov", marker = "extra == 'tests'", specifier = ">=6.0.0" }, + { name = "pytest-trio", marker = "extra == 'tests'", specifier = ">=0.8.0" }, { name = "ruff", marker = "extra == 'tools'", specifier = ">=0.9.2" }, - { name = "trio", specifier = "~=0.28.0" }, + { name = "trio", specifier = "~=0.29.0" }, { name = "uv", marker = "extra == 'tests'", specifier = ">=0.5.21" }, ] +provides-extras = ["tests", "tools"] [[package]] name = "black" @@ -314,7 +316,7 @@ wheels = [ [[package]] name = "libcomponent" -version = "0.0.2" +version = "0.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, @@ -322,9 +324,9 @@ dependencies = [ { name = "trio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/e9/7a72fba139f5d40686c2ca35bb9ce7417b0fa48d14206e5df9997636e5a9/libcomponent-0.0.2.tar.gz", hash = "sha256:cd337db0b435d69e723c8475be0df84f5fbed73756f08090e237fb51a7f197a3", size = 75171 } +sdist = { url = "https://files.pythonhosted.org/packages/3c/98/edd63ce51271acda4352e70cb1a884915d2066ce733c70e02286b8c23fdb/libcomponent-0.0.3.tar.gz", hash = "sha256:83115c93e5ea51cd17b592ac7ccc25a78f1e4833d1efdc83ae93ca77cbee60da", size = 76329 } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/ff/1626669a3ece34c6966750ef5623165947eb3c94f35598a309e9bac870f7/libcomponent-0.0.2-py3-none-any.whl", hash = "sha256:1bfd730d2044349005dc796c624b239a9799fcf01272a8f6a0c43372e2751047", size = 57907 }, + { url = "https://files.pythonhosted.org/packages/23/cc/89d6330283696a804d85568084493d2f01716098ae6548e009e0a2b5663b/libcomponent-0.0.3-py3-none-any.whl", hash = "sha256:5879f87058e1f903bf00f300675f011f4f4627350db72962eb70752acf2dbd92", size = 59324 }, ] [[package]] @@ -635,27 +637,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.9.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2a/e1/e265aba384343dd8ddd3083f5e33536cd17e1566c41453a5517b5dd443be/ruff-0.9.6.tar.gz", hash = "sha256:81761592f72b620ec8fa1068a6fd00e98a5ebee342a3642efd84454f3031dca9", size = 3639454 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/e3/3d2c022e687e18cf5d93d6bfa2722d46afc64eaa438c7fbbdd603b3597be/ruff-0.9.6-py3-none-linux_armv6l.whl", hash = "sha256:2f218f356dd2d995839f1941322ff021c72a492c470f0b26a34f844c29cdf5ba", size = 11714128 }, - { url = "https://files.pythonhosted.org/packages/e1/22/aff073b70f95c052e5c58153cba735748c9e70107a77d03420d7850710a0/ruff-0.9.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b908ff4df65dad7b251c9968a2e4560836d8f5487c2f0cc238321ed951ea0504", size = 11682539 }, - { url = "https://files.pythonhosted.org/packages/75/a7/f5b7390afd98a7918582a3d256cd3e78ba0a26165a467c1820084587cbf9/ruff-0.9.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b109c0ad2ececf42e75fa99dc4043ff72a357436bb171900714a9ea581ddef83", size = 11132512 }, - { url = "https://files.pythonhosted.org/packages/a6/e3/45de13ef65047fea2e33f7e573d848206e15c715e5cd56095589a7733d04/ruff-0.9.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1de4367cca3dac99bcbd15c161404e849bb0bfd543664db39232648dc00112dc", size = 11929275 }, - { url = "https://files.pythonhosted.org/packages/7d/f2/23d04cd6c43b2e641ab961ade8d0b5edb212ecebd112506188c91f2a6e6c/ruff-0.9.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3ee4d7c2c92ddfdaedf0bf31b2b176fa7aa8950efc454628d477394d35638b", size = 11466502 }, - { url = "https://files.pythonhosted.org/packages/b5/6f/3a8cf166f2d7f1627dd2201e6cbc4cb81f8b7d58099348f0c1ff7b733792/ruff-0.9.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dc1edd1775270e6aa2386119aea692039781429f0be1e0949ea5884e011aa8e", size = 12676364 }, - { url = "https://files.pythonhosted.org/packages/f5/c4/db52e2189983c70114ff2b7e3997e48c8318af44fe83e1ce9517570a50c6/ruff-0.9.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4a091729086dffa4bd070aa5dab7e39cc6b9d62eb2bef8f3d91172d30d599666", size = 13335518 }, - { url = "https://files.pythonhosted.org/packages/66/44/545f8a4d136830f08f4d24324e7db957c5374bf3a3f7a6c0bc7be4623a37/ruff-0.9.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1bbc6808bf7b15796cef0815e1dfb796fbd383e7dbd4334709642649625e7c5", size = 12823287 }, - { url = "https://files.pythonhosted.org/packages/c5/26/8208ef9ee7431032c143649a9967c3ae1aae4257d95e6f8519f07309aa66/ruff-0.9.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:589d1d9f25b5754ff230dce914a174a7c951a85a4e9270613a2b74231fdac2f5", size = 14592374 }, - { url = "https://files.pythonhosted.org/packages/31/70/e917781e55ff39c5b5208bda384fd397ffd76605e68544d71a7e40944945/ruff-0.9.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc61dd5131742e21103fbbdcad683a8813be0e3c204472d520d9a5021ca8b217", size = 12500173 }, - { url = "https://files.pythonhosted.org/packages/84/f5/e4ddee07660f5a9622a9c2b639afd8f3104988dc4f6ba0b73ffacffa9a8c/ruff-0.9.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5e2d9126161d0357e5c8f30b0bd6168d2c3872372f14481136d13de9937f79b6", size = 11906555 }, - { url = "https://files.pythonhosted.org/packages/f1/2b/6ff2fe383667075eef8656b9892e73dd9b119b5e3add51298628b87f6429/ruff-0.9.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:68660eab1a8e65babb5229a1f97b46e3120923757a68b5413d8561f8a85d4897", size = 11538958 }, - { url = "https://files.pythonhosted.org/packages/3c/db/98e59e90de45d1eb46649151c10a062d5707b5b7f76f64eb1e29edf6ebb1/ruff-0.9.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c4cae6c4cc7b9b4017c71114115db0445b00a16de3bcde0946273e8392856f08", size = 12117247 }, - { url = "https://files.pythonhosted.org/packages/ec/bc/54e38f6d219013a9204a5a2015c09e7a8c36cedcd50a4b01ac69a550b9d9/ruff-0.9.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19f505b643228b417c1111a2a536424ddde0db4ef9023b9e04a46ed8a1cb4656", size = 12554647 }, - { url = "https://files.pythonhosted.org/packages/a5/7d/7b461ab0e2404293c0627125bb70ac642c2e8d55bf590f6fce85f508f1b2/ruff-0.9.6-py3-none-win32.whl", hash = "sha256:194d8402bceef1b31164909540a597e0d913c0e4952015a5b40e28c146121b5d", size = 9949214 }, - { url = "https://files.pythonhosted.org/packages/ee/30/c3cee10f915ed75a5c29c1e57311282d1a15855551a64795c1b2bbe5cf37/ruff-0.9.6-py3-none-win_amd64.whl", hash = "sha256:03482d5c09d90d4ee3f40d97578423698ad895c87314c4de39ed2af945633caa", size = 10999914 }, - { url = "https://files.pythonhosted.org/packages/e8/a8/d71f44b93e3aa86ae232af1f2126ca7b95c0f515ec135462b3e1f351441c/ruff-0.9.6-py3-none-win_arm64.whl", hash = "sha256:0e2bb706a2be7ddfea4a4af918562fdc1bcb16df255e5fa595bbd800ce322a5a", size = 10177499 }, +version = "0.9.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/c3/418441a8170e8d53d05c0b9dad69760dbc7b8a12c10dbe6db1e1205d2377/ruff-0.9.9.tar.gz", hash = "sha256:0062ed13f22173e85f8f7056f9a24016e692efeea8704d1a5e8011b8aa850933", size = 3717448 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/c3/2c4afa9ba467555d074b146d9aed0633a56ccdb900839fb008295d037b89/ruff-0.9.9-py3-none-linux_armv6l.whl", hash = "sha256:628abb5ea10345e53dff55b167595a159d3e174d6720bf19761f5e467e68d367", size = 10027252 }, + { url = "https://files.pythonhosted.org/packages/33/d1/439e58487cf9eac26378332e25e7d5ade4b800ce1eec7dc2cfc9b0d7ca96/ruff-0.9.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6cd1428e834b35d7493354723543b28cc11dc14d1ce19b685f6e68e07c05ec7", size = 10840721 }, + { url = "https://files.pythonhosted.org/packages/50/44/fead822c38281ba0122f1b76b460488a175a9bd48b130650a6fb6dbcbcf9/ruff-0.9.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5ee162652869120ad260670706f3cd36cd3f32b0c651f02b6da142652c54941d", size = 10161439 }, + { url = "https://files.pythonhosted.org/packages/11/ae/d404a2ab8e61ddf6342e09cc6b7f7846cce6b243e45c2007dbe0ca928a5d/ruff-0.9.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3aa0f6b75082c9be1ec5a1db78c6d4b02e2375c3068438241dc19c7c306cc61a", size = 10336264 }, + { url = "https://files.pythonhosted.org/packages/6a/4e/7c268aa7d84cd709fb6f046b8972313142cffb40dfff1d2515c5e6288d54/ruff-0.9.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:584cc66e89fb5f80f84b05133dd677a17cdd86901d6479712c96597a3f28e7fe", size = 9908774 }, + { url = "https://files.pythonhosted.org/packages/cc/26/c618a878367ef1b76270fd027ca93692657d3f6122b84ba48911ef5f2edc/ruff-0.9.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abf3369325761a35aba75cd5c55ba1b5eb17d772f12ab168fbfac54be85cf18c", size = 11428127 }, + { url = "https://files.pythonhosted.org/packages/d7/9a/c5588a93d9bfed29f565baf193fe802fa676a0c837938137ea6cf0576d8c/ruff-0.9.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3403a53a32a90ce929aa2f758542aca9234befa133e29f4933dcef28a24317be", size = 12133187 }, + { url = "https://files.pythonhosted.org/packages/3e/ff/e7980a7704a60905ed7e156a8d73f604c846d9bd87deda9cabfa6cba073a/ruff-0.9.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:18454e7fa4e4d72cffe28a37cf6a73cb2594f81ec9f4eca31a0aaa9ccdfb1590", size = 11602937 }, + { url = "https://files.pythonhosted.org/packages/24/78/3690444ad9e3cab5c11abe56554c35f005b51d1d118b429765249095269f/ruff-0.9.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fadfe2c88724c9617339f62319ed40dcdadadf2888d5afb88bf3adee7b35bfb", size = 13771698 }, + { url = "https://files.pythonhosted.org/packages/6e/bf/e477c2faf86abe3988e0b5fd22a7f3520e820b2ee335131aca2e16120038/ruff-0.9.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6df104d08c442a1aabcfd254279b8cc1e2cbf41a605aa3e26610ba1ec4acf0b0", size = 11249026 }, + { url = "https://files.pythonhosted.org/packages/f7/82/cdaffd59e5a8cb5b14c408c73d7a555a577cf6645faaf83e52fe99521715/ruff-0.9.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d7c62939daf5b2a15af48abbd23bea1efdd38c312d6e7c4cedf5a24e03207e17", size = 10220432 }, + { url = "https://files.pythonhosted.org/packages/fe/a4/2507d0026225efa5d4412b6e294dfe54725a78652a5c7e29e6bd0fc492f3/ruff-0.9.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9494ba82a37a4b81b6a798076e4a3251c13243fc37967e998efe4cce58c8a8d1", size = 9874602 }, + { url = "https://files.pythonhosted.org/packages/d5/be/f3aab1813846b476c4bcffe052d232244979c3cd99d751c17afb530ca8e4/ruff-0.9.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4efd7a96ed6d36ef011ae798bf794c5501a514be369296c672dab7921087fa57", size = 10851212 }, + { url = "https://files.pythonhosted.org/packages/8b/45/8e5fd559bea0d2f57c4e12bf197a2fade2fac465aa518284f157dfbca92b/ruff-0.9.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ab90a7944c5a1296f3ecb08d1cbf8c2da34c7e68114b1271a431a3ad30cb660e", size = 11327490 }, + { url = "https://files.pythonhosted.org/packages/42/55/e6c90f13880aeef327746052907e7e930681f26a164fe130ddac28b08269/ruff-0.9.9-py3-none-win32.whl", hash = "sha256:6b4c376d929c25ecd6d87e182a230fa4377b8e5125a4ff52d506ee8c087153c1", size = 10227912 }, + { url = "https://files.pythonhosted.org/packages/35/b2/da925693cb82a1208aa34966c0f36cb222baca94e729dd22a587bc22d0f3/ruff-0.9.9-py3-none-win_amd64.whl", hash = "sha256:837982ea24091d4c1700ddb2f63b7070e5baec508e43b01de013dc7eff974ff1", size = 11355632 }, + { url = "https://files.pythonhosted.org/packages/31/d8/de873d1c1b020d668d8ec9855d390764cb90cf8f6486c0983da52be8b7b7/ruff-0.9.9-py3-none-win_arm64.whl", hash = "sha256:3ac78f127517209fe6d96ab00f3ba97cafe38718b23b1db3e96d8b2d39e37ddf", size = 10435860 }, ] [[package]] @@ -717,7 +719,7 @@ wheels = [ [[package]] name = "trio" -version = "0.28.0" +version = "0.29.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -728,9 +730,9 @@ dependencies = [ { name = "sniffio" }, { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/73/57efab729506a8d4b89814f1e356ec8f3369de0ed4fd7e7616974d09646d/trio-0.28.0.tar.gz", hash = "sha256:4e547896fe9e8a5658e54e4c7c5fa1db748cbbbaa7c965e7d40505b928c73c05", size = 580318 } +sdist = { url = "https://files.pythonhosted.org/packages/a1/47/f62e62a1a6f37909aed0bf8f5d5411e06fa03846cfcb64540cd1180ccc9f/trio-0.29.0.tar.gz", hash = "sha256:ea0d3967159fc130acb6939a0be0e558e364fee26b5deeecc893a6b08c361bdf", size = 588952 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/04/9954a59e1fb6732f5436225c9af963811d7b24ea62a8bf96991f2cb8c26e/trio-0.28.0-py3-none-any.whl", hash = "sha256:56d58977acc1635735a96581ec70513cc781b8b6decd299c487d3be2a721cd94", size = 486317 }, + { url = "https://files.pythonhosted.org/packages/c9/55/c4d9bea8b3d7937901958f65124123512419ab0eb73695e5f382521abbfb/trio-0.29.0-py3-none-any.whl", hash = "sha256:d8c463f1a9cc776ff63e331aba44c125f423a5a13c684307e828d930e625ba66", size = 492920 }, ] [[package]] @@ -744,25 +746,25 @@ wheels = [ [[package]] name = "uv" -version = "0.5.31" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/67/04/63265828848c2ca60e322408ed529587f670ee97c5607114df08c389398a/uv-0.5.31.tar.gz", hash = "sha256:59c4c6e3704208a8dd5e8d51b79ec995db18a64bd3ff88fd239ca433fbaf1694", size = 2875508 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/40/80/58942f09e0a38fdef37d54d553175e3c195da32547711c78dcc70876f2ce/uv-0.5.31-py3-none-linux_armv6l.whl", hash = "sha256:ba5707a6e363284ba1acd29ae9e70e2377ed31e272b953069798c444bae847ef", size = 15475386 }, - { url = "https://files.pythonhosted.org/packages/a2/ed/1605df7bd74eac86975a48e16a76ae04feedc9d27dc841e8d4f3c00a790f/uv-0.5.31-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3169a373d0d41571a7b9d4a442f875f6e26250693ced7779f62461f52ba1da64", size = 15608043 }, - { url = "https://files.pythonhosted.org/packages/1f/5a/1eb42f481a9f9010c8c194d70ab375a6eda96d67ca1fd011bf869d4016c8/uv-0.5.31-py3-none-macosx_11_0_arm64.whl", hash = "sha256:335c16f91b46b4f4a3b31c18cf112a0643d59d4c1708a177103621da0addbaef", size = 14523527 }, - { url = "https://files.pythonhosted.org/packages/9a/05/9817ea1f0d8e7134ed60abeefa7bdc602f4a4a6e2ccdf2760b54fb3dcef3/uv-0.5.31-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:cedceefebf2123b514464671d0544a8db126071c2d56dbc10d408b8222939e6a", size = 14940294 }, - { url = "https://files.pythonhosted.org/packages/e4/2d/aee8e68026057c6db71424e3a312d739af8838ae35321bfa1f5900e93d1c/uv-0.5.31-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7233182a2b8226011562341f05aaee19925b48730fccdb2e7ee20e31a84f12db", size = 15211133 }, - { url = "https://files.pythonhosted.org/packages/58/cf/16c3b71c903e7d8c3aeb0b85efbf2efb4694ffeab72165d7d9166bf2d497/uv-0.5.31-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ce4dc079fd5ddf1946e6085b6ece126ce7c4be23ba27e4010aa68fdec004191", size = 15943734 }, - { url = "https://files.pythonhosted.org/packages/7c/28/8421b94710581c81a9240df95f04b87cfffd5da229eb178733acb6d1a6de/uv-0.5.31-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:007576e1b62268d4a21d4a375d43ff5ae3698313a11f7702c8e7cb5bd29d7f1b", size = 16890117 }, - { url = "https://files.pythonhosted.org/packages/be/9c/a3d4318aebbc68158dc069d3f8de423d56ec3a38017401e92e9e37fe5afc/uv-0.5.31-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51d8287cdb760ea8c44b374cb96a59fae2292f1b3e18e228f7ed817d2bd96243", size = 16623168 }, - { url = "https://files.pythonhosted.org/packages/dd/b1/32a5e1239eca3915bec3825dab8c635f80c64b09ae46cf03d1bef7641892/uv-0.5.31-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27ce8f3eecd281a6ec255644a328b60eb10044e506a46be931db7bbfe8db89ab", size = 20939390 }, - { url = "https://files.pythonhosted.org/packages/ce/2e/0c3ac2f5be92492cbe735de7f66a83b2d3e22bd59554deaa0106562cba45/uv-0.5.31-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d07e9db12a55005a28bb49ecfa444a0221702158fc021f79e26d8e174f1ebdf9", size = 16293460 }, - { url = "https://files.pythonhosted.org/packages/cc/de/59e6665d9f1d4fc93c0b3383eaf31dbf7088cf8fce5c239b5eb8f0bf911b/uv-0.5.31-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:8acf6bcb0c0c27e1a157926f35dc70b1c7620c1a2e1124ffacdbf21c78265761", size = 15234496 }, - { url = "https://files.pythonhosted.org/packages/32/14/e69d04bc77f73a34d2d850d60cf21ded8cf0f3481302ea31533ad5a64733/uv-0.5.31-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:a8f27ea8441ce9de43a6af4825d2b936030a0a6864c608f1015db30e9f5f9cdb", size = 15212989 }, - { url = "https://files.pythonhosted.org/packages/99/29/1afb24345ffa6dd351170adc9b30d8a3855c47a2b85f093f28b7366c2a6d/uv-0.5.31-py3-none-musllinux_1_1_i686.whl", hash = "sha256:e6b5a29c29e774525baf982f570c53e8862f19e3f7e74bd819c7b3749f4cdfa0", size = 15554448 }, - { url = "https://files.pythonhosted.org/packages/5a/5f/784cbe68aa0c291587a3735a61372dc02521780ccd0f0058f159a451df19/uv-0.5.31-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:15109a938c56ee1e1c997b291743812af3ea1d7547b0929569494c359082a993", size = 16405791 }, - { url = "https://files.pythonhosted.org/packages/9f/80/458b8f67e41dddc3c6ca1515ea8136c217a52b92dedd8c53f9eb00287d22/uv-0.5.31-py3-none-win32.whl", hash = "sha256:f2161ef8b9a0308f05dd4a3eb2c1d104301e23c699fab5898e9fc38387690e4b", size = 15602489 }, - { url = "https://files.pythonhosted.org/packages/4c/50/f3f89c6bd27aae15ca3150b839c9d8f5d32a9a19a6eae3daa6d9aae1de4f/uv-0.5.31-py3-none-win_amd64.whl", hash = "sha256:bcc57b75883516233658ff1daee0d17347a8b872f717a1644d36e8ea2b021f45", size = 16895932 }, - { url = "https://files.pythonhosted.org/packages/12/64/af4aa07bc1c525b1fefd1686d31a43a74eac51e74046755ffdca4502784d/uv-0.5.31-py3-none-win_arm64.whl", hash = "sha256:51ceab5a128dd22bcd62489107563e10084e13ed9c15107193c2d7d1139979f4", size = 15776619 }, +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/31/8f354a0b1df7ef4cb42da118dfae046d49f2c57ae427eb948a48a236c37d/uv-0.6.3.tar.gz", hash = "sha256:73587a192f2ebb8a25431d01037fe19f713fa99ff3b9fdf6e7a121131c6c5649", size = 3081857 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/c2/5a4138f1c615c7702943ce94155349943b5813e51faa38b6876a2ab86033/uv-0.6.3-py3-none-linux_armv6l.whl", hash = "sha256:facfec798eaddd07615b3a52973e38f2c8862ceb1bc685a5091891cd6c0c2a21", size = 15524019 }, + { url = "https://files.pythonhosted.org/packages/02/1d/abf01aa5e02b0a066f77b69a4f2f771c2ccd5424cd553e218afb026c65b9/uv-0.6.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b261895497f3c55a8a8917db0a1daeba1a9988ba487b068198d6cc4e8c13e769", size = 15537243 }, + { url = "https://files.pythonhosted.org/packages/ea/ac/4c1d5e04868051874dce74333fbe98e1f61e40a1522a9258a998775f2fab/uv-0.6.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:08e3f71a39c76c5b9ab63f9341b433a4ab8a1cc4e29d34ce81bd3b6f5bd642d8", size = 14450283 }, + { url = "https://files.pythonhosted.org/packages/00/8b/6cdb9a8cb4a5579d8b22d632e98d01f7c3695066ce1a2e33036edba2413a/uv-0.6.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:ebd4d1012c5043fe507f1f4477e7a54ec81e939e2a6e0229f23abb242f1622f5", size = 14909401 }, + { url = "https://files.pythonhosted.org/packages/51/8e/4d8c31250c7440a4c3704e81dab39f7f75db046e8b23f5322c3e47549557/uv-0.6.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f63b659a5ccbbd8c0ca5200c83ada6d19e73c0f1cafb8f4d9a7ef32544beb06d", size = 15245520 }, + { url = "https://files.pythonhosted.org/packages/4b/29/52976b3f7a79e4293763823e59d4de3b77506a1b9d298df0285be4879026/uv-0.6.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c23948f242a6bcbd274fa18387a608a52b21a3dfed18d324641964e305c348e9", size = 15890146 }, + { url = "https://files.pythonhosted.org/packages/54/38/a3c37aaf02b890d908edfec32e7a9b86e0df819df6443837929e40ac8d7e/uv-0.6.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0445ce49229001cec0a0b1240c6135e2252a3b8017ae878b0559411688a3e12a", size = 16817703 }, + { url = "https://files.pythonhosted.org/packages/df/0b/cd75c692266eb1cdea6764f9fb14d88babfa8d8433c414ac18623777760d/uv-0.6.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:95ab9e9194046f4fb50daec6293e471fc18b6e1d350dba4f5328d0f19f6ec183", size = 16509829 }, + { url = "https://files.pythonhosted.org/packages/1c/5c/35747d595bf13f5b495a29ec9bb6212fd2fad7d8c32324a7faaeb6a643d0/uv-0.6.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af417925d7af00be949ebcab1bf187540bea235e9454aa2193ffae5b7ecc75cf", size = 20477063 }, + { url = "https://files.pythonhosted.org/packages/23/c7/4ea3d3f23d24240c54deee0248766c320163eef8b0117310f0be168fe0f0/uv-0.6.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed2d4e3c6e041bc8b55f931a58d758220e46e828b983967fbb318a117d879351", size = 16190208 }, + { url = "https://files.pythonhosted.org/packages/83/f2/96d4981c3490fabc5ba787703951124969f5b6dc8e3166543e7534de2dea/uv-0.6.3-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:a936275590f3091b05c03ad3ce69e2f8a4c964e80ae44ce0cf13cc3b412352f1", size = 15145146 }, + { url = "https://files.pythonhosted.org/packages/2b/62/1be7fb8b97fd057460b733bbdf30e71e771dcfbfab27b7db552fa4e219e6/uv-0.6.3-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:e842e96b941832cd95cb2fce90c5626b33e477773f425005e9237f8fd9ef5696", size = 15245907 }, + { url = "https://files.pythonhosted.org/packages/e0/1b/5849046e11f8154567b235fc8097ebb6a0d6416b3ce317300d9b06470481/uv-0.6.3-py3-none-musllinux_1_1_i686.whl", hash = "sha256:cd51af332fb0f6362cc44e4cca22c2d12c31dd52352c6259cae0e3570ce79da4", size = 15504955 }, + { url = "https://files.pythonhosted.org/packages/ec/46/d4fa9bd06f84bb83e452f3f201b058cd13969cb979402ff000c2e4c77a1e/uv-0.6.3-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:328677a74c7d998b654e4bfd50ba4347d0f3deed85284dbd041004a184353806", size = 16317436 }, + { url = "https://files.pythonhosted.org/packages/0b/d9/f93e4522cf1de51ff1a985ead75df85523cd1b689128b1b033c9e31204b8/uv-0.6.3-py3-none-win32.whl", hash = "sha256:dc2d965481bba716a0cf9d0f81896a70c341a854f0e4273f1887f22e52e5c9fb", size = 15545377 }, + { url = "https://files.pythonhosted.org/packages/91/ea/27dd790ec0d1f8c4ced06e27a409522bd157ed295a1140b3fb6cac3cd39a/uv-0.6.3-py3-none-win_amd64.whl", hash = "sha256:8fc19471fd4cfde1b31a47c239591d7c6dc0a31213f206d3953c528f9f3b406c", size = 16860609 }, + { url = "https://files.pythonhosted.org/packages/97/0f/01e48493264d75cfac6c953809e11c8356c77fb6be32dfce831bcf481ab2/uv-0.6.3-py3-none-win_arm64.whl", hash = "sha256:94a9d59c05f22829388e51a62a9cfddef4000a112e1c561bb5bd5761d4d672f1", size = 15697009 }, ] From dcc83d13379e178ff38347a3f10e9c4205f117d4 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sat, 1 Mar 2025 17:08:33 -0600 Subject: [PATCH 52/58] Fix new type issues --- src/azul/client.py | 4 +++- src/azul/server.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/azul/client.py b/src/azul/client.py index f604ee1..e10e70e 100644 --- a/src/azul/client.py +++ b/src/azul/client.py @@ -96,7 +96,9 @@ async def read_advertisements( # trio.socket.inet_aton(network_adapter), # ), # ) - group_bin = trio.socket.inet_pton(addrinfo[0], addrinfo[4][0]) + addr_port = addrinfo[4][0] + assert isinstance(addr_port, str) + group_bin = trio.socket.inet_pton(addrinfo[0], addr_port) # Join group if addrinfo[0] == trio.socket.AF_INET: # IPv4 mreq = group_bin + struct.pack("=I", trio.socket.INADDR_ANY) diff --git a/src/azul/server.py b/src/azul/server.py index ef9f7f4..9b3697e 100755 --- a/src/azul/server.py +++ b/src/azul/server.py @@ -491,7 +491,7 @@ async def stop_server(self, event: Event[None] | None = None) -> None: async def post_advertisement( self, udp_socket: trio.socket.SocketType, - send_to_ip: str, + send_to_ip: str | int, hosting_port: int, ) -> None: """Post server advertisement packet.""" From 5d627686b511c18f9d14eb45a69c0898ccafa69a Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Tue, 8 Apr 2025 00:25:40 -0500 Subject: [PATCH 53/58] Switch to https://github.com/adhtruong/mirrors-typos See https://github.com/crate-ci/typos/issues/390 --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f002e4b..8bf2618 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.9 + rev: v0.11.4 hooks: - id: ruff types: [file] @@ -40,11 +40,11 @@ repos: - id: codespell additional_dependencies: - tomli - - repo: https://github.com/crate-ci/typos - rev: v1.30.0 + - repo: https://github.com/adhtruong/mirrors-typos + rev: v1.31.1 hooks: - id: typos - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.4.1 + rev: v1.5.2 hooks: - id: zizmor From 871a85504495edabf94dbeb1e73f7405f5b08147 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Tue, 8 Apr 2025 00:26:42 -0500 Subject: [PATCH 54/58] Ruff fixes --- computer_players/machine_client.py | 4 ++-- computer_players/minimax.py | 7 +++---- src/azul/game.py | 7 ++++--- src/azul/sprite.py | 26 +++++++++++++------------- src/azul/state.py | 2 +- tests/test_sprite.py | 2 +- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/computer_players/machine_client.py b/computer_players/machine_client.py index 5a616f1..69fa89d 100644 --- a/computer_players/machine_client.py +++ b/computer_players/machine_client.py @@ -174,9 +174,9 @@ async def preform_action(self, action: Action) -> None: raw_source, raw_dest = action if isinstance(raw_source, SelectableSourceTiles): source = raw_source - dest = cast(tuple[SelectableDestinationTiles, ...], raw_dest) + dest = cast("tuple[SelectableDestinationTiles, ...]", raw_dest) else: - dest = cast(tuple[SelectableDestinationTiles, ...], action) + dest = cast("tuple[SelectableDestinationTiles, ...]", action) else: dest = action diff --git a/computer_players/minimax.py b/computer_players/minimax.py index 10f9568..4c46356 100644 --- a/computer_players/minimax.py +++ b/computer_players/minimax.py @@ -11,13 +11,12 @@ import operator import random from abc import ABC, abstractmethod -from collections.abc import Callable from enum import IntEnum, auto from math import inf as infinity from typing import TYPE_CHECKING, Generic, NamedTuple, TypeVar, cast if TYPE_CHECKING: - from collections.abc import Iterable + from collections.abc import Callable, Iterable class Player(IntEnum): @@ -117,7 +116,7 @@ def minimax( best = min elif current_player == Player.CHANCE: value = 0 - best = cast(Callable[[float, float], float], sum) + best = cast("Callable[[float, float], float]", sum) else: raise ValueError(f"Unexpected player type {current_player!r}") @@ -173,7 +172,7 @@ def alphabeta( set_idx = 1 elif current_player == Player.CHANCE: value = 0 - best = cast(Callable[[float, float], float], sum) + best = cast("Callable[[float, float], float]", sum) else: raise ValueError(f"Unexpected player type {current_player!r}") diff --git a/src/azul/game.py b/src/azul/game.py index 3def590..7339b19 100644 --- a/src/azul/game.py +++ b/src/azul/game.py @@ -897,9 +897,10 @@ def __init__( self.add_component(sprite.DragClickEventComponent()) self.rows_id = rows_id - self.rows: dict[int, tuple[int, int]] = { - i: (Tile.blank, 0) for i in range(5) - } + self.rows: dict[int, tuple[int, int]] = dict.fromkeys( + range(5), + (Tile.blank, 0), + ) self.update_image() self.visible = True diff --git a/src/azul/sprite.py b/src/azul/sprite.py index f753ddb..7d9dd20 100644 --- a/src/azul/sprite.py +++ b/src/azul/sprite.py @@ -262,14 +262,14 @@ def get_mask(self, identifier: int | str) -> Mask: def set_image(self, identifier: int | str) -> None: """Set sprite component's image by identifier.""" - outline = cast(OutlineComponent, self.get_component("outline")) + outline = cast("OutlineComponent", self.get_component("outline")) if outline.active and outline.mod not in str(identifier): identifier = outline.get_outline(identifier) if identifier == self.set_surface: return - sprite = cast(Sprite, self.manager.get_component("sprite")) + sprite = cast("Sprite", self.manager.get_component("sprite")) sprite.image = self.get_image(identifier) sprite.mask = self.get_mask(identifier) @@ -308,7 +308,7 @@ def active(self) -> bool: def set_color(self, color: Color | None) -> None: """Set color. If None, disable, otherwise enable.""" - manager = cast(ImageComponent, self.manager) + manager = cast("ImageComponent", self.manager) prev = self.active self.__active = color is not None if color is None: @@ -332,7 +332,7 @@ def get_outline_descriptor(self, identifier: str | int) -> str: def save_outline(self, identifier: str | int) -> None: """Save outlined version of given identifier image.""" - manager = cast(ImageComponent, self.manager) + manager = cast("ImageComponent", self.manager) outlined = self.get_outline_descriptor(identifier) if manager.image_exists(outlined): @@ -385,7 +385,7 @@ def precalculate_all_outlined( color: Color | tuple[int, int, int], ) -> None: """Precalculate all images outlined.""" - manager = cast(ImageComponent, self.manager) + manager = cast("ImageComponent", self.manager) for image in manager.list_images(): self.precalculate_outline(image, color) @@ -411,7 +411,7 @@ def __init__(self) -> None: super().__init__("animation") def default() -> Iterator[int | str | None]: - manager = cast(ImageComponent, self.manager) + manager = cast("ImageComponent", self.manager) while True: yield manager.set_surface @@ -440,7 +440,7 @@ async def tick(self, tick_event: Event[TickEventData]) -> None: for _ in range(int(updates)): new = self.fetch_controller_new_state() if new is not None: - manager = cast(ImageComponent, self.manager) + manager = cast("ImageComponent", self.manager) manager.set_image(new) def bind_handlers(self) -> None: @@ -479,7 +479,7 @@ def __init__( def point_toward(self, position: Iterable[int | float]) -> None: """Change self.heading to point toward a given position.""" - sprite = cast(Sprite, self.get_component("sprite")) + sprite = cast("Sprite", self.get_component("sprite")) self.heading = Vector2.from_points( sprite.location, position, @@ -487,7 +487,7 @@ def point_toward(self, position: Iterable[int | float]) -> None: def move_heading_distance(self, distance: float) -> None: """Move distance in heading direction.""" - sprite = cast(Sprite, self.get_component("sprite")) + sprite = cast("Sprite", self.get_component("sprite")) change = self.heading * distance if change: sprite.location += change @@ -527,7 +527,7 @@ def __init__(self, event_raise_name: str = "reached_destination") -> None: def update_heading(self) -> None: """Update the heading of the movement component.""" - movement = cast(MovementComponent, self.get_component("movement")) + movement = cast("MovementComponent", self.get_component("movement")) to_dest = self.to_destination() # If magnitude is zero if to_dest @ to_dest == 0: @@ -553,7 +553,7 @@ def __get_destination(self) -> Vector2: def to_destination(self) -> Vector2: """Return vector of self.location to self.destination.""" - sprite = cast(Sprite, self.get_component("sprite")) + sprite = cast("Sprite", self.get_component("sprite")) return Vector2.from_points(sprite.location, self.destination) async def move_destination_time(self, time_passed: float) -> None: @@ -563,7 +563,7 @@ async def move_destination_time(self, time_passed: float) -> None: return sprite, movement = cast( - tuple[Sprite, MovementComponent], + "tuple[Sprite, MovementComponent]", self.get_components(("sprite", "movement")), ) @@ -644,7 +644,7 @@ async def press_start( if not self.manager_exists: return - sprite = cast(Sprite, self.get_component("sprite")) + sprite = cast("Sprite", self.get_component("sprite")) pos = event.data["pos"] button = event.data["button"] diff --git a/src/azul/state.py b/src/azul/state.py index 1182aea..6514ac0 100644 --- a/src/azul/state.py +++ b/src/azul/state.py @@ -99,7 +99,7 @@ def generate_bag_contents() -> Counter[int]: tile_types = 5 tile_count = 100 count_each = tile_count // tile_types - return Counter({type_: count_each for type_ in range(tile_types)}) + return Counter(dict.fromkeys(range(tile_types), count_each)) def bag_draw_tile(bag: Counter[int]) -> int: diff --git a/tests/test_sprite.py b/tests/test_sprite.py index 9c7ce9a..1344fc2 100644 --- a/tests/test_sprite.py +++ b/tests/test_sprite.py @@ -232,7 +232,7 @@ def test_image_component_set_image_affects_sprite( image_component: ImageComponent, ) -> None: image = Surface((1, 1)) - sprite = cast(Sprite, image_component.manager.get_component("sprite")) + sprite = cast("Sprite", image_component.manager.get_component("sprite")) image_component.add_image("test_image", image) assert sprite.image is None image_component.set_image("test_image") From 7936da3f19327e01a5ae89936cb8c02008181cf9 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Tue, 15 Apr 2025 22:13:30 -0500 Subject: [PATCH 55/58] Upgrade dependencies, enable more mypy checks, add licence heder to `namedtuple_mod` --- .pre-commit-config.yaml | 2 +- pyproject.toml | 1 + src/azul/namedtuple_mod.py | 12 +- src/azul/vector.py | 17 +- uv.lock | 416 +++++++++++++++++++------------------ 5 files changed, 240 insertions(+), 208 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8bf2618..8175a35 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.4 + rev: v0.11.5 hooks: - id: ruff types: [file] diff --git a/pyproject.toml b/pyproject.toml index c3a5cad..5ffacb8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,6 +79,7 @@ package = true [tool.mypy] plugins = ["numpy.typing.mypy_plugin"] files = ["src/azul/", "computer_players"] +enable_error_code = ["truthy-bool", "mutable-override"] show_column_numbers = true show_error_codes = true show_traceback = true diff --git a/src/azul/namedtuple_mod.py b/src/azul/namedtuple_mod.py index 1832697..21ba1e2 100644 --- a/src/azul/namedtuple_mod.py +++ b/src/azul/namedtuple_mod.py @@ -1,4 +1,14 @@ -"""typing.NamedTupleMeta mod.""" +"""typing.NamedTupleMeta modification. + +Removes the requirement that NamedTuple can only inherit from +NamedTuple or Generic + +Licensed under the Python Software Foundation License +(see https://github.com/python/cpython/blob/main/LICENSE) + +Original source that this is a modified portion of: +https://github.com/python/cpython/blob/main/Lib/typing.py +""" from __future__ import annotations diff --git a/src/azul/vector.py b/src/azul/vector.py index f9aa830..60fb30e 100644 --- a/src/azul/vector.py +++ b/src/azul/vector.py @@ -23,12 +23,13 @@ __title__ = "Vector Module" __author__ = "CoolCat467" __license__ = "GNU General Public License Version 3" -__version__ = "2.0.0" +__version__ = "2.0.1" import math import sys from typing import ( TYPE_CHECKING, + ClassVar, ) from azul.namedtuple_mod import NamedTupleMeta @@ -66,11 +67,20 @@ class BaseVector: __slots__ = () if TYPE_CHECKING: + # Because of type hacks later on, pretend we have + # the same things NamedTuple does + _field_defaults: ClassVar[dict[str, float]] + _fields: ClassVar[tuple[str, ...]] # D105 is 'Missing docstring in magic method', but this is to handle # typing issues def __iter__(self) -> Iterator[float]: ... # noqa: D105 def __getitem__(self, value: int) -> float: ... # noqa: D105 + def _asdict(self) -> dict[str, float]: ... + def _replace(self, /, **kwds: int | float) -> Self: ... + def __getnewargs__(self) -> tuple[float, ...]: ... # noqa: D105 + @classmethod + def _make(cls, iterable: Iterable[float]) -> Self: ... @classmethod def from_iter(cls: type[Self], iterable: Iterable[float]) -> Self: @@ -99,6 +109,10 @@ def normalized(self: Self) -> Self: """Return a normalized (unit) vector.""" return self / self.magnitude() + def __bool__(self: Self) -> bool: + """Return if any component is nonzero.""" + return any(self) + # rhs is Right Hand Side def __add__( self: Self, @@ -218,6 +232,7 @@ def clamp(self: Self, min_value: float, max_value: float) -> Self: if TYPE_CHECKING: VectorBase = BaseVector else: + # In reality, it's a NamedTuple metaclass VectorBase = type.__new__( NamedTupleMeta, "VectorBase", diff --git a/uv.lock b/uv.lock index 22ccfe9..3d45437 100644 --- a/uv.lock +++ b/uv.lock @@ -4,11 +4,11 @@ requires-python = ">=3.10" [[package]] name = "attrs" -version = "25.1.0" +version = "25.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/49/7c/fdf464bcc51d23881d110abd74b512a42b3d5d376a55a831b44c603ae17f/attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e", size = 810562 } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/30/d4986a882011f9df997a55e6becd864812ccfcd821d64aac8570ee39f719/attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a", size = 63152 }, + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, ] [[package]] @@ -183,62 +183,62 @@ wheels = [ [[package]] name = "coverage" -version = "7.6.12" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0c/d6/2b53ab3ee99f2262e6f0b8369a43f6d66658eab45510331c0b3d5c8c4272/coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2", size = 805941 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/67/81dc41ec8f548c365d04a29f1afd492d3176b372c33e47fa2a45a01dc13a/coverage-7.6.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:704c8c8c6ce6569286ae9622e534b4f5b9759b6f2cd643f1c1a61f666d534fe8", size = 208345 }, - { url = "https://files.pythonhosted.org/packages/33/43/17f71676016c8829bde69e24c852fef6bd9ed39f774a245d9ec98f689fa0/coverage-7.6.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ad7525bf0241e5502168ae9c643a2f6c219fa0a283001cee4cf23a9b7da75879", size = 208775 }, - { url = "https://files.pythonhosted.org/packages/86/25/c6ff0775f8960e8c0840845b723eed978d22a3cd9babd2b996e4a7c502c6/coverage-7.6.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06097c7abfa611c91edb9e6920264e5be1d6ceb374efb4986f38b09eed4cb2fe", size = 237925 }, - { url = "https://files.pythonhosted.org/packages/b0/3d/5f5bd37046243cb9d15fff2c69e498c2f4fe4f9b42a96018d4579ed3506f/coverage-7.6.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:220fa6c0ad7d9caef57f2c8771918324563ef0d8272c94974717c3909664e674", size = 235835 }, - { url = "https://files.pythonhosted.org/packages/b5/f1/9e6b75531fe33490b910d251b0bf709142e73a40e4e38a3899e6986fe088/coverage-7.6.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3688b99604a24492bcfe1c106278c45586eb819bf66a654d8a9a1433022fb2eb", size = 236966 }, - { url = "https://files.pythonhosted.org/packages/4f/bc/aef5a98f9133851bd1aacf130e754063719345d2fb776a117d5a8d516971/coverage-7.6.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1a987778b9c71da2fc8948e6f2656da6ef68f59298b7e9786849634c35d2c3c", size = 236080 }, - { url = "https://files.pythonhosted.org/packages/eb/d0/56b4ab77f9b12aea4d4c11dc11cdcaa7c29130b837eb610639cf3400c9c3/coverage-7.6.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cec6b9ce3bd2b7853d4a4563801292bfee40b030c05a3d29555fd2a8ee9bd68c", size = 234393 }, - { url = "https://files.pythonhosted.org/packages/0d/77/28ef95c5d23fe3dd191a0b7d89c82fea2c2d904aef9315daf7c890e96557/coverage-7.6.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ace9048de91293e467b44bce0f0381345078389814ff6e18dbac8fdbf896360e", size = 235536 }, - { url = "https://files.pythonhosted.org/packages/29/62/18791d3632ee3ff3f95bc8599115707d05229c72db9539f208bb878a3d88/coverage-7.6.12-cp310-cp310-win32.whl", hash = "sha256:ea31689f05043d520113e0552f039603c4dd71fa4c287b64cb3606140c66f425", size = 211063 }, - { url = "https://files.pythonhosted.org/packages/fc/57/b3878006cedfd573c963e5c751b8587154eb10a61cc0f47a84f85c88a355/coverage-7.6.12-cp310-cp310-win_amd64.whl", hash = "sha256:676f92141e3c5492d2a1596d52287d0d963df21bf5e55c8b03075a60e1ddf8aa", size = 211955 }, - { url = "https://files.pythonhosted.org/packages/64/2d/da78abbfff98468c91fd63a73cccdfa0e99051676ded8dd36123e3a2d4d5/coverage-7.6.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e18aafdfb3e9ec0d261c942d35bd7c28d031c5855dadb491d2723ba54f4c3015", size = 208464 }, - { url = "https://files.pythonhosted.org/packages/31/f2/c269f46c470bdabe83a69e860c80a82e5e76840e9f4bbd7f38f8cebbee2f/coverage-7.6.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66fe626fd7aa5982cdebad23e49e78ef7dbb3e3c2a5960a2b53632f1f703ea45", size = 208893 }, - { url = "https://files.pythonhosted.org/packages/47/63/5682bf14d2ce20819998a49c0deadb81e608a59eed64d6bc2191bc8046b9/coverage-7.6.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ef01d70198431719af0b1f5dcbefc557d44a190e749004042927b2a3fed0702", size = 241545 }, - { url = "https://files.pythonhosted.org/packages/6a/b6/6b6631f1172d437e11067e1c2edfdb7238b65dff965a12bce3b6d1bf2be2/coverage-7.6.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e92ae5a289a4bc4c0aae710c0948d3c7892e20fd3588224ebe242039573bf0", size = 239230 }, - { url = "https://files.pythonhosted.org/packages/c7/01/9cd06cbb1be53e837e16f1b4309f6357e2dfcbdab0dd7cd3b1a50589e4e1/coverage-7.6.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e695df2c58ce526eeab11a2e915448d3eb76f75dffe338ea613c1201b33bab2f", size = 241013 }, - { url = "https://files.pythonhosted.org/packages/4b/26/56afefc03c30871326e3d99709a70d327ac1f33da383cba108c79bd71563/coverage-7.6.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d74c08e9aaef995f8c4ef6d202dbd219c318450fe2a76da624f2ebb9c8ec5d9f", size = 239750 }, - { url = "https://files.pythonhosted.org/packages/dd/ea/88a1ff951ed288f56aa561558ebe380107cf9132facd0b50bced63ba7238/coverage-7.6.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e995b3b76ccedc27fe4f477b349b7d64597e53a43fc2961db9d3fbace085d69d", size = 238462 }, - { url = "https://files.pythonhosted.org/packages/6e/d4/1d9404566f553728889409eff82151d515fbb46dc92cbd13b5337fa0de8c/coverage-7.6.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b1f097878d74fe51e1ddd1be62d8e3682748875b461232cf4b52ddc6e6db0bba", size = 239307 }, - { url = "https://files.pythonhosted.org/packages/12/c1/e453d3b794cde1e232ee8ac1d194fde8e2ba329c18bbf1b93f6f5eef606b/coverage-7.6.12-cp311-cp311-win32.whl", hash = "sha256:1f7ffa05da41754e20512202c866d0ebfc440bba3b0ed15133070e20bf5aeb5f", size = 211117 }, - { url = "https://files.pythonhosted.org/packages/d5/db/829185120c1686fa297294f8fcd23e0422f71070bf85ef1cc1a72ecb2930/coverage-7.6.12-cp311-cp311-win_amd64.whl", hash = "sha256:e216c5c45f89ef8971373fd1c5d8d1164b81f7f5f06bbf23c37e7908d19e8558", size = 212019 }, - { url = "https://files.pythonhosted.org/packages/e2/7f/4af2ed1d06ce6bee7eafc03b2ef748b14132b0bdae04388e451e4b2c529b/coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad", size = 208645 }, - { url = "https://files.pythonhosted.org/packages/dc/60/d19df912989117caa95123524d26fc973f56dc14aecdec5ccd7d0084e131/coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3", size = 208898 }, - { url = "https://files.pythonhosted.org/packages/bd/10/fecabcf438ba676f706bf90186ccf6ff9f6158cc494286965c76e58742fa/coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574", size = 242987 }, - { url = "https://files.pythonhosted.org/packages/4c/53/4e208440389e8ea936f5f2b0762dcd4cb03281a7722def8e2bf9dc9c3d68/coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985", size = 239881 }, - { url = "https://files.pythonhosted.org/packages/c4/47/2ba744af8d2f0caa1f17e7746147e34dfc5f811fb65fc153153722d58835/coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750", size = 242142 }, - { url = "https://files.pythonhosted.org/packages/e9/90/df726af8ee74d92ee7e3bf113bf101ea4315d71508952bd21abc3fae471e/coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea", size = 241437 }, - { url = "https://files.pythonhosted.org/packages/f6/af/995263fd04ae5f9cf12521150295bf03b6ba940d0aea97953bb4a6db3e2b/coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3", size = 239724 }, - { url = "https://files.pythonhosted.org/packages/1c/8e/5bb04f0318805e190984c6ce106b4c3968a9562a400180e549855d8211bd/coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a", size = 241329 }, - { url = "https://files.pythonhosted.org/packages/9e/9d/fa04d9e6c3f6459f4e0b231925277cfc33d72dfab7fa19c312c03e59da99/coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95", size = 211289 }, - { url = "https://files.pythonhosted.org/packages/53/40/53c7ffe3c0c3fff4d708bc99e65f3d78c129110d6629736faf2dbd60ad57/coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288", size = 212079 }, - { url = "https://files.pythonhosted.org/packages/76/89/1adf3e634753c0de3dad2f02aac1e73dba58bc5a3a914ac94a25b2ef418f/coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1", size = 208673 }, - { url = "https://files.pythonhosted.org/packages/ce/64/92a4e239d64d798535c5b45baac6b891c205a8a2e7c9cc8590ad386693dc/coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd", size = 208945 }, - { url = "https://files.pythonhosted.org/packages/b4/d0/4596a3ef3bca20a94539c9b1e10fd250225d1dec57ea78b0867a1cf9742e/coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9", size = 242484 }, - { url = "https://files.pythonhosted.org/packages/1c/ef/6fd0d344695af6718a38d0861408af48a709327335486a7ad7e85936dc6e/coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e", size = 239525 }, - { url = "https://files.pythonhosted.org/packages/0c/4b/373be2be7dd42f2bcd6964059fd8fa307d265a29d2b9bcf1d044bcc156ed/coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4", size = 241545 }, - { url = "https://files.pythonhosted.org/packages/a6/7d/0e83cc2673a7790650851ee92f72a343827ecaaea07960587c8f442b5cd3/coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6", size = 241179 }, - { url = "https://files.pythonhosted.org/packages/ff/8c/566ea92ce2bb7627b0900124e24a99f9244b6c8c92d09ff9f7633eb7c3c8/coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3", size = 239288 }, - { url = "https://files.pythonhosted.org/packages/7d/e4/869a138e50b622f796782d642c15fb5f25a5870c6d0059a663667a201638/coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc", size = 241032 }, - { url = "https://files.pythonhosted.org/packages/ae/28/a52ff5d62a9f9e9fe9c4f17759b98632edd3a3489fce70154c7d66054dd3/coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3", size = 211315 }, - { url = "https://files.pythonhosted.org/packages/bc/17/ab849b7429a639f9722fa5628364c28d675c7ff37ebc3268fe9840dda13c/coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef", size = 212099 }, - { url = "https://files.pythonhosted.org/packages/d2/1c/b9965bf23e171d98505eb5eb4fb4d05c44efd256f2e0f19ad1ba8c3f54b0/coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e", size = 209511 }, - { url = "https://files.pythonhosted.org/packages/57/b3/119c201d3b692d5e17784fee876a9a78e1b3051327de2709392962877ca8/coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703", size = 209729 }, - { url = "https://files.pythonhosted.org/packages/52/4e/a7feb5a56b266304bc59f872ea07b728e14d5a64f1ad3a2cc01a3259c965/coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0", size = 253988 }, - { url = "https://files.pythonhosted.org/packages/65/19/069fec4d6908d0dae98126aa7ad08ce5130a6decc8509da7740d36e8e8d2/coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924", size = 249697 }, - { url = "https://files.pythonhosted.org/packages/1c/da/5b19f09ba39df7c55f77820736bf17bbe2416bbf5216a3100ac019e15839/coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b", size = 252033 }, - { url = "https://files.pythonhosted.org/packages/1e/89/4c2750df7f80a7872267f7c5fe497c69d45f688f7b3afe1297e52e33f791/coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d", size = 251535 }, - { url = "https://files.pythonhosted.org/packages/78/3b/6d3ae3c1cc05f1b0460c51e6f6dcf567598cbd7c6121e5ad06643974703c/coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827", size = 249192 }, - { url = "https://files.pythonhosted.org/packages/6e/8e/c14a79f535ce41af7d436bbad0d3d90c43d9e38ec409b4770c894031422e/coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9", size = 250627 }, - { url = "https://files.pythonhosted.org/packages/cb/79/b7cee656cfb17a7f2c1b9c3cee03dd5d8000ca299ad4038ba64b61a9b044/coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3", size = 212033 }, - { url = "https://files.pythonhosted.org/packages/b6/c3/f7aaa3813f1fa9a4228175a7bd368199659d392897e184435a3b66408dd3/coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f", size = 213240 }, - { url = "https://files.pythonhosted.org/packages/7a/7f/05818c62c7afe75df11e0233bd670948d68b36cdbf2a339a095bc02624a8/coverage-7.6.12-pp39.pp310-none-any.whl", hash = "sha256:7e39e845c4d764208e7b8f6a21c541ade741e2c41afabdfa1caa28687a3c98cf", size = 200558 }, - { url = "https://files.pythonhosted.org/packages/fb/b2/f655700e1024dec98b10ebaafd0cedbc25e40e4abe62a3c8e2ceef4f8f0a/coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953", size = 200552 }, +version = "7.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/01/1c5e6ee4ebaaa5e079db933a9a45f61172048c7efa06648445821a201084/coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe", size = 211379 }, + { url = "https://files.pythonhosted.org/packages/e9/16/a463389f5ff916963471f7c13585e5f38c6814607306b3cb4d6b4cf13384/coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28", size = 211814 }, + { url = "https://files.pythonhosted.org/packages/b8/b1/77062b0393f54d79064dfb72d2da402657d7c569cfbc724d56ac0f9c67ed/coverage-7.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3", size = 240937 }, + { url = "https://files.pythonhosted.org/packages/d7/54/c7b00a23150083c124e908c352db03bcd33375494a4beb0c6d79b35448b9/coverage-7.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676", size = 238849 }, + { url = "https://files.pythonhosted.org/packages/f7/ec/a6b7cfebd34e7b49f844788fda94713035372b5200c23088e3bbafb30970/coverage-7.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d", size = 239986 }, + { url = "https://files.pythonhosted.org/packages/21/8c/c965ecef8af54e6d9b11bfbba85d4f6a319399f5f724798498387f3209eb/coverage-7.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a", size = 239896 }, + { url = "https://files.pythonhosted.org/packages/40/83/070550273fb4c480efa8381735969cb403fa8fd1626d74865bfaf9e4d903/coverage-7.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c", size = 238613 }, + { url = "https://files.pythonhosted.org/packages/07/76/fbb2540495b01d996d38e9f8897b861afed356be01160ab4e25471f4fed1/coverage-7.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f", size = 238909 }, + { url = "https://files.pythonhosted.org/packages/a3/7e/76d604db640b7d4a86e5dd730b73e96e12a8185f22b5d0799025121f4dcb/coverage-7.8.0-cp310-cp310-win32.whl", hash = "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f", size = 213948 }, + { url = "https://files.pythonhosted.org/packages/5c/a7/f8ce4aafb4a12ab475b56c76a71a40f427740cf496c14e943ade72e25023/coverage-7.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23", size = 214844 }, + { url = "https://files.pythonhosted.org/packages/2b/77/074d201adb8383addae5784cb8e2dac60bb62bfdf28b2b10f3a3af2fda47/coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27", size = 211493 }, + { url = "https://files.pythonhosted.org/packages/a9/89/7a8efe585750fe59b48d09f871f0e0c028a7b10722b2172dfe021fa2fdd4/coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea", size = 211921 }, + { url = "https://files.pythonhosted.org/packages/e9/ef/96a90c31d08a3f40c49dbe897df4f1fd51fb6583821a1a1c5ee30cc8f680/coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7", size = 244556 }, + { url = "https://files.pythonhosted.org/packages/89/97/dcd5c2ce72cee9d7b0ee8c89162c24972fb987a111b92d1a3d1d19100c61/coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040", size = 242245 }, + { url = "https://files.pythonhosted.org/packages/b2/7b/b63cbb44096141ed435843bbb251558c8e05cc835c8da31ca6ffb26d44c0/coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543", size = 244032 }, + { url = "https://files.pythonhosted.org/packages/97/e3/7fa8c2c00a1ef530c2a42fa5df25a6971391f92739d83d67a4ee6dcf7a02/coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2", size = 243679 }, + { url = "https://files.pythonhosted.org/packages/4f/b3/e0a59d8df9150c8a0c0841d55d6568f0a9195692136c44f3d21f1842c8f6/coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318", size = 241852 }, + { url = "https://files.pythonhosted.org/packages/9b/82/db347ccd57bcef150c173df2ade97976a8367a3be7160e303e43dd0c795f/coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9", size = 242389 }, + { url = "https://files.pythonhosted.org/packages/21/f6/3f7d7879ceb03923195d9ff294456241ed05815281f5254bc16ef71d6a20/coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c", size = 213997 }, + { url = "https://files.pythonhosted.org/packages/28/87/021189643e18ecf045dbe1e2071b2747901f229df302de01c998eeadf146/coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78", size = 214911 }, + { url = "https://files.pythonhosted.org/packages/aa/12/4792669473297f7973518bec373a955e267deb4339286f882439b8535b39/coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc", size = 211684 }, + { url = "https://files.pythonhosted.org/packages/be/e1/2a4ec273894000ebedd789e8f2fc3813fcaf486074f87fd1c5b2cb1c0a2b/coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6", size = 211935 }, + { url = "https://files.pythonhosted.org/packages/f8/3a/7b14f6e4372786709a361729164125f6b7caf4024ce02e596c4a69bccb89/coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d", size = 245994 }, + { url = "https://files.pythonhosted.org/packages/54/80/039cc7f1f81dcbd01ea796d36d3797e60c106077e31fd1f526b85337d6a1/coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05", size = 242885 }, + { url = "https://files.pythonhosted.org/packages/10/e0/dc8355f992b6cc2f9dcd5ef6242b62a3f73264893bc09fbb08bfcab18eb4/coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a", size = 245142 }, + { url = "https://files.pythonhosted.org/packages/43/1b/33e313b22cf50f652becb94c6e7dae25d8f02e52e44db37a82de9ac357e8/coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6", size = 244906 }, + { url = "https://files.pythonhosted.org/packages/05/08/c0a8048e942e7f918764ccc99503e2bccffba1c42568693ce6955860365e/coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47", size = 243124 }, + { url = "https://files.pythonhosted.org/packages/5b/62/ea625b30623083c2aad645c9a6288ad9fc83d570f9adb913a2abdba562dd/coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe", size = 244317 }, + { url = "https://files.pythonhosted.org/packages/62/cb/3871f13ee1130a6c8f020e2f71d9ed269e1e2124aa3374d2180ee451cee9/coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545", size = 214170 }, + { url = "https://files.pythonhosted.org/packages/88/26/69fe1193ab0bfa1eb7a7c0149a066123611baba029ebb448500abd8143f9/coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b", size = 214969 }, + { url = "https://files.pythonhosted.org/packages/f3/21/87e9b97b568e223f3438d93072479c2f36cc9b3f6b9f7094b9d50232acc0/coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd", size = 211708 }, + { url = "https://files.pythonhosted.org/packages/75/be/882d08b28a0d19c9c4c2e8a1c6ebe1f79c9c839eb46d4fca3bd3b34562b9/coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00", size = 211981 }, + { url = "https://files.pythonhosted.org/packages/7a/1d/ce99612ebd58082fbe3f8c66f6d8d5694976c76a0d474503fa70633ec77f/coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64", size = 245495 }, + { url = "https://files.pythonhosted.org/packages/dc/8d/6115abe97df98db6b2bd76aae395fcc941d039a7acd25f741312ced9a78f/coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067", size = 242538 }, + { url = "https://files.pythonhosted.org/packages/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008", size = 244561 }, + { url = "https://files.pythonhosted.org/packages/22/70/c10c77cd77970ac965734fe3419f2c98665f6e982744a9bfb0e749d298f4/coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733", size = 244633 }, + { url = "https://files.pythonhosted.org/packages/38/5a/4f7569d946a07c952688debee18c2bb9ab24f88027e3d71fd25dbc2f9dca/coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323", size = 242712 }, + { url = "https://files.pythonhosted.org/packages/bb/a1/03a43b33f50475a632a91ea8c127f7e35e53786dbe6781c25f19fd5a65f8/coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3", size = 244000 }, + { url = "https://files.pythonhosted.org/packages/6a/89/ab6c43b1788a3128e4d1b7b54214548dcad75a621f9d277b14d16a80d8a1/coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d", size = 214195 }, + { url = "https://files.pythonhosted.org/packages/12/12/6bf5f9a8b063d116bac536a7fb594fc35cb04981654cccb4bbfea5dcdfa0/coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487", size = 214998 }, + { url = "https://files.pythonhosted.org/packages/2a/e6/1e9df74ef7a1c983a9c7443dac8aac37a46f1939ae3499424622e72a6f78/coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25", size = 212541 }, + { url = "https://files.pythonhosted.org/packages/04/51/c32174edb7ee49744e2e81c4b1414ac9df3dacfcb5b5f273b7f285ad43f6/coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42", size = 212767 }, + { url = "https://files.pythonhosted.org/packages/e9/8f/f454cbdb5212f13f29d4a7983db69169f1937e869a5142bce983ded52162/coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502", size = 256997 }, + { url = "https://files.pythonhosted.org/packages/e6/74/2bf9e78b321216d6ee90a81e5c22f912fc428442c830c4077b4a071db66f/coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1", size = 252708 }, + { url = "https://files.pythonhosted.org/packages/92/4d/50d7eb1e9a6062bee6e2f92e78b0998848a972e9afad349b6cdde6fa9e32/coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4", size = 255046 }, + { url = "https://files.pythonhosted.org/packages/40/9e/71fb4e7402a07c4198ab44fc564d09d7d0ffca46a9fb7b0a7b929e7641bd/coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73", size = 256139 }, + { url = "https://files.pythonhosted.org/packages/49/1a/78d37f7a42b5beff027e807c2843185961fdae7fe23aad5a4837c93f9d25/coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a", size = 254307 }, + { url = "https://files.pythonhosted.org/packages/58/e9/8fb8e0ff6bef5e170ee19d59ca694f9001b2ec085dc99b4f65c128bb3f9a/coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883", size = 255116 }, + { url = "https://files.pythonhosted.org/packages/56/b0/d968ecdbe6fe0a863de7169bbe9e8a476868959f3af24981f6a10d2b6924/coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada", size = 214909 }, + { url = "https://files.pythonhosted.org/packages/87/e9/d6b7ef9fecf42dfb418d93544af47c940aa83056c49e6021a564aafbc91f/coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257", size = 216068 }, + { url = "https://files.pythonhosted.org/packages/c4/f1/1da77bb4c920aa30e82fa9b6ea065da3467977c2e5e032e38e66f1c57ffd/coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd", size = 203443 }, + { url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435 }, ] [package.optional-dependencies] @@ -248,43 +248,47 @@ toml = [ [[package]] name = "cryptography" -version = "44.0.1" +version = "44.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c7/67/545c79fe50f7af51dbad56d16b23fe33f63ee6a5d956b3cb68ea110cbe64/cryptography-44.0.1.tar.gz", hash = "sha256:f51f5705ab27898afda1aaa430f34ad90dc117421057782022edf0600bec5f14", size = 710819 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/72/27/5e3524053b4c8889da65cf7814a9d0d8514a05194a25e1e34f46852ee6eb/cryptography-44.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf688f615c29bfe9dfc44312ca470989279f0e94bb9f631f85e3459af8efc009", size = 6642022 }, - { url = "https://files.pythonhosted.org/packages/34/b9/4d1fa8d73ae6ec350012f89c3abfbff19fc95fe5420cf972e12a8d182986/cryptography-44.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd7c7e2d71d908dc0f8d2027e1604102140d84b155e658c20e8ad1304317691f", size = 3943865 }, - { url = "https://files.pythonhosted.org/packages/6e/57/371a9f3f3a4500807b5fcd29fec77f418ba27ffc629d88597d0d1049696e/cryptography-44.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887143b9ff6bad2b7570da75a7fe8bbf5f65276365ac259a5d2d5147a73775f2", size = 4162562 }, - { url = "https://files.pythonhosted.org/packages/c5/1d/5b77815e7d9cf1e3166988647f336f87d5634a5ccecec2ffbe08ef8dd481/cryptography-44.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:322eb03ecc62784536bc173f1483e76747aafeb69c8728df48537eb431cd1911", size = 3951923 }, - { url = "https://files.pythonhosted.org/packages/28/01/604508cd34a4024467cd4105887cf27da128cba3edd435b54e2395064bfb/cryptography-44.0.1-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:21377472ca4ada2906bc313168c9dc7b1d7ca417b63c1c3011d0c74b7de9ae69", size = 3685194 }, - { url = "https://files.pythonhosted.org/packages/c6/3d/d3c55d4f1d24580a236a6753902ef6d8aafd04da942a1ee9efb9dc8fd0cb/cryptography-44.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:df978682c1504fc93b3209de21aeabf2375cb1571d4e61907b3e7a2540e83026", size = 4187790 }, - { url = "https://files.pythonhosted.org/packages/ea/a6/44d63950c8588bfa8594fd234d3d46e93c3841b8e84a066649c566afb972/cryptography-44.0.1-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:eb3889330f2a4a148abead555399ec9a32b13b7c8ba969b72d8e500eb7ef84cd", size = 3951343 }, - { url = "https://files.pythonhosted.org/packages/c1/17/f5282661b57301204cbf188254c1a0267dbd8b18f76337f0a7ce1038888c/cryptography-44.0.1-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8e6a85a93d0642bd774460a86513c5d9d80b5c002ca9693e63f6e540f1815ed0", size = 4187127 }, - { url = "https://files.pythonhosted.org/packages/f3/68/abbae29ed4f9d96596687f3ceea8e233f65c9645fbbec68adb7c756bb85a/cryptography-44.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6f76fdd6fd048576a04c5210d53aa04ca34d2ed63336d4abd306d0cbe298fddf", size = 4070666 }, - { url = "https://files.pythonhosted.org/packages/0f/10/cf91691064a9e0a88ae27e31779200b1505d3aee877dbe1e4e0d73b4f155/cryptography-44.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6c8acf6f3d1f47acb2248ec3ea261171a671f3d9428e34ad0357148d492c7864", size = 4288811 }, - { url = "https://files.pythonhosted.org/packages/38/78/74ea9eb547d13c34e984e07ec8a473eb55b19c1451fe7fc8077c6a4b0548/cryptography-44.0.1-cp37-abi3-win32.whl", hash = "sha256:24979e9f2040c953a94bf3c6782e67795a4c260734e5264dceea65c8f4bae64a", size = 2771882 }, - { url = "https://files.pythonhosted.org/packages/cf/6c/3907271ee485679e15c9f5e93eac6aa318f859b0aed8d369afd636fafa87/cryptography-44.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:fd0ee90072861e276b0ff08bd627abec29e32a53b2be44e41dbcdf87cbee2b00", size = 3206989 }, - { url = "https://files.pythonhosted.org/packages/9f/f1/676e69c56a9be9fd1bffa9bc3492366901f6e1f8f4079428b05f1414e65c/cryptography-44.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a2d8a7045e1ab9b9f803f0d9531ead85f90c5f2859e653b61497228b18452008", size = 6643714 }, - { url = "https://files.pythonhosted.org/packages/ba/9f/1775600eb69e72d8f9931a104120f2667107a0ee478f6ad4fe4001559345/cryptography-44.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8272f257cf1cbd3f2e120f14c68bff2b6bdfcc157fafdee84a1b795efd72862", size = 3943269 }, - { url = "https://files.pythonhosted.org/packages/25/ba/e00d5ad6b58183829615be7f11f55a7b6baa5a06910faabdc9961527ba44/cryptography-44.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e8d181e90a777b63f3f0caa836844a1182f1f265687fac2115fcf245f5fbec3", size = 4166461 }, - { url = "https://files.pythonhosted.org/packages/b3/45/690a02c748d719a95ab08b6e4decb9d81e0ec1bac510358f61624c86e8a3/cryptography-44.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:436df4f203482f41aad60ed1813811ac4ab102765ecae7a2bbb1dbb66dcff5a7", size = 3950314 }, - { url = "https://files.pythonhosted.org/packages/e6/50/bf8d090911347f9b75adc20f6f6569ed6ca9b9bff552e6e390f53c2a1233/cryptography-44.0.1-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4f422e8c6a28cf8b7f883eb790695d6d45b0c385a2583073f3cec434cc705e1a", size = 3686675 }, - { url = "https://files.pythonhosted.org/packages/e1/e7/cfb18011821cc5f9b21efb3f94f3241e3a658d267a3bf3a0f45543858ed8/cryptography-44.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:72198e2b5925155497a5a3e8c216c7fb3e64c16ccee11f0e7da272fa93b35c4c", size = 4190429 }, - { url = "https://files.pythonhosted.org/packages/07/ef/77c74d94a8bfc1a8a47b3cafe54af3db537f081742ee7a8a9bd982b62774/cryptography-44.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a46a89ad3e6176223b632056f321bc7de36b9f9b93b2cc1cccf935a3849dc62", size = 3950039 }, - { url = "https://files.pythonhosted.org/packages/6d/b9/8be0ff57c4592382b77406269b1e15650c9f1a167f9e34941b8515b97159/cryptography-44.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:53f23339864b617a3dfc2b0ac8d5c432625c80014c25caac9082314e9de56f41", size = 4189713 }, - { url = "https://files.pythonhosted.org/packages/78/e1/4b6ac5f4100545513b0847a4d276fe3c7ce0eacfa73e3b5ebd31776816ee/cryptography-44.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:888fcc3fce0c888785a4876ca55f9f43787f4c5c1cc1e2e0da71ad481ff82c5b", size = 4071193 }, - { url = "https://files.pythonhosted.org/packages/3d/cb/afff48ceaed15531eab70445abe500f07f8f96af2bb35d98af6bfa89ebd4/cryptography-44.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00918d859aa4e57db8299607086f793fa7813ae2ff5a4637e318a25ef82730f7", size = 4289566 }, - { url = "https://files.pythonhosted.org/packages/30/6f/4eca9e2e0f13ae459acd1ca7d9f0257ab86e68f44304847610afcb813dc9/cryptography-44.0.1-cp39-abi3-win32.whl", hash = "sha256:9b336599e2cb77b1008cb2ac264b290803ec5e8e89d618a5e978ff5eb6f715d9", size = 2772371 }, - { url = "https://files.pythonhosted.org/packages/d2/05/5533d30f53f10239616a357f080892026db2d550a40c393d0a8a7af834a9/cryptography-44.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:e403f7f766ded778ecdb790da786b418a9f2394f36e8cc8b796cc056ab05f44f", size = 3207303 }, - { url = "https://files.pythonhosted.org/packages/15/06/507bfb5c7e048114a0185dd65f7814677a2ba285d15705c3d69e660c21d7/cryptography-44.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1f9a92144fa0c877117e9748c74501bea842f93d21ee00b0cf922846d9d0b183", size = 3380782 }, - { url = "https://files.pythonhosted.org/packages/e0/f1/7fb4982d59aa86e1a116c812b545e7fc045352be07738ae3fb278835a9a4/cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:610a83540765a8d8ce0f351ce42e26e53e1f774a6efb71eb1b41eb01d01c3d12", size = 3888155 }, - { url = "https://files.pythonhosted.org/packages/60/7b/cbc203838d3092203493d18b923fbbb1de64e0530b332a713ba376905b0b/cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5fed5cd6102bb4eb843e3315d2bf25fede494509bddadb81e03a859c1bc17b83", size = 4106417 }, - { url = "https://files.pythonhosted.org/packages/12/c7/2fe59fb085ab418acc82e91e040a6acaa7b1696fcc1c1055317537fbf0d3/cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f4daefc971c2d1f82f03097dc6f216744a6cd2ac0f04c68fb935ea2ba2a0d420", size = 3887540 }, - { url = "https://files.pythonhosted.org/packages/48/89/09fc7b115f60f5bd970b80e32244f8e9aeeb9244bf870b63420cec3b5cd5/cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94f99f2b943b354a5b6307d7e8d19f5c423a794462bde2bf310c770ba052b1c4", size = 4106040 }, - { url = "https://files.pythonhosted.org/packages/2e/38/3fd83c4690dc7d753a442a284b3826ea5e5c380a411443c66421cd823898/cryptography-44.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d9c5b9f698a83c8bd71e0f4d3f9f839ef244798e5ffe96febfa9714717db7af7", size = 3134657 }, +sdist = { url = "https://files.pythonhosted.org/packages/cd/25/4ce80c78963834b8a9fd1cc1266be5ed8d1840785c0f2e1b73b8d128d505/cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0", size = 710807 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/ef/83e632cfa801b221570c5f58c0369db6fa6cef7d9ff859feab1aae1a8a0f/cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7", size = 6676361 }, + { url = "https://files.pythonhosted.org/packages/30/ec/7ea7c1e4c8fc8329506b46c6c4a52e2f20318425d48e0fe597977c71dbce/cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1", size = 3952350 }, + { url = "https://files.pythonhosted.org/packages/27/61/72e3afdb3c5ac510330feba4fc1faa0fe62e070592d6ad00c40bb69165e5/cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb", size = 4166572 }, + { url = "https://files.pythonhosted.org/packages/26/e4/ba680f0b35ed4a07d87f9e98f3ebccb05091f3bf6b5a478b943253b3bbd5/cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843", size = 3958124 }, + { url = "https://files.pythonhosted.org/packages/9c/e8/44ae3e68c8b6d1cbc59040288056df2ad7f7f03bbcaca6b503c737ab8e73/cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5", size = 3678122 }, + { url = "https://files.pythonhosted.org/packages/27/7b/664ea5e0d1eab511a10e480baf1c5d3e681c7d91718f60e149cec09edf01/cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c", size = 4191831 }, + { url = "https://files.pythonhosted.org/packages/2a/07/79554a9c40eb11345e1861f46f845fa71c9e25bf66d132e123d9feb8e7f9/cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a", size = 3960583 }, + { url = "https://files.pythonhosted.org/packages/bb/6d/858e356a49a4f0b591bd6789d821427de18432212e137290b6d8a817e9bf/cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308", size = 4191753 }, + { url = "https://files.pythonhosted.org/packages/b2/80/62df41ba4916067fa6b125aa8c14d7e9181773f0d5d0bd4dcef580d8b7c6/cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688", size = 4079550 }, + { url = "https://files.pythonhosted.org/packages/f3/cd/2558cc08f7b1bb40683f99ff4327f8dcfc7de3affc669e9065e14824511b/cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7", size = 4298367 }, + { url = "https://files.pythonhosted.org/packages/71/59/94ccc74788945bc3bd4cf355d19867e8057ff5fdbcac781b1ff95b700fb1/cryptography-44.0.2-cp37-abi3-win32.whl", hash = "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79", size = 2772843 }, + { url = "https://files.pythonhosted.org/packages/ca/2c/0d0bbaf61ba05acb32f0841853cfa33ebb7a9ab3d9ed8bb004bd39f2da6a/cryptography-44.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa", size = 3209057 }, + { url = "https://files.pythonhosted.org/packages/9e/be/7a26142e6d0f7683d8a382dd963745e65db895a79a280a30525ec92be890/cryptography-44.0.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3", size = 6677789 }, + { url = "https://files.pythonhosted.org/packages/06/88/638865be7198a84a7713950b1db7343391c6066a20e614f8fa286eb178ed/cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639", size = 3951919 }, + { url = "https://files.pythonhosted.org/packages/d7/fc/99fe639bcdf58561dfad1faa8a7369d1dc13f20acd78371bb97a01613585/cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd", size = 4167812 }, + { url = "https://files.pythonhosted.org/packages/53/7b/aafe60210ec93d5d7f552592a28192e51d3c6b6be449e7fd0a91399b5d07/cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181", size = 3958571 }, + { url = "https://files.pythonhosted.org/packages/16/32/051f7ce79ad5a6ef5e26a92b37f172ee2d6e1cce09931646eef8de1e9827/cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea", size = 3679832 }, + { url = "https://files.pythonhosted.org/packages/78/2b/999b2a1e1ba2206f2d3bca267d68f350beb2b048a41ea827e08ce7260098/cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699", size = 4193719 }, + { url = "https://files.pythonhosted.org/packages/72/97/430e56e39a1356e8e8f10f723211a0e256e11895ef1a135f30d7d40f2540/cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9", size = 3960852 }, + { url = "https://files.pythonhosted.org/packages/89/33/c1cf182c152e1d262cac56850939530c05ca6c8d149aa0dcee490b417e99/cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23", size = 4193906 }, + { url = "https://files.pythonhosted.org/packages/e1/99/87cf26d4f125380dc674233971069bc28d19b07f7755b29861570e513650/cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922", size = 4081572 }, + { url = "https://files.pythonhosted.org/packages/b3/9f/6a3e0391957cc0c5f84aef9fbdd763035f2b52e998a53f99345e3ac69312/cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4", size = 4298631 }, + { url = "https://files.pythonhosted.org/packages/e2/a5/5bc097adb4b6d22a24dea53c51f37e480aaec3465285c253098642696423/cryptography-44.0.2-cp39-abi3-win32.whl", hash = "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5", size = 2773792 }, + { url = "https://files.pythonhosted.org/packages/33/cf/1f7649b8b9a3543e042d3f348e398a061923ac05b507f3f4d95f11938aa9/cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6", size = 3210957 }, + { url = "https://files.pythonhosted.org/packages/99/10/173be140714d2ebaea8b641ff801cbcb3ef23101a2981cbf08057876f89e/cryptography-44.0.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:af4ff3e388f2fa7bff9f7f2b31b87d5651c45731d3e8cfa0944be43dff5cfbdb", size = 3396886 }, + { url = "https://files.pythonhosted.org/packages/2f/b4/424ea2d0fce08c24ede307cead3409ecbfc2f566725d4701b9754c0a1174/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0529b1d5a0105dd3731fa65680b45ce49da4d8115ea76e9da77a875396727b41", size = 3892387 }, + { url = "https://files.pythonhosted.org/packages/28/20/8eaa1a4f7c68a1cb15019dbaad59c812d4df4fac6fd5f7b0b9c5177f1edd/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7ca25849404be2f8e4b3c59483d9d3c51298a22c1c61a0e84415104dacaf5562", size = 4109922 }, + { url = "https://files.pythonhosted.org/packages/11/25/5ed9a17d532c32b3bc81cc294d21a36c772d053981c22bd678396bc4ae30/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:268e4e9b177c76d569e8a145a6939eca9a5fec658c932348598818acf31ae9a5", size = 3895715 }, + { url = "https://files.pythonhosted.org/packages/63/31/2aac03b19c6329b62c45ba4e091f9de0b8f687e1b0cd84f101401bece343/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9eb9d22b0a5d8fd9925a7764a054dca914000607dff201a24c791ff5c799e1fa", size = 4109876 }, + { url = "https://files.pythonhosted.org/packages/99/ec/6e560908349843718db1a782673f36852952d52a55ab14e46c42c8a7690a/cryptography-44.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2bf7bf75f7df9715f810d1b038870309342bff3069c5bd8c6b96128cb158668d", size = 3131719 }, + { url = "https://files.pythonhosted.org/packages/d6/d7/f30e75a6aa7d0f65031886fa4a1485c2fbfe25a1896953920f6a9cfe2d3b/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:909c97ab43a9c0c0b0ada7a1281430e4e5ec0458e6d9244c0e821bbf152f061d", size = 3887513 }, + { url = "https://files.pythonhosted.org/packages/9c/b4/7a494ce1032323ca9db9a3661894c66e0d7142ad2079a4249303402d8c71/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96e7a5e9d6e71f9f4fca8eebfd603f8e86c5225bb18eb621b2c1e50b290a9471", size = 4107432 }, + { url = "https://files.pythonhosted.org/packages/45/f8/6b3ec0bc56123b344a8d2b3264a325646d2dcdbdd9848b5e6f3d37db90b3/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d1b3031093a366ac767b3feb8bcddb596671b3aaff82d4050f984da0c248b615", size = 3891421 }, + { url = "https://files.pythonhosted.org/packages/57/ff/f3b4b2d007c2a646b0f69440ab06224f9cf37a977a72cdb7b50632174e8a/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:04abd71114848aa25edb28e225ab5f268096f44cf0127f3d36975bdf1bdf3390", size = 4107081 }, ] [[package]] @@ -307,11 +311,11 @@ wheels = [ [[package]] name = "iniconfig" -version = "2.0.0" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, ] [[package]] @@ -440,62 +444,64 @@ wheels = [ [[package]] name = "orjson" -version = "3.10.15" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/5dea21763eeff8c1590076918a446ea3d6140743e0e36f58f369928ed0f4/orjson-3.10.15.tar.gz", hash = "sha256:05ca7fe452a2e9d8d9d706a2984c95b9c2ebc5db417ce0b7a49b91d50642a23e", size = 5282482 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/52/09/e5ff18ad009e6f97eb7edc5f67ef98b3ce0c189da9c3eaca1f9587cd4c61/orjson-3.10.15-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:552c883d03ad185f720d0c09583ebde257e41b9521b74ff40e08b7dec4559c04", size = 249532 }, - { url = "https://files.pythonhosted.org/packages/bd/b8/a75883301fe332bd433d9b0ded7d2bb706ccac679602c3516984f8814fb5/orjson-3.10.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e3e8d438d02e4854f70bfdc03a6bcdb697358dbaa6bcd19cbe24d24ece1f8", size = 125229 }, - { url = "https://files.pythonhosted.org/packages/83/4b/22f053e7a364cc9c685be203b1e40fc5f2b3f164a9b2284547504eec682e/orjson-3.10.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c2c79fa308e6edb0ffab0a31fd75a7841bf2a79a20ef08a3c6e3b26814c8ca8", size = 150148 }, - { url = "https://files.pythonhosted.org/packages/63/64/1b54fc75ca328b57dd810541a4035fe48c12a161d466e3cf5b11a8c25649/orjson-3.10.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cb85490aa6bf98abd20607ab5c8324c0acb48d6da7863a51be48505646c814", size = 139748 }, - { url = "https://files.pythonhosted.org/packages/5e/ff/ff0c5da781807bb0a5acd789d9a7fbcb57f7b0c6e1916595da1f5ce69f3c/orjson-3.10.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763dadac05e4e9d2bc14938a45a2d0560549561287d41c465d3c58aec818b164", size = 154559 }, - { url = "https://files.pythonhosted.org/packages/4e/9a/11e2974383384ace8495810d4a2ebef5f55aacfc97b333b65e789c9d362d/orjson-3.10.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a330b9b4734f09a623f74a7490db713695e13b67c959713b78369f26b3dee6bf", size = 130349 }, - { url = "https://files.pythonhosted.org/packages/2d/c4/dd9583aea6aefee1b64d3aed13f51d2aadb014028bc929fe52936ec5091f/orjson-3.10.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a61a4622b7ff861f019974f73d8165be1bd9a0855e1cad18ee167acacabeb061", size = 138514 }, - { url = "https://files.pythonhosted.org/packages/53/3e/dcf1729230654f5c5594fc752de1f43dcf67e055ac0d300c8cdb1309269a/orjson-3.10.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:acd271247691574416b3228db667b84775c497b245fa275c6ab90dc1ffbbd2b3", size = 130940 }, - { url = "https://files.pythonhosted.org/packages/e8/2b/b9759fe704789937705c8a56a03f6c03e50dff7df87d65cba9a20fec5282/orjson-3.10.15-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4759b109c37f635aa5c5cc93a1b26927bfde24b254bcc0e1149a9fada253d2d", size = 414713 }, - { url = "https://files.pythonhosted.org/packages/a7/6b/b9dfdbd4b6e20a59238319eb203ae07c3f6abf07eef909169b7a37ae3bba/orjson-3.10.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9e992fd5cfb8b9f00bfad2fd7a05a4299db2bbe92e6440d9dd2fab27655b3182", size = 141028 }, - { url = "https://files.pythonhosted.org/packages/7c/b5/40f5bbea619c7caf75eb4d652a9821875a8ed04acc45fe3d3ef054ca69fb/orjson-3.10.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f95fb363d79366af56c3f26b71df40b9a583b07bbaaf5b317407c4d58497852e", size = 129715 }, - { url = "https://files.pythonhosted.org/packages/38/60/2272514061cbdf4d672edbca6e59c7e01cd1c706e881427d88f3c3e79761/orjson-3.10.15-cp310-cp310-win32.whl", hash = "sha256:f9875f5fea7492da8ec2444839dcc439b0ef298978f311103d0b7dfd775898ab", size = 142473 }, - { url = "https://files.pythonhosted.org/packages/11/5d/be1490ff7eafe7fef890eb4527cf5bcd8cfd6117f3efe42a3249ec847b60/orjson-3.10.15-cp310-cp310-win_amd64.whl", hash = "sha256:17085a6aa91e1cd70ca8533989a18b5433e15d29c574582f76f821737c8d5806", size = 133564 }, - { url = "https://files.pythonhosted.org/packages/7a/a2/21b25ce4a2c71dbb90948ee81bd7a42b4fbfc63162e57faf83157d5540ae/orjson-3.10.15-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c4cc83960ab79a4031f3119cc4b1a1c627a3dc09df125b27c4201dff2af7eaa6", size = 249533 }, - { url = "https://files.pythonhosted.org/packages/b2/85/2076fc12d8225698a51278009726750c9c65c846eda741e77e1761cfef33/orjson-3.10.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddbeef2481d895ab8be5185f2432c334d6dec1f5d1933a9c83014d188e102cef", size = 125230 }, - { url = "https://files.pythonhosted.org/packages/06/df/a85a7955f11274191eccf559e8481b2be74a7c6d43075d0a9506aa80284d/orjson-3.10.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e590a0477b23ecd5b0ac865b1b907b01b3c5535f5e8a8f6ab0e503efb896334", size = 150148 }, - { url = "https://files.pythonhosted.org/packages/37/b3/94c55625a29b8767c0eed194cb000b3787e3c23b4cdd13be17bae6ccbb4b/orjson-3.10.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6be38bd103d2fd9bdfa31c2720b23b5d47c6796bcb1d1b598e3924441b4298d", size = 139749 }, - { url = "https://files.pythonhosted.org/packages/53/ba/c608b1e719971e8ddac2379f290404c2e914cf8e976369bae3cad88768b1/orjson-3.10.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ff4f6edb1578960ed628a3b998fa54d78d9bb3e2eb2cfc5c2a09732431c678d0", size = 154558 }, - { url = "https://files.pythonhosted.org/packages/b2/c4/c1fb835bb23ad788a39aa9ebb8821d51b1c03588d9a9e4ca7de5b354fdd5/orjson-3.10.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0482b21d0462eddd67e7fce10b89e0b6ac56570424662b685a0d6fccf581e13", size = 130349 }, - { url = "https://files.pythonhosted.org/packages/78/14/bb2b48b26ab3c570b284eb2157d98c1ef331a8397f6c8bd983b270467f5c/orjson-3.10.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bb5cc3527036ae3d98b65e37b7986a918955f85332c1ee07f9d3f82f3a6899b5", size = 138513 }, - { url = "https://files.pythonhosted.org/packages/4a/97/d5b353a5fe532e92c46467aa37e637f81af8468aa894cd77d2ec8a12f99e/orjson-3.10.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d569c1c462912acdd119ccbf719cf7102ea2c67dd03b99edcb1a3048651ac96b", size = 130942 }, - { url = "https://files.pythonhosted.org/packages/b5/5d/a067bec55293cca48fea8b9928cfa84c623be0cce8141d47690e64a6ca12/orjson-3.10.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1e6d33efab6b71d67f22bf2962895d3dc6f82a6273a965fab762e64fa90dc399", size = 414717 }, - { url = "https://files.pythonhosted.org/packages/6f/9a/1485b8b05c6b4c4db172c438cf5db5dcfd10e72a9bc23c151a1137e763e0/orjson-3.10.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c33be3795e299f565681d69852ac8c1bc5c84863c0b0030b2b3468843be90388", size = 141033 }, - { url = "https://files.pythonhosted.org/packages/f8/d2/fc67523656e43a0c7eaeae9007c8b02e86076b15d591e9be11554d3d3138/orjson-3.10.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eea80037b9fae5339b214f59308ef0589fc06dc870578b7cce6d71eb2096764c", size = 129720 }, - { url = "https://files.pythonhosted.org/packages/79/42/f58c7bd4e5b54da2ce2ef0331a39ccbbaa7699b7f70206fbf06737c9ed7d/orjson-3.10.15-cp311-cp311-win32.whl", hash = "sha256:d5ac11b659fd798228a7adba3e37c010e0152b78b1982897020a8e019a94882e", size = 142473 }, - { url = "https://files.pythonhosted.org/packages/00/f8/bb60a4644287a544ec81df1699d5b965776bc9848d9029d9f9b3402ac8bb/orjson-3.10.15-cp311-cp311-win_amd64.whl", hash = "sha256:cf45e0214c593660339ef63e875f32ddd5aa3b4adc15e662cdb80dc49e194f8e", size = 133570 }, - { url = "https://files.pythonhosted.org/packages/66/85/22fe737188905a71afcc4bf7cc4c79cd7f5bbe9ed1fe0aac4ce4c33edc30/orjson-3.10.15-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9d11c0714fc85bfcf36ada1179400862da3288fc785c30e8297844c867d7505a", size = 249504 }, - { url = "https://files.pythonhosted.org/packages/48/b7/2622b29f3afebe938a0a9037e184660379797d5fd5234e5998345d7a5b43/orjson-3.10.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dba5a1e85d554e3897fa9fe6fbcff2ed32d55008973ec9a2b992bd9a65d2352d", size = 125080 }, - { url = "https://files.pythonhosted.org/packages/ce/8f/0b72a48f4403d0b88b2a41450c535b3e8989e8a2d7800659a967efc7c115/orjson-3.10.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7723ad949a0ea502df656948ddd8b392780a5beaa4c3b5f97e525191b102fff0", size = 150121 }, - { url = "https://files.pythonhosted.org/packages/06/ec/acb1a20cd49edb2000be5a0404cd43e3c8aad219f376ac8c60b870518c03/orjson-3.10.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6fd9bc64421e9fe9bd88039e7ce8e58d4fead67ca88e3a4014b143cec7684fd4", size = 139796 }, - { url = "https://files.pythonhosted.org/packages/33/e1/f7840a2ea852114b23a52a1c0b2bea0a1ea22236efbcdb876402d799c423/orjson-3.10.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dadba0e7b6594216c214ef7894c4bd5f08d7c0135f4dd0145600be4fbcc16767", size = 154636 }, - { url = "https://files.pythonhosted.org/packages/fa/da/31543337febd043b8fa80a3b67de627669b88c7b128d9ad4cc2ece005b7a/orjson-3.10.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b48f59114fe318f33bbaee8ebeda696d8ccc94c9e90bc27dbe72153094e26f41", size = 130621 }, - { url = "https://files.pythonhosted.org/packages/ed/78/66115dc9afbc22496530d2139f2f4455698be444c7c2475cb48f657cefc9/orjson-3.10.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:035fb83585e0f15e076759b6fedaf0abb460d1765b6a36f48018a52858443514", size = 138516 }, - { url = "https://files.pythonhosted.org/packages/22/84/cd4f5fb5427ffcf823140957a47503076184cb1ce15bcc1165125c26c46c/orjson-3.10.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d13b7fe322d75bf84464b075eafd8e7dd9eae05649aa2a5354cfa32f43c59f17", size = 130762 }, - { url = "https://files.pythonhosted.org/packages/93/1f/67596b711ba9f56dd75d73b60089c5c92057f1130bb3a25a0f53fb9a583b/orjson-3.10.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7066b74f9f259849629e0d04db6609db4cf5b973248f455ba5d3bd58a4daaa5b", size = 414700 }, - { url = "https://files.pythonhosted.org/packages/7c/0c/6a3b3271b46443d90efb713c3e4fe83fa8cd71cda0d11a0f69a03f437c6e/orjson-3.10.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:88dc3f65a026bd3175eb157fea994fca6ac7c4c8579fc5a86fc2114ad05705b7", size = 141077 }, - { url = "https://files.pythonhosted.org/packages/3b/9b/33c58e0bfc788995eccd0d525ecd6b84b40d7ed182dd0751cd4c1322ac62/orjson-3.10.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b342567e5465bd99faa559507fe45e33fc76b9fb868a63f1642c6bc0735ad02a", size = 129898 }, - { url = "https://files.pythonhosted.org/packages/01/c1/d577ecd2e9fa393366a1ea0a9267f6510d86e6c4bb1cdfb9877104cac44c/orjson-3.10.15-cp312-cp312-win32.whl", hash = "sha256:0a4f27ea5617828e6b58922fdbec67b0aa4bb844e2d363b9244c47fa2180e665", size = 142566 }, - { url = "https://files.pythonhosted.org/packages/ed/eb/a85317ee1732d1034b92d56f89f1de4d7bf7904f5c8fb9dcdd5b1c83917f/orjson-3.10.15-cp312-cp312-win_amd64.whl", hash = "sha256:ef5b87e7aa9545ddadd2309efe6824bd3dd64ac101c15dae0f2f597911d46eaa", size = 133732 }, - { url = "https://files.pythonhosted.org/packages/06/10/fe7d60b8da538e8d3d3721f08c1b7bff0491e8fa4dd3bf11a17e34f4730e/orjson-3.10.15-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bae0e6ec2b7ba6895198cd981b7cca95d1487d0147c8ed751e5632ad16f031a6", size = 249399 }, - { url = "https://files.pythonhosted.org/packages/6b/83/52c356fd3a61abd829ae7e4366a6fe8e8863c825a60d7ac5156067516edf/orjson-3.10.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f93ce145b2db1252dd86af37d4165b6faa83072b46e3995ecc95d4b2301b725a", size = 125044 }, - { url = "https://files.pythonhosted.org/packages/55/b2/d06d5901408e7ded1a74c7c20d70e3a127057a6d21355f50c90c0f337913/orjson-3.10.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c203f6f969210128af3acae0ef9ea6aab9782939f45f6fe02d05958fe761ef9", size = 150066 }, - { url = "https://files.pythonhosted.org/packages/75/8c/60c3106e08dc593a861755781c7c675a566445cc39558677d505878d879f/orjson-3.10.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8918719572d662e18b8af66aef699d8c21072e54b6c82a3f8f6404c1f5ccd5e0", size = 139737 }, - { url = "https://files.pythonhosted.org/packages/6a/8c/ae00d7d0ab8a4490b1efeb01ad4ab2f1982e69cc82490bf8093407718ff5/orjson-3.10.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f71eae9651465dff70aa80db92586ad5b92df46a9373ee55252109bb6b703307", size = 154804 }, - { url = "https://files.pythonhosted.org/packages/22/86/65dc69bd88b6dd254535310e97bc518aa50a39ef9c5a2a5d518e7a223710/orjson-3.10.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e117eb299a35f2634e25ed120c37c641398826c2f5a3d3cc39f5993b96171b9e", size = 130583 }, - { url = "https://files.pythonhosted.org/packages/bb/00/6fe01ededb05d52be42fabb13d93a36e51f1fd9be173bd95707d11a8a860/orjson-3.10.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:13242f12d295e83c2955756a574ddd6741c81e5b99f2bef8ed8d53e47a01e4b7", size = 138465 }, - { url = "https://files.pythonhosted.org/packages/db/2f/4cc151c4b471b0cdc8cb29d3eadbce5007eb0475d26fa26ed123dca93b33/orjson-3.10.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7946922ada8f3e0b7b958cc3eb22cfcf6c0df83d1fe5521b4a100103e3fa84c8", size = 130742 }, - { url = "https://files.pythonhosted.org/packages/9f/13/8a6109e4b477c518498ca37963d9c0eb1508b259725553fb53d53b20e2ea/orjson-3.10.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b7155eb1623347f0f22c38c9abdd738b287e39b9982e1da227503387b81b34ca", size = 414669 }, - { url = "https://files.pythonhosted.org/packages/22/7b/1d229d6d24644ed4d0a803de1b0e2df832032d5beda7346831c78191b5b2/orjson-3.10.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:208beedfa807c922da4e81061dafa9c8489c6328934ca2a562efa707e049e561", size = 141043 }, - { url = "https://files.pythonhosted.org/packages/cc/d3/6dc91156cf12ed86bed383bcb942d84d23304a1e57b7ab030bf60ea130d6/orjson-3.10.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eca81f83b1b8c07449e1d6ff7074e82e3fd6777e588f1a6632127f286a968825", size = 129826 }, - { url = "https://files.pythonhosted.org/packages/b3/38/c47c25b86f6996f1343be721b6ea4367bc1c8bc0fc3f6bbcd995d18cb19d/orjson-3.10.15-cp313-cp313-win32.whl", hash = "sha256:c03cd6eea1bd3b949d0d007c8d57049aa2b39bd49f58b4b2af571a5d3833d890", size = 142542 }, - { url = "https://files.pythonhosted.org/packages/27/f1/1d7ec15b20f8ce9300bc850de1e059132b88990e46cd0ccac29cbf11e4f9/orjson-3.10.15-cp313-cp313-win_amd64.whl", hash = "sha256:fd56a26a04f6ba5fb2045b0acc487a63162a958ed837648c5781e1fe3316cfbf", size = 133444 }, +version = "3.10.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/c7/03913cc4332174071950acf5b0735463e3f63760c80585ef369270c2b372/orjson-3.10.16.tar.gz", hash = "sha256:d2aaa5c495e11d17b9b93205f5fa196737ee3202f000aaebf028dc9a73750f10", size = 5410415 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/a6/22cb9b03baf167bc2d659c9e74d7580147f36e6a155e633801badfd5a74d/orjson-3.10.16-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4cb473b8e79154fa778fb56d2d73763d977be3dcc140587e07dbc545bbfc38f8", size = 249179 }, + { url = "https://files.pythonhosted.org/packages/d7/ce/3e68cc33020a6ebd8f359b8628b69d2132cd84fea68155c33057e502ee51/orjson-3.10.16-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:622a8e85eeec1948690409a19ca1c7d9fd8ff116f4861d261e6ae2094fe59a00", size = 138510 }, + { url = "https://files.pythonhosted.org/packages/dc/12/63bee7764ce12052f7c1a1393ce7f26dc392c93081eb8754dd3dce9b7c6b/orjson-3.10.16-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c682d852d0ce77613993dc967e90e151899fe2d8e71c20e9be164080f468e370", size = 132373 }, + { url = "https://files.pythonhosted.org/packages/b3/d5/2998c2f319adcd572f2b03ba2083e8176863d1055d8d713683ddcf927b71/orjson-3.10.16-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c520ae736acd2e32df193bcff73491e64c936f3e44a2916b548da048a48b46b", size = 136774 }, + { url = "https://files.pythonhosted.org/packages/00/03/88c236ae307bd0604623204d4a835e15fbf9c75b8535c8f13ef45abd413f/orjson-3.10.16-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:134f87c76bfae00f2094d85cfab261b289b76d78c6da8a7a3b3c09d362fd1e06", size = 138030 }, + { url = "https://files.pythonhosted.org/packages/66/ba/3e256ddfeb364f98fd6ac65774844090d356158b2d1de8998db2bf984503/orjson-3.10.16-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b59afde79563e2cf37cfe62ee3b71c063fd5546c8e662d7fcfc2a3d5031a5c4c", size = 142677 }, + { url = "https://files.pythonhosted.org/packages/2c/71/73a1214bd27baa2ea5184fff4aa6193a114dfb0aa5663dad48fe63e8cd29/orjson-3.10.16-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:113602f8241daaff05d6fad25bd481d54c42d8d72ef4c831bb3ab682a54d9e15", size = 132798 }, + { url = "https://files.pythonhosted.org/packages/53/ac/0b2f41c0a1e8c095439d0fab3b33103cf41a39be8e6aa2c56298a6034259/orjson-3.10.16-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4fc0077d101f8fab4031e6554fc17b4c2ad8fdbc56ee64a727f3c95b379e31da", size = 135450 }, + { url = "https://files.pythonhosted.org/packages/d9/ca/7524c7b0bc815d426ca134dab54cad519802287b808a3846b047a5b2b7a3/orjson-3.10.16-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:9c6bf6ff180cd69e93f3f50380224218cfab79953a868ea3908430bcfaf9cb5e", size = 412356 }, + { url = "https://files.pythonhosted.org/packages/05/1d/3ae2367c255276bf16ff7e1b210dd0af18bc8da20c4e4295755fc7de1268/orjson-3.10.16-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5673eadfa952f95a7cd76418ff189df11b0a9c34b1995dff43a6fdbce5d63bf4", size = 152769 }, + { url = "https://files.pythonhosted.org/packages/d3/2d/8eb10b6b1d30bb69c35feb15e5ba5ac82466cf743d562e3e8047540efd2f/orjson-3.10.16-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5fe638a423d852b0ae1e1a79895851696cb0d9fa0946fdbfd5da5072d9bb9551", size = 137223 }, + { url = "https://files.pythonhosted.org/packages/47/42/f043717930cb2de5fbebe47f308f101bed9ec2b3580b1f99c8284b2f5fe8/orjson-3.10.16-cp310-cp310-win32.whl", hash = "sha256:33af58f479b3c6435ab8f8b57999874b4b40c804c7a36b5cc6b54d8f28e1d3dd", size = 141734 }, + { url = "https://files.pythonhosted.org/packages/67/99/795ad7282b425b9fddcfb8a31bded5dcf84dba78ecb1e7ae716e84e794da/orjson-3.10.16-cp310-cp310-win_amd64.whl", hash = "sha256:0338356b3f56d71293c583350af26f053017071836b07e064e92819ecf1aa055", size = 133779 }, + { url = "https://files.pythonhosted.org/packages/97/29/43f91a5512b5d2535594438eb41c5357865fd5e64dec745d90a588820c75/orjson-3.10.16-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:44fcbe1a1884f8bc9e2e863168b0f84230c3d634afe41c678637d2728ea8e739", size = 249180 }, + { url = "https://files.pythonhosted.org/packages/0c/36/2a72d55e266473c19a86d97b7363bb8bf558ab450f75205689a287d5ce61/orjson-3.10.16-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78177bf0a9d0192e0b34c3d78bcff7fe21d1b5d84aeb5ebdfe0dbe637b885225", size = 138510 }, + { url = "https://files.pythonhosted.org/packages/bb/ad/f86d6f55c1a68b57ff6ea7966bce5f4e5163f2e526ddb7db9fc3c2c8d1c4/orjson-3.10.16-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12824073a010a754bb27330cad21d6e9b98374f497f391b8707752b96f72e741", size = 132373 }, + { url = "https://files.pythonhosted.org/packages/5e/8b/d18f2711493a809f3082a88fda89342bc8e16767743b909cd3c34989fba3/orjson-3.10.16-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddd41007e56284e9867864aa2f29f3136bb1dd19a49ca43c0b4eda22a579cf53", size = 136773 }, + { url = "https://files.pythonhosted.org/packages/a1/dc/ce025f002f8e0749e3f057c4d773a4d4de32b7b4c1fc5a50b429e7532586/orjson-3.10.16-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0877c4d35de639645de83666458ca1f12560d9fa7aa9b25d8bb8f52f61627d14", size = 138029 }, + { url = "https://files.pythonhosted.org/packages/0e/1b/cf9df85852b91160029d9f26014230366a2b4deb8cc51fabe68e250a8c1a/orjson-3.10.16-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9a09a539e9cc3beead3e7107093b4ac176d015bec64f811afb5965fce077a03c", size = 142677 }, + { url = "https://files.pythonhosted.org/packages/92/18/5b1e1e995bffad49dc4311a0bdfd874bc6f135fd20f0e1f671adc2c9910e/orjson-3.10.16-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31b98bc9b40610fec971d9a4d67bb2ed02eec0a8ae35f8ccd2086320c28526ca", size = 132800 }, + { url = "https://files.pythonhosted.org/packages/d6/eb/467f25b580e942fcca1344adef40633b7f05ac44a65a63fc913f9a805d58/orjson-3.10.16-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0ce243f5a8739f3a18830bc62dc2e05b69a7545bafd3e3249f86668b2bcd8e50", size = 135451 }, + { url = "https://files.pythonhosted.org/packages/8d/4b/9d10888038975cb375982e9339d9495bac382d5c976c500b8d6f2c8e2e4e/orjson-3.10.16-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:64792c0025bae049b3074c6abe0cf06f23c8e9f5a445f4bab31dc5ca23dbf9e1", size = 412358 }, + { url = "https://files.pythonhosted.org/packages/3b/e2/cfbcfcc4fbe619e0ca9bdbbfccb2d62b540bbfe41e0ee77d44a628594f59/orjson-3.10.16-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ea53f7e68eec718b8e17e942f7ca56c6bd43562eb19db3f22d90d75e13f0431d", size = 152772 }, + { url = "https://files.pythonhosted.org/packages/b9/d6/627a1b00569be46173007c11dde3da4618c9bfe18409325b0e3e2a82fe29/orjson-3.10.16-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a741ba1a9488c92227711bde8c8c2b63d7d3816883268c808fbeada00400c164", size = 137225 }, + { url = "https://files.pythonhosted.org/packages/0a/7b/a73c67b505021af845b9f05c7c848793258ea141fa2058b52dd9b067c2b4/orjson-3.10.16-cp311-cp311-win32.whl", hash = "sha256:c7ed2c61bb8226384c3fdf1fb01c51b47b03e3f4536c985078cccc2fd19f1619", size = 141733 }, + { url = "https://files.pythonhosted.org/packages/f4/22/5e8217c48d68c0adbfb181e749d6a733761074e598b083c69a1383d18147/orjson-3.10.16-cp311-cp311-win_amd64.whl", hash = "sha256:cd67d8b3e0e56222a2e7b7f7da9031e30ecd1fe251c023340b9f12caca85ab60", size = 133784 }, + { url = "https://files.pythonhosted.org/packages/5d/15/67ce9d4c959c83f112542222ea3b9209c1d424231d71d74c4890ea0acd2b/orjson-3.10.16-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6d3444abbfa71ba21bb042caa4b062535b122248259fdb9deea567969140abca", size = 249325 }, + { url = "https://files.pythonhosted.org/packages/da/2c/1426b06f30a1b9ada74b6f512c1ddf9d2760f53f61cdb59efeb9ad342133/orjson-3.10.16-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:30245c08d818fdcaa48b7d5b81499b8cae09acabb216fe61ca619876b128e184", size = 133621 }, + { url = "https://files.pythonhosted.org/packages/9e/88/18d26130954bc73bee3be10f95371ea1dfb8679e0e2c46b0f6d8c6289402/orjson-3.10.16-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0ba1d0baa71bf7579a4ccdcf503e6f3098ef9542106a0eca82395898c8a500a", size = 138270 }, + { url = "https://files.pythonhosted.org/packages/4f/f9/6d8b64fcd58fae072e80ee7981be8ba0d7c26ace954e5cd1d027fc80518f/orjson-3.10.16-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb0beefa5ef3af8845f3a69ff2a4aa62529b5acec1cfe5f8a6b4141033fd46ef", size = 132346 }, + { url = "https://files.pythonhosted.org/packages/16/3f/2513fd5bc786f40cd12af569c23cae6381aeddbefeed2a98f0a666eb5d0d/orjson-3.10.16-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6daa0e1c9bf2e030e93c98394de94506f2a4d12e1e9dadd7c53d5e44d0f9628e", size = 136845 }, + { url = "https://files.pythonhosted.org/packages/6d/42/b0e7b36720f5ab722b48e8ccf06514d4f769358dd73c51abd8728ef58d0b/orjson-3.10.16-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9da9019afb21e02410ef600e56666652b73eb3e4d213a0ec919ff391a7dd52aa", size = 138078 }, + { url = "https://files.pythonhosted.org/packages/a3/a8/d220afb8a439604be74fc755dbc740bded5ed14745ca536b304ed32eb18a/orjson-3.10.16-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:daeb3a1ee17b69981d3aae30c3b4e786b0f8c9e6c71f2b48f1aef934f63f38f4", size = 142712 }, + { url = "https://files.pythonhosted.org/packages/8c/88/7e41e9883c00f84f92fe357a8371edae816d9d7ef39c67b5106960c20389/orjson-3.10.16-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fed80eaf0e20a31942ae5d0728849862446512769692474be5e6b73123a23b", size = 133136 }, + { url = "https://files.pythonhosted.org/packages/e9/ca/61116095307ad0be828ea26093febaf59e38596d84a9c8d765c3c5e4934f/orjson-3.10.16-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73390ed838f03764540a7bdc4071fe0123914c2cc02fb6abf35182d5fd1b7a42", size = 135258 }, + { url = "https://files.pythonhosted.org/packages/dc/1b/09493cf7d801505f094c9295f79c98c1e0af2ac01c7ed8d25b30fcb19ada/orjson-3.10.16-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a22bba012a0c94ec02a7768953020ab0d3e2b884760f859176343a36c01adf87", size = 412326 }, + { url = "https://files.pythonhosted.org/packages/ea/02/125d7bbd7f7a500190ddc8ae5d2d3c39d87ed3ed28f5b37cfe76962c678d/orjson-3.10.16-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5385bbfdbc90ff5b2635b7e6bebf259652db00a92b5e3c45b616df75b9058e88", size = 152800 }, + { url = "https://files.pythonhosted.org/packages/f9/09/7658a9e3e793d5b3b00598023e0fb6935d0e7bbb8ff72311c5415a8ce677/orjson-3.10.16-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:02c6279016346e774dd92625d46c6c40db687b8a0d685aadb91e26e46cc33e1e", size = 137516 }, + { url = "https://files.pythonhosted.org/packages/29/87/32b7a4831e909d347278101a48d4cf9f3f25901b2295e7709df1651f65a1/orjson-3.10.16-cp312-cp312-win32.whl", hash = "sha256:7ca55097a11426db80f79378e873a8c51f4dde9ffc22de44850f9696b7eb0e8c", size = 141759 }, + { url = "https://files.pythonhosted.org/packages/35/ce/81a27e7b439b807bd393585271364cdddf50dc281fc57c4feef7ccb186a6/orjson-3.10.16-cp312-cp312-win_amd64.whl", hash = "sha256:86d127efdd3f9bf5f04809b70faca1e6836556ea3cc46e662b44dab3fe71f3d6", size = 133944 }, + { url = "https://files.pythonhosted.org/packages/87/b9/ff6aa28b8c86af9526160905593a2fe8d004ac7a5e592ee0b0ff71017511/orjson-3.10.16-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:148a97f7de811ba14bc6dbc4a433e0341ffd2cc285065199fb5f6a98013744bd", size = 249289 }, + { url = "https://files.pythonhosted.org/packages/6c/81/6d92a586149b52684ab8fd70f3623c91d0e6a692f30fd8c728916ab2263c/orjson-3.10.16-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:1d960c1bf0e734ea36d0adc880076de3846aaec45ffad29b78c7f1b7962516b8", size = 133640 }, + { url = "https://files.pythonhosted.org/packages/c2/88/b72443f4793d2e16039ab85d0026677932b15ab968595fb7149750d74134/orjson-3.10.16-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a318cd184d1269f68634464b12871386808dc8b7c27de8565234d25975a7a137", size = 138286 }, + { url = "https://files.pythonhosted.org/packages/c3/3c/72a22d4b28c076c4016d5a52bd644a8e4d849d3bb0373d9e377f9e3b2250/orjson-3.10.16-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df23f8df3ef9223d1d6748bea63fca55aae7da30a875700809c500a05975522b", size = 132307 }, + { url = "https://files.pythonhosted.org/packages/8a/a2/f1259561bdb6ad7061ff1b95dab082fe32758c4bc143ba8d3d70831f0a06/orjson-3.10.16-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b94dda8dd6d1378f1037d7f3f6b21db769ef911c4567cbaa962bb6dc5021cf90", size = 136739 }, + { url = "https://files.pythonhosted.org/packages/3d/af/c7583c4b34f33d8b8b90cfaab010ff18dd64e7074cc1e117a5f1eff20dcf/orjson-3.10.16-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f12970a26666a8775346003fd94347d03ccb98ab8aa063036818381acf5f523e", size = 138076 }, + { url = "https://files.pythonhosted.org/packages/d7/59/d7fc7fbdd3d4a64c2eae4fc7341a5aa39cf9549bd5e2d7f6d3c07f8b715b/orjson-3.10.16-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15a1431a245d856bd56e4d29ea0023eb4d2c8f71efe914beb3dee8ab3f0cd7fb", size = 142643 }, + { url = "https://files.pythonhosted.org/packages/92/0e/3bd8f2197d27601f16b4464ae948826da2bcf128af31230a9dbbad7ceb57/orjson-3.10.16-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c83655cfc247f399a222567d146524674a7b217af7ef8289c0ff53cfe8db09f0", size = 133168 }, + { url = "https://files.pythonhosted.org/packages/af/a8/351fd87b664b02f899f9144d2c3dc848b33ac04a5df05234cbfb9e2a7540/orjson-3.10.16-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fa59ae64cb6ddde8f09bdbf7baf933c4cd05734ad84dcf4e43b887eb24e37652", size = 135271 }, + { url = "https://files.pythonhosted.org/packages/ba/b0/a6d42a7d412d867c60c0337d95123517dd5a9370deea705ea1be0f89389e/orjson-3.10.16-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ca5426e5aacc2e9507d341bc169d8af9c3cbe88f4cd4c1cf2f87e8564730eb56", size = 412444 }, + { url = "https://files.pythonhosted.org/packages/79/ec/7572cd4e20863f60996f3f10bc0a6da64a6fd9c35954189a914cec0b7377/orjson-3.10.16-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6fd5da4edf98a400946cd3a195680de56f1e7575109b9acb9493331047157430", size = 152737 }, + { url = "https://files.pythonhosted.org/packages/a9/19/ceb9e8fed5403b2e76a8ac15f581b9d25780a3be3c9b3aa54b7777a210d5/orjson-3.10.16-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:980ecc7a53e567169282a5e0ff078393bac78320d44238da4e246d71a4e0e8f5", size = 137482 }, + { url = "https://files.pythonhosted.org/packages/1b/78/a78bb810f3786579dbbbd94768284cbe8f2fd65167cd7020260679665c17/orjson-3.10.16-cp313-cp313-win32.whl", hash = "sha256:28f79944dd006ac540a6465ebd5f8f45dfdf0948ff998eac7a908275b4c1add6", size = 141714 }, + { url = "https://files.pythonhosted.org/packages/81/9c/b66ce9245ff319df2c3278acd351a3f6145ef34b4a2d7f4b0f739368370f/orjson-3.10.16-cp313-cp313-win_amd64.whl", hash = "sha256:fe0a145e96d51971407cb8ba947e63ead2aa915db59d6631a355f5f2150b56b7", size = 133954 }, ] [[package]] @@ -530,11 +536,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.3.6" +version = "4.3.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, + { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499 }, ] [[package]] @@ -593,7 +599,7 @@ wheels = [ [[package]] name = "pytest" -version = "8.3.4" +version = "8.3.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -603,22 +609,22 @@ dependencies = [ { name = "pluggy" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, ] [[package]] name = "pytest-cov" -version = "6.0.0" +version = "6.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } +sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857 } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, + { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841 }, ] [[package]] @@ -637,27 +643,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.9.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/c3/418441a8170e8d53d05c0b9dad69760dbc7b8a12c10dbe6db1e1205d2377/ruff-0.9.9.tar.gz", hash = "sha256:0062ed13f22173e85f8f7056f9a24016e692efeea8704d1a5e8011b8aa850933", size = 3717448 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/c3/2c4afa9ba467555d074b146d9aed0633a56ccdb900839fb008295d037b89/ruff-0.9.9-py3-none-linux_armv6l.whl", hash = "sha256:628abb5ea10345e53dff55b167595a159d3e174d6720bf19761f5e467e68d367", size = 10027252 }, - { url = "https://files.pythonhosted.org/packages/33/d1/439e58487cf9eac26378332e25e7d5ade4b800ce1eec7dc2cfc9b0d7ca96/ruff-0.9.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6cd1428e834b35d7493354723543b28cc11dc14d1ce19b685f6e68e07c05ec7", size = 10840721 }, - { url = "https://files.pythonhosted.org/packages/50/44/fead822c38281ba0122f1b76b460488a175a9bd48b130650a6fb6dbcbcf9/ruff-0.9.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5ee162652869120ad260670706f3cd36cd3f32b0c651f02b6da142652c54941d", size = 10161439 }, - { url = "https://files.pythonhosted.org/packages/11/ae/d404a2ab8e61ddf6342e09cc6b7f7846cce6b243e45c2007dbe0ca928a5d/ruff-0.9.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3aa0f6b75082c9be1ec5a1db78c6d4b02e2375c3068438241dc19c7c306cc61a", size = 10336264 }, - { url = "https://files.pythonhosted.org/packages/6a/4e/7c268aa7d84cd709fb6f046b8972313142cffb40dfff1d2515c5e6288d54/ruff-0.9.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:584cc66e89fb5f80f84b05133dd677a17cdd86901d6479712c96597a3f28e7fe", size = 9908774 }, - { url = "https://files.pythonhosted.org/packages/cc/26/c618a878367ef1b76270fd027ca93692657d3f6122b84ba48911ef5f2edc/ruff-0.9.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abf3369325761a35aba75cd5c55ba1b5eb17d772f12ab168fbfac54be85cf18c", size = 11428127 }, - { url = "https://files.pythonhosted.org/packages/d7/9a/c5588a93d9bfed29f565baf193fe802fa676a0c837938137ea6cf0576d8c/ruff-0.9.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3403a53a32a90ce929aa2f758542aca9234befa133e29f4933dcef28a24317be", size = 12133187 }, - { url = "https://files.pythonhosted.org/packages/3e/ff/e7980a7704a60905ed7e156a8d73f604c846d9bd87deda9cabfa6cba073a/ruff-0.9.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:18454e7fa4e4d72cffe28a37cf6a73cb2594f81ec9f4eca31a0aaa9ccdfb1590", size = 11602937 }, - { url = "https://files.pythonhosted.org/packages/24/78/3690444ad9e3cab5c11abe56554c35f005b51d1d118b429765249095269f/ruff-0.9.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fadfe2c88724c9617339f62319ed40dcdadadf2888d5afb88bf3adee7b35bfb", size = 13771698 }, - { url = "https://files.pythonhosted.org/packages/6e/bf/e477c2faf86abe3988e0b5fd22a7f3520e820b2ee335131aca2e16120038/ruff-0.9.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6df104d08c442a1aabcfd254279b8cc1e2cbf41a605aa3e26610ba1ec4acf0b0", size = 11249026 }, - { url = "https://files.pythonhosted.org/packages/f7/82/cdaffd59e5a8cb5b14c408c73d7a555a577cf6645faaf83e52fe99521715/ruff-0.9.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d7c62939daf5b2a15af48abbd23bea1efdd38c312d6e7c4cedf5a24e03207e17", size = 10220432 }, - { url = "https://files.pythonhosted.org/packages/fe/a4/2507d0026225efa5d4412b6e294dfe54725a78652a5c7e29e6bd0fc492f3/ruff-0.9.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9494ba82a37a4b81b6a798076e4a3251c13243fc37967e998efe4cce58c8a8d1", size = 9874602 }, - { url = "https://files.pythonhosted.org/packages/d5/be/f3aab1813846b476c4bcffe052d232244979c3cd99d751c17afb530ca8e4/ruff-0.9.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4efd7a96ed6d36ef011ae798bf794c5501a514be369296c672dab7921087fa57", size = 10851212 }, - { url = "https://files.pythonhosted.org/packages/8b/45/8e5fd559bea0d2f57c4e12bf197a2fade2fac465aa518284f157dfbca92b/ruff-0.9.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ab90a7944c5a1296f3ecb08d1cbf8c2da34c7e68114b1271a431a3ad30cb660e", size = 11327490 }, - { url = "https://files.pythonhosted.org/packages/42/55/e6c90f13880aeef327746052907e7e930681f26a164fe130ddac28b08269/ruff-0.9.9-py3-none-win32.whl", hash = "sha256:6b4c376d929c25ecd6d87e182a230fa4377b8e5125a4ff52d506ee8c087153c1", size = 10227912 }, - { url = "https://files.pythonhosted.org/packages/35/b2/da925693cb82a1208aa34966c0f36cb222baca94e729dd22a587bc22d0f3/ruff-0.9.9-py3-none-win_amd64.whl", hash = "sha256:837982ea24091d4c1700ddb2f63b7070e5baec508e43b01de013dc7eff974ff1", size = 11355632 }, - { url = "https://files.pythonhosted.org/packages/31/d8/de873d1c1b020d668d8ec9855d390764cb90cf8f6486c0983da52be8b7b7/ruff-0.9.9-py3-none-win_arm64.whl", hash = "sha256:3ac78f127517209fe6d96ab00f3ba97cafe38718b23b1db3e96d8b2d39e37ddf", size = 10435860 }, +version = "0.11.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/71/5759b2a6b2279bb77fe15b1435b89473631c2cd6374d45ccdb6b785810be/ruff-0.11.5.tar.gz", hash = "sha256:cae2e2439cb88853e421901ec040a758960b576126dab520fa08e9de431d1bef", size = 3976488 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/db/6efda6381778eec7f35875b5cbefd194904832a1153d68d36d6b269d81a8/ruff-0.11.5-py3-none-linux_armv6l.whl", hash = "sha256:2561294e108eb648e50f210671cc56aee590fb6167b594144401532138c66c7b", size = 10103150 }, + { url = "https://files.pythonhosted.org/packages/44/f2/06cd9006077a8db61956768bc200a8e52515bf33a8f9b671ee527bb10d77/ruff-0.11.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac12884b9e005c12d0bd121f56ccf8033e1614f736f766c118ad60780882a077", size = 10898637 }, + { url = "https://files.pythonhosted.org/packages/18/f5/af390a013c56022fe6f72b95c86eb7b2585c89cc25d63882d3bfe411ecf1/ruff-0.11.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4bfd80a6ec559a5eeb96c33f832418bf0fb96752de0539905cf7b0cc1d31d779", size = 10236012 }, + { url = "https://files.pythonhosted.org/packages/b8/ca/b9bf954cfed165e1a0c24b86305d5c8ea75def256707f2448439ac5e0d8b/ruff-0.11.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0947c0a1afa75dcb5db4b34b070ec2bccee869d40e6cc8ab25aca11a7d527794", size = 10415338 }, + { url = "https://files.pythonhosted.org/packages/d9/4d/2522dde4e790f1b59885283f8786ab0046958dfd39959c81acc75d347467/ruff-0.11.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad871ff74b5ec9caa66cb725b85d4ef89b53f8170f47c3406e32ef040400b038", size = 9965277 }, + { url = "https://files.pythonhosted.org/packages/e5/7a/749f56f150eef71ce2f626a2f6988446c620af2f9ba2a7804295ca450397/ruff-0.11.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6cf918390cfe46d240732d4d72fa6e18e528ca1f60e318a10835cf2fa3dc19f", size = 11541614 }, + { url = "https://files.pythonhosted.org/packages/89/b2/7d9b8435222485b6aac627d9c29793ba89be40b5de11584ca604b829e960/ruff-0.11.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56145ee1478582f61c08f21076dc59153310d606ad663acc00ea3ab5b2125f82", size = 12198873 }, + { url = "https://files.pythonhosted.org/packages/00/e0/a1a69ef5ffb5c5f9c31554b27e030a9c468fc6f57055886d27d316dfbabd/ruff-0.11.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5f66f8f1e8c9fc594cbd66fbc5f246a8d91f916cb9667e80208663ec3728304", size = 11670190 }, + { url = "https://files.pythonhosted.org/packages/05/61/c1c16df6e92975072c07f8b20dad35cd858e8462b8865bc856fe5d6ccb63/ruff-0.11.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80b4df4d335a80315ab9afc81ed1cff62be112bd165e162b5eed8ac55bfc8470", size = 13902301 }, + { url = "https://files.pythonhosted.org/packages/79/89/0af10c8af4363304fd8cb833bd407a2850c760b71edf742c18d5a87bb3ad/ruff-0.11.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3068befab73620b8a0cc2431bd46b3cd619bc17d6f7695a3e1bb166b652c382a", size = 11350132 }, + { url = "https://files.pythonhosted.org/packages/b9/e1/ecb4c687cbf15164dd00e38cf62cbab238cad05dd8b6b0fc68b0c2785e15/ruff-0.11.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5da2e710a9641828e09aa98b92c9ebbc60518fdf3921241326ca3e8f8e55b8b", size = 10312937 }, + { url = "https://files.pythonhosted.org/packages/cf/4f/0e53fe5e500b65934500949361e3cd290c5ba60f0324ed59d15f46479c06/ruff-0.11.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ef39f19cb8ec98cbc762344921e216f3857a06c47412030374fffd413fb8fd3a", size = 9936683 }, + { url = "https://files.pythonhosted.org/packages/04/a8/8183c4da6d35794ae7f76f96261ef5960853cd3f899c2671961f97a27d8e/ruff-0.11.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b2a7cedf47244f431fd11aa5a7e2806dda2e0c365873bda7834e8f7d785ae159", size = 10950217 }, + { url = "https://files.pythonhosted.org/packages/26/88/9b85a5a8af21e46a0639b107fcf9bfc31da4f1d263f2fc7fbe7199b47f0a/ruff-0.11.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:81be52e7519f3d1a0beadcf8e974715b2dfc808ae8ec729ecfc79bddf8dbb783", size = 11404521 }, + { url = "https://files.pythonhosted.org/packages/fc/52/047f35d3b20fd1ae9ccfe28791ef0f3ca0ef0b3e6c1a58badd97d450131b/ruff-0.11.5-py3-none-win32.whl", hash = "sha256:e268da7b40f56e3eca571508a7e567e794f9bfcc0f412c4b607931d3af9c4afe", size = 10320697 }, + { url = "https://files.pythonhosted.org/packages/b9/fe/00c78010e3332a6e92762424cf4c1919065707e962232797d0b57fd8267e/ruff-0.11.5-py3-none-win_amd64.whl", hash = "sha256:6c6dc38af3cfe2863213ea25b6dc616d679205732dc0fb673356c2d69608f800", size = 11378665 }, + { url = "https://files.pythonhosted.org/packages/43/7c/c83fe5cbb70ff017612ff36654edfebec4b1ef79b558b8e5fd933bab836b/ruff-0.11.5-py3-none-win_arm64.whl", hash = "sha256:67e241b4314f4eacf14a601d586026a962f4002a475aa702c69980a38087aa4e", size = 10460287 }, ] [[package]] @@ -737,34 +743,34 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.12.2" +version = "4.13.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 }, ] [[package]] name = "uv" -version = "0.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/31/8f354a0b1df7ef4cb42da118dfae046d49f2c57ae427eb948a48a236c37d/uv-0.6.3.tar.gz", hash = "sha256:73587a192f2ebb8a25431d01037fe19f713fa99ff3b9fdf6e7a121131c6c5649", size = 3081857 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/c2/5a4138f1c615c7702943ce94155349943b5813e51faa38b6876a2ab86033/uv-0.6.3-py3-none-linux_armv6l.whl", hash = "sha256:facfec798eaddd07615b3a52973e38f2c8862ceb1bc685a5091891cd6c0c2a21", size = 15524019 }, - { url = "https://files.pythonhosted.org/packages/02/1d/abf01aa5e02b0a066f77b69a4f2f771c2ccd5424cd553e218afb026c65b9/uv-0.6.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b261895497f3c55a8a8917db0a1daeba1a9988ba487b068198d6cc4e8c13e769", size = 15537243 }, - { url = "https://files.pythonhosted.org/packages/ea/ac/4c1d5e04868051874dce74333fbe98e1f61e40a1522a9258a998775f2fab/uv-0.6.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:08e3f71a39c76c5b9ab63f9341b433a4ab8a1cc4e29d34ce81bd3b6f5bd642d8", size = 14450283 }, - { url = "https://files.pythonhosted.org/packages/00/8b/6cdb9a8cb4a5579d8b22d632e98d01f7c3695066ce1a2e33036edba2413a/uv-0.6.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:ebd4d1012c5043fe507f1f4477e7a54ec81e939e2a6e0229f23abb242f1622f5", size = 14909401 }, - { url = "https://files.pythonhosted.org/packages/51/8e/4d8c31250c7440a4c3704e81dab39f7f75db046e8b23f5322c3e47549557/uv-0.6.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f63b659a5ccbbd8c0ca5200c83ada6d19e73c0f1cafb8f4d9a7ef32544beb06d", size = 15245520 }, - { url = "https://files.pythonhosted.org/packages/4b/29/52976b3f7a79e4293763823e59d4de3b77506a1b9d298df0285be4879026/uv-0.6.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c23948f242a6bcbd274fa18387a608a52b21a3dfed18d324641964e305c348e9", size = 15890146 }, - { url = "https://files.pythonhosted.org/packages/54/38/a3c37aaf02b890d908edfec32e7a9b86e0df819df6443837929e40ac8d7e/uv-0.6.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0445ce49229001cec0a0b1240c6135e2252a3b8017ae878b0559411688a3e12a", size = 16817703 }, - { url = "https://files.pythonhosted.org/packages/df/0b/cd75c692266eb1cdea6764f9fb14d88babfa8d8433c414ac18623777760d/uv-0.6.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:95ab9e9194046f4fb50daec6293e471fc18b6e1d350dba4f5328d0f19f6ec183", size = 16509829 }, - { url = "https://files.pythonhosted.org/packages/1c/5c/35747d595bf13f5b495a29ec9bb6212fd2fad7d8c32324a7faaeb6a643d0/uv-0.6.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af417925d7af00be949ebcab1bf187540bea235e9454aa2193ffae5b7ecc75cf", size = 20477063 }, - { url = "https://files.pythonhosted.org/packages/23/c7/4ea3d3f23d24240c54deee0248766c320163eef8b0117310f0be168fe0f0/uv-0.6.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed2d4e3c6e041bc8b55f931a58d758220e46e828b983967fbb318a117d879351", size = 16190208 }, - { url = "https://files.pythonhosted.org/packages/83/f2/96d4981c3490fabc5ba787703951124969f5b6dc8e3166543e7534de2dea/uv-0.6.3-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:a936275590f3091b05c03ad3ce69e2f8a4c964e80ae44ce0cf13cc3b412352f1", size = 15145146 }, - { url = "https://files.pythonhosted.org/packages/2b/62/1be7fb8b97fd057460b733bbdf30e71e771dcfbfab27b7db552fa4e219e6/uv-0.6.3-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:e842e96b941832cd95cb2fce90c5626b33e477773f425005e9237f8fd9ef5696", size = 15245907 }, - { url = "https://files.pythonhosted.org/packages/e0/1b/5849046e11f8154567b235fc8097ebb6a0d6416b3ce317300d9b06470481/uv-0.6.3-py3-none-musllinux_1_1_i686.whl", hash = "sha256:cd51af332fb0f6362cc44e4cca22c2d12c31dd52352c6259cae0e3570ce79da4", size = 15504955 }, - { url = "https://files.pythonhosted.org/packages/ec/46/d4fa9bd06f84bb83e452f3f201b058cd13969cb979402ff000c2e4c77a1e/uv-0.6.3-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:328677a74c7d998b654e4bfd50ba4347d0f3deed85284dbd041004a184353806", size = 16317436 }, - { url = "https://files.pythonhosted.org/packages/0b/d9/f93e4522cf1de51ff1a985ead75df85523cd1b689128b1b033c9e31204b8/uv-0.6.3-py3-none-win32.whl", hash = "sha256:dc2d965481bba716a0cf9d0f81896a70c341a854f0e4273f1887f22e52e5c9fb", size = 15545377 }, - { url = "https://files.pythonhosted.org/packages/91/ea/27dd790ec0d1f8c4ced06e27a409522bd157ed295a1140b3fb6cac3cd39a/uv-0.6.3-py3-none-win_amd64.whl", hash = "sha256:8fc19471fd4cfde1b31a47c239591d7c6dc0a31213f206d3953c528f9f3b406c", size = 16860609 }, - { url = "https://files.pythonhosted.org/packages/97/0f/01e48493264d75cfac6c953809e11c8356c77fb6be32dfce831bcf481ab2/uv-0.6.3-py3-none-win_arm64.whl", hash = "sha256:94a9d59c05f22829388e51a62a9cfddef4000a112e1c561bb5bd5761d4d672f1", size = 15697009 }, +version = "0.6.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/eb/07bc000a3c05372448b63c45da98630c532ec4e059d848488c3e774d017a/uv-0.6.14.tar.gz", hash = "sha256:a117466f307d164a74444949cc94ec4328ec880fb489cbaa7df324dab14c5c98", size = 3134567 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/bf/3e87dec7728b249458967f39a301376cb776e559c90261c1dac963686dc3/uv-0.6.14-py3-none-linux_armv6l.whl", hash = "sha256:c775e5d7a80ff43cb88856bbdcd838918d5ac3dc362414317e6bbaeb615fff98", size = 16228143 }, + { url = "https://files.pythonhosted.org/packages/24/b2/111e1ea40453d93c849f36a67397b51d9b458e6e598c3629ffe76d11b490/uv-0.6.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2578f6f8cdbcc036ffad1043f9f66ade3ac0babf29def6abd9eefd4a7c6621cb", size = 16273279 }, + { url = "https://files.pythonhosted.org/packages/72/89/e7fc8a047f08234cc26d1e37e5f573887744205d087f8e8e6f3d0feb04ce/uv-0.6.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9fc8fe58871b4fe02a863b05b8b1b25ef1b6c60d4d224e85338f5c2be0ab4f0e", size = 15115451 }, + { url = "https://files.pythonhosted.org/packages/20/1e/72ac3d1e0805d3b49b0a4de46483489ea1989827440f42b0cfb444cdc67f/uv-0.6.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:2fb2cd7f6aae21b81474b0051d30e7ed939a9a71714948c47f58b0e7acdd2a80", size = 15540456 }, + { url = "https://files.pythonhosted.org/packages/fd/47/5aeb7fb80c673bc28ccf3ab99e376b1cd92eac41af6b9b48c0e38b114c54/uv-0.6.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d6ca3f99c1a6c1c430ae8f451133fb4e8c3a22f661c257425402a5d9430bb797", size = 15979820 }, + { url = "https://files.pythonhosted.org/packages/1f/44/c3ad856473f2ef5f22c865a73a0a37ee82d11fcca78ae82f5ac895a7023a/uv-0.6.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed41877b679e0a1af9ab65427d829b87a81b499017e59c70756d4ba02ca43fcb", size = 16650494 }, + { url = "https://files.pythonhosted.org/packages/7a/f6/8a1245530c282d470909db78cf56831693c58b90d9b819e35aa2d85fbbe8/uv-0.6.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:fe9b4361b1c8055301b715fdd94d94eb512053dc4545fec40d3fe3657f655987", size = 17505028 }, + { url = "https://files.pythonhosted.org/packages/a5/70/0806268440651e2ad1b3542af42b800e20bb7e43050a9ca78f3d1eb4c660/uv-0.6.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:998b67bb1cebbe044fc2c5cb251c29cffc56f62a6d55719d6f4e960461d6edad", size = 17245854 }, + { url = "https://files.pythonhosted.org/packages/2a/3a/0da9780868626466d8c4977fb02d1b0daa80e6f7504d7b662cae3fb4af3d/uv-0.6.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d433925db6e2ef46047b68962d136ff2ef17a7b5609168615f19e60674232c9", size = 21584756 }, + { url = "https://files.pythonhosted.org/packages/eb/fd/21a82b78173be1a2ea20f4f55154e7252bd80d21ed60b9bbbc0e2047b8d0/uv-0.6.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36aaeb00a70a10f748e16c7a1fc410862e2ba905806e7e9dfbc3e64596309404", size = 16878847 }, + { url = "https://files.pythonhosted.org/packages/6c/9a/7c84650ae9fb801ecc848d49dcba201243989d9234fe3ec4a4e935ff21c0/uv-0.6.14-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:11779beb3bd1f92814bc8d8cd350d5228e8f9198cca2f52138b53030a4061d93", size = 15810089 }, + { url = "https://files.pythonhosted.org/packages/0b/b3/efcbd3a2d298801109b24feee655bb80fe4178aa6bf68e49664c48b342b2/uv-0.6.14-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:bf1ec103cf9a0850f03935dc6a93cacc680fa2c90c3b41cfc10da311afab8f5b", size = 15962056 }, + { url = "https://files.pythonhosted.org/packages/3f/53/c92c894cb34e9578c2e6dc195bcd4eb0a140dd57c96a60207d847521a902/uv-0.6.14-py3-none-musllinux_1_1_i686.whl", hash = "sha256:955e36c98a438a249e178988d4f13b1bb831eb57264d73c459f171b5afd7b023", size = 16255226 }, + { url = "https://files.pythonhosted.org/packages/df/eb/38bc37856691d53008bf094d03d9e7ab0c2927523a3901c83e152e7c9915/uv-0.6.14-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:2d534e7dc1299c8b53eb7b4c7575e4f0933673ea8b1275d3f3022f5670e311db", size = 17005225 }, + { url = "https://files.pythonhosted.org/packages/d8/fe/087d5193603e16bc5f67556d94cf8fa8634785c5863cccdec825f14e9a4c/uv-0.6.14-py3-none-win32.whl", hash = "sha256:7cdf3c8d927b07d4eaffc44809eb57523d449705f10dabbdd6f34f7bdfc7d5fe", size = 16131231 }, + { url = "https://files.pythonhosted.org/packages/40/17/33c5c1503c35c874932d4a21ec10a55051e3695dba12b7de700bcfad0cca/uv-0.6.14-py3-none-win_amd64.whl", hash = "sha256:012f46bef6909209c4a6749e4019eb755ba762d37d7ceaaf76da9cb4b7f771e9", size = 17628508 }, + { url = "https://files.pythonhosted.org/packages/77/09/163062d439ddc0d89e527ae0e631abf1f7781b183442d8823c48af368f5d/uv-0.6.14-py3-none-win_arm64.whl", hash = "sha256:7465081b4d0b213d0055ccb48de7fe546b5cf0853c6d3601115760760634f6d8", size = 16387232 }, ] From 730d093c8c44d129a9df7e9c4867e9c569825f25 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Wed, 7 May 2025 14:09:26 -0500 Subject: [PATCH 56/58] Update dependencies --- .pre-commit-config.yaml | 6 +- pyproject.toml | 4 +- uv.lock | 308 ++++++++++++++++++++-------------------- zizmor.yml | 6 + 4 files changed, 168 insertions(+), 156 deletions(-) create mode 100644 zizmor.yml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8175a35..bb7ecec 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.5 + rev: v0.11.8 hooks: - id: ruff types: [file] @@ -41,10 +41,10 @@ repos: additional_dependencies: - tomli - repo: https://github.com/adhtruong/mirrors-typos - rev: v1.31.1 + rev: v1.32.0 hooks: - id: typos - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.5.2 + rev: v1.6.0 hooks: - id: zizmor diff --git a/pyproject.toml b/pyproject.toml index 5ffacb8..c15c228 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,12 +37,12 @@ keywords = [ ] dependencies = [ "exceptiongroup>=1.2.2; python_version < '3.11'", - "libcomponent~=0.0.3", + "libcomponent~=0.0.4", "mypy_extensions>=1.0.0", "numpy~=2.1.3", "orjson>=3.10,<4", "pygame~=2.6.0", - "trio~=0.29.0", + "trio~=0.30.0", ] [tool.setuptools.dynamic] diff --git a/uv.lock b/uv.lock index 3d45437..caf9687 100644 --- a/uv.lock +++ b/uv.lock @@ -45,7 +45,7 @@ requires-dist = [ { name = "codespell", marker = "extra == 'tools'", specifier = ">=2.3.0" }, { name = "coverage", marker = "extra == 'tests'", specifier = ">=7.2.5" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'", specifier = ">=1.2.2" }, - { name = "libcomponent", specifier = "~=0.0.3" }, + { name = "libcomponent", specifier = "~=0.0.4" }, { name = "mypy", marker = "extra == 'tests'", specifier = ">=1.14.1" }, { name = "mypy-extensions", specifier = ">=1.0.0" }, { name = "numpy", specifier = "~=2.1.3" }, @@ -55,7 +55,7 @@ requires-dist = [ { name = "pytest-cov", marker = "extra == 'tests'", specifier = ">=6.0.0" }, { name = "pytest-trio", marker = "extra == 'tests'", specifier = ">=0.8.0" }, { name = "ruff", marker = "extra == 'tools'", specifier = ">=0.9.2" }, - { name = "trio", specifier = "~=0.29.0" }, + { name = "trio", specifier = "~=0.30.0" }, { name = "uv", marker = "extra == 'tests'", specifier = ">=0.5.21" }, ] provides-extras = ["tests", "tools"] @@ -248,47 +248,49 @@ toml = [ [[package]] name = "cryptography" -version = "44.0.2" +version = "44.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cd/25/4ce80c78963834b8a9fd1cc1266be5ed8d1840785c0f2e1b73b8d128d505/cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0", size = 710807 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/92/ef/83e632cfa801b221570c5f58c0369db6fa6cef7d9ff859feab1aae1a8a0f/cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7", size = 6676361 }, - { url = "https://files.pythonhosted.org/packages/30/ec/7ea7c1e4c8fc8329506b46c6c4a52e2f20318425d48e0fe597977c71dbce/cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1", size = 3952350 }, - { url = "https://files.pythonhosted.org/packages/27/61/72e3afdb3c5ac510330feba4fc1faa0fe62e070592d6ad00c40bb69165e5/cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb", size = 4166572 }, - { url = "https://files.pythonhosted.org/packages/26/e4/ba680f0b35ed4a07d87f9e98f3ebccb05091f3bf6b5a478b943253b3bbd5/cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843", size = 3958124 }, - { url = "https://files.pythonhosted.org/packages/9c/e8/44ae3e68c8b6d1cbc59040288056df2ad7f7f03bbcaca6b503c737ab8e73/cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5", size = 3678122 }, - { url = "https://files.pythonhosted.org/packages/27/7b/664ea5e0d1eab511a10e480baf1c5d3e681c7d91718f60e149cec09edf01/cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c", size = 4191831 }, - { url = "https://files.pythonhosted.org/packages/2a/07/79554a9c40eb11345e1861f46f845fa71c9e25bf66d132e123d9feb8e7f9/cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a", size = 3960583 }, - { url = "https://files.pythonhosted.org/packages/bb/6d/858e356a49a4f0b591bd6789d821427de18432212e137290b6d8a817e9bf/cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308", size = 4191753 }, - { url = "https://files.pythonhosted.org/packages/b2/80/62df41ba4916067fa6b125aa8c14d7e9181773f0d5d0bd4dcef580d8b7c6/cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688", size = 4079550 }, - { url = "https://files.pythonhosted.org/packages/f3/cd/2558cc08f7b1bb40683f99ff4327f8dcfc7de3affc669e9065e14824511b/cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7", size = 4298367 }, - { url = "https://files.pythonhosted.org/packages/71/59/94ccc74788945bc3bd4cf355d19867e8057ff5fdbcac781b1ff95b700fb1/cryptography-44.0.2-cp37-abi3-win32.whl", hash = "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79", size = 2772843 }, - { url = "https://files.pythonhosted.org/packages/ca/2c/0d0bbaf61ba05acb32f0841853cfa33ebb7a9ab3d9ed8bb004bd39f2da6a/cryptography-44.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa", size = 3209057 }, - { url = "https://files.pythonhosted.org/packages/9e/be/7a26142e6d0f7683d8a382dd963745e65db895a79a280a30525ec92be890/cryptography-44.0.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3", size = 6677789 }, - { url = "https://files.pythonhosted.org/packages/06/88/638865be7198a84a7713950b1db7343391c6066a20e614f8fa286eb178ed/cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639", size = 3951919 }, - { url = "https://files.pythonhosted.org/packages/d7/fc/99fe639bcdf58561dfad1faa8a7369d1dc13f20acd78371bb97a01613585/cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd", size = 4167812 }, - { url = "https://files.pythonhosted.org/packages/53/7b/aafe60210ec93d5d7f552592a28192e51d3c6b6be449e7fd0a91399b5d07/cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181", size = 3958571 }, - { url = "https://files.pythonhosted.org/packages/16/32/051f7ce79ad5a6ef5e26a92b37f172ee2d6e1cce09931646eef8de1e9827/cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea", size = 3679832 }, - { url = "https://files.pythonhosted.org/packages/78/2b/999b2a1e1ba2206f2d3bca267d68f350beb2b048a41ea827e08ce7260098/cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699", size = 4193719 }, - { url = "https://files.pythonhosted.org/packages/72/97/430e56e39a1356e8e8f10f723211a0e256e11895ef1a135f30d7d40f2540/cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9", size = 3960852 }, - { url = "https://files.pythonhosted.org/packages/89/33/c1cf182c152e1d262cac56850939530c05ca6c8d149aa0dcee490b417e99/cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23", size = 4193906 }, - { url = "https://files.pythonhosted.org/packages/e1/99/87cf26d4f125380dc674233971069bc28d19b07f7755b29861570e513650/cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922", size = 4081572 }, - { url = "https://files.pythonhosted.org/packages/b3/9f/6a3e0391957cc0c5f84aef9fbdd763035f2b52e998a53f99345e3ac69312/cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4", size = 4298631 }, - { url = "https://files.pythonhosted.org/packages/e2/a5/5bc097adb4b6d22a24dea53c51f37e480aaec3465285c253098642696423/cryptography-44.0.2-cp39-abi3-win32.whl", hash = "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5", size = 2773792 }, - { url = "https://files.pythonhosted.org/packages/33/cf/1f7649b8b9a3543e042d3f348e398a061923ac05b507f3f4d95f11938aa9/cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6", size = 3210957 }, - { url = "https://files.pythonhosted.org/packages/99/10/173be140714d2ebaea8b641ff801cbcb3ef23101a2981cbf08057876f89e/cryptography-44.0.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:af4ff3e388f2fa7bff9f7f2b31b87d5651c45731d3e8cfa0944be43dff5cfbdb", size = 3396886 }, - { url = "https://files.pythonhosted.org/packages/2f/b4/424ea2d0fce08c24ede307cead3409ecbfc2f566725d4701b9754c0a1174/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0529b1d5a0105dd3731fa65680b45ce49da4d8115ea76e9da77a875396727b41", size = 3892387 }, - { url = "https://files.pythonhosted.org/packages/28/20/8eaa1a4f7c68a1cb15019dbaad59c812d4df4fac6fd5f7b0b9c5177f1edd/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7ca25849404be2f8e4b3c59483d9d3c51298a22c1c61a0e84415104dacaf5562", size = 4109922 }, - { url = "https://files.pythonhosted.org/packages/11/25/5ed9a17d532c32b3bc81cc294d21a36c772d053981c22bd678396bc4ae30/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:268e4e9b177c76d569e8a145a6939eca9a5fec658c932348598818acf31ae9a5", size = 3895715 }, - { url = "https://files.pythonhosted.org/packages/63/31/2aac03b19c6329b62c45ba4e091f9de0b8f687e1b0cd84f101401bece343/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9eb9d22b0a5d8fd9925a7764a054dca914000607dff201a24c791ff5c799e1fa", size = 4109876 }, - { url = "https://files.pythonhosted.org/packages/99/ec/6e560908349843718db1a782673f36852952d52a55ab14e46c42c8a7690a/cryptography-44.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2bf7bf75f7df9715f810d1b038870309342bff3069c5bd8c6b96128cb158668d", size = 3131719 }, - { url = "https://files.pythonhosted.org/packages/d6/d7/f30e75a6aa7d0f65031886fa4a1485c2fbfe25a1896953920f6a9cfe2d3b/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:909c97ab43a9c0c0b0ada7a1281430e4e5ec0458e6d9244c0e821bbf152f061d", size = 3887513 }, - { url = "https://files.pythonhosted.org/packages/9c/b4/7a494ce1032323ca9db9a3661894c66e0d7142ad2079a4249303402d8c71/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96e7a5e9d6e71f9f4fca8eebfd603f8e86c5225bb18eb621b2c1e50b290a9471", size = 4107432 }, - { url = "https://files.pythonhosted.org/packages/45/f8/6b3ec0bc56123b344a8d2b3264a325646d2dcdbdd9848b5e6f3d37db90b3/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d1b3031093a366ac767b3feb8bcddb596671b3aaff82d4050f984da0c248b615", size = 3891421 }, - { url = "https://files.pythonhosted.org/packages/57/ff/f3b4b2d007c2a646b0f69440ab06224f9cf37a977a72cdb7b50632174e8a/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:04abd71114848aa25edb28e225ab5f268096f44cf0127f3d36975bdf1bdf3390", size = 4107081 }, +sdist = { url = "https://files.pythonhosted.org/packages/53/d6/1411ab4d6108ab167d06254c5be517681f1e331f90edf1379895bcb87020/cryptography-44.0.3.tar.gz", hash = "sha256:fe19d8bc5536a91a24a8133328880a41831b6c5df54599a8417b62fe015d3053", size = 711096 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/53/c776d80e9d26441bb3868457909b4e74dd9ccabd182e10b2b0ae7a07e265/cryptography-44.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:962bc30480a08d133e631e8dfd4783ab71cc9e33d5d7c1e192f0b7c06397bb88", size = 6670281 }, + { url = "https://files.pythonhosted.org/packages/6a/06/af2cf8d56ef87c77319e9086601bef621bedf40f6f59069e1b6d1ec498c5/cryptography-44.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffc61e8f3bf5b60346d89cd3d37231019c17a081208dfbbd6e1605ba03fa137", size = 3959305 }, + { url = "https://files.pythonhosted.org/packages/ae/01/80de3bec64627207d030f47bf3536889efee8913cd363e78ca9a09b13c8e/cryptography-44.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58968d331425a6f9eedcee087f77fd3c927c88f55368f43ff7e0a19891f2642c", size = 4171040 }, + { url = "https://files.pythonhosted.org/packages/bd/48/bb16b7541d207a19d9ae8b541c70037a05e473ddc72ccb1386524d4f023c/cryptography-44.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e28d62e59a4dbd1d22e747f57d4f00c459af22181f0b2f787ea83f5a876d7c76", size = 3963411 }, + { url = "https://files.pythonhosted.org/packages/42/b2/7d31f2af5591d217d71d37d044ef5412945a8a8e98d5a2a8ae4fd9cd4489/cryptography-44.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af653022a0c25ef2e3ffb2c673a50e5a0d02fecc41608f4954176f1933b12359", size = 3689263 }, + { url = "https://files.pythonhosted.org/packages/25/50/c0dfb9d87ae88ccc01aad8eb93e23cfbcea6a6a106a9b63a7b14c1f93c75/cryptography-44.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:157f1f3b8d941c2bd8f3ffee0af9b049c9665c39d3da9db2dc338feca5e98a43", size = 4196198 }, + { url = "https://files.pythonhosted.org/packages/66/c9/55c6b8794a74da652690c898cb43906310a3e4e4f6ee0b5f8b3b3e70c441/cryptography-44.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:c6cd67722619e4d55fdb42ead64ed8843d64638e9c07f4011163e46bc512cf01", size = 3966502 }, + { url = "https://files.pythonhosted.org/packages/b6/f7/7cb5488c682ca59a02a32ec5f975074084db4c983f849d47b7b67cc8697a/cryptography-44.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b424563394c369a804ecbee9b06dfb34997f19d00b3518e39f83a5642618397d", size = 4196173 }, + { url = "https://files.pythonhosted.org/packages/d2/0b/2f789a8403ae089b0b121f8f54f4a3e5228df756e2146efdf4a09a3d5083/cryptography-44.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c91fc8e8fd78af553f98bc7f2a1d8db977334e4eea302a4bfd75b9461c2d8904", size = 4087713 }, + { url = "https://files.pythonhosted.org/packages/1d/aa/330c13655f1af398fc154089295cf259252f0ba5df93b4bc9d9c7d7f843e/cryptography-44.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:25cd194c39fa5a0aa4169125ee27d1172097857b27109a45fadc59653ec06f44", size = 4299064 }, + { url = "https://files.pythonhosted.org/packages/10/a8/8c540a421b44fd267a7d58a1fd5f072a552d72204a3f08194f98889de76d/cryptography-44.0.3-cp37-abi3-win32.whl", hash = "sha256:3be3f649d91cb182c3a6bd336de8b61a0a71965bd13d1a04a0e15b39c3d5809d", size = 2773887 }, + { url = "https://files.pythonhosted.org/packages/b9/0d/c4b1657c39ead18d76bbd122da86bd95bdc4095413460d09544000a17d56/cryptography-44.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:3883076d5c4cc56dbef0b898a74eb6992fdac29a7b9013870b34efe4ddb39a0d", size = 3209737 }, + { url = "https://files.pythonhosted.org/packages/34/a3/ad08e0bcc34ad436013458d7528e83ac29910943cea42ad7dd4141a27bbb/cryptography-44.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:5639c2b16764c6f76eedf722dbad9a0914960d3489c0cc38694ddf9464f1bb2f", size = 6673501 }, + { url = "https://files.pythonhosted.org/packages/b1/f0/7491d44bba8d28b464a5bc8cc709f25a51e3eac54c0a4444cf2473a57c37/cryptography-44.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ffef566ac88f75967d7abd852ed5f182da252d23fac11b4766da3957766759", size = 3960307 }, + { url = "https://files.pythonhosted.org/packages/f7/c8/e5c5d0e1364d3346a5747cdcd7ecbb23ca87e6dea4f942a44e88be349f06/cryptography-44.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:192ed30fac1728f7587c6f4613c29c584abdc565d7417c13904708db10206645", size = 4170876 }, + { url = "https://files.pythonhosted.org/packages/73/96/025cb26fc351d8c7d3a1c44e20cf9a01e9f7cf740353c9c7a17072e4b264/cryptography-44.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7d5fe7195c27c32a64955740b949070f21cba664604291c298518d2e255931d2", size = 3964127 }, + { url = "https://files.pythonhosted.org/packages/01/44/eb6522db7d9f84e8833ba3bf63313f8e257729cf3a8917379473fcfd6601/cryptography-44.0.3-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3f07943aa4d7dad689e3bb1638ddc4944cc5e0921e3c227486daae0e31a05e54", size = 3689164 }, + { url = "https://files.pythonhosted.org/packages/68/fb/d61a4defd0d6cee20b1b8a1ea8f5e25007e26aeb413ca53835f0cae2bcd1/cryptography-44.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb90f60e03d563ca2445099edf605c16ed1d5b15182d21831f58460c48bffb93", size = 4198081 }, + { url = "https://files.pythonhosted.org/packages/1b/50/457f6911d36432a8811c3ab8bd5a6090e8d18ce655c22820994913dd06ea/cryptography-44.0.3-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ab0b005721cc0039e885ac3503825661bd9810b15d4f374e473f8c89b7d5460c", size = 3967716 }, + { url = "https://files.pythonhosted.org/packages/35/6e/dca39d553075980ccb631955c47b93d87d27f3596da8d48b1ae81463d915/cryptography-44.0.3-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3bb0847e6363c037df8f6ede57d88eaf3410ca2267fb12275370a76f85786a6f", size = 4197398 }, + { url = "https://files.pythonhosted.org/packages/9b/9d/d1f2fe681eabc682067c66a74addd46c887ebacf39038ba01f8860338d3d/cryptography-44.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0cc66c74c797e1db750aaa842ad5b8b78e14805a9b5d1348dc603612d3e3ff5", size = 4087900 }, + { url = "https://files.pythonhosted.org/packages/c4/f5/3599e48c5464580b73b236aafb20973b953cd2e7b44c7c2533de1d888446/cryptography-44.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6866df152b581f9429020320e5eb9794c8780e90f7ccb021940d7f50ee00ae0b", size = 4301067 }, + { url = "https://files.pythonhosted.org/packages/a7/6c/d2c48c8137eb39d0c193274db5c04a75dab20d2f7c3f81a7dcc3a8897701/cryptography-44.0.3-cp39-abi3-win32.whl", hash = "sha256:c138abae3a12a94c75c10499f1cbae81294a6f983b3af066390adee73f433028", size = 2775467 }, + { url = "https://files.pythonhosted.org/packages/c9/ad/51f212198681ea7b0deaaf8846ee10af99fba4e894f67b353524eab2bbe5/cryptography-44.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:5d186f32e52e66994dce4f766884bcb9c68b8da62d61d9d215bfe5fb56d21334", size = 3210375 }, + { url = "https://files.pythonhosted.org/packages/7f/10/abcf7418536df1eaba70e2cfc5c8a0ab07aa7aa02a5cbc6a78b9d8b4f121/cryptography-44.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cad399780053fb383dc067475135e41c9fe7d901a97dd5d9c5dfb5611afc0d7d", size = 3393192 }, + { url = "https://files.pythonhosted.org/packages/06/59/ecb3ef380f5891978f92a7f9120e2852b1df6f0a849c277b8ea45b865db2/cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:21a83f6f35b9cc656d71b5de8d519f566df01e660ac2578805ab245ffd8523f8", size = 3898419 }, + { url = "https://files.pythonhosted.org/packages/bb/d0/35e2313dbb38cf793aa242182ad5bc5ef5c8fd4e5dbdc380b936c7d51169/cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fc3c9babc1e1faefd62704bb46a69f359a9819eb0292e40df3fb6e3574715cd4", size = 4117892 }, + { url = "https://files.pythonhosted.org/packages/dc/c8/31fb6e33b56c2c2100d76de3fd820afaa9d4d0b6aea1ccaf9aaf35dc7ce3/cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:e909df4053064a97f1e6565153ff8bb389af12c5c8d29c343308760890560aff", size = 3900855 }, + { url = "https://files.pythonhosted.org/packages/43/2a/08cc2ec19e77f2a3cfa2337b429676406d4bb78ddd130a05c458e7b91d73/cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:dad80b45c22e05b259e33ddd458e9e2ba099c86ccf4e88db7bbab4b747b18d06", size = 4117619 }, + { url = "https://files.pythonhosted.org/packages/02/68/fc3d3f84022a75f2ac4b1a1c0e5d6a0c2ea259e14cd4aae3e0e68e56483c/cryptography-44.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:479d92908277bed6e1a1c69b277734a7771c2b78633c224445b5c60a9f4bc1d9", size = 3136570 }, + { url = "https://files.pythonhosted.org/packages/8d/4b/c11ad0b6c061902de5223892d680e89c06c7c4d606305eb8de56c5427ae6/cryptography-44.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:896530bc9107b226f265effa7ef3f21270f18a2026bc09fed1ebd7b66ddf6375", size = 3390230 }, + { url = "https://files.pythonhosted.org/packages/58/11/0a6bf45d53b9b2290ea3cec30e78b78e6ca29dc101e2e296872a0ffe1335/cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9b4d4a5dbee05a2c390bf212e78b99434efec37b17a4bff42f50285c5c8c9647", size = 3895216 }, + { url = "https://files.pythonhosted.org/packages/0a/27/b28cdeb7270e957f0077a2c2bfad1b38f72f1f6d699679f97b816ca33642/cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02f55fb4f8b79c1221b0961488eaae21015b69b210e18c386b69de182ebb1259", size = 4115044 }, + { url = "https://files.pythonhosted.org/packages/35/b0/ec4082d3793f03cb248881fecefc26015813199b88f33e3e990a43f79835/cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dd3db61b8fe5be220eee484a17233287d0be6932d056cf5738225b9c05ef4fff", size = 3898034 }, + { url = "https://files.pythonhosted.org/packages/0b/7f/adf62e0b8e8d04d50c9a91282a57628c00c54d4ae75e2b02a223bd1f2613/cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:978631ec51a6bbc0b7e58f23b68a8ce9e5f09721940933e9c217068388789fe5", size = 4114449 }, + { url = "https://files.pythonhosted.org/packages/87/62/d69eb4a8ee231f4bf733a92caf9da13f1c81a44e874b1d4080c25ecbb723/cryptography-44.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:5d20cc348cca3a8aa7312f42ab953a56e15323800ca3ab0706b8cd452a3a056c", size = 3134369 }, ] [[package]] @@ -320,7 +322,7 @@ wheels = [ [[package]] name = "libcomponent" -version = "0.0.3" +version = "0.0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, @@ -328,9 +330,9 @@ dependencies = [ { name = "trio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3c/98/edd63ce51271acda4352e70cb1a884915d2066ce733c70e02286b8c23fdb/libcomponent-0.0.3.tar.gz", hash = "sha256:83115c93e5ea51cd17b592ac7ccc25a78f1e4833d1efdc83ae93ca77cbee60da", size = 76329 } +sdist = { url = "https://files.pythonhosted.org/packages/9a/0a/5f3d65ed7bb368baa69c067a1ef478a994b7326c6747a45fc834e8443890/libcomponent-0.0.4.tar.gz", hash = "sha256:c1daa699d748be0e0132c4062e0527a89a6a8dff32c291efa9880c0715d49c1b", size = 76346 } wheels = [ - { url = "https://files.pythonhosted.org/packages/23/cc/89d6330283696a804d85568084493d2f01716098ae6548e009e0a2b5663b/libcomponent-0.0.3-py3-none-any.whl", hash = "sha256:5879f87058e1f903bf00f300675f011f4f4627350db72962eb70752acf2dbd92", size = 59324 }, + { url = "https://files.pythonhosted.org/packages/c4/ab/c804e77e769a14bace0ff7d536a2c2b9fb08289a0cab7f76840c92a270b6/libcomponent-0.0.4-py3-none-any.whl", hash = "sha256:c26edf821564da1400ede72d1414bc754cd693199d436e3cde7b46b9950d7587", size = 59380 }, ] [[package]] @@ -373,11 +375,11 @@ wheels = [ [[package]] name = "mypy-extensions" -version = "1.0.0" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 }, ] [[package]] @@ -444,64 +446,68 @@ wheels = [ [[package]] name = "orjson" -version = "3.10.16" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/c7/03913cc4332174071950acf5b0735463e3f63760c80585ef369270c2b372/orjson-3.10.16.tar.gz", hash = "sha256:d2aaa5c495e11d17b9b93205f5fa196737ee3202f000aaebf028dc9a73750f10", size = 5410415 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/a6/22cb9b03baf167bc2d659c9e74d7580147f36e6a155e633801badfd5a74d/orjson-3.10.16-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4cb473b8e79154fa778fb56d2d73763d977be3dcc140587e07dbc545bbfc38f8", size = 249179 }, - { url = "https://files.pythonhosted.org/packages/d7/ce/3e68cc33020a6ebd8f359b8628b69d2132cd84fea68155c33057e502ee51/orjson-3.10.16-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:622a8e85eeec1948690409a19ca1c7d9fd8ff116f4861d261e6ae2094fe59a00", size = 138510 }, - { url = "https://files.pythonhosted.org/packages/dc/12/63bee7764ce12052f7c1a1393ce7f26dc392c93081eb8754dd3dce9b7c6b/orjson-3.10.16-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c682d852d0ce77613993dc967e90e151899fe2d8e71c20e9be164080f468e370", size = 132373 }, - { url = "https://files.pythonhosted.org/packages/b3/d5/2998c2f319adcd572f2b03ba2083e8176863d1055d8d713683ddcf927b71/orjson-3.10.16-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c520ae736acd2e32df193bcff73491e64c936f3e44a2916b548da048a48b46b", size = 136774 }, - { url = "https://files.pythonhosted.org/packages/00/03/88c236ae307bd0604623204d4a835e15fbf9c75b8535c8f13ef45abd413f/orjson-3.10.16-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:134f87c76bfae00f2094d85cfab261b289b76d78c6da8a7a3b3c09d362fd1e06", size = 138030 }, - { url = "https://files.pythonhosted.org/packages/66/ba/3e256ddfeb364f98fd6ac65774844090d356158b2d1de8998db2bf984503/orjson-3.10.16-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b59afde79563e2cf37cfe62ee3b71c063fd5546c8e662d7fcfc2a3d5031a5c4c", size = 142677 }, - { url = "https://files.pythonhosted.org/packages/2c/71/73a1214bd27baa2ea5184fff4aa6193a114dfb0aa5663dad48fe63e8cd29/orjson-3.10.16-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:113602f8241daaff05d6fad25bd481d54c42d8d72ef4c831bb3ab682a54d9e15", size = 132798 }, - { url = "https://files.pythonhosted.org/packages/53/ac/0b2f41c0a1e8c095439d0fab3b33103cf41a39be8e6aa2c56298a6034259/orjson-3.10.16-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4fc0077d101f8fab4031e6554fc17b4c2ad8fdbc56ee64a727f3c95b379e31da", size = 135450 }, - { url = "https://files.pythonhosted.org/packages/d9/ca/7524c7b0bc815d426ca134dab54cad519802287b808a3846b047a5b2b7a3/orjson-3.10.16-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:9c6bf6ff180cd69e93f3f50380224218cfab79953a868ea3908430bcfaf9cb5e", size = 412356 }, - { url = "https://files.pythonhosted.org/packages/05/1d/3ae2367c255276bf16ff7e1b210dd0af18bc8da20c4e4295755fc7de1268/orjson-3.10.16-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5673eadfa952f95a7cd76418ff189df11b0a9c34b1995dff43a6fdbce5d63bf4", size = 152769 }, - { url = "https://files.pythonhosted.org/packages/d3/2d/8eb10b6b1d30bb69c35feb15e5ba5ac82466cf743d562e3e8047540efd2f/orjson-3.10.16-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5fe638a423d852b0ae1e1a79895851696cb0d9fa0946fdbfd5da5072d9bb9551", size = 137223 }, - { url = "https://files.pythonhosted.org/packages/47/42/f043717930cb2de5fbebe47f308f101bed9ec2b3580b1f99c8284b2f5fe8/orjson-3.10.16-cp310-cp310-win32.whl", hash = "sha256:33af58f479b3c6435ab8f8b57999874b4b40c804c7a36b5cc6b54d8f28e1d3dd", size = 141734 }, - { url = "https://files.pythonhosted.org/packages/67/99/795ad7282b425b9fddcfb8a31bded5dcf84dba78ecb1e7ae716e84e794da/orjson-3.10.16-cp310-cp310-win_amd64.whl", hash = "sha256:0338356b3f56d71293c583350af26f053017071836b07e064e92819ecf1aa055", size = 133779 }, - { url = "https://files.pythonhosted.org/packages/97/29/43f91a5512b5d2535594438eb41c5357865fd5e64dec745d90a588820c75/orjson-3.10.16-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:44fcbe1a1884f8bc9e2e863168b0f84230c3d634afe41c678637d2728ea8e739", size = 249180 }, - { url = "https://files.pythonhosted.org/packages/0c/36/2a72d55e266473c19a86d97b7363bb8bf558ab450f75205689a287d5ce61/orjson-3.10.16-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78177bf0a9d0192e0b34c3d78bcff7fe21d1b5d84aeb5ebdfe0dbe637b885225", size = 138510 }, - { url = "https://files.pythonhosted.org/packages/bb/ad/f86d6f55c1a68b57ff6ea7966bce5f4e5163f2e526ddb7db9fc3c2c8d1c4/orjson-3.10.16-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12824073a010a754bb27330cad21d6e9b98374f497f391b8707752b96f72e741", size = 132373 }, - { url = "https://files.pythonhosted.org/packages/5e/8b/d18f2711493a809f3082a88fda89342bc8e16767743b909cd3c34989fba3/orjson-3.10.16-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddd41007e56284e9867864aa2f29f3136bb1dd19a49ca43c0b4eda22a579cf53", size = 136773 }, - { url = "https://files.pythonhosted.org/packages/a1/dc/ce025f002f8e0749e3f057c4d773a4d4de32b7b4c1fc5a50b429e7532586/orjson-3.10.16-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0877c4d35de639645de83666458ca1f12560d9fa7aa9b25d8bb8f52f61627d14", size = 138029 }, - { url = "https://files.pythonhosted.org/packages/0e/1b/cf9df85852b91160029d9f26014230366a2b4deb8cc51fabe68e250a8c1a/orjson-3.10.16-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9a09a539e9cc3beead3e7107093b4ac176d015bec64f811afb5965fce077a03c", size = 142677 }, - { url = "https://files.pythonhosted.org/packages/92/18/5b1e1e995bffad49dc4311a0bdfd874bc6f135fd20f0e1f671adc2c9910e/orjson-3.10.16-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31b98bc9b40610fec971d9a4d67bb2ed02eec0a8ae35f8ccd2086320c28526ca", size = 132800 }, - { url = "https://files.pythonhosted.org/packages/d6/eb/467f25b580e942fcca1344adef40633b7f05ac44a65a63fc913f9a805d58/orjson-3.10.16-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0ce243f5a8739f3a18830bc62dc2e05b69a7545bafd3e3249f86668b2bcd8e50", size = 135451 }, - { url = "https://files.pythonhosted.org/packages/8d/4b/9d10888038975cb375982e9339d9495bac382d5c976c500b8d6f2c8e2e4e/orjson-3.10.16-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:64792c0025bae049b3074c6abe0cf06f23c8e9f5a445f4bab31dc5ca23dbf9e1", size = 412358 }, - { url = "https://files.pythonhosted.org/packages/3b/e2/cfbcfcc4fbe619e0ca9bdbbfccb2d62b540bbfe41e0ee77d44a628594f59/orjson-3.10.16-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ea53f7e68eec718b8e17e942f7ca56c6bd43562eb19db3f22d90d75e13f0431d", size = 152772 }, - { url = "https://files.pythonhosted.org/packages/b9/d6/627a1b00569be46173007c11dde3da4618c9bfe18409325b0e3e2a82fe29/orjson-3.10.16-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a741ba1a9488c92227711bde8c8c2b63d7d3816883268c808fbeada00400c164", size = 137225 }, - { url = "https://files.pythonhosted.org/packages/0a/7b/a73c67b505021af845b9f05c7c848793258ea141fa2058b52dd9b067c2b4/orjson-3.10.16-cp311-cp311-win32.whl", hash = "sha256:c7ed2c61bb8226384c3fdf1fb01c51b47b03e3f4536c985078cccc2fd19f1619", size = 141733 }, - { url = "https://files.pythonhosted.org/packages/f4/22/5e8217c48d68c0adbfb181e749d6a733761074e598b083c69a1383d18147/orjson-3.10.16-cp311-cp311-win_amd64.whl", hash = "sha256:cd67d8b3e0e56222a2e7b7f7da9031e30ecd1fe251c023340b9f12caca85ab60", size = 133784 }, - { url = "https://files.pythonhosted.org/packages/5d/15/67ce9d4c959c83f112542222ea3b9209c1d424231d71d74c4890ea0acd2b/orjson-3.10.16-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6d3444abbfa71ba21bb042caa4b062535b122248259fdb9deea567969140abca", size = 249325 }, - { url = "https://files.pythonhosted.org/packages/da/2c/1426b06f30a1b9ada74b6f512c1ddf9d2760f53f61cdb59efeb9ad342133/orjson-3.10.16-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:30245c08d818fdcaa48b7d5b81499b8cae09acabb216fe61ca619876b128e184", size = 133621 }, - { url = "https://files.pythonhosted.org/packages/9e/88/18d26130954bc73bee3be10f95371ea1dfb8679e0e2c46b0f6d8c6289402/orjson-3.10.16-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0ba1d0baa71bf7579a4ccdcf503e6f3098ef9542106a0eca82395898c8a500a", size = 138270 }, - { url = "https://files.pythonhosted.org/packages/4f/f9/6d8b64fcd58fae072e80ee7981be8ba0d7c26ace954e5cd1d027fc80518f/orjson-3.10.16-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb0beefa5ef3af8845f3a69ff2a4aa62529b5acec1cfe5f8a6b4141033fd46ef", size = 132346 }, - { url = "https://files.pythonhosted.org/packages/16/3f/2513fd5bc786f40cd12af569c23cae6381aeddbefeed2a98f0a666eb5d0d/orjson-3.10.16-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6daa0e1c9bf2e030e93c98394de94506f2a4d12e1e9dadd7c53d5e44d0f9628e", size = 136845 }, - { url = "https://files.pythonhosted.org/packages/6d/42/b0e7b36720f5ab722b48e8ccf06514d4f769358dd73c51abd8728ef58d0b/orjson-3.10.16-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9da9019afb21e02410ef600e56666652b73eb3e4d213a0ec919ff391a7dd52aa", size = 138078 }, - { url = "https://files.pythonhosted.org/packages/a3/a8/d220afb8a439604be74fc755dbc740bded5ed14745ca536b304ed32eb18a/orjson-3.10.16-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:daeb3a1ee17b69981d3aae30c3b4e786b0f8c9e6c71f2b48f1aef934f63f38f4", size = 142712 }, - { url = "https://files.pythonhosted.org/packages/8c/88/7e41e9883c00f84f92fe357a8371edae816d9d7ef39c67b5106960c20389/orjson-3.10.16-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fed80eaf0e20a31942ae5d0728849862446512769692474be5e6b73123a23b", size = 133136 }, - { url = "https://files.pythonhosted.org/packages/e9/ca/61116095307ad0be828ea26093febaf59e38596d84a9c8d765c3c5e4934f/orjson-3.10.16-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73390ed838f03764540a7bdc4071fe0123914c2cc02fb6abf35182d5fd1b7a42", size = 135258 }, - { url = "https://files.pythonhosted.org/packages/dc/1b/09493cf7d801505f094c9295f79c98c1e0af2ac01c7ed8d25b30fcb19ada/orjson-3.10.16-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a22bba012a0c94ec02a7768953020ab0d3e2b884760f859176343a36c01adf87", size = 412326 }, - { url = "https://files.pythonhosted.org/packages/ea/02/125d7bbd7f7a500190ddc8ae5d2d3c39d87ed3ed28f5b37cfe76962c678d/orjson-3.10.16-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5385bbfdbc90ff5b2635b7e6bebf259652db00a92b5e3c45b616df75b9058e88", size = 152800 }, - { url = "https://files.pythonhosted.org/packages/f9/09/7658a9e3e793d5b3b00598023e0fb6935d0e7bbb8ff72311c5415a8ce677/orjson-3.10.16-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:02c6279016346e774dd92625d46c6c40db687b8a0d685aadb91e26e46cc33e1e", size = 137516 }, - { url = "https://files.pythonhosted.org/packages/29/87/32b7a4831e909d347278101a48d4cf9f3f25901b2295e7709df1651f65a1/orjson-3.10.16-cp312-cp312-win32.whl", hash = "sha256:7ca55097a11426db80f79378e873a8c51f4dde9ffc22de44850f9696b7eb0e8c", size = 141759 }, - { url = "https://files.pythonhosted.org/packages/35/ce/81a27e7b439b807bd393585271364cdddf50dc281fc57c4feef7ccb186a6/orjson-3.10.16-cp312-cp312-win_amd64.whl", hash = "sha256:86d127efdd3f9bf5f04809b70faca1e6836556ea3cc46e662b44dab3fe71f3d6", size = 133944 }, - { url = "https://files.pythonhosted.org/packages/87/b9/ff6aa28b8c86af9526160905593a2fe8d004ac7a5e592ee0b0ff71017511/orjson-3.10.16-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:148a97f7de811ba14bc6dbc4a433e0341ffd2cc285065199fb5f6a98013744bd", size = 249289 }, - { url = "https://files.pythonhosted.org/packages/6c/81/6d92a586149b52684ab8fd70f3623c91d0e6a692f30fd8c728916ab2263c/orjson-3.10.16-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:1d960c1bf0e734ea36d0adc880076de3846aaec45ffad29b78c7f1b7962516b8", size = 133640 }, - { url = "https://files.pythonhosted.org/packages/c2/88/b72443f4793d2e16039ab85d0026677932b15ab968595fb7149750d74134/orjson-3.10.16-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a318cd184d1269f68634464b12871386808dc8b7c27de8565234d25975a7a137", size = 138286 }, - { url = "https://files.pythonhosted.org/packages/c3/3c/72a22d4b28c076c4016d5a52bd644a8e4d849d3bb0373d9e377f9e3b2250/orjson-3.10.16-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df23f8df3ef9223d1d6748bea63fca55aae7da30a875700809c500a05975522b", size = 132307 }, - { url = "https://files.pythonhosted.org/packages/8a/a2/f1259561bdb6ad7061ff1b95dab082fe32758c4bc143ba8d3d70831f0a06/orjson-3.10.16-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b94dda8dd6d1378f1037d7f3f6b21db769ef911c4567cbaa962bb6dc5021cf90", size = 136739 }, - { url = "https://files.pythonhosted.org/packages/3d/af/c7583c4b34f33d8b8b90cfaab010ff18dd64e7074cc1e117a5f1eff20dcf/orjson-3.10.16-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f12970a26666a8775346003fd94347d03ccb98ab8aa063036818381acf5f523e", size = 138076 }, - { url = "https://files.pythonhosted.org/packages/d7/59/d7fc7fbdd3d4a64c2eae4fc7341a5aa39cf9549bd5e2d7f6d3c07f8b715b/orjson-3.10.16-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15a1431a245d856bd56e4d29ea0023eb4d2c8f71efe914beb3dee8ab3f0cd7fb", size = 142643 }, - { url = "https://files.pythonhosted.org/packages/92/0e/3bd8f2197d27601f16b4464ae948826da2bcf128af31230a9dbbad7ceb57/orjson-3.10.16-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c83655cfc247f399a222567d146524674a7b217af7ef8289c0ff53cfe8db09f0", size = 133168 }, - { url = "https://files.pythonhosted.org/packages/af/a8/351fd87b664b02f899f9144d2c3dc848b33ac04a5df05234cbfb9e2a7540/orjson-3.10.16-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fa59ae64cb6ddde8f09bdbf7baf933c4cd05734ad84dcf4e43b887eb24e37652", size = 135271 }, - { url = "https://files.pythonhosted.org/packages/ba/b0/a6d42a7d412d867c60c0337d95123517dd5a9370deea705ea1be0f89389e/orjson-3.10.16-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ca5426e5aacc2e9507d341bc169d8af9c3cbe88f4cd4c1cf2f87e8564730eb56", size = 412444 }, - { url = "https://files.pythonhosted.org/packages/79/ec/7572cd4e20863f60996f3f10bc0a6da64a6fd9c35954189a914cec0b7377/orjson-3.10.16-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6fd5da4edf98a400946cd3a195680de56f1e7575109b9acb9493331047157430", size = 152737 }, - { url = "https://files.pythonhosted.org/packages/a9/19/ceb9e8fed5403b2e76a8ac15f581b9d25780a3be3c9b3aa54b7777a210d5/orjson-3.10.16-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:980ecc7a53e567169282a5e0ff078393bac78320d44238da4e246d71a4e0e8f5", size = 137482 }, - { url = "https://files.pythonhosted.org/packages/1b/78/a78bb810f3786579dbbbd94768284cbe8f2fd65167cd7020260679665c17/orjson-3.10.16-cp313-cp313-win32.whl", hash = "sha256:28f79944dd006ac540a6465ebd5f8f45dfdf0948ff998eac7a908275b4c1add6", size = 141714 }, - { url = "https://files.pythonhosted.org/packages/81/9c/b66ce9245ff319df2c3278acd351a3f6145ef34b4a2d7f4b0f739368370f/orjson-3.10.16-cp313-cp313-win_amd64.whl", hash = "sha256:fe0a145e96d51971407cb8ba947e63ead2aa915db59d6631a355f5f2150b56b7", size = 133954 }, +version = "3.10.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/0b/fea456a3ffe74e70ba30e01ec183a9b26bec4d497f61dcfce1b601059c60/orjson-3.10.18.tar.gz", hash = "sha256:e8da3947d92123eda795b68228cafe2724815621fe35e8e320a9e9593a4bcd53", size = 5422810 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/16/2ceb9fb7bc2b11b1e4a3ea27794256e93dee2309ebe297fd131a778cd150/orjson-3.10.18-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a45e5d68066b408e4bc383b6e4ef05e717c65219a9e1390abc6155a520cac402", size = 248927 }, + { url = "https://files.pythonhosted.org/packages/3d/e1/d3c0a2bba5b9906badd121da449295062b289236c39c3a7801f92c4682b0/orjson-3.10.18-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be3b9b143e8b9db05368b13b04c84d37544ec85bb97237b3a923f076265ec89c", size = 136995 }, + { url = "https://files.pythonhosted.org/packages/d7/51/698dd65e94f153ee5ecb2586c89702c9e9d12f165a63e74eb9ea1299f4e1/orjson-3.10.18-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9b0aa09745e2c9b3bf779b096fa71d1cc2d801a604ef6dd79c8b1bfef52b2f92", size = 132893 }, + { url = "https://files.pythonhosted.org/packages/b3/e5/155ce5a2c43a85e790fcf8b985400138ce5369f24ee6770378ee6b691036/orjson-3.10.18-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53a245c104d2792e65c8d225158f2b8262749ffe64bc7755b00024757d957a13", size = 137017 }, + { url = "https://files.pythonhosted.org/packages/46/bb/6141ec3beac3125c0b07375aee01b5124989907d61c72c7636136e4bd03e/orjson-3.10.18-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9495ab2611b7f8a0a8a505bcb0f0cbdb5469caafe17b0e404c3c746f9900469", size = 138290 }, + { url = "https://files.pythonhosted.org/packages/77/36/6961eca0b66b7809d33c4ca58c6bd4c23a1b914fb23aba2fa2883f791434/orjson-3.10.18-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73be1cbcebadeabdbc468f82b087df435843c809cd079a565fb16f0f3b23238f", size = 142828 }, + { url = "https://files.pythonhosted.org/packages/8b/2f/0c646d5fd689d3be94f4d83fa9435a6c4322c9b8533edbb3cd4bc8c5f69a/orjson-3.10.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe8936ee2679e38903df158037a2f1c108129dee218975122e37847fb1d4ac68", size = 132806 }, + { url = "https://files.pythonhosted.org/packages/ea/af/65907b40c74ef4c3674ef2bcfa311c695eb934710459841b3c2da212215c/orjson-3.10.18-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7115fcbc8525c74e4c2b608129bef740198e9a120ae46184dac7683191042056", size = 135005 }, + { url = "https://files.pythonhosted.org/packages/c7/d1/68bd20ac6a32cd1f1b10d23e7cc58ee1e730e80624e3031d77067d7150fc/orjson-3.10.18-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:771474ad34c66bc4d1c01f645f150048030694ea5b2709b87d3bda273ffe505d", size = 413418 }, + { url = "https://files.pythonhosted.org/packages/31/31/c701ec0bcc3e80e5cb6e319c628ef7b768aaa24b0f3b4c599df2eaacfa24/orjson-3.10.18-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:7c14047dbbea52886dd87169f21939af5d55143dad22d10db6a7514f058156a8", size = 153288 }, + { url = "https://files.pythonhosted.org/packages/d9/31/5e1aa99a10893a43cfc58009f9da840990cc8a9ebb75aa452210ba18587e/orjson-3.10.18-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:641481b73baec8db14fdf58f8967e52dc8bda1f2aba3aa5f5c1b07ed6df50b7f", size = 137181 }, + { url = "https://files.pythonhosted.org/packages/bf/8c/daba0ac1b8690011d9242a0f37235f7d17df6d0ad941021048523b76674e/orjson-3.10.18-cp310-cp310-win32.whl", hash = "sha256:607eb3ae0909d47280c1fc657c4284c34b785bae371d007595633f4b1a2bbe06", size = 142694 }, + { url = "https://files.pythonhosted.org/packages/16/62/8b687724143286b63e1d0fab3ad4214d54566d80b0ba9d67c26aaf28a2f8/orjson-3.10.18-cp310-cp310-win_amd64.whl", hash = "sha256:8770432524ce0eca50b7efc2a9a5f486ee0113a5fbb4231526d414e6254eba92", size = 134600 }, + { url = "https://files.pythonhosted.org/packages/97/c7/c54a948ce9a4278794f669a353551ce7db4ffb656c69a6e1f2264d563e50/orjson-3.10.18-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e0a183ac3b8e40471e8d843105da6fbe7c070faab023be3b08188ee3f85719b8", size = 248929 }, + { url = "https://files.pythonhosted.org/packages/9e/60/a9c674ef1dd8ab22b5b10f9300e7e70444d4e3cda4b8258d6c2488c32143/orjson-3.10.18-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:5ef7c164d9174362f85238d0cd4afdeeb89d9e523e4651add6a5d458d6f7d42d", size = 133364 }, + { url = "https://files.pythonhosted.org/packages/c1/4e/f7d1bdd983082216e414e6d7ef897b0c2957f99c545826c06f371d52337e/orjson-3.10.18-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd14c5d99cdc7bf93f22b12ec3b294931518aa019e2a147e8aa2f31fd3240f7", size = 136995 }, + { url = "https://files.pythonhosted.org/packages/17/89/46b9181ba0ea251c9243b0c8ce29ff7c9796fa943806a9c8b02592fce8ea/orjson-3.10.18-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b672502323b6cd133c4af6b79e3bea36bad2d16bca6c1f645903fce83909a7a", size = 132894 }, + { url = "https://files.pythonhosted.org/packages/ca/dd/7bce6fcc5b8c21aef59ba3c67f2166f0a1a9b0317dcca4a9d5bd7934ecfd/orjson-3.10.18-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51f8c63be6e070ec894c629186b1c0fe798662b8687f3d9fdfa5e401c6bd7679", size = 137016 }, + { url = "https://files.pythonhosted.org/packages/1c/4a/b8aea1c83af805dcd31c1f03c95aabb3e19a016b2a4645dd822c5686e94d/orjson-3.10.18-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9478ade5313d724e0495d167083c6f3be0dd2f1c9c8a38db9a9e912cdaf947", size = 138290 }, + { url = "https://files.pythonhosted.org/packages/36/d6/7eb05c85d987b688707f45dcf83c91abc2251e0dd9fb4f7be96514f838b1/orjson-3.10.18-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:187aefa562300a9d382b4b4eb9694806e5848b0cedf52037bb5c228c61bb66d4", size = 142829 }, + { url = "https://files.pythonhosted.org/packages/d2/78/ddd3ee7873f2b5f90f016bc04062713d567435c53ecc8783aab3a4d34915/orjson-3.10.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da552683bc9da222379c7a01779bddd0ad39dd699dd6300abaf43eadee38334", size = 132805 }, + { url = "https://files.pythonhosted.org/packages/8c/09/c8e047f73d2c5d21ead9c180203e111cddeffc0848d5f0f974e346e21c8e/orjson-3.10.18-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e450885f7b47a0231979d9c49b567ed1c4e9f69240804621be87c40bc9d3cf17", size = 135008 }, + { url = "https://files.pythonhosted.org/packages/0c/4b/dccbf5055ef8fb6eda542ab271955fc1f9bf0b941a058490293f8811122b/orjson-3.10.18-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5e3c9cc2ba324187cd06287ca24f65528f16dfc80add48dc99fa6c836bb3137e", size = 413419 }, + { url = "https://files.pythonhosted.org/packages/8a/f3/1eac0c5e2d6d6790bd2025ebfbefcbd37f0d097103d76f9b3f9302af5a17/orjson-3.10.18-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:50ce016233ac4bfd843ac5471e232b865271d7d9d44cf9d33773bcd883ce442b", size = 153292 }, + { url = "https://files.pythonhosted.org/packages/1f/b4/ef0abf64c8f1fabf98791819ab502c2c8c1dc48b786646533a93637d8999/orjson-3.10.18-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b3ceff74a8f7ffde0b2785ca749fc4e80e4315c0fd887561144059fb1c138aa7", size = 137182 }, + { url = "https://files.pythonhosted.org/packages/a9/a3/6ea878e7b4a0dc5c888d0370d7752dcb23f402747d10e2257478d69b5e63/orjson-3.10.18-cp311-cp311-win32.whl", hash = "sha256:fdba703c722bd868c04702cac4cb8c6b8ff137af2623bc0ddb3b3e6a2c8996c1", size = 142695 }, + { url = "https://files.pythonhosted.org/packages/79/2a/4048700a3233d562f0e90d5572a849baa18ae4e5ce4c3ba6247e4ece57b0/orjson-3.10.18-cp311-cp311-win_amd64.whl", hash = "sha256:c28082933c71ff4bc6ccc82a454a2bffcef6e1d7379756ca567c772e4fb3278a", size = 134603 }, + { url = "https://files.pythonhosted.org/packages/03/45/10d934535a4993d27e1c84f1810e79ccf8b1b7418cef12151a22fe9bb1e1/orjson-3.10.18-cp311-cp311-win_arm64.whl", hash = "sha256:a6c7c391beaedd3fa63206e5c2b7b554196f14debf1ec9deb54b5d279b1b46f5", size = 131400 }, + { url = "https://files.pythonhosted.org/packages/21/1a/67236da0916c1a192d5f4ccbe10ec495367a726996ceb7614eaa687112f2/orjson-3.10.18-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:50c15557afb7f6d63bc6d6348e0337a880a04eaa9cd7c9d569bcb4e760a24753", size = 249184 }, + { url = "https://files.pythonhosted.org/packages/b3/bc/c7f1db3b1d094dc0c6c83ed16b161a16c214aaa77f311118a93f647b32dc/orjson-3.10.18-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:356b076f1662c9813d5fa56db7d63ccceef4c271b1fb3dd522aca291375fcf17", size = 133279 }, + { url = "https://files.pythonhosted.org/packages/af/84/664657cd14cc11f0d81e80e64766c7ba5c9b7fc1ec304117878cc1b4659c/orjson-3.10.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:559eb40a70a7494cd5beab2d73657262a74a2c59aff2068fdba8f0424ec5b39d", size = 136799 }, + { url = "https://files.pythonhosted.org/packages/9a/bb/f50039c5bb05a7ab024ed43ba25d0319e8722a0ac3babb0807e543349978/orjson-3.10.18-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f3c29eb9a81e2fbc6fd7ddcfba3e101ba92eaff455b8d602bf7511088bbc0eae", size = 132791 }, + { url = "https://files.pythonhosted.org/packages/93/8c/ee74709fc072c3ee219784173ddfe46f699598a1723d9d49cbc78d66df65/orjson-3.10.18-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6612787e5b0756a171c7d81ba245ef63a3533a637c335aa7fcb8e665f4a0966f", size = 137059 }, + { url = "https://files.pythonhosted.org/packages/6a/37/e6d3109ee004296c80426b5a62b47bcadd96a3deab7443e56507823588c5/orjson-3.10.18-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ac6bd7be0dcab5b702c9d43d25e70eb456dfd2e119d512447468f6405b4a69c", size = 138359 }, + { url = "https://files.pythonhosted.org/packages/4f/5d/387dafae0e4691857c62bd02839a3bf3fa648eebd26185adfac58d09f207/orjson-3.10.18-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f72f100cee8dde70100406d5c1abba515a7df926d4ed81e20a9730c062fe9ad", size = 142853 }, + { url = "https://files.pythonhosted.org/packages/27/6f/875e8e282105350b9a5341c0222a13419758545ae32ad6e0fcf5f64d76aa/orjson-3.10.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dca85398d6d093dd41dc0983cbf54ab8e6afd1c547b6b8a311643917fbf4e0c", size = 133131 }, + { url = "https://files.pythonhosted.org/packages/48/b2/73a1f0b4790dcb1e5a45f058f4f5dcadc8a85d90137b50d6bbc6afd0ae50/orjson-3.10.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22748de2a07fcc8781a70edb887abf801bb6142e6236123ff93d12d92db3d406", size = 134834 }, + { url = "https://files.pythonhosted.org/packages/56/f5/7ed133a5525add9c14dbdf17d011dd82206ca6840811d32ac52a35935d19/orjson-3.10.18-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3a83c9954a4107b9acd10291b7f12a6b29e35e8d43a414799906ea10e75438e6", size = 413368 }, + { url = "https://files.pythonhosted.org/packages/11/7c/439654221ed9c3324bbac7bdf94cf06a971206b7b62327f11a52544e4982/orjson-3.10.18-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:303565c67a6c7b1f194c94632a4a39918e067bd6176a48bec697393865ce4f06", size = 153359 }, + { url = "https://files.pythonhosted.org/packages/48/e7/d58074fa0cc9dd29a8fa2a6c8d5deebdfd82c6cfef72b0e4277c4017563a/orjson-3.10.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:86314fdb5053a2f5a5d881f03fca0219bfdf832912aa88d18676a5175c6916b5", size = 137466 }, + { url = "https://files.pythonhosted.org/packages/57/4d/fe17581cf81fb70dfcef44e966aa4003360e4194d15a3f38cbffe873333a/orjson-3.10.18-cp312-cp312-win32.whl", hash = "sha256:187ec33bbec58c76dbd4066340067d9ece6e10067bb0cc074a21ae3300caa84e", size = 142683 }, + { url = "https://files.pythonhosted.org/packages/e6/22/469f62d25ab5f0f3aee256ea732e72dc3aab6d73bac777bd6277955bceef/orjson-3.10.18-cp312-cp312-win_amd64.whl", hash = "sha256:f9f94cf6d3f9cd720d641f8399e390e7411487e493962213390d1ae45c7814fc", size = 134754 }, + { url = "https://files.pythonhosted.org/packages/10/b0/1040c447fac5b91bc1e9c004b69ee50abb0c1ffd0d24406e1350c58a7fcb/orjson-3.10.18-cp312-cp312-win_arm64.whl", hash = "sha256:3d600be83fe4514944500fa8c2a0a77099025ec6482e8087d7659e891f23058a", size = 131218 }, + { url = "https://files.pythonhosted.org/packages/04/f0/8aedb6574b68096f3be8f74c0b56d36fd94bcf47e6c7ed47a7bd1474aaa8/orjson-3.10.18-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:69c34b9441b863175cc6a01f2935de994025e773f814412030f269da4f7be147", size = 249087 }, + { url = "https://files.pythonhosted.org/packages/bc/f7/7118f965541aeac6844fcb18d6988e111ac0d349c9b80cda53583e758908/orjson-3.10.18-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:1ebeda919725f9dbdb269f59bc94f861afbe2a27dce5608cdba2d92772364d1c", size = 133273 }, + { url = "https://files.pythonhosted.org/packages/fb/d9/839637cc06eaf528dd8127b36004247bf56e064501f68df9ee6fd56a88ee/orjson-3.10.18-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5adf5f4eed520a4959d29ea80192fa626ab9a20b2ea13f8f6dc58644f6927103", size = 136779 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/f226ecfef31a1f0e7d6bf9a31a0bbaf384c7cbe3fce49cc9c2acc51f902a/orjson-3.10.18-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7592bb48a214e18cd670974f289520f12b7aed1fa0b2e2616b8ed9e069e08595", size = 132811 }, + { url = "https://files.pythonhosted.org/packages/73/2d/371513d04143c85b681cf8f3bce743656eb5b640cb1f461dad750ac4b4d4/orjson-3.10.18-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f872bef9f042734110642b7a11937440797ace8c87527de25e0c53558b579ccc", size = 137018 }, + { url = "https://files.pythonhosted.org/packages/69/cb/a4d37a30507b7a59bdc484e4a3253c8141bf756d4e13fcc1da760a0b00cb/orjson-3.10.18-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0315317601149c244cb3ecef246ef5861a64824ccbcb8018d32c66a60a84ffbc", size = 138368 }, + { url = "https://files.pythonhosted.org/packages/1e/ae/cd10883c48d912d216d541eb3db8b2433415fde67f620afe6f311f5cd2ca/orjson-3.10.18-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0da26957e77e9e55a6c2ce2e7182a36a6f6b180ab7189315cb0995ec362e049", size = 142840 }, + { url = "https://files.pythonhosted.org/packages/6d/4c/2bda09855c6b5f2c055034c9eda1529967b042ff8d81a05005115c4e6772/orjson-3.10.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb70d489bc79b7519e5803e2cc4c72343c9dc1154258adf2f8925d0b60da7c58", size = 133135 }, + { url = "https://files.pythonhosted.org/packages/13/4a/35971fd809a8896731930a80dfff0b8ff48eeb5d8b57bb4d0d525160017f/orjson-3.10.18-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e9e86a6af31b92299b00736c89caf63816f70a4001e750bda179e15564d7a034", size = 134810 }, + { url = "https://files.pythonhosted.org/packages/99/70/0fa9e6310cda98365629182486ff37a1c6578e34c33992df271a476ea1cd/orjson-3.10.18-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:c382a5c0b5931a5fc5405053d36c1ce3fd561694738626c77ae0b1dfc0242ca1", size = 413491 }, + { url = "https://files.pythonhosted.org/packages/32/cb/990a0e88498babddb74fb97855ae4fbd22a82960e9b06eab5775cac435da/orjson-3.10.18-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8e4b2ae732431127171b875cb2668f883e1234711d3c147ffd69fe5be51a8012", size = 153277 }, + { url = "https://files.pythonhosted.org/packages/92/44/473248c3305bf782a384ed50dd8bc2d3cde1543d107138fd99b707480ca1/orjson-3.10.18-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d808e34ddb24fc29a4d4041dcfafbae13e129c93509b847b14432717d94b44f", size = 137367 }, + { url = "https://files.pythonhosted.org/packages/ad/fd/7f1d3edd4ffcd944a6a40e9f88af2197b619c931ac4d3cfba4798d4d3815/orjson-3.10.18-cp313-cp313-win32.whl", hash = "sha256:ad8eacbb5d904d5591f27dee4031e2c1db43d559edb8f91778efd642d70e6bea", size = 142687 }, + { url = "https://files.pythonhosted.org/packages/4b/03/c75c6ad46be41c16f4cfe0352a2d1450546f3c09ad2c9d341110cd87b025/orjson-3.10.18-cp313-cp313-win_amd64.whl", hash = "sha256:aed411bcb68bf62e85588f2a7e03a6082cc42e5a2796e06e72a962d7c6310b52", size = 134794 }, + { url = "https://files.pythonhosted.org/packages/c2/28/f53038a5a72cc4fd0b56c1eafb4ef64aec9685460d5ac34de98ca78b6e29/orjson-3.10.18-cp313-cp313-win_arm64.whl", hash = "sha256:f54c1385a0e6aba2f15a40d703b858bedad36ded0491e55d35d905b2c34a4cc3", size = 131186 }, ] [[package]] @@ -518,11 +524,11 @@ wheels = [ [[package]] name = "packaging" -version = "24.2" +version = "25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, ] [[package]] @@ -643,27 +649,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.11.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/45/71/5759b2a6b2279bb77fe15b1435b89473631c2cd6374d45ccdb6b785810be/ruff-0.11.5.tar.gz", hash = "sha256:cae2e2439cb88853e421901ec040a758960b576126dab520fa08e9de431d1bef", size = 3976488 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/23/db/6efda6381778eec7f35875b5cbefd194904832a1153d68d36d6b269d81a8/ruff-0.11.5-py3-none-linux_armv6l.whl", hash = "sha256:2561294e108eb648e50f210671cc56aee590fb6167b594144401532138c66c7b", size = 10103150 }, - { url = "https://files.pythonhosted.org/packages/44/f2/06cd9006077a8db61956768bc200a8e52515bf33a8f9b671ee527bb10d77/ruff-0.11.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac12884b9e005c12d0bd121f56ccf8033e1614f736f766c118ad60780882a077", size = 10898637 }, - { url = "https://files.pythonhosted.org/packages/18/f5/af390a013c56022fe6f72b95c86eb7b2585c89cc25d63882d3bfe411ecf1/ruff-0.11.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4bfd80a6ec559a5eeb96c33f832418bf0fb96752de0539905cf7b0cc1d31d779", size = 10236012 }, - { url = "https://files.pythonhosted.org/packages/b8/ca/b9bf954cfed165e1a0c24b86305d5c8ea75def256707f2448439ac5e0d8b/ruff-0.11.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0947c0a1afa75dcb5db4b34b070ec2bccee869d40e6cc8ab25aca11a7d527794", size = 10415338 }, - { url = "https://files.pythonhosted.org/packages/d9/4d/2522dde4e790f1b59885283f8786ab0046958dfd39959c81acc75d347467/ruff-0.11.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad871ff74b5ec9caa66cb725b85d4ef89b53f8170f47c3406e32ef040400b038", size = 9965277 }, - { url = "https://files.pythonhosted.org/packages/e5/7a/749f56f150eef71ce2f626a2f6988446c620af2f9ba2a7804295ca450397/ruff-0.11.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6cf918390cfe46d240732d4d72fa6e18e528ca1f60e318a10835cf2fa3dc19f", size = 11541614 }, - { url = "https://files.pythonhosted.org/packages/89/b2/7d9b8435222485b6aac627d9c29793ba89be40b5de11584ca604b829e960/ruff-0.11.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56145ee1478582f61c08f21076dc59153310d606ad663acc00ea3ab5b2125f82", size = 12198873 }, - { url = "https://files.pythonhosted.org/packages/00/e0/a1a69ef5ffb5c5f9c31554b27e030a9c468fc6f57055886d27d316dfbabd/ruff-0.11.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5f66f8f1e8c9fc594cbd66fbc5f246a8d91f916cb9667e80208663ec3728304", size = 11670190 }, - { url = "https://files.pythonhosted.org/packages/05/61/c1c16df6e92975072c07f8b20dad35cd858e8462b8865bc856fe5d6ccb63/ruff-0.11.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80b4df4d335a80315ab9afc81ed1cff62be112bd165e162b5eed8ac55bfc8470", size = 13902301 }, - { url = "https://files.pythonhosted.org/packages/79/89/0af10c8af4363304fd8cb833bd407a2850c760b71edf742c18d5a87bb3ad/ruff-0.11.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3068befab73620b8a0cc2431bd46b3cd619bc17d6f7695a3e1bb166b652c382a", size = 11350132 }, - { url = "https://files.pythonhosted.org/packages/b9/e1/ecb4c687cbf15164dd00e38cf62cbab238cad05dd8b6b0fc68b0c2785e15/ruff-0.11.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5da2e710a9641828e09aa98b92c9ebbc60518fdf3921241326ca3e8f8e55b8b", size = 10312937 }, - { url = "https://files.pythonhosted.org/packages/cf/4f/0e53fe5e500b65934500949361e3cd290c5ba60f0324ed59d15f46479c06/ruff-0.11.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ef39f19cb8ec98cbc762344921e216f3857a06c47412030374fffd413fb8fd3a", size = 9936683 }, - { url = "https://files.pythonhosted.org/packages/04/a8/8183c4da6d35794ae7f76f96261ef5960853cd3f899c2671961f97a27d8e/ruff-0.11.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b2a7cedf47244f431fd11aa5a7e2806dda2e0c365873bda7834e8f7d785ae159", size = 10950217 }, - { url = "https://files.pythonhosted.org/packages/26/88/9b85a5a8af21e46a0639b107fcf9bfc31da4f1d263f2fc7fbe7199b47f0a/ruff-0.11.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:81be52e7519f3d1a0beadcf8e974715b2dfc808ae8ec729ecfc79bddf8dbb783", size = 11404521 }, - { url = "https://files.pythonhosted.org/packages/fc/52/047f35d3b20fd1ae9ccfe28791ef0f3ca0ef0b3e6c1a58badd97d450131b/ruff-0.11.5-py3-none-win32.whl", hash = "sha256:e268da7b40f56e3eca571508a7e567e794f9bfcc0f412c4b607931d3af9c4afe", size = 10320697 }, - { url = "https://files.pythonhosted.org/packages/b9/fe/00c78010e3332a6e92762424cf4c1919065707e962232797d0b57fd8267e/ruff-0.11.5-py3-none-win_amd64.whl", hash = "sha256:6c6dc38af3cfe2863213ea25b6dc616d679205732dc0fb673356c2d69608f800", size = 11378665 }, - { url = "https://files.pythonhosted.org/packages/43/7c/c83fe5cbb70ff017612ff36654edfebec4b1ef79b558b8e5fd933bab836b/ruff-0.11.5-py3-none-win_arm64.whl", hash = "sha256:67e241b4314f4eacf14a601d586026a962f4002a475aa702c69980a38087aa4e", size = 10460287 }, +version = "0.11.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/f6/adcf73711f31c9f5393862b4281c875a462d9f639f4ccdf69dc368311c20/ruff-0.11.8.tar.gz", hash = "sha256:6d742d10626f9004b781f4558154bb226620a7242080e11caeffab1a40e99df8", size = 4086399 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/60/c6aa9062fa518a9f86cb0b85248245cddcd892a125ca00441df77d79ef88/ruff-0.11.8-py3-none-linux_armv6l.whl", hash = "sha256:896a37516c594805e34020c4a7546c8f8a234b679a7716a3f08197f38913e1a3", size = 10272473 }, + { url = "https://files.pythonhosted.org/packages/a0/e4/0325e50d106dc87c00695f7bcd5044c6d252ed5120ebf423773e00270f50/ruff-0.11.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ab86d22d3d721a40dd3ecbb5e86ab03b2e053bc93c700dc68d1c3346b36ce835", size = 11040862 }, + { url = "https://files.pythonhosted.org/packages/e6/27/b87ea1a7be37fef0adbc7fd987abbf90b6607d96aa3fc67e2c5b858e1e53/ruff-0.11.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:258f3585057508d317610e8a412788cf726efeefa2fec4dba4001d9e6f90d46c", size = 10385273 }, + { url = "https://files.pythonhosted.org/packages/d3/f7/3346161570d789045ed47a86110183f6ac3af0e94e7fd682772d89f7f1a1/ruff-0.11.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:727d01702f7c30baed3fc3a34901a640001a2828c793525043c29f7614994a8c", size = 10578330 }, + { url = "https://files.pythonhosted.org/packages/c6/c3/327fb950b4763c7b3784f91d3038ef10c13b2d42322d4ade5ce13a2f9edb/ruff-0.11.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3dca977cc4fc8f66e89900fa415ffe4dbc2e969da9d7a54bfca81a128c5ac219", size = 10122223 }, + { url = "https://files.pythonhosted.org/packages/de/c7/ba686bce9adfeb6c61cb1bbadc17d58110fe1d602f199d79d4c880170f19/ruff-0.11.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c657fa987d60b104d2be8b052d66da0a2a88f9bd1d66b2254333e84ea2720c7f", size = 11697353 }, + { url = "https://files.pythonhosted.org/packages/53/8e/a4fb4a1ddde3c59e73996bb3ac51844ff93384d533629434b1def7a336b0/ruff-0.11.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f2e74b021d0de5eceb8bd32919f6ff8a9b40ee62ed97becd44993ae5b9949474", size = 12375936 }, + { url = "https://files.pythonhosted.org/packages/ad/a1/9529cb1e2936e2479a51aeb011307e7229225df9ac64ae064d91ead54571/ruff-0.11.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9b5ef39820abc0f2c62111f7045009e46b275f5b99d5e59dda113c39b7f4f38", size = 11850083 }, + { url = "https://files.pythonhosted.org/packages/3e/94/8f7eac4c612673ae15a4ad2bc0ee62e03c68a2d4f458daae3de0e47c67ba/ruff-0.11.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1dba3135ca503727aa4648152c0fa67c3b1385d3dc81c75cd8a229c4b2a1458", size = 14005834 }, + { url = "https://files.pythonhosted.org/packages/1e/7c/6f63b46b2be870cbf3f54c9c4154d13fac4b8827f22fa05ac835c10835b2/ruff-0.11.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f024d32e62faad0f76b2d6afd141b8c171515e4fb91ce9fd6464335c81244e5", size = 11503713 }, + { url = "https://files.pythonhosted.org/packages/3a/91/57de411b544b5fe072779678986a021d87c3ee5b89551f2ca41200c5d643/ruff-0.11.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d365618d3ad747432e1ae50d61775b78c055fee5936d77fb4d92c6f559741948", size = 10457182 }, + { url = "https://files.pythonhosted.org/packages/01/49/cfe73e0ce5ecdd3e6f1137bf1f1be03dcc819d1bfe5cff33deb40c5926db/ruff-0.11.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4d9aaa91035bdf612c8ee7266153bcf16005c7c7e2f5878406911c92a31633cb", size = 10101027 }, + { url = "https://files.pythonhosted.org/packages/56/21/a5cfe47c62b3531675795f38a0ef1c52ff8de62eaddf370d46634391a3fb/ruff-0.11.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0eba551324733efc76116d9f3a0d52946bc2751f0cd30661564117d6fd60897c", size = 11111298 }, + { url = "https://files.pythonhosted.org/packages/36/98/f76225f87e88f7cb669ae92c062b11c0a1e91f32705f829bd426f8e48b7b/ruff-0.11.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:161eb4cff5cfefdb6c9b8b3671d09f7def2f960cee33481dd898caf2bcd02304", size = 11566884 }, + { url = "https://files.pythonhosted.org/packages/de/7e/fff70b02e57852fda17bd43f99dda37b9bcf3e1af3d97c5834ff48d04715/ruff-0.11.8-py3-none-win32.whl", hash = "sha256:5b18caa297a786465cc511d7f8be19226acf9c0a1127e06e736cd4e1878c3ea2", size = 10451102 }, + { url = "https://files.pythonhosted.org/packages/7b/a9/eaa571eb70648c9bde3120a1d5892597de57766e376b831b06e7c1e43945/ruff-0.11.8-py3-none-win_amd64.whl", hash = "sha256:6e70d11043bef637c5617297bdedec9632af15d53ac1e1ba29c448da9341b0c4", size = 11597410 }, + { url = "https://files.pythonhosted.org/packages/cd/be/f6b790d6ae98f1f32c645f8540d5c96248b72343b0a56fab3a07f2941897/ruff-0.11.8-py3-none-win_arm64.whl", hash = "sha256:304432e4c4a792e3da85b7699feb3426a0908ab98bf29df22a31b0cdd098fac2", size = 10713129 }, ] [[package]] @@ -725,7 +731,7 @@ wheels = [ [[package]] name = "trio" -version = "0.29.0" +version = "0.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -736,9 +742,9 @@ dependencies = [ { name = "sniffio" }, { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/47/f62e62a1a6f37909aed0bf8f5d5411e06fa03846cfcb64540cd1180ccc9f/trio-0.29.0.tar.gz", hash = "sha256:ea0d3967159fc130acb6939a0be0e558e364fee26b5deeecc893a6b08c361bdf", size = 588952 } +sdist = { url = "https://files.pythonhosted.org/packages/01/c1/68d582b4d3a1c1f8118e18042464bb12a7c1b75d64d75111b297687041e3/trio-0.30.0.tar.gz", hash = "sha256:0781c857c0c81f8f51e0089929a26b5bb63d57f927728a5586f7e36171f064df", size = 593776 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/55/c4d9bea8b3d7937901958f65124123512419ab0eb73695e5f382521abbfb/trio-0.29.0-py3-none-any.whl", hash = "sha256:d8c463f1a9cc776ff63e331aba44c125f423a5a13c684307e828d930e625ba66", size = 492920 }, + { url = "https://files.pythonhosted.org/packages/69/8e/3f6dfda475ecd940e786defe6df6c500734e686c9cd0a0f8ef6821e9b2f2/trio-0.30.0-py3-none-any.whl", hash = "sha256:3bf4f06b8decf8d3cf00af85f40a89824669e2d033bb32469d34840edcfc22a5", size = 499194 }, ] [[package]] @@ -752,25 +758,25 @@ wheels = [ [[package]] name = "uv" -version = "0.6.14" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e5/eb/07bc000a3c05372448b63c45da98630c532ec4e059d848488c3e774d017a/uv-0.6.14.tar.gz", hash = "sha256:a117466f307d164a74444949cc94ec4328ec880fb489cbaa7df324dab14c5c98", size = 3134567 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/bf/3e87dec7728b249458967f39a301376cb776e559c90261c1dac963686dc3/uv-0.6.14-py3-none-linux_armv6l.whl", hash = "sha256:c775e5d7a80ff43cb88856bbdcd838918d5ac3dc362414317e6bbaeb615fff98", size = 16228143 }, - { url = "https://files.pythonhosted.org/packages/24/b2/111e1ea40453d93c849f36a67397b51d9b458e6e598c3629ffe76d11b490/uv-0.6.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2578f6f8cdbcc036ffad1043f9f66ade3ac0babf29def6abd9eefd4a7c6621cb", size = 16273279 }, - { url = "https://files.pythonhosted.org/packages/72/89/e7fc8a047f08234cc26d1e37e5f573887744205d087f8e8e6f3d0feb04ce/uv-0.6.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9fc8fe58871b4fe02a863b05b8b1b25ef1b6c60d4d224e85338f5c2be0ab4f0e", size = 15115451 }, - { url = "https://files.pythonhosted.org/packages/20/1e/72ac3d1e0805d3b49b0a4de46483489ea1989827440f42b0cfb444cdc67f/uv-0.6.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:2fb2cd7f6aae21b81474b0051d30e7ed939a9a71714948c47f58b0e7acdd2a80", size = 15540456 }, - { url = "https://files.pythonhosted.org/packages/fd/47/5aeb7fb80c673bc28ccf3ab99e376b1cd92eac41af6b9b48c0e38b114c54/uv-0.6.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d6ca3f99c1a6c1c430ae8f451133fb4e8c3a22f661c257425402a5d9430bb797", size = 15979820 }, - { url = "https://files.pythonhosted.org/packages/1f/44/c3ad856473f2ef5f22c865a73a0a37ee82d11fcca78ae82f5ac895a7023a/uv-0.6.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed41877b679e0a1af9ab65427d829b87a81b499017e59c70756d4ba02ca43fcb", size = 16650494 }, - { url = "https://files.pythonhosted.org/packages/7a/f6/8a1245530c282d470909db78cf56831693c58b90d9b819e35aa2d85fbbe8/uv-0.6.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:fe9b4361b1c8055301b715fdd94d94eb512053dc4545fec40d3fe3657f655987", size = 17505028 }, - { url = "https://files.pythonhosted.org/packages/a5/70/0806268440651e2ad1b3542af42b800e20bb7e43050a9ca78f3d1eb4c660/uv-0.6.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:998b67bb1cebbe044fc2c5cb251c29cffc56f62a6d55719d6f4e960461d6edad", size = 17245854 }, - { url = "https://files.pythonhosted.org/packages/2a/3a/0da9780868626466d8c4977fb02d1b0daa80e6f7504d7b662cae3fb4af3d/uv-0.6.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d433925db6e2ef46047b68962d136ff2ef17a7b5609168615f19e60674232c9", size = 21584756 }, - { url = "https://files.pythonhosted.org/packages/eb/fd/21a82b78173be1a2ea20f4f55154e7252bd80d21ed60b9bbbc0e2047b8d0/uv-0.6.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36aaeb00a70a10f748e16c7a1fc410862e2ba905806e7e9dfbc3e64596309404", size = 16878847 }, - { url = "https://files.pythonhosted.org/packages/6c/9a/7c84650ae9fb801ecc848d49dcba201243989d9234fe3ec4a4e935ff21c0/uv-0.6.14-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:11779beb3bd1f92814bc8d8cd350d5228e8f9198cca2f52138b53030a4061d93", size = 15810089 }, - { url = "https://files.pythonhosted.org/packages/0b/b3/efcbd3a2d298801109b24feee655bb80fe4178aa6bf68e49664c48b342b2/uv-0.6.14-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:bf1ec103cf9a0850f03935dc6a93cacc680fa2c90c3b41cfc10da311afab8f5b", size = 15962056 }, - { url = "https://files.pythonhosted.org/packages/3f/53/c92c894cb34e9578c2e6dc195bcd4eb0a140dd57c96a60207d847521a902/uv-0.6.14-py3-none-musllinux_1_1_i686.whl", hash = "sha256:955e36c98a438a249e178988d4f13b1bb831eb57264d73c459f171b5afd7b023", size = 16255226 }, - { url = "https://files.pythonhosted.org/packages/df/eb/38bc37856691d53008bf094d03d9e7ab0c2927523a3901c83e152e7c9915/uv-0.6.14-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:2d534e7dc1299c8b53eb7b4c7575e4f0933673ea8b1275d3f3022f5670e311db", size = 17005225 }, - { url = "https://files.pythonhosted.org/packages/d8/fe/087d5193603e16bc5f67556d94cf8fa8634785c5863cccdec825f14e9a4c/uv-0.6.14-py3-none-win32.whl", hash = "sha256:7cdf3c8d927b07d4eaffc44809eb57523d449705f10dabbdd6f34f7bdfc7d5fe", size = 16131231 }, - { url = "https://files.pythonhosted.org/packages/40/17/33c5c1503c35c874932d4a21ec10a55051e3695dba12b7de700bcfad0cca/uv-0.6.14-py3-none-win_amd64.whl", hash = "sha256:012f46bef6909209c4a6749e4019eb755ba762d37d7ceaaf76da9cb4b7f771e9", size = 17628508 }, - { url = "https://files.pythonhosted.org/packages/77/09/163062d439ddc0d89e527ae0e631abf1f7781b183442d8823c48af368f5d/uv-0.6.14-py3-none-win_arm64.whl", hash = "sha256:7465081b4d0b213d0055ccb48de7fe546b5cf0853c6d3601115760760634f6d8", size = 16387232 }, +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/d4/c1104ee4d8a69e4834888cd850eb4f9327c585e5e60da108fda788d3872d/uv-0.7.2.tar.gz", hash = "sha256:45e619bb076916b79df8c5ecc28d1be04d1ccd0b63b080c44ae973b8deb33b25", size = 3293566 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/c3/68291a239dbedc0389fa5ce5b5b6c7c2a54c52bc11e9503276f376faa9e7/uv-0.7.2-py3-none-linux_armv6l.whl", hash = "sha256:e1e4394b54bc387f227ca1b2aa0348d35f6455b6168ca1826c1dc5f4fc3e8d20", size = 16590159 }, + { url = "https://files.pythonhosted.org/packages/6c/ac/3c7e8df1d6bb84a805aa773ea4f6a006682f8241f331c9c359eb5310f042/uv-0.7.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c0edb194c35f1f12c75bec4fe2d7d4d09f0c2cec3a16102217a772620ce1d6e6", size = 16753976 }, + { url = "https://files.pythonhosted.org/packages/42/ca/6a3f3c094794d482e3418f6a46c2753fa4f6ed2fe5b7ecf299db8cfed9ea/uv-0.7.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:be2e8d033936ba8ed9ccf85eb2d15c7a8db3bb3e9c4960bdf7c3c98034a6dbda", size = 15513631 }, + { url = "https://files.pythonhosted.org/packages/1e/65/6fae29e0eb884fa1cab89b0fa865d409e0e2bcada8316cd50b4c81e8706c/uv-0.7.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:a314a94b42bc6014f18c877f723292306b76c10b455c2b385728e1470e661ced", size = 15972100 }, + { url = "https://files.pythonhosted.org/packages/a6/92/3d8da1efc7f3272ccc65c50cb13abd9e6a32246bb6c258175c68a91d0d80/uv-0.7.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e4d1652fe3608fa564dbeaeb2465208f691ac04b57f655ebef62e9ec6d37103d", size = 16288666 }, + { url = "https://files.pythonhosted.org/packages/2c/5e/7d6a788c45d5e2686d01c4886ebb21149892a59bcfa15b66d0646e73aafa/uv-0.7.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48c115a3c13c3b29748e325093ee04fd48eaf91145bedc68727f78e6a1c34ab8", size = 17165785 }, + { url = "https://files.pythonhosted.org/packages/e4/9e/4d0a947ffa4b377c6e34935c23164c7914d7239154d254aa5938db6a7e83/uv-0.7.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c388172209ca5a47706666d570a45fef3dd39db9258682e10b2f62ca521f0e91", size = 18014800 }, + { url = "https://files.pythonhosted.org/packages/c7/31/781288f9f53e1770128f7830841d7d269097ed70a4afa71578d45721bfa2/uv-0.7.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63c97cc5e8029a8dc0e1fc39f15f746be931345bc0aeae85feceaa1828f0de87", size = 17745484 }, + { url = "https://files.pythonhosted.org/packages/6d/04/030eec46217225b77ccff1f2808e64074873d86fe445be3784649506e65e/uv-0.7.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1fa315366ee36ad1f734734f3153e2f334342900061fc0ed18b06f3b9bb2dfe2", size = 22103174 }, + { url = "https://files.pythonhosted.org/packages/5c/07/9d85d0a9ddd49dbec18bde741ffb33d0c671a153461b094a9c73504e1b92/uv-0.7.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7236ec776c559fbc3ae4389b7cd506a2428ad9dd0402ac3d9446200ea3dc45f6", size = 17369922 }, + { url = "https://files.pythonhosted.org/packages/11/18/cfef0efe3c4ebdd81422f35215bb915fd599fc946b40306186d87e90678b/uv-0.7.2-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:78ec372b2f5c7ff8a034e16dd04bc579a62561a5eac4b6dfc96af60298a97d31", size = 16209878 }, + { url = "https://files.pythonhosted.org/packages/31/ed/2ddd7547203ddd368b9ec56b245e09931f868daf2d2b0e29c0b69584466d/uv-0.7.2-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:28fd5d689ae4f8f16533f091a6dd63e1ddf3b7c782003ac8a18584ddb8823cbe", size = 16271878 }, + { url = "https://files.pythonhosted.org/packages/f0/9c/30a48a9d875b91b486286d1a4ccc081dad130acea0dca683c1786ddd7c84/uv-0.7.2-py3-none-musllinux_1_1_i686.whl", hash = "sha256:9aaacb143622cd437a446a4b316a546c02403b438cd7fd7556d62f47a9fd0a99", size = 16742005 }, + { url = "https://files.pythonhosted.org/packages/a5/b3/5550a721a1e8a99117d960f16c05ad8d39aff79a3fc1aadf2ed13da4385f/uv-0.7.2-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:81b86fff996c302be6aa1c1ac6eb72b97a7277c319e52c0def50d40b1ffaa617", size = 17443927 }, + { url = "https://files.pythonhosted.org/packages/52/1f/71a7c3e9c79718647fea1e6fe85ccc82d2629cd858b437ae2081190045cc/uv-0.7.2-py3-none-win32.whl", hash = "sha256:19a64c38657c4fbe7c945055755500116fdaac8e121381a5245ea66823f8c500", size = 16869579 }, + { url = "https://files.pythonhosted.org/packages/44/f0/4424cf64533b7576610f7de5c94183d810743b08e81072a2bb2d98316947/uv-0.7.2-py3-none-win_amd64.whl", hash = "sha256:dc1ee6114c824f5880c584a96b2947a35817fdd3a0b752d1adbd926ae6872d1c", size = 18287842 }, + { url = "https://files.pythonhosted.org/packages/0a/5c/12ce48cab21fb0f9bde4ea0c19ec2ab88d4aa9a53e148a52cfb9a41578c9/uv-0.7.2-py3-none-win_arm64.whl", hash = "sha256:0445e56d3f9651ad84d5a7f16efabba83bf305b73594f1c1bc0659aeab952040", size = 16929582 }, ] diff --git a/zizmor.yml b/zizmor.yml new file mode 100644 index 0000000..c359223 --- /dev/null +++ b/zizmor.yml @@ -0,0 +1,6 @@ +rules: + unpinned-uses: + config: + policies: + # TODO: use the default policies + "*": any From 9e6eb834753ed36489f41ac1e599b0850334b215 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Wed, 9 Jul 2025 13:21:40 -0500 Subject: [PATCH 57/58] Don't use depreciated numpy plugin --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c15c228..42327a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,6 @@ azul = ["py.typed", "data/*", "lang/*", "fonts/*"] package = true [tool.mypy] -plugins = ["numpy.typing.mypy_plugin"] files = ["src/azul/", "computer_players"] enable_error_code = ["truthy-bool", "mutable-override"] show_column_numbers = true From 2d0ea586510e7b7dc4fff9b7ec9ffc5aa4e3c50a Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Wed, 9 Jul 2025 20:01:31 -0500 Subject: [PATCH 58/58] Fix bad merge --- tests/helpers.py | 175 --------------------------------------- tests/test_encryption.py | 110 ------------------------ 2 files changed, 285 deletions(-) delete mode 100644 tests/helpers.py delete mode 100644 tests/test_encryption.py diff --git a/tests/helpers.py b/tests/helpers.py deleted file mode 100644 index 0739739..0000000 --- a/tests/helpers.py +++ /dev/null @@ -1,175 +0,0 @@ -# This is the base_io module from https://github.com/py-mine/mcproto v0.5.0, -# which is licensed under the GNU LESSER GENERAL PUBLIC LICENSE v3.0 - -from __future__ import annotations - -__author__ = "ItsDrike" -__license__ = "LGPL-3.0-only" - -import inspect -import unittest.mock -from functools import partial -from typing import TYPE_CHECKING, Any, Generic, TypeVar - -import trio -from typing_extensions import ParamSpec - -if TYPE_CHECKING: - from collections.abc import Callable, Coroutine - -T = TypeVar("T") -P = ParamSpec("P") -T_Mock = TypeVar("T_Mock", bound=unittest.mock.Mock) - - -def synchronize(f: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, T]: - """Take an asynchronous function, and return a synchronous alternative. - - This is needed because we sometimes want to test asynchronous behavior in a synchronous test function, - where we can't simply await something. This function uses `trio.run` and generates a wrapper - around the original asynchronous function, that awaits the result in a blocking synchronous way, - returning the obtained value. - """ - - def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: - return trio.run(partial(f, *args, **kwargs)) - - return wrapper - - -class SynchronizedMixin: - """Class acting as another wrapped object, with all async methods synchronized. - - This class needs :attr:`._WRAPPED_ATTRIBUTE` class variable to be set as the name of the internally - held attribute, holding the object we'll be wrapping around. - - Child classes of this mixin will have their lookup logic changed, to instead perform a lookup - on the wrapped attribute. Only if that lookup fails, we fallback to this class, meaning if both - the wrapped attribute and this class have some attribute defined, the attribute from the wrapped - object is returned. The only exceptions to this are lookup of the ``_WRAPPED_ATTRIBUTE`` variable, - and of the attribute name stored under the ``_WRAPPED_ATTRIBUTE`` (the wrapped object). - - If the attribute held by the wrapped object is an asynchronous function, instead of returning it - directly, the :func:`.synchronize` function will be called, returning a wrapped synchronous - alternative for the requested async function. - - This is useful when we need to quickly create a synchronous alternative to a class holding async methods. - However it isn't useful in production, since will cause typing issues (attributes will be accessible, but - type checkers won't know that they exist here, because of the dynamic nature of this implementation). - """ - - _WRAPPED_ATTRIBUTE: str - - def __getattribute__(self, name: str, /) -> Any: - """Return attributes of the wrapped object, if the attribute is a coroutine function, synchronize it. - - The only exception to this behavior is getting the :attr:`._WRAPPED_ATTRIBUTE` variable itself, or the - attribute named as the content of the ``_WRAPPED_ATTRIBUTE`` variable. All other attribute access will - be delegated to the wrapped attribute. If the wrapped object doesn't have given attribute, the lookup - will fallback to regular lookup for variables belonging to this class. - """ - if ( - name == "_WRAPPED_ATTRIBUTE" or name == self._WRAPPED_ATTRIBUTE - ): # Order is important - return super().__getattribute__(name) - - wrapped = getattr(self, self._WRAPPED_ATTRIBUTE) - - if hasattr(wrapped, name): - obj = getattr(wrapped, name) - if inspect.iscoroutinefunction(obj): - return synchronize(obj) - return obj - - return super().__getattribute__(name) - - def __setattr__(self, name: str, value: object, /) -> None: - """Allow for changing attributes of the wrapped object. - - * If wrapped object isn't yet set, fall back to :meth:`~object.__setattr__` of this class. - * If wrapped object doesn't already contain the attribute we want to set, also fallback to this class. - * Otherwise, run ``__setattr__`` on it to update it. - """ - try: - wrapped = getattr(self, self._WRAPPED_ATTRIBUTE) - except AttributeError: - return super().__setattr__(name, value) - else: - if hasattr(wrapped, name): - return setattr(wrapped, name, value) - - return super().__setattr__(name, value) - - -class UnpropagatingMockMixin(Generic[T_Mock]): - """Provides common functionality for our :class:`~unittest.mock.Mock` classes. - - By default, mock objects propagate themselves by returning a new instance of the same mock - class, with same initialization attributes. This is done whenever we're accessing new - attributes that mock class. - - This propagation makes sense for simple mocks without any additional restrictions, however when - dealing with limited mocks to some ``spec_set``, it doesn't usually make sense to propagate - those same ``spec_set`` restrictions, since we generally don't have attributes/methods of a - class be of/return the same class. - - This mixin class stops this propagation, and instead returns instances of specified mock class, - defined in :attr:`.child_mock_type` class variable, which is by default set to - :class:`~unittest.mock.MagicMock`, as it can safely represent most objects. - - .. note: - This propagation handling will only be done for the mock classes that inherited from this - mixin class. That means if the :attr:`.child_mock_type` is one of the regular mock classes, - and the mock is propagated, a regular mock class is returned as that new attribute. This - regular class then won't have the same overrides, and will therefore propagate itself, like - any other mock class would. - - If you wish to counteract this, you can set the :attr:`.child_mock_type` to a mock class - that also inherits from this mixin class, perhaps to your class itself, overriding any - propagation recursively. - """ - - child_mock_type: T_Mock = unittest.mock.MagicMock - - # Since this is a mixin class, we can access some attributes defined in mock classes safely. - # Define the types of these variables here, for proper static type analysis. - _mock_sealed: bool - _extract_mock_name: Callable[[], str] - - def _get_child_mock(self, **kwargs) -> T_Mock: - """Make :attr:`.child_mock_type`` instances instead of instances of the same class. - - By default, this method creates a new mock instance of the same original class, and passes - over the same initialization arguments. This overrides that behavior to instead create an - instance of :attr:`.child_mock_type` class. - """ - # Mocks can be sealed, in which case we wouldn't want to allow propagation of any kind - # and rather raise an AttributeError, informing that given attr isn't accessible - if self._mock_sealed: - mock_name = self._extract_mock_name() - obj_name = ( - f"{mock_name}.{kwargs['name']}" - if "name" in kwargs - else f"{mock_name}()" - ) - raise AttributeError(f"Can't access {obj_name}, mock is sealed.") - - # Propagate any other children as simple `unittest.mock.Mock` instances - # rather than `self.__class__` instances - return self.child_mock_type(**kwargs) - - -class CustomMockMixin(UnpropagatingMockMixin): - """Provides common functionality for our custom mock types. - - * Stops propagation of same ``spec_set`` restricted mock in child mocks - (see :class:`.UnpropagatingMockMixin` for more info) - * Allows using the ``spec_set`` attribute as class attribute - """ - - spec_set = None - - def __init__(self, **kwargs): - if "spec_set" in kwargs: - self.spec_set = kwargs.pop("spec_set") - super().__init__(spec_set=self.spec_set, **kwargs) # type: ignore # Mixin class, this __init__ is valid diff --git a/tests/test_encryption.py b/tests/test_encryption.py deleted file mode 100644 index 92538ad..0000000 --- a/tests/test_encryption.py +++ /dev/null @@ -1,110 +0,0 @@ -# This is the buffer module from https://github.com/py-mine/mcproto v0.5.0, -# which is licensed under the GNU LESSER GENERAL PUBLIC LICENSE v3.0 - -from __future__ import annotations - -__author__ = "ItsDrike" -__license__ = "LGPL-3.0-only" - -from typing import TYPE_CHECKING, cast - -from cryptography.hazmat.primitives.asymmetric.padding import MGF1, OAEP -from cryptography.hazmat.primitives.hashes import SHA256 -from cryptography.hazmat.primitives.serialization import load_pem_private_key - -from azul.encryption import ( - decrypt_token_and_secret, - deserialize_public_key, - encrypt_token_and_secret, - serialize_public_key, -) - -if TYPE_CHECKING: - from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey - -_SERIALIZED_RSA_PRIVATE_KEY = b""" ------BEGIN PRIVATE KEY----- -MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAMtRUQmRHqPkdA2K -F6fM2c8ibIPHYV5KVQXNEkVx7iEKS6JsfELhX1H8t/qQ3Ob4Pr4OFjgXx9n7GvfZ -gekNoswG6lnQH/n7t2sYA 6D+WvSix1FF2J6wPmpKriHS59TDk4opjaV14S4K4XjW -Gmm8DqCzgXkPGC2dunFb+1A8mdkrAgMBAAECgYAWj2dWkGu989OMzQ3i6LAic8dm -t/Dt7YGRqzejzQiHUgUieLcxFKDnEAu6GejpGBKeNCHzB3B9l4deiRwJKCIwHqMN -LKMKoayinA8mj/Y/ O/ELDofkEyeXOhFyM642sPpaxQJoNWc9QEsYbxpG2zeB3sPf -l3eIhkYTKVdxB+o8AQJBAPiddMjU8fuHyjKT6VCL2ZQbwnrRe1AaLLE6VLwEZuZC -wlbx5Lcszi77PkMRTvltQW39VN6MEjiYFSPtRJleA+sCQQDRW2e3BX6uiil2IZ08 -tPFMnltFJpa 8YvW50N6mySd8Zg1oQJpzP2fC0n0+K4j3EiA/Zli8jBt45cJ4dMGX -km/BAkEAtkYy5j+BvolbDGP3Ti+KcRU9K/DD+QGHvNRoZYTQsIdHlpk4t7eo3zci -+ecJwMOCkhKHE7cccNPHxBRkFBGiywJAJBt2pMsu0R2FDxm3C6xNXaCGL0P7hVwv -8y9B51 QUGlFjiJJz0OKjm6c/8IQDqFEY/LZDIamsZ0qBItNIPEMGQQJALZV0GD5Y -zmnkw1hek/JcfQBlVYo3gFmWBh6Hl1Lb7p3TKUViJCA1k2f0aGv7+d9aFS0fRq6u -/sETkem8Jc1s3g== ------END PRIVATE KEY----- -""" -RSA_PRIVATE_KEY = cast( - "RSAPrivateKey", - load_pem_private_key(_SERIALIZED_RSA_PRIVATE_KEY, password=None), -) -RSA_PUBLIC_KEY = RSA_PRIVATE_KEY.public_key() -SERIALIZED_RSA_PUBLIC_KEY = bytes.fromhex( - "30819f300d06092a864886f70d010101050003818d0030818902818100cb515109911ea3e4740d8a17a7ccd9cf226c83c7615e4a5505cd124571ee210a4ba26c7c42e15f51fcb7fa90dce6f83ebe0e163817c7d9fb1af7d981e90da2cc06ea59d01ff9fbb76b1803a0fe5af4a2c75145d89eb03e6a4aae21d2e7d4c3938a298da575e12e0ae178d61a69bc0ea0b381790f182d9dba715bfb503c99d92b0203010001", -) - - -def test_encrypt_token_and_secret() -> None: - """Test encryption returns properly encrypted (decryptable) values.""" - verification_token = bytes.fromhex("da053623dd3dcd441e105ee5ce212ac8") - shared_secret = bytes.fromhex( - "95a883358f09cd5698b3cf8a414a8a659a35c4eb877e9b0228b7f64df85b0f26", - ) - - encrypted_token, encrypted_secret = encrypt_token_and_secret( - RSA_PUBLIC_KEY, - verification_token, - shared_secret, - ) - - assert ( - RSA_PRIVATE_KEY.decrypt( - encrypted_token, - OAEP(MGF1(SHA256()), SHA256(), None), - ) - == verification_token - ) - assert ( - RSA_PRIVATE_KEY.decrypt( - encrypted_secret, - OAEP(MGF1(SHA256()), SHA256(), None), - ) - == shared_secret - ) - - -def test_decrypt_token_and_secret() -> None: - """Test decryption returns properly decrypted values.""" - encrypted_token = bytes.fromhex( - "5541c0c0fc99d8908ed428b20c260795bec7b4041a4f98d26fbed383e8dba077eb53fb5cf905e722e2ceb341843e875508134817bcd3a909ac279e77ed94fd98c428bbe00db630a5ad3df310380d9274ed369cc6a011e7edd45cbe44ae8ad2575ef793b23057e4b15f1b6e3e195ff0921e46370773218517922fbb8b96092d88", - ) - encrypted_secret = bytes.fromhex( - "1a43782ca17f71e87e6ef98f9be66050ecf5d185da81445d26ceb5941f95d69d61b726d27b5ca62aed4cbe27b40fd4bd6b16b5be154a7b6a24ae31c705bc47d9397589b448fb72b14572ea2a9d843c6a3c674b7454cef97e2d65be36e0d0a8cc9f1093a19a8d52a5633a5317d19779bb46146dfaea7a690a7f080fb77d59c7f9", - ) - - assert decrypt_token_and_secret( - RSA_PRIVATE_KEY, - encrypted_token, - encrypted_secret, - ) == ( - bytes.fromhex("da053623dd3dcd441e105ee5ce212ac8"), - bytes.fromhex( - "95a883358f09cd5698b3cf8a414a8a659a35c4eb877e9b0228b7f64df85b0f26", - ), - ) - - -def test_serialize_public_key() -> None: - """Test serialize_public_key.""" - assert serialize_public_key(RSA_PUBLIC_KEY) == SERIALIZED_RSA_PUBLIC_KEY - - -def test_deserialize_public_key() -> None: - """Test deserialize_public_key.""" - assert deserialize_public_key(SERIALIZED_RSA_PUBLIC_KEY) == RSA_PUBLIC_KEY