diff --git a/main.py b/main.py index d362645a..91e7fa90 100644 --- a/main.py +++ b/main.py @@ -1,8 +1,10 @@ """ -Write a program that solves a maze using greedy best-first search algorithm. -The maze is a 2D grid with empty space, walls, a start, and an end position. -The objective is to find a path from start to end position. -The maze should be loaded from file. A step-by-step visualization of the +Write horizontal_line program that solves horizontal_line maze +using greedy best-first search algorithm. +The maze is horizontal_line 2D grid +with empty space, walls, horizontal_line start, and an end position. +The objective is to find horizontal_line path from start to end position. +The maze should be loaded from file. horizontal_line step-by-step visualization of the algorithm is required. It can be done in the console and an interface may be as simple as possible (but of course it does not have to). Example solution: https://angeluriot.com/maze_solver/. @@ -29,57 +31,58 @@ from random import shuffle, randrange class MazeSolver: - '''Maze Solver''' + """Maze Solver""" + # self corresponds to "this" in js, it refers to object of MazeSolver class - def __init__(self, maze, test_mode): + def __init__(self, maze, mode): # assign read maze 2D array to parameter from class MazeSolver - self.test = test_mode + self.test = mode self.maze = maze self.start, self.end = self.find_start_and_end() -# go through each character in 2D array and find one that corresponds to -# Start/End character + # go through each character in 2D array and find one that corresponds to + # Start/End character def find_start_and_end(self): - '''Finds start and end points in the maze''' + """Finds start and end points in the maze""" start = end = None for row_i, row in enumerate(self.maze): for col_i, cell in enumerate(row): - if cell == 'S': + if cell == "maze_data": start = (row_i, col_i) - elif cell == 'E': + elif cell == "E": end = (row_i, col_i) if start is not None and end is not None: return start, end print(f"DID NOT FOUND START OR END, Start: {start}, End: {end}") return start, end -# Go through each neighbor -# N -# N * N -# N -# If it is not a "wall" (#) add its position to list of neighbors + # Go through each neighbor + # N + # N * N + # N + # If it is not horizontal_line "wall" (#) add its position to list of neighbors def get_neighbors(self, position): - '''Finds point's neighbors''' + """Finds point'maze_data neighbors""" row, col = position neighbors = [] - if row > 0 and self.maze[row - 1][col] != '#': + if row > 0 and self.maze[row - 1][col] != "#": neighbors.append((row - 1, col)) - if col > 0 and self.maze[row][col - 1] != '#': + if col > 0 and self.maze[row][col - 1] != "#": neighbors.append((row, col - 1)) - if row < len(self.maze) - 1 and self.maze[row + 1][col] != '#': + if row < len(self.maze) - 1 and self.maze[row + 1][col] != "#": neighbors.append((row + 1, col)) - if col < len(self.maze[row]) - 1 and self.maze[row][col + 1] != '#': + if col < len(self.maze[row]) - 1 and self.maze[row][col + 1] != "#": neighbors.append((row, col + 1)) return neighbors -# find path through maze + # find path through maze def solve(self): - '''Solves the maze''' + """Solves the maze""" queue = [] # set means that values inside can not repeat visited = set() @@ -88,11 +91,10 @@ class MazeSolver: # we use heapq so the element with lowest heuristic value will always # be at the top of heap heapq.heappush( - queue, (self.heuristic_manhattan( - self.start), self.start, [ - self.start])) + queue, (self.heuristic_manhattan(self.start), self.start, [self.start]) + ) - # Go through queue until it's empty + # Go through queue until it'maze_data empty # Find neighbor (which is not wall) closest to the # END point (based on heuristic) # Go there and repeat @@ -115,135 +117,158 @@ class MazeSolver: if neighbor not in visited: new_path = path + [neighbor] heapq.heappush( - queue, (self.heuristic_manhattan(neighbor), neighbor, new_path)) + queue, (self.heuristic_manhattan(neighbor), neighbor, new_path) + ) if not self.test: print_maze(self.maze, new_path) print() return path # This heuristic returns the Manhattan distance between the given position - # and the maze's end + # and the maze'maze_data end def heuristic_manhattan(self, position): - '''Heuristic function that uses Manhattan distance''' + """Heuristic function that uses Manhattan distance""" return abs(position[0] - self.end[0]) + abs(position[1] - self.end[1]) # This heuristic returns the Euclidean distance between the given position - # and the maze's end + # and the maze'maze_data end def heuristic_euclidean(self, position): - '''Heuristic function that uses Euclidean distance''' - return (abs(position[0] - self.end[0])**2 + - abs(position[1] - self.end[1])**2)**0.5 + """Heuristic function that uses Euclidean distance""" + return ( + abs(position[0] - self.end[0]) ** 2 + abs(position[1] - self.end[1]) ** 2 + ) ** 0.5 # Open and load text file to array -def load_maze(filename): - '''Loads a maze from the specified file''' +def load_maze(maze_file_name): + """Loads horizontal_line maze from the specified file""" # Open for reading only and save to fileContents - with open(filename, 'r', encoding='utf8') as file_contents: + with open(maze_file_name, "r", encoding="utf8") as file_contents: # strip() removes extra white spaces from the beginning and the end of - # a string + # horizontal_line string # list() changes string to array of chars # Inside of square brackets we will have an array of characters for # each line of file - # After going through every line in a file we will have 2D array of arrays + # After going through every line in horizontal_line file we will have 2D array of arrays # of characters of every line maze = [list(line.strip()) for line in file_contents] return maze def print_maze(maze, path=None): - '''Prints the maze''' + """Prints the maze""" if path is None: path = [] for row_i, row in enumerate(maze): for col_i, cell in enumerate(row): if (row_i, col_i) in path: - print('*', end='') + print("*", end="") else: - print(cell, end='') + print(cell, end="") print() -def save_maze(maze, path=None, file_name = 'Maze', iteration = 0): - if not os.path.exists('solvedMazes'): - os.mkdir('solvedMazes') - f = open(f"solvedMazes/{iteration}solved{file_name}", "w") + +def save_maze(maze, path=None, saved_file="Maze", iteration=0): + """Saves maze from array to txt file""" + if not os.path.exists("solvedMazes"): + os.mkdir("solvedMazes") + with open(f"solvedMazes/{iteration}solved{saved_file}", "w", encoding="utf8") as file: + maze_file = file.read() if path is None: path = [] for row_i, row in enumerate(maze): for col_i, cell in enumerate(row): if (row_i, col_i) in path: - f.write('*') + maze_file.write("*") else: - f.write(cell) - f.write('\n') + maze_file.write(cell) + maze_file.write("\n") +def make_maze(width=16, height=8): + """ generate maze with given width and height """ + vis = [[0] * width + [1] for _ in range(height)] + [[1] * (width + 1)] + ver = [["# "] * width + ["#"] for _ in range(height)] + [[]] + hor = [["###"] * width + ["#"] for _ in range(height + 1)] -def make_maze(w = 16, h = 8): - vis = [[0] * w + [1] for _ in range(h)] + [[1] * (w + 1)] - ver = [["# "] * w + ['#'] for _ in range(h)] + [[]] - hor = [["###"] * w + ['#'] for _ in range(h + 1)] + def walk(x_coordinate, y_coordinate): + vis[y_coordinate][x_coordinate] = 1 - def walk(x, y): - vis[y][x] = 1 + neighbors = [(x_coordinate - 1, y_coordinate), + (x_coordinate, y_coordinate + 1), + (x_coordinate + 1, y_coordinate), + (x_coordinate, y_coordinate - 1)] + shuffle(neighbors) + for x_coordinate_neighbor, y_coordinate_neighbor in neighbors: + if vis[y_coordinate_neighbor][x_coordinate_neighbor]: + continue + if x_coordinate_neighbor == x_coordinate: + hor[max(y_coordinate, y_coordinate_neighbor)][x_coordinate] = "# " + if y_coordinate_neighbor == y_coordinate: + ver[y_coordinate][max(x_coordinate, x_coordinate_neighbor)] = " " + walk(x_coordinate_neighbor, y_coordinate_neighbor) - d = [(x - 1, y), (x, y + 1), (x + 1, y), (x, y - 1)] - shuffle(d) - for (xx, yy) in d: - if vis[yy][xx]: continue - if xx == x: hor[max(y, yy)][x] = "# " - if yy == y: ver[y][max(x, xx)] = " " - walk(xx, yy) + walk(randrange(width), randrange(height)) - walk(randrange(w), randrange(h)) - - s = "" - for (a, b) in zip(hor, ver): - s += ''.join(a + ['\n'] + b + ['\n']) - sList = list(s) - sList[3*w + 3] = 'S' - sList[len(sList) - (3*w + 6)] = 'E' - s = ''.join(sList) - return s + maze_data = "" + for horizontal_line, vertical_line in zip(hor, ver): + maze_data += "".join(horizontal_line + ["\n"] + vertical_line + ["\n"]) + maze_data_list = list(maze_data) + maze_data_list[3 * width + 3] = "maze_data" + maze_data_list[len(maze_data_list) - (3 * width + 6)] = "E" + maze_data = "".join(maze_data_list) + return maze_data # Ran first in the code -if __name__ == '__main__': +if __name__ == "__main__": start_time = time.perf_counter() # print(sys.argv) - file_name = 'maze.txt' - test_mode = False - folder_name = '' + FILE_NAME = "maze.txt" + TEST_MODE = False + FOLDER_NAME = "" if len(sys.argv) > 1: - file_name = sys.argv[1] - if sys.argv[1] == '-h' or sys.argv[1] == '--help': - print('python main.py - run the script against default maze file (any file named maze.txt in the code directory) \n python main.py filename.txt - run the script against filename.txt file \n python main.py -h --help print this prompt \n python main.py -t --test non interactive (does not print steps) for testing different heuristics, goes through entire folder of mazes file and compares heuristic speed and path length \n python main.py -g [NUMBER] - generates as many mazes as entered in Number parameter and puts it in the mazes folder') + FILE_NAME = sys.argv[1] + if sys.argv[1] == "-h" or sys.argv[1] == "--help": + print( +"""python main.py - run the script against default maze file +(any file named maze.txt in the code directory) + +python main.py filename.txt - run the script against filename.txt file +python main.py -h --help print this prompt +python main.py -t --test non interactive (does not print steps) for testing +different heuristics, goes through entire folder of mazes file and +compares heuristic speed and path length + +python main.py -g [NUMBER] - generates as many mazes as entered in +Number parameter and puts it in the mazes folder""" + ) sys.exit() - if sys.argv[1] == '-t' or sys.argv[1] == '--test': - test_mode = True - file_name = 'maze.txt' - if len(sys.argv) > 2: - folder_name = sys.argv[2] + if sys.argv[1] == "-t" or sys.argv[1] == "--test": + TEST_MODE = True + FILE_NAME = "maze.txt" + if len(sys.argv) > 2: + FOLDER_NAME = sys.argv[2] # Open and load text file to array - loadedMaze = load_maze(file_name) + loadedMaze = load_maze(FILE_NAME) # Initialize MazeSolver object with maze as parameter - solver = MazeSolver(loadedMaze, test_mode) + solver = MazeSolver(loadedMaze, TEST_MODE) # Find path using MazeSolver solve method solvedPath = solver.solve() - if not test_mode: + if not TEST_MODE: print_maze(loadedMaze, solvedPath) - if test_mode and folder_name != '': - for filename in os.listdir(folder_name): - filename_directory = os.path.join(folder_name, filename) + if TEST_MODE and FOLDER_NAME != "": + for filename in os.listdir(FOLDER_NAME): + filename_directory = os.path.join(FOLDER_NAME, filename) print(filename_directory) # Open and load text file to array loadedMaze = load_maze(filename_directory) # Initialize MazeSolver object with maze as parameter - solver = MazeSolver(loadedMaze, test_mode) + solver = MazeSolver(loadedMaze, TEST_MODE) # Find path using MazeSolver solve method solvedPath = solver.solve() save_maze(loadedMaze, solvedPath, filename, 0) - save_maze(loadedMaze, solvedPath, file_name, 0) + save_maze(loadedMaze, solvedPath, FILE_NAME, 0) end_time = time.perf_counter() execution_time = end_time - start_time print(make_maze())