Compare commits

...

6 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
14 changed files with 243 additions and 18 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

View File

@@ -1,9 +1,9 @@
import os
from time import sleep
from typing import override
import pygame
from src.algorithms.wfc import WFCSolver
from src.algorithms.bruteforce import BruteForceSolver
from src.net import NetGame
@@ -32,8 +32,9 @@ class PieceSprite(pygame.sprite.Sprite):
@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()
image.set_colorkey("#ffffff")
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")
@@ -87,6 +88,7 @@ class NetGUI:
def run_game(self):
current_solver = None
display_solver = False
step_solver = False
while not self.game.solved():
events = pygame.event.get()
@@ -102,16 +104,21 @@ class NetGUI:
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_b:
current_solver = BruteForceSolver(self.game).solve()
if event.key == pygame.K_d:
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)
sleep(2)
NetGUI(3, 3).run_game()
NetGUI(5, 5).run_game()

View File

@@ -15,16 +15,8 @@ class BruteForceSolver:
self.game.set_direction(x, y, Direction.UP)
def solve(self) -> Generator[None]:
attempts = 0
required = 4 ** (self.game.width * self.game.height)
while not self.game.solved():
yield
attempts += 1
print(
f"{attempts:0>{len(str(required))}}/{required} ({attempts / required * 100}%) ",
end="\r",
)
self.game.turn_cw(0, 0)
for prev, curr in pairwise(
chain(

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

@@ -13,14 +13,18 @@ class 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.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()
selected = choice(lines).strip()
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]:
@@ -71,8 +75,8 @@ class NetGame:
width: int
height: int
def __init__(self, width: int, height: int) -> None:
self._grid = Grid(width, height)
def __init__(self, width: int, height: int, specific: int | None = None) -> None:
self._grid = Grid(width, height, specific)
self.width = width
self.height = height
@@ -94,5 +98,8 @@ class NetGame:
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()

View File

@@ -25,6 +25,9 @@ class Direction(IntEnum):
else:
return Direction(dy + 1)
def flip(self) -> Direction:
return Direction((self + 2) % 4)
class Piece:
type: PieceType