AoC 2024 Day 15: Warehouse Woes

Dated Feb 28, 2026; last modified on Sat, 28 Feb 2026

Parsing

########
#..O.O.#
##@.O..#
#...O..#
#.#.O..#
#...O..#
#......#
########

<^^>>>vv<v>>v<<
v^^>>><<v^^>>>^

@ denotes the robot, O denotes a box, and # denotes a wall. <^^>>>vv<v>>v<< describes the sequence of moves that the robot will attempt to make. Ignore the newlines within the move sequence. If there are any boxes in the way, the robot attempts to push them. However, if the action makes the robot or a box move into the wall, nothing moves.

How should the \(R \times C\) region be represented? If the walls and boxes are scarce, then a map representation will be memory efficient. The test input is \(50 \times 50\) with \(396\) walls and \(613\) boxes, and thus \(59.64\%\) empty.

Given a move, I need to look up and update the \(R \times C\) region in at most \(R-2\) or \(C-2\) cells. These lookups and updates need to be fast. A 2D array will have good cache locality especially when processing lateral moves.

Part One

The GPS coordinate of a box is equal to \(100\) times its distance from the top edge of a map plus its distance from the left edge of the map. After the robot is finished moving, what is the sum of all boxes’ GPS coordinates?

Part Two

Everything except the robot is twice as wide: # -> ##, O -> [], . -> .., and @ -> @.. For these larger boxes, distances are measured from the edge of the map to the closest edge of the box in question. What is the sum of all boxes’ final GPS coordinates?

Solution

There are some stateless operations that can be solved first and independently in WarehouseWoes.Extensions.cs without considering any algorithms:

  • Convert a direction, e.g., ^ to a \((dr, dc)\) vector.
  • Given a coordinate \((r, c)\), add/subtract \((dr, dc)\) to/from it.
  • Test if a coordinate \((r, c)\) is in bounds.
  • Tell if a direction is horizontal (<, >) or vertical (^, v).
WarehouseWoes.Extensions.cs
using System.Diagnostics;
using AoC2024.WarehouseWoesDataTypes;
using ExhaustiveMatching;

namespace AoC2024;

public static class WarehouseWoesExtensions
{
    extension(Coordinate source)
    {
        public static Coordinate operator +(Coordinate coord, Delta delta) =>
            new(coord.R + delta.dR, coord.C + delta.dC);

        public static Coordinate operator -(Coordinate coord, Delta delta) =>
            new(coord.R - delta.dR, coord.C - delta.dC);
    }

    public static bool IsInBounds(this CellType[,] grid, Coordinate coordinate) =>
        coordinate.R >= 0 && coordinate.R < grid.GetLength(0)
        && coordinate.C >= 0 && coordinate.C < grid.GetLength(1);

    public static Delta ToDelta(this Direction direction) => direction switch
    {
        Direction.Down => new(1, 0),
        Direction.Up => new(-1, 0),
        Direction.Left => new(0, -1),
        Direction.Right => new(0, 1),
        _ => throw ExhaustiveMatch.Failed(direction)
    };

    public static bool IsLateral(this Direction direction) =>
        direction == Direction.Left || direction == Direction.Right;

    public static int SumBoxGpsCoordinates(this CellType[,] grid)
    {
        var sum = 0;
        for (int r = 0; r < grid.GetLength(0); r++)
        {
            for (int c = 0; c < grid.GetLength(1); c++)
            {
                var cellType = grid[r, c];
                switch (cellType)
                {
                    case CellType.BoxStart:
                    case CellType.Box:
                        sum += r * 100 + c;
                        break;
                    default:
                        continue;
                }
            }
        }
        return sum;
    }

    public static void Visualize(this CellType[,] grid, Coordinate robotPosition)
    {
        for (int r = 0; r < grid.GetLength(0); r++)
        {
            for (int c = 0; c < grid.GetLength(1); c++)
            {
                if (r == robotPosition.R && c == robotPosition.C)
                {
                    Debug.Write('@');
                    continue;
                }

                var val = grid[r, c] switch
                {
                    CellType.Wall => '#',
                    CellType.Box => 'O',
                    CellType.Free => '.',
                    CellType.BoxStart => '[',
                    CellType.BoxEnd => ']',
                    _ => throw ExhaustiveMatch.Failed(grid[r, c])
                };
                Debug.Write(val);
            }
            Debug.Write('\n');
        }
        Debug.WriteLine("");
    }
}

Compared to Part One , Part Two ’s Move operation is more complicated because [ and ] need to move as a unit. Furthermore, there is a domino effect, e.g., ^ doesn’t succeed because the top-right ] can’t move:

##############
##......##..##
##.....[]...##
##....[]....##
##.....@....##
##..........##
##############

I overindexed on: Bad programmers worry about the code. Good programmers worry about data structures and their relationships. I had a costly detour away from the 2D array with the desire to make it impossible to move [ and ] independently. However, even though the data structure collapsed [] into a single C# record, the algorithm was gnarly and still performing a lot of vector arithmetic. Keeping the 2D array and then simplifying the algorithm worked better here. Remember, the goal is to reduce overall complexity.

I initially had two equally complex phases. Starting from \((r_0, c_0)\), I tried computing the final “move frontier” of cells that would be affected by the move in question. Then in the second phase, I’d walk back and apply the shifts by computing which cells needed to change on the way back. So much bookkeeping of edge cases.

WarehouseWoes.Common.cs final solution trades off algorithmic complexity by incurring extra memory overhead. In the initial phase of a move, I collect all of the \((r_i, c_i)\) that can shift over. Applying the actual move phase is now a simpler swap operation starting from the farthest cells out.

Buggy solution for part 2.
Buggy solution for part 2. Visualizing the result helped me spot and fix the zigzag pattern. GPT 5.3-Codex wrote this script using the SixLabors.ImageSharp pattern.
WarehouseWoes.Common.cs
using AoC2024.WarehouseWoesDataTypes;
using ExhaustiveMatching;

namespace AoC2024;

public partial class WarehouseWoes
{
    public int Solve()
    {
        foreach (var direction in _directions)
            RobotPosition = Move(_grid, RobotPosition, direction);
        return _grid.SumBoxGpsCoordinates();
    }

    private static Coordinate Move(CellType[,] grid, Coordinate origin, Direction direction)
    {
        var cellsToShiftOver = GetCellsToShiftOver(grid, origin, direction);
        if (cellsToShiftOver.Count == 0)
            return origin;

        var delta = direction.ToDelta();
        foreach (var source in cellsToShiftOver.Reverse())
        {
            var target = source + delta;
            grid[target.R, target.C] = grid[source.R, source.C];
            grid[source.R, source.C] = CellType.Free;
        }

        return origin + delta;
    }

    private static IReadOnlyList<Coordinate> GetCellsToShiftOver(
        CellType[,] grid, Coordinate origin, Direction direction)
    {
        var delta = direction.ToDelta();

        IEnumerable<Coordinate> candidates = GetCellsAffectedBySingleMove(grid, origin, direction);
        List<Coordinate> cellsToShift = [];
        cellsToShift.Add(origin);
        while (candidates.All(grid.IsInBounds))
        {
            var cellTypes = candidates.Select(coord => grid[coord.R, coord.C]).ToArray();

            if (cellTypes.Any(ct => ct is CellType.Wall))
                return [];

            if (cellTypes.All(ct => ct is CellType.Free))
                return cellsToShift;

            cellsToShift.AddRange(candidates);
            candidates = candidates
                .SelectMany(coord => GetCellsAffectedBySingleMove(grid, coord, direction))
                .ToHashSet();
        }

        return [];
    }

    private static IEnumerable<Coordinate> GetCellsAffectedBySingleMove(
        CellType[,] grid, Coordinate origin, Direction direction)
    {
        var delta = direction.ToDelta();
        var target = origin + delta;

        if (direction.IsLateral())
        {
            yield return target;
            yield break;
        }

        if (!grid.IsInBounds(target))
        {
            yield return target;
            yield break;
        }

        var cellType = grid[target.R, target.C];
        switch (cellType)
        {
            case CellType.Wall:
            case CellType.Box:
                yield return target;
                yield break;
            case CellType.Free:
                yield break;
            case CellType.BoxStart:
                yield return target;
                yield return target + RightDelta;
                yield break;
            case CellType.BoxEnd:
                yield return target + LeftDelta;
                yield return target;
                yield break;
            default:
                throw ExhaustiveMatch.Failed(cellType);
        }
    }

    private static readonly Delta LeftDelta = new(0, -1);
    private static readonly Delta RightDelta = new(0, 1);
}

References

  1. Day 15 - Advent of Code 2024: Warehouse Woes. Eric Wastl. adventofcode.com . Accessed Feb 28, 2026.
  2. Torvalds' quote about good programmer [closed]. softwareengineering.stackexchange.com . Accessed Mar 1, 2026.