util/
words.rs

1// util::words
2
3use std::fs::File;
4use std::io::Read;
5
6use indicatif::ProgressBar;
7
8pub struct Guess {
9    pub value: String,
10    pub entropy: f64,
11}
12
13pub fn get_words() -> Vec<String> {
14    let args: Vec<String> = std::env::args().collect();
15    let mut file;
16    let mut words: String = String::new();
17    let result;
18    if args.len() < 2 {
19        file = match File::open("words.txt") {
20            Ok(f) => f,
21            Err(_) => {
22                println!("When executing ezwordle, you must specify a raw text file containing Wordle's allowed word list.");
23                println!("You may download this file from https://github.com/hobbsbros/ezwordle/blob/main/src/words/words.txt\n");
24                println!("\nFor example: $ ezwordle words.txt\n");
25                println!("If the file words.txt exists within the directory, you do not need to specify words.txt as an argument.");
26                panic!("No word list specified");
27            }
28        };
29        result = file.read_to_string(&mut words);
30    } else {
31        let filename = args[1].clone();
32        let mut file = match File::open(filename) {
33            Ok(f) => f,
34            Err(_) => {
35                println!("ERROR: EZWordle could not find the file specified.\n");
36                panic!("Could not find specified word list file");
37            }
38        };
39        result = file.read_to_string(&mut words);
40    }
41    match result {
42        Ok(_) => {},
43        Err(_) => {
44            println!("ERROR: EZWordle found the file specified but was unable to read it.\n");
45            panic!("Could not read word list file");
46        }
47    }
48    let output: Vec<String> = words.lines().map(String::from).collect();
49    output
50}
51
52// Checks if a guess and given feedback could match to a word in the wordlist
53// Helps to reduce the space of possible words
54pub fn check_match(colored_word: String, combination: String, word_to_check: String) -> bool {
55    // word_to_check refers to a (possible) secret word
56    // colored_word refers to the word that the user has guessed
57    for ((i, letter), truth) in colored_word.chars().enumerate().zip(combination.chars()) {
58        let letter_to_check: char = word_to_check.as_bytes()[i] as char;
59        if truth == '.' {
60            if letter_to_check != letter {
61                return false;
62            }
63        } else if truth == '/' {
64            if !word_to_check.contains(letter) || letter_to_check == letter {
65                return false;
66            }
67        } else if truth == 'x' {
68            if word_to_check.contains(letter) {
69                return false;
70            }
71        }
72    }
73    return true;
74}
75
76// Helper function: get all words that match a combination in the wordlist
77pub fn get_matches(colored_word: String, wordlist: Vec<String>, combination: String) -> Vec<String> {
78    let mut words: Vec<String> = Vec::new();
79    for word in wordlist.iter() {
80        let chk: bool = check_match(colored_word.clone(), combination.clone(), word.to_string().clone());
81        if chk {
82            words.push(word.to_string());
83        }
84    }
85    words
86}
87
88// Gets a list of all possible output combinations
89pub fn get_all_combinations(len: u8) -> Vec<String> {
90    let mut result: Vec<String> = Vec::new();
91    
92    // BASE CASE
93    if len == 1 {
94        return vec!["x", "/", "."].into_iter().map(String::from).collect::<Vec<String>>();
95    }
96
97    // RECURSIVE CASE
98    for sub in get_all_combinations(len - 1) {
99        result.push(sub.clone() + "x");
100        result.push(sub.clone() + "/");
101        result.push(sub.clone() + ".");
102    }
103    result
104}
105
106// Computes the contribution to information entropy of a given colored word & combination
107pub fn compute_contribution(colored_word: String, wordlist: Vec<String>, combination: String) -> f64 {
108    let new_wordlist = get_matches(colored_word.clone(), wordlist.clone(), combination.clone());
109    if new_wordlist.len() == 0 {
110        return 0.0;
111    }
112    let p: f64 = (new_wordlist.len() as f64)/(wordlist.len() as f64);
113    let mut logp: f64 = 0.0;
114    if p > 0.0 {
115        logp = p.ln();
116    }
117    return -p * logp;
118}
119
120// Computes the expected amount of information (information entropy) from a given guess
121pub fn compute_entropy(colored_word: String, wordlist: Vec<String>) -> f64 {
122    let combos: Vec<String> = get_all_combinations(5);
123    let mut entropy: f64 = 0.0;
124    for combo in combos {
125        entropy += compute_contribution(colored_word.clone(), wordlist.clone(), combo.clone());
126    }
127    entropy
128}
129
130// Guesses a word and returns the reduced wordlist
131pub fn guess(wordlist: Vec<String>, verbose: bool) -> (String, f64) {
132    let num_of_words = wordlist.len();
133    let mut guesses: Vec<Guess> = Vec::new();
134
135    let bar = ProgressBar::new(num_of_words as u64);
136
137    if verbose {
138        println!("Searching {} words for the optimal guess...", num_of_words);
139    }
140
141    for word in wordlist.clone().into_iter() {
142        guesses.push(Guess {value: word.clone(), entropy: compute_entropy(word.clone(), wordlist.clone())});
143        if verbose {bar.inc(1)}
144    }
145    if verbose {
146        bar.finish();
147        println!("Done searching!\n");
148    }
149
150    let sorted_guesses = &mut guesses[..];
151    if sorted_guesses.len() == 0 {
152        return (String::new(), 0.0);
153    }
154    sorted_guesses.sort_by(|x, y| y.entropy.partial_cmp(&x.entropy).unwrap());
155
156    let top_guess = sorted_guesses[0].value.clone();
157    let guess_entropy = sorted_guesses[0].entropy;
158
159    return (top_guess.to_string(), guess_entropy);
160}