1use std::collections::VecDeque;
2
3mod edge;
4mod shuffle;
5mod weighted_word;
6
7use edge::Edge;
8use rand::{SeedableRng, seq::SliceRandom};
9use rand_chacha::ChaCha20Rng;
10pub use shuffle::Shuffle;
11use weighted_word::WeightedWord;
12
13use crate::Error;
14
15#[derive(Debug)]
16pub struct LettersBoxed {
17 letters: Vec<char>,
18 words: Vec<String>,
19 invalid_pairs: Vec<(char, char)>,
20 word_chain: Vec<String>,
21 edges: Vec<Edge>,
22 max_chain: Option<usize>,
23 shuffle_depth: Option<i8>,
24}
25
26impl Default for LettersBoxed {
27 fn default() -> Self {
28 let letters = vec!['o', 'u', 'h', 'i', 'm', 'a', 'g', 'p', 'l', 'r', 'y', 'f'];
29 let edges = vec![
30 Edge::new('o', 'u', 'h'),
31 Edge::new('i', 'm', 'a'),
32 Edge::new('g', 'p', 'l'),
33 Edge::new('r', 'y', 'f'),
34 ];
35 Self {
36 letters,
37 words: Vec::new(),
38 invalid_pairs: Vec::new(),
39 word_chain: Vec::new(),
40 edges,
41 max_chain: None,
42 shuffle_depth: None,
43 }
44 }
45}
46
47impl LettersBoxed {
48 pub fn new(letters: &[char], words: &[String]) -> Self {
49 let mut s = Self::default();
50 if !letters.is_empty() {
51 s.letters = Vec::from(letters);
52 s.generate_edges();
53 }
54 if !words.is_empty() {
55 s.words = Vec::from(words);
56 }
57
58 s
59 }
60
61 pub fn set_max_chain(&mut self, value: usize) -> &mut Self {
62 self.max_chain = Some(value);
63 self
64 }
65
66 pub fn set_shuffle_depth(&mut self, value: i8) -> &mut Self {
67 self.shuffle_depth = Some(value);
68 self
69 }
70
71 #[tracing::instrument(skip(self))]
72 pub fn filter_words_with_letters_only(&mut self) -> &mut Self {
73 let filtered = self
74 .words
75 .iter()
76 .filter(|word| word.chars().all(|c| self.letters.contains(&c)))
77 .map(|w| w.to_string())
78 .collect::<Vec<String>>();
79
80 tracing::info!("Filtered to {} words", filtered.len());
81 self.words = filtered;
82 self
83 }
84
85 #[tracing::instrument(skip(self))]
86 fn generate_edges(&mut self) -> &mut Self {
87 let mut deque = VecDeque::from(self.letters.clone());
88
89 let mut edges = Vec::new();
90
91 while !deque.is_empty() {
92 let edge = Edge::new(
93 deque.pop_front().unwrap(),
94 deque.pop_front().unwrap(),
95 deque.pop_front().unwrap(),
96 );
97
98 edges.push(edge);
99 }
100
101 self.edges = edges;
102
103 self
104 }
105
106 #[tracing::instrument(skip(self))]
107 fn generate_invalid_pairs(&mut self) -> &mut Self {
108 let mut invalid_pairs = Vec::new();
109 for edge in self.edges.iter() {
110 for pair in edge.pairs() {
111 if !self.invalid_pairs.contains(&pair) {
112 invalid_pairs.push(pair);
113 }
114 }
115 }
116
117 tracing::debug!("Generated {} invalid pairs", invalid_pairs.len());
118 tracing::trace!("Invalid pairs: {:#?}", invalid_pairs);
119 self.invalid_pairs = invalid_pairs;
120
121 self
122 }
123
124 #[tracing::instrument(skip(self))]
125 pub fn filter_exclude_invalid_pairs(&mut self) -> &mut Self {
126 self.generate_invalid_pairs();
127
128 let filtered = self
129 .words
130 .iter()
131 .filter(|word| {
132 let chars = word.chars().collect::<Vec<char>>();
133 let mut a = chars[0];
134 for b in chars.iter().skip(1) {
135 if self.invalid_pairs.contains(&(a, *b))
136 || self.invalid_pairs.contains(&(*b, a))
137 {
138 return false;
139 }
140 a = *b;
141 }
142 true
143 })
144 .map(|w| w.to_string())
145 .collect::<Vec<String>>();
146
147 tracing::info!("Filtered to {} words", filtered.len());
148 self.words = filtered;
149 self
150 }
151
152 #[tracing::instrument(skip(self, shuffle))]
153 pub fn build_word_chain(&mut self, shuffle: &mut Shuffle) -> Result<(), Error> {
154 tracing::info!("Building word chain");
155 let mut rng = ChaCha20Rng::from_os_rng();
157 let all_words = self.words.clone();
158 let words_list = all_words.clone();
159 let word_chain = Vec::new();
160 let unused_letters = String::from_iter(self.letters.clone());
161 let shuffle_depth = self.shuffle_depth.unwrap_or(-1);
162
163 let word_chain = get_word(
164 all_words,
165 words_list,
166 word_chain,
167 unused_letters,
168 &mut rng,
169 shuffle,
170 self.max_chain,
171 shuffle_depth,
172 )?;
173
174 self.word_chain = word_chain;
175 Ok(())
176 }
177
178 #[tracing::instrument(skip(self))]
179 pub fn solution_string(&self) -> String {
180 self.word_chain.join(" -> ").to_string()
181 }
182
183 #[tracing::instrument(skip(self))]
184 pub fn chain_length(&self) -> usize {
185 self.word_chain.len()
186 }
187}
188
189#[allow(clippy::too_many_arguments)]
190#[tracing::instrument(skip(
191 all_words,
192 words_list,
193 word_chain,
194 unused_letters,
195 rng,
196 shuffle,
197 max_chain,
198 shuffle_depth
199))]
200pub fn get_word(
201 all_words: Vec<String>,
202 mut words_list: Vec<String>,
203 mut word_chain: Vec<String>,
204 mut unused_letters: String,
205 rng: &mut ChaCha20Rng,
206 shuffle: &mut Shuffle,
207 max_chain: Option<usize>,
208 mut shuffle_depth: i8,
209) -> Result<Vec<String>, Error> {
210 let initial_unused_letters = unused_letters.clone();
211 tracing::trace!("Starting word chain: {}", word_chain.join(" -> "));
212 tracing::trace!(
213 "Shuffle set to: {} and word list length: {}",
214 shuffle,
215 words_list.len()
216 );
217
218 if shuffle == &Shuffle::Twice {
220 tracing::debug!("Shuffling words list.");
221 words_list.shuffle(rng);
222 }
223 let mut words_list = words_list
224 .iter()
225 .map(|word| WeightedWord::new(word.clone(), unused_letters.clone()))
226 .collect::<Vec<WeightedWord>>();
227 words_list.sort_by_key(|ww| ww.weight);
228 let words_list = words_list
229 .iter()
230 .rev()
231 .map(|ww| ww.word.to_string())
232 .collect::<Vec<String>>();
233
234 let mut words = if shuffle != &Shuffle::None && shuffle_depth != 0 {
236 tracing::debug!("Shuffling top half of weighted words list.");
237 let words_list = shuffle_top_half(words_list, rng);
238 VecDeque::from(words_list)
239 } else {
240 VecDeque::from(words_list)
241 };
242
243 loop {
246 tracing::trace!(
247 "List of {} words starting with: {:?}",
248 words.len(),
249 words.front()
250 );
251 let Some(word) = words.pop_front() else {
252 return Err(Error::NoWordFound);
253 };
254
255 let letter_count = &unused_letters.len();
256 tracing::trace!("Letters unused before check: {}", unused_letters);
257 for letter in word.chars() {
258 if let Some(idx) = unused_letters.find(letter) {
259 unused_letters.remove(idx);
260 }
261 }
262 tracing::trace!("Letters unused after check: {}", unused_letters);
263
264 if unused_letters.is_empty() {
266 word_chain.push(word);
267 break;
268 }
269 tracing::trace!(
270 "Still {} letters unused: {}",
271 unused_letters.len(),
272 unused_letters
273 );
274
275 if unused_letters.len() < *letter_count {
277 if let Some(max_chain) = max_chain {
278 if word_chain.len() >= max_chain {
279 return Err(Error::ChainTooLong);
280 }
281 }
282 let mut next_word_chain = word_chain.clone();
283 let next_unused_letters = unused_letters.clone();
284 let next_all_words = all_words.clone();
285 let last_letter = word.chars().last().unwrap();
286 let words_list = all_words
287 .iter()
288 .filter(|w| w.chars().next().unwrap() == last_letter)
289 .map(|w| w.to_string())
290 .collect::<Vec<String>>();
291 if words.is_empty() {
292 return Err(Error::NoWordFound);
293 }
294 next_word_chain.push(word);
295 if shuffle_depth > 0 {
296 shuffle_depth -= 1;
297 }
298 match get_word(
299 next_all_words,
300 words_list,
301 next_word_chain,
302 next_unused_letters,
303 rng,
304 shuffle,
305 max_chain,
306 shuffle_depth,
307 ) {
308 Ok(chain) => {
309 word_chain = chain;
310 break;
311 }
312 Err(e) => {
313 tracing::debug!("No word found, resetting");
314 if e == Error::ChainTooLong {
315 return Err(e);
316 }
317 unused_letters = initial_unused_letters.clone();
318 continue;
319 }
320 };
321 }
322 }
323
324 tracing::debug!("Current word chain: {}", word_chain.join("-"));
325
326 Ok(word_chain)
327}
328
329fn shuffle_top_half(mut words: Vec<String>, rng: &mut ChaCha20Rng) -> Vec<String> {
330 let half_len = words.len() / 2;
331 let mut top_half = words.drain(..half_len).collect::<Vec<String>>();
332 let bottom_half = words.iter().map(|w| w.to_string()).collect::<Vec<String>>();
333 top_half.shuffle(rng);
334 top_half.extend(bottom_half);
335 top_half
336}
337
338#[cfg(test)]
339mod tests {
340 use std::vec;
341
342 use super::*;
343 #[test]
344 fn test_filter_words_with_letters_only() {
345 let letters = vec!['o', 'u', 'h', 'i', 'd', 'a', 'g', 'e', 'l', 'r', 'y', 'w'];
346 let words = vec![
347 "hello".to_string(),
348 "world".to_string(),
349 "foo".to_string(),
350 "bar".to_string(),
351 "baz".to_string(),
352 ];
353
354 let mut letters_boxed = LettersBoxed::new(&letters, &words);
355 letters_boxed.filter_words_with_letters_only();
356 assert_eq!(letters_boxed.words.len(), 2);
357 assert_eq!(letters_boxed.words[0], "hello".to_string());
358 assert_eq!(letters_boxed.words[1], "world".to_string());
359 }
360
361 #[test]
362 fn test_filter_words_with_invalid_pairs() {
363 let letters = vec!['o', 'u', 'm', 'i', 'd', 'a', 'g', 'e', 'l', 'r', 'y', 'w'];
364 let words = vec![
365 "embolden".to_string(),
366 "world".to_string(),
367 "foo".to_string(),
368 "dire".to_string(),
369 "glow".to_string(),
370 "game".to_string(),
371 "quux".to_string(),
372 "corse".to_string(),
373 "gaunt".to_string(),
374 "grape".to_string(),
375 "waldo".to_string(),
376 "fred".to_string(),
377 ];
378
379 let mut letters_boxed = LettersBoxed::new(&letters, &words);
380
381 println!("{:#?}", letters_boxed.edges);
382
383 letters_boxed.filter_words_with_letters_only();
384 println!("{:#?}", letters_boxed.words);
385
386 letters_boxed.filter_exclude_invalid_pairs();
387 println!("{:#?}", letters_boxed.words);
388
389 assert_eq!(letters_boxed.words.len(), 3);
390 assert_eq!(letters_boxed.words[0], "world".to_string());
391 assert_eq!(letters_boxed.words[1], "game".to_string());
392 assert_eq!(letters_boxed.words[2], "waldo".to_string());
393 }
394
395 #[test]
396 fn test_shuffle_top_half() {
397 let words = vec![
398 "hello".to_string(),
399 "world".to_string(),
400 "foo".to_string(),
401 "bar".to_string(),
402 "baz".to_string(),
403 ];
404 println!("before: {words:?}");
405 let mut rng = ChaCha20Rng::seed_from_u64(1);
406 let shuffled = shuffle_top_half(words, &mut rng);
407 println!("after: {shuffled:?}");
408 assert_eq!(shuffled.len(), 5);
409 assert_eq!(shuffled[0], "world".to_string());
410 assert_eq!(shuffled[1], "hello".to_string());
411 assert_eq!(shuffled[2], "foo".to_string());
412 assert_eq!(shuffled[3], "bar".to_string());
413 assert_eq!(shuffled[4], "baz".to_string());
414 }
415}