Day 4 gives us a fun grid puzzle! 📜
We have a 2D grid filled with @ (paper rolls) and . (empty space).
@.@@.@@@..
@@..@.@.@.
.@.@@..@.@
@@.@.@@@..
.@@@..@.@@
A paper roll is accessible if it has fewer than 4 adjacent @ neighbours (using 4-directional adjacency – no diagonals).
Part 1
Simple enough – just count how many @ cells have fewer than 4 @ neighbours:
function getRollCoords(grid: Map<string, string>): Coord[] {
let paperRolls: Coord[] = [];
grid.forEach((value, coord) => {
if (value === '@') {
const adjacentCells = getNeighbors(Coord.deserialize(coord), grid, true);
const rolls = adjacentCells.filter(cell => cell === '@').length;
if (rolls < 4)
paperRolls.push(Coord.deserialize(coord));
}
});
return paperRolls;
}
function findAccessiblePaperRolls(grid: Grid) {
return getRollCoords(grid).length;
}
The getNeighbors utility returns the values of all orthogonal neighbours. The true flag means we only get cells that exist in the grid (no out-of-bounds). Any @ with fewer than 4 @ neighbours is accessible – boundary cells are naturally accessible since they have fewer neighbours overall.
Part 2
Part 2 asks for the total number of rolls removed across all rounds of a peeling process: each round, we find all currently accessible rolls, remove them (replacing @ with .), and then repeat until no accessible rolls remain.
function findAndRemoveAccessiblePaperRolls(grid: Grid) {
let totalAccessiblePaperRolls = 0;
let paperRollCoords: Coord[] = getRollCoords(grid);
while (paperRollCoords.length > 0) {
totalAccessiblePaperRolls += paperRollCoords.length;
paperRollCoords.forEach((p) => grid.set(p.serialize(), '.'));
paperRollCoords = getRollCoords(grid);
}
return totalAccessiblePaperRolls;
}
This is a classic “onion peeling” algorithm – each iteration strips away the outermost accessible layer, making previously interior rolls accessible in the next round. The loop continues until there’s nothing left to remove (or only rolls with 4 @ neighbours remain). ⭐⭐
