Wordle, but with ZK proofs!

Overview

Zordle: ZK Wordle

Zordle is Wordle, but with zero-knowledge proofs. Zordle uses ZK proofs to prove that a player knows words that map to their shared grid, but does not reveal those words to a verifier. Zordle is probably the first end-to-end web app built using Halo 2 ZK proofs!

This project was made as part of 0xPARC Halo 2 Learning Group. Big shoutout to Ying Tong for basically hand-holding me through Halo2 circuit writing and to Uma and Blaine for significant work on porting the Halo 2 library to WASM.

✨Demo: Live at zordle.xyz ✨

zordle-demo.mov

Motivation and user flow

Earlier this year, Wordle became one of the most popular word games, with millions logging on every day to attempt the day's Wordle and share their successes with friends and social media. Wordle's popularity was primarily driven by a really simple to share grid:

At some point, my only form of communication with some of my friends was Wordle grid exchanges

However, the ease of sharing these emoji boxes came with an unfortunate flaw: A player could just edit their grid after the game and make themselves seem much smarter than they originally were. I was always suspicious if my friends really got the scores they claimed or not. ZK SNARKs to the rescue! 🤓

In Zordle, after solving the day's Wordle, a user additionally generates a ZK proof attesting that they know the set of words that perfectly correspond to a set of emoji boxes that they're sharing!1

Learning about the shiny new features of Halo 2, Wordle seemed like a cool toy application to get my hands dirty with the library, so I chose to work on this as my learning group project! The rest of this README will be a technical note on the circuits and an informal introduction to the Halo 2 Library and features of PLONKish proving systems.

Circuit

The over-simplified mental model of Halo 2 I've come to appreciate is that of a giant spreadsheet: You have cells in a tabular format you can fill values in, mutate the values from cell to cell, and check that relationships and constraints you'd desire hold. Additionally, you have access to some "global" structures that are more powerful than just plain cell relationship comparators: you can check if row A is a permutation of row B for a very cheap cost, and its also very cheap to check set membership of the value of a particular cell in a giant list (as long as you can define said giant list at "compile time").

Muse MuseBoard 2022-07-13 11 37 57

To be slightly more precise, Halo 2 essentially structures circuits as row-column operations. There are 4 primary types of columns:

  • Instance columns: these are best associated with public inputs,
  • Advice columns: these are best associated with private inputs and the computation trace of the circuit, the "witness",
  • Fixed columns: constants used in the computation, known at "compile time" and,
  • Selector columns: these are binary values used to "select" particular advice and instance cells and define constraints between them.

Additionally, there's the notion of a lookup column that allows you to check set membership efficiently but that's perhaps best thought of as a giant fixed set instead of a circuit table column.

Of course, the natural question, given this abstraction, is to figure out what's the right way to write efficient ZK circuits inside this playground? Should I use more rows? Or more columns? The answer is quite complicated.

For simpler schemes like Groth16 that are based on R1CS, circuit engineers have universally accepted the metric of "the number of constraints" since most tasks associated with the ZK proof (compilation, proving time, trusted setup compute etc.) scale linearly with the number of non-linear constraints. The structure of PLONK circuits, on the other hand, allows for much more flexibility in defining a circuit, and with it comes a very tough-to-grasp cost model. There are some rough heuristics. For instance, more rows make proving time slower (notably, however, the big jump in proving times occurs when the number of rows crosses powers of 2, where the time cost of the intermediate polynomial FFTs required doubles) while more columns make verification time slower. Notably, also, Halo 2, the library, is very flexible and allows for the instantiation of circuits using different polynomial commitment schemes (such as IPA and KZG) which makes cost-modelling instantiation dependent as well.

The abstraction of a spreadsheet for PLONKish arithmetisation is quite powerful because it allows the library to lay out and pack the rows and columns of your circuit tighter automatically (and paves the way for an IR/automated circuit optimiser long term). While great for optimizations, unfortunately, this ability to auto-pack comes at the expense of making the API more nuanced and makes the cost modelling of circuits even more non-trivial to a circuit programmer.

To elaborate on the nuance of the API, Halo 2 defines the concept of a "region" inside the spreadsheet. A region is the minimal set of cells such that all constraints relating to any one of them are contained entirely within this region. This is a mouthful, but in essence, regions are the minimal building blocks of a circuit. Typically, even non-ZK apps are written in disparate modules - a good analogy for this is perhaps the Clock app on your mobile phone: the app is structured coherently to a user (around "time") but if you think about it like a programmer, the timer tab has very little in common with the world clock tab. The same is true for ZK circuits, a Clock circuit might want to check both the world clock and the status of a timer, and the representation of each of these is its own "region" in the Clock circuit, independent of each other. In the Halo 2 setup, a programmer will write both of them almost independently, and let the compiler figure out the best way to "pack" them into the spreadsheet.

image0 The concept of "time" is coherent to a user, but the world clock and the timer are disparate modules to a programmer

While cost-modelling seems to be quite problematic for circuit writers on the surface with the Halo 2 library set up right now, the general read of participants in the Halo 2 Learning Group seems to be that these APIs give circuit writers enough breathing room to hyper-optimise computation for commonly used primitive circuits, and in future, these efficient primitives can be composed (perhaps inefficiently) into real apps by higher-level circuit writers. Hopefully, eventually, ZK circuits will get to a point where a few low-level programmers will hyper-optimise a minimal instruction set, and the rest of us will just roll our circuits into compilers composing and optimising circuits on those instruction sets.2

It's certainly fun to theorise about the future of ZK circuit writing, but right now, we have a very real task at hand: making an anti-cheat that doesn't really work for a meaningless word game that's not even popular anymore 🤡. And the only way to write these circuits is to delve into the weeds and think about this spreadsheet and its regions and whatnot as a "low-level" programmer.

First, let's quickly formalise our public/private inputs:

Public inputs

  • The solution word
  • The grid of boxes of 6 words x 5 slots (one for each letter): each cell in the grid is either green, yellow or grey

Private inputs

  • 6 words of 5 letters each

For starters, observe that Wordle's structure is such that every guess is quite independent of the others - if a guess is valid on its own, its always valid inside a game and vice-versa. This signals that one clean structure for the circuit is to make an individual region for each guess.

With this one region per guess construction, let's think about what checks are necessary for each guess:

  • The guess must be an English word of 5 letters
  • If the grid box at a spot is green, the letter at the corresponding spot of the guess must match the solution's letter
  • If a grid box is not green, the letter in the guess at the corresponding spot must not match the solution's letter
  • Similar checks follow if the grid box is yellow (and if it is not yellow)

English word

Typically, in an R1CS circuit, you would make the check for a guess being a dictionary word a Merkle proof: You would make a Merkle tree of all the words in the dictionary and witness the Merkle path of your guess in the tree3. In PLONK/Halo 2 however, you have the added unlock of lookup tables! While it's not particularly efficient to use lookup tables this way (since your circuit will now have 12000+ rows), it is a cool way to make use of the feature, and I wanted to get more familiar with the API so I decided to try this out.

Green

Precisely, this check is: for each slot of the grid, if the slot is green, compare the letters at that slot in the guess and the solution. They should be equal.

Not green

This check is: for each slot of the grid, if the slot is not green, compare the letters at that slot in the guess and the solution. They should not be equal. In other words, the difference of the letters at that slot should be non-zero. We'll use this reformulation later.

Yellow and not yellow

The check for yellow color works almost the same way: Instead of comparing the letters at the exact slot, the comparison is just replaced by a giant OR on all possible pairings of the guess letter with letters of the solution.

Let's ignore the yellow color boxes for now and just try to lay the intermediate variables out in one region of the spreadsheet, considering only the green boxes:

image

Consider the witness trace of this circuit: We start with the guess (the first row) and the final solution (the second row) and go through a few intermediate computations to obtain the expected value of green boxes. diff_green, the third row, is the difference between the letters at the corresponding slots of the solution and the guess (for instance, slot 2, "U" - "L" = 9). Next, to do the two aforementioned green checks, we need to additionally know "is diff_green zero?". This'll live in the next row, and finally, we'll have a copy of the output green grid boxes from the instance in the last row to compare with our expected value. Finally, we'll add another column to our spreadsheet that'll just contain the hash of the letters of the guess. This is so we can check the lookup table for the guesses' validation with a single lookup.

Now that we have a high-level intuition for what our circuit should "do", let's figure out how to actually code this and constrain the circuit with Halo 2.

API

Fundamentally, the Halo 2 API is a way to write functions (or formulas if you will) on the abstraction of the underlying spreadsheet data structure. Since we can't populate each cell and hand wire each constraint by hand, we need a programmatic layer to do this for us.

The Halo 2 library splits this circuit programming into a 2 pass structure: in the first pass, you decide on and assign all the constraints and logical gates that each region/row/cell must abide by. The second pass is assignment/witness generation: you populate values into this spreadsheet and "instantiate" it.4

While I've already mentioned some details of the idea of regions before, a lot of the Halo 2 library is wrapping these regions into tight APIs that reduce programmer overhead. Besides regions that act as locality constructions in the spreadsheet, another accompanying concept introduced by the Halo 2 API is that of rotation. Imagine that you are processing the spreadsheet row by row, top to bottom. The rotation is just a way to express a row relative to the current row. So the current row is the current "rotation" in this sequential process, the row just above is the "-1" (or "previous") rotation and so forth.

Now, let's pick off our circuit design in the previous section and use the Halo 2 library to codify it:

First, let's make a list of constraints/checks we'll want to add to the spreadsheet:

  • The first row (containing the guess) should be an English word (this'll take a lookup check) and we should additionally constrain that the hash of the guess matches the letters in the other columns.
  • The second row has no checks
  • The third row should check the difference of the corresponding spot on the first two rows matches the difference expressed in this row.
  • The fourth row should check that the spots take value 1 only if the row above is zero. This is a rather complicated check - the Halo 2 API allows for a weak abstraction known as a "chip" that allows you to compose smaller sub checks a bit more easily. A chip is really just a fancy word for a "sub" circuit setting up its own gates and witness assignment and being "callable" from a larger circuit.
  • Finally, the final row simply needs to check if this spot is 1, the third row must be 0 or if it is 0, the fourth row must be 1.

Now, instantiation and filling in the witness is simply a matter of inputting values according to the mentioned rules.

Ultimately, we have a clean 5 row, 7 column region that asserts everything necessary for one guess. We just repeat 6 of these for each of the possible user guesses to obtain our entire circuit!

Some other miscellaneous notes/thoughts about the Halo 2 API I couldn't fit elsewhere:

  • One quirk of the Halo 2 API is that while advice columns are referred to by offset rotations, the instance columns are referred to by absolute row numbers, which adds to some confusion. But this is mostly a function of these instance columns being entirely independent of the regions abstraction.
  • Note that the spreadsheet model of layouting is a very intentional choice of the Halo 2 Library. There are many other ways to model ZK circuits while still using them with PLONKish arithmetisation. For instance, Yi Sun/Jonathan Wang from the learning group used only a single column to write their circuit (the halo2wrong repo does something similar) primarily to reduce verification cost and simplify cost-modelling. On the other hand, Circom developers are planning to stick to the R1CS-like circuit layout structure but just add the ability to define custom gates using PLONK. Ultimately, I personally think the generalised many-row many-column spreadsheet-like structure is the most flexible representation amongst these, but there are definitely tradeoffs in ease-of-use vs powerfulness to be explored.
  • I love the detail and care put into debug info for the Halo 2 library. Coming from circom-land (where debugging detail is quite lacking to say the least), Halo 2's debugging hand-holding was a breath of fresh air. :)) And I love the little parrot! 🦜

image

WASM Port

The Halo 2 documentation now has a WASM Guide based on Zordle.

Halo 2 is written in Rust and is currently only used by Zcash in their daemon software that runs on metal. As application developers, however, we wanted our circuits to prove and verify in web apps. Pulling together a WASM port of Halo 2 proving and verification was quite non-trivial. Original, my project was a CLI-based Wordle but based on Uma's work on running Halo 2 prover and verifier in-browser, we ported the JS prototype to a React/TS friendly library and further discovered some speedup and memory utilisation tricks to make the Wordle circuit work in browser in a reasonable time frame. These tricks include Blaine's discovery of esoteric flags required to bump up available memory for a Rust ported WebAssembly worker from a random GitHub issue and precomputing params and serving them as static files to the Rust WASM. All of these tricks are rolled into a small test-client that might be helpful to future Halo WASM porters. :)

Feel free to hit me up if you have thoughts on any of the notes in this README, many of these are half-baked thoughts and ideas I'd like to flesh out :))

Thanks to 0xPARC for hosting the learning group and to the 0xPARC community for discussions, reading drafts of this README and everything in between.

Footnotes

  1. Ignore the minor technical detail that they can always just cheat by looking up the day's word elsewhere. 😅

  2. Supposedly, this will also mark the switching point where we can stop bothering with hyper-optimised zkEVMs and instead just write a zkMIPS machine for all VMs. Some notes on this tradeoff here.

  3. Alternately, you can tightly pack polynomial hashes of words in field elements 🥲

  4. Sidenote that soundness/under-constraining bugs in this Halo 2 model essentially lie at the margin of the difference of these two passes. This is a useful fact to keep in mind as a circuit writer.

You might also like...
 The Bloat-Free Browser Game in Rust but in C and in UEFI
The Bloat-Free Browser Game in Rust but in C and in UEFI

rust-browser-game but in UEFI instead of browser quick start deps rust gnu-efi gcc make build process $ make running after building everything you wil

rust-browser-game but native and rendered with ncurses in C without the Browser
rust-browser-game but native and rendered with ncurses in C without the Browser

Spin-off of rust-browser-game-but-sdl but with ncurses Nothing much to say. Just see rust-browser-game-but-sdl and rust-browser-game. Quick Start $ ma

The Bloat-Free Browser Game in Rust but in C and not in a Browser
The Bloat-Free Browser Game in Rust but in C and not in a Browser

rust-browser-game but native and rendered with SDL in C without the Browser The original idea of rust-browser-game is that the game.wasm module is com

A tetris game I wrote in rust using ncurses. I'm sure that there's a better way to write a tetris game, and the code may be sus, but it techinically works
A tetris game I wrote in rust using ncurses. I'm sure that there's a better way to write a tetris game, and the code may be sus, but it techinically works

rustetris A tetris game I wrote in rust using ncurses. I'm sure that there's a better way to write a tetris game, and the code may be sus, but it tech

Like minecraft, but crispier!

crispycraft Like minecraft, but crispier! Links Library documentation WebGPU: https://docs.rs/wgpu/0.12.0/wgpu/ building_blocks: https://docs.rs/build

API tool,but egui style and rusty
API tool,but egui style and rusty

WEAVER About Weaver is a simple,easy-to-use and cross-platform API tool.Inspired by hoppscotch . It uses the Rust egui GUI library. Features Get,Post

🐵 Monkeytype, but for desktop

Monkeytype Desktop Monkeytype desktop is a desktop client for monkeytype.com with various quality-of-life features such as a Discord Rich Presence. Yo

A Rust library and cli for Wordle. Inspired by Wordle in Bash
A Rust library and cli for Wordle. Inspired by Wordle in Bash

A Rust library and cli for Wordle. Inspired by Wordle in Bash

A wordle implementation that I made for a competition of wordle solvers

Wordle tester A wordle implementation that I made for a competition of wordle solvers. Runs tests using a list of words and outputs the total turns ta

IDP2P is a peer-to-peer identity protocol which enables a controller to create, manage and share its own proofs as well as did documents
IDP2P is a peer-to-peer identity protocol which enables a controller to create, manage and share its own proofs as well as did documents

IDP2P Experimental, inspired by ipfs, did:peer and keri Background See also (related topics): Decentralized Identifiers (DIDs) Verifiable Credentials

This crate is an implementation of Sonic, a protocol for quickly verifiable, compact zero-knowledge proofs of arbitrary computations

Sonic This crate is an implementation of Sonic, a protocol for quickly verifiable, compact zero-knowledge proofs of arbitrary computations. Sonic is i

ZKP fork for rust-secp256k1, adds wrappers for range proofs, pedersen commitments, etc

rust-secp256k1 rust-secp256k1 is a wrapper around libsecp256k1, a C library by Peter Wuille for producing ECDSA signatures using the SECG curve secp25

Bulletproofs and Bulletproofs+ Rust implementation for Aggregated Range Proofs over multiple elliptic curves

Bulletproofs This library implements Bulletproofs+ and Bulletproofs aggregated range proofs with multi-exponent verification. The library supports mul

Noir is a domain specific language for zero knowledge proofs

The Noir Programming Language Noir is a Domain Specific Language for SNARK proving systems. It has been designed to use any ACIR compatible proving sy

P2P Network to verify authorship & ownership, store & deliver proofs.

Anagolay Network Node Anagolay is a next-generation framework for ownerships, copyrights and digital licenses. 🚀 Local Development The installation a

A library for constructing Groth-Sahai proofs using pre-built wrappers

Groth-Sahai Wrappers A Rust library containing wrappers that facilitate the construction of non-interactive witness-indistinguishable and zero-knowled

The Light Protocol program verifies zkSNARK proofs to enable anonymous transactions on Solana.

Light Protocol DISCLAIMER: THIS SOFTWARE IS NOT AUDITED. Do not use in production! Tests cd ./program && cargo test-bpf deposit_should_succeed cd ./pr

Python/Rust implementations and notes from Proofs Arguments and Zero Knowledge study group

What is this? This is where I'll be collecting resources related to the Study Group on Dr. Justin Thaler's Proofs Arguments And Zero Knowledge Book. T

Thaler's Proofs, Args, and ZK Implemented in Rust using arkworks
Thaler's Proofs, Args, and ZK Implemented in Rust using arkworks

rthaler • Dr. Thaler's book Proofs, Args, and ZK implemented in rust using the arkworks cryptographic rust toolset. Various Zero Knowledge Protocols a

Comments
  • Uncaught (in promise) RuntimeError: unreachable

    Uncaught (in promise) RuntimeError: unreachable

    Great work!

    Although it seems that something wrong when run test.

    Env:

    • Chrome: 103.0.5060.114(x86_64)
    • Mac 12.4(Intel)
    • node v18.6.0

    Here are logs:

    react-dom.development.js:29840 Download the React DevTools for a better development experience: https://reactjs.org/link/react-devtools
    DevTools failed to load source map: Could not load content for chrome-extension://bjiiiblnpkonoiegdlifcciokocjbhkd/sourcemaps/portal-contentscript.js.map: System error: net::ERR_BLOCKED_BY_CLIENT
    DevTools failed to load source map: Could not load content for chrome-extension://bjiiiblnpkonoiegdlifcciokocjbhkd/sourcemaps/portal-inpage.js.map: System error: net::ERR_BLOCKED_BY_CLIENT
    halo-worker.ts:4 diffing
    App.tsx:17 in between (6) [Array(2), Array(2), Array(2), Array(2), Array(2), Array(2)]
    App.tsx:18 btw 125.23499999940395
    halo-worker.ts:22 param length 1711
    halo-worker.ts:23 params Uint8Array(1711) [60, 33, 68, 79, 67, 84, 89, 80, 69, 32, 104, 116, 109, 108, 62, 10, 60, 104, 116, 109, 108, 32, 108, 97, 110, 103, 61, 34, 101, 110, 34, 62, 10, 32, 32, 60, 104, 101, 97, 100, 62, 10, 32, 32, 32, 32, 60, 109, 101, 116, 97, 32, 99, 104, 97, 114, 115, 101, 116, 61, 34, 117, 116, 102, 45, 56, 34, 32, 47, 62, 10, 32, 32, 32, 32, 60, 108, 105, 110, 107, 32, 114, 101, 108, 61, 34, 105, 99, 111, 110, 34, 32, 104, 114, 101, 102, 61, 34, 47, 102, …]
    halo-worker.ts:25 genning proof
    halo-worker.ts:31 here we go
    wordle_bg.a4fc301cf35a6c6e6137.wasm:0xf8cae Uncaught (in promise) RuntimeError: unreachable
        at wordle_bg.a4fc301cf35a6c6e6137.wasm:0xf8cae
        at wordle_bg.a4fc301cf35a6c6e6137.wasm:0xb804c
        at wordle_bg.a4fc301cf35a6c6e6137.wasm:0xf4ad6
        at wordle_bg.a4fc301cf35a6c6e6137.wasm:0xda951
        at wordle_bg.a4fc301cf35a6c6e6137.wasm:0x40fba
        at wordle_bg.a4fc301cf35a6c6e6137.wasm:0xb522f
        at wordle_bg.a4fc301cf35a6c6e6137.wasm:0xccc2b
        at wordle_bg.a4fc301cf35a6c6e6137.wasm:0xf9438
        at __wbg_adapter_30 (wordle.js:138:1)
        at real (wordle.js:123:1)
    $func783 @ wordle_bg.a4fc301cf35a6c6e6137.wasm:0xf8cae
    $func268 @ wordle_bg.a4fc301cf35a6c6e6137.wasm:0xb804c
    $func735 @ wordle_bg.a4fc301cf35a6c6e6137.wasm:0xf4ad6
    $func589 @ wordle_bg.a4fc301cf35a6c6e6137.wasm:0xda951
    $func57 @ wordle_bg.a4fc301cf35a6c6e6137.wasm:0x40fba
    $func251 @ wordle_bg.a4fc301cf35a6c6e6137.wasm:0xb522f
    $func416 @ wordle_bg.a4fc301cf35a6c6e6137.wasm:0xccc2b
    $_dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__haef9329f49c97952 @ wordle_bg.a4fc301cf35a6c6e6137.wasm:0xf9438
    __wbg_adapter_30 @ wordle.js:138
    real @ wordle.js:123
    Promise.then (async)
    imports.wbg.__wbg_then_1c698eedca15eed6 @ wordle.js:470
    $func862 @ wordle_bg.a4fc301cf35a6c6e6137.wasm:0xf9ca8
    $func374 @ wordle_bg.a4fc301cf35a6c6e6137.wasm:0xc80c7
    $wasm_bindgen__convert__closures__invoke2_mut__hd4573d3b0ab790e5 @ wordle_bg.a4fc301cf35a6c6e6137.wasm:0xf93bb
    __wbg_adapter_81 @ wordle.js:198
    cb0 @ wordle.js:454
    imports.wbg.__wbg_new_78403b138428b684 @ wordle.js:459
    $func913 @ wordle_bg.a4fc301cf35a6c6e6137.wasm:0xfa514
    $prove_play @ wordle_bg.a4fc301cf35a6c6e6137.wasm:0xdb5c2
    prove_play @ wordle.js:182
    prove_play @ halo-worker.ts:32
    await in prove_play (async)
    callback @ comlink.ts:312
    
    
    opened by spartucus 3
This is a tool for solving the excellent Wordle puzzle

Wordle Tool This is a tool for solving the excellent Wordle puzzle. It mainly exists as an exercise to learn the (by all accounts) equally excellent R

Brett Henderson 2 Jan 17, 2022
Eldrow: Wordle in Reverse

Eldrow: Wordle in Reverse Setup First you are gonna have to get Rust at rust-lang.org. Then, you will need to have nodejs installed. For the WebAssemb

Xuming Zeng 8 Sep 16, 2022
Naive and quick Wordle optimal starting word Analysis.

wordlentropy Naive and quick Wordle optimal starting word Analysis. This Rust code can analyze all 2315 Wordle games with 10657 word choices in 100 mi

Mufeed VH 2 Feb 7, 2022
A reimplementation of the excellent word game Wordle by Josh Wardle.

Paudle A reimplementation of the excellent word game Wordle by Josh Wardle. This version was created using Yew and Rust. I cribbed the colors and layo

Paul Sanford 39 Dec 5, 2022
Rushes are new ephemeral projects at Hive Helsinki. Wordle is the first in this series with 48 hours time window

Rushes are new ephemeral projects at Hive Helsinki. Wordle is the first in this series with 48 hours time window

Jiri Novotny 1 Feb 24, 2022
A Wordle solving assistant

Wordler A Wordle solving assistant. What and Why? Affected by the virally memetic game wordle, but linguistically incapable of solving it, I set forth

alberto 4 Feb 18, 2022
An application that tries to solve a Wordle puzzle only by using clues

wordlebot An application that tries to solve a Wordle puzzle only by using clues. You decide the word to discover (if you want to compete with wordleb

Rich Neswold 2 May 25, 2022
Solves wordle optimally by means of set subdivision

rust-wordle-solver Solves wordle optimally by means of set subdivision Building and running You should probably use the release build, as the debug bu

Gorgi Kosev 3 Feb 22, 2022
Using information theory, this is the optimal wordle player

enwordle Using information theory, this is the optimal wordle player. It is written in Rust and runs on the command-line. Theory When you pick a word

Aaron Hillegass 2 May 6, 2022
An interactive, universal Wordle solver

Eldrow (Wordle in reverse) is an interactive, universal Wordle solver that attempts to achieve near to mathematically perfect performance without rely

agubelu 3 Sep 2, 2022