diff --git a/Cargo.lock b/Cargo.lock index 03e2e0e..83976b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -476,6 +476,7 @@ dependencies = [ "derive_more", "getrandom", "indicatif", + "js-sys", "rand", "rayon", "tabular", diff --git a/Cargo.toml b/Cargo.toml index 4cf00c9..8f2981f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ colored = "3.1.1" derive_more = { version = "2.1.1", features = [ "debug", "display" ] } getrandom = { version = "0.4.3", optional = true } indicatif = { version = "0.18.6", features = ["rayon"] } +js-sys = { version = "0.3.104", optional = true } rand = "0.10.2" rayon = "1.12.0" tabular = { version = "0.2.0", features = ["ansi-cell"] } @@ -31,4 +32,4 @@ wasm-bindgen = { version = "0.2.127", optional = true } [features] default = [] -wasm-bindgen = ["dep:wasm-bindgen", "getrandom/wasm_js"] +wasm-bindgen = ["dep:wasm-bindgen", "getrandom/wasm_js", "dep:js-sys"] diff --git a/src/lib/solver.rs b/src/lib/solver.rs index 1da1117..dda10c2 100644 --- a/src/lib/solver.rs +++ b/src/lib/solver.rs @@ -151,6 +151,31 @@ impl Solver { SolverJob { receiver: rc } } + pub fn evaluate_all_words_cb(&self, progress: F) -> Result, String> + where + F: Fn(usize, usize) + Send + Sync + 'static, + { + let mut completed = 0; + let mut result = self + .valid_words + .words() + .map(|w| { + let r = self.evaluate_word(w); + completed += 1; + progress(completed, self.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) + }); + } + result + } + pub fn evaluate_all_words(&mut self) -> Result, String> { let mut best_words: Vec = self .valid_words diff --git a/src/lib/wasm.rs b/src/lib/wasm.rs index 1854a20..19c5823 100644 --- a/src/lib/wasm.rs +++ b/src/lib/wasm.rs @@ -21,6 +21,30 @@ pub enum Color { GREEN, } +#[wasm_bindgen] +pub struct WordStats(solver::WordStats); + +#[allow(dead_code)] +#[wasm_bindgen] +impl WordStats { + #[wasm_bindgen(getter)] + pub fn word(&self) -> String { + self.0.word.clone() + } + #[wasm_bindgen(getter)] + pub fn solution_probability(&self) -> f64 { + self.0.solution_probability + } + #[wasm_bindgen(getter)] + pub fn expected_information_gained(&self) -> f64 { + self.0.expected_information_gained + } + #[wasm_bindgen(getter)] + pub fn expected_score_after_guess(&self) -> f64 { + self.0.expected_score_after_guess + } +} + #[allow(dead_code)] #[wasm_bindgen] impl Solver { @@ -50,6 +74,22 @@ impl Solver { Ok(()) } + pub fn evaluate_words( + &self, + n: u32, + progress: js_sys::Function, + ) -> Result, String> { + Ok(self + .0 + .evaluate_all_words_cb(move |i, n| { + let _ = progress.call2(&JsValue::NULL, &JsValue::from(i), &JsValue::from(n)); + })? + .into_iter() + .take(n as usize) + .map(|ws| WordStats(ws)) + .collect()) + } + pub fn guess_infos(&self) -> Vec { self.0 .guesses() diff --git a/web/src/App.svelte b/web/src/App.svelte index 030fef4..3ef2f63 100644 --- a/web/src/App.svelte +++ b/web/src/App.svelte @@ -1,8 +1,18 @@ + +{#await data} +
+ Solving... + {#if progress} + + {progress.i} / {progress.n} + + {/if} +
+{:then d} +
+ + + + + + + + + + + + {#each d.stats as s} + + + + + + + {/each} + +
Wordp[solution]E[Inf.]E[Score]
{s.word}{s.solution_probability.toFixed(3)}{s.expected_information_gained.toFixed(3)}{s.expected_score_after_guess.toFixed(3)}
+
+{:catch e} +
+ {e} +
+{/await} diff --git a/web/src/solver/solver.ts b/web/src/solver/solver.ts index 965744f..cfb86b4 100644 --- a/web/src/solver/solver.ts +++ b/web/src/solver/solver.ts @@ -1,6 +1,6 @@ import type { Color } from "wordle-solver"; import SolverWorker from "./worker?worker"; -import type { GuessResult, SolutionSpaceResult, SolverRequest, SolverResponse } from "./types"; +import type { GuessResult, SolutionSpaceResult, EvaluateWordsResult, SolverRequest, SolverResponse, EvaluateWordsProgress } from "./types"; let solver_worker = new SolverWorker(); @@ -11,6 +11,8 @@ const pending = new Map void; }>(); +const progresses = new Map void>(); + export function solver_guess(word: string[], colors: Color[]): Promise { const id = msg_id++; @@ -29,7 +31,7 @@ export function solver_guess(word: string[], colors: Color[]): Promise { const id = msg_id++; - + return new Promise((resolve, reject) => { pending.set(id, { resolve, reject }); @@ -43,9 +45,40 @@ export function solver_solution_space(): Promise { }); } +export function solver_evaluate_words(n: number, progress: ((p: EvaluateWordsProgress) => void) | null): Promise { + const id = msg_id++; + + if (progress) { + progresses.set(id, progress); + } + + return new Promise((resolve, reject) => { + pending.set(id, { resolve, reject }); + + const r: SolverRequest = { + type: "evaluate_words", + id: id, + input: { n }, + }; + + solver_worker.postMessage(JSON.parse(JSON.stringify(r))); + }); +} + solver_worker.onmessage = (event: MessageEvent) => { let res = event.data; + + if (res.type == "evaluate_words_progress") { + let progress = progresses.get(res.id); + + if (progress) { + progress(res.result); + } + + return; + } + const pendingRequest = pending.get(res.id); if (!pendingRequest) { @@ -53,6 +86,7 @@ solver_worker.onmessage = (event: MessageEvent) => { } pending.delete(res.id); + progresses.delete(res.id); if (res.error) { pendingRequest.reject(res.error); diff --git a/web/src/solver/types.ts b/web/src/solver/types.ts index 5ae557d..bb3b374 100644 --- a/web/src/solver/types.ts +++ b/web/src/solver/types.ts @@ -30,6 +30,11 @@ export type SolverResponse = error: string | null, result: EvaluateWordsResult | null } + | { + type: "evaluate_words_progress", + id: number, + result: EvaluateWordsProgress + } | { type: "solution_space", id: number, @@ -47,8 +52,15 @@ export type GuessResult = { export type EvaluateWords = { n: number, }; +export type EvaluateWordsProgress = { + i: number, + n: number +}; export type WordStats = { word: string, + solution_probability: number, + expected_information_gained: number, + expected_score_after_guess: number }; export type EvaluateWordsResult = { stats: WordStats[] diff --git a/web/src/solver/worker.ts b/web/src/solver/worker.ts index 267a2da..807c1a4 100644 --- a/web/src/solver/worker.ts +++ b/web/src/solver/worker.ts @@ -1,5 +1,5 @@ -import { Solver } from "wordle-solver"; -import type { SolverRequest, SolverResponse } from "./types"; +import { Solver, } from "wordle-solver"; +import type { EvaluateWordsProgress, SolverRequest, SolverResponse, WordStats } from "./types"; let solver = Solver.new(); @@ -19,9 +19,7 @@ self.onmessage = (event: MessageEvent) => { result: { info_gained: infos[infos.length - 1] } }; - self.setTimeout(() => { - self.postMessage(JSON.parse(JSON.stringify(response))); - }, 1000); + self.postMessage(JSON.parse(JSON.stringify(response))); } catch (e) { const response: SolverResponse = { type: "guess", @@ -30,23 +28,48 @@ self.onmessage = (event: MessageEvent) => { result: null, }; - self.setTimeout(() => { - self.postMessage(JSON.parse(JSON.stringify(response))); - }, 1000); + self.postMessage(JSON.parse(JSON.stringify(response))); } break; }; case "evaluate_words": { - const response: SolverResponse = { - type: "evaluate_words", - id: request.id, - error: "not implemented", - result: null, - }; + try { + let stats = solver.evaluate_words(request.input.n, (i: number, n: number) => { + let p: SolverResponse = { + type: "evaluate_words_progress", + id: request.id, + result: { i, n } + }; + self.postMessage(JSON.parse(JSON.stringify(p))); + }).map((ws): WordStats => { + return { + word: ws.word, + solution_probability: ws.solution_probability, + expected_information_gained: ws.expected_information_gained, + expected_score_after_guess: ws.expected_score_after_guess + }; + }); + const response: SolverResponse = { + type: "evaluate_words", + id: request.id, + error: null, + result: { stats }, + }; + + self.postMessage(JSON.parse(JSON.stringify(response))); + } catch (e) { + const response: SolverResponse = { + type: "evaluate_words", + id: request.id, + error: e, + result: null, + }; + + self.postMessage(JSON.parse(JSON.stringify(response))); + } - self.postMessage(JSON.parse(JSON.stringify(response))); break; }; case "solution_space": {