{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "
Peter Norvig
Feb 2022
\n", "\n", "# Winning Wordle\n", "\n", "Years ago when I did a [notebook to solve Jotto](Jotto.ipynb), I never expected that a similar word game, [Wordle](https://www.nytimes.com/games/wordle/index.html), would become so popular. Congratulations to Josh Wardle for making this happen. I [added Wordle](Jotto.ipynb#Wordle) to my old notebook, but in this notebook, I answer two questions about Wordle (based on the pre-NYTimes version, with its [word list](wordle-small.txt) of 2,315 possible words).\n", "\n", "\n", "# (1) If I win in two guesses, am I good or lucky?\n", "\n", "I see people brag that they won in two guesses. Does this really attest to their acumen? Or is it analagous to [Little Jack Horner](https://en.wikipedia.org/wiki/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. \n", "\n", "What is your chance of winning on your second guess? It depends on 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`. Less obviously, if the reply to `HELLO` is `.YGY.` (a miss, a yellow, a green, another yellow, and another miss), then the only possible target word is `ALLEY`. So in either case, you are guaranteed to win on your second guess (assuming you can recognize the sole possible word). On the other hand, if the reply is `..GY.` then the target word might be either `ALLAY` or `LILAC`, so you'd have a 50% chance of winning on the second guess. So we have two questions:\n", "- Q: What first guess maximizes the number of **guaranteed wins** on the second guess?\n", "
A: `BRUTE` and `CHANT` guarantee you 40 wins (out of 2,315).\n", "- Q: What first guess maximizes the number of **expected wins** on the second guess?\n", "
A: `FILET` gives you 57.5 expected wins (out of 2,315). \n", "\n", "The probability of winning in two guesses is about 2%, so the answer is: mostly lucky.\n", "\n", "# (2) What is a winning strategy I can memorize?\n", "\n", "There has been some nice work on defining the [optimal Wordle strategy](https://www.poirrier.ca/notes/wordle-optimal/) for various ways of posing the problem. But the strategies are all complex tree structures with thousands of branch points; not the kind of thing that can be memorized by a typical human. [Christos Papadimitriou](https://www.engineering.columbia.edu/faculty/christos-papadimitriou) had the idea of using aradically simple strategy: 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. \n", "- Q: What simple strategy **guarantees a win** within 6 guesses?\n", "
A: Guess the four words `HANDY`, `SWIFT`, `GLOVE`, `CRUMP` first.
For guesses 5 and 6, guess any word consistent with the replies.\n", "\n", "If you follow this strategy, then out of 2,315 possible target words, there are:\n", "- 4 chances that you will win on one of the first four guesses\n", "- 2,147 chances that there will be only one consistent word left, which you will guess on the 5th guess. \n", "- 158 chances that there will be two consistent words left; so you'll win on either the 5th or 6th guess.\n", "- 6 chances that there are three consistent words left, but you can guess any one and if it is wrong, the reply will tell you which of the other two to guess on the 6th guess.\n", "\n", "With this strategy you will always win, with an average of a little over 5 guesses. That's worse than the complex strategies that are guaranteed to win in 5 guesses and have an average of around 3.5 or 3.4, but you only need to remember four words to use this strategy.\n", "\n", "# The Details\n", "\n", "Here are some basics, including the word list, `words`, and the `reply_for` function." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "from typing import *\n", "from collections import defaultdict\n", "import random \n", "import functools\n", "\n", "cache = functools.lru_cache(None)\n", "\n", "Word = str # A word is a lower-case string of five different letters\n", "Reply = str # A reply is five characters taken from 'GY.': Green, Yellow, Miss\n", "Green, Yellow, Miss = 'GY.'\n", "\n", "words = open('wordle-small.txt').read().upper().split() # 2,315 target words\n", "\n", "@cache\n", "def reply_for(guess, target) -> Reply: \n", " \"The five-character reply for this guess on this target in Wordle.\"\n", " # We'll start by having each reply be either Green or Miss ...\n", " reply = [Green if guess[i] == target[i] else Miss for i in range(5)]\n", " # ... then we'll change the replies that should be yellow\n", " counts = Counter(target[i] for i in range(5) if guess[i] != target[i])\n", " for i in range(5):\n", " if reply[i] == Miss and counts[guess[i]] > 0:\n", " counts[guess[i]] -= 1\n", " reply[i] = Yellow\n", " return ''.join(reply)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "An example of the replies you get for the guess `HELLO`, from a few possible target words:" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'HELLO': 'GGGGG',\n", " 'WORLD': '...GY',\n", " 'CELLO': '.GGGG',\n", " 'ALLEY': '.YGY.',\n", " 'HEAVY': 'GG...',\n", " 'HEART': 'GG...',\n", " 'ALLAY': '..GY.',\n", " 'LILAC': '..GY.'}" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "few = 'HELLO WORLD CELLO ALLEY HEAVY HEART ALLAY LILAC'.split()\n", "\n", "{w: reply_for('HELLO', w) for w in few}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We say that a guess **partitions** the word list into **bins**, where each bin is labeled by a reply and has 1 or more words. In the example above, the `'..GY.'` bin has two words: `ALLAY` and `LILAC`. We can get the bin sizes as follows:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Counter({'GGGGG': 1,\n", " '...GY': 1,\n", " '.GGGG': 1,\n", " '.YGY.': 1,\n", " 'GG...': 2,\n", " '..GY.': 2})" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "Counter(reply_for('HELLO', w) for w in few)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here is the function to compute `bin_sizes`, and then functions to answer our questions:" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "@cache\n", "def bin_sizes(guess) -> List[int]: \n", " \"\"\"Sizes of the bins when `words` are partitioned by `guess`.\"\"\"\n", " ctr = Counter(reply_for(guess, target) for target in words)\n", " return list(ctr.values())\n", "\n", "def top(n, items, key=None) -> dict:\n", " \"\"\"Top (best) `n` {item: key(item)} pairs, as ranked by `key`.\"\"\"\n", " return {item: key(item) for item in sorted(items, key=key, reverse=True)[:n]}\n", "\n", "def bot(n, items, key=None) -> dict:\n", " \"\"\"Bottom (worst) `n` {item: key(item)} pairs, as ranked by `key`.\"\"\"\n", " return {item: key(item) for item in sorted(items, key=key)[:n]}\n", "\n", "def wins(guess) -> int: \n", " \"\"\"The number of guaranteed wins on the 2nd guess (after `guess` first).\"\"\"\n", " return bin_sizes(guess).count(1)\n", "\n", "def expected_wins(guess):\n", " \"\"\"The expected number of wins on the 2nd guess (after `guess` first).\n", " With n words in a bin, you have a 1 / n chance of guessing the right one.\"\"\"\n", " return sum(1 / n for n in bin_sizes(guess))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Below we see that `BRUTE` and `CHANT` give the most guaranteed wins (bins of size 1), while `FILET` has the most expected wins (because it has many bins of size 2):" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'BRUTE': 40,\n", " 'CHANT': 40,\n", " 'METRO': 39,\n", " 'SPILT': 39,\n", " 'DINER': 38,\n", " 'HORDE': 38,\n", " 'BARON': 37,\n", " 'BERTH': 37,\n", " 'BURNT': 37,\n", " 'CIDER': 37}" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "top(10, words, wins)" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'FILET': 57.49225359588394,\n", " 'PARSE': 57.158766264952895,\n", " 'DINER': 56.80471418211567,\n", " 'BRUTE': 55.518562422003676,\n", " 'METRO': 55.227427161935054,\n", " 'TRUCE': 55.08315688051648,\n", " 'TRACE': 54.9442057981406,\n", " 'EARTH': 54.66697429572324,\n", " 'TRIED': 54.52412463700236,\n", " 'STALE': 54.34210156658472}" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "top(10, words, expected_wins)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Below we see that `MUMMY` is the worst first guess, by both metrics:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'MUMMY': 5, 'QUEEN': 5, 'QUEER': 5, 'QUEUE': 6, 'KIOSK': 7}" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "bot(5, words, wins)" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'MUMMY': 10.766462361593344,\n", " 'QUEUE': 10.939009001096343,\n", " 'QUEER': 12.173425009634647,\n", " 'QUEEN': 12.2180304500002,\n", " 'JAZZY': 13.38489636915653}" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "bot(5, words, expected_wins)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Winning Strategy \n", "\n", "Christos Papadimitriou came up with a set of 4 words (`ARISE`, `CLOMP`, `THUNK`, `BAWDY`) that allow you to win over 99% of the time if you guess them first, and then guess consistent words. But I wanted to get to 100%. His set uses the letter `A` twice; I decided to:\n", "1. Look for a set of 4 words that have 20 distinct letters (but not any of the rarest letters, `JQXZ`).\n", "2. Check if we can always win in six guesses with that set of words. \n", "3. If not, generate another set and try again.\n", "\n", "First, generating the set of four words:" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "letters = set('ABCDEFGHIKLMNOPRSTUVWY') # Missing JQXZ\n", "\n", "def disjoint_words(n=4, words=words, letters=letters) -> Tuple[Word, ...]:\n", " \"\"\"Tuple of `n` words made of `letters`, with no repeated letters.\"\"\"\n", " if n == 0:\n", " return ()\n", " for w in words:\n", " wletters = set(w)\n", " if wletters.issubset(letters) and len(wletters) == 5:\n", " rest = disjoint_words(n - 1, words, letters - wletters)\n", " if rest is not None:\n", " return (w, *rest)\n", " return None" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "('ABHOR', 'CLEFT', 'DUMPY', 'SWING')" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "guesses = disjoint_words(4)\n", "guesses" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To check if a set of guesses can win, partition the word list into bins based on the replies to all four words, and then for every bin, check that either:\n", "- The bin is 1 or 2 words.\n", "- If we guess any word in the bin (for the 5th guess), we will be left with all bins of size 1 (for the 6th guess)." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "def can_win(guesses, targets=words) -> bool:\n", " \"\"\"Will these initial guesses always lead to a win in 2 more guesses?\"\"\"\n", " return all(len(bin) < 2 or all(can_win_bin(w, bin) for w in bin)\n", " for bin in partition(guesses, targets).values())\n", "\n", "def can_win_bin(guess: Word, bin: List[Word]) -> bool:\n", " \"\"\"If `guess` is the first guess, can we solve the bin by the second guess?\"\"\"\n", " # `bin` is partitioned into bins called `bin5` by `guess`; check each one\n", " return all(len(bin5) == 1 \n", " for bin5 in partition([guess], bin).values())\n", "\n", "def partition(guesses, targets=words) -> Dict[Tuple[Reply, ...], List[Word]]:\n", " \"\"\"Partition `words` into bins of {(reply, ...): [word, ...]}\"\"\"\n", " partition = defaultdict(list)\n", " for target in targets:\n", " replies = tuple(reply_for(guess, target) for guess in guesses)\n", " partition[replies].append(target)\n", " return partition" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here's an example of how `partition` works on the two guesses `HELLO` and `WORLD`:" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "defaultdict(list,\n", " {('GGGGG', '.Y.G.'): ['HELLO'],\n", " ('...GY', 'GGGGG'): ['WORLD'],\n", " ('.GGGG', '.Y.G.'): ['CELLO'],\n", " ('.YGY.', '...Y.'): ['ALLEY'],\n", " ('GG...', '.....'): ['HEAVY'],\n", " ('GG...', '..Y..'): ['HEART'],\n", " ('..GY.', '...Y.'): ['ALLAY', 'LILAC']})" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "partition(('HELLO', 'WORLD'), few)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now can we win with the four guesses given by `disjoint_words`?" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "False" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "can_win(guesses)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Too bad. Why did it fail? I'll generate some output to say why:" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [], "source": [ "def report(guesses):\n", " \"\"\"Print a report on these guesses: do they win or not, and why?\"\"\"\n", " bins = list(partition(guesses).values())\n", " counts = Counter(map(len, bins))\n", " print(f'\\n{guesses} is a {\"winner\" if can_win(guesses) else \"loser\"}')\n", " print(f' bin counts: {dict(counts)}')\n", " print(f' bins with more than 2 words:')\n", " for bin in bins:\n", " if len(bin) > 2:\n", " fails = \", \".join(w for w in bin if not can_win_bin(w, bin))\n", " print(f' {bin} {\"*** \" + fails + \" will not work!\" if fails else \"\"}')\n", " return guesses" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "('ABHOR', 'CLEFT', 'DUMPY', 'SWING') is a loser\n", " bin counts: {1: 2123, 2: 79, 3: 10, 4: 1}\n", " bins with more than 2 words:\n", " ['BELIE', 'BIBLE', 'LIBEL'] \n", " ['BELLE', 'BEVEL', 'BEZEL'] *** BELLE will not work!\n", " ['BOBBY', 'BOOBY', 'BOOZY'] \n", " ['BRAKE', 'BRAVE', 'ZEBRA'] *** ZEBRA will not work!\n", " ['CARVE', 'CRAVE', 'CRAZE'] \n", " ['EAGLE', 'GAVEL', 'LEGAL'] \n", " ['GAUGE', 'GAUZE', 'VAGUE'] *** VAGUE will not work!\n", " ['JAUNT', 'TAUNT', 'VAUNT'] *** JAUNT, TAUNT, VAUNT will not work!\n", " ['PIPER', 'RIPER', 'VIPER'] *** PIPER, RIPER, VIPER will not work!\n", " ['RESIN', 'RINSE', 'RISEN'] \n", " ['SKATE', 'STAKE', 'STATE', 'STAVE'] *** STAKE, STATE, STAVE will not work!\n" ] }, { "data": { "text/plain": [ "('ABHOR', 'CLEFT', 'DUMPY', 'SWING')" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "report(guesses)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can see that, for example, when the remaining bin is `['BELLE', 'BEVEL', 'BEZEL']`, if we guess `BELLE` on the fifth guess, then either `BEVEL` or `BEZEL` remains possible on the 6th (because we haven't tested `V` or `Z` yet). It is true that guessing `BEVEL` fifth would give you a reply that would distinguish between `BELLE` and `BEZEL` on the sixth guess, but it only counts as a winner if you can guess *any* consistent word on the fifth and sixth guesses, without having to strategize about what words remain.\n", "\n", "Since that one wasn't a winner, I will try again. Here I will generate 200 wordsets, shuffling the words before each one so that the resulting wordsets will be different." ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "CPU times: user 46.5 s, sys: 14.4 ms, total: 46.5 s\n", "Wall time: 46.5 s\n" ] } ], "source": [ "def shuffle(items: List) -> List:\n", " \"\"\"Randomly shuffle the list, and return it.\"\"\"\n", " random.shuffle(items)\n", " return items\n", "\n", "random.seed(42)\n", "N = 200\n", "%time wordsets = [disjoint_words(4, shuffle(words)) for _ in range(N)]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "I'll report on the first three of them:" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "('SCARY', 'FIGHT', 'PLUMB', 'WOVEN') is a loser\n", " bin counts: {2: 102, 1: 2089, 4: 1, 3: 6}\n", " bins with more than 2 words:\n", " ['DEBAR', 'BREAK', 'BREAD', 'REBAR'] *** BREAK, REBAR will not work!\n", " ['TAUNT', 'JAUNT', 'DAUNT'] *** TAUNT, JAUNT, DAUNT will not work!\n", " ['BOBBY', 'BOOZY', 'BOOBY'] \n", " ['RUDER', 'QUEER', 'UDDER'] \n", " ['RABID', 'RABBI', 'BRIAR'] *** BRIAR will not work!\n", " ['STATE', 'STAKE', 'SKATE'] \n", " ['CHILD', 'CHILI', 'CHILL'] *** CHILD, CHILI, CHILL will not work!\n", "\n", "('TEACH', 'DUMPY', 'BLOWN', 'FRISK') is a loser\n", " bin counts: {1: 2099, 2: 86, 4: 2, 3: 12}\n", " bins with more than 2 words:\n", " ['AGREE', 'GAZER', 'EAGER', 'RARER'] *** RARER will not work!\n", " ['OTTER', 'OVERT', 'VOTER'] \n", " ['VAGUE', 'GAUGE', 'GAUZE'] *** VAGUE will not work!\n", " ['EAGLE', 'GAVEL', 'VALVE'] \n", " ['ROVER', 'GORGE', 'ROGER'] \n", " ['ANGLE', 'NAVEL', 'ANGEL'] \n", " ['STAVE', 'STATE', 'STAGE'] *** STAVE, STATE, STAGE will not work!\n", " ['GAUNT', 'JAUNT', 'VAUNT'] *** GAUNT, JAUNT, VAUNT will not work!\n", " ['DRIED', 'DRIER', 'DRIVE'] *** DRIVE will not work!\n", " ['VIPER', 'RIPER', 'PIPER'] *** VIPER, RIPER, PIPER will not work!\n", " ['BEZEL', 'BEVEL', 'BELLE'] *** BELLE will not work!\n", " ['PUREE', 'PURGE', 'RUPEE', 'PURER'] \n", " ['EXTRA', 'EATER', 'AVERT'] \n", " ['NERVE', 'GENRE', 'NEVER'] \n", "\n", "('SHREW', 'GLAND', 'MUCKY', 'PIVOT') is a loser\n", " bin counts: {1: 2084, 2: 98, 3: 10, 5: 1}\n", " bins with more than 2 words:\n", " ['BOBBY', 'BOOZY', 'BOOBY'] \n", " ['ALOFT', 'FLOAT', 'BLOAT'] \n", " ['LATTE', 'FETAL', 'TABLE'] \n", " ['ORDER', 'RODEO', 'ODDER'] *** RODEO will not work!\n", " ['JUDGE', 'BUDGE', 'FUDGE'] *** JUDGE, BUDGE, FUDGE will not work!\n", " ['HATCH', 'BATCH', 'CATCH'] *** HATCH, BATCH, CATCH will not work!\n", " ['CLOCK', 'FLOCK', 'BLOCK'] *** CLOCK, FLOCK, BLOCK will not work!\n", " ['TATTY', 'FATTY', 'TAFFY', 'TABBY', 'BATTY'] *** TATTY, FATTY, TAFFY, TABBY, BATTY will not work!\n", " ['JOLLY', 'LOBBY', 'FOLLY'] *** LOBBY will not work!\n", " ['RABBI', 'FRIAR', 'BRIAR'] \n", " ['CLACK', 'BLACK', 'FLACK'] *** CLACK, BLACK, FLACK will not work!\n" ] } ], "source": [ "for ws in wordsets[:3]:\n", " report(ws)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now I'll find all the winners out opf the 200 candidates:" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "CPU times: user 972 ms, sys: 5.41 ms, total: 977 ms\n", "Wall time: 977 ms\n", "\n", "('DUTCH', 'WOVEN', 'SPRIG', 'BALMY') is a winner\n", " bin counts: {1: 2117, 2: 88, 3: 6, 4: 1}\n", " bins with more than 2 words:\n", " ['FINER', 'INFER', 'INNER'] \n", " ['STALE', 'SLATE', 'STEAL'] \n", " ['BOBBY', 'BOOZY', 'BOOBY'] \n", " ['ALOFT', 'FLOAT', 'ATOLL'] \n", " ['STATE', 'STEAK', 'STAKE', 'SKATE'] \n", " ['PLEAT', 'LEAPT', 'PLATE'] \n", " ['GLAZE', 'LEGAL', 'ALGAE'] \n", "\n", "('CAVIL', 'DEBUG', 'NYMPH', 'WORST') is a winner\n", " bin counts: {1: 2133, 2: 80, 3: 6, 4: 1}\n", " bins with more than 2 words:\n", " ['FINER', 'INFER', 'INNER'] \n", " ['ALOFT', 'FLOAT', 'ALLOT'] \n", " ['STATE', 'STEAK', 'STAKE', 'SKATE'] \n", " ['ORDER', 'ERODE', 'ODDER'] \n", " ['RIPER', 'PRIZE', 'PIPER'] \n", " ['FLIER', 'RIFLE', 'FILER'] \n", " ['TATTY', 'FATTY', 'TAFFY'] \n", "\n", "('CRAFT', 'SWING', 'HOVEL', 'DUMPY') is a winner\n", " bin counts: {1: 2151, 2: 73, 3: 6}\n", " bins with more than 2 words:\n", " ['AGREE', 'GAZER', 'EAGER'] \n", " ['BOBBY', 'BOOZY', 'BOOBY'] \n", " ['STATE', 'STAKE', 'SKATE'] \n", " ['BEGIN', 'GENIE', 'BINGE'] \n", " ['PUREE', 'RUPEE', 'PURER'] \n", " ['TATTY', 'TABBY', 'BATTY'] \n" ] } ], "source": [ "%time winners = [ws for ws in wordsets if can_win(ws)]\n", "\n", "for ws in winners:\n", " report(ws)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "It looks like about 1% or 2% of random 4-word sets are winners.\n", "\n", "Below are the two best 4-word sets that I've come up with (in previous runs of this notebook): best in that they only have two 3-word bins. Interestingly, they both can be seen as relating to a baseball game." ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "('BALMY', 'PITCH', 'SWUNG', 'DROVE') is a winner\n", " bin counts: {1: 2135, 2: 87, 3: 2}\n", " bins with more than 2 words:\n", " ['ALOFT', 'FLOAT', 'ATOLL'] \n", " ['STATE', 'STAKE', 'SKATE'] \n" ] }, { "data": { "text/plain": [ "('BALMY', 'PITCH', 'SWUNG', 'DROVE')" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "report(('BALMY', 'PITCH', 'SWUNG', 'DROVE',))" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "('HANDY', 'SWIFT', 'GLOVE', 'CRUMP') is a winner\n", " bin counts: {1: 2151, 2: 79, 3: 2}\n", " bins with more than 2 words:\n", " ['STATE', 'STAKE', 'SKATE'] \n", " ['TATTY', 'TABBY', 'BATTY'] \n" ] }, { "data": { "text/plain": [ "('HANDY', 'SWIFT', 'GLOVE', 'CRUMP')" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "report(('HANDY', 'SWIFT', 'GLOVE', 'CRUMP'))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can compute the mean number of guesses for the `('HANDY', 'SWIFT', 'GLOVE', 'CRUMP')` wordset:" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "5.0315334773218146" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "(1 + 2 + 3 + 4 # could be one of the first 4 guesses\n", " + (2151 - 4) * 5 # 1 word bins: win in 5 guesses\n", " + 79 * (5 + 6) # 2 word bins: get one of them in 5 guesses, one in 6\n", " + 2 * (5 + 6 + 6) # 3 word bins: get one of them in 5 guesses, the others in 6\n", " ) / len(words)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.6" } }, "nbformat": 4, "nbformat_minor": 4 }