Compare commits

..
28 Commits
Author SHA1 Message Date
jonas c1f14a5bba build(docker): remove bash from web image
Build docker images. / build-wordle-solver-image (push) Successful in 31s
Build docker images. / build-wordle-solver-web-image (push) Successful in 5m9s
2026-09-10 16:56:31 +02:00
jonas 0cad77fb39 docs: add readme
Build docker images. / build-wordle-solver-image (push) Successful in 55s
Build docker images. / build-wordle-solver-web-image (push) Successful in 8m44s
2026-09-10 16:27:27 +02:00
jonas 14e22db8ec web: add serve cmd 2026-09-10 16:27:17 +02:00
jonas a827bd5eef ci: push latest
Build docker images. / build-wordle-solver-image (push) Successful in 44s
Build docker images. / build-wordle-solver-web-image (push) Successful in 5m21s
2026-09-08 18:24:46 +02:00
jonas 911ae77516 ci: fix job script running
Build docker images. / build-wordle-solver-image (push) Successful in 48s
Build docker images. / build-wordle-solver-web-image (push) Successful in 6m40s
2026-09-08 18:03:22 +02:00
jonas 6707364251 ci: add docker jobs
Build docker images. / build-wordle-solver-image (push) Failing after 12m1s
Build docker images. / build-wordle-solver-web-image (push) Failing after 14m6s
2026-09-08 17:46:03 +02:00
jonas 9b34a045fa build: add docker images 2026-09-08 17:34:07 +02:00
jonas 5ea156c181 build(flake): split into flake modules 2026-09-06 16:39:29 +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
jonas d1eb2d0706 feat(web): add click-only interaction 2026-08-22 19:56:26 +02:00
jonas 726d35aaea chore(web): remove logs 2026-08-22 19:56:12 +02:00
jonas 3d78d1a429 feat(web): ui improvements
- flex-wrap elements
- refine output table
- reset state
2026-08-22 18:34:42 +02:00
jonas 06a1e0d707 feat(web): use input types as cells 2026-08-22 01:24:41 +02:00
jonas 6ddffce672 fix(web): disable click on inactive lines 2026-08-21 21:23:57 +02:00
jonas b36cdbb0dc feat(web): add rank indicator 2026-08-21 21:11:32 +02:00
jonas 7c8ac0c447 feat(web): add repo link 2026-08-21 21:07:30 +02:00
jonas 2d147fd97a feat(web): solver output enhancing
- math render header
- tooltips
- copy word to input
- cancel solver
2026-08-21 20:35:22 +02:00
jonas 539534ad90 feat(web): optional wordle-answers 2026-08-21 17:57:45 +02:00
jonas 87dfa0c142 fix(yarn): release bundle 2026-08-21 16:09:12 +02:00
jonas 7667ff24cd feat(web): add solver output 2026-08-21 15:49:24 +02:00
jonas 8d501d42a3 feat(web): add solution space info 2026-08-21 14:14:24 +02:00
jonas 3475a5338b feat(web): add async solver worker 2026-08-21 02:25:08 +02:00
jonas fcb215f4c7 feat(lib): add async solver capabilities 2026-08-20 21:48:23 +02:00
32 changed files with 1654 additions and 292 deletions
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -euo pipefail
REGISTRY="${REGISTRY#https://}"
REGISTRY="${REGISTRY#http://}"
REGISTRY="${REGISTRY%/}"
IMAGE="${REGISTRY}/${REPOSITORY}:${TAG}"
echo "Pushing ${IMAGE}"
skopeo copy \
--dest-creds "${USERNAME}:${PASSWORD}" \
"docker-archive:result" \
"docker://${IMAGE}"
if [ "${REF_NAME:-}" = "main" ]; then
skopeo copy \
--dest-creds "${USERNAME}:${PASSWORD}" \
"docker-archive:result" \
"docker://${REGISTRY}/${REPOSITORY}:latest"
fi
+47
View File
@@ -0,0 +1,47 @@
name: Build docker images.
on:
push:
jobs:
build-wordle-solver-image:
runs-on: nixos
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Build wordle-solver image
run: |
nix build .#wordle-solver-docker
- name: Push image
env:
REGISTRY: "${{ gitea.server_url }}"
REPOSITORY: "${{ gitea.repository }}"
USERNAME: "${{ secrets.REGISTRY_USER }}"
PASSWORD: "${{ secrets.REGISTRY_TOKEN }}"
REF_NAME: "${{ gitea.ref_name }}"
TAG: "${{ gitea.sha }}"
run: bash .gitea/scripts/push-image.sh
build-wordle-solver-web-image:
runs-on: nixos
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Build wordle-solver-web image
run: |
nix build .#wordle-solver-web-docker
- name: Push image
env:
REGISTRY: "${{ gitea.server_url }}"
REPOSITORY: "${{ gitea.repository }}-web"
USERNAME: "${{ secrets.REGISTRY_USER }}"
PASSWORD: "${{ secrets.REGISTRY_TOKEN }}"
REF_NAME: "${{ gitea.ref_name }}"
TAG: "${{ gitea.sha }}"
run: bash .gitea/scripts/push-image.sh
Generated
+1
View File
@@ -476,6 +476,7 @@ dependencies = [
"derive_more", "derive_more",
"getrandom", "getrandom",
"indicatif", "indicatif",
"js-sys",
"rand", "rand",
"rayon", "rayon",
"tabular", "tabular",
+2 -1
View File
@@ -24,6 +24,7 @@ colored = "3.1.1"
derive_more = { version = "2.1.1", features = [ "debug", "display" ] } derive_more = { version = "2.1.1", features = [ "debug", "display" ] }
getrandom = { version = "0.4.3", optional = true } getrandom = { version = "0.4.3", optional = true }
indicatif = { version = "0.18.6", features = ["rayon"] } indicatif = { version = "0.18.6", features = ["rayon"] }
js-sys = { version = "0.3.104", optional = true }
rand = "0.10.2" rand = "0.10.2"
rayon = "1.12.0" rayon = "1.12.0"
tabular = { version = "0.2.0", features = ["ansi-cell"] } tabular = { version = "0.2.0", features = ["ansi-cell"] }
@@ -31,4 +32,4 @@ wasm-bindgen = { version = "0.2.127", optional = true }
[features] [features]
default = [] default = []
wasm-bindgen = ["dep:wasm-bindgen", "getrandom/wasm_js"] wasm-bindgen = ["dep:wasm-bindgen", "getrandom/wasm_js", "dep:js-sys"]
+60
View File
@@ -0,0 +1,60 @@
# 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.
There are two ways to use the wordle-solver.
1. a CLI tool
2. a web interface
# Docker
#### CLI
```sh
docker run --rm -it git.jroeger.de/jonas/wordle-solver-rs
```
#### web
``` sh
docker run -p 8080:80 --rm -it git.jroeger.de/jonas/wordle-solver-rs-web
```
# Nix
#### CLI
``` sh
nix run .
```
#### Static web site
``` sh
nix build git+http://git.jroeger.de/jonas/wordle-solver-rs#wordle-solver-web
```
# Native
#### CLI
you will need to have `cargo` installed.
```sh
cargo run --profile release
```
#### web
you need the following tools:
- `wasm-pack`
- `wasm-bindgen`
- `cargo`
- `nodejs`
- `yarn`
``` sh
cd web
yarn install
yarn serve # or yarn build
```
+14 -33
View File
@@ -16,7 +16,6 @@
outputs = inputs @ { outputs = inputs @ {
flake-parts, flake-parts,
crate2nix,
rust-overlay, rust-overlay,
... ...
}: }:
@@ -28,33 +27,24 @@
"x86_64-linux" "x86_64-linux"
]; ];
imports = [
./nix/wasm-bindgen.nix
./nix/wordle-solver-docker.nix
./nix/wordle-solver-wasm-pkg.nix
./nix/wordle-solver-wasm.nix
./nix/wordle-solver-web-docker.nix
./nix/wordle-solver-web.nix
./nix/wordle-solver.nix
];
perSystem = { perSystem = {
pkgs, pkgs,
system, system,
self', self',
... ...
}: let }: let
# Use rust-bin to generate the toolchain from rust-toolchain.toml utils = import ./nix/utils.nix;
rust-toolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; rust-toolchain = utils.rust-toolchain pkgs;
cross-pkgs = pkgs.pkgsCross.wasm32-unknown-none;
toolchain = pkgs.callPackage ./nix/with-toolchain.nix {
cargo = rust-toolchain;
rustc = rust-toolchain;
};
crossToolchain = cross-pkgs.callPackage ./nix/with-toolchain.nix {
cargo = rust-toolchain;
rustc = rust-toolchain;
};
wasm-bindgen = toolchain.pkgs.callPackage ./nix/wasm-bindgen.nix {};
root-crate = pkgs.callPackage ./nix/root-crate.nix {
cargo = rust-toolchain;
rustc = rust-toolchain;
inherit crate2nix;
};
in { in {
# Overlay pkgs with rust-bin # Overlay pkgs with rust-bin
_module.args.pkgs = import inputs.nixpkgs { _module.args.pkgs = import inputs.nixpkgs {
@@ -63,22 +53,13 @@
config.allowUnsupportedSystem = true; config.allowUnsupportedSystem = true;
}; };
packages = { packages.default = self'.packages.wordle-solver;
wordle-solver-web = pkgs.callPackage ./nix/wordle-solver-web.nix {inherit (self'.packages) wordle-solver-wasm-pkg;};
wordle-solver-wasm = crossToolchain.pkgs.callPackage ./nix/wordle-solver-wasm.nix {};
wordle-solver-wasm-pkg = pkgs.callPackage ./nix/wordle-solver-wasm-pkg.nix {
inherit wasm-bindgen root-crate;
inherit (self'.packages) wordle-solver-wasm;
};
wordle-solver = root-crate.build;
default = self'.packages.wordle-solver;
};
devShells.default = pkgs.mkShell { devShells.default = pkgs.mkShell {
RA_LOG = "rust_analyzer=info"; RA_LOG = "rust_analyzer=info";
buildInputs = [ buildInputs = [
rust-toolchain rust-toolchain
wasm-bindgen self'.packages.wasm-bindgen
pkgs.nodejs pkgs.nodejs
pkgs.svelte-language-server pkgs.svelte-language-server
pkgs.typescript-language-server pkgs.typescript-language-server
-23
View File
@@ -1,23 +0,0 @@
{
crate2nix,
rustc,
cargo,
pkgs,
...
}: let
buildRustCrateForPkgs = _:
pkgs.buildRustCrate.override {
inherit cargo rustc;
};
# Cargo.nix for IFD
generatedCargoNix = crate2nix.tools.${pkgs.hostPlatform.system}.generatedCargoNix {
name = "rustnix";
src = ../.;
};
cargoNix = import generatedCargoNix {
inherit pkgs buildRustCrateForPkgs;
};
in
cargoNix.rootCrate
+21
View File
@@ -0,0 +1,21 @@
rec {
rust-toolchain = pkgs: pkgs.rust-bin.fromRustupToolchainFile ../rust-toolchain.toml;
toolchain-pkgs = pkgs:
pkgs.extend (_: _: {
rustPlatform = pkgs.makeRustPlatform {
cargo = rust-toolchain pkgs;
rustc = rust-toolchain pkgs;
};
});
toolchain-crosspkgs = pkgs: let
crossPkgs = pkgs.pkgsCross.wasm32-unknown-none;
in
crossPkgs.extend (_: _: {
rustPlatform = crossPkgs.makeRustPlatform {
cargo = rust-toolchain pkgs;
rustc = rust-toolchain pkgs;
};
});
}
+9 -1
View File
@@ -1,4 +1,5 @@
{ let
drv = {
fetchCrate, fetchCrate,
buildWasmBindgenCli, buildWasmBindgenCli,
rustPlatform, rustPlatform,
@@ -16,4 +17,11 @@ buildWasmBindgenCli rec {
inherit (src) pname version; inherit (src) pname version;
hash = "sha256-FTv2GZIAQs0ePdIZXIXil7JbZ6kIT05VG6vqC1qNFxQ="; hash = "sha256-FTv2GZIAQs0ePdIZXIXil7JbZ6kIT05VG6vqC1qNFxQ=";
}; };
};
utils = import ./utils.nix;
in {
perSystem = {pkgs, ...}: {
packages.wasm-bindgen = (utils.toolchain-pkgs pkgs).callPackage drv {};
};
} }
-16
View File
@@ -1,16 +0,0 @@
{
makeRustPlatform,
cargo,
rustc,
pkgs,
...
}: let
p = pkgs;
in rec {
rustPlatform = makeRustPlatform {
inherit cargo rustc;
};
pkgs = p.extend (_: _: {
inherit rustPlatform;
});
}
+39
View File
@@ -0,0 +1,39 @@
let
drv = {
buildEnv,
dockerTools,
wordle-solver,
runCommand,
...
}:
dockerTools.buildImage {
name = "wordle-solver";
created = "now";
copyToRoot = buildEnv {
name = "image-root";
paths = [
wordle-solver
(runCommand "wordle-data" {} ''
mkdir -p $out/pwd
cp ${../valid-words.txt} $out/pwd/valid-words.txt
cp ${../answers.txt} $out/pwd/answers.txt
'')
];
pathsToLink = ["/bin" "/pwd"];
};
config = {
Cmd = ["/bin/wordle-solver"];
WorkingDir = "/pwd";
};
};
in {
perSystem = {
pkgs,
self',
...
}: {
packages.wordle-solver-docker = pkgs.callPackage drv {
inherit (self'.packages) wordle-solver;
};
};
}
+13 -2
View File
@@ -1,8 +1,13 @@
{ {
perSystem = {
pkgs,
self',
...
}: let
drv = {
stdenvNoCC, stdenvNoCC,
wasm-bindgen, wasm-bindgen,
wordle-solver-wasm, wordle-solver-wasm,
root-crate,
... ...
}: let }: let
snake_name = "wordle_solver"; snake_name = "wordle_solver";
@@ -10,7 +15,7 @@
name = "wordle-solver"; name = "wordle-solver";
type = "module"; type = "module";
description = "module"; description = "module";
version = root-crate.build.crateVersion; version = "0.1.0";
files = [ files = [
"${snake_name}_bg.wasm" "${snake_name}_bg.wasm"
"${snake_name}.js" "${snake_name}.js"
@@ -36,4 +41,10 @@ in
${package-json} ${package-json}
EOF EOF
''; '';
};
in {
packages.wordle-solver-wasm-pkg = pkgs.callPackage drv {
inherit (self'.packages) wordle-solver-wasm wasm-bindgen;
};
};
} }
+20 -3
View File
@@ -1,8 +1,14 @@
{ {
perSystem = {pkgs, ...}: let
utils = import ./utils.nix;
rust-toolchain = utils.rust-toolchain pkgs;
toolchain-crosspkgs = utils.toolchain-crosspkgs pkgs;
drv = {
rustPlatform, rustPlatform,
buildPackages, buildPackages,
lld, lld,
name ? "wordle-solver", name ? "wordle-solver",
removeReferencesTo,
... ...
}: }:
rustPlatform.buildRustPackage { rustPlatform.buildRustPackage {
@@ -19,6 +25,7 @@ rustPlatform.buildRustPackage {
buildPackages.stdenv.cc buildPackages.stdenv.cc
lld # Needed for wasm-ld lld # Needed for wasm-ld
]; ];
nativeBuildInputs = [removeReferencesTo];
cargoBuildFlags = [ cargoBuildFlags = [
"--lib" "--lib"
@@ -27,13 +34,23 @@ rustPlatform.buildRustPackage {
]; ];
# Configure rustc for WASM # Configure rustc for WASM
env = { RUSTFLAGS = ''--cfg=web_sys_unstable_apis -C linker=wasm-ld -C strip=debuginfo -C debuginfo=0'';
RUSTFLAGS = ''--cfg=web_sys_unstable_apis -C linker=wasm-ld'';
};
# Copy the WASM file to the output # Copy the WASM file to the output
installPhase = '' installPhase = ''
mkdir -p $out/lib mkdir -p $out/lib
cp target/wasm32-unknown-unknown/release/*.wasm $out/lib/ cp target/wasm32-unknown-unknown/release/*.wasm $out/lib/
runHook postInstall
''; '';
# Remove toolchain from closure (store paths are debug artifacts)
postInstall = ''
remove-references-to -t ${rust-toolchain} $out/lib/*.wasm
'';
};
in {
packages.toolchain = rust-toolchain;
packages.wordle-solver-wasm = toolchain-crosspkgs.callPackage drv {};
};
} }
+64
View File
@@ -0,0 +1,64 @@
let
drv = {
buildEnv,
dockerTools,
fakeNss,
nginx,
runCommand,
wordle-solver-web,
...
}:
dockerTools.buildImage {
name = "wordle-solver-web";
created = "now";
copyToRoot = buildEnv {
name = "image-root";
paths = [
nginx
(fakeNss.override {
extraGroupLines = [
"nogroup:x:65534:"
];
})
(runCommand "nginx-conf" {} ''
mkdir -p $out/etc
cat <<EOF > $out/etc/nginx.conf
events {}
error_log /dev/stderr;
http {
include ${nginx}/conf/mime.types;
server {
listen 80;
root ${wordle-solver-web};
access_log /dev/stdout;
}
}
daemon off;
EOF
'')
];
pathsToLink = ["/etc"];
};
runAsRoot = ''
mkdir -p /var/log/nginx
'';
config = {
Cmd = ["${nginx}/bin/nginx" "-c" "/etc/nginx.conf"];
};
};
in {
perSystem = {
pkgs,
self',
...
}: {
packages.wordle-solver-web-docker = pkgs.callPackage drv {
inherit (self'.packages) wordle-solver-web;
};
};
}
+14 -2
View File
@@ -1,4 +1,10 @@
{ {
perSystem = {
pkgs,
self',
...
}: let
drv = {
stdenv, stdenv,
fetchYarnDeps, fetchYarnDeps,
yarnConfigHook, yarnConfigHook,
@@ -14,7 +20,7 @@ stdenv.mkDerivation (finalAttrs: {
yarnOfflineCache = fetchYarnDeps { yarnOfflineCache = fetchYarnDeps {
yarnLock = finalAttrs.src + "/yarn.lock"; yarnLock = finalAttrs.src + "/yarn.lock";
hash = "sha256-ND+4leEGamH86IsM9JVtIqlwyrwVRQII75+rrawbiMI="; hash = "sha256-MTyHX2+/U7jRB1SJWbK9Td43zq+nclQwx7oDZbX0DqA=";
}; };
yarnBuildScript = "build-no-wasm"; yarnBuildScript = "build-no-wasm";
@@ -34,4 +40,10 @@ stdenv.mkDerivation (finalAttrs: {
# Needed for executing package.json scripts # Needed for executing package.json scripts
nodejs nodejs
]; ];
}) });
in {
packages.wordle-solver-web = pkgs.callPackage drv {
inherit (self'.packages) wordle-solver-wasm-pkg;
};
};
}
+27
View File
@@ -0,0 +1,27 @@
{inputs, ...}: {
perSystem = {pkgs, ...}: let
# Cargo.nix for IFD
generatedCargoNix = inputs.crate2nix.tools.${pkgs.hostPlatform.system}.generatedCargoNix {
name = "rustnix";
src = ../.;
};
utils = import ./utils.nix;
rust-toolchain = utils.rust-toolchain pkgs;
toolchain-pkgs = utils.toolchain-pkgs pkgs;
buildRustCrateForPkgs = _:
pkgs.buildRustCrate.override {
cargo = rust-toolchain;
rustc = rust-toolchain;
};
cargoNix = import generatedCargoNix {
pkgs = toolchain-pkgs;
inherit buildRustCrateForPkgs;
};
in {
packages.wordle-solver = cargoNix.rootCrate.build;
};
}
+11 -8
View File
@@ -1,5 +1,6 @@
use std::{env::args, path::Path}; use std::{env::args, path::Path};
use indicatif::ProgressBar;
use wordle_solver::{game::Game, solver::Solver, word_list::WordList}; use wordle_solver::{game::Game, solver::Solver, word_list::WordList};
pub fn do_simulation(valid_words: WordList, target_words: WordList, opener: Option<&str>) -> u64 { pub fn do_simulation(valid_words: WordList, target_words: WordList, opener: Option<&str>) -> u64 {
@@ -24,14 +25,16 @@ pub fn do_simulation(valid_words: WordList, target_words: WordList, opener: Opti
} }
loop { loop {
let best_word = solver let bar = ProgressBar::new(0);
.evaluate_all_words() let best_words = solver
.unwrap() .evaluate_all_words_async(move |i, n| {
.first() bar.set_position(i as u64);
.unwrap() bar.set_length(n as u64);
.word })
.clone(); .wait()
println!("{}", solver.best_word_format(5)); .unwrap();
let best_word = best_words.first().unwrap().word.clone();
println!("{}", Solver::best_word_format(best_words, 5));
match game.guess(&best_word) { match game.guess(&best_word) {
Ok(pat) => { Ok(pat) => {
+5 -3
View File
@@ -39,9 +39,11 @@ pub fn main() -> io::Result<()> {
io::stdin().read_line(&mut buf)?; io::stdin().read_line(&mut buf)?;
let pat_str = buf.trim_ascii().to_ascii_lowercase(); let pat_str = buf.trim_ascii().to_ascii_lowercase();
solver.apply_guess(Pattern::try_new(&guess, pat_from_str(&pat_str)).unwrap()); solver
solver.evaluate_all_words(); .apply_guess(Pattern::try_new(&guess, pat_from_str(&pat_str)).unwrap())
.unwrap();
let best_words = solver.evaluate_all_words().unwrap();
println!("\n{}\n", solver.tabular_format()); println!("\n{}\n", solver.tabular_format());
println!("{}", solver.best_word_format(10)); println!("{}", Solver::best_word_format(best_words, 10));
} }
} }
+1 -1
View File
@@ -8,7 +8,7 @@ pub enum CharStatus {
GREEN, GREEN,
} }
#[derive(Debug, PartialEq, Eq, Hash)] #[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub struct Pattern { pub struct Pattern {
chars: Vec<char>, chars: Vec<char>,
stats: Vec<CharStatus>, stats: Vec<CharStatus>,
+86 -9
View File
@@ -1,3 +1,8 @@
use std::sync::{
atomic::{AtomicUsize, Ordering},
mpsc, Arc,
};
use crate::pattern::Pattern; use crate::pattern::Pattern;
use indicatif::ParallelProgressIterator; use indicatif::ParallelProgressIterator;
use rayon::iter::{ParallelBridge, ParallelIterator}; use rayon::iter::{ParallelBridge, ParallelIterator};
@@ -7,16 +12,29 @@ use crate::word_list::WordList;
use derive_more::Display; use derive_more::Display;
use tabular::{Row, Table}; use tabular::{Row, Table};
#[derive(Clone)]
pub struct Guess { pub struct Guess {
pub pattern: Pattern, pub pattern: Pattern,
pub information_gained: f64, pub information_gained: f64,
} }
#[derive(Clone)]
pub struct Solver { pub struct Solver {
valid_words: WordList, valid_words: WordList,
possible_solutions: WordList, possible_solutions: WordList,
guesses: Vec<Guess>, guesses: Vec<Guess>,
best_words: Vec<WordStats>, }
pub struct SolverJob {
receiver: mpsc::Receiver<Result<Vec<WordStats>, String>>,
}
impl SolverJob {
pub fn wait(self) -> Result<Vec<WordStats>, String> {
self.receiver
.recv()
.map_err(|_| "calculation task cancelled".to_string())?
}
} }
#[derive(Display, Clone)] #[derive(Display, Clone)]
@@ -40,7 +58,6 @@ impl Solver {
valid_words: word_list.clone(), valid_words: word_list.clone(),
possible_solutions: word_list, possible_solutions: word_list,
guesses: vec![], guesses: vec![],
best_words: vec![],
} }
} }
@@ -49,7 +66,6 @@ impl Solver {
valid_words, valid_words,
possible_solutions, possible_solutions,
guesses: vec![], guesses: vec![],
best_words: vec![],
} }
} }
@@ -59,7 +75,7 @@ impl Solver {
- new_possible_solutions.equal_likelieness_entropy(); - new_possible_solutions.equal_likelieness_entropy();
if new_possible_solutions.len() == 0 { if new_possible_solutions.len() == 0 {
return Err("Solution space is empty".to_string()); return Err("Applying this guess would empty the solution space!".to_string());
} }
self.possible_solutions = new_possible_solutions; self.possible_solutions = new_possible_solutions;
@@ -99,8 +115,69 @@ impl Solver {
}) })
} }
pub fn evaluate_all_words_async<F>(&self, progress: F) -> SolverJob
where
F: Fn(usize, usize) + Send + Sync + 'static,
{
let solver_locked = (*self).clone();
let (tx, rc) = mpsc::channel();
let completed = Arc::new(AtomicUsize::new(0));
rayon::spawn_fifo(move || {
let mut result = solver_locked
.valid_words
.words()
.par_bridge()
.map(|w| {
let r = solver_locked.evaluate_word(w);
let completed = completed.fetch_add(1, Ordering::Relaxed) + 1;
progress(completed, solver_locked.valid_words.len());
r
})
.collect::<Result<Vec<WordStats>, String>>();
if let Ok(result) = &mut result {
result.sort_by(|a, b| {
a.expected_score_after_guess
.total_cmp(&b.expected_score_after_guess)
});
}
let _ = tx.send(result);
});
SolverJob { receiver: rc }
}
pub fn evaluate_all_words_cb<F>(&self, progress: F) -> Result<Vec<WordStats>, 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::<Result<Vec<WordStats>, 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<Vec<WordStats>, String> { pub fn evaluate_all_words(&mut self) -> Result<Vec<WordStats>, String> {
self.best_words = self let mut best_words: Vec<WordStats> = self
.valid_words .valid_words
.words() .words()
.par_bridge() .par_bridge()
@@ -108,15 +185,15 @@ impl Solver {
.map(|w| self.evaluate_word(w)) .map(|w| self.evaluate_word(w))
.collect::<Result<_, String>>()?; .collect::<Result<_, String>>()?;
self.best_words.sort_by(|a, b| { best_words.sort_by(|a, b| {
a.expected_score_after_guess a.expected_score_after_guess
.total_cmp(&b.expected_score_after_guess) .total_cmp(&b.expected_score_after_guess)
}); });
Ok(self.best_words.clone()) Ok(best_words)
} }
pub fn best_word_format(&self, n: usize) -> String { pub fn best_word_format(best_words: Vec<WordStats>, n: usize) -> String {
let mut table = Table::new("#{:<} {:>} | {:<} {:<} {:<}"); let mut table = Table::new("#{:<} {:>} | {:<} {:<} {:<}");
table.add_row( table.add_row(
@@ -127,7 +204,7 @@ impl Solver {
.with_cell("E[I]") .with_cell("E[I]")
.with_cell("E[score]"), .with_cell("E[score]"),
); );
for (i, ws) in self.best_words.iter().take(n).enumerate() { for (i, ws) in best_words.iter().take(n).enumerate() {
table.add_row( table.add_row(
Row::new() Row::new()
.with_cell(i + 1) .with_cell(i + 1)
+51 -4
View File
@@ -21,15 +21,46 @@ pub enum Color {
GREEN, 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)] #[allow(dead_code)]
#[wasm_bindgen] #[wasm_bindgen]
impl Solver { impl Solver {
pub fn new() -> Result<Solver, String> { pub fn new(use_wordle_answers: bool) -> Result<Solver, String> {
let valid_words = let valid_words =
WordList::try_from(io::BufReader::new(io::Cursor::new(DEFAULT_VALID_WORDS)))?; WordList::try_from(io::BufReader::new(io::Cursor::new(DEFAULT_VALID_WORDS)))?;
let answers = WordList::try_from(io::BufReader::new(io::Cursor::new(DEFAULT_ANSWERS)))?;
if use_wordle_answers {
let answers = WordList::try_from(io::BufReader::new(io::Cursor::new(DEFAULT_ANSWERS)))?;
Ok(Solver(solver::Solver::new(valid_words, answers))) Ok(Solver(solver::Solver::new(valid_words, answers)))
} else {
Ok(Solver(solver::Solver::new(
valid_words.clone(),
valid_words,
)))
}
} }
pub fn guess(&mut self, word: &str, colors: Vec<Color>) -> Result<(), String> { pub fn guess(&mut self, word: &str, colors: Vec<Color>) -> Result<(), String> {
@@ -50,6 +81,22 @@ impl Solver {
Ok(()) Ok(())
} }
pub fn evaluate_words(
&self,
n: u32,
progress: js_sys::Function,
) -> Result<Vec<WordStats>, 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<f64> { pub fn guess_infos(&self) -> Vec<f64> {
self.0 self.0
.guesses() .guesses()
@@ -58,8 +105,8 @@ impl Solver {
.collect() .collect()
} }
pub fn solution_space(&self) -> u64 { pub fn solution_space(&self) -> u32 {
self.0.stats().0 self.0.stats().0 as u32
} }
pub fn solution_space_uncertainty(&self) -> f64 { pub fn solution_space_uncertainty(&self) -> f64 {
+6 -1
View File
@@ -4,18 +4,20 @@
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"serve": "run-s wasm-pack:release vite:serve",
"build-no-wasm": "run-s tsc vite:build", "build-no-wasm": "run-s tsc vite:build",
"build": "run-s wasm-pack:release tsc vite:build", "build": "run-s wasm-pack:release tsc vite:build",
"dev": "run-s wasm-pack:dev vite:dev", "dev": "run-s wasm-pack:dev vite:dev",
"tsc": "tsc", "tsc": "tsc",
"vite:build": "vite build", "vite:build": "vite build",
"vite:dev": "vite -c vite.config.dev.ts", "vite:dev": "vite -c vite.config.dev.ts",
"vite:preview": "vite preview", "vite:serve": "vite -c vite.config.ts",
"wasm-pack:dev": "rm -rf pkg; wasm-pack build --dev --features wasm-bindgen --manifest-path ../Cargo.toml; mv ../pkg .", "wasm-pack:dev": "rm -rf pkg; wasm-pack build --dev --features wasm-bindgen --manifest-path ../Cargo.toml; mv ../pkg .",
"wasm-pack:release": "rm -rf pkg; wasm-pack build --features wasm-bindgen --manifest-path ../Cargo.toml; mv ../pkg ." "wasm-pack:release": "rm -rf pkg; wasm-pack build --features wasm-bindgen --manifest-path ../Cargo.toml; mv ../pkg ."
}, },
"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,7 +28,10 @@
"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",
"esbuild": "^0.28.2", "esbuild": "^0.28.2",
"katex": "^0.18.4",
"rollup": "^4.62.4", "rollup": "^4.62.4",
"svelte": "^5.56.9", "svelte": "^5.56.9",
"wordle-solver": "link:./pkg" "wordle-solver": "link:./pkg"
+282 -30
View File
@@ -1,64 +1,316 @@
<script lang="ts"> <script lang="ts">
import { Color, Solver } from "wordle-solver"; import { Color } from "wordle-solver";
import WordleLine from "./components/WordleLine.svelte" import WordleLine from "./components/WordleLine.svelte";
import {
solver_evaluate_words,
solver_guess,
solver_reset,
solver_solution_space,
} from "./solver/solver.ts";
import type {
GuessResult,
SolutionSpaceResult,
EvaluateWordsResult,
EvaluateWordsProgress,
} from "./solver/types.ts";
import info_md from "./assets/info.md?raw";
import SolverOutput from "./components/SolverOutput.svelte";
import { CircleX, FolderGit2, Info } from "@lucide/svelte";
import { tick, onMount } from "svelte";
import SvelteMarkdown from "@humanspeak/svelte-markdown";
let guesses = $state([{ let guesses = $state([
{
letters: ["", "", "", "", ""], letters: ["", "", "", "", ""],
colors: [Color.GREY, Color.GREY, Color.GREY, Color.GREY, Color.GREY] colors: [
}]); Color.GREY,
Color.GREY,
Color.GREY,
Color.GREY,
Color.GREY,
],
},
]);
let show_info = $state(false);
let infos_gained: (Promise<GuessResult> | null)[] = $state([null]);
let use_default_wordle_answers: boolean = $state(true);
let solution_space: Promise<SolutionSpaceResult> = $state(
solver_solution_space(),
);
let best_words: Promise<EvaluateWordsResult> | null = $state(null);
let best_words_progress: EvaluateWordsProgress | null = $state(null);
let solving: boolean = $state(false);
let w_lines: WordleLine[] = $state([]);
let cursor = $state(0); let cursor = $state(0);
let solver = Solver.new(); let is_desktop = $state(false);
let info_gained = $state(solver.guess_infos()); onMount(() => {
let solution_space = $state(solver.solution_space()); const update = () => {
let solution_space_uncertainty = $state(solver.solution_space_uncertainty()); is_desktop = window.innerWidth >= 768;
};
function handleKey(event: KeyboardEvent) { update();
if (event.key === "Enter") { window.addEventListener("resize", update);
cursor = 0;
return () => window.removeEventListener("resize", update);
});
async function toggleSolver() {
if (solving) {
await stopSolver();
} else {
best_words = solver_evaluate_words(5, (p) => {
best_words_progress = p;
});
}
}
async function reset() {
await solver_reset(use_default_wordle_answers);
guesses = [
{
letters: ["", "", "", "", ""],
colors: [
Color.GREY,
Color.GREY,
Color.GREY,
Color.GREY,
Color.GREY,
],
},
];
infos_gained = [null];
best_words = null;
solution_space = solver_solution_space();
}
async function stopSolver() {
await solver_reset(use_default_wordle_answers);
for (const g of guesses.slice(0, -1)) {
try {
await solver_guess(g.letters, g.colors);
} catch (_) {}
}
}
async function doGuess() {
const guess = guesses[guesses.length - 1]; const guess = guesses[guesses.length - 1];
solver.guess(guess.letters.join(""), guess.colors) if (guess.letters.join("").length != 5) {
solution_space = solver.solution_space(); return;
solution_space_uncertainty = solver.solution_space_uncertainty(); }
info_gained = solver.guess_infos();
infos_gained[infos_gained.length - 1] = solver_guess(
guess.letters,
guess.colors,
);
infos_gained.push(null);
solution_space = solver_solution_space();
cursor = 0;
guesses.push({ guesses.push({
letters: ["", "", "", "", ""], letters: ["", "", "", "", ""],
colors: [Color.GREY, Color.GREY, Color.GREY, Color.GREY, Color.GREY] colors: [
Color.GREY,
Color.GREY,
Color.GREY,
Color.GREY,
Color.GREY,
],
}); });
await tick();
w_lines[w_lines.length - 1].focus();
}
async function handleKey(event: KeyboardEvent) {
if (/^[a-zA-Z ]$/.test(event.key)) {
w_lines[guesses.length - 1].focus();
} else if (event.key === "Enter" && event.shiftKey) {
best_words = solver_evaluate_words(5, (p) => {
best_words_progress = p;
});
} else if (event.key === "Escape" && event.ctrlKey) {
await reset();
} else if (event.key === "Escape") {
await stopSolver();
} else if (event.key === "Enter") {
await doGuess();
} }
} }
</script> </script>
<svelte:window onkeydown={handleKey} /> <svelte:window onkeydown={handleKey} />
<div class="min-h-screen bg-zinc-950 text-zinc-100 flex items-center justify-center"> <div
<div class="text-center"> class="h-dvh w-full max-w-full
<h1 class="mb-6 text-3xl font-bold"> bg-zinc-950 text-zinc-100
Wordle Solver "
</h1> >
<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
class="fixed top-4 right-4"
href="https://git.jroeger.de/jonas/wordle-solver-rs"
target="_blank"
>
<FolderGit2 />
</a>
<a
class="fixed top-4 left-4"
onclick={() => {
show_info = true;
}}
>
<Info />
</a>
<p class="mt-4 text-sm text-zinc-500"> <div class="text-center">
Solution-space: {solution_space} · {solution_space_uncertainty.toFixed(2)}bits <h1 class="mb-4 text-3xl font-bold">Wordle Solver</h1>
<p class="text-sm text-zinc-500">
Use Wordle-Answers:
<input
type="checkbox"
bind:checked={use_default_wordle_answers}
onfocus={(e) => {
if (e.currentTarget) {
e.currentTarget.blur();
}
}}
onchange={reset}
/>
</p> </p>
<div class="mt-4 flex flex-col gap-2"> <p class="mt-0 text-sm text-zinc-500">
{#await solution_space}
loading
{:then s}
Solution-space: {s.size}, Uncertainty: {s.uncertainty.toFixed(
2,
)}bits
{:catch e}
{e}
{/await}
</p>
<div class="mt-2 flex flex-col gap-2">
{#each guesses as g, i (g)} {#each guesses as g, i (g)}
<WordleLine active={i === guesses.length-1} <WordleLine
active={i === guesses.length - 1}
on_guess_pressed={i === guesses.length - 1
? doGuess
: null}
bind:this={w_lines[i]}
bind:cursor bind:cursor
bind:letters={guesses[i].letters} bind:letters={guesses[i].letters}
bind:colors={guesses[i].colors} bind:colors={guesses[i].colors}
info_gained={i < info_gained.length ? info_gained[i] : null} info_gained={infos_gained[i]}
/> />
{/each} {/each}
</div> </div>
{#if is_desktop}
<p class="mt-4 text-sm text-zinc-500"> <p class="mt-4 text-sm text-zinc-500 mx-auto w-3/4">
Type letters · ← → navigate · Space cycle color · Backspace delete · Enter apply Type letters · ← → navigate · Space cycle color · Backspace
delete · Enter apply · Shift+Enter solve · Escape abort
solver · Ctrl+Esc reset
</p> </p>
{/if}
<button
tabindex="-1"
class="mt-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-full"
onclick={(e) => {
e.currentTarget.blur();
toggleSolver();
}}
>
{#if solving}
Cancel
{:else}
Solve
{/if}
</button>
</div>
{#if best_words}
<SolverOutput
data={best_words}
progress={best_words_progress}
insert_guess={(g) => {
guesses[guesses.length - 1].letters = g;
}}
bind:solving
/>
{/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> </div>
</div>
{/if}
</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.\].
+117
View File
@@ -0,0 +1,117 @@
<script lang="ts">
import katex from "katex";
import "katex/dist/katex.min.css";
import type {
EvaluateWordsProgress,
EvaluateWordsResult,
} from "../solver/types";
let {
data,
progress = null,
insert_guess = null,
solving = $bindable(true),
}: {
data: Promise<EvaluateWordsResult>;
progress: EvaluateWordsProgress | null;
insert_guess: ((word: string[]) => void) | null;
solving: boolean;
} = $props();
$effect(() => {
solving = true;
data.finally(() => {
solving = false;
});
});
function renderMath(txt: string | null) {
return (element: HTMLElement) => {
if (txt) {
katex.render(txt, element);
} else {
katex.render(element.innerText, element);
}
};
}
</script>
{#await data}
<div
class="flex items-center justify-center gap-2 text-gray-600 max-w-full"
>
Solving...
{#if progress}
<span class="text-sm">
{progress.i} / {progress.n}
</span>
{/if}
</div>
{:then d}
<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">
<thead class="divide-y divide-x bg-gray-800 text-xs text-gray-600">
<tr>
<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 math-header"
title="The probability that this word is the solution."
{@attach renderMath("p(x)")}
></th>
<th
title="The expected information to gain when guessing this word."
class="px-4 py-3 math-header"
{@attach renderMath("E[\\text{Inf.}]")}
></th>
<th
title="The expected number of guesses for this wordle when guessing this word."
class="px-4 py-3 math-header"
{@attach renderMath("E[\\text{Score}]")}
></th>
</tr>
</thead>
<tbody class="divide-y divide-x divide-gray-500">
{#each d.stats as s, i}
<tr
class="transition-colors hover:bg-gray-50"
onclick={() => {
if (insert_guess) {
insert_guess([...s.word]);
}
}}
>
<td class="px-4 py-3 font-bold">#{i + 1}</td>
<td class="px-4 py-3 font-bold">{s.word}</td>
<td class="px-4 py-3">
{(s.solution_probability * 100).toFixed(2) + "%"}
</td>
<td class="px-4 py-3">
{s.expected_information_gained.toFixed(2)} bits
</td>
<td class="px-4 py-3">
{s.expected_score_after_guess.toFixed(3)}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{:catch e}
<div class="rounded-lg border border-red-400 bg-gray-800 p-4 text-red-400">
{e}
</div>
{/await}
<style>
.math-header :global(.katex) {
font-size: 1em;
}
.math-header :global(.katex .mord) {
margin: 0;
}
</style>
+112 -25
View File
@@ -1,29 +1,58 @@
<script lang="ts"> <script lang="ts">
import { Color } from "wordle-solver"; import { Color } from "wordle-solver";
import type { GuessResult } from "../solver/types";
let { active=false, let {
active = false,
cursor = $bindable(0), cursor = $bindable(0),
letters = $bindable(["", "", "", "", ""]), letters = $bindable(["", "", "", "", ""]),
colors=$bindable([Color.GREY, Color.GREY, Color.GREY, Color.GREY, Color.GREY]), colors = $bindable([
info_gained=null } = $props(); Color.GREY,
Color.GREY,
Color.GREY,
Color.GREY,
Color.GREY,
]),
info_gained = null,
on_guess_pressed = null,
}: {
active: boolean;
cursor: number;
letters: string[];
colors: Color[];
info_gained: Promise<GuessResult> | null;
on_guess_pressed: (() => void) | null;
} = $props();
let invalid: boolean = $state(false);
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() {
inputs[cursor].focus();
}
function handleKey(event: KeyboardEvent) { function handleKey(event: KeyboardEvent) {
if (!active) { if (!active) {
return; return;
} }
if (/^[a-zA-Z]$/.test(event.key)) {
letters[cursor] = event.key.toUpperCase();
if (cursor < 4) {
cursor++;
} else {
cursor = 0;
}
}
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) {
@@ -32,11 +61,15 @@
} }
if (event.key === "ArrowLeft") { if (event.key === "ArrowLeft") {
cursor = Math.max(0, cursor - 1); const nc = Math.max(0, cursor - 1);
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[nc].focus();
cursor = nc;
} }
if (event.key === " ") { if (event.key === " ") {
@@ -48,24 +81,78 @@
<svelte:window onkeydown={handleKey} /> <svelte:window onkeydown={handleKey} />
<div class="flex justify-center gap-2"> <div class="flex justify-center gap-2">
{#each letters as letter, i} {#each letters as _, i}
<div <input
disabled={!active}
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 transition-all text-center caret-transparent
focus:outline-none
" "
bind:this={inputs[i]}
bind:value={letters[i]}
class:border-blue-500={i === cursor && active} class:border-blue-500={i === cursor && active}
class:border-zinc-700={i !== cursor || !active} class:border-zinc-700={i !== cursor || !active}
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}
{letter} onblur={() => {
</div> cursor = -1;
}}
onbeforeinput={(event) => {
if (event.inputType === "insertText") {
event.preventDefault();
let key = event.data ?? "";
if (/^[a-zA-Z]$/.test(key)) {
letters[i] = key.toUpperCase();
let nc = cursor;
if (nc < 4) {
nc++;
} else {
nc = 0;
}
inputs[nc].focus();
cursor = nc;
}
}
}}
onclick={() => {
if (!active) {
return;
}
if (cursor === i) {
colors[cursor] = (colors[cursor] + 1) % 3;
} else {
cursor = i;
}
}}
/>
{/each} {/each}
<span class="w-12 text-sm text-zinc-500 self-center">{ <span
(info_gained !== null) ? info_gained.toFixed(2) + "bits" : '↵' class="max-w-14 w-1/6 text-sm text-zinc-500 self-center flex items-center justify-center"
}</span> onclick={info_gained === null ? on_guess_pressed : null}
>
{#if info_gained_wrapped === null}
{:else}
{#await info_gained_wrapped}
<div
class="size-3 animate-spin rounded-full border-2 border-gray-300 border-t-gray-700"
></div>
{:then i}
{i.info_gained.toFixed(2)} bits
{:catch e}
<span
title={e}
class="lex size-4 items-center justify-center rounded-full border border-red-500 text-[10px] font-bold text-red-500"
>!</span
>
{/await}
{/if}
</span>
</div> </div>
+145
View File
@@ -0,0 +1,145 @@
import type { Color } from "wordle-solver";
import SolverWorker from "./worker?worker";
import type { GuessResult, SolutionSpaceResult, EvaluateWordsResult, SolverRequest, SolverResponse, EvaluateWordsProgress } from "./types";
let solver_worker = new SolverWorker();
await init_solver();
let msg_id = 0;
const pending = new Map<number, {
resolve: (value: any) => void;
reject: (error: any) => void;
}>();
const progresses = new Map<number, (p: EvaluateWordsProgress) => void>();
function init_solver(): Promise<void> {
return new Promise((resolve) => {
solver_worker.onmessage = (event: any) => {
if (event.data.type === "ready") {
solver_worker.onmessage = workerHandleMsg;
resolve();
} else {
workerHandleMsg(event);
}
};
});
}
function create_solver_worker(): Promise<void> {
solver_worker = new SolverWorker();
return init_solver();
}
export function solver_guess(word: string[], colors: Color[]): Promise<GuessResult> {
const id = msg_id++;
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject });
const r: SolverRequest = {
type: "guess",
id: id,
input: { word: word, colors: colors },
};
solver_worker.postMessage(JSON.parse(JSON.stringify(r)));
});
}
export function solver_solution_space(): Promise<SolutionSpaceResult> {
const id = msg_id++;
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject });
const r: SolverRequest = {
type: "solution_space",
id: id,
input: null,
};
solver_worker.postMessage(JSON.parse(JSON.stringify(r)));
});
}
export function solver_evaluate_words(n: number, progress: ((p: EvaluateWordsProgress) => void) | null): Promise<EvaluateWordsResult> {
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)));
});
}
export async function solver_reset(wordle_answers: boolean): Promise<void> {
solver_worker.terminate();
await create_solver_worker();
pending.forEach((v, _) => {
v.reject("Solver was reset");
});
pending.clear();
progresses.clear();
const id = msg_id++;
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject });
const r: SolverRequest = {
type: "reload_wordlist",
id: id,
input: { use_wordle_ansers: wordle_answers },
};
solver_worker.postMessage(JSON.parse(JSON.stringify(r)));
});
}
function workerHandleMsg(event: MessageEvent<SolverResponse>) {
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) {
return;
}
pending.delete(res.id);
progresses.delete(res.id);
if (res.error) {
pendingRequest.reject(res.error);
} else if (res.result) {
pendingRequest.resolve(res.result);
} else {
pendingRequest.reject("No result");
}
}
+86
View File
@@ -0,0 +1,86 @@
import { Color } from "wordle-solver";
export type SolverRequest =
| {
type: "guess",
id: number,
input: Guess
}
| {
type: "evaluate_words",
id: number,
input: EvaluateWords
}
| {
type: "solution_space",
id: number,
input: null,
}
| {
type: "reload_wordlist",
id: number,
input: ReloadWordlist
};
export type SolverResponse =
| {
type: "guess",
id: number,
error: string | null,
result: GuessResult | null
}
| {
type: "evaluate_words",
id: number,
error: string | null,
result: EvaluateWordsResult | null
}
| {
type: "evaluate_words_progress",
id: number,
error: null,
result: EvaluateWordsProgress
}
| {
type: "solution_space",
id: number,
error: string | null,
result: SolutionSpaceResult | null
}
| {
type: "reload_wordlist",
id: number,
error: string | null
result: boolean | null,
};
export type Guess = {
word: string[],
colors: Color[]
};
export type GuessResult = {
info_gained: number,
};
export type EvaluateWords = {
n: number,
};
export type EvaluateWordsProgress = {
i: number,
n: number
};
export type ReloadWordlist = {
use_wordle_ansers: boolean
};
export type WordStats = {
word: string,
solution_probability: number,
expected_information_gained: number,
expected_score_after_guess: number
};
export type EvaluateWordsResult = {
stats: WordStats[]
};
export type SolutionSpaceResult = {
size: number,
uncertainty: number
};
+105
View File
@@ -0,0 +1,105 @@
import { Solver, } from "wordle-solver";
import type { SolverRequest, SolverResponse, WordStats } from "./types";
let solver = Solver.new(true);
self.postMessage({
type: "ready",
});
self.onmessage = (event: MessageEvent<SolverRequest>) => {
const request = event.data;
switch (request.type) {
case "guess": {
try {
solver.guess(request.input.word.join(""), request.input.colors);
const infos = solver.guess_infos();
const response: SolverResponse = {
type: "guess",
id: request.id,
error: null,
result: { info_gained: infos[infos.length - 1] }
};
self.postMessage(JSON.parse(JSON.stringify(response)));
} catch (e) {
const response: SolverResponse = {
type: "guess",
id: request.id,
error: e as string,
result: null,
};
self.postMessage(JSON.parse(JSON.stringify(response)));
}
break;
};
case "evaluate_words": {
try {
let stats = solver.evaluate_words(request.input.n, (i: number, n: number) => {
let p: SolverResponse = {
type: "evaluate_words_progress",
id: request.id,
error: null,
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 as string,
result: null,
};
self.postMessage(JSON.parse(JSON.stringify(response)));
}
break;
};
case "solution_space": {
const response: SolverResponse = {
type: "solution_space",
id: request.id,
error: null,
result: { size: solver.solution_space(), uncertainty: solver.solution_space_uncertainty() }
};
self.postMessage(JSON.parse(JSON.stringify(response)));
break;
};
case "reload_wordlist": {
solver.free();
solver = Solver.new(request.input.use_wordle_ansers);
const response: SolverResponse = {
type: "reload_wordlist",
id: request.id,
error: null,
result: true
};
self.postMessage(JSON.parse(JSON.stringify(response)));
break;
};
}
};
+6
View File
@@ -5,4 +5,10 @@ import wasm from "vite-plugin-wasm";
export default defineConfig({ export default defineConfig({
plugins: [wasm(), svelte(), tailwindcss()], plugins: [wasm(), svelte(), tailwindcss()],
build: {
target: "esnext"
},
worker: {
format: "es",
},
}); });
+112
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"
@@ -188,6 +203,11 @@
"@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14" "@jridgewell/sourcemap-codec" "^1.4.14"
"@lucide/svelte@^1.33.0":
version "1.33.0"
resolved "https://registry.yarnpkg.com/@lucide/svelte/-/svelte-1.33.0.tgz#c7344737270e7e7bf85756daac3c9b739c2c38f3"
integrity sha512-b+osTYG2V4dge5Lr7tnaTaahhy/4vH7Xb8zcqVjmctjwgkpXS0NNOJPkGzHHZoxtw/OO+2YAU28omeMfYCaawg==
"@napi-rs/lzma-linux-x64-gnu@1.5.1": "@napi-rs/lzma-linux-x64-gnu@1.5.1":
version "1.5.1" version "1.5.1"
resolved "https://registry.yarnpkg.com/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz#e57d4306966078662038094fb38eb9146dc3aea9" resolved "https://registry.yarnpkg.com/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz#e57d4306966078662038094fb38eb9146dc3aea9"
@@ -621,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"
@@ -767,6 +794,11 @@ color-name@1.1.3:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
commander@^8.3.0:
version "8.3.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
concat-map@0.0.1: concat-map@0.0.1:
version "0.0.1" version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@@ -783,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"
@@ -843,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"
@@ -860,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"
@@ -1105,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"
@@ -1171,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"
@@ -1385,6 +1472,13 @@ json-parse-better-errors@^1.0.1:
resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
katex@^0.18.4:
version "0.18.4"
resolved "https://registry.yarnpkg.com/katex/-/katex-0.18.4.tgz#aa09d0bfbcbab71a9a0f9d420def6c0d0a227a94"
integrity sha512-IMPntbRLOU+eu88XDiFKqQ8Akhr9Tv7jDMXqPhjG9SI1JMA4DIgXk4x9k4skJz2NZJXBRbC+2pYBLj9olqcZow==
dependencies:
commander "^8.3.0"
lightningcss-android-arm64@1.32.0: lightningcss-android-arm64@1.32.0:
version "1.32.0" version "1.32.0"
resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz#f033885116dfefd9c6f54787523e3514b61e1968" resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz#f033885116dfefd9c6f54787523e3514b61e1968"
@@ -1562,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"
@@ -1701,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"
@@ -2138,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"