From abfa9e50ba5723dc971f2429658b1b1937bc778e Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Sat, 14 Dec 2024 12:58:28 -0600 Subject: [PATCH] feat: solved day 13 part 1 and part 2 --- 13/ClawContraption.Tests/MachineTests.cs | 180 +++++++++++++++++++++++ 13/ClawContraption/Button.cs | 3 + 13/ClawContraption/Machine.cs | 111 ++++++++++++++ 13/ClawContraption/PrizeLocation.cs | 3 + 13/ClawContraption/Program.cs | 17 ++- README.md | 1 + 6 files changed, 312 insertions(+), 3 deletions(-) create mode 100644 13/ClawContraption.Tests/MachineTests.cs create mode 100644 13/ClawContraption/Button.cs create mode 100644 13/ClawContraption/Machine.cs create mode 100644 13/ClawContraption/PrizeLocation.cs diff --git a/13/ClawContraption.Tests/MachineTests.cs b/13/ClawContraption.Tests/MachineTests.cs new file mode 100644 index 0000000..b38089d --- /dev/null +++ b/13/ClawContraption.Tests/MachineTests.cs @@ -0,0 +1,180 @@ +namespace ClawContraption.Tests; + +public class MachineTests +{ + private async Task> GetPuzzleInput() + { + var input = await File.ReadAllTextAsync(Path.Combine(AppContext.BaseDirectory, "INPUT.txt")); + + return input + .Split($"{Environment.NewLine}{Environment.NewLine}") + .Select(s => s.Split(Environment.NewLine)) + .ToList(); + } + + [Test] + public async Task SolutionPartOne_WhenGivenPuzzleInput_ItShouldReturnExpectedResult() + { + var input = await GetPuzzleInput(); + var machines = input.Select(s => Machine.From(s, 100)); + var result = machines.Sum(m => m.TokenCost); + + await Assert.That(result).IsEqualTo(28262); + } + + [Test] + public async Task SolutionPartTwo_WhenGivenPuzzleInput_ItShouldReturnExpectedResult() + { + var input = await GetPuzzleInput(); + var machines = input.Select(s => Machine.From(s, maxPresses: null, 10_000_000_000_000)); + var result = machines.Sum(m => m.TokenCost); + + await Assert.That(result).IsEqualTo(101406661266314); + } + + [Test] + [MethodDataSource(nameof(FromTestCases))] + public async Task From_WhenCalledWithInput_ItShouldReturnExpectedMachine(FromTestCase testCase) + { + var expectedMachine = Machine.From( + buttonA: new(testCase.ExpectedButtonAValues.X, testCase.ExpectedButtonAValues.Y), + buttonB: new(testCase.ExpectedButtonBValues.X, testCase.ExpectedButtonBValues.Y), + prizeLocation: new( + testCase.ExpectedPrizeValues.X + testCase.PrizeLocationOffset, + testCase.ExpectedPrizeValues.Y + testCase.PrizeLocationOffset + ), + maxPresses: testCase.MaxPresses + ); + + var machine = Machine.From(testCase.MachineInput, testCase.MaxPresses, testCase.PrizeLocationOffset); + + await Assert.That(machine).IsEquivalentTo(expectedMachine); + await Assert.That(machine.TokenCost).IsEqualTo(testCase.ExpectedTokenCost); + } + + public static IEnumerable> FromTestCases() + { + yield return () => new( + [ + "Button A: X+94, Y+34", + "Button B: X+22, Y+67", + "Prize: X=8400, Y=5400", + ], + 0, + 100, + (94, 34), + (22, 67), + (8400, 5400), + 280 + ); + + yield return () => new( + [ + "Button A: X+26, Y+66", + "Button B: X+67, Y+21", + "Prize: X=12748, Y=12176", + ], + 0, + 100, + (26, 66), + (67, 21), + (12748, 12176), + 0 + ); + + yield return () => new( + [ + "Button A: X+17, Y+86", + "Button B: X+84, Y+37", + "Prize: X=7870, Y=6450", + ], + 0, + 100, + (17, 86), + (84, 37), + (7870, 6450), + 200 + ); + + yield return () => new( + [ + "Button A: X+69, Y+23", + "Button B: X+27, Y+71", + "Prize: X=18641, Y=10279", + ], + 0, + 100, + (69, 23), + (27, 71), + (18641, 10279), + 0 + ); + + // part 2 cases + yield return () => new( + [ + "Button A: X+94, Y+34", + "Button B: X+22, Y+67", + "Prize: X=8400, Y=5400", + ], + 10_000_000_000_000, + null, + (94, 34), + (22, 67), + (8400, 5400), + 0 + ); + + yield return () => new( + [ + "Button A: X+26, Y+66", + "Button B: X+67, Y+21", + "Prize: X=12748, Y=12176", + ], + 10_000_000_000_000, + null, + (26, 66), + (67, 21), + (12748, 12176), + 459236326669 + ); + + yield return () => new( + [ + "Button A: X+17, Y+86", + "Button B: X+84, Y+37", + "Prize: X=7870, Y=6450", + ], + 10_000_000_000_000, + null, + (17, 86), + (84, 37), + (7870, 6450), + 0 + ); + + yield return () => new( + [ + "Button A: X+69, Y+23", + "Button B: X+27, Y+71", + "Prize: X=18641, Y=10279", + ], + 10_000_000_000_000, + null, + (69, 23), + (27, 71), + (18641, 10279), + 416082282239 + ); + } + + public record FromTestCase( + string[] MachineInput, + long PrizeLocationOffset, + int? MaxPresses, + (int X, int Y) ExpectedButtonAValues, + (int X, int Y) ExpectedButtonBValues, + (int X, int Y) ExpectedPrizeValues, + decimal ExpectedTokenCost + ); +} \ No newline at end of file diff --git a/13/ClawContraption/Button.cs b/13/ClawContraption/Button.cs new file mode 100644 index 0000000..b6304c8 --- /dev/null +++ b/13/ClawContraption/Button.cs @@ -0,0 +1,3 @@ +namespace ClawContraption; + +record Button(decimal XMovement, decimal YMovement); \ No newline at end of file diff --git a/13/ClawContraption/Machine.cs b/13/ClawContraption/Machine.cs new file mode 100644 index 0000000..0b10100 --- /dev/null +++ b/13/ClawContraption/Machine.cs @@ -0,0 +1,111 @@ +using System.Text.RegularExpressions; + +namespace ClawContraption; + +partial class Machine +{ + private readonly int? _maxPresses; + private readonly Button _buttonA; + private readonly Button _buttonB; + private readonly PrizeLocation _prizeLocation; + private decimal NumberOfButtonBPresses => + ((_prizeLocation.Y * _buttonA.XMovement) - (_prizeLocation.X * _buttonA.YMovement)) / ((_buttonB.YMovement * _buttonA.XMovement) - (_buttonB.XMovement * _buttonA.YMovement)); + + private decimal NumberOfButtonAPresses => + (_prizeLocation.X - (_buttonB.XMovement * NumberOfButtonBPresses)) / _buttonA.XMovement; + + private bool HasSolution => CheckForSolution(); + + public decimal TokenCost => HasSolution ? (NumberOfButtonBPresses * 1) + (NumberOfButtonAPresses * 3) : 0; + + private Machine(Button buttonA, Button buttonB, PrizeLocation prizeLocation, int? maxPresses = null) + { + _buttonA = buttonA; + _buttonB = buttonB; + _prizeLocation = prizeLocation; + _maxPresses = maxPresses; + } + + internal static Machine From(Button buttonA, Button buttonB, PrizeLocation prizeLocation, int? maxPresses) => + new(buttonA, buttonB, prizeLocation, maxPresses); + + public static Machine From(string[] input, int? maxPresses = null, long prizeLocationOffset = 0) + { + if (input.Length is not 3) + { + throw new ApplicationException("Input describing machine is not valid"); + } + + var buttonAInput = input[0]; + var buttonBInput = input[1]; + var prizeInput = input[2]; + + var buttonAMatches = ButtonARegex().Match(buttonAInput); + var buttonBMatches = ButtonBRegex().Match(buttonBInput); + var prizeMatches = PrizeRegex().Match(prizeInput); + + if (buttonAMatches.Groups.Count is not 3) + { + throw new ApplicationException("input for button A missing values"); + } + + if (buttonBMatches.Groups.Count is not 3) + { + throw new ApplicationException("input for button B missing values"); + } + + if (prizeMatches.Groups.Count is not 3) + { + throw new ApplicationException("input for prize missing values"); + } + + var buttonA = new Button( + int.Parse(buttonAMatches.Groups[1].Value), + int.Parse(buttonAMatches.Groups[2].Value) + ); + + var buttonB = new Button( + int.Parse(buttonBMatches.Groups[1].Value), + int.Parse(buttonBMatches.Groups[2].Value) + ); + + var prize = new PrizeLocation( + int.Parse(prizeMatches.Groups[1].Value) + prizeLocationOffset, + int.Parse(prizeMatches.Groups[2].Value) + prizeLocationOffset + ); + + return new(buttonA, buttonB, prize, maxPresses); + } + + private bool CheckForSolution() + { + if (Math.Floor(NumberOfButtonAPresses) != NumberOfButtonAPresses) + { + return false; + } + + if (Math.Floor(NumberOfButtonBPresses) != NumberOfButtonBPresses) + { + return false; + } + + if (_maxPresses.HasValue && NumberOfButtonAPresses > _maxPresses) + { + return false; + } + + if (_maxPresses.HasValue && NumberOfButtonBPresses > _maxPresses) + { + return false; + } + + return true; + } + + [GeneratedRegex(@"Button A: X\+(\d+), Y\+(\d+)")] + private static partial Regex ButtonARegex(); + [GeneratedRegex(@"Button B: X\+(\d+), Y\+(\d+)")] + private static partial Regex ButtonBRegex(); + [GeneratedRegex(@"Prize: X=(\d+), Y=(\d+)")] + private static partial Regex PrizeRegex(); +} \ No newline at end of file diff --git a/13/ClawContraption/PrizeLocation.cs b/13/ClawContraption/PrizeLocation.cs new file mode 100644 index 0000000..56bfc43 --- /dev/null +++ b/13/ClawContraption/PrizeLocation.cs @@ -0,0 +1,3 @@ +namespace ClawContraption; + +record PrizeLocation(decimal X, decimal Y); \ No newline at end of file diff --git a/13/ClawContraption/Program.cs b/13/ClawContraption/Program.cs index 5f94bf8..0366ff0 100644 --- a/13/ClawContraption/Program.cs +++ b/13/ClawContraption/Program.cs @@ -1,5 +1,7 @@ using System.Diagnostics; +using ClawContraption; + if (args.Length is 0) { Console.WriteLine("Please provide a path to the input file."); @@ -13,12 +15,21 @@ if (File.Exists(args[0]) is false) } var isPart2 = args.Length is 2 && args[1] is "part2"; -var input = await File.ReadAllLinesAsync(args[0]); +var input = await File.ReadAllTextAsync(args[0]); var stopwatch = new Stopwatch(); stopwatch.Start(); -// TODO: Implement solution +var offset = isPart2 ? 10_000_000_000_000 : 0; +int? maxPresses = isPart2 ? null : 100; + +var machines = input + .Split($"{Environment.NewLine}{Environment.NewLine}") + .Select(s => s.Split(Environment.NewLine)) + .Select(s => Machine.From(s, maxPresses, offset)) + .ToList(); + +var result = machines.Sum(m => m.TokenCost); stopwatch.Stop(); -Console.WriteLine($". ({stopwatch.ElapsedMilliseconds}ms)"); \ No newline at end of file +Console.WriteLine($"The fewest tokens to when all possible games is {result}. ({stopwatch.ElapsedMilliseconds}ms)"); \ No newline at end of file diff --git a/README.md b/README.md index fdd41b0..e823cb6 100644 --- a/README.md +++ b/README.md @@ -56,3 +56,4 @@ dotnet build | 10 | [Problem](./10/PROBLEM.md) | [Solution](./10/HoofIt/) | I recognized the solution for this based on pattern from last year. Solved using BFS. | | 11 | [Problem](./11/PROBLEM.md) | [Solution](./11/PlutonianPebbles/) | Dictionary > List | | 12 | [Problem](./12/PROBLEM.md) | [Solution](./12/GardenGroups/) | CORNERS == SIDES | +| 13 | [Problem](./13/PROBLEM.md) | [Solution](./13/ClawContraption/) | Yay for algebra |