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()