advent-2022-python/advent/days/day09/solution.py
2022-12-10 19:30:10 +01:00

93 lines
2.8 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from typing import Iterator, Self
day_num = 9
def part1(lines: Iterator[str]) -> int:
commands = (Command.parse(line) for line in lines)
return simulate(commands, 2)
def part2(lines: Iterator[str]) -> int:
commands = (Command.parse(line) for line in lines)
return simulate(commands, 10)
@dataclass(frozen=True, slots=True)
class Point:
x: int
y: int
@classmethod
def parse_direction(cls, char: str) -> Self:
""" Parses the given direction to a Point. May raise if invalid """
match char:
case 'R':
return cls(1, 0)
case 'U':
return cls(0, 1)
case 'L':
return cls(-1, 0)
case 'D':
return cls(0, -1)
case _:
raise Exception(f"Unkown Direction: {char}")
def add(self, other: Point) -> Point:
return Point(self.x + other.x, self.y + other.y)
def sub(self, other: Point) -> Point:
return Point(self.x - other.x, self.y - other.y)
def is_unit(self) -> bool:
""" return true, if this describes any point (diagonally) adjacent to the origin"""
return abs(self.x) <= 1 and abs(self.y) <= 1
def as_unit(self) -> Point:
""" Compresses this Point to a point with unit components """
def unit(num: int) -> int:
return 0 if num == 0 else num // abs(num)
return Point(unit(self.x), unit(self.y))
def step_to(self, other: Point) -> Point | None:
""" Returns a point one step towards the other point. Returns None, if already adjacent """
diff = other.sub(self)
if diff.is_unit():
return None
return self.add(diff.as_unit())
@dataclass(frozen=True, slots=True)
class Command:
dir: Point
steps: int
@classmethod
def parse(cls, line: str) -> Self:
""" Parse a command line. My raise exception if the was an illegal line"""
match line.split():
case [dir, steps]:
return Command(Point.parse_direction(dir), int(steps))
case _:
raise Exception(f"Illegal line: {line}")
def simulate(lst: Iterator[Command], rope_length: int) -> int:
""" Walks the whole rope in Planck length steps according to commands """
rope = [Point(0, 0)] * rope_length
visited = {rope[-1]}
for command in lst:
for _ in range(command.steps):
rope[0] = rope[0].add(command.dir)
for n in range(1, rope_length):
moved_piece = rope[n].step_to(rope[n - 1])
if not moved_piece:
break
rope[n] = moved_piece
if n == rope_length - 1:
visited.add(rope[n])
return len(visited)