"""AOC 2020 Day 24""" import pathlib import time import re import operator import itertools TEST_INPUT = """sesenwnenenewseeswwswswwnenewsewsw neeenesenwnwwswnenewnwwsewnenwseswesw seswneswswsenwwnwse nwnwneseeswswnenewneswwnewseswneseene swweswneswnenwsewnwneneseenw eesenwseswswnenwswnwnwsewwnwsene sewnenenenesenwsewnenwwwse wenwwweseeeweswwwnwwe wsweesenenewnwwnwsenewsenwwsesesenwne neeswseenwwswnwswswnw nenwswwsewswnenenewsenwsenwnesesenew enewnwewneswsewnwswenweswnenwsenwsw sweneswneswneneenwnewenewwneswswnese swwesenesewenwneswnwwneseswwne enesenwswwswneneswsenwnewswseenwsese wnwnesenesenenwwnenwsewesewsesesew nenewswnwewswnenesenwnesewesw eneswnwswnwsenenwnwnwwseeswneewsenese neswnwewnwnwseenwseesewsenwsweewe wseweeenwnesenwwwswnew""" STEPMAP = { 'e': (1, 0), 'se': (1, 1), 'sw': (0, 1), 'w': (-1, 0), 'nw': (-1, -1), 'ne': (0, -1) } def read_input(input_path: str) -> str: """take input file path and return a str with the file's content""" with open(input_path, 'r') as input_file: input_data = input_file.read().strip() return input_data def extract(input_data: str) -> set: """take input data and return the appropriate data structure""" rexp_steps = re.compile(r'e|se|sw|w|nw|ne') tiles_steps = [rexp_steps.findall(line) for line in input_data.split('\n')] black_tiles = set() for steps in tiles_steps: dst_tile = find_dst_tile(steps) if dst_tile in black_tiles: black_tiles.remove(dst_tile) else: black_tiles.add(dst_tile) return black_tiles def find_dst_tile(steps: list) -> tuple: """calculate the destination tile based on the steps""" x_dst, y_dst = 0, 0 for step in steps: d_x, d_y = STEPMAP[step] x_dst += d_x y_dst += d_y return x_dst, y_dst def count_black_neighbors(tiles: set, x_tile: int, y_tile: int) -> int: """return the number of black adjacent tile of a given one""" return sum((x_tile+d_x, y_tile+d_y) in tiles for d_x, d_y in STEPMAP.values()) def calculate_floor_bounds(tiles: set) -> tuple: """return the floor boundaries""" min_x = min(map(operator.itemgetter(0), tiles)) - 1 min_y = min(map(operator.itemgetter(1), tiles)) - 1 max_x = max(map(operator.itemgetter(0), tiles)) + 2 max_y = max(map(operator.itemgetter(1), tiles)) + 2 return range(min_x, max_x), range(min_y, max_y) def flip_tiles(tiles: set) -> set: """calculate the new daily floor""" new_floor = set() for tile in itertools.product(*calculate_floor_bounds(tiles)): black_neighbors = count_black_neighbors(tiles, *tile) if tile in tiles and not (black_neighbors == 0 or black_neighbors > 2): new_floor.add(tile) elif tile not in tiles and black_neighbors == 2: new_floor.add(tile) return new_floor def part1(entries: set) -> int: """part1 solver""" return len(entries) def part2(entries: list) -> int: """part2 solver""" for _ in range(100): entries = flip_tiles(entries) return len(entries) def test_input_day_24(): """pytest testing function""" entries = extract(TEST_INPUT) assert part1(entries) == 10 assert part2(entries) == 2208 def test_bench_day_24(benchmark): """pytest-benchmark function""" benchmark(main) def main(): """main function""" input_path = str(pathlib.Path(__file__).resolve().parent.parent) + "/inputs/" + str(pathlib.Path(__file__).stem) start_time = time.time() input_data = read_input(input_path) entries = extract(input_data) print("Part 1: %s" % part1(entries)) print("Part 2: %s" % part2(entries)) end_time = time.time() print("Execution time: %f" % (end_time-start_time)) if __name__ == "__main__": main()