Advent of Code 2022 day 17

Part 1 is now redundant because Part 2 also covers its case. Part 1 also takes an incredibly long time. Part 2 uses two tricks to speed up the calculation:

  1. it only looks at the top part of the grid which is still relevant;

  2. once a loop has been found, it works out how often the loop would occur and ‘skips’ doing them.

Part 1

def create_shape(data: str) -> dict:
    return {
        "height": data.count("\n")+1,
        "width": len(data.split("\n")[0]),
        "shape": data.replace("#", "@")
    }

def grid_top(grid: str) -> int:
    return ((len(grid.rstrip("."))-1) // grid_width) + 1

def display_grid(grid: str) -> None:
    print(
        "\n".join(
            "|"+grid[i*grid_width:(i+1)*grid_width]+"|"
            for i in reversed(range(grid_top(grid)+1))
        ) +
        "\n" +
        "+" + "-"*grid_width + "+"
    )

def initialise_shape(shape: dict, grid: str) -> str:
    end_height = init_x + grid_top(grid)
    start_height = shape["height"] +  end_height
    return (
        grid[:end_height*grid_width] +
        "".join(
            "."*init_y +
            shape["shape"].split("\n")[i] +
            "."*(grid_width - init_y - shape["width"])
            for i in reversed(range(shape["height"]))
        ) +
        grid[start_height*grid_width:]
    )

def can_move_left(grid: str) -> bool:
    return all(
        i % grid_width != 0 and grid[i-1] != "#"
        for i, x in enumerate(grid) if x == "@"
    )

def can_move_right(grid: str) -> bool:
    return all(
        i % grid_width != grid_width-1 and grid[i+1] != "#"
        for i, x in enumerate(grid) if x == "@"
    )

def can_move_down(grid: str) -> bool:
    return all(
        i - grid_width > 0 and grid[i-grid_width] != "#"
        for i, x in enumerate(grid) if x == "@"
    )

def move_left(grid: str) -> str:
    new_grid = grid.replace("@", ".")
    for i, x in enumerate(grid):
        if x == "@":
            new_grid = (
                new_grid[:i-1] + "@" + new_grid[i:]
            )
    return new_grid

def move_right(grid: str) -> str:
    new_grid = grid.replace("@", ".")
    for i, x in enumerate(grid):
        if x == "@":
            new_grid = (
                new_grid[:i+1] + "@" + new_grid[i+2:]
            )
    return new_grid

def move_down(grid: str) -> str:
    new_grid = grid.replace("@", ".")
    for i, x in enumerate(grid):
        if x == "@":
            new_grid = (
                new_grid[:i-grid_width] + "@" + new_grid[i-grid_width+1:]
            )
    return new_grid

def move_left_right(grid: str) -> str:
    next_move = move()
    if next_move == "<" and can_move_left(grid):
        grid = move_left(grid)
    elif next_move == ">" and can_move_right(grid):
        grid = move_right(grid)
    return grid

def turn_stationary(grid: str) -> str:
    return grid.replace("@", "#")

shapes_raw = """####

.#.
###
.#.

..#
..#
###

#
#
#
#

##
##"""

shapes = [create_shape(x) for x in shapes_raw.split("\n\n")]

number_of_rocks = 2022
# number_of_rocks = 20
grid_height = number_of_rocks*4
grid_width = 7

init_x = 3
init_y = 2

grid = "."*grid_width*grid_height

with open("17.txt") as f:
    jet_stream = f.read().strip()
    print(jet_stream)

move_i = -1

def move() -> str:
    global move_i
    move_i += 1
    return jet_stream[move_i % len(jet_stream)]

for i in range(number_of_rocks):
    print(i)
    shape = shapes[i % len(shapes)]
    grid = initialise_shape(shape, grid)
    # display_grid(grid)
    grid = move_left_right(grid)
    while can_move_down(grid):
        grid = move_down(grid)
        grid = move_left_right(grid)
    grid = turn_stationary(grid)

print(grid_top(grid))

Part 2

def create_shape(data: str) -> dict:
    return {
        "height": data.count("\n")+1,
        "width": len(data.split("\n")[0]),
        "shape": data.replace("#", "@")
    }

def grid_top(grid: str) -> int:
    return ((len(grid.rstrip("."))-1) // grid_width) + 1

def display_grid(grid: str) -> None:
    print(
        "\n".join(
            "|"+grid[i*grid_width:(i+1)*grid_width]+"|"
            for i in reversed(range(grid_top(grid)+1))
        ) +
        "\n" +
        "+" + "-"*grid_width + "+"
    )

def cut_grid(grid: str, grid_height: int) -> (str, int):
    at = len(grid)
    for i in range(grid_width):
        row = 0
        for j in range(grid_top(grid)):
            if grid[j*grid_width+i] == "#":
                row = j
        if row < at:
            at = row
    if at == len(grid):
        return grid+grid_addon, grid_height
    return (grid[at*grid_width:]+grid_addon, grid_height+at)

def trim_grid(grid: str) -> str:
    return grid[:(grid_top(grid)+1)*grid_width]

def initialise_shape(shape: dict, grid: str) -> str:
    end_height = init_x + grid_top(grid)
    start_height = shape["height"] + end_height
    return (
        grid[:end_height*grid_width] +
        "".join(
            "."*init_y +
            shape["shape"].split("\n")[i] +
            "."*(grid_width - init_y - shape["width"])
            for i in reversed(range(shape["height"]))
        ) +
        grid[start_height*grid_width:]
    )

def can_move_left(grid: str) -> bool:
    return all(
        i % grid_width != 0 and grid[i-1] != "#"
        for i, x in enumerate(grid) if x == "@"
    )

def can_move_right(grid: str) -> bool:
    return all(
        i % grid_width != grid_width-1 and grid[i+1] != "#"
        for i, x in enumerate(grid) if x == "@"
    )

def can_move_down(grid: str) -> bool:
    return all(
        i - grid_width > 0 and grid[i-grid_width] != "#"
        for i, x in enumerate(grid) if x == "@"
    )

def move_left(grid: str) -> str:
    new_grid = grid.replace("@", ".")
    for i, x in enumerate(grid):
        if x == "@":
            new_grid = (
                new_grid[:i-1] + "@" + new_grid[i:]
            )
    return new_grid

def move_right(grid: str) -> str:
    new_grid = grid.replace("@", ".")
    for i, x in enumerate(grid):
        if x == "@":
            new_grid = (
                new_grid[:i+1] + "@" + new_grid[i+2:]
            )
    return new_grid

def move_down(grid: str) -> str:
    new_grid = grid.replace("@", ".")
    for i, x in enumerate(grid):
        if x == "@":
            new_grid = (
                new_grid[:i-grid_width] + "@" + new_grid[i-grid_width+1:]
            )
    return new_grid

def move_left_right(grid: str) -> str:
    def move() -> str:
        global move_i
        move_i += 1
        return jet_stream[move_i % len(jet_stream)]

    next_move = move()
    if next_move == "<" and can_move_left(grid):
        grid = move_left(grid)
    elif next_move == ">" and can_move_right(grid):
        grid = move_right(grid)
    return grid

def turn_stationary(grid: str) -> str:
    return grid.replace("@", "#")

shapes_raw = """####

.#.
###
.#.

..#
..#
###

#
#
#
#

##
##"""

shapes = [create_shape(x) for x in shapes_raw.split("\n\n")]

number_of_rocks = 1000000000000
# number_of_rocks = 2022
# number_of_rocks = 100

grid_height = 0
grid_width = 7

init_x = 3
init_y = 2

with open("17.txt") as f:
# with open("17_test.txt") as f:
    jet_stream = f.read().strip()
    print(jet_stream)

move_i = -1
grid_addon = "."*grid_width*(init_x+1+max(shape["height"] for shape in shapes))
grid = grid_addon

grids_so_far = set()

def find_previous_turn(grids_so_far: set, grid: str, move_i: int):
    for g, m, old_turn, old_grid_height in grids_so_far:
        if g == grid and m == move_i:
            return g, m, old_turn, old_grid_height
    return None, None, None, None

# # Manually
# for i in range(number_of_rocks):
#     shape = shapes[i % len(shapes)]
#     grid, grid_height = cut_grid(grid, grid_height)
#     grid = initialise_shape(shape, grid)
#     grid = trim_grid(grid)
#     # display_grid(grid)
#     grid = move_left_right(grid)
#     while can_move_down(grid):
#         grid = move_down(grid)
#         grid = move_left_right(grid)
#     grid = turn_stationary(grid)

# print("Real answer", grid_height+grid_top(grid))
# display_grid(grid)


# ###################################
# # Copied over
# with open("17.txt") as f:
# # with open("17_test.txt") as f:
#     jet_stream = f.read().strip()
#     print(jet_stream)

# move_i = -1
# grid_addon = "."*grid_width*(init_x+1+max(shape["height"] for shape in shapes))
# grid = grid_addon
# grid_height = 0

# grids_so_far = set()
# ###################################

old_turn = 0
old_grid_height = 0
for i in range(number_of_rocks):
    shape = shapes[i % len(shapes)]
    grid, grid_height = cut_grid(grid, grid_height)
    grid = initialise_shape(shape, grid)
    grid = trim_grid(grid)
    grid = move_left_right(grid)
    while can_move_down(grid):
        grid = move_down(grid)
        grid = move_left_right(grid)
    grid = turn_stationary(grid)
    turns = i
    if any(
            grid == g and move_i % len(jet_stream) == m
            for g, m, old_i, old_grid_height in grids_so_far
    ):
        old_grid, _, old_turn, old_grid_height = find_previous_turn(
            grids_so_far,
            grid,
            move_i % len(jet_stream)
        )
        break
    else:
        grids_so_far.add((grid, move_i % len(jet_stream), i, grid_height))


repeat = (number_of_rocks // (turns - old_turn)) - 1
grid_height = old_grid_height + ((grid_height - old_grid_height) * repeat)

for i in range(old_turn + (turns-old_turn)*repeat + 1, number_of_rocks):
    shape = shapes[i % len(shapes)]
    grid, grid_height = cut_grid(grid, grid_height)
    grid = initialise_shape(shape, grid)
    grid = trim_grid(grid)
    grid = move_left_right(grid)
    while can_move_down(grid):
        grid = move_down(grid)
        grid = move_left_right(grid)
    grid = turn_stationary(grid)

print("answer", grid_height+grid_top(grid))