diff --git a/ipynb/jotto.ipynb b/ipynb/jotto.ipynb
index eae0737..b795aef 100644
--- a/ipynb/jotto.ipynb
+++ b/ipynb/jotto.ipynb
@@ -10,7 +10,7 @@
"\n",
"[Jotto](https://en.wikipedia.org/wiki/Jotto) is a word game in which a **guesser** tries to guess a secret **target** word, which is chosen from a **word list** of allowable words, in as few guesses as possible. Each guess must be one of the allowable words, and the **reply** to each guess is the number of letters in common between the guess word and the target word, regardless of the positions of the letters. \n",
"\n",
- "Here is an example Jotto game, where I show the guesses, the replies, the number of remaining targets that are **consistent** with all the replies seen so far, and finally the letters that matched (the matches are an aid to you, the reader; they are not known to the guesser). In this game, the guesser gets to the target word, \"wonky\", in 7 guesses. \n",
+ "Here is an example Jotto game, where I show the guesses, the replies, the number of remaining consistent target words, and finally the letters that matched (the matches are an aid to you, the reader; they are not known to the guesser). A **consistent target** is a word that would give the same reply to each guess as the replies actually observed. In this game, the guesser gets to the target word, \"wonky\", in 7 guesses. \n",
"\n",
" Guess 1: \"stoma\" Reply: 1; Consistent targets: 1118 (Matched: \"o\")\n",
" Guess 2: \"bairn\" Reply: 1; Consistent targets: 441 (Matched: \"n\")\n",
@@ -21,13 +21,13 @@
" Guess 7: \"wonky\" Reply: 5; Consistent targets: 1 (Matched: \"wonky\")\n",
"\n",
"\n",
- "There are several variants of the game; here are five key questions and my answers:\n",
+ "There are several variants of Jotto; here are five key questions and my answers:\n",
"\n",
- "- Q: How many letters can each word be? A: **Only five-letter words are allowed**.\n",
- "- Q: Does a guess have to be a word in the word list? A: **Yes.**\n",
- "- Q: What is the reply for a word that has the same letter twice, like the \"s\" in \"stars\"? A: **Only words with no repeated letters are allowed in the word list**.\n",
- "- Q: What if the reply is \"5\", but the guess is not the target? A: **No two words in the word list are allowed to have the same set of five letters**. (For example, only one of the anagrams apers/pares/parse/pears/reaps/spare/spear is allowed.)\n",
- "- Q: Who chooses the target word? A: **Random chance**. Jotto is sometimes a two-person game where the chooser is an adversary, but not here.\n",
+ "- How many letters can each word have? **Five**.\n",
+ "- Does a guess have to be a word in the word list? **Yes.**\n",
+ "- Can a word have repeated letters, like the \"s\" in \"stars\"? **No.** Every word must have 5 different letters.\n",
+ "- What if the reply is \"5\", but the guess is not the target? **Not allowed**. No two words in the word list may have the same set of five letters. (For example, only one of the anagrams apers/pares/parse/pears/reaps/spare/spear is allowed.)\n",
+ "- Who chooses the target word? **Random chance**. Jotto is sometimes a two-person game where the chooser is an adversary, but not here.\n",
"\n",
"# Jotto Preliminaries\n",
"\n",
@@ -45,12 +45,14 @@
"from statistics import mean, median, stdev\n",
"from collections import defaultdict\n",
"from math import log2\n",
- "import random\n",
+ "import random \n",
"import matplotlib.pyplot as plt\n",
"\n",
"Word = str # A word is a lower-case string of five different letters\n",
"Score = int # A score is the number of guesses it took to get the target word\n",
- "Reply = int # A reply is the number of letters in common between guess and target words"
+ "Reply = int # A reply is the number of letters in common between guess and target words\n",
+ "\n",
+ "random.seed(42) # For reproducibility"
]
},
{
@@ -58,7 +60,7 @@
"metadata": {},
"source": [
"We can make a Jotto word list by:\n",
- "- Starting with a list of words.\n",
+ "- Starting with a file containing a list of words.\n",
"- Discarding the ones that don't have 5 distinct letters.\n",
"- Putting the rest into a dict of anagrams keyed by the set of letters.\n",
"- Keeping only one word for each anagram."
@@ -70,6 +72,8 @@
"metadata": {},
"outputs": [],
"source": [
+ "def read_words(filename) -> List[Word]: return open(filename).read().split()\n",
+ "\n",
"def allowable(words) -> List[Word]:\n",
" \"\"\"Build a list of allowable Jotto words from an iterable of words.\"\"\"\n",
" anagrams = {frozenset(w): w for w in words if len(w) == 5 == len(set(w))}\n",
@@ -91,7 +95,7 @@
{
"data": {
"text/plain": [
- "(5756, 2845)"
+ "2845"
]
},
"execution_count": 3,
@@ -102,17 +106,16 @@
"source": [
"! [ -e sgb-words.txt ] || curl -O https://norvig.com/ngrams/sgb-words.txt\n",
" \n",
- "sgb_words = open('sgb-words.txt').read().split()\n",
- "wordlist = allowable(sgb_words)\n",
+ "wordlist = allowable(read_words('sgb-words.txt'))\n",
"\n",
- "len(sgb_words), len(wordlist)"
+ "len(wordlist)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "We see there are 2,845 allowable Jotto words out of the 5,756 words in [sgb-words.txt](sgb-words.txt)."
+ "We see there are 2,845 allowable Jotto words in [sgb-words.txt](sgb-words.txt)."
]
},
{
@@ -125,14 +128,14 @@
"- `guesser`: a `callable` (e.g., a function) that should return the guess to make. The guesser is passed two arguments: \n",
" - The reply to the previous guess.\n",
" - A list of the words that are consistent with all the guesses made so far. \n",
- " (If the guesser wants to keep track of all the guesses made so far, or all the words in the word list, it is welcome to do so.)\n",
+ " (If the guesser wants to keep track of all the guesses made so far, or all the words in the word list, it can.)\n",
"- `target`: The target word. If none is given, the target word is chosen at random from the wordlist.\n",
"- `wordlist`: The list of allowable words.\n",
"- `verbose`: Unless false, print a message for each guess.\n",
"\n",
"Two corner cases: \n",
"1. If the guesser improperly guesses a non-word, the reply is `None`. \n",
- "2. To prevent a poor guesser from creating an infinite loop, the worst score you can get is the number of words in the wordlist."
+ "2. To prevent an infinite loop, the worst score you can get is the number of words in the wordlist."
]
},
{
@@ -144,20 +147,20 @@
"Guesser = Callable[[Reply, List[Word]], Word]\n",
"\n",
"def play(guesser: Guesser, target=None, wordlist=wordlist, verbose=True) -> Score:\n",
- " \"\"\"How many guesses does it take for `guesser` to guess the Jotto target word,\n",
- " which is selected by `chooser` from the words in `wordlist`?\"\"\"\n",
+ " \"\"\"The number of guesses it take for `guesser` to guess the Jotto word,\n",
+ " which is given by `target` or selected from the words in `wordlist`?\"\"\"\n",
" target = target or random.choice(wordlist) # Choose a random target if none was given\n",
" targets = wordlist # The targets that are consistent with all replies\n",
" reply = None # For the first guess, there is no previous reply\n",
- " N = len(wordlist) # After N guesses stop the game and record a score of N\n",
- " for i in range(1, N + 1):\n",
+ " N = len(wordlist)\n",
+ " for turn in range(1, N + 1):\n",
" guess = guesser(reply, targets)\n",
" reply = reply_for(guess, target) if guess in wordlist else None\n",
" targets = [t for t in targets if reply_for(guess, t) == reply]\n",
" if verbose: \n",
- " print(f'Guess {i}: \"{guess}\" Reply: {reply}; Consistent targets: {len(targets)}')\n",
- " if guess == target or i == N: \n",
- " return i\n",
+ " print(f'Guess {turn}: \"{guess}\" Reply: {reply}; Consistent targets: {len(targets)}')\n",
+ " if guess == target or turn == N: \n",
+ " return turn\n",
" \n",
"def reply_for(guess, target) -> Reply: \n",
" \"The number of letters in common between the target and guess\"\n",
@@ -168,9 +171,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "To play a game, we will need a guesser. Here are two simple ones:\n",
- "- `human_guesser` asks a human for `input`.\n",
- "- `random_guesser` guesses one of the remaining consistent targets, picked at random. \n"
+ "To play a game, we will need a guesser. Here are two simple ones:\n"
]
},
{
@@ -179,9 +180,13 @@
"metadata": {},
"outputs": [],
"source": [
- "def human_guesser(reply, targets) -> Word: return input(f'Reply was {reply}. Your guess? ')\n",
+ "def human_guesser(reply, targets) -> Word: \n",
+ " \"\"\"Ask a human to make a guess.\"\"\"\n",
+ " return input(f'Reply was {reply}. Your guess? ')\n",
"\n",
- "def random_guesser(reply, targets) -> Word: return random.choice(targets)"
+ "def random_guesser(reply, targets) -> Word: \n",
+ " \"\"\"Choose a guess at random from the consistent targets.\"\"\"\n",
+ " return random.choice(targets)"
]
},
{
@@ -202,19 +207,20 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "Guess 1: \"tempo\" Reply: 0; Consistent targets: 451\n",
- "Guess 2: \"flick\" Reply: 0; Consistent targets: 67\n",
- "Guess 3: \"hydra\" Reply: 3; Consistent targets: 21\n",
- "Guess 4: \"hands\" Reply: 2; Consistent targets: 7\n",
- "Guess 5: \"bawdy\" Reply: 2; Consistent targets: 4\n",
- "Guess 6: \"guard\" Reply: 3; Consistent targets: 3\n",
- "Guess 7: \"grays\" Reply: 5; Consistent targets: 1\n"
+ "Guess 1: \"strop\" Reply: 3; Consistent targets: 322\n",
+ "Guess 2: \"party\" Reply: 1; Consistent targets: 112\n",
+ "Guess 3: \"obits\" Reply: 3; Consistent targets: 44\n",
+ "Guess 4: \"sloth\" Reply: 2; Consistent targets: 12\n",
+ "Guess 5: \"brows\" Reply: 2; Consistent targets: 5\n",
+ "Guess 6: \"pious\" Reply: 4; Consistent targets: 4\n",
+ "Guess 7: \"pions\" Reply: 4; Consistent targets: 3\n",
+ "Guess 8: \"dipso\" Reply: 5; Consistent targets: 1\n"
]
},
{
"data": {
"text/plain": [
- "7"
+ "8"
]
},
"execution_count": 6,
@@ -235,20 +241,18 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "Guess 1: \"chard\" Reply: 2; Consistent targets: 833\n",
- "Guess 2: \"frank\" Reply: 1; Consistent targets: 377\n",
- "Guess 3: \"lased\" Reply: 3; Consistent targets: 91\n",
- "Guess 4: \"decks\" Reply: 2; Consistent targets: 58\n",
- "Guess 5: \"haves\" Reply: 2; Consistent targets: 44\n",
- "Guess 6: \"mated\" Reply: 4; Consistent targets: 8\n",
- "Guess 7: \"taxed\" Reply: 4; Consistent targets: 3\n",
- "Guess 8: \"adept\" Reply: 5; Consistent targets: 1\n"
+ "Guess 1: \"carpy\" Reply: 0; Consistent targets: 602\n",
+ "Guess 2: \"litho\" Reply: 2; Consistent targets: 249\n",
+ "Guess 3: \"loved\" Reply: 0; Consistent targets: 21\n",
+ "Guess 4: \"fugit\" Reply: 2; Consistent targets: 7\n",
+ "Guess 5: \"skint\" Reply: 1; Consistent targets: 1\n",
+ "Guess 6: \"thumb\" Reply: 5; Consistent targets: 1\n"
]
},
{
"data": {
"text/plain": [
- "8"
+ "6"
]
},
"execution_count": 7,
@@ -269,18 +273,19 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "Guess 1: \"chefs\" Reply: 0; Consistent targets: 420\n",
- "Guess 2: \"junta\" Reply: 0; Consistent targets: 42\n",
- "Guess 3: \"wimpy\" Reply: 1; Consistent targets: 10\n",
- "Guess 4: \"dorky\" Reply: 3; Consistent targets: 3\n",
- "Guess 5: \"glory\" Reply: 3; Consistent targets: 1\n",
- "Guess 6: \"world\" Reply: 5; Consistent targets: 1\n"
+ "Guess 1: \"thous\" Reply: 1; Consistent targets: 1141\n",
+ "Guess 2: \"vocal\" Reply: 2; Consistent targets: 340\n",
+ "Guess 3: \"snack\" Reply: 0; Consistent targets: 46\n",
+ "Guess 4: \"vigor\" Reply: 2; Consistent targets: 19\n",
+ "Guess 5: \"oxlip\" Reply: 2; Consistent targets: 10\n",
+ "Guess 6: \"roble\" Reply: 3; Consistent targets: 4\n",
+ "Guess 7: \"world\" Reply: 5; Consistent targets: 1\n"
]
},
{
"data": {
"text/plain": [
- "6"
+ "7"
]
},
"execution_count": 8,
@@ -296,90 +301,14 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "# Evaluating Guessers\n",
- "\n",
- "To properly evaluate a guesser, a sample of 3 games is not enough. We will have to play at least a few hundred games to get a statistically reliable result. The function `evaluate` takes a list of scores from playing multiple games and reports the following statistics:\n",
- "- The median, mean, standard deviation, and worst case number of guesses, and total number of scores.\n",
- "- The cumulative percentages guessed correctly (e.g., `\"≤5:11%\"` means 11% of the targets took 5 or fewer guesses).\n",
- "- A histogram of scores."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 9,
- "metadata": {},
- "outputs": [],
- "source": [
- "def report(scores: Iterable[Score]) -> None:\n",
- " \"\"\"Report statistics and a histogram for these scores.\"\"\"\n",
- " scores = list(scores)\n",
- " ctr = Counter(scores)\n",
- " bins = range(min(ctr), max(ctr) + 2)\n",
- " scale = 100 / len(scores)\n",
- " weights = [scale * ctr[score] for score in ctr]\n",
- " plt.hist(list(ctr), weights=weights, align='left', rwidth=0.9, bins=bins)\n",
- " plt.xticks(bins[:-1])\n",
- " plt.xlabel('Number of guesses'); plt.ylabel('% of scores')\n",
- " print(f'median: {median(scores):.0f} guesses, mean: {mean(scores):.2f}',\n",
- " f'± {stdev(scores):.2f}, worst: {max(scores)}, scores: {len(scores):,d}')\n",
- " def pct(g): \n",
- " \"\"\"What percent of games requires no mare than g guesses?\"\"\"\n",
- " percent = scale * sum(ctr[i] for i in range(1, g + 1))\n",
- " return round(percent, (1 if 99 < percent < 100 else None))\n",
- " print('cumulative:', ', '.join(f'≤{g}:{pct(g)}%' for g in range(3, 11)))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "I'll evaluate `random_guesser` on every word in the word list:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 10,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "median: 7 guesses, mean: 7.35 ± 1.68, worst: 16, scores: 2,845\n",
- "cumulative: ≤3:1%, ≤4:3%, ≤5:11%, ≤6:31%, ≤7:57%, ≤8:79%, ≤9:91%, ≤10:96%\n"
- ]
- },
- {
- "data": {
- "image/png": "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\n",
- "text/plain": [
- "
"
- ]
- },
- "metadata": {
- "needs_background": "light"
- },
- "output_type": "display_data"
- }
- ],
- "source": [
- "report(play(random_guesser, target, verbose=False) for target in wordlist)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The random consistent guesser strategy might have seemed hopelessly naive, but it is actually a pretty decent strategy. The median is 7 guesses and about 80% of the time it will take 8 guesses or fewer. Can a better strategy improve on that?\n",
- "\n",
- "# Guessers that Partition Target Words\n",
+ "# Partitioning Target Words\n",
"\n",
"A key idea in guessing is to reduce the number of consistent targets. When there is a single consistent target left, the game is over. We can think of a guess as **partitioning** the consistent targets into different **branches** of a tree, each branch corresponding to a different reply:"
]
},
{
"cell_type": "code",
- "execution_count": 11,
+ "execution_count": 9,
"metadata": {},
"outputs": [],
"source": [
@@ -395,86 +324,48 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "Here are two different partitions of just the first 22 words in `wordlist`; one by the guess word `'girth'` and one by `'ethos'`:"
+ "To get a feel for how this works, a 2,845 word list is too much to deal with; let's consider just the first 22 words in `wordlist`. Here are two partitions of those 22 words, one by the guess `'girth'` and one by `'ethos'`:"
]
},
{
"cell_type": "code",
- "execution_count": 12,
+ "execution_count": 10,
"metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "defaultdict(list,\n",
- " {4: ['their', 'might'],\n",
- " 1: ['about', 'sword', 'resay', 'nuder', 'house'],\n",
- " 0: ['would', 'cloud', 'place', 'sound', 'fondu'],\n",
- " 3: ['throe', 'write', 'rifts', 'think', 'grate'],\n",
- " 2: ['water', 'after', 'ethos', 'while'],\n",
- " 5: ['girth']})"
- ]
- },
- "execution_count": 12,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
+ "outputs": [],
"source": [
"words22 = wordlist[:22]\n",
"\n",
- "partition('girth', words22)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 13,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "defaultdict(list,\n",
- " {3: ['their'],\n",
- " 2: ['about',\n",
- " 'sword',\n",
- " 'write',\n",
- " 'rifts',\n",
- " 'water',\n",
- " 'after',\n",
- " 'girth',\n",
- " 'think',\n",
- " 'resay',\n",
- " 'sound',\n",
- " 'grate',\n",
- " 'might',\n",
- " 'while'],\n",
- " 1: ['would', 'cloud', 'place', 'fondu', 'nuder'],\n",
- " 4: ['throe', 'house'],\n",
- " 5: ['ethos']})"
- ]
- },
- "execution_count": 13,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "partition('ethos', words22)"
+ "assert (partition('girth', words22) ==\n",
+ " {0: ['would', 'cloud', 'place', 'sound', 'fondu'],\n",
+ " 1: ['about', 'sword', 'resay', 'nuder', 'house'],\n",
+ " 2: ['water', 'after', 'ethos', 'while'],\n",
+ " 3: ['throe', 'write', 'rifts', 'think', 'grate'],\n",
+ " 4: ['their', 'might'],\n",
+ " 5: ['girth']})\n",
+ "\n",
+ "assert (partition('ethos', words22) ==\n",
+ " {1: ['would', 'cloud', 'place', 'fondu', 'nuder'],\n",
+ " 2: ['about', 'sword', 'write', 'rifts', 'water', 'after', 'girth', 'think', 'resay', \n",
+ " 'sound', 'grate', 'might', 'while'],\n",
+ " 3: ['their'],\n",
+ " 4: ['throe', 'house'],\n",
+ " 5: ['ethos']})"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "We see that after guesssing `'girth'`, no matter what the reply is, we will be left with no more than 5 targets. However, if we guessed `'ethos'` then a majority of the time the reply would be 2, and there would be 13 possible targets remaining. That suggests that `'girth'` is a better guess and that a good strategy is: **guess a word that partitions the possible targets into branches with small numbers of words.**\n",
+ "No matter what the reply to `'girth'` is, we will be left with no more than 5 targets. \n",
+ "\n",
+ "But if we guess `'ethos'` then 13 of the 22 targets would get a reply of 2, and there would be 13 possible targets remaining to deal with. That suggests that `'girth'` is a better guess and that a good strategy is: **guess a word that partitions the possible targets into small branches.**\n",
"\n",
"Since we only need to know the *size* of each branch, not the list of words therein, we can use `partition_counts`:"
]
},
{
"cell_type": "code",
- "execution_count": 14,
+ "execution_count": 11,
"metadata": {},
"outputs": [],
"source": [
@@ -486,7 +377,7 @@
},
{
"cell_type": "code",
- "execution_count": 15,
+ "execution_count": 12,
"metadata": {},
"outputs": [
{
@@ -495,7 +386,7 @@
"[2, 5, 5, 5, 4, 1]"
]
},
- "execution_count": 15,
+ "execution_count": 12,
"metadata": {},
"output_type": "execute_result"
}
@@ -506,7 +397,7 @@
},
{
"cell_type": "code",
- "execution_count": 16,
+ "execution_count": 13,
"metadata": {},
"outputs": [
{
@@ -515,7 +406,7 @@
"[1, 13, 5, 2, 1]"
]
},
- "execution_count": 16,
+ "execution_count": 13,
"metadata": {},
"output_type": "execute_result"
}
@@ -528,23 +419,22 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "# Comparing Partitions\n",
+ "# Metrics for Minimizing Partitions\n",
"\n",
- "What exactly is the best metric for deciding which partition is best? Ideally, we want the partition that minimizes the average number of additional guesses it will take to finish the game, but since we don't know that, we can instead minimize one of the following proxy metrics to rank partitions, based on the sizes of the branches:\n",
+ "We want partitions with **small branches**, but what exactly does that mean? Ideally, we want the partition that minimizes the average number of additional guesses it will take to finish the game. But since we don't know that, we can instead minimize one of the following proxy metrics:\n",
"\n",
"- **Maximum**: choose the partition that minimizes the size of the largest branch.\n",
"\n",
+ "- **Expectation**: In probability theory, the expectation (also known as expected value) is the weighted average of a random variable. Here it means the sum, over all branches, of the probability of ending up in the branch multiplied by the size of the branch. We are assuming that every target is equally likely, so the probability of a branch is proportional to the number of targets in it.\n",
"\n",
- "- **Expectation**: In probability theory the expectation or **expected value** is the weighted average of a random variable. Here it means the sum, over all branches, of the size of the branch multiplied by the probability of ending up in the branch; that's what we want to minimize. We are assuming that every target is equally likely, so the probability of a branch is proportional to the number of targets in it.\n",
- "\n",
- "- **Negative Entropy**: Entropy is an information-theoretic measure that is similar to expectation, except that it weights each branch size by its base 2 logarithm (whereas expectation weights it by its actual size). We want to maximkize entropy, or minimize *negative* entropy.\n",
+ "- **Negative Entropy**: In information theory, entropy is the weighted average amount of \"information\" measured in bits. The calculation is the same as expectation except that we count the base 2 logarithm of the branch sizes, not the branches sizes themselves. You can think of the base 2 logarithm as the number of times that you need to cut a branch in half to get it down to one word. We want to maximize entropy, or minimize *negative* entropy.\n",
"\n",
"The maximum is just the builtin `max` function; here are the other two metrics:"
]
},
{
"cell_type": "code",
- "execution_count": 17,
+ "execution_count": 14,
"metadata": {},
"outputs": [],
"source": [
@@ -565,155 +455,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "# Best and Worst First Guesses\n",
- "\n",
- "Here's a function to print a table of the best and worst words to partition the word list–that is, the best and worst first guesses in a game–according to each of the three metrics:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 18,
- "metadata": {},
- "outputs": [],
- "source": [
- "def partition_scores(n, metrics=(max, expectation, neg_entropy), wordlist=wordlist): \n",
- " \"\"\"The top and bottom `n` words to guess, according to each partition metric.\"\"\"\n",
- " rankings = {metric: sorted((metric(partition_counts(g, wordlist)), g) for g in wordlist)\n",
- " for metric in metrics}\n",
- " def fmt(score): \n",
- " return f'{round(score):5d}' if score >= 100 or isinstance(score, int) else f'{score:5.2f}'\n",
- " bar = ' + '.join(['-' * 11] * len(metrics))\n",
- " print(' | '.join(f'{metric.__name__:11}' for metric in metrics))\n",
- " print(bar)\n",
- " for i in [*range(n), *range(-n, 0)]:\n",
- " fmts = [f'{word} {fmt(score)}' for metric in metrics \n",
- " for score, word in [rankings[metric][i]]]\n",
- " if i == -n: print(bar)\n",
- " print(' | '.join(fmts))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "First I'll try it with the 22-word list:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 19,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "max | expectation | neg_entropy\n",
- "----------- + ----------- + -----------\n",
- "girth 5 | girth 4.36 | girth -2.42\n",
- "after 6 | grate 4.45 | grate -2.41\n",
- "grate 6 | after 4.55 | after -2.39\n",
- "write 6 | write 4.64 | write -2.38\n",
- "might 8 | their 4.91 | their -2.36\n",
- "place 8 | water 4.91 | water -2.36\n",
- "rifts 8 | would 5.27 | would -2.26\n",
- "their 8 | might 5.55 | might -2.21\n",
- "think 8 | think 5.64 | fondu -2.20\n",
- "throe 8 | rifts 5.73 | sound -2.18\n",
- "water 8 | fondu 5.73 | cloud -2.11\n",
- "----------- + ----------- + -----------\n",
- "would 8 | sound 5.82 | think -2.10\n",
- "fondu 9 | throe 5.91 | rifts -2.08\n",
- "house 9 | resay 6.00 | resay -2.06\n",
- "resay 9 | cloud 6.36 | throe -2.04\n",
- "sound 9 | while 6.55 | while -1.96\n",
- "while 9 | sword 6.82 | sword -1.94\n",
- "about 10 | place 6.82 | house -1.77\n",
- "cloud 10 | house 7.64 | place -1.77\n",
- "sword 10 | nuder 8.00 | nuder -1.75\n",
- "nuder 11 | ethos 9.09 | ethos -1.65\n",
- "ethos 13 | about 9.18 | about -1.44\n"
- ]
- }
- ],
- "source": [
- "partition_scores(11, wordlist=words22)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The three metrics are similar; \"girth\" is good and \"ethos\" is bad on all three metrics.\n",
- "\n",
- "Now with the full wordlist (this will take about 12 seconds to run):"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 20,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "max | expectation | neg_entropy\n",
- "----------- + ----------- + -----------\n",
- "wader 1012 | raved 813 | debar -1.95\n",
- "cadre 1026 | debar 818 | alder -1.95\n",
- "armed 1028 | roved 827 | raved -1.94\n",
- "diner 1029 | orbed 827 | dater -1.94\n",
- "coder 1030 | wader 827 | cadre -1.94\n",
- "padre 1035 | armed 827 | armed -1.94\n",
- "raved 1038 | fader 828 | garde -1.94\n",
- "rayed 1038 | dater 829 | wader -1.94\n",
- "delta 1039 | alder 830 | lased -1.93\n",
- "drone 1041 | cadre 830 | padre -1.93\n",
- "eland 1043 | garde 830 | fader -1.93\n",
- "garde 1043 | padre 832 | dears -1.93\n",
- "heard 1044 | deign 832 | drone -1.93\n",
- "tired 1044 | gored 834 | diner -1.93\n",
- "debar 1046 | laved 834 | rayed -1.93\n",
- "fader 1048 | rayed 837 | tired -1.93\n",
- "----------- + ----------- + -----------\n",
- "vacuo 1465 | jumpy 1067 | gauzy -1.61\n",
- "miaow 1474 | gauze 1067 | quake -1.61\n",
- "pique 1480 | juicy 1070 | humpf -1.61\n",
- "okapi 1485 | quail 1073 | coqui -1.61\n",
- "imago 1491 | imago 1081 | whump -1.60\n",
- "haiku 1493 | miaow 1084 | diazo -1.60\n",
- "audio 1494 | quake 1086 | zombi -1.59\n",
- "gauze 1497 | coqui 1091 | okapi -1.59\n",
- "quake 1500 | quota 1098 | azoic -1.59\n",
- "quail 1507 | haiku 1099 | haiku -1.58\n",
- "diazo 1513 | diazo 1100 | mujik -1.58\n",
- "coqui 1535 | okapi 1105 | jumpy -1.57\n",
- "quota 1548 | azoic 1120 | juicy -1.57\n",
- "azoic 1555 | axiom 1157 | axiom -1.56\n",
- "axiom 1615 | audio 1184 | audio -1.49\n",
- "ouija 1848 | ouija 1413 | ouija -1.29\n"
- ]
- }
- ],
- "source": [
- "partition_scores(16)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The top guesses favor the letters \"a\", \"d\", \"e\", and \"r\". \n",
- "\n",
- "The word \"ouija\" is a uniquely terrible guess; mostly what it does is confirm that a majority of the words have exactly one of the vowels \"aiou\"."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Guess Trees: Caching Best Guesses\n",
+ "# Caching Best Guesses: Guess Trees\n",
"\n",
"Going through every word in the wordlist to decide which one makes the best partition takes some time. I would prefer to do that computation just once and cache it, rather than have to repeat the computation in every new game. I will cache the best guesses in a structure called a **guess tree**: a tree that has branches for every possible path the game might take, with the best guess for each situation already computed. A guess tree is either:\n",
"- An **interior node**, which has a guess and a dict of branches, `Node(guess, {reply: subtree, ...})`, where each subtree covers all the target words that are consistent with the corresponding reply.\n",
@@ -722,7 +464,7 @@
},
{
"cell_type": "code",
- "execution_count": 21,
+ "execution_count": 15,
"metadata": {},
"outputs": [],
"source": [
@@ -731,6 +473,8 @@
" \"\"\"A node in a guess tree. It stores the best guess, and a branch for every possible reply.\"\"\"\n",
" guess: Word\n",
" branches: Dict[Reply, 'Tree']\n",
+ " \n",
+ " def __repr__(self) -> str: return f'Node(\"{self.guess}\", {self.branches})'\n",
"\n",
"Tree = Union[Node, Word] # A Tree is either an interior Node or a leaf Word"
]
@@ -744,7 +488,7 @@
},
{
"cell_type": "code",
- "execution_count": 22,
+ "execution_count": 16,
"metadata": {},
"outputs": [],
"source": [
@@ -763,33 +507,43 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "Here is a tree that covers a small list of five words:"
+ "Here is a minimizing tree that covers the 22 words. The tree says that the first guess is `\"girth\"`, and if the reply is `0` the next guess is `\"would\"`. If, say, the reply to `\"would\"` is 4, then there is only one other word left, the leaf word `'cloud'`. If the reply is 5, that means `\"would\"` was correct. I won't go through the other branches of the tree."
]
},
{
"cell_type": "code",
- "execution_count": 23,
+ "execution_count": 17,
"metadata": {},
"outputs": [],
"source": [
- "words5 = ['purge', 'bites', 'sulky', 'patsy', 'hayed']\n",
- "tree5 = minimizing_tree(max, words5)\n",
+ "tree22 = minimizing_tree(max, words22)\n",
"\n",
- "assert tree5 == Node(guess='bites', \n",
- " branches={1: Node(guess='purge', \n",
- " branches={1: Node(guess='sulky', \n",
- " branches={1: 'hayed', \n",
- " 5: 'sulky'}), \n",
- " 5: 'purge'}), \n",
- " 2: 'patsy', \n",
- " 5: 'bites'})"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The tree says that the first guess is `'bites'`, and if the reply is `1` there is a complex subtree to consider (starting with the guess `'purge'`), but if the reply is `2` the target can only be `'patsy'` and of course if the reply is `5` then `'bites'` was the target and the game is over."
+ "assert (tree22 ==\n",
+ " Node(\"girth\", \n",
+ " {0: Node(\"would\", \n",
+ " {1: 'place', \n",
+ " 3: Node(\"sound\", {4: 'fondu', 5: 'sound'}), \n",
+ " 4: 'cloud', \n",
+ " 5: 'would'}), \n",
+ " 1: Node(\"about\", \n",
+ " {1: Node(\"sword\", \n",
+ " {2: Node(\"resay\", {2: 'nuder', 5: 'resay'}), \n",
+ " 5: 'sword'}), \n",
+ " 2: 'house', \n",
+ " 5: 'about'}), \n",
+ " 2: Node(\"after\", \n",
+ " {1: 'while', \n",
+ " 2: 'ethos', \n",
+ " 4: 'water', \n",
+ " 5: 'after'}), \n",
+ " 3: Node(\"throe\", \n",
+ " {2: Node(\"rifts\", {2: 'think', 5: 'rifts'}), \n",
+ " 3: Node(\"write\", {3: 'grate', 5: 'write'}), \n",
+ " 5: 'throe'}), \n",
+ " 4: Node(\"their\", \n",
+ " {3: 'might', \n",
+ " 5: 'their'}), \n",
+ " 5: 'girth'}))"
]
},
{
@@ -800,14 +554,15 @@
"\n",
"A tree is not a guesser, but we can easily make a guesser from a tree. A `TreeGuesser` works as follows:\n",
"- When *initialized*, it takes a tree as input, and stores the tree under the `.root` attribute.\n",
- "- When *called* for the first turn in a game (when the reply is `None`), it resets `.tree` to `.root`.\n",
- "- When *called* on subsequent turns within a game, it updates `.tree` to be the branch corresponding to the reply.\n",
- "- It then returns the guess for that branch: either the `.guess` attribute or the leaf word. "
+ "- When *called*, it sets the `.tree` attribute:\n",
+ " - For the first turn in a game (when the reply is `None`), it resets `.tree` to `.root`.\n",
+ " - On subsequent turns, it updates `.tree` to be the branch corresponding to the reply.\n",
+ "- It then returns the guess for the current tree: either the `.guess` attribute or the leaf word. "
]
},
{
"cell_type": "code",
- "execution_count": 24,
+ "execution_count": 18,
"metadata": {},
"outputs": [],
"source": [
@@ -815,11 +570,11 @@
" \"\"\"Given a guess tree, use it to create a callable Guesser that can play Jotto.\"\"\"\n",
" def __init__(self, tree): self.root = tree\n",
" \n",
- " def __call__(self, reply, targets) -> str:\n",
+ " def __call__(self, reply, targets) -> Word:\n",
" \"\"\"If reply is None, start a new game; otherwise follow the branch for the reply.\n",
" Then return the current leaf or interior node guess.\"\"\"\n",
- " self.tree = self.root if reply is None else self.tree.branches[reply]\n",
- " return self.tree.guess if isinstance(self.tree, Node) else self.tree"
+ " tree = self.tree = self.root if reply is None else self.tree.branches[reply]\n",
+ " return tree.guess if isinstance(tree, Node) else tree"
]
},
{
@@ -831,7 +586,7 @@
},
{
"cell_type": "code",
- "execution_count": 25,
+ "execution_count": 19,
"metadata": {},
"outputs": [],
"source": [
@@ -843,44 +598,12 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "# Sample Jotto Games with the Minimizing Guesser\n"
+ "# Sample Games with the Minimizing Guesser\n"
]
},
{
"cell_type": "code",
- "execution_count": 26,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Guess 1: \"wader\" Reply: 2; Consistent targets: 1004\n",
- "Guess 2: \"lawns\" Reply: 2; Consistent targets: 354\n",
- "Guess 3: \"sutra\" Reply: 2; Consistent targets: 125\n",
- "Guess 4: \"heaps\" Reply: 1; Consistent targets: 40\n",
- "Guess 5: \"carny\" Reply: 3; Consistent targets: 12\n",
- "Guess 6: \"bairn\" Reply: 5; Consistent targets: 1\n"
- ]
- },
- {
- "data": {
- "text/plain": [
- "6"
- ]
- },
- "execution_count": 26,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "play(guesser)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 27,
+ "execution_count": 20,
"metadata": {},
"outputs": [
{
@@ -889,18 +612,19 @@
"text": [
"Guess 1: \"wader\" Reply: 0; Consistent targets: 466\n",
"Guess 2: \"tings\" Reply: 2; Consistent targets: 160\n",
- "Guess 3: \"gunky\" Reply: 2; Consistent targets: 44\n",
- "Guess 4: \"plink\" Reply: 2; Consistent targets: 13\n",
- "Guess 5: \"bonks\" Reply: 5; Consistent targets: 1\n"
+ "Guess 3: \"gunky\" Reply: 1; Consistent targets: 54\n",
+ "Guess 4: \"bouts\" Reply: 2; Consistent targets: 16\n",
+ "Guess 5: \"clogs\" Reply: 0; Consistent targets: 1\n",
+ "Guess 6: \"mufti\" Reply: 5; Consistent targets: 1\n"
]
},
{
"data": {
"text/plain": [
- "5"
+ "6"
]
},
- "execution_count": 27,
+ "execution_count": 20,
"metadata": {},
"output_type": "execute_result"
}
@@ -911,30 +635,29 @@
},
{
"cell_type": "code",
- "execution_count": 28,
+ "execution_count": 21,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
- "Guess 1: \"wader\" Reply: 2; Consistent targets: 1004\n",
- "Guess 2: \"lawns\" Reply: 2; Consistent targets: 354\n",
- "Guess 3: \"sutra\" Reply: 2; Consistent targets: 125\n",
- "Guess 4: \"heaps\" Reply: 2; Consistent targets: 43\n",
- "Guess 5: \"eclat\" Reply: 2; Consistent targets: 13\n",
- "Guess 6: \"hilar\" Reply: 0; Consistent targets: 3\n",
- "Guess 7: \"wefts\" Reply: 4; Consistent targets: 1\n",
- "Guess 8: \"owest\" Reply: 5; Consistent targets: 1\n"
+ "Guess 1: \"wader\" Reply: 1; Consistent targets: 1012\n",
+ "Guess 2: \"actin\" Reply: 2; Consistent targets: 355\n",
+ "Guess 3: \"flats\" Reply: 2; Consistent targets: 113\n",
+ "Guess 4: \"cloak\" Reply: 3; Consistent targets: 17\n",
+ "Guess 5: \"backs\" Reply: 2; Consistent targets: 5\n",
+ "Guess 6: \"clamp\" Reply: 3; Consistent targets: 2\n",
+ "Guess 7: \"plank\" Reply: 5; Consistent targets: 1\n"
]
},
{
"data": {
"text/plain": [
- "8"
+ "7"
]
},
- "execution_count": 28,
+ "execution_count": 21,
"metadata": {},
"output_type": "execute_result"
}
@@ -943,33 +666,41 @@
"play(guesser)"
]
},
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Evaluating Guess Trees\n",
- "\n",
- "We could evaluate a Guess Tree with repeated calls to `play`, but there is a faster way: walk the tree and keep track of how many guesses it takes to get to each leaf word. The function `tree_scores` does this. For each subtree branch in the tree there are three cases:\n",
- "- If the subtree is a leaf word that is the same as the node's guess, we're done; it took one guess.\n",
- "- If the subtree is a leaf word that is not the guess, it took two guesses: one for the incorrect guess and one for the leaf word.\n",
- "- If the subtree is a Node, add one to each of the scores from the subtree and yield those scores."
- ]
- },
{
"cell_type": "code",
- "execution_count": 29,
+ "execution_count": 22,
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Guess 1: \"wader\" Reply: 3; Consistent targets: 319\n",
+ "Guess 2: \"sword\" Reply: 1; Consistent targets: 131\n",
+ "Guess 3: \"paled\" Reply: 4; Consistent targets: 21\n",
+ "Guess 4: \"abled\" Reply: 3; Consistent targets: 8\n",
+ "Guess 5: \"paved\" Reply: 4; Consistent targets: 6\n",
+ "Guess 6: \"caped\" Reply: 4; Consistent targets: 5\n",
+ "Guess 7: \"paged\" Reply: 4; Consistent targets: 4\n",
+ "Guess 8: \"adept\" Reply: 4; Consistent targets: 3\n",
+ "Guess 9: \"paned\" Reply: 4; Consistent targets: 2\n",
+ "Guess 10: \"payed\" Reply: 4; Consistent targets: 1\n",
+ "Guess 11: \"amped\" Reply: 5; Consistent targets: 1\n"
+ ]
+ },
+ {
+ "data": {
+ "text/plain": [
+ "11"
+ ]
+ },
+ "execution_count": 22,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
- "def tree_scores(node: Node) -> Iterable[Score]:\n",
- " \"\"\"All the scores for playing all the target words in the tree under `node`.\"\"\"\n",
- " for subtree in node.branches.values():\n",
- " if isinstance(subtree, Word):\n",
- " yield 1 if subtree == node.guess else 2\n",
- " else:\n",
- " yield from (score + 1 for score in tree_scores(subtree))\n",
- " \n",
- "assert sorted(tree_scores(tree5)) == [1, 2, 2, 3, 4]"
+ "play(guesser)"
]
},
{
@@ -980,12 +711,58 @@
"\n",
"So far, we have always guessed one of the consistent targets. That seems reasonable; why waste a guess on a word that could not possibly be the target? But it turns out that in some cases it ***is*** a good strategy to guess such a word.\n",
"\n",
- "I will redefine `minimizing_tree` so that it is passed both the list of remaining consistent target words and the complete word list. It also takes a flag, `inconsistent`. When this flag is true, any word in the wordlist can be considered as a guess; when false, only consistent targets are considered, as before. (Note: when there are 3 or fewer target words left there is no use considering inconsistent guesses, since they cannot improve the average score over a consistent guess.)"
+ "Consider the branch of `tree22` where the reply to the first guess is 1. There are 5 consistent words remaining, and the max-minimizing tree (which starts by guessing `\"about\"`), can take up to 4 more guesses to find the target:"
]
},
{
"cell_type": "code",
- "execution_count": 30,
+ "execution_count": 23,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "assert (tree22.branches[1] == \n",
+ " Node(\"about\", \n",
+ " {1: Node(\"sword\", \n",
+ " {2: Node(\"resay\", \n",
+ " {2: 'nuder', \n",
+ " 5: 'resay'}), \n",
+ " 5: 'sword'}), \n",
+ " 2: 'house', \n",
+ " 5: 'about'}))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "But now consider this tree, which covers the same five words:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 24,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "tree5 = Node(\"nerdy\", {0: 'about', \n",
+ " 1: 'house', \n",
+ " 2: 'sword', \n",
+ " 3: 'resay', \n",
+ " 4: 'nuder'})"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The first guess, `\"nerdy\"` is inconsistent–it is not one of the five words. This tree sacrifices the 1-in-5 chance of being right on the first guess in order to be assured that the second guess will be correct. So the min, max, mean, and median is 2 guesses. On the whole, this is better than guessing `\"about\"`, which gives a mean of 2.4 and a worst case of 4. \n",
+ "\n",
+ "I will redefine `minimizing_tree` so that it is passed both the list of remaining consistent target words and the complete word list. It also takes a flag, `inconsistent`. When this flag is true, any word in the wordlist can be considered as a guess; when false, only consistent targets are considered, as before. (Note: when there are 3 or fewer target words left there is no use considering inconsistent guesses, since they cannot improve the average score over a consistent guess. Also, when there are many targets, the odds are one of them will be as good as any inconsistent guess. The value of *many* was empirically chosen as `inconsistent_max = 400`.)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 25,
"metadata": {},
"outputs": [],
"source": [
@@ -994,112 +771,123 @@
" if len(targets) == 1:\n",
" return targets[0]\n",
" else:\n",
- " guesses = wordlist if (inconsistent and len(targets) > 3) else targets\n",
+ " guesses = wordlist if (inconsistent and 3 < len(targets) <= inconsistent_max) else targets\n",
" guess = min(guesses, key=lambda guess: metric(partition_counts(guess, targets))) \n",
" branches = partition(guess, targets)\n",
" return Node(guess, {reply: minimizing_tree(metric, branches[reply], wordlist, inconsistent) \n",
- " for reply in sorted(branches)})"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Here we see that by default, the redefined `minimizing_tree` behaves just as it did before:"
+ " for reply in sorted(branches)})\n",
+ " \n",
+ "inconsistent_max = 400"
]
},
{
"cell_type": "code",
- "execution_count": 31,
+ "execution_count": 26,
"metadata": {},
"outputs": [],
"source": [
- "assert minimizing_tree(max, words5) == tree5"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "But with `inconsistent=True`, we get a better tree:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 32,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "Node(guess='dashy', branches={0: 'purge', 1: 'bites', 2: 'sulky', 3: 'patsy', 4: 'hayed'})"
- ]
- },
- "execution_count": 32,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "tree5i = minimizing_tree(max, words5, inconsistent=True)\n",
- "tree5i"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The first guess by `tree5i` is an inconsistent word, `'dashy'`. There is no chance that this is the target, but it sets us up so that after we get the reply we will always be able to guess correctly on the second guess. So the minimum, mean, median, and maximum number of guesses is 2. \n",
+ "words5 = ['about', 'house', 'sword', 'resay', 'nuder']\n",
"\n",
- "In contrast, `tree5` makes only consistent guesses and has a mean of 2.4 guesses and a maximum of 4:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 33,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "median: 2 guesses, mean: 2.40 ± 1.14, worst: 4, scores: 5\n",
- "cumulative: ≤3:80%, ≤4:100%, ≤5:100%, ≤6:100%, ≤7:100%, ≤8:100%, ≤9:100%, ≤10:100%\n"
- ]
- },
- {
- "data": {
- "image/png": "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\n",
- "text/plain": [
- "
"
- ]
- },
- "metadata": {
- "needs_background": "light"
- },
- "output_type": "display_data"
- }
- ],
- "source": [
- "report(play(TreeGuesser(tree5), w, words5, False) for w in words5)"
+ "assert minimizing_tree(max, words5, inconsistent=True) == tree5"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "As a more extreme example, consider the following list of 11 words, which differ only in the first letter. A consistent guesser could do no better than to just go through the list one at a time (in any order), and would be equally likely to guess right on any of guesses 1 through 11, for an average of 6:"
+ "# Evaluating and Reporting on Guessers\n",
+ "\n",
+ "To properly evaluate a guesser, a sample of 3 games is not enough to be statistically reliable. I'll introduce three functions that together will evaluate a guesser and produce a report:\n",
+ "\n",
+ "- `report_minimizing_tree` builds a minimizing tree, gets its tree scores, and calls `report`.\n",
+ "- `report` takes a list of scores and reports the following statistics:\n",
+ " - The median, mean, standard deviation, and worst case number of guesses, and total number of scores.\n",
+ " - The cumulative percentages guessed correctly (e.g., `\"≤5:11%\"` means 11% of the targets took 5 or fewer guesses).\n",
+ " - A histogram of scores.\n",
+ "- `tree_scores` takes a guesser and returns a list of *all* the scores it would make for *all* its targets. For each subtree branch in the tree there are three cases:\n",
+ " - If the subtree is a leaf word that is the same as the node's guess, we're done; it took one guess.\n",
+ " - If the subtree is a leaf word that is not the guess, it took two guesses: one for the incorrect guess and one for the leaf word.\n",
+ " - If the subtree is a Node, add one to each of the scores from the subtree and yield those scores."
]
},
{
"cell_type": "code",
- "execution_count": 34,
+ "execution_count": 27,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def report_minimizing_tree(metric, targets=wordlist, wordlist=wordlist, inconsistent=False) -> Tree:\n",
+ " \"\"\"Build a minimizing tree and report on its scores.\"\"\"\n",
+ " print(f\"minimizing the {metric.__name__} of partition sizes over\",\n",
+ " f\"{len(targets):,d} targets in a {len(wordlist):,d} word list,\")\n",
+ " print(f\"{'' if inconsistent else 'not '}including inconsistent words.\")\n",
+ " tree = minimizing_tree(metric, targets, wordlist, inconsistent)\n",
+ " print(f'first guess: \"{tree.guess}\"')\n",
+ " report(tree_scores(tree))\n",
+ " return tree\n",
+ "\n",
+ "def report(scores: Iterable[Score], label='') -> None:\n",
+ " \"\"\"Report statistics and a histogram for these scores.\"\"\"\n",
+ " scores = list(scores)\n",
+ " ctr = Counter(scores)\n",
+ " bins = range(min(ctr), max(ctr) + 2)\n",
+ " scale = 100 / len(scores)\n",
+ " weights = [scale * ctr[score] for score in ctr]\n",
+ " plt.hist(list(ctr), weights=weights, align='left', rwidth=0.9, bins=bins)\n",
+ " plt.xticks(bins[:-1])\n",
+ " plt.xlabel('Number of guesses'); plt.ylabel('% of scores')\n",
+ " def cumulative_pct(g) -> str: \n",
+ " \"\"\"What percent of games requires no more than g guesses?\"\"\"\n",
+ " percent = scale * sum(ctr[i] for i in range(1, g + 1))\n",
+ " return f'≤{g}:{percent:.{1 if 99 < percent < 100 else 0}f}%'\n",
+ " print(f'median: {median(scores):.0f} guesses, mean: {mean(scores):.2f}',\n",
+ " f'± {stdev(scores):.2f}, worst: {max(scores)}, scores: {len(scores):,d}\\n'\n",
+ " 'cumulative:', ', '.join(map(cumulative_pct, range(3, 11))))\n",
+ " \n",
+ "def tree_scores(node: Node) -> Iterable[Score]:\n",
+ " \"\"\"All the scores for playing all the target words in the tree under `node`.\"\"\"\n",
+ " for subtree in node.branches.values():\n",
+ " if isinstance(subtree, Word):\n",
+ " yield 1 if subtree == node.guess else 2\n",
+ " else:\n",
+ " yield from (score + 1 for score in tree_scores(subtree))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "To test these functions, and to give another example of inconsistent guessing, consider this list of 11 words:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 28,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "ails = 'bails fails hails jails mails nails pails rails tails vails wails'.split()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "A consistent guesser could guess them in any order, but wouldn't gain much information from the replies–every reply is either a 5 (which ends the game) or a 4, which leaves you with all the remaining words. Here is the report:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 29,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
+ "minimizing the max of partition sizes over 11 targets in a 2,845 word list,\n",
+ "not including inconsistent words.\n",
+ "first guess: \"bails\"\n",
"median: 6 guesses, mean: 6.00 ± 3.32, worst: 11, scores: 11\n",
"cumulative: ≤3:27%, ≤4:36%, ≤5:45%, ≤6:55%, ≤7:64%, ≤8:73%, ≤9:82%, ≤10:91%\n"
]
@@ -1118,27 +906,28 @@
}
],
"source": [
- "ails = 'bails fails hails jails mails nails pails rails tails vails wails'.split()\n",
- "\n",
- "report(tree_scores(minimizing_tree(max, ails)))"
+ "t = report_minimizing_tree(max, ails)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "But an inconsistent guesser can make a first guess that partitions the remaining words, eventually leading to an average of only 4 guesses: "
+ "But an inconsistent guesser can make a guesses that more evenly partition the remaining words, giving an average of only 4 guesses, and a worst case of 5 guesses: "
]
},
{
"cell_type": "code",
- "execution_count": 35,
+ "execution_count": 30,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
+ "minimizing the max of partition sizes over 11 targets in a 2,845 word list,\n",
+ "including inconsistent words.\n",
+ "first guess: \"front\"\n",
"median: 4 guesses, mean: 4.00 ± 0.77, worst: 5, scores: 11\n",
"cumulative: ≤3:27%, ≤4:73%, ≤5:100%, ≤6:100%, ≤7:100%, ≤8:100%, ≤9:100%, ≤10:100%\n"
]
@@ -1157,61 +946,34 @@
}
],
"source": [
- "report(tree_scores(minimizing_tree(max, ails, inconsistent=True)))"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 36,
- "metadata": {},
- "outputs": [],
- "source": [
- "assert (minimizing_tree(max, ails, inconsistent=True) == \n",
- " Node(guess='front', \n",
- " branches={0: Node(guess='thumb', \n",
- " branches={0: Node(guess='power', \n",
- " branches={0: Node(guess='jails', \n",
- " branches={4: 'vails', \n",
- " 5: 'jails'}), \n",
- " 1: Node(guess='pails', \n",
- " branches={4: 'wails', \n",
- " 5: 'pails'})}), \n",
- " 1: Node(guess='bails', \n",
- " branches={4: Node(guess='hails', \n",
- " branches={4: 'mails', \n",
- " 5: 'hails'}), \n",
- " 5: 'bails'})}), \n",
- " 1: Node(guess='their', \n",
- " branches={1: Node(guess='fails', \n",
- " branches={4: 'nails', \n",
- " 5: 'fails'}), \n",
- " 2: Node(guess='rails', \n",
- " branches={4: 'tails', \n",
- " 5: 'rails'})})}))"
+ "t = report_minimizing_tree(max, ails, inconsistent=True)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "# Evaluating Consistent Guessers\n",
+ "# Reports on Consistent Guessers\n",
"\n",
- "Here are the evaluations of trees made from minimizing the three metrics, with only consistent guesses allowed:"
+ "Here are reports on trees made from minimizing the three metrics, with only consistent guesses allowed: "
]
},
{
"cell_type": "code",
- "execution_count": 37,
+ "execution_count": 31,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
+ "minimizing the max of partition sizes over 2,845 targets in a 2,845 word list,\n",
+ "not including inconsistent words.\n",
+ "first guess: \"wader\"\n",
"median: 7 guesses, mean: 7.15 ± 1.81, worst: 18, scores: 2,845\n",
"cumulative: ≤3:1%, ≤4:4%, ≤5:13%, ≤6:35%, ≤7:67%, ≤8:86%, ≤9:92%, ≤10:95%\n",
- "CPU times: user 6.71 s, sys: 17.1 ms, total: 6.73 s\n",
- "Wall time: 6.76 s\n"
+ "CPU times: user 6.73 s, sys: 11.3 ms, total: 6.74 s\n",
+ "Wall time: 6.75 s\n"
]
},
{
@@ -1228,22 +990,25 @@
}
],
"source": [
- "%time report(tree_scores(minimizing_tree(max, wordlist, inconsistent=False)))"
+ "%time t = report_minimizing_tree(max, inconsistent=False)"
]
},
{
"cell_type": "code",
- "execution_count": 38,
+ "execution_count": 32,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
+ "minimizing the expectation of partition sizes over 2,845 targets in a 2,845 word list,\n",
+ "not including inconsistent words.\n",
+ "first guess: \"raved\"\n",
"median: 7 guesses, mean: 7.14 ± 1.82, worst: 17, scores: 2,845\n",
"cumulative: ≤3:1%, ≤4:4%, ≤5:13%, ≤6:36%, ≤7:68%, ≤8:85%, ≤9:91%, ≤10:95%\n",
- "CPU times: user 6.75 s, sys: 25.6 ms, total: 6.78 s\n",
- "Wall time: 6.89 s\n"
+ "CPU times: user 6.43 s, sys: 5.51 ms, total: 6.44 s\n",
+ "Wall time: 6.44 s\n"
]
},
{
@@ -1260,22 +1025,25 @@
}
],
"source": [
- "%time report(tree_scores(minimizing_tree(expectation, wordlist, inconsistent=False)))"
+ "%time t = report_minimizing_tree(expectation, inconsistent=False)"
]
},
{
"cell_type": "code",
- "execution_count": 39,
+ "execution_count": 33,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
+ "minimizing the neg_entropy of partition sizes over 2,845 targets in a 2,845 word list,\n",
+ "not including inconsistent words.\n",
+ "first guess: \"debar\"\n",
"median: 7 guesses, mean: 7.09 ± 1.78, worst: 19, scores: 2,845\n",
"cumulative: ≤3:1%, ≤4:4%, ≤5:13%, ≤6:36%, ≤7:69%, ≤8:86%, ≤9:92%, ≤10:96%\n",
- "CPU times: user 6.68 s, sys: 18.8 ms, total: 6.7 s\n",
- "Wall time: 6.73 s\n"
+ "CPU times: user 6.45 s, sys: 6.68 ms, total: 6.46 s\n",
+ "Wall time: 6.46 s\n"
]
},
{
@@ -1292,34 +1060,34 @@
}
],
"source": [
- "%time report(tree_scores(minimizing_tree(neg_entropy, wordlist, inconsistent=False)))"
+ "%time t = report_minimizing_tree(neg_entropy, inconsistent=False)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "We might as well also get an evaluation of the random guesser on every target in the wordlist:"
+ "The random guesser can also be classified as a consistent guesser. Here is a report on it:|"
]
},
{
"cell_type": "code",
- "execution_count": 40,
+ "execution_count": 34,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
- "median: 7 guesses, mean: 7.34 ± 1.73, worst: 18, scores: 2,845\n",
- "cumulative: ≤3:1%, ≤4:4%, ≤5:12%, ≤6:29%, ≤7:57%, ≤8:80%, ≤9:91%, ≤10:96%\n",
- "CPU times: user 6.9 s, sys: 26.7 ms, total: 6.92 s\n",
- "Wall time: 6.95 s\n"
+ "median: 7 guesses, mean: 7.42 ± 1.71, worst: 17, scores: 2,845\n",
+ "cumulative: ≤3:1%, ≤4:3%, ≤5:11%, ≤6:28%, ≤7:55%, ≤8:77%, ≤9:91%, ≤10:96%\n",
+ "CPU times: user 6.57 s, sys: 9.57 ms, total: 6.58 s\n",
+ "Wall time: 6.58 s\n"
]
},
{
"data": {
- "image/png": "iVBORw0KGgoAAAANSUhEUgAAAX4AAAEGCAYAAABiq/5QAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjMsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+AADFEAAAXJklEQVR4nO3dfZRkdX3n8fdHGJUnBWUgCGTHEHRNXIMwIgY1CuoquDyoGFkxJOLBJxQwJhl118UYzwKKGt0EDwpoAqIkgAK6AkGIMauDM4SHIQMxwUHBgWnEiIoiD9/9496JTTPdXVXdt3qG+36dU6eqbtX93W91V3/q9q/u/f1SVUiS+uNRC12AJGm8DH5J6hmDX5J6xuCXpJ4x+CWpZzZf6AIGsf3229eSJUsWugxJ2qSsXLnyzqpaPHX5JhH8S5YsYcWKFQtdhiRtUpLcsqHldvVIUs8Y/JLUMwa/JPWMwS9JPWPwS1LPGPyS1DMGvyT1jMEvST1j8EtSz2wSZ+7qkWfJsi+NvO6aEw+cx0qk/nGPX5J6xuCXpJ4x+CWpZwx+SeoZg1+Sesbgl6SeMfglqWcMfknqGYNfknrG4JeknjH4JalnDH5J6hmDX5J6xuCXpJ4x+CWpZwx+SeqZzoI/ya5JrkiyOskNSY5tl5+Q5LYk17SXA7qqQZL0cF3OwHU/8IdVdXWSbYCVSS5rH/tIVX2ow21LkqbRWfBX1VpgbXv7x0lWAzt3tT1J0mDG0sefZAnwTGB5u+iYJNclOSPJdtOsc3SSFUlWTExMjKNMSeqFzoM/ydbAecBxVXU3cCqwG7AHzX8Ep2xovao6raqWVtXSxYsXd12mJPVGp8GfZBFN6J9dVecDVNUdVfVAVT0IfBLYu8saJEkP1eVRPQFOB1ZX1YcnLd9p0tMOBVZ1VYMk6eG6PKpnX+B1wPVJrmmXvRs4PMkeQAFrgDd2WIMkaYouj+r5OpANPPTlrrYpSZqdZ+5KUs8Y/JLUMwa/JPWMwS9JPWPwS1LPGPyS1DMGvyT1TJcncEmdWrLsSyOvu+bEA+exEmnT4h6/JPWMwS9JPWPwS1LPGPyS1DMGvyT1jMEvST1j8EtSzxj8ktQzBr8k9YzBL0k9Y/BLUs8Y/JLUMwa/JPWMwS9JPWPwS1LPGPyS1DMGvyT1jMEvST1j8EtSzxj8ktQznQV/kl2TXJFkdZIbkhzbLn9CksuSfLu93q6rGiRJD9flHv/9wB9W1dOAfYC3JvkNYBlweVXtDlze3pckjUlnwV9Va6vq6vb2j4HVwM7AwcBn2qd9BjikqxokSQ83lj7+JEuAZwLLgR2rai00Hw7ADtOsc3SSFUlWTExMjKNMSeqFzoM/ydbAecBxVXX3oOtV1WlVtbSqli5evLi7AiWpZzoN/iSLaEL/7Ko6v118R5Kd2sd3AtZ1WYMk6aG6PKonwOnA6qr68KSHLgSObG8fCXyxqxokSQ+3eYdt7wu8Drg+yTXtsncDJwLnJjkK+C5wWIc1SJKm6Cz4q+rrQKZ5eP+utitJmpln7kpSzxj8ktQzBr8k9YzBL0k9Y/BLUs8Y/JLUMwa/JPWMwS9JPWPwS1LPzBr8SU5O8rgki5JcnuTOJEeMozhJ0vwbZI//Je1wyi8HbgWeAvxRp1VJkjozSPAvaq8PAM6pqrs6rEeS1LFBBmm7KMmNwM+AtyRZDPy827IkSV2ZNfiralmSk4C7q+qBJPfQzJurnlqy7Esjr7vmxAPnsRJJoxjky90tgbcCp7aLngQs7bIoSVJ3BunjPxP4BfDb7f1bgT/rrCJJUqcGCf7dqupk4D6AqvoZ00+wIknayA0S/L9IsgVQAEl2A+7ttCpJUmcGOarnfwFfAXZNcjbNXLq/32VRkqTuzBj8SQLcCLwC2Iemi+fYqrpzDLVJkjowY/BXVSX5QlXtBYx+DJ8kaaMxSB//N5M8q/NKJEljMUgf/wuBNya5BfgpTXdPVdUzOq1MktSJQYL/ZZ1XIUkam1m7eqrqFmBb4L+1l23bZZKkTdAgQzYcC5wN7NBezkrytq4LkyR1Y5CunqOAZ1fVTwHaAdu+AXy8y8IkSd0Y5KieAA9Muv8ADtkgSZusQfb4zwSWJ7mgvX8IcHp3JUmSujTIl7sfBv4AuAv4IfAHVfXR2dZLckaSdUlWTVp2QpLbklzTXg6YS/GSpOHNusefZB/ghqq6ur2/TZJnV9XyWVb9NPB/gL+asvwjVfWhUYqVJM3dIH38pwI/mXT/p/xyUpZpVdXXaP5LkCRtRAb6creqav2dqnqQwb4bmM4xSa5ru4K2m3ajydFJViRZMTExMYfNSZImGyT4b07y9iSL2suxwM0jbu9UYDdgD2AtcMp0T6yq06pqaVUtXbx48YibkyRNNUjwv4lm2sXbaKZdfDZw9Cgbq6o7quqB9r+GTwJ7j9KOJGl0s3bZVNU64DXzsbEkO1XV2vbuocCqmZ4vSZp/gwzZcHKSx7XdPJcnuTPJEQOsdw7NGb5PTXJrkqOAk5Ncn+Q6mlE/j5/zK5AkDWWQL2lfUlV/nORQmq6ew4ArgLNmWqmqDt/AYk/8kqQFNkgf/6L2+gDgnKryEE1J2oQNssd/UZIbgZ8Bb0myGPh5t2VJkroyyJANy4DnAEur6j7gHuDgrguTJHVjoBOxquqHk27/lObsXUnSJmiQPn5J0iPItMGfZN/2+jHjK0eS1LWZ9vg/1l5/YxyFSJLGY6Y+/vuSnAnsnORjUx+sqrd3V5YkqSszBf/LgRcB+wErx1OOJKlr0wZ/Vd0JfC7J6qq6dow1SZI6NMhRPT9IckE7jeIdSc5LskvnlUmSOjFI8J8JXAg8CdgZuKhdJknaBA0S/DtU1ZlVdX97+TTgzCiStIkaJPgnkhyRZLP2cgTwg64LkyR1Y5Dgfz3wauB2mukSX9UukyRtggaZgeu7wEFjqEWSNAaO1SNJPWPwS1LPGPyS1DMDB3+SfZJ8Nck/Jjmky6IkSd2Z9svdJL9SVbdPWvQOmi95A/w/4Asd1yZJ6sBMR/V8IslK4INV9XPg34H/DjwI3D2O4iRJ82/arp6qOgS4Brg4yeuA42hCf0vArh5J2kTN2MdfVRcB/xXYFjgfuKmqPlZVE+MoTpI0/2aaevGgJF8HvgqsAl4DHJrknCS7jatASdL8mqmP/8+A5wBbAF+uqr2BdyTZHfgAzQeBJGkTM1Pw/4gm3LcA1q1fWFXfxtCXpE3WTMF/KHA4cB/N0TzSI86SZV8aed01Jx44j5VI4zPb1IsfH7XhJGfQzNu7rqqe3i57AvB5YAmwBnh1Vf1w1G1IkobX5ZANnwZeOmXZMuDyqtoduLy9L0kao86Cv6q+Btw1ZfHBwGfa25/B8wEkaezGPUjbjlW1FqC93mG6JyY5OsmKJCsmJjxtQJLmy0Y7OmdVnVZVS6tq6eLFTvErSfNl3MF/R5KdANrrdbM8X5I0z8Yd/BcCR7a3jwS+OObtS1LvdRb8Sc4BvgE8NcmtSY4CTgRenOTbwIvb+5KkMZp1svVRVdXh0zy0f1fblCTNbqP9cleS1A2DX5J6xuCXpJ4x+CWpZwx+SeoZg1+Sesbgl6SeMfglqWcMfknqGYNfknrG4JeknulsrB5tnJxcXJJ7/JLUMwa/JPWMwS9JPWPwS1LPGPyS1DMGvyT1jMEvST1j8EtSzxj8ktQzBr8k9YzBL0k9Y/BLUs8Y/JLUMwa/JPWMwS9JPWPwS1LPLMhELEnWAD8GHgDur6qlC1GHJPXRQs7A9cKqunMBty9JveTUi9IczWU6S3BKS43fQvXxF3BpkpVJjt7QE5IcnWRFkhUTExNjLk+SHrkWKvj3rao9gZcBb03y/KlPqKrTqmppVS1dvHjx+CuUpEeoBQn+qvp+e70OuADYeyHqkKQ+GnvwJ9kqyTbrbwMvAVaNuw5J6quF+HJ3R+CCJOu3/9mq+soC1CFJvTT24K+qm4HfGvd2JUkNz9yVpJ4x+CWpZwx+SeoZg1+Sesbgl6SeMfglqWcMfknqGYNfknrG4JeknjH4JalnDH5J6hmDX5J6xuCXpJ5xzt1NyFzmdnVeV0nruccvST3jHr+0EZjLf3Pgf3Qajnv8ktQzBr8k9YzBL0k9Y/BLUs8Y/JLUMwa/JPWMwS9JPWPwS1LPeALXmDjcgrrmSWAalHv8ktQzBr8k9YzBL0k9Y/BLUs8syJe7SV4K/DmwGfCpqjpxIeqQ9FB+QdwPYw/+JJsBfwG8GLgV+FaSC6vqn8ddy6A8IkcanB8eG7+F2OPfG/jXqroZIMnngIOBjTb4JY2XHx7dSlWNd4PJq4CXVtUb2vuvA55dVcdMed7RwNHt3acCN3VU0vbAnbZhG7ZhG5twG9P5T1W1eOrChdjjzwaWPezTp6pOA07rvJhkRVUttQ3bsA3b2FTbGNZCHNVzK7DrpPu7AN9fgDokqZcWIvi/Beye5MlJHg28BrhwAeqQpF4ae1dPVd2f5BjgEprDOc+oqhvGXcck89GdZBu2YRu2sZBtDGXsX+5KkhaWZ+5KUs8Y/JLUM70N/iRnJFmXZNWI6++a5Iokq5PckOTYEdp4bJKrklzbtvG+UWpp29osyT8luXjE9dckuT7JNUlWjNjGtkn+NsmN7c/lOUOu/9R2++svdyc5boQ6jm9/nquSnJPksSO0cWy7/g3D1LCh91WSJyS5LMm32+vtRmjjsLaWB5PMeujfNG18sP3dXJfkgiTbjtDG+9v1r0lyaZInDdvGpMfemaSSbD9CHSckuW3Se+WAUepI8rYkN7U/25NHqOPzk2pYk+SaEdrYI8k31//tJdl7pjbmRVX18gI8H9gTWDXi+jsBe7a3twH+BfiNIdsIsHV7exGwHNhnxHreAXwWuHjE9dcA28/xZ/oZ4A3t7UcD286hrc2A22lOQBlmvZ2B7wBbtPfPBX5/yDaeDqwCtqQ5AOLvgN1HfV8BJwPL2tvLgJNGaONpNCcyXgksHbGOlwCbt7dPGrGOx026/XbgE8O20S7fleYAj1tme99NU8cJwDuH+J1uqI0Xtr/bx7T3dxjltUx6/BTgvSPUcSnwsvb2AcCVw7xfR7n0do+/qr4G3DWH9ddW1dXt7R8Dq2lCZ5g2qqp+0t5d1F6G/rY9yS7AgcCnhl13viR5HM2b+nSAqvpFVf37HJrcH/i3qrplhHU3B7ZIsjlNeA97nsjTgG9W1T1VdT/w98Chg6w4zfvqYJoPRdrrQ4Zto6pWV9XAZ69P08al7esB+CbNOTTDtnH3pLtbMcv7dYa/s48Afzzb+rO0MbBp2ngzcGJV3ds+Z92odSQJ8GrgnBHaKOBx7e3HM4bzmnob/PMpyRLgmTR77MOuu1n77+E64LKqGroN4KM0f0QPjrDuegVcmmRlmuEyhvVrwARwZtvl9KkkW82hntcwyx/RhlTVbcCHgO8Ca4EfVdWlQzazCnh+kicm2ZJmL2zXWdaZyY5Vtbatby2wwxzami+vB/7vKCsm+UCS7wGvBd47wvoHAbdV1bWjbH+SY9pupzNm6z6bxlOA5yVZnuTvkzxrDrU8D7ijqr49wrrHAR9sf6YfAt41hzoGYvDPUZKtgfOA46bsDQ2kqh6oqj1o9r72TvL0Ibf/cmBdVa0cdttT7FtVewIvA96a5PlDrr85zb+wp1bVM4Gf0nRrDC3NiX0HAX8zwrrb0exhPxl4ErBVkiOGaaOqVtN0hVwGfAW4Frh/xpU2IUneQ/N6zh5l/ap6T1Xt2q5/zGzPn7LtLYH3MMIHxhSnArsBe9B8wJ8yQhubA9sB+wB/BJzb7rmP4nBG2FFpvRk4vv2ZHk/7X3OXDP45SLKIJvTPrqrz59JW2y1yJfDSIVfdFzgoyRrgc8B+Sc4aYfvfb6/XARfQjKI6jFuBWyf9x/K3NB8Eo3gZcHVV3THCui8CvlNVE1V1H3A+8NvDNlJVp1fVnlX1fJp/zUfZk1vvjiQ7AbTXM3YpdCnJkcDLgddW26k8B58FXjnkOrvRfChf275ndwGuTvIrwzRSVXe0O00PAp9k+PcrNO/Z89su16to/mOe8YvmDWm7FF8BfH6EGgCOpHmfQrOz0/mXuwb/iNo9g9OB1VX14RHbWLz+yIokW9CE1o3DtFFV76qqXapqCU33yFeraqg93CRbJdlm/W2aLwGHOtqpqm4Hvpfkqe2i/Rl9qO257D19F9gnyZbt72h/mu9fhpJkh/b6V2n+qEetB5ohSY5sbx8JfHEObY0szQRIfwIcVFX3jNjG7pPuHsTw79frq2qHqlrSvmdvpTlI4vYh69hp0t1DGfL92voCsF/b3lNoDkgYZZTMFwE3VtWtI6wLTZ/+77S392NuOxmD6frb4431QvOHvBa4j+bNd9SQ6z+Xpl/8OuCa9nLAkG08A/into1VzHJEwADtvYARjuqh6Z+/tr3cALxnxO3vAaxoX88XgO1GaGNL4AfA4+fwc3gfTSCtAv6a9qiNIdv4B5oPrmuB/efyvgKeCFxO8wd9OfCEEdo4tL19L3AHcMkIbfwr8L1J79fZjsjZUBvntT/X64CLgJ2HbWPK42uY/aieDdXx18D1bR0XAjuN0MajgbPa13M1sN8orwX4NPCmObw/ngusbN9ry4G9Rn3vD3pxyAZJ6hm7eiSpZwx+SeoZg1+Sesbgl6SeMfglqWcMfi24doTGUybdf2eSE+ap7U8nedV8tDXLdg5LMyLpFV1vS5org18bg3uBV8w2PO+4JdlsiKcfBbylql7YVT3SfDH4tTG4n2be0eOnPjB1jz3JT9rrF7QDa52b5F+SnJjktWnmN7g+yW6TmnlRkn9on/fydv3N0oxP/612oK83Tmr3iiSfpTlBaGo9h7ftr0pyUrvsvTQn4XwiyQenPP9RSf6yHe/94iRfXv962vHbt29vL01yZXt7q3bgsW+1A94d3C7/zfb1XdPWvHv73C+lmdNhVZLfbZ+7V/vzWZnkkklDRrw9yT+3639uhN+VHgHGPtm6NI2/AK7LLJNhTPFbNEMo3wXcDHyqqvZOMynO22hGPQRYQnNK/G7AFUl+Hfg9mpE7n5XkMcA/Jlk/iufewNOr6juTN5Zm0pGTgL2AH9KMZnpIVf1pkv1oxoefOonNK9rt/xeaUTlXA2fM8rreQzP0xuvbIT2uSvJ3wJuAP6+qs9uB7DajGTn0+1V1YFvj49sxpD4OHFxVE+2HwQdoRuRcBjy5qu7NLBOx6JHLPX5tFKoZ2fSvaCb3GNS3qpkX4V7g32gmtIBmT33JpOedW1UPVjNk7s3Af6YZj+j30gyJvZxmWIX149BcNTX0W8+imSRjoppx7c+mmYNgJs8F/qbd/u3AIN8BvARY1tZ2JfBY4FeBbwDvTvInNBPU/Kx9rS9KclKS51XVj2gmbHk6cFnbxv/gl2PvXwecnWbE0kfMiKMajnv82ph8lGbMlDMnLbufdgelHXTt0ZMeu3fS7Qcn3X+Qh763p45LUjSzn72tqi6Z/ECSF9AMKb0howzZO9M6//HaaMJ98jqvrIdPvLI6yXKaSXcuSfKGqvpqkr1o9vz/d/tfywXADVW1oakvD6T5sDoI+J9JfrN+OTmLesI9fm00quoumqkSj5q0eA1N1wo04+wvGqHpw9q+9t1oBqS7iWbavze33SIkeUpmnzhmOfA7SbZvv/g9nGZ2rpl8HXhlu/0daQbSW28Nv3xtk4c3vgR4W/tBR5Jntte/BtxcVR+jGZjsGW330z1VdRbNJB57tq9vcdo5j5Msar8feBSwa1VdQTNxz7bA1rPUr0cg9/i1sTmFh07u8Ungi0muohnZcrq98ZncRBPQO9KMovjzJJ+i6Q66ug3YCWafEnFtknfRdNcE+HJVzTbE8nk0Q0OvopmXeTnwo/ax9wGnJ3k3D5297f00//1c19a2hmYM/d8FjkhyH818xH9K0/30wSQP0oz4+Oaq+kX7BfLHkjye5u/8o+32z2qXBfhIzW16TG2iHJ1T6liSravqJ0meCFxFM9vZUOPPS/PJPX6pexe3R9A8Gni/oa+F5h6/JPWMX+5KUs8Y/JLUMwa/JPWMwS9JPWPwS1LP/H9a1kc9wb17UQAAAABJRU5ErkJggg==\n",
+ "image/png": "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\n",
"text/plain": [
"
"
]
@@ -1331,31 +1099,40 @@
}
],
"source": [
- "%time report(play(random_guesser, target, verbose=False) for target in wordlist)"
+ "%time report(play(random_guesser, target, wordlist, verbose=False) for target in wordlist)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "# Evaluating Inconsistent Guessers\n",
- "\n",
- "Now we'll build and evaluate trees with inconsistent guesses allowed. This will take longer; about 30 seconds per tree."
+ "The random consistent guesser strategy might have seemed hopelessly naive, but it is actually a pretty decent strategy, with mean number of guesses only 5% worse than the best minimizing tree. "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Reports on Inconsistent Guessers\n",
+ "Now we'll report on trees with inconsistent guesses allowed. This will take longer; about 30 seconds per tree."
]
},
{
"cell_type": "code",
- "execution_count": 41,
+ "execution_count": 35,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
+ "minimizing the max of partition sizes over 2,845 targets in a 2,845 word list,\n",
+ "including inconsistent words.\n",
+ "first guess: \"wader\"\n",
"median: 7 guesses, mean: 7.05 ± 0.98, worst: 10, scores: 2,845\n",
"cumulative: ≤3:0%, ≤4:1%, ≤5:6%, ≤6:24%, ≤7:69%, ≤8:95%, ≤9:99.9%, ≤10:100%\n",
- "CPU times: user 30.5 s, sys: 116 ms, total: 30.7 s\n",
- "Wall time: 30.8 s\n"
+ "CPU times: user 25.9 s, sys: 17.6 ms, total: 25.9 s\n",
+ "Wall time: 25.9 s\n"
]
},
{
@@ -1372,22 +1149,25 @@
}
],
"source": [
- "%time report(tree_scores(minimizing_tree(max, wordlist, inconsistent=True)))"
+ "%time t = report_minimizing_tree(max, inconsistent=True)"
]
},
{
"cell_type": "code",
- "execution_count": 42,
+ "execution_count": 36,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
+ "minimizing the expectation of partition sizes over 2,845 targets in a 2,845 word list,\n",
+ "including inconsistent words.\n",
+ "first guess: \"raved\"\n",
"median: 7 guesses, mean: 6.84 ± 0.95, worst: 10, scores: 2,845\n",
"cumulative: ≤3:0%, ≤4:1%, ≤5:7%, ≤6:32%, ≤7:78%, ≤8:97%, ≤9:100.0%, ≤10:100%\n",
- "CPU times: user 29.7 s, sys: 39.8 ms, total: 29.7 s\n",
- "Wall time: 29.7 s\n"
+ "CPU times: user 26.3 s, sys: 19.1 ms, total: 26.3 s\n",
+ "Wall time: 26.3 s\n"
]
},
{
@@ -1404,22 +1184,25 @@
}
],
"source": [
- "%time report(tree_scores(minimizing_tree(expectation, wordlist, inconsistent=True)))"
+ "%time t = report_minimizing_tree(expectation, inconsistent=True)"
]
},
{
"cell_type": "code",
- "execution_count": 43,
+ "execution_count": 37,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
+ "minimizing the neg_entropy of partition sizes over 2,845 targets in a 2,845 word list,\n",
+ "including inconsistent words.\n",
+ "first guess: \"debar\"\n",
"median: 7 guesses, mean: 6.82 ± 1.00, worst: 10, scores: 2,845\n",
"cumulative: ≤3:0%, ≤4:1%, ≤5:8%, ≤6:35%, ≤7:78%, ≤8:97%, ≤9:99.6%, ≤10:100%\n",
- "CPU times: user 30 s, sys: 15.8 ms, total: 30 s\n",
- "Wall time: 30 s\n"
+ "CPU times: user 27.1 s, sys: 24.7 ms, total: 27.2 s\n",
+ "Wall time: 27.2 s\n"
]
},
{
@@ -1436,25 +1219,23 @@
}
],
"source": [
- "%time report(tree_scores(minimizing_tree(neg_entropy, wordlist, inconsistent=True)))"
+ "%time t = report_minimizing_tree(neg_entropy, inconsistent=True)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "# Jotto Evaluation Summary\n",
+ "# Report Summary\n",
"\n",
- "Here's a table of evaluation results for the mean and maximum number of guesses for the various approaches:\n",
+ "Here's a table of the mean and maximum number of guesses for the seven approaches:\n",
"\n",
- "|
Algorithm|Consistent Only Mean (Max)|Inconsistent Allowed Mean (Max)|\n",
+ "| 2,845 words Algorithm|Consistent Only Mean (Max)|Inconsistent Allowed Mean (Max)|\n",
"|--|--|--|\n",
- "|random guesser|7.34 (18)| |\n",
+ "|random guesser|7.42 (17)| |\n",
"|minimize max|7.15 (18)|7.05 (10)|\n",
"|minimize expectation|7.14 (17)|6.84 (10)|\n",
- "|minimize neg_entropy|7.09 (19)|6.82 (10)|\n",
- "\n",
- "(There can be slight variation from run to run.)\n"
+ "|minimize neg_entropy|7.09 (19)|6.82 (10)|\n"
]
},
{
@@ -1463,31 +1244,34 @@
"source": [
"# Wordle\n",
"\n",
- "[Wordle](https://www.powerlanguage.co.uk/wordle/) is a [suddenly-popular](https://www.nytimes.com/2022/01/03/technology/wordle-word-game-creator.html) variant of Jotto (with some Mastermind thrown in) with these differences:\n",
- "- Words with repeated letters are allowed, as are anagrams.\n",
+ "[Wordle](https://www.powerlanguage.co.uk/wordle/) is a [suddenly-popular](https://www.nytimes.com/2022/01/03/technology/wordle-word-game-creator.html) variant of Jotto (with a little Mastermind thrown in) with these differences:\n",
+ "- Words with repeated letters are allowed (e.g. `'aback'`).\n",
+ "- Anagrams are allowed (e.g. `'arise'` and `'raise'`).\n",
"- The reply to a guess consists of 5 pieces ([trits](https://en.wiktionary.org/wiki/trit#English)) of information, one for each position in the guess:\n",
" - *Green* if the guess letter is in the correct spot.\n",
" - *Yellow* if the guess letter is in the word but in the wrong spot.\n",
" - *Miss* if the letter is not in the word in any spot.\n",
+ "- Wordle uses a larger list of 12,971 allowable guess words, but only 2,315 of them can be target words. (I think the idea is to follow [Postel's law](https://en.wikipedia.org/wiki/Robustness_principle) to avoid annoying a player: be conservative in the targets (so that a player is very likely to be familiar with the target word) and be liberal in accepting guess words.)\n",
" \n",
- "Since repeated letters and anagrams are allowed, I can use all of `sgb_words` as my list of allowable Wordle words.\n",
- "\n",
- "There seems to be an ambiguity in the rules. Assume the guess is *etude* and the target is *poems*. I think the correct reply should be that one letter *e* is *yellow* and the other is a *miss*, although a strict reading of the rules would say they both should be *yellow*, because both instances of *e* are \"in the word but in the wrong spot.\" I decided that in cases like this I would report the first one as yellow and the second as a miss."
+ "There is an ambiguity in the rules. Assume the guess is *etude* and the target is *poems*. A strict reading of the rules would say they both *e* positions should be *yellow*, because both instances of *e* are \"in the word but in the wrong spot.\" But it seems Wordle actually reports the first *e* as yellow and the second *e* as a miss."
]
},
{
"cell_type": "code",
- "execution_count": 44,
+ "execution_count": 38,
"metadata": {},
"outputs": [],
"source": [
- "Green, Yellow, Miss = 'GY.' # A reply is 5 characters, each one of 'GY.'\n",
+ "wordle_small = read_words('wordle-small.txt') # 2,315 target words\n",
+ "wordle_big = read_words('wordle-big.txt') # 12,971 guess words\n",
+ "\n",
+ "Green, Yellow, Miss = 'GY.' # A Wordle reply is 5 characters, each one of 'GY.'\n",
"\n",
"def wordle_reply_for(guess, target) -> str: \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 put in the replies that should be yellow\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",
@@ -1500,28 +1284,58 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "The right thing to do now would be to refactor the code to allow for the injection of a different `reply_for` function. However, I'm not going to do that; instead I'm going to \"cheat\" and just redefine `reply_for` to be `wordle_reply_for`. So if you want to go back in this notebook and re-run some Jotto cells, you'll have to re-run the \"`def reply_for`\" cell first."
+ "The right thing to do now would be to refactor the code to allow for the injection of a different `reply_for` function, so that either game could be played at any time.\n",
+ "\n",
+ "However, I'm going to take a shortcut: I'm going to require the programmer to call `setup` with the name of the game they want, `'jotto'` or `'wordle'`. It will set global variables accordingly. I'll also require the programmer to use the right word list and target words."
]
},
{
"cell_type": "code",
- "execution_count": 45,
+ "execution_count": 39,
"metadata": {},
"outputs": [],
"source": [
- "reply_for = wordle_reply_for"
+ "jotto_reply_for = reply_for"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 40,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def setup(game: str) -> None:\n",
+ " \"Set global variables to play either 'jotto' or 'wordle'.\"\n",
+ " global reply_for, inconsistent_max\n",
+ " if game == 'jotto':\n",
+ " reply_for = jotto_reply_for\n",
+ " inconsistent_max = 400\n",
+ " elif game == 'wordle':\n",
+ " reply_for = wordle_reply_for\n",
+ " inconsistent_max = 125\n",
+ " else:\n",
+ " raise ValueError(f'unknown game: {game}')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 41,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "setup('wordle')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "Note that in Jotto, `reply_for` was symmetric; `reply_for(g, t) == reply_for(t, g)`. But that is not true for Wordle. (I had a bug somewhere in my code that showed up when playing Wordle but not Jotto, and I thought it might be because I had inadvertently reversed arguments to `reply_for` somewhere (but it turned out to be that I had left out a `wordlist=sgb_words`).) Anyway, here are some tests for `wordle_reply_for`:"
+ "Note that in Jotto, `reply_for` was symmetric; `reply_for(g, t) == reply_for(t, g)`. But that is not true for Wordle. I made some tests for `wordle_reply_for` to give me some confidence I got it right:"
]
},
{
"cell_type": "code",
- "execution_count": 46,
+ "execution_count": 42,
"metadata": {},
"outputs": [],
"source": [
@@ -1543,7 +1357,7 @@
},
{
"cell_type": "code",
- "execution_count": 47,
+ "execution_count": 43,
"metadata": {},
"outputs": [
{
@@ -1569,7 +1383,7 @@
" 'G.G.G': ['while']})"
]
},
- "execution_count": 47,
+ "execution_count": 43,
"metadata": {},
"output_type": "execute_result"
}
@@ -1586,7 +1400,138 @@
"\n",
"# Sample Wordle Games\n",
"\n",
- "Let's see what some Wordle games with a random guesser looks like:"
+ "Let's see what some Wordle games look like, using a random guesser:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 44,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Guess 1: \"pluck\" Reply: .Y...; Consistent targets: 269\n",
+ "Guess 2: \"false\" Reply: ..G.Y; Consistent targets: 9\n",
+ "Guess 3: \"below\" Reply: GGGGG; Consistent targets: 1\n"
+ ]
+ },
+ {
+ "data": {
+ "text/plain": [
+ "3"
+ ]
+ },
+ "execution_count": 44,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "play(random_guesser, wordlist=wordle_small)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 45,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Guess 1: \"caddy\" Reply: .....; Consistent targets: 729\n",
+ "Guess 2: \"rupee\" Reply: Y....; Consistent targets: 62\n",
+ "Guess 3: \"snort\" Reply: G.GGY; Consistent targets: 2\n",
+ "Guess 4: \"stork\" Reply: GGGG.; Consistent targets: 1\n",
+ "Guess 5: \"storm\" Reply: GGGGG; Consistent targets: 1\n"
+ ]
+ },
+ {
+ "data": {
+ "text/plain": [
+ "5"
+ ]
+ },
+ "execution_count": 45,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "play(random_guesser, wordlist=wordle_small)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 46,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Guess 1: \"wiser\" Reply: .G...; Consistent targets: 73\n",
+ "Guess 2: \"ninth\" Reply: .GG..; Consistent targets: 9\n",
+ "Guess 3: \"final\" Reply: .GG.G; Consistent targets: 1\n",
+ "Guess 4: \"vinyl\" Reply: GGGGG; Consistent targets: 1\n"
+ ]
+ },
+ {
+ "data": {
+ "text/plain": [
+ "4"
+ ]
+ },
+ "execution_count": 46,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "play(random_guesser, wordlist=wordle_small)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Reports on Wordle Guessers\n",
+ "\n",
+ "Wordle has about the same number of target words as Jotto, but many more guess words, and the `wordle_reply_for` computation is more complex, so computations take longer (more so for the inconsistent guessers). Here are reports on the seven strategies:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 47,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "median: 4 guesses, mean: 4.10 ± 1.04, worst: 8, scores: 2,315\n",
+ "cumulative: ≤3:28%, ≤4:69%, ≤5:92%, ≤6:98%, ≤7:99.4%, ≤8:100%, ≤9:100%, ≤10:100%\n",
+ "CPU times: user 30.1 s, sys: 29.3 ms, total: 30.1 s\n",
+ "Wall time: 30.2 s\n"
+ ]
+ },
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ "
"
]
@@ -1784,35 +1748,25 @@
}
],
"source": [
- "%time wtree = minimizing_tree(neg_entropy, sgb_words, sgb_words, inconsistent=True)\n",
- "report(tree_scores(wtree))"
+ "%time t = report_minimizing_tree(neg_entropy, wordle_small, wordle_big, inconsistent=True)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "Pretty good! The Wordle web site challenges you to solve each puzzle in six guesses; we can now do that 99.9% of the time when inconsistent guesses are allowed, a big jump from the 95% without inconsistent guesses and the 92% with random consistent guesses. \n",
- "\n",
- "This is all on the `sgb-words.txt` file. I poked around in the Wordle javascript, trying to find the official Wordle word list. If I correctly interpreted what I found, my algorithm gets these results:\n",
- "\n",
- " median: 3 guesses, mean: 3.49 ± 0.60, worst: 6, scores: 2,315\n",
- " cumulative: ≤3:53%, ≤4:96%, ≤5:99.8%, ≤6:100%, ≤7:100%, ≤8:100%, ≤9:100%, ≤10:100%\n",
- " \n",
- "I won't post the word list here, because I don't have the author's permission.\n",
+ "**Mission accomplished!** All three metrics solve every target word in 6 guesses or less. \n",
"\n",
"# Jotto and Wordle Evaluation Summary\n",
"\n",
- "Here is a summary (the first four columns on `sgb-words.txt`, the last on the Wordle word list):\n",
+ "Here is a summary of the reports on both games:\n",
"\n",
- "|
Algorithm|JOTTO Consistent Only Mean (Max)|JOTTO Inconsistent Allowed Mean (Max)|WORDLE Consistent Only Mean (Max)|WORDLE Inconsistent Allowed Mean (Max)|WORDLE Official Wordlist Mean (Max)|\n",
+ "|