Skip to content
Snippets Groups Projects
Unverified Commit a574dbf0 authored by STEVAN Antoine's avatar STEVAN Antoine :crab:
Browse files

add src.ui.maze and a README

parent 08acbeb8
No related branches found
No related tags found
No related merge requests found
Below are snippets that show how to define a maze and use the `src.ui.maze` module:
- import the library
```python
from src.ui.maze import Canva
```
- define the maze, it should be a dictionary with the same keys as show below
```python
maze = {
"width": 32,
"height": 32,
"vertices": [],
"edges": [],
"size": 25,
}
```
- create the canva that will hold the animation
```python
canva = Canva(
maze["size"] * maze["width"],
maze["size"] * maze["height"],
caption="",
frame_rate=60,
)
canva.setup()
```
- your "_build_" function that will create the maze should take an optional
`callback` argument which should be a function that takes a maze as input and
calls `Canva.step`
```python
lambda m: canva.step(m)
```
- finally, run the main loop
```python
# setup some variables
p = []
complete = False
# loop 'til the end of time
while True:
# run the animation and get back a signal from the canva
res = canva.loop(maze, path=p, complete=complete)
# if the signal is a _falsy_ boolean, then the _path_ and _complete_ should
# be reset
if isinstance(res, bool) and not res:
p = []
complete = False
# continue to loop and wait for another input
continue
# otherwise, the canva just sent back the positions of the path to compute
(i_s, j_s), (i_e, j_e) = res
p = path(
...
callback=lambda m, p, v, n: canva.step(m, p, v, n, complete=False),
)
complete = True
```
## key bindings
- _SPACE_ to pause and unpause the animation
- _move the mouse_ to preselect a cell
- _left click_ on two different maze cells to select them
- _right click_ to unselect all the cells
- _ENTER_ to run the path algorithm once the maze generation is done
import pygame
import logging
from collections import namedtuple
from typing import Tuple
_VALID_SQUARE_GRID_STYLES = ["lines", "cells"]
SquareGridStyle = namedtuple(
"SquareGridStyle",
["type", 'm', 'r', 'w'],
defaults=["lines", .9, .1, 5],
)
def square(
screen: pygame.surface.Surface,
width: int,
height: int,
size: int,
color: Tuple[int, int, int],
style: SquareGridStyle = SquareGridStyle(),
):
if style.type not in _VALID_SQUARE_GRID_STYLES:
logging.error(
f"invalid square grid style '{style}', expected one of {_VALID_SQUARE_GRID_STYLES}"
)
return
if style.type == "lines":
w, h = screen.get_size()
for i in range(1, height):
pygame.draw.line(
screen, color, (0, i * size), (w, i * size), width=style.w
)
for j in range(1, width):
pygame.draw.line(
screen, color, (j * size, 0), (j * size, h), width=style.w
)
elif style.type == "cells":
d = (style.m * size) // 2
s = int(size * style.m)
for i in range(height):
for j in range(width):
cell(screen, i, j, size, color, d, style.r, s)
def cell(
screen: pygame.surface.Surface,
i: int,
j: int,
size: int,
color: Tuple[int, int, int],
d: int,
r: int,
s: int,
):
c_x, c_y = int((j + .5) * size), int((i + .5) * size)
pygame.draw.rect(
screen,
color,
(c_x - d, c_y - d, s, s),
border_radius=int(size * r),
)
from dataclasses import dataclass
import pygame
import sys
import itertools
from typing import Tuple, List, Union
from math import cos, sin, pi
from src.ui import grid
StartEndPair = Tuple[Tuple[int, int], Tuple[int, int]]
COLORS = {
"background": (0, 0, 0),
"empty cell": (200, 200, 200),
"selected cell": (200, 200, 0),
"cell under mouse": (0, 200, 200),
"visited cell": (50, 50, 50),
"visited cell link": (25, 25, 25),
"next cell": (0, 255, 0),
"cell": (0, 128, 0),
"link": (0, 102, 0),
"walls": (200, 200, 200),
"path": (100, 0, 0),
"path link": (80, 0, 0),
"path ends": (255, 165, 0),
"path ends link": (255 * 0.7, 165 * 0.7, 0),
"path ends ends": (255, 0, 255),
"pause": (255, 0, 0),
}
STYLE = {
"link width": .5,
"grid": grid.SquareGridStyle(type="cells", m=.9, r=.1),
"walls": .15,
}
def _rotate_line(
a: Tuple[float, float],
b: Tuple[float, float],
angle: float,
) -> Tuple[Tuple[float, float], Tuple[float, float]]:
x_a, y_a = a
x_b, y_b = b
# middle
x_m = (x_a + x_b) / 2
y_m = (y_a + y_b) / 2
# shift
x_a -= x_m
y_a -= y_m
x_b -= x_m
y_b -= y_m
cos_a, sin_a = cos(angle), sin(angle)
# rotate
x_a, y_a = cos_a * x_a + sin_a * y_a, -sin_a * x_a + cos_a * y_a
x_b, y_b = cos_a * x_b + sin_a * y_b, -sin_a * x_b + cos_a * y_b
# shift
x_a += x_m
y_a += y_m
x_b += x_m
y_b += y_m
return (x_a, y_a), (x_b, y_b)
@dataclass
class Canva:
width: int
height: int
caption: str
frame_rate: int
screen: pygame.surface.Surface = None
clock: pygame.time.Clock = None
def setup(self):
pygame.init()
self.screen = pygame.display.set_mode((self.width, self.height))
pygame.display.set_caption(self.caption)
self.clock = pygame.time.Clock()
self.mouse = None
self.selected = []
def __handle_events(self, s: int) -> Union[StartEndPair, bool]:
selected_reset = False
for event in pygame.event.get():
if event.type == pygame.QUIT or (
event.type == pygame.KEYDOWN and
event.key == pygame.K_ESCAPE
):
pygame.quit()
sys.exit()
elif event.type == pygame.MOUSEBUTTONUP:
if event.button == pygame.BUTTON_RIGHT:
self.selected = []
elif event.button == pygame.BUTTON_LEFT:
x, y = event.pos
i, j = y // s, x // s
if len(self.selected) < 2 and (i, j) not in self.selected:
if len(self.selected) == 0:
selected_reset = True
self.selected.append((i, j))
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_RETURN:
if len(self.selected) == 2:
return tuple(self.selected)
elif event.key == pygame.K_SPACE:
w, h = self.screen.get_size()
pygame.draw.rect(self.screen, COLORS["pause"], (0, 0, w, h), width=5)
pygame.display.flip()
pause = True
while pause:
for event in pygame.event.get():
if event.type == pygame.QUIT or (
event.type == pygame.KEYDOWN and
event.key == pygame.K_ESCAPE
):
pygame.quit()
sys.exit()
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
pause = False
elif event.type == pygame.MOUSEMOTION:
self.mouse = event.pos
if selected_reset:
return not selected_reset
def __render(
self,
maze,
path: List[int] = None,
visited: List[int] = None,
next: List[int] = None,
complete: bool = False,
):
self.screen.fill(COLORS["background"])
grid.square(
self.screen,
width=maze["width"],
height=maze["height"],
size=maze["size"],
color=COLORS["empty cell"],
style=STYLE["grid"],
)
# edges
for a, b in maze["edges"]:
x_a = (.5 + a % maze["width"]) * maze["size"]
y_a = (.5 + a // maze["width"]) * maze["size"]
x_b = (.5 + b % maze["width"]) * maze["size"]
y_b = (.5 + b // maze["width"]) * maze["size"]
pygame.draw.line(
self.screen,
COLORS["link"],
(x_a, y_a),
(x_b, y_b),
width=int(maze["size"] * STYLE["link width"]),
)
d = int(maze["size"] * STYLE["grid"].m)
r = int(maze["size"] * STYLE["grid"].r)
a = (x_a - d // 2, y_a - d // 2, d, d)
b = (x_b - d // 2, y_b - d // 2, d, d)
pygame.draw.rect(self.screen, COLORS["cell"], a, border_radius=r)
pygame.draw.rect(self.screen, COLORS["cell"], b, border_radius=r)
if visited is not None:
for v in visited:
grid.cell(
self.screen,
v // maze["width"],
v % maze["width"],
maze["size"],
COLORS["visited cell"],
(STYLE["grid"].m * maze["size"]) // 2,
STYLE["grid"].r,
int(maze["size"] * STYLE["grid"].m),
)
if next is not None:
for v in next:
grid.cell(
self.screen,
v // maze["width"],
v % maze["width"],
maze["size"],
COLORS["next cell"],
(STYLE["grid"].m * maze["size"]) // 2,
STYLE["grid"].r,
int(maze["size"] * STYLE["grid"].m),
)
if path is not None:
for i, (a, b) in enumerate(itertools.pairwise(path)):
x_a = (.5 + a % maze["width"]) * maze["size"]
y_a = (.5 + a // maze["width"]) * maze["size"]
x_b = (.5 + b % maze["width"]) * maze["size"]
y_b = (.5 + b // maze["width"]) * maze["size"]
if complete:
pygame.draw.line(
self.screen,
COLORS["path ends link"],
(x_a, y_a),
(x_b, y_b),
width=int(maze["size"] * STYLE["link width"]),
)
else:
pygame.draw.line(
self.screen,
COLORS["path link"],
(x_a, y_a),
(x_b, y_b),
width=int(maze["size"] * STYLE["link width"]),
)
d = int(maze["size"] * STYLE["grid"].m)
r = int(maze["size"] * STYLE["grid"].r)
a = (x_a - d // 2, y_a - d // 2, d, d)
b = (x_b - d // 2, y_b - d // 2, d, d)
if complete:
if i == 0:
pygame.draw.rect(self.screen, COLORS["path ends ends"], a, border_radius=r)
pygame.draw.rect(self.screen, COLORS["path ends"], b, border_radius=r)
elif i == len(path) - 2:
pygame.draw.rect(self.screen, COLORS["path ends"], a, border_radius=r)
pygame.draw.rect(self.screen, COLORS["path ends ends"], b, border_radius=r)
else:
pygame.draw.rect(self.screen, COLORS["path ends"], a, border_radius=r)
pygame.draw.rect(self.screen, COLORS["path ends"], b, border_radius=r)
else:
pygame.draw.rect(self.screen, COLORS["path"], a, border_radius=r)
pygame.draw.rect(self.screen, COLORS["path"], b, border_radius=r)
# walls
h = [
(maze["width"] * j + i, maze["width"] * j + i + 1)
for j in range(maze["height"]) for i in range(maze["width"] - 1)
]
v = [
(maze["width"] * j + i, maze["width"] * j + i + maze["width"])
for j in range(maze["height"] - 1) for i in range(maze["width"])
]
walls = [
(a, b)
for (a, b) in h + v
if (a, b) not in maze["edges"] and (b, a) not in maze["edges"]
]
for a, b in walls:
x_a = (.5 + a % maze["width"]) * maze["size"] - 1
y_a = (.5 + a // maze["width"]) * maze["size"] - 1
x_b = (.5 + b % maze["width"]) * maze["size"] - 1
y_b = (.5 + b // maze["width"]) * maze["size"] - 1
a, b = _rotate_line((x_a, y_a), (x_b, y_b), pi / 2)
pygame.draw.line(
self.screen, COLORS["walls"], a, b, width=int(maze["size"] * STYLE["walls"])
)
# borders
w, h = maze["width"] * maze["size"], maze["height"] * maze["size"]
for a, b in [
((0, 0), (w, 0)),
((0, h - 1), (w - 1, h - 1)),
((0, 0), (0, h)),
((w - 1, 0), (w - 1, h - 1)),
]:
pygame.draw.line(
self.screen, COLORS["walls"], a, b, width=int(maze["size"] * STYLE["walls"])
)
if self.mouse is not None:
x, y = self.mouse
grid.cell(
self.screen,
y // maze["size"],
x // maze["size"],
maze["size"],
COLORS["cell under mouse"],
(STYLE["grid"].m * maze["size"]) // 2,
STYLE["grid"].r,
int(maze["size"] * STYLE["grid"].m),
)
for i, j in self.selected:
grid.cell(
self.screen,
i,
j,
maze["size"],
COLORS["selected cell"],
(STYLE["grid"].m * maze["size"]) // 2,
STYLE["grid"].r,
int(maze["size"] * STYLE["grid"].m),
)
pygame.display.flip()
def step(
self,
maze,
path: List[int] = None,
visited: List[int] = None,
next: List[int] = None,
complete: bool = False,
) -> Union[StartEndPair, bool]:
res = self.__handle_events(s=maze["size"])
if res is not None:
return res
self.__render(maze, path, visited, next, complete)
self.clock.tick(self.frame_rate)
def loop(
self,
maze,
path: List[int] = None,
visited: List[int] = None,
next: List[int] = None,
complete: bool = False,
) -> Union[StartEndPair, bool]:
if complete:
self.selected = []
while True:
res = self.step(maze, path, visited, next, complete)
if res is not None:
return res
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment