first version draft

This commit is contained in:
2026-08-18 20:22:48 +02:00
parent 48fa1052ad
commit 992890ebb2
12 changed files with 16395 additions and 7 deletions
Generated
+472
View File
@@ -2,6 +2,478 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "arrayvec"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]]
name = "bumpalo"
version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chacha20"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
dependencies = [
"cfg-if",
"cpufeatures",
"rand_core",
]
[[package]]
name = "colored"
version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
dependencies = [
"windows-sys",
]
[[package]]
name = "console"
version = "0.16.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c"
dependencies = [
"encode_unicode",
"libc",
"unicode-width 0.2.2",
"windows-sys",
]
[[package]]
name = "convert_case"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
[[package]]
name = "derive_more"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
dependencies = [
"derive_more-impl",
]
[[package]]
name = "derive_more-impl"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
dependencies = [
"convert_case",
"proc-macro2",
"quote",
"rustc_version",
"syn",
"unicode-xid",
]
[[package]]
name = "either"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
[[package]]
name = "encode_unicode"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]]
name = "futures-core"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
[[package]]
name = "futures-task"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
[[package]]
name = "futures-util"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "getrandom"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"rand_core",
]
[[package]]
name = "indicatif"
version = "0.18.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9433806cd6b4ec1aba79c021c7e4c58fb4c3b9977c085062e611ac929998fb0c"
dependencies = [
"console",
"portable-atomic",
"rayon",
"unicode-width 0.2.2",
"unit-prefix",
"web-time",
]
[[package]]
name = "js-sys"
version = "0.3.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
dependencies = [
"cfg-if",
"futures-util",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "portable-atomic"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
dependencies = [
"chacha20",
"getrandom",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "rayon"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "rustversion"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "semver"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "strip-ansi-escapes"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "011cbb39cf7c1f62871aea3cc46e5817b0937b49e9447370c93cacbe93a766d8"
dependencies = [
"vte",
]
[[package]]
name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tabular"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9a2882c514780a1973df90de9d68adcd8871bacc9a6331c3f28e6d2ff91a3d1"
dependencies = [
"strip-ansi-escapes",
"unicode-width 0.1.14",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-segmentation"
version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
[[package]]
name = "unicode-width"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "unicode-width"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "unit-prefix"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "vte"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983"
dependencies = [
"arrayvec",
"utf8parse",
"vte_generate_state_changes",
]
[[package]]
name = "vte_generate_state_changes"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e369bee1b05d510a7b4ed645f5faa90619e05437111783ea5848f28d97d3c2e"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
dependencies = [
"unicode-ident",
]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "wordle-solver"
version = "0.1.0"
dependencies = [
"colored",
"derive_more",
"indicatif",
"rand",
"rayon",
"tabular",
]
+14 -2
View File
@@ -8,8 +8,20 @@ edition = "2021"
[lib]
name = "wordle_solver"
path = "src/lib.rs"
path = "src/lib/lib.rs"
[[bin]]
name = "wordle_solver"
name = "wordle-solver"
path = "src/app/wordle_solver.rs"
[[bin]]
name = "wordle-simulation"
path = "src/app/wordle_simulation.rs"
[dependencies]
colored = "3.1.1"
derive_more = { version = "2.1.1", features = [ "debug", "display" ] }
indicatif = { version = "0.18.6", features = ["rayon"] }
rand = "0.10.2"
rayon = "1.12.0"
tabular = { version = "0.2.0", features = ["ansi-cell"] }
+2315
View File
File diff suppressed because it is too large Load Diff
+33
View File
@@ -0,0 +1,33 @@
use std::{env::args, path::Path};
use wordle_solver::{game::Game, solver::Solver, word_list::WordList};
pub fn main() {
let valid_words = WordList::from_file(Path::new("valid-words.txt")).unwrap();
let target_words = WordList::from_file(Path::new("answers.txt")).unwrap();
let mut game = Game::new(target_words.random_word());
let mut solver = Solver::new(valid_words, target_words);
if let Some(first_guess) = args().skip(1).next() {
if let Some(pat) = game.guess(&first_guess) {
solver.apply_guess(pat);
println!("{}", solver.tabular_format());
} else {
println!("Found solution: {}", first_guess);
}
}
loop {
let best_word = &solver.evaluate_all_words().first().unwrap().clone().word;
println!("{}", solver.best_word_format());
if let Some(pat) = game.guess(best_word) {
solver.apply_guess(pat);
println!("{}", solver.tabular_format());
} else {
println!("Found solution: {}", best_word);
break;
}
}
}
+18 -2
View File
@@ -1,5 +1,21 @@
use wordle_solver::foo;
use std::path::Path;
use wordle_solver::{pattern::Pattern, solver::Solver, word_list::WordList};
pub fn main() {
foo()
let wl = WordList::from_file(Path::new("valid-words.csv")).unwrap();
let mut solver = Solver::from_single_word_list(wl);
solver.apply_guess(Pattern::from_guess("CRANE", "CASTS").unwrap());
println!("{}", solver.tabular_format());
println!("{}", solver.best_word_format());
solver.apply_guess(Pattern::from_guess("CRANE", "WORKS").unwrap());
println!("{}", solver.tabular_format());
println!("{}", solver.best_word_format());
solver.apply_guess(Pattern::from_guess("CRANE", "PAPER").unwrap());
println!("{}", solver.tabular_format());
println!("{}", solver.best_word_format());
}
-3
View File
@@ -1,3 +0,0 @@
pub fn foo() {
println!("Hello from wordle-solver");
}
+25
View File
@@ -0,0 +1,25 @@
use crate::pattern::Pattern;
pub struct Game {
score: u64,
word: String,
}
impl Game {
pub fn new(word: &str) -> Game {
Game {
score: 0,
word: word.to_string(),
}
}
pub fn guess(&mut self, word: &str) -> Option<Pattern> {
self.score += 1;
if word == self.word {
None
} else {
Some(Pattern::from_guess(&self.word, word).unwrap())
}
}
}
+4
View File
@@ -0,0 +1,4 @@
pub mod game;
pub mod pattern;
pub mod solver;
pub mod word_list;
+270
View File
@@ -0,0 +1,270 @@
use colored::Colorize;
use derive_more::Display;
#[derive(Display, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Hash)]
pub enum CharStatus {
GREY,
YELLOW,
GREEN,
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct Pattern {
chars: Vec<char>,
stats: Vec<CharStatus>,
}
impl Pattern {
pub fn ansi_format(&self) -> String {
self.chars
.iter()
.zip(self.stats.iter())
.map(|(c, s)| match s {
CharStatus::GREEN => c.to_string().green().to_string(),
CharStatus::YELLOW => c.to_string().yellow().to_string(),
CharStatus::GREY => c.to_string().truecolor(100, 100, 100).to_string(),
})
.collect()
}
pub fn try_new(word: &str, stats: Vec<CharStatus>) -> Result<Pattern, String> {
if word.len() != stats.len() {
Err(format!(
"Word length ({}) does not match stats length ({})",
word.len(),
stats.len()
))
} else {
Ok(Pattern {
chars: word.chars().collect(),
stats,
})
}
}
pub fn from_guess(word: &str, guess: &str) -> Result<Pattern, String> {
if word.len() != guess.len() {
return Err(format!(
"Word length ({}) does not match guess length ({})",
word.len(),
guess.len()
));
}
let guess: Vec<char> = guess.chars().collect();
let chars: Vec<char> = word.chars().collect();
let mut stats: Vec<CharStatus> = vec![CharStatus::GREY; chars.len()];
let mut rest = chars.clone();
// Greens
for i in 0..chars.len() {
if chars[i] == guess[i] {
stats[i] = CharStatus::GREEN;
rest[i] = ' ';
}
}
// Yellows
for i in 0..chars.len() {
if let Some((j, _)) = rest.iter().enumerate().find(|(_, x)| **x == guess[i]) {
stats[i] = CharStatus::YELLOW;
rest[j] = ' ';
}
}
Ok(Pattern {
chars: guess,
stats,
})
}
pub fn matches(&self, word: &Vec<char>) -> bool {
let mut rest = word.clone();
if word.len() != self.chars.len() {
return false;
}
// Green + trivial yellow check
for (i, s) in self.stats.iter().enumerate() {
match *s {
CharStatus::GREEN => {
if self.chars[i] != rest[i] {
return false;
}
rest[i] = ' ';
}
CharStatus::YELLOW => {
if self.chars[i] == rest[i] {
return false;
}
}
CharStatus::GREY => {}
}
}
// Yellow check
for (i, s) in self.stats.iter().enumerate() {
if *s != CharStatus::YELLOW {
continue;
}
if let Some((j, _)) = rest.iter().enumerate().find(|(_, x)| **x == self.chars[i]) {
rest[j] = ' ';
} else {
return false;
}
}
// Grey check
for (i, s) in self.stats.iter().enumerate() {
if *s != CharStatus::GREY {
continue;
}
if let Some(_) = rest.iter().find(|x| **x == self.chars[i]) {
return false;
}
}
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pattern_from_guess() {
assert_eq!(
Pattern::from_guess("abcde", "abcde"),
Pattern::try_new(
"abcde",
vec![
CharStatus::GREEN,
CharStatus::GREEN,
CharStatus::GREEN,
CharStatus::GREEN,
CharStatus::GREEN,
]
)
);
assert_eq!(
Pattern::from_guess("abcde", "eabcd"),
Pattern::try_new(
"abcde",
vec![
CharStatus::YELLOW,
CharStatus::YELLOW,
CharStatus::YELLOW,
CharStatus::YELLOW,
CharStatus::YELLOW,
]
)
);
assert_eq!(
Pattern::from_guess("abcde", "fghij"),
Pattern::try_new(
"abcde",
vec![
CharStatus::GREY,
CharStatus::GREY,
CharStatus::GREY,
CharStatus::GREY,
CharStatus::GREY,
]
)
);
assert_eq!(
Pattern::from_guess("aabbc", "bbaab"),
Pattern::try_new(
"aabbc",
vec![
CharStatus::YELLOW,
CharStatus::YELLOW,
CharStatus::YELLOW,
CharStatus::YELLOW,
CharStatus::GREY,
]
)
);
}
#[test]
fn pattern_match_1() {
let p1 = Pattern::try_new(
"abcde",
vec![
CharStatus::GREY,
CharStatus::GREY,
CharStatus::GREY,
CharStatus::GREY,
CharStatus::GREY,
],
)
.unwrap();
assert!(p1.matches(&"fghij".chars().collect()));
assert!(!p1.matches(&"f".chars().collect()));
}
#[test]
fn pattern_match_2() {
let p1 = Pattern::try_new(
"abcde",
vec![
CharStatus::GREY,
CharStatus::GREY,
CharStatus::GREEN,
CharStatus::GREY,
CharStatus::GREY,
],
)
.unwrap();
assert!(!p1.matches(&"fghij".chars().collect()));
assert!(!p1.matches(&"abcde".chars().collect()));
assert!(p1.matches(&"ffcff".chars().collect()));
assert!(p1.matches(&"ccccc".chars().collect()));
}
#[test]
fn pattern_match_3() {
let p1 = Pattern::try_new(
"abcde",
vec![
CharStatus::GREEN,
CharStatus::GREEN,
CharStatus::GREEN,
CharStatus::GREEN,
CharStatus::GREEN,
],
)
.unwrap();
assert!(!p1.matches(&"fghij".chars().collect()));
assert!(p1.matches(&"abcde".chars().collect()));
assert!(!p1.matches(&"abcdf".chars().collect()));
assert!(!p1.matches(&"abced".chars().collect()));
}
#[test]
fn pattern_match_4() {
let p1 = Pattern::try_new(
"aabbcc",
vec![
CharStatus::YELLOW,
CharStatus::YELLOW,
CharStatus::YELLOW,
CharStatus::GREY,
CharStatus::YELLOW,
CharStatus::GREEN,
],
)
.unwrap();
assert!(p1.matches(&"bcaafc".chars().collect()));
assert!(!p1.matches(&"bfaafc".chars().collect()));
assert!(!p1.matches(&"bbaafc".chars().collect()));
assert!(!p1.matches(&"aabfcc".chars().collect()));
}
}
+154
View File
@@ -0,0 +1,154 @@
use crate::pattern::Pattern;
use indicatif::ParallelProgressIterator;
use rayon::iter::{ParallelBridge, ParallelIterator};
use crate::word_list::WordList;
use derive_more::Display;
use tabular::{Row, Table};
struct Guess {
pattern: Pattern,
information_gained: f64,
}
pub struct Solver {
valid_words: WordList,
possible_solutions: WordList,
guesses: Vec<Guess>,
best_words: Vec<WordStats>,
}
#[derive(Display, Clone)]
#[display(
"WordStats({}, p={:.02}%, E[I]={:.02}, E[s]={:.02})",
word,
solution_probability*100.0,
expected_information_gained,
expected_score_after_guess
)]
pub struct WordStats {
pub word: String,
pub solution_probability: f64,
pub expected_information_gained: f64,
pub expected_score_after_guess: f64,
}
impl Solver {
pub fn from_single_word_list(word_list: WordList) -> Solver {
Solver {
valid_words: word_list.clone(),
possible_solutions: word_list,
guesses: vec![],
best_words: vec![],
}
}
pub fn new(valid_words: WordList, possible_solutions: WordList) -> Solver {
Solver {
valid_words,
possible_solutions,
guesses: vec![],
best_words: vec![],
}
}
pub fn apply_guess(&mut self, guess: Pattern) {
let new_possible_solutions = self.possible_solutions.apply_guess(&guess);
let information_gained = self.possible_solutions.equal_likelieness_entropy()
- new_possible_solutions.equal_likelieness_entropy();
self.possible_solutions = new_possible_solutions;
self.guesses.push(Guess {
pattern: guess,
information_gained,
});
}
pub fn evaluate_word(&self, word: &str) -> WordStats {
let solution_probability = self.possible_solutions.word_probability(word);
let expected_information_gained = self.possible_solutions.entropy_if_guessed(word);
let score = (self.guesses.len() + 1) as f64;
let expected_score_after_guess = score * solution_probability
+ (1.0 - solution_probability)
* (score
+ ((self.valid_words.equal_likelieness_entropy()
- expected_information_gained)
.log2()
+ 1.0));
WordStats {
word: word.to_string(),
solution_probability,
expected_information_gained,
expected_score_after_guess,
}
}
pub fn evaluate_all_words(&mut self) -> Vec<WordStats> {
self.best_words = self
.valid_words
.words()
.par_bridge()
.progress_count(self.valid_words.len() as u64)
.map(|w| self.evaluate_word(w))
.collect();
self.best_words.sort_by(|a, b| {
a.expected_score_after_guess
.total_cmp(&b.expected_score_after_guess)
});
self.best_words.clone()
}
pub fn best_word_format(&self) -> String {
let mut table = Table::new("#{:<} {:>} | {:<} {:<} {:<}");
table.add_row(
Row::new()
.with_cell("Best")
.with_cell("Word")
.with_cell("p[solution]")
.with_cell("E[I]")
.with_cell("E[score]"),
);
for (i, ws) in self.best_words.iter().take(10).enumerate() {
table.add_row(
Row::new()
.with_cell(i + 1)
.with_cell(&ws.word)
.with_cell(ws.solution_probability)
.with_cell(ws.expected_information_gained)
.with_cell(ws.expected_score_after_guess),
);
}
table.to_string()
}
pub fn tabular_format(&self) -> String {
let mut table = Table::new("{:<} | {:<}");
table.add_row(Row::new().with_cell("Guess").with_cell("Info-Gained"));
for g in &self.guesses {
table.add_row(
Row::new()
.with_ansi_cell(g.pattern.ansi_format())
.with_cell(format!("{:.04}", g.information_gained)),
);
}
format!(
"Solution Space: {}, Uncertainty: {:.02} bits\n{}",
self.possible_solutions.len(),
self.possible_solutions.equal_likelieness_entropy(),
table
)
}
}
+118
View File
@@ -0,0 +1,118 @@
use rand::{rng, seq::IteratorRandom};
use std::{
collections::{hash_set, HashSet},
fs::{self},
io::{self, BufRead},
path::Path,
};
use derive_more::Display;
use crate::pattern::Pattern;
#[derive(Display, Clone)]
#[display("{:?}", words)]
pub struct WordList {
words: HashSet<String>,
}
impl WordList {
pub fn len(&self) -> usize {
self.words.len()
}
pub fn from_file(file: &Path) -> Result<WordList, String> {
let file = fs::File::open(file).map_err(|e| e.to_string())?;
let mut len = None;
let words: Result<HashSet<String>, String> = io::BufReader::new(file)
.lines()
.map_while(Result::ok)
.map(|s| s.trim_ascii().to_ascii_uppercase())
.filter(|s| !s.is_empty())
.map(|s| {
if !s.chars().all(|c| c.is_ascii_alphabetic()) {
return Err(format!("Word {} is not ascii alphabetic", s));
}
if let Some(len) = len {
if len == s.len() {
return Ok(s);
} else {
return Err(format!(
"Word {} does not match initial length of {}",
s, len
));
}
} else {
len = Some(s.len());
return Ok(s);
}
})
.collect();
Ok(WordList { words: words? })
}
pub fn apply_guess(&self, pat: &Pattern) -> WordList {
WordList {
words: self
.words
.iter()
.filter(|w| pat.matches(&w.chars().collect()))
.map(Clone::clone)
.collect(),
}
}
pub fn count_matches(&self, pat: &Pattern) -> usize {
self.words
.iter()
.filter(|w| pat.matches(&w.chars().collect()))
.count()
}
pub fn entropy_if_guessed(&self, word: &str) -> f64 {
let mut e = 0.0;
let mut observed_patterns = HashSet::new();
for w in self.words.iter() {
let pat = Pattern::from_guess(w, word).unwrap();
if observed_patterns.contains(&pat) {
continue;
}
let m = self.count_matches(&pat);
observed_patterns.insert(pat);
if m != 0 {
let p = m as f64 / self.words.len() as f64;
e += p * (1.0 / p).log2();
}
}
e
}
pub fn word_probability(&self, word: &str) -> f64 {
if self.words.contains(word) {
1.0 / self.words.len() as f64
} else {
0.0
}
}
pub fn contains(&self, word: &str) -> bool {
self.words.contains(word)
}
pub fn words<'a>(&'a self) -> hash_set::Iter<'a, String> {
self.words.iter()
}
pub fn equal_likelieness_entropy(&self) -> f64 {
let p = 1.0 / self.words.len() as f64;
-p.log2()
}
pub fn random_word<'a>(&'a self) -> &'a str {
self.words.iter().choose(&mut rng()).unwrap()
}
}
+12972
View File
File diff suppressed because it is too large Load Diff