diff --git a/src/main/kotlin/utils/Point.kt b/src/main/kotlin/utils/Point.kt index 529dd70..c28291f 100644 --- a/src/main/kotlin/utils/Point.kt +++ b/src/main/kotlin/utils/Point.kt @@ -48,6 +48,14 @@ data class Point(val x: Int, val y: Int) { copy(x = x + it.x, y = y + it.y) } + fun getNeighbour(direction: Char): Point = when (direction) { + '^' -> n + '>' -> e + 'v' -> s + '<' -> w + else -> error("Unknown direction: $direction") + } + operator fun plus(other: Point) = Point(other.x + x, other.y + y) operator fun minus(other: Point) = Point(x - other.x, y - other.y) @@ -77,6 +85,18 @@ fun Map.traverse(block: (Point, T?) -> Unit) { } } +fun Map.printGrid(default: Char = '.') { + val width = maxOf { it.key.x } + val height = maxOf { it.key.y } + + repeat(height.inc()) { y -> + repeat(width.inc()) { x -> + print(getOrDefault(Point(x, y), default)) + } + println() + } +} + fun pointsInArea(point1: Point, point2: Point): List { val (minY, maxY) = listOf(point1.y, point2.y).sorted() val (minX, maxX) = listOf(point1.x, point2.x).sorted() diff --git a/src/main/kotlin/y2024/day15/Day15.kt b/src/main/kotlin/y2024/day15/Day15.kt new file mode 100644 index 0000000..47ffed6 --- /dev/null +++ b/src/main/kotlin/y2024/day15/Day15.kt @@ -0,0 +1,142 @@ +package y2024.day15 + +import utils.Point +import utils.getInputFile +import utils.toPointGrid + +fun main() { + println("Part one: " + Day15.solvePartOne()) + println("Part two: " + Day15.solvePartTwo()) +} + +object Day15 { + + private val instructions: CharArray + private val gridInput: List + + init { + val (gridInput, instructionInput) = getInputFile(this::class.java.packageName, example = false) + .readText() + .split("\n\n") + .map { it.split("\n") } + instructions = instructionInput.joinToString("").toCharArray() + this.gridInput = gridInput + } + + fun solvePartOne(): Long { + val grid = gridInput.toPointGrid().toMutableMap() + var robot = grid.entries.find { it.value == '@' }!!.key + + instructions.forEach { direction -> + val movement = generateSequence(robot.getNeighbour(direction).takeUnless { grid.isWall(it) }) { lastPos -> + if (grid[lastPos] == '.') return@generateSequence null + lastPos.getNeighbour(direction).takeUnless { grid.isWall(it) } + }.toList() + if (movement.isNotEmpty() && movement.any { grid[it] == '.' }) { + grid[robot] = '.' + robot = movement.first() + grid[robot] = '@' + + if (movement.size > 1) { + grid[movement.last()] = 'O' + } + } + } + + return grid + .filterValues { it == 'O' } + .keys + .sumOf { (x, y) -> x + y * 100L } + } + + fun solvePartTwo(): Long { + val grid = gridInput.map { + it.map { c -> + when (c) { + '@' -> "@." + 'O' -> "[]" + else -> "$c$c" + } + }.joinToString(separator = "") + }.toPointGrid().toMutableMap() + var robot = grid.entries.find { it.value == '@' }!!.key + val horizontalMoves = listOf('<', '>') + + instructions.forEach ins@{ direction -> + val nextPos = if (direction in horizontalMoves) { + // Horizontal logic is just the same as P1 + val movement = generateSequence(robot.getNeighbour(direction).takeUnless { grid.isWall(it) }) { lastPos -> + if (grid[lastPos] == '.') return@generateSequence null + lastPos.getNeighbour(direction).takeUnless { grid.isWall(it) } + }.toList() + if (movement.isEmpty() || grid[movement.last()] != '.') return@ins + movement + .reversed() + .windowed(2) + .forEach { (a, b) -> + grid[a] = grid[b]!! + } + movement.first() + } else { + // Vertical logic needs to account for larger boxes... + val movingBoxes = mutableSetOf>() + val searchBoxes = mutableSetOf>() + val nextPos = robot.getNeighbour(direction).takeUnless { grid.isWall(it) } ?: return@ins + + if (grid.isBox(nextPos)) { + searchBoxes += grid.fullBox(nextPos) + } + + while (searchBoxes.isNotEmpty()) { + val nextBox = searchBoxes.first() + val nextLeft = nextBox.first.getNeighbour(direction) + val nextRight = nextBox.second.getNeighbour(direction) + + if (grid.isWall(nextLeft) || grid.isWall(nextRight)) return@ins // Box will move into a wall, instruction is impossible, exit + + if (grid.isBox(nextLeft)) { + searchBoxes += grid.fullBox(nextLeft) + } + if (grid.isBox(nextRight)) { + searchBoxes += grid.fullBox(nextRight) + } + + movingBoxes += nextBox + searchBoxes -= nextBox + } + + movingBoxes + .onEach { (bl, br) -> + // Mark all moving box spaces as empty + grid[bl] = '.' + grid[br] = '.' + } + .forEach { (bl, br) -> + // Update all new box positions + grid[bl.getNeighbour(direction)] = '[' + grid[br.getNeighbour(direction)] = ']' + } + + nextPos + } + + // Move robot + grid[robot] = '.' + robot = nextPos + grid[robot] = '@' + } + + return grid + .filterValues { it == '[' } + .keys + .sumOf { (x, y) -> x + y * 100L } + } + + private fun Map.isWall(point: Point) = get(point) == '#' + private fun Map.isBox(point: Point) = get(point) == '[' || get(point) == ']' + private fun Map.fullBox(point: Point) = when (get(point)) { + '[' -> point to point.e + ']' -> point.w to point + else -> error("No a box point") + } +} diff --git a/src/main/kotlin/y2024/day15/input.txt b/src/main/kotlin/y2024/day15/input.txt new file mode 100644 index 0000000..c350f67 Binary files /dev/null and b/src/main/kotlin/y2024/day15/input.txt differ diff --git a/src/test/kotlin/y2024/day15/Day15Test.kt b/src/test/kotlin/y2024/day15/Day15Test.kt new file mode 100644 index 0000000..ae085a3 --- /dev/null +++ b/src/test/kotlin/y2024/day15/Day15Test.kt @@ -0,0 +1,16 @@ +package y2024.day15 + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +internal class Day15Test { + @Test + fun solvePartOne() { + assertEquals(1487337, Day15.solvePartOne()) + } + + @Test + fun solvePartTwo() { + assertEquals(1521952, Day15.solvePartTwo()) + } +}