--- 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