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())