diff --git a/ipynb/SpellingBee.ipynb b/ipynb/SpellingBee.ipynb index 40361e5..e951ac2 100644 --- a/ipynb/SpellingBee.ipynb +++ b/ipynb/SpellingBee.ipynb @@ -28,14 +28,17 @@ "\n", "# Approach to a Solution\n", "\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", + "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. But this puzzle is different because it deals with *unordered sets* of letters, not *ordered permutations* of letters. That makes things much easier. When I searched for an optimal 5×5 Boggle board, I couldn't exhaustively try all $26^{(5×5)} \\approx 10^{35}$ possibilites; I could only do hillclimbing to find a local maximum. 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", "- 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." + "- A honeycomb can be represented by a `(letterset, center)` pair, for example `('AEGLMPX', 'G')` for the honeycomb above.\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:\n", + " * Consider all possible pangram lettersets and all possible centers for each pangram\n", + " * Compute the game score for each one\n", + " * Take the maximum; that's guaranteed to be the best possible honeycomb.\n", + "- So it all comes down to having an efficient-enough `game_score` function. " ] }, { @@ -63,10 +66,10 @@ "metadata": {}, "outputs": [], "source": [ - "def Words(text) -> set:\n", - " \"\"\"The set of all the valid space-separated words in a str.\"\"\"\n", - " return {w for w in text.upper().split() \n", - " if len(w) >= 4 and 'S' not in w and len(set(w)) <= 7}\n", + "def Words(text) -> list:\n", + " \"\"\"A list of all the valid space-separated words in a str.\"\"\"\n", + " return [w for w in text.upper().split() \n", + " if len(w) >= 4 and 'S' not in w and len(set(w)) <= 7]\n", "\n", "def word_score(word) -> int: \n", " \"\"\"The points for this word, including bonus for pangram.\"\"\"\n", @@ -79,7 +82,7 @@ " return len(set(word)) == 7\n", "\n", "def letterset(word) -> str:\n", - " \"\"\"The set of letters in a word, represented as a sorted str of letters.\"\"\"\n", + " \"\"\"The set of letters in a word, represented as a sorted str.\"\"\"\n", " return ''.join(sorted(set(word)))" ] }, @@ -98,7 +101,7 @@ { "data": { "text/plain": [ - "{'AMALGAM', 'CACCIATORE', 'EROTICA', 'GAME', 'GLAM', 'MEGAPLEX'}" + "['AMALGAM', 'GAME', 'GLAM', 'MEGAPLEX', 'CACCIATORE', 'EROTICA']" ] }, "execution_count": 3, @@ -128,12 +131,12 @@ { "data": { "text/plain": [ - "{'GAME': 1,\n", - " 'CACCIATORE': 17,\n", - " 'AMALGAM': 7,\n", + "{'AMALGAM': 7,\n", + " 'GAME': 1,\n", " 'GLAM': 1,\n", - " 'EROTICA': 14,\n", - " 'MEGAPLEX': 15}" + " 'MEGAPLEX': 15,\n", + " 'CACCIATORE': 17,\n", + " 'EROTICA': 14}" ] }, "execution_count": 4, @@ -173,12 +176,12 @@ { "data": { "text/plain": [ - "{'GAME': 'AEGM',\n", - " 'CACCIATORE': 'ACEIORT',\n", - " 'AMALGAM': 'AGLM',\n", + "{'AMALGAM': 'AGLM',\n", + " 'GAME': 'AEGM',\n", " 'GLAM': 'AGLM',\n", - " 'EROTICA': 'ACEIORT',\n", - " 'MEGAPLEX': 'AEGLMPX'}" + " 'MEGAPLEX': 'AEGLMPX',\n", + " 'CACCIATORE': 'ACEIORT',\n", + " 'EROTICA': 'ACEIORT'}" ] }, "execution_count": 6, @@ -190,13 +193,20 @@ "{w: letterset(w) for w in words}" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that AMALGAM and GLAM have the same letterset, as do CACCIATORE and EROTICA. " + ] + }, { "cell_type": "markdown", "metadata": {}, "source": [ "# The enable1 Word List\n", "\n", - "Now I will load in the `enable1` word list and see what we have:" + "Now I will load in the enable1 word list and see what we have:" ] }, { @@ -246,16 +256,7 @@ { "data": { "text/plain": [ - "['MODIFIER',\n", - " 'FLUORIC',\n", - " 'PENTANOL',\n", - " 'COMPLECT',\n", - " 'COVERTURE',\n", - " 'GNOTOBIOTIC',\n", - " 'INTREATED',\n", - " 'COMMUTATOR',\n", - " 'PREPLANT',\n", - " 'PRINTERY']" + "14741" ] }, "execution_count": 9, @@ -265,7 +266,7 @@ ], "source": [ "pangrams = [w for w in enable1 if is_pangram(w)]\n", - "pangrams[:10] # Just sample some of them" + "len(pangrams)" ] }, { @@ -276,7 +277,21 @@ { "data": { "text/plain": [ - "14741" + "{'AARDWOLF': 15,\n", + " 'BABBLEMENT': 17,\n", + " 'CABEZON': 14,\n", + " 'COLLOGUING': 17,\n", + " 'DEMERGERING': 18,\n", + " 'ETYMOLOGY': 16,\n", + " 'GARROTTING': 17,\n", + " 'IDENTIFY': 15,\n", + " 'LARVICIDAL': 17,\n", + " 'MORTGAGEE': 16,\n", + " 'OVERHELD': 15,\n", + " 'PRAWNED': 14,\n", + " 'REINITIATED': 18,\n", + " 'TOWHEAD': 14,\n", + " 'UTOPIAN': 14}" ] }, "execution_count": 10, @@ -285,14 +300,14 @@ } ], "source": [ - "len(pangrams)" + "{w: word_score(w) for w in pangrams[::1000]} # Just sample some of them" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "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", + "So: we start with 172,820 words in the enable1 word list, reduce that to 44,585 valid Spelling Bee words, and find that 14,741 of those words are pangrams. \n", "\n", "I'm curious: what's the highest-scoring individual word?" ] @@ -305,7 +320,7 @@ { "data": { "text/plain": [ - "(23, 'ANTITOTALITARIAN')" + "'ANTITOTALITARIAN'" ] }, "execution_count": 11, @@ -314,7 +329,7 @@ } ], "source": [ - "max((word_score(w), w) for w in enable1)" + "max(enable1, key=word_score)" ] }, { @@ -332,7 +347,7 @@ { "data": { "text/plain": [ - "Counter({'short': 922, 'valid': 44585, 's': 103913, 'long': 23400})" + "Counter({'short': 922, 'valid': 44585, 'S': 103913, 'long': 23400})" ] }, "execution_count": 12, @@ -341,26 +356,26 @@ } ], "source": [ - "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())" + "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().upper().split())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "About 60% of the words have an 's' in them." + "There are more than twice as many words with an 'S' than there are valid words." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# Game Score\n", + "# Game Score and Best Honeycomb\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", + "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? It can if (1) the word contains the honeycomb's center and (2) every letter in the word is in the honeycomb. Another way of saying (2) 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:" + "I can find the highest-scoring honeycomb by considering all pangram lettersets, and for each one considering all 7 possible centers, and keeping the one with the maximum game score:" ] }, { @@ -369,18 +384,27 @@ "metadata": {}, "outputs": [], "source": [ - "def game_score(honeycomb, words):\n", + "def game_score(honeycomb, words) -> int:\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))" + " if center in word and is_subset(word, letters))\n", + "\n", + "def best_honeycomb(words) -> list: \n", + " \"\"\"Return [score, honeycomb] for the honeycomb with highest score on these words.\"\"\"\n", + " pangrams = {letterset(w) for w in words if is_pangram(w)}\n", + " honeycombs = ((pangram, center) for pangram in pangrams for center in pangram)\n", + " return max([game_score(h, words), h]\n", + " for h in honeycombs)\n", + "\n", + "def is_subset(subset, superset) -> bool: return all(x in superset for x in subset)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Let's try it, and see how long it takes to get the game score for one honeycomb:" + "Let's try it:" ] }, { @@ -388,18 +412,10 @@ "execution_count": 14, "metadata": {}, "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 9.86 ms, sys: 343 µs, total: 10.2 ms\n", - "Wall time: 10 ms\n" - ] - }, { "data": { "text/plain": [ - "153" + "24" ] }, "execution_count": 14, @@ -410,281 +426,13 @@ "source": [ "honeycomb = ('AEGLMPX', 'G')\n", "\n", - "%time game_score(honeycomb, enable1)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "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", - "# Efficient Game Score\n", - "\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:" + "game_score(honeycomb, words)" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, - "outputs": [], - "source": [ - "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(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]\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": [ - "Let's look into how this works. First the `letter_subsets`:" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "64" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "len(letter_subsets(honeycomb)) # It will always be 64, for any honeycomb" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "['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, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "letter_subsets(('ABCDE', 'C')) # A small `honeycomb` with only 5 letters gives 2**4 = 16 subsets" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now the `points_table` (but first a reminder of our honeycomb and our words and their scores):" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "('AEGLMPX', 'G')" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "honeycomb" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'GAME': 1,\n", - " 'CACCIATORE': 17,\n", - " 'AMALGAM': 7,\n", - " 'GLAM': 1,\n", - " 'EROTICA': 14,\n", - " 'MEGAPLEX': 15}" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "{w: word_score(w) for w in words}" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Counter({'AEGM': 1, 'ACEIORT': 31, 'AGLM': 8, 'AEGLMPX': 15})" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "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))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# The Solution: The Best Honeycomb\n", - "\n", - "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": 23, - "metadata": {}, - "outputs": [], - "source": [ - "def best_honeycomb(words) -> tuple: \n", - " \"\"\"Return (score, honeycomb) for the honeycomb with highest score on these words.\"\"\"\n", - " 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)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "First the solution for the tiny `words` list:" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, "outputs": [ { "data": { @@ -692,7 +440,7 @@ "[31, ('ACEIORT', 'T')]" ] }, - "execution_count": 24, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } @@ -705,20 +453,229 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now the solution for the problem that The Riddler posed, the big `enable1` word list:" + "**We're done!** At least theoretically. But how long will it take to run the computation on the big `enable1` word list? Let's see how long it takes to compute the game score of a single honeycomb:" ] }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 2.04 s, sys: 7.02 ms, total: 2.05 s\n", - "Wall time: 2.06 s\n" + "CPU times: user 7.38 ms, sys: 332 µs, total: 7.71 ms\n", + "Wall time: 7.41 ms\n" + ] + }, + { + "data": { + "text/plain": [ + "153" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time game_score(honeycomb, enable1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "About 8 milliseconds. How many minutes would that be for all 14,741 pangrams and all 7 possible center letters for each pangram?" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "13.758266666666666" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + ".008 * 14741 * 7 / 60" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "About 14 minutes. I could run `best_honeycomb(enable1)` right now and take a coffee break until it completes, but I'm predisposed to think that a puzzle like this deserves a more elegant solution. I'd like to get the run time under a minute (as in [Project Euler](https://projecteuler.net/)).\n", + "\n", + "# Making it Faster\n", + "\n", + "Here's my plan for a more efficient program:\n", + "\n", + "1. Keep the same strategy of trying every pangram, but do some precomputation that will make `game_score` faster.\n", + "1. The precomputation is: compute the `letterset` and `word_score` for each word, and make a table of `{letterset: points}` giving the total number of points that can be made with each letterset. I call this a `points_table`.\n", + "3. These calculations are independent of the honeycomb, so they only need to be done once per word, not 14,741 × 7 times per word. \n", + "4. Within `game_score`, 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 subset entries in the points table.\n", + "\n", + "\n", + "That means that in `game_score` we no longer need to iterate over 44,585 words and check if each word is a subset of the honeycomb. Instead we iterate over the 64 subsets of the honeycomb and check if each one is a word (or more than word) and how many total points those word(s) score. \n", + "\n", + "Since 64 < 44,585, that's a nice improvement!\n", + "\n", + "\n", + "Here's the code. Notice we've changed the interface to `game_score`; it now takes a points table, not a word list." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "def best_honeycomb(words) -> tuple: \n", + " \"\"\"Return (score, honeycomb) for the honeycomb with highest score on these words.\"\"\"\n", + " 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)\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\n", + "\n", + "def letter_subsets(honeycomb) -> list:\n", + " \"\"\"The 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]\n", + "\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))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's get a feel for how this works. First the `letter_subsets`:" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['C', 'AC', 'BC', 'CD', 'ABC', 'ACD', 'BCD', 'ABCD']" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "letter_subsets(('ABCD', 'C')) # A 4-letter honeycomb gives 2**3 = 8 subsets; 7-letter gives 64" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now the `points_table`:" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['AMALGAM', 'GAME', 'GLAM', 'MEGAPLEX', 'CACCIATORE', 'EROTICA']\n" + ] + }, + { + "data": { + "text/plain": [ + "Counter({'AGLM': 8, 'AEGM': 1, 'AEGLMPX': 15, 'ACEIORT': 31})" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "print(words)\n", + "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. \n", + "\n", + "Let's make sure we haven't broken the `game_score` and `best_honeycomb` functions:" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [], + "source": [ + "assert game_score(honeycomb, points_table(words)) == 24\n", + "assert best_honeycomb(words) == [31, ('ACEIORT', 'T')]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# The Solution" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, the solution to the puzzle:" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 2.08 s, sys: 9.46 ms, total: 2.09 s\n", + "Wall time: 2.12 s\n" ] }, { @@ -727,7 +684,7 @@ "[3898, ('AEGINRT', 'R')]" ] }, - "execution_count": 25, + "execution_count": 22, "metadata": {}, "output_type": "execute_result" } @@ -742,14 +699,14 @@ "source": [ "**Wow! 3898 is a high score!** And it took only 2 seconds to find it!\n", "\n", - "# Fancier Report\n", + "# Fancy 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": 26, + "execution_count": 23, "metadata": { "scrolled": false }, @@ -758,19 +715,18 @@ "from textwrap import fill\n", "\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", + " \"\"\"Print stats, words, and word scores for the given honeycomb (or \n", + " for the best honeycomb if no honeycomb is given) over the given word list.\"\"\"\n", " optimal = (\"\" if honeycomb else \"optimal \")\n", - " if not honeycomb:\n", + " if honeycomb is None:\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", - " print(f'The {optimal}honeycomb ({letters}, {center}) forms '\n", - " f'{N} words for {score:,d} points.')\n", + " print(f'The {optimal}honeycomb {honeycomb} forms '\n", + " f'{N:,d} words for {score:,d} points.')\n", " print(f'Here are the words formed, with pangrams first:\\n')\n", " for s in sorted(subsets, key=lambda s: (-len(s), s)):\n", " if bins[s]:\n", @@ -790,7 +746,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 24, "metadata": {}, "outputs": [ { @@ -798,7 +754,7 @@ "output_type": "stream", "text": [ "For this list of 6 words:\n", - "The honeycomb (AEGLMPX, G) forms 4 words for 24 points.\n", + "The honeycomb ('AEGLMPX', 'G') forms 4 words for 24 points.\n", "Here are the words formed, with pangrams first:\n", "\n", "AEGLMPX forms 1 words for 15 points:\n", @@ -816,29 +772,7 @@ }, { "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", - "Here are the words formed, with pangrams first:\n", - "\n", - "ACEIORT forms 2 words for 31 points:\n", - " CACCIATORE(17) EROTICA(14)\n" - ] - } - ], - "source": [ - "report(words)" - ] - }, - { - "cell_type": "code", - "execution_count": 29, + "execution_count": 25, "metadata": { "scrolled": false }, @@ -848,7 +782,7 @@ "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", + "The optimal honeycomb ('AEGINRT', 'R') forms 537 words for 3,898 points.\n", "Here are the words formed, with pangrams first:\n", "\n", "AEGINRT forms 50 words for 832 points:\n", @@ -1011,6 +945,318 @@ "source": [ "report(enable1)" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# S Words\n", + "\n", + "What if we did allow honeycombs (and words) with an S in them?" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [], + "source": [ + "def S_words(text) -> list:\n", + " \"\"\"A list of all the valid space-separated words in a str, including words with an S.\"\"\"\n", + " return [w for w in text.upper().split() \n", + " if len(w) >= 4 and len(set(w)) <= 7]" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "For this list of 98,141 words:\n", + "The optimal honeycomb ('AEINRST', 'E') forms 1,179 words for 8,681 points.\n", + "Here are the words formed, with pangrams first:\n", + "\n", + "AEINRST forms 86 words for 1,381 points:\n", + " ANESTRI(14) ANTISERA(15) ANTISTRESS(17) ANTSIER(14) ARENITES(15)\n", + " ARSENITE(15) ARSENITES(16) ARTINESS(15) ARTINESSES(17) ATTAINERS(16)\n", + " ENTERTAINERS(19) ENTERTAINS(17) ENTRAINERS(17) ENTRAINS(15) ENTREATIES(17)\n", + " ERRANTRIES(17) INERTIAS(15) INSTANTER(16) INTENERATES(18) INTERSTATE(17)\n", + " INTERSTATES(18) INTERSTRAIN(18) INTERSTRAINS(19) INTRASTATE(17) INTREATS(15)\n", + " IRATENESS(16) IRATENESSES(18) ITINERANTS(17) ITINERARIES(18) ITINERATES(17)\n", + " NASTIER(14) NITRATES(15) RAINIEST(15) RATANIES(15) RATINES(14) REATTAINS(16)\n", + " REINITIATES(18) REINSTATE(16) REINSTATES(17) RESINATE(15) RESINATES(16)\n", + " RESISTANT(16) RESISTANTS(17) RESTRAIN(15) RESTRAINER(17) RESTRAINERS(18)\n", + " RESTRAINS(16) RESTRAINT(16) RESTRAINTS(17) RETAINERS(16) RETAINS(14)\n", + " RETINAS(14) RETIRANTS(16) RETRAINS(15) RETSINA(14) RETSINAS(15)\n", + " SANITARIES(17) SEATRAIN(15) SEATRAINS(16) STAINER(14) STAINERS(15)\n", + " STANNARIES(17) STEARIN(14) STEARINE(15) STEARINES(16) STEARINS(15)\n", + " STRAINER(15) STRAINERS(16) STRAITEN(15) STRAITENS(16) STRAITNESS(17)\n", + " STRAITNESSES(19) TANISTRIES(17) TANNERIES(16) TEARSTAIN(16) TEARSTAINS(17)\n", + " TENANTRIES(17) TERNARIES(16) TERRAINS(15) TERTIANS(15) TRAINEES(15)\n", + " TRAINERS(15) TRANSIENT(16) TRANSIENTS(17) TRISTEARIN(17) TRISTEARINS(18)\n", + "AEINRS forms 16 words for 124 points:\n", + " AIRINESS(8) AIRINESSES(10) ANSERINE(8) ANSERINES(9) ARISEN(6) ARSINE(6)\n", + " ARSINES(7) INSANER(7) INSNARE(7) INSNARER(8) INSNARERS(9) INSNARES(8)\n", + " SENARII(7) SIERRAN(7) SIRENIAN(8) SIRENIANS(9)\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", + "AEINST forms 80 words for 713 points:\n", + " ANISETTE(8) ANISETTES(9) ANTISENSE(9) ANTISTATE(9) ANTSIEST(8)\n", + " ASININITIES(11) ASSASSINATE(11) ASSASSINATES(12) ASTATINE(8) ASTATINES(9)\n", + " ENTASIA(7) ENTASIAS(8) ENTASIS(7) ETESIAN(7) ETESIANS(8) INANEST(7)\n", + " INANITIES(9) INITIATES(9) INNATENESS(10) INNATENESSES(12) INSANEST(8)\n", + " INSANITIES(10) INSATIATE(9) INSATIATENESS(13) INSATIATENESSES(15)\n", + " INSENSATE(9) INSTANTANEITIES(15) INSTANTIATE(11) INSTANTIATES(12)\n", + " INSTANTNESS(11) INSTANTNESSES(13) INSTATE(7) INSTATES(8) INTESTATE(9)\n", + " INTESTATES(10) ISATINE(7) ISATINES(8) NASTIES(7) NASTIEST(8) NASTINESS(9)\n", + " NASTINESSES(11) NATTIEST(8) NATTINESS(9) NATTINESSES(11) SANITATE(8)\n", + " SANITATES(9) SANITIES(8) SANITISE(8) SANITISES(9) SATINET(7) SATINETS(8)\n", + " SENTENTIA(9) SENTENTIAE(10) SESTINA(7) SESTINAS(8) STANINE(7) STANINES(8)\n", + " STANNITE(8) STANNITES(9) TAENIAS(7) TAENIASES(9) TAENIASIS(9) TANSIES(7)\n", + " TASTINESS(9) TASTINESSES(11) TATTINESS(9) TATTINESSES(11) TENIAS(6)\n", + " TENIASES(8) TENIASIS(8) TETANIES(8) TETANISE(8) TETANISES(9) TINEAS(6)\n", + " TISANE(6) TISANES(7) TITANATES(9) TITANESS(8) TITANESSES(10) TITANITES(9)\n", + "AEIRST forms 60 words for 473 points:\n", + " AERIEST(7) AIREST(6) AIRIEST(7) ARIETTAS(8) ARIETTES(8) ARISTAE(7)\n", + " ARISTATE(8) ARTERIES(8) ARTERITIS(9) ARTIEST(7) ARTISTE(7) ARTISTES(8)\n", + " ARTISTRIES(10) ARTSIER(7) ARTSIEST(8) ASSISTER(8) ASSISTERS(9) ASTERIA(7)\n", + " ASTERIAS(8) ATRESIA(7) ATRESIAS(8) ATTIRES(7) EATERIES(8) IRATEST(7)\n", + " IRRITATES(9) ITERATES(8) RARITIES(8) RATITES(7) RATTIEST(8) REITERATES(10)\n", + " SATIRE(6) SATIRES(7) SATIRISE(8) SATIRISES(9) SERIATE(7) SERIATES(8)\n", + " SESTERTIA(9) STARRIER(8) STARRIEST(9) STRAITER(8) STRAITEST(9) STRIAE(6)\n", + " STRIATE(7) STRIATES(8) TARRIERS(8) TARRIES(7) TARRIEST(8) TARSIER(7)\n", + " TARSIERS(8) TASTIER(7) TEARIEST(8) TERAIS(6) TERTIARIES(10) TITRATES(8)\n", + " TRAITRESS(9) TRAITRESSES(11) TREATIES(8) TREATISE(8) TREATISES(9)\n", + " TRISTATE(8)\n", + "AENRST forms 40 words for 336 points:\n", + " ANTEATERS(9) ANTRES(6) ARRESTANT(9) ARRESTANTS(10) ARSENATE(8) ARSENATES(9)\n", + " ASSENTER(8) ASSENTERS(9) ASTERN(6) EARNEST(7) EARNESTNESS(11)\n", + " EARNESTNESSES(13) EARNESTS(8) EASTERN(7) EASTERNER(9) EASTERNERS(10)\n", + " ENTRANTS(8) ENTREATS(8) ERRANTS(7) NARRATERS(9) NARRATES(8) NATTERS(7)\n", + " NEAREST(7) RANTERS(7) RATTEENS(8) RATTENERS(9) RATTENS(7) REENTRANTS(10)\n", + " RETREATANTS(11) SARSENET(8) SARSENETS(9) SERENATA(8) SERENATAS(9)\n", + " SERENATE(8) STERNA(6) TANNERS(7) TARANTASES(10) TARTNESS(8) TARTNESSES(10)\n", + " TERRANES(8)\n", + "EINRST forms 70 words for 582 points:\n", + " ENTERITIS(9) ENTERITISES(11) ENTIRENESS(10) ENTIRENESSES(12) ENTIRES(7)\n", + " ENTIRETIES(10) ENTRIES(7) ESTRIN(6) ESTRINS(7) ETERNISE(8) ETERNISES(9)\n", + " ETERNITIES(10) INERTNESS(9) INERTNESSES(11) INERTS(6) INSERT(6) INSERTER(8)\n", + " INSERTERS(9) INSERTS(7) INSETTER(8) INSETTERS(9) INSISTER(8) INSISTERS(9)\n", + " INTENSER(8) INTEREST(8) INTERESTS(9) INTERNEES(9) INTERNES(8) INTERNIST(9)\n", + " INTERNISTS(10) INTERNS(7) INTERS(6) INTERTIES(9) NITERIES(8) NITERS(6)\n", + " NITRES(6) NITRITES(8) REENTRIES(9) REINSERT(8) REINSERTS(9) REINTERS(8)\n", + " RENTIERS(8) RETINENES(9) RETINES(7) RETINITES(9) RETINITIS(9) RETINTS(7)\n", + " SENTRIES(8) SERENITIES(10) SINISTER(8) SINISTERNESS(12) SINISTERNESSES(14)\n", + " SINTER(6) SINTERS(7) STERNITE(8) STERNITES(9) STINTER(7) STINTERS(8)\n", + " TEENSIER(8) TEENTSIER(9) TERRINES(8) TINNERS(7) TINTERS(7) TRIENES(7)\n", + " TRIENS(6) TRIENTES(8) TRINES(6) TRINITIES(9) TRITENESS(9) TRITENESSES(11)\n", + "AEINR forms 3 words for 19 points:\n", + " INANER(6) NARINE(6) RAINIER(7)\n", + "AEINS forms 17 words for 129 points:\n", + " ANISE(5) ANISES(6) ASININE(7) EASINESS(8) EASINESSES(10) INANENESS(9)\n", + " INANENESSES(11) INANES(6) INSANE(6) INSANENESS(10) INSANENESSES(12)\n", + " NANNIES(7) SANIES(6) SANSEI(6) SANSEIS(7) SIENNA(6) SIENNAS(7)\n", + "AEINT forms 10 words for 64 points:\n", + " ENTIA(5) INITIATE(8) INNATE(6) TAENIA(6) TAENIAE(7) TENIA(5) TENIAE(6)\n", + " TINEA(5) TITANATE(8) TITANITE(8)\n", + "AEIRS forms 17 words for 106 points:\n", + " AERIES(6) AIRERS(6) ARISE(5) ARISES(6) ARRISES(7) EASIER(6) RAISE(5)\n", + " RAISER(6) RAISERS(7) RAISES(6) RERAISE(7) RERAISES(8) SASSIER(7) SERAI(5)\n", + " SERAIS(6) SIERRA(6) SIERRAS(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", + "AEIST forms 15 words for 112 points:\n", + " EASIEST(7) ETATIST(7) SASSIEST(8) SATIATE(7) SATIATES(8) SATIETIES(9)\n", + " SIESTA(6) SIESTAS(7) STEATITE(8) STEATITES(9) TASSIE(6) TASSIES(7)\n", + " TASTIEST(8) TATTIES(7) TATTIEST(8)\n", + "AENRS forms 25 words for 172 points:\n", + " ANEARS(6) ARENAS(6) EARNERS(7) EARNS(5) ENSNARE(7) ENSNARER(8) ENSNARERS(9)\n", + " ENSNARES(8) NARES(5) NEARNESS(8) NEARNESSES(10) NEARS(5) RANEES(6)\n", + " RARENESS(8) RARENESSES(10) REEARNS(7) RENNASE(7) RENNASES(8) SANER(5)\n", + " SARSEN(6) SARSENS(7) SNARE(5) SNARER(6) SNARERS(7) SNARES(6)\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", + "AENST forms 32 words for 217 points:\n", + " ANATASE(7) ANATASES(8) ANENST(6) ANNATES(7) ANSATE(6) ANTENNAS(8) ANTES(5)\n", + " ASSENT(6) ASSENTS(7) ENATES(6) ENTASES(7) ETNAS(5) NATES(5) NEATENS(7)\n", + " NEATEST(7) NEATNESS(8) NEATNESSES(10) NEATS(5) SANEST(6) SATEEN(6)\n", + " SATEENS(7) SENATE(6) SENATES(7) SENSATE(7) SENSATES(8) SETENANT(8)\n", + " SETENANTS(9) STANE(5) STANES(6) TANNATES(8) TANNEST(7) TENANTS(7)\n", + "AERST forms 85 words for 604 points:\n", + " AERATES(7) ARETES(6) ARREST(6) ARRESTEE(8) ARRESTEES(9) ARRESTER(8)\n", + " ARRESTERS(9) ARRESTS(7) ASSERT(6) ASSERTER(8) ASSERTERS(9) ASSERTS(7)\n", + " ASTER(5) ASTERS(6) ATTESTER(8) ATTESTERS(9) EASTER(6) EASTERS(7) EATERS(6)\n", + " ERRATAS(7) ESTERASE(8) ESTERASES(9) ESTREAT(7) ESTREATS(8) RAREST(6)\n", + " RASTER(6) RASTERS(7) RATERS(6) RATES(5) RATTERS(7) REARREST(8) REARRESTS(9)\n", + " REASSERT(8) REASSERTS(9) REATAS(6) RESEAT(6) RESEATS(7) RESTART(7)\n", + " RESTARTS(8) RESTATE(7) RESTATES(8) RETASTE(7) RETASTES(8) RETEARS(7)\n", + " RETREATERS(10) RETREATS(8) SEAREST(7) SEATER(6) SEATERS(7) SERRATE(7)\n", + " SERRATES(8) STARE(5) STARER(6) STARERS(7) STARES(6) STARETS(7) STARTER(7)\n", + " STARTERS(8) STATER(6) STATERS(7) STEARATE(8) STEARATES(9) STRASSES(8)\n", + " STRETTA(7) STRETTAS(8) TARES(5) TARRES(6) TARTEST(7) TARTRATES(9) TASTER(6)\n", + " TASTERS(7) TATERS(6) TATTERS(7) TEARERS(7) TEARS(5) TEASER(6) TEASERS(7)\n", + " TERRAS(6) TERRASES(8) TESSERA(7) TESSERAE(8) TETRAS(6) TRASSES(7)\n", + " TREATERS(8) TREATS(6)\n", + "EINRS forms 29 words for 184 points:\n", + " EERINESS(8) EERINESSES(10) ESERINE(7) ESERINES(8) INNERS(6) NEREIS(6)\n", + " REINS(5) RENINS(6) RENNINS(7) RERISEN(7) RESIN(5) RESINS(6) RINSE(5)\n", + " RINSER(6) RINSERS(7) RINSES(6) RISEN(5) SEINER(6) SEINERS(7) SEREIN(6)\n", + " SEREINS(7) SERIN(5) SERINE(6) SERINES(7) SERINS(6) SINNER(6) SINNERS(7)\n", + " SIREN(5) SIRENS(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", + "EINST forms 58 words for 469 points:\n", + " EINSTEIN(8) EINSTEINS(9) ENTITIES(8) INSENTIENT(10) INSET(5) INSETS(6)\n", + " INSISTENT(9) INTENSE(7) INTENSENESS(11) INTENSENESSES(13) INTENSEST(9)\n", + " INTENSITIES(11) INTENTNESS(10) INTENTNESSES(12) INTENTS(7) INTESTINE(9)\n", + " INTESTINES(10) INTINES(7) NEIST(5) NETTIEST(8) NINETEENS(9) NINETIES(8)\n", + " NITES(5) NITTIEST(8) SENITI(6) SENNIT(6) SENNITS(7) SENSITISE(9)\n", + " SENSITISES(10) SENTI(5) SENTIENT(8) SENTIENTS(9) SESTINE(7) SESTINES(8)\n", + " SIENITE(7) SIENITES(8) SITTEN(6) STEIN(5) STEINS(6) TEENIEST(8) TEENSIEST(9)\n", + " TEENTSIEST(10) TENNIES(7) TENNIS(6) TENNISES(8) TENNIST(7) TENNISTS(8)\n", + " TENSITIES(9) TENTIEST(8) TESTINESS(9) TESTINESSES(11) TINES(5) TINIEST(7)\n", + " TININESS(8) TININESSES(10) TINNIEST(8) TINNINESS(9) TINNINESSES(11)\n", + "EIRST forms 38 words for 262 points:\n", + " EERIEST(7) IRITISES(8) RESIST(6) RESISTER(8) RESISTERS(9) RESISTS(7)\n", + " RESITE(6) RESITES(7) RETIES(6) RETIREES(8) RETIRERS(8) RETIRES(7) RETRIES(7)\n", + " RITES(5) RITTERS(7) SISTER(6) SISTERS(7) SITTER(6) SITTERS(7) STIRRER(7)\n", + " STIRRERS(8) STRETTI(7) TERRIERS(8) TERRIES(7) TERRITS(7) TESTIER(7) TIERS(5)\n", + " TIRES(5) TITERS(6) TITRES(6) TITTERERS(9) TITTERS(7) TRESSIER(8)\n", + " TRESSIEST(9) TRIERS(6) TRIES(5) TRISTE(6) TRITEST(7)\n", + "ENRST forms 35 words for 246 points:\n", + " ENTERERS(8) ENTERS(6) ENTREES(7) NERTS(5) NESTER(6) NESTERS(7) NETTERS(7)\n", + " REENTERS(8) RENEST(6) RENESTS(7) RENNETS(7) RENTERS(7) RENTES(6) RENTS(5)\n", + " RESENT(6) RESENTS(7) RETENES(7) SERENEST(8) STERN(5) STERNER(7) STERNEST(8)\n", + " STERNNESS(9) STERNNESSES(11) STERNS(6) TEENERS(7) TENNERS(7) TENSER(6)\n", + " TENTERS(7) TERNES(6) TERNS(5) TERREENS(8) TERRENES(8) TERSENESS(9)\n", + " TERSENESSES(11) TREENS(6)\n", + "AEIN forms 2 words for 11 points:\n", + " INANE(5) NANNIE(6)\n", + "AEIR forms 4 words for 22 points:\n", + " AERIE(5) AERIER(6) AIRER(5) AIRIER(6)\n", + "AEIS forms 2 words for 13 points:\n", + " EASIES(6) SASSIES(7)\n", + "AEIT forms 1 words for 6 points:\n", + " TATTIE(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", + "AENS forms 9 words for 46 points:\n", + " ANES(1) ANSAE(5) SANE(1) SANENESS(8) SANENESSES(10) SANES(5) SENNA(5)\n", + " SENNAS(6) SENSA(5)\n", + "AENT forms 13 words for 63 points:\n", + " ANENT(5) ANTAE(5) ANTE(1) ANTENNA(7) ANTENNAE(8) ATTENT(6) EATEN(5) ENATE(5)\n", + " ETNA(1) NEAT(1) NEATEN(6) TANNATE(7) TENANT(6)\n", + "AERS forms 26 words for 121 points:\n", + " AREAS(5) ARES(1) ARREARS(7) ARSE(1) ARSES(5) EARS(1) ERAS(1) ERASE(5)\n", + " ERASER(6) ERASERS(7) ERASES(6) RARES(5) RASE(1) RASER(5) RASERS(6) RASES(5)\n", + " REARERS(7) REARS(5) REASSESS(8) REASSESSES(10) SAREE(5) SAREES(6) SEAR(1)\n", + " SEARER(6) SEARS(5) SERA(1)\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", + "AEST forms 35 words for 164 points:\n", + " ASSET(5) ASSETS(6) ATES(1) ATTEST(6) ATTESTS(7) EAST(1) EASTS(5) EATS(1)\n", + " ESTATE(6) ESTATES(7) ETAS(1) SATE(1) SATES(5) SEAT(1) SEATS(5) SETA(1)\n", + " SETAE(5) STASES(6) STATE(5) STATES(6) TASSE(5) TASSES(6) TASSET(6)\n", + " TASSETS(7) TASTE(5) TASTES(6) TATES(5) TEAS(1) TEASE(5) TEASES(6) TEATS(5)\n", + " TESTA(5) TESTAE(6) TESTATE(7) TESTATES(8)\n", + "EINR forms 4 words for 17 points:\n", + " INNER(5) REIN(1) RENIN(5) RENNIN(6)\n", + "EINS forms 10 words for 53 points:\n", + " NINES(5) NINNIES(7) NISEI(5) NISEIS(6) SEINE(5) SEINES(6) SEISIN(6)\n", + " SEISINS(7) SINE(1) SINES(5)\n", + "EINT forms 6 words for 28 points:\n", + " INTENT(6) INTINE(6) NINETEEN(8) NITE(1) TENTIE(6) TINE(1)\n", + "EIRS forms 20 words for 101 points:\n", + " IRES(1) IRISES(6) REIS(1) RERISE(6) RERISES(7) RISE(1) RISER(5) RISERS(6)\n", + " RISES(5) SEISER(6) SEISERS(7) SERIES(6) SERRIES(7) SIRE(1) SIREE(5)\n", + " SIREES(6) SIRES(5) SIRREE(6) SIRREES(7) SISSIER(7)\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", + "EIST forms 8 words for 41 points:\n", + " SISSIEST(8) SITE(1) SITES(5) STIES(5) TESTIEST(8) TESTIS(6) TIES(1)\n", + " TITTIES(7)\n", + "ENRS forms 12 words for 80 points:\n", + " ERNES(5) ERNS(1) RESEEN(6) SERENE(6) SERENENESS(10) SERENENESSES(12)\n", + " SERENER(7) SERENES(7) SNEER(5) SNEERER(7) SNEERERS(8) SNEERS(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", + "ENST forms 18 words for 94 points:\n", + " ENTENTES(8) NEST(1) NESTS(5) NETS(1) NETTS(5) SENNET(6) SENNETS(7) SENT(1)\n", + " SENTE(5) TEENS(5) TENETS(6) TENS(1) TENSE(5) TENSENESS(9) TENSENESSES(11)\n", + " TENSES(6) TENSEST(7) TENTS(5)\n", + "ERST forms 44 words for 266 points:\n", + " ERST(1) ESTER(5) ESTERS(6) REEST(5) REESTS(6) RESET(5) RESETS(6) RESETTER(8)\n", + " RESETTERS(9) REST(1) RESTER(6) RESTERS(7) RESTRESS(8) RESTRESSES(10)\n", + " RESTS(5) RETEST(6) RETESTS(7) RETS(1) SEREST(6) SETTER(6) SETTERS(7)\n", + " STEER(5) STEERER(7) STEERERS(8) STEERS(6) STERE(5) STERES(6) STREET(6)\n", + " STREETS(7) STRESS(6) STRESSES(8) STRETTE(7) TEETERS(7) TERRETS(7) TERSE(5)\n", + " TERSER(6) TERSEST(7) TESTER(6) TESTERS(7) TETTERS(7) TREES(5) TRESS(5)\n", + " TRESSES(7) TRETS(5)\n", + "AER forms 7 words for 25 points:\n", + " AREA(1) AREAE(5) ARREAR(6) RARE(1) RARER(5) REAR(1) REARER(6)\n", + "AES forms 8 words for 33 points:\n", + " ASEA(1) ASSES(5) ASSESS(6) ASSESSES(8) EASE(1) EASES(5) SASSES(6) SEAS(1)\n", + "AET forms 2 words for 2 points:\n", + " TATE(1) TEAT(1)\n", + "EIN forms 1 words for 1 points:\n", + " NINE(1)\n", + "EIR forms 2 words for 11 points:\n", + " EERIE(5) EERIER(6)\n", + "EIS forms 7 words for 35 points:\n", + " ISSEI(5) ISSEIS(6) SEIS(1) SEISE(5) SEISES(6) SISES(5) SISSIES(7)\n", + "EIT forms 1 words for 6 points:\n", + " TITTIE(6)\n", + "ENR forms 1 words for 1 points:\n", + " ERNE(1)\n", + "ENS forms 6 words for 20 points:\n", + " NESS(1) NESSES(6) SEEN(1) SENE(1) SENSE(5) SENSES(6)\n", + "ENT forms 5 words for 15 points:\n", + " ENTENTE(7) NETT(1) TEEN(1) TENET(5) TENT(1)\n", + "ERS forms 13 words for 52 points:\n", + " ERRS(1) ERSES(5) REES(1) RESEE(5) RESEES(6) SEER(1) SEERESS(7) SEERESSES(9)\n", + " SEERS(5) SERE(1) SERER(5) SERES(5) SERS(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", + "EST forms 18 words for 79 points:\n", + " SESTET(6) SESTETS(7) SETS(1) SETT(1) SETTEE(6) SETTEES(7) SETTS(5) STET(1)\n", + " STETS(5) TEES(1) TEST(1) TESTEE(6) TESTEES(7) TESTES(6) TESTS(5) TETS(1)\n", + " TSETSE(6) TSETSES(7)\n", + "EN forms 1 words for 1 points:\n", + " NENE(1)\n", + "ES forms 3 words for 7 points:\n", + " ESES(1) ESSES(5) SEES(1)\n" + ] + } + ], + "source": [ + "report(S_words(open('enable1.txt').read()))" + ] } ], "metadata": {