diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..527dce1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +src/main/resources/*.txt filter=git-crypt diff=git-crypt diff --git a/.github/workflows/scala.yml b/.github/workflows/scala.yml new file mode 100644 index 0000000..e2dc28c --- /dev/null +++ b/.github/workflows/scala.yml @@ -0,0 +1,22 @@ +name: Tests + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 21 + uses: actions/setup-java@v3 + with: + java-version: 21 + distribution: temurin + - name: Unlock git-crypt + run: | + sudo apt-get update + sudo apt-get install -y git-crypt + echo ${{ secrets.GIT_CRYPT_KEY }} | base64 -d | git-crypt unlock - + - name: Run tests + run: sbt test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0e4b1ce --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +target +*.sc +project/project diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 0000000..6e8673e --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,4 @@ +version = "3.0.5" +runner.dialect = scala3 +align.preset = more +maxColumn = 100 diff --git a/README.md b/README.md new file mode 100644 index 0000000..a282877 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# aoc2021 + +[![Scala CI](https://github.com/lupari/aoc2021/actions/workflows/scala.yml/badge.svg)](https://github.com/lupari/aoc2021/actions?query=workflow%3A%22Scala+CI%22) + +Advent of Code 2021 (http://adventofcode.com/2021) personal exercises in Scala + +For running these you need SBT (http://scala-sbt.org) installed. diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..2070ca3 --- /dev/null +++ b/build.sbt @@ -0,0 +1,11 @@ +name := "aoc2021" + +version := "0.1" + +scalaVersion := "3.1.0" + +libraryDependencies += "org.scala-lang.modules" %% "scala-parser-combinators" % "2.1.0" + +libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.10" % "test" + +scalacOptions += "-deprecation" diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..7a7e80d --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.5.5 diff --git a/src/main/resources/day01.txt b/src/main/resources/day01.txt new file mode 100644 index 0000000..1e83a37 Binary files /dev/null and b/src/main/resources/day01.txt differ diff --git a/src/main/resources/day02.txt b/src/main/resources/day02.txt new file mode 100644 index 0000000..5252e8e Binary files /dev/null and b/src/main/resources/day02.txt differ diff --git a/src/main/resources/day03.txt b/src/main/resources/day03.txt new file mode 100644 index 0000000..ea9dcef Binary files /dev/null and b/src/main/resources/day03.txt differ diff --git a/src/main/resources/day04.txt b/src/main/resources/day04.txt new file mode 100644 index 0000000..2f0d896 Binary files /dev/null and b/src/main/resources/day04.txt differ diff --git a/src/main/resources/day05.txt b/src/main/resources/day05.txt new file mode 100644 index 0000000..2aae6a3 Binary files /dev/null and b/src/main/resources/day05.txt differ diff --git a/src/main/resources/day06.txt b/src/main/resources/day06.txt new file mode 100644 index 0000000..53999e8 Binary files /dev/null and b/src/main/resources/day06.txt differ diff --git a/src/main/resources/day07.txt b/src/main/resources/day07.txt new file mode 100644 index 0000000..e139e49 Binary files /dev/null and b/src/main/resources/day07.txt differ diff --git a/src/main/resources/day08.txt b/src/main/resources/day08.txt new file mode 100644 index 0000000..74b1a03 Binary files /dev/null and b/src/main/resources/day08.txt differ diff --git a/src/main/resources/day09.txt b/src/main/resources/day09.txt new file mode 100644 index 0000000..854cf59 Binary files /dev/null and b/src/main/resources/day09.txt differ diff --git a/src/main/resources/day10.txt b/src/main/resources/day10.txt new file mode 100644 index 0000000..45db1a4 Binary files /dev/null and b/src/main/resources/day10.txt differ diff --git a/src/main/resources/day11.txt b/src/main/resources/day11.txt new file mode 100644 index 0000000..62a6516 Binary files /dev/null and b/src/main/resources/day11.txt differ diff --git a/src/main/resources/day12.txt b/src/main/resources/day12.txt new file mode 100644 index 0000000..fabf150 Binary files /dev/null and b/src/main/resources/day12.txt differ diff --git a/src/main/resources/day13.txt b/src/main/resources/day13.txt new file mode 100644 index 0000000..a4cc45c Binary files /dev/null and b/src/main/resources/day13.txt differ diff --git a/src/main/resources/day14.txt b/src/main/resources/day14.txt new file mode 100644 index 0000000..22fdec5 Binary files /dev/null and b/src/main/resources/day14.txt differ diff --git a/src/main/resources/day15.txt b/src/main/resources/day15.txt new file mode 100644 index 0000000..91fa34c Binary files /dev/null and b/src/main/resources/day15.txt differ diff --git a/src/main/resources/day16.txt b/src/main/resources/day16.txt new file mode 100644 index 0000000..5ed7edb Binary files /dev/null and b/src/main/resources/day16.txt differ diff --git a/src/main/resources/day17.txt b/src/main/resources/day17.txt new file mode 100644 index 0000000..ec7e85d Binary files /dev/null and b/src/main/resources/day17.txt differ diff --git a/src/main/resources/day18.txt b/src/main/resources/day18.txt new file mode 100644 index 0000000..6c1dc06 Binary files /dev/null and b/src/main/resources/day18.txt differ diff --git a/src/main/resources/day19.txt b/src/main/resources/day19.txt new file mode 100644 index 0000000..110031f Binary files /dev/null and b/src/main/resources/day19.txt differ diff --git a/src/main/resources/day20.txt b/src/main/resources/day20.txt new file mode 100644 index 0000000..9aac69c Binary files /dev/null and b/src/main/resources/day20.txt differ diff --git a/src/main/resources/day21.txt b/src/main/resources/day21.txt new file mode 100644 index 0000000..7f1352d Binary files /dev/null and b/src/main/resources/day21.txt differ diff --git a/src/main/resources/day22.txt b/src/main/resources/day22.txt new file mode 100644 index 0000000..6dcd55e Binary files /dev/null and b/src/main/resources/day22.txt differ diff --git a/src/main/resources/day24.txt b/src/main/resources/day24.txt new file mode 100644 index 0000000..8799a5e Binary files /dev/null and b/src/main/resources/day24.txt differ diff --git a/src/main/resources/day25.txt b/src/main/resources/day25.txt new file mode 100644 index 0000000..81fb2f8 Binary files /dev/null and b/src/main/resources/day25.txt differ diff --git a/src/main/scala/challenge/Day01.scala b/src/main/scala/challenge/Day01.scala new file mode 100644 index 0000000..61e0b83 --- /dev/null +++ b/src/main/scala/challenge/Day01.scala @@ -0,0 +1,10 @@ +package challenge + +import scala.io.Source + +object Day01: + + val input: List[Int] = Source.fromResource("day01.txt").getLines().map(_.toInt).toList + + def partOne(): Int = input.sliding(2).count(p => p.last > p.head) + def partTwo(): Int = input.sliding(4).count(p => p.last > p.head) diff --git a/src/main/scala/challenge/Day02.scala b/src/main/scala/challenge/Day02.scala new file mode 100644 index 0000000..305f31e --- /dev/null +++ b/src/main/scala/challenge/Day02.scala @@ -0,0 +1,41 @@ +package challenge + +import scala.io.Source +import lib.Points.{Point, Position} + +object Day02: + + trait Cmd + case class MoveHorizontal(amount: Int) extends Cmd + case class MoveVertical(amount: Int) extends Cmd + + trait Pos: + val p: Point + val product: Int = p.x * p.y + def moveHorizontal(n: Int): Pos + def moveVertical(n: Int): Pos + + case class Pos1(p: Point) extends Pos: + override def moveHorizontal(n: Int) = Pos1(p + Point(n, 0)) + override def moveVertical(n: Int) = Pos1(p + Point(0, n)) + + case class Pos2(p: Point, aim: Int = 0) extends Pos: + override def moveHorizontal(n: Int) = copy(p = p + Point(n, aim * n)) + override def moveVertical(n: Int) = copy(aim = aim + n) + + private val regex = """(forward|down|up) (\d+)""".r + def parse(cmd: String): Cmd = cmd match + case regex(name, amount) => + name match + case "forward" => MoveHorizontal(amount.toInt) + case "down" => MoveVertical(amount.toInt) + case "up" => MoveVertical(-amount.toInt) + + def advance(pos: Pos, cmd: Cmd): Pos = cmd match + case MoveHorizontal(n) => pos.moveHorizontal(n) + case MoveVertical(n) => pos.moveVertical(n) + + val input: List[Cmd] = Source.fromResource("day02.txt").getLines().map(parse).toList + + def partOne(): Int = input.foldLeft(Pos1(Position.zero))(advance).product + def partTwo(): Int = input.foldLeft(Pos2(Position.zero))(advance).product diff --git a/src/main/scala/challenge/Day03.scala b/src/main/scala/challenge/Day03.scala new file mode 100644 index 0000000..4325d06 --- /dev/null +++ b/src/main/scala/challenge/Day03.scala @@ -0,0 +1,31 @@ +package challenge + +import scala.io.Source +import scala.annotation.tailrec +import lib.Numbers._ + +object Day03: + + def commonest(xs: List[Char]): Char = if xs.count(_ == '0') > xs.length / 2 then '0' else '1' + def rarest(xs: List[Char]): Char = if commonest(xs) == '0' then '1' else '0' + def findMatch(xs: List[String])(fn: (List[Char]) => Char): String = + @tailrec + def helper(xs: List[String], i: Int): String = xs match + case h :: Nil => h + case _ => + val c = xs.transpose.map(fn)(i) + helper(xs.filter(x => x(i) == c), i + 1) + + helper(xs, 0) + + val input: List[String] = Source.fromResource("day03.txt").getLines().toList + + def partOne(): Long = + val gamma = input.transpose.map(commonest).mkString + val epsilon = input.transpose.map(rarest).mkString + bin2dec(gamma) * bin2dec(epsilon) + + def partTwo(): Long = + val o = findMatch(input)(commonest) + val co2 = findMatch(input)(rarest) + bin2dec(o) * bin2dec(co2) diff --git a/src/main/scala/challenge/Day04.scala b/src/main/scala/challenge/Day04.scala new file mode 100644 index 0000000..dfb6686 --- /dev/null +++ b/src/main/scala/challenge/Day04.scala @@ -0,0 +1,42 @@ +package challenge + +import scala.io.Source +import scala.annotation.tailrec + +object Day04: + + case class Player(board: List[Set[Int]]): + def mark(n: Int): Player = copy(board = board.map(_ - n)) + val score: Int = board.flatten.toSet.sum + val hasBingo: Boolean = board.exists(_.isEmpty) + + def toPlayer(lines: List[List[Int]]): Player = Player((lines ++ lines.transpose).map(_.toSet)) + + def findScore()(fn: (List[Player]) => (Option[Player], List[Player])): Option[Int] = + @tailrec + def helper(xs: List[Int], ps: List[Player]): Option[Int] = xs match + case Nil => None + case h :: t => + fn(ps.map(_.mark(h))) match + case (None, ps2) => helper(t, ps2) + case (Some(p), _) => Option(p.score * h) + + helper(numbers, players) + + val input: List[String] = Source.fromResource("day04.txt").getLines().toList + val numbers: List[Int] = input.head.split(",").map(_.toInt).toList + val players: List[Player] = + input + .drop(2) + .grouped(6) + .map(_.filter(_.nonEmpty)) + .map(ls => toPlayer(ls.map(_.trim.split(" +").toList.map(_.toInt)))) + .toList + + def partOne(): Int = findScore()(ps => (ps.find(_.hasBingo), ps)).get + def partTwo(): Int = + def findLoser = (ps: List[Player]) => + val losers = ps.filterNot(_.hasBingo) + if losers.isEmpty && ps.length == 1 then (Some(ps.head), Nil) else (None, losers) + + findScore()(findLoser).get diff --git a/src/main/scala/challenge/Day05.scala b/src/main/scala/challenge/Day05.scala new file mode 100644 index 0000000..fb24604 --- /dev/null +++ b/src/main/scala/challenge/Day05.scala @@ -0,0 +1,21 @@ +package challenge + +import scala.io.Source +import lib.Points.{Point, Line} + +object Day05: + + private val regex = """(\d+),(\d+) -> (\d+),(\d+)""".r + def parse(s: String): Line = s match + case regex(x1, y1, x2, y2) => Line(Point(x1.toInt, y1.toInt), Point(x2.toInt, y2.toInt)) + + def intersections(lines: List[Line]): Int = + lines + .flatMap(_.points()) + .groupBy(identity) + .values + .count(_.length > 1) + + val input: List[Line] = Source.fromResource("day05.txt").getLines().map(parse).toList + def partOne(): Int = intersections(input.filter(l => l.dx == 0 || l.dy == 0)) + def partTwo(): Int = intersections(input) diff --git a/src/main/scala/challenge/Day06.scala b/src/main/scala/challenge/Day06.scala new file mode 100644 index 0000000..53f3b85 --- /dev/null +++ b/src/main/scala/challenge/Day06.scala @@ -0,0 +1,17 @@ +package challenge + +import scala.io.Source + +object Day06: + + def evolve(population: Map[Int, Long]): Map[Int, Long] = + val population2 = population.filter(_._1 > 0).map(p => p._1 - 1 -> p._2) + val eights = population.get(0).getOrElse(0L) + val sixes = eights + population.get(7).getOrElse(0L) + population2 + (6 -> sixes) + (8 -> eights) + + val input: List[Int] = Source.fromResource("day06.txt").mkString.split(",").map(_.toInt).toList + val population: Map[Int, Long] = input.groupMapReduce(identity)(_ => 1L)(_ + _) + + def partOne(): Long = Iterator.iterate(population)(evolve).drop(80).next.values.sum + def partTwo(): Long = Iterator.iterate(population)(evolve).drop(256).next.values.sum diff --git a/src/main/scala/challenge/Day07.scala b/src/main/scala/challenge/Day07.scala new file mode 100644 index 0000000..15111eb --- /dev/null +++ b/src/main/scala/challenge/Day07.scala @@ -0,0 +1,17 @@ +package challenge + +import scala.io.Source +import lib.Math._ + +object Day07: + + def fuelCost(n: Int)(costFn: (Int, Int) => Int): Int = crabs.map(costFn(n, _)).sum + + val crabs: List[Int] = Source.fromResource("day07.txt").mkString.split(",").map(_.toInt).toList + + def partOne(): Int = fuelCost(median(crabs))((a, b) => (a - b).abs) + def partTwo(): Int = + val avg = mean(crabs) + Seq(avg.floor.toInt, avg.ceil.toInt) + .map(fuelCost(_)((a, b) => arithmeticSeries(a, b, normalized = true))) + .min diff --git a/src/main/scala/challenge/Day08.scala b/src/main/scala/challenge/Day08.scala new file mode 100644 index 0000000..ba450b5 --- /dev/null +++ b/src/main/scala/challenge/Day08.scala @@ -0,0 +1,50 @@ +package challenge + +import scala.io.Source + +object Day08: + + type Digit = Set[Char] + case class Entry(pattern: Seq[Digit], output: Seq[Digit]) + + def find06(s1: Digit, s2: Digit, five: Digit) = + if (five &~ s1).nonEmpty then (s1, s2) else (s2, s1) + + def find25(s1: Digit, s2: Digit, nine: Digit) = + if (s1 &~ nine).nonEmpty then (s1, s2) else (s2, s1) + + def find3(s1: Digit, s2: Digit, s3: Digit) = + s1 &~ (s2 | s3) match + case s if s.isEmpty => (s1, (s2, s3)) + case _ => + if (s2 &~ (s1 | s3)).isEmpty then (s2, (s1, s3)) else (s3, (s1, s2)) + + def find9(s1: Digit, s2: Digit, s3: Digit, three: Digit) = + three &~ s1 match + case s if s.isEmpty => (s1, (s2, s3)) + case _ => + if (three &~ s2).isEmpty then (s2, (s1, s3)) else (s3, (s1, s2)) + + def decode(entry: Entry): Int = + val Seq(f1, f2, f3) = entry.pattern.filter(_.size == 5) + val Seq(s1, s2, s3) = entry.pattern.filter(_.size == 6) + val (three, (f4, f5)) = find3(f1, f2, f3) + val (nine, (s4, s5)) = find9(s1, s2, s3, three) + val (two, five) = find25(f4, f5, nine) + val (zero, six) = find06(s4, s5, five) + val cipher = Map(0 -> zero, 2 -> two, 3 -> three, 5 -> five, 6 -> six, 9 -> nine) + + def num(d: Digit): Option[Int] = predef.get(d.size).orElse(cipher.find(_._2 == d).map(_._1)) + + entry.output.flatMap(num).mkString.toInt + + def parse(s: String): Entry = + def _parse(xs: String) = xs.split(" ").map(_.toSet).toSeq + val (pattern, output) = (s.takeWhile(_ != '|'), s.dropWhile(_ != '|')) + Entry(_parse(pattern), _parse(output)) + + val entries: List[Entry] = Source.fromResource("day08.txt").getLines().map(parse).toList + val predef: Map[Int, Int] = Map(2 -> 1, 3 -> 7, 4 -> 4, 7 -> 8) + + def partOne(): Int = entries.flatMap(_.output).count(o => predef.contains(o.size)) + def partTwo(): Int = entries.map(decode).sum diff --git a/src/main/scala/challenge/Day09.scala b/src/main/scala/challenge/Day09.scala new file mode 100644 index 0000000..f05046f --- /dev/null +++ b/src/main/scala/challenge/Day09.scala @@ -0,0 +1,29 @@ +package challenge + +import scala.io.Source +import scala.annotation.tailrec +import lib.Points.Point +import lib.Graphs + +object Day09: + + import lib.GridExtensions._ + + def isLowPoint(p: Point): Boolean = p.neighbors.forall(n => grid(n) > grid(p)) + + lazy val basins: List[Set[Point]] = + @tailrec + def helper(xs: Set[Point], acc: List[Set[Point]]): List[Set[Point]] = xs match + case s if s.isEmpty => acc + case s => + val basin = Graphs.bfs(s.head)(_.neighbors.filterNot(grid(_) == 9)).keySet + helper(s -- basin, acc :+ basin) + + helper(lowPoints.keySet, Nil) + + val grid: Grid[Int] = + Source.fromResource("day09.txt").mkString.toList.toIntGrid.withDefaultValue(9) + val lowPoints: Grid[Int] = grid.filter(kv => isLowPoint(kv._1)) + + def partOne(): Int = lowPoints.map(_._2 + 1).sum + def partTwo(): Int = basins.map(_.size).sorted.takeRight(3).product diff --git a/src/main/scala/challenge/Day10.scala b/src/main/scala/challenge/Day10.scala new file mode 100644 index 0000000..69745b4 --- /dev/null +++ b/src/main/scala/challenge/Day10.scala @@ -0,0 +1,41 @@ +package challenge + +import scala.io.Source +import scala.annotation.tailrec + +object Day10: + + type Inspection = Either[Char, List[Char]] + case class Chunk(closure: Char, errorScore: Int, completionScore: Int) + val chunks = Map( + '(' -> Chunk(')', 3, 1), + '[' -> Chunk(']', 57, 2), + '{' -> Chunk('}', 1197, 3), + '<' -> Chunk('>', 25137, 4) + ) + + def errorScore(c: Char): Int = chunks.find(_._2.closure == c).get._2.errorScore + def autocomplete(xs: List[Char]): Long = + xs.reverse.foldLeft(0L)((a, b) => a * 5 + chunks(b).completionScore) + + def inspect(s: String): Inspection = + @tailrec + def helper(xs: List[Char], unclosed: List[Char]): Inspection = xs match + case Nil => Right(unclosed) + case h :: t => + chunks.find(_._2.closure == h) match + case None => + helper(t, unclosed :+ h) + case Some(x, _) => + if unclosed.last == x then helper(t, unclosed.init) + else Left(h) + + helper(s.toList.tail, List(s.head)) + + val input: List[String] = Source.fromResource("day10.txt").getLines().toList + val lines: List[Inspection] = input.map(inspect) + + def partOne(): Int = lines.collect({ case Left(e) => errorScore(e) }).sum + def partTwo(): Long = + val scores = lines.collect({ case Right(l) => autocomplete(l) }) + scores.sorted.drop(scores.length / 2).head diff --git a/src/main/scala/challenge/Day11.scala b/src/main/scala/challenge/Day11.scala new file mode 100644 index 0000000..94576da --- /dev/null +++ b/src/main/scala/challenge/Day11.scala @@ -0,0 +1,39 @@ +package challenge + +import scala.io.Source +import scala.annotation.tailrec +import lib.Points.Point + +object Day11 { + import lib.GridExtensions._ + + def evolve(xs: Grid[Int]): Grid[Int] = + @tailrec + def helper(prev: Set[Point], grid: Grid[Int]): Grid[Int] = + val flashed = grid.filter(_._2 > 9).map(_._1).toSet -- prev + if flashed.isEmpty then grid.map(kv => (kv._1, if kv._2 > 9 then 0 else kv._2)) + else + val incOne = flashed.toList.flatMap(_.surroundings).groupMapReduce(identity)(_ => 1)(_ + _) + val g2 = grid.map(kv => (kv._1, kv._2 + incOne.getOrElse(kv._1, 0))).toMap + helper(flashed ++ prev, g2) + + helper(Set.empty, xs.map(kv => (kv._1, kv._2 + 1))) + + val input: Grid[Int] = Source.fromResource("day11.txt").mkString.toList.toIntGrid + + def partOne(): Int = + Iterator + .iterate((input, 0))(x => (evolve(x._1), x._2 + x._1.count(_._2 == 0))) + .drop(101) + .next + ._2 + + def partTwo(): Int = + Iterator + .iterate(input)(evolve) + .zipWithIndex + .dropWhile(x => x._1.count(_._2 == 0) != 100) + .next + ._2 + +} diff --git a/src/main/scala/challenge/Day12.scala b/src/main/scala/challenge/Day12.scala new file mode 100644 index 0000000..fd5cd47 --- /dev/null +++ b/src/main/scala/challenge/Day12.scala @@ -0,0 +1,40 @@ +package challenge + +import scala.io.Source +import lib.Graphs + +object Day12: + + type Caves = Map[String, Set[String]] + case class Node(path: List[String], revisited: Boolean = false) + + def neighbors(node: Node): Set[Node] = + caves(node.path.head) + .filter(n => n.forall(_.isUpper) || !node.path.contains(n)) + .map(n => Node(n :: node.path)) + + def neighbors2(node: Node): Set[Node] = node.path match + case h :: t if h != "end" => + caves(h) + .filter(n => n != "start" && (node.revisited || n.forall(_.isUpper) || !t.contains(n))) + .map(n => + val revisit = + if node.revisited && n.forall(_.isLower) && t.contains(n) then false + else node.revisited + Node(n :: node.path, revisit) + ) + case _ => Set.empty + + def search(start: Node)(nf: (Node) => Set[Node]) = + val nodes = Graphs.bfs(start)(nf) + nodes.count(_._1.path.head == "end") + + def parse(input: List[String]): Caves = + val parts = input.map(_.split('-').toList) + val graph = parts.flatMap(x => Seq((x.head, x.last), (x.last, x.head))) + graph.groupMapReduce(_._1)(kv => Set(kv._2))(_ ++ _) + + val caves: Caves = parse(Source.fromResource("day12.txt").getLines().toList) + + def partOne() = search(Node(List("start")))(neighbors) + def partTwo() = search(Node(List("start"), revisited = true))(neighbors2) diff --git a/src/main/scala/challenge/Day13.scala b/src/main/scala/challenge/Day13.scala new file mode 100644 index 0000000..4c66728 --- /dev/null +++ b/src/main/scala/challenge/Day13.scala @@ -0,0 +1,45 @@ +package challenge + +import scala.io.Source +import lib.Points.Point + +object Day13: + import lib.GridExtensions._ + + trait Fold + case class XFold(i: Int) extends Fold + case class YFold(i: Int) extends Fold + + private val pointRegex = """(\d+),(\d+)""".r + def parsePoint(s: String): (Point, Char) = s match + case pointRegex(a, b) => (Point(a.toInt, b.toInt), '#') + + private val foldRegex = """fold along (x|y)=(\d+)""".r + def parseFold(s: String): Fold = s match + case foldRegex("x", x) => XFold(x.toInt) + case foldRegex("y", y) => YFold(y.toInt) + + def foldY(y: Int, grid: Grid[Char]): Grid[Char] = + val (upper, lower) = grid.partition(_._1.y < y) + val max = grid.maxBy(_._1.y)._1.y + val folded = lower.map(kv => (Point(kv._1.x, max - kv._1.y), kv._2)) + folded ++ upper + + def foldX(x: Int, grid: Grid[Char]): Grid[Char] = + val (left, right) = grid.partition(_._1.x < x) + val max = grid.maxBy(_._1.x)._1.x + val folded = right.map(kv => (Point(max - kv._1.x, kv._1.y), kv._2)) + folded ++ left + + def fold(f: Fold, g: Grid[Char]) = f match + case XFold(x) => foldX(x, g) + case YFold(y) => foldY(y, g) + + val input: List[String] = Source.fromResource("day13.txt").getLines().toList + val grid: Grid[Char] = input.takeWhile(_.nonEmpty).map(parsePoint).toMap + val folds: List[Fold] = input.dropWhile(!_.startsWith("fold")).map(parseFold) + + def partOne(): Int = fold(folds.head, grid).count(_._2 == '#') + def partTwo(): Array[Array[Char]] = + val g = folds.foldLeft(grid)((a, b) => fold(b, a)) + g.canvas(' ')(c => if c == '#' then '█' else c) diff --git a/src/main/scala/challenge/Day14.scala b/src/main/scala/challenge/Day14.scala new file mode 100644 index 0000000..96dd4a4 --- /dev/null +++ b/src/main/scala/challenge/Day14.scala @@ -0,0 +1,38 @@ +package challenge + +import scala.io.Source + +object Day14: + + type Count = Map[String, Long] + + private val regex = """(.+) -> (.+)""".r + def parse(s: String): (String, Char) = s match + case regex(a, b) => (a, b.head) + + def insert(rules: Map[String, Char])(pairs: Count): Count = + pairs.toList + .flatMap((pair, count) => + List( + (pair.head.toString + rules(pair), count), + (rules(pair).toString + pair.last.toString, count) + ) + ) + .groupMapReduce(_._1)(_._2)(_ + _) + + def count(c: Char, pairs: Count): Seq[Long] = + val counts = + pairs.toList.map((pair, count) => (pair.last, count)).groupMapReduce(_._1)(_._2)(_ + _) + (counts + (c -> counts.get(c).map(_ + 1).getOrElse(1L))).values.toSeq + + val input = Source.fromResource("day14.txt").getLines().toList + val rules = input.tail.tail.map(parse).toMap + val counts = input.head.sliding(2).toList.groupMapReduce(_.mkString)(_ => 1L)(_ + _) + + def resolve(n: Int) = + val lastCounts = Iterator.iterate(counts)(insert(rules)).drop(n).next + val c = count(input.head.head, lastCounts) + c.max - c.min + + def partOne(): Long = resolve(10) + def partTwo(): Long = resolve(40) diff --git a/src/main/scala/challenge/Day15.scala b/src/main/scala/challenge/Day15.scala new file mode 100644 index 0000000..8ef38cd --- /dev/null +++ b/src/main/scala/challenge/Day15.scala @@ -0,0 +1,30 @@ +package challenge + +import scala.io.Source +import lib.Points.Point +import lib.Graphs + +object Day15: + import lib.GridExtensions._ + + def expand(g: Grid[Int]): Grid[Int] = + val end = g.keys.maxBy(p => p.x * p.y) + val (w, h) = (end.x + 1, end.y + 1) + List + .tabulate(5, 5)((x, y) => + g.map(kv => Point(x * w + kv._1.x, y * h + kv._1.y) -> (1 + (kv._2 - 1 + x + y) % 9)) + ) + .flatten + .flatten + .toMap + + def solve(g: Grid[Int]) = + val goal: Point = Point(g.maxBy(_._1.x)._1.x, g.maxBy(_._1.y)._1.y) + def nf(p: Point) = p.neighbors.filter(g.contains) + def cf(a: Point, b: Point) = g(b) + Graphs.dijkstra(Point.zero, goal)(nf)(cf)._2.get._2 + + val grid = Source.fromResource("day15.txt").mkString.toList.toIntGrid + + def partOne(): Int = solve(grid) + def partTwo(): Int = solve(expand(grid)) diff --git a/src/main/scala/challenge/Day16.scala b/src/main/scala/challenge/Day16.scala new file mode 100644 index 0000000..8d0f9f9 --- /dev/null +++ b/src/main/scala/challenge/Day16.scala @@ -0,0 +1,64 @@ +package challenge + +import scala.io.Source +import lib.Numbers._ + +object Day16: + + trait Packet + case class Literal(version: Long, value: Long) extends Packet + case class Operator(version: Long, typeId: Long, packets: List[Packet]) extends Packet + + def parseLiteral(xs: String, version: Long) = + val (blocks1, blocks0) = xs.drop(6).grouped(5).toList.span(_.head == '1') + val value = (blocks1 :+ blocks0.head).map(_.tail).mkString + val size = 6 + (blocks1.size + 1) * 5 + (size, Literal(version, bin2dec(value))) + + def parseOperator(xs: String, version: Long, typeId: Long, lengthId: Int): (Int, Packet) = + val (size, packets) = lengthId match + case 1 => + val packetCount = bin2dec(xs.drop(7).take(11)).toInt + Iterator + .iterate((7 + 11, List.empty[Packet]))((sz, p) => parse(xs, sz, p)) + .drop(packetCount) + .next() + case _ => + val packetsSize = bin2dec(xs.drop(7).take(15)) + 7 + 15 + Iterator + .iterate((7 + 15, List.empty[Packet]))((sz, p) => parse(xs, sz, p)) + .dropWhile(_._1 < packetsSize) + .next() + (size, Operator(version, typeId, packets)) + + def parse(xs: String, size: Int = 0, packets: List[Packet] = Nil): (Int, List[Packet]) = + val data = xs.drop(size) + val version = bin2dec(data.take(3)) + val typeId = bin2dec(data.drop(3).take(3)) + val (size2, packet2) = + if typeId == 4 then parseLiteral(data, version) + else parseOperator(data, version, typeId, data(6).asDigit) + (size + size2, packets :+ packet2) + + def versionSum(packet: Packet): Long = packet match + case Literal(version, _) => version + case Operator(version, _, packets) => version + packets.map(versionSum).sum + + def expression(packet: Packet): Option[Long] = packet match + case Literal(_, value) => Some(value) + case Operator(_, typeId, packets) => + (typeId, packets.flatMap(expression)) match + case (0, xs) => Some(xs.sum) + case (1, xs) => Some(xs.product) + case (2, xs) => Some(xs.min) + case (3, xs) => Some(xs.max) + case (5, h :: i :: t) => Some(if h > i then 1 else 0) + case (6, h :: i :: t) => Some(if h < i then 1 else 0) + case (7, h :: i :: t) => Some(if h == i then 1 else 0) + case _ => None + + val input: String = hex2bin(Source.fromResource("day16.txt").mkString) + val packet: Packet = parse(input)._2.head + + def partOne(): Long = versionSum(packet) + def partTwo(): Long = expression(packet).get diff --git a/src/main/scala/challenge/Day17.scala b/src/main/scala/challenge/Day17.scala new file mode 100644 index 0000000..6bd41cf --- /dev/null +++ b/src/main/scala/challenge/Day17.scala @@ -0,0 +1,33 @@ +package challenge + +import scala.io.Source +import lib.Points.Point + +object Day17: + case class Probe(location: Point, velocity: Point): + def move(): Probe = + val vx = if velocity.x < 0 then 1 else if velocity.x > 0 then -1 else 0 + Probe(location + velocity, Point(velocity.x + vx, velocity.y - 1)) + case class Target(tl: Point, br: Point): + def didHit(p: Point) = p.x >= tl.x && p.x <= br.x && p.y <= tl.y && p.y >= br.y + def didMiss(p: Point) = p.x > br.x || p.y < br.y + def awaiting(p: Point) = !didHit(p) && !didMiss(p) + + def shoot(p: Probe): Option[Probe] = + val trajectory = Iterator + .iterate(List(p))(ps => ps :+ ps.last.move()) + .dropWhile(ps => target.awaiting(ps.last.location)) + .next + if target.didHit(trajectory.last.location) then Some(trajectory.maxBy(_.location.y)) else None + + private val regex = """.*x=(\d+)..(\d+), y=(-?\d+)..(-?\d+)""".r + def parseTarget(s: String): Target = s match + case regex(x1, x2, y2, y1) => Target(Point(x1.toInt, y1.toInt), Point(x2.toInt, y2.toInt)) + + val input = Source.fromResource("day17.txt").mkString + val target = parseTarget(input) + val trajectories: Seq[Probe] = + for x <- 1 to target.br.x; y <- target.br.y to -target.br.y yield Probe(Point.zero, Point(x, y)) + + def partOne(): Int = trajectories.flatMap(shoot).maxBy(_.location.y).location.y + def partTwo(): Int = trajectories.flatMap(shoot).size diff --git a/src/main/scala/challenge/Day18.scala b/src/main/scala/challenge/Day18.scala new file mode 100644 index 0000000..3cd99f6 --- /dev/null +++ b/src/main/scala/challenge/Day18.scala @@ -0,0 +1,70 @@ +package challenge + +import scala.annotation.tailrec +import scala.io.Source + +object Day18: + case class Number(n: Int, level: Int) + + def parse(s: String): List[Number] = + @tailrec + def helper(xs: List[Char], level: Int, acc: List[Number]): List[Number] = xs match + case Nil => acc + case h :: t if h.isDigit => helper(t, level, acc :+ Number(h.asDigit, level)) + case '[' :: t => helper(t, level + 1, acc) + case ']' :: t => helper(t, level - 1, acc) + case _ :: t => helper(t, level, acc) + + helper(s.toList, -1, Nil) + + def split(xs: List[Number], i: Int): List[Number] = + val num = xs(i) + val (n1, n2) = ((num.n.toDouble / 2).floor.toInt, (num.n.toDouble / 2).ceil.toInt) + xs.take(i) ++ List(Number(n1, num.level + 1), Number(n2, num.level + 1)) ++ xs.drop(i + 1) + + def explode(xs: List[Number], i: Int): List[Number] = i match + case 0 => + val Seq(n2, n3) = xs.tail.take(2) + List(Number(0, 3), Number(n2.n + n3.n, n3.level)) ++ xs.drop(3) + case x if x < xs.length - 2 => + val Seq(n0, n1, n2, n3) = xs.slice(i - 1, i + 3) + val mid = List(Number(n0.n + n1.n, n0.level), Number(0, 3), Number(n2.n + n3.n, n3.level)) + xs.take(x - 1) ++ mid ++ xs.drop(x + 3) + case _ => + val Seq(n1, n2, n3) = xs.takeRight(3) + xs.take(i - 1) ++ List(Number(n1.n + n2.n, 3), Number(0, 3)) + + def reduce(xs: List[Number]): List[Number] = + Iterator + .iterate((xs, false))((xs, _) => + xs.zipWithIndex.find(_._1.level == 4).map(_._2) match + case Some(i) => (explode(xs, i), false) + case None => + xs.zipWithIndex.find(_._1.n > 9).map(_._2) match + case Some(i) => (split(xs, i), false) + case None => (xs, true) + ) + .dropWhile(_._2 == false) + .next + ._1 + + def join(a: List[Number], b: List[Number]): List[Number] = reduce( + (a ++ b).map(num => Number(num.n, num.level + 1)) + ) + + @tailrec + def magnitude(xs: List[Number]): Int = xs match + case h :: Nil => h.n + case _ => + val maxLevel = xs.maxBy(_.level).level + val (head, tail) = xs.span(_.level != maxLevel) + val (n1, n2) = (tail.head.n, tail.tail.head.n) + magnitude((head :+ Number(3 * n1 + 2 * n2, maxLevel - 1)) ++ tail.drop(2)) + + val input: List[List[Number]] = Source.fromResource("day18.txt").getLines().map(parse).toList + + def partOne(): Int = magnitude(input.reduce(join)) + def partTwo(): Int = input + .combinations(2) + .flatMap(_.permutations.map(p => magnitude(join(p.head, p.last)))) + .max diff --git a/src/main/scala/challenge/Day19.scala b/src/main/scala/challenge/Day19.scala new file mode 100644 index 0000000..21eb5ec --- /dev/null +++ b/src/main/scala/challenge/Day19.scala @@ -0,0 +1,103 @@ +package challenge + +import scala.annotation.tailrec +import scala.io.Source + +object Day19: + + type Scanner = Set[Point3] + type BeaconMap = (Scanner, Map[Int, Point3]) + case class Point3(x: Int, y: Int, z: Int): + def +(that: Point3) = Point3(x + that.x, y + that.y, z + that.z) + def -(that: Point3) = Point3(x - that.x, y - that.y, z - that.z) + def manhattan(that: Point3) = ((x - that.x).abs + (y - that.y).abs + (z - that.z).abs) + object Point3: + //https://www.euclideanspace.com/maths/algebra/matrix/transforms/examples/index.htm + def orientations(p: Point3): Seq[Point3] = + val Point3(x, y, z) = p + Seq( + Point3(x, y, z), + Point3(x, -y, -z), + Point3(-x, y, -z), + Point3(-x, -y, z), + Point3(x, z, -y), + Point3(x, -z, y), + Point3(-x, z, y), + Point3(-x, -z, -y), + Point3(y, z, x), + Point3(y, -z, -x), + Point3(-y, z, -x), + Point3(-y, -z, x), + Point3(y, x, -z), + Point3(y, -x, z), + Point3(-y, x, z), + Point3(-y, -x, -z), + Point3(z, x, y), + Point3(z, -x, -y), + Point3(-z, x, -y), + Point3(-z, -x, y), + Point3(z, y, -x), + Point3(z, -y, x), + Point3(-z, y, x), + Point3(-z, -y, -x) + ) + + def orientations(scanner: Scanner): Seq[Scanner] = + scanner.toSeq.map(Point3.orientations).transpose.map(_.toSet) + + def pair(s1: Scanner, s2: Scanner): Option[(Scanner, Point3)] = + (for + orientated <- orientations(s2) + p1 <- s1 + p2 <- orientated + dist = p1 - p2 + if (orientated.map(_ + dist) & s1).size > 11 + yield (orientated, dist)).headOption + + def findBeacons(scanners: Seq[Scanner]): BeaconMap = + @tailrec + def helper( + xs: Seq[(Scanner, Int)], + acc: Scanner, + points: Map[Int, Point3] + ): (Scanner, Map[Int, Point3]) = xs match + case Nil => (acc, points) + case _ => + val (scanner, i, scanner2, point) = xs + .collect(x => + pair(acc, x._1) match + case Some(pair) => Some(x._1, x._2, pair._1, pair._2) + case _ => None + ) + .flatten + .head + + val beacons = acc ++ scanner2.map(_ + point) + val scanners2 = xs.filterNot(_ == (scanner, i)) + val points2 = points + (i -> point) + helper(scanners2, beacons, points2) + + val indexed = scanners.zipWithIndex + helper(indexed.tail, indexed.head._1, Map(0 -> Point3(0, 0, 0))) + + def parse(lines: List[String]): Scanner = + def parse(line: String): Point3 = + val parts = line.split(",").map(_.toInt).toList + Point3(parts.head, parts.tail.head, parts.last) + lines.map(parse).toSet + + def maxDistance(scanners: Map[Int, Point3]): Int = + scanners.values.toList.combinations(2).map(xs => xs.head.manhattan(xs.last)).max + + val input: List[Scanner] = + Source + .fromResource("day19.txt") + .mkString + .split("\n\n") + .toList + .map(_.split("\n").toList.tail) + .map(parse) + lazy val result: BeaconMap = findBeacons(input) + + def partOne(): Int = result._1.size + def partTwo(): Int = maxDistance(result._2) diff --git a/src/main/scala/challenge/Day20.scala b/src/main/scala/challenge/Day20.scala new file mode 100644 index 0000000..28392d7 --- /dev/null +++ b/src/main/scala/challenge/Day20.scala @@ -0,0 +1,45 @@ +package challenge + +import scala.io.Source + +import lib.Points.Point +import lib.GridExtensions._ +import lib.Numbers.bin2dec + +object Day20: + extension (c: Char) def toDigit: Int = if c == '#' then 1 else 0 + + def neighbours(p: Point): Seq[Point] = + for dy <- -1 to 1; dx <- -1 to 1 yield Point(p.x + dx, p.y + dy) + + case class Image(pixels: Map[Point, Int], min: Int, max: Int): + def at(point: Point, default: Int): Int = + bin2dec(neighbours(point).map(pixels.getOrElse(_, default)).mkString).toInt + + def parse(input: List[String]): (List[Int], Image) = + val algorithm = input.head.map(toDigit).toList + val data = input.drop(2) + val pixels = + for y <- 0 until data.size; x <- 0 until data.size yield Point(x, y) -> data(y)(x).toDigit + (algorithm, Image(pixels.toMap, -1, data.size)) + + def step(algorithm: Seq[Int])(default: Int, img: Image): (Int, Image) = + val pixels = for + x <- img.min to img.max + y <- img.min to img.max + yield + val i = img.at(Point(x, y), default) + Point(x, y) -> algorithm(i) + + val nextDefault = if default == 0 then 1 else 0 + val nextGrid = Image(pixels.toMap, img.min - 1, img.max + 1) + (nextDefault, nextGrid) + + def enhance(n: Int): Int = + Iterator.iterate((0, img))(step(algorithm)).drop(n).next()._2.pixels.values.sum + + val input = Source.fromResource("day20.txt").getLines().toList + val (algorithm, img) = parse(input) + + def partOne(): Int = enhance(n = 2) + def partTwo(): Int = enhance(n = 50) diff --git a/src/main/scala/challenge/Day21.scala b/src/main/scala/challenge/Day21.scala new file mode 100644 index 0000000..2903571 --- /dev/null +++ b/src/main/scala/challenge/Day21.scala @@ -0,0 +1,62 @@ +package challenge + +import scala.collection.mutable +import scala.io.Source +import scala.annotation.tailrec + +object Day21: + case class Player(pos: Int, score: Int): + def move(amount: Int): Player = { + val pos2 = (pos + amount - 1) % 10 + 1 + Player(pos2, score + pos2) + } + + def play(p1: Player, p2: Player): (Player, Player, Int) = + + @tailrec + def helper(p1: Player, p2: Player, die: Int): (Player, Player, Int) = + val movement1 = (1 to 3).map(i => (die + i) % 100).sum + val movement2 = (4 to 6).map(i => (die + i) % 100).sum + val (next1, next2) = (p1.move(movement1), p2.move(movement2)) + if next1.score > 999 then (next1, p2, die + 3) + else if next2.score > 999 then (next2, next1, die + 6) + else helper(next1, next2, die + 6) + + helper(p1, p2, 0) + + def play2(p1: Player, p2: Player): (Long, Long) = + val memo = mutable.Map.empty[(Player, Player), (Long, Long)] + val frequencies = + (for + r1 <- 1 to 3 + r2 <- 1 to 3 + r3 <- 1 to 3 + yield r1 + r2 + r3).groupMapReduce(identity)(_ => 1)(_ + _) + + def helper(p1: Player, p2: Player): (Long, Long) = + memo.getOrElseUpdate( + (p1, p2), + if (p1.score > 20) then (1L, 0L) + else if (p2.score > 20) then (0L, 1L) + else + val rolls = + for + (roll, rollCount) <- frequencies + next1 = p1.move(roll) + (p2Wins, p1Wins) = helper(p2, next1) + yield (rollCount * p1Wins, rollCount * p2Wins) + rolls.reduce({ case ((a, b), (c, d)) => (a + c, b + d) }) + ) + + helper(p1, p2) + + val input = Source.fromResource("day21.txt").getLines().toList + val (pos1, pos2) = (input.head.last.asDigit, input.last.last.asDigit) + + def partOne(): Int = + val (_, loser, rolls) = play(Player(pos1, 0), Player(pos2, 0)) + loser.score * rolls + + def partTwo(): Long = + val (s1, s2) = play2(Player(pos1, 0), Player(pos2, 0)) + s1 max s2 diff --git a/src/main/scala/challenge/Day22.scala b/src/main/scala/challenge/Day22.scala new file mode 100644 index 0000000..b452e8e --- /dev/null +++ b/src/main/scala/challenge/Day22.scala @@ -0,0 +1,97 @@ +package challenge + +import scala.io.Source +import scala.annotation.tailrec +import scala.collection.immutable.Range.Inclusive + +object Day22: + + extension (r: Inclusive) + def volume(): Long = 1 + r.last - r.head + def intersection(other: Inclusive): Option[Inclusive] = + val (min, max) = (r.head, r.last) + val (min2, max2) = (other.head, other.last) + if min2 <= min && max <= max2 then Some(r) + else if min < min2 && max2 < max then Some(other) + else if min2 <= min && min <= max2 then Some(min to max2) + else if min <= min2 && min2 <= max then Some(min2 to max) + else None + + def split(other: Inclusive): List[Inclusive] = + val (min, max) = (r.head, r.last) + val (min2, max2) = (other.head, other.last) + if min2 <= min && max <= max2 then List(r) + else if min < min2 && max2 < max then List(min2 to max2, min to min2 - 1, max2 + 1 to max) + else if min <= max2 && max2 < max then List(min to max2, max2 + 1 to max) + else if min < min2 && min2 <= max then List(min2 to max2, min to min2 - 1) + else Nil + + case class Cuboid(x: Inclusive, y: Inclusive, z: Inclusive): + def volume: Long = x.volume() * y.volume() * z.volume() + def intersection(other: Cuboid): Option[Cuboid] = + for + xs <- x.intersection(other.x) + ys <- y.intersection(other.y) + zs <- z.intersection(other.z) + yield (Cuboid(xs, ys, zs)) + + def split(other: Cuboid): List[Cuboid] = + val (hx :: tx) = x.split(other.x) + val (hy :: ty) = y.split(other.y) + val (hz :: tz) = z.split(other.z) + tx.map(Cuboid(_, y, z)) ++ ty.map(Cuboid(hx, _, z)) ++ tz.map(Cuboid(hx, hy, _)) + + trait Instruction: + val cuboid: Cuboid + case class TurnOn(cuboid: Cuboid) extends Instruction + case class TurnOff(cuboid: Cuboid) extends Instruction + + private val regex = """(on|off) x=(-?\d+)..(-?\d+),y=(-?\d+)..(-?\d+),z=(-?\d+)..(-?\d+)""".r + def parse(s: String): Instruction = s match + case regex("on", x1, x2, y1, y2, z1, z2) => + TurnOn(Cuboid(x1.toInt to x2.toInt, y1.toInt to y2.toInt, z1.toInt to z2.toInt)) + case regex("off", x1, x2, y1, y2, z1, z2) => + TurnOff(Cuboid(x1.toInt to x2.toInt, y1.toInt to y2.toInt, z1.toInt to z2.toInt)) + + def process(xs: List[Instruction]): Set[(Int, Int, Int)] = + val bounds = Cuboid(-50 to 50, -50 to 50, -50 to 50) + @tailrec + def helper(xs: List[Instruction], acc: Set[(Int, Int, Int)]): Set[(Int, Int, Int)] = xs match + case Nil => acc + case h :: t => + h.cuboid.intersection(bounds) match + case Some(c) => + val next = for x <- c.x; y <- c.y; z <- c.z yield (x, y, z) + h match + case TurnOn(_) => helper(t, acc ++ next) + case TurnOff(_) => helper(t, acc -- next) + case None => helper(t, acc) + + helper(xs, Set.empty) + + def process2(xs: List[Instruction]): List[Cuboid] = + @tailrec + def helper(xs: List[Instruction], acc: List[Cuboid]): List[Cuboid] = xs match + case Nil => acc + case h :: t => + h match + case TurnOn(c) => + acc.view.flatMap(_.intersection(c)).headOption match + case None => helper(t, c :: acc) + case Some(i) => + val remaining = h.cuboid.split(i).map(TurnOn(_)) + helper(remaining ++ t, acc) + case TurnOff(c) => + val next = acc.flatMap(n => + n.intersection(h.cuboid) match + case None => List(n) + case Some(i) => n.split(i) + ) + helper(t, next) + + helper(xs, Nil) + + val input = Source.fromResource("day22.txt").getLines().map(parse).toList + + def partOne(): Int = process(input).size + def partTwo(): Long = process2(input).map(_.volume).sum diff --git a/src/main/scala/challenge/Day24.scala b/src/main/scala/challenge/Day24.scala new file mode 100644 index 0000000..0d99aea --- /dev/null +++ b/src/main/scala/challenge/Day24.scala @@ -0,0 +1,38 @@ +package challenge + +import scala.io.Source + +object Day24: + + def findModelNumberStartingFrom(num: String, input: List[String]): Long = + val blocks = input.grouped(18).toSeq + def operand(i: Int)(n: Int) = blocks(i).map(_.split(' ').last)(n).toInt + + def step(acc: List[Int], stack: List[(Int, Int)], i: Int): (List[Int], List[(Int, Int)]) = + // indexes 4, 5 and 15 are where things differentiate for each block + val Seq(div, add1, add2) = Seq(4, 5, 15).map(operand(i)) + if div == 1 then (acc, stack :+ (i, add2)) + else + val (j, added) = stack.last + val acc2 = acc.updated(i, acc(j) + add1 + added) + val res = acc2(i) match + case x if x > 9 => acc2.updated(j, acc2(j) - x + 9).updated(i, 9) + case x if x < 1 => acc2.updated(j, acc2(j) - x + 1).updated(i, 1) + case _ => acc2 + (res, stack.init) + + Iterator + .iterate((num.toList.map(_.asDigit), List.empty[(Int, Int)], 0))((res, stack, i) => + val (r2, s2) = step(res, stack, i) + (r2, s2, i + 1) + ) + .drop(14) + .next + ._1 + .mkString + .toLong + + val input = Source.fromResource("day24.txt").getLines().toList + + def partOne(): Long = findModelNumberStartingFrom("99999999999999", input) + def partTwo(): Long = findModelNumberStartingFrom("11111111111111", input) diff --git a/src/main/scala/challenge/Day25.scala b/src/main/scala/challenge/Day25.scala new file mode 100644 index 0000000..26d6bca --- /dev/null +++ b/src/main/scala/challenge/Day25.scala @@ -0,0 +1,26 @@ +package challenge + +import scala.io.Source +import lib.GridExtensions._ +import lib.Points.Point + +object Day25: + + def moved(g: Grid[Char], c: Char)(next: (Point => Point)): Grid[Char] = + val (movable, unmovable) = g.filter(_._2 == c).partition(p => g(next(p._1)) == '.') + g.filterNot(_._2 == c) ++ unmovable ++ movable.map(kv => next(kv._1) -> kv._2) + + def step(g: Grid[Char]): Grid[Char] = + val movedRight = moved(g, '>')(p => Point((p.x + 1) % maxX, p.y)) + moved(movedRight, 'v')(p => Point(p.x, (p.y + 1) % maxY)) + + val grid = Source.fromResource("day25.txt").mkString.toList.toGrid.withDefaultValue('.') + val (maxX, maxY) = (grid.maxBy(_._1.x)._1.x + 1, grid.maxBy(_._1.y)._1.y + 1) + + def partOne(): Int = Iterator + .iterate((grid, 1))((g, i) => (step(g), i + 1)) + .sliding(2) + .dropWhile(x => x.head._1 != x.last._1) + .next + .head + ._2 diff --git a/src/main/scala/lib/Graphs.scala b/src/main/scala/lib/Graphs.scala new file mode 100644 index 0000000..e88f971 --- /dev/null +++ b/src/main/scala/lib/Graphs.scala @@ -0,0 +1,67 @@ +package lib + +import scala.annotation.tailrec +import scala.collection.mutable + +object Graphs: + object dfs: + def apply[A](start: A)(nf: A => Iterable[A]): Iterable[A] = + def _dfs(s: A, seen: Iterable[A]): Iterable[A] = + if seen.iterator.contains(s) then seen + else + val neighbors = nf(s).filterNot(seen.iterator.contains) + neighbors.foldLeft(Iterable(s) ++ seen)((b, a) => _dfs(a, b)) + + _dfs(start, Nil) + + object bfs: + def apply[A](start: A)(nf: A => Iterable[A]): Map[A, Int] = + @tailrec + def _bfs(seen: Map[A, Int], unseen: Map[A, Int]): Map[A, Int] = + val neighbors = for { + (node, cost) <- unseen; newNode <- nf(node) + } yield newNode -> (cost + 1) + val seen2 = seen ++ unseen + val unseen2 = neighbors.filterNot(n => seen.contains(n._1)) + if unseen2.isEmpty then seen2 else _bfs(seen2, unseen2) + + _bfs(Map.empty, Map(start -> 0)) + + object aStar: + def apply[A](start: A, goal: A)(cf: (A, A) => Int)(nf: A => Iterable[A])( + hf: A => Int + ): (Map[A, Int], Option[(A, Int)]) = + val seen: mutable.Map[A, Int] = mutable.Map.empty + val unseen: mutable.PriorityQueue[(Int, Int, A)] = + mutable.PriorityQueue.empty(Ordering.by(-_._1)) + unseen.enqueue((hf(start), 0, start)) + while unseen.nonEmpty do + val (_, dist, node) = unseen.dequeue() + if !seen.contains(node) then + seen(node) = dist + if node == goal then return (seen.toMap, Some(node -> dist)) + else + def visit(n: A, d: Int) = + if !seen.contains(n) then unseen.enqueue((dist + d + hf(n), dist + d, n)) + nf(node).map(n => (n, cf(node, n))).foreach(n => visit(n._1, n._2)) + + (seen.toMap, None) + + object dijkstra: + def apply[A](start: A, goal: A)(nf: A => Iterable[A])( + cf: (A, A) => Int + ): (Map[A, Int], Option[(A, Int)]) = + val seen: mutable.Map[A, Int] = mutable.Map.empty + val unseen: mutable.PriorityQueue[(Int, A)] = mutable.PriorityQueue.empty(Ordering.by(-_._1)) + unseen.enqueue((0, start)) + while unseen.nonEmpty do + val (dist, node) = unseen.dequeue() + if !seen.contains(node) then + seen(node) = dist + if node == goal then return (seen.toMap, Some(node -> dist)) + else + def visit(n: A, d: Int) = + if !seen.contains(n) then unseen.enqueue((dist + d, n)) + nf(node).map(n => (n, cf(node, n))).foreach(n => visit(n._1, n._2)) + + (seen.toMap, None) diff --git a/src/main/scala/lib/GridExtensions.scala b/src/main/scala/lib/GridExtensions.scala new file mode 100644 index 0000000..c01989a --- /dev/null +++ b/src/main/scala/lib/GridExtensions.scala @@ -0,0 +1,29 @@ +package lib + +import lib.Points.Point + +import scala.annotation.tailrec +import scala.reflect.ClassTag + +object GridExtensions: + + type Grid[A] = Map[Point, A] + + extension [A](grid: Grid[A]) + def canvas(default: A)(cf: A => A)(using classTag: ClassTag[A]): Array[Array[A]] = + val (x, y) = (grid.keys.maxBy(_.x).x, grid.keys.maxBy(_.y).y) + val canvas = Array.tabulate(y + 1, x + 1)((_, _) => default) + for p <- grid yield canvas(p._1.y)(p._1.x) = cf(p._2) + canvas + + private def makeGrid[A](input: Seq[Char])(fn: (Char => A)): Grid[A] = + @tailrec + def helper(xs: Seq[Char], acc: Grid[A], current: Point): Grid[A] = + xs match + case h :: t if h == '\n' => helper(t, acc, Point(0, current.y + 1)) + case h :: t => helper(t, acc.updated(current, fn(h)), Point(current.x + 1, current.y)) + case _ => acc + helper(input, Map.empty, Point(0, 0)) + + extension (input: Seq[Char]) def toGrid: Grid[Char] = makeGrid(input)(identity) + extension (input: Seq[Char]) def toIntGrid: Grid[Int] = makeGrid(input)(_.asDigit) diff --git a/src/main/scala/lib/Math.scala b/src/main/scala/lib/Math.scala new file mode 100644 index 0000000..8ec4e7c --- /dev/null +++ b/src/main/scala/lib/Math.scala @@ -0,0 +1,18 @@ +package lib + +object Math: + + def mean(seq: Seq[Int]): Double = seq.sum / seq.length.toDouble + + def median(seq: Seq[Int]): Int = + val sorted = seq.sorted + if seq.size % 2 == 1 then sorted(seq.size / 2) + else + val (up, down) = sorted.splitAt(seq.size / 2) + (up.last + down.head) / 2 + + def arithmeticSeries(a: Int, b: Int, step: Int = 1, normalized: Boolean = false): Int = + val dist = (a - b).abs + val (a1, an) = if normalized then (0, dist) else (a, b) + val n = dist / step + 1 + n * (a1 + an) / 2 diff --git a/src/main/scala/lib/Numbers.scala b/src/main/scala/lib/Numbers.scala new file mode 100644 index 0000000..20700be --- /dev/null +++ b/src/main/scala/lib/Numbers.scala @@ -0,0 +1,10 @@ +package lib + +object Numbers { + def leftPad(l: Int, s: String, c: Char = '0'): String = List.fill(l - s.length)(c).mkString + s + def hex2bin(hex: Char): String = + val bin = Integer.parseInt(hex.toString, 16).toBinaryString + leftPad(4, bin) + def hex2bin(hex: String): String = hex.flatMap(hex2bin(_)) + def bin2dec(bin: String): Long = BigInt(bin, 2).longValue +} diff --git a/src/main/scala/lib/Parsers.scala b/src/main/scala/lib/Parsers.scala new file mode 100644 index 0000000..a4486c9 --- /dev/null +++ b/src/main/scala/lib/Parsers.scala @@ -0,0 +1,80 @@ +package lib + +import scala.annotation.tailrec + +object Parsers: + type Op = Char + + object AST: + trait Node + case class Value(v: Int) extends Node + case class Tree(op: Op, left: Node, right: Node) extends Node + + def eval(node: Node): Long = node match + case Tree(op, l, r) => + op match + case '+' => eval(l) + eval(r) + case '-' => eval(l) - eval(r) + case '*' => eval(l) * eval(r) + case '/' => eval(l) / eval(r) + case Value(op) => op.toLong + + def parse(expr: String)(precedence: (Op, Op) => Boolean): Node = + + def compose(nodes: List[Node], ops: List[Op]): List[Node] = + ops.foldLeft(nodes)((ns, op) => Tree(op, ns.tail.head, ns.head) +: ns.tail.tail) + + @tailrec + def helper(xs: List[Char], ops: List[Op], acc: List[Node]): Node = xs match + case Nil => compose(acc, ops).head + case h :: t => + h match + case n if n.isDigit => helper(t, ops, Value(n.asDigit) +: acc) + case '(' => helper(t, h +: ops, acc) + case ')' => + val (out, ops2) = ops.span(_ != '(') + helper(t, ops2.tail, compose(acc, out)) + case _ => + val (out, ops2) = ops.span(o => o != '(' && precedence(o, h)) + helper(t, h +: ops2, compose(acc, out)) + + helper(expr.filterNot(_ == ' ').toList, Nil, Nil) + + object Postfix: + + def eval(expr: List[Char]): Long = + + @tailrec + def helper(xs: List[Char], stack: List[Long]): Long = xs match + case Nil => stack.head + case h :: t => + h match + case n if n.isDigit => helper(t, n.asDigit.toLong +: stack) + case _ => + val (right, left) = (stack.head, stack.tail.head) + val v = h match + case '+' => left + right + case '-' => left - right + case '*' => left * right + case '/' => left / right + helper(t, v +: stack.drop(2)) + + helper(expr, Nil) + + def parse(expr: String)(precedence: (Op, Op) => Boolean): List[Char] = + + @tailrec + def helper(xs: List[Char], ops: List[Op], acc: List[Char]): List[Char] = xs match + case Nil => acc ++ ops + case h :: t => + h match + case n if n.isDigit => helper(t, ops, acc :+ n) + case '(' => helper(t, h +: ops, acc) + case ')' => + val (out, ops2) = ops.span(_ != '(') + helper(t, ops2.tail, acc ++ out) + case _ => + val (out, ops2) = ops.span(o => o != '(' && precedence(o, h)) + helper(t, h +: ops2, acc ++ out) + + helper(expr.filterNot(_ == ' ').toList, Nil, Nil) diff --git a/src/main/scala/lib/Points.scala b/src/main/scala/lib/Points.scala new file mode 100644 index 0000000..c328b8e --- /dev/null +++ b/src/main/scala/lib/Points.scala @@ -0,0 +1,71 @@ +package lib + +object Points: + + case class Point(x: Int, y: Int): + def +(p: Point): Point = Point(x + p.x, y + p.y) + def -(p: Point): Point = Point(x - p.y, y - p.y) + def *(n: Int): Point = Point(x * n, y * n) + def neighbors: List[Point] = + List(Point(x, y - 1), Point(x + 1, y), Point(x, y + 1), Point(x - 1, y)) + def corners: List[Point] = + List(Point(x - 1, y - 1), Point(x + 1, y - 1), Point(x - 1, y + 1), Point(x + 1, y + 1)) + def surroundings: List[Point] = neighbors ++ corners + def rotate(deg: Int): Point = deg % 360 match + case 90 | -270 => Point(y, -x) + case 180 | -180 => Point(-x, -y) + case -90 | 270 => Point(-y, x) + case _ => this + + def manhattan(p: Point = Position.zero): Int = (p.x - x).abs + (p.y - y).abs + def directionTo(other: Point): Char = other match + case Point(x2, y2) if x2 == x && y2 < y => 'N' + case Point(x2, y2) if x2 == x && y2 > y => 'S' + case Point(x2, y2) if y2 == y && x2 < x => 'W' + case Point(x2, y2) if y2 == y && x2 > x => 'E' + case _ => throw new IllegalArgumentException("Points are equal") + + object Point: + val zero: Point = Point(0, 0) + + case class Line(p1: Point, p2: Point): + val (dx, dy) = ((p2.x - p1.x).sign, (p2.y - p1.y).sign) + def points(): Seq[Point] = + val max = math.max((p2.x - p1.x).abs, (p2.y - p1.y).abs) + (0 to max).map(i => Point(p1.x + dx * i, p1.y + dy * i)) + + object Position: + def zero: Point = Point(0, 0) + def neighbors: List[Point] = List(Point(1, 0), Point(-1, 0), Point(0, 1), Point(0, -1)) + def corners: List[Point] = List(Point(1, 1), Point(-1, 1), Point(1, -1), Point(-1, -1)) + def surroundings: List[Point] = neighbors ++ corners + def directions: Map[Char, Point] = List('E', 'W', 'N', 'S').zip(neighbors).toMap + + case class Dir(p: Point, dir: Char) { + def forward(n: Int = 1): Dir = dir match + case 'U' | 'N' => copy(p = p.copy(y = p.y - n)) + case 'D' | 'S' => copy(p = p.copy(y = p.y + n)) + case 'L' | 'W' => copy(p = p.copy(x = p.x - n)) + case 'R' | 'E' => copy(p = p.copy(x = p.x + n)) + + def rotate(clockwise: Boolean = true, n: Int = 0): Dir = dir match // turn and move + case 'U' => if (clockwise) Dir(Point(p.x + n, p.y), 'R') else Dir(Point(p.x - n, p.y), 'L') + case 'D' => if (clockwise) Dir(Point(p.x - n, p.y), 'L') else Dir(Point(p.x + n, p.y), 'R') + case 'L' => if (clockwise) Dir(Point(p.x, p.y - n), 'U') else Dir(Point(p.x, p.y + n), 'D') + case 'R' => if (clockwise) Dir(Point(p.x, p.y + n), 'D') else Dir(Point(p.x, p.y - n), 'U') + + def turn(d: Char): Dir = copy(dir = d) + def turn(degrees: Int): Dir = + val dirs = Seq('U', 'R', 'D', 'L') + val deg = (degrees.abs / 90) % 360 + val i = dirs.indexOf(dir) + (if (degrees < 0) dirs.length - deg else deg) + copy(dir = dirs(i % dirs.length)) + + } + + case class Box(min: Point, max: Point): + def iterator: Iterator[Point] = + for + x <- (min.x to max.x).iterator + y <- (min.y to max.y).iterator + yield Point(x, y) diff --git a/src/test/scala/Test01.scala b/src/test/scala/Test01.scala new file mode 100644 index 0000000..9754ce6 --- /dev/null +++ b/src/test/scala/Test01.scala @@ -0,0 +1,9 @@ +import org.scalatest._ +import challenge.Day01 + +class Test01 extends flatspec.AnyFlatSpec with matchers.should.Matchers: + + it should "calculate correct result" in { + Day01.partOne() should be(1184) + Day01.partTwo() should be(1158) + } diff --git a/src/test/scala/Test02.scala b/src/test/scala/Test02.scala new file mode 100644 index 0000000..bd19ac5 --- /dev/null +++ b/src/test/scala/Test02.scala @@ -0,0 +1,9 @@ +import org.scalatest._ +import challenge.Day02 + +class Test02 extends flatspec.AnyFlatSpec with matchers.should.Matchers: + + it should "calculate correct result" in { + Day02.partOne() should be(1962940) + Day02.partTwo() should be(1813664422) + } diff --git a/src/test/scala/Test03.scala b/src/test/scala/Test03.scala new file mode 100644 index 0000000..ff9d7a3 --- /dev/null +++ b/src/test/scala/Test03.scala @@ -0,0 +1,9 @@ +import org.scalatest._ +import challenge.Day03 + +class Test03 extends flatspec.AnyFlatSpec with matchers.should.Matchers: + + it should "calculate correct result" in { + Day03.partOne() should be(1997414) + Day03.partTwo() should be(1032597) + } diff --git a/src/test/scala/Test04.scala b/src/test/scala/Test04.scala new file mode 100644 index 0000000..3553d68 --- /dev/null +++ b/src/test/scala/Test04.scala @@ -0,0 +1,9 @@ +import org.scalatest._ +import challenge.Day04 + +class Test04 extends flatspec.AnyFlatSpec with matchers.should.Matchers: + + it should "calculate correct result" in { + Day04.partOne() should be(8580) + Day04.partTwo() should be(9576) + } diff --git a/src/test/scala/Test05.scala b/src/test/scala/Test05.scala new file mode 100644 index 0000000..9ae7e98 --- /dev/null +++ b/src/test/scala/Test05.scala @@ -0,0 +1,9 @@ +import org.scalatest._ +import challenge.Day05 + +class Test05 extends flatspec.AnyFlatSpec with matchers.should.Matchers: + + it should "calculate correct result" in { + Day05.partOne() should be(5442) + Day05.partTwo() should be(19571) + } diff --git a/src/test/scala/Test06.scala b/src/test/scala/Test06.scala new file mode 100644 index 0000000..bf2c2ce --- /dev/null +++ b/src/test/scala/Test06.scala @@ -0,0 +1,9 @@ +import org.scalatest._ +import challenge.Day06 + +class Test06 extends flatspec.AnyFlatSpec with matchers.should.Matchers: + + it should "calculate correct result" in { + Day06.partOne() should be(387413) + Day06.partTwo() should be(1738377086345L) + } diff --git a/src/test/scala/Test07.scala b/src/test/scala/Test07.scala new file mode 100644 index 0000000..0dc5e78 --- /dev/null +++ b/src/test/scala/Test07.scala @@ -0,0 +1,9 @@ +import org.scalatest._ +import challenge.Day07 + +class Test07 extends flatspec.AnyFlatSpec with matchers.should.Matchers: + + it should "calculate correct result" in { + Day07.partOne() should be(329389) + Day07.partTwo() should be(86397080) + } diff --git a/src/test/scala/Test08.scala b/src/test/scala/Test08.scala new file mode 100644 index 0000000..f0781e1 --- /dev/null +++ b/src/test/scala/Test08.scala @@ -0,0 +1,9 @@ +import org.scalatest._ +import challenge.Day08 + +class Test08 extends flatspec.AnyFlatSpec with matchers.should.Matchers: + + it should "calculate correct result" in { + //Day08.partOne() should be(532) + Day08.partTwo() should be(1011284) + } diff --git a/src/test/scala/Test09.scala b/src/test/scala/Test09.scala new file mode 100644 index 0000000..25839a5 --- /dev/null +++ b/src/test/scala/Test09.scala @@ -0,0 +1,9 @@ +import org.scalatest._ +import challenge.Day09 + +class Test09 extends flatspec.AnyFlatSpec with matchers.should.Matchers: + + it should "calculate correct result" in { + Day09.partOne() should be(564) + Day09.partTwo() should be(1038240) + } diff --git a/src/test/scala/Test10.scala b/src/test/scala/Test10.scala new file mode 100644 index 0000000..acc0d78 --- /dev/null +++ b/src/test/scala/Test10.scala @@ -0,0 +1,9 @@ +import org.scalatest._ +import challenge.Day10 + +class Test10 extends flatspec.AnyFlatSpec with matchers.should.Matchers: + + it should "calculate correct result" in { + Day10.partOne() should be(369105) + Day10.partTwo() should be(3999363569L) + } diff --git a/src/test/scala/Test11.scala b/src/test/scala/Test11.scala new file mode 100644 index 0000000..410310e --- /dev/null +++ b/src/test/scala/Test11.scala @@ -0,0 +1,9 @@ +import org.scalatest._ +import challenge.Day11 + +class Test11 extends flatspec.AnyFlatSpec with matchers.should.Matchers: + + it should "calculate correct result" in { + Day11.partOne() should be(1655) + Day11.partTwo() should be(337) + } diff --git a/src/test/scala/Test12.scala b/src/test/scala/Test12.scala new file mode 100644 index 0000000..2327c70 --- /dev/null +++ b/src/test/scala/Test12.scala @@ -0,0 +1,9 @@ +import org.scalatest._ +import challenge.Day12 + +class Test12 extends flatspec.AnyFlatSpec with matchers.should.Matchers: + + it should "calculate correct result" in { + Day12.partOne() should be(4659) + Day12.partTwo() should be(148962) + } diff --git a/src/test/scala/Test13.scala b/src/test/scala/Test13.scala new file mode 100644 index 0000000..dec85ec --- /dev/null +++ b/src/test/scala/Test13.scala @@ -0,0 +1,15 @@ +import org.scalatest._ +import challenge.Day13 + +class Test13 extends flatspec.AnyFlatSpec with matchers.should.Matchers: + + it should "calculate correct result" in { + Day13.partOne() should be(704) + } + + it should "display HGAJBEHC" in { + val canvas = Day13.partTwo() + canvas foreach { row => + row.mkString foreach print; println() + } + } diff --git a/src/test/scala/Test14.scala b/src/test/scala/Test14.scala new file mode 100644 index 0000000..186f50f --- /dev/null +++ b/src/test/scala/Test14.scala @@ -0,0 +1,9 @@ +import org.scalatest._ +import challenge.Day14 + +class Test14 extends flatspec.AnyFlatSpec with matchers.should.Matchers: + + it should "calculate correct result" in { + Day14.partOne() should be(2584) + Day14.partTwo() should be(3816397135460L) + } diff --git a/src/test/scala/Test15.scala b/src/test/scala/Test15.scala new file mode 100644 index 0000000..ef98450 --- /dev/null +++ b/src/test/scala/Test15.scala @@ -0,0 +1,9 @@ +import org.scalatest._ +import challenge.Day15 + +class Test15 extends flatspec.AnyFlatSpec with matchers.should.Matchers: + + it should "calculate correct result" in { + Day15.partOne() should be(790) + Day15.partTwo() should be(2998) + } diff --git a/src/test/scala/Test16.scala b/src/test/scala/Test16.scala new file mode 100644 index 0000000..7bf7a21 --- /dev/null +++ b/src/test/scala/Test16.scala @@ -0,0 +1,9 @@ +import org.scalatest._ +import challenge.Day16 + +class Test16 extends flatspec.AnyFlatSpec with matchers.should.Matchers: + + it should "calculate correct result" in { + Day16.partOne() should be(955) + Day16.partTwo() should be(158135423448L) + } diff --git a/src/test/scala/Test17.scala b/src/test/scala/Test17.scala new file mode 100644 index 0000000..4949269 --- /dev/null +++ b/src/test/scala/Test17.scala @@ -0,0 +1,9 @@ +import org.scalatest._ +import challenge.Day17 + +class Test17 extends flatspec.AnyFlatSpec with matchers.should.Matchers: + + it should "calculate correct result" in { + Day17.partOne() should be(10878) + Day17.partTwo() should be(4716) + } diff --git a/src/test/scala/Test18.scala b/src/test/scala/Test18.scala new file mode 100644 index 0000000..9859de1 --- /dev/null +++ b/src/test/scala/Test18.scala @@ -0,0 +1,9 @@ +import org.scalatest._ +import challenge.Day18 + +class Test18 extends flatspec.AnyFlatSpec with matchers.should.Matchers: + + it should "calculate correct result" in { + Day18.partOne() should be(3524) + Day18.partTwo() should be(4656) + } diff --git a/src/test/scala/Test19.scala b/src/test/scala/Test19.scala new file mode 100644 index 0000000..99045f4 --- /dev/null +++ b/src/test/scala/Test19.scala @@ -0,0 +1,9 @@ +import org.scalatest._ +import challenge.Day19 + +class Test19 extends flatspec.AnyFlatSpec with matchers.should.Matchers: + + it should "calculate correct result" in { + Day19.partOne() should be(491) + Day19.partTwo() should be(13374) + } diff --git a/src/test/scala/Test20.scala b/src/test/scala/Test20.scala new file mode 100644 index 0000000..2dbdb4e --- /dev/null +++ b/src/test/scala/Test20.scala @@ -0,0 +1,9 @@ +import org.scalatest._ +import challenge.Day20 + +class Test20 extends flatspec.AnyFlatSpec with matchers.should.Matchers: + + it should "calculate correct result" in { + Day20.partOne() should be(5354) + Day20.partTwo() should be(18269) + } diff --git a/src/test/scala/Test21.scala b/src/test/scala/Test21.scala new file mode 100644 index 0000000..28f303e --- /dev/null +++ b/src/test/scala/Test21.scala @@ -0,0 +1,9 @@ +import org.scalatest._ +import challenge.Day21 + +class Test21 extends flatspec.AnyFlatSpec with matchers.should.Matchers: + + it should "calculate correct result" in { + Day21.partOne() should be(604998) + Day21.partTwo() should be(157253621231420L) + } diff --git a/src/test/scala/Test22.scala b/src/test/scala/Test22.scala new file mode 100644 index 0000000..eb2223e --- /dev/null +++ b/src/test/scala/Test22.scala @@ -0,0 +1,9 @@ +import org.scalatest._ +import challenge.Day22 + +class Test22 extends flatspec.AnyFlatSpec with matchers.should.Matchers: + + it should "calculate correct result" in { + Day22.partOne() should be(503864) + Day22.partTwo() should be(1255547543528356L) + } diff --git a/src/test/scala/Test24.scala b/src/test/scala/Test24.scala new file mode 100644 index 0000000..d1cca77 --- /dev/null +++ b/src/test/scala/Test24.scala @@ -0,0 +1,9 @@ +import org.scalatest._ +import challenge.Day24 + +class Test24 extends flatspec.AnyFlatSpec with matchers.should.Matchers: + + it should "calculate correct result" in { + Day24.partOne() should be(99394899891971L) + Day24.partTwo() should be(92171126131911L) + } diff --git a/src/test/scala/Test25.scala b/src/test/scala/Test25.scala new file mode 100644 index 0000000..ef80100 --- /dev/null +++ b/src/test/scala/Test25.scala @@ -0,0 +1,8 @@ +import org.scalatest._ +import challenge.Day25 + +class Test25 extends flatspec.AnyFlatSpec with matchers.should.Matchers: + + it should "calculate correct result" in { + Day25.partOne() should be(378) + }