Programming

Advent of Code 2019 Day 5 in Scala

Advent of Code 2019

A second, more advanced IntCode computer for Advent of Code 2019! I am extremely happy with my solution here. I haven’t looked at any other solutions, and I’m almost afraid to crush my happy feelings. This puzzle required using a previous IntCode computer and updating it and expanding it. It was extremely fun to build. Let’s dive straight into the code.

IntCode Computer

package is.kow.adventofcode._2019

import scala.annotation.tailrec
import scala.collection.mutable

class IntCode(data: List[Int]) {

  //A mutable state in here to collect the outputs in order
  val outputCollector: mutable.Stack[Int] = new mutable.Stack[Int]()

  private val outputOpCode = new OutputOpCode({ output =>
    outputCollector.push(output)
  })

  def execute(input: Option[Int]): List[Int] = {
    val END_OP = EndOpCode()

    val OP_CODES = List(
      new JumpIfFalseOpCode(),
      new JumpIfTrueOpCode(),
      new LessThanOpCode(),
      new EqualsOpCode(),
      new AddOpCode(),
      new MultOpCode(),
      END_OP,
      outputOpCode) ++ (if (input.isDefined) List(new InputOpCode(input.get)) else List.empty)

    @tailrec def process(completedOps: List[OpCode], acc: List[Int], opLocation: Int = 0): List[Int] = {
      val currentOp = acc.drop(opLocation).head
      if (END_OP.matches(currentOp)) {
        acc
      } else {
        val maybeOp = OP_CODES.find(_.matches(currentOp))
        val operated = maybeOp.map { operation =>
          operation.operate(opLocation, acc)
        } getOrElse {
          throw new RuntimeException(s"Invalid Operation: $currentOp")
        }
        val op = maybeOp.get
        process(op :: completedOps, operated.data, operated.opPointer)
      }
    }

    process(List.empty, data)
  }
}

This is significantly different than my previous IntCode logic. Previously, I had all the operations in the same class, as it was quite simple. All ops had 3 parameters, so everything was super simple. The next generation has significantly more capability, which requires a better framework to do the work. I am not quite good enough at Scala to write something that I could use to pattern match out the entire operation, but I can detect if it’s the right thing, and then call the right function.

I moved my OpCodes into a file on their own so I could develop them.

case class OperationResult(data: List[Int], opPointer: Int)

abstract class OpCode(val opValue: Int, val parameterSize: Int) extends LazyLogging {
  val opCodeFormatted: String = codeFormat(opValue)
  protected final val IMMEDIATE = '1'
  protected final val POSITION = '0'

  def codeFormat(input: Int): String = {
    ("%0" + (parameterSize + 2) + "d").format(input)
  }

  def operate(offset: Int, data: List[Int]): OperationResult

  def matches(code: Int): Boolean = {
    //convert the code to a string also
    val actualOpCode = codeFormat(code).slice(parameterSize, parameterSize + 2)
    //Determine if the last two of the formatted thing, as an integer, are our opcode
    opCodeFormatted.slice(parameterSize, parameterSize + 2) == actualOpCode
  }

  protected def logOperation(index: Int, name: String, opParams: String, params: Int*): Unit = {
    logger.whenDebugEnabled({
      val paramSize = params.foldLeft(Int.MinValue) { (acc, p) =>
        val paramLength = p.toString.length
        if (paramLength > acc) paramLength else acc
      }

      val formatString = "%4d %10s: %5s " + params.foldLeft("") { (acc, p) =>
        acc + s"%${paramSize}d "
      }
      val intermediary = Seq(index, name, opParams) ++ params
      logger.debug(formatString.format(intermediary: _*))
    })
  }
}

This is the foundation for my Opcodes. About half of the code is logging output. That just made it a whole lot easier for me to trace my way through when I made typos. I should’ve probably written a test for every single opcode, and then I’d have less debugging to do. There is still a lot of copypasta in there, looking at all the opcodes.

The original two opcodes

class AddOpCode() extends OpCode(1, 3) {
  override def operate(offset: Int, data: List[Int]): OperationResult = {
    data.drop(offset) match {
      case opParams :: in1 :: in2 :: dest :: xs =>
        val formattedOpParams = codeFormat(opParams)
        val operand1 = if (formattedOpParams(2) == POSITION) data(in1) else in1
        val operand2 = if (formattedOpParams(1) == POSITION) data(in2) else in2
        logOperation(offset, "ADD", formattedOpParams, operand1, operand2)
        OperationResult(data.updated(dest, operand1 + operand2), offset + 1 + parameterSize)
    }
  }
}

class MultOpCode() extends OpCode(2, 3) {
  override def operate(offset: Int, data: List[Int]): OperationResult = {
    data.drop(offset) match {
      case opParams :: in1 :: in2 :: dest :: xs =>
        val formattedOpParams = codeFormat(opParams)
        val operand1 = if (formattedOpParams(2) == POSITION) data(in1) else in1
        val operand2 = if (formattedOpParams(1) == POSITION) data(in2) else in2
        logOperation(offset, "MULT", formattedOpParams, operand1, operand2)
        OperationResult(data.updated(dest, operand1 * operand2), offset + 1 + parameterSize)
    }
  }
}

Here’s the original two opcodes from the first iteration of the IntCode computer. They perform the same operations as before, but have been enhanced to do the Immediate and Positional requirement. And, I also encapsulated the result into an object that lets me define where the pointer goes (part of the later jump operations).

Side Effects with Input and Output

class InputOpCode(val input: Int) extends OpCode(3, 1) {
  override def operate(offset: Int, data: List[Int]): OperationResult = {
    data.drop(offset) match {
      case opParams :: p1 :: xs =>
        logOperation(offset, "INPUT", codeFormat(opParams), p1, input)
        OperationResult(data.updated(p1, input), offset + 1 + parameterSize)
    }
  }
}

//Can have multiple outputs...
class OutputOpCode(output: (Int => Unit)) extends OpCode(4, 1) {
  override def operate(offset: Int, data: List[Int]): OperationResult = {
    data.drop(offset) match {
      case opParams :: p1 :: xs =>
        val fOpParams = codeFormat(opParams)
        val operand1 = if(fOpParams(0) == POSITION) data(p1) else p1
        logOperation(offset, "OUTPUT", codeFormat(opParams), operand1)
        output(operand1)
        OperationResult(data, offset + 1 + parameterSize)
    }
  }
}

These two operators have a bit more magic in them. For the input opcode, I needed to provide the actual input value. The Output OpCode produced side-effects. I chose to use a function that I could have the IntCode computer deal with the potential mutable state. Keeps the Opcode simple. It just calls a function that takes an int and is Unit, returning no type. This inherently means it’s a side-effecting function. That’s handled in the IntCode class, where I use a stack, and just push stuff into it. I can collect my output that way.

The telltale sign of a function with side effects is that its result type (return type) is Unit.

Programming in Scala

The other operators follow a similar pattern. I truly enjoyed this puzzle and I got a good flow going, because I feel like I also wrote code that was easy to understand. I was able to keep it mostly immutable too, and that makes me happy.

Further reading

Related posts

Advent of Code 2019 Day 4 in Scala

David Kowis

Advent of Code Day 1 and 2 in Scala

David Kowis

Advent of Code 2019 in Scala Day 7

David Kowis

Leave a Reply

avatar
  Subscribe  
Notify of
Adventures in Software Development, Home Labbing, and DevOps