Advent of Code 2025 Day 4

Part 1

Read in a grid of squares that contain either a roll of paper (@) or are empty (.). Count the number of squares containing a roll of paper which have fewer than 4 rolls of paper in the adjacent squares.

Solution

Things to consider:

  1. This is a fixed grid with edges, i.e. it does not stretch to infinity or wrap around (some puzzles do have these characteristics).
  2. Adjacent means any of the compass points, i.e. horizontally, vertically or diagonally. The minimum number of adjacent squares is 3 (the corner squares) and the maximum is 8.
  3. As a shortcut, if a square has fewer than 4 adjacent squares, then by definition it has fewer than 4 adjacent paper rolls (but we won’t bother with this for part 1 at least).

As usual, first of all we need to design some data structures. We need a data structure for a square which has a field for the contents, with constants for the possible options:

const (
	UNKNOWN = -1
	EMPTY   = iota
	PAPER_ROLL
)

type Square struct {
	contents int
}

We also need a grid, which is a slice of slices. Each outer slice represents a single line of the grid and the inner slices represent a square on the line.

To find the adjacent squares, we need to know how to identify them using relative coordinates, which we can represent as deltas in the x and y coordinates. Bear in mind that (0, 0) on our grid starts at the top left, rather than the traditional bottom left, and y increases as we move down the grid. To provide human readable names for the deltas, we’ll label the direction with the compass points.

const (
	NORTH = iota
	NORTH_EAST
	EAST
	SOUTH_EAST
	SOUTH
	SOUTH_WEST
	WEST
	NORTH_WEST
)

type SquareDelta struct {
	direction int
	x         int
	y         int
}

var squareDeltas = []SquareDelta{
	{
		direction: NORTH,
		x:         0,
		y:         -1,
	},
	{
		direction: NORTH_EAST,
		x:         1,
		y:         -1,
	},
	{
		direction: EAST,
		x:         1,
		y:         0,
	},
	{
		direction: SOUTH_EAST,
		x:         1,
		y:         1,
	},
	{
		direction: SOUTH,
		x:         0,
		y:         1,
	},
	{
		direction: SOUTH_WEST,
		x:         -1,
		y:         1,
	},
	{
		direction: WEST,
		x:         -1,
		y:         0,
	},
	{
		direction: NORTH_WEST,
		x:         -1,
		y:         -1,
	},
}

Now we can parse the input. First map any single character to a Square:

func getSquare(input string) Square {
	square := Square{}

	switch input {
	case "@":
		square.contents = PAPER_ROLL
	case ".":
		square.contents = EMPTY
	default:
		square.contents = UNKNOWN
	}

	return square
}

Map a line of input to a slice of Squares:

func getLine(input string) []Square {
	line := []Square{}

	input = strings.Trim(input, "\n")

	squareCharacters := strings.Split(input, "")

	for sc := range squareCharacters {
		square := getSquare(squareCharacters[sc])
		if square.contents != UNKNOWN {
			line = append(line, square)
		}
	}

	return line
}

Finally map the entire input to a grid, i.e. a slice of slices of Squares:

func getGrid(input string) [][]Square {
	grid := [][]Square{}

	lines := strings.Split(input, "\n")

	for lineIndex := range lines {
		if len(lines[lineIndex]) > 0 {
			grid = append(grid, getLine(lines[lineIndex]))
		}
	}

	return grid
}

Now that we have the input in a data structure, we can count the number of moveable paper rolls by moving through each square on the grid and counting how many any adjacent squares contain paper rolls. We do this by iterating over the compass points, but checking the resulting coordinates against minimum and maximum values for x and y to ensure that we do not move out of the grid:

func countMoveablePaperRolls(grid [][]Square, maxAdjacentRolls int) int {
	moveablePaperRolls := 0
	minX := 0
	maxX := len(grid[0]) - 1
	minY := 0
	maxY := len(grid) - 1

	for x := minX; x <= maxX; x++ {
		for y := minY; y <= maxY; y++ {
			// Only check this square if it contains a paper roll
			if grid[x][y].contents == PAPER_ROLL {
				// Check for adjacent squares using deltas
				adjacentRolls := 0

				for sd := range squareDeltas {
					adjacentX := x + squareDeltas[sd].x
					adjacentY := y + squareDeltas[sd].y

					// Check that we have valid coordinates
					if adjacentX >= minX && adjacentX <= maxX && adjacentY >= minY && adjacentY <= maxY {
						if grid[adjacentX][adjacentY].contents == PAPER_ROLL {
							adjacentRolls++
						}
					}
				}

				if adjacentRolls <= maxAdjacentRolls {
					moveablePaperRolls++
				}
			}
		}
	}

	return moveablePaperRolls
}

All we need to do now is write a very short main function to convert the input and call the function to count the moveable paper rolls:

func main() {
	inputBytes, _ := os.ReadFile("../2025-04-input.txt")
	inputString := string(inputBytes)
	grid := getGrid(inputString)
	fmt.Println(countMoveablePaperRolls(grid, 3))
}

Part 2

Overall thoughts

Other than my mistake in not reading the specification correctly (caught by my test suite), this was an easy puzzle to solve. I worked out how to extract data from within a slice of structs (using lo.Map) and then process it, which I think will be useful in other puzzles.