diff --git a/src/AdventOfCode/Day22.cs b/src/AdventOfCode/Day22.cs
new file mode 100644
index 0000000..7043b4c
--- /dev/null
+++ b/src/AdventOfCode/Day22.cs
@@ -0,0 +1,205 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using AdventOfCode.Utilities;
+
+namespace AdventOfCode
+{
+ ///
+ /// Solver for Day 22
+ ///
+ public class Day22
+ {
+ public int Part1(string[] input)
+ {
+ var cubes = input.Select((line, i) => SandCube.Parse(i, line)).ToArray();
+ var tower = SandCubeTower.Build(cubes);
+
+ int total = 0;
+
+ foreach (SandCube cube in cubes)
+ {
+ if (!tower.Supporting.TryGetValue(cube.Id, out ISet holdingUp))
+ {
+ // cube doesn't support anything, safe to remove
+ total++;
+ continue;
+ }
+
+ if (holdingUp.All(h => tower.SupportedBy[h].Count > 1))
+ {
+ // we're supporting things, but everything we're supporting is itself supported by at least one other thing
+ total++;
+ }
+ }
+
+ return total;
+ }
+
+ public int Part2(string[] input)
+ {
+ var cubes = input.Select((line, i) => SandCube.Parse(i, line)).ToArray();
+ var tower = SandCubeTower.Build(cubes);
+
+ int total = 0;
+
+ foreach (SandCube cube in cubes)
+ {
+ Queue queue = new();
+ HashSet fell = new() { cube.Id };
+
+ queue.Enqueue(cube.Id);
+
+ while (queue.Count > 0)
+ {
+ int id = queue.Dequeue();
+
+ if (!tower.Supporting.TryGetValue(id, out ISet holdingUp))
+ {
+ // not holding anything up, so nothing to fall
+ continue;
+ }
+
+ foreach (int heldUp in holdingUp)
+ {
+ if (tower.SupportedBy[heldUp].All(fell.Contains))
+ {
+ // everything supporting this one fell down
+ queue.Enqueue(heldUp);
+ fell.Add(heldUp);
+ }
+ }
+ }
+
+ total += fell.Count - 1; // don't count the block that we removed
+ }
+
+ return total;
+ }
+
+ ///
+ /// Tower of sand cubes
+ ///
+ /// Lookup of each sand cube to which other sand cubes it is supporting
+ /// Lookup of each sand cube to which other sand cubes it is supported by
+ private record SandCubeTower(IDictionary> Supporting, IDictionary> SupportedBy)
+ {
+ ///
+ /// Building the tower from the given starting cube positions representing falling cubes
+ ///
+ /// Falling cubes
+ /// Sand cube tower after all cubes have fallen and settled
+ public static SandCubeTower Build(IEnumerable falling)
+ {
+ Dictionary> supporting = new();
+ Dictionary> supportedBy = new();
+ Dictionary occupiedSpace = new();
+
+ foreach (SandCube cube in falling.OrderBy(c => c.BottomLeftFront.Z))
+ {
+ SandCube current = cube;
+ SandCube dropped = cube.Drop();
+
+ // drop until we hit either the ground or a point occupied by another cube
+ while (dropped.BottomLayer().All(p => p.Z > 0 && !occupiedSpace.ContainsKey(p)))
+ {
+ current = dropped;
+ dropped = current.Drop();
+ }
+
+ // mark the cubes below as supporting this one
+ foreach (Point3D point in dropped.BottomLayer().Where(occupiedSpace.ContainsKey))
+ {
+ int supportingId = occupiedSpace[point];
+ supporting.GetOrCreate(supportingId, () => new HashSet()).Add(current.Id);
+ supportedBy.GetOrCreate(current.Id, () => new HashSet()).Add(supportingId);
+ }
+
+ // settle this cube in place
+ foreach (Point3D point in current.Points())
+ {
+ occupiedSpace[point] = current.Id;
+ }
+ }
+
+ return new SandCubeTower(supporting, supportedBy);
+ }
+ }
+
+ ///
+ /// A cube of sand
+ ///
+ /// Cube ID
+ /// The bottom front left corner
+ /// The top right back corner
+ private record SandCube(int Id, Point3D BottomLeftFront, Point3D TopRightBack)
+ {
+ ///
+ /// Parse a sand cube from input
+ ///
+ /// Cube ID
+ /// Input line
+ /// Sand cube
+ public static SandCube Parse(int id, string input)
+ {
+ int[] numbers = input.Numbers();
+
+ Point3D bottomLeftFront = (Math.Min(numbers[0], numbers[3]),
+ Math.Min(numbers[1], numbers[4]),
+ Math.Min(numbers[2], numbers[5]));
+
+ Point3D topRightBack = (Math.Max(numbers[0], numbers[3]),
+ Math.Max(numbers[1], numbers[4]),
+ Math.Max(numbers[2], numbers[5]));
+
+ return new SandCube(id, bottomLeftFront, topRightBack);
+ }
+
+ ///
+ /// Drop the current cube by one place
+ ///
+ /// Same cube but space one lower down
+ public SandCube Drop()
+ {
+ return this with
+ {
+ BottomLeftFront = this.BottomLeftFront - (0, 0, 1),
+ TopRightBack = this.TopRightBack - (0, 0, 1)
+ };
+ }
+
+ ///
+ /// Enumerate all the points of space taken up by this cube
+ ///
+ /// Cube points
+ public IEnumerable Points()
+ {
+ for (int x = this.BottomLeftFront.X; x <= this.TopRightBack.X; x++)
+ {
+ for (int y = this.BottomLeftFront.Y; y <= this.TopRightBack.Y; y++)
+ {
+ for (int z = this.BottomLeftFront.Z; z <= this.TopRightBack.Z; z++)
+ {
+ yield return (x, y, z);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Enumerate all the points of space taken up by the bottom layer of this cube
+ ///
+ /// Bottom layer
+ public IEnumerable BottomLayer()
+ {
+ for (int x = this.BottomLeftFront.X; x <= this.TopRightBack.X; x++)
+ {
+ for (int y = this.BottomLeftFront.Y; y <= this.TopRightBack.Y; y++)
+ {
+ yield return (x, y, this.BottomLeftFront.Z);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/AdventOfCode/inputs/day22.txt b/src/AdventOfCode/inputs/day22.txt
new file mode 100644
index 0000000..c38b88b
Binary files /dev/null and b/src/AdventOfCode/inputs/day22.txt differ
diff --git a/tests/AdventOfCode.Tests/Day22Tests.cs b/tests/AdventOfCode.Tests/Day22Tests.cs
new file mode 100644
index 0000000..45fa637
--- /dev/null
+++ b/tests/AdventOfCode.Tests/Day22Tests.cs
@@ -0,0 +1,81 @@
+using System.IO;
+using Xunit;
+using Xunit.Abstractions;
+
+
+namespace AdventOfCode.Tests
+{
+ public class Day22Tests
+ {
+ private readonly ITestOutputHelper output;
+ private readonly Day22 solver;
+
+ public Day22Tests(ITestOutputHelper output)
+ {
+ this.output = output;
+ this.solver = new Day22();
+ }
+
+ private static string[] GetRealInput()
+ {
+ string[] input = File.ReadAllLines("inputs/day22.txt");
+ return input;
+ }
+
+ private static string[] GetSampleInput()
+ {
+ return new string[]
+ {
+ "1,0,1~1,2,1",
+ "0,0,2~2,0,2",
+ "0,2,3~2,2,3",
+ "0,0,4~0,2,4",
+ "2,0,5~2,2,5",
+ "0,1,6~2,1,6",
+ "1,1,8~1,1,9",
+ };
+ }
+
+ [Fact]
+ public void Part1_SampleInput_ProducesCorrectResponse()
+ {
+ var expected = 5;
+
+ var result = solver.Part1(GetSampleInput());
+
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void Part1_RealInput_ProducesCorrectResponse()
+ {
+ var expected = 395;
+
+ var result = solver.Part1(GetRealInput());
+ output.WriteLine($"Day 22 - Part 1 - {result}");
+
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void Part2_SampleInput_ProducesCorrectResponse()
+ {
+ var expected = 7;
+
+ var result = solver.Part2(GetSampleInput());
+
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void Part2_RealInput_ProducesCorrectResponse()
+ {
+ var expected = 64714;
+
+ var result = solver.Part2(GetRealInput());
+ output.WriteLine($"Day 22 - Part 2 - {result}");
+
+ Assert.Equal(expected, result);
+ }
+ }
+}