From 2d56943da72ecef0a8e784691352466b7577b19c Mon Sep 17 00:00:00 2001 From: smabuk <2011834+smabuk@users.noreply.github.com> Date: Sun, 15 Dec 2024 12:01:41 +0000 Subject: [PATCH] 2024 Day15 Solved --- Console/Program.cs | 1 + Solutions/2024/Day15.cs | 286 +++++++++++++++++++++++++++++++++++----- Tests/2024/Tests_15.cs | 84 ++++++++++++ 3 files changed, 340 insertions(+), 31 deletions(-) diff --git a/Console/Program.cs b/Console/Program.cs index 5044826..3a87f79 100644 --- a/Console/Program.cs +++ b/Console/Program.cs @@ -106,6 +106,7 @@ static void VisualiseOutput(string[] lines, bool clearScreen = false) } Console.Write(string.Join(Environment.NewLine, lines)); + //Task.Delay(1).Wait(); } diff --git a/Solutions/2024/Day15.cs b/Solutions/2024/Day15.cs index 6d6a338..b9849e1 100644 --- a/Solutions/2024/Day15.cs +++ b/Solutions/2024/Day15.cs @@ -8,7 +8,8 @@ public static partial class Day15 { private static char[,] _map = default!; - private static IEnumerable _directions = []; + private static List _directions = []; + private static Action? _visualise = null; [Init] public static void Load(string[] input, Action? visualise = null) @@ -16,66 +17,250 @@ public static void Load(string[] input, Action? visualise = null _map = input .TakeWhile(i => !string.IsNullOrWhiteSpace(i)) .To2dArray(); - _directions = string.Join("", input.Skip(_map.RowsCount())) - .Select(c => c.ToDirection()); + + _directions = [.. + string.Join("", input.Skip(_map.RowsCount())) + .Select(c => c.ToDirection()) + ]; + + _visualise = visualise; + _map.VisualiseMap("Initial state:"); } - public static int Part1(string[] _, Action? visualise = null, params object[]? args) + public static int Part1(string[] _) { char[,] map = (char[,])_map.Clone(); - map.VisualiseMap("Initial state:", visualise); - Robot robot = new(map.ForEachCell().Single(c => c.Value is ROBOT).Index); - foreach (var direction in _directions) { - if (robot.TryMove(direction, map, out Thing newRobot)) { + foreach (Direction direction in _directions) { + if (robot.TryMovePart1(direction, map, out Thing newRobot)) { robot = (Robot)newRobot; } - map.VisualiseMap($"Move {direction.FromDirection()}:", visualise); + map.VisualiseMap($"Move {direction.FromDirection()}:", clearScreen: true); } - map.VisualiseMap("Final state:", visualise); + map.VisualiseMap("Final state:"); return map.SumOfGpsCoordinates(); } - private static bool TryMove(this Thing thing, Direction direction, char[,] map, out Thing newThing) + private static bool TryMovePart1(this Thing thing, Direction direction, char[,] map, out Thing newThing) { newThing = thing with { Location = thing.Location + direction.Delta() }; - if (map.TryGetValue(newThing.Location, out char value) && value is WALL) { - return false; - } + char value = newThing.GetTheValue(map); if (value is BOX) { Box box = new(newThing.Location); - _ = box.TryMove(direction, map, out Thing _); - _ = map.TryGetValue(newThing.Location, out value); + _ = box.TryMovePart1(direction, map, out Thing _); + value = newThing.GetTheValue(map); } if (value is EMPTY) { - map[thing.Location.X, thing.Location.Y] = EMPTY; - thing = newThing; - map[thing.Location.X, thing.Location.Y] = thing is Box ? BOX : ROBOT; + map.UpdateMap(thing, EMPTY); + map.UpdateMap(newThing, thing is Robot ? ROBOT : BOX); + return true; + } + + return false; + } + + + + + public static int Part2(string[] _) + { + char[,] map = _map.MakeWide(); + map.VisualiseMap("Wide initial state:"); + Robot robot = new(map.ForEachCell().Single(c => c.Value is ROBOT).Index); + + foreach (Direction direction in _directions) { + if (robot.TryMoveRobot(direction, map, out Thing newRobot)) { + robot = (Robot)newRobot; + } + + map.VisualiseMap($"Move {direction.FromDirection()}:", clearScreen: true); + } + + map.VisualiseMap("Final state:"); + + return map.SumOfGpsCoordinates(); + } + + private static bool TryMoveRobot(this Robot robot, Direction direction, char[,] map, out Thing newRobot) + { + newRobot = robot with { Location = robot.Location + direction.Delta() }; + + char value = newRobot.GetTheValue(map); + + if (value is BOX_LEFT) { + WideBox box = new(newRobot.Location); + _ = box.TryMoveBox(direction, map); + value = newRobot.GetTheValue(map); + } else if (value is BOX_RIGHT) { + WideBox box = new(newRobot.Location.MoveLeft()); + _ = box.TryMoveBox(direction, map); + value = newRobot.GetTheValue(map); + } + + if (value is EMPTY) { + map.UpdateMap(robot, EMPTY); + map.UpdateMap(newRobot, ROBOT); + return true; + } + + return false; + } + + private static bool TryMoveBox(this WideBox box, Direction direction, char[,] map) + { + WideBox newBox = box with { Location = box.Location + direction.Delta() }; + + return direction switch + { + Direction.Left or Direction.Right => newBox.TryMoveBoxHorizontally(box, direction, map), + Direction.Up or Direction.Down => newBox.TryMoveBoxVertically(box, direction, map), + _ => throw new NotImplementedException(), + }; + } + + private static bool TryMoveBoxHorizontally(this WideBox newBox, WideBox box, Direction direction, char[,] map) + { + char value = newBox.GetTheValue(map); + + if (direction is Direction.Left && value is BOX_RIGHT) { + WideBox box1 = newBox with { Location = newBox.Location.MoveLeft() }; + _ = box1.TryMoveBox(direction, map); + value = newBox.GetTheValue(map); + } else if (direction is Direction.Right) { + value = newBox.Location.MoveRight().GetTheValue(map); + if (value is BOX_RIGHT) { + _ = newBox.TryMoveBox(direction, map); + value = newBox.GetTheValue(map); + } else if (value is BOX_LEFT) { + WideBox box1 = newBox with { Location = newBox.Location.MoveRight() }; + _ = box1.TryMoveBox(direction, map); + value = box1.GetTheValue(map); + } + } + + if (value is EMPTY) { + map.UpdateMap(box, EMPTY, EMPTY); + map.UpdateMap(newBox, BOX_LEFT, BOX_RIGHT); + return true; + } + + return false; + } + + private static bool TryMoveBoxVertically(this WideBox newBox, WideBox box, Direction direction, char[,] map) + { + char value1 = newBox.L.GetTheValue(map); + char value2 = newBox.R.GetTheValue(map); + + if (value1 is WALL || value2 is WALL) { + return false; + } + + if (value1 is BOX_LEFT or BOX_RIGHT || value2 is BOX_LEFT or BOX_RIGHT) { + (bool canIMove, List boxes1) = box.CanIMove(direction, map); + if (canIMove) { + List boxes = direction switch + { + Direction.Up => [.. boxes1.OrderBy(b => b.Location)], + Direction.Down => [.. boxes1.OrderByDescending(b => b.Location)], + _ => throw new NotImplementedException(), + }; + + foreach (WideBox boxToMove in boxes) { + newBox = box with { Location = boxToMove.Location + direction.Delta() }; + map.UpdateMap(boxToMove, EMPTY, EMPTY); + map.UpdateMap(newBox, BOX_LEFT, BOX_RIGHT); + } + + return true; + } + } + + if (value1 is EMPTY && value2 is EMPTY) { + newBox = box with { Location = box.Location + direction.Delta() }; + map.UpdateMap(box, EMPTY, EMPTY); + map.UpdateMap(newBox, BOX_LEFT, BOX_RIGHT); return true; } return false; } + private static (bool Yes, List WideBoxes) CanIMove(this WideBox? box, Direction direction, char[,] map) + { + if (box is null) { + return (true, []); + } + + WideBox newBox = box with { Location = box.Location + direction.Delta() }; + char value1 = newBox.L.GetTheValue(map); + char value2 = newBox.R.GetTheValue(map); + + if (value1 is EMPTY && value2 is EMPTY) { + return (true, []); + } + + if (value1 is WALL || value2 is WALL) { + //map.VisualiseMap($"Can't Move: {direction} {box}"); + return (false, []); + } + + bool canIMove = true; + List boxes = [box]; + + WideBox? box1 = value1 switch { + BOX_RIGHT => newBox with { Location = newBox.Location.MoveLeft() }, + BOX_LEFT => newBox with { }, + _ => null, + }; + + WideBox? box2 = value2 switch { + BOX_LEFT => newBox with { Location = newBox.Location.MoveRight() }, + _ => null, + }; + + if (box1 is not null) { + (bool result1, List boxes1) = box1.CanIMove(direction, map); + canIMove = canIMove && result1; + boxes = [.. boxes, box1, .. boxes1]; + } + + if (box2 is not null) { + (bool result2, List boxes2) = box2.CanIMove(direction, map); + canIMove = canIMove && result2; + boxes = [.. boxes, box2, .. boxes2]; + } + + return (canIMove, [.. boxes.Distinct()]); + } + + private static int GpsCoordinate(this Point location) => (location.Y * 100) + location.X; private static int SumOfGpsCoordinates(this char[,] map) { return map .ForEachCell() - .Where(c => c.Value is BOX) + .Where(c => c.Value is BOX or BOX_LEFT) .Sum(c => c.Index.GpsCoordinate()); } - private static int GpsCoordinate(this Point location) => (location.Y * 100) + location.X; + private static void UpdateMap(this char[,] map, Thing thing, char value) => map[thing.X, thing.Y] = value; + private static void UpdateMap(this char[,] map, WideBox widebox, char value1, char value2) + { + map[widebox.L.X, widebox.L.Y] = value1; + map[widebox.R.X, widebox.R.Y] = value2; + } + + private static char GetTheValue(this Point location, char[,] map) => map[location.X, location.Y]; + private static char GetTheValue(this Thing thing, char[,] map) => map[thing.Location.X, thing.Location.Y]; - public static string Part2(string[] input, params object[]? args) => NO_SOLUTION_WRITTEN_MESSAGE; public static Direction ToDirection(this char c) { @@ -101,24 +286,63 @@ public static char FromDirection(this Direction direction) }; } - private static void VisualiseMap(this char[,] map, string title, Action? visualise, bool clearScreen = false) + private static char[,] MakeWide(this char[,] map) + { + char[,] newMap = new char[map.ColsCount() * 2, map.RowsCount()]; + newMap = newMap.Fill('.'); + + foreach (Cell cell in map.ForEachCell()) { + if (cell.Value is ROBOT) { + newMap[cell.Index.X * 2, cell.Index.Y] = ROBOT; + } else if (cell.Value is WALL) { + newMap[cell.Index.X * 2, cell.Index.Y] = WALL; + newMap[(cell.Index.X * 2) + 1, cell.Index.Y] = WALL; + } else if (cell.Value is BOX) { + newMap[cell.Index.X * 2, cell.Index.Y] = BOX_LEFT; + newMap[(cell.Index.X * 2) + 1, cell.Index.Y] = BOX_RIGHT; + } + } + + return newMap; + } + + + private static void VisualiseMap(this char[,] map, string title, bool clearScreen = false) { - if (visualise is null) { + if (_visualise is null) { return; } - string[] output = ["", title, .. map.AsStrings().Select(s => s.Replace('0', '.'))]; - visualise?.Invoke(output, clearScreen); + string[] output = ["", title]; + if (map.ColsCount() > 20) { + output = [.. output, .. map.AsStrings().Select(s => s.Replace(EMPTY, ' '))]; + } else { + output = [.. output, .. map.AsStrings()]; + } + + _visualise?.Invoke(output, clearScreen); } - private abstract record Thing(Point Location); + private abstract record Thing(Point Location) + { + public int X => Location.X; + public int Y => Location.Y; + }; + private record Robot(Point Location) : Thing(Location); private record Box(Point Location) : Thing(Location); + private record WideBox(Point Location) : Thing(Location) + { + public Point L => Location; + public Point R => Location.MoveRight(); + } - private const char WALL = '#'; - private const char EMPTY = '.'; - private const char BOX = 'O'; - private const char ROBOT = '@'; + private const char BOX = 'O'; + private const char BOX_LEFT = '['; + private const char BOX_RIGHT = ']'; + private const char EMPTY = '.'; + private const char ROBOT = '@'; + private const char WALL = '#'; } diff --git a/Tests/2024/Tests_15.cs b/Tests/2024/Tests_15.cs index 0bf01e3..a1dba07 100644 --- a/Tests/2024/Tests_15.cs +++ b/Tests/2024/Tests_15.cs @@ -48,7 +48,91 @@ public async Task Part1(string input, int expected) } + [Theory] + [InlineData(""" + ######### + #..O..O@# + ######### + + <<<<<<<< + """, 206)] + [InlineData(""" + ######### + #@.O..O.# + ######### + + >>>>>>>>>> + """, 226)] + [InlineData(""" + ######### + #..O#.O@# + ######### + + <<<<<<<< + """, 216)] + [InlineData(""" + ######### + #..#....# + #.......# + #..OO...# + #.......# + #..@....# + #.......# + ######### + + ^^^ + """, 514)] + [InlineData(""" + ####### + #...#.# + #.....# + #..OO@# + #..O..# + #.....# + ####### + + vv<^ + """, 611)] + [InlineData(""" + ########## + #..O..O.O# + #......O.# + #.OO..O.O# + #..O@..O.# + #O#..O...# + #O..O..O.# + #.OO.O.OO# + #....O...# + ########## + ^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^<<^ + """, 9021)] + public async Task Part2(string input, int expected) + { + _ = int.TryParse(SolutionRouter.SolveProblem(YEAR, DAY, PART2, input, new Action(Callback)), out int actual); + actual.ShouldBe(expected); + await Task.Delay(200); // Allow time to visualise + } private void Callback(string[] lines, bool _) {