158 lines
4.2 KiB
Python
158 lines
4.2 KiB
Python
|
#!/usr/bin/env python
|
||
|
|
||
|
import enum
|
||
|
import itertools
|
||
|
import sys
|
||
|
from collections.abc import Iterator
|
||
|
from typing import NamedTuple
|
||
|
|
||
|
|
||
|
class Point(NamedTuple):
|
||
|
x: int
|
||
|
y: int
|
||
|
|
||
|
def __add__(self, other):
|
||
|
if not isinstance(other, Point):
|
||
|
return NotImplemented
|
||
|
return Point(self.x + other.x, self.y + other.y)
|
||
|
|
||
|
def __sub__(self, other):
|
||
|
if not isinstance(other, Point):
|
||
|
return NotImplemented
|
||
|
return Point(self.x - other.x, self.y - other.y)
|
||
|
|
||
|
|
||
|
def translate(points: set[Point], delta: Point) -> set[Point]:
|
||
|
return {p + delta for p in points}
|
||
|
|
||
|
|
||
|
class Rock(str, enum.Enum):
|
||
|
LINE = "####"
|
||
|
PLUS = ".#.\n###\n.#."
|
||
|
CORNER = "..#\n..#\n###"
|
||
|
VERTICAL_LINE = "#\n#\n#\n#"
|
||
|
SQUARE = "##\n##"
|
||
|
|
||
|
@classmethod
|
||
|
def stream(cls) -> Iterator["Rock"]:
|
||
|
yield from itertools.cycle(iter(cls))
|
||
|
|
||
|
def to_points(self) -> set[Point]:
|
||
|
res: set[Point] = set()
|
||
|
|
||
|
for y, line in enumerate(reversed(self.splitlines())):
|
||
|
for x, c in enumerate(line):
|
||
|
if c == ".":
|
||
|
continue
|
||
|
res.add(Point(x, y))
|
||
|
|
||
|
return res
|
||
|
|
||
|
|
||
|
class JetStream(str, enum.Enum):
|
||
|
LEFT = "<"
|
||
|
RIGHT = ">"
|
||
|
|
||
|
@classmethod
|
||
|
def stream(cls, jet_pattern: str) -> Iterator["JetStream"]:
|
||
|
yield from itertools.cycle(map(cls, jet_pattern))
|
||
|
|
||
|
def as_delta(self) -> Point:
|
||
|
if self == self.LEFT:
|
||
|
return Point(-1, 0)
|
||
|
if self == self.RIGHT:
|
||
|
return Point(1, 0)
|
||
|
assert False # Sanity check
|
||
|
|
||
|
|
||
|
def solve(input: list[str]) -> int:
|
||
|
fallen_stack: set[Point] = set()
|
||
|
max_height = 0
|
||
|
|
||
|
LEFT_WALL = -1
|
||
|
RIGHT_WALL = 7
|
||
|
FLOOR = 0
|
||
|
|
||
|
rocks = list(iter(Rock))
|
||
|
jet_stream = [JetStream(c) for c in input[0]]
|
||
|
|
||
|
t = 0
|
||
|
jet_index = 0
|
||
|
rock_index = 0
|
||
|
|
||
|
def step(rock: set[Point], jet: JetStream) -> tuple[set[Point], bool]:
|
||
|
# Check if it can be pushed by the jet, or if it hits an obstacle
|
||
|
pushed_rock = translate(rock, jet.as_delta())
|
||
|
if not (fallen_stack & pushed_rock) and all(
|
||
|
LEFT_WALL < p.x < RIGHT_WALL for p in pushed_rock
|
||
|
):
|
||
|
rock = pushed_rock
|
||
|
|
||
|
# Check if it can go down
|
||
|
fallen_rock = translate(rock, Point(0, -1))
|
||
|
|
||
|
if not (fallen_stack & fallen_rock) and all(p.y > FLOOR for p in fallen_rock):
|
||
|
return fallen_rock, True
|
||
|
return rock, False
|
||
|
|
||
|
def simulate_rock_fall() -> None:
|
||
|
nonlocal max_height
|
||
|
nonlocal jet_index
|
||
|
nonlocal rock_index
|
||
|
|
||
|
rock = rocks[rock_index].to_points()
|
||
|
|
||
|
# Align 2 units away from LEFT_WALL and 3 higher than
|
||
|
# current stack
|
||
|
rock = translate(rock, Point(2, max_height + 3 + 1))
|
||
|
|
||
|
while True:
|
||
|
rock, keep_going = step(rock, jet_stream[jet_index])
|
||
|
jet_index = (jet_index + 1) % len(jet_stream)
|
||
|
if not keep_going:
|
||
|
break
|
||
|
|
||
|
fallen_stack.update(rock)
|
||
|
max_height = max(max_height, max(p.y for p in rock))
|
||
|
rock_index = (rock_index + 1) % len(rocks)
|
||
|
|
||
|
StackStateHash = tuple[int, int, frozenset[Point]]
|
||
|
|
||
|
def stack_state_hash() -> StackStateHash:
|
||
|
top = frozenset(
|
||
|
Point(p.x, p.y - max_height)
|
||
|
for p in fallen_stack
|
||
|
if p.y >= (max_height - 50) # Cut-off point chosen arbitrarily...
|
||
|
)
|
||
|
return rock_index, jet_index, top
|
||
|
|
||
|
assert len(input) == 1 # Sanity check
|
||
|
|
||
|
cache: dict[StackStateHash, tuple[int, int]] = {}
|
||
|
added_height = 0
|
||
|
|
||
|
END_OF_SIMULATION = 1_000_000_000_000
|
||
|
while t < END_OF_SIMULATION:
|
||
|
simulate_rock_fall()
|
||
|
t += 1
|
||
|
stack_hash = stack_state_hash()
|
||
|
if stack_hash in cache:
|
||
|
previous_t, previous_height = cache[stack_hash]
|
||
|
cycle_length = t - previous_t
|
||
|
num_cycles = (END_OF_SIMULATION - t) // cycle_length
|
||
|
added_height += num_cycles * (max_height - previous_height)
|
||
|
t += num_cycles * cycle_length
|
||
|
else:
|
||
|
cache[stack_hash] = t, max_height
|
||
|
|
||
|
return max_height + added_height
|
||
|
|
||
|
|
||
|
def main() -> None:
|
||
|
input = sys.stdin.read().splitlines()
|
||
|
print(solve(input))
|
||
|
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
main()
|