Day four of AoC 2025 (available at https://adventofcode.com/2025/day/4) puts us in a warehouse with a grid of paper rolls. We're playing a forklift operator, figuring out which rolls we can access and remove.
A roll is accessible if it has fewer than four neighboring rolls in the eight adjacent positions. Empty spaces are marked with ., rolls with #.
Parsing the Input
The input is a straightforward character grid:
type Cell = "." | "#";
type Matrix = Cell[][];
const processInput = (day: number): Matrix => {
const lines = readInputLines(day);
const cells = lines.map(line =>
line.split("") as Cell[]
);
return cells;
};
Always test your input parsing before diving into the algorithm. I can't count how many times I've debugged a "broken" algorithm only to find the input was malformed.
Part One: Counting Accessible Rolls
For each roll, we count its neighbors in all eight directions. If fewer than four neighbors are rolls, that roll is accessible.
const countNeighbors = (matrix: Matrix, row: number, col: number): number => {
const topRow = matrix[row - 1];
const bottomRow = matrix[row + 1];
const currentRow = matrix[row];
const topLeft = topRow?.[col - 1] === "#" ? 1 : 0;
const top = topRow?.[col] === "#" ? 1 : 0;
const topRight = topRow?.[col + 1] === "#" ? 1 : 0;
const left = currentRow?.[col - 1] === "#" ? 1 : 0;
const right = currentRow?.[col + 1] === "#" ? 1 : 0;
const bottomLeft = bottomRow?.[col - 1] === "#" ? 1 : 0;
const bottom = bottomRow?.[col] === "#" ? 1 : 0;
const bottomRight = bottomRow?.[col + 1] === "#" ? 1 : 0;
return topLeft + top + topRight + left + right + bottomLeft + bottom + bottomRight;
};
JavaScript's optional chaining (?.) saves us here. Accessing matrix[-1] returns undefined, and undefined?.[0] is also undefined, which is not "#", so we get 0. No boundary checks needed.
The Mutation Bug
My first attempt modified the input matrix directly, marking accessible rolls with X. This broke everything — once you mark a roll as X, its neighbors suddenly have fewer roll neighbors, cascading incorrectly through the grid.
The fix: always work on a copy.
const copy = matrix.map(row => [...row]);
This creates an independent copy where modifications don't affect the original. I went against my instincts of never mutating input and paid for it with debugging time.
Part Two: Iterative Removal
Part two asks: keep removing accessible rolls until no more can be removed. How many total rolls do we remove?
This is where a graph-based solution would shine — model each roll as a node, edges to neighbors, and update connectivity as rolls are removed. Very efficient, very elegant.
But brute force works too:
type RemovalResult = {
matrix: Matrix;
removed: number;
};
const removeLayer = (matrix: Matrix): RemovalResult => {
const copy = matrix.map(row => [...row]);
let count = 0;
for (let row = 0; row < matrix.length; row++) {
for (let col = 0; col < matrix[row].length; col++) {
if (matrix[row][col] !== "#") continue;
const neighbors = countNeighbors(matrix, row, col);
if (neighbors < 4) {
copy[row][col] = ".";
count++;
}
}
}
return { matrix: copy, removed: count };
};
const partTwo = (input: Matrix, debug: boolean) => {
let matrix = input;
let total = 0;
let passCount = 0;
while (true) {
const { matrix: newMatrix, removed } = removeLayer(matrix);
if (removed === 0) break;
total += removed;
matrix = newMatrix;
passCount++;
}
return total;
};
We repeatedly call removeLayer until it returns 0 — a stable configuration where all remaining rolls have four or more neighbors and can't be accessed.
Performance
The brute force solution runs in 77 milliseconds over 45 passes on a 140×140 grid. Could a graph solution be faster? Absolutely. Is it worth implementing? For this input size, no.
My rule of thumb: optimize when you need to, not when you can. The graph approach would add complexity for negligible real-world benefit here.
Takeaways
Don't mutate your input: Creating a copy before modification prevents cascading bugs. The few bytes of memory are worth the sanity.
Optional chaining for boundary conditions: JavaScript's
?.operator elegantly handles out-of-bounds array access, returningundefinedinstead of throwing.Brute force has limits, but they're often far away: 45 passes over 19,600 cells in 77ms. Sometimes the "naive" solution is plenty fast.
Test your input parsing first: Before writing any algorithm, verify you're reading the data correctly. It saves hours of debugging the wrong thing.
The full solution is available in the repository.