— 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
; output1
(if it is) or0
(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 than8
; output1
(if it is) or0
(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 to8
; output1
(if it is) or0
(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