Compare commits

..

6 Commits

Author SHA1 Message Date
Alfred Baumann
cf6036f269 parser für die Descriptions, die Tatham's version generiert 2026-05-15 21:27:13 +02:00
CheesePlated
5a5283074a Debugzeilen entfernen 2026-05-15 19:31:14 +02:00
CheesePlated
c3405118d7 Bildschirmgrösse flicken 2026-05-15 14:03:44 +02:00
CheesePlated
d21764188d Grossteil vom GUI vom Minesweeper porten, muss noch fertiggestellt werden 2026-05-14 20:31:32 +02:00
CheesePlated
5fedf513fc Anfang spiellogik 2026-05-14 14:52:05 +02:00
CheesePlated
a25bd700c9 init uv and nix flake 2026-05-08 13:56:23 +02:00
23 changed files with 72 additions and 6383 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -7,8 +7,3 @@ requires-python = ">=3.13"
dependencies = [ dependencies = [
"pygame>=2.6.1", "pygame>=2.6.1",
] ]
[dependency-groups]
dev = [
"basedpyright>=1.39.6",
]

View File

@@ -1,17 +1,12 @@
import os import os
from typing import override from typing import override
import pygame import pygame
from src.algorithms.wfc import WFCSolver
from src.algorithms.bruteforce import BruteForceSolver
from src.net import NetGame from src.net import NetGame
# pyright: reportUnusedCallResult=false, reportAny=false # pyright: reportUnusedCallResult=false, reportAny=false
LEFT_TURN = pygame.USEREVENT + 1 LEFT_TURN = pygame.USEREVENT + 1
RIGHT_TURN = pygame.USEREVENT + 2 RIGHT_TURN = pygame.USEREVENT + 2
LOCK = pygame.USEREVENT + 3
class PieceSprite(pygame.sprite.Sprite): class PieceSprite(pygame.sprite.Sprite):
@@ -32,14 +27,8 @@ class PieceSprite(pygame.sprite.Sprite):
@override @override
def update(self, game: NetGame, events: list[pygame.event.Event]): def update(self, game: NetGame, events: list[pygame.event.Event]):
piece = game.get_piece(self.x, self.y) piece = game.get_piece(self.x, self.y)
image = pygame.image.load( image = pygame.image.load(os.path.join("assets", f"{piece.type}.bmp")).convert()
os.path.join("assets", f"{piece.type}.bmp") image = pygame.transform.rotate(image, -90 * piece.direction)
).convert_alpha()
image = pygame.transform.rotate(image, -90 * int(piece.direction))
if piece.locked:
self.image.fill("#a0a0a0")
else:
self.image.fill("#ffffff")
self.image.blit(image, (0, 0)) self.image.blit(image, (0, 0))
for event in events: for event in events:
if event.type == pygame.MOUSEBUTTONUP: if event.type == pygame.MOUSEBUTTONUP:
@@ -49,10 +38,6 @@ class PieceSprite(pygame.sprite.Sprite):
pygame.event.post( pygame.event.post(
pygame.event.Event(LEFT_TURN, {"x": self.x, "y": self.y}) pygame.event.Event(LEFT_TURN, {"x": self.x, "y": self.y})
) )
elif event.button == 2:
pygame.event.post(
pygame.event.Event(LOCK, {"x": self.x, "y": self.y})
)
elif event.button == 3: elif event.button == 3:
pygame.event.post( pygame.event.post(
pygame.event.Event(RIGHT_TURN, {"x": self.x, "y": self.y}) pygame.event.Event(RIGHT_TURN, {"x": self.x, "y": self.y})
@@ -62,34 +47,22 @@ class PieceSprite(pygame.sprite.Sprite):
class NetGUI: class NetGUI:
game: NetGame game: NetGame
window: pygame.Surface window: pygame.Surface
pieceSprites: pygame.sprite.Group[ pieceSprites: pygame.sprite.Group[PieceSprite]
PieceSprite # pyright: ignore[reportInvalidTypeArguments]
]
def __init__(self, width: int, height: int): def __init__(self):
self.game = NetGame(width, height) self.game = NetGame(5, 5)
pygame.init() pygame.init()
self.window = pygame.display.set_mode((width * 30, height * 30)) self.window = pygame.display.set_mode((5 * 30, 5 * 30))
self.window.fill("#ffffff") self.window.fill("#ffffff")
pygame.display.set_caption("Net") pygame.display.set_caption("Net")
self.pieceSprites = pygame.sprite.Group() self.pieceSprites = pygame.sprite.Group()
for x in range(width): for x in range(5):
for y in range(height): for y in range(5):
self.pieceSprites.add(PieceSprite(x, y)) self.pieceSprites.add(PieceSprite(x, y))
def update_display(self, events: list[pygame.event.Event]):
self.pieceSprites.update(self.game, events)
for sprite in self.pieceSprites:
self.window.blit(sprite.image, sprite.rect)
pygame.display.flip()
def run_game(self): def run_game(self):
current_solver = None while True:
display_solver = False
step_solver = False
while not self.game.solved():
events = pygame.event.get() events = pygame.event.get()
for event in events: for event in events:
@@ -97,28 +70,18 @@ class NetGUI:
raise SystemExit raise SystemExit
elif event.type == LEFT_TURN: elif event.type == LEFT_TURN:
self.game.turn_ccw(event.x, event.y) self.game.turn_ccw(event.x, event.y)
if self.game.solved():
raise SystemExit
elif event.type == RIGHT_TURN: elif event.type == RIGHT_TURN:
self.game.turn_cw(event.x, event.y) self.game.turn_cw(event.x, event.y)
elif event.type == LOCK: if self.game.solved():
self.game.lock(event.x, event.y) raise SystemExit
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_b: self.pieceSprites.update(self.game, events)
current_solver = BruteForceSolver(self.game).solve() for sprite in self.pieceSprites:
elif event.key == pygame.K_d: self.window.blit(sprite.image, sprite.rect)
display_solver = not display_solver
elif event.key == pygame.K_s: pygame.display.flip()
step_solver = not step_solver
elif event.key == pygame.K_w:
current_solver = WFCSolver(self.game).solve()
if current_solver:
try:
_ = next(current_solver)
except StopIteration:
current_solver = None
if step_solver:
_ = input()
if (not current_solver) or display_solver:
self.update_display(events)
NetGUI(5, 5).run_game() NetGUI().run_game()

View File

@@ -1,29 +0,0 @@
from collections.abc import Generator
from itertools import chain, pairwise
from src.net import NetGame
from src.netTypes import Direction
class BruteForceSolver:
game: NetGame
def __init__(self, game: NetGame) -> None:
self.game = game
for x in range(game.height):
for y in range(game.width):
self.game.set_direction(x, y, Direction.UP)
def solve(self) -> Generator[None]:
while not self.game.solved():
yield
self.game.turn_cw(0, 0)
for prev, curr in pairwise(
chain(
*self.game.get_field()
) # 2d-liste zu 1d machen, damit man einfacher über alle zellen iterieren kann
):
if prev.direction == Direction.UP:
curr.turn_cw()
else:
break

View File

@@ -1,25 +0,0 @@
import time
from multiprocessing import Pool
from src.algorithms.wfc import WFCSolver
from src.algorithms.bruteforce import BruteForceSolver
from src.net import NetGame
def test_run(i: int) -> float:
game = NetGame(11, 11, i)
solver = WFCSolver(game)
a = time.perf_counter()
for _ in solver.solve():
pass
b = time.perf_counter()
return b - a
if __name__ == "__main__":
total = 0
with Pool() as p:
processes = [p.apply_async(test_run, (i,)) for i in range(1000)]
for proc in processes:
total += proc.get()
print(total / 1000)

View File

@@ -1,191 +0,0 @@
from collections.abc import Generator
from copy import deepcopy
from dataclasses import dataclass
from enum import Enum, auto
from src.netTypes import Direction, PieceType
from src.net import NetGame
type RollbackType = list[
tuple[
list[tuple[int, int]],
list[list[Direction]],
set[Direction],
int,
int,
list[list[PieceConnectionState]],
]
]
class ConnectionState(Enum):
DISCONNECTED = auto()
CONNECTED = auto()
UNKNOWN = auto()
@dataclass
class PieceConnectionState:
up: ConnectionState = ConnectionState.UNKNOWN
down: ConnectionState = ConnectionState.UNKNOWN
left: ConnectionState = ConnectionState.UNKNOWN
right: ConnectionState = ConnectionState.UNKNOWN
def directions_with_state(self, state: ConnectionState) -> set[Direction]:
out: set[Direction] = set()
if self.up == state:
out.add(Direction.UP)
if self.down == state:
out.add(Direction.DOWN)
if self.left == state:
out.add(Direction.LEFT)
if self.right == state:
out.add(Direction.RIGHT)
return out
def connected_or_unknown(self) -> set[Direction]:
"""Return wert ist die Richtungen, deren State `CONNECTED` ist.
Falls es keine solche gibt, gibt es die Richtungen mit `UNKNOWN` zurück."""
if connected := self.directions_with_state(ConnectionState.CONNECTED):
return connected
else:
return self.directions_with_state(ConnectionState.UNKNOWN)
def set_direction(self, direction: Direction, state: ConnectionState) -> None:
if direction == Direction.UP:
self.up = state
elif direction == Direction.DOWN:
self.down = state
elif direction == Direction.LEFT:
self.left = state
elif direction == Direction.RIGHT:
self.right = state
class WFCSolver:
game: NetGame
connection_states: list[list[PieceConnectionState]]
def __init__(self, game: NetGame) -> None:
self.game = game
self.connection_states = [
[PieceConnectionState() for _ in range(self.game.width)]
for _ in range(self.game.width)
]
# Es kann nicht mit dem Rand des Feldes eine Verbindung bestehen
for column in self.connection_states:
column[0].up = ConnectionState.DISCONNECTED
column[-1].down = ConnectionState.DISCONNECTED
for s in self.connection_states[0]:
s.left = ConnectionState.DISCONNECTED
for s in self.connection_states[-1]:
s.right = ConnectionState.DISCONNECTED
def get_possible_corner_directions(self, x: int, y: int) -> set[Direction]:
cstates = self.connection_states[x][y]
connected = cstates.directions_with_state(ConnectionState.CONNECTED)
if connected:
possible = {Direction.UP, Direction.DOWN, Direction.LEFT, Direction.RIGHT}
for direction in connected:
possible.intersection_update(
{direction, Direction((direction - 1) % 4)}
)
else:
possible = cstates.directions_with_state(ConnectionState.UNKNOWN)
possible.update(map(lambda d: Direction((d - 1) % 4), possible.copy()))
disconnected = cstates.directions_with_state(ConnectionState.DISCONNECTED)
possible.difference_update(disconnected)
possible.difference_update(map(lambda d: Direction((d - 1) % 4), disconnected))
return possible
def get_possible_directions(self, x: int, y: int) -> set[Direction]:
ptype = self.game.get_piece(x, y).type
cstates = self.connection_states[x][y]
dirs = cstates.connected_or_unknown()
if ptype == PieceType.NODE:
return dirs
elif ptype == PieceType.CORNER:
return self.get_possible_corner_directions(x, y)
elif ptype == PieceType.STRAIGHT:
connected = cstates.directions_with_state(ConnectionState.CONNECTED)
if connected:
return {connected.pop()}
disconnected = cstates.directions_with_state(ConnectionState.DISCONNECTED)
if disconnected:
return {Direction((disconnected.pop() + 1) % 4)}
else:
return cstates.directions_with_state(ConnectionState.UNKNOWN)
else:
disconnected = cstates.directions_with_state(ConnectionState.DISCONNECTED)
if len(dirs) == 4:
return dirs
elif disconnected:
return {disconnected.pop().flip()}
else:
possible = {
Direction.UP,
Direction.DOWN,
Direction.LEFT,
Direction.RIGHT,
}
for d in dirs:
possible.intersection_update(
{d, Direction((d + 1) % 4), Direction((d - 1) % 4)}
)
return possible
def get_field_rotations(self) -> list[list[Direction]]:
return [[p.direction for p in col] for col in self.game.get_field()]
def set_field_rotations(self, rotations: list[list[Direction]]):
for x, col in enumerate(rotations):
for y, d in enumerate(col):
self.game.set_direction(x, y, d)
def solve(self) -> Generator[None]:
unfixed = list(
((i, j) for i in range(self.game.width) for j in range(self.game.height))
)
rollback: RollbackType = []
while not self.game.solved():
yield
if len(unfixed) == 0:
unfixed, rotations, dirs, x, y, self.connection_states = rollback.pop()
self.set_field_rotations(rotations)
else:
(x, y) = min(
unfixed, key=lambda t: len(self.get_possible_directions(*t))
)
dirs = self.get_possible_directions(x, y)
if len(dirs) == 0:
unfixed, rotations, dirs, x, y, self.connection_states = rollback.pop()
self.set_field_rotations(rotations)
direction = dirs.pop()
if len(dirs) > 0:
rollback.append(
(
unfixed.copy(),
self.get_field_rotations(),
dirs,
x,
y,
deepcopy(self.connection_states),
)
)
self.game.set_direction(x, y, direction)
self.game.lock(x, y)
unfixed.remove((x, y))
for nx, ny in self.game.neighbors(x, y):
dx = nx - x
dy = ny - y
ndir = Direction.from_offset(dx, dy)
if ndir not in self.game.get_piece(x, y).connected_directions():
self.connection_states[nx][ny].set_direction(
ndir.flip(), ConnectionState.DISCONNECTED
)
else:
self.connection_states[nx][ny].set_direction(
ndir.flip(), ConnectionState.CONNECTED
)

View File

@@ -1,4 +1,4 @@
from src.netTypes import Piece, Direction, PieceType from src.types import Piece, Direction, PieceType
# Tatham's board description format: # Tatham's board description format:

View File

@@ -2,10 +2,49 @@
Kernlogik des Spiels Kernlogik des Spiels
""" """
from random import choice from src.types import Piece, Direction
from src.interface import parse_description from src.interface import parse_description
from src.netTypes import Piece, Direction, Coordinate
# DEMO_FIELD = [
# [
# Piece(PieceType.NODE),
# Piece(PieceType.NODE),
# Piece(PieceType.T_JUNCTION),
# Piece(PieceType.NODE),
# Piece(PieceType.NODE),
# ],
# [
# Piece(PieceType.STRAIGHT),
# Piece(PieceType.NODE),
# Piece(PieceType.T_JUNCTION),
# Piece(PieceType.STRAIGHT),
# Piece(PieceType.CORNER),
# ],
# [
# Piece(PieceType.T_JUNCTION),
# Piece(PieceType.T_JUNCTION),
# Piece(PieceType.T_JUNCTION),
# Piece(PieceType.T_JUNCTION),
# Piece(PieceType.NODE),
# ],
# [
# Piece(PieceType.STRAIGHT),
# Piece(PieceType.NODE),
# Piece(PieceType.T_JUNCTION),
# Piece(PieceType.T_JUNCTION),
# Piece(PieceType.NODE),
# ],
# [
# Piece(PieceType.NODE),
# Piece(PieceType.NODE),
# Piece(PieceType.CORNER),
# Piece(PieceType.CORNER),
# Piece(PieceType.CORNER),
# ],
# ]
DEMO_FIELD = parse_description("5x5:c7634887c213e5b8db3e69282")
class Grid: class Grid:
@@ -13,19 +52,12 @@ class Grid:
width: int width: int
height: int height: int
def __init__(self, width: int, height: int, specific: int | None = None) -> None: def __init__(self, width: int, height: int) -> None:
self.height = height self.height = height
self.width = width self.width = width
if width != height or width not in [3, 5, 7, 9, 11, 13]: self.pieces = (
raise ValueError("Feldgrösse nicht erlaubt") DEMO_FIELD # TODO: Field generation or import from Tatham's version
with open(f"descriptions/{width}x{height}.txt") as f: )
lines = f.readlines()
if specific is not None:
selected = lines[specific].strip()
else:
selected = choice(lines).strip()
print(f"Seed: {lines.index(selected + "\n")}")
self.pieces = parse_description(selected)
def neighbors(self, x: int, y: int) -> list[Coordinate]: def neighbors(self, x: int, y: int) -> list[Coordinate]:
neighbors: list[Coordinate] = [] neighbors: list[Coordinate] = []
@@ -50,7 +82,7 @@ class Grid:
) -> list[list[bool]]: ) -> list[list[bool]]:
connected[x][y] = True connected[x][y] = True
connected_neighbors: list[Coordinate] = [] connectedNeighbors: list[Coordinate] = []
for nx, ny in self.neighbors(x, y): for nx, ny in self.neighbors(x, y):
dx = x - nx dx = x - nx
dy = y - ny dy = y - ny
@@ -60,9 +92,9 @@ class Grid:
and Direction.from_offset(-dx, -dy) and Direction.from_offset(-dx, -dy)
in self.pieces[x][y].connected_directions() in self.pieces[x][y].connected_directions()
): ):
connected_neighbors.append((nx, ny)) connectedNeighbors.append((nx, ny))
for nx, ny in connected_neighbors: for nx, ny in connectedNeighbors:
if connected[nx][ny]: if connected[nx][ny]:
continue continue
connected = self._solve_floodfill(nx, ny, connected) connected = self._solve_floodfill(nx, ny, connected)
@@ -72,13 +104,9 @@ class Grid:
class NetGame: class NetGame:
_grid: Grid _grid: Grid
width: int
height: int
def __init__(self, width: int, height: int, specific: int | None = None) -> None: def __init__(self, width: int, height: int) -> None:
self._grid = Grid(width, height, specific) self._grid = Grid(width, height)
self.width = width
self.height = height
def get_field(self) -> list[list[Piece]]: def get_field(self) -> list[list[Piece]]:
return self._grid.pieces return self._grid.pieces
@@ -92,14 +120,5 @@ class NetGame:
def turn_ccw(self, x: int, y: int) -> None: def turn_ccw(self, x: int, y: int) -> None:
self._grid.pieces[x][y].turn_ccw() self._grid.pieces[x][y].turn_ccw()
def lock(self, x: int, y: int) -> None:
self._grid.pieces[x][y].locked = not self._grid.pieces[x][y].locked
def set_direction(self, x: int, y: int, dir: Direction) -> None:
self._grid.pieces[x][y].direction = dir
def neighbors(self, x: int, y: int) -> list[Coordinate]:
return self._grid.neighbors(x, y)
def solved(self): def solved(self):
return self._grid.solved() return self._grid.solved()

View File

@@ -25,9 +25,6 @@ class Direction(IntEnum):
else: else:
return Direction(dy + 1) return Direction(dy + 1)
def flip(self) -> Direction:
return Direction((self + 2) % 4)
class Piece: class Piece:
type: PieceType type: PieceType
@@ -54,11 +51,7 @@ class Piece:
] ]
def turn_cw(self) -> None: def turn_cw(self) -> None:
if self.locked:
return
self.direction = Direction((self.direction + 1) % 4) self.direction = Direction((self.direction + 1) % 4)
def turn_ccw(self) -> None: def turn_ccw(self) -> None:
if self.locked:
return
self.direction = Direction((self.direction - 1) % 4) self.direction = Direction((self.direction - 1) % 4)

36
uv.lock generated
View File

@@ -2,18 +2,6 @@ version = 1
revision = 3 revision = 3
requires-python = ">=3.13" requires-python = ">=3.13"
[[package]]
name = "basedpyright"
version = "1.39.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nodejs-wheel-binaries" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7a/1a/48296b4479ccc9051eb9617a6507a69a68f5b68693fb6a118cfe08199270/basedpyright-1.39.6.tar.gz", hash = "sha256:d00ec5f8ba4e1a67dfc2fa3a9474229c89f61f207d14c02d320db78f57aa16ef", size = 25504244, upload-time = "2026-05-24T07:44:41.864Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d4/07/6d1b3192715d42e8c9887876684a941eff28ec5d79c23a0f3758377e2182/basedpyright-1.39.6-py3-none-any.whl", hash = "sha256:5e0b9befbae6b26d0fbcc6645ac26923725e749d1224539e24f05ab07f9365ad", size = 13182122, upload-time = "2026-05-24T07:44:47.086Z" },
]
[[package]] [[package]]
name = "maturaarbeit" name = "maturaarbeit"
version = "0.1.0" version = "0.1.0"
@@ -22,33 +10,9 @@ dependencies = [
{ name = "pygame" }, { name = "pygame" },
] ]
[package.dev-dependencies]
dev = [
{ name = "basedpyright" },
]
[package.metadata] [package.metadata]
requires-dist = [{ name = "pygame", specifier = ">=2.6.1" }] requires-dist = [{ name = "pygame", specifier = ">=2.6.1" }]
[package.metadata.requires-dev]
dev = [{ name = "basedpyright", specifier = ">=1.39.6" }]
[[package]]
name = "nodejs-wheel-binaries"
version = "24.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a3/22/2a5beb4e21417c73233d9f65cf6f3e96e891b80d2f550a8f630ebc6b88c6/nodejs_wheel_binaries-24.16.0.tar.gz", hash = "sha256:c973cb69dc5fd16e6f6dc6e579e2c3d5534e2a1f57619dddf5ba070efa7dde37", size = 8056, upload-time = "2026-05-30T16:52:09.807Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/83/d1/68b43b53cd0fa83ae6fd406705023ca988d9e0ca41c724d82e66fbeb2ef6/nodejs_wheel_binaries-24.16.0-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:d9f8f677dcf30e37ac244f07869726abe043f01eb0f45722b1df31cc2af7093c", size = 55666374, upload-time = "2026-05-30T16:51:39.588Z" },
{ url = "https://files.pythonhosted.org/packages/e9/b2/40a989159599080da485de966c4c2d207e852ac7aa7864702626d96c8bf5/nodejs_wheel_binaries-24.16.0-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:3d0370fe7120ce9697a4f60d40480d2bd8808d9f30131458d5afc0040d4e5a51", size = 55838487, upload-time = "2026-05-30T16:51:43.383Z" },
{ url = "https://files.pythonhosted.org/packages/d7/a7/cd42174fb5ff6faff7fa8d326a18914d8f232098ab5de055b57c16fa13ca/nodejs_wheel_binaries-24.16.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:85dc92bbb79c851569c5925dcc2a4c915a034efab375f99e4e7e6bbe9cca8342", size = 60179540, upload-time = "2026-05-30T16:51:47.036Z" },
{ url = "https://files.pythonhosted.org/packages/2b/95/c8a1f9ae140aa28df8744d984d01d4b3af7cdd6555af12127f40ceb45a7d/nodejs_wheel_binaries-24.16.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:2f3036292811514ba847b3708492644764f88a833ac425c5f55007014308ddfd", size = 60716262, upload-time = "2026-05-30T16:51:50.711Z" },
{ url = "https://files.pythonhosted.org/packages/64/c9/7c35b3737f59e36d0249c265397b7bff570519b95301d6e16ea361e904ad/nodejs_wheel_binaries-24.16.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:db8a8a76ebd2b28ecbfc9ad464baa3707241b9e050a30e2efdf6f60c0f886502", size = 62230592, upload-time = "2026-05-30T16:51:55Z" },
{ url = "https://files.pythonhosted.org/packages/04/96/d931255cf9d11a84d6b54d882dba7434646467d568ccf070ea3418638df3/nodejs_wheel_binaries-24.16.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f1a3d8f7b4491cbbd023ba3fc4e901fcca2d9fb80d57f24ba3890de8b1dbac03", size = 62841759, upload-time = "2026-05-30T16:51:59.407Z" },
{ url = "https://files.pythonhosted.org/packages/a2/7b/8b7a3f41bc255411be30b6d7d288aab8ffd9ea2055db8555ced3548007b9/nodejs_wheel_binaries-24.16.0-py2.py3-none-win_amd64.whl", hash = "sha256:bb136be9944f0662dcf1120f45193a6b75b13fac378971a95cc42c9f879a81aa", size = 42027734, upload-time = "2026-05-30T16:52:03.348Z" },
{ url = "https://files.pythonhosted.org/packages/17/66/1ed71f1f529b8ca727d42c7ceb9db0bef145ce4a13dfc86fb50aa44f3be6/nodejs_wheel_binaries-24.16.0-py2.py3-none-win_arm64.whl", hash = "sha256:8308940b5edd0a50dc5267ea36ba21c9f668e83fe0d9f293937174d3a7e31c36", size = 39714528, upload-time = "2026-05-30T16:52:06.421Z" },
]
[[package]] [[package]]
name = "pygame" name = "pygame"
version = "2.6.1" version = "2.6.1"