Skip to main content

Solving the maze problem

We will go through the given problem the same way as I have suggested in the previous post.

Summary of the problem

We have a robot in some kind of a maze and we have to find our way out that is marked with a so-called “beeper”. We've been given a restriction not to use any variables, we can use just backtracking and recursion.

Brainstorming the idea

Let's start with some brainstorming of the solution.

  • How will I know what I've checked without any variables?
    • answer: recursion will need to take care of that, cause I'm not allowed anything else
  • How will I pass around the fact I've found the exit?
    • answer: I can return values from helper functions, so I should be able to indicate found/not found
  • How is the exit marked?
    • answer: there is one “beeper” as a mark
  • Can I reduce my problem somehow?
    • answer: I could check each possible direction as a reduced search space

»Rough« pseudocode

We should be able to construct a skeleton of our solution at least. Pseudocode follows:

def find_exit
if found the exit then
signal others
terminate
end

check left
check front
check right
end

As you can see, we only mention what we want to do very roughly, technical details are left out, except for the early return (which is the base of our recursive function).

»Proper« pseudocode

In the proper pseudocode we will need to dive into the technical details like the way we check for exit, move around, etc.

We can start by cleaning up and decomposing the function written above:

def find_exit
# BASE: found exit
if found_exit() then
return true
end

# check left
if left_is_clear() then
turn_left()
step()
if find_exit() then
return true
end

turn_around()
step()
turn_left()
end

# check front
if front_is_clear() then
step()
if find_exit() then
return true
end

turn_around()
step()
turn_around()
end

# check right
if right_is_clear() then
turn_right()
step()
if find_exit() then
return true
end

turn_around()
step()
turn_right()
end

return false
end

We are missing few of the functions that we use in our pseudocode above:

  • found_exit()
  • turn_around()
  • turn_right()

We can implement those easily:

def found_exit
if not beepers_present() then
return false
end

pick_beeper()
if beepers_present() then
put_beeper()
return false
end

put_beeper()
return true
end

def turn_around
turn_left()
turn_left()
end

def turn_right
turn_around()
turn_left()
end

Now we have everything ready for implementing it in Python.

Actual implementation

It's just a matter of rewriting the pseudocode into Python1:

class SuperKarel(Karel):
# you can define your own helper functions on Karel here, if you wish to

def found_exit(self) -> bool:
if not self.beepers_present():
return False

self.pick_beeper()
if self.beepers_present():
self.put_beeper()
return False

self.put_beeper()
return True

def turn_around(self):
for _ in range(2):
self.turn_left()

def turn_right(self):
for _ in range(3):
self.turn_left()

def find_exit(self) -> bool:
if self.found_exit():
return True

# check left
if self.left_is_clear():
self.turn_left()
self.step()
if self.find_exit():
return True

self.turn_around()
self.step()
self.turn_left()

# check front
if self.front_is_clear():
self.step()
if self.find_exit():
return True

self.turn_around()
self.step()
self.turn_around()

# check right
if self.right_is_clear():
self.turn_right()
self.step()
if self.find_exit():
return True

self.turn_around()
self.step()
self.turn_right()

return False

def run(self):
self.find_exit()

We have relatively repetitive code for checking each of the directions, I would propose to refactor a bit, in a fashion of checkin just forward, so it's more readable:

def find_exit(self) -> bool:
if self.found_exit():
return True

self.turn_left()
for _ in range(3):
if self.front_is_blocked():
self.turn_right()
continue

self.step()
if self.find_exit():
return True

self.step()
self.turn_around()
self.turn_right()

return False

We can also notice that turning around takes 2 left turns and turning to right does 3. We get 5 left turns in total when we turn around and right afterwards… Taking 4 left turns just rotates us back to our initial direction, therefore it is sufficient to do just one left turn (5 % 4 == 1). That way we get:

def find_exit(self) -> bool:
if self.found_exit():
return True

self.turn_left()
for _ in range(3):
if self.front_is_blocked():
self.turn_right()
continue

self.step()
if self.find_exit():
return True

self.step()
# turning around and right is same as one turn to the left
self.turn_left()

return False

Testing

In the skeleton, with the previous post, I have included multiple mazes that can be tested. I have tried this solution with all of the given mazes, and it was successful in finding the exit. However there is one precondition of our solution that we haven't spoken about.

We are silently expecting the maze not to have any loops. Example of such maze can be the maze666.kw:

┌─────────┬─┐
│. . . . .│.│
│ ┌─────┐ │ │
│.│. . .│.│.│
│ │ │ │ │ │
│.│.│. . .│.│
│ │ │ └─┤
│.│.│. . . 1│
│ │ │ │ ┌─┤
│.│. . .│.│.│
│ └─────┘ │ │
│. > . . .│.│
└─────────┴─┘

If you try running our solution on this map, Karel just loops and never finds the solution. Let's have a look at the loop he gets stuck in:

┌─────────┬─┐
│* * * * *│.│
│ ┌─────┐ │ │
│*│* * *│*│.│
│ │ │ │ │ │
│*│*│. * *│.│
│ │ │ └─┤
│*│*│. * * 1│
│ │ │ │ ┌─┤
│*│* * *│*│.│
│ └─────┘ │ │
│* * * * *│.│
└─────────┴─┘

He walks past the exit, but can't see it, cause there's always a feasible path that is worth trying.

Algorithm

The algorithm we have written to find the exit is a depth-first search (DFS). However, as opposed to the usual implementation, we have no notion of paths that are being (or have already been) explored.

Fixing the issue

Since we are not allowed to use variables, the only way to resolve this issue is to mark the “cells” that we have tried. We can easily use beepers for this, but we need to be careful not to confuse the exit with already visited cell.

To do that we'll use 2 beepers instead of the one. Implementation follows:

def visited(self) -> bool:
if not self.beepers_present():
return False

self.pick_beeper()
if not self.beepers_present():
self.put_beeper()
return False

self.pick_beeper()
if self.beepers_present():
assert False, "no cell shall be marked with 3 beepers"

self.put_beeper()
self.put_beeper()
return True


def find_exit(self) -> bool:
# BASE: already tried
if self.visited():
self.turn_around()
return False

# BASE
if self.found_exit():
return True

# mark the cell as visited
for _ in range(2):
self.put_beeper()

self.turn_left()
for _ in range(3):
if self.front_is_blocked():
self.turn_right()
continue

self.step()
if self.find_exit():
return True

self.step()
# turning around and right is same as one turn to the left
self.turn_left()

return False

Now our solution works also for mazes that have loops.

Footnotes

  1. which is usually very easy matter