From e85c852868415505d68b791e8f70a2d799b4a902 Mon Sep 17 00:00:00 2001 From: sururi Date: Thu, 13 Aug 2020 12:24:07 +0300 Subject: [PATCH 1/2] Implemented a maze solution algorithm. As the created maze is a 'perfect' maze i.e., every position can be reached from any other position, it can be solved using a cellular automata approach by marking the dead-ends as closed and shrinking the available cells by checking their relations to these dead-ends of the previous iteration and marking them as dead-ends as well if they only have one open side (one entry and no exit) at each iteration. * df_maze.py : - A new method, 'solve_from_to(start,end)' is added. It uses cellular automata approach to find the path from the 'start' (a (x0,y0) tuple) position to the 'end' (a (x1,y1) tuple) position and returns a numpy array 'path_line' that contains the sequential cell positions (starting from the 'start', ending at the 'end' position). A check is made on the values of the 'start' & 'end' values to ensure that they both are within the boundaries of the maze. - Two new optional parameters ('start' & ''end') are added to the 'write_svg()' method. If the 'end' parameter is supplied, the generated SVG image will contain the solution path from 'start' (default value: the entry position) to the end position. - Importing of numpy: Even though numpy's ndarrays are used for practical purposes and could have been substituted with Python's native lists as array (at the cost of extra procedures), numpy was crucial mostly for the 'logical_or()' function where cell-based direct evaluations could be made. * make_df_maze.py: The example case is expanded to demonstrate the maze solution algorithm. * maze.svg: is updated to be compatible with the newly generated solution image. * maze_solved.svg: The output of the 'write_svg()' method with the maze solution enabled, hence 'maze.svg' with the solution path embedded. --- maze/df_maze.py | 162 ++++++++++++++++++++- maze/make_df_maze.py | 7 + maze/maze.svg | 196 ++++++++++++------------- maze/maze_solved.svg | 337 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 602 insertions(+), 100 deletions(-) create mode 100644 maze/maze_solved.svg diff --git a/maze/df_maze.py b/maze/df_maze.py index cac3e18..bd5945c 100644 --- a/maze/df_maze.py +++ b/maze/df_maze.py @@ -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/ @@ -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. @@ -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('' + .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(''.format(width), file=f) @@ -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 diff --git a/maze/make_df_maze.py b/maze/make_df_maze.py index e921e7c..f2ce322 100644 --- a/maze/make_df_maze.py +++ b/maze/make_df_maze.py @@ -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)) diff --git a/maze/maze.svg b/maze/maze.svg index 4c95d3b..6077efe 100644 --- a/maze/maze.svg +++ b/maze/maze.svg @@ -12,212 +12,211 @@ line { ]]> - + + - - + - + - - + + - - - + + + - - + + - - + - + - - + + - - - - + + + + + + + - - + + - - + + + - - - - + + - - + + - + - + - - - - + + - + - - + + + + + - + - + - - - - + + - - + - - - - + + + + - - - + + + - - - - - + + + + - - + + - + + + - - - + + + - + - + - - - + + + - - + - - - - + + + + - + - + + - + + - - + + - - - - - + + + + - - - - - + + + + + - - - - - - + + + + + + - @@ -226,6 +225,7 @@ line { + diff --git a/maze/maze_solved.svg b/maze/maze_solved.svg new file mode 100644 index 0000000..b4f015c --- /dev/null +++ b/maze/maze_solved.svg @@ -0,0 +1,337 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 79049034d454c91df3ada6339a65a85b72faae00 Mon Sep 17 00:00:00 2001 From: sururi Date: Fri, 14 Aug 2020 09:39:03 +0300 Subject: [PATCH 2/2] Small fix on boundary conditions df_maze.py: the boundary check should also have included the upper limits of the maze dimensions. ('{<,>}=' instead of '{<,>}') --- maze/df_maze.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/maze/df_maze.py b/maze/df_maze.py index bd5945c..6c7586c 100644 --- a/maze/df_maze.py +++ b/maze/df_maze.py @@ -204,15 +204,15 @@ def solve_from_to(self, start, end): np_start = np.array(start) np_end = np.array(end) np_start[np_start < 0] = 0 - if np_start[0] > self.nx: + if np_start[0] >= self.nx: np_start[0] = self.nx - 1 - if np_start[1] > self.ny: + if np_start[1] >= self.ny: np_start[1] = self.ny - 1 np_end[np_end < 0] = 0 - if np_end[0] > self.nx: + if np_end[0] >= self.nx: np_end[0] = self.nx - 1 - if np_end[1] > self.ny: + if np_end[1] >= self.ny: np_end[1] = self.ny - 1 x0 = np_start[0]