174 lines
5.4 KiB
Python
174 lines
5.4 KiB
Python
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)
|