Advent of Code 2023 - Day 04

By Eric Burden | December 4, 2023

It’s that time of year again! Just like last year, I’ll be posting my solutions to the Advent of Code puzzles. This year, I’ll be solving the puzzles in Kotlin. I’ll post my solutions and code to GitHub as well. If you haven’t given AoC a try, I encourage you to do so along with me!

Day 4 - Scratchcards

Find the problem description HERE.

The Input - Scratching the Itch

Today’s elf friend seems to have a bit of a problem… Which is that his “friends” decided that a big pile of scratchcards would make an appropriate gift. I’m not judging, but it looks like it’s put our elf friend to a lot of effort for dubious reward. Regardless, our first job is to translate this text into something representing these scratchers, so here we go!

/**
 * This class represents one of the elve's scratchcards
 *
 * @property id The id number assigned to this card.
 * @property luckyNumbers The winning scratchard numbers.
 * @property myNumbers The numbers revealed for the elf.
 */
data class Card private constructor(val id: Int, val matches: Int) {
  companion object {
    /**
     * Parses a [Card] from a String
     *
     * @param input The string to be parsed.
     * @return A [Card] represented by the input.
     */
    fun fromString(input: String): Card {
      // Split a string like "Card 1: 1 2 | 2 3 4" into ["Card 1", "1 2", "2 3 4"]
      val parts = input.split("[:|]".toRegex()).map { it.trim() }
      require(parts.size == 3) { "$input cannot be parsed to a [Card]!" }

      val (cardPart, luckyNumbersPart, myNumbersPart) = parts

      // Remove the "Card   " prefix. Note, the example only included one space
      // after "Card", but the real input included multiple for formatting.
      val id =
          cardPart.replaceFirst("Card\\s+".toRegex(), "").toIntOrNull()
              ?: throw IllegalArgumentException("$cardPart cannot be parsed to a card id!")

      // Parse lists of space-separated numeric strings into integers.
      val luckyNumbers =
          luckyNumbersPart.split("\\s+".toRegex()).map {
            it.toIntOrNull() ?: throw IllegalArgumentException("$it is not a number!")
          }
      val myNumbers =
          myNumbersPart.split("\\s+".toRegex()).map {
            it.toIntOrNull() ?: throw IllegalArgumentException("$it is not a number!")
          }

      // Turns out, we don't care about the numbers at all. Just count how many
      // of 'myNumbers' are in `luckyNumbers' and keep track of that.`
      val matches = myNumbers.filter { luckyNumbers.contains(it) }.count()

      return Card(id, matches)
    }
  }
}

class Day04(input: List<String>) {

  // Except for the trailing empty line, parse all the lines from
  // the input into [Card]s.
  private val parsed = input.filter { it.isNotEmpty() }.map(Card::fromString)
}

In my original iteration, I kept the lists of numbers as well. Since it turned out we didn’t really need to keep them around, I’ve left them out of this final version.

Part One - Counting Card’s Value

Ok, looks like the reward for all these scratchcards are “points” of unspecified value. On a value-scaling basis, they’re probably worth about as much as likes or upvotes, I figure. Given how precious these points are, we want to make sure we tally them correctly! Each card is worth a number of points based on how many of the revealed numbers match the winning numbers displayed on the card. Let’s see how rich this elf is!

data class Card private constructor(val id: Int, val matches: Int) {
  
  // companion object { ... }
  
  /**
   * Calculate the score for this card
   *
   * The score is 1 for a single match, doubled for every subsequent match. Mathematically, this
   * works out to 2 ** (matches - 1) for one or more matches.
   *
   * @return The calculated score for this card
   */
  fun score(): Int {
    // Apparently, Kotlin doesn't have a "nice" way to exponentiate integers
    // without first casting it to a double. I don't _want_ to do type
    // conversion for exponents! So, I'm just doubling the value in a loop.
    var score = if (matches == 0) 0 else 1
    if (matches > 1) {
      repeat(matches - 1) { score = score * 2 }
    }

    return score
  }
}

class Day04(input: List<String>) {

  // private val parsed = ...

  // In Part 1, we score each card and return the sum of all the scores.
  fun solvePart1(): Int = parsed.sumOf { it.score() }
}

Now that we’ve counted the elf’s vast wealth (in points), we should probably figure out how to spend them!

Part Two - The House Always Wins

Ah, I see, seems like we should have looked more closely at these cards. Turns out, our elf friend’s fabulous prize is… more scratchcards! Not sure how this translates, but at least he should have plenty of kindling or paper mâché materials. Elves are pretty crafty, so I’m sure he’s excited to find out just how many of these cards he’s entitled to. Let’s help him out.

class Day04(input: List<String>) {

  // private val parsed = ...

  // fun solvePart1(): Int = ...

  // In Part 2, we re-learn that reading is key and calculate the number
  // of cards our elf friend ends up with.
  fun solvePart2(): Int {
    // Each winning card increases the number of copies of subsequent cards
    // by one. The number of matches on the winning card indicates how many
    // of the following cards we "win" new copies of. So, a card with four
    // matches gets us one extra copy of the next four cards. We start
    // by initializing a list of card counts. Since cards are 1-indexed,
    // we make a list a little bigger than we need and just zero the first
    // value (no "Card 0").
    val cardsCounted = MutableList(parsed.size + 1) { 1 }
    cardsCounted[0] = 0

    // Here, we increase subsequent cards by the number of existing cards,
    // so that 10 copies of a winning card with three matches will add ten
    // more copies to each of the following three card types.
    parsed.forEach { card ->
      for (i in 1..card.matches) {
        cardsCounted[card.id + i] += cardsCounted[card.id]
      }
    }

    return cardsCounted.sum()
  }
}

Gotta be honest, I think I’d prefer to have the points!

Wrap Up

I was starting to get a bit worried after the relatively involved parsing of yesterday’s input, but it seems like we’re back on track in terms of relative difficulty for where we are in the calendar. I feel like I’m starting to get a handle on some of the standard library stuff, although the need to call .toRegex() on my strings in order to split the input by regular expressions threw me for a bit of a loop. Other than that, though, today’s puzzle seemed really straightforward. Oh, also, no integer exponentiation? Really? That seems like a bit of an odd gap. Thankfully, it’s easy enough to roll your own. Probably the biggest ’efficiency’ was adding the won cards in batches instead of one at a time, so I felt pretty good about that.

comments powered by Disqus