diff --git a/2024/src/aoc/days/day15.py b/2024/src/aoc/days/day15.py new file mode 100644 index 0000000..4f4245f --- /dev/null +++ b/2024/src/aoc/days/day15.py @@ -0,0 +1,167 @@ +import itertools + +import numpy + +from . import SeparateRunner + + +def parse_input(data: str) -> tuple[numpy.array, str]: + grid, steps = data.split("\n\n") + + grid_split = numpy.array([list(line) for line in grid.split("\n")]) + + steps = "".join(steps.split("\n")) + + return grid_split, steps + + +class DayRunner(SeparateRunner): + @classmethod + def part1(cls, input: str) -> None: + grid, steps = parse_input(input) + + y, x = numpy.where(grid == "@") + x, y = x[0], y[0] + + for c in steps: + match c: + case "^": + dx, dy = 0, -1 + case ">": + dx, dy = 1, 0 + case "<": + dx, dy = -1, 0 + case "v": + dx, dy = 0, 1 + case other: + raise ValueError(f"Invalid movement: {other}") + + match grid[y + dy, x + dx]: + case "#": + continue + case "O": + crashed = False + for dist in itertools.count(2): + match grid[y + dist * dy, x + dist * dx]: + case "O": + continue + case "#": + crashed = True + break + case _: + crashed = False + break + + if crashed: + continue + + grid[y + dist * dy, x + dist * dx] = "O" + case _: + pass + + grid[y, x] = "." + x += dx + y += dy + grid[y, x] = "@" + + stones = numpy.where(grid == "O") + + return sum(100 * y + x for y, x in zip(*stones)) + + @classmethod + def part2(cls, input: str) -> None: + input = input.replace(".", "..") + input = input.replace("#", "##") + input = input.replace("O", "[]") + input = input.replace("@", "@.") + + grid, steps = parse_input(input) + + y, x = numpy.where(grid == "@") + x, y = x[0], y[0] + + for c in steps: + match c: + case "^": + dx, dy = 0, -1 + case ">": + dx, dy = 1, 0 + case "<": + dx, dy = -1, 0 + case "v": + dx, dy = 0, 1 + case other: + raise ValueError(f"Invalid movement: {other}") + + match grid[y + dy, x + dx]: + case "#": + continue + case "]" | "[": + crashed = False + if dy == 0: + # easy case: just move linearly + for dist in itertools.count(2): + match grid[y, x + dist * dx]: + case "[" | "]": + continue + case "#": + crashed = True + break + case _: + break + + if crashed: + continue + + # shuffle all grid points one over + for steps in range(dist, 1, -1): + grid[y, x + dx * steps] = grid[y, x + dx * (steps - 1)] + else: + if grid[y + dy, x] == "[": + to_check = {x, x + 1} + else: + to_check = {x, x - 1} + + moving_stones = [to_check] + + for dist in itertools.count(2): + to_check_next = set() + + for cx in to_check: + match grid[y + dist * dy, cx]: + case "#": + crashed = True + break + case "[": + to_check_next.add(cx) + to_check_next.add(cx + 1) + case "]": + to_check_next.add(cx) + to_check_next.add(cx - 1) + case _: + continue + + if crashed or not to_check_next: + break + moving_stones.append(to_check_next) + to_check = to_check_next + + if crashed: + continue + + for steps in range(len(moving_stones), 0, -1): + dist = steps + 1 + for cx in moving_stones[steps - 1]: + grid[y + dy * dist, cx] = grid[y + dy * (dist - 1), cx] + grid[y + dy * (dist - 1), cx] = "." + case _: + pass + + grid[y, x] = "." + x += dx + y += dy + grid[y, x] = "@" + + stones = numpy.where(grid == "[") + + return sum(100 * y + x for y, x in zip(*stones)) diff --git a/2024/tests/samples/15.1.txt b/2024/tests/samples/15.1.txt new file mode 100644 index 0000000..84cf1fb --- /dev/null +++ b/2024/tests/samples/15.1.txt @@ -0,0 +1,21 @@ +########## +#..O..O.O# +#......O.# +#.OO..O.O# +#..O@..O.# +#O#..O...# +#O..O..O.# +#.OO.O.OO# +#....O...# +########## + +^v>^vv^v>v<>v^v<<><>>v^v^>^<<<><^ +vvv<<^>^v^^><<>>><>^<<><^vv^^<>vvv<>><^^v>^>vv<>v<<<^<^^>>>^<>vv>v^v^<>><>>>><^^>vv>v<^^^>>v^v^<^^>v^^>v^<^v>v<>>v^v^v^^<^^vv< +<>^^^^>>>v^<>vvv^>^^^vv^^>v<^^^^v<>^>vvvv><>>v^<<^^^^^ +^><^><>>><>^^<<^^v>>><^^>v>>>^v><>^v><<<>vvvv>^<><<>^>< +^>><>^v<><^vvv<^^<><^v<<<><<<^^<^>>^<<<^>>^v^>>^v>vv>^<<^v<>><<><<>v<^vv<<<>^^v^>^^>>><<^v>>v^v><^^>>^<>vv^ +<><^^>^^^<>^vv<<^><<><<><<<^^<<<^<<>><<><^^^>^^<>^>v<> +^^>vv<^v^v^<>^^^>>>^^vvv^>vvv<>>>^<^>>>>>^<<^v>^vvv<>^<>< +v^^>>><<^^<>>^v^v^<<>^<^v^v><^<<<><<^vv>>v>v^<<^ diff --git a/2024/tests/samples/15.2.txt b/2024/tests/samples/15.2.txt new file mode 100644 index 0000000..8163605 --- /dev/null +++ b/2024/tests/samples/15.2.txt @@ -0,0 +1,10 @@ +######## +#..O.O.# +##@.O..# +#...O..# +#.#.O..# +#...O..# +#......# +######## + +<^^>>>vv>v<< diff --git a/2024/tests/samples/15.3.txt b/2024/tests/samples/15.3.txt new file mode 100644 index 0000000..6ee6098 --- /dev/null +++ b/2024/tests/samples/15.3.txt @@ -0,0 +1,9 @@ +####### +#...#.# +#.....# +#..OO@# +#..O..# +#.....# +####### + + None: + assert DayRunner.part1(data) == result + + +@pytest.mark.parametrize( + "data,result", + [ + (get_data(15, 1), 9021), + (get_data(15, 3), 618), + ], +) +def test_sample_part2(data: str, result: int) -> None: + assert DayRunner.part2(data) == result