RWGdle
Press Generate and the dictionary deals you a random English word. The score — with every factor that made it — is the game.
Every generate is saved on the leaderboard. Logged-out rows are named Anonymous. Log in to put a username on your one roll per UTC day.
Run
npm install
npm run precompute
npm test
npm run dev
Open http://127.0.0.1:4721. The dev server listens on 0.0.0.0:4721.
npm run precompute rebuilds public/words.txt and src/data/histogram.json from data/enable1.txt. Commit those generated files with any scoring change so the tiers stay honest. You only need to rerun it after editing the scorer or the word list.
Cloudflare
The Worker config is wrangler.jsonc and open-next.config.ts. D1 binding name: DB. Database name: rngwordle. Email binding name: EMAIL, from [email protected]. Schema: migrations/0001_accounts.sql. npm run dev does not use that database or send mail. It keeps using data/local.sqlite and prints the login link in the server log. npm run deploy builds with OpenNext and deploys the Worker, including the EMAIL binding.
Word list
Blocked words. data/blocked-words.txt lists slurs the game never deals (69 words: racial and ethnic, homophobic, and ableist). npm run precompute drops them from the dictionary before anything else is built, so they never appear inside another card either (Inside, Anagram, Alphabet twins, Shrinking word), and it writes src/data/blocked-words.json for the app. Share links and score lookups refuse them, old rolls of them drop off the boards, and usernames containing them are rejected (look-alike spellings included) or shown as "Hidden player". Words that are only sometimes offensive are listed in the file but commented out; delete the # to block one, then rerun precompute.
The list is ENABLE1 (Enhanced North American Benchmark Lexicon), a public-domain word list compiled by Alan Beale for word games. It is vendored as data/enable1.txt from the public-domain enable1.txt in dolph/dictionary.
ENABLE1 is already lowercase a–z dictionary words: no proper nouns, no abbreviations, no punctuation. The precompute step keeps entries of at least two letters and drops anything that isn’t plain letters. The bundled list runs from 2-letter words through 28-letter words (172,823 words), so both ends of the length multiplier have something to do.
Scoring
The base is the Scrabble tile sum. Everything else multiplies it. The breakdown shows each multiplier, and a miss stays an em dash.
Tile pile. Standard English Scrabble values: A E I O U L N S T R = 1, D G = 2, B C M P = 3, F H V W Y = 4, K = 5, J X = 8, Q Z = 10. Y is always a consonant here.
Length. ×1 at 9 letters, the average and the median in this list. Each step shorter doubles the multiplier. Each step longer adds 1.
| Letters | Multiplier |
|---|
| 2 | ×128 |
| 3 | ×64 |
| 4 | ×32 |
| 5 | ×16 |
| 6 | ×8 |
| 7 | ×4 |
| 8 | ×2 |
| 9 | ×1 |
| 10 | ×2 |
| 11 | ×3 |
| 12 | ×4 |
| 15 | ×7 |
| 20 | ×12 |
Factors. A hit multiplies the running total once. The multiplier is round(3 × log10(list size / matches)), at least ×2, counted on all 172,823 words.
| Name | When it hits | Matches | Multiplier |
|---|
| Twins | A letter repeated back to back | 41,209 | ×2 |
| Double twins | Two runs of exactly two identical letters sitting against each other, like ffee in coffee. A run of three or more of the same letter is not a pair. Three pairs in a row also hit | 223 | ×9 |
| Triple twins | Three runs of exactly two identical letters in a row, like ookkee in bookkeeper. A run of three or more of the same letter is not a pair | 4 | ×14 |
| No repeats | Every letter is different | 34,816 | ×2 |
| Even company | Every distinct letter appears exactly twice. A letter that appears once, or three or more times, misses. The card lights the whole word | 92 | ×10 |
| Perfectly shared | Every distinct letter occurs the same number of times. A count of one counts, so a word with no repeated letters hits. A single-letter word hits when it is in the list. noon hits, because n and o each occur twice. book misses, because the counts differ. Only a–z letters count, ignoring case. One card per word, and it lights the whole word | 34,909 | ×2 |
| Inside | A dictionary word of at least 3 letters sits inside. The whole word does not count. Each match multiplies once, including a word nested in a longer match | 167,370 | ×2 |
| Letter sandwich | Removing the first and last letters leaves an ENABLE word of at least 3 letters. Shorter words miss. The card lights that middle and names it | 7,298 | ×4 |
| Front or back | The word with its first letter removed and the word with its last letter removed are both ENABLE words. If only one is a word, it misses. The card names both and lights the whole word | 5,031 | ×5 |
| Shrinking word | A chain of at least 5 ENABLE words, counting this word, each made by deleting one letter from anywhere in the previous word. The card names the chain and lights the whole word | 9,924 | ×4 |
| Inside out | Moving the first letter to the end makes a different ENABLE word. The same spelling again misses. The card names that word and lights the whole rolled word | 1,061 | ×7 |
| Swap shop | Swapping two different adjacent letters makes a different ENABLE word. Each pair is its own card, lighting those two letters and naming the word. Identical letters and non-adjacent swaps do not count |
Definitions are the first plain English sentence from the kaikki.org English Wiktionary dump, cut to one sentence and 180 characters, and shipped in src/data/definitions.json. Wiktionary text is available under CC BY-SA. A roll asks /api/definition for that one sentence. The glosses ship as static assets split by the word's first two letters (public/definitions/ab.json), not part of the Worker script, so a lookup reads one small shard. The page does not call Wiktionary. A word with no gloss says “No definition on file.” 167,356 of the 172,823 ENABLE words have one. Rebuild with curl -fsL https://kaikki.org/dictionary/English/kaikki.org-dictionary-English.jsonl | python3 scripts/build-definitions.py, then npm run split-definitions. The dump itself is not vendored.
Origin tags are built offline from English Wiktionary, using the kaikki.org wiktextract dump, and shipped with the game. Wiktionary text is available under CC BY-SA. Only borrowed-from, inherited-from, and derived-from templates count. A cognate mentioned as “akin to” does not. A hop through Middle English, or through another modern English stage, is followed to that word’s own borrowed, inherited, or derived templates. There is no From Middle English card. An inflected form inherits the lemma’s origin languages. That copy uses Wiktionary’s form-of link when the entry says this word is a form of another English word. A regular stem is used only when that link is missing and exactly one ENABLE stem is unambiguous. Compounds such as computer and email stay without an origin from this pass. 57,920 of the 172,823 ENABLE words have at least one of these origins. A miss means Wiktionary has no usable origin for that language. The sound-word row is still the public-domain note in Webster’s Revised Unabridged Dictionary (1913).
Rewind and Ditto need no outside list. Rewind is a semordnilap. Ditto is a tautonym. Palindromes were already Mirror. Noun, verb, and adjective still need part-of-speech tags this list does not have. No vowels is already Bone dry, and a doubled letter is already Twins.
Rarity. The total is ranked against the precomputed score of every bundled word. “Beats n%” means the share of the list with a strictly lower score. Ties do not count as beaten.
| Tier | Standing |
|---|
| Mythic | Beats at least 99% |
| Epic | Beats at least 95% |
| Rare | Beats at least 85% |
| Uncommon | Beats at least 65% |
| Common | Beats at least 35% |
| Common | Beats under 35% |
Accounts
There is no password. Log in opens over the page you are on. Enter an email, and the game emails a one-time link from [email protected]. The popup does not show the link. The link lasts 30 minutes and works once. Opening it does not use it: /auth/verify says the link stays unused until it is confirmed, by a script in the browser that asked or by the Log in button anywhere else, and that is a normal form POST. Mail scanners run neither. Another request for the same address within a minute does not send again; the popup counts down to Resend. The tab that asked waits for the link: tapping it anywhere, in another app or on another device, logs that tab in too. Opened in the same browser, the link logs in with no extra tap. In any other browser it asks once and names the device that asked, so a link nobody requested cannot log in someone else. Sessions last a year and each visit renews them. After it logs you in, pick a username: 3–20 characters, letters, numbers, and underscores, unique ignoring case.
Accounts, login links, sessions, and saved rolls use the same tables in two places. next dev writes data/local.sqlite (gitignored) when the Cloudflare D1 binding is absent. The Worker uses the D1 binding DB (database_name rngwordle). The schema is migrations/0001_accounts.sql: text ids, integer unix milliseconds, a digit-string score, and UNIQUE (account_id, utc_day). A roll with no account leaves account_id null, which SQLite does not treat as one shared key, so each anonymous generate adds a row. The app rebuilds an older rolls table that required an account the first time it opens the database. Each statement is prepared on its own: D1's exec() treats every line as a separate query, so a formatted CREATE TABLE stops there with incomplete input. No API keys. The Worker sends the login link with the EMAIL binding. npm run dev prints that link in the server log instead.
Leaderboard
The leaderboard is at /leaderboard. The shared boards and today's top roll are cached for 30 seconds per Cloudflare location, and indexes on the score order (rolls_rank, rolls_day_rank) let a board read only the rows it shows. Four views, top 100 each, highest score first: today (UTC day), this week (Monday 00:00 UTC through now), this month (calendar month UTC), and all time. Each row is rank, username, word, and score. Anonymous rows are included. A tie goes to the earlier roll. An empty period says so.
A logged-in player with a username can switch that leaderboard to Friends. It ranks their rolls plus accepted friends, for the same four periods. Anonymous players have no friends list.
Friends
Open Friends, type the other player's exact username, and send a request. They accept on their own Friends page. Until then it stays pending. The requester can cancel. The other player can decline. Either player can remove an accepted friend. No email is sent. You need a username first, and you cannot add yourself. An unknown username, a request to yourself, and a duplicate request each show an error. The friendships table is created the first time the app opens the database, the same way an older rolls table is rebuilt, so an existing D1 database grows it without pasted SQL.
Rolls
Generate asks the server to deal. The server uses the same list and the same scorer. On the Worker that list is the /words.txt asset, because the Worker does not keep the app directory as its working directory. A logged-out browser gets one leaderboard roll per UTC day, saved with no account as Anonymous and tracked by a hashed cookie (rngworlde_guest, table guest_days); its { date, word } stays in this browser under rngworlde.roll.v1. Asking again that day returns the same word: nobody, guest or player, can roll a second word until the next UTC day, and the header shows a countdown to it. Rolls are rate limited per visitor (60 a minute, Cloudflare ROLL_LIMITER), and login emails too (5 a minute via LOGIN_LIMITER, 30 a day in login_requests). A logged-in roll is saved once per account per UTC day. The row uses the username if they have one, and Anonymous if they do not. Another generate that day shows the saved word and does not replace it.