Compare commits
6 Commits
21b9ce7ad7
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4ef3c7555 | ||
|
|
8139af00d5 | ||
|
|
d39e6ecc25 | ||
|
|
62864b7c00 | ||
|
|
f3a6257cd4 | ||
|
|
52142da3f3 |
BIN
assets/1.bmp
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
BIN
assets/1on.bmp
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
BIN
assets/2.bmp
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
BIN
assets/2on.bmp
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
BIN
assets/3.bmp
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
BIN
assets/3on.bmp
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
BIN
assets/4.bmp
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
BIN
assets/4on.bmp
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
19
src/GUI.py
@@ -1,9 +1,9 @@
|
|||||||
import os
|
import os
|
||||||
from time import sleep
|
|
||||||
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.algorithms.bruteforce import BruteForceSolver
|
||||||
from src.net import NetGame
|
from src.net import NetGame
|
||||||
|
|
||||||
@@ -32,8 +32,9 @@ 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.set_colorkey("#ffffff")
|
os.path.join("assets", f"{piece.type}.bmp")
|
||||||
|
).convert_alpha()
|
||||||
image = pygame.transform.rotate(image, -90 * int(piece.direction))
|
image = pygame.transform.rotate(image, -90 * int(piece.direction))
|
||||||
if piece.locked:
|
if piece.locked:
|
||||||
self.image.fill("#a0a0a0")
|
self.image.fill("#a0a0a0")
|
||||||
@@ -87,6 +88,7 @@ class NetGUI:
|
|||||||
def run_game(self):
|
def run_game(self):
|
||||||
current_solver = None
|
current_solver = None
|
||||||
display_solver = False
|
display_solver = False
|
||||||
|
step_solver = False
|
||||||
while not self.game.solved():
|
while not self.game.solved():
|
||||||
events = pygame.event.get()
|
events = pygame.event.get()
|
||||||
|
|
||||||
@@ -102,16 +104,21 @@ class NetGUI:
|
|||||||
elif event.type == pygame.KEYDOWN:
|
elif event.type == pygame.KEYDOWN:
|
||||||
if event.key == pygame.K_b:
|
if event.key == pygame.K_b:
|
||||||
current_solver = BruteForceSolver(self.game).solve()
|
current_solver = BruteForceSolver(self.game).solve()
|
||||||
if event.key == pygame.K_d:
|
elif event.key == pygame.K_d:
|
||||||
display_solver = not display_solver
|
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:
|
if current_solver:
|
||||||
try:
|
try:
|
||||||
_ = next(current_solver)
|
_ = next(current_solver)
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
current_solver = None
|
current_solver = None
|
||||||
|
if step_solver:
|
||||||
|
_ = input()
|
||||||
if (not current_solver) or display_solver:
|
if (not current_solver) or display_solver:
|
||||||
self.update_display(events)
|
self.update_display(events)
|
||||||
sleep(2)
|
|
||||||
|
|
||||||
|
|
||||||
NetGUI(3, 3).run_game()
|
NetGUI(5, 5).run_game()
|
||||||
|
|||||||
@@ -15,16 +15,8 @@ class BruteForceSolver:
|
|||||||
self.game.set_direction(x, y, Direction.UP)
|
self.game.set_direction(x, y, Direction.UP)
|
||||||
|
|
||||||
def solve(self) -> Generator[None]:
|
def solve(self) -> Generator[None]:
|
||||||
attempts = 0
|
|
||||||
required = 4 ** (self.game.width * self.game.height)
|
|
||||||
while not self.game.solved():
|
while not self.game.solved():
|
||||||
yield
|
yield
|
||||||
attempts += 1
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"{attempts:0>{len(str(required))}}/{required} ({attempts / required * 100}%) ",
|
|
||||||
end="\r",
|
|
||||||
)
|
|
||||||
self.game.turn_cw(0, 0)
|
self.game.turn_cw(0, 0)
|
||||||
for prev, curr in pairwise(
|
for prev, curr in pairwise(
|
||||||
chain(
|
chain(
|
||||||
|
|||||||
25
src/algorithms/profile.py
Normal 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
@@ -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
|
||||||
|
)
|
||||||
13
src/net.py
@@ -13,14 +13,18 @@ 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
|
||||||
if width != height or width not in [3, 5, 7, 9, 11, 13]:
|
if width != height or width not in [3, 5, 7, 9, 11, 13]:
|
||||||
raise ValueError("Feldgrösse nicht erlaubt")
|
raise ValueError("Feldgrösse nicht erlaubt")
|
||||||
with open(f"descriptions/{width}x{height}.txt") as f:
|
with open(f"descriptions/{width}x{height}.txt") as f:
|
||||||
lines = f.readlines()
|
lines = f.readlines()
|
||||||
|
if specific is not None:
|
||||||
|
selected = lines[specific].strip()
|
||||||
|
else:
|
||||||
selected = choice(lines).strip()
|
selected = choice(lines).strip()
|
||||||
|
print(f"Seed: {lines.index(selected + "\n")}")
|
||||||
self.pieces = parse_description(selected)
|
self.pieces = parse_description(selected)
|
||||||
|
|
||||||
def neighbors(self, x: int, y: int) -> list[Coordinate]:
|
def neighbors(self, x: int, y: int) -> list[Coordinate]:
|
||||||
@@ -71,8 +75,8 @@ class NetGame:
|
|||||||
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._grid = Grid(width, height)
|
self._grid = Grid(width, height, specific)
|
||||||
self.width = width
|
self.width = width
|
||||||
self.height = height
|
self.height = height
|
||||||
|
|
||||||
@@ -94,5 +98,8 @@ class NetGame:
|
|||||||
def set_direction(self, x: int, y: int, dir: Direction) -> None:
|
def set_direction(self, x: int, y: int, dir: Direction) -> None:
|
||||||
self._grid.pieces[x][y].direction = dir
|
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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||