Big long hiatus since Day 4 due to, first, flying to London for the Times Crossword Championships 2019 (5th place, good but no cigar) and then flying back to discover most of the people in my company (but fortunately not me) made redundant to a drastic "rightsizing".

Day 5 builds on Day 2 to make the "IntCode Computer" a much more sophisticated machine. I'm not going to pretend I know much about object-oriented Python programming but it seemed clear this beast was becoming complex enough to warrant some organizing.

class Instruction:                                                                      
    HALT = 99                                                                           
    ADD = 1                                                                             
    MULTIPLY = 2                                                                        
    INPUT = 3                                                                           
    OUTPUT = 4                                                                          
    JUMP_IF_TRUE = 5                                                                    
    JUMP_IF_FALSE = 6                                                                   
    LESS_THAN = 7                                                                       
    EQUALS = 8                                                                          
                                                                                        
    def __init__(self, instruction):                                                    
        instruction = str(instruction)                                                  
        self.instruction = instruction                                                  
        self.opcode = int(instruction[-2:])                                             
        self.modes = map(int, reversed(instruction[:-2]))                               
        self.arity = self.get_arity()                                                   
                                                                                        
    def get_arity(self):                                                                
        if self.opcode in [                                                             
            self.ADD,                                                                   
            self.MULTIPLY,                                                              
            self.LESS_THAN,                                                             
            self.EQUALS                                                                 
        ]:                                                                              
            return 3                                                                    
        elif self.opcode in [                                                           
            self.JUMP_IF_TRUE,                                                          
            self.JUMP_IF_FALSE                                                          
        ]:                                                                              
            return 2                                                                    
        elif self.opcode in [                                                           
            self.INPUT,                                                                 
            self.OUTPUT                                                                 
        ]:                                                                              
            return 1                                                                    
        else:                                                                           
            return 0                                                                    
                                                                                        
                                                                                        
class IntCode:                                                                                                                                                           
    def __init__(self, program):                                                        
        self.program = program                                                          
        self.copy = program[:]                                                          
        self.pointer = 0                                                                
        self.output = []                                                                
        self.jump = None                                                                
                                                                                        
    def get_params(self, n):                                                            
        return self.program[self.pointer+1: self.pointer+1+n]                           
                                                                                        
    def get_modal_values(self, modes, *vals):                                           
        results = []                                                                    
        for index, value in enumerate(vals):                                            
            if len(modes) > index and modes[index] == 1:                                
                results.append(value)                                                   
            else:                                                                       
                results.append(self.copy[value])                                        
        return results                                                                  
                                                                                        
    def advance_pointer(self, arity):                                                   
        self.pointer += 1  # advance one for the opcode instruction               
        self.pointer += arity  # plus one for each argument                             
                                                                                        
    def move_pointer_to(self, loc):                                                     
        self.pointer = loc                                                              
                                                                                        
    def store(self, val, loc):                                                          
        self.copy[loc] = val                                                            
                                                                                        
    def run(self, input):                                                               
        while True:                                                                     
            instruction = Instruction(self.copy[self.pointer])                          
            opcode = instruction.opcode                                                 
            params = self.get_params(instruction.arity)                                 
            modes = instruction.modes                                                   
            if opcode == instruction.HALT:                                              
                return self.output                                                      
            elif opcode in [instruction.ADD, instruction.MULTIPLY]:                     
                [p1, p2, loc] = params                                                  
                [v1, v2] = self.get_modal_values(modes, p1, p2)                         
                if opcode == instruction.ADD:                                           
                    result = v1 + v2                                                    
                else:                                                                   
                    result = v1 * v2                                                    
                self.store(result, loc)                                                 
            elif opcode == instruction.INPUT:                                           
                [loc] = params                                                          
                self.store(input, loc)                                                  
            elif opcode == instruction.OUTPUT:                                          
                [p1] = params                                                           
                [v1] = self.get_modal_values(modes, p1)                                 
                self.output.append(v1)                                                  
            elif opcode == instruction.JUMP_IF_TRUE:                                    
                [p1, p2] = params                                                       
                [v1, v2] = self.get_modal_values(modes, p1, p2)                         
                if v1 != 0:                                                             
                    self.jump = v2                                                      
            elif opcode == instruction.JUMP_IF_FALSE:                                   
                [p1, p2] = params                                                       
                [v1, v2] = self.get_modal_values(modes, p1, p2)                         
                if v1 == 0:                                                             
                    self.jump = v2                                                      
            elif opcode == instruction.LESS_THAN:                                       
                [p1, p2, loc] = params                                                  
                [v1, v2] = self.get_modal_values(modes, p1, p2)                         
                self.store(int(v1 < v2), loc)                                           
            elif opcode == instruction.EQUALS:                                          
                [p1, p2, loc] = params                                                  
                [v1, v2] = self.get_modal_values(modes, p1, p2)                         
                self.store(int(v1 == v2), loc)                                          
            else:                                                                       
                raise Exception('unknown opcode %d' % opcode)                           
                                                                                        
            if self.jump is not None:                                                   
                self.move_pointer_to(self.jump)                                         
                self.jump = None                                                        
            else:                                                                       
                self.advance_pointer(instruction.arity)