From 62505c21a8ebe36513d67169abdf835cf2689891 Mon Sep 17 00:00:00 2001 From: Jonathan George Date: Thu, 21 Feb 2019 15:34:16 +0000 Subject: [PATCH] Day 15 part 1 --- AoC2018.Runner/AoC2018.Runner.csproj | 3 + AoC2018.Runner/Input/day15.txt | 32 ++++ AoC2018.Runner/Input/day18.txt | 50 ++++++ AoC2018.Runner/Properties/launchSettings.json | 2 +- AoC2018.Solutions/Day15/Elf.cs | 12 ++ AoC2018.Solutions/Day15/Goblin.cs | 12 ++ AoC2018.Solutions/Day15/LocationHelper.cs | 18 ++ AoC2018.Solutions/Day15/MapSpace.cs | 41 +++++ AoC2018.Solutions/Day15/MapSpaceExtensions.cs | 10 ++ AoC2018.Solutions/Day15/Part01.cs | 40 +++++ AoC2018.Solutions/Day15/State.cs | 32 ++++ AoC2018.Solutions/Day15/StateExtensions.cs | 68 ++++++++ AoC2018.Solutions/Day15/Unit.cs | 13 ++ AoC2018.Solutions/Day15/UnitExtensions.cs | 155 ++++++++++++++++++ AoC2018.Tests/AoCTestCases.cs | 5 + AoC2018.Tests/Day15/StateExtensionTests.cs | 44 +++++ AoC2018.Tests/Day15/StateTests.cs | 76 +++++++++ AoC2018.Tests/Day15/UnitExtensionTests.cs | 38 +++++ 18 files changed, 650 insertions(+), 1 deletion(-) create mode 100644 AoC2018.Runner/Input/day15.txt create mode 100644 AoC2018.Runner/Input/day18.txt create mode 100644 AoC2018.Solutions/Day15/Elf.cs create mode 100644 AoC2018.Solutions/Day15/Goblin.cs create mode 100644 AoC2018.Solutions/Day15/LocationHelper.cs create mode 100644 AoC2018.Solutions/Day15/MapSpace.cs create mode 100644 AoC2018.Solutions/Day15/MapSpaceExtensions.cs create mode 100644 AoC2018.Solutions/Day15/Part01.cs create mode 100644 AoC2018.Solutions/Day15/State.cs create mode 100644 AoC2018.Solutions/Day15/StateExtensions.cs create mode 100644 AoC2018.Solutions/Day15/Unit.cs create mode 100644 AoC2018.Solutions/Day15/UnitExtensions.cs create mode 100644 AoC2018.Tests/Day15/StateExtensionTests.cs create mode 100644 AoC2018.Tests/Day15/StateTests.cs create mode 100644 AoC2018.Tests/Day15/UnitExtensionTests.cs diff --git a/AoC2018.Runner/AoC2018.Runner.csproj b/AoC2018.Runner/AoC2018.Runner.csproj index d07f7b2..ef630c3 100644 --- a/AoC2018.Runner/AoC2018.Runner.csproj +++ b/AoC2018.Runner/AoC2018.Runner.csproj @@ -28,6 +28,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/AoC2018.Runner/Input/day15.txt b/AoC2018.Runner/Input/day15.txt new file mode 100644 index 0000000..c17b555 --- /dev/null +++ b/AoC2018.Runner/Input/day15.txt @@ -0,0 +1,32 @@ +################################ +######..#######.################ +######...######..#.############# +######...#####.....############# +#####....###G......############# +#####.#G..#..GG..##########..#.# +#######G..G.G.....##..###......# +######....G...........#.....#### +#######.G......G........##..#### +######..#..G.......E...........# +######..G................E..E..# +####.............E..........#..# +#####.........#####........##..# +########.....#######.......##### +########..G.#########......##### +#######.....#########....####### +#######G....#########..G...##.## +#.#.....GG..#########E##...#..## +#.#....G....#########.##...#...# +#....##......#######..###..##E.# +####G.........#####...####....## +####..G...............######..## +####....#.....##############.### +#..#..###......################# +#..#..###......################# +#.....#####....################# +###########.E.E.################ +###########.E...################ +###########.E..################# +#############.################## +#############.################## +################################ \ No newline at end of file diff --git a/AoC2018.Runner/Input/day18.txt b/AoC2018.Runner/Input/day18.txt new file mode 100644 index 0000000..03c9c9f --- /dev/null +++ b/AoC2018.Runner/Input/day18.txt @@ -0,0 +1,50 @@ +||.|..#|.|.....|..|........|..#..|#...|.........#. +...#....|#..|......#.|||...#...|...#....|.......#. +##.#|.#..|.#|....||.|#.#....#.#.###.|..|...|#.#|.. +..||.#|#|#..#.#|...|....||.|...##.....|.....#...#| +#|..#...#.##|.|.##||..|...|...#||.|...#.##.###|.|. +#.#||#...#.|#.....|#.......|#.||..|..|#..#.##...#. +|.||.|.|........|.|.#.|...##.|.||..#....#..|.|#..# +....#...|#...#....|#.|.|||#.||#....||.......|.#|.. +|#..#..#.|.|#.||#...|.|...|#.||...#...##....|....| +..#...|......#.#...|#...#|.|......#.|||#.|#.|#.#.| +..##.#|#|...||....##|.|||.|##|#.....#|...#...###|| +...||......#...|....#.....#|#......#......#..|.|#. +..........#|.|#..|..#.##.#...#|...#..#...|...#|.|| +|.#..#|###..||...|#|....|...........|...#|#.#..#|. +|.#..##.........#||.....|#|.###.#.|.....#.|..|.##. +....#||..#|...|.#...#.|.#.|..#............#.|..... +...|#...##.#.#.|.||.......#...|.....###.||...##..| +..|...|#.|.#..|..#|..||.#.....#||.|..#..|..|.||.#. +#.##||#.#.....||.|.#.......#.|.##|##|#..#|.....|#. +...#..#.#..#.|.....|.#...|.....###||||..#..##...#. +...|...|.|.##|..#.#|#....#.#.|#||||....#...|||.#.# +..|...##..|#||..||#|#..|..|##.|..##...#...#|.##.#| +|#...#.#.......|...||...##.|.||#.....#.||.......#| +..#.....#|.#...#...|.#|.#.#|.....#.##.#|.....#.... +......#|..|||.#.#..||..|.|..|...##.....#..#.###... +....##..#..|..||..##.||#.|..|.#...#..||#...#..|... +.||......||#.#|#.##.||.#..|..#..#..#.|..|#|..||... +|...#|...|....|.|.|.....||.|.|..|#.#.#..|.#..##..# +..|..#..#.|.##.....|#..|..#|.|..#.#.|..|..#|..|... +....|...#..|#...#.###.|..####..||#..|.|.|......||. +.....||.#.|##|.....|##.||.##..#.#.|...#.#...||.#.. +.|#..#.|...||#..|.#..#....###.#...#.....||......## +||...#||..#...##.|..|...|#.|#|.|##|#|##....#..#|.. +.|..#.##..|......#|.....||..|||||||#.#.|#..|.|.|.. +|#.##.|............##||##.#...#.#........|..#..... +.||....|.#||#|.#..|..........#....##.#.|...#.||..| +......|...#.......##.#.#|#......#||##...#..#||.|#. +..||.#..#.#.......|.|.##.#.|...#...###..|...|..|.. +.#....|..||##.|.|..#.|.#|..#|......#....#.#.|..||. +.........|....|.#..|...|.|#....#..|.....#...|#|#.| +.|.||..|#.|...##.|.#...#..|.||||..##.##|#.##.|##.# +.|#.#.....#||#.#....||.|..##......|.#.|..#...##||. +|.#........|..|........#..#.#....|.|#.......#.|... +#..##.|.#......#...#..#.|.....#|.|.|#.##..#|...|.. +..#....####....|..||...||..||.|.|...##.#..|||.||.| +...|..#.|||.||.||.#|...|..#.##..|#..........#.##|. +..|..||.#|.|.#..|.###|#.|....|.||.....|.........#. +#.|#.....#.|.#|#.|.#.#||...|.....||..|..|.##|#.|#| +..|...||#.|.#...|#.#....#.|##.#.|.....##..#..|.#.. +..|.#...|.|...##|..#.#|.#|..|..#.|#.#...#|#.#||#.. \ No newline at end of file diff --git a/AoC2018.Runner/Properties/launchSettings.json b/AoC2018.Runner/Properties/launchSettings.json index 8ffbf88..3c21f9a 100644 --- a/AoC2018.Runner/Properties/launchSettings.json +++ b/AoC2018.Runner/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "AoC2018.Runner": { "commandName": "Project", - "commandLineArgs": "14 2" + "commandLineArgs": "15 1" } } } \ No newline at end of file diff --git a/AoC2018.Solutions/Day15/Elf.cs b/AoC2018.Solutions/Day15/Elf.cs new file mode 100644 index 0000000..d0a24f6 --- /dev/null +++ b/AoC2018.Solutions/Day15/Elf.cs @@ -0,0 +1,12 @@ +namespace AoC2018.Solutions.Day15 +{ + using System; + + public class Elf : Unit + { + public override string ToString() + { + return "E"; + } + } +} diff --git a/AoC2018.Solutions/Day15/Goblin.cs b/AoC2018.Solutions/Day15/Goblin.cs new file mode 100644 index 0000000..167e459 --- /dev/null +++ b/AoC2018.Solutions/Day15/Goblin.cs @@ -0,0 +1,12 @@ +namespace AoC2018.Solutions.Day15 +{ + using System; + + public class Goblin : Unit + { + public override string ToString() + { + return "G"; + } + } +} diff --git a/AoC2018.Solutions/Day15/LocationHelper.cs b/AoC2018.Solutions/Day15/LocationHelper.cs new file mode 100644 index 0000000..443686b --- /dev/null +++ b/AoC2018.Solutions/Day15/LocationHelper.cs @@ -0,0 +1,18 @@ +namespace AoC2018.Solutions.Day15 +{ + using System.Collections.Generic; + using System.Linq; + + public static class LocationHelper + { + public static (int X, int Y) GetCoordinates(int location, int yOffset) + { + return (location % yOffset, location / yOffset); + } + + public static int GetLocation(int x, int y, int yOffset) + { + return x + (y * yOffset); + } + } +} diff --git a/AoC2018.Solutions/Day15/MapSpace.cs b/AoC2018.Solutions/Day15/MapSpace.cs new file mode 100644 index 0000000..6726648 --- /dev/null +++ b/AoC2018.Solutions/Day15/MapSpace.cs @@ -0,0 +1,41 @@ +namespace AoC2018.Solutions.Day15 +{ + using System; + + public class MapSpace + { + public int Location { get; set; } + + public Unit Unit { get; set; } + + public static MapSpace Parse(char input, int location) + { + var result = new MapSpace(); + + switch (input) + { + case '#': + return null; + + case '.': + break; + + case 'E': + result.Unit = new Elf { CurrentLocation = result }; + break; + + case 'G': + result.Unit = new Goblin { CurrentLocation = result }; + break; + } + + result.Location = location; + return result; + } + + public override string ToString() + { + return this.Unit?.ToString() ?? "."; + } + } +} diff --git a/AoC2018.Solutions/Day15/MapSpaceExtensions.cs b/AoC2018.Solutions/Day15/MapSpaceExtensions.cs new file mode 100644 index 0000000..569502e --- /dev/null +++ b/AoC2018.Solutions/Day15/MapSpaceExtensions.cs @@ -0,0 +1,10 @@ +namespace AoC2018.Solutions.Day15 +{ + public static class MapSpaceExtensions + { + public static bool IsEmpty(this MapSpace space) + { + return space.Unit == null; + } + } +} diff --git a/AoC2018.Solutions/Day15/Part01.cs b/AoC2018.Solutions/Day15/Part01.cs new file mode 100644 index 0000000..1f466de --- /dev/null +++ b/AoC2018.Solutions/Day15/Part01.cs @@ -0,0 +1,40 @@ +namespace AoC2018.Solutions.Day15 +{ + using System; + + public class Part01 : ISolution + { + public string Solve(string input) + { + var currentState = State.Parse(input); + int rounds = 0; + + ////Console.WriteLine(currentState.ToString()); + + while (true) + { + bool roundCompleted = currentState.Round(); + + if (roundCompleted) + { + rounds++; + } + + if (currentState.IsCombatEnded()) + { + break; + } + + ////Console.WriteLine(); + ////Console.WriteLine($"Round {rounds}"); + ////Console.WriteLine(currentState.ToString()); + } + + ////Console.WriteLine(); + ////Console.WriteLine($"Combat ended during round {rounds + 1}"); + ////Console.WriteLine(currentState.ToString()); + + return (rounds * currentState.TotalRemainingHitPoints()).ToString(); + } + } +} diff --git a/AoC2018.Solutions/Day15/State.cs b/AoC2018.Solutions/Day15/State.cs new file mode 100644 index 0000000..20905a5 --- /dev/null +++ b/AoC2018.Solutions/Day15/State.cs @@ -0,0 +1,32 @@ +namespace AoC2018.Solutions.Day15 +{ + using System; + using System.Collections.Generic; + using System.Linq; + + public class State + { + public int YOffset { get; set; } + + public MapSpace[] Map { get; set; } + + public int MaxY { get; set; } + + public static State Parse(string input) + { + string[] inputRows = input.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); + int yOffset = inputRows[0].Length; + char[] mapChars = inputRows.SelectMany(x => x.ToCharArray()).ToArray(); + MapSpace[] map = Enumerable.Range(0, mapChars.Length).Select(x => MapSpace.Parse(mapChars[x], x)).ToArray(); + + return new State { Map = map, YOffset = yOffset, MaxY = inputRows.Length }; + } + + public override string ToString() + { + string mapString = string.Join(string.Empty, this.Map.Select(x => x?.ToString() ?? "#")); + IEnumerable rows = Enumerable.Range(0, this.Map.Length / this.YOffset).Select(x => mapString.Substring(x * this.YOffset, this.YOffset)); + return string.Join(Environment.NewLine, rows); + } + } +} diff --git a/AoC2018.Solutions/Day15/StateExtensions.cs b/AoC2018.Solutions/Day15/StateExtensions.cs new file mode 100644 index 0000000..0f25042 --- /dev/null +++ b/AoC2018.Solutions/Day15/StateExtensions.cs @@ -0,0 +1,68 @@ +namespace AoC2018.Solutions.Day15 +{ + using System.Collections.Generic; + using System.Linq; + + public static class StateExtensions + { + public static bool Round(this State initialState) + { + // Every unit gets a turn, taken in reading order. + // (we get reading order just by iterating the original + // state + State currentState = initialState; + + // Get the units. Note that it is important to materialise this collection immediately, + // otherwise we run the risk of double moving units as we iterate through the map. + Unit[] units = initialState.Map.Where(x => x?.Unit != null).OrderBy(x => x.Location).Select(x => x.Unit).ToArray(); + + foreach (Unit current in units) + { + if (currentState.IsCombatEnded()) + { + return false; + } + + // Make sure it hasn't been killed already + if (!current.IsKilled()) + { + current.TakeTurn(currentState); + } + } + + return true; + } + + public static IEnumerable GetAdjacentSpaces(this State state, MapSpace space) + { + // Note: because we know the caverns in question are walled all around, we + // don't need to worry about bounds checking. If this wasn't the case we'd + // need to check to see if the space we're looking at was on the edge of the + // map before checking the adjacent spaces. + int[] adjacentLocations = new[] + { + space.Location - state.YOffset, + space.Location - 1, + space.Location + 1, + space.Location + state.YOffset, + }; + + return adjacentLocations.Select(x => state.Map[x]).Where(x => x != null); + } + + public static IEnumerable GetEmptyAdjacentSpaces(this State state, MapSpace space) + { + return state.GetAdjacentSpaces(space).Where(MapSpaceExtensions.IsEmpty); + } + + public static bool IsCombatEnded(this State state) + { + return !state.Map.Any(x => x?.Unit is Elf) || !state.Map.Any(x => x?.Unit is Goblin); + } + + public static int TotalRemainingHitPoints(this State state) + { + return state.Map.Sum(x => x?.Unit?.HitPoints ?? 0); + } + } +} diff --git a/AoC2018.Solutions/Day15/Unit.cs b/AoC2018.Solutions/Day15/Unit.cs new file mode 100644 index 0000000..dbca592 --- /dev/null +++ b/AoC2018.Solutions/Day15/Unit.cs @@ -0,0 +1,13 @@ +namespace AoC2018.Solutions.Day15 +{ + using System; + + public abstract class Unit + { + public MapSpace CurrentLocation { get; set; } + + public int AttackStrength { get; set; } = 3; + + public int HitPoints { get; set; } = 200; + } +} diff --git a/AoC2018.Solutions/Day15/UnitExtensions.cs b/AoC2018.Solutions/Day15/UnitExtensions.cs new file mode 100644 index 0000000..fdb06eb --- /dev/null +++ b/AoC2018.Solutions/Day15/UnitExtensions.cs @@ -0,0 +1,155 @@ +namespace AoC2018.Solutions.Day15 +{ + using System; + using System.Collections.Generic; + using System.Linq; + + public static class UnitExtensions + { + public static void TakeTurn(this Unit unit, State state) + { + if (unit.IsKilled()) + { + throw new InvalidOperationException("A killed unit cannot take a turn"); + } + + unit.Move(state); + unit.Attack(state); + } + + public static Unit InRangeEnemy(this Unit unit, State currentState) + { + Type targetFoeType = unit.EnemyType(); + + return currentState.GetAdjacentSpaces(unit.CurrentLocation).Where(x => x.Unit?.GetType() == targetFoeType).Select(x => x.Unit).OrderBy(x => x.HitPoints).ThenBy(x => x.CurrentLocation.Location).FirstOrDefault(); + } + + public static void Move(this Unit unit, State state) + { + // Are we already in range? + if (unit.InRangeEnemy(state) != null) + { + // We're going nowhere. + return; + } + + MapSpace[] inRangeSpaces = unit.FindSpacesInRangeOfAnEnemy(state); + + // Find the closest in range space + MapSpace[] pathToClosestSpace = unit.GetPathToClosest(inRangeSpaces, state); + + // If we found a path, take a step towards it (otherwise stay here - this unit + // is blocked from moving for some reason) + if (pathToClosestSpace != null) + { + MapSpace targetSpace = pathToClosestSpace[1]; + + unit.MoveTo(targetSpace); + } + } + + public static void MoveTo(this Unit unit, MapSpace targetSpace) + { + if (targetSpace.Unit != null) + { + throw new InvalidOperationException("Target space is already occupied"); + } + + unit.CurrentLocation.Unit = null; + unit.CurrentLocation = targetSpace; + unit.CurrentLocation.Unit = unit; + } + + public static Type EnemyType(this Unit unit) + { + // Who's the enemy? + return unit is Elf ? typeof(Goblin) : typeof(Elf); + } + + public static MapSpace[] FindSpacesInRangeOfAnEnemy(this Unit unit, State currentState) + { + // Who's the enemy? + Type targetFoeType = unit.EnemyType(); + + // Where's the enemy? + IEnumerable enemyLocations = currentState.Map.Where(x => x?.Unit != null && x.Unit.GetType() == targetFoeType); + + // Find in-range spaces + return enemyLocations.SelectMany(currentState.GetEmptyAdjacentSpaces).OrderBy(x => x.Location).Distinct().ToArray(); + } + + public static MapSpace[] GetPathToClosest(this Unit unit, MapSpace[] targetSpaces, State currentState) + { + // We're doing a basic breadth first search where we'll track all the possible moves + // until we hit a target space. + var pathFindingQueue = new Queue>(); + pathFindingQueue.Enqueue(new List { unit.CurrentLocation }); + var visitedSpaces = new List(currentState.YOffset * currentState.YOffset); + + while (pathFindingQueue.Count > 0) + { + List currentPath = pathFindingQueue.Dequeue(); + MapSpace currentSpace = currentPath[currentPath.Count - 1]; + + if (visitedSpaces.Contains(currentSpace)) + { + // We've been here already, and took less time to do it. So abandon this path + continue; + } + else + { + visitedSpaces.Add(currentSpace); + } + + // Get the adjacent spaces that haven't already been visited by any path before + MapSpace[] adjacentSpaces = currentState.GetEmptyAdjacentSpaces(currentSpace).Where(x => !visitedSpaces.Contains(x)).ToArray(); + + if (adjacentSpaces.Length == 0) + { + // Dead end. + continue; + } + + // See if any of the adjacent spaces are in the list of target spaces + MapSpace target = Array.Find(adjacentSpaces, x => targetSpaces.Contains(x)); + + if (target != null) + { + var resultingPath = new List(currentPath) { target }; + return resultingPath.ToArray(); + } + + // None of the adjacent spaces are in the list, so we'll continue searching + foreach (MapSpace space in adjacentSpaces) + { + var newPath = new List(currentPath) { space }; + pathFindingQueue.Enqueue(newPath); + } + } + + // If we're here, we've exhausted our search. There's nothing in range + return null; + } + + public static void Attack(this Unit unit, State currentState) + { + Unit inRangeEnemy = unit.InRangeEnemy(currentState); + + if (inRangeEnemy != null) + { + inRangeEnemy.HitPoints -= unit.AttackStrength; + + if (inRangeEnemy.IsKilled()) + { + inRangeEnemy.CurrentLocation.Unit = null; + inRangeEnemy.CurrentLocation = null; + } + } + } + + public static bool IsKilled(this Unit unit) + { + return unit.HitPoints <= 0; + } + } +} diff --git a/AoC2018.Tests/AoCTestCases.cs b/AoC2018.Tests/AoCTestCases.cs index 009bb6a..ef97375 100644 --- a/AoC2018.Tests/AoCTestCases.cs +++ b/AoC2018.Tests/AoCTestCases.cs @@ -48,6 +48,11 @@ public class AoCTestCases [TestCase(14, 2, "01245", "5")] [TestCase(14, 2, "92510", "18")] [TestCase(14, 2, "59414", "2018")] + [TestCase(15, 1, "#######\r\n#G..#E#\r\n#E#E.E#\r\n#G.##.#\r\n#...#E#\r\n#...E.#\r\n#######", "36334")] + [TestCase(15, 1, "#######\r\n#E..EG#\r\n#.#G.E#\r\n#E.##E#\r\n#G..#.#\r\n#..E#.#\r\n#######", "39514")] + [TestCase(15, 1, "#######\r\n#E.G#.#\r\n#.#G..#\r\n#G.#.G#\r\n#G..#.#\r\n#...E.#\r\n#######", "27755")] + [TestCase(15, 1, "#######\r\n#.E...#\r\n#.#..G#\r\n#.###.#\r\n#E#G#G#\r\n#...#G#\r\n#######", "28944")] + [TestCase(15, 1, "#########\r\n#G......#\r\n#.E.#...#\r\n#..##..G#\r\n#...##..#\r\n#...#...#\r\n#.G...G.#\r\n#.....G.#\r\n#########", "18740")] public void Tests(int day, int part, string input, string expectedResult) { ISolution solution = SolutionFactory.GetSolution(day, part); diff --git a/AoC2018.Tests/Day15/StateExtensionTests.cs b/AoC2018.Tests/Day15/StateExtensionTests.cs new file mode 100644 index 0000000..049c0c9 --- /dev/null +++ b/AoC2018.Tests/Day15/StateExtensionTests.cs @@ -0,0 +1,44 @@ +namespace AoC2018.Tests.Day15 +{ + using System.Collections.Generic; + using System.Linq; + using AoC2018.Solutions.Day15; + using NUnit.Framework; + + public class StateExtensionTests + { + [TestCase("#######\r\n#E..G.#\r\n#...#.#\r\n#.G.#G#\r\n#######", 8, new[] { 9, 15 }, Description = "Walls above/left")] + [TestCase("#######\r\n#E..G.#\r\n#...#.#\r\n#.G.#G#\r\n#######", 11, new[] { 10, 12 }, Description = "Walls above/below")] + [TestCase("#######\r\n#E..G.#\r\n#...#.#\r\n#.G.#G#\r\n#######", 26, new[] { 19 }, Description = "Walls above/left/right")] + [TestCase("#######\r\n#E..G.#\r\n#...#.#\r\n#.G.#G#\r\n#######", 16, new[] { 9, 15, 17 }, Description = "Unit below")] + [TestCase("#######\r\n#E..G.#\r\n#...#E#\r\n#.G.#G#\r\n#######", 26, new int[0], Description = "Unit below")] + public void GetAdjacentSpacesTests(string stateInput, int location, int[] expectedResults) + { + var state = State.Parse(stateInput); + MapSpace startLocation = state.Map[location]; + MapSpace[] result = state.GetEmptyAdjacentSpaces(startLocation).ToArray(); + + Assert.That(result, Has.Length.EqualTo(expectedResults.Length)); + IEnumerable resultLocations = result.Select(x => x.Location); + + Assert.That(resultLocations, Is.EquivalentTo(expectedResults)); + } + + [TestCase("#########\r\n#G..G..G#\r\n#.......#\r\n#.......#\r\n#G..E..G#\r\n#.......#\r\n#.......#\r\n#G..G..G#\r\n#########", 1, "#########\r\n#.G...G.#\r\n#...G...#\r\n#...E..G#\r\n#.G.....#\r\n#.......#\r\n#G..G..G#\r\n#.......#\r\n#########")] + [TestCase("#########\r\n#G..G..G#\r\n#.......#\r\n#.......#\r\n#G..E..G#\r\n#.......#\r\n#.......#\r\n#G..G..G#\r\n#########", 2, "#########\r\n#..G.G..#\r\n#...G...#\r\n#.G.E.G.#\r\n#.......#\r\n#G..G..G#\r\n#.......#\r\n#.......#\r\n#########")] + [TestCase("#########\r\n#G..G..G#\r\n#.......#\r\n#.......#\r\n#G..E..G#\r\n#.......#\r\n#.......#\r\n#G..G..G#\r\n#########", 3, "#########\r\n#.......#\r\n#..GGG..#\r\n#..GEG..#\r\n#G..G...#\r\n#......G#\r\n#.......#\r\n#.......#\r\n#########")] + public void RoundTests(string initialState, int numberOfRounds, string expectedState) + { + var state = State.Parse(initialState); + + for (int i = 0; i < numberOfRounds; i++) + { + state.Round(); + } + + var expected = State.Parse(expectedState); + + Assert.That(state.ToString(), Is.EqualTo(expected.ToString())); + } + } +} diff --git a/AoC2018.Tests/Day15/StateTests.cs b/AoC2018.Tests/Day15/StateTests.cs new file mode 100644 index 0000000..7fd5e06 --- /dev/null +++ b/AoC2018.Tests/Day15/StateTests.cs @@ -0,0 +1,76 @@ +namespace AoC2018.Tests.Day15 +{ + using AoC2018.Solutions.Day15; + using NUnit.Framework; + + public class StateTests + { + [Test] + public void ParseSetsYOffsetCorrectly() + { + var state = State.Parse("#######\r\n#.G.E.#\r\n#E.G.E#\r\n#.G.E.#\r\n#######"); + + Assert.That(state.YOffset, Is.EqualTo(7)); + } + + [Test] + public void ParseSetsYMaxCorrectly() + { + var state = State.Parse("#######\r\n#.G.E.#\r\n#E.G.E#\r\n#.G.E.#\r\n#######"); + + Assert.That(state.MaxY, Is.EqualTo(5)); + } + + [Test] + public void ParseSetsWallsCorrectly() + { + var state = State.Parse("#######\r\n#.G.E.#\r\n#E.G.E#\r\n#.G.E.#\r\n#######"); + + int[] wallLocations = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 13, 14, 20, 21, 27, 28, 29, 30, 31, 32, 33, 34 }; + + foreach (int current in wallLocations) + { + Assert.That(state.Map[current], Is.Null); + } + } + + [Test] + public void ParseSetsSpacesCorrectly() + { + var state = State.Parse("#######\r\n#.G.E.#\r\n#E.G.E#\r\n#.G.E.#\r\n#######"); + + int[] spaceLocations = new[] { 8, 9, 10, 11, 12, 15, 16, 17, 18, 19, 22, 23, 24, 25, 26 }; + + foreach (int current in spaceLocations) + { + Assert.That(state.Map[current], Is.InstanceOf()); + } + } + + [Test] + public void ParseSetsElvesCorrectly() + { + var state = State.Parse("#######\r\n#.G.E.#\r\n#E.G.E#\r\n#.G.E.#\r\n#######"); + + int[] elfLocations = new[] { 11, 15, 19, 25 }; + + foreach (int current in elfLocations) + { + Assert.That(state.Map[current].Unit, Is.InstanceOf()); + } + } + + [Test] + public void ParseSetsGoblinsCorrectly() + { + var state = State.Parse("#######\r\n#.G.E.#\r\n#E.G.E#\r\n#.G.E.#\r\n#######"); + + int[] goblinLocations = new[] { 9, 17, 23 }; + + foreach (int current in goblinLocations) + { + Assert.That(state.Map[current].Unit, Is.InstanceOf()); + } + } + } +} diff --git a/AoC2018.Tests/Day15/UnitExtensionTests.cs b/AoC2018.Tests/Day15/UnitExtensionTests.cs new file mode 100644 index 0000000..fac6599 --- /dev/null +++ b/AoC2018.Tests/Day15/UnitExtensionTests.cs @@ -0,0 +1,38 @@ +namespace AoC2018.Tests.Day15 +{ + using System.Collections.Generic; + using System.Linq; + using AoC2018.Solutions.Day15; + using NUnit.Framework; + + public class UnitExtensionTests + { + [Test] + public void FindInRangeSpacesTests() + { + var state = State.Parse("#######\r\n#E..G.#\r\n#...#.#\r\n#.G.#G#\r\n#######"); + + // Get the elf in the top left corner + Unit targetUnit = state.Map[8].Unit; + + MapSpace[] inRangeSpaces = targetUnit.FindSpacesInRangeOfAnEnemy(state); + IEnumerable inRangeLocations = inRangeSpaces.Select(x => x.Location); + Assert.That(inRangeLocations, Is.EquivalentTo(new[] { 10, 12, 16, 19, 22, 24 })); + } + + [TestCase("#######\r\n#E..G.#\r\n#...#.#\r\n#.G.#G#\r\n#######", 8, new[] { 8, 9, 10 })] + [TestCase("#######\r\n#.E...#\r\n#...?.#\r\n#..?G?#\r\n#######", 9, new[] { 9, 10, 11, 18 })] + public void GetPathToClosestTests(string input, int startLocation, int[] expectedPath) + { + var state = State.Parse(input); + + // Get the elf in the top left corner + Unit targetUnit = state.Map[startLocation].Unit; + MapSpace[] inRangeSpaces = targetUnit.FindSpacesInRangeOfAnEnemy(state); + MapSpace[] path = targetUnit.GetPathToClosest(inRangeSpaces, state); + IEnumerable pathLocations = path.Select(x => x.Location); + + Assert.That(pathLocations, Is.EquivalentTo(expectedPath)); + } + } +}