I like Wordle and Wordle like games. I don’t find them that difficult, on the whole, but thought I’d make a Wordle solver. The easiest solution would be to use regular expressions and a word list. The idea being to enter a word in the form:
ch...
Where the full stops equal any letter and get all matches for that pattern. So ‘ch...’ would match chafe, chaff through to chasm, minus any words with letters that didn’t match at all. Part of the fun of Wordle, which makes it different than checking each word in turn, is that it gives feedback about letters that are in the word but not at that position, so if I entered ‘caulk’ as a starting word Wordle may return:
c!a.lk
I’ve used the exclamation mark to mean not. So ‘c!a.lk’ can be translated as “c followed by ‘not a here, but somewhere’ followed by any remaining letters followed by k”.
My favourite programming language is Rust because it stops me doing inherently stupid stuff that works (logic errors and otherwise sub-optimal choices, alas, are just fine). I don't know it that well, and Rust has a fine regular expression library that is very fast. There is a wonderful book called Mastering Regular Expressions published by O’Reilly that I last read in the late 1990s. I really didn’t fancy brushing up on regular expressions unless I had to. I’ve have a very basic understanding of regular expressions and I know I’d ideally use positive and negative lookaheads. Which are not features of Rust’s regular expressions library because they’re quite slow. There are several fairly easy workarounds including building a paragraph sized regular expression or chaining multiple regular expressions together. That didn’t seem that cool. Problems like this really underline just how cool our brains are and the point of machine learning.
So I decided to do it in pure Rust and make a library. It’s very small and annotated. This is the lib.rs file:
#[derive(Debug)]
pub struct CandidateWord {
potentialletters: Vec<String>,
matchpattern: String,
mustcontain: String,
wordlength: usize,
}
impl CandidateWord {
// Usage new(remaining letters (string), match pattern (string), length (number of letters))
// match format l = literal letter or !l matched but not this position or . any letter so 'ch!l.k' means
// match 'c', match 'h', not 'l' here, any renaining letter here, match 'c'
pub fn new(remaining: String, mpattern: String, length: usize) -> CandidateWord {
// holds potential letters for matching by position
let mut p_letters: Vec<String> = Vec::with_capacity(length);
// push remaining letters into a vector of potential matches
for _ in 0..length {
p_letters.push(remaining.clone());
}
// Return candidate word struct
CandidateWord {
potentialletters: p_letters,
matchpattern: mpattern,
mustcontain: String::with_capacity(length),
wordlength: length,
}
}
// parse match pattern over each letters potential matches, replace potential matches with existing matches
pub fn parse(&mut self) {
let mut negationcode = false; // has the negation code been invoked?
let mut count = 0; // clumsy vector index
for command in self.matchpattern.chars() {
if count==self.wordlength {
break;
}
if !negationcode {
if command == '!' { // check for negation code
negationcode = true;
continue; // restart loop
} else if command != '.' {
self.potentialletters[count] = String::from(command);
}
} else { // else if negationcode == true {
self.potentialletters[count].retain(|c| !format!("{}", command).contains(c)); // remove letter from porential matches
self.mustcontain.push(command); // but it must match somewhere so add it to a list of letters it *must* contain
negationcode = false; // done everything with the negation codde so set false
count+=1; // increment the count/index by one
continue; // restart the loop
}
count+=1;
}
}
pub fn check_word_str(&mut self, input_word: &str) -> bool {
// if the word doesn't contain at a minimum all letters return false
for x in self.mustcontain.chars() {
if !input_word.contains(x) {
return false;
}
}
// check each letter from the input word is contained in the potential letters
// if eveey letter matches a potential letter return true
for (x,y) in self.potentialletters.iter().zip(input_word.chars()) {
if !x.contains(y) {
return false;
}
}
true
}
}
It works for any Wordle style puzzle of any wordlength or, given a suitable word list, any language compatible with the Rust chars function. Here’s an example program that will match ‘joyed’ in a 5 letter word list (this is the main.rs file - which was part of a cargo new wordle_solver_b):
use wordle_solver_b::CandidateWord;
use std::fs::File;
use std::io::{BufReader,Read};
fn main() {
let mut _test = CandidateWord::new("qeyuodfjkzxb".to_string(),".o!d.!e".to_string(), 5);
let wordlist_file;
match File::open("wordlist_five_letters.txt") {
Ok(x) => {
wordlist_file=x;
}
Err(y) => {
panic!("{}",y)
}
};
let mut reader=BufReader::new(wordlist_file);
let mut wordlist = String::with_capacity(0);
let _ = reader.read_to_string(&mut wordlist);
_test.parse();
for candidate in wordlist.lines() {
if _test.check_word_str(candidate) {
println!("{}", &candidate);
}
}
}
Obviously no warranty provided and it does no input checking. If interacting with people it needs all of that stuff.
back© John B Everitt