import argparse import enum import logging from sys import stdout from typing import List, Tuple logger = logging.Logger(__name__) logger_2 = logging.Logger(f"{__name__}_2") formatter = logging.Formatter('[%(asctime)s][%(levelname)s] %(message)s') sh = logging.StreamHandler(stdout) sh.setLevel(logging.INFO) sh.setFormatter(formatter) fh = logging.FileHandler("./day02-2.log", mode="w", encoding="utf-8") fh_2 = logging.FileHandler("./day02-2_round2.log", mode="w", encoding="utf-8") fh.setLevel(logging.DEBUG) fh.setFormatter(formatter) fh_2.setLevel(logging.DEBUG) fh_2.setFormatter(formatter) logger.addHandler(sh) logger.addHandler(fh) logger_2.addHandler(fh_2) class Direction(enum.Enum): UP = 0 RIGHT = 1 DOWN = 2 LEFT = 3 class Guard: def __init__(self, grid: List[List[str]], initial_x: int = None, initial_y: int = None, initial_dir: int = None, test_mode: bool = False) -> object: self.grid = grid self.height = len(self.grid) self.width = len(self.grid[0]) logger.info(f"Received a grid with {self.height} lines and {self.width} characters per line.") self.initial_x = initial_x self.x = self.initial_x self.initial_y = initial_y self.y = self.initial_y self.initial_dir = initial_dir self.dir = self.initial_dir self.test_mode = test_mode self.visited = set() self.traveled = 1 def whereami(self) -> Tuple[int,int]: # Identify the guard's initial position and direction in the grid breakout = False x,y,dir = None,None,None for j, line in enumerate(self.grid): if breakout: break for i, char in enumerate(line): if char in ["^",">","<","v"]: x, y = i, j match char: case "^": dir = Direction.UP case ">": dir = Direction.RIGHT case "v": dir = Direction.DOWN case "<": dir = Direction.LEFT case "_": raise ValueError(f"char must be one of '^','>','v','<', received {char}") breakout = True break self.initial_x = x self.x = self.initial_x self.initial_y = y self.y = self.initial_y self.initial_dir = dir self.dir = self.initial_dir return (x,y) def proceed(self): if self.dir is None: logger.error("You can't move until you have a direction set.") raise ValueError logger.info(f"Proceeding {self.dir}") match self.dir: case Direction.UP: return self.go_north() case Direction.RIGHT: return self.go_east() case Direction.DOWN: return self.go_south() case Direction.LEFT: return self.go_west() case _: logger.error(f"Unknown direction! {self.dir}") raise ValueError def go_north(self) -> bool: path = [self.grid[y][self.x] for y in range(self.y-1, -1, -1)] logger.info(f"Path created from {self.x},{self.y} to {self.x},{0}: {path}.") exited = "#" not in path if exited: blocked = 999999 else: blocked = path.index("#") if self.test_mode: logger.info(f"Found a blocker at ({self.x}, {self.y-blocked-1}).") for i, char in enumerate(path[:blocked+1]): j = i + 1 if exited else i if self.test_mode: logger.info(f"Walked to ({self.x}, {self.y-j}).") self.visited.add((self.x, self.y-j)) self.traveled += 1 new_y = self.y - (blocked) self.y = new_y self.dir = Direction.RIGHT return exited def go_east(self) -> bool: path = [self.grid[self.y][x] for x in range(self.x+1, self.width)] logger.info(f"Path created from {self.x},{self.y} to {self.width-1},{self.y}: {path}.") exited = "#" not in path if exited: blocked = 999999 else: blocked = path.index("#") if self.test_mode: logger.info(f"Found a blocker at ({self.x+blocked+1}, {self.y}).") for i, char in enumerate(path[:blocked+1]): j = i + 1 if exited else i if self.test_mode: logger.info(f"Walked to ({self.x+j}, {self.y}).") self.visited.add((self.x+j, self.y)) self.traveled += 1 new_x = self.x + (blocked) self.x = new_x self.dir = Direction.DOWN return exited def go_south(self) -> bool: path = [self.grid[y][self.x] for y in range(self.y+1, self.height)] logger.info(f"Path created from {self.x},{self.y} to {self.x},{self.height}: {path}.") exited = "#" not in path if exited: blocked = 999999 else: blocked = path.index("#") if self.test_mode: logger.info(f"Found a blocker at ({self.x}, {self.y+blocked+1}).") for i, char in enumerate(path[:blocked+1]): j = i + 1 if exited else i if self.test_mode: logger.info(f"Walked to ({self.x}, {self.y+j}).") self.visited.add((self.x, self.y+j)) self.traveled += 1 new_y = self.y + (blocked) self.y = new_y self.dir = Direction.LEFT return exited def go_west(self) -> bool: path = [self.grid[self.y][x] for x in range(self.x-1, -1, -1)] logger.info(f"Path created from {self.x},{self.y} to {0},{self.y}: {path}.") exited = "#" not in path if exited: blocked = 999999 else: blocked = path.index("#") if self.test_mode: logger.info(f"Found a blocker at ({self.x-blocked-1}, {self.y}).") for i, char in enumerate(path[:blocked+1]): j = i + 1 if exited else i if self.test_mode: logger.info(f"Walked to ({self.x-j}, {self.y}).") self.visited.add((self.x-j, self.y)) self.traveled += 1 new_x = self.x - (blocked) self.x = new_x self.dir = Direction.UP return exited def pathfind(self): exited = False while not exited: exited = self.proceed() logger.info(f"Found an exit after {self.traveled} steps!") parser = argparse.ArgumentParser() parser.add_argument("--test", action="store_true", help="Do a test run instead of the full puzzle") def main061(): args = parser.parse_args() if args.test: input_grid = """....#..... .........# .......... ..#....... .......#.. .......... .#..^..... ........#. #......... ......#...""" grid = [list(l) for l in input_grid.split("\n")] print("\n".join(["".join(line) for line in grid])) else: with open("input06.txt", "r", encoding="utf-8") as f: grid = [list(l) for l in f.readlines()] guard = Guard(grid, test_mode=args.test) logger.info(f"Guard has been created: x:{guard.x}, y:{guard.y}, dir:{guard.dir}, test_mode:{guard.test_mode}, width:{guard.width}, height:{guard.height}.") guard_position = guard.whereami() logger.info(f"Starting guard's walk at {guard_position}.") guard.pathfind() logger.info(f"The guard visited {len(guard.visited)} unique positions.") if __name__ == "__main__": main061()