Advent of Code 2022 day 24

Use breadth-first search. Wasn’t as painful as I had anticipated. Luckily I had abstracted enough in part 1 so that part 2 didn’t require any change to the underlying logic.

with open("24.txt") as f:
    data = f.read()

def get_board(data):
    return [
        list(line) for line in data.splitlines()
    ]

def get_blizzards(board):
    blizzards = dict()
    for row in range(len(board)):
        for col in range(len(board[0])):
            if board[row][col] in (">", "v", "<", "^"):
                blizzards[(row, col)] = {board[row][col]}
    return blizzards

def get_walls(board):
    return [
        (row, col)
        for row in range(len(board))
        for col in range(len(board[0]))
        if board[row][col] == "#"
    ]

blizzard_movement = {
    ">": (0, 1),
    "v": (1, 0),
    "<": (0, -1),
    "^": (-1, 0),
    " ": (0, 0),
}

def add_movement(x, y):
    a, b = x
    c, d = y
    return (a+c, b+d)

def get_answer(board, blizzards, walls):
    def is_within_board(pos):
        row, col = pos
        if row < 0 or row >= len(board):
            return False
        elif col < 0 or col >= len(board[0]):
            return False
        return True

    def within_board(pos):
        row, col = pos
        if row < 0 or row >= len(board):
            return (row % len(board), col)
        elif col < 0 or col >= len(board[0]):
            return (row, col % len(board[0]))
        return pos

    def move_blizzard(pos, blizzard):
        new_pos = within_board(add_movement(pos, blizzard_movement[blizzard]))
        if new_pos in walls:
            return move_blizzard(new_pos, blizzard)
        return new_pos

    def move_blizzards(blizzard_positions):
        new_blizzards = dict()
        for pos, blizzards in blizzard_positions.items():
            for blizzard in blizzards:
                new_pos = move_blizzard(pos, blizzard)
                if new_pos in new_blizzards:
                    new_blizzards[new_pos].add(blizzard)
                else:
                    new_blizzards[new_pos] = {blizzard}
        return new_blizzards

    def valid_moves(pos):
        for move in ">v<^ ":
            to = add_movement(pos, blizzard_movement[move])
            if (
                    is_within_board(to) and
                    pos not in walls and
                    pos not in blizzards
            ):
                yield to

    def get_paths(paths):
        new_paths = set()
        for pos in paths:
            for new_pos in valid_moves(pos):
                new_paths.add(new_pos)
        return new_paths

    start = (0, 1)
    end = (len(board)-1, len(board[0])-2)
    time = 1

    paths = {(start)}
    going = True
    while going:
        time += 1
        blizzards = move_blizzards(blizzards)
        paths = get_paths(paths)
        going = not any(path == end for path in paths)
    # # Uncomment this for part 2
    # paths = {(end)}
    # returning = True
    # while returning:
    #     time += 1
    #     blizzards = move_blizzards(blizzards)
    #     paths = get_paths(paths)
    #     returning = not any(path == start for path in paths)
    # paths = {(start)}
    # going_back = True
    # while going_back:
    #     time += 1
    #     blizzards = move_blizzards(blizzards)
    #     paths = get_paths(paths)
    #     going_back = not any(path == end for path in paths)
    return time

board = get_board(data)
blizzards = get_blizzards(board)
walls = get_walls(board)

print(get_answer(board, blizzards, walls))