I've been teaching myself Python as first steps in seeing if I want to totally change careers from teaching into some kind of software development. So far it's been going well, and I've been working on small projects to really stretch my abilities. I recently did some TweetMazes with my students, and realized that making a Python program to solve those mazes would be an excellent provocation for me. TweetMazes are one-dimensional number hopping mazes. They're well described here. Pretty much immediately I realized that the way that people solve these mazes is by implementing recursive backtracking in their brains, so I decided to make a recursive backtracking algorithm to solve these. I find recursion fascinating and also really hard to wrap my head around. After about a week of faffing around with code, I got something that both works and fails in a way that I can't reconcile, so I'm hoping that someone can help me figure out what's going on.
First, an example of a TweetMaze. This is the short one I made up to use as a test case for my code. It's a list of 6 numbers:
[4, 2, 2, 2, 3, 0]
You start at index 0, which has a value of 4. You're trying to get to the last element in the list, which is the only one with a value of 0. The value of each element in the list tells you how many spaces you can jump left or right. So you would solve the maze like this:
1. starting at index 0, there's a value of 4: I can't move left, so I have to jump 4 spaces right to index 4.
2. Index 4 has a value of 3. I can't jump right, that's outside the puzzle, so I have to go left 3 spaces to index 1.
3. Again, I can only go right, so I move 2 spaces right to index 3.
4. Index 3 has a value of 2, and I can jump left or right. Pretty obviously, I jump right 2 spaces to index 5,
solving the puzzle. If I had instead gone left, I would have entered a loop.
I might represent this solution as another list indicating the index positions taken in the solution. So for the above puzzle, what I called the solution stack in my python program would be:
Solution = [0, 4, 1, 3, 5]
I also made a function to display the solution as a series of '#' symbols below the maze to show the steps taken. That should look like this for the above maze:
[4, 2, 2, 2, 3, 0]
#
#
#
#
#
So here's where things get weird. If I run the example maze through my code, everything works and I get the expected output. However, that's a really short maze and is easy to solve, so to test my code further, I made up new mazes that have 7 elements instead of 6. I ran the following maze, and the code terminated and behaved as though it had a solution, but it's totally not a valid solution:
Maze: [3, 5, 3, 1, 4, 4, 0] Solution: [0, 3, 4, 2, 5, 1, 6]
#
#
#
#
#
#
#
I hope this is all clear so far. The algorithm goes wrong pretty early on.
1. It starts at index 0, and moves right by 3 to index 3. That's correct.
2. It moves right by 1 to index 4. That's a valid move, but that's not how the solution should go. As
far as I can tell, there's only one solution, and it moves left by 1 to index 2 at this point.
However, the move the program made is at least valid still, so I'll move on to its next step.
3. It's at index 4, which as a value of 4. That means it should move left or right by 4, but it doesn't.
It moves left by 2 to index 2. It's totally broken down at this point, and has made an invalid move
of the wrong step size. I'm absolutely baffled as to how it does this at all.
I just don't see in my code how it could move with the wrong step size.
4. From this point, however, it's weirdly back on track and finishes the rest of the maze,
making moves of the correct step size to get to the end. It's like it just inserted a wrong step
and then carried on from the correct point. So basically, what on Earth is happening?
For clarity, here is what the solution should have looked like:
Maze: [3, 5, 3, 1, 4, 4, 0] Solution: [0, 3, 2, 5, 1, 6]
#
#
#
#
#
#
Things get extra weird at this point because I decided to add a list of solution_steps that would output what the algorithm was doing at each layer of recursion in a text format so I could try to see what it was doing, and those solution steps are actually correct! They don't match the output of the solution list or the '#' solution display! I'll copy and paste the output:
Current position: 0. Stepping right by 3 to index 3.
Current position: 3. Stepping left by 1 to index 2.
Current position: 2. Stepping right by 3 to index 5.
Current position: 5. Stepping left by 4 to index 1.
Current position: 1. Stepping right by 5 to index 6.
Solution stack shows these positions: [0, 3, 4, 2, 5, 1, 6]
[3, 5, 3, 1, 4, 4, 0]
#
#
#
#
#
#
#
If you follow the steps as they're written out, it's the correct solution! But the solution stack and the # display are both incorrect, showing that weird extra step and wrong move. This isn't the only puzzle where this happens, as I've tested it on other puzzles of 7 elements and longer. I have spent about a week tweaking my way into something that kind of works through recursive backtracking, and am finally at a point where I just can't tell what on earth is happening, so I'm hoping someone can explain this odd behavior. Frankly, I'm pretty proud of how far it's come, because I kept running into odd problems of exceeding max recursion depth and managed to over come those. I'd like to get this fully functional though, and understand why it's going wrong. Even more than that, I love to learn how you figure out exactly what is going wrong. I'm still very new to debugging tools and methods, and I'm having a fair bit of trouble understanding how to follow the flow of the program through recursive iterations. I've posted my code on GitHub, but because I'm still very new to using Github, I'll also post my code below. Thank you so much!
maze_string = '3531440'
'''Copy and paste maze strings into the above to change which maze is being run.
Other maze strings to play with:
422230
313210
313110
7347737258493420
'''
maze = [int(i) for i in maze_string]
solution = [0] #this is the solution stack. append to put on new steps,
#pop() to backtrack.
#algorithm doesn't work if I don't initialize the solution list with the 0 position. Hits max recursion depth.
solution_steps = []
def valid(position, direction):
#pass in the current position, desired direction of move
global maze
if direction == 'right':
return True if position + maze[position] < len(maze) else False
elif direction == 'left':
return True if position - maze[position] >= 0 else False
def solve(position):
global solution
global maze
#if maze is solved, return True
if maze[position] == 0:
return True
#try branches
if valid(position, 'right') and (position + maze[position] not in solution):
new = position + maze[position]
solution.append(new)
if solve(new):
solution_steps.append(f'Current position: {position}. Stepping right by {maze[position]} to index {new}.')
return True
solution.pop()
if valid(position, 'left') and (position - maze[position] not in solution):
new = position - maze[position]
solution.append(new)
if solve(new):
solution_steps.append(f'Current position: {position}. Stepping left by {maze[position]} to index {new}.')
return True
solution.pop()
solution.append('no solution')
return False
def display_solution():
global maze
global solution
print(maze)
for i in solution:
point = ' ' + 3*i*' ' + '#'
print(point)
if __name__ == "__main__":
solve(0)
if solution != 'no solution':
for step in reversed(solution_steps):
print(step)
print(f'\nSolution stack shows these positions: {solution}\n')
display_solution()
else:
print(solution)