In which previous bad code shoots me in the foot.
This was a continuation of the puzzle from day 2. If you don’t recall, I
made a small intcode computer that was able to add, multiply, and stop. I need
to augment this computer by adding the ability to take input and produce output.
I also need to handle “parameter modes” and expanded intcodes
First, I want to be able to handle expanded intcodes. I also wanted to ensure
it is backwards compatible with day 2. intcodes are now 5 digit numbers, where
the first three digits indicate the parameter mode for a given parameter and the
final 2 digits are the operational code.
def run(%{program: prog, position: pos} = computer) do
code =
prog
|> Enum.at(pos)
|> Integer.digits()
|> pad_code()
op(computer, code)
end
defp pad_code(code) do
List.duplicate(0, 5 - length(code)) ++ code
end
As leading zeros are not included when calling Integer.digits/2, I just pad
them at the front once I have a list so [1, 0, 2] becomes [0, 0, 1, 0, 2].
This also makes the codes from day 2 into “expanded” codes, and things will
continue to work. To handle parameter modes, I will create a helper function
that checks the mode and returns the correct value for the parameter.
defp parameter(0, x, prog), do: Enum.at(prog, x)
defp parameter(1, x, _prog), do: x
defp parameter(_, x, prog), do: parameter(0, x, prog)
A mode of 0 indicates the parameter is a pointer to somewhere else in the
program, which is exactly how day 2 operated. A mode of 1 indicates that the
value is the parameter.
- Mode
0: A parameter of3means the value is found at index3of the program list, so I must find and return it. - Mode
1: A parameter of3means the value is3, so I can return it immediately.
I added a default case that ensures the mode is 0 in case something goes
wrong, but crashing here would probably be better. As these parameter codes also
affected the output of an opcode, I created a helper to handling storing values
(it would come in handy when handling input as well).
defp store(prog, pos, v, m) do
List.update_at(prog, parameter(m, pos, prog), fn _ -> v end)
end
While I’m sure output would always be mode 0, it doesn’t hurt to handle it
just in case there’s a throwaway value. The problem also doesn’t explicitly
state one way or the other, so I’d rather be safe than sorry.
I’ll also make a small helper for shifting the position of the program, to cover all my bases.
defp shift(pos, x), do: pos + x
Again, probably unnecessary but it felt right.
Now that I have a bunch of helpers, I’ll amplify my existing op/2 functions to
handle the new modes with a new compute/3 method.
def op(computer, [a, b, c, 0, 1]),
do: {:ok, compute(computer, &Kernel.+/2, [a, c, b])}
def op(computer, [a, b, c, 0, 2]),
do: {:ok, compute(computer, &Kernel.*/2, [a, c, b])}
defp compute(%__MODULE__{program: prog, position: pos} = computer, f, [m | modes] = args) do
vals =
modes
|> Enum.with_index(1)
|> Enum.map(fn {m, i} -> parameter(m, Enum.at(prog, pos + i), prog) end)
prog = store(prog, pos + length(args), run(f, vals), m)
pos = shift(pos, length(args) + 1)
%__MODULE__{computer | program: prog, position: pos}
end
defp run(f, args) do
apply(f, args)
end
OK, so compute/3 is the big many-step process that should run the things:
- Fetch the values for the parameters for the op-code
- Run the provided elixir function for the op-code with the given parameters
- Store the result in the appropriate location
- Shift the program pointer the right number of steps
- Return an updated
%Intcode{}struct.
This is still compatible with the problem from day 2! Time for the new instructions.
Input and Output
I don’t like the idea of actually prompting for input and printing output, so I added some lists to keep track of it.
defstruct program: [],
position: 0,
input: [],
output: []
and alter new to allow the passing in of input
def new(program, input \\ []), do: %__MODULE__{program: program, input: input}
At this point, I realized I’d have to pass input and output all around, and
decided (after an hour of mulling it over), to just make some new methods for
them specifically
def op(computer, [_a, _b, c, 0, 3]),
do: {:ok, input(computer, c)}
def op(computer, [_a, _b, c, 0, 4]),
do: {:ok, output(computer, c)}
defp output(%__MODULE__{program: prog, position: pos, output: out} = computer, m) do
output = out ++ [parameter(m, Enum.at(prog, pos + 1), prog)]
pos = shift(pos, 2)
%__MODULE__{computer | program: prog, position: pos, output: output}
end
defp input(%__MODULE__{program: prog, position: pos, input: [i | t]}, m) do
prog = store(prog, pos + 1, i, m)
%__MODULE__{program: prog, position: pos + 2, input: t}
end
All that’s left is to update run_all to ensure I can access the output and do
the puzzle!
def run_all({:halt, computer}), do: computer
import AdventOfCode
alias AdventOfCode.Y2019.Utils.Intcode
aoc 2019, 5 do
def p1(), do: compute([1])
def compute(i) do
input_stream()
|> Enum.at(0)
|> String.split(",")
|> Enum.map(&String.to_integer/1)
|> Intcode.new(i)
|> Intcode.run_all()
|> Map.get(:output)
end
end
one done!
Puzzle 2
Oh wow more instructions! Let’s get hopping!
Jumps
To handle the jumps, I added a jump/4 function that checks if the provided
function is true and jumps!
def op(computer, [_a, b, c, 0, 5]),
do: {:ok, jump(computer, c, b, &Kernel.!=/2)}
def op(computer, [_a, b, c, 0, 6]),
do: {:ok, jump(computer, c, b, &Kernel.==/2)}
defp jump(%__MODULE__{program: prog, position: pos} = computer, a, b, f) do
if f.(0, parameter(a, Enum.at(prog, pos + 1), prog)) do
%__MODULE__{computer | position: parameter(b, Enum.at(prog, pos + 2), prog)}
else
%__MODULE__{computer | position: pos + 3}
end
end
Comparisons
Comparisons, thankfully, work exactly as the first two op-codes, so adding them is as simple as the following.
def op(computer, [a, b, c, 0, 7]),
do: {:ok, compute(computer, &Kernel.</2, [a, c, b])}
def op(computer, [a, b, c, 0, 8]),
do: {:ok, compute(computer, &Kernel.==/2, [a, c, b])}
After some quick testing, and more writing, puzzle 2 is done.
def p2(), do: compute([5])
Conclusion
There were a number of hiccups along the way. Again, I started way too late. I
also spent a very long time trying to make compute/3 handle all of my
op-codes. I still think its possible, but I was running out of time and needed
to get something done. I also misread a test case, so my first answer of Puzzle 2
was wrong, again staying up too late. The worst part is that my code worked
correctly when I wrote it, and the bad test case made me change it. In the end,
I was successful.
abound, and ready for the next day!