2020-12-15

Created on Tuesday, December 15, 2020.

Day 15 - Automata

Elves count and keep track of when they last said something. This feels like a fairly simple cellular automata. We want to set it up with the right rules, set it going and stop when it reaches 2020. I'm sure there's a maths way to solve it, but I don't have that kind of brain, so lets make it simple.

We keep track of the game state, which is a dictionary of numbers spoken, and both the turn they were spoken at most recently, and the turn before.

Each turn, we look at the last number spoken, if it's never been seen, we set next number to 0, and set the last seen for the number to the turn id. If it had been spoken before, then we calculate the difference, set the last seen for the number to be the turn_id, and set the next number to the difference

We don't need to keep track of all the numnbers spoken, we just need to keep a lookup table for each number spoken that records the last time it was spoken. However, I think we're going to need to store the last 2 times, or the logic will get really messy

I don't normally use classes in this kind of python coding, I think classes can create some ugly behaviour, but in this case, I think the memory might make a good class (plus a chance to show off hte encapsulation benefits).

import ipytest
ipytest.autoconfig()
from collections import defaultdict, deque

class Memory(object):
    def __init__(self, initial):
        self.spoken = {}
        for i,v in enumerate(initial):
            self.add(i+1,v)
    def __repr__(self):
        return repr(self.spoken)
    def add(self, i, v):
        if not v in self.spoken:
            self.spoken[v] = deque([i], 2)
        else:
            self.spoken[v].append(i)
    def age(self,v):
        if v not in self.spoken:
            return 0
        elif len(self.spoken[v]) == 1:
            return 0
        else:
            return self.spoken[v][1] - self.spoken[v][0]
            
test_mem = Memory([0, 3, 6])
print(test_mem)
assert test_mem.age(6) == 0
test_mem.add(4,0)
assert test_mem.age(0) == 3
test_mem.add(5,3)
assert test_mem.age(3) == 3
test_mem.add(6,3)
assert test_mem.age(3) == 1
test_mem.add(7,1)
assert test_mem.age(1) == 0
test_mem.add(8,0)

{0: deque([1], maxlen=2), 3: deque([2], maxlen=2), 6: deque([3], maxlen=2)}

That class seems to keep track, so let's write a function that takes an initial set of numbers, and a number of iterations, and then returns the last number spoken

def play_game(initial, rounds):
    mem = Memory(initial)
    last = initial[-1]
    for x in range(len(initial), rounds):
        last = mem.age(last)
        mem.add(x+1,last)
    return last

assert play_game([0,3,6], 4) == 0
assert play_game([0,3,6], 5) == 3
assert play_game([0,3,6], 6) == 3
assert play_game([0,3,6], 7) == 1
assert play_game([0,3,6], 8) == 0

# Other examples
assert play_game([0,3,6], 10) == 0
assert play_game([0,3,6], 2020) == 436

assert play_game([1,3,2], 2020) == 1
assert play_game([2,1,3], 2020) == 10
assert play_game([1,2,3], 2020) == 27
assert play_game([2,3,1], 2020) == 78
assert play_game([3,2,1], 2020) == 438
assert play_game([3,1,2], 2020) == 1836
print(play_game([0,13,1,16,6,17], 2020))

234

Part 2 Big numbers

Well this is a surprise, I think that the code should just work, we just need to iterate more times... lets try it

assert play_game([1,3,2], 30000000) == 2578

Slow, very slow, but works... lets try the real number

print(play_game([0,13,1,16,6,17], 30000000))

8984

Next

2020-12-16

Previous

2020-12-14