#! /usr/bin/env python3 from typing import Iterable from typing import Tuple from typing import Optional from expanding import AutoExpanding from expanding import GridItem from expanding import Point from expanding import World from expanding import add_points D2P = { "se": Point(1, -1), "sw": Point(-1, -1), "ne": Point(1, 1), "nw": Point(-1, 1), "w": Point(-2, 0), "e": Point(2, 0), } def directions_to_point(s: str) -> Point: point = Point(0, 0) direction = "" for c in s: direction += c if direction in D2P: point = add_points(point, D2P[direction]) direction = "" if direction != "": raise ValueError("Something left over unexpectedly") return point class Tile(GridItem): def __init__(self, loc: Point, world: World, is_black: bool = False): # Give this tile a unique id for tracking later self.is_black = is_black def _tock(): # print("Default tock") pass self.tock = _tock super().__init__(loc, world) def flip(self): self.is_black = not self.is_black def neighbors(self, expand=False) -> Iterable["Tile"]: for diff in D2P.values(): neighbor_point = add_points(self._loc, diff) neighbor = self._world.get_point(neighbor_point, expand=expand) if neighbor: yield neighbor def tick(self): num_black = count_black(self.neighbors(expand=True)) if self.is_black and (num_black == 0 or num_black > 2): self.tock = self.flip if not self.is_black and num_black == 2: self.tock = self.flip def count_black(tiles: Iterable[Tile]) -> int: return sum(map(lambda x: 1 if x.is_black else 0, tiles)) class TileRow(AutoExpanding): def __init__(self, y: int, world: World): self._y = y self._world = world super().__init__() def _item_factory(self, x: int) -> GridItem: return Tile(Point(x, self._y), self._world) def __repr__(self) -> str: return "[" + " ".join((str(c) for c in self._l)) + "]" def to_str(self) -> str: return "".join((c.to_str() for c in self._l)) class Floor(World): def __init__(self): super().__init__() def _item_factory(self, y: int) -> TileRow: return TileRow(y, self) def iter_items(self) -> Iterable[GridItem]: for r in self: for t in r: yield t def get_point(self, p: Point, expand=True) -> Optional[GridItem]: try: return self.get( p.y, expand=expand, ).get( p.x, expand=expand, ) except IndexError: return None def get_all_bounds(self) -> Tuple[Point, Point]: min_y, max_y = self.get_bounds() min_x, max_x = 0, 0 for r in self: if r.min_index() < min_x: min_x = r.min_index() if r.max_index() > max_x: max_x = r.max_index() return Point(min_x, min_y, 0), Point(max_x, max_y, 0) def true_up(self): min_point, max_point = self.get_all_bounds() for r in self: r.resize(min_point.x, max_point.x) def expand_all(self, i: int): min_point, max_point = self.get_all_bounds() min_point = add_points(min_point, Point(-i, -i)) max_point = add_points(max_point, Point(i, i)) self.get_point(min_point, expand=True) self.get_point(max_point, expand=True) def tick(self): for t in self.iter_items(): t.tick() def tock(self): for t in self.iter_items(): t.tock() def sample(): print(directions_to_point("nwwswee")) seen = {Point(0, 0)} floor = Floor() for line in ( "sesenwnenenewseeswwswswwnenewsewsw", "neeenesenwnwwswnenewnwwsewnenwseswesw", "seswneswswsenwwnwse", "nwnwneseeswswnenewneswwnewseswneseene", "swweswneswnenwsewnwneneseenw", "eesenwseswswnenwswnwnwsewwnwsene", "sewnenenenesenwsewnenwwwse", "wenwwweseeeweswwwnwwe", "wsweesenenewnwwnwsenewsenwwsesesenwne", "neeswseenwwswnwswswnw", "nenwswwsewswnenenewsenwsenwnesesenew", "enewnwewneswsewnwswenweswnenwsenwsw", "sweneswneswneneenwnewenewwneswswnese", "swwesenesewenwneswnwwneseswwne", "enesenwswwswneneswsenwnewswseenwsese", "wnwnesenesenenwwnenwsewesewsesesew", "nenewswnwewswnenesenwnesewesw", "eneswnwswnwsenenwnwnwwseeswneewsenese", "neswnwewnwnwseenwseesewsenwsweewe", "wseweeenwnesenwwwswnew", ): p = directions_to_point(line) print(f"Parsed point {p}") if p in seen: print("seen!") seen.add(p) floor.get_point(p, expand=True).flip() black = count_black(floor.iter_items()) print("Day 0") print(f"Number black is {black}") for day in range(100): floor.expand_all(1) floor.true_up() floor.tick() floor.tock() black = count_black(floor.iter_items()) total = sum(map(lambda x: 1, floor.iter_items())) print(f"Day {day+1}: {black} / {total}") def part1(): floor = Floor() with open("input.txt") as f: for line in f: line = line.strip() p = directions_to_point(line) floor.get_point(p, expand=True).flip() black = count_black(floor.iter_items()) print(f"Number black is {black}") def part2(): floor = Floor() with open("input.txt") as f: for line in f: line = line.strip() p = directions_to_point(line) floor.get_point(p, expand=True).flip() black = count_black(floor.iter_items()) print("Day 0") print(f"Number black is {black}") for day in range(1, 11): floor.tick() floor.tock() black = count_black(floor.iter_items()) print(f"Day {day}: {black}") if __name__ == "__main__": sample() # part1() # part2()