182 lines
4.9 KiB
Python
182 lines
4.9 KiB
Python
from typing import *
|
|
from aocpy import BaseChallenge, Vector, RepeatingConsumer, foldl
|
|
|
|
Tube = Dict[Vector, None]
|
|
Rock = Tuple[int, int, int, int, int]
|
|
|
|
|
|
TUBE_WIDTH = 7
|
|
|
|
# Coordinate system:
|
|
# ^
|
|
# |
|
|
# x --------->
|
|
# |
|
|
# y
|
|
|
|
|
|
def parse_rock(x: str) -> Tuple[Tuple[Vector], Tuple[Vector]]:
|
|
res = []
|
|
for y, line in enumerate(reversed(x.strip().splitlines())):
|
|
for x, char in enumerate(line):
|
|
if char == "#":
|
|
res.append(Vector(x, y))
|
|
|
|
height = y + 1
|
|
width = x + 1
|
|
|
|
left = []
|
|
right = []
|
|
for y in range(height):
|
|
cx = tuple(coord.x for coord in res if coord.y == y)
|
|
min_x = min(cx)
|
|
max_x = max(cx)
|
|
|
|
left.append(Vector(min_x, y))
|
|
right.append(Vector(max_x, y))
|
|
|
|
below = []
|
|
for x in range(width):
|
|
min_y = min(coord.y for coord in res if coord.x == x)
|
|
below.append(Vector(x, min_y - 1))
|
|
|
|
return tuple(res), tuple(left), tuple(right), tuple(below), width
|
|
|
|
|
|
ROCKS: List[Rock] = [
|
|
parse_rock(lines)
|
|
for lines in "####\n\n.#.\n###\n.#.\n\n..#\n..#\n###\n\n#\n#\n#\n#\n\n##\n##".split(
|
|
"\n\n"
|
|
)
|
|
]
|
|
|
|
|
|
def get_num_rows(tube: Tube) -> int:
|
|
filled_spaces = tube.keys()
|
|
return max(x.y for x in filled_spaces) + 1 if len(filled_spaces) != 0 else 0
|
|
|
|
|
|
def get_rock_position(
|
|
rock: Rock, instructions: RepeatingConsumer, tube: Tube
|
|
) -> Vector:
|
|
(_, slots_left, slots_right, slots_below, rock_width) = rock
|
|
|
|
filled_spaces = tube.keys()
|
|
position = Vector(2, get_num_rows(tube) + 3)
|
|
|
|
while True:
|
|
shift_direction = instructions.take()
|
|
|
|
delta_x = 0
|
|
|
|
if shift_direction == "<":
|
|
if position.x - 1 >= 0:
|
|
delta_x = -1
|
|
elif shift_direction == ">":
|
|
if position.x + rock_width < TUBE_WIDTH:
|
|
# rock_width includes a +1 for our needs
|
|
delta_x = 1
|
|
else:
|
|
raise ValueError("invalid shift direction")
|
|
|
|
if delta_x != 0:
|
|
slots = slots_left if delta_x < 0 else slots_right
|
|
can_move = foldl(
|
|
lambda x, y: x and y,
|
|
(
|
|
((p.x + position.x + delta_x, p.y + position.y) not in tube)
|
|
for p in slots
|
|
),
|
|
True,
|
|
)
|
|
|
|
if can_move:
|
|
position.x += delta_x
|
|
|
|
can_move_down = False
|
|
if position.y != 0:
|
|
can_move_down = foldl(
|
|
lambda x, y: x and y,
|
|
(
|
|
((p.x + position.x, p.y + position.y) not in tube)
|
|
for p in slots_below
|
|
),
|
|
True,
|
|
)
|
|
|
|
if not can_move_down:
|
|
break
|
|
|
|
position.y -= 1
|
|
|
|
return position
|
|
|
|
|
|
def add_rock(rock: Rock, position: Vector, tube: Tube):
|
|
rock_shape = rock[0]
|
|
for p in rock_shape:
|
|
tube[Vector(position.x + p.x, position.y + p.y)] = None
|
|
|
|
|
|
def get_heights(tube: Tube) -> Tuple[Tuple[int], int]:
|
|
max_height = get_num_rows(tube)
|
|
res = []
|
|
for x in range(TUBE_WIDTH):
|
|
largest = 0
|
|
for v in (p.y for p in tube if p.x == x):
|
|
if v > largest:
|
|
largest = v
|
|
res.append(max_height - largest)
|
|
return tuple(res), max_height
|
|
|
|
|
|
class Challenge(BaseChallenge):
|
|
@staticmethod
|
|
def one(instr: str) -> int:
|
|
tube: Tube = {}
|
|
instructions = RepeatingConsumer(instr.strip())
|
|
rocks = RepeatingConsumer(ROCKS)
|
|
|
|
for _ in range(2022):
|
|
rock = rocks.take()
|
|
pos = get_rock_position(rock, instructions, tube)
|
|
add_rock(rock, pos, tube)
|
|
|
|
return get_num_rows(tube)
|
|
|
|
@staticmethod
|
|
def two(instr: str) -> int:
|
|
tube: Tube = {}
|
|
instructions = RepeatingConsumer(instr.strip())
|
|
rocks = RepeatingConsumer(ROCKS)
|
|
|
|
seen: Dict[int, int] = {}
|
|
|
|
target = 1000000000000
|
|
|
|
num_rocks_thrown = 0
|
|
while num_rocks_thrown < target:
|
|
rock = rocks.take()
|
|
pos = get_rock_position(rock, instructions, tube)
|
|
add_rock(rock, pos, tube)
|
|
|
|
col_heights, max_height = get_heights(tube)
|
|
h = (rocks.i, instructions.i, *col_heights)
|
|
if h in seen:
|
|
cycle_begins, height_at_cycle_begin = seen[h]
|
|
|
|
rocks_thrown_per_cycle = num_rocks_thrown - cycle_begins
|
|
height_increase_per_cycle = max_height - height_at_cycle_begin
|
|
|
|
rocks_left_to_throw = target - num_rocks_thrown
|
|
full_cycles_remaining = rocks_left_to_throw // rocks_thrown_per_cycle
|
|
num_rocks_thrown += rocks_thrown_per_cycle * full_cycles_remaining
|
|
|
|
# so we don't get any more cache hit while we run through the last few
|
|
seen = {}
|
|
else:
|
|
seen[h] = (num_rocks_thrown, max_height)
|
|
|
|
num_rocks_thrown += 1
|
|
|
|
return get_num_rows(tube) + (height_increase_per_cycle * full_cycles_remaining)
|