advent-of-code/2022/d22/ex2/ex2.py

231 lines
7.1 KiB
Python
Raw Normal View History

2022-12-22 16:23:41 +01:00
#!/usr/bin/env python
import enum
import itertools
import sys
from collections.abc import Iterable, Iterator
from typing import NamedTuple, TypeVar, Union
T = TypeVar("T")
def take(n: int, iterable: Iterable[T]) -> Iterator[T]:
return itertools.islice(iterable, n)
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)
class Tile(str, enum.Enum):
AIR = "."
WALL = "#"
class Direction(enum.IntEnum):
EAST = 0
SOUTH = 1
WEST = 2
NORTH = 3
def turn(self, rot: "Rotation") -> "Direction":
if rot == Rotation.LEFT:
return Direction((self - 1 + 4) % 4)
if rot == Rotation.RIGHT:
return Direction((self + 1) % 4)
assert False # Sanity check
def to_delta(self) -> Point:
match self:
case Direction.NORTH:
return Point(-1, 0)
case Direction.SOUTH:
return Point(1, 0)
case Direction.EAST:
return Point(0, 1)
case Direction.WEST:
return Point(0, -1)
class Rotation(str, enum.Enum):
LEFT = "L"
RIGHT = "R"
class CubeFace(enum.IntEnum):
# A B
# C
# D E
# F
A = enum.auto()
B = enum.auto()
C = enum.auto()
D = enum.auto()
E = enum.auto()
F = enum.auto()
def minmax(self) -> tuple[Point, Point]:
match self:
case CubeFace.A:
return Point(1, 51), Point(50, 100)
case CubeFace.B:
return Point(1, 101), Point(50, 150)
case CubeFace.C:
return Point(51, 51), Point(100, 100)
case CubeFace.D:
return Point(101, 1), Point(150, 50)
case CubeFace.E:
return Point(101, 51), Point(150, 100)
case CubeFace.F:
return Point(151, 1), Point(200, 50)
def belongs(self, p: Point) -> bool:
(minx, miny), (maxx, maxy) = self.minmax()
return (minx <= p.x <= maxx) and (miny <= p.y <= maxy)
@classmethod
def from_point(cls, p: Point) -> "CubeFace":
return next(f for f in cls if f.belongs(p))
def walk_along(self, p: Point, dir: Direction) -> tuple[Point, Direction]:
assert self.belongs(p) # Sanity check
new_p = p + dir.to_delta()
if self.belongs(new_p):
return new_p, dir
return self._do_wrap(p, dir)
def _do_wrap(self, p: Point, dir: Direction) -> tuple[Point, Direction]:
match (self, dir):
case CubeFace.A, Direction.EAST: # A -> B
return p + dir.to_delta(), dir
case CubeFace.A, Direction.SOUTH: # A -> C
return p + dir.to_delta(), dir
case CubeFace.A, Direction.WEST: # A -> D
return Point(151 - p.x, 1), Direction.EAST
case CubeFace.A, Direction.NORTH: # A -> F
return Point(100 + p.y, 1), Direction.EAST
case CubeFace.B, Direction.EAST: # B -> E
return Point(151 - p.x, 100), Direction.WEST
case CubeFace.B, Direction.SOUTH: # B -> C
return Point(p.y - 50, 100), Direction.WEST
case CubeFace.B, Direction.WEST: # B -> A
return p + dir.to_delta(), dir
case CubeFace.B, Direction.NORTH: # B -> F
return Point(200, p.y - 100), Direction.NORTH
case CubeFace.C, Direction.EAST: # C -> B
return Point(50, p.x + 50), Direction.NORTH
case CubeFace.C, Direction.SOUTH: # C -> E
return p + dir.to_delta(), dir
case CubeFace.C, Direction.WEST: # C -> D
return Point(101, p.x - 50), Direction.SOUTH
case CubeFace.C, Direction.NORTH: # C -> A
return p + dir.to_delta(), dir
case CubeFace.D, Direction.EAST: # D -> E
return p + dir.to_delta(), dir
case CubeFace.D, Direction.SOUTH: # D -> F
return p + dir.to_delta(), dir
case CubeFace.D, Direction.WEST: # D -> A
return Point(151 - p.x, 51), Direction.EAST
case CubeFace.D, Direction.NORTH: # D -> C
return Point(50 + p.y, 51), Direction.EAST
case CubeFace.E, Direction.EAST: # E -> B
return Point(151 - p.x, 150), Direction.WEST
case CubeFace.E, Direction.SOUTH: # E -> F
return Point(100 + p.y, 50), Direction.WEST
case CubeFace.E, Direction.WEST: # E -> D
return p + dir.to_delta(), dir
case CubeFace.E, Direction.NORTH: # E -> C
return p + dir.to_delta(), dir
case CubeFace.F, Direction.EAST: # F -> E
return Point(150, p.x - 100), Direction.NORTH
case CubeFace.F, Direction.SOUTH: # F -> B
return Point(1, 100 + p.y), Direction.SOUTH
case CubeFace.F, Direction.WEST: # F -> A
return Point(1, p.x - 100), Direction.SOUTH
case CubeFace.F, Direction.NORTH: # F -> D
return p + dir.to_delta(), dir
assert False
Map = dict[Point, Tile]
def solve(input: list[str]) -> int:
def parse_map(input: list[str]) -> tuple[Point, Map]:
res: Map = {}
for i, line in enumerate(input, start=1):
for j, c in enumerate(line, start=1):
if c == " ":
continue
res[Point(i, j)] = Tile(c)
return min(p for p in res.keys()), res
def parse_instruction(input: str) -> list[Union[Rotation, int]]:
res: list[Union[Rotation, int]] = []
i = 0
while i < len(input):
# Parse direction
if input[i] in list(Rotation):
res.append(Rotation(input[i]))
i += 1
continue
# Parse int
j = i + 1
while j < len(input) and input[j] not in list(Rotation):
j += 1
res.append(int(input[i:j]))
i = j
return res
def points_along(start: Point, dir: Direction) -> Iterator[tuple[Point, Direction]]:
while True:
start, dir = CubeFace.from_point(start).walk_along(start, dir)
yield start, dir
assert input[-2] == "" # Sanity check
facing = Direction.EAST
start, map = parse_map(input[:-2])
instructions = parse_instruction(input[-1])
for instr in instructions:
if isinstance(instr, Rotation):
facing = facing.turn(instr)
continue
for p, new_facing in take(instr, points_along(start, facing)):
if map[p] == Tile.WALL:
break
start = p
facing = new_facing
return 1000 * start.x + 4 * start.y + facing
def main() -> None:
input = sys.stdin.read().splitlines()
print(solve(input))
if __name__ == "__main__":
main()