diff --git a/aoc/day10.py b/aoc/day10.py new file mode 100644 index 0000000..b8e188f --- /dev/null +++ b/aoc/day10.py @@ -0,0 +1,146 @@ +from dataclasses import dataclass +from typing import TypeAlias + +from .aoc import Aoc2, AocSameParser + +Position: TypeAlias = tuple[int, int] +Graph: TypeAlias = dict[Position, list[Position]] + + +@dataclass +class PipeDiagram: + raw_input: list[str] + start_pos: Position + start_shape: str + reachable_nodes: list[Position] + + +class Day10(AocSameParser[PipeDiagram], Aoc2[PipeDiagram, PipeDiagram]): + @staticmethod + def position_plus(a: Position, b: Position) -> Position: + return (a[0] + b[0], a[1] + b[1]) + + @staticmethod + def find_start(inpt: list[str]) -> Position: + for y, line in enumerate(inpt): + try: + return line.index("S"), y + except ValueError: + pass + raise ValueError("S not found") + + @staticmethod + def connects(shape: str) -> list[Position]: + match shape: + case "|": + return [(0, 1), (0, -1)] + case "-": + return [(1, 0), (-1, 0)] + case "F": + return [(0, 1), (1, 0)] + case "L": + return [(0, -1), (1, 0)] + case "J": + return [(0, -1), (-1, 0)] + case "7": + return [(0, 1), (-1, 0)] + case _: + return [] + + @staticmethod + def shape_of_start(pos: Position, inpt: list[str]) -> str: + possibilities = [ + inpt[npos[1]][npos[0]] + for npos in ( + Day10.position_plus(pos, next) + for next in ((-1, 0), (1, 0), (0, 1), (0, -1)) + ) + ] # Left, Right, Down, Up + + possibles_shapes = {"|", "-", "F", "L", "J", "7"} + if possibilities[0] not in "FL-": + possibles_shapes = possibles_shapes.difference({"-", "7", "J"}) + if possibilities[1] not in "J7-": + possibles_shapes = possibles_shapes.difference({"-", "F", "L"}) + if possibilities[2] not in "J|L": + possibles_shapes = possibles_shapes.difference({"|", "F", "7"}) + if possibilities[3] not in "7|F": + possibles_shapes = possibles_shapes.difference({"|", "J", "L"}) + + if len(possibles_shapes) != 1: + raise RuntimeError("Malformed Pipes") + + return list(possibles_shapes)[0] + + @staticmethod + def reach( + start_pos: Position, shape_of_start: str, inpt: list[str] + ) -> list[Position]: + reach = [start_pos] + + last_pos = start_pos + current_pos = Day10.position_plus(start_pos, Day10.connects(shape_of_start)[0]) + while current_pos != start_pos: + reach.append(current_pos) + shape = inpt[current_pos[1]][current_pos[0]] + next_pos = list( + filter( + lambda p: p != last_pos, + ( + Day10.position_plus(current_pos, p) + for p in Day10.connects(shape) + ), + ) + ) + last_pos = current_pos + current_pos = next_pos[0] + + return reach + + def parseinput(self, inpt: str) -> PipeDiagram: + lines = inpt.splitlines() + start_pos = Day10.find_start(lines) + start_shape = Day10.shape_of_start(start_pos, lines) + positions = Day10.reach(start_pos, start_shape, inpt.splitlines()) + return PipeDiagram(lines, start_pos, start_shape, positions) + + def part1(self, inpt: PipeDiagram) -> int: + return len(inpt.reachable_nodes) // 2 + + def part2(self, inpt: PipeDiagram) -> int: + r = set(inpt.reachable_nodes) + inside = 0 + for y, line in enumerate(inpt.raw_input): + last_char = None + accum = 0 + for x, char in enumerate(line): + if (x, y) not in r: + if accum % 2 != 0: + inside += 1 + continue + if char == "|": + accum += 1 + elif char == "S": + char = inpt.start_shape + + if char not in "FJ7L": + continue + if last_char is None: + last_char = char + else: + match last_char, char: + case "F", "J": + accum += 1 + case "L", "7": + accum += 1 + last_char = char + return inside + + +def main() -> None: + day10 = Day10(2023, 10, example_code_nr1=6, example_code_nr2=-10) + day10.run() + + +if __name__ == "__main__": + main()