Advent of Code (in MiniScript), Day 11

JoeStrout - Dec 11 '22 - - Dev Community

Welcome back to my series of Advent of Code solutions in MiniScript! In this challenge, we are tasked with tracking the movement of items (numeric values) as they are tossed around among a set of ornery monkeys.

Each monkey is defined by a section in the input file that looks like this:

Monkey 6:
  Starting items: 81, 51, 85
  Operation: new = old * old
  Test: divisible by 5
    If true: throw to monkey 5
    If false: throw to monkey 1
Enter fullscreen mode Exit fullscreen mode

The "Starting items" are a list of values, the things that get transformed and passed around. "Operation" is how each monkey transforms a value on its turn; in the example above, the new value is the old value times itself. Other monkeys do things like "old + 7" or "old * 5". The "Test" is always whether the number is divisible by something, and the final two lines indicate the monkey the number is passed to, based on that divisibility.

That "Operation" line is a problem. MiniScript does not have a built-in eval function (though there is a repo for that). If I weren't under time pressure, I'd probably write a very simple expression parser and evaluator that can parse and evaluate A op B, where A and B are either "old" or a number, and op is either '+' or '*'. But this being Advent of Code, I was under time pressure. That seemed like a poor choice.

So, I did what any self-respecting programmer would have done... I cheated.

OK, it's not really cheating. There's nothing in the rules that says you have to write code that reads the input file. So I loaded the input file in my favorite text editor, and manually used search & replace to transform it into valid MiniScript code. The example above became:

    m = new Monkey
    m.init [81, 51, 85]
    m.op = function(old); return old * old; end function
    m.divBy = 5
    m.ifTrue = 5
    m.ifFalse = 1
    monkeys.push m
Enter fullscreen mode Exit fullscreen mode

This after I had defined a Monkey class, of course, with the appropriate methods and properties, and a monkeys list to manage them (in retrospect, I probably should have called that barrel instead).

The result was a longer program than usual, but not particularly more difficult than usual. The example input had 4 monkeys, and the real input had 8 (which I transformed separately -- also in retrospect, I should have combined them into one file, transformed them into code all at once, and then split them up again). Here's the final program for Part A:

if 1 then fname = "input.txt" else fname = "example.txt"

Monkey = {}
Monkey.items = null
Monkey.op = null
Monkey.divBy = 1
Monkey.ifTrue = -1
Monkey.ifFalse = -1
Monkey.init = function(items)
    self.items = items
end function
Monkey.inspectCount = 0

Monkey.doOneItem = function
    if not self.items then return false
    item = self.items.pull
    self.inspectCount = self.inspectCount + 1
    print "monkey " + monkeys.indexOf(self) + " inspects item " + item
    item = self.op(item)
    print "worry level becomes " + item
    item = floor(item/3)

    print "monkey gets bored, worry level drops to " + item
    if item % self.divBy == 0 then
        print "monkey passes to " + self.ifTrue
        monkeys[self.ifTrue].items.push item
    else
        print "monkey passes to " + self.ifFalse
        monkeys[self.ifFalse].items.push item
    end if
    return self.items.len > 0
end function

doOneRound = function
    for idx in monkeys.indexes
        m = monkeys[idx]
        while m.doOneItem; end while
    end for
end function

monkeys = []
if fname == "example.txt" then
    m = new Monkey
    m.init [79,98]
    m.op = function(old); return old*19; end function
    m.divBy = 23
    m.ifTrue = 2
    m.ifFalse = 3
    monkeys.push m

    m = new Monkey
    m.init [54, 65, 75, 74]
    m.op = function(old); return old + 6; end function
    m.divBy = 19
    m.ifTrue = 2
    m.ifFalse = 0
    monkeys.push m

    m = new Monkey
    m.init [79, 60, 97]
    m.op = function(old); return old * old; end function
    m.divBy = 13
    m.ifTrue = 1
    m.ifFalse = 3
    monkeys.push m

    m = new Monkey
    m.init [74]
    m.op = function(old); return old + 3; end function
    m.divBy = 17
    m.ifTrue = 0
    m.ifFalse = 1
    monkeys.push m
else
    m = new Monkey
    m.init [83, 97, 95, 67]
    m.op = function(old); return old * 19; end function
    m.divBy = 17
    m.ifTrue = 2
    m.ifFalse = 7
    monkeys.push m

    m = new Monkey
    m.init [71, 70, 79, 88, 56, 70]
    m.op = function(old); return old + 2; end function
    m.divBy = 19
    m.ifTrue = 7
    m.ifFalse = 0
    monkeys.push m

    m = new Monkey
    m.init [98, 51, 51, 63, 80, 85, 84, 95]
    m.op = function(old); return old + 7; end function
    m.divBy = 7
    m.ifTrue = 4
    m.ifFalse = 3
    monkeys.push m

    m = new Monkey
    m.init [77, 90, 82, 80, 79]
    m.op = function(old); return old + 1; end function
    m.divBy = 11
    m.ifTrue = 6
    m.ifFalse = 4
    monkeys.push m

    m = new Monkey
    m.init [68]
    m.op = function(old); return old * 5; end function
    m.divBy = 13
    m.ifTrue = 6
    m.ifFalse = 5
    monkeys.push m

    m = new Monkey
    m.init [60, 94]
    m.op = function(old); return old + 5; end function
    m.divBy = 3
    m.ifTrue = 1
    m.ifFalse = 0
    monkeys.push m

    m = new Monkey
    m.init [81, 51, 85]
    m.op = function(old); return old * old; end function
    m.divBy = 5
    m.ifTrue = 5
    m.ifFalse = 1
    monkeys.push m

    m = new Monkey
    m.init [98, 81, 63, 65, 84, 71, 84]
    m.op = function(old); return old + 3; end function
    m.divBy = 2
    m.ifTrue = 2
    m.ifFalse = 3
    monkeys.push m
end if


for round in range(1,20)
    doOneRound
end for

// sort by inspection count
monkeys.sort "inspectCount"
print "Most activity: " + monkeys[-1].inspectCount + ", " + monkeys[-2].inspectCount
print "Monkey business: " + monkeys[-1].inspectCount * monkeys[-2].inspectCount
Enter fullscreen mode Exit fullscreen mode

This produced the right answer.

Part B

In the first part of the challenge, one of the rules was that after each monkey does its operation on the value, we divide it by 3 (rounding down). And we only do 20 rounds of monkey work before computing the answer.

But in part 2, the division by three is removed, and we do 10 thousand rounds. As a result, the numbers blow up. The challenge says "You'll need to find another way to keep your [values] manageable."

I tried running it without finding such a way, and sure enough, the values by the end printed as Infinity (and the monkey business calculation was all wrong on the sample data).

What to do? Well since the tests are always for divisibility, it occurred to me that we could take our values mod something and get the same result. For example, if we're testing for divisibility by 10, we'll get the same answer for 42 as for 142%100, 242%100, 342%100, etc... because 100 (the thing we're modding by) is a multiple of 10 (the thing we're testing for divisibily by).

So, I just looked at the numbers our monkeys were using for divisibility. They were all relatively small prime numbers. I multiplied these together, and got 96577 for the sample monkeys, and 9699690 for the real monkeys. So, where we were dividing by 3 before, we now take the mod of the appropriate number instead. I also took out all the print statements because, at 10000 iterations, that would just take way too long. The new Monkey.doOneItem method looks like this.

Monkey.doOneItem = function
    if not self.items then return false
    item = self.items.pull
    self.inspectCount = self.inspectCount + 1
    item = self.op(item)
//  item = item % 96577    // for example data
    item = item % 9699690  // for real data

    if item % self.divBy == 0 then
        monkeys[self.ifTrue].items.push item
    else
        monkeys[self.ifFalse].items.push item
    end if
    return self.items.len > 0
end function
Enter fullscreen mode Exit fullscreen mode

And, I change the main loop to:

for round in range(1,10000)
    doOneRound
    if round % 1000 == 0 then
        print "Round " + round
        for i in monkeys.indexes
            print "Monkey " + i + " inspected " + monkeys[i].inspectCount + " times"
        end for
    end if
end for
Enter fullscreen mode Exit fullscreen mode

This chugged away for a few seconds, printing out its progress every 1000 iterations, ending with:

Round 10000
Monkey 0 inspected 164077 times
Monkey 1 inspected 31322 times
Monkey 2 inspected 124565 times
Monkey 3 inspected 141137 times
Monkey 4 inspected 140371 times
Monkey 5 inspected 140372 times
Monkey 6 inspected 31686 times
Monkey 7 inspected 156713 times
Most activity: 164077, 156713
Monkey business: 25712998901
Enter fullscreen mode Exit fullscreen mode

And this again was the right answer.

Final Thoughts

I felt pretty good about my solution -- time seemed to fly by as I was doing all this, and I never felt like I was spinning my wheels or wasting time. But in the end, Part A took me about 24 minutes, placing 929th. Part A and B together took 29.5 minutes, placing 513th. So not terrible, but certainly not my best.

I went to bed wondering if I made the right choice with converting the input files to code. Should I have parsed it instead? I'm thinking now of making a parser that plucks a value out of a stream of input lines, and advances to the next line, so I could write stuff like:

   m = new Monkey
   m.init parser.get("  Starting items: {list}")
   m.operation = parser.get("  Operation: {str}")
   m.divBy = parser.get{"  Test: divisible by {n}")
Enter fullscreen mode Exit fullscreen mode

and so forth. With that, it would be almost worth parsing that data. The problem though is still that "operation". I would have had to download and apply that MiniScriptEval package, and I just don't know if I could have done that in less time than it took me to text-edit that stuff into code.

But now that I know Advent of Code is likely to throw such things at us, maybe I'll get that eval package ready, and also make a convenient line-parser as imagined above. Fortune favors the prepared!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .