mirror of
https://github.com/mfocko/blog.git
synced 2024-11-25 06:11:55 +01:00
397 lines
8.8 KiB
Markdown
397 lines
8.8 KiB
Markdown
---
|
|
id: solution
|
|
slug: /recursion/karel/solution
|
|
title: Solution to the problem
|
|
description: |
|
|
Solving the problem introduced in the previous post.
|
|
tags:
|
|
- python
|
|
- karel
|
|
- recursion
|
|
- backtracking
|
|
- solution
|
|
last_update:
|
|
date: 2023-12-24
|
|
---
|
|
|
|
# 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:
|
|
|
|
```ruby
|
|
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:
|
|
|
|
```ruby
|
|
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:
|
|
|
|
```ruby
|
|
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 Python[^1]:
|
|
|
|
```py
|
|
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:
|
|
|
|
```py
|
|
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:
|
|
|
|
```py
|
|
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.
|
|
|
|
:::tip 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:
|
|
|
|
```py
|
|
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.
|
|
|
|
[^1]: which is usually very easy matter
|