Compare commits

...

16 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
Alfred Baumann
d49493b778 parser für die Descriptions, die Tatham's version generiert 2026-05-15 21:31:08 +02:00
Alfred Baumann
a7fa8ca9c3 Debugzeilen entfernen 2026-05-15 21:31:07 +02:00
Alfred Baumann
e330d23a5d Bildschirmgrösse flicken 2026-05-15 21:31:01 +02:00
Alfred Baumann
f6c5a98624 Grossteil vom GUI vom Minesweeper porten, muss noch fertiggestellt werden 2026-05-15 21:30:58 +02:00
Alfred Baumann
162a2ca1a3 Anfang spiellogik 2026-05-15 21:30:56 +02:00
Alfred Baumann
9d806dc15e init uv and nix flake 2026-05-15 21:30:45 +02:00
27 changed files with 6745 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.envrc
.direnv
__pycache__/

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.14

BIN
assets/1.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
assets/1on.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
assets/2.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
assets/2on.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
assets/3.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
assets/3on.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
assets/4.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
assets/4on.bmp Normal file

Binary file not shown.

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

27
flake.lock generated Normal file
View File

@@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1777954456,
"narHash": "sha256-hGdgeU2Nk87RAuZyYjyDjFL6LK7dAZN5RE9+hrDTkDU=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "549bd84d6279f9852cae6225e372cc67fb91a4c1",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

33
flake.nix Normal file
View File

@@ -0,0 +1,33 @@
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
};
outputs = {nixpkgs, ...}: let
system = "x86_64-linux";
pkgs = import nixpkgs {inherit system;};
in {
devShells.${system}.default = pkgs.mkShell {
name = "maturaarbeit";
packages = with pkgs; [
uv
python314
SDL2
SDL2_image
SDL2_mixer
SDL2_ttf
libX11
];
shellHook = ''
uv sync
. .venv/bin/activate
'';
SDL_VIDEODRIVER = "wayland";
};
};
}

14
pyproject.toml Normal file
View File

@@ -0,0 +1,14 @@
[project]
name = "maturaarbeit"
version = "0.1.0"
description = "Maturaarbeit Alfred Baumann"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"pygame>=2.6.1",
]
[dependency-groups]
dev = [
"basedpyright>=1.39.6",
]

124
src/GUI.py Normal file
View File

@@ -0,0 +1,124 @@
import os
from typing import override
import pygame
from src.algorithms.wfc import WFCSolver
from src.algorithms.bruteforce import BruteForceSolver
from src.net import NetGame
# pyright: reportUnusedCallResult=false, reportAny=false
LEFT_TURN = pygame.USEREVENT + 1
RIGHT_TURN = pygame.USEREVENT + 2
LOCK = pygame.USEREVENT + 3
class PieceSprite(pygame.sprite.Sprite):
image: pygame.Surface
rect: pygame.Rect
x: int
y: int
def __init__(self, x: int, y: int) -> None:
super().__init__()
self.image = pygame.Surface((28, 28))
self.rect = self.image.get_rect()
self.rect.x = 30 * x + 1
self.rect.y = 30 * y + 1
self.x = x
self.y = y
@override
def update(self, game: NetGame, events: list[pygame.event.Event]):
piece = game.get_piece(self.x, self.y)
image = pygame.image.load(
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))
for event in events:
if event.type == pygame.MOUSEBUTTONUP:
if not self.rect.collidepoint(*event.pos):
continue
if event.button == 1:
pygame.event.post(
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:
pygame.event.post(
pygame.event.Event(RIGHT_TURN, {"x": self.x, "y": self.y})
)
class NetGUI:
game: NetGame
window: pygame.Surface
pieceSprites: pygame.sprite.Group[
PieceSprite # pyright: ignore[reportInvalidTypeArguments]
]
def __init__(self, width: int, height: int):
self.game = NetGame(width, height)
pygame.init()
self.window = pygame.display.set_mode((width * 30, height * 30))
self.window.fill("#ffffff")
pygame.display.set_caption("Net")
self.pieceSprites = pygame.sprite.Group()
for x in range(width):
for y in range(height):
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):
current_solver = None
display_solver = False
step_solver = False
while not self.game.solved():
events = pygame.event.get()
for event in events:
if event.type == pygame.QUIT:
raise SystemExit
elif event.type == LEFT_TURN:
self.game.turn_ccw(event.x, event.y)
elif event.type == RIGHT_TURN:
self.game.turn_cw(event.x, event.y)
elif event.type == LOCK:
self.game.lock(event.x, event.y)
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_b:
current_solver = BruteForceSolver(self.game).solve()
elif event.key == pygame.K_d:
display_solver = not display_solver
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(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
)

64
src/interface.py Normal file
View File

@@ -0,0 +1,64 @@
from src.netTypes import Piece, Direction, PieceType
# Tatham's board description format:
# {width}x{height}:{board}
# the board is a string of single-digit hex numbers
# where each number is a bitfield of which sides the piece is connected to
RIGHT = 0x1
UP = 0x2
LEFT = 0x4
DOWN = 0x8
def flip_direction(d: int) -> int:
return (d >> 2) | ((d << 2) & 0xC)
def direction_from_tatham(d: int) -> Direction:
if d == RIGHT:
return Direction.RIGHT
elif d == UP:
return Direction.UP
elif d == LEFT:
return Direction.LEFT
else:
return Direction.DOWN
def corner_direction_from_sides(sides: int) -> Direction:
if sides == 0x3:
return Direction.UP
elif sides == 0x6:
return Direction.LEFT
elif sides == 0xC:
return Direction.DOWN
else:
return Direction.RIGHT
def parse_description(desc: str) -> list[list[Piece]]:
dimensions, field = desc.split(":")
width, height = map(int, dimensions.split("x"))
parsed: list[list[Piece]] = []
for y in range(height):
line: list[Piece] = []
for x in range(width):
value = int(field[x * width + y], 16)
connected_sides = value.bit_count()
if connected_sides == 1:
ptype = PieceType.NODE
direction = direction_from_tatham(value)
elif connected_sides == 2:
if value == 0x5 or value >> 1 == 0x5:
ptype = PieceType.STRAIGHT
direction = direction_from_tatham(value & 0x3)
else:
ptype = PieceType.CORNER
direction = corner_direction_from_sides(value)
else:
ptype = PieceType.T_JUNCTION
direction = direction_from_tatham(flip_direction(~value & 0xF))
line.append(Piece(ptype, direction))
parsed.append(line)
return parsed

105
src/net.py Normal file
View File

@@ -0,0 +1,105 @@
"""
Kernlogik des Spiels
"""
from random import choice
from src.interface import parse_description
from src.netTypes import Piece, Direction, Coordinate
class Grid:
pieces: list[list[Piece]]
width: int
height: int
def __init__(self, width: int, height: int, specific: int | None = None) -> None:
self.height = height
self.width = width
if width != height or width not in [3, 5, 7, 9, 11, 13]:
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]:
neighbors: list[Coordinate] = []
for dx, dy in ((-1, 0), (1, 0), (0, -1), (0, 1)):
if (
x + dx < 0
or x + dx >= self.width
or y + dy < 0
or y + dy >= self.height
):
continue
neighbors.append((x + dx, y + dy))
return neighbors
def solved(self) -> bool:
connected = [([False] * self.height) for _ in range(self.width)]
connected = self._solve_floodfill(0, 0, connected)
return all(map(lambda x: all(x), connected))
def _solve_floodfill(
self, x: int, y: int, connected: list[list[bool]]
) -> list[list[bool]]:
connected[x][y] = True
connected_neighbors: list[Coordinate] = []
for nx, ny in self.neighbors(x, y):
dx = x - nx
dy = y - ny
if (
Direction.from_offset(dx, dy)
in self.pieces[nx][ny].connected_directions()
and Direction.from_offset(-dx, -dy)
in self.pieces[x][y].connected_directions()
):
connected_neighbors.append((nx, ny))
for nx, ny in connected_neighbors:
if connected[nx][ny]:
continue
connected = self._solve_floodfill(nx, ny, connected)
return connected
class NetGame:
_grid: Grid
width: int
height: int
def __init__(self, width: int, height: int, specific: int | None = None) -> None:
self._grid = Grid(width, height, specific)
self.width = width
self.height = height
def get_field(self) -> list[list[Piece]]:
return self._grid.pieces
def get_piece(self, x: int, y: int) -> Piece:
return self._grid.pieces[x][y]
def turn_cw(self, x: int, y: int) -> None:
self._grid.pieces[x][y].turn_cw()
def turn_ccw(self, x: int, y: int) -> None:
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):
return self._grid.solved()

64
src/netTypes.py Normal file
View File

@@ -0,0 +1,64 @@
from enum import IntEnum, auto
type Coordinate = tuple[int, int]
class PieceType(IntEnum):
T_JUNCTION = auto() # Direction is the piece at the "stem" of the T
STRAIGHT = auto()
CORNER = auto() # Direction is the more counter-clockwise connection
NODE = auto()
class Direction(IntEnum):
UP = 0
RIGHT = auto()
DOWN = auto()
LEFT = auto()
@staticmethod
def from_offset(dx: int, dy: int) -> Direction:
if (dx != 0 and dy != 0) or abs(dx + dy) != 1:
raise ValueError(f"({dx}, {dy}) is not a valid direction offset")
if dx != 0:
return Direction(dx % 4)
else:
return Direction(dy + 1)
def flip(self) -> Direction:
return Direction((self + 2) % 4)
class Piece:
type: PieceType
direction: Direction
locked: bool = False
def __init__(self, type: PieceType, direction: Direction = Direction.UP) -> None:
self.type = type
self.direction = direction
def connected_directions(self) -> list[Direction]:
match self.type:
case PieceType.NODE:
return [self.direction]
case PieceType.CORNER:
return [self.direction, Direction((self.direction + 1) % 4)]
case PieceType.STRAIGHT:
return [self.direction, Direction((self.direction + 2) % 4)]
case PieceType.T_JUNCTION:
return [
self.direction,
Direction((self.direction + 1) % 4),
Direction((self.direction - 1) % 4),
]
def turn_cw(self) -> None:
if self.locked:
return
self.direction = Direction((self.direction + 1) % 4)
def turn_ccw(self) -> None:
if self.locked:
return
self.direction = Direction((self.direction - 1) % 4)

65
uv.lock generated Normal file
View File

@@ -0,0 +1,65 @@
version = 1
revision = 3
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]]
name = "maturaarbeit"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "pygame" },
]
[package.dev-dependencies]
dev = [
{ name = "basedpyright" },
]
[package.metadata]
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]]
name = "pygame"
version = "2.6.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/49/cc/08bba60f00541f62aaa252ce0cfbd60aebd04616c0b9574f755b583e45ae/pygame-2.6.1.tar.gz", hash = "sha256:56fb02ead529cee00d415c3e007f75e0780c655909aaa8e8bf616ee09c9feb1f", size = 14808125, upload-time = "2024-09-29T13:41:34.698Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/91/718acf3e2a9d08a6ddcc96bd02a6f63c99ee7ba14afeaff2a51c987df0b9/pygame-2.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae6039f3a55d800db80e8010f387557b528d34d534435e0871326804df2a62f2", size = 13090765, upload-time = "2024-09-29T14:27:02.377Z" },
{ url = "https://files.pythonhosted.org/packages/0e/c6/9cb315de851a7682d9c7568a41ea042ee98d668cb8deadc1dafcab6116f0/pygame-2.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2a3a1288e2e9b1e5834e425bedd5ba01a3cd4902b5c2bff8ed4a740ccfe98171", size = 12381704, upload-time = "2024-09-29T14:27:10.228Z" },
{ url = "https://files.pythonhosted.org/packages/9f/8f/617a1196e31ae3b46be6949fbaa95b8c93ce15e0544266198c2266cc1b4d/pygame-2.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27eb17e3dc9640e4b4683074f1890e2e879827447770470c2aba9f125f74510b", size = 13581091, upload-time = "2024-09-29T11:30:27.653Z" },
{ url = "https://files.pythonhosted.org/packages/3b/87/2851a564e40a2dad353f1c6e143465d445dab18a95281f9ea458b94f3608/pygame-2.6.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c1623180e70a03c4a734deb9bac50fc9c82942ae84a3a220779062128e75f3b", size = 14273844, upload-time = "2024-09-29T11:40:04.138Z" },
{ url = "https://files.pythonhosted.org/packages/85/b5/aa23aa2e70bcba42c989c02e7228273c30f3b44b9b264abb93eaeff43ad7/pygame-2.6.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef07c0103d79492c21fced9ad68c11c32efa6801ca1920ebfd0f15fb46c78b1c", size = 13951197, upload-time = "2024-09-29T11:40:06.785Z" },
{ url = "https://files.pythonhosted.org/packages/a6/06/29e939b34d3f1354738c7d201c51c250ad7abefefaf6f8332d962ff67c4b/pygame-2.6.1-cp313-cp313-win32.whl", hash = "sha256:3acd8c009317190c2bfd81db681ecef47d5eb108c2151d09596d9c7ea9df5c0e", size = 10249309, upload-time = "2024-09-29T11:10:23.329Z" },
{ url = "https://files.pythonhosted.org/packages/7e/11/17f7f319ca91824b86557e9303e3b7a71991ef17fd45286bf47d7f0a38e6/pygame-2.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:813af4fba5d0b2cb8e58f5d95f7910295c34067dcc290d34f1be59c48bd1ea6a", size = 10620084, upload-time = "2024-09-29T11:48:51.587Z" },
]