DEV Community

Cover image for AoC Day 16: Chronal Classification
Ryan Palo
Ryan Palo

Posted on • Edited on

AoC Day 16: Chronal Classification

Note: I accidentally blew away all of the content of this post, so forgive me if it's suddenly much different than it used to be.

On Day 16, we've got some example inputs and outputs to machine instructions in our time machine and we need to figure out which opcodes do which things.

There were a few challenges similar to this in years past. Let's see how everybody likes this one.

Top comments (6)

Collapse
 
neilgall profile image
Neil Gall

Machine simulations have come up before in Advent of Code and are fun to implement. This one is an interesting twist. As I read the description I thought we were going to have to decipher the opcodes, but fortunately part 1 is a bit simpler than that.

Parsing

Complex text structure again. You know what that means! A data model and a JParsec parser.

data class Registers(val regs: IntArray)

data class Arguments(val a: Int, val b: Int, val c: Int)

data class Instruction(val opcode: Int, val arguments: Arguments)

data class Sample(val before: Registers, val instruction: Instruction, val after: Registers)

data class Input(val samples: List<Sample>, val program: List<Instruction>)

fun parse(input: String): Input {
    val integer: Parser<Int> = INTEGER.map(String::toInt)
    val registers = integer.sepBy(string(", ")).between(string("["), string("]")).map(::Registers)
    val before = string("Before: ").next(registers)
    val after = string("After:  ").next(registers)
    val arguments = sequence(integer, WHITESPACES.next(integer), WHITESPACES.next(integer), ::Arguments)
    val instruction = sequence(integer, WHITESPACES.next(arguments), ::Instruction)
    val sample = sequence(before, WHITESPACES.next(instruction), WHITESPACES.next(after), ::Sample)
    val samples = sample.sepBy(WHITESPACES)
    val program = instruction.sepBy(WHITESPACES)

    return sequence(samples.followedBy(WHITESPACES), program, ::Input).parse(input.trim())
}

Machine simulation

At first I wrote all the instructions out as Kotlin functions but then decided it might be better to encode them as lambdas along with additional information such as the name. I wrote them as destructive in-place operations on a Registers since I have the feeling we're going to have to run the program at some point!

data class Operation(val name: String, val run: (Registers, Arguments) -> Unit)

val operations: List<Operation> = listOf(
    Operation("addr", { r, (a, b, c) -> r[c] = r[a] + r[b] }),
    Operation("addi", { r, (a, b, c) -> r[c] = r[a] +   b  }),

    Operation("mulr", { r, (a, b, c) -> r[c] = r[a] * r[b] }),
    Operation("muli", { r, (a, b, c) -> r[c] = r[a] *   b  }),

    Operation("banr", { r, (a, b, c) -> r[c] = r[a] and r[b] }),
    Operation("bani", { r, (a, b, c) -> r[c] = r[a] and   b  }),

    Operation("borr", { r, (a, b, c) -> r[c] = r[a] or r[b] }),
    Operation("bori", { r, (a, b, c) -> r[c] = r[a] or   b  }),

    Operation("setr", { r, (a, _, c) -> r[c] = r[a] }),
    Operation("seti", { r, (a, _, c) -> r[c] =   a  }),

    Operation("gtri", { r, (a, b, c) -> r[c] = if (r[a] >   b ) 1 else 0 }),
    Operation("gtir", { r, (a, b, c) -> r[c] = if (  a  > r[b]) 1 else 0 }),
    Operation("gtrr", { r, (a, b, c) -> r[c] = if (r[a] > r[b]) 1 else 0 }),

    Operation("eqri", { r, (a, b, c) -> r[c] = if (r[a] ==   b ) 1 else 0 }),
    Operation("eqir", { r, (a, b, c) -> r[c] = if (  a  == r[b]) 1 else 0 }),
    Operation("eqrr", { r, (a, b, c) -> r[c] = if (r[a] == r[b]) 1 else 0 })
)

Part 1

Since we need to run different instructions over the same input data we really need non-destructive versions of these operations. That's possible with a simple wrapper.

class PureOperation(val name: String, val run: (Registers, Arguments) -> Registers) {
    constructor(op: Operation): this(op.name, { r, a -> 
        val r_ = r.copy()
        op.run(r_, a)
        r_
    })

    override fun toString(): String = name
}

val pureOperations = operations.map(::PureOperation)

Checking the number of operations which match a sample is easy;

fun Sample.test(): Int =
    pureOperations.count { op -> op.run(before, instruction.arguments) == after }

And checking the number of samples which pass this test is similar:

fun part1(input: Input): Int =
    input.samples.map(Sample::test).count { it >= 3 }

Part 2

Now we really have to map opcodes to instructions! This is an extension of the part 1 puzzle where the samples are matches to opcodes. We have to deduce the unique mapping of opcodes to instructions however. My algorithm for this was:

  1. For each sample and each operation
    • if the operation matches the sample output, add it to the possible set for that opcode
    • if the operation does not match the sample output, add it to the impossible set for that opcode
  2. Each opcode's resolved set is its possible operations minus its impossible operations
  3. While any opcode's resolved set is not a single operation
    • find all the opcodes which have resolved to a single operation
    • remove that operation from all other opcodes

In Kotlin:

fun matchOpcodes(input: Input): Map<Int, PureOperation> {

    data class Match(val possible: MutableSet<PureOperation>, val notPossible: MutableSet<PureOperation>) {
        val resolve: Set<PureOperation> get() = possible - notPossible
        val isResolved: Boolean get() = resolve.size == 1
        override fun toString(): String = resolve.toString()
    }

    val matches = mutableMapOf<Int, Match>()
    (0..15).forEach { i -> matches[i] = Match(mutableSetOf(), mutableSetOf()) }

    input.samples.forEach { sample -> 
        pureOperations.forEach { op ->
            val match = matches[sample.instruction.opcode]!!
            if (sample.matches(op))
                match.possible += op
            else
                match.notPossible += op
        }
    }

    while (matches.any { (_, m) -> !m.isResolved }) {
        matches.filter { (_, m) -> m.isResolved }.forEach { (opcode, resolvedMatch) ->
            matches.filter  { (key, _) -> key != opcode }
                   .forEach { (_, match: Match) -> match.notPossible += resolvedMatch.resolve }
        }
    }

    if (matches.any { e -> !e.value.isResolved })
        throw IllegalStateException("failed to match all opcodes: $matches")

    return matches.mapValues { entry -> entry.value.resolve.first() }
}

Running the program is a simple fold over the registers. Turns out I didn't need my in-place destructive operations after all, so there's a possible refactor there.

val finalState: Registers = input.program.fold(Registers()) { registers, instr ->
    opcodes[instr.opcode]!!.run(registers, instr.arguments)
}

Full code on github

Collapse
 
choroba profile image
E. Choroba • Edited

Fortunately, much easier than yesterday, it would have ruined my Advent otherwise.

I started implementing the dispatch table for the opcodes like this

    addr => sub { $r[ $_[2] ] = $r[ $_[0] ] + $r[ $_[1] ] },
    addi => sub { $r[ $_[2] ] = $r[ $_[0] ] + $_[1] },
    ...

but it was a lot of repetitive typing, so I let Perl generate the code for me from simpler strings.

#!/usr/bin/perl
use warnings;
use strict;
use feature qw{ say };

my %opcodes = (
    addr => 'rc=ra+rb',  addi => 'rc=ra+vb',
    mulr => 'rc=ra*rb',  muli => 'rc=ra*vb',
    banr => 'rc=ra&rb',  bani => 'rc=ra&vb',
    borr => 'rc=ra|rb',  bori => 'rc=ra|vb',
    setr => 'rc=ra',     seti => 'rc=va',
    gtir => 'rc=va>rb',  gtri => 'rc=ra>vb',  gtrr => 'rc=ra>rb',
    eqir => 'rc=va==rb', eqri => 'rc=ra==vb', eqrr => 'rc=ra==rb',
);

my @r;
for (values %opcodes) {
    s/v//g;
    s/([abc])/'$_[' . (ord($1) - 97) . ']'/ge;
    s/r([^\]]+\])/\$r[$1]/g;
    $_ = eval "sub{$_}";
}

The rest of part 1 was easy: for each input case, just count how many opcodes modify all the registers in the given way and keep a count of the cases where there were at least 3 such opcodes.

my $count;
local $/ = "";
while (<>) {
    my @registers_before = /Before: \[(\d+), (\d+), (\d+), (\d+)\]/;
    my @registers_after  = /After:  \[(\d+), (\d+), (\d+), (\d+)\]/;
    my @code = /(\d+) (\d+) (\d+) (\d+)/;
    last unless @registers_before;

    my $number_of_opcodes = 0;
    for my $opcode (keys %opcodes) {
        @r = @registers_before;
        $opcodes{$opcode}->(@code[1, 2, 3]);
        my $same = grep $r[$_] == $registers_after[$_], 0 .. $#r;
        ++$number_of_opcodes if @r == $same;
    }
    ++$count if $number_of_opcodes >= 3;
}

say $count;

For part 2, I collected all the guesses for each instruction number, and kept removing those that only had one solution until I knew all of them. Then I just translated each instruction number to the opcode and ran it.

my %guess;
$guess{$_} = { map +($_ => undef), keys %opcodes }
    for 0 .. (keys %opcodes) - 1;
my $program;
local $/ = "";
while (<>) {
    my @registers_before = /Before: \[(\d+), (\d+), (\d+), (\d+)\]/;
    my @registers_after  = /After:  \[(\d+), (\d+), (\d+), (\d+)\]/;
    my @code = /(\d+) (\d+) (\d+) (\d+)/;
    $program = $_, last unless @registers_before;

    my $number_of_opcodes = 0;
    for my $opcode (keys %opcodes) {
        @r = @registers_before;
        $opcodes{$opcode}->(@code[1, 2, 3]);
        my $same = grep $r[$_] == $registers_after[$_], 0 .. $#r;
        delete $guess{ $code[0] }{$opcode} unless $same == @r;
    }
}

my %know;
while (grep keys %$_ > 1, values %guess) {
    my @single = grep 1 == keys %{ $guess{$_} }, keys %guess;
    for my $single (@single) {
        $know{$single} = (keys %{ $guess{$single} })[0];
        delete $_->{ $know{$single}  } for values %guess;
    }
}
keys %{ $guess{$_} } or delete $guess{$_} for keys %guess;
for my $left (keys %guess) {
    $know{$left} = (keys %{ $guess{$left} })[0];
}

@r = (0) x 4;
while ($program =~ /(\d+) (\d+) (\d+) (\d+)/g) {
    $opcodes{ $know{$1} }->($2, $3, $4);
}
say $r[0];
Collapse
 
jenovs profile image
Viktors Jenovs

For this challenge I spent most of the time parsing input (those pesky line breaks).

After I meticulously recreated table with opcodes, solution itself was straight forward.

Part 1 - test each command, add to counter if success, add to bigger counter if more than three successes.

Part 2 - test each command to get a table with all possible opcodes, then find opcode(s) that have only one option, remove that opcode from the table. Repeat until the table is empty.

<?php
$rawInput = require_once 'readFile.php';

$input = [];
$testInput = [];

function parseRegStr($str) {
  return array_values(array_filter(str_split(explode(": ", $str)[1]), function($val) {
    return is_numeric($val);
  }));
}

function parseInStr($str) {
  $str = preg_replace("/\r|\n/", "", $str);
  return explode(" ", $str);
}

for ($i=0; true;) {
  if (empty($rawInput[$i])) {
    break;
  }
  if (explode(": ", $rawInput[$i])[0] != 'Before') {
    if (empty($rawInput[$i])) {
      break;
    }
    $testInput[] = explode(" ", preg_replace("/\r|\n/", "", $rawInput[$i]));
    $i++;
  } else {
    $input[] = ['b' => parseRegStr($rawInput[$i]), 'i' => parseInStr($rawInput[$i+1]), 'a' => parseRegStr($rawInput[$i+2])];
    $i += 4;
  }
}

$testInput = array_values(array_filter($testInput, function($val) {
  return count($val) > 1;
}));

$asm = [
  'addr' => function($r, $a, $b, $c) {
    $r[$c] = $r[$a] + $r[$b];
    return $r;
  },
  'addi' => function($r, $a, $b, $c) {
    $r[$c] = $r[$a] + $b;
    return $r;
  },
  'mulr' => function($r, $a, $b, $c) {
    $r[$c] = $r[$a] * $r[$b];
    return $r;
  },
  'muli' => function($r, $a, $b, $c) {
    $r[$c] = $r[$a] * $b;
    return $r;
  },
  'banr' => function($r, $a, $b, $c) {
    $r[$c] = $r[$a] & $r[$b];
    return $r;
  },
  'bani' => function($r, $a, $b, $c) {
    $r[$c] = $r[$a] & $b;
    return $r;
  },
  'borr' => function($r, $a, $b, $c) {
    $r[$c] = $r[$a] | $r[$b];
    return $r;
  },
  'bori' => function($r, $a, $b, $c) {
    $r[$c] = $r[$a] | $b;
    return $r;
  },
  'setr' => function($r, $a, $b, $c) {
    $r[$c] = $r[$a];
    return $r;
  },
  'seti' => function($r, $a, $b, $c) {
    $r[$c] = $a;
    return $r;
  },
  'gtir' => function($r, $a, $b, $c) {
    $r[$c] = $a > $r[$b] ? 1 : 0;
    return $r;
  },
  'gtri' => function($r, $a, $b, $c) {
    $r[$c] = $r[$a] > $b ? 1 : 0;
    return $r;
  },
  'gtrr' => function($r, $a, $b, $c) {
    $r[$c] = $r[$a] > $r[$b] ? 1 : 0;
    return $r;
  },
  'eqir' => function($r, $a, $b, $c) {
    $r[$c] = $a == $r[$b] ? 1 : 0;
    return $r;
  },
  'eqri' => function($r, $a, $b, $c) {
    $r[$c] = $r[$a] == $b ? 1 : 0;
    return $r;
  },
  'eqrr' => function($r, $a, $b, $c) {
    $r[$c] = $r[$a] == $r[$b] ? 1 : 0;
    return $r;
  },
];

function countThree($input, $asm) {
  $count = 0;
  foreach ($input as $value) {
    [$opcode, $a, $b, $c] = $value['i'];

    $samples = 0;
    foreach ($asm as $key => $val) {
      $result = $asm[$key]($value['b'], $a, $b, $c) == $value['a'];
      $result && $samples++;
    }
    $samples >= 3 && $count++;
  }
  return $count;
}

function getOpcodesTable($input, $asm) {
  $found = [];
  $table = [];
  foreach ($input as $value) {
    [$opcode, $a, $b, $c] = $value['i'];

    $samples = [];
    foreach (array_keys($asm) as $key) {
      $x = $asm[$key]($value['b'], $a, $b, $c) == $value['a'];
      if ($x) {
        $samples[$key] = $opcode;
      }
    }
    foreach ($samples as $k => $v) {
      if ($v != NULL && (empty($found[$k]) || !in_array($v, $found[$k]))) {
        $found[$k][] = $opcode;
      }
    }
  }

  while(count($found)) {
    foreach ($found as $key => $value) {
      if (count($value) == 1) {
        $opcode = $value[0];
        $table[$opcode] = $key;
        $found[$key] = NULL;
        foreach ($found as $key => $value) {
          if (!empty($found[$key])) {
            $found[$key] = array_values(array_filter($value, function($v) use($opcode) {
              return $v != $opcode;
            }));
          }
        }
      }
    }
    $found = array_filter($found);
  }
  return $table;
}

function runProgramm($input, $opcodes, $asm) {
  $r = [0, 0, 0, 0];
  foreach ($input as $v) {
    $r = $asm[$opcodes[$v[0]]]($r, $v[1], $v[2], $v[3]);
  }
  return $r[0];
}

function partTwo($input, $testInput, $asm) {
  $opcodes = getOpcodesTable($input, $asm);
  return runProgramm($testInput, $opcodes, $asm);
}

echo "Part 1: ". countThree($input, $asm) . "\n";
echo "Part 2: ". partTwo($input, $testInput, $asm) . "\n";
?>

readFile.php

<?php
$file = fopen("input.txt", "r") or exit("Unable to open file");

while(!feof($file)) {
  $array[] = fgets($file);
}

fclose($file);

return array_filter($array);
?>
Collapse
 
rpalo profile image
Ryan Palo

I agree with the main sentiment here, that there didn't seem to be a whole lot to this one. Lots of typing, but not too much thinking. I didn't like my initial code very much, though, I so I refactored some. I'm leaving all of the operations out addr, addi, etc. because they take up a lot of space and are all basically the same. Check them out in the git repo.

"""Day 16: Chronal Classification

Identify and classify time machine opcodes
"""

from collections import defaultdict, namedtuple
from itertools import chain

from day16_ops import OPERATIONS

Example = namedtuple("Example", ["before", "opcode", "after"])


def parse_examples(text):
    """Parses text to build 'before, command, after' examples.

    Input text has the pattern:
    Before: [0, 0, 0, 0]
    12 4 2 1
    After: [1, 2, 3, 4]
    """
    examples = []
    for triplet in text.split("\n\n"):
        before, command, after = triplet.splitlines()
        before_registers = [int(c) for c in before if c.isdigit()]
        opcode = [int(num) for num in command.split()]
        after_registers = [int(c) for c in after if c.isdigit()]
        examples.append(Example(
            before=before_registers,
            opcode=opcode,
            after=after_registers,
        ))
    return examples


def at_least_n_possible_opcodes(examples, n, operations):
    """Returns the number of cases that could have applied at least n operations"""
    result = 0
    for example in examples:
        _, a, b, c = example.opcode
        possible_ops = sum(
            1 for op in operations if op(example.before, a, b, c) == example.after
        )
        if possible_ops >= n:
            result += 1
    return result


def potential_op_ids(examples, operations):
    """Figure out which op_ids *could* match which operations"""
    potential_ids = defaultdict(set)
    for example in examples:
        op_id, a, b, c = example.opcode
        for op in operations:
            if op(example.before, a, b, c) == example.after:
                potential_ids[op].add(op_id)
    return potential_ids


def identify_opcodes(examples, operations):
    """Given a set of operation input/outputs, identifies which opcodes
    go with which operations.
    """
    potential_ids = potential_op_ids(examples, operations)
    available_op_ids = set(chain.from_iterable(potential_ids.values()))

    # Whittle down the possibilities by finding each op id that is guaranteed
    # to match to a particular operation function.  Then, remove that op id
    # from the running for every other op function.  This will cause more
    # op functions to only have one possible op id.  Lather, rinse, repeat.
    result = {}
    while potential_ids:
        for op, codes in potential_ids.items():
            if len(codes) == 1:
                code = list(codes)[0]
                result[code] = op
                available_op_ids.remove(code)
            # Remove any op_ids that are "spoken for" by another operation
            # Thank God for set mathematics
            potential_ids[op] = codes & available_op_ids

        # Filter out any operation functions that don't need figured out anymore
        potential_ids = {
            op: codes
            for op, codes in potential_ids.items()
            if len(codes) > 0
        }
    return result


def process_instructions(instructions, optable):
    """Given a starting bank of registers with value 0, what are the contents
    of the registers after all instructions are processed"""
    registers = [0] * 4
    for instruction in instructions:
        op_id, a, b, c = instruction
        registers = optable[op_id](registers, a, b, c)
    return registers


if __name__ == "__main__":
    with open("python/data/day16.txt", "r") as f:
        text = f.read()

    # Part 1
    examples = parse_examples(text)
    print(at_least_n_possible_opcodes(examples, 3, OPERATIONS))

    # Part 2
    optable = identify_opcodes(examples, OPERATIONS)
    with open("python/data/day16_ops.txt", "r") as f:
        instruction_text = f.read()
    instructions = [
        [int(value) for value in instruction.split()]
        for instruction in instruction_text.splitlines()
    ]
    print(process_instructions(instructions, optable))
Collapse
 
carstenk_dev profile image
Carsten

Of course we have to see some of those ;)

I like these kind of problems very much - they are especially easy to do in languages with coproduct-type support.

Compared to yesterday this one was easier - I hope we'll come back to this again.

Collapse
 
themindfuldev profile image
Tiago Romero

JavaScript solution

I'm gonna omit reader.js which is the same as the other solutions and jump to the point:

16-common.js

const beforeRegex = /^Before: \[(?<b0>\d), (?<b1>\d), (?<b2>\d), (?<b3>\d)\]$/;
const instructionRegex = /^(?<op>\d+) (?<a>\d) (?<b>\d) (?<c>\d)$/;
const afterRegex = /^After:  \[(?<a0>\d), (?<a1>\d), (?<a2>\d), (?<a3>\d)\]$/;

const readInput = lines => {
    const samples = [];
    const program = [];
    const n = lines.length;
    for (let i = 0; i < n; i++) {
        const line = lines[i];
        const sampleMatch = line.match(beforeRegex);
        if (sampleMatch) {
            const { b0, b1, b2, b3 } = sampleMatch.groups;
            const { op, a, b, c } = lines[i+1].match(instructionRegex).groups;
            const { a0, a1, a2, a3 } = lines[i+2].match(afterRegex).groups;
            samples.push({
                before: [+b0, +b1, +b2, +b3],
                instructions: [+op, +a, +b, +c],
                after: [+a0, +a1, +a2, +a3]
            });
            i += 2;
        }
        else {
            const programMatch = line.match(instructionRegex);
            if (programMatch) {
                const { op, a, b, c } = programMatch.groups;
                program.push([+op, +a, +b, +c]);
            }
        }

    }
    return { samples, program };
};

const operations = {
    'addr': ([op, a, b, c]) => registers => { 
        const result = [...registers];
        result[c] = registers[a] + registers[b]; 
        return result; 
    },
    'addi': ([op, a, b, c]) => registers => { 
        const result = [...registers];
        result[c] = registers[a] + b; 
        return result; 
    },
    'mulr': ([op, a, b, c]) => registers => { 
        const result = [...registers];
        result[c] = registers[a] * registers[b]; 
        return result; 
    },
    'muli': ([op, a, b, c]) => registers => { 
        const result = [...registers];
        result[c] = registers[a] * b; 
        return result; 
    },
    'banr': ([op, a, b, c]) => registers => { 
        const result = [...registers];
        result[c] = registers[a] & registers[b]; 
        return result; 
    },
    'bani': ([op, a, b, c]) => registers => { 
        const result = [...registers];
        result[c] = registers[a] & b; 
        return result; 
    },
    'borr': ([op, a, b, c]) => registers => { 
        const result = [...registers];
        result[c] = registers[a] | registers[b]; 
        return result; 
    },
    'bori': ([op, a, b, c]) => registers => { 
        const result = [...registers];
        result[c] = registers[a] | b; 
        return result; 
    },
    'setr': ([op, a, b, c]) => registers => { 
        const result = [...registers];
        result[c] = registers[a]; 
        return result; 
    },
    'seti': ([op, a, b, c]) => registers => { 
        const result = [...registers];
        result[c] = a; 
        return result; 
    },
    'gtir': ([op, a, b, c]) => registers => { 
        const result = [...registers];
        result[c] = a > registers[b] ? 1 : 0; 
        return result; 
    },
    'gtri': ([op, a, b, c]) => registers => { 
        const result = [...registers];
        result[c] = registers[a] > b ? 1 : 0; 
        return result; 
    },
    'gtrr': ([op, a, b, c]) => registers => { 
        const result = [...registers];
        result[c] = registers[a] > registers[b] ? 1 : 0; 
        return result; 
    },
    'eqir': ([op, a, b, c]) => registers => { 
        const result = [...registers];
        result[c] = a === registers[b] ? 1 : 0; 
        return result; 
    },
    'eqri': ([op, a, b, c]) => registers => { 
        const result = [...registers];
        result[c] = registers[a] === b ? 1 : 0; 
        return result; 
    },
    'eqrr': ([op, a, b, c]) => registers => { 
        const result = [...registers];
        result[c] = registers[a] === registers[b] ? 1 : 0;
        return result;
     }
};

module.exports = {
    readInput,
    operations
};

16a.js

const { readFile } = require('./reader');
const {
    readInput,
    operations
} = require('./16-common');

const evaluateSamples = samples => {
    let numberOfSamples = 0;

    for (let sample of samples) {
        const { before, instructions, after } = sample;

        const opcodes = Object.values(operations)
            .map(op => op(instructions)(before))
            .filter(result => result.toString() === after.toString()).length;

        if (opcodes >= 3) numberOfSamples += 1;
    }
    return numberOfSamples;
};

(async () => {
    const lines = await readFile('16-input.txt');

    const { samples } = readInput(lines);
    const numberOfSamples = evaluateSamples(samples);

    console.log(`The number of samples with three or more opcodes is ${numberOfSamples}`);    
})();

16b.js

const { readFile } = require('./reader');
const {
    readInput,
    operations
} = require('./16-common');

const determineOpcodes = samples => {   
    const opcodes = [];

    const remainingOperations = new Map(Object.entries(operations));

    let i = 0;
    while (remainingOperations.size > 0) {
        const { before, instructions, after } = samples[i];

        const candidates = [...remainingOperations]
            .map(([name, op]) => ({
                name,
                op,
                result: op(instructions)(before)
            }))
            .filter(({ result }) => result.toString() === after.toString());

        if (candidates.length === 1) {
            const { name, op } = candidates[0];

            const opcode = instructions[0];
            opcodes[opcode] = op;
            remainingOperations.delete(name);

            samples = samples.filter(({ instructions }) => instructions[0] !== opcode);
            i = 0;
        }

        i = (i + 1) % samples.length;
    }
    return opcodes;
};

const runProgram = (opcodes, program) => {
    let result = [0, 0, 0, 0];
    for (const instruction of program) {
        const op = opcodes[instruction[0]];
        result = op(instruction)(result);
    }
    return result;
};

(async () => {
    const lines = await readFile('16-input.txt');

    const { samples, program } = readInput(lines);
    const opcodes = determineOpcodes(samples);
    const result = runProgram(opcodes, program);

    console.log(`The state of the registers after executing the test program is ${result}`);
})();