Advent of Code in Kotlin 2021 - Day 17

This is part 17 of a series, so if you haven't read the previous parts, start here.

For today's puzzle we are given a target area in 2 dimensional space and have to aim a shot that lands in the target area based on a set of rules involving gravity and drag. For part 1 we need to determine how high a shot can ark and still hit the target, for part 2 we need to determine the number of possible shots (defined by their initial velocity along each dimension) that hit the target.

I initially assumed, I needed some clever way to limit the search space, but it turns out a brute force solution works just fine. As a result, my code is not particularly clever, I just iterate over the possible shots based on a broad heuristic, trace the shots using a Kotlin sequence and tracking the result.

fun main() {
    val input = inputText(2021, 17)
    val targetXRange = input.substringAfter("x=").substringBefore(",").let(::rangeStringToRange)
    val targetYRange = input.substringAfter("y=").let(::rangeStringToRange)

    val xVelocityRange = (0..targetXRange.last)
    val yVelocityRange = (targetYRange.first..-targetYRange.first)

    val paths = (xVelocityRange).flatMap { x -> yVelocityRange.map { y -> getPath(x,  y, targetXRange, targetYRange) } }
    val hittingPaths = paths.filter(Path::hits)
    println(hittingPaths.maxOf { it.maxY })
    println(hittingPaths.size)
}

private fun rangeStringToRange(rangeString: String): IntRange =
    rangeString.split("..").let { (it.first().toInt()..it.last().toInt()) }

private fun getPath(initialXVelocity: Int, initialYVelocity: Int, targetXRange: IntRange, targetYRange: IntRange): Path =
    generateSequence(Path(0, 0, 0, initialXVelocity, initialYVelocity, false)) { (x, y, maxY, xVelocity, yVelocity, hits) ->
        val nextX = x + xVelocity
        val nextY = y + yVelocity
        val nextHits = hits || (nextX in targetXRange && nextY in targetYRange)
        Path(nextX, nextY, max(maxY, nextY), adjustXVelocity(xVelocity), yVelocity - 1, nextHits, )
    }.takeWhile { it.x <= targetXRange.last && it.y >= targetYRange.first }.last()

private fun adjustXVelocity(xVelocity: Int) = when {
    xVelocity > 0 -> xVelocity - 1
    xVelocity < 0 -> xVelocity + 1
    else -> 0
}

private data class Path(val x: Int, val y: Int, val maxY: Int, val xVelocity: Int, val yVelocity: Int, val hits: Boolean)

Two more stars in the bag.