From fcb215f4c73a3ea470f92207d498f011223e949e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20R=C3=B6ger?= Date: Thu, 20 Aug 2026 21:48:23 +0200 Subject: [PATCH] feat(lib): add async solver capabilities --- src/app/wordle_simulation.rs | 19 +++++----- src/app/wordle_solver.rs | 8 +++-- src/lib/pattern.rs | 2 +- src/lib/solver.rs | 68 +++++++++++++++++++++++++++++++----- 4 files changed, 77 insertions(+), 20 deletions(-) diff --git a/src/app/wordle_simulation.rs b/src/app/wordle_simulation.rs index 7b997c1..a191e65 100644 --- a/src/app/wordle_simulation.rs +++ b/src/app/wordle_simulation.rs @@ -1,5 +1,6 @@ use std::{env::args, path::Path}; +use indicatif::ProgressBar; use wordle_solver::{game::Game, solver::Solver, word_list::WordList}; pub fn do_simulation(valid_words: WordList, target_words: WordList, opener: Option<&str>) -> u64 { @@ -24,14 +25,16 @@ pub fn do_simulation(valid_words: WordList, target_words: WordList, opener: Opti } loop { - let best_word = solver - .evaluate_all_words() - .unwrap() - .first() - .unwrap() - .word - .clone(); - println!("{}", solver.best_word_format(5)); + let bar = ProgressBar::new(0); + let best_words = solver + .evaluate_all_words_async(move |i, n| { + bar.set_position(i as u64); + bar.set_length(n as u64); + }) + .wait() + .unwrap(); + let best_word = best_words.first().unwrap().word.clone(); + println!("{}", Solver::best_word_format(best_words, 5)); match game.guess(&best_word) { Ok(pat) => { diff --git a/src/app/wordle_solver.rs b/src/app/wordle_solver.rs index 35937b9..40ace7d 100644 --- a/src/app/wordle_solver.rs +++ b/src/app/wordle_solver.rs @@ -39,9 +39,11 @@ pub fn main() -> io::Result<()> { io::stdin().read_line(&mut buf)?; let pat_str = buf.trim_ascii().to_ascii_lowercase(); - solver.apply_guess(Pattern::try_new(&guess, pat_from_str(&pat_str)).unwrap()); - solver.evaluate_all_words(); + solver + .apply_guess(Pattern::try_new(&guess, pat_from_str(&pat_str)).unwrap()) + .unwrap(); + let best_words = solver.evaluate_all_words().unwrap(); println!("\n{}\n", solver.tabular_format()); - println!("{}", solver.best_word_format(10)); + println!("{}", Solver::best_word_format(best_words, 10)); } } diff --git a/src/lib/pattern.rs b/src/lib/pattern.rs index 9d9ff64..9ceb0f7 100644 --- a/src/lib/pattern.rs +++ b/src/lib/pattern.rs @@ -8,7 +8,7 @@ pub enum CharStatus { GREEN, } -#[derive(Debug, PartialEq, Eq, Hash)] +#[derive(Debug, PartialEq, Eq, Hash, Clone)] pub struct Pattern { chars: Vec, stats: Vec, diff --git a/src/lib/solver.rs b/src/lib/solver.rs index 0086c44..c69c5c8 100644 --- a/src/lib/solver.rs +++ b/src/lib/solver.rs @@ -1,3 +1,8 @@ +use std::sync::{ + atomic::{AtomicUsize, Ordering}, + mpsc, Arc, +}; + use crate::pattern::Pattern; use indicatif::ParallelProgressIterator; use rayon::iter::{ParallelBridge, ParallelIterator}; @@ -7,16 +12,29 @@ use crate::word_list::WordList; use derive_more::Display; use tabular::{Row, Table}; +#[derive(Clone)] pub struct Guess { pub pattern: Pattern, pub information_gained: f64, } +#[derive(Clone)] pub struct Solver { valid_words: WordList, possible_solutions: WordList, guesses: Vec, - best_words: Vec, +} + +pub struct SolverJob { + receiver: mpsc::Receiver, String>>, +} + +impl SolverJob { + pub fn wait(self) -> Result, String> { + self.receiver + .recv() + .map_err(|_| "calculation task cancelled".to_string())? + } } #[derive(Display, Clone)] @@ -40,7 +58,6 @@ impl Solver { valid_words: word_list.clone(), possible_solutions: word_list, guesses: vec![], - best_words: vec![], } } @@ -49,7 +66,6 @@ impl Solver { valid_words, possible_solutions, guesses: vec![], - best_words: vec![], } } @@ -99,8 +115,44 @@ impl Solver { }) } + pub fn evaluate_all_words_async(&self, progress: F) -> SolverJob + where + F: Fn(usize, usize) + Send + Sync + 'static, + { + let solver_locked = (*self).clone(); + + let (tx, rc) = mpsc::channel(); + + let completed = Arc::new(AtomicUsize::new(0)); + + rayon::spawn_fifo(move || { + let mut result = solver_locked + .valid_words + .words() + .par_bridge() + .map(|w| { + let r = solver_locked.evaluate_word(w); + let completed = completed.fetch_add(1, Ordering::Relaxed) + 1; + progress(completed, solver_locked.valid_words.len()); + r + }) + .collect::, String>>(); + + if let Ok(result) = &mut result { + result.sort_by(|a, b| { + a.expected_score_after_guess + .total_cmp(&b.expected_score_after_guess) + }); + } + + let _ = tx.send(result); + }); + + SolverJob { receiver: rc } + } + pub fn evaluate_all_words(&mut self) -> Result, String> { - self.best_words = self + let mut best_words: Vec = self .valid_words .words() .par_bridge() @@ -108,15 +160,15 @@ impl Solver { .map(|w| self.evaluate_word(w)) .collect::>()?; - self.best_words.sort_by(|a, b| { + best_words.sort_by(|a, b| { a.expected_score_after_guess .total_cmp(&b.expected_score_after_guess) }); - Ok(self.best_words.clone()) + Ok(best_words) } - pub fn best_word_format(&self, n: usize) -> String { + pub fn best_word_format(best_words: Vec, n: usize) -> String { let mut table = Table::new("#{:<} {:>} | {:<} {:<} {:<}"); table.add_row( @@ -127,7 +179,7 @@ impl Solver { .with_cell("E[I]") .with_cell("E[score]"), ); - for (i, ws) in self.best_words.iter().take(n).enumerate() { + for (i, ws) in best_words.iter().take(n).enumerate() { table.add_row( Row::new() .with_cell(i + 1)