Skip to Content

Advent of Code 2025 - Day 4, in Kotlin - Printing Department

Kotlin solutions to parts 1 and 2 of Advent of Code 2025, Day 4: 'Printing Department'

Posted on

Of all the various types of problems we tend to see during Advent of Code, the grid-based puzzles are among my favorite. Today we’ll define a Point2D class to help this and other potential future grid-based puzzles.

If you’d rather just view the code, my GitHub Repository is here.

Puzzle Input

We’re four days in and it’s time to create Point2D, a class we usually end up needing for some of the grid-based puzzles. I borrowed this from my Advent of Code 2024 repo, but it is similar to most of the Point2D objects we’ve developed over the years.

Important points to note: It’s a data class, it has methods to get all of the neighbors() or just the cardinalNeighbors, the distanceTo another Point2D, plus/minus, a factory method, and constants for each of the cardinal directions.

data class Point2D(val x: Int, val y: Int) {

    fun cardinalNeighbors(): Set<Point2D> =
        setOf(
            this + NORTH,
            this + EAST,
            this + SOUTH,
            this + WEST
        )

    fun neighbors(): Set<Point2D> =
        setOf(
            this + NORTH + WEST,
            this + NORTH,
            this + NORTH + EAST,
            this + EAST,
            this + SOUTH + EAST,
            this + SOUTH,
            this + SOUTH + WEST,
            this + WEST
        )

    fun distanceTo(other: Point2D): Int =
        (x - other.x).absoluteValue + (y - other.y).absoluteValue

    operator fun minus(other: Point2D): Point2D =
        Point2D(x - other.x, y - other.y)

    operator fun plus(other: Point2D): Point2D =
        Point2D(x + other.x, y + other.y)

    companion object {
        val ORIGIN = Point2D(0, 0)
        val NORTH = Point2D(0, -1)
        val EAST = Point2D(1, 0)
        val SOUTH = Point2D(0, 1)
        val WEST = Point2D(-1, 0)

        fun of(input: String): Point2D =
            input.split(",").let {
                Point2D(it.first().toInt(), it.last().toInt())
            }
    }
}

We won’t use all of these methods in today’s solution, but I highly suspect we will soon, so I left them in.

Now that that’s sorted, we can parse our input. I was tempted to parse the grid into an Array<CharArray> but felt a Set<Point2D> might end up being a bit cleaner. This way we can quickly test if a Point2D is in the grid or not.

When we parse the input, we set up a structure we’ll probably see in the future as well. Namely, an outer flatMapIndexed and an inner mapIndexedNotNull. This gets us x and y, and allows us to optionally create elements (the “not null” bit). As mentioned above, we’ll make this a Set.

class Day04(input: List<String>) {

    private val paper: Set<Point2D> = parseInput(input)

    private fun parseInput(input: List<String>): Set<Point2D> =
        input.flatMapIndexed { x, row ->
            row.mapIndexedNotNull { y, c ->
                if (c == '@') Point2D(x, y)
                else null
            }
        }.toSet()
}

⭐ Day 4, Part 1

The puzzle text can be found here.

Since we have a neighbors() method on Point2D that gives us all of the 8 neighbors as Point2D objects, we can count all of the rolls of paper that have fewer than 4 neighbors in the paper set.

// In Day04

fun solvePart1(): Int =
    paper.count { roll ->
        roll.neighbors().count { it in paper } < 4
    }

Star earned! Onward!

⭐ Day 4, Part 2

The puzzle text can be found here.

Part 2 is a bit more complicated because we need to alter the set by removing paper that is removable. Initially, I did this with a MutableSet but felt a generated sequence might be easier to reason about. The sequence generator is seeded with an initial value, paper, all of the rolls. Each time we need a new element from the sequence, we do work similar to part 1, loop through all of the rolls and keep the ones that have four or more neighbors. This creates a List<Point2D> of all the rolls of paper that cannot move, and discards any that can move.

If the size of the new List<Point2D> is smaller than when we started, that means something was removed. Otherwise, nothing was removed and we can return null from our generateSequence function, a signal to Kotlin that the sequence has ended. Note that we change our List<Point2D> into a Set<Point2D> only if we are not returning null.

// In Day04

fun solvePart2(): Int =
    paper.size - generateSequence(paper) { rolls ->
        rolls.filter { roll ->
            roll.neighbors().count { it in rolls } >= 4
        }.takeIf { it.size < rolls.size }?.toSet()
    }.last().size

Because the sequence returns all of the intermediate steps, and we don’t care about anything except the final state, we take the last() element from the sequence and subtract its size from the initial size of the paper rolls. This gives us our answer!

Perhaps this could be a bit quicker if we mutate a grid, or use a MutableSet, but to me this is a bit easier to follow and we don’t sacrifice a lot of time. Besides, this is Advent of Code and I’m unlikely to run into production issues if this is a few milliseconds slower, eh?

Star earned! See you tomorrow!

Further Reading

  1. Index of All Solutions - All posts and solutions for 2025, in Kotlin.
  2. My Github repo - Solutions and tests for each day.
  3. Solution - Full code for day 4
  4. Advent of Code - Come join in and do these challenges yourself!