diff --git a/Cargo.toml b/Cargo.toml index 7ee46dd..1e39de9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,16 +13,21 @@ keywords = ["wordle", "benchmark"] default-run = "wordlec" [features] -default = ["game", "bench", "tui", "solvers"] -game = [] +default = ["game", "bench", "tui", "solvers", "builtin_wlist", "serde"] +builtin_wlist = ["dep:serde_json", "serde"] +game = ["builtin_wlist"] solvers = [] tui = ["game"] bench = [] +serde = ["dep:serde"] [dependencies] anyhow = "1.0.81" clap = { version = "4.5.3", features = ["derive"] } libpt = "0.4.2" +rand = "0.8.5" +serde = { version = "1.0.197", optional = true, features = ["serde_derive"] } +serde_json = {version = "1.0.114", optional = true} [[bin]] name = "wordlec" diff --git a/src/bin/game/cli.rs b/src/bin/game/cli.rs index 0d10e78..c5e3ec3 100644 --- a/src/bin/game/cli.rs +++ b/src/bin/game/cli.rs @@ -3,6 +3,8 @@ #![warn(missing_debug_implementations)] use clap::Parser; use libpt::log::*; +use wordle_analyzer::game::Game; +use wordle_analyzer::wlist::builtin::BuiltinWList; use wordle_analyzer::{self, game}; #[derive(Parser, Clone, Debug)] @@ -24,7 +26,7 @@ fn main() -> anyhow::Result<()> { Logger::build_mini(Some(Level::TRACE))?; debug!("dumping CLI: {:#?}", cli); - let game = game::Game::builder() + let game: Game = game::Game::builder() .length(cli.length) .precompute(cli.precompute) .build()?; diff --git a/src/game/mod.rs b/src/game/mod.rs index 76a63fb..c3090be 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -1,15 +1,21 @@ -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Game { +use crate::wlist::WordList; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Game +where + WL: WordList, +{ length: usize, precompute: bool, max_steps: usize, step: usize, solution: String, + wordlist: WL, } -impl Game { +impl Game { /// get a new [`GameBuilder`] - pub fn builder() -> GameBuilder { + pub fn builder() -> GameBuilder { GameBuilder::default() } /// Create a [Game] of wordle @@ -24,22 +30,16 @@ impl Game { /// # Errors /// /// This function will return an error if . - pub(crate) fn build(length: usize, precompute: bool, max_steps: usize) -> anyhow::Result { + pub(crate) fn build(length: usize, precompute: bool, max_steps: usize, wlist: WL) -> anyhow::Result { let _game = Game { length, precompute, max_steps, step: 0, solution: String::default(), // we actually set this later + wordlist: wlist }; - // TODO: load wordlist of possible answers - // TODO: select one as a solution at random - // NOTE: The possible answers should be determined with a wordlist that has the - // frequencies/probabilities of the words. We then use a sigmoid function to determine if a - // word can be a solution based on that value. Only words above some threshold of - // commonness will be available as solutions then. Next, we choose one of the allowed words - // randomly. todo!(); } } @@ -77,16 +77,17 @@ impl Game { /// ``` /// #[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct GameBuilder { +pub struct GameBuilder { length: usize, precompute: bool, max_steps: usize, + wordlist: WL } -impl GameBuilder { +impl GameBuilder { /// build a [`Game`] with the stored configuration - pub fn build(self) -> anyhow::Result { - let game: Game = Game::build(self.length, self.precompute, self.max_steps)?; + pub fn build(self) -> anyhow::Result> { + let game: Game = Game::build(self.length, self.precompute, self.max_steps, WL::default())?; Ok(game) } @@ -116,12 +117,13 @@ impl GameBuilder { } } -impl Default for GameBuilder { +impl Default for GameBuilder { fn default() -> Self { Self { length: super::DEFAULT_WORD_LENGTH, precompute: false, max_steps: super::DEFAULT_MAX_STEPS, + wordlist: WL::default() } } } diff --git a/src/lib.rs b/src/lib.rs index 484c748..22c9942 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,3 +13,4 @@ pub mod bench; pub mod game; #[cfg(feature = "solvers")] pub mod solvers; +pub mod wlist; diff --git a/src/wlist/builtin.rs b/src/wlist/builtin.rs new file mode 100644 index 0000000..56d149d --- /dev/null +++ b/src/wlist/builtin.rs @@ -0,0 +1,30 @@ +use serde_json; + +use super::Word; + +const RAW_WORDLIST_FILE: &str = include_str!("../../data/wordlists/en_US_3b1b_freq_map.json"); + +#[derive(Clone, Debug)] +pub struct BuiltinWList { + words: super::WordMap +} + +impl super::WordList for BuiltinWList { + fn solutions(&self) -> Vec<&Word> { + // PERF: this can be made faster if we were to use parallel iterators or chunking + self.words.keys().collect() + } + fn length_range(&self) -> impl std::ops::RangeBounds { + 5..5 + } +} + +impl Default for BuiltinWList { + fn default() -> Self { + let words: super::WordMap = serde_json::from_str(RAW_WORDLIST_FILE).unwrap(); + + Self { + words + } + } +} diff --git a/src/wlist/mod.rs b/src/wlist/mod.rs new file mode 100644 index 0000000..dab7484 --- /dev/null +++ b/src/wlist/mod.rs @@ -0,0 +1,25 @@ +use rand::{prelude::*, seq::IteratorRandom}; +use std::collections::HashMap; +use std::ops::RangeBounds; + +#[cfg(feature = "builtin_wlist")] +pub mod builtin; +pub mod word; +use word::*; + +pub type AnyWordlist = Box; + +pub trait WordList: Clone + std::fmt::Debug + Default { + // NOTE: The possible answers should be determined with a wordlist that has the + // frequencies/probabilities of the words. We then use a sigmoid function to determine if a + // word can be a solution based on that value. Only words above some threshold of + // commonness will be available as solutions then. Next, we choose one of the allowed words + // randomly. + // NOTE: must never return nothing + fn solutions(&self) -> Vec<&Word>; + fn rand_solution(&self) -> &Word { + let mut rng = rand::thread_rng(); + self.solutions().iter().choose(&mut rng).unwrap() + } + fn length_range(&self) -> impl RangeBounds; +} diff --git a/src/wlist/word.rs b/src/wlist/word.rs new file mode 100644 index 0000000..f639dfc --- /dev/null +++ b/src/wlist/word.rs @@ -0,0 +1,25 @@ +use std::collections::HashMap; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +// NOTE: We might need a different implementation for more precision +#[derive(Clone, Debug, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Frequency { + inner: f64 +} +// PERF: Hash for String is probably a bottleneck +pub type Word = String; + +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct WordMap { + inner: HashMap +} + +impl WordMap { + pub fn keys(&self) -> std::collections::hash_map::Keys<'_, String, Frequency> { + self.inner.keys() + } +}