from collections import deque from tools.aoc import AOCDay from tools.coordinate import Coordinate from tools.grid import Grid from typing import Any VALID_NEIGHBOURS = { Coordinate(1, 0): { "check": ["F", "-", "L", "S"], "valid": ["7", "-", "J"], }, Coordinate(0, 1): { "check": ["7", "|", "F", "S"], "valid": ["J", "|", "L"], }, Coordinate(-1, 0): { "check": ["J", "-", "7", "S"], "valid": ["F", "-", "L"], }, Coordinate(0, -1): { "check": ["L", "|", "J", "S"], "valid": ["F", "|", "7"], }, } def remove_clutter(grid: Grid, start: Coordinate): pipe_coords = set() pipe_coords.add(start) q = deque() q.append(start) while q: cur = q.pop() for c, v in VALID_NEIGHBOURS.items(): dc = cur + c if dc not in pipe_coords and grid.get(cur) in v["check"] and grid.get(dc) in v["valid"]: pipe_coords.add(dc) q.append(dc) for c in grid.getActiveCells(): if c not in pipe_coords: grid.set(c, False) def get_farthest(grid: Grid, start: Coordinate) -> int: q = deque() q.append((0, start)) visited = set() max_dist = 0 while q: dist, cur = q.popleft() if cur in visited: continue visited.add(cur) for c, v in VALID_NEIGHBOURS.items(): dc = cur + c if dc not in visited and grid.get(cur) in v["check"] and grid.get(dc) in v["valid"]: q.append((dist + 1, dc)) max_dist = max(max_dist, dist + 1) return max_dist 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) -> int: 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), }, }, } 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" direction = Coordinate(0, 1) outside = get_area(grid, cur + Coordinate(0, -1)) inside = set() visited = set() while cur not in visited: visited.add(cur) cur += direction c = grid.get(cur) 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"] return len(inside) class Day(AOCDay): inputs = [ [ (4, "input10_test1"), (8, "input10_test2"), (6864, "input10"), ], [ (4, "input10_test3"), (4, "input10_test4"), (8, "input10_test5"), (10, "input10_test6"), (349, "input10"), ], ] def parse_input(self) -> (Grid, Coordinate): 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, start def part1(self) -> Any: grid, start = self.parse_input() return get_farthest(grid, start) def part2(self) -> Any: grid, _ = self.parse_input() return walk(grid) if __name__ == "__main__": day = Day(2023, 10) day.run(verbose=True)