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

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

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

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 Day4Part2 (as in the end of Day4Part2). You also might like to check out Getting Started – or my solution for Day2Part2 to get up to speed. I hope somebody finds this useful!

So in this puzzle it looks like we’re revisiting the intcode computer from Day 2 – or as I’ve called it the ElfComputer. Judging by the comments on Reddit we’ll be returning to the intcode computer several times… It looks like we’ll be adding operations for inputs and outputs as well as introducing an immediate parameter mode.

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

So let’s start by creating a new ElfComputer2 which is just a copy from Day 2 Part 2 – as I don’t want to affect my solution from that part. But I will carry forward the end-to-end tests from Part2 to make sure we don’t break them.

/tests/Day5Part1Test.php

<?php

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

final class Day5Part1Test 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 ElfComputer2('day2/part1/' . $inputFileNumber);
            $puzzleSolver->initialise();
            $puzzleSolver->run();
            $this->assertSame($output, $puzzleSolver->getOutput());
        }
    }

    public function testInitialiseLoadsInputToProgramAndSetsPositionTo0(): void
    {
        $elfComputer = new ElfComputer2('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 testReadMemory(): void
    {
        $elfComputer = new ElfComputer2('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 ElfComputer2('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 ElfComputer2('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 ElfComputer2('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]);
    }
}

Now let’s create some end-to-end tests for the 3 examples provided in the explanation:

/inputs/day5/0

3,0,4,0,99

/inputs/day5/1

1002,4,3,4,33

/inputs/day5/2

1101,100,-1,4,0
public function testEndToEnd()
{
    $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 ElfComputer2('day5/part1/' . $inputFileNumber);
        $elfComputer->initialise();
        $elfComputer->input = 42;
        $elfComputer->run();

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

Now in this test I’ve assumed two new properties in the ElfComputer input and outputs. It appears that we can accept 1 input but produce multiple outputs.

Running make tests shows us that this is failing. So let’s try and make the first input sequence 3,0,4,0,99 pass which should take an input 42 and write it to position 0 then output what is at position 0 and halt. So basically take an input and add it to the outputs.

First let’s add the new properties in ElfComputer2. We also need to add an instruction length as these instructions are only 2 integers long. I also decided to just make everything public for convenience…

public $program;
public $position;
public $input;
public $outputs;
public $instructionLength = 4;

Let’s try and add the opcodes for them

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

if ($opcode == 4) {
    $operand1Position = $this->readMemoryOffset(1);
    $this->outputs[] = $this->readMemory($operand1Position);
    $this->instructionLength = 2;
}

Next we need to increment the position pointer by the instruction length rather than 4:

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

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

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

Now running make tests is passing for our first input file! Looking at the second file 1002,4,3,4,33 we are going to have to implement parameter modes by parsing the instruction. So let’s write a test for this first:

public function testParseInstruction()
{
    $elfComputer = new ElfComputer2('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']);
}

the rightmost two digits of the first value, 02, indicate opcode 2, multiplication. Then, going right to left, the parameter modes are 0 (hundreds digit), 1 (thousands digit), and 0 (ten-thousands digit, not present and therefore zero)

Now let’s make that test pass…

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;
}
Day5 Part1
 ✘ End to end
   │
   │ Failed asserting that null is identical to Array &0 ().
   │
   │ /home/hamish/sites/adventofcode2019/tests/Day5Part1Test.php:43

Looks like we need to initialise outputs to an empty array:

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

Next we need to modify our execute and run function so that we’re dealing with string “instructions” instead of simple opcodes:

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 execute($instruction)
{
    $opcode = $instruction['opcode'];

...

Now our test is showing that we’re getting close:

Day5 Part1
 ✘ End to end
   │
   │ Failed asserting that two arrays are identical.
   │ --- Expected
   │ +++ Actual
   │ @@ @@
   │      1 => 4
   │      2 => 3
   │      3 => 4
   │ -    4 => 99
   │ +    4 => 132
   │  )
   

Now we need to modify our opcode execution blocks to respect the parameter modes being passed in the instruction. First I added this function:

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

    return $parameter;
}

And then execute becomes:

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();
    }
}

Note that I added instruction lengths to each opcode and that writeMemory instructions are always in parameter mode. Now let’s run make tests again and hooray – now our end-to-end tests pass as well! Let’s get our puzzle input and write our solve.php script

/src/day5/solve.php

<?php

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

use PuzzleSolvers\Day5\ElfComputer2;

$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";

/inputs/day5/part1/input

3,225,1,225,6,6,1100,1,238,225,104,0,1002,114,46,224,1001,224,-736,224,4,224,1002,223,8,223,1001,224,3,224,1,223,224,223,1,166,195,224,1001,224,-137,224,4,224,102,8,223,223,101,5,224,224,1,223,224,223,1001,169,83,224,1001,224,-90,224,4,224,102,8,223,223,1001,224,2,224,1,224,223,223,101,44,117,224,101,-131,224,224,4,224,1002,223,8,223,101,5,224,224,1,224,223,223,1101,80,17,225,1101,56,51,225,1101,78,89,225,1102,48,16,225,1101,87,78,225,1102,34,33,224,101,-1122,224,224,4,224,1002,223,8,223,101,7,224,224,1,223,224,223,1101,66,53,224,101,-119,224,224,4,224,102,8,223,223,1001,224,5,224,1,223,224,223,1102,51,49,225,1101,7,15,225,2,110,106,224,1001,224,-4539,224,4,224,102,8,223,223,101,3,224,224,1,223,224,223,1102,88,78,225,102,78,101,224,101,-6240,224,224,4,224,1002,223,8,223,101,5,224,224,1,224,223,223,4,223,99,0,0,0,677,0,0,0,0,0,0,0,0,0,0,0,1105,0,99999,1105,227,247,1105,1,99999,1005,227,99999,1005,0,256,1105,1,99999,1106,227,99999,1106,0,265,1105,1,99999,1006,0,99999,1006,227,274,1105,1,99999,1105,1,280,1105,1,99999,1,225,225,225,1101,294,0,0,105,1,0,1105,1,99999,1106,0,300,1105,1,99999,1,225,225,225,1101,314,0,0,106,0,0,1105,1,99999,1107,226,677,224,102,2,223,223,1006,224,329,101,1,223,223,1108,226,677,224,1002,223,2,223,1005,224,344,101,1,223,223,8,226,677,224,102,2,223,223,1006,224,359,1001,223,1,223,1007,226,677,224,1002,223,2,223,1005,224,374,101,1,223,223,1008,677,677,224,1002,223,2,223,1005,224,389,1001,223,1,223,1108,677,226,224,1002,223,2,223,1006,224,404,1001,223,1,223,1007,226,226,224,1002,223,2,223,1005,224,419,1001,223,1,223,1107,677,226,224,1002,223,2,223,1006,224,434,101,1,223,223,108,677,677,224,1002,223,2,223,1005,224,449,1001,223,1,223,1107,677,677,224,102,2,223,223,1005,224,464,1001,223,1,223,108,226,226,224,1002,223,2,223,1006,224,479,1001,223,1,223,1008,226,226,224,102,2,223,223,1005,224,494,101,1,223,223,108,677,226,224,102,2,223,223,1005,224,509,1001,223,1,223,8,677,226,224,1002,223,2,223,1006,224,524,101,1,223,223,7,226,677,224,1002,223,2,223,1006,224,539,101,1,223,223,7,677,226,224,102,2,223,223,1006,224,554,1001,223,1,223,7,226,226,224,1002,223,2,223,1006,224,569,101,1,223,223,107,677,677,224,102,2,223,223,1006,224,584,101,1,223,223,1108,677,677,224,102,2,223,223,1006,224,599,1001,223,1,223,1008,677,226,224,1002,223,2,223,1005,224,614,1001,223,1,223,8,677,677,224,1002,223,2,223,1006,224,629,1001,223,1,223,107,226,677,224,1002,223,2,223,1006,224,644,101,1,223,223,1007,677,677,224,102,2,223,223,1006,224,659,101,1,223,223,107,226,226,224,1002,223,2,223,1006,224,674,1001,223,1,223,4,223,99,226

And here is the final ElfComputer2 Class

<?php

namespace PuzzleSolvers\Day5;

use PuzzleSolvers\PuzzleSolver;

class ElfComputer2 extends PuzzleSolver
{
    public $program;
    public $position;
    public $input;
    public $outputs;
    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;
    }
}

Now let’s get that gold star!

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

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 5 Part 2

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

— Day 4: Secure Container (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 Day4Part1 (as in the end of Day4Part1). You also might like to check out Getting Started – or my solution for Day4Part1 to get up to speed. I hope somebody finds this useful!

Following on from Part 1 – it looks like this is only a minor change to our testHasTwoAdjacentSameDigits()

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

From the examples it sounds like we just need to reject passwords that only have 3 or more repeating digits.

I’m going to go through the motions of setting up the tests as I’ve done in previous days so far and duplicating the PasswordCracker to PasswordCracker2 to maintain backwards compatibility:

/tests/Day4Part2Test.php

<?php

use PHPUnit\Framework\TestCase;
use PuzzleSolvers\Day4\PasswordCracker2;

final class Day4Part2Test extends TestCase
{
    public function testHasTwoAdjacentSameDigits()
    {
        $passwordCracker = new PasswordCracker2('day4/part1/sample_range');

        $passwordCracker->password = '112233';
        $this->assertTrue($passwordCracker->hasTwoAdjacentSameDigits());

        $passwordCracker->password = '111122';
        $this->assertTrue($passwordCracker->hasTwoAdjacentSameDigits());

        $passwordCracker->password = '123444';
        $this->assertFalse($passwordCracker->hasTwoAdjacentSameDigits());

        $passwordCracker->password = '223333';
        $this->assertTrue($passwordCracker->hasTwoAdjacentSameDigits());
    }
}

make tests are failing so let’s modify our hasTwoAdjacentSameDigits() function. I’m sure this can be done with a simple regex change… But I resorted to looping through the characters:

public function hasTwoAdjacentSameDigits()
{
    $i = 1;
    $repeatingCount = 1;
    while ($i < strlen($this->password)) {
        if ($this->password[$i] != $this->password[$i - 1] && $repeatingCount == 2) {
            return true;
        }

        if ($this->password[$i] == $this->password[$i - 1]) {
            $repeatingCount++;
        } else {
            $repeatingCount = 1;
        }

        if ($i == strlen($this->password) - 1 && $repeatingCount == 2) {
            return true;
        }

        $i++;
    }

    return false;
}

Essentially I’m counting the consecutive occurance of a digit. If consecutive – increase the count. Otherwise set it back to 1.

If the next digit doesn’t match – but we just had 2 in a row then the password is valid.

If we are at the end of the string and we just had 2 in a row then the password is valid.

Otherwise the password is not valid.

OK make tests is now passing. Here is my final class:

<?php

namespace PuzzleSolvers\Day4;

use PuzzleSolvers\PuzzleSolver;

class PasswordCracker2 extends PuzzleSolver
{
    public $lowerRange;
    public $upperRange;
    public $password;

    public function __construct($filename)
    {
        parent::__construct($filename);

        $inputPieces = explode('-', $this->inputs[0]);
        $this->lowerRange = $inputPieces[0];
        $this->upperRange = $inputPieces[1];
    }

    public function run()
    {
        $validPasswords = [];
        $invalidPasswords = [];
        $this->password = $this->lowerRange;
        while ($this->password <= $this->upperRange) {
            if (
                $this->hasSixDigits()
                && $this->isWithinRange()
                && $this->hasTwoAdjacentSameDigits()
                && $this->hasDigitsNeverDecrease()
            ) {
                $validPasswords[] = $this->password;
            } else {
                $invalidPasswords[] = $this->password;
            }

            $this->password = (string) ($this->password + 1);
        }

        $this->output = count($validPasswords);
    }

    public function hasSixDigits()
    {
        return (strlen($this->password) === 6);
    }

    public function isWithinRange()
    {
        return ($this->password >= $this->lowerRange && $this->password <= $this->upperRange);
    }

    public function hasTwoAdjacentSameDigits()
    {
        $i = 1;
        $repeatingCount = 1;
        while ($i < strlen($this->password)) {
            if ($this->password[$i] != $this->password[$i - 1] && $repeatingCount == 2) {
                return true;
            }

            if ($this->password[$i] == $this->password[$i - 1]) {
                $repeatingCount++;
            } else {
                $repeatingCount = 1;
            }

            if ($i == strlen($this->password) - 1 && $repeatingCount == 2) {
                return true;
            }

            $i++;
        }

        return false;
    }

    public function hasDigitsNeverDecrease()
    {
        $i = 1;
        while ($i < strlen($this->password)) {
            if ($this->password[$i] < $this->password[$i - 1]) {
                return false;
            }
            $i++;
        }

        return true;
    }
}

Let’s update solve.php

/src/day4/solve.php

<?php

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

use PuzzleSolvers\Day4\PasswordCracker;
use PuzzleSolvers\Day4\PasswordCracker2;

$passwordCracker = new PasswordCracker('day4/part1/input');
$passwordCracker->run();
$output = $passwordCracker->getOutput();

echo "Day 4 Part 1 answer: " . $output . "\n";

$passwordCracker = new PasswordCracker2('day4/part1/input');
$passwordCracker->run();
$output = $passwordCracker->getOutput();

echo "Day 4 Part 2 answer: " . $output . "\n";

Let’s get that gold star!

$ php src/day4/solve.php 
Day 4 Part 1 answer: 2814
Day 4 Part 2 answer: 1991

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/day4/part1/input with your own file. Did it work? Let me know!

Next up: Day 5 Part 1

Advent of Code 2019 Day 4 (Part 1) PHP Hints

— Day 4: Secure Container (Part 1) —

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 Day3Part2 (as in the end of Day3Part2). You also might like to check out Getting Started – or my solution for Day3Part1 to get up to speed. I hope somebody finds this useful!

So this puzzle looks like it might not be too complex. I’m thinking to write a few test cases for valid passwords. Then loop through all the possible numbers and record those which pass all the tests.

So lets start by setting up a test file – from the instructions it looks like there’s four good tests to start with:

  • 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 1<em>22</em>345).
  • Going from left to right, the digits never decrease; they only ever increase or stay the same (like 111123 or 135679)

/tests/Day4Part1Test.php

<?php

use PHPUnit\Framework\TestCase;
use PuzzleSolvers\Day4\PasswordCracker;

final class Day4Part1Test extends TestCase
{
    public function testHasSixDigits()
    {
    }

    public function testIsWithingRange()
    {
    }

    public function testHasTwoAdjacentSameDigits()
    {
    }

    public function testDigitsNeverDecrease()
    {
    }
}

And let’s fill those out:

<?php

use PHPUnit\Framework\TestCase;
use PuzzleSolvers\Day4\PasswordCracker;

final class Day4Part1Test extends TestCase
{
    public function testEndToEnd(): void
    {
        $outputs = [1, 0, 0];
        foreach ($outputs as $inputFileNumber => $output) {
            $inputFile = 'day4/part1/' . $inputFileNumber;
            \PuzzleDebugger::print('TESTING INPUT FILE ' . $inputFile);
            $puzzleSolver = new CrossedWiresMeasurer($inputFile);
            $puzzleSolver->run();
            $this->assertSame($output, $puzzleSolver->getOutput());
        }
    }

    public function testHasSixDigits()
    {
        $passwordCracker = new PasswordCracker('day4/part1/sample_range');

        $passwordCracker->password = '123';
        $this->assertFalse($passwordCracker->hasSixDigits());

        $passwordCracker->password = '123456';
        $this->assertTrue($passwordCracker->hasSixDigits());
    }

    public function testIsWithingRange()
    {
        $passwordCracker = new PasswordCracker('day4/part1/sample_range');

        $passwordCracker->password = '1';
        $this->assertFalse($passwordCracker->isWithinRange());

        $passwordCracker->password = '123456';
        $this->assertTrue($passwordCracker->isWithinRange());
    }

    public function testHasTwoAdjacentSameDigits()
    {
        $passwordCracker = new PasswordCracker('day4/part1/sample_range');

        $passwordCracker->password = '123456';
        $this->assertFalse($passwordCracker->hasTwoAdjacentSameDigits());

        $passwordCracker->password = '223456';
        $this->assertTrue($passwordCracker->hasTwoAdjacentSameDigits());
    }

    public function testDigitsNeverDecrease()
    {
        $passwordCracker = new PasswordCracker('day4/part1/sample_range');

        $passwordCracker->password = '654321';
        $this->assertFalse($passwordCracker->hasDigitsNeverDecrease());

        $passwordCracker->password = '123456';
        $this->assertTrue($passwordCracker->hasDigitsNeverDecrease());
    }
}

Now lets create the PasswordCracker class

/src/day4/PasswordCracker.php

<?php

namespace PuzzleSolvers\Day4;

use PuzzleSolvers\PuzzleSolver;

class PasswordCracker extends PuzzleSolver
{
    public $lowerRange;
    public $upperRange;
    public $password;

    public function run()
    {
    }

    public function hasSixDigits()
    {
    }

    public function isWithinRange()
    {
    }

    public function hasTwoAdjacentSameDigits()
    {
    }

    public function hasDigitsNeverDecrease()
    {
    }
}

Next I created a test input file sample_range

/inputs/day4/part1/sample_range

100000-200000

Now let’s run make tests and get the tests passing one-by-one. Let’s start with hasSixDigits()

public function hasSixDigits()
{
    return (strlen($this->password) === 6);
}

That test is now passing. Let’s move on to isWithinRange()

public function __construct($filename)
{
    parent::__construct($filename);
    $inputPieces = explode('-', $this->inputs[0]);
    $this->lowerRange = $inputPieces[0];
    $this->upperRange = $inputPieces[1];
}

public function isWithinRange()
{
    return ($this->password >= $this->lowerRange && $this->password <= $this->upperRange);
}

That test is now passing. Let’s move on to hasTwoAdjacentSameDigits()

public function hasTwoAdjacentSameDigits()
{
    return preg_match('/([0-9])\\1/', $this->password) === 1;
}

The regex here basically just checks for a digit [0-9] and then the result of that match again directly after \\1

That test is now passing. Let’s move on to hasDigitsNeverDecrease()

public function hasDigitsNeverDecrease()
{
    $i = 1;
    while ($i < strlen($this->password)) {
        if ($this->password[$i] < $this->password[$i -1]) {
            return false;
        }
        $i++;
    }

    return true;
}

Ok – so make tests are all passing now. Next we want to loop through all of the inputs and test them for validity. Let’s create our end-to-end tests now, with 3 input files restricting the input range to the provided examples:

/inputs/day4/part1/0

111111-111111

/inputs/day4/part1/1

223450-223450

/inputs/day4/part1/2

123789-123789
public function testEndToEnd(): void
{
    $outputs = [1, 0, 0];
    foreach ($outputs as $inputFileNumber => $output) {
        $inputFile = 'day4/part1/' . $inputFileNumber;
        \PuzzleDebugger::print('TESTING INPUT FILE ' . $inputFile);
        $puzzleSolver = new PasswordCracker($inputFile);
        $puzzleSolver->run();
        $this->assertSame($output, $puzzleSolver->getOutput());
    }
}

Now let’s implement the logic in run()

public function run() {
    $validPasswords = [];
    $this->password = $this->lowerRange;
    while ($this->password <= $this->upperRange) {
        if (
            $this->hasSixDigits()
            && $this->isWithinRange()
            && $this->hasTwoAdjacentSameDigits()
            && $this->hasDigitsNeverDecrease()
        ) {
            $validPasswords[] = $this->password;
        }

        $this->password = (string) ($this->password + 1);
    }

    $this->output = count($validPasswords);
}

Alright make tests everything is passing. Note that I am treating password as a string to need to cast it back after adding 1. I think we’re ready to write our solve.php script

/src/day4/solve.php

<?php

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

use PuzzleSolvers\Day4\PasswordCracker;

$passwordCracker = new PasswordCracker('day4/part1/input');
$passwordCracker->run();
$output = $passwordCracker->getOutput();

echo "Day 4 Part 1 answer: " . $output . "\n";

And don’t forget to grab our input for the puzzle

/inputs/day4/part1/input

109165-576723

Here is the final PasswordCracker class:

<?php

namespace PuzzleSolvers\Day4;

use PuzzleSolvers\PuzzleSolver;

class PasswordCracker extends PuzzleSolver
{
    public $lowerRange;
    public $upperRange;
    public $password;

    public function __construct($filename)
    {
        parent::__construct($filename);

        $inputPieces = explode('-', $this->inputs[0]);
        $this->lowerRange = $inputPieces[0];
        $this->upperRange = $inputPieces[1];
    }

    public function run()
    {
        $validPasswords = [];
        $invalidPasswords = [];
        $this->password = $this->lowerRange;
        while ($this->password <= $this->upperRange) {
            if (
                $this->hasSixDigits()
                && $this->isWithinRange()
                && $this->hasTwoAdjacentSameDigits()
                && $this->hasDigitsNeverDecrease()
            ) {
                $validPasswords[] = $this->password;
            } else {
                $invalidPasswords[] = $this->password;
            }

            $this->password = (string) ($this->password + 1);
        }

        $this->output = count($validPasswords);
    }

    public function hasSixDigits()
    {
        return (strlen($this->password) === 6);
    }

    public function isWithinRange()
    {
        return ($this->password >= $this->lowerRange && $this->password <= $this->upperRange);
    }

    public function hasTwoAdjacentSameDigits()
    {
        return preg_match('/([0-9])\\1/', $this->password) === 1;
    }

    public function hasDigitsNeverDecrease()
    {
        $i = 1;
        while ($i < strlen($this->password)) {
            if ($this->password[$i] < $this->password[$i - 1]) {
                return false;
            }
            $i++;
        }

        return true;
    }
}

Let’s get that gold star!

$ php src/day4/solve.php 
Day 4 Part 1 answer: 2814

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/day4/part1/input with your own file. Did it work? Let me know!

Next up: Day 4 Part 2

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

— Day 3: Crossed Wires (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 Day3Part1 (as in the end of Day3Part1). You also might like to check out Getting Started – or my solution for Day2Part1 to get up to speed. I hope somebody finds this useful!

I also created a javascript Visualization for this puzzle here if you’re interested.

So it looks like part 2 to this puzzle is only a minor adjustment in the way we calculate the distance. Rather than using the Manhatten Distance we need to:

calculate the number of steps each wire takes to reach each intersection; choose the intersection where the sum of both wires’ steps is lowest.

So the first thing I did was set up my end-to-end tests using the provided examples:

/tests/Day3Part2Test.php

<?php

use PHPUnit\Framework\TestCase;
use PuzzleSolvers\Day3\CrossedWiresMeasurer2;

final class Day3Part2Test extends TestCase
{
    public function testEndToEnd(): void
    {
        $outputs = [30, 610, 410];
        foreach ($outputs as $inputFileNumber => $output) {
            $inputFile = 'day3/part1/' . $inputFileNumber;
            \PuzzleDebugger::print('TESTING INPUT FILE ' . $inputFile);
            $puzzleSolver = new CrossedWiresMeasurer2($inputFile);
            $puzzleSolver->run();
            $this->assertSame($output, $puzzleSolver->getOutput());
        }
    }
}

I’m thinking that the easiest way to do this is to just keep track of the length of the wire as we are laying it. When we identify the intersections we can just lookup the length at that point.

So I copied the CrossedWiresMeasurer class from the Day3Part1 and created CrossedWiresMeasurer2

/src/day3/CrossedWiresMeasurer2.php

public $wires = [
    [
        '0,0' => [
            'x' => 0,
            'y' => 0,
            'length' => 0,
        ]
    ],
    [
        '0,0' => [
            'x' => 0,
            'y' => 0,
            'length' => 0,
        ]
    ],
];

public $wireEndPoints = [
    [
        'x' => 0,
        'y' => 0,
        'length' => 0,
    ],
    [
        'x' => 0,
        'y' => 0,
        'length' => 0,
    ],
];

Now I want to modify our wires and wiretEndPoints to also store length:

public $wires = [
    [
        '0,0' => [
            'x' => 0,
            'y' => 0,
            'length' => 0,
        ]
    ],
    [
        '0,0' => [
            'x' => 0,
            'y' => 0,
            'length' => 0,
        ]
    ],
];

public $wireEndPoints = [
    [
        'x' => 0,
        'y' => 0,
        'length' => 0,
    ],
    [
        'x' => 0,
        'y' => 0,
        'length' => 0,
    ],
];

And create a function to get the sum of wire lengths at a specific point

public function getWireLengths($point)
{
    return $this->wires[0][$point['x'] . ',' . $point['y']]['length'] + $this->wires[1][$point['x'] . ',' . $point['y']]['length'];
}

Now modify the layWires() function to update the wires lengths as we lay them

public function layWire(int $wireId, string $direction, int $length = 1)
{
    ...

    $newPositionKey = $newX . ',' . $newY;
    $newLength = $wireEndPoint['length'] + 1;

    $this->wires[$wireId][$newPositionKey] = [
        'x' => $newX,
        'y' => $newY,
        'length' => $newLength,
    ];

Now modify the run function to use the new getWireLengths():

public function run()
{
    $this->initialise();
    $intersections = $this->getIntersections();
    foreach ($intersections as $intersectionPoint) {
        $distance = $this->getWireLengths($intersectionPoint);
        if (empty($this->output) || $distance < $this->output) {
            $this->output = $distance;
        }
    }
}

Now this works for our input but I think there is a bug that we are overwriting the length each time a line intersects on itself. We actually want to keep the shortest length. So lets write a quick test for that. First I created a short wire path where the wires intersect at x=0, y=1 and saved it as a test input file:
/inputs/day3/part2/crossing_wires_length

U2,R1,D1,L1
U2,R1,D1,L1

Now I want to test that the intersection point has length 1 and not length 5

public function testWirePointStoresShortestLength()
{
    $inputFile = 'day3/part2/crossing_wires_length';
    $crossedWiresMeasurer2 = new CrossedWiresMeasurer2($inputFile);
    $crossedWiresMeasurer2->initialise();

    \PuzzleDebugger::print($crossedWiresMeasurer2->wires[0]);

    $this->assertSame(1, $crossedWiresMeasurer2->wires[0]['0,1']['length']);
}

This test indeed fails so we can make it pass by checking for a length value and not overwriting – however the wireEndPoint length still has to always increase:

if (isset($this->wires[$wireId][$newPositionKey])) {
    $newLength = $this->wires[$wireId][$newPositionKey]['length'];
} else {
    $newLength = $wireEndPoint['length'] + 1;
}

$this->wires[$wireId][$newPositionKey] = [
    'x' => $newX,
    'y' => $newY,
    'length' => $newLength,
];

$this->wireEndPoints[$wireId] = [
    'x' => $newX,
    'y' => $newY,
    'length' => $wireEndPoint['length'] + 1,
];

Now that test passes as well, it’s time to update our solve.php script.

/src/day3/solve.php

<?php

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

use PuzzleSolvers\Day3\CrossedWiresMeasurer;
use PuzzleSolvers\Day3\CrossedWiresMeasurer2;

$crossedWiresMeasurer = new CrossedWiresMeasurer('day3/part1/input');
$crossedWiresMeasurer->run();
$output = $crossedWiresMeasurer->getOutput();

echo "Day 3 Part 1 answer: " . $output . "\n";

$crossedWiresMeasurer2 = new CrossedWiresMeasurer2('day3/part1/input');
$crossedWiresMeasurer2->run();
$output = $crossedWiresMeasurer2->getOutput();

echo "Day 3 Part 2 answer: " . $output . "\n";

Here is the final class:

<?php

namespace PuzzleSolvers\Day3;

use PuzzleSolvers\PuzzleSolver;

class CrossedWiresMeasurer2 extends PuzzleSolver
{
    public $wires = [
        [
            '0,0' => [
                'x' => 0,
                'y' => 0,
                'length' => 0,
            ]
        ],
        [
            '0,0' => [
                'x' => 0,
                'y' => 0,
                'length' => 0,
            ]
        ],
    ];

    public $wireEndPoints = [
        [
            'x' => 0,
            'y' => 0,
            'length' => 0,
        ],
        [
            'x' => 0,
            'y' => 0,
            'length' => 0,
        ],
    ];

    public function run()
    {
        $this->initialise();
        $intersections = $this->getIntersections();
        foreach ($intersections as $intersectionPoint) {
            $distance = $this->getWireLengths($intersectionPoint);
            if (empty($this->output) || $distance < $this->output) {
                $this->output = $distance;
            }
        }
    }

    public function getIntersections()
    {
        $intersections = [];

        $intersections = array_intersect_key($this->wires[0], $this->wires[1]);
        unset($intersections['0,0']);

        return $intersections;
    }

    public function getWireLengths($point)
    {
        return $this->wires[0][$point['x'] . ',' . $point['y']]['length'] + $this->wires[1][$point['x'] . ',' . $point['y']]['length'];
    }

    public function getPointManhattenDistance($point)
    {
        return abs($point['x']) + abs($point['y']);
    }

    public function initialise()
    {
        foreach ($this->inputs as $wireId => $wireSequenceString) {
            $wireSequence = explode(',', $wireSequenceString);
            foreach ($wireSequence as $moveInstruction) {
                $direction = $moveInstruction[0];
                $length = intval(substr($moveInstruction, 1));
                $this->layWire($wireId, $direction, $length);
            }
        }
    }

    public function layWire(int $wireId, string $direction, int $length = 1)
    {
        for ($i = 0; $i < $length; $i++) {
            $wireEndPoint = $this->wireEndPoints[$wireId];

            if ($direction === 'U') {
                $newY = $wireEndPoint['y'] + 1;
                $newX = $wireEndPoint['x'];
            }

            if ($direction === 'D') {
                $newY = $wireEndPoint['y'] - 1;
                $newX = $wireEndPoint['x'];
            }

            if ($direction === 'R') {
                $newY = $wireEndPoint['y'];
                $newX = $wireEndPoint['x'] + 1;
            }

            if ($direction === 'L') {
                $newY = $wireEndPoint['y'];
                $newX = $wireEndPoint['x'] - 1;
            }

            $newPositionKey = $newX . ',' . $newY;

            if (isset($this->wires[$wireId][$newPositionKey])) {
                $newLength = $this->wires[$wireId][$newPositionKey]['length'];
            } else {
                $newLength = $wireEndPoint['length'] + 1;
            }

            $this->wires[$wireId][$newPositionKey] = [
                'x' => $newX,
                'y' => $newY,
                'length' => $newLength,
            ];

            $this->wireEndPoints[$wireId] = [
                'x' => $newX,
                'y' => $newY,
                'length' => $wireEndPoint['length'] + 1,
            ];
        }
    }
}

let’s get that gold star!

$ php src/day3/solve.php 
Day 3 Part 1 answer: 273
Day 3 Part 2 answer: 15622

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/day3/part1/input with your own file. Did it work? Let me know!

Next up: Day 4 Part 1

Advent of Code 2019 Day 3 (Part 1) PHP Hints

— Day 3: Crossed Wires (Part 1) —

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 Day2Part2 (as in the end of Day2Part2). You also might like to check out Getting Started – or my solution for Day2Part1 to get up to speed. I hope somebody finds this useful!

I also created a javascript Visualization for this puzzle here if you’re interested.

Ok, so after reading through this puzzle description I think this looks like quite a fun one. My initial thoughts are to maintain a couple of arrays of coordinates for the wires paths and then use some kind of array intersection to get the path intersections.

So as in previous posts – the first thing I did was to create some end-to-end tests.

/tests/Day3Part1Test.php

<?php

use PHPUnit\Framework\TestCase;
use PuzzleSolvers\Day3\CrossedWiresMeasurer;

final class Day3Part1Test extends TestCase
{
    public function testEndToEnd(): void
    {
        $outputs = [6, 159, 135];
        foreach ($outputs as $inputFileNumber => $output) {
            $puzzleSolver = new CrossedWiresMeasurer('day3/part1/' . $inputFileNumber);
            $puzzleSolver->run();
            $this->assertSame($output, $puzzleSolver->getOutput());
        }
    }
}

And added the relevent input files using the 3 examples to /inputs/day3/part1/

R8,U5,L5,D3
U7,R6,D4,L4
R75,D30,R83,U83,L12,D49,R71,U7,L72
U62,R66,U55,R34,D71,R55,D58,R83
R98,U47,R26,D63,R33,U87,L62,D20,R33,U53,R51
U98,R91,D20,R16,D67,R40,U7,R15,U6,R7

Finally I initialised my CrossedWiresMeasurer Class

/src/day3/CrossedWiresMeasurer.php

<?php

namespace PuzzleSolvers\Day3;

use PuzzleSolvers\PuzzleSolver;

class CrossedWiresMeasurer extends PuzzleSolver
{
    public function run()
    {}
}

Now we have some failing tests after running make tests. Hooray!

In this puzzle I decided to add a basic debug class that will send stuff to STDERR which will print on the PHPUnit output. I called it PuzzleDebugger.

/src/PuzzleDebugger.php

<?php

class PuzzleDebugger
{
    public static function print($message)
    {
        if (is_array($message)) {
            $message = print_r($message, true);
        }

        fwrite(STDERR, $message . "\n");
    }
}

So we can add an info message to our testEndToEnd() as to which file is currently running:

/tests/Day3Part1Test.php

<?php

use PHPUnit\Framework\TestCase;
use PuzzleSolvers\Day3\CrossedWiresMeasurer;

final class Day3Part1Test extends TestCase
{
    public function testEndToEnd(): void
    {
        $outputs = [6, 159, 135];
        foreach ($outputs as $inputFileNumber => $output) {
            $inputFile = 'day3/part1/' . $inputFileNumber;
            \PuzzleDebugger::print('TESTING INPUT FILE ' . $inputFile);
            $puzzleSolver = new CrossedWiresMeasurer($inputFile);
            $puzzleSolver->run();
            $this->assertSame($output, $puzzleSolver->getOutput());
        }
    }
}

Ok so now lets start by adding some properties to our main class to track the positions of laid wire.

Each position is going to consist of an x and a y coordinate. Also in the interest of using the array key as a sort of hash of the position I’ll store the key as a string in the format "$x,$y". I don’t think the value of the array actually matters too much for this puzzle? But I’ll just store a 2 element array with an x an y value.

This time I’m just going to make everything public and not worry about setters and getters etc.

<?php

namespace PuzzleSolvers\Day3;

use PuzzleSolvers\PuzzleSolver;

class CrossedWiresMeasurer extends PuzzleSolver
{
    public $wires = [
        [
            '0,0' => [
                'x' => 0,
                'y' => 0,
            ]
        ],
        [
            '0,0' => [
                'x' => 0,
                'y' => 0,
            ]
        ],
    ];

    public function run()
    { }
}

Next I want to write a function which allows us to lay the wire according to the moves U, D, L, R. So first I write a test for the Up move.

/tests/Day3Part1Test.php

public function testLayWireUp()
{
    $crossedWiresMeasurer = new CrossedWiresMeasurer('day3/part1/0');

    $crossedWiresMeasurer->layWire(0, 'U');

    $this->assertSame([
        '0,0' => [
            'x' => 0,
            'y' => 0,
        ],
        '0,1' => [
            'x' => 0,
            'y' => 1,
        ],
    ], $crossedWiresMeasurer->wires[0]);
}

Now in my coordinate system y + 1 is up, y - 1 is down, x + 1 is to the right, x -1 is to the left. Now make the test pass.

public function layWire($wireId, $direction)
{
    $wireEndPosition = end($this->wires[$wireId]);

    if ($direction === 'U') {
        $newY = $wireEndPosition['y'] + 1;
        $newX = $wireEndPosition['x'];
    }

    $newPositionKey = $newX . ',' . $newY;

    $this->wires[$wireId][$newPositionKey] = [
        'x' => $newX,
        'y' => $newY,
    ];
}

OK so make tests is now passing for our testLayWireUp

I can see how this could (should?) probably be decomposed into some separate objects. But I’m just going to plow ahead 🙂

Let’s add 3 more tests for laying Down, Right and Left:

public function testLayWireDown()
{
    $crossedWiresMeasurer = new CrossedWiresMeasurer('day3/part1/0');

    $crossedWiresMeasurer->layWire(0, 'D');

    $this->assertSame([
        '0,0' => [
            'x' => 0,
            'y' => 0,
        ],
        '0,-1' => [
            'x' => 0,
            'y' => -1,
        ],
    ], $crossedWiresMeasurer->wires[0]);
}

public function testLayWireRight()
{
    $crossedWiresMeasurer = new CrossedWiresMeasurer('day3/part1/0');

    $crossedWiresMeasurer->layWire(0, 'R');

    $this->assertSame([
        '0,0' => [
            'x' => 0,
            'y' => 0,
        ],
        '1,0' => [
            'x' => 1,
            'y' => 0,
        ],
    ], $crossedWiresMeasurer->wires[0]);
}

public function testLayWireLeft()
{
    $crossedWiresMeasurer = new CrossedWiresMeasurer('day3/part1/0');

    $crossedWiresMeasurer->layWire(0, 'L');

    $this->assertSame([
        '0,0' => [
            'x' => 0,
            'y' => 0,
        ],
        '-1,0' => [
            'x' => -1,
            'y' => 0,
        ],
    ], $crossedWiresMeasurer->wires[0]);
}

And make them pass – this should be fairly straightforward extention of what we had for U:

public function layWire($wireId, $direction)
{
    $wireEndPosition = end($this->wires[$wireId]);

    if ($direction === 'U') {
        $newY = $wireEndPosition['y'] + 1;
        $newX = $wireEndPosition['x'];
    }

    if ($direction === 'D') {
        $newY = $wireEndPosition['y'] - 1;
        $newX = $wireEndPosition['x'];
    }

    if ($direction === 'R') {
        $newY = $wireEndPosition['y'];
        $newX = $wireEndPosition['x'] + 1;
    }

    if ($direction === 'L') {
        $newY = $wireEndPosition['y'];
        $newX = $wireEndPosition['x'] - 1;
    }

    $newPositionKey = $newX . ',' . $newY;

    $this->wires[$wireId][$newPositionKey] = [
        'x' => $newX,
        'y' => $newY,
    ];
}

Cool make tests pass. We’re getting close to laying some cable… The next thing we probably want to do is to specify how far to lay as part of the function. So let’s write a test for laying 2 segments:

public function testLayWireLeft2()
{
    $crossedWiresMeasurer = new CrossedWiresMeasurer('day3/part1/0');

    $crossedWiresMeasurer->layWire(0, 'L', 2);

    $this->assertSame([
        '0,0' => [
            'x' => 0,
            'y' => 0,
        ],
        '-1,0' => [
            'x' => -1,
            'y' => 0,
        ],
        '-2,0' => [
            'x' => -2,
            'y' => 0,
        ],
    ], $crossedWiresMeasurer->wires[0]);
}

and then make it pass. Essentially we just want to add for ($i = 0; $i &lt; $length; $i++) { around the whole function and accept an optional new argument. Here is the final function:

public function layWire(int $wireId, string $direction, int $length = 1)
{
    for ($i = 0; $i < $length; $i++) {
        $wireEndPosition = end($this->wires[$wireId]);

        if ($direction === 'U') {
            $newY = $wireEndPosition['y'] + 1;
            $newX = $wireEndPosition['x'];
        }

        if ($direction === 'D') {
            $newY = $wireEndPosition['y'] - 1;
            $newX = $wireEndPosition['x'];
        }

        if ($direction === 'R') {
            $newY = $wireEndPosition['y'];
            $newX = $wireEndPosition['x'] + 1;
        }

        if ($direction === 'L') {
            $newY = $wireEndPosition['y'];
            $newX = $wireEndPosition['x'] - 1;
        }

        $newPositionKey = $newX . ',' . $newY;

        $this->wires[$wireId][$newPositionKey] = [
            'x' => $newX,
            'y' => $newY,
        ];
    }
}

Eventually I realised that there is a bug in this approach were we cannot reliably use end() to get the last point in the wire when it crosses back on itself. So I refactored this by adding a new property:

public $wireEndPoints = [
    [
        'x' => 0,
        'y' => 0,
    ],
    [
        'x' => 0,
        'y' => 0,
    ],
];

And layWire() to:

public function layWire(int $wireId, string $direction, int $length = 1)
{
for ($i = 0; $i < $length; $i++) {
    $wireEndPoint = $this->wireEndPoints[$wireId];

    if ($direction === 'U') {
        $newY = $wireEndPoint['y'] + 1;
        $newX = $wireEndPoint['x'];
    }

    if ($direction === 'D') {
        $newY = $wireEndPoint['y'] - 1;
        $newX = $wireEndPoint['x'];
    }

    if ($direction === 'R') {
        $newY = $wireEndPoint['y'];
        $newX = $wireEndPoint['x'] + 1;
    }

    if ($direction === 'L') {
        $newY = $wireEndPoint['y'];
        $newX = $wireEndPoint['x'] - 1;
    }

    $newPositionKey = $newX . ',' . $newY;

    $this->wires[$wireId][$newPositionKey] = [
        'x' => $newX,
        'y' => $newY,
    ];

    $this->wireEndPoints[$wireId] = [
        'x' => $newX,
        'y' => $newY,
    ];
}

OK cool – now we can lay some wires. Let’s create a function to use the inputs to initialise the sequence. First let’s write a test with a shortish wire path:

public function testInitialiseWires()
{
    $crossedWiresMeasurer = new CrossedWiresMeasurer('day3/part1/test_intitialise_wires');

    $crossedWiresMeasurer->initialise();

    $this->assertSame([
        [
            '0,0' => [
                'x' => 0,
                'y' => 0,
            ],
            '1,0' => [
                'x' => 1,
                'y' => 0,
            ],
            '2,0' => [
                'x' => 2,
                'y' => 0,
            ],
            '2,-1' => [
                'x' => 2,
                'y' => -1,
            ],
        ],
        [
            '0,0' => [
                'x' => 0,
                'y' => 0,
            ],
            '-1,0' => [
                'x' => -1,
                'y' => 0,
            ],
            '-1,1' => [
                'x' => -1,
                'y' => 1,
            ],
            '-1,2' => [
                'x' => -1,
                'y' => 2,
            ],
        ]
    ], $crossedWiresMeasurer->wires);
}

Next create the test input file

/inputs/day3/test_intitialise_wires

R2,D1
L1,U2

Now let’s make it pass. My first attempt looked like this:

    public function initialise()
    {
        foreach ($this->inputs as $wireId => $wireSequence) {
            foreach ($wireSequence as $moveInstruction) {
                $direction = $moveInstruction[0];
                $length = $moveInstruction[1];
                $this->layWire($wireId, $direction, $length);
            }
        }
    }

make tests

 ✘ Initialise wires
   │
   │ Invalid argument supplied for foreach()
   │
   │ /home/hamish/sites/adventofcode2019/src/day3/CrossedWiresMeasurer.php:30
   │ /home/hamish/sites/adventofcode2019/tests/Day3Part1Test.php:118

OK – so I forgot to convert the input move sequence into an array. Here’s my second attempt:

    public function initialise()
    {
        foreach ($this->inputs as $wireId => $wireSequenceString) {
            $wireSequence = explode(',', $wireSequenceString);
            foreach ($wireSequence as $moveInstruction) {
                $direction = $moveInstruction[0];
                $length = $moveInstruction[1];
                $this->layWire($wireId, $direction, $length);
            }
        }
    }

And that passes 🙂 So we can now initialise our wires. The next step is to get the points where they are intersecting. Because our two wire arrays use the coordinates as the key we can simply use PHPs handy array_intersect_key() function. So first make a test:

public function testGetIntersections()
{
    $crossedWiresMeasurer = new CrossedWiresMeasurer('day3/part1/0');
    $crossedWiresMeasurer->wires = [
        [
            '0,0' => [
                'x' => 0,
                'y' => 0,
            ],
            '5,5' => [
                'x' => 5,
                'y' => 5,
            ],
            '3,2' => [
                'x' => 3,
                'y' => 2,
            ]
        ],
        [
            '0,0' => [
                'x' => 0,
                'y' => 0,
            ],
            '1,6' => [
                'x' => 1,
                'y' => 6,
            ],
            '5,5' => [
                'x' => 5,
                'y' => 5,
            ]
        ],
    ];

    $intersections = $crossedWiresMeasurer->getIntersections();

    $this->assertSame([
        '5,5' => [
            'x' => 5,
            'y' => 5,
        ],
    ], $intersections);
}

In my test the wires are not contiguous – but I don’t think that matters. I’ve also excluded the 0,0 intersection from the result as per the instructions:

While the wires do technically cross right at the central port where they both start, this point does not count

OK – so let’s make it pass:

    public function getIntersections()
    {
        $intersections = [];

        $intersections = array_intersect_key($this->wires[0], $this->wires[1]);
        unset($intersections['0,0']);

        return $intersections;
    }

OK – so we have our intersections array of points. Let’s look at the question again to see what we need to do to get the actual answer!

What is the Manhattan distance from the central port to the closest intersection?

OK so this is basically just adding the absolute value of the x and y coordinates. Lets write a test first:

public function testGetPointManhattenDistance()
{
    $crossedWiresMeasurer = new CrossedWiresMeasurer('day3/part1/0');
    $point = [
        'x' => 3,
        'y' => 3,
    ];

    $pointManhattenDistance = $crossedWiresMeasurer->getPointManhattenDistance($point);

    $this->assertSame(6, $pointManhattenDistance);
}

And make it pass:

public function getPointManhattenDistance($point)
{
    return abs($point['x']) + abs($point['y']);
}

OK – now everything is in place to implement the run() function and see if we can make our original end-to-end tests pass!

public function run()
{
    $this->initialise();
    $intersections = $this->getIntersections();
    foreach ($intersections as $intersectionPoint) {
        $distance = $this->getPointManhattenDistance($intersectionPoint);
        if (empty($this->output) || $distance < $this->output) {
            $this->output = $distance;
        }
    }
}

Running make tests we can see that our first end-to-end test passes but the second one fails 🙁

 ✘ End to end
   │
   │ Failed asserting that 15 is identical to 159.
   │
   │ /home/hamish/sites/adventofcode2019/tests/Day3Part1Test.php:16
   │

The distance is way to small… So what’s gone wrong? Well at this point some debugging is required which is where the PuzzleDebugger class came in handy. To cut a long story short a casual assumption I made earlier was incorrect. Placing debug statements throughout the code and examining the output revealed the issue:

public function layWire(int $wireId, string $direction, int $length = 1)
{
    \PuzzleDebugger::print($direction);
    \PuzzleDebugger::print($length);
    ...

The output from test file 1 which has the instructions R75,D30,R83,U83... was the following:

TESTING INPUT FILE day3/part1/1
R
7
D
3
R
8
U
8

So basically this is telling us that only the first digit of the length is being passed through to the layWire() function. This is not something I covered in my tests so far as my tests only use lengths < 10! The issue here is initialise:

public function initialise()
{
    foreach ($this->inputs as $wireId => $wireSequenceString) {
        $wireSequence = explode(',', $wireSequenceString);
        foreach ($wireSequence as $moveInstruction) {
            $direction = $moveInstruction[0];
            $length = $moveInstruction[1];
            $this->layWire($wireId, $direction, $length);
        }
    }
}

Specifically:

$direction = $moveInstruction[0];
$length = $moveInstruction[1];

Is only taking the first digit for length! So I updated this to capture the rest of the string:

$direction = $moveInstruction[0];
$length = substr($moveInstruction, 1);

Now let’s run make tests again – and all our tests pass! So let’s create a solve.php script similar to the other puzzles:

/src/day3/solve.php

<?php

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

use PuzzleSolvers\Day3\CrossedWiresMeasurer;

$crossedWiresMeasurer = new CrossedWiresMeasurer('day3/part1/input');
$crossedWiresMeasurer->run();
$output = $crossedWiresMeasurer->getOutput();

echo "Day 3 Part 1 answer: " . $output . "\n";

Final /src/day3/CrossedWiresMeasurer.php

<?php

namespace PuzzleSolvers\Day3;

use PuzzleSolvers\PuzzleSolver;

class CrossedWiresMeasurer extends PuzzleSolver
{
    public $wires = [
        [
            '0,0' => [
                'x' => 0,
                'y' => 0,
            ]
        ],
        [
            '0,0' => [
                'x' => 0,
                'y' => 0,
            ]
        ],
    ];

    public $wireEndPoints = [
        [
            'x' => 0,
            'y' => 0,
        ],
        [
            'x' => 0,
            'y' => 0,
        ],
    ];

    public function run()
    {
        $this->initialise();
        $intersections = $this->getIntersections();
        foreach ($intersections as $intersectionPoint) {
            $distance = $this->getPointManhattenDistance($intersectionPoint);
            if (empty($this->output) || $distance < $this->output) {
                $this->output = $distance;
            }
        }
    }

    public function getIntersections()
    {
        $intersections = [];

        $intersections = array_intersect_key($this->wires[0], $this->wires[1]);
        unset($intersections['0,0']);

        return $intersections;
    }

    public function getPointManhattenDistance($point)
    {
        return abs($point['x']) + abs($point['y']);
    }

    public function initialise()
    {
        foreach ($this->inputs as $wireId => $wireSequenceString) {
            $wireSequence = explode(',', $wireSequenceString);
            foreach ($wireSequence as $moveInstruction) {
                $direction = $moveInstruction[0];
                $length = intval(substr($moveInstruction, 1));
                $this->layWire($wireId, $direction, $length);
            }
        }
    }

    public function layWire(int $wireId, string $direction, int $length = 1)
    {
        for ($i = 0; $i < $length; $i++) {
            $wireEndPoint = $this->wireEndPoints[$wireId];

            if ($direction === 'U') {
                $newY = $wireEndPoint['y'] + 1;
                $newX = $wireEndPoint['x'];
            }

            if ($direction === 'D') {
                $newY = $wireEndPoint['y'] - 1;
                $newX = $wireEndPoint['x'];
            }

            if ($direction === 'R') {
                $newY = $wireEndPoint['y'];
                $newX = $wireEndPoint['x'] + 1;
            }

            if ($direction === 'L') {
                $newY = $wireEndPoint['y'];
                $newX = $wireEndPoint['x'] - 1;
            }

            $newPositionKey = $newX . ',' . $newY;

            $this->wires[$wireId][$newPositionKey] = [
                'x' => $newX,
                'y' => $newY,
            ];

            $this->wireEndPoints[$wireId] = [
                'x' => $newX,
                'y' => $newY,
            ];
        }
    }
}

My input file is shown below

/inputs/day3/part1/input

R1009,U993,L383,D725,R163,D312,R339,U650,R558,U384,R329,D61,L172,D555,R160,D972,L550,D801,L965,U818,L123,D530,R176,D353,L25,U694,L339,U600,L681,D37,R149,D742,R762,U869,R826,U300,L949,U978,L303,U361,R136,D343,L909,U551,R745,U913,L566,D292,R820,U886,R205,D431,L93,D71,R577,U872,L705,U510,L698,U963,R607,U527,L669,D543,R690,U954,L929,D218,R490,U500,L589,D332,R949,D538,R696,U659,L188,U468,L939,U833,L445,D430,R78,D303,R130,D649,R849,D712,L511,U745,R51,U973,R799,U829,R605,D771,L837,U204,L414,D427,R538,U116,R540,D168,R493,U900,L679,U431,L521,D500,L428,U332,L954,U717,L853,D339,L88,U807,L607,D496,L163,U468,L25,U267,L759,D898,L591,U445,L469,U531,R596,D486,L728,D677,R350,D429,R39,U568,R92,D875,L835,D841,R877,U178,L221,U88,R592,U692,R455,U693,L419,U90,R609,U672,L293,U168,R175,D456,R319,D570,R504,D165,L232,D624,L604,D68,R807,D59,R320,D281,L371,U956,L788,D897,L231,D829,R287,D798,L443,U194,R513,D925,L232,U225,L919,U563,R448,D889,R661,U852,L950,D558,L269,U186,L625,U673,L995,U732,R435,U849,L413,D690,L158,D234,R361,D458,L271,U90,L781,U754,R256,U162,L842,U927,L144,D62,R928,D238,R473,U97,L745,U303,L487,D349,L520,D31,L825,U385,L133,D948,L39,U62,R801,D664,L333,U134,R692,U385,L658,U202,L279,D374,R489,D686,L182,U222,R733,U177,R94,D603,L376,U901,R216,D851,L155,D214,L460,U758,R121,D746,L180,U175,L943,U146,L166,D251,L238,U168,L642,D341,R281,U182,R539,D416,R553,D67,L748,U272,R257,D869,L340,U180,R791,U138,L755,D976,R731,U713,R602,D284,L258,U176,R509,U46,R935,U576,R96,U89,L913,U703,R833
L1006,D998,R94,D841,R911,D381,R532,U836,L299,U237,R781,D597,L399,D800,L775,D405,L485,U636,R589,D942,L878,D779,L751,U711,L973,U410,L151,U15,L685,U417,L106,D648,L105,D461,R448,D743,L589,D430,R883,U37,R155,U350,L421,U23,R337,U816,R384,D671,R615,D410,L910,U914,L579,U385,R916,U13,R268,D519,R289,U410,L389,D885,L894,U734,L474,U707,L72,U155,L237,U760,L127,U806,L15,U381,L557,D727,L569,U320,L985,D452,L8,D884,R356,U732,L672,D458,L485,U402,L238,D30,R644,U125,R753,U183,L773,U487,R849,U210,L164,D808,L595,D668,L340,U785,R313,D72,L76,D263,R689,U604,R471,U688,R462,D915,R106,D335,R869,U499,R190,D916,R468,D882,R56,D858,L143,D741,L386,U856,R50,U853,R151,D114,L773,U854,L290,D344,L23,U796,L531,D932,R314,U960,R643,D303,L661,D493,L82,D491,L722,U848,L686,U4,L985,D509,L135,D452,R500,U105,L326,D101,R222,D944,L645,D362,L628,U305,L965,U356,L358,D137,R787,U728,R967,U404,R18,D928,L695,D965,R281,D597,L791,U731,R746,U163,L780,U41,L255,U81,L530,D964,R921,D297,R475,U663,L226,U623,L984,U943,L143,U201,R926,U572,R343,U839,R764,U751,R128,U939,R987,D108,R474,U599,R412,D248,R125,U797,L91,D761,L840,U290,L281,U779,R650,D797,R185,D320,L25,U378,L696,U332,R75,D620,L213,D667,R558,U267,L846,U306,R939,D220,R311,U827,R345,U534,R56,D679,R48,D845,R898,U8,R862,D960,R753,U319,L886,D795,R805,D265,R876,U729,R894,D368,R858,U744,R506,D327,L903,U919,L721,U507,L463,U753,R775,D719,R315,U128,R17,D376,R999,D386,L259,U181,L162,U605,L265,D430,R35,D968,R207,U466,R796,D667,R93,U749,L315,D410,R312,U929,L923,U260,R638

Let’s get that gold star!

$ php src/day3/solve.php 
Day 3 Part 1 answer: 273

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/day3/part1/input with your own file. Did it work? Let me know!

Next up: Day 3 Part 2

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

— Day 2: 1202 Program Alarm (Part 2) —

The second part of the Day 2 puzzle looks like it doesn’t require any changes to our ElfComputer itself – but requires another ElfComputerInputFinder to run multiple programs to find a specific output after trying different program inputs.

So for this part I’m just going to take the logic from solve.php from Day 2 Part 1 and modify it to brute force loop through all the possible inputs until we get the correct output. The value ranges seem small enough that this wouldn’t take very long…

Each of the two input values will be between 0 and 99, inclusive

So that means that there are 100 * 100 = 10,000 possible input combinations to test.

So first I created a new test class – this time I decided that the ElfComputerInputFinder should return an output true/false if it could successfully generate the desired output value by modifying it’s internal program. I appreciate what is an “input” and what is and “output” is a little muddled here… So my test file looked like this:

<?php

use PHPUnit\Framework\TestCase;
use PuzzleSolvers\Day2\ElfComputerInputFinder;

final class Day2Part2Test extends TestCase
{
    public function testInputs(): void
    {
        $outputs = [
            true,
        ];
        foreach ($outputs as $inputFileNumber => $output) {
            $puzzleSolver = new ElfComputerInputFinder('day2/part2/' . $inputFileNumber);
            $puzzleSolver->run();
            $this->assertSame($output, $puzzleSolver->getOutput());
        }
    }
}

The value which we are trying to get as an output from the ElfComputer is the input to the ElfComputerInputFinder. Your own value might be different?

/inputs/day2/part2/0

19690720

Next I created the ElfComputerInputFinder Class which as I mentioned was just using the code from solve.php in Day 2 Part 1 with some slight modifications. The most important being that I set the output using the following comparison (NOTE that we cast the input to an integer)

$this->output = ($elfComputerProgram[0] === (int) $this->inputs[0]);
<?php

namespace PuzzleSolvers\Day2;

use PuzzleSolvers\PuzzleSolver;
use PuzzleSolvers\Day2\ElfComputer;

class ElfComputerInputFinder extends PuzzleSolver
{
    public function run()
    {
        $elfComputer = new ElfComputer('day2/part1/input.txt');
        $elfComputer->initialise();
        $elfComputer->writeMemory(12, 1);
        $elfComputer->writeMemory(2, 2);
        $elfComputer->run();
        $elfComputerProgram = $elfComputer->getProgram();

        $this->output = ($elfComputerProgram[0] === (int) $this->inputs[0]);
    }
}

Running make tests shows that we have a failing test. So the next step it to loop through all the possible inputs (0-99) for the two writeMemory() calls and halt if we find a set of inputs that produces the correct outcome.

public function run()
{
    $elfComputer = new ElfComputer('day2/part1/input.txt');

    for ($input0 = 0; $input0 < 99; $input0++) {
        for ($input1 = 0; $input1 < 99; $input1++) {
            $elfComputer->initialise();
            $elfComputer->writeMemory($input0, 1);
            $elfComputer->writeMemory($input1, 2);
            $elfComputer->run();
            $elfComputerProgram = $elfComputer->getProgram();

            $this->output = ($elfComputerProgram[0] === (int) $this->inputs[0]);
        }
    }    
}

Now make tests still failed for the above… One mistake is that the output is always being overwritten based on the last comparison. So let’s make sure we exit the loop as soon as we’ve found a match.

for ($input0 = 0; $input0 < 99 && !$this->output; $input0++) {
    for ($input1 = 0; $input1 < 99 && !$this->output; $input1++) {

Now we are exiting the loop, the current values for $input0 and $input1 should be the correct inputs for the desired output. So lets read ahead in the puzzle it looks like we still have to do something with those values.

Find the input noun and verb that cause the program to produce the output 19690720What is 100 * noun + verb? (For example, if noun=12 and verb=2, the answer would be 1202.)

Ok so lets write a test that assumes we have set a noun and verb and produces the correct output

public function testOutputForVerbNoun()
{
    $elfComputerInputFinder = new ElfComputerInputFinder('day2/part2/0');
    $elfComputerInputFinder->SetNoun(12);
    $elfComputerInputFinder->SetVerb(2);

    $output = $elfComputerInputFinder->getOutput();

    $this->assertSame(1202, $output);
}

Now lets make the test pass – add the properties and the required functions

private $noun;
private $verb;

...

public function setNoun(int $value)
{
    $this->noun = $value;
}

public function setVerb(int $value)
{
    $this->verb = $value;
}

public function getOutput()
{
    return 100 * $this->noun + $this->verb;
}

Running make tests shows that this is passing – but we’ve now broken the initial test as we’ve modified what getOutput() returns.

Instead let’s use something like isMatchFound() in that test

public function testInputs(): void
{
    $outputs = [
        true,
    ];
    foreach ($outputs as $inputFileNumber => $output) {
        $puzzleSolver = new ElfComputerInputFinder('day2/part2/' . $inputFileNumber);
        $puzzleSolver->run();
        $this->assertSame($output, $puzzleSolver->isMatchFound());
    }
}

and make it pass

class ElfComputerInputFinder extends PuzzleSolver
{

...

    private $isMatchFound;

...

        for ($input0 = 0; $input0 < 99 && !$this->isMatchFound; $input0++) {
            for ($input1 = 0; $input1 < 99 && !$this->isMatchFound; $input1++) {

...

                $this->isMatchFound = ($elfComputerProgram[0] === (int) $this->inputs[0]);
 
...

    public function isMatchFound()
    {
        return $this->isMatchFound;
    }

...

We also need to actually set the noun and verb if we find a match

        if ($this->isMatchFound) {
            $this->noun = $input0;
            $this->verb = $input1;
        }

So here is the final class

/src/day2/ElfComputerInputFinder.php

<?php

namespace PuzzleSolvers\Day2;

use PuzzleSolvers\PuzzleSolver;
use PuzzleSolvers\Day2\ElfComputer;

class ElfComputerInputFinder extends PuzzleSolver
{
    private $noun;
    private $verb;
    private $isMatchFound;

    public function run()
    {
        $elfComputer = new ElfComputer('day2/part1/input.txt');

        for ($input0 = 0; $input0 < 99 && !$this->isMatchFound; $input0++) {
            for ($input1 = 0; $input1 < 99 && !$this->isMatchFound; $input1++) {
                $elfComputer->initialise();
                $elfComputer->writeMemory($input0, 1);
                $elfComputer->writeMemory($input1, 2);
                $elfComputer->run();
                $elfComputerProgram = $elfComputer->getProgram();

                $this->isMatchFound = ($elfComputerProgram[0] === (int) $this->inputs[0]);
                if ($this->isMatchFound) {
                    $this->noun = $input0;
                    $this->verb = $input1;
                }
            }
        }
    }

    public function setNoun(int $value)
    {
        $this->noun = $value;
    }

    public function isMatchFound()
    {
        return $this->isMatchFound;
    }

    public function setVerb(int $value)
    {
        $this->verb = $value;
    }

    public function getOutput()
    {
        return 100 * $this->noun + $this->verb;
    }
}

OK now our tests are passing! I can see quite a few holes in the code in terms of edge cases but I think it’s good enough to have a go at getting that star. So let’s update solve.php to run it.

/src/day2/solve.php

<?php

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

use PuzzleSolvers\Day2\ElfComputer;
use PuzzleSolvers\Day2\ElfComputerInputFinder;

$elfComputer = new ElfComputer('day2/part1/input.txt');
$elfComputer->initialise();
$elfComputer->writeMemory(12, 1);
$elfComputer->writeMemory(2, 2);
$elfComputer->run();
$output = $elfComputer->getProgram();

echo "Day 2 Part 1 answer: " . $output[0] . "\n";

$elfComputerInputFinder = new ElfComputerInputFinder('day2/part2/0');
$elfComputerInputFinder->run();
$output = $elfComputerInputFinder->getOutput();

echo "Day 2 Part 2 answer: " . $output . "\n";

Let’s get that gold star!

$ php src/day2/solve.php 
Day 2 Part 1 answer: 4138687
Day 2 Part 2 answer: 6635

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/day2/part2/0 with your own file. Did it work? Let me know!

Next up: Day 3 Part 1

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

— Day 2: 1202 Program Alarm (Part 1) —

So I’ve realised that it’s taking me far longer to write these blog posts than to actually solve the puzzles so far – and I’m never going to catch up with the most recent puzzles if I spend too long making these blog posts as well! So I’m going to try and be a bit briefer on stuff already covered in Day 1 Part 1 and Day 1 Part 2 – like setting up the test files.

So the first thing I did was take the provided inputs and place them into input files as before and create a new test file. In this part it looks like we are creating a sort of computer so I’ve called my class ElfComputer. I’ve also adopted the Day#Part# terminology for folders rather than puzzle#.

/src/day2/ElfComputer.php

<?php

namespace PuzzleSolvers\Day2;

use PuzzleSolvers\PuzzleSolver;

class ElfComputer extends PuzzleSolver
{
    public function run()
    {
    }
}

/tests/Day2Part1Test.php

<?php

use PHPUnit\Framework\TestCase;
use PuzzleSolvers\Day2\ElfComputer;

final class Puzzle1Part2Test extends TestCase
{
    public function testInputs(): 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 ElfComputer('day2/part1/' . $inputFileNumber);
            $puzzleSolver->run();
            $this->assertSame($output, $puzzleSolver->getOutput());
        }
    }
}

I’ve also updated the make tests command to use --testdox and --debug as I think this will be more useful going forward

Makefile

.PHONY: tests
tests:
	composer dump-autoload
	./vendor/phpunit/phpunit/phpunit --testdox --debug tests

So now we have some failing tests lets look at the requirements and see if we can break the problem down.

It looks like there are a few main concepts

  • program – which is an array of integers
  • position – which points at a specific part of a program
  • opcodes – which defines a type of action to perform

Step 1 – initialise the program and position

So lets start by loading the input we receive into the initial program and set the initial position to 0. I wrote a test first:

public function testInitialiseLoadsInputToProgramAndSetsPositionTo0(): void
{
    $elfComputer = new ElfComputer('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());
}

Now make to make the test pass I tried:

private $program;
private $position;

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

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

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

This was almost correct – except we’re doing strict type checking on the elements of the program array as integers in the test and we are getting strings instead.

   │ Failed asserting that two arrays are identical.
   │ --- Expected
   │ +++ Actual
   │ @@ @@
   │  Array &0 (
   │ -    0 => 1
   │ -    1 => 9
   │ -    2 => 10
   │ -    3 => 3
   │ -    4 => 2
   │ -    5 => 3
   │ -    6 => 11
   │ -    7 => 0
   │ -    8 => 99
   │ -    9 => 30
   │ -    10 => 40
   │ -    11 => 50
   │ +    0 => '1'
   │ +    1 => '9'
   │ +    2 => '10'
   │ +    3 => '3'
   │ +    4 => '2'
   │ +    5 => '3'
   │ +    6 => '11'
   │ +    7 => '0'
   │ +    8 => '99'
   │ +    9 => '30'
   │ +    10 => '40'
   │ +    11 => '50'
   │  )

So we need to map all the elements to integers which I did using array_map()

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

We now have a passing test 🙂

✔ Initialise loads input to program and sets position to 0

Step 2 – loop through the program and read opcodes

First I wanted to add two basic functions to read values at a given position or at current position with an optional offset. So I wrote the tests first using file 0 as input:

public function testReadMemory(): void
{
    $elfComputer = new ElfComputer('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 ElfComputer('day2/part1/0');
    $elfComputer->initialise();
    $elfComputer->setPosition(1);

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

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

Then make them pass

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 setPosition(int $position): void
{
    $this->position = $position;
}

So next we want to implement the loop to read from memory through the program

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

        $this->position++;
    }
}

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

Running make tests showed that we broke our initial test testInputs and introduced an error

Puzzle1 Part2
 ✘ Inputs
   │
   │ count(): Parameter must be an array or an object that implements Countable
   │
   │ /home/hamish/sites/adventofcode2019/src/day2/ElfComputer.php:30
   │ /home/hamish/sites/adventofcode2019/src/day2/ElfComputer.php:20
   │ /home/hamish/sites/adventofcode2019/tests/Day2Part1Test.php:19

This is because we are not calling initialise() on the elfComputer

foreach ($outputs as $inputFileNumber => $output) {
    $puzzleSolver = new ElfComputer('day2/part1/' . $inputFileNumber);
    $puzzleSolver->initialise();
    $puzzleSolver->run();
    $this->assertSame($output, $puzzleSolver->getOutput());
}

Step 3 – execute operations

So the next step is to start reading opcodes and executing them – this should start to make some of our basic test inputs pass to validate addition and multiplication

At this point I realised it would be useful to have some write operations to match our read operations so I created 2 more tests:

public function testWriteMemory(): void
{
    $elfComputer = new ElfComputer('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 ElfComputer('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]);
}

And make them pass

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;
}

We are given 3 opcodes

  • 1 – add
  • 2 – multiply
  • 99 – exit

The input example in file 1

1,0,0,0,99&nbsp;becomes&nbsp;<em>2</em>,0,0,0,99&nbsp;(1 + 1 = 2)

provides a functional test for the add operation.

So let’s just try and implement the operation

Opcode 1 adds together numbers read from two positions and stores the result in a third position. The three integers immediately after the opcode tell you these three positions – the first two indicate the positions from which you should read the input values, and the third indicates the position at which the output should be stored.

public function execute(int $opcode)
{
    if ($opcode === 1) {
        $value = $this->readMemoryOffset(1) + $this->readMemoryOffset(2);
        $this->writeMemory($value, 3);
    }
}

At this point I realised that we aren’t setting the output correctly at the conclusion of the program. So I updated the run() function as follows:

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

        $this->position++;
    }

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

Running tests again I can see that we’re getting close now. Although I’ve decided that the testInputs function is not actually clear enough as to which inputs are passing and which are failing – I’ll probably look at that in the next part…

Puzzle1 Part2
 ✘ Inputs
   │
   │ Failed asserting that two strings are identical.
   │ --- Expected
   │ +++ Actual
   │ @@ @@
   │ -'3500,9,10,70,2,3,11,0,99,30,40,50'
   │ +'1,9,10,19,2,3,11,0,99,30,40,50'

Next I implemented the multiply operation which is basically the same as 1 but with multiplication:

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

At this point after still having failing tests I realised I’ve made a mistake – my code is writing to position 3 every time – rather than reading the value at position offset 3 and then using that as the new position to write to. Here’s the fix:

public function execute(int $opcode)
{
    if ($opcode === 1) {
        $value = $this->readMemoryOffset(1) + $this->readMemoryOffset(2);
        $this->writeMemory($value, $this->readMemoryOffset(3));
    }

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

Running tests again I can see that I still haven’t got it right. I need to read the description more closely!

Opcode 1 adds together numbers read from two positions and stores the result in a third position. The three integers immediately after the opcode tell you these three positions – the first two indicate the positions from which you should read the input values, and the third indicates the position at which the output should be stored.

Again the key bit there is “the first two indicate the positions from which you should read the input values”. Currently we’re just reading them as actual values to be added. Ok so let’s try again:

public function execute(int $opcode)
{
    if ($opcode === 1) {
        $operand1Position = $this->readMemoryOffset(1);
        $operand2Position = $this->readMemoryOffset(2);
        $value = $this->readMemory($operand1Position) + $this->readMemory($operand2Position);
        $this->writeMemory($value, $this->readMemoryOffset(3));
    }

    if ($opcode === 2) {
        $operand1Position = $this->readMemoryOffset(1);
        $operand2Position = $this->readMemoryOffset(2);
        $value = $this->readMemory($operand1Position) * $this->readMemory($operand2Position);
        $this->writeMemory($value, $this->readMemoryOffset(3));
    }
}

OK finally we’re getting close. The above code could be condensed down to something like

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

But I prefer to keep it a bit more human-readable!

Finally lets add opcode 99 which just causes an early exit

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

make tests is now succeeding for everything except the last example

   │ Failed asserting that two strings are identical.
   │ --- Expected
   │ +++ Actual
   │ @@ @@
   │ -'30,1,1,4,2,5,6,0,99'
   │ +'30,1,3,4,2,5,6,0,99'

There’s still one major function of the program which we haven’t implemented yet – and it’s kind of amazing that the programs have been successful so far…

Once you’re done processing an opcode, move to the next one by stepping forward 4 positions.

So we’re currently iterating over every element in the program and executing it as if it were a valid opcode position! So let’s move up by 4 positions instead of 1 after execution:

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

        $this->position += 4;
    }

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

And voila! make tests are green.

But while tests are green there is catch in the final part of the puzzle

before running the program, replace position 1 with the value 12 and replace position 2 with the value 2What value is left at position 0 after the program halts?

So let’s get our real test input file, save it to /day2/part1/input.txt

1,0,0,3,1,1,2,3,1,3,4,3,1,5,0,3,2,6,1,19,1,19,10,23,2,13,23,27,1,5,27,31,2,6,31,35,1,6,35,39,2,39,9,43,1,5,43,47,1,13,47,51,1,10,51,55,2,55,10,59,2,10,59,63,1,9,63,67,2,67,13,71,1,71,6,75,2,6,75,79,1,5,79,83,2,83,9,87,1,6,87,91,2,91,6,95,1,95,6,99,2,99,13,103,1,6,103,107,1,2,107,111,1,111,9,0,99,2,14,0,0

Let’s create a basic solve.php script and modify it slightly to get the answer exactly as requested. Using our solve.php script from Day 1 Part 1 gives:

/src/day2/solve.php

<?php

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

use PuzzleSolvers\Day2\ElfComputer;

$elfComputer = new ElfComputer('day2/part1/input.txt');
$elfComputer->run();
echo "Day 2 Part 1 answer: " . $elfComputer->getOutput() . "\n";

So how do we modify the above to get the precise answer:

<?php

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

use PuzzleSolvers\Day2\ElfComputer;

$elfComputer = new ElfComputer('day2/part1/input.txt');
$elfComputer->initialise();
$elfComputer->writeMemory(12, 1);
$elfComputer->writeMemory(2, 2);
$elfComputer->run();
$output = $elfComputer->getProgram();

echo "Day 2 Part 1 answer: " . $output[0] . "\n";

Note that I’m using getProgram() – which returns an array – rather than getOutput() – which returns a string.

Let’s get that gold star!

$ php src/day2/solve.php 
Day 2 Part 1 answer: 4138687

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

Next up: Day 2 Part 2

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

— Day 1: The Tyranny of the Rocket Equation (Part 2) —

So we got through Day 1 Part 1 and a lot of the ground work with setting up the test suite to handle inputs and outputs – it should all be plain sailing from here! Well maybe…

The first impressions of the Part 2 puzzle description

However, that fuel also requires fuel, and that fuel requires fuel, and so on.

(and from what I know about the real rocket equation) is that this part could be solved with some kind of recursion or while loop.

So again the first thing I want to do is to set up my tests using the inputs and outputs provided. I created /tests/Puzzle1Part2Test.php replacing FuelCounterUpper with FuelCounterUpper2:

/tests/Puzzle1Part2Test.php

<?php

use PHPUnit\Framework\TestCase;
use PuzzleSolvers\Puzzle1\FuelCounterUpper2;

final class Puzzle1Part2Test extends TestCase
{
    public function testInputs(): void
    {
        $outputs = [2, 966, 50346];
        foreach ($outputs as $inputFileNumber => $output) {
            $puzzleSolver = new FuelCounterUpper2('puzzle1/part2/' . $inputFileNumber . '.txt');
            $puzzleSolver->run();
            $this->assertSame($output, $puzzleSolver->getOutput());
        }
    }
}

And then created new input files in /inputs/puzzle1/part2/

/inputs/puzzle1/part2/0.txt

14

/inputs/puzzle1/part2/1.txt

1969

/inputs/puzzle1/part2/2.txt

100756

FuelCounterUpper2 is just a direct copy of FuelCounterUpper to begin with

/src/puzzle1/FuelCounterUpper2.php

<?php

namespace PuzzleSolvers\Puzzle1;

use PuzzleSolvers\PuzzleSolver;

class FuelCounterUpper2 extends PuzzleSolver
{
    public function run()
    {
        $this->output = 0;

        foreach ($this->inputs as $mass) {
            $this->output += $this->calculateFuelForMass($mass);
        }
    }

    public function calculateFuelForMass($mass)
    {
        return (int) floor($mass / 3) - 2;
    }
}

Next running make tests shows that we’re still passing for the first input but not the others:

There was 1 failure:

1) Puzzle1Part2Test::testInputs
Failed asserting that 654 is identical to 966.

With that setup out of the way it was time to dig into the new requirements a bit more and work out what was missing.

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

So how do we achieve this?

Basically we want to calculate fuelForModules by running $mass = $this-&gt;calculateFuelForMass($mass); inside a while loop until the mass becomes less than 0. Then add that to the total output.

public function run()
{
    $this->output = 0;

    foreach ($this->inputs as $mass) {
        $fuelForModule = 0;

        while ($mass > 0) {
            $mass = $this->calculateFuelForMass($mass);
            $fuelForModule += $mass;
        }
        $this->output += $fuelForModule;
    }
}

But make tests showed that this was failing giving an output of 0 instead of 2 for the input of 14. I realised that the second time through the while loop was returning a mass of -2 and was not being ignored.

Any mass that would require negative fuel should instead be treated as if it requires zero fuel;

The next change was to only add mass when greater than zero:

if ($mass > 0) {
    $fuelForModule += $mass;
}

Now make tests all passed! As in part 1 – I combined the inputs and outputs to create a new multi-line test case

/inputs/puzzle1/part2/3.txt

14
1969
100756

And updated the outputs array in /tests/Puzzle1Part2.php

$outputs = [2, 966, 50346, (2 + 966 + 50346)];

make tests all passed. So next I updated my solve.php script to provide answers to both parts:

/src/puzzle1/solve.php

<?php

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

use PuzzleSolvers\Puzzle1\FuelCounterUpper;
use PuzzleSolvers\Puzzle1\FuelCounterUpper2;

$fuelCounterUpper = new FuelCounterUpper('puzzle1/input.txt');
$fuelCounterUpper->run();
echo "Part 1 answer: " . $fuelCounterUpper->getOutput() . "\n";

$fuelCounterUpper2 = new FuelCounterUpper2('puzzle1/input.txt');
$fuelCounterUpper2->run();
echo "Part 2 answer: " . $fuelCounterUpper2->getOutput() . "\n";

Let’s get that gold star!

$ php src/puzzle1/solve.php
Part 1 answer: 3464458
Part 2 answer: 5193796

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

Next up: Day 2 Part 1

Advent of Code 2019 Day 1 (Part 1) PHP Hints

— Day 1: The Tyranny of the Rocket Equation (Part 1) —

At last it’s time to dive in to some code – if you haven’t yet please check out the Getting Started post to make sure you have the same setup or similar to me if you want to follow along. The final code for this part can be found on GitHub here.

So reading through the puzzle description (which is great) we are building a FuelCounterUpper – essentially we’re trying to solve the rocket equation to calculate how much fuel is required to launch a given module is based on its mass.

We are given an explanation of how this should be done and also crucially 4 test scenarios with input and expected output. So the first thing to do is to add them to our test harness.

I created a new test file called Puzzle1Test.php. In this file I wanted to define the test outputs, loop through each of the test files, create a new FuelCounterUpper object using the input and test its output against the expected output.

/tests/Puzzle1Test.php

<?php

use PHPUnit\Framework\TestCase;
use PuzzleSolvers\Puzzle1\FuelCounterUpper;

final class Puzzle1Test extends TestCase
{
    public function testInputs(): void
    {
        $outputs = [2, 2, 654, 33583];
        foreach ($outputs as $inputFileNumber => $output) {
            $puzzleSolver = new FuelCounterUpper('puzzle1/' . $inputFileNumber . '.txt');
            $puzzleSolver->run();
            $this->assertSame($output, $puzzleSolver->getOutput());
        }
    }
}

Next I created 4 test input files with just the provided inputs as content.

/inputs/puzzle1/0.txt

12

/inputs/puzzle1/1.txt

14

/inputs/puzzle1/2.txt

1969

/inputs/puzzle1/3.txt

12

Using the files is a bit cumbersome for single inputs but reading ahead we will be provided with a larger more complex file for the actual final calculation. So I thought it’s a good idea to just always use files. Finally I created the FuelCounterUpper Class which extends the PuzzleSolver Class from Getting Started.

/src/FuelCounterUpper.php

<?php

namespace PuzzleSolvers;

class FuelCounterUpper extends PuzzleSolver
{
    public function run()
    {

    }
}

Now running make tests is providing an end-to-end test suite for us to implement our actual logic in the run function.

NOTE: After adding classes I needed to regenerate the composer autoload paths by running composer dump-autoload. I decided to add this to the Makefile before running the tests.

Makefile

.PHONY: tests
tests:
	composer dump-autoload
	./vendor/phpunit/phpunit/phpunit tests/*

Solution

This is really where we get to think about the specific problem at hand – hopefully the setup so far is useful in getting started and you might want to just take it from here.

For me the next step was to look at the specific mass to fuel calculation and create a new function in the FuelCounterUpper Class.

public function calculateFuelForMass(int $mass)
{
}

From the instructions we are told

to find the fuel required for a module, take its mass, divide by three, round down, and subtract 2.

So this seems reasonably simple to implement. First let’s try something like this:

public function calculateFuelForMass(int $mass)
{
    return $mass / 3 - 2;
}

And modify the run() function to loop through the inputs and pass them as arguments to the new function:

public function run()
{
    foreach ($this->inputs as $mass) {
        $this->output = $this->calculateFuelForMass($mass);
    }
}

Now if we run make tests again we see we have 1 passing assertion! But followed by a failing assertion.

There was 1 failure:

1) Puzzle1Test::testInputs
Failed asserting that 2.666666666666667 is identical to 2.

/tests/Puzzle1Test.php:14

FAILURES!
Tests: 1, Assertions: 2, Failures: 1.

So our calculation is correct for input file 0.txt but not for 1.txt. What’s the missing piece?

The missing piece is the rounding down part of the equation

to find the fuel required for a module, take its mass, divide by three, round down, and subtract 2.

In PHP there is a handy floor() function to round numbers down. So our function becomes:

public function calculateFuelForMass(int $mass)
{
    return floor($mass / 3) - 2;
}

Running make tests again we still get an interesting failure

Failed asserting that 2.0 is identical to 2

Because my tests are using the stricter assertSame function and PHP plays fast and loose with types – the values are not technically identical. Our calculated value was converted to a float when calling the floor() function. From php.net

The return value of floor() is still of type float because the value range of float is usually bigger than that of integer.

https://www.php.net/manual/en/function.floor.php

So we just need to ensure we cast the value back to an integer.

public function calculateFuelForMass(int $mass)
{
    return (int) floor($mass / 3) - 2;
}

Now if we run make tests again we have all 4 assertions passing! So we can be pretty confident that our FuelCounterUpper is doing the right thing. If you want you could create your own test inputs and expected outputs to test other scenarios and edge cases. But for the purposes of this blog I’m generally ignoring that unless it is clearly needed.

Ok so next it’s time to get your live input. Click on the link in the puzzle description and it should open a new page. Copy and Paste the contents into a new file in your /inputs/puzzle1 folder. I believe that every person will get a unique input file – and will require a unique answer. Here was the input I received:
<strong>/inputs/puzzle1/real.txt</strong>

63455
147371
83071
57460
74392
145303
130181
53102
120073
93111
144471
105327
116466
67222
122845
146097
92014
114428
96796
131140
101481
87953
101415
75739
64263
94257
140426
62387
84464
104547
103581
89121
123301
64993
143555
55246
120986
67596
146173
149707
60285
83517
73782
103464
140506
78400
140672
141638
84470
116879
100701
63976
135748
65021
120086
147249
55441
135315
147426
93676
91384
110918
123368
102430
144807
82761
134357
62990
85171
134886
69166
119744
80648
96752
89379
136178
95175
124306
51990
57564
111347
79317
95357
85765
137827
105014
110742
105014
149330
78437
107908
139044
143304
90614
52119
147113
119815
125634
104335
138295

So we can now see that rather than a single mass value in the file there is a a long list. In the last part of the puzzle we’re told:

The Fuel Counter-Upper needs to know the total fuel requirement. To find it, individually calculate the fuel needed for the mass of each module (your puzzle input), then add together all the fuel values.

So we need to modify our program to add each of the outputs together rather than just a single fuel calculation. Now it’s time to create my own test file using what we’ve learned so far – I combined all of the inputs from the original tests into /inputs/puzzle1/4.txt and then added the sum of the outputs as the new expected output (2 + 2 + 654 + 33583).

<strong>/inputs/puzzle1/4.txt</strong>

12
14
1969
100756

/tests/Puzzle1Test.php

<?php

use PHPUnit\Framework\TestCase;
use PuzzleSolvers\Puzzle1\FuelCounterUpper;

final class Puzzle1Test extends TestCase
{
    public function testInputs(): void
    {
        $outputs = [2, 2, 654, 33583, (2 + 2 + 654 + 33583)];
        foreach ($outputs as $inputFileNumber => $output) {
            $puzzleSolver = new FuelCounterUpper('puzzle1/' . $inputFileNumber . '.txt');
            $puzzleSolver->run();
            $this->assertSame($output, $puzzleSolver->getOutput());
        }
    }
}

After running make tests to see that we had a failing assertion again the last change was to sum the output after each calculation.

public function run()
{
    $this->output = 0;

    foreach ($this->inputs as $mass) {
        $this->output += $this->calculateFuelForMass($mass);
    }
}

Now all assertions are passing – we still need a way to just run the real puzzle input file through our class. I created a simple solve.php script:

<strong>src/puzzle1/solve.php</strong>

<?php

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

use PuzzleSolvers\Puzzle1\FuelCounterUpper;

$fuelCounterUpper = new FuelCounterUpper('puzzle1/input.txt');
$fuelCounterUpper->run();
echo "answer: " . $fuelCounterUpper->getOutput() . "\n";

Let’s get that gold star!

$ php src/puzzle1/solve.php
answer: 3464458

The final code for this part can be found on GitHub here. Remember my answer will not be the same as yours for your real input. Make sure to replace your /inputs/puzzle1/input.txt with your own file. Did it work? Let me know! In the next part I’ll take a look at getting that 2nd gold star.