generics are crazy
cargo devel CI / cargo CI (push) Successful in 49s Details

This commit is contained in:
Christoph J. Scherr 2024-03-21 14:32:24 +01:00
parent 9f908e3812
commit 880826dd85
Signed by: cscherrNT
GPG Key ID: 8E2B45BC51A27EA7
7 changed files with 110 additions and 20 deletions

View File

@ -13,16 +13,21 @@ keywords = ["wordle", "benchmark"]
default-run = "wordlec" default-run = "wordlec"
[features] [features]
default = ["game", "bench", "tui", "solvers"] default = ["game", "bench", "tui", "solvers", "builtin_wlist", "serde"]
game = [] builtin_wlist = ["dep:serde_json", "serde"]
game = ["builtin_wlist"]
solvers = [] solvers = []
tui = ["game"] tui = ["game"]
bench = [] bench = []
serde = ["dep:serde"]
[dependencies] [dependencies]
anyhow = "1.0.81" anyhow = "1.0.81"
clap = { version = "4.5.3", features = ["derive"] } clap = { version = "4.5.3", features = ["derive"] }
libpt = "0.4.2" 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]] [[bin]]
name = "wordlec" name = "wordlec"

View File

@ -3,6 +3,8 @@
#![warn(missing_debug_implementations)] #![warn(missing_debug_implementations)]
use clap::Parser; use clap::Parser;
use libpt::log::*; use libpt::log::*;
use wordle_analyzer::game::Game;
use wordle_analyzer::wlist::builtin::BuiltinWList;
use wordle_analyzer::{self, game}; use wordle_analyzer::{self, game};
#[derive(Parser, Clone, Debug)] #[derive(Parser, Clone, Debug)]
@ -24,7 +26,7 @@ fn main() -> anyhow::Result<()> {
Logger::build_mini(Some(Level::TRACE))?; Logger::build_mini(Some(Level::TRACE))?;
debug!("dumping CLI: {:#?}", cli); debug!("dumping CLI: {:#?}", cli);
let game = game::Game::builder() let game: Game<BuiltinWList> = game::Game::builder()
.length(cli.length) .length(cli.length)
.precompute(cli.precompute) .precompute(cli.precompute)
.build()?; .build()?;

View File

@ -1,15 +1,21 @@
#[derive(Debug, Clone, PartialEq, Eq, Hash)] use crate::wlist::WordList;
pub struct Game {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Game<WL>
where
WL: WordList,
{
length: usize, length: usize,
precompute: bool, precompute: bool,
max_steps: usize, max_steps: usize,
step: usize, step: usize,
solution: String, solution: String,
wordlist: WL,
} }
impl Game { impl<WL: WordList> Game<WL> {
/// get a new [`GameBuilder`] /// get a new [`GameBuilder`]
pub fn builder() -> GameBuilder { pub fn builder() -> GameBuilder<WL> {
GameBuilder::default() GameBuilder::default()
} }
/// Create a [Game] of wordle /// Create a [Game] of wordle
@ -24,22 +30,16 @@ impl Game {
/// # Errors /// # Errors
/// ///
/// This function will return an error if . /// This function will return an error if .
pub(crate) fn build(length: usize, precompute: bool, max_steps: usize) -> anyhow::Result<Self> { pub(crate) fn build(length: usize, precompute: bool, max_steps: usize, wlist: WL) -> anyhow::Result<Self> {
let _game = Game { let _game = Game {
length, length,
precompute, precompute,
max_steps, max_steps,
step: 0, step: 0,
solution: String::default(), // we actually set this later 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!(); todo!();
} }
} }
@ -77,16 +77,17 @@ impl Game {
/// ``` /// ```
/// ///
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct GameBuilder { pub struct GameBuilder<WL: WordList> {
length: usize, length: usize,
precompute: bool, precompute: bool,
max_steps: usize, max_steps: usize,
wordlist: WL
} }
impl GameBuilder { impl<WL: WordList> GameBuilder<WL> {
/// build a [`Game`] with the stored configuration /// build a [`Game`] with the stored configuration
pub fn build(self) -> anyhow::Result<Game> { pub fn build(self) -> anyhow::Result<Game<WL>> {
let game: Game = Game::build(self.length, self.precompute, self.max_steps)?; let game: Game<WL> = Game::build(self.length, self.precompute, self.max_steps, WL::default())?;
Ok(game) Ok(game)
} }
@ -116,12 +117,13 @@ impl GameBuilder {
} }
} }
impl Default for GameBuilder { impl<WL: WordList> Default for GameBuilder<WL> {
fn default() -> Self { fn default() -> Self {
Self { Self {
length: super::DEFAULT_WORD_LENGTH, length: super::DEFAULT_WORD_LENGTH,
precompute: false, precompute: false,
max_steps: super::DEFAULT_MAX_STEPS, max_steps: super::DEFAULT_MAX_STEPS,
wordlist: WL::default()
} }
} }
} }

View File

@ -13,3 +13,4 @@ pub mod bench;
pub mod game; pub mod game;
#[cfg(feature = "solvers")] #[cfg(feature = "solvers")]
pub mod solvers; pub mod solvers;
pub mod wlist;

30
src/wlist/builtin.rs Normal file
View File

@ -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<usize> {
5..5
}
}
impl Default for BuiltinWList {
fn default() -> Self {
let words: super::WordMap = serde_json::from_str(RAW_WORDLIST_FILE).unwrap();
Self {
words
}
}
}

25
src/wlist/mod.rs Normal file
View File

@ -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<dyn WordList>;
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<usize>;
}

25
src/wlist/word.rs Normal file
View File

@ -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<Word,Frequency>
}
impl WordMap {
pub fn keys(&self) -> std::collections::hash_map::Keys<'_, String, Frequency> {
self.inner.keys()
}
}