aoc/year2019/
day25.rs

1//! # Cryostasis
2//!
3//! Plays the game automatically, solving the weight puzzle using
4//! [Gray codes](https://en.wikipedia.org/wiki/Gray_code) to check every combination of items
5//! by swapping only one item at a time.
6//!
7//! Makes some assumptions:
8//! * The ship's layout contains no loops, so a depth first search will explore every room
9//!   then return to the starting point.
10//! * The 5 dangerous items are common across all inputs.
11//! * No items are called "north", "south", "west" or "east".
12//! * The final room is called "Pressure-Sensitive Floor".
13//!
14//! If these assumptions hold then this solution will solve any arbitrary combination of ship
15//! layout and items.
16//!
17//! Just for fun this solution can be played interactively on the command line if
18//! "--features frivolity" is enabled.
19use super::intcode::*;
20use crate::util::hash::*;
21use crate::util::parse::*;
22use std::fmt::Write as _;
23
24pub fn parse(input: &str) -> Vec<i64> {
25    input.iter_signed().collect()
26}
27
28pub fn part1(input: &[i64]) -> String {
29    if cfg!(feature = "frivolity") { play_manually(input) } else { play_automatically(input) }
30}
31
32pub fn part2(_input: &[i64]) -> &'static str {
33    "n/a"
34}
35
36// Let a human play the game interactively.
37fn play_manually(input: &[i64]) -> String {
38    use std::io::stdin;
39
40    let mut computer = Computer::new(input);
41    let mut output = String::new();
42    let mut input = String::new();
43
44    loop {
45        match computer.run() {
46            State::Output(value) => {
47                let ascii = (value as u8) as char;
48                output.push(ascii);
49            }
50            State::Input => {
51                pretty_print(&output);
52                output.clear();
53                let _unused = stdin().read_line(&mut input);
54                computer.input_ascii(&input);
55                input.clear();
56            }
57            State::Halted => {
58                pretty_print(&output);
59                output.retain(|c| c.is_ascii_digit());
60                break output;
61            }
62        }
63    }
64}
65
66// Use ANSI codes to colorize the output to highlight the text.
67fn pretty_print(output: &str) {
68    use crate::util::ansi::*;
69
70    let mut buffer = String::new();
71    let mut item = GREEN;
72
73    for line in output.lines() {
74        if line.starts_with('=') {
75            let _ = write!(&mut buffer, "{BOLD}{WHITE}{line}{RESET}");
76        } else if line.starts_with('-') {
77            let _ = write!(&mut buffer, "{item}{line}{RESET}");
78        } else if line.starts_with("Items here:") {
79            item = YELLOW;
80            buffer.push_str(line);
81        } else {
82            buffer.push_str(line);
83        }
84        buffer.push('\n');
85    }
86
87    println!("{buffer}");
88}
89
90fn play_automatically(input: &[i64]) -> String {
91    let mut computer = Computer::new(input);
92    let mut stack = Vec::new();
93    let mut path = Vec::new();
94    let mut inventory = Vec::new();
95
96    // DFS through the ship, picking up all 8 safe items, then return to the starting point.
97    explore(&mut computer, &mut stack, &mut path, &mut inventory);
98
99    // Retrace our path back to the Security Checkpoint.
100    let last = path.pop().unwrap();
101
102    for direction in path {
103        movement_silent(&mut computer, &direction);
104    }
105
106    // Use Gray codes to take or drop one item at a time, until we are exactly the right weight.
107    // As an optimization we keep track of combinations of items that are too heavy or too light.
108    // If we are adding an item to a collection that is already too heavy or vice-versa,
109    // then we can skip the pressure plate check.
110    let combinations: u32 = 1 << inventory.len();
111    let mut output = String::new();
112    let mut too_light = FastSet::new();
113    let mut too_heavy = FastSet::new();
114
115    for i in 1..combinations {
116        let current = gray_code(i);
117        let previous = gray_code(i - 1);
118        let changed = current ^ previous;
119        let index = changed.trailing_zeros() as usize;
120
121        // Since we start with all items in our possesion, the meaning of bits in the gray code is
122        // reversed. 0 is take an item and 1 is drop an item.
123        if current & changed == 0 {
124            take_item(&mut computer, &inventory[index]);
125
126            if too_heavy.contains(&previous) {
127                too_heavy.insert(current);
128                continue;
129            }
130        } else {
131            drop_item(&mut computer, &inventory[index]);
132
133            if too_light.contains(&previous) {
134                too_light.insert(current);
135                continue;
136            }
137        }
138
139        if matches!(movement_noisy(&mut computer, &last, &mut output), State::Halted) {
140            // Keep only the password digits from Santa's response.
141            output.retain(|b| b.is_ascii_digit());
142            break;
143        } else if output.contains("heavier") {
144            too_light.insert(current);
145        } else {
146            too_heavy.insert(current);
147        }
148
149        output.clear();
150    }
151
152    output
153}
154
155fn explore(
156    computer: &mut Computer,
157    stack: &mut Vec<String>,
158    path: &mut Vec<String>,
159    inventory: &mut Vec<String>,
160) {
161    let direction = stack.last().map_or("none", |d| d.as_str());
162    let reverse = opposite(direction);
163
164    let mut output = String::new();
165    movement_noisy(computer, direction, &mut output);
166
167    for line in output.lines() {
168        if line.starts_with("== Pressure-Sensitive Floor ==") {
169            path.clone_from(stack);
170            return;
171        } else if let Some(suffix) = line.strip_prefix("- ") {
172            if opposite(suffix) == "none" {
173                let item = String::from(suffix);
174                if !dangerous(&item) {
175                    take_item(computer, &item);
176                    inventory.push(item);
177                }
178            } else {
179                let direction = String::from(suffix);
180                if direction != reverse {
181                    stack.push(direction);
182                    explore(computer, stack, path, inventory);
183                    stack.pop();
184                }
185            }
186        }
187    }
188
189    movement_silent(computer, reverse);
190}
191
192fn opposite(direction: &str) -> &'static str {
193    match direction {
194        "north" => "south",
195        "south" => "north",
196        "east" => "west",
197        "west" => "east",
198        _ => "none",
199    }
200}
201
202fn dangerous(item: &str) -> bool {
203    matches!(
204        item,
205        "escape pod" | "giant electromagnet" | "infinite loop" | "molten lava" | "photons"
206    )
207}
208
209fn movement_noisy(computer: &mut Computer, direction: &str, output: &mut String) -> State {
210    if direction != "none" {
211        computer.input_ascii(&format!("{direction}\n"));
212    }
213    loop {
214        match computer.run() {
215            State::Output(value) => {
216                let ascii = (value as u8) as char;
217                output.push(ascii);
218            }
219            other => break other,
220        }
221    }
222}
223
224fn movement_silent(computer: &mut Computer, direction: &str) {
225    if direction != "none" {
226        computer.input_ascii(&format!("{direction}\n"));
227        drain_output(computer);
228    }
229}
230
231fn take_item(computer: &mut Computer, item: &str) {
232    computer.input_ascii(&format!("take {item}\n"));
233    drain_output(computer);
234}
235
236fn drop_item(computer: &mut Computer, item: &str) {
237    computer.input_ascii(&format!("drop {item}\n"));
238    drain_output(computer);
239}
240
241// A quirk of the intcode program is that commands can't be stacked. We must first read all the
242// input from previous command before the next command can be submitted.
243fn drain_output(computer: &mut Computer) {
244    while let State::Output(_) = computer.run() {}
245}
246
247// Convert an normal binary number to its Gray Code equivalent
248fn gray_code(n: u32) -> u32 {
249    n ^ (n >> 1)
250}