Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented a maze solution algorithm. #5

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 160 additions & 2 deletions maze/df_maze.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# df_maze.py
import random

import numpy as np


# Create a maze using the depth-first algorithm described at
# https://scipython.com/blog/making-a-maze/
Expand Down Expand Up @@ -75,8 +77,14 @@ def __str__(self):
maze_rows.append(''.join(maze_row))
return '\n'.join(maze_rows)

def write_svg(self, filename):
"""Write an SVG image of the maze to filename."""
def write_svg(self, filename, start=(), end=()):
"""Write an SVG image of the maze to filename.

if the optional 'end' parameter (end=(x,y)) is supplied,
the maze will be solved from the 'start' (default value:
entry position) to the 'end' position and will be indicated
in the SVG image.
"""

aspect_ratio = self.nx / self.ny
# Pad the maze all around by this amount.
Expand Down Expand Up @@ -119,6 +127,27 @@ def write_wall(ww_f, ww_x1, ww_y1, ww_x2, ww_y2):
if self.cell_at(x, y).walls['E']:
x1, y1, x2, y2 = (x + 1) * scx, y * scy, (x + 1) * scx, (y + 1) * scy
write_wall(f, x1, y1, x2, y2)

# VISUALIZE THE MAZE SOLUTION (if demanded) ### begin #
# If the write_svg method is called with the "end" parameter
# specified, then solve the maze using the cellular automata
# approach and include it in the output SVG image.
if end:
# The end cell is included in the call, so solve the maze
if not start:
# Starting cell is not specified, so use the initial designation
start = (self.ix, self.iy)

# Solve the maze:
solution = self.solve_from_to(start, end)
for i in range(np.size(solution, 0) - 1):
x1, y1 = solution[i, :]
x2, y2 = solution[i + 1, :]
x1, y1, x2, y2 = (x1 + 0.5) * scx, (y1 + 0.5) * scy, (x2 + 0.5) * scx, (y2 + 0.5) * scy
print('<line x1="{}" y1="{}" x2="{}" y2="{}" style="stroke:#7d8059;" />'
.format(x1, y1, x2, y2), file=f)
# VISUALIZE THE MAZE SOLUTION (if demanded) ### end #

# Draw the North and West maze border, which won't have been drawn
# by the procedure above.
print('<line x1="0" y1="0" x2="{}" y2="0"/>'.format(width), file=f)
Expand Down Expand Up @@ -163,3 +192,132 @@ def make_maze(self):
cell_stack.append(current_cell)
current_cell = next_cell
nv += 1

def solve_from_to(self, start, end):
"""Solves the path from start = (x0,y0) to end = (x1,y1)
using cellular automata.

Returns the steps' coordinates
"""

# Check that the start & end parameters are within boundaries:
np_start = np.array(start)
np_end = np.array(end)
np_start[np_start < 0] = 0
if np_start[0] >= self.nx:
np_start[0] = self.nx - 1
if np_start[1] >= self.ny:
np_start[1] = self.ny - 1

np_end[np_end < 0] = 0
if np_end[0] >= self.nx:
np_end[0] = self.nx - 1
if np_end[1] >= self.ny:
np_end[1] = self.ny - 1

x0 = np_start[0]
y0 = np_start[1]
x1 = np_end[0]
y1 = np_end[1]

# Initially set the states of all cells to "O" for Open
arr_states = np.full((self.nx, self.ny), "O", dtype=str)
arr_states[x0, y0] = 'S' # Designates Start
arr_states[x1, y1] = 'E' # Designates End

# Defining a canvas to apply the filter
# for our playground
canvas = np.empty((self.nx, self.ny), dtype=object)
for x in range(self.nx):
for y in range(self.ny):
canvas[x, y] = (x, y)

state2logic = {"S": False, "E": False, "O": False, "X": True}
neigh2state = ["O", "O", "O", "X"]
# Pick the "open" states as they are the ones whose states can change:
filt = arr_states == "O"
num_Os = np.sum(filt)

# We will continue our investigation, closing those cells
# that are in contact with three closed cells+walls (meaning
# that, it is a dead end itself) at each iteration. Mind that,
# it is an _online_ process where the next cell is checked
# against the already updated states of the cells coming before
# it (as opposed to the _batch_ process approach)
#
# The iteration is continued until no cell has changed its
# status, meaning that we have arrived at a solution.
num_Os_pre = num_Os + 1
while num_Os != num_Os_pre:
num_Os_pre = num_Os
for xy in canvas[filt]:
num_Os_pre = num_Os
x, y = xy
walls = np.array(list(self.cell_at(x, y).walls.values()))

# N-S-E-W
neighbours = np.array([True, True, True, True])
# We are using try..except to ensure the neighbours
# exist (considering the boundaries)
# (for the purpose here, they are much "cheaper" than
# if clauses)
try:
neighbours[0] = state2logic[arr_states[x, y - 1]]
except:
pass
try:
neighbours[1] = state2logic[arr_states[x, y + 1]]
except:
pass
try:
neighbours[2] = state2logic[arr_states[x + 1, y]]
except:
pass
try:
neighbours[3] = state2logic[arr_states[x - 1, y]]
except:
pass
# Being bounded by a wall at a specific direction
# or having a closed neighbour there are equivalent
# in action and if the total number of such directions
# is 3 (i.e., 1 entrance, no exit), then the cell is
# closed.
res = np.logical_or(walls, neighbours)
arr_states[x, y] = neigh2state[np.sum(res)]
# For the next iteration, focus only on the still Open ones
# (This also causes the process to get faster as it proceeds)
filt = arr_states == "O"
num_Os = np.sum(filt)

# Now we have the canvas containing the path,
# starting from "S", followed by "O"s up to "E"
pos_start = canvas[arr_states == "S"][0]
path_line = np.array([pos_start])
pos_end = canvas[arr_states == "E"][0]
pos = pos_start
pos_pre = (-1, -1)
step = 0
# Define a control (filled with -1) array to keep track of visited cells
arr_path = np.ones((self.nx, self.ny)) * -1
arr_path[pos_start[0], pos_start[1]] = 0
arr_path[pos_end[0], pos_end[1]] = "999999"
directions = np.array(["N", "S", "E", "W"])
while pos != pos_pre:
pos_pre = pos
step += 1
possible_ways = np.array(list(self.cell_at(pos[0], pos[1]).walls.values())) == False
delta = {"N": (0, -1), "S": (0, 1), "W": (-1, 0), "E": (1, 0)}
for direction in directions[possible_ways]:
# pick this direction if it is open and not visited before
if (arr_states[pos[0] + delta[direction][0], pos[1] + delta[direction][1]] == "O" and arr_path[
pos[0] + delta[direction][0], pos[1] + delta[direction][1]] == -1):
arr_path[pos[0] + delta[direction][0], pos[1] + delta[direction][1]] = step
pos = (pos[0] + delta[direction][0], pos[1] + delta[direction][1])
path_line = np.append(path_line, [pos], axis=0)
break
# Even though being an auxiliary and internal variable, if needed,
# arr_path contains the steps at which the corresponding cell is visited,
# thus paving the way to the exit.
arr_path[pos_end[0], pos_end[1]] = step
path_line = np.append(path_line, [pos_end], axis=0)
return path_line
7 changes: 7 additions & 0 deletions maze/make_df_maze.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,10 @@

print(maze)
maze.write_svg('maze.svg')

# Solve the maze from the entry position to (13,12)
# solution_path = maze.solve_from_to((ix,iy),(13,12))
# print(solution_path)

# Draw the solution from the entry position to (10,5)
maze.write_svg('maze_solved.svg', end=(10, 5))
Loading