Day 17
This commit is contained in:
parent
fd76032ee1
commit
81cc189717
1 changed files with 334 additions and 0 deletions
334
day17.py
Normal file
334
day17.py
Normal file
|
@ -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<mu:
|
||||||
|
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 kin < mu and kin < prodigy and len(cmp_bytes(vm.outbuf, mem)) > 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<up:
|
||||||
|
up = kin
|
||||||
|
up_hit = len(cmp_bytes(vm.outbuf, mem))
|
||||||
|
up_len = len(vm.outbuf)
|
||||||
|
print(f"Found new upper bound: {up}, {up_hit=}, {up_len=}", end=" ")
|
||||||
|
vm.flush()
|
||||||
|
continue
|
||||||
|
|
||||||
|
assert(lo < mu)
|
||||||
|
assert(mu < up)
|
||||||
|
|
||||||
|
|
||||||
|
sd = sd//generation
|
||||||
|
if sd<100:
|
||||||
|
sd = round(random.uniform(0.00001, 1.0)*(up-lo))
|
||||||
|
print(f"\033[36mA fungus infected the generation and changed mutations. New mutation with {sd=}\033[0m")
|
||||||
|
|
||||||
|
print(f"{sd=}")
|
||||||
|
truncnorm = Truncnorm(mu, sd, lo, up)
|
||||||
|
generation += 1
|
||||||
|
|
||||||
|
# we evolved to a species capable of genetic engineering the very best
|
||||||
|
# prodigy we found is 164278764924544. We even found that multiple
|
||||||
|
# times. Now brute force ourselves to the top of the food chain.
|
||||||
|
#
|
||||||
|
# Our probabilistic algorithm is good a finding a a very good solution
|
||||||
|
# (up to 15 elements correct) but it is actually bad at pinpointing
|
||||||
|
# the exactly best solution.
|
||||||
|
|
||||||
|
mu = 164278764924544
|
||||||
|
for breed in range(mu-(2**(5*3)), mu+(2**(5*3))):
|
||||||
|
reg = array('q', [breed, reg_b, reg_c])
|
||||||
|
vm = VM(reg, mem, pc)
|
||||||
|
vm.run()
|
||||||
|
|
||||||
|
if vm.cmp(mem):
|
||||||
|
print(f"Self replicating {breed=}")
|
||||||
|
vm.flush()
|
||||||
|
|
||||||
|
print(reg_a)
|
||||||
|
vm.flush()
|
Loading…
Add table
Reference in a new issue