Skip to content

Advent of Code 2025 Day 4: Printing Department

"""Advent of Code 2025: Day 4 - Printing Department"""

from advent import parse, grid_to_dict, grid_neighbors, Generator

grid: dict[complex, str] = grid_to_dict(parse(4, list), elem_filter=lambda c: c == '@')


def is_accessible(pos: complex) -> bool:
    """Indicate whether the specified position is accessible i.e. it has less than 4 neighbors."""
    return len(grid_neighbors(grid, pos)) < 4


def accessible_rolls(grid: dict[complex, str]) -> list[complex]:
    """Return a list of coordinates (as complex numbers) for all accessible rolls in the grid."""
    return [pos for pos in grid if is_accessible(pos)]


def remove_rolls(grid: dict[complex, str]) -> Generator[int, None, None]:
    """Iterate removal of accessible rolls while yielding the number of rolls removed
    for each iteration. Continue until there are no accessible rolls remaining."""
    while rolls := accessible_rolls(grid):
        for pos in rolls:
            del grid[pos]
        yield len(rolls)


removed_counts = list(remove_rolls(grid))

assert removed_counts[0] == 1564
assert sum(removed_counts) == 9401

Day 4

Input Parsing

Our first grid puzzle of the year! Since grids come up so often in Advent of Code,my advent library includes grid_to_dict and grid_neighbors functions to convert the input to a Python dict[complex, str] and return a list of neighbors for a position. Using complex numbers to represent (x, y) coordinates is very handy.

In this particular case, we only need a set, but it's more trouble than it's worth to convert the dict to a set, since a dict is just as convenient for testing set membership, etc. In other cases, there may be more than one type of character we're interested in.

The grid_to_dict function allows filtering and transforming both rows and elements. In this case, we simply want to grab all the @ elements, so the following is all we need:

grid_to_dict(parse(4, list), elem_filter=lambda c: c == '@')

Solution

Again, the two parts are very similar. Part 1 removes accessible rolls once, and part 2 iterates until there are no more rolls to remove.

Since our dict only has entries for rolls, the is_accessible function indicates whether a roll is "accessible", i.e. has less than 4 adjacent neighbors, by simply counting the list of neighbors returned by grid_neighbors.

The accessible function iterates over all items in the grid, and filters by whether they are accessible.

The remove_rolls function generates a list of the number of rolls removed for each iteration. We store the resulting list in removed_counts.

The answer for part 1 is just the first item in removed_counts, and the answer for part 2 is the sum of removed_counts.

End