Skip to content

Commit

Permalink
day(17): Clumsy Crucible 🎄
Browse files Browse the repository at this point in the history
  • Loading branch information
adamrodger committed Dec 17, 2023
1 parent f948e61 commit 81dd549
Show file tree
Hide file tree
Showing 4 changed files with 249 additions and 3 deletions.
160 changes: 160 additions & 0 deletions src/AdventOfCode/Day17.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
using System;
using System.Collections.Generic;
using AdventOfCode.Utilities;

namespace AdventOfCode
{
/// <summary>
/// Solver for Day 17
/// </summary>
public class Day17
{
public int Part1(string[] input)
{
LavaGrid grid = LavaGrid.Parse(input);
return grid.ShortestPath(0, 3);
}

public int Part2(string[] input)
{
LavaGrid grid = LavaGrid.Parse(input);
return grid.ShortestPath(4, 10);
}

/// <summary>
/// Lava grid
/// </summary>
/// <param name="Grid">Grid of tile costs</param>
/// <param name="Width">Grid width</param>
/// <param name="Height">Grid height</param>
private record LavaGrid(int[,] Grid, int Width, int Height)
{
/// <summary>
/// Parse the grid
/// </summary>
/// <param name="input">Input</param>
/// <returns>Lava grid</returns>
public static LavaGrid Parse(IReadOnlyList<string> input)
{
int[,] grid = input.ToGrid<int>();

return new LavaGrid(grid, input[0].Length, input.Count);
}

/// <summary>
/// Calculate the shortest valid path from the top left of the grid to the bottom right
/// within the given constraints
/// </summary>
/// <param name="minimumMoves">Minimum moves before we're allowed to make a turn</param>
/// <param name="maximumMoves">Maximum moves before we must make a turn</param>
/// <returns>Shortest valid path</returns>
/// <exception cref="InvalidOperationException">No path found</exception>
public int ShortestPath(int minimumMoves, int maximumMoves)
{
StepState startSouth = new((0, 1), Bearing.South, 1);
StepState startEast = new((1, 0), Bearing.East, 1);
Point2D target = (this.Width - 1, this.Height - 1);

Dictionary<StepState, int> distances = new()
{
[startSouth] = this.Grid[1, 0],
[startEast] = this.Grid[0, 1],
};

PriorityQueue<StepState, int> queue = new();
queue.Enqueue(startSouth, this.Grid[1, 0]);
queue.Enqueue(startEast, this.Grid[0, 1]);

HashSet<StepState> visited = new();

while (queue.Count > 0)
{
StepState current = queue.Dequeue();
visited.Add(current);

if (current.Point == target)
{
// guaranteed to be shortest because the queue is ordered by cost
return distances[current];
}

foreach (StepState next in NextMoves(current, minimumMoves, maximumMoves))
{
if (!this.InBounds(next.Point))
{
continue;
}

if (visited.Contains(next))
{
continue;
}

int nextDistance = distances[current] + this.Grid[next.Point.Y, next.Point.X];

// only move if it's cheaper than the current best path
if (!distances.TryGetValue(next, out int currentBest) || nextDistance < currentBest)
{
distances[next] = nextDistance;
queue.Enqueue(next, nextDistance);
}
}
}

throw new InvalidOperationException("No path found");
}

/// <summary>
/// Enumerable the possible next moves from the current state
/// </summary>
/// <param name="state">Current state</param>
/// <param name="minimumMoves">Minimum moves before we're allowed to make a turn</param>
/// <param name="maximumMoves">Maximum moves before we must make a turn</param>
/// <returns>Valid next moves</returns>
private static IEnumerable<StepState> NextMoves(StepState state, int minimumMoves, int maximumMoves)
{
if (state.Consecutive < minimumMoves)
{
yield return new StepState(state.Point.Move(state.Bearing), state.Bearing, state.Consecutive + 1);

// not met minimum yet, so can't make any further moves
yield break;
}

if (state.Consecutive < maximumMoves)
{
yield return new StepState(state.Point.Move(state.Bearing), state.Bearing, state.Consecutive + 1);
}

switch (state.Bearing)
{
case Bearing.South or Bearing.North:
yield return new StepState(state.Point.Move(Bearing.West), Bearing.West, 1);
yield return new StepState(state.Point.Move(Bearing.East), Bearing.East, 1);
break;

case Bearing.East or Bearing.West:
yield return new StepState(state.Point.Move(Bearing.North), Bearing.North, 1);
yield return new StepState(state.Point.Move(Bearing.South), Bearing.South, 1);
break;
}
}

/// <summary>
/// Check if the given point is in bounds
/// </summary>
/// <param name="point">Point</param>
/// <returns>Point is in bounds</returns>
private bool InBounds(Point2D point) => point.X >= 0 && point.X < this.Width
&& point.Y >= 0 && point.Y < this.Height;
}

/// <summary>
/// Step state
/// </summary>
/// <param name="Point">Current point</param>
/// <param name="Bearing">Bearing taken to enter this point (to prevent backtracking, which is not allowed)</param>
/// <param name="Consecutive">Number of consecutive steps taken on this bearing</param>
private record StepState(Point2D Point, Bearing Bearing, int Consecutive);
}
}
6 changes: 3 additions & 3 deletions src/AdventOfCode/Utilities/GridUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ private static readonly (int x, int y)[] Deltas =
/// </summary>
/// <param name="input">Input lines</param>
/// <returns>Grid</returns>
public static T[,] ToGrid<T>(this string[] input)
public static T[,] ToGrid<T>(this IReadOnlyList<string> input)
{
// y,x remember, not x,y
T[,] grid = new T[input.Length, input[0].Length];
T[,] grid = new T[input.Count, input[0].Length];

for (int y = 0; y < input.Length; y++)
for (int y = 0; y < input.Count; y++)
{
for (int x = 0; x < input[y].Length; x++)
{
Expand Down
Binary file added src/AdventOfCode/inputs/day17.txt
Binary file not shown.
86 changes: 86 additions & 0 deletions tests/AdventOfCode.Tests/Day17Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System.IO;
using Xunit;
using Xunit.Abstractions;

namespace AdventOfCode.Tests
{
public class Day17Tests
{
private readonly ITestOutputHelper output;
private readonly Day17 solver;

public Day17Tests(ITestOutputHelper output)
{
this.output = output;
this.solver = new Day17();
}

private static string[] GetRealInput()
{
string[] input = File.ReadAllLines("inputs/day17.txt");
return input;
}

private static string[] GetSampleInput()
{
return new string[]
{
"2413432311323",
"3215453535623",
"3255245654254",
"3446585845452",
"4546657867536",
"1438598798454",
"4457876987766",
"3637877979653",
"4654967986887",
"4564679986453",
"1224686865563",
"2546548887735",
"4322674655533",
};
}

[Fact]
public void Part1_SampleInput_ProducesCorrectResponse()
{
var expected = 102;

var result = solver.Part1(GetSampleInput());

Assert.Equal(expected, result);
}

[Fact]
public void Part1_RealInput_ProducesCorrectResponse()
{
var expected = 928;

var result = solver.Part1(GetRealInput());
output.WriteLine($"Day 17 - Part 1 - {result}");

Assert.Equal(expected, result);
}

[Fact]
public void Part2_SampleInput_ProducesCorrectResponse()
{
var expected = 94;

var result = solver.Part2(GetSampleInput());

Assert.Equal(expected, result);
}

[Fact]
public void Part2_RealInput_ProducesCorrectResponse()
{
var expected = 1104;

var result = solver.Part2(GetRealInput());
output.WriteLine($"Day 17 - Part 2 - {result}");

Assert.Equal(expected, result);
}
}
}

0 comments on commit 81dd549

Please sign in to comment.