Skip to content

Commit 5e6372c

Browse files
committed
Day 15
1 parent 6012c0b commit 5e6372c

File tree

3 files changed

+226
-6
lines changed

3 files changed

+226
-6
lines changed

src/main/kotlin/y24/Day14.kt

+4-6
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,12 @@ class Day14(val input: Input, val rows: Int = 103, val cols: Int = 101) : Puzzle
1818
data class Robot(val p: Point, val v: Point)
1919

2020
private fun Robot.move(n: Int): Robot {
21-
var x = p.x + v.x * n
22-
while (x < 0) x += cols
23-
var y = p.y + v.y * n
24-
while (y < 0) y += rows
21+
val x = Math.floorMod(p.x + v.x * n, cols)
22+
val y = Math.floorMod(p.y + v.y * n, rows)
2523

2624
return Robot(Point(
27-
col = x % cols,
28-
row = y % rows,
25+
col = x,
26+
row = y,
2927
), v)
3028
}
3129

src/main/kotlin/y24/Day15.kt

+179
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
package y24
2+
3+
import common.puzzle.solvePuzzle
4+
import common.puzzle.Input
5+
import common.puzzle.Puzzle
6+
import common.datastructures.*
7+
import common.ext.*
8+
import common.util.*
9+
import java.util.*
10+
import kotlin.math.*
11+
import kotlin.system.exitProcess
12+
13+
14+
fun main() = solvePuzzle(year = 2024, day = 15) { Day15(it) }
15+
16+
class Day15(val input: Input) : Puzzle {
17+
18+
sealed class Tile {
19+
data object Wall : Tile()
20+
data class Box(val left: Boolean) : Tile()
21+
data object Empty : Tile()
22+
}
23+
24+
data class Parsed(
25+
val grid: Grid<Tile>,
26+
val robot: Point,
27+
val moves: List<Dir>,
28+
)
29+
30+
private fun parseInput(lines: List<String>): Parsed {
31+
val emptyLine = lines.indexOf("")
32+
var robot: Point? = null
33+
val grid = Grid(lines.subList(0, emptyLine)) { row, col, c ->
34+
when (c) {
35+
'#' -> Tile.Wall
36+
'.' -> Tile.Empty
37+
'@' -> {
38+
robot = Point(col, row)
39+
Tile.Empty
40+
}
41+
'O' -> Tile.Box(true)
42+
else -> error("unknown tile $c")
43+
}
44+
}
45+
46+
val moves = lines.subList(emptyLine + 1, lines.size).flatMap {
47+
it.map { c ->
48+
when (c) {
49+
'<' -> directions[0]
50+
'^' -> directions[1]
51+
'>' -> directions[2]
52+
'v' -> directions[3]
53+
else -> error("unknown direction $c")
54+
}
55+
}
56+
}
57+
58+
val r = robot ?: error("no robot found")
59+
return Parsed(grid, r, moves)
60+
}
61+
62+
private fun tryMove(grid: Grid<Tile>, p: Point, dir: Dir): Boolean {
63+
val neigh = grid[p + dir.toPoint()]
64+
if (neigh.value == Tile.Wall) {
65+
return false
66+
}
67+
68+
if (neigh.value is Tile.Box) {
69+
if (!tryMove(grid, neigh.toPoint(), dir)) {
70+
return false
71+
}
72+
}
73+
74+
neigh.value = grid[p].value
75+
grid[p].value = Tile.Empty
76+
return true
77+
}
78+
79+
private fun tryMove2(grid: Grid<Tile>, p: Point, dir: Dir): Boolean {
80+
if (!move2(grid, p, dir, dryRun = true)) {
81+
return false
82+
}
83+
84+
return move2(grid, p, dir, dryRun = false)
85+
}
86+
87+
private fun move2(grid: Grid<Tile>, p: Point, dir: Dir, dryRun: Boolean): Boolean {
88+
val toMove = when (val t = grid[p].value) {
89+
Tile.Empty -> listOf(p) // Robot
90+
Tile.Box(left = true) -> listOf(p + Point(1, 0), p)
91+
Tile.Box(left = false) -> listOf(p + Point(-1, 0), p)
92+
else -> error("unexpected tile $t")
93+
}
94+
95+
return toMove.all { point ->
96+
val neigh = grid[point + dir.toPoint()]
97+
if (neigh.value == Tile.Wall) {
98+
return@all false
99+
}
100+
101+
if (neigh.value is Tile.Box) {
102+
val neighIsLeftBox = (neigh.value as Tile.Box).left
103+
// Prevent recursion, this is the left side pushing into the right side to the right (or vice versa).
104+
// We will already check this move as part of the right side of the box.
105+
val skip = (dir == Dir(0, 1) && !neighIsLeftBox) ||
106+
(dir == Dir(0, -1) && neighIsLeftBox)
107+
if (!skip && !move2(grid, neigh.toPoint(), dir, dryRun)) {
108+
return@all false
109+
}
110+
}
111+
112+
if (dryRun) {
113+
return@all true
114+
}
115+
116+
neigh.value = grid[point].value
117+
grid[point].value = Tile.Empty
118+
return@all true
119+
}
120+
}
121+
122+
private fun expand(grid: Grid<Tile>): Grid<Tile> {
123+
return Grid(rows = grid.numRows, cols = grid.numCols * 2) { row, col ->
124+
val v = grid[row][col / 2].value
125+
if (v is Tile.Box) {
126+
v.copy(left = col % 2 == 0)
127+
} else {
128+
v
129+
}
130+
}
131+
}
132+
133+
private fun Grid<Tile>.gpsSum(): Int {
134+
return cells().sumOf { (row, col, value) ->
135+
when (value) {
136+
Tile.Box(true) -> 100 * row + col
137+
else -> 0
138+
}
139+
}
140+
}
141+
142+
override fun solveLevel1(): Any {
143+
val (grid, robotInitial, moves) = parseInput(input.lines)
144+
145+
var robot = robotInitial
146+
moves.forEach { dir ->
147+
if (tryMove(grid, robot, dir)) {
148+
robot += dir.toPoint()
149+
}
150+
}
151+
152+
return grid.gpsSum()
153+
}
154+
155+
override fun solveLevel2(): Any {
156+
val (grid, robotInitial, moves) = parseInput(input.lines)
157+
val expandedGrid = expand(grid)
158+
var robot = robotInitial.copy(col = robotInitial.col * 2)
159+
moves.forEach { dir ->
160+
if (tryMove2(expandedGrid, robot, dir)) {
161+
robot += dir.toPoint()
162+
}
163+
164+
expandedGrid.cellFormatter = Grid.CellFormatter({ cell ->
165+
if (cell.toPoint() == robot) return@CellFormatter "@"
166+
when (cell.value) {
167+
Tile.Wall -> "#"
168+
Tile.Empty -> "."
169+
Tile.Box(true) -> "["
170+
Tile.Box(false) -> "]"
171+
else -> error("")
172+
}
173+
}, "")
174+
// println(expandedGrid)
175+
}
176+
177+
return expandedGrid.gpsSum()
178+
}
179+
}

src/test/kotlin/y24/Day15Test.kt

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package y24
2+
3+
import common.puzzle.Input
4+
import org.junit.jupiter.api.Assertions.assertEquals
5+
import org.junit.jupiter.api.Test
6+
7+
internal class Day15Test {
8+
private val sample = Input("""
9+
##########
10+
#..O..O.O#
11+
#......O.#
12+
#.OO..O.O#
13+
14+
#O#..O...#
15+
#O..O..O.#
16+
#.OO.O.OO#
17+
#....O...#
18+
##########
19+
20+
<vv>^<v^>v>^vv^v>v<>v^v<v<^vv<<<^><<><>>v<vvv<>^v^>^<<<><<v<<<v^vv^v>^
21+
vvv<<^>^v^^><<>>><>^<<><^vv^^<>vvv<>><^^v>^>vv<>v<<<<v<^v>^<^^>>>^<v<v
22+
><>vv>v^v^<>><>>>><^^>vv>v<^^^>>v^v^<^^>v^^>v^<^v>v<>>v^v^<v>v^^<^^vv<
23+
<<v<^>>^^^^>>>v^<>vvv^><v<<<>^^^vv^<vvv>^>v<^^^^v<>^>vvvv><>>v^<<^^^^^
24+
^><^><>>><>^^<<^^v>>><^<v>^<vv>>v>>>^v><>^v><<<<v>>v<v<v>vvv>^<><<>^><
25+
^>><>^v<><^vvv<^^<><v<<<<<><^v<<<><<<^^<v<^^^><^>>^<v^><<<^>>^v<v^v<v^
26+
>^>>^v>vv>^<<^v<>><<><<v<<v><>v<^vv<<<>^^v^>^^>>><<^v>>v^v><^^>>^<>vv^
27+
<><^^>^^^<><vvvvv^v<v<<>^v<v>v<<^><<><<><<<^^<<<^<<>><<><^^^>^^<>^>v<>
28+
^^>vv<^v^v<vv>^<><v<^v>^^^>>>^^vvv^>vvv<>>>^<^>>>>>^<<^v>^vvv<>^<><<v>
29+
v^^>>><<^^<>>^v^<v^vv<>v^<<>^<^v^v><^<<<><<^<v><v<>vv>>v><v^<vv<>v^<<^
30+
""".trimIndent())
31+
32+
private val day = Day15(sample)
33+
34+
@Test
35+
fun solveLevel1() {
36+
assertEquals(10092, day.solveLevel1())
37+
}
38+
39+
@Test
40+
fun solveLevel2() {
41+
assertEquals(9021, day.solveLevel2())
42+
}
43+
}

0 commit comments

Comments
 (0)