Skip to content

Commit e90ee40

Browse files
committed
Implement 2024 day 15
1 parent 98983f6 commit e90ee40

File tree

5 files changed

+234
-0
lines changed

5 files changed

+234
-0
lines changed

2024/src/aoc/days/day15.py

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import itertools
2+
3+
import numpy
4+
5+
from . import SeparateRunner
6+
7+
8+
def parse_input(data: str) -> tuple[numpy.array, str]:
9+
grid, steps = data.split("\n\n")
10+
11+
grid_split = numpy.array([list(line) for line in grid.split("\n")])
12+
13+
steps = "".join(steps.split("\n"))
14+
15+
return grid_split, steps
16+
17+
18+
class DayRunner(SeparateRunner):
19+
@classmethod
20+
def part1(cls, input: str) -> None:
21+
grid, steps = parse_input(input)
22+
23+
y, x = numpy.where(grid == "@")
24+
x, y = x[0], y[0]
25+
26+
for c in steps:
27+
match c:
28+
case "^":
29+
dx, dy = 0, -1
30+
case ">":
31+
dx, dy = 1, 0
32+
case "<":
33+
dx, dy = -1, 0
34+
case "v":
35+
dx, dy = 0, 1
36+
case other:
37+
raise ValueError(f"Invalid movement: {other}")
38+
39+
match grid[y + dy, x + dx]:
40+
case "#":
41+
continue
42+
case "O":
43+
crashed = False
44+
for dist in itertools.count(2):
45+
match grid[y + dist * dy, x + dist * dx]:
46+
case "O":
47+
continue
48+
case "#":
49+
crashed = True
50+
break
51+
case _:
52+
crashed = False
53+
break
54+
55+
if crashed:
56+
continue
57+
58+
grid[y + dist * dy, x + dist * dx] = "O"
59+
case _:
60+
pass
61+
62+
grid[y, x] = "."
63+
x += dx
64+
y += dy
65+
grid[y, x] = "@"
66+
67+
stones = numpy.where(grid == "O")
68+
69+
return sum(100 * y + x for y, x in zip(*stones))
70+
71+
@classmethod
72+
def part2(cls, input: str) -> None:
73+
input = input.replace(".", "..")
74+
input = input.replace("#", "##")
75+
input = input.replace("O", "[]")
76+
input = input.replace("@", "@.")
77+
78+
grid, steps = parse_input(input)
79+
80+
y, x = numpy.where(grid == "@")
81+
x, y = x[0], y[0]
82+
83+
for c in steps:
84+
match c:
85+
case "^":
86+
dx, dy = 0, -1
87+
case ">":
88+
dx, dy = 1, 0
89+
case "<":
90+
dx, dy = -1, 0
91+
case "v":
92+
dx, dy = 0, 1
93+
case other:
94+
raise ValueError(f"Invalid movement: {other}")
95+
96+
match grid[y + dy, x + dx]:
97+
case "#":
98+
continue
99+
case "]" | "[":
100+
crashed = False
101+
if dy == 0:
102+
# easy case: just move linearly
103+
for dist in itertools.count(2):
104+
match grid[y, x + dist * dx]:
105+
case "[" | "]":
106+
continue
107+
case "#":
108+
crashed = True
109+
break
110+
case _:
111+
break
112+
113+
if crashed:
114+
continue
115+
116+
# shuffle all grid points one over
117+
for steps in range(dist, 1, -1):
118+
grid[y, x + dx * steps] = grid[y, x + dx * (steps - 1)]
119+
else:
120+
if grid[y + dy, x] == "[":
121+
to_check = {x, x + 1}
122+
else:
123+
to_check = {x, x - 1}
124+
125+
moving_stones = [to_check]
126+
127+
for dist in itertools.count(2):
128+
to_check_next = set()
129+
130+
for cx in to_check:
131+
match grid[y + dist * dy, cx]:
132+
case "#":
133+
crashed = True
134+
break
135+
case "[":
136+
to_check_next.add(cx)
137+
to_check_next.add(cx + 1)
138+
case "]":
139+
to_check_next.add(cx)
140+
to_check_next.add(cx - 1)
141+
case _:
142+
continue
143+
144+
if crashed or not to_check_next:
145+
break
146+
moving_stones.append(to_check_next)
147+
to_check = to_check_next
148+
149+
if crashed:
150+
continue
151+
152+
for steps in range(len(moving_stones), 0, -1):
153+
dist = steps + 1
154+
for cx in moving_stones[steps - 1]:
155+
grid[y + dy * dist, cx] = grid[y + dy * (dist - 1), cx]
156+
grid[y + dy * (dist - 1), cx] = "."
157+
case _:
158+
pass
159+
160+
grid[y, x] = "."
161+
x += dx
162+
y += dy
163+
grid[y, x] = "@"
164+
165+
stones = numpy.where(grid == "[")
166+
167+
return sum(100 * y + x for y, x in zip(*stones))

2024/tests/samples/15.1.txt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
##########
2+
#..O..O.O#
3+
#......O.#
4+
#.OO..O.O#
5+
#..O@..O.#
6+
#O#..O...#
7+
#O..O..O.#
8+
#.OO.O.OO#
9+
#....O...#
10+
##########
11+
12+
<vv>^<v^>v>^vv^v>v<>v^v<v<^vv<<<^><<><>>v<vvv<>^v^>^<<<><<v<<<v^vv^v>^
13+
vvv<<^>^v^^><<>>><>^<<><^vv^^<>vvv<>><^^v>^>vv<>v<<<<v<^v>^<^^>>>^<v<v
14+
><>vv>v^v^<>><>>>><^^>vv>v<^^^>>v^v^<^^>v^^>v^<^v>v<>>v^v^<v>v^^<^^vv<
15+
<<v<^>>^^^^>>>v^<>vvv^><v<<<>^^^vv^<vvv>^>v<^^^^v<>^>vvvv><>>v^<<^^^^^
16+
^><^><>>><>^^<<^^v>>><^<v>^<vv>>v>>>^v><>^v><<<<v>>v<v<v>vvv>^<><<>^><
17+
^>><>^v<><^vvv<^^<><v<<<<<><^v<<<><<<^^<v<^^^><^>>^<v^><<<^>>^v<v^v<v^
18+
>^>>^v>vv>^<<^v<>><<><<v<<v><>v<^vv<<<>^^v^>^^>>><<^v>>v^v><^^>>^<>vv^
19+
<><^^>^^^<><vvvvv^v<v<<>^v<v>v<<^><<><<><<<^^<<<^<<>><<><^^^>^^<>^>v<>
20+
^^>vv<^v^v<vv>^<><v<^v>^^^>>>^^vvv^>vvv<>>>^<^>>>>>^<<^v>^vvv<>^<><<v>
21+
v^^>>><<^^<>>^v^<v^vv<>v^<<>^<^v^v><^<<<><<^<v><v<>vv>>v><v^<vv<>v^<<^

2024/tests/samples/15.2.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
########
2+
#..O.O.#
3+
##@.O..#
4+
#...O..#
5+
#.#.O..#
6+
#...O..#
7+
#......#
8+
########
9+
10+
<^^>>>vv<v>>v<<

2024/tests/samples/15.3.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#######
2+
#...#.#
3+
#.....#
4+
#..OO@#
5+
#..O..#
6+
#.....#
7+
#######
8+
9+
<vv<<^^<<^^

2024/tests/test_day15.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import pytest
2+
3+
from aoc.days.day15 import DayRunner
4+
5+
from . import get_data
6+
7+
8+
@pytest.mark.parametrize(
9+
"data,result",
10+
[
11+
(get_data(15, 1), 10092),
12+
(get_data(15, 2), 2028),
13+
],
14+
)
15+
def test_sample_part1(data: str, result: int) -> None:
16+
assert DayRunner.part1(data) == result
17+
18+
19+
@pytest.mark.parametrize(
20+
"data,result",
21+
[
22+
(get_data(15, 1), 9021),
23+
(get_data(15, 3), 618),
24+
],
25+
)
26+
def test_sample_part2(data: str, result: int) -> None:
27+
assert DayRunner.part2(data) == result

0 commit comments

Comments
 (0)