WUT_Computer_Science/Programming/EARIN/lab1/main.py

378 lines
14 KiB
Python
Raw Permalink Normal View History

2023-03-18 16:27:54 +01:00
"""
2023-03-22 15:47:07 +01:00
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
2023-03-18 16:27:54 +01:00
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/.
Test multiple heuristics (at least two) h(n) and discuss the differences be-
tween the obtained results.
2023-03-22 11:32:50 +01:00
Technical requirements:
2023-03-18 16:27:54 +01:00
- implemented in Python.
- adheres to basic standards of lean coding in accordance to PEP8
- comments in the crucial parts to help with readability and understanding.
- The clear instruction how to run and test the code should be included.
2023-03-22 11:32:50 +01:00
2023-03-18 16:27:54 +01:00
Thinks that do not work:
Does not work if no Start (Should print out NO START FOUND)
Does not work if no End (Should print out NO END FOUND)
Does not work if no path (Should print out NO PATH FOUND)
"""
2023-03-22 10:32:16 +01:00
import heapq
import sys
2023-03-22 14:19:20 +01:00
import time
import os
2023-03-22 18:10:19 +01:00
from random import shuffle, randrange, random
2023-03-18 16:27:54 +01:00
class MazeSolver:
2023-03-22 15:47:07 +01:00
"""Maze Solver"""
2023-03-22 10:32:16 +01:00
# self corresponds to "this" in js, it refers to object of MazeSolver class
2023-03-22 11:32:50 +01:00
2023-03-22 15:47:07 +01:00
def __init__(self, maze, mode):
2023-03-22 11:16:07 +01:00
# assign read maze 2D array to parameter from class MazeSolver
2023-03-22 15:47:07 +01:00
self.test = mode
2023-03-22 10:32:16 +01:00
self.maze = maze
self.start, self.end = self.find_start_and_end()
2023-03-22 15:47:07 +01:00
# go through each character in 2D array and find one that corresponds to
# Start/End character
2023-03-22 10:32:16 +01:00
def find_start_and_end(self):
2023-03-22 15:47:07 +01:00
"""Finds start and end points in the maze"""
2023-03-22 10:32:16 +01:00
start = end = None
2023-03-22 11:32:50 +01:00
for row_i, row in enumerate(self.maze):
for col_i, cell in enumerate(row):
2023-03-22 17:30:25 +01:00
if cell == "S":
2023-03-22 11:32:50 +01:00
start = (row_i, col_i)
2023-03-22 15:47:07 +01:00
elif cell == "E":
2023-03-22 11:32:50 +01:00
end = (row_i, col_i)
2023-03-22 10:32:16 +01:00
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}")
2023-03-22 11:32:50 +01:00
return start, end
2023-03-18 16:27:54 +01:00
2023-03-22 15:47:07 +01:00
# Go through each neighbor
# N
# N * N
# N
# If it is not horizontal_line "wall" (#) add its position to list of neighbors
2023-03-18 16:27:54 +01:00
2023-03-22 10:32:16 +01:00
def get_neighbors(self, position):
2023-03-22 15:47:07 +01:00
"""Finds point'maze_data neighbors"""
2023-03-22 10:32:16 +01:00
row, col = position
neighbors = []
2023-03-22 15:47:07 +01:00
if row > 0 and self.maze[row - 1][col] != "#":
2023-03-22 10:32:16 +01:00
neighbors.append((row - 1, col))
2023-03-22 15:47:07 +01:00
if col > 0 and self.maze[row][col - 1] != "#":
2023-03-22 10:32:16 +01:00
neighbors.append((row, col - 1))
2023-03-22 15:47:07 +01:00
if row < len(self.maze) - 1 and self.maze[row + 1][col] != "#":
2023-03-22 10:32:16 +01:00
neighbors.append((row + 1, col))
2023-03-22 15:47:07 +01:00
if col < len(self.maze[row]) - 1 and self.maze[row][col + 1] != "#":
2023-03-22 10:32:16 +01:00
neighbors.append((row, col + 1))
return neighbors
2023-03-18 16:27:54 +01:00
2023-03-22 15:47:07 +01:00
# find path through maze
2023-03-18 16:27:54 +01:00
2023-03-22 17:30:25 +01:00
def solve_loop(self, queue, visited):
""" Goes through maze and finds the path """
2023-03-22 19:41:33 +01:00
heuristic_total_time = 0
heuristics_called = 0
2023-03-22 10:32:16 +01:00
while queue:
# pop first element of heap
# first value is skipped and we only save current position and path
# on heap
_, current, path = heapq.heappop(queue)
# if we already visited current skip code and go to next iteration
if current in visited:
continue
2023-03-22 11:32:50 +01:00
# if we found the end return path
2023-03-22 10:32:16 +01:00
if current == self.end:
2023-03-22 11:32:50 +01:00
break
2023-03-22 10:32:16 +01:00
visited.add(current)
for neighbor in self.get_neighbors(current):
if neighbor not in visited:
new_path = path + [neighbor]
heuristic, heuristic_time = self.heuristic_euclidean(
neighbor)
2023-03-22 19:41:33 +01:00
heuristic_total_time += heuristic_time
heuristics_called += 1
2023-03-22 10:32:16 +01:00
heapq.heappush(
2023-03-22 19:41:33 +01:00
queue, (heuristic, neighbor, new_path)
2023-03-22 15:47:07 +01:00
)
2023-03-22 14:19:20 +01:00
if not self.test:
print_maze(self.maze, path, visited)
2023-03-22 14:19:20 +01:00
print()
return path, visited, heuristic_total_time, heuristics_called
2023-03-22 10:32:16 +01:00
2023-03-22 17:30:25 +01:00
def solve(self):
"""Solves the maze"""
queue = []
# set means that values inside can not repeat
visited = set()
# https://docs.python.org/3/library/heapq.html
# push onto the queue (which becomes heapq), element containing values
# we use heapq so the element with lowest heuristic value will always
# be at the top of heap
heuristic = self.heuristic_euclidean(self.start)
2023-03-22 17:30:25 +01:00
heapq.heappush(
2023-03-22 19:41:33 +01:00
queue, (heuristic, self.start, [self.start])
2023-03-22 17:30:25 +01:00
)
# 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
# if cannot find path it starts over but skips the path that lead it to
# dead end
return self.solve_loop(queue, visited)
2023-03-22 11:16:07 +01:00
# This heuristic returns the Manhattan distance between the given position
2023-03-22 15:47:07 +01:00
# and the maze'maze_data end
def heuristic_manhattan(self, position):
2023-03-22 15:47:07 +01:00
"""Heuristic function that uses Manhattan distance"""
2023-03-22 19:41:33 +01:00
start_time = time.perf_counter()
heuristic = abs(position[0] - self.end[0]) + \
abs(position[1] - self.end[1])
2023-03-22 19:41:33 +01:00
end_time = time.perf_counter()
heuristic_time = end_time - start_time
return heuristic, heuristic_time
2023-03-22 10:32:16 +01:00
# This heuristic returns the Euclidean distance between the given position
2023-03-22 15:47:07 +01:00
# and the maze'maze_data end
2023-03-22 11:32:50 +01:00
def heuristic_euclidean(self, position):
2023-03-22 15:47:07 +01:00
"""Heuristic function that uses Euclidean distance"""
2023-03-22 19:41:33 +01:00
start_time = time.perf_counter()
heuristic = (
abs(position[0] - self.end[0]) ** 2 +
abs(position[1] - self.end[1]) ** 2
2023-03-22 15:47:07 +01:00
) ** 0.5
2023-03-22 19:41:33 +01:00
end_time = time.perf_counter()
heuristic_time = end_time - start_time
return heuristic, heuristic_time
2023-03-18 16:27:54 +01:00
def heuristic_random(self, position):
2023-03-22 18:10:19 +01:00
"""Heuristic function that just returns random value between 0 and 1"""
2023-03-22 19:41:33 +01:00
start_time = time.perf_counter()
heuristic = random()
end_time = time.perf_counter()
heuristic_time = end_time - start_time
return heuristic, heuristic_time
2023-03-22 18:10:19 +01:00
2023-03-18 16:27:54 +01:00
# Open and load text file to array
2023-03-22 15:47:07 +01:00
def load_maze(maze_file_name):
"""Loads horizontal_line maze from the specified file"""
2023-03-22 10:32:16 +01:00
# Open for reading only and save to fileContents
2023-03-22 15:47:07 +01:00
with open(maze_file_name, "r", encoding="utf8") as file_contents:
2023-03-22 11:32:50 +01:00
# strip() removes extra white spaces from the beginning and the end of
2023-03-22 15:47:07 +01:00
# horizontal_line string
2023-03-22 11:32:50 +01:00
# list() changes string to array of chars
# Inside of square brackets we will have an array of characters for
# each line of file
2023-03-22 15:47:07 +01:00
# After going through every line in horizontal_line file we will have 2D array of arrays
2023-03-22 11:32:50 +01:00
# of characters of every line
maze = [list(line.strip()) for line in file_contents]
2023-03-22 10:32:16 +01:00
return maze
2023-03-18 16:27:54 +01:00
def print_maze(maze, path=None, visited=None):
2023-03-22 15:47:07 +01:00
"""Prints the maze"""
2023-03-22 10:32:16 +01:00
if path is None:
path = []
if visited is None:
visited = []
2023-03-22 11:32:50 +01:00
for row_i, row in enumerate(maze):
for col_i, cell in enumerate(row):
if (row_i, col_i) in path and cell == " ":
2023-03-22 15:47:07 +01:00
print("*", end="")
elif (row_i, col_i) in visited and cell == " ":
print("·", end="")
2023-03-22 10:32:16 +01:00
else:
2023-03-22 15:47:07 +01:00
print(cell, end="")
2023-03-22 10:32:16 +01:00
print()
2023-03-18 16:27:54 +01:00
2023-03-22 17:42:06 +01:00
def create_maze_folder(solved):
""" Creates folder for generated or solved mazes"""
2023-03-22 17:30:25 +01:00
if solved:
folder_name = "solvedMazes"
else:
folder_name = "generatedMazes"
if not os.path.exists(folder_name):
os.mkdir(folder_name)
2023-03-22 17:42:06 +01:00
return folder_name
def save_maze(maze, solved=True, path=None, visited=None, saved_file="Maze", iteration=0):
2023-03-22 17:42:06 +01:00
"""Saves maze from array to txt file"""
folder_name = create_maze_folder(solved)
if path is None:
path = []
if visited is None:
visited = []
with open(f"{folder_name}/{iteration}{os.path.basename(saved_file)}", "w", encoding="utf8") as maze_file:
2023-03-22 17:30:25 +01:00
for row_i, row in enumerate(maze):
for col_i, cell in enumerate(row):
if (row_i, col_i) in path and cell == " ":
2023-03-22 17:30:25 +01:00
maze_file.write("*")
elif (row_i, col_i) in visited and cell == " ":
maze_file.write("·")
2023-03-22 17:30:25 +01:00
else:
maze_file.write(cell)
if solved:
maze_file.write("\n")
if not solved:
maze_file.write("\n")
2023-03-22 14:55:57 +01:00
2023-03-22 17:42:06 +01:00
def fill_generated_maze(hor, ver, width):
""" Fills generated maze array from horizontal and vertical lines """
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] = "S"
maze_data_list[len(maze_data_list) - (3 * width + 6)] = "E"
maze_data = "".join(maze_data_list)
return maze_data
2023-03-22 15:47:07 +01:00
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)]
2023-03-22 14:55:57 +01:00
2023-03-22 15:47:07 +01:00
def walk(x_coordinate, y_coordinate):
vis[y_coordinate][x_coordinate] = 1
2023-03-22 14:55:57 +01:00
2023-03-22 15:47:07 +01:00
neighbors = [(x_coordinate - 1, y_coordinate),
(x_coordinate, y_coordinate + 1),
(x_coordinate + 1, y_coordinate),
(x_coordinate, y_coordinate - 1)]
2023-03-22 15:47:07 +01:00
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] = "# "
2023-03-22 15:47:07 +01:00
if y_coordinate_neighbor == y_coordinate:
ver[y_coordinate][max(
x_coordinate, x_coordinate_neighbor)] = " "
2023-03-22 15:47:07 +01:00
walk(x_coordinate_neighbor, y_coordinate_neighbor)
2023-03-22 14:55:57 +01:00
2023-03-22 15:47:07 +01:00
walk(randrange(width), randrange(height))
2023-03-22 14:55:57 +01:00
2023-03-22 17:42:06 +01:00
return fill_generated_maze(hor, ver, width)
2023-03-22 14:55:57 +01:00
2023-03-22 17:30:25 +01:00
def print_help():
"""prints help"""
print(
"""python main.py - run the script against default maze file
2023-03-22 15:47:07 +01:00
(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
2023-03-22 17:42:06 +01:00
different heuristics, goes through entire generatedMazes folder and
compares heuristic speed and path length
python main.py -t --test [FOLDER] non interactive (does not print steps) for testing
different heuristics, goes through entire [FOLDER] folder and
2023-03-22 15:47:07 +01:00
compares heuristic speed and path length
2023-03-22 17:30:25 +01:00
python main.py -g --generate [NUMBER] - generates as many mazes as entered in
2023-03-22 17:42:06 +01:00
Number parameter and puts it in the generatedMazes folder"""
)
2023-03-22 17:30:25 +01:00
def test_mode():
""" Loads and solves multiple mazes in order to compare heuristics """
2023-03-22 18:10:19 +01:00
create_maze_folder(False)
sum_of_paths = 0
files_amount = 0
sum_of_time = 0
2023-03-22 19:41:33 +01:00
heuristic_total_total_time = 0
all_heuristic_called = 0
2023-03-22 17:30:25 +01:00
for filename in os.listdir(FOLDER_NAME):
filename_directory = os.path.join(FOLDER_NAME, filename)
# Open and load text file to array
2023-03-22 17:42:06 +01:00
loaded_maze = load_maze(filename_directory)
2023-03-22 17:30:25 +01:00
# Initialize MazeSolver object with maze as parameter
2023-03-22 17:42:06 +01:00
solver_test = MazeSolver(loaded_maze, TEST_MODE)
2023-03-22 17:30:25 +01:00
# Find path using MazeSolver solve method
2023-03-22 18:10:19 +01:00
start_time = time.perf_counter()
solved_path, visited, heuristic_total_time, heuristics_called = solver_test.solve()
2023-03-22 19:41:33 +01:00
heuristic_total_total_time += heuristic_total_time
all_heuristic_called += heuristics_called
2023-03-22 18:10:19 +01:00
end_time = time.perf_counter()
sum_of_time += end_time - start_time
sum_of_paths += len(solved_path)
save_maze(loaded_maze, True, solved_path, visited, filename, 0)
2023-03-22 18:10:19 +01:00
files_amount += 1
if files_amount == 0:
print("no mazes found! Generate some using python main.py -g [NUMBER]")
sys.exit()
2023-03-22 18:10:19 +01:00
average_path = sum_of_paths / files_amount
average_time = sum_of_time / files_amount
print(f"""For: {files_amount} files,
sum of path lengths = {sum_of_paths},
average path length = {average_path},
sum_of_time = {sum_of_time},
average time to solve: {average_time},
heuristic_total_total_time: {heuristic_total_total_time},
all_heuristic_called: {all_heuristic_called},
average_heuristic_time: {heuristic_total_total_time / all_heuristic_called}""")
2023-03-22 17:30:25 +01:00
2023-03-22 17:42:06 +01:00
def default():
""" Runs default operation - reads, solves and prints single maze from file """
# Open and load text file to array
loaded_maze = load_maze(FILE_NAME)
# Initialize MazeSolver object with maze as parameter
solver = MazeSolver(loaded_maze, TEST_MODE)
# Find path using MazeSolver solve method
solved_path, visited, _, _ = solver.solve()
print_maze(loaded_maze, solved_path, visited)
save_maze(loaded_maze, True, solved_path, visited, FILE_NAME, 0)
2023-03-22 17:30:25 +01:00
# Ran first in the code
if __name__ == "__main__":
# print(sys.argv)
FILE_NAME = "maze.txt"
TEST_MODE = False
FOLDER_NAME = ""
GENERATE_AMOUNT = 0
if len(sys.argv) > 1:
if sys.argv[1] == "-h" or sys.argv[1] == "--help":
print_help()
2023-03-22 14:19:20 +01:00
sys.exit()
2023-03-22 15:47:07 +01:00
if sys.argv[1] == "-t" or sys.argv[1] == "--test":
TEST_MODE = True
FILE_NAME = "maze.txt"
2023-03-22 17:42:06 +01:00
FOLDER_NAME = "generatedMazes"
2023-03-22 15:47:07 +01:00
if len(sys.argv) > 2:
FOLDER_NAME = sys.argv[2]
2023-03-22 17:42:06 +01:00
test_mode()
sys.exit()
2023-03-22 17:30:25 +01:00
if sys.argv[1] == '-g' or sys.argv[1] == '--generate':
if len(sys.argv) > 2:
GENERATE_AMOUNT = int(sys.argv[2])
2023-03-22 17:42:06 +01:00
for n in range(GENERATE_AMOUNT):
GENERATED_MAZE = make_maze()
save_maze(GENERATED_MAZE, False, None,
None, f'generated{n}.txt')
2023-03-22 18:10:19 +01:00
sys.exit()
2023-03-22 17:42:06 +01:00
FILE_NAME = sys.argv[1]
default()