diff --git a/15/WarehouseWoes.Tests/PuzzleParserTests.cs b/15/WarehouseWoes.Tests/PuzzleParserTests.cs new file mode 100644 index 0000000..287e42f --- /dev/null +++ b/15/WarehouseWoes.Tests/PuzzleParserTests.cs @@ -0,0 +1,30 @@ +namespace WarehouseWoes.Tests; + +public class PuzzleParserTests +{ + private async Task GetPuzzleInput() + { + return await File.ReadAllTextAsync(Path.Combine(AppContext.BaseDirectory, "INPUT.txt")); + } + + [Test] + public async Task Parse_WhenCalled_ItShouldReturnMapAndDirections() + { + var input = await GetPuzzleInput(); + var result = PuzzleParser.Parse(input); + + await Assert.That(result.Map).IsTypeOf(); + await Assert.That(result.Directions).IsTypeOf>(); + } + + [Test] + public async Task PartOneSolution_WhenGivenPuzzleInput_ItShouldReturnExpectedSolution() + { + var input = await GetPuzzleInput(); + var (map, directions) = PuzzleParser.Parse(input); + + var result = map.MoveRobot(directions).CalculateTotalBoxGpsCoordinates(); + + await Assert.That(result).IsEqualTo(1318523); + } +} \ No newline at end of file diff --git a/15/WarehouseWoes.Tests/WarehouseMapTests.cs b/15/WarehouseWoes.Tests/WarehouseMapTests.cs new file mode 100644 index 0000000..df3e5da --- /dev/null +++ b/15/WarehouseWoes.Tests/WarehouseMapTests.cs @@ -0,0 +1,443 @@ +namespace WarehouseWoes.Tests; + +public class WarehouseMapTests +{ + private static readonly string[] LargeExampleMap = [ + "##########", + "#..O..O.O#", + "#......O.#", + "#.OO..O.O#", + "#..O@..O.#", + "#O#..O...#", + "#O..O..O.#", + "#.OO.O.OO#", + "#....O...#", + "##########", + ]; + + private static readonly string[] SmallExampleMap = [ + "########", + "#..O.O.#", + "##@.O..#", + "#...O..#", + "#.#.O..#", + "#...O..#", + "#......#", + "########", + ]; + + private static readonly string[] LargeExampleDirections = [ + "^v>^vv^v>v<>v^v<<><>>v^v^>^<<<><^", + "vvv<<^>^v^^><<>>><>^<<><^vv^^<>vvv<>><^^v>^>vv<>v<<<^<^^>>>^<>vv>v^v^<>><>>>><^^>vv>v<^^^>>v^v^<^^>v^^>v^<^v>v<>>v^v^v^^<^^vv<", + "<>^^^^>>>v^<>vvv^>^^^vv^^>v<^^^^v<>^>vvvv><>>v^<<^^^^^", + "^><^><>>><>^^<<^^v>>><^^>v>>>^v><>^v><<<>vvvv>^<><<>^><", + "^>><>^v<><^vvv<^^<><^v<<<><<<^^<^>>^<<<^>>^v^>>^v>vv>^<<^v<>><<><<>v<^vv<<<>^^v^>^^>>><<^v>>v^v><^^>>^<>vv^", + "<><^^>^^^<>^vv<<^><<><<><<<^^<<<^<<>><<><^^^>^^<>^>v<>", + "^^>vv<^v^v^<>^^^>>>^^vvv^>vvv<>>>^<^>>>>>^<<^v>^vvv<>^<><", + "v^^>>><<^^<>>^v^v^<<>^<^v^v><^<<<><<^vv>>v>v^<<^", + ]; + + private static readonly string[] SmallExampleDirections = [ + "<^^>>>vv>v<<", + ]; + + [Test] + [MethodDataSource(nameof(MoveTestCases))] + public async Task Move_WhenCalled_ItShouldMoveValuesToExpectedPositions(MoveTestCase testCase) + { + var expected = WarehouseMap.From(testCase.ExpectedMap); + var directions = testCase.Directions.Select(Direction.From).ToList(); + + var result = WarehouseMap.From(testCase.MapInput).MoveRobot(directions); + + await Assert.That(result.Positions).IsEquivalentTo(expected.Positions); + } + + public static IEnumerable> MoveTestCases() + { + yield return () => new( + [ + "########", + "#..O.O.#", + "##@.O..#", + "#...O..#", + "#.#.O..#", + "#...O..#", + "#......#", + "########", + ], + "<", + [ + "########", + "#..O.O.#", + "##@.O..#", + "#...O..#", + "#.#.O..#", + "#...O..#", + "#......#", + "########", + ] + ); + + yield return () => new( + [ + "########", + "#..O.O.#", + "##@.O..#", + "#...O..#", + "#.#.O..#", + "#...O..#", + "#......#", + "########", + ], + "^", + [ + "########", + "#.@O.O.#", + "##..O..#", + "#...O..#", + "#.#.O..#", + "#...O..#", + "#......#", + "########", + ] + ); + + yield return () => new( + [ + "########", + "#.@O.O.#", + "##..O..#", + "#...O..#", + "#.#.O..#", + "#...O..#", + "#......#", + "########", + ], + "^", + [ + "########", + "#.@O.O.#", + "##..O..#", + "#...O..#", + "#.#.O..#", + "#...O..#", + "#......#", + "########", + ] + ); + + yield return () => new( + [ + "########", + "#.@O.O.#", + "##..O..#", + "#...O..#", + "#.#.O..#", + "#...O..#", + "#......#", + "########", + ], + ">", + [ + "########", + "#..@OO.#", + "##..O..#", + "#...O..#", + "#.#.O..#", + "#...O..#", + "#......#", + "########", + ] + ); + + yield return () => new( + [ + "########", + "#..@OO.#", + "##..O..#", + "#...O..#", + "#.#.O..#", + "#...O..#", + "#......#", + "########", + ], + ">", + [ + "########", + "#...@OO#", + "##..O..#", + "#...O..#", + "#.#.O..#", + "#...O..#", + "#......#", + "########", + ] + ); + + yield return () => new( + [ + "########", + "#...@OO#", + "##..O..#", + "#...O..#", + "#.#.O..#", + "#...O..#", + "#......#", + "########", + ], + ">", + [ + "########", + "#...@OO#", + "##..O..#", + "#...O..#", + "#.#.O..#", + "#...O..#", + "#......#", + "########", + ] + ); + + yield return () => new( + [ + "########", + "#...@OO#", + "##..O..#", + "#...O..#", + "#.#.O..#", + "#...O..#", + "#......#", + "########", + ], + "v", + [ + "########", + "#....OO#", + "##..@..#", + "#...O..#", + "#.#.O..#", + "#...O..#", + "#...O..#", + "########", + ] + ); + + yield return () => new( + [ + "########", + "#....OO#", + "##..@..#", + "#...O..#", + "#.#.O..#", + "#...O..#", + "#...O..#", + "########", + ], + "v", + [ + "########", + "#....OO#", + "##..@..#", + "#...O..#", + "#.#.O..#", + "#...O..#", + "#...O..#", + "########", + ] + ); + + yield return () => new( + [ + "########", + "#....OO#", + "##..@..#", + "#...O..#", + "#.#.O..#", + "#...O..#", + "#...O..#", + "########", + ], + "<", + [ + "########", + "#....OO#", + "##.@...#", + "#...O..#", + "#.#.O..#", + "#...O..#", + "#...O..#", + "########", + ] + ); + + yield return () => new( + [ + "########", + "#....OO#", + "##.@...#", + "#...O..#", + "#.#.O..#", + "#...O..#", + "#...O..#", + "########", + ], + "v", + [ + "########", + "#....OO#", + "##.....#", + "#..@O..#", + "#.#.O..#", + "#...O..#", + "#...O..#", + "########", + ] + ); + + yield return () => new( + [ + "########", + "#....OO#", + "##.....#", + "#..@O..#", + "#.#.O..#", + "#...O..#", + "#...O..#", + "########", + ], + ">", + [ + "########", + "#....OO#", + "##.....#", + "#...@O.#", + "#.#.O..#", + "#...O..#", + "#...O..#", + "########", + ] + ); + + yield return () => new( + [ + "########", + "#....OO#", + "##.....#", + "#...@O.#", + "#.#.O..#", + "#...O..#", + "#...O..#", + "########", + ], + ">", + [ + "########", + "#....OO#", + "##.....#", + "#....@O#", + "#.#.O..#", + "#...O..#", + "#...O..#", + "########", + ] + ); + + yield return () => new( + [ + "########", + "#....OO#", + "##.....#", + "#....@O#", + "#.#.O..#", + "#...O..#", + "#...O..#", + "########", + ], + "v", + [ + "########", + "#....OO#", + "##.....#", + "#.....O#", + "#.#.O@.#", + "#...O..#", + "#...O..#", + "########", + ] + ); + + yield return () => new( + [ + "########", + "#....OO#", + "##.....#", + "#.....O#", + "#.#.O@.#", + "#...O..#", + "#...O..#", + "########", + ], + "<", + [ + "########", + "#....OO#", + "##.....#", + "#.....O#", + "#.#O@..#", + "#...O..#", + "#...O..#", + "########", + ] + ); + + yield return () => new( + [ + "########", + "#....OO#", + "##.....#", + "#.....O#", + "#.#O@..#", + "#...O..#", + "#...O..#", + "########", + ], + "<", + [ + "########", + "#....OO#", + "##.....#", + "#.....O#", + "#.#O@..#", + "#...O..#", + "#...O..#", + "########", + ] + ); + } + + public record MoveTestCase(string[] MapInput, string Directions, string[] ExpectedMap); + + [Test] + [MethodDataSource(nameof(CalculateTotalBoxGpsCoordinatesTestCases))] + public async Task CalculateTotalBoxGpsCoordinates_WhenCalledWithExampleInput_ItShouldReturnExpectedValue(CalculateTotalBoxGpsCoordinatesTestCase testCase) + { + var map = WarehouseMap.From(testCase.MapInput); + var directions = string.Join(string.Empty, testCase.DirectionsInput).Select(Direction.From).ToList(); + + var mapAfterRobotMoved = map.MoveRobot(directions); + var result = mapAfterRobotMoved.CalculateTotalBoxGpsCoordinates(); + + await Assert.That(result).IsEqualTo(testCase.ExpectedValue); + } + + public static IEnumerable> CalculateTotalBoxGpsCoordinatesTestCases() + { + yield return () => new(LargeExampleMap, LargeExampleDirections, 10092); + yield return () => new(SmallExampleMap, SmallExampleDirections, 2028); + } + + public record CalculateTotalBoxGpsCoordinatesTestCase(string[] MapInput, string[] DirectionsInput, int ExpectedValue); +} \ No newline at end of file diff --git a/15/WarehouseWoes/Program.cs b/15/WarehouseWoes/Program.cs index 8ab9fc2..b358dc7 100644 --- a/15/WarehouseWoes/Program.cs +++ b/15/WarehouseWoes/Program.cs @@ -1,5 +1,5 @@ using System.Diagnostics; -using System.Runtime.Intrinsics.X86; +using System.Text; if (args.Length is 0) { @@ -14,27 +14,38 @@ if (File.Exists(args[0]) is false) } var isPart2 = args.Length is 2 && args[1] is "part2"; -var input = await File.ReadAllTextAsync(Path.Combine(AppContext.BaseDirectory, "INPUT.txt")); +var input = await File.ReadAllTextAsync(args[0]); var stopwatch = new Stopwatch(); stopwatch.Start(); -var inputParts = input.Split($"{Environment.NewLine}{Environment.NewLine}"); - -var mapLines = inputParts[0].Split(Environment.NewLine); - -var directions = inputParts[1] - .Where(character => character.ToString() != Environment.NewLine && character.ToString() != string.Empty) - .Select(Direction.From) - .ToList(); - -var result = WarehouseMap.From(mapLines) - .MoveRobot(directions) - .CalculateTotalBoxGpsCoordinates(); +var (map, directions) = PuzzleParser.Parse(input); +var result = map.MoveRobot(directions).CalculateTotalBoxGpsCoordinates(); stopwatch.Stop(); Console.WriteLine($"The sum of all boxes' GPS coordinates is {result}. ({stopwatch.ElapsedMilliseconds}ms)"); +static class PuzzleParser +{ + public static (WarehouseMap Map, List Directions) Parse(string input) + { + var inputParts = input.Split($"{Environment.NewLine}{Environment.NewLine}"); + + var map = WarehouseMap.From(inputParts[0].Split(Environment.NewLine)); + + var directions = inputParts[1] + .Where(character => + { + var str = character.ToString(); + return str != "\r" && str != "\n" && str != "\r\n"; + }) + .Select(Direction.From) + .ToList(); + + return (map, directions); + } +} + class WarehouseMap { private const char Robot = '@'; @@ -42,21 +53,20 @@ class WarehouseMap private const char Box = 'O'; private const char Empty = '.'; - private readonly Dictionary _positions; - private Position RobotPosition => _positions.First(kvp => kvp.Value is Robot).Key; - + public readonly Dictionary Positions; + private WarehouseMap(Dictionary positions) { - _positions = positions; + Positions = positions; } public static WarehouseMap From(string[] input) { var positions = new Dictionary(); - for (int y = 0; y < input.Length; y++) + for (var y = 0; y < input.Length; y++) { - for (int x = 0; x < input[0].Length; x++) + for (var x = 0; x < input[0].Length; x++) { positions.Add(new(x, y), input[y][x]); } @@ -67,11 +77,11 @@ class WarehouseMap public WarehouseMap MoveRobot(List directions) { - var positions = _positions.ToDictionary(); - var robotPosition = RobotPosition; - + var positions = Positions.ToDictionary(); + foreach (var direction in directions) { + var robotPosition = positions.First(kvp => kvp.Value is Robot).Key; var nextPosition = robotPosition.GetNextPosition(direction); var hasNextPosition = positions.TryGetValue(nextPosition, out var nextPositionValue); @@ -80,36 +90,53 @@ class WarehouseMap continue; } - if (nextPositionValue is Wall) + switch (nextPositionValue) { - continue; - } - - if (nextPositionValue is Empty) - { - positions[robotPosition] = Empty; - positions[nextPosition] = Robot; - continue; - } - - if (nextPositionValue is Box && CanPushBox(nextPosition, direction, positions)) - { - if (CanPushBox(nextPosition, direction, positions)) - { + case Wall: + continue; + case Empty: + positions[robotPosition] = Empty; + positions[nextPosition] = Robot; + continue; + case Box when CanPushBox(nextPosition, direction, positions): positions = PushBox(nextPosition, direction, positions); positions[robotPosition] = Empty; positions[nextPosition] = Robot; - } + break; } } return new(positions); } - public int CalculateTotalBoxGpsCoordinates() => _positions - .Where(kvp => kvp.Value is Box) - .Select(kvp => kvp.Key) - .Sum(p => p.GpsCoordinate); + public int CalculateTotalBoxGpsCoordinates() + { + var boxes = Positions.Where(kvp => kvp.Value is Box).ToList(); + return boxes.Select(kvp => kvp.Key).Sum(p => p.GpsCoordinate); + } + + public override string ToString() + { + var maxX = Positions.Select(kvp => kvp.Key).Max(p => p.X); + var maxY = Positions.Select(kvp => kvp.Key).Max(p => p.Y); + + var map = new StringBuilder(); + + for (var y = 0; y <= maxY; y++) + { + var row = new StringBuilder(); + + for (var x = 0; x <= maxX; x++) + { + var value = Positions[new(x, y)]; + row.Append(value); + } + + map.AppendLine(row.ToString()); + } + + return map.ToString(); + } private static bool CanPushBox(Position boxPosition, Direction direction, Dictionary positions) { @@ -138,9 +165,10 @@ class WarehouseMap private static Dictionary PushBox(Position boxPosition, Direction direction, Dictionary currentPositions) { + var positions = currentPositions.ToDictionary(); + while (true) { - var positions = currentPositions.ToDictionary(); var nextPosition = boxPosition.GetNextPosition(direction); var hasNextPosition = positions.TryGetValue(nextPosition, out var nextPositionValue); @@ -152,11 +180,9 @@ class WarehouseMap if (nextPositionValue is Box) { boxPosition = nextPosition; - currentPositions = positions; continue; } - - positions[boxPosition] = Empty; + positions[nextPosition] = Box; return positions; diff --git a/AdventOfCode2024.sln b/AdventOfCode2024.sln index 09aff6f..8500db6 100644 --- a/AdventOfCode2024.sln +++ b/AdventOfCode2024.sln @@ -117,6 +117,10 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WarehouseWoes.Tests", "15\WarehouseWoes.Tests\WarehouseWoes.Tests.csproj", "{055CE647-AEFA-4501-B518-D16FF400E3D0}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "16", "16", "{C50481FF-1766-4C51-BEE3-91BEC5642818}" + ProjectSection(SolutionItems) = preProject + 16\INPUT.txt = 16\INPUT.txt + 16\PROBLEM.md = 16\PROBLEM.md + EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReindeerMaze", "16\ReindeerMaze\ReindeerMaze.csproj", "{5610FB67-037B-4C87-8D5D-E86D9D7DDB51}" EndProject