├── 2021 ├── README.md ├── project.scala └── src │ ├── day1.scala │ ├── day10.scala │ ├── day11.scala │ ├── day13.scala │ ├── day14.scala │ ├── day15.scala │ ├── day16.scala │ ├── day17.scala │ ├── day2.scala │ ├── day20.scala │ ├── day21.scala │ ├── day22.scala │ ├── day23.scala │ ├── day25.scala │ ├── day3.scala │ ├── day4.scala │ ├── day5.scala │ ├── day6.scala │ ├── day7.scala │ ├── day8.scala │ └── day9.scala ├── 2022 ├── README.md ├── project.scala └── src │ ├── day01.scala │ ├── day02.scala │ ├── day03.scala │ ├── day04.scala │ ├── day05.scala │ ├── day06.scala │ ├── day07.scala │ ├── day08.scala │ ├── day09.scala │ ├── day10.scala │ ├── day11.scala │ ├── day12.scala │ ├── day13.scala │ ├── day15.scala │ ├── day16.scala │ ├── day18.scala │ ├── day21.scala │ ├── day25.scala │ ├── inputs.scala │ └── locations.scala ├── 2023 ├── README.md ├── project.scala └── src │ ├── day01.scala │ ├── day02.scala │ ├── day03.scala │ ├── day04.scala │ ├── day10.scala │ ├── day11.scala │ ├── day12.scala │ ├── day12.test.scala │ ├── day13.scala │ ├── day15.scala │ ├── day17.scala │ ├── day18.scala │ ├── day19.scala │ ├── day19.test.scala │ ├── day20.scala │ ├── day23.scala │ ├── day24.scala │ ├── day25.scala │ ├── inputs.scala │ └── locations.scala ├── 2024 ├── project.scala └── src │ ├── day13.scala │ ├── day14.scala │ ├── day19.scala │ ├── day20.scala │ ├── day21.scala │ ├── day22.scala │ ├── day25.scala │ ├── inputs.scala │ └── locations.scala ├── .github └── workflows │ ├── add-daily-article.yml │ └── schedule-add-daily-article.yml ├── .gitignore ├── LICENSE ├── README.md └── img └── code-lenses.png /.github/workflows/add-daily-article.yml: -------------------------------------------------------------------------------- 1 | name: Add Daily Article Manual 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | add-article: 8 | name: Create pull request to add today's article 9 | permissions: 10 | pull-requests: write # for repo-sync/pull-request to create a PR 11 | contents: write # for repo-sync/pull-request to create a PR 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Get current date 15 | id: date 16 | run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT 17 | - uses: actions/checkout@v3 18 | with: 19 | ref: website 20 | submodules: true 21 | 22 | - uses: coursier/cache-action@v6 23 | 24 | - uses: VirtusLab/scala-cli-setup@v0.1.18 25 | with: 26 | jvm: "temurin:17" 27 | 28 | - name: Generate todays article 29 | run: scala-cli run .github/workflows/scripts/addDay.scala 30 | 31 | - name: Push article to new branch 32 | run: | 33 | git config user.name gh-actions 34 | git config user.email actions@github.com 35 | git checkout -b "add-article-${{ steps.date.outputs.date }}" 36 | git add . 37 | if ! git diff-index --quiet HEAD; then 38 | git commit -m "Add article of ${{ steps.date.outputs.date }}" 39 | git push --set-upstream origin add-article-${{ steps.date.outputs.date }} 40 | fi 41 | 42 | - name: Create PR to merge article 43 | uses: repo-sync/pull-request@v2 44 | with: 45 | source_branch: add-article-${{ steps.date.outputs.date }} 46 | destination_branch: website 47 | pr_title: Add advent of code article for ${{ steps.date.outputs.date }} 48 | pr_body: | 49 | This PR was automatically generated to add the article of today. 50 | pr_assignee: ${{ github.event.head_commit.author.username }} 51 | -------------------------------------------------------------------------------- /.github/workflows/schedule-add-daily-article.yml: -------------------------------------------------------------------------------- 1 | name: Add Daily Article Scheduled 2 | 3 | on: 4 | schedule: 5 | - cron: "21 5 1-25 12 *" # every 5:21 AM UTC 1st-25th December 6 | 7 | jobs: 8 | add-article: 9 | name: Add today's article without a PR 10 | permissions: 11 | pull-requests: write # for repo-sync/pull-request to create a PR 12 | contents: write # for repo-sync/pull-request to create a PR 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Get current date 16 | id: date 17 | run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT 18 | - uses: actions/checkout@v3 19 | with: 20 | ref: website 21 | submodules: true 22 | 23 | - uses: coursier/cache-action@v6 24 | 25 | - uses: VirtusLab/scala-cli-setup@v0.1.18 26 | with: 27 | jvm: "temurin:17" 28 | apps: sbt 29 | 30 | - name: Generate todays article 31 | run: scala-cli run .github/workflows/scripts/addDay.scala 32 | 33 | - name: Push article to website branch 34 | run: | 35 | git config user.name gh-actions 36 | git config user.email actions@github.com 37 | git add . 38 | if ! git diff-index --quiet HEAD; then 39 | git commit -m "Add article of ${{ steps.date.outputs.date }}" 40 | git push -f 41 | fi 42 | 43 | - name: Publish ${{ github.ref }} 44 | run: sbt docs/docusaurusPublishGhpages 45 | env: 46 | GITHUB_DEPLOY_KEY: ${{ secrets.DOC }} 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Advent of Code stuff 2 | ## DO NOT COMMIT YOUR INPUTS - THEY ARE COPYRIGHT PROTECTED 3 | input 4 | 5 | # Scala stuff 6 | .bloop/ 7 | .bsp/ 8 | metals.sbt 9 | .metals/ 10 | target/ 11 | .scala/ 12 | .scala-build 13 | 14 | # Solutions 15 | /solutions/ 16 | 17 | # Dependencies 18 | website/node_modules 19 | 20 | # Production 21 | website/build 22 | 23 | # Generated files 24 | .docusaurus 25 | .cache-loader 26 | 27 | # Misc 28 | .DS_Store 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | npm-debug.log* 35 | yarn-debug.log* 36 | yarn-error.log* 37 | .vscode 38 | 39 | 20[0-9][0-9]/input/** 40 | -------------------------------------------------------------------------------- /2021/README.md: -------------------------------------------------------------------------------- 1 | # Scala Advent of Code 2021 2 | 3 | Solutions in Scala for the annual [Advent of Code](https://adventofcode.com/) challenge. _Note: this repo is not affiliated with Advent of Code._ 4 | 5 | ## Website 6 | 7 | The [Scala Advent of Code](https://scalacenter.github.io/scala-advent-of-code/) website contains: 8 | - some explanation of our solutions to [Advent of Code (adventofcode.com)](https://adventofcode.com/) 9 | - more solutions from the community 10 | 11 | ## Setup 12 | 13 | We use Visual Studio Code with Metals to write Scala code, and scala-cli to compile and run it. 14 | 15 | You can follow these [steps](https://scalacenter.github.io/scala-advent-of-code/setup) to set up your environement. 16 | 17 | ### How to open in Visual Studio Code 18 | 19 | After you clone the repository, open a terminal and run: 20 | ``` 21 | $ cd scala-advent-of-code/2021 22 | $ scala-cli setup-ide src 23 | $ mkdir input 24 | $ code . 25 | ``` 26 | 27 | `code .` will open Visual Studio Code and start Metals. 28 | 29 | ### How to run a solution 30 | 31 | First copy your input to the folder `scala-advent-of-code/2021/input`. 32 | 33 | Next, in a terminal you can run: 34 | ``` 35 | $ cd scala-advent-of-code/2021 36 | $ scala-cli . -M day1.part1 37 | Compiling project (Scala 3.x.y, JVM) 38 | Compiled project (Scala 3.x.y, JVM) 39 | The solution is 1559 40 | ``` 41 | 42 | The result will likely be different for you, as inputs are different for each user. 43 | 44 | Or, to run another solution: 45 | ``` 46 | $ scala-cli . -M . 47 | ``` 48 | 49 | By default the solution programs run on our input files which are stored in the `input` folder. 50 | To get your solutions you can change the content of those files in the `input` folder. 51 | 52 | 53 | #### How to run day3 54 | 55 | The solution of day 3 is written for the javascript target. 56 | You can run it locally, if you have [Node.js](https://nodejs.org/en/) installed, by adding the `--js` option: 57 | ``` 58 | $ scala-cli . --js -M day3.part1 59 | ``` 60 | 61 | ## Contributing 62 | - Please do not commit your puzzle inputs, we can not accept them as they are protected by copyright 63 | -------------------------------------------------------------------------------- /2021/project.scala: -------------------------------------------------------------------------------- 1 | //> using scala 3.3.1 2 | -------------------------------------------------------------------------------- /2021/src/day1.scala: -------------------------------------------------------------------------------- 1 | package day1 2 | 3 | import scala.io.Source 4 | 5 | @main def part1(): Unit = 6 | val input = Source.fromFile("input/day1").mkString 7 | val answer = part1(input) 8 | println(s"The solution is $answer") 9 | 10 | @main def part2(): Unit = 11 | val input = Source.fromFile("input/day1").mkString 12 | val answer = part2(input) 13 | println(s"The solution is $answer") 14 | 15 | def part1(input: String): Int = 16 | val depths = input.linesIterator.map(_.toInt) 17 | val pairs = depths.sliding(2).map(arr => (arr(0), arr(1))) 18 | pairs.count((prev, next) => prev < next) 19 | 20 | def part2(input: String): Int = 21 | val depths = input.linesIterator.map(_.toInt) 22 | val sums = depths.sliding(3).map(_.sum) 23 | val pairs = sums.sliding(2).map(arr => (arr(0), arr(1))) 24 | pairs.count((prev, next) => prev < next) 25 | -------------------------------------------------------------------------------- /2021/src/day10.scala: -------------------------------------------------------------------------------- 1 | package day10 2 | 3 | import scala.util.Using 4 | import scala.io.Source 5 | 6 | @main def part1(): Unit = 7 | println(s"The solution is ${part1(readInput())}") 8 | 9 | @main def part2(): Unit = 10 | println(s"The solution is ${part2(readInput())}") 11 | 12 | def readInput(): String = 13 | Using.resource(Source.fromFile("input/day10"))(_.mkString) 14 | 15 | enum CheckResult: 16 | case Ok 17 | case IllegalClosing(expected: Option[Symbol], found: Symbol) 18 | case Incomplete(pending: List[Symbol]) 19 | 20 | extension (illegalClosing: CheckResult.IllegalClosing) 21 | def score: Int = 22 | import Kind.* 23 | illegalClosing.found.kind match 24 | case Parenthesis => 3 25 | case Bracket => 57 26 | case Brace => 1197 27 | case Diamond => 25137 28 | 29 | enum Direction: 30 | case Open, Close 31 | 32 | enum Kind: 33 | case Parenthesis, Bracket, Brace, Diamond 34 | 35 | case class Symbol(kind: Kind, direction: Direction): 36 | def isOpen: Boolean = direction == Direction.Open 37 | 38 | def checkChunks(expression: List[Symbol]): CheckResult = 39 | @scala.annotation.tailrec 40 | def iter(pending: List[Symbol], input: List[Symbol]): CheckResult = 41 | input match 42 | case Nil => 43 | if pending.isEmpty then CheckResult.Ok 44 | else CheckResult.Incomplete(pending) 45 | case nextChar :: remainingChars => 46 | if nextChar.isOpen then iter(nextChar :: pending, remainingChars) 47 | else pending match 48 | case Nil => CheckResult.IllegalClosing(None, nextChar) 49 | case lastOpened :: previouslyOpened => 50 | if lastOpened.kind == nextChar.kind then iter(previouslyOpened, remainingChars) 51 | else CheckResult.IllegalClosing(Some(lastOpened), nextChar) 52 | 53 | iter(List.empty, expression) 54 | 55 | def parseRow(row: String): List[Symbol] = 56 | import Direction.* 57 | import Kind.* 58 | row.to(List).map { 59 | case '(' => Symbol(Parenthesis, Open) 60 | case ')' => Symbol(Parenthesis, Close) 61 | case '[' => Symbol(Bracket, Open) 62 | case ']' => Symbol(Bracket, Close) 63 | case '{' => Symbol(Brace, Open) 64 | case '}' => Symbol(Brace, Close) 65 | case '<' => Symbol(Diamond, Open) 66 | case '>' => Symbol(Diamond, Close) 67 | case _ => throw IllegalArgumentException("Symbol not supported") 68 | } 69 | 70 | def part1(input: String): Int = 71 | val rows: LazyList[List[Symbol]] = 72 | input.linesIterator 73 | .to(LazyList) 74 | .map(parseRow) 75 | 76 | rows.map(checkChunks) 77 | .collect { case illegal: CheckResult.IllegalClosing => illegal.score } 78 | .sum 79 | 80 | extension (incomplete: CheckResult.Incomplete) 81 | def score: BigInt = 82 | import Kind.* 83 | incomplete.pending.foldLeft(BigInt(0)) { (currentScore, symbol) => 84 | val points = symbol.kind match 85 | case Parenthesis => 1 86 | case Bracket => 2 87 | case Brace => 3 88 | case Diamond => 4 89 | 90 | currentScore * 5 + points 91 | } 92 | 93 | def part2(input: String): BigInt = 94 | val rows: LazyList[List[Symbol]] = 95 | input.linesIterator 96 | .to(LazyList) 97 | .map(parseRow) 98 | 99 | val scores = 100 | rows.map(checkChunks) 101 | .collect { case incomplete: CheckResult.Incomplete => incomplete.score } 102 | .toVector 103 | .sorted 104 | 105 | scores(scores.length / 2) 106 | -------------------------------------------------------------------------------- /2021/src/day11.scala: -------------------------------------------------------------------------------- 1 | package day11 2 | 3 | import scala.util.Using 4 | import scala.io.Source 5 | import scala.annotation.tailrec 6 | import scala.collection.immutable.Queue 7 | 8 | @main def part1(): Unit = 9 | println(s"The solution is ${part1(readInput())}") 10 | 11 | @main def part2(): Unit = 12 | println(s"The solution is ${part2(readInput())}") 13 | 14 | def readInput(): String = 15 | Using.resource(Source.fromFile("input/day11"))(_.mkString) 16 | 17 | def parse(input: String): Octopei = 18 | val lines = input.split("\n") 19 | val allPoints = for 20 | (line, y) <- lines.zipWithIndex 21 | (char, x) <- line.zipWithIndex 22 | yield Point(x, y) -> char.toString.toInt 23 | Octopei(allPoints.toMap) 24 | 25 | trait Step: 26 | def increment: Step 27 | def addFlashes(f: Int): Step 28 | def shouldStop: Boolean 29 | def currentFlashes: Int 30 | def stepNumber: Int 31 | 32 | case class MaxIterStep(currentFlashes: Int, stepNumber: Int, max: Int) extends Step: 33 | def increment = this.copy(stepNumber = stepNumber + 1) 34 | def addFlashes(f: Int) = this.copy(currentFlashes = currentFlashes + f) 35 | def shouldStop = stepNumber == max 36 | 37 | case class SynchronizationStep( 38 | currentFlashes: Int, 39 | stepNumber: Int, 40 | maxChange: Int, 41 | lastFlashes: Int 42 | ) extends Step: 43 | def increment = this.copy(stepNumber = stepNumber + 1) 44 | def addFlashes(f: Int) = 45 | this.copy(currentFlashes = currentFlashes + f, lastFlashes = currentFlashes) 46 | def shouldStop = currentFlashes - lastFlashes == maxChange 47 | 48 | case class Point(x: Int, y: Int) 49 | case class Octopei(inputMap: Map[Point, Int]): 50 | 51 | @tailrec 52 | private def propagate( 53 | toVisit: Queue[Point], 54 | alreadyFlashed: Set[Point], 55 | currentSituation: Map[Point, Int] 56 | ): Map[Point, Int] = 57 | toVisit.dequeueOption match 58 | case None => currentSituation 59 | case Some((point, dequeued)) => 60 | currentSituation.get(point) match 61 | case Some(value) if value > 9 && !alreadyFlashed(point) => 62 | val propagated = 63 | Seq( 64 | point.copy(x = point.x + 1), 65 | point.copy(x = point.x - 1), 66 | point.copy(y = point.y + 1), 67 | point.copy(y = point.y - 1), 68 | point.copy(x = point.x + 1, y = point.y + 1), 69 | point.copy(x = point.x + 1, y = point.y - 1), 70 | point.copy(x = point.x - 1, y = point.y + 1), 71 | point.copy(x = point.x - 1, y = point.y - 1) 72 | ) 73 | val newSituation = propagated.foldLeft(currentSituation) { 74 | case (map, point) => 75 | map.get(point) match 76 | case Some(value) => map.updated(point, value + 1) 77 | case _ => map 78 | } 79 | propagate( 80 | dequeued.appendedAll(propagated), 81 | alreadyFlashed + point, 82 | newSituation 83 | ) 84 | case _ => 85 | propagate(dequeued, alreadyFlashed, currentSituation) 86 | end propagate 87 | 88 | def simulate(step: Step) = simulateIter(step, inputMap) 89 | 90 | @tailrec 91 | private def simulateIter( 92 | step: Step, 93 | currentSituation: Map[Point, Int] 94 | ): Step = 95 | if step.shouldStop then step 96 | else 97 | val incremented = currentSituation.map { case (point, value) => 98 | (point, value + 1) 99 | } 100 | val flashes = incremented.collect { 101 | case (point, value) if value > 9 => point 102 | }.toList 103 | val propagated = propagate(Queue(flashes*), Set.empty, incremented) 104 | val newFlashes = propagated.collect { 105 | case (point, value) if value > 9 => 1 106 | }.sum 107 | val zeroed = propagated.map { 108 | case (point, value) if value > 9 => (point, 0) 109 | case same => same 110 | } 111 | simulateIter(step.increment.addFlashes(newFlashes), zeroed) 112 | end simulateIter 113 | 114 | end Octopei 115 | 116 | def part1(input: String) = 117 | val octopei = parse(input) 118 | val step = MaxIterStep(0, 0, 100) 119 | octopei.simulate(step).currentFlashes 120 | 121 | def part2(input: String) = 122 | val octopei = parse(input) 123 | val step = SynchronizationStep(0, 0, octopei.inputMap.size, 0) 124 | octopei.simulate(step).stepNumber 125 | -------------------------------------------------------------------------------- /2021/src/day13.scala: -------------------------------------------------------------------------------- 1 | package day13 2 | 3 | import scala.util.Using 4 | import scala.io.Source 5 | 6 | @main def part1(): Unit = 7 | val answer = part1(readInput()) 8 | println(s"The answer is: $answer") 9 | 10 | @main def part2(): Unit = 11 | val answer = part2(readInput()) 12 | println(s"The answer is:\n$answer") 13 | 14 | def readInput(): String = 15 | Using.resource(Source.fromFile("input/day13"))(_.mkString) 16 | 17 | def part1(input: String): Int = 18 | val (dots, folds) = parseInstructions(input) 19 | dots.map(folds.head.apply).size 20 | 21 | def part2(input: String): String = 22 | val (dots, folds) = parseInstructions(input) 23 | val foldedDots = folds.foldLeft(dots)((dots, fold) => dots.map(fold.apply)) 24 | 25 | val (width, height) = (foldedDots.map(_.x).max + 1, foldedDots.map(_.y).max + 1) 26 | val paper = Array.fill(height, width)('.') 27 | for dot <- foldedDots do paper(dot.y)(dot.x) = '#' 28 | 29 | paper.map(_.mkString).mkString("\n") 30 | 31 | def parseInstructions(input: String): (Set[Dot], List[Fold]) = 32 | val sections = input.split("\n\n") 33 | val dots = sections(0).linesIterator.map(Dot.parse).toSet 34 | val folds = sections(1).linesIterator.map(Fold.parse).toList 35 | (dots, folds) 36 | 37 | case class Dot(x: Int, y: Int) 38 | 39 | object Dot: 40 | def parse(line: String): Dot = 41 | line match 42 | case s"$x,$y" => Dot(x.toInt, y.toInt) 43 | case _ => throw new Exception(s"Cannot parse '$line' to Dot") 44 | 45 | enum Fold: 46 | case Vertical(x: Int) 47 | case Horizontal(y: Int) 48 | 49 | def apply(dot: Dot): Dot = 50 | this match 51 | case Vertical(x: Int) => Dot(x = fold(along = x)(dot.x), dot.y) 52 | case Horizontal(y : Int) => Dot(dot.x, fold(along = y)(dot.y)) 53 | 54 | private def fold(along: Int)(point: Int): Int = 55 | if point < along then point 56 | else along - (point - along) 57 | 58 | object Fold: 59 | def parse(line: String): Fold = 60 | line match 61 | case s"fold along x=$x" => Vertical(x.toInt) 62 | case s"fold along y=$y" => Horizontal(y.toInt) 63 | case _ => throw new Exception(s"Cannot parse '$line' to Fold") 64 | -------------------------------------------------------------------------------- /2021/src/day14.scala: -------------------------------------------------------------------------------- 1 | package day14 2 | 3 | import scala.collection.mutable 4 | import scala.util.Using 5 | import scala.io.Source 6 | 7 | @main def part1(): Unit = 8 | val answer = part1(readInput()) 9 | println(s"The answer is: $answer") 10 | 11 | @main def part2(): Unit = 12 | val answer = part2(readInput()) 13 | println(s"The answer is:\n$answer") 14 | 15 | def readInput(): String = 16 | Using.resource(Source.fromFile("input/day14"))(_.mkString) 17 | 18 | type Polymer = List[Char] 19 | type CharPair = (Char, Char) 20 | type InsertionRules = Map[CharPair, Char] 21 | type Frequencies = Map[Char, Long] 22 | 23 | def part1(input: String): Long = 24 | val (initialPolymer, insertionRules) = parseInput(input) 25 | val finalPolymer = (0 until 10).foldLeft(initialPolymer)((polymer, _) => applyRules(polymer, insertionRules)) 26 | val frequencies: Frequencies = finalPolymer.groupMapReduce(identity)(_ => 1L)(_ + _) 27 | val max = frequencies.values.max 28 | val min = frequencies.values.min 29 | max - min 30 | 31 | def parseInput(input: String): (Polymer, InsertionRules) = 32 | val sections = input.split("\n\n") 33 | val initialPolymer = sections(0).toList 34 | val insertionRules = sections(1).linesIterator.map(parseRule).toMap 35 | (initialPolymer, insertionRules) 36 | 37 | def parseRule(line: String): (CharPair, Char) = 38 | line match 39 | case s"$pairStr -> $inserStr" => (pairStr(0), pairStr(1)) -> inserStr(0) 40 | case _ => throw new Exception(s"Cannot parse '$line' as an insertion rule") 41 | 42 | def applyRules(polymer: Polymer, rules: InsertionRules): Polymer = 43 | val pairs = polymer.zip(polymer.tail) 44 | val insertionsAndSeconds: List[List[Char]] = 45 | for pair @ (first, second) <- pairs 46 | yield rules(pair) :: second :: Nil 47 | polymer.head :: insertionsAndSeconds.flatten 48 | 49 | def part2(input: String): Long = 50 | val (initialPolymer, insertionRules) = parseInput(input) 51 | 52 | /* S(ab...f, n) := 53 | * frequencies of all letters *except the first one* in the 54 | * expansion of "ab...f" after n iterations 55 | * 56 | * Frequencies are multisets. We use + and ∑ to denote their sum. 57 | * 58 | * For any string longer than 2 chars, we have 59 | * S(x₁x₂x₃...xₚ, n) = ∑(S(xᵢxⱼ, n)) for i = 0 to n-1 and j = i+1 60 | * because each initial pair expands independently of the others. Each 61 | * initial char is counted exactly once in the final frequencies because it 62 | * is counted as part of the expansion of the pair on its left, and not the 63 | * expansion of the pair on its right (we always exclude the first char). 64 | * 65 | * As particular case of the above, for a string of 3 chars xzy, we have 66 | * S(xzy, n) = S(xz, n) + S(zy, n) 67 | * 68 | * For strings of length 2, we have two cases: n = 0 and n > 0. 69 | * 70 | * Base case: a pair 'xy', and n = 0 71 | * S(xy, 0) = {y -> 1} for all x, y 72 | * 73 | * Inductive case: a pair 'xy', and n > 0 74 | * S(xy, n) 75 | * = S(xzy, n-1) where z is the insertion char for the pair 'xy' (by definition) 76 | * = S(xz, n-1) + S(zy, n-1) -- the particular case of 3 chars above 77 | * 78 | * And that means we can iteratively construct S(xy, n) for all pairs 'xy' 79 | * and for n ranging from 0 to 40. 80 | */ 81 | 82 | // S : (charPair, n) -> frequencies of everything but the first char after n iterations from charPair 83 | val S = mutable.Map.empty[(CharPair, Int), Frequencies] 84 | 85 | // Base case: S(xy, 0) = {y -> 1} for all x, y 86 | for (pair @ (first, second), insert) <- insertionRules do 87 | S((pair, 0)) = Map(second -> 1L) 88 | 89 | // Recursive case S(xy, n) = S(xz, n - 1) + S(zy, n - 1) with z = insertionRules(xy) 90 | for n <- 1 to 40 do 91 | for (pair, insert) <- insertionRules do 92 | val (x, y) = pair 93 | val z = insertionRules(pair) 94 | S((pair, n)) = addFrequencies(S((x, z), n - 1), S((z, y), n - 1)) 95 | 96 | // S(polymer, 40) = ∑(S(pair, 40)) 97 | val pairsInPolymer = initialPolymer.zip(initialPolymer.tail) 98 | val polymerS = (for pair <- pairsInPolymer yield S(pair, 40)).reduce(addFrequencies) 99 | 100 | // We have to add the very first char to get all the frequencies 101 | val frequencies = addFrequencies(polymerS, Map(initialPolymer.head -> 1L)) 102 | 103 | // Finally, we can finish the computation as in part 1 104 | val max = frequencies.values.max 105 | val min = frequencies.values.min 106 | max - min 107 | 108 | def addFrequencies(a: Frequencies, b: Frequencies): Frequencies = 109 | b.foldLeft(a) { case (prev, (char, frequency)) => 110 | prev + (char -> (prev.getOrElse(char, 0L) + frequency)) 111 | } 112 | -------------------------------------------------------------------------------- /2021/src/day15.scala: -------------------------------------------------------------------------------- 1 | package day15 2 | 3 | import scala.util.Using 4 | import scala.io.Source 5 | import scala.collection.mutable 6 | 7 | @main def part1(): Unit = 8 | val answer = part1(readInput()) 9 | println(s"The answer is: $answer") 10 | 11 | @main def part2(): Unit = 12 | val answer = part2(readInput()) 13 | println(s"The answer is:\n$answer") 14 | 15 | def readInput(): String = 16 | Using.resource(Source.fromFile("input/day15"))(_.mkString) 17 | 18 | type Coord = (Int, Int) 19 | class GameMap(cells: IndexedSeq[IndexedSeq[Int]]): 20 | val maxRow = cells.length - 1 21 | val maxCol = cells.head.length - 1 22 | 23 | def neighboursOf(c: Coord): List[Coord] = 24 | val (row, col) = c 25 | val lb = mutable.ListBuffer.empty[Coord] 26 | if row < maxRow then lb.append((row+1, col)) 27 | if row > 0 then lb.append((row-1, col)) 28 | if col < maxCol then lb.append((row, col+1)) 29 | if col > 0 then lb.append((row, col-1)) 30 | lb.toList 31 | 32 | def costOf(c: Coord): Int = c match 33 | case (row, col) => cells(row)(col) 34 | end GameMap 35 | 36 | def cheapestDistance(gameMap: GameMap): Int = 37 | val visited = mutable.Set.empty[Coord] 38 | val dist = mutable.Map[Coord, Int]((0, 0) -> 0) 39 | val queue = java.util.PriorityQueue[Coord](Ordering.by(dist)) 40 | queue.add((0, 0)) 41 | 42 | while queue.peek() != null do 43 | val c = queue.poll() 44 | visited += c 45 | val newNodes: List[Coord] = gameMap.neighboursOf(c).filterNot(visited) 46 | val cDist = dist(c) 47 | for n <- newNodes do 48 | val newDist = cDist + gameMap.costOf(n) 49 | if !dist.contains(n) || dist(n) > newDist then 50 | dist(n) = newDist 51 | queue.remove(n) 52 | queue.add(n) 53 | 54 | dist((gameMap.maxRow, gameMap.maxCol)) 55 | end cheapestDistance 56 | 57 | def parse(text: String): IndexedSeq[IndexedSeq[Int]] = 58 | for line <- text.split("\n").toIndexedSeq yield 59 | for char <- line.toIndexedSeq yield char.toString.toInt 60 | 61 | def part1(input: String) = 62 | val gameMap = GameMap(parse(input)) 63 | cheapestDistance(gameMap) 64 | 65 | def part2(input: String) = 66 | val seedTile = parse(input) 67 | val gameMap = GameMap( 68 | (0 until 5).flatMap { tileIdVertical => 69 | for row <- seedTile yield 70 | for 71 | tileIdHorizontal <- 0 until 5 72 | cell <- row 73 | yield (cell + tileIdHorizontal + tileIdVertical - 1) % 9 + 1 74 | } 75 | ) 76 | cheapestDistance(gameMap) 77 | -------------------------------------------------------------------------------- /2021/src/day16.scala: -------------------------------------------------------------------------------- 1 | package day16 2 | 3 | import scala.util.Using 4 | import scala.io.Source 5 | import scala.annotation.tailrec 6 | 7 | @main def part1(): Unit = 8 | println(s"The solution is ${part1(readInput())}") 9 | 10 | @main def part2(): Unit = 11 | println(s"The solution is ${part2(readInput())}") 12 | 13 | def readInput(): String = 14 | Using.resource(Source.fromFile("input/day16"))(_.mkString) 15 | 16 | val hexadecimalMapping = 17 | Map( 18 | '0' -> "0000", 19 | '1' -> "0001", 20 | '2' -> "0010", 21 | '3' -> "0011", 22 | '4' -> "0100", 23 | '5' -> "0101", 24 | '6' -> "0110", 25 | '7' -> "0111", 26 | '8' -> "1000", 27 | '9' -> "1001", 28 | 'A' -> "1010", 29 | 'B' -> "1011", 30 | 'C' -> "1100", 31 | 'D' -> "1101", 32 | 'E' -> "1110", 33 | 'F' -> "1111" 34 | ) 35 | 36 | /* 37 | * Structures for all possible operators 38 | */ 39 | enum Packet(version: Int, typeId: Int): 40 | case Sum(version: Int, exprs: List[Packet]) extends Packet(version, 0) 41 | case Product(version: Int, exprs: List[Packet]) extends Packet(version, 1) 42 | case Minimum(version: Int, exprs: List[Packet]) extends Packet(version, 2) 43 | case Maximum(version: Int, exprs: List[Packet]) extends Packet(version, 3) 44 | case Literal(version: Int, literalValue: Long) extends Packet(version, 4) 45 | case GreaterThan(version: Int, lhs: Packet, rhs: Packet) extends Packet(version, 5) 46 | case LesserThan(version: Int, lhs: Packet, rhs: Packet) extends Packet(version, 6) 47 | case Equals(version: Int, lhs: Packet, rhs: Packet) extends Packet(version, 7) 48 | 49 | def versionSum: Int = 50 | this match 51 | case Sum(version, exprs) => version + exprs.map(_.versionSum).sum 52 | case Product(version, exprs) => version + exprs.map(_.versionSum).sum 53 | case Minimum(version, exprs) => version + exprs.map(_.versionSum).sum 54 | case Maximum(version, exprs) => version + exprs.map(_.versionSum).sum 55 | case Literal(version, value) => version 56 | case GreaterThan(version, lhs, rhs) => version + lhs.versionSum + rhs.versionSum 57 | case LesserThan(version, lhs, rhs) => version + lhs.versionSum + rhs.versionSum 58 | case Equals(version, lhs, rhs) => version + lhs.versionSum + rhs.versionSum 59 | 60 | def value: Long = 61 | this match 62 | case Sum(version, exprs) => exprs.map(_.value).sum 63 | case Product(version, exprs) => exprs.map(_.value).reduce(_ * _) 64 | case Minimum(version, exprs) => exprs.map(_.value).min 65 | case Maximum(version, exprs) => exprs.map(_.value).max 66 | case Literal(version, value) => value 67 | case GreaterThan(version, lhs, rhs) => if lhs.value > rhs.value then 1 else 0 68 | case LesserThan(version, lhs, rhs) => if lhs.value < rhs.value then 1 else 0 69 | case Equals(version, lhs, rhs) => if lhs.value == rhs.value then 1 else 0 70 | end Packet 71 | 72 | type BinaryData = List[Char] 73 | 74 | def toInt(input: BinaryData): Int = 75 | Integer.parseInt(input.mkString, 2) 76 | 77 | def toLong(input: BinaryData): Long = 78 | java.lang.Long.parseLong(input.mkString, 2) 79 | 80 | @tailrec 81 | def readLiteralBody(input: BinaryData, numAcc: BinaryData): (Long, BinaryData) = 82 | val (num, rest) = input.splitAt(5) 83 | if num(0) == '1' then readLiteralBody(rest, numAcc.appendedAll(num.drop(1))) 84 | else 85 | val bits = numAcc.appendedAll(num.drop(1)) 86 | (toLong(bits), rest) 87 | end readLiteralBody 88 | 89 | def readOperatorBody(input: BinaryData): (List[Packet], BinaryData) = 90 | val (lenId, rest) = input.splitAt(1) 91 | 92 | @tailrec 93 | def readMaxBits( 94 | input: BinaryData, 95 | remaining: Int, 96 | acc: List[Packet] 97 | ): (List[Packet], BinaryData) = 98 | if remaining == 0 then (acc, input) 99 | else 100 | val (newExpr, rest) = decodePacket(input) 101 | readMaxBits(rest, remaining - (input.size - rest.size), acc :+ newExpr) 102 | end readMaxBits 103 | 104 | @tailrec 105 | def readMaxPackages( 106 | input: BinaryData, 107 | remaining: Int, 108 | acc: List[Packet] 109 | ): (List[Packet], BinaryData) = 110 | if remaining == 0 then (acc, input) 111 | else 112 | val (newExpr, rest) = decodePacket(input) 113 | readMaxPackages(rest, remaining - 1, acc :+ newExpr) 114 | end readMaxPackages 115 | 116 | // read based on length 117 | if lenId(0) == '0' then 118 | val (size, packets) = rest.splitAt(15) 119 | readMaxBits(packets, toInt(size), Nil) 120 | // read based on number of packages 121 | else 122 | val (size, packets) = rest.splitAt(11) 123 | readMaxPackages(packets, toInt(size), Nil) 124 | end readOperatorBody 125 | 126 | def decodePacket(input: BinaryData): (Packet, BinaryData) = 127 | val (versionBits, rest) = input.splitAt(3) 128 | val version = toInt(versionBits) 129 | val (typeBits, body) = rest.splitAt(3) 130 | val tpe = toInt(typeBits) 131 | 132 | tpe match 133 | case 4 => 134 | val (value, remaining) = readLiteralBody(body, Nil) 135 | (Packet.Literal(version, value), remaining) 136 | case otherTpe => 137 | val (values, remaining) = readOperatorBody(body) 138 | otherTpe match 139 | case 0 => (Packet.Sum(version, values), remaining) 140 | case 1 => (Packet.Product(version, values), remaining) 141 | case 2 => (Packet.Minimum(version, values), remaining) 142 | case 3 => (Packet.Maximum(version, values), remaining) 143 | case 5 => (Packet.GreaterThan(version, values(0), values(1)), remaining) 144 | case 6 => (Packet.LesserThan(version, values(0), values(1)), remaining) 145 | case 7 => (Packet.Equals(version, values(0), values(1)), remaining) 146 | end match 147 | end decodePacket 148 | 149 | def parse(input: String) = 150 | val number = input.toList.flatMap(hex => hexadecimalMapping(hex).toCharArray) 151 | val (operator, _) = decodePacket(number) 152 | operator 153 | 154 | def part1(input: String) = 155 | val packet = parse(input) 156 | packet.versionSum 157 | 158 | def part2(input: String) = 159 | val packet = parse(input) 160 | packet.value 161 | end part2 162 | -------------------------------------------------------------------------------- /2021/src/day17.scala: -------------------------------------------------------------------------------- 1 | package day17 2 | 3 | import scala.util.Using 4 | import scala.io.Source 5 | 6 | @main def part1(): Unit = 7 | println(s"The solution is ${part1(readInput())}") 8 | 9 | @main def part2(): Unit = 10 | println(s"The solution is ${part2(readInput())}") 11 | 12 | def readInput(): String = 13 | Using.resource(Source.fromFile("input/day17"))(_.mkString) 14 | 15 | case class Target(xs: Range, ys: Range) 16 | 17 | case class Velocity(x: Int, y: Int) 18 | 19 | case class Position(x: Int, y: Int) 20 | 21 | val initial = Position(x = 0, y = 0) 22 | 23 | case class Probe(position: Position, velocity: Velocity) 24 | 25 | def step(probe: Probe): Probe = 26 | val Probe(Position(px, py), Velocity(vx, vy)) = probe 27 | Probe(Position(px + vx, py + vy), Velocity(vx - vx.sign, vy - 1)) 28 | 29 | def collides(probe: Probe, target: Target): Boolean = 30 | val Probe(Position(px, py), _) = probe 31 | val Target(xs, ys) = target 32 | xs.contains(px) && ys.contains(py) 33 | 34 | def beyond(probe: Probe, target: Target): Boolean = 35 | val Probe(Position(px, py), Velocity(vx, vy)) = probe 36 | val Target(xs, ys) = target 37 | val beyondX = (vx == 0 && px < xs.min) || px > xs.max 38 | val beyondY = vy < 0 && py < ys.min 39 | beyondX || beyondY 40 | 41 | def simulate(probe: Probe, target: Target): Option[Int] = 42 | LazyList 43 | .iterate((probe, 0))((probe, maxY) => (step(probe), maxY `max` probe.position.y)) 44 | .dropWhile((probe, _) => !collides(probe, target) && !beyond(probe, target)) 45 | .headOption 46 | .collect { case (probe, maxY) if collides(probe, target) => maxY } 47 | 48 | def allMaxHeights(target: Target)(positiveOnly: Boolean): Seq[Int] = 49 | val upperBoundX = target.xs.max 50 | val upperBoundY = target.ys.min.abs 51 | val lowerBoundY = if positiveOnly then 0 else -upperBoundY 52 | for 53 | vx <- 0 to upperBoundX 54 | vy <- lowerBoundY to upperBoundY 55 | maxy <- simulate(Probe(initial, Velocity(vx, vy)), target) 56 | yield 57 | maxy 58 | 59 | type Parser[A] = PartialFunction[String, A] 60 | 61 | val IntOf: Parser[Int] = 62 | case s if s.matches(raw"-?\d+") => s.toInt 63 | 64 | val RangeOf: Parser[Range] = 65 | case s"${IntOf(begin)}..${IntOf(end)}" => begin to end 66 | 67 | val Input: Parser[Target] = 68 | case s"target area: x=${RangeOf(xs)}, y=${RangeOf(ys)}" => Target(xs, ys) 69 | 70 | def part1(input: String) = 71 | allMaxHeights(Input(input.trim))(positiveOnly = true).max 72 | 73 | def part2(input: String) = 74 | allMaxHeights(Input(input.trim))(positiveOnly = false).size 75 | -------------------------------------------------------------------------------- /2021/src/day2.scala: -------------------------------------------------------------------------------- 1 | package day2 2 | 3 | import scala.io.Source 4 | 5 | @main def part1(): Unit = 6 | val input = util.Using.resource(Source.fromFile("input/day2"))(_.mkString) 7 | val answer = part1(input) 8 | println(s"The solution is $answer") 9 | 10 | @main def part2(): Unit = 11 | val input = util.Using.resource(Source.fromFile("input/day2"))(_.mkString) 12 | val answer = part2(input) 13 | println(s"The solution is $answer") 14 | 15 | def part1(input: String): Int = 16 | val entries = input.linesIterator.map(Command.from) 17 | val firstPosition = Position(0, 0) 18 | val lastPosition = entries.foldLeft(firstPosition)((position, direction) => 19 | position.move(direction) 20 | ) 21 | lastPosition.result 22 | 23 | def part2(input: String): Int = 24 | val entries = input.linesIterator.map(Command.from) 25 | val firstPosition = PositionWithAim(0, 0, 0) 26 | val lastPosition = entries.foldLeft(firstPosition)((position, direction) => 27 | position.move(direction) 28 | ) 29 | lastPosition.result 30 | 31 | case class PositionWithAim(horizontal: Int, depth: Int, aim: Int): 32 | def move(p: Command): PositionWithAim = 33 | p match 34 | case Command.Forward(x) => 35 | PositionWithAim(horizontal + x, depth + x * aim, aim) 36 | case Command.Down(x) => PositionWithAim(horizontal, depth, aim + x) 37 | case Command.Up(x) => PositionWithAim(horizontal, depth, aim - x) 38 | 39 | def result = horizontal * depth 40 | 41 | case class Position(horizontal: Int, depth: Int): 42 | def move(p: Command): Position = 43 | p match 44 | case Command.Forward(x) => Position(horizontal + x, depth) 45 | case Command.Down(x) => Position(horizontal, depth + x) 46 | case Command.Up(x) => Position(horizontal, depth - x) 47 | 48 | def result = horizontal * depth 49 | 50 | enum Command: 51 | case Forward(x: Int) 52 | case Down(x: Int) 53 | case Up(x: Int) 54 | 55 | object Command: 56 | def from(s: String): Command = 57 | s match 58 | case s"forward $x" if x.toIntOption.isDefined => Forward(x.toInt) 59 | case s"up $x" if x.toIntOption.isDefined => Up(x.toInt) 60 | case s"down $x" if x.toIntOption.isDefined => Down(x.toInt) 61 | case _ => throw new Exception(s"value $s is not valid command") 62 | -------------------------------------------------------------------------------- /2021/src/day20.scala: -------------------------------------------------------------------------------- 1 | package day20 2 | 3 | import scala.util.Using 4 | import scala.io.Source 5 | import scala.annotation.tailrec 6 | 7 | @main def part1(): Unit = 8 | println(s"The solution is ${part1(readInput())}") 9 | 10 | @main def part2(): Unit = 11 | println(s"The solution is ${part2(readInput())}") 12 | 13 | def readInput(): String = 14 | Using.resource(Source.fromFile("input/day20"))(_.mkString) 15 | 16 | def part1(input: String): Int = 17 | val (enhancer, image0) = parseEnhancerAndImage(input) 18 | val image1 = enhancer.enhance(image0) 19 | val image2 = enhancer.enhance(image1) 20 | image2.countLitPixels() 21 | 22 | def parseEnhancerAndImage(input: String): (Enhancer, Image) = 23 | val enhancerAndImage = input.split("\n\n") 24 | val enhancer = Enhancer.parse(enhancerAndImage(0)) 25 | val image = Image.parse(enhancerAndImage(1)) 26 | (enhancer, image) 27 | 28 | enum Pixel: 29 | case Lit, Dark 30 | 31 | object Pixel: 32 | 33 | def parse(char: Char): Pixel = 34 | char match 35 | case '#' => Pixel.Lit 36 | case '.' => Pixel.Dark 37 | 38 | end Pixel 39 | 40 | object Enhancer: 41 | 42 | def parse(input: String): Enhancer = 43 | Enhancer(input.map(Pixel.parse)) 44 | 45 | end Enhancer 46 | 47 | class Enhancer(enhancementString: IndexedSeq[Pixel]): 48 | 49 | def enhance(image: Image): Image = 50 | val pixels = 51 | for y <- -1 until (image.height + 1) 52 | yield 53 | for x <- -1 until (image.width + 1) 54 | yield enhancementString(locationValue(image, x, y)) 55 | 56 | val outOfBoundsPixel = 57 | val value = if image.outOfBoundsPixel == Pixel.Dark then 0 else 511 58 | enhancementString(value) 59 | 60 | Image(pixels, outOfBoundsPixel) 61 | end enhance 62 | 63 | private def locationValue(image: Image, x: Int, y: Int): Int = 64 | var result = 0 65 | for 66 | yy <- (y - 1) to (y + 1) 67 | xx <- (x - 1) to (x + 1) 68 | do 69 | result = result << 1 70 | if image.pixel(xx, yy) == Pixel.Lit then 71 | result = result | 1 72 | end if 73 | end for 74 | result 75 | end locationValue 76 | 77 | end Enhancer 78 | 79 | class Image(pixels: IndexedSeq[IndexedSeq[Pixel]], val outOfBoundsPixel: Pixel): 80 | 81 | require(pixels.map(_.length).distinct.size == 1, "All the rows must have the same length") 82 | 83 | val height = pixels.length 84 | val width = pixels(0).length 85 | 86 | def pixel(x: Int, y: Int): Pixel = 87 | if y < 0 || y >= height then outOfBoundsPixel 88 | else if x < 0 || x >= width then outOfBoundsPixel 89 | else pixels(y)(x) 90 | 91 | def countLitPixels(): Int = 92 | pixels.view.flatten.count(_ == Pixel.Lit) 93 | 94 | end Image 95 | 96 | object Image: 97 | def parse(input: String): Image = 98 | val pixels = 99 | input 100 | .linesIterator.map(line => line.map(Pixel.parse)) 101 | .toIndexedSeq 102 | val outOfBoundsPixel = Pixel.Dark 103 | Image(pixels, outOfBoundsPixel) 104 | 105 | def part2(input: String): Int = 106 | val (enhancer, image) = parseEnhancerAndImage(input) 107 | LazyList 108 | .iterate(image)(enhancer.enhance) 109 | .apply(50) 110 | .countLitPixels() 111 | -------------------------------------------------------------------------------- /2021/src/day21.scala: -------------------------------------------------------------------------------- 1 | package day21 2 | 3 | import scala.annotation.tailrec 4 | import scala.util.Using 5 | import scala.io.Source 6 | 7 | @main def part1(): Unit = 8 | val answer = part1(readInput()) 9 | println(s"The answer is: $answer") 10 | 11 | @main def part2(): Unit = 12 | val answer = part2(readInput()) 13 | println(s"The answer is:\n$answer") 14 | 15 | def readInput(): String = 16 | Using.resource(Source.fromFile("input/day21"))(_.mkString) 17 | 18 | type Cell = Int // from 0 to 9, to simplify computations 19 | 20 | case class Player(cell: Cell, score: Long) 21 | 22 | type Players = (Player, Player) 23 | 24 | final class DeterministicDie { 25 | var throwCount: Int = 0 26 | private var lastValue: Int = 100 27 | 28 | def nextResult(): Int = 29 | throwCount += 1 30 | lastValue = (lastValue % 100) + 1 31 | lastValue 32 | } 33 | 34 | def part1(input: String): Long = 35 | val players = parseInput(input) 36 | val die = new DeterministicDie 37 | val loserScore = playWithDeterministicDie(players, die) 38 | loserScore * die.throwCount 39 | 40 | def parseInput(input: String): Players = 41 | val lines = input.split("\n") 42 | (parsePlayer(lines(0)), parsePlayer(lines(1))) 43 | 44 | def parsePlayer(line: String): Player = 45 | line match 46 | case s"Player $num starting position: $cell" => 47 | Player(cell.toInt - 1, 0L) 48 | 49 | @tailrec 50 | def playWithDeterministicDie(players: Players, die: DeterministicDie): Long = 51 | val diesValue = die.nextResult() + die.nextResult() + die.nextResult() 52 | val player = players(0) 53 | val newCell = (player.cell + diesValue) % 10 54 | val newScore = player.score + (newCell + 1) 55 | if newScore >= 1000 then 56 | players(1).score 57 | else 58 | val newPlayer = Player(newCell, newScore) 59 | playWithDeterministicDie((players(1), newPlayer), die) 60 | 61 | final class Wins(var player1Wins: Long, var player2Wins: Long) 62 | 63 | def part2(input: String): Long = 64 | val players = parseInput(input) 65 | val wins = new Wins(0L, 0L) 66 | playWithDiracDie(players, player1Turn = true, wins, inHowManyUniverses = 1L) 67 | Math.max(wins.player1Wins, wins.player2Wins) 68 | 69 | /** For each 3-die throw, how many of each total sum do we have? */ 70 | val dieCombinations: List[(Int, Long)] = 71 | val possibleRolls: List[Int] = 72 | for 73 | die1 <- List(1, 2, 3) 74 | die2 <- List(1, 2, 3) 75 | die3 <- List(1, 2, 3) 76 | yield 77 | die1 + die2 + die3 78 | possibleRolls.groupMapReduce(identity)(_ => 1L)(_ + _).toList 79 | 80 | def playWithDiracDie(players: Players, player1Turn: Boolean, wins: Wins, inHowManyUniverses: Long): Unit = 81 | for (diesValue, count) <- dieCombinations do 82 | val newInHowManyUniverses = inHowManyUniverses * count 83 | val player = players(0) 84 | val newCell = (player.cell + diesValue) % 10 85 | val newScore = player.score + (newCell + 1) 86 | if newScore >= 21 then 87 | if player1Turn then 88 | wins.player1Wins += newInHowManyUniverses 89 | else 90 | wins.player2Wins += newInHowManyUniverses 91 | else 92 | val newPlayer = Player(newCell, newScore) 93 | playWithDiracDie((players(1), newPlayer), !player1Turn, wins, newInHowManyUniverses) 94 | end for 95 | -------------------------------------------------------------------------------- /2021/src/day22.scala: -------------------------------------------------------------------------------- 1 | // inspired by solution https://topaz.github.io/paste/#XQAAAQAeCAAAAAAAAAAzHIoib6p4r/McpYgEEgWhHoa5LSRMlQ+wWN2dSgFba+3OQLAQwPs2n+kH2p5lUvR4TJnE77lx5kFb6HW+mKObQM2MOxuv8Z0Ikuk8EytmoUVdCtg9KGX3EgBJtaZ1KUYGSX44w3DeWgEYnt4217TlxVAwn+5nGWeZWu11MdZIUg0S0DptNm4ymgHBUjFFBjfwS9dAE8lGus2C676rW5dFQTWsPSbX0KPErnY+Au6A3b83ioXEigKgv43S/YV5xd5UPogLgH1bRepf9ZNHvkHnlJ3Fd8zU5YiQjfY5gIE5QhyClAOLQAvcaeuqL1Lwi3AUVhbn0S6PoR9fI8CgSKgHxG/5OMMTxp0XmDbJSozSliQrG9gteNy/c9lwoX3ACTodcPOxhvJDpTAw5ZEJ47i/vPvBLkv+6/0vZYIveS86bML4r9niB2s0A5jOO+JzdYtkpZTnbm9eRXGZRjSdtCoXHwprlF308Xe+6+HHDqBhy7cGh5CwT9+SnwIVdPGJYQh3e5VAOI1I5+9bI+B2L91PDrMpszHrymcQHgJTbwLVPMyQ0oC2Gx5/2RDpcGyzxQVmOXvtmF47wD44rTALuAor27Bcc5HoPORpHW3ZJ8O3L/fz30m1bWv8EIYeWY1jdgX0lB98R7+bVHrqnzVsmKVAy+rIXnDqMJvojQF1a3Reetfg83JQTIbJoa+jnghFw/hYZ+thAB54sovYyutIFGGWx5JknARI3wngn+iEmbhxO3lM5Z8PiLES69y6erunAmEzXwlL6hMvTtx1znp3sp8GoYk4AyZJ/sFaukNpX4970vioZf+sZ+7rzJ4bKUiBc1fuebalSH2EJoT9Bkf33IU/OfkgZXgv067jeaY9Ktu+3oxELBs9Ea6g80BsTb3Xe33WaL1DUbpwTOw304VRILT9/dyVGg== 2 | 3 | package day22 4 | 5 | import scala.util.Using 6 | import scala.io.Source 7 | 8 | import Command.* 9 | import scala.collection.mutable.ListBuffer 10 | 11 | @main def part1(): Unit = 12 | println(s"The solution is ${part1(readInput())}") 13 | 14 | @main def part2(): Unit = 15 | println(s"The solution is ${part2(readInput())}") 16 | 17 | def readInput(): String = 18 | Using.resource(Source.fromFile("input/day22"))(_.mkString) 19 | 20 | case class Dimension(min: Int, max: Int): 21 | require(min <= max) 22 | 23 | def isSubset(d: Dimension): Boolean = 24 | min >= d.min && max <= d.max 25 | 26 | infix def insersect(d: Dimension): Option[Dimension] = 27 | Option.when(max >= d.min && min <= d.max) { 28 | (min max d.min) by (max min d.max) 29 | } 30 | 31 | def size: Int = max - min + 1 32 | 33 | extension (x1: Int) 34 | infix def by (x2: Int): Dimension = Dimension(x1, x2) 35 | 36 | case class Cuboid(xs: Dimension, ys: Dimension, zs: Dimension): 37 | 38 | def volume: BigInt = BigInt(xs.size) * ys.size * zs.size 39 | 40 | infix def intersect(curr: Cuboid): Option[Cuboid] = 41 | for 42 | xs <- this.xs insersect curr.xs 43 | ys <- this.ys insersect curr.ys 44 | zs <- this.zs insersect curr.zs 45 | yield 46 | Cuboid(xs, ys, zs) 47 | 48 | enum Command: 49 | case On, Off 50 | 51 | case class Step(command: Command, cuboid: Cuboid) 52 | 53 | def subdivide(old: Cuboid, hole: Cuboid): Set[Cuboid] = 54 | var on = Set.empty[Cuboid] 55 | if old.xs.min != hole.xs.min then 56 | on += Cuboid(xs = old.xs.min by hole.xs.min - 1, ys = old.ys, zs = old.zs) 57 | if old.xs.max != hole.xs.max then 58 | on += Cuboid(xs = hole.xs.max + 1 by old.xs.max, ys = old.ys, zs = old.zs) 59 | if old.ys.min != hole.ys.min then 60 | on += Cuboid(xs = hole.xs, ys = old.ys.min by hole.ys.min - 1, zs = old.zs) 61 | if old.ys.max != hole.ys.max then 62 | on += Cuboid(xs = hole.xs, ys = hole.ys.max + 1 by old.ys.max, zs = old.zs) 63 | if old.zs.min != hole.zs.min then 64 | on += Cuboid(xs = hole.xs, ys = hole.ys, zs = old.zs.min by hole.zs.min - 1) 65 | if old.zs.max != hole.zs.max then 66 | on += Cuboid(xs = hole.xs, ys = hole.ys, zs = hole.zs.max + 1 by old.zs.max) 67 | on 68 | 69 | def run(steps: Iterator[Step]): Set[Cuboid] = 70 | 71 | def subtract(cuboid: Cuboid)(on: Set[Cuboid], previouslyOn: Cuboid): Set[Cuboid] = 72 | previouslyOn intersect cuboid match 73 | case Some(hole) => 74 | on | subdivide(previouslyOn, hole) 75 | case _ => 76 | on + previouslyOn 77 | 78 | def turnOnCubes(on: Set[Cuboid], step: Step): Set[Cuboid] = 79 | val Step(command, cuboid) = step 80 | val newOn = if command == On then Set(cuboid) else Set.empty 81 | on.foldLeft(newOn)(subtract(cuboid)) 82 | 83 | steps.foldLeft(Set.empty)(turnOnCubes) 84 | 85 | def summary(on: Set[Cuboid]): BigInt = 86 | on.foldLeft(BigInt(0))((acc, cuboid) => acc + cuboid.volume) 87 | 88 | def challenge(steps: Iterator[Step], filter: Step => Boolean): BigInt = 89 | summary(run(steps.filter(filter))) 90 | 91 | def isInit(cuboid: Cuboid): Boolean = 92 | Seq(cuboid.xs, cuboid.ys, cuboid.zs).forall(_.isSubset(-50 by 50)) 93 | 94 | type Parser[A] = PartialFunction[String, A] 95 | 96 | val NumOf: Parser[Int] = 97 | case s if s.matches(raw"-?\d+") => s.toInt 98 | 99 | val DimensionOf: Parser[Dimension] = 100 | case s"${NumOf(begin)}..${NumOf(end)}" => begin by end 101 | 102 | val CuboidOf: Parser[Cuboid] = 103 | case s"x=${DimensionOf(xs)},y=${DimensionOf(ys)},z=${DimensionOf(zs)}" => Cuboid(xs, ys, zs) 104 | 105 | val CommandOf: Parser[Command] = 106 | case "on" => On 107 | case "off" => Off 108 | 109 | val StepOf: Parser[Step] = 110 | case s"${CommandOf(command)} ${CuboidOf(cuboid)}" => Step(command, cuboid) 111 | 112 | def part1(input: String): BigInt = 113 | challenge(input.linesIterator.map(StepOf), s => isInit(s.cuboid)) 114 | 115 | def part2(input: String): BigInt = 116 | challenge(input.linesIterator.map(StepOf), _ => true) 117 | -------------------------------------------------------------------------------- /2021/src/day23.scala: -------------------------------------------------------------------------------- 1 | package day23 2 | 3 | import scala.util.Using 4 | import scala.io.Source 5 | import scala.annotation.tailrec 6 | import scala.collection.mutable 7 | 8 | 9 | @main def part1(): Unit = 10 | val answer = part1(readInput()) 11 | println(s"The answer is: $answer") 12 | 13 | @main def part2(): Unit = 14 | val answer = part2(readInput()) 15 | println(s"The answer is: $answer") 16 | 17 | def readInput(): String = 18 | Using.resource(Source.fromFile("input/day23"))(_.mkString) 19 | 20 | case class Position(val x: Int, val y: Int) 21 | 22 | enum Room(val x: Int): 23 | case A extends Room(3) 24 | case B extends Room(5) 25 | case C extends Room(7) 26 | case D extends Room(9) 27 | 28 | type Energy = Int 29 | 30 | enum Amphipod(val energy: Energy, val destination: Room): 31 | case A extends Amphipod(1, Room.A) 32 | case B extends Amphipod(10, Room.B) 33 | case C extends Amphipod(100, Room.C) 34 | case D extends Amphipod(1000, Room.D) 35 | 36 | object Amphipod: 37 | def tryParse(input: Char): Option[Amphipod] = 38 | input match 39 | case 'A' => Some(Amphipod.A) 40 | case 'B' => Some(Amphipod.B) 41 | case 'C' => Some(Amphipod.C) 42 | case 'D' => Some(Amphipod.D) 43 | case _ => None 44 | 45 | val hallwayStops: Seq[Position] = Seq( 46 | Position(1, 1), 47 | Position(2, 1), 48 | Position(4, 1), 49 | Position(6, 1), 50 | Position(8, 1), 51 | Position(10, 1), 52 | Position(11, 1) 53 | ) 54 | 55 | case class Situation(positions: Map[Position, Amphipod], roomSize: Int): 56 | def moveAllAmphipodsOnce: Seq[(Situation, Energy)] = 57 | for 58 | (start, amphipod) <- positions.toSeq 59 | stop <- nextStops(amphipod, start) 60 | path = getPath(start, stop) 61 | if path.forall(isEmpty) 62 | yield 63 | val newPositions = positions - start + (stop -> amphipod) 64 | val energy = path.size * amphipod.energy 65 | (copy(positions = newPositions), energy) 66 | 67 | def isFinal = 68 | positions.forall((position, amphipod) => position.x == amphipod.destination.x) 69 | 70 | /** 71 | * Return a list of positions to which an amphipod at position `from` can go: 72 | * - If the amphipod is in its destination room and the room is free it must not go anywhere. 73 | * - If the amphipod is in its destination room and the room is not free it can go to the hallway. 74 | * - If the amphipod is in the hallway it can only go to its destination. 75 | * - Otherwise it can go to the hallway. 76 | */ 77 | private def nextStops(amphipod: Amphipod, from: Position): Seq[Position] = 78 | from match 79 | case Position(x, y) if x == amphipod.destination.x => 80 | if isDestinationFree(amphipod) then Seq.empty 81 | else hallwayStops 82 | case Position(_, 1) => 83 | if isDestinationFree(amphipod) then 84 | (roomSize + 1).to(2, step = -1) 85 | .map(y => Position(amphipod.destination.x, y)) 86 | .find(isEmpty) 87 | .toSeq 88 | else Seq.empty 89 | case _ => hallwayStops 90 | 91 | 92 | private def isDestinationFree(amphipod: Amphipod): Boolean = 93 | 2.to(roomSize + 1) 94 | .flatMap(y => positions.get(Position(amphipod.destination.x, y))) 95 | .forall(_ == amphipod) 96 | 97 | // Build the path to go from `start` to `stop` 98 | private def getPath(start: Position, stop: Position): Seq[Position] = 99 | val hallway = 100 | if start.x < stop.x 101 | then (start.x + 1).to(stop.x).map(Position(_, 1)) 102 | else (start.x - 1).to(stop.x, step = -1).map(Position(_, 1)) 103 | val startRoom = (start.y - 1).to(1, step = -1).map(Position(start.x, _)) 104 | val stopRoom = 2.to(stop.y).map(Position(stop.x, _)) 105 | startRoom ++ hallway ++ stopRoom 106 | 107 | private def isEmpty(position: Position) = 108 | !positions.contains(position) 109 | 110 | object Situation: 111 | def parse(input: String, roomSize: Int): Situation = 112 | val positions = 113 | for 114 | (line, y) <- input.linesIterator.zipWithIndex 115 | (char, x) <- line.zipWithIndex 116 | amphipod <- Amphipod.tryParse(char) 117 | yield Position(x, y) -> amphipod 118 | Situation(positions.toMap, roomSize) 119 | 120 | class DijkstraSolver(initialSituation: Situation): 121 | private val bestSituations = mutable.Map(initialSituation -> 0) 122 | private val situationsToExplore = 123 | mutable.PriorityQueue((initialSituation, 0))(Ordering.by((_, energy) => -energy)) 124 | 125 | @tailrec 126 | final def solve(): Energy = 127 | val (situation, energy) = situationsToExplore.dequeue 128 | if situation.isFinal then energy 129 | else if bestSituations(situation) < energy then solve() 130 | else 131 | for 132 | (nextSituation, consumedEnergy) <- situation.moveAllAmphipodsOnce 133 | nextEnergy = energy + consumedEnergy 134 | knownEnergy = bestSituations.getOrElse(nextSituation, Int.MaxValue) 135 | if nextEnergy < knownEnergy 136 | do 137 | bestSituations.update(nextSituation, nextEnergy) 138 | situationsToExplore.enqueue((nextSituation, nextEnergy)) 139 | solve() 140 | 141 | def part1(input: String): Energy = 142 | val initialSituation = Situation.parse(input, roomSize = 2) 143 | DijkstraSolver(initialSituation).solve() 144 | 145 | def part2(input: String): Energy = 146 | val lines = input.linesIterator 147 | val unfoldedInput = (lines.take(3) ++ Seq(" #D#C#B#A#", " #D#B#A#C#") ++ lines.take(2)).mkString("\n") 148 | val initialSituation = Situation.parse(unfoldedInput, roomSize = 4) 149 | DijkstraSolver(initialSituation).solve() 150 | -------------------------------------------------------------------------------- /2021/src/day25.scala: -------------------------------------------------------------------------------- 1 | package day25 2 | 3 | import scala.util.Using 4 | import scala.io.Source 5 | 6 | @main def part1(): Unit = 7 | val answer = part1(readInput()) 8 | println(s"The answer is: $answer") 9 | 10 | def readInput(): String = 11 | Using.resource(Source.fromFile("input/day25"))(_.mkString) 12 | 13 | enum SeaCucumber: 14 | case Empty, East, South 15 | 16 | object SeaCucumber: 17 | def fromChar(c: Char) = c match 18 | case '.' => Empty 19 | case '>' => East 20 | case 'v' => South 21 | 22 | type Board = Seq[Seq[SeaCucumber]] 23 | 24 | def part1(input: String): Int = 25 | val board: Board = input.linesIterator.map(_.map(SeaCucumber.fromChar(_))).toSeq 26 | fixedPoint(board) 27 | 28 | def fixedPoint(board: Board, step: Int = 1): Int = 29 | val next = move(board) 30 | if board == next then step else fixedPoint(next, step + 1) 31 | 32 | def move(board: Board) = moveSouth(moveEast(board)) 33 | def moveEast(board: Board) = moveImpl(board, SeaCucumber.East) 34 | def moveSouth(board: Board) = moveImpl(board.transpose, SeaCucumber.South).transpose 35 | 36 | def moveImpl(board: Board, cucumber: SeaCucumber): Board = 37 | board.map { l => 38 | zip3(l.last +: l.init, l, (l.tail :+ l.head)).map{ 39 | case (`cucumber`, SeaCucumber.Empty, _) => `cucumber` 40 | case (_, `cucumber`, SeaCucumber.Empty) => SeaCucumber.Empty 41 | case (_, curr, _) => curr 42 | } 43 | } 44 | 45 | def zip3[A,B,C](l1: Seq[A], l2: Seq[B], l3: Seq[C]): Seq[(A,B,C)] = 46 | l1.zip(l2).zip(l3).map { case ((a, b), c) => (a,b,c) } 47 | -------------------------------------------------------------------------------- /2021/src/day3.scala: -------------------------------------------------------------------------------- 1 | //> using target.platform "scala-js" 2 | //> using jsModuleKind "common" 3 | 4 | package day3 5 | 6 | import scala.scalajs.js 7 | import scala.scalajs.js.annotation._ 8 | 9 | @main def part1(): Unit = 10 | val input = readFileSync("input/day3", "utf-8") 11 | val answer = part1(input) 12 | println(s"The solution is $answer") 13 | 14 | @main def part2(): Unit = 15 | val input = readFileSync("input/day3", "utf-8") 16 | val answer = part2(input) 17 | println(s"The solution is $answer") 18 | 19 | @js.native @JSImport("fs", "readFileSync") 20 | def readFileSync(path: String, charset: String): String = js.native 21 | 22 | def part1(input: String): Int = 23 | val bitLines: List[BitLine] = input.linesIterator.map(parseBitLine).toList 24 | 25 | val sumsOfOneBits: IndexedSeq[Int] = bitLines.reduceLeft((prevSum, line) => 26 | for ((prevBitSum, lineBit) <- prevSum.zip(line)) 27 | yield prevBitSum + lineBit 28 | ) 29 | val total = bitLines.size // this will walk the list a second time, but that's OK 30 | 31 | val gammaRateBits: BitLine = 32 | for (sumOfOneBits <- sumsOfOneBits) 33 | yield (if (sumOfOneBits * 2 > total) 1 else 0) 34 | val gammaRate = bitLineToInt(gammaRateBits) 35 | 36 | val epsilonRateBits: BitLine = 37 | for (sumOfOneBits <- sumsOfOneBits) 38 | yield (if (sumOfOneBits * 2 < total) 1 else 0) 39 | val epsilonRate = bitLineToInt(epsilonRateBits) 40 | 41 | gammaRate * epsilonRate 42 | 43 | type BitLine = IndexedSeq[Int] 44 | 45 | def parseBitLine(line: String): BitLine = 46 | line.map(c => c - '0') // 1 or 0 47 | 48 | def bitLineToInt(bitLine: BitLine): Int = 49 | Integer.parseInt(bitLine.mkString, 2) 50 | 51 | def part2(input: String): Int = 52 | val bitLines: List[BitLine] = input.linesIterator.map(parseBitLine).toList 53 | 54 | val oxygenGeneratorRatingLine: BitLine = 55 | recursiveFilter(bitLines, 0, keepMostCommon = true) 56 | val oxygenGeneratorRating = bitLineToInt(oxygenGeneratorRatingLine) 57 | 58 | val co2ScrubberRatingLine: BitLine = 59 | recursiveFilter(bitLines, 0, keepMostCommon = false) 60 | val co2ScrubberRating = bitLineToInt(co2ScrubberRatingLine) 61 | 62 | oxygenGeneratorRating * co2ScrubberRating 63 | 64 | @scala.annotation.tailrec 65 | def recursiveFilter(bitLines: List[BitLine], bitPosition: Int, 66 | keepMostCommon: Boolean): BitLine = 67 | bitLines match 68 | case Nil => 69 | throw new AssertionError("this shouldn't have happened") 70 | case lastRemainingLine :: Nil => 71 | lastRemainingLine 72 | case _ => 73 | val (bitLinesWithOne, bitLinesWithZero) = 74 | bitLines.partition(line => line(bitPosition) == 1) 75 | val onesAreMostCommon = bitLinesWithOne.sizeCompare(bitLinesWithZero) >= 0 76 | val bitLinesToKeep = 77 | if onesAreMostCommon then 78 | if keepMostCommon then bitLinesWithOne else bitLinesWithZero 79 | else 80 | if keepMostCommon then bitLinesWithZero else bitLinesWithOne 81 | recursiveFilter(bitLinesToKeep, bitPosition + 1, keepMostCommon) 82 | -------------------------------------------------------------------------------- /2021/src/day4.scala: -------------------------------------------------------------------------------- 1 | package day4 2 | 3 | import scala.io.Source 4 | 5 | @main def run(): Unit = 6 | val input = util.Using.resource(Source.fromFile("input/day4"))(_.mkString) 7 | val (part1, part2) = answers(input) 8 | println(s"The answer of part 1 is $part1.") 9 | println(s"The answer of part 2 is $part2.") 10 | 11 | case class Board(lines: List[List[Int]]): 12 | def mapNumbers(f: Int => Int): Board = Board(lines.map(_.map(f))) 13 | def columns: List[List[Int]] = lines.transpose 14 | 15 | object Board: 16 | def parse(inputBoard: String): Board = 17 | val numberParser = raw"\d+".r 18 | def parseLine(inputLine: String): List[Int] = 19 | numberParser.findAllIn(inputLine).toList.map(_.toInt) 20 | 21 | val lines = inputBoard.split('\n').toList 22 | Board(lines.map(parseLine)) 23 | end parse 24 | 25 | def answers(input: String): (Int, Int) = 26 | val inputSections: List[String] = input.split("\n\n").toList 27 | val numbers: List[Int] = inputSections.head.split(',').map(_.toInt).toList 28 | 29 | val originalBoards: List[Board] = inputSections.tail.map(Board.parse) 30 | 31 | val numberToTurn = numbers.zipWithIndex.toMap 32 | val turnToNumber = numberToTurn.map((number, turn) => (turn, number)) 33 | 34 | val boards = originalBoards.map(board => board.mapNumbers(numberToTurn)) 35 | 36 | def winningTurn(board: Board): Int = 37 | val lineMin = board.lines.map(line => line.max).min 38 | val colMin = board.columns.map(col => col.max).min 39 | lineMin min colMin 40 | 41 | // for each board, the number of turns until it wins 42 | val winningTurns: List[(Board, Int)] = 43 | boards.map(board => (board, winningTurn(board))) 44 | 45 | def score(board: Board, turn: Int) = 46 | val sumNumsNotDrawn = 47 | board.lines.map{ line => 48 | line.filter(_ > turn).map(turnToNumber(_)).sum 49 | }.sum 50 | turnToNumber(turn) * sumNumsNotDrawn 51 | end score 52 | 53 | val (winnerBoard, winnerTurn) = winningTurns.minBy((_, turn) => turn) 54 | val winnerScore = score(winnerBoard, winnerTurn) 55 | 56 | val (loserBoard, loserTurn) = winningTurns.maxBy((_, turn) => turn) 57 | val loserScore = score(loserBoard, loserTurn) 58 | 59 | (winnerScore, loserScore) 60 | -------------------------------------------------------------------------------- /2021/src/day5.scala: -------------------------------------------------------------------------------- 1 | package day5 2 | 3 | import scala.util.Using 4 | import scala.io.Source 5 | import scala.collection.mutable 6 | 7 | @main def part1(): Unit = 8 | val answer = part1(readInput()) 9 | println(s"The answer is: $answer") 10 | 11 | @main def part2(): Unit = 12 | val answer = part2(readInput()) 13 | println(s"The answer is: $answer") 14 | 15 | def readInput(): String = 16 | Using.resource(Source.fromFile("input/day5"))(_.mkString) 17 | 18 | case class Point(x: Int, y: Int) 19 | 20 | object Point: 21 | def apply(str: String): Point = 22 | str.split(",") match 23 | case Array(start, end) => new Point(start.trim.toInt, end.trim.toInt) 24 | case _ => 25 | throw new java.lang.IllegalArgumentException(s"Wrong point input $str") 26 | 27 | case class Vent(start: Point, end: Point) 28 | 29 | object Vent: 30 | def apply(str: String) = 31 | str.split("->") match 32 | case Array(start, end) => 33 | new Vent(Point(start), Point(end)) 34 | case _ => 35 | throw new java.lang.IllegalArgumentException(s"Wrong vent input $str") 36 | 37 | def findDangerousPoints(vents: Seq[Vent]): Int = 38 | val map = mutable.Map[Point, Int]().withDefaultValue(0) 39 | def update(p: Point) = 40 | val current = map(p) 41 | map.update(p, current + 1) 42 | 43 | for vent <- vents do 44 | def rangex = 45 | val stepx = if vent.end.x > vent.start.x then 1 else -1 46 | vent.start.x.to(vent.end.x, stepx) 47 | def rangey = 48 | val stepy = if vent.end.y > vent.start.y then 1 else -1 49 | vent.start.y.to(vent.end.y, stepy) 50 | if vent.start.x == vent.end.x then 51 | for py <- rangey do update(Point(vent.start.x, py)) 52 | else if vent.start.y == vent.end.y then 53 | for px <- rangex do update(Point(px, vent.start.y)) 54 | else for (px, py) <- rangex.zip(rangey) do update(Point(px, py)) 55 | end for 56 | 57 | map.count { case (_, v) => v > 1 } 58 | end findDangerousPoints 59 | 60 | def part1(input: String): Int = 61 | val onlySimple = input.linesIterator 62 | .map(Vent.apply) 63 | .filter(v => v.start.x == v.end.x || v.start.y == v.end.y) 64 | .toSeq 65 | findDangerousPoints(onlySimple) 66 | 67 | def part2(input: String): Int = 68 | val allVents = input.linesIterator.map(Vent.apply).toSeq 69 | findDangerousPoints(allVents) 70 | -------------------------------------------------------------------------------- /2021/src/day6.scala: -------------------------------------------------------------------------------- 1 | package day6 2 | 3 | import scala.util.Using 4 | import scala.collection.mutable 5 | import scala.io.Source 6 | 7 | @main def part1(): Unit = 8 | println(s"The solution is ${part1(readInput())}") 9 | 10 | @main def part2(): Unit = 11 | println(s"The solution is ${part2(readInput())}") 12 | 13 | def readInput(): String = 14 | Using.resource(Source.fromFile("input/day6"))(_.mkString) 15 | 16 | // "Find a way to simulate lanternfish. How many lanternfish would there be after 80 17 | // days?" 18 | def part1(input: String): Int = 19 | simulate( 20 | days = 80, 21 | initialPopulation = Fish.parseSeveral(input) 22 | ) 23 | 24 | // "You can model each fish as a single number that represents the number of days 25 | // until it creates a new lanternfish." 26 | case class Fish(timer: Int) 27 | 28 | object Fish: 29 | // "Suppose you were given the following list: 30 | // 31 | // 3,4,3,1,2 32 | // 33 | // This list means that the first fish has an internal timer of 3, the second fish 34 | // has an internal timer of 4, and so on until the fifth fish, which has an 35 | // internal timer of 2." 36 | def parseSeveral(input: String): Seq[Fish] = 37 | for timerString <- input.trim.split(",").toIndexedSeq 38 | yield Fish(timerString.toInt.ensuring(timer => timer >= 0 && timer <= 8)) 39 | 40 | /** 41 | * Simulate the evolution of the population and return the number 42 | * of fishes at the end of the simulation. 43 | * @param days Number of days to simulate 44 | * @param initialPopulation Initial population 45 | */ 46 | def simulate(days: Int, initialPopulation: Seq[Fish]): Int = 47 | (1 to days) 48 | .foldLeft(initialPopulation)((population, _) => tick(population)) 49 | .size 50 | 51 | /** 52 | * Compute a new population after one day passes. 53 | * @param population Current population 54 | * @return New population 55 | */ 56 | def tick(population: Seq[Fish]): Seq[Fish] = 57 | population.flatMap { fish => 58 | // "Each day, a `0` becomes a `6` and adds a new `8` to the end of the list" 59 | if fish.timer == 0 then 60 | Seq(Fish(6), Fish(8)) 61 | // "while each other number decreases by 1" 62 | else 63 | Seq(Fish(fish.timer - 1)) 64 | } 65 | 66 | // "How many lanternfish would there be after 256 days?" 67 | def part2(input: String): BigInt = 68 | simulate( 69 | days = 256, 70 | Fish.parseSeveral(input).groupMapReduce(_.timer)(_ => BigInt(1))(_ + _) 71 | ) 72 | 73 | def simulate(days: Int, initialPopulation: Map[Int, BigInt]): BigInt = 74 | (1 to days) 75 | .foldLeft(initialPopulation)((population, _) => tick(population)) 76 | .values 77 | .sum 78 | 79 | def tick(population: Map[Int, BigInt]): Map[Int, BigInt] = 80 | def countPopulation(daysLeft: Int): BigInt = population.getOrElse(daysLeft, BigInt(0)) 81 | Map( 82 | 0 -> countPopulation(1), 83 | 1 -> countPopulation(2), 84 | 2 -> countPopulation(3), 85 | 3 -> countPopulation(4), 86 | 4 -> countPopulation(5), 87 | 5 -> countPopulation(6), 88 | 6 -> (countPopulation(7) + countPopulation(0)), 89 | 7 -> countPopulation(8), 90 | 8 -> countPopulation(0) 91 | ) 92 | -------------------------------------------------------------------------------- /2021/src/day7.scala: -------------------------------------------------------------------------------- 1 | package day7 2 | 3 | import scala.util.Using 4 | import scala.io.Source 5 | import scala.annotation.tailrec 6 | 7 | @main def part1(): Unit = 8 | println(s"The solution is ${part1(readInput())}") 9 | 10 | @main def part2(): Unit = 11 | println(s"The solution is ${part2(readInput())}") 12 | 13 | def readInput(): String = 14 | Using.resource(Source.fromFile("input/day7"))(_.mkString) 15 | 16 | sealed trait Crabmarine: 17 | def moveForward(): Crabmarine 18 | def moveBackward(): Crabmarine 19 | def horizontal: Int 20 | def fuelCost: Int 21 | 22 | case class ConstantCostCrabmarine(horizontal: Int) extends Crabmarine: 23 | def fuelCost: Int = 1 24 | def moveForward(): Crabmarine = this.copy(horizontal = horizontal + 1) 25 | def moveBackward(): Crabmarine = this.copy(horizontal = horizontal - 1) 26 | 27 | case class IncreasingCostCrabmarine(horizontal: Int, fuelCost: Int = 1) 28 | extends Crabmarine: 29 | def moveForward() = 30 | this.copy(horizontal = horizontal + 1, fuelCost = fuelCost + 1) 31 | def moveBackward() = 32 | this.copy(horizontal = horizontal - 1, fuelCost = fuelCost + 1) 33 | 34 | case class Crabmada(crabmarines: List[Crabmarine]): 35 | 36 | require(crabmarines.nonEmpty) 37 | 38 | @tailrec 39 | final def align( 40 | situation: List[Crabmarine] = crabmarines, 41 | fuelCost: Int = 0 42 | ): Int = 43 | val allTheSame = situation.forall(_.horizontal == situation.head.horizontal) 44 | if allTheSame then fuelCost 45 | else 46 | val maxHorizontal = situation.maxBy(_.horizontal) 47 | val minHorizontal = situation.minBy(_.horizontal) 48 | 49 | val fuelCostForMax = situation.collect { 50 | case crabmarine if crabmarine.horizontal == maxHorizontal.horizontal => 51 | crabmarine.fuelCost 52 | }.sum 53 | val fuelCostForMin = situation.collect { 54 | case crabmarine if crabmarine.horizontal == minHorizontal.horizontal => 55 | crabmarine.fuelCost 56 | }.sum 57 | if fuelCostForMax < fuelCostForMin then 58 | val updated = situation.map { crabmarine => 59 | if crabmarine.horizontal == maxHorizontal.horizontal then 60 | crabmarine.moveBackward() 61 | else crabmarine 62 | } 63 | align(updated, fuelCost + fuelCostForMax) 64 | else 65 | 66 | val updated = situation.map { crabmarine => 67 | if crabmarine.horizontal == minHorizontal.horizontal then 68 | crabmarine.moveForward() 69 | else crabmarine 70 | } 71 | align(updated, fuelCost + fuelCostForMin) 72 | end if 73 | end if 74 | end align 75 | end Crabmada 76 | 77 | def part1(input: String): Int = 78 | val horizontalPositions = input.split(",").map(_.toInt).toList 79 | val crabmarines = 80 | horizontalPositions.map(horizontal => ConstantCostCrabmarine(horizontal)) 81 | Crabmada(crabmarines).align() 82 | 83 | def part2(input: String): Int = 84 | val horizontalPositions = input.split(",").map(_.toInt).toList 85 | val crabmarines = 86 | horizontalPositions.map(horizontal => IncreasingCostCrabmarine(horizontal)) 87 | Crabmada(crabmarines).align() 88 | -------------------------------------------------------------------------------- /2021/src/day8.scala: -------------------------------------------------------------------------------- 1 | package day8 2 | 3 | import scala.util.Using 4 | import scala.util.chaining.* 5 | import scala.io.Source 6 | 7 | import Segment.* 8 | import Digit.* 9 | 10 | def readInput(): String = 11 | Using.resource(Source.fromFile("input/day8"))(_.mkString) 12 | 13 | @main def part1: Unit = 14 | println(s"The solution is ${part1(readInput())}") 15 | 16 | @main def part2: Unit = 17 | println(s"The solution is ${part2(readInput())}") 18 | 19 | enum Segment: 20 | case A, B, C, D, E, F, G 21 | 22 | val char = toString.head.toLower 23 | 24 | object Segment: 25 | type Segments = Set[Segment] 26 | 27 | val fromChar: Map[Char, Segment] = values.map(s => s.char -> s).toMap 28 | 29 | def parseSegments(s: String): Segments = 30 | s.map(fromChar).toSet 31 | 32 | end Segment 33 | 34 | enum Digit(val segments: Segment*): 35 | case Zero extends Digit(A, B, C, E, F, G) 36 | case One extends Digit(C, F) 37 | case Two extends Digit(A, C, D, E, G) 38 | case Three extends Digit(A, C, D, F, G) 39 | case Four extends Digit(B, C, D, F) 40 | case Five extends Digit(A, B, D, F, G) 41 | case Six extends Digit(A, B, D, E, F, G) 42 | case Seven extends Digit(A, C, F) 43 | case Eight extends Digit(A, B, C, D, E, F, G) 44 | case Nine extends Digit(A, B, C, D, F, G) 45 | 46 | object Digit: 47 | 48 | val index: IndexedSeq[Digit] = values.toIndexedSeq 49 | 50 | private val uniqueLookup: Map[Int, Digit] = 51 | index.groupBy(_.segments.size).collect { case k -> Seq(d) => k -> d } 52 | 53 | def lookupUnique(segments: Segments): Option[Digit] = 54 | uniqueLookup.get(segments.size) 55 | 56 | end Digit 57 | 58 | def part1(input: String): Int = 59 | 60 | def getDisplay(line: String): String = line.split('|')(1).trim 61 | 62 | def parseUniqueDigit(s: String): Option[Digit] = 63 | Digit.lookupUnique(Segment.parseSegments(s)) 64 | 65 | val uniqueDigits: Iterator[Digit] = 66 | for 67 | display <- input.linesIterator.map(getDisplay) 68 | segments <- display.split(" ") 69 | uniqueDigit <- parseUniqueDigit(segments) 70 | yield 71 | uniqueDigit 72 | 73 | uniqueDigits.size 74 | end part1 75 | 76 | def part2(input: String): Int = 77 | 78 | def parseSegmentsSeq(segments: String): Seq[Segments] = 79 | segments.trim.split(" ").toSeq.map(Segment.parseSegments) 80 | 81 | def splitParts(line: String): (Seq[Segments], Seq[Segments]) = 82 | val Array(cipher, plaintext) = line.split('|').map(parseSegmentsSeq) 83 | (cipher, plaintext) 84 | 85 | def digitsToInt(digits: Seq[Digit]): Int = 86 | digits.foldLeft(0)((acc, d) => acc * 10 + d.ordinal) 87 | 88 | val problems = input.linesIterator.map(splitParts) 89 | 90 | val solutions = problems.map((cipher, plaintext) => 91 | plaintext.map(substitutions(cipher)) 92 | ) 93 | 94 | solutions.map(digitsToInt).sum 95 | 96 | end part2 97 | 98 | def substitutions(cipher: Seq[Segments]): Map[Segments, Digit] = 99 | 100 | def lookup(section: Seq[Segments], withSegments: Segments): (Segments, Seq[Segments]) = 101 | val (Seq(uniqueMatch), remaining) = section.partition(withSegments.subsetOf) 102 | (uniqueMatch, remaining) 103 | 104 | val uniques: Map[Digit, Segments] = 105 | Map.from( 106 | for 107 | segments <- cipher 108 | digit <- Digit.lookupUnique(segments) 109 | yield 110 | digit -> segments 111 | ) 112 | 113 | val ofSizeFive = cipher.filter(_.sizeIs == 5) 114 | val ofSizeSix = cipher.filter(_.sizeIs == 6) 115 | 116 | val one = uniques(One) 117 | val four = uniques(Four) 118 | val seven = uniques(Seven) 119 | val eight = uniques(Eight) 120 | val (three, remainingFives) = lookup(ofSizeFive, withSegments = one) 121 | val (nine, remainingSixes) = lookup(ofSizeSix, withSegments = three) 122 | val (zero, Seq(six)) = lookup(remainingSixes, withSegments = seven) 123 | val (five, Seq(two)) = lookup(remainingFives, withSegments = four &~ one) 124 | 125 | val decode: Map[Segments, Digit] = 126 | Seq(zero, one, two, three, four, five, six, seven, eight, nine) 127 | .zip(Digit.index) 128 | .toMap 129 | 130 | decode 131 | end substitutions 132 | -------------------------------------------------------------------------------- /2021/src/day9.scala: -------------------------------------------------------------------------------- 1 | package day9 2 | 3 | import scala.collection.immutable.Queue 4 | import scala.util.Using 5 | import scala.io.Source 6 | 7 | @main def part1(): Unit = 8 | println(s"The solution is ${part1(readInput())}") 9 | 10 | @main def part2(): Unit = 11 | println(s"The solution is ${part2(readInput())}") 12 | 13 | def readInput(): String = 14 | Using.resource(Source.fromFile("input/day9"))(_.mkString) 15 | 16 | type Height = Int 17 | case class Position(x: Int, y: Int) 18 | 19 | case class Heightmap(width: Int, height: Int, data: Vector[Vector[Height]]): 20 | 21 | def apply(pos: Position): Height = data(pos.y)(pos.x) 22 | 23 | def neighborsOf(pos: Position): List[(Position, Height)] = 24 | val Position(x, y) = pos 25 | List( 26 | Option.when(x > 0)(Position(x - 1, y)), 27 | Option.when(x < width - 1)(Position(x + 1, y)), 28 | Option.when(y > 0)(Position(x, y - 1)), 29 | Option.when(y < height - 1)(Position(x, y + 1)) 30 | ).flatMap(List.from) 31 | .map(pos => pos -> apply(pos)) 32 | 33 | def lowPointsPositions: LazyList[Position] = 34 | LazyList.range(0, height).flatMap { y => 35 | LazyList.range(0, width).map { x => 36 | val pos = Position(x, y) 37 | ( 38 | apply(pos), 39 | pos, 40 | this.neighborsOf(pos).map(_._2) 41 | ) 42 | } 43 | } 44 | .collect { 45 | case (value, poss, neighbors) if neighbors.forall(value < _) => 46 | poss 47 | } 48 | end Heightmap 49 | 50 | 51 | object Heightmap: 52 | def fromString(raw: String): Heightmap = 53 | val data = raw.linesIterator.map(line => line.map(_.asDigit).toVector).toVector 54 | Heightmap(data(0).length, data.length, data) 55 | end Heightmap 56 | 57 | 58 | def drawGrid(height: Int, width: Int, data: (Position, Height)*) = 59 | val provided: Map[Position, Height] = data.toMap 60 | val result = StringBuilder() 61 | for y <- 0 until height do 62 | for x <- 0 until width do 63 | val value = provided.getOrElse(Position(x, y), -1) 64 | result.append(if value >= 0 then value else '-') 65 | result.append('\n') 66 | result.toString 67 | 68 | def part1(input: String): Int = 69 | val heightMap = Heightmap.fromString(input) 70 | 71 | heightMap.lowPointsPositions.map(heightMap(_) + 1).sum 72 | end part1 73 | 74 | 75 | def basin(lowPoint: Position, heightMap: Heightmap): Set[Position] = 76 | @scala.annotation.tailrec 77 | def iter(visited: Set[Position], toVisit: Queue[Position], basinAcc: Set[Position]): Set[Position] = 78 | if toVisit.isEmpty then basinAcc 79 | else 80 | val (currentPos, remaining) = toVisit.dequeue 81 | val newNodes = heightMap.neighborsOf(currentPos).toList.collect { 82 | case (pos, height) if !visited(currentPos) && height != 9 => pos 83 | } 84 | iter(visited + currentPos, remaining ++ newNodes, basinAcc ++ newNodes) 85 | 86 | iter(Set.empty, Queue(lowPoint), Set(lowPoint)) 87 | 88 | 89 | def part2(input: String): Int = 90 | val heightMap = Heightmap.fromString(input) 91 | val lowPoints = heightMap.lowPointsPositions 92 | val basins = lowPoints.map(basin(_, heightMap)) 93 | 94 | basins 95 | .to(LazyList) 96 | .map(_.size) 97 | .sorted(Ordering[Int].reverse) 98 | .take(3) 99 | .product 100 | 101 | end part2 102 | -------------------------------------------------------------------------------- /2022/README.md: -------------------------------------------------------------------------------- 1 | # Scala Advent of Code 2022 2 | 3 | Solutions in Scala for the annual [Advent of Code](https://adventofcode.com/) challenge. _Note: this repo is not affiliated with Advent of Code._ 4 | 5 | > See earlier editions: 6 | > - [2021](/2021/README.md) 7 | 8 | ## Website 9 | 10 | The [Scala Advent of Code](https://scalacenter.github.io/scala-advent-of-code/) website contains: 11 | - some explanation of our solutions to [Advent of Code (adventofcode.com)](https://adventofcode.com/) 12 | - more solutions from the community 13 | 14 | ## Setup 15 | 16 | We use Visual Studio Code with Metals to write Scala code, and scala-cli to compile and run it. 17 | 18 | You can follow these [steps](https://scalacenter.github.io/scala-advent-of-code/setup) to set up your environement. 19 | 20 | ### How to open in Visual Studio Code 21 | 22 | After you clone the repository, open a terminal and run: 23 | ``` 24 | $ cd scala-advent-of-code 25 | $ scala-cli setup-ide 2022 26 | $ mkdir 2022/input 27 | $ code 2022 28 | ``` 29 | 30 | `code 2022` will open Visual Studio Code and start Metals. If not you may have to go to the Metals pane and click 31 | the button labelled "Start Metals". 32 | 33 | When you navigate to a file, e.g. `2022/src/day01.scala` metals should index the project, and then display code lenses 34 | above each of the main methods `part1` and `part2`, as shown in this image: 35 | ![](img/code-lenses.png) 36 | 37 | To run a solution, first copy your input to the folder `2022/input`. 38 | Then click `run` in VS Code which will run the code and display the results of the program. Or `debug`, 39 | which will let you pause on breakpoints, and execute expressions in the debug console. 40 | 41 | ### How to run a solution with command line 42 | 43 | In a terminal you can run: 44 | ``` 45 | $ scala-cli 2022 -M day01.part01 46 | Compiling project (Scala 3.x.y, JVM) 47 | Compiled project (Scala 3.x.y, JVM) 48 | The solution is 64929 49 | ``` 50 | 51 | Or, to run another solution: 52 | ``` 53 | $ scala-cli 2022 -M . 54 | ``` 55 | 56 | By default the solution programs run on our input files which are stored in the `2022/input` folder. 57 | To get your solutions you can change the content of those files in the `2022/input` folder. 58 | 59 | ## Contributing 60 | - Please do not commit your puzzle inputs, we can not accept them as they are protected by copyright 61 | -------------------------------------------------------------------------------- /2022/project.scala: -------------------------------------------------------------------------------- 1 | //> using scala 3.3.1 2 | -------------------------------------------------------------------------------- /2022/src/day01.scala: -------------------------------------------------------------------------------- 1 | package day01 2 | 3 | import scala.math.Ordering 4 | 5 | import locations.Directory.currentDir 6 | import inputs.Input.loadFileSync 7 | 8 | @main def part1: Unit = 9 | println(s"The solution is ${part1(loadInput())}") 10 | 11 | @main def part2: Unit = 12 | println(s"The solution is ${part2(loadInput())}") 13 | 14 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day01") 15 | 16 | def part1(input: String): Int = 17 | maxInventories(scanInventories(input), 1).head 18 | 19 | def part2(input: String): Int = 20 | maxInventories(scanInventories(input), 3).sum 21 | 22 | case class Inventory(items: List[Int]) 23 | 24 | def scanInventories(input: String): List[Inventory] = 25 | val inventories = List.newBuilder[Inventory] 26 | var items = List.newBuilder[Int] 27 | for line <- input.linesIterator do 28 | if line.isEmpty then 29 | inventories += Inventory(items.result()) 30 | items = List.newBuilder 31 | else items += line.toInt 32 | inventories.result() 33 | 34 | def maxInventories(inventories: List[Inventory], n: Int): List[Int] = 35 | inventories.map(_.items.sum).sorted(using Ordering.Int.reverse).take(n) 36 | -------------------------------------------------------------------------------- /2022/src/day02.scala: -------------------------------------------------------------------------------- 1 | package day02 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | import Position.* 6 | 7 | @main def part1: Unit = 8 | println(s"The solution is ${part1(loadInput())}") 9 | 10 | @main def part2: Unit = 11 | println(s"The solution is ${part2(loadInput())}") 12 | 13 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day02") 14 | 15 | def part1(input: String): Int = 16 | scores(input, pickPosition).sum 17 | 18 | def part2(input: String): Int = 19 | scores(input, winLoseOrDraw).sum 20 | 21 | enum Position: 22 | case Rock, Paper, Scissors 23 | 24 | def winsAgainst: Position = fromOrdinal((ordinal + 2) % 3) // two positions after this one, wrapping around 25 | def losesAgainst: Position = fromOrdinal((ordinal + 1) % 3) // one position after this one, wrapping around 26 | end Position 27 | 28 | def readCode(opponent: String) = opponent match 29 | case "A" => Rock 30 | case "B" => Paper 31 | case "C" => Scissors 32 | 33 | def scores(input: String, strategy: (Position, String) => Position): Iterator[Int] = 34 | for case s"$x $y" <- input.linesIterator yield 35 | val opponent = readCode(x) 36 | score(opponent, strategy(opponent, y)) 37 | 38 | def winLoseOrDraw(opponent: Position, code: String): Position = code match 39 | case "X" => opponent.winsAgainst // we need to lose 40 | case "Y" => opponent // we need to tie 41 | case "Z" => opponent.losesAgainst // we need to win 42 | 43 | def pickPosition(opponent: Position, code: String): Position = code match 44 | case "X" => Rock 45 | case "Y" => Paper 46 | case "Z" => Scissors 47 | 48 | def score(opponent: Position, player: Position): Int = 49 | val pointsOutcome = 50 | if opponent == player then 3 // tie 51 | else if player.winsAgainst == opponent then 6 // win 52 | else 0 // lose 53 | 54 | val pointsPlay = player.ordinal + 1 // Rock = 1, Paper = 2, Scissors = 3 55 | 56 | pointsPlay + pointsOutcome 57 | end score 58 | -------------------------------------------------------------------------------- /2022/src/day03.scala: -------------------------------------------------------------------------------- 1 | package day03 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | 6 | @main def part1: Unit = 7 | println(s"The solution is ${part1(loadInput())}") 8 | 9 | @main def part2: Unit = 10 | println(s"The solution is ${part2(loadInput())}") 11 | 12 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day03") 13 | 14 | object Priorities: 15 | opaque type Set = Long // can fit all 52 priorities in a bitset 16 | 17 | // encode priorities as a random access lookup 18 | private val lookup = 19 | val arr = new Array[Int](128) // max key is `'z'.toInt == 122` 20 | for (c, i) <- (('a' to 'z') ++ ('A' to 'Z')).zipWithIndex do 21 | arr(c.toInt) = i + 1 22 | IArray.unsafeFromArray(arr) 23 | 24 | val emptySet: Set = 0L 25 | 26 | extension (b: Set) 27 | infix def add(c: Char): Set = b | (1L << lookup(c.toInt)) 28 | infix def &(that: Set): Set = b & that 29 | def head: Int = java.lang.Long.numberOfTrailingZeros(b) 30 | 31 | end Priorities 32 | 33 | def priorities(str: String) = str.foldLeft(Priorities.emptySet)(_ add _) 34 | 35 | def part1(input: String): Int = 36 | val intersections = 37 | for line <- input.linesIterator yield 38 | val (left, right) = line.splitAt(line.length / 2) 39 | (priorities(left) & priorities(right)).head 40 | intersections.sum 41 | 42 | def part2(input: String): Int = 43 | val badges = 44 | for case Seq(a, b, c) <- input.linesIterator.grouped(3) yield 45 | (priorities(a) & priorities(b) & priorities(c)).head 46 | badges.sum -------------------------------------------------------------------------------- /2022/src/day04.scala: -------------------------------------------------------------------------------- 1 | package day04 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | 6 | @main def part1: Unit = 7 | println(s"The solution is ${part1(loadInput())}") 8 | 9 | @main def part2: Unit = 10 | println(s"The solution is ${part2(loadInput())}") 11 | 12 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day04") 13 | 14 | def part1(input: String): Int = 15 | foldPairs(input, subsumes) 16 | 17 | def part2(input: String): Int = 18 | foldPairs(input, overlaps) 19 | 20 | def subsumes(x: Int, y: Int)(a: Int, b: Int): Boolean = x <= a && y >= b 21 | def overlaps(x: Int, y: Int)(a: Int, b: Int): Boolean = x <= a && y >= a || x <= b && y >= b 22 | 23 | def foldPairs(input: String, hasOverlap: (Int, Int) => (Int, Int) => Boolean): Int = 24 | val matches = 25 | for line <- input.linesIterator yield 26 | val Array(x,y,a,b) = line.split("[,-]").map(_.toInt): @unchecked 27 | hasOverlap(x,y)(a,b) || hasOverlap(a,b)(x,y) 28 | matches.count(identity) -------------------------------------------------------------------------------- /2022/src/day05.scala: -------------------------------------------------------------------------------- 1 | package day05 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | import scala.collection.mutable.Builder 6 | 7 | @main def part1: Unit = 8 | println(s"The solution is ${part1(loadInput())}") 9 | 10 | @main def part2: Unit = 11 | println(s"The solution is ${part2(loadInput())}") 12 | 13 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day05") 14 | 15 | def part1(input: String): String = 16 | moveAllCrates(input, _ reverse_::: _) // concat in reverse order 17 | 18 | def part2(input: String): String = 19 | moveAllCrates(input, _ ::: _) // concat in normal order 20 | 21 | /** each column is 4 chars wide (or 3 if terminal) */ 22 | def parseRow(row: String) = 23 | for i <- 0 to row.length by 4 yield 24 | if row(i) == '[' then 25 | row(i + 1) // the crate id 26 | else 27 | '#' // empty slot 28 | 29 | def parseColumns(header: IndexedSeq[String]): IndexedSeq[List[Char]] = 30 | val crates :+ colsStr = header: @unchecked 31 | val columns = colsStr.split(" ").filter(_.nonEmpty).length 32 | 33 | val rows = crates.map(parseRow(_).padTo(columns, '#')) // pad empty slots at the end 34 | 35 | // transpose the rows to get the columns, then remove the terminal empty slots from each column 36 | rows.transpose.map(_.toList.filterNot(_ == '#')) 37 | end parseColumns 38 | 39 | def moveAllCrates(input: String, moveCrates: (List[Char], List[Char]) => List[Char]): String = 40 | val (headerLines, rest0) = input.linesIterator.span(_.nonEmpty) 41 | val instructions = rest0.drop(1) // drop the empty line after the header 42 | 43 | def move(cols: IndexedSeq[List[Char]], n: Int, idxA: Int, idxB: Int) = 44 | val (toMove, aRest) = cols(idxA).splitAt(n) 45 | val b2 = moveCrates(toMove, cols(idxB)) 46 | cols.updated(idxA, aRest).updated(idxB, b2) 47 | 48 | val columns = parseColumns(headerLines.to(IndexedSeq)) 49 | 50 | val columns1 = instructions.foldLeft(columns) { case (columns, s"move $n from $a to $b") => 51 | move(columns, n.toInt, a.toInt - 1, b.toInt - 1) 52 | } 53 | columns1.map(_.head).mkString 54 | -------------------------------------------------------------------------------- /2022/src/day06.scala: -------------------------------------------------------------------------------- 1 | package day06 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | import scala.collection.mutable.Builder 6 | 7 | @main def part1: Unit = 8 | println(s"The solution is ${part1(loadInput())}") 9 | 10 | @main def part2: Unit = 11 | println(s"The solution is ${part2(loadInput())}") 12 | 13 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day06") 14 | 15 | def part1(input: String): Int = 16 | findIndex(input, n = 4) 17 | 18 | def part2(input: String): Int = 19 | findIndex(input, n = 14) 20 | 21 | def findIndex(input: String, n: Int): Int = 22 | val firstIndex = input.iterator 23 | .zipWithIndex 24 | .sliding(n) 25 | .find(_.map(_(0)).toSet.size == n) 26 | .get 27 | .head(1) 28 | firstIndex + n 29 | -------------------------------------------------------------------------------- /2022/src/day07.scala: -------------------------------------------------------------------------------- 1 | package day07 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | 6 | @main def part1: Unit = 7 | println(s"The solution is ${part1(loadInput())}") 8 | 9 | @main def part2: Unit = 10 | println(s"The solution is ${part2(loadInput())}") 11 | 12 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day07") 13 | 14 | def parse(input: String): List[Command] = 15 | input 16 | .linesIterator 17 | .map(Command.fromString) 18 | .toList 19 | 20 | def part1(input: String): Long = 21 | val root: Directory = Directory("/") 22 | run(parse(input), List(root)) 23 | allSubdirs(root) 24 | .map(totalSize) 25 | .filter(_ <= 100_000L) 26 | .sum 27 | 28 | def part2(input: String): Long = 29 | val root: Directory = Directory("/") 30 | run(parse(input), List(root)) 31 | val sizeNeeded = totalSize(root) - 40_000_000L 32 | allSubdirs(root) 33 | .map(totalSize) 34 | .filter(_ >= sizeNeeded) 35 | .min 36 | 37 | // data model & parsing: directory tree 38 | 39 | import collection.mutable.ListBuffer 40 | 41 | enum Node: 42 | case Directory(name: String, children: ListBuffer[Node] = ListBuffer.empty) 43 | case File(name: String, size: Long) 44 | object Node: 45 | def fromString(s: String) = s match 46 | case s"dir $name" => Directory(name) 47 | case s"$size $name" => File(name, size.toLong) 48 | 49 | import Node.* 50 | 51 | def totalSize(e: Node): Long = e match 52 | case Directory(_, children) => 53 | children.map(totalSize).sum 54 | case File(_, size) => 55 | size 56 | 57 | def allSubdirs(root: Directory): Iterator[Directory] = 58 | Iterator(root) ++ 59 | root.children.collect: 60 | case d: Directory => d 61 | .iterator.flatMap(allSubdirs) 62 | 63 | // data model & parsing: commands 64 | 65 | enum Command: 66 | case Cd(dest: String) 67 | case Ls 68 | case Output(s: String) 69 | object Command: 70 | def fromString(s: String) = s match 71 | case "$ ls" => Ls 72 | case s"$$ cd $dest" => Cd(dest) 73 | case _ => Output(s) 74 | 75 | // interpreter 76 | 77 | @annotation.tailrec 78 | def run(lines: List[Command], dirs: List[Directory]): Unit = 79 | lines match 80 | case Nil => // done 81 | case line :: more => 82 | line match 83 | case Command.Cd("/") => 84 | run(more, List(dirs.last)) 85 | case Command.Cd("..") => 86 | run(more, dirs.tail) 87 | case Command.Cd(dest) => 88 | val newCwd = 89 | dirs.head.children.collectFirst: 90 | case dir @ Directory(`dest`, _) => dir 91 | .get 92 | run(more, newCwd :: dirs) 93 | case Command.Ls => 94 | val (outputLines, more2) = more.span(_.isInstanceOf[Command.Output]) 95 | for Command.Output(s) <- outputLines.map(_.asInstanceOf[Command.Output]) do 96 | dirs.head.children += Node.fromString(s) 97 | run(more2, dirs) 98 | case _: Command.Output => 99 | throw new IllegalStateException(line.toString) 100 | -------------------------------------------------------------------------------- /2022/src/day08.scala: -------------------------------------------------------------------------------- 1 | package day08 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | import scala.collection.mutable.Builder 6 | 7 | @main def part1: Unit = 8 | println(s"The solution is ${part1(loadInput())}") 9 | 10 | @main def part2: Unit = 11 | println(s"The solution is ${part2(loadInput())}") 12 | 13 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day08") 14 | 15 | def part1(input: String): Int = 16 | val parsed = parse(input) 17 | val visibilityField: VisibilityField = computeInAllDirections(parsed, computeVisibility).reduce(combine(_ | _)) 18 | visibilityField.megaMap(if _ then 1 else 0).megaReduce(_ + _) 19 | 20 | def part2(input: String): Int = 21 | val parsed = parse(input) 22 | val scoreField: ScoreField = computeInAllDirections(parsed, computeScore).reduce(combine(_ * _)) 23 | scoreField.megaReduce(_ max _) 24 | 25 | type Field[A] = List[List[A]] 26 | 27 | extension [A](xss: Field[A]) 28 | def megaZip[B](yss: Field[B]): Field[(A, B)] = (xss zip yss).map( (xs, ys) => xs zip ys ) 29 | def megaMap[B](f: A => B): Field[B] = xss.map(_.map(f)) 30 | def megaReduce(f: (A,A) => A): A = xss.map(_.reduce(f)).reduce(f) 31 | 32 | def combine[A](op: ((A,A)) => A)(f1: Field[A], f2: Field[A]): Field[A] = f1.megaZip(f2).megaMap(op) 33 | 34 | def computeInAllDirections[A, B](xss: Field[A], f: Field[A] => Field[B]): List[Field[B]] = 35 | for 36 | transpose <- List(false, true) 37 | reverse <- List(false, true) 38 | yield 39 | val t = if transpose then xss.transpose else xss 40 | val in = if reverse then t.map(_.reverse) else t 41 | val res = f(in) 42 | val r = if reverse then res.map(_.reverse) else res 43 | val out = if transpose then r.transpose else r 44 | out 45 | 46 | type HeightField = Field[Int] 47 | type ScoreField = Field[Int] 48 | 49 | type VisibilityField = Field[Boolean] 50 | 51 | def parse(input: String): HeightField = input.split("\n").map(line => line.map(char => char.toInt - '0').toList).toList 52 | 53 | def computeVisibility(ls: HeightField): VisibilityField = ls.map{ line => 54 | line.scanLeft((-1, false)){ case ((prev, _), curr ) => (Math.max(prev, curr), curr > prev)}.tail.map(_._2) 55 | } 56 | 57 | def computeScore(ls: HeightField) = ls.map{ line => 58 | val distances = line.scanRight((-1, List.fill(10)(0))){ case (curr, (_, lengths)) => 59 | val newLengths = lengths.zipWithIndex.map{ case (v, i) => if i <= curr then 1 else v+1 } 60 | (lengths(curr), newLengths) 61 | } 62 | distances.map(_._1).init 63 | } 64 | -------------------------------------------------------------------------------- /2022/src/day09.scala: -------------------------------------------------------------------------------- 1 | package day09 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | 6 | import Direction.* 7 | 8 | @main def part1: Unit = 9 | println(s"The solution is ${part1(loadInput())}") 10 | 11 | @main def part2: Unit = 12 | println(s"The solution is ${part2(loadInput())}") 13 | 14 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day09") 15 | 16 | def part1(input: String): Int = 17 | uniquePositions(input, knots = 2) 18 | 19 | def part2(input: String): Int = 20 | uniquePositions(input, knots = 10) 21 | 22 | case class Position(x: Int, y: Int): 23 | def moveOne(dir: Direction): Position = dir match 24 | case U => Position(x, y + 1) 25 | case D => Position(x, y - 1) 26 | case L => Position(x - 1, y) 27 | case R => Position(x + 1, y) 28 | 29 | def follow(head: Position): Position = 30 | val dx = head.x - x 31 | val dy = head.y - y 32 | if dx.abs > 1 || dy.abs > 1 then Position(x + dx.sign, y + dy.sign) // follow the head 33 | else this // stay put 34 | 35 | case class State(uniques: Set[Position], head: Position, knots: List[Position]) 36 | 37 | enum Direction: 38 | case U, D, L, R 39 | 40 | def followAll(head: Position, knots: List[Position]) = 41 | knots.foldLeft((head, List.newBuilder[Position])) { case ((prev, knots), knot) => 42 | val next = knot.follow(prev) 43 | (next, knots += next) 44 | } 45 | 46 | def moves(state: State, dir: Direction): Iterator[State] = 47 | Iterator.iterate(state)({ case State(uniques, head, knots) => 48 | val head1 = head.moveOne(dir) 49 | val (terminal, knots1) = followAll(head1, knots) 50 | State(uniques + terminal, head1, knots1.result()) 51 | }) 52 | 53 | def uniquePositions(input: String, knots: Int): Int = 54 | val zero = Position(0, 0) 55 | val empty = State(Set(zero), zero, List.fill(knots - 1)(zero)) 56 | val end = input.linesIterator.foldLeft(empty) { case (state, line) => 57 | val (s"$dir $steps") = line: @unchecked 58 | moves(state, Direction.valueOf(dir)).drop(steps.toInt).next() 59 | } 60 | end.uniques.size 61 | -------------------------------------------------------------------------------- /2022/src/day10.scala: -------------------------------------------------------------------------------- 1 | package day10 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | import Command.* 6 | 7 | @main def part1: Unit = 8 | println(s"The solution is ${part1(loadInput())}") 9 | 10 | @main def part2: Unit = 11 | println(s"The solution is ${part2(loadInput())}") 12 | 13 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day10") 14 | 15 | enum Command: 16 | case Noop 17 | case Addx(X: Int) 18 | 19 | def commandsIterator(input: String): Iterator[Command] = 20 | for line <- input.linesIterator yield line match 21 | case "noop" => Noop 22 | case s"addx $x" if x.toIntOption.isDefined => Addx(x.toInt) 23 | case _ => throw IllegalArgumentException(s"Invalid command '$line''") 24 | 25 | val RegisterStartValue = 1 26 | 27 | def registerValuesIterator(input: String): Iterator[Int] = 28 | val steps = commandsIterator(input).scanLeft(RegisterStartValue :: Nil) { (values, cmd) => 29 | val value = values.last 30 | cmd match 31 | case Noop => value :: Nil 32 | case Addx(x) => value :: value + x :: Nil 33 | } 34 | steps.flatten 35 | 36 | def registerStrengthsIterator(input: String): Iterator[Int] = 37 | val it = for (reg, i) <- registerValuesIterator(input).zipWithIndex yield (i + 1) * reg 38 | it.drop(19).grouped(40).map(_.head) 39 | 40 | def part1(input: String): Int = registerStrengthsIterator(input).sum 41 | 42 | val CRTWidth: Int = 40 43 | 44 | def CRTCharIterator(input: String): Iterator[Char] = 45 | for (reg, crtPos) <- registerValuesIterator(input).zipWithIndex yield 46 | if (reg - (crtPos % CRTWidth)).abs <= 1 then 47 | '#' 48 | else 49 | '.' 50 | 51 | def part2(input: String): String = CRTCharIterator(input).grouped(CRTWidth).map(_.mkString).mkString("\n") 52 | -------------------------------------------------------------------------------- /2022/src/day11.scala: -------------------------------------------------------------------------------- 1 | package day11 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | 6 | import scala.collection.immutable.Queue 7 | 8 | @main def part1: Unit = 9 | println(s"The solution is ${part1(loadInput())}") 10 | 11 | @main def part2: Unit = 12 | println(s"The solution is ${part2(loadInput())}") 13 | 14 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day11") 15 | 16 | def part1(input: String): Long = 17 | run(initial = parseInput(input), times = 20, adjust = _ / 3) 18 | 19 | def part2(input: String): Long = 20 | run(initial = parseInput(input), times = 10_000, adjust = identity) 21 | 22 | type Worry = Long 23 | type Op = Worry => Worry 24 | type Monkeys = IndexedSeq[Monkey] 25 | 26 | case class Monkey( 27 | items: Queue[Worry], 28 | divisibleBy: Int, 29 | ifTrue: Int, 30 | ifFalse: Int, 31 | op: Op, 32 | inspected: Int 33 | ) 34 | 35 | def iterate[Z](times: Int)(op: Z => Z)(z: Z): Z = 36 | (0 until times).foldLeft(z) { (z, _) => op(z) } 37 | 38 | def run(initial: Monkeys, times: Int, adjust: Op): Long = 39 | val lcm = initial.map(_.divisibleBy.toLong).product 40 | val monkeys = iterate(times)(round(adjust, lcm))(initial) 41 | monkeys.map(_.inspected.toLong).sorted.reverseIterator.take(2).product 42 | 43 | def round(adjust: Op, lcm: Worry)(monkeys: Monkeys): Monkeys = 44 | monkeys.indices.foldLeft(monkeys) { (monkeys, index) => 45 | turn(index, monkeys, adjust, lcm) 46 | } 47 | 48 | def turn(index: Int, monkeys: Monkeys, adjust: Op, lcm: Worry): Monkeys = 49 | val monkey = monkeys(index) 50 | val Monkey(items, divisibleBy, ifTrue, ifFalse, op, inspected) = monkey 51 | 52 | val monkeys1 = items.foldLeft(monkeys) { (monkeys, item) => 53 | val inspected = op(item) 54 | val nextWorry = adjust(inspected) % lcm 55 | val thrownTo = 56 | if nextWorry % divisibleBy == 0 then ifTrue 57 | else ifFalse 58 | val thrownToMonkey = 59 | val m = monkeys(thrownTo) 60 | m.copy(items = m.items :+ nextWorry) 61 | monkeys.updated(thrownTo, thrownToMonkey) 62 | } 63 | val monkey1 = monkey.copy( 64 | items = Queue.empty, 65 | inspected = inspected + items.size 66 | ) 67 | monkeys1.updated(index, monkey1) 68 | end turn 69 | 70 | def parseInput(input: String): Monkeys = 71 | 72 | def eval(by: String): Op = 73 | if by == "old" then identity 74 | else Function.const(by.toInt) 75 | 76 | def parseOperator(op: String, left: Op, right: Op): Op = 77 | op match 78 | case "+" => old => left(old) + right(old) 79 | case "*" => old => left(old) * right(old) 80 | 81 | IArray.from( 82 | for 83 | case Seq( 84 | s"Monkey $n:", 85 | s" Starting items: $items", 86 | s" Operation: new = $left $operator $right", 87 | s" Test: divisible by $div", 88 | s" If true: throw to monkey $ifTrue", 89 | s" If false: throw to monkey $ifFalse", 90 | _* 91 | ) <- input.linesIterator.grouped(7) 92 | yield 93 | val op = parseOperator(operator, eval(left), eval(right)) 94 | val itemsQueue = items.split(", ").map(_.toLong).to(Queue) 95 | Monkey(itemsQueue, div.toInt, ifTrue.toInt, ifFalse.toInt, op, inspected = 0) 96 | ) 97 | end parseInput 98 | -------------------------------------------------------------------------------- /2022/src/day12.scala: -------------------------------------------------------------------------------- 1 | package day12 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | 6 | @main def part1: Unit = 7 | println(s"The solution is ${part1(loadInput())}") 8 | 9 | @main def part2: Unit = 10 | println(s"The solution is ${part2(loadInput())}") 11 | 12 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day12") 13 | 14 | def part1(data: String): Int = 15 | solution(IndexedSeq.from(data.linesIterator), 'S') 16 | 17 | def part2(data: String): Int = 18 | solution(IndexedSeq.from(data.linesIterator), 'a') 19 | 20 | case class Point(x: Int, y: Int): 21 | def move(dx: Int, dy: Int): 22 | Point = Point(x + dx, y + dy) 23 | override def toString: String = 24 | s"($x, $y)" 25 | end Point 26 | 27 | val up = (0, 1) 28 | val down = (0, -1) 29 | val left = (-1, 0) 30 | val right = (1, 0) 31 | val possibleMoves = List(up, down, left, right) 32 | 33 | def path(point: Point, net: Map[Point, Char]): Seq[Point] = 34 | possibleMoves.map(point.move).filter(net.contains) 35 | 36 | def matching(point: Point, net: Map[Point, Char]): Char = 37 | net(point) match 38 | case 'S' => 'a' 39 | case 'E' => 'z' 40 | case other => other 41 | 42 | def solution(source: IndexedSeq[String], srchChar: Char): Int = 43 | // create a sequence of Point objects and their corresponding character in source 44 | val points = 45 | for 46 | y <- source.indices 47 | x <- source.head.indices 48 | yield 49 | Point(x, y) -> source(y)(x) 50 | val p = points.toMap 51 | val initial = p.map(_.swap)('E') 52 | val queue = collection.mutable.Queue(initial) 53 | val length = collection.mutable.Map(initial -> 0) 54 | //bfs 55 | while queue.nonEmpty do 56 | val visited = queue.dequeue() 57 | if p(visited) == srchChar then 58 | return length(visited) 59 | for visited1 <- path(visited, p) do 60 | val shouldAdd = 61 | !length.contains(visited1) 62 | && matching(visited, p) - matching(visited1, p) <= 1 63 | if shouldAdd then 64 | queue.enqueue(visited1) 65 | length(visited1) = length(visited) + 1 66 | end for 67 | end while 68 | throw IllegalStateException("unexpected end of search area") 69 | end solution 70 | -------------------------------------------------------------------------------- /2022/src/day13.scala: -------------------------------------------------------------------------------- 1 | package day13 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | 6 | import scala.collection.immutable.Queue 7 | import scala.math.Ordered.given 8 | import Packet.* 9 | 10 | @main def part1: Unit = 11 | println(s"The solution is ${part1(loadInput())}") 12 | 13 | @main def part2: Unit = 14 | println(s"The solution is ${part2(loadInput())}") 15 | 16 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day13") 17 | 18 | def part1(input: String): Int = 19 | findOrderedIndices(input) 20 | 21 | def part2(input: String): Int = 22 | findDividerIndices(input) 23 | 24 | def findOrderedIndices(input: String): Int = 25 | val indices = ( 26 | for 27 | case (Seq(a, b, _*), i) <- input.linesIterator.grouped(3).zipWithIndex 28 | if readPacket(a) <= readPacket(b) 29 | yield 30 | i + 1 31 | ) 32 | indices.sum 33 | 34 | def findDividerIndices(input: String): Int = 35 | val dividers = List("[[2]]", "[[6]]").map(readPacket) 36 | val lookup = dividers.toSet 37 | val packets = input 38 | .linesIterator 39 | .filter(_.nonEmpty) 40 | .map(readPacket) 41 | val indices = (dividers ++ packets) 42 | .sorted 43 | .iterator 44 | .zipWithIndex 45 | .collect { case (p, i) if lookup.contains(p) => i + 1 } 46 | indices.take(2).product 47 | 48 | enum Packet: 49 | case Nested(packets: List[Packet]) 50 | case Num(value: Int) 51 | 52 | override def toString(): String = this match 53 | case Nested(packets) => packets.mkString("[", ",", "]") 54 | case Num(value) => value.toString 55 | 56 | case class State(number: Int, values: Queue[Packet]): 57 | def nextWithDigit(digit: Int): State = // add digit to number 58 | copy(number = if number == -1 then digit else number * 10 + digit) 59 | def nextWithNumber: State = 60 | if number == -1 then this // no number to commit 61 | else State(number = -1, values = values :+ Packet.Num(number)) // reset number, add number to values 62 | 63 | object State: 64 | def empty = State(-1, Queue.empty) 65 | def fromValues(values: Queue[Packet]) = State(number = -1, values) 66 | 67 | def readPacket(input: String): Packet = 68 | def loop(i: Int, state: State, stack: List[Queue[Packet]]): Packet = 69 | input(i) match // assume that list is well-formed. 70 | case '[' => 71 | loop(i + 1, State.empty, state.values :: stack) // push old state to stack 72 | case ']' => // add trailing number, close packet 73 | val packet = Nested(state.nextWithNumber.values.toList) 74 | stack match 75 | case values1 :: rest => // restore old state 76 | loop(i + 1, State.fromValues(values1 :+ packet), rest) 77 | case Nil => // terminating case 78 | packet 79 | case ',' => loop(i + 1, state.nextWithNumber, stack) 80 | case n => loop(i + 1, state.nextWithDigit(n.asDigit), stack) 81 | end loop 82 | if input.nonEmpty && input(0) == '[' then 83 | loop(i = 1, State.empty, stack = Nil) 84 | else 85 | throw IllegalArgumentException(s"Invalid input: `$input`") 86 | end readPacket 87 | 88 | given PacketOrdering: Ordering[Packet] with 89 | 90 | def nestedCompare(ls: List[Packet], rs: List[Packet]): Int = (ls, rs) match 91 | case (l :: ls1, r :: rs1) => 92 | val res = compare(l, r) 93 | if res == 0 then nestedCompare(ls1, rs1) // equal, look at next element 94 | else res // less or greater 95 | 96 | case (_ :: _, Nil) => 1 // right ran out of elements first 97 | case (Nil, _ :: _) => -1 // left ran out of elements first 98 | case (Nil, Nil) => 0 // equal size 99 | end nestedCompare 100 | 101 | def compare(left: Packet, right: Packet): Int = (left, right) match 102 | case (Num(l), Num(r)) => l compare r 103 | case (Nested(l), Nested(r)) => nestedCompare(l, r) 104 | case (num @ Num(_), Nested(r)) => nestedCompare(num :: Nil, r) 105 | case (Nested(l), num @ Num(_)) => nestedCompare(l, num :: Nil) 106 | end compare 107 | 108 | end PacketOrdering 109 | -------------------------------------------------------------------------------- /2022/src/day15.scala: -------------------------------------------------------------------------------- 1 | package day15 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | 6 | @main def part1: Unit = 7 | println(s"The solution is ${part1(loadInput())}") 8 | 9 | @main def part2: Unit = 10 | println(s"The solution is ${part2(loadInput())}") 11 | 12 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day15") 13 | 14 | case class Position(x: Int, y: Int) 15 | 16 | def parse(input: String): List[(Position, Position)] = 17 | input.split("\n").toList.map{ 18 | case s"Sensor at x=$sx, y=$sy: closest beacon is at x=$bx, y=$by" => 19 | (Position(sx.toInt, sy.toInt), Position(bx.toInt, by.toInt)) 20 | } 21 | 22 | def distance(p1: Position, p2: Position): Int = 23 | Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y) 24 | 25 | def distanceToLine(p: Position, y: Int): Int = 26 | Math.abs(p.y - y) 27 | 28 | def lineCoverage(sensor: Position, radius: Int, lineY: Int): Range = 29 | val radiusInLine = radius - distanceToLine(sensor, lineY) 30 | 31 | // if radiusInLine is smaller than 0, the range will be empty 32 | (sensor.x - radiusInLine) to (sensor.x + radiusInLine) 33 | 34 | def coverOfLine(sensorsWithDistances: List[(Position, Int)], line: Int) = 35 | sensorsWithDistances.map( (sensor, radius) => lineCoverage(sensor, radius, line) ).filter(_.nonEmpty) 36 | 37 | def smartDiff(r1: Range, r2: Range): List[Range] = 38 | val innit = r1.start to Math.min(r2.start - 1, r1.last) 39 | val tail = Math.max(r1.start, r2.last + 1) to r1.last 40 | val res = if innit == tail then 41 | List(innit) 42 | else 43 | List(innit, tail) 44 | res.filter(_.nonEmpty).toList 45 | 46 | def remainingSpots(target: Range, cover: List[Range]): Set[Int] = 47 | 48 | def rec(partialTarget: List[Range], remainingCover: List[Range]): List[Range] = 49 | if remainingCover.isEmpty then 50 | partialTarget 51 | else 52 | val (curr: Range) :: rest = remainingCover: @unchecked 53 | rec( 54 | partialTarget = partialTarget.flatMap( r => smartDiff(r, curr) ), 55 | remainingCover = rest 56 | ) 57 | 58 | rec(List(target), cover).flatten.toSet 59 | 60 | def part1(input: String): Int = 61 | val parsed: List[(Position, Position)] = parse(input) 62 | val beacons: Set[Position] = parsed.map(_._2).toSet 63 | val sensorsWithDistances: List[(Position, Int)] = 64 | parsed.map( (sensor, beacon) => (sensor, distance(sensor, beacon)) ) 65 | 66 | val line = 2000000 67 | val cover: List[Range] = coverOfLine(sensorsWithDistances, line) 68 | val beaconsOnLine: Set[Position] = beacons.filter(_.y == line) 69 | val count: Int = cover.map(_.size).sum - beaconsOnLine.size 70 | count 71 | 72 | def part2(input: String): Any = 73 | 74 | val parsed: List[(Position, Position)] = parse(input) 75 | val beacons: Set[Position] = parsed.map(_._2).toSet 76 | val sensorsWithDistances: List[(Position, Int)] = 77 | parsed.map( (sensor, beacon) => (sensor, distance(sensor, beacon)) ) 78 | 79 | val target: Range = 0 until 4_000_000 80 | val spots: Seq[Position] = target.flatMap{ 81 | line => 82 | val cover: List[Range] = coverOfLine(sensorsWithDistances, line) 83 | val beaconsOnLine: Set[Position] = beacons.filter(_.y == line) 84 | 85 | val remainingRanges: List[Range] = cover.foldLeft(List(target)){ 86 | case (acc: List[Range], range: Range) => 87 | acc.flatMap( r => smartDiff(r, range) ) 88 | } 89 | val potential = remainingRanges.flatten.toSet 90 | 91 | val spotsOnLine = potential diff beaconsOnLine.map( b => b.x ) 92 | spotsOnLine.map( x => Position(x, line) ) 93 | } 94 | def tuningFrequency(p: Position): BigInt = BigInt(p.x) * 4_000_000 + p.y 95 | 96 | println(spots.mkString(", ")) 97 | assert(spots.size == 1) 98 | tuningFrequency(spots.head) 99 | -------------------------------------------------------------------------------- /2022/src/day16.scala: -------------------------------------------------------------------------------- 1 | package day16 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | 6 | @main def part1: Unit = 7 | println(s"The solution is ${part1(loadInput())}") 8 | 9 | @main def part2: Unit = 10 | println(s"The solution is ${part2(loadInput())}") 11 | 12 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day16") 13 | 14 | /* 15 | Copyright 2022 Tyler Coles (javadocmd.com), Quentin Bernet, Sébastien Doeraene and Jamie Thompson 16 | 17 | Licensed under the Apache License, Version 2.0 (the "License"); 18 | you may not use this file except in compliance with the License. 19 | You may obtain a copy of the License at 20 | 21 | http://www.apache.org/licenses/LICENSE-2.0 22 | 23 | Unless required by applicable law or agreed to in writing, software 24 | distributed under the License is distributed on an "AS IS" BASIS, 25 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 26 | See the License for the specific language governing permissions and 27 | limitations under the License. 28 | */ 29 | 30 | type Id = String 31 | case class Room(id: Id, flow: Int, tunnels: List[Id]) 32 | 33 | type Input = List[Room] 34 | // $_ to avoid tunnel/tunnels distinction and so on 35 | def parse(xs: String): Input = xs.split("\n").map{ case s"Valve $id has flow rate=$flow; tunnel$_ lead$_ to valve$_ $tunnelsStr" => 36 | val tunnels = tunnelsStr.split(", ").toList 37 | Room(id, flow.toInt, tunnels) 38 | }.toList 39 | 40 | case class RoomsInfo( 41 | /** map of rooms by id */ 42 | rooms: Map[Id, Room], 43 | /** map from starting room to a map containing the best distance to all other rooms */ 44 | routes: Map[Id, Map[Id, Int]], 45 | /** rooms containing non-zero-flow valves */ 46 | valves: Set[Id] 47 | ) 48 | 49 | // precalculate useful things like pathfinding 50 | def constructInfo(input: Input): RoomsInfo = 51 | val rooms: Map[Id, Room] = Map.from(for r <- input yield r.id -> r) 52 | val valves: Set[Id] = Set.from(for r <- input if r.flow > 0 yield r.id) 53 | val tunnels: Map[Id, List[Id]] = rooms.mapValues(_.tunnels).toMap 54 | val routes: Map[Id, Map[Id, Int]] = (valves + "AA").iterator.map{ id => id -> computeRoutes(id, tunnels) }.toMap 55 | RoomsInfo(rooms, routes, valves) 56 | 57 | // a modified A-star to calculate the best distance to all rooms rather then the best path to a single room 58 | def computeRoutes(start: Id, neighbors: Id => List[Id]): Map[Id, Int] = 59 | 60 | case class State(frontier: List[(Id, Int)], scores: Map[Id, Int]): 61 | 62 | private def getScore(id: Id): Int = scores.getOrElse(id, Int.MaxValue) 63 | private def setScore(id: Id, s: Int) = State((id, s + 1) :: frontier, scores + (id -> s)) 64 | 65 | def dequeued: (Id, State) = 66 | val sorted = frontier.sortBy(_._2) 67 | (sorted.head._1, copy(frontier = sorted.tail)) 68 | 69 | def considerEdge(from: Id, to: Id): State = 70 | val toScore = getScore(from) + 1 71 | if toScore >= getScore(to) then this 72 | else setScore(to, toScore) 73 | end State 74 | 75 | object State: 76 | def initial(start: Id) = State(List((start, 0)), Map(start -> 0)) 77 | 78 | def recurse(state: State): State = 79 | if state.frontier.isEmpty then 80 | state 81 | else 82 | val (curr, currState) = state.dequeued 83 | val newState = neighbors(curr) 84 | .foldLeft(currState) { (s, n) => 85 | s.considerEdge(curr, n) 86 | } 87 | recurse(newState) 88 | 89 | recurse(State.initial(start)).scores 90 | 91 | end computeRoutes 92 | 93 | 94 | // find the best path (the order of valves to open) and the total pressure released by taking it 95 | def bestPath(map: RoomsInfo, start: Id, valves: Set[Id], timeAllowed: Int): Int = 96 | // each step involves moving to a room with a useful valve and opening it 97 | // we don't need to track each (empty) room in between 98 | // we limit our options by only considering the still-closed valves 99 | // and `valves` has already culled any room with a flow value of 0 -- no point in considering these rooms! 100 | 101 | val valvesLookup = IArray.from(valves) 102 | val valveCount = valvesLookup.size 103 | val _activeValveIndices = Array.fill[Boolean](valveCount + 1)(true) // add an extra valve for the initial state 104 | def valveIndexLeft(i: Int) = _activeValveIndices(i) 105 | def withoutValve(i: Int)(f: => Int) = 106 | _activeValveIndices(i) = false 107 | val result = f 108 | _activeValveIndices(i) = true 109 | result 110 | val roomsByIndices = IArray.tabulate(valveCount)(i => map.rooms(valvesLookup(i))) 111 | 112 | def recurse(hiddenValve: Int, current: Id, timeLeft: Int, totalValue: Int): Int = withoutValve(hiddenValve): 113 | // recursively consider all plausible options 114 | // we are finished when we no longer have time to reach another valve or all valves are open 115 | val routesOfCurrent = map.routes(current) 116 | var bestValue = totalValue 117 | for index <- 0 to valveCount do 118 | if valveIndexLeft(index) then 119 | val id = valvesLookup(index) 120 | val distance = routesOfCurrent(id) 121 | // how much time is left after we traverse there and open the valve? 122 | val t = timeLeft - distance - 1 123 | // if `t` is zero or less this option can be skipped 124 | if t > 0 then 125 | // the value of choosing a particular valve (over the life of our simulation) 126 | // is its flow rate multiplied by the time remaining after opening it 127 | val value = roomsByIndices(index).flow * t 128 | val recValue = recurse(hiddenValve = index, id, t, totalValue + value) 129 | if recValue > bestValue then 130 | bestValue = recValue 131 | end if 132 | end if 133 | end for 134 | bestValue 135 | end recurse 136 | recurse(valveCount, start, timeAllowed, 0) 137 | 138 | def part1(input: String) = 139 | val time = 30 140 | val map = constructInfo(parse(input)) 141 | bestPath(map, "AA", map.valves, time) 142 | end part1 143 | 144 | def part2(input: String) = 145 | val time = 26 146 | val map = constructInfo(parse(input)) 147 | 148 | // in the optimal solution, the elephant and I will have divided responsibility for switching the valves 149 | // 15 (useful valves) choose 7 (half) yields only 6435 possible divisions which is a reasonable search space! 150 | val valvesA = map.valves.toList 151 | .combinations(map.valves.size / 2) 152 | .map(_.toSet) 153 | 154 | // NOTE: I assumed an even ditribution of valves would be optimal, and that turned out to be true. 155 | // However I suppose it's possible an uneven distribution could have been optimal for some graphs. 156 | // To be safe, you could re-run this using all reasonable values of `n` for `combinations` (1 to 7) and 157 | // taking the best of those. 158 | 159 | // we can now calculate the efforts separately and sum their values to find the best 160 | val allPaths = 161 | for va <- valvesA yield 162 | val vb = map.valves -- va 163 | val scoreA = bestPath(map, "AA", va, time) 164 | val scoreB = bestPath(map, "AA", vb, time) 165 | scoreA + scoreB 166 | 167 | allPaths.max 168 | end part2 169 | -------------------------------------------------------------------------------- /2022/src/day18.scala: -------------------------------------------------------------------------------- 1 | package day18 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | 6 | @main def part1: Unit = 7 | println(s"The solution is ${part1(loadInput())}") 8 | 9 | @main def part2: Unit = 10 | println(s"The solution is ${part2(loadInput())}") 11 | 12 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day18") 13 | 14 | def part1(input: String): Int = 15 | sides(cubes(input)) 16 | 17 | def part2(input: String): Int = 18 | sidesNoPockets(cubes(input)) 19 | 20 | def cubes(input: String): Set[(Int, Int, Int)] = 21 | val cubesIt = input.linesIterator.collect { 22 | case s"$x,$y,$z" => (x.toInt, y.toInt, z.toInt) 23 | } 24 | cubesIt.toSet 25 | 26 | def adjacent(x: Int, y: Int, z: Int): Set[(Int, Int, Int)] = { 27 | Set( 28 | (x + 1, y, z), 29 | (x - 1, y, z), 30 | (x, y + 1, z), 31 | (x, y - 1, z), 32 | (x, y, z + 1), 33 | (x, y, z - 1) 34 | ) 35 | } 36 | 37 | def sides(cubes: Set[(Int, Int, Int)]): Int = { 38 | cubes.foldLeft(0) { case (total, (x, y, z)) => 39 | val adj = adjacent(x, y, z) 40 | val numAdjacent = adj.filter(cubes).size 41 | total + 6 - numAdjacent 42 | } 43 | } 44 | 45 | def interior(cubes: Set[(Int, Int, Int)]): Set[(Int, Int, Int)] = { 46 | val allAdj = cubes.flatMap((x, y, z) => adjacent(x, y, z).filterNot(cubes)) 47 | val sts = allAdj.map { case adj @ (x, y, z) => 48 | adjacent(x, y, z).filterNot(cubes) + adj 49 | } 50 | def cc(sts: List[Set[(Int, Int, Int)]]): List[Set[(Int, Int, Int)]] = { 51 | sts match { 52 | case Nil => Nil 53 | case set :: rst => 54 | val (matching, other) = rst.partition(s => s.intersect(set).nonEmpty) 55 | val joined = matching.foldLeft(set)(_ ++ _) 56 | if (matching.nonEmpty) cc(joined :: other) else joined :: cc(other) 57 | } 58 | } 59 | val conn = cc(sts.toList) 60 | val exterior = conn.maxBy(_.maxBy(_(0))) 61 | conn.filterNot(_ == exterior).foldLeft(Set())(_ ++ _) 62 | } 63 | 64 | def sidesNoPockets(cubes: Set[(Int, Int, Int)]): Int = { 65 | val int = interior(cubes) 66 | val allAdj = cubes.flatMap(adjacent) 67 | allAdj.foldLeft(sides(cubes)) { case (total, (x, y, z)) => 68 | val adj = adjacent(x, y, z) 69 | if (int((x, y, z))) total - adj.filter(cubes).size else total 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /2022/src/day21.scala: -------------------------------------------------------------------------------- 1 | package day21 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | 6 | import annotation.tailrec 7 | import Operation.* 8 | 9 | @main def part1: Unit = 10 | println(s"The solution is ${part1(loadInput())}") 11 | 12 | @main def part2: Unit = 13 | println(s"The solution is ${part2(loadInput())}") 14 | 15 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day21") 16 | 17 | def part1(input: String): Long = 18 | resolveRoot(input) 19 | 20 | def part2(input: String): Long = 21 | whichValue(input) 22 | 23 | enum Operator(val eval: BinOp, val invRight: BinOp, val invLeft: BinOp): 24 | case `+` extends Operator(_ + _, _ - _, _ - _) 25 | case `-` extends Operator(_ - _, _ + _, (x, y) => y - x) 26 | case `*` extends Operator(_ * _, _ / _, _ / _) 27 | case `/` extends Operator(_ / _, _ * _, (x, y) => y / x) 28 | 29 | enum Operation: 30 | case Binary(op: Operator, depA: String, depB: String) 31 | case Constant(value: Long) 32 | 33 | type BinOp = (Long, Long) => Long 34 | type Resolved = Map[String, Long] 35 | type Source = Map[String, Operation] 36 | type Substitutions = List[(String, PartialFunction[Operation, Operation])] 37 | 38 | def readAll(input: String): Map[String, Operation] = 39 | Map.from( 40 | for case s"$name: $action" <- input.linesIterator yield 41 | name -> action.match 42 | case s"$x $binop $y" => 43 | Binary(Operator.valueOf(binop), x, y) 44 | case n => 45 | Constant(n.toLong) 46 | ) 47 | 48 | @tailrec 49 | def reachable(names: List[String], source: Source, resolved: Resolved): Resolved = names match 50 | case name :: rest => 51 | source.get(name) match 52 | case None => resolved // return as name is not reachable 53 | case Some(operation) => operation match 54 | case Binary(op, x, y) => 55 | (resolved.get(x), resolved.get(y)) match 56 | case (Some(a), Some(b)) => 57 | reachable(rest, source, resolved + (name -> op.eval(a, b))) 58 | case _ => 59 | reachable(x :: y :: name :: rest, source, resolved) 60 | case Constant(value) => 61 | reachable(rest, source, resolved + (name -> value)) 62 | case Nil => 63 | resolved 64 | end reachable 65 | 66 | def resolveRoot(input: String): Long = 67 | val values = reachable("root" :: Nil, readAll(input), Map.empty) 68 | values("root") 69 | 70 | def whichValue(input: String): Long = 71 | val source = readAll(input) - "humn" 72 | 73 | @tailrec 74 | def binarySearch(name: String, goal: Option[Long], resolved: Resolved): Long = 75 | 76 | def resolve(name: String) = 77 | val values = reachable(name :: Nil, source, resolved) 78 | values.get(name).map(_ -> values) 79 | 80 | def nextGoal(inv: BinOp, value: Long): Long = goal match 81 | case Some(prev) => inv(prev, value) 82 | case None => value 83 | 84 | (source.get(name): @unchecked) match 85 | case Some(Operation.Binary(op, x, y)) => 86 | ((resolve(x), resolve(y)): @unchecked) match 87 | case (Some(xValue -> resolvedX), _) => // x is known, y has a hole 88 | binarySearch(y, Some(nextGoal(op.invLeft, xValue)), resolvedX) 89 | case (_, Some(yValue -> resolvedY)) => // y is known, x has a hole 90 | binarySearch(x, Some(nextGoal(op.invRight, yValue)), resolvedY) 91 | case None => 92 | goal.get // hole found 93 | end binarySearch 94 | 95 | binarySearch(goal = None, name = "root", resolved = Map.empty) 96 | end whichValue 97 | -------------------------------------------------------------------------------- /2022/src/day25.scala: -------------------------------------------------------------------------------- 1 | package day25 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | 6 | @main def part1: Unit = 7 | println(s"The solution is ${part1(loadInput())}") 8 | 9 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day25") 10 | 11 | def part1(input: String): String = 12 | challenge(input) 13 | 14 | val digitToInt = Map( 15 | '0' -> 0, 16 | '1' -> 1, 17 | '2' -> 2, 18 | '-' -> -1, 19 | '=' -> -2, 20 | ) 21 | val intToDigit = digitToInt.map(_.swap) 22 | 23 | def showSnafu(value: BigInt): String = 24 | val buf = StringBuilder() 25 | var v = value 26 | while v != 0 do 27 | val digit = (v % 5).toInt match 28 | case 0 => 0 29 | case 1 => 1 30 | case 2 => 2 31 | case 3 => -2 32 | case 4 => -1 33 | buf.append(intToDigit(digit)) 34 | v = (v - digit) / 5 35 | buf.reverseInPlace().toString() 36 | 37 | def readSnafu(line: String): BigInt = 38 | line.foldLeft(BigInt(0)) { (acc, digit) => acc * 5 + digitToInt(digit) } 39 | 40 | def challenge(input: String): String = 41 | showSnafu(value = input.linesIterator.map(readSnafu).sum) 42 | -------------------------------------------------------------------------------- /2022/src/inputs.scala: -------------------------------------------------------------------------------- 1 | package inputs 2 | 3 | import scala.util.Using 4 | import scala.io.Source 5 | 6 | object Input: 7 | 8 | def loadFileSync(path: String): String = 9 | Using.resource(Source.fromFile(path))(_.mkString) 10 | -------------------------------------------------------------------------------- /2022/src/locations.scala: -------------------------------------------------------------------------------- 1 | package locations 2 | 3 | import scala.quoted.* 4 | 5 | object Directory: 6 | 7 | /** The absolute path of the parent directory of the file that calls this method 8 | * This is stable no matter which directory runs the program. 9 | */ 10 | inline def currentDir: String = ${ parentDirImpl } 11 | 12 | private def parentDirImpl(using Quotes): Expr[String] = 13 | // position of the call to `currentDir` in the source code 14 | val position = quotes.reflect.Position.ofMacroExpansion 15 | // get the path of the file calling this macro 16 | val srcFilePath = position.sourceFile.getJPath.get 17 | // get the parent of the path, which is the directory containing the file 18 | val parentDir = srcFilePath.getParent().toAbsolutePath 19 | Expr(parentDir.toString) // convert the String to Expr[String] 20 | -------------------------------------------------------------------------------- /2023/README.md: -------------------------------------------------------------------------------- 1 | # Scala Advent of Code 2023 2 | 3 | Solutions in Scala for the annual [Advent of Code](https://adventofcode.com/) challenge. _Note: this repo is not affiliated with Advent of Code._ 4 | 5 | > See earlier editions: 6 | > - [2021](/2021/README.md) 7 | > - [2022](/2022/README.md) 8 | 9 | ## Website 10 | 11 | The [Scala Advent of Code](https://scalacenter.github.io/scala-advent-of-code/) website contains: 12 | - some explanation of our solutions to [Advent of Code (adventofcode.com)](https://adventofcode.com/) 13 | - more solutions from the community 14 | 15 | ## Setup 16 | 17 | We use Visual Studio Code with Metals to write Scala code, and scala-cli to compile and run it. 18 | 19 | You can follow these [steps](https://scalacenter.github.io/scala-advent-of-code/setup) to set up your environement. 20 | 21 | ### How to open in Visual Studio Code 22 | 23 | After you clone the repository, open a terminal and run: 24 | ``` 25 | $ cd scala-advent-of-code 26 | $ scala-cli setup-ide 2023 27 | $ mkdir 2023/input 28 | $ code 2023 29 | ``` 30 | 31 | `code 2023` will open Visual Studio Code and start Metals. If not you may have to go to the Metals pane and click 32 | the button labelled "Start Metals". 33 | 34 | When you navigate to a file, e.g. `2023/src/day01.scala` metals should index the project, and then display code lenses 35 | above each of the main methods `part1` and `part2`, as shown in this image: 36 | ![](img/code-lenses.png) 37 | 38 | To run a solution, first copy your input to the folder `2023/input`. 39 | Then click `run` in VS Code which will run the code and display the results of the program. Or `debug`, 40 | which will let you pause on breakpoints, and execute expressions in the debug console. 41 | 42 | ### How to run a solution with command line 43 | 44 | In a terminal you can run: 45 | ``` 46 | $ scala-cli 2023 -M day01.part1 47 | Compiling project (Scala 3.x.y, JVM) 48 | Compiled project (Scala 3.x.y, JVM) 49 | The solution is 64929 50 | ``` 51 | 52 | Or, to run another solution: 53 | ``` 54 | $ scala-cli 2023 -M . 55 | ``` 56 | 57 | By default the solution programs run on our input files which are stored in the `2023/input` folder. 58 | To get your solutions you can change the content of those files in the `2023/input` folder. 59 | 60 | ## Contributing 61 | - Please do not commit your puzzle inputs, we can not accept them as they are protected by copyright 62 | -------------------------------------------------------------------------------- /2023/project.scala: -------------------------------------------------------------------------------- 1 | //> using scala 3.3.1 2 | //> using option -Wunused:all 3 | //> using test.dep org.scalameta::munit::1.0.0-M10 4 | -------------------------------------------------------------------------------- /2023/src/day01.scala: -------------------------------------------------------------------------------- 1 | package day01 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | 6 | @main def part1: Unit = 7 | println(s"The solution is ${part1(loadInput())}") 8 | 9 | @main def part2: Unit = 10 | println(s"The solution is ${part2(loadInput())}") 11 | 12 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day01") 13 | 14 | def part1(input: String): String = 15 | // Convert one line into the appropriate coordinates 16 | def lineToCoordinates(line: String): Int = 17 | val firstDigit = line.find(_.isDigit).get 18 | val lastDigit = line.findLast(_.isDigit).get 19 | s"$firstDigit$lastDigit".toInt 20 | 21 | // Convert each line to its coordinates and sum all the coordinates 22 | val result = input 23 | .linesIterator 24 | .map(lineToCoordinates(_)) 25 | .sum 26 | result.toString() 27 | end part1 28 | 29 | /** The textual representation of digits. */ 30 | val stringDigitReprs = Map( 31 | "one" -> 1, 32 | "two" -> 2, 33 | "three" -> 3, 34 | "four" -> 4, 35 | "five" -> 5, 36 | "six" -> 6, 37 | "seven" -> 7, 38 | "eight" -> 8, 39 | "nine" -> 9, 40 | ) 41 | 42 | /** All the string representation of digits, including the digits themselves. */ 43 | val digitReprs = stringDigitReprs ++ (1 to 9).map(i => i.toString() -> i) 44 | 45 | def part2(input: String): String = 46 | // A regex that matches any of the keys of `digitReprs` 47 | val digitReprRegex = digitReprs.keysIterator.mkString("|").r 48 | 49 | def lineToCoordinates(line: String): Int = 50 | /* Find all the digit representations in the line. 51 | * 52 | * A first attempt was to use 53 | * 54 | * val matches = digitReprRegex.findAllIn(line).toList 55 | * 56 | * however, that misses overlapping string representations of digits. 57 | * In particular, it will only find "one" in "oneight", although it should 58 | * also find the "eight". 59 | * 60 | * In our dataset, we had the line "29oneightt" which must parse as 28, not 21. 61 | * 62 | * Therefore, we explicitly try a find a match starting at each position 63 | * of the line. This is equivalent to finding a *prefix* match at every 64 | * *suffix* of the line. `line.tails` conveniently iterates over all the 65 | * suffixes. 66 | */ 67 | val matchesIter = 68 | for 69 | lineTail <- line.tails 70 | oneMatch <- digitReprRegex.findPrefixOf(lineTail) 71 | yield 72 | oneMatch 73 | val matches = matchesIter.toList 74 | 75 | // Convert the string representations into actual digits and form the result 76 | val firstDigit = digitReprs(matches.head) 77 | val lastDigit = digitReprs(matches.last) 78 | s"$firstDigit$lastDigit".toInt 79 | end lineToCoordinates 80 | 81 | // Process lines as in part1 82 | val result = input 83 | .linesIterator 84 | .map(lineToCoordinates(_)) 85 | .sum 86 | result.toString() 87 | end part2 88 | -------------------------------------------------------------------------------- /2023/src/day02.scala: -------------------------------------------------------------------------------- 1 | package day02 2 | // based on solution from https://github.com/bishabosha/advent-of-code-2023/blob/main/2023-day02.scala 3 | 4 | import locations.Directory.currentDir 5 | import inputs.Input.loadFileSync 6 | 7 | @main def part1: Unit = 8 | println(s"The solution is ${part1(loadInput())}") 9 | 10 | @main def part2: Unit = 11 | println(s"The solution is ${part2(loadInput())}") 12 | 13 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day02") 14 | 15 | case class Colors(color: String, count: Int) 16 | case class Game(id: Int, hands: List[List[Colors]]) 17 | type Summary = Game => Int 18 | 19 | def parseColors(pair: String): Colors = 20 | val Array(count0, color0) = pair.split(" ") 21 | Colors(color = color0, count = count0.toInt) 22 | 23 | def parse(line: String): Game = 24 | val Array(game0, hands) = line.split(": "): @unchecked 25 | val Array(_, id) = game0.split(" "): @unchecked 26 | val hands0 = hands.split("; ").toList 27 | val hands1 = hands0.map(_.split(", ").map(parseColors).toList) 28 | Game(id = id.toInt, hands = hands1) 29 | 30 | def solution(input: String, summarise: Summary): Int = 31 | input.linesIterator.map(parse andThen summarise).sum 32 | 33 | val possibleCubes = Map( 34 | "red" -> 12, 35 | "green" -> 13, 36 | "blue" -> 14, 37 | ) 38 | 39 | def validGame(game: Game): Boolean = 40 | game.hands.forall: hand => 41 | hand.forall: 42 | case Colors(color, count) => 43 | count <= possibleCubes.getOrElse(color, 0) 44 | 45 | val possibleGame: Summary = 46 | case game if validGame(game) => game.id 47 | case _ => 0 48 | 49 | def part1(input: String): Int = solution(input, possibleGame) 50 | 51 | val initial = Seq("red", "green", "blue").map(_ -> 0).toMap 52 | 53 | def minimumCubes(game: Game): Int = 54 | var maximums = initial 55 | for 56 | hand <- game.hands 57 | Colors(color, count) <- hand 58 | do 59 | maximums += (color -> (maximums(color) `max` count)) 60 | maximums.values.product 61 | 62 | def part2(input: String): Int = solution(input, minimumCubes) 63 | -------------------------------------------------------------------------------- /2023/src/day03.scala: -------------------------------------------------------------------------------- 1 | package day03 2 | // based on solution from https://github.com/bishabosha/advent-of-code-2023/blob/main/2023-day03.scala 3 | 4 | import locations.Directory.currentDir 5 | import inputs.Input.loadFileSync 6 | import scala.util.matching.Regex.Match 7 | 8 | @main def part1: Unit = 9 | println(s"The solution is ${part1(loadInput())}") 10 | 11 | @main def part2: Unit = 12 | println(s"The solution is ${part2(loadInput())}") 13 | 14 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day03") 15 | 16 | case class Coord(x: Int, y: Int): 17 | def within(start: Coord, end: Coord) = 18 | if y < start.y || y > end.y then false 19 | else if x < start.x || x > end.x then false 20 | else true 21 | case class PartNumber(value: Int, start: Coord, end: Coord) 22 | case class Symbol(sym: String, pos: Coord): 23 | def neighborOf(number: PartNumber) = pos.within( 24 | Coord(number.start.x - 1, number.start.y - 1), 25 | Coord(number.end.x + 1, number.end.y + 1) 26 | ) 27 | 28 | object IsInt: 29 | def unapply(in: Match): Option[Int] = in.matched.toIntOption 30 | 31 | def findPartsAndSymbols(source: String) = 32 | val extractor = """(\d+)|[^.\d]""".r 33 | source.split("\n").zipWithIndex.flatMap: (line, i) => 34 | extractor 35 | .findAllMatchIn(line) 36 | .map: 37 | case m @ IsInt(nb) => 38 | PartNumber(nb, Coord(m.start, i), Coord(m.end - 1, i)) 39 | case s => Symbol(s.matched, Coord(s.start, i)) 40 | 41 | def part1(input: String) = 42 | val all = findPartsAndSymbols(input) 43 | val symbols = all.collect { case s: Symbol => s } 44 | all 45 | .collect: 46 | case n: PartNumber if symbols.exists(_.neighborOf(n)) => 47 | n.value 48 | .sum 49 | 50 | case class Gear(part: PartNumber, symbol: Symbol) 51 | 52 | def part2(input: String) = 53 | val all = findPartsAndSymbols(input) 54 | val symbols = all.collect { case s: Symbol => s } 55 | all 56 | .flatMap: 57 | case n: PartNumber => 58 | symbols 59 | .find(_.neighborOf(n)) 60 | .filter(_.sym == "*") 61 | .map(Gear(n, _)) 62 | case _ => None 63 | .groupMap(_.symbol)(_.part.value) 64 | .filter(_._2.length == 2) 65 | .foldLeft(0) { _ + _._2.product } -------------------------------------------------------------------------------- /2023/src/day04.scala: -------------------------------------------------------------------------------- 1 | package day04 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | 6 | @main def part1: Unit = 7 | println(s"The solution is ${part1(loadInput())}") 8 | 9 | @main def part2: Unit = 10 | println(s"The solution is ${part2(loadInput())}") 11 | 12 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day04") 13 | 14 | def countWinning(card: String): Int = 15 | val numbers = card 16 | .substring(card.indexOf(":") + 1) // discard "Card X:" 17 | .split(" ") 18 | .filterNot(_.isEmpty()) 19 | val (winningNumberStrs, givenNumberStrs) = numbers.span(_ != "|") 20 | val winningNumbers = winningNumberStrs.map(_.toInt).toSet 21 | // drop the initial "|" 22 | val givenNumbers = givenNumberStrs.drop(1).map(_.toInt).toSet 23 | winningNumbers.intersect(givenNumbers).size 24 | end countWinning 25 | 26 | def winningCounts(input: String): Iterator[Int] = 27 | input.linesIterator.map(countWinning) 28 | end winningCounts 29 | 30 | def part1(input: String): String = 31 | winningCounts(input) 32 | .map(winning => if winning > 0 then Math.pow(2, winning - 1).toInt else 0) 33 | .sum.toString() 34 | end part1 35 | 36 | def part2(input: String): String = 37 | winningCounts(input) 38 | // we only track the multiplicities of the next few cards as needed, not all of them; 39 | // and the first element always exists, and corresponds to the current card; 40 | // and the elements are always positive (because there is at least 1 original copy of each card) 41 | .foldLeft((0, Vector(1))){ case ((numCards, multiplicities), winning) => 42 | val thisMult = multiplicities(0) 43 | val restMult = multiplicities 44 | .drop(1) 45 | // these are the original copies of the next few cards 46 | .padTo(Math.max(1, winning), 1) 47 | .zipWithIndex 48 | // these are the extra copies we just won 49 | .map((mult, idx) => if idx < winning then mult + thisMult else mult) 50 | (numCards + thisMult, restMult) 51 | } 52 | ._1.toString() 53 | end part2 54 | -------------------------------------------------------------------------------- /2023/src/day10.scala: -------------------------------------------------------------------------------- 1 | package day10 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | 6 | @main def part1: Unit = 7 | println(s"The solution is ${part1(loadInput())}") 8 | 9 | @main def part2: Unit = 10 | println(s"The solution is ${part2(loadInput())}") 11 | 12 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day10") 13 | 14 | def parse(input: String) = input.linesIterator.toSeq 15 | 16 | /** The tiles connected to point `p` in the `grid` */ 17 | def connected(grid: Seq[String])(p: (Int, Int)): Set[(Int, Int)] = 18 | val (i, j) = p 19 | grid(i)(j) match 20 | case '|' => Set((i - 1, j), (i + 1, j)) 21 | case '-' => Set((i, j - 1), (i, j + 1)) 22 | case 'L' => Set((i - 1, j), (i, j + 1)) 23 | case 'J' => Set((i - 1, j), (i, j - 1)) 24 | case '7' => Set((i + 1, j), (i, j - 1)) 25 | case 'F' => Set((i + 1, j), (i, j + 1)) 26 | case '.' => Set() 27 | case 'S' => Set((i + 1, j), (i - 1, j), (i, j + 1), (i, j - 1)) 28 | .filter((i, j) => grid.isDefinedAt(i) && grid(i).isDefinedAt(j)) 29 | .filter(connected(grid)(_).contains(i, j)) 30 | end connected 31 | 32 | /** The loop starting from 'S' in the grid */ 33 | def findLoop(grid: Seq[String]): Seq[(Int, Int)] = 34 | val start = 35 | val startI = grid.indexWhere(_.contains('S')) 36 | (startI, grid(startI).indexOf('S')) 37 | 38 | /** List of connected points starting from 'S' (p0, p1) :: (p1, p2) :: (p2, p3) :: ... */ 39 | val loop = LazyList.iterate((start, connected(grid)(start).head)): (prev, curr) => 40 | val next = connected(grid)(curr) - prev 41 | (curr, next.head) 42 | 43 | start +: loop.map(_._2).takeWhile(_ != start) 44 | end findLoop 45 | 46 | def part1(input: String): String = 47 | val grid = parse(input) 48 | val loop = findLoop(grid) 49 | (loop.length / 2).toString 50 | end part1 51 | 52 | def part2(input: String): String = 53 | val grid = parse(input) 54 | val inLoop = findLoop(grid).toSet 55 | 56 | /** True iff `grid(i)(j)` is a pipe connecting to the north */ 57 | def connectesNorth(i: Int, j: Int): Boolean = connected(grid)(i, j).contains(i - 1, j) 58 | 59 | /** Number of tiles enclosed by the loop in `grid(i)` */ 60 | def enclosedInLine(i: Int): Int = (grid(i).indices.foldLeft(false, 0): 61 | case ((enclosed, count), j) if inLoop(i, j) => (enclosed ^ connectesNorth(i, j), count) 62 | case ((true, count), j) => (true, count + 1) 63 | case ((false, count), j) => (false, count) 64 | )._2 65 | 66 | grid.indices.map(enclosedInLine).sum.toString 67 | end part2 68 | -------------------------------------------------------------------------------- /2023/src/day11.scala: -------------------------------------------------------------------------------- 1 | package day11 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | 6 | @main def part1: Unit = 7 | println(s"The solution is ${part1(loadInput())}") 8 | // println(s"The solution is ${part1(sample1)}") 9 | 10 | @main def part2: Unit = 11 | println(s"The solution is ${part2(loadInput())}") 12 | // println(s"The solution is ${part2(sample1)}") 13 | 14 | def loadInput(): Seq[String] = 15 | loadFileSync(s"$currentDir/../input/day11").linesIterator.toSeq 16 | 17 | def part1(input: Seq[String]) = SweepLine(2)(input) 18 | 19 | def part2(input: Seq[String]) = SweepLine(1_000_000)(input) 20 | 21 | /** Sweep line algorithm. 22 | * 23 | * It follows from the following observations: 24 | * - The distance between two points is simply a distance between the x coordinates, plus 25 | * the difference between the y coordinates. Therefore, we can calculate them independently. 26 | * - When we are calculating one coordinate, the other one does not matter: therefore we can 27 | * simply treat all the other ones as the same; for example, when calculating the x coordinate 28 | * distances, we treat all points having the same y coordinate the same. 29 | * We just need to keep a count of points for each row and column. 30 | * - For each point, we calculate its distance from it to all points coming before (from left->right 31 | * or top->down). Note that for a set of points from before, the total distance from them to a certain 32 | * point on the right increases by "no. of points" x "delta coordinate" as we move that latter point 33 | * by the delta. So we can just keep track of "no. of points" and the total distance. 34 | */ 35 | class SweepLine(expandEmptyBy: Int): 36 | def apply(board: Seq[String]) = 37 | val b = board.map(_.toSeq) 38 | val byRow = countByRow(b).toList 39 | val byCol = countByColumn(b).toList 40 | loop(0, 0, 0)(byRow) + loop(0, 0, 0)(byCol) 41 | 42 | /** Count the number of # for each row in the input board */ 43 | def countByRow(board: Seq[Seq[Char]]) = board.map(_.count(_ == '#')) 44 | /** Same thing, but by columns */ 45 | def countByColumn(board: Seq[Seq[Char]]) = countByRow(board.transpose) 46 | 47 | @scala.annotation.tailrec 48 | private def loop( 49 | /** the number of points we saw */ 50 | points: Int, 51 | /** The total distance from all points we saw */ 52 | totalDistance: Long, 53 | /** The accumulated sum of distance so far */ 54 | accum: Long 55 | )( 56 | /** The list of count */ 57 | counts: List[Int] 58 | ): Long = counts match 59 | case Nil => accum /* no more rows */ 60 | case 0 :: next => /* empty row, we expand it by [[expandEmptyBy]] */ 61 | loop(points, totalDistance + points.toLong * expandEmptyBy, accum)(next) 62 | case now :: next => /* non-empty row */ 63 | val addedDistance = now * totalDistance /* from each point `totalDistance` is the sum of distance to all previous points */ 64 | val addedPoints = points + now 65 | loop(addedPoints, totalDistance + addedPoints, accum + addedDistance)(next) 66 | -------------------------------------------------------------------------------- /2023/src/day12.scala: -------------------------------------------------------------------------------- 1 | package day12 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | 6 | /** The example puzzle from the problem description. */ 7 | val examplePuzzle = IArray( 8 | "???.### 1,1,3", 9 | ".??..??...?##. 1,1,3", 10 | "?#?#?#?#?#?#?#? 1,3,1,6", 11 | "????.#...#... 4,1,1", 12 | "????.######..#####. 1,6,5", 13 | "###???????? 3,2,1" 14 | ) 15 | 16 | /** Our personal puzzle input. */ 17 | val personalPuzzle = loadFileSync(s"$currentDir/../input/day12") 18 | 19 | val slowPuzzleSize = 16 20 | val slowPuzzle = 21 | ("??." * slowPuzzleSize) + " " + ("1," * (slowPuzzleSize - 1)) + "1" 22 | 23 | /** Entry point for part 1. */ 24 | @main def part1(): Unit = println(countAll(personalPuzzle)) 25 | 26 | /** Sums `countRow` over all rows in `input`. */ 27 | def countAll(input: String): Long = input.split("\n").map(countRow).sum 28 | 29 | /** Counts all of the different valid arrangements of operational and broken 30 | * springs in the given row. 31 | */ 32 | def countRow(input: String): Long = 33 | val Array(conditions, damagedCounts) = input.split(" ") 34 | count2( 35 | conditions.toList, 36 | damagedCounts.split(",").map(_.toInt).toList 37 | ) 38 | 39 | val cache = collection.mutable.Map.empty[(List[Char], List[Int], Long), Long] 40 | private def count(input: List[Char], ds: List[Int], d: Int = 0): Long = 41 | cache.getOrElseUpdate((input, ds, d), countUncached(input, ds, d)) 42 | 43 | var ops = 0 44 | def countUncached(input: List[Char], ds: List[Int], d: Int = 0): Long = 45 | ops += 1 46 | // We've reached the end of the input. 47 | if input.isEmpty then 48 | // This is a valid arrangement if there are no sequences of damaged springs 49 | // left to place (ds.isEmpty) and we're not currently in a sequence of 50 | // damaged springs (d == 0). 51 | if ds.isEmpty && d == 0 then 1L 52 | // This is also a valid arrangement if there is one sequence of damaged 53 | // springs left to place (ds.length == 1) and its size is d (ds.head == d). 54 | else if ds.length == 1 && ds.head == d then 1L 55 | // Otherwise, this is not a valid arrangement. 56 | else 0 57 | else 58 | def operationalCase() = 59 | // We can consume all following operational springs. 60 | if d == 0 then count(input.tail, ds, 0) 61 | // We are currently in a sequence of damaged springs, which this 62 | // operational spring ends. If the length of the damaged sequence is the 63 | // expected one, the we can continue with the next damaged sequence. 64 | else if !ds.isEmpty && ds.head == d then count(input.tail, ds.tail, 0) 65 | // Otherwise, this is not a valid arrangement. 66 | else 0L 67 | def damagedCase() = 68 | // If no damaged springs are expected, then this is not a valid 69 | // arrangement. 70 | if ds.isEmpty then 0L 71 | // Optimization: no need to recurse if d becomes greater than the 72 | // expected damaged sequence length. 73 | else if d == ds.head then 0L 74 | // Otherwise, we can consume a damaged spring. 75 | else count(input.tail, ds, d + 1) 76 | input.head match 77 | // If we encounter a question mark, this position can be either an 78 | // operational or a damaged spring. 79 | case '?' => operationalCase() + damagedCase() 80 | // If we encounter a dot, this position has an operational spring. 81 | case '.' => operationalCase() 82 | // If we encounter a hash, this position has damaged spring. 83 | case '#' => damagedCase() 84 | 85 | extension (b: Boolean) private inline def toLong: Long = if b then 1L else 0L 86 | 87 | def count2(input: List[Char], ds: List[Int]): Long = 88 | val dim1 = input.length + 1 89 | val dim2 = ds.length + 1 90 | val cache = Array.fill(dim1 * dim2)(-1L) 91 | 92 | inline def count2Cached(input: List[Char], ds: List[Int]): Long = 93 | val key = input.length * dim2 + ds.length 94 | val result = cache(key) 95 | if result == -1L then 96 | val result = count2Uncached(input, ds) 97 | cache(key) = result 98 | result 99 | else result 100 | 101 | def count2Uncached(input: List[Char], ds: List[Int]): Long = 102 | ops += 1 103 | // We've seen all expected damaged sequences. The arrangement is therefore 104 | // valid only if the input does not contain damaged springs. 105 | if ds.isEmpty then input.forall(_ != '#').toLong 106 | // The input is empty but we expected some damaged springs, so this is not a 107 | // valid arrangement. 108 | else if input.isEmpty then 0L 109 | else 110 | inline def operationalCase(): Long = 111 | // Operational case: we can consume all operational springs to get to 112 | // the next choice. 113 | count2Cached(input.tail.dropWhile(_ == '.'), ds) 114 | inline def damagedCase(): Long = 115 | // If the length of the input is less than the expected length of the 116 | // damaged sequence, then this is not a valid arrangement. 117 | if input.length < ds.head then 0L 118 | else 119 | // Split the input into a group of length ds.head and the rest. 120 | val (group, rest) = input.splitAt(ds.head) 121 | // If the group contains any operational springs, then this is not a a 122 | // group of damaged springs, so this is not a valid arrangement. 123 | if !group.forall(_ != '.') then 0L 124 | // If the rest of the input is empty, then this is a valid arrangement 125 | // only if the damaged sequence is the last one expected. 126 | else if rest.isEmpty then ds.tail.isEmpty.toLong 127 | // If we now have a damaged spring, then this is not the end of a 128 | // damaged sequence as expected, and therefore not a valid 129 | // arrangement. 130 | else if rest.head == '#' then 0L 131 | // Otherwise, we can continue with the rest of the input and the next 132 | // expected damaged sequence. 133 | else count2Cached(rest.tail, ds.tail) 134 | input.head match 135 | case '?' => operationalCase() + damagedCase() 136 | case '.' => operationalCase() 137 | case '#' => damagedCase() 138 | 139 | count2Cached(input, ds) 140 | 141 | @main def countOps = 142 | val puzzles = 143 | IArray( 144 | ("example", examplePuzzle.mkString("\n"), false), 145 | ("personal", personalPuzzle, false), 146 | ("slow", slowPuzzle, false), 147 | ("personal unfolded", personalPuzzle, true) 148 | ) 149 | for (name, input, unfold) <- puzzles do 150 | ops = 0 151 | val start = System.nanoTime() 152 | val result = if unfold then countAllUnfolded(input) else countAll(input) 153 | val end = System.nanoTime() 154 | val elapsed = (end - start) / 1_000_000 155 | println(f"$name%17s: $result%13d ($ops%9d calls, $elapsed%4d ms)") 156 | 157 | @main def part2(): Unit = 158 | println(countAllUnfolded(personalPuzzle)) 159 | 160 | def countAllUnfolded(input: String): Long = 161 | input.split("\n").map(unfoldRow).map(countRow).sum 162 | 163 | def unfoldRow(input: String): String = 164 | val Array(conditions, damagedCounts) = input.split(" ") 165 | val conditionsUnfolded = (0 until 5).map(_ => conditions).mkString("?") 166 | val damagedCountUnfolded = (0 until 5).map(_ => damagedCounts).mkString(",") 167 | f"$conditionsUnfolded $damagedCountUnfolded" 168 | -------------------------------------------------------------------------------- /2023/src/day12.test.scala: -------------------------------------------------------------------------------- 1 | package day12 2 | 3 | class Day12Test extends munit.FunSuite: 4 | test("example row 1"): 5 | assertEquals(countRow(examplePuzzle(0)), 1L) 6 | 7 | test("example row 2"): 8 | assertEquals(countRow(examplePuzzle(1)), 4L) 9 | 10 | test("example row 3"): 11 | assertEquals(countRow(examplePuzzle(2)), 1L) 12 | 13 | test("example row 4"): 14 | assertEquals(countRow(examplePuzzle(3)), 1L) 15 | 16 | test("example row 5"): 17 | assertEquals(countRow(examplePuzzle(4)), 4L) 18 | 19 | test("example row 6"): 20 | assertEquals(countRow(examplePuzzle(5)), 10L) 21 | 22 | test("example"): 23 | assertEquals(countAll(examplePuzzle.mkString("\n")), 21L) 24 | 25 | test("puzzle input"): 26 | assertEquals(countAll(personalPuzzle), 7118L) 27 | 28 | test("puzzle input 2"): 29 | assertEquals(countAllUnfolded(personalPuzzle), 7030194981795L) 30 | 31 | test("slow"): 32 | assertEquals(countAll(slowPuzzle), 1L << slowPuzzleSize) 33 | 34 | test("unfold example"): 35 | assertEquals(unfoldRow(".# 1"), ".#?.#?.#?.#?.# 1,1,1,1,1") 36 | -------------------------------------------------------------------------------- /2023/src/day13.scala: -------------------------------------------------------------------------------- 1 | package day13 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | import scala.collection.mutable.Buffer 6 | 7 | @main def part1: Unit = 8 | println(s"The solution is ${part1(loadInput())}") 9 | 10 | @main def part2: Unit = 11 | println(s"The solution is ${part2(loadInput())}") 12 | 13 | def loadInput(): Seq[String] = 14 | loadFileSync(s"$currentDir/../input/day13").linesIterator.toSeq 15 | 16 | type Tile = '.' | '#' 17 | type Line = Seq[Tile] 18 | type Pattern = Seq[Line] 19 | 20 | def part1(input: Seq[String]): Int = 21 | parseInput(input) 22 | .flatMap: pattern => 23 | findReflection(pattern).map(100 * _).orElse(findReflection(pattern.transpose)) 24 | .sum 25 | 26 | def part2(input: Seq[String]) = 27 | parseInput(input) 28 | .flatMap: pattern => 29 | findReflectionWithSmudge(pattern).map(100 * _) 30 | .orElse(findReflectionWithSmudge(pattern.transpose)) 31 | .sum 32 | 33 | def parseInput(input: Seq[String]): Seq[Pattern] = 34 | val currentPattern = Buffer.empty[Line] 35 | val patterns = Buffer.empty[Pattern] 36 | def addPattern() = 37 | patterns += currentPattern.toSeq 38 | currentPattern.clear() 39 | for lineStr <- input do 40 | if lineStr.isEmpty then addPattern() 41 | else 42 | val line = lineStr.collect[Tile] { case tile: Tile => tile } 43 | currentPattern += line 44 | addPattern() 45 | patterns.toSeq 46 | 47 | def findReflection(pattern: Pattern): Option[Int] = 48 | 1.until(pattern.size).find: i => 49 | val (leftPart, rightPart) = pattern.splitAt(i) 50 | leftPart.reverse.zip(rightPart).forall(_ == _) 51 | 52 | def findReflectionWithSmudge(pattern: Pattern): Option[Int] = 53 | 1.until(pattern.size).find: i => 54 | val (leftPart, rightPart) = pattern.splitAt(i) 55 | val smudges = leftPart.reverse 56 | .zip(rightPart) 57 | .map((l1, l2) => l1.zip(l2).count(_ != _)) 58 | .sum 59 | smudges == 1 60 | -------------------------------------------------------------------------------- /2023/src/day15.scala: -------------------------------------------------------------------------------- 1 | package day15 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | 6 | @main def part1: Unit = 7 | println(s"The solution is ${part1(loadInput())}") 8 | 9 | @main def part2: Unit = 10 | println(s"The solution is ${part2(loadInput())}") 11 | 12 | def loadInput(): String = 13 | loadFileSync(s"$currentDir/../input/day15") 14 | 15 | /** The `HASH` function. */ 16 | def hash(sequence: String): Int = 17 | sequence.foldLeft(0) { (prev, c) => 18 | ((prev + c.toInt) * 17) % 256 19 | } 20 | end hash 21 | 22 | /** Parses the input into a list of sequences. */ 23 | def inputToSequences(input: String): List[String] = 24 | input.filter(_ != '\n').split(',').toList 25 | 26 | def part1(input: String): String = 27 | val sequences = inputToSequences(input) 28 | val result = sequences.map(hash(_)).sum 29 | println(result) 30 | result.toString() 31 | end part1 32 | 33 | /** A labeled lens, as found in the boxes. */ 34 | final case class LabeledLens(label: String, focalLength: Int) 35 | 36 | def part2(input: String): String = 37 | val steps = inputToSequences(input) 38 | 39 | val boxes = Array.fill[List[LabeledLens]](256)(Nil) 40 | 41 | // --- Processing all the steps -------------------- 42 | 43 | // Remove the lens with the given label from the box it belongs to 44 | def removeLens(label: String): Unit = 45 | val boxIndex = hash(label) 46 | boxes(boxIndex) = boxes(boxIndex).filter(_.label != label) 47 | 48 | // Add a lens in the contents of a box; replace an existing label or add to the end 49 | def addLensToList(lens: LabeledLens, list: List[LabeledLens]): List[LabeledLens] = 50 | list match 51 | case Nil => lens :: Nil // add to the end 52 | case LabeledLens(lens.label, _) :: tail => lens :: tail // replace 53 | case head :: tail => head :: addLensToList(lens, tail) // keep looking 54 | 55 | // Add a lens with the given label and focal length into the box it belongs to, in the right place 56 | def addLens(label: String, focalLength: Int): Unit = 57 | val lens = LabeledLens(label, focalLength) 58 | val boxIndex = hash(label) 59 | boxes(boxIndex) = addLensToList(lens, boxes(boxIndex)) 60 | 61 | // Parse and execute the steps 62 | for step <- steps do 63 | step match 64 | case s"$label-" => removeLens(label) 65 | case s"$label=$focalLength" => addLens(label, focalLength.toInt) 66 | 67 | // --- Computing the focusing power -------------------- 68 | 69 | // Focusing power of a lens in a given box and at a certain position within that box 70 | def focusingPower(boxIndex: Int, lensIndex: Int, lens: LabeledLens): Int = 71 | (boxIndex + 1) * (lensIndex + 1) * lens.focalLength 72 | 73 | // Focusing power of all the lenses 74 | val focusingPowers = 75 | for 76 | (box, boxIndex) <- boxes.zipWithIndex 77 | (lens, lensIndex) <- box.zipWithIndex 78 | yield 79 | focusingPower(boxIndex, lensIndex, lens) 80 | 81 | // Sum it up 82 | val result = focusingPowers.sum 83 | result.toString() 84 | end part2 85 | -------------------------------------------------------------------------------- /2023/src/day17.scala: -------------------------------------------------------------------------------- 1 | package day17 2 | // based on solution from https://github.com/stewSquared/adventofcode/blob/src/main/scala/2023/Day17.worksheet.sc 3 | 4 | import locations.Directory.currentDir 5 | import inputs.Input.loadFileSync 6 | 7 | @main def part1: Unit = 8 | println(s"The solution is ${search(_.nextStates)}") 9 | 10 | @main def part2: Unit = 11 | println(s"The solution is ${search(_.nextStates2)}") 12 | 13 | def loadInput(): Vector[Vector[Int]] = Vector.from: 14 | val file = loadFileSync(s"$currentDir/../input/day17") 15 | for line <- file.split("\n") 16 | yield line.map(_.asDigit).toVector 17 | 18 | enum Dir: 19 | case N, S, E, W 20 | 21 | def turnRight = this match 22 | case Dir.N => E 23 | case Dir.E => S 24 | case Dir.S => W 25 | case Dir.W => N 26 | 27 | def turnLeft = this match 28 | case Dir.N => W 29 | case Dir.W => S 30 | case Dir.S => E 31 | case Dir.E => N 32 | 33 | val grid = loadInput() 34 | 35 | val xRange = grid.head.indices 36 | val yRange = grid.indices 37 | 38 | case class Point(x: Int, y: Int): 39 | def move(dir: Dir) = dir match 40 | case Dir.N => copy(y = y - 1) 41 | case Dir.S => copy(y = y + 1) 42 | case Dir.E => copy(x = x + 1) 43 | case Dir.W => copy(x = x - 1) 44 | 45 | def inBounds(p: Point) = 46 | xRange.contains(p.x) && yRange.contains(p.y) 47 | 48 | def heatLoss(p: Point) = 49 | if inBounds(p) then grid(p.y)(p.x) else 0 50 | 51 | case class State(pos: Point, dir: Dir, streak: Int): 52 | def straight: State = 53 | State(pos.move(dir), dir, streak + 1) 54 | 55 | def turnLeft: State = 56 | val newDir = dir.turnLeft 57 | State(pos.move(newDir), newDir, 1) 58 | 59 | def turnRight: State = 60 | val newDir = dir.turnRight 61 | State(pos.move(newDir), newDir, 1) 62 | 63 | def nextStates: List[State] = 64 | List(straight, turnLeft, turnRight).filter: s => 65 | inBounds(s.pos) && s.streak <= 3 66 | 67 | def nextStates2: List[State] = 68 | if streak < 4 then List(straight) 69 | else List(straight, turnLeft, turnRight).filter: s => 70 | inBounds(s.pos) && s.streak <= 10 71 | 72 | def search(next: State => List[State]): Int = 73 | import collection.mutable.{PriorityQueue, Map} 74 | 75 | val minHeatLoss = Map.empty[State, Int] 76 | 77 | given Ordering[State] = Ordering.by(minHeatLoss) 78 | val pq = PriorityQueue.empty[State].reverse 79 | 80 | var visiting = State(Point(0, 0), Dir.E, 0) 81 | minHeatLoss(visiting) = 0 82 | 83 | val end = Point(xRange.max, yRange.max) 84 | while visiting.pos != end do 85 | val states = next(visiting).filterNot(minHeatLoss.contains) 86 | states.foreach: s => 87 | minHeatLoss(s) = minHeatLoss(visiting) + heatLoss(s.pos) 88 | pq.enqueue(s) 89 | visiting = pq.dequeue() 90 | 91 | minHeatLoss(visiting) 92 | -------------------------------------------------------------------------------- /2023/src/day18.scala: -------------------------------------------------------------------------------- 1 | package src 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | 6 | @main def part1: Unit = 7 | println(s"The solution is ${part1(loadInput())}") 8 | 9 | @main def part2: Unit = 10 | println(s"The solution is ${part2(loadInput())}") 11 | 12 | @main def test: Unit = 13 | assert(part1(loadInput()) == "38188") 14 | assert(part2(loadInput()) == "93325849869340") 15 | 16 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day18") 17 | 18 | enum Direction: 19 | case Up, Down, Left, Right 20 | object Direction: 21 | def fromChar(c: Char): Direction = c match 22 | case 'U' => Up case 'D' => Down case 'L' => Left case 'R' => Right 23 | def fromInt(i: Char): Direction = i match 24 | case '0' => Right case '1' => Down case '2' => Left case '3' => Up 25 | import Direction.* 26 | 27 | case class Trench(dir: Direction, length: Int) 28 | 29 | def area(digPlan: Seq[Trench]): Long = 30 | val (_, area) = digPlan.foldLeft((0, 0), 1L): 31 | case (((x, y), area), Trench(dir, len)) => dir match 32 | case Right => ((x + len, y), area + len) 33 | case Down => ((x, y + len), area + (x + 1) * len.toLong) 34 | case Left => ((x - len, y), area) 35 | case Up => ((x, y - len), area - x * len.toLong) 36 | area 37 | 38 | def part1(input: String): String = 39 | val digPlan = for 40 | case s"$dirC $len (#$_)" <- input.linesIterator 41 | dir = Direction.fromChar(dirC.head) 42 | yield Trench(dir, len.toInt) 43 | 44 | area(digPlan.toSeq).toString 45 | 46 | def part2(input: String): String = 47 | val digPlan = for 48 | case s"$_ $_ (#$color)" <- input.linesIterator 49 | dir = Direction.fromInt(color.last) 50 | len = BigInt(x = color.init, radix = 16) 51 | yield Trench(dir, len.toInt) 52 | 53 | area(digPlan.toSeq).toString 54 | -------------------------------------------------------------------------------- /2023/src/day19.scala: -------------------------------------------------------------------------------- 1 | package day19 2 | 3 | import inputs.Input.loadFileSync 4 | import locations.Directory.currentDir 5 | 6 | val personalPuzzle = loadFileSync(f"$currentDir/../input/day19") 7 | 8 | /*-----------------*/ 9 | /* Data structures */ 10 | /*-----------------*/ 11 | 12 | enum Channel: 13 | case X, M, A, S 14 | 15 | enum Operator: 16 | case LessThan, GreaterThan 17 | 18 | enum Result: 19 | case Reject, Accept 20 | 21 | enum Instruction: 22 | case IfThenElse( 23 | channel: Channel, 24 | operator: Operator, 25 | value: Int, 26 | thenBranch: GoTo | Return, 27 | elseBranch: Instruction 28 | ) 29 | case Return(result: Result) 30 | case GoTo(target: String) 31 | 32 | import Instruction.* 33 | 34 | type Workflow = Map[String, Instruction] 35 | 36 | case class Part(x: Int, m: Int, a: Int, s: Int) 37 | 38 | /*---------*/ 39 | /* Parsing */ 40 | /*---------*/ 41 | 42 | object Channel: 43 | def parse(input: String): Channel = 44 | input match 45 | case "x" => Channel.X 46 | case "m" => Channel.M 47 | case "a" => Channel.A 48 | case "s" => Channel.S 49 | case _ => throw Exception(s"Invalid channel: $input") 50 | 51 | object Operator: 52 | def parse(input: String): Operator = 53 | input match 54 | case "<" => Operator.LessThan 55 | case ">" => Operator.GreaterThan 56 | case _ => throw Exception(s"Invalid operator: $input") 57 | 58 | object Result: 59 | def parse(input: String): Result = 60 | input match 61 | case "R" => Result.Reject 62 | case "A" => Result.Accept 63 | case _ => throw Exception(s"Invalid result: $input") 64 | 65 | object Instruction: 66 | private val IfThenElseRegex = """([xmas])([<>])(\d+):(\w+),(.*)""".r 67 | private val ReturnRegex = """([RA])""".r 68 | private val GoToRegex = """(\w+)""".r 69 | def parse(input: String): Instruction = 70 | input match 71 | case IfThenElseRegex(channel, operator, value, thenBranch, elseBranch) => 72 | Instruction.parse(thenBranch) match 73 | case thenBranch: (GoTo | Return) => 74 | IfThenElse( 75 | Channel.parse(channel), 76 | Operator.parse(operator), 77 | value.toInt, 78 | thenBranch, 79 | Instruction.parse(elseBranch) 80 | ) 81 | case _ => throw Exception(s"Invalid then branch: $thenBranch") 82 | case ReturnRegex(result) => Return(Result.parse(result)) 83 | case GoToRegex(target) => GoTo(target) 84 | case _ => throw Exception(s"Invalid instruction: $input") 85 | 86 | object Workflow: 87 | def parse(input: String): Workflow = 88 | input.split("\n").map(parseBlock).toMap 89 | 90 | private val BlockRegex = """(\w+)\{(.*?)\}""".r 91 | private def parseBlock(input: String): (String, Instruction) = 92 | input match 93 | case BlockRegex(label, body) => 94 | (label, Instruction.parse(body)) 95 | 96 | object Part: 97 | val PartRegex = """\{x=(\d+),m=(\d+),a=(\d+),s=(\d+)\}""".r 98 | def parse(input: String): Part = 99 | input match 100 | case PartRegex(x, m, a, s) => Part(x.toInt, m.toInt, a.toInt, s.toInt) 101 | case _ => throw Exception(s"Invalid part: $input") 102 | 103 | /*---------------------*/ 104 | /* Evaluation (part 1) */ 105 | /*---------------------*/ 106 | 107 | @main def part1: Unit = println(part1(personalPuzzle)) 108 | 109 | def part1(input: String): Int = 110 | val Array(workflowLines, partLines) = input.split("\n\n") 111 | val workflow = Workflow.parse(workflowLines.trim()) 112 | val parts = partLines.trim().split("\n").map(Part.parse) 113 | 114 | def eval(part: Part, instruction: Instruction): Result = 115 | instruction match 116 | case IfThenElse(channel, operator, value, thenBranch, elseBranch) => 117 | val channelValue = channel match 118 | case Channel.X => part.x 119 | case Channel.M => part.m 120 | case Channel.A => part.a 121 | case Channel.S => part.s 122 | val result = operator match 123 | case Operator.LessThan => channelValue < value 124 | case Operator.GreaterThan => channelValue > value 125 | if result then eval(part, thenBranch) else eval(part, elseBranch) 126 | case Return(result) => result 127 | case GoTo(target) => eval(part, workflow(target)) 128 | 129 | parts 130 | .collect: part => 131 | eval(part, workflow("in")) match 132 | case Result.Reject => 0 133 | case Result.Accept => part.x + part.m + part.a + part.s 134 | .sum 135 | 136 | /*------------------------------*/ 137 | /* Abstract evaluation (part 2) */ 138 | /*-----------------.------------*/ 139 | 140 | case class Range(from: Long, until: Long): 141 | assert(from < until) 142 | def count() = until - from 143 | 144 | object Range: 145 | def safe(from: Long, until: Long): Option[Range] = 146 | if from < until then Some(Range(from, until)) else None 147 | 148 | case class AbstractPart(x: Range, m: Range, a: Range, s: Range): 149 | def count() = x.count() * m.count() * a.count() * s.count() 150 | 151 | def withChannel(channel: Channel, newRange: Range) = 152 | channel match 153 | case Channel.X => copy(x = newRange) 154 | case Channel.M => copy(m = newRange) 155 | case Channel.A => copy(a = newRange) 156 | case Channel.S => copy(s = newRange) 157 | 158 | def getChannel(channel: Channel) = 159 | channel match 160 | case Channel.X => x 161 | case Channel.M => m 162 | case Channel.A => a 163 | case Channel.S => s 164 | 165 | def split( 166 | channel: Channel, 167 | value: Int 168 | ): (Option[AbstractPart], Option[AbstractPart]) = 169 | val currentRange = getChannel(channel) 170 | ( 171 | Range.safe(currentRange.from, value).map(withChannel(channel, _)), 172 | Range.safe(value, currentRange.until).map(withChannel(channel, _)) 173 | ) 174 | 175 | @main def part2: Unit = println(part2(personalPuzzle, 4001)) 176 | 177 | def part2(input: String, until: Long): Long = 178 | val Array(workflowLines, _) = input.split("\n\n") 179 | val workflow = Workflow.parse(workflowLines.trim()) 180 | 181 | def count(part: AbstractPart, instruction: Instruction): Long = 182 | instruction match 183 | case IfThenElse(channel, operator, value, thenBranch, elseBranch) => 184 | val (trueValues, falseValues) = 185 | operator match 186 | case Operator.LessThan => part.split(channel, value) 187 | case Operator.GreaterThan => part.split(channel, value + 1).swap 188 | trueValues.map(count(_, thenBranch)).getOrElse(0L) 189 | + falseValues.map(count(_, elseBranch)).getOrElse(0L) 190 | case Return(Result.Accept) => part.count() 191 | case Return(Result.Reject) => 0L 192 | case GoTo(target) => count(part, workflow(target)) 193 | 194 | count( 195 | AbstractPart( 196 | Range(1, until), 197 | Range(1, until), 198 | Range(1, until), 199 | Range(1, until) 200 | ), 201 | workflow("in") 202 | ) 203 | -------------------------------------------------------------------------------- /2023/src/day19.test.scala: -------------------------------------------------------------------------------- 1 | package day19 2 | 3 | class Day19Test extends munit.FunSuite: 4 | test("Workflow.parse: example line 2"): 5 | assertEquals( 6 | Workflow.parse("pv{a>1716:R,A}"), 7 | Map( 8 | "pv" -> Instruction.IfThenElse( 9 | Channel.A, 10 | Operator.GreaterThan, 11 | 1716, 12 | Instruction.Return(Result.Reject), 13 | Instruction.Return(Result.Accept) 14 | ) 15 | ) 16 | ) 17 | 18 | test("Workflow.parse: example line 1"): 19 | assertEquals( 20 | Workflow.parse("px{a<2006:qkq,m>2090:A,rfg}"), 21 | Map( 22 | "px" -> Instruction.IfThenElse( 23 | Channel.A, 24 | Operator.LessThan, 25 | 2006, 26 | Instruction.GoTo("qkq"), 27 | Instruction.IfThenElse( 28 | Channel.M, 29 | Operator.GreaterThan, 30 | 2090, 31 | Instruction.Return(Result.Accept), 32 | Instruction.GoTo("rfg") 33 | ) 34 | ) 35 | ) 36 | ) 37 | 38 | test("Part.parse: example line 12"): 39 | assertEquals( 40 | Part.parse("{x=787,m=2655,a=1222,s=2876}"), 41 | Part(787, 2655, 1222, 2876) 42 | ) 43 | 44 | val examplePuzzle = """px{a<2006:qkq,m>2090:A,rfg} 45 | |pv{a>1716:R,A} 46 | |lnx{m>1548:A,A} 47 | |rfg{s<537:gd,x>2440:R,A} 48 | |qs{s>3448:A,lnx} 49 | |qkq{x<1416:A,crn} 50 | |crn{x>2662:A,R} 51 | |in{s<1351:px,qqz} 52 | |qqz{s>2770:qs,m<1801:hdj,R} 53 | |gd{a>3333:R,R} 54 | |hdj{m>838:A,pv} 55 | | 56 | |{x=787,m=2655,a=1222,s=2876} 57 | |{x=1679,m=44,a=2067,s=496} 58 | |{x=2036,m=264,a=79,s=2244} 59 | |{x=2461,m=1339,a=466,s=291} 60 | |{x=2127,m=1623,a=2188,s=1013}""".stripMargin.trim 61 | 62 | test("part1: example"): 63 | assertEquals(part1(examplePuzzle), 19114) 64 | 65 | test("Range.count"): 66 | assertEquals(Range(0, 1).count(), 1L) 67 | assertEquals(Range(0, 2).count(), 2L) 68 | 69 | test("AbstractPart.count"): 70 | assertEquals( 71 | AbstractPart(Range(0, 1), Range(0, 2), Range(0, 3), Range(0, 4)).count(), 72 | 24L 73 | ) 74 | 75 | test("AbstractPart.split: X"): 76 | assertEquals( 77 | AbstractPart(Range(0, 1), Range(0, 2), Range(0, 3), Range(0, 4)) 78 | .split(Channel.X, 0), 79 | ( 80 | None, 81 | Some(AbstractPart(Range(0, 1), Range(0, 2), Range(0, 3), Range(0, 4))) 82 | ) 83 | ) 84 | 85 | test("AbstractPart.split: A"): 86 | assertEquals( 87 | AbstractPart(Range(0, 1), Range(0, 2), Range(0, 3), Range(0, 4)) 88 | .split(Channel.A, 1), 89 | ( 90 | Some(AbstractPart(Range(0, 1), Range(0, 2), Range(0, 1), Range(0, 4))), 91 | Some(AbstractPart(Range(0, 1), Range(0, 2), Range(1, 3), Range(0, 4))) 92 | ) 93 | ) 94 | 95 | test("part2: example"): 96 | assertEquals(part2(examplePuzzle, 4001), 167409079868000L) 97 | 98 | val minimalExample = """in{s<1000:A,A} 99 | | 100 | |{x=0,m=0,a=0,s=0}""".stripMargin.trim 101 | 102 | test("part2: minimal example"): 103 | assertEquals(part2(minimalExample, 4001), 4000L * 4000L * 4000L * 4000L) 104 | 105 | val minimalExample2 = """in{x<2001:R,A} 106 | | 107 | |{x=0,m=0,a=0,s=0}""".stripMargin.trim 108 | 109 | test("part2: minimal example 2"): 110 | assertEquals(part2(minimalExample2, 4001), 2000L * 4000L * 4000L * 4000L) 111 | 112 | val minimalExample3 = """in{x<2001:A,cont} 113 | |cont{m>1000:R,A} 114 | | 115 | |{x=0,m=0,a=0,s=0}""".stripMargin.trim 116 | 117 | test("part2: minimal example 3"): 118 | assertEquals( 119 | part2(minimalExample3, 4001), 120 | ((2000L * 4000L) + (2000L * 1000L)) * 4000L * 4000L 121 | ) 122 | 123 | val minimalExample4 = """in{x<2:A,cont} 124 | |cont{m>1:R,R} 125 | | 126 | |{x=0,m=0,a=0,s=0}""".stripMargin.trim 127 | 128 | test("part2: minimal example 4"): 129 | assertEquals(part2(minimalExample4, 4), (1L * 3L * 3L * 3L)) 130 | 131 | val minimalExample5 = """in{x<2:A,cont} 132 | |cont{m>1:R,A} 133 | | 134 | |{x=0,m=0,a=0,s=0}""".stripMargin.trim 135 | 136 | test("part2: minimal example 5"): 137 | assertEquals( 138 | part2(minimalExample5, 4), 139 | (1L * 3L * 3L * 3L) + (2L * 3L * 1L * 3L) 140 | ) 141 | -------------------------------------------------------------------------------- /2023/src/day20.scala: -------------------------------------------------------------------------------- 1 | package day20 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | import scala.annotation.tailrec 6 | import scala.collection.immutable.Queue 7 | 8 | @main def part1: Unit = 9 | println(s"The solution is ${part1(loadInput())}") 10 | // println(s"The solution is ${part1(sample1)}") 11 | 12 | @main def part2: Unit = 13 | println(s"The solution is ${part2(loadInput())}") 14 | // println(s"The solution is ${part2(sample1)}") 15 | 16 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day20") 17 | 18 | val sample1 = """ 19 | broadcaster -> a 20 | %a -> inv, con 21 | &inv -> b 22 | %b -> con 23 | &con -> output 24 | """.strip 25 | 26 | type ModuleName = String 27 | 28 | // Pulses are the messages of our primary state machine. They are either low 29 | // (false) or high (true) and travel from a source to a destination module 30 | 31 | final case class Pulse( 32 | source: ModuleName, 33 | destination: ModuleName, 34 | level: Boolean, 35 | ) 36 | 37 | object Pulse: 38 | final val ButtonPress = Pulse("button", "broadcaster", false) 39 | 40 | // The modules include pass-throughs which simply forward pulses, flip flips 41 | // which toggle state and emit when they receive a low pulse, and conjunctions 42 | // which emit a low signal when all inputs are high. 43 | 44 | sealed trait Module: 45 | def name: ModuleName 46 | def destinations: Vector[ModuleName] 47 | // Generate pulses for all the destinations of this module 48 | def pulses(level: Boolean): Vector[Pulse] = 49 | destinations.map(Pulse(name, _, level)) 50 | end Module 51 | 52 | final case class PassThrough( 53 | name: ModuleName, 54 | destinations: Vector[ModuleName], 55 | ) extends Module 56 | 57 | final case class FlipFlop( 58 | name: ModuleName, 59 | destinations: Vector[ModuleName], 60 | state: Boolean, 61 | ) extends Module 62 | 63 | final case class Conjunction( 64 | name: ModuleName, 65 | destinations: Vector[ModuleName], 66 | // The source modules that most-recently sent a high pulse 67 | state: Set[ModuleName], 68 | ) extends Module 69 | 70 | // The machine comprises a collection of named modules and a map that gathers 71 | // which modules serve as sources for each module in the machine. 72 | 73 | final case class Machine( 74 | modules: Map[ModuleName, Module], 75 | sources: Map[ModuleName, Set[ModuleName]] 76 | ): 77 | inline def +(module: Module): Machine = 78 | copy( 79 | modules = modules.updated(module.name, module), 80 | sources = module.destinations.foldLeft(sources): (sources, destination) => 81 | sources.updatedWith(destination): 82 | case None => Some(Set(module.name)) 83 | case Some(values) => Some(values + module.name) 84 | ) 85 | end Machine 86 | 87 | object Machine: 88 | final val Initial = Machine(Map.empty, Map.empty) 89 | 90 | // To parse the input we first parse all of the modules and then fold them 91 | // into a new machine 92 | 93 | def parse(input: String): Machine = 94 | val modules = input.linesIterator.map: 95 | case s"%$name -> $targets" => 96 | FlipFlop(name, targets.split(", ").toVector, false) 97 | case s"&$name -> $targets" => 98 | Conjunction(name, targets.split(", ").toVector, Set.empty) 99 | case s"$name -> $targets" => 100 | PassThrough(name, targets.split(", ").toVector) 101 | modules.foldLeft(Initial)(_ + _) 102 | end Machine 103 | 104 | // The primary state machine state comprises the machine itself, the number of 105 | // button presses and a queue of outstanding pulses. 106 | 107 | final case class MachineFSM( 108 | machine: Machine, 109 | presses: Long = 0, 110 | queue: Queue[Pulse] = Queue.empty, 111 | ): 112 | def nextState: MachineFSM = queue.dequeueOption match 113 | // If the queue is empty, we increment the button presses and enqueue a 114 | // button press pulse 115 | case None => 116 | copy(presses = presses + 1, queue = Queue(Pulse.ButtonPress)) 117 | 118 | case Some((Pulse(source, destination, level), tail)) => 119 | machine.modules.get(destination) match 120 | // If a pulse reaches a pass-through, enqueue pulses for all the module 121 | // destinations 122 | case Some(passThrough: PassThrough) => 123 | copy(queue = tail ++ passThrough.pulses(level)) 124 | 125 | // If a low pulse reaches a flip-flop, update the flip-flop state in the 126 | // machine and enqueue pulses for all the module destinations 127 | case Some(flipFlop: FlipFlop) if !level => 128 | val flipFlop2 = flipFlop.copy(state = !flipFlop.state) 129 | copy( 130 | machine = machine + flipFlop2, 131 | queue = tail ++ flipFlop2.pulses(flipFlop2.state) 132 | ) 133 | 134 | // If a pulse reaches a conjunction, update the source state in the 135 | // conjunction and enqueue pulses for all the module destinations 136 | // according to the conjunction state 137 | case Some(conjunction: Conjunction) => 138 | val conjunction2 = conjunction.copy( 139 | state = if level then conjunction.state + source 140 | else conjunction.state - source 141 | ) 142 | val active = machine.sources(conjunction2.name) == conjunction2.state 143 | copy( 144 | machine = machine + conjunction2, 145 | queue = tail ++ conjunction2.pulses(!active) 146 | ) 147 | 148 | // In all other cases just discard the pulse and proceed 149 | case _ => 150 | copy(queue = tail) 151 | end MachineFSM 152 | 153 | // An unruly and lawless find-map-get 154 | extension [A](self: Iterator[A]) 155 | def findMap[B](f: A => Option[B]): B = self.flatMap(f).next() 156 | 157 | // The problem 1 state machine comprises the number of low and high pulses 158 | // processed, and whether the problem is complete (after 1000 presses). This 159 | // state machine gets updated by each state of the primary state machine. 160 | 161 | final case class Problem1FSM( 162 | lows: Long, 163 | highs: Long, 164 | complete: Boolean, 165 | ): 166 | // If the head of the pulse queue is a low or high pulse then update the 167 | // low/high count. If the pulse queue is empty and the button has been pressed 168 | // 1000 times then complete. 169 | inline def +(state: MachineFSM): Problem1FSM = 170 | state.queue.headOption match 171 | case Some(Pulse(_, _, false)) => copy(lows = lows + 1) 172 | case Some(Pulse(_, _, true)) => copy(highs = highs + 1) 173 | case None if state.presses == 1000 => copy(complete = true) 174 | case None => this 175 | 176 | // The result is the product of lows and highs 177 | def solution: Option[Long] = Option.when(complete)(lows * highs) 178 | end Problem1FSM 179 | 180 | object Problem1FSM: 181 | final val Initial = Problem1FSM(0, 0, false) 182 | 183 | // Part 1 is solved by first constructing the primary state machine that 184 | // executes the pulse machinery. Each state of this machine is then fed to a 185 | // second problem 1 state machine. We then run the combined state machines to 186 | // completion. 187 | 188 | def part1(input: String): Long = 189 | val machine = Machine.parse(input) 190 | Iterator 191 | .iterate(MachineFSM(machine))(_.nextState) 192 | .scanLeft(Problem1FSM.Initial)(_ + _) 193 | .findMap(_.solution) 194 | end part1 195 | 196 | // The problem 2 state machine is looking for the least common multiple of the 197 | // cycle lengths of the subgraphs that feed into the output "rx" module. When it 198 | // observes a high pulse from the final module of one these subgraphs, it 199 | // records the number of button presses to reach this state. 200 | 201 | final case class Problem2FSM( 202 | cycles: Map[ModuleName, Long], 203 | ): 204 | inline def +(state: MachineFSM): Problem2FSM = 205 | state.queue.headOption match 206 | case Some(Pulse(src, _, true)) if cycles.get(src).contains(0L) => 207 | copy(cycles = cycles + (src -> state.presses)) 208 | case _ => this 209 | 210 | // We are complete if we have the cycle value for each subgraph 211 | def solution: Option[Long] = 212 | Option.when(cycles.values.forall(_ > 0))(lcm(cycles.values)) 213 | 214 | private def lcm(list: Iterable[Long]): Long = 215 | list.foldLeft(1L)((a, b) => b * a / gcd(a, b)) 216 | 217 | @tailrec private def gcd(x: Long, y: Long): Long = 218 | if y == 0 then x else gcd(y, x % y) 219 | end Problem2FSM 220 | 221 | object Problem2FSM: 222 | def apply(machine: Machine): Problem2FSM = 223 | new Problem2FSM(subgraphs(machine).map(_ -> 0L).toMap) 224 | 225 | // The problem is characterized by a terminal module ("rx") that is fed by 226 | // several subgraphs so we look to see which are the sources of the terminal 227 | // module; these are the subgraphs whose cycle lengths we need to count. 228 | private def subgraphs(machine: Machine): Set[ModuleName] = 229 | val terminal = (machine.sources.keySet -- machine.modules.keySet).head 230 | machine.sources(machine.sources(terminal).head) 231 | 232 | // Part 2 is solved by first constructing the primary state machine that 233 | // executes the pulse machinery. Each state of this machine is then fed to a 234 | // second problem 2 state machine. We then run the combined state machines to 235 | // completion. 236 | 237 | def part2(input: String): Long = 238 | val machine = Machine.parse(input) 239 | 240 | Iterator 241 | .iterate(MachineFSM(machine))(_.nextState) 242 | .scanLeft(Problem2FSM(machine))(_ + _) 243 | .findMap(_.solution) 244 | end part2 -------------------------------------------------------------------------------- /2023/src/day23.scala: -------------------------------------------------------------------------------- 1 | package day23 2 | // based on solution from https://github.com/stewSquared/adventofcode/blob/src/main/scala/2023/Day23.worksheet.sc 3 | 4 | import locations.Directory.currentDir 5 | import inputs.Input.loadFileSync 6 | 7 | @main def part1: Unit = 8 | println(s"The solution is ${part1(loadInput())}") 9 | 10 | @main def part2: Unit = 11 | println(s"The solution is ${part2(loadInput())}") 12 | 13 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day23") 14 | 15 | import collection.immutable.BitSet 16 | 17 | def part1(input: String): Int = 18 | given maze: Maze = Maze(parseInput(input)) 19 | longestDownhillHike 20 | 21 | def part2(input: String): Int = 22 | given maze: Maze = Maze(parseInput(input)) 23 | longestHike 24 | 25 | def parseInput(fileInput: String): Vector[Vector[Char]] = Vector.from: 26 | for line <- fileInput.split("\n") 27 | yield line.toVector 28 | 29 | enum Dir: 30 | case N, S, E, W 31 | 32 | def turnRight = this match 33 | case Dir.N => E 34 | case Dir.E => S 35 | case Dir.S => W 36 | case Dir.W => N 37 | 38 | def turnLeft = this match 39 | case Dir.N => W 40 | case Dir.W => S 41 | case Dir.S => E 42 | case Dir.E => N 43 | 44 | case class Point(x: Int, y: Int): 45 | def dist(p: Point) = math.abs(x - p.x) + math.abs(y - p.y) 46 | def adjacent = List(copy(x = x + 1), copy(x = x - 1), copy(y = y + 1), copy(y = y - 1)) 47 | 48 | def move(dir: Dir) = dir match 49 | case Dir.N => copy(y = y - 1) 50 | case Dir.S => copy(y = y + 1) 51 | case Dir.E => copy(x = x + 1) 52 | case Dir.W => copy(x = x - 1) 53 | 54 | case class Maze(grid: Vector[Vector[Char]]): 55 | 56 | def apply(p: Point): Char = grid(p.y)(p.x) 57 | 58 | val xRange: Range = grid.head.indices 59 | val yRange: Range = grid.indices 60 | 61 | def points: Iterator[Point] = for 62 | y <- yRange.iterator 63 | x <- xRange.iterator 64 | yield Point(x, y) 65 | 66 | val walkable: Set[Point] = points.filter(p => grid(p.y)(p.x) != '#').toSet 67 | val start: Point = walkable.minBy(_.y) 68 | val end: Point = walkable.maxBy(_.y) 69 | 70 | val junctions: Set[Point] = walkable.filter: p => 71 | Dir.values.map(p.move).count(walkable) > 2 72 | .toSet + start + end 73 | 74 | val slopes = Map.from[Point, Dir]: 75 | points.collect: 76 | case p if apply(p) == '^' => p -> Dir.N 77 | case p if apply(p) == 'v' => p -> Dir.S 78 | case p if apply(p) == '>' => p -> Dir.E 79 | case p if apply(p) == '<' => p -> Dir.W 80 | 81 | def connectedJunctions(pos: Point)(using maze: Maze) = List.from[(Point, Int)]: 82 | def walk(pos: Point, dir: Dir): Option[Point] = 83 | val p = pos.move(dir) 84 | Option.when(maze.walkable(p) && maze.slopes.get(p).forall(_ == dir))(p) 85 | 86 | def search(pos: Point, facing: Dir, dist: Int): Option[(Point, Int)] = 87 | if maze.junctions.contains(pos) then Some(pos, dist) else 88 | val adjacentSearch = for 89 | nextFacing <- LazyList(facing, facing.turnRight, facing.turnLeft) 90 | nextPos <- walk(pos, nextFacing) 91 | yield search(nextPos, nextFacing, dist + 1) 92 | 93 | if adjacentSearch.size == 1 then adjacentSearch.head else None 94 | 95 | for 96 | d <- Dir.values 97 | p <- walk(pos, d) 98 | junction <- search(p, d, 1) 99 | yield junction 100 | 101 | def longestDownhillHike(using maze: Maze): Int = 102 | def search(pos: Point, dist: Int)(using maze: Maze): Int = 103 | if pos == maze.end then dist else 104 | connectedJunctions(pos).foldLeft(0): 105 | case (max, (n, d)) => max.max(search(n, dist + d)) 106 | 107 | search(maze.start, 0) 108 | 109 | def longestHike(using maze: Maze): Int = 110 | type Index = Int 111 | 112 | val indexOf: Map[Point, Index] = 113 | maze.junctions.toList.sortBy(_.dist(maze.start)).zipWithIndex.toMap 114 | 115 | val adjacent: Map[Index, List[(Index, Int)]] = 116 | maze.junctions.toList.flatMap: p1 => 117 | connectedJunctions(p1).flatMap: (p2, d) => 118 | val forward = indexOf(p1) -> (indexOf(p2), d) 119 | val reverse = indexOf(p2) -> (indexOf(p1), d) 120 | List(forward, reverse) 121 | .groupMap(_._1)(_._2) 122 | 123 | def search(junction: Index, visited: BitSet, totalDist: Int): Int = 124 | if junction == indexOf(maze.end) then totalDist else 125 | adjacent(junction).foldLeft(0): 126 | case (longest, (nextJunct, dist)) => 127 | if visited(nextJunct) then longest else 128 | longest.max(search(nextJunct, visited + nextJunct, totalDist + dist)) 129 | 130 | search(indexOf(maze.start), BitSet.empty, 0) 131 | -------------------------------------------------------------------------------- /2023/src/day24.scala: -------------------------------------------------------------------------------- 1 | package day24 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | 6 | @main def part1: Unit = 7 | println(s"The solution is ${part1(loadInput())}") 8 | // println(s"The solution is ${part1(sample1)}") 9 | 10 | @main def part2: Unit = 11 | println(s"The solution is ${part2(loadInput())}") 12 | // println(s"The solution is ${part2(sample1)}") 13 | 14 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day24") 15 | 16 | val sample1 = """ 17 | 19, 13, 30 @ -2, 1, -2 18 | 18, 19, 22 @ -1, -1, -2 19 | 20, 25, 34 @ -2, -2, -4 20 | 12, 31, 28 @ -1, -2, -1 21 | 20, 19, 15 @ 1, -5, -3 22 | """.strip 23 | 24 | final case class Hail(x: Long, y: Long, z: Long, vx: Long, vy: Long, vz: Long): 25 | def xyProjection: Hail2D = Hail2D(x, y, vx, vy) 26 | def xzProjection: Hail2D = Hail2D(x, z, vx, vz) 27 | 28 | object Hail: 29 | def parseAll(input: String): Vector[Hail] = 30 | input.linesIterator.toVector.map: 31 | case s"$x, $y, $z @ $dx, $dy, $dz" => 32 | Hail(x.trim.toLong, y.trim.toLong, z.trim.toLong, 33 | dx.trim.toLong, dy.trim.toLong, dz.trim.toLong) 34 | 35 | final case class Hail2D(x: Long, y: Long, vx: Long, vy: Long): 36 | private val a: BigDecimal = BigDecimal(vy) 37 | private val b: BigDecimal = BigDecimal(-vx) 38 | private val c: BigDecimal = BigDecimal(vx * y - vy * x) 39 | 40 | def deltaV(dvx: Long, dvy: Long): Hail2D = copy(vx = vx - dvx, vy = vy - dvy) 41 | 42 | // If the paths of these hailstones intersect, return the intersection 43 | def intersect(hail: Hail2D): Option[(BigDecimal, BigDecimal)] = 44 | val denominator = a * hail.b - hail.a * b 45 | Option.when(denominator != 0): 46 | ((b * hail.c - hail.b * c) / denominator, 47 | (c * hail.a - hail.c * a) / denominator) 48 | 49 | // Return the time at which this hail will intersect the given point 50 | def timeTo(posX: BigDecimal, posY: BigDecimal): BigDecimal = 51 | if vx == 0 then (posY - y) / vy else (posX - x) / vx 52 | end Hail2D 53 | 54 | extension [A](self: Vector[A]) 55 | // all non-self element pairs 56 | def allPairs: Vector[(A, A)] = self.tails.toVector.tail.flatMap(self.zip) 57 | 58 | extension [A](self: Iterator[A]) 59 | // An unruly and lawless find-map-get 60 | def findMap[B](f: A => Option[B]): B = self.flatMap(f).next() 61 | 62 | def intersections( 63 | hails: Vector[Hail2D], 64 | min: Long, 65 | max: Long 66 | ): Vector[(Hail2D, Hail2D)] = 67 | for 68 | (hail0, hail1) <- hails.allPairs 69 | (x, y) <- hail0.intersect(hail1) 70 | if x >= min && x <= max && y >= min && y <= max && 71 | hail0.timeTo(x, y) >= 0 && hail1.timeTo(x, y) >= 0 72 | yield (hail0, hail1) 73 | end intersections 74 | 75 | def part1(input: String): Long = 76 | val hails = Hail.parseAll(input) 77 | val hailsXY = hails.map(_.xyProjection) 78 | intersections(hailsXY, 200000000000000L, 400000000000000L).size 79 | end part1 80 | 81 | def findRockOrigin( 82 | hails: Vector[Hail2D], 83 | vx: Long, 84 | vy: Long 85 | ): Option[(Long, Long)] = 86 | val hail0 +: hail1 +: hail2 +: _ = hails.map(_.deltaV(vx, vy)): @unchecked 87 | for 88 | (x0, y0) <- hail0.intersect(hail1) 89 | (x1, y1) <- hail0.intersect(hail2) 90 | if x0 == x1 && y0 == y1 91 | time = hail0.timeTo(x0, y0) 92 | yield (hail0.x + hail0.vx * time.longValue, 93 | hail0.y + hail0.vy * time.longValue) 94 | end findRockOrigin 95 | 96 | final case class Spiral( 97 | x: Long, 98 | y: Long, 99 | dx: Long, 100 | dy: Long, 101 | count: Long, 102 | limit: Long 103 | ): 104 | def next: Spiral = 105 | if count > 0 then 106 | copy(x = x + dx, y = y + dy, count = count - 1) 107 | else if dy == 0 then 108 | copy(x = x + dx, y = y + dy, dy = dx, dx = -dy, count = limit) 109 | else 110 | copy(x = x + dx, y = y + dy, dy = dx, dx = -dy, 111 | count = limit + 1, limit = limit + 1) 112 | end next 113 | end Spiral 114 | 115 | object Spiral: 116 | final val Start = Spiral(0, 0, 1, 0, 0, 0) 117 | 118 | def part2(input: String): Long = 119 | val hails = Hail.parseAll(input) 120 | 121 | val hailsXY = hails.map(_.xyProjection) 122 | val (x, y) = Iterator 123 | .iterate(Spiral.Start)(_.next) 124 | .findMap: spiral => 125 | findRockOrigin(hailsXY, spiral.x, spiral.y) 126 | 127 | val hailsXZ = hails.map(_.xzProjection) 128 | val (_, z) = Iterator 129 | .iterate(Spiral.Start)(_.next) 130 | .findMap: spiral => 131 | findRockOrigin(hailsXZ, spiral.x, spiral.y) 132 | 133 | x + y + z 134 | end part2 135 | -------------------------------------------------------------------------------- /2023/src/day25.scala: -------------------------------------------------------------------------------- 1 | package day25 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | 6 | @main def part1: Unit = 7 | val start = System.currentTimeMillis() 8 | val res = part1(loadInput()) 9 | val end = System.currentTimeMillis() 10 | println(s"The solution is ${res}\nTime: ${end - start} ms") 11 | 12 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day25") 13 | 14 | import scala.collection.immutable.BitSet 15 | import scala.collection.immutable.TreeSet 16 | 17 | def part1(input: String): Int = 18 | val alist = parse(input) 19 | val g = readGraph(alist) 20 | val (graph, cut) = minimumCut(g) 21 | val (out, in) = graph.partition(cut) 22 | in.size * out.size 23 | 24 | type Id = Int 25 | type Vertices = BitSet 26 | type Weight = Map[Id, Map[Id, Int]] 27 | 28 | def parse(input: String): Map[String, Set[String]] = 29 | input 30 | .linesIterator 31 | .map: 32 | case s"$key: $values" => key -> values.split(" ").toSet 33 | .toMap 34 | 35 | def readGraph(alist: Map[String, Set[String]]): Graph = 36 | val all = alist.flatMap((k, vs) => vs + k).toSet 37 | 38 | val (_, lookup) = 39 | // perfect hashing 40 | val initial = (0, Map.empty[String, Id]) 41 | all.foldLeft(initial): (acc, s) => 42 | val (id, seen) = acc 43 | (id + 1, seen + (s -> id)) 44 | 45 | def asEdges(k: String, v: String) = 46 | val t = (lookup(k), lookup(v)) 47 | t :: t.swap :: Nil 48 | 49 | val v = lookup.values.to(BitSet) 50 | val nodes = v.unsorted.map(id => id -> BitSet(id)).toMap 51 | val edges = 52 | for 53 | (k, vs) <- alist.toSet 54 | v <- vs 55 | e <- asEdges(k, v) 56 | yield 57 | e 58 | 59 | val w = edges 60 | .groupBy((v, _) => v) 61 | .view 62 | .mapValues: m => 63 | m 64 | .groupBy((_, v) => v) 65 | .view 66 | .mapValues(_ => 1) 67 | .toMap 68 | .toMap 69 | Graph(v, nodes, w) 70 | 71 | class MostConnected( 72 | totalWeights: Map[Id, Int], 73 | queue: TreeSet[MostConnected.Entry] 74 | ): 75 | 76 | def pop = 77 | val id = queue.head.id 78 | id -> MostConnected(totalWeights - id, queue.tail) 79 | 80 | def expand(z: Id, explore: Vertices, w: Weight) = 81 | val connectedEdges = 82 | w(z).view.filterKeys(explore) 83 | var totalWeights0 = totalWeights 84 | var queue0 = queue 85 | for (id, w) <- connectedEdges do 86 | val w1 = totalWeights0.getOrElse(id, 0) + w 87 | totalWeights0 += id -> w1 88 | queue0 += MostConnected.Entry(id, w1) 89 | MostConnected(totalWeights0, queue0) 90 | end expand 91 | 92 | end MostConnected 93 | 94 | object MostConnected: 95 | def empty = MostConnected(Map.empty, TreeSet.empty) 96 | given Ordering[Entry] = (e1, e2) => 97 | val first = e2.weight.compareTo(e1.weight) 98 | if first == 0 then e2.id.compareTo(e1.id) else first 99 | class Entry(val id: Id, val weight: Int): 100 | override def hashCode: Int = id 101 | override def equals(that: Any): Boolean = that match 102 | case that: Entry => id == that.id 103 | case _ => false 104 | 105 | case class Graph(v: Vertices, nodes: Map[Id, Vertices], w: Weight): 106 | def cutOfThePhase(t: Id) = Graph.Cut(t = t, edges = w(t)) 107 | 108 | def partition(cut: Graph.Cut): (Vertices, Vertices) = 109 | (nodes(cut.t), (v - cut.t).flatMap(nodes)) 110 | 111 | def shrink(s: Id, t: Id): Graph = 112 | def fetch(x: Id) = 113 | w(x).view.filterKeys(y => y != s && y != t) 114 | 115 | val prunedW = (w - t).view.mapValues(_ - t).toMap 116 | 117 | val fromS = fetch(s).toMap 118 | val fromT = fetch(t).map: (y, w0) => 119 | y -> (fromS.getOrElse(y, 0) + w0) 120 | val mergedWeights = fromS ++ fromT 121 | 122 | val reverseMerged = mergedWeights.view.map: (y, w0) => 123 | y -> (prunedW(y) + (s -> w0)) 124 | 125 | val v1 = v - t // 5. 126 | val w1 = prunedW + (s -> mergedWeights) ++ reverseMerged 127 | val nodes1 = nodes - t + (s -> (nodes(s) ++ nodes(t))) 128 | Graph(v1, nodes1, w1) 129 | end shrink 130 | 131 | object Graph: 132 | def emptyCut = Cut(t = -1, edges = Map.empty) 133 | 134 | case class Cut(t: Id, edges: Map[Id, Int]): 135 | lazy val weight: Int = edges.values.sum 136 | 137 | def minimumCutPhase(g: Graph) = 138 | val a = g.v.head 139 | var A = a :: Nil 140 | var explore = g.v - a 141 | var mostConnected = 142 | MostConnected.empty.expand(a, explore, g.w) 143 | while explore.nonEmpty do 144 | val (z, rest) = mostConnected.pop 145 | A ::= z 146 | explore -= z 147 | mostConnected = rest.expand(z, explore, g.w) 148 | val t :: s :: _ = A: @unchecked 149 | (g.shrink(s, t), g.cutOfThePhase(t)) 150 | 151 | /** See Stoer-Wagner min cut algorithm 152 | * https://dl.acm.org/doi/pdf/10.1145/263867.263872 153 | */ 154 | def minimumCut(g: Graph) = 155 | var g0 = g 156 | var min = (g, Graph.emptyCut) 157 | while g0.v.size > 1 do 158 | val (g1, cutOfThePhase) = minimumCutPhase(g0) 159 | if cutOfThePhase.weight < min(1).weight 160 | || min(1).weight == 0 // initial case 161 | then 162 | min = (g0, cutOfThePhase) 163 | g0 = g1 164 | min 165 | -------------------------------------------------------------------------------- /2023/src/inputs.scala: -------------------------------------------------------------------------------- 1 | package inputs 2 | 3 | import scala.util.Using 4 | import scala.io.Source 5 | 6 | object Input: 7 | 8 | def loadFileSync(path: String): String = 9 | Using.resource(Source.fromFile(path))(_.mkString) 10 | -------------------------------------------------------------------------------- /2023/src/locations.scala: -------------------------------------------------------------------------------- 1 | package locations 2 | 3 | import scala.quoted.* 4 | 5 | object Directory: 6 | 7 | /** The absolute path of the parent directory of the file that calls this method 8 | * This is stable no matter which directory runs the program. 9 | */ 10 | inline def currentDir: String = ${ parentDirImpl } 11 | 12 | private def parentDirImpl(using Quotes): Expr[String] = 13 | // position of the call to `currentDir` in the source code 14 | val position = quotes.reflect.Position.ofMacroExpansion 15 | // get the path of the file calling this macro 16 | val srcFilePath = position.sourceFile.getJPath.get 17 | // get the parent of the path, which is the directory containing the file 18 | val parentDir = srcFilePath.getParent().toAbsolutePath 19 | Expr(parentDir.toString) // convert the String to Expr[String] 20 | -------------------------------------------------------------------------------- /2024/project.scala: -------------------------------------------------------------------------------- 1 | //> using scala 3.5.2 2 | //> using option -Wunused:all 3 | //> using test.dep org.scalameta::munit::1.0.2 4 | -------------------------------------------------------------------------------- /2024/src/day13.scala: -------------------------------------------------------------------------------- 1 | package day13 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | 6 | @main def part1: Unit = 7 | println(s"The solution is ${part1(loadInput())}") 8 | 9 | @main def part2: Unit = 10 | println(s"The solution is ${part2(loadInput())}") 11 | 12 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day13") 13 | 14 | case class Claw(ax: Long, ay: Long, bx: Long, by: Long, x: Long, y: Long): 15 | def solve: Option[Long] = for 16 | b <- (x * ay - y * ax) safeDiv (bx * ay - by * ax) 17 | a <- (x - b * bx) safeDiv ax 18 | yield a * 3 + b 19 | 20 | object Claw: 21 | def parse(xs: Seq[String]): Option[Claw] = xs match 22 | case Seq( 23 | s"Button A: X+${L(ax)}, Y+${L(ay)}", 24 | s"Button B: X+${L(bx)}, Y+${L(by)}", 25 | s"Prize: X=${L(x)}, Y=${L(y)}", 26 | ) => 27 | Some(Claw(ax, ay, bx, by, x, y)) 28 | case _ => None 29 | 30 | def parse(input: String): Seq[Claw] = 31 | input.split("\n+").toSeq.grouped(3).flatMap(Claw.parse).toSeq 32 | 33 | extension (a: Long) 34 | infix def safeDiv(b: Long): Option[Long] = 35 | Option.when(b != 0 && a % b == 0)(a / b) 36 | 37 | object L: 38 | def unapply(s: String): Option[Long] = s.toLongOption 39 | 40 | def part1(input: String): Long = 41 | parse(input).flatMap(_.solve).sum 42 | 43 | def part2(input: String): Long = 44 | val diff = 10_000_000_000_000L 45 | parse(input) 46 | .map(c => c.copy(x = c.x + diff, y = c.y + diff)) 47 | .flatMap(_.solve) 48 | .sum 49 | -------------------------------------------------------------------------------- /2024/src/day14.scala: -------------------------------------------------------------------------------- 1 | package day14 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | 6 | @main def part1: Unit = 7 | println("The solution is ${part1(loadInput())}") 8 | 9 | @main def part2: Unit = 10 | println("The solution is ${part2(loadInput())}") 11 | 12 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day14") 13 | 14 | case class Vec2i(x: Int, y: Int) 15 | 16 | val size = Vec2i(101, 103) 17 | 18 | extension (self: Int) 19 | infix def rem(that: Int): Int = 20 | val m = math.abs(self) % that 21 | if self < 0 then 22 | that - m 23 | else 24 | m 25 | 26 | case class Robot(pos: Vec2i, velocity: Vec2i): 27 | def stepN(n: Int = 1): Robot = 28 | copy(pos = pos.copy(x = (pos.x + n * velocity.x) rem size.x, y = (pos.y + n * velocity.y) rem size.y)) 29 | 30 | def parse(str: String): List[Robot] = 31 | str.linesIterator.map { 32 | case s"p=$px,$py v=$vx,$vy" => 33 | Robot(Vec2i(px.toInt, py.toInt), Vec2i(vx.toInt, vy.toInt)) 34 | }.toList 35 | 36 | extension (robots: List[Robot]) { 37 | def stepN(n: Int = 1): List[Robot] = robots.map(_.stepN(n)) 38 | 39 | def safety: Int = 40 | val middleX = size.x / 2 41 | val middleY = size.y / 2 42 | 43 | robots.groupBy { robot => 44 | (robot.pos.x.compareTo(middleX), robot.pos.y.compareTo(middleY)) match 45 | case (0, _) | (_, 0) => -1 46 | case ( 1, -1) => 0 47 | case (-1, -1) => 1 48 | case (-1, 1) => 2 49 | case ( 1, 1) => 3 50 | }.removed(-1).values.map(_.length).product 51 | 52 | def findEasterEgg: Int = 53 | (0 to 10403).find { i => 54 | val newRobots = robots.stepN(i) 55 | newRobots.groupBy(_.pos.y).count(_._2.length >= 10) > 15 && newRobots.groupBy(_.pos.x).count(_._2.length >= 15) >= 3 56 | }.getOrElse(-1) 57 | } 58 | 59 | def part1(input: String): Int = parse(input).stepN(100).safety 60 | 61 | def part2(input: String): Int = parse(input).findEasterEgg 62 | -------------------------------------------------------------------------------- /2024/src/day19.scala: -------------------------------------------------------------------------------- 1 | package day19 2 | 3 | import scala.collection.mutable 4 | 5 | import locations.Directory.currentDir 6 | import inputs.Input.loadFileSync 7 | 8 | @main def part1: Unit = 9 | println(s"The solution is ${part1(loadInput())}") 10 | 11 | @main def part2: Unit = 12 | println(s"The solution is ${part2(loadInput())}") 13 | 14 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day19") 15 | 16 | type Towel = String 17 | type Pattern = String 18 | 19 | def parse(input: String): (List[Towel], List[Pattern]) = 20 | val Array(towelsString, patternsString) = input.split("\n\n") 21 | val towels = towelsString.split(", ").toList 22 | val patterns = patternsString.split("\n").toList 23 | (towels, patterns) 24 | 25 | def part1(input: String): Int = 26 | val (towels, patterns) = parse(input) 27 | patterns.count(isPossible(towels)) 28 | 29 | def isPossible(towels: List[Towel])(pattern: Pattern): Boolean = 30 | val regex = towels.mkString("^(", "|", ")*$").r 31 | regex.matches(pattern) 32 | 33 | def part2(input: String): Long = 34 | val (towels, patterns) = parse(input) 35 | countOptions(towels, patterns) 36 | 37 | def countOptions(towels: List[Towel], patterns: List[Pattern]): Long = 38 | val cache = mutable.Map.empty[Pattern, Long] 39 | 40 | def loop(pattern: Pattern): Long = 41 | cache.getOrElseUpdate( 42 | pattern, 43 | towels 44 | .collect { 45 | case towel if pattern.startsWith(towel) => 46 | pattern.drop(towel.length) 47 | } 48 | .map { remainingPattern => 49 | if (remainingPattern.isEmpty) 1 50 | else loop(remainingPattern) 51 | } 52 | .sum 53 | ) 54 | 55 | patterns.map(loop).sum 56 | -------------------------------------------------------------------------------- /2024/src/day20.scala: -------------------------------------------------------------------------------- 1 | package day20 2 | 3 | import scala.annotation.tailrec 4 | import scala.collection.immutable.Range.Inclusive 5 | 6 | import locations.Directory.currentDir 7 | import inputs.Input.loadFileSync 8 | 9 | @main def part1: Unit = 10 | println(s"The solution is ${part1(loadInput())}") 11 | 12 | @main def part2: Unit = 13 | println(s"The solution is ${part2(loadInput())}") 14 | 15 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day20") 16 | 17 | extension (x: Int) inline def ±(y: Int) = x - y to x + y 18 | extension (x: Inclusive) 19 | inline def &(y: Inclusive) = (x.start max y.start) to (x.end min y.end) 20 | 21 | opaque type Pos = Int 22 | 23 | object Pos: 24 | val up = Pos(0, -1) 25 | val down = Pos(0, 1) 26 | val left = Pos(-1, 0) 27 | val right = Pos(1, 0) 28 | val zero = Pos(0, 0) 29 | inline def apply(x: Int, y: Int): Pos = y << 16 | x 30 | 31 | extension (p: Pos) 32 | inline def x = p & 0xffff 33 | inline def y = p >> 16 34 | inline def neighbors: List[Pos] = 35 | List(p + up, p + right, p + down, p + left) 36 | inline def +(q: Pos): Pos = Pos(p.x + q.x, p.y + q.y) 37 | inline infix def taxiDist(q: Pos) = (p.x - q.x).abs + (p.y - q.y).abs 38 | 39 | case class Rect(x: Inclusive, y: Inclusive): 40 | inline def &(that: Rect) = Rect(x & that.x, y & that.y) 41 | 42 | def iterator: Iterator[Pos] = for 43 | y <- y.iterator 44 | x <- x.iterator 45 | yield Pos(x, y) 46 | 47 | object Track: 48 | def parse(input: String) = 49 | val lines = input.trim.split('\n') 50 | val bounds = Rect(0 to lines.head.size - 1, 0 to lines.size - 1) 51 | val track = Track(Pos.zero, Pos.zero, Set.empty, bounds) 52 | bounds.iterator.foldLeft(track) { (track, p) => 53 | lines(p.y)(p.x) match 54 | case 'S' => track.copy(start = p) 55 | case 'E' => track.copy(end = p) 56 | case '#' => track.copy(walls = track.walls + p) 57 | case _ => track 58 | } 59 | 60 | case class Track(start: Pos, end: Pos, walls: Set[Pos], bounds: Rect): 61 | lazy val path: Vector[Pos] = 62 | inline def canMove(prev: List[Pos])(p: Pos) = 63 | !walls.contains(p) && Some(p) != prev.headOption 64 | 65 | @tailrec def go(xs: List[Pos]): List[Pos] = xs match 66 | case Nil => Nil 67 | case p :: _ if p == end => xs 68 | case p :: ys => go(p.neighbors.filter(canMove(ys)) ++ xs) 69 | 70 | go(List(start)).reverseIterator.toVector 71 | 72 | lazy val zipped = path.zipWithIndex 73 | lazy val pathMap = zipped.toMap 74 | 75 | def cheatedPaths(maxDist: Int) = 76 | def radius(p: Pos) = 77 | (Rect(p.x ± maxDist, p.y ± maxDist) & bounds).iterator 78 | .filter(p.taxiDist(_) <= maxDist) 79 | 80 | zipped.map { (p, i) => 81 | radius(p) 82 | .flatMap(pathMap.get) 83 | .map { j => (j - i) - (p taxiDist path(j)) } 84 | .count(_ >= 100) 85 | }.sum 86 | 87 | def part1(input: String): Int = Track.parse(input).cheatedPaths(2) 88 | def part2(input: String): Int = Track.parse(input).cheatedPaths(20) 89 | -------------------------------------------------------------------------------- /2024/src/day21.scala: -------------------------------------------------------------------------------- 1 | package day21 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | 6 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day21") 7 | 8 | case class Pos(x: Int, y: Int): 9 | def +(other: Pos) = Pos(x + other.x, y + other.y) 10 | def -(other: Pos) = Pos(x - other.x, y - other.y) 11 | def projX = Pos(x, 0) 12 | def projY = Pos(0, y) 13 | 14 | val numericKeypad = Map( 15 | '7' -> Pos(0, 0), '8' -> Pos(1, 0), '9' -> Pos(2, 0), 16 | '4' -> Pos(0, 1), '5' -> Pos(1, 1), '6' -> Pos(2, 1), 17 | '1' -> Pos(0, 2), '2' -> Pos(1, 2), '3' -> Pos(2, 2), 18 | '0' -> Pos(1, 3), 'A' -> Pos(2, 3), 19 | ) 20 | val numericKeypadPositions = numericKeypad.values.toSet 21 | 22 | val directionalKeypad = Map( 23 | '^' -> Pos(1, 0), 'A' -> Pos(2, 0), 24 | '<' -> Pos(0, 1), 'v' -> Pos(1, 1), '>' -> Pos(2, 1), 25 | ) 26 | val directionalKeypadPositions = directionalKeypad.values.toSet 27 | 28 | 29 | /**********/ 30 | /* Part 1 */ 31 | /**********/ 32 | 33 | def minPathStep(from: Pos, to: Pos, positions: Set[Pos]): String = 34 | val shift = to - from 35 | val h = (if shift.x > 0 then ">" else "<") * shift.x.abs 36 | val v = (if shift.y > 0 then "v" else "^") * shift.y.abs 37 | val reverse = !positions(from + shift.projX) || (positions(from + shift.projY) && shift.x > 0) 38 | if reverse then v + h + 'A' else h + v + 'A' 39 | 40 | def minPath(input: String, isNumeric: Boolean = false): String = 41 | val keypad = if isNumeric then numericKeypad else directionalKeypad 42 | val positions = if isNumeric then numericKeypadPositions else directionalKeypadPositions 43 | (s"A$input").map(keypad).sliding(2).map(p => minPathStep(p(0), p(1), positions)).mkString 44 | 45 | def part1(input: String): Long = 46 | input 47 | .linesIterator 48 | .filter(_.nonEmpty) 49 | .map: line => // 029A 50 | val path1 = minPath(line, isNumeric = true) // AvvvA 51 | val path2 = minPath(path1) // v<>^AAA^AA 52 | val path3 = minPath(path2) // >^AvAA<^A>Av<>^AvA^Av<>^AAA^AAv<A^>AAAA^A 53 | val num = line.init.toLong // 29 54 | val len = path3.length() // 68 55 | len * num // 211930 56 | .sum 57 | 58 | @main def part1: Unit = 59 | println(s"The solution is ${part1(loadInput())}") 60 | 61 | 62 | /**********/ 63 | /* Part 2 */ 64 | /**********/ 65 | 66 | val cache = collection.mutable.Map.empty[(Pos, Pos, Int, Int), Long] 67 | def minPathStepCost(from: Pos, to: Pos, level: Int, maxLevel: Int): Long = 68 | cache.getOrElseUpdate((from, to, level, maxLevel), { 69 | val positions = if level == 0 then numericKeypadPositions else directionalKeypadPositions 70 | val shift = to - from 71 | val h = (if shift.x > 0 then ">" else "<") * shift.x.abs 72 | val v = (if shift.y > 0 then "v" else "^") * shift.y.abs 73 | val reverse = !positions(from + shift.projX) || (positions(from + shift.projY) && shift.x > 0) 74 | val res = if reverse then v + h + 'A' else h + v + 'A' 75 | if level == maxLevel then res.length() else minPathCost(res, level + 1, maxLevel) 76 | }) 77 | 78 | def minPathCost(input: String, level: Int, maxLevel: Int): Long = 79 | val keypad = if level == 0 then numericKeypad else directionalKeypad 80 | (s"A$input").map(keypad).sliding(2).map(p => minPathStepCost(p(0), p(1), level, maxLevel)).sum 81 | 82 | def part2(input: String): Long = 83 | input 84 | .linesIterator 85 | .filter(_.nonEmpty) 86 | .map(line => minPathCost(line, 0, 25) * line.init.toLong) 87 | .sum 88 | 89 | @main def part2: Unit = 90 | println(s"The solution is ${part2(loadInput())}") 91 | -------------------------------------------------------------------------------- /2024/src/day22.scala: -------------------------------------------------------------------------------- 1 | package day22 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | 6 | @main def part1: Unit = 7 | println(s"The solution is ${part1(loadInput())}") 8 | 9 | @main def part2: Unit = 10 | println(s"The solution is ${part2(loadInput())}") 11 | 12 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day22") 13 | 14 | def part1(input: String): Long = 15 | input.linesIterator.foldMap: line => 16 | line.toLong.secretsIterator.nth(2000) 17 | 18 | def part2(input: String): Long = 19 | val deltaTotals = input.linesIterator.foldMap: line => 20 | monkeyMap(line) 21 | deltaTotals.values.max 22 | 23 | def monkeyMap(line: String): Map[(Long, Long, Long, Long), Long] = 24 | given Semigroup[Long] = leftBiasedSemigroup 25 | line.toLong.secretsIterator.map(_ % 10).take(2000).sliding(5).foldMap: quintuple => 26 | Map(deltaQuartuple(quintuple) -> quintuple(4)) 27 | 28 | def deltaQuartuple(q: Seq[Long]): (Long, Long, Long, Long) = 29 | (q(1) - q(0), q(2) - q(1), q(3) - q(2), q(4) - q(3)) 30 | 31 | extension (self: Long) 32 | private inline def step(f: Long => Long): Long = mix(f(self)).prune 33 | private inline def mix(n: Long): Long = self ^ n 34 | private inline def prune: Long = self % 16777216 35 | private inline def nextSecret: Long = step(_ * 64).step(_ / 32).step(_ * 2048) 36 | 37 | def secretsIterator: Iterator[Long] = 38 | Iterator.iterate(self)(_.nextSecret) 39 | 40 | trait Semigroup[A]: 41 | def combine(a0: A, a1: A): A 42 | 43 | trait Monoid[A] extends Semigroup[A]: 44 | def zero: A 45 | 46 | given NumericMonoid[A](using N: Numeric[A]): Monoid[A] with 47 | def zero: A = N.zero 48 | def combine(a0: A, a1: A): A = N.plus(a0, a1) 49 | 50 | given MapMonoid[A, B](using S: Semigroup[B]): Monoid[Map[A, B]] with 51 | def zero: Map[A, B] = Map.empty 52 | 53 | def combine(ab0: Map[A, B], ab1: Map[A, B]): Map[A, B] = 54 | ab1.foldLeft(ab0): 55 | case (ab, (a1, b1)) => 56 | ab.updatedWith(a1): 57 | case Some(b0) => Some(S.combine(b0, b1)) 58 | case None => Some(b1) 59 | 60 | def leftBiasedSemigroup[A]: Semigroup[A] = (a0: A, _: A) => a0 61 | 62 | extension [A](self: Iterator[A]) 63 | def nth(n: Int): A = 64 | self.drop(n).next() 65 | 66 | def foldMap[B](f: A => B)(using M: Monoid[B]): B = 67 | self.map(f).foldLeft(M.zero)(M.combine) 68 | -------------------------------------------------------------------------------- /2024/src/day25.scala: -------------------------------------------------------------------------------- 1 | package day25 2 | 3 | import locations.Directory.currentDir 4 | import inputs.Input.loadFileSync 5 | 6 | @main def part1: Unit = 7 | println(s"The solution is ${part1(loadInput())}") 8 | 9 | def loadInput(): String = loadFileSync(s"$currentDir/../input/day25") 10 | 11 | def part1(input: String): Int = 12 | val (locks, keys) = input.split("\n\n").partition(_.startsWith("#")) 13 | 14 | val matches = for 15 | lock <- locks 16 | key <- keys 17 | if lock.zip(key).forall: (lockChar, keyChar) => 18 | lockChar != '#' || keyChar != '#' 19 | yield lock -> key 20 | 21 | matches.length 22 | -------------------------------------------------------------------------------- /2024/src/inputs.scala: -------------------------------------------------------------------------------- 1 | package inputs 2 | 3 | import scala.util.Using 4 | import scala.io.Source 5 | 6 | object Input: 7 | 8 | def loadFileSync(path: String): String = 9 | Using.resource(Source.fromFile(path))(_.mkString) 10 | -------------------------------------------------------------------------------- /2024/src/locations.scala: -------------------------------------------------------------------------------- 1 | package locations 2 | 3 | import scala.quoted.* 4 | 5 | object Directory: 6 | 7 | /** The absolute path of the parent directory of the file that calls this method 8 | * This is stable no matter which directory runs the program. 9 | */ 10 | inline def currentDir: String = ${ parentDirImpl } 11 | 12 | private def parentDirImpl(using Quotes): Expr[String] = 13 | // position of the call to `currentDir` in the source code 14 | val position = quotes.reflect.Position.ofMacroExpansion 15 | // get the path of the file calling this macro 16 | val srcFilePath = position.sourceFile.getJPath.get 17 | // get the parent of the path, which is the directory containing the file 18 | val parentDir = srcFilePath.getParent().toAbsolutePath 19 | Expr(parentDir.toString) // convert the String to Expr[String] 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scala Advent of Code 2024 2 | 3 | Solutions in Scala for the annual [Advent of Code (adventofcode.com)](https://adventofcode.com/) challenge. 4 | 5 | _Note: this repo is not affiliated with Advent of Code._ 6 | 7 | See earlier editions: 8 | 9 | - [2021](/2021/README.md) 10 | - [2022](/2022/README.md) 11 | - [2023](/2023/README.md) 12 | 13 | ## Website 14 | 15 | The [Scala Advent of Code](https://scalacenter.github.io/scala-advent-of-code/) website contains: 16 | 17 | - some explanation of our solutions to [Advent of Code](https://adventofcode.com/) 18 | - more solutions from the community 19 | 20 | ## Setup 21 | 22 | We use Visual Studio Code with Metals to write Scala code, and scala-cli to compile and run it. 23 | 24 | You can follow these [steps](https://scalacenter.github.io/scala-advent-of-code/setup) to set up your environement. 25 | 26 | ### How to open in Visual Studio Code 27 | 28 | After you clone the repository, open a terminal and run: 29 | ``` 30 | $ cd scala-advent-of-code 31 | $ scala-cli setup-ide 2024 32 | $ mkdir 2024/input 33 | $ code 2024 34 | ``` 35 | 36 | `code 2024` will open Visual Studio Code and start Metals. If not you may have to go to the Metals pane and click 37 | the button labelled "Start Metals". 38 | 39 | When you navigate to a file, e.g. `2024/src/day01.scala` metals should index the project, and then display code lenses 40 | above each of the main methods `part1` and `part2`, as shown in this image: 41 | ![](img/code-lenses.png) 42 | 43 | To run a solution, first copy your input to the folder `2024/input`. 44 | Then click `run` in VS Code which will run the code and display the results of the program. Or `debug`, 45 | which will let you pause on breakpoints, and execute expressions in the debug console. 46 | 47 | ### How to run a solution with command line 48 | 49 | To run a solution, first copy your input to the folder `2024/input`. 50 | 51 | In a terminal you can run: 52 | ``` 53 | $ scala-cli 2024 -M day01.part1 54 | Compiling project (Scala 3.x.y, JVM) 55 | Compiled project (Scala 3.x.y, JVM) 56 | The solution is 64929 57 | ``` 58 | 59 | Or, to run another solution: 60 | ``` 61 | $ scala-cli 2024 -M . 62 | ``` 63 | 64 | ## Contributing 65 | 66 | - Please do not commit your puzzle inputs, we can not accept them as they are protected by copyright 67 | -------------------------------------------------------------------------------- /img/code-lenses.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalacenter/scala-advent-of-code/39a887016dc05ae225817cf90fa0fa2b18d15f6b/img/code-lenses.png --------------------------------------------------------------------------------