Skip to content

Commit

Permalink
day(16): The Floor Will Be Lava 🎁
Browse files Browse the repository at this point in the history
  • Loading branch information
adamrodger committed Dec 16, 2023
1 parent e1f8dd7 commit f948e61
Show file tree
Hide file tree
Showing 4 changed files with 278 additions and 3 deletions.
192 changes: 192 additions & 0 deletions src/AdventOfCode/Day16.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
using System.Collections.Generic;
using System.Linq;
using AdventOfCode.Utilities;

namespace AdventOfCode
{
/// <summary>
/// Solver for Day 16
/// </summary>
public class Day16
{
public int Part1(string[] input) => BeamGrid.Parse(input).EnergisedTiles((0, 0), Bearing.East);

public int Part2(string[] input) => BeamGrid.Parse(input).MaxEnergisedTiles();

/// <summary>
/// A grid of reflectors and splitters for light beams
/// </summary>
/// <param name="Grid">Grid</param>
/// <param name="Width">Grid width</param>
/// <param name="Height">Grid height</param>
private record BeamGrid(char[,] Grid, int Width, int Height)
{
private readonly Queue<(Point2D Point, Bearing Bearing)> queue = new();
private readonly HashSet<(Point2D Point, Bearing Bearing)> seen = new();

/// <summary>
/// Parse the beam grid
/// </summary>
/// <param name="input">Input</param>
/// <returns>Beam grid</returns>
public static BeamGrid Parse(IReadOnlyList<string> input)
{
var grid = input.ToGrid();
int width = input[0].Length;
int height = input.Count;

return new BeamGrid(grid, width, height);
}

/// <summary>
/// Maximum energised tiles that can be achieved from all starting edge points
/// </summary>
/// <returns>Max energised tiles</returns>
public int MaxEnergisedTiles() => this.WalkEdges().Max(edge => this.EnergisedTiles(edge.Point, edge.Bearing));

/// <summary>
/// Calculate how many tiles would be energised by the light beam starting at the given point on the given bearing
/// </summary>
/// <param name="startPoint">Start point</param>
/// <param name="startBearing">Start bearing</param>
/// <returns>Number of energised tiles</returns>
public int EnergisedTiles(Point2D startPoint, Bearing startBearing)
{
this.queue.Enqueue((startPoint, startBearing));

while (this.queue.Count > 0)
{
(Point2D point, Bearing bearing) = this.queue.Dequeue();

this.seen.Add((point, bearing));

char current = this.Grid[point.Y, point.X];

foreach (Bearing move in NextMoves(current, bearing))
{
(Point2D Point, Bearing Bearing) next = (point.Move(move), move);

if (this.InBounds(next.Point) && !this.seen.Contains(next))
{
this.queue.Enqueue(next);
}
}
}

int energised = this.seen.Select(pair => pair.Point).Distinct().Count();

// TODO: Retain the seen list between calls so we can memoize future calls without re-simulating
this.seen.Clear();

return energised;
}

/// <summary>
/// Calculate where the light beam will move next after hitting the current tile on the given bearing
///
/// The beam can split if it hits a splitter, hence why more than one result can be returned
/// </summary>
/// <param name="current">Current tile</param>
/// <param name="bearing">Current bearing</param>
/// <returns>Bearing(s) of the light beam continuation(s)</returns>
private static IEnumerable<Bearing> NextMoves(char current, Bearing bearing)
{
switch (current)
{
case '.':
yield return bearing;
break;
case '-':
switch (bearing)
{
case Bearing.East or Bearing.West:
yield return bearing;
break;
case Bearing.North or Bearing.South:
yield return Bearing.East;
yield return Bearing.West;
break;
}

break;
case '|':
switch (bearing)
{
case Bearing.East or Bearing.West:
yield return Bearing.North;
yield return Bearing.South;
break;
case Bearing.North or Bearing.South:
yield return bearing;
break;
}

break;
case '\\':
switch (bearing)
{
case Bearing.North:
yield return Bearing.West;
break;
case Bearing.South:
yield return Bearing.East;
break;
case Bearing.East:
yield return Bearing.South;
break;
case Bearing.West:
yield return Bearing.North;
break;
}

break;
case '/':
switch (bearing)
{
case Bearing.North:
yield return Bearing.East;
break;
case Bearing.South:
yield return Bearing.West;
break;
case Bearing.East:
yield return Bearing.North;
break;
case Bearing.West:
yield return Bearing.South;
break;
}

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>
/// Get all the edge locations along with their inward pointing bearing
/// </summary>
/// <returns>Edges</returns>
private IEnumerable<(Point2D Point, Bearing Bearing)> WalkEdges()
{
foreach (int x in Enumerable.Range(0, this.Width))
{
yield return ((x, 0), Bearing.South);
yield return ((x, this.Height - 1), Bearing.North);
}

foreach (int y in Enumerable.Range(0, this.Height))
{
yield return ((0, y), Bearing.East);
yield return ((this.Width - 1, y), Bearing.West);
}
}
}
}
}
6 changes: 3 additions & 3 deletions src/AdventOfCode/Utilities/GridUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ private static readonly (int x, int y)[] Deltas =
/// </summary>
/// <param name="input">Input</param>
/// <returns>Char grid</returns>
public static char[,] ToGrid(this string[] input)
public static char[,] ToGrid(this IReadOnlyList<string> input)
{
// y,x remember, not x,y
char[,] grid = new char[input.Length, input[0].Length];
char[,] grid = new char[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/day16.txt
Binary file not shown.
83 changes: 83 additions & 0 deletions tests/AdventOfCode.Tests/Day16Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System.IO;
using Xunit;
using Xunit.Abstractions;

namespace AdventOfCode.Tests
{
public class Day16Tests
{
private readonly ITestOutputHelper output;
private readonly Day16 solver;

public Day16Tests(ITestOutputHelper output)
{
this.output = output;
this.solver = new Day16();
}

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

private static string[] GetSampleInput()
{
return new string[]
{
@".|...\....",
@"|.-.\.....",
@".....|-...",
@"........|.",
@"..........",
@".........\",
@"..../.\\..",
@".-.-/..|..",
@".|....-|.\",
@"..//.|....",
};
}

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

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

Assert.Equal(expected, result);
}

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

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

Assert.Equal(expected, result);
}

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

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

Assert.Equal(expected, result);
}

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

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

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

0 comments on commit f948e61

Please sign in to comment.