Sean McLemon | Advent of Code

Home | Czech | Blog | GitHub | Advent Of Code | Notes


2019-12-11 - Space Police

(original .ipynb)

Day 11 puzzle input is an IntCode program which is supposed to control a little robot that paints the surface of a ship (mine is here). Part 1 involves running the program and keeping track of the number of panels it paints. Part 2 involves running the program over a panel coloured white, keeping track of what the robot paints - which should spell out a registration number.

Note: this is one of the ones I disliked because a few instructions were rather vague and weirdly worded, like this:

The panel under the robot (not visible here because a ^ is shown instead) is also black, and so any input instructions at this point should be provided 0.

This "provided N" appears a few times in the text.

opcode_add = 1
opcode_mul = 2
opcode_read = 3
opcode_write = 4
opcode_jump_true = 5
opcode_jump_false = 6
opcode_lt = 7
opcode_eq = 8
opcode_rebase = 9

opcode_terminate = 99

mode_position = 0
mode_immediate = 1
mode_relative = 2

class IntCodeCpu:
    def __init__(self, memory_image):
        self.memory = [ x for x in memory_image ] # copy memory image, in case it's reused
        self.stalled = True
        self.input_buffer = None
        self.output_buffer = None
        self.pc = 0
        self.initialise_opcodes()
        self.offset = 0
        self.done = False
        
    def start(self, input_buffer, output_buffer, noun=None, verb=None):
        self.input_buffer = input_buffer
        self.output_buffer = output_buffer

        if noun:
            self.memory[1] = noun
        if verb:
            self.memory[2] = verb
           
        return self.run()
            
            
    def run(self):
        instr = self.memory[self.pc]
        self.stalled = False

        while int(instr) != opcode_terminate and not self.stalled:
            (op, modes) = self.decode_instr(instr)
            self.pc = op(modes)
            instr = self.memory[self.pc]

        return self.memory[0]

    #-HELPERS-----------------------------
    def try_pop_mode(self, modes):
        if len(modes) == 0:
            return 0
    
        return modes.pop()
    
    def resize_memory(self, target_addr):
        self.memory += ([0] * (1 + target_addr - len(self.memory)))
    
    #-DECODE-INSTRUCTIONS-----------------
    def initialise_opcodes(self):
        self.opcodes = {
            opcode_add: self.op_add,
            opcode_mul: self.op_mul,
            opcode_read: self.op_read,
            opcode_write: self.op_write,
            opcode_jump_true: self.op_jump_true,
            opcode_jump_false: self.op_jump_false,
            opcode_lt: self.op_lt,
            opcode_eq: self.op_eq,
            opcode_rebase: self.op_rebase
        }    
    
    def decode_instr(self, instr):
        instr = str(instr)
        opcode = self.decode_op(instr)
        modes = self.decode_modes(instr)

        if not (opcode in self.opcodes):
            raise Exception(f"Invalid opcode {opcode}")

        return (self.opcodes[opcode], modes)    
    
    def decode_op(self, instr):
        if len(instr) > 2:
            return int(instr[-2:])
        return int(instr)
    
    def decode_modes(self, instr):
        if len(instr) > 2:
            return [ int(d) for d in instr[:-2]]
        return []
    
    #-MICRO-OPS---------------------------
    def uop_read(self, value, mode):
        if mode == mode_position:
            if value >= len(self.memory):
                self.resize_memory(value)        

            return int(self.memory[value])

        elif mode == mode_relative:
            if self.offset + value >= len(self.memory):
                self.resize_memory(self.offset + value)     

            return int(self.memory[self.offset + value])

        elif mode == mode_immediate:
            return int(value)

        else:
            raise Exception("UNKNOWN MODE")

    def uop_write(self, dst, value, mode):
        if mode == mode_position:
            if dst >= len(self.memory):
                self.resize_memory(dst)
            self.memory[dst] = value

        elif mode == mode_relative:
            if self.offset + dst >= len(self.memory):
                self.resize_memory(self.offset + dst)     

            self.memory[self.offset + dst] = value

        elif mode == mode_immediate:
            raise Exception(f"cannot write {value} to literal {dst}")


    def uop_cond_jump(self, modes, cond):
        param_mode = self.try_pop_mode(modes)
        param_raw = int(self.memory[self.pc + 1])
        param = self.uop_read(param_raw, param_mode)

        dest_mode = self.try_pop_mode(modes)
        dest_raw = int(self.memory[self.pc + 2])
        dest = self.uop_read(dest_raw, dest_mode)

        if cond(param):
            return dest

        return self.pc + 3

    def uop_cmp(self, modes, cmp):

        param0_mode = self.try_pop_mode(modes)
        param0_raw = int(self.memory[self.pc + 1])
        param0 = self.uop_read(param0_raw, param0_mode)

        param1_mode = self.try_pop_mode(modes)
        param1_raw = int(self.memory[self.pc + 2])
        param1 = self.uop_read(param1_raw, param1_mode)

        dest_mode = self.try_pop_mode(modes)
        dest = int(self.memory[self.pc + 3])

        if cmp(param0, param1):
            self.uop_write(dest, 1, dest_mode)
        else:
            self.uop_write(dest, 0, dest_mode)

        return self.pc + 4
        
    
    #-OPCODES-----------------------------
    def op_add(self, modes):
        arg0_mode = self.try_pop_mode(modes)
        arg1_mode = self.try_pop_mode(modes)
        dest_mode = self.try_pop_mode(modes)

        arg0_raw = int(self.memory[self.pc + 1])
        arg1_raw = int(self.memory[self.pc + 2])
        dest = int(self.memory[self.pc + 3])

        arg0 = self.uop_read(arg0_raw, arg0_mode)
        arg1 = self.uop_read(arg1_raw, arg1_mode)

        self.uop_write(dest, str(int(arg0) + int(arg1)), dest_mode)
        return self.pc + 4

    def op_mul(self, modes):
        arg0_mode = self.try_pop_mode(modes)
        arg1_mode = self.try_pop_mode(modes)
        dest_mode = self.try_pop_mode(modes)

        arg0_raw = int(self.memory[self.pc + 1])
        arg1_raw = int(self.memory[self.pc + 2])
        dest = int(self.memory[self.pc + 3])

        arg0 = self.uop_read(arg0_raw, arg0_mode)
        arg1 = self.uop_read(arg1_raw, arg1_mode)

        self.uop_write(dest, str(int(arg0) * int(arg1)), dest_mode)    
        return self.pc + 4


    def op_read(self, modes):
        dest_mode = self.try_pop_mode(modes)
        dest = int(self.memory[self.pc + 1])
        
        # if the input buffer is empty, we should "stall" and 
        # resume later
        if not self.input_buffer:
            self.stalled = True
            return self.pc
        
        val = self.input_buffer.pop()

        self.uop_write(dest, str(val), dest_mode)

        return self.pc + 2

    def op_write(self, modes):
        src_mode = self.try_pop_mode(modes)
        src_raw = int(self.memory[self.pc + 1])
        src = self.uop_read(src_raw, src_mode)

        self.output_buffer.append(src)

        return self.pc + 2

    def op_jump_true(self, modes):
        return self.uop_cond_jump(modes, lambda x: x != 0)


    def op_jump_false(self, modes):
        return self.uop_cond_jump(modes, lambda x: x == 0)

    def op_lt(self, modes):
        return self.uop_cmp(modes, lambda x, y: x < y)

    def op_eq(self, modes):
        return self.uop_cmp(modes, lambda x, y: x == y)

    def op_rebase(self, modes):    
        param_mode = self.try_pop_mode(modes)
        param_raw = int(self.memory[self.pc + 1])
        param = self.uop_read(param_raw, param_mode)

        self.offset += param

        return self.pc + 2
raw_code = open("puzzle_input/day11.txt").read()
code = raw_code.split(",")


turn_left = 0
turn_right = 1

color_black = 0
color_white = 1

direction_up =    (0,  1)
direction_right = (1,  0)
direction_down =  (0, -1)
direction_left =  (-1, 0)


def find_next_direction(current_direction, turn):
    if current_direction == direction_up:
        if turn == turn_left:
            return direction_left
        else:
            return direction_right
        
    elif current_direction == direction_right:
        if turn == turn_left:
            return direction_up
        else:
            return direction_down
        
    elif current_direction == direction_down:
        if turn == turn_left:
            return direction_right
        else:
            return direction_left
        
    elif current_direction == direction_left:
        if turn == turn_left:
            return direction_down
        else:
            return direction_up
    
    raise Exception(f"Could not determine next direction, {current_direction} is not valid")
    
assert direction_up == find_next_direction(direction_right, turn_left)
assert direction_up == find_next_direction(direction_left, turn_right)
assert direction_down == find_next_direction(direction_right, turn_right)
assert direction_down == find_next_direction(direction_left, turn_left)
assert direction_left == find_next_direction(direction_up, turn_left)
assert direction_left == find_next_direction(direction_down, turn_right)
assert direction_right == find_next_direction(direction_up, turn_right)
assert direction_right == find_next_direction(direction_down, turn_left)

def next_position(current_position, current_direction, turn):
    current_x, current_y = current_position
    next_direction = find_next_direction(current_direction, turn)
    
    move_x, move_y = next_direction
    
    next_position = (current_x + move_x, current_y + move_y)
    return (next_position, next_direction)


assert (-1,  0) == next_position((0, 0), direction_up, turn_left)[0]
assert ( 1,  0) == next_position((0, 0), direction_up, turn_right)[0]
assert ( 1,  0) == next_position((0, 0), direction_down, turn_left)[0]
assert (-1,  0) == next_position((0, 0), direction_down, turn_right)[0]
assert ( 0, -1) == next_position((0, 0), direction_left, turn_left)[0]
assert ( 0,  1) == next_position((0, 0), direction_left, turn_right)[0]
assert ( 0,  1) == next_position((0, 0), direction_right, turn_left)[0]
assert ( 0, -1) == next_position((0, 0), direction_right, turn_right)[0]

def parse_output_instructions(outputs):
    instructions = []
    while outputs:
        color = outputs.pop(0)
        turn = outputs.pop(0)
        instructions.append((color, turn))
    return instructions


def color_at_position(painted_panels, position):
    
    if position not in painted_panels:
        return color_black
    
    return painted_panels[position]
    

def solve(starting_color):

    cpu = IntCodeCpu(code)
    
    input_buffer = [starting_color]
    output_buffer = []

    cpu.start(input_buffer, output_buffer)

    current_direction = direction_up
    current_position = (0, 0)

    painted_panels = {}

    while cpu.stalled:
        output_instructions = parse_output_instructions(output_buffer)

        for color, turn in output_instructions:
            painted_panels[current_position] = color
            (current_position, current_direction) = next_position(current_position, current_direction, turn)

        input_buffer.append(color_at_position(painted_panels, current_position))

        cpu.run()
        
    return painted_panels
    
print(len(solve(color_black)))
1863
from PIL import Image, ImageDraw
from IPython.display import Image as IPythonImage
from IPython.display import display

panels = solve(color_white)

min_x = min(x for (x,y) in panels if panels[(x,y)])
max_x = max(x for (x,y) in panels if panels[(x,y)])
min_y = min(y for (x,y) in panels if panels[(x,y)])
max_y = max(y for (x,y) in panels if panels[(x,y)])

offset_x = 0 - min_x
offset_y = 0 - min_y

img = Image.new("RGB", (max_y + offset_y + 20, max_x + offset_x + 20), "black")

draw = ImageDraw.Draw(img)

color_lookup = {
    0: "black",
    1: "white"
}
                
for point in panels:
    x, y = point
    color = panels[point]
    draw.point((y + offset_y + 10, x + offset_x + 10), fill=color_lookup[color])
    
img.save('img/day11.png')

display(IPythonImage(url="./img/day11.png"))