Skip to main content

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 then
9//!   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 std::fmt::Write as _;
20
21use super::intcode::*;
22use crate::util::bitset::*;
23use crate::util::parse::*;
24
25pub fn parse(input: &str) -> Vec<i64> {
26    input.iter_signed().collect()
27}
28
29pub fn part1(input: &[i64]) -> String {
30    if cfg!(feature = "frivolity") { play_manually(input) } else { play_automatically(input) }
31}
32
33pub fn part2(_input: &[i64]) -> &'static str {
34    "n/a"
35}
36
37// Let a human play the game interactively.
38fn play_manually(input: &[i64]) -> String {
39    use std::io::stdin;
40
41    let mut computer = Computer::new(input);
42    let mut output = String::new();
43    let mut input = String::new();
44
45    loop {
46        match computer.run() {
47            State::Output(value) => {
48                let ascii = (value as u8) as char;
49                output.push(ascii);
50            }
51            State::Input => {
52                pretty_print(&output);
53                output.clear();
54                let _unused = stdin().read_line(&mut input);
55                computer.input_ascii(&input);
56                input.clear();
57            }
58            State::Halted => {
59                pretty_print(&output);
60                output.retain(|c| c.is_ascii_digit());
61                break output;
62            }
63        }
64    }
65}
66
67// Use ANSI codes to colorize the output to highlight the text.
68fn pretty_print(output: &str) {
69    use crate::util::ansi::*;
70
71    let mut buffer = String::new();
72    let mut item = GREEN;
73
74    for line in output.lines() {
75        if line.starts_with('=') {
76            let _ = write!(&mut buffer, "{BOLD}{WHITE}{line}{RESET}");
77        } else if line.starts_with('-') {
78            let _ = write!(&mut buffer, "{item}{line}{RESET}");
79        } else if line.starts_with("Items here:") {
80            item = YELLOW;
81            buffer.push_str(line);
82        } else {
83            buffer.push_str(line);
84        }
85        buffer.push('\n');
86    }
87
88    println!("{buffer}");
89}
90
91fn play_automatically(input: &[i64]) -> String {
92    let mut computer = Computer::new(input);
93    let mut stack = Vec::new();
94    let mut path = Vec::new();
95    let mut inventory = Vec::new();
96
97    // DFS through the ship, picking up all 8 safe items, then return to the starting point.
98    explore(&mut computer, &mut stack, &mut path, &mut inventory);
99
100    // Retrace our path back to the Security Checkpoint.
101    let last = path.pop().unwrap();
102
103    for direction in path {
104        movement_silent(&mut computer, &direction);
105    }
106
107    // Use Gray codes to take or drop one item at a time, until we are exactly the right weight.
108    // As an optimization we keep track of combinations of items that are too heavy or too light.
109    // If we are adding an item to a collection that is already too heavy or vice-versa,
110    // then we can skip the pressure plate check.
111    let combinations: u32 = 1 << inventory.len();
112    let mut have = combinations - 1;
113    let mut want = have;
114    let mut output = String::new();
115    let mut too_light = Vec::with_capacity(combinations as usize);
116    let mut too_heavy = Vec::with_capacity(combinations as usize);
117
118    'outer: for i in 1..combinations {
119        let current = gray_code(i);
120        let previous = gray_code(i - 1);
121        let changed = current ^ previous;
122
123        // Since we start with all items in our possession, the meaning of bits in the Gray code is
124        // reversed. 0 is take an item and 1 is drop an item. Iterating over too_heavy and
125        // too_light is still cheaper than the cost to emulate another take or drop, so it is worth
126        // seeing if we can skip altering inventory to a given configuration.
127        want ^= changed;
128        for heavy in &too_heavy {
129            if (want & heavy) == *heavy {
130                // Want is a superset of a known heavy configuration.
131                continue 'outer;
132            }
133        }
134        for light in &too_light {
135            if (want & light) == want {
136                // Want is a subset of a known light configuration.
137                continue 'outer;
138            }
139        }
140
141        sync_items(&mut computer, have, want, &inventory);
142        have = want;
143        if matches!(movement_noisy(&mut computer, &last, &mut output), State::Halted) {
144            // Keep only the password digits from Santa's response.
145            output.retain(|b| b.is_ascii_digit());
146            break;
147        } else if output.contains("heavier") {
148            too_light.push(want);
149        } else {
150            too_heavy.push(want);
151        }
152
153        output.clear();
154    }
155
156    output
157}
158
159fn explore(
160    computer: &mut Computer,
161    stack: &mut Vec<String>,
162    path: &mut Vec<String>,
163    inventory: &mut Vec<String>,
164) {
165    let direction = stack.last().map_or("none", String::as_str);
166    let reverse = opposite(direction);
167
168    let mut output = String::new();
169    movement_noisy(computer, direction, &mut output);
170
171    for line in output.lines() {
172        if line.starts_with("== Pressure-Sensitive Floor ==") {
173            path.clone_from(stack);
174            return;
175        } else if let Some(suffix) = line.strip_prefix("- ") {
176            if opposite(suffix) == "none" {
177                if !dangerous(suffix) {
178                    take_item(computer, suffix);
179                    inventory.push(suffix.to_string());
180                }
181            } else if suffix != reverse {
182                stack.push(suffix.to_string());
183                explore(computer, stack, path, inventory);
184                stack.pop();
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
241fn sync_items(computer: &mut Computer, have: u32, want: u32, inventory: &[String]) {
242    for i in (want ^ have).biterator() {
243        if have & (1 << i) != 0 {
244            drop_item(computer, &inventory[i]);
245        } else {
246            take_item(computer, &inventory[i]);
247        }
248    }
249}
250
251// A quirk of the intcode program is that commands can't be stacked. We must first read all the
252// output from the previous command before the next command can be submitted.
253fn drain_output(computer: &mut Computer) {
254    while let State::Output(_) = computer.run() {}
255}
256
257/// Convert a normal binary number to its Gray Code equivalent.
258fn gray_code(n: u32) -> u32 {
259    n ^ (n >> 1)
260}