Compare commits

..

4 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
13 changed files with 191 additions and 104 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

@@ -1,13 +1,14 @@
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() -> float:
game = NetGame(3, 3)
solver = BruteForceSolver(game)
def test_run(i: int) -> float:
game = NetGame(11, 11, i)
solver = WFCSolver(game)
a = time.perf_counter()
for _ in solver.solve():
pass
@@ -18,7 +19,7 @@ def test_run() -> float:
if __name__ == "__main__":
total = 0
with Pool() as p:
processes = [p.apply_async(test_run) for _ in range(100)]
processes = [p.apply_async(test_run, (i,)) for i in range(1000)]
for proc in processes:
total += proc.get()
print(total)
print(total / 1000)

View File

@@ -1,122 +1,191 @@
from netTypes import Direction, PieceType
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
observed: list[list[bool]]
possible_connections: list[list[set[Direction]]]
connection_states: list[list[PieceConnectionState]]
def __init__(self, game: NetGame) -> None:
self.game = game
self.observed = [[False] * self.game.width for _ in range(self.game.height)]
self.possible_connections = [
[
{Direction.UP, Direction.RIGHT, Direction.DOWN, Direction.LEFT}
for _ in range(self.game.width)
]
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.possible_connections:
column[0].remove(Direction.UP)
column[-1].remove(Direction.DOWN)
for column in self.connection_states:
column[0].up = ConnectionState.DISCONNECTED
column[-1].down = ConnectionState.DISCONNECTED
for i in range(len(self.possible_connections[0])):
self.possible_connections[0][i].remove(Direction.LEFT)
for s in self.connection_states[0]:
s.left = ConnectionState.DISCONNECTED
for i in range(len(self.possible_connections[-1])):
self.possible_connections[-1][i].remove(Direction.RIGHT)
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
dirs = self.possible_connections[x][y]
cstates = self.connection_states[x][y]
dirs = cstates.connected_or_unknown()
if ptype == PieceType.NODE:
return dirs
elif ptype == PieceType.CORNER:
possible: set[Direction] = set()
if dirs.union({Direction.UP, Direction.RIGHT}):
possible.add(Direction.UP)
if dirs.union({Direction.RIGHT, Direction.DOWN}):
possible.add(Direction.RIGHT)
if dirs.union({Direction.DOWN, Direction.LEFT}):
possible.add(Direction.DOWN)
if dirs.union({Direction.LEFT, Direction.UP}):
possible.add(Direction.LEFT)
return possible
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 {Direction.UP, Direction.DOWN}.issubset(dirs):
return {Direction.UP}
elif {Direction.LEFT, Direction.RIGHT}.issubset(dirs):
return {Direction.RIGHT}
elif disconnected:
return {disconnected.pop().flip()}
else:
return set()
else:
if len(dirs) == 4:
return dirs
elif len(dirs) == 3:
not_connected = (
{Direction.UP, Direction.DOWN, Direction.LEFT, Direction.RIGHT}
.difference(dirs)
.pop()
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 {Direction((not_connected + 2) % 4)}
else:
return set()
return possible
def get_possible_direction_count(self, x: int, y: int) -> int:
ptype = self.game.get_piece(x, y).type
connectable = self.possible_connections[x][y]
camount = len(connectable)
if camount == 4:
return 4
if ptype == PieceType.NODE:
return camount
elif ptype == PieceType.CORNER:
if camount == 3:
return 2
elif (
camount == 2
and connectable != {Direction.UP, Direction.DOWN}
and connectable != {Direction.LEFT, Direction.RIGHT}
): # 2 nicht-gegenüberliegende seiten
return 1
else:
return 0
elif ptype == PieceType.STRAIGHT:
if camount == 4:
return 4
elif (
camount == 3
or connectable == {Direction.UP, Direction.DOWN}
or connectable == {Direction.LEFT, Direction.RIGHT}
):
return 1
else:
return 0
else: # T-JUNCTION
if camount == 4:
return 4
elif camount == 3:
return 1
else:
return 0
def get_field_rotations(self) -> list[list[Direction]]:
return [[p.direction for p in col] for col in self.game.get_field()]
def solve(self) -> None:
coordinates = list(
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(
coordinates, key=lambda t: self.get_possible_direction_count(*t)
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),
)
if self.get_possible_direction_count(x, y) == 0:
raise NotImplementedError(
"Irgendwo eine falsche Richtung gewählt, muss diese Logik noch schreiben"
)
direction = self.get_possible_directions(x, y).pop()
self.game.set_direction(x, y, direction)
# TODO: Nachbaren updaten
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()
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