Advent of Code 2019 Day 5 (Part 2) PHP Hints

— Day 5: Sunny with a Chance of Asteroids (Part 2) —

Hello – if you’re just visiting this blog for the first time all code can be found on GitHub here. The state of the code at the beginning of this post is release Day5Part1 (as in the end of Day5Part1). You also might like to check out Getting Started – or my walkthrough for Day5Part1 to get up to speed. I hope somebody finds this useful!

OK, so following on from Part 1 it doesn’t look like this is going to be a tiny modification!

So as before I’ve created a new test file but copied over the tests from part 1. I think I’ll have to review the best way to do this going forward as it’s a bit cumbersome… Here’s my test file:

/tests/Day5Part2Test.php

<?php

use PHPUnit\Framework\TestCase;
use PuzzleSolvers\Day5\ElfComputer3;

final class Day5Part2Test extends TestCase
{
    public function testDay2EndToEnd(): void
    {
        $outputs = [
            '3500,9,10,70,2,3,11,0,99,30,40,50',
            '2,0,0,0,99',
            '2,3,0,6,99',
            '2,4,4,5,99,9801',
            '30,1,1,4,2,5,6,0,99',
        ];
        foreach ($outputs as $inputFileNumber => $output) {
            $puzzleSolver = new ElfComputer3('day2/part1/' . $inputFileNumber);
            $puzzleSolver->initialise();
            $puzzleSolver->run();
            $this->assertSame($output, $puzzleSolver->getOutput());
        }
    }

    public function testInitialiseLoadsInputToProgramAndSetsPositionTo0(): void
    {
        $elfComputer = new ElfComputer3('day2/part1/0');

        $elfComputer->initialise();

        $this->assertSame([1, 9, 10, 3, 2, 3, 11, 0, 99, 30, 40, 50], $elfComputer->getProgram());
        $this->assertSame(0, $elfComputer->getPosition());
    }

    public function testPart1EndToEnd()
    {
        $outputs = [
            [42, 0, 4, 0, 99],
            [1002, 4, 3, 4, 99],
            [1101, 100, -1, 4, 99],
        ];
        $programOutput = [
            [42],
            [],
            [],
        ];
        foreach ($outputs as $inputFileNumber => $output) {
            $elfComputer = new ElfComputer3('day5/part1/' . $inputFileNumber);
            $elfComputer->initialise();
            $elfComputer->input = 42;
            $elfComputer->run();

            $this->assertSame($programOutput[$inputFileNumber], $elfComputer->outputs);
            $this->assertSame($outputs[$inputFileNumber], $elfComputer->program);
        }
    }

    public function testReadMemory(): void
    {
        $elfComputer = new ElfComputer3('day2/part1/0');
        $elfComputer->initialise();
        $elfComputer->setPosition(1);

        $value0 = $elfComputer->readMemory(0);
        $this->assertSame(1, $value0);

        $value1 = $elfComputer->readMemory();
        $this->assertSame(9, $value1);

        $value2 = $elfComputer->readMemory(2);
        $this->assertSame(10, $value2);
    }

    public function testReadMemoryOffset(): void
    {
        $elfComputer = new ElfComputer3('day2/part1/0');
        $elfComputer->initialise();
        $elfComputer->setPosition(1);

        $value1 = $elfComputer->readMemoryOffset(0);
        $this->assertSame(9, $value1);

        $value3 = $elfComputer->readMemoryOffset(2);
        $this->assertSame(3, $value3);
    }

    public function testWriteMemory(): void
    {
        $elfComputer = new ElfComputer3('day2/part1/0');
        $elfComputer->initialise();
        $elfComputer->setPosition(1);

        $elfComputer->writeMemory(5, 0);
        $elfComputer->writeMemory(42);

        $program = $elfComputer->getProgram();
        $this->assertSame(5, $program[0]);
        $this->assertSame(42, $program[1]);
    }

    public function testWriteMemoryOffset(): void
    {
        $elfComputer = new ElfComputer3('day2/part1/0');
        $elfComputer->initialise();
        $elfComputer->setPosition(1);

        $elfComputer->writeMemoryOffset(5, 2);
        $elfComputer->writeMemoryOffset(42);

        $program = $elfComputer->getProgram();
        $this->assertSame(5, $program[3]);
        $this->assertSame(42, $program[1]);
    }

    public function testParseInstruction()
    {
        $elfComputer = new ElfComputer3('day5/part1/0');
        $parsedInstruction = $elfComputer->parseInstruction('1002');

        $this->assertSame(2, $parsedInstruction['opcode']);
        $this->assertSame(0, $parsedInstruction['mode1']);
        $this->assertSame(1, $parsedInstruction['mode2']);
        $this->assertSame(0, $parsedInstruction['mode3']);
    }
}

And copy across our ElfComputer2 from Part 1 into ElfComputer3

/src/day5/ElfComputer3.php

<?php

namespace PuzzleSolvers\Day5;

use PuzzleSolvers\PuzzleSolver;

class ElfComputer2 extends PuzzleSolver
{
    public $program;
    public $position;
    public $input;
    public $outputs;
    public $parameterMode;
    public $instructionLength = 4;

    public function initialise()
    {
        $this->position = 0;
        $this->program = array_map('intval', explode(',', $this->inputs[0]));
        $this->outputs = [];
    }

    public function run()
    {
        while ($this->position < $this->getProgramSize()) {
            $instruction = $this->parseInstruction($this->readMemory());
            $this->execute($instruction);

            $this->position += $this->instructionLength;
        }

        $this->output = implode(',', $this->program);
    }

    public function parseInstruction($instruction)
    {
        $parsedInstuction = [];
        $parsedInstuction['opcode'] = (int) substr($instruction, -2, 2);
        $parsedInstuction['mode1'] = (strlen($instruction) > 2) ? (int) substr($instruction, -3, 1) : 0;
        $parsedInstuction['mode2'] = (strlen($instruction) > 3) ? (int) substr($instruction, -4, 1) : 0;
        $parsedInstuction['mode3'] = (strlen($instruction) > 4) ? (int) substr($instruction, -5, 1) : 0;

        return $parsedInstuction;
    }

    public function getParameter(int $offset, int $mode)
    {
        $parameter = $this->readMemoryOffset($offset);
        if ($mode == 0) {
            $parameter = $this->readMemory($parameter);
        }

        return $parameter;
    }

    public function execute($instruction)
    {
        $opcode = $instruction['opcode'];

        if ($opcode === 1) {
            $parameter1 = $this->getParameter(1, $instruction['mode1']);
            $parameter2 = $this->getParameter(2, $instruction['mode2']);
            $parameter3 = $this->readMemoryOffset(3);
            $value = $parameter1 + $parameter2;
            $this->writeMemory($value, $parameter3);
            $this->instructionLength = 4;
        }

        if ($opcode === 2) {
            $parameter1 = $this->getParameter(1, $instruction['mode1']);
            $parameter2 = $this->getParameter(2, $instruction['mode2']);
            $parameter3 = $this->readMemoryOffset(3);
            $value = $parameter1 * $parameter2;
            $this->writeMemory($value, $parameter3);
            $this->instructionLength = 4;
        }

        if ($opcode == 3) {
            $this->writeMemory($this->input, $this->readMemoryOffset(1));
            $this->instructionLength = 2;
        }

        if ($opcode == 4) {
            $parameter1 = $this->getParameter(1, $instruction['mode1']);
            $this->outputs[] = $parameter1;
            $this->instructionLength = 2;
        }

        if ($opcode === 99) {
            $this->position = $this->getProgramSize();
        }
    }

    public function getProgramSize(): int
    {
        return count($this->program);
    }

    public function readMemory($position = null)
    {
        if (is_null($position)) {
            $position = $this->position;
        }

        return $this->program[$position];
    }

    public function readMemoryOffset($offset = 0)
    {
        return $this->program[$this->position + $offset];
    }

    public function writeMemoryOffset(int $value, $offset = 0)
    {
        $this->program[$this->position + $offset] = $value;
    }

    public function writeMemory(int $value, $position = null)
    {
        if (is_null($position)) {
            $position = $this->position;
        }
        $this->program[$position] = $value;
    }

    public function getProgram(): array
    {
        return $this->program ?: [];
    }

    public function getPosition(): int
    {
        return $this->position ?: 0;
    }

    public function setPosition(int $position): void
    {
        $this->position = $position;
    }
}

And check that make tests all pass. Now let’s add our new input example files and some end-to-end tests for this part let’s start with the first sequence

Using position mode, consider whether the input is equal to 8; output 1 (if it is) or 0 (if it is not)

/inputs/day5/part2/0

3,9,8,9,10,9,4,9,99,-1,8

Let’s copy this same input file into /inputs/day5/part2/1 and /inputs/day5/part2/2 as well so we can write a test case where input is lower, equal and greater

public function testEndToEnd()
{
    $outputs = [
        [0],
        [1],
        [0],
    ];
    $inputs = [
        7,
        8,
        9,
    ];
    foreach ($outputs as $inputFileNumber => $output) {
        \PuzzleDebugger::print('TESTING FILE: ' . $inputFileNumber);
        $elfComputer = new ElfComputer3('day5/part2/' . $inputFileNumber);
        $elfComputer->initialise();
        $elfComputer->input = $inputs[$inputFileNumber];
        $elfComputer->run();

        $this->assertSame($outputs[$inputFileNumber], $elfComputer->outputs);
    }
}

So let’s have a go at implementing opcode 8

if ($opcode == 8) {
    $parameter1 = $this->getParameter(1, $instruction['mode1']);
    $parameter2 = $this->getParameter(2, $instruction['mode2']);
    $parameter3 = $this->readMemoryOffset(3);
    $value = ($parameter1 == $parameter2) ? 1 : 0;
    $this->writeMemory($value, $parameter3);
    $this->instructionLength = 4;
}

Ok this passes now let’s try the next example – I’m adding two test files again

3,9,7,9,10,9,4,9,99,-1,8 – Using position mode, consider whether the input is less than 8; output 1 (if it is) or 0 (if it is not)

/inputs/day5/part2/3, /inputs/day5/part2/4

3,9,7,9,10,9,4,9,99,-1,8

And the end-to-end test becomes

public function testEndToEnd()
{
    $outputs = [
        [0],
        [1],
        [0],
        [0],
        [1],
    ];
    $inputs = [
        7,
        8,
        9,
        8,
        7,
    ];
    foreach ($outputs as $inputFileNumber => $output) {
        \PuzzleDebugger::print('TESTING FILE: ' . $inputFileNumber);
        $elfComputer = new ElfComputer3('day5/part2/' . $inputFileNumber);
        $elfComputer->initialise();
        $elfComputer->input = $inputs[$inputFileNumber];
        $elfComputer->run();

        $this->assertSame($outputs[$inputFileNumber], $elfComputer->outputs);
    }
}

Now let’s implement opcode 7 similar to 8

if ($opcode == 7) {
    $parameter1 = $this->getParameter(1, $instruction['mode1']);
    $parameter2 = $this->getParameter(2, $instruction['mode2']);
    $parameter3 = $this->readMemoryOffset(3);
    $value = ($parameter1 < $parameter2) ? 1 : 0;
    $this->writeMemory($value, $parameter3);
    $this->instructionLength = 4;
}

And make tests all pass 🙂 Now let’s look at the next example – I’m adding two test files again

3,3,1108,-1,8,3,4,3,99 – Using immediate mode, consider whether the input is equal to 8; output 1 (if it is) or 0 (if it is not).

/inputs/day5/part2/5, /inputs/day5/part2/6

3,3,1108,-1,8,3,4,3,99

And our end-to-end test becomes

public function testEndToEnd()
{
    $outputs = [
        [0],
        [1],
        [0],
        [0],
        [1],
        [0],
        [1],
    ];
    $inputs = [
        7,
        8,
        9,
        8,
        7,
        55,
        8,
    ];
    foreach ($outputs as $inputFileNumber => $output) {
        \PuzzleDebugger::print('TESTING FILE: ' . $inputFileNumber);
        $elfComputer = new ElfComputer3('day5/part2/' . $inputFileNumber);
        $elfComputer->initialise();
        $elfComputer->input = $inputs[$inputFileNumber];
        $elfComputer->run();

        $this->assertSame($outputs[$inputFileNumber], $elfComputer->outputs);
    }
}

This passes first time so lets add the next example. I’m adding two test files again

3,3,1107,-1,8,3,4,3,99 – Using immediate mode, consider whether the input is less than 8; output 1 (if it is) or 0 (if it is not).

/inputs/day5/part2/7, /inputs/day5/part2/8

3,3,1107,-1,8,3,4,3,99

And the end-to-end tests become

public function testEndToEnd()
{
    $outputs = [
        [0],
        [1],
        [0],
        [0],
        [1],
        [0],
        [1],
        [0],
        [1],
    ];
    $inputs = [
        7,
        8,
        9,
        8,
        7,
        55,
        8,
        8,
        7,
    ];
    foreach ($outputs as $inputFileNumber => $output) {
        \PuzzleDebugger::print('TESTING FILE: ' . $inputFileNumber);
        $elfComputer = new ElfComputer3('day5/part2/' . $inputFileNumber);
        $elfComputer->initialise();
        $elfComputer->input = $inputs[$inputFileNumber];
        $elfComputer->run();

        $this->assertSame($outputs[$inputFileNumber], $elfComputer->outputs);
    }
}

And these pass first time as well 🙂 So now it’s time to look at the jump commands… For some reason I missed the two small examples and went straight for the big one but added it 3 times to test less than, equal and greater than

/inputs/day5/part2/9, /inputs/day5/part2/10, /inputs/day5/part2/11

3,21,1008,21,8,20,1005,20,22,107,8,21,20,1006,20,31,1106,0,36,98,0,0,1002,21,125,20,4,20,1105,1,46,104,999,1105,1,46,1101,1000,1,20,4,20,1105,1,46,98,99

The above example program uses an input instruction to ask for a single number. The program will then output 999 if the input value is below 8, output 1000 if the input value is equal to 8, or output 1001 if the input value is greater than 8.

And here’s the end-to-end test

public function testEndToEnd()
{
    $outputs = [
        [0],
        [1],
        [0],
        [0],
        [1],
        [0],
        [1],
        [0],
        [1],
        [999],
        [1000],
        [1001],
    ];
    $inputs = [
        7,
        8,
        9,
        8,
        7,
        55,
        8,
        8,
        7,
        7,
        8,
        9,
    ];
    foreach ($outputs as $inputFileNumber => $output) {
        \PuzzleDebugger::print('TESTING FILE: ' . $inputFileNumber);
        $elfComputer = new ElfComputer3('day5/part2/' . $inputFileNumber);
        $elfComputer->initialise();
        $elfComputer->input = $inputs[$inputFileNumber];
        $elfComputer->run();

        $this->assertSame($outputs[$inputFileNumber], $elfComputer->outputs);
    }
}

Now back to the opcode 5 and 6 instructions

  • 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.

the first thing that I can see is that we should not be using a computer wide instructionLength. Let’s refactor that so that each instruction just updates the position directly. We also have to ensure that if no opcode is executed that we increment the position by 1 – or else the computer will just hang forever – so I moved everything inside if/else statements with a default

if ($opcode === 1) {
    $parameter1 = $this->getParameter(1, $instruction['mode1']);
    $parameter2 = $this->getParameter(2, $instruction['mode2']);
    $parameter3 = $this->readMemoryOffset(3);
    $value = $parameter1 + $parameter2;
    $this->writeMemory($value, $parameter3);
    $this->position += 4;
}

else if ($opcode === 2) {
    $parameter1 = $this->getParameter(1, $instruction['mode1']);
    $parameter2 = $this->getParameter(2, $instruction['mode2']);
    $parameter3 = $this->readMemoryOffset(3);
    $value = $parameter1 * $parameter2;
    $this->writeMemory($value, $parameter3);
    $this->position += 4;
}

else if ($opcode == 3) {
    $this->writeMemory($this->input, $this->readMemoryOffset(1));
    $this->position += 2;
}

else if ($opcode == 4) {
    $parameter1 = $this->getParameter(1, $instruction['mode1']);
    $this->outputs[] = $parameter1;
    $this->position += 2;
}

else if ($opcode == 7) {
    $parameter1 = $this->getParameter(1, $instruction['mode1']);
    $parameter2 = $this->getParameter(2, $instruction['mode2']);
    $parameter3 = $this->readMemoryOffset(3);
    $value = ($parameter1 < $parameter2) ? 1 : 0;
    $this->writeMemory($value, $parameter3);
    $this->position += 4;
}

else if ($opcode == 8) {
    $parameter1 = $this->getParameter(1, $instruction['mode1']);
    $parameter2 = $this->getParameter(2, $instruction['mode2']);
    $parameter3 = $this->readMemoryOffset(3);
    $value = ($parameter1 == $parameter2) ? 1 : 0;
    $this->writeMemory($value, $parameter3);
    $this->position += 4;
}

else if ($opcode === 99) {
    $this->position = $this->getProgramSize();
} else {
    $this->position += 1;
}

And remove it from run()

public function run()
{
    while ($this->position < $this->getProgramSize()) {
        $instruction = $this->parseInstruction($this->readMemory());
        $this->execute($instruction);
    }

    $this->output = implode(',', $this->program);
}

now make tests completes but throws a mysterious error

Day5 Part2
 ✘ End to end
   │
   │ Undefined offset: 1002
   │
   │ /home/hamish/sites/adventofcode2019/src/day5/ElfComputer3.php:123
   │ /home/hamish/sites/adventofcode2019/src/day5/ElfComputer3.php:48
   │ /home/hamish/sites/adventofcode2019/src/day5/ElfComputer3.php:88
   │ /home/hamish/sites/adventofcode2019/src/day5/ElfComputer3.php:27
   │ /home/hamish/sites/adventofcode2019/tests/Day5Part2Test.php:93

So let’s have a go at implementing opcode 5 and 6

else if ($opcode == 5) {
    $parameter1 = $this->getParameter(1, $instruction['mode1']);
    $parameter2 = $this->getParameter(2, $instruction['mode2']);
    if ($parameter1 != 0) {
        $this->position = $parameter2;
    } else {
        $this->position += 3;
    }
}

else if ($opcode == 6) {
    $parameter1 = $this->getParameter(1, $instruction['mode1']);
    $parameter2 = $this->getParameter(2, $instruction['mode2']);
    if ($parameter1 == 0) {
        $this->position = $parameter2;
    } else {
        $this->position += 3;
    }
}

Now make tests are all passing! Let’s update our solve.php

/src/day5/solve.php

<?php

require_once __DIR__ . '/../../vendor/autoload.php';

use PuzzleSolvers\Day5\ElfComputer2;
use PuzzleSolvers\Day5\ElfComputer3;

$elfComputer = new ElfComputer2('day5/part1/input');
$elfComputer->initialise();
$elfComputer->input = 1;
$elfComputer->run();
$outputs = $elfComputer->outputs;

echo "Day 5 Part 1 answer: " . end($outputs) . "\n";

$elfComputer = new ElfComputer3('day5/part1/input');
$elfComputer->initialise();
$elfComputer->input = 5;
$elfComputer->run();
$outputs = $elfComputer->outputs;

echo "Day 5 Part 2 answer: " . end($outputs) . "\n";

And here is the final ElfComputer3 Class

<?php

namespace PuzzleSolvers\Day5;

use PuzzleSolvers\PuzzleSolver;

class ElfComputer3 extends PuzzleSolver
{
    public $program;
    public $position;
    public $input;
    public $outputs;

    public function initialise()
    {
        $this->position = 0;
        $this->program = array_map('intval', explode(',', $this->inputs[0]));
        $this->outputs = [];
    }

public function run()
{
    while ($this->position < $this->getProgramSize()) {
        $instruction = $this->parseInstruction($this->readMemory());
        $this->execute($instruction);
    }

    $this->output = implode(',', $this->program);
}

    public function parseInstruction($instruction)
    {
        $parsedInstuction = [];
        $parsedInstuction['opcode'] = (int) substr($instruction, -2, 2);
        $parsedInstuction['mode1'] = (strlen($instruction) > 2) ? (int) substr($instruction, -3, 1) : 0;
        $parsedInstuction['mode2'] = (strlen($instruction) > 3) ? (int) substr($instruction, -4, 1) : 0;
        $parsedInstuction['mode3'] = (strlen($instruction) > 4) ? (int) substr($instruction, -5, 1) : 0;

        return $parsedInstuction;
    }

    public function getParameter(int $offset, int $mode)
    {
        $parameter = $this->readMemoryOffset($offset);
        if ($mode == 0) {
            $parameter = $this->readMemory($parameter);
        }

        return $parameter;
    }

    public function execute($instruction)
    {
        $opcode = $instruction['opcode'];

        if ($opcode === 1) {
            $parameter1 = $this->getParameter(1, $instruction['mode1']);
            $parameter2 = $this->getParameter(2, $instruction['mode2']);
            $parameter3 = $this->readMemoryOffset(3);
            $value = $parameter1 + $parameter2;
            $this->writeMemory($value, $parameter3);
            $this->position += 4;
        }

        else if ($opcode === 2) {
            $parameter1 = $this->getParameter(1, $instruction['mode1']);
            $parameter2 = $this->getParameter(2, $instruction['mode2']);
            $parameter3 = $this->readMemoryOffset(3);
            $value = $parameter1 * $parameter2;
            $this->writeMemory($value, $parameter3);
            $this->position += 4;
        }

        else if ($opcode == 3) {
            $this->writeMemory($this->input, $this->readMemoryOffset(1));
            $this->position += 2;
        }

        else if ($opcode == 4) {
            $parameter1 = $this->getParameter(1, $instruction['mode1']);
            $this->outputs[] = $parameter1;
            $this->position += 2;
        }

        else if ($opcode == 5) {
            $parameter1 = $this->getParameter(1, $instruction['mode1']);
            $parameter2 = $this->getParameter(2, $instruction['mode2']);
            if ($parameter1 != 0) {
                $this->position = $parameter2;
            } else {
                $this->position += 3;
            }
        }

        else if ($opcode == 6) {
            $parameter1 = $this->getParameter(1, $instruction['mode1']);
            $parameter2 = $this->getParameter(2, $instruction['mode2']);
            if ($parameter1 == 0) {
                $this->position = $parameter2;
            } else {
                $this->position += 3;
            }
        }

        else if ($opcode == 7) {
            $parameter1 = $this->getParameter(1, $instruction['mode1']);
            $parameter2 = $this->getParameter(2, $instruction['mode2']);
            $parameter3 = $this->readMemoryOffset(3);
            $value = ($parameter1 < $parameter2) ? 1 : 0;
            $this->writeMemory($value, $parameter3);
            $this->position += 4;
        }

        else if ($opcode == 8) {
            $parameter1 = $this->getParameter(1, $instruction['mode1']);
            $parameter2 = $this->getParameter(2, $instruction['mode2']);
            $parameter3 = $this->readMemoryOffset(3);
            $value = ($parameter1 == $parameter2) ? 1 : 0;
            $this->writeMemory($value, $parameter3);
            $this->position += 4;
        }

        else if ($opcode === 99) {
            $this->position = $this->getProgramSize();
        } else {
            $this->position += 1;
        }
    }

    public function getProgramSize(): int
    {
        return count($this->program);
    }

    public function readMemory($position = null)
    {
        if (is_null($position)) {
            $position = $this->position;
        }

        return $this->program[$position];
    }

    public function readMemoryOffset($offset = 0)
    {
        return $this->program[$this->position + $offset];
    }

    public function writeMemoryOffset(int $value, $offset = 0)
    {
        $this->program[$this->position + $offset] = $value;
    }

    public function writeMemory(int $value, $position = null)
    {
        if (is_null($position)) {
            $position = $this->position;
        }
        $this->program[$position] = $value;
    }

    public function getProgram(): array
    {
        return $this->program ?: [];
    }

    public function getPosition(): int
    {
        return $this->position ?: 0;
    }

    public function setPosition(int $position): void
    {
        $this->position = $position;
    }
}

Now let’s get that star!

$ php src/day5/solve.php 
Day 5 Part 1 answer: 7692125
Day 5 Part 2 answer: 14340395

The final code for this part can be found on GitHub here. Remember my answer may not be the same as yours for your real input. Make sure to replace your /inputs/day5/part1/input with your own file. Did it work? Let me know!

Next up: Day 6 Part 1

Leave a comment

Your email address will not be published. Required fields are marked *