#!/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