231 lines
7.1 KiB
Python
231 lines
7.1 KiB
Python
|
#!/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()
|