Compare commits

...

10 Commits

Author SHA1 Message Date
Alfred Baumann
f4ef3c7555 komplexeres wfc, funktioniert jetzt immer 2026-06-11 16:01:44 +02:00
Alfred Baumann
8139af00d5 Anfang kompliziertere Version vom wfc 2026-06-10 17:19:22 +02:00
Alfred Baumann
d39e6ecc25 simples wfc, funktioniert noch nicht immer 2026-06-10 14:41:34 +02:00
Alfred Baumann
62864b7c00 anzeigen von locked felder flicken 2026-06-10 14:41:00 +02:00
Alfred Baumann
f3a6257cd4 anfang des wave function collapse algorithmus 2026-06-09 16:16:44 +02:00
Alfred Baumann
52142da3f3 einfaches timen vom brute-force 2026-06-09 11:16:31 +02:00
Alfred Baumann
21b9ce7ad7 brute-force funktioniert, 3x3 felder damit man es überhaupt brauchen kann 2026-06-08 14:25:34 +02:00
Alfred Baumann
6de80582d9 brute-force 2026-06-08 11:41:48 +02:00
Alfred Baumann
7b7df0f7a8 Descriptions benutzen und andere Feldgrössen als 5x5 erlauben 2026-06-02 17:50:24 +02:00
Alfred Baumann
a903f5eb91 descriptions von tatham's version generiert.
(jeweils generiert durch `./net --generate 1000 (grösse)x(grösse)`)
2026-06-01 17:59:15 +02:00
23 changed files with 6383 additions and 72 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

1000
descriptions/11x11.txt Normal file

File diff suppressed because it is too large Load Diff

1000
descriptions/13x13.txt Normal file

File diff suppressed because it is too large Load Diff

1000
descriptions/3x3.txt Normal file

File diff suppressed because it is too large Load Diff

1000
descriptions/5x5.txt Normal file

File diff suppressed because it is too large Load Diff

1000
descriptions/7x7.txt Normal file

File diff suppressed because it is too large Load Diff

1000
descriptions/9x9.txt Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,12 +1,17 @@
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):
@@ -27,8 +32,14 @@ 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(os.path.join("assets", f"{piece.type}.bmp")).convert() image = pygame.image.load(
image = pygame.transform.rotate(image, -90 * piece.direction) os.path.join("assets", f"{piece.type}.bmp")
).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:
@@ -38,6 +49,10 @@ 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})
@@ -47,22 +62,34 @@ class PieceSprite(pygame.sprite.Sprite):
class NetGUI: class NetGUI:
game: NetGame game: NetGame
window: pygame.Surface window: pygame.Surface
pieceSprites: pygame.sprite.Group[PieceSprite] pieceSprites: pygame.sprite.Group[
PieceSprite # pyright: ignore[reportInvalidTypeArguments]
]
def __init__(self): def __init__(self, width: int, height: int):
self.game = NetGame(5, 5) self.game = NetGame(width, height)
pygame.init() pygame.init()
self.window = pygame.display.set_mode((5 * 30, 5 * 30)) self.window = pygame.display.set_mode((width * 30, height * 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(5): for x in range(width):
for y in range(5): for y in range(height):
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):
while True: current_solver = None
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:
@@ -70,18 +97,28 @@ 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)
if self.game.solved(): elif event.type == LOCK:
raise SystemExit self.game.lock(event.x, event.y)
elif event.type == pygame.KEYDOWN:
self.pieceSprites.update(self.game, events) if event.key == pygame.K_b:
for sprite in self.pieceSprites: current_solver = BruteForceSolver(self.game).solve()
self.window.blit(sprite.image, sprite.rect) elif event.key == pygame.K_d:
display_solver = not display_solver
pygame.display.flip() elif event.key == pygame.K_s:
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().run_game() NetGUI(5, 5).run_game()

View File

@@ -0,0 +1,29 @@
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

25
src/algorithms/profile.py Normal file
View File

@@ -0,0 +1,25 @@
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)

191
src/algorithms/wfc.py Normal file
View File

@@ -0,0 +1,191 @@
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.types import Piece, Direction, PieceType from src.netTypes import Piece, Direction, PieceType
# Tatham's board description format: # Tatham's board description format:

View File

@@ -2,49 +2,10 @@
Kernlogik des Spiels Kernlogik des Spiels
""" """
from src.types import Piece, Direction from random import choice
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:
@@ -52,12 +13,19 @@ class Grid:
width: int width: int
height: int height: int
def __init__(self, width: int, height: int) -> None: def __init__(self, width: int, height: int, specific: int | None = None) -> None:
self.height = height self.height = height
self.width = width self.width = width
self.pieces = ( if width != height or width not in [3, 5, 7, 9, 11, 13]:
DEMO_FIELD # TODO: Field generation or import from Tatham's version raise ValueError("Feldgrösse nicht erlaubt")
) 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] = []
@@ -82,7 +50,7 @@ class Grid:
) -> list[list[bool]]: ) -> list[list[bool]]:
connected[x][y] = True connected[x][y] = True
connectedNeighbors: list[Coordinate] = [] connected_neighbors: 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
@@ -92,9 +60,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()
): ):
connectedNeighbors.append((nx, ny)) connected_neighbors.append((nx, ny))
for nx, ny in connectedNeighbors: for nx, ny in connected_neighbors:
if connected[nx][ny]: if connected[nx][ny]:
continue continue
connected = self._solve_floodfill(nx, ny, connected) connected = self._solve_floodfill(nx, ny, connected)
@@ -104,9 +72,13 @@ class Grid:
class NetGame: class NetGame:
_grid: Grid _grid: Grid
width: int
height: int
def __init__(self, width: int, height: int) -> None: def __init__(self, width: int, height: int, specific: int | None = None) -> None:
self._grid = Grid(width, height) self._grid = Grid(width, height, specific)
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
@@ -120,5 +92,14 @@ 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,6 +25,9 @@ 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
@@ -51,7 +54,11 @@ 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,6 +2,18 @@ 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"
@@ -10,9 +22,33 @@ 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"