I’m a little late to the party, but I heard about this from a fellow student, and it happened to occur during a very hectic time for me personally. Still, I wanted to work on it because it seemeed cute and fun!

Day 1: The Tyranny of the Rocket Equation

The first challenge is clearly about the Rocket Equation.

Part one challenge:

…At the first Go / No Go poll, every Elf is Go until the Fuel Counter-Upper. They haven’t determined the amount of fuel required yet.

Fuel required to launch a given module is based on its mass. Specifically, to find the fuel required for a module, take its mass, divide by three, round down, and subtract 2.

What is the sum of the fuel requirements for all of the modules on your spacecraft?

Ok, easy enough:

>>> def rocket_fuel(mass):
...     return (mass // 3) - 2

I attempted to use the requests module to get my puzzle input, but…

>>> import requests
>>> requests.get('https://adventofcode.com/2019/day/1/input')
<Response [400]>
>>> _.text
'Puzzle inputs differ by user.  Please log in to get your puzzle input.\n'

Well shoot, It might be easier just to copy-paste the puzzle input into the interpreter…

>>> text = """125860
... <pasted puzzle input>
... 132329
... 65057"""

>>> text
'125860\n66059\n147392\n64447\n72807\n136018\n144626\n68233\n130576\n92645\n52805\n79642\n74361\n98270\n110796\n62578\n58421\n125079\n52683\n144885\n148484\n113638\n125026\n112534\n125479\n51539\n122007\n60048\n67923\n76115\n144822\n115991\n133505\n85249\n142441\n90211\n87022\n68196\n117577\n58112\n116865\n108253\n127674\n93302\n58817\n126794\n89824\n134386\n99700\n125855\n119753\n64456\n68167\n88047\n127864\n146890\n71912\n128375\n134365\n91544\n104179\n84700\n95937\n78409\n94604\n130423\n98348\n87489\n105103\n94794\n123723\n134298\n88283\n59543\n53645\n89325\n109301\n143668\n96250\n130371\n140436\n95857\n98543\n91372\n137056\n142578\n116185\n96588\n93025\n122275\n99201\n110492\n109700\n106755\n120979\n60957\n134983\n130840\n132329\n65057'

>>> sum(rocket_fuel(int(x)) for x in text.split())
3427947

Which turns out to have been the correct answer for my input.

Part two challenge:

During the second Go / No Go poll, the Elf in charge of the Rocket Equation Double-Checker stops the launch sequence. Apparently, you forgot to include additional fuel for the fuel you just added.

Fuel itself requires fuel just like a module - take its mass, divide by three, round down, and subtract 2. However, that fuel also requires fuel, and that fuel requires fuel, and so on. Any mass that would require negative fuel should instead be treated as if it requires zero fuel; the remaining mass, if any, is instead handled by wishing really hard, which has no mass and is outside the scope of this calculation.

So, for each module mass, calculate its fuel and add it to the total. Then, treat the fuel amount you just calculated as the input mass and repeat the process, continuing until a fuel requirement is zero or negative.

What is the sum of the fuel requirements for all of the modules on your spacecraft when also taking into account the mass of the added fuel? (Calculate the fuel requirements for each module separately, then add them all up at the end.)

Ok, how about this

>>> answer = _
>>> extra_fuel = rocket_fuel(answer)
... while extra_fuel > 0:
...     answer += extra_fuel
...     extra_fuel = rocket_fuel(extra_fuel)

>>> answer
5141879

Nope! That would be too easy. How about this?

>>> def calculate_rocket_fuel(module_masses):
...     def rocket_fuel(mass):
...         return (mass // 3) - 2
...     initial_fuel_requirement = sum(rocket_fuel(mass) for mass in module_masses)
...     total_fuel = initial_fuel_requirement
...     extra_fuel = rocket_fuel(initial_fuel_requirement)
...     while extra_fuel > 0:
...         total_fuel += extra_fuel
...         extra_fuel = rocket_fuel(extra_fuel)
...     return total_fuel

>>> calculate_rocket_fuel(int(x) for x in text.split())
5141879

Still no… Hmm… Oh! I’m supposed to calculate the fuel requirements for each module separately according to the challenge text.

>>> def calculate_rocket_fuel(module_masses):
...     def rocket_fuel(mass):
...         return (mass // 3) - 2
...     total_fuel = 0
...     for mass in module_masses:
...         initial_fuel_requirement = rocket_fuel(mass)
...         module_fuel = initial_fuel_requirement
...         extra_fuel = rocket_fuel(initial_fuel_requirement)
...         while extra_fuel > 0:
...             module_fuel += extra_fuel
...             extra_fuel = rocket_fuel(extra_fuel)
...         total_fuel += module_fuel
...     return total_fuel

>>> calculate_rocket_fuel(int(x) for x in text.split())
5139037

That did it! Now we have the correct amount of fuel, and we’re one step closer to saving Santa!

Day 2: 1202 Program Alarm

On the way to your gravity assist around the Moon, your ship computer beeps angrily about a “1202 program alarm”. On the radio, an Elf is already explaining how to handle the situation: “Don’t worry, that’s perfectly norma–” The ship computer bursts into flames.

Ha! Must have hit the HCF instruction. Anyway, I imagine we’re supposed to do something about this… The rest of the puzzle prompt details the operation of an “Intcode Computer”. We’re supposed to implement a new one, and…

Once you have a working computer, the first step is to restore the gravity assist program (your puzzle input) to the “1202 program alarm” state it had just before the last computer caught fire. To do this, before running the program, replace position 1 with the value 12 and replace position 2 with the value 2. What value is left at position 0 after the program halts?

Ok, first thing’s first, condense the spec for Intcode:

  1. Intcode programs consist of a one dimensional list of integers
    • Each element is called a “position”
    • Positions are numbered starting from 0
    • Input files are decimal encoded, separated by commas and arbitrary amounts of whitespace (including line breaks)
  2. Execution begins at position 0
  3. Positions may be treated either as an “opcode”, or an argument to an opcode.
  4. Opcode 1 accepts three arguments (in the next three positions): a, b, c
    • arguments represent positions
    • c = a + b
  5. Opcode 2 behaves exactly like opcode 1, except c = a * b
  6. Opcode 99 accepts no arguments, and causes the program to halt.
  7. Other opcodes are illegal

Let’s jump right in. First, we learn from our mistakes in day one by saving the puzzle input to a file. Next, parse the puzzle input:

def parse_intcode(text):
    return [int(x.strip()) for x in text.split(',')]

with open('day2.txt') as f:
    prog = parse_intcode(f.read())

Ok, next we implement the intcode interpreter:

def execute_intcode(prog):
    pos = 0
    while True:
        opcode = prog[pos]
        if opcode == 1:
            a, b, c = prog[pos + 1:pos + 4]
            prog[c] = prog[a] + prog[b]
        elif opcode == 2:
            a, b, c = prog[pos + 1:pos + 4]
            prog[c] = prog[a] * prog[b]
        elif opcode == 99:
            break
        else:
            raise ValueError(f'Invalid opcode {opcode} at {pos}')
        pos += 4

Since the program is self-modifying, we should save the original state in case we mess it up.

prog_orig = [i for i in prog]

The last part of the puzzle text asks us to modify position 1 with the value 12, and position 2 with the value 2 before we run the program.

prog[1] = 12
prog[2] = 2

And finally, let’s try it out!

>>> execute_intcode(prog)
>>> prog[0]
12490719

Yay, That was correct! On to part 2…

“Good, the new computer seems to be working correctly! Keep it nearby during this mission - you’ll probably use it again. Real Intcode computers support many more features than your new one, but we’ll let you know what they are as you need them.”

The next prompts define some terminology, and then lay out the next part of the challenge:

“With terminology out of the way, we’re ready to proceed. To complete the gravity assist, you need to determine what pair of inputs produces the output 19690720.”

The inputs to the program are the values at positions 1 (called the noun) and 2 (called the verb), as given in part one, and they can range between 0 and 99, inclusive. The program output is in position 0 after the program halts.

The final answer to the puzzle is 100 * noun + verb

So, this seems to be a brute force problem. There are two input symbols with 100 possible values each, so there are 100 ** 2 == 10000 combinations of input…

Let’s get started!

def brute_force(prog, val):
    from itertools import permutations
    for noun, verb in permutations(range(100), 2):
        prog_copy = [i for i in prog]
        prog_copy[1] = noun
        prog_copy[2] = verb
        execute_intcode(prog_copy)
        out = prog_copy[0]
        if out == val:
            return 100 * noun + verb
    return None

And, let’s try it…

>>> brute_force(prog_orig, 19690720)
2003

Hooray! Our gravity assist around the Moon was successful.

Day 3: Crossed Wires

The gravity assist was successful, and you’re well on your way to the Venus refuelling station. During the rush back on Earth, the fuel management system wasn’t completely installed, so that’s next on the priority list.

Opening the front panel reveals a jumble of wires. Specifically, two wires are connected to a central port and extend outward on a grid. You trace the path each wire takes as it leaves the central port, one wire per line of text (your puzzle input).

The next part of the puzzle prompt defines a language for defining how these wires run. It consists again of comma separated values, but this time it consists of a directional prefix (Up, Down, Left, and Right) followed by a decimal integer representing how many units the wire travels in that direction.

Both wires begin from a “central port”, and the challenge is to find the Manhattan Distance between the origin, and the nearest intersection of the two wires.

First, I think a more convenient form for these wires would be as a set of line segments. Each line segment should be a 4-tuple, like (x1, y1, x2, y2). First step then, is to parse the wire directions into a list of line segments:

def parse_segments(line):
    segments = line.split(',')
    x, y = 0, 0
    out = []
    directions = {
        'U': lambda dist, x, y: (x, y + dist),
        'D': lambda dist, x, y: (x, y - dist),
        'L': lambda dist, x, y: (x - dist, y),
        'R': lambda dist, x, y: (x + dist, y),
    }
    for segment in segments:
        direction = segment[0]
        dist = int(segment[1:])
        x1, y1 = directions[segment[0]](int(segment[1:]), x, y)
        out.append((x, y, x1, y1))
        x, y = x1, y1
    return out

Now, we need to get all the intersections between all the segments of the first wire and all the segments of the second wire. Let’s first define a function that will return the coordinate of the intersection between two segments, or None if there isn’t an intersection.

Two line segments will never intersect if they are parallel, so that should be the first check. Then, we can get their theoretical intersection point by taking the x component of the vertical line and the y component of the horizontal line. (since in this problem the line segments can only be horizontal or vertical).

Finally, we check that the intersection point is within the bounds of each individual segment.

def is_vertical(seg):
    return seg[0] == seg[2]

def get_intersection(s1, s2):
    try:
        vert = [s for s in [s1, s2] if is_vertical(s)][0]
        horiz = [s for s in [s1, s2] if not is_vertical(s)][0]
        x, y = vert[0], horiz[1]
        if (x > horiz[0]) ^ (x < horiz[2]):
            return None
        if (y > vert[1]) ^ (y < vert[3]):
            return None
        return (x, y)
    except IndexError:
        return None

Now, iterate over all the possible combinations:

def get_intersections(w1, w2):
    intersections = []
    for w1_seg in w1:
        for w2_seg in w2:
            intersection = get_intersection(w1_seg, w2_seg)
            if intersection is not None:
                intersections.append(intersection)
    return intersections

All together now:

>>> with open('day3.txt') as f:
...     wires = [parse_segments(line) for line in f]
>>> intersections = get_intersections(wires[0], wires[1])
>>> closest = min(intersections, key=lambda x: abs(x[0]) + abs(x[1]))
>>> dist = abs(closest[0]) + abs(closest[1])
>>> dist
2427

And, there’s always a part two!

It turns out that this circuit is very timing-sensitive; you actually need to minimize the signal delay.

To do this, calculate the number of steps each wire takes to reach each intersection; choose the intersection where the sum of both wires’ steps is lowest. If a wire visits a position on the grid multiple times, use the steps value from the first time it visits that position when calculating the total value of a specific intersection.

Now, instead of minimizing the Manhattan distance, we have to minimize the distance along the wire paths. To do that, we have to calculate it first!

def on_segment(seg, point):
    if point[0] == seg[0] and point[0] == seg[2]:
        min_coord, max_coord = min(seg[1], seg[3]), max(seg[1], seg[3])
        coord = point[1]
    elif point[1] == seg[1] and point[1] == seg[3]:
        min_coord, max_coord = min(seg[0], seg[2]), max(seg[0], seg[2])
        coord = point[0]
    else:
        return False
    return coord > min_coord and coord < max_coord

def segment_len(seg):
    return abs(seg[2] - seg[0]) + abs(seg[3] - seg[1])

def wire_dist(wire, dest):
    dist = 0
    for seg in wire:
        if on_segment(seg, dest):
            dist += segment_len((seg[0], seg[1], dest[0], dest[1]))
            return dist
        else:
            dist += segment_len(seg)
    raise ValueError("dest does not intersect wire")

def loop_dist(w1, w2, dest):
    return wire_dist(w1, dest) + wire_dist(w2, dest)

And, to wrap it all up:

>>> closest = min(intersections, key=lambda x: loop_dist(wires[0], wires[1], x))
>>> dist = loop_dist(wires[0], wires[1], closest)
>>> dist
27890

And that’s day 3 done! Our fuel management system has been repaired.

Day 4: Secure Container

You arrive at the Venus fuel depot only to discover it’s protected by a password. The Elves had written the password on a sticky note, but someone threw it out.

Hmm, Santa should require that his employees use a password manager…

However, they do remember a few key facts about the password:

  • It is a six-digit number.
  • The value is within the range given in your puzzle input.
  • Two adjacent digits are the same (like 22 in 122345).
  • Going from left to right, the digits never decrease; they only ever increase or stay the same (like 111123 or 135679).

Other than the range rule, the following are true:

  • 111111 meets these criteria (double 11, never decreases).
  • 223450 does not meet these criteria (decreasing pair of digits 50).
  • 123789 does not meet these criteria (no double).

How many different passwords within the range given in your puzzle input meet these criteria?

Your puzzle input is 245182-790572.

So we’re making baby’s first hashcat!

Since our range is fairly small, I’m going to try just checking every integer in this range against the given rules:

def matches_rules(n):
    nstr = format(n, '06d')
    # never decreasing
    prev = 0
    for char in nstr:
        cur = int(char)
        if cur < prev:
            return False
        prev = cur
    # contains repeating digit
    prev = nstr[0]
    for char in nstr[1:]:
        if prev == char:
            return True
        prev = char

Now, we check the range…

def count_matches(start, stop):
    matches = 0
    for i in range(start, stop):
        if matches_rules(i):
            matches += 1
    return matches

Now how many passwords do we need to check?

>>> count_matches(245182, 790572)
1099

Oof. Maybe part two will provide us a solution?

An Elf just remembered one more important detail: the two adjacent matching digits are not part of a larger group of matching digits.

Ok, so we just need to modify the matches_rule function to accomodate the new rule.

def matches_rules(n):
    nstr = format(n, '06d')
    # never decreasing
    prev = 0
    for char in nstr:
        cur = int(char)
        if cur < prev:
            return False
        prev = cur
    # contains repeating digit
    prev = nstr[0]
    run = 1
    pairflag = False
    for char in nstr[1:]:
        if prev == char:
            run += 1
            if run == 2:
                pairflag = True
            elif run > 2:
                return False
        else:
            prev = char
            run = 0
    return pairflag

Ok, so how many does that leave us with?

>>> count_matches(245182, 790572)
476

Nope! That’s incorrect… What did we do wrong?

111122 meets the criteria (even though 1 is repeated more than twice, it still contains a double 22).

Oops, we were too restrictive. Let’s try this one more time…

def matches_rules(n):
    nstr = format(n, '06d')
    # never decreasing
    prev = 0
    for char in nstr:
        cur = int(char)
        if cur < prev:
            return False
        prev = cur
    # contains repeating digit
    prev = nstr[0]
    run = 1
    for char in nstr[1:]:
        if prev == char:
            run += 1
        else:
            if run == 2:
                return True
            prev = char
            run = 0
    return run == 2
>>> count_matches(245182, 790572)
516

That’s still incorrect! Let’s try a different tactic:

def clump(iterable):
    clumps = []
    current_clump = [iterable[0]]
    for x in iterable[1:]:
        if x == current_clump[0]:
            current_clump.append(x)
        else:
            clumps.append(current_clump)
            current_clump = [x]
    clumps.append(current_clump)
    return clumps

def matches_rules(n):
    nstr = format(n, '06d')
    # never decreasing
    prev = 0
    for char in nstr:
        cur = int(char)
        if cur < prev:
            return False
        prev = cur
    # contains at least one clump size 2
    clumps = clump(nstr)
    pairs = [x for x in clumps if len(x) == 2]
    return bool(pairs)
>>> count_matches(245182, 790572)
710

And that was the answer! It looks like defining the problem more explictly was the key for me. On to Day 5

Day 5: Sunny with a Chance of Asteroids

You’re starting to sweat as the ship makes its way toward Mercury. The Elves suggest that you get the air conditioner working by upgrading your ship  computer to support the Thermal Environment Supervision Terminal.

The Thermal Environment Supervision Terminal (TEST) starts by running a diagnostic program (your puzzle input). The TEST diagnostic program will run on your existing Intcode computer after a few modifications:

Well, looks like it’s time to make our intcode interpreter a bit more robust. Let’s put it into a dedicated file of it’s own with some argparse trappings:

import argparse


def parse_intcode(text):
    return [int(x.strip()) for x in text.split(',')]


def execute_intcode(prog):
    pos = 0
    while True:
        opcode = prog[pos]
        if opcode == 1:
            a, b, c = prog[pos + 1:pos + 4]
            prog[c] = prog[a] + prog[b]
        elif opcode == 2:
            a, b, c = prog[pos + 1:pos + 4]
            prog[c] = prog[a] * prog[b]
        elif opcode == 99:
            break
        else:
            raise ValueError(f'Invalid opcode {opcode} at {pos}')
        pos += 4


if __name__ == '__main__':
    p = argparse.ArgumentParser()
    p.add_argument('infile', type=argparse.FileType('r'))
    args = p.parse_args()
    execute_intcode(parse_intcode(args.infile))

Ok, now what are the modifications we need to make?

First, you’ll need to add two new instructions:

  • Opcode 3 takes a single integer as input and saves it to the position given by its only parameter. For example, the instruction 3,50 would take an input value and store it at address 50.
  • Opcode 4 outputs the value of its only parameter. For example, the instruction 4,50 would output the value at address 50.

Programs that use these instructions will come with documentation that explains what should be connected to the input and output. The program 3,0,4,0,99 outputs whatever it gets as input, then halts.

Seems easy enough, let’s add some conditions to the opcode handler:

...
elif opcode == 3:
    # input
    a = prog[pos + 1]
    prog[a] = int(input()) # placeholder
elif opcode == 4:
    # output
    a = prog[pos + 1]
    print(prog[a]) # placeholder
...

But this brings up a problem with our current design, in that we now have differently sized instructions. We can fix that easily enough by moving the pos update statement from the end of the loop body to the end of each instruction handler.

Now the second modification:

Second, you’ll need to add support for parameter modes:

Each parameter of an instruction is handled based on its parameter mode. Right now, your ship computer already understands parameter mode 0, position mode, which causes the parameter to be interpreted as a position - if the parameter is 50, its value is the value stored at address 50 in memory. Until now, all parameters have been in position mode.

Now, your ship computer will also need to handle parameters in mode 1, immediate mode. In immediate mode, a parameter is interpreted as a value - if the parameter is 50, its value is simply 50.

Parameter modes are stored in the same value as the instruction’s opcode. The opcode is a two-digit number based only on the ones and tens digit of the value, that is, the opcode is the rightmost two digits of the first value in an instruction. Parameter modes are single digits, one per parameter, read right-to-left from the opcode: the first parameter’s mode is in the hundreds digit, the second parameter’s mode is in the thousands digit, the third parameter’s mode is in the ten-thousands digit, and so on. Any missing modes are 0.

I think that this is a much less trivial modification. For maximum future-proofing, I’m pretty much going to re-write the intcode interpreter from scratch. After the description of the parameter modes modification, the instructions state that

Parameters that an instruction writes to will never be in immediate mode.

Which is great, becuase that wouldn’t make any sense: how do you write to a constant?

My rewrite consists of one main class for generic instructions, and subclasses for each particular instruction:

class Instruction:
    def __init__(self, opcode, prog, pos, verbose):
        self.prog = prog
        self.pos = pos
        self.opcode_val = opcode
        opcode //= 100
        param_modes = []
        for i in range(self.nargs):
            param_modes.append(opcode % 10)
            opcode //= 10
        self.param_modes = param_modes
        self.verbose = verbose

    def read(self, param):
        mode = self.param_modes[param]
        param_pos = self.pos + 1 + param
        if mode == 1:
            # immediate
            return self.prog[param_pos]
        elif mode == 0:
            # position
            return self.prog[self.prog[param_pos]]
        else:
            raise ValueError(f'Unknown parameter mode {mode} at {param_pos}')

    def write(self, param, val):
        mode = self.param_modes[param]
        param_pos = self.pos + 1 + param
        if mode == 0:
            # position
            self.prog[self.prog[param_pos]] = val
        else:
            # immediate is illegal for write parameters
            raise ValueError(f'Unknown parameter mode {mode} at {param_pos}')

    def run(self):
        rval = self._execute()
        return rval if rval is not None else self.pos + 1 + self.nargs

The subclasses each implement the _execute method. Each implementation uses the read and write methods to evaluate the parameters of the instruction. These methods enforce and implement the parameter modes defined in the prompt.

class IntcodeAdd(Instruction):
    opcode = 1
    nargs = 3

    def _execute(self):
        self.write(2, self.read(0) + self.read(1))


class IntcodeMultiply(Instruction):
    opcode = 2
    nargs = 3

    def _execute(self):
        self.write(2, self.read(0) * self.read(1))


class IntcodeInput(Instruction):
    opcode = 3
    nargs = 1

    def _execute(self):
        self.write(0, int(input(f'In[{self.pos}]: ')))


class IntcodeOutput(Instruction):
    opcode = 4
    nargs = 1

    def _execute(self):
        print(f'Out[{self.pos}]: {self.read(0)}')


class IntcodeHalt(Instruction):
    opcode = 99
    nargs = 0

    def _execute(self):
        return False

These instruction subclasses are combined with with a function for decoding an intcode instruction from the given program:

instructions = {
    IntcodeAdd.opcode: IntcodeAdd,
    IntcodeMultiply.opcode: IntcodeMultiply,
    IntcodeInput.opcode: IntcodeInput,
    IntcodeOutput.opcode: IntcodeOutput,
    IntcodeHalt.opcode: IntcodeHalt,
}


def decode(prog, pos, verbose):
    opcode = prog[pos]
    return instructions[opcode % 100](opcode, prog, pos, verbose)

And finally, the main function that ties it all together and executes the whole program:

def execute_intcode(prog, verbose):
    pos = 0
    while pos is not False:
        inst = decode(prog, pos, verbose)
        pos = inst.run()
$ python3 intcode.py day5.txt
# In[0]: 1
# Out[10]: 0
# Out[20]: 0
# Out[50]: 0
# Out[72]: 0
# Out[94]: 0
# Out[116]: 0
# Out[138]: 0
# Out[160]: 0
# Out[206]: 0
# Out[220]: 3122865

And 3122865 is the correct answer! On to part two…

After some more charming spacecraft flavor, we discover that we need to add a few conditional jump and comparison instructions to our intcode computer – finally making it a turing-complete model of computation!

Your computer is only missing a few opcodes:

  • Opcode 5 is jump-if-true: if the first parameter is non-zero, it sets the instruction pointer to the value from the second parameter. Otherwise, it does nothing.
  • Opcode 6 is jump-if-false: if the first parameter is zero, it sets the instruction pointer to the value from the second parameter. Otherwise, it does nothing.
  • Opcode 7 is less than: if the first parameter is less than the second parameter, it stores 1 in the position given by the third parameter. Otherwise, it stores 0.
  • Opcode 8 is equals: if the first parameter is equal to the second parameter, it stores 1 in the position given by the third parameter. Otherwise, it stores 0.

Like all instructions, these instructions need to support parameter modes as described above.

Normally, after an instruction is finished, the instruction pointer increases by the number of values in that instruction. However, if the instruction modifies the instruction pointer, that value is used and the instruction pointer is not automatically increased.

Let’s tackle the jump-if-true instruction first. When defining our run and _execute functions, we left some room for this behavior in anticipation of this need:

...
rval = self._execute()
return rval if rval is not None else self.pos + 1 + self.nargs

If the return value of _execute is not None, the run function just passes it through. Otherwise, it computes the address of the next instruction to execute and returns that instead. The exectute_intcode function uses this return value as the index for the next instruction it executes.

We simply need to return None in the case that we don’t jump, and the new execution location in the case that we do:

class IntcodeJumpIfTrue(Instruction):
    opcode = 5
    nargs = 2

    def _execute(self):
        if self.read(0) != 0:
            return self.read(1)
        else:
            return None

And that pretty much takes care of the jump-if-false instruction as well, because all we need to do is change comparison from != to ==.

Next, we tackle the comparison instructions. They are pretty straightforward:

class IntcodeLessThan(Instruction):
    opcode = 7
    nargs = 3

    def _execute(self):
        val = 1 if self.read(0) < self.read(1) else 0
        self.write(2, val)

And, in the same vein as the jump instruction, we simply change the comparison from < in the less than instruction, to == in the equals instruction.

Then, we simply add the new instructions to the instructions dict, and we’re ready to go:

instructions = {
    IntcodeAdd.opcode: IntcodeAdd,
    IntcodeMultiply.opcode: IntcodeMultiply,
    IntcodeInput.opcode: IntcodeInput,
    IntcodeOutput.opcode: IntcodeOutput,
    IntcodeJumpIfTrue.opcode: IntcodeJumpIfTrue,
    IntcodeJumpIfFalse.opcode: IntcodeJumpIfFalse,
    IntcodeLessThan.opcode: IntcodeLessThan,
    IntcodeEquals.opcode: IntcodeEquals,
    IntcodeHalt.opcode: IntcodeHalt,
}

The puzzle text says to use the same puzzle input, except that we should enter 5 at the prompt:

$ python3 intcode.py day5.txt
# In[0]: 5
# Out[674]: 773660

And with that, the first five days of the challenge are complete! This has been a lot of fun so far, even if I am now …checks watch… almost four months late! “Real life” has been quite eventful recently, and I don’t feel bad about potentially taking another four months to finish the rest of this challenge.