mazes2/maze.py

640 lines
20 KiB
Python

import abc, enum, random
class Maze(abc.ABC):
@property
@abc.abstractmethod
def width(self) -> int:
pass
@property
@abc.abstractmethod
def height(self) -> int:
pass
@property
def highlighted(self) -> tuple[int,int] | None:
pass
@abc.abstractmethod
def __getitem__(self, index: tuple[int,int]) -> bool:
pass
@abc.abstractmethod
def step(self) -> bool: # returns False when algorithm is done
pass
def __str__(self) -> str:
s = ""
for y in range(self.height):
for x in range(self.width):
s += ' ' if self[x,y] else 'x'
s += '\n'
return s
def __list__(self) -> list[list[bool]]:
return [list([self[x,y] for x in range(self.width)]) for y in range(self.height)]
def _neighbors(self, index: tuple[int,int]) -> list[tuple[int,int]]:
neighbors: list[tuple[int,int]] = []
x, y = index
if x - 2 >= 0:
neighbors.append((x - 2,y))
if x + 2 < self.width:
neighbors.append((x + 2,y))
if y - 2 >= 0:
neighbors.append((x,y - 2))
if y + 2 < self.height:
neighbors.append((x,y + 2))
return neighbors
class VectorEnum(enum.Enum):
Null = -1
Zero = 0
Up = 2
Down = 4
Left = 8
Right = 16
def __or__(self, value: "VectorEnum | int") -> int:
if not isinstance(value, int): value = value.value
return self.value | value
def __ror__(self, value: "VectorEnum | int") -> int:
if not isinstance(value, int): value = value.value
return self.value | value
def __and__(self, value: "VectorEnum | int") -> "VectorEnum":
if not isinstance(value, int): value = value.value
return VectorEnum(self.value & value)
def __rand__(self, value: "VectorEnum | int") -> "VectorEnum":
if not isinstance(value, int): value = value.value
return VectorEnum(self.value & value)
class VectorMaze(Maze):
@abc.abstractmethod
def __getitem__(self, index: tuple[int,int]) -> VectorEnum:
pass
def __list__(self) -> list[list[VectorEnum]]:
return [list([self[x,y] for x in range(self.width)]) for y in range(self.height)]
def _neighbors(self, index: tuple[int,int]) -> list[tuple[int,int]]:
neighbors: list[tuple[int,int]] = []
x, y = index
if x - 1 >= 0:
neighbors.append((x - 1,y))
if x + 1 < self.width:
neighbors.append((x + 1,y))
if y - 1 >= 0:
neighbors.append((x,y - 1))
if y + 1 < self.height:
neighbors.append((x,y + 1))
return neighbors
class VectorWrapper(Maze):
__maze: VectorMaze
def __init__(self, maze: VectorMaze):
self.__maze = maze
@property
def width(self) -> int:
return self.__from_vec(self.__maze.width)
@property
def height(self) -> int:
return self.__from_vec(self.__maze.height)
@property
def highlighted(self) -> tuple[int,int] | None:
if self.__maze.highlighted is not None:
return self.__from_vec(self.__maze.highlighted[0]), self.__from_vec(self.__maze.highlighted[1])
else: return None
def __getitem__(self, index: tuple[int,int]) -> bool:
x, y = index
if x % 2 and y % 2:
if self.__maze[self.__to_vec(x), self.__to_vec(y)] != VectorEnum.Null:
return True
elif x % 2:
if self.__to_vec(y-1) >= 0:
if self.__maze[self.__to_vec(x), self.__to_vec(y-1)] == VectorEnum.Down:
return True
if self.__to_vec(y+1) < self.__maze.height:
if self.__maze[self.__to_vec(x), self.__to_vec(y+1)] == VectorEnum.Up:
return True
elif y % 2:
if self.__to_vec(x-1) >= 0:
if self.__maze[self.__to_vec(x-1), self.__to_vec(y)] == VectorEnum.Right:
return True
if self.__to_vec(x+1) < self.__maze.width:
if self.__maze[self.__to_vec(x+1), self.__to_vec(y)] == VectorEnum.Left:
return True
return False
def step(self) -> bool: return self.__maze.step()
@property
def maze(self) -> VectorMaze:
return self.__maze
@staticmethod
def __from_vec(i: int) -> int:
return ((i * 2) + 1)
@staticmethod
def __to_vec(i: int) -> int:
return ((i - 1) // 2)
class RecursiveBacktracker(Maze):
__cells: list[list[bool]]
__stack: list[tuple[int,int]]
__width: int
__height: int
def __init__(self, width: int, height: int | None = None):
self.__width = width
self.__height = height or width
self.__cells = [list([False for _ in range(self.width)]) for _ in range(self.height)]
self.__stack = [(
(random.randint(0, ((self.width - 1) // 2) - 1) * 2) + 1,
(random.randint(0, ((self.height - 1) // 2) - 1) * 2) + 1
)]
self.__cells[self.highlighted[1]][self.highlighted[0]] = True # type: ignore
@property
def width(self) -> int:
return self.__width
@property
def height(self) -> int:
return self.__height
@property
def highlighted(self) -> tuple[int,int] | None:
if self.__stack: return self.__stack[-1]
else: return None
def __getitem__(self, index: tuple[int,int]) -> bool:
return self.__cells[index[1]][index[0]]
def step(self) -> bool:
if self.highlighted is not None:
neighbors = self.__unvisited_neighbors(self.highlighted)
if neighbors:
cell = neighbors[random.randint(0,len(neighbors)-1)]
x = (self.highlighted[0] + cell[0]) // 2
y = (self.highlighted[1] + cell[1]) // 2
self.__cells[y][x] = True
self.__cells[cell[1]][cell[0]] = True
self.__stack.append(cell)
else: self.__stack.pop()
return True
else: return False
def __unvisited_neighbors(self, index: tuple[int,int]) -> list[tuple[int,int]]:
return [(x,y) for x,y in self._neighbors(index) if not self[x,y]]
class OriginShift(VectorMaze):
__cells: list[list[VectorEnum]]
__width: int
__height: int
def __init__(self, width: int, height: int | None = None):
self.__width = width
self.__height = height or width
self.__cells = [list([self.__start(x,y) for x in range(self.width)]) for y in range(self.height)]
@property
def width(self) -> int:
return self.__width
@property
def height(self) -> int:
return self.__height
@property
def highlighted(self) -> tuple[int,int] | None:
for y, row in enumerate(self.__cells):
for x, cell in enumerate(row):
if cell == VectorEnum.Zero:
return (x, y)
def __getitem__(self, index: tuple[int,int]) -> VectorEnum:
x, y = index
return self.__cells[y][x]
def step(self) -> bool:
if self.highlighted is not None:
x, y = self.highlighted
neighbors = self._neighbors((x,y))
if neighbors:
cell = neighbors[random.randint(0,len(neighbors)-1)]
self.__cells[y][x] = self.__direction((x,y), cell)
self.__cells[cell[1]][cell[0]] = VectorEnum.Zero
return True
else: return False
@classmethod
def clone(cls, other: VectorMaze) -> "OriginShift":
self = cls(other.width, other.height)
self.__cells = [list([other[x,y] for x in range(other.width)]) for y in range(other.height)]
return self
def __direction(self, start: tuple[int,int], end: tuple[int,int]) -> VectorEnum:
if start[0] - end[0] > 0: return VectorEnum.Left
if start[0] - end[0] < 0: return VectorEnum.Right
if start[1] - end[1] > 0: return VectorEnum.Up
if start[1] - end[1] < 0: return VectorEnum.Down
return VectorEnum.Zero
def __start(self, x: int, y: int) -> VectorEnum:
if x == self.width - 1 and y == self.height - 1: return VectorEnum.Zero
elif x == self.width - 1: return VectorEnum.Down
else: return VectorEnum.Right
class BinaryTree(VectorMaze):
__cells: list[list[VectorEnum]]
__width: int
__height: int
__bias: int
__x: int
__y: int
def __init__(self, width: int, height: int | None = None, *, bias: int | None = None ):
self.__width = width
self.__height = height or width
self.__bias = bias or (VectorEnum.Up | VectorEnum.Left)
self.__x = 0
self.__y = 0
self.__cells = [list([VectorEnum.Null for x in range(self.width)]) for y in range(self.height)]
@property
def width(self) -> int:
return self.__width
@property
def height(self) -> int:
return self.__height
@property
def highlighted(self) -> tuple[int,int] | None:
if self.__x < self.width and self.__y < self.height:
return self.__x, self.__y
def __getitem__(self, index: tuple[int,int]) -> VectorEnum:
x, y = index
return self.__cells[y][x]
def step(self) -> bool:
if self.highlighted is not None:
neighbors = self._neighbors(self.highlighted)
if neighbors:
cell = neighbors[random.randint(0,len(neighbors)-1)]
self.__cells[self.__y][self.__x] = self.__direction(self.highlighted, cell)
else:
self.__cells[self.__y][self.__x] = VectorEnum.Zero
self.__x += 1
if self.__x >= self.width:
self.__x = 0
self.__y += 1
return True
else: return False
def __direction(self, start: tuple[int,int], end: tuple[int,int]) -> VectorEnum:
if start[0] - end[0] > 0: return VectorEnum.Left
if start[0] - end[0] < 0: return VectorEnum.Right
if start[1] - end[1] > 0: return VectorEnum.Up
if start[1] - end[1] < 0: return VectorEnum.Down
return VectorEnum.Zero
def _neighbors(self, index: tuple[int,int]) -> list[tuple[int,int]]:
neighbors: list[tuple[int,int]] = []
x, y = index
if x - 1 >= 0 and (self.__bias & VectorEnum.Left) == VectorEnum.Left:
neighbors.append((x - 1,y))
if x + 1 < self.width and (self.__bias & VectorEnum.Right) == VectorEnum.Right:
neighbors.append((x + 1,y))
if y - 1 >= 0 and (self.__bias & VectorEnum.Up) == VectorEnum.Up:
neighbors.append((x,y - 1))
if y + 1 < self.height and (self.__bias & VectorEnum.Down) == VectorEnum.Down:
neighbors.append((x,y + 1))
return neighbors
class Sidewinder(Maze):
__cells: list[list[bool]]
__run: list[tuple[int,int]]
__run_param: float
__start: VectorEnum
__width: int
__height: int
def __init__(
self,
width: int,
height: int | None = None,
*,
run_param: float = 0.5,
start: VectorEnum = VectorEnum.Up,
):
self.__width = width
self.__height = height or width
self.__cells = [list([False for _ in range(self.width)]) for _ in range(self.height)]
self.__start = start
self.__run_param = run_param
if self.__start == VectorEnum.Up:
self.__run = [(1,1)]
elif self.__start == VectorEnum.Down:
self.__run = [(self.width-2,self.height-2)]
elif self.__start == VectorEnum.Right:
self.__run = [(self.width-2,self.height-2)]
elif self.__start == VectorEnum.Left:
self.__run = [(1,1)]
else:
self.__run = []
self.__cells[self.highlighted[1]][self.highlighted[0]] = True # type: ignore
@property
def width(self) -> int:
return self.__width
@property
def height(self) -> int:
return self.__height
@property
def highlighted(self) -> tuple[int,int] | None:
if self.__run: return self.__run[-1]
else: return None
def __getitem__(self, index: tuple[int,int]) -> bool:
return self.__cells[index[1]][index[0]]
def step(self) -> bool:
if self.highlighted is not None:
hx, hy = self.highlighted
if hx - 2 >= 0 and self.__start == VectorEnum.Down:
cell = (hx - 2, hy)
carve = hy + 2 >= self.height or random.random() > self.__run_param
elif hx + 2 < self.width and self.__start == VectorEnum.Up:
cell = (hx + 2, hy)
carve = hy - 2 < 0 or random.random() > self.__run_param
elif hy - 2 >= 0 and self.__start == VectorEnum.Right:
cell = (hx, hy - 2)
carve = hx + 2 >= self.width or random.random() > self.__run_param
elif hy + 2 < self.height and self.__start == VectorEnum.Left:
cell = (hx, hy + 2)
carve = hx - 2 < 0 or random.random() > self.__run_param
else:
cell = None
carve = False
if cell is not None and carve:
x = (hx + cell[0]) // 2
y = (hy + cell[1]) // 2
self.__cells[y][x] = True
self.__cells[cell[1]][cell[0]] = True
self.__run.append(cell)
else:
hx, hy = self.highlighted
sx, sy = self.__run[random.randint(0,len(self.__run)-1)]
self.__run.clear()
if self.__start == VectorEnum.Down:
if hx - 2 >= 0:
x, y = (hx - 2, hy)
self.__run.append((x,y))
self.__cells[y][x] = True
else:
if hy - 2 >= 0:
x, y = (self.width - 2, hy - 2)
self.__run.append((x,y))
self.__cells[y][x] = True
if sy + 2 < self.height: cell = (sx, sy + 2)
else: cell = None
elif self.__start == VectorEnum.Up:
if hx + 2 < self.width:
x, y = (hx + 2, hy)
self.__run.append((x,y))
self.__cells[y][x] = True
else:
if hy + 2 < self.height:
x, y = (1, hy + 2)
self.__run.append((x,y))
self.__cells[y][x] = True
if sy - 2 >= 0: cell = (sx, sy - 2)
else: cell = None
elif self.__start == VectorEnum.Right:
if hy - 2 >= 0:
x, y = (hx, hy - 2)
self.__run.append((x,y))
self.__cells[y][x] = True
else:
if hx - 2 >= 0:
x, y = (hx - 2, self.height - 2)
self.__run.append((x,y))
self.__cells[y][x] = True
if sx + 2 < self.height: cell = (sx + 2, sy)
else: cell = None
elif self.__start == VectorEnum.Left:
if hy + 2 < self.height:
x, y = (hx, hy + 2)
self.__run.append((x,y))
self.__cells[y][x] = True
else:
if hx + 2 < self.width:
x, y = (hx + 2, 1)
self.__run.append((x,y))
self.__cells[y][x] = True
if sx - 2 >= 0: cell = (sx - 2, sy)
else: cell = None
if cell is not None:
x = (sx + cell[0]) // 2
y = (sy + cell[1]) // 2
self.__cells[y][x] = True
return True
else: return False
class _Window:
__maze: "RecursiveDivision"
__x_start: int
__x_stop: int
__y_start: int
__y_stop: int
def __init__(self, maze: "RecursiveDivision", x_start: int, x_stop: int, y_start: int, y_stop: int):
self.__maze = maze
self.__x_start = x_start
self.__x_stop = x_stop
self.__y_start = y_start
self.__y_stop = y_stop
@property
def width(self) -> int:
return self.__x_stop - self.__x_start
@property
def height(self) -> int:
return self.__y_stop - self.__y_start
def __getitem__(self, index: tuple[int,int]) -> bool:
return self.__maze[index[1] - self.__x_start, index[0] - self.__y_start]
def x(self, x: int) -> int: return x + self.__x_start
def y(self, y: int) -> int: return y + self.__y_start
def vertical_bisect(self, x: int) -> "tuple[_Window, _Window]":
return _Window(
self.__maze,
self.__x_start,
self.__x_start + x,
self.__y_start,
self.__y_stop,
), _Window(
self.__maze,
self.__x_start + x + 1,
self.__x_stop,
self.__y_start,
self.__y_stop,
)
def horizontal_bisect(self, y: int) -> "tuple[_Window, _Window]":
return _Window(
self.__maze,
self.__x_start,
self.__x_stop,
self.__y_start,
self.__y_start + y,
), _Window(
self.__maze,
self.__x_start,
self.__x_stop,
self.__y_start + y + 1,
self.__y_stop,
)
class RecursiveDivision(Maze):
__cells: list[list[bool]]
__stack: list[tuple[bool | None, _Window]]
__split: tuple[bool, int, _Window] | None
__binary: bool
__uniform: bool
__depth_first: bool
__width: int
__height: int
def __init__(
self,
width: int,
height: int | None = None,
*,
binary: bool = False,
uniform: bool = True,
depth_first: bool = True,
):
self.__width = width
self.__height = height or width
self.__binary = binary
self.__uniform = uniform
self.__depth_first = depth_first
self.__cells = [
list([(0 < x < self.width-1 and 0 < y < self.height-1)
for x in range(self.width)]) for y in range(self.height)]
self.__stack = [
((True if self.__uniform else None),
_Window(self,1,self.width,1,self.height))]
self.__split = None
@property
def width(self) -> int:
return self.__width
@property
def height(self) -> int:
return self.__height
def __getitem__(self, index: tuple[int,int]) -> bool:
return self.__cells[index[1]][index[0]]
def step(self) -> bool:
if self.__split is not None:
if self.__split[0]:
axis, y, window = self.__split
x = (random.randint(0, (window.width - 1) // 2) * 2)
self.__cells[window.y(y)][window.x(x)] = True
a, b = [
((not axis if self.__uniform else None), w)
for w in window.horizontal_bisect(y)]
else:
axis, x, window = self.__split
y = (random.randint(0, (window.height - 1) // 2) * 2)
self.__cells[window.y(y)][window.x(x)] = True
a, b = [
((not axis if self.__uniform else None), w)
for w in window.vertical_bisect(x)]
if a[1].width > 2 and a[1].height > 2: self.__stack.append(a)
if b[1].width > 2 and b[1].height > 2: self.__stack.append(b)
self.__split = None
return True
elif self.__stack:
axis, window = self.__stack.pop(-1 if self.__depth_first else 0)
if axis is None: axis = bool(random.randint(0,1))
if axis:
if self.__binary: y = (((window.height - 3) // 4) * 2) + 1
else: y = (random.randint(0, (window.height - 3) // 2) * 2) + 1
for x in range(window.width):
self.__cells[window.y(y)][window.x(x)] = False
self.__split = axis, y, window
else:
if self.__binary: x = (((window.width - 3) // 4) * 2) + 1
else: x = (random.randint(0, (window.width - 3) // 2) * 2) + 1
for y in range(window.height):
self.__cells[window.y(y)][window.x(x)] = False
self.__split = axis, x, window
return True
else: return False