boggle_maker/
boggle_board_solver.rs

1use std::collections::{HashSet, HashMap};
2use word_trie::trie::Trie;
3use word_trie::TrieBuilder;
4use crate::boggle_dfs::{WordVisitor,BoggleDfsContext,BoggleDfs,get_word_score};
5use crate::boggle_board::Board;
6
7/// Boggle board result after dfs search
8pub struct BoggleBoardResult {
9    words: HashSet<String>,
10    path_tracks: Vec<Vec<u16>>,
11    counts: Vec<u32>,
12    length_map: HashMap<usize, usize>,
13}
14
15impl BoggleBoardResult {
16    /// Initiate a new instance of BoggleBoardResult.
17    pub fn new() -> Self {
18        Self{
19            words: HashSet::new(),
20            path_tracks: Vec::new(),
21            counts: vec![0;12], 
22            length_map: HashMap::new(),
23        }
24    }
25
26    /// gets the result for query of how many words exists in the board with specefic score.
27    pub fn how_many(&self, score: usize) -> u32 {        
28        if score > 11 { 0 } else { self.counts[score] }        
29    }
30
31    /// gets a refrence to all score counts vector
32    pub fn score_counts(&self) -> &Vec<u32> {
33        &self.counts
34    }
35
36    /// gets a reference to word length' count map.
37    pub fn len_counts(&self) -> &HashMap<usize,usize> {
38        &self.length_map
39    }
40
41    /// gets the board's all words hash set
42    pub fn words(&self) -> &HashSet<String> {
43        &self.words
44    }
45
46    /// gets a vector of all valid paths in the boggle board.
47    pub fn path_tracks(&self) -> &Vec<Vec<u16>> {
48        &self.path_tracks
49    }
50
51    pub(crate) fn add_word(&mut self, word: String){
52        //update the score map
53        let score = get_word_score(&word);
54        self.inc_score(score);       
55        
56        let word_len = word.len();
57        let entry = self.length_map.entry(word_len).or_insert(0);
58        *entry += 1;
59
60        //consume the word memory, this should be last statement
61        self.words.insert(word);
62    }
63
64    pub(crate) fn add_path(&mut self, path: Vec<u16>){
65        self.path_tracks.push(path);
66    }
67
68    fn inc_score(&mut self, score: u32){
69        Self::score_guard(score);
70        self.counts[score as usize] += 1;
71    }
72
73    fn score_guard(score: u32){
74        if score > 12 {
75            panic!("the functionality has not been implemented for score bigger than 11");
76        }
77    }
78}
79
80struct BoggleBoardSolverVisitor(BoggleBoardResult);
81
82impl WordVisitor for BoggleBoardSolverVisitor {
83    fn visit(&mut self, word: &str, path: &Vec<u16>){
84
85        //add the path to path_tracks
86        self.0.add_path(path.to_vec());
87
88        if self.0.words().contains(word) {
89            return;
90        }
91
92        //add the word to visited list
93        self.0.add_word(word.to_string());
94    }
95}
96
97/// the boggle board solver struct
98#[derive(Default)]
99pub struct BoggleBoardSolver (Option<Trie>); 
100
101impl BoggleBoardSolver {
102    /// gets new instance of BoggleBoardSolver
103    pub fn new() -> Self {
104        Self::default()
105    }
106
107    ///sets the trie dictionary text file path
108    pub fn with_dictionary<P: Into<String>>(mut self, path: P) -> Result<Self, std::io::Error> {        
109        self.0 = Some(TrieBuilder::new()
110        .from_file(path.into())
111        .expect("Failed to load trie from file"));
112        
113        Ok(self)
114    } 
115
116    /// solve a vector representing the boggle board
117    pub fn solve_vec(&self, board: &Vec<char>, width: usize, length: usize) -> Option<BoggleBoardResult> {
118        match &self.0 {
119            Some(trie) => {
120                let context = BoggleDfsContext::new(&trie, width, length);
121
122                if board.len() != context.count() {
123                    panic!("The board size must be fit to the length:{0} and width:{1}", length, width);
124                }
125
126                let mut visitor = BoggleBoardSolverVisitor(BoggleBoardResult::new());
127                BoggleDfs::new(&context, board).with_visitor(&mut visitor).search();
128
129                Some(visitor.0)
130            },
131            None => None
132        }
133    }
134
135    /// solve a boggle board
136    pub fn solve(&self, board: &Board) -> Option<BoggleBoardResult> {
137        self.solve_vec(board.value(), board.width(), board.length())
138    }
139}
140
141#[cfg(test)]
142pub mod tests {
143    use super::*;
144    use crate::builder::BoggleBuilder;
145
146    fn get_sample_solver() -> Result<BoggleBoardSolver, std::io::Error> {
147        BoggleBoardSolver::new().with_dictionary("words.txt")
148    }
149
150    fn get_sample_board() -> Vec<char> {
151        vec!['S','E','R','S','P','A','T','G','L','I','N','E','S','E','R','S']
152    }
153
154    fn solve_sample_board() -> BoggleBoardResult {
155        let solver = get_sample_solver().unwrap();
156        let board = get_sample_board();
157
158        let result = solver.solve_vec(&board, 4, 4);
159        assert!(result.is_some(), "the result does not have value");
160
161        result.unwrap()
162    }
163
164    #[test]
165    fn sample_solver_loads_successfully(){
166        let solver = get_sample_solver();
167        assert!(solver.is_ok(), "Failed to load trie from file");
168    }
169
170    #[test]
171    fn can_solve_sample_board(){
172        let _ = solve_sample_board();
173    }
174
175    #[test]
176    fn there_should_be_atleast_10_words_with_score_3(){
177        let result = solve_sample_board();
178        let three_count = result.how_many(3);
179        assert!(three_count>10);
180        println!("There are {three_count} words with score equal to 3");
181    }
182
183    #[test]
184    fn there_should_be_no_words_with_score_4(){
185        let result = solve_sample_board();
186        let four_count = result.how_many(4);
187        assert_eq!(four_count,0);
188        println!("There are {four_count} words with score equal to 4");
189    }
190
191    #[test]
192    fn there_should_be_some_words_with_3_scores_for_a_board_generated(){
193        let solver = get_sample_solver().unwrap();
194
195        let board = BoggleBuilder::new()
196                .with_dictionary_path("words.txt")
197                .with_target_score(3000)
198                .with_length(4)
199                .with_width(4)
200                .build()
201                .unwrap();
202        assert!(board.is_some());
203        let board = board.unwrap();
204        
205        let result = solver.solve(&board).unwrap();
206        let three_count = result.how_many(3);
207        assert!(three_count > 0);
208        println!("There are {three_count} words with score equal to 3");
209    }
210
211    #[test]
212    fn there_should_a_word_with_length_7(){
213        let result = solve_sample_board();
214        let key = 7;
215        assert!(result.len_counts().contains_key(&key));
216        println!("There are {} words with length equal to 7", result.len_counts()[&key]);
217    }
218}