Day 16: The Floor Will Be Lava

Completed in ~4 hours, 33 minutes.
8,641ᵗʰ worldwide.
2023-12-16

With the beam of light completely focused somewhere, the reindeer leads you deeper still into the Lava Production Facility. At some point, you realize that the steel facility walls have been replaced with cave, and the doorways are just cave, and the floor is cave, and you're pretty sure this is actually just a giant cave.

Finally, as you approach what must be the heart of the mountain, you see a bright light in a cavern up ahead. There, you discover that the beam of light you so carefully focused is emerging from the cavern wall closest to the facility and pouring all of its energy into a contraption on the opposite side.

Upon closer inspection, the contraption appears to be a flat, two-dimensional square grid containing empty space (.), mirrors (/ and \), and splitters (| and -).

The contraption is aligned so that most of the beam bounces around the grid, but each tile on the grid converts some of the beam's light into heat to melt the rock in the cavern.

You note the layout of the contraption (your puzzle input). For example:

1
2
3
4
5
6
7
8
9
10
.|...\.... |.-.\..... .....|-... ........|. .......... .........\ ..../.\\.. .-.-/..|.. .|....-|.\ ..//.|....

The beam enters in the top-left corner from the left and heading to the right. Then, its behavior depends on what it encounters as it moves:

  • If the beam encounters empty space (.), it continues in the same direction.
  • If the beam encounters a mirror (/ or \), the beam is reflected 90 degrees depending on the angle of the mirror. For instance, a rightward-moving beam that encounters a / mirror would continue upward in the mirror's column, while a rightward-moving beam that encounters a \ mirror would continue downward from the mirror's column.
  • If the beam encounters the pointy end of a splitter (| or -), the beam passes through the splitter as if the splitter were empty space. For instance, a rightward-moving beam that encounters a - splitter would continue in the same direction.
  • If the beam encounters the flat side of a splitter (| or -), the beam is split into two beams going in each of the two directions the splitter's pointy ends are pointing. For instance, a rightward-moving beam that encounters a | splitter would split into two beams: one that continues upward from the splitter's column and one that continues downward from the splitter's column.

Beams do not interact with other beams; a tile can have many beams passing through it at the same time. A tile is energized if that tile has at least one beam pass through it, reflect in it, or split in it.

In the above example, here is how the beam of light bounces around the contraption:

1
2
3
4
5
6
7
8
9
10
>|<<<\.... |v-.\^.... .v...|->>> .v...v^.|. .v...v^... .v...v^..\ .v../2\\.. <->-/vv|.. .|<<<2-|.\ .v//.|.v..

Beams are only shown on empty tiles; arrows indicate the direction of the beams. If a tile contains beams moving in multiple directions, the number of distinct directions is shown instead. Here is the same diagram but instead only showing whether a tile is energized (#) or not (.):

1
2
3
4
5
6
7
8
9
10
11
######.... .#...#.... .#...##### .#...##... .#...##... .#...##... .#..####.. ########.. .#######.. .#...#.#..

Ultimately, in this example, 46 tiles become energized.

The light isn't energizing enough tiles to produce lava; to debug the contraption, you need to start by analyzing the current situation. With the beam starting in the top-left heading right, how many tiles end up being energized?

Basically, just gotta keep track of...

  • All of the tiles that have been energized
  • All of the beams (the first beam splits almost immediately)

Each beam requires you keeping track of it's position and direction. The direction of the beam can when it lands on a non-empty (.) tile.

I chose to keep track of beams in a coordinate-grid of beams instead of putting position information on the beams themselves.

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
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
from dataclasses import dataclass, field from pathlib import Path from typing import Literal import pytest @dataclass(frozen=True) class Coordinate: x: int y: int @dataclass(frozen=True) class Beam: x_direction: Literal[-1, 0, 1] y_direction: Literal[-1, 0, 1] def __post_init__(self) -> None: assert abs(self.x_direction) != abs(self.y_direction) def default_beam_grid() -> dict[Coordinate, list[Beam]]: return { Coordinate(x=0, y=0): [ Beam( x_direction=1, y_direction=0, ) ] } def default_energized_grid() -> dict[Coordinate, set[Beam]]: return {Coordinate(x=0, y=0): set()} @dataclass class Grid: grid: dict[Coordinate, str] = field(default_factory=dict) beam_grid: dict[Coordinate, list[Beam]] = field(default_factory=default_beam_grid) energized_grid: dict[Coordinate, set[Beam]] = field( default_factory=default_energized_grid ) max_x: int = 0 max_y: int = 0 def add_coordinate(self, *, x: int, y: int, character: str) -> None: self.max_x = max(self.max_x, x + 1) self.max_y = max(self.max_y, y + 1) self.grid[Coordinate(x=x, y=y)] = character def start(self, coordinate: Coordinate, beam: Beam) -> None: self.beam_grid = {coordinate: [beam]} self.energized_grid = {coordinate: set()} def step(self) -> None: new_beam_grid: dict[Coordinate, list[Beam]] = {} for coordinate, beams in self.beam_grid.items(): if not beams: # No beams left in coordinate, just skip continue if coordinate not in self.grid: # Coordinate outside of the grid? # Beam has left the grid and can no longer return continue for beam in beams: if self.grid[coordinate] == ".": # Move the beam in the direction it was going new_coordinate = Coordinate( x=coordinate.x + beam.x_direction, y=coordinate.y + beam.y_direction, ) if new_coordinate not in new_beam_grid: new_beam_grid[new_coordinate] = [] new_beam_grid[new_coordinate].append(beam) elif self.grid[coordinate] == "|": if beam.y_direction in [-1, 1]: # Keep the beam moving new_coordinate = Coordinate( x=coordinate.x + beam.x_direction, y=coordinate.y + beam.y_direction, ) if new_coordinate not in new_beam_grid: new_beam_grid[new_coordinate] = [] new_beam_grid[new_coordinate].append(beam) elif beam.x_direction in [-1, 1]: # Split the beam to go up new_coordinate_up = Coordinate( x=coordinate.x, y=coordinate.y - 1, ) if new_coordinate_up not in new_beam_grid: new_beam_grid[new_coordinate_up] = [] new_beam_grid[new_coordinate_up].append( Beam( x_direction=0, y_direction=-1, ) ) # Split the beam to go down new_coordinate_down = Coordinate( x=coordinate.x, y=coordinate.y + 1, ) if new_coordinate_down not in new_beam_grid: new_beam_grid[new_coordinate_down] = [] new_beam_grid[new_coordinate_down].append( Beam( x_direction=0, y_direction=1, ) ) elif self.grid[coordinate] == "-": if beam.x_direction in [-1, 1]: # Keep the beam moving new_coordinate = Coordinate( x=coordinate.x + beam.x_direction, y=coordinate.y + beam.y_direction, ) if new_coordinate not in new_beam_grid: new_beam_grid[new_coordinate] = [] new_beam_grid[new_coordinate].append(beam) elif beam.y_direction in [-1, 1]: # Split the beam to go left new_coordinate_up = Coordinate( x=coordinate.x - 1, y=coordinate.y, ) if new_coordinate_up not in new_beam_grid: new_beam_grid[new_coordinate_up] = [] new_beam_grid[new_coordinate_up].append( Beam( x_direction=-1, y_direction=0, ) ) # Split the beam to go right new_coordinate_down = Coordinate( x=coordinate.x + 1, y=coordinate.y, ) if new_coordinate_down not in new_beam_grid: new_beam_grid[new_coordinate_down] = [] new_beam_grid[new_coordinate_down].append( Beam( x_direction=1, y_direction=0, ) ) elif self.grid[coordinate] == "/": if beam.x_direction == 1: # Move the beam up new_coordinate = Coordinate( x=coordinate.x, y=coordinate.y - 1, ) if new_coordinate not in new_beam_grid: new_beam_grid[new_coordinate] = [] new_beam_grid[new_coordinate].append( Beam( x_direction=0, y_direction=-1, ) ) elif beam.x_direction == -1: # Move the beam down new_coordinate = Coordinate( x=coordinate.x, y=coordinate.y + 1, ) if new_coordinate not in new_beam_grid: new_beam_grid[new_coordinate] = [] new_beam_grid[new_coordinate].append( Beam( x_direction=0, y_direction=1, ) ) elif beam.y_direction == 1: # Move the beam left new_coordinate = Coordinate( x=coordinate.x - 1, y=coordinate.y, ) if new_coordinate not in new_beam_grid: new_beam_grid[new_coordinate] = [] new_beam_grid[new_coordinate].append( Beam( x_direction=-1, y_direction=0, ) ) elif beam.y_direction == -1: # Move the beam right new_coordinate = Coordinate( x=coordinate.x + 1, y=coordinate.y, ) if new_coordinate not in new_beam_grid: new_beam_grid[new_coordinate] = [] new_beam_grid[new_coordinate].append( Beam( x_direction=1, y_direction=0, ) ) elif self.grid[coordinate] == "\\": if beam.x_direction == 1: # Move the beam down new_coordinate = Coordinate( x=coordinate.x, y=coordinate.y + 1, ) if new_coordinate not in new_beam_grid: new_beam_grid[new_coordinate] = [] new_beam_grid[new_coordinate].append( Beam( x_direction=0, y_direction=1, ) ) elif beam.x_direction == -1: # Move the beam up new_coordinate = Coordinate( x=coordinate.x, y=coordinate.y - 1, ) if new_coordinate not in new_beam_grid: new_beam_grid[new_coordinate] = [] new_beam_grid[new_coordinate].append( Beam( x_direction=0, y_direction=-1, ) ) elif beam.y_direction == 1: # Move the beam right new_coordinate = Coordinate( x=coordinate.x + 1, y=coordinate.y, ) if new_coordinate not in new_beam_grid: new_beam_grid[new_coordinate] = [] new_beam_grid[new_coordinate].append( Beam( x_direction=1, y_direction=0, ) ) elif beam.y_direction == -1: # Move the beam left new_coordinate = Coordinate( x=coordinate.x - 1, y=coordinate.y, ) if new_coordinate not in new_beam_grid: new_beam_grid[new_coordinate] = [] new_beam_grid[new_coordinate].append( Beam( x_direction=-1, y_direction=0, ) ) # If we've seen this beam at this coordinate in the energized grid, remove it cleaned_beam_grid: dict[Coordinate, list[Beam]] = {} for coordinate, beams in new_beam_grid.items(): cleaned_beam_grid[coordinate] = [] if coordinate in self.energized_grid: for beam in beams: if beam in self.energized_grid[coordinate]: pass else: self.energized_grid[coordinate].add(beam) cleaned_beam_grid[coordinate].append(beam) elif coordinate in self.grid: if coordinate not in self.energized_grid: self.energized_grid[coordinate] = set() self.energized_grid[coordinate].update(set(beams)) cleaned_beam_grid[coordinate] = beams self.beam_grid = cleaned_beam_grid def count_energized(self) -> int: count = 0 for coordinate in self.grid: if coordinate in self.energized_grid: count += 1 return count def runner(document: list[str]) -> int: grid = Grid() for y_index, line in enumerate(document): for x_index, character in enumerate(line): grid.add_coordinate(x=x_index, y=y_index, character=character) grid.start(Coordinate(x=0, y=0), Beam(x_direction=1, y_direction=0)) while grid.beam_grid: grid.step() return grid.count_energized() @pytest.mark.parametrize( "filename,output", [ ("example-1.txt", 46), ("example-2.txt", 3), ("example-3.txt", 3), ("example-4.txt", 2), ("example-5.txt", 2), ("example-6.txt", 4), ("example-7.txt", 41), ("example-8.txt", 31), ("example-9.txt", 7543), ], ) def test_runner(filename: str, output: int) -> None: with open(Path(__file__).with_name(filename)) as file: result = runner(file.read().splitlines()) assert result == output

Answer: 7,543

As you try to work out what might be wrong, the reindeer tugs on your shirt and leads you to a nearby control panel. There, a collection of buttons lets you align the contraption so that the beam enters from any edge tile and heading away from that edge. (You can choose either of two directions for the beam if it starts on a corner; for instance, if the beam starts in the bottom-right corner, it can start heading either left or upward.)

So, the beam could start on any tile in the top row (heading downward), any tile in the bottom row (heading upward), any tile in the leftmost column (heading right), or any tile in the rightmost column (heading left). To produce lava, you need to find the configuration that energizes as many tiles as possible.

In the above example, this can be achieved by starting the beam in the fourth tile from the left in the top row:

1
2
3
4
5
6
7
8
9
10
.|<2<\.... |v-v\^.... .v.v.|->>> .v.v.v^.|. .v.v.v^... .v.v.v^..\ .v.v/2\\.. <-2-/vv|.. .|<<<2-|.\ .v//.|.v..

Using this configuration, 51 tiles are energized:

1
2
3
4
5
6
7
8
9
10
.#####.... .#.#.#.... .#.#.##### .#.#.##... .#.#.##... .#.#.##... .#.#####.. ########.. .#######.. .#...#.#..

Find the initial beam configuration that energizes the largest number of tiles; how many tiles are energized in that configuration?

Identical solution to Part 1, but check energized counts for starting at each edge tile.

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
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
from dataclasses import dataclass, field from pathlib import Path from typing import Literal import pytest @dataclass(frozen=True) class Coordinate: x: int y: int @dataclass(frozen=True) class Beam: x_direction: Literal[-1, 0, 1] y_direction: Literal[-1, 0, 1] def __post_init__(self) -> None: assert abs(self.x_direction) != abs(self.y_direction) def default_beam_grid() -> dict[Coordinate, list[Beam]]: return { Coordinate(x=0, y=0): [ Beam( x_direction=1, y_direction=0, ) ] } def default_energized_grid() -> dict[Coordinate, set[Beam]]: return {Coordinate(x=0, y=0): set()} @dataclass class Grid: grid: dict[Coordinate, str] = field(default_factory=dict) beam_grid: dict[Coordinate, list[Beam]] = field(default_factory=default_beam_grid) energized_grid: dict[Coordinate, set[Beam]] = field( default_factory=default_energized_grid ) max_x: int = 0 max_y: int = 0 def add_coordinate(self, *, x: int, y: int, character: str) -> None: self.max_x = max(self.max_x, x + 1) self.max_y = max(self.max_y, y + 1) self.grid[Coordinate(x=x, y=y)] = character def start(self, coordinate: Coordinate, beam: Beam) -> None: self.beam_grid = {coordinate: [beam]} self.energized_grid = {coordinate: set()} def step(self) -> None: new_beam_grid: dict[Coordinate, list[Beam]] = {} for coordinate, beams in self.beam_grid.items(): if not beams: # No beams left in coordinate, just skip continue if coordinate not in self.grid: # Coordinate outside of the grid? # Beam has left the grid and can no longer return continue for beam in beams: if self.grid[coordinate] == ".": # Move the beam in the direction it was going new_coordinate = Coordinate( x=coordinate.x + beam.x_direction, y=coordinate.y + beam.y_direction, ) if new_coordinate not in new_beam_grid: new_beam_grid[new_coordinate] = [] new_beam_grid[new_coordinate].append(beam) elif self.grid[coordinate] == "|": if beam.y_direction in [-1, 1]: # Keep the beam moving new_coordinate = Coordinate( x=coordinate.x + beam.x_direction, y=coordinate.y + beam.y_direction, ) if new_coordinate not in new_beam_grid: new_beam_grid[new_coordinate] = [] new_beam_grid[new_coordinate].append(beam) elif beam.x_direction in [-1, 1]: # Split the beam to go up new_coordinate_up = Coordinate( x=coordinate.x, y=coordinate.y - 1, ) if new_coordinate_up not in new_beam_grid: new_beam_grid[new_coordinate_up] = [] new_beam_grid[new_coordinate_up].append( Beam( x_direction=0, y_direction=-1, ) ) # Split the beam to go down new_coordinate_down = Coordinate( x=coordinate.x, y=coordinate.y + 1, ) if new_coordinate_down not in new_beam_grid: new_beam_grid[new_coordinate_down] = [] new_beam_grid[new_coordinate_down].append( Beam( x_direction=0, y_direction=1, ) ) elif self.grid[coordinate] == "-": if beam.x_direction in [-1, 1]: # Keep the beam moving new_coordinate = Coordinate( x=coordinate.x + beam.x_direction, y=coordinate.y + beam.y_direction, ) if new_coordinate not in new_beam_grid: new_beam_grid[new_coordinate] = [] new_beam_grid[new_coordinate].append(beam) elif beam.y_direction in [-1, 1]: # Split the beam to go left new_coordinate_up = Coordinate( x=coordinate.x - 1, y=coordinate.y, ) if new_coordinate_up not in new_beam_grid: new_beam_grid[new_coordinate_up] = [] new_beam_grid[new_coordinate_up].append( Beam( x_direction=-1, y_direction=0, ) ) # Split the beam to go right new_coordinate_down = Coordinate( x=coordinate.x + 1, y=coordinate.y, ) if new_coordinate_down not in new_beam_grid: new_beam_grid[new_coordinate_down] = [] new_beam_grid[new_coordinate_down].append( Beam( x_direction=1, y_direction=0, ) ) elif self.grid[coordinate] == "/": if beam.x_direction == 1: # Move the beam up new_coordinate = Coordinate( x=coordinate.x, y=coordinate.y - 1, ) if new_coordinate not in new_beam_grid: new_beam_grid[new_coordinate] = [] new_beam_grid[new_coordinate].append( Beam( x_direction=0, y_direction=-1, ) ) elif beam.x_direction == -1: # Move the beam down new_coordinate = Coordinate( x=coordinate.x, y=coordinate.y + 1, ) if new_coordinate not in new_beam_grid: new_beam_grid[new_coordinate] = [] new_beam_grid[new_coordinate].append( Beam( x_direction=0, y_direction=1, ) ) elif beam.y_direction == 1: # Move the beam left new_coordinate = Coordinate( x=coordinate.x - 1, y=coordinate.y, ) if new_coordinate not in new_beam_grid: new_beam_grid[new_coordinate] = [] new_beam_grid[new_coordinate].append( Beam( x_direction=-1, y_direction=0, ) ) elif beam.y_direction == -1: # Move the beam right new_coordinate = Coordinate( x=coordinate.x + 1, y=coordinate.y, ) if new_coordinate not in new_beam_grid: new_beam_grid[new_coordinate] = [] new_beam_grid[new_coordinate].append( Beam( x_direction=1, y_direction=0, ) ) elif self.grid[coordinate] == "\\": if beam.x_direction == 1: # Move the beam down new_coordinate = Coordinate( x=coordinate.x, y=coordinate.y + 1, ) if new_coordinate not in new_beam_grid: new_beam_grid[new_coordinate] = [] new_beam_grid[new_coordinate].append( Beam( x_direction=0, y_direction=1, ) ) elif beam.x_direction == -1: # Move the beam up new_coordinate = Coordinate( x=coordinate.x, y=coordinate.y - 1, ) if new_coordinate not in new_beam_grid: new_beam_grid[new_coordinate] = [] new_beam_grid[new_coordinate].append( Beam( x_direction=0, y_direction=-1, ) ) elif beam.y_direction == 1: # Move the beam right new_coordinate = Coordinate( x=coordinate.x + 1, y=coordinate.y, ) if new_coordinate not in new_beam_grid: new_beam_grid[new_coordinate] = [] new_beam_grid[new_coordinate].append( Beam( x_direction=1, y_direction=0, ) ) elif beam.y_direction == -1: # Move the beam left new_coordinate = Coordinate( x=coordinate.x - 1, y=coordinate.y, ) if new_coordinate not in new_beam_grid: new_beam_grid[new_coordinate] = [] new_beam_grid[new_coordinate].append( Beam( x_direction=-1, y_direction=0, ) ) # If we've seen this beam at this coordinate in the energized grid, remove it cleaned_beam_grid: dict[Coordinate, list[Beam]] = {} for coordinate, beams in new_beam_grid.items(): cleaned_beam_grid[coordinate] = [] if coordinate in self.energized_grid: for beam in beams: if beam in self.energized_grid[coordinate]: pass else: self.energized_grid[coordinate].add(beam) cleaned_beam_grid[coordinate].append(beam) elif coordinate in self.grid: if coordinate not in self.energized_grid: self.energized_grid[coordinate] = set() self.energized_grid[coordinate].update(set(beams)) cleaned_beam_grid[coordinate] = beams self.beam_grid = cleaned_beam_grid def count_energized(self) -> int: count = 0 for coordinate in self.grid: if coordinate in self.energized_grid: count += 1 return count def runner(document: list[str]) -> int: grid = Grid() max_x = len(document[0]) max_y = len(document) for y_index, line in enumerate(document): for x_index, character in enumerate(line): grid.add_coordinate(x=x_index, y=y_index, character=character) max_energized = 0 # Start from left moving right for y in range(max_y): grid.start(Coordinate(x=0, y=y), Beam(x_direction=1, y_direction=0)) while grid.beam_grid: grid.step() max_energized = max(max_energized, grid.count_energized()) # Start from right moving left for y in range(max_y): grid.start(Coordinate(x=max_x, y=y), Beam(x_direction=-1, y_direction=0)) while grid.beam_grid: grid.step() max_energized = max(max_energized, grid.count_energized()) # Start from top moving down for x in range(max_x): grid.start(Coordinate(x=x, y=0), Beam(x_direction=0, y_direction=1)) while grid.beam_grid: grid.step() max_energized = max(max_energized, grid.count_energized()) # Start from bottom moving up for x in range(max_x): grid.start(Coordinate(x=x, y=max_y), Beam(x_direction=0, y_direction=-1)) while grid.beam_grid: grid.step() max_energized = max(max_energized, grid.count_energized()) return max_energized @pytest.mark.parametrize( "filename,output", [ ("example-1.txt", 51), ("example-9.txt", 8231), # Very slow ], ) def test_runner(filename: str, output: int) -> None: with open(Path(__file__).with_name(filename)) as file: result = runner(file.read().splitlines()) assert result == output

Answer: 8,231

DayPart 1 TimePart 1 RankPart 2 TimePart 2 Rank
1604:11:449,00704:33:048,641

This was pretty rough...

Not because the challenge was all that difficult. This was a really fun challenge!!

No, the difficult part was finding bugs in my code. I have a line that looks like this:

for coordinate, beams in self.beam_grid.items():

And later in the code, within the same scope, I reused the coordinate variable instead of creating a new_coordinate variable. This meant that when I had multiple beams from for coordinate, beams in self.beam_grid.items():, I was modifying the starting coordinate for any beam other than the first. This would cause some beams to move diagonally when they overlapped with another beam.

I spent the vast, vast majority of my time trying to debug my code. I ended up creating a visualization for the grid and watching the full input map iterate step by step until I saw a discrepancy between steps (when a beam moved diagonally). Once I saw the behavior, I was able to recreate this with some test examples, and I was able to find the bug pretty quickly after that.

Some obvious ways I could have avoided this:

  • Keep my scopes small in functions by splitting that big method up into smaller methods
  • Better linting tools to alert me to variable redefinition. Notably, if I used TypeScript instead of Python, I would've caught this issue immediately

I was so happy when I finally finished Part 1, I took a 15 minute breather before attempting Part 2. Thankfully, Part 2 was super simple!