Skip to main content

year2019/
day25.rs

1use crate::intcode::features::Day09Features;
2use crate::intcode::{Event, Interpreter};
3use core::assert_matches;
4use utils::array::ArrayVec;
5use utils::bit::BitIterator;
6use utils::geometry::Direction;
7use utils::prelude::*;
8
9/// Interpreting machine code to complete a text adventure game.
10///
11/// This solution makes many assumptions derived from running the puzzle input.
12#[derive(Clone, Debug)]
13pub struct Day25 {
14    interpreter: Interpreter,
15}
16
17const DIRECTIONS: [&str; 4] = ["north", "east", "south", "west"];
18const DOORS_HEADER: &str = "Doors here lead:";
19const ITEMS_HEADER: &str = "Items here:";
20const TOO_LIGHT: &str = "Droids on this ship are heavier than the detected value";
21const TOO_HEAVY: &str = "Droids on this ship are lighter than the detected value";
22const CODE_PREFIX: &str = "typing ";
23const CODE_SUFFIX: &str = " on the keypad at the main airlock";
24
25impl Day25 {
26    pub fn new(input: &str, _: InputType) -> Result<Self, InputError> {
27        Ok(Self {
28            interpreter: Interpreter::parse(input, 1)?,
29        })
30    }
31
32    #[must_use]
33    pub fn part1(&self) -> u64 {
34        Searcher {
35            interpreter: self.interpreter.clone(),
36            output: String::with_capacity(512),
37            items: Vec::new(),
38            path: Vec::new(),
39            floor_route: Vec::new(),
40        }
41        .search()
42    }
43
44    #[must_use]
45    pub fn part2(&self) -> &'static str {
46        "🎄"
47    }
48}
49
50#[derive(Debug)]
51struct Searcher {
52    interpreter: Interpreter,
53    output: String,
54    items: Vec<String>,
55    path: Vec<Direction>,
56    floor_route: Vec<Direction>,
57}
58
59impl Searcher {
60    fn search(&mut self) -> u64 {
61        self.read_output();
62
63        // Explore the whole map, taking every safe item and recording the route to the
64        // pressure-sensitive floor
65        self.explore(None);
66
67        // Follow the route back to the room before the pressure-sensitive floor
68        let mut route = std::mem::take(&mut self.floor_route);
69        let Some(floor_direction) = route.pop() else {
70            panic!("expected to find the pressure-sensitive floor")
71        };
72        for direction in route {
73            self.move_command(direction);
74        }
75
76        assert!(
77            self.items.len() < u64::BITS as usize,
78            "expected fewer inventory items"
79        );
80        let combinations = 1u64 << self.items.len();
81
82        // Drop subsets of the inventory until the weight is accepted.
83        // Skip sets that are supersets of a set known to be too light or subsets of one known to be
84        // too heavy. Trying sets in Gray code order means that each set differs from the previous
85        // one by one bit, increasing the number of subset/superset matches and skipped sets.
86        let mut dropped = 0;
87        let mut too_heavy = Vec::<u64>::new();
88        let mut too_light = Vec::<u64>::new();
89        for step in 0..combinations {
90            let next = step ^ (step >> 1);
91            if too_light.iter().any(|&mask| mask & !next == 0)
92                || too_heavy.iter().any(|&mask| next & !mask == 0)
93            {
94                continue;
95            }
96
97            for (item, _) in BitIterator::ones(next & !dropped) {
98                self.drop_item(item as usize);
99            }
100            for (item, _) in BitIterator::ones(dropped & !next) {
101                self.take_item(item as usize);
102            }
103            dropped = next;
104
105            self.move_command(floor_direction);
106
107            if let Some((_, rest)) = self.output.split_once(CODE_PREFIX)
108                && let Some((code, _)) = rest.split_once(CODE_SUFFIX)
109                && let Ok(code) = code.parse()
110            {
111                return code;
112            } else if self.output.contains(TOO_LIGHT) {
113                too_light.push(dropped);
114            } else if self.output.contains(TOO_HEAVY) {
115                too_heavy.push(dropped);
116            } else {
117                panic!("unexpected output: {}", self.output);
118            }
119        }
120
121        panic!("no solution found")
122    }
123
124    fn explore(&mut self, entered: Option<Direction>) {
125        if let Some(direction) = entered {
126            self.move_command(direction);
127            if self.output.contains(TOO_LIGHT) || self.output.contains(TOO_HEAVY) {
128                self.floor_route.clone_from(&self.path);
129                return;
130            }
131        }
132
133        let mut lines = self.output.lines();
134        let mut doors = ArrayVec::<Direction, 4>::new();
135        if lines.any(|line| line == DOORS_HEADER) {
136            for line in lines.by_ref() {
137                let Some(value) = line.strip_prefix("- ") else {
138                    break;
139                };
140
141                let Some(direction) = DIRECTIONS
142                    .iter()
143                    .position(|&x| x == value)
144                    .map(|x| Direction::from(x as u8))
145                else {
146                    panic!("unexpected door: {value}");
147                };
148
149                if entered.is_none_or(|entered| direction != !entered) {
150                    doors.push(direction).unwrap();
151                }
152            }
153        }
154
155        let mut room_items = Vec::new();
156        if lines.any(|line| line == ITEMS_HEADER) {
157            for line in lines {
158                let Some(item) = line.strip_prefix("- ") else {
159                    break;
160                };
161                if !Self::dangerous(item) {
162                    room_items.push(item.to_owned());
163                }
164            }
165        }
166
167        for item in room_items {
168            self.items.push(item);
169            self.take_item(self.items.len() - 1);
170        }
171
172        for &direction in &doors {
173            self.path.push(direction);
174            self.explore(Some(direction));
175            self.path.pop();
176        }
177
178        if let Some(direction) = entered {
179            self.move_command(!direction);
180        }
181    }
182
183    fn move_command(&mut self, direction: Direction) {
184        self.interpreter.push_bytes(DIRECTIONS[direction as usize]);
185        self.interpreter.push_bytes("\n");
186
187        self.read_output();
188        assert!(
189            self.output.trim_ascii_start().starts_with("== "),
190            "unexpected output after move command:\n{}",
191            self.output
192        );
193    }
194
195    fn take_item(&mut self, item: usize) {
196        self.item_command("take", item);
197    }
198
199    fn drop_item(&mut self, item: usize) {
200        self.item_command("drop", item);
201    }
202
203    fn item_command(&mut self, action: &str, item: usize) {
204        self.interpreter.push_bytes(action);
205        self.interpreter.push_bytes(" ");
206        self.interpreter.push_bytes(&self.items[item]);
207        self.interpreter.push_bytes("\n");
208
209        self.read_output();
210        assert!(
211            self.output
212                .trim_ascii_start()
213                .strip_prefix("You ")
214                .and_then(|rest| rest.strip_prefix(action))
215                .and_then(|rest| rest.strip_prefix(" the "))
216                .is_some_and(|rest| rest.starts_with(&self.items[item])),
217            "unexpected output after item command: {}",
218            self.output
219        );
220    }
221
222    fn read_output(&mut self) {
223        self.output.clear();
224        while let Event::Output(x) = self.interpreter.run::<Day09Features>() {
225            assert_matches!(x, 0..=127, "expected ascii output");
226            self.output.push(x as u8 as char);
227        }
228    }
229
230    fn dangerous(item: &str) -> bool {
231        matches!(
232            item,
233            "escape pod" | "giant electromagnet" | "infinite loop" | "molten lava" | "photons"
234        )
235    }
236}
237
238examples!(Day25 -> (u64, &'static str) []);