advent-of-code 10 min read

[Day 16] The Floor Will Be Lava // Advent of Code 2023 (Python)

Solving Advent of Code 2023 Day 16 in Python: simulating a light beam through mirrors and splitters with BFS, then trying every edge entry point for Part 2.

The Floor Will Be Lava

The Floor Will Be Lava

Advent of Code
With the beam of light completely focused somewhere, the reindeer leads you deeper still into the Lava Production…adventofcode.com


Part One

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 heatto melt the rock in the cavern.

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

.|…\…
|.-.\…
…|-…
…|.
…
…\
…/.\\..
.-.-/..|..
.|…-|.\
..//.|…

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 upwardin 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:

|<<<\…
|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 (.):

######…
.#…#…
.#…#####
.#…##…
.#…##…
.#…##…
.#..####..
########..
.#######..
.#…#.#..

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?


Seems like we have to simular a light beam entering the grid and then interest the beam with empty space . mirrors / \ and splitters | -

Let’s see the rules and maybe draw a visualization in our whiteboard to understand the test case better:

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 upwardin 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!

Here’s how the light beam travels!

Here’s how the light beam travels!

So, now all the tiles that our beam will pass through will be energized # v/s not . meaning our input grid will transform into something like:

Then all we need to do is, just count the number of energized # tiles and that’s our answer!

💡See, breaking down the problem along with some visualizations always help! Once you practise enough, you will be able to form with visualisations in your mind and then break down the problem into smaller chunks!

Let’s do the first chunk, where we will try to simulate the light beam travelling from top left (0, 0) and then do the traversal. One important point we can already observe from our viz above is that light beam can get trapped in cycles such as this subsection here:

So, we will need to be mindful of these cycles!

Let’s write our base test case first?

I plan to structure my code like this!

I plan to structure my code like this!

I think we should do a BFS (Breadth first search) to explore all paths in direction of beam as it will allow us to traverse multiple paths in case of beam split or exit early in case beam leaves the grid! We would need some kind of a cycle detector to ensure the same part of beam is not reflecting across again and again!

First, let’s define some helpers:

Direction to track directions, Tile to track type of tile, and LightBeamNode to just track the idx, last direction and next direction.

Using these, we can implement our super messy code for simulate_light_beam :

Truly a spagheti code! Had to pop VS Code on vertical montior to snap this one!

Truly a spagheti code! Had to pop VS Code on vertical montior to snap this one!

Let’s debug and walkover this one…well before that, for my own and my code interpretor sanity, I have decided to rename the \ and / mirrors to L and R respectively as backslash \ is a reserved character in Python and I dont want to deal with handling that everywhere!

So, my input_str is now:

input_str = """.|...L....
|.-.L.....
.....|-...
........|.
..........
.........L
....R.LL..
.-.-R..|..
.|....-|.L
..RR.|...."""

And similarly, my Tile enum is now:

class Tile(Enum):
    EMPTY = "."
    VERTICAL_SPLITTER = "|"
    HORIZONTAL_SPLITTER = "-"
    RIGHT_TILTED_MIRROR = "R"
    LEFT_TILTED_MIRROR = "L"

Rest of the functionality remains the same!

Continue to debugging, found that my Directions values were wrong 😅

class Direction(Enum):
    UP = (-1, 0)
    DOWN = (1, 0)
    LEFT = (0, -1)
    RIGHT = (0, 1)

Found another bug here as we can’t just check if a title is visited before, as our light beam can visit a tile from 4 different directions, so in order to not block our light beam from discovering new tiles, we will have to hash a tuple of (tile_idx, last_dir)

Something like this:

Perfect! Now once our queue is empty, we should have a set of energized_tiles which may have repeated values for tile_idx but with different directions! All we need to do now is to iterate over tile_idx and update our grid with # and return it!

Looks good!

Looks good!

Now, let’s just count the number of # cells and we should be able to test the base case…

Executing our test function…we have the correct solution!

[Running] python3 "/Users/jmalik/github/advent-of-code-2023/day_16/the_floor_will_be_lava.py"
✅ test_get_energized_tiles passed

[Done] exited with code=0 in 0.268 seconds

This seems pretty efficient as we are just tracing light beam, node by node over each tile and we are practising early returns in case of out of bounds or if we have seen the same tile before for same light beam node!

Let’s load up our puzzle input and test!

❗️Remember, we need to replace \ and / with L and R in our puzzle input as well! VS Code find and replace for rescue here!

Done!

Done!

Let’s run this!

[Running] python3 "/Users/jmalik/github/advent-of-code-2023/day_16/the_floor_will_be_lava.py"
❗️ Number of energized tiles: XXXX

[Done] exited with code=0 in 0.234 seconds

We got an answer in just 0.234 seconds! Let’s put this in the puzzle answer box and ….

😎

😎

Let’s debug the code and see how our grid looks like post light beam simulation:

This is beautiful! I have a strong urge to animate this! :D

This is beautiful! I have a strong urge to animate this! :D

Here’s our code till now, it’s a bit spagheti code, I will fix it in final form for Part 2:

# Day 16 - The Floor will be Lava

from enum import Enum
from collections import deque

class Direction(Enum):
    UP = (-1, 0)
    DOWN = (1, 0)
    LEFT = (0, -1)
    RIGHT = (0, 1)

class Tile(Enum):
    EMPTY = "."
    VERTICAL_SPLITTER = "|"
    HORIZONTAL_SPLITTER = "-"
    RIGHT_TILTED_MIRROR = "R"
    LEFT_TILTED_MIRROR = "L"

class LightBeamNode:
    def __init__(self, idx, last_dir, next_dir):
        self.idx = idx
        self.last_dir = last_dir
        self.next_dir = next_dir

def get_next_tile_idx(tile_idx, direction):
    x, y = tile_idx
    dx, dy = direction.value
    return (x + dx, y + dy)

def simulate_light_beam(grid, start_pos=(0,0)):
    energized_tiles = set() 
    queue = deque()
    # init position
    node = LightBeamNode(start_pos, Direction.LEFT, Direction.RIGHT)
    queue.append(node)

    while queue:
        node = queue.popleft()

        # check if node is out of bounds for grid
        if node.idx[0] < 0 or node.idx[0] >= len(grid[0]) or node.idx[1] < 0 or node.idx[1] >= len(grid):
            continue

        if (node.idx, node.last_dir) in energized_tiles:
            continue
        else:
            energized_tiles.add((node.idx, node.last_dir))

        tile = grid[node.idx[0]][node.idx[1]]
        
        if tile == Tile.EMPTY.value:
            queue.append(LightBeamNode(get_next_tile_idx(node.idx, node.next_dir), node.last_dir, node.next_dir))

        elif tile == Tile.VERTICAL_SPLITTER.value:
            if node.last_dir in [Direction.UP, Direction.DOWN]:
                queue.append(LightBeamNode(get_next_tile_idx(node.idx, node.next_dir), node.last_dir, node.next_dir))
            else:
                queue.append(LightBeamNode(get_next_tile_idx(node.idx, Direction.UP), Direction.DOWN, Direction.UP))
                queue.append(LightBeamNode(get_next_tile_idx(node.idx, Direction.DOWN), Direction.UP, Direction.DOWN))

        elif tile == Tile.HORIZONTAL_SPLITTER.value:
            if node.last_dir in [Direction.LEFT, Direction.RIGHT]:
                queue.append(LightBeamNode(get_next_tile_idx(node.idx, node.next_dir), node.last_dir, node.next_dir))
            else:
                queue.append(LightBeamNode(get_next_tile_idx(node.idx, Direction.LEFT), Direction.RIGHT, Direction.LEFT))
                queue.append(LightBeamNode(get_next_tile_idx(node.idx, Direction.RIGHT), Direction.LEFT, Direction.RIGHT))

        elif tile == Tile.RIGHT_TILTED_MIRROR.value:
            if node.last_dir == Direction.UP:
                queue.append(LightBeamNode(get_next_tile_idx(node.idx, Direction.LEFT), Direction.RIGHT, Direction.LEFT))
            elif node.last_dir == Direction.RIGHT:
                queue.append(LightBeamNode(get_next_tile_idx(node.idx, Direction.DOWN), Direction.UP, Direction.DOWN))
            elif node.last_dir == Direction.DOWN:
                queue.append(LightBeamNode(get_next_tile_idx(node.idx, Direction.RIGHT), Direction.LEFT, Direction.RIGHT))
            elif node.last_dir == Direction.LEFT:
                queue.append(LightBeamNode(get_next_tile_idx(node.idx, Direction.UP), Direction.DOWN, Direction.UP))

        elif tile == Tile.LEFT_TILTED_MIRROR.value:
            if node.last_dir == Direction.UP:
                queue.append(LightBeamNode(get_next_tile_idx(node.idx, Direction.RIGHT), Direction.LEFT, Direction.RIGHT))
            elif node.last_dir == Direction.RIGHT:
                queue.append(LightBeamNode(get_next_tile_idx(node.idx, Direction.UP), Direction.DOWN, Direction.UP))
            elif node.last_dir == Direction.DOWN:
                queue.append(LightBeamNode(get_next_tile_idx(node.idx, Direction.LEFT), Direction.RIGHT, Direction.LEFT))
            elif node.last_dir == Direction.LEFT:
                queue.append(LightBeamNode(get_next_tile_idx(node.idx, Direction.DOWN), Direction.UP, Direction.DOWN))
    
    for tile, _ in energized_tiles:
        grid[tile[0]][tile[1]] = "#"

    return grid

def get_num_of_energized_tiles(grid):
    count = 0
    for row in grid:
        for tile in row:
            if tile == "#":
                count += 1
    return count

def parse_input(input_str):
    grid = []
    for line in input_str.splitlines():
        grid.append(list(line))
    return grid

def part_one():
    with open("day_16/input.txt", "r") as f:
        input_str = f.read()
    grid = parse_input(input_str)
    updated_grid = simulate_light_beam(grid)
    num_of_energized_tiles = get_num_of_energized_tiles(updated_grid)
    print(f"❗️ Number of energized tiles: {num_of_energized_tiles}")

def test_get_energized_tiles():
    input_str = """.|...L....
|.-.L.....
.....|-...
........|.
..........
.........L
....R.LL..
.-.-R..|..
.|....-|.L
..RR.|...."""
    
    grid = parse_input(input_str)
    updated_grid = simulate_light_beam(grid)
    num_of_energized_tiles = get_num_of_energized_tiles(updated_grid)
    assert num_of_energized_tiles == 46, f"Expected 46, got {num_of_energized_tiles}"
    print("✅ test_get_energized_tiles passed")

if __name__ == "__main__":
    # test_get_energized_tiles()
    part_one()

Part Two

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 tileand 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:

.|<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:

.#####…
.#.#.#…
.#.#.#####
.#.#.##…
.#.#.##…
.#.#.##…
.#.#####..
########..
.#######..
.#…#.#..

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


So…our reindeer friend has just shared that:

A collection of buttons lets you align the contraption so that the beam enters from any edge tileand 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.)

Now, as per the question, 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.

I am just proud of my part 1 solution as my intuition told me we need to keep a track of prev_direction and next_direction. That part of the code will prove critical here!

Well, this does explode our search space by quite a lot, but given the puzzle input is a 110 x 110 grid, we have 110 + 110 + 108 + 108 = 436 possible configurations for ingress points + 4 extra direction option per corner leading to 440 combinations!

Looking at how, we were able to produce Part 1 answer in just 0.234 seconds I am just inclined to run all 440 combinations and just pick the max value of all outputs!

We will need to modify our
def simulate_light_beam(grid, start_pos=(0,0)) signature to also accept last_dir, next_dir. Let’s do that:

I have set default values to ensure part_one() doesn’t break!

Now, let’s create a new function to basically call those 440 combinations?

This utilizes a helper function process_tile that basically just calls simulate_light_beam and then append get_num_of_energized_tiles() output to the record:

We just return max(energized_tiles_record) at the end!

Let’s write a new test case to test this brute force approach for given test case:

Running this one:

[Running] python3 "/Users/jmalik/github/advent-of-code-2023/day_16/the_floor_will_be_lava.py"
✅ test_get_max_energized_tiles passed

[Done] exited with code=0 in 0.214 seconds

Not a bad runtime as it would have tested 10+10+8+8=36 combinations for test input!

Let’s load up our puzzle input and run this one 🤞

And….

[Running] python3 "/Users/jmalik/github/advent-of-code-2023/day_16/the_floor_will_be_lava.py"
❗️❗️ Number of energized tiles: XXXX

[Done] exited with code=0 in 6.619 seconds

We get an answer in 6.619 seconds not great, not terrible! Let’s check if this is correct one?

And… 🙌

⭐️ ⭐️

⭐️ ⭐️

Here’s out final code:

# Day 16 - The Floor will be Lava

from enum import Enum
from collections import deque

class Direction(Enum):
    UP = (-1, 0)
    DOWN = (1, 0)
    LEFT = (0, -1)
    RIGHT = (0, 1)

class Tile(Enum):
    EMPTY = "."
    VERTICAL_SPLITTER = "|"
    HORIZONTAL_SPLITTER = "-"
    RIGHT_TILTED_MIRROR = "R"
    LEFT_TILTED_MIRROR = "L"

class LightBeamNode:
    def __init__(self, idx, last_dir, next_dir):
        self.idx = idx
        self.last_dir = last_dir
        self.next_dir = next_dir

def get_next_tile_idx(tile_idx, direction):
    dx, dy = direction.value
    return (tile_idx[0] + dx, tile_idx[1] + dy)

def simulate_light_beam(
    grid, start_pos=(0, 0), last_dir=Direction.LEFT, next_dir=Direction.RIGHT
):
    energized_tiles = set()
    queue = deque()

    # Initialize the light beam's starting position and direction.
    node = LightBeamNode(start_pos, last_dir, next_dir)
    queue.append(node)

    while queue:
        # Process the next node in the queue.
        node = queue.popleft()

        # Skip processing if the node is out of the grid's bounds.
        if (
            node.idx[0] < 0
            or node.idx[0] >= len(grid[0])
            or node.idx[1] < 0
            or node.idx[1] >= len(grid)
        ):
            continue

        # Skip processing if the tile in this direction has already been energized.
        if (node.idx, node.last_dir) in energized_tiles:
            continue
        else:
            energized_tiles.add((node.idx, node.last_dir))

        # Get the tile at the current node's position.
        tile = grid[node.idx[0]][node.idx[1]]

        # If the tile is empty, propagate the light beam in the same direction.
        if tile == Tile.EMPTY.value:
            queue.append(
                LightBeamNode(
                    get_next_tile_idx(node.idx, node.next_dir),
                    node.last_dir,
                    node.next_dir,
                )
            )

        # Handle interaction with vertical splitters.
        elif tile == Tile.VERTICAL_SPLITTER.value:
            # Continue in the same direction if moving vertically.
            if node.last_dir in [Direction.UP, Direction.DOWN]:
                queue.append(
                    LightBeamNode(
                        get_next_tile_idx(node.idx, node.next_dir),
                        node.last_dir,
                        node.next_dir,
                    )
                )

            # Split the beam in up and down directions if moving horizontally.
            else:
                queue.append(
                    LightBeamNode(
                        get_next_tile_idx(node.idx, Direction.UP),
                        Direction.DOWN,
                        Direction.UP,
                    )
                )
                queue.append(
                    LightBeamNode(
                        get_next_tile_idx(node.idx, Direction.DOWN),
                        Direction.UP,
                        Direction.DOWN,
                    )
                )

        # Handle interaction with horizontal splitters.
        elif tile == Tile.HORIZONTAL_SPLITTER.value:
            # Continue in the same direction if moving horizontally.
            if node.last_dir in [Direction.LEFT, Direction.RIGHT]:
                queue.append(
                    LightBeamNode(
                        get_next_tile_idx(node.idx, node.next_dir),
                        node.last_dir,
                        node.next_dir,
                    )
                )

            # Split the beam in left and right directions if moving vertically.
            else:
                queue.append(
                    LightBeamNode(
                        get_next_tile_idx(node.idx, Direction.LEFT),
                        Direction.RIGHT,
                        Direction.LEFT,
                    )
                )
                queue.append(
                    LightBeamNode(
                        get_next_tile_idx(node.idx, Direction.RIGHT),
                        Direction.LEFT,
                        Direction.RIGHT,
                    )
                )

        # Handle interaction with right-tilted mirrors.
        elif tile == Tile.RIGHT_TILTED_MIRROR.value:
            # Change direction based on the mirror's orientation and the beam's current direction.
            if node.last_dir == Direction.UP:
                queue.append(
                    LightBeamNode(
                        get_next_tile_idx(node.idx, Direction.LEFT),
                        Direction.RIGHT,
                        Direction.LEFT,
                    )
                )
            elif node.last_dir == Direction.RIGHT:
                queue.append(
                    LightBeamNode(
                        get_next_tile_idx(node.idx, Direction.DOWN),
                        Direction.UP,
                        Direction.DOWN,
                    )
                )
            elif node.last_dir == Direction.DOWN:
                queue.append(
                    LightBeamNode(
                        get_next_tile_idx(node.idx, Direction.RIGHT),
                        Direction.LEFT,
                        Direction.RIGHT,
                    )
                )
            elif node.last_dir == Direction.LEFT:
                queue.append(
                    LightBeamNode(
                        get_next_tile_idx(node.idx, Direction.UP),
                        Direction.DOWN,
                        Direction.UP,
                    )
                )

        # Handle interaction with left-tilted mirrors.
        elif tile == Tile.LEFT_TILTED_MIRROR.value:
            # Change direction based on the mirror's orientation and the beam's current direction.
            if node.last_dir == Direction.UP:
                queue.append(
                    LightBeamNode(
                        get_next_tile_idx(node.idx, Direction.RIGHT),
                        Direction.LEFT,
                        Direction.RIGHT,
                    )
                )
            elif node.last_dir == Direction.RIGHT:
                queue.append(
                    LightBeamNode(
                        get_next_tile_idx(node.idx, Direction.UP),
                        Direction.DOWN,
                        Direction.UP,
                    )
                )
            elif node.last_dir == Direction.DOWN:
                queue.append(
                    LightBeamNode(
                        get_next_tile_idx(node.idx, Direction.LEFT),
                        Direction.RIGHT,
                        Direction.LEFT,
                    )
                )
            elif node.last_dir == Direction.LEFT:
                queue.append(
                    LightBeamNode(
                        get_next_tile_idx(node.idx, Direction.DOWN),
                        Direction.UP,
                        Direction.DOWN,
                    )
                )

    # Create a new grid representation showing the energized tiles.
    new_grid = [
        [Tile.EMPTY.value for _ in range(len(grid[0]))] for _ in range(len(grid))
    ]

    # Mark energized tiles in the new grid.
    for tile, _ in energized_tiles:
        new_grid[tile[0]][tile[1]] = "#"

    return new_grid

def process_tile(grid, tile, last_dir, next_dir, record):
    updated_grid = simulate_light_beam(
        grid, start_pos=tile, last_dir=last_dir, next_dir=next_dir
    )
    record.append(sum(tile == "#" for row in updated_grid for tile in row))

def get_max_energized_tiles(grid):
    energized_tiles_record = []

    grid_corners = [
        (0, 0),
        (0, len(grid[0]) - 1),
        (len(grid) - 1, 0),
        (len(grid) - 1, len(grid[0]) - 1),
    ]

    directions = [Direction.LEFT, Direction.UP, Direction.RIGHT, Direction.DOWN]

    # Process corners
    for corner in grid_corners:
        for i in range(2):
            process_tile(
                grid,
                corner,
                directions[i * 2],
                directions[i * 2 + 1],
                energized_tiles_record,
            )

    # Process edges without corners
    for i in range(1, len(grid[0]) - 1):  # Top and bottom rows
        process_tile(grid, (0, i), Direction.UP, Direction.DOWN, energized_tiles_record)
        process_tile(
            grid,
            (len(grid) - 1, i),
            Direction.DOWN,
            Direction.UP,
            energized_tiles_record,
        )

    for i in range(1, len(grid) - 1):  # Left and right columns
        process_tile(
            grid, (i, 0), Direction.LEFT, Direction.RIGHT, energized_tiles_record
        )
        process_tile(
            grid,
            (i, len(grid[0]) - 1),
            Direction.RIGHT,
            Direction.LEFT,
            energized_tiles_record,
        )

    return max(energized_tiles_record)

def parse_input(input_str):
    grid = []
    for line in input_str.splitlines():
        grid.append(list(line))
    return grid

def part_one():
    with open("day_16/input.txt", "r") as f:
        input_str = f.read()
    grid = parse_input(input_str)
    updated_grid = simulate_light_beam(grid)
    num_of_energized_tiles = sum(tile == "#" for row in updated_grid for tile in row)
    print(f"❗️ Number of energized tiles: {num_of_energized_tiles}")

def part_two():
    with open("day_16/input.txt", "r") as f:
        input_str = f.read()
    grid = parse_input(input_str)
    maximum_energized_tiles = get_max_energized_tiles(grid)
    print(f"❗️❗️ Number of energized tiles: {maximum_energized_tiles}")

def test_get_max_energized_tiles():
    input_str = """.|...L....
|.-.L.....
.....|-...
........|.
..........
.........L
....R.LL..
.-.-R..|..
.|....-|.L
..RR.|...."""
    grid = parse_input(input_str)
    maximum_energized_tiles = get_max_energized_tiles(grid)
    assert maximum_energized_tiles == 51, f"Expected 51, got {maximum_energized_tiles}"
    print("✅ test_get_max_energized_tiles passed")

def test_get_energized_tiles():
    input_str = """.|...L....
|.-.L.....
.....|-...
........|.
..........
.........L
....R.LL..
.-.-R..|..
.|....-|.L
..RR.|...."""

    grid = parse_input(input_str)
    updated_grid = simulate_light_beam(grid)
    num_of_energized_tiles = sum(tile == "#" for row in updated_grid for tile in row)
    assert num_of_energized_tiles == 46, f"Expected 46, got {num_of_energized_tiles}"
    print("✅ test_get_energized_tiles passed")

if __name__ == "__main__":
    test_get_energized_tiles()
    part_one()

    test_get_max_energized_tiles()
    part_two()

🎅 I have not fixed extreme nesting in simulate_light_beam , if you have made it till here in this post, and are passionate for clean python code, please raise a PR in my project and I will gift you something in spirit of Merry Christmas! 🎄


p.s: I am deliberating not adding answers here so you can actually spend some time trying to understand how we solved the problem!

Feel free to checkout code on my Github repo and try to set it up and run on your machine 👇

GitHub - jatinkrmalik/advent-of-code-2023: Well, here’s my attempt to solve all 25 problems of…
Well, here’s my attempt to solve all 25 problems of Advent of Code 2023 with Python! 🎄 - GitHub …github.com

Leave a comment

Keep reading

advent-of-code 2 min

🎄Solve Advent of Code 2024 With Me!

Announcing a spoiler-free YouTube video series for Advent of Code 2024 that focuses on how to analyze and approach each puzzle, not just the answers.