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 2 D 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
2023-03-22 11:14:38 +01:00
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-22 11:14:38 +01:00
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 ]
2023-03-22 19:41:33 +01:00
heuristic , heuristic_time = self . heuristic_manhattan ( neighbor )
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 , new_path )
print ( )
2023-03-22 19:41:33 +01:00
return path , 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
2023-03-22 19:41:33 +01:00
heuristic = self . heuristic_manhattan ( 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
2023-03-22 11:35:54 +01:00
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 ] )
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 = (
2023-03-22 15:47:07 +01:00
abs ( position [ 0 ] - self . end [ 0 ] ) * * 2 + abs ( position [ 1 ] - self . end [ 1 ] ) * * 2
) * * 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
2023-03-22 18:10:19 +01:00
def heuristic_random ( self , position ) :
""" 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 ) :
2023-03-22 15:47:07 +01:00
""" Prints the maze """
2023-03-22 10:32:16 +01:00
if path is None :
path = [ ]
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 :
2023-03-22 15:47:07 +01:00
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 , saved_file = " Maze " , iteration = 0 ) :
""" Saves maze from array to txt file """
folder_name = create_maze_folder ( solved )
2023-03-22 17:30:25 +01:00
with open ( f " { folder_name } / { iteration } { saved_file } " , " w " , encoding = " utf8 " ) as maze_file :
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 :
maze_file . write ( " * " )
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 ) ]
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 )
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 (
2023-03-22 15:47:07 +01:00
""" 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
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 15:47:07 +01:00
)
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 ( )
2023-03-22 19:41:33 +01:00
solved_path , heuristic_total_time , heuristics_called = solver_test . solve ( )
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 )
2023-03-22 17:42:06 +01:00
save_maze ( loaded_maze , True , solved_path , filename , 0 )
2023-03-22 18:10:19 +01:00
files_amount + = 1
average_path = sum_of_paths / files_amount
average_time = sum_of_time / files_amount
2023-03-22 19:41:33 +01:00
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 = solver . solve ( )
print_maze ( loaded_maze , solved_path )
save_maze ( loaded_maze , True , solved_path , 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 , 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 ( )