diff --git a/day17.py b/day17.py new file mode 100644 index 0000000..402aff5 --- /dev/null +++ b/day17.py @@ -0,0 +1,334 @@ +#!/usr/bin/env python3 + +import re +from random import randrange +from array import array +from copy import copy + +class VM(): + def __init__(self, reg: array = array('q'), mem: list = [], pc: int = 0): + self.reg = copy(reg) + self.mem = copy(mem) + self.pc = copy(pc) + self.outbuf = [] + + def run(self, abort: int = 0): + while self.pc < len(self.mem): + if abort>0 and len(self.mem) > abort: return + + op = self.mem[self.pc] + self.pc += 1 + + if op == 0b000: # adv + self.reg[0] = self.reg[0] // (2**self._combo(self.mem[self.pc])) + self.pc += 1 + if op == 0b001: # bxl + self.reg[1] = self.reg[1] ^ self.mem[self.pc] + self.pc += 1 + if op == 0b010: # bst + self.reg[1] = self._combo(self.mem[self.pc]) & 0b111 + self.pc += 1 + if op == 0b011: # jnz + self.pc = self.mem[self.pc] if self.reg[0] != 0 else self.pc+1 + if op == 0b100: # bxc + self.reg[1] = self.reg[1] ^ self.reg[2] + self.pc += 1 + if op == 0b101: # out + self.outbuf.append(self._combo(self.mem[self.pc]) & 0b111) + self.pc += 1 + if op == 0b110: # bdv + self.reg[1] = self.reg[0] // (2**self._combo(self.mem[self.pc])) + self.pc += 1 + if op == 0b111: # cdv + self.reg[2] = self.reg[0] // (2**self._combo(self.mem[self.pc])) + self.pc += 1 + + def _combo(self, c) -> int: + if c == 0: return 0 + if c == 1: return 1 + if c == 2: return 2 + if c == 3: return 3 + if c == 4: return self.reg[0] + if c == 5: return self.reg[1] + if c == 6: return self.reg[2] + raise RuntimeError() + + def flush(self, binfmt = False): + if not binfmt: + print(','.join(map(str, self.outbuf))) + else: + for o in self.outbuf: + print(f"{o:03b},", end="") + print() + + def cmp(self, prog): + return self.outbuf == prog + + +reg: array = array('q') +mem: list = [] +pc: int = 0 + +with open('day17') as f: + reg_a = int(re.match('Register A: ([0-9]+)', f.readline()).groups()[0]) + reg_b = int(re.match('Register B: ([0-9]+)', f.readline()).groups()[0]) + reg_c = int(re.match('Register C: ([0-9]+)', f.readline()).groups()[0]) + + reg = array('q', [reg_a, reg_b, reg_c]) + + f.readline() + + mem_str = re.match('Program: (.*)', f.readline()).groups()[0] + + for c in mem_str: + if c == ',': continue + mem.append(int(c)) + +# day 1 + +vm = VM(reg, mem, pc) +vm.run() +vm.flush() + +# day 2 + +# enumerates all 3 bit number, then increases size by one +# enumerates them again + +# -> we can find first boundaries by getting numbers where +# output buffer length is too small or too large + +# but first we need to find where to even start +# we know the reg_a from the puzzle input is +# too small, but start from here and increas rapidly: + +reg_a = 17323786 +while True: + reg = array('q', [reg_a, reg_b, reg_c]) + vm = VM(reg, mem, pc) + vm.run(abort=len(mem)) + if vm.cmp(mem): break + + if len(mem) == len(vm.outbuf): break + + reg_a *= 10 + + +print(reg_a) +vm.flush() + +# we get: 173237860000000 +# producing a program: 4,4,4,4,1,6,0,0,0,2,1,0,0,1,3,0 +# that's exactly the length we want + +# let's find our first set of boundaries so we can start a search +known_bytes = vm.outbuf[-2:] +init_reg_a = reg_a +upper_bound = -1 +lower_bound = -1 + +reg_a = init_reg_a +while True: + reg = array('q', [reg_a, reg_b, reg_c]) + vm = VM(reg, mem, pc) + vm.run(abort=len(mem)) + if vm.cmp(mem): break + + if len(vm.outbuf) > len(mem): + upper_bound = reg_a + break + + reg_a *= 10 + + +print(reg_a) +vm.flush() + +reg_a = init_reg_a +while True: + reg = array('q', [reg_a, reg_b, reg_c]) + vm = VM(reg, mem, pc) + vm.run(abort=len(mem)) + if vm.cmp(mem): break + + if len(vm.outbuf) < len(mem): + lower_bound = reg_a + break + + reg_a //= 10 + + +print(reg_a) +vm.flush() + + +# that was fast again and did not produce very enlightening bounds, +# nevertheless we have bounds: +# +# ub: 1732378600000000 +# out: 4,4,0,7,0,7,6,0,3,5,1,5,4,2,4,4,2 +# +# mid: 173237860000000 +# out: 4,4,4,4,1,6,0,0,0,2,1,0,0,1,3,0 +# +# lb: 17323786000000 +# out: 4,4,5,5,5,0,1,4,6,4,4,4,7,3,7 + + +# helper function +def cmp_bytes(a,b): + if len(a) != len(b): return [] + + same = [] + for i in range(1, len(a)): + if a[-i] == b[-i]: + same.append(a[-i]) + continue + break + + same.reverse() + return same + +# params for our algorithm to tune +generation = 1 +population = 1 +mu = 173237860000000 # current best guess +mu_hit = 2 # correct positions of mu +mu_len = 16 +sd = 171505481400000 # standard deviation, (up-low)*0.1 - I pulled that out of my nose +lo = 17323786000000 # lower bound +lo_hit = 0 +lo_len = 15 +up = 1732378600000000 # upper bound +up_hit = 0 +up_len = 17 + + +from day17_truncnorm import Truncnorm +truncnorm = Truncnorm(mu, sd, lo, up) + +import random + +prodigy = mu +prodigy_hit = mu_hit +prodigy_len = mu_len + +mus = [(mu, mu_hit, mu_len)] + +evolve = True +while evolve: + # each generation we birth 10000 kin. The new generation's genes center + # around the best produced offsprings so far. However, we allow for + # mutations with decreasing likelihood the more deformed the kin would be. + # + # The mutations are bounded. Mutations outside these bounds lead to + # a miscarriage. + # + # With each generation we re-asses and adapt our parameters. + # + # Occasionally a catastrophic event happens erasing the whole + # popluation. This hopefully prevents us from being trapped in a + # local optima. + # + # To preserve their legacy during these catastrophic events the mu + # collect and freeze prodigies - the most promising genes they + # know. + # + # All this allows us to search a search space probabilistically which + # would be way too large to search exhaustively. + + catastrophy = random.randint(0,100) + if catastrophy % 10 == 0: + mus.append((mu, mu_hit, mu_len)) + print(f"\033[31mA catastrophy erased the entire population. Not all is lost, a new {prodigy=} awakes.\033[0m") + mu = prodigy + mu_hit = prodigy_hit + mu_len = prodigy_len + print(f"\033[31mIn a last attempt to preserve their culture the best known mus are engraved in a stone wall:\033[0m {mus=}") + sd = round(random.uniform(0.00001, 1.0)*(up-lo)) + print(f"\033[31mEvolution restarts with {sd=}\033[0m") + population += 1 + generation = 1 + + print(f"\033[35mGeneration {generation} (Population {population}):\033[0m {mu=}, {sd=}, {lo=}, {up=}") + + + for kin in truncnorm.draw(30000): + kin = round(kin) + + reg = array('q', [kin, reg_b, reg_c]) + vm = VM(reg, mem, pc) + vm.run() + + if len(cmp_bytes(vm.outbuf, mem))>mu_hit: + mu = kin + mu_hit = len(cmp_bytes(vm.outbuf, mem)) + mu_len = len(vm.outbuf) + print(f"\033[32mFound new best kin: {mu=}, {mu_hit=}\033[0m", end=" ") + vm.flush() + continue + + if len(cmp_bytes(vm.outbuf, mem))==mu_hit and kin 3: + prodigy = kin + prodigy_hit = len(cmp_bytes(vm.outbuf, mem)) + prodigy_len = len(vm.outbuf) + print(f"Found new prodigy: {prodigy=}, {prodigy_hit=}", end=" ") + vm.flush() + + if len(vm.outbuf) < mu_len and kin>lo: + lo = kin + lo_hit = len(cmp_bytes(vm.outbuf, mem)) + lo_len = len(vm.outbuf) + print(f"Found new lower bound: {lo}, {lo_hit=}, {lo_len=}", end=" ") + vm.flush() + continue + + if len(vm.outbuf) > mu_len and kin