Compare commits

..
Author SHA1 Message Date
jonas 84c931ad17 feat: parallel web solving 2026-08-28 18:49:01 +02:00
jonas b3b9db5954 feat: parallel web solving 2026-08-28 18:48:44 +02:00
jonas 30ba157d43 feat: parallel web solving 2026-08-23 20:19:26 +02:00
jonas 46e977acf5 feat(web): add info page 2026-08-23 14:00:35 +02:00
jonas 6e6e1b67ec feat(web): make ivalid guesses visually irrelevant 2026-08-23 01:18:22 +02:00
jonas 4ed16d404f chore(web): solver output error style 2026-08-22 22:36:46 +02:00
jonas ab7c9b5670 fix(web): cursor focusing 2026-08-22 22:36:34 +02:00
jonas ab79575942 feat(web): responsive design
- fix mobile accessibility
2026-08-22 22:14:54 +02:00
jonas c8a764e82f feat(web): shrink table 2026-08-22 20:26:04 +02:00
17 changed files with 595 additions and 113 deletions
Generated
+79 -2
View File
@@ -8,6 +8,12 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.20.3" version = "3.20.3"
@@ -114,7 +120,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"rustc_version", "rustc_version",
"syn", "syn 2.0.119",
"unicode-xid", "unicode-xid",
] ]
@@ -199,6 +205,15 @@ version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.4" version = "1.21.4"
@@ -278,6 +293,25 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "rmp"
version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c"
dependencies = [
"num-traits",
]
[[package]]
name = "rmp-serde"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155"
dependencies = [
"rmp",
"serde",
]
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.4.1" version = "0.4.1"
@@ -299,6 +333,36 @@ version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.3",
]
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.12" version = "0.4.12"
@@ -325,6 +389,17 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "syn"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]] [[package]]
name = "tabular" name = "tabular"
version = "0.2.0" version = "0.2.0"
@@ -430,7 +505,7 @@ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.119",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@@ -479,6 +554,8 @@ dependencies = [
"js-sys", "js-sys",
"rand", "rand",
"rayon", "rayon",
"rmp-serde",
"serde",
"tabular", "tabular",
"wasm-bindgen", "wasm-bindgen",
] ]
+3 -1
View File
@@ -27,9 +27,11 @@ indicatif = { version = "0.18.6", features = ["rayon"] }
js-sys = { version = "0.3.104", optional = true } js-sys = { version = "0.3.104", optional = true }
rand = "0.10.2" rand = "0.10.2"
rayon = "1.12.0" rayon = "1.12.0"
rmp-serde = { version = "1.3.1", optional = true }
serde = { version = "1.0.229", optional = true, features = ["derive"] }
tabular = { version = "0.2.0", features = ["ansi-cell"] } tabular = { version = "0.2.0", features = ["ansi-cell"] }
wasm-bindgen = { version = "0.2.127", optional = true } wasm-bindgen = { version = "0.2.127", optional = true }
[features] [features]
default = [] default = []
wasm-bindgen = ["dep:wasm-bindgen", "getrandom/wasm_js", "dep:js-sys"] wasm-bindgen = ["dep:wasm-bindgen", "getrandom/wasm_js", "dep:js-sys", "dep:serde", "dep:rmp-serde"]
+1 -1
View File
@@ -14,7 +14,7 @@ stdenv.mkDerivation (finalAttrs: {
yarnOfflineCache = fetchYarnDeps { yarnOfflineCache = fetchYarnDeps {
yarnLock = finalAttrs.src + "/yarn.lock"; yarnLock = finalAttrs.src + "/yarn.lock";
hash = "sha256-okhACATsv6jqUrbvoRN+PTTc3FbqDT7iocnL6lr7R70="; hash = "sha256-MTyHX2+/U7jRB1SJWbK9Td43zq+nclQwx7oDZbX0DqA=";
}; };
yarnBuildScript = "build-no-wasm"; yarnBuildScript = "build-no-wasm";
+4
View File
@@ -1,7 +1,10 @@
use colored::Colorize; use colored::Colorize;
use derive_more::Display; use derive_more::Display;
#[cfg(feature = "wasm-bindgen")]
use serde::{Deserialize, Serialize};
#[derive(Display, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Hash)] #[derive(Display, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Hash)]
#[cfg_attr(feature = "wasm-bindgen", derive(Serialize, Deserialize))]
pub enum CharStatus { pub enum CharStatus {
GREY, GREY,
YELLOW, YELLOW,
@@ -9,6 +12,7 @@ pub enum CharStatus {
} }
#[derive(Debug, PartialEq, Eq, Hash, Clone)] #[derive(Debug, PartialEq, Eq, Hash, Clone)]
#[cfg_attr(feature = "wasm-bindgen", derive(Serialize, Deserialize))]
pub struct Pattern { pub struct Pattern {
chars: Vec<char>, chars: Vec<char>,
stats: Vec<CharStatus>, stats: Vec<CharStatus>,
+5
View File
@@ -7,18 +7,23 @@ use crate::pattern::Pattern;
use indicatif::ParallelProgressIterator; use indicatif::ParallelProgressIterator;
use rayon::iter::{ParallelBridge, ParallelIterator}; use rayon::iter::{ParallelBridge, ParallelIterator};
#[cfg(feature = "wasm-bindgen")]
use serde::{Deserialize, Serialize};
use crate::word_list::WordList; use crate::word_list::WordList;
use derive_more::Display; use derive_more::Display;
use tabular::{Row, Table}; use tabular::{Row, Table};
#[derive(Clone)] #[derive(Clone)]
#[cfg_attr(feature = "wasm-bindgen", derive(Serialize, Deserialize))]
pub struct Guess { pub struct Guess {
pub pattern: Pattern, pub pattern: Pattern,
pub information_gained: f64, pub information_gained: f64,
} }
#[derive(Clone)] #[derive(Clone)]
#[cfg_attr(feature = "wasm-bindgen", derive(Serialize, Deserialize))]
pub struct Solver { pub struct Solver {
valid_words: WordList, valid_words: WordList,
possible_solutions: WordList, possible_solutions: WordList,
+22
View File
@@ -1,5 +1,7 @@
use std::io; use std::io;
use rmp_serde::{from_slice, to_vec};
use serde::Serialize;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use crate::{ use crate::{
@@ -63,6 +65,26 @@ impl Solver {
} }
} }
pub fn serialize(&self) -> Result<js_sys::Uint8Array, String> {
let r = to_vec(&self.0).map_err(|e| {
let s = e.to_string();
drop(e);
s
})?;
Ok(js_sys::Uint8Array::from(&r[..]))
}
pub fn new_from_serialized(bytes: &js_sys::Uint8Array) -> Result<Solver, String> {
from_slice(&bytes.to_vec()[..])
.map_err(|e| {
let s = e.to_string();
drop(e);
s
})
.map(Solver)
}
pub fn guess(&mut self, word: &str, colors: Vec<Color>) -> Result<(), String> { pub fn guess(&mut self, word: &str, colors: Vec<Color>) -> Result<(), String> {
let pat = Pattern::try_new( let pat = Pattern::try_new(
word, word,
+3
View File
@@ -1,4 +1,6 @@
use rand::{rng, seq::IteratorRandom}; use rand::{rng, seq::IteratorRandom};
#[cfg(feature = "wasm-bindgen")]
use serde::{Deserialize, Serialize};
use std::{ use std::{
collections::{hash_set, HashSet}, collections::{hash_set, HashSet},
fs::{self}, fs::{self},
@@ -11,6 +13,7 @@ use derive_more::Display;
use crate::pattern::Pattern; use crate::pattern::Pattern;
#[derive(Display, Clone)] #[derive(Display, Clone)]
#[cfg_attr(feature = "wasm-bindgen", derive(Serialize, Deserialize))]
#[display("{:?}", words)] #[display("{:?}", words)]
pub struct WordList { pub struct WordList {
words: HashSet<String>, words: HashSet<String>,
+2
View File
@@ -16,6 +16,7 @@
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^7.3.0", "@sveltejs/vite-plugin-svelte": "^7.3.0",
"@tailwindcss/typography": "^0.5.20",
"@tailwindcss/vite": "^4.3.3", "@tailwindcss/vite": "^4.3.3",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"tailwindcss": "^4.3.3", "tailwindcss": "^4.3.3",
@@ -26,6 +27,7 @@
"vite-plugin-wasm-pack-watcher": "^1.0.1" "vite-plugin-wasm-pack-watcher": "^1.0.1"
}, },
"dependencies": { "dependencies": {
"@humanspeak/svelte-markdown": "^1.8.7",
"@lucide/svelte": "^1.33.0", "@lucide/svelte": "^1.33.0",
"esbuild": "^0.28.2", "esbuild": "^0.28.2",
"katex": "^0.18.4", "katex": "^0.18.4",
+104 -18
View File
@@ -6,6 +6,7 @@
solver_guess, solver_guess,
solver_reset, solver_reset,
solver_solution_space, solver_solution_space,
solver_stop,
} from "./solver/solver.ts"; } from "./solver/solver.ts";
import type { import type {
GuessResult, GuessResult,
@@ -13,9 +14,11 @@
EvaluateWordsResult, EvaluateWordsResult,
EvaluateWordsProgress, EvaluateWordsProgress,
} from "./solver/types.ts"; } from "./solver/types.ts";
import info_md from "./assets/info.md?raw";
import SolverOutput from "./components/SolverOutput.svelte"; import SolverOutput from "./components/SolverOutput.svelte";
import { FolderGit2 } from "@lucide/svelte"; import { CircleX, FolderGit2, Info } from "@lucide/svelte";
import { tick } from "svelte"; import { tick, onMount } from "svelte";
import SvelteMarkdown from "@humanspeak/svelte-markdown";
let guesses = $state([ let guesses = $state([
{ {
@@ -30,6 +33,8 @@
}, },
]); ]);
let show_info = $state(false);
let infos_gained: (Promise<GuessResult> | null)[] = $state([null]); let infos_gained: (Promise<GuessResult> | null)[] = $state([null]);
let use_default_wordle_answers: boolean = $state(true); let use_default_wordle_answers: boolean = $state(true);
@@ -45,6 +50,19 @@
let w_lines: WordleLine[] = $state([]); let w_lines: WordleLine[] = $state([]);
let cursor = $state(0); let cursor = $state(0);
let is_desktop = $state(false);
onMount(() => {
const update = () => {
is_desktop = window.innerWidth >= 768;
};
update();
window.addEventListener("resize", update);
return () => window.removeEventListener("resize", update);
});
async function toggleSolver() { async function toggleSolver() {
if (solving) { if (solving) {
await stopSolver(); await stopSolver();
@@ -76,12 +94,7 @@
} }
async function stopSolver() { async function stopSolver() {
await solver_reset(use_default_wordle_answers); await solver_stop();
for (const g of guesses.slice(0, -1)) {
try {
await solver_guess(g.letters, g.colors);
} catch (_) {}
}
} }
async function doGuess() { async function doGuess() {
@@ -136,13 +149,16 @@
<svelte:window onkeydown={handleKey} /> <svelte:window onkeydown={handleKey} />
<div <div
class="min-h-screen w-full max-w-full class="h-dvh w-full max-w-full
bg-zinc-950 text-zinc-100 bg-zinc-950 text-zinc-100
flex flex-wrap
items-center justify-center p-10 gap-2
overflow-x-hidden
" "
> >
<div
class="h-full overflow-y-auto
flex flex-wrap md:flex-row
items-center justify-center p-2 md:p-10 gap-4
overflow-hidden select-none"
>
<a <a
class="fixed top-4 right-4" class="fixed top-4 right-4"
href="https://git.jroeger.de/jonas/wordle-solver-rs" href="https://git.jroeger.de/jonas/wordle-solver-rs"
@@ -150,6 +166,15 @@
> >
<FolderGit2 /> <FolderGit2 />
</a> </a>
<a
class="fixed top-4 left-4"
onclick={() => {
show_info = true;
}}
>
<Info />
</a>
<div class="text-center"> <div class="text-center">
<h1 class="mb-4 text-3xl font-bold">Wordle Solver</h1> <h1 class="mb-4 text-3xl font-bold">Wordle Solver</h1>
@@ -179,11 +204,13 @@
{/await} {/await}
</p> </p>
<div class="ml-16 mt-2 flex flex-col gap-2"> <div class="mt-2 flex flex-col gap-2">
{#each guesses as g, i (g)} {#each guesses as g, i (g)}
<WordleLine <WordleLine
active={i === guesses.length - 1} active={i === guesses.length - 1}
on_guess_pressed={i === guesses.length - 1 ? doGuess : null} on_guess_pressed={i === guesses.length - 1
? doGuess
: null}
bind:this={w_lines[i]} bind:this={w_lines[i]}
bind:cursor bind:cursor
bind:letters={guesses[i].letters} bind:letters={guesses[i].letters}
@@ -193,14 +220,16 @@
{/each} {/each}
</div> </div>
{#if is_desktop}
<p class="mt-4 text-sm text-zinc-500 mx-auto w-3/4"> <p class="mt-4 text-sm text-zinc-500 mx-auto w-3/4">
Type letters · ← → navigate · Space cycle color · Backspace delete · Type letters · ← → navigate · Space cycle color · Backspace
Enter apply · Shift+Enter solve · Escape abort solver · Ctrl+Esc delete · Enter apply · Shift+Enter solve · Escape abort
reset solver · Ctrl+Esc reset
</p> </p>
{/if}
<button <button
tabindex="-1" tabindex="-1"
class="mt-2 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-full" class="mt-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-full"
onclick={(e) => { onclick={(e) => {
e.currentTarget.blur(); e.currentTarget.blur();
toggleSolver(); toggleSolver();
@@ -223,4 +252,61 @@
bind:solving bind:solving
/> />
{/if} {/if}
</div>
{#if show_info}
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60"
tabindex="-1"
role="button"
onclick={() => (show_info = false)}
onkeydown={() => (show_info = false)}
>
<div
class="relative flex flex-col
rounded-lg
bg-zinc-900
p-6 shadow-xl
md:max-w-[75vw]
md:max-h-[75vh]
max-w-[calc(100vw-3rem)]
max-h-[calc(100vh-3rem)]
overflow-hidden"
tabindex="0"
role="dialog"
onclick={(e) => e.stopPropagation()}
onkeydown={(e) => e.stopPropagation()}
>
<div
class="absolute top-4 right-4"
tabindex="-1"
role="button"
onclick={() => (show_info = false)}
onkeydown={() => (show_info = false)}
>
<CircleX />
</div>
<div
class="prose prose-invert max-w-none
h-full
overflow-y-auto
overflow-x-hidden"
>
<SvelteMarkdown source={info_md} />
</div>
</div>
</div>
{/if}
</div> </div>
<style>
:global(body) {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
overscroll-behavior: none;
background: #09090b;
}
</style>
+1
View File
@@ -1,4 +1,5 @@
@import "tailwindcss"; @import "tailwindcss";
@plugin "@tailwindcss/typography";
html, html,
body { body {
+45
View File
@@ -0,0 +1,45 @@
# Wordle Solver
This wordle solver uses an information theory based approach to
evaluate the next-best-guess. It's heavily inspired by [3b1b's approach](https://www.youtube.com/watch?v=v68zYyaEmEA)
on solving wordle.
## How to use?
You can run the solver at any time to tell you five best next guesses (ranked).
However when the solver has no information this can take quite some time. So it's
best to make your first guess (e.g. "CRANE", "SALES", ...):
1. Once your wordle game gave you an answer, type it into the interface:
- type letters to input them
- click a cell to change it's color (or cycle it with <Space>)
2. To give the solver the information of your guess press enter (or the little enter icon next to the line)
3. The solver will tell you the amount of information gained by this guess.
4. Run the solver to get a new next-best-guess and repeat until you found the answer
## Solution Space
The solution space is by default the list of possible wordle-answers (uncheck the box to use all valid words as possible answers).
Above your inputs you can see how many possible answers are still left given your guesses with a value of
remaining uncertainty in bits (how many yes-no questions are required to identify an item in the equally distributed set).
The goal is to reach 0bits of uncertainty, meaning one word left.
Sometimes when adding a guess there may appear an error symbol. This occurrs when the guess would empty
the solution space. This can only happen if the solution space did not contain the solution of your wordle game (or there some inconsistencies in the guesses).
In this case you might wanna allow all valid words as possible answers.
## The Solver's output
- p(x) is the probability, that the word is actually the solution.
Often it will be 0, because the word is not a possible solution anymore.
If so, the expected information of guessing this that high, that it is worth guessing it.
- E\[Inf.\] The expected information to gain when guessing this word. (I.e. by how much do we expect to shrink the solution space?)
- E\[Score.\] The expected number of guesses required to solve the wordle when you guess this word now. It's based on a heuristic combination
of the current score, p(x) and E\[Inf.\].
+6 -4
View File
@@ -48,9 +48,11 @@
{/if} {/if}
</div> </div>
{:then d} {:then d}
<div class="rounded-lg border border-gray-500 shadow-sm max-w-full"> <div
class="select-none rounded-lg border border-gray-500 shadow-sm max-w-full"
>
<table class="w-full text-left text-sm text-gray-700"> <table class="w-full text-left text-sm text-gray-700">
<thead class="bg-gray-800 text-xs uppercase text-gray-600"> <thead class="divide-y divide-x bg-gray-800 text-xs text-gray-600">
<tr> <tr>
<th class="px-4 py-3" title="Rank the solver assigned"></th> <th class="px-4 py-3" title="Rank the solver assigned"></th>
<th class="px-4 py-3" title="The best words">Word</th> <th class="px-4 py-3" title="The best words">Word</th>
@@ -72,7 +74,7 @@
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-500"> <tbody class="divide-y divide-x divide-gray-500">
{#each d.stats as s, i} {#each d.stats as s, i}
<tr <tr
class="transition-colors hover:bg-gray-50" class="transition-colors hover:bg-gray-50"
@@ -99,7 +101,7 @@
</table> </table>
</div> </div>
{:catch e} {:catch e}
<div class="rounded-lg border border-red-200 bg-red-50 p-4 text-red-700"> <div class="rounded-lg border border-red-400 bg-gray-800 p-4 text-red-400">
{e} {e}
</div> </div>
{/await} {/await}
+36 -13
View File
@@ -24,7 +24,20 @@
on_guess_pressed: (() => void) | null; on_guess_pressed: (() => void) | null;
} = $props(); } = $props();
let invalid: boolean = $state(false);
let inputs: HTMLInputElement[] = $state([]); let inputs: HTMLInputElement[] = $state([]);
let info_gained_wrapped: Promise<GuessResult> | null = $state(null);
$effect(() => {
if (info_gained) {
info_gained_wrapped = info_gained.catch((e) => {
invalid = true;
throw e;
});
} else {
info_gained_wrapped = null;
}
});
export function focus() { export function focus() {
inputs[cursor].focus(); inputs[cursor].focus();
@@ -37,7 +50,9 @@
if (event.key === "Backspace") { if (event.key === "Backspace") {
if (letters[cursor] === "" && cursor > 0) { if (letters[cursor] === "" && cursor > 0) {
cursor--; const nc = cursor - 1;
inputs[nc].focus(); // Focusing / bluring resets cursor temporarily
cursor = nc;
} }
if (active) { if (active) {
@@ -46,13 +61,15 @@
} }
if (event.key === "ArrowLeft") { if (event.key === "ArrowLeft") {
cursor = Math.max(0, cursor - 1); const nc = Math.max(0, cursor - 1);
inputs[cursor].focus(); inputs[nc].focus();
cursor = nc;
} }
if (event.key === "ArrowRight") { if (event.key === "ArrowRight") {
cursor = Math.min(4, cursor + 1); const nc = Math.min(4, cursor + 1);
inputs[cursor].focus(); inputs[nc].focus();
cursor = nc;
} }
if (event.key === " ") { if (event.key === " ") {
@@ -69,7 +86,7 @@
disabled={!active} disabled={!active}
tabindex="-1" tabindex="-1"
class=" class="
flex h-16 w-16 items-center justify-center flex aspect-square max-w-16 w-1/6 items-center justify-center
rounded-lg border-2 rounded-lg border-2
text-3xl font-bold text-3xl font-bold
transition-all text-center caret-transparent transition-all text-center caret-transparent
@@ -82,6 +99,10 @@
class:bg-green-600={colors[i] === Color.GREEN} class:bg-green-600={colors[i] === Color.GREEN}
class:bg-yellow-600={colors[i] === Color.YELLOW} class:bg-yellow-600={colors[i] === Color.YELLOW}
class:bg-grey-600={colors[i] === Color.GREY} class:bg-grey-600={colors[i] === Color.GREY}
class:opacity-20={invalid}
onblur={() => {
cursor = -1;
}}
onbeforeinput={(event) => { onbeforeinput={(event) => {
if (event.inputType === "insertText") { if (event.inputType === "insertText") {
event.preventDefault(); event.preventDefault();
@@ -89,12 +110,14 @@
if (/^[a-zA-Z]$/.test(key)) { if (/^[a-zA-Z]$/.test(key)) {
letters[i] = key.toUpperCase(); letters[i] = key.toUpperCase();
if (cursor < 4) { let nc = cursor;
cursor++; if (nc < 4) {
nc++;
} else { } else {
cursor = 0; nc = 0;
} }
inputs[cursor].focus(); inputs[nc].focus();
cursor = nc;
} }
} }
}} }}
@@ -111,13 +134,13 @@
/> />
{/each} {/each}
<span <span
class="w-16 text-sm text-zinc-500 self-center flex items-center justify-center" class="max-w-14 w-1/6 text-sm text-zinc-500 self-center flex items-center justify-center"
onclick={info_gained === null ? on_guess_pressed : null} onclick={info_gained === null ? on_guess_pressed : null}
> >
{#if info_gained === null} {#if info_gained_wrapped === null}
{:else} {:else}
{#await info_gained} {#await info_gained_wrapped}
<div <div
class="size-3 animate-spin rounded-full border-2 border-gray-300 border-t-gray-700" class="size-3 animate-spin rounded-full border-2 border-gray-300 border-t-gray-700"
></div> ></div>
+56 -5
View File
@@ -1,9 +1,6 @@
import type { Color } from "wordle-solver"; import type { Color } from "wordle-solver";
import SolverWorker from "./worker?worker"; import SolverWorker from "./worker?worker";
import type { GuessResult, SolutionSpaceResult, EvaluateWordsResult, SolverRequest, SolverResponse, EvaluateWordsProgress } from "./types"; import type { GuessResult, SolutionSpaceResult, EvaluateWordsResult, SolverRequest, SolverResponse, EvaluateWordsProgress, SolverStateResult, RestoreSolverState } from "./types";
let solver_worker = new SolverWorker();
await init_solver();
let msg_id = 0; let msg_id = 0;
@@ -14,6 +11,12 @@ const pending = new Map<number, {
const progresses = new Map<number, (p: EvaluateWordsProgress) => void>(); const progresses = new Map<number, (p: EvaluateWordsProgress) => void>();
let solver_worker = new SolverWorker();
await init_solver();
let last_solver_state = await solver_state();
function init_solver(): Promise<void> { function init_solver(): Promise<void> {
return new Promise((resolve) => { return new Promise((resolve) => {
solver_worker.onmessage = (event: any) => { solver_worker.onmessage = (event: any) => {
@@ -65,13 +68,15 @@ export function solver_solution_space(): Promise<SolutionSpaceResult> {
}); });
} }
export function solver_evaluate_words(n: number, progress: ((p: EvaluateWordsProgress) => void) | null): Promise<EvaluateWordsResult> { export async function solver_evaluate_words(n: number, progress: ((p: EvaluateWordsProgress) => void) | null): Promise<EvaluateWordsResult> {
const id = msg_id++; const id = msg_id++;
if (progress) { if (progress) {
progresses.set(id, progress); progresses.set(id, progress);
} }
last_solver_state = await solver_state();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject }); pending.set(id, { resolve, reject });
@@ -85,6 +90,52 @@ export function solver_evaluate_words(n: number, progress: ((p: EvaluateWordsPro
}); });
} }
function solver_state(): Promise<SolverStateResult> {
const id = msg_id++;
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject });
const r: SolverRequest = {
type: "solver_state",
id: id,
input: null,
};
solver_worker.postMessage(JSON.parse(JSON.stringify(r)));
});
}
function restore_solver_state(s: RestoreSolverState): Promise<void> {
const id = msg_id++;
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject });
const r: SolverRequest = {
type: "restore_solver_state",
id: id,
input: s,
};
solver_worker.postMessage(r);
});
}
export async function solver_stop(): Promise<void> {
solver_worker.terminate();
pending.forEach((v, _) => {
v.reject("Solver was stopped");
});
pending.clear();
progresses.clear();
await create_solver_worker();
await restore_solver_state(last_solver_state);
}
export async function solver_reset(wordle_answers: boolean): Promise<void> { export async function solver_reset(wordle_answers: boolean): Promise<void> {
solver_worker.terminate(); solver_worker.terminate();
+30
View File
@@ -20,8 +20,19 @@ export type SolverRequest =
type: "reload_wordlist", type: "reload_wordlist",
id: number, id: number,
input: ReloadWordlist input: ReloadWordlist
}
| {
type: "solver_state",
id: number,
input: null
}
| {
type: "restore_solver_state",
id: number,
input: RestoreSolverState
}; };
export type SolverResponse = export type SolverResponse =
| { | {
type: "guess", type: "guess",
@@ -52,8 +63,21 @@ export type SolverResponse =
id: number, id: number,
error: string | null error: string | null
result: boolean | null, result: boolean | null,
}
| {
type: "solver_state",
id: number,
error: string | null
result: SolverStateResult | null,
}
| {
type: "restore_solver_state",
id: number,
error: string | null
result: boolean | null,
}; };
export type Guess = { export type Guess = {
word: string[], word: string[],
colors: Color[] colors: Color[]
@@ -84,3 +108,9 @@ export type SolutionSpaceResult = {
size: number, size: number,
uncertainty: number uncertainty: number
}; };
export type SolverStateResult = {
raw: Uint8Array
};
export type RestoreSolverState = {
raw: Uint8Array
};
+34
View File
@@ -99,6 +99,40 @@ self.onmessage = (event: MessageEvent<SolverRequest>) => {
self.postMessage(JSON.parse(JSON.stringify(response))); self.postMessage(JSON.parse(JSON.stringify(response)));
break; break;
}; };
case "solver_state": {
let response: SolverResponse = {
type: "solver_state",
id: request.id,
error: null,
result: null
};
try {
response.result = { raw: solver.serialize() };
} catch (e) {
response.error = e as string;
}
self.postMessage(response);
break;
};
case "restore_solver_state": {
let response: SolverResponse = {
type: "restore_solver_state",
id: request.id,
error: null,
result: null
};
try {
solver.free();
solver = Solver.new_from_serialized(request.input.raw);
response.result = true;
} catch (e) {
response.error = e as string;
}
self.postMessage(response);
break;
}
} }
}; };
+95
View File
@@ -154,6 +154,21 @@
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz#b21affb804cc167c133d95f45b3a1dc1323b9a87" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz#b21affb804cc167c133d95f45b3a1dc1323b9a87"
integrity sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g== integrity sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==
"@humanspeak/memory-cache@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@humanspeak/memory-cache/-/memory-cache-1.1.2.tgz#2bd14a51782b30232fb544dab54fc20efb0171a8"
integrity sha512-6kmeqrCVThw4RYZsJ74l9cOdZQ84XHnGIzrv+Po49CZBNNVDEzi0CGsTx+KKmr/Hb6hY6yDGxeWTiz4VHkdsEg==
"@humanspeak/svelte-markdown@^1.8.7":
version "1.8.7"
resolved "https://registry.yarnpkg.com/@humanspeak/svelte-markdown/-/svelte-markdown-1.8.7.tgz#30f21678adf8434ac35f22c8cdd4ddf0666ee08c"
integrity sha512-IJFFF7QuKytGBIDy/fp8xeAxMjuFfV1rt3H8t6kjDlIckVTwCTD6WPFIbirlgeingOdD6j4U6Ox8aHu0YTe2Xw==
dependencies:
"@humanspeak/memory-cache" "^1.1.2"
github-slugger "^2.0.0"
htmlparser2 "^12.0.0"
marked "^18.0.9"
"@jridgewell/gen-mapping@^0.3.5": "@jridgewell/gen-mapping@^0.3.5":
version "0.3.13" version "0.3.13"
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f"
@@ -626,6 +641,13 @@
"@tailwindcss/oxide-win32-arm64-msvc" "4.3.3" "@tailwindcss/oxide-win32-arm64-msvc" "4.3.3"
"@tailwindcss/oxide-win32-x64-msvc" "4.3.3" "@tailwindcss/oxide-win32-x64-msvc" "4.3.3"
"@tailwindcss/typography@^0.5.20":
version "0.5.20"
resolved "https://registry.yarnpkg.com/@tailwindcss/typography/-/typography-0.5.20.tgz#9feb7fb1d5f2f7b5360c22e24651210db74c34df"
integrity sha512-hwbzQuNUfcPvbegQFatVPl/MY/tcM9KLl963hQ5laJKPh81TEZ1+dNG9PirGvcaDBkp+BCshExAyKVPW91dozw==
dependencies:
postcss-selector-parser "6.0.10"
"@tailwindcss/vite@^4.3.3": "@tailwindcss/vite@^4.3.3":
version "4.3.3" version "4.3.3"
resolved "https://registry.yarnpkg.com/@tailwindcss/vite/-/vite-4.3.3.tgz#1f4037badcb5e6c6bb574d3d8a0172c83f26b97f" resolved "https://registry.yarnpkg.com/@tailwindcss/vite/-/vite-4.3.3.tgz#1f4037badcb5e6c6bb574d3d8a0172c83f26b97f"
@@ -793,6 +815,11 @@ cross-spawn@^6.0.5:
shebang-command "^1.2.0" shebang-command "^1.2.0"
which "^1.2.9" which "^1.2.9"
cssesc@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
data-view-buffer@^1.0.2: data-view-buffer@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz#211a03ba95ecaf7798a8c7198d79536211f88570" resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz#211a03ba95ecaf7798a8c7198d79536211f88570"
@@ -853,6 +880,36 @@ devalue@^5.8.1:
resolved "https://registry.yarnpkg.com/devalue/-/devalue-5.9.0.tgz#5d30db41a0db9171cf4ee9dbf6617e0b77cd7c2a" resolved "https://registry.yarnpkg.com/devalue/-/devalue-5.9.0.tgz#5d30db41a0db9171cf4ee9dbf6617e0b77cd7c2a"
integrity sha512-RWrqdArjvPbsATEhOPUo6Wndc/iWnkWKlhIrdlF3zMMYo/c3CVtoaVAyLtWxz5h8nSlkHzxnzV2uLydPXmtF+A== integrity sha512-RWrqdArjvPbsATEhOPUo6Wndc/iWnkWKlhIrdlF3zMMYo/c3CVtoaVAyLtWxz5h8nSlkHzxnzV2uLydPXmtF+A==
dom-serializer@^3.0.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-3.1.1.tgz#54be70ee4fcc2da010f488165e62294798dc57d3"
integrity sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==
dependencies:
domelementtype "^3.0.0"
domhandler "^6.0.0"
entities "^8.0.0"
domelementtype@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-3.0.0.tgz#e6c0a24bd39ca5eb7ea67a98d2f699497d7bcc55"
integrity sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==
domhandler@^6.0.0:
version "6.0.1"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-6.0.1.tgz#75f351a03a6e10c35e08418f9cf0d287e00797cf"
integrity sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==
dependencies:
domelementtype "^3.0.0"
domutils@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-4.0.2.tgz#0c1ac1fdbe8f554a60a6f5eb143ee85630327c94"
integrity sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==
dependencies:
dom-serializer "^3.0.0"
domelementtype "^3.0.0"
domhandler "^6.0.0"
dunder-proto@^1.0.0, dunder-proto@^1.0.1: dunder-proto@^1.0.0, dunder-proto@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
@@ -870,6 +927,11 @@ enhanced-resolve@^5.24.1:
graceful-fs "^4.2.4" graceful-fs "^4.2.4"
tapable "^2.3.3" tapable "^2.3.3"
entities@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-8.0.0.tgz#c1df5fe3602429747fa233d0dd26f142f0ce4743"
integrity sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==
error-ex@^1.3.1: error-ex@^1.3.1:
version "1.3.4" version "1.3.4"
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.4.tgz#b3a8d8bb6f92eecc1629e3e27d3c8607a8a32414" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.4.tgz#b3a8d8bb6f92eecc1629e3e27d3c8607a8a32414"
@@ -1115,6 +1177,11 @@ get-symbol-description@^1.1.0:
es-errors "^1.3.0" es-errors "^1.3.0"
get-intrinsic "^1.2.6" get-intrinsic "^1.2.6"
github-slugger@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-2.0.0.tgz#52cf2f9279a21eb6c59dd385b410f0c0adda8f1a"
integrity sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==
globalthis@^1.0.4: globalthis@^1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236"
@@ -1181,6 +1248,16 @@ hosted-git-info@^2.1.4:
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
htmlparser2@^12.0.0:
version "12.0.0"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-12.0.0.tgz#6a679d0f57c525990f9cbad8a585b320ecc6d198"
integrity sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw==
dependencies:
domelementtype "^3.0.0"
domhandler "^6.0.0"
domutils "^4.0.2"
entities "^8.0.0"
internal-slot@^1.1.0: internal-slot@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.1.0.tgz#1eac91762947d2f7056bc838d93e13b2e9604961" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.1.0.tgz#1eac91762947d2f7056bc838d93e13b2e9604961"
@@ -1579,6 +1656,11 @@ magic-string@^1.0.0:
dependencies: dependencies:
"@jridgewell/sourcemap-codec" "^1.5.5" "@jridgewell/sourcemap-codec" "^1.5.5"
marked@^18.0.9:
version "18.0.10"
resolved "https://registry.yarnpkg.com/marked/-/marked-18.0.10.tgz#a09ac1f54e378b9efc8b31707adc188fdc047d2e"
integrity sha512-FJeH4bRpYoXiggcgriCGItKCSv3xkngJc4QCZ/rkQCogU3VYaLxYJoZl8Nw/b4+x7iij/pd+09mZ6A1dXzpL0A==
math-intrinsics@^1.1.0: math-intrinsics@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
@@ -1718,6 +1800,14 @@ possible-typed-array-names@^1.0.0, possible-typed-array-names@^1.1.0:
resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae" resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae"
integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg== integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==
postcss-selector-parser@6.0.10:
version "6.0.10"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d"
integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==
dependencies:
cssesc "^3.0.0"
util-deprecate "^1.0.2"
postcss@^8.5.25: postcss@^8.5.25:
version "8.5.26" version "8.5.26"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.26.tgz#6e75135780c7e10df3433bf2266c552d35c8c620" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.26.tgz#6e75135780c7e10df3433bf2266c552d35c8c620"
@@ -2155,6 +2245,11 @@ unbox-primitive@^1.1.0:
has-symbols "^1.1.0" has-symbols "^1.1.0"
which-boxed-primitive "^1.1.1" which-boxed-primitive "^1.1.1"
util-deprecate@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
uuid@10.0.0: uuid@10.0.0:
version "10.0.0" version "10.0.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294" resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294"