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
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::bitset::*;
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 have = combinations - 1;
112    let mut want = have;
113    let mut output = String::new();
114    let mut too_light = Vec::with_capacity(combinations as usize);
115    let mut too_heavy = Vec::with_capacity(combinations as usize);
116
117    'outer: for i in 1..combinations {
118        let current = gray_code(i);
119        let previous = gray_code(i - 1);
120        let changed = current ^ previous;
121
122        // Since we start with all items in our possession, the meaning of bits in the Gray code is
123        // reversed. 0 is take an item and 1 is drop an item. Iterating over too_heavy and
124        // too_light is still cheaper than the cost to emulate another take or drop, so it is worth
125        // seeing if we can skip altering inventory to a given configuration.
126        want ^= changed;
127        for heavy in &too_heavy {
128            if (want & heavy) == *heavy {
129                // Want is a superset of a known heavy configuration.
130                continue 'outer;
131            }
132        }
133        for light in &too_light {
134            if (want & light) == want {
135                // Want is a subset of a known light configuration.
136                continue 'outer;
137            }
138        }
139
140        sync_items(&mut computer, have, want, &inventory);
141        have = want;
142        if matches!(movement_noisy(&mut computer, &last, &mut output), State::Halted) {
143            // Keep only the password digits from Santa's response.
144            output.retain(|b| b.is_ascii_digit());
145            break;
146        } else if output.contains("heavier") {
147            too_light.push(want);
148        } else {
149            too_heavy.push(want);
150        }
151
152        output.clear();
153    }
154
155    output
156}
157
158fn explore(
159    computer: &mut Computer,
160    stack: &mut Vec<String>,
161    path: &mut Vec<String>,
162    inventory: &mut Vec<String>,
163) {
164    let direction = stack.last().map_or("none", String::as_str);
165    let reverse = opposite(direction);
166
167    let mut output = String::new();
168    movement_noisy(computer, direction, &mut output);
169
170    for line in output.lines() {
171        if line.starts_with("== Pressure-Sensitive Floor ==") {
172            path.clone_from(stack);
173            return;
174        } else if let Some(suffix) = line.strip_prefix("- ") {
175            if opposite(suffix) == "none" {
176                if !dangerous(suffix) {
177                    take_item(computer, suffix);
178                    inventory.push(suffix.to_string());
179                }
180            } else if suffix != reverse {
181                stack.push(suffix.to_string());
182                explore(computer, stack, path, inventory);
183                stack.pop();
184            }
185        }
186    }
187
188    movement_silent(computer, reverse);
189}
190
191fn opposite(direction: &str) -> &'static str {
192    match direction {
193        "north" => "south",
194        "south" => "north",
195        "east" => "west",
196        "west" => "east",
197        _ => "none",
198    }
199}
200
201fn dangerous(item: &str) -> bool {
202    matches!(
203        item,
204        "escape pod" | "giant electromagnet" | "infinite loop" | "molten lava" | "photons"
205    )
206}
207
208fn movement_noisy(computer: &mut Computer, direction: &str, output: &mut String) -> State {
209    if direction != "none" {
210        computer.input_ascii(&format!("{direction}\n"));
211    }
212    loop {
213        match computer.run() {
214            State::Output(value) => {
215                let ascii = (value as u8) as char;
216                output.push(ascii);
217            }
218            other => break other,
219        }
220    }
221}
222
223fn movement_silent(computer: &mut Computer, direction: &str) {
224    if direction != "none" {
225        computer.input_ascii(&format!("{direction}\n"));
226        drain_output(computer);
227    }
228}
229
230fn take_item(computer: &mut Computer, item: &str) {
231    computer.input_ascii(&format!("take {item}\n"));
232    drain_output(computer);
233}
234
235fn drop_item(computer: &mut Computer, item: &str) {
236    computer.input_ascii(&format!("drop {item}\n"));
237    drain_output(computer);
238}
239
240fn sync_items(computer: &mut Computer, have: u32, want: u32, inventory: &[String]) {
241    for i in (want ^ have).biterator() {
242        if have & (1 << i) != 0 {
243            drop_item(computer, &inventory[i]);
244        } else {
245            take_item(computer, &inventory[i]);
246        }
247    }
248}
249
250// A quirk of the intcode program is that commands can't be stacked. We must first read all the
251// output from the previous command before the next command can be submitted.
252fn drain_output(computer: &mut Computer) {
253    while let State::Output(_) = computer.run() {}
254}
255
256/// Convert a normal binary number to its Gray Code equivalent.
257fn gray_code(n: u32) -> u32 {
258    n ^ (n >> 1)
259}