You appear back inside your own mini submarine! Each Historian drives their mini submarine in a different direction; maybe the Chief has his own submarine down here somewhere as well?
You look up to see a vast school of lanternfish swimming past you. On closer inspection, they seem quite anxious, so you drive your mini submarine over to see if you can help.
Visit the website for the full story and full puzzle description.
A nice Sokoban-style puzzle for the weekend! The main difference is that in the original Sokoban, the robot could push only a single box, not multiple boxes. This adds complexity to both parts of the puzzle. However, it’s not that difficult to handle... I moved the hard parts into the TryToStep
function that takes the map, a position, and a direction, then attempts to make a move in that direction.
If the position corresponds to the robot or a box, the function checks whether the neighboring cell is free or can be made free by pushing boxes in the given direction. The .NET API sometimes uses the TryToDoX
pattern, where a function returns a boolean result and provides an out
parameter. I reused this pattern here. On success, the updated map is returned via the ref
parameter. If the move fails, the map remains unchanged.
The real challenge lies in part 2
, where recursion needs to branch whenever the robot pushes more than one box at a time. In such cases, we must invoke TryToStep
for each box. Due to the recursive nature of the algorithm, it’s possible for one branch to succeed while the other fails. When this happens, the entire map must be reset to its original state before we pop from the recursion. This could be quite tricky to manage unless you make copies of the dictionary that holds the state — or, as I did, use an immutable dictionary instead.
I’m not entirely satisfied with the "if-ladder" structure of the TryToStep
function, but for now, I don’t have a better idea to handle all the cases in a more readable way.
namespace AdventOfCode.Y2024.Day15;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Numerics;
using Map = System.Collections.Immutable.IImmutableDictionary<System.Numerics.Complex, char>;
[ProblemName("Warehouse Woes")]
class Solution : Solver {
static Complex Up = -Complex.ImaginaryOne;
static Complex Down = Complex.ImaginaryOne;
static Complex Left = -1;
static Complex Right = 1;
public object PartOne(string input) => Solve(input);
public object PartTwo(string input) => Solve(ScaleUp(input));
public double Solve(string input) {
var (map, steps) = Parse(input);
var robot = map.Keys.Single(k => map[k] == '@');
foreach (var dir in steps) {
if (TryToStep(ref map, robot, dir)) {
robot += dir;
}
}
return map.Keys
.Where(k => map[k] == '[' || map[k] == 'O')
.Sum(box => box.Real + 100 * box.Imaginary);
}
// Attempts to move the robot in the given direction on the map, pushing boxes as necessary.
// If the move is successful, the map is updated to reflect the new positions and the function returns true.
// Otherwise, the map remains unchanged and the function returns false.
bool TryToStep(ref Map map, Complex pos, Complex dir) {
var mapOrig = map;
if (map[pos] == '.') {
return true;
} else if (map[pos] == 'O' || map[pos] == '@') {
if (TryToStep(ref map, pos + dir, dir)) {
map = map
.SetItem(pos + dir, map[pos])
.SetItem(pos, '.');
return true;
}
} else if (map[pos] == ']') {
return TryToStep(ref map, pos + Left, dir);
} else if (map[pos] == '[') {
if (dir == Left) {
if (TryToStep(ref map, pos + Left, dir)) {
map = map
.SetItem(pos + Left, '[')
.SetItem(pos, ']')
.SetItem(pos + Right, '.');
return true;
}
} else if (dir == Right) {
if (TryToStep(ref map, pos + 2 * Right, dir)) {
map = map
.SetItem(pos, '.')
.SetItem(pos + Right, '[')
.SetItem(pos + 2 * Right, ']');
return true;
}
} else {
if (TryToStep(ref map, pos + dir, dir) && TryToStep(ref map, pos + Right + dir, dir)) {
map = map
.SetItem(pos, '.')
.SetItem(pos + Right, '.')
.SetItem(pos + dir, '[')
.SetItem(pos + dir + Right, ']');
return true;
}
}
}
map = mapOrig;
return false;
}
string ScaleUp(string input) =>
input.Replace("#", "##").Replace(".", "..").Replace("O", "[]").Replace("@", "@.");
(Map, Complex[]) Parse(string input) {
var blocks = input.Split("\n\n");
var lines = blocks[0].Split("\n");
var map = (
from y in Enumerable.Range(0, lines.Length)
from x in Enumerable.Range(0, lines[0].Length)
select new KeyValuePair<Complex, char>(x + y * Down, lines[y][x])
).ToImmutableDictionary();
var steps = blocks[1].ReplaceLineEndings("").Select(ch =>
ch switch {
'^' => Up,
'<' => Left,
'>' => Right,
'v' => Down,
_ => throw new Exception()
});
return (map, steps.ToArray());
}
}
Please ☆ my repo if you like it!