-
Notifications
You must be signed in to change notification settings - Fork 0
/
Advent08.scala
121 lines (99 loc) · 3.27 KB
/
Advent08.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
package jurisk.adventofcode.y2023
import cats.implicits._
import jurisk.math.lcmMany
import jurisk.utils.CollectionOps.IndexedSeqOps
import jurisk.utils.CollectionOps.SeqOps
import jurisk.utils.FileInput._
import jurisk.utils.Parsing.StringOps
import jurisk.utils.Simulation
object Advent08 {
sealed trait Instruction
object Instruction {
case object Left extends Instruction
case object Right extends Instruction
def parse(ch: Char): Instruction =
ch match {
case 'L' => Instruction.Left
case 'R' => Instruction.Right
case _ => ch.toString.failedToParse
}
}
private type NodeId = Int
private type NodeName = String
final case class Mapping(
name: NodeName,
left: NodeId,
right: NodeId,
) {
def move(instruction: Instruction): NodeId =
instruction match {
case Instruction.Left => left
case Instruction.Right => right
}
}
final case class Input(
private val instructions: IndexedSeq[Instruction],
private val mappings: IndexedSeq[Mapping],
) {
def instructionAtStep(step: Long): Instruction =
instructions.atIndexWithWraparound(step)
def findNodesByNameFilter(
predicate: NodeName => Boolean
): IndexedSeq[NodeId] =
mappings.indices.filter(index => predicate(mapping(index).name))
def findNodeByName(name: NodeName): NodeId =
mappings.firstIndexWhereUnsafe(_.name === name)
def mapping(nodeId: NodeId): Mapping = mappings(nodeId)
}
def parse(input: List[List[String]]): Input = {
val List(List(instructionLine), mappingLines) = input
val instructions = instructionLine.map(Instruction.parse)
val mappingTuples: IndexedSeq[(NodeName, NodeName, NodeName)] =
mappingLines.toIndexedSeq
.map {
case s"$from = ($left, $right)" => (from, left, right)
case line => line.failedToParse
}
val mappings = mappingTuples map { case (from, left, right) =>
def idx(name: NodeName): NodeId = mappingTuples.indexWhere {
case (from, _, _) => from == name
}
Mapping(
name = from,
left = idx(left),
right = idx(right),
)
}
Input(
instructions = instructions,
mappings = mappings,
)
}
private def loopAt(
game: Input,
start: NodeId,
isTerminal: NodeId => Boolean,
): Long =
Simulation.runWithIterationCount(start) { case (node, counter) =>
if (isTerminal(node)) counter.asLeft
else game.mapping(node).move(game.instructionAtStep(counter)).asRight
}
def part1(game: Input): Long = {
val start = game.findNodeByName("AAA")
val finish = game.findNodeByName("ZZZ")
loopAt(game, start, _ == finish)
}
def part2(game: Input): Long = {
val starts = game.findNodesByNameFilter(_.last === 'A')
val finishes = game.findNodesByNameFilter(_.last === 'Z')
val individualResults = starts.map(loopAt(game, _, finishes.contains))
lcmMany(individualResults)
}
def parseFile(fileName: String): Input =
parse(readLineGroups(fileName))
def main(args: Array[String]): Unit = {
val realData: Input = parseFile("2023/08.txt")
println(s"Part 1: ${part1(realData)}")
println(s"Part 2: ${part2(realData)}")
}
}