Day 4/2025 - Paper Rolls and Forklifts

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 = "." | "@";

const processInput = (day: number) => {
    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.

The neighbor counting is inline in the removeLayer function, which handles both the counting and the removal:

type Cell = "." | "@";

type RemoveResponse = {
    matrix: Cell[][];
    removed: number;
};

const removeLayer = (matrix: Cell[][]): RemoveResponse => {
    let count = 0;
    const copy = matrix.map(row => [...row]);
    
    for (let row = 0; row < matrix.length; row++) {
        for (let col = 0; col < matrix[row].length; col++) {
            if (matrix[row][col] !== "@") continue;

            const topRow = matrix[row - 1];
            const topLeft = topRow?.[col - 1] === "@" ? 1 : 0;
            const topTop = topRow?.[col] === "@" ? 1 : 0;
            const topRight = topRow?.[col + 1] === "@" ? 1 : 0;
            const left = matrix[row][col - 1] === "@" ? 1 : 0;
            const right = matrix[row][col + 1] === "@" ? 1 : 0;
            const bottomRow = matrix[row + 1];
            const bottomLeft = bottomRow?.[col - 1] === "@" ? 1 : 0;
            const bottomBottom = bottomRow?.[col] === "@" ? 1 : 0;
            const bottomRight = bottomRow?.[col + 1] === "@" ? 1 : 0;

            const neighbors = topLeft + topTop + topRight + left + right + bottomLeft + bottomBottom + bottomRight;

            if (neighbors < 4) {
                copy[row][col] = ".";
                count += 1;
            }
        }
    }
    return { matrix: copy, removed: count };
};

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 key insight: we read from the original matrix but write to the copy. This prevents the cascading bug where removing one roll affects its neighbors' counts.

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:

const partOne = (input: Cell[][], debug: boolean) => {
    const {removed: result} = removeLayer(input);
    return result;
};

const partTwo = (input: Cell[][], debug: boolean) => {
    let {removed, matrix} = removeLayer(input);
    let totalRemoved = removed;

    let passCount = 1;
    while (removed > 0) {
        passCount += 1;
        const response = removeLayer(matrix);
        matrix = response.matrix;
        removed = response.removed;
        totalRemoved += removed;
    }

    return totalRemoved;
};

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

  1. Don't mutate your input: Creating a copy before modification prevents cascading bugs. The few bytes of memory are worth the sanity.

  2. Optional chaining for boundary conditions: JavaScript's ?. operator elegantly handles out-of-bounds array access, returning undefined instead of throwing.

  3. 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.

  4. 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.