Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions src/main/resources/day14_cave_system

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions src/main/scala/ca/ulrichs/aoc/core/algebra/Coordinate.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package ca.ulrichs.aoc.core.algebra

import ca.ulrichs.aoc.core.input.InputParsing

case class Coordinate(x: Int, y: Int):
def +(coordinate: Coordinate): Coordinate = Coordinate(x = coordinate.x + x, y = coordinate.y + y)
def -(coordinate: Coordinate): Coordinate = Coordinate(x = coordinate.x - x, y = coordinate.y - y)
Expand Down Expand Up @@ -28,6 +30,14 @@ case class Coordinate(x: Int, y: Int):

lazy val surrounding: Seq[Coordinate] = adjacent ++ diagonal

def to(coordinate: Coordinate): Seq[Coordinate] = coordinate match{
case Coordinate(targetX, targetY) if targetX == x => buildRange(y, targetY).map(newY => copy(y = newY))
case Coordinate(targetX, targetY) if targetY == y => buildRange(x, targetX).map(newX => copy(x = newX))
case _ => throw IllegalArgumentException(s"Can only create as range of Coordinates on a straight axis: $this -> $coordinate.")
}

private def buildRange(start: Int, end: Int): Range = if start < end then start to end else (end to start).reverse

override def toString: String = s"($x, $y)"

object Coordinate:
Expand All @@ -43,6 +53,9 @@ object Coordinate:
case _ => throw IllegalArgumentException(s"Cannot build a Coordinate from $input")
}

given InputParsing[Coordinate] with
def parse(input: String): Coordinate = Coordinate.parse(input)

given Conversion[(Int, Int), Coordinate] with
def apply(tuple: (Int, Int)): Coordinate = Coordinate(tuple._1, tuple._2)

Expand Down
20 changes: 11 additions & 9 deletions src/main/scala/ca/ulrichs/aoc/core/algebra/Grid.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package ca.ulrichs.aoc.core.algebra

case class Grid[+A](private val points: Map[Coordinate, A]):
import scala.collection.immutable.HashMap

case class Grid[+A](private val points: HashMap[Coordinate, A]):
lazy val maximumColumn = keys.map(_.x).max
lazy val maximumRow = keys.map(_.y).max

Expand Down Expand Up @@ -46,7 +48,7 @@ case class Grid[+A](private val points: Map[Coordinate, A]):
(Grid(left), Grid(right))
}

def mapValues[B](f: A => B): Grid[B] = Grid(points.view.mapValues(f).toMap)
def mapValues[B](f: A => B): Grid[B] = Grid(HashMap.from(points.view.mapValues(f)))
def mapKeys(f: Coordinate => Coordinate): Grid[A] = Grid(points.map { case(coordinate, value) => f(coordinate) -> value })

def dropColumn(column: Int): Grid[A] = Grid(points.collect {
Expand All @@ -72,12 +74,13 @@ case class Grid[+A](private val points: Map[Coordinate, A]):

def transformAt[B >: A](positions: Seq[Coordinate])(f: B => B): Grid[B] =
if positions.isEmpty then this
else Grid(
positions.foldLeft(Map.from[Coordinate, B](points)) { case (accumulation, key) => accumulation.get(key) match {
else
val newPoints = positions.foldLeft(Map.from[Coordinate, B](points)) { case (accumulation, key) => accumulation.get(key) match {
case Some(value) => accumulation.updated[B](key, f(value))
case None => accumulation
} }
)

Grid(HashMap.from(newPoints))

def merge[B >: A](other: Grid[B]): Grid[B] = Grid(points ++ other.points)
def merge[B >: A](grid: Grid[B])(f: (B, B) => B): Grid[B] = Grid(
Expand All @@ -100,7 +103,7 @@ case class Grid[+A](private val points: Map[Coordinate, A]):
println(toString(stringify, default))

object Grid:
def apply[A](input: Seq[(Coordinate, A)]): Grid[A] = Grid(input.toMap)
def apply[A](input: Seq[(Coordinate, A)]): Grid[A] = Grid(HashMap.from(input))

def parse[A](input: Seq[String])(parser: (Coordinate, Char) => A): Grid[A] = Grid(
input.zipWithIndex.flatMap { (row, y) =>
Expand All @@ -110,13 +113,12 @@ object Grid:
}
)

def fromCoordinates[A](input: Seq[String], default: A)(fill: Coordinate => A): Grid[A] = {
val coordinates = input.map(Coordinate.parse)
def fromCoordinates[A](coordinates: Seq[Coordinate], default: A)(fill: Coordinate => A): Grid[A] = {
val width = coordinates.map(_.x).max + 1
val height = coordinates.map(_.y).max + 1

create(width = width, height = height) { coordinate =>
coordinates.find(_ == coordinate).map(fill).getOrElse(default)
coordinates.find(_ == coordinate).map(fill).getOrElse(default)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package ca.ulrichs.aoc.core.algebra.pathing
import ca.ulrichs.aoc.core.algebra.Coordinate
import ca.ulrichs.aoc.core.algebra.grid.Grid


import scala.collection.{mutable, *}
import scala.util.control.Breaks.{break, breakable}

Expand Down
5 changes: 1 addition & 4 deletions src/main/scala/ca/ulrichs/aoc/core/input/InputParsing.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package ca.ulrichs.aoc.core.input

import ca.ulrichs.aoc.core.algebra.{Coordinate, RangeHelpers}
import ca.ulrichs.aoc.core.algebra.RangeHelpers

trait InputParsing[A]:
def parse(input: String): A
Expand All @@ -17,6 +17,3 @@ object InputParsing:

given InputParsing[Range] with
def parse(input: String): Range = RangeHelpers.parse(input)

given InputParsing[Coordinate] with
def parse(input: String): Coordinate = Coordinate.parse(input)
74 changes: 74 additions & 0 deletions src/main/scala/ca/ulrichs/aoc/island/waterfall/CaveSystem.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package ca.ulrichs.aoc.island.waterfall

import ca.ulrichs.aoc.core.algebra.Coordinate
import ca.ulrichs.aoc.core.algebra.grid.Grid
import ca.ulrichs.aoc.core.input.SourceInput
import ca.ulrichs.aoc.core.input.StringParsing.*
import ca.ulrichs.aoc.island.waterfall.CaveSystem.SandStart

import scala.annotation.tailrec

case class CaveSystem(grid: Grid[Char]):
lazy val sandUnits: Seq[Coordinate] = grid.findAll('o')

lazy val afterSandfall: CaveSystem = sandFall(SandStart, grid)

@tailrec
final def sandFall(sand: Coordinate, innerGrid: Grid[Char]): CaveSystem = {
val availableSpots = (innerGrid.at(sand.down), innerGrid.at(sand.downLeft), innerGrid.at(sand.downRight))
val maxY = Seq(sand.down, sand.downLeft, sand.downLeft).map(_.y).max
// val maxX = Seq(sand.down, sand.downLeft, sand.downLeft).map(_.x).max

if maxY >= innerGrid.dimensions.height then return CaveSystem(innerGrid)
// if maxX >= innerGrid.width then return CaveSystem(innerGrid)

availableSpots match {
case (Some('.'), _, _) => sandFall(sand.down, innerGrid)
case (_, Some('.'), _) => sandFall(sand.downLeft, innerGrid)
case (_, _, Some('.')) => sandFall(sand.downRight, innerGrid)
case _ =>
if sand == SandStart then CaveSystem(innerGrid.updated(SandStart, 'o'))
else sandFall(SandStart, innerGrid.updated(sand, 'o'))
}
}


object CaveSystem:
val SandStart: Coordinate = Coordinate(500, 0)

def parse(input: SourceInput): CaveSystem = {
val all = input.asSeq[String].flatMap { rawRockLine =>
val coordinateRangeList = rawRockLine.split(" -> ").map(_.as[Coordinate])

coordinateRangeList.foldLeft(Seq.empty[Coordinate]) {
case (Nil, target) => Seq(target)
case (head :: Nil, target) => head to target
case (init :+ last, target) => init ++ (last to target)
}
}

val grid = Grid.fromCoordinates(all)(value => if all.contains(value) then '#' else '.').updated(SandStart, '+')
CaveSystem(grid)
}

def parseWithFloor(input: SourceInput): CaveSystem = {
val coordinateRanges: Seq[Seq[Coordinate]] = input.asSeq[String].map(_.split(" -> ").map(_.as[Coordinate]))

val coordinates: Seq[Coordinate] = coordinateRanges.flatMap { coordinateList =>
coordinateList.foldLeft(Seq.empty[Coordinate]) {
case (Nil, target) => Seq(target)
case (head :: Nil, target) => head to target
case (init :+ last, target) => init ++ (last to target)
}
}

val maxX = coordinates.map(_.x).max
val floorY = coordinates.map(_.y).max + 2

val floorToSandStart = (floorY - SandStart.y)
val floor = Coordinate(SandStart.x - floorToSandStart, floorY) to Coordinate(SandStart.x + floorToSandStart, floorY)
val all = coordinates ++ floor :+ SandStart

val grid = Grid.fromCoordinates(all)(value => if all.contains(value) then '#' else '.').updated(SandStart, '+')
CaveSystem(grid)
}
2 changes: 1 addition & 1 deletion src/test/scala/ca/ulrichs/aoc/core/algebra/GridTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ class GridTest extends AnyFunSuite with LoneElement:
}

test("Can create a grid given a list of coordinates") {
val grid = Grid.fromCoordinates(Seq("3, 3", "5, 5"), default = 0)(_.x)
val grid = Grid.fromCoordinates(Seq(Coordinate(3, 3), Coordinate(5, 5)), default = 0)(_.x)

grid.height shouldBe 6
grid.width shouldBe 6
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package ca.ulrichs.aoc.island.waterfall

import ca.ulrichs.aoc.core.input.SourceInput
import org.scalatest.*
import matchers.should.Matchers.*
import org.scalatest.funsuite.AnyFunSuite

class CaveSystemTest extends AnyFunSuite:
val exampleSource = SourceInput(
"498,4 -> 498,6 -> 496,6",
"503,4 -> 502,4 -> 502,9 -> 494,9"
)
// test("Can parse the ranges of rocks into a Cave System") {
// CaveSystem.parseWithFloor(exampleSource).afterSandfall.grid.print((c, v) => v.toString)
// CaveSystem.parseWithFloor(exampleSource).afterSandfall.sandUnits.length shouldBe 93
// }

test("Can find when the cave system fills up for a real cave system with a floor") {
val source = SourceInput.fromResource("day14_cave_system")
val afterSandfall = CaveSystem.parseWithFloor(source).afterSandfall

afterSandfall.grid.print((c, v) => v.toString)
afterSandfall.sandUnits.length shouldBe 28145
}

// test("Do some sandfall") {
// CaveSystem.parse(exampleSource).afterSandfall.grid.print((c, v) => v.toString)
// CaveSystem.parse(exampleSource).afterSandfall.sandUnits.length shouldBe 24
// }
//
// test("Do some sandfall for real") {
// val source = SourceInput.fromResource("day14_cave_system")
// CaveSystem.parse(source).afterSandfall.sandUnits.length shouldBe 24
// }