diff --git a/maze/df_maze.py b/maze/df_maze.py index cac3e18..6c7586c 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +