-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathday_15.py
278 lines (221 loc) · 8.54 KB
/
day_15.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
from utils import read_multisection_input, create_grid, Coordinate
from typing import Tuple, NamedTuple, Literal
class Delta(NamedTuple):
dx: int
dy: int
MapData = list[str]
Grid = dict[Coordinate, str]
Direction = Literal["^", ">", "<", "v"]
directions = {
"^": Delta(0, -1),
">": Delta(1, 0),
"v": Delta(0, 1),
"<": Delta(-1, 0),
}
def print_grid(grid: Grid, robot: Coordinate) -> None:
"""Prints a sparse grid, replacing missing items with '.'
and the robot position with '@'."""
max_x = max(x for x, y in grid)
max_y = max(y for x, y in grid)
index_row = "".join([str(x) for x in list(range(0, max_x + 1))])
print(" ", index_row)
for y in range(max_y + 1):
print(y, end=" ")
for x in range(max_x + 1):
if robot == (x, y):
print("@", end="")
else:
print(grid.get((x, y), "."), end="")
print()
print()
## Part 1 helpers
def move(
position: Coordinate,
direction: Literal["^", ">", "<", "v"],
grid: Grid,
) -> Tuple[Coordinate, Grid]:
delta = directions[direction]
next_position = Coordinate(position.x + delta.dx, position.y + delta.dy)
next_spot = grid.get(next_position, None)
if next_spot is None:
return next_position, grid
elif next_spot == "#":
return position, grid
elif next_spot == "O":
to_move = [next_position]
next_next_position = Coordinate(
next_position.x + delta.dx, next_position.y + delta.dy
)
next_spot = grid.get(next_next_position, None)
while next_spot == "O":
to_move.append(next_next_position)
next_next_position = Coordinate(
next_next_position.x + delta.dx, next_next_position.y + delta.dy
)
next_spot = grid.get(next_next_position, None)
if next_spot == "#":
return position, grid
for coord in to_move[::-1]:
n = Coordinate(coord.x + delta.dx, coord.y + delta.dy)
grid[n] = "O"
del grid[coord]
position = next_position
return position, grid
## Part 2 helpers
def create_expanded_grid(data: MapData) -> Tuple[Grid, Coordinate]:
"""Creates a grid based on the expansion rules:
1. . -> ..
2. # -> ##
3. O -> []
4. @ -> @.
:returns Grid
"""
grid = {}
start_position = None
for y, row in enumerate(data):
x = 0
for _, cell in enumerate(row):
if cell == "#":
grid[Coordinate(x, y)] = cell
grid[Coordinate(x + 1, y)] = cell
elif cell == "O":
grid[Coordinate(x, y)] = "["
grid[Coordinate(x + 1, y)] = "]"
elif cell == "@":
start_position = Coordinate(x, y)
x += 2
return grid, start_position
def can_push_up_or_down(position: Coordinate, direction: Direction, grid: Grid) -> bool:
"""Determines recursively if a stack of [] boxes can be moved to given up/down direction"""
delta = directions[direction]
above_or_below = grid.get((position.x, position.y + delta.dy))
if above_or_below is None:
return True
coordinates: list[Coordinate] = []
if above_or_below == "[":
coordinates.append(Coordinate(position.x, position.y + delta.dy))
coordinates.append(Coordinate(position.x + 1, position.y + delta.dy))
elif above_or_below == "]":
coordinates.append(Coordinate(position.x, position.y + delta.dy))
coordinates.append(Coordinate(position.x - 1, position.y + delta.dy))
elif above_or_below == "#":
return False
return all(
can_push_up_or_down(coordinate, direction, grid) for coordinate in coordinates
)
def get_coordinates_for_vertical_push(
direction: Direction,
grid: Grid,
coordinates: set[Coordinate],
) -> list[Coordinate]:
delta = directions[direction]
# If we have coordinates and all of the spots
# above or below them are empty, return all current coordinates
if coordinates and all(
grid.get((c.x, c.y + delta.dy)) is None for c in coordinates
):
return coordinates
# Find the next row to check
next_row = set()
# For every coordinate in the previous row
for coord in coordinates:
above_or_below = grid.get((coord.x, coord.y + delta.dy))
# If left side of the box, add it and its right neighbour
if above_or_below == "[":
next_row.add(Coordinate(coord.x, coord.y + delta.dy))
next_row.add(Coordinate(coord.x + 1, coord.y + delta.dy))
# If right side of the box, add it and its left neighbour
elif above_or_below == "]":
next_row.add(Coordinate(coord.x, coord.y + delta.dy))
next_row.add(Coordinate(coord.x - 1, coord.y + delta.dy))
# Recusively go through every position
return coordinates | get_coordinates_for_vertical_push(direction, grid, next_row)
def push_horizontally(position: Coordinate, direction: Direction, grid: Grid):
delta = directions[direction]
next_position = Coordinate(position.x + delta.dx, position.y + delta.dy)
next_spot = grid.get(next_position, None)
x = next_position.x
# Go to the end of a line of boxes
while next_spot in "[]":
next_spot = grid.get(Coordinate(x + delta.dx, next_position.y), ".")
x += delta.dx
# If there's a wall at the end, move nothing
if next_spot == "#":
return position, grid
# If there's an empty spot
if next_spot == ".":
# Walk back from the empty spot
while x != next_position.x:
# Replace spot with its neighbour
grid[Coordinate(x, next_position.y)] = grid[
Coordinate(x - delta.dx, next_position.y)
]
x -= delta.dx
# Remove the first moved item so we don't duplicate
del grid[next_position]
return next_position, grid
def push_vertically(position: Coordinate, direction: Direction, grid: Grid):
delta = directions[direction]
# Check if we can push the full stack
if not can_push_up_or_down(position, direction, grid):
return position, grid
# Find coordinates for each item to be pushed
to_move = get_coordinates_for_vertical_push(direction, grid, set([position])) - set(
[position]
) # Remove starting position as that gets moved separately
# Sort them by y axis so we're always moving from the end first
reverse = not (direction == "^")
to_move = sorted(to_move, key=lambda x: x.y, reverse=reverse)
for coordinate in to_move:
# Move each item to their next position
grid[Coordinate(coordinate.x, coordinate.y + delta.dy)] = grid[coordinate]
# Delete moved item
del grid[coordinate]
# Return robot's new position which is one up or down from starting, and the grid.
return Coordinate(position.x, position.y + delta.dy), grid
def move_expanded(
position: Coordinate,
direction: Direction,
grid: Grid,
) -> Tuple[Coordinate, Grid]:
delta = directions[direction]
next_position = Coordinate(position.x + delta.dx, position.y + delta.dy)
next_spot = grid.get(next_position, None)
if next_spot is None:
return next_position, grid
elif next_spot == "#":
return position, grid
elif next_spot in "[]":
if direction in "<>":
return push_horizontally(position, direction, grid)
if direction in "^v":
return push_vertically(position, direction, grid)
def part_1():
map_data, movement_data = read_multisection_input(15, [str, str])
grid = create_grid(map_data.split("\n"), predicate=lambda x: x != ".")
position = [key for key, value in grid.items() if value == "@"][0]
del grid[position]
movements = "".join(movement_data.split("\n"))
for movement in movements:
position, grid = move(position, movement, grid)
gps = 0
for coord, value in grid.items():
if value == "O":
gps += coord.y * 100 + coord.x
print(f"Part 1: {gps}")
assert gps == 1413675
def part_2():
map_data, movement_data = read_multisection_input(15, [str, str])
movements = "".join(movement_data.split("\n"))
grid, position = create_expanded_grid(map_data.split("\n"))
for movement in movements:
position, grid = move_expanded(position, movement, grid)
gps = 0
for coord, value in grid.items():
if value == "[":
gps += coord.y * 100 + coord.x
print(f"Part 2: {gps}")
assert gps == 1399772
if __name__ == "__main__":
part_1()
part_2()