Skip to content

Commit

Permalink
Day 21 Solutions
Browse files Browse the repository at this point in the history
  • Loading branch information
byte2pixel committed Dec 24, 2024
1 parent d1552e5 commit b3a619f
Show file tree
Hide file tree
Showing 12 changed files with 317 additions and 0 deletions.
52 changes: 52 additions & 0 deletions 2024/Advent.Tests/Commands/Day21CommandTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Advent.Common.Settings;
using Spectre.Console.Cli;

namespace Advent.Tests.Commands;

public class Day21CommandTests
{
private readonly List<string> _arguments = [];
private readonly IRemainingArguments _remaining = Substitute.For<IRemainingArguments>();
private readonly TestConsole console = new();

public Day21CommandTests()
{
console.Profile.Capabilities.Interactive = true;
}

[Fact]
public async Task Day21Command_Solves_Part1_Correctly()
{
var mockReader = Substitute.For<IFileReader>();
mockReader
.ReadInputAsync(Arg.Any<string>())
.Returns(Task.FromResult(TestData.Day21TestData));

var command = new Day21Command(mockReader, console);
var result = await command.ExecuteAsync(
new CommandContext(_arguments, _remaining, "day21", null),
new AdventSettings { Part = "Part 1" }
);
result.Should().Be(0);
console.Output.Should().Contain("Day 21 Part 1");
console.Output.Should().Contain("The answer is 126384");
}

[Fact]
public async Task Day21Command_Solves_Part2_Correctly()
{
var mockReader = Substitute.For<IFileReader>();
mockReader
.ReadInputAsync(Arg.Any<string>())
.Returns(Task.FromResult(TestData.Day21TestData));

var command = new Day21Command(mockReader, console);
var result = await command.ExecuteAsync(
new CommandContext(_arguments, _remaining, "day21", null),
new AdventSettings { Part = "Part 2" }
);
result.Should().Be(0);
console.Output.Should().Contain("Day 21 Part 2");
console.Output.Should().Contain("The answer is 154115708116294");
}
}
2 changes: 2 additions & 0 deletions 2024/Advent.Tests/Common/TestData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -328,4 +328,6 @@ internal static class TestData
+ "#.#.#.#.#.#.###\n"
+ "#...#...#...###\n"
+ "###############\n";

public const string Day21TestData = "029A\n" + "980A\n" + "179A\n" + "456A\n" + "379A\n";
}
48 changes: 48 additions & 0 deletions 2024/Advent.Tests/IntegrationTests/CommandAppTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -967,6 +967,54 @@ public async Task Day20Part2_IntegrationTest_Success()
result.Output.Should().Contain("The answer is 993178");
}
#endregion

#region Day21
[Fact]
public async Task Day21Part1_IntegrationTest_Success()
{
// Arrange
var args = new string[] { "day21", "--part", "Part 1" };
var app = new CommandAppTester(_registrar);

app.Configure(config =>
{
config.PropagateExceptions();
config.ConfigureConsole(_console);
config.AddCommand<Day21Command>("day21");
});

// Act
var result = await app.RunAsync(args);

// Assert
result.ExitCode.Should().Be(0);
result.Output.Should().Contain("Day 21 Part 1");
result.Output.Should().Contain("The answer is 176870");
}

[Fact]
public async Task Day21Part2_IntegrationTest_Success()
{
// Arrange
var args = new string[] { "day21", "--part", "Part 2" };
var app = new CommandAppTester(_registrar);

app.Configure(config =>
{
config.PropagateExceptions();
config.ConfigureConsole(_console);
config.AddCommand<Day21Command>("day21");
});

// Act
var result = await app.RunAsync(args);

// Assert
result.ExitCode.Should().Be(0);
result.Output.Should().Contain("Day 21 Part 2");
result.Output.Should().Contain("The answer is 223902935165512");
}
#endregion
}

public class TestFixture
Expand Down
18 changes: 18 additions & 0 deletions 2024/Advent.Tests/UseCases/Day21/Day21ParserTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Advent.UseCases.Day21;

namespace Advent.Tests.UseCases.Day21;

public class Day21ParserTests
{
[Fact]
public void Parse_Returns_Correct_Data()
{
var input = TestData.Day21TestData;
var result = Day21Parser.Parse(input);
result.Should().HaveCount(5);
result.Should().Contain("029A");
result.Should().Contain("980A");
result.Should().Contain("179A");
result.Should().Contain("379A");
}
}
34 changes: 34 additions & 0 deletions 2024/Advent.Tests/UseCases/Day21/Day21Part1SolverTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using Advent.UseCases.Day21;

namespace Advent.Tests.UseCases.Day21;

public class Day21Part1SolverTests
{
[Fact]
public void Solve_ShouldReturnCorrectCost_ForDepth2()
{
// Arrange

var solver = new Day21Part1Solver(2);

// Act
var solution = solver.Solve(["029A", "980A", "179A", "456A", "379A"]);

// Assert
solution.Should().Be("126384");
}

[Fact]
public void Solve_ShouldReturnCorrectCost_ForDepth25()
{
// Arrange

var solver = new Day21Part1Solver(25);

// Act
var solution = solver.Solve(["029A", "980A", "179A", "456A", "379A"]);

// Assert
solution.Should().Be("154115708116294");
}
}
31 changes: 31 additions & 0 deletions 2024/Advent/Commands/Day21Command.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Advent.Common;
using Advent.Common.Commands;
using Advent.Common.Settings;
using Advent.UseCases.Day21;
using Spectre.Console;
using Spectre.Console.Cli;

namespace Advent.Commands;

public class Day21Command(IFileReader reader, IAnsiConsole console)
: AdventCommand<AdventSettings>(reader, console)
{
public override async Task<int> ExecuteAsync(CommandContext context, AdventSettings settings)
{
var input = await _reader.ReadInputAsync("../input/day21input.txt");
var data = Day21Parser.Parse(input);

var choice = settings.Part ?? PromptForPartChoice();
IDay21Solver solver = choice switch
{
"Part 1" => new Day21Part1Solver(2),
"Part 2" => new Day21Part1Solver(25),
_ => throw new InvalidOperationException("Invalid choice")
};

var result = solver.Solve(data);
_console.MarkupLine($"[bold green]Day 21 {choice} [/]");
_console.MarkupLine($"The answer is [bold yellow]{result}[/]");
return 0;
}
}
1 change: 1 addition & 0 deletions 2024/Advent/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
config.AddCommand<Day18Command>("day18").WithDescription("Advent of Code 2024 Day 18");
config.AddCommand<Day19Command>("day19").WithDescription("Advent of Code 2024 Day 19");
config.AddCommand<Day20Command>("day20").WithDescription("Advent of Code 2024 Day 20");
config.AddCommand<Day21Command>("day21").WithDescription("Advent of Code 2024 Day 21");
});

return await app.RunAsync(args);
9 changes: 9 additions & 0 deletions 2024/Advent/UseCases/Day21/Day21Parser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Advent.UseCases.Day21;

public static class Day21Parser
{
public static string[] Parse(string input)
{
return input.Split("\n", StringSplitOptions.RemoveEmptyEntries);
}
}
101 changes: 101 additions & 0 deletions 2024/Advent/UseCases/Day21/Day21Part1Solver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using System.Diagnostics;
using Advent.Common;
using Spectre.Console;
using Cache = System.Collections.Generic.Dictionary<
(char currentKey, char nextKey, int depth),
long
>;
using Keypad = System.Collections.Generic.Dictionary<Advent.Common.GridCell, char>;

namespace Advent.UseCases.Day21;

public class Day21Part1Solver(int depth) : IDay21Solver
{
private readonly int _depth = depth;

public string Solve(string[] input)
{
Stopwatch sw = new();
sw.Start();
var numPad = CreateKeypads(["789", "456", "123", " 0A"]);
var dirPad = CreateKeypads([" ^A", "<v>"]);
var keypads = Enumerable.Repeat(dirPad, _depth).Prepend(numPad).ToArray();
var cache = new Cache();
long result = 0L;

foreach (var line in input)
{
var mutiplier = int.Parse(line[..^1]);
var cost = ProcessKeys(line, keypads, cache);
result += mutiplier * cost;
}
sw.Stop();
AnsiConsole.WriteLine($"Elapsed time: {sw.Elapsed.TotalMilliseconds} ms");
return result.ToString();
}

private long ProcessKeys(string line, Keypad[] keypads, Cache cache)
{
if (keypads.Length == 0)
{
return line.Length;
}
else
{
var currentKey = 'A';
var cost = 0L;

foreach (var key in line)
{
cost += GetCost(currentKey, key, keypads, cache);
currentKey = key;
}
return cost;
}
}

private long GetCost(char currentKey, char nextKey, Keypad[] keypads, Cache cache)
{
if (cache.TryGetValue((currentKey, nextKey, keypads.Length), out var cacheCost))
{
return cacheCost;
}
var keypad = keypads[0];

var currentKeyPosition = keypad.Single(kvp => kvp.Value == currentKey).Key;
var nextKeyPosition = keypad.Single(kvp => kvp.Value == nextKey).Key;

var dRow = nextKeyPosition.Row - currentKeyPosition.Row;
var vert = new string(dRow < 0 ? 'v' : '^', Math.Abs(dRow));
var dCol = nextKeyPosition.Column - currentKeyPosition.Column;
var horiz = new string(dCol < 0 ? '<' : '>', Math.Abs(dCol));

var cost = long.MaxValue;

if (keypad[new GridCell(currentKeyPosition.Row, nextKeyPosition.Column)] != ' ')
{
cost = Math.Min(cost, ProcessKeys($"{horiz}{vert}A", keypads[1..], cache));
}
if (keypad[new GridCell(nextKeyPosition.Row, currentKeyPosition.Column)] != ' ')
{
cost = Math.Min(cost, ProcessKeys($"{vert}{horiz}A", keypads[1..], cache));
}
cache[(currentKey, nextKey, keypads.Length)] = cost;
return cost;
}

private static Keypad CreateKeypads(string[] keys)
{
List<KeyValuePair<GridCell, char>> keyValues = [];
foreach (var row in Enumerable.Range(0, keys.Length))
{
foreach (var col in Enumerable.Range(0, keys[0].Length))
{
keyValues.Add(
new KeyValuePair<GridCell, char>(new GridCell(-row, col), keys[row][col])
);
}
}
return keyValues.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}
}
6 changes: 6 additions & 0 deletions 2024/Advent/UseCases/Day21/IDay21Solver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Advent.UseCases.Day21;

public interface IDay21Solver
{
public string Solve(string[] input);
}
10 changes: 10 additions & 0 deletions 2024/Advent/UseCases/Day21/IRobotControl.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Advent.Common;

namespace Advent.UseCases.Day21;

public interface IRobotControl
{
public char Move(Direction direction);
public char CurrentKey { get; }
public List<Direction> AvailableMoves(GridCell newPosition);
}
5 changes: 5 additions & 0 deletions 2024/input/day21input.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
540A
582A
169A
593A
579A

0 comments on commit b3a619f

Please sign in to comment.