diff --git a/AdvenOfCode.Contracts/AdvenOfCode.Contracts.csproj b/AdvenOfCode.Contracts/AdvenOfCode.Contracts.csproj new file mode 100644 index 0000000..17b910f --- /dev/null +++ b/AdvenOfCode.Contracts/AdvenOfCode.Contracts.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/AdvenOfCode.Contracts/IPuzzleSolver.cs b/AdvenOfCode.Contracts/IPuzzleSolver.cs new file mode 100644 index 0000000..45bf322 --- /dev/null +++ b/AdvenOfCode.Contracts/IPuzzleSolver.cs @@ -0,0 +1,7 @@ +namespace AdvenOfCode.Contracts; + +public interface IPuzzleSolver +{ + public T SolvePart1(string puzzleInputPath); + public T SolvePart2(string puzzleInputPath); +} \ No newline at end of file diff --git a/AdventOfCode.sln b/AdventOfCode.sln new file mode 100644 index 0000000..1750e5c --- /dev/null +++ b/AdventOfCode.sln @@ -0,0 +1,41 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AoC_2025", "AoC_2025", "{05EBAA1C-B33B-4AFE-8635-A58D7CA509A7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AoC_2025", "AoC_2025\AoC_2025.csproj", "{4F395AE3-DABB-4546-A4D0-5A62425FE168}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AoC_2025.Tests", "AoC_2025.Tests\AoC_2025.Tests.csproj", "{197D9628-C2D1-4B33-9DB8-134F87F79904}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AoC_2025.Runner", "AoC_2025.Runner\AoC_2025.Runner.csproj", "{FAB5E28F-706A-4DE1-B5FD-54E31C3A8405}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdvenOfCode.Contracts", "AdvenOfCode.Contracts\AdvenOfCode.Contracts.csproj", "{0249A329-D5C1-4699-88BE-A668B362432E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4F395AE3-DABB-4546-A4D0-5A62425FE168}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F395AE3-DABB-4546-A4D0-5A62425FE168}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F395AE3-DABB-4546-A4D0-5A62425FE168}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F395AE3-DABB-4546-A4D0-5A62425FE168}.Release|Any CPU.Build.0 = Release|Any CPU + {197D9628-C2D1-4B33-9DB8-134F87F79904}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {197D9628-C2D1-4B33-9DB8-134F87F79904}.Debug|Any CPU.Build.0 = Debug|Any CPU + {197D9628-C2D1-4B33-9DB8-134F87F79904}.Release|Any CPU.ActiveCfg = Release|Any CPU + {197D9628-C2D1-4B33-9DB8-134F87F79904}.Release|Any CPU.Build.0 = Release|Any CPU + {FAB5E28F-706A-4DE1-B5FD-54E31C3A8405}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FAB5E28F-706A-4DE1-B5FD-54E31C3A8405}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FAB5E28F-706A-4DE1-B5FD-54E31C3A8405}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FAB5E28F-706A-4DE1-B5FD-54E31C3A8405}.Release|Any CPU.Build.0 = Release|Any CPU + {0249A329-D5C1-4699-88BE-A668B362432E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0249A329-D5C1-4699-88BE-A668B362432E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0249A329-D5C1-4699-88BE-A668B362432E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0249A329-D5C1-4699-88BE-A668B362432E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {4F395AE3-DABB-4546-A4D0-5A62425FE168} = {05EBAA1C-B33B-4AFE-8635-A58D7CA509A7} + {197D9628-C2D1-4B33-9DB8-134F87F79904} = {05EBAA1C-B33B-4AFE-8635-A58D7CA509A7} + {FAB5E28F-706A-4DE1-B5FD-54E31C3A8405} = {05EBAA1C-B33B-4AFE-8635-A58D7CA509A7} + EndGlobalSection +EndGlobal diff --git a/AoC_2025.Runner/AoC_2025.Runner.csproj b/AoC_2025.Runner/AoC_2025.Runner.csproj new file mode 100644 index 0000000..375c1ed --- /dev/null +++ b/AoC_2025.Runner/AoC_2025.Runner.csproj @@ -0,0 +1,14 @@ + + + + Exe + net9.0 + enable + enable + + + + + + + diff --git a/AoC_2025.Runner/Program.cs b/AoC_2025.Runner/Program.cs new file mode 100644 index 0000000..1d9e432 --- /dev/null +++ b/AoC_2025.Runner/Program.cs @@ -0,0 +1,28 @@ +// See https://aka.ms/new-console-template for more information + +using System.Globalization; +using AoC_2025; + +var day01 = new Day01(); +const string day01Path = @"C:\Users\p01004051\Documents\AoC-PuzzleInputs\2025\Prod\day01.txt"; +Console.WriteLine(day01.SolvePart1(day01Path)); +Console.WriteLine(day01.SolvePart2(day01Path)); +Console.WriteLine(); + +var day02 = new Day02(); +const string day02Path = @"C:\Users\p01004051\Documents\AoC-PuzzleInputs\2025\Prod\day02.txt"; +Console.WriteLine(day02.SolvePart1(day02Path)); +Console.WriteLine(day02.SolvePart2(day02Path)); +Console.WriteLine(); + +var day03 = new Day03(); +const string day03Path = @"C:\Users\p01004051\Documents\AoC-PuzzleInputs\2025\Prod\day03.txt"; +Console.WriteLine(day03.SolvePart1(day03Path)); +Console.WriteLine(day03.SolvePart2(day03Path)); +Console.WriteLine(); + +var day04 = new Day04(); +const string day04Path = @"C:\Users\p01004051\Documents\AoC-PuzzleInputs\2025\Prod\day04.txt"; +Console.WriteLine(day04.SolvePart1(day04Path)); +Console.WriteLine(day04.SolvePart2(day04Path)); +Console.WriteLine(); \ No newline at end of file diff --git a/AoC_2025.Tests/AoC_2025.Tests.csproj b/AoC_2025.Tests/AoC_2025.Tests.csproj new file mode 100644 index 0000000..19ac148 --- /dev/null +++ b/AoC_2025.Tests/AoC_2025.Tests.csproj @@ -0,0 +1,25 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + + + + + + diff --git a/AoC_2025.Tests/Day01Test.cs b/AoC_2025.Tests/Day01Test.cs new file mode 100644 index 0000000..e1b7e75 --- /dev/null +++ b/AoC_2025.Tests/Day01Test.cs @@ -0,0 +1,46 @@ +using AdvenOfCode.Contracts; + +namespace AoC_2025.Tests; + +public class Day01Test +{ + private readonly IPuzzleSolver _sut; + private const string TestInputPath = @"C:\Users\p01004051\Documents\AoC-PuzzleInputs\2025\Test\day01.txt"; + private const string ProdInputPath = @"C:\Users\p01004051\Documents\AoC-PuzzleInputs\2025\Prod\day01.txt"; + + public Day01Test() + { + _sut = new Day01(); + } + [Fact] + public void Part01_Test_equals_3() + { + var actual = _sut.SolvePart1(TestInputPath); + + Assert.Equal(3, actual); + } + + [Fact] + public void Part01_Prod_equals_1097() + { + var actual = _sut.SolvePart1(ProdInputPath); + + Assert.Equal(1097, actual); + } + + [Fact] + public void Part02_Test_equals_16() + { + var actual = _sut.SolvePart2(TestInputPath); + + Assert.Equal(16, actual); + } + + [Fact] + public void Part02_Prod_equals_7101() + { + var actual = _sut.SolvePart2(ProdInputPath); + + Assert.Equal(7101, actual); + } +} \ No newline at end of file diff --git a/AoC_2025.Tests/Day02Test.cs b/AoC_2025.Tests/Day02Test.cs new file mode 100644 index 0000000..1ce0a2b --- /dev/null +++ b/AoC_2025.Tests/Day02Test.cs @@ -0,0 +1,46 @@ +using AdvenOfCode.Contracts; + +namespace AoC_2025.Tests; + +public class Day02Test +{ + private readonly IPuzzleSolver _sut; + private const string TestInputPath = @"C:\Users\p01004051\Documents\AoC-PuzzleInputs\2025\Test\day02.txt"; + private const string ProdInputPath = @"C:\Users\p01004051\Documents\AoC-PuzzleInputs\2025\Prod\day02.txt"; + + public Day02Test() + { + _sut = new Day02(); + } + [Fact] + public void Part01_Test_equals_1227775554() + { + var actual = _sut.SolvePart1(TestInputPath); + + Assert.Equal(1227775554, actual); + } + + [Fact] + public void Part01_Prod_equals_13919717792() + { + var actual = _sut.SolvePart1(ProdInputPath); + + Assert.Equal(13919717792, actual); + } + + [Fact] + public void Part02_Test_equals_4174379265() + { + var actual = _sut.SolvePart2(TestInputPath); + + Assert.Equal(4174379265, actual); + } + + [Fact] + public void Part02_Prod_equals_14582313461() + { + var actual = _sut.SolvePart2(ProdInputPath); + + Assert.Equal(14582313461, actual); + } +} \ No newline at end of file diff --git a/AoC_2025.Tests/Day03Test.cs b/AoC_2025.Tests/Day03Test.cs new file mode 100644 index 0000000..271d923 --- /dev/null +++ b/AoC_2025.Tests/Day03Test.cs @@ -0,0 +1,46 @@ +using AdvenOfCode.Contracts; + +namespace AoC_2025.Tests; + +public class Day03Test +{ + private readonly IPuzzleSolver _sut; + private const string TestInputPath = @"C:\Users\p01004051\Documents\AoC-PuzzleInputs\2025\Test\day03.txt"; + private const string ProdInputPath = @"C:\Users\p01004051\Documents\AoC-PuzzleInputs\2025\Prod\day03.txt"; + + public Day03Test() + { + _sut = new Day03(); + } + [Fact] + public void Part01_Test_equals_357() + { + var actual = _sut.SolvePart1(TestInputPath); + + Assert.Equal(357, actual); + } + + [Fact] + public void Part01_Prod_equals_16812() + { + var actual = _sut.SolvePart1(ProdInputPath); + + Assert.Equal(16812, actual); + } + + [Fact] + public void Part02_Test_equals_3121910778619() + { + var actual = _sut.SolvePart2(TestInputPath); + + Assert.Equal(3121910778619, actual); + } + + [Fact] + public void Part02_Prod_equals_166345822896410() + { + var actual = _sut.SolvePart2(ProdInputPath); + + Assert.Equal(166345822896410, actual); + } +} \ No newline at end of file diff --git a/AoC_2025.Tests/Day04Test.cs b/AoC_2025.Tests/Day04Test.cs new file mode 100644 index 0000000..944d32b --- /dev/null +++ b/AoC_2025.Tests/Day04Test.cs @@ -0,0 +1,46 @@ +using AdvenOfCode.Contracts; + +namespace AoC_2025.Tests; + +public class Day04Test +{ + private readonly IPuzzleSolver _sut; + private const string TestInputPath = @"C:\Users\p01004051\Documents\AoC-PuzzleInputs\2025\Test\day04.txt"; + private const string ProdInputPath = @"C:\Users\p01004051\Documents\AoC-PuzzleInputs\2025\Prod\day04.txt"; + + public Day04Test() + { + _sut = new Day04(); + } + [Fact] + public void Part01_Test_equals_13() + { + var actual = _sut.SolvePart1(TestInputPath); + + Assert.Equal(13, actual); + } + + [Fact] + public void Part01_Prod_equals_1411() + { + var actual = _sut.SolvePart1(ProdInputPath); + + Assert.Equal(1411, actual); + } + + [Fact] + public void Part02_Test_equals_43() + { + var actual = _sut.SolvePart2(TestInputPath); + + Assert.Equal(43, actual); + } + + [Fact] + public void Part02_Prod_equals_8557() + { + var actual = _sut.SolvePart2(ProdInputPath); + + Assert.Equal(8557, actual); + } +} \ No newline at end of file diff --git a/AoC_2025/AoC_2025.csproj b/AoC_2025/AoC_2025.csproj new file mode 100644 index 0000000..e85e30f --- /dev/null +++ b/AoC_2025/AoC_2025.csproj @@ -0,0 +1,13 @@ + + + + net9.0 + enable + enable + + + + + + + diff --git a/AoC_2025/Day01.cs b/AoC_2025/Day01.cs new file mode 100644 index 0000000..14c9ff1 --- /dev/null +++ b/AoC_2025/Day01.cs @@ -0,0 +1,112 @@ +using System; +using System.IO; +using System.Linq; +using AdvenOfCode.Contracts; + +namespace AoC_2025; + +public class Day01 : IPuzzleSolver +{ + private string[] ParsePuzzleInput(string path) + { + var puzzleInput = File.ReadAllLines(path) + .Where(l => !string.IsNullOrWhiteSpace(l)) + .ToArray(); + return puzzleInput; + } + + public int SolvePart1(string pathToPuzzleInput) + { + var rotationInstructions = ParsePuzzleInput(pathToPuzzleInput); + var dialPosition = 50; + var timesZeroWasHit = 0; + foreach (var rotationAmount in rotationInstructions.Select(ParseRotationAmount)) + { + dialPosition = (dialPosition + rotationAmount) % 100; + if (dialPosition == 0) + timesZeroWasHit++; + } + return timesZeroWasHit; + } + + public int SolvePart2(string pathToPuzzleInput) + { + var rotationInstructions = ParsePuzzleInput(pathToPuzzleInput); + var dialPosition = 50; + var timesZeroWasHit = 0; + foreach (var rotationAmount in rotationInstructions.Select(ParseRotationAmount)) + { + var actualRotationAmount = 0; + if (rotationAmount < 0) + { + (var fullTurns, actualRotationAmount) = DivMod(rotationAmount, -100); + timesZeroWasHit += fullTurns; + if (dialPosition != 0 && dialPosition + actualRotationAmount <= 0) + timesZeroWasHit++; + } + else + { + (var fullTurns, actualRotationAmount) = DivMod(rotationAmount, 100); + timesZeroWasHit += fullTurns; + if (dialPosition + actualRotationAmount >= 100) + timesZeroWasHit++; + } + + dialPosition = Mod(dialPosition + actualRotationAmount, 100); + } + return timesZeroWasHit; + } + + private (int div, int mod) DivMod(int number, int divisor) + { + return (number / divisor, Mod(number, divisor)); + } + + private int Mod(int number, int divident) + { + var mod = number % divident; + if (number < 0 && divident > 0) + { + mod = divident + mod; + } + + return mod; + } + + private static bool HasPassedZero(bool hasOverflow, int dialPosition) + { + return dialPosition != 0 && hasOverflow; + } + + private static (int nextDialPosition, bool passedZero) GetNextDialPosition(int rotationAmount, int dialPosition) + { + var actualRotation = rotationAmount % 100; + var nextDialPosition = dialPosition + actualRotation; + var hasOverflow = nextDialPosition is > 99 or < 1; + nextDialPosition %= 100; + if (nextDialPosition < 0) + { + nextDialPosition = 100 + nextDialPosition; + } + return (nextDialPosition, hasOverflow); + } + + private static int ParseRotationAmount(string puzzleInput) + { + var direction = puzzleInput[..1]; + var rotationAmount = int.Parse(puzzleInput[1..]); + if (direction == "L") + { + rotationAmount *= -1; + } + + return rotationAmount; + } + + private static int CountFullRotations(int rotationAmount) + { + var fullRotations = Math.Abs(rotationAmount / 100); + + return fullRotations; + } +} \ No newline at end of file diff --git a/AoC_2025/Day02.cs b/AoC_2025/Day02.cs new file mode 100644 index 0000000..e08cb18 --- /dev/null +++ b/AoC_2025/Day02.cs @@ -0,0 +1,131 @@ +using AdvenOfCode.Contracts; + +namespace AoC_2025; + +public class Day02 : IPuzzleSolver +{ + private string[] ParsePuzzleInput(string path) + { + var puzzleInput = File.ReadAllText(path) + .Trim() + .Split(',', StringSplitOptions.RemoveEmptyEntries); + return puzzleInput; + } + + public long SolvePart1(string pathToPuzzleInput) + { + var idRanges = ParsePuzzleInput(pathToPuzzleInput); + var invalidIds = idRanges.SelectMany(range => FindInvalidIds(range)); + var sum = invalidIds.Sum(); + return sum; + } + + public long SolvePart2(string pathToPuzzleInput) + { + var idRanges = ParsePuzzleInput(pathToPuzzleInput); + var invalidIds = idRanges.SelectMany(range => FindInvalidSegmentedIds(range)); + var sum = invalidIds.Sum(); + return sum; + } + + private IEnumerable FindInvalidIds(string range) + { + var (start, end) = Parse(range); + var halvableIds = FindHalvableIds(start, end); + var invalidIds = GetInvalidIds(halvableIds); + return invalidIds; + } + + private IEnumerable FindInvalidSegmentedIds(string range) + { + var (start, end) = Parse(range); + var idsAsAllSegments = GetIdAsAllSegments(start, end); + var invalidIdSegments = FindIdsWhereAllSegmentsAreEqual(idsAsAllSegments); + var invalidIds = MergeSegments(invalidIdSegments); + var invalidIdNumbers = invalidIds.Select(id => long.Parse(id)); + return invalidIdNumbers; + } + + private (long start, long end) Parse(string range) + { + var parts = range.Split('-', StringSplitOptions.RemoveEmptyEntries); + return (long.Parse(parts[0]), long.Parse(parts[1])); + } + + private IEnumerable FindHalvableIds(long start, long end) + { + var ids = Range(start, end); + var idStrings = ids.Select(id => id.ToString()); + var halvableIds = idStrings.Where(id => (id.Length % 2) == 0); + return halvableIds; + } + + private IEnumerable GetInvalidIds(IEnumerable halvableIds) + { + var idParts = halvableIds.Select(id => (id[..(id.Length / 2)], id[(id.Length / 2)..])); + var invalidIds = idParts.Where(parts=> parts.Item1 == parts.Item2); + var invalidNumberedIds = invalidIds.Select(id => long.Parse(id.Item1 + id.Item2)); + return invalidNumberedIds; + } + + private IEnumerable Range(long start, long end) + { + for (var i = start; i <= end; i++) + { + yield return i; + } + } + + private IEnumerable>> GetIdAsAllSegments(long start, long end) + { + var ids = Range(start, end); + var idStrings = ids.Select(id => id.ToString()); + var idSegments = idStrings.Select(id => GetSegments(id)); + return idSegments; + } + + private IEnumerable> GetSegments(string id) + { + for (int i = id.Length / 2; i > 0; i--) + { + if (id.Length % i == 0) + { + yield return id.Chunk(i).Select(chunks => new string(chunks)); + } + } + } + + private IEnumerable> FindIdsWhereAllSegmentsAreEqual(IEnumerable>> idAllSegments) + { + var segmentsWithOneAllMatching = FilterForAnyAllSegmentsEqual(idAllSegments); + var idSegments = GetFirstForEachSegments(segmentsWithOneAllMatching); + return idSegments; + } + + private static IEnumerable> GetFirstForEachSegments(IEnumerable>> segmentsWithOneAllMatching) + { + var idSegments = segmentsWithOneAllMatching + .Select(segments => segments.First()); + return idSegments; + } + + private IEnumerable MergeSegments(IEnumerable> segments) + { + return segments.Select(segment => string.Concat(segment)); + } + + private IEnumerable>> FilterForAnyAllSegmentsEqual(IEnumerable>> idAllSegments) + { + var segmentsWithOneAllMatching = idAllSegments + .Where(idSegments => idSegments.Any(segments => CheckAllSegmentsEqual(segments))); + return segmentsWithOneAllMatching; + } + + private bool CheckAllSegmentsEqual(IEnumerable segments) + { + var (allEqual, _) = segments.Aggregate( + (allEqual: true, prev: ""), + (tuple, segment) => (tuple.allEqual && (tuple.prev == "" || tuple.prev == segment), segment)); + return allEqual; + } +} \ No newline at end of file diff --git a/AoC_2025/Day03.cs b/AoC_2025/Day03.cs new file mode 100644 index 0000000..e62af4c --- /dev/null +++ b/AoC_2025/Day03.cs @@ -0,0 +1,88 @@ +using System; +using System.IO; +using System.Linq; +using AdvenOfCode.Contracts; + +namespace AoC_2025; + +public class Day03 : IPuzzleSolver +{ + private string[] ParsePuzzleInput(string path) + { + var puzzleInput = File.ReadAllLines(path) + .Where(line => !string.IsNullOrWhiteSpace(line)) + .ToArray(); + return puzzleInput; + } + + public long SolvePart1(string pathToPuzzleInput) + { + var batterieBankStrings = ParsePuzzleInput(pathToPuzzleInput); + var batterieBanks = batterieBankStrings.Select(bbs => ParseBatterieBank(bbs)); + var voltages = batterieBanks.Select(bb => FindMaxVoltage(bb)); + var sumVoltages = voltages.Sum(); + return sumVoltages; + } + + public long SolvePart2(string pathToPuzzleInput) + { + const int voltageLength = 12; + var batterieBankStrings = ParsePuzzleInput(pathToPuzzleInput); + var batterieBanks = batterieBankStrings.Select(bbs => ParseBatterieBank(bbs)); + var voltages = batterieBanks.Select(bb => GetMaxSuperVoltage(bb, voltageLength)); + var sumVoltages = voltages.Sum(); + return sumVoltages; + } + + private int[] ParseBatterieBank(string batterieBank) + { + return batterieBank + .Select(c => new string(c, 1)) + .Select(value => int.Parse(value)) + .ToArray(); + } + + private long FindMaxVoltage(Span batterieBank) + { + var (firstNumber, position) = GetMax(batterieBank[..^1]); + var (secondNumber, _) = GetMax(batterieBank[(position + 1)..]); + var voltage = Combine(firstNumber, secondNumber); + return voltage; + } + + private long Combine(long first, long second) + { + return (first * 10) + second; + } + + private long GetMaxSuperVoltage(Span batterieBank, int voltageLength) + { + var indexedVoltageLength = voltageLength - 1; + var currentBankPosition = 0; + var superVoltage = 0L; + for (var i = 0; i < voltageLength; i++) + { + var (max, numberPosition) = GetMax(batterieBank[currentBankPosition..^(indexedVoltageLength - i)]); + superVoltage = Combine(superVoltage, max); + currentBankPosition += numberPosition + 1; + } + + return superVoltage; + } + + private (int max, int currentPosition) GetMax(Span batterieBank) + { + var max = batterieBank[0]; + var currentPosition = 0; + for (var i = 1; i < batterieBank.Length; i++) + { + if (max < batterieBank[i]) + { + max = batterieBank[i]; + currentPosition = i; + } + } + + return (max, currentPosition); + } +} \ No newline at end of file diff --git a/AoC_2025/Day04.cs b/AoC_2025/Day04.cs new file mode 100644 index 0000000..b276e44 --- /dev/null +++ b/AoC_2025/Day04.cs @@ -0,0 +1,102 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using AdvenOfCode.Contracts; +using Coordinate = (int x, int y); + +namespace AoC_2025; + +public class Day04 : IPuzzleSolver +{ + private string[] ParsePuzzleInput(string path) + { + var puzzleInput = File.ReadAllLines(path) + .Where(line => !string.IsNullOrWhiteSpace(line)) + .Select(line => line.Trim()) + .ToArray(); + return puzzleInput; + } + + public long SolvePart1(string pathToPuzzleInput) + { + var paperrollGrid = ParsePuzzleInput(pathToPuzzleInput); + var accessiblePaperrolls = GetAccessiblePaperrolls(paperrollGrid); + var countAccessibleRolls = accessiblePaperrolls.Count(); + return countAccessibleRolls; + } + + public long SolvePart2(string pathToPuzzleInput) + { + var paperrollGrid = ParsePuzzleInput(pathToPuzzleInput); + var rollCoordinates = GetRollCoordinates(paperrollGrid); + var countAccessibleRolls = RecursiveCountAllRemovableRolls(rollCoordinates); + return countAccessibleRolls; + } + + private IEnumerable GetAccessiblePaperrolls(IEnumerable paperrollGrid) + { + var rollCoordinates = GetRollCoordinates(paperrollGrid); + var rollsWithNeighbours = GetNeighbours(rollCoordinates); + var accessiblePaperrolls = FilterForLessThan(rollsWithNeighbours, 4); + return accessiblePaperrolls; + } + + private int RecursiveCountAllRemovableRolls(HashSet coordinates) + { + var rollsWithNeighbours = GetNeighbours(coordinates); + var accessiblePaperrolls = FilterForLessThan(rollsWithNeighbours, 4).ToArray(); + var countAccessibleRolls = accessiblePaperrolls.Length; + coordinates.ExceptWith(accessiblePaperrolls); + return countAccessibleRolls == 0 + ? 0 + : countAccessibleRolls + RecursiveCountAllRemovableRolls(coordinates); + } + + private HashSet GetRollCoordinates(IEnumerable paperrollGrid) + { + var coordinates = paperrollGrid + .SelectMany((gridLine, xIndex) => GetCoordinates(gridLine, xIndex)) + .ToHashSet(); + return coordinates; + } + + private IEnumerable GetCoordinates(string gridLine, int xIndex) + { + var indexedCharacters = gridLine.Select((character, yIndex) => (character, yIndex)); + var validRolls = indexedCharacters.Where(tuple => tuple.character == '@'); + var rollCoordinates = validRolls.Select(tuple => (xIndex, tuple.yIndex)); + return rollCoordinates; + } + + private IEnumerable<(Coordinate coordinate, Coordinate[] neighbours)> GetNeighbours(HashSet rollCoordinates) + { + var coordsWithNeighbourCount = rollCoordinates + .Select(coord => (coord, GetNeighbours(coord, rollCoordinates))); + return coordsWithNeighbourCount; + } + + private Coordinate[] GetNeighbours(Coordinate coordinate, HashSet coordinates) + { + Coordinate[] neighbourDirections = + [ + (-1, -1), (-1, 0), (-1, 1), + (0, -1), (0, 1), + (1, -1), (1, 0), (1, 1), + ]; + var possibleNeighbours = neighbourDirections + .Select(direction => (coordinate.x + direction.x, coordinate.y + direction.y)); + var actualNeighbours = possibleNeighbours + .Where(coord => coordinates.Contains(coord)) + .ToArray(); + return actualNeighbours; + } + + private IEnumerable FilterForLessThan(IEnumerable<(Coordinate coordinate, Coordinate[] neighbours)> coordinatesWithNeighbours, + int lessThan) + { + var coordinatesWithLess = coordinatesWithNeighbours + .Where(entry => entry.neighbours.Count() < lessThan) + .Select(entry => entry.coordinate); + return coordinatesWithLess; + } +} \ No newline at end of file