diff --git a/2022/d22/ex2/ex2.py b/2022/d22/ex2/ex2.py new file mode 100755 index 0000000..9a4f2cb --- /dev/null +++ b/2022/d22/ex2/ex2.py @@ -0,0 +1,230 @@ +#!/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()