|
| 1 | +package aockt.y2024 |
| 2 | + |
| 3 | +import aockt.util.parse |
| 4 | +import aockt.util.spacial.Direction |
| 5 | +import aockt.util.spacial.Direction.* |
| 6 | +import aockt.util.spacial.* |
| 7 | +import aockt.y2024.Y2024D15.Warehouse.Tile |
| 8 | +import aockt.y2024.Y2024D15.Warehouse.Tile.* |
| 9 | +import io.github.jadarma.aockt.core.Solution |
| 10 | + |
| 11 | +object Y2024D15 : Solution { |
| 12 | + |
| 13 | + /** Model of a lanternfish food warehouse. */ |
| 14 | + private class Warehouse(val data: MutableGrid<Tile>) : Grid<Tile> by data { |
| 15 | + |
| 16 | + /** The types of tile a singular grid cell can have. */ |
| 17 | + enum class Tile { Wall, Empty, Robot, Crate, WideCrateLeft, WideCrateRight } |
| 18 | + |
| 19 | + /** The current robot position. */ |
| 20 | + private lateinit var robot: Point |
| 21 | + |
| 22 | + /** |
| 23 | + * Thorough input validation to ensure later assumptions: |
| 24 | + * - All edges are walls. |
| 25 | + * - Only one robot. |
| 26 | + * - Wide crate halves are properly paired. |
| 27 | + */ |
| 28 | + init { |
| 29 | + var foundRobot = false |
| 30 | + data.points().forEach { (p, v) -> |
| 31 | + if (p.x == 0L || p.y == 0L || p.x == width - 1L || p.y == height - 1L) { |
| 32 | + require(v == Wall) { "Invalid warehouse map. Edges should be walls." } |
| 33 | + } |
| 34 | + |
| 35 | + if (v == Robot) { |
| 36 | + require(!foundRobot) { "There can only be one robot." } |
| 37 | + foundRobot = true |
| 38 | + robot = p |
| 39 | + } |
| 40 | + |
| 41 | + if (v == WideCrateLeft) { |
| 42 | + require(getOrNull(p.move(Right)) == WideCrateRight) { "Wide crate cannot have split halves." } |
| 43 | + } |
| 44 | + |
| 45 | + if (v == WideCrateRight) { |
| 46 | + require(getOrNull(p.move(Left)) == WideCrateLeft) { "Wide crate cannot have split halves." } |
| 47 | + } |
| 48 | + } |
| 49 | + |
| 50 | + require(foundRobot) { "Warehouse must have exactly one robot, but none was found." } |
| 51 | + } |
| 52 | + |
| 53 | + /** Calculate the sum of crates' GPS signals. */ |
| 54 | + fun boxGpsSignal(): Long = points().sumOf { (p, v) -> |
| 55 | + when (v) { |
| 56 | + Crate, WideCrateLeft -> (height - 1 - p.y) * 100 + p.x |
| 57 | + else -> 0L |
| 58 | + } |
| 59 | + } |
| 60 | + |
| 61 | + /** |
| 62 | + * Attempt to move the robot in a [direction] and move crates accordingly. |
| 63 | + * This mutates the warehouse and returns a reference to the same instance. |
| 64 | + */ |
| 65 | + fun move(direction: Direction): Warehouse = apply { shift(direction) } |
| 66 | + |
| 67 | + /** Checks if the robot can move in a [direction]. It can do so if it is an empty space, or a movable crate. */ |
| 68 | + private fun canMove(direction: Direction): Boolean { |
| 69 | + |
| 70 | + fun recurse(point: Point): Boolean { |
| 71 | + val next = point.move(direction) |
| 72 | + return when (get(point)) { |
| 73 | + Empty -> true |
| 74 | + Wall -> false |
| 75 | + WideCrateLeft -> recurse(next) && (direction is Horizontal || recurse(next.move(Right))) |
| 76 | + WideCrateRight -> recurse(next) && (direction is Horizontal || recurse(next.move(Left))) |
| 77 | + Crate, Robot -> recurse(next) |
| 78 | + } |
| 79 | + } |
| 80 | + |
| 81 | + return recurse(robot) |
| 82 | + } |
| 83 | + |
| 84 | + /** Moves the robot in a [direction] and moves crates if necessary. Only performs the action if it is valid. */ |
| 85 | + private fun shift(direction: Direction) { |
| 86 | + |
| 87 | + if (!canMove(direction)) return |
| 88 | + |
| 89 | + fun recurse(point: Point) { |
| 90 | + val current = get(point) |
| 91 | + val next = point.move(direction) |
| 92 | + |
| 93 | + if (current == Empty) return |
| 94 | + check(current != Wall) { "Tried to shift a wall." } |
| 95 | + |
| 96 | + recurse(next) |
| 97 | + data[next] = current |
| 98 | + data[point] = Empty |
| 99 | + if (current == Robot) robot = next |
| 100 | + |
| 101 | + if (direction is Horizontal) return |
| 102 | + |
| 103 | + if (current == WideCrateLeft) { |
| 104 | + recurse(next.move(Right)) |
| 105 | + data[next.move(Right)] = WideCrateRight |
| 106 | + data[point.move(Right)] = Empty |
| 107 | + } |
| 108 | + |
| 109 | + if (current == WideCrateRight) { |
| 110 | + recurse(next.move(Left)) |
| 111 | + data[next.move(Left)] = WideCrateLeft |
| 112 | + data[point.move(Left)] = Empty |
| 113 | + } |
| 114 | + } |
| 115 | + |
| 116 | + recurse(robot) |
| 117 | + } |
| 118 | + } |
| 119 | + |
| 120 | + /** |
| 121 | + * Parse the [input] and return the [Warehouse] layout and the robot movements. |
| 122 | + * If [wide], returns the part two warehouse. |
| 123 | + */ |
| 124 | + private fun parseInput(input: String, wide: Boolean): Pair<Warehouse, List<Direction>> = parse { |
| 125 | + val (originalLayout, movements) = input.split("\n\n", limit = 2) |
| 126 | + val layout = if (!wide) originalLayout else originalLayout |
| 127 | + .replace("#", "##") |
| 128 | + .replace(".", "..") |
| 129 | + .replace("O", "[]") |
| 130 | + .replace("@", "@.") |
| 131 | + |
| 132 | + val directions = movements.mapNotNull { |
| 133 | + when (it) { |
| 134 | + '<' -> Left |
| 135 | + '>' -> Right |
| 136 | + '^' -> Up |
| 137 | + 'v' -> Down |
| 138 | + '\n' -> null |
| 139 | + else -> error("Invalid direction: $it") |
| 140 | + } |
| 141 | + } |
| 142 | + |
| 143 | + val tiles = MutableGrid(layout) { |
| 144 | + when (it) { |
| 145 | + '#' -> Wall |
| 146 | + '.' -> Empty |
| 147 | + 'O' -> Crate |
| 148 | + '[' -> WideCrateLeft |
| 149 | + ']' -> WideCrateRight |
| 150 | + '@' -> Robot |
| 151 | + else -> error("Invalid map tile: $it") |
| 152 | + } |
| 153 | + } |
| 154 | + |
| 155 | + Warehouse(tiles) to directions |
| 156 | + } |
| 157 | + |
| 158 | + /** Common solution. */ |
| 159 | + private fun solve(input: String, wide: Boolean): Long = |
| 160 | + parseInput(input, wide) |
| 161 | + .let { (warehouse, movements) -> movements.fold(warehouse, Warehouse::move) } |
| 162 | + .boxGpsSignal() |
| 163 | + |
| 164 | + override fun partOne(input: String): Long = solve(input, wide = false) |
| 165 | + override fun partTwo(input: String): Long = solve(input, wide = true) |
| 166 | +} |
0 commit comments