diff --git a/puzzles/solutions/2022/d09/consts.py b/puzzles/solutions/2022/d09/consts.py new file mode 100644 index 00000000..7492e5b5 --- /dev/null +++ b/puzzles/solutions/2022/d09/consts.py @@ -0,0 +1,3 @@ +DIRECTION_TO_STEP = {"R": (1, 0), "U": (0, 1), "L": (-1, 0), "D": (0, -1)} +ADJACENT_DISTANCE = 1 +FAR_DISTANCE = 2 diff --git a/puzzles/solutions/2022/d09/knot.py b/puzzles/solutions/2022/d09/knot.py new file mode 100644 index 00000000..b2059782 --- /dev/null +++ b/puzzles/solutions/2022/d09/knot.py @@ -0,0 +1,57 @@ +import dataclasses +import math + +from typing import Self + +import consts + + +@dataclasses.dataclass +class Knot: + """A knot on a rope which can move.""" + + row: int + column: int + + def move(self, step: tuple[int, int]) -> None: + """ + Move the knot by the given step. + :param step: pair of (row, column) movement + """ + x, y = step + self.row += x + self.column += y + + def is_touching(self, other: Self) -> bool: + """ + :param other: other knot + :return: whether this knot and the other knot are touching + """ + return ( + abs(self.row - other.row) < consts.FAR_DISTANCE + and abs(self.column - other.column) < consts.FAR_DISTANCE + ) + + def move_to_other(self, other: Self) -> None: + """ + Move this knot, so it will be touching the other knot. + :param other: other knot to move towards. + """ + if self.is_touching(other): + return + + row_distance = self.row - other.row + column_distance = self.column - other.column + + # If the distance is "far", we want to move the knot, so it will be on the same row/column, but not overlapping, + # so the distance to move should be 1. + # If the distance is "adjacent", it means we want to move diagonally (remember -- the knots are not touching, + # otherwise, we would return immediately at the beginning of this method), so the distance to move + # stays 1, too. + # If the distance is 0, no movement should occur, the condition is falsy, so the distance to move stays 0. + if abs(row_distance) == consts.FAR_DISTANCE: + row_distance = math.copysign(consts.ADJACENT_DISTANCE, row_distance) + if abs(column_distance) == consts.FAR_DISTANCE: + column_distance = math.copysign(consts.ADJACENT_DISTANCE, column_distance) + + self.move((-int(row_distance), -int(column_distance))) diff --git a/puzzles/solutions/2022/d09/p1.py b/puzzles/solutions/2022/d09/p1.py new file mode 100644 index 00000000..206fec78 --- /dev/null +++ b/puzzles/solutions/2022/d09/p1.py @@ -0,0 +1,53 @@ +import sys +from typing import Iterator + +from knot import Knot +import consts + + +def get_steps(input_text: str) -> Iterator[tuple[int, int]]: + """ + :param input_text: puzzle input + :return: sequence of steps, one by one + """ + steps = [] + for step_count in input_text.splitlines(): + direction, count = step_count.split() + step = consts.DIRECTION_TO_STEP[direction] + count = int(count) + yield from count * [step] + return tuple(steps) + + +def get_visited_positions_amount( + steps: Iterator[tuple[int, int]], knots_amount: int +) -> int: + """ + :param steps: sequence of steps, one by one + :param knots_amount: amount of knots in the rope + :return: number of positions the tail of the rope visits at least once + """ + knots = [Knot(0, 0) for _ in range(knots_amount)] + head = knots[0] + tail = knots[-1] + visited_positions = set() + for step in steps: + head.move(step) + for index in range(1, knots_amount): + # Move each knot according to the knot it follows. + knots[index].move_to_other(knots[index - 1]) + visited_positions.add((tail.row, tail.column)) + return len(visited_positions) + + +def get_answer(input_text: str): + """Return the number of positions the tail of the rope visits at least once, when the rope has total 2 knots.""" + steps = get_steps(input_text) + return get_visited_positions_amount(steps, 2) + + +if __name__ == "__main__": + try: + print(get_answer(sys.argv[1])) + except IndexError: + pass # Don't crash if no input was passed through command line arguments. diff --git a/puzzles/solutions/2022/d09/p2.py b/puzzles/solutions/2022/d09/p2.py new file mode 100644 index 00000000..e34bc55d --- /dev/null +++ b/puzzles/solutions/2022/d09/p2.py @@ -0,0 +1,16 @@ +import sys + +import p1 + + +def get_answer(input_text: str): + """Return the number of positions the tail of the rope visits at least once, when the rope has total 10 knots.""" + steps = p1.get_steps(input_text) + return p1.get_visited_positions_amount(steps, 10) + + +if __name__ == "__main__": + try: + print(get_answer(sys.argv[1])) + except IndexError: + pass # Don't crash if no input was passed through command line arguments.