How to solve Advent of Code 2022 - Day 14 with Python

How to solve Advent of Code 2022 - Day 14 with Python

If you missed any previous days, click here for all my content about that: Advent of Code, if you want to know why you should participate and try to code the solution for yourself, click here: Advent Of Code 2022 – 7 Reasons why you should participate. If you're here to see the solution of Day 14, continue reading ;)

GitHub Repository

https://github.com/GalaxyInfernoCodes/Advent_Of_Code_2022

I will upload all of my solutions there - in the form of Python (or Scala alternatives) notebooks. You have to use your own input for the "input.txt" since everyone gets their own and everyone needs to provide a different solution based on their input.

Day 14 Puzzle

On day 14 of Advent of Code, we had to simulate sand falling. I predict that at this rate, we will be watching paint dry by next week. Anyways, there is rock in the way of the sand, which was described as follows:

498,4 -> 498,6 -> 496,6
503,4 -> 502,4 -> 502,9 -> 494,9

Which means the rocks look like this after you connect those specified corners with straight lines of rock:

  4     5  5
  9     0  0
  4     0  3
0 ......+...
1 ..........
2 ..........
3 ..........
4 ....#...##
5 ....#...#.
6 ..###...#.
7 ........#.
8 ........#.
9 #########.

Parsing the rock

Parsing seemed like 50% of the work here. Maybe even more.

If the following looks a bit confusing... yeah, I know. The issue I faced was that the line can either have increasing indices (498,4 -> 498,6 has an increasing y coordinate), but also decreasing indices (502,9 -> 494,9 has a decreasing x coordinate). So just taking a range from start to finish could end up with an empty range in the decreasing case :/

So I ordered each pair of endpoints by taking the min and max :)

After that I can easily set each rock coordinate in the grid to 1.

def register_rock_tiles(file):
    with open(file, 'r') as f:
        lines = f.readlines()
        lines = [entry.strip() for entry in lines]
    
    grid = np.zeros((1000, 200))
    end_of_rock_field = 0
    for line in lines:
        corners = line.split(' -> ')
        for i in range(len(corners) - 1):
            start_corner, end_corner = corners[i], corners[i+1]
            xs = [int(start_corner.split(',')[0]), int(end_corner.split(',')[0])]
            ys = [int(start_corner.split(',')[1]), int(end_corner.split(',')[1])]
            for x in range(min(xs), max(xs)+1):
                for y in range(min(ys), max(ys)+1):
                    if y > end_of_rock_field:
                        end_of_rock_field = y
                    grid[x, y] = 1
    return grid, end_of_rock_field

Sand falling logic

Magic christmas sand always fall down. Either:

  • straight down
  • left down
  • or right down

After that, it continues to fall down until none of those next 3 spots are free.

If all 3 spots are taken, the sand gives up and just stays where it is. Here's the logic:

def add_sand_particle(grid, end_of_rock_field):
    sand = (500,0)
    while sand[1] <= end_of_rock_field:
        # down
        if grid[sand[0], sand[1]+1] == 0:
            sand = (sand[0], sand[1]+1)
        # left down
        elif grid[sand[0]-1, sand[1]+1] == 0:
            sand = (sand[0]-1, sand[1]+1)
        # right down
        elif grid[sand[0]+1, sand[1]+1] == 0:
            sand = (sand[0]+1, sand[1]+1)
        else:
            return sand
    return (-1, -1)

The starting point is always (500,0). The while loop encodes the condition, that if we pass by the lowest point in the rock field, we're in free fall, so I return (-1, -1).

Part 1

Now we let sand drop one by one into our grid and count the sand_tiles that we add. Sand is also encoded as 1 in the grid, because it will block the path of future sand particles the same way as rock.

def solve1(file):
    grid, end_of_rock_field = register_rock_tiles(file)

    sand_tiles = 0
    new_sand = add_sand_particle(grid, end_of_rock_field)
    while new_sand != (-1, -1):
        grid[new_sand[0], new_sand[1]] = 1
        sand_tiles +=1
        new_sand = add_sand_particle(grid, end_of_rock_field)
    print(sand_tiles)

Part 2

Slight changes to the plan, guys! There is indeed a floor and it is exactly 2 lower than the last rock. So now sand piles up from the floor until it blocks the sand-entry at (500,0).

The changes to the sand falling procedure look like this:

def add_sand_particle2(grid, end_of_rock_field):
    sand = (500,0)
    bottom = end_of_rock_field + 2
    while sand[1]+1 < bottom:
        # down
        if grid[sand[0], sand[1]+1] == 0:
            sand = (sand[0], sand[1]+1)
        # left down
        elif grid[sand[0]-1, sand[1]+1] == 0:
            sand = (sand[0]-1, sand[1]+1)
        # right down
        elif grid[sand[0]+1, sand[1]+1] == 0:
            sand = (sand[0]+1, sand[1]+1)
        else:
            return sand
    return sand
  • bottom was added at end_of_rock_field + 2
  • a bottom check is done: if a sand particle has reached the bottom it can't go anywhere and has reached its final destination so we stop the loop

The final sand particle will be the one blocking the entry so we check that in the main loop. While the entry is still free (grid[500, 0] == 0), we keep adding sand.

def solve2(file):
    grid, end_of_rock_field = register_rock_tiles(file)

    sand_tiles = 0
    new_sand = add_sand_particle2(grid, end_of_rock_field)
    while grid[500, 0] == 0:
        grid[new_sand[0], new_sand[1]] = 1
        sand_tiles += 1
        new_sand = add_sand_particle2(grid, end_of_rock_field)
    print(sand_tiles)

Conclusion

No idea who cleans up all the sand.