from collections import deque from tools.aoc import AOCDay from tools.coordinate import Coordinate from tools.grid import Grid from typing import Any LOOKUP = { "|": { Coordinate(0, 1): {"o_dir": [Coordinate(-1, 0)], "i_dir": [Coordinate(1, 0)], "n_dir": Coordinate(0, 1)}, Coordinate(0, -1): {"o_dir": [Coordinate(1, 0)], "i_dir": [Coordinate(-1, 0)], "n_dir": Coordinate(0, -1)}, }, "-": { Coordinate(1, 0): {"o_dir": [Coordinate(0, 1)], "i_dir": [Coordinate(0, -1)], "n_dir": Coordinate(1, 0)}, Coordinate(-1, 0): {"o_dir": [Coordinate(0, -1)], "i_dir": [Coordinate(0, 1)], "n_dir": Coordinate(-1, 0)}, }, "7": { Coordinate(1, 0): { "o_dir": None, "i_dir": [Coordinate(0, -1), Coordinate(1, 0)], "n_dir": Coordinate(0, 1), }, Coordinate(0, -1): { "o_dir": [Coordinate(1, 0), Coordinate(0, -1)], "i_dir": None, "n_dir": Coordinate(-1, 0), }, }, "J": { Coordinate(0, 1): { "o_dir": None, "i_dir": [Coordinate(1, 0), Coordinate(0, 1)], "n_dir": Coordinate(-1, 0), }, Coordinate(1, 0): { "o_dir": [Coordinate(1, 0), Coordinate(0, 1)], "i_dir": None, "n_dir": Coordinate(0, -1), }, }, "F": { Coordinate(0, -1): { "o_dir": None, "i_dir": [Coordinate(-1, 0), Coordinate(0, -1)], "n_dir": Coordinate(1, 0), }, Coordinate(-1, 0): { "o_dir": [Coordinate(-1, 0), Coordinate(0, -1)], "i_dir": None, "n_dir": Coordinate(0, 1), }, }, "L": { Coordinate(-1, 0): { "o_dir": None, "i_dir": [Coordinate(-1, 0), Coordinate(0, 1)], "n_dir": Coordinate(0, -1), }, Coordinate(0, 1): { "o_dir": [Coordinate(-1, 0), Coordinate(0, 1)], "i_dir": None, "n_dir": Coordinate(1, 0), }, }, } def remove_clutter(grid: Grid, start: Coordinate): pipe_coords = set() for x in LOOKUP[grid.get(start)]: dir_ = LOOKUP[grid.get(start)][x]["n_dir"] break while start not in pipe_coords: pipe_coords.add(start) start += dir_ dir_ = LOOKUP[grid.get(start)][dir_]["n_dir"] for c in grid.getActiveCells(): if c not in pipe_coords: grid.set(c, False) def get_area(grid: Grid, start: Coordinate) -> set[Coordinate]: if grid.get(start): return set() area = set() q = deque() q.append(start) while q: c = q.popleft() if c in area: continue area.add(c) for n in c.getNeighbours(includeDiagonal=True): if grid.get(n): continue if grid.isWithinBoundaries(n): q.append(n) return area def walk(grid: Grid, part2: bool = False) -> int: cur = None for x in grid.rangeX(): for y in grid.rangeY(): if grid.get(Coordinate(x, y)): cur = Coordinate(x, y) break if cur is not None: break assert grid.get(cur) == "F" pipe_len = 0 direction = Coordinate(0, 1) outside = get_area(grid, cur + Coordinate(0, -1)) inside = set() visited = set() while cur not in visited: pipe_len += 1 visited.add(cur) cur += direction c = grid.get(cur) if part2: oc = LOOKUP[c][direction]["o_dir"] ic = LOOKUP[c][direction]["i_dir"] if oc is not None: for doc in oc: check = cur + doc if check not in outside and not grid.get(check): outside |= get_area(grid, check) if ic is not None: for dic in ic: check = cur + dic if check not in inside and not grid.get(check): inside |= get_area(grid, check) direction = LOOKUP[c][direction]["n_dir"] if part2: return len(inside) else: return pipe_len // 2 class Day(AOCDay): inputs = [ [ (4, "input10_test1"), (8, "input10_test2"), (6823, "input10_dennis"), (6864, "input10"), ], [ (4, "input10_test3"), (4, "input10_test4"), (8, "input10_test5"), (10, "input10_test6"), (415, "input10_dennis"), (349, "input10"), ], ] def parse_input(self) -> Grid: grid = Grid() start = None for y, line in enumerate(self.getInput()): for x, ch in enumerate(line): if ch == ".": continue if ch == "S": start = Coordinate(x, y) grid.set(Coordinate(x, y), ch) up = grid.get(start + Coordinate(0, -1)) down = grid.get(start + Coordinate(0, 1)) left = grid.get(start + Coordinate(-1, 0)) right = grid.get(start + Coordinate(1, 0)) if up in ["7", "|", "F"]: if left in ["L", "-", "F"]: grid.set(start, "J") elif right in ["J", "-", "7"]: grid.set(start, "L") else: grid.set(start, "|") elif down in ["L", "|", "J"]: if left in ["L", "-", "F"]: grid.set(start, "7") else: grid.set(start, "F") else: grid.set(start, "-") remove_clutter(grid, start) return grid def part1(self) -> Any: return walk(self.parse_input()) def part2(self) -> Any: return walk(self.parse_input(), True) if __name__ == "__main__": day = Day(2023, 10) day.run(verbose=True)