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#[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 self.explore(None);
66
67 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 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) []);