Sean McLemon | Advent of Code

Home | Czech | Blog | GitHub | Advent Of Code | Notes


2020-12-11 - Seating System

(original .ipynb)

Day 11 puzzle input is a grid (mine is here) representing a seating layout - where "L" indicates an empty seat, "#" indicates an occupied seat and "." indicating no seat whatsoever. There are some rules given as to when a seat changes from occupied to empty, and vice versa over a given time-step. Part 1 applying the rules to the grid until no changes occur and finding the number of occupied seats at that steady state. Part 2 involves doing the same with some small modifications to the rules.

WARNING: I produce an animated gif showing the seats getting filled and emptied over time, it turns out that this results in some quite vivid flashing imagery. If you have epilepsy or anything else that may be triggered by flashing, you may want to avoid checking this solution out.

puzzle_input_str = open("puzzle_input/day11.txt", "r").read()

test_input_str = """L.LL.LL.LL
LLLLLLL.LL
L.L.L..L..
LLLL.LL.LL
L.LL.LL.LL
L.LLLLL.LL
..L.L.....
LLLLLLLLLL
L.LLLLLL.L
L.LLLLL.LL"""

def is_occupied(s):
    return s == "#"

def is_empty(s):
    return s == "L"

def is_floor(s):
    return s == "."   

def look(grid, r, c, dr, dc, distance):
    r_max = len(grid) - 1    
    c_max = len(grid[r]) - 1
    
    r += dr
    c += dc
    while c >= 0 and c <= c_max and r >= 0 and r <= r_max and distance > 0:
        seat = grid[r][c]
        
        if is_occupied(seat):
            return True
        elif is_empty(seat):
            return False
        
        r += dr
        c += dc
        distance -= 1
        
    return False


def look_up(grid, r, c, distance):
    return look(grid, r, c, -1, 0, distance)

def look_down(grid, r, c, distance):
    return look(grid, r, c, 1, 0, distance)

def look_left(grid, r, c, distance):
    return look(grid, r, c, 0, -1, distance)

def look_right(grid, r, c, distance):
    return look(grid, r, c, 0, 1, distance)

def look_up_left(grid, r, c, distance):
    return look(grid, r, c, -1, -1, distance)

def look_up_right(grid, r, c, distance):
    return look(grid, r, c, -1, 1, distance)

def look_down_left(grid, r, c, distance):
    return look(grid, r, c, 1, -1, distance)

def look_down_right(grid, r, c, distance):
    return look(grid, r, c, 1, 1, distance)

def neighbouring_occupied_seats(grid, r, c, max_distance):
    occupied = [
       look_up(grid, r, c, max_distance), 
       look_down(grid, r, c, max_distance),
       look_left(grid, r, c, max_distance), 
       look_right(grid, r, c, max_distance), 
       look_up_left(grid, r, c, max_distance), 
       look_up_right(grid, r, c, max_distance),
       look_down_left(grid, r, c, max_distance),
       look_down_right(grid, r, c, max_distance)
    ]
    
    return len([t for t in occupied if t]) # TODO: nicer way to count "True" in list?

def should_empty_become_occupied(grid, r, c, max_distance):
    return neighbouring_occupied_seats(grid, r, c, max_distance) == 0

def should_occupied_become_empty(grid, r, c, max_distance, vacate_seat_threshold):
    return neighbouring_occupied_seats(grid, r, c, max_distance) >= vacate_seat_threshold   
    
def step_grid(grid, max_distance, vacate_seat_threshold):
    new_grid = []    
    
    for r, row in enumerate(grid):
        new_row = []
        
        for c, col in enumerate(row):
            if is_empty(col) and should_empty_become_occupied(grid, r, c, max_distance):
                new_row.append("#")
                
            elif is_occupied(col) and should_occupied_become_empty(grid, r, c, max_distance, vacate_seat_threshold):
                new_row.append("L")
                
            else:
                new_row.append(col)
            
        new_grid.append(new_row)
    
    return new_grid  

def count_occupied(grid):
    occupied = 0
    for row in grid:
        for col in row:
            if is_occupied(col):
                occupied += 1
    return occupied

# this 'advent' is something I put together a few weeks back, it's obviously not on pypi
# or anything, but the source code is here: https://github.com/smcl/advent
from advent import grid_animated_render_frame, grid_animated_combine

def solve(input_str, max_distance=1, vacate_seat_threshold=4, filename=None):
    grid = [
        [s for s in line]
        for line 
        in input_str.split("\n")
    ]
    
    generations = 0
    palette = {
        ".": "white",
        "#": "red",
        "L": "pink"
    }
    image_frames = [grid_animated_render_frame(grid, palette)] if filename else []
    
    grid_has_stabilised = False
    while not grid_has_stabilised:
        new_grid = step_grid(grid, max_distance, vacate_seat_threshold)
        grid_has_stabilised = new_grid == grid
        grid = new_grid
        if filename:
            image_frames.append(grid_animated_render_frame(grid, palette))
        
    if filename:
        grid_animated_combine(image_frames, filename, show=True)

    return count_occupied(grid)

assert 37 == solve(test_input_str)
print(solve(puzzle_input_str, filename="day-11-part1.gif"))
2275
import math
assert 26 == solve(test_input_str, math.inf, 5)
print(solve(puzzle_input_str, math.inf, 5, filename="day-11-part2.gif"))
2121