Advent of Code 2024, Day 15
It's Sokoban, but I wrote the logic! Part 2 lost me three days, and it was kind of a simple fix and I'm mad about it, but onwards I go.
Part of the AoC 2024 series
This is Sokoban! If youâve not played Sokoban back in ye olde days of computing, Iâm talking early 1990s, then this reference flies over your head. You have a room of some shape, a bunch of boxes, youâre a little guy, and you need to move the boxes into a specific target area. You can only push the boxes, and if theyâre against a wall, you canât push them any more.
Found an online Sokoban page:
And the code for todayâs solution:
Spoilers
Part 1
There wasnât much complexity in part 1.
Gotta parse the map into a HashMap
of Coordinate
s and Entity
types. Gotta parse the string soup into Command
s after joining the lines together.
Then it was a matter of actually executing all the commands. I figured because robot pushes boxes, boxes push other boxes, and everything stops if at the end of the chain we encounter a wall, or an empty space, I could collect all the actions that a command would do in a buffer as I go, and if I hit a wall, I can discard that buffer. If I donât hit a wall, I can then execute every replacement in one go.
A replacement is basically just âat this coordinate, replace this entity with this other entity.â It always starts with âreplace the robot with empty space on wherever the robot started.â
I ran what I thought was a half finished code on the example input, got correct result, so I ran it on my input, also got correct result, so yay â! Part 2. After gym!
Part 2
Today is 18th December, I lost 3 days to this damn part đđ. And do you know what it was? A glorified offâbyâone error.
There were a bunch of parts to it:
- make the map W I D E. No problem, fairly straightforward, everything is doubled, unless itâs a robot, because
@
becomes@.
, or a box, becauseO
becomes[]
- do the push logic, as previously, again not a problem, for the most part
- do this while the boxes push other boxes by their sides â ď¸
So that third point gave me so much trouble. For context, itâs situations like this:
// Starting position, about to go down
##############
##....@.....##
##....[]....##
##...[][]...##
##..[][][]..##
##..........##
##############
If you push this one down, this is what you should get:
// Good, we want this
##############
##..........##
##....@.....##
##....[]....##
##...[][]...##
##..[][][]..##
##############
But due to some fancy logic, I always end up losing half a box somewhere so it looks like this:
// What I kept ending up with
##############
##..........##
##....@.....##
##....[]....##
##...[.[]...## <-- note the [. there
##..[][][]..##
##############
This is because when Iâm pushing a box down, and I encounter its left side, I need to also push its right side. I donât know whether the thing on the right to the robot, or another box part, also gets pushed down, so by default I replace that with an Empty space.
This results in situations where the same coordinate gets modified due to two different pushes, one of the replacements overwriting the other one.
In the end I decided to keep the replacements sorted by recursion depth, and if the replacing entity is not an Empty block, record that in a set of âIâve already done this replacementâ.
Recursion depth is due to the implementation of push. The rules are:
- robot pushes a thing. If that thing is a box, then box pushes a thing (depth +1)
- if thing is empty, we return the current list of replacements with
Replacement{depth, coordinate, entity}
- once returned from a push, we merge those into our list, and return the merged vector
- if we encounter a wall, we return a
None
, so no replacements get done at all
Because the replacements is a vector, duplicate items might be present. That would cause the unwarranted overwrite.
Using the set I solve the problem of empty overwriting a nonâempty
- if weâve already replaced that coordinate, skip replacing it, whatever the new entity is
- if the replacing entity is not an empty space, then I add it to the set, and replace the tile
- next time an empty comes in for a coordinate that already had a replacement, it gets skipped
- if empty comes in for a coordinate that will be replaced later, it does not get added to the set, so the new replacement can take over
Luckily all other tests still pass. But also, a thousand lines of code. Granted most of it is commented out and inline comments anyways.
Donât matter, ââ.
Spoiler photo by Tiago Ferreira on Unsplash