from __future__ import annotations from dataclasses import dataclass from collections import defaultdict import logging from typing import NamedTuple, TypeGuard from enum import Enum import sys class Point(NamedTuple): x: int y: int c: str def add_delta(self, delta: PointDelta, labyrinth: Labyrinth) -> Point | None: if (0 <= self.x + delta.x < len(labyrinth[0])) and ( 0 <= self.y + delta.y < len(labyrinth) ): return labyrinth[self.y + delta.y][self.x + delta.x] return None class PointDelta(NamedTuple): x: int y: int class Deltas(Enum): UP = PointDelta(0, -1) DOWN = PointDelta(0, 1) LEFT = PointDelta(-1, 0) RIGHT = PointDelta(1, 0) NONE = PointDelta(0, 0) NO_MOVEMENT = (Deltas.NONE, Deltas.NONE) PIPES = defaultdict( lambda: NO_MOVEMENT, [ ("|", (Deltas.UP, Deltas.DOWN)), ("-", (Deltas.LEFT, Deltas.RIGHT)), ("L", (Deltas.UP, Deltas.RIGHT)), ("J", (Deltas.UP, Deltas.LEFT)), ("7", (Deltas.DOWN, Deltas.LEFT)), ("F", (Deltas.DOWN, Deltas.RIGHT)), ], ) @dataclass(init=False) class Labyrinth: _data: tuple[tuple[Point, ...], ...] # stored as [Y][X] start: Point first_points: tuple[Point, ...] def __init__( self, data: tuple[tuple[Point, ...], ...], start: Point, first_points: tuple[Point, ...] | None = None, ): self._data = data self.start = start self.first_points = ( self._find_first_points() if first_points is None else first_points ) logging.debug(("start", start)) logging.debug(("first_points", self.first_points)) def __getitem__(self, key: int) -> tuple[Point, ...]: return self._data[key] def __len__(self): return len(self._data) def __iter__(self): return iter(self._data) def _find_first_points(self) -> tuple[Point, ...]: up = self.start.add_delta(Deltas.UP.value, self) up = up if up is not None and Deltas.DOWN in PIPES[up.c] else None down = self.start.add_delta(Deltas.DOWN.value, self) down = down if down is not None and Deltas.UP in PIPES[down.c] else None left = self.start.add_delta(Deltas.LEFT.value, self) left = left if left is not None and Deltas.RIGHT in PIPES[left.c] else None right = self.start.add_delta(Deltas.RIGHT.value, self) right = right if right is not None and Deltas.LEFT in PIPES[right.c] else None def point_is_not_none(n: Point | None) -> TypeGuard[Point]: # makes mypy happy return n is not None return tuple(filter(point_is_not_none, [up, down, left, right])) def get_labyrinth(self): return self._data def get_path(self) -> list[Point]: path = [self.start] previous_point = self.start current_point = self.first_points[0] while current_point != self.start: path.append(current_point) new_point = movement(current_point, previous_point, self) previous_point = current_point current_point = new_point logging.debug(current_point) path.append(current_point) return path def get_empty_points(self) -> list[Point]: empty_points = [] for line in self._data: for point in line: if point.c == ".": empty_points.append(point) return empty_points def clean(self) -> Labyrinth: path = self.get_path() new_data = [] for y, line in enumerate(self._data): new_line = [] for x, point in enumerate(line): if point not in path: new_line.append(Point(x, y, ".")) else: new_line.append(point) new_data.append(tuple(new_line)) return Labyrinth(new_data, self.start, self.first_points) def show(self, descriptor=sys.stdout) -> None: for line in self._data: linestr = "".join(map(lambda p: p.c, line)) + "\n" descriptor.write(linestr) def movement( current_point: Point, previous_point: Point, labyrinth: Labyrinth ) -> Point: movement_type = PIPES[current_point.c] if movement_type == NO_MOVEMENT: return current_point movement_value = tuple(x.value for x in movement_type) diff = PointDelta( previous_point.x - current_point.x, previous_point.y - current_point.y ) if diff not in movement_value: return current_point idx = 0 if movement_value.index(diff) == 1 else 1 return labyrinth[current_point.y + movement_value[idx].y][ current_point.x + movement_value[idx].x ] def parse(input_file: str) -> Labyrinth: logging.debug(f"parsing {input_file}") array = [] start_point = Point(-1, -1, "") line_no = 0 with open(input_file, "r", encoding="UTF-8") as input_handle: while line := input_handle.readline().rstrip("\n").rstrip(): ar_line = [] for col_no, char in enumerate(line): new_point = Point(col_no, line_no, char) logging.debug(f"new point: {new_point}") ar_line.append(new_point) if char == "S": start_point = new_point array.append(tuple(ar_line)) line_no += 1 return Labyrinth(data=tuple(array), start=start_point)