lib_wpsr/boxed/
solution.rs

1use std::collections::HashMap;
2
3use colorful::Colorful;
4
5use crate::{DEFAULT_BOXED_SOURCE_FILE, DEFAULT_SOURCE_DIR, Error};
6
7pub use letters_boxed::{LettersBoxed, Shuffle};
8
9use super::Shape;
10
11mod letters_boxed;
12
13#[derive(Debug, Default)]
14pub struct Solution {
15    settings: HashMap<String, String>,
16    letters: Vec<char>,
17    word_source: String,
18    words: Vec<String>,
19    max_chain: usize,
20    shuffle_depth: i8,
21    solutions: Vec<String>,
22    distribution: HashMap<usize, i32>,
23}
24
25impl Solution {
26    pub fn new(letters: &str, settings: HashMap<String, String>) -> Result<Self, Error> {
27        if letters.len() < 9 || letters.len() > 24 {
28            return Err(Error::TooFewOrManyLetters(letters.len()));
29        }
30
31        if !(letters.len() % 3) == 0 {
32            return Err(Error::MustBeDivisibleBy3(letters.len()));
33        }
34
35        let letters = letters
36            .chars()
37            .map(|l| l.to_ascii_lowercase())
38            .collect::<Vec<char>>();
39
40        Ok(Self {
41            settings,
42            letters,
43            max_chain: 10,
44            shuffle_depth: 3,
45            ..Default::default()
46        })
47    }
48
49    pub fn set_word_source(&mut self, dir: Option<String>, file: Option<String>) -> &mut Self {
50        // Setup settings
51        let mut src_directory = self
52            .settings
53            .get("source_dir")
54            .map_or(DEFAULT_SOURCE_DIR, |v| v)
55            .to_string();
56        let mut src_file = self
57            .settings
58            .get("source_boxed_file")
59            .map_or(DEFAULT_BOXED_SOURCE_FILE, |v| v)
60            .to_string();
61
62        if let Some(sd) = dir {
63            src_directory = sd;
64        };
65        if let Some(sf) = file {
66            src_file = sf;
67        };
68
69        let src = format!("{}/{}", src_directory.clone(), src_file.clone());
70        tracing::info!("Using word list: {}", src);
71
72        self.word_source = src;
73
74        self
75    }
76
77    pub fn load_words(&mut self) -> &mut Self {
78        let mut words = Vec::new();
79
80        for line in std::fs::read_to_string(&self.word_source)
81            .expect("Failed to read words file")
82            .lines()
83        {
84            if !line.is_empty() {
85                let ws = line.split_whitespace();
86                for w in ws {
87                    words.push(w.to_string());
88                }
89            }
90        }
91
92        self.words = words;
93
94        self
95    }
96
97    pub fn set_max_chain(&mut self, value: usize) -> &mut Self {
98        self.max_chain = value;
99        self
100    }
101
102    pub fn set_shuffle_depth(&mut self, value: i8) -> &mut Self {
103        self.shuffle_depth = value;
104        self
105    }
106
107    pub fn find_best_solution(&mut self) -> Result<&mut Self, Error> {
108        tracing::info!("Get un-shuffled word list");
109        let mut shuffle = Shuffle::None;
110        let mut puzzle = LettersBoxed::new(&self.letters, &self.words);
111        match puzzle
112            .filter_words_with_letters_only()
113            .filter_exclude_invalid_pairs()
114            .set_max_chain(self.max_chain)
115            .build_word_chain(&mut shuffle)
116        {
117            Ok(_) => {
118                tracing::info!("Word chain built successfully");
119                self.solutions.push(puzzle.solution_string());
120                self.count_solution(puzzle.chain_length());
121            }
122            Err(e) => {
123                tracing::error!("Failed to build word chain: {}", e);
124            }
125        };
126
127        Ok(self)
128    }
129
130    #[tracing::instrument(skip(self))]
131    pub fn find_random_solution(&mut self, mut shuffle: Shuffle) -> Result<&mut Self, Error> {
132        tracing::info!("Get un-shuffled word list");
133        let mut puzzle = LettersBoxed::new(&self.letters, &self.words);
134        match puzzle
135            .filter_words_with_letters_only()
136            .filter_exclude_invalid_pairs()
137            .set_max_chain(self.max_chain)
138            .set_shuffle_depth(self.shuffle_depth)
139            .build_word_chain(&mut shuffle)
140        {
141            Ok(_) => {
142                tracing::info!("Word chain built successfully");
143                if self.solutions.contains(&puzzle.solution_string()) {
144                    tracing::info!("Solution already found");
145                    return Err(Error::SolutionAlreadyFound);
146                }
147                self.solutions.push(puzzle.solution_string());
148                self.count_solution(puzzle.chain_length());
149            }
150            Err(e) => {
151                tracing::error!("Failed to build word chain: {}", e);
152                return Err(e);
153            }
154        };
155
156        Ok(self)
157    }
158
159    pub fn count_solution(&mut self, chain_length: usize) -> &mut Self {
160        if let Some(count) = self.distribution.get(&chain_length) {
161            let v = count + 1;
162            self.distribution.insert(chain_length, v);
163        } else {
164            self.distribution.insert(chain_length, 1);
165        }
166
167        self
168    }
169
170    pub fn shape_len(&self) -> usize {
171        match Shape::from_edges((self.letters.len() / 3) as u8) {
172            Ok(shape) => shape.to_string().len(),
173            Err(_) => "Unknown shape".to_string().len(),
174        }
175    }
176
177    pub fn shape_string(&self) -> String {
178        match Shape::from_edges((self.letters.len() / 3) as u8) {
179            Ok(shape) => shape.to_string().bold().light_blue().to_string(),
180            Err(_) => "Unknown shape".to_string(),
181        }
182    }
183
184    pub fn word_source_string(&self) -> String {
185        let s1 = "Using words sourced from ".light_cyan().dim().to_string();
186        let s2 = self.word_source.clone().light_cyan().bold().to_string();
187        format!("{s1}{s2}")
188    }
189
190    pub fn distribution_string(&self) -> String {
191        let mut s = String::new();
192        let mut distributions = self.distribution.iter().collect::<Vec<_>>();
193        distributions.sort_by(|a, b| a.0.cmp(b.0));
194
195        for d in distributions {
196            s.push_str(&format!(
197                "  - {:3.0} solutions with {:2.0} words\n",
198                d.1, d.0
199            ));
200        }
201        s
202    }
203
204    pub fn solutions_title(&self) -> String {
205        let intro = "Solutions for ";
206        let mut ul = String::new();
207        for _ in 0..(intro.len() + self.shape_len()) {
208            ul.push('‾');
209        }
210
211        let summary = format!("{}{}", intro.yellow().bold(), self.shape_string());
212        format!("{}\n{}", summary, ul.bold().yellow())
213    }
214
215    pub fn solve_title(&self) -> String {
216        let intro = "Solutions for ";
217        let mut ul = String::new();
218        for _ in 0..(intro.len() + self.shape_len()) {
219            ul.push('‾');
220        }
221
222        let summary = format!("{}{}", intro.yellow().bold(), self.shape_string(),);
223
224        format!("{}\n{}", summary, ul.bold().yellow())
225    }
226
227    pub fn solutions_string(&self) -> String {
228        let mut s = String::new();
229        let mut solutions = self
230            .solutions
231            .iter()
232            .map(|s| {
233                let words = s.chars().filter(|c| *c == '>').count() + 1;
234                (words, s)
235            })
236            .collect::<Vec<_>>();
237        solutions.sort_by(|a, b| a.0.cmp(&b.0));
238
239        let mut word_length = solutions.first().unwrap_or(&(0, &"".to_string())).0;
240
241        s.push_str("  ");
242        s.push_str(
243            &format!(
244                "{} Solutions with {} words.",
245                self.distribution.get(&word_length).unwrap_or(&0),
246                word_length
247            )
248            .underlined()
249            .yellow()
250            .to_string(),
251        );
252        s.push_str("\n\n");
253
254        for solution in solutions {
255            if solution.0 != word_length {
256                word_length = solution.0;
257                s.push_str("\n  ");
258                s.push_str(
259                    &format!(
260                        "{} Solutions with {} words.",
261                        self.distribution.get(&word_length).unwrap_or(&0),
262                        word_length
263                    )
264                    .underlined()
265                    .yellow()
266                    .to_string(),
267                );
268                s.push_str("\n\n");
269            }
270            s.push_str(&format!("    {}\n", solution.1));
271        }
272        s
273    }
274}