|
1 | 1 | package AdventOfCode2022
|
2 | 2 |
|
3 | 3 | object Day22:
|
4 |
| - val (right, down, left, up) = (Point(1, 0), Point(0, 1), Point(-1, 0), Point(0, -1)) |
5 |
| - |
6 |
| - type State = (Point, Point) |
7 |
| - |
8 |
| - enum Tile: |
9 |
| - case Open, Solid, Wrap |
10 |
| - |
11 | 4 | case class Point(x: Int, y: Int):
|
12 | 5 | def +(other: Point): Point = Point(x + other.x, y + other.y)
|
13 | 6 | def clockwise: Point = Point(-y, x)
|
14 | 7 | def counterClockwise: Point = Point(y, -x)
|
| 8 | + def score: Int = 1000 * (y + 1) + 4 * (x + 1) |
| 9 | + |
| 10 | + case class Vec(x: Int, y: Int, z: Int): |
| 11 | + def *(k: Int): Vec = Vec(x * k, y * k, z * k) |
| 12 | + def +(b: Vec): Vec = Vec(x + b.x, y + b.y, z + b.z) |
| 13 | + def cross(b: Vec): Vec = Vec(y * b.z - z * b.y, z * b.x - x * b.z, x * b.y - y * b.x) |
| 14 | + |
| 15 | + case class Info(point: Point, i: Vec, j: Vec, k: Vec) |
15 | 16 |
|
16 |
| - def parse(input: Seq[String]): (Map[Point, Tile], String) = |
| 17 | + def parseTiles(input: Seq[String]): Map[Point, Boolean] = |
17 | 18 | val points = for
|
18 | 19 | (row, y) <- input.dropRight(2).zipWithIndex
|
19 | 20 | (cell, x) <- row.zipWithIndex
|
20 | 21 | if cell != ' '
|
21 |
| - yield Point(x, y) -> (if cell == '.' then Tile.Open else Tile.Solid) |
22 |
| - (points.toMap.withDefaultValue(Tile.Wrap), input.last) |
23 |
| - |
24 |
| - def follow(tiles: Map[Point, Tile], path: String, handleWrap: State => State): Int = |
25 |
| - val numbers = path.split("\\D+").map(_.toInt).toSeq |
26 |
| - val letters = path.split("\\d+").toSeq |
27 |
| - val moves = numbers.zip(letters) |
28 |
| - |
29 |
| - val initial = (Point(50, 0), Point(1, 0)) |
30 |
| - val (position, direction) = moves.foldLeft(initial) { case ((position, direction), (number, letter)) => |
31 |
| - val nextDirection = letter match |
32 |
| - case "L" => direction.counterClockwise |
33 |
| - case "R" => direction.clockwise |
34 |
| - case _ => direction |
35 |
| - |
36 |
| - (1 to number).foldLeft((position, nextDirection)) { case ((position, direction), _) => |
37 |
| - val next = position + direction |
38 |
| - tiles(next) match |
39 |
| - case Tile.Open => (next, direction) |
40 |
| - case Tile.Solid => (position, direction) |
41 |
| - case Tile.Wrap => |
42 |
| - val (wrapPosition, wrapDirection) = handleWrap(position, direction) |
43 |
| - if tiles(wrapPosition) == Tile.Open then (wrapPosition, wrapDirection) else (position, direction) |
44 |
| - } |
45 |
| - } |
| 22 | + yield Point(x, y) -> (cell == '.') |
| 23 | + points.toMap |
46 | 24 |
|
47 |
| - 1000 * (position.y + 1) + 4 * (position.x + 1) + Seq(right, down, left, up).indexOf(direction) |
48 |
| - end follow |
| 25 | + def parseMoves(input: Seq[String]): Seq[Char] = |
| 26 | + val tokens = input.last.replace("L", " L ").replace("R", " R ").split(" ").toSeq |
| 27 | + tokens.flatMap(token => if token.head.isDigit then "F" * token.toInt else token) |
49 | 28 |
|
50 | 29 | def part1(input: Seq[String]): Int =
|
51 |
| - val (tiles, path) = parse(input) |
| 30 | + val tiles = parseTiles(input) |
| 31 | + val moves = parseMoves(input) |
52 | 32 |
|
53 | 33 | val minX = tiles.keys.groupMapReduce(_.y)(_.x)(_ min _)
|
54 | 34 | val maxX = tiles.keys.groupMapReduce(_.y)(_.x)(_ max _)
|
55 | 35 | val minY = tiles.keys.groupMapReduce(_.x)(_.y)(_ min _)
|
56 | 36 | val maxY = tiles.keys.groupMapReduce(_.x)(_.y)(_ max _)
|
57 | 37 |
|
58 |
| - def handleWrap(position: Point, direction: Point): State = direction match |
59 |
| - case `right` => position.copy(x = minX(position.y)) -> right |
60 |
| - case `left` => position.copy(x = maxX(position.y)) -> left |
61 |
| - case `down` => position.copy(y = minY(position.x)) -> down |
62 |
| - case `up` => position.copy(y = maxY(position.x)) -> up |
| 38 | + val facing = Seq(Point(1, 0), Point(0, 1), Point(-1, 0), Point(0, -1)) |
| 39 | + val Seq(right, down, left, up) = facing |
| 40 | + val topLeft = tiles.keys.filter(_.y == 0).minBy(_.x) |
| 41 | + |
| 42 | + val (position, direction) = moves.foldLeft((topLeft, right)) { case ((position, direction), move) => |
| 43 | + move match |
| 44 | + case 'L' => (position, direction.counterClockwise) |
| 45 | + case 'R' => (position, direction.clockwise) |
| 46 | + case _ => |
| 47 | + val next = position + direction |
| 48 | + tiles.get(next) match |
| 49 | + case Some(true) => (next, direction) |
| 50 | + case Some(false) => (position, direction) |
| 51 | + case None => |
| 52 | + val wrapPosition = direction match |
| 53 | + case `right` => position.copy(x = minX(position.y)) |
| 54 | + case `left` => position.copy(x = maxX(position.y)) |
| 55 | + case `down` => position.copy(y = minY(position.x)) |
| 56 | + case `up` => position.copy(y = maxY(position.x)) |
| 57 | + if tiles(wrapPosition) then (wrapPosition, direction) else (position, direction) |
| 58 | + } |
63 | 59 |
|
64 |
| - follow(tiles, path, handleWrap) |
| 60 | + position.score + facing.indexOf(direction) |
65 | 61 | end part1
|
66 | 62 |
|
67 |
| - def part2(input: Seq[String]): Int = |
68 |
| - // Cube faces: |
69 |
| - // BA |
70 |
| - // C |
71 |
| - // ED |
72 |
| - // F |
73 |
| - def handleWrap(position: Point, direction: Point): State = |
74 |
| - val (cubeX, cubeY) = (position.x / 50, position.y / 50) |
75 |
| - val (modX, modY) = (position.x % 50, position.y % 50) |
76 |
| - (cubeX, cubeY, direction) match |
77 |
| - case (2, 0, `up`) => Point(modX, 199) -> up // A to F |
78 |
| - case (2, 0, `down`) => Point(99, 50 + modX) -> left // A to C |
79 |
| - case (2, 0, `right`) => Point(99, 149 - modY) -> left // A to D |
80 |
| - case (1, 0, `up`) => Point(0, 150 + modX) -> right // B to F |
81 |
| - case (1, 0, `left`) => Point(0, 149 - modY) -> right // B to E |
82 |
| - case (1, 1, `left`) => Point(modY, 100) -> down // C to E |
83 |
| - case (1, 1, `right`) => Point(100 + modY, 49) -> up // C to A |
84 |
| - case (1, 2, `down`) => Point(49, 150 + modX) -> left // D to F |
85 |
| - case (1, 2, `right`) => Point(149, 49 - modY) -> left // D to A |
86 |
| - case (0, 2, `up`) => Point(50, 50 + modX) -> right // E to C |
87 |
| - case (0, 2, `left`) => Point(50, 49 - modY) -> right // E to B |
88 |
| - case (0, 3, `down`) => Point(100 + modX, 0) -> down // F to A |
89 |
| - case (0, 3, `left`) => Point(50 + modY, 0) -> down // F to B |
90 |
| - case (0, 3, `right`) => Point(50 + modY, 149) -> up // F to D |
91 |
| - |
92 |
| - val (tiles, path) = parse(input) |
93 |
| - follow(tiles, path, handleWrap) |
| 63 | + def part2(input: Seq[String], block: Int): Int = |
| 64 | + val tiles = parseTiles(input) |
| 65 | + val moves = parseMoves(input) |
| 66 | + |
| 67 | + val scaleIJ = block - 1 |
| 68 | + val scaleK = block + 1 |
| 69 | + val startingPosition = Vec(-scaleIJ, -scaleIJ, -scaleK) |
| 70 | + val startingDirection = Vec(2, 0, 0) |
| 71 | + |
| 72 | + val topLeft = tiles.keys.filter(_.y == 0).minBy(_.x) |
| 73 | + val start = Info(topLeft, Vec(1, 0, 0), Vec(0, 1, 0), Vec(0, 0, 1)) |
| 74 | + |
| 75 | + val todo = collection.mutable.Queue(start) |
| 76 | + val visited = collection.mutable.Set(topLeft) |
| 77 | + val points = collection.mutable.Map[Vec, Info]() |
| 78 | + |
| 79 | + while todo.nonEmpty do |
| 80 | + val Info(offset, i, j, k) = todo.dequeue() |
| 81 | + |
| 82 | + for x <- 0 until block do |
| 83 | + for y <- 0 until block do |
| 84 | + val key = (i * (2 * x - scaleIJ)) + (j * (2 * y - scaleIJ)) + (k * -scaleK) // Scale by 2 to keep points integer |
| 85 | + points(key) = Info(offset + Point(x, y), i, j, k) |
| 86 | + |
| 87 | + val neighbours = Seq( |
| 88 | + Info(offset + Point(-block, 0), j.cross(i), j, j.cross(k)), // Left |
| 89 | + Info(offset + Point(block, 0), i.cross(j), j, k.cross(j)), // Right |
| 90 | + Info(offset + Point(0, -block), i, j.cross(i), k.cross(i)), // Up |
| 91 | + Info(offset + Point(0, block), i, i.cross(j), i.cross(k))) // Down |
| 92 | + |
| 93 | + neighbours.foreach { next => |
| 94 | + if tiles.contains(next.point) && !visited.contains(next.point) then |
| 95 | + todo += next |
| 96 | + visited += next.point |
| 97 | + } |
| 98 | + end while |
| 99 | + |
| 100 | + val (position, direction) = moves.foldLeft((startingPosition, startingDirection)) { case ((position, direction), move) => |
| 101 | + move match |
| 102 | + case 'L' => (position, direction.cross(points(position).k)) |
| 103 | + case 'R' => (position, points(position).k.cross(direction)) |
| 104 | + case _ => |
| 105 | + val next = position + direction |
| 106 | + if points.contains(next) then |
| 107 | + if tiles(points(next).point) then (next, direction) else (position, direction) |
| 108 | + else |
| 109 | + val wrapDirection = points(position).k * 2 // This is the fun part |
| 110 | + val wrapPosition = next + wrapDirection |
| 111 | + if tiles(points(wrapPosition).point) then (wrapPosition, wrapDirection) else (position, direction) |
| 112 | + } |
| 113 | + |
| 114 | + val Info(point, i, j, k) = points(position) |
| 115 | + point.score + Seq(i * 2, j * 2, i * -2, j * -2).indexOf(direction) |
94 | 116 | end part2
|
95 | 117 |
|
96 | 118 | def main(args: Array[String]): Unit =
|
97 | 119 | val data = io.Source.fromResource("AdventOfCode2022/Day22.txt").getLines().toSeq
|
98 | 120 | println(part1(data))
|
99 |
| - println(part2(data)) |
| 121 | + println(part2(data, 50)) |
0 commit comments