feat: solved day 15 part 2

This commit is contained in:
Stevan Freeborn
2024-12-16 23:51:43 -06:00
parent 3b06f96259
commit 8e64d65e7d
11 changed files with 764 additions and 205 deletions
@@ -27,4 +27,15 @@ public class PuzzleParserTests
await Assert.That(result).IsEqualTo(1318523); await Assert.That(result).IsEqualTo(1318523);
} }
[Test]
public async Task PartTwoSolution_WhenGivenPuzzleInput_ItShouldReturnExpectedSolution()
{
var input = await GetPuzzleInput();
var (map, directions) = PuzzleParser.Parse(input);
var result = map.Scale().MoveRobot(directions).CalculateTotalBoxGpsCoordinates();
await Assert.That(result).IsEqualTo(1337648);
}
} }
@@ -43,6 +43,70 @@ public class WarehouseMapTests
"<^^>>>vv<v>>v<<", "<^^>>>vv<v>>v<<",
]; ];
[Test]
[MethodDataSource(nameof(ScaleTestCases))]
public async Task Scale_WhenCalled_ItShouldReturnExpectedMap(ScaleTestCase testCase)
{
var expected = WideWarehouseMap.From(testCase.ExpectedMap);
var result = WarehouseMap.From(testCase.MapInput).Scale();
await Assert.That(result.Positions).IsEquivalentTo(expected.Positions);
}
public static IEnumerable<Func<ScaleTestCase>> ScaleTestCases()
{
yield return () => new(
[
"##########",
"#..O..O.O#",
"#......O.#",
"#.OO..O.O#",
"#..O@..O.#",
"#O#..O...#",
"#O..O..O.#",
"#.OO.O.OO#",
"#....O...#",
"##########",
],
[
"####################",
"##....[]....[]..[]##",
"##............[]..##",
"##..[][]....[]..[]##",
"##....[]@.....[]..##",
"##[]##....[]......##",
"##[]....[]....[]..##",
"##..[][]..[]..[][]##",
"##........[]......##",
"####################",
]
);
yield return () => new(
[
"#######",
"#...#.#",
"#.....#",
"#..OO@#",
"#..O..#",
"#.....#",
"#######",
],
[
"##############",
"##......##..##",
"##..........##",
"##....[][]@.##",
"##....[]....##",
"##..........##",
"##############",
]
);
}
public record ScaleTestCase(string[] MapInput, string[] ExpectedMap);
[Test] [Test]
[MethodDataSource(nameof(MoveTestCases))] [MethodDataSource(nameof(MoveTestCases))]
public async Task Move_WhenCalled_ItShouldMoveValuesToExpectedPositions(MoveTestCase testCase) public async Task Move_WhenCalled_ItShouldMoveValuesToExpectedPositions(MoveTestCase testCase)
@@ -0,0 +1,295 @@
namespace WarehouseWoes.Tests;
public class WideWarehouseMapTests
{
[Test]
[MethodDataSource(nameof(CalculateTotalBoxGpsCoordinatesTestCases))]
public async Task CalculateTotalBoxGpsCoordinates_WhenCalledWithExampleInput_ItShouldReturnExpectedValue(CalculateTotalBoxGpsCoordinatesTestCase testCase)
{
var map = WideWarehouseMap.From(testCase.MapInput);
var result = map.CalculateTotalBoxGpsCoordinates();
await Assert.That(result).IsEqualTo(testCase.ExpectedValue);
}
public static IEnumerable<Func<CalculateTotalBoxGpsCoordinatesTestCase>> CalculateTotalBoxGpsCoordinatesTestCases()
{
yield return () => new(
[
"####################",
"##[].......[].[][]##",
"##[]...........[].##",
"##[]........[][][]##",
"##[]......[]....[]##",
"##..##......[]....##",
"##..[]............##",
"##..@......[].[][]##",
"##......[][]..[]..##",
"####################",
],
9021
);
}
public record CalculateTotalBoxGpsCoordinatesTestCase(string[] MapInput, int ExpectedValue);
[Test]
[MethodDataSource(nameof(MoveTestCases))]
public async Task Move_WhenCalled_ItShouldMoveValuesToExpectedPositions(MoveTestCase testCase)
{
var expected = WideWarehouseMap.From(testCase.ExpectedMap);
var directions = testCase.Directions.Select(Direction.From).ToList();
var result = WideWarehouseMap.From(testCase.MapInput).MoveRobot(directions);
await Assert.That(result.Positions).IsEquivalentTo(expected.Positions);
}
public static IEnumerable<Func<MoveTestCase>> MoveTestCases()
{
yield return () => new(
[
"##############",
"##......##..##",
"##..........##",
"##....[][]@.##",
"##....[]....##",
"##..........##",
"##############",
],
"<",
[
"##############",
"##......##..##",
"##..........##",
"##...[][]@..##",
"##....[]....##",
"##..........##",
"##############",
]
);
yield return () => new(
[
"##############",
"##......##..##",
"##..........##",
"##...[][]@..##",
"##....[]....##",
"##..........##",
"##############",
],
"v",
[
"##############",
"##......##..##",
"##..........##",
"##...[][]...##",
"##....[].@..##",
"##..........##",
"##############",
]
);
yield return () => new(
[
"##############",
"##......##..##",
"##..........##",
"##...[][]...##",
"##....[].@..##",
"##..........##",
"##############",
],
"v",
[
"##############",
"##......##..##",
"##..........##",
"##...[][]...##",
"##....[]....##",
"##.......@..##",
"##############",
]
);
yield return () => new(
[
"##############",
"##......##..##",
"##..........##",
"##...[][]...##",
"##....[]....##",
"##.......@..##",
"##############",
],
"<",
[
"##############",
"##......##..##",
"##..........##",
"##...[][]...##",
"##....[]....##",
"##......@...##",
"##############",
]
);
yield return () => new(
[
"##############",
"##......##..##",
"##..........##",
"##...[][]...##",
"##....[]....##",
"##......@...##",
"##############",
],
"<",
[
"##############",
"##......##..##",
"##..........##",
"##...[][]...##",
"##....[]....##",
"##.....@....##",
"##############",
]
);
yield return () => new(
[
"##############",
"##......##..##",
"##..........##",
"##...[][]...##",
"##....[]....##",
"##.....@....##",
"##############",
],
"^",
[
"##############",
"##......##..##",
"##...[][]...##",
"##....[]....##",
"##.....@....##",
"##..........##",
"##############",
]
);
yield return () => new(
[
"##############",
"##......##..##",
"##...[][]...##",
"##....[]....##",
"##.....@....##",
"##..........##",
"##############",
],
"^",
[
"##############",
"##......##..##",
"##...[][]...##",
"##....[]....##",
"##.....@....##",
"##..........##",
"##############",
]
);
yield return () => new(
[
"##############",
"##......##..##",
"##...[][]...##",
"##....[]....##",
"##.....@....##",
"##..........##",
"##############",
],
"<",
[
"##############",
"##......##..##",
"##...[][]...##",
"##....[]....##",
"##....@.....##",
"##..........##",
"##############",
]
);
yield return () => new(
[
"##############",
"##......##..##",
"##...[][]...##",
"##....[]....##",
"##....@.....##",
"##..........##",
"##############",
],
"<",
[
"##############",
"##......##..##",
"##...[][]...##",
"##....[]....##",
"##...@......##",
"##..........##",
"##############",
]
);
yield return () => new(
[
"##############",
"##......##..##",
"##...[][]...##",
"##....[]....##",
"##...@......##",
"##..........##",
"##############",
],
"^",
[
"##############",
"##......##..##",
"##...[][]...##",
"##...@[]....##",
"##..........##",
"##..........##",
"##############",
]
);
yield return () => new(
[
"##############",
"##......##..##",
"##...[][]...##",
"##...@[]....##",
"##..........##",
"##..........##",
"##############",
],
"^",
[
"##############",
"##...[].##..##",
"##...@.[]...##",
"##....[]....##",
"##..........##",
"##..........##",
"##############",
]
);
}
public record MoveTestCase(string[] MapInput, string Directions, string[] ExpectedMap);
}
+26
View File
@@ -0,0 +1,26 @@
namespace WarehouseWoes;
record Direction(int Xd, int Yd)
{
private const char UpCharacter = '^';
private const char RightCharacter = '>';
private const char DownCharacter = 'v';
private const char LeftCharacter = '<';
private static readonly Direction Up = new(0, -1);
private static readonly Direction Down = new(0, 1);
public static readonly Direction Left = new(-1, 0);
public static readonly Direction Right = new(1, 0);
public static Direction From(char character)
{
return character switch
{
UpCharacter => Up,
RightCharacter => Right,
DownCharacter => Down,
LeftCharacter => Left,
_ => throw new ArgumentException(@$"{nameof(character)} is not a valid: {character}"),
};
}
}
+58
View File
@@ -0,0 +1,58 @@
using System.Text;
namespace WarehouseWoes;
abstract class Map(Dictionary<Position, char> positions)
{
protected const char Robot = '@';
protected const char Wall = '#';
protected const char Box = 'O';
protected const char Empty = '.';
protected const char LeftBoxSide = '[';
protected const char RightBoxSide = ']';
public readonly Dictionary<Position, char> Positions = positions;
protected Position RobotPosition => Positions.First(kvp => kvp.Value is Robot).Key;
private int MaxX => Positions.Select(kvp => kvp.Key).Max(p => p.X);
private int MaxY => Positions.Select(kvp => kvp.Key).Max(p => p.Y);
public string[] MapGrid => ToString().Split(Environment.NewLine);
public abstract int CalculateTotalBoxGpsCoordinates();
public abstract Map MoveRobot(List<Direction> directions);
public virtual Map Scale() => this;
protected static Dictionary<Position, char> ConvertToDictionary(string[] input)
{
var positions = new Dictionary<Position, char>();
for (var y = 0; y < input.Length; y++)
{
for (var x = 0; x < input[0].Length; x++)
{
positions.Add(new(x, y), input[y][x]);
}
}
return new(positions);
}
public override string ToString()
{
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();
}
}
+11
View File
@@ -0,0 +1,11 @@
namespace WarehouseWoes;
record Position(int X, int Y)
{
public int GpsCoordinate => (100 * Y) + X;
public Position GetNextPosition(Direction direction)
{
return new(X + direction.Xd, Y + direction.Yd);
}
}
+9 -203
View File
@@ -1,5 +1,6 @@
using System.Diagnostics; using System.Diagnostics;
using System.Text;
using WarehouseWoes;
if (args.Length is 0) if (args.Length is 0)
{ {
@@ -19,208 +20,13 @@ var input = await File.ReadAllTextAsync(args[0]);
var stopwatch = new Stopwatch(); var stopwatch = new Stopwatch();
stopwatch.Start(); stopwatch.Start();
var (map, directions) = PuzzleParser.Parse(input); var (parsedMap, directions) = PuzzleParser.Parse(input);
var map = isPart2
? parsedMap.Scale()
: parsedMap;
var result = map.MoveRobot(directions).CalculateTotalBoxGpsCoordinates(); var result = map.MoveRobot(directions).CalculateTotalBoxGpsCoordinates();
stopwatch.Stop(); stopwatch.Stop();
Console.WriteLine($"The sum of all boxes' GPS coordinates is {result}. ({stopwatch.ElapsedMilliseconds}ms)"); Console.WriteLine($"The sum of all boxes' GPS coordinates is {result}. ({stopwatch.ElapsedMilliseconds}ms)");
static class PuzzleParser
{
public static (WarehouseMap Map, List<Direction> 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 = '@';
private const char Wall = '#';
private const char Box = 'O';
private const char Empty = '.';
public readonly Dictionary<Position, char> Positions;
private WarehouseMap(Dictionary<Position, char> positions)
{
Positions = positions;
}
public static WarehouseMap From(string[] input)
{
var positions = new Dictionary<Position, char>();
for (var y = 0; y < input.Length; y++)
{
for (var x = 0; x < input[0].Length; x++)
{
positions.Add(new(x, y), input[y][x]);
}
}
return new(positions);
}
public WarehouseMap MoveRobot(List<Direction> directions)
{
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);
if (hasNextPosition is false)
{
continue;
}
switch (nextPositionValue)
{
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()
{
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<Position, char> positions)
{
while (true)
{
var nextPosition = boxPosition.GetNextPosition(direction);
var hasNextPosition = positions.TryGetValue(nextPosition, out var nextPositionValue);
if (hasNextPosition is false)
{
return false;
}
switch (nextPositionValue)
{
case Box:
boxPosition = nextPosition;
continue;
case Empty:
return true;
default:
return false;
}
}
}
private static Dictionary<Position, Char> PushBox(Position boxPosition, Direction direction, Dictionary<Position, char> currentPositions)
{
var positions = currentPositions.ToDictionary();
while (true)
{
var nextPosition = boxPosition.GetNextPosition(direction);
var hasNextPosition = positions.TryGetValue(nextPosition, out var nextPositionValue);
if (hasNextPosition is false)
{
return positions;
}
if (nextPositionValue is Box)
{
boxPosition = nextPosition;
continue;
}
positions[nextPosition] = Box;
return positions;
}
}
}
record Position(int X, int Y)
{
public int GpsCoordinate => (100 * Y) + X;
public Position GetNextPosition(Direction direction)
{
return new(X + direction.Xd, Y + direction.Yd);
}
}
record Direction(int Xd, int Yd)
{
private const char UpCharacter = '^';
private const char RightCharacter = '>';
private const char DownCharacter = 'v';
private const char LeftCharacter = '<';
private static readonly Direction Up = new(0, -1);
private static readonly Direction Down = new(0, 1);
private static readonly Direction Left = new(-1, 0);
private static readonly Direction Right = new(1, 0);
public static Direction From(char character)
{
return character switch
{
UpCharacter => Up,
RightCharacter => Right,
DownCharacter => Down,
LeftCharacter => Left,
_ => throw new ArgumentException(@$"{nameof(character)} is not a valid: {character}"),
};
}
}
+22
View File
@@ -0,0 +1,22 @@
namespace WarehouseWoes;
static class PuzzleParser
{
public static (Map Map, List<Direction> 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);
}
}
+137
View File
@@ -0,0 +1,137 @@
namespace WarehouseWoes;
class WarehouseMap : Map
{
private WarehouseMap(Dictionary<Position, char> positions) : base(positions) {}
public static WarehouseMap From(string[] input) => new(ConvertToDictionary(input));
public override Map Scale()
{
var scaledPositions = new Dictionary<Position, char>();
var scaleFactor = 2;
foreach (var (position, value) in Positions)
{
var scaledX = position.X * scaleFactor;
var scaledY = position.Y;
switch (value)
{
case Wall:
scaledPositions[new(scaledX, scaledY)] = Wall;
scaledPositions[new(scaledX + 1, scaledY)] = Wall;
break;
case Box:
scaledPositions[new(scaledX, scaledY)] = WideWarehouseMap.LeftBoxSide;
scaledPositions[new(scaledX + 1, scaledY)] = WideWarehouseMap.RightBoxSide;
break;
case Empty:
scaledPositions[new(scaledX, scaledY)] = Empty;
scaledPositions[new(scaledX + 1, scaledY)] = Empty;
break;
case Robot:
scaledPositions[new(scaledX, scaledY)] = Robot;
scaledPositions[new(scaledX + 1, scaledY)] = Empty;
break;
default:
throw new ApplicationException($"Unknown value present {value}");
}
}
return WideWarehouseMap.From(scaledPositions);
}
public override Map MoveRobot(List<Direction> directions)
{
var positions = Positions.ToDictionary();
var robotPosition = RobotPosition;
foreach (var direction in directions)
{
var nextPosition = robotPosition.GetNextPosition(direction);
var hasNextPosition = positions.TryGetValue(nextPosition, out var nextPositionValue);
if (hasNextPosition is false)
{
continue;
}
switch (nextPositionValue)
{
case Wall:
continue;
case Empty:
positions[robotPosition] = Empty;
positions[nextPosition] = Robot;
robotPosition = nextPosition;
continue;
case Box when CanPushBox(nextPosition, direction, positions):
positions = PushBox(nextPosition, direction, positions);
positions[robotPosition] = Empty;
positions[nextPosition] = Robot;
robotPosition = nextPosition;
break;
}
}
return new WarehouseMap(positions);
}
public override int CalculateTotalBoxGpsCoordinates()
{
var boxes = Positions.Where(kvp => kvp.Value is Box).ToList();
return boxes.Select(kvp => kvp.Key).Sum(p => p.GpsCoordinate);
}
private static bool CanPushBox(Position boxPosition, Direction direction, Dictionary<Position, char> positions)
{
while (true)
{
var nextPosition = boxPosition.GetNextPosition(direction);
var hasNextPosition = positions.TryGetValue(nextPosition, out var nextPositionValue);
if (hasNextPosition is false)
{
return false;
}
switch (nextPositionValue)
{
case Box:
boxPosition = nextPosition;
continue;
case Empty:
return true;
default:
return false;
}
}
}
private static Dictionary<Position, Char> PushBox(Position boxPosition, Direction direction, Dictionary<Position, char> currentPositions)
{
var positions = currentPositions.ToDictionary();
while (true)
{
var nextPosition = boxPosition.GetNextPosition(direction);
var hasNextPosition = positions.TryGetValue(nextPosition, out var nextPositionValue);
if (hasNextPosition is false)
{
return positions;
}
if (nextPositionValue is Box)
{
boxPosition = nextPosition;
continue;
}
positions[nextPosition] = Box;
return positions;
}
}
}
+129
View File
@@ -0,0 +1,129 @@
namespace WarehouseWoes;
class WideWarehouseMap : Map
{
private WideWarehouseMap(Dictionary<Position, char> positions) : base(positions) {}
public static WideWarehouseMap From(Dictionary<Position, char> positions) => new(positions);
public static WideWarehouseMap From(string[] input) => new(ConvertToDictionary(input));
public override Map MoveRobot(List<Direction> directions)
{
var positions = Positions.ToDictionary();
var robotPosition = RobotPosition;
foreach (var direction in directions)
{
var nextPosition = robotPosition.GetNextPosition(direction);
var hasNextPosition = positions.TryGetValue(nextPosition, out var nextPositionValue);
if (hasNextPosition is false)
{
continue;
}
switch (nextPositionValue)
{
case Wall:
continue;
case Empty:
positions[robotPosition] = Empty;
positions[nextPosition] = Robot;
robotPosition = nextPosition;
continue;
case LeftBoxSide:
case RightBoxSide:
var newPositions = PushBox(nextPosition, direction, positions);
if (newPositions == positions)
{
break;
}
positions = newPositions;
positions[robotPosition] = Empty;
positions[nextPosition] = Robot;
robotPosition = nextPosition;
break;
}
}
return new WideWarehouseMap(positions);
}
public override int CalculateTotalBoxGpsCoordinates()
{
var boxes = Positions.Where(kvp => kvp.Value is LeftBoxSide).ToList();
return boxes.Select(kvp => kvp.Key).Sum(p => p.GpsCoordinate);
}
private static Dictionary<Position, Char> PushBox(Position boxPosition, Direction direction, Dictionary<Position, char> currentPositions)
{
var positions = currentPositions.ToDictionary();
var boxPositions = new Queue<Position>();
var visitedPositions = new HashSet<Position>();
boxPositions.Enqueue(boxPosition);
while (boxPositions.Count is not 0)
{
var currentPosition = boxPositions.Dequeue();
if (visitedPositions.Contains(currentPosition))
{
continue;
}
visitedPositions.Add(currentPosition);
var nextPosition = currentPosition.GetNextPosition(direction);
var hasCurrentPosition = positions.TryGetValue(currentPosition, out var currentPositionValue);
var hasNextPosition = positions.TryGetValue(nextPosition, out var nextPositionValue);
if (hasCurrentPosition is false || hasNextPosition is false)
{
return currentPositions;
}
if (nextPositionValue is Wall)
{
return currentPositions;
}
switch (currentPositionValue)
{
case RightBoxSide:
{
var leftBoxSidePosition = currentPosition.GetNextPosition(Direction.Left);
boxPositions.Enqueue(leftBoxSidePosition);
break;
}
case LeftBoxSide:
{
var rightBoxSidePosition = currentPosition.GetNextPosition(Direction.Right);
boxPositions.Enqueue(rightBoxSidePosition);
break;
}
}
if (nextPositionValue is Empty)
{
continue;
}
boxPositions.Enqueue(nextPosition);
}
foreach (var position in visitedPositions.Reverse())
{
var nextPosition = position.GetNextPosition(direction);
var currentValue = positions[position];
var nextValue = positions[nextPosition];
positions[nextPosition] = currentValue;
positions[position] = nextValue;
}
return positions;
}
}
+2 -2
View File
@@ -36,8 +36,7 @@ dotnet run -- <path-to-input-file> part2
You can also build the projects and run the executables directly. You can also build the projects and run the executables directly.
```bash ```bash
dotnet build dotnet build ./bin/Debug/net9.0/<project-name> <path-to-input-file>
./bin/Debug/net9.0/<project-name> <path-to-input-file>
``` ```
## Challenges ## Challenges
@@ -58,3 +57,4 @@ dotnet build
| 12 | [Problem](./12/PROBLEM.md) | [Solution](./12/GardenGroups/) | CORNERS == SIDES | | 12 | [Problem](./12/PROBLEM.md) | [Solution](./12/GardenGroups/) | CORNERS == SIDES |
| 13 | [Problem](./13/PROBLEM.md) | [Solution](./13/ClawContraption/) | Yay for algebra | | 13 | [Problem](./13/PROBLEM.md) | [Solution](./13/ClawContraption/) | Yay for algebra |
| 14 | [PROBLEM](./14/PROBLEM.md) | [Solution](./14/RestroomRedoubt/) | Part 2 is not as complicated as you think. The answer is using the safety factor | | 14 | [PROBLEM](./14/PROBLEM.md) | [Solution](./14/RestroomRedoubt/) | Part 2 is not as complicated as you think. The answer is using the safety factor |
| 15 | [PROBLEM](./15/PROBLEM.md) | [Solution](./15/WarehouseWoes/) | This breaks my previous star record! |