diff --git a/day10/py/main.py b/day10/py/main.py index 34ec6b2..4f8aa24 100644 --- a/day10/py/main.py +++ b/day10/py/main.py @@ -1,12 +1,14 @@ import sys +import copy import fractions import math -from typing import Tuple, Set +from typing import List, Iterable, Tuple + ASTEROID_CHAR = '#' SPACE_CHAR = '.' - +# Get the coordinates of all items in a line def get_items_in_slope(space_map: Tuple[Tuple[str]], start_row: int, start_col: int, rise: int, run: int) -> Tuple[Tuple[int, int]]: row = start_row col = start_col @@ -24,24 +26,34 @@ def get_items_in_slope(space_map: Tuple[Tuple[str]], start_row: int, start_col: return tuple(items) -def search_for_visible_in_all_directions(space_map: Tuple[Tuple[str, ...]], start_row: int, start_col: int, rise: int, run: int) -> Set[Tuple[int, int]]: - asteroids = set() - for i in (-1, 1): - for j in (-1, 1): - line_items = get_items_in_slope(space_map, start_row, start_col, i * rise, j * run) - for row, col in line_items: - if space_map[row][col] == ASTEROID_CHAR: - asteroids.add((row, col)) - break +# Spread a set of coordinates into all quadrants (e.g. (1,1) will produce (1,1) (1,-1) (-1,1) (-1,-1) +def put_items_in_all_quadrants(items: Iterable[Tuple[int, int]]) -> List[Tuple[int, int]]: + mirrored = set() + for item in items: + for i in (-1, 1): + for j in (-1, 1): + mirrored.add((i * item[0], j * item[1])) - return asteroids + return list(mirrored) -def get_visible_asteroid_count(space_map: Tuple[Tuple[str, ...]], start_row: int, start_col: int) -> int: - asteroids = set() +# Start a list of slopes clockwise, starting at pi/2 +def sort_slopes_clockwise(items: List[Tuple[int, int]]) -> List[Tuple[int, int]]: + # math.atan(-x[0]/abs(x[0])) allows us to get the correct positive/negative infinity for arctan + # (remember, our coords are flipped from a standard coordinate plane) + sort_key = lambda x: math.atan(-x[0]/abs(x[0]) * math.inf if x[1] == 0 else -x[0]/x[1]) + quad_1_3_items = sorted((item for item in items if item[1] >= 0), reverse=True, key=sort_key) + quad_2_4_items = sorted((item for item in items if item[1] < 0), reverse=True, key=sort_key) + + return quad_1_3_items + quad_2_4_items + + +def generate_all_slopes(height: int, width: int) -> Tuple[Tuple[int, int]]: + # Get all slopes in the first quadrant + quad_slopes = set() used_slopes = set() - for rise in range(len(space_map)): - for run in range(len(space_map[0])): + for rise in range(height): + for run in range(width): effective_rise, effective_run = rise, run if rise == 0 and run == 0: continue @@ -55,23 +67,58 @@ def get_visible_asteroid_count(space_map: Tuple[Tuple[str, ...]], start_row: int continue used_slopes.add(slope_fraction) - found_asteroids = search_for_visible_in_all_directions(space_map, start_row, start_col, effective_rise, effective_run) - asteroids = asteroids.union(found_asteroids) + quad_slopes.add((effective_rise, effective_run)) - return len(asteroids) + # Spread them across the rest of the coordinates + slopes = put_items_in_all_quadrants(quad_slopes) + + return sort_slopes_clockwise(slopes) -def part1(space_map: Tuple[Tuple[int, ...]]): +# Get all asteroids that are visible from a certian point, marking whether to destroy them on scan or not +def get_visible_asteroids(space_map: List[List[int]], start_row: int, start_col: int, destroy=False) -> List[Tuple[int, int]]: + asteroids = [] + slopes = generate_all_slopes(len(space_map), len(space_map[0])) + for slope in slopes: + rise, run = slope + line_items = get_items_in_slope(space_map, start_row, start_col, rise, run) + for row, col in line_items: + if space_map[row][col] == ASTEROID_CHAR: + if destroy: + space_map[row][col] = SPACE_CHAR + if (row, col) not in asteroids: + asteroids.append((row, col)) + break + + return asteroids + + +def part1(input_space_map: List[List[str]]) -> Tuple[Tuple[int, int], int]: + space_map = copy.deepcopy(input_space_map) best_count = None + best_pos = None for i, row in enumerate(space_map): for j, item in enumerate(row): if item != ASTEROID_CHAR: continue - visible_asteroid_count = get_visible_asteroid_count(space_map, i, j) + visible_asteroid_count = len(get_visible_asteroids(space_map, i, j)) if best_count is None or visible_asteroid_count > best_count: + best_pos = (i, j) best_count = visible_asteroid_count - return best_count + return best_pos, best_count + + +def part2(input_space_map: Tuple[Tuple[str, ...]], station_pos: Tuple[int, int]) -> int: + space_map = copy.deepcopy(input_space_map) + destroyed_asteroids = [] + station_row, station_col = station_pos + while len(destroyed_asteroids) < 200: + destroyed_asteroids += get_visible_asteroids(space_map, station_row, station_col, True) + + asteroid = destroyed_asteroids[199] + + return asteroid[1] * 100 + asteroid[0] if __name__ == "__main__": @@ -80,10 +127,11 @@ if __name__ == "__main__": sys.exit(1) with open(sys.argv[1]) as f: - inputs = tuple(tuple(line.strip()) for line in f.readlines()) + inputs = [list(line.strip()) for line in f.readlines()] for row in inputs: print(''.join(row)) - # print(get_visible_asteroid_count(inputs, 0, 1)) - print(part1(inputs)) + best_pos, best_count = part1(inputs) + print(best_count) + print(part2(inputs, best_pos))