diff --git a/ipynb/SpellingBee.ipynb b/ipynb/SpellingBee.ipynb index 87693f9..3ac26f9 100644 --- a/ipynb/SpellingBee.ipynb +++ b/ipynb/SpellingBee.ipynb @@ -28,19 +28,14 @@ "\n", "# Approach to a Solution\n", "\n", - "Since the word list was on my web site (it is a standard Scrabble word list that I happen to host a copy of), I felt somewhat compelled to submit an answer. I had worked on word puzzles before, like Scrabble and Boggle. My first thought is that this puzzle is rather different because it deals with *unordered sets* of letters, not *ordered permutations* of letters. That makes things much easier. When I tried to find the optimal 5×5 Boggle board, I couldn't exhaustively try all $26^{(5×5)} \\approx 10^{35}$ possibilites; I had to do hillclimbing to find a locally (but not necessarily globally) optimal solution. But for Spelling Bee, it is feasible to try every possibility. Here's a sketch of an approach:\n", + "Since the referenced [word list](https://norvig.com/ngrams/enable1.txt) was on my web site (it is a standard Scrabble word list that I happen to host a copy of), I felt somewhat compelled to submit an answer. I had worked on word puzzles before, like Scrabble and Boggle. My first thought is that this puzzle is rather different because it deals with *unordered sets* of letters, not *ordered permutations* of letters. That makes things much easier. When I tried to find the optimal 5×5 Boggle board, I couldn't exhaustively try all $26^{(5×5)} \\approx 10^{35}$ possibilites; I had to do hillclimbing to find a local maximum solution. But for Spelling Bee, it is feasible to try every possibility and get a guaranteed highest-scoring honeycomb. Here's a sketch of my approach:\n", " \n", - "\n", - "- Since every honeycomb must contain a pangram, I can find the best honeycomb by considering all possible pangrams and all possible centers for each pangram and taking the one that scores highest. Something like:\n", - "\n", - " max(game_score(pangram, center) \n", - " for pangram in pangrams for center in pangram)\n", - " \n", - "- So it comes down to having an efficient-enough computation of the words that a honeycomb can make.\n", - "- Represent a word as a set of letters, which I'll implement as a sorted string, e.g.: \n", - " letterset(\"GLAM\") == letterset(\"AMALGAM\") == \"AGLM\". \n", - "- Note: I could have used a `frozenset`, but strings have a more compact printed representation, making them easier to debug, and they take up less memory. I won't need any fancy `set` operations like union and intersection.\n", - "- Represent a honeycomb as a letterset of 7 letters, along with an indication of which one is the center. So the honeycomb in the image above would be represented by `('AEGLMPX', 'G')`." + "- Since order and repetition don't count, we can represent a word as a **set** of letters, which I will call a `letterset`. For simplicity I'll choose to implement that as a sorted string (not as a Python `set` or `frozenset`). For example:\n", + " letterset(\"GLAM\") == letterset(\"AMALGAM\") == \"AGLM\"\n", + "- A word is a **pangram** if and only if its letterset has exactly 7 letters.\n", + "- A honeycomb can be represented by a `(letterset, center)` pair, for example `('AEGLMPX', 'G')`.\n", + "- Since the rules say every valid honeycomb must contain a pangram, it must be that case that every valid honeycomb *is* a pangram. That means I can find the highest-scoring honeycomb by considering all possible pangram lettersets and all possible centers for each pangram, computing the game score for each one, and taking the maximum.\n", + "- So it all comes down to having an efficient-enough `game_score` function. We'll know how efficient it has to be once we figure out how many pangram lettersets there are (1,000? 100,000?). Note that it will be less than the number of pangrams, because, for example, the pangrams CACCIATORE and EROTICA both have the same letterset, ACEIORT." ] }, { @@ -58,8 +53,8 @@ "metadata": {}, "outputs": [], "source": [ - "from itertools import combinations\n", - "from collections import Counter" + "from collections import Counter, defaultdict\n", + "from itertools import combinations" ] }, { @@ -103,7 +98,7 @@ { "data": { "text/plain": [ - "{'AMALGAM', 'GAME', 'GLAM', 'MAPLE', 'MEGAPLEX', 'PELAGIC'}" + "{'AMALGAM', 'CACCIATORE', 'EROTICA', 'GAME', 'GLAM', 'MEGAPLEX'}" ] }, "execution_count": 3, @@ -112,7 +107,7 @@ } ], "source": [ - "words = Words('amalgam amalgamation game games gem glam maple megaplex pelagic I me')\n", + "words = Words('amalgam amalgamation game games gem glam megaplex cacciatore erotica I me')\n", "words" ] }, @@ -120,7 +115,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Note that `I`, `me` and `gem` are too short, `games` has an `s` which is not allowed, and `amalgamation` has too many distinct letters. \n", + "Note that `I`, `me` and `gem` are too short, `games` has an `s` which is not allowed, and `amalgamation` has too many distinct letters. We're left with six valid words out of the original eleven.\n", "\n", "Here are examples of the functions in action:" ] @@ -133,7 +128,12 @@ { "data": { "text/plain": [ - "{'AMALGAM': 7, 'MEGAPLEX': 15, 'GLAM': 1, 'MAPLE': 5, 'GAME': 1, 'PELAGIC': 14}" + "{'CACCIATORE': 17,\n", + " 'GLAM': 1,\n", + " 'GAME': 1,\n", + " 'AMALGAM': 7,\n", + " 'EROTICA': 14,\n", + " 'MEGAPLEX': 15}" ] }, "execution_count": 4, @@ -153,7 +153,7 @@ { "data": { "text/plain": [ - "{'MEGAPLEX', 'PELAGIC'}" + "{'CACCIATORE', 'EROTICA', 'MEGAPLEX'}" ] }, "execution_count": 5, @@ -173,12 +173,12 @@ { "data": { "text/plain": [ - "{'AMALGAM': 'AGLM',\n", - " 'MEGAPLEX': 'AEGLMPX',\n", + "{'CACCIATORE': 'ACEIORT',\n", " 'GLAM': 'AGLM',\n", - " 'MAPLE': 'AELMP',\n", " 'GAME': 'AEGM',\n", - " 'PELAGIC': 'ACEGILP'}" + " 'AMALGAM': 'AGLM',\n", + " 'EROTICA': 'ACEIORT',\n", + " 'MEGAPLEX': 'AEGLMPX'}" ] }, "execution_count": 6, @@ -246,16 +246,16 @@ { "data": { "text/plain": [ - "['CRACKLIER',\n", - " 'TURBINE',\n", - " 'METHIONINE',\n", - " 'UPGAZING',\n", - " 'CUMBERED',\n", - " 'BREEZEWAY',\n", - " 'JAMBING',\n", - " 'PAPERBACK',\n", - " 'TRIPINNATE',\n", - " 'TUNICAE']" + "['LINKWORK',\n", + " 'IMPURITY',\n", + " 'CROWBERRY',\n", + " 'ENDAMOEBA',\n", + " 'ADUMBRAL',\n", + " 'AGENTIAL',\n", + " 'PLIANCY',\n", + " 'MENTIONING',\n", + " 'INCARNADINE',\n", + " 'UNMEWING']" ] }, "execution_count": 9, @@ -292,9 +292,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "So: we start with 172,820 words in the word list, reduce that to 44,585 valid words (the others are either shorter than 4 letters in length, or contain an 'S', or have more than 7 distinct letters), and discover that 14,741 of those words are pangrams. \n", + "So: we start with 172,820 words in the word list, reduce that to 44,585 valid words, and find that 14,741 of those words are pangrams. \n", "\n", - "I'm also curious: what's the highest-scoring individual word?" + "I'm curious: what's the highest-scoring individual word?" ] }, { @@ -305,7 +305,7 @@ { "data": { "text/plain": [ - "('ANTITOTALITARIAN', True, 23)" + "(23, 'ANTITOTALITARIAN')" ] }, "execution_count": 11, @@ -314,65 +314,73 @@ } ], "source": [ - "w = max(enable1, key=word_score)\n", - "w, is_pangram(w), word_score(w)" + "max((word_score(w), w) for w in enable1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# Efficiency: Caching a Scoring Table\n", - "\n", - "The goal is to find the honeycomb that maximizes the `game_score`: the total score of all words that can be made with the honeycomb. I've chosen to go down the path of considering all 14,741 pangrams, and all 7 centers for each pangram, for a total of 103,187 candidate honeycombs. \n", - "I'll make things more efficient by *caching* some important information after computing it once, so I don't need to recompute it 103,187 times.\n", - "- For each word, I'll precompute the `letterset` and the `word_score`.\n", - "- For each letterset, I'll add up the total `word_score` points (over all the words with that letterset).\n", - "- The function `scoring_table(words)` will return a table (dict) with this information:" + "And what's the breakdown of reasons why words are invalid?\n" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, - "outputs": [], - "source": [ - "def scoring_table(words) -> dict:\n", - " \"\"\"Return a dict of {letterset: sum_of_word_scores} over words.\"\"\"\n", - " table = Counter()\n", - " for w in words:\n", - " table[letterset(w)] += word_score(w)\n", - " return table" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "Counter({'AGLM': 8, 'AEGLMPX': 15, 'AELMP': 5, 'AEGM': 1, 'ACEGILP': 14})" + "Counter({'short': 922, 'valid': 44585, 's': 103913, 'long': 23400})" ] }, - "execution_count": 13, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "scoring_table(words)" + "Counter(('s' if 's' in w else 'short' if len(w) < 4 else 'long' if len(set(w)) > 7 else 'valid')\n", + " for w in open('enable1.txt').read().split())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Note the letterset\n", - "`'AGLM'` scores 8 points as the sum over two words: 7 for `'AMALGAM'` and 1 for `'GLAM'`. The other lettersets get their points from just one word.\n", - "The following calculation says that there are about twice as many words as lettersets: on average about two words have the same letterset.\n", - "\n" + "About 60% of the words have an 's' in them." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Game Score\n", + "\n", + "The game score for a honeycomb is the sum of the word scores for all the words that the honeycomb can make. How do we know if a honeycomb can make a word? Well, a honeycomb can make a word if the word contains the honeycomb's center and every letter in the word is in the honeycomb. Another way of saying this is that the letters in the word must be a subset of the letters in the honeycomb.\n", + "\n", + "So the brute-force approach to `game_score` is:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "def game_score(honeycomb, words):\n", + " \"\"\"The total score for this honeycomb.\"\"\"\n", + " (letters, center) = honeycomb\n", + " return sum(word_score(word) for word in words \n", + " if center in word and all(c in letters for c in word))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's try it, and see how long it takes to get the game score for one honeycomb:" ] }, { @@ -380,10 +388,18 @@ "execution_count": 14, "metadata": {}, "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 10.3 ms, sys: 436 µs, total: 10.7 ms\n", + "Wall time: 10.6 ms\n" + ] + }, { "data": { "text/plain": [ - "2.058307557361156" + "153" ] }, "execution_count": 14, @@ -392,20 +408,28 @@ } ], "source": [ - "len(enable1) / len(scoring_table(enable1))" + "honeycomb = ('AEGLMPX', 'G')\n", + "\n", + "%time game_score(honeycomb, enable1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# Computing the Game Score\n", + "About 10 milliseconds. No problem if we only want to do it a few times. But to find the best honeycomb we're going to have to go through 14,741 pangrams, and try each of the 7 possible letters as the center. Note that 14,741 × 7 × 10 milliseconds is 15 or 20 minutes. I could leave it at that, but, for these kinds of puzzles, you don't feel like you're done until you get the runtime under one minute.\n", "\n", - "The brute force approach would be to take each of the 103,187 honeycombs, and for each honeycomb look at each of the 44,585 words and add up the word scores of the words that can be made by the honeycomb. That seems slow. I have an idea for a faster approach:\n", + "# Efficient Game Score\n", "\n", - "- For each honeycomb, generate every possible *subset* of the letters in the honeycomb. A subset must include the central letter, and it may or may not include each of the other 6 letters, so there are $2^6 = 64$ subsets. The function `letter_subsets(letters)` returns these.\n", - "- We already have letterset scores in the scoring table, so we can compute the `game_score` of a honeycomb just by fetching 64 entries in the scoring table and adding them up.\n", - "- 64 is less than 44,585, so that's a nice optimization!\n" + "Here's my idea:\n", + "\n", + "1. Go through all the words, compute the `letterset` and `word_score` for each one, and make a table of `{letterset: points}` giving the total number of points that can be made with that letterset. I call this a `points_table`.\n", + "3. The above calculations are independent of the honeycomb, so they only need to be done once, not 14,741 × 7 times. Nice saving!\n", + "4. Now for each honeycomb, generate every valid **subset** of the letters in the honeycomb. A valid subset must include the center letter, and it may or may not include each of the other 6 letters, so there are exactly $2^6 = 64$ subsets. The function `letter_subsets(honeycomb)` returns these.\n", + "5. To compute `game_score`, just take the sum of the 64 entries in the points table.\n", + "6. So we're only iterating over 64 lettersets in `game_score` rather than over 44,585 words. That's a nice improvement!\n", + "\n", + "Here's the code:" ] }, { @@ -414,24 +438,31 @@ "metadata": {}, "outputs": [], "source": [ - "def game_score(letters, center, table) -> int:\n", - " \"\"\"The total score for this honeycomb, given a scoring table.\"\"\"\n", - " subsets = letter_subsets(letters, center)\n", - " return sum(table[s] for s in subsets)\n", + "def game_score(honeycomb, pts_table) -> int:\n", + " \"\"\"The total score for this honeycomb, given a points_table.\"\"\"\n", + " return sum(pts_table[s] for s in letter_subsets(honeycomb))\n", "\n", - "def letter_subsets(letters, center) -> list:\n", - " \"\"\"All subsets of `letters` that contain the letter `center`.\"\"\"\n", - " return [letterset(subset) \n", + "def letter_subsets(honeycomb) -> list:\n", + " \"\"\"All 64 subsets of the letters in the honeycomb that contain the center letter.\"\"\"\n", + " letters, center = honeycomb\n", + " return [''.join(subset) \n", " for n in range(1, 8) \n", " for subset in combinations(letters, n)\n", - " if center in subset]" + " if center in subset]\n", + "\n", + "def points_table(words) -> dict:\n", + " \"\"\"Return a dict of {letterset: points} from words.\"\"\"\n", + " table = Counter()\n", + " for w in words:\n", + " table[letterset(w)] += word_score(w)\n", + " return table" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Trying out `letter_subsets`:" + "Let's look into how this works. First the `letter_subsets`:" ] }, { @@ -451,7 +482,7 @@ } ], "source": [ - "len(letter_subsets('ABCDEFG', 'C')) # It will always be 64, for any honeycomb" + "len(letter_subsets(honeycomb)) # It will always be 64, for any honeycomb" ] }, { @@ -462,7 +493,22 @@ { "data": { "text/plain": [ - "['C', 'AC', 'BC', 'CD', 'ABC', 'ACD', 'BCD', 'ABCD']" + "['C',\n", + " 'AC',\n", + " 'BC',\n", + " 'CD',\n", + " 'CE',\n", + " 'ABC',\n", + " 'ACD',\n", + " 'ACE',\n", + " 'BCD',\n", + " 'BCE',\n", + " 'CDE',\n", + " 'ABCD',\n", + " 'ABCE',\n", + " 'ACDE',\n", + " 'BCDE',\n", + " 'ABCDE']" ] }, "execution_count": 17, @@ -471,14 +517,14 @@ } ], "source": [ - "letter_subsets('ABCD', 'C') # A smaller example gives 2**3 = 8 subsets" + "letter_subsets(('ABCDE', 'C')) # A small `honeycomb` with only 5 letters gives 2**4 = 16 subsets" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Trying out `game_score`:" + "Now the `points_table` (but first a reminder of our honeycomb and our words and their scores):" ] }, { @@ -489,7 +535,7 @@ { "data": { "text/plain": [ - "24" + "('AEGLMPX', 'G')" ] }, "execution_count": 18, @@ -498,7 +544,7 @@ } ], "source": [ - "game_score('AEGLMPX', 'G', scoring_table(words)) " + "honeycomb" ] }, { @@ -509,7 +555,12 @@ { "data": { "text/plain": [ - "153" + "{'CACCIATORE': 17,\n", + " 'GLAM': 1,\n", + " 'GAME': 1,\n", + " 'AMALGAM': 7,\n", + " 'EROTICA': 14,\n", + " 'MEGAPLEX': 15}" ] }, "execution_count": 19, @@ -518,14 +569,7 @@ } ], "source": [ - "game_score('AEGLMPX', 'G', scoring_table(enable1)) " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's choose some more common letters and see if we can score more:" + "{w: word_score(w) for w in words}" ] }, { @@ -536,7 +580,7 @@ { "data": { "text/plain": [ - "2240" + "Counter({'ACEIORT': 31, 'AGLM': 8, 'AEGM': 1, 'AEGLMPX': 15})" ] }, "execution_count": 20, @@ -545,7 +589,65 @@ } ], "source": [ - "game_score('ETANHRD', 'E', scoring_table(enable1)) " + "points_table(words)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The letterset `'ACEIORT'` gets 31 points, 17 for CACCIATORE and 14 for EROTICA, and the letterset `'AGLM'` gets 8 points, 7 for AMALGAM and 1 for GLAM. The other lettersets represent one word each. Now, finally, we can compute the game score:" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "24" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "game_score(honeycomb, points_table(words))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "That's 15 points for MEGAPLEX, 7 for AMALGAM, 1 for GLAM and 1 for GAME.\n", + "\n", + "\n", + "The following calculation says that there are about twice as many words as lettersets: on average about two words have the same letterset.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "2.058307557361156" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(enable1) / len(points_table(enable1))" ] }, { @@ -554,23 +656,22 @@ "source": [ "# The Solution: The Best Honeycomb\n", "\n", - "\n", - "Finally, here's the function that will give us the solution: `best_honeycomb` searches through every possible pangram and center and finds the combination that gives the highest game score:" + "Now that we have an efficient `game_score` function, I can define `best_honeycomb` to search through every possible pangram and center and find the honeycomb that gives the highest game score:" ] }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 23, "metadata": {}, "outputs": [], "source": [ "def best_honeycomb(words) -> tuple: \n", " \"\"\"Return (score, letters, center) for the honeycomb with highest score on these words.\"\"\"\n", - " table = scoring_table(words)\n", - " pangrams = {s for s in table if len(s) == 7}\n", - " return max([game_score(pangram, center, table), pangram, center]\n", - " for pangram in pangrams\n", - " for center in pangram)" + " pts_table = points_table(words)\n", + " pangrams = [s for s in pts_table if len(s) == 7]\n", + " honeycombs = ((pangram, center) for pangram in pangrams for center in pangram)\n", + " return max([game_score(h, pts_table), h]\n", + " for h in honeycombs)" ] }, { @@ -582,16 +683,16 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 24, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[29, 'AEGLMPX', 'M']" + "[31, ('ACEIORT', 'T')]" ] }, - "execution_count": 22, + "execution_count": 24, "metadata": {}, "output_type": "execute_result" } @@ -609,24 +710,24 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 25, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 4.28 s, sys: 11.2 ms, total: 4.29 s\n", - "Wall time: 4.3 s\n" + "CPU times: user 2.07 s, sys: 7.27 ms, total: 2.08 s\n", + "Wall time: 2.09 s\n" ] }, { "data": { "text/plain": [ - "[3898, 'AEGINRT', 'R']" + "[3898, ('AEGINRT', 'R')]" ] }, - "execution_count": 23, + "execution_count": 25, "metadata": {}, "output_type": "execute_result" } @@ -639,49 +740,103 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "**Wow. 3898** is a high score! And it took less than 5 seconds to find it.\n", + "**Wow! 3898 is a high score!** And it took only 2 seconds to find it!\n", "\n", - "However, I'd like to see the actual words in addition to the score. If I had designed my program to be modular rather than to be efficient, that would be trivial. But as is, I need to define a new function, `scoring_words`, before I can create such a report:" + "# Fancier Report\n", + "\n", + "I'd like to see the actual words in addition to the total score, and I'm curious about how the words are divided up by letterset. Here's a function to provide such a report. I remembered that there is a `fill` function in Python (it is in the `textwrap` module) but this all turned out to be more complicated than I expected." ] }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 26, "metadata": { "scrolled": false }, "outputs": [], "source": [ - "def scoring_words(letters, center, words) -> set:\n", - " \"\"\"The set of words that this honeycomb can make.\"\"\"\n", - " subsets = letter_subsets(letters, center)\n", - " return {w for w in words if letterset(w) in subsets}\n", + "from textwrap import fill, wrap\n", "\n", - "def report(words):\n", - " \"\"\"Print stats and word scores for the best honeycomb on these words.\"\"\"\n", - " (score, letters, center) = best_honeycomb(words)\n", - " sw = scoring_words(letters, center, words)\n", - " top = max(sw, key=word_score)\n", - " np = sum(map(is_pangram, sw))\n", - " assert score == sum(map(word_score, sw))\n", - " print(f'''\n", - " The highest-scoring honeycomb for this list of {len(words)} words is:\n", - " {letters} (center {center})\n", - " It scores {score} points on {len(sw)} words with {np} pangrams*\n", - " The top scoring word is {top} for {word_score(top)} points.\\n''')\n", - " printcolumns(4, 20, [f'{w} ({word_score(w)}){\"*\" if is_pangram(w) else \" \"}'\n", - " for w in sorted(sw)])\n", + "def report(words, honeycomb=None):\n", + " \"\"\"Print stats and word scores for the given honeycomb (or for the best honeycomb\n", + " if no honeycomb is given) on the given word list.\"\"\"\n", + " optimal = (\"\" if honeycomb else \"optimal \")\n", + " if not honeycomb:\n", + " _, honeycomb = best_honeycomb(words)\n", + " letters, center = honeycomb\n", + " subsets = letter_subsets(honeycomb)\n", + " bins = group_by(words, letterset)\n", + " score = sum(word_score(w) for w in words if letterset(w) in subsets)\n", + " N = sum(len(bins[s]) for s in subsets)\n", + " print(f'For this list of {len(words):,d} words:\\n'\n", + " f'The {optimal}honeycomb ({letters}, {center}) forms '\n", + " f'{N} words for {score:,d} points.\\n')\n", + " for s in sorted(subsets, key=lambda s: (-len(s), s)):\n", + " if bins[s]:\n", + " p = (' pangram' if len(s) == 7 else '')\n", + " pts = sum(word_score(w) for w in bins[s])\n", + " print(f'{s} forms {len(bins[s])}{p} words for {pts:,d} points:')\n", + " words = [f'{w}({word_score(w)})' for w in sorted(bins[s])]\n", + " print(fill(' '.join(words), width=80,\n", + " initial_indent=' ', subsequent_indent=' '))\n", "\n", - "def printcolumns(cols, width, items):\n", - " \"\"\"Print items in designated columns of designated width.\"\"\"\n", - " for i, item in enumerate(items, 1):\n", - " print(item.ljust(width), end='')\n", - " if i % cols == 0: print()" + "def group_by(items, key):\n", + " \"Group items into bins of a dict, each bin keyed by key(item).\"\n", + " bins = defaultdict(list)\n", + " for item in items:\n", + " bins[key(item)].append(item)\n", + " return bins" ] }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "For this list of 6 words:\n", + "The honeycomb (AEGLMPX, G) forms 4 words for 24 points.\n", + "\n", + "AEGLMPX forms 1 pangram words for 15 points:\n", + " MEGAPLEX(15)\n", + "AEGM forms 1 words for 1 points:\n", + " GAME(1)\n", + "AGLM forms 2 words for 8 points:\n", + " AMALGAM(7) GLAM(1)\n" + ] + } + ], + "source": [ + "report(words, honeycomb)" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "For this list of 6 words:\n", + "The optimal honeycomb (ACEIORT, T) forms 2 words for 31 points.\n", + "\n", + "ACEIORT forms 2 pangram words for 31 points:\n", + " CACCIATORE(17) EROTICA(14)\n" + ] + } + ], + "source": [ + "report(words)" + ] + }, + { + "cell_type": "code", + "execution_count": 29, "metadata": { "scrolled": false }, @@ -690,147 +845,163 @@ "name": "stdout", "output_type": "stream", "text": [ + "For this list of 44,585 words:\n", + "The optimal honeycomb (AEGINRT, R) forms 537 words for 3,898 points.\n", "\n", - " The highest-scoring honeycomb for this list of 44585 words is:\n", - " AEGINRT (center R)\n", - " It scores 3898 points on 537 words with 50 pangrams*\n", - " The top scoring word is REINTEGRATING for 20 points.\n", - "\n", - "AERATE (6) AERATING (15)* AERIE (5) AERIER (6) \n", - "AGAR (1) AGER (1) AGGER (5) AGGREGATE (9) \n", - "AGGREGATING (18)* AGINNER (7) AGRARIAN (8) AGREE (5) \n", - "AGREEING (8) AGRIA (5) AIGRET (6) AIGRETTE (8) \n", - "AIRER (5) AIRIER (6) AIRING (6) AIRN (1) \n", - "AIRT (1) AIRTING (7) ANEAR (5) ANEARING (8) \n", - "ANERGIA (7) ANGARIA (7) ANGER (5) ANGERING (8) \n", - "ANGRIER (7) ANTEATER (8) ANTIAIR (7) ANTIAR (6) \n", - "ANTIARIN (8) ANTRA (5) ANTRE (5) AREA (1) \n", - "AREAE (5) ARENA (5) ARENITE (7) ARETE (5) \n", - "ARGENT (6) ARGENTINE (16)* ARGENTITE (16)* ARGININE (8) \n", - "ARIA (1) ARIETTA (7) ARIETTE (7) ARRAIGN (7) \n", - "ARRAIGNING (10) ARRANGE (7) ARRANGER (8) ARRANGING (9) \n", - "ARRANT (6) ARREAR (6) ARREARAGE (9) ARTIER (6) \n", - "ATRIA (5) ATTAINER (8) ATTAR (5) ATTIRE (6) \n", - "ATTIRING (8) ATTRITE (7) EAGER (5) EAGERER (7) \n", - "EAGRE (5) EARING (6) EARN (1) EARNER (6) \n", - "EARNING (7) EARRING (7) EATER (5) EERIE (5) \n", - "EERIER (6) EGER (1) EGGAR (5) EGGER (5) \n", - "EGRET (5) ENGAGER (7) ENGINEER (8) ENGINEERING (11) \n", - "ENGIRT (6) ENGRAIN (7) ENGRAINING (10) ENRAGE (6) \n", - "ENRAGING (8) ENTER (5) ENTERA (6) ENTERER (7) \n", - "ENTERING (8) ENTERTAIN (9) ENTERTAINER (11) ENTERTAINING (19)* \n", - "ENTIRE (6) ENTRAIN (7) ENTRAINER (9) ENTRAINING (17)* \n", - "ENTRANT (7) ENTREAT (7) ENTREATING (17)* ENTREE (6) \n", - "ERGATE (6) ERNE (1) ERRANT (6) ERRATA (6) \n", - "ERRING (6) ETAGERE (7) ETERNE (6) GAGER (5) \n", - "GAGGER (6) GAINER (6) GAITER (6) GANGER (6) \n", - "GANGRENE (8) GANGRENING (10) GARAGE (6) GARAGING (8) \n", - "GARGET (6) GARNER (6) GARNERING (9) GARNET (6) \n", - "GARNI (5) GARNIERITE (17)* GARRET (6) GARRING (7) \n", - "GARTER (6) GARTERING (16)* GEAR (1) GEARING (7) \n", - "GENERA (6) GENERATE (8) GENERATING (17)* GENRE (5) \n", - "GERENT (6) GETTER (6) GETTERING (9) GINGER (6) \n", - "GINGERING (9) GINNER (6) GINNIER (7) GIRN (1) \n", - "GIRNING (7) GIRT (1) GIRTING (7) GITTERN (7) \n", - "GNAR (1) GNARR (5) GNARRING (8) GNATTIER (15)* \n", - "GRAIN (5) GRAINER (7) GRAINIER (8) GRAINING (8) \n", - "GRAN (1) GRANA (5) GRANGE (6) GRANGER (7) \n", - "GRANITA (7) GRANITE (14)* GRANNIE (7) GRANT (5) \n", - "GRANTEE (7) GRANTER (7) GRANTING (8) GRAT (1) \n", - "GRATE (5) GRATER (6) GRATIN (6) GRATINE (14)* \n", - "GRATINEE (15)* GRATINEEING (18)* GRATING (7) GREAT (5) \n", - "GREATEN (7) GREATENING (17)* GREATER (7) GREE (1) \n", - "GREEGREE (8) GREEING (7) GREEN (5) GREENER (7) \n", - "GREENGAGE (9) GREENIE (7) GREENIER (8) GREENING (8) \n", - "GREET (5) GREETER (7) GREETING (8) GREGARINE (9) \n", - "GREIGE (6) GRIG (1) GRIGRI (6) GRIN (1) \n", - "GRINNER (7) GRINNING (8) GRIT (1) GRITTIER (8) \n", - "GRITTING (8) IGNITER (7) INANER (6) INERRANT (8) \n", - "INERT (5) INERTIA (7) INERTIAE (8) INGRAIN (7) \n", - "INGRAINING (10) INGRATE (14)* INGRATIATE (17)* INGRATIATING (12) \n", - "INNER (5) INTEGER (7) INTEGRATE (16)* INTEGRATING (18)* \n", - "INTENERATE (10) INTENERATING (19)* INTER (5) INTERAGE (15)* \n", - "INTERGANG (16)* INTERN (6) INTERNE (7) INTERNEE (8) \n", - "INTERNING (9) INTERREGNA (17)* INTERRING (9) INTERTIE (8) \n", - "INTRANT (7) INTREAT (7) INTREATING (17)* INTRIGANT (9) \n", - "IRATE (5) IRATER (6) IRING (5) IRRIGATE (8) \n", - "IRRIGATING (10) IRRITANT (8) IRRITATE (8) IRRITATING (10) \n", - "ITERANT (7) ITERATE (7) ITERATING (16)* ITINERANT (9) \n", - "ITINERATE (9) ITINERATING (18)* NAGGER (6) NAGGIER (7) \n", - "NAIRA (5) NARINE (6) NARRATE (7) NARRATER (8) \n", - "NARRATING (9) NATTER (6) NATTERING (16)* NATTIER (7) \n", - "NEAR (1) NEARER (6) NEARING (7) NEATER (6) \n", - "NEGATER (7) NETTER (6) NETTIER (7) NIGGER (6) \n", - "NITER (5) NITERIE (7) NITRATE (7) NITRATING (9) \n", - "NITRE (5) NITRITE (7) NITTIER (7) RAGA (1) \n", - "RAGE (1) RAGEE (5) RAGGEE (6) RAGGING (7) \n", - "RAGI (1) RAGING (6) RAGTAG (6) RAIA (1) \n", - "RAIN (1) RAINIER (7) RAINING (7) RANEE (5) \n", - "RANG (1) RANGE (5) RANGER (6) RANGIER (7) \n", - "RANGING (7) RANI (1) RANT (1) RANTER (6) \n", - "RANTING (7) RARE (1) RARER (5) RARING (6) \n", - "RATAN (5) RATATAT (7) RATE (1) RATER (5) \n", - "RATINE (6) RATING (6) RATITE (6) RATTAN (6) \n", - "RATTEEN (7) RATTEN (6) RATTENER (8) RATTENING (16)* \n", - "RATTER (6) RATTIER (7) RATTING (7) REAGENT (7) \n", - "REAGGREGATE (11) REAGGREGATING (20)* REAGIN (6) REAR (1) \n", - "REARER (6) REARING (7) REARRANGE (9) REARRANGING (11) \n", - "REATA (5) REATTAIN (8) REATTAINING (18)* REEARN (6) \n", - "REEARNING (9) REENGAGE (8) REENGAGING (10) REENGINEER (10) \n", - "REENGINEERING (13) REENTER (7) REENTERING (10) REENTRANT (9) \n", - "REGAIN (6) REGAINER (8) REGAINING (9) REGATTA (7) \n", - "REGEAR (6) REGEARING (9) REGENERATE (10) REGENERATING (19)* \n", - "REGENT (6) REGGAE (6) REGINA (6) REGINAE (7) \n", - "REGNA (5) REGNANT (7) REGRANT (7) REGRANTING (17)* \n", - "REGRATE (7) REGRATING (16)* REGREEN (7) REGREENING (10) \n", - "REGREET (7) REGREETING (10) REGRET (6) REGRETTER (9) \n", - "REGRETTING (10) REIGN (5) REIGNING (8) REIGNITE (8) \n", - "REIGNITING (10) REIN (1) REINING (7) REINITIATE (10) \n", - "REINITIATING (19)* REINTEGRATE (18)* REINTEGRATING (20)* REINTER (7) \n", - "REINTERRING (11) REITERATE (9) REITERATING (18)* RENEGE (6) \n", - "RENEGER (7) RENEGING (8) RENIG (5) RENIGGING (9) \n", - "RENIN (5) RENITENT (8) RENNET (6) RENNIN (6) \n", - "RENT (1) RENTE (5) RENTER (6) RENTIER (7) \n", - "RENTING (7) RERAN (5) RERIG (5) RERIGGING (9) \n", - "RETAG (5) RETAGGING (16)* RETAIN (6) RETAINER (8) \n", - "RETAINING (16)* RETARGET (8) RETARGETING (18)* RETE (1) \n", - "RETEAR (6) RETEARING (16)* RETENE (6) RETIA (5) \n", - "RETIARII (8) RETIE (5) RETINA (6) RETINAE (7) \n", - "RETINE (6) RETINENE (8) RETINITE (8) RETINT (6) \n", - "RETINTING (9) RETIRANT (8) RETIRE (6) RETIREE (7) \n", - "RETIRER (7) RETIRING (8) RETRAIN (7) RETRAINING (17)* \n", - "RETREAT (7) RETREATANT (10) RETREATER (9) RETREATING (17)* \n", - "RETTING (7) RIANT (5) RIATA (5) RIGGER (6) \n", - "RIGGING (7) RING (1) RINGENT (7) RINGER (6) \n", - "RINGGIT (7) RINGING (7) RINNING (7) RITE (1) \n", - "RITTER (6) TAGGER (6) TAGRAG (6) TANAGER (7) \n", - "TANGERINE (16)* TANGIER (14)* TANNER (6) TANTARA (7) \n", - "TANTRA (6) TARE (1) TARGE (5) TARGET (6) \n", - "TARGETING (16)* TARING (6) TARN (1) TARRE (5) \n", - "TARRIER (7) TARRING (7) TART (1) TARTAN (6) \n", - "TARTANA (7) TARTAR (6) TARTER (6) TARTING (7) \n", - "TARTRATE (8) TATAR (5) TATER (5) TATTER (6) \n", - "TATTERING (16)* TATTIER (7) TEAR (1) TEARER (6) \n", - "TEARIER (7) TEARING (14)* TEENAGER (8) TEENER (6) \n", - "TEENIER (7) TEETER (6) TEETERING (9) TENNER (6) \n", - "TENTER (6) TENTERING (9) TENTIER (7) TERAI (5) \n", - "TERETE (6) TERGA (5) TERGITE (7) TERN (1) \n", - "TERNATE (7) TERNE (5) TERRA (5) TERRAE (6) \n", - "TERRAIN (7) TERRANE (7) TERRARIA (8) TERREEN (7) \n", - "TERRENE (7) TERRET (6) TERRIER (7) TERRINE (7) \n", - "TERRIT (6) TERTIAN (7) TETRA (5) TETTER (6) \n", - "TIARA (5) TIER (1) TIERING (7) TIGER (5) \n", - "TINIER (6) TINNER (6) TINNIER (7) TINTER (6) \n", - "TIRE (1) TIRING (6) TITER (5) TITRANT (7) \n", - "TITRATE (7) TITRATING (9) TITRE (5) TITTER (6) \n", - "TITTERER (8) TITTERING (9) TRAGI (5) TRAIN (5) \n", - "TRAINEE (7) TRAINER (7) TRAINING (8) TRAIT (5) \n", - "TREAT (5) TREATER (7) TREATING (15)* TREE (1) \n", - "TREEING (7) TREEN (5) TRET (1) TRIAGE (6) \n", - "TRIAGING (8) TRIENE (6) TRIENNIA (8) TRIER (5) \n", - "TRIG (1) TRIGGER (7) TRIGGERING (10) TRIGGING (8) \n", - "TRINE (5) TRINING (7) TRINITARIAN (11) TRITE (5) \n", - "TRITER (6) " + "AEGINRT forms 50 pangram words for 832 points:\n", + " AERATING(15) AGGREGATING(18) ARGENTINE(16) ARGENTITE(16) ENTERTAINING(19)\n", + " ENTRAINING(17) ENTREATING(17) GARNIERITE(17) GARTERING(16) GENERATING(17)\n", + " GNATTIER(15) GRANITE(14) GRATINE(14) GRATINEE(15) GRATINEEING(18)\n", + " GREATENING(17) INGRATE(14) INGRATIATE(17) INTEGRATE(16) INTEGRATING(18)\n", + " INTENERATING(19) INTERAGE(15) INTERGANG(16) INTERREGNA(17) INTREATING(17)\n", + " ITERATING(16) ITINERATING(18) NATTERING(16) RATTENING(16) REAGGREGATING(20)\n", + " REATTAINING(18) REGENERATING(19) REGRANTING(17) REGRATING(16)\n", + " REINITIATING(19) REINTEGRATE(18) REINTEGRATING(20) REITERATING(18)\n", + " RETAGGING(16) RETAINING(16) RETARGETING(18) RETEARING(16) RETRAINING(17)\n", + " RETREATING(17) TANGERINE(16) TANGIER(14) TARGETING(16) TATTERING(16)\n", + " TEARING(14) TREATING(15)\n", + "AEGINR forms 35 words for 270 points:\n", + " AGINNER(7) AGREEING(8) ANEARING(8) ANERGIA(7) ANGERING(8) ANGRIER(7)\n", + " ARGININE(8) EARING(6) EARNING(7) EARRING(7) ENGRAIN(7) ENGRAINING(10)\n", + " ENRAGING(8) GAINER(6) GANGRENING(10) GARNERING(9) GEARING(7) GRAINER(7)\n", + " GRAINIER(8) GRANNIE(7) GREGARINE(9) NAGGIER(7) NEARING(7) RANGIER(7)\n", + " REAGIN(6) REARING(7) REARRANGING(11) REEARNING(9) REENGAGING(10) REGAIN(6)\n", + " REGAINER(8) REGAINING(9) REGEARING(9) REGINA(6) REGINAE(7)\n", + "AEGIRT forms 5 words for 34 points:\n", + " AIGRET(6) AIGRETTE(8) GAITER(6) IRRIGATE(8) TRIAGE(6)\n", + "AEGNRT forms 13 words for 94 points:\n", + " ARGENT(6) GARNET(6) GENERATE(8) GRANTEE(7) GRANTER(7) GREATEN(7) NEGATER(7)\n", + " REAGENT(7) REGENERATE(10) REGNANT(7) REGRANT(7) TANAGER(7) TEENAGER(8)\n", + "AEINRT forms 30 words for 232 points:\n", + " ARENITE(7) ATTAINER(8) ENTERTAIN(9) ENTERTAINER(11) ENTRAIN(7) ENTRAINER(9)\n", + " INERRANT(8) INERTIA(7) INERTIAE(8) INTENERATE(10) INTREAT(7) ITERANT(7)\n", + " ITINERANT(9) ITINERATE(9) NATTIER(7) NITRATE(7) RATINE(6) REATTAIN(8)\n", + " REINITIATE(10) RETAIN(6) RETAINER(8) RETINA(6) RETINAE(7) RETIRANT(8)\n", + " RETRAIN(7) TERRAIN(7) TERTIAN(7) TRAINEE(7) TRAINER(7) TRIENNIA(8)\n", + "AGINRT forms 21 words for 167 points:\n", + " AIRTING(7) ATTIRING(8) GRANITA(7) GRANTING(8) GRATIN(6) GRATING(7)\n", + " INGRATIATING(12) INTRIGANT(9) IRRIGATING(10) IRRITATING(10) NARRATING(9)\n", + " NITRATING(9) RANTING(7) RATING(6) RATTING(7) TARING(6) TARRING(7) TARTING(7)\n", + " TITRATING(9) TRAINING(8) TRIAGING(8)\n", + "EGINRT forms 26 words for 218 points:\n", + " ENGIRT(6) ENTERING(8) GETTERING(9) GITTERN(7) GREETING(8) IGNITER(7)\n", + " INTEGER(7) INTERNING(9) INTERRING(9) REENTERING(10) REGREETING(10)\n", + " REGRETTING(10) REIGNITE(8) REIGNITING(10) REINTERRING(11) RENTING(7)\n", + " RETINTING(9) RETIRING(8) RETTING(7) RINGENT(7) TEETERING(9) TENTERING(9)\n", + " TIERING(7) TITTERING(9) TREEING(7) TRIGGERING(10)\n", + "AEGNR forms 18 words for 120 points:\n", + " ANGER(5) ARRANGE(7) ARRANGER(8) ENGAGER(7) ENRAGE(6) GANGER(6) GANGRENE(8)\n", + " GARNER(6) GENERA(6) GRANGE(6) GRANGER(7) GREENGAGE(9) NAGGER(6) RANGE(5)\n", + " RANGER(6) REARRANGE(9) REENGAGE(8) REGNA(5)\n", + "AEGRT forms 19 words for 123 points:\n", + " AGGREGATE(9) ERGATE(6) ETAGERE(7) GARGET(6) GARRET(6) GARTER(6) GRATE(5)\n", + " GRATER(6) GREAT(5) GREATER(7) REAGGREGATE(11) REGATTA(7) REGRATE(7) RETAG(5)\n", + " RETARGET(8) TAGGER(6) TARGE(5) TARGET(6) TERGA(5)\n", + "AEINR forms 3 words for 19 points:\n", + " INANER(6) NARINE(6) RAINIER(7)\n", + "AEIRT forms 20 words for 135 points:\n", + " ARIETTA(7) ARIETTE(7) ARTIER(6) ATTIRE(6) ATTRITE(7) IRATE(5) IRATER(6)\n", + " IRRITATE(8) ITERATE(7) RATITE(6) RATTIER(7) REITERATE(9) RETIA(5)\n", + " RETIARII(8) TARRIER(7) TATTIER(7) TEARIER(7) TERAI(5) TERRARIA(8) TITRATE(7)\n", + "AENRT forms 19 words for 132 points:\n", + " ANTEATER(8) ANTRE(5) ENTERA(6) ENTRANT(7) ENTREAT(7) ERRANT(6) NARRATE(7)\n", + " NARRATER(8) NATTER(6) NEATER(6) RANTER(6) RATTEEN(7) RATTEN(6) RATTENER(8)\n", + " REENTRANT(9) RETREATANT(10) TANNER(6) TERNATE(7) TERRANE(7)\n", + "AGINR forms 19 words for 138 points:\n", + " AGRARIAN(8) AIRING(6) ANGARIA(7) ARRAIGN(7) ARRAIGNING(10) ARRANGING(9)\n", + " GARAGING(8) GARNI(5) GARRING(7) GNARRING(8) GRAIN(5) GRAINING(8) INGRAIN(7)\n", + " INGRAINING(10) RAGGING(7) RAGING(6) RAINING(7) RANGING(7) RARING(6)\n", + "AGIRT forms 1 words for 5 points:\n", + " TRAGI(5)\n", + "AGNRT forms 1 words for 5 points:\n", + " GRANT(5)\n", + "AINRT forms 9 words for 64 points:\n", + " ANTIAIR(7) ANTIAR(6) ANTIARIN(8) INTRANT(7) IRRITANT(8) RIANT(5) TITRANT(7)\n", + " TRAIN(5) TRINITARIAN(11)\n", + "EGINR forms 24 words for 186 points:\n", + " ENGINEER(8) ENGINEERING(11) ERRING(6) GINGER(6) GINGERING(9) GINNER(6)\n", + " GINNIER(7) GREEING(7) GREENIE(7) GREENIER(8) GREENING(8) GRINNER(7)\n", + " NIGGER(6) REENGINEER(10) REENGINEERING(13) REGREENING(10) REIGN(5)\n", + " REIGNING(8) REINING(7) RENEGING(8) RENIG(5) RENIGGING(9) RERIGGING(9)\n", + " RINGER(6)\n", + "EGIRT forms 4 words for 27 points:\n", + " GRITTIER(8) TERGITE(7) TIGER(5) TRIGGER(7)\n", + "EGNRT forms 2 words for 12 points:\n", + " GERENT(6) REGENT(6)\n", + "EINRT forms 29 words for 190 points:\n", + " ENTIRE(6) INERT(5) INTER(5) INTERN(6) INTERNE(7) INTERNEE(8) INTERTIE(8)\n", + " NETTIER(7) NITER(5) NITERIE(7) NITRE(5) NITRITE(7) NITTIER(7) REINTER(7)\n", + " RENITENT(8) RENTIER(7) RETINE(6) RETINENE(8) RETINITE(8) RETINT(6)\n", + " TEENIER(7) TENTIER(7) TERRINE(7) TINIER(6) TINNER(6) TINNIER(7) TINTER(6)\n", + " TRIENE(6) TRINE(5)\n", + "GINRT forms 6 words for 43 points:\n", + " GIRTING(7) GRITTING(8) RINGGIT(7) TIRING(6) TRIGGING(8) TRINING(7)\n", + "AEGR forms 17 words for 84 points:\n", + " AGER(1) AGGER(5) AGREE(5) ARREARAGE(9) EAGER(5) EAGERER(7) EAGRE(5) EGGAR(5)\n", + " GAGER(5) GAGGER(6) GARAGE(6) GEAR(1) RAGE(1) RAGEE(5) RAGGEE(6) REGEAR(6)\n", + " REGGAE(6)\n", + "AEIR forms 4 words for 22 points:\n", + " AERIE(5) AERIER(6) AIRER(5) AIRIER(6)\n", + "AENR forms 9 words for 40 points:\n", + " ANEAR(5) ARENA(5) EARN(1) EARNER(6) NEAR(1) NEARER(6) RANEE(5) REEARN(6)\n", + " RERAN(5)\n", + "AERT forms 24 words for 127 points:\n", + " AERATE(6) ARETE(5) EATER(5) ERRATA(6) RATE(1) RATER(5) RATTER(6) REATA(5)\n", + " RETEAR(6) RETREAT(7) RETREATER(9) TARE(1) TARRE(5) TARTER(6) TARTRATE(8)\n", + " TATER(5) TATTER(6) TEAR(1) TEARER(6) TERRA(5) TERRAE(6) TETRA(5) TREAT(5)\n", + " TREATER(7)\n", + "AGIR forms 2 words for 6 points:\n", + " AGRIA(5) RAGI(1)\n", + "AGNR forms 5 words for 13 points:\n", + " GNAR(1) GNARR(5) GRAN(1) GRANA(5) RANG(1)\n", + "AGRT forms 3 words for 13 points:\n", + " GRAT(1) RAGTAG(6) TAGRAG(6)\n", + "AINR forms 4 words for 8 points:\n", + " AIRN(1) NAIRA(5) RAIN(1) RANI(1)\n", + "AIRT forms 5 words for 21 points:\n", + " AIRT(1) ATRIA(5) RIATA(5) TIARA(5) TRAIT(5)\n", + "ANRT forms 10 words for 50 points:\n", + " ANTRA(5) ARRANT(6) RANT(1) RATAN(5) RATTAN(6) TANTARA(7) TANTRA(6) TARN(1)\n", + " TARTAN(6) TARTANA(7)\n", + "EGIR forms 3 words for 17 points:\n", + " GREIGE(6) RERIG(5) RIGGER(6)\n", + "EGNR forms 6 words for 37 points:\n", + " GENRE(5) GREEN(5) GREENER(7) REGREEN(7) RENEGE(6) RENEGER(7)\n", + "EGRT forms 7 words for 45 points:\n", + " EGRET(5) GETTER(6) GREET(5) GREETER(7) REGREET(7) REGRET(6) REGRETTER(9)\n", + "EINR forms 4 words for 17 points:\n", + " INNER(5) REIN(1) RENIN(5) RENNIN(6)\n", + "EIRT forms 17 words for 87 points:\n", + " RETIE(5) RETIRE(6) RETIREE(7) RETIRER(7) RITE(1) RITTER(6) TERRIER(7)\n", + " TERRIT(6) TIER(1) TIRE(1) TITER(5) TITRE(5) TITTER(6) TITTERER(8) TRIER(5)\n", + " TRITE(5) TRITER(6)\n", + "ENRT forms 19 words for 104 points:\n", + " ENTER(5) ENTERER(7) ENTREE(6) ETERNE(6) NETTER(6) REENTER(7) RENNET(6)\n", + " RENT(1) RENTE(5) RENTER(6) RETENE(6) TEENER(6) TENNER(6) TENTER(6) TERN(1)\n", + " TERNE(5) TERREEN(7) TERRENE(7) TREEN(5)\n", + "GINR forms 9 words for 44 points:\n", + " GIRN(1) GIRNING(7) GRIN(1) GRINNING(8) IRING(5) RIGGING(7) RING(1)\n", + " RINGING(7) RINNING(7)\n", + "GIRT forms 3 words for 3 points:\n", + " GIRT(1) GRIT(1) TRIG(1)\n", + "AER forms 7 words for 25 points:\n", + " AREA(1) AREAE(5) ARREAR(6) RARE(1) RARER(5) REAR(1) REARER(6)\n", + "AGR forms 2 words for 2 points:\n", + " AGAR(1) RAGA(1)\n", + "AIR forms 2 words for 2 points:\n", + " ARIA(1) RAIA(1)\n", + "ART forms 5 words for 24 points:\n", + " ATTAR(5) RATATAT(7) TART(1) TARTAR(6) TATAR(5)\n", + "EGR forms 4 words for 15 points:\n", + " EGER(1) EGGER(5) GREE(1) GREEGREE(8)\n", + "EIR forms 2 words for 11 points:\n", + " EERIE(5) EERIER(6)\n", + "ENR forms 1 words for 1 points:\n", + " ERNE(1)\n", + "ERT forms 7 words for 27 points:\n", + " RETE(1) TEETER(6) TERETE(6) TERRET(6) TETTER(6) TREE(1) TRET(1)\n", + "GIR forms 2 words for 7 points:\n", + " GRIG(1) GRIGRI(6)\n" ] } ],