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.

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