Issue:
- Usual breadth first search is too slow to solve the size of maze poster requires i.e. 24 x 24.
Algorithm & Code
Key Algorithm Modification from normal wall following
- Continues to step in the same direction until we hit a wall
- Create new branching paths to left and right of current direction when hit a wall in current direction
- Use heap to focus on extending paths with minimum distances and number of direction changes
Code
# Modification of https://github.com/nerijus-st/Maze-Solving/blob/master/left%20hand%20rule.py
import numpy as np
import heapq
class Maze():
# Movement directions
directions = ["N", "E", "S", "W"]
def __init__(self, maze):
assert isinstance(maze, np.ndarray) # maze should be 2D numpy array
self.maze = maze
self.m, self.n = maze.shape
def solve_maze(self, start, goal):
"""
N
W E
S
UP (N) - get_neighbours()['N']
RIGHT (E) - get_neighbours()['E']
DOWN (S) - get_neighbours()['S']
LEFT (W) - get_neighbours()['W']
maze 2D Numpy array
start tuple for stating position
goal tuple for destination
Strategy: Keeps going in same direction until a wall is reached. Then try
form branches for both left and right directions (i.e. search both)
"""
# Try all starting directions
heap = [(0, 0, facing, [start]) for facing in Maze.directions]
heapq.heapify(heap)
while heap:
dist, changes, facing, path = heapq.heappop(heap)
changes = -changes # Negative to make earlier since using min heap
if path[-1] == goal:
return self.compress_path(path)
self.x, self.y = path[-1] # Set current position in maze
front_wall = self.get_front_wall(facing) # Coordinates next position in front
if front_wall and not self.maze[front_wall] and not front_wall in path: # if Clear in front
# Front direction clear
heapq.heappush(heap, (dist+1, -changes, facing, path + [front_wall]))
elif len(path) > 1:
# Try to left
left_wall = self.get_left_wall(facing) # Coordinates to the left
if left_wall and not self.maze[left_wall] and not left_wall in path: # if clear to the left
left_facing = self.rotate_facing(facing, "CCW") # Rotate left (i.e. counter clockwise)
heapq.heappush(heap, (dist+1, -(changes+1), left_facing, path + [left_wall]))
# Try to the right
right_wall = self.get_right_wall(facing) # Coordinates to the right
if right_wall and not self.maze[right_wall] and not right_wall in path: # if Clear to the right
right_facing = self.rotate_facing(facing, "CW") # Rotate right (i.e. clockwise)
heapq.heappush(heap, (dist+1, -(changes+1), right_facing, path + [right_wall]))
def compress_path(self, path):
if not path:
return
if len(path) < 3:
return path
result = [path[0]]
for i, p in enumerate(zip(path, path[1:])):
direction = self.get_direction(*p)
if i == 0:
prev_direction = direction
result.append(p[1][:])
continue
if len(result) > 2:
if prev_direction == direction:
result[-1] = p[1][:]
else:
result.append(p[1][:])
else:
result.append(p[1][:])
prev_direction = direction
return result
def get_neighbours(self):
' Neighbors of current position '
x, y = self.x, self.y
result = {}
result['N'] = (x-1, y) if x + 1 < self.m else None
result['S'] = (x+1, y) if x-1 >= 0 else None
result['E'] = (x, y+1) if y + 1 < self.n else None
result['W'] = (x, y-1) if y -1 >= 0 else None
return result
def get_direction(self, point1, point2):
x1, y1 = point1
x2, y2 = point2
if y1 == y2 and x1 - 1 == x2:
return 'N'
if y1 == y2 and x1 + 1 == x2:
return 'S'
if x1 == x2 and y1 + 1 == y2:
return 'E'
if x1 == x2 and y1 - 1 == y2:
return 'W'
def get_left_wall(self, facing):
if facing == "N":
return self.get_neighbours()['W'] # cell to the West
elif facing == "E":
return self.get_neighbours()['N']
elif facing == "S":
return self.get_neighbours()['E']
elif facing == "W":
return self.get_neighbours()['S']
def get_right_wall(self, facing):
if facing == "N":
return self.get_neighbours()['E'] # cell to the East
elif facing == "E":
return self.get_neighbours()['S']
elif facing == "S":
return self.get_neighbours()['W']
elif facing == "W":
return self.get_neighbours()['N']
def get_front_wall(self, facing):
return self.get_neighbours()[facing]
def rotate_facing(self, facing, rotation):
facindex = Maze.directions.index(facing)
if rotation == "CW":
if facindex == len(Maze.directions) - 1:
return Maze.directions[0]
else:
return Maze.directions[facindex + 1]
elif rotation == "CCW":
if facindex == 0:
return Maze.directions[-1]
else:
return Maze.directions[facindex - 1]
Test
Test 1
maze = [[1, 1, 1, 1, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 0],
[1, 1, 1, 1, 1]]
M = Maze(np.array(maze))
p = M.solve_maze((1,1), (2,3))
print('Answer:', p)
# Output: Answer: [(1, 1), (2, 1), (2, 3)]
Test 2
maze = [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1],
[1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1],
[1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1],
[1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1],
[1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1],
[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1],
[1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1],
[1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1],
[1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1],
[1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1],
[1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1],
[1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1],
[1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
[1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]
M = Maze(np.array(maze))
p = M.solve_maze((1,1), (23, 23))
print('Answer:', p)
# Output: Answer: [(1, 1), (1, 2), (1, 9), (3, 9), (3, 12), (5, 12), (5, 10), (7, 10), (7, 9), (17, 9), (17, 10), (23, 10), (23, 23)]