lib_wpsr/boxed/
solution.rs1use 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 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}