Today’s puzzle was my favourite type of Advent of Code puzzles: pure programming and data structure manipulation and no need to know any specific tricks. It was also quite a long one, took me a good five and a half hours to get through.
Read input
Today’s input is two-parter.
First section is a map:
where # is a wall, O is a box, @ is a robot and . is an empty space.
Second section is a sequence of movements:
The movements are split to multiple lines for readability but they are interpreted as one continuous set of movements.
We read the sections as strings, then create a sparse grid where empty spaces are ignored. I then find the starting position (@) and delete that from the grid as we don’t need to keep the robot in the field.
Movements we combine into a single string.
Part 1
In the first part, we need to simulate the movements of the robot. Every time a robot is next to a box (O) and is moving towards it, it tries to push it. If there’s empty space behind it (or a line of boxes), each box in the sequence gets moved.
For example:
moves both top row boxes one step to right:
We start by defining a few helpers:
Delta is a namedtuple that is used to calculate new positions. MapData, Grid and Direction are type aliases to better communicate what types of values are passed in and out of functions.
directions is a map that tells us how we calculate the next location based on the direction.
First, we find the next position and its contents from the current position:
If we run into an empty space, we move to that new location:
If we run into a wall, we don’t do anything:
The main logic happens when we run into a box. Here we keep track of all the positions that need to move (to_move) and keep calculating those positions for as long as there is another box.
Once we’ve run out of boxes, we check if we hit the wall and if we did, don’t move anything.
If we didn’t hit a wall, we move every item (starting from the last) one position and delete the old location. After that, we update the robot’s position and return both the new position and the grid.
The desired output is a sum of each box’s GPS (Goods Positioning System) value. It’s calculated as 100 * distance from left edge + distance from top edge. In our coordinate system terms, it’s 100 * x + y.
That was fun!
Part 2
The second part adds a bit of complexity but without increasing the computational complexity in any meaningful way.
To do that, we need to expand the map input, making everything but the robot twice as wide:
# becomes ##
. becomes ..
O becomes []
@ becomes @.
The twist of this part is that boxes now take up two spaces so calculating how to push them up and down changes significantly.
To create this new expanded grid, we do:
We move 2 x positions for each cell in the input and expand according to the rules.
The new move function starts as previously:
If we encounter a wall or an empty spot, we either move to it or stop.
If the spot is part of a box ([ or ]), we need new logic:
Since going left/right is very different from going up/down, I split those two in separate functions.
Left/right is the easier. Compared to our first part’s solution, the only difference is that now each box is two coordinates wide.
We continue walking along the x axis until we find something that is not part of a box (either [ or ]). If it’s a wall, we do nothing. If it’s an empty space, we move all the items to that direction one at a time and finally we return robot’s new location and the modified grid.
The real challenge comes in the vertical push. I feel like my solution is overly complex and it does some double work but it helped me build it step-by-step and after it took so long, I didn’t feel like refactoring it today.
First we check if the stack can be pushed to the desired direction.
Every time we see part of a box in the next row, we add both of its coordinates into the next row to be checked. If at any point, we run into a wall, the stack cannot be moved. If all paths lead to empty slots, we know we can move the stack.
To move the stack, we then find all the coordinates for the box pieces that are part of that stack. Here we do a lot of repeated work that likely could be combined with the above:
If all the coordinates above/below the current row are empty, we’ve reached the end of the stack. Otherwise we go through each of those above/below coordinates and if we find more boxes, we add them to the next check.
Then we move all of those box pieces to their correct next spots:
We sort the coordinates by y axis so that I don’t accidentally overwrite some coordinates.
Finally, we return the new position for our robot and the updated grid:
Second part’s wrapper is then almost the same as first one:
I managed to get it to work with the test input but it failed with my full input. (The above code is my working final code.)
Given that there are 20 000 steps and a grid of 99x49, finding the mistake manually was not gonna get me through it and I got quite frustrated. All the corner cases I was able to come up with passed and I had a hard time finding what the issue could be.
Luckily, I’m not the only one solving these puzzles.
Toni offered his code so I can print the grids for each step with my malfunctioning code and his correct code. So I did and saved both to text files:
I then compared them line by line to find the first occasion where the mistake happened using cmp:
cmp incorrect correct
and it immediately found the mistake. On line 330 735. Good thing I didn’t try to go through them manually:
I searched the output file for the given line, found the issue and created a reproduction to test:
It turned out, my original code moved the top left box. I found the issue, fixed the code and got to the right outcome.