├── project └── build.properties ├── .gitignore ├── .scalafmt.conf ├── src ├── main │ ├── resources │ │ └── synacor │ │ │ └── challenge.bin │ └── scala │ │ ├── 2015 │ │ ├── Day01.worksheet.sc │ │ ├── Day09.worksheet.sc │ │ ├── Day04.worksheet.sc │ │ ├── Day02.worksheet.sc │ │ ├── Day17.worksheet.sc │ │ ├── Day08.worksheet.sc │ │ ├── Day13.worksheet.sc │ │ ├── Day03.worksheet.sc │ │ ├── Day10.worksheet.sc │ │ ├── Day18.worksheet.sc │ │ ├── Day14.worksheet.sc │ │ ├── Day19.worksheet.sc │ │ ├── Day11.worksheet.sc │ │ ├── Day05.worksheet.sc │ │ ├── Day16.worksheet.sc │ │ ├── Day06.worksheet.sc │ │ ├── Day15.worksheet.sc │ │ └── Day07.worksheet.sc │ │ ├── 2018 │ │ ├── Day01.worksheet.sc │ │ ├── Day02.worksheet.sc │ │ ├── Day03.worksheet.sc │ │ ├── Day05.worksheet.sc │ │ └── Day04.worksheet.sc │ │ ├── 2019 │ │ ├── Day02.worksheet.sc │ │ └── Day05.worksheet.sc │ │ ├── 2020 │ │ ├── Day01.worksheet.sc │ │ ├── Day02.worksheet.sc │ │ ├── Day06.worksheet.sc │ │ ├── Day05.worksheet.sc │ │ ├── Day09.worksheet.sc │ │ ├── Day10.worksheet.sc │ │ ├── Day03.worksheet.sc │ │ ├── Day15.worksheet.sc │ │ ├── Day08.worksheet.sc │ │ ├── Day19.worksheet.sc │ │ ├── Day07.worksheet.sc │ │ ├── Day13.worksheet.sc │ │ ├── Day16.worksheet.sc │ │ ├── Day17.worksheet.sc │ │ ├── Day04.worksheet.sc │ │ ├── Day14.worksheet.sc │ │ ├── Day12.worksheet.sc │ │ ├── Day18.worksheet.sc │ │ └── Day11.worksheet.sc │ │ ├── 2021 │ │ ├── Day01.worksheet.sc │ │ ├── Day07.worksheet.sc │ │ ├── Day02.worksheet.sc │ │ ├── Day06.worksheet.sc │ │ ├── Day12.worksheet.sc │ │ ├── Day13.worksheet.sc │ │ ├── Day03.worksheet.sc │ │ ├── Day04.worksheet.sc │ │ ├── Day05.worksheet.sc │ │ ├── Day14.worksheet.sc │ │ ├── Day09.worksheet.sc │ │ ├── Day11.worksheet.sc │ │ ├── Day15.worksheet.sc │ │ ├── Day17.worksheet.sc │ │ ├── Day08.worksheet.sc │ │ ├── Day25.worksheet.sc │ │ ├── Day10.worksheet.sc │ │ ├── Day19.worksheet.sc │ │ └── Day20.worksheet.sc │ │ ├── 2022 │ │ ├── Day06.worksheet.sc │ │ ├── Day01.worksheet.sc │ │ ├── Day04.worksheet.sc │ │ ├── README.md │ │ ├── Day03.worksheet.sc │ │ ├── Day25.worksheet.sc │ │ ├── Day05.worksheet.sc │ │ ├── Day09.worksheet.sc │ │ ├── Day18.worksheet.sc │ │ ├── Day10.worksheet.sc │ │ ├── Day02.worksheet.sc │ │ ├── Day20.worksheet.sc │ │ ├── Day12.worksheet.sc │ │ ├── Day14.worksheet.sc │ │ ├── Day08.worksheet.sc │ │ ├── Day11.worksheet.sc │ │ ├── Day07.worksheet.sc │ │ ├── Day16.worksheet.sc │ │ ├── Day13.worksheet.sc │ │ ├── Day23.worksheet.sc │ │ ├── Day24.worksheet.sc │ │ ├── Day15.worksheet.sc │ │ ├── Day21.worksheet.sc │ │ └── Day17.worksheet.sc │ │ ├── 2023 │ │ ├── Day09.worksheet.sc │ │ ├── Day25.worksheet.sc │ │ ├── Day11.worksheet.sc │ │ ├── Day04.worksheet.sc │ │ ├── Day02.worksheet.sc │ │ ├── Day01.worksheet.sc │ │ ├── Day08.worksheet.sc │ │ ├── Day06.worksheet.sc │ │ ├── Day13.worksheet.sc │ │ ├── Day15.worksheet.sc │ │ ├── Day03.worksheet.sc │ │ ├── Day05Part1.worksheet.sc │ │ ├── Day12.worksheet.sc │ │ ├── Day22.worksheet.sc │ │ ├── Day17.worksheet.sc │ │ ├── Day21.worksheet.sc │ │ ├── Day14.worksheet.sc │ │ ├── Day07.worksheet.sc │ │ ├── Day05.worksheet.sc │ │ ├── Day16.worksheet.sc │ │ ├── Day23.worksheet.sc │ │ ├── Day17Part2.worksheet.sc │ │ ├── Day05Part2.worksheet.sc │ │ ├── README.md │ │ └── Day18.worksheet.sc │ │ ├── 2024 │ │ ├── Day01.worksheet.sc │ │ ├── Day19.worksheet.sc │ │ ├── Day02.worksheet.sc │ │ ├── Day25.worksheet.sc │ │ ├── Day07.worksheet.sc │ │ ├── Day03.worksheet.sc │ │ ├── Day11.worksheet.sc │ │ ├── Day23.worksheet.sc │ │ ├── Day05.worksheet.sc │ │ ├── Day10.worksheet.sc │ │ ├── Day18.worksheet.sc │ │ ├── Day22.worksheet.sc │ │ ├── Day08.worksheet.sc │ │ ├── Day20.worksheet.sc │ │ ├── Day12.worksheet.sc │ │ ├── Day04.worksheet.sc │ │ ├── Day14Variants.worksheet.sc │ │ ├── Day16.worksheet.sc │ │ ├── Day13.worksheet.sc │ │ ├── Day06.worksheet.sc │ │ └── Day20.scala │ │ ├── 2025 │ │ ├── Day12Golf.worksheet.sc │ │ ├── Day02.worksheet.sc │ │ ├── Day05.worksheet.sc │ │ ├── Day03.worksheet.sc │ │ ├── Day01.worksheet.sc │ │ ├── Day07.worksheet.sc │ │ ├── Day04.worksheet.sc │ │ ├── Day09.worksheet.sc │ │ ├── Day12.worksheet.sc │ │ ├── Day06.worksheet.sc │ │ ├── Day08.worksheet.sc │ │ └── Day10Solver.worksheet.sc │ │ ├── Main.scala │ │ └── synacor │ │ ├── Main.scala │ │ ├── Play.worksheet.sc │ │ ├── numbers │ │ └── package.scala │ │ └── README.md └── test │ └── scala │ └── Test1.scala ├── input.sh ├── splits.sh ├── LICENSE └── README.md /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.10.5 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | /*.json 3 | scratch 4 | /notes/* 5 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "3.0.5" 2 | runner.dialect = scala3 3 | -------------------------------------------------------------------------------- /src/main/resources/synacor/challenge.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stewSquared/advent-of-code/HEAD/src/main/resources/synacor/challenge.bin -------------------------------------------------------------------------------- /src/main/scala/Main.scala: -------------------------------------------------------------------------------- 1 | @main def hello: Unit = 2 | println("Hello world!") 3 | println(msg) 4 | 5 | def msg = "I was compiled by Scala 3. :)" 6 | -------------------------------------------------------------------------------- /src/test/scala/Test1.scala: -------------------------------------------------------------------------------- 1 | import org.junit.Test 2 | import org.junit.Assert.* 3 | 4 | class Test1: 5 | @Test def t1(): Unit = 6 | assertEquals("I was compiled by Scala 3. :)", msg) 7 | -------------------------------------------------------------------------------- /src/main/scala/2025/Day12Golf.worksheet.sc: -------------------------------------------------------------------------------- 1 | io.Source.fromResource("2025/day-12.txt").getLines.drop(30).count: 2 | case s"${w}x${l}: $ns" => 3 | ns.split(' ').map(_.toInt).sum * 9 <= w.toInt * l.toInt 4 | -------------------------------------------------------------------------------- /src/main/scala/2021/Day01.worksheet.sc: -------------------------------------------------------------------------------- 1 | val sweep = io.Source.fromResource("2021/day-01-1.txt").getLines.map(_.toInt).toList 2 | 3 | val ans1 = sweep.zip(sweep.tail).count(_ < _) 4 | val ans2 = sweep.zip(sweep.drop(3)).count(_ < _) 5 | -------------------------------------------------------------------------------- /src/main/scala/2022/Day06.worksheet.sc: -------------------------------------------------------------------------------- 1 | val msg = io.Source.fromResource("2022/day-06.txt").getLines().mkString 2 | 3 | val ans1 = 4 + msg.sliding(4).indexWhere { g => 4 | g.distinct.size == g.size 5 | } 6 | 7 | val ans2 = 14 + msg.sliding(14).indexWhere { g => 8 | g.distinct.size == g.size 9 | } 10 | -------------------------------------------------------------------------------- /src/main/scala/2015/Day01.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2015/day-01.txt").getLines().next() 2 | 3 | val ans1 = input.count(_ == '(') - input.count(_ == ')') 4 | 5 | val positions = input.scanLeft(0): 6 | case (l, '(') => l + 1 7 | case (l, ')') => l - 1 8 | 9 | val ans2 = positions.indexWhere(_ < 0) 10 | -------------------------------------------------------------------------------- /src/main/scala/2022/Day01.worksheet.sc: -------------------------------------------------------------------------------- 1 | val lines = io.Source.fromResource("2022/day-01.txt").getLines() 2 | val lb = collection.mutable.ListBuffer.empty[Int] 3 | while lines.hasNext do 4 | lb += lines.takeWhile(_.nonEmpty).map(_.toInt).sum 5 | val calories = lb.result() 6 | val ans1 = calories.max 7 | val ans2 = calories.sorted.takeRight(3).sum 8 | -------------------------------------------------------------------------------- /input.sh: -------------------------------------------------------------------------------- 1 | aoc-input () { 2 | local -r year=$1 3 | local -r day=$2 4 | local -r source="https://adventofcode.com/$year/day/$day/input" 5 | local -r dest="src/main/resources/$year/day-`printf %02d $day`.txt" 6 | echo "reading from $source" 7 | echo "writing to $dest" 8 | curl --cookie session=$AOC_SESSION $source >| $dest 9 | } 10 | -------------------------------------------------------------------------------- /src/main/scala/2022/Day04.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2022/day-04.txt").getLines().collect { 2 | case s"$a-$b,$c-$d" => 3 | (a.toInt to b.toInt, c.toInt to d.toInt) 4 | } 5 | 6 | val ans1 = input.count { case (left, right) => 7 | left.containsSlice(right) || right.containsSlice(left) 8 | } 9 | 10 | val ans2 = input.count(_.intersect(_).nonEmpty) 11 | -------------------------------------------------------------------------------- /src/main/scala/2022/README.md: -------------------------------------------------------------------------------- 1 | Hi! The solutions in this folder are all written during a live stream on twitch and cleaned up afterwards. 2 | If you want to see it in the making visit me at https://twitch.tv/stewSquared when the problem comes out. 3 | I start a little bit early to do review/refactor/warmup. 4 | Eventually, I'll add a table of stats an links to recordings in this README. 5 | -------------------------------------------------------------------------------- /src/main/scala/2024/Day01.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2024/day-01.txt").getLines().toList 2 | 3 | val List(left, right) = input.map(_.split(" +").map(_.toInt).toList).transpose 4 | 5 | val ans1 = left.sorted.zip(right.sorted).map(_ - _).map(_.abs).sum 6 | 7 | val counts = right.groupMapReduce(identity)(_ => 1)(_ + _).withDefaultValue(0) 8 | 9 | val ans2 = left.map(n => counts(n) * n).sum 10 | -------------------------------------------------------------------------------- /src/main/scala/2020/Day01.worksheet.sc: -------------------------------------------------------------------------------- 1 | 2 | // val expenses = List(1721, 979, 366, 299, 675, 1456) 3 | val expenses = io.Source.fromResource("2020/day-01.txt").getLines.map(_.toInt).toList 4 | 5 | expenses.size 6 | 7 | val ans1 = expenses.sorted.combinations(2).collectFirst { 8 | case xs if xs.sum == 2020 => xs.product 9 | } 10 | 11 | val ans2 = expenses.sorted.combinations(3).collectFirst { 12 | case xs if xs.sum == 2020 => xs.product 13 | } 14 | -------------------------------------------------------------------------------- /src/main/scala/2025/Day02.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2025/day-02.txt").getLines().next() 2 | 3 | val ranges = input.split(',').map: 4 | case s"$a-$b" => a.toLong to b.toLong 5 | 6 | def invalid(id: Long) = """(\d+)\1""".r.matches(id.toString) 7 | def invalid2(id: Long) = """(\d+)\1+""".r.matches(id.toString) 8 | 9 | val ans1 = ranges.iterator.flatten.filter(invalid).sum 10 | val ans2 = ranges.flatten.filter(invalid2).sum 11 | -------------------------------------------------------------------------------- /src/main/scala/2020/Day02.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2020/day-02.txt").getLines.toList 2 | 3 | val ans = input.count { 4 | case s"$low-$high $char: $password" => 5 | (low.toInt to high.toInt).contains(password.count(_ == char.head)) 6 | } 7 | 8 | val ans2 = input.count { 9 | case s"$a-$b $char: $password" => 10 | val indices = List(a, b).map(_.toInt - 1) 11 | indices.count(password.charAt(_) == char.head) == 1 12 | } 13 | -------------------------------------------------------------------------------- /src/main/scala/2022/Day03.worksheet.sc: -------------------------------------------------------------------------------- 1 | val sacks = io.Source.fromResource("2022/day-03.txt").getLines().toList 2 | 3 | def prio(c: Char) = 4 | if c.isUpper then c.toInt - 65 + 27 5 | else c.toInt - 97 + 1 6 | 7 | val ans1 = sacks.map { sack => 8 | val (left, right) = sack.splitAt(sack.size / 2) 9 | prio(left.intersect(right).distinct.head) 10 | }.sum 11 | 12 | val ans2 = sacks.grouped(3).map { group => 13 | prio(group.reduce(_ intersect _).head) 14 | }.sum 15 | -------------------------------------------------------------------------------- /src/main/scala/2025/Day05.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2025/day-05.txt").getLines().toList 2 | 3 | import aoc.Interval 4 | 5 | val fresh = input.collect: 6 | case s"$a-$b" => Interval(a.toLong, b.toLong) 7 | .foldLeft(List.empty[Interval[Long]]): 8 | case (acc, n) => n.union(acc) 9 | 10 | val available = input.flatMap(_.toLongOption) 11 | 12 | val ans1 = available.count(id => fresh.exists(_.contains(id))) 13 | val ans2 = fresh.map(_.size).sum 14 | -------------------------------------------------------------------------------- /src/main/scala/2020/Day06.worksheet.sc: -------------------------------------------------------------------------------- 1 | import util.Using 2 | 3 | val groups = Using(io.Source.fromResource("2020/day-06.txt")){ source => 4 | val lines = source.getLines 5 | val lb = collection.mutable.ListBuffer[List[String]]() 6 | while lines.hasNext 7 | do lb += lines.takeWhile(_.nonEmpty).toList 8 | lb.result() 9 | }.get 10 | 11 | val ans = groups.map(g => g.map(_.toSet).reduce(_ union _).size).sum 12 | 13 | val ans2 = groups.map(g => g.map(_.toSet).reduce(_ intersect _).size).sum 14 | -------------------------------------------------------------------------------- /src/main/scala/2025/Day03.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2025/day-03.txt").getLines().toList 2 | 3 | val banks = input.map(s => IArray.from(s.iterator.map(_.asDigit))) 4 | 5 | def joltage(bank: IArray[Int], batteries: Int): Long = 6 | val indices = (batteries to 1 by -1).scanLeft(-1): 7 | case (i, b) => (i + 1 to bank.length - b).maxBy(bank) 8 | indices.tail.map(bank).mkString.toLong 9 | 10 | val ans1 = banks.map(joltage(_, 2)).sum 11 | val ans2 = banks.map(joltage(_, 12)).sum 12 | -------------------------------------------------------------------------------- /src/main/scala/2015/Day09.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2015/day-09.txt").getLines().toList 2 | 3 | val distance = Map.from[(String, String), Int]: 4 | input.flatMap: 5 | case s"$a to $b = $dist" => 6 | List((a, b) -> dist.toInt, (b, a) -> dist.toInt) 7 | 8 | val cities = distance.keys.flatMap: 9 | case (a, b) => List(a, b) 10 | .toList 11 | 12 | val ans1 = cities.permutations.map: p => 13 | p.sliding(2).map: 14 | case List(a, b) => distance(a -> b) 15 | .sum 16 | .max 17 | 18 | // 19 | -------------------------------------------------------------------------------- /src/main/scala/2021/Day07.worksheet.sc: -------------------------------------------------------------------------------- 1 | val positions = io.Source.fromResource("2021/day-07-1.txt") 2 | .getLines.toList.head 3 | .split(",").map(_.toInt).toList 4 | 5 | def linearCost(n: Int) = positions.map(p => (n - p).abs).sum 6 | 7 | def triangularCost(n: Int) = positions.map { p => 8 | val d = (n - p).abs 9 | d * (d + 1) / 2 10 | }.sum 11 | 12 | // naieve brute force for input size of 1000 is fine 13 | val ans1 = positions.indices.map(linearCost).min 14 | val ans2 = positions.indices.map(triangularCost).min 15 | -------------------------------------------------------------------------------- /src/main/scala/2024/Day19.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2024/day-19.txt").getLines.toList 2 | 3 | val available = input(0).split(", ").toList 4 | val desired = input.drop(2).toList 5 | 6 | val memo = collection.mutable.Map("" -> 1L) 7 | 8 | def possible(t: String): Long = 9 | lazy val recurse = available.collect: 10 | case a if t.startsWith(a) => possible(t.stripPrefix(a)) 11 | 12 | memo.getOrElseUpdate(t, recurse.sum) 13 | 14 | val ans1 = desired.count(possible(_) > 0) 15 | val ans2 = desired.map(possible).sum 16 | -------------------------------------------------------------------------------- /src/main/scala/2015/Day04.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = "bgvyzdsv" 2 | 3 | // calculate md5 hash using standard library 4 | import java.security.MessageDigest 5 | def md5(s: String): String = 6 | val digest = MessageDigest.getInstance("MD5").digest(s.getBytes) 7 | digest.map("%02x".format(_)).mkString 8 | 9 | md5("abcdef" + 609043.toString) 10 | 11 | val ans1 = Iterator.from(1).find: n => 12 | md5(input + n.toString).startsWith("00000") 13 | .get 14 | 15 | val ans2 = Iterator.from(1).find: n => 16 | md5(input + n.toString).startsWith("000000") 17 | .get 18 | -------------------------------------------------------------------------------- /src/main/scala/2021/Day02.worksheet.sc: -------------------------------------------------------------------------------- 1 | val commands = io.Source.fromResource("2021/day-02-1.txt").getLines.toList 2 | 3 | case class Pos(horiz: Int, depth: Int, aim: Int): 4 | def move(command: String) = command match 5 | case s"up $x" => copy(aim = aim - x.toInt) 6 | case s"down $x" => copy(aim = aim + x.toInt) 7 | case s"forward $x" => copy(horiz + x.toInt, depth + aim * x.toInt) 8 | 9 | val finalPos = commands.foldLeft(Pos(0, 0, 0))(_ move _) 10 | 11 | val ans1 = finalPos.horiz * finalPos.aim 12 | val ans2 = finalPos.horiz * finalPos.depth 13 | -------------------------------------------------------------------------------- /src/main/scala/2023/Day09.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2023/day-09.txt").getLines().toVector 2 | val hists = input.map(_.split(" ").map(_.toLong).toVector) 3 | 4 | def deltas(values: Vector[Long]) = values.zip(values.tail).map((a, b) => b - a) 5 | 6 | def prediction(hist: Vector[Long]): Long = 7 | val finalDeltas = Iterator.unfold(hist): line => 8 | Option.unless(line.forall(_ == 0)): 9 | line.last -> deltas(line) 10 | finalDeltas.sum 11 | 12 | val ans1 = hists.map(prediction).sum 13 | val ans2 = hists.map(_.reverse).map(prediction).sum 14 | -------------------------------------------------------------------------------- /src/main/scala/2025/Day01.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2025/day-01.txt").getLines().toList 2 | 3 | val rotations = input.collect: 4 | case s"R$n" => n.toInt 5 | case s"L$n" => -n.toInt 6 | 7 | val (positions, clicks) = rotations.scanLeft(50, 0): 8 | case ((pos, _), rot) => 9 | val next = pos + rot 10 | val leftToZero = pos != 0 && next <= 0 11 | val clicks = next.abs / 100 + (if leftToZero then 1 else 0) 12 | (math.floorMod(next, 100), clicks) 13 | .unzip 14 | 15 | val ans1 = positions.count(_ == 0) 16 | val ans2 = clicks.sum 17 | -------------------------------------------------------------------------------- /splits.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | aoc-day () { 4 | jq -r --arg day $1 '.members | .[] | ( 5 | .completion_day_level | select(has($day)) | .[$day] 6 | | .[] | .get_star_ts | tostring | strptime("%s") 7 | | ((strftime("%d") | tonumber) - ($day | tonumber)) as $days 8 | | (strftime("%H") | tonumber) as $hours 9 | | ($days * 24 + $hours | tostring) + strftime(":%M:%S") 10 | ) + " - " + .name' | sort -h 11 | } 12 | 13 | aoc-all-days () { 14 | local -r json=$1 15 | for DAY in {1..25} 16 | do 17 | echo "Day $DAY:" 18 | aoc-day $DAY <$json 19 | done 20 | } 21 | -------------------------------------------------------------------------------- /src/main/scala/2015/Day02.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2015/day-02.txt").getLines().toList 2 | 3 | val dimensions = input.map: 4 | case s"${l}x${w}x${h}" => (l.toInt, w.toInt, h.toInt) 5 | 6 | val paperAreas = dimensions.map: 7 | case (l, w, h) => 8 | val surfaceArea = (l*w + w*h + l*h)*2 9 | val slack = List(l,w,h).sorted.take(2).product 10 | surfaceArea + slack 11 | 12 | val ans1 = paperAreas.sum 13 | 14 | val ribbonLengths = dimensions.map: 15 | case (l, w, h) => 16 | List(l+w, w+h, l+h).min * 2 + l*w*h 17 | 18 | val ans2 = ribbonLengths.sum 19 | 20 | 2 + 2 21 | 22 | // 23 | -------------------------------------------------------------------------------- /src/main/scala/2021/Day06.worksheet.sc: -------------------------------------------------------------------------------- 1 | import util.chaining.* 2 | 3 | val input = io.Source.fromResource("2021/day-06-1.txt").getLines.next() 4 | val start = input.split(",").map(_.toInt) 5 | 6 | val memo = collection.mutable.Map.empty[Int, Long] 7 | 8 | def descendants(days: Int): Long = 9 | memo.get(days).getOrElse { 10 | val offspring = (days - 9) to 0 by -7 11 | (offspring.size + offspring.map(descendants).sum).tap(memo(days) = _) 12 | } 13 | 14 | val ans1 = start.map(days => descendants(80 + 8 - days)).sum + start.size 15 | val ans2 = start.map(days => descendants(256 + 8 - days)).sum + start.size 16 | -------------------------------------------------------------------------------- /src/main/scala/2015/Day17.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2015/day-17.txt").getLines().toList 2 | 3 | val containers = input.map(_.toInt).sorted 4 | 5 | def count2(goal: Int, containers: List[Int], used: List[Int]): List[List[Int]] = 6 | if goal < 0 then Nil 7 | else if goal == 0 then List(used) 8 | else containers match 9 | case c::cs => count2(goal - c, cs, c::used) ::: count2(goal, cs, used) 10 | case Nil => Nil 11 | 12 | val solutions = count2(150, containers, Nil) 13 | 14 | val ans1 = solutions.size 15 | 16 | val min = solutions.map(_.size).min 17 | val ans2 = solutions.count(_.size == min) 18 | -------------------------------------------------------------------------------- /src/main/scala/2020/Day05.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2020/day-05.txt").getLines.toList 2 | 3 | val ids = input.map { 4 | seat => 5 | val row = 6 | val bin = seat.take(7).map(c => if c == 'F' then '0' else '1') 7 | Integer.parseInt(bin, 2) 8 | val col = 9 | val bin = seat.drop(7).map(c => if c == 'R' then '1' else '0') 10 | Integer.parseInt(bin, 2) 11 | row * 8 + col 12 | } 13 | 14 | val ans = ids.max 15 | 16 | ids.size 17 | 18 | val ans2 = ids.sorted 19 | .dropWhile(_ < 8) 20 | .sliding(2) 21 | .collectFirst { 22 | case Seq(l, r) if l + 2 == r => l + 1 23 | } 24 | -------------------------------------------------------------------------------- /src/main/scala/2015/Day08.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2015/day-08.txt").getLines().toList 2 | 3 | def count(s: String): Int = s match 4 | case "" => 0 5 | case s"\"$rem" => count(rem) 6 | case s"\\\"$rem" => 1 + count(rem) 7 | case s"\\\\$rem" => 1 + count(rem) 8 | case s"\\x$rem" => 1 + count(rem.drop(2)) 9 | case _ => 1 + count(s.drop(1)) 10 | 11 | def encode(s: String): String = s.flatMap: 12 | case '"' => "\\\"" 13 | case '\\' => "\\\\" 14 | case c => c.toString 15 | .mkString("\"", "", "\"") 16 | 17 | val ans1 = input.map(s => s.length - count(s)).sum 18 | val ans2 = input.map(s => encode(s).length - s.length).sum 19 | -------------------------------------------------------------------------------- /src/main/scala/2024/Day02.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2024/day-02.txt").getLines() 2 | .toList.map(_.split(" ").map(_.toInt).toList) 3 | 4 | def deltas(levels: List[Int]) = 5 | levels.zip(levels.tail).map((a, b) => b - a) 6 | 7 | def safe(levels: List[Int]): Boolean = 8 | val d = deltas(levels) 9 | d.forall((1 to 3).contains) || d.forall((-1 to -3 by -1).contains) 10 | 11 | def safeDampened(levels: List[Int]): Boolean = 12 | safe(levels) || levels.indices.exists: i => 13 | val (left, right) = levels.splitAt(i) 14 | safe(left ++ right.tail) 15 | 16 | val ans1 = input.count(safe) 17 | val ans2 = input.count(safeDampened) 18 | -------------------------------------------------------------------------------- /src/main/scala/2025/Day07.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2025/day-07.txt").getLines().toList 2 | 3 | val start = input.head.indexOf("S") 4 | 5 | val beamStates = input.tail.scanLeft(Map(start -> 1L).withDefaultValue(0L)): 6 | case (beams, line) => 7 | beams.foldLeft(beams): 8 | case (acc, (i, c)) if line(i) == '^' => 9 | acc.removed(i) 10 | .updated(i + 1, acc(i + 1) + c) 11 | .updated(i - 1, acc(i - 1) + c) 12 | case (acc, _) => acc 13 | 14 | val ans1 = beamStates.zip(input.drop(2)).map: 15 | case (beams, line) => beams.keys.count(line(_) == '^') 16 | .sum 17 | 18 | val ans2 = beamStates.last.values.sum 19 | -------------------------------------------------------------------------------- /src/main/scala/2018/Day01.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2018/day-01.txt").getLines().map(_.toInt).toList 2 | 3 | input.size 4 | 5 | val ans1 = input.sum 6 | 7 | lazy val deltas: LazyList[Int] = LazyList.fromSpecific(input) #::: deltas 8 | 9 | val frequencies = deltas.scanLeft(0)(_ + _) 10 | 11 | // object for tailrec 12 | object Foo: 13 | val offset = input.sum 14 | def search(seen: Set[Int], toCheck: LazyList[Int]): Int = 15 | toCheck match 16 | case f #:: fs => 17 | val positive = f + offset 18 | if seen(positive) then f 19 | else search(seen + positive, fs) 20 | 21 | val ans2 = Foo.search(Set.empty, frequencies) 22 | -------------------------------------------------------------------------------- /src/main/scala/2022/Day25.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2022/day-25.txt").getLines().toVector 2 | 3 | val snits = "=-012" 4 | def toSnit(digit: Int): Char = snits(digit + 2) 5 | def toDigit(snit: Char): Int = snits.indexOf(snit) + 2 6 | 7 | def toNum(snafu: String): Long = 8 | snafu.foldLeft(0L)((n, s) => n * 5 + toDigit(s)) 9 | 10 | def toSnafu(num: Long): String = 11 | val snitsLE = Iterator.unfold(num) { n => 12 | Option.when(n != 0) { 13 | val s = math.floorMod(n + 2, 5).toInt - 2 14 | toSnit(s) -> (n - s) / 5 15 | } 16 | } 17 | if snitsLE.isEmpty then "0" 18 | else snitsLE.mkString.reverse 19 | 20 | val ans = toSnafu(input.map(toNum).sum) 21 | -------------------------------------------------------------------------------- /src/main/scala/2025/Day04.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2025/day-04.txt").getLines().toVector 2 | 3 | import aoc.* 4 | 5 | val rolls = Area(input).pointsIterator.filter(input(_) == '@').toSet 6 | 7 | def accessible(rolls: Set[Point]): Set[Point] = 8 | rolls.filter(_.surrounding.count(rolls) < 4) 9 | 10 | val ans1 = accessible(rolls).size 11 | 12 | def remove(rolls: Set[Point]): Set[Point] = 13 | rolls.diff(accessible(rolls)) 14 | 15 | val steps = LazyList.iterate(rolls)(remove) 16 | lazy val pairs = steps.zip(steps.tail) 17 | 18 | val remaining = pairs.collectFirst: 19 | case (s, t) if s.size == t.size => s.size 20 | .get 21 | 22 | val ans2 = rolls.size - remaining 23 | -------------------------------------------------------------------------------- /src/main/scala/2015/Day13.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2015/day-13.txt").getLines().toList 2 | 3 | val prefs = Map.from[(String, String), Int]: 4 | input.collect: 5 | case s"$alice would $gain $unit happiness units by sitting next to $bob." => 6 | (alice, bob) -> (if gain == "gain" then unit.toInt else -unit.toInt) 7 | .withDefaultValue(0) 8 | 9 | val names = prefs.keys.map(_._1) 10 | 11 | def happiness(seats: List[String]): Int = 12 | seats.zip(seats.last :: seats).map: 13 | case (a, b) => prefs(a,b) + prefs(b,a) 14 | .sum 15 | 16 | val ans1 = names.toList.permutations.map(happiness).max 17 | val ans2 = ("stew" :: names.toList).permutations.map(happiness).max 18 | -------------------------------------------------------------------------------- /src/main/scala/2015/Day03.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2015/day-03.txt").getLines().next() 2 | 3 | import aoc.Point 4 | 5 | def move(p: Point, c: Char): Point = (p, c) match 6 | case (p, '^') => p.n 7 | case (p, 'v') => p.s 8 | case (p, '<') => p.w 9 | case (p, '>') => p.e 10 | // TODO add char movement to the library 11 | 12 | val path = input.scanLeft(Point.origin)(move) 13 | 14 | val ans1 = path.distinct.size 15 | 16 | val santaPath = input.sliding(1, 2).flatten 17 | .scanLeft(Point.origin)(move) 18 | 19 | val roboPath = input.tail.sliding(1, 2).flatten 20 | .scanLeft(Point.origin)(move) 21 | 22 | val ans2 = (santaPath.toSet union roboPath.toSet).size 23 | 24 | // 25 | -------------------------------------------------------------------------------- /src/main/scala/2018/Day02.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2018/day-02.txt").getLines().toList 2 | 3 | def counts(s: String): Map[Char, Int] = s.groupMapReduce(identity)(_ => 1)(_ + _) 4 | 5 | val exactlyTwo = input.count(counts(_).values.toSet.contains(2)) 6 | val exactlyThree = input.count(counts(_).values.toSet.contains(3)) 7 | 8 | val ans1 = exactlyTwo * exactlyThree 9 | 10 | def dist(s1: String, s2: String): Int = 11 | s1.zip(s2).count { case (a, b) => a != b } 12 | 13 | input.size 14 | 15 | val candidates = for 16 | s1 <- input 17 | s2 <- input 18 | if dist(s1, s2) == 1 19 | yield s1.zip(s2).collect: 20 | case (a, b) if a == b => a 21 | 22 | val ans2 = candidates.head.mkString 23 | -------------------------------------------------------------------------------- /src/main/scala/2024/Day25.worksheet.sc: -------------------------------------------------------------------------------- 1 | val schematics: List[Vector[String]] = 2 | val lines = io.Source.fromResource("2024/day-25.txt").getLines 3 | val lb = collection.mutable.ListBuffer.empty[Vector[String]] 4 | while lines.hasNext do 5 | val schematic = lines.takeWhile(_.nonEmpty) 6 | lb += schematic.toVector 7 | lb.toList 8 | 9 | val locks: List[Vector[Int]] = schematics 10 | .filter(_.head.forall(_ == '#')) 11 | .map(_.transpose.map(_.count(_ == '#') - 1)) 12 | 13 | val keys = schematics 14 | .filter(_.last.forall(_ == '#')) 15 | .map(_.transpose.map(_.count(_ == '#') - 1)) 16 | 17 | val ans1 = keys.map: k => 18 | locks.count: l => 19 | l.zip(k).map(_ + _).forall(_ <= 5) 20 | .sum 21 | -------------------------------------------------------------------------------- /src/main/scala/2025/Day09.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2025/day-09.txt").getLines().toList 2 | 3 | import aoc.{Area, Point} 4 | 5 | val tiles = input.collect: 6 | case s"$x,$y" => Point(x.toInt, y.toInt) 7 | 8 | val rects = tiles.combinations(2).collect: 9 | case Seq(p, q) => Area.bounding(p, q) 10 | .toList 11 | 12 | val ans1 = rects.map(_.size[Long]).max 13 | 14 | val lines = tiles.zip(tiles.last :: tiles).map: 15 | case (p, q) => Area.bounding(p, q) 16 | 17 | def uncrossed(a: Area): Boolean = 18 | val inner = a.expand(-1) 19 | lines.forall: l => 20 | util.Try: 21 | l.intersect(inner).isEmpty 22 | .getOrElse(false) 23 | 24 | val ans2 = rects.filter(uncrossed).map(_.size[Long]).max 25 | -------------------------------------------------------------------------------- /src/main/scala/2023/Day25.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2023/day-25.txt").getLines.toList 2 | 3 | val adj: Map[String, Set[String]] = 4 | val edges = input.toSet.flatMap: 5 | case s"$c: $cs" => 6 | cs.split(" ").flatMap(c2 => Set(c -> c2, c2 -> c)) 7 | edges.groupMap(_._1)(_._2) 8 | 9 | def search(connected: Set[String], reachable: Set[String]): Set[String] = 10 | if reachable.size == 3 then connected else 11 | val (next, newOptions) = reachable.iterator.map: n => 12 | n -> adj(n).diff(connected) 13 | .minBy(_._2.size) 14 | search(connected + next, (reachable - next) union newOptions) 15 | 16 | val ans1 = 17 | val s = search(Set.empty, Set("klg")) 18 | s.size * (adj.size - s.size) 19 | -------------------------------------------------------------------------------- /src/main/scala/2024/Day07.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2024/day-07.txt").getLines.toList 2 | 3 | val equations: List[(Long, List[Long])] = 4 | input.collect: 5 | case s"$lhs: $rhs" => 6 | lhs.toLong -> rhs.split(" ").map(_.toLong).toList.reverse 7 | 8 | def solvable(lhs: Long, rhs: List[Long]): Boolean = 9 | rhs match 10 | case Nil => ??? 11 | case head :: Nil => head == lhs 12 | case n :: ns => 13 | solvable(lhs - n, ns) 14 | || ((lhs % n == 0) && solvable(lhs / n, ns)) 15 | || (lhs > n && (lhs.toString.endsWith(n.toString)) && solvable(lhs.toString.dropRight(n.toString.length).toLong, ns)) 16 | 17 | val ans = equations 18 | .filter(solvable) 19 | .map(_._1) 20 | .sum 21 | -------------------------------------------------------------------------------- /src/main/scala/2024/Day03.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2024/day-03.txt").getLines().toList 2 | 3 | val mulRegex = "mul\\(\\d+,\\d+\\)".r 4 | 5 | val ans1 = mulRegex.findAllIn(input.mkString).map: 6 | case s"mul($x,$y)" => x.toInt * y.toInt 7 | .sum 8 | 9 | val doRegex = "do\\(\\)".r 10 | val dontRegex = "don't\\(\\)".r 11 | val instRegex = s"(${mulRegex}|${doRegex}|${dontRegex})".r 12 | 13 | val (ans2, _) = instRegex.findAllIn(input.mkString).foldLeft(0, true): 14 | case ((sum, enabled), instruction) => 15 | instruction match 16 | case "do()" => (sum, true) 17 | case "don't()" => (sum, false) 18 | case s"mul($x,$y)" if enabled => (sum + x.toInt * y.toInt, enabled) 19 | case _ => (sum, enabled) 20 | -------------------------------------------------------------------------------- /src/main/scala/2018/Day03.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2018/day-03.txt").getLines().toList 2 | 3 | import aoc.{Point, Area} 4 | 5 | val claims: List[Area] = input.collect: 6 | case s"#${id} @ ${x},${y}: ${w}x${h}" => 7 | val xRange = x.toInt until x.toInt + w.toInt 8 | val yRange = y.toInt until y.toInt + h.toInt 9 | Area(xRange, yRange) 10 | 11 | val (_, overlapping) = claims.foldLeft((Set.empty[Point], Set.empty[Point])): 12 | case ((seen, overlap), claim) => 13 | val (newOverlap, newSeen) = claim.pointsIterator.partition(seen) 14 | (seen ++ newSeen, overlap ++ newOverlap) 15 | 16 | val ans1 = overlapping.size 17 | 18 | val ans2 = 1 + claims.indexWhere: claim => 19 | claim.pointsIterator.forall(!overlapping(_)) 20 | -------------------------------------------------------------------------------- /src/main/scala/2015/Day10.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = "1321131112" 2 | 3 | def lookSay(look: String): String = 4 | val n = look.length 5 | val pairs = Iterator.unfold(0): 6 | case (start) => Option.when(start < n): 7 | val head = look.charAt(start) 8 | val end = look.indexWhere(_ != head, start + 1) match 9 | case -1 => n 10 | case i => i 11 | val count = end - start 12 | (count, head) -> end 13 | 14 | val say = StringBuilder(look.length * 2) 15 | for (count, head) <- pairs do 16 | say ++= count.toString 17 | say += head 18 | say.result() 19 | 20 | lookSay(input) 21 | 22 | val ans1 = Iterator.iterate(input)(lookSay).drop(40).next().length 23 | val ans2 = Iterator.iterate(input)(lookSay).drop(50).next().length 24 | -------------------------------------------------------------------------------- /src/main/scala/2023/Day11.worksheet.sc: -------------------------------------------------------------------------------- 1 | import aoc.* 2 | 3 | val input = io.Source.fromResource("2023/day-11.txt").getLines().toVector 4 | 5 | val area = Area(input) 6 | val galaxies = area.pointsIterator.filter(input(_) == '#').toVector 7 | 8 | val xOccupied = galaxies.map(_.x).toSet 9 | val yOccupied = galaxies.map(_.y).toSet 10 | 11 | def dist(p: Point, q: Point, growth: Long): Long = 12 | val bounds = Area.bounding(p, q) 13 | val emptyX = bounds.xRange.count(!xOccupied(_)) 14 | val emptyY = bounds.yRange.count(!yOccupied(_)) 15 | p.dist(q) + (emptyX + emptyY) * (growth - 1) 16 | 17 | def distances(growth: Long) = galaxies.combinations(2).collect: 18 | case Vector(g, h) => dist(g, h, growth) 19 | 20 | val ans1 = distances(2).sum 21 | val ans2 = distances(1_000_000).sum 22 | -------------------------------------------------------------------------------- /src/main/scala/2024/Day11.worksheet.sc: -------------------------------------------------------------------------------- 1 | val stones = io.Source.fromResource("2024/day-11.txt") 2 | .getLines.mkString 3 | .split(" ").map(_.toLong).toVector 4 | 5 | import collection.mutable.Map 6 | val memo = Map.empty[(Long, Int), Long] 7 | 8 | def blink(stone: Long, times: Int): Long = 9 | lazy val calculate = 10 | if times == 0 then 1L else 11 | stone match 12 | case 0L => blink(1L, times - 1) 13 | case n if n.toString.size % 2 == 0 => 14 | val s = n.toString 15 | val (l, r) = s.splitAt(s.length/2) 16 | blink(l.toLong, times - 1) + blink(r.toLong, times - 1) 17 | case n => blink(n * 2024, times - 1) 18 | 19 | memo.getOrElseUpdate((stone, times), calculate) 20 | 21 | stones.map(blink(_, 25)).sum 22 | stones.map(blink(_, 75)).sum 23 | -------------------------------------------------------------------------------- /src/main/scala/2025/Day12.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2025/day-12.txt").getLines().toList 2 | 3 | val shapeVolumes = List.from: 4 | Iterator.unfold(input): lines => 5 | Option.when(lines.head.endsWith(":")): 6 | val grid = lines.slice(1,5) 7 | val volume = grid.map(_.count(_ == '#')).sum 8 | volume -> lines.drop(5) 9 | 10 | val regions = input.collect: 11 | case s"${w}x${l}: $counts" => 12 | val area = w.toInt * l.toInt 13 | area -> counts.split(' ').map(_.toInt).toList 14 | 15 | val lowerBound = regions.count: 16 | case (area, counts) => 17 | shapeVolumes.zip(counts).map(_ * _).sum <= area 18 | 19 | val upperBound = regions.count: 20 | case (area, counts) => counts.sum * 9 <= area 21 | 22 | assert(lowerBound == upperBound) 23 | 24 | val ans1 = lowerBound 25 | -------------------------------------------------------------------------------- /src/main/scala/2018/Day05.worksheet.sc: -------------------------------------------------------------------------------- 1 | val polymer = io.Source.fromResource("2018/day-05.txt").getLines().mkString 2 | 3 | polymer.size 4 | 5 | def react(polymer: String): String = 6 | ('a' to 'z').foldLeft(polymer): 7 | case (acc, c) => 8 | acc.replaceAll(s"$c${c.toUpper}|${c.toUpper}$c", "") 9 | 10 | def fullyReacted(polymer: String) = Iterator 11 | .iterate(polymer)(react) 12 | .sliding(2) 13 | .collectFirst: 14 | case Seq(a, b) if a.size == b.size => a 15 | .get 16 | 17 | val ans1 = fullyReacted(polymer).size 18 | 19 | def remove(unit: Char, polymer: String): String = 20 | polymer.filterNot(_.toLower == unit) 21 | 22 | // Warning: 10 second runtime 23 | // val ans2 = ('a' to 'z') 24 | // .map(remove(_, polymer)) 25 | // .map(fullyReacted) 26 | // .map(_.size) 27 | // .min 28 | // 4956 29 | -------------------------------------------------------------------------------- /src/main/scala/2024/Day23.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2024/day-23.txt").getLines.toList 2 | 3 | val adj: Map[String, List[String]] = input.flatMap: 4 | case s"$a-$b" => List(a -> b, b -> a) 5 | .groupMap(_._1)(_._2) 6 | .withDefaultValue(Nil) 7 | 8 | val trips: List[List[String]] = adj.toList.flatMap: 9 | case (a, vs) => vs.combinations(2).collect: 10 | case List(b, c) if adj(b).contains(c) => List(a,b,c) 11 | 12 | val ans1 = trips.count(_.exists(_.startsWith("t"))) 13 | 14 | def connected12(nodes: List[String]): Option[List[String]] = 15 | nodes.combinations(12).find: vs => 16 | vs.combinations(2).forall: 17 | case List(a, b) => adj(a).contains(b) 18 | 19 | val ans2 = adj.view.mapValues(connected12).collectFirst: 20 | case (k, Some(vs)) => (k::vs).sorted.mkString(",") 21 | .get 22 | -------------------------------------------------------------------------------- /src/main/scala/2020/Day09.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2020/day-09.txt") 2 | .getLines.map(_.toLong).toIndexedSeq 3 | 4 | // val preamble = input.take(25) 5 | // val encrypted = input.drop(25) 6 | 7 | def valid(preamble: Seq[Long]): Set[Long] = 8 | preamble.combinations(2).map(_.sum).toSet 9 | 10 | val ans1 = input.sliding(26).collectFirst { 11 | case ns: IndexedSeq[Long] if !valid(ns.init).contains(ns.last) => ns.last 12 | } 13 | 14 | val target = ans1.get 15 | 16 | def findContiguous(from: Int, until: Int): Seq[Long] = 17 | val slice = input.slice(from, until) 18 | if slice.sum < target then findContiguous(from, until + 1) 19 | else if slice.sum > target then findContiguous(from + 1, until) 20 | else slice 21 | 22 | val ans2 = 23 | val range = findContiguous(0, 2) 24 | range.min + range.max 25 | -------------------------------------------------------------------------------- /src/main/scala/2020/Day10.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2020/day-10.txt").getLines.toList 2 | 3 | val joltages = input.map(_.toInt).sorted 4 | 5 | val adjPairs = joltages.zip(joltages.tail) 6 | val oneDifferences = adjPairs.count(_ + 1 == _) 7 | val threeDifferences = adjPairs.count(_ + 3 == _) 8 | 9 | val ans1 = (oneDifferences + 1) * (threeDifferences + 1) 10 | 11 | joltages.size 12 | 13 | val memo = collection.mutable.Map[(Int, List[Int]), Long]() 14 | 15 | def arrangements(current: Int, remaining: List[Int]): Long = 16 | memo.getOrElseUpdate( 17 | (current, remaining), 18 | remaining match 19 | case last :: Nil if last <= current + 3 => 1L 20 | case j :: js if j <= current + 3 => 21 | arrangements(current, js) + arrangements(j, js) 22 | case _ => 0L 23 | ) 24 | 25 | val ans2 = arrangements(0, joltages) 26 | -------------------------------------------------------------------------------- /src/main/scala/2021/Day12.worksheet.sc: -------------------------------------------------------------------------------- 1 | val edges: List[(String, String)] = io.Source.fromResource("2021/day-12-1-test.txt") 2 | .getLines.toList.flatMap { case s"$a-$b" => List(a -> b, b -> a) } 3 | 4 | val adjacent: Map[String, List[String]] = edges.groupMap(_._1)(_._2).toMap 5 | 6 | def search(path: List[String], twiceAllowed: Boolean): List[List[String]] = 7 | adjacent(path.head).flatMap { 8 | case "start" => Nil 9 | case "end" => ("end" :: path).reverse :: Nil 10 | case b if b.head.isUpper => search(b :: path, twiceAllowed) 11 | case s if !path.contains(s) => search(s :: path, twiceAllowed) 12 | case s if twiceAllowed => search(s :: path, twiceAllowed = false) 13 | case _ => Nil 14 | } 15 | 16 | search(List("start"), twiceAllowed = false).size 17 | search(List("start"), twiceAllowed = true).size 18 | -------------------------------------------------------------------------------- /src/main/scala/2020/Day03.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2020/day-03.txt").getLines.toList 2 | 3 | val width = input(0).length 4 | 5 | def cols = Iterator.from(0).map(_ * 3).map(_ % width) 6 | 7 | cols.take(10) foreach println 8 | 9 | val ans = input.zip(cols).count { 10 | case (row, col) => row(col) == '#' 11 | } 12 | 13 | println(ans) 14 | 15 | val slopes = List(1, 3, 5, 7) 16 | val ans2 = 17 | slopes.map { s => 18 | val cols = Iterator.from(0).map(_ * s).map(_ % width) 19 | input.zip(cols).count { 20 | case (row, col) => row(col) == '#' 21 | } 22 | }.map(_.toLong).product * { 23 | val everyOther = input.zipWithIndex.collect { 24 | case (row, i) if i%2 == 0 => row 25 | } 26 | val cols = Iterator.from(0).map(_ % width) 27 | everyOther.zip(cols).count { 28 | case (row, col) => row(col) == '#' 29 | }.toLong 30 | } 31 | -------------------------------------------------------------------------------- /src/main/scala/synacor/Main.scala: -------------------------------------------------------------------------------- 1 | package synacor 2 | 3 | import numbers.* 4 | 5 | @main def main(): Unit = 6 | def resource = Thread.currentThread() 7 | .getContextClassLoader 8 | .getResourceAsStream("synacor/challenge.bin") 9 | 10 | val memory = util.Using(resource): is => 11 | val bytes = is.readAllBytes() 12 | assert(bytes.size % 2 == 0) 13 | assert(bytes.sizeIs <= 0x10000) 14 | val a = Array.ofDim[Word](0x8000) 15 | 16 | for i <- bytes.indices by 2 do 17 | a(i / 2) = Word.fromBytes(bytes(i), bytes(i + 1)) 18 | 19 | Memory(a.toVector) 20 | .get 21 | 22 | val start = VMState( 23 | pc = Adr.fromInt(0), 24 | registers = Registers.init, 25 | stack = Nil, 26 | memory 27 | ) 28 | 29 | Iterator.unfold(start)(_.step).flatten.foreach: ch => 30 | print(ch) 31 | Thread.sleep(80) 32 | 33 | println("Synacor VM Halted") 34 | -------------------------------------------------------------------------------- /src/main/scala/synacor/Play.worksheet.sc: -------------------------------------------------------------------------------- 1 | import synacor.* 2 | import synacor.numbers.* 3 | 4 | def resource = Thread.currentThread() 5 | .getContextClassLoader 6 | .getResourceAsStream("synacor/challenge.bin") 7 | 8 | val memory = util.Using(resource): is => 9 | val bytes = is.readAllBytes() 10 | assert(bytes.size % 2 == 0) 11 | assert(bytes.sizeIs <= 0x10000) 12 | val a = Array.ofDim[Word](0x8000) 13 | 14 | for i <- bytes.indices by 2 do 15 | a(i / 2) = Word.fromBytes(bytes(i), bytes(i + 1)) 16 | 17 | Memory(a.toVector) 18 | 19 | memory.get 20 | 21 | val start = VMState( 22 | pc = Adr.fromInt(0), 23 | registers = Registers.init, 24 | stack = Nil, 25 | memory.get 26 | ) 27 | 28 | start.step 29 | 30 | // def states = Iterator.unfold(start)(_.step) 31 | 32 | // val ans = states.foreach: 33 | // case None => () 34 | // case Some(char) => print(char) 35 | 36 | // 37 | -------------------------------------------------------------------------------- /src/main/scala/2021/Day13.worksheet.sc: -------------------------------------------------------------------------------- 1 | case class Point(x: Int, y: Int): 2 | def foldX(line: Int): Point = copy(x = if x > line then 2 * line - x else x) 3 | def foldY(line: Int): Point = copy(y = if y > line then 2 * line - y else y) 4 | 5 | val input = io.Source.fromResource("2021/day-13-1.txt").getLines.toList 6 | 7 | val dots = input.collect { case s"$x,$y" => Point(x.toInt, y.toInt) }.toSet 8 | 9 | val instructions = input.collect[Point => Point] { 10 | case s"fold along x=$line" => _.foldX(line.toInt) 11 | case s"fold along y=$line" => _.foldY(line.toInt) 12 | } 13 | 14 | val ans1 = dots.map(instructions.head).size 15 | 16 | val code = instructions.foldLeft(dots)(_ map _) 17 | 18 | val ans2 = 19 | for y <- 0 to code.map(_.y).max 20 | yield for x <- 0 to code.map(_.x).max 21 | yield if code(Point(x, y)) then "#" else "." 22 | 23 | ans2.foreach(row => println(row.mkString)) 24 | // PERCGJPB 25 | -------------------------------------------------------------------------------- /src/main/scala/2023/Day04.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2023/day-04.txt").getLines().toList 2 | 3 | val matches: Map[Int, Set[Int]] = 4 | val tups = input.map: 5 | case s"Card $n: $left | $right" => 6 | val have = left.split(" +").filter(_.nonEmpty).map(_.toInt).toSet 7 | val winning = right.split(" +").filter(_.nonEmpty).map(_.toInt).toSet 8 | n.stripLeading.toInt -> have.intersect(winning) 9 | tups.toMap 10 | 11 | def score(n: Int) = 12 | math.pow(2, matches(n).size - 1).toInt 13 | 14 | val ans1 = matches.keysIterator.map(score).sum 15 | 16 | val counts = scala.collection.mutable.Map.empty[Int, Int] 17 | matches.keysIterator.foreach(counts(_) = 1) 18 | 19 | for 20 | n <- 1 to matches.size 21 | newCards = (n + 1) to (n + matches(n).size) 22 | c <- newCards if matches.contains(c) 23 | do 24 | counts(c) += counts(n) 25 | 26 | val ans2 = counts.valuesIterator.sum 27 | -------------------------------------------------------------------------------- /src/main/scala/2015/Day18.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2015/day-18.txt").getLines().toVector 2 | 3 | import aoc.{Point, Area} 4 | 5 | val area = Area(input) 6 | 7 | val init = Set.from[Point]: 8 | area.pointsIterator.filter: p => 9 | input(p) == '#' 10 | 11 | def next(state: Set[Point]): Set[Point] = Set.from[Point]: 12 | area.pointsIterator.filter: p => 13 | (state(p), p.surrounding.count(state)) match 14 | case (true, 2|3) => true 15 | case (true, _) => false 16 | case (false, 3) => true 17 | case (false, _) => false 18 | 19 | val states = LazyList.iterate(init)(next) 20 | val ans1 = states(100).size 21 | 22 | val corners = Set(area.topLeft, area.topRight, area.botLeft, area.botRight) 23 | 24 | def next2(state: Set[Point]): Set[Point] = 25 | next(state.union(corners)).union(corners) 26 | 27 | val states2 = LazyList.iterate(init)(next2) 28 | val ans2 = states2(100).size 29 | -------------------------------------------------------------------------------- /src/main/scala/2021/Day03.worksheet.sc: -------------------------------------------------------------------------------- 1 | val report = io.Source.fromResource("2021/day-03-1.txt").getLines.toList 2 | 3 | val mostCommon = report.transpose.map(_.groupBy(identity).maxBy(_._2.size)._1) 4 | 5 | val gammaRate = Integer.parseInt(mostCommon.mkString, 2) 6 | val epsilonRate = gammaRate ^ Integer.parseInt("1" * mostCommon.length, 2) 7 | 8 | val oxyBits = Iterator.unfold(report) { kept => 9 | Option.when(kept.exists(_.nonEmpty)) { 10 | kept.groupMap(_.head)(_.tail).maxBy((h, t) => t.size -> h) 11 | } 12 | } 13 | 14 | val co2Bits = Iterator.unfold(report) { kept => 15 | Option.when(kept.exists(_.nonEmpty)) { 16 | kept.groupMap(_.head)(_.tail).minBy((h, t) => t.size -> h) 17 | } 18 | } 19 | 20 | val oxyRating = Integer.parseInt(oxyBits.mkString, 2) 21 | val co2Rating = Integer.parseInt(co2Bits.mkString, 2) 22 | 23 | val powerConsumption = gammaRate * epsilonRate 24 | val lifeSupportRating = oxyRating * co2Rating 25 | -------------------------------------------------------------------------------- /src/main/scala/2023/Day02.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2023/day-02.txt").getLines().toList 2 | 3 | case class Game(id: Int, hands: List[Map[String, Int]]) 4 | 5 | val games = input.map: 6 | case s"Game $id: $handsStr" => 7 | val hands = handsStr.split("; ").toList.map: str => 8 | str.split(", ").collect { 9 | case s"$n blue" => "blue" -> n.toInt 10 | case s"$n red" => "red" -> n.toInt 11 | case s"$n green" => "green" -> n.toInt 12 | }.toMap.withDefaultValue(0) 13 | Game(id.toInt, hands) 14 | 15 | val possibleGames = games 16 | .filter(_.hands.map(_("red")).max <= 12) 17 | .filter(_.hands.map(_("green")).max <= 13) 18 | .filter(_.hands.map(_("blue")).max <= 14) 19 | 20 | def power(game: Game): Int = 21 | game.hands.map(_("red")).max * game.hands.map(_("green")).max * game.hands.map(_("blue")).max 22 | 23 | val ans = possibleGames.map(_.id).sum 24 | val ans2 = games.map(power).sum 25 | -------------------------------------------------------------------------------- /src/main/scala/2023/Day01.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2023/day-01.txt").getLines().toList 2 | 3 | def digitIndices(s: String): Seq[(Int, Int)] = 4 | s.zipWithIndex.collect: 5 | case (c, i) if c.isDigit => (c.asDigit, i) 6 | 7 | val digitWords = Vector("zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine") 8 | def wordToDigit(word: String): Int = digitWords.indexOf(word) 9 | 10 | def wordIndices(s: String): Seq[(Int, Int)] = 11 | digitWords.flatMap: word => 12 | val occurences = Iterator.unfold(0): index => 13 | val i = s.indexOf(word, index) 14 | Option.when(i != -1): 15 | (wordToDigit(word), i) -> (i + 1) 16 | occurences.toVector 17 | 18 | val calibrationNumbers = input.map: line => 19 | val indices = (wordIndices(line) ++ digitIndices(line)) 20 | val (first, _) = indices.minBy(_._2) 21 | val (last, _) = indices.maxBy(_._2) 22 | first * 10 + last 23 | 24 | val ans = calibrationNumbers.sum 25 | -------------------------------------------------------------------------------- /src/main/scala/2024/Day05.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2024/day-05.txt").getLines.toList 2 | 3 | val pageOrderingRules = input.collect: 4 | case s"$before|$after" => before.toInt -> after.toInt 5 | 6 | val dependencies: Map[Int, Set[Int]] = 7 | pageOrderingRules 8 | .groupMap(_._2)(_._1) 9 | .view.mapValues(_.toSet) 10 | .toMap.withDefaultValue(Set.empty) 11 | 12 | val updates = input.collect: 13 | case line if line.contains(",") => 14 | line.split(",").map(_.toInt).toList 15 | 16 | def correctOrder(pages: List[Int]) = 17 | pages.reverse.tails.forall: 18 | case Nil => true 19 | case page :: before => 20 | before.forall(dependencies(page)) 21 | 22 | def middlePage(pages: List[Int]) = 23 | pages(pages.size / 2) 24 | 25 | val ans1 = updates.filter(correctOrder).map(middlePage).sum 26 | 27 | val ans2 = updates.filterNot(correctOrder) 28 | .map(_.sortWith((b, a) => dependencies(a).contains(b))) 29 | .map(middlePage) 30 | .sum 31 | -------------------------------------------------------------------------------- /src/main/scala/2023/Day08.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2023/day-08.txt").getLines().toList 2 | 3 | val directions = input.head 4 | 5 | val adj = input.drop(2).foldLeft(Map.empty[String, (String, String)]): 6 | case (m, s"$start = ($left, $right)") => m.updated(start, (left, right)) 7 | 8 | def move(pos: String, dir: Char): String = 9 | val (left, right) = adj(pos) 10 | dir match 11 | case 'L' => left 12 | case 'R' => right 13 | 14 | def infDir: LazyList[Char] = LazyList(directions*) #::: infDir 15 | 16 | def positions = infDir.scanLeft("AAA")(move) 17 | 18 | val ans1 = positions.indexWhere(_ == "ZZZ") 19 | 20 | val starts = adj.keys.filter(_.endsWith("A")).toList 21 | 22 | val destinations = adj.keysIterator 23 | .map(start => start -> directions.foldLeft(start)(move)) 24 | .toMap 25 | 26 | val periods = starts.map: start => 27 | Iterator.iterate(start)(destinations).indexWhere(_.endsWith("Z")) 28 | 29 | val ans2 = periods.map(_.toLong).product * directions.length 30 | -------------------------------------------------------------------------------- /src/main/scala/2024/Day10.worksheet.sc: -------------------------------------------------------------------------------- 1 | val topoMap = io.Source.fromResource("2024/day-10.txt").getLines.toVector 2 | 3 | import aoc.{Area, Point} 4 | 5 | val area = Area(topoMap) 6 | 7 | val trailHeads = area.pointsIterator 8 | .filter(topoMap(_) == '0') 9 | .toList 10 | 11 | def height(p: Point): Int = 12 | val c = topoMap(p) 13 | if c.isDigit then c.asDigit else -1 14 | 15 | def climb(p: Point): List[Point] = 16 | val h = height(p) 17 | p.adjacent.filter(area.contains).filter(h + 1 == height(_)) 18 | .toList 19 | 20 | def score1(p: Point): Int = 21 | val h = height(p) 22 | if h == 9 then 1 else 23 | climb(p).map(score1).sum 24 | 25 | def peaks(p: Point): Set[Point] = 26 | val h = height(p) 27 | println(" " * h + p.toString + s" $h") 28 | if h == 9 then Set(p) else 29 | climb(p).map(peaks).foldLeft(Set.empty[Point])(_ union _) 30 | 31 | def score2(p: Point): Int = peaks(p).size 32 | 33 | val ans1 = trailHeads.map(score1).sum 34 | val ans2 = trailHeads.map(score2).sum 35 | -------------------------------------------------------------------------------- /src/main/scala/2021/Day04.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2021/day-04-1.txt").getLines.toVector 2 | 3 | type Board = Vector[Vector[Int]] 4 | 5 | val numbers = input.head.split(",").map(_.toInt).toList 6 | 7 | val boards: List[Board] = input.tail 8 | .filter(_.nonEmpty) 9 | .grouped(5) 10 | .map(_.map(_.strip.split(" ").flatMap(_.toIntOption).toVector)) 11 | .toList 12 | 13 | extension (board: Board) 14 | def bingo(marked: List[Int]): Boolean = { 15 | val rowBingo = board.exists(_.forall(marked.contains)) 16 | val colBingo = board.transpose.exists(_.forall(marked.contains)) 17 | rowBingo || colBingo 18 | } 19 | 20 | val prefixes = numbers.scanLeft(List[Int]())(_ :+ _) 21 | 22 | def score(board: Board): Option[(Int, Int)] = { 23 | prefixes.find(board.bingo).map { marked => 24 | board.flatten.filterNot(marked.contains).sum * marked.last -> marked.size 25 | } 26 | } 27 | 28 | val ans1 = boards.flatMap(score).minBy(_._2)._1 29 | val ans2 = boards.flatMap(score).maxBy(_._2)._1 30 | -------------------------------------------------------------------------------- /src/main/scala/2022/Day05.worksheet.sc: -------------------------------------------------------------------------------- 1 | type State = Vector[String] 2 | 3 | val lines = io.Source.fromResource("2022/day-05.txt").getLines() 4 | val drawing = lines.takeWhile(_.nonEmpty).toVector 5 | val steps = lines.toList 6 | 7 | val start: State = drawing.init.transpose.collect { 8 | case col if col.exists(_.isLetter) => col.mkString.stripLeading 9 | } 10 | 11 | def move(state: State, step: String, multi: Boolean): State = step match 12 | case s"move $c from $s to $d" => 13 | val count = c.toInt 14 | val source = s.toInt - 1 15 | val dest = d.toInt - 1 16 | val moving = state(source).take(count) 17 | 18 | state 19 | .updated(source, state(source).drop(count)) 20 | .updated(dest, (if multi then moving else moving.reverse) ++ state(dest)) 21 | 22 | val finalState1 = steps.foldLeft(start)(move(_, _, multi = false)) 23 | val ans1 = finalState1.map(_.head).mkString 24 | 25 | val finalState2 = steps.foldLeft(start)(move(_, _, multi = true)) 26 | val ans2 = finalState2.map(_.head).mkString 27 | -------------------------------------------------------------------------------- /src/main/scala/2021/Day05.worksheet.sc: -------------------------------------------------------------------------------- 1 | import io.Source 2 | import util.Using 3 | 4 | val lines = Using(Source.fromResource("2021/day-05-1.txt")) { 5 | _.getLines.map { case s"$x1,$y1 -> $x2,$y2" => 6 | Line(Point(x1.toInt, y1.toInt), Point(x2.toInt, y2.toInt)) 7 | }.toSeq 8 | }.get 9 | 10 | case class Point(x: Int, y: Int) 11 | 12 | case class Line(p: Point, q: Point): 13 | val xDist = q.x - p.x 14 | val yDist = q.y - p.y 15 | 16 | def horizontal = yDist == 0 17 | def vertical = xDist == 0 18 | 19 | def xRange = (p.x to q.x by xDist.sign) 20 | def yRange = (p.y to q.y by yDist.sign) 21 | 22 | def points: Seq[Point] = 23 | if horizontal then xRange.map(Point(_, p.y)) 24 | else if vertical then yRange.map(Point(p.x, _)) 25 | else (xRange zip yRange).map(Point(_, _)) 26 | 27 | val ans1 = lines 28 | .filter(l => l.horizontal || l.vertical) 29 | .flatMap(_.points) 30 | .groupBy(identity) 31 | .count(_._2.size > 1) 32 | 33 | val ans2 = lines 34 | .flatMap(_.points) 35 | .groupBy(identity) 36 | .count(_._2.size > 1) 37 | -------------------------------------------------------------------------------- /src/main/scala/2015/Day14.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2015/day-14.txt").getLines().toList 2 | 3 | case class Deer(speed: Int, endurance: Int, rest: Int) 4 | 5 | val deer = input.collect: 6 | case s"$name can fly $speed km/s for $endurance seconds, but then must rest for $rest seconds." => 7 | Deer(speed.toInt, endurance.toInt, rest.toInt) 8 | 9 | def distance(deer: Deer, time: Int): Int = 10 | val cycleTime = deer.endurance + deer.rest 11 | val cycleDist = deer.speed * deer.endurance 12 | val numFullCycles = time / cycleTime 13 | 14 | val timeRemaining = time - numFullCycles * cycleTime 15 | val lastLegRunTime = deer.endurance.min(timeRemaining) 16 | val lastLegDistance = lastLegRunTime * deer.speed 17 | 18 | cycleDist * numFullCycles + lastLegDistance 19 | 20 | val ans1 = deer.map(distance(_, 2503)).max 21 | 22 | val scoreCard = (1 to 2503).map: time => 23 | val positions = deer.map(distance(_, time)) 24 | val ahead = positions.max 25 | positions.map(p => if p == ahead then 1 else 0) 26 | 27 | val ans2 = scoreCard.transpose.map(_.sum).max 28 | -------------------------------------------------------------------------------- /src/main/scala/2015/Day19.worksheet.sc: -------------------------------------------------------------------------------- 1 | // val input = io.Source.fromResource("2015/day-19.txt").getLines().toList 2 | 3 | val input = List( 4 | "H => HO", 5 | "H => OH", 6 | "O => HH", 7 | "e => H", 8 | "e => O", 9 | "HOH" 10 | ) 11 | 12 | val replacements: List[(String, String)] = input.collect: 13 | case s"$a => $b" => a -> b 14 | // .groupMap(_._1)(_._2) 15 | 16 | val medicine = input.last 17 | 18 | def newMolecules(left: String, right: String, molecule: String): List[String] = 19 | def sliceIndices(from: Int): List[Int] = 20 | molecule.indexOfSlice(left, from) match 21 | case -1 => Nil 22 | case i => i :: sliceIndices(i + 1) 23 | 24 | sliceIndices(0).map(molecule.patch(_, right, left.length)) 25 | 26 | val ms = replacements.flatMap(newMolecules(_, _, medicine)) 27 | 28 | ms foreach println 29 | 30 | ms.size 31 | val ans1 = ms.distinct.size 32 | 33 | // def search(): Int = 34 | // import collection.mutable.{PriorityQueue, Map} 35 | // val cost = Map[String, Int] 36 | // ??? 37 | 38 | 39 | 40 | 41 | 42 | // 480 too low 43 | // 448 too low 44 | -------------------------------------------------------------------------------- /src/main/scala/2023/Day06.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2023/day-06.txt").getLines() 2 | 3 | val rawTime = input.next() 4 | val rawDistance = input.next() 5 | 6 | def waysToWin(timeLimit: Long, recordDistance: Long): Long = 7 | val z1 = (timeLimit + math.sqrt(timeLimit.toDouble * timeLimit - 4 * recordDistance)) / 2 8 | val z2 = (timeLimit - math.sqrt(timeLimit.toDouble * timeLimit - 4 * recordDistance)) / 2 9 | 10 | val max = (z1 max z2).ceil.toLong 11 | val min = (z1 min z2).floor.toLong 12 | println(min to max) 13 | 14 | (min to max).size - 2 15 | 16 | val timeLimits = rawTime.stripPrefix("Time:").trim.split(" +").map(_.toLong).toList 17 | val recordDistances = rawDistance.stripPrefix("Distance:").trim().split(" +").map(_.toLong).toList 18 | 19 | timeLimits.zip(recordDistances).map(waysToWin) 20 | val ans1 = timeLimits.zip(recordDistances).map(waysToWin).product 21 | 22 | val timeLimit = rawTime.stripPrefix("Time:").split(" ").mkString.toLong 23 | val recordDistance = rawDistance.stripPrefix("Distance:").split(" ").mkString.toLong 24 | 25 | val ans2 = waysToWin(timeLimit, recordDistance) 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Stewart Stewart 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/scala/2019/Day02.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2019/day-02.txt").getLines().next() 2 | val intcode = input.split(',').map(_.toInt).toVector 3 | 4 | def step(intcode: Vector[Int], pc: Int): Option[(Vector[Int], Int)] = 5 | if intcode(pc) == 99 then None else 6 | intcode.slice(pc, pc + 4) match 7 | case Vector(1, x, y, addr) => 8 | Some(intcode.updated(addr, intcode(x) + intcode(y)) -> (pc + 4)) 9 | case Vector(2, x, y, addr) => 10 | Some(intcode.updated(addr, intcode(x) * intcode(y)) -> (pc + 4)) 11 | case _ => ??? 12 | 13 | val test = Vector(1,9,10,3,2,3,11,0,99,30,40,50) 14 | 15 | step(test, 0) 16 | 17 | def run(initialState: Vector[Int]): Vector[Int] = 18 | LazyList.unfold(initialState -> 0) { 19 | step(_, _).map { case (state, pc) => state -> (state, pc) } 20 | }.last 21 | 22 | run(Vector(1,0,0,0,99)) 23 | 24 | val ans1 = run(intcode.updated(1, 12).updated(2, 2))(0) 25 | 26 | val ans2 = 27 | for 28 | noun <- 0 to 99 29 | verb <- 0 to 99 30 | start = intcode.updated(1, noun).updated(2, verb) 31 | if run(start)(0) == 19690720 32 | yield 33 | noun -> verb 34 | -------------------------------------------------------------------------------- /src/main/scala/2015/Day11.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = "vzbxkghb" 2 | 3 | def hasStraight(pw: String) = 4 | pw.sliding(3).exists: 5 | s => s(0) + 1 == s(1) && s(1) + 1 == s(2) 6 | 7 | def hasTwoPair(pw: String) = 8 | val pairs = pw.zip(pw.tail) 9 | val i = pw.zip(pw.tail).indexWhere(_ == _) 10 | pairs.indexWhere(_ == _, i + 2) != -1 11 | 12 | def increment(c: Char): (Char, Boolean) = 13 | val c2 = ((c + 1 - 97) % 26 + 97).toChar match 14 | case 'i' => 'j' 15 | case 'o' => 'p' 16 | case 'l' => 'm' 17 | case c => c 18 | c2 -> (c2 == 'a') 19 | 20 | def increment(pw: String): String = 21 | val (ones, carry) = increment(pw.last) 22 | 23 | val (next, overflow) = pw.init.foldRight(ones.toString, carry): 24 | case (digit, (acc, carry)) => 25 | if carry then 26 | val (d, c) = increment(digit) 27 | (d +: acc, c) 28 | else 29 | (digit +: acc, carry) 30 | next 31 | 32 | val ans1 = Iterator.iterate(input)(increment).find: 33 | pw => hasStraight(pw) && hasTwoPair(pw) 34 | .get 35 | 36 | val ans2 = Iterator.iterate(ans1)(increment).drop(1).find: 37 | pw => hasStraight(pw) && hasTwoPair(pw) 38 | .get 39 | -------------------------------------------------------------------------------- /src/main/scala/2022/Day09.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2022/day-09.txt").getLines().toList 2 | 3 | case class Point(x: Int, y: Int) 4 | type Rope = List[Point] 5 | type Move = Point => Point 6 | 7 | def chase(h: Point, t: Point): Move = 8 | val dx = h.x - t.x 9 | val dy = h.y - t.y 10 | val noGap = dx.abs <= 1 && dy.abs <= 1 11 | if noGap then identity 12 | else p => Point(p.x + dx.sign, p.y + dy.sign) 13 | 14 | extension (rope: Rope) 15 | def move(f: Move): Rope = rope match 16 | case h :: n :: tail => 17 | f(h) :: (n :: tail).move(chase(f(h), n)) 18 | case r => r.map(f) 19 | 20 | val movements: List[Move] = input.flatMap { 21 | case s"U $n" => List.fill(n.toInt)(p => p.copy(y = p.y + 1)) 22 | case s"D $n" => List.fill(n.toInt)(p => p.copy(y = p.y - 1)) 23 | case s"R $n" => List.fill(n.toInt)(p => p.copy(x = p.x + 1)) 24 | case s"L $n" => List.fill(n.toInt)(p => p.copy(x = p.x - 1)) 25 | } 26 | 27 | def ans(rope: Rope) = 28 | val states = movements.scanLeft(rope)(_ move _) 29 | states.map(_.last).toSet.size 30 | 31 | val ans1 = ans(List.fill(2)(Point(0, 0))) 32 | val ans2 = ans(List.fill(10)(Point(0, 0))) 33 | -------------------------------------------------------------------------------- /src/main/scala/2015/Day05.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2015/day-05.txt").getLines().toList 2 | 3 | val vowels = "aeiou".toSet 4 | val badStrings = List("ab", "cd", "pq", "xy") 5 | 6 | def nice(s: String) = 7 | val threeVowels = s.count(vowels) >= 3 8 | val duplicate = s.sliding(2).exists(p => p.head == p.last) 9 | val noBadStrings = !badStrings.exists(s.contains) 10 | threeVowels && duplicate && noBadStrings 11 | 12 | input.count(nice) 13 | 14 | nice("aaa") 15 | nice("ugknbfddgicrmopn") 16 | 17 | def pairAppearsTwice(s: String): Boolean = 18 | val appearances: Map[String, List[Int]] = 19 | s.sliding(2).zipWithIndex.toList.groupMap((pair, index) => pair)(_._2) 20 | 21 | appearances.exists: 22 | case (pair, indices) => 23 | indices.sizeIs > 2 || ( 24 | indices.sizeIs == 2 && (indices.head - indices.last).abs != 1 25 | ) 26 | 27 | def repeatWithGap(s: String) = 28 | s.sliding(3).exists: 29 | s => s.head == s.last 30 | 31 | pairAppearsTwice("aaa") 32 | pairAppearsTwice("aaaa") 33 | pairAppearsTwice("xyaaaaaxy") 34 | 35 | val ans2 = input.count(s => pairAppearsTwice(s) && repeatWithGap(s)) 36 | 37 | // 38 | -------------------------------------------------------------------------------- /src/main/scala/2015/Day16.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2015/day-16.txt").getLines().toList 2 | 3 | val forensics = Map( 4 | "children" -> 3, 5 | "cats" -> 7, 6 | "samoyeds" -> 2, 7 | "pomeranians" -> 3, 8 | "akitas" -> 0, 9 | "vizslas" -> 0, 10 | "goldfish" -> 5, 11 | "trees" -> 3, 12 | "cars" -> 2, 13 | "perfumes" -> 1, 14 | ) 15 | 16 | val aunts = input.collect: 17 | case s"Sue $id: $things" => 18 | id -> things.split(',').map(_.strip()).toList.collect: 19 | case s"$thing: $n" => thing -> n.toInt 20 | 21 | val matches = aunts.filter: 22 | case (id, things) => things.forall: 23 | case (thing, n) if forensics.contains(thing) => forensics(thing) == n 24 | case _ => true 25 | 26 | val ans1 = matches.head._1 27 | 28 | val matches2 = aunts.filter: 29 | case (id, things) => things.forall: 30 | case (thing, n) if !forensics.contains(thing) => true 31 | case (thing@("cats" | "trees"), n) => forensics(thing) < n 32 | case (thing@("pomeranians" | "goldfish"), n) => forensics(thing) > n 33 | case (thing, n) => forensics(thing) == n 34 | 35 | matches2.size 36 | val ans2 = matches2.head._1 37 | -------------------------------------------------------------------------------- /src/main/scala/2025/Day06.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2025/day-06.txt").getLines().toList 2 | 3 | val numbers: Vector[Vector[Long]] = input.toVector.init.collect: 4 | line => line.split(' ').toVector.flatMap(_.toLongOption) 5 | 6 | val ops = input.last.split(' ').toVector.filter(_.nonEmpty).map(_.head) 7 | 8 | val ans1 = ops.zip(numbers.transpose).map: 9 | case ('*', nums) => nums.product 10 | case ('+', nums) => nums.sum 11 | .sum 12 | 13 | val maxLength = input.map(_.length).max 14 | input.map(_.length).distinct 15 | 16 | val nums2 = input.init.transpose.map: col => 17 | col.filterNot(_.isWhitespace).mkString.toLongOption 18 | 19 | val opLine = input.last 20 | 21 | val initOp = opLine.head 22 | 23 | val ans2 = opLine.zip(nums2).foldLeft(opLine.head -> List.empty[Long]): 24 | case ((op, acc), (' ', None)) => op -> acc 25 | case ((op, acc), ('*', Some(n))) => '*' -> (n::acc) 26 | case ((op, acc), ('+', Some(n))) => '+' -> (n::acc) 27 | case ((op, acc::rest), (' ', Some(n))) => op match 28 | case '*' => op -> ((acc*n) :: rest) 29 | case '+' => op -> ((acc+n) :: rest) 30 | ._2.sum 31 | 32 | // 13837321028548 33 | // 3393053451590101 34 | 35 | // 36 | -------------------------------------------------------------------------------- /src/main/scala/2024/Day18.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2024/day-18.txt").getLines.toList 2 | 3 | import aoc.{Area, Point} 4 | 5 | val area = Area(0 to 70, 0 to 70) 6 | 7 | val bytes: List[Point] = input.collect: 8 | case s"$x,$y" => Point(x.toInt, y.toInt) 9 | 10 | val grid1024: Set[Point] = bytes.take(1024).toSet 11 | 12 | val start = area.topLeft 13 | val end = area.botRight 14 | 15 | def floodFill(blocked: Set[Point]): Iterator[Set[Point]] = 16 | Iterator.unfold(Set.empty[Point] -> Set(start)): 17 | case (prev, curr) => Option.when(curr.nonEmpty): 18 | val next = curr.flatMap(_.adjacent) 19 | .diff(prev) 20 | .diff(blocked) 21 | .filter(_.inBounds(area)) 22 | 23 | curr -> (curr -> next) 24 | 25 | val ans1 = floodFill(grid1024).indexWhere(_.contains(end)) 26 | 27 | def reachable(n: Int): Boolean = 28 | val blocked = bytes.take(n + 1).toSet 29 | floodFill(blocked).exists(_.contains(end)) 30 | 31 | def bs(range: Range): Point = 32 | if range.size == 1 then bytes(range.start) else 33 | val (left, right) = range.splitAt(range.size / 2) 34 | if reachable(left.last) then bs(right) else bs(left) 35 | 36 | val ans2 = bs(bytes.indices.drop(1024)) 37 | -------------------------------------------------------------------------------- /src/main/scala/2022/Day18.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2022/day-18.txt").getLines().toList 2 | 3 | val cubes = input.collect { case s"$x,$y,$z" => 4 | Point(x.toInt, y.toInt, z.toInt) 5 | }.toSet 6 | 7 | val xRange = (cubes.map(_.x).min - 1) to (cubes.map(_.x).max + 1) 8 | val yRange = (cubes.map(_.y).min - 1) to (cubes.map(_.y).max + 1) 9 | val zRange = (cubes.map(_.z).min - 1) to (cubes.map(_.z).max + 1) 10 | 11 | case class Point(x: Int, y: Int, z: Int): 12 | def inBounds(p: Point) = 13 | xRange.contains(x) && yRange.contains(y) && zRange.contains(z) 14 | 15 | def adj = Set( 16 | copy(x = x + 1), 17 | copy(y = y + 1), 18 | copy(z = z + 1), 19 | copy(x = x - 1), 20 | copy(y = y - 1), 21 | copy(z = z - 1) 22 | ).filter(inBounds) 23 | 24 | val ans1 = cubes.iterator.map(_.adj.diff(cubes).size).sum 25 | 26 | val start = Point(xRange.min, yRange.min, zRange.min) 27 | 28 | def search = Iterator.unfold(Set.empty[Point], Set(start)) { (visited, visiting) => 29 | Option.when(visiting.nonEmpty) { 30 | val (blocked, air) = visiting.flatMap(_.adj.diff(visited)).partition(cubes) 31 | blocked.size -> (visited ++ visiting, air) 32 | } 33 | } 34 | 35 | val ans2 = search.sum 36 | -------------------------------------------------------------------------------- /src/main/scala/2022/Day10.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2022/day-10.txt").getLines().toList 2 | 3 | case class Point(x: Int, y: Int) 4 | 5 | case class State(clock: Int, x: Int, crt: Map[Point, Boolean]): 6 | val pixel = Point((clock - 1) % 40, (clock - 1) / 40) 7 | def tick = copy(clock + 1) 8 | def draw = copy(crt = crt.updated(pixel, (pixel.x - x).abs <= 1)) 9 | def addX(v: Int) = copy(x = x + v.toInt) 10 | 11 | def step(inst: String): State = inst match 12 | case "noop" => draw.tick 13 | case s"addx $v" => draw.tick.draw.addX(v.toInt).tick 14 | 15 | val start = State(clock = 1, x = 1, crt = Map.empty) 16 | val states = input.scanLeft(start)(_ step _) 17 | val cycles = List(20, 60, 100, 140, 180, 220) 18 | 19 | val signalStrengths = List.unfold(cycles, states) { case (cycles, states) => 20 | cycles.headOption.map { cycle => 21 | val (before, after) = states.span(_.clock <= cycle) 22 | before.last.x * cycle -> (cycles.tail, after) 23 | } 24 | } 25 | 26 | val ans1 = signalStrengths.sum 27 | 28 | val finalCrt = states.last.crt 29 | 30 | for y <- 0 to 5 do 31 | for x <- 0 to 39 do 32 | val p = Point(x, y) 33 | print(if finalCrt(p) then '\u2588' else ' ') 34 | println() 35 | -------------------------------------------------------------------------------- /src/main/scala/2023/Day13.worksheet.sc: -------------------------------------------------------------------------------- 1 | import aoc.* 2 | 3 | val input = io.Source.fromResource("2023/day-13.txt").getLines() 4 | 5 | val rockMaps: List[(Set[Point], Area)] = 6 | val lb = collection.mutable.ListBuffer.empty[(Set[Point], Area)] 7 | while input.hasNext do 8 | val grid = input.takeWhile(_.nonEmpty).toVector 9 | val area = Area(grid) 10 | val points = area.pointsIterator.filter(grid(_) == '#').toSet 11 | lb += (points -> area) 12 | lb.result() 13 | 14 | def findMirror(points: Set[Point], area: Area, smudge: Int): Option[Int] = 15 | val flipped = points.map(p => Point(area.right - p.x, p.y)) 16 | (1 to area.right).find: cols => 17 | val shift = 2 * cols - area.width 18 | val shifted = flipped.map(p => p.copy(x = p.x + shift)) 19 | val overlap = area.intersect(Area.bounding(shifted)).get 20 | points.filter(overlap(_)).diff(shifted).sizeIs == smudge 21 | 22 | def score(points: Set[Point], area: Area, smudge: Int) = 23 | val vert = findMirror(points, area, smudge) 24 | val horiz = findMirror(points.map(_.swap), area.transpose, smudge) 25 | horiz.fold(0)(_ * 100) + vert.getOrElse(0) 26 | 27 | val ans1 = rockMaps.map(score(_, _, 0)).sum 28 | val ans2 = rockMaps.map(score(_, _, 1)).sum 29 | -------------------------------------------------------------------------------- /src/main/scala/2022/Day02.worksheet.sc: -------------------------------------------------------------------------------- 1 | val guide = io.Source.fromResource("2022/day-02.txt").getLines().toList 2 | 3 | enum Outcome: 4 | case Loss, Draw, Win 5 | val score = this.ordinal * 3 6 | 7 | import Outcome.* 8 | 9 | enum Hand: 10 | case Rock, Paper, Scissors 11 | val score = this.ordinal + 1 12 | 13 | def outcomeAgainst(other: Hand) = 14 | val diff = (this.ordinal - other.ordinal + 3) % 3 15 | if diff == 1 then Win 16 | else if diff == 2 then Loss 17 | else Draw 18 | 19 | def choiceFor(outcome: Outcome) = outcome match 20 | case Draw => this 21 | case Win => Hand.fromOrdinal((this.ordinal + 1) % 3) 22 | case Loss => Hand.fromOrdinal((this.ordinal + 2) % 3) 23 | 24 | val ans1 = guide.map { 25 | case s"$o $c" => 26 | val opponent = Hand.fromOrdinal("ABC".indexOf(o)) 27 | val choice = Hand.fromOrdinal("XYZ".indexOf(c)) 28 | val outcome = choice.outcomeAgainst(opponent) 29 | choice.score + outcome.score 30 | }.sum 31 | 32 | val ans2 = guide.map { 33 | case s"$o $c" => 34 | val opponent = Hand.fromOrdinal("ABC".indexOf(o)) 35 | val outcome = Outcome.fromOrdinal("XYZ".indexOf(c)) 36 | val choice = opponent.choiceFor(outcome) 37 | choice.score + outcome.score 38 | }.sum 39 | -------------------------------------------------------------------------------- /src/main/scala/2021/Day14.worksheet.sc: -------------------------------------------------------------------------------- 1 | import util.chaining.* 2 | 3 | val input = io.Source.fromResource("2021/day-14-1.txt").getLines.toList 4 | 5 | type Pair = (Char, Char) 6 | 7 | val template = input.head 8 | val insertionRules: Map[Pair, Char] = 9 | input.collect { case s"$pair -> $c" => (pair(0), pair(1)) -> c.head }.toMap 10 | 11 | val start: Map[Pair, Long] = 12 | Map.from(template.zip(template.tail).groupMapReduce(identity)(_ => 1L)(_ + _)) 13 | 14 | def step(pairCounts: Map[Pair, Long]): Map[Pair, Long] = 15 | pairCounts.foldLeft(Map.empty.withDefaultValue(0L)) { 16 | case (counting, ((l, r), lrCount)) => 17 | val n = insertionRules(l -> r) 18 | counting 19 | .updated(l -> n, counting(l -> n) + lrCount) 20 | .updated(n -> r, counting(n -> r) + lrCount) 21 | } 22 | 23 | def elementCounts(pairCounts: Map[Pair, Long]): Map[Char, Long] = 24 | pairCounts.foldLeft(Map(template.head -> 1L).withDefaultValue(0L)) { 25 | case (counting, ((_, r), count)) => 26 | counting.updated(r, counting(r) + count) 27 | } 28 | 29 | val quantityDiff = LazyList 30 | .iterate(start)(step) 31 | .map(elementCounts(_).values.pipe(c => c.max - c.min)) 32 | 33 | val ans1 = quantityDiff(10) 34 | val ans2 = quantityDiff(40) 35 | -------------------------------------------------------------------------------- /src/main/scala/2023/Day15.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2023/day-15.txt").mkString.trim.split(",").toList 2 | 3 | def hash(string: String): Int = 4 | string.foldLeft(0): (acc, char) => 5 | ((acc + char.toInt) * 17) % 256 6 | 7 | val ans1 = input.map(hash).sum 8 | 9 | type Box = Vector[(String, Int)] 10 | def insertLens(box: Box, label: String, focalLength: Int): Box = 11 | val slotNumber = box.indexWhere(_._1 == label) 12 | if slotNumber == -1 then box :+ (label, focalLength) 13 | else box.updated(slotNumber, (label, focalLength)) 14 | 15 | def step(boxes: Map[Int, Box], string: String) = string match 16 | case s"$label-" => 17 | val i = hash(label) 18 | boxes + (i -> boxes(i).filterNot(_._1 == label)) 19 | case s"$label=$focalLength" => 20 | val i = hash(label) 21 | boxes + (i -> insertLens(boxes(i), label, focalLength.toInt)) 22 | 23 | val startBoxes = Map.empty[Int, Box].withDefaultValue(Vector.empty) 24 | val finalBoxes = input.foldLeft(startBoxes)(step) 25 | 26 | val focusingPowers = finalBoxes.flatMap: 27 | case (boxNumber, box) => 28 | box.zipWithIndex.map: 29 | case ((label, focalLength), slotNumber) => 30 | (boxNumber + 1) * (slotNumber + 1) * focalLength 31 | 32 | val ans2 = focusingPowers.sum 33 | -------------------------------------------------------------------------------- /src/main/scala/2015/Day06.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2015/day-06.txt").getLines().toList 2 | 3 | import aoc.{Point, Area} 4 | 5 | val grid = Area(0 to 999, 0 to 999) 6 | 7 | enum Instruction: 8 | case On(a: Area) 9 | case Off(a: Area) 10 | case Toggle(a: Area) 11 | 12 | import Instruction.{On, Off, Toggle} 13 | 14 | val instructions = input.map: 15 | case s"turn on $x1,$y1 through $x2,$y2" => On: 16 | Area.bounding(Point(x1.toInt, y1.toInt), Point(x2.toInt, y2.toInt)) 17 | case s"turn off $x1,$y1 through $x2,$y2" => Off: 18 | Area.bounding(Point(x1.toInt, y1.toInt), Point(x2.toInt, y2.toInt)) 19 | case s"toggle $x1,$y1 through $x2,$y2" => Toggle: 20 | Area.bounding(Point(x1.toInt, y1.toInt), Point(x2.toInt, y2.toInt)) 21 | 22 | val ans1 = grid.pointsIterator.map: p => 23 | instructions.foldLeft(false): 24 | case (b, On(a)) if a.contains(p) => true 25 | case (b, Off(a)) if a.contains(p) => false 26 | case (b, Toggle(a)) if a.contains(p) => !b 27 | case (b, _) => b 28 | .count(identity) 29 | 30 | val ans2 = grid.pointsIterator.map: p => 31 | instructions.foldLeft(0): 32 | case (b, On(a)) if a.contains(p) => b + 1 33 | case (b, Off(a)) if a.contains(p) && b > 0 => b - 1 34 | case (b, Toggle(a)) if a.contains(p) => b + 2 35 | case (b, _) => b 36 | .sum 37 | -------------------------------------------------------------------------------- /src/main/scala/2021/Day09.worksheet.sc: -------------------------------------------------------------------------------- 1 | import io.Source 2 | import util.Using 3 | 4 | case class Point(x: Int, y: Int): 5 | def u = copy(y = y - 1) 6 | def d = copy(y = y + 1) 7 | def l = copy(x = x - 1) 8 | def r = copy(x = x + 1) 9 | 10 | extension (grid: Vector[Vector[Int]]) def apply(p: Point) = grid(p.y)(p.x) 11 | 12 | val height = Using(Source.fromResource("2021/day-09-1.txt")) { 13 | _.getLines.map(_.map(_.asDigit).toVector).toVector 14 | }.get 15 | 16 | val xRange = height(0).indices 17 | val yRange = height.indices 18 | 19 | def adjacent(p: Point) = Set(p.u, p.d, p.l, p.r).filter { case Point(x, y) => 20 | xRange.contains(x) && yRange.contains(y) 21 | } 22 | 23 | val lowPoints = for 24 | y <- yRange 25 | x <- xRange 26 | p = Point(x, y) 27 | if adjacent(p).forall(height(_) > height(p)) 28 | yield p 29 | 30 | def basin(low: Point): Set[Point] = 31 | lazy val points: LazyList[Set[Point]] = 32 | Set.empty #:: Set(low) #:: 33 | points.zip(points.tail).map { (previous, toVisit) => 34 | toVisit.flatMap(adjacent).filter(height(_) < 9).diff(previous) 35 | } 36 | points.tail.takeWhile(_.nonEmpty).reduce(_ union _) 37 | 38 | val ans1 = lowPoints.map(height(_) + 1).sum 39 | 40 | val basinSizes = lowPoints.map(basin(_).size) 41 | val ans2 = basinSizes.sorted.reverse.take(3).product 42 | -------------------------------------------------------------------------------- /src/main/scala/2024/Day22.worksheet.sc: -------------------------------------------------------------------------------- 1 | import util.chaining.* 2 | 3 | val secrets = io.Source.fromResource("2024/day-22.txt").getLines 4 | .toList.map(_.toLong) 5 | 6 | extension(secret: Long) 7 | inline def mix(f: Long => Long): Long = f(secret) ^ secret 8 | inline def prune: Long = secret % 16777216 9 | 10 | def next(secret: Long): Long = 11 | secret.mix(_ * 64).prune 12 | .pipe(_.mix(_ / 32).prune) 13 | .pipe(_.mix(_ * 2048).prune) 14 | 15 | def last(secret: Long) = Iterator.iterate(secret)(next).drop(2000).next 16 | 17 | val ans1 = secrets.map(last).sum 18 | 19 | def prices(secret: Long) = Iterator.iterate(secret)(next) 20 | .map[Int](p => (p % 10).toInt) 21 | 22 | def deltas(secret: Long) = prices(secret).sliding(2).map: 23 | case Seq(a, b) => b - a 24 | 25 | def index(secret: Long): Map[(Int, Int, Int, Int), Int] = 26 | val deltaTups = deltas(secret).take(2000).sliding(4).map: 27 | case Seq(a,b,c,d) => (a,b,c,d) 28 | 29 | deltaTups.zip(prices(secret).drop(4)).toList 30 | .groupMapReduce(_._1)(_._2)((a, b) => a) 31 | 32 | val fullIndex = 33 | val start = Map.empty[(Int, Int, Int, Int), Int].withDefaultValue(0) 34 | secrets.map(index).foldLeft(start): (acc, i) => 35 | i.foldLeft(acc): 36 | case (acc, (k, v)) => acc.updated(k, v + acc(k)) 37 | 38 | val ans2 = fullIndex.values.max 39 | -------------------------------------------------------------------------------- /src/main/scala/2020/Day15.worksheet.sc: -------------------------------------------------------------------------------- 1 | 2 | val input = io.Source.fromResource("2020/day-15.txt").getLines.toList 3 | 4 | case class State(last: Int, turns: Int, history: Map[Int, Int]): 5 | def next: State = 6 | val speaking = history.get(last).fold(0)(turns - _) 7 | copy(last = speaking, turns + 1, history = history.updated(last, turns)) 8 | 9 | 10 | var exampleState = State(6, 3, Map(0 -> 1, 3 -> 2)) 11 | 12 | exampleState = exampleState.next 13 | print(exampleState) 14 | exampleState = exampleState.next 15 | print(exampleState) 16 | exampleState = exampleState.next 17 | print(exampleState) 18 | exampleState = exampleState.next 19 | print(exampleState) 20 | exampleState = exampleState.next 21 | print(exampleState) 22 | exampleState = exampleState.next 23 | print(exampleState) 24 | exampleState = exampleState.next 25 | print(exampleState) 26 | 27 | 28 | // val start = State(6, 3, Map(0 -> 1, 3 -> 2)) 29 | 30 | val start = State( 31 | 0, 6, 32 | Map( 33 | 12 -> 1, 34 | 1 -> 2, 35 | 16 -> 3, 36 | 3 -> 4, 37 | 11 -> 5 38 | ) 39 | ) 40 | 41 | val ans1 = Iterator.iterate(start)(_.next).collectFirst { 42 | case State(s, 2020, _) => s 43 | }.get 44 | 45 | // val ans2 = Iterator.iterate(start)(_.next).collectFirst { 46 | // case State(s, 30000000, _) => s 47 | // }.get 48 | 49 | // TODO 20 seconds can optimize 50 | -------------------------------------------------------------------------------- /src/main/scala/2020/Day08.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2020/day-08.txt").getLines.toIndexedSeq 2 | 3 | case class State(pointer: Int, acc: Int, visited: List[Int], instructions: IndexedSeq[String]): 4 | def next: State = instructions(pointer) match 5 | case s"nop $arg" => copy(pointer + 1, acc, pointer :: visited) 6 | case s"acc $n" => copy(pointer + 1, acc + n.toInt, pointer :: visited) 7 | case s"jmp $o" => copy(pointer + o.toInt, acc, pointer :: visited) 8 | 9 | def execute(instructions: IndexedSeq[String]): Iterator[State] = 10 | val start = State(pointer = 0, acc = 0, visited = Nil, instructions) 11 | Iterator.iterate(start)(_.next) 12 | 13 | val ans = execute(input).takeWhile(s => ! s.visited.contains(s.pointer)).toSeq.last.acc 14 | 15 | val ans2 = 16 | val instructionSets = input.zipWithIndex.collect { 17 | case (s"nop $arg", i) => input.updated(i, s"jmp $arg") 18 | case (s"jmp $arg", i) => input.updated(i, s"nop $arg") 19 | } 20 | 21 | val corrected = instructionSets.flatMap { instructions => 22 | execute(instructions) 23 | .takeWhile(s => ! s.visited.contains(s.pointer)) 24 | .find(s => s.pointer == instructions.size) 25 | } 26 | 27 | corrected.head.acc 28 | 29 | println(ans2) 30 | 31 | // instructions.count(_.startsWith("nop")) 32 | // instructions.count(_.startsWith("jmp")) 33 | -------------------------------------------------------------------------------- /src/main/scala/2022/Day20.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2022/day-20.txt").getLines().toVector 2 | val numbers = input.map(_.toLong) 3 | 4 | type Perm = Array[Int] 5 | val id: Perm = numbers.indices.toArray 6 | 7 | def shuffle(numbers: Vector[Long])(p: Perm, i: Int): Perm = 8 | val s = p(i) 9 | val n = math.floorMod(numbers(i) + s, numbers.size - 1).toInt - s 10 | if n == 0 then p 11 | else 12 | val e = s + n 13 | val r = (s to e by n.sign) 14 | p.map { 15 | case `s` => e 16 | case j if r.contains(j) => j - n.sign 17 | case j => j 18 | } 19 | 20 | def permute[T](numbers: Vector[T], p: Perm): Vector[T] = 21 | numbers.zipWithIndex 22 | .sortBy((n, i) => p(i)) 23 | .map((n, i) => n) 24 | .toVector 25 | 26 | def groveCoordinateSum(mixed: Vector[Long]) = 27 | val circ = Iterator.continually(mixed).flatten.dropWhile(_ != 0).drop(1) 28 | val x = circ.drop(999).next() 29 | val y = circ.drop(999).next() 30 | val z = circ.drop(999).next() 31 | x + y + z 32 | 33 | val perm1 = id.foldLeft(id)(shuffle(numbers)) 34 | val ans1 = groveCoordinateSum(permute(numbers, perm1)) 35 | 36 | val key = 811589153L 37 | val decrypted = numbers.map(_.toLong * key) 38 | 39 | val perm2 = LazyList.iterate(id)(id.foldLeft(_)(shuffle(decrypted)))(10) 40 | val ans2 = groveCoordinateSum(permute(decrypted, perm2)) 41 | -------------------------------------------------------------------------------- /src/main/scala/2022/Day12.worksheet.sc: -------------------------------------------------------------------------------- 1 | val grid = io.Source.fromResource("2022/day-12.txt").getLines().toVector 2 | 3 | val xRange = grid(0).indices 4 | val yRange = grid.indices 5 | 6 | case class Point(x: Int, y: Int): 7 | def u = copy(y = y + 1) 8 | def d = copy(y = y - 1) 9 | def l = copy(x = x + 1) 10 | def r = copy(x = x - 1) 11 | 12 | def inBounds = xRange.contains(x) && yRange.contains(y) 13 | def adj = Set(u, d, l, r).filter(_.inBounds) 14 | 15 | val points = for 16 | y <- yRange 17 | x <- xRange 18 | yield Point(x, y) 19 | 20 | def elevation(p: Point) = grid(p.y)(p.x) match 21 | case 'S' => 'a' 22 | case 'E' => 'z' 23 | case c => c 24 | 25 | def floodFill(source: Set[Point]): LazyList[Set[Point]] = 26 | LazyList.unfold(Set.empty[Point] -> source) { case (visitedLast, visiting) => 27 | Option.when(visiting.nonEmpty) { 28 | val next = visiting 29 | .flatMap(p => p.adj.filter(elevation(_) <= elevation(p) + 1)) 30 | .filterNot(visitedLast) 31 | visiting -> (visiting, next) 32 | } 33 | } 34 | 35 | val start = points.find(p => grid(p.y)(p.x) == 'S').get 36 | val end = points.find(p => grid(p.y)(p.x) == 'E').get 37 | val lowPoints = points.filter(elevation(_) == 'a') 38 | 39 | val ans1 = floodFill(Set(start)).indexWhere(_.contains(end)) 40 | val ans2 = floodFill(Set(lowPoints*)).indexWhere(_.contains(end)) 41 | -------------------------------------------------------------------------------- /src/main/scala/2020/Day19.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2020/day-19.txt").getLines.toList 2 | 3 | enum Rule: 4 | case MatchString(s: String) 5 | case AndThen(a: Rule, b: Rule) 6 | case Either(a: Rule, b: Rule) 7 | case Ref(id: Int) 8 | 9 | import Rule.* 10 | 11 | object Rule: 12 | def parse(raw: String): Rule = raw match 13 | case s""""$s"""" => MatchString(s) 14 | case s"$a | $b" => Either(parse(a), parse(b)) 15 | case s"$a $b" => AndThen(parse(a), parse(b)) 16 | case ref => Ref(ref.toInt) 17 | 18 | val rules: Map[Int, Rule] = input.collect { 19 | case "8: 42" => 8 -> Rule.parse("42 | 42 8") 20 | case "11: 42 31" => 11 -> Rule.parse("42 31 | 42 11 31") 21 | case s"$id: $rawRule" => id.toInt -> Rule.parse(rawRule) 22 | }.toMap 23 | 24 | extension (rule: Rule) 25 | def matchStart(message: String): Vector[String] = rule match 26 | case MatchString(s) => Option.when(message.startsWith(s))(message.stripPrefix(s)).toVector 27 | case AndThen(a, b) => a.matchStart(message).flatMap(b.matchStart) 28 | case Either(a, b) => a.matchStart(message) ++ b.matchStart(message) 29 | case Ref(id) => rules(id).matchStart(message) 30 | 31 | def valid(message: String): Boolean = 32 | rule.matchStart(message).contains("") 33 | 34 | val messages = input.dropWhile(_.contains(":")).tail 35 | 36 | val ans = messages.count(rules(0).valid) 37 | -------------------------------------------------------------------------------- /src/main/scala/2022/Day14.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2022/day-14.txt").getLines().toList 2 | 3 | case class Point(x: Int, y: Int) 4 | 5 | val paths = input.map { 6 | _.split(" -> ").map { case s"$x,$y" => Point(x.toInt, y.toInt) }.toList 7 | } 8 | 9 | val rocks = paths.flatMap { 10 | _.sliding(2).flatMap { case List(p1, p2) => 11 | val dx = p2.x - p1.x 12 | val dy = p2.y - p1.y 13 | 14 | if dx == 0 then (p1.y to p2.y by dy.sign).map(Point(p1.x, _)) 15 | else (p1.x to p2.x by dx.sign).map(Point(_, p1.y)) 16 | } 17 | }.toSet 18 | 19 | val source = Point(500, 0) 20 | val lowestRock = rocks.map(_.y).max 21 | val floor = lowestRock + 2 22 | 23 | case class SearchState(path: List[Point], sand: Set[Point]): 24 | def air(p: Point) = !sand(p) && !rocks(p) 25 | 26 | def next: Option[SearchState] = path.headOption.map { case pos@Point(x, y) => 27 | val d = Option(Point(x, y + 1)).filter(air) 28 | val dl = Option(Point(x - 1, y + 1)).filter(air) 29 | val dr = Option(Point(x + 1, y + 1)).filter(air) 30 | 31 | val fallPos = (d orElse dl orElse dr).filter(_.y < floor) 32 | fallPos.fold(copy(path.tail, sand + pos))(p => copy(p :: path)) 33 | } 34 | 35 | def states = LazyList.unfold(SearchState(List(source), Set.empty)) { 36 | _.next.map(s => s -> s) 37 | } 38 | 39 | val ans1 = states.takeWhile(_.path.head.y < lowestRock).last.sand.size 40 | 41 | val ans2 = states.last.sand.size 42 | -------------------------------------------------------------------------------- /src/main/scala/2023/Day03.worksheet.sc: -------------------------------------------------------------------------------- 1 | import aoc.* 2 | 3 | val grid = io.Source.fromResource("2023/day-03.txt").getLines().toVector 4 | 5 | val area = Area(grid) 6 | 7 | val symbols = Map.from[Point, Char]: 8 | area.pointsIterator.map(p => p -> grid(p)).filterNot: 9 | case (p, c) => c.isDigit || c == '.' 10 | 11 | def numberRanges(row: String): List[(Int, Range)] = 12 | val nums = Iterator.unfold(row, 0): (rest, index) => 13 | Option.when(rest.nonEmpty && rest.exists(_.isDigit)): 14 | val (pre, numStart) = rest.span(!_.isDigit) 15 | val (digits, post) = numStart.span(_.isDigit) 16 | val num = digits.toInt 17 | val numMinIndex = index + pre.length 18 | val numMaxIndex = numMinIndex + digits.length 19 | val range = numMinIndex until numMaxIndex 20 | (num, range) -> (post, numMaxIndex) 21 | nums.toList 22 | 23 | val numberAreas = for 24 | (row, y) <- grid.zipWithIndex 25 | (num, range) <- numberRanges(row) 26 | yield 27 | num -> Area(range, y to y).expand(1) 28 | 29 | val partNumbers = numberAreas.collect: 30 | case (num, area) if symbols.keysIterator.exists(area.contains) => num 31 | 32 | val ans1 = partNumbers.sum 33 | 34 | val gears = symbols.filter(_._2 == '*').keysIterator.toList 35 | 36 | val ratios = gears.map: point => 37 | numberAreas.collect: 38 | case (num, area) if area.contains(point) => num 39 | 40 | val ans2 = ratios.filter(_.size == 2).map(_.product).sum 41 | -------------------------------------------------------------------------------- /src/main/scala/2024/Day08.worksheet.sc: -------------------------------------------------------------------------------- 1 | val grid = io.Source.fromResource("2024/day-08.txt").getLines.toVector 2 | 3 | import aoc.{Area, Point} 4 | 5 | val bounds = Area(grid) 6 | 7 | val antennaGroups: Map[Char, List[Point]] = 8 | bounds.pointsIterator 9 | .collect: 10 | case p if grid(p) != '.' => grid(p) -> p 11 | .toList 12 | .groupMap(_._1)(_._2) 13 | 14 | def antinodes(ps: List[Point]): Set[Point] = 15 | ps.combinations(2) 16 | .flatMap: 17 | case List(p1, p2) => 18 | val d = p2 - p1 19 | List(p2 + d, p1 - d) 20 | .toSet 21 | 22 | val ans1 = antennaGroups.values 23 | .map(antinodes).reduce(_ union _) 24 | .filter(bounds.contains) 25 | .size 26 | 27 | def gcf(a: Int, b: Int): Int = // TODO add to math library 28 | if b == 0 then a else gcf(b, a % b) 29 | 30 | def antinodes2(ps: List[Point]): Set[Point] = 31 | ps.combinations(2) 32 | .flatMap: 33 | case List(p1, p2) => 34 | val d = p2 - p1 35 | val e = 36 | val f = gcf(d.x, d.y) 37 | Point(d.x / f, d.y / f) 38 | 39 | val as1 = Iterator 40 | .iterate(p1)(_ + e) 41 | .takeWhile(bounds.contains) 42 | 43 | val as2 = Iterator 44 | .iterate(p1)(_ - e) 45 | .takeWhile(bounds.contains) 46 | 47 | (as1 ++ as2) 48 | .toSet 49 | 50 | val ans2 = antennaGroups 51 | .values.map(antinodes2) 52 | .reduce(_ union _) 53 | .size 54 | -------------------------------------------------------------------------------- /src/main/scala/2024/Day20.worksheet.sc: -------------------------------------------------------------------------------- 1 | import aoc.{Area, Point, Dir} 2 | 3 | val grid = io.Source.fromResource("2024/day-20.txt").getLines.toVector 4 | 5 | val area = Area(grid) 6 | val startPos = area.pointsIterator.find(grid(_) == 'S').get 7 | val endPos = area.pointsIterator.find(grid(_) == 'E').get 8 | val walls = area.pointsIterator.filter(grid(_) == '#').toSet 9 | 10 | val path: List[Point] = 11 | def search(pos: Point, visited: Set[Point]): List[Point] = 12 | pos.adjacent.diff(visited).diff(walls).headOption match 13 | case Some(next) => pos :: search(next, visited + pos) 14 | case None => List(pos) 15 | search(startPos, Set.empty) 16 | 17 | val normalTime = path.zipWithIndex.toMap 18 | 19 | def cheat(p: Point): Option[Int] = 20 | val pathPoints = p.adjacent.filter(area.contains).filterNot(walls) 21 | // Note: assuming no diagonal walls 22 | Option.when(pathPoints.size == 2): 23 | val List(a, b) = pathPoints.toList.sortBy(normalTime) 24 | normalTime(endPos) - normalTime(b) + normalTime(a) + 2 25 | 26 | val innerWallPoints = walls.filter(area.expand(-1).contains).toList 27 | 28 | val ans1 = innerWallPoints 29 | .flatMap(cheat) 30 | .count(_ <= normalTime(endPos) - 100) 31 | 32 | def timeSaved(from: Point, to: Point): Int = 33 | - (normalTime(from) - normalTime(to) + from.dist(to)) 34 | 35 | def cheatable(from: Point): List[Point] = 36 | path.filter(to => from.dist(to) <= 20 && timeSaved(from, to) >= 100) 37 | 38 | val ans2 = path.map(cheatable(_).size).sum 39 | -------------------------------------------------------------------------------- /src/main/scala/2021/Day11.worksheet.sc: -------------------------------------------------------------------------------- 1 | import io.Source 2 | import util.chaining.* 3 | import util.Using 4 | 5 | import collection.mutable.ArraySeq 6 | 7 | case class Point(x: Int, y: Int) 8 | 9 | def input(): Grid = Using(Source.fromResource("2021/day-11-1.txt")) { source => 10 | Grid(source.getLines.map(_.map(_.asDigit).to(ArraySeq)).to(ArraySeq)) 11 | }.get 12 | 13 | class Grid(rows: ArraySeq[ArraySeq[Int]]): 14 | def apply(p: Point): Int = rows(p.y)(p.x) 15 | def update(p: Point, n: Int): Unit = rows(p.y)(p.x) = n 16 | 17 | def xRange = 0 until rows(0).length 18 | def yRange = 0 until rows.length 19 | 20 | def inBounds(p: Point): Boolean = xRange.contains(p.x) && yRange.contains(p.y) 21 | 22 | def adjacent(p: Point): Iterable[Point] = for 23 | dx <- -1 to 1 24 | dy <- -1 to 1 25 | q = p.copy(x = p.x + dx, y = p.y + dy) 26 | if inBounds(q) 27 | yield q 28 | 29 | def allPoints: Seq[Point] = for 30 | x <- xRange 31 | y <- yRange 32 | yield Point(x, y) 33 | 34 | def increment(p: Point): Unit = if this(p) < 10 then 35 | this(p) += 1 36 | if this(p) == 10 then adjacent(p).foreach(increment) 37 | 38 | def step() = 39 | allPoints.foreach(increment) 40 | allPoints.filter(this(_) == 10).foreach(this(_) = 0) 41 | 42 | def flashCounts(grid: Grid = input()) = Iterator.continually { 43 | grid.step() 44 | grid.allPoints.count(grid(_) == 0) 45 | } 46 | 47 | val ans1 = flashCounts().take(100).sum 48 | 49 | val count = input().allPoints.size 50 | val ans2 = flashCounts().indexWhere(_ == count) + 1 51 | -------------------------------------------------------------------------------- /src/main/scala/2020/Day07.worksheet.sc: -------------------------------------------------------------------------------- 1 | import util.Using 2 | 3 | val input = io.Source.fromResource("2020/day-07.txt").getLines.toList 4 | 5 | val adj = input.map { 6 | case s"$outerColor bags contain no other bags" => outerColor -> Nil 7 | case s"$outerColor bags contain $innerBags" => 8 | val innerColors = innerBags.split(", ").collect { 9 | case s"$n $color bag$plural" => color 10 | } 11 | outerColor -> innerColors.toList 12 | }.toMap[String, List[String]] 13 | 14 | adj foreach println 15 | 16 | val rev = adj.toList 17 | .flatMap {case (outer, inners) => inners.map(_ -> outer)} 18 | .groupMap(_._1)(_._2) 19 | 20 | rev foreach println 21 | 22 | // rev("shiny gold") 23 | 24 | def canContain(inner: String): Set[String] = 25 | rev.get(inner).map { 26 | direct => 27 | direct.toSet union direct.toSet.flatMap(canContain) 28 | }.getOrElse(Set.empty) 29 | 30 | val ans1 = canContain("shiny gold").size 31 | 32 | val adj2: Map[String, List[(Long, String)]] = input.map { 33 | case s"$outerColor bags contain no other bags." => outerColor -> Nil 34 | case s"$outerColor bags contain $innerBags." => 35 | val innerColors = innerBags.split(", ").collect { 36 | case s"1 $color bag" => 1L -> color 37 | case s"$n $color bags" => n.toLong -> color 38 | } 39 | outerColor -> innerColors.toList 40 | }.toMap 41 | 42 | def containsCount(outer: String): Long = 43 | println(s"checking $outer") 44 | adj2(outer).map { 45 | case (n, inner) => n + n * containsCount(inner) 46 | }.sum 47 | 48 | val ans2 = containsCount("shiny gold") 49 | -------------------------------------------------------------------------------- /src/main/scala/2022/Day08.worksheet.sc: -------------------------------------------------------------------------------- 1 | val trees: Vector[Vector[Int]] = io.Source.fromResource("2022/day-08.txt") 2 | .getLines() 3 | .toVector 4 | .map(_.map(_.asDigit).toVector) 5 | 6 | case class Point(x: Int, y: Int): 7 | def up = copy(y = y - 1) 8 | def down = copy(y = y + 1) 9 | def left = copy(x = x - 1) 10 | def right = copy(x = x + 1) 11 | 12 | val xRange = trees.head.indices 13 | val yRange = trees.indices 14 | 15 | def inBounds(p: Point) = 16 | xRange.contains(p.x) && yRange.contains(p.y) 17 | 18 | def height(p: Point) = trees(p.y)(p.x) 19 | 20 | def lineOfSight(p: Point, d: Point => Point): Iterator[Point] = 21 | Iterator.iterate(p)(d).takeWhile(inBounds).drop(1) 22 | 23 | def visible(p: Point): Boolean = 24 | val h = height(p) 25 | lineOfSight(p, _.up).forall(height(_) < h) || 26 | lineOfSight(p, _.down).forall(height(_) < h) || 27 | lineOfSight(p, _.right).forall(height(_) < h) || 28 | lineOfSight(p, _.left).forall(height(_) < h) 29 | 30 | def scenicScore(p: Point): Int = 31 | val h = height(p) 32 | def treesVisible(it: Iterator[Point]): Int = 33 | val (low, blocking) = it.span(height(_) < h) 34 | low.size + (if blocking.hasNext then 1 else 0) 35 | 36 | val north = treesVisible(lineOfSight(p, _.up)) 37 | val south = treesVisible(lineOfSight(p, _.down)) 38 | val east = treesVisible(lineOfSight(p, _.right)) 39 | val west = treesVisible(lineOfSight(p, _.left)) 40 | north * south * east * west 41 | 42 | val points = for 43 | x <- xRange 44 | y <- yRange 45 | yield Point(x, y) 46 | 47 | val ans1 = points.count(visible) 48 | val ans2 = points.map(scenicScore).max 49 | -------------------------------------------------------------------------------- /src/main/scala/2021/Day15.worksheet.sc: -------------------------------------------------------------------------------- 1 | import collection.mutable.{PriorityQueue, Map} 2 | 3 | val input = io.Source.fromResource("2021/day-15-1.txt").getLines.toVector 4 | 5 | type Grid = Vector[Vector[Int]] 6 | 7 | val riskLevels: Grid = input.map(_.map(_.asDigit).toVector).toVector 8 | 9 | val fullRiskLevels: Grid = 10 | def inc(by: Int)(risk: Int): Int = (risk + by - 1) % 9 + 1 11 | (0 until 5).foldLeft(Vector.empty) { (gridAcc, dy) => 12 | gridAcc ++ riskLevels.map { row => 13 | (0 until 5).foldLeft(Vector.empty) { (rowAcc, dx) => 14 | rowAcc ++ row.map(inc(by = dy + dx)) 15 | } 16 | } 17 | } 18 | 19 | case class Point(x: Int, y: Int): 20 | def r = copy(x = x + 1) 21 | def d = copy(y = y + 1) 22 | def l = copy(x = x - 1) 23 | def u = copy(y = y - 1) 24 | 25 | extension (rows: Grid) 26 | def riskAt(p: Point): Int = rows(p.y)(p.x) 27 | 28 | def adjacent(p: Point) = Set(p.r, p.d, p.l, p.u).filter { q => 29 | rows.indices.contains(q.x) && rows(0).indices.contains(q.y) 30 | } 31 | 32 | def lowestTotalRiskPath: Int = 33 | val end = Point(rows(0).indices.last, rows.indices.last) 34 | var visiting = Point(0, 0) 35 | val totalRisk = Map(visiting -> 0) 36 | val toVisit = PriorityQueue.empty[Point](Ordering.by(totalRisk)).reverse 37 | 38 | while visiting != end do 39 | for q <- adjacent(visiting) if !totalRisk.contains(q) do 40 | totalRisk(q) = totalRisk(visiting) + riskAt(q) 41 | toVisit.enqueue(q) 42 | visiting = toVisit.dequeue() 43 | 44 | totalRisk(end) 45 | 46 | val ans1 = riskLevels.lowestTotalRiskPath 47 | val ans2 = fullRiskLevels.lowestTotalRiskPath 48 | -------------------------------------------------------------------------------- /src/main/scala/2023/Day05Part1.worksheet.sc: -------------------------------------------------------------------------------- 1 | import scala.compiletime.ops.int 2 | import scala.collection.immutable.{Range => _} 3 | 4 | type Range = scala.collection.immutable.NumericRange[Long] 5 | 6 | val input = io.Source.fromResource("2023/day-05.txt").getLines() 7 | 8 | val seeds = 9 | val s"seeds: $seedsStr" = (input.next(): @unchecked) 10 | seedsStr.split(" ").map(_.toLong).toList 11 | 12 | input.next() 13 | 14 | def extractRanges(): List[(Range, Range)] = 15 | val lines = input.takeWhile(_.nonEmpty) 16 | lines.map { 17 | case s"$a $b $c" => 18 | val dest = a.toLong 19 | val source = b.toLong 20 | val length = c.toLong 21 | (dest until dest + length, source until source + length) 22 | }.toList 23 | 24 | def next(current: List[Long], ranges: List[(Range, Range)]): List[Long] = 25 | current.map: c => 26 | ranges.find(_._2.contains(c)).fold(c): (d, s) => 27 | println(s"found $c in $s going to $d") 28 | // d.min + (c - s.min) 29 | c + (d.min - s.min) 30 | 31 | var current = seeds 32 | 33 | println(current) 34 | input.next() 35 | current = next(current, extractRanges()) 36 | println(current) 37 | input.next() 38 | current = next(current, extractRanges()) 39 | println(current) 40 | input.next() 41 | current = next(current, extractRanges()) 42 | println(current) 43 | input.next() 44 | current = next(current, extractRanges()) 45 | println(current) 46 | input.next() 47 | current = next(current, extractRanges()) 48 | println(current) 49 | input.next() 50 | current = next(current, extractRanges()) 51 | println(current) 52 | input.next() 53 | val locations = next(current, extractRanges()) 54 | 55 | val ans1 = locations.min 56 | -------------------------------------------------------------------------------- /src/main/scala/2020/Day13.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2020/day-13.txt").getLines.toList 2 | 3 | val currentTime = input(0).toInt 4 | val buses = input(1).split(",").flatMap(_.toIntOption).toList 5 | 6 | 7 | def next(time: Int, bus: Int): Int = 8 | time / bus * bus + bus 9 | 10 | val nextBus = buses.minBy(next(currentTime, _)) 11 | 12 | val ans = nextBus * (next(currentTime, nextBus) - currentTime) 13 | 14 | 15 | val buses2 = input(1).split(",").map(_.toIntOption).toList 16 | 17 | 18 | println((13 to 13 * 17 by 13).mkString(",")) 19 | println((17 to 13 * 17 by 17).mkString(",")) 20 | 21 | def xCount(bs: List[Option[Int]]): (Int, List[Option[Int]]) = 22 | println(bs) 23 | val (wildcards, remaining) = bs.tail.span(_.isEmpty) 24 | wildcards.size -> remaining 25 | 26 | List(Some(10), None, None, Some(13)).exists(_.isEmpty) 27 | xCount(List(Some(10), None, None, Some(13))) 28 | 29 | xCount(List(Some(10))) 30 | 31 | 32 | val deltas = 33 | Iterator.unfold(buses2)(bs => 34 | Option(xCount(bs)).filter(_._2.nonEmpty) 35 | ).toList 36 | 37 | buses2.flatten 38 | deltas 39 | 40 | buses2.flatten.size 41 | deltas.size 42 | 43 | def foo(b1: Int, b2: Int, gap: Int): (Int, Int) = 44 | def adj(b1s: List[Int], b2s: List[Int]): (Int, Int) = (b1s, b2s) match 45 | case (l :: _, r :: _) if l + gap == r => (l, r) 46 | case (l :: ls, r :: rs) if l + gap < r => adj(ls, b2s) 47 | case (l :: ls, r :: rs) if l + gap > r => adj(b1s, rs) 48 | case _ => ??? 49 | 50 | adj((b1 to b1 * b2 by b1).toList, (b2 to b1 * b2 by b2).toList) 51 | 52 | foo(41, 37, 34) 53 | 41 * 34 54 | 55 | foo(37, 379, 5) 56 | 57 | 37 * 379 58 | 59 | // def solve(buses: List[Int], deltas: List[Int]) 60 | -------------------------------------------------------------------------------- /src/main/scala/2025/Day08.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2025/day-08.txt").getLines().toList 2 | 3 | case class Point(x: Long, y: Long, z: Long): 4 | def dist(p: Point): Long = 5 | val dx = x - p.x 6 | val dy = y - p.y 7 | val dz = z - p.z 8 | dx*dx + dy*dy + dz*dz 9 | 10 | type Edge = (Point, Point) 11 | type Adj = Map[Point, Set[Point]] 12 | 13 | val boxes: List[Point] = input.collect: 14 | case s"$x,$y,$z" => Point(x.toLong, y.toLong, z.toLong) 15 | 16 | val edges = boxes.combinations(2).collect: 17 | case List(p, q) => p -> q 18 | .toList.sortBy(_.dist(_)) 19 | 20 | def connect(adj: Adj, edge: Edge): Adj = 21 | val (p, q) = edge 22 | adj.updated(p, adj(p) + q) 23 | .updated(q, adj(q) + p) 24 | 25 | def component(a: Point, adj: Adj): Set[Point] = 26 | def flood(visited: Set[Point], current: Set[Point]): Set[Point] = 27 | if current.isEmpty then visited else 28 | val next = current.flatMap(adj).diff(visited) 29 | flood(visited.union(current), next) 30 | flood(Set.empty, Set(a)) 31 | 32 | def components(adj: Adj) = List.from: 33 | Iterator.unfold(boxes.toSet): unsorted => 34 | unsorted.headOption.map: p => 35 | val c = component(p, adj) 36 | c -> unsorted.diff(c) 37 | 38 | val emptyAdj: Adj = Map.empty.withDefaultValue(Set.empty) 39 | val states = LazyList.from(edges).scanLeft(emptyAdj)(connect(_, _)) 40 | 41 | val ans1 = components(states(1000)).map(_.size) 42 | .sorted.takeRight(3).product 43 | 44 | def fullyConnected(adj: Adj): Boolean = 45 | adj.keys.headOption.exists(component(_, adj).sizeIs == boxes.size) 46 | 47 | val finalEdge = edges(states.indexWhere(fullyConnected)) 48 | 49 | val ans2 = 50 | val (p, q) = finalEdge 51 | p.x.toLong * q.x.toLong 52 | -------------------------------------------------------------------------------- /src/main/scala/2020/Day16.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2020/day-16.txt").getLines.toList 2 | 3 | case class Rule(field: String, r1: Range, r2: Range): 4 | def valid(value: Int): Boolean = r1.contains(value) || r2.contains(value) 5 | 6 | object Rule: 7 | def parse(raw: String): Rule = raw match 8 | case s"$field: $x1-$x2 or $y1-$y2" => 9 | Rule(field, x1.toInt to x2.toInt, y1.toInt to y2.toInt) 10 | 11 | 12 | type Ticket = Vector[Int] 13 | 14 | val (rules: List[Rule], your: Ticket, nearby: List[Ticket]) = 15 | util.Using(io.Source.fromResource("2020/day-16.txt")){ s => 16 | val lines = s.getLines() 17 | val rules = lines.takeWhile(_.nonEmpty).map(Rule.parse).toList 18 | def ticket(line: String) = line.split(",").map(_.toInt).toVector 19 | val your = ticket(lines.drop(1).next()) 20 | val nearby = lines.drop(2).toList.map(ticket) 21 | 22 | (rules, your, nearby) 23 | }.get 24 | 25 | def completelyInvalid(value: Int): Boolean = !rules.exists(_.valid(value)) 26 | 27 | val ans1 = nearby.flatten.filter(completelyInvalid).sum 28 | 29 | val validTickets = nearby.filterNot(_.exists(completelyInvalid)) 30 | 31 | val fieldValues = validTickets.transpose 32 | 33 | val possibleFieldIndices = rules.map { rule => 34 | val possibleIndices = fieldValues.zipWithIndex.collect { 35 | case (values, index) if values.forall(rule.valid) => index 36 | } 37 | rule.field -> possibleIndices 38 | } 39 | 40 | val fieldIndices = possibleFieldIndices.sortBy(_._2.length).foldLeft(Map.empty[String, Int]){ 41 | case (solved, (field, indices)) => 42 | solved + (field -> indices.diff(solved.values.toSeq).head) 43 | } 44 | 45 | val ans2 = fieldIndices.collect { 46 | case (field, i) if field.startsWith("departure") => your(i).toLong 47 | }.product 48 | -------------------------------------------------------------------------------- /src/main/scala/2022/Day11.worksheet.sc: -------------------------------------------------------------------------------- 1 | val lines = io.Source.fromResource("2022/day-11.txt").getLines().toVector 2 | 3 | val items: Vector[Vector[Long]] = lines.collect { case s"$_ items: $xs" => 4 | xs.split(", ").map(_.toLong).toVector 5 | } 6 | 7 | val divTests = lines.collect { case s"$_ divisible by $p" => p.toInt } 8 | 9 | val lcm = divTests.product 10 | 11 | val worryOp: Vector[Long => Long] = lines.collect { 12 | case s"$_ new = old * old" => o => o * o % lcm 13 | case s"$_ new = old * $x" => o => o * x.toInt % lcm 14 | case s"$_ new = old + $x" => o => o + x.toInt % lcm 15 | } 16 | 17 | val throwTo: Vector[Long => Int] = 18 | val ifTrue = lines.collect { case s"$_ true: $_ monkey $n" => n.toInt } 19 | val ifFalse = lines.collect { case s"$_ false: $_ monkey $n" => n.toInt } 20 | 21 | divTests.lazyZip(ifTrue).lazyZip(ifFalse).map { case (d, t, f) => 22 | (w: Long) => if w % d == 0 then t else f 23 | } 24 | 25 | def turnCounts(worryMore: Boolean) = 26 | Iterator.iterate((0, items, Vector.fill(items.size)(0L))) { 27 | case (m, items, counts) => 28 | val nextItems = items(m) 29 | .foldLeft(items.updated(m, Vector.empty)) { (items, w) => 30 | val newWorry = worryOp(m)(w) / (if worryMore then 1 else 3) 31 | val m2 = throwTo(m)(newWorry) 32 | items.updated(m2, items(m2).appended(newWorry)) 33 | } 34 | val nextCounts = counts.updated(m, counts(m) + items(m).size) 35 | ((m + 1) % items.size, nextItems, nextCounts) 36 | } 37 | 38 | def roundCount(rounds: Int, worryMore: Boolean): Vector[Long] = 39 | turnCounts(worryMore).drop(items.size * rounds).next()._3 40 | 41 | val ans1 = roundCount(20, false).sorted.takeRight(2).product 42 | 43 | val ans2 = roundCount(10000, true).sorted.takeRight(2).product 44 | -------------------------------------------------------------------------------- /src/main/scala/2025/Day10Solver.worksheet.sc: -------------------------------------------------------------------------------- 1 | import optimus.algebra.Constraint 2 | import optimus.optimization.* 3 | import optimus.optimization.enums.SolverLib 4 | import optimus.optimization.subjectTo 5 | import optimus.optimization.model.MPIntVar 6 | import optimus.algebra.ConstraintRelation.EQ 7 | import optimus.algebra.Const 8 | import optimus.algebra.Expression 9 | 10 | val input = io.Source.fromResource("2025/day-10.txt").getLines().toList 11 | 12 | case class Machine(buttons: List[Set[Int]], joltages: Vector[Int]): 13 | def solve(using MPModel): Long = 14 | val vars = buttons.zip('a' to 'z').map: 15 | case (b, l) => 16 | // val max = joltages.filter(b.contains).min 17 | MPIntVar(l.toString, 0 to joltages.max) -> b 18 | .toMap 19 | 20 | // vars.toList.sortBy(_._1.symbol) foreach println 21 | 22 | val constraints = joltages.zipWithIndex.map: 23 | case (j, i) => 24 | val vs = vars.collect: 25 | case (v, b) if b.contains(i) => v 26 | Constraint(vs.reduce(_ + _), EQ, Const(j)) 27 | 28 | // constraints foreach println 29 | 30 | val sum: Expression = vars.keys.reduce(_ + _) 31 | 32 | minimize(sum) 33 | subjectTo(constraints*) 34 | start() 35 | vars.keys.toList.map: v => 36 | v.value.get.round 37 | //.tapEach(value => println(s"${v.symbol} = $value")) 38 | .sum 39 | 40 | val machines = input.collect: 41 | case s"[$goal] $buttons {$joltages}" => 42 | val buttonInts = buttons.split(' ').toList.map: s => 43 | s.slice(1, s.length - 1).split(',').map(_.toInt).toSet 44 | 45 | Machine(buttonInts, joltages.split(',').map(_.toInt).toVector) 46 | 47 | machines.map(_.solve(using MPModel())).sum 48 | 49 | // upper bound 65021 50 | machines.map(_.joltages.map(_.toLong).sum).sum 51 | 52 | // 16459 too low 53 | -------------------------------------------------------------------------------- /src/main/scala/2022/Day07.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2022/day-07.txt").getLines().toList 2 | 3 | case class Dir(path: List[String]): 4 | def up = copy(path = path.tail) 5 | def cd(sub: String) = copy(path = sub :: path) 6 | def pathString = "/" + path.reverse.mkString("/") 7 | 8 | object Dir: 9 | val root = Dir(Nil) 10 | 11 | case class File(name: String, size: Long) 12 | 13 | def buildContents( 14 | lines: List[String], 15 | curDir: Dir = Dir.root, 16 | contents: Map[Dir, List[File | Dir]] = Map.empty 17 | ): Map[Dir, List[File | Dir]] = 18 | lines.headOption.fold(contents) { 19 | case "$ cd /" => buildContents(lines.tail, Dir.root, contents) 20 | case "$ cd .." => buildContents(lines.tail, curDir.up, contents) 21 | case s"$$ cd $dir" => buildContents(lines.tail, curDir.cd(dir), contents) 22 | case "$ ls" => 23 | val (stdout, nextCommands) = lines.tail.span(o => !o.startsWith("$")) 24 | val listedFiles: List[Dir | File] = stdout.map { 25 | case s"dir $sub" => curDir.cd(sub) 26 | case s"$size $name" => File(name, size.toLong) 27 | } 28 | buildContents(nextCommands, curDir, contents.updated(curDir, listedFiles)) 29 | } 30 | 31 | val contents = buildContents(input, Dir.root, Map.empty) 32 | 33 | val memo = collection.mutable.Map.empty[Dir, Long] 34 | 35 | def size(dir: Dir): Long = 36 | memo.getOrElseUpdate( 37 | dir, 38 | contents(dir).map { 39 | case File(_, size) => size 40 | case sub: Dir => size(sub) 41 | }.sum 42 | ) 43 | 44 | val ans1 = contents.keysIterator.map(size(_)).filter(_ <= 100000).sum 45 | 46 | val ans2 = 47 | val spaceAvailable = 70_000_000 - size(Dir.root) 48 | val spaceNeeded = 30000000 - spaceAvailable 49 | memo.values.toList.sorted.find(_ >= spaceNeeded).get 50 | -------------------------------------------------------------------------------- /src/main/scala/2024/Day12.worksheet.sc: -------------------------------------------------------------------------------- 1 | val farm = io.Source.fromResource("2024/day-12.txt").getLines.toVector 2 | 3 | import aoc.{Area, Point} 4 | 5 | val area = Area(farm) 6 | 7 | def floodFill(start: Point): Set[Point] = 8 | def search(visited: Set[Point], visiting: Set[Point]): Set[Point] = 9 | if visiting.isEmpty then visited else 10 | val next = visiting 11 | .flatMap(_.adjacent) 12 | .diff(visited) 13 | .filter(area.contains) 14 | .filter(farm(_) == farm(start)) 15 | search(visited.union(visiting), next) 16 | search(Set.empty, Set(start)) 17 | 18 | val areaPoints = area.pointsIterator.toSet 19 | 20 | val allRegions = LazyList.unfold(areaPoints): unfenced => 21 | Option.when(unfenced.nonEmpty): 22 | val r = floodFill(unfenced.head) 23 | r -> unfenced.diff(r) 24 | 25 | def perimeter(region: Set[Point]): Int = 26 | region.toList.map: p => 27 | p.adjacent.count(!region.contains(_)) 28 | .sum 29 | 30 | val ans1 = allRegions 31 | .map(r => r.size.toLong * perimeter(r).toLong).sum 32 | 33 | def cornerCount(region: Set[Point]): Int = 34 | val bound = Area.bounding(region).expand(1) 35 | val subAreas = bound.pointsIterator.toList.map: p => 36 | Area.bounding(p, p.d.r) 37 | 38 | val normalCorners = subAreas.count: a => 39 | val c = a.pointsIterator.count(region) 40 | c == 1 || c == 3 41 | 42 | val diagonalCorners = subAreas.count: a => 43 | val c = a.pointsIterator.count(region) 44 | val inBounds = a.pointsIterator.forall(area.contains) 45 | c == 2 && inBounds && (farm(a.topLeft) == farm(a.botRight) || farm(a.botLeft) == farm(a.topRight)) 46 | 47 | normalCorners + diagonalCorners*2 48 | 49 | def price(region: Set[Point]): Long = 50 | region.size.toLong * cornerCount(region).toLong 51 | 52 | val ans2 = allRegions.map(price).sum 53 | -------------------------------------------------------------------------------- /src/main/scala/2015/Day15.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2015/day-15.txt").getLines().toList 2 | 3 | val ingredients: Vector[Vector[Long]] = input.toVector.collect: 4 | case s"$name: capacity $c, durability $d, flavor $f, texture $t, calories $cal" => 5 | Vector(c.toLong, d.toLong, f.toLong, t.toLong, cal.toLong) 6 | 7 | def score(amounts: Long*): Long = 8 | assert(amounts.size == 4) 9 | assert(amounts.sum == 100) 10 | val scaled = amounts.zip(ingredients).map: 11 | case (n, props) => props.init.map(_ * n) 12 | val props = scaled.transpose.map(_.sum).map: 13 | case n if n < 0 => 0 14 | case n => n 15 | props.product 16 | 17 | val proportions = for 18 | a <- 0L to 100L 19 | b <- 0L to (100L - a) 20 | c <- 0L to (100L - a - b) 21 | d = 100L - a - b - c 22 | yield List(a,b,c,d) 23 | 24 | def calories(amounts: Long*): Long = 25 | val cals = ingredients.transpose.last 26 | amounts.zip(cals).map(_ * _).sum 27 | 28 | val props = proportions.maxBy(p => score(p*)) 29 | 30 | val ans1 = score(props*) 31 | val ans2 = proportions.collect: 32 | case ps if calories(ps*) == 500 => score(ps*) 33 | .max 34 | // 13324953600 too high 35 | 36 | 37 | // score(24, 24, 26, 26) 38 | // score(60, 10, 15, 15) 39 | // score(1,1,1,97) 40 | // score(1,1,49,49) 41 | // score(40, 40, 10, 10) 42 | 43 | 44 | // val ingredients = input.collect: 45 | // case s"$name: capacity $c, durability $d, flavor $f, texture $t, calories $cal" => 46 | // Ingredient( 47 | // capacity = c.toInt, 48 | // durability = d.toInt, 49 | // flavor = f.toInt, 50 | // texture = t.toInt, 51 | // calories = cal.toInt 52 | // ) 53 | 54 | // List(sprinkles, butterscotch, chocolate, candy) = ingredients 55 | 56 | // def score( 57 | // sp: Int, 58 | // bu: Int, 59 | // ch: Int, 60 | // ca: Int 61 | // ): Int = 62 | 63 | 64 | 65 | // 66 | -------------------------------------------------------------------------------- /src/main/scala/2020/Day17.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2020/day-17.txt").getLines.toList 2 | 3 | // val input = List( 4 | // ".#.", 5 | // "..#", 6 | // "###" 7 | // ) 8 | 9 | case class Point(x: Int, y: Int, z: Int, w: Int): 10 | def adjacent = for 11 | xd <- (x - 1 to x + 1).iterator 12 | yd <- y - 1 to y + 1 13 | zd <- z - 1 to z + 1 14 | wd <- w - 1 to w + 1 15 | if !(xd == x && yd == y && zd == z && wd == w) 16 | yield Point(xd, yd, zd, wd) 17 | 18 | extension (r: Range) def expand: Range = (r.min - 1) to (r.max + 1) 19 | 20 | case class Area(xRange: Range, yRange: Range, zRange: Range, wRange: Range): 21 | def expand3d: Area = Area(xRange.expand, yRange.expand, zRange.expand, wRange) 22 | 23 | def expand: Area = 24 | Area(xRange.expand, yRange.expand, zRange.expand, wRange.expand) 25 | 26 | def points = for 27 | x <- xRange 28 | y <- yRange 29 | z <- zRange 30 | w <- wRange 31 | yield Point(x, y, z, w) 32 | 33 | case class Grid(area: Area, active: Set[Point]): 34 | def activeNext(p: Point): Boolean = 35 | val count = p.adjacent.filter(active).take(4).toList 36 | count.sizeIs == 3 || active(p) && count.sizeIs == 2 37 | 38 | def next3d: Grid = 39 | val expanded = area.expand3d 40 | Grid(expanded, expanded.points.filter(activeNext).toSet) 41 | 42 | def next: Grid = 43 | val expanded = area.expand 44 | Grid(expanded, expanded.points.filter(activeNext).toSet) 45 | 46 | val start = 47 | val xRange = 0 until input(0).length 48 | val yRange = 0 until input.length 49 | val area = Area(xRange, yRange, 0 to 0, 0 to 0) 50 | val active = area.points.filter { case Point(x, y, z, w) => 51 | input(y)(x) == '#' 52 | }.toSet 53 | Grid(area, active) 54 | 55 | val ans1 = LazyList.iterate(start)(_.next3d)(6).active.size 56 | 57 | val ans2 = LazyList.iterate(start)(_.next)(6).active.size 58 | -------------------------------------------------------------------------------- /src/main/scala/2023/Day12.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2023/day-12.txt").getLines().toVector 2 | 3 | def parse(line: String): (String, List[Int]) = line match 4 | case s"$record $groups" => 5 | val contiguousDamaged = groups.split(",").toList.map(_.toInt) 6 | (record + ".") -> contiguousDamaged 7 | 8 | def parse2(line: String): (String, List[Int]) = line match 9 | case s"$record $groups" => 10 | val contiguousDamaged = groups.split(",").toList.map(_.toInt) 11 | (List.fill(5)(record).mkString("?") + ".") -> List.fill(5)(contiguousDamaged).flatten 12 | 13 | def matchRanges(record: String, from: Int, groupSize: Int): List[Int] = 14 | val nextDamaged = record.indexOf("#", from) 15 | val operational = (from until record.length).filter(record(_) == '.').toSet 16 | 17 | val lastFrom = List(nextDamaged, record.size - groupSize - 1).filterNot(_ == -1).min 18 | val froms = (from to lastFrom).filter: i => 19 | val noneOpreational = !(i until (i + groupSize)).exists(operational) 20 | val notDamagedAfter = record(i + groupSize) != '#' 21 | noneOpreational && notDamagedAfter 22 | 23 | froms.toList 24 | 25 | def count(record: String, groups: List[Int]): Long = 26 | val memo = collection.mutable.Map.empty[(String, Int, List[Int]), Long] 27 | 28 | def search(record: String, from: Int, groups: List[Int]): Long = 29 | lazy val calculate: Long = 30 | if groups.isEmpty then 31 | if record.indexOf("#", from) == -1 then 1 else 0 32 | else 33 | val recursiveCounts = matchRanges(record, from, groups.head).map: i => 34 | search(record, i + groups.head + 1, groups.tail) 35 | recursiveCounts.sum 36 | memo.getOrElseUpdate((record, from, groups), calculate) 37 | 38 | search(record, 0, groups) 39 | 40 | val ans1 = input.map(parse).map(count.tupled).sum 41 | val ans2 = input.map(parse2).map(count.tupled).sum 42 | -------------------------------------------------------------------------------- /src/main/scala/2023/Day22.worksheet.sc: -------------------------------------------------------------------------------- 1 | import aoc.* 2 | 3 | val input = io.Source.fromResource("2023/day-22.txt").getLines.toVector 4 | 5 | case class Brick(area: Area, depth: Interval[Int]) 6 | 7 | val falling = List.from[Brick]: 8 | input.map: 9 | case s"$x1,$y1,$z1~$x2,$y2,$z2" => 10 | val xRange = Interval(x1.toInt, x2.toInt) 11 | val yRange = Interval(y1.toInt, y2.toInt) 12 | val zRange = Interval(z1.toInt, z2.toInt) 13 | val area = Area(xRange, yRange) 14 | Brick(area, zRange) 15 | 16 | val settled = falling.sortBy(_.depth.min).foldLeft(List.empty[Brick]): 17 | case (settled, brick) => 18 | val newBot = settled.filter(_.area.intersect(brick.area).nonEmpty) 19 | .map(_.depth.max + 1).maxOption.getOrElse(0) 20 | 21 | val newDepth = Interval(newBot until newBot + brick.depth.size) 22 | Brick(brick.area, newDepth) :: settled 23 | 24 | val paired: List[(Brick, Brick)] = 25 | settled.combinations(2).toList.filter: 26 | case List(a, b) => a.area.intersect(b.area).nonEmpty 27 | case _ => ??? 28 | .collect: 29 | case List(a, b) if a.depth.min - 1 == b.depth.max => a -> b 30 | case List(b, a) if a.depth.min - 1 == b.depth.max => a -> b 31 | 32 | val bricksBelow: Map[Brick, List[Brick]] = 33 | paired.groupMap(_._1)(_._2).withDefaultValue(Nil) 34 | 35 | val bricksAbove: Map[Brick, List[Brick]] = 36 | paired.map(_.swap).groupMap(_._1)(_._2).withDefaultValue(Nil) 37 | 38 | val ans1 = settled.count: b => 39 | bricksAbove(b).forall: a => 40 | bricksBelow(a).sizeIs > 1 41 | 42 | def chain(dropped: Set[Brick], dropping: Brick): Set[Brick] = 43 | val next = bricksAbove(dropping).filter: a => 44 | bricksBelow(a).forall(b => dropped(b) || b == dropping) 45 | next.foldLeft(dropped + dropping)(chain) 46 | 47 | def disintegrate(brick: Brick): Int = 48 | chain(Set.empty, brick).size - 1 49 | 50 | val ans2 = settled.map(disintegrate).sum 51 | -------------------------------------------------------------------------------- /src/main/scala/2022/Day16.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2022/day-16.txt").getLines().toList 2 | 3 | input.size 4 | 5 | val flowRate: Map[String, Int] = input.collect { 6 | case s"Valve $valve has flow rate=$rate; $_" => 7 | valve -> rate.toInt 8 | }.toMap 9 | 10 | val adj: Map[String, List[String]] = input.collect { 11 | case s"Valve $valve has $_ to valve$_ $valves" => 12 | valve -> valves.split(", ").toList 13 | }.toMap 14 | 15 | val majorValves = adj.keySet.filter(v => flowRate(v) > 0 || v == "AA") 16 | 17 | def distances(start: String): Map[String, Int] = 18 | val floodFill = Iterator.unfold(Set.empty[String], Set(start)) { 19 | (visited, visiting) => 20 | Option.when(visiting.nonEmpty) { 21 | val next = visiting.flatMap(adj).diff(visited) 22 | visiting -> (visited ++ visiting, next) 23 | } 24 | } 25 | floodFill.zipWithIndex.flatMap { (valves, distance) => 26 | valves.intersect(majorValves).map(_ -> distance) 27 | }.toMap 28 | 29 | val costs = majorValves.map(v => v -> distances(v)).toMap 30 | 31 | def maxPressure( 32 | valve: String, 33 | time: Int, 34 | pressure: Int, 35 | opened: Set[String], 36 | indent: Int = 0 37 | ): Int = 38 | val paths = costs(valve).collect { 39 | case (dest, cost) if !opened(dest) && time - cost - 1 > 0 => 40 | println(s"${"." * indent}$valve to $dest at $cost with $time left") 41 | maxPressure( 42 | valve = dest, 43 | time = time - 1 - cost, 44 | pressure = pressure + flowRate(valve) * (time - 1), 45 | opened = opened + dest, 46 | indent + 1 47 | ) 48 | } 49 | paths.maxOption.getOrElse { 50 | println(s"${"." * indent}staying at $valve with $time left (pressure: $pressure)") 51 | pressure 52 | } 53 | 54 | costs("AA").map { 55 | case (dest, cost) => 56 | maxPressure(dest, 30 - cost, 0, Set("AA", dest)) 57 | }.max 58 | -------------------------------------------------------------------------------- /src/main/scala/2015/Day07.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2015/day-07.txt").getLines().toList 2 | 3 | enum Ref: 4 | case Constant(value: Int) 5 | case Wire(label: String) 6 | 7 | import Ref.* 8 | 9 | enum Gate: 10 | case AND(a: Ref, b: Ref) 11 | case OR(a: Ref, b: Ref) 12 | case LSHIFT(a: Ref, n: Int) 13 | case RSHIFT(a: Ref, n: Int) 14 | case NOT(a: Ref) 15 | 16 | import Gate.* 17 | 18 | def arg(s: String): Ref = 19 | s.toIntOption match 20 | case Some(n) => Constant(n) 21 | case None => Wire(s) 22 | 23 | given circuit: Map[Wire, Gate | Ref] = Map.from[Wire, Gate | Ref]: 24 | input.collect: 25 | case s"$a AND $b -> $c" => Wire(c) -> AND(arg(a), arg(b)) 26 | case s"$a OR $b -> $c" => Wire(c) -> OR(arg(a), arg(b)) 27 | case s"$a LSHIFT $n -> $c" => Wire(c) -> LSHIFT(arg(a), n.toInt) 28 | case s"$a RSHIFT $n -> $c" => Wire(c) -> RSHIFT(arg(a), n.toInt) 29 | case s"NOT $a -> $c" => Wire(c) -> NOT(arg(a)) 30 | case s"$a -> $b" => Wire(b) -> arg(a) 31 | 32 | val memo = collection.mutable.Map.empty[Ref, Int] 33 | 34 | def eval(a: Ref)(using circuit: Map[Wire, Gate | Ref]): Int = 35 | lazy val calc = a match 36 | case Constant(n) => n 37 | case w: Wire => 38 | circuit(w) match 39 | case ref: Ref => eval(ref) 40 | case Gate.AND(a, b) => (eval(a) & eval(b)) 41 | case Gate.OR(a, b) => (eval(a) | eval(b)) 42 | case LSHIFT(a, n) => (eval(a) << n) 43 | case RSHIFT(a, n) => (eval(a) >>> n) 44 | case NOT(a) => (~eval(a)) 45 | memo.getOrElseUpdate(a, calc) 46 | 47 | val ans1 = eval(Wire("a")) 48 | 49 | memo.clear() 50 | 51 | val circuit2 = circuit.updated(Wire("b"), Constant(ans1)) 52 | 53 | val ans2 = eval(Wire("a"))(using circuit2) 54 | 55 | // eval("d") // 72 56 | // eval("e") // 507 57 | // eval("f") // 492 58 | // eval("g") // 114 59 | // eval("h") // 65412 60 | // eval("i") // 65079 61 | // eval("x") // 123 62 | // eval("y") // 456 63 | -------------------------------------------------------------------------------- /src/main/scala/2022/Day13.worksheet.sc: -------------------------------------------------------------------------------- 1 | import math.Ordering.Implicits.given 2 | 3 | val input = io.Source.fromResource("2022/day-13.txt").getLines().toVector 4 | 5 | enum Packet: 6 | case PList(packets: Packet*) 7 | case Num(n: Int) 8 | 9 | override def toString = this match 10 | case PList(packets*) => packets.mkString("[", ",", "]") 11 | case Num(n) => n.toString 12 | 13 | object Packet: 14 | def parse(raw: String): Packet = raw match 15 | case s"[$packets]" => PList(splitTopLevel(packets).map(parse)*) 16 | case num => Num(num.toInt) 17 | 18 | def splitTopLevel(commaSeparated: String): Seq[String] = 19 | val depths = commaSeparated.iterator.scanLeft(0) { 20 | case (depth, '[') => depth + 1 21 | case (depth, ']') => depth - 1 22 | case (depth, _) => depth 23 | } 24 | val semicolonSep = depths.zip(commaSeparated).map { 25 | case (0, ',') => ';' 26 | case (d, c) => c 27 | } 28 | semicolonSep.mkString.split(';').filter(_.nonEmpty).toSeq 29 | 30 | given Ordering[Packet] with 31 | def compare(left: Packet, right: Packet): Int = (left, right) match 32 | case (Num(l), Num(r)) => l - r 33 | case (PList(ls*), PList(rs*)) => Ordering[Seq[Packet]].compare(ls, rs) 34 | case (PList(_*), Num(r)) => compare(left, PList(right)) 35 | case (Num(l), PList(_*)) => compare(PList(left), right) 36 | 37 | import Packet.{PList, Num, parse} 38 | 39 | val packets = input 40 | .sliding(2, 3) 41 | .collect { case Vector(l, r) => (parse(l), parse(r)) } 42 | .toVector 43 | 44 | val ans1 = packets.zipWithIndex.collect { 45 | case ((left, right), i) if left <= right => i + 1 46 | }.sum 47 | 48 | val d1 = parse("[[2]]") 49 | val d2 = parse("[[6]]") 50 | val allPackets = (packets.appended(d1 -> d2)).flatMap(_.toList).sorted 51 | 52 | val i1 = allPackets.indexOf(d1) + 1 53 | val i2 = allPackets.indexOf(d2) + 1 54 | val ans2 = i1 * i2 55 | -------------------------------------------------------------------------------- /src/main/scala/2024/Day04.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2024/day-04.txt").getLines().toList 2 | 3 | def countXmas(s: String): Int = 4 | val occurences = Iterator.iterate(-1): i => 5 | s.indexOf("XMAS", i + 1) 6 | occurences.drop(1).takeWhile(_ != -1).size 7 | 8 | val right = input.map(countXmas).sum 9 | val left = input.map(_.reverse).map(countXmas).sum 10 | val up = input.transpose.map(_.mkString).map(countXmas).sum 11 | val down = input.transpose.map(_.mkString.reverse).map(countXmas).sum 12 | 13 | val shiftedRight = input.zipWithIndex.map: 14 | case (s, i) => " " * i + s + " " * (input.size - i - 1) 15 | 16 | val rightUp = shiftedRight.transpose.map(_.mkString).map(countXmas).sum 17 | val rightDown = shiftedRight.transpose.map(_.mkString).map(_.reverse).map(countXmas).sum 18 | 19 | val shiftedLeft = input.zipWithIndex.map: 20 | case (s, i) => " " * (input.size - i - 1) + s + " " * i 21 | 22 | val leftUp = shiftedLeft.transpose.map(_.mkString).map(countXmas).sum 23 | val leftDown = shiftedLeft.transpose.map(_.mkString).map(_.reverse).map(countXmas).sum 24 | 25 | val ans1 = right + left + up + down + rightUp + rightDown + leftUp + leftDown 26 | 27 | val grid = input.map(_.toVector).toVector 28 | 29 | import aoc.Area 30 | import aoc.Point 31 | 32 | val area = Area(grid) 33 | 34 | val subAreas = 35 | for 36 | x <- 0 to area.xRange.last - 2 37 | y <- 0 to area.yRange.last - 2 38 | yield Area.bounding(Point(x, y), Point(x + 2, y + 2)) 39 | 40 | def countXmasDiagonal(subArea: Area): Int = 41 | if grid(subArea.topLeft.d.r) != 'A' then 0 42 | else 43 | val crosses = for 44 | (tl, br) <- "MS" zip "SM" 45 | (tr, bl) <- "MS" zip "SM" 46 | yield 47 | grid(subArea.topLeft) == tl && grid(subArea.botRight) == br && 48 | grid(subArea.topRight) == tr && grid(subArea.botLeft) == bl 49 | if crosses.exists(identity) then 1 else 0 50 | 51 | val ans2 = subAreas.map(countXmasDiagonal).sum 52 | -------------------------------------------------------------------------------- /src/main/scala/2021/Day17.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2021/day-17-1.txt").getLines.next() 2 | 3 | case class Point(x: Int, y: Int) 4 | 5 | object Point: 6 | opaque type Position <: Point = Point 7 | opaque type Velocity <: Point = Point 8 | 9 | def Position(x: Int, y: Int): Position = Point(x, y) 10 | def Velocity(x: Int, y: Int): Velocity = Point(x, y) 11 | final val Origin = Position(0, 0) 12 | 13 | extension (p: Position) def move(v: Velocity) = Position(p.x + v.x, p.y + v.y) 14 | extension (v: Velocity) def drag = Velocity(v.x - v.x.sign, v.y - 1) 15 | 16 | import Point.* 17 | 18 | case class Area(left: Int, right: Int, bot: Int, top: Int): 19 | def contains(pos: Position) = 20 | (left to right).contains(pos.x) && (bot to top).contains(pos.y) 21 | 22 | case class Probe(pos: Position, vel: Velocity): 23 | def step = copy(pos.move(vel), vel.drag) 24 | 25 | def missed(a: Area): Boolean = 26 | val falling = vel.y < 0 27 | val vertical = vel.x == 0 28 | val short = pos.x < a.left 29 | val below = pos.y < a.bot 30 | val beyond = pos.x > a.right 31 | short && vertical || below && falling || beyond 32 | 33 | object Probe: 34 | opaque type Trajectory <: LazyList[Probe] = LazyList[Probe] 35 | extension (trajectory: Trajectory) 36 | def hits(target: Area) = trajectory 37 | .takeWhile(!_.missed(target)) 38 | .exists(state => target.contains(state.pos)) 39 | 40 | def launch(vel: Velocity): Trajectory = 41 | LazyList.iterate(Probe(Origin, vel))(_.step) 42 | 43 | val target = input match 44 | case s"target area: x=$left..$right, y=$bot..$top" => 45 | Area(left.toInt, right.toInt, bot.toInt, top.toInt) 46 | 47 | import target.{left, right, bot} 48 | 49 | val launches = for 50 | x <- math.sqrt(left).toInt to right 51 | y <- bot until bot.abs 52 | yield Probe launch Velocity(x, y) 53 | 54 | val ans1 = (bot * bot + bot) / 2 55 | val ans2 = launches.count(_ hits target) 56 | -------------------------------------------------------------------------------- /src/main/scala/2023/Day17.worksheet.sc: -------------------------------------------------------------------------------- 1 | import aoc.* 2 | 3 | val grid = io.Source.fromResource("2023/day-17.txt").getLines 4 | .map(s => s.map(_.asDigit).toVector).toVector 5 | 6 | val area = Area(grid) 7 | 8 | def heatLoss(p: Point) = if area.contains(p) then grid(p) else 0 9 | 10 | case class State(pos: Point, dir: Dir, streak: Int, totalHeatLoss: Int): 11 | def straight: State = 12 | val newPos = pos.move(dir) 13 | State(pos.move(dir), dir, streak + 1, totalHeatLoss + heatLoss(newPos)) 14 | 15 | def turnLeft: State = 16 | val newDir = dir.turnLeft 17 | val newPos = pos.move(newDir) 18 | State(newPos, newDir, 1, totalHeatLoss + heatLoss(newPos)) 19 | 20 | def turnRight: State = 21 | val newDir = dir.turnRight 22 | val newPos = pos.move(newDir) 23 | State(pos.move(newDir), newDir, 1, totalHeatLoss + heatLoss(newPos)) 24 | 25 | def nextStates: List[State] = 26 | List(straight, turnLeft, turnRight).filter: s => 27 | area.contains(s.pos) && s.streak <= 3 28 | 29 | def nextStates2: List[State] = 30 | if streak < 4 then List(straight) 31 | else List(straight, turnLeft, turnRight).filter: s => 32 | area.contains(s.pos) && s.streak <= 10 33 | 34 | def trajectory = (pos, dir, streak) 35 | 36 | def search(next: State => List[State]): Int = 37 | import collection.mutable.{PriorityQueue, Set, Map} 38 | 39 | given Ordering[State] = Ordering.by(_.totalHeatLoss) 40 | val pq = PriorityQueue.empty[State].reverse 41 | 42 | var visiting = State(Point.origin, Dir.E, 0, 0) 43 | val visited = Set(visiting.trajectory) 44 | 45 | while visiting.pos != area.botRight do 46 | println(s"visiting: $visiting") 47 | val states = next(visiting).filterNot(s => visited(s.trajectory)) 48 | pq.enqueue(states*) 49 | visited ++= states.map(_.trajectory) 50 | visiting = pq.dequeue() 51 | 52 | visiting.totalHeatLoss 53 | 54 | val ans1 = search(_.nextStates) 55 | val ans2 = search(_.nextStates2) 56 | -------------------------------------------------------------------------------- /src/main/scala/2021/Day08.worksheet.sc: -------------------------------------------------------------------------------- 1 | import io.Source 2 | import util.Using 3 | import util.chaining.* 4 | 5 | val entries = Using(Source.fromResource("2021/day-08-1.txt")) { 6 | _.getLines.map(Entry.parse).toList 7 | }.get 8 | 9 | enum Segment(val numbers: Set[Int]): 10 | case Top extends Segment(Set(0, 2,3, 5,6,7,8,9)) // 8 11 | case Tl extends Segment(Set(0, 4,5,6, 8,9)) // 6* 12 | case Tr extends Segment(Set(0,1,2,3,4, 7,8,9)) // 8 13 | case Mid extends Segment(Set( 2,3,4,5,6, 8,9)) // 7 14 | case Bl extends Segment(Set(0, 2, 6, 8)) // 4* 15 | case Br extends Segment(Set(0,1, 3,4,5,6,7,8,9)) // 9* 16 | case Bot extends Segment(Set(0, 2,3, 5,6, 8,9)) // 7 17 | // [*] segment uniquely identified by size 18 | 19 | import Segment.* 20 | 21 | case class Entry(patterns: List[String], output: List[String]): 22 | require(patterns.distinct.size == 10) 23 | def solve: Int = { 24 | output.map { signal => 25 | val counts = signal.map(c => patterns.count(_.contains(c))) 26 | signal.size match { 27 | case 2 => 1 28 | case 3 => 7 29 | case 4 => 4 30 | case 7 => 8 31 | case 5 => // 2 3 5 32 | if counts.contains(Bl.numbers.size) then 2 33 | else if counts.contains(Tl.numbers.size) then 5 34 | else 3 35 | case 6 => // 0 6 9 36 | if counts.count(_ == Mid.numbers.size) == 1 then 0 // bot appears as often as mid 37 | else if counts.contains(Bl.numbers.size) then 6 38 | else 9 39 | } 40 | }.mkString.toInt 41 | } 42 | 43 | object Entry: 44 | def parse(raw: String): Entry = 45 | raw.split('|') match { 46 | case Array(p, o) => Entry( 47 | patterns = p.split(' ').filter(_.nonEmpty).toList, 48 | output = o.split(' ').filter(_.nonEmpty).toList 49 | ) 50 | } 51 | 52 | val ans1 = entries.map(_.solve).mkString.count("1478".contains) 53 | val ans2 = entries.map(_.solve).sum 54 | -------------------------------------------------------------------------------- /src/main/scala/2023/Day21.worksheet.sc: -------------------------------------------------------------------------------- 1 | import aoc.* 2 | 3 | val grid = io.Source.fromResource("2023/day-21.txt").getLines.toVector 4 | 5 | val area = Area(grid) 6 | val start = area.pointsIterator.find(p => grid(p) == 'S').get 7 | val rocks = area.pointsIterator.filter(p => grid(p) == '#').toSet 8 | 9 | def stepsFrom(start: Point) = LazyList.unfold(Set(start)): current => 10 | val next = current.flatMap(_.adjacent).filter(area.contains).diff(rocks) 11 | Option.when(current.nonEmpty): 12 | current -> next 13 | 14 | val steps = stepsFrom(start) 15 | 16 | val ans1 = steps(64).size 17 | 18 | val maxSteps = 26501365L 19 | 20 | val centerRight = start.copy(x = area.right) 21 | val centerLeft = start.copy(x = area.left) 22 | val centerTop = start.copy(y = area.top) 23 | val centerBot = start.copy(y = area.bot) 24 | 25 | val cornerPoints = List(area.botLeft, area.topRight, area.botRight, area.topLeft) 26 | val midPoints = List(centerRight, centerLeft, centerTop, centerBot) 27 | 28 | val fullGrids = (maxSteps - start.dist(area.topRight)) / area.width 29 | val stepsRemaining = (maxSteps.toInt - start.dist(area.topRight)) % area.width 30 | 31 | val smallCornerSteps = stepsRemaining - 2 32 | val largeCornerSteps = stepsRemaining + area.topRight.dist(area.topLeft) - 1 33 | val farCornerSteps = stepsRemaining + area.topRight.dist(centerRight) - 1 34 | 35 | val oddCovering = steps(start.dist(area.topRight) + 1) 36 | val evenCovering = steps(start.dist(area.topRight)) 37 | 38 | val smallCorners = cornerPoints.map(stepsFrom(_)(smallCornerSteps)) 39 | val largeCorners = cornerPoints.map(stepsFrom(_)(largeCornerSteps)) 40 | val farCorners = midPoints.map(stepsFrom(_)(farCornerSteps)) 41 | 42 | val ans2 = List( 43 | evenCovering.size.toLong * (fullGrids + 1) * (fullGrids + 1), 44 | oddCovering.size.toLong * fullGrids * fullGrids, 45 | smallCorners.map(_.size.toLong).sum * (fullGrids + 1), 46 | largeCorners.map(_.size.toLong).sum * fullGrids, 47 | farCorners.map(_.size.toLong).sum, 48 | ).sum 49 | -------------------------------------------------------------------------------- /src/main/scala/synacor/numbers/package.scala: -------------------------------------------------------------------------------- 1 | package synacor 2 | package numbers 3 | 4 | opaque type Word = Int 5 | opaque type U15 <: Word = Int 6 | opaque type Adr <: U15 = Int 7 | opaque type Lit <: U15 = Int 8 | opaque type Reg <: Word = Int 9 | 10 | extension (n: Int) 11 | def toLit: Lit = Word.fromInt(n) 12 | 13 | object Word: 14 | def fromBytes(low: Byte, high: Byte): Word = 15 | low & 0xFF | (high << 8) & 0xFF00 16 | 17 | def fromInt(n: Int): Word = 18 | require(n <= 0xFFFF) 19 | n 20 | 21 | extension (w: Word) 22 | def adr: Adr = Adr.fromInt(w) 23 | def lit: Lit = Lit.fromInt(w) 24 | def reg: Reg = Reg.fromInt(w) 25 | // def value: Reg | Lit = 26 | // if (w & 0x8000) == 0x8000 then Reg.fromInt(w) 27 | // else Lit.fromInt(w) 28 | def asChar: Char = w.toChar 29 | def fitsU15: Boolean = (w & 0x8000) == 0x8000 30 | def op: Opcode = Opcode.fromOrdinal(w) 31 | 32 | object U15: 33 | val MaxValue: U15 = 0x7FFF 34 | def fromInt(n: Int): U15 = 35 | require(n <= U15.MaxValue) 36 | n 37 | 38 | object Adr: 39 | def fromInt(n: Int): Adr = U15.fromInt(n) 40 | 41 | extension (a: Adr) 42 | inline def inc1: Adr = a + 1 43 | inline def inc2: Adr = a + 2 44 | inline def inc3: Adr = a + 3 45 | inline def inc4: Adr = a + 4 46 | inline def toIndex: Int = a & 0x7FFF 47 | 48 | object Lit: 49 | def fromInt(n: Int): Lit = U15.fromInt(n) 50 | 51 | extension (a: Lit) 52 | infix def +(b: Lit): Lit = (a + b) & U15.MaxValue 53 | infix def *(b: Lit): Lit = (a * b) & U15.MaxValue 54 | infix def %(b: Lit): Lit = (a % b) & U15.MaxValue 55 | infix def &(b: Lit): Lit = (a & b) & U15.MaxValue 56 | infix def |(b: Lit): Lit = (a | b) & U15.MaxValue 57 | infix def >(b: Lit): Boolean = a > b 58 | def unary_~ = ((~a) & U15.MaxValue): Lit 59 | 60 | object Reg: 61 | def fromInt(n: Int): Reg = 62 | require(0x8000 <= n && n <= 0x8007) 63 | n 64 | 65 | extension (r: Reg) 66 | def toIndex(using DummyImplicit): Int = r & 0x7FFF 67 | -------------------------------------------------------------------------------- /src/main/scala/2022/Day23.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2022/day-23.txt").getLines().toVector 2 | 3 | val elvesStart: Set[Point] = 4 | val points = for 5 | y <- input.indices 6 | x <- input.head.indices 7 | if input(y)(x) == '#' 8 | yield Point(x, y) 9 | points.toSet 10 | 11 | case class Point(x: Int, y: Int): 12 | def n = copy(y = y - 1) 13 | def s = copy(y = y + 1) 14 | def w = copy(x = x - 1) 15 | def e = copy(x = x + 1) 16 | def surrounding = List(n, s, w, e, n.w, n.e, s.w, s.e) 17 | 18 | def propose(elf: Point, elves: Set[Point], round: Int): Option[Point] = 19 | import elf.* 20 | def tryMoving = 21 | def goNorth = Option.unless(List(n, n.w, n.e).exists(elves))(n) 22 | def goSouth = Option.unless(List(s, s.w, s.e).exists(elves))(s) 23 | def goWest = Option.unless(List(w, n.w, s.w).exists(elves))(w) 24 | def goEast = Option.unless(List(e, n.e, s.e).exists(elves))(e) 25 | round % 4 match 26 | case 0 => goNorth orElse goSouth orElse goWest orElse goEast 27 | case 1 => goSouth orElse goWest orElse goEast orElse goNorth 28 | case 2 => goWest orElse goEast orElse goNorth orElse goSouth 29 | case 3 => goEast orElse goNorth orElse goSouth orElse goWest 30 | if surrounding.exists(elves) then tryMoving else None 31 | 32 | def rounds = LazyList.unfold(elvesStart -> 0) { case (elves, round) => 33 | val proposals = elves 34 | .groupBy(propose(_, elves, round)) 35 | .collect { 36 | case (Some(dest), starts) if starts.sizeIs == 1 => 37 | starts.head -> dest 38 | } 39 | Option.when(proposals.nonEmpty) { 40 | val (starts, dests) = proposals.toSet.unzip 41 | val next = elves.diff(starts).union(dests) 42 | next -> (next, round + 1) 43 | } 44 | } 45 | 46 | val roundTen = rounds(9) 47 | 48 | val xRange = roundTen.map(_.x).min to roundTen.map(_.x).max 49 | val yRange = roundTen.map(_.y).min to roundTen.map(_.y).max 50 | 51 | val ans1 = xRange.size * yRange.size - roundTen.size 52 | 53 | val ans2 = 1 + rounds.size 54 | -------------------------------------------------------------------------------- /src/main/scala/2021/Day25.worksheet.sc: -------------------------------------------------------------------------------- 1 | import util.Using 2 | import io.Source 3 | import util.chaining.* 4 | 5 | val input = Using(Source.fromResource("2021/day-25-1.txt"))(_.getLines.toList).get 6 | 7 | case class Point(x: Int, y: Int) 8 | 9 | case class Floor(width: Int, height: Int, south: Set[Point], east: Set[Point]): 10 | extension(p: Point) 11 | def s = p.copy(y = (p.y + 1) % height) 12 | def e = p.copy(x = (p.x + 1) % width) 13 | def clear: Boolean = !(south(p) || east(p)) 14 | 15 | def moveSouth: (Set[Point], Floor) = 16 | val (moving, stayed) = south.partition(_.s.clear) 17 | val moved = moving.map(_.s) 18 | moved -> copy(south = stayed union moved) 19 | 20 | def moveEast: (Set[Point], Floor) = 21 | val (moving, stayed) = east.partition(_.e.clear) 22 | val moved = moving.map(_.e) 23 | moved -> copy(east = stayed union moved) 24 | 25 | def step: (Set[Point], Floor) = 26 | this.moveEast.pipe { 27 | case (movedEast, state) => 28 | state.moveSouth.pipe { 29 | case (movedSouth, state) => 30 | (movedEast union movedSouth) -> state 31 | } 32 | } 33 | 34 | def draw: String = 35 | val sb = collection.mutable.StringBuilder() 36 | for y <- 0 until width do 37 | for x <- 0 until width do 38 | val p = Point(x,y) 39 | sb += (if south(p) then 'v' else if east(p) then '>' else '.') 40 | sb += '\n' 41 | sb.result() 42 | 43 | val floor = 44 | val eb = collection.mutable.ListBuffer.empty[Point] 45 | val sb = collection.mutable.ListBuffer.empty[Point] 46 | for (row, y) <- input.zipWithIndex do 47 | for (o, x) <- row.zipWithIndex do 48 | o match 49 | case '>' => eb += Point(x,y) 50 | case 'v' => sb += Point(x,y) 51 | case _ => () 52 | Floor(width = input(0).size, height = input.size, south = sb.result().toSet, east = eb.result().toSet) 53 | 54 | def steps: Iterator[Set[Point]] = Iterator.unfold(floor)(f => Some(f.step)) 55 | 56 | val ans1 = steps.indexWhere(_.isEmpty) + 1 57 | -------------------------------------------------------------------------------- /src/main/scala/2024/Day14Variants.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2024/day-14.txt").getLines.toList 2 | 3 | import aoc.{Point, Area} 4 | 5 | val area = Area(0 until 101, 0 until 103) 6 | 7 | def wrap(p: Point) = 8 | val x = (p.x + area.width) % area.width 9 | val y = (p.y + area.height) % area.height 10 | Point(x, y) 11 | 12 | // todo test negative 13 | 14 | case class Robot(p: Point, v: Point): 15 | def move(n: Int): Robot = copy(p = wrap(p + v*n)) 16 | 17 | val robots = input.collect: 18 | case s"p=$px,$py v=$vx,$vy" => 19 | Robot(Point(px.toInt, py.toInt), Point(vx.toInt, vy.toInt)) 20 | 21 | val finalPositions = robots.map(_.move(100)).map(_.p) 22 | 23 | val quadrants = 24 | val midX = area.width / 2 25 | val midY = area.height / 2 26 | List( 27 | Area(0 until midX, 0 until midY), 28 | Area((midX + 1) until 101, 0 until midY), 29 | Area(0 until midX, (midY + 1) until 103), 30 | Area((midX + 1) until 101, (midY + 1) until 103)) 31 | 32 | val ans1 = quadrants 33 | .map(q => finalPositions.count(q.contains)) 34 | .map(_.toLong) 35 | .product 36 | 37 | def showImage(state: List[Robot]): Unit = 38 | val ps = state.map(_.p).toSet 39 | println: 40 | area.draw: p => 41 | if ps(p) then '#' else '.' 42 | 43 | // correct vertical state found visually 44 | // could find programmatically using density heuristic 45 | val state65 = robots.map(_.move(65)) 46 | 47 | def possibleSolutions = Iterator 48 | .iterate(state65)(_.map(_.move(area.height))) 49 | .take(area.width) 50 | 51 | // possibleSolutions.zipWithIndex.foreach: (s, i) => 52 | // println(s"seconds: ${65 + area.height * i}") 53 | // showImage(s) 54 | // println() 55 | 56 | showImage(robots.map(_.move(7687))) 57 | 58 | // Approaches: 59 | // find border - requires guessing that it has a border 60 | // find solid area - requires guessing 61 | // seek state with many connected components - works with holes or off center 62 | // symmetry -- image technically symmetric, but not centered, requires guessing 63 | // 64 | -------------------------------------------------------------------------------- /src/main/scala/2022/Day24.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2022/day-24.txt").getLines().toVector 2 | 3 | val inBounds: Set[Point] = 4 | val points = for 5 | y <- input.indices 6 | x <- input.head.indices 7 | c = input(y)(x) 8 | if c != '#' 9 | yield Point(x, y) 10 | points.toSet 11 | 12 | case class Point(x: Int, y: Int): 13 | def n = copy(y = y - 1) 14 | def s = copy(y = y + 1) 15 | def e = copy(x = x + 1) 16 | def w = copy(x = x - 1) 17 | def adj = List(n, s, e, w).filter(inBounds) 18 | def distTo(that: Point): Int = 19 | val dx = that.x - this.x 20 | val dy = that.y - this.y 21 | dx.abs + dy.abs 22 | 23 | val start = inBounds.minBy(_.y) 24 | val end = inBounds.maxBy(_.y) 25 | 26 | val valley = inBounds - start - end 27 | 28 | val xRange = valley.map(_.x).min to valley.map(_.x).max 29 | val yRange = valley.map(_.y).min to valley.map(_.y).max 30 | 31 | case class State(pos: Point, time: Int): 32 | def nextStates = 33 | (pos :: pos.adj).map(State(_, time + 1)).filterNot(_.blizzard) 34 | 35 | def blizzard = 36 | val ny = math.floorMod(pos.y + time - 1, yRange.size) + 1 37 | val sy = math.floorMod(pos.y - time - 1, yRange.size) + 1 38 | val ex = math.floorMod(pos.x - time - 1, xRange.size) + 1 39 | val wx = math.floorMod(pos.x + time - 1, xRange.size) + 1 40 | 41 | input(ny)(pos.x) == '^' 42 | || input(sy)(pos.x) == 'v' 43 | || input(pos.y)(ex) == '>' 44 | || input(pos.y)(wx) == '<' 45 | 46 | def search(start: Point, end: Point, time: Int): Int = 47 | import collection.mutable.{PriorityQueue, Set} 48 | given Ordering[State] = Ordering.by(s => s.time + s.pos.distTo(end)) 49 | 50 | val pq = PriorityQueue.empty[State].reverse 51 | var visiting = State(start, time) 52 | val visited = Set(visiting) 53 | 54 | while visiting.pos != end do 55 | val states = visiting.nextStates.filterNot(visited) 56 | pq.enqueue(states*) 57 | visited ++= states 58 | visiting = pq.dequeue() 59 | 60 | visiting.time 61 | 62 | val ans1 = search(start, end, 0) 63 | val t2 = search(end, start, ans1) 64 | val ans2 = search(start, end, t2) 65 | -------------------------------------------------------------------------------- /src/main/scala/2023/Day14.worksheet.sc: -------------------------------------------------------------------------------- 1 | import aoc.* 2 | 3 | val grid = io.Source.fromResource("2023/day-14.txt").getLines().toVector 4 | val area = Area(grid) 5 | 6 | val rounded: Set[Point] = area.pointsIterator.filter(grid(_) == 'O').toSet 7 | val squared: Set[Point] = 8 | val real = area.pointsIterator.filter(grid(_) == '#').toSet 9 | val outer = area.expand(1).diff(area).flatMap(_.pointsIterator).toSet 10 | real union outer 11 | 12 | val openArea = Map.from[Dir, Map[Point, Area]]: 13 | Dir.values.map: dir => 14 | dir -> Map.from: 15 | squared.flatMap: squarePoint => 16 | val points = Set.from: 17 | Iterator.iterate(squarePoint)(_.move(dir)).drop(1) 18 | .takeWhile(area.contains) 19 | .takeWhile(!squared(_)) 20 | Option.when(points.nonEmpty)(squarePoint -> Area.bounding(points)) 21 | 22 | def rollAll(start: Set[Point], dir: Dir): Set[Point] = 23 | openArea.apply(dir.reverse).foldLeft(Set.empty[Point]): 24 | case (acc, (p, a)) => 25 | val movingCount = a.pointsIterator.count(start) 26 | acc union Set.from: 27 | dir match 28 | case Dir.N => (p.s.y to p.y + movingCount).map(Point(p.x, _)) 29 | case Dir.W => (p.e.x to p.x + movingCount).map(Point(_, p.y)) 30 | case Dir.S => (p.n.y to p.y - movingCount by -1).map(Point(p.x, _)) 31 | case Dir.E => (p.w.x to p.x - movingCount by -1).map(Point(_, p.y)) 32 | 33 | def cycle(start: Set[Point]) = 34 | List(Dir.N, Dir.W, Dir.S, Dir.E).foldLeft(start)(rollAll) 35 | 36 | def load(points: Set[Point]): Int = 37 | points.iterator.map(p => area.height - p.y).sum 38 | 39 | val ans1 = load(rollAll(rounded, Dir.N)) 40 | 41 | val cycles = LazyList.iterate(rounded)(cycle) 42 | val hashes = cycles.map(_.hashCode()) 43 | 44 | val seenHashes = hashes.scanLeft(Set.empty[Int]): 45 | case (seen, hash) => (seen + hash) 46 | 47 | val repeat = seenHashes.zip(hashes).indexWhere((seen, hash) => seen(hash)) 48 | val first = hashes.indexWhere(_ == hashes(repeat)) 49 | val period = repeat - first 50 | 51 | val congruent = first + (1_000_000_000 - first) % period 52 | val ans2 = load(cycles(congruent)) 53 | -------------------------------------------------------------------------------- /src/main/scala/2020/Day04.worksheet.sc: -------------------------------------------------------------------------------- 1 | import util.Using 2 | 3 | val passports = Using(io.Source.fromResource("2020/day-04.txt")){ source => 4 | val lines = source.getLines 5 | val ps = new collection.mutable.ListBuffer[String]() 6 | while lines.hasNext 7 | do ps += lines.takeWhile(_.nonEmpty).mkString(" ") 8 | ps.result() 9 | }.get 10 | 11 | passports foreach println 12 | 13 | passports(0) 14 | passports(1) 15 | passports(2) 16 | 17 | val fields = Set("byr", "iyr", "eyr", "hgt", "hcl", "ecl", "pid") 18 | 19 | val ans1 = passports.count(p => fields.forall(p.contains)) 20 | 21 | def valid(passport: String): Boolean = 22 | val kvs = passport.split(" ") 23 | kvs.map { case s"$key:$value" => key -> value } 24 | .forall { 25 | case ("byr", digits) => 1920 to 2002 contains digits.toInt 26 | case ("iyr", digits) => 2010 to 2020 contains digits.toInt 27 | case ("eyr", digits) => 2020 to 2030 contains digits.toInt 28 | case ("hgt", s"${cm}cm") => 150 to 193 contains cm.toInt 29 | case ("hgt", s"${in}in") => 59 to 76 contains in.toInt 30 | case ("hcl", s"#$hex") => hex.sizeIs == 6 && hex.forall(c => ('0' to '9' contains c) || ('a' to 'f' contains c)) 31 | case ("ecl", ecl) => "amb blu brn gry grn hzl oth".split(" ").contains(ecl) 32 | case ("pid", number) => number.sizeIs == 9 33 | case ("cid", _) => true 34 | case _ => false 35 | } 36 | 37 | 38 | valid("iyr:2010 hgt:158cm hcl:#b6652a ecl:blu byr:1944 eyr:2021 pid:093154719") 39 | 40 | valid("hcl:dab227 iyr:2012 ecl:brn hgt:182cm pid:021572410 eyr:2020 byr:1992 cid:277") 41 | 42 | 43 | // val s"#$hex" = "hcl:dab227" 44 | 45 | // val hex = "123abz" 46 | // hex.forall(c => ('0' to '9' contains c) || ('a' to 'f' contains c)) 47 | 48 | // val ecl = "brn" 49 | // "amb blu brn gry grn hzl oth".split(" ").contains(ecl) 50 | 51 | // val s"${in}in" = "190in" 52 | // 59 to 76 contains in.toInt 53 | // val s"${cm}cm" = "190cm" 54 | // 150 to 193 contains cm.toInt 55 | 56 | 57 | val ans2 = passports 58 | .filter(p => fields.forall(p.contains)) 59 | .count(valid) 60 | 61 | // kvs.map { case s"$key:$value" => key -> value }.toMap 62 | // kvs.get("byr").collect { case digits if } 63 | -------------------------------------------------------------------------------- /src/main/scala/2024/Day16.worksheet.sc: -------------------------------------------------------------------------------- 1 | import aoc.{Point, Area, Dir} 2 | 3 | val maze = io.Source.fromResource("2024/day-16.txt").getLines.toVector 4 | 5 | val area = Area(maze) 6 | val startPos = area.pointsIterator.find(maze(_) == 'S').get 7 | val endPos = area.pointsIterator.find(maze(_) == 'E').get 8 | val walls = area.pointsIterator.filter(maze(_) == '#').toSet 9 | 10 | case class State(pos: Point, dir: Dir): 11 | def next: List[(State, Int)] = 12 | List(dir, dir.turnLeft, dir.turnRight) 13 | .zip(List(1, 1001, 1001)) 14 | .map((d, s) => ((pos.move(d), d, s))) 15 | .filter((p, _, _) => p.inBounds(area) && !walls(p)) 16 | .map((p, d, s) => State(p, d) -> s) 17 | 18 | def search: (Int, Int) = 19 | import collection.mutable.{PriorityQueue, Map} 20 | 21 | val start = State(startPos, Dir.E) 22 | val minCost = Map[State, Int](start -> 0) 23 | val queue = PriorityQueue.empty[State](Ordering.by(minCost).reverse) 24 | 25 | val pathsBack = Map 26 | .empty[(State, Int), List[(State, Int)]] 27 | .withDefaultValue(Nil) 28 | 29 | var visiting = start 30 | var endCost = Int.MaxValue 31 | 32 | while minCost(visiting) <= endCost do 33 | if visiting.pos == endPos then 34 | endCost = endCost min minCost(visiting) 35 | 36 | visiting.next 37 | .filterNot: (s, c) => 38 | minCost.get(s).exists(_ < minCost(visiting) + c) 39 | .foreach: (s, c) => 40 | minCost(s) = minCost(visiting) + c 41 | queue.enqueue(s) 42 | pathsBack(s -> minCost(s)) = 43 | (visiting, minCost(visiting)) :: pathsBack(s -> minCost(s)) 44 | 45 | visiting = queue.dequeue 46 | 47 | val endStates = Dir.values.map(State(endPos, _) -> endCost).toSet 48 | val seats = Iterator.unfold(endStates): states => 49 | val nextStates = states.flatMap(pathsBack) 50 | val nextPositions = nextStates.map(_._1.pos) 51 | Option.when(nextStates.nonEmpty)(nextPositions -> nextStates) 52 | .reduce(_ union _) + endPos 53 | 54 | println: 55 | area.draw: p => 56 | if seats.contains(p) then '█' 57 | else if walls(p) then '#' 58 | else ' ' 59 | 60 | (endCost, seats.size) 61 | 62 | val (ans1, ans2) = search 63 | -------------------------------------------------------------------------------- /src/main/scala/2024/Day13.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2024/day-13.txt").getLines.toList 2 | 3 | // import aoc.Point 4 | 5 | case class Point(x: Long, y: Long): 6 | def plus(p: Point) = Point(x + p.x, y + p.y) 7 | def minus(p: Point) = Point(x - p.x, y - p.y) 8 | 9 | infix def -(p: Point) = minus(p) 10 | infix def +(p: Point) = plus(p) 11 | infix def *(n: Long) = Point(x * n, y * n) 12 | 13 | object Point: 14 | val origin = Point(0L, 0L) 15 | 16 | val aButtons = input.collect: 17 | case s"Button A: X+$x, Y+$y" => Point(x.toLong, y.toLong) 18 | 19 | val bButtons = input.collect: 20 | case s"Button B: X+$x, Y+$y" => Point(x.toLong, y.toLong) 21 | 22 | val prizes = input.collect: 23 | case s"Prize: X=$x, Y=$y" => Point(x.toLong, y.toLong) 24 | 25 | case class Machine( 26 | a: Point, 27 | b: Point, 28 | prize: Point 29 | ) 30 | 31 | val machines = aButtons.zip(bButtons).zip(prizes).map: 32 | case ((a, b), prize) => Machine(a, b, prize) 33 | 34 | val machines2 = machines.map: m => 35 | val off = 10000000000000L 36 | m.copy(prize = Point(m.prize.x + off, m.prize.y + off)) 37 | 38 | def winnable(m: Machine): Boolean = 39 | // necessary but not sufficient 40 | import aoc.math.{gcf, gcfN, lcm, lcmN} 41 | val xWorks = m.prize.x % gcf(m.a.x, m.b.x) == 0 42 | val yWorks = m.prize.y % gcf(m.a.y, m.b.y) == 0 43 | xWorks && yWorks 44 | 45 | machines.count(winnable) 46 | machines.flatMap(solve).size 47 | 48 | machines 49 | .filter(m => solve(m).nonEmpty) 50 | .count(winnable) 51 | 52 | def solve(m: Machine): Option[(Long, Long)] = 53 | import m.{a, b, prize => p} 54 | for 55 | tb <- locally: 56 | val n = a.y*p.x - a.x*p.y 57 | val d = b.x*a.y - b.y*a.x 58 | Option.when(n % d == 0)(n / d) 59 | ta <- locally: 60 | val n = p.y - b.y * tb 61 | val d = a.y 62 | Option.when(n % d == 0)(n / d) 63 | yield 64 | assert(a*ta + b*tb == p) 65 | assert(ta > 0) 66 | assert(tb > 0) 67 | // TODO: why is it always positive? 68 | ta -> tb 69 | 70 | def cost(c: (Long, Long)): Long = c._1*3 + c._2 71 | 72 | val ans1 = machines.flatMap(solve).map(cost) .sum 73 | val ans2 = machines2.flatMap(solve).map(cost).sum 74 | -------------------------------------------------------------------------------- /src/main/scala/2020/Day14.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2020/day-14.txt").getLines.toList 2 | 3 | input.size 4 | // val mem = collection.mutable.Map().withDefaultValue(0) 5 | 6 | def masked(n: Long, mask: String): Long = 7 | val bin = n.toBinaryString.reverse.padTo(36, '0').reverse 8 | val newBin = bin.zip(mask).map { 9 | case (b, 'X') => b 10 | case (_, '1') => '1' 11 | case (_, '0') => '0' 12 | } 13 | BigInt.apply(newBin.mkString, 2).toLong 14 | 15 | def maskedAddresses(a: Long, mask: String): List[Long] = 16 | val bin = a.toBinaryString.reverse.padTo(36, '0').reverse.toList 17 | def newBins(addresses: List[String], pairs: List[(Char, Char)]): List[String] = pairs match 18 | case (_, 'X') :: tail => 19 | val both = addresses.flatMap { 20 | addr => List(addr :+ '0', addr :+ '1') 21 | } 22 | newBins(both, tail) 23 | case (_, '1') :: tail => newBins(addresses.map(_ :+ '1'), tail) 24 | case (b, '0') :: tail => newBins(addresses.map(_ :+ b), tail) 25 | case Nil => addresses 26 | newBins(List(""), bin.zip(mask)) 27 | // .tapEach(println(_)) 28 | .map(a => BigInt.apply(a.mkString, 2).toLong) 29 | 30 | 31 | maskedAddresses(42L, "000000000000000000000000000000X1001X") 32 | maskedAddresses(26L, "00000000000000000000000000000000X0XX") 33 | 34 | case class State(mask: String, memory: Map[Long, Long]): 35 | def interperet(inst: String): State = inst match 36 | case s"mask = $m" => copy(mask = m) 37 | case s"mem[$a] = $v" => copy(memory = memory.updated(a.toLong, masked(v.toLong, mask))) 38 | 39 | def interperet2(inst: String): State = inst match 40 | case s"mask = $m" => copy(mask = m) 41 | case s"mem[$a] = $v" => 42 | val addrs = maskedAddresses(a.toLong, mask) 43 | addrs.foldLeft(this)((s, addr) => 44 | s.copy(memory = s.memory.updated(addr, v.toLong)) 45 | ) 46 | 47 | 48 | val start = State(mask = "X" * 36, Map.empty.withDefaultValue(0L)) 49 | 50 | val finalState: State = input.foldLeft(start)((s, i) => s.interperet(i)) 51 | 52 | val ans = finalState.memory.values.sum 53 | 54 | val ans2 = 55 | val s = input.foldLeft(start)(_ interperet2 _) 56 | s.memory.values.sum 57 | 58 | input.foldLeft(start)(_ interperet2 _).memory 59 | -------------------------------------------------------------------------------- /src/main/scala/2022/Day15.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2022/day-15.txt").getLines().toList 2 | 3 | case class Point(x: Int, y: Int): 4 | def dist(other: Point) = 5 | (other.x - x).abs + (other.y - y).abs 6 | 7 | val radii = input.collect { 8 | case s"Sensor at x=$sx, y=$sy: closest beacon is at x=$bx, y=$by" => 9 | val sensor = Point(sx.toInt, sy.toInt) 10 | val beacon = Point(bx.toInt, by.toInt) 11 | Radius(sensor, sensor.dist(beacon)) 12 | } 13 | 14 | case class Interval(min: Int, max: Int): 15 | override def toString = s"$min..$max" 16 | 17 | def size = max + 1 - min 18 | def contains(n: Int) = min <= n && n <= max 19 | def iterator = scala.Range.inclusive(min, max).iterator 20 | 21 | def diff(n: Interval): List[Interval] = 22 | if min < n.min && n.max < max then 23 | List(copy(max = n.min - 1), copy(min = n.max + 1)) 24 | else if n.min <= min && max <= n.max then Nil 25 | else if n contains max then List(copy(max = n.min - 1)) 26 | else if n contains min then List(copy(min = n.max + 1)) 27 | else List(this) 28 | 29 | def diff(intervals: Seq[Interval]): List[Interval] = 30 | intervals.foldLeft(List(this)) { (disjoint, n) => 31 | disjoint.flatMap(_.diff(n)) 32 | } 33 | 34 | object Interval: 35 | def apply(n1: Int, n2: Int): Interval = new Interval(n1 min n2, n1 max n2) 36 | 37 | case class Radius(s: Point, d: Int): 38 | def atRow(y: Int): Option[Interval] = 39 | val h = d - (s.y - y).abs 40 | Option.when(h >= 0)(Interval(s.x - h, s.x + h)) 41 | 42 | def disjointUnion(ranges: Seq[Interval]): List[Interval] = 43 | ranges.foldLeft[List[Interval]](Nil) { (disjoint, r) => 44 | r :: disjoint.flatMap(_.diff(r)) 45 | } 46 | 47 | val xNonRanges = radii.flatMap(_.atRow(y = 2000000)) 48 | val ans1 = disjointUnion(xNonRanges).map(_.size).sum 49 | 50 | val MAX = 4000000 51 | val possibleRange = Interval(0, MAX) 52 | 53 | val possibleBeacons: Iterator[Point] = 54 | for 55 | y <- possibleRange.iterator 56 | xNonRanges = radii.flatMap(_.atRow(y)) 57 | xRange <- possibleRange.diff(xNonRanges) 58 | x <- xRange.iterator 59 | yield Point(x, y) 60 | 61 | val distress = possibleBeacons.next() 62 | val ans2 = distress.x.toLong * MAX + distress.y 63 | -------------------------------------------------------------------------------- /src/main/scala/2018/Day04.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2018/day-04.txt").getLines().toList.sorted 2 | 3 | input foreach println 4 | 5 | type Time = String 6 | enum Event(time: Time): 7 | // case BeginShift(id: Int) 8 | case Asleep(time: Time) extends Event(time) 9 | case Awake(time: Time) extends Event(time) 10 | 11 | import Event.{Asleep, Awake} 12 | 13 | input.take(20) foreach println 14 | 15 | case class Shift(id: Int, start: Time, events: List[Event]) 16 | 17 | def parseShift(logs: List[String]): (Shift, List[String]) = 18 | assert(logs.nonEmpty) 19 | val s"[$timestamp] Guard #$id begins shift" = logs.head 20 | val (activity, remainder) = logs.tail.span(!_.contains('#')) 21 | println(s"parsing events from $activity") 22 | val events = activity.map: 23 | case s"[$timestamp] falls asleep" => Asleep(timestamp) 24 | case s"[$timestamp] wakes up" => Awake(timestamp) 25 | (Shift(id.toInt, timestamp, events), remainder) 26 | 27 | val allShifts: List[Shift] = 28 | Iterator.unfold(input): logs => 29 | Option.when(logs.nonEmpty)(parseShift(logs)) 30 | .toList 31 | 32 | val shiftsByGuard = allShifts.groupBy(_.id) 33 | 34 | def whichMinutesAsleep(shift: Shift): List[Int] = 35 | shift.events.grouped(2).flatMap: 36 | case List(Asleep(start), Awake(end)) => 37 | val s"$day1 $h1:$m1" = start 38 | val startMin = h1.toInt * 60 + m1.toInt 39 | 40 | val s"$day2 $h2:$m2" = end 41 | val endMin = h2.toInt * 60 + m2.toInt 42 | 43 | (startMin until endMin) 44 | .toList 45 | 46 | def totalMinutesAsleep(shift: Shift): Int = 47 | whichMinutesAsleep(shift).size 48 | 49 | val (sleepiestGuard, sleepyShifts) = 50 | shiftsByGuard.maxBy: 51 | case (id, shifts) => shifts.map(totalMinutesAsleep).sum 52 | 53 | val ans1 = sleepiestMinute(sleepyShifts)._1 * sleepiestGuard 54 | 55 | def sleepiestMinute(shifts: List[Shift]): (Int, Int) = 56 | shifts 57 | .flatMap(whichMinutesAsleep) 58 | .groupMapReduce(identity)(_ => 1)(_ + _) 59 | .maxByOption(_._2).getOrElse((0, 0)) 60 | 61 | val ans2 = shiftsByGuard.map: 62 | case (id, shifts) => (id, sleepiestMinute(shifts)) 63 | .maxByOption: 64 | case (id, (minute, count)) => count 65 | .map: 66 | case (id, (minute, count)) => id * minute 67 | -------------------------------------------------------------------------------- /src/main/scala/2023/Day07.worksheet.sc: -------------------------------------------------------------------------------- 1 | import math.Ordering.Implicits.given 2 | 3 | val input = io.Source.fromResource("2023/day-07.txt").getLines().toList 4 | 5 | enum Card: 6 | case Ace, King, Queen, Ten, Nine, Eight, Seven, Six, Five, Four, Three, Two, Joker 7 | 8 | given Ordering[Card] = Ordering.by(-_.ordinal) 9 | 10 | enum Hand: 11 | case FiveKind, FourKind, FullHouse, ThreeKind, TwoPair, OnePair, HighCard 12 | 13 | given Ordering[Hand] = Ordering.by(-_.ordinal) 14 | 15 | import Hand.* 16 | import Card.* 17 | 18 | def parseCard(c: Char) = c match 19 | case 'J' => Joker 20 | case '2' => Two 21 | case '3' => Three 22 | case '4' => Four 23 | case '5' => Five 24 | case '6' => Six 25 | case '7' => Seven 26 | case '8' => Eight 27 | case '9' => Nine 28 | case 'T' => Ten 29 | case 'Q' => Queen 30 | case 'K' => King 31 | case 'A' => Ace 32 | 33 | 34 | val hands: List[(Hand, Vector[Card], Int)] = input.map: line => 35 | val s"$cardsStr $bid" = (line: @unchecked) 36 | val cards = cardsStr.map(parseCard).toVector 37 | (chooseHand(cards), cards, bid.toInt) 38 | 39 | def chooseHand(cards: Vector[Card]): Hand = 40 | val cardGroups = cards.groupMapReduce(identity)(_ => 1)(_ + _).toList.sortBy((c, count) => count -> c).reverse 41 | cardGroups match 42 | case List((card, 5)) => FiveKind 43 | case List((card, 4), (card2, 1)) => 44 | if cards.contains(Joker) then FiveKind 45 | else FourKind 46 | case List((card, 3), (card2, 2)) => 47 | if cards.contains(Joker) then FiveKind 48 | else FullHouse 49 | case List((card, 3), (card2, 1), (card3, 1)) => 50 | if cards.contains(Joker) then FourKind 51 | else ThreeKind 52 | case List((card, 2), (card2, 2), (card3, 1)) => 53 | if card == Joker || card2 == Joker then FourKind 54 | else if card3 == Joker then FullHouse 55 | else TwoPair 56 | case List((card, 2), (card2, 1), (card3, 1), (card4, 1)) => 57 | if cards.contains(Joker) then ThreeKind 58 | else OnePair 59 | case _ => 60 | if cards.contains(Joker) then OnePair 61 | else HighCard 62 | 63 | val sorted = hands.sortBy: 64 | case (hand, cards, bid) => (hand, cards) 65 | 66 | hands.filter(_._2.contains(Joker)).sortBy: 67 | case (hand, cards, bid) => (hand, cards) 68 | 69 | val scores = sorted.zipWithIndex.map: 70 | case ((_, _, bid), i) => bid * (i + 1) 71 | 72 | val ans1 = scores.sum 73 | -------------------------------------------------------------------------------- /src/main/scala/2023/Day05.worksheet.sc: -------------------------------------------------------------------------------- 1 | import annotation.unchecked 2 | 3 | type Range = aoc.Interval[Long] 4 | val Range = aoc.Interval 5 | case class ConversionMap(name: String, ranges: List[(Range, Range)]) 6 | 7 | val input = io.Source.fromResource("2023/day-05.txt").getLines() 8 | 9 | val rawSeeds = input.next() 10 | 11 | val seeds1: List[Range] = 12 | // @annotation.nowarn("msg=binding&msg=StringContext") 13 | // val s"seeds: $seedsStr" = (rawSeeds: @unchecked) 14 | // val s"seeds: $seedsStr" = input.next() 15 | @annotation.nowarn("msg=binding&msg=StringContext") 16 | val s"seeds: $seedsStr" = rawSeeds 17 | val nums = seedsStr.split(" ").map(_.toLong).toList 18 | nums.map(n => Range(n, n)) 19 | 20 | val seeds2: List[Range] = 21 | @annotation.nowarn("msg=binding&msg=StringContext") 22 | val s"seeds: $seedsStr" = rawSeeds 23 | val nums = seedsStr.split(" ").map(_.toLong).toList 24 | nums.grouped(2).toList.collect: 25 | case List(a, b) => Range(a until a + b) 26 | 27 | input.next() 28 | 29 | val conversionMaps = 30 | val lb = collection.mutable.ListBuffer[ConversionMap]() 31 | while input.nonEmpty do 32 | val name = input.next() 33 | val lines = input.takeWhile(_.nonEmpty) 34 | val ranges = lines.map: 35 | case s"$a $b $c" => 36 | val dest = a.toLong 37 | val source = b.toLong 38 | val length = c.toLong 39 | (Range(dest until dest + length), Range(source until source + length)) 40 | lb += ConversionMap(name, ranges.toList) 41 | lb.result() 42 | 43 | conversionMaps.map(_.name) foreach println 44 | 45 | def translate(c: Range, d: Range, s: Range): Range = 46 | val i = c.intersect(s).get 47 | val delta = d.min - s.min 48 | Range(i.min + delta, i.max + delta) 49 | 50 | def next(current: List[Range], conversionMap: ConversionMap): List[Range] = 51 | import conversionMap.ranges 52 | current.flatMap: c => 53 | val overlapping = ranges.filter: 54 | case (_, s) => s.contains(c.max) || s.contains(c.min) 55 | 56 | val translations = overlapping.map(translate(c, _, _)) 57 | 58 | val remainders = overlapping.foldLeft(List(c)): 59 | case (ranges, (_, s)) => ranges.flatMap(_.diff(s)) 60 | 61 | translations ++ remainders 62 | 63 | val locations1 = conversionMaps.foldLeft(seeds1)(next) 64 | val ans1 = locations1.map(_.min).min 65 | 66 | val locations2 = conversionMaps.foldLeft(seeds2)(next) 67 | val ans2 = locations2.map(_.min).min 68 | -------------------------------------------------------------------------------- /src/main/scala/2024/Day06.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2024/day-06.txt").getLines.toVector 2 | 3 | import aoc.{Point, Area, Dir, Line} 4 | 5 | val area = Area(input.toVector) 6 | 7 | val obstacles = Set.from: 8 | for 9 | x <- area.xRange 10 | y <- area.yRange 11 | p = Point(x, y) 12 | if input(p) == '#' 13 | yield p 14 | 15 | case class Guard(pos: Point, dir: Dir) 16 | 17 | val initGuard: Guard = area 18 | .pointsIterator 19 | .collectFirst: 20 | case p if input(p) == '^' => Guard(p, Dir.N) 21 | .get 22 | 23 | def walk(guard: Guard, obstacles: Set[Point]): Guard = 24 | val next = guard.pos.move(guard.dir) 25 | if obstacles(next) then 26 | Guard(guard.pos, guard.dir.turnRight) 27 | else 28 | Guard(next, guard.dir) 29 | 30 | def walk(guard: Guard, obstaclesVert: Map[Int, Set[Point]], obstaclesHorz: Map[Int, Set[Point]]): Option[Guard] = 31 | guard match 32 | case Guard(pos@Point(x, y), dir) => 33 | val pointsInWay = 34 | if dir.isVertical then 35 | obstaclesVert(x) 36 | .filter(o => if dir == Dir.N then o.y < y else o.y > y) 37 | else 38 | obstaclesHorz(y) 39 | .filter(o => if dir == Dir.W then o.x < x else o.x > x) 40 | 41 | Option.when(pointsInWay.nonEmpty): 42 | val endPos = pointsInWay.minBy(pos.dist).move(dir.reverse) 43 | Guard(endPos, dir.turnRight) 44 | 45 | val path = Iterator.iterate(initGuard)(walk(_, obstacles)) 46 | 47 | val visited = path.takeWhile(g => area.contains(g.pos)).map(_.pos).toSet 48 | 49 | val ans1 = visited.size 50 | 51 | def isLoop(path: Iterator[Guard]): Boolean = 52 | // use tortoise hare 53 | val (tPath, hPath) = path.duplicate 54 | hPath.next 55 | 56 | // functional approach 57 | while hPath.hasNext do 58 | val t = tPath.next 59 | val h = hPath.next 60 | if t == h then return true 61 | if hPath.hasNext then 62 | hPath.next 63 | false 64 | 65 | def linePath(guard: Guard, obs: Set[Point]): Iterator[Guard] = 66 | val obstaclesVert: Map[Int, Set[Point]] = obs.groupBy(_.x).withDefaultValue(Set.empty) 67 | val obstaclesHorz: Map[Int, Set[Point]] = obs.groupBy(_.y).withDefaultValue(Set.empty) 68 | 69 | Iterator.unfold(guard): guard => 70 | val next = walk(guard, obstaclesVert, obstaclesHorz) 71 | next.map(g => (g, g)) 72 | 73 | val ans2 = (visited - initGuard.pos) 74 | .map(p => linePath(initGuard, obstacles + p)) 75 | .filter(isLoop) 76 | .size 77 | -------------------------------------------------------------------------------- /src/main/scala/2023/Day16.worksheet.sc: -------------------------------------------------------------------------------- 1 | import aoc.* 2 | 3 | val input = io.Source.fromResource("2023/day-16.txt").getLines.toVector 4 | 5 | val area = Area(input) 6 | 7 | val mirrors: Map[Point, Char] = Map.from: 8 | area.pointsIterator.filter(input(_) != '.').map: 9 | p => p -> input(p) 10 | 11 | val seen = collection.mutable.Set.empty[(Point, Dir)] 12 | 13 | def path(from: Point, dir: Dir): Set[Point] = 14 | if seen((from, dir)) then Set.empty 15 | else 16 | seen += ((from, dir)) 17 | val (energized, continue) = Iterator.iterate(from)(_.move(dir)) 18 | .drop(1) 19 | .takeWhile(area.contains) 20 | .span(!mirrors.contains(_)) 21 | val energizedSet = energized.toSet 22 | println(s"energized $energizedSet") 23 | continue.nextOption.fold(energizedSet): p => 24 | println(s"moving $dir into $p") 25 | (mirrors(p), dir) match 26 | case ('/', Dir.E | Dir.W) => 27 | energizedSet union path(p, dir.turnLeft) + p 28 | case ('\\', Dir.E | Dir.W) => 29 | energizedSet union path(p, dir.turnRight) + p 30 | case ('/', Dir.N | Dir.S) => 31 | energizedSet union path(p, dir.turnRight) + p 32 | case ('\\', Dir.N | Dir.S) => 33 | energizedSet union path(p, dir.turnLeft) + p 34 | case ('|', Dir.N | Dir.S) => 35 | energizedSet union path(p, dir) + p 36 | case ('-', Dir.N | Dir.S) => 37 | energizedSet union path(p, dir.turnRight) union path(p, dir.turnLeft) + p 38 | case ('-', Dir.E | Dir.W) => 39 | energizedSet union path(p, dir) + p 40 | case ('|', Dir.E | Dir.W) => 41 | energizedSet union path(p, dir.turnRight) union path(p, dir.turnLeft) + p 42 | case _ => ??? 43 | 44 | val last = path(Point.origin.w, Dir.E) 45 | val ans1 = last.count(area.contains) 46 | 47 | val l = area.leftBorder.points.map: p => 48 | seen.clear() 49 | path(p.w, Dir.E).size 50 | 51 | val r = area.rightBorder.points.map: p => 52 | seen.clear() 53 | path(p.e, Dir.W).size 54 | 55 | val t = area.topBorder.points.map: p => 56 | seen.clear() 57 | path(p.n, Dir.S).size 58 | 59 | val b = area.botBorder.points.map: p => 60 | seen.clear() 61 | path(p.s, Dir.N).size 62 | 63 | val ans2 = List(l.max, r.max, t.max, b.max).max 64 | 65 | println: 66 | area.draw: 67 | case p if last(p) => '#' 68 | case p if mirrors.contains(p) => mirrors(p) 69 | case _ => '.' 70 | 71 | // val ans1 = path(Point.origin.w, Dir.E).size 72 | 73 | 74 | // 75 | -------------------------------------------------------------------------------- /src/main/scala/2020/Day12.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2020/day-12.txt").getLines.toList 2 | 3 | case class Point(x: Int, y: Int) 4 | 5 | enum Dir: 6 | case N, E, S, W 7 | def rotate(degrees: Int): Dir = 8 | util.Try(Dir.fromOrdinal((this.ordinal + ((degrees + 360) / 90)) % 4)) 9 | .getOrElse(throw Exception(s"couldn't handle $degrees")) 10 | 11 | case class State(pos: Point, dir: Dir): 12 | def move(action: String): State = action match 13 | case s"N$d" => copy(pos.copy(y = pos.y + d.toInt)) 14 | case s"S$d" => copy(pos.copy(y = pos.y - d.toInt)) 15 | case s"E$d" => copy(pos.copy(x = pos.x + d.toInt)) 16 | case s"W$d" => copy(pos.copy(x = pos.x - d.toInt)) 17 | case s"R$d" => copy(dir = dir.rotate(d.toInt)) 18 | case s"L$d" => copy(dir = dir.rotate(- d.toInt)) 19 | case s"F$d" => move(s"$dir$d") 20 | 21 | val start = State(Point(0, 0), Dir.E) 22 | 23 | val last = input.foldLeft(start)(_ move _) 24 | 25 | val ans = last.pos.x.abs + last.pos.y.abs 26 | 27 | case class State2(pos: Point, dir: Dir, wayPoint: Point): 28 | def rotate90: State2 = wayPoint match 29 | case Point(x, y) => copy(wayPoint = Point(x = y, y = -x)) 30 | 31 | def move(action: String): State2 = action match 32 | case s"N$d" => copy(wayPoint = wayPoint.copy(y = wayPoint.y + d.toInt)) 33 | case s"S$d" => copy(wayPoint = wayPoint.copy(y = wayPoint.y - d.toInt)) 34 | case s"E$d" => copy(wayPoint = wayPoint.copy(x = wayPoint.x + d.toInt)) 35 | case s"W$d" => copy(wayPoint = wayPoint.copy(x = wayPoint.x - d.toInt)) 36 | case s"R$d" => 37 | val quarterTurns = d.toInt / 90 % 4 38 | Iterator.iterate(this)(_.rotate90).drop(quarterTurns).next() 39 | case s"L$d" => 40 | val quarterTurns = (360 - d.toInt) / 90 % 4 41 | Iterator.iterate(this)(_.rotate90).drop(quarterTurns).next() 42 | case s"F$d" => 43 | val times = d.toInt 44 | copy(pos = pos.copy( 45 | x = pos.x + wayPoint.x * times, 46 | y = pos.y + wayPoint.y * times) 47 | ) 48 | 49 | val start2 = State2(pos = Point(0, 0), dir = Dir.E, wayPoint = Point(10, 1)) 50 | 51 | val last2 = input.foldLeft(start2)(_ move _) 52 | 53 | val ans2 = last2.pos.x.abs + last2.pos.y.abs 54 | 55 | start2 56 | .move("F10") 57 | 58 | start2 59 | .move("F10") 60 | .move("N3") 61 | .move("F7") 62 | 63 | start2.rotate90 64 | start2.rotate90.rotate90 65 | start2.rotate90.rotate90.rotate90 66 | start2.rotate90.rotate90.rotate90.rotate90 67 | 68 | // LazyList.iterate(start)() 69 | -------------------------------------------------------------------------------- /src/main/scala/2023/Day23.worksheet.sc: -------------------------------------------------------------------------------- 1 | import aoc.* 2 | 3 | import collection.immutable.BitSet 4 | 5 | val maze = io.Source.fromResource("2023/day-23.txt").getLines.toVector 6 | 7 | val area = Area(maze) 8 | 9 | val slopes = Map.from[Point, Dir]: 10 | area.pointsIterator.collect: 11 | case p if maze(p) == '>' => p -> Dir.E 12 | case p if maze(p) == 'v' => p -> Dir.S 13 | case p if maze(p) == '<' => p -> Dir.W 14 | case p if maze(p) == '^' => p -> Dir.N 15 | 16 | val walkable = Set.from[Point]: 17 | area.pointsIterator.filter(p => maze(p) != '#') 18 | 19 | val start = walkable.minBy(_.y) 20 | val end = walkable.maxBy(_.y) 21 | 22 | val junctions: Set[Point] = walkable.filter: p => 23 | p.adjacent.count(walkable) > 2 24 | .toSet + start + end 25 | 26 | def connectedJunctions(pos: Point) = List.from[(Point, Int)]: 27 | def walk(pos: Point, dir: Dir): Option[Point] = 28 | val p = pos.move(dir) 29 | Option.when(walkable(p) && slopes.get(p).forall(_ == dir))(p) 30 | 31 | def search(pos: Point, facing: Dir, dist: Int): Option[(Point, Int)] = 32 | if junctions.contains(pos) then Some(pos, dist) else 33 | val next = for 34 | nextFacing <- LazyList(facing, facing.turnRight, facing.turnLeft) 35 | nextPos <- walk(pos, nextFacing) 36 | yield search(nextPos, nextFacing, dist + 1) 37 | 38 | next.headOption.flatten 39 | 40 | for 41 | d <- Dir.values 42 | p <- walk(pos, d) 43 | junction <- search(p, d, 1) 44 | yield junction 45 | 46 | def longestDownhillHike(pos: Point, dist: Int): Int = 47 | if pos == end then dist else 48 | connectedJunctions(pos).foldLeft(0): 49 | case (max, (n, d)) => max.max(longestDownhillHike(n, dist + d)) 50 | 51 | val ans1 = longestDownhillHike(start, 0) 52 | 53 | type Index = Int 54 | val indexOf: Map[Point, Index] = 55 | junctions.toList.sortBy(_.dist(start)).zipWithIndex.toMap 56 | 57 | val fullAdj: Map[Index, List[(Index, Int)]] = 58 | junctions.toList.flatMap: p1 => 59 | connectedJunctions(p1).flatMap: (p2, d) => 60 | val forward = indexOf(p1) -> (indexOf(p2), d) 61 | val reverse = indexOf(p2) -> (indexOf(p1), d) 62 | List(forward, reverse) 63 | .groupMap(_._1)(_._2) 64 | 65 | def longestHike(junction: Index, visited: BitSet, totalDist: Int): Int = 66 | if junction == indexOf(end) then totalDist else 67 | fullAdj(junction).foldLeft(0): 68 | case (max, (j, _)) if visited(j) => max 69 | case (max, (j, d)) => max.max(longestHike(j, visited + j, totalDist + d)) 70 | 71 | val ans2 = longestHike(indexOf(start), BitSet.empty, 0) 72 | -------------------------------------------------------------------------------- /src/main/scala/synacor/README.md: -------------------------------------------------------------------------------- 1 | # Synacor Challenge 2 | 3 | This directory contains my work for completing the [Synacor Challenge](http://github.com/stewSquared/synacor-challenge) as a [live coding stream](https://www.youtube.com/playlist?list=PLnP_dObOt-rVv2-4VHCeLHy6JKph18abo). 4 | 5 | Synacor is a multi-layered VM reverse-engineering puzzle created by Eric Wastl for OSCON in 2012, years before he released Advent of Code. We're given a VM specification and binary file and left to explore and unlock all the puzzles contained inside. This will likely involve emulation, interpreters, reverse engineering, and who knows what else. 6 | 7 | I'm sharing my problem solving and design approach live on stream! If you'd like to see my thought process and bounce ideas with me, you can catch me on [Twitch](https://twitch.tv/stewSquared) and [YouTube](https://youtube.com/@stewSquared) just before midnight on weekdays. 8 | 9 | ## Stream Notes 10 | 11 | ### 2025-12-14 -- Part 1 -- Introduction and Modeling 12 | 13 | - laid the **framework** for our VM emulator, following the approach from my Intcode solutions for Advent of Code 2019. 14 | 15 | - Modeled opcodes as an enum. We can figure out parsing later when we clean up the number system. 16 | 17 | - Modeled state as an immutable case class with a program counter, registers, stack, and memory. These are all modeled with placeholder types aliases for now, but we want to keep implementation details away from our model code. Added helpers for state transitions that Opcodes effect. 18 | 19 | - Modeled the unsigned 16-bit numbers as a case class containing the low and high bytes. We probably just want an opaque type over Int with bitmasking, because translating things to little endian is cumbersome and error-prone. 20 | 21 | - Implemented opcodes while slowly growing a DSL. We still need to introduce typesafety for words, proper memory and register handling, and Opcodes related to the call stack. 22 | 23 | ### 2025-12-15 -- Part 2 -- Getting a Runnable VM Emulator 24 | 25 | - Added typesafe number system for handling words as literals, addresses, or register references. Backed by Int with opaque types. 26 | - Added proper memory model 27 | - Added proper register bank model 28 | - Implemented all Opcodes except IN 29 | - Loaded binary memory and ran a welcome message 30 | 31 | ## Wishlist 32 | 33 | - [ ] Implement IN Opcode 34 | - [ ] Interface for entering input into running VM 35 | - [ ] Interact with power-on self test! 36 | - [ ] type params for valid state transitions? 37 | - [ ] use a state monad and transformer? 38 | - [ ] nice error types? 39 | -------------------------------------------------------------------------------- /src/main/scala/2021/Day10.worksheet.sc: -------------------------------------------------------------------------------- 1 | import io.Source 2 | import util.Using 3 | import util.{Either, Right, Left} 4 | 5 | object Token: 6 | opaque type Open = Char 7 | opaque type Close = Char 8 | type Chunk = Either[Close, List[Close]] 9 | 10 | val closeOf: Map[Open, Close] = 11 | Map('(' -> ')', '[' -> ']', '{' -> '}', '<' -> '>') 12 | 13 | object Open: 14 | def unapply(c: Char): Option[Open] = closeOf.keys.find(_ == c) 15 | 16 | object Close: 17 | def unapply(c: Char): Option[Close] = closeOf.values.find(_ == c) 18 | 19 | val errorScore: Map[Close, Int] = 20 | Map(')' -> 3, ']' -> 57, '}' -> 1197, '>' -> 25137) 21 | 22 | def completionScore(completion: List[Close]): Long = 23 | completion.foldLeft(0L) { (s, c) => 24 | s.toLong * 5 + " )]}>".indexOf(c) 25 | } 26 | 27 | import Token.* 28 | 29 | val chunks: List[Chunk] = Using(Source.fromResource("2021/day-10-1.txt")) { 30 | _.getLines.toList.map(line => parse(line.toList, Nil)) 31 | }.get 32 | 33 | def parse(tokens: List[Char], completion: List[Close]): Chunk = tokens match 34 | case Nil => Right(completion) 35 | case Open(open) :: ts => parse(ts, closeOf(open) :: completion) 36 | case Close(close) :: ts => 37 | completion match 38 | case `close` :: unmatched => parse(ts, unmatched) 39 | case _ => Left(close) 40 | case invalid :: ts => parse(ts, completion) 41 | 42 | val ans1 = chunks.flatMap(_.swap.toOption).map(errorScore).sum 43 | 44 | val completions = chunks.flatMap(_.toOption) 45 | val ans2 = completions.map(completionScore).sorted.apply(completions.size / 2) 46 | 47 | //////////////// test //////////////// 48 | 49 | def parseLine(line: String) = parse(line.toList, Nil) 50 | 51 | val corrupted = chunks.filter(_.isLeft) 52 | corrupted foreach println 53 | 54 | def error(chunk: String) = parseLine(chunk).swap.toOption.get 55 | 56 | println(error("{([(<{}[<>[]}>{[]{[(<()>")) 57 | println(error("[[<[([]))<([[{}[[()]]]")) 58 | println(error("[{[{({}]{}}([{[{{{}}([]")) 59 | println(error("[<(<(<(<{}))><([]([]()")) 60 | println(error("<{([([[(<>()){}]>(<<{{")) 61 | 62 | val incomplete = chunks.filter(_.isRight) 63 | incomplete foreach println 64 | 65 | def completion(chunk: String) = parseLine(chunk).toOption.get 66 | 67 | println(completion("[({(<(())[]>[[{[]{<()<>>").mkString) 68 | println(completion("[(()[<>])]({[<{<<[]>>(").mkString) 69 | println(completion("(((({<>}<{<{<>}{[]{[]{}").mkString) 70 | println(completion("{<[[]]>}<{[{[{[]{()[[[]").mkString) 71 | println(completion("<{([{{}}[<[[[<>{}]]]>[]]").mkString) 72 | 73 | incomplete.flatMap(_.toSeq).map(completionScore) 74 | -------------------------------------------------------------------------------- /src/main/scala/2022/Day21.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2022/day-21.txt").getLines().toVector 2 | 3 | val yells: Map[String, String] = input.map { 4 | case s"$name: $op" => name -> op 5 | }.toMap 6 | 7 | def eval(name: String): Long = yells(name) match 8 | case s"$a + $b" => eval(a) + eval(b) 9 | case s"$a - $b" => eval(a) - eval(b) 10 | case s"$a * $b" => eval(a) * eval(b) 11 | case s"$a / $b" => eval(a) / eval(b) 12 | case n => n.toLong 13 | 14 | val ans1 = eval("root") 15 | 16 | def parse(name: String): Exp = 17 | if name == "humn" then Var else yells(name) match 18 | case s"$a + $b" => Add(parse(a), parse(b)) 19 | case s"$a - $b" => Sub(parse(a), parse(b)) 20 | case s"$a * $b" => Mul(parse(a), parse(b)) 21 | case s"$a / $b" => Div(parse(a), parse(b)) 22 | case n => Val(n.toLong) 23 | 24 | sealed trait Exp: 25 | override def toString: String = this match 26 | case Add(a, b) => s"($a + $b)" 27 | case Sub(a, b) => s"($a - $b)" 28 | case Mul(a, b) => s"($a * $b)" 29 | case Div(a, b) => s"($a / $b)" 30 | case Val(n) => n.toString 31 | case Var => "???" 32 | 33 | def hasVar: Boolean = this match 34 | case Add(a, b) => a.hasVar || b.hasVar 35 | case Sub(a, b) => a.hasVar || b.hasVar 36 | case Mul(a, b) => a.hasVar || b.hasVar 37 | case Div(a, b) => a.hasVar || b.hasVar 38 | case Val(n) => false 39 | case Var => true 40 | 41 | def eval: Long = this match 42 | case Add(a, b) => a.eval + b.eval 43 | case Sub(a, b) => a.eval - b.eval 44 | case Mul(a, b) => a.eval * b.eval 45 | case Div(a, b) => a.eval / b.eval 46 | case Val(n) => n 47 | case Var => ??? 48 | 49 | case class Add(a: Exp, b: Exp) extends Exp 50 | case class Sub(a: Exp, b: Exp) extends Exp 51 | case class Mul(a: Exp, b: Exp) extends Exp 52 | case class Div(a: Exp, b: Exp) extends Exp 53 | case class Val(n: Long) extends Exp 54 | case object Var extends Exp 55 | 56 | case class Eqn(lhs: Exp, rhs: Exp): 57 | def solve = Iterator.iterate(this) { case Eqn(lhs, rhs) => 58 | (lhs: @unchecked) match 59 | case Add(a, b) if a.hasVar => Eqn(a, Sub(rhs, b)) 60 | case Sub(a, b) if a.hasVar => Eqn(a, Add(rhs, b)) 61 | case Mul(a, b) if a.hasVar => Eqn(a, Div(rhs, b)) 62 | case Div(a, b) if a.hasVar => Eqn(a, Mul(rhs, b)) 63 | case Add(a, b) if b.hasVar => Eqn(b, Sub(rhs, a)) 64 | case Sub(a, b) if b.hasVar => Eqn(b, Sub(a, rhs)) 65 | case Mul(a, b) if b.hasVar => Eqn(b, Div(rhs, a)) 66 | case Div(a, b) if b.hasVar => Eqn(b, Div(a, rhs)) 67 | case Var => Eqn(lhs, rhs) 68 | case _ if rhs.hasVar => Eqn(rhs, lhs) 69 | } collectFirst { case Eqn(Var, rhs) => rhs.eval } 70 | 71 | val s"$left $_ $right" = yells("root"): @unchecked 72 | 73 | val ans2 = Eqn(parse(left), parse(right)).solve.get 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Advent of Code Solutions 2 | 3 | My Solutions to [Advent of Code](https://adventofcode.com), written in Scala 3 4 | 5 | ## Links 6 | - 📊 I have a [leaderboard](https://adventofcode.com/2025/leaderboard/private/view/1567548)! Code to join is `1567548-37bb9c26` 7 | - 🔴 I live stream solutions on [Twitch](https://twitch.tv/stewSquared) and [YouTube](https://youtube.com/@stewSquared). 8 | - 💬 Comments and questions? Come chat in [Discord](https://discord.gg/SgZemzbHPa). 9 | 10 | ## Advent of Code 2025 11 | 12 | [Solutions and Breakdown](src/main/scala/2025/) 13 | 14 | [Live Recordings Playlist](https://www.youtube.com/playlist?list=PLnP_dObOt-rWB2QisPZ67anfI7CZx3Vsq) 15 | 16 | Sources: 17 | | | | | | | 18 | |---|---|---|---|---| 19 | |[01](src/main/scala/2025/Day01.worksheet.sc)|[02](src/main/scala/2025/Day02.worksheet.sc)|[03](src/main/scala/2025/Day03.worksheet.sc)|[04](src/main/scala/2025/Day04.worksheet.sc)|[05](src/main/scala/2025/Day05.worksheet.sc)| 20 | |[06](src/main/scala/2025/Day06.worksheet.sc)|[07](src/main/scala/2025/Day07.worksheet.sc)|[08](src/main/scala/2025/Day08.worksheet.sc)|[09](src/main/scala/2025/Day09.worksheet.sc)|[10](src/main/scala/2025/Day10.worksheet.sc)| 21 | |[11](src/main/scala/2025/Day11.worksheet.sc)|[12](src/main/scala/2025/Day12.worksheet.sc)|[13](src/main/scala/2025/Day13.worksheet.sc)|[14](src/main/scala/2025/Day14.worksheet.sc)|[15](src/main/scala/2025/Day15.worksheet.sc)| 22 | |[16](src/main/scala/2025/Day16.worksheet.sc)|[17](src/main/scala/2025/Day17.worksheet.sc)|[18](src/main/scala/2025/Day18.worksheet.sc)|[19](src/main/scala/2025/Day19.worksheet.sc)|[20](src/main/scala/2025/Day20.worksheet.sc)| 23 | |[21](src/main/scala/2025/Day21.worksheet.sc)|[22](src/main/scala/2025/Day22.worksheet.sc)|[23](src/main/scala/2025/Day23.worksheet.sc)|[24](src/main/scala/2025/Day24.worksheet.sc)|[25](src/main/scala/2025/Day25.worksheet.sc)| 24 | 25 | ## Usage 26 | 27 | Solutions are in Scala 3 worksheet files. To run these, files need to be opened in an editor running [Metals](https://scalameta.org/metals/). Intellij *might* also work, but I haven't used its worksheets. 28 | 29 | Input files are not distributed, as per the AoC creator's request. A [script](input.sh) is included that will save your problem input to the appropriate path. To use it, you will need your advent of code session key: 30 | 31 | ```bash 32 | AOC_SESSION= 33 | source ./input.sh 34 | aoc-input 35 | ``` 36 | 37 | ## Videos 38 | 39 | Starting 2023 these solutions are live streamed on [YouTube](https://youtube.com/@stewSquared) and [Twitch](https://twitch.tv/stewSquared). VODs will be available long-term on YouTube. Check out [this playlist](https://www.youtube.com/playlist?list=PLnP_dObOt-rWB2QisPZ67anfI7CZx3Vsq) for 2025 videos. If you found code in this repository interesting, consider following me on either of these platforms. 40 | -------------------------------------------------------------------------------- /src/main/scala/2023/Day17Part2.worksheet.sc: -------------------------------------------------------------------------------- 1 | import aoc.* 2 | 3 | val grid = io.Source.fromResource("2023/day-17.txt").getLines.toVector 4 | 5 | val area = Area(grid) 6 | 7 | def viable2(dir: Dir, straight: Int): List[Dir] = 8 | if straight < 10 then 9 | List(dir, dir.turnLeft, dir.turnRight) 10 | else 11 | List(dir.turnLeft, dir.turnRight) 12 | 13 | case class State(pos: Point, dir: Dir, straight: Int, heat: Int): 14 | def nextStates2: List[State] = 15 | viable2(dir, straight).filter: newDir => 16 | if newDir == dir then area.contains(pos.move(newDir)) 17 | else area.contains(pos.move(newDir, 4)) 18 | .map: newDir => 19 | if newDir != dir then 20 | val newPos = pos.move(newDir, 4) 21 | val newStraight = 4 22 | val newHeat = Line(pos.move(newDir), newPos).points.map(grid(_).asDigit).sum + heat 23 | State(newPos, newDir, newStraight, newHeat) 24 | else 25 | val newPos = pos.move(newDir) 26 | val newStraight = straight + 1 27 | State(newPos, newDir, newStraight, heat + grid(newPos).asDigit) 28 | 29 | def search2(start: Point, end: Point): Int = 30 | import collection.mutable.{PriorityQueue, Set, Map} 31 | 32 | // given Ordering[State] = Ordering.by(s => s.pos.dist(end) + s.heat) 33 | given Ordering[State] = Ordering.by(s => s.pos.dist(end) + s.heat) 34 | 35 | val movedEast = State(start.move(Dir.E, 4), Dir.E, 4, Line(start.e, start.move(Dir.E, 4)).points.map(grid(_).asDigit).sum) 36 | val movedSouth = State(start.move(Dir.S, 4), Dir.S, 4, Line(start.s, start.move(Dir.S, 4)).points.map(grid(_).asDigit).sum) 37 | var visiting = List(movedEast, movedSouth).minBy(_.heat) 38 | val other = List(movedEast, movedSouth).maxBy(_.heat) 39 | 40 | val pq = PriorityQueue.empty[State].reverse 41 | val visited = Set(visiting) 42 | pq.enqueue(other) 43 | val bestHeat = Map.empty[Point, Int] 44 | bestHeat(visiting.pos) = visiting.heat 45 | bestHeat(other.pos) = other.heat 46 | 47 | while visiting.pos != end do 48 | println(s"visiting: $visiting") 49 | val states = visiting.nextStates2 50 | .filterNot: s => 51 | (4 to s.straight).exists: straight => 52 | visited(s.copy(straight = straight)) 53 | .filter: s => 54 | bestHeat.get(s.pos).forall(s.heat - 20 < _) 55 | 56 | // states.foreach: s => 57 | // println(s"enqueuing $s") 58 | 59 | states.foreach: s => 60 | bestHeat(s.pos) = bestHeat.getOrElse(s.pos, Int.MaxValue).min(s.heat) 61 | pq.enqueue(states*) 62 | visited ++= states 63 | visiting = pq.dequeue() 64 | 65 | visiting.heat 66 | 67 | 2 + 2 68 | // val ans1 = search(Point.origin, area.botRight) 69 | val ans2 = search2(Point.origin, area.botRight) 70 | // 1137 too high 71 | 72 | // State(Point.origin.move(Dir.E, 4), Dir.E, 4, Line(Point.origin.e, Point.origin.move(Dir.E, 4)).points.map(grid(_).asDigit).sum) 73 | 74 | 75 | 76 | // 77 | -------------------------------------------------------------------------------- /src/main/scala/2024/Day20.scala: -------------------------------------------------------------------------------- 1 | package y2024 2 | 3 | import aoc.{Area, Point, Dir} 4 | 5 | @main def d20: Unit = 6 | 7 | val grid = io.Source.fromResource("2024/day-20.txt").getLines.toVector 8 | val area = Area(grid) 9 | val startPos = area.pointsIterator.find(grid(_) == 'S').get 10 | val endPos = area.pointsIterator.find(grid(_) == 'E').get 11 | val walls = area.pointsIterator.filter(grid(_) == '#').toSet 12 | 13 | enum Cheat: 14 | case CanCheat 15 | case Cheating(time: Int, pos: Point) 16 | case Cheated(time: Int, pos: Point) 17 | 18 | def next(time: Int, pos: Point): Cheat = this match 19 | case CanCheat => Cheating(time, pos) 20 | case Cheating(time, pos) => Cheated(time, pos) 21 | case Cheated(_, _) => ??? 22 | 23 | case class State(pos: Point, cheat: Cheat): 24 | def nonCheatingNext: Set[State] = 25 | pos.adjacent.diff(walls).filter(area.contains).map(State(_, cheat)) 26 | 27 | def cheatingNext(time: Int): Set[State] = 28 | pos.adjacent.filter(area.contains).map: newPos => 29 | State(newPos, cheat.next(time + 1, newPos)) 30 | 31 | def next(time: Int): Set[State] = cheat match 32 | case Cheat.CanCheat => nonCheatingNext union cheatingNext(time) 33 | case Cheat.Cheating(_, _) => nonCheatingNext 34 | case Cheat.Cheated(_, _) => nonCheatingNext 35 | 36 | def search: Int = 37 | import collection.mutable.{PriorityQueue, Map} 38 | 39 | val start = State(startPos, Cheat.Cheated(0, startPos)) 40 | val cost = Map[State, Int](start -> 0) 41 | val queue = PriorityQueue.empty[State](Ordering.by(cost)).reverse 42 | 43 | var visiting = start 44 | 45 | while visiting.pos != endPos do 46 | visiting.next(cost(visiting)).diff(cost.keySet).foreach: s => 47 | cost(s) = cost(visiting) + 1 48 | queue.enqueue(s) 49 | visiting = queue.dequeue 50 | 51 | cost(visiting) 52 | 53 | def findCheats(benchmark: Int): Int = 54 | import collection.mutable.{PriorityQueue, Map} 55 | 56 | val start = State(startPos, Cheat.CanCheat) 57 | val cost = Map[State, Int](start -> 0) 58 | val queue = PriorityQueue.empty[State](Ordering.by(cost)).reverse 59 | 60 | var visiting = start 61 | // val cheats = collection.mutable.Set.empty[(State, Int)] 62 | var cheatCount = 0 63 | 64 | while cost(visiting) <= benchmark do 65 | if visiting.pos == endPos then 66 | // println(cost(visiting) -> visiting) 67 | // cheats += (visiting -> cost(visiting)) 68 | cheatCount += 1 69 | else 70 | visiting.next(cost(visiting)).diff(cost.keySet).foreach: s => 71 | cost(s) = cost(visiting) + 1 72 | queue.enqueue(s) 73 | visiting = queue.dequeue 74 | 75 | // cheats.toSet 76 | cheatCount 77 | 78 | val noCheatCost = search 79 | 80 | println(noCheatCost) 81 | 82 | val cheats = findCheats(noCheatCost - 100) 83 | 84 | val ans1 = cheats 85 | 86 | println(ans1) 87 | -------------------------------------------------------------------------------- /src/main/scala/2022/Day17.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2022/day-17.txt").getLines().next() 2 | 3 | case class Point(x: Long, y: Long): 4 | def l = copy(x = x - 1) 5 | def r = copy(x = x + 1) 6 | def d = copy(y = y - 1) 7 | 8 | enum Shape(points: List[Point]): 9 | case Horz extends Shape((2 to 5).map(Point(_, 4)).toList) 10 | case Cross extends Shape(List((3, 4), (2, 5), (3, 5), (4, 5), (3, 6)).map(Point(_, _))) 11 | case Angle extends Shape(List((2, 4), (3, 4), (4, 4), (4, 5), (4, 6)).map(Point(_, _))) 12 | case Vert extends Shape((4 to 7).map(Point(2, _)).toList) 13 | case Box extends Shape(List((2, 4), (3, 4), (2, 5), (3, 5)).map(Point(_, _))) 14 | 15 | def start(max: Long) = this.points.map(p => p.copy(y = p.y + max)) 16 | 17 | def jetAt(index: Int): Char = input(index % input.length) 18 | 19 | case class State(rocks: Set[Point], jets: Int, height: Long): 20 | def drop(shape: Shape): State = 21 | val start = shape.start(height) 22 | val positions = Iterator.iterate((start, jets, true)) { 23 | case (shape, i, false) => (shape, i, false) 24 | case (shape, i, falling) => 25 | val pushed = 26 | val dir = if jetAt(i) == '<' then shape.map(_.l) else shape.map(_.r) 27 | val valid = 28 | dir.forall(p => (0 until 7).contains(p.x) && !rocks(p)) 29 | if valid then dir else shape 30 | val down = pushed.map(_.d) 31 | val stopped = down.exists(rocks) 32 | val next = if stopped then pushed else down 33 | (next, i + 1, !stopped) 34 | } 35 | val (resting, i) = positions.dropWhile(_._3).next().take(2) 36 | State(rocks.concat(resting), i, height.max(resting.maxBy(_.y).y)) 37 | 38 | def states = 39 | val floor = (0 until 7).map(x => Point(x, 0)).toSet 40 | val shapes = LazyList.continually(Shape.values).flatten 41 | shapes.scanLeft(State(floor, 0, 0))(_ drop _) 42 | 43 | val heightAfter = states.map(_.height) 44 | 45 | def repeated(seq: Vector[Int]): (Int, Int) = 46 | def search(i: Int): (Int, Int) = 47 | val hook = seq.slice(i, i + 50) 48 | val matchIndex = seq.indexOfSlice(hook) 49 | val repeating = seq.slice(matchIndex, i) 50 | if matchIndex < i && seq.drop(matchIndex).startsWith(repeating) then 51 | matchIndex -> repeating.size 52 | else search(i + 1) 53 | 54 | search(0) 55 | 56 | val (cycleStart, period) = 57 | val jetsUsed = states.map(_.jets) 58 | val deltas = jetsUsed.tail.zip(jetsUsed).map(_ - _) 59 | repeated(deltas.take(3000).toVector) 60 | 61 | val tril = 1_000_000_000_000L 62 | val extra = ((tril - cycleStart) % period).toInt 63 | val heightWithExtra = heightAfter(cycleStart + extra) 64 | 65 | val numCycles = (tril - cycleStart) / period 66 | 67 | val heightBeforeCycle = heightAfter(cycleStart) 68 | val heightAfterFirst = heightAfter(cycleStart + period) 69 | val cycleHeight = heightAfterFirst - heightBeforeCycle 70 | 71 | val ans1 = heightAfter(2022) 72 | val ans2 = heightWithExtra + (cycleHeight * numCycles) 73 | -------------------------------------------------------------------------------- /src/main/scala/2020/Day18.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2020/day-18.txt").getLines.toList 2 | 3 | enum Exp: 4 | case Num(n: Int) 5 | case Mul(a: Exp, b: Exp) 6 | case Add(a: Exp, b: Exp) 7 | 8 | def eval: Long = this match 9 | case Num(n) => n.toLong 10 | case Mul(a, b) => a.eval * b.eval 11 | case Add(a, b) => a.eval + b.eval 12 | 13 | import Exp.{Num, Mul, Add} 14 | 15 | enum Tree: 16 | case Tok(c: Char) 17 | case Paren(toks: List[Tree]) 18 | 19 | import Tree.{Tok, Paren} 20 | 21 | def splitMatching(raw: String): (String, String) = 22 | val nestingDepth = raw.scanLeft(0) { 23 | case (depth, '(') => depth + 1 24 | case (depth, ')') => depth - 1 25 | case (depth, c) => depth 26 | } 27 | val matchingIndex = nestingDepth.indexWhere(_ == 0, from = 1) 28 | val (inside, outside) = raw.splitAt(matchingIndex) 29 | inside.tail.init -> outside 30 | 31 | def tokenize(rawExp: String): List[Tree] = 32 | rawExp match 33 | case flat if !flat.contains("(") => 34 | flat.replaceAll(" ", "").map(Tok.apply).toList 35 | case nested => 36 | val (leading, toSplit) = nested.splitAt(nested.indexOf("(")) 37 | val (paren, trailing) = splitMatching(toSplit) 38 | tokenize(leading) ::: Paren(tokenize(paren)) :: tokenize(trailing) 39 | 40 | def parse(tokens: List[Tree]): Exp = 41 | def parseTerm(tok: Tree): Exp = tok match 42 | case Paren(toks) => parse(toks) 43 | case Tok(c) if c.isDigit => Num(c.asDigit) 44 | case Tok(c) => throw Exception(s"expected digit, found: $c") 45 | 46 | val first = parseTerm(tokens.head) 47 | 48 | tokens.tail.grouped(2).foldLeft(first) { case (left, List(op, tok)) => 49 | val right = parseTerm(tok) 50 | op match 51 | case Tok('*') => Mul(left, right) 52 | case Tok('+') => Add(left, right) 53 | case Tok(c) => throw Exception(s"unexpected operator: $c") 54 | } 55 | 56 | def parse2(tokens: List[Tree]): Exp = 57 | def sum(addends: List[Tree]): Exp = 58 | addends 59 | .collect { 60 | case Tok(d) if d.isDigit => Num(d.asDigit) 61 | case Paren(toks) => parse2(toks) 62 | } 63 | .reduceLeft(Add.apply) 64 | 65 | Iterator 66 | .unfold(tokens) { remaining => 67 | Option.when(remaining.nonEmpty) { 68 | val (term, next) = remaining.span(_ != Tok('*')) 69 | sum(term) -> next.drop(1) 70 | } 71 | } 72 | .reduceLeft(Mul.apply) 73 | 74 | val ans1 = input.map(raw => parse(tokenize(raw)).eval).sum 75 | 76 | val ans2 = input.map(raw => parse2(tokenize(raw)).eval).sum 77 | 78 | tokenize("3 * (4 + (2 * 1) + 5) + 6") 79 | 80 | tokenize("3 + 4") 81 | parse(tokenize("3 + 4")) 82 | 83 | parse(tokenize("(3 + 4) * 5")) 84 | 85 | parse(tokenize("2 * (3 + 4)")) 86 | 87 | parse(tokenize("2 * (3 + 4)")).eval 88 | 89 | parse(tokenize("2 + 3 * 4")) 90 | 91 | parse(tokenize("2 + 3 * 4")).eval 92 | 93 | (tokenize andThen parse2)("((2 + 4 * 9) * (6 + 9 * 8 + 6) + 6) + 2 + 4 * 2").eval 94 | -------------------------------------------------------------------------------- /src/main/scala/2021/Day19.worksheet.sc: -------------------------------------------------------------------------------- 1 | import util.Using 2 | import io.Source 3 | import collection.mutable.ListBuffer 4 | 5 | val scanners = Using(Source.fromResource("2021/day-19-1.txt")) { source => 6 | val lines = source.getLines() 7 | val lb = new ListBuffer[Scanner] 8 | while lines.hasNext do 9 | val s"---$label---" = lines.next() 10 | val points = lines 11 | .takeWhile(_.nonEmpty) 12 | .collect { case s"$x,$y,$z" => 13 | Point(x.toInt, y.toInt, z.toInt) 14 | } 15 | .toList 16 | lb += Scanner(name = label.strip, beacons = points.toSet) 17 | lb.result 18 | }.get 19 | 20 | case class Point(x: Int, y: Int, z: Int): 21 | def rotX: Point = copy(y = z, z = -y) 22 | def rotY: Point = copy(z = x, x = -z) 23 | def rotZ: Point = copy(x = y, y = -x) 24 | def translate(p: Point): Point = 25 | copy(x = x + p.x, y = y + p.y, z = z + p.z) 26 | 27 | def -(p: Point): Point = 28 | Point(x - p.x, y - p.y, z - p.z) 29 | 30 | def manhattanDist(p: Point): Int = 31 | (x - p.x).abs + (y - p.y).abs + (z - p.z).abs 32 | 33 | case class Scanner( 34 | name: String, 35 | beacons: Set[Point], 36 | origin: Point = Point(0, 0, 0) 37 | ): 38 | def rotX = copy(beacons = beacons.map(_.rotX), origin = origin.rotX) 39 | def rotY = copy(beacons = beacons.map(_.rotY), origin = origin.rotY) 40 | def rotZ = copy(beacons = beacons.map(_.rotZ), origin = origin.rotZ) 41 | 42 | def translate(t: Point) = 43 | copy(beacons = beacons.map(_.translate(t)), origin = origin.translate(t)) 44 | 45 | def intersections(that: Scanner): Set[Point] = 46 | this.beacons intersect that.beacons 47 | 48 | def rotations: Iterator[Scanner] = 49 | for 50 | p <- List(this, rotX, rotX.rotX, rotX.rotX.rotX).iterator 51 | q <- List(p, p.rotZ.rotZ) 52 | r <- List(q, q.rotY, q.rotZ) 53 | yield r 54 | 55 | def locate(that: Scanner): Option[Scanner] = 56 | val translations = for 57 | s2r <- that.rotations 58 | b1 <- beacons 59 | b2 <- s2r.beacons 60 | yield s2r.translate(b1 - b2) 61 | translations.find(_.beacons.intersect(beacons).sizeIs >= 12) 62 | 63 | val distances = scanners.map { s => 64 | s.name -> s.beacons.toList 65 | .combinations(2) 66 | .collect { case List(p, q) => p.manhattanDist(q) } 67 | .toSet 68 | }.toMap 69 | 70 | def overlap(s1: Scanner, s2: Scanner): Boolean = 71 | distances(s1.name).intersect(distances(s2.name)).sizeIs >= 12 * 11 / 2 72 | 73 | val oriented = 74 | val search = Iterator.iterate(scanners.splitAt(1)) { (from, toCheck) => 75 | toCheck.partitionMap { c => 76 | from.find(overlap(_, c)).flatMap(_.locate(c)).toLeft(c) 77 | } 78 | } 79 | search.map(_._1).takeWhile(_.nonEmpty).toList.flatten 80 | 81 | val beacons = oriented.map(_.beacons).reduce(_ union _) 82 | val ans1 = beacons.size 83 | 84 | val origins = oriented.map(_.origin) 85 | 86 | val ans2 = origins 87 | .combinations(2) 88 | .collect { case List(p, q) => p.manhattanDist(q) } 89 | .max 90 | -------------------------------------------------------------------------------- /src/main/scala/2023/Day05Part2.worksheet.sc: -------------------------------------------------------------------------------- 1 | import scala.collection.immutable.{Range => _} 2 | 3 | type Range = scala.collection.immutable.NumericRange[Long] 4 | 5 | val input = io.Source.fromResource("2023/day-05.txt").getLines() 6 | 7 | val seeds: List[Range] = 8 | val s"seeds: $seedsStr" = (input.next(): @unchecked) 9 | val nums = seedsStr.split(" ").map(_.toLong).toList 10 | nums.grouped(2).toList.map: 11 | case List(a, b) => a until (a + b) 12 | case _ => ??? 13 | 14 | seeds foreach println 15 | 16 | input.next() 17 | 18 | def extractRanges(): List[(Range, Range)] = 19 | val lines = input.takeWhile(_.nonEmpty) 20 | lines.map { 21 | case s"$a $b $c" => 22 | val dest = a.toLong 23 | val source = b.toLong 24 | val length = c.toLong 25 | (dest until dest + length, source until source + length) 26 | }.toList 27 | 28 | def supersetOf(l: Range, r: Range) = l.min <= r.min && r.max <= l.max 29 | 30 | def intersect(l: Range, r: Range): Option[Range] = 31 | if supersetOf(l, r) then Some(r) 32 | else if r contains l.min then Some(l.min to l.max.min(r.max)) 33 | else if r contains l.max then Some(l.min.max(r.min) to l.max) 34 | else None 35 | 36 | intersect(79L to 93L, 50L to 98L) 37 | intersect(79L to 93L, 150L to 198L) 38 | intersect(79L to 93L, -150L to 198L) 39 | intersect(79L to 93L, -150L to 85L) 40 | intersect(79L to 93L, 85L to 100L) 41 | 42 | def applyRange(c: Range, d: Range, s: Range): Range = 43 | val i = intersect(c, s).get 44 | val diff = d.min - s.min 45 | i.min + diff to i.max + diff 46 | 47 | applyRange(79L to 93L, 52L until 100L, 50L to 98L) 48 | 49 | def diff(m: Range, n: Range): List[Range] = 50 | if m.min < n.min && n.max < m.max then 51 | List(m.min to n.min - 1, n.max + 1 to m.max) 52 | else if n.min <= m.min && m.max <= n.max then Nil 53 | else if n contains m.max then List(m.min to n.min - 1) 54 | else if n contains m.min then List(n.max + 1 to m.max) 55 | else List(m) 56 | 57 | diff(1L to 10L, 8L to 12L) 58 | diff(1L to 10L, 3L to 5L) 59 | diff(1L to 10L, -5L to 5L) 60 | diff(1L to 10L, -5L to 15L) 61 | diff(1L to 10L, 15L to 25L) 62 | 63 | def next(current: List[Range], ranges: List[(Range, Range)]): List[Range] = 64 | current.flatMap: c => 65 | val overlaps = ranges.filter: (_, s) => 66 | s.contains(c.max) || s.contains(c.min) 67 | 68 | println(s"overlaps for $c: $overlaps") 69 | 70 | val overlapTranslations = overlaps.map(applyRange(c, _, _)) 71 | 72 | overlapTranslations.foreach: nc => 73 | print("- translations:") 74 | println(nc) 75 | 76 | val remaining = overlaps.map(_._2).foldLeft(List(c)): (ranges, s) => 77 | ranges.flatMap(diff(_, s)) 78 | 79 | overlapTranslations ++ remaining 80 | 81 | var current = seeds 82 | 83 | // input.next() 84 | // next(seeds, extractRanges()) 85 | 86 | val its = Iterator.iterate(seeds): current => 87 | println(current) 88 | println(input.next()) 89 | next(current, extractRanges()) 90 | 91 | val finalRanges = its.take(8).toList.last 92 | val ans2 = finalRanges.map(_.min).min 93 | -------------------------------------------------------------------------------- /src/main/scala/2023/README.md: -------------------------------------------------------------------------------- 1 | # Advent of Code 2023 2 | 3 | The solutions in this folder are all written during live streams on and cleaned up afterwards. 4 | 5 | Questions or comments about my solutions? Drop by my [Discord](https://discord.gg/SgZemzbHPa) to chat about it. 6 | 7 | Wanna see these in the making? Visit my channel on [Twitch](https://twitch.tv/stewSquared) or [YouTube](https://youtube.com/@stewSquared) just before the problems come out at midnight EST. I start a little early for review/refactor/warmup. 8 | 9 | A [playlist](https://www.youtube.com/playlist?list=PLnP_dObOt-rWnSqOUQDc5T_r9TMdaNxJl) of live recordings is available. Recordings typically include some warmup, refactor, recap, and development of library code. Directl links in the table below will link to the timestamp of the actual solution. 10 | 11 | As problems come out, I'll fill in this table of stats and links to recordings below. 12 | 13 | ## Solution Stats and Details 14 | 15 | | Day | P1 Time | P1 Rank | P2 Time | P2 Rank | LoC | Recording Links | Notes | 16 | | --: | -------: | ------: | -------: | ------: | --: | --------------- | ----- | 17 | | 01 | 00:04:41 | 1836 | 00:18:44 | 1557 | | [Part 1](https://www.youtube.com/watch?v=Jv9B1crzpWM) [Part 2](https://www.youtube.com/watch?v=Jv9B1crzpWM&t=4m38s) | | 18 | | 02 | 00:10:30 | 1405 | 00:13:18 | 1169 | | [Part 1]() [Part 2]() | | 19 | | 03 | 00:36:42 | 4078 | 00:45:47 | 3022 | | [Part 1]() [Part 2]() | | 20 | | 04 | 00:12:58 | 4625 | 00:43:37 | 6925 | | [Part 1]() [Part 2]() | | 21 | | 05 | 01:27:36 | 10669 | 02:24:39 | 5307 | | [Part 1]() [Part 2]() | | 22 | | 06 | 00:14:42 | 4901 | 00:32:47 | 7766 | | [Part 1]() [Part 2]() | | 23 | | 07 | 01:45:56 | 10791 | 02:13:01 | 9090 | | [Part 1]() [Part 2]() | | 24 | | 08 | 00:12:00 | 3081 | 01:53:13 | 8419 | | [Part 1]() [Part 2]() | | 25 | | 09 | 00:17:04 | 3181 | 00:18:17 | 2203 | | [Part 1]() [Part 2]() | | 26 | | 10 | 00:37:45 | 2084 | 02:26:46 | 3264 | | [Part 1]() [Part 2]() | | 27 | | 11 | 00:28:59 | 3656 | 00:31:42 | 2352 | | [Part 1]() [Part 2]() | | 28 | | 12 | 00:46:33 | 3326 | N/A | N/A | | [Part 1]() [Part 2]() | Stopped early and finished during the next night's warmup | 29 | | 13 | 01:08:00 | 5076 | 01:11:08 | 3273 | | [Part 1]() [Part 2]() | | 30 | | 14 | 00:33:57 | 5042 | N/A | N/A | | [Part 1]() [Part 2]() | Stopped early and finished during the next night's warmup | 31 | | 15 | 00:03:55 | 702 | 00:33:21 | 2583 | | [Part 1]() [Part 2]() | | 32 | | 16 | 00:29:22 | 1525 | 00:35:52 | 1356 | | [Part 1]() [Part 2]() | | 33 | 34 | ## Solution Code Highlights 35 | 36 | 55 | -------------------------------------------------------------------------------- /src/main/scala/2023/Day18.worksheet.sc: -------------------------------------------------------------------------------- 1 | import aoc.* 2 | 3 | val input = io.Source.fromResource("2023/day-18.txt").getLines.toList 4 | 5 | def parse(instruction: String): (Dir, Int) = 6 | instruction match 7 | case s"$rawDir $n (#$rawHex)" => 8 | val distance = n.toInt 9 | val dir = rawDir match 10 | case "U" => Dir.N 11 | case "D" => Dir.S 12 | case "R" => Dir.E 13 | case "L" => Dir.W 14 | dir -> distance 15 | 16 | def parse2(instruction: String): (Dir, Int) = 17 | instruction match 18 | case s"$rawDir $n (#$rawHex)" => 19 | val distance = Integer.parseInt(rawHex.take(5), 16) 20 | val dir = rawHex(5) match 21 | case '0' => Dir.E 22 | case '1' => Dir.S 23 | case '2' => Dir.W 24 | case '3' => Dir.N 25 | dir -> distance 26 | 27 | val parsed = input.map(parse2) 28 | 29 | val maxRight = Integer.parseInt("FFFFF", 16) * input.size / 4 30 | 31 | val (p, nsArea) = parsed.foldLeft((Point.origin, 0L)): 32 | case ((pos, area), (newDir, distance)) => 33 | val newPos = pos.move(newDir, distance) 34 | val line = Line(pos, newPos) 35 | newDir match 36 | case Dir.E | Dir.W => newPos -> area 37 | case Dir.N => 38 | val a = Area(line.xRange.min to maxRight, line.yRange) 39 | newPos -> (area + a.size[Long]) 40 | case Dir.S => 41 | val a = Area(line.xRange.min + 1 to maxRight, line.yRange) 42 | newPos -> (area - a.size[Long]) 43 | 44 | val ewPrevNext = 45 | val prev = (parsed.last :: parsed) 46 | val ew = parsed 47 | val next = (parsed.tail :+ parsed.head) 48 | prev.zip(ew).zip(next) 49 | 50 | val (pew, ewArea) = ewPrevNext.foldLeft(Point.origin -> 0L): 51 | case ((pos, area), (((prevDir, _), (newDir, distance)), (nextDir, _))) => 52 | val newPos = pos.move(newDir, distance) 53 | val line = Line(pos, newPos) 54 | println(s"$pos: $prevDir $newDir $distance $nextDir") 55 | newDir match 56 | case Dir.N | Dir.S => newPos -> area 57 | case Dir.E => (prevDir, nextDir) match 58 | case (Dir.N, Dir.N) => 59 | val a = Area(newPos.x to maxRight, pos.y to pos.y) 60 | newPos -> (area - a.size[Long]) 61 | case (Dir.N, Dir.S) => 62 | newPos -> area 63 | case (Dir.S, Dir.S) => 64 | val a = Area(pos.e.x to maxRight, pos.y to pos.y) 65 | newPos -> (area + a.size[Long]) 66 | case (Dir.S, Dir.N) => 67 | val a = Area(pos.e.x to newPos.w.x, pos.y to pos.y) 68 | newPos -> (area + a.size[Long]) 69 | case Dir.W => (prevDir, nextDir) match 70 | case (Dir.N, Dir.N) => 71 | val a = Area(pos.x to maxRight, pos.y to pos.y) 72 | println(s"subtracting $a -${a.size[Long]}") 73 | newPos -> (area - a.size[Long]) 74 | case (Dir.N, Dir.S) => 75 | val a = Area(newPos.e.x to pos.w.x, pos.y to pos.y) 76 | println(s"adding $a +${a.size[Long]}") 77 | newPos -> (area + a.size[Long]) 78 | case (Dir.S, Dir.S) => 79 | val a1 = Area(newPos.e.x to maxRight, pos.y to pos.y) 80 | newPos -> (area + a1.size[Long]) 81 | case (Dir.S, Dir.N) => 82 | newPos -> area 83 | 84 | val ans2 = nsArea + ewArea 85 | -------------------------------------------------------------------------------- /src/main/scala/2020/Day11.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2020/day-11.txt").getLines.toList 2 | 3 | case class Point(x: Int, y: Int): 4 | def u = Point(x, y + 1) 5 | def d = Point(x, y - 1) 6 | def r = Point(x + 1, y) 7 | def l = Point(x - 1, y) 8 | 9 | type Grid = Map[Point, Char] 10 | 11 | val width = input(0).length 12 | val height = input.size 13 | 14 | val xRange = 0 until width 15 | val yRange = 0 until height 16 | 17 | val start: Grid = 18 | val points = 19 | for 20 | (line, y) <- input.zipWithIndex 21 | (char, x) <- line.zipWithIndex 22 | yield Point(x, y) -> char 23 | points.toMap 24 | 25 | def show(grid: Grid): String = 26 | val sb = collection.mutable.StringBuilder() 27 | for y <- yRange do 28 | for x <- xRange do sb += grid(Point(x, y)) 29 | sb += '\n' 30 | sb.result() 31 | 32 | def adjacent(p: Point, g: Grid): List[Point] = 33 | List(p.u.l, p.u, p.u.r, p.l, p.r, p.d.l, p.d, p.d.r) 34 | .filter(q => xRange.contains(q.x) && yRange.contains(q.y)) 35 | 36 | def inBounds(p: Point): Boolean = 37 | xRange.contains(p.x) && yRange.contains(p.y) 38 | 39 | def next1(p: Point, g: Grid): Char = g(p) match 40 | case 'L' if !adjacent(p, g).map(g).contains('#') => '#' 41 | case '#' if adjacent(p, g).map(g).count(_ == '#') >= 4 => 'L' 42 | case c => c 43 | 44 | def next1(grid: Grid): Grid = 45 | val nextPoints = for 46 | y <- yRange 47 | x <- xRange 48 | p = Point(x, y) 49 | yield p -> next1(p, grid) 50 | nextPoints.toMap 51 | 52 | val states = Iterator.iterate(start)(next1) 53 | 54 | val ans1 = states 55 | .sliding(2) 56 | .collectFirst { 57 | case Seq(l, r) if l == r => r.values.count(_ == '#') 58 | }.get 59 | 60 | def visibleSeat(origin: Point, dir: Point, grid: Grid): Option[Char] = 61 | val points = Iterator.iterate(origin) { p => 62 | p.copy(x = p.x + dir.x, y = p.y + dir.y) 63 | } 64 | points 65 | .drop(1) 66 | .takeWhile(inBounds) 67 | .collectFirst { case p if grid(p) != '.' => grid(p) } 68 | 69 | def visibleSeats(p: Point, g: Grid): Seq[Char] = 70 | for 71 | dx <- -1 to 1 72 | dy <- -1 to 1 73 | dir = Point(dx, dy) 74 | if dir != Point(0, 0) 75 | seat <- visibleSeat(p, dir, g) 76 | yield seat 77 | 78 | def next2(grid: Grid): Grid = 79 | val nextPoints = for 80 | y <- yRange 81 | x <- xRange 82 | p = Point(x, y) 83 | yield 84 | val nextSeatState = grid.apply(p) match 85 | case 'L' if !visibleSeats(p, grid).contains('#') => '#' 86 | case '#' if visibleSeats(p, grid).count(_ == '#') >= 5 => 'L' 87 | case c => c 88 | p -> nextSeatState 89 | nextPoints.toMap 90 | 91 | val states2 = Iterator.iterate(start)(next2) 92 | 93 | println(show(states2.next())) 94 | val all = states2.next() 95 | println(show(all)) 96 | 97 | visibleSeats(Point(width - 1, 0), all) 98 | 99 | println(show(states2.next())) 100 | println(show(states2.next())) 101 | 102 | val ans2 = states2 103 | .sliding(2) 104 | .collectFirst { 105 | case Seq(l, r) if l == r => r.values.count(_ == '#') 106 | }.get 107 | -------------------------------------------------------------------------------- /src/main/scala/2019/Day05.worksheet.sc: -------------------------------------------------------------------------------- 1 | val input = io.Source.fromResource("2019/day-05.txt").getLines().next() 2 | val intcode = input.split(',').map(_.toInt).toVector 3 | 4 | enum Ops(val numParams: Int): 5 | case Add extends Ops(3) 6 | case Mul extends Ops(3) 7 | case Save extends Ops(1) 8 | case Out extends Ops(1) 9 | case JumpTrue extends Ops(2) 10 | case JumpFalse extends Ops(2) 11 | case LT extends Ops(3) 12 | case EQ extends Ops(3) 13 | case Halt extends Ops(0) 14 | 15 | import Ops.* 16 | 17 | object Ops: 18 | def fromInst(inst: Int) = 19 | if inst == 99 then Ops.Halt else Ops.fromOrdinal(inst % 100 - 1) 20 | 21 | case class State(memory: Vector[Int], ip: Int): 22 | lazy val inst = memory(ip) 23 | 24 | def value(pos: Int): Int = 25 | val param = memory(ip + 1 + pos) 26 | val mode = (inst / 100).toString.reverse.padTo(3, '0')(pos) 27 | if mode == '0' then memory(param) else param 28 | def addr(op: Ops): Int = memory(ip + op.numParams) 29 | 30 | def progress(op: Ops): State = copy(ip = ip + 1 + op.numParams) 31 | def jump(addr: Int): State = copy(ip = addr) 32 | def write(value: Int, op: Ops): State = 33 | // Note: Parameters that an instruction writes to will never be in immediate mode. 34 | copy(memory.updated(addr(op), value)) 35 | 36 | def withOutput(output: Int) = Some(Some(output) -> this) 37 | def noOutput = Some(None -> this) 38 | 39 | def step(input: Int): Option[(Option[Int], State)] = 40 | println(memory.drop(ip).take(4)) 41 | val op = Ops.fromInst(inst) 42 | op match 43 | case Add => 44 | write(value(0) + value(1), op).progress(op).noOutput 45 | case Mul => 46 | write(value(0) * value(1), op).progress(op).noOutput 47 | case Save => 48 | write(input, op).progress(op).noOutput 49 | case Out => 50 | progress(op).withOutput(memory(addr(op))) 51 | case JumpTrue => 52 | if value(0) != 0 then jump(value(1)).noOutput 53 | else progress(op).noOutput 54 | case JumpFalse => 55 | if value(0) == 0 then jump(value(1)).noOutput 56 | else progress(op).noOutput 57 | case LT => 58 | val result = if value(0) < value(1) then 1 else 0 59 | write(result, op).progress(op).noOutput 60 | case EQ => 61 | val result = if value(0) == value(1) then 1 else 0 62 | write(result, op).progress(op).noOutput 63 | case Halt => None 64 | 65 | def run(initialMemory: Vector[Int], input: Int): Vector[Option[Int]] = 66 | LazyList 67 | .unfold(State(initialMemory, ip = 0))(_.step(input)) 68 | .toVector 69 | 70 | run(Vector(3,9,8,9,10,9,4,9,99,-1,8), 8) 71 | run(Vector(3,9,8,9,10,9,4,9,99,-1,8), 9) 72 | 73 | run(Vector(3,9,7,9,10,9,4,9,99,-1,8), 7) 74 | run(Vector(3,9,7,9,10,9,4,9,99,-1,8), 8) 75 | 76 | run(Vector(3,3,1108,-1,8,3,4,3,99), 8) 77 | run(Vector(3,3,1108,-1,8,3,4,3,99), 9) 78 | 79 | run(Vector(3,3,1107,-1,8,3,4,3,99), 7) 80 | run(Vector(3,3,1107,-1,8,3,4,3,99), 8) 81 | 82 | Vector(3,12,6,12,15,1,13,14,13,4,13,99,-1,0,1,9).size 83 | Vector(3,12,6,12,15,1,13,14,13,4,13,99,-1,0,1,9).indexOf(4) 84 | 85 | run(Vector(3,12,6,12,15,1,13,14,13,4,13,99,-1,0,1,9), 0) 86 | run(Vector(3,12,6,12,15,1,13,14,13,4,13,99,-1,0,1,9), 1) 87 | 88 | val ans1 = run(intcode, input = 1).last.get 89 | val ans2 = run(intcode, input = 5).last.get 90 | -------------------------------------------------------------------------------- /src/main/scala/2021/Day20.worksheet.sc: -------------------------------------------------------------------------------- 1 | import collection.immutable.BitSet 2 | 3 | val input = io.Source.fromResource("2021/day-20-1.txt").getLines.toVector 4 | 5 | val alg = BitSet.fromSpecific( 6 | input.head.iterator.zipWithIndex.collect { case ('#', i) => i } 7 | ) 8 | 9 | val rawImage = input.drop(2) 10 | 11 | case class Area(left: Int, right: Int, top: Int, bot: Int): 12 | def expand(n: Int): Area = 13 | copy(left - n, right + n, top - n, bot + n) 14 | val xRange = left until right 15 | val yRange = top until bot 16 | val width = right - left 17 | val height = top - bot 18 | 19 | def pointToInt(x: Int, y: Int): Option[Int] = 20 | val inBounds = xRange.contains(x) && yRange.contains(y) 21 | if !inBounds then None 22 | else Some((y - top) * width + (x - left)) 23 | 24 | case class Point(x: Int, y: Int): 25 | def toInt(a: Area) = a.pointToInt(x, y) 26 | def nine: Seq[Point] = 27 | for 28 | dy <- -1 to 1 29 | dx <- -1 to 1 30 | yield Point(x + dx, y + dy) 31 | 32 | case class Image(grid: BitSet, inverted: Boolean, area: Area): 33 | def litAt(p: Point) = p.toInt(area).exists(grid) ^ inverted 34 | 35 | def numLit = if inverted then Int.MaxValue else grid.size 36 | def numDark = if inverted then grid.size else Int.MaxValue 37 | 38 | def show: String = 39 | val lines = for y <- area.yRange yield 40 | val line = 41 | for x <- area.xRange 42 | yield if litAt(Point(x, y)) then '#' else '.' 43 | line.mkString 44 | lines.mkString("\n") 45 | 46 | def index(p: Point): Int = 47 | val bits = p.nine.map(q => if litAt(q) then '1' else '0') 48 | Integer.parseInt(bits.mkString, 2) 49 | 50 | def enhance: Image = 51 | val nextInverted = if inverted then alg(511) else alg(0) 52 | val nextArea = area.expand(1) 53 | val nextPoints = for 54 | y <- nextArea.yRange 55 | x <- nextArea.xRange 56 | p = Point(x, y) 57 | if alg(index(p)) ^ nextInverted 58 | yield p.toInt(nextArea).get 59 | val nextGrid = BitSet.fromSpecific(nextPoints) 60 | Image(nextGrid, nextInverted, nextArea) 61 | 62 | val image0: Image = 63 | val area = Area(0, rawImage(0).size, 0, rawImage.size) 64 | val points = for 65 | y <- area.yRange.iterator 66 | x <- area.xRange.iterator 67 | if rawImage(y)(x) == '#' 68 | yield area.pointToInt(x, y).get 69 | val grid = BitSet.fromSpecific(points) 70 | Image(grid, inverted = false, area) 71 | 72 | val images = LazyList.iterate(image0, 51)(_.enhance) 73 | 74 | val ans1 = images(2).numLit 75 | val ans2 = images(50).numLit 76 | 77 | // tests 78 | 79 | val a0 = Area(0, 10, 0, 10) 80 | Point(a0.xRange(0), a0.yRange(0)).toInt(a0) 81 | Point(a0.xRange.last, a0.yRange(0)).toInt(a0) 82 | Point(a0.xRange(0), a0.yRange(1)).toInt(a0) 83 | 84 | val a50 = a0.expand(50 * 2) 85 | Point(a50.xRange(0), a50.yRange(0)).toInt(a50) 86 | Point(a50.xRange.last, a50.yRange(0)).toInt(a50) 87 | Point(a50.xRange(0), a50.yRange(1)).toInt(a50) 88 | 89 | image0.inverted 90 | image0.numLit 91 | image0.numDark 92 | println(image0.show) 93 | 94 | val image1 = images(1) 95 | image1.inverted 96 | image1.numLit 97 | image1.numDark 98 | println(image1.show) 99 | 100 | val image2 = images(2) 101 | image2.inverted 102 | image2.numLit 103 | image2.numDark 104 | println(image2.show) 105 | --------------------------------------------------------------------------------