The Problem

The printing department has rolls of paper (@) arranged on a grid. Forklifts can only access a roll if it has fewer than 4 neighbors (out of 8 possible adjacent positions). Help optimize the forklifts to clear the way to the cafeteria!

Summary

Part

Question

Strategy

1

How many rolls can forklifts initially access?

Count rolls with < 4 adjacent neighbors

2

How many total rolls can be removed?

Iteratively remove accessible rolls until stable

Part 1: Count Accessible Rolls

Goal: Count rolls of paper (@) that have fewer than 4 filled neighbors.

Grid
Neighbors Found0/8
Accessible (<4)0
.
.
@
@
.
@
@
@
@
.
@
@
@
.
@
.
@
.
@
@
@
@
@
@
@
.
@
.
@
@
@
.
@
@
@
@
.
.
@
.
@
@
.
@
@
@
@
.
@
@
.
@
@
@
@
@
@
@
.
@
.
@
.
@
.
@
.
@
@
@
@
.
@
@
@
.
@
@
@
@
.
@
@
@
@
@
@
@
@
.
@
.
@
.
@
@
@
.
@
.
Event Log
Waiting for events...

The 8 Neighbors

Each cell has up to 8 adjacent positions (including diagonals):

const adjacentPositions = [
    [-1, -1], [0, -1], [1, -1], // top row
    [-1, 0],          [1, 0],   // middle row
    [-1, 1],  [0, 1], [1, 1],   // bottom row
];

Counting Neighbors

const getAdjacentCount = ({ grid, width, height, x, y }) => {
    return adjacentPositions.reduce((count, [dx, dy]) => {
        const newX = x + dx;
        const newY = y + dy;

        // Bounds check
        if (newX < 0 || newX >= width || newY < 0 || newY >= height) 
            return count;

        return grid[newY]?.[newX] === FILLED ? count + 1 : count;
    }, 0);
};

A roll is accessible if adjacentCount < 4. Simple scan of the grid:

for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
        if (grid[y]?.[x] !== FILLED) continue;
        
        if (getAdjacentCount({ grid, width, height, x, y }) < 4) {
            count++;
        }
    }
}

Part 2: Cascade Removal 🎯

Goal: Remove accessible rolls, then check again. Repeat until no more can be removed.

This is a cellular automaton-style simulation! Removing rolls exposes new rolls that become accessible.

Grid
Neighbors Found0/8
Accessible (<4)0
Round0
Removed0
.
.
@
@
.
@
@
@
@
.
@
@
@
.
@
.
@
.
@
@
@
@
@
@
@
.
@
.
@
@
@
.
@
@
@
@
.
.
@
.
@
@
.
@
@
@
@
.
@
@
.
@
@
@
@
@
@
@
.
@
.
@
.
@
.
@
.
@
@
@
@
.
@
@
@
.
@
@
@
@
.
@
@
@
@
@
@
@
@
.
@
.
@
.
@
@
@
.
@
.
Event Log
Waiting for events...

The Cascade Effect


Rolls on the edges or with few neighbors get removed first. This exposes interior rolls, which then become removable.

The Algorithm

while (true) {
    const toRemove: [number, number][] = [];

    // Find all currently accessible rolls
    for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
            if (grid[y]?.[x] !== FILLED) continue;
            
            if (getAdjacentCount({ grid, width, height, x, y }) < 4) {
                toRemove.push([x, y]);
            }
        }
    }

    // No more accessible rolls? We're done!
    if (toRemove.length === 0) break;

    // Remove all accessible rolls simultaneously
    for (const [x, y] of toRemove) {
        grid[y][x] = EMPTY;
    }

    count += toRemove.length;
}

Why Batch Removal?

We collect all accessible rolls before removing any. This ensures we don't accidentally affect the neighbor count of rolls we haven't checked yet in this iteration.

If we removed one-by-one, the order would matter and we'd get inconsistent results!

Performance

Part

Time

Part 1

6.11ms

Part 2

40.77ms

Part 2 is slower because it runs multiple iterations until the grid stabilizes. Each iteration requires a full grid scan. There is probably some optimization to do there.