#!/usr/bin/env python3 from math import gcd from fractions import Fraction from itertools import product from functools import total_ordering from collections import defaultdict ASTEROID = '#' def zrange(x, y, step): if x == y == step == 0: def yzero(): while True: yield 0 return yzero() return range(x, y, step) def angle(v): if v.x == 0: return (2 * int(v.y > 0), 0) halve = 1 if v.x > 0 else 3 return (halve, Fraction(v.y, v.x)) def rebase(base, vectors): return (v - base for v in vectors if v != base) @total_ordering class Vector: def __init__(self, x, y): self.x = x self.y = y def __add__(self, other): return Vector(self.x + other.x, self.y + other.y) def __sub__(self, other): return self + Vector(-other.x, -other.y) def __eq__(self, other): return self.x == other.x and self.y == other.y def __lt__(self, other): return (self.x**2 + self.y**2) < (other.x**2 + other.y**2) def __str__(self): return str((self.x, self.y)) def __repr__(self): return str(self) class Map: def __init__(self, map): self.map = map def __getitem__(self, v): return self.map[v.y][v.x] def __str__(self): return '\n'.join(self.map) def is_asteroid(self, v): return self[v] == ASTEROID def vectors(self): X, Y = len(self.map[0]), len(self.map) return (Vector(x, y) for x, y in product(range(X), range(Y))) def asteroid_vectors(self): return filter(lambda v: self.is_asteroid(v), self.vectors()) def num_in_sight(self, v): vs = rebase(v, self.asteroid_vectors()) return len(set(map(angle, vs))) def best(self): return max((self.num_in_sight(v), v) for v in self.asteroid_vectors()) def vaporize_seq(self, base): vs = rebase(base, self.asteroid_vectors()) angles = defaultdict(list) for v in vs: angles[angle(v)].append(v) for ang in angles.keys(): angles[ang].sort() r = [] for i in range(max(map(len, angles.values()))): r += [angles[ang][i] + base for ang in sorted(angles.keys()) if i < len(angles[ang])] return r def preproc(puzzle_input): asteroid_map = Map(puzzle_input.split()) score, asteroid = asteroid_map.best() return asteroid_map, asteroid, score def partI(packed): _, _, score = packed return score def partII(packed): asteroid_map, asteroid, _ = packed v = asteroid_map.vaporize_seq(asteroid)[199] return 100*v.x + v.y from main import Tests tests = Tests() map1 = \ """.#..# ..... ##### ....# ...##""" map2 = \ """#.#...#.#. .###....#. .#....#... ##.#.#.#.# ....#.#.#. .##..###.# ..#...##.. ..##....## ......#... .####.###. """ map3 = \ """.#..##.###...####### ##.############..##. .#.######.########.# .###.#######.####.#. #####.##.#.##.###.## ..#####..#.######### #################### #.####....###.#.#.## ##.################# #####.##.###..####.. ..######..##.####### ####.##.####...##..# .#####..#.######.### ##...#.##########... #.##########.####### .####.#.###.###.#.## ....##.##.###..##### .#.#.###########.### #.#.#.#####.####.### ###.##.####.##.#..## """ tests.add(map1, partI=8) tests.add(map2, partI=35) tests.add(map3, partI=210, partII=802)