diff --git a/src/holt59/aoc/2024/day15.py b/src/holt59/aoc/2024/day15.py index e5dba3f..67cbc7a 100644 --- a/src/holt59/aoc/2024/day15.py +++ b/src/holt59/aoc/2024/day15.py @@ -1,5 +1,4 @@ -import itertools -from typing import Any, Callable, Iterator, TypeAlias +from typing import Any, Callable, Final, Iterator, TypeAlias import numpy as np from numpy.typing import NDArray @@ -7,24 +6,83 @@ from numpy.typing import NDArray from ..base import BaseSolver Robot: TypeAlias = tuple[int, int] -Grid: TypeAlias = list[list[str]] +Grid: TypeAlias = list[list[int]] ImageGrid: TypeAlias = NDArray[np.uint8] +class BoxUtils: + FREE: Final = 0 + BLOCK: Final = 1 + + @staticmethod + def convert(grid_s: list[str], large: bool): + grid: list[list[int]] = [] + robot: tuple[int, int] | None = None + box_counter = 2 if not large else 3 + for i_row, row in enumerate(grid_s): + row_u: list[int] = [] + for i_col, col in enumerate(row): + if col == "." or col == "@": + row_u.extend( + (BoxUtils.FREE, BoxUtils.FREE) if large else (BoxUtils.FREE,) + ) + robot = (i_row, i_col) + elif col == "#": + row_u.extend( + (BoxUtils.BLOCK, BoxUtils.BLOCK) if large else (BoxUtils.BLOCK,) + ) + else: + row_u.extend( + (box_counter, -box_counter) if large else (box_counter,) + ) + box_counter += 2 + grid.append(row_u) + assert robot is not None + return grid, robot + + @staticmethod + def is_box(c: int): + return c >= 2 and c % 2 == 0 + + @staticmethod + def is_open_or_close_box(c: int): + return abs(c) >= 2 and c % 2 == 1 + + @staticmethod + def is_open_box(c: int): + return c >= 2 and c % 2 == 1 + + @staticmethod + def is_close_box(c: int): + return c < 0 + + @staticmethod + def to_char(c: int): + if c == BoxUtils.FREE: + return "." + elif c == BoxUtils.BLOCK: + return "#" + elif BoxUtils.is_box(c): + return "O" + elif BoxUtils.is_open_box(c): + return "[" + else: + return "]" + + class Solver(BaseSolver): def print_grid(self, name: str, grid: Grid, robot: Robot): if self.files: - grid = [[c for c in row] for row in grid] - grid[robot[0]][robot[1]] = "\033[31;1m@\033[00m" + grid_s = [[BoxUtils.to_char(c) for c in row] for row in grid] + grid_s[robot[0]][robot[1]] = "\033[31;1m@\033[00m" self.files.create( - name, "\n".join("".join(row) for row in grid).encode(), True + name, "\n".join("".join(row) for row in grid_s).encode(), True ) def convert_grid(self, grid: Grid, robot: Robot) -> ImageGrid: import numpy as np - grid = [[c for c in row] for row in grid] - grid[robot[0]][robot[1]] = "@" + # grid[robot[0]][robot[1]] = "@" # return np.array( # [ # [ @@ -43,7 +101,7 @@ class Solver(BaseSolver): # ], # dtype=np.uint8, # ) - return np.array(grid) + return np.abs(np.array(grid)) def step_part1(self, grid: Grid, move: str, robot: Robot): match move: @@ -59,20 +117,20 @@ class Solver(BaseSolver): assert False row, col = robot - if grid[row + d_row][col + d_col] == ".": + if grid[row + d_row][col + d_col] == BoxUtils.FREE: robot = (row + d_row, col + d_col) - elif grid[row + d_row][col + d_col] != "#": - assert grid[row + d_row][col + d_col] == "O" - + elif grid[row + d_row][col + d_col] != BoxUtils.BLOCK: n = 1 - while grid[row + n * d_row][col + n * d_col] == "O": + while BoxUtils.is_box(grid[row + n * d_row][col + n * d_col]): n += 1 - if grid[row + n * d_row][col + n * d_col] == ".": + if grid[row + n * d_row][col + n * d_col] == BoxUtils.FREE: robot = (row + d_row, col + d_col) - grid[row + d_row][col + d_col] = "." for k in range(2, n + 1): - grid[row + k * d_row][col + k * d_col] = "O" + grid[row + k * d_row][col + k * d_col] = grid[ + row + (k - 1) * d_row + ][col + (k - 1) * d_col] + grid[row + d_row][col + d_col] = BoxUtils.FREE return grid, robot @@ -90,53 +148,47 @@ class Solver(BaseSolver): assert False row, col = robot - if grid[row + d_row][col + d_col] == ".": + if grid[row + d_row][col + d_col] == BoxUtils.FREE: robot = (row + d_row, col + d_col) - elif grid[row + d_row][col + d_col] == "#": + elif grid[row + d_row][col + d_col] == BoxUtils.BLOCK: ... elif move in "<>": - assert d_row == 0 - assert grid[row][col + d_col] in "[]" - n = 1 - while grid[row][col + n * d_col] in "[]": + while BoxUtils.is_open_or_close_box(grid[row][col + n * d_col]): n += 1 - if grid[row][col + n * d_col] == ".": + if grid[row][col + n * d_col] == BoxUtils.FREE: robot = (row, col + d_col) for k in range(n, 1, -1): grid[row][col + k * d_col] = grid[row][col + (k - 1) * d_col] - grid[row + d_row][col + d_col] = "." + grid[row + d_row][col + d_col] = BoxUtils.FREE elif move in "^v": - assert d_col == 0 - assert grid[row + d_row][col + d_col] in "[]" - n = 1 boxes: list[set[int]] = [{col}] while True: to_move = boxes[-1] - if any(grid[row + n * d_row][c] == "#" for c in to_move): + if any(grid[row + n * d_row][c] == BoxUtils.BLOCK for c in to_move): break - if all(grid[row + n * d_row][c] == "." for c in to_move): + if all(grid[row + n * d_row][c] == BoxUtils.FREE for c in to_move): break as_move: set[int] = set() for c in to_move: - if grid[row + n * d_row][c] == "]": + if BoxUtils.is_close_box(grid[row + n * d_row][c]): as_move.update({c - 1, c}) - elif grid[row + n * d_row][c] == "[": + elif BoxUtils.is_open_box(grid[row + n * d_row][c]): as_move.update({c, c + 1}) boxes.append(as_move) n += 1 - if all(grid[row + n * d_row][c] == "." for c in boxes[-1]): + if all(grid[row + n * d_row][c] == BoxUtils.FREE for c in boxes[-1]): for k, to_move in zip(range(n, 1, -1), boxes[-1:0:-1], strict=True): for c in to_move: grid[row + k * d_row][c] = grid[row + (k - 1) * d_row][c] - grid[row + (k - 1) * d_row][c] = "." + grid[row + (k - 1) * d_row][c] = BoxUtils.FREE robot = (row + d_row, col + d_col) return grid, robot @@ -145,6 +197,7 @@ class Solver(BaseSolver): self, name: str, grid: Grid, + robot: Robot, moves: str, fn: Callable[ [Grid, str, Robot], @@ -152,13 +205,6 @@ class Solver(BaseSolver): ], generate: bool, ) -> tuple[Grid, list[ImageGrid]]: - # find robot - n_rows, n_cols = len(grid), len(grid[0]) - robot = next( - (i, j) for i in range(n_rows) for j in range(n_cols) if grid[i][j] == "@" - ) - grid[robot[0]][robot[1]] = "." - # initialize images: list[ImageGrid] = [] @@ -184,35 +230,37 @@ class Solver(BaseSolver): grid, images = self.run( "part1", - [[c for c in r] for r in grid_s.splitlines()], + *BoxUtils.convert(grid_s.splitlines(), False), moves, self.step_part1, self.files is not None, ) + if self.files: + print(images[0].max()) + self.files.image( + "anim_part1.gif", np.stack(images, axis=0).astype(np.uint8) + ) yield sum( 100 * i_row + i_col for i_row, row in enumerate(grid) for i_col, col in enumerate(row) - if col == "O" + if BoxUtils.is_box(col) ) grid, images = self.run( "part2", - [ - list( - itertools.chain.from_iterable( - {"O": "[]", "@": "@.", "#": "##", ".": ".."}[c] for c in r - ) - ) - for r in grid_s.splitlines() - ], + *BoxUtils.convert(grid_s.splitlines(), True), moves, self.step_part2, self.files is not None, ) + if self.files: + self.files.image( + "anim_part2.gif", np.stack(images, axis=0).astype(np.uint8) + ) yield sum( 100 * i_row + i_col for i_row, row in enumerate(grid) for i_col, col in enumerate(row) - if col == "[" + if BoxUtils.is_open_box(col) ) diff --git a/src/holt59/aoc/base.py b/src/holt59/aoc/base.py index c343025..7b66d63 100644 --- a/src/holt59/aoc/base.py +++ b/src/holt59/aoc/base.py @@ -25,11 +25,8 @@ class FileHandler: import imageio.v3 as iio - io = BytesIO() - iio.imwrite(io, image, extension=Path(filename).suffix) # type: ignore - io.seek(0) - - self.create(filename, io.read(), False) + data = iio.imwrite("", image, extension=Path(filename).suffix) # type: ignore + self.create(filename, data, False) class BaseSolver: