use std::{
collections::HashMap,
io::{self, Write},
};
use clap::{Parser, Subcommand};
use env_logger::{Builder, Target};
use itertools::Itertools;
use log::{debug, info, LevelFilter};
use prompter::*;
const WORD_LEN: usize = 5;
const ROUND_NUM: usize = 6;
#[derive(Parser)]
#[clap(name = "prompter")]
#[clap(about = "A Wordle solver in Rust", long_about = None)]
struct Cli {
#[clap(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Play {},
Simulate {
#[clap(long, short, value_name = "WORD")]
start: Option<String>,
#[clap(long, short, requires = "start", value_name = "WORD")]
target: Option<String>,
},
Buckets {
#[clap(value_name = "WORD")]
word: String,
},
}
fn main() {
let args = Cli::parse();
match &args.command {
Commands::Play {} => {
play();
}
Commands::Simulate { start, target } => {
let mut builder = Builder::new();
builder
.format(|buf, record| writeln!(buf, "{}", record.args()))
.target(Target::Stdout);
let level = match (start, target) {
(Some(_), Some(_)) => LevelFilter::Debug,
(None, None) => LevelFilter::Warn,
(_, _) => LevelFilter::Info,
};
builder.filter_level(level);
builder.init();
simulate_all(start.as_ref(), target.as_ref());
}
Commands::Buckets { word } => {
let word = Word::from(word);
let wordlist = Wordlist::load();
let mut map = HashMap::new();
for w in &wordlist {
let code = word.match_code(w);
let vec = map.entry(code).or_insert_with(Vec::new);
vec.push(w);
}
println!("\"{}\" has {} Wordle buckets.", word, map.len());
for (code, words) in map.iter().sorted() {
println!("\n{} ({} word{})", code, words.len(), plural(words.len()));
for w in words {
println!("{}", w);
}
}
}
}
}
fn plural(number: usize) -> String {
let s = if number == 1 { "" } else { "s" };
s.to_string()
}
fn play() {
println!("Welcome! Let's play Wordle.");
let mut wordlist = Wordlist::load();
for i in 1..=ROUND_NUM {
println!(
"\n---[ Round #{} ]------------------------------------------------",
i
);
let w_count = wordlist.len();
println!("\n{} candidate word{} left.", w_count, plural(w_count));
let start = std::time::Instant::now();
let candidates = wordlist.rank_words();
let duration = start.elapsed();
println!("\nTop candidate word{}:", plural(w_count));
for (w, score) in candidates.take(10) {
println!("{} ({})", w, score);
}
debug!("\nTime elapsed for word ranking: {:?}", duration);
if wordlist.len() == 1 {
println!("\nCongratulations! You won after {} round{}.", i, plural(i));
break;
}
let mut word = get_user_word(i);
while let Err(error) = word {
println!("\nError: {}", error);
word = get_user_word(i);
}
let mut constraints = get_contraints(word.as_ref().unwrap());
while let Err(error) = constraints {
println!("\nError: {}", error);
constraints = get_contraints(word.as_ref().unwrap());
}
if constraints.as_ref().unwrap().correct_word() {
println!("\nCongratulations! You won after {} round{}.", i, plural(i));
break;
}
wordlist = Wordlist::from_iter(wordlist.filter(&constraints.unwrap()));
wordlist.remove(word.as_ref().unwrap());
if wordlist.len() > 1 && i == ROUND_NUM {
println!("\n{} candidate words left.", wordlist.len());
println!("\nGame over.");
break;
}
if wordlist.is_empty() {
println!("\nSomething went wrong. There are no matching words left.");
break;
}
}
}
fn simulate(start: &Word, target: &Word) -> Option<usize> {
let mut wordlist = Wordlist::load();
debug!("{} -> {}", start, target);
for i in 1..=ROUND_NUM {
debug!(
"\n---[ Round #{} ]------------------------------------------------",
i
);
let w_count = wordlist.len();
debug!("\n{} candidate word{} left.", w_count, plural(w_count));
let w = match i {
1 => start,
_ => wordlist.rank_words().next().unwrap().0,
};
debug!("Top candidate word: {}", w);
if wordlist.len() == 1 {
debug!("\nI won after {} round{}.", i, plural(i));
return Some(i);
}
let w_string = w.to_string();
let color_code = w.match_code(target);
debug!("Wordle hint: {}", color_code);
let constraints = ConstraintSet::try_from((w_string.as_ref(), color_code.as_ref()));
if constraints.as_ref().unwrap().correct_word() {
debug!("\nI won after {} round{}.", i, plural(i));
return Some(i);
}
wordlist = Wordlist::from_iter(wordlist.filter(&constraints.unwrap()));
wordlist.remove(&w_string);
if wordlist.len() > 1 && i == ROUND_NUM {
debug!("\n{} candidate words left.", wordlist.len());
debug!("\nGame over.");
break;
}
}
None
}
fn word_iter<'a>(
word_opt: Option<&'a Word>,
wordlist: &'a Wordlist,
) -> impl Iterator<Item = &'a Word> {
let iter = if word_opt.is_none() {
Some(wordlist.iter())
} else {
None
};
iter.into_iter().flatten().chain(word_opt)
}
fn simulate_all(start: Option<&String>, target: Option<&String>) {
let wordlist = Wordlist::load();
let start_word = start.map(Word::from);
let start_words = word_iter(start_word.as_ref(), &wordlist);
for s in start_words {
let mut scores = Vec::with_capacity(wordlist.len());
let target_word = target.map(Word::from);
let target_words = word_iter(target_word.as_ref(), &wordlist);
for t in target_words {
if let Some(score) = simulate(s, t) {
scores.push(score);
info!("{} -> {}: Won after {} round{}", s, t, score, plural(score));
} else {
info!("{} -> {}: Lost", s, t);
}
}
if !(start.is_some() && target.is_some()) {
print_results(s, scores.iter().sum(), scores.len(), wordlist.len());
}
}
}
fn print_results(start_word: &Word, total_score: usize, won_count: usize, game_count: usize) {
let won_percentage = won_count as f32 / game_count as f32 * 100.0;
let avg_score = total_score as f32 / won_count as f32;
println!(
"With start word \"{}\", I won {} / {} games ({:.2} %) in on average {:.2} rounds.",
start_word, won_count, game_count, won_percentage, avg_score
)
}
fn user_input() -> String {
let mut buffer = String::new();
print!("> ");
io::stdout().flush().unwrap();
io::stdin().read_line(&mut buffer).unwrap();
buffer.trim().to_string()
}
fn get_user_word(i: usize) -> Result<String, InputError> {
println!(
"\nPlease enter your {} word.",
if i == 1 { "first" } else { "next" }
);
let word = user_input();
if word.len() != WORD_LEN {
return Err(InputError::IncorrectWordLength(WORD_LEN));
}
Ok(word)
}
fn get_contraints(word: &str) -> Result<ConstraintSet, InputError> {
println!("\nPlease enter Wordle's answer. (G = Green, Y = Yellow, _ = Gray)");
let colors = user_input();
if colors.len() != WORD_LEN {
return Err(InputError::IncorrectColorCodeLength(WORD_LEN));
}
ConstraintSet::try_from((word, colors.as_ref()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verify_app() {
use clap::CommandFactory;
Cli::command().debug_assert()
}
}