|
import functools |
|
import itertools |
|
import os |
|
import typing |
|
|
|
def get_new_coords(guard: dict, inp_map: list) -> typing.Optional[tuple]: |
|
dx, dy = DIRECTIONS[guard["dir"]]["x"], DIRECTIONS[guard["dir"]]["y"] |
|
new_x, new_y = guard["x"] + dx, guard["y"] + dy |
|
|
|
if 0 <= new_x < COL_COUNT and 0 <= new_y < ROW_COUNT: |
|
return new_x, new_y |
|
|
|
return None # Out of bounds |
|
|
|
@functools.lru_cache |
|
def rotate_direction(current_dir: str) -> str: |
|
return POSSIBLE_DIRECTIONS[(POSSIBLE_DIRECTIONS.index(current_dir) + 1) % POSSIBLE_DIRECTIONS_LEN] |
|
|
|
def part1(inp_map: list, guard: dict) -> int: |
|
distinct_positions = {(guard["x"], guard["y"])} # Use tuple directly for coordinates |
|
|
|
guard = guard.copy() |
|
|
|
while True: |
|
new_coords = get_new_coords(guard, inp_map) |
|
|
|
if new_coords is None: # Out of bounds |
|
break |
|
|
|
x, y = new_coords |
|
|
|
if inp_map[y][x] == "#": |
|
guard["dir"] = rotate_direction(guard["dir"]) |
|
else: |
|
guard["x"], guard["y"] = x, y |
|
distinct_positions.add((x, y)) |
|
|
|
return distinct_positions |
|
|
|
def part2(obstacle_positions: set, inp_map: list, guard: dict) -> int: |
|
matches = 0 |
|
|
|
for x, y in obstacle_positions: |
|
if inp_map[y][x] not in DIRECTIONS and inp_map[y][x] != "#": |
|
# Modify map in-place for obstruction and restore after |
|
original_char = inp_map[y][x] |
|
inp_map[y][x] = "#" |
|
|
|
current_guard = { |
|
"x": guard["x"], |
|
"y": guard["y"], |
|
"dir": guard["dir"], |
|
} |
|
distinct_states = set() |
|
|
|
while True: |
|
new_coords = get_new_coords(current_guard, inp_map) |
|
|
|
if new_coords is None: # Out of bounds |
|
break |
|
|
|
cx, cy = new_coords |
|
|
|
if inp_map[cy][cx] == "#": |
|
current_guard["dir"] = rotate_direction(current_guard["dir"]) |
|
|
|
if (current_guard["x"], current_guard["y"], current_guard["dir"]) in distinct_states: |
|
matches += 1 |
|
break |
|
|
|
distinct_states.add((current_guard["x"], current_guard["y"], current_guard["dir"])) |
|
else: |
|
current_guard["x"], current_guard["y"] = cx, cy |
|
|
|
# Restore map to original state |
|
inp_map[y][x] = original_char |
|
|
|
return matches |
|
|
|
DIRECTIONS: typing.Final[dict] = { # Directions in order of rotation |
|
"^": {"x": 0, "y": -1}, |
|
">": {"x": 1, "y": 0}, |
|
"v": {"x": 0, "y": 1}, |
|
"<": {"x": -1, "y": 0}, |
|
} |
|
POSSIBLE_DIRECTIONS = list(DIRECTIONS.keys()) |
|
POSSIBLE_DIRECTIONS_LEN = len(POSSIBLE_DIRECTIONS) |
|
|
|
path = os.path.split(__file__)[0] |
|
|
|
with open(f"{path}/input.txt", "r") as f: |
|
inp_raw = f.read() |
|
|
|
inp_map = [list(line) for line in inp_raw.split("\n")] |
|
guard = None |
|
|
|
ROW_COUNT = len(inp_map) |
|
COL_COUNT = len(inp_map[0]) |
|
|
|
for y, line in enumerate(inp_map): |
|
for x, char in enumerate(line): |
|
if char in DIRECTIONS: |
|
guard = {"x": x, "y": y, "dir": char} |
|
break |
|
|
|
part_1_data = part1(inp_map, guard) |
|
|
|
print(f"Part 1: {len(part_1_data)}") |
|
|
|
part_2_data = part2(part_1_data, inp_map, guard) |
|
|
|
print(f"Part 2: {part_2_data}") |