- If status = 0, we have hit a wall, update the map so we know it is a wall and can skip it in the future.
- If status = 1, we moved one step. Now:
- If this new position is unvisited, add it to the frontier and update the data structures we are maintaining.
- Move in the reverse direction so that the next movement in the loop starts from the same base position.
- If status = 2, we have found the oxygen system. Exit after printing out the distance from the start position.
Introduction
Advent of Code(AoC) is an annual programming puzzle set created by Eric Wastl starting from 2015 where a new puzzle is released every day from 1 December to 25 December[1]. The puzzles slowly ramp up in difficulty as the date increases with the first day being rather trivial but getting extremely challenging roughly around day 17 onwards. While the puzzles are usually distinct from each other, in a twist, AoC 2019 introduced a common computer language shared between some days called Intcode. Intcode is an interesting language where it is conceptually similar to CISC machine code but is impractical to implement in hardware due to features like base-10 instruction decoding. To solve the Intcode days, participants needed to write an interpreter and environment that can successfully run the provided Intcode programs.
This article details the features of the Intcode language and goes into creating the interpreter and the environment programs (the code that interacts with the interpreter running the specific Intcode for a given puzzle in order to solve that puzzle). AoC 2019 participants had to iteratively adapt their interpreter to deal with the changes incrementally added to the Intcode specification and also create new environment programs for each puzzle. In spite of the fact that this article was written about 4 years after the the solution it is referencing (which in turn was written some 3 years after the publication of the challenges), I shall attempt to document the thought process behind the code and the incremental nature of solving these puzzles.
All of this is written in Rust. The code used in this article is available on a Mercurial repository hosted on Sourcehut. In particular, you might find the commit history interesting. Additionally, a summary of all features of Intcode can be found on the Intcode page of the Esolangs wiki.
Basic arithmetic (day 2)
An Intcode program is a comma-separated list of integers representing values in the memory of the program. We run this program by interpreting instructions from this list. The index of the current instruction is called the instruction pointer and starts out at 0. Instructions consist of an opcode followed optionally by some parameters. The opcode is either 1, 2, or 99. Opcode 1 takes 3 parameters and adds the integer at the position specified by its first parameter with the integer stored at the position specified by its second parameter and stores the result in the position specified by its third parameter. Opcode 2 works the same way as opcode 1 but performs multiplication instead of addition. Opcode 99 tells the program to halt immediately and has zero parameters. We increment the instruction pointer by number of values in the instruction (1 for opcode + number of parameters) to move onto the next instruction.
Program setup
I designed this as a simple CLI that takes two arguments: the puzzle program to run and the Intcode file that will be inputted. I add a few crates: argh for argument parsing, strum for a convenient way to convert enums to/from strings, and color_eyre for a convenient way to group unrelated errors and provide colorful error traces.
# Cargo.toml
[package]
name = "intcode"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
color-eyre = "0.5"
strum = "0.23"
strum_macros = "0.23"
argh = "0.1.7"use argh::FromArgs;
use color_eyre::eyre;
use std::fs;
use std::io;
use std::path::PathBuf;
use strum_macros::EnumString;
mod day_2;
fn main() -> eyre::Result<()> {
let Args {
program,
input_file,
} = argh::from_env();
let raw_intcode = if let Some(path) = input_file {
println!("Reading from {:?} ...", path);
fs::read_to_string(path)?
} else {
println!("Reading from stdin ...");
read_stdin()?
};
let intcode = parse_intcode(&raw_intcode)?;
println!("Successfully read and parsed intcode.");
match program {
Program::Day2Part1 => day_2::part_1(intcode)?,
};
Ok(())
}
fn read_stdin() -> io::Result<String> {
use io::Read;
let mut stdin = io::stdin();
let mut string = String::new();
stdin.read_to_string(&mut string)?;
Ok(string)
}
#[derive(Debug, Clone, FromArgs)]
/// An interpreter for the Intcode language introduced in Advent of Code 2019.
///
/// Currently implemented programs are:
/// day2part1
struct Args {
/// the program you want to run
#[argh(positional)]
program: Program,
/// the file to parse Intcode from. If excluded, reads from stdin
#[argh(positional)]
input_file: Option<PathBuf>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString)]
enum Program {
#[strum(serialize = "day2part1", serialize = "d2p1")]
Day2Part1,
}
type Intcode = Vec<i32>;
fn parse_intcode(raw: &str) -> eyre::Result<Intcode> {
Ok(raw
.split(',')
.map(str::trim)
.map(str::parse)
.collect::<Result<Vec<_>, _>>()?)
}For now, there is only one program to run and the string is parsed into a Vec of i32s. Each day will be in a separate module and every part of each day will be in its own function within that day’s module.
Solving part one
There is a rudimentary method for doing IO for day 2: modifying memory directly for inputs and reading from memory for output. Part one has us modify position 1 of memory to 12 and position 2 to 2 before running the program and the answer is the value at position 0 after the program finishes executing. I implement the (quite simple for now) logic directly in day 2 part 1 function to run the Intcode program and then print out the value at position 0 of memory.
We keep track of the current instruction using a variable. Then, we loop continuously and execute each instruction, matching on the instruction’s opcode, making sure to update the instruction pointer after the instruction is done computing. We break this loop only when we encounter a halt instruction.
// day_2.rs
use color_eyre::eyre;
pub fn part_1(mut intcode: Vec<i32>) -> eyre::Result<()> {
intcode[1] = 12;
intcode[2] = 2;
let mut current = 0;
loop {
match intcode[current] {
1 => {
let param_1 = intcode[current + 1] as usize;
let param_2 = intcode[current + 2] as usize;
let param_3 = intcode[current + 3] as usize;
intcode[param_3] = intcode[param_1] + intcode[param_2];
current += 4;
}
2 => {
let param_1 = intcode[current + 1] as usize;
let param_2 = intcode[current + 2] as usize;
let param_3 = intcode[current + 3] as usize;
intcode[param_3] = intcode[param_1] * intcode[param_2];
current += 4;
}
99 => {
break;
}
opcode @ _ => eyre::bail!("Unknown opcode {}", opcode),
}
}
println!("{}", intcode[0]);
Ok(())
}We have successfully completed the very first Intcode puzzle!
Solving part two
The two inputs to the program are called the noun and verb, respectively. We need to find the combination of noun and verb values that cause the computer to output the number 19690720. The noun and the verb are both limited to being between 0 and 99, inclusive.
This requires us to refactor the implementation into something we can call repeatedly. This could have been done with a standalone function, but I decided to go a small step further and create a struct in its own module that represents an Intcode computer and move the logic to a method on that struct. This struct keeps track of the current instruction (the instruction pointer) for us as an abstraction.
// runner.rs
use color_eyre::eyre;
#[derive(Debug, Clone)]
pub struct Intcode {
pub memory: Vec<i32>,
current: usize,
}
impl Intcode {
pub fn from_vec(vec: Vec<i32>) -> Self {
Self {
memory: vec,
current: 0,
}
}
pub fn execute(&mut self) -> eyre::Result<()> {
loop {
match self.memory[self.current] {
1 => {
let param_1 = self.memory[self.current + 1] as usize;
let param_2 = self.memory[self.current + 2] as usize;
let param_3 = self.memory[self.current + 3] as usize;
self.memory[param_3] = self.memory[param_1] + self.memory[param_2];
self.current += 4;
}
2 => {
let param_1 = self.memory[self.current + 1] as usize;
let param_2 = self.memory[self.current + 2] as usize;
let param_3 = self.memory[self.current + 3] as usize;
self.memory[param_3] = self.memory[param_1] * self.memory[param_2];
self.current += 4;
}
99 => {
break;
}
opcode @ _ => eyre::bail!("Unknown opcode {}", opcode),
}
}
Ok(())
}
}In main.rs, we add the new module and update the code to add the part 2 program.
// main.rs diff
+mod day_2;
+pub mod runner;
+
+use crate::runner::Intcode;
use argh::FromArgs;
use color_eyre::eyre;
use std::fs;
...
use std::path::PathBuf;
use strum_macros::EnumString;
-mod day_2;
-
fn main() -> eyre::Result<()> {
...
match program {
Program::Day2Part1 => day_2::part_1(intcode)?,
+ Program::Day2Part2 => day_2::part_2(intcode)?,
};
...
/// An interpreter for the Intcode language introduced in Advent of Code 2019.
///
/// Currently implemented programs are:
-/// day2part1
+/// day2part1,
+/// day2part2
struct Args {
...
enum Program {
#[strum(serialize = "day2part1", serialize = "d2p1")]
Day2Part1,
+
+ #[strum(serialize = "day2part2", serialize = "d2p2")]
+ Day2Part2,
}
-type Intcode = Vec<i32>;
-
fn parse_intcode(raw: &str) -> eyre::Result<Intcode> {
- Ok(raw
+ let vec = raw
.split(',')
.map(str::trim)
.map(str::parse)
- .collect::<Result<Vec<_>, _>>()?)
+ .collect::<Result<Vec<_>, _>>()?;
+ Ok(Intcode::from_vec(vec))
}In day_2.rs, we use the refactored out logic instead of directly implementing it in the day 2 module and add the code for part two that iterates over all possible noun and verb combinations and returns the matching noun-verb pair in the format specified.
// day_2.rs (new)
use crate::runner::Intcode;
use color_eyre::eyre;
use eyre::eyre;
pub fn part_1(mut intcode: Intcode) -> eyre::Result<()> {
intcode.memory[1] = 12;
intcode.memory[2] = 2;
intcode.execute()?;
println!("{}", intcode.memory[0]);
Ok(())
}
pub fn part_2(intcode: Intcode) -> eyre::Result<()> {
for noun in 0..100 {
for verb in 0..100 {
let mut intcode = intcode.clone();
intcode.memory[1] = noun;
intcode.memory[2] = verb;
intcode.execute()?;
if intcode.memory[0] == 19690720 {
println!("{}", noun * 100 + verb);
return Ok(());
}
}
}
Err(eyre!("no output found"))
}And we are done with day 2!
Many more features (day 5)
Day five adds quite a few features to the Intcode computer. It now is a much more full-fledged computer with IO, comparison, and branching.
Input and Output
Two new opcodes are introduced, one for input and one for output. Opcode 3 takes a single integer as input and stores it in the position given by its only parameter while opcode 4 takes a single parameter and outputs the value stored in memory at that position. It is upto the interpreter/surrounding program how the input and output is routed.
Parameter modes
The opcode is now only the 2 least significant digits of the instruction’s first number. The remaining digits now give the parameter modes for the instruction’s parameters with the first parameter’s mode being given by the 3rd least significant digit of the first number, the second parameter’s mode is the 4th least significant digit and so on. Modes can be either 0 or 1. A mode of 0 was the default until now and indicates position mode, the parameter is interpreted as a position of the actual value. A mode of 1 indicates immediate mode, that is, the value of the parameter is the parameter itself. Output parameters will always be in position mode and never in immediate mode.
For example:
- The instruction
1102,2,3,4tells the computer to store the product of 2 and 3 at position 4 in memory. - The instruction
1002,7,2,7doubles the value stored at memory address 7. - The instruction
11101,7,7,9is invalid because we cannot store the result in an immediate mode parameter.
With immediate mode parameters, negative integers must now also be supported. We don’t need to change anything for this since we are already using signed integers.
Solving part one
We need to extend our Intcode struct to support parameter modes and the new opcodes. The way I choose to route the IO for now is to just request/emit it interactively from/to stdin/stdout. I did it this way for simplicity as it requires minimal changes to the interpreter interface. I added helper methods that get the opcode and the parameter modes for any given instruction’s first integer. I also added a helper method on Intcode that returns N parameter values of the current instruction according to parameter modes of the first integer of the instruction.
// runner.rs
impl Intcode {
fn get_param_values<const N: usize>(&self) -> [i32; N] {
let mut param_modes = get_param_modes(self.memory[self.current]);
let mut i = self.current;
[(); N]
.map(|_| {
i += 1;
i
})
.map(|i| {
let mode = param_modes.next().unwrap();
match mode {
0 => self.memory[self.memory[i] as usize],
1 => self.memory[i],
other => panic!("Unknown parameter mode {}.", other),
}
})
}
}
fn get_opcode(num: i32) -> i32 {
num % 100
}
fn get_param_modes(num: i32) -> impl Iterator<Item = i32> {
use std::iter;
iter::repeat(()).scan(num / 100, |state, _| {
let out = *state % 10;
*state /= 10;
Some(out)
})
}Now, all that’s left is to modify our execute method to add the new opcodes and the parameter modes.
// runner.rs diff
pub fn execute(&mut self) -> eyre::Result<()> {
loop {
- match self.memory[self.current] {
+ match get_opcode(self.memory[self.current]) {
1 => {
- let param_1 = self.memory[self.current + 1] as usize;
- let param_2 = self.memory[self.current + 2] as usize;
- let param_3 = self.memory[self.current + 3] as usize;
- self.memory[param_3] = self.memory[param_1] + self.memory[param_2];
+ let [param_1, param_2] = self.get_param_values::<2>();
+ let out_index = self.memory[self.current + 3] as usize;
+ self.memory[out_index] = param_1 + param_2;
self.current += 4;
}
2 => {
- let param_1 = self.memory[self.current + 1] as usize;
- let param_2 = self.memory[self.current + 2] as usize;
- let param_3 = self.memory[self.current + 3] as usize;
- self.memory[param_3] = self.memory[param_1] * self.memory[param_2];
+ let [param_1, param_2] = self.get_param_values::<2>();
+ let out_index = self.memory[self.current + 3] as usize;
+ self.memory[out_index] = param_1 * param_2;
self.current += 4;
}
+ 3 => {
+ use std::io::{stdin, stdout, Write};
+ print!("Enter an integer> ");
+ stdout().flush();
+ let mut line = String::new();
+ stdin().read_line(&mut line)?;
+ let int = line.trim().parse()?;
+
+ let out_index = self.memory[self.current + 1] as usize;
+ self.memory[out_index] = int;
+ self.current += 2;
+ }
+ 4 => {
+ let [param] = self.get_param_values::<1>();
+ println!("Output> {}", param);
+ self.current += 2;
+ }
+
99 => {
break;
}
opcode @ _ => eyre::bail!("Unknown opcode {}", opcode),
}
}
Ok(())
}The provided Intcode program takes in a single number as input and outputs a single number. We need to provide it the number 1 to its single input instruction and the puzzle answer is the single number that it outputs. Our day 5 module is very sparse, with the function just calling the execute function provided by our Intcode struct. We type in 1 when the first input instruction runs and get the puzzle answer as output.
// day_5.rs
use crate::runner::Intcode;
use color_eyre::eyre;
use eyre::eyre;
pub fn part_1(mut intcode: Intcode) -> eyre::Result<()> {
intcode.execute()?;
Ok(())
}We are now done with day 5 part 1!
Comparisions and jumps
Part two of day five introduces 4 new opcodes for comparisons and branching.
- Opcode 5 is jump-if-true: if its first parameter is non-zero, it sets the instruction pointer to the second parameter’s value.
- Opcode 6 is jump-if-false: same as opcode 5 but jumps when its first parameter equals zero
- Opcode 7 is less than: if the first parameter is less than the second parameter, stores 1 in the position given by the third parameter
- Opcode 8 is equals: same as opcode 7 but checks if the first parameter equals the second parameter
Solving part two
We implement the 4 new instructions in our interpreter, being careful with how we update the instruction pointer for the 2 jump instructions.
self.current += 2;
}
+ 5 => {
+ let [param_1, param_2] = self.get_param_values::<2>();
+ if param_1 != 0 {
+ self.current = param_2 as usize;
+ } else {
+ self.current += 3;
+ }
+ }
+ 6 => {
+ let [param_1, param_2] = self.get_param_values::<2>();
+ if param_1 == 0 {
+ self.current = param_2 as usize;
+ } else {
+ self.current += 3;
+ }
+ }
+ 7 => {
+ let [param_1, param_2] = self.get_param_values::<2>();
+ let out_index = self.memory[self.current + 3] as usize;
+ self.memory[out_index] = (param_1 < param_2) as i32;
+ self.current += 4;
+ }
+ 8 => {
+ let [param_1, param_2] = self.get_param_values::<2>();
+ let out_index = self.memory[self.current + 3] as usize;
+ self.memory[out_index] = (param_1 == param_2) as i32;
+ self.current += 4;
+ }
99 => {
break;I also decided to do a bunch of miscellaneous refactoring after adding these 4 new opcodes.
I made it so the Intcode struct can be created by the parse method on str by implementing FromStr on it. I made the input file CLI option mandatory to simplify code.
- let raw_intcode = if let Some(path) = input_file {
- println!("Reading from {:?} ...", path);
- fs::read_to_string(path)?
- } else {
- println!("Reading from stdin ...");
- read_stdin()?
- };
- let intcode = parse_intcode(&raw_intcode)?;
+ println!("Reading from {:?} ...", input_file);
+ let raw_intcode = fs::read_to_string(input_file)?;
+ let intcode = raw_intcode.parse()?;
println!("Successfully read and parsed intcode.");
...
struct Args {
/// the program you want to run
#[argh(positional)]
program: Program,
- /// the file to parse Intcode from. If excluded, reads from stdin
+ /// the file to parse Intcode from
#[argh(positional)]
- input_file: Option<PathBuf>,
+ input_file: PathBuf,
}
...
-
-fn parse_intcode(raw: &str) -> eyre::Result<Intcode> {
- let vec = raw
- .split(',')
- .map(str::trim)
- .map(str::parse)
- .collect::<Result<Vec<_>, _>>()?;
- Ok(Intcode::from_vec(vec))
-}Also, I made a few minor changes to the interpreter module like adding an #[inline] declaration to the get opcode function to hopefully improve performance and improving the error message when the interpreter encounters an unknown opcode.
99 => {
break;
}
- opcode @ _ => eyre::bail!("Unknown opcode {}", opcode),
+ opcode @ _ => eyre::bail!("Unknown opcode {} at position {}", opcode, self.current),
}
}
...
+#[inline]
fn get_opcode(num: i32) -> i32 {
num % 100
}To test these new instructions, we are now supposed to input 5 to the program. Since the surrounding code for both part 1 and part 2 is the same for day 5, I create a single function that prints instructions for the user before executing the Intcode program and modify the main function to call this function for both parts.
// day_5.rs (new)
use crate::runner::Intcode;
use color_eyre::eyre;
pub fn exec(mut intcode: Intcode) -> eyre::Result<()> {
println!("Enter 1 for part 1 and 5 for part 2.");
intcode.execute()?;
Ok(())
}And we are done with day 5!
Some routing and multiple computers (day 7)
On this day, the puzzle input represents software for 5 amplifiers that are connected in series with each other and are labelled A to E depending on their position in the series. Each amplifier needs two inputs, the first one specifying a phase setting which is a number between 0 and 4 inclusive, and the second one is the signal to amplify. Each amplifier outputs one number, the output signal of the amplifier.
Amplifier A initially receives the input signal 0 and its output signal is passed to amplifier B’s input signal. B’s output signal is passed to C’s input signal and so on until we get to E’s output signal, which is the final amplified signal.
For part one, we need to try all permutations of phase settings possible and then find the highest possible final amplified signal. Two amplifiers cannot have the same phase setting.
Modifying our IO routing
All of this means that routing the Intcode computer’s IO to stdin/stdout will no longer suffice. We need to programmatically give input and receive output. Now, in terms of API design for this feature that we need to add, some thought is required. A simple approach would be to assume that the program takes a fixed number of inputs and then outputs numbers only after taking inputs and then we could have our execute function take a Vec of inputs and return a Vec of outputs.
While this approach would work for day 7 part 1, with a bit of foresight, we can anticipate that these assumptions will not hold true for future days or parts and should come up with a design that can handle inputs interspersed with outputs and have outside logic run in between IO. This is to allow the code outside the interpreter to process output when the interpreter encounters an output instruction and provide input when it encounters an input instruction. This means that the execution of the Intcode computer will need to pause and return to the surrounding context upon encountering IO instructions. Further, the surrounding context will need to know the reason behind this return. So now, there are three cases that the surrounding code will need to consider when the execute function returns: a) the program halted normally via the 99 opcode; b) the program gave some output; c) the program needs input. An enum in the return value of the execute function will be perfect to specify all three cases.
We also need to figure out how to handle the resumption of the Intcode program after the rest of the program is done processing output or computing input. The output case is easy, just call the execute function again. The input case is more tricky: we need to pass input to the program on the second call of the execute function. This requires a second argument to the execute function that has to be optional to handle the case of the first call that needed no input (or had its input already handled by an input instruction). We also need to be careful and invalidate the input parameter inside the execute function once we process an input instruction so that we don’t pass the same value to all input instructions. The take method on Option was really useful for this. To simplify day 2’s code, let’s also add a method that executes without any IO, mimicking the old behaviour of the execute function.
// runner.rs diff
impl Intcode {
- pub fn execute(&mut self) -> eyre::Result<()> {
+ pub fn execute_without_io(&mut self) -> eyre::Result<()> {
+ match self.execute(None)? {
+ HaltReason::NormalHalt => (),
+ other @ _ => eyre::bail!(
+ "Unexpected halt reason {:?} when executing without io",
+ other
+ ),
+ }
+ Ok(())
+ }
+ pub fn execute(&mut self, mut input: Option<i32>) -> eyre::Result<HaltReason> {
loop {
match get_opcode(self.memory[self.current]) {
1 => {
...
+
3 => {
- use std::io::{stdin, stdout, Write};
- print!("Enter an integer> ");
- stdout().flush()?;
- let mut line = String::new();
- stdin().read_line(&mut line)?;
- let int = line.trim().parse()?;
-
- let out_index = self.memory[self.current + 1] as usize;
- self.memory[out_index] = int;
- self.current += 2;
+ if let Some(input) = input.take() {
+ let out_index = self.memory[self.current + 1] as usize;
+ self.memory[out_index] = input;
+ self.current += 2;
+ } else {
+ break Ok(HaltReason::NeedInput);
+ }
}
4 => {
- let [param] = self.get_param_values::<1>();
- println!("Output> {}", param);
+ let [output] = self.get_param_values::<1>();
self.current += 2;
+ break Ok(HaltReason::GaveOutput(output));
}
...
99 => {
- break;
+ break Ok(HaltReason::NormalHalt);
}
opcode @ _ => eyre::bail!("Unknown opcode {} at position {}", opcode, self.current),
}
}
- Ok(())
}
...
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum HaltReason {
+ GaveOutput(i32),
+ NeedInput,
+ NormalHalt,
+}These API changes to our interpreter also necessitate changes in places where the execute function is called.
// day_2.rs diff
pub fn part_1(mut intcode: Intcode) -> eyre::Result<()> {
intcode.memory[1] = 12;
intcode.memory[2] = 2;
- intcode.execute()?;
+ intcode.execute_without_io()?;
println!("{}", intcode.memory[0]);
Ok(())
}
...
let mut intcode = intcode.clone();
intcode.memory[1] = noun;
intcode.memory[2] = verb;
- intcode.execute()?;
+ intcode.execute_without_io()?;
if intcode.memory[0] == 19690720 {
println!("{}", noun * 100 + verb);// day_5.rs diff
-use crate::runner::Intcode;
+use crate::runner::{HaltReason, Intcode};
use color_eyre::eyre;
-pub fn exec(mut intcode: Intcode) -> eyre::Result<()> {
- println!("Enter 1 for part 1 and 5 for part 2.");
- intcode.execute()?;
+pub fn part_1(intcode: Intcode) -> eyre::Result<()> {
+ exec(intcode, 1)
+}
+
+pub fn part_2(intcode: Intcode) -> eyre::Result<()> {
+ exec(intcode, 5)
+}
+
+fn exec(mut intcode: Intcode, input: i32) -> eyre::Result<()> {
+ let mut input = Some(input);
+ loop {
+ match intcode.execute(input.take())? {
+ HaltReason::NormalHalt => break,
+ HaltReason::GaveOutput(output) => println!("{}", output),
+ HaltReason::NeedInput => {
+ eyre::bail!("Invalid second request for input; input already given.")
+ }
+ }
+ }
Ok(())
}Solving part one
Now with the IO changes done, we can start solving day 7. We first need to find all length-5 permutations of the list [0, 1, 2, 3, 4]. There are several ways to do this:
- Compute it manually and hardcode it in our code. This would be very ugly and error-prone, given that P(5, 5) = 120, that is, there are 120 5-permutations of a set with 5 elements.
- Write code that computes permutations. Not too bad to do but would be somewhat annoying.
- Use an external library to compute the permutations for us. Very convenient but adds dependencies which increase compile time.
For convenience, I chose option 3. Option 2 would also be fine but I’d strongly advise against option 1. Let’s pull in the itertools crate as it has a permutations method that does exactly what we want.
# Cargo.toml diff
...
strum = "0.23"
strum_macros = "0.23"
argh = "0.1.7"
+itertools = "0.10"We iterate over all permutations of phase settings, and run the amplifier series for each phase setting and find the maximum final output signal we can get. To run the amplifier series, we create a list of 5 separate Intcode computers and call the execute function in a loop for each Intcode computer, first giving it its phase setting and then the output of the previous amplifier (or 0 if there’s no previous amplifier), storing the outputs in another list, and finally return the output of amplifier E.
// day_7.rs
use crate::runner::{HaltReason, Intcode};
use color_eyre::eyre;
use itertools::Itertools;
pub fn part_1(intcode: Intcode) -> eyre::Result<()> {
let mut highest_signal = i32::MIN;
for phase_setting in (0..=4).permutations(5).map(|vec| match vec[..] {
[a, b, c, d, e] => [a, b, c, d, e],
_ => unreachable!(),
}) {
highest_signal = highest_signal.max(run_amplifiers(intcode.clone(), phase_setting)?);
}
println!("{}", highest_signal);
Ok(())
}
fn run_amplifiers(intcode: Intcode, phase_setting: [i32; 5]) -> eyre::Result<i32> {
let mut amplifiers = [(); 5].map(|_| intcode.clone());
let mut outputs = [i32::MIN; 5];
for i in 0..5 {
let mut input = Some(phase_setting[i]);
loop {
match amplifiers[i].execute(input.take())? {
HaltReason::NormalHalt => break,
HaltReason::GaveOutput(output) => {
outputs[i] = output;
}
HaltReason::NeedInput => {
input = Some(outputs.get(i - 1).copied().unwrap_or(0));
}
}
}
}
Ok(outputs[4])
}We have now solved part one!
... Or have we? Running the above gives us:
> cargo run -- d7p1 inputs/day7
warning: unused import: `std::io`
--> src/main.rs:9:5
|
9 | use std::io;
| ^^^^^^^
|
= note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default
warning: `intcode` (bin "intcode") generated 1 warning (run `cargo fix --bin "intcode" -p intcode` to apply 1 suggestion)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.04s
Running `target/debug/intcode d7p1 inputs/day7`
Reading from "inputs/day7" ...
Successfully read and parsed intcode.
The application panicked (crashed).
Message: attempt to subtract with overflow
Location: src/day_7.rs:29
Backtrace omitted.
Run with RUST_BACKTRACE=1 environment variable to display it.
Run with RUST_BACKTRACE=full to include source snippets.Looking at line 29, we have input = Some(outputs.get(i - 1).copied().unwrap_or(0)); and since i is type-inferred to be a usize as we use it for indexing, i - 1 panics when i is 0. This is a bug in our code! Given that this happens the first time the for loop is run, it’s actually good because it always panics, making the bug obvious when executing. I only noticed this bug at the end of finishing all the Intcode problems in AoC 2019. Given that I was able to successfully solve Day 7 part 1 though, I think what happened was that this bug was added when I refactored the code but did not exist pre-refactor and I ran the pre-refactor version to get the answer but only commited the refactored version.
How did I fix this bug later when I noticed it? I changed the subtraction operator to a .wrapping_sub method call. This wraps the computation of 0 - 1 to usize::MAX, making the .get method return None and thus fixes the issue.
HaltReason::NeedInput => {
- input = Some(outputs.get(i - 1).copied().unwrap_or(0));
+ input = Some(outputs.get(i.wrapping_sub(1)).copied().unwrap_or(0));
}Solving part two
It turns out that simply connecting the amplifiers in series wasn’t enough: we need to connect them in a loop to truly maximize output. In feedback loop mode, new phase setting numbers are required, from 5 to 9, inclusive. The way feedback loop mode works is that the output of amplifier E is connected to the input of amplifier A, everything else being the same. Amplifiers now take in input multiple times and give output multiple times. The first input is for the phase setting, additional inputs and outputs are all signals. The loop is initiated by giving amplifier A 0 as input for its first input signal. Every amplifier computer must not be restarted after it gives output but must keep running and doing IO until it halts. The amplifiers will all eventually halt and the final output signal is last number that amplifier E outputted.
We iterate over all permutations of phase settings and run the amplifiers for each phase setting like we did for part 1. The thing that really changes for part 2 is how we run the amplifiers. Instead of maintaining a list of output values, we now maintain a list of output queues, which also act as input queues for the next amplifier. We initialize the output queues with the phase setting of the next amplifier so that the first input instruction of the next amplifier can get the phase settings. We then push 0 to amplifier E’s output queue so that amplifier A can get 0 as its first input signal. We then go over all the amplifier computers repeatedly, executing each computer until it either halts or needs input with an empty input queue. We exit this loop of continuous execution when amplifier E halts. At last, we pop amplifier E’s output queue to get the final amplified signal and return it.
// day_7.rs
pub fn part_2(intcode: Intcode) -> eyre::Result<()> {
let mut highest_signal = i32::MIN;
for phase_setting in (5..=9).permutations(5).map(|vec| match vec[..] {
[a, b, c, d, e] => [a, b, c, d, e],
_ => unreachable!(),
}) {
highest_signal = highest_signal.max(run_amplifiers_looped(intcode.clone(), phase_setting)?);
}
println!("{}", highest_signal);
Ok(())
}
fn run_amplifiers_looped(intcode: Intcode, mut phase_setting: [i32; 5]) -> eyre::Result<i32> {
let mut amplifiers = [(); 5].map(|_| intcode.clone());
phase_setting.rotate_left(1);
let mut outputs = phase_setting.map(|phase| Some(phase).into_iter().collect::<VecDeque<_>>());
outputs[4].push_back(0);
'amp_loop: for i in (0..5).cycle() {
let _: usize = i;
loop {
let previous_amp = i.checked_sub(1).unwrap_or(4);
let input = outputs[previous_amp].pop_front();
match amplifiers[i].execute(input)? {
HaltReason::NormalHalt if i == 4 => break 'amp_loop,
HaltReason::NormalHalt => break,
HaltReason::GaveOutput(output) => outputs[i].push_back(output),
HaltReason::NeedInput if outputs[previous_amp].is_empty() => break,
HaltReason::NeedInput => {},
}
}
}
Ok(outputs[4].pop_front().unwrap())
}We are now done with day 7!
Adding all features (day 9)
Day 9 introduces a few new features to the language. After adding these features, our Intcode computer will be complete with no new features needed. Day 9 is supposed to test that these new features work properly.
Relative addressing
There is a new parameter mode, mode 2. Mode 2 is called relative mode and like position mode (mode 0), it represents a position to read values from or write values to. The difference is that the parameter is added with a value called the relative base to get the position. The relative base starts out at 0 and is modified by a new instruction, opcode 9. Opcode 9 takes one parameter as input and adds it to the relative base.
Big integers
We now need to support "large integers". It is not specified how large the integers can be. To fully comply with this rather vague requirement, we could use proper BigInts but since day 9 is supposed to test this feature, anything that successfuly executes the day 9 program should be fine for the rest of the days. I tried out using 64-bit integers and those worked which lets us avoid the added complexity and slowness of using proper BigInts.
Auto-expanding memory
The program can now access memory outside the initial program. This memory can be both written to and read from and is initialized to 0.
One way to implement this would be with a list of memory vectors each representing a different range of (the Intcode computer’s) memory so that we are not wasting (the host computer’s) memory for the gaps between the accessed portions of (the Intcode computer’s) memory. Another way would be to directly expand the memory upto the new position, filling it with 0s. The first approach would be quite complicated and I’m not sure how the performance disadvantage of having indirection for memory access would compare to the performance advantage of using up less memory. This is why I don’t think the first approach isn’t very reasonable and chose the second approach.
Implementing the new features
The feature that is easiest to implement is the big integer one. We just use 64-bit integers instead of the 32-bit integers we were using before. In Rust, that means we switch our types from i32 to i64. We need to change this in both the interpreter and all downstream modules that use the interpreter.
Next up, let’s implement relative addressing. We add a field in our Intcode struct for the relative base. We then implement opcode 9 to update the relative base. We need to update our parameter value function to implement parameter mode 2 too now. Until now, we had been assuming that the output parameters of a function were always in position mode and had hardcoded the position code logic in each opcode. Let’s now create a method that returns the position a parameter refers to by looking at its parameter mode and use that in the logic for each opcode.
Moving on to the expanding memory, since memory can now be accessed from outside the bounds of the memory that is stored, we need to abstract away memory reads and writes into methods that take care of this. For memory reads, we know that uninitialized memory is 0 so we can just return 0 in the case it hits uninitialized memory instead of actually expanding the memory. For writes, we need to actually expand our memory Vec upto the new position.
// runner.rs diff
...
#[derive(Debug, Clone)]
pub struct Intcode {
- pub memory: Vec<i32>,
+ pub memory: Vec<i64>,
current: usize,
+ relative_base: i64,
}
impl FromStr for Intcode {
...
Ok(Self {
memory: vec,
current: 0,
+ relative_base: 0,
})
}
}
...
}
Ok(())
}
- pub fn execute(&mut self, mut input: Option<i32>) -> eyre::Result<HaltReason> {
+ pub fn execute(&mut self, mut input: Option<i64>) -> eyre::Result<HaltReason> {
loop {
- match get_opcode(self.memory[self.current]) {
+ match get_opcode(self.access_memory(self.current)) {
1 => {
let [param_1, param_2] = self.get_param_values::<2>();
- let out_index = self.memory[self.current + 3] as usize;
- self.memory[out_index] = param_1 + param_2;
+ let out_index = self.get_param_value_as_index(3);
+ *self.access_memory_mut(out_index) = param_1 + param_2;
self.current += 4;
}
2 => {
let [param_1, param_2] = self.get_param_values::<2>();
- let out_index = self.memory[self.current + 3] as usize;
- self.memory[out_index] = param_1 * param_2;
+ let out_index = self.get_param_value_as_index(3);
+ *self.access_memory_mut(out_index) = param_1 * param_2;
self.current += 4;
}
3 => {
if let Some(input) = input.take() {
- let out_index = self.memory[self.current + 1] as usize;
- self.memory[out_index] = input;
+ let out_index = self.get_param_value_as_index(1);
+ *self.access_memory_mut(out_index) = input;
self.current += 2;
} else {
break Ok(HaltReason::NeedInput);
...
7 => {
let [param_1, param_2] = self.get_param_values::<2>();
- let out_index = self.memory[self.current + 3] as usize;
- self.memory[out_index] = (param_1 < param_2) as i32;
+ let out_index = self.get_param_value_as_index(3);
+ *self.access_memory_mut(out_index) = (param_1 < param_2) as i64;
self.current += 4;
}
8 => {
let [param_1, param_2] = self.get_param_values::<2>();
- let out_index = self.memory[self.current + 3] as usize;
- self.memory[out_index] = (param_1 == param_2) as i32;
+ let out_index = self.get_param_value_as_index(3);
+ *self.access_memory_mut(out_index) = (param_1 == param_2) as i64;
self.current += 4;
}
+ 9 => {
+ let [param] = self.get_param_values::<1>();
+ self.relative_base += param;
+ self.current += 2;
+ }
+
99 => {
break Ok(HaltReason::NormalHalt);
}
...
}
}
- fn get_param_values<const N: usize>(&self) -> [i32; N] {
+ fn access_memory(&self, index: usize) -> i64 {
+ self.memory.get(index).copied().unwrap_or(0)
+ }
+
+ fn access_memory_mut(&mut self, index: usize) -> &mut i64 {
+ if index >= self.memory.len() {
+ let additional_items = index - self.memory.len() + 1;
+ self.memory
+ .extend(std::iter::repeat(0).take(additional_items));
+ }
+ self.memory.get_mut(index).unwrap()
+ }
+
+ fn get_param_values<const N: usize>(&self) -> [i64; N] {
let mut param_modes = get_param_modes(self.memory[self.current]);
let mut i = self.current;
[(); N]
...
.map(|i| {
let mode = param_modes.next().unwrap();
match mode {
- 0 => self.memory[self.memory[i] as usize],
- 1 => self.memory[i],
+ 0 => self.access_memory(self.access_memory(i) as usize),
+ 1 => self.access_memory(i),
+ 2 => self.access_memory((self.access_memory(i) + self.relative_base) as usize),
other => panic!("Unknown parameter mode {}.", other),
}
})
}
+
+ fn get_param_value_as_index(&self, offset: usize) -> usize {
+ let param_mode = get_param_modes(self.memory[self.current]).nth(offset - 1).unwrap();
+ let i = self.current + offset;
+ match param_mode {
+ 0 => self.access_memory(i) as usize,
+ 2 => (self.access_memory(i) + self.relative_base) as usize,
+ 1 => panic!("Cannot access index when parameter mode is 1."),
+
+ other => panic!("Unknown parameter mode {}.", other),
+ }
+ }
}
#[inline]
-fn get_opcode(num: i32) -> i32 {
+fn get_opcode(num: i64) -> i64 {
num % 100
}
-fn get_param_modes(num: i32) -> impl Iterator<Item = i32> {
+fn get_param_modes(num: i64) -> impl Iterator<Item = i64> {
use std::iter;
iter::repeat(()).scan(num / 100, |state, _| {
let out = *state % 10;
...
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum HaltReason {
- GaveOutput(i32),
+ GaveOutput(i64),
NeedInput,
NormalHalt,
}// day_5.rs diff
...
-fn exec(mut intcode: Intcode, input: i32) -> eyre::Result<()> {
+fn exec(mut intcode: Intcode, input: i64) -> eyre::Result<()> {
let mut input = Some(input);
loop {// day_7.rs diff
...
pub fn part_1(intcode: Intcode) -> eyre::Result<()> {
- let mut highest_signal = i32::MIN;
+ let mut highest_signal = i64::MIN;
for phase_setting in (0..=4).permutations(5).map(|vec| match vec[..] {
[a, b, c, d, e] => [a, b, c, d, e],
_ => unreachable!(),
...
-fn run_amplifiers(intcode: Intcode, phase_setting: [i32; 5]) -> eyre::Result<i32> {
+fn run_amplifiers(intcode: Intcode, phase_setting: [i64; 5]) -> eyre::Result<i64> {
let mut amplifiers = [(); 5].map(|_| intcode.clone());
- let mut outputs = [i32::MIN; 5];
+ let mut outputs = [i64::MIN; 5];
for i in 0..5 {
let mut input = Some(phase_setting[i]);
loop {
...
pub fn part_2(intcode: Intcode) -> eyre::Result<()> {
- let mut highest_signal = i32::MIN;
+ let mut highest_signal = i64::MIN;
for phase_setting in (5..=9).permutations(5).map(|vec| match vec[..] {
[a, b, c, d, e] => [a, b, c, d, e],
...
-fn run_amplifiers_looped(intcode: Intcode, mut phase_setting: [i32; 5]) -> eyre::Result<i32> {
+fn run_amplifiers_looped(intcode: Intcode, mut phase_setting: [i64; 5]) -> eyre::Result<i64> {
let mut amplifiers = [(); 5].map(|_| intcode.clone());
phase_setting.rotate_left(1);Solving part one
Now, we can move on to actually solving day 9 part 1. The program takes one input and gives out one output, just like day 5. We need to run it in test mode for part 1, to see whether the new features work. For this, we need to pass 1 as input.
// day_9.rs
use crate::runner::{HaltReason, Intcode};
use color_eyre::eyre;
pub fn part_1(mut intcode: Intcode) -> eyre::Result<()> {
let mut input = Some(1);
loop {
match intcode.execute(input.take())? {
HaltReason::NormalHalt => break,
HaltReason::GaveOutput(output) => println!("{}", output),
HaltReason::NeedInput => {
eyre::bail!("Invalid second request for input; input already given.")
}
}
}
Ok(())
}Solving part two
Now in part 2, we need to actually run the program, and for that we pass it 2 as input. Since the logic is the same as part 1, we can abstract the execution into a function that takes the number to input as a parameter.
// day_9.rs diff
...
-pub fn part_1(mut intcode: Intcode) -> eyre::Result<()> {
- let mut input = Some(1);
+pub fn part_1(intcode: Intcode) -> eyre::Result<()> {
+ exec(intcode, 1)
+}
+
+pub fn part_2(intcode: Intcode) -> eyre::Result<()> {
+ exec(intcode, 2)
+}
+
+fn exec(mut intcode: Intcode, input: i64) -> eyre::Result<()> {
+ let mut input = Some(input);
loop {
match intcode.execute(input.take())? {
HaltReason::NormalHalt => break,And we have successfully added everything to have a full-featured Intcode interpreter and will not have to change anything further in our interpreter[2].
Multiplexed IO (day 11)
For this day, we are controlling a robot that paints the hull of a spaceship. The robot works by first taking in as input the color of the tile (1 for white and 0 for black) it is currently on. Then it outputs 2 values, the color to paint that tile (same encoding as the input) and then the direction to turn (0 for 90 degrees left and 1 for 90 degrees right). The robot will continue running like this until it halts, which happens when it has finished painting. The robot always moves forward by one tile after turning and starts out pointing up. The hull starts out as all black.
Solving part one
For part one, we need to “implement” this hull painting robot and count how many tiles were painted atleast once.
To run the robot, we need to keep track of which tile has been painted with what color and also the current position and direction of the robot. Since the hull is unbounded and starts out as all black, a reasonable way to represent it is using a set that only keeps track of the white tiles. For the current position and direction, we can use a tuple of two signed integers, the direction being represented as one of { (-1, 0), (1, 0), (0, -1), (0, 1) } for the four cardinal directions on the xy plane. To get the answer, we also need to keep track of the tiles the robot has painted. For this, we can also use a set to get rid of positions that we paint twice.
For the main control loop, we need to execute the Intcode in a loop while handling IO, breaking the loop when the Intcode computer halts. The input case is simple, we give it the color of the current position in all cases. The output case where we receive the color to paint and the direction to turn is more complicated but not too bad as we need to update our state variables and also need to execute it again after we receive the first output to get the second output. After we get the color, we update the hull map and the set of visited tiles. Then we execute the Intcode computer to get the direction making sure that we error out if we don’t get an output. After getting the direction, we update the current direction and the current position and let the execution loop continue.
// day_11.rs
use crate::runner::{HaltReason, Intcode};
use color_eyre::eyre;
use eyre::bail;
use std::collections::HashSet;
pub fn part_1(mut intcode: Intcode) -> eyre::Result<()> {
let mut map = HashSet::<(i32, i32)>::new();
let mut visited_nodes = HashSet::<(i32, i32)>::new();
let mut current_position = (0, 0);
visited_nodes.insert(current_position);
let mut current_orientation = (0, -1);
let mut input = None;
loop {
match intcode.execute(input.take())? {
HaltReason::NormalHalt => break,
HaltReason::GaveOutput(color) => {
if color == 1 {
map.insert(current_position);
} else {
map.remove(¤t_position);
}
visited_nodes.insert(current_position);
let turn = match intcode.execute(None)? {
HaltReason::GaveOutput(output) => output,
HaltReason::NormalHalt => bail!("Unexpected halt. Expected to recieve turn."),
HaltReason::NeedInput => {
bail!("Unexpected request for input. Expected to recieve turn.")
}
};
if turn == 0 {
current_orientation = turn_left(current_orientation);
} else {
current_orientation = turn_right(current_orientation);
}
current_position = add(current_orientation, current_position);
}
HaltReason::NeedInput => {
input = Some(if map.contains(¤t_position) {
1
} else {
0
});
}
}
}
println!("{}", visited_nodes.len());
Ok(())
}
fn add(a: (i32, i32), b: (i32, i32)) -> (i32, i32) {
(a.0 + b.0, a.1 + b.1)
}
fn turn_left((x, y): (i32, i32)) -> (i32, i32) {
(y, -x)
}
fn turn_right((x, y): (i32, i32)) -> (i32, i32) {
(-y, x)
}Solving part two
The robot failed to print out the code it was supposed to on the hull because it was supposed to start out on a white tile instead of a black tile. The remaining tiles are still black. We now need to find out the code the robot will print now that we have fixed the starting tile color.
This needs basically the same code as part one but we now need a way to print what was painted on the hull and also can get rid of the code that tracks the visited tiles.
// day_11.rs
pub fn part_2(mut intcode: Intcode) -> eyre::Result<()> {
let mut map = HashSet::<(i32, i32)>::new();
let mut current_position = (0, 0);
map.insert(current_position);
let mut current_orientation = (0, -1);
let mut input = None;
loop {
match intcode.execute(input.take())? {
HaltReason::NormalHalt => break,
HaltReason::GaveOutput(color) => {
if color == 1 {
map.insert(current_position);
} else {
map.remove(¤t_position);
}
let turn = match intcode.execute(None)? {
HaltReason::GaveOutput(output) => output,
HaltReason::NormalHalt => bail!("Unexpected halt. Expected to recieve turn."),
HaltReason::NeedInput => {
bail!("Unexpected request for input. Expected to recieve turn.")
}
};
if turn == 0 {
current_orientation = turn_left(current_orientation);
} else {
current_orientation = turn_right(current_orientation);
}
current_position = add(current_orientation, current_position);
}
HaltReason::NeedInput => {
input = Some(if map.contains(¤t_position) {
1
} else {
0
});
}
}
}
print_map(&map);
Ok(())
}
fn print_map(map: &HashSet<(i32, i32)>) {
let min_x = map.iter().copied().min_by_key(|&(x, _y)| x).unwrap().0;
let max_x = map.iter().copied().max_by_key(|&(x, _y)| x).unwrap().0;
let min_y = map.iter().copied().min_by_key(|&(_x, y)| y).unwrap().1;
let max_y = map.iter().copied().max_by_key(|&(_x, y)| y).unwrap().1;
for y in min_y..=max_y {
for x in min_x..=max_x {
let ch = if map.contains(&(x, y)) { '█' } else { ' ' };
print!("{}", ch);
}
println!()
}
}This is what happens when we run part two’s solution:
fish > cargo run -- d11p2 inputs/day11
Compiling intcode v0.1.0 (/data/coding/aoc_2019_intcode)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.14s
Running `target/debug/intcode d11p2 inputs/day11`
Reading from "inputs/day11" ...
Successfully read and parsed intcode.
████ ██ ███ ███ ██ ████ ██ █ █
█ █ █ █ █ █ █ █ █ █ █ █ █
███ █ █ █ █ ███ █ ███ █ ██
█ ████ ███ █ █ █ █ █ █ █
█ █ █ █ █ █ █ █ █ █ █ █ █ █
█ █ █ █ █ ███ ██ █ ██ █ █And we are done with day 11!
Automated interactive gaming (day 13)
This time, we need to build an arcade cabinet that runs a given program (the game). In part one, we simply render a single frame of the game, while in part two, we have to write a program to play the game.
The way this arcade cabinet works is that the program outputs 3 numbers for each tile that it wants to display on the screen. The first number is the tile’s x-coordinate, the second the tile’s y-coordinate and the third is the type of tile to display at that location.
The tile type works like this:
- 0 is an empty tile. No game object appears in this tile.
- 1 is a wall tile. Walls are indestructible barriers.
- 2 is a block tile. Blocks can be broken by the ball.
- 3 is a horizontal paddle tile. The paddle is indestructible.
- 4 is a ball tile. The ball moves diagonally and bounces off objects.
Solving part one
For part one, we need to run the program and count the number of block tiles that are outputted. When we run our Intcode program and receive an output, we then continue the program again in the GaveOutput branch until we get two more outputs so that we have all three numbers representing a tile together. We then store the x and y coordinates of each block tile in a hashset so that we can deal with duplicate tiles and then our answer at the end is the length of this hashset.
// day_13.rs
pub fn part_1(mut intcode: Intcode) -> eyre::Result<()> {
let mut block_tiles = HashSet::<(i64, i64)>::new();
loop {
match intcode.execute(None)? {
HaltReason::NormalHalt => break,
HaltReason::GaveOutput(x) => {
let y = match intcode.execute(None)? {
HaltReason::GaveOutput(output) => output,
HaltReason::NormalHalt => {
bail!("Unexpected halt. Expected to recieve y coordinate.")
}
HaltReason::NeedInput => {
bail!("Unexpected request for input. Expected to recieve y coordinate.")
}
};
let tile_id = match intcode.execute(None)? {
HaltReason::GaveOutput(output) => output,
HaltReason::NormalHalt => {
bail!("Unexpected halt. Expected to recieve tile id.")
}
HaltReason::NeedInput => {
bail!("Unexpected request for input. Expected to recieve tile id.")
}
};
if tile_id == 2 {
block_tiles.insert((x, y));
}
}
HaltReason::NeedInput => bail!("Unexpected request for input."),
}
}
println!("{}", block_tiles.len());
Ok(())
}Solving part two
Now, we need to play this game. There are a few new things we need to change for this:
- We need to “put coins” in the machine by changing the value of memory address 0 to 2.
- Input to a joystick that controls the paddle should now be given using the input commands to the program. -1 means that we move the paddle left, 0 means we don’t move the paddle and 1 means that we move it right.
- The program now also outputs the current score of the game. When the x-coordinate is -1 and the y-coordinate is 0, the third number represents the score instead of the tile type.
This also has to be automated because it took way too long to play manually when I tried (Solving this by playing it manually would be a fun challenge though!). The puzzle answer is the final score of the program before it exits.
The way this game works is that the diagonally-moving ball bounces off the walls and the paddle and destroys blocks. To win the game you need to destroy all the blocks. The game ends early when the ball falls out of the bottom of the board without bouncing off of the paddle. This should become clearer with a picture of a frame of the game:
██████████████████████████████████████
█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█
█░▓▓░▓░░░▓▓░░▓▓▓░▓▓▓░░▓▓▓░▓▓▓░░░▓▓▓▓░█
█░▓░▓▓▓▓▓▓░▓░░░▓░▓▓▓▓▓░▓░░▓▓▓▓▓░░▓░░░█
█░▓▓▓░▓▓▓▓░░▓▓▓▓░▓▓▓▓░▓▓░░▓░░░▓░░▓▓░░█
█░░░░▓▓░░▓░▓░▓░░▓▓▓▓░▓▓░░▓░░▓▓▓▓▓░░░░█
█░▓▓▓▓▓▓▓░▓▓▓░▓▓░░░▓▓░░▓░▓▓░░▓▓▓░▓▓▓░█
█░▓░░▓▓▓░░░░░░░░▓▓░░▓▓░▓▓░▓▓▓░░░▓░░░░█
█░▓▓░▓▓░▓▓▓░░▓░▓▓░░░▓░▓░▓▓▓░░▓▓░▓▓▓▓░█
█░░░░▓▓▓░▓▓▓░▓▓▓░░▓▓░░▓▓▓▓░▓▓░░▓░░▓░░█
█░▓▓▓▓▓▓▓▓░▓░░░░▓▓░░░░░░░▓▓▓░▓░▓▓▓░▓░█
█░▓░░▓▓░▓▓░▓▓░▓▓▓░░▓▓▓░░▓░▓▓▓░▓▓░░░░░█
█░▓▓▓░▓▓▓▓▓░▓░▓▓▓▓▓▓▓▓░░░▓░▓░▓▓▓▓▓▓░░█
█░▓░▓░░░▓▓░▓░░▓▓░░░░▓▓░▓▓▓░░▓░▓░▓░▓░░█
█░░▓▓▓░░▓░░▓░▓▓░▓░▓▓░░░░░░▓░░▓▓░▓▓▓▓░█
█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█
█░░░░░░░░░░░░░░░░o░░░░░░░░░░░░░░░░░░░█
█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█
█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█
█░░░░░░░░░░░░░░░░░░—░░░░░░░░░░░░░░░░░█
█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█
Score: 0The logic for how to move the paddle turns out to be very simple: Move it right when the ball is right of the paddle, move it left when it is left and keep it stationary when the x coordinates are equal. Given that the ball doesn’t move by more than one square in the x direction per frame and same for the paddle, this can be represented with joystick input = (ball x) - (paddle x).
I created a Tile enum to represent the different tile types. This is better than having magic numbers in our code and makes it more readable. Next, I changed the board representation from a set of all the block tile positions to a map of positions to tile type. The main loop also needs to be modified to pass input of which direction to move the paddle and to update the score when it is provided.
For visualization purposes, I print out the board and the score each time input is requested. There is also a delay associated with each frame printed out to the terminal. This delay is proportional to the number of times within the last 32 frames the ball was close to a block tile. This makes the visualization faster when the ball is going through empty space and slower when it is near the tiles. This is done using a u32 that acts as a list of 32 bools using bitwise arithmetic.
I also made a recording of this visualization.
// day_13.rs new additions
pub fn part_2(mut intcode: Intcode) -> eyre::Result<()> {
intcode.memory[0] = 2;
let mut tiles = HashMap::<(i64, i64), Tile>::new();
let mut score = i64::MIN;
let mut input = None;
let mut last_32_moves: u32 = u32::MAX;
loop {
match intcode.execute(input.take())? {
HaltReason::NormalHalt => break,
HaltReason::GaveOutput(x) => {
let y = match intcode.execute(None)? {
HaltReason::GaveOutput(output) => output,
HaltReason::NormalHalt => {
bail!("Unexpected halt. Expected to recieve y coordinate.")
}
HaltReason::NeedInput => {
bail!("Unexpected request for input. Expected to recieve y coordinate.")
}
};
let tile_id = match intcode.execute(None)? {
HaltReason::GaveOutput(output) => output,
HaltReason::NormalHalt => {
bail!("Unexpected halt. Expected to recieve tile id.")
}
HaltReason::NeedInput => {
bail!("Unexpected request for input. Expected to recieve tile id.")
}
};
if let (-1, 0) = (x, y) {
score = tile_id;
} else {
let tile =
Tile::from_id(tile_id).ok_or(eyre!("Invalid tile id {}", tile_id))?;
tiles.insert((x, y), tile);
}
}
HaltReason::NeedInput => {
use std::{thread::sleep, time::Duration};
display_tiles(&tiles);
println!("Score: {}", score);
sleep(Duration::from_millis(
(4_u64 * (last_32_moves.count_ones() as u64)).max(8),
));
let paddle_x = tiles
.iter()
.find(|&(_coord, &tile)| tile == Tile::Paddle)
.map(|(&(x, _y), _tile)| x)
.unwrap();
let (ball_x, ball_y) = tiles
.iter()
.find(|&(_coord, &tile)| tile == Tile::Ball)
.map(|(&coord, _tile)| coord)
.unwrap();
last_32_moves <<= 1;
last_32_moves |= is_near_block(&tiles, (ball_x, ball_y)) as u32;
input = Some(ball_x - paddle_x);
}
}
}
display_tiles(&tiles);
println!("Final score: {}", score);
Ok(())
}
fn is_near_block(map: &HashMap<(i64, i64), Tile>, (x, y): (i64, i64)) -> bool {
let neighbors = [
(x + 1, y + 1),
(x + 1, y - 1),
(x - 1, y - 1),
(x - 1, y + 1),
(x + 1, y),
(x, y - 1),
(x - 1, y),
(x, y + 1),
];
neighbors
.iter()
.filter_map(|coord| map.get(coord))
.any(|&tile| tile == Tile::Block)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Tile {
Empty,
Wall,
Block,
Paddle,
Ball,
}
impl Default for Tile {
fn default() -> Self {
Self::Empty
}
}
impl Tile {
fn from_id(id: i64) -> Option<Self> {
Some(match id {
0 => Tile::Empty,
1 => Tile::Wall,
2 => Tile::Block,
3 => Tile::Paddle,
4 => Tile::Ball,
_ => None?,
})
}
fn to_char(self) -> char {
use Tile::*;
match self {
Empty => '░',
Wall => '█',
Block => '▓',
Paddle => '—',
Ball => 'o',
}
}
}
fn display_tiles(tiles: &HashMap<(i64, i64), Tile>) {
let min_x = tiles.keys().copied().min_by_key(|&(x, _y)| x).unwrap().0;
let max_x = tiles.keys().copied().max_by_key(|&(x, _y)| x).unwrap().0;
let min_y = tiles.keys().copied().min_by_key(|&(_x, y)| y).unwrap().1;
let max_y = tiles.keys().copied().max_by_key(|&(_x, y)| y).unwrap().1;
for y in min_y..=max_y {
for x in min_x..=max_x {
let tile = tiles.get(&(x, y)).copied().unwrap_or_default();
print!("{}", tile.to_char());
}
println!()
}
}Grid pathfinding (day 15)
On this day, we need to fix a oxygen system using a remote controlled robot that is on a 2D grid. The robot accepts commands that tell it to move in one of the 4 cardinal directions and reports a status. We don’t know anything about the state of the grid until we explore it by giving the robot commands.
The intcode program, in a loop, takes as input a movement command and outputs the status of trying to move according to the command.
The movement commands are:
- [1] North
- [2] South
- [3] West
- [4] East
The status responses are:
- [0] The robot hit a wall. Its position did not change.
- [1] The robot successfully moved one square in the direction specified. Its position gets updated.
- [2] Same as 1, but the robot has reached the oxygen system after moving.
Solving part one
For part one, we need to find out the minimum number of movement commands needed to reach the oxygen system from the start position. Since it asks us for the minimum number of steps, breadth-first-search (BFS) sounds appropriate for this. This is because BFS explores all the squares at a certain depth from the start position before moving on to the squares at the next depth, meaning that with equal movement costs (a constant 1 in this case), it will give the shortest path to the target.
The core of how BFS works is by keeping track of a frontier queue along with already visited squares. This frontier starts out containing only the start element and is expanded by removing a current element from the front of the queue and adding all of current’s unvisited neighbors to the back of the queue. For an in-depth explanation of how BFS works, I recommend this webpage written by Amit Patel.
One caveat of using BFS for this puzzle is that since we can only move the robot one step at a time, we need to backtrack to the start and only then can we go to the new spot from the frontier queue. This is somewhat inefficient as moving the robot requires running the Intcode computer for each step. One way to minimize this backtracking to the start and then moving to another node that is probably close to it on the grid would be to use depth-first-search (DFS) to explore the grid and know where all the walls are and also where the oxygen system is. Then, we could do BFS on our in-memory representation of the grid to find the shortest path. DFS would minimize robot movement as it would explore the entire path in a certain direction and only then backtrack to explore the rest of the grid.
In addition to the frontier and the visited nodes, in my solution I also keep track of 4 more things: the cost to reach any given position to get the answer, whether a given point is a wall or not for minor efficiency gains, the movement needed to get to any point alongwith the previous point associated with that point for backtracking, and the last point removed from the frontier.
I use a VecDeque for the frontier as it gives efficient access to both ends of the list, unlike a Vec where popping from the front of the list requires moving the rest of the elements back one position one by one. For the other things that I keep track of, since it’s a mapping from explored point to some data about that point, a HashMap is the obvious choice.
Actually running the Intcode computer is very straightforward in this day as it takes in inputs in a loop and each input corresponds to a single output.
After popping the point from the frontier’s front, we need to do two things: 1) backtrack from the last accessed point to the start point; 2) go from the start point to the current point that we popped from the frontier. Since I create a mapping of each point to the movement needed to reach that point alongwith the previous point from where that movement was taken, we can query this hashmap in a loop starting from the last accessed point, then the point before that and so on to do the backtracking. Then, for the retracking to the new point, we need to do the same from the new point, but now in reverse. For this I used a stack to keep track of list of movements and then iterated on that in LIFO (last-in first-out) order to reach the new point.
One optimization I can think of while writing this article for the process described in the previous paragraph is to find a common ancestor of both nodes and backtrack only till that common ancestor.
Then, for each movement we can perform after we have gone to the current point, we:
- Skip trying to move there if we have encountered that square before and know it is a wall
Try making the movement and check the status:
// day_15.rs
use crate::runner::{HaltReason, Intcode};
use color_eyre::eyre;
use eyre::bail;
use std::collections::{HashMap, VecDeque};
use std::ops::Not;
pub fn part_1(mut intcode: Intcode) -> eyre::Result<()> {
let mut map = HashMap::<(i64, i64), bool>::new();
let mut sources = HashMap::<(i64, i64), Option<((i64, i64), Movement)>>::new();
let mut costs = HashMap::<(i64, i64), u64>::new();
let mut frontier = VecDeque::new();
map.insert((0, 0), false);
costs.insert((0, 0), 0);
sources.insert((0, 0), None);
frontier.push_back((0, 0));
let mut last_accessed = (0, 0);
while frontier.is_empty().not() {
let to_access = frontier.pop_front().unwrap();
while let Some((source_coord, movement)) = sources[&last_accessed] {
make_movement(&mut intcode, movement.reverse())?;
last_accessed = source_coord;
}
let mut source_stack = Vec::new();
let mut to_access_parent = to_access;
while let Some((source_coord, movement)) = sources[&to_access_parent] {
source_stack.push(movement);
to_access_parent = source_coord;
}
for movement in source_stack.into_iter().rev() {
make_movement(&mut intcode, movement)?;
}
last_accessed = to_access;
for movement in Movement::all() {
let new_coord = add_movement(to_access, movement);
if let Some(true) = map.get(&new_coord) {
continue;
}
let new_cost = costs[&to_access] + 1;
match make_movement(&mut intcode, movement)? {
0 => {
map.insert(new_coord, true);
}
1 => {
if costs.contains_key(&new_coord).not() {
costs.insert(new_coord, new_cost);
map.insert(new_coord, false);
frontier.push_back(new_coord);
sources.insert(new_coord, Some((to_access, movement)));
}
make_movement(&mut intcode, movement.reverse())?;
}
2 => {
println!("Distance to oxygen system: {}", new_cost);
return Ok(());
}
other @ _ => bail!("Unexpected status {} given by intcode program", other),
};
}
}
bail!("Oxygen system not found")
}
fn make_movement(intcode: &mut Intcode, movement: Movement) -> eyre::Result<i64> {
match intcode.execute(Some(movement.to_int()))? {
HaltReason::GaveOutput(output) => Ok(output),
HaltReason::NormalHalt => bail!("Unexpected halt of intcode program."),
HaltReason::NeedInput => bail!("Invalid request of input, input already given."),
}
}
fn add_movement((x, y): (i64, i64), movement: Movement) -> (i64, i64) {
use Movement::*;
match movement {
North => (x, y - 1),
South => (x, y + 1),
West => (x - 1, y),
East => (x + 1, y),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Movement {
North,
South,
East,
West,
}
impl Movement {
fn to_int(self) -> i64 {
use Movement::*;
match self {
North => 1,
South => 2,
West => 3,
East => 4,
}
}
fn all() -> [Self; 4] {
use Movement::*;
[North, South, East, West]
}
fn reverse(self) -> Self {
use Movement::*;
match self {
North => South,
South => North,
East => West,
West => East,
}
}
}Solving part two
Now, we need to find out how long it takes for the oxygen system to fill the entire area of open spaces with oxygen. Each square that has oxygen spreads it to each neighboring square (no diagonals) in one minute. At minute 0, only the oxygen system square has oxygen in it.
To solve this, we have to traverse the grid twice: once to find the oxygen system, and then once to find the max depth of the grid’s open squares as seen from the oxygen system. For the second, once again we need BFS as DFS could lead to extra counting if there is a loop in the graph.
My code is similar to the first part, but now we need to restart the BFS once we find the oxygen system with its position as the new starting point. Another change I did apart from implementing part 2 is that since we need to do the whole backtracking and retracking business again, I abstracted that out into a function.
while frontier.is_empty().not() {
let to_access = frontier.pop_front().unwrap();
-
- while let Some((source_coord, movement)) = sources[&last_accessed] {
- make_movement(&mut intcode, movement.reverse())?;
- last_accessed = source_coord;
- }
-
- let mut source_stack = Vec::new();
- let mut to_access_parent = to_access;
- while let Some((source_coord, movement)) = sources[&to_access_parent] {
- source_stack.push(movement);
- to_access_parent = source_coord;
- }
- for movement in source_stack.into_iter().rev() {
- make_movement(&mut intcode, movement)?;
- }
+ move_to_new_coord(&mut intcode, &sources, last_accessed, to_access)?;
last_accessed = to_access;
...
+fn move_to_new_coord(
+ intcode: &mut Intcode,
+ sources: &HashMap<(i64, i64), Option<((i64, i64), Movement)>>,
+ mut last_accessed: (i64, i64),
+ to_access: (i64, i64),
+) -> eyre::Result<()> {
+ while let Some((source_coord, movement)) = sources[&last_accessed] {
+ make_movement(intcode, movement.reverse())?;
+ last_accessed = source_coord;
+ }
+
+ let mut source_stack = Vec::new();
+ let mut to_access_parent = to_access;
+ while let Some((source_coord, movement)) = sources[&to_access_parent] {
+ source_stack.push(movement);
+ to_access_parent = source_coord;
+ }
+ for movement in source_stack.into_iter().rev() {
+ make_movement(intcode, movement)?;
+ }
+ Ok(())
+}
+
fn make_movement(intcode: &mut Intcode, movement: Movement) -> eyre::Result<i64> {// day_15.rs new additions
pub fn part_2(mut intcode: Intcode) -> eyre::Result<()> {
let mut map = HashMap::<(i64, i64), bool>::new();
let mut sources = HashMap::<(i64, i64), Option<((i64, i64), Movement)>>::new();
let mut frontier = VecDeque::new();
let mut oxygen_system_distances = HashMap::<(i64, i64), u64>::new();
map.insert((0, 0), false);
sources.insert((0, 0), None);
frontier.push_back((0, 0));
let mut last_accessed = (0, 0);
let mut oxygen_system = None;
'outer: while frontier.is_empty().not() {
let to_access = frontier.pop_front().unwrap();
move_to_new_coord(&mut intcode, &sources, last_accessed, to_access)?;
last_accessed = to_access;
for movement in Movement::all() {
let new_coord = add_movement(to_access, movement);
if let Some(true) = map.get(&new_coord) {
continue;
}
let status = make_movement(&mut intcode, movement)?;
match status {
0 => {
map.insert(new_coord, true);
}
1 | 2 => {
if sources.contains_key(&new_coord).not() {
map.insert(new_coord, false);
frontier.push_back(new_coord);
sources.insert(new_coord, Some((to_access, movement)));
if oxygen_system.is_some() {
let new_distance = oxygen_system_distances[&to_access] + 1;
oxygen_system_distances.insert(new_coord, new_distance);
}
}
make_movement(&mut intcode, movement.reverse())?; // C
if status == 2 && oxygen_system.is_none() {
oxygen_system = Some(new_coord);
frontier.drain(..);
sources.drain();
map.drain();
frontier.push_back(new_coord);
oxygen_system_distances.insert(new_coord, 1); // A
sources.insert(new_coord, None);
map.insert(new_coord, false);
last_accessed = new_coord; // B
continue 'outer;
}
}
other @ _ => bail!("Unexpected status {} given by intcode program", other),
};
}
}
println!(
"Time taken to fill area with oxygen is {} minutes.",
oxygen_system_distances.values().copied().max().unwrap()
);
Ok(())
}The astute among you might have noticed a problem: In the line marked as A in the above code block, I initialize the oxygen system’s depth as 1 but logically it should be zero as the problem specifies that it takes 0 minutes for oxygen to reach the oxygen system. This isn’t directly compensated somehow in other parts of the code either. However, even with this off-by-one bug, this code manages to produce the correct result. How?!
This is the same question I asked when I was reviewing this code when writing this article. Around an hour of head-scratching and code-staring later, I found the answer: There is a second bug in this code that ends up compensating for the off-by-one for certain grids. Note that when I restart the BFS, I assume that robot’s position is at the oxygen system (See the line marked as B). However this assumption is wrong as on the line marked as C, I change the position of the robot back so that the next movement in the inner loop starts at the current point and not the place where the last movement ended. Thus, although the second BFS’s coordinates are relative to the position of the oxygen system, the actual traversal happens relative to the predecessor of the oxygen system.
These bugs ended up cancelling each other out in the input I got but could have led to a off-by-two for certain grids. And the best part is that I only discovered this some 4.5 years after writing the code! I think this is a good place to remind everyone that bugs can always exist in code no matter how carefully you write it and in very surprising ways – such is the nature of software.
ASCII IO (day 17)
On this day, we are remote-controlling a robot again. This time, we have a camera feed of the robot, which is on some scaffolding. The Intcode program representing the remote control gives its output in ASCII encoding and in part 2, takes in ASCII input too.
The program outputs an ASCII map. This map represents a path that bends at 90 degree angles and can intersect itself. In the camera feed output, a # represents the path, a . represents blank space, and one of {^, v, <, >} represent the robot when it’s on the path, the particular character showing the orientation of the robot. A X represents a robot that has gone off the map and is falling.
Solving part one
We need to find all the points where the path intersects itself and then at each such point, we compute the product of the x and y coordinate and then the final answer is the sum of all those products.
First, we need to collect the output into a single string and then assemble a mapping from coordinates to squares. Then, we need to identify intersections in that mapping. Looking at the map, we see that these points are # characters that have all four neighbors also be # characters. Then, we compute the sum of products required.
// day_17.rs (new file)
use crate::runner::{HaltReason, Intcode};
use color_eyre::eyre;
use std::collections::{HashMap, HashSet, VecDeque};
use std::ops::Not;
pub fn part_1(mut intcode: Intcode) -> eyre::Result<()> {
let mut output = String::new();
loop {
match intcode.execute(None)? {
HaltReason::NormalHalt => break,
HaltReason::GaveOutput(int) => {
let ch = char::from(int as u8);
output.push(ch);
}
HaltReason::NeedInput => {
eyre::bail!("Invalid request for input from intcode program.")
}
}
}
let width = output.trim().lines().next().unwrap().len();
let map = output
.chars()
.filter(|c| ['.', '#', '^', 'v', '>', '<'].contains(c))
.enumerate()
.map(|(i, c)| (((i % width) as i32, (i / width) as i32), c != '.'))
.collect::<HashMap<_, _>>();
let mut alignment_parameter_sum = 0;
for (&(x, y), &c) in map.iter() {
if c {
let neighbors = [(x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)];
if neighbors
.into_iter()
.all(|neighbor| map.get(&neighbor) == Some(&true))
{
alignment_parameter_sum += x * y;
}
}
}
println!(
"The sum of the alignment parameters is {}.",
alignment_parameter_sum
);
Ok(())
}Solving part two
Now, we need to visit each part of the path on the grid. To do this, first we make the robot accept commands by changing the contents of memory address 0 to 2. Now, after printing the grid, the intcode progam will prompt for inputs. Each prompt consists of some text (in ASCII chars, outputted one-by-one), followed by a newline, followed by input instructions. The program stops requesting input once a newline is given as input. It then moves on to the next prompt. The grid is separated from the first prompt by a blank line (a newline where the previous char outputted was also a newline).
The program will prompt 5 times. In order, these prompts are:
- The list of movement functions to run called the main movement routine.
- Movement function A
- Movement function B
- Movement function C
- Whether to enable continuous display of the robot’s movement (y/n)
The main movement routine and the movement functions are all comma-separated lists limited to 20 characters (not counting the terminating newline). The main movement routine is a list of movement functions to run in order and each list item must be either A, B, or C. For the movement functions, there are three commands we can provide as the list item: a) L to turn the robot 90 degrees counterclockwise in place; b) R to turn it 90 degrees clockwise; c) A decimal integer specifying the number to squares to move forward in the robot’s current orientation.
Answering y to the continuous display prompt will make the intcode program output the grid with the current robot position and orientation after each command run, while answering n will disable this feature.
After the robot successfully traverses the entire path, it will output a single large number outside the ASCII range which is the puzzle answer before halting.
The solution can be split into three sections:
- Creating a map of the grid and finding out the initial robot position and orientation
- Finding a list of commands that would traverse the entire path
- Splitting this list of commands into a list of movement functions
For the first section, the way I chose to represent the path is a hashmap of coordinates to a list of directions the path goes in from that point. This particular representation was chosen because it maps well to the way we traverse the grid in section two. Since there are four directions we can move in, each square has 5 possibilites based on the number of directions the path goes in from that square:
- 0 directions: Isolated from the path completely, probably a blank square.
- 1 direction: Terminal point of the path, either the start or the end. Could also be a blank square neighboring the path.
- 2 directions: Non-intersection point in the middle of the path. Could also be a blank square neighboring the path.
- 3 directions: Would represent a branching point in the path, which we assume is not possible. Could also be a blank square neighboring the path.
- 4 directions: A point where the path intersects itself.
For the second section, we see if the current orientation of the robot is included in the map entry of the current position of the robot. If it is, we update the position of the robot and then either modify increment the last command if it was a move forward command or push a move forward by one command. This handles the straight line sections of the path and also moves us forward through intersections. The other case is that we cannot move forward, which means that there is a bend in the middle of the path and we have to turn either left or right before moving forward depending on the directions the path goes in at that point. We continue doing this in a loop until we reach the end point of the path.
In the third section, we need to split this list of commands into three movement functions. This could realistically be done by hand in less than 2 hours by my rough estimate given that my input path requires 74 commands to be given. However, that would be boring, tedious and arduous. The length limit of 20 characters on the functions mean that each function could have a maximum of 10 commands accounting for the comma-separation. I take a greedy approach to finding repeated common segments of the path. I start out by marking each command as being part of a function or not with this marking starting out as all unmarked. Then, three times, I do the following, one time for each function.
- Find the first unmarked stretch of commands.
Consider patterns starting from the start of this unmarked stretch, with a few constraints:
- The length of the pattern must be between 6 and 10, both inclusive.
- The length of the pattern must be less than the length of the unmarked stretch
- Choosing this pattern for the function must not leave an unmarked stretch of length less than 6 after the pattern
- The pattern must appear atleast 3 times in the command list.
- All the instances of the largest such pattern are marked with a function.
Then, we ensure that each command got marked, exiting the program with an error if it didn’t. Then, to convert this marking into a list of functions to execute, we convert each chain of by removing consecutive equal consecutive function markers from the marking list using the handy standard library .dedup method.
Now, with our main movement routine and movement functions created, we can run the intcode program to get our answer.
While this solution works and solves the puzzle, it is not fully general and make the following rather dissatisfying assumptions. The arbitrary constraints in the third section, which were probably the result of trial and error are especially dissatisfying.
- The path is linear with no branching that would require backtracking
- Each intersection doesn’t require us to choose between different directions, and we can continue in the same direction when we encounter an intersection
- The path doesn’t loop back to the start square
- The path will not require executing the same movement function twice in a row
- Splitting the command list into functions doesn’t require splitting a long move forward command into multiple commands
- Arbitrary constraints are applied when splitting the path commands into movement functions
// day_17.rs new additions
pub fn part_2(mut intcode: Intcode) -> eyre::Result<()> {
intcode.memory[0] = 2;
let mut last_character = '\0';
let mut output = String::new();
loop {
match intcode.execute(None)? {
HaltReason::NormalHalt => eyre::bail!("Unexpected halt of intcode program."),
HaltReason::NeedInput => eyre::bail!("Invalid request for input."),
HaltReason::GaveOutput(int) => {
let ch = char::from(int as u8);
output.push(ch);
if ch == '\n' && last_character == '\n' {
break;
}
last_character = ch;
}
}
}
let (map, mut robot_coord, mut robot_orientation) = make_map(output);
let end_coord = map
.iter()
.find(|(coord, vec)| vec.len() == 1 && **coord != robot_coord)
.map(|(coord, _vec)| coord)
.copied()
.unwrap();
let mut instructions = Vec::<Instruction>::new();
while robot_coord != end_coord {
if map[&robot_coord].contains(&robot_orientation) {
robot_coord = add_orientation(robot_coord, robot_orientation);
if instructions
.last()
.map(|instruction| matches!(instruction, Instruction::Forward(_)).not())
.unwrap_or(false)
{
instructions.push(Instruction::Forward(0));
}
match instructions.last_mut().unwrap() {
Instruction::Forward(ref mut x) => {
*x += 1;
}
_ => unreachable!(),
}
} else {
let final_orientation = map[&robot_coord]
.iter()
.find(|&&orientation| orientation != robot_orientation.reverse())
.copied()
.unwrap();
if robot_orientation.left() == final_orientation {
robot_orientation = final_orientation;
instructions.push(Instruction::Left);
} else if robot_orientation.right() == final_orientation {
robot_orientation = final_orientation;
instructions.push(Instruction::Right);
} else {
eyre::bail!("Broken input.");
}
}
}
let mut marking = vec![None; instructions.len()];
let a = mark_common(&instructions, &mut marking, 'A')?;
let b = mark_common(&instructions, &mut marking, 'B')?;
let c = mark_common(&instructions, &mut marking, 'C')?;
eyre::ensure!(
marking.iter().all(Option::is_some),
"Could not mark all instructions into movement functions."
);
marking.dedup();
let main_routine = marking
.iter()
.copied()
.map(Option::unwrap)
.flat_map(|ch| [',', ch])
.skip(1)
.chain(Some('\n'))
.map(|ch| ch as u8);
let function_a = format_functions(a);
let function_b = format_functions(b);
let function_c = format_functions(c);
let interactive_display = [b'n', b'\n'];
let mut input_queue = main_routine
.chain(function_a)
.chain(function_b)
.chain(function_c)
.chain(interactive_display)
.collect::<VecDeque<_>>();
let mut last_output = 0;
let mut input = None;
loop {
match intcode.execute(input.take())? {
HaltReason::NormalHalt => {
break;
}
HaltReason::NeedInput => {
if input_queue.is_empty() {
eyre::bail!("No input left!")
}
let first_element = input_queue.pop_front();
input = first_element.map(i64::from);
print!("{}", first_element.unwrap() as char);
}
HaltReason::GaveOutput(int) => {
if let Ok(ch) = u8::try_from(int) {
print!("{}", ch as char);
}
last_output = int;
}
}
}
println!("Total dust collected by vaccum robot: {}", last_output);
Ok(())
}
fn format_functions(function: &[Instruction]) -> impl Iterator<Item = u8> + '_ {
function
.iter()
.copied()
.flat_map(|instruction| match instruction {
Instruction::Left => [b',', b'L'].to_vec(),
Instruction::Right => [b',', b'R'].to_vec(),
Instruction::Forward(n) => format!(",{}", n).as_bytes().to_vec(),
})
.skip(1)
.chain(Some(b'\n'))
}
fn mark_common<'a, 'b>(
instructions: &'a [Instruction],
marking: &'b mut [Option<char>],
marker: char,
) -> eyre::Result<&'a [Instruction]> {
let start = marking
.iter()
.enumerate()
.find(|(_i, opt)| opt.is_none())
.map(|(i, _opt)| i)
.unwrap();
let empty_len = marking
.iter()
.skip_while(|opt| opt.is_some())
.take_while(|opt| opt.is_none())
.count();
for to_consider_len in (6..=10).rev() {
if to_consider_len > empty_len {
continue;
}
if (empty_len - to_consider_len < 6) && (empty_len != to_consider_len) {
continue;
}
let pattern = &instructions[start..][..to_consider_len];
let window_starts = instructions[start..]
.windows(to_consider_len)
.enumerate()
.filter(|&(i, window)| {
marking[start..][i..][..to_consider_len]
.iter()
.all(Option::is_none)
&& window == pattern
})
.map(|(i, _window)| i)
.collect::<Vec<usize>>();
if window_starts.len() > 2 {
for i in window_starts {
marking[start..][i..][..to_consider_len].fill(Some(marker));
}
return Ok(pattern);
}
}
eyre::bail!("No suitable marking found")
}
fn make_map(
string: String,
) -> (
HashMap<(i32, i32), Vec<Orientation>>,
(i32, i32),
Orientation,
) {
let width = string.trim().lines().next().unwrap().len();
let (robot_coord, robot_orientation) = string
.chars()
.filter(|ch| ['.', '#', '^', 'v', '>', '<'].contains(ch))
.enumerate()
.map(|(i, ch)| (((i % width) as i32, (i / width) as i32), ch))
.find(|(_coord, ch)| ['^', 'v', '>', '<'].contains(ch))
.map(|(coord, ch)| {
(
coord,
match ch {
'^' => Orientation::Top,
'v' => Orientation::Bottom,
'>' => Orientation::Right,
'<' => Orientation::Left,
_ => unreachable!(),
},
)
})
.unwrap();
let coords = string
.chars()
.filter(|ch| ['.', '#', '^', 'v', '>', '<'].contains(ch))
.enumerate()
.map(|(i, ch)| (((i % width) as i32, (i / width) as i32), ch != '.'))
.filter(|&(_coord, is_not_space)| is_not_space)
.map(|(coord, _bool)| coord)
.collect::<HashSet<_>>();
let mut map = HashMap::with_capacity(coords.len());
for coord in coords.iter().copied() {
let mut vec = Vec::with_capacity(4);
for orientation in Orientation::all() {
if coords.contains(&add_orientation(coord, orientation)) {
vec.push(orientation);
}
}
map.insert(coord, vec);
}
(map, robot_coord, robot_orientation)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum Instruction {
Left,
Right,
Forward(u8),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum Orientation {
Top,
Bottom,
Left,
Right,
}
fn add_orientation((x, y): (i32, i32), orientation: Orientation) -> (i32, i32) {
use Orientation::*;
match orientation {
Top => (x, y - 1),
Bottom => (x, y + 1),
Left => (x - 1, y),
Right => (x + 1, y),
}
}
impl Orientation {
fn left(self) -> Self {
use Orientation::*;
match self {
Top => Left,
Left => Bottom,
Bottom => Right,
Right => Top,
}
}
fn right(self) -> Self {
use Orientation::*;
match self {
Top => Right,
Left => Top,
Bottom => Left,
Right => Bottom,
}
}
fn reverse(self) -> Self {
use Orientation::*;
match self {
Top => Bottom,
Left => Right,
Bottom => Top,
Right => Left,
}
}
fn all() -> [Self; 4] {
use Orientation::*;
[Top, Left, Bottom, Right]
}
}Optimization problem (day 19)
This day is surprisingly simple this late into the puzzles. Part one is trivial compared to days 17 and 15 and part two is only slightly more difficult and just takes quite a bit of execution time. We are checking the effectiveness of a “tractor beam” by deploying drones into space. The intcode program takes in a coordinate (non-negative X value then a non-negative Y value) and outputs whether it is being pulled by the beam (1) or not (0).
Solving part one
We need to find the number of points affected by the tractor beam in the 50x50 area closest to the origin. This can be done by creating a function to get the status of a particular point and then calling that function in a nested loop. Note that this function takes in a clone of the program because we lose the drone after we deploy it and the program halts after outputting the status.
// day_19.rs (new file)
use crate::runner::{HaltReason, Intcode};
use color_eyre::eyre;
pub fn part_1(intcode: Intcode) -> eyre::Result<()> {
let mut count = 0;
for y in 0..50 {
for x in 0..50 {
if get_status(intcode.clone(), x, y)? {
count += 1;
}
}
}
println!(
"{} points are affected by the tractor beam in the 50x50 area closest to the emitter.",
count
);
Ok(())
}
fn get_status(mut intcode: Intcode, x: i64, y: i64) -> eyre::Result<bool> {
let mut input = Some(x);
loop {
match intcode.execute(input.take())? {
HaltReason::NormalHalt => eyre::bail!("Unexpected halt from Intcode program"),
HaltReason::GaveOutput(0) => return Ok(false),
HaltReason::GaveOutput(1) => return Ok(true),
HaltReason::GaveOutput(other_output) => {
eyre::bail!("Unexpected output {} from Intcode program", other_output)
}
HaltReason::NeedInput => {
input = Some(y);
}
}
}
}Solving part two
Now, part two asks us for the 100x100 square closest to the origin where all points are affected by the tractor beam. The answer is 10000 × (the x coordinate) + (the y coordinate) of the point in that 100x100 square that’s closest to the origin.
This is entirely possible to brute-force and that’s exactly what I did. It just takes a long time. One way to optimize this brute force is by noticing a pattern in the beam. The beam goes down and to the right with expanding width at a certain angle and most points are not affected by the beam. One way to exploit this property would be to sample the first 500x500 square and figure out the angle at which it is going on both sides and using that to be selective in which squares we sample next. However, I decided to avoid complexity and did a simple brute force solution.
Since the puzzle asks us to multiply the x coordinate with 10000, a reasonable guess is that the x and y coordinates of the answer have 10000 as the upper bound. In my code, I take 3000 to be the upper bound to make it run faster. I then store the status of all the points from (0, 0) to (2999, 2999) in a list so that I don’t redo computation when checking each 100x100 square. I decided to use a flat list instead of a hashmap as that is more efficient as it can directly index into memory instead of comparing hashes and dealing with hash collisions.
I must reiterate that this approach is slow. It takes 39 seconds with the upper bound set to 3000 and 468 seconds with it set to 10000 on my Intel 8th gen i3 laptop.
I also made a few changes to the rest of the code when doing part 2 of day 19.
for x in 0..50 {
if get_status(intcode.clone(), x, y)? {
count += 1;
+ print!("*");
+ } else {
+ print!(".");
}
}
+ println!();
}
...
-fn get_status(mut intcode: Intcode, x: i64, y: i64) -> eyre::Result<bool> {
- let mut input = Some(x);
+fn get_status(mut intcode: Intcode, x: usize, y: usize) -> eyre::Result<bool> {
+ let mut input = Some(x as i64);
loop {
match intcode.execute(input.take())? {
HaltReason::NormalHalt => eyre::bail!("Unexpected halt from Intcode program"),
HaltReason::GaveOutput(0) => return Ok(false),
HaltReason::GaveOutput(1) => return Ok(true),
HaltReason::GaveOutput(other_output) => {
- eyre::bail!("Unexpected output {} from Intcode program", other_output)
+ eyre::bail!(
+ "Unexpected output {} from Intcode program. Expected either a 0 or a 1.",
+ other_output
+ )
}
HaltReason::NeedInput => {
- input = Some(y);
+ input = Some(y as i64);
}
}
}// day_19.rs new additions
pub fn part_2(intcode: Intcode) -> eyre::Result<()> {
use std::time::Instant;
println!("Please wait for my brute force solution to finish computing. This may take several minutes if you're on a particularly old computer.");
const WIDTH: usize = 3000;
const HEIGHT: usize = 3000;
let point_in_time = Instant::now();
let mut map = vec![false; WIDTH * HEIGHT];
for y in 0..HEIGHT {
for x in 0..WIDTH {
let status = get_status(intcode.clone(), x, y)?;
if status {
map[y * WIDTH + x] = true;
}
}
}
for y in 0..(HEIGHT - 100) {
'b: for x in 0..(WIDTH - 100) {
for y_2 in (y..).take(100) {
for x_2 in (x..).take(100) {
if map[y_2 * WIDTH + x_2] != true {
continue 'b;
};
}
}
println!("Part 2 answer: {}.", 10000 * x + y);
println!("Took {:?} to compute.", point_in_time.elapsed());
return Ok(());
}
}
eyre::bail!("");
}An assembly language within (day 21)
This time, the intcode computer once again is controlling a robot. However, this time, the robot takes input as ASCII assembly instructions. The machine has 2 writable registers T and J and 4 read-only registers A through D, all storing a single boolean value (true or false). The two writeable registers have the value false at the start of the program. The robot moves forward one square forward each tick, normally staying on the ground. It executes the assembly program each tick and jumps if the value of the J register is true. Jumping means that it is airborne for the next three ticks while it still moves ahead, letting it cross holes in the ground upto 3 squares wide that would have otherwise killed it. The robot cannot jump if it is airborne. The registers A through D tell us whether there is ground 1 through 4 squares ahead of the robot, having a true value ground exists and a false value if a hole is there.
The three assembly instructions available to us are the following. In each instruction, X can be any register but Y must be a writeable register (either T or J).
AND X Ystores in registerYthe result of ANDing registerXwith registerY.OR X Ystores in registerYthe result of ORing registerXwith registerY.NOT X Ystores in registerYthe inverse of registerX.
Solving part one
We need to program the robot to navigate without falling into any holes. The intcode program prompts us for input in ASCII (see Day 17) and we need to input each assembly instruction terminated by a newline. The program is terminated with the WALK command, followed by a newline. The intcode program’s memory is limited and thus it can only accept a maximum of 15 assembly instructions. After the program is finished taking in input, it will output the status of the robot, showing a depiction of the robot’s last moments if it falls into a hole, all in ASCII. Finally, at the end, the intcode program gives a single output outside the ASCII range before halting. This is the puzzle answer.
There isn’t much programming in this day, it is more similar to solving a logic puzzle to create the assembly program that jumps correctly. Since the robot jumps 3 squares and lands on the square, before jumping, we must verify that D is true. Let’s try just that as our logic. This can be done using just OR D J given that J and T start out as false.
use crate::runner::{HaltReason, Intcode};
use color_eyre::eyre;
use std::collections::VecDeque;
pub fn part_1(mut intcode: Intcode) -> eyre::Result<()> {
const SPRINGSCRIPT_PROGRAM: &str = "NOT A T
OR D J
WALK
";
let mut lines = SPRINGSCRIPT_PROGRAM
.chars()
.map(|ch| ch as u8)
.collect::<VecDeque<_>>();
let mut input = None;
loop {
match intcode.execute(input.take())? {
HaltReason::NormalHalt => break,
HaltReason::GaveOutput(int) => {
if int < 128 {
let ch = char::from(int as u8);
print!("{}", ch);
} else {
println!("Hull damage reported by springdroid: {}", int);
}
}
HaltReason::NeedInput => {
let ch = lines.pop_front().unwrap();
print!("{}", ch as char);
input = Some(ch).map(i64::from);
}
}
}
Ok(())
}We get the following output:
Input instructions:
OR D J
WALK
Walking...
Didn't make it across:
.................
.................
@................
#####.#..########
.................
.@...............
.................
#####.#..########
..@..............
.................
.................
#####.#..########
.................
...@.............
.................
#####.#..########
.................
.................
....@............
#####.#..########
.................
.................
.................
#####@#..########So, we need to modify this to only jump when there is a hole in front of us. This can be done by implementing the function J = ~A & D, which we can do with the following assembly.
NOT A J
AND D JThis still doesn’t work, giving us the following output:
Input instructions:
NOT A J
AND D J
WALK
Walking...
Didn't make it across:
.................
.................
@................
#####.#..########
.................
.................
.@...............
#####.#..########
.................
.................
..@..............
#####.#..########
.................
.................
...@.............
#####.#..########
.................
.................
....@............
#####.#..########
.................
.................
.................
#####@#..########The robot falls in a hole because when the next square was a hole, it could not jump as the square 4 squares ahead was also a hole. We need to eagerly jump whenever any of the next three squares are a hole and the fourth square is solid ground. This means implementing the function (~A | ~B | ~C) & D, which we can do with the following assembly.
NOT A T
NOT B J
OR T J
NOT C T
OR J T
NOT D J
NOT J J
AND T JThis successfully solves part 1!
Input instructions:
NOT A T
NOT B J
OR T J
NOT C T
OR J T
NOT D J
NOT J J
AND T J
WALK
Walking...
Hull damage reported by springdroid: 19354083Solving part two
There are many areas our robot can’t reach still. To resolve this, we enable extended sensor mode, by terminating the program with RUN instead of WALK. This gives us access to 5 new read-only registers, E through I which represent ground 5 through 9 tiles away from the robot.
Let’s first try running the same program but with RUN to see what happens.
Input instructions:
NOT A T
NOT B J
OR T J
NOT C T
OR J T
NOT D J
NOT J J
AND T J
RUN
Running...
Didn't make it across:
.................
.................
@................
#####.#.#...#####
.................
.................
.@...............
#####.#.#...#####
.................
.................
..@..............
#####.#.#...#####
.................
...@.............
.................
#####.#.#...#####
....@............
.................
.................
#####.#.#...#####
.................
.....@...........
.................
#####.#.#...#####
.................
.................
......@..........
#####.#.#...#####
.................
.................
.................
#####.#@#...#####The program jumps over a hole onto an “island” which it can’t get off of because moving forward will give it a hole and jumping will also give it a hole. Let’s account for these two situations by not jumping when both 5 steps ahead (one ahead of the jump target) or 8 steps ahead (jumping from the jump target) is a hole. This is equivalent to ANDing our previous logic by E | H making our final expression to put in J be (~A | ~B | ~C) & (E | H) & D. This, using De Morgan’s laws, it can be converted into ~(A & B & C) & D & (E | H). This conversion step is unnecessary and I do not know why I did it for my part 2 solution. The assembly code is:
NOT A T
NOT T T
AND B T
AND C T
NOT T J
AND D J
NOT H T
NOT T T
OR E T
AND T J
RUNThis gives the following output:
Input instructions:
NOT A T
NOT T T
AND B T
AND C T
NOT T J
AND D J
NOT H T
NOT T T
OR E T
AND T J
RUN
Running...
Hull damage reported by springdroid: 1143499964And day 21 is complete!
Parallel execution (day 23)
This day, we need to create a network of 50 intcode computers all talking to each other. Each computer has an input queue associated with it. All computers start out with the same intcode (the puzzle input). Each computer starts out by asking for its network address with an input instruction; these addresses range from 0 to 49 (both inclusive) and each computer must have a unique address.
All the communication between computers is done by sending and receiving packets which consist of an X value and a Y value. To send a packet to another computer, the sending computer will output 3 values: the destination computer’s address, the packet’s X value, and the packet’s Y value. Sending a packet immediately puts it in the recipient’s input queue without waiting for the recipient to actually recieve the packet via input instructions. To receive a packet, the receiving computer takes in as input the X value and the Y value and then removes it from the input queue. If the packet queue is empty, the computer is given -1 as input.
Solving part one
Packets must be given to the recipient in the order they were sent by any senders. This correct ordering property made this day tricky to get right and I have a vague recollection of spending a lot of time debugging my initial approach and then rewriting it completely to form my current correct solution.
The key to this day is to run the computers in lockstep, one instruction of each computer at one time, without waiting for IO to create pauses in execution. This means we have to modify our intcode interpreter to support running only a single instruction and then yielding.
Another approach could be to use the operating system’s multithreading capabilities and spawn 50 threads, one for each computer, but I don’t know how synchronizing that and writing into a specific input queue would work. Also, given that there is a mix of different operations available to the intcode program, the physical computer’s CPU will take different times to execute different instructions, meaning that different intcode computers could theoretically go out of sync. Another source of desynchronization could be the CPU/OS scheduler[3].
I added a new execute_single method, and replace the execute method to call that.
impl Intcode {
...
#[inline]
pub fn execute_single(&mut self, mut input: Option<i64>) -> eyre::Result<Option<HaltReason>> {
match get_opcode(self.access_memory(self.current)) {
1 => {
let [param_1, param_2] = self.get_param_values::<2>();
let out_index = self.get_param_value_as_index(3);
*self.access_memory_mut(out_index) = param_1 + param_2;
self.current += 4;
}
2 => {
let [param_1, param_2] = self.get_param_values::<2>();
let out_index = self.get_param_value_as_index(3);
*self.access_memory_mut(out_index) = param_1 * param_2;
self.current += 4;
}
3 => {
if let Some(input) = input.take() {
let out_index = self.get_param_value_as_index(1);
*self.access_memory_mut(out_index) = input;
self.current += 2;
} else {
return Ok(Some(HaltReason::NeedInput));
}
}
4 => {
let [output] = self.get_param_values::<1>();
self.current += 2;
return Ok(Some(HaltReason::GaveOutput(output)));
}
5 => {
let [param_1, param_2] = self.get_param_values::<2>();
if param_1 != 0 {
self.current = param_2 as usize;
} else {
self.current += 3;
}
}
6 => {
let [param_1, param_2] = self.get_param_values::<2>();
if param_1 == 0 {
self.current = param_2 as usize;
} else {
self.current += 3;
}
}
7 => {
let [param_1, param_2] = self.get_param_values::<2>();
let out_index = self.get_param_value_as_index(3);
*self.access_memory_mut(out_index) = (param_1 < param_2) as i64;
self.current += 4;
}
8 => {
let [param_1, param_2] = self.get_param_values::<2>();
let out_index = self.get_param_value_as_index(3);
*self.access_memory_mut(out_index) = (param_1 == param_2) as i64;
self.current += 4;
}
9 => {
let [param] = self.get_param_values::<1>();
self.relative_base += param;
self.current += 2;
}
99 => {
return Ok(Some(HaltReason::NormalHalt));
}
opcode @ _ => eyre::bail!("Unknown opcode {} at position {}", opcode, self.current),
}
Ok(None)
}
pub fn execute(&mut self, mut input: Option<i64>) -> eyre::Result<HaltReason> {
let mut running_input = None;
loop {
match self.execute_single(running_input.take())? {
Some(HaltReason::NeedInput) if input.is_some() => running_input = input.take(),
Some(other_halt_reason) => break Ok(other_halt_reason),
None => {}
}
}
}
...
}Note how since the execute_single method consumes the input parameter whether or not it needs input for the current instruction, in the execute method, we pass None to execute_single and wait until it encounters an input instruction so that we guarantee that the input we pass to it is used by an input instruction instead of being dropped without being used.
With this change to our interpreter, the main environment code for day 23 isn’t very complicated. The number we have to find for our answer is first Y value that any computer tries to send to address 255.
One more thing, there is actually a source of desyncronization bugs in my solution: each iteration of the outer loop doesn’t correspond to a single instruction executed by each computer due to IO instructions, which lead to an entire packet being sent/received (which takes multiple instructions) in a single outer loop iteration.
// day_23.rs (new file)
use crate::runner::{HaltReason, Intcode};
use color_eyre::eyre;
use std::collections::VecDeque;
#[derive(Debug)]
struct Computer {
intcode: Intcode,
input_queue: VecDeque<i64>,
}
impl Computer {
fn initialize(mut intcode: Intcode, network_address: i64) -> eyre::Result<Self> {
intcode.execute(Some(network_address))?;
Ok(Self {
intcode,
input_queue: VecDeque::new(),
})
}
}
pub fn part_1(intcode: Intcode) -> eyre::Result<()> {
let mut computers = (0..50)
.map(|i| Computer::initialize(intcode.clone(), i))
.collect::<eyre::Result<Vec<_>>>()?;
let answer = 'main: loop {
for i in 0..50 {
match computers[i].intcode.execute_single(None)? {
None => {}
Some(HaltReason::GaveOutput(destination)) => {
let (x_value, y_value) = get_packet_values(&mut computers[i])?;
if destination == 255 {
break 'main y_value;
}
if let Some(destination_computer) = computers.get_mut(destination as usize) {
destination_computer.input_queue.extend([x_value, y_value]);
}
}
Some(HaltReason::NeedInput) => recieve_packet(&mut computers[i])?,
Some(HaltReason::NormalHalt) => eyre::bail!("Hmm, a halt. Sus 👀. Computer #{}", i),
}
}
};
dbg!(answer);
Ok(())
}
fn recieve_packet(computer: &mut Computer) -> eyre::Result<()> {
if let Some(x_value) = computer.input_queue.pop_front() {
let y_value = computer
.input_queue
.pop_front()
.ok_or(eyre::eyre!("No y value in input queue."))?;
match computer.intcode.execute(Some(x_value))? {
HaltReason::NormalHalt => {
eyre::bail!("Unexpected halt in middle of recieving packet values")
}
HaltReason::NeedInput => {
computer.intcode.execute_single(Some(y_value))?;
}
HaltReason::GaveOutput(out) => {
eyre::bail!(
"Unexpected output of value {} in middle of recieving packet values",
out
)
}
}
} else {
computer.intcode.execute_single(Some(-1))?;
}
Ok(())
}
fn get_packet_values(computer: &mut Computer) -> eyre::Result<(i64, i64)> {
let mut x_value = None;
let mut y_value = None;
let mut input = None;
loop {
match computer.intcode.execute(input.take())? {
HaltReason::NormalHalt => {
eyre::bail!("Unexpected halt in middle of sending packet values")
}
HaltReason::NeedInput => {
println!("Hmm, A NeedInput while giving packet values. Sus. 👀");
input = Some(computer.input_queue.pop_front().unwrap_or(-1));
}
HaltReason::GaveOutput(output) => {
*(if x_value.is_none() {
&mut x_value
} else if y_value.is_none() {
&mut y_value
} else {
unreachable!()
}) = Some(output);
if y_value.is_some() {
break;
}
}
}
}
Ok((x_value.unwrap(), y_value.unwrap()))
}Solving part two
The address 255 contains a device called the NAT (Not Always Transmitting) that controls idle periods in the network. The NAT stores only the last packet it received, discarding the old packet when it receives a new packet. The network is considered idle if all computers have empty queues and are continuously trying to receive packets without sending anything. Once the network is idle, the NAT sends the packet it last received to address 0 to restart network activity. To get the answer for part 2, we need to track the Y values of the packets being sent to address 0 by the NAT find out the first such Y value sent twice in a row.
The way I decided to implement the network idle condition was to add a flag to each computer that indicates that it is waiting, that is, it is “continuously trying to receive packets without sending packets”. This flag starts out as false, is raised when the computer tries to receive input from an empty queue, and is lowered when it either successfully gets input from the queue or gives output. I consider the network idle when all computer input queues are empty and all computers have their waiting flag set to true for 100 instruction iterations. The number 100 here was arbitrarily chosen as it sounds like a big enough number to deal with any intermediate states between IO for the computers.
In hindsight, maybe a better approach would be to have a receive miss counter instead of a flag and check if that value ever exceeds a certain number for all computers to check if the network is idle. This feels less arbitrary and random than counting the number of instructions executed as it is directly connected to the IO.
// day_23.rs (changes)
struct Computer {
intcode: Intcode,
input_queue: VecDeque<i64>,
+ is_waiting: bool,
}
impl Computer {
...
Ok(Self {
intcode,
input_queue: VecDeque::new(),
+ is_waiting: false,
})
}
}
...
fn recieve_packet(computer: &mut Computer) -> eyre::Result<()> {
if let Some(x_value) = computer.input_queue.pop_front() {
+ computer.is_waiting = false;
let y_value = computer
.input_queue
.pop_front()
...
} else {
computer.intcode.execute_single(Some(-1))?;
+ computer.is_waiting = true;
}
Ok(())
}
fn get_packet_values(computer: &mut Computer) -> eyre::Result<(i64, i64)> {
+ computer.is_waiting = false;
let mut x_value = None;// day_23.rs new additions
pub fn part_2(intcode: Intcode) -> eyre::Result<()> {
let mut computers = (0..50)
.map(|i| Computer::initialize(intcode.clone(), i))
.collect::<eyre::Result<Vec<_>>>()?;
let mut nat = None;
let mut nat_outbound = Vec::new();
let mut idle_iter_count = 0;
let answer = loop {
for i in 0..50 {
match computers[i].intcode.execute_single(None)? {
None => {}
Some(HaltReason::GaveOutput(destination)) => {
let (x_value, y_value) = get_packet_values(&mut computers[i])?;
if destination == 255 {
nat = Some((x_value, y_value));
}
if let Some(destination_computer) = computers.get_mut(destination as usize) {
destination_computer.input_queue.extend([x_value, y_value]);
}
}
Some(HaltReason::NeedInput) => recieve_packet(&mut computers[i])?,
Some(HaltReason::NormalHalt) => eyre::bail!("Hmm, a halt. Sus 👀. Computer #{}", i),
}
}
let all_waiting = computers
.iter()
.all(|computer| computer.input_queue.is_empty() && computer.is_waiting);
if all_waiting {
idle_iter_count += 1;
} else {
idle_iter_count = 0;
}
if idle_iter_count > 100 {
let (x_value, y_value) = nat
.take()
.ok_or(eyre::eyre!("No NAT value but network is idle."))?;
if nat_outbound.last().copied() == Some(y_value) {
break y_value;
}
computers[0].input_queue.extend([x_value, y_value]);
nat_outbound.push(y_value);
idle_iter_count = 0;
}
};
dbg!(answer);
Ok(())
}Manual interactive text game (day 25)
The finale to AoC 2019 is reasonably difficult even though it doesn’t involve much coding. The intcode program represents an text adventure game with ASCII IO that we have to play to get the answer. In this game we are seeing through the eyes of a robot on a spaceship. The robot will describe its surroundings and prompt us for input with a Command? prompt. We can then respond with a command terminated by a newline. Commands must be exact with no extra spaces. The commands are:
north;south;east;westfor movement between roomsinvto get a list of items the droid is carryingtake <item name>to pick up an item from the environmentdrop <item name>to put down an item into the environment
We need to find the password for the main airlock by playing this game (and it would be pretty hard to automate this). To do this, we need explore the spaceship and pass a weight check found in a certain room by picking up a certain set of items found in the spaceship.
Solving part one
There isn’t much coding to do. I call the execute function in a loop, printing out any characters outputted, and adding a line from stdin to an input queue if it is empty or giving the program the front of the input queue as input otherwise.
use crate::runner::{HaltReason, Intcode};
use color_eyre::eyre;
use std::collections::VecDeque;
use std::io::{stdin, stdout, Write};
// A hint for the people looking at the source:
// The items for my input that unlocked the door were
// `spool of cat6`, `fixed point, `shell, and, `candy cane`
pub fn part_1(mut intcode: Intcode) -> eyre::Result<()> {
let mut input_queue = VecDeque::new();
let mut input = None;
loop {
match intcode.execute(input.take())? {
HaltReason::NormalHalt => break,
HaltReason::GaveOutput(int) => {
let ch = char::from(int as u8);
print!("{}", ch);
}
HaltReason::NeedInput => {
if input_queue.is_empty() {
stdout().flush()?;
let mut line = String::new();
stdin().read_line(&mut line)?;
input_queue.extend(line.as_bytes().iter().copied().map(i64::from));
} else {
input = input_queue.pop_front();
}
}
}
}
Ok(())
}Solving part two
There is no part two for day 25. The star for day 25 part two is obtained only after obtaining all other 49 stars of Advent of Code 2019.
Fin
With this, we are now done with all the Intcode problems of Advent of Code 2019. You have made it to the end of this rather long article! I hope you found it fun and interesting.