From 77c5bda019bc9e371e0ea1e3678f8d890177c9d8 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Fri, 20 Sep 2024 01:55:40 -0500 Subject: [PATCH 01/21] Compile project with mypyc --- pyproject.toml | 24 ++++++++++----- src/checkers/__init__.py | 8 ++++- src/checkers/base2d.py | 20 ++----------- src/checkers/client.py | 15 ++++++---- src/checkers/component.py | 4 ++- src/checkers/game.py | 53 +++++++++++++++++++++------------- src/checkers/network.py | 10 +------ src/checkers/network_shared.py | 9 +++--- src/checkers/sound.py | 2 +- src/checkers/state.py | 22 ++++++++++---- 10 files changed, 95 insertions(+), 72 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f84af82..c49eee7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [build-system] -requires = ["setuptools >= 64"] -build-backend = "setuptools.build_meta" +requires = ["hatchling >= 1.20.0"] +build-backend = "hatchling.build" [project] name = "checkers" @@ -39,13 +39,14 @@ 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'", ] -[tool.setuptools.dynamic] -version = {attr = "checkers.game.__version__"} +[tool.hatch.version] +path = "src/checkers/game.py" [project.urls] "Homepage" = "https://github.com/CoolCat467/Checkers" @@ -53,10 +54,19 @@ 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/*"] +[tool.hatch.build.targets.wheel.hooks.mypyc] +dependencies = [ + "hatch-mypyc>=0.16.0", + "mypy==1.11.2", +] +require-runtime-dependencies = true +exclude = [ + "src/checkers/vector.py", + "src/checkers/base_io.py", + "src/checkers/buffer.py", +] [tool.mypy] mypy_path = "src" 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..e71ab88 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 @@ -68,6 +67,7 @@ Iterable, Sequence, ) + from types import ModuleType from pygame.surface import Surface @@ -105,11 +105,21 @@ T = TypeVar("T") -DATA_FOLDER: Final = Path(__file__).parent / "data" +if globals().get("__file__") is None: + if TYPE_CHECKING: -IS_WINDOWS: Final = platform.system() == "Windows" + def resolve(name: str) -> ModuleType: ... # noqa: D103 + + else: + from importlib.resources._common import resolve + + __file__ = str( + Path(resolve("checkers.data").__path__[0]).parent / "game.py", + ) -Pos: TypeAlias = tuple[int, int] +DATA_FOLDER: Final = Path(__file__).absolute().parent / "data" + +IS_WINDOWS: Final = platform.system() == "Windows" def render_text( @@ -704,7 +714,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 +783,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 +836,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 +851,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 +957,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 +1125,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 +1279,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 +1425,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, ) @@ -1455,7 +1468,7 @@ async def do_actions(self) -> None: class CheckersClient(sprite.GroupProcessor): """Checkers Game Client.""" - __slots__ = ("manager",) + __slots__ = ("manager", "__weakref__") def __init__(self, manager: ComponentManager) -> None: """Initialize Checkers Client.""" @@ -1503,7 +1516,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..6af98cb 100644 --- a/src/checkers/state.py +++ b/src/checkers/state.py @@ -26,7 +26,17 @@ import copy import math -from typing import TYPE_CHECKING, Any, NamedTuple, Self, TypeVar, cast +from typing import ( + TYPE_CHECKING, + Any, + NamedTuple, + Self, + TypeAlias, + TypeVar, + cast, +) + +from mypy_extensions import u8 if TYPE_CHECKING: from collections.abc import Callable, Generator, Iterable @@ -45,7 +55,7 @@ T = TypeVar("T") -Pos = tuple[int, int] +Pos: TypeAlias = tuple[u8, u8] class Action(NamedTuple): @@ -79,7 +89,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 +114,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 +295,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 +493,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 From d250b05241ddaf41f10610660a3859c7aba0c4ca Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Fri, 20 Sep 2024 02:26:57 -0500 Subject: [PATCH 02/21] Fix type issues and remove duplicate weakref slot --- src/checkers/game.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/checkers/game.py b/src/checkers/game.py index e71ab88..b64a62f 100644 --- a/src/checkers/game.py +++ b/src/checkers/game.py @@ -67,7 +67,6 @@ Iterable, Sequence, ) - from types import ModuleType from pygame.surface import Surface @@ -106,15 +105,11 @@ T = TypeVar("T") if globals().get("__file__") is None: - if TYPE_CHECKING: - - def resolve(name: str) -> ModuleType: ... # noqa: D103 - - else: - from importlib.resources._common import resolve + import importlib __file__ = str( - Path(resolve("checkers.data").__path__[0]).parent / "game.py", + Path(importlib.import_module("checkers.data").__path__[0]).parent + / "game.py", ) DATA_FOLDER: Final = Path(__file__).absolute().parent / "data" @@ -1468,7 +1463,7 @@ async def do_actions(self) -> None: class CheckersClient(sprite.GroupProcessor): """Checkers Game Client.""" - __slots__ = ("manager", "__weakref__") + __slots__ = ("manager",) def __init__(self, manager: ComponentManager) -> None: """Initialize Checkers Client.""" From d4ae9e2d30e08ec563d397caf0126d16e300f692 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Fri, 20 Sep 2024 02:29:42 -0500 Subject: [PATCH 03/21] Fix `Self` not existing in older versions --- src/checkers/state.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/checkers/state.py b/src/checkers/state.py index 6af98cb..e00ce49 100644 --- a/src/checkers/state.py +++ b/src/checkers/state.py @@ -30,7 +30,6 @@ TYPE_CHECKING, Any, NamedTuple, - Self, TypeAlias, TypeVar, cast, @@ -41,6 +40,8 @@ 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? From cc0a1b68fa024a8c3e822fceb9ad051dc7402db7 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sun, 13 Oct 2024 16:12:22 -0500 Subject: [PATCH 04/21] Don't compile `namedtuple_mod` --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 63cd243..55d238a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,6 +65,7 @@ exclude = [ "src/checkers/vector.py", "src/checkers/base_io.py", "src/checkers/buffer.py", + "src/checkers/namedtuple_mod.py", ] [tool.mypy] From 9bf4662386e73283507e9bc9203cd37bbf33cbdb Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sat, 19 Oct 2024 22:26:59 -0500 Subject: [PATCH 05/21] Don't compile `component`, uses weak references --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 0f3ff0f..b47901f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,6 +66,7 @@ exclude = [ "src/checkers/base_io.py", "src/checkers/buffer.py", "src/checkers/namedtuple_mod.py", + "src/checkers/component.py", ] [tool.mypy] From 0f2f9f14366c4f209d524ee3b22944405b73abd8 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sat, 19 Oct 2024 22:55:01 -0500 Subject: [PATCH 06/21] Avoid mypyc errors --- pyproject.toml | 1 + src/checkers/client.py | 6 ++- src/checkers/game.py | 57 ++-------------------- src/checkers/multi_inherit.py | 89 +++++++++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 56 deletions(-) create mode 100644 src/checkers/multi_inherit.py diff --git a/pyproject.toml b/pyproject.toml index b47901f..5a3d5ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,6 +67,7 @@ exclude = [ "src/checkers/buffer.py", "src/checkers/namedtuple_mod.py", "src/checkers/component.py", + "src/checkers/multi_inherit.py", ] [tool.mypy] diff --git a/src/checkers/client.py b/src/checkers/client.py index 0f8e2ad..66071fa 100644 --- a/src/checkers/client.py +++ b/src/checkers/client.py @@ -323,7 +323,9 @@ async def handle_client_connect( traceback.print_exception(ex) else: self.running = True - while not self.not_connected and self.running: + while self.running: + if self.not_connected: + break await self.handle_read_event() self.running = False @@ -341,7 +343,7 @@ async def handle_client_connect( async def read_callback_ping(self, event: Event[bytearray]) -> None: """Read callback_ping event from server.""" - ns = int.from_bytes(event.data) + ns = int.from_bytes(event.data, byteorder="big") now = int(time.time() * 1e9) difference = now - ns diff --git a/src/checkers/game.py b/src/checkers/game.py index 5a66d45..84a9ce2 100644 --- a/src/checkers/game.py +++ b/src/checkers/game.py @@ -52,6 +52,7 @@ Event, ExternalRaiseManager, ) +from checkers.multi_inherit import ConnectionElement, ReturnElement from checkers.network_shared import DEFAULT_PORT, Pos, find_ip from checkers.objects import Button, OutlinedText from checkers.server import GameServer @@ -1254,60 +1255,6 @@ class PlayInternalHostingState(PlayHostingState): 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, - _: Event[sprite.PygameMouseButtonEventData], - ) -> None: - """Handle Click Event.""" - await self.raise_event( - Event("return_to_title", None, 2), - ) - - -class ConnectionElement(element_list.Element, objects.Button): - """Connection list element sprite.""" - - __slots__ = () - - def __init__( - self, - 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, - _: Event[sprite.PygameMouseButtonEventData], - ) -> None: - """Handle Click Event.""" - details = self.name - await self.raise_event( - Event("join_server", details, 2), - ) - - class PlayJoiningState(GameState): """Start running client.""" @@ -1343,6 +1290,8 @@ async def entry_actions(self) -> None: 30, ) return_button = ReturnElement("return_button", return_font) + return_button.outline = RED + return_button.location = (SCREEN_SIZE[0] // 2, 10) connections.add_element(return_button) self.manager.register_handlers( diff --git a/src/checkers/multi_inherit.py b/src/checkers/multi_inherit.py new file mode 100644 index 0000000..526eb18 --- /dev/null +++ b/src/checkers/multi_inherit.py @@ -0,0 +1,89 @@ +"""Objects that inherit from multipls base classes because mypyc is dumb.""" + +# Programmed by CoolCat467 + +from __future__ import annotations + +# Objects that inherit from multipls base classes because mypyc is dumb +# 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__ = "Multi-inherit Objects" +__author__ = "CoolCat467" +__version__ = "0.0.0" +__license__ = "GNU General Public License Version 3" + + +from typing import TYPE_CHECKING + +from checkers import element_list, objects +from checkers.component import Event + +if TYPE_CHECKING: + import pygame + + from checkers import sprite + + +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.text = "Return to Title" + self.visible = True + + async def handle_click( + self, + _: Event[sprite.PygameMouseButtonEventData], + ) -> None: + """Handle Click Event.""" + await self.raise_event( + Event("return_to_title", None, 2), + ) + + +class ConnectionElement(element_list.Element, objects.Button): + """Connection list element sprite.""" + + __slots__ = () + + def __init__( + self, + 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, + _: Event[sprite.PygameMouseButtonEventData], + ) -> None: + """Handle Click Event.""" + details = self.name + await self.raise_event( + Event("join_server", details, 2), + ) From de1d791968dc9ea6dcf8366f256583d6772cd1e3 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Mon, 21 Oct 2024 20:41:00 -0500 Subject: [PATCH 07/21] Statemachine also uses weak references --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 5a3d5ce..6c9386e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,6 +68,7 @@ exclude = [ "src/checkers/namedtuple_mod.py", "src/checkers/component.py", "src/checkers/multi_inherit.py", + "src/checkers/statemachine.py", ] [tool.mypy] From 6bded4305139d56528b2ca3d64edfa0f983c3393 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 19 Jan 2025 00:49:53 +0000 Subject: [PATCH 08/21] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/checkers/multi_inherit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/checkers/multi_inherit.py b/src/checkers/multi_inherit.py index 526eb18..687c22d 100644 --- a/src/checkers/multi_inherit.py +++ b/src/checkers/multi_inherit.py @@ -1,10 +1,10 @@ -"""Objects that inherit from multipls base classes because mypyc is dumb.""" +"""Objects that inherit from multiples base classes because mypyc is dumb.""" # Programmed by CoolCat467 from __future__ import annotations -# Objects that inherit from multipls base classes because mypyc is dumb +# Objects that inherit from multiples base classes because mypyc is dumb # Copyright (C) 2024 CoolCat467 # # This program is free software: you can redistribute it and/or modify From 52a0aad060a7b69e6ff2ed56786e5bf9064f203a Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sat, 18 Jan 2025 19:25:59 -0600 Subject: [PATCH 09/21] Fix spelling issue --- src/checkers/multi_inherit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/checkers/multi_inherit.py b/src/checkers/multi_inherit.py index 687c22d..4241761 100644 --- a/src/checkers/multi_inherit.py +++ b/src/checkers/multi_inherit.py @@ -1,10 +1,10 @@ -"""Objects that inherit from multiples base classes because mypyc is dumb.""" +"""Objects that inherit from multiple base classes because mypyc is dumb.""" # Programmed by CoolCat467 from __future__ import annotations -# Objects that inherit from multiples base classes because mypyc is dumb +# Objects that inherit from multiple base classes because mypyc is dumb # Copyright (C) 2024 CoolCat467 # # This program is free software: you can redistribute it and/or modify From ccd195219ddc5062121cb643eb568f8137e62ba5 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sat, 18 Jan 2025 19:30:00 -0600 Subject: [PATCH 10/21] Update requirements --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 28c6593..d1b6b08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ keywords = [ "checkers-game", "ai-support", "networked-game" ] dependencies = [ - "libcomponent~=0.0.0", + "libcomponent~=0.0.1", "pygame~=2.6.0", "typing_extensions>=4.12.2", "mypy_extensions>=1.0.0", @@ -73,7 +73,7 @@ tools = [ [tool.hatch.build.targets.wheel.hooks.mypyc] dependencies = [ "hatch-mypyc>=0.16.0", - "mypy==1.11.2", + "mypy>=1.14.1", ] require-runtime-dependencies = true exclude = [ From 66253aa98e9963c3192e83d7a48eb1c8a76baeaf Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sat, 18 Jan 2025 19:36:44 -0600 Subject: [PATCH 11/21] `component` -> `libcomponent` --- pyproject.toml | 3 --- src/checkers/multi_inherit.py | 5 +++-- uv.lock | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d1b6b08..db692be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,10 +78,7 @@ dependencies = [ require-runtime-dependencies = true exclude = [ "src/checkers/vector.py", - "src/checkers/base_io.py", - "src/checkers/buffer.py", "src/checkers/namedtuple_mod.py", - "src/checkers/component.py", "src/checkers/multi_inherit.py", "src/checkers/statemachine.py", ] diff --git a/src/checkers/multi_inherit.py b/src/checkers/multi_inherit.py index 4241761..b72328e 100644 --- a/src/checkers/multi_inherit.py +++ b/src/checkers/multi_inherit.py @@ -28,8 +28,9 @@ from typing import TYPE_CHECKING -from checkers import element_list, objects -from checkers.component import Event +from libcomponent.component import Event + +from checkers import element_list, objects, sprite if TYPE_CHECKING: import pygame diff --git a/uv.lock b/uv.lock index e608ed7..08b3575 100644 --- a/uv.lock +++ b/uv.lock @@ -134,7 +134,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'" }, - { name = "libcomponent", specifier = "~=0.0.0" }, + { name = "libcomponent", specifier = "~=0.0.1" }, { name = "mypy", marker = "extra == 'tests'", specifier = ">=1.14.1" }, { name = "mypy-extensions", specifier = ">=1.0.0" }, { name = "pygame", specifier = "~=2.6.0" }, From 04570dc4fe21ed169852438035a8251e1d5f0445 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Mon, 28 Jul 2025 23:02:30 -0500 Subject: [PATCH 12/21] Move some code around to support mypyc --- pyproject.toml | 33 +- src/checkers/server.py | 46 +-- src/checkers/server_state.py | 86 +++++ src/checkers/state.py | 63 ++-- .../checkers_minimax.py | 312 ++++++++++++++++++ src/checkers_computer_players/minimax.py | 40 ++- src/checkers_computer_players/minimax_ai.py | 299 ++--------------- 7 files changed, 513 insertions(+), 366 deletions(-) create mode 100644 src/checkers/server_state.py create mode 100644 src/checkers_computer_players/checkers_minimax.py diff --git a/pyproject.toml b/pyproject.toml index ea1bc14..3ae6f90 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,19 +74,46 @@ tools = [ "attrs>=25.3.0", ] +[tool.hatch.build.targets.wheel] +packages = ["src/checkers", "src/checkers_computer_players"] + [tool.hatch.build.targets.wheel.hooks.mypyc] dependencies = [ "hatch-mypyc>=0.16.0", - "mypy>=1.14.1", + "mypy>=1.17.0", ] require-runtime-dependencies = true +include = [ + "src/checkers/base2d.py", + "src/checkers/network_shared.py", + "src/checkers/sound.py", + "src/checkers/state.py", + "src/checkers/server_state.py", + "src/checkers_computer_players/minimax.py", + "src/checkers_computer_players/checkers_minimax.py", +] exclude = [ - "src/checkers/vector.py", - "src/checkers/namedtuple_mod.py", + "src/checkers/client.py", + "src/checkers/element_list.py", + "src/checkers/game.py", "src/checkers/multi_inherit.py", + "src/checkers/namedtuple_mod.py", + "src/checkers/objects.py", + "src/checkers/server.py", + "src/checkers/sprite.py", "src/checkers/statemachine.py", + "src/checkers/vector.py", + "src/checkers_computer_players/example_ai.py", + "src/checkers_computer_players/max_y_jumper_ai.py", + "src/checkers_computer_players/machine_client.py", + "src/checkers_computer_players/minimax_ai.py", ] +[tool.hatch.build.targets.wheel.hooks.mypyc.options] +# 2025-07-28 Mypy v1.17.0 Compilation fails if I increase this value +opt_level = "1" +#multi_file = true + [tool.uv] package = true diff --git a/src/checkers/server.py b/src/checkers/server.py index 9465615..780d0da 100755 --- a/src/checkers/server.py +++ b/src/checkers/server.py @@ -56,7 +56,8 @@ read_position, write_position, ) -from checkers.state import State, generate_pieces +from checkers.server_state import CheckersState +from checkers.state import generate_pieces if TYPE_CHECKING: from collections.abc import Awaitable, Callable, Iterable @@ -319,49 +320,6 @@ async def start_encryption_request(self) -> None: await self.handle_encryption_response(event) -class CheckersState(State): - """Subclass of State that keeps track of actions in `action_queue`.""" - - __slots__ = ("action_queue",) - - def __init__( - self, - size: Pos, - pieces: dict[Pos, int], - turn: bool = True, - ) -> None: - """Initialize Checkers State.""" - super().__init__(size, pieces, turn) - self.action_queue: deque[tuple[str, Iterable[Pos | int]]] = deque() - - def piece_kinged(self, piece_pos: Pos, new_type: int) -> None: - """Add king event to action queue.""" - super().piece_kinged(piece_pos, new_type) - self.action_queue.append(("king", (piece_pos, new_type))) - - def piece_moved(self, start_pos: Pos, end_pos: Pos) -> None: - """Add move event to action queue.""" - super().piece_moved(start_pos, end_pos) - self.action_queue.append( - ( - "move", - ( - start_pos, - end_pos, - ), - ), - ) - - def piece_jumped(self, jumped_piece_pos: Pos) -> None: - """Add jump event to action queue.""" - super().piece_jumped(jumped_piece_pos) - self.action_queue.append(("jump", (jumped_piece_pos,))) - - def get_action_queue(self) -> deque[tuple[str, Iterable[Pos | int]]]: - """Return action queue.""" - return self.action_queue - - class GameServer(network.Server): """Checkers server. diff --git a/src/checkers/server_state.py b/src/checkers/server_state.py new file mode 100644 index 0000000..8cf14a0 --- /dev/null +++ b/src/checkers/server_state.py @@ -0,0 +1,86 @@ +"""Server State.""" + +# Programmed by CoolCat467 + +from __future__ import annotations + +# Server State +# Copyright (C) 2023-2025 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 State" +__author__ = "CoolCat467" +__version__ = "0.0.0" +__license__ = "GNU General Public License Version 3" + + +from collections import deque +from typing import TYPE_CHECKING + +from checkers.state import State + +if TYPE_CHECKING: + from collections.abc import Iterable + + from mypy_extensions import u8 + + from checkers.network_shared import Pos + + +class CheckersState(State): + """Subclass of State that keeps track of actions in `action_queue`.""" + + __slots__ = ("action_queue",) + + def __init__( + self, + size: Pos, + pieces: dict[Pos, u8], + turn: bool = True, + ) -> None: + """Initialize Checkers State.""" + super().__init__(size, pieces, turn) + self.action_queue: deque[tuple[str, Iterable[Pos | u8]]] = deque() + + def piece_kinged(self, piece_pos: Pos, new_type: u8) -> None: + """Add king event to action queue.""" + super().piece_kinged(piece_pos, new_type) + self.action_queue.append(("king", (piece_pos, new_type))) + + def piece_moved(self, start_pos: Pos, end_pos: Pos) -> None: + """Add move event to action queue.""" + super().piece_moved(start_pos, end_pos) + self.action_queue.append( + ( + "move", + ( + start_pos, + end_pos, + ), + ), + ) + + def piece_jumped(self, jumped_piece_pos: Pos) -> None: + """Add jump event to action queue.""" + super().piece_jumped(jumped_piece_pos) + self.action_queue.append(("jump", (jumped_piece_pos,))) + + def get_action_queue(self) -> deque[tuple[str, Iterable[Pos | u8]]]: + """Return action queue.""" + return self.action_queue + + +if __name__ == "__main__": + print(f"{__title__} v{__version__}\nProgrammed by {__author__}.\n") diff --git a/src/checkers/state.py b/src/checkers/state.py index 6079b32..7c3069d 100644 --- a/src/checkers/state.py +++ b/src/checkers/state.py @@ -34,7 +34,7 @@ TypeVar, ) -from mypy_extensions import u8 +from mypy_extensions import i16, u8 if TYPE_CHECKING: from collections.abc import Callable, Generator, Iterable @@ -76,13 +76,15 @@ class ActionSet(NamedTuple): 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 = [] + cx_i16 = i16(cx) + cy_i16 = i16(cy) + sides: list[Pos] = [] for raw_dy in range(2): - dy = raw_dy * 2 - 1 - ny = cy + dy + dy: i16 = raw_dy * 2 - 1 + ny: u8 = u8(cy_i16 + dy) for raw_dx in range(2): dx = raw_dx * 2 - 1 - nx = cx + dx + nx = u8(cx_i16 + dx) sides.append((nx, ny)) tuple_sides = tuple(sides) assert len(tuple_sides) == 4 @@ -109,8 +111,8 @@ def pawn_modify(moves: tuple[T, ...], piece_type: u8) -> tuple[T, ...]: class State: """Represents state of checkers game.""" - size: tuple[int, int] - pieces: dict[Pos, int] + size: tuple[u8, u8] + pieces: dict[Pos, u8] turn: bool = True # Black moves first def __str__(self) -> str: @@ -121,7 +123,7 @@ def __str__(self) -> str: for y in range(h): line = [] for x in range(w): - if (x + y + 1) % 2: + if (x + y + 1) % 2 != 0: # line.append("_") line.append(" ") continue @@ -148,7 +150,7 @@ def calculate_actions(self, position: Pos) -> ActionSet: ends.update(moves) return ActionSet(jumps, moves, ends) - def piece_kinged(self, piece_pos: Pos, new_type: int) -> None: + def piece_kinged(self, piece_pos: Pos, new_type: u8) -> None: """Piece kinged.""" # print(f'piece_kinged {piece = }') @@ -210,7 +212,7 @@ def preform_action(self, action: Action) -> Self: not self.turn, ) - def get_tile_name(self, x: int, y: int) -> str: + def get_tile_name(self, x: u8, y: u8) -> str: """Return name of a given tile.""" return chr(65 + x) + str(self.size[1] - y) @@ -230,21 +232,21 @@ def valid_location(self, position: Pos) -> bool: 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: + def does_piece_king(self, piece_type: u8, 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: + def get_enemy(self_type: u8) -> u8: """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]: + def get_piece_types(self_type: u8) -> tuple[u8, u8]: """Return piece types of given piece type.""" # If we are kinged, get a pawn version of ourselves. self_pawn = self_type % 2 @@ -253,9 +255,9 @@ def get_piece_types(self_type: int) -> tuple[int, int]: def get_jumps( self, position: Pos, - piece_type: int | None = None, - _pieces: dict[Pos, int] | None = None, - _recursion: int = 0, + piece_type: u8 | None = None, + _pieces: dict[Pos, u8] | None = None, + _recursion: u8 = 0, ) -> dict[Pos, list[Pos]]: """Return valid jumps a piece can make. @@ -325,19 +327,20 @@ def get_jumps( # 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): + next_recursion = _recursion + 1 + if next_recursion > 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 + next_recursion = 0 add_valid = self.get_jumps( end_tile, piece_type_copy, _pieces=_pieces, - _recursion=_recursion + 1, + _recursion=next_recursion, ) # For each key in the new dictionary of valid tile's keys, for end_pos, jumped_pieces in add_valid.items(): @@ -383,7 +386,7 @@ def get_actions(self, position: Pos) -> Generator[Action, None, None]: for end in ends: yield self.action_from_points(position, end) - def get_all_actions(self, player: int) -> Generator[Action, None, None]: + def get_all_actions(self, player: u8) -> Generator[Action, None, None]: """Yield all actions for given player.""" player_pieces = {player, player + 2} if not MANDATORY_CAPTURE: @@ -408,7 +411,7 @@ def get_all_actions(self, player: int) -> Generator[Action, None, None]: continue yield from self.wrap_actions(position, self.get_moves) - def check_for_win(self) -> int | None: + def check_for_win(self) -> u8 | None: """Return player number if they won else None.""" # For each of the two players, for player in range(2): @@ -424,25 +427,25 @@ def check_for_win(self) -> int | None: return (player + 1) % 2 return None - def can_player_select_piece(self, player: int, tile_pos: Pos) -> bool: + def can_player_select_piece(self, player: u8, 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], ...]: + def get_pieces(self) -> tuple[tuple[Pos, u8], ...]: """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]: + board_width: u8, + board_height: u8, + colors: u8 = 2, +) -> dict[Pos, u8]: """Generate data about each piece.""" - pieces: dict[Pos, int] = {} + pieces: dict[Pos, u8] = {} # 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 @@ -453,8 +456,8 @@ def generate_pieces( # 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)): + if (color == 0) 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) + piece_type = u8(y <= z_to_1) pieces[x, y] = piece_type return pieces diff --git a/src/checkers_computer_players/checkers_minimax.py b/src/checkers_computer_players/checkers_minimax.py new file mode 100644 index 0000000..73253af --- /dev/null +++ b/src/checkers_computer_players/checkers_minimax.py @@ -0,0 +1,312 @@ +"""Checkers Minimax.""" + +# Programmed by CoolCat467 + +from __future__ import annotations + +# Checkers Minimax +# Copyright (C) 2024-2025 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 Minimax" +__author__ = "CoolCat467" +__version__ = "0.0.0" +__license__ = "GNU General Public License Version 3" + + +import math +import random +import time +from collections import Counter +from math import inf as infinity +from typing import TYPE_CHECKING, Any, TypeVar + +from checkers.state import Action, State +from checkers_computer_players.minimax import ( + Minimax, + MinimaxResult, + Player, +) + +if TYPE_CHECKING: + from collections.abc import Iterable + + from mypy_extensions import u8 + +T = TypeVar("T") + +# Player: +# 0 = False = Person = MIN = 0, 2 +# 1 = True = AI (Us) = MAX = 1, 3 + + +class MinimaxWithTT(Minimax[State, Action]): + """Minimax with transposition table.""" + + __slots__ = ("transposition_table",) + + # Simple Transposition Table: + # key → (stored_depth, result, flag) + # flag: 'EXACT', 'LOWERBOUND', 'UPPERBOUND' + def __init__(self) -> None: + """Initialize this object.""" + super().__init__() + + self.transposition_table: dict[ + int, + tuple[u8, MinimaxResult[Any], str], + ] = {} + + def _transposition_table_lookup( + self, + state_hash: int, + depth: u8, + alpha: float, + beta: float, + ) -> MinimaxResult[Action] | None: + """Lookup in transposition_table. Return (value, action) or None.""" + entry = self.transposition_table.get(state_hash) + if entry is None: + return None + + stored_depth, result, flag = entry + # only use if stored depth is deep enough + if stored_depth >= depth and ( + (flag == "EXACT") + or (flag == "LOWERBOUND" and result.value > alpha) + or (flag == "UPPERBOUND" and result.value < beta) + ): + return result + return None + + def _transposition_table_store( + self, + state_hash: int, + depth: u8, + result: MinimaxResult[Action], + alpha: float, + beta: float, + ) -> None: + """Store in transposition_table with proper flag.""" + if result.value <= alpha: + flag = "UPPERBOUND" + elif result.value >= beta: + flag = "LOWERBOUND" + else: + flag = "EXACT" + self.transposition_table[state_hash] = (depth, result, flag) + + @classmethod + def hash_state(cls, state: State) -> int: + """Your state-to-hash function. Must be consistent.""" + # For small games you might do: return hash(state) + # For larger, use Zobrist or custom. + return hash(state) + + def alphabeta_transposition_table( + self, + state: State, + depth: u8 = 5, + a: float = -infinity, + b: float = infinity, + ) -> MinimaxResult[Action]: + """AlphaBeta with transposition table.""" + if self.terminal(state): + return MinimaxResult(self.value(state), None) + if depth <= 0: + # Choose a random action + # No need for cryptographic secure random + return MinimaxResult( + self.value(state), + random.choice(tuple(self.actions(state))), # noqa: S311 + ) + next_down = depth - 1 + + state_h = self.hash_state(state) + # 1) Try transposition_table lookup + transposition_table_hit = self._transposition_table_lookup( + state_h, + depth, + a, + b, + ) + if transposition_table_hit is not None: + return transposition_table_hit + next_down = None if depth is None else depth - 1 + + current_player = self.player(state) + value: float + + best_action: Action | None = None + + if current_player == Player.MAX: + value = -infinity + for action in self.actions(state): + child = self.alphabeta_transposition_table( + self.result(state, action), + next_down, + a, + b, + ) + if child.value > value: + value = child.value + best_action = action + a = max(a, value) + if a >= b: + break + + elif current_player == Player.MIN: + value = infinity + for action in self.actions(state): + child = self.alphabeta_transposition_table( + self.result(state, action), + next_down, + a, + b, + ) + if child.value < value: + value = child.value + best_action = action + b = min(b, value) + if b <= a: + break + else: + raise NotImplementedError(f"{current_player = }") + + # 2) Store in transposition_table + result = MinimaxResult(value, best_action) + self._transposition_table_store(state_h, depth, result, a, b) + return result + + def iterative_deepening( + self, + state: State, + start_depth: u8 = 5, + max_depth: u8 = 7, + time_limit_ns: int | float | None = None, + ) -> MinimaxResult[Action]: + """Run alpha-beta with increasing depth up to max_depth. + + If time_limit_ns is None, do all depths. Otherwise stop early. + """ + best_result: MinimaxResult[Action] = MinimaxResult(0.0, None) + start_t = time.perf_counter_ns() + + for depth in range(start_depth, max_depth + 1): + # clear or keep transposition_table between depths? often you keep it + # self.transposition_table.clear() + + result = self.alphabeta_transposition_table( + state, + depth, + ) + best_result = result + + # Optional: if you find a forced win/loss you can stop + if abs(result.value) == self.HIGHEST: + print(f"reached terminal state stop {depth=}") + break + + # optional time check + if ( + time_limit_ns + and (time.perf_counter_ns() - start_t) > time_limit_ns + ): + print( + f"break from time expired {depth=} ({(time.perf_counter_ns() - start_t) / 1e9} seconds elaped)", + ) + break + print( + f"{depth=} ({(time.perf_counter_ns() - start_t) / 1e9} seconds elaped)", + ) + + return best_result + + +# Minimax[State, Action] +class CheckersMinimax(MinimaxWithTT): + """Minimax Algorithm for Checkers.""" + + __slots__ = () + + @classmethod + def hash_state(cls, state: State) -> int: + """Return state hash value.""" + # For small games you might do: return hash(state) + # For larger, use Zobrist or custom. + return hash((state.size, tuple(state.pieces.items()), state.turn)) + + @classmethod + def value(cls, state: State) -> float: + """Return value of given game state.""" + # Return winner if possible + win = state.check_for_win() + # If no winner, we have to predict the value + if win is None: + # We'll estimate the value by the pieces in play + counts = Counter(state.pieces.values()) + # Score is pawns plus 3 times kings + min_ = counts[0] + 3 * counts[2] + max_ = counts[1] + 3 * counts[3] + # 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 float(win) * 2.0 - 1.0 + + @classmethod + def terminal(cls, state: State) -> bool: + """Return if game state is terminal.""" + return state.check_for_win() is not None + + @classmethod + def player(cls, state: State) -> Player: + """Return Player enum from current state's turn.""" + return Player.MAX if state.get_turn() else Player.MIN + + @classmethod + def actions(cls, state: State) -> Iterable[Action]: + """Return all actions that are able to be performed for the current player in the given state.""" + return state.get_all_actions(int(state.get_turn())) + + @classmethod + def result(cls, state: State, action: Action) -> State: + """Return new state after performing given action on given current state.""" + return state.preform_action(action) + + @classmethod + def adaptive_depth_minimax( + cls, + state: State, + minimum: int, + maximum: int, + ) -> MinimaxResult[Action]: + """Return minimax result from adaptive max depth.""" + ## types = state.pieces.values() + ## current = len(types) + ## w, h = state.size + ## max_count = w * h // 6 << 1 + ## old_depth = (1 - (current / max_count)) * math.floor( + ## math.sqrt(w**2 + h**2) + ## ) + + depth = cls.value(state) * maximum + minimum + final_depth = min(maximum, max(minimum, math.floor(depth))) + print(f"{depth = } {final_depth = }") + return cls.minimax(state, final_depth) + + +if __name__ == "__main__": + print(f"{__title__} v{__version__}\nProgrammed by {__author__}.\n") diff --git a/src/checkers_computer_players/minimax.py b/src/checkers_computer_players/minimax.py index c0de284..08bac3a 100644 --- a/src/checkers_computer_players/minimax.py +++ b/src/checkers_computer_players/minimax.py @@ -13,11 +13,13 @@ from abc import ABC, abstractmethod from enum import IntEnum, auto from math import inf as infinity -from typing import TYPE_CHECKING, Generic, NamedTuple, TypeVar +from typing import TYPE_CHECKING, ClassVar, Generic, NamedTuple, TypeVar if TYPE_CHECKING: from collections.abc import Callable, Iterable + from mypy_extensions import u8 + class Player(IntEnum): """Enum for player status.""" @@ -35,7 +37,7 @@ class Player(IntEnum): class MinimaxResult(NamedTuple, Generic[Action]): """Minimax Result.""" - value: int | float + value: float action: Action | None @@ -44,12 +46,12 @@ class Minimax(ABC, Generic[State, Action]): __slots__ = () - LOWEST = -1 - HIGHEST = 1 + LOWEST: ClassVar[float] = -1.0 + HIGHEST: ClassVar[float] = 1.0 @classmethod @abstractmethod - def value(cls, state: State) -> int | float: + def value(cls, state: State) -> float: """Return the value of a given game state. Should be in range [cls.LOWEST, cls.HIGHEST]. @@ -91,7 +93,7 @@ def probability(cls, action: Action) -> float: def minimax( cls, state: State, - depth: int | None = 5, + depth: u8 | None = 5, ) -> MinimaxResult[Action]: """Return minimax result best action for a given state for the current player.""" if cls.terminal(state): @@ -106,8 +108,8 @@ def minimax( next_down = None if depth is None else depth - 1 current_player = cls.player(state) - value: int | float - best: Callable[[int | float, int | float], int | float] + value: float + best: Callable[[float, float], float] if current_player == Player.MAX: value = -infinity best = max @@ -115,7 +117,7 @@ def minimax( value = infinity best = min elif current_player == Player.CHANCE: - value = 0 + value = 0.0 best = sum # type: ignore[assignment] else: raise ValueError(f"Unexpected player type {current_player!r}") @@ -123,8 +125,9 @@ def minimax( best_action: Action | None = None for action in cls.actions(state): result = cls.minimax(cls.result(state, action), next_down) - result_value = result.value + result_value: float = result.value if current_player == Player.CHANCE: + result_value = float(result_value) # Probability[action] result_value *= cls.probability(action) new_value = best(value, result_value) @@ -137,9 +140,9 @@ def minimax( def alphabeta( cls, state: State, - depth: int | None = 5, - a: int | float = -infinity, - b: int | float = infinity, + depth: u8 | None = 5, + a: float = -infinity, + b: float = infinity, ) -> MinimaxResult[Action]: """Return minimax alphabeta pruning result best action for given current state.""" # print(f'alphabeta {depth = } {a = } {b = }') @@ -156,8 +159,9 @@ def alphabeta( next_down = None if depth is None else depth - 1 current_player = cls.player(state) - value: int | float + value: float best: Callable[[int | float, int | float], int | float] + set_idx: u8 if current_player == Player.MAX: value = -infinity best = max @@ -169,15 +173,15 @@ def alphabeta( compare = operator.lt # less than (<) set_idx = 1 elif current_player == Player.CHANCE: - value = 0 + value = 0.0 best = sum # type: ignore[assignment] 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 + expect_a = successors * (float(a) - cls.HIGHEST) + cls.HIGHEST + expect_b = successors * (float(b) - cls.LOWEST) + cls.LOWEST best_action: Action | None = None for action in actions: @@ -192,7 +196,7 @@ def alphabeta( ax, bx, ) - score = result.value + score = float(result.value) # Check for a, b cutoff conditions if score <= expect_a: return MinimaxResult(a, None) diff --git a/src/checkers_computer_players/minimax_ai.py b/src/checkers_computer_players/minimax_ai.py index d955c6e..54b1ff7 100755 --- a/src/checkers_computer_players/minimax_ai.py +++ b/src/checkers_computer_players/minimax_ai.py @@ -3,35 +3,41 @@ """Minimax Checkers AI.""" +# Programmed by CoolCat467 + from __future__ import annotations -# Programmed by CoolCat467 +# Minimax Checkers AI +# Copyright (C) 2024-2025 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__ = "Minimax AI" __author__ = "CoolCat467" __version__ = "0.0.0" -import math -import random -import time import traceback -from collections import Counter -from math import inf as infinity -from typing import TYPE_CHECKING, Any, ClassVar, TypeVar +from typing import TYPE_CHECKING, TypeVar -from checkers.state import Action, State +from checkers_computer_players.checkers_minimax import CheckersMinimax from checkers_computer_players.machine_client import ( RemoteState, run_clients_in_local_servers_sync, ) -from checkers_computer_players.minimax import ( - Minimax, - MinimaxResult, - Player, -) if TYPE_CHECKING: - from collections.abc import Iterable + from checkers.state import Action T = TypeVar("T") @@ -40,265 +46,16 @@ # 1 = True = AI (Us) = MAX = 1, 3 -class MinimaxWithID(Minimax[State, Action]): - """Minimax with ID.""" - - __slots__ = () - - # Simple Transposition Table: - # key → (stored_depth, value, action, flag) - # flag: 'EXACT', 'LOWERBOUND', 'UPPERBOUND' - TRANSPOSITION_TABLE: ClassVar[ - dict[int, tuple[int, MinimaxResult[Any], str]] - ] = {} - - @classmethod - def _transposition_table_lookup( - cls, - state_hash: int, - depth: int, - alpha: float, - beta: float, - ) -> MinimaxResult[Action] | None: - """Lookup in transposition_table. Return (value, action) or None.""" - entry = cls.TRANSPOSITION_TABLE.get(state_hash) - if entry is None: - return None - - stored_depth, result, flag = entry - # only use if stored depth is deep enough - if stored_depth >= depth and ( - (flag == "EXACT") - or (flag == "LOWERBOUND" and result.value > alpha) - or (flag == "UPPERBOUND" and result.value < beta) - ): - return result - return None - - @classmethod - def _transposition_table_store( - cls, - state_hash: int, - depth: int, - result: MinimaxResult[Action], - alpha: float, - beta: float, - ) -> None: - """Store in transposition_table with proper flag.""" - if result.value <= alpha: - flag = "UPPERBOUND" - elif result.value >= beta: - flag = "LOWERBOUND" - else: - flag = "EXACT" - cls.TRANSPOSITION_TABLE[state_hash] = (depth, result, flag) - - @classmethod - def hash_state(cls, state: State) -> int: - """Your state-to-hash function. Must be consistent.""" - # For small games you might do: return hash(state) - # For larger, use Zobrist or custom. - return hash(state) - - @classmethod - def alphabeta_transposition_table( - cls, - state: State, - depth: int = 5, - a: int | float = -infinity, - b: int | float = infinity, - ) -> MinimaxResult[Action]: - """AlphaBeta with transposition table.""" - if cls.terminal(state): - return MinimaxResult(cls.value(state), None) - if 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 = depth - 1 - - state_h = cls.hash_state(state) - # 1) Try transposition_table lookup - transposition_table_hit = cls._transposition_table_lookup( - state_h, - depth, - a, - b, - ) - if transposition_table_hit is not None: - return transposition_table_hit - next_down = None if depth is None else depth - 1 - - current_player = cls.player(state) - value: int | float - - best_action: Action | None = None - - if current_player == Player.MAX: - value = -infinity - for action in cls.actions(state): - child = cls.alphabeta_transposition_table( - cls.result(state, action), - next_down, - a, - b, - ) - if child.value > value: - value = child.value - best_action = action - a = max(a, value) - if a >= b: - break - - elif current_player == Player.MIN: - value = infinity - for action in cls.actions(state): - child = cls.alphabeta_transposition_table( - cls.result(state, action), - next_down, - a, - b, - ) - if child.value < value: - value = child.value - best_action = action - b = min(b, value) - if b <= a: - break - else: - raise NotImplementedError(f"{current_player = }") - - # 2) Store in transposition_table - result = MinimaxResult(value, best_action) - cls._transposition_table_store(state_h, depth, result, a, b) - return result - - @classmethod - def iterative_deepening( - cls, - state: State, - start_depth: int = 5, - max_depth: int = 7, - time_limit_ns: int | float | None = None, - ) -> MinimaxResult[Action]: - """Run alpha-beta with increasing depth up to max_depth. - - If time_limit_ns is None, do all depths. Otherwise stop early. - """ - best_result: MinimaxResult[Action] = MinimaxResult(0, None) - start_t = time.perf_counter_ns() - - for depth in range(start_depth, max_depth + 1): - # clear or keep transposition_table between depths? often you keep it - # cls.TRANSPOSITION_TABLE.clear() - - result = cls.alphabeta_transposition_table( - state, - depth, - ) - best_result = result - - # Optional: if you find a forced win/loss you can stop - if abs(result.value) == cls.HIGHEST: - print(f"reached terminal state stop {depth=}") - break - - # optional time check - if ( - time_limit_ns - and (time.perf_counter_ns() - start_t) > time_limit_ns - ): - print( - f"break from time expired {depth=} ({(time.perf_counter_ns() - start_t) / 1e9} seconds elaped)", - ) - break - print( - f"{depth=} ({(time.perf_counter_ns() - start_t) / 1e9} seconds elaped)", - ) - - return best_result - - -# Minimax[State, Action] -class CheckersMinimax(MinimaxWithID): - """Minimax Algorithm for Checkers.""" - - __slots__ = () - - @classmethod - def hash_state(cls, state: State) -> int: - """Return state hash value.""" - # For small games you might do: return hash(state) - # For larger, use Zobrist or custom. - return hash((state.size, tuple(state.pieces.items()), state.turn)) - - @staticmethod - def value(state: State) -> int | float: - """Return value of given game state.""" - # Return winner if possible - win = state.check_for_win() - # If no winner, we have to predict the value - if win is None: - # We'll estimate the value by the pieces in play - counts = Counter(state.pieces.values()) - # Score is pawns plus 3 times kings - min_ = counts[0] + 3 * counts[2] - max_ = counts[1] + 3 * counts[3] - # 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 win * 2 - 1 - - @staticmethod - def terminal(state: State) -> bool: - """Return if game state is terminal.""" - return state.check_for_win() is not None - - @staticmethod - def player(state: State) -> Player: - """Return Player enum from current state's turn.""" - return Player.MAX if state.get_turn() 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.""" - return state.get_all_actions(int(state.get_turn())) - - @staticmethod - def result(state: State, action: Action) -> State: - """Return new state after performing given action on given current state.""" - return state.preform_action(action) - - @classmethod - def adaptive_depth_minimax( - cls, - state: State, - minimum: int, - maximum: int, - ) -> MinimaxResult[Action]: - """Return minimax result from adaptive max depth.""" - ## types = state.pieces.values() - ## current = len(types) - ## w, h = state.size - ## max_count = w * h // 6 << 1 - ## old_depth = (1 - (current / max_count)) * math.floor( - ## math.sqrt(w**2 + h**2) - ## ) - - depth = cls.value(state) * maximum + minimum - final_depth = min(maximum, max(minimum, math.floor(depth))) - print(f"{depth = } {final_depth = }") - return cls.minimax(state, final_depth) - - class MinimaxPlayer(RemoteState): """Minimax Player.""" - __slots__ = () + __slots__ = ("minimax",) + + def __init__(self) -> None: + """Initialize minimax player.""" + super().__init__() + + self.minimax = CheckersMinimax() async def preform_turn(self) -> Action: """Perform turn.""" @@ -308,7 +65,7 @@ async def preform_turn(self) -> Action: ##) ##value, action = CheckersMinimax.minimax(self.state, 4) ##value, action = CheckersMinimax.alphabeta(self.state, 4) - value, action = CheckersMinimax.iterative_deepening( + value, action = self.minimax.iterative_deepening( self.state, 4, 20, From 1a2a1b83ce872f4930f6ba835a3fc1c48e6c3bdc Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Fri, 1 Aug 2025 00:40:46 -0500 Subject: [PATCH 13/21] Avoid mypyc compilation issue See https://github.com/python/mypy/issues/19557 for more information --- pyproject.toml | 5 ++--- src/checkers_computer_players/checkers_minimax.py | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3ae6f90..e66536d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -109,9 +109,8 @@ exclude = [ "src/checkers_computer_players/minimax_ai.py", ] -[tool.hatch.build.targets.wheel.hooks.mypyc.options] -# 2025-07-28 Mypy v1.17.0 Compilation fails if I increase this value -opt_level = "1" +#[tool.hatch.build.targets.wheel.hooks.mypyc.options] +#opt_level = "1" #multi_file = true [tool.uv] diff --git a/src/checkers_computer_players/checkers_minimax.py b/src/checkers_computer_players/checkers_minimax.py index 73253af..c487083 100644 --- a/src/checkers_computer_players/checkers_minimax.py +++ b/src/checkers_computer_players/checkers_minimax.py @@ -132,7 +132,6 @@ def alphabeta_transposition_table( self.value(state), random.choice(tuple(self.actions(state))), # noqa: S311 ) - next_down = depth - 1 state_h = self.hash_state(state) # 1) Try transposition_table lookup @@ -144,7 +143,8 @@ def alphabeta_transposition_table( ) if transposition_table_hit is not None: return transposition_table_hit - next_down = None if depth is None else depth - 1 + + next_down = depth - 1 current_player = self.player(state) value: float From 458e4f706b3caca35c67315b5f03243a493215a7 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Fri, 1 Aug 2025 21:48:19 -0500 Subject: [PATCH 14/21] Fix windows not having a socket attribute hopefully --- src/checkers/client.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/checkers/client.py b/src/checkers/client.py index 5e1ffb6..f9671dd 100644 --- a/src/checkers/client.py +++ b/src/checkers/client.py @@ -64,11 +64,12 @@ async def read_advertisements( # 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, - ) + if hasattr(trio.socket, "SO_REUSEADDR"): + udp_socket.setsockopt( + trio.socket.SOL_SOCKET, + trio.socket.SO_REUSEADDR, + 1, + ) await udp_socket.bind(("", ADVERTISEMENT_PORT)) @@ -101,7 +102,15 @@ async def read_advertisements( mreq, ) else: # IPv6 - mreq = group_bin + struct.pack("@I", 0) + # print( + # "\n".join( + # f"{iface_index}: {iface_name}" + # for iface_index, iface_name in trio.socket.if_nameindex() + # ) + # ) + # iface_index = socket.if_nametoindex(iface_name) + iface_index = 0 + mreq = group_bin + struct.pack("@I", iface_index) udp_socket.setsockopt( trio.socket.IPPROTO_IPV6, trio.socket.IPV6_JOIN_GROUP, From dd4abcbfc04d24722e5b8f51701149d5dade7dfa Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 19 Sep 2025 04:00:51 +0000 Subject: [PATCH 15/21] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/checkers_computer_players/minimax_ai.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/checkers_computer_players/minimax_ai.py b/src/checkers_computer_players/minimax_ai.py index 62a192a..4d3f4af 100755 --- a/src/checkers_computer_players/minimax_ai.py +++ b/src/checkers_computer_players/minimax_ai.py @@ -122,7 +122,7 @@ def alphabeta_transposition_table( # No need for cryptographic secure random return MinimaxResult( cls.value(state), - random.choice(tuple(cls.actions(state))), # noqa: S311 + random.choice(tuple(cls.actions(state))), ) next_down = depth - 1 From 4e90b6f0e4d5657563983a21e51fd7910eb445df Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Thu, 18 Sep 2025 23:08:18 -0500 Subject: [PATCH 16/21] Fix merge issues --- src/checkers_computer_players/checkers_minimax.py | 2 +- src/checkers_computer_players/minimax_ai.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/checkers_computer_players/checkers_minimax.py b/src/checkers_computer_players/checkers_minimax.py index c487083..1d609a2 100644 --- a/src/checkers_computer_players/checkers_minimax.py +++ b/src/checkers_computer_players/checkers_minimax.py @@ -284,7 +284,7 @@ def actions(cls, state: State) -> Iterable[Action]: @classmethod def result(cls, state: State, action: Action) -> State: """Return new state after performing given action on given current state.""" - return state.preform_action(action) + return state.perform_action(action) @classmethod def adaptive_depth_minimax( diff --git a/src/checkers_computer_players/minimax_ai.py b/src/checkers_computer_players/minimax_ai.py index 4d3f4af..2818799 100755 --- a/src/checkers_computer_players/minimax_ai.py +++ b/src/checkers_computer_players/minimax_ai.py @@ -27,17 +27,25 @@ __author__ = "CoolCat467" __version__ = "0.0.0" +import math +import random +import time import traceback -from typing import TYPE_CHECKING, TypeVar +from collections import Counter +from math import inf as infinity +from typing import TYPE_CHECKING, Any, ClassVar, TypeVar +from checkers.state import Action, State from checkers_computer_players.checkers_minimax import CheckersMinimax from checkers_computer_players.machine_client import ( RemoteState, run_clients_in_local_servers_sync, ) +from checkers_computer_players.minimax import Minimax, MinimaxResult, Player if TYPE_CHECKING: - from checkers.state import Action + from collections.abc import Iterable + T = TypeVar("T") From c88bfa95eb423b090c36bd2d2718bf4d0237b1f4 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Thu, 18 Sep 2025 23:24:13 -0500 Subject: [PATCH 17/21] Remove duplciated code and update mypy require to 1.18.2 --- pyproject.toml | 4 +- src/checkers/state.py | 2 +- src/checkers_computer_players/minimax_ai.py | 267 +------------------- 3 files changed, 5 insertions(+), 268 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e66536d..92fa425 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,7 @@ tests = [ "uv>=0.5.21", ] tools = [ - "mypy>=1.17.0", + "mypy>=1.18.2", "ruff>=0.9.2", "codespell>=2.3.0", "pre-commit>=4.2.0", @@ -80,7 +80,7 @@ packages = ["src/checkers", "src/checkers_computer_players"] [tool.hatch.build.targets.wheel.hooks.mypyc] dependencies = [ "hatch-mypyc>=0.16.0", - "mypy>=1.17.0", + "mypy>=1.18.2", ] require-runtime-dependencies = true include = [ diff --git a/src/checkers/state.py b/src/checkers/state.py index ff97d31..3dc7380 100644 --- a/src/checkers/state.py +++ b/src/checkers/state.py @@ -123,7 +123,7 @@ def __str__(self) -> str: for y in range(h): line = [] for x in range(w): - if (x + y + 1) % 2 != 0: + if (x + y + 1) & 1 != 0: # line.append("_") line.append(" ") continue diff --git a/src/checkers_computer_players/minimax_ai.py b/src/checkers_computer_players/minimax_ai.py index 2818799..d82268f 100755 --- a/src/checkers_computer_players/minimax_ai.py +++ b/src/checkers_computer_players/minimax_ai.py @@ -27,25 +27,17 @@ __author__ = "CoolCat467" __version__ = "0.0.0" -import math -import random -import time import traceback -from collections import Counter -from math import inf as infinity -from typing import TYPE_CHECKING, Any, ClassVar, TypeVar +from typing import TYPE_CHECKING, TypeVar -from checkers.state import Action, State from checkers_computer_players.checkers_minimax import CheckersMinimax from checkers_computer_players.machine_client import ( RemoteState, run_clients_in_local_servers_sync, ) -from checkers_computer_players.minimax import Minimax, MinimaxResult, Player if TYPE_CHECKING: - from collections.abc import Iterable - + from checkers.state import Action T = TypeVar("T") @@ -54,261 +46,6 @@ # 1 = True = AI (Us) = MAX = 1, 3 -class MinimaxWithID(Minimax[State, Action]): - """Minimax with ID.""" - - __slots__ = () - - # Simple Transposition Table: - # key → (stored_depth, value, action, flag) - # flag: 'EXACT', 'LOWERBOUND', 'UPPERBOUND' - TRANSPOSITION_TABLE: ClassVar[ - dict[int, tuple[int, MinimaxResult[Any], str]] - ] = {} - - @classmethod - def _transposition_table_lookup( - cls, - state_hash: int, - depth: int, - alpha: float, - beta: float, - ) -> MinimaxResult[Action] | None: - """Lookup in transposition_table. Return (value, action) or None.""" - entry = cls.TRANSPOSITION_TABLE.get(state_hash) - if entry is None: - return None - - stored_depth, result, flag = entry - # only use if stored depth is deep enough - if stored_depth >= depth and ( - (flag == "EXACT") - or (flag == "LOWERBOUND" and result.value > alpha) - or (flag == "UPPERBOUND" and result.value < beta) - ): - return result - return None - - @classmethod - def _transposition_table_store( - cls, - state_hash: int, - depth: int, - result: MinimaxResult[Action], - alpha: float, - beta: float, - ) -> None: - """Store in transposition_table with proper flag.""" - if result.value <= alpha: - flag = "UPPERBOUND" - elif result.value >= beta: - flag = "LOWERBOUND" - else: - flag = "EXACT" - cls.TRANSPOSITION_TABLE[state_hash] = (depth, result, flag) - - @classmethod - def hash_state(cls, state: State) -> int: - """Your state-to-hash function. Must be consistent.""" - # For small games you might do: return hash(state) - # For larger, use Zobrist or custom. - return hash(state) - - @classmethod - def alphabeta_transposition_table( - cls, - state: State, - depth: int = 5, - a: int | float = -infinity, - b: int | float = infinity, - ) -> MinimaxResult[Action]: - """AlphaBeta with transposition table.""" - if cls.terminal(state): - return MinimaxResult(cls.value(state), None) - if depth <= 0: - # Choose a random action - # No need for cryptographic secure random - return MinimaxResult( - cls.value(state), - random.choice(tuple(cls.actions(state))), - ) - next_down = depth - 1 - - state_h = cls.hash_state(state) - # 1) Try transposition_table lookup - transposition_table_hit = cls._transposition_table_lookup( - state_h, - depth, - a, - b, - ) - if transposition_table_hit is not None: - return transposition_table_hit - next_down = None if depth is None else depth - 1 - - current_player = cls.player(state) - value: int | float - - best_action: Action | None = None - - if current_player == Player.MAX: - value = -infinity - for action in cls.actions(state): - child = cls.alphabeta_transposition_table( - cls.result(state, action), - next_down, - a, - b, - ) - if child.value > value: - value = child.value - best_action = action - a = max(a, value) - if a >= b: - break - - elif current_player == Player.MIN: - value = infinity - for action in cls.actions(state): - child = cls.alphabeta_transposition_table( - cls.result(state, action), - next_down, - a, - b, - ) - if child.value < value: - value = child.value - best_action = action - b = min(b, value) - if b <= a: - break - else: - raise NotImplementedError(f"{current_player = }") - - # 2) Store in transposition_table - result = MinimaxResult(value, best_action) - cls._transposition_table_store(state_h, depth, result, a, b) - return result - - @classmethod - def iterative_deepening( - cls, - state: State, - start_depth: int = 5, - max_depth: int = 7, - time_limit_ns: int | float | None = None, - ) -> MinimaxResult[Action]: - """Run alpha-beta with increasing depth up to max_depth. - - If time_limit_ns is None, do all depths. Otherwise stop early. - """ - best_result: MinimaxResult[Action] = MinimaxResult(0, None) - start_t = time.perf_counter_ns() - - for depth in range(start_depth, max_depth + 1): - # clear or keep transposition_table between depths? often you keep it - # cls.TRANSPOSITION_TABLE.clear() - - result = cls.alphabeta_transposition_table( - state, - depth, - ) - best_result = result - - # Optional: if you find a forced win/loss you can stop - if abs(result.value) == cls.HIGHEST: - print(f"reached terminal state stop {depth=}") - break - - # optional time check - if ( - time_limit_ns - and (time.perf_counter_ns() - start_t) > time_limit_ns - ): - print( - f"break from time expired {depth=} ({(time.perf_counter_ns() - start_t) / 1e9} seconds elaped)", - ) - break - print( - f"{depth=} ({(time.perf_counter_ns() - start_t) / 1e9} seconds elaped)", - ) - - return best_result - - -# Minimax[State, Action] -class CheckersMinimax(MinimaxWithID): - """Minimax Algorithm for Checkers.""" - - __slots__ = () - - @classmethod - def hash_state(cls, state: State) -> int: - """Return state hash value.""" - # For small games you might do: return hash(state) - # For larger, use Zobrist or custom. - return hash((state.size, tuple(state.pieces.items()), state.turn)) - - @staticmethod - def value(state: State) -> int | float: - """Return value of given game state.""" - # Return winner if possible - win = state.check_for_win() - # If no winner, we have to predict the value - if win is None: - # We'll estimate the value by the pieces in play - counts = Counter(state.pieces.values()) - # Score is pawns plus 3 times kings - min_ = counts[0] + 3 * counts[2] - max_ = counts[1] + 3 * counts[3] - # 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 win * 2 - 1 - - @staticmethod - def terminal(state: State) -> bool: - """Return if game state is terminal.""" - return state.check_for_win() is not None - - @staticmethod - def player(state: State) -> Player: - """Return Player enum from current state's turn.""" - return Player.MAX if state.get_turn() 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.""" - return state.get_all_actions(int(state.get_turn())) - - @staticmethod - def result(state: State, action: Action) -> State: - """Return new state after performing given action on given current state.""" - return state.perform_action(action) - - @classmethod - def adaptive_depth_minimax( - cls, - state: State, - minimum: int, - maximum: int, - ) -> MinimaxResult[Action]: - """Return minimax result from adaptive max depth.""" - ## types = state.pieces.values() - ## current = len(types) - ## w, h = state.size - ## max_count = w * h // 6 << 1 - ## old_depth = (1 - (current / max_count)) * math.floor( - ## math.sqrt(w**2 + h**2) - ## ) - - depth = cls.value(state) * maximum + minimum - final_depth = min(maximum, max(minimum, math.floor(depth))) - print(f"{depth = } {final_depth = }") - return cls.minimax(state, final_depth) - - class MinimaxPlayer(RemoteState): """Minimax Player.""" From eacd4183fdb1a8df763ab666c941f25a4dba9063 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Thu, 18 Sep 2025 23:25:21 -0500 Subject: [PATCH 18/21] Update lockfile --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index b1551df..012c1a1 100644 --- a/uv.lock +++ b/uv.lock @@ -137,7 +137,7 @@ requires-dist = [ { name = "coverage", marker = "extra == 'tests'", specifier = ">=7.2.5" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "libcomponent", specifier = "~=0.0.3" }, - { name = "mypy", marker = "extra == 'tools'", specifier = ">=1.17.0" }, + { name = "mypy", marker = "extra == 'tools'", specifier = ">=1.18.2" }, { name = "mypy-extensions", specifier = ">=1.0.0" }, { name = "pre-commit", marker = "extra == 'tools'", specifier = ">=4.2.0" }, { name = "pygame", specifier = "~=2.6.0" }, From 170787c65b06079c13484240f1faa84b124e3f94 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Thu, 18 Sep 2025 23:54:55 -0500 Subject: [PATCH 19/21] Fix position of return button --- src/checkers/game.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/checkers/game.py b/src/checkers/game.py index 184da69..62bbe68 100644 --- a/src/checkers/game.py +++ b/src/checkers/game.py @@ -1308,7 +1308,7 @@ async def entry_actions(self) -> None: ) return_button = ReturnElement("return_button", return_font) return_button.outline = RED - return_button.location = (SCREEN_SIZE[0] // 2, 10) + return_button.location = (SCREEN_SIZE[0] // 2, 30) connections.add_element(return_button) self.manager.register_handlers( From e35d869aeb80ff62450280afa71193283b9a6aa3 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Fri, 19 Sep 2025 00:31:35 -0500 Subject: [PATCH 20/21] Fix mypyc issues --- pyproject.toml | 2 ++ src/checkers/game.py | 4 ++-- src/checkers/namedtuple_mod.py | 23 +++++++++++++---------- src/checkers/server.py | 2 +- src/checkers/state.py | 8 ++++---- tests/test_sprite.py | 2 +- tests/test_statemachine.py | 15 ++++++++------- 7 files changed, 31 insertions(+), 25 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 92fa425..9283cff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -201,6 +201,8 @@ extend-ignore = [ [tool.ruff.lint.per-file-ignores] "tests/*" = [ "D100", # undocumented-public-module + "D101", # undocumented-public-class + "D102", # undocumented-public-method "D103", # undocumented-public-function "D107", # undocumented-public-init ] diff --git a/src/checkers/game.py b/src/checkers/game.py index 62bbe68..7600051 100644 --- a/src/checkers/game.py +++ b/src/checkers/game.py @@ -692,7 +692,7 @@ def generate_tile_images(self) -> None: else: image.add_image_and_mask(name, surface, "tile_0") - if index % 2 != 0: + if index & 1 != 0: continue outline_color = GREEN @@ -713,7 +713,7 @@ def generate_tile_images(self) -> None: color, ) - if piece_type % 2 == 0: + if piece_type & 1 == 0: image.add_image(name, surface) else: image.add_image_and_mask( diff --git a/src/checkers/namedtuple_mod.py b/src/checkers/namedtuple_mod.py index a88bbd2..8611d8f 100644 --- a/src/checkers/namedtuple_mod.py +++ b/src/checkers/namedtuple_mod.py @@ -4,7 +4,10 @@ import typing +import mypy_extensions + +@mypy_extensions.mypyc_attr(native_class=False) class NamedTupleMeta(type): """NamedTuple Metaclass.""" @@ -62,13 +65,13 @@ def __new__( return nm_tpl -def apply_namedtuple_mod() -> None: # pragma: nocover - """Allow NamedTuple to subclass things other than just NamedTuple or Generic.""" - typing.NamedTupleMeta = NamedTupleMeta # type: ignore[attr-defined] - typing._NamedTuple = type.__new__(NamedTupleMeta, "NamedTuple", (), {}) # type: ignore[attr-defined] - - def _namedtuple_mro_entries(bases: tuple[type, ...]) -> tuple[type, ...]: - assert typing.NamedTuple in bases - return (typing._NamedTuple,) # type: ignore[attr-defined] - - typing.NamedTuple.__mro_entries__ = _namedtuple_mro_entries # type: ignore[attr-defined] +# def apply_namedtuple_mod() -> None: # pragma: nocover +# """Allow NamedTuple to subclass things other than just NamedTuple or Generic.""" +# typing.NamedTupleMeta = NamedTupleMeta # type: ignore[attr-defined] +# typing._NamedTuple = type.__new__(NamedTupleMeta, "NamedTuple", (), {}) # type: ignore[attr-defined] +# +# def _namedtuple_mro_entries(bases: tuple[type, ...]) -> tuple[type, ...]: +# assert typing.NamedTuple in bases +# return (typing._NamedTuple,) # type: ignore[attr-defined] +# +# typing.NamedTuple.__mro_entries__ = _namedtuple_mro_entries # type: ignore[attr-defined] diff --git a/src/checkers/server.py b/src/checkers/server.py index 2a8d380..0329fde 100755 --- a/src/checkers/server.py +++ b/src/checkers/server.py @@ -465,7 +465,7 @@ def setup_teams(client_ids: list[int]) -> dict[int, int]: players: dict[int, int] = {} for idx, client_id in enumerate(client_ids): if idx < 2: - players[client_id] = idx % 2 + players[client_id] = idx & 1 else: players[client_id] = 0xFF # Spectator return players diff --git a/src/checkers/state.py b/src/checkers/state.py index 3dc7380..d8d7637 100644 --- a/src/checkers/state.py +++ b/src/checkers/state.py @@ -243,13 +243,13 @@ def get_enemy(self_type: u8) -> u8: """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 + return (self_type + 1) & 1 @staticmethod def get_piece_types(self_type: u8) -> tuple[u8, u8]: """Return piece types of given piece type.""" # If we are kinged, get a pawn version of ourselves. - self_pawn = self_type % 2 + self_pawn = self_type & 1 return (self_pawn, self_pawn + 2) def get_jumps( @@ -424,7 +424,7 @@ def check_for_win(self) -> u8 | None: 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 (player + 1) & 1 return None def can_player_select_piece(self, player: u8, tile_pos: Pos) -> bool: @@ -432,7 +432,7 @@ def can_player_select_piece(self, player: u8, tile_pos: Pos) -> bool: piece_at_pos = self.pieces.get(tile_pos) if piece_at_pos is None: return False - return (piece_at_pos % 2) == player + return (piece_at_pos & 1) == player def get_pieces(self) -> tuple[tuple[Pos, u8], ...]: """Return all pieces.""" diff --git a/tests/test_sprite.py b/tests/test_sprite.py index 83e909a..5687b65 100644 --- a/tests/test_sprite.py +++ b/tests/test_sprite.py @@ -194,7 +194,7 @@ def test_movement_component_point_toward( def test_movement_component_move_heading_time( movement_component: MovementComponent, ) -> None: - movement_component.speed = 5 + movement_component.speed = 5.0 movement_component.move_heading_time(1) assert movement_component.heading * 5 == movement_component.heading diff --git a/tests/test_statemachine.py b/tests/test_statemachine.py index 0898145..8af5ed3 100644 --- a/tests/test_statemachine.py +++ b/tests/test_statemachine.py @@ -125,6 +125,14 @@ async def test_async_state_machine_add() -> None: assert bob.machine is not None +class ToBob(AsyncState[AsyncStateMachine]): + __slots__ = () + + async def check_conditions(self) -> str: + await super().check_conditions() + return "bob" + + @pytest.mark.trio async def test_async_state_machine_think() -> None: machine = AsyncStateMachine() @@ -145,13 +153,6 @@ async def test_async_state_machine_think() -> None: await machine.set_state("bob") machine.add_state(AsyncState("bob")) - class ToBob(AsyncState[AsyncStateMachine]): - __slots__ = () - - async def check_conditions(self) -> str: - await super().check_conditions() - return "bob" - machine.add_state(ToBob("tom")) await machine.set_state("tom") await machine.think() From c9ca7951808e6e3de52a358d5d1d85d99cdf5c7d Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Fri, 21 Nov 2025 17:25:46 -0600 Subject: [PATCH 21/21] Disable slots for some reason (I forget) --- pyproject.toml | 23 ++++---- src/checkers/__main__.py | 23 ++++++++ src/checkers/client.py | 2 +- src/checkers/element_list.py | 4 +- src/checkers/game.py | 44 +++++++------- src/checkers/sprite.py | 2 +- src/checkers/state.py | 9 ++- src/checkers_computer_players/__main__.py | 31 ++++++++++ tools/build_direct.py | 70 +++++++++++++++++++++++ 9 files changed, 168 insertions(+), 40 deletions(-) create mode 100644 src/checkers/__main__.py create mode 100644 src/checkers_computer_players/__main__.py create mode 100755 tools/build_direct.py diff --git a/pyproject.toml b/pyproject.toml index 9283cff..eb4e43a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,13 +10,11 @@ authors = [ ] description = "Graphical Checkers Game with AI support" readme = {file = "README.md", content-type = "text/markdown"} -license = {file = "LICENSE"} +license = "GPL-3.0" requires-python = ">=3.10" classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: End Users/Desktop", - "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", - "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", "Natural Language :: English", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", @@ -93,20 +91,21 @@ include = [ "src/checkers_computer_players/checkers_minimax.py", ] exclude = [ + "tests/test_statemachine.py", "src/checkers/client.py", - "src/checkers/element_list.py", - "src/checkers/game.py", +## "src/checkers/element_list.py", +## "src/checkers/game.py", "src/checkers/multi_inherit.py", - "src/checkers/namedtuple_mod.py", - "src/checkers/objects.py", +## "src/checkers/namedtuple_mod.py", +## "src/checkers/objects.py", "src/checkers/server.py", - "src/checkers/sprite.py", - "src/checkers/statemachine.py", +## "src/checkers/sprite.py", +## "src/checkers/statemachine.py", "src/checkers/vector.py", - "src/checkers_computer_players/example_ai.py", - "src/checkers_computer_players/max_y_jumper_ai.py", +## "src/checkers_computer_players/example_ai.py", +## "src/checkers_computer_players/max_y_jumper_ai.py", "src/checkers_computer_players/machine_client.py", - "src/checkers_computer_players/minimax_ai.py", +## "src/checkers_computer_players/minimax_ai.py", ] #[tool.hatch.build.targets.wheel.hooks.mypyc.options] diff --git a/src/checkers/__main__.py b/src/checkers/__main__.py new file mode 100644 index 0000000..08e48e7 --- /dev/null +++ b/src/checkers/__main__.py @@ -0,0 +1,23 @@ +"""Checkers Game Module.""" + +# Programmed by CoolCat467 + +# Copyright (C) 2023-2025 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 . + +if __name__ == "__main__": + from checkers.game import cli_run as cli_run + + cli_run() diff --git a/src/checkers/client.py b/src/checkers/client.py index f9671dd..d773e0b 100644 --- a/src/checkers/client.py +++ b/src/checkers/client.py @@ -161,7 +161,7 @@ class GameClient(ClientNetworkEventComponent): to the server, and reading and raising incoming events from the server. """ - __slots__ = ("connect_event_lock", "running") + # __slots__ = ("connect_event_lock", "running") def __init__(self, name: str) -> None: """Initialize GameClient.""" diff --git a/src/checkers/element_list.py b/src/checkers/element_list.py index ce1a0d8..7aa5574 100644 --- a/src/checkers/element_list.py +++ b/src/checkers/element_list.py @@ -38,7 +38,7 @@ class Element(sprite.Sprite): """Element sprite.""" - __slots__ = () + # __slots__ = () def self_destruct(self) -> None: """Remove this element.""" @@ -55,7 +55,7 @@ def __del__(self) -> None: class ElementList(sprite.Sprite): """Element List sprite.""" - __slots__ = ("_order",) + # __slots__ = ("_order",) def __init__(self, name: object) -> None: """Initialize connection list.""" diff --git a/src/checkers/game.py b/src/checkers/game.py index 7600051..8c4fba6 100644 --- a/src/checkers/game.py +++ b/src/checkers/game.py @@ -133,13 +133,13 @@ def render_text( class Piece(sprite.Sprite): """Piece Sprite.""" - __slots__ = ( - "board_position", - "destination_tiles", - "piece_type", - "position_name", - "selected", - ) + ## __slots__ = ( + ## "board_position", + ## "destination_tiles", + ## "piece_type", + ## "position_name", + ## "selected", + ## ) def __init__( self, @@ -283,7 +283,7 @@ async def handle_update_event(self, event: Event[int]) -> None: class Tile(sprite.Sprite): """Outlined tile sprite - Only exists for selecting destination.""" - __slots__ = ("board_position", "color") + # __slots__ = ("board_position", "color") def __init__( self, @@ -380,14 +380,14 @@ def play_sound( class GameBoard(sprite.Sprite): """Entity that stores data about the game board and renders it.""" - __slots__ = ( - "animation_queue", - "board_size", - "pieces", - "processing_animations", - "tile_size", - "tile_surfs", - ) + ## __slots__ = ( + ## "animation_queue", + ## "board_size", + ## "pieces", + ## "processing_animations", + ## "tile_size", + ## "tile_surfs", + ## ) # Define Tile Color Map and Piece Map # tile_color_map = (BLACK, RED) @@ -813,7 +813,7 @@ def generate_board_image(self) -> Surface: class ClickDestinationComponent(Component): """Component that will use targeting to go to wherever you click on the screen.""" - __slots__ = ("selected",) + # __slots__ = ("selected",) outline = pygame.color.Color(255, 220, 0) def __init__(self) -> None: @@ -1015,7 +1015,7 @@ async def check_conditions(self) -> None: class GameState(AsyncState["CheckersClient"]): """Checkers Game Asynchronous State base class.""" - __slots__ = ("id", "manager") + # __slots__ = ("id", "manager") def __init__(self, name: str) -> None: """Initialize Game State.""" @@ -1225,7 +1225,7 @@ async def entry_actions(self) -> None: class PlayHostingState(AsyncState["CheckersClient"]): """Start running server.""" - __slots__ = ("address",) + # __slots__ = ("address",) internal_server = False @@ -1275,7 +1275,7 @@ class PlayInternalHostingState(PlayHostingState): class PlayJoiningState(GameState): """Start running client.""" - __slots__ = ("font",) + # __slots__ = ("font",) def __init__(self) -> None: """Initialize Joining State.""" @@ -1380,7 +1380,7 @@ async def handle_return_to_title(self, _: Event[None]) -> None: class PlayState(GameState): """Game Play State.""" - __slots__ = ("exit_data",) + # __slots__ = ("exit_data",) def __init__(self) -> None: """Initialize Play State.""" @@ -1519,7 +1519,7 @@ async def do_actions(self) -> None: class CheckersClient(sprite.GroupProcessor): """Checkers Game Client.""" - __slots__ = ("manager",) + # __slots__ = ("manager",) def __init__(self, manager: ExternalRaiseManager) -> None: """Initialize Checkers Client.""" diff --git a/src/checkers/sprite.py b/src/checkers/sprite.py index dc4f3eb..e1e6083 100644 --- a/src/checkers/sprite.py +++ b/src/checkers/sprite.py @@ -73,7 +73,7 @@ class PygameMouseMotion(PygameMouseEventData): class Sprite(ComponentManager, WeakDirtySprite): """Client sprite component.""" - __slots__ = ("__image", "mask", "rect", "update_location_on_resize") + # __slots__ = ("__image", "mask", "rect", "update_location_on_resize") def __init__(self, name: object) -> None: """Initialize with name.""" diff --git a/src/checkers/state.py b/src/checkers/state.py index d8d7637..70460b9 100644 --- a/src/checkers/state.py +++ b/src/checkers/state.py @@ -29,6 +29,7 @@ from dataclasses import dataclass from typing import ( TYPE_CHECKING, + Final, NamedTuple, TypeAlias, TypeVar, @@ -41,8 +42,12 @@ 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? +MANDATORY_CAPTURE: Final = ( + True # If a jump is available, do you have to or not? +) +PAWN_JUMP_FORWARD_ONLY: Final = ( + True # Pawns not allowed to go backwards in jumps? +) # Note: Tile Ids are chess board tile titles, A1 to H8 # A8 ... H8 diff --git a/src/checkers_computer_players/__main__.py b/src/checkers_computer_players/__main__.py new file mode 100644 index 0000000..0a04bcf --- /dev/null +++ b/src/checkers_computer_players/__main__.py @@ -0,0 +1,31 @@ +"""Computer Players Module.""" + +# Programmed by CoolCat467 + +from __future__ import annotations + +# Computer Players Module +# Copyright (C) 2025 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__ = "Computer Players Module" +__author__ = "CoolCat467" +__version__ = "0.0.0" +__license__ = "GNU General Public License Version 3" + +from checkers_computer_players.minimax_ai import run + +if __name__ == "__main__": + run() diff --git a/tools/build_direct.py b/tools/build_direct.py new file mode 100755 index 0000000..49c9f42 --- /dev/null +++ b/tools/build_direct.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 + +"""TITLE - DESCRIPTION.""" + +# Programmed by CoolCat467 + +from __future__ import annotations + +# TITLE - DESCRIPTION +# Copyright (C) 2025 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__ = "TITLE" +__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: Final = "tool.hatch.build.targets.wheel.hooks.mypyc" + +HERE: Final = Path(__file__).absolute().parent +ROOT: Final = HERE.parent +PYPROJECT_TOML: Final = ROOT / "pyproject.toml" + + +def run() -> int: + """Run program.""" + assert (ROOT / "LICENSE").exists() + + with PYPROJECT_TOML.open("rb") as fp: + pyproject = tomllib.load(fp) + + temp: dict[str, object] = pyproject + for key in KEY.split("."): + temp = temp[key] + mypyc_data = temp + + exclude = [f"--exclude={e!r}" for e in sorted(mypyc_data["exclude"])] + + command = ( + # "MYPYC_MULTI_FILE=1", + "mypyc", + *exclude, + "-a 'warnings.html'", + ) + + print(" ".join(command)) + + return 0 + + +if __name__ == "__main__": + sys.exit(run())