Skip to content

Commit eaa10b8

Browse files
author
Mallory Brickerd
committed
feat: aoc 2025 day 4 solution + tests
1 parent e04592b commit eaa10b8

File tree

4 files changed

+178
-0
lines changed

4 files changed

+178
-0
lines changed

_2025/solutions/day04.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"""Day 4: Printing Department
2+
3+
This module provides the solution for Advent of Code 2025 - Day 4.
4+
5+
It processes a grid of paper rolls to determine which are accessible based
6+
on the number of adjacent rolls. A roll is accessible if it has fewer than
7+
four neighbors.
8+
9+
The module contains a Solution class to calculate the number of initially
10+
accessible rolls (Part 1) and the total number of rolls that can be removed
11+
iteratively (Part 2).
12+
"""
13+
14+
from typing import ClassVar
15+
16+
from aoc.models.base import SolutionBase
17+
18+
19+
class Solution(SolutionBase):
20+
"""Determine accessible and removable paper rolls from a grid layout.
21+
22+
This solution scans a grid representing paper roll locations ('@'). A roll
23+
is deemed accessible if it has fewer than four adjacent rolls (including
24+
diagonals).
25+
26+
Part 1 counts the number of rolls that are initially accessible.
27+
Part 2 simulates the process of removing all accessible rolls, re-evaluating
28+
accessibility, and repeating until no more rolls can be removed, counting
29+
the total number of removed rolls.
30+
"""
31+
32+
DIRECTIONS: ClassVar[list[tuple[int, int]]] = [
33+
(-1, -1), # up-left
34+
(-1, 0), # up
35+
(-1, 1), # up-right
36+
(0, -1), # left
37+
(0, 1), # right
38+
(1, -1), # down-left
39+
(1, 0), # down
40+
(1, 1), # down-right
41+
]
42+
43+
def find_open_spaces(self, grid: list[list[str]], rows: int, cols: int) -> list[tuple[int, int]]:
44+
"""Find all accessible paper rolls in the current grid.
45+
46+
An accessible roll is one marked '@' with fewer than four adjacent '@' rolls.
47+
48+
Args:
49+
grid: The current grid of paper rolls and empty spaces.
50+
rows: The number of rows in the grid.
51+
cols: The number of columns in the grid.
52+
53+
Returns
54+
-------
55+
list[tuple[int, int]]: A list of (row, col) coordinates for accessible rolls.
56+
"""
57+
positions = []
58+
for r in range(rows):
59+
for c in range(cols):
60+
if grid[r][c] != "@":
61+
continue
62+
63+
count = 0
64+
for dr, dc in self.DIRECTIONS:
65+
nr = r + dr
66+
nc = c + dc
67+
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == "@":
68+
count += 1
69+
70+
if count < 4:
71+
positions.append((r, c))
72+
73+
return positions
74+
75+
def part1(self, data: list[str]) -> int:
76+
"""Count the number of paper rolls that are initially accessible.
77+
78+
This method parses the input grid and counts how many paper rolls ('@')
79+
have fewer than four adjacent rolls.
80+
81+
Args:
82+
data: A list of strings representing the grid layout.
83+
84+
Returns
85+
-------
86+
int: The total number of initially accessible paper rolls.
87+
"""
88+
grid = [list(row) for row in data]
89+
rows, cols = len(grid), len(grid[0])
90+
return len(self.find_open_spaces(grid, rows, cols))
91+
92+
def part2(self, data: list[str]) -> int:
93+
"""Count the total number of rolls that can be removed.
94+
95+
This method simulates the iterative removal of accessible paper rolls.
96+
In each step, it finds all accessible rolls, adds them to a total count,
97+
removes them from the grid (by changing '@' to '.'), and then repeats
98+
the process until no more rolls are accessible.
99+
100+
Args:
101+
data: A list of strings representing the initial grid layout.
102+
103+
Returns
104+
-------
105+
int: The total number of paper rolls that can be removed.
106+
"""
107+
grid = [list(row) for row in data]
108+
rows, cols = len(grid), len(grid[0])
109+
positions = self.find_open_spaces(grid, rows, cols)
110+
111+
count = 0
112+
while (c := len(positions)) > 0:
113+
count += c
114+
for y, x in positions:
115+
grid[y][x] = "."
116+
117+
positions = self.find_open_spaces(grid, rows, cols)
118+
119+
return count
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
..@@.@@@@.
2+
@@@.@.@.@@
3+
@@@@@.@.@@
4+
@.@@@@..@.
5+
@@.@@@@.@@
6+
.@@@@@@@.@
7+
.@.@.@.@@@
8+
@.@@@.@@@@
9+
.@@@@@@@@.
10+
@.@.@@@.@.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
..@@.@@@@.
2+
@@@.@.@.@@
3+
@@@@@.@.@@
4+
@.@@@@..@.
5+
@@.@@@@.@@
6+
.@@@@@@@.@
7+
.@.@.@.@@@
8+
@.@@@.@@@@
9+
.@@@@@@@@.
10+
@.@.@@@.@.

_2025/tests/test_04.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""Test suite for Advent of Code daily challenge solution.
2+
3+
This module contains the test cases for the solution to the Advent of Code
4+
daily puzzle. It uses the TestSolutionUtility to run tests for both parts
5+
of the challenge.
6+
7+
The template provides a structure for defining tests for a specific day's
8+
solution, ensuring that the implementation meets the expected outputs for
9+
given test inputs.
10+
"""
11+
from aoc.models.tester import TestSolutionUtility
12+
13+
def test_day04_part1() -> None:
14+
"""Test the solution for Part 1 of the daily puzzle.
15+
16+
This test runs the solution for Part 1 of the puzzle against the
17+
provided test input and compares the result with the expected output.
18+
"""
19+
TestSolutionUtility.run_test(
20+
year=2025,
21+
day=4,
22+
is_raw=False,
23+
part_num=1,
24+
expected=13,
25+
)
26+
27+
def test_day04_part2() -> None:
28+
"""Test the solution for Part 2 of the daily puzzle.
29+
30+
This test runs the solution for Part 2 of the puzzle against the
31+
provided test input and compares the result with the expected output.
32+
"""
33+
TestSolutionUtility.run_test(
34+
year=2025,
35+
day=4,
36+
is_raw=False,
37+
part_num=2,
38+
expected=43,
39+
)

0 commit comments

Comments
 (0)