151 lines
5 KiB
Python
151 lines
5 KiB
Python
from __future__ import annotations
|
|
from dataclasses import dataclass
|
|
from itertools import cycle
|
|
|
|
from typing import Iterator, Self
|
|
|
|
day_num = 17
|
|
|
|
|
|
def part1(lines: Iterator[str]) -> int:
|
|
cave = Cave.create(7, next(lines))
|
|
return cave.process_many_rocks(2022)
|
|
|
|
|
|
def part2(lines: Iterator[str]) -> int:
|
|
cave = Cave.create(7, next(lines))
|
|
return cave.process_many_rocks(1_000_000_000_000)
|
|
|
|
|
|
patterns = [["####"],
|
|
[" # ", "###", " # "],
|
|
["###", " #", " #"],
|
|
["#", "#", "#", "#"],
|
|
["##", "##"]
|
|
]
|
|
|
|
|
|
@dataclass(slots=True, frozen=True)
|
|
class Position:
|
|
x: int
|
|
y: int
|
|
|
|
def left(self) -> Position:
|
|
return Position(self.x - 1, self.y)
|
|
|
|
def right(self) -> Position:
|
|
return Position(self.x + 1, self.y)
|
|
|
|
def down(self) -> Position:
|
|
return Position(self.x, self.y - 1)
|
|
|
|
|
|
@dataclass(slots=True, frozen=True)
|
|
class Pattern:
|
|
lines: list[str]
|
|
|
|
@property
|
|
def height(self) -> int:
|
|
return len(self.lines)
|
|
|
|
@property
|
|
def width(self) -> int:
|
|
return len(self.lines[0])
|
|
|
|
def stones(self, position: Position) -> Iterator[Position]:
|
|
for y, line in enumerate(self.lines):
|
|
for x, block in enumerate(line):
|
|
if block == "#":
|
|
yield Position(position.x + x, position.y + y)
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class Cave:
|
|
width: int
|
|
cave: list[str]
|
|
gas_pushes: Iterator[str]
|
|
rock_dispenser: Iterator[Pattern]
|
|
|
|
@property
|
|
def height(self) -> int:
|
|
return len(self.cave)
|
|
|
|
def check_free(self, rock: Pattern, position: Position) -> bool:
|
|
for block in rock.stones(position):
|
|
if block.y < len(self.cave) and self.cave[block.y][block.x] == '#':
|
|
return False
|
|
return True
|
|
|
|
def fix_rock(self, rock: Pattern, position: Position):
|
|
for _ in range(len(self.cave), position.y + rock.height):
|
|
self.cave.append(' ' * self.width)
|
|
for block in rock.stones(position):
|
|
old = self.cave[block.y]
|
|
self.cave[block.y] = old[:block.x] + '#' + old[block.x + 1:]
|
|
|
|
@ classmethod
|
|
def create(cls, width: int, gas_pushes: str) -> Self:
|
|
cave = []
|
|
return cls(width, cave, cycle(gas_pushes), cycle(Pattern(pattern) for pattern in patterns))
|
|
|
|
def process_one_rock(self) -> tuple[Pattern, Position]:
|
|
rock = next(self.rock_dispenser)
|
|
position = Position(2, len(self.cave) + 3)
|
|
|
|
while True:
|
|
push = next(self.gas_pushes)
|
|
if push == '<':
|
|
if position.x > 0 and self.check_free(rock, position.left()):
|
|
position = position.left()
|
|
else:
|
|
if (position.x + rock.width < self.width
|
|
and self.check_free(rock, position.right())):
|
|
position = position.right()
|
|
|
|
if position.y > 0 and self.check_free(rock, position.down()):
|
|
position = position.down()
|
|
else:
|
|
self.fix_rock(rock, position)
|
|
return rock, position
|
|
|
|
def process_many_rocks(self, max_rounds: int) -> int:
|
|
last_landing_row = 0
|
|
last_max_drop_time = 0
|
|
last_max_drop_height = 0
|
|
last_max_drop_row = 0
|
|
last_max_drop_pattern: Pattern | None = None
|
|
max_drop_height = 0
|
|
time = 0
|
|
added_height = 0
|
|
while time < max_rounds:
|
|
pattern, position = self.process_one_rock()
|
|
|
|
if position.y < last_landing_row:
|
|
drop_height = last_landing_row - position.y
|
|
if drop_height == max_drop_height and last_max_drop_pattern == pattern:
|
|
drop_cycle_height = position.y - last_max_drop_row
|
|
if last_max_drop_row - drop_cycle_height > 0:
|
|
different = False
|
|
for row in range(last_max_drop_row - drop_cycle_height, last_max_drop_row):
|
|
if self.cave[row] != self.cave[row - drop_cycle_height]:
|
|
different = True
|
|
break
|
|
if not different:
|
|
time_diff = time - last_max_drop_time
|
|
height_diff = self.height - last_max_drop_height
|
|
cycle_count = (max_rounds - time) // time_diff
|
|
time += cycle_count * time_diff
|
|
added_height = cycle_count * height_diff
|
|
last_max_drop_time = time
|
|
last_max_drop_height = self.height
|
|
last_max_drop_row = position.y
|
|
elif drop_height > max_drop_height:
|
|
last_max_drop_time = time
|
|
last_max_drop_height = self.height
|
|
last_max_drop_row = position.y
|
|
last_max_drop_pattern = pattern
|
|
max_drop_height = drop_height
|
|
last_landing_row = position.y
|
|
time += 1
|
|
|
|
return self.height + added_height
|