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 crate::util::bitset::*;
22use crate::util::intcode::*;
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) => output.push(char::from(value as u8)),
48            State::Input => {
49                pretty_print(&output);
50                output.clear();
51                let _unused = stdin().read_line(&mut input);
52                computer.input_ascii(&input);
53                input.clear();
54            }
55            State::Halted => {
56                pretty_print(&output);
57                output.retain(|c| c.is_ascii_digit());
58                break output;
59            }
60        }
61    }
62}
63
64// Use ANSI codes to colorize the output to highlight the text.
65fn pretty_print(output: &str) {
66    use crate::util::ansi::*;
67
68    let mut buffer = String::new();
69    let mut item = GREEN;
70
71    for line in output.lines() {
72        if line.starts_with('=') {
73            let _ = write!(&mut buffer, "{BOLD}{WHITE}{line}{RESET}");
74        } else if line.starts_with('-') {
75            let _ = write!(&mut buffer, "{item}{line}{RESET}");
76        } else if line.starts_with("Items here:") {
77            item = YELLOW;
78            buffer.push_str(line);
79        } else {
80            buffer.push_str(line);
81        }
82        buffer.push('\n');
83    }
84
85    println!("{buffer}");
86}
87
88fn play_automatically(input: &[i64]) -> String {
89    let mut computer = Computer::new(input);
90    let mut stack = Vec::new();
91    let mut path = Vec::new();
92    let mut inventory = Vec::new();
93
94    // DFS through the ship, picking up all 8 safe items, then return to the starting point.
95    explore(&mut computer, &mut stack, &mut path, &mut inventory);
96
97    // Retrace our path back to the Security Checkpoint.
98    let last = path.pop().unwrap();
99
100    for direction in path {
101        movement_silent(&mut computer, &direction);
102    }
103
104    // Use Gray codes to take or drop one item at a time, until we are exactly the right weight.
105    // As an optimization we keep track of combinations of items that are too heavy or too light.
106    // If we are adding an item to a collection that is already too heavy or vice-versa,
107    // then we can skip the pressure plate check.
108    let combinations = 1 << inventory.len();
109    let mut have = combinations - 1;
110    let mut want = have;
111    let mut output = String::new();
112    let mut too_light = Vec::with_capacity(combinations as usize);
113    let mut too_heavy = Vec::with_capacity(combinations as usize);
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
120        // Since we start with all items in our possession, the meaning of bits in the Gray code is
121        // reversed. 0 is take an item and 1 is drop an item. Iterating over too_heavy and
122        // too_light is still cheaper than the cost to emulate another take or drop, so it is worth
123        // seeing if we can skip altering inventory to a given configuration.
124        want ^= changed;
125
126        // Skip supersets of known heavy configurations and subsets of known light ones.
127        if too_heavy.iter().any(|&heavy| want & heavy == heavy)
128            || too_light.iter().any(|&light| want & light == want)
129        {
130            continue;
131        }
132
133        sync_items(&mut computer, have, want, &inventory);
134        have = want;
135
136        if matches!(movement_noisy(&mut computer, &last, &mut output), State::Halted) {
137            // Keep only the password digits from Santa's response.
138            output.retain(|b| b.is_ascii_digit());
139            break;
140        } else if output.contains("heavier") {
141            too_light.push(want);
142        } else {
143            too_heavy.push(want);
144        }
145
146        output.clear();
147    }
148
149    output
150}
151
152fn explore(
153    computer: &mut Computer,
154    stack: &mut Vec<String>,
155    path: &mut Vec<String>,
156    inventory: &mut Vec<String>,
157) {
158    let direction = stack.last().map_or("none", String::as_str);
159    let reverse = opposite(direction);
160
161    let mut output = String::new();
162    movement_noisy(computer, direction, &mut output);
163
164    for line in output.lines() {
165        if line.starts_with("== Pressure-Sensitive Floor ==") {
166            path.clone_from(stack);
167            return;
168        } else if let Some(suffix) = line.strip_prefix("- ") {
169            if opposite(suffix) == "none" {
170                if !dangerous(suffix) {
171                    take_item(computer, suffix);
172                    inventory.push(suffix.to_string());
173                }
174            } else if suffix != reverse {
175                stack.push(suffix.to_string());
176                explore(computer, stack, path, inventory);
177                stack.pop();
178            }
179        }
180    }
181
182    movement_silent(computer, reverse);
183}
184
185fn opposite(direction: &str) -> &'static str {
186    match direction {
187        "north" => "south",
188        "south" => "north",
189        "east" => "west",
190        "west" => "east",
191        _ => "none",
192    }
193}
194
195fn dangerous(item: &str) -> bool {
196    matches!(
197        item,
198        "escape pod" | "giant electromagnet" | "infinite loop" | "molten lava" | "photons"
199    )
200}
201
202fn movement_noisy(computer: &mut Computer, direction: &str, output: &mut String) -> State {
203    if direction != "none" {
204        computer.input_ascii(&format!("{direction}\n"));
205    }
206    loop {
207        match computer.run() {
208            State::Output(value) => output.push(char::from(value as u8)),
209            other => break other,
210        }
211    }
212}
213
214fn movement_silent(computer: &mut Computer, direction: &str) {
215    if direction != "none" {
216        computer.input_ascii(&format!("{direction}\n"));
217        drain_output(computer);
218    }
219}
220
221fn take_item(computer: &mut Computer, item: &str) {
222    computer.input_ascii(&format!("take {item}\n"));
223    drain_output(computer);
224}
225
226fn drop_item(computer: &mut Computer, item: &str) {
227    computer.input_ascii(&format!("drop {item}\n"));
228    drain_output(computer);
229}
230
231fn sync_items(computer: &mut Computer, have: u32, want: u32, inventory: &[String]) {
232    for i in (want ^ have).biterator() {
233        if have & (1 << i) != 0 {
234            drop_item(computer, &inventory[i]);
235        } else {
236            take_item(computer, &inventory[i]);
237        }
238    }
239}
240
241// A quirk of the intcode program is that commands can't be stacked. We must first read all the
242// output from the 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 a normal binary number to its Gray Code equivalent.
248fn gray_code(n: u32) -> u32 {
249    n ^ (n >> 1)
250}