Years ago when I did a notebook to solve Jotto, I never expected that a similar word game, Wordle, would become so popular. Congratulations to Josh Wardle for making this happen. I added Wordle to my old Jotto notebook, and in this notebook, I answer three additional questions about Wordle.
I see people brag that they won Wordle in two guesses. Does this really attest to their acumen? Or is it analagous to Little Jack Horner, who pulled out a plum and said What a good boy am I!, oblivious to the fact that his prosperity had more to do with the plethora of plums than with his prowess.
What is your chance of winning on your second guess? It depends on your first guess, the reply you get from the first guess, and on how many other possible words have the same reply. For example, if your guess is HELLO
, and the reply is .GGGG
(a miss followed by 4 green squares), then the only possible target word is CELLO
(note that JELLO
™ is a proper noun, and thus is not in the word list). Less obviously, if the reply to HELLO
is .YGY.
(a miss, a yellow E, a green L, another yellow L, and another miss), then the only possible target word is ALLEY
. So in either case, you are guaranteed to win on your second guess, because there is only one possible word remaining (assuming you can recognize the sole possible word). On the other hand, if the reply is .....
(all misses), then there are 406 possible target words, and the chance of guessing right on the second guess is very low. Below we work out all the details and find that:
Answer: mostly lucky. The first guess BRUTE
has the most guaranteed two-guess wins: 40 out of the 2,309 words in the word list (about 2%). The first guess FILET
gives the most expected wins, 56.7, assuming you have average luck in guessing with the second guess.
Christos Papadimitriou had the idea of using a radically simple strategy that takes very little thought or memorization: always choose the same first 4 guesses (regardless of the replies), and with the last two guesses, guess any word that is consistent with the replies so far. Christos came up with 4 words that win 99% of the time. I was able to refine that and find 4 guesses that always lead to a win (assuming you can recognize the consistent guesses).
Answer: Guess the four word set {HANDY SWIFT GLOVE CRUMP}
first; then guess any word consistent with the replies.
The four-preset-words strategy described above is guaranteed to win in 6 or fewer guesses, but it averages about 5.
If I'm not satisfied to win in 5 or 6 guesses, what strategies can I use (especially simple ones)?
Answers:
{BLIND SHAME CRYPT}
first wins 99.8% of the time with 4.2 average guesses.{RETCH SNAIL}
first wins 99.4% with 3.8 average guesses.{RAISE}
first wins 98% with 3.9 average guesses.I'll start with some basics: imports, and reading in the word list, words
:
from typing import List, Tuple, Dict, Counter, Iterable
from collections import defaultdict
from pathlib import Path
from functools import lru_cache
import pandas as pd
import random
Word = str # A type: a word is a string of five letters
! [ -e wordle-small.txt ] || curl -O https://norvig.com/ngrams/wordle-small.txt
words = Path('wordle-small.txt').read_text().split() # 2,309 target words
Below are functions to compute the reply_for
a single guess and the replies_for
a sequence of guesses:
Reply = str # A reply is five characters, e.g. '.Y..G'
Green, Yellow, Miss = 'G', 'Y', '.' # Components of a Reply
@lru_cache(None)
def reply_for(guess: Word, target: Word) -> Reply:
"The five-character reply for this guess on this target in Wordle."
# (1) Start by having each reply be either Green or Miss ...
reply = [(Green if guess[i] == target[i] else Miss) for i in range(5)]
# (2) Then change the replies that should be yellow
counts = Counter(target[i] for i in range(5) if guess[i] != target[i])
for i in range(5):
if reply[i] == Miss and counts[guess[i]] > 0:
counts[guess[i]] -= 1
reply[i] = Yellow
return ''.join(reply)
def replies_for(guesses, target) -> Tuple[Reply]:
"""A tuple of replies for a sequence of guesses."""
return tuple(reply_for(guess, target) for guess in guesses)
For example, if the target word is 'LILAC'
, here is the reply for the guess 'HELLO'
:
reply_for('HELLO', 'LILAC')
'..GY.'
And the replies for the two-guess sequence ['HELLO', 'DOLLY']
:
replies_for(['HELLO', 'DOLLY'], 'LILAC')
('..GY.', '..GY.')
We say that a sequence of guesses partitions a word list into bins:
partitions(guesses, targets)
returns a dict where each key is a replies_for(guesses, t)
for some target word t
, and the corresponding value is the list of words that give the same replies for those guesses.bins(guesses, targets)
just gives the bins, without the replies.Bin = List[Word] # Type for a Bin of words
Wordset = Tuple[Word, ...] # Type for a tuple of guess words
Partition = Dict[Wordset, Bin] # Type for a Partition
def partition(guesses, targets) -> Partition:
"""Partition `targets` by replies to `guesses`: {(reply, ...): [word, ...]}"""
dic = defaultdict(list)
for target in targets:
if target not in guesses:
replies = replies_for(guesses, target)
dic[replies].append(target)
return dic
def bins(guesses, targets) -> Iterable[Bin]:
"""Partition `targets`, and return the bins without the replies."""
return partition(guesses, targets).values()
To see this in action, I'll define few
as a list of a few words (9 to be exact):
few = 'HELLO WORLD DOLLY CELLO ALLEY HEAVY HEART ALLAY LILAC'.split()
Here is ['HELLO', 'DOLLY']
partitioning the few words:
partition(['HELLO', 'DOLLY'], few)
defaultdict(list, {('...GY', 'YG.G.'): ['WORLD'], ('.GGGG', '.YGG.'): ['CELLO'], ('.YGY.', '..GYG'): ['ALLEY'], ('GG...', '....G'): ['HEAVY'], ('GG...', '.....'): ['HEART'], ('..GY.', '..GYG'): ['ALLAY'], ('..GY.', '..GY.'): ['LILAC']})
All the bins have only one word in them, which means that after guessing 'HELLO'
and 'DOLLY'
we could always win the game in one more guess. On the other hand, the two guess sequence ['HELLO', 'WORLD']
leaves one bin with size 2, and thus we could not always win on the third guess; we'd have to be lucky in choosing between 'ALLAY'
or 'LILAC'
for our third guess.
partition(['HELLO', 'WORLD'], few)
defaultdict(list, {('..GGY', '.G.GY'): ['DOLLY'], ('.GGGG', '.Y.G.'): ['CELLO'], ('.YGY.', '...Y.'): ['ALLEY'], ('GG...', '.....'): ['HEAVY'], ('GG...', '..Y..'): ['HEART'], ('..GY.', '...Y.'): ['ALLAY', 'LILAC']})
I will make a table of all possible first-guess words, where each row of the table contains:
def guess_row(guess) -> Tuple[Word, int, float, int]:
"""A tuple of a (guess word, nuber of guaranteed wins, expected wins, maximum bin size)."""
B = bins([guess], words)
return (guess, sum(len(bin) == 1 for bin in B), sum(1 / len(bin) for bin in B))
df = pd.DataFrame(map(guess_row, words), columns=['Guess', 'Wins', 'E(Wins)'])
Now I'll sort the table by most guaranteed wins, and see that 'BRUTE'
devlivers the most guaranteed wins, 40:
df.sort_values('Wins', ascending=False)
Guess | Wins | E(Wins) | |
---|---|---|---|
291 | BRUTE | 40 | 55.018860 |
354 | CHANT | 39 | 52.746261 |
1223 | METRO | 38 | 54.233278 |
1865 | SPILT | 38 | 50.831156 |
988 | HORDE | 37 | 50.137364 |
... | ... | ... | ... |
2299 | WRYLY | 6 | 14.640533 |
1513 | QUEUE | 5 | 9.939117 |
1509 | QUEER | 4 | 11.173628 |
1272 | MUMMY | 4 | 9.767194 |
1508 | QUEEN | 4 | 11.218357 |
2309 rows × 3 columns
Next I'll sort by expected wins, and see that 'FILET'
is best on this metric:
df.sort_values('E(Wins)', ascending=False)
Guess | Wins | E(Wins) | |
---|---|---|---|
733 | FILET | 36 | 56.659162 |
1372 | PARSE | 35 | 56.173751 |
553 | DINER | 37 | 55.975379 |
291 | BRUTE | 40 | 55.018860 |
1223 | METRO | 38 | 54.233278 |
... | ... | ... | ... |
1048 | JAZZY | 8 | 12.385749 |
1508 | QUEEN | 4 | 11.218357 |
1509 | QUEER | 4 | 11.173628 |
1513 | QUEUE | 5 | 9.939117 |
1272 | MUMMY | 4 | 9.767194 |
2309 rows × 3 columns
Still, 56.6 wins out of 2,309 targets is less than 2.5%, so if you win on the second guess, you're very lucky.
(Note that 'MUMMY'
is the worst first guess on both metrics.)
Christos Papadimitriou came up with a fixed set of 4 words, {ARISE CLOMP THUNK BAWDY}
, that allow you to win almost all of the time if you use them as your first 4 guesses, and then guess any consistent word on your 5th and 6th guesses. The strategy would be much harder if we had to rack our brains to think of all the possible consistent words on the fifth and sixth guesses; it is critical to the simplicity of the strategy that as soon as you think of one consistent word you can guess it and always be guaranteed to win. The function always_wins
verifies this property:
def always_wins(guesses, more=2, words=words) -> bool:
"""After the sequence of guesses, are we guaranteed to always win in `more` consistent guesses?
We are if every bin created by `guesses` has `more` words or less, or if guessing any word in
the bin leads to an `always_win` with one fewer guess."""
return all(len(bin) <= more or
more >= 1 and all(always_wins([guess], more - 1, bin) for guess in bin)
for bin in bins(guesses, words))
always_wins(('ARISE', 'CLOMP', 'THUNK', 'BAWDY')) # Christos's 4 words
False
Sadly, Christos's guess set does not always win. At the top of the notebook, I showed a guess set that does always win:
always_wins(('HANDY', 'SWIFT', 'GLOVE', 'CRUMP'))
True
Here is my approach for finding winning guess sets:
disjoint_guess_set
returns a collection of guess words with distinct letters (by depth-first exhaustive search).random_disjoint_guess_sets
returns a list of N
disjoint guess_sets.JQXZ
.random_disjoint_guess_sets
shuffles the good words after each call to disjoint_guess_set
to yield N
different guess sets.letters = ''.join(a for a, _ in Counter(''.join(words)).most_common()) # letters ordered by frequency
letters22 = set(letters[:22]) # 22 most frequent letters, omitting `JQXZ`.
def disjoint_guess_set(W, letters:set, good_words) -> Wordset:
"""Tuple of `W` words made of `letters`, with no repeated letters."""
if W == 0:
return ()
for word in good_words:
if letters.issuperset(word):
others = disjoint_guess_set(W - 1, letters - set(word), good_words)
if others is not None:
return (word, *others)
return None
def random_disjoint_guess_sets(N, W=4, letters=letters22, words=words) -> Iterable[Wordset]:
"""`N` random disjoint `W`-word guess sets made out of distinct `letters`."""
good_words = [w for w in words if len(set(w)) == 5 and letters.issuperset(w)]
for _ in range(N):
yield disjoint_guess_set(W, letters, good_words)
random.shuffle(good_words)
For example:
list(random_disjoint_guess_sets(5, 4, letters22)) # 5 different 4-word guess sets
[('ABHOR', 'CLEFT', 'DUMPY', 'SWING'), ('STONE', 'PUDGY', 'WHARF', 'CLIMB'), ('HAREM', 'BLOWN', 'PUDGY', 'STICK'), ('OVARY', 'THUMB', 'FLECK', 'SWING'), ('GODLY', 'CRUMB', 'SHIFT', 'KNAVE')]
And finally, we can find the winners within the guess_sets:
%time winners = list(filter(always_wins, random_disjoint_guess_sets(1000, 4, letters22)))
winners
CPU times: user 1min 59s, sys: 1.87 s, total: 2min 1s Wall time: 2min 14s
[('CRUST', 'VEGAN', 'HOWDY', 'BLIMP'), ('WELSH', 'COMFY', 'GRUNT', 'VAPID'), ('SLURP', 'MIGHT', 'COVEN', 'BAWDY'), ('STAID', 'CRUMB', 'WOVEN', 'GLYPH'), ('DUTCH', 'BALMY', 'WOVEN', 'SPRIG'), ('CLANG', 'WISPY', 'THUMB', 'DROVE')]
It works! There are lots of guess sets that win every time. (It looks like about 1% of the random disjoint guess sets always win.)
There are some important caveats; the strategy only works under the following assumptions:
It is great that a simple strategy is guaranteed to win, but it will probably take 5 or 6 guesses. I'd like to quantify exactly how many guesses, on average. To do that, I'll start by defining the following:
Frequency
is an alias for Counter
, but the values might be fractions, not integers. For example, in the case where I need one guess 1/4 of the time and 2 guesses 3/4 of that time, I can represent that as Frequency({1: 0.25, 2: 0.75})
.scores(guesses)
gives a frequency counter of {score: number_of_times_we_get_that_score}
, summed over all possible target words, and averaged over all possible guess words in a bin. Sometimes the number of times will be a fraction, because we are averaging over guesses within a bin.average([freq, freq, ...])
gives the average of the frequency counters.Frequency = Counter # Type to hold {item: frequency} mapping; frequency need not be an integer
def scores(guesses, so_far=0, targets=words) -> Frequency:
"""A frequency counter of all possible scores from playing these guesses first,
then playing any consistent guess (and averaging over the possible consistent guesses)."""
result = Frequency(range(so_far + 1, so_far + 1 + len(guesses))) # Initial guesses might be right
so_far += len(guesses)
for bin in bins(guesses, targets):
result += (Frequency([so_far + 1]) if len(bin) == 1 else
average(scores([guess], so_far, bin) for guess in bin))
return result
def average(frequencies) -> Frequency:
"""The mean of k Frequency counters."""
frequencies = list(frequencies)
total = sum(frequencies, start=Frequency())
k = len(frequencies)
return {i: total[i] / k for i in total}
assert average([Frequency({1: 0.25, 2: 0.75}), Frequency({1: 0.75, 2: 0.25, 3: 1})]) == {1: 0.5, 2: 0.5, 3: 0.5}
Next, report
will print a report on how well an initial guess set scores:
def report(guesses, show_bins=3):
"""Print a report on these guesses: do they win or not, and why?"""
def fmt(words): return "{" + " ".join(words) + "}" # sequence -> str
freq = scores(guesses)
freq2 = {k: round(v) if v > 1 else round(v, 3) for k, v in freq.items()}
p = sum(freq[s] for s in range(1, 7)) / len(words)
print(f'\n{fmt(guesses)} wins {p:.2%} of the time')
print(f'mean score: {mean_score(freq):.2f}; max score: {max(freq)}')
print(f'score frequencies: {freq2}')
print(f'bin sizes: {dict(Counter(sorted(map(len, bins(guesses, words)))))}')
for bin in sorted(bins(guesses, words), key=len, reverse=True):
if len(bin) >= show_bins:
bad_words = [w for w in bin if not always_wins([w], 6 - len(guesses) - 1, bin)]
if bad_words:
print(f'bin {fmt(bin)} can lose with: {fmt(bad_words)}')
def mean_score(freq: Frequency) -> float:
"""Given a frequency counter, compute the mean of the keys weighted by the values."""
return sum(s * freq[s] for s in freq) / sum(freq.values())
Here is the report
on Christos's guess set, and on my winners:
report(('ARISE', 'CLOMP', 'THUNK', 'BAWDY')) # Christos's 4 words
{ARISE CLOMP THUNK BAWDY} wins 99.76% of the time mean score: 5.06; max score: 7 score frequencies: {1: 1, 2: 1, 3: 1, 4: 1, 5: 2159, 6: 140, 7: 8} bin sizes: {1: 2032, 2: 107, 3: 19, 4: 1} bin {OTTER OVERT RETRO VOTER} can lose with: {RETRO} bin {EAGER GAZER RARER} can lose with: {RARER} bin {ESTER RESET STEER} can lose with: {RESET} bin {FIXER GIVER RIVER} can lose with: {FIXER} bin {FOCAL LOCAL VOCAL} can lose with: {FOCAL LOCAL VOCAL} bin {FOLLY GOLLY JOLLY} can lose with: {FOLLY GOLLY JOLLY} bin {GAUNT JAUNT VAUNT} can lose with: {GAUNT JAUNT VAUNT} bin {OFFER ROGER ROVER} can lose with: {OFFER} bin {PIPER RIPER VIPER} can lose with: {PIPER RIPER VIPER} bin {STAGE STATE STAVE} can lose with: {STAGE STATE STAVE} bin {WAFER WAGER WAVER} can lose with: {WAFER WAGER WAVER}
for winner in [('HANDY', 'SWIFT', 'GLOVE', 'CRUMP')] + winners:
report(winner)
{HANDY SWIFT GLOVE CRUMP} wins 100.00% of the time mean score: 5.03; max score: 6 score frequencies: {1: 1, 2: 1, 3: 1, 4: 1, 5: 2222, 6: 83} bin sizes: {1: 2141, 2: 79, 3: 2} {CRUST VEGAN HOWDY BLIMP} wins 100.00% of the time mean score: 5.04; max score: 6 score frequencies: {1: 1, 2: 1, 3: 1, 4: 1, 5: 2209, 6: 96} bin sizes: {1: 2124, 2: 77, 3: 5, 4: 3} {WELSH COMFY GRUNT VAPID} wins 100.00% of the time mean score: 5.04; max score: 6 score frequencies: {1: 1, 2: 1, 3: 1, 4: 1, 5: 2200, 6: 105} bin sizes: {1: 2102, 2: 92, 3: 5, 4: 1} {SLURP MIGHT COVEN BAWDY} wins 100.00% of the time mean score: 5.03; max score: 6 score frequencies: {1: 1, 2: 1, 3: 1, 4: 1, 5: 2222, 6: 83} bin sizes: {1: 2147, 2: 69, 3: 4, 4: 2} {STAID CRUMB WOVEN GLYPH} wins 100.00% of the time mean score: 5.04; max score: 6 score frequencies: {1: 1, 2: 1, 3: 1, 4: 1, 5: 2210, 6: 95} bin sizes: {1: 2122, 2: 82, 3: 5, 4: 1} {DUTCH BALMY WOVEN SPRIG} wins 100.00% of the time mean score: 5.04; max score: 6 score frequencies: {1: 1, 2: 1, 3: 1, 4: 1, 5: 2202, 6: 103} bin sizes: {1: 2107, 2: 88, 3: 6, 4: 1} {CLANG WISPY THUMB DROVE} wins 100.00% of the time mean score: 5.04; max score: 6 score frequencies: {1: 1, 2: 1, 3: 1, 4: 1, 5: 2205, 6: 100} bin sizes: {1: 2108, 2: 94, 3: 3}
With 4 preset guesses, we're destined to win in 5 guesses most of the time, or sometimes 6. What if we aspire to win in 4 guesses? Or 3? We could try a smaller preset guess set, and find one with a low mean score. This function will help:
def good_guess_set(N, W=4, letters=letters22) -> Wordset:
"""Generate `N` `W`-word guess sets, and see which one has the lowest mean score."""
return min(random_disjoint_guess_sets(N, W, letters),
key=lambda guesses: mean_score(scores(guesses)))
I'll search for a good guess set with 3 words, and then with 2 words:
report(good_guess_set(200, 3, set(letters[:18])))
{BLIND SHAME CRYPT} wins 99.83% of the time mean score: 4.19; max score: 9 score frequencies: {1: 1, 2: 1, 3: 1, 4: 1892, 5: 380, 6: 30, 7: 4, 8: 0.086, 9: 0.004} bin sizes: {1: 1607, 2: 213, 3: 40, 4: 20, 5: 5, 6: 4, 7: 2, 10: 1} bin {FEVER FEWER JOKER OFFER QUEER REFER ROGER ROVER ROWER WOOER} can lose with: {FEVER FEWER JOKER OFFER QUEER REFER ROGER WOOER} bin {FOLLY FULLY GOLLY GULLY JOLLY LOWLY WOOLY} can lose with: {LOWLY WOOLY} bin {AFTER EATER EXTRA TAKER TERRA WATER} can lose with: {TERRA} bin {EAGER GAZER RARER WAFER WAGER WAVER} can lose with: {EAGER GAZER RARER WAFER WAVER} bin {OTTER OUTER RETRO TOWER UTTER VOTER} can lose with: {RETRO} bin {AWOKE GAFFE GAUGE GAUZE VAGUE} can lose with: {AWOKE} bin {SKATE STAGE STAKE STATE STAVE} can lose with: {STAGE STAKE STATE STAVE} bin {DODGE FUDGE JUDGE WEDGE} can lose with: {DODGE WEDGE} bin {GAUNT JAUNT TAUNT VAUNT} can lose with: {GAUNT JAUNT TAUNT VAUNT}
report(good_guess_set(20, 2, set(letters[:14])))
{RETCH SNAIL} wins 99.38% of the time mean score: 3.79; max score: 10 score frequencies: {1: 1, 2: 1, 3: 953, 4: 966, 5: 310, 6: 64, 7: 12, 8: 2, 9: 0.089, 10: 0.002} bin sizes: {1: 535, 2: 170, 3: 92, 4: 44, 5: 34, 6: 22, 7: 11, 8: 10, 9: 5, 10: 6, 11: 3, 12: 3, 13: 3, 14: 2, 15: 3, 16: 1, 17: 1, 18: 1, 19: 1, 21: 1, 23: 1, 25: 1, 28: 1, 29: 1, 39: 1} bin {BOXER BREED BROKE BUYER DROVE DRYER EMBER ERODE ERROR EVERY FORGE FOYER FREED FREER FROZE GORGE GREED GROPE GROVE JOKER MOVER MOWER ODDER OFFER OMBRE ORDER POKER POWER PROBE PROVE PRUDE PUREE PURER PURGE QUEER QUERY UDDER UPPER WOOER} can lose with: {BOXER BREED BROKE BUYER DROVE DRYER EMBER ERODE ERROR EVERY FORGE FOYER FREED FREER FROZE GORGE GREED GROPE GROVE JOKER MOVER MOWER ODDER OFFER OMBRE ORDER POKER POWER PROBE PROVE PRUDE PUREE PURER PURGE QUEER QUERY UDDER UPPER WOOER} bin {BOBBY BOOBY BOOZY BUDDY BUGGY BUXOM DODGY DOWDY DUMMY DUMPY FOGGY FUZZY GOODY GOOFY GUMBO GUMMY GUPPY JUMBO JUMPY MOODY MUDDY MUMMY POPPY PUDGY PUFFY PUPPY PYGMY WOODY WOOZY} can lose with: {BOBBY BOOBY BOOZY BUDDY BUGGY BUXOM DOWDY DUMMY FOGGY FUZZY GUMMY JUMBO JUMPY MUDDY MUMMY POPPY PUFFY PUPPY PYGMY WOODY WOOZY} bin {BLOOD BLOOM BLUFF BULKY BULLY DOLLY DULLY FLOOD FLUFF FOLLY FULLY GLOOM GODLY GOLLY GULLY JOLLY LOBBY LOOPY LOWLY LUMPY MOLDY ODDLY PLUMB PLUMP POLYP PULPY WOOLY WOULD} can lose with: {BLOOD BLOOM BLUFF BULKY BULLY FLUFF GLOOM JOLLY LOBBY LOOPY LOWLY LUMPY MOLDY ODDLY PLUMB PLUMP POLYP PULPY WOOLY} bin {ADORE AGREE AMBER ARGUE AZURE BAKER BARGE BREAD BREAK DREAD DREAM EAGER FREAK GAMER GAYER GAZER MAKER OPERA PAPER PARER PAYER WAFER WAGER WAVER WREAK} can lose with: {ADORE AGREE AMBER ARGUE AZURE BAKER BARGE BREAD BREAK DREAD DREAM EAGER FREAK GAMER GAYER GAZER MAKER OPERA PAPER PARER PAYER WAFER WAGER WAVER WREAK} bin {BRIBE BRIDE BRIEF DIRGE DIVER DRIED DRIER DRIVE FIBER FIERY FIXER FRIED GIVER GRIEF GRIME GRIPE PIPER PRIDE PRIED PRIME PRIZE VIPER WIDER} can lose with: {BRIBE BRIEF DIVER DRIED DRIER DRIVE FIBER FIERY FIXER FRIED GIVER GRIEF GRIME PIPER PRIME PRIZE VIPER WIDER} bin {BROOD BROOK BROOM DOWRY DROOP FJORD FORGO FORUM FUROR FURRY GOURD GROOM GROUP GRUFF JUROR MURKY PROOF PROUD PROXY WORDY WORRY} can lose with: {DOWRY GRUFF JUROR WORRY} bin {ARBOR ARDOR ARMOR AROMA ARRAY ARROW AUGUR BORAX BROAD FAVOR FORAY KARMA MAJOR MARRY MAYOR PARKA PARRY UMBRA VAPOR} can lose with: {BORAX FORAY PARKA} bin {BAGGY BAWDY BAYOU DADDY DOGMA GAMMA GAUDY GAWKY JAZZY KAPPA KAYAK MADAM MAGMA MAMBO MAMMA MAMMY PADDY VODKA} can lose with: {BAGGY BAYOU DADDY GAMMA GAWKY JAZZY KAPPA KAYAK MAGMA MAMBO MAMMA MAMMY PADDY} bin {AGLOW ALBUM ALLAY ALLOW ALLOY ALOOF ALOUD AMPLY APPLY BADLY BALMY BYLAW DALLY GAYLY MADLY POLKA} can lose with: {ALOOF APPLY} bin {ABLED ALGAE ALLEY AMBLE AMPLE APPLE BLEAK EAGLE FABLE GLEAM LADLE MAPLE PLEAD VALUE VALVE} can lose with: {ALGAE BLEAK VALUE VALVE} bin {AGONY AMONG BANJO DANDY FANNY FAUNA GONAD MANGA MANGO MANGY NANNY NOMAD PAGAN WAGON WOMAN} can lose with: {BANJO MANGA PAGAN} bin {BOOZE BUDGE DODGE DOPEY EMBED EPOXY EVOKE FUDGE FUGUE GOOEY GOUGE JUDGE MODEM QUEUE VOGUE} can lose with: {EPOXY} bin {ABBEY ABODE ABOVE ADOBE AWOKE BADGE GAFFE GAUGE GAUZE MAUVE MAYBE OMEGA PAYEE VAGUE} can lose with: {ABODE ABOVE ADOBE AWOKE} bin {BERRY DEFER DEMUR DERBY FEMUR FERRY FEVER FEWER JERKY MERGE MERRY PERKY VERGE VERVE} can lose with: {DEMUR FEMUR FEVER FEWER MERGE VERGE VERVE} bin {BILLY BLIMP BUILD DILLY DIMLY FILLY FILMY GUILD IGLOO IMPLY LIMBO MILKY WILLY} can lose with: {BLIMP GUILD IGLOO IMPLY MILKY} bin {BINGO DINGO DINGY DOING DYING FUNGI GOING KINKY NINNY OWING PINKY VYING WINDY} can lose with: {BINGO FUNGI} bin {BROWN DONOR DROWN DRUNK FROND FROWN GROWN MORON MOURN PRONG WRONG WRUNG} can lose with: {MORON MOURN} bin {CORER COVER COWER CREDO CREED CREEK CREEP CREME CREPE CRUDE CURVE CYBER} can lose with: {CREED CREEK CREEP CREME CREPE CRUDE CURVE} bin {AUNTY DAUNT GAUNT JAUNT JUNTA TANGO TANGY TAUNT TAWNY TONGA VAUNT} can lose with: {AUNTY DAUNT GAUNT JAUNT JUNTA TAUNT TAWNY VAUNT} bin {BONGO BOUND BUNNY DOWNY FOUND FUNKY FUNNY MOUND POUND WOUND YOUNG} can lose with: {BONGO BOUND BUNNY DOWNY FOUND FUNKY FUNNY MOUND POUND WOUND YOUNG} bin {ADULT ALLOT ALOFT BLOAT FAULT FLOAT GLOAT TALLY VAULT WALTZ} can lose with: {VAULT WALTZ} bin {AWARE BRAKE BRAVE DRAKE DRAPE FRAME GRADE GRAPE GRAVE GRAZE} can lose with: {AWARE BRAKE BRAVE DRAKE DRAPE FRAME GRAZE} bin {BIDDY DIZZY FIZZY GIDDY IDIOM JIFFY OPIUM PIGGY WIDOW WIMPY} can lose with: {JIFFY OPIUM WIMPY} bin {BONEY DOZEN EBONY MONEY NUDGE OZONE QUEEN WOKEN WOMEN WOVEN} can lose with: {BONEY EBONY NUDGE OZONE QUEEN} bin {RODEO ROGER ROGUE ROUGE ROVER ROWER RUDER RUPEE} can lose with: {RUPEE} bin {SKUNK SOUND SPOON SPUNK SUNNY SWOON SWUNG SYNOD} can lose with: {SUNNY} bin {BOWEL DOWEL DWELL EXPEL MODEL QUELL VOWEL} can lose with: {QUELL} bin {BATCH CATCH HATCH MATCH PATCH WATCH} can lose with: {BATCH CATCH HATCH MATCH PATCH WATCH} bin {ASSET BASTE PASTE TASTE WASTE} can lose with: {ASSET} bin {BATTY DATUM FATTY PATTY TATTY} can lose with: {DATUM} bin {BUNCH CONCH HUNCH MUNCH PUNCH} can lose with: {CONCH} bin {SAPPY SASSY SAVOY SAVVY SQUAD} can lose with: {SQUAD} bin {SAUTE STEAD STEAK STEAM SWEAT} can lose with: {SAUTE} bin {SHADE SHAKE SHAME SHAPE SHAVE} can lose with: {SHADE SHAKE SHAME SHAPE SHAVE}
I hope this gives you some ideas for Wordle strategies you can use, and/or new computational ideas to explore.