diff --git a/day24/day24.py b/day24/day24.py index be932b5..4880852 100644 --- a/day24/day24.py +++ b/day24/day24.py @@ -1,2 +1,64 @@ -INPUT = "day24/input.txt" -INPUT_SMALL = "day24/input-small.txt" +"""day24 solution""" +from typing import Optional + +from day24.lib.classes import Hailstone, Vector2 +from day24.lib.parsers import parse_input + +INPUT = ("day24/input.txt", Vector2(200000000000000, 400000000000000)) +INPUT_SMALL = ("day24/input-small.txt", Vector2(7, 27)) + + +def get_intersection_2d(left: Hailstone, right: Hailstone) -> Optional[Vector2]: + pos1 = left.position.xy + dir1 = left.trajectory.xy + pos2 = right.position.xy + dir2 = right.trajectory.xy + + determinant = (dir1.x * dir2.y) - (dir1.y * dir2.x) + + if determinant == 0: + return None # parallel + + t1 = ((pos2.x - pos1.x) * dir2.y - (pos2.y - pos1.y) * dir2.x) / determinant + t2 = ((pos2.x - pos1.x) * dir1.y - (pos2.y - pos1.y) * dir1.x) / determinant + + if t1 < 0: # in the past :( + return None + if t2 < 0: # in the past :( + return None + intersection_point = Vector2(pos1.x + t1 * dir1.x, pos1.y + t1 * dir1.y) + return intersection_point + + +def within_2d(point: Vector2, min_max: Vector2) -> bool: + return min_max.x <= point.x <= min_max.y and min_max.x <= point.y <= min_max.y + + +def part1(hailstones: list[Hailstone], valid_range: Vector2) -> int: + result = 0 + print(len(hailstones)) + left: Hailstone + right: Hailstone + for index, left in enumerate(hailstones[:-1]): + for right in hailstones[index + 1 :]: + intersection: Optional[Vector2] = get_intersection_2d(left, right) + + if intersection is None: + continue + + if within_2d(intersection, valid_range): + result += 1 + + return result + + +def main() -> None: + input_data, valid_range = INPUT + + hailstones: list[Hailstone] = parse_input(input_data) + print(len(hailstones)) + print(part1(hailstones, valid_range)) + + +if __name__ == "__main__": + main() diff --git a/day24/lib/classes.py b/day24/lib/classes.py index 24c5419..1125a86 100644 --- a/day24/lib/classes.py +++ b/day24/lib/classes.py @@ -1,11 +1,21 @@ from dataclasses import dataclass -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class Vector3: - x: int - y: int - z: int + x: float + y: float + z: float + + @property + def xy(self) -> "Vector2": + return Vector2(self.x, self.y) + + +@dataclass(frozen=True, slots=True) +class Vector2: + x: float + y: float @dataclass(frozen=True) diff --git a/day24/tests/test_day24.py b/day24/tests/test_day24.py new file mode 100644 index 0000000..c80001e --- /dev/null +++ b/day24/tests/test_day24.py @@ -0,0 +1,30 @@ +from day24.day24 import INPUT_SMALL, get_intersection_2d, part1, within_2d +from day24.lib.classes import Hailstone, Vector2, Vector3 +from day24.lib.parsers import parse_input + + +def test_part1() -> None: + file_path, valid_range = INPUT_SMALL + hailstones: list[Hailstone] = parse_input(file_path) + assert part1(hailstones, valid_range) == 2 + + +def test_get_intersection_2d() -> None: + hailstone_a = Hailstone(Vector3(20, 25, 34), Vector3(-2, -2, -4)) + hailstone_b = Hailstone(Vector3(12, 31, 28), Vector3(-1, -2, -1)) + + assert get_intersection_2d(hailstone_a, hailstone_b) == Vector2(-2, 3) + + +def test_within_2d() -> None: + valid_range = Vector2(7, 27) + v1 = Vector2(14 + 1 / 3, 15 + 1 / 3) + v2 = Vector2(11 + 6 / 9, 16 + 6 / 9) + v3 = Vector2(6.2, 19.4) + v4 = Vector2(-6, -5) + v5 = Vector2(-2, 3) + assert within_2d(v1, valid_range) + assert within_2d(v2, valid_range) + assert not within_2d(v3, valid_range) + assert not within_2d(v4, valid_range) + assert not within_2d(v5, valid_range) diff --git a/day24/tests/test_parsers.py b/day24/tests/test_parsers.py index 65a074b..8407fb7 100644 --- a/day24/tests/test_parsers.py +++ b/day24/tests/test_parsers.py @@ -16,7 +16,8 @@ def test_vector3() -> None: def test_parser() -> None: - hailstones: list[Hailstone] = parse_input(INPUT_SMALL) + file_path, _ = INPUT_SMALL + hailstones: list[Hailstone] = parse_input(file_path) assert len(hailstones) == 5 assert hailstones[0].position == Vector3(19, 13, 30)