diff --git a/pyproject.toml b/pyproject.toml index f84af82..b735671 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ keywords = [ dependencies = [ "pygame~=2.6.0", "typing_extensions>=4.12.2", + "mypy_extensions>=1.0.0", "trio~=0.26.2", "cryptography>=43.0.0", "exceptiongroup; python_version < '3.11'", @@ -53,7 +54,7 @@ version = {attr = "checkers.game.__version__"} "Bug Tracker" = "https://github.com/CoolCat467/Checkers/issues" [project.scripts] -checkers_game = "checkers.game:cli_run" +checkers_game = "checkers:cli_run" [tool.setuptools.package-data] checkers = ["py.typed", "data/*"] diff --git a/src/checkers/__init__.py b/src/checkers/__init__.py index 5007862..54a86b3 100644 --- a/src/checkers/__init__.py +++ b/src/checkers/__init__.py @@ -2,7 +2,7 @@ # Programmed by CoolCat467 -# Copyright (C) 2023 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 @@ -16,3 +16,9 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . + + +from checkers.game import cli_run as cli_run + +if __name__ == "__main__": + cli_run() diff --git a/src/checkers/base2d.py b/src/checkers/base2d.py index 10dbbff..aeeb543 100644 --- a/src/checkers/base2d.py +++ b/src/checkers/base2d.py @@ -33,7 +33,7 @@ from checkers.vector import Vector2 if TYPE_CHECKING: - from collections.abc import Callable, Generator, Iterable, Sequence + from collections.abc import Callable, Iterable, Sequence def amol( @@ -148,20 +148,6 @@ def get_colors( return colors -def average_color( - surface: pygame.surface.Surface, -) -> Generator[int, None, None]: - """Return the average RGB value of a surface.""" - s_r, s_g, s_b = 0, 0, 0 - colors = get_colors(surface) - for color in colors: - r, g, b = color - s_r += r - s_g += g - s_b += b - return (int(x / len(colors)) for x in (s_r, s_g, s_b)) - - def replace_with_color( surface: pygame.surface.Surface, color: tuple[int, int, int], @@ -357,8 +343,8 @@ def __init__( self.value = 0 self.max_value = int(states) self.anim = anim - self.press_time: float = 1 - self.last_press: float = 0 + self.press_time: float = 1.0 + self.last_press: float = 0.0 self.scan = int(max(get_surf_lens(self.anim)) / 2) + 2 keys = list(kwargs.keys()) diff --git a/src/checkers/client.py b/src/checkers/client.py index 5f2cf1e..6795201 100644 --- a/src/checkers/client.py +++ b/src/checkers/client.py @@ -46,6 +46,9 @@ write_position, ) +if TYPE_CHECKING: + from mypy_extensions import u8 + async def read_advertisements( timeout: int = 3, # noqa: ASYNC109 @@ -304,7 +307,7 @@ async def read_create_piece(self, event: Event[bytearray]) -> None: buffer = Buffer(event.data) piece_pos = read_position(buffer) - piece_type = buffer.read_value(StructFormat.UBYTE) + piece_type: u8 = buffer.read_value(StructFormat.UBYTE) await self.raise_event( Event("gameboard_create_piece", (piece_pos, piece_type)), @@ -381,7 +384,7 @@ async def read_update_piece_animation( buffer = Buffer(event.data) piece_pos = read_position(buffer) - piece_type = buffer.read_value(StructFormat.UBYTE) + piece_type: u8 = buffer.read_value(StructFormat.UBYTE) await self.raise_event( Event("gameboard_update_piece_animation", (piece_pos, piece_type)), @@ -415,7 +418,7 @@ async def read_game_over(self, event: Event[bytearray]) -> None: """Read update_piece event from server.""" buffer = Buffer(event.data) - winner = buffer.read_value(StructFormat.UBYTE) + winner: u8 = buffer.read_value(StructFormat.UBYTE) await self.raise_event(Event("game_winner", winner)) self.running = False @@ -430,7 +433,7 @@ async def read_action_complete(self, event: Event[bytearray]) -> None: from_pos = read_position(buffer) to_pos = read_position(buffer) - current_turn = buffer.read_value(StructFormat.UBYTE) + current_turn: u8 = buffer.read_value(StructFormat.UBYTE) await self.raise_event( Event("game_action_complete", (from_pos, to_pos, current_turn)), @@ -441,7 +444,7 @@ async def read_initial_config(self, event: Event[bytearray]) -> None: buffer = Buffer(event.data) board_size = read_position(buffer) - current_turn = 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)), @@ -451,7 +454,7 @@ async def read_playing_as(self, event: Event[bytearray]) -> None: """Read playing_as event from server.""" buffer = Buffer(event.data) - playing_as = buffer.read_value(StructFormat.UBYTE) + playing_as: u8 = buffer.read_value(StructFormat.UBYTE) await self.raise_event( Event("game_playing_as", playing_as), diff --git a/src/checkers/component.py b/src/checkers/component.py index 9d8997f..463fc20 100644 --- a/src/checkers/component.py +++ b/src/checkers/component.py @@ -33,6 +33,8 @@ if TYPE_CHECKING: from collections.abc import Awaitable, Callable, Generator, Iterable + from mypy_extensions import u8 + T = TypeVar("T") @@ -45,7 +47,7 @@ def __init__( self, name: str, data: T, - levels: int = 0, + levels: u8 = 0, ) -> None: """Initialize event.""" self.name = name diff --git a/src/checkers/game.py b/src/checkers/game.py index a728feb..b64a62f 100644 --- a/src/checkers/game.py +++ b/src/checkers/game.py @@ -22,7 +22,7 @@ __title__ = "Checkers" __author__ = "CoolCat467" __license__ = "GNU General Public License Version 3" -__version__ = "2.0.1" +__version__ = "2.0.2" # Note: Tile Ids are chess board tile titles, A1 to H8 # A8 ... H8 @@ -34,9 +34,8 @@ import contextlib import platform from collections import deque -from os import path from pathlib import Path -from typing import TYPE_CHECKING, Any, Final, TypeAlias, TypeVar +from typing import TYPE_CHECKING, Any, Final, TypeVar import pygame import trio @@ -53,7 +52,7 @@ Event, ExternalRaiseManager, ) -from checkers.network_shared import DEFAULT_PORT, find_ip +from checkers.network_shared import DEFAULT_PORT, Pos, find_ip from checkers.objects import Button, OutlinedText from checkers.server import GameServer from checkers.sound import SoundData, play_sound as base_play_sound @@ -105,11 +104,17 @@ T = TypeVar("T") -DATA_FOLDER: Final = Path(__file__).parent / "data" +if globals().get("__file__") is None: + import importlib -IS_WINDOWS: Final = platform.system() == "Windows" + __file__ = str( + Path(importlib.import_module("checkers.data").__path__[0]).parent + / "game.py", + ) + +DATA_FOLDER: Final = Path(__file__).absolute().parent / "data" -Pos: TypeAlias = tuple[int, int] +IS_WINDOWS: Final = platform.system() == "Windows" def render_text( @@ -704,7 +709,7 @@ def generate_tile_images(self) -> None: outline_ident = outline.precalculate_outline(name, outline_color) image.add_image(f"{name}_outlined", outline_ident) - def get_tile_location(self, position: Pos) -> Vector2: + def get_tile_location(self, position: tuple[int, int]) -> Vector2: """Return the center point of a given tile position.""" location = Vector2.from_iter(position) * self.tile_size center = self.tile_size // 2 @@ -773,7 +778,7 @@ def generate_board_image(self) -> Surface: ### Blit the id of the tile at the tile's location ##surf.blit( ## render_text( - ## trio.Path(path.dirname(__file__), "data", "VeraSerif.ttf"), + ## DATA_FOLDER / "VeraSerif.ttf", ## 20, ## "".join(map(str, (x, y))), ## GREEN @@ -826,7 +831,7 @@ async def update_selected(self) -> None: if not self.selected: movement: sprite.MovementComponent = self.get_component("movement") - movement.speed = 0 + movement.speed = 0.0 async def click(self, event: Event[dict[str, int]]) -> None: """Toggle selected.""" @@ -841,15 +846,18 @@ async def drag(self, event: Event[Any]) -> None: self.selected = True await self.update_selected() movement: sprite.MovementComponent = self.get_component("movement") - movement.speed = 0 + movement.speed = 0.0 - async def mouse_down(self, event: Event[dict[str, int | Pos]]) -> None: + async def mouse_down( + self, + event: Event[dict[str, int | tuple[int, int]]], + ) -> 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 + movement.speed = 200.0 target: sprite.TargetingComponent = self.get_component("targeting") assert isinstance(event.data["pos"], tuple) target.destination = Vector2.from_iter(event.data["pos"]) @@ -944,7 +952,7 @@ class FPSCounter(objects.Text): def __init__(self) -> None: """Initialize FPS counter.""" font = pygame.font.Font( - trio.Path(path.dirname(__file__), "data", "VeraSerif.ttf"), + DATA_FOLDER / "VeraSerif.ttf", 28, ) super().__init__("fps", font) @@ -1112,11 +1120,11 @@ async def entry_actions(self) -> None: self.id = self.machine.new_group("title") button_font = pygame.font.Font( - trio.Path(path.dirname(__file__), "data", "VeraSerif.ttf"), + DATA_FOLDER / "VeraSerif.ttf", 28, ) title_font = pygame.font.Font( - trio.Path(path.dirname(__file__), "data", "VeraSerif.ttf"), + DATA_FOLDER / "VeraSerif.ttf", 56, ) @@ -1266,7 +1274,7 @@ def __init__(self) -> None: self.buttons: dict[tuple[str, int], int] = {} self.font = pygame.font.Font( - trio.Path(path.dirname(__file__), "data", "VeraSerif.ttf"), + DATA_FOLDER / "VeraSerif.ttf", 28, ) @@ -1412,7 +1420,7 @@ async def do_actions(self) -> None: self.exit_data = (exit_status, message, True) font = pygame.font.Font( - trio.Path(path.dirname(__file__), "data", "VeraSerif.ttf"), + DATA_FOLDER / "VeraSerif.ttf", 28, ) @@ -1503,7 +1511,7 @@ async def async_run() -> None: client = CheckersClient(event_manager) background = pygame.image.load( - path.join(path.dirname(__file__), "data", "background.png"), + DATA_FOLDER / "background.png", ).convert() client.clear(screen, background) diff --git a/src/checkers/network.py b/src/checkers/network.py index c199eff..2a0cb5a 100644 --- a/src/checkers/network.py +++ b/src/checkers/network.py @@ -30,11 +30,8 @@ TYPE_CHECKING, Any, AnyStr, - Generic, Literal, NoReturn, - SupportsIndex, - TypeAlias, ) import trio @@ -51,15 +48,10 @@ ) if TYPE_CHECKING: - from collections.abc import Iterable from types import TracebackType from typing_extensions import Self - BytesConvertable: TypeAlias = SupportsIndex | Iterable[SupportsIndex] -else: - BytesConvertable = Generic - class NetworkTimeoutError(Exception): """Network Timeout Error.""" @@ -67,7 +59,7 @@ class NetworkTimeoutError(Exception): __slots__ = () -class NetworkStreamNotConnectedError(RuntimeError): +class NetworkStreamNotConnectedError(Exception): """Network Stream Not Connected Error.""" __slots__ = () diff --git a/src/checkers/network_shared.py b/src/checkers/network_shared.py index 69264b0..1eab3b7 100644 --- a/src/checkers/network_shared.py +++ b/src/checkers/network_shared.py @@ -2,7 +2,7 @@ # Programmed by CoolCat467 -# Copyright (C) 2023 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 @@ -26,6 +26,7 @@ from typing import TYPE_CHECKING, Final, NamedTuple, TypeAlias import trio +from mypy_extensions import u8 from .base_io import StructFormat @@ -37,7 +38,7 @@ DEFAULT_PORT: Final = 31613 -Pos: TypeAlias = tuple[int, int] +Pos: TypeAlias = tuple[u8, u8] class TickEventData(NamedTuple): @@ -49,8 +50,8 @@ class TickEventData(NamedTuple): def read_position(buffer: Buffer) -> Pos: """Read a position tuple from buffer.""" - pos_x = buffer.read_value(StructFormat.UBYTE) - pos_y = buffer.read_value(StructFormat.UBYTE) + pos_x: u8 = buffer.read_value(StructFormat.UBYTE) + pos_y: u8 = buffer.read_value(StructFormat.UBYTE) return pos_x, pos_y diff --git a/src/checkers/sound.py b/src/checkers/sound.py index b8ac825..9e120a3 100644 --- a/src/checkers/sound.py +++ b/src/checkers/sound.py @@ -52,7 +52,7 @@ def play_sound( """Play sound with pygame.""" sound_object = mixer.Sound(filename) sound_object.set_volume(sound_data.volume) - seconds = sound_object.get_length() + seconds: int | float = sound_object.get_length() if sound_data.maxtime > 0: seconds = sound_data.maxtime _channel = sound_object.play( diff --git a/src/checkers/state.py b/src/checkers/state.py index f0e6ab8..e00ce49 100644 --- a/src/checkers/state.py +++ b/src/checkers/state.py @@ -26,11 +26,22 @@ import copy import math -from typing import TYPE_CHECKING, Any, NamedTuple, Self, TypeVar, cast +from typing import ( + TYPE_CHECKING, + Any, + 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? @@ -45,7 +56,7 @@ T = TypeVar("T") -Pos = tuple[int, int] +Pos: TypeAlias = tuple[u8, u8] class Action(NamedTuple): @@ -79,7 +90,7 @@ def get_sides(xy: Pos) -> tuple[Pos, Pos, Pos, Pos]: return cast(tuple[Pos, Pos, Pos, Pos], tuple_sides) -def pawn_modify(moves: tuple[T, ...], piece_type: int) -> tuple[T, ...]: +def pawn_modify(moves: tuple[T, ...], piece_type: u8) -> tuple[T, ...]: """Return moves but remove invalid moves for pawns.""" assert ( len(moves) == 4 @@ -104,7 +115,7 @@ def __init__( self, size: tuple[int, int], pieces: dict[Pos, int], - turn: int = 1, # Black moves first + turn: bool = True, # Black moves first /, pre_calculated_actions: dict[Pos, ActionSet] | None = None, ) -> None: @@ -285,7 +296,7 @@ def action_from_points(start: Pos, end: Pos) -> Action: def get_turn(self) -> int: """Return whose turn it is. 0 = red, 1 = black.""" - return self.turn + return int(self.turn) def valid_location(self, position: Pos) -> bool: """Return if position is valid.""" @@ -483,7 +494,7 @@ def check_for_win(self) -> int | None: has_move = True # Player has at least one move, no need to continue break - if not has_move and self.turn == player: + 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