├── figures ├── temp.png ├── word_pdf.png ├── lett_freq.png ├── absurdle_solution.png ├── word_pdf_stump_new.png ├── word_pdf_stump_edited.png ├── absurdle_hard_mode_solution.png ├── common_word_strat_comparison.png ├── common_word_strat_comparison_hard.png ├── strat_using_any_guess_prioritize_size.png ├── strat_using_any_guess_prioritize_entropy.png ├── strat_using_any_guess_prioritize_splits.png ├── strat_using_solutions_only_prioritize_size.png ├── strat_using_solutions_only_hard_mode_optimal.png ├── strat_using_solutions_only_prioritize_entropy.png ├── strat_using_solutions_only_prioritize_splits.png ├── strat_using_hard_mode_any_guess_prioritize_size.png ├── strat_using_hard_mode_any_guess_prioritize_entropy.png ├── strat_using_hard_mode_any_guess_prioritize_splits.png ├── strat_using_hard_mode_solutions_only_prioritize_size.png ├── strat_using_hard_mode_solutions_only_prioritize_entropy.png └── strat_using_hard_mode_solutions_only_prioritize_splits.png ├── work.jl ├── .ipynb_checkpoints ├── work-checkpoint.jl ├── wordle_solver-checkpoint.ipynb ├── solutions_nyt-checkpoint.txt ├── solutions-checkpoint.txt ├── utils-checkpoint.jl └── hard_mode_exhaustive_search-checkpoint.ipynb ├── .gitignore ├── README.md ├── wordle_solver.ipynb ├── solutions_nyt.txt ├── solutions.txt ├── utils.jl └── hard_mode_exhaustive_search.ipynb /figures/temp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurentLessard/wordlesolver/HEAD/figures/temp.png -------------------------------------------------------------------------------- /figures/word_pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurentLessard/wordlesolver/HEAD/figures/word_pdf.png -------------------------------------------------------------------------------- /figures/lett_freq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurentLessard/wordlesolver/HEAD/figures/lett_freq.png -------------------------------------------------------------------------------- /figures/absurdle_solution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurentLessard/wordlesolver/HEAD/figures/absurdle_solution.png -------------------------------------------------------------------------------- /figures/word_pdf_stump_new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurentLessard/wordlesolver/HEAD/figures/word_pdf_stump_new.png -------------------------------------------------------------------------------- /figures/word_pdf_stump_edited.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurentLessard/wordlesolver/HEAD/figures/word_pdf_stump_edited.png -------------------------------------------------------------------------------- /figures/absurdle_hard_mode_solution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurentLessard/wordlesolver/HEAD/figures/absurdle_hard_mode_solution.png -------------------------------------------------------------------------------- /figures/common_word_strat_comparison.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurentLessard/wordlesolver/HEAD/figures/common_word_strat_comparison.png -------------------------------------------------------------------------------- /figures/common_word_strat_comparison_hard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurentLessard/wordlesolver/HEAD/figures/common_word_strat_comparison_hard.png -------------------------------------------------------------------------------- /figures/strat_using_any_guess_prioritize_size.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurentLessard/wordlesolver/HEAD/figures/strat_using_any_guess_prioritize_size.png -------------------------------------------------------------------------------- /figures/strat_using_any_guess_prioritize_entropy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurentLessard/wordlesolver/HEAD/figures/strat_using_any_guess_prioritize_entropy.png -------------------------------------------------------------------------------- /figures/strat_using_any_guess_prioritize_splits.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurentLessard/wordlesolver/HEAD/figures/strat_using_any_guess_prioritize_splits.png -------------------------------------------------------------------------------- /figures/strat_using_solutions_only_prioritize_size.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurentLessard/wordlesolver/HEAD/figures/strat_using_solutions_only_prioritize_size.png -------------------------------------------------------------------------------- /figures/strat_using_solutions_only_hard_mode_optimal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurentLessard/wordlesolver/HEAD/figures/strat_using_solutions_only_hard_mode_optimal.png -------------------------------------------------------------------------------- /figures/strat_using_solutions_only_prioritize_entropy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurentLessard/wordlesolver/HEAD/figures/strat_using_solutions_only_prioritize_entropy.png -------------------------------------------------------------------------------- /figures/strat_using_solutions_only_prioritize_splits.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurentLessard/wordlesolver/HEAD/figures/strat_using_solutions_only_prioritize_splits.png -------------------------------------------------------------------------------- /figures/strat_using_hard_mode_any_guess_prioritize_size.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurentLessard/wordlesolver/HEAD/figures/strat_using_hard_mode_any_guess_prioritize_size.png -------------------------------------------------------------------------------- /figures/strat_using_hard_mode_any_guess_prioritize_entropy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurentLessard/wordlesolver/HEAD/figures/strat_using_hard_mode_any_guess_prioritize_entropy.png -------------------------------------------------------------------------------- /figures/strat_using_hard_mode_any_guess_prioritize_splits.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurentLessard/wordlesolver/HEAD/figures/strat_using_hard_mode_any_guess_prioritize_splits.png -------------------------------------------------------------------------------- /figures/strat_using_hard_mode_solutions_only_prioritize_size.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurentLessard/wordlesolver/HEAD/figures/strat_using_hard_mode_solutions_only_prioritize_size.png -------------------------------------------------------------------------------- /figures/strat_using_hard_mode_solutions_only_prioritize_entropy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurentLessard/wordlesolver/HEAD/figures/strat_using_hard_mode_solutions_only_prioritize_entropy.png -------------------------------------------------------------------------------- /figures/strat_using_hard_mode_solutions_only_prioritize_splits.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurentLessard/wordlesolver/HEAD/figures/strat_using_hard_mode_solutions_only_prioritize_splits.png -------------------------------------------------------------------------------- /work.jl: -------------------------------------------------------------------------------- 1 | include("utils.jl") 2 | 3 | # This script searches whether using the starting word with index ARGS[1] can lead to a guaranteed win in 4 moves or fewer. 4 | # If successful, this returns the explicit strategy. Otherwise, it returns "no solution" 5 | # Takes about 25 min. to run on a laptop for each starting word. Can be massively parallelized. 6 | 7 | cache_word_scores(ALL_WORDS, SOLUTION_WORDS) 8 | optimize_max_num_shards() 9 | 10 | initial_guess = ARGS[1] 11 | 12 | r = get_optimal_strategy_exhaustive_helper( 13 | ALL_WORD_IDXS[1:20], 14 | SOLUTION_WORD_IDXS[1:20], 15 | initial_guess, 16 | hard_mode = false, 17 | turns_budget = 4 18 | ) 19 | if isnothing(r) 20 | println(ALL_WORDS[initial_guess], ": no solution") 21 | else 22 | best_num_turns, best_strat = r 23 | print_strategy(initial_guess, best_strat) 24 | end -------------------------------------------------------------------------------- /.ipynb_checkpoints/work-checkpoint.jl: -------------------------------------------------------------------------------- 1 | include("utils.jl") 2 | 3 | # This script searches whether using the starting word with index ARGS[1] can lead to a guaranteed win in 4 moves or fewer. 4 | # If successful, this returns the explicit strategy. Otherwise, it returns "no solution" 5 | # Takes about 25 min. to run on a laptop for each starting word. Can be massively parallelized. 6 | 7 | cache_word_scores(ALL_WORDS, SOLUTION_WORDS) 8 | optimize_max_num_shards() 9 | 10 | initial_guess = ARGS[1] 11 | 12 | r = get_optimal_strategy_exhaustive_helper( 13 | ALL_WORD_IDXS[1:20], 14 | SOLUTION_WORD_IDXS[1:20], 15 | initial_guess, 16 | hard_mode = false, 17 | turns_budget = 4 18 | ) 19 | if isnothing(r) 20 | println(ALL_WORDS[initial_guess], ": no solution") 21 | else 22 | best_num_turns, best_strat = r 23 | print_strategy(initial_guess, best_strat) 24 | end -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files generated by invoking Julia with --code-coverage 2 | *.jl.cov 3 | *.jl.*.cov 4 | 5 | # Files generated by invoking Julia with --track-allocation 6 | *.jl.mem 7 | 8 | # System-specific files and directories generated by the BinaryProvider and BinDeps packages 9 | # They contain absolute paths specific to the host computer, and so should not be committed 10 | deps/deps.jl 11 | deps/build.log 12 | deps/downloads/ 13 | deps/usr/ 14 | deps/src/ 15 | 16 | # Build artifacts for creating documentation generated by the Documenter package 17 | docs/build/ 18 | docs/site/ 19 | 20 | # File generated by Pkg, the package manager, based on a corresponding Project.toml 21 | # It records a fixed state of all packages used by the project. As such, it should not be 22 | # committed for packages, but should be committed for applications that require a static 23 | # environment. 24 | Manifest.toml 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a simple [Wordle](https://www.nytimes.com/games/wordle/index.html) solver written in [Julia](https://julialang.org/). For an in-depth analysis and discussion of the findings, please see the [accompanying blog post](https://laurentlessard.com/solving-wordle/). 2 | 3 | ## Description of files 4 | 5 | ### Word lists 6 | - `solutions.txt` is the set of words that might appear as solutions to the puzzle. This list contains 2315 words. 7 | - `nonsolutions.txt` is the set of additional words that can be used as guesses but will never appear as solutions. Contains 10657 words. 8 | - `solutions_nyt.txt` is the updated list of solution words (the list changed after Wordle was acquired by the New York Times). The updated list contains 2309 words. 9 | - `nonsolutions_nyt.txt` is the updated list of additional words that can be used as guesses but will never appear as solutions. Contains 10638 words. 10 | 11 | ### Scripts 12 | - `utils.jl` contains all the helper functions. 13 | - `work.jl` is a script that determines whether a given start word admits a strategy guaranteed to always win in 4 moves or fewer. Takes about 25 minutes to run for each word. This script was run in parallel on a multi-core machine to show that no 4-move strategy exists for Wordle. 14 | 15 | ### Notebooks 16 | - `wordle_solver.ipynb` is an interactive solver. You can specify your guess words and the responses, and compute optimal next moves based on several different greedy heuristics. 17 | - `performance.ipynb` creates several figures (saved in the [figures] subfolder) that analyze the performance of different heuristics. Also generates the other figures used in the [blog post](https://laurentlessard.com/solving-wordle/). 18 | - `hard_mode_exhaustive_search.ipynb` performs an exhaustive search of all hard-mode strategies and finds one that is guaranteed to solve in 5 moves or fewer using only common words. 19 | 20 | ### Strategies 21 | - `hard_mode_strategy.md` contains the full strategy found using the hard mode exhaustive search mentioned above, starting with the word SCAMP. 22 | 23 | -------------------------------------------------------------------------------- /wordle_solver.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "8ce4c147-7bba-45eb-a398-f748ed7000f0", 6 | "metadata": {}, 7 | "source": [ 8 | "# Wordle solver\n", 9 | "by [Laurent Lessard](https://laurentlessard.com)\n", 10 | "\n", 11 | "This is a solver for the popular [Wordle](https://www.powerlanguage.co.uk/wordle/) game.\n", 12 | "Run all the cells in order. When you get to \"Play the game\", follow the instructions to input the game responses." 13 | ] 14 | }, 15 | { 16 | "cell_type": "markdown", 17 | "id": "1dbbd7f9-f458-4bed-a680-56f0f1befa7d", 18 | "metadata": {}, 19 | "source": [ 20 | "### Import Utils" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": 5, 26 | "id": "e1d5c736-e8fb-48ec-9c34-9a8d995711e6", 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "include(\"utils.jl\");" 31 | ] 32 | }, 33 | { 34 | "cell_type": "markdown", 35 | "id": "d4396374-5339-49ce-ac73-c5d010f3daae", 36 | "metadata": {}, 37 | "source": [ 38 | "### Find best starting word" 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": 6, 44 | "id": "b582e316-198a-4702-8158-e9b7dc8860a0", 45 | "metadata": {}, 46 | "outputs": [ 47 | { 48 | "name": "stdout", 49 | "output_type": "stream", 50 | "text": [ 51 | " 13.693640 seconds (120.06 M allocations: 13.441 GiB, 8.99% gc time, 1.08% compilation time)\n" 52 | ] 53 | }, 54 | { 55 | "data": { 56 | "text/plain": [ 57 | "\"trace\"" 58 | ] 59 | }, 60 | "execution_count": 6, 61 | "metadata": {}, 62 | "output_type": "execute_result" 63 | } 64 | ], 65 | "source": [ 66 | "# find the best first move according to a given heuristic\n", 67 | "@time begin\n", 68 | " first_guess = find_move(ALL_WORDS, SOLUTION_WORDS, heuristic=PRIORITIZE_SPLITS)\n", 69 | "end" 70 | ] 71 | }, 72 | { 73 | "cell_type": "markdown", 74 | "id": "6ff42ed3-3bb0-42f5-b96f-8244e64292b3", 75 | "metadata": {}, 76 | "source": [ 77 | "# Play the game!\n", 78 | "Run each cell in sequence, filling in `response = \".....\"`\n", 79 | "with the response from the wordle website. Use the format `\"01020\"`, where\n", 80 | "- `0` = empty square\n", 81 | "- `1` = yellow square\n", 82 | "- `2` = green square\n", 83 | "\n", 84 | "You can also customize the game mode and heuristic by changing how the `find_move` function is called:\n", 85 | "1. To search over all possible words, use `find_move(ALL_WORDS, pool)`\n", 86 | "2. To only search over common words, use `find_move(SOLUTION_WORDS, pool)`\n", 87 | "3. To play in hard mode (only remaining valid words are admissible), use `find_move(pool, pool)`\n", 88 | "\n", 89 | "Finally, you can choose which heuristic to use by adding a `heuristic=...` argument to the `find_move` function call. Options are:\n", 90 | "1. `PRIORITIZE_ENTROPY` (maximize entropy of ensuing distribution of words, this is the default)\n", 91 | "2. `PRIORITIZE_MAX_GROUP_SIZE` (minimize the word group of largest size, a greedy worst-case approach)\n", 92 | "3. `PRIORITIZE_SPLITS` (maximize the support of the ensuing distribution of words)\n", 93 | "\n", 94 | "For example, writing `next_guess = find_move(ALL_WORDS, pool, heuristic=PRIORITIZE_MAX_GROUP_SIZE)` will pick the next move from the word list of all available words, by maximizing the worst case scenario. This reduces the size of the valid solution pool by as much as possible in the worst case, which works well if you are playing against an adversarial opponent such as [Absurdle](https://qntm.org/files/wordle/index.html)." 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": 335, 100 | "id": "b35b759c-1894-483e-ae9b-366f28fa5cfa", 101 | "metadata": {}, 102 | "outputs": [ 103 | { 104 | "name": "stdout", 105 | "output_type": "stream", 106 | "text": [ 107 | "FIRST MOVE: raise\n" 108 | ] 109 | } 110 | ], 111 | "source": [ 112 | "pool = SOLUTION_WORDS\n", 113 | "next_guess = first_guess\n", 114 | "println(\"FIRST MOVE: \", next_guess)" 115 | ] 116 | }, 117 | { 118 | "cell_type": "code", 119 | "execution_count": 336, 120 | "id": "856797cc-1223-4731-8479-ad8cc0b3e174", 121 | "metadata": {}, 122 | "outputs": [ 123 | { 124 | "name": "stdout", 125 | "output_type": "stream", 126 | "text": [ 127 | "list of possible solutions (24): shrub, story, scour, shrug, usurp, scorn, sorry, sport, strut, stork, sworn, storm, scrub, short, slurp, spurn, snort, syrup, sword, spurt, surly, torus, scrum, shorn\n", 128 | "NEXT MOVE: uncut\n" 129 | ] 130 | }, 131 | { 132 | "data": { 133 | "text/plain": [ 134 | "\"stern\"" 135 | ] 136 | }, 137 | "execution_count": 336, 138 | "metadata": {}, 139 | "output_type": "execute_result" 140 | } 141 | ], 142 | "source": [ 143 | "response = \"10010\"\n", 144 | "pool = trim_pool(next_guess, response, pool)\n", 145 | "println(\"list of possible solutions ($(length(pool))): \", join(pool, \", \"))\n", 146 | "next_guess = find_move(ALL_WORDS, pool, heuristic=PRIORITIZE_MAX_GROUP_SIZE)\n", 147 | "println(\"NEXT MOVE: \", next_guess)\n", 148 | "next_guess = \"stern\"" 149 | ] 150 | }, 151 | { 152 | "cell_type": "code", 153 | "execution_count": 337, 154 | "id": "89945f87-e569-47ab-9692-94f745152a7d", 155 | "metadata": {}, 156 | "outputs": [ 157 | { 158 | "name": "stdout", 159 | "output_type": "stream", 160 | "text": [ 161 | "list of possible solutions (3): story, stork, storm\n", 162 | "NEXT MOVE: karma\n" 163 | ] 164 | } 165 | ], 166 | "source": [ 167 | "response = \"22020\"\n", 168 | "pool = trim_pool(next_guess, response, pool)\n", 169 | "println(\"list of possible solutions ($(length(pool))): \", join(pool, \", \"))\n", 170 | "next_guess = find_move(ALL_WORDS, pool)\n", 171 | "println(\"NEXT MOVE: \", next_guess)" 172 | ] 173 | }, 174 | { 175 | "cell_type": "code", 176 | "execution_count": 338, 177 | "id": "42b66f4b-7405-4c4e-afbf-d3fc88846d99", 178 | "metadata": {}, 179 | "outputs": [ 180 | { 181 | "name": "stdout", 182 | "output_type": "stream", 183 | "text": [ 184 | "list of possible solutions (1): storm\n", 185 | "NEXT MOVE: storm\n" 186 | ] 187 | } 188 | ], 189 | "source": [ 190 | "response = \"00110\"\n", 191 | "pool = trim_pool(next_guess, response, pool)\n", 192 | "println(\"list of possible solutions ($(length(pool))): \", join(pool, \", \"))\n", 193 | "next_guess = find_move(ALL_WORDS, pool)\n", 194 | "println(\"NEXT MOVE: \", next_guess)" 195 | ] 196 | }, 197 | { 198 | "cell_type": "code", 199 | "execution_count": 339, 200 | "id": "ee962ba3-fd86-48f2-8752-e22d3ac794a1", 201 | "metadata": { 202 | "collapsed": true, 203 | "jupyter": { 204 | "outputs_hidden": true 205 | }, 206 | "tags": [] 207 | }, 208 | "outputs": [ 209 | { 210 | "name": "stdout", 211 | "output_type": "stream", 212 | "text": [ 213 | "Unexpected response; skipping ...\n", 214 | "list of possible solutions (1): storm\n", 215 | "NEXT MOVE: storm\n" 216 | ] 217 | } 218 | ], 219 | "source": [ 220 | "response = \".....\"\n", 221 | "pool = trim_pool(next_guess, response, pool)\n", 222 | "println(\"list of possible solutions ($(length(pool))): \", join(pool, \", \"))\n", 223 | "next_guess = find_move(ALL_WORDS, pool)\n", 224 | "println(\"NEXT MOVE: \", next_guess)" 225 | ] 226 | } 227 | ], 228 | "metadata": { 229 | "kernelspec": { 230 | "display_name": "Julia 1.7.1", 231 | "language": "julia", 232 | "name": "julia-1.7" 233 | }, 234 | "language_info": { 235 | "file_extension": ".jl", 236 | "mimetype": "application/julia", 237 | "name": "julia", 238 | "version": "1.7.1" 239 | } 240 | }, 241 | "nbformat": 4, 242 | "nbformat_minor": 5 243 | } 244 | -------------------------------------------------------------------------------- /.ipynb_checkpoints/wordle_solver-checkpoint.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "8ce4c147-7bba-45eb-a398-f748ed7000f0", 6 | "metadata": {}, 7 | "source": [ 8 | "# Wordle solver\n", 9 | "by [Laurent Lessard](https://laurentlessard.com)\n", 10 | "\n", 11 | "This is a solver for the popular [Wordle](https://www.powerlanguage.co.uk/wordle/) game.\n", 12 | "Run all the cells in order. When you get to \"Play the game\", follow the instructions to input the game responses." 13 | ] 14 | }, 15 | { 16 | "cell_type": "markdown", 17 | "id": "1dbbd7f9-f458-4bed-a680-56f0f1befa7d", 18 | "metadata": {}, 19 | "source": [ 20 | "### Import Utils" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": 5, 26 | "id": "e1d5c736-e8fb-48ec-9c34-9a8d995711e6", 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "include(\"utils.jl\");" 31 | ] 32 | }, 33 | { 34 | "cell_type": "markdown", 35 | "id": "d4396374-5339-49ce-ac73-c5d010f3daae", 36 | "metadata": {}, 37 | "source": [ 38 | "### Find best starting word" 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": 6, 44 | "id": "b582e316-198a-4702-8158-e9b7dc8860a0", 45 | "metadata": {}, 46 | "outputs": [ 47 | { 48 | "name": "stdout", 49 | "output_type": "stream", 50 | "text": [ 51 | " 13.693640 seconds (120.06 M allocations: 13.441 GiB, 8.99% gc time, 1.08% compilation time)\n" 52 | ] 53 | }, 54 | { 55 | "data": { 56 | "text/plain": [ 57 | "\"trace\"" 58 | ] 59 | }, 60 | "execution_count": 6, 61 | "metadata": {}, 62 | "output_type": "execute_result" 63 | } 64 | ], 65 | "source": [ 66 | "# find the best first move according to a given heuristic\n", 67 | "@time begin\n", 68 | " first_guess = find_move(ALL_WORDS, SOLUTION_WORDS, heuristic=PRIORITIZE_SPLITS)\n", 69 | "end" 70 | ] 71 | }, 72 | { 73 | "cell_type": "markdown", 74 | "id": "6ff42ed3-3bb0-42f5-b96f-8244e64292b3", 75 | "metadata": {}, 76 | "source": [ 77 | "# Play the game!\n", 78 | "Run each cell in sequence, filling in `response = \".....\"`\n", 79 | "with the response from the wordle website. Use the format `\"01020\"`, where\n", 80 | "- `0` = empty square\n", 81 | "- `1` = yellow square\n", 82 | "- `2` = green square\n", 83 | "\n", 84 | "You can also customize the game mode and heuristic by changing how the `find_move` function is called:\n", 85 | "1. To search over all possible words, use `find_move(ALL_WORDS, pool)`\n", 86 | "2. To only search over common words, use `find_move(SOLUTION_WORDS, pool)`\n", 87 | "3. To play in hard mode (only remaining valid words are admissible), use `find_move(pool, pool)`\n", 88 | "\n", 89 | "Finally, you can choose which heuristic to use by adding a `heuristic=...` argument to the `find_move` function call. Options are:\n", 90 | "1. `PRIORITIZE_ENTROPY` (maximize entropy of ensuing distribution of words, this is the default)\n", 91 | "2. `PRIORITIZE_MAX_GROUP_SIZE` (minimize the word group of largest size, a greedy worst-case approach)\n", 92 | "3. `PRIORITIZE_SPLITS` (maximize the support of the ensuing distribution of words)\n", 93 | "\n", 94 | "For example, writing `next_guess = find_move(ALL_WORDS, pool, heuristic=PRIORITIZE_MAX_GROUP_SIZE)` will pick the next move from the word list of all available words, by maximizing the worst case scenario. This reduces the size of the valid solution pool by as much as possible in the worst case, which works well if you are playing against an adversarial opponent such as [Absurdle](https://qntm.org/files/wordle/index.html)." 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": 335, 100 | "id": "b35b759c-1894-483e-ae9b-366f28fa5cfa", 101 | "metadata": {}, 102 | "outputs": [ 103 | { 104 | "name": "stdout", 105 | "output_type": "stream", 106 | "text": [ 107 | "FIRST MOVE: raise\n" 108 | ] 109 | } 110 | ], 111 | "source": [ 112 | "pool = SOLUTION_WORDS\n", 113 | "next_guess = first_guess\n", 114 | "println(\"FIRST MOVE: \", next_guess)" 115 | ] 116 | }, 117 | { 118 | "cell_type": "code", 119 | "execution_count": 336, 120 | "id": "856797cc-1223-4731-8479-ad8cc0b3e174", 121 | "metadata": {}, 122 | "outputs": [ 123 | { 124 | "name": "stdout", 125 | "output_type": "stream", 126 | "text": [ 127 | "list of possible solutions (24): shrub, story, scour, shrug, usurp, scorn, sorry, sport, strut, stork, sworn, storm, scrub, short, slurp, spurn, snort, syrup, sword, spurt, surly, torus, scrum, shorn\n", 128 | "NEXT MOVE: uncut\n" 129 | ] 130 | }, 131 | { 132 | "data": { 133 | "text/plain": [ 134 | "\"stern\"" 135 | ] 136 | }, 137 | "execution_count": 336, 138 | "metadata": {}, 139 | "output_type": "execute_result" 140 | } 141 | ], 142 | "source": [ 143 | "response = \"10010\"\n", 144 | "pool = trim_pool(next_guess, response, pool)\n", 145 | "println(\"list of possible solutions ($(length(pool))): \", join(pool, \", \"))\n", 146 | "next_guess = find_move(ALL_WORDS, pool, heuristic=PRIORITIZE_MAX_GROUP_SIZE)\n", 147 | "println(\"NEXT MOVE: \", next_guess)\n", 148 | "next_guess = \"stern\"" 149 | ] 150 | }, 151 | { 152 | "cell_type": "code", 153 | "execution_count": 337, 154 | "id": "89945f87-e569-47ab-9692-94f745152a7d", 155 | "metadata": {}, 156 | "outputs": [ 157 | { 158 | "name": "stdout", 159 | "output_type": "stream", 160 | "text": [ 161 | "list of possible solutions (3): story, stork, storm\n", 162 | "NEXT MOVE: karma\n" 163 | ] 164 | } 165 | ], 166 | "source": [ 167 | "response = \"22020\"\n", 168 | "pool = trim_pool(next_guess, response, pool)\n", 169 | "println(\"list of possible solutions ($(length(pool))): \", join(pool, \", \"))\n", 170 | "next_guess = find_move(ALL_WORDS, pool)\n", 171 | "println(\"NEXT MOVE: \", next_guess)" 172 | ] 173 | }, 174 | { 175 | "cell_type": "code", 176 | "execution_count": 338, 177 | "id": "42b66f4b-7405-4c4e-afbf-d3fc88846d99", 178 | "metadata": {}, 179 | "outputs": [ 180 | { 181 | "name": "stdout", 182 | "output_type": "stream", 183 | "text": [ 184 | "list of possible solutions (1): storm\n", 185 | "NEXT MOVE: storm\n" 186 | ] 187 | } 188 | ], 189 | "source": [ 190 | "response = \"00110\"\n", 191 | "pool = trim_pool(next_guess, response, pool)\n", 192 | "println(\"list of possible solutions ($(length(pool))): \", join(pool, \", \"))\n", 193 | "next_guess = find_move(ALL_WORDS, pool)\n", 194 | "println(\"NEXT MOVE: \", next_guess)" 195 | ] 196 | }, 197 | { 198 | "cell_type": "code", 199 | "execution_count": 339, 200 | "id": "ee962ba3-fd86-48f2-8752-e22d3ac794a1", 201 | "metadata": { 202 | "collapsed": true, 203 | "jupyter": { 204 | "outputs_hidden": true 205 | }, 206 | "tags": [] 207 | }, 208 | "outputs": [ 209 | { 210 | "name": "stdout", 211 | "output_type": "stream", 212 | "text": [ 213 | "Unexpected response; skipping ...\n", 214 | "list of possible solutions (1): storm\n", 215 | "NEXT MOVE: storm\n" 216 | ] 217 | } 218 | ], 219 | "source": [ 220 | "response = \".....\"\n", 221 | "pool = trim_pool(next_guess, response, pool)\n", 222 | "println(\"list of possible solutions ($(length(pool))): \", join(pool, \", \"))\n", 223 | "next_guess = find_move(ALL_WORDS, pool)\n", 224 | "println(\"NEXT MOVE: \", next_guess)" 225 | ] 226 | } 227 | ], 228 | "metadata": { 229 | "kernelspec": { 230 | "display_name": "Julia 1.7.1", 231 | "language": "julia", 232 | "name": "julia-1.7" 233 | }, 234 | "language_info": { 235 | "file_extension": ".jl", 236 | "mimetype": "application/julia", 237 | "name": "julia", 238 | "version": "1.7.1" 239 | } 240 | }, 241 | "nbformat": 4, 242 | "nbformat_minor": 5 243 | } 244 | -------------------------------------------------------------------------------- /solutions_nyt.txt: -------------------------------------------------------------------------------- 1 | "cigar", "rebut", "sissy", "humph", "awake", "blush", "focal", "evade", "naval", "serve", "heath", "dwarf", "model", "karma", "stink", "grade", "quiet", "bench", "abate", "feign", "major", "death", "fresh", "crust", "stool", "colon", "abase", "marry", "react", "batty", "pride", "floss", "helix", "croak", "staff", "paper", "unfed", "whelp", "trawl", "outdo", "adobe", "crazy", "sower", "repay", "digit", "crate", "cluck", "spike", "mimic", "pound", "maxim", "linen", "unmet", "flesh", "booby", "forth", "first", "stand", "belly", "ivory", "seedy", "print", "yearn", "drain", "bribe", "stout", "panel", "crass", "flume", "offal", "agree", "error", "swirl", "argue", "bleed", "delta", "flick", "totem", "wooer", "front", "shrub", "parry", "biome", "lapel", "start", "greet", "goner", "golem", "lusty", "loopy", "round", "audit", "lying", "gamma", "labor", "islet", "civic", "forge", "corny", "moult", "basic", "salad", "agate", "spicy", "spray", "essay", "fjord", "spend", "kebab", "guild", "aback", "motor", "alone", "hatch", "hyper", "thumb", "dowry", "ought", "belch", "dutch", "pilot", "tweed", "comet", "jaunt", "enema", "steed", "abyss", "growl", "fling", "dozen", "boozy", "erode", "world", "gouge", "click", "briar", "great", "altar", "pulpy", "blurt", "coast", "duchy", "groin", "fixer", "group", "rogue", "badly", "smart", "pithy", "gaudy", "chill", "heron", "vodka", "finer", "surer", "radio", "rouge", "perch", "retch", "wrote", "clock", "tilde", "store", "prove", "bring", "solve", "cheat", "grime", "exult", "usher", "epoch", "triad", "break", "rhino", "viral", "conic", "masse", "sonic", "vital", "trace", "using", "peach", "champ", "baton", "brake", "pluck", "craze", "gripe", "weary", "picky", "acute", "ferry", "aside", "tapir", "troll", "unify", "rebus", "boost", "truss", "siege", "tiger", "banal", "slump", "crank", "gorge", "query", "drink", "favor", "abbey", "tangy", "panic", "solar", "shire", "proxy", "point", "robot", "prick", "wince", "crimp", "knoll", "sugar", "whack", "mount", "perky", "could", "wrung", "light", "those", "moist", "shard", "pleat", "aloft", "skill", "elder", "frame", "humor", "pause", "ulcer", "ultra", "robin", "cynic", "aroma", "caulk", "shake", "dodge", "swill", "tacit", "other", "thorn", "trove", "bloke", "vivid", "spill", "chant", "choke", "rupee", "nasty", "mourn", "ahead", "brine", "cloth", "hoard", "sweet", "month", "lapse", "watch", "today", "focus", "smelt", "tease", "cater", "movie", "saute", "allow", "renew", "their", "slosh", "purge", "chest", "depot", "epoxy", "nymph", "found", "shall", "harry", "stove", "lowly", "snout", "trope", "fewer", "shawl", "natal", "comma", "foray", "scare", "stair", "black", "squad", "royal", "chunk", "mince", "shame", "cheek", "ample", "flair", "foyer", "cargo", "oxide", "plant", "olive", "inert", "askew", "heist", "shown", "zesty", "hasty", "trash", "fella", "larva", "forgo", "story", "hairy", "train", "homer", "badge", "midst", "canny", "fetus", "butch", "farce", "slung", "tipsy", "metal", "yield", "delve", "being", "scour", "glass", "gamer", "scrap", "money", "hinge", "album", "vouch", "asset", "tiara", "crept", "bayou", "atoll", "manor", "creak", "showy", "phase", "froth", "depth", "gloom", "flood", "trait", "girth", "piety", "payer", "goose", "float", "donor", "atone", "primo", "apron", "blown", "cacao", "loser", "input", "gloat", "awful", "brink", "smite", "beady", "rusty", "retro", "droll", "gawky", "hutch", "pinto", "gaily", "egret", "lilac", "sever", "field", "fluff", "hydro", "flack", "agape", "voice", "stead", "stalk", "berth", "madam", "night", "bland", "liver", "wedge", "augur", "roomy", "wacky", "flock", "angry", "bobby", "trite", "aphid", "tryst", "midge", "power", "elope", "cinch", "motto", "stomp", "upset", "bluff", "cramp", "quart", "coyly", "youth", "rhyme", "buggy", "alien", "smear", "unfit", "patty", "cling", "glean", "label", "hunky", "khaki", "poker", "gruel", "twice", "twang", "shrug", "treat", "unlit", "waste", "merit", "woven", "octal", "needy", "clown", "widow", "irony", "ruder", "gauze", "chief", "onset", "prize", "fungi", "charm", "gully", "inter", "whoop", "taunt", "leery", "class", "theme", "lofty", "tibia", "booze", "alpha", "thyme", "eclat", "doubt", "parer", "chute", "stick", "trice", "alike", "sooth", "recap", "saint", "liege", "glory", "grate", "admit", "brisk", "soggy", "usurp", "scald", "scorn", "leave", "twine", "sting", "bough", "marsh", "sloth", "dandy", "vigor", "howdy", "enjoy", "valid", "ionic", "equal", "unset", "floor", "catch", "spade", "stein", "exist", "quirk", "denim", "grove", "spiel", "mummy", "fault", "foggy", "flout", "carry", "sneak", "libel", "waltz", "aptly", "piney", "inept", "aloud", "photo", "dream", "stale", "vomit", "ombre", "fanny", "unite", "snarl", "baker", "there", "glyph", "pooch", "hippy", "spell", "folly", "louse", "gulch", "vault", "godly", "threw", "fleet", "grave", "inane", "shock", "crave", "spite", "valve", "skimp", "claim", "rainy", "musty", "pique", "daddy", "quasi", "arise", "aging", "valet", "opium", "avert", "stuck", "recut", "mulch", "genre", "plume", "rifle", "count", "incur", "total", "wrest", "mocha", "deter", "study", "lover", "safer", "rivet", "funny", "smoke", "mound", "undue", "sedan", "pagan", "swine", "guile", "gusty", "equip", "tough", "canoe", "chaos", "covet", "human", "udder", "lunch", "blast", "stray", "manga", "melee", "lefty", "quick", "paste", "given", "octet", "risen", "groan", "leaky", "grind", "carve", "loose", "sadly", "spilt", "apple", "slack", "honey", "final", "sheen", "eerie", "minty", "slick", "derby", "wharf", "spelt", "coach", "erupt", "singe", "price", "spawn", "fairy", "jiffy", "filmy", "stack", "chose", "sleep", "ardor", "nanny", "niece", "woozy", "handy", "grace", "ditto", "stank", "cream", "usual", "diode", "valor", "angle", "ninja", "muddy", "chase", "reply", "prone", "spoil", "heart", "shade", "diner", "arson", "onion", "sleet", "dowel", "couch", "palsy", "bowel", "smile", "evoke", "creek", "lance", "eagle", "idiot", "siren", "built", "embed", "award", "dross", "annul", "goody", "frown", "patio", "laden", "humid", "elite", "lymph", "edify", "might", "reset", "visit", "gusto", "purse", "vapor", "crock", "write", "sunny", "loath", "chaff", "slide", "queer", "venom", "stamp", "sorry", "still", "acorn", "aping", "pushy", "tamer", "hater", "mania", "awoke", "brawn", "swift", "exile", "birch", "lucky", "freer", "risky", "ghost", "plier", "lunar", "winch", "snare", "nurse", "house", "borax", "nicer", "lurch", "exalt", "about", "savvy", "toxin", "tunic", "pried", "inlay", "chump", "lanky", "cress", "eater", "elude", "cycle", "kitty", "boule", "moron", "tenet", "place", "lobby", "plush", "vigil", "index", "blink", "clung", "qualm", "croup", "clink", "juicy", "stage", "decay", "nerve", "flier", "shaft", "crook", "clean", "china", "ridge", "vowel", "gnome", "snuck", "icing", "spiny", "rigor", "snail", "flown", "rabid", "prose", "thank", "poppy", "budge", "fiber", "moldy", "dowdy", "kneel", "track", "caddy", "quell", "dumpy", "paler", "swore", "rebar", "scuba", "splat", "flyer", "horny", "mason", "doing", "ozone", "amply", "molar", "ovary", "beset", "queue", "cliff", "magic", "truce", "sport", "fritz", "edict", "twirl", "verse", "llama", "eaten", "range", "whisk", "hovel", "rehab", "macaw", "sigma", "spout", "verve", "sushi", "dying", "fetid", "brain", "buddy", "thump", "scion", "candy", "chord", "basin", "march", "crowd", "arbor", "gayly", "musky", "stain", "dally", "bless", "bravo", "stung", "title", "ruler", "kiosk", "blond", "ennui", "layer", "fluid", "tatty", "score", "cutie", "zebra", "barge", "matey", "bluer", "aider", "shook", "river", "privy", "betel", "frisk", "bongo", "begun", "azure", "weave", "genie", "sound", "glove", "braid", "scope", "wryly", "rover", "assay", "ocean", "bloom", "irate", "later", "woken", "silky", "wreck", "dwelt", "slate", "smack", "solid", "amaze", "hazel", "wrist", "jolly", "globe", "flint", "rouse", "civil", "vista", "relax", "cover", "alive", "beech", "jetty", "bliss", "vocal", "often", "dolly", "eight", "joker", "since", "event", "ensue", "shunt", "diver", "poser", "worst", "sweep", "alley", "creed", "anime", "leafy", "bosom", "dunce", "stare", "pudgy", "waive", "choir", "stood", "spoke", "outgo", "delay", "bilge", "ideal", "clasp", "seize", "hotly", "laugh", "sieve", "block", "meant", "grape", "noose", "hardy", "shied", "drawl", "daisy", "putty", "strut", "burnt", "tulip", "crick", "idyll", "vixen", "furor", "geeky", "cough", "naive", "shoal", "stork", "bathe", "aunty", "check", "prime", "brass", "outer", "furry", "razor", "elect", "evict", "imply", "demur", "quota", "haven", "cavil", "swear", "crump", "dough", "gavel", "wagon", "salon", "nudge", "harem", "pitch", "sworn", "pupil", "excel", "stony", "cabin", "unzip", "queen", "trout", "polyp", "earth", "storm", "until", "taper", "enter", "child", "adopt", "minor", "fatty", "husky", "brave", "filet", "slime", "glint", "tread", "steal", "regal", "guest", "every", "murky", "share", "spore", "hoist", "buxom", "inner", "otter", "dimly", "level", "sumac", "donut", "stilt", "arena", "sheet", "scrub", "fancy", "slimy", "pearl", "silly", "porch", "dingo", "sepia", "amble", "shady", "bread", "friar", "reign", "dairy", "quill", "cross", "brood", "tuber", "shear", "posit", "blank", "villa", "shank", "piggy", "freak", "which", "among", "fecal", "shell", "would", "algae", "large", "rabbi", "agony", "amuse", "bushy", "copse", "swoon", "knife", "pouch", "ascot", "plane", "crown", "urban", "snide", "relay", "abide", "viola", "rajah", "straw", "dilly", "crash", "amass", "third", "trick", "tutor", "woody", "blurb", "grief", "disco", "where", "sassy", "beach", "sauna", "comic", "clued", "creep", "caste", "graze", "snuff", "frock", "gonad", "drunk", "prong", "lurid", "steel", "halve", "buyer", "vinyl", "utile", "smell", "adage", "worry", "tasty", "local", "trade", "finch", "ashen", "modal", "gaunt", "clove", "enact", "adorn", "roast", "speck", "sheik", "missy", "grunt", "snoop", "party", "touch", "mafia", "emcee", "array", "south", "vapid", "jelly", "skulk", "angst", "tubal", "lower", "crest", "sweat", "cyber", "adore", "tardy", "swami", "notch", "groom", "roach", "hitch", "young", "align", "ready", "frond", "strap", "puree", "realm", "venue", "swarm", "offer", "seven", "dryer", "diary", "dryly", "drank", "acrid", "heady", "theta", "junto", "pixie", "quoth", "bonus", "shalt", "penne", "amend", "datum", "build", "piano", "shelf", "lodge", "suing", "rearm", "coral", "ramen", "worth", "psalm", "infer", "overt", "mayor", "ovoid", "glide", "usage", "poise", "randy", "chuck", "prank", "fishy", "tooth", "ether", "drove", "idler", "swath", "stint", "while", "begat", "apply", "slang", "tarot", "radar", "credo", "aware", "canon", "shift", "timer", "bylaw", "serum", "three", "steak", "iliac", "shirk", "blunt", "puppy", "penal", "joist", "bunny", "shape", "beget", "wheel", "adept", "stunt", "stole", "topaz", "chore", "fluke", "afoot", "bloat", "bully", "dense", "caper", "sneer", "boxer", "jumbo", "lunge", "space", "avail", "short", "slurp", "loyal", "flirt", "pizza", "conch", "tempo", "droop", "plate", "bible", "plunk", "afoul", "savoy", "steep", "agile", "stake", "dwell", "knave", "beard", "arose", "motif", "smash", "broil", "glare", "shove", "baggy", "mammy", "swamp", "along", "rugby", "wager", "quack", "squat", "snaky", "debit", "mange", "skate", "ninth", "joust", "tramp", "spurn", "medal", "micro", "rebel", "flank", "learn", "nadir", "maple", "comfy", "remit", "gruff", "ester", "least", "mogul", "fetch", "cause", "oaken", "aglow", "meaty", "gaffe", "shyly", "racer", "prowl", "thief", "stern", "poesy", "rocky", "tweet", "waist", "spire", "grope", "havoc", "patsy", "truly", "forty", "deity", "uncle", "swish", "giver", "preen", "bevel", "lemur", "draft", "slope", "annoy", "lingo", "bleak", "ditty", "curly", "cedar", "dirge", "grown", "horde", "drool", "shuck", "crypt", "cumin", "stock", "gravy", "locus", "wider", "breed", "quite", "chafe", "cache", "blimp", "deign", "fiend", "logic", "cheap", "elide", "rigid", "false", "renal", "pence", "rowdy", "shoot", "blaze", "envoy", "posse", "brief", "never", "abort", "mouse", "mucky", "sulky", "fiery", "media", "trunk", "yeast", "clear", "skunk", "scalp", "bitty", "cider", "koala", "duvet", "segue", "creme", "super", "grill", "after", "owner", "ember", "reach", "nobly", "empty", "speed", "gipsy", "recur", "smock", "dread", "merge", "burst", "kappa", "amity", "shaky", "hover", "carol", "snort", "synod", "faint", "haunt", "flour", "chair", "detox", "shrew", "tense", "plied", "quark", "burly", "novel", "waxen", "stoic", "jerky", "blitz", "beefy", "lyric", "hussy", "towel", "quilt", "below", "bingo", "wispy", "brash", "scone", "toast", "easel", "saucy", "value", "spice", "honor", "route", "sharp", "bawdy", "radii", "skull", "phony", "issue", "lager", "swell", "urine", "gassy", "trial", "flora", "upper", "latch", "wight", "brick", "retry", "holly", "decal", "grass", "shack", "dogma", "mover", "defer", "sober", "optic", "crier", "vying", "nomad", "flute", "hippo", "shark", "drier", "obese", "bugle", "tawny", "chalk", "feast", "ruddy", "pedal", "scarf", "cruel", "bleat", "tidal", "slush", "semen", "windy", "dusty", "sally", "igloo", "nerdy", "jewel", "shone", "whale", "hymen", "abuse", "fugue", "elbow", "crumb", "pansy", "welsh", "syrup", "terse", "suave", "gamut", "swung", "drake", "freed", "afire", "shirt", "grout", "oddly", "tithe", "plaid", "dummy", "broom", "blind", "torch", "enemy", "again", "tying", "pesky", "alter", "gazer", "noble", "ethos", "bride", "extol", "decor", "hobby", "beast", "idiom", "utter", "these", "sixth", "alarm", "erase", "elegy", "spunk", "piper", "scaly", "scold", "hefty", "chick", "sooty", "canal", "whiny", "slash", "quake", "joint", "swept", "prude", "heavy", "wield", "femme", "lasso", "maize", "shale", "screw", "spree", "smoky", "whiff", "scent", "glade", "spent", "prism", "stoke", "riper", "orbit", "cocoa", "guilt", "humus", "shush", "table", "smirk", "wrong", "noisy", "alert", "shiny", "elate", "resin", "whole", "hunch", "pixel", "polar", "hotel", "sword", "cleat", "mango", "rumba", "puffy", "filly", "billy", "leash", "clout", "dance", "ovate", "facet", "chili", "paint", "liner", "curio", "salty", "audio", "snake", "fable", "cloak", "navel", "spurt", "pesto", "balmy", "flash", "unwed", "early", "churn", "weedy", "stump", "lease", "witty", "wimpy", "spoof", "saner", "blend", "salsa", "thick", "warty", "manic", "blare", "squib", "spoon", "probe", "crepe", "knack", "force", "debut", "order", "haste", "teeth", "agent", "widen", "icily", "slice", "ingot", "clash", "juror", "blood", "abode", "throw", "unity", "pivot", "slept", "troop", "spare", "sewer", "parse", "morph", "cacti", "tacky", "spool", "demon", "moody", "annex", "begin", "fuzzy", "patch", "water", "lumpy", "admin", "omega", "limit", "tabby", "macho", "aisle", "skiff", "basis", "plank", "verge", "botch", "crawl", "lousy", "slain", "cubic", "raise", "wrack", "guide", "foist", "cameo", "under", "actor", "revue", "fraud", "harpy", "scoop", "climb", "refer", "olden", "clerk", "debar", "tally", "ethic", "cairn", "tulle", "ghoul", "hilly", "crude", "apart", "scale", "older", "plain", "sperm", "briny", "abbot", "rerun", "quest", "crisp", "bound", "befit", "drawn", "suite", "itchy", "cheer", "bagel", "guess", "broad", "axiom", "chard", "caput", "leant", "harsh", "curse", "proud", "swing", "opine", "taste", "lupus", "gumbo", "miner", "green", "chasm", "lipid", "topic", "armor", "brush", "crane", "mural", "abled", "habit", "bossy", "maker", "dusky", "dizzy", "lithe", "brook", "jazzy", "fifty", "sense", "giant", "surly", "legal", "fatal", "flunk", "began", "prune", "small", "slant", "scoff", "torus", "ninny", "covey", "viper", "taken", "moral", "vogue", "owing", "token", "entry", "booth", "voter", "chide", "elfin", "ebony", "neigh", "minim", "melon", "kneed", "decoy", "voila", "ankle", "arrow", "mushy", "tribe", "cease", "eager", "birth", "graph", "odder", "terra", "weird", "tried", "clack", "color", "rough", "weigh", "uncut", "ladle", "strip", "craft", "minus", "dicey", "titan", "lucid", "vicar", "dress", "ditch", "gypsy", "pasta", "taffy", "flame", "swoop", "aloof", "sight", "broke", "teary", "chart", "sixty", "wordy", "sheer", "leper", "nosey", "bulge", "savor", "clamp", "funky", "foamy", "toxic", "brand", "plumb", "dingy", "butte", "drill", "tripe", "bicep", "tenor", "krill", "worse", "drama", "hyena", "think", "ratio", "cobra", "basil", "scrum", "bused", "phone", "court", "camel", "proof", "heard", "angel", "petal", "pouty", "throb", "maybe", "fetal", "sprig", "spine", "shout", "cadet", "macro", "dodgy", "satyr", "rarer", "binge", "trend", "nutty", "leapt", "amiss", "split", "myrrh", "width", "sonar", "tower", "baron", "fever", "waver", "spark", "belie", "sloop", "expel", "smote", "baler", "above", "north", "wafer", "scant", "frill", "awash", "snack", "scowl", "frail", "drift", "limbo", "fence", "motel", "ounce", "wreak", "revel", "talon", "prior", "knelt", "cello", "flake", "debug", "anode", "crime", "salve", "scout", "imbue", "pinky", "stave", "vague", "chock", "fight", "video", "stone", "teach", "cleft", "frost", "prawn", "booty", "twist", "apnea", "stiff", "plaza", "ledge", "tweak", "board", "grant", "medic", "bacon", "cable", "brawl", "slunk", "raspy", "forum", "drone", "women", "mucus", "boast", "toddy", "coven", "tumor", "truer", "wrath", "stall", "steam", "axial", "purer", "daily", "trail", "niche", "mealy", "juice", "nylon", "plump", "merry", "flail", "papal", "wheat", "berry", "cower", "erect", "brute", "leggy", "snipe", "sinew", "skier", "penny", "jumpy", "rally", "umbra", "scary", "modem", "gross", "avian", "greed", "satin", "tonic", "parka", "sniff", "livid", "stark", "trump", "giddy", "reuse", "taboo", "avoid", "quote", "devil", "liken", "gloss", "gayer", "beret", "noise", "gland", "dealt", "sling", "rumor", "opera", "thigh", "tonga", "flare", "wound", "white", "bulky", "etude", "horse", "circa", "paddy", "inbox", "fizzy", "grain", "exert", "surge", "gleam", "belle", "salvo", "crush", "fruit", "sappy", "taker", "tract", "ovine", "spiky", "frank", "reedy", "filth", "spasm", "heave", "mambo", "right", "clank", "trust", "lumen", "borne", "spook", "sauce", "amber", "lathe", "carat", "corer", "dirty", "slyly", "affix", "alloy", "taint", "sheep", "kinky", "wooly", "mauve", "flung", "yacht", "fried", "quail", "brunt", "grimy", "curvy", "cagey", "rinse", "deuce", "state", "grasp", "milky", "bison", "graft", "sandy", "baste", "flask", "hedge", "girly", "swash", "boney", "coupe", "endow", "abhor", "welch", "blade", "tight", "geese", "miser", "mirth", "cloud", "cabal", "leech", "close", "tenth", "pecan", "droit", "grail", "clone", "guise", "ralph", "tango", "biddy", "smith", "mower", "payee", "serif", "drape", "fifth", "spank", "glaze", "allot", "truck", "kayak", "virus", "testy", "tepee", "fully", "zonal", "metro", "curry", "grand", "banjo", "axion", "bezel", "occur", "chain", "nasal", "gooey", "filer", "brace", "allay", "pubic", "raven", "plead", "gnash", "flaky", "munch", "dully", "eking", "thing", "slink", "hurry", "theft", "shorn", "pygmy", "ranch", "wring", "lemon", "shore", "mamma", "froze", "newer", "style", "moose", "antic", "drown", "vegan", "chess", "guppy", "union", "lever", "lorry", "image", "cabby", "druid", "exact", "truth", "dopey", "spear", "cried", "chime", "crony", "stunk", "timid", "batch", "gauge", "rotor", "crack", "curve", "latte", "witch", "bunch", "repel", "anvil", "soapy", "meter", "broth", "madly", "dried", "scene", "known", "magma", "roost", "woman", "thong", "punch", "pasty", "downy", "knead", "whirl", "rapid", "clang", "anger", "drive", "goofy", "email", "music", "stuff", "bleep", "rider", "mecca", "folio", "setup", "verso", "quash", "fauna", "gummy", "happy", "newly", "fussy", "relic", "guava", "ratty", "fudge", "femur", "chirp", "forte", "alibi", "whine", "petty", "golly", "plait", "fleck", "felon", "gourd", "brown", "thrum", "ficus", "stash", "decry", "wiser", "junta", "visor", "daunt", "scree", "impel", "await", "press", "whose", "turbo", "stoop", "speak", "mangy", "eying", "inlet", "crone", "pulse", "mossy", "staid", "hence", "pinch", "teddy", "sully", "snore", "ripen", "snowy", "attic", "going", "leach", "mouth", "hound", "clump", "tonal", "bigot", "peril", "piece", "blame", "haute", "spied", "undid", "intro", "basal", "shine", "gecko", "rodeo", "guard", "steer", "loamy", "scamp", "scram", "manly", "hello", "vaunt", "organ", "feral", "knock", "extra", "condo", "adapt", "willy", "polka", "rayon", "skirt", "faith", "torso", "match", "mercy", "tepid", "sleek", "riser", "twixt", "peace", "flush", "catty", "login", "eject", "roger", "rival", "untie", "refit", "aorta", "adult", "judge", "rower", "artsy", "rural", "shave" -------------------------------------------------------------------------------- /.ipynb_checkpoints/solutions_nyt-checkpoint.txt: -------------------------------------------------------------------------------- 1 | "cigar", "rebut", "sissy", "humph", "awake", "blush", "focal", "evade", "naval", "serve", "heath", "dwarf", "model", "karma", "stink", "grade", "quiet", "bench", "abate", "feign", "major", "death", "fresh", "crust", "stool", "colon", "abase", "marry", "react", "batty", "pride", "floss", "helix", "croak", "staff", "paper", "unfed", "whelp", "trawl", "outdo", "adobe", "crazy", "sower", "repay", "digit", "crate", "cluck", "spike", "mimic", "pound", "maxim", "linen", "unmet", "flesh", "booby", "forth", "first", "stand", "belly", "ivory", "seedy", "print", "yearn", "drain", "bribe", "stout", "panel", "crass", "flume", "offal", "agree", "error", "swirl", "argue", "bleed", "delta", "flick", "totem", "wooer", "front", "shrub", "parry", "biome", "lapel", "start", "greet", "goner", "golem", "lusty", "loopy", "round", "audit", "lying", "gamma", "labor", "islet", "civic", "forge", "corny", "moult", "basic", "salad", "agate", "spicy", "spray", "essay", "fjord", "spend", "kebab", "guild", "aback", "motor", "alone", "hatch", "hyper", "thumb", "dowry", "ought", "belch", "dutch", "pilot", "tweed", "comet", "jaunt", "enema", "steed", "abyss", "growl", "fling", "dozen", "boozy", "erode", "world", "gouge", "click", "briar", "great", "altar", "pulpy", "blurt", "coast", "duchy", "groin", "fixer", "group", "rogue", "badly", "smart", "pithy", "gaudy", "chill", "heron", "vodka", "finer", "surer", "radio", "rouge", "perch", "retch", "wrote", "clock", "tilde", "store", "prove", "bring", "solve", "cheat", "grime", "exult", "usher", "epoch", "triad", "break", "rhino", "viral", "conic", "masse", "sonic", "vital", "trace", "using", "peach", "champ", "baton", "brake", "pluck", "craze", "gripe", "weary", "picky", "acute", "ferry", "aside", "tapir", "troll", "unify", "rebus", "boost", "truss", "siege", "tiger", "banal", "slump", "crank", "gorge", "query", "drink", "favor", "abbey", "tangy", "panic", "solar", "shire", "proxy", "point", "robot", "prick", "wince", "crimp", "knoll", "sugar", "whack", "mount", "perky", "could", "wrung", "light", "those", "moist", "shard", "pleat", "aloft", "skill", "elder", "frame", "humor", "pause", "ulcer", "ultra", "robin", "cynic", "aroma", "caulk", "shake", "dodge", "swill", "tacit", "other", "thorn", "trove", "bloke", "vivid", "spill", "chant", "choke", "rupee", "nasty", "mourn", "ahead", "brine", "cloth", "hoard", "sweet", "month", "lapse", "watch", "today", "focus", "smelt", "tease", "cater", "movie", "saute", "allow", "renew", "their", "slosh", "purge", "chest", "depot", "epoxy", "nymph", "found", "shall", "harry", "stove", "lowly", "snout", "trope", "fewer", "shawl", "natal", "comma", "foray", "scare", "stair", "black", "squad", "royal", "chunk", "mince", "shame", "cheek", "ample", "flair", "foyer", "cargo", "oxide", "plant", "olive", "inert", "askew", "heist", "shown", "zesty", "hasty", "trash", "fella", "larva", "forgo", "story", "hairy", "train", "homer", "badge", "midst", "canny", "fetus", "butch", "farce", "slung", "tipsy", "metal", "yield", "delve", "being", "scour", "glass", "gamer", "scrap", "money", "hinge", "album", "vouch", "asset", "tiara", "crept", "bayou", "atoll", "manor", "creak", "showy", "phase", "froth", "depth", "gloom", "flood", "trait", "girth", "piety", "payer", "goose", "float", "donor", "atone", "primo", "apron", "blown", "cacao", "loser", "input", "gloat", "awful", "brink", "smite", "beady", "rusty", "retro", "droll", "gawky", "hutch", "pinto", "gaily", "egret", "lilac", "sever", "field", "fluff", "hydro", "flack", "agape", "voice", "stead", "stalk", "berth", "madam", "night", "bland", "liver", "wedge", "augur", "roomy", "wacky", "flock", "angry", "bobby", "trite", "aphid", "tryst", "midge", "power", "elope", "cinch", "motto", "stomp", "upset", "bluff", "cramp", "quart", "coyly", "youth", "rhyme", "buggy", "alien", "smear", "unfit", "patty", "cling", "glean", "label", "hunky", "khaki", "poker", "gruel", "twice", "twang", "shrug", "treat", "unlit", "waste", "merit", "woven", "octal", "needy", "clown", "widow", "irony", "ruder", "gauze", "chief", "onset", "prize", "fungi", "charm", "gully", "inter", "whoop", "taunt", "leery", "class", "theme", "lofty", "tibia", "booze", "alpha", "thyme", "eclat", "doubt", "parer", "chute", "stick", "trice", "alike", "sooth", "recap", "saint", "liege", "glory", "grate", "admit", "brisk", "soggy", "usurp", "scald", "scorn", "leave", "twine", "sting", "bough", "marsh", "sloth", "dandy", "vigor", "howdy", "enjoy", "valid", "ionic", "equal", "unset", "floor", "catch", "spade", "stein", "exist", "quirk", "denim", "grove", "spiel", "mummy", "fault", "foggy", "flout", "carry", "sneak", "libel", "waltz", "aptly", "piney", "inept", "aloud", "photo", "dream", "stale", "vomit", "ombre", "fanny", "unite", "snarl", "baker", "there", "glyph", "pooch", "hippy", "spell", "folly", "louse", "gulch", "vault", "godly", "threw", "fleet", "grave", "inane", "shock", "crave", "spite", "valve", "skimp", "claim", "rainy", "musty", "pique", "daddy", "quasi", "arise", "aging", "valet", "opium", "avert", "stuck", "recut", "mulch", "genre", "plume", "rifle", "count", "incur", "total", "wrest", "mocha", "deter", "study", "lover", "safer", "rivet", "funny", "smoke", "mound", "undue", "sedan", "pagan", "swine", "guile", "gusty", "equip", "tough", "canoe", "chaos", "covet", "human", "udder", "lunch", "blast", "stray", "manga", "melee", "lefty", "quick", "paste", "given", "octet", "risen", "groan", "leaky", "grind", "carve", "loose", "sadly", "spilt", "apple", "slack", "honey", "final", "sheen", "eerie", "minty", "slick", "derby", "wharf", "spelt", "coach", "erupt", "singe", "price", "spawn", "fairy", "jiffy", "filmy", "stack", "chose", "sleep", "ardor", "nanny", "niece", "woozy", "handy", "grace", "ditto", "stank", "cream", "usual", "diode", "valor", "angle", "ninja", "muddy", "chase", "reply", "prone", "spoil", "heart", "shade", "diner", "arson", "onion", "sleet", "dowel", "couch", "palsy", "bowel", "smile", "evoke", "creek", "lance", "eagle", "idiot", "siren", "built", "embed", "award", "dross", "annul", "goody", "frown", "patio", "laden", "humid", "elite", "lymph", "edify", "might", "reset", "visit", "gusto", "purse", "vapor", "crock", "write", "sunny", "loath", "chaff", "slide", "queer", "venom", "stamp", "sorry", "still", "acorn", "aping", "pushy", "tamer", "hater", "mania", "awoke", "brawn", "swift", "exile", "birch", "lucky", "freer", "risky", "ghost", "plier", "lunar", "winch", "snare", "nurse", "house", "borax", "nicer", "lurch", "exalt", "about", "savvy", "toxin", "tunic", "pried", "inlay", "chump", "lanky", "cress", "eater", "elude", "cycle", "kitty", "boule", "moron", "tenet", "place", "lobby", "plush", "vigil", "index", "blink", "clung", "qualm", "croup", "clink", "juicy", "stage", "decay", "nerve", "flier", "shaft", "crook", "clean", "china", "ridge", "vowel", "gnome", "snuck", "icing", "spiny", "rigor", "snail", "flown", "rabid", "prose", "thank", "poppy", "budge", "fiber", "moldy", "dowdy", "kneel", "track", "caddy", "quell", "dumpy", "paler", "swore", "rebar", "scuba", "splat", "flyer", "horny", "mason", "doing", "ozone", "amply", "molar", "ovary", "beset", "queue", "cliff", "magic", "truce", "sport", "fritz", "edict", "twirl", "verse", "llama", "eaten", "range", "whisk", "hovel", "rehab", "macaw", "sigma", "spout", "verve", "sushi", "dying", "fetid", "brain", "buddy", "thump", "scion", "candy", "chord", "basin", "march", "crowd", "arbor", "gayly", "musky", "stain", "dally", "bless", "bravo", "stung", "title", "ruler", "kiosk", "blond", "ennui", "layer", "fluid", "tatty", "score", "cutie", "zebra", "barge", "matey", "bluer", "aider", "shook", "river", "privy", "betel", "frisk", "bongo", "begun", "azure", "weave", "genie", "sound", "glove", "braid", "scope", "wryly", "rover", "assay", "ocean", "bloom", "irate", "later", "woken", "silky", "wreck", "dwelt", "slate", "smack", "solid", "amaze", "hazel", "wrist", "jolly", "globe", "flint", "rouse", "civil", "vista", "relax", "cover", "alive", "beech", "jetty", "bliss", "vocal", "often", "dolly", "eight", "joker", "since", "event", "ensue", "shunt", "diver", "poser", "worst", "sweep", "alley", "creed", "anime", "leafy", "bosom", "dunce", "stare", "pudgy", "waive", "choir", "stood", "spoke", "outgo", "delay", "bilge", "ideal", "clasp", "seize", "hotly", "laugh", "sieve", "block", "meant", "grape", "noose", "hardy", "shied", "drawl", "daisy", "putty", "strut", "burnt", "tulip", "crick", "idyll", "vixen", "furor", "geeky", "cough", "naive", "shoal", "stork", "bathe", "aunty", "check", "prime", "brass", "outer", "furry", "razor", "elect", "evict", "imply", "demur", "quota", "haven", "cavil", "swear", "crump", "dough", "gavel", "wagon", "salon", "nudge", "harem", "pitch", "sworn", "pupil", "excel", "stony", "cabin", "unzip", "queen", "trout", "polyp", "earth", "storm", "until", "taper", "enter", "child", "adopt", "minor", "fatty", "husky", "brave", "filet", "slime", "glint", "tread", "steal", "regal", "guest", "every", "murky", "share", "spore", "hoist", "buxom", "inner", "otter", "dimly", "level", "sumac", "donut", "stilt", "arena", "sheet", "scrub", "fancy", "slimy", "pearl", "silly", "porch", "dingo", "sepia", "amble", "shady", "bread", "friar", "reign", "dairy", "quill", "cross", "brood", "tuber", "shear", "posit", "blank", "villa", "shank", "piggy", "freak", "which", "among", "fecal", "shell", "would", "algae", "large", "rabbi", "agony", "amuse", "bushy", "copse", "swoon", "knife", "pouch", "ascot", "plane", "crown", "urban", "snide", "relay", "abide", "viola", "rajah", "straw", "dilly", "crash", "amass", "third", "trick", "tutor", "woody", "blurb", "grief", "disco", "where", "sassy", "beach", "sauna", "comic", "clued", "creep", "caste", "graze", "snuff", "frock", "gonad", "drunk", "prong", "lurid", "steel", "halve", "buyer", "vinyl", "utile", "smell", "adage", "worry", "tasty", "local", "trade", "finch", "ashen", "modal", "gaunt", "clove", "enact", "adorn", "roast", "speck", "sheik", "missy", "grunt", "snoop", "party", "touch", "mafia", "emcee", "array", "south", "vapid", "jelly", "skulk", "angst", "tubal", "lower", "crest", "sweat", "cyber", "adore", "tardy", "swami", "notch", "groom", "roach", "hitch", "young", "align", "ready", "frond", "strap", "puree", "realm", "venue", "swarm", "offer", "seven", "dryer", "diary", "dryly", "drank", "acrid", "heady", "theta", "junto", "pixie", "quoth", "bonus", "shalt", "penne", "amend", "datum", "build", "piano", "shelf", "lodge", "suing", "rearm", "coral", "ramen", "worth", "psalm", "infer", "overt", "mayor", "ovoid", "glide", "usage", "poise", "randy", "chuck", "prank", "fishy", "tooth", "ether", "drove", "idler", "swath", "stint", "while", "begat", "apply", "slang", "tarot", "radar", "credo", "aware", "canon", "shift", "timer", "bylaw", "serum", "three", "steak", "iliac", "shirk", "blunt", "puppy", "penal", "joist", "bunny", "shape", "beget", "wheel", "adept", "stunt", "stole", "topaz", "chore", "fluke", "afoot", "bloat", "bully", "dense", "caper", "sneer", "boxer", "jumbo", "lunge", "space", "avail", "short", "slurp", "loyal", "flirt", "pizza", "conch", "tempo", "droop", "plate", "bible", "plunk", "afoul", "savoy", "steep", "agile", "stake", "dwell", "knave", "beard", "arose", "motif", "smash", "broil", "glare", "shove", "baggy", "mammy", "swamp", "along", "rugby", "wager", "quack", "squat", "snaky", "debit", "mange", "skate", "ninth", "joust", "tramp", "spurn", "medal", "micro", "rebel", "flank", "learn", "nadir", "maple", "comfy", "remit", "gruff", "ester", "least", "mogul", "fetch", "cause", "oaken", "aglow", "meaty", "gaffe", "shyly", "racer", "prowl", "thief", "stern", "poesy", "rocky", "tweet", "waist", "spire", "grope", "havoc", "patsy", "truly", "forty", "deity", "uncle", "swish", "giver", "preen", "bevel", "lemur", "draft", "slope", "annoy", "lingo", "bleak", "ditty", "curly", "cedar", "dirge", "grown", "horde", "drool", "shuck", "crypt", "cumin", "stock", "gravy", "locus", "wider", "breed", "quite", "chafe", "cache", "blimp", "deign", "fiend", "logic", "cheap", "elide", "rigid", "false", "renal", "pence", "rowdy", "shoot", "blaze", "envoy", "posse", "brief", "never", "abort", "mouse", "mucky", "sulky", "fiery", "media", "trunk", "yeast", "clear", "skunk", "scalp", "bitty", "cider", "koala", "duvet", "segue", "creme", "super", "grill", "after", "owner", "ember", "reach", "nobly", "empty", "speed", "gipsy", "recur", "smock", "dread", "merge", "burst", "kappa", "amity", "shaky", "hover", "carol", "snort", "synod", "faint", "haunt", "flour", "chair", "detox", "shrew", "tense", "plied", "quark", "burly", "novel", "waxen", "stoic", "jerky", "blitz", "beefy", "lyric", "hussy", "towel", "quilt", "below", "bingo", "wispy", "brash", "scone", "toast", "easel", "saucy", "value", "spice", "honor", "route", "sharp", "bawdy", "radii", "skull", "phony", "issue", "lager", "swell", "urine", "gassy", "trial", "flora", "upper", "latch", "wight", "brick", "retry", "holly", "decal", "grass", "shack", "dogma", "mover", "defer", "sober", "optic", "crier", "vying", "nomad", "flute", "hippo", "shark", "drier", "obese", "bugle", "tawny", "chalk", "feast", "ruddy", "pedal", "scarf", "cruel", "bleat", "tidal", "slush", "semen", "windy", "dusty", "sally", "igloo", "nerdy", "jewel", "shone", "whale", "hymen", "abuse", "fugue", "elbow", "crumb", "pansy", "welsh", "syrup", "terse", "suave", "gamut", "swung", "drake", "freed", "afire", "shirt", "grout", "oddly", "tithe", "plaid", "dummy", "broom", "blind", "torch", "enemy", "again", "tying", "pesky", "alter", "gazer", "noble", "ethos", "bride", "extol", "decor", "hobby", "beast", "idiom", "utter", "these", "sixth", "alarm", "erase", "elegy", "spunk", "piper", "scaly", "scold", "hefty", "chick", "sooty", "canal", "whiny", "slash", "quake", "joint", "swept", "prude", "heavy", "wield", "femme", "lasso", "maize", "shale", "screw", "spree", "smoky", "whiff", "scent", "glade", "spent", "prism", "stoke", "riper", "orbit", "cocoa", "guilt", "humus", "shush", "table", "smirk", "wrong", "noisy", "alert", "shiny", "elate", "resin", "whole", "hunch", "pixel", "polar", "hotel", "sword", "cleat", "mango", "rumba", "puffy", "filly", "billy", "leash", "clout", "dance", "ovate", "facet", "chili", "paint", "liner", "curio", "salty", "audio", "snake", "fable", "cloak", "navel", "spurt", "pesto", "balmy", "flash", "unwed", "early", "churn", "weedy", "stump", "lease", "witty", "wimpy", "spoof", "saner", "blend", "salsa", "thick", "warty", "manic", "blare", "squib", "spoon", "probe", "crepe", "knack", "force", "debut", "order", "haste", "teeth", "agent", "widen", "icily", "slice", "ingot", "clash", "juror", "blood", "abode", "throw", "unity", "pivot", "slept", "troop", "spare", "sewer", "parse", "morph", "cacti", "tacky", "spool", "demon", "moody", "annex", "begin", "fuzzy", "patch", "water", "lumpy", "admin", "omega", "limit", "tabby", "macho", "aisle", "skiff", "basis", "plank", "verge", "botch", "crawl", "lousy", "slain", "cubic", "raise", "wrack", "guide", "foist", "cameo", "under", "actor", "revue", "fraud", "harpy", "scoop", "climb", "refer", "olden", "clerk", "debar", "tally", "ethic", "cairn", "tulle", "ghoul", "hilly", "crude", "apart", "scale", "older", "plain", "sperm", "briny", "abbot", "rerun", "quest", "crisp", "bound", "befit", "drawn", "suite", "itchy", "cheer", "bagel", "guess", "broad", "axiom", "chard", "caput", "leant", "harsh", "curse", "proud", "swing", "opine", "taste", "lupus", "gumbo", "miner", "green", "chasm", "lipid", "topic", "armor", "brush", "crane", "mural", "abled", "habit", "bossy", "maker", "dusky", "dizzy", "lithe", "brook", "jazzy", "fifty", "sense", "giant", "surly", "legal", "fatal", "flunk", "began", "prune", "small", "slant", "scoff", "torus", "ninny", "covey", "viper", "taken", "moral", "vogue", "owing", "token", "entry", "booth", "voter", "chide", "elfin", "ebony", "neigh", "minim", "melon", "kneed", "decoy", "voila", "ankle", "arrow", "mushy", "tribe", "cease", "eager", "birth", "graph", "odder", "terra", "weird", "tried", "clack", "color", "rough", "weigh", "uncut", "ladle", "strip", "craft", "minus", "dicey", "titan", "lucid", "vicar", "dress", "ditch", "gypsy", "pasta", "taffy", "flame", "swoop", "aloof", "sight", "broke", "teary", "chart", "sixty", "wordy", "sheer", "leper", "nosey", "bulge", "savor", "clamp", "funky", "foamy", "toxic", "brand", "plumb", "dingy", "butte", "drill", "tripe", "bicep", "tenor", "krill", "worse", "drama", "hyena", "think", "ratio", "cobra", "basil", "scrum", "bused", "phone", "court", "camel", "proof", "heard", "angel", "petal", "pouty", "throb", "maybe", "fetal", "sprig", "spine", "shout", "cadet", "macro", "dodgy", "satyr", "rarer", "binge", "trend", "nutty", "leapt", "amiss", "split", "myrrh", "width", "sonar", "tower", "baron", "fever", "waver", "spark", "belie", "sloop", "expel", "smote", "baler", "above", "north", "wafer", "scant", "frill", "awash", "snack", "scowl", "frail", "drift", "limbo", "fence", "motel", "ounce", "wreak", "revel", "talon", "prior", "knelt", "cello", "flake", "debug", "anode", "crime", "salve", "scout", "imbue", "pinky", "stave", "vague", "chock", "fight", "video", "stone", "teach", "cleft", "frost", "prawn", "booty", "twist", "apnea", "stiff", "plaza", "ledge", "tweak", "board", "grant", "medic", "bacon", "cable", "brawl", "slunk", "raspy", "forum", "drone", "women", "mucus", "boast", "toddy", "coven", "tumor", "truer", "wrath", "stall", "steam", "axial", "purer", "daily", "trail", "niche", "mealy", "juice", "nylon", "plump", "merry", "flail", "papal", "wheat", "berry", "cower", "erect", "brute", "leggy", "snipe", "sinew", "skier", "penny", "jumpy", "rally", "umbra", "scary", "modem", "gross", "avian", "greed", "satin", "tonic", "parka", "sniff", "livid", "stark", "trump", "giddy", "reuse", "taboo", "avoid", "quote", "devil", "liken", "gloss", "gayer", "beret", "noise", "gland", "dealt", "sling", "rumor", "opera", "thigh", "tonga", "flare", "wound", "white", "bulky", "etude", "horse", "circa", "paddy", "inbox", "fizzy", "grain", "exert", "surge", "gleam", "belle", "salvo", "crush", "fruit", "sappy", "taker", "tract", "ovine", "spiky", "frank", "reedy", "filth", "spasm", "heave", "mambo", "right", "clank", "trust", "lumen", "borne", "spook", "sauce", "amber", "lathe", "carat", "corer", "dirty", "slyly", "affix", "alloy", "taint", "sheep", "kinky", "wooly", "mauve", "flung", "yacht", "fried", "quail", "brunt", "grimy", "curvy", "cagey", "rinse", "deuce", "state", "grasp", "milky", "bison", "graft", "sandy", "baste", "flask", "hedge", "girly", "swash", "boney", "coupe", "endow", "abhor", "welch", "blade", "tight", "geese", "miser", "mirth", "cloud", "cabal", "leech", "close", "tenth", "pecan", "droit", "grail", "clone", "guise", "ralph", "tango", "biddy", "smith", "mower", "payee", "serif", "drape", "fifth", "spank", "glaze", "allot", "truck", "kayak", "virus", "testy", "tepee", "fully", "zonal", "metro", "curry", "grand", "banjo", "axion", "bezel", "occur", "chain", "nasal", "gooey", "filer", "brace", "allay", "pubic", "raven", "plead", "gnash", "flaky", "munch", "dully", "eking", "thing", "slink", "hurry", "theft", "shorn", "pygmy", "ranch", "wring", "lemon", "shore", "mamma", "froze", "newer", "style", "moose", "antic", "drown", "vegan", "chess", "guppy", "union", "lever", "lorry", "image", "cabby", "druid", "exact", "truth", "dopey", "spear", "cried", "chime", "crony", "stunk", "timid", "batch", "gauge", "rotor", "crack", "curve", "latte", "witch", "bunch", "repel", "anvil", "soapy", "meter", "broth", "madly", "dried", "scene", "known", "magma", "roost", "woman", "thong", "punch", "pasty", "downy", "knead", "whirl", "rapid", "clang", "anger", "drive", "goofy", "email", "music", "stuff", "bleep", "rider", "mecca", "folio", "setup", "verso", "quash", "fauna", "gummy", "happy", "newly", "fussy", "relic", "guava", "ratty", "fudge", "femur", "chirp", "forte", "alibi", "whine", "petty", "golly", "plait", "fleck", "felon", "gourd", "brown", "thrum", "ficus", "stash", "decry", "wiser", "junta", "visor", "daunt", "scree", "impel", "await", "press", "whose", "turbo", "stoop", "speak", "mangy", "eying", "inlet", "crone", "pulse", "mossy", "staid", "hence", "pinch", "teddy", "sully", "snore", "ripen", "snowy", "attic", "going", "leach", "mouth", "hound", "clump", "tonal", "bigot", "peril", "piece", "blame", "haute", "spied", "undid", "intro", "basal", "shine", "gecko", "rodeo", "guard", "steer", "loamy", "scamp", "scram", "manly", "hello", "vaunt", "organ", "feral", "knock", "extra", "condo", "adapt", "willy", "polka", "rayon", "skirt", "faith", "torso", "match", "mercy", "tepid", "sleek", "riser", "twixt", "peace", "flush", "catty", "login", "eject", "roger", "rival", "untie", "refit", "aorta", "adult", "judge", "rower", "artsy", "rural", "shave" -------------------------------------------------------------------------------- /solutions.txt: -------------------------------------------------------------------------------- 1 | "cigar", "rebut", "sissy", "humph", "awake", "blush", "focal", "evade", "naval", "serve", "heath", "dwarf", "model", "karma", "stink", "grade", "quiet", "bench", "abate", "feign", "major", "death", "fresh", "crust", "stool", "colon", "abase", "marry", "react", "batty", "pride", "floss", "helix", "croak", "staff", "paper", "unfed", "whelp", "trawl", "outdo", "adobe", "crazy", "sower", "repay", "digit", "crate", "cluck", "spike", "mimic", "pound", "maxim", "linen", "unmet", "flesh", "booby", "forth", "first", "stand", "belly", "ivory", "seedy", "print", "yearn", "drain", "bribe", "stout", "panel", "crass", "flume", "offal", "agree", "error", "swirl", "argue", "bleed", "delta", "flick", "totem", "wooer", "front", "shrub", "parry", "biome", "lapel", "start", "greet", "goner", "golem", "lusty", "loopy", "round", "audit", "lying", "gamma", "labor", "islet", "civic", "forge", "corny", "moult", "basic", "salad", "agate", "spicy", "spray", "essay", "fjord", "spend", "kebab", "guild", "aback", "motor", "alone", "hatch", "hyper", "thumb", "dowry", "ought", "belch", "dutch", "pilot", "tweed", "comet", "jaunt", "enema", "steed", "abyss", "growl", "fling", "dozen", "boozy", "erode", "world", "gouge", "click", "briar", "great", "altar", "pulpy", "blurt", "coast", "duchy", "groin", "fixer", "group", "rogue", "badly", "smart", "pithy", "gaudy", "chill", "heron", "vodka", "finer", "surer", "radio", "rouge", "perch", "retch", "wrote", "clock", "tilde", "store", "prove", "bring", "solve", "cheat", "grime", "exult", "usher", "epoch", "triad", "break", "rhino", "viral", "conic", "masse", "sonic", "vital", "trace", "using", "peach", "champ", "baton", "brake", "pluck", "craze", "gripe", "weary", "picky", "acute", "ferry", "aside", "tapir", "troll", "unify", "rebus", "boost", "truss", "siege", "tiger", "banal", "slump", "crank", "gorge", "query", "drink", "favor", "abbey", "tangy", "panic", "solar", "shire", "proxy", "point", "robot", "prick", "wince", "crimp", "knoll", "sugar", "whack", "mount", "perky", "could", "wrung", "light", "those", "moist", "shard", "pleat", "aloft", "skill", "elder", "frame", "humor", "pause", "ulcer", "ultra", "robin", "cynic", "agora", "aroma", "caulk", "shake", "pupal", "dodge", "swill", "tacit", "other", "thorn", "trove", "bloke", "vivid", "spill", "chant", "choke", "rupee", "nasty", "mourn", "ahead", "brine", "cloth", "hoard", "sweet", "month", "lapse", "watch", "today", "focus", "smelt", "tease", "cater", "movie", "lynch", "saute", "allow", "renew", "their", "slosh", "purge", "chest", "depot", "epoxy", "nymph", "found", "shall", "harry", "stove", "lowly", "snout", "trope", "fewer", "shawl", "natal", "fibre", "comma", "foray", "scare", "stair", "black", "squad", "royal", "chunk", "mince", "slave", "shame", "cheek", "ample", "flair", "foyer", "cargo", "oxide", "plant", "olive", "inert", "askew", "heist", "shown", "zesty", "hasty", "trash", "fella", "larva", "forgo", "story", "hairy", "train", "homer", "badge", "midst", "canny", "fetus", "butch", "farce", "slung", "tipsy", "metal", "yield", "delve", "being", "scour", "glass", "gamer", "scrap", "money", "hinge", "album", "vouch", "asset", "tiara", "crept", "bayou", "atoll", "manor", "creak", "showy", "phase", "froth", "depth", "gloom", "flood", "trait", "girth", "piety", "payer", "goose", "float", "donor", "atone", "primo", "apron", "blown", "cacao", "loser", "input", "gloat", "awful", "brink", "smite", "beady", "rusty", "retro", "droll", "gawky", "hutch", "pinto", "gaily", "egret", "lilac", "sever", "field", "fluff", "hydro", "flack", "agape", "wench", "voice", "stead", "stalk", "berth", "madam", "night", "bland", "liver", "wedge", "augur", "roomy", "wacky", "flock", "angry", "bobby", "trite", "aphid", "tryst", "midge", "power", "elope", "cinch", "motto", "stomp", "upset", "bluff", "cramp", "quart", "coyly", "youth", "rhyme", "buggy", "alien", "smear", "unfit", "patty", "cling", "glean", "label", "hunky", "khaki", "poker", "gruel", "twice", "twang", "shrug", "treat", "unlit", "waste", "merit", "woven", "octal", "needy", "clown", "widow", "irony", "ruder", "gauze", "chief", "onset", "prize", "fungi", "charm", "gully", "inter", "whoop", "taunt", "leery", "class", "theme", "lofty", "tibia", "booze", "alpha", "thyme", "eclat", "doubt", "parer", "chute", "stick", "trice", "alike", "sooth", "recap", "saint", "liege", "glory", "grate", "admit", "brisk", "soggy", "usurp", "scald", "scorn", "leave", "twine", "sting", "bough", "marsh", "sloth", "dandy", "vigor", "howdy", "enjoy", "valid", "ionic", "equal", "unset", "floor", "catch", "spade", "stein", "exist", "quirk", "denim", "grove", "spiel", "mummy", "fault", "foggy", "flout", "carry", "sneak", "libel", "waltz", "aptly", "piney", "inept", "aloud", "photo", "dream", "stale", "vomit", "ombre", "fanny", "unite", "snarl", "baker", "there", "glyph", "pooch", "hippy", "spell", "folly", "louse", "gulch", "vault", "godly", "threw", "fleet", "grave", "inane", "shock", "crave", "spite", "valve", "skimp", "claim", "rainy", "musty", "pique", "daddy", "quasi", "arise", "aging", "valet", "opium", "avert", "stuck", "recut", "mulch", "genre", "plume", "rifle", "count", "incur", "total", "wrest", "mocha", "deter", "study", "lover", "safer", "rivet", "funny", "smoke", "mound", "undue", "sedan", "pagan", "swine", "guile", "gusty", "equip", "tough", "canoe", "chaos", "covet", "human", "udder", "lunch", "blast", "stray", "manga", "melee", "lefty", "quick", "paste", "given", "octet", "risen", "groan", "leaky", "grind", "carve", "loose", "sadly", "spilt", "apple", "slack", "honey", "final", "sheen", "eerie", "minty", "slick", "derby", "wharf", "spelt", "coach", "erupt", "singe", "price", "spawn", "fairy", "jiffy", "filmy", "stack", "chose", "sleep", "ardor", "nanny", "niece", "woozy", "handy", "grace", "ditto", "stank", "cream", "usual", "diode", "valor", "angle", "ninja", "muddy", "chase", "reply", "prone", "spoil", "heart", "shade", "diner", "arson", "onion", "sleet", "dowel", "couch", "palsy", "bowel", "smile", "evoke", "creek", "lance", "eagle", "idiot", "siren", "built", "embed", "award", "dross", "annul", "goody", "frown", "patio", "laden", "humid", "elite", "lymph", "edify", "might", "reset", "visit", "gusto", "purse", "vapor", "crock", "write", "sunny", "loath", "chaff", "slide", "queer", "venom", "stamp", "sorry", "still", "acorn", "aping", "pushy", "tamer", "hater", "mania", "awoke", "brawn", "swift", "exile", "birch", "lucky", "freer", "risky", "ghost", "plier", "lunar", "winch", "snare", "nurse", "house", "borax", "nicer", "lurch", "exalt", "about", "savvy", "toxin", "tunic", "pried", "inlay", "chump", "lanky", "cress", "eater", "elude", "cycle", "kitty", "boule", "moron", "tenet", "place", "lobby", "plush", "vigil", "index", "blink", "clung", "qualm", "croup", "clink", "juicy", "stage", "decay", "nerve", "flier", "shaft", "crook", "clean", "china", "ridge", "vowel", "gnome", "snuck", "icing", "spiny", "rigor", "snail", "flown", "rabid", "prose", "thank", "poppy", "budge", "fiber", "moldy", "dowdy", "kneel", "track", "caddy", "quell", "dumpy", "paler", "swore", "rebar", "scuba", "splat", "flyer", "horny", "mason", "doing", "ozone", "amply", "molar", "ovary", "beset", "queue", "cliff", "magic", "truce", "sport", "fritz", "edict", "twirl", "verse", "llama", "eaten", "range", "whisk", "hovel", "rehab", "macaw", "sigma", "spout", "verve", "sushi", "dying", "fetid", "brain", "buddy", "thump", "scion", "candy", "chord", "basin", "march", "crowd", "arbor", "gayly", "musky", "stain", "dally", "bless", "bravo", "stung", "title", "ruler", "kiosk", "blond", "ennui", "layer", "fluid", "tatty", "score", "cutie", "zebra", "barge", "matey", "bluer", "aider", "shook", "river", "privy", "betel", "frisk", "bongo", "begun", "azure", "weave", "genie", "sound", "glove", "braid", "scope", "wryly", "rover", "assay", "ocean", "bloom", "irate", "later", "woken", "silky", "wreck", "dwelt", "slate", "smack", "solid", "amaze", "hazel", "wrist", "jolly", "globe", "flint", "rouse", "civil", "vista", "relax", "cover", "alive", "beech", "jetty", "bliss", "vocal", "often", "dolly", "eight", "joker", "since", "event", "ensue", "shunt", "diver", "poser", "worst", "sweep", "alley", "creed", "anime", "leafy", "bosom", "dunce", "stare", "pudgy", "waive", "choir", "stood", "spoke", "outgo", "delay", "bilge", "ideal", "clasp", "seize", "hotly", "laugh", "sieve", "block", "meant", "grape", "noose", "hardy", "shied", "drawl", "daisy", "putty", "strut", "burnt", "tulip", "crick", "idyll", "vixen", "furor", "geeky", "cough", "naive", "shoal", "stork", "bathe", "aunty", "check", "prime", "brass", "outer", "furry", "razor", "elect", "evict", "imply", "demur", "quota", "haven", "cavil", "swear", "crump", "dough", "gavel", "wagon", "salon", "nudge", "harem", "pitch", "sworn", "pupil", "excel", "stony", "cabin", "unzip", "queen", "trout", "polyp", "earth", "storm", "until", "taper", "enter", "child", "adopt", "minor", "fatty", "husky", "brave", "filet", "slime", "glint", "tread", "steal", "regal", "guest", "every", "murky", "share", "spore", "hoist", "buxom", "inner", "otter", "dimly", "level", "sumac", "donut", "stilt", "arena", "sheet", "scrub", "fancy", "slimy", "pearl", "silly", "porch", "dingo", "sepia", "amble", "shady", "bread", "friar", "reign", "dairy", "quill", "cross", "brood", "tuber", "shear", "posit", "blank", "villa", "shank", "piggy", "freak", "which", "among", "fecal", "shell", "would", "algae", "large", "rabbi", "agony", "amuse", "bushy", "copse", "swoon", "knife", "pouch", "ascot", "plane", "crown", "urban", "snide", "relay", "abide", "viola", "rajah", "straw", "dilly", "crash", "amass", "third", "trick", "tutor", "woody", "blurb", "grief", "disco", "where", "sassy", "beach", "sauna", "comic", "clued", "creep", "caste", "graze", "snuff", "frock", "gonad", "drunk", "prong", "lurid", "steel", "halve", "buyer", "vinyl", "utile", "smell", "adage", "worry", "tasty", "local", "trade", "finch", "ashen", "modal", "gaunt", "clove", "enact", "adorn", "roast", "speck", "sheik", "missy", "grunt", "snoop", "party", "touch", "mafia", "emcee", "array", "south", "vapid", "jelly", "skulk", "angst", "tubal", "lower", "crest", "sweat", "cyber", "adore", "tardy", "swami", "notch", "groom", "roach", "hitch", "young", "align", "ready", "frond", "strap", "puree", "realm", "venue", "swarm", "offer", "seven", "dryer", "diary", "dryly", "drank", "acrid", "heady", "theta", "junto", "pixie", "quoth", "bonus", "shalt", "penne", "amend", "datum", "build", "piano", "shelf", "lodge", "suing", "rearm", "coral", "ramen", "worth", "psalm", "infer", "overt", "mayor", "ovoid", "glide", "usage", "poise", "randy", "chuck", "prank", "fishy", "tooth", "ether", "drove", "idler", "swath", "stint", "while", "begat", "apply", "slang", "tarot", "radar", "credo", "aware", "canon", "shift", "timer", "bylaw", "serum", "three", "steak", "iliac", "shirk", "blunt", "puppy", "penal", "joist", "bunny", "shape", "beget", "wheel", "adept", "stunt", "stole", "topaz", "chore", "fluke", "afoot", "bloat", "bully", "dense", "caper", "sneer", "boxer", "jumbo", "lunge", "space", "avail", "short", "slurp", "loyal", "flirt", "pizza", "conch", "tempo", "droop", "plate", "bible", "plunk", "afoul", "savoy", "steep", "agile", "stake", "dwell", "knave", "beard", "arose", "motif", "smash", "broil", "glare", "shove", "baggy", "mammy", "swamp", "along", "rugby", "wager", "quack", "squat", "snaky", "debit", "mange", "skate", "ninth", "joust", "tramp", "spurn", "medal", "micro", "rebel", "flank", "learn", "nadir", "maple", "comfy", "remit", "gruff", "ester", "least", "mogul", "fetch", "cause", "oaken", "aglow", "meaty", "gaffe", "shyly", "racer", "prowl", "thief", "stern", "poesy", "rocky", "tweet", "waist", "spire", "grope", "havoc", "patsy", "truly", "forty", "deity", "uncle", "swish", "giver", "preen", "bevel", "lemur", "draft", "slope", "annoy", "lingo", "bleak", "ditty", "curly", "cedar", "dirge", "grown", "horde", "drool", "shuck", "crypt", "cumin", "stock", "gravy", "locus", "wider", "breed", "quite", "chafe", "cache", "blimp", "deign", "fiend", "logic", "cheap", "elide", "rigid", "false", "renal", "pence", "rowdy", "shoot", "blaze", "envoy", "posse", "brief", "never", "abort", "mouse", "mucky", "sulky", "fiery", "media", "trunk", "yeast", "clear", "skunk", "scalp", "bitty", "cider", "koala", "duvet", "segue", "creme", "super", "grill", "after", "owner", "ember", "reach", "nobly", "empty", "speed", "gipsy", "recur", "smock", "dread", "merge", "burst", "kappa", "amity", "shaky", "hover", "carol", "snort", "synod", "faint", "haunt", "flour", "chair", "detox", "shrew", "tense", "plied", "quark", "burly", "novel", "waxen", "stoic", "jerky", "blitz", "beefy", "lyric", "hussy", "towel", "quilt", "below", "bingo", "wispy", "brash", "scone", "toast", "easel", "saucy", "value", "spice", "honor", "route", "sharp", "bawdy", "radii", "skull", "phony", "issue", "lager", "swell", "urine", "gassy", "trial", "flora", "upper", "latch", "wight", "brick", "retry", "holly", "decal", "grass", "shack", "dogma", "mover", "defer", "sober", "optic", "crier", "vying", "nomad", "flute", "hippo", "shark", "drier", "obese", "bugle", "tawny", "chalk", "feast", "ruddy", "pedal", "scarf", "cruel", "bleat", "tidal", "slush", "semen", "windy", "dusty", "sally", "igloo", "nerdy", "jewel", "shone", "whale", "hymen", "abuse", "fugue", "elbow", "crumb", "pansy", "welsh", "syrup", "terse", "suave", "gamut", "swung", "drake", "freed", "afire", "shirt", "grout", "oddly", "tithe", "plaid", "dummy", "broom", "blind", "torch", "enemy", "again", "tying", "pesky", "alter", "gazer", "noble", "ethos", "bride", "extol", "decor", "hobby", "beast", "idiom", "utter", "these", "sixth", "alarm", "erase", "elegy", "spunk", "piper", "scaly", "scold", "hefty", "chick", "sooty", "canal", "whiny", "slash", "quake", "joint", "swept", "prude", "heavy", "wield", "femme", "lasso", "maize", "shale", "screw", "spree", "smoky", "whiff", "scent", "glade", "spent", "prism", "stoke", "riper", "orbit", "cocoa", "guilt", "humus", "shush", "table", "smirk", "wrong", "noisy", "alert", "shiny", "elate", "resin", "whole", "hunch", "pixel", "polar", "hotel", "sword", "cleat", "mango", "rumba", "puffy", "filly", "billy", "leash", "clout", "dance", "ovate", "facet", "chili", "paint", "liner", "curio", "salty", "audio", "snake", "fable", "cloak", "navel", "spurt", "pesto", "balmy", "flash", "unwed", "early", "churn", "weedy", "stump", "lease", "witty", "wimpy", "spoof", "saner", "blend", "salsa", "thick", "warty", "manic", "blare", "squib", "spoon", "probe", "crepe", "knack", "force", "debut", "order", "haste", "teeth", "agent", "widen", "icily", "slice", "ingot", "clash", "juror", "blood", "abode", "throw", "unity", "pivot", "slept", "troop", "spare", "sewer", "parse", "morph", "cacti", "tacky", "spool", "demon", "moody", "annex", "begin", "fuzzy", "patch", "water", "lumpy", "admin", "omega", "limit", "tabby", "macho", "aisle", "skiff", "basis", "plank", "verge", "botch", "crawl", "lousy", "slain", "cubic", "raise", "wrack", "guide", "foist", "cameo", "under", "actor", "revue", "fraud", "harpy", "scoop", "climb", "refer", "olden", "clerk", "debar", "tally", "ethic", "cairn", "tulle", "ghoul", "hilly", "crude", "apart", "scale", "older", "plain", "sperm", "briny", "abbot", "rerun", "quest", "crisp", "bound", "befit", "drawn", "suite", "itchy", "cheer", "bagel", "guess", "broad", "axiom", "chard", "caput", "leant", "harsh", "curse", "proud", "swing", "opine", "taste", "lupus", "gumbo", "miner", "green", "chasm", "lipid", "topic", "armor", "brush", "crane", "mural", "abled", "habit", "bossy", "maker", "dusky", "dizzy", "lithe", "brook", "jazzy", "fifty", "sense", "giant", "surly", "legal", "fatal", "flunk", "began", "prune", "small", "slant", "scoff", "torus", "ninny", "covey", "viper", "taken", "moral", "vogue", "owing", "token", "entry", "booth", "voter", "chide", "elfin", "ebony", "neigh", "minim", "melon", "kneed", "decoy", "voila", "ankle", "arrow", "mushy", "tribe", "cease", "eager", "birth", "graph", "odder", "terra", "weird", "tried", "clack", "color", "rough", "weigh", "uncut", "ladle", "strip", "craft", "minus", "dicey", "titan", "lucid", "vicar", "dress", "ditch", "gypsy", "pasta", "taffy", "flame", "swoop", "aloof", "sight", "broke", "teary", "chart", "sixty", "wordy", "sheer", "leper", "nosey", "bulge", "savor", "clamp", "funky", "foamy", "toxic", "brand", "plumb", "dingy", "butte", "drill", "tripe", "bicep", "tenor", "krill", "worse", "drama", "hyena", "think", "ratio", "cobra", "basil", "scrum", "bused", "phone", "court", "camel", "proof", "heard", "angel", "petal", "pouty", "throb", "maybe", "fetal", "sprig", "spine", "shout", "cadet", "macro", "dodgy", "satyr", "rarer", "binge", "trend", "nutty", "leapt", "amiss", "split", "myrrh", "width", "sonar", "tower", "baron", "fever", "waver", "spark", "belie", "sloop", "expel", "smote", "baler", "above", "north", "wafer", "scant", "frill", "awash", "snack", "scowl", "frail", "drift", "limbo", "fence", "motel", "ounce", "wreak", "revel", "talon", "prior", "knelt", "cello", "flake", "debug", "anode", "crime", "salve", "scout", "imbue", "pinky", "stave", "vague", "chock", "fight", "video", "stone", "teach", "cleft", "frost", "prawn", "booty", "twist", "apnea", "stiff", "plaza", "ledge", "tweak", "board", "grant", "medic", "bacon", "cable", "brawl", "slunk", "raspy", "forum", "drone", "women", "mucus", "boast", "toddy", "coven", "tumor", "truer", "wrath", "stall", "steam", "axial", "purer", "daily", "trail", "niche", "mealy", "juice", "nylon", "plump", "merry", "flail", "papal", "wheat", "berry", "cower", "erect", "brute", "leggy", "snipe", "sinew", "skier", "penny", "jumpy", "rally", "umbra", "scary", "modem", "gross", "avian", "greed", "satin", "tonic", "parka", "sniff", "livid", "stark", "trump", "giddy", "reuse", "taboo", "avoid", "quote", "devil", "liken", "gloss", "gayer", "beret", "noise", "gland", "dealt", "sling", "rumor", "opera", "thigh", "tonga", "flare", "wound", "white", "bulky", "etude", "horse", "circa", "paddy", "inbox", "fizzy", "grain", "exert", "surge", "gleam", "belle", "salvo", "crush", "fruit", "sappy", "taker", "tract", "ovine", "spiky", "frank", "reedy", "filth", "spasm", "heave", "mambo", "right", "clank", "trust", "lumen", "borne", "spook", "sauce", "amber", "lathe", "carat", "corer", "dirty", "slyly", "affix", "alloy", "taint", "sheep", "kinky", "wooly", "mauve", "flung", "yacht", "fried", "quail", "brunt", "grimy", "curvy", "cagey", "rinse", "deuce", "state", "grasp", "milky", "bison", "graft", "sandy", "baste", "flask", "hedge", "girly", "swash", "boney", "coupe", "endow", "abhor", "welch", "blade", "tight", "geese", "miser", "mirth", "cloud", "cabal", "leech", "close", "tenth", "pecan", "droit", "grail", "clone", "guise", "ralph", "tango", "biddy", "smith", "mower", "payee", "serif", "drape", "fifth", "spank", "glaze", "allot", "truck", "kayak", "virus", "testy", "tepee", "fully", "zonal", "metro", "curry", "grand", "banjo", "axion", "bezel", "occur", "chain", "nasal", "gooey", "filer", "brace", "allay", "pubic", "raven", "plead", "gnash", "flaky", "munch", "dully", "eking", "thing", "slink", "hurry", "theft", "shorn", "pygmy", "ranch", "wring", "lemon", "shore", "mamma", "froze", "newer", "style", "moose", "antic", "drown", "vegan", "chess", "guppy", "union", "lever", "lorry", "image", "cabby", "druid", "exact", "truth", "dopey", "spear", "cried", "chime", "crony", "stunk", "timid", "batch", "gauge", "rotor", "crack", "curve", "latte", "witch", "bunch", "repel", "anvil", "soapy", "meter", "broth", "madly", "dried", "scene", "known", "magma", "roost", "woman", "thong", "punch", "pasty", "downy", "knead", "whirl", "rapid", "clang", "anger", "drive", "goofy", "email", "music", "stuff", "bleep", "rider", "mecca", "folio", "setup", "verso", "quash", "fauna", "gummy", "happy", "newly", "fussy", "relic", "guava", "ratty", "fudge", "femur", "chirp", "forte", "alibi", "whine", "petty", "golly", "plait", "fleck", "felon", "gourd", "brown", "thrum", "ficus", "stash", "decry", "wiser", "junta", "visor", "daunt", "scree", "impel", "await", "press", "whose", "turbo", "stoop", "speak", "mangy", "eying", "inlet", "crone", "pulse", "mossy", "staid", "hence", "pinch", "teddy", "sully", "snore", "ripen", "snowy", "attic", "going", "leach", "mouth", "hound", "clump", "tonal", "bigot", "peril", "piece", "blame", "haute", "spied", "undid", "intro", "basal", "shine", "gecko", "rodeo", "guard", "steer", "loamy", "scamp", "scram", "manly", "hello", "vaunt", "organ", "feral", "knock", "extra", "condo", "adapt", "willy", "polka", "rayon", "skirt", "faith", "torso", "match", "mercy", "tepid", "sleek", "riser", "twixt", "peace", "flush", "catty", "login", "eject", "roger", "rival", "untie", "refit", "aorta", "adult", "judge", "rower", "artsy", "rural", "shave" -------------------------------------------------------------------------------- /.ipynb_checkpoints/solutions-checkpoint.txt: -------------------------------------------------------------------------------- 1 | "cigar", "rebut", "sissy", "humph", "awake", "blush", "focal", "evade", "naval", "serve", "heath", "dwarf", "model", "karma", "stink", "grade", "quiet", "bench", "abate", "feign", "major", "death", "fresh", "crust", "stool", "colon", "abase", "marry", "react", "batty", "pride", "floss", "helix", "croak", "staff", "paper", "unfed", "whelp", "trawl", "outdo", "adobe", "crazy", "sower", "repay", "digit", "crate", "cluck", "spike", "mimic", "pound", "maxim", "linen", "unmet", "flesh", "booby", "forth", "first", "stand", "belly", "ivory", "seedy", "print", "yearn", "drain", "bribe", "stout", "panel", "crass", "flume", "offal", "agree", "error", "swirl", "argue", "bleed", "delta", "flick", "totem", "wooer", "front", "shrub", "parry", "biome", "lapel", "start", "greet", "goner", "golem", "lusty", "loopy", "round", "audit", "lying", "gamma", "labor", "islet", "civic", "forge", "corny", "moult", "basic", "salad", "agate", "spicy", "spray", "essay", "fjord", "spend", "kebab", "guild", "aback", "motor", "alone", "hatch", "hyper", "thumb", "dowry", "ought", "belch", "dutch", "pilot", "tweed", "comet", "jaunt", "enema", "steed", "abyss", "growl", "fling", "dozen", "boozy", "erode", "world", "gouge", "click", "briar", "great", "altar", "pulpy", "blurt", "coast", "duchy", "groin", "fixer", "group", "rogue", "badly", "smart", "pithy", "gaudy", "chill", "heron", "vodka", "finer", "surer", "radio", "rouge", "perch", "retch", "wrote", "clock", "tilde", "store", "prove", "bring", "solve", "cheat", "grime", "exult", "usher", "epoch", "triad", "break", "rhino", "viral", "conic", "masse", "sonic", "vital", "trace", "using", "peach", "champ", "baton", "brake", "pluck", "craze", "gripe", "weary", "picky", "acute", "ferry", "aside", "tapir", "troll", "unify", "rebus", "boost", "truss", "siege", "tiger", "banal", "slump", "crank", "gorge", "query", "drink", "favor", "abbey", "tangy", "panic", "solar", "shire", "proxy", "point", "robot", "prick", "wince", "crimp", "knoll", "sugar", "whack", "mount", "perky", "could", "wrung", "light", "those", "moist", "shard", "pleat", "aloft", "skill", "elder", "frame", "humor", "pause", "ulcer", "ultra", "robin", "cynic", "agora", "aroma", "caulk", "shake", "pupal", "dodge", "swill", "tacit", "other", "thorn", "trove", "bloke", "vivid", "spill", "chant", "choke", "rupee", "nasty", "mourn", "ahead", "brine", "cloth", "hoard", "sweet", "month", "lapse", "watch", "today", "focus", "smelt", "tease", "cater", "movie", "lynch", "saute", "allow", "renew", "their", "slosh", "purge", "chest", "depot", "epoxy", "nymph", "found", "shall", "harry", "stove", "lowly", "snout", "trope", "fewer", "shawl", "natal", "fibre", "comma", "foray", "scare", "stair", "black", "squad", "royal", "chunk", "mince", "slave", "shame", "cheek", "ample", "flair", "foyer", "cargo", "oxide", "plant", "olive", "inert", "askew", "heist", "shown", "zesty", "hasty", "trash", "fella", "larva", "forgo", "story", "hairy", "train", "homer", "badge", "midst", "canny", "fetus", "butch", "farce", "slung", "tipsy", "metal", "yield", "delve", "being", "scour", "glass", "gamer", "scrap", "money", "hinge", "album", "vouch", "asset", "tiara", "crept", "bayou", "atoll", "manor", "creak", "showy", "phase", "froth", "depth", "gloom", "flood", "trait", "girth", "piety", "payer", "goose", "float", "donor", "atone", "primo", "apron", "blown", "cacao", "loser", "input", "gloat", "awful", "brink", "smite", "beady", "rusty", "retro", "droll", "gawky", "hutch", "pinto", "gaily", "egret", "lilac", "sever", "field", "fluff", "hydro", "flack", "agape", "wench", "voice", "stead", "stalk", "berth", "madam", "night", "bland", "liver", "wedge", "augur", "roomy", "wacky", "flock", "angry", "bobby", "trite", "aphid", "tryst", "midge", "power", "elope", "cinch", "motto", "stomp", "upset", "bluff", "cramp", "quart", "coyly", "youth", "rhyme", "buggy", "alien", "smear", "unfit", "patty", "cling", "glean", "label", "hunky", "khaki", "poker", "gruel", "twice", "twang", "shrug", "treat", "unlit", "waste", "merit", "woven", "octal", "needy", "clown", "widow", "irony", "ruder", "gauze", "chief", "onset", "prize", "fungi", "charm", "gully", "inter", "whoop", "taunt", "leery", "class", "theme", "lofty", "tibia", "booze", "alpha", "thyme", "eclat", "doubt", "parer", "chute", "stick", "trice", "alike", "sooth", "recap", "saint", "liege", "glory", "grate", "admit", "brisk", "soggy", "usurp", "scald", "scorn", "leave", "twine", "sting", "bough", "marsh", "sloth", "dandy", "vigor", "howdy", "enjoy", "valid", "ionic", "equal", "unset", "floor", "catch", "spade", "stein", "exist", "quirk", "denim", "grove", "spiel", "mummy", "fault", "foggy", "flout", "carry", "sneak", "libel", "waltz", "aptly", "piney", "inept", "aloud", "photo", "dream", "stale", "vomit", "ombre", "fanny", "unite", "snarl", "baker", "there", "glyph", "pooch", "hippy", "spell", "folly", "louse", "gulch", "vault", "godly", "threw", "fleet", "grave", "inane", "shock", "crave", "spite", "valve", "skimp", "claim", "rainy", "musty", "pique", "daddy", "quasi", "arise", "aging", "valet", "opium", "avert", "stuck", "recut", "mulch", "genre", "plume", "rifle", "count", "incur", "total", "wrest", "mocha", "deter", "study", "lover", "safer", "rivet", "funny", "smoke", "mound", "undue", "sedan", "pagan", "swine", "guile", "gusty", "equip", "tough", "canoe", "chaos", "covet", "human", "udder", "lunch", "blast", "stray", "manga", "melee", "lefty", "quick", "paste", "given", "octet", "risen", "groan", "leaky", "grind", "carve", "loose", "sadly", "spilt", "apple", "slack", "honey", "final", "sheen", "eerie", "minty", "slick", "derby", "wharf", "spelt", "coach", "erupt", "singe", "price", "spawn", "fairy", "jiffy", "filmy", "stack", "chose", "sleep", "ardor", "nanny", "niece", "woozy", "handy", "grace", "ditto", "stank", "cream", "usual", "diode", "valor", "angle", "ninja", "muddy", "chase", "reply", "prone", "spoil", "heart", "shade", "diner", "arson", "onion", "sleet", "dowel", "couch", "palsy", "bowel", "smile", "evoke", "creek", "lance", "eagle", "idiot", "siren", "built", "embed", "award", "dross", "annul", "goody", "frown", "patio", "laden", "humid", "elite", "lymph", "edify", "might", "reset", "visit", "gusto", "purse", "vapor", "crock", "write", "sunny", "loath", "chaff", "slide", "queer", "venom", "stamp", "sorry", "still", "acorn", "aping", "pushy", "tamer", "hater", "mania", "awoke", "brawn", "swift", "exile", "birch", "lucky", "freer", "risky", "ghost", "plier", "lunar", "winch", "snare", "nurse", "house", "borax", "nicer", "lurch", "exalt", "about", "savvy", "toxin", "tunic", "pried", "inlay", "chump", "lanky", "cress", "eater", "elude", "cycle", "kitty", "boule", "moron", "tenet", "place", "lobby", "plush", "vigil", "index", "blink", "clung", "qualm", "croup", "clink", "juicy", "stage", "decay", "nerve", "flier", "shaft", "crook", "clean", "china", "ridge", "vowel", "gnome", "snuck", "icing", "spiny", "rigor", "snail", "flown", "rabid", "prose", "thank", "poppy", "budge", "fiber", "moldy", "dowdy", "kneel", "track", "caddy", "quell", "dumpy", "paler", "swore", "rebar", "scuba", "splat", "flyer", "horny", "mason", "doing", "ozone", "amply", "molar", "ovary", "beset", "queue", "cliff", "magic", "truce", "sport", "fritz", "edict", "twirl", "verse", "llama", "eaten", "range", "whisk", "hovel", "rehab", "macaw", "sigma", "spout", "verve", "sushi", "dying", "fetid", "brain", "buddy", "thump", "scion", "candy", "chord", "basin", "march", "crowd", "arbor", "gayly", "musky", "stain", "dally", "bless", "bravo", "stung", "title", "ruler", "kiosk", "blond", "ennui", "layer", "fluid", "tatty", "score", "cutie", "zebra", "barge", "matey", "bluer", "aider", "shook", "river", "privy", "betel", "frisk", "bongo", "begun", "azure", "weave", "genie", "sound", "glove", "braid", "scope", "wryly", "rover", "assay", "ocean", "bloom", "irate", "later", "woken", "silky", "wreck", "dwelt", "slate", "smack", "solid", "amaze", "hazel", "wrist", "jolly", "globe", "flint", "rouse", "civil", "vista", "relax", "cover", "alive", "beech", "jetty", "bliss", "vocal", "often", "dolly", "eight", "joker", "since", "event", "ensue", "shunt", "diver", "poser", "worst", "sweep", "alley", "creed", "anime", "leafy", "bosom", "dunce", "stare", "pudgy", "waive", "choir", "stood", "spoke", "outgo", "delay", "bilge", "ideal", "clasp", "seize", "hotly", "laugh", "sieve", "block", "meant", "grape", "noose", "hardy", "shied", "drawl", "daisy", "putty", "strut", "burnt", "tulip", "crick", "idyll", "vixen", "furor", "geeky", "cough", "naive", "shoal", "stork", "bathe", "aunty", "check", "prime", "brass", "outer", "furry", "razor", "elect", "evict", "imply", "demur", "quota", "haven", "cavil", "swear", "crump", "dough", "gavel", "wagon", "salon", "nudge", "harem", "pitch", "sworn", "pupil", "excel", "stony", "cabin", "unzip", "queen", "trout", "polyp", "earth", "storm", "until", "taper", "enter", "child", "adopt", "minor", "fatty", "husky", "brave", "filet", "slime", "glint", "tread", "steal", "regal", "guest", "every", "murky", "share", "spore", "hoist", "buxom", "inner", "otter", "dimly", "level", "sumac", "donut", "stilt", "arena", "sheet", "scrub", "fancy", "slimy", "pearl", "silly", "porch", "dingo", "sepia", "amble", "shady", "bread", "friar", "reign", "dairy", "quill", "cross", "brood", "tuber", "shear", "posit", "blank", "villa", "shank", "piggy", "freak", "which", "among", "fecal", "shell", "would", "algae", "large", "rabbi", "agony", "amuse", "bushy", "copse", "swoon", "knife", "pouch", "ascot", "plane", "crown", "urban", "snide", "relay", "abide", "viola", "rajah", "straw", "dilly", "crash", "amass", "third", "trick", "tutor", "woody", "blurb", "grief", "disco", "where", "sassy", "beach", "sauna", "comic", "clued", "creep", "caste", "graze", "snuff", "frock", "gonad", "drunk", "prong", "lurid", "steel", "halve", "buyer", "vinyl", "utile", "smell", "adage", "worry", "tasty", "local", "trade", "finch", "ashen", "modal", "gaunt", "clove", "enact", "adorn", "roast", "speck", "sheik", "missy", "grunt", "snoop", "party", "touch", "mafia", "emcee", "array", "south", "vapid", "jelly", "skulk", "angst", "tubal", "lower", "crest", "sweat", "cyber", "adore", "tardy", "swami", "notch", "groom", "roach", "hitch", "young", "align", "ready", "frond", "strap", "puree", "realm", "venue", "swarm", "offer", "seven", "dryer", "diary", "dryly", "drank", "acrid", "heady", "theta", "junto", "pixie", "quoth", "bonus", "shalt", "penne", "amend", "datum", "build", "piano", "shelf", "lodge", "suing", "rearm", "coral", "ramen", "worth", "psalm", "infer", "overt", "mayor", "ovoid", "glide", "usage", "poise", "randy", "chuck", "prank", "fishy", "tooth", "ether", "drove", "idler", "swath", "stint", "while", "begat", "apply", "slang", "tarot", "radar", "credo", "aware", "canon", "shift", "timer", "bylaw", "serum", "three", "steak", "iliac", "shirk", "blunt", "puppy", "penal", "joist", "bunny", "shape", "beget", "wheel", "adept", "stunt", "stole", "topaz", "chore", "fluke", "afoot", "bloat", "bully", "dense", "caper", "sneer", "boxer", "jumbo", "lunge", "space", "avail", "short", "slurp", "loyal", "flirt", "pizza", "conch", "tempo", "droop", "plate", "bible", "plunk", "afoul", "savoy", "steep", "agile", "stake", "dwell", "knave", "beard", "arose", "motif", "smash", "broil", "glare", "shove", "baggy", "mammy", "swamp", "along", "rugby", "wager", "quack", "squat", "snaky", "debit", "mange", "skate", "ninth", "joust", "tramp", "spurn", "medal", "micro", "rebel", "flank", "learn", "nadir", "maple", "comfy", "remit", "gruff", "ester", "least", "mogul", "fetch", "cause", "oaken", "aglow", "meaty", "gaffe", "shyly", "racer", "prowl", "thief", "stern", "poesy", "rocky", "tweet", "waist", "spire", "grope", "havoc", "patsy", "truly", "forty", "deity", "uncle", "swish", "giver", "preen", "bevel", "lemur", "draft", "slope", "annoy", "lingo", "bleak", "ditty", "curly", "cedar", "dirge", "grown", "horde", "drool", "shuck", "crypt", "cumin", "stock", "gravy", "locus", "wider", "breed", "quite", "chafe", "cache", "blimp", "deign", "fiend", "logic", "cheap", "elide", "rigid", "false", "renal", "pence", "rowdy", "shoot", "blaze", "envoy", "posse", "brief", "never", "abort", "mouse", "mucky", "sulky", "fiery", "media", "trunk", "yeast", "clear", "skunk", "scalp", "bitty", "cider", "koala", "duvet", "segue", "creme", "super", "grill", "after", "owner", "ember", "reach", "nobly", "empty", "speed", "gipsy", "recur", "smock", "dread", "merge", "burst", "kappa", "amity", "shaky", "hover", "carol", "snort", "synod", "faint", "haunt", "flour", "chair", "detox", "shrew", "tense", "plied", "quark", "burly", "novel", "waxen", "stoic", "jerky", "blitz", "beefy", "lyric", "hussy", "towel", "quilt", "below", "bingo", "wispy", "brash", "scone", "toast", "easel", "saucy", "value", "spice", "honor", "route", "sharp", "bawdy", "radii", "skull", "phony", "issue", "lager", "swell", "urine", "gassy", "trial", "flora", "upper", "latch", "wight", "brick", "retry", "holly", "decal", "grass", "shack", "dogma", "mover", "defer", "sober", "optic", "crier", "vying", "nomad", "flute", "hippo", "shark", "drier", "obese", "bugle", "tawny", "chalk", "feast", "ruddy", "pedal", "scarf", "cruel", "bleat", "tidal", "slush", "semen", "windy", "dusty", "sally", "igloo", "nerdy", "jewel", "shone", "whale", "hymen", "abuse", "fugue", "elbow", "crumb", "pansy", "welsh", "syrup", "terse", "suave", "gamut", "swung", "drake", "freed", "afire", "shirt", "grout", "oddly", "tithe", "plaid", "dummy", "broom", "blind", "torch", "enemy", "again", "tying", "pesky", "alter", "gazer", "noble", "ethos", "bride", "extol", "decor", "hobby", "beast", "idiom", "utter", "these", "sixth", "alarm", "erase", "elegy", "spunk", "piper", "scaly", "scold", "hefty", "chick", "sooty", "canal", "whiny", "slash", "quake", "joint", "swept", "prude", "heavy", "wield", "femme", "lasso", "maize", "shale", "screw", "spree", "smoky", "whiff", "scent", "glade", "spent", "prism", "stoke", "riper", "orbit", "cocoa", "guilt", "humus", "shush", "table", "smirk", "wrong", "noisy", "alert", "shiny", "elate", "resin", "whole", "hunch", "pixel", "polar", "hotel", "sword", "cleat", "mango", "rumba", "puffy", "filly", "billy", "leash", "clout", "dance", "ovate", "facet", "chili", "paint", "liner", "curio", "salty", "audio", "snake", "fable", "cloak", "navel", "spurt", "pesto", "balmy", "flash", "unwed", "early", "churn", "weedy", "stump", "lease", "witty", "wimpy", "spoof", "saner", "blend", "salsa", "thick", "warty", "manic", "blare", "squib", "spoon", "probe", "crepe", "knack", "force", "debut", "order", "haste", "teeth", "agent", "widen", "icily", "slice", "ingot", "clash", "juror", "blood", "abode", "throw", "unity", "pivot", "slept", "troop", "spare", "sewer", "parse", "morph", "cacti", "tacky", "spool", "demon", "moody", "annex", "begin", "fuzzy", "patch", "water", "lumpy", "admin", "omega", "limit", "tabby", "macho", "aisle", "skiff", "basis", "plank", "verge", "botch", "crawl", "lousy", "slain", "cubic", "raise", "wrack", "guide", "foist", "cameo", "under", "actor", "revue", "fraud", "harpy", "scoop", "climb", "refer", "olden", "clerk", "debar", "tally", "ethic", "cairn", "tulle", "ghoul", "hilly", "crude", "apart", "scale", "older", "plain", "sperm", "briny", "abbot", "rerun", "quest", "crisp", "bound", "befit", "drawn", "suite", "itchy", "cheer", "bagel", "guess", "broad", "axiom", "chard", "caput", "leant", "harsh", "curse", "proud", "swing", "opine", "taste", "lupus", "gumbo", "miner", "green", "chasm", "lipid", "topic", "armor", "brush", "crane", "mural", "abled", "habit", "bossy", "maker", "dusky", "dizzy", "lithe", "brook", "jazzy", "fifty", "sense", "giant", "surly", "legal", "fatal", "flunk", "began", "prune", "small", "slant", "scoff", "torus", "ninny", "covey", "viper", "taken", "moral", "vogue", "owing", "token", "entry", "booth", "voter", "chide", "elfin", "ebony", "neigh", "minim", "melon", "kneed", "decoy", "voila", "ankle", "arrow", "mushy", "tribe", "cease", "eager", "birth", "graph", "odder", "terra", "weird", "tried", "clack", "color", "rough", "weigh", "uncut", "ladle", "strip", "craft", "minus", "dicey", "titan", "lucid", "vicar", "dress", "ditch", "gypsy", "pasta", "taffy", "flame", "swoop", "aloof", "sight", "broke", "teary", "chart", "sixty", "wordy", "sheer", "leper", "nosey", "bulge", "savor", "clamp", "funky", "foamy", "toxic", "brand", "plumb", "dingy", "butte", "drill", "tripe", "bicep", "tenor", "krill", "worse", "drama", "hyena", "think", "ratio", "cobra", "basil", "scrum", "bused", "phone", "court", "camel", "proof", "heard", "angel", "petal", "pouty", "throb", "maybe", "fetal", "sprig", "spine", "shout", "cadet", "macro", "dodgy", "satyr", "rarer", "binge", "trend", "nutty", "leapt", "amiss", "split", "myrrh", "width", "sonar", "tower", "baron", "fever", "waver", "spark", "belie", "sloop", "expel", "smote", "baler", "above", "north", "wafer", "scant", "frill", "awash", "snack", "scowl", "frail", "drift", "limbo", "fence", "motel", "ounce", "wreak", "revel", "talon", "prior", "knelt", "cello", "flake", "debug", "anode", "crime", "salve", "scout", "imbue", "pinky", "stave", "vague", "chock", "fight", "video", "stone", "teach", "cleft", "frost", "prawn", "booty", "twist", "apnea", "stiff", "plaza", "ledge", "tweak", "board", "grant", "medic", "bacon", "cable", "brawl", "slunk", "raspy", "forum", "drone", "women", "mucus", "boast", "toddy", "coven", "tumor", "truer", "wrath", "stall", "steam", "axial", "purer", "daily", "trail", "niche", "mealy", "juice", "nylon", "plump", "merry", "flail", "papal", "wheat", "berry", "cower", "erect", "brute", "leggy", "snipe", "sinew", "skier", "penny", "jumpy", "rally", "umbra", "scary", "modem", "gross", "avian", "greed", "satin", "tonic", "parka", "sniff", "livid", "stark", "trump", "giddy", "reuse", "taboo", "avoid", "quote", "devil", "liken", "gloss", "gayer", "beret", "noise", "gland", "dealt", "sling", "rumor", "opera", "thigh", "tonga", "flare", "wound", "white", "bulky", "etude", "horse", "circa", "paddy", "inbox", "fizzy", "grain", "exert", "surge", "gleam", "belle", "salvo", "crush", "fruit", "sappy", "taker", "tract", "ovine", "spiky", "frank", "reedy", "filth", "spasm", "heave", "mambo", "right", "clank", "trust", "lumen", "borne", "spook", "sauce", "amber", "lathe", "carat", "corer", "dirty", "slyly", "affix", "alloy", "taint", "sheep", "kinky", "wooly", "mauve", "flung", "yacht", "fried", "quail", "brunt", "grimy", "curvy", "cagey", "rinse", "deuce", "state", "grasp", "milky", "bison", "graft", "sandy", "baste", "flask", "hedge", "girly", "swash", "boney", "coupe", "endow", "abhor", "welch", "blade", "tight", "geese", "miser", "mirth", "cloud", "cabal", "leech", "close", "tenth", "pecan", "droit", "grail", "clone", "guise", "ralph", "tango", "biddy", "smith", "mower", "payee", "serif", "drape", "fifth", "spank", "glaze", "allot", "truck", "kayak", "virus", "testy", "tepee", "fully", "zonal", "metro", "curry", "grand", "banjo", "axion", "bezel", "occur", "chain", "nasal", "gooey", "filer", "brace", "allay", "pubic", "raven", "plead", "gnash", "flaky", "munch", "dully", "eking", "thing", "slink", "hurry", "theft", "shorn", "pygmy", "ranch", "wring", "lemon", "shore", "mamma", "froze", "newer", "style", "moose", "antic", "drown", "vegan", "chess", "guppy", "union", "lever", "lorry", "image", "cabby", "druid", "exact", "truth", "dopey", "spear", "cried", "chime", "crony", "stunk", "timid", "batch", "gauge", "rotor", "crack", "curve", "latte", "witch", "bunch", "repel", "anvil", "soapy", "meter", "broth", "madly", "dried", "scene", "known", "magma", "roost", "woman", "thong", "punch", "pasty", "downy", "knead", "whirl", "rapid", "clang", "anger", "drive", "goofy", "email", "music", "stuff", "bleep", "rider", "mecca", "folio", "setup", "verso", "quash", "fauna", "gummy", "happy", "newly", "fussy", "relic", "guava", "ratty", "fudge", "femur", "chirp", "forte", "alibi", "whine", "petty", "golly", "plait", "fleck", "felon", "gourd", "brown", "thrum", "ficus", "stash", "decry", "wiser", "junta", "visor", "daunt", "scree", "impel", "await", "press", "whose", "turbo", "stoop", "speak", "mangy", "eying", "inlet", "crone", "pulse", "mossy", "staid", "hence", "pinch", "teddy", "sully", "snore", "ripen", "snowy", "attic", "going", "leach", "mouth", "hound", "clump", "tonal", "bigot", "peril", "piece", "blame", "haute", "spied", "undid", "intro", "basal", "shine", "gecko", "rodeo", "guard", "steer", "loamy", "scamp", "scram", "manly", "hello", "vaunt", "organ", "feral", "knock", "extra", "condo", "adapt", "willy", "polka", "rayon", "skirt", "faith", "torso", "match", "mercy", "tepid", "sleek", "riser", "twixt", "peace", "flush", "catty", "login", "eject", "roger", "rival", "untie", "refit", "aorta", "adult", "judge", "rower", "artsy", "rural", "shave" -------------------------------------------------------------------------------- /utils.jl: -------------------------------------------------------------------------------- 1 | # For plotting operations 2 | using PyPlot 3 | # For @showprogress 4 | using ProgressMeter 5 | # for countmap 6 | using StatsBase 7 | # for Statistics.mean 8 | using Statistics 9 | 10 | function parse_word_list(filename::String)::Vector{String} 11 | s = open(filename) do file 12 | read(file, String) 13 | end 14 | s = replace(s, '\"' => "") 15 | return split(s, ", ") 16 | end 17 | 18 | # list of words that can potentially be solutions 19 | SOLUTION_WORDS = parse_word_list("solutions_nyt.txt") 20 | 21 | # list of words that are valid guesses, but will never be solutions 22 | NONSOLUTION_WORDS = parse_word_list("nonsolutions_nyt.txt") 23 | 24 | # all possible valid guesses 25 | ALL_WORDS = [SOLUTION_WORDS; NONSOLUTION_WORDS] 26 | 27 | SOLUTION_WORD_IDXS = 1:length(SOLUTION_WORDS) 28 | ALL_WORD_IDXS = 1:length(ALL_WORDS) 29 | 30 | 31 | function get_word_score(guess::String, solution::String)::UInt8 32 | counts = countmap(solution) 33 | s2 = 0 34 | for i = 1:5 35 | s2 *= 3 36 | if guess[i] == solution[i] 37 | s2 += 2 38 | counts[guess[i]] -= 1 39 | end 40 | end 41 | s1 = 0 42 | for i = 1:5 43 | s1 *= 3 44 | if guess[i] != solution[i] && guess[i] in solution && counts[guess[i]] > 0 45 | s1 += 1 46 | counts[guess[i]] -= 1 47 | end 48 | end 49 | return s1 + s2 50 | end 51 | 52 | WORD_SCORES = Array{UInt8}(undef, length(ALL_WORDS), length(ALL_WORDS)) 53 | 54 | 55 | """ 56 | Precompute and cache results of get_word_score in WORD_SCORES to 57 | 1) Avoid duplicate work 58 | 2) Eliminate function call overhead 59 | """ 60 | function cache_word_scores(guess_pool::AbstractVector{String}, solution_pool::AbstractVector{String}) 61 | @time begin 62 | for (i1, w1) in enumerate(guess_pool) 63 | for (i2, w2) in enumerate(solution_pool) 64 | WORD_SCORES[i1, i2] = get_word_score(w1, w2) 65 | end 66 | end 67 | end 68 | end 69 | 70 | 71 | # Maximum number of different equivalence classes that a single guess can split the solution 72 | # space into 73 | MAX_NUM_SHARDS = 3^5 - 5 74 | 75 | function optimize_max_num_shards(; verbose::Bool = true) 76 | global MAX_NUM_SHARDS = maximum(map(guess -> length(get_groups(guess, SOLUTION_WORD_IDXS)), ALL_WORD_IDXS)) 77 | if verbose 78 | println("Updated MAX_NUM_SHARDS to $(MAX_NUM_SHARDS).") 79 | end 80 | end 81 | 82 | 83 | """Return entropy of distribution of `group_sizes`` (we want this to be large!) 84 | """ 85 | function get_entropy(group_sizes::Vector{Int})::Float64 86 | pmf = group_sizes 87 | return sum(-p * log(p) for p in pmf) 88 | end 89 | 90 | """Given a `guess`, partitions words in `solution_pool` into groups resulting in the same score. 91 | """ 92 | function get_groups( 93 | guess::T, 94 | solution_pool::AbstractVector{T} 95 | )::Dict{UInt8,Vector{T}} where {T<:Union{Int,String}} 96 | out = Dict{UInt8,Vector{T}}() 97 | for w in solution_pool 98 | s = (T <: Int) ? WORD_SCORES[guess, w] : get_word_score(guess, w) 99 | push!(get!(out, s, String[]), w) 100 | end 101 | return out 102 | end 103 | 104 | """ 105 | Given a `guess`, partitions words in `solution_pool` into groups resulting in the same score, 106 | returning the _size_ of each group. 107 | """ 108 | function get_group_sizes( 109 | guess::T, 110 | solution_pool::AbstractVector{T} 111 | )::Vector{Int} where {T<:Union{Int,String}} 112 | out = Dict{UInt8,Int}() 113 | for w in solution_pool 114 | s = (T <: Int) ? WORD_SCORES[guess, w] : get_word_score(guess, w) 115 | out[s] = get(out, s, 0) + 1 116 | end 117 | return collect(values(out)) 118 | end 119 | 120 | 121 | Base.Enums.@enum Heuristic begin 122 | PRIORITIZE_ENTROPY = 1 123 | PRIORITIZE_MAX_GROUP_SIZE = 2 124 | PRIORITIZE_SPLITS = 3 125 | end 126 | 127 | 128 | function find_move_idx( 129 | guess_pool::AbstractVector{T}, 130 | solution_pool::AbstractVector{T}, 131 | group_sizes::Vector{Vector{Int}}; 132 | heuristic::Heuristic = PRIORITIZE_ENTROPY 133 | )::Int where {T<:Union{Int,String}} 134 | maximum_group_size::Vector{Int} = map(maximum, group_sizes) 135 | entropy::Vector{Float64} = map(get_entropy, group_sizes) 136 | is_potential_solution::Vector{Bool} = map(guess -> guess in solution_pool, guess_pool) 137 | 138 | if heuristic == PRIORITIZE_ENTROPY 139 | # first maximize entropy 140 | # if there are ties, we prefer words in the solution pool 141 | # if there are still ties, we minimize the maximum group size 142 | solution_score = zip(entropy, is_potential_solution, -maximum_group_size) 143 | elseif heuristic == PRIORITIZE_MAX_GROUP_SIZE 144 | # first minimize the maximum group size 145 | # if there are ties, we prefer words in the solution pool 146 | # if there are still ties, we maximize the entropy 147 | solution_score = zip(-maximum_group_size, is_potential_solution, entropy) 148 | elseif heuristic == PRIORITIZE_SPLITS 149 | num_splits::Vector{Int} = map(length, group_sizes) 150 | solution_score = zip(num_splits, entropy, is_potential_solution, -maximum_group_size) 151 | else 152 | throw(ArgumentError("Unexpected heuristic.")) 153 | end 154 | 155 | return argmax(collect(solution_score)) 156 | end 157 | 158 | 159 | function find_move( 160 | guess_pool::AbstractVector{T}, 161 | solution_pool::AbstractVector{T}; 162 | heuristic::Heuristic = PRIORITIZE_ENTROPY 163 | )::T where {T<:Union{Int,String}} 164 | group_sizes::Vector{Vector{Int}} = map(guess -> get_group_sizes(guess, solution_pool), guess_pool) 165 | idx = find_move_idx(guess_pool, solution_pool, group_sizes, heuristic = heuristic) 166 | return guess_pool[idx] 167 | end 168 | 169 | 170 | function find_move_pool( 171 | guess_pool::AbstractVector{T}, 172 | solution_pool::AbstractVector{T}; 173 | heuristic::Heuristic = PRIORITIZE_ENTROPY, 174 | verbose::Bool = false 175 | )::Tuple{T,Dict{UInt8,Vector{T}}} where {T<:Union{Int,String}} 176 | groups::Vector{Dict{UInt8,Vector{T}}} = map(w -> get_groups(w, solution_pool), guess_pool) 177 | group_sizes::Vector{Vector{Int}} = map(e -> map(length, values(e)), groups) 178 | idx = find_move_idx(guess_pool, solution_pool, group_sizes, heuristic = heuristic) 179 | best_word::T = guess_pool[idx] 180 | if verbose 181 | println("Guess: $(get_word(best_word))") 182 | end 183 | remaining_groups::Dict{UInt8,Vector{T}} = groups[idx] 184 | 185 | return best_word, remaining_groups 186 | end 187 | 188 | 189 | """ 190 | Filter the words `w` in `pool` to those where the score when guessing `guess` if the solution is 191 | `w` is `score`. 192 | """ 193 | function trim_pool( 194 | guess::T, 195 | score::UInt8, 196 | pool::AbstractVector{T} 197 | )::Vector{T} where {T<:Union{Int,String}} 198 | newpool = [w for w in pool if (T <: Int ? WORD_SCORES[guess, w] : get_word_score(guess, w)) == score] 199 | @assert !isempty(newpool) "there are no solutions!" 200 | return newpool 201 | end 202 | 203 | 204 | function trim_pool( 205 | guess::String, 206 | score::String, 207 | pool::AbstractVector{String} 208 | )::Vector{String} 209 | if length(score) != 5 210 | println("Your response should be of length 5.") 211 | return pool 212 | end 213 | try 214 | return trim_pool(guess, parse(UInt8, score; base = 3), pool) 215 | catch e 216 | if isa(e, ArgumentError) 217 | println("Unexpected response; skipping ...") 218 | return pool 219 | else 220 | rethrow(e) 221 | end 222 | end 223 | end 224 | 225 | """ 226 | Returns number of moves required to identify `solution`, given `initial_guess`. 227 | 228 | - `guess_pool` are the words that we can guess. 229 | - `solution_pool` are the words that are potential solutions. 230 | """ 231 | function apply_strategy( 232 | solution::T, 233 | initial_guess::T, 234 | guess_pool::AbstractVector{T}, 235 | solution_pool::AbstractVector{T}; 236 | heuristic::Heuristic = PRIORITIZE_ENTROPY, 237 | hard_mode::Bool = false 238 | )::Int where {T<:Union{Int,String}} 239 | @assert solution in guess_pool 240 | @assert solution in solution_pool 241 | @assert initial_guess in guess_pool 242 | 243 | guess = initial_guess 244 | 245 | for j = 1:10 246 | score = T <: Int ? WORD_SCORES[guess, solution] : get_word_score(guess, solution) 247 | if score == 3^5 - 1 248 | return j 249 | end 250 | solution_pool = trim_pool(guess, score, solution_pool) 251 | if hard_mode 252 | guess_pool = trim_pool(guess, score, guess_pool) 253 | end 254 | guess = find_move(guess_pool, solution_pool, heuristic = heuristic) 255 | end 256 | @assert false "error: took more than 10 moves to find the solution" 257 | end 258 | 259 | 260 | function get_word(w::String)::String 261 | w 262 | end 263 | 264 | 265 | function get_word(w_idx::Int)::String 266 | ALL_WORDS[w_idx] 267 | end 268 | 269 | 270 | function get_num_turns( 271 | guess_pool::AbstractVector{T}, 272 | solution_pool::AbstractVector{T}; 273 | heuristic::Heuristic = PRIORITIZE_ENTROPY, 274 | hard_mode::Bool = false, 275 | verbose::Bool = false, 276 | starting_word::Union{T,Nothing} = nothing 277 | )::Vector{Int} where {T<:Union{Int,String}} 278 | if length(solution_pool) == 1 279 | return [1] 280 | end 281 | num_turns = [] 282 | 283 | if isnothing(starting_word) 284 | best_guess, remaining_groups = find_move_pool( 285 | guess_pool, 286 | solution_pool, 287 | heuristic = heuristic, 288 | verbose = verbose 289 | ) 290 | else 291 | best_guess = starting_word 292 | remaining_groups = get_groups(starting_word, solution_pool) 293 | end 294 | for (score, group) in remaining_groups 295 | if score == 3^5 - 1 296 | push!(num_turns, 1) 297 | else 298 | append!(num_turns, 1 .+ get_num_turns( 299 | hard_mode ? trim_pool(best_guess, score, guess_pool) : guess_pool, 300 | group, 301 | heuristic = heuristic, 302 | hard_mode = hard_mode 303 | )) 304 | end 305 | end 306 | return num_turns 307 | end 308 | 309 | """ 310 | Given a pool of allowed guesses and a pool of potential solutions that we need to distinguish 311 | between, returns a strategy to distinguish between each of the solutions, optimizing first for the 312 | worst-case number of turns, then for the average number of turns among strategies tied in the 313 | worst case. 314 | 315 | This strategy is found via an exhaustive search of all possible strategies. 316 | 317 | Params 318 | ------ 319 | guess_pool: Pool of allowed guesses 320 | solution_pool: Pool of potential solutions. Expected to be a subset of `guess_pool`. 321 | hard_mode: If true, guesses must be consistent with all known information; that is, the pool of 322 | allowed guesses for subsequent turns will be pruned to words that are still possible given the 323 | score for the most recent guess. 324 | turns_budget: Budget of turns for the strategy. 325 | show_progress: If true, shows a progress bar during the search process. 326 | 327 | Returns 328 | ------- 329 | 1) `nothing`, if no strategy is possible within the limited number of turns 330 | 2) A tuple with 3 entries: 331 | - The number of turns required by the strategy (in no particular order) 332 | - The optimal first word to guess. 333 | - A strategy dictionary mapping each score we could observe to a tuple of: 334 | - The next word to guess, given that score. 335 | - A strategy dictionary for that guess. 336 | """ 337 | function get_optimal_strategy_exhaustive( 338 | guess_pool::AbstractVector{T}, 339 | solution_pool::AbstractVector{T}; 340 | hard_mode::Bool = false, 341 | turns_budget::Int = typemax(Int), 342 | show_progress::Bool = false 343 | )::Union{Nothing,Tuple{Vector{Int},T,Dict{UInt8,Tuple{T,Dict}}}} where {T<:Union{Int,String}} 344 | # returns number of turns, next guess, and a dictionary specifying what to do for subsequent turns. 345 | @assert turns_budget >= 1 346 | if length(solution_pool) == 1 347 | # if there is one remaining solution, we guess that word, and get a solution in one turn. 348 | return [1], solution_pool[1], Dict() 349 | end 350 | best_max_num_turns = turns_budget 351 | best_average_num_turns = turns_budget 352 | best_guess = nothing 353 | best_num_turns = nothing 354 | best_strategy = nothing 355 | # num_skipped corresponds to the number of guesses that don't satisfy `turns_budget`. 356 | num_skipped = 0 357 | if show_progress 358 | valid_guesses = Tuple{String,Float64,Float64}[] 359 | end 360 | 361 | if show_progress 362 | pmeter = Progress(length(guess_pool)) 363 | end 364 | 365 | function update_progress( 366 | best_guess::Union{Nothing,T}, 367 | best_max_num_turns::Number, 368 | best_average_num_turns::Number, 369 | num_skipped::Int, 370 | ) 371 | ProgressMeter.next!(pmeter; showvalues = [ 372 | (:best_guess, best_guess === nothing ? "N/A" : get_word(best_guess)), 373 | (:best_max_num_turns, best_guess === nothing ? "N/A" : best_max_num_turns), 374 | (:best_average_num_turns, best_guess === nothing ? "N/A" : best_average_num_turns), 375 | (:num_skipped, num_skipped), 376 | (:valid_guesses, valid_guesses) 377 | ]) 378 | end 379 | 380 | for guess in guess_pool 381 | r = get_optimal_strategy_exhaustive_helper( 382 | guess_pool, 383 | solution_pool, 384 | guess, 385 | hard_mode = hard_mode, 386 | turns_budget = best_max_num_turns 387 | ) 388 | if r === nothing 389 | num_skipped += 1 390 | if show_progress 391 | update_progress(best_guess, best_max_num_turns, best_average_num_turns, num_skipped) 392 | end 393 | continue 394 | end 395 | num_turns, strategy = r 396 | max_num_turns = maximum(num_turns) 397 | average_num_turns = Statistics.mean(num_turns) 398 | 399 | if show_progress 400 | push!(valid_guesses, (get_word(guess), max_num_turns, average_num_turns)) 401 | end 402 | 403 | if max_num_turns < best_max_num_turns || average_num_turns < best_average_num_turns 404 | best_max_num_turns = max_num_turns 405 | best_average_num_turns = average_num_turns 406 | best_num_turns = num_turns 407 | best_guess = guess 408 | best_strategy = strategy 409 | end 410 | 411 | if show_progress 412 | update_progress(best_guess, best_max_num_turns, best_average_num_turns, num_skipped) 413 | end 414 | end 415 | if best_guess === nothing 416 | return nothing 417 | end 418 | return best_num_turns, best_guess, best_strategy 419 | end 420 | 421 | """ 422 | Given a pool of allowed guesses, a pool of potential solutions that we need to distinguish 423 | between, AND a fixed initial guess, return the optimal strategy to distinguish between the 424 | solutions. 425 | 426 | See `get_optimal_strategy_exhaustive` for more on the parameters and interpreting the return value. 427 | 428 | NOTE: `turns_budget` includes one turn for the `initial_guess` we are about to make. 429 | """ 430 | function get_optimal_strategy_exhaustive_helper( 431 | guess_pool::AbstractVector{T}, 432 | solution_pool::AbstractVector{T}, 433 | initial_guess::T; 434 | hard_mode::Bool = false, 435 | turns_budget = typemax(Int) 436 | )::Union{Nothing,Tuple{Vector{Int},Dict{UInt8,Tuple{T,Dict}}}} where {T<:Union{Int,String}} 437 | # 1. FAIL: Can't solve if we're down to 0 turns. 438 | if turns_budget == 0 439 | return nothing 440 | end 441 | if turns_budget == 1 442 | if solution_pool == [initial_guess] 443 | return ([1], Dict()) 444 | end 445 | # 2. FAIL: Can't solve if the word guessed is not the only word in the pool. 446 | return nothing 447 | end 448 | 449 | sharded_solution_pool = get_groups(initial_guess, solution_pool) 450 | # 3. FAIL. Don't use a guess that doesn't give any new information. 451 | # (An example where we end up in this state is that we guess the same word twice). 452 | if length(sharded_solution_pool) == 1 453 | return nothing 454 | end 455 | 456 | # 4. FAIL. Largest shard has too many solution candidates left for our `turns_budget` 457 | largest_shard_size = maximum(map(length, values(sharded_solution_pool))) 458 | if turns_budget == 2 && largest_shard_size > 1 459 | # no way to solve in two turns using `initial_guess` if you have multiple solutions to 460 | # consider after this guess 461 | return nothing 462 | end 463 | if turns_budget == 3 && largest_shard_size > MAX_NUM_SHARDS 464 | # no way to solve in three turns using `initial_guess` if we have more than MAX_NUM_SHARDS 465 | # solutions to consider after this guess 466 | return nothing 467 | end 468 | if hard_mode 469 | sharded_guess_pool = get_groups(initial_guess, guess_pool) 470 | end 471 | best_num_turns = [] 472 | best_strategy::Dict{UInt8,Tuple{T,Dict}} = Dict() 473 | for (score, solution_subpool) in sort( 474 | collect(sharded_solution_pool), 475 | by = x -> length(x[2]), 476 | rev = !hard_mode 477 | ) 478 | # in hard mode, we start with the smallest subpools, since exploring those takes less time 479 | # when _not_ in hard mode, we try solving for the largest subpools first; 480 | # we are most likely to fail there and be able to return early. 481 | if solution_subpool == [initial_guess] 482 | push!(best_num_turns, 1) 483 | else 484 | r = get_optimal_strategy_exhaustive( 485 | hard_mode ? sharded_guess_pool[score] : guess_pool, 486 | solution_subpool, 487 | hard_mode = hard_mode, 488 | turns_budget = turns_budget - 1 489 | ) 490 | # 5. FAIL. Some subpool is not solvable in the remaining budget. 491 | if r === nothing 492 | return nothing 493 | end 494 | shard_num_turns, shard_next_guess, shard_strat = r 495 | append!(best_num_turns, 1 .+ shard_num_turns) 496 | best_strategy[score] = shard_next_guess, shard_strat 497 | end 498 | end 499 | return best_num_turns, best_strategy 500 | end 501 | 502 | """Display a strategy returned by `get_optimal_strategy_exhaustive.` 503 | """ 504 | function get_strategy_text( 505 | guesses::AbstractVector{T}, 506 | strategy::Dict{UInt8,Tuple{T,Dict}}; 507 | print_prefix::Bool = false 508 | )::String where {T<:Union{Int,String}} 509 | output = "" 510 | 511 | # 0. Prefix 512 | if print_prefix 513 | output *= """ 514 | This is a guide to solving [Wordle](https://www.powerlanguage.co.uk/wordle/) in hard mode, brought 515 | to you by [Vincent Tjeng](https://vtjeng.com). It covers all 2315 possible solutions to the game. 516 | 517 | Wordle provides feedback on how close your guess is to the solution by coloring each of the five 518 | tiles green, yellow or grey. In the guide, each guess is accompanied by a table that maps the 519 | feedback to the most recent guess to the best next word to guess. To look up your next guess, 520 | you'll need to convert the colored tiles into a 5-digit number, with 🟩=2, 🟨=1, ⬜=0. 521 | Look under the 'hint' column for this number and use the corresponding word as your next guess. 522 | 523 | Here's a worked example from Jan 17, 2022. 524 | 525 | - SCAMP is our first guess. For this guess, we receive the hint 🟩⬜⬜⬜⬜, corresponding to 20000. 526 | - In the table under the section tiled 'scamp', we see that when the hint is 20000, the 527 | next guess recommended is STERN. 528 | - STERN is our second guess. For this guess, we receive the hint 🟩⬜🟨🟩⬜, corresponding to 20120. 529 | - In the table under the section titled 'scamp, stern', we see that when the hint is 530 | 20120, the next guess recommended is SHIRE. 531 | - SHIRE is our third guess. We got lucky; that's the word! Looking under the section titled 532 | 'scamp, stern, shire', we see that two other words were possible based on the feedback to our 533 | first two guesses: SWORE and SHORE. 534 | 535 | """ 536 | end 537 | # 1. Header 538 | output *= "#"^length(guesses) * " " * join(map(get_word, guesses), ", ") * "\n\n" 539 | 540 | # 2a. Table of best responses 541 | output *= "| Hint | Next Guess |\n" 542 | output *= "| ----- | ---------- |\n" 543 | sorted_strategy = sort(collect(strategy)) 544 | for (score, (shard_guess, _)) in sorted_strategy 545 | parsed_score = lpad(string(score, base = 3), 5, "0") 546 | output *= "| $(parsed_score) | $(get_word(shard_guess)) |\n" 547 | end 548 | output *= "| 22222 | (n/a) |\n" 549 | output *= "\n" 550 | 551 | # 2b. Strategy for subsequent guesses. 552 | for (_, (shard_guess, shard_strat)) in sorted_strategy 553 | output *= get_strategy_text([guesses; shard_guess], shard_strat) 554 | end 555 | return output 556 | end 557 | 558 | 559 | function plot_num_turns( 560 | turns::AbstractVector{Int}; 561 | title_text::String, 562 | saved_filename::Union{Nothing,String} = nothing, 563 | max_val::Int = 0 564 | ) 565 | n = ( max_val==0 ? maximum(turns) : max_val ) 566 | msol = hist(turns, bins = 1:n+1, density = true, align = "left", zorder = 3) 567 | xlabel("number of guesses required") 568 | ylabel("frequency") 569 | title(title_text) 570 | for i = 1:n 571 | text(i, 0.15, length(findall(turns.==i)), horizontalalignment = "center") 572 | end 573 | grid(zorder = 0) 574 | 575 | if saved_filename !== nothing 576 | if !isdir("figures") 577 | mkdir("figures") 578 | end 579 | savefig("figures/" * saved_filename) 580 | end 581 | end 582 | -------------------------------------------------------------------------------- /.ipynb_checkpoints/utils-checkpoint.jl: -------------------------------------------------------------------------------- 1 | # For plotting operations 2 | using PyPlot 3 | # For @showprogress 4 | using ProgressMeter 5 | # for countmap 6 | using StatsBase 7 | # for Statistics.mean 8 | using Statistics 9 | 10 | function parse_word_list(filename::String)::Vector{String} 11 | s = open(filename) do file 12 | read(file, String) 13 | end 14 | s = replace(s, '\"' => "") 15 | return split(s, ", ") 16 | end 17 | 18 | # list of words that can potentially be solutions 19 | SOLUTION_WORDS = parse_word_list("solutions_nyt.txt") 20 | 21 | # list of words that are valid guesses, but will never be solutions 22 | NONSOLUTION_WORDS = parse_word_list("nonsolutions_nyt.txt") 23 | 24 | # all possible valid guesses 25 | ALL_WORDS = [SOLUTION_WORDS; NONSOLUTION_WORDS] 26 | 27 | SOLUTION_WORD_IDXS = 1:length(SOLUTION_WORDS) 28 | ALL_WORD_IDXS = 1:length(ALL_WORDS) 29 | 30 | 31 | function get_word_score(guess::String, solution::String)::UInt8 32 | counts = countmap(solution) 33 | s2 = 0 34 | for i = 1:5 35 | s2 *= 3 36 | if guess[i] == solution[i] 37 | s2 += 2 38 | counts[guess[i]] -= 1 39 | end 40 | end 41 | s1 = 0 42 | for i = 1:5 43 | s1 *= 3 44 | if guess[i] != solution[i] && guess[i] in solution && counts[guess[i]] > 0 45 | s1 += 1 46 | counts[guess[i]] -= 1 47 | end 48 | end 49 | return s1 + s2 50 | end 51 | 52 | WORD_SCORES = Array{UInt8}(undef, length(ALL_WORDS), length(ALL_WORDS)) 53 | 54 | 55 | """ 56 | Precompute and cache results of get_word_score in WORD_SCORES to 57 | 1) Avoid duplicate work 58 | 2) Eliminate function call overhead 59 | """ 60 | function cache_word_scores(guess_pool::AbstractVector{String}, solution_pool::AbstractVector{String}) 61 | @time begin 62 | for (i1, w1) in enumerate(guess_pool) 63 | for (i2, w2) in enumerate(solution_pool) 64 | WORD_SCORES[i1, i2] = get_word_score(w1, w2) 65 | end 66 | end 67 | end 68 | end 69 | 70 | 71 | # Maximum number of different equivalence classes that a single guess can split the solution 72 | # space into 73 | MAX_NUM_SHARDS = 3^5 - 5 74 | 75 | function optimize_max_num_shards(; verbose::Bool = true) 76 | global MAX_NUM_SHARDS = maximum(map(guess -> length(get_groups(guess, SOLUTION_WORD_IDXS)), ALL_WORD_IDXS)) 77 | if verbose 78 | println("Updated MAX_NUM_SHARDS to $(MAX_NUM_SHARDS).") 79 | end 80 | end 81 | 82 | 83 | """Return entropy of distribution of `group_sizes`` (we want this to be large!) 84 | """ 85 | function get_entropy(group_sizes::Vector{Int})::Float64 86 | pmf = group_sizes 87 | return sum(-p * log(p) for p in pmf) 88 | end 89 | 90 | """Given a `guess`, partitions words in `solution_pool` into groups resulting in the same score. 91 | """ 92 | function get_groups( 93 | guess::T, 94 | solution_pool::AbstractVector{T} 95 | )::Dict{UInt8,Vector{T}} where {T<:Union{Int,String}} 96 | out = Dict{UInt8,Vector{T}}() 97 | for w in solution_pool 98 | s = (T <: Int) ? WORD_SCORES[guess, w] : get_word_score(guess, w) 99 | push!(get!(out, s, String[]), w) 100 | end 101 | return out 102 | end 103 | 104 | """ 105 | Given a `guess`, partitions words in `solution_pool` into groups resulting in the same score, 106 | returning the _size_ of each group. 107 | """ 108 | function get_group_sizes( 109 | guess::T, 110 | solution_pool::AbstractVector{T} 111 | )::Vector{Int} where {T<:Union{Int,String}} 112 | out = Dict{UInt8,Int}() 113 | for w in solution_pool 114 | s = (T <: Int) ? WORD_SCORES[guess, w] : get_word_score(guess, w) 115 | out[s] = get(out, s, 0) + 1 116 | end 117 | return collect(values(out)) 118 | end 119 | 120 | 121 | Base.Enums.@enum Heuristic begin 122 | PRIORITIZE_ENTROPY = 1 123 | PRIORITIZE_MAX_GROUP_SIZE = 2 124 | PRIORITIZE_SPLITS = 3 125 | end 126 | 127 | 128 | function find_move_idx( 129 | guess_pool::AbstractVector{T}, 130 | solution_pool::AbstractVector{T}, 131 | group_sizes::Vector{Vector{Int}}; 132 | heuristic::Heuristic = PRIORITIZE_ENTROPY 133 | )::Int where {T<:Union{Int,String}} 134 | maximum_group_size::Vector{Int} = map(maximum, group_sizes) 135 | entropy::Vector{Float64} = map(get_entropy, group_sizes) 136 | is_potential_solution::Vector{Bool} = map(guess -> guess in solution_pool, guess_pool) 137 | 138 | if heuristic == PRIORITIZE_ENTROPY 139 | # first maximize entropy 140 | # if there are ties, we prefer words in the solution pool 141 | # if there are still ties, we minimize the maximum group size 142 | solution_score = zip(entropy, is_potential_solution, -maximum_group_size) 143 | elseif heuristic == PRIORITIZE_MAX_GROUP_SIZE 144 | # first minimize the maximum group size 145 | # if there are ties, we prefer words in the solution pool 146 | # if there are still ties, we maximize the entropy 147 | solution_score = zip(-maximum_group_size, is_potential_solution, entropy) 148 | elseif heuristic == PRIORITIZE_SPLITS 149 | num_splits::Vector{Int} = map(length, group_sizes) 150 | solution_score = zip(num_splits, entropy, is_potential_solution, -maximum_group_size) 151 | else 152 | throw(ArgumentError("Unexpected heuristic.")) 153 | end 154 | 155 | return argmax(collect(solution_score)) 156 | end 157 | 158 | 159 | function find_move( 160 | guess_pool::AbstractVector{T}, 161 | solution_pool::AbstractVector{T}; 162 | heuristic::Heuristic = PRIORITIZE_ENTROPY 163 | )::T where {T<:Union{Int,String}} 164 | group_sizes::Vector{Vector{Int}} = map(guess -> get_group_sizes(guess, solution_pool), guess_pool) 165 | idx = find_move_idx(guess_pool, solution_pool, group_sizes, heuristic = heuristic) 166 | return guess_pool[idx] 167 | end 168 | 169 | 170 | function find_move_pool( 171 | guess_pool::AbstractVector{T}, 172 | solution_pool::AbstractVector{T}; 173 | heuristic::Heuristic = PRIORITIZE_ENTROPY, 174 | verbose::Bool = false 175 | )::Tuple{T,Dict{UInt8,Vector{T}}} where {T<:Union{Int,String}} 176 | groups::Vector{Dict{UInt8,Vector{T}}} = map(w -> get_groups(w, solution_pool), guess_pool) 177 | group_sizes::Vector{Vector{Int}} = map(e -> map(length, values(e)), groups) 178 | idx = find_move_idx(guess_pool, solution_pool, group_sizes, heuristic = heuristic) 179 | best_word::T = guess_pool[idx] 180 | if verbose 181 | println("Guess: $(get_word(best_word))") 182 | end 183 | remaining_groups::Dict{UInt8,Vector{T}} = groups[idx] 184 | 185 | return best_word, remaining_groups 186 | end 187 | 188 | 189 | """ 190 | Filter the words `w` in `pool` to those where the score when guessing `guess` if the solution is 191 | `w` is `score`. 192 | """ 193 | function trim_pool( 194 | guess::T, 195 | score::UInt8, 196 | pool::AbstractVector{T} 197 | )::Vector{T} where {T<:Union{Int,String}} 198 | newpool = [w for w in pool if (T <: Int ? WORD_SCORES[guess, w] : get_word_score(guess, w)) == score] 199 | @assert !isempty(newpool) "there are no solutions!" 200 | return newpool 201 | end 202 | 203 | 204 | function trim_pool( 205 | guess::String, 206 | score::String, 207 | pool::AbstractVector{String} 208 | )::Vector{String} 209 | if length(score) != 5 210 | println("Your response should be of length 5.") 211 | return pool 212 | end 213 | try 214 | return trim_pool(guess, parse(UInt8, score; base = 3), pool) 215 | catch e 216 | if isa(e, ArgumentError) 217 | println("Unexpected response; skipping ...") 218 | return pool 219 | else 220 | rethrow(e) 221 | end 222 | end 223 | end 224 | 225 | """ 226 | Returns number of moves required to identify `solution`, given `initial_guess`. 227 | 228 | - `guess_pool` are the words that we can guess. 229 | - `solution_pool` are the words that are potential solutions. 230 | """ 231 | function apply_strategy( 232 | solution::T, 233 | initial_guess::T, 234 | guess_pool::AbstractVector{T}, 235 | solution_pool::AbstractVector{T}; 236 | heuristic::Heuristic = PRIORITIZE_ENTROPY, 237 | hard_mode::Bool = false 238 | )::Int where {T<:Union{Int,String}} 239 | @assert solution in guess_pool 240 | @assert solution in solution_pool 241 | @assert initial_guess in guess_pool 242 | 243 | guess = initial_guess 244 | 245 | for j = 1:10 246 | score = T <: Int ? WORD_SCORES[guess, solution] : get_word_score(guess, solution) 247 | if score == 3^5 - 1 248 | return j 249 | end 250 | solution_pool = trim_pool(guess, score, solution_pool) 251 | if hard_mode 252 | guess_pool = trim_pool(guess, score, guess_pool) 253 | end 254 | guess = find_move(guess_pool, solution_pool, heuristic = heuristic) 255 | end 256 | @assert false "error: took more than 10 moves to find the solution" 257 | end 258 | 259 | 260 | function get_word(w::String)::String 261 | w 262 | end 263 | 264 | 265 | function get_word(w_idx::Int)::String 266 | ALL_WORDS[w_idx] 267 | end 268 | 269 | 270 | function get_num_turns( 271 | guess_pool::AbstractVector{T}, 272 | solution_pool::AbstractVector{T}; 273 | heuristic::Heuristic = PRIORITIZE_ENTROPY, 274 | hard_mode::Bool = false, 275 | verbose::Bool = false, 276 | starting_word::Union{T,Nothing} = nothing 277 | )::Vector{Int} where {T<:Union{Int,String}} 278 | if length(solution_pool) == 1 279 | return [1] 280 | end 281 | num_turns = [] 282 | 283 | if isnothing(starting_word) 284 | best_guess, remaining_groups = find_move_pool( 285 | guess_pool, 286 | solution_pool, 287 | heuristic = heuristic, 288 | verbose = verbose 289 | ) 290 | else 291 | best_guess = starting_word 292 | remaining_groups = get_groups(starting_word, solution_pool) 293 | end 294 | for (score, group) in remaining_groups 295 | if score == 3^5 - 1 296 | push!(num_turns, 1) 297 | else 298 | append!(num_turns, 1 .+ get_num_turns( 299 | hard_mode ? trim_pool(best_guess, score, guess_pool) : guess_pool, 300 | group, 301 | heuristic = heuristic, 302 | hard_mode = hard_mode 303 | )) 304 | end 305 | end 306 | return num_turns 307 | end 308 | 309 | """ 310 | Given a pool of allowed guesses and a pool of potential solutions that we need to distinguish 311 | between, returns a strategy to distinguish between each of the solutions, optimizing first for the 312 | worst-case number of turns, then for the average number of turns among strategies tied in the 313 | worst case. 314 | 315 | This strategy is found via an exhaustive search of all possible strategies. 316 | 317 | Params 318 | ------ 319 | guess_pool: Pool of allowed guesses 320 | solution_pool: Pool of potential solutions. Expected to be a subset of `guess_pool`. 321 | hard_mode: If true, guesses must be consistent with all known information; that is, the pool of 322 | allowed guesses for subsequent turns will be pruned to words that are still possible given the 323 | score for the most recent guess. 324 | turns_budget: Budget of turns for the strategy. 325 | show_progress: If true, shows a progress bar during the search process. 326 | 327 | Returns 328 | ------- 329 | 1) `nothing`, if no strategy is possible within the limited number of turns 330 | 2) A tuple with 3 entries: 331 | - The number of turns required by the strategy (in no particular order) 332 | - The optimal first word to guess. 333 | - A strategy dictionary mapping each score we could observe to a tuple of: 334 | - The next word to guess, given that score. 335 | - A strategy dictionary for that guess. 336 | """ 337 | function get_optimal_strategy_exhaustive( 338 | guess_pool::AbstractVector{T}, 339 | solution_pool::AbstractVector{T}; 340 | hard_mode::Bool = false, 341 | turns_budget::Int = typemax(Int), 342 | show_progress::Bool = false 343 | )::Union{Nothing,Tuple{Vector{Int},T,Dict{UInt8,Tuple{T,Dict}}}} where {T<:Union{Int,String}} 344 | # returns number of turns, next guess, and a dictionary specifying what to do for subsequent turns. 345 | @assert turns_budget >= 1 346 | if length(solution_pool) == 1 347 | # if there is one remaining solution, we guess that word, and get a solution in one turn. 348 | return [1], solution_pool[1], Dict() 349 | end 350 | best_max_num_turns = turns_budget 351 | best_average_num_turns = turns_budget 352 | best_guess = nothing 353 | best_num_turns = nothing 354 | best_strategy = nothing 355 | # num_skipped corresponds to the number of guesses that don't satisfy `turns_budget`. 356 | num_skipped = 0 357 | if show_progress 358 | valid_guesses = Tuple{String,Float64,Float64}[] 359 | end 360 | 361 | if show_progress 362 | pmeter = Progress(length(guess_pool)) 363 | end 364 | 365 | function update_progress( 366 | best_guess::Union{Nothing,T}, 367 | best_max_num_turns::Number, 368 | best_average_num_turns::Number, 369 | num_skipped::Int, 370 | ) 371 | ProgressMeter.next!(pmeter; showvalues = [ 372 | (:best_guess, best_guess === nothing ? "N/A" : get_word(best_guess)), 373 | (:best_max_num_turns, best_guess === nothing ? "N/A" : best_max_num_turns), 374 | (:best_average_num_turns, best_guess === nothing ? "N/A" : best_average_num_turns), 375 | (:num_skipped, num_skipped), 376 | (:valid_guesses, valid_guesses) 377 | ]) 378 | end 379 | 380 | for guess in guess_pool 381 | r = get_optimal_strategy_exhaustive_helper( 382 | guess_pool, 383 | solution_pool, 384 | guess, 385 | hard_mode = hard_mode, 386 | turns_budget = best_max_num_turns 387 | ) 388 | if r === nothing 389 | num_skipped += 1 390 | if show_progress 391 | update_progress(best_guess, best_max_num_turns, best_average_num_turns, num_skipped) 392 | end 393 | continue 394 | end 395 | num_turns, strategy = r 396 | max_num_turns = maximum(num_turns) 397 | average_num_turns = Statistics.mean(num_turns) 398 | 399 | if show_progress 400 | push!(valid_guesses, (get_word(guess), max_num_turns, average_num_turns)) 401 | end 402 | 403 | if max_num_turns < best_max_num_turns || average_num_turns < best_average_num_turns 404 | best_max_num_turns = max_num_turns 405 | best_average_num_turns = average_num_turns 406 | best_num_turns = num_turns 407 | best_guess = guess 408 | best_strategy = strategy 409 | end 410 | 411 | if show_progress 412 | update_progress(best_guess, best_max_num_turns, best_average_num_turns, num_skipped) 413 | end 414 | end 415 | if best_guess === nothing 416 | return nothing 417 | end 418 | return best_num_turns, best_guess, best_strategy 419 | end 420 | 421 | """ 422 | Given a pool of allowed guesses, a pool of potential solutions that we need to distinguish 423 | between, AND a fixed initial guess, return the optimal strategy to distinguish between the 424 | solutions. 425 | 426 | See `get_optimal_strategy_exhaustive` for more on the parameters and interpreting the return value. 427 | 428 | NOTE: `turns_budget` includes one turn for the `initial_guess` we are about to make. 429 | """ 430 | function get_optimal_strategy_exhaustive_helper( 431 | guess_pool::AbstractVector{T}, 432 | solution_pool::AbstractVector{T}, 433 | initial_guess::T; 434 | hard_mode::Bool = false, 435 | turns_budget = typemax(Int) 436 | )::Union{Nothing,Tuple{Vector{Int},Dict{UInt8,Tuple{T,Dict}}}} where {T<:Union{Int,String}} 437 | # 1. FAIL: Can't solve if we're down to 0 turns. 438 | if turns_budget == 0 439 | return nothing 440 | end 441 | if turns_budget == 1 442 | if solution_pool == [initial_guess] 443 | return ([1], Dict()) 444 | end 445 | # 2. FAIL: Can't solve if the word guessed is not the only word in the pool. 446 | return nothing 447 | end 448 | 449 | sharded_solution_pool = get_groups(initial_guess, solution_pool) 450 | # 3. FAIL. Don't use a guess that doesn't give any new information. 451 | # (An example where we end up in this state is that we guess the same word twice). 452 | if length(sharded_solution_pool) == 1 453 | return nothing 454 | end 455 | 456 | # 4. FAIL. Largest shard has too many solution candidates left for our `turns_budget` 457 | largest_shard_size = maximum(map(length, values(sharded_solution_pool))) 458 | if turns_budget == 2 && largest_shard_size > 1 459 | # no way to solve in two turns using `initial_guess` if you have multiple solutions to 460 | # consider after this guess 461 | return nothing 462 | end 463 | if turns_budget == 3 && largest_shard_size > MAX_NUM_SHARDS 464 | # no way to solve in three turns using `initial_guess` if we have more than MAX_NUM_SHARDS 465 | # solutions to consider after this guess 466 | return nothing 467 | end 468 | if hard_mode 469 | sharded_guess_pool = get_groups(initial_guess, guess_pool) 470 | end 471 | best_num_turns = [] 472 | best_strategy::Dict{UInt8,Tuple{T,Dict}} = Dict() 473 | for (score, solution_subpool) in sort( 474 | collect(sharded_solution_pool), 475 | by = x -> length(x[2]), 476 | rev = !hard_mode 477 | ) 478 | # in hard mode, we start with the smallest subpools, since exploring those takes less time 479 | # when _not_ in hard mode, we try solving for the largest subpools first; 480 | # we are most likely to fail there and be able to return early. 481 | if solution_subpool == [initial_guess] 482 | push!(best_num_turns, 1) 483 | else 484 | r = get_optimal_strategy_exhaustive( 485 | hard_mode ? sharded_guess_pool[score] : guess_pool, 486 | solution_subpool, 487 | hard_mode = hard_mode, 488 | turns_budget = turns_budget - 1 489 | ) 490 | # 5. FAIL. Some subpool is not solvable in the remaining budget. 491 | if r === nothing 492 | return nothing 493 | end 494 | shard_num_turns, shard_next_guess, shard_strat = r 495 | append!(best_num_turns, 1 .+ shard_num_turns) 496 | best_strategy[score] = shard_next_guess, shard_strat 497 | end 498 | end 499 | return best_num_turns, best_strategy 500 | end 501 | 502 | """Display a strategy returned by `get_optimal_strategy_exhaustive.` 503 | """ 504 | function get_strategy_text( 505 | guesses::AbstractVector{T}, 506 | strategy::Dict{UInt8,Tuple{T,Dict}}; 507 | print_prefix::Bool = false 508 | )::String where {T<:Union{Int,String}} 509 | output = "" 510 | 511 | # 0. Prefix 512 | if print_prefix 513 | output *= """ 514 | This is a guide to solving [Wordle](https://www.powerlanguage.co.uk/wordle/) in hard mode, brought 515 | to you by [Vincent Tjeng](https://vtjeng.com). It covers all 2315 possible solutions to the game. 516 | 517 | Wordle provides feedback on how close your guess is to the solution by coloring each of the five 518 | tiles green, yellow or grey. In the guide, each guess is accompanied by a table that maps the 519 | feedback to the most recent guess to the best next word to guess. To look up your next guess, 520 | you'll need to convert the colored tiles into a 5-digit number, with 🟩=2, 🟨=1, ⬜=0. 521 | Look under the 'hint' column for this number and use the corresponding word as your next guess. 522 | 523 | Here's a worked example from Jan 17, 2022. 524 | 525 | - SCAMP is our first guess. For this guess, we receive the hint 🟩⬜⬜⬜⬜, corresponding to 20000. 526 | - In the table under the section tiled 'scamp', we see that when the hint is 20000, the 527 | next guess recommended is STERN. 528 | - STERN is our second guess. For this guess, we receive the hint 🟩⬜🟨🟩⬜, corresponding to 20120. 529 | - In the table under the section titled 'scamp, stern', we see that when the hint is 530 | 20120, the next guess recommended is SHIRE. 531 | - SHIRE is our third guess. We got lucky; that's the word! Looking under the section titled 532 | 'scamp, stern, shire', we see that two other words were possible based on the feedback to our 533 | first two guesses: SWORE and SHORE. 534 | 535 | """ 536 | end 537 | # 1. Header 538 | output *= "#"^length(guesses) * " " * join(map(get_word, guesses), ", ") * "\n\n" 539 | 540 | # 2a. Table of best responses 541 | output *= "| Hint | Next Guess |\n" 542 | output *= "| ----- | ---------- |\n" 543 | sorted_strategy = sort(collect(strategy)) 544 | for (score, (shard_guess, _)) in sorted_strategy 545 | parsed_score = lpad(string(score, base = 3), 5, "0") 546 | output *= "| $(parsed_score) | $(get_word(shard_guess)) |\n" 547 | end 548 | output *= "| 22222 | (n/a) |\n" 549 | output *= "\n" 550 | 551 | # 2b. Strategy for subsequent guesses. 552 | for (_, (shard_guess, shard_strat)) in sorted_strategy 553 | output *= get_strategy_text([guesses; shard_guess], shard_strat) 554 | end 555 | return output 556 | end 557 | 558 | 559 | function plot_num_turns( 560 | turns::AbstractVector{Int}; 561 | title_text::String, 562 | saved_filename::Union{Nothing,String} = nothing, 563 | max_val::Int = 0 564 | ) 565 | n = ( max_val==0 ? maximum(turns) : max_val ) 566 | msol = hist(turns, bins = 1:n+1, density = true, align = "left", zorder = 3) 567 | xlabel("number of guesses required") 568 | ylabel("frequency") 569 | title(title_text) 570 | for i = 1:n 571 | text(i, 0.15, length(findall(turns.==i)), horizontalalignment = "center") 572 | end 573 | grid(zorder = 0) 574 | 575 | if saved_filename !== nothing 576 | if !isdir("figures") 577 | mkdir("figures") 578 | end 579 | savefig("figures/" * saved_filename) 580 | end 581 | end 582 | -------------------------------------------------------------------------------- /hard_mode_exhaustive_search.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "4c42d7a4-569f-46ad-8009-9ddbf62c8f1a", 6 | "metadata": {}, 7 | "source": [ 8 | "# Hard Mode\n", 9 | "\n", 10 | "Analysis by [Vincent Tjeng](https://vtjeng.com)\n", 11 | "\n", 12 | "[Wordle](https://www.powerlanguage.co.uk/wordle/) has a game mode known as \"hard mode\", in which any guess must be consistent with the hints provided from previous guesses. While it can take more turns for _humans_ to solve Wordle in hard mode, it's actually more straightforward for a _computer_ to generate a solution for Wordle (and prove that it is optimal) via brute force in hard mode, since the pool of guesses allowed diminishes rapidly after a few turns" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": 36, 18 | "id": "e4060d2f-045d-462d-a4ad-b412b5f2bbbb", 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [ 22 | "include(\"utils.jl\");\n", 23 | "ProgressMeter.ijulia_behavior(:clear);" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": 2, 29 | "id": "0e042948", 30 | "metadata": {}, 31 | "outputs": [ 32 | { 33 | "name": "stdout", 34 | "output_type": "stream", 35 | "text": [ 36 | " 2.415100 seconds (29.63 M allocations: 2.507 GiB, 9.86% gc time, 0.13% compilation time)\n", 37 | "Updated MAX_NUM_SHARDS to 150.\n" 38 | ] 39 | } 40 | ], 41 | "source": [ 42 | "cache_word_scores(SOLUTION_WORDS, SOLUTION_WORDS)\n", 43 | "optimize_max_num_shards()" 44 | ] 45 | }, 46 | { 47 | "cell_type": "markdown", 48 | "id": "33ffa5ab", 49 | "metadata": {}, 50 | "source": [ 51 | "# Algorithm\n", 52 | "\n", 53 | "The `get_optimal_strategy_exhaustive` function searches for the optimal strategy for Wordle in hard mode given a limited `turns_budget`. It either returns FAIL (if no strategy satisfying the `turns_budget` exists) or returns a strategy with the best worst-case number of turns (ties are broken by the average number of turns).\n", 54 | "\n", 55 | "## Concepts\n", 56 | "- We think of each guess as \"splitting\" the solution pool into one or more \"shards\". (A \"shard\" contains all of the words in the solution pool that would have returned the same response to the guess; the shards cover the solution pool but don't overlap). \n", 57 | "- For a particular solution pool, a strategy satisfying `turns_budget` exists if and only if there exists an initial guess that splits the solution pool into shards, where _each_ shard has a strategy that succeeds in at most `turns_budget - 1` turns.\n", 58 | "\n", 59 | "## Optimizations\n", 60 | "A naive algorithm tries every possible combination of guesses. We implement the following optimizations:\n", 61 | "\n", 62 | "- We reduce `turns_budget` to the worst-case number of turns seen so far.\n", 63 | "- For a given first guess, we try to find a successful strategy for the shards in increasing order of size. Exploring smaller shards is quicker, and it's possible to fail relatively early on; if we do fail, we can short-circuit the computation and return FAIL for that first guess (and continue trying other guesses).\n", 64 | "- When left with a budget of 1 turn and a solution pool that contains 2 or more words, we return FAIL.\n", 65 | "- When left with a budget of 2 turns and a solution pool that contains 151 or more words, we return FAIL. (TRACE, which does best, splits the original solution pool into 150 buckets). \n", 66 | "\n", 67 | "### Considered but Unimplemented\n", 68 | "- We are currently trying the words in the order they were found in the word list. Sorting them by a heuristic might allow us to reduce `turns_budget` more quickly and stop trying guesses in less promising situations" 69 | ] 70 | }, 71 | { 72 | "cell_type": "markdown", 73 | "id": "eee58443", 74 | "metadata": {}, 75 | "source": [ 76 | "# Hard mode, only guessing words that can be solutions, `turns_budget` = 4\n", 77 | "\n", 78 | "Given a budget of 4 turns, no strategy exists when you constrain yourself to guess only potential solutions." 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": 3, 84 | "id": "ef01b9b3", 85 | "metadata": {}, 86 | "outputs": [ 87 | { 88 | "name": "stderr", 89 | "output_type": "stream", 90 | "text": [ 91 | "\u001b[32mProgress: 100%|█████████████████████████████████████████| Time: 0:00:04\u001b[39m\n", 92 | "\u001b[34m best_guess: N/A\u001b[39m\n", 93 | "\u001b[34m best_max_num_turns: N/A\u001b[39m\n", 94 | "\u001b[34m best_average_num_turns: N/A\u001b[39m\n", 95 | "\u001b[34m num_skipped: 2309\u001b[39m\n", 96 | "\u001b[34m valid_guesses: Tuple{String, Float64, Float64}[]\u001b[39m\n" 97 | ] 98 | } 99 | ], 100 | "source": [ 101 | "r = get_optimal_strategy_exhaustive(\n", 102 | " SOLUTION_WORD_IDXS, \n", 103 | " SOLUTION_WORD_IDXS, \n", 104 | " hard_mode=true, \n", 105 | " turns_budget=4,\n", 106 | " show_progress=true\n", 107 | ")" 108 | ] 109 | }, 110 | { 111 | "cell_type": "markdown", 112 | "id": "df7a80b0", 113 | "metadata": {}, 114 | "source": [ 115 | "# Hard mode, only guessing words that can be solutions, `turns_budget` = 5\n", 116 | "\n", 117 | "Given a budget of 5 turns, two initial guesses (\"scowl\" and \"stamp\") succeed when you constrain yourself to guess only potential solutions. Excitingly, the brute-force search takes only 3 minutes!" 118 | ] 119 | }, 120 | { 121 | "cell_type": "code", 122 | "execution_count": 4, 123 | "id": "fe27bee5", 124 | "metadata": {}, 125 | "outputs": [ 126 | { 127 | "name": "stderr", 128 | "output_type": "stream", 129 | "text": [ 130 | "\u001b[32mProgress: 100%|█████████████████████████████████████████| Time: 0:02:25\u001b[39m\n", 131 | "\u001b[34m best_guess: scamp\u001b[39m\n", 132 | "\u001b[34m best_max_num_turns: 5\u001b[39m\n", 133 | "\u001b[34m best_average_num_turns: 3.715461238631442\u001b[39m\n", 134 | "\u001b[34m num_skipped: 2307\u001b[39m\n", 135 | "\u001b[34m valid_guesses: [(\"scowl\", 5.0, 3.7522737115634475), (\"scamp\", 5.0, 3.715461238631442)]\u001b[39m\n" 136 | ] 137 | } 138 | ], 139 | "source": [ 140 | "r = get_optimal_strategy_exhaustive(\n", 141 | " SOLUTION_WORD_IDXS, \n", 142 | " SOLUTION_WORD_IDXS, \n", 143 | " hard_mode=true, \n", 144 | " turns_budget=5,\n", 145 | " show_progress=true\n", 146 | ")\n", 147 | "num_turns, guess, strat = r;" 148 | ] 149 | }, 150 | { 151 | "cell_type": "code", 152 | "execution_count": 38, 153 | "id": "1646263d", 154 | "metadata": {}, 155 | "outputs": [ 156 | { 157 | "data": { 158 | "image/png": "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gKWlZZlqlDKVSoWYmBj4+/tDoVDouxzJqq7jPP5ItL5LICpXgYGB+i5BJ9X1vaMiFJ15KQudw82KFStgbm6Ot99+W6N948aNyM7OLnOosbOzg1wu1zpKk5aWpnU052nu7u4AgObNm+PWrVsICwsrMdwYGxvD2NhYq12hULz0O8qTOB6Vg+NMpF/V9fXH9w7dnjudLyieO3cu7OzstNodHBzw+eefl3k5RkZG8PLy0jrUFhMTg3bt2pV5OUIIjWtqiIiI6OWm85Gbq1evqo+cPMnNza3EW7hLEhoaiqFDh8Lb2xs+Pj5YunQpkpOTMXr0aACPTynduHEDq1evBgB89913qF27tvpi5d9//x3z58/HuHHjdN0MIiIikiidw42DgwNOnjyJOnXqaLSfOHECtra2Oi1rwIABSE9Px+zZs5GSkoJmzZohKioKbm5uAICUlBSNwFRYWIipU6ciKSkJhoaGqFevHubOnYv3339f180gIiIiidI53AwcOBAffPABLCws8NprrwEADh48iPHjx2PgwIE6FxASEoKQkJBiH1u5cqXG9Lhx43iUhoiIiEqlc7iZM2cOrl69Cj8/PxgaPp69sLAQw4YN0+maGyIiIqKKoHO4MTIywoYNG/DZZ5/hxIkTUCqVaN68ufpUEhEREZE+Pfc3dDVs2BANGzYsz1qIiIiIXpjO4aagoAArV67E3r17kZaWhsLCQo3H9+3bV27FEREREelK53Azfvx4rFy5Ej179kSzZs2e+W3CRERERJVJ53Czfv16/Pzzz9XuK6yJiIjo5aDzNxQbGRmhfv36FVELERER0QvTOdx8+OGHWLBgAYQQFVEPERER0QvR+bTU77//jv3792Pnzp1o2rSp1j+y2rx5c7kVR0RERKQrncONtbU1+vbtWxG1EBEREb0wncPNihUrKqIOIiIionKh8zU3AJCfn489e/ZgyZIlyMzMBADcvHkTWVlZ5VocERERka50PnJz9epVdO/eHcnJycjNzYW/vz8sLCzw5ZdfIicnB99//31F1ElERERUJjofuRk/fjy8vb1x7949KJVKdXvfvn2xd+/eci2OiIiISFfPdbfUH3/8ASMjI412Nzc33Lhxo9wKIyIiInoeOh+5KSwsREFBgVb79evXYWFhUS5FERERET0vncONv78/IiMj1dMymQxZWVmYOXMm/yUDERER6Z3Op6W+/vprdO7cGU2aNEFOTg4GDx6MCxcuwM7ODj/99FNF1EhERERUZjqHGxcXFyQmJuKnn37CX3/9hcLCQgQHB2PIkCEaFxgTERER6YPO4QYAlEolRo4ciZEjR5Z3PUREREQvROdws3r16lIfHzZs2HMXQ0RERPSidA4348eP15hWqVTIzs6GkZERTE1NGW6IiIhIr3S+W+revXsaP1lZWTh//jw6dOjAC4qJiIhI757rf0s9rUGDBpg7d67WUR0iIiKiylYu4QYA5HI5bt68WV6LIyIiInouOl9zs23bNo1pIQRSUlLw7bffon379uVWGBEREdHz0Dnc9OnTR2NaJpPB3t4eXbp0wVdffVVedRERERE9F53DTWFhYUXUQURERFQuyu2aGyIiIqKqQOcjN6GhoWXuGxERoeviiYiIiF6IzuEmISEBf/31F/Lz89GoUSMAwD///AO5XI5XXnlF3U8mk5VflURERERlpHO46dWrFywsLLBq1SrUqFEDwOMv9hsxYgR8fX3x4YcflnuRRERERGWl8zU3X331FcLDw9XBBgBq1KiBOXPm8G4pIiIi0judw01GRgZu3bql1Z6WlobMzMxyKYqIiIjoeekcbvr27YsRI0bgl19+wfXr13H9+nX88ssvCA4OxptvvlkRNRIRERGVmc7X3Hz//ff46KOP8M4770ClUj1eiKEhgoODMW/evHIvkIiIiEgXOocbU1NTLFq0CPPmzcOlS5cghED9+vVhZmZWEfURERER6eS5v8QvJSUFKSkpaNiwIczMzCCEKM+6iIiIiJ6LzuEmPT0dfn5+aNiwIQIDA5GSkgIAGDVqFG8DJyIiIr3TOdxMnDgRCoUCycnJMDU1VbcPGDAAu3btKtfiiIiIiHSl8zU30dHR2L17N2rVqqXR3qBBA1y9erXcCiMiIiJ6HjofuXn48KHGEZsid+7cgbGxcbkURURERPS8dA43r732GlavXq2elslkKCwsxLx589C5c+dyLY6IiIhIVzqflpo3bx46deqEuLg45OXlYdKkSfj7779x9+5d/PHHHxVRIxEREVGZ6XzkpkmTJjh58iRat24Nf39/PHz4EG+++SYSEhJQr169iqiRiIiIqMx0OnKjUqkQEBCAJUuWYNasWRVVExEREdFz0+nIjUKhwOnTpyGTySqqHiIiIqIXovNpqWHDhmHZ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IQgJCSn2sZUrV2q1CSEquCIiIiKqzvT+7xeIiIiIyhPDDREREUkKww0RERFJCsMNERERSQrDDREREUkKww0RERFJCsMNERERSQrDDREREUkKww0RERFJCsMNERERSQrDDREREUkKww0RERFJCsMNERERSQrDDREREUkKww0RERFJCsMNERERSQrDDREREUkKww0RERFJCsMNERERSQrDDREREUkKww0RERFJCsMNERERSQrDDREREUkKww0RERFJCsMNERERSQrDDREREUkKww0RERFJCsMNERERSQrDDREREUkKww0RERFJCsMNERERSQrDDREREUkKww0RERFJCsMNERERSQrDDREREUkKww0RERFJiqG+CyB6HnWm7NB3Cc/BEOOPROu7CCIiyeORGyIiIpIUhhsiIiKSFIYbIiIikhSGGyIiIpIUhhsiIiKSFIYbIiIikhSGGyIiIpIUhhsiIiKSFIYbIiIikhSGGyIiIpIUhhsiIiKSFIYbIiIikhSGGyIiIpIUhhsiIiKSFIYbIiIikhSGGyIiIpIUhhsiIiKSFIYbIiIikhSGGyIiIpIUhhsiIiKSFIYbIiIikhSGGyIiIpIUvYebRYsWwd3dHSYmJvDy8sLhw4dL7X/w4EF4eXnBxMQEdevWxffff19JlRIREVF1oNdws2HDBkyYMAHTpk1DQkICfH190aNHDyQnJxfbPykpCYGBgfD19UVCQgI++eQTfPDBB9i0aVMlV05ERERVlV7DTUREBIKDgzFq1Ch4eHggMjISrq6uWLx4cbH9v//+e9SuXRuRkZHw8PDAqFGjMHLkSMyfP7+SKyciIqKqSm/hJi8vD/Hx8QgICNBoDwgIQGxsbLHzHDlyRKt/t27dEBcXB5VKVWG1EhERUfVhqK8V37lzBwUFBXB0dNRod3R0RGpqarHzpKamFts/Pz8fd+7cgbOzs9Y8ubm5yM3NVU8/ePAAAHD37t0KCURe846W+zIrniHGH4nWdxFERCQR6enp5b7MzMxMAIAQ4pl99RZuishkMo1pIYRW27P6F9deJDw8HLNmzdJqd3d317VUIiIiKgO7Lypu2ZmZmbCysiq1j97CjZ2dHeRyudZRmrS0NK2jM0WcnJyK7W9oaAhbW9ti55k6dSpCQ0PV04WFhbh79y5sbW1LDVEvi4yMDLi6uuLatWuwtLTUdzmS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", 159 | "text/plain": [ 160 | "Figure(PyObject
)" 161 | ] 162 | }, 163 | "metadata": {}, 164 | "output_type": "display_data" 165 | } 166 | ], 167 | "source": [ 168 | "plot_num_turns(\n", 169 | " num_turns, \n", 170 | " title_text=\"5-turn solution for hard mode, starting with SCAMP\",\n", 171 | " saved_filename=\"strat_using_solutions_only_hard_mode_optimal.png\"\n", 172 | ")" 173 | ] 174 | }, 175 | { 176 | "cell_type": "code", 177 | "execution_count": 7, 178 | "id": "79b0fc5c", 179 | "metadata": {}, 180 | "outputs": [], 181 | "source": [ 182 | "open(\"hard_mode_strategy.md\", \"w\") do io\n", 183 | " println(io, get_strategy_text([guess], strat, print_prefix = true))\n", 184 | "end" 185 | ] 186 | }, 187 | { 188 | "cell_type": "markdown", 189 | "id": "7473eabf", 190 | "metadata": {}, 191 | "source": [ 192 | "# Hard mode, guess any 5-letter word, `turns_budget` = 4\n", 193 | "\n", 194 | "We revisit the situation where our budget is 4 turns, this time relaxing the constraint that we guess only potential solutions. It takes a bit more time, but we can show that no strategy guarantees a solution in 4 steps - even when we allow guessing all ~13k valid 5-letter words (rather than just the ~2.3k potential solutions)!" 195 | ] 196 | }, 197 | { 198 | "cell_type": "code", 199 | "execution_count": 7, 200 | "id": "7a42c810", 201 | "metadata": {}, 202 | "outputs": [ 203 | { 204 | "name": "stdout", 205 | "output_type": "stream", 206 | "text": [ 207 | " 86.447014 seconds (996.38 M allocations: 80.041 GiB, 18.71% gc time)\n" 208 | ] 209 | } 210 | ], 211 | "source": [ 212 | "cache_word_scores(ALL_WORDS, ALL_WORDS)" 213 | ] 214 | }, 215 | { 216 | "cell_type": "code", 217 | "execution_count": 8, 218 | "id": "28a289f9", 219 | "metadata": {}, 220 | "outputs": [ 221 | { 222 | "name": "stderr", 223 | "output_type": "stream", 224 | "text": [ 225 | "\u001b[32mProgress: 100%|█████████████████████████████████████████| Time: 2:39:58\u001b[39m\n", 226 | "\u001b[34m best_guess: N/A\u001b[39m\n", 227 | "\u001b[34m best_max_num_turns: N/A\u001b[39m\n", 228 | "\u001b[34m best_average_num_turns: N/A\u001b[39m\n", 229 | "\u001b[34m num_skipped: 12972\u001b[39m\n", 230 | "\u001b[34m valid_guesses: Tuple{String, Float64, Float64}[]\u001b[39m\n" 231 | ] 232 | } 233 | ], 234 | "source": [ 235 | "r = get_optimal_strategy_exhaustive(\n", 236 | " ALL_WORD_IDXS, \n", 237 | " SOLUTION_WORD_IDXS, \n", 238 | " hard_mode=true, \n", 239 | " turns_budget=4,\n", 240 | " show_progress=true\n", 241 | ")" 242 | ] 243 | }, 244 | { 245 | "cell_type": "code", 246 | "execution_count": null, 247 | "id": "503363db", 248 | "metadata": {}, 249 | "outputs": [], 250 | "source": [] 251 | }, 252 | { 253 | "cell_type": "markdown", 254 | "id": "73e6f8a5-6da0-4bf4-b37d-0d8e8c348778", 255 | "metadata": {}, 256 | "source": [ 257 | "# Easy mode, guess any 5-letter word, `turns_budget` = 4" 258 | ] 259 | }, 260 | { 261 | "cell_type": "code", 262 | "execution_count": 1, 263 | "id": "7704ec2b-5501-434a-a3dd-659987cc8d1b", 264 | "metadata": {}, 265 | "outputs": [], 266 | "source": [ 267 | "include(\"utils.jl\");\n", 268 | "ProgressMeter.ijulia_behavior(:clear);" 269 | ] 270 | }, 271 | { 272 | "cell_type": "code", 273 | "execution_count": 2, 274 | "id": "c5f50e23-7b3c-4595-bf42-59833061d1d4", 275 | "metadata": {}, 276 | "outputs": [ 277 | { 278 | "name": "stdout", 279 | "output_type": "stream", 280 | "text": [ 281 | " 12.488186 seconds (172.37 M allocations: 14.203 GiB, 8.42% gc time, 0.03% compilation time)\n", 282 | "Updated MAX_NUM_SHARDS to 150.\n" 283 | ] 284 | } 285 | ], 286 | "source": [ 287 | "cache_word_scores(ALL_WORDS, SOLUTION_WORDS)\n", 288 | "optimize_max_num_shards()" 289 | ] 290 | }, 291 | { 292 | "cell_type": "code", 293 | "execution_count": 3, 294 | "id": "caa91989-5112-4173-bbd7-afe3b5414f5d", 295 | "metadata": {}, 296 | "outputs": [ 297 | { 298 | "name": "stderr", 299 | "output_type": "stream", 300 | "text": [ 301 | "\u001b[32mProgress: 100%|█████████████████████████████████████████| Time: 0:26:29\u001b[39m\n", 302 | "\u001b[34m best_guess: N/A\u001b[39m\n", 303 | "\u001b[34m best_max_num_turns: N/A\u001b[39m\n", 304 | "\u001b[34m best_average_num_turns: N/A\u001b[39m\n", 305 | "\u001b[34m num_skipped: 12972\u001b[39m\n", 306 | "\u001b[34m valid_guesses: Tuple{String, Float64, Float64}[]\u001b[39m\n" 307 | ] 308 | }, 309 | { 310 | "name": "stdout", 311 | "output_type": "stream", 312 | "text": [ 313 | "cigar: no solution\n" 314 | ] 315 | } 316 | ], 317 | "source": [ 318 | "initial_guess = 1\n", 319 | "\n", 320 | "r = get_optimal_strategy_exhaustive_helper(\n", 321 | " ALL_WORD_IDXS,\n", 322 | " SOLUTION_WORD_IDXS,\n", 323 | " initial_guess,\n", 324 | " hard_mode = false,\n", 325 | " turns_budget = 4\n", 326 | ")\n", 327 | "if isnothing(r)\n", 328 | " println(ALL_WORDS[initial_guess], \": no solution\")\n", 329 | "else\n", 330 | " best_num_turns, best_strat = r\n", 331 | " print_strategy(initial_guess, best_strat)\n", 332 | "end" 333 | ] 334 | }, 335 | { 336 | "cell_type": "code", 337 | "execution_count": null, 338 | "id": "24971763-27f7-41f1-b7eb-99aebc4b3fa8", 339 | "metadata": {}, 340 | "outputs": [], 341 | "source": [] 342 | } 343 | ], 344 | "metadata": { 345 | "kernelspec": { 346 | "display_name": "Julia 1.7.1", 347 | "language": "julia", 348 | "name": "julia-1.7" 349 | }, 350 | "language_info": { 351 | "file_extension": ".jl", 352 | "mimetype": "application/julia", 353 | "name": "julia", 354 | "version": "1.7.1" 355 | } 356 | }, 357 | "nbformat": 4, 358 | "nbformat_minor": 5 359 | } 360 | -------------------------------------------------------------------------------- /.ipynb_checkpoints/hard_mode_exhaustive_search-checkpoint.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "4c42d7a4-569f-46ad-8009-9ddbf62c8f1a", 6 | "metadata": {}, 7 | "source": [ 8 | "# Hard Mode\n", 9 | "\n", 10 | "Analysis by [Vincent Tjeng](https://vtjeng.com)\n", 11 | "\n", 12 | "[Wordle](https://www.powerlanguage.co.uk/wordle/) has a game mode known as \"hard mode\", in which any guess must be consistent with the hints provided from previous guesses. While it can take more turns for _humans_ to solve Wordle in hard mode, it's actually more straightforward for a _computer_ to generate a solution for Wordle (and prove that it is optimal) via brute force in hard mode, since the pool of guesses allowed diminishes rapidly after a few turns" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": 36, 18 | "id": "e4060d2f-045d-462d-a4ad-b412b5f2bbbb", 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [ 22 | "include(\"utils.jl\");\n", 23 | "ProgressMeter.ijulia_behavior(:clear);" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": 2, 29 | "id": "0e042948", 30 | "metadata": {}, 31 | "outputs": [ 32 | { 33 | "name": "stdout", 34 | "output_type": "stream", 35 | "text": [ 36 | " 2.415100 seconds (29.63 M allocations: 2.507 GiB, 9.86% gc time, 0.13% compilation time)\n", 37 | "Updated MAX_NUM_SHARDS to 150.\n" 38 | ] 39 | } 40 | ], 41 | "source": [ 42 | "cache_word_scores(SOLUTION_WORDS, SOLUTION_WORDS)\n", 43 | "optimize_max_num_shards()" 44 | ] 45 | }, 46 | { 47 | "cell_type": "markdown", 48 | "id": "33ffa5ab", 49 | "metadata": {}, 50 | "source": [ 51 | "# Algorithm\n", 52 | "\n", 53 | "The `get_optimal_strategy_exhaustive` function searches for the optimal strategy for Wordle in hard mode given a limited `turns_budget`. It either returns FAIL (if no strategy satisfying the `turns_budget` exists) or returns a strategy with the best worst-case number of turns (ties are broken by the average number of turns).\n", 54 | "\n", 55 | "## Concepts\n", 56 | "- We think of each guess as \"splitting\" the solution pool into one or more \"shards\". (A \"shard\" contains all of the words in the solution pool that would have returned the same response to the guess; the shards cover the solution pool but don't overlap). \n", 57 | "- For a particular solution pool, a strategy satisfying `turns_budget` exists if and only if there exists an initial guess that splits the solution pool into shards, where _each_ shard has a strategy that succeeds in at most `turns_budget - 1` turns.\n", 58 | "\n", 59 | "## Optimizations\n", 60 | "A naive algorithm tries every possible combination of guesses. We implement the following optimizations:\n", 61 | "\n", 62 | "- We reduce `turns_budget` to the worst-case number of turns seen so far.\n", 63 | "- For a given first guess, we try to find a successful strategy for the shards in increasing order of size. Exploring smaller shards is quicker, and it's possible to fail relatively early on; if we do fail, we can short-circuit the computation and return FAIL for that first guess (and continue trying other guesses).\n", 64 | "- When left with a budget of 1 turn and a solution pool that contains 2 or more words, we return FAIL.\n", 65 | "- When left with a budget of 2 turns and a solution pool that contains 151 or more words, we return FAIL. (TRACE, which does best, splits the original solution pool into 150 buckets). \n", 66 | "\n", 67 | "### Considered but Unimplemented\n", 68 | "- We are currently trying the words in the order they were found in the word list. Sorting them by a heuristic might allow us to reduce `turns_budget` more quickly and stop trying guesses in less promising situations" 69 | ] 70 | }, 71 | { 72 | "cell_type": "markdown", 73 | "id": "eee58443", 74 | "metadata": {}, 75 | "source": [ 76 | "# Hard mode, only guessing words that can be solutions, `turns_budget` = 4\n", 77 | "\n", 78 | "Given a budget of 4 turns, no strategy exists when you constrain yourself to guess only potential solutions." 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": 3, 84 | "id": "ef01b9b3", 85 | "metadata": {}, 86 | "outputs": [ 87 | { 88 | "name": "stderr", 89 | "output_type": "stream", 90 | "text": [ 91 | "\u001b[32mProgress: 100%|█████████████████████████████████████████| Time: 0:00:04\u001b[39m\n", 92 | "\u001b[34m best_guess: N/A\u001b[39m\n", 93 | "\u001b[34m best_max_num_turns: N/A\u001b[39m\n", 94 | "\u001b[34m best_average_num_turns: N/A\u001b[39m\n", 95 | "\u001b[34m num_skipped: 2309\u001b[39m\n", 96 | "\u001b[34m valid_guesses: Tuple{String, Float64, Float64}[]\u001b[39m\n" 97 | ] 98 | } 99 | ], 100 | "source": [ 101 | "r = get_optimal_strategy_exhaustive(\n", 102 | " SOLUTION_WORD_IDXS, \n", 103 | " SOLUTION_WORD_IDXS, \n", 104 | " hard_mode=true, \n", 105 | " turns_budget=4,\n", 106 | " show_progress=true\n", 107 | ")" 108 | ] 109 | }, 110 | { 111 | "cell_type": "markdown", 112 | "id": "df7a80b0", 113 | "metadata": {}, 114 | "source": [ 115 | "# Hard mode, only guessing words that can be solutions, `turns_budget` = 5\n", 116 | "\n", 117 | "Given a budget of 5 turns, two initial guesses (\"scowl\" and \"stamp\") succeed when you constrain yourself to guess only potential solutions. Excitingly, the brute-force search takes only 3 minutes!" 118 | ] 119 | }, 120 | { 121 | "cell_type": "code", 122 | "execution_count": 4, 123 | "id": "fe27bee5", 124 | "metadata": {}, 125 | "outputs": [ 126 | { 127 | "name": "stderr", 128 | "output_type": "stream", 129 | "text": [ 130 | "\u001b[32mProgress: 100%|█████████████████████████████████████████| Time: 0:02:25\u001b[39m\n", 131 | "\u001b[34m best_guess: scamp\u001b[39m\n", 132 | "\u001b[34m best_max_num_turns: 5\u001b[39m\n", 133 | "\u001b[34m best_average_num_turns: 3.715461238631442\u001b[39m\n", 134 | "\u001b[34m num_skipped: 2307\u001b[39m\n", 135 | "\u001b[34m valid_guesses: [(\"scowl\", 5.0, 3.7522737115634475), (\"scamp\", 5.0, 3.715461238631442)]\u001b[39m\n" 136 | ] 137 | } 138 | ], 139 | "source": [ 140 | "r = get_optimal_strategy_exhaustive(\n", 141 | " SOLUTION_WORD_IDXS, \n", 142 | " SOLUTION_WORD_IDXS, \n", 143 | " hard_mode=true, \n", 144 | " turns_budget=5,\n", 145 | " show_progress=true\n", 146 | ")\n", 147 | "num_turns, guess, strat = r;" 148 | ] 149 | }, 150 | { 151 | "cell_type": "code", 152 | "execution_count": 38, 153 | "id": "1646263d", 154 | "metadata": {}, 155 | "outputs": [ 156 | { 157 | "data": { 158 | "image/png": "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gKWlZZlqlDKVSoWYmBj4+/tDoVDouxzJqq7jPP5ItL5LICpXgYGB+i5BJ9X1vaMiFJ15KQudw82KFStgbm6Ot99+W6N948aNyM7OLnOosbOzg1wu1zpKk5aWpnU052nu7u4AgObNm+PWrVsICwsrMdwYGxvD2NhYq12hULz0O8qTOB6Vg+NMpF/V9fXH9w7dnjudLyieO3cu7OzstNodHBzw+eefl3k5RkZG8PLy0jrUFhMTg3bt2pV5OUIIjWtqiIiI6OWm85Gbq1evqo+cPMnNza3EW7hLEhoaiqFDh8Lb2xs+Pj5YunQpkpOTMXr0aACPTynduHEDq1evBgB89913qF27tvpi5d9//x3z58/HuHHjdN0MIiIikiidw42DgwNOnjyJOnXqaLSfOHECtra2Oi1rwIABSE9Px+zZs5GSkoJmzZohKioKbm5uAICUlBSNwFRYWIipU6ciKSkJhoaGqFevHubOnYv3339f180gIiIiidI53AwcOBAffPABLCws8NprrwEADh48iPHjx2PgwIE6FxASEoKQkJBiH1u5cqXG9Lhx43iUhoiIiEqlc7iZM2cOrl69Cj8/PxgaPp69sLAQw4YN0+maGyIiIqKKoHO4MTIywoYNG/DZZ5/hxIkTUCqVaN68ufpUEhEREZE+Pfc3dDVs2BANGzYsz1qIiIiIXpjO4aagoAArV67E3r17kZaWhsLCQo3H9+3bV27FEREREelK53Azfvx4rFy5Ej179kSzZs2e+W3CRERERJVJ53Czfv16/Pzzz9XuK6yJiIjo5aDzNxQbGRmhfv36FVELERER0QvTOdx8+OGHWLBgAYQQFVEPERER0QvR+bTU77//jv3792Pnzp1o2rSp1j+y2rx5c7kVR0RERKQrncONtbU1+vbtWxG1EBEREb0wncPNihUrKqIOIiIionKh8zU3AJCfn489e/ZgyZIlyMzMBADcvHkTWVlZ5VocERERka50PnJz9epVdO/eHcnJycjNzYW/vz8sLCzw5ZdfIicnB99//31F1ElERERUJjofuRk/fjy8vb1x7949KJVKdXvfvn2xd+/eci2OiIiISFfPdbfUH3/8ASMjI412Nzc33Lhxo9wKIyIiInoeOh+5KSwsREFBgVb79evXYWFhUS5FERERET0vncONv78/IiMj1dMymQxZWVmYOXMm/yUDERER6Z3Op6W+/vprdO7cGU2aNEFOTg4GDx6MCxcuwM7ODj/99FNF1EhERERUZjqHGxcXFyQmJuKnn37CX3/9hcLCQgQHB2PIkCEaFxgTERER6YPO4QYAlEolRo4ciZEjR5Z3PUREREQvROdws3r16lIfHzZs2HMXQ0RERPSidA4348eP15hWqVTIzs6GkZERTE1NGW6IiIhIr3S+W+revXsaP1lZWTh//jw6dOjAC4qJiIhI757rf0s9rUGDBpg7d67WUR0iIiKiylYu4QYA5HI5bt68WV6LIyIiInouOl9zs23bNo1pIQRSUlLw7bffon379uVWGBEREdHz0Dnc9OnTR2NaJpPB3t4eXbp0wVdffVVedRERERE9F53DTWFhYUXUQURERFQuyu2aGyIiIqKqQOcjN6GhoWXuGxERoeviiYiIiF6IzuEmISEBf/31F/Lz89GoUSMAwD///AO5XI5XXnlF3U8mk5VflURERERlpHO46dWrFywsLLBq1SrUqFEDwOMv9hsxYgR8fX3x4YcflnuRRERERGWl8zU3X331FcLDw9XBBgBq1KiBOXPm8G4pIiIi0judw01GRgZu3bql1Z6WlobMzMxyKYqIiIjoeekcbvr27YsRI0bgl19+wfXr13H9+nX88ssvCA4OxptvvlkRNRIRERGVmc7X3Hz//ff46KOP8M4770ClUj1eiKEhgoODMW/evHIvkIiIiEgXOocbU1NTLFq0CPPmzcOlS5cghED9+vVhZmZWEfURERER6eS5v8QvJSUFKSkpaNiwIczMzCCEKM+6iIiIiJ6LzuEmPT0dfn5+aNiwIQIDA5GSkgIAGDVqFG8DJyIiIr3TOdxMnDgRCoUCycnJMDU1VbcPGDAAu3btKtfiiIiIiHSl8zU30dHR2L17N2rVqqXR3qBBA1y9erXcCiMiIiJ6HjofuXn48KHGEZsid+7cgbGxcbkURURERPS8dA43r732GlavXq2elslkKCwsxLx589C5c+dyLY6IiIhIVzqflpo3bx46deqEuLg45OXlYdKkSfj7779x9+5d/PHHHxVRIxEREVGZ6XzkpkmTJjh58iRat24Nf39/PHz4EG+++SYSEhJQr169iqiRiIiIqMx0OnKjUqkQEBCAJUuWYNasWRVVExEREdFz0+nIjUKhwOnTpyGTySqqHiIiIqIXovNpqWHDhmHZsmUVUQsRERHRC9P5guK8vDz88MMPiImJgbe3t9b/lIqIiCi34oiIiIh0VaZwc/LkSTRr1gwGBgY4ffo0XnnlFQDAP//8o9GPp6uIiIhI38oUbjw9PZGSkgIHBwdcvXoVx48fh62tbUXXRkRERKSzMl1zY21tjaSkJADAlStXUFhYWKFFERERET2vMh25eeutt9CxY0c4OztDJpPB29sbcrm82L6XL18u1wKJiIiIdFGmcLN06VK8+eabuHjxIj744AO8++67sLCwqOjaiIiIiHRW5rulunfvDgCIj4/H+PHjGW6IiIioStL5VvAVK1ZURB1ERERE5ULnL/EjIiIiqsoYboiIiEhSGG6IiIhIUvQebhYtWgR3d3eYmJjAy8sLhw8fLrHv5s2b4e/vD3t7e1haWsLHxwe7d++uxGqJiIioqtNruNmwYQMmTJiAadOmISEhAb6+vujRoweSk5OL7X/o0CH4+/sjKioK8fHx6Ny5M3r16oWEhIRKrpyIiIiqKr2Gm4iICAQHB2PUqFHw8PBAZGQkXF1dsXjx4mL7R0ZGYtKkSXj11VfRoEEDfP7552jQoAF+++23Sq6ciIiIqiqdbwUvL3l5eYiPj8eUKVM02gMCAhAbG1umZRQWFiIzMxM2NjYl9snNzUVubq56OiMjAwCgUqmgUqmeo3JpKRoDjkXF4jgTVQ3V7TXI945/6TIGegs3d+7cQUFBARwdHTXaHR0dkZqaWqZlfPXVV3j48CH69+9fYp/w8HDMmjVLqz06Ohqmpqa6FS1hMTEx+i7hpVD9xllvbxFEFSIqKkrfJTyX6vfeUf6ys7PL3Ffv71wymUxjWgih1Vacn376CWFhYdi6dSscHBxK7Dd16lSEhoaqpzMyMuDq6oqAgABYWlo+f+ESoVKpEBMTA39/fygUCn2XI1nVdZzHH4nWdwlE5SowMFDfJeikur53VISiMy9lobdwY2dnB7lcrnWUJi0tTetoztM2bNiA4OBgbNy4EV27di21r7GxMYyNjbXaFQrFS7+jPInjUTk4zkT6VV1ff3zv0O2509sFxUZGRvDy8tI61BYTE4N27dqVON9PP/2E4cOHY926dejZs2dFl0lERETVjF5PS4WGhmLo0KHw9vaGj48Pli5diuTkZIwePRrA41NKN27cwOrVqwE8DjbDhg3DggUL0LZtW/VRH6VSCSsrK71tBxEREVUdeg03AwYMQHp6OmbPno2UlBQ0a9YMUVFRcHNzAwCkpKRofOfNkiVLkJ+fjzFjxmDMmDHq9qCgIKxcubKyyyciIqIqSO8XFIeEhCAkJKTYx54OLAcOHKj4goiIiKha0/u/XyAiIiIqTww3REREJCkMN0RERCQpDDdEREQkKQw3REREJCkMN0RERCQpDDdEREQkKQw3REREJCkMN0RERCQpDDdEREQkKQw3REREJCkMN0RERCQpDDdEREQkKQw3REREJCkMN0RERCQpDDdEREQkKQw3REREJCkMN0RERCQpDDdEREQkKYb6LoD049ChQ5g3bx7i4+ORkpKCjRs3ol+/fvouq9rLzMzEjBkzsGXLFqSlpcHT0xMLFixAq1atkJ+fj6lTp2L37t24fPkyrKys0LVrV8ydOxcuLi76Lr3Kur54JAoy0rTazT17wjbgPwAA1Z1ruHdwBXKSTwMQUNjWhn2fyTC0dNCYRwiBtI1hyEmKh33faTBt6FMZm1Al5Vw7jYxjm5B36xIKsu5qjUf2+VhkJu5E3q1LKHyUAefh38DIsa7GMlLXTUHutdMabaaNfWHfe7J6WnX3Bu7tX47cG2chClQwsq8Da9+hMHFrUbEbWEWFh4dj8+bNOHfuHJRKJdq1a4cvvvgCjRo1Uve5desWJk+ejOjoaNy/fx+NGzdGgwYN0KRJE63lCSEQGBiIXbt2YcuWLejTp08lbk3VxXDzknr48CFatmyJoUOHYsCAAfouRzJGjRqF06dPY82aNXBxccHatWvRtWtXnDhxArm5uUhMTMSMGTPQsmVL3Lt3DxMmTMAbb7yBuLg4fZdeZTkHfQ0UFqqn8+5cRdqG6TBr3B4AoLqXgtQfJ8G8hT+sOwyBzNgMqvRrkMmNtJaVGbcVkFVa6VWayMuBwqEuzJv74/avn2s9XqjKgXGtJjBt3AF3dy0scTnmLbvBusM76mmZQnPc034Jg6JGTTgO/C9khkbIiNuGtE2zUPO9HyA3r1F+G1RNHDx4EGPGjMGrr76K/Px8TJs2DQEBAThz5gzMzMwghECfPn2gUCiwdetWKJVKfPjhh+jRo4e6z5MiIyMhk3GnfhrDzUuqR48e6NGjB1Qqlb5LkYxHjx5h06ZN2Lp1K1577TUAQFhYGH799VcsWbIEbdu2xc6dO6FQKNTzLFy4EK1bt0ZycjJq166tr9KrNLmplcb0o6MbYWjtDGPX5gCA+4dWQ1nPGzU6j1T3UVg7aS0nL+0yMo7/CudhX+P6d0MrtuhqQFnPG8p63iU+bt6sCwAg/8GtUpcjMzQuMaQUZD9A/r0U2PYYDyMHdwBAjY5ByErYgbw7V6F8CcPNrl27NKZXrFgBBwcHxMfH47XXXsOFCxdw9OhRnD59Gk2bNoVKpcL777+Pd999Fz/99BNGjRqlnvfEiROIiIjA8ePH4ezsXNmbUqXxmhuicpKfn4+CggKYmJhotCuVSsTGxhY7z4MHDyCTyWBtbV0JFVZ/okCFh2cOwLyFP2QyGYQoxKPLcTCs4YJbG2bg2sIhSFkdiux/jmjMV6jKwZ1t82DjP/qlPFpQkR6eOYBr3wzGzR9CcG/fMhTmZqsfM1BaQmHrioen96EwLweisACZibtgYGYNY6f6eqy66njw4AEAwMbGBgCQm5sLABrvI3K5HEZGRvj999/VbdnZ2Rg0aBC+/fZbODlph/mXHcMNUTmxsLCAj48PPvvsM9y8eRMFBQVYu3Ytjh07hpSUFK3+OTk5mDJlCgYPHgxLS0s9VFz9ZP9zFIU5WTBr5gcAKHz4ACLvETKO/QJlXS849v8Mpg19cHvL58hJPqWe797eH2Bc0wOmDdrqq3RJMmvSCXZvTILjoM9h1W4AHv4Ti9tb/j3FJZPJ4DDgM+SlXca1r99G8vy+yIz7FY5vz4aBibkeK68ahBAIDQ1Fhw4d0KxZMwBA48aN4ebmhqlTp+LevXvIy8vDpk2bkJqaqvE+MnHiRLRr1w69e/fWV/lVGk9LEZWjNWvWYOTIkahZsybkcjleeeUVDB48GPHx8Rr9VCoVBg4ciMLCQixatEhP1VY/WSejoazrBUMLWwCAEI+vxVHWbwvLV/sAAIwc6yL3xllkJu6ESe3myL5wDDnJJ+A8/Bt9lS1ZFq26q383sq8DQ5uaSF01AbmpF2HsVB9CCNyNXgwDUys4DvkCMkMjZJ2MRtovs+AU9DUMzW30WL3+jR07FidPntQ4IqNQKLBp0yYEBwfDxsYGcrkcLVq0QPfu3dXX1mzbtg379u1DQkKCvkqv8njkhqgc1atXDwcPHkRWVhauXbuGP//8EyqVCu7u7uo+KpUK/fv3R1JSEmJiYnjUpozyH6Qh5+oJmLfspm6Tm1oCBnIo7Fw1+ipsXVGQcRsAkHP1BPLvpeJa5ABc/fINXP3yDQDA7V/DkbpuSuVtwEvAyLEeYGCI/Hs3ATwe+0eXjsP+jckwqdUExk71YRsQApnCCA9P79Vztfo1btw4bNu2Dfv370etWrU0HvPy8kJiYiLu37+P5ORkzJw5E+np6er3kX379uHSpUuwtraGoaEhDA0fH6d466230KlTp8relCqJR26IKoCZmRnMzMxw79497N69G+Hh4QAeB5shQ4bgwoUL2L9/P2xtbfVcafWRdSoGclMrKOu9qm6TyRUwdmqA/Ls3NPqq7t6A/P9vA7dq+zbMWwZoPJ6yfCxqdBkFZf3WFV/4S0R15ypQmA/5/x+REfmPrx/B03fzyAwAISq5uqpBCIFx48Zhy5YtOHDggMYfPk+zsrKCqakpbt68ifj4eMyZMwcAMGXKFI0LiwGgefPm+Prrr9GrV68Krb+6YLh5SWVlZeHixYvqu6WuXLmCxMRE2NjY8K6dF7B7924IIdCoUSNcvHgRH3/8MRo1aoSgoCDs2rULAwYMQGJiIrZv346CggKkpqYCeHwxoZGR9q3L9JgQhcg6tQdmzfwgM5BrPGbZ5k3c3voljGs1hYlbCzy6HI9HF/+E4+DHgVJuXqPYi4gNLe2LvavqZVGY9wj59/69hiP/wS3k3boMA6U5DC0dUPAoEwUZt1GQlQ4AUN29DgCQmz0eT9W9FDw8cwDKut6Qm1pCdScZd/ctg5FjPRjX9AAAGLs0hoGJOdJ3fA2r9gMhMzRG1ondyL9/q9Q7taRszJgxWLduHbZu3QoLCwv1e4CVlRWUSiUAYOPGjbC3t0ft2rWRkJCAmTNn4o033kBAwOOQ7uTkVOxFxLVr1y41LL1MGG5eUnFxcejcubN6+uOPPwYABAUFYeXKlXqqqvp78OABpk6diuvXr8PGxgZvvfUW/vvf/0KhUODOnTvYvn07AKBVq1Ya8+3fv5+Hk0uRcyURBRm3Yd7CX+sx04btYNstBA+ObsS9vUthaFMT9n0/gUmtpnqotPrIS72AWz99op6+t+8HAIBZMz/Y9ZyIRxePIT0qUv34nW1fAgCs2g96/H1CckPkXD2BzLhtKFQ9gqGFPZT1vGHVfrA6gMpNreDw9izcP7Qat36aBlGYD4VdbTi8OR1GDppfCPiyWLx4MQBovd5XrFiB4cOHAwBSUlIQGhqKW7duwdnZGZ06dcKKFSsqudLqTSbEy3VsMCMjA1ZWVnjw4AGvdcDj0yRRUVEIDAzU+P4VKl/VdZzrTNmh7xKIytWVuT31XYJOqut7R0XQ5fObFxQTERGRpDDcEBERkaQw3BAREZGkMNwQERGRpDDcEBERkaQw3BAREZGkMNwQERGRpDDcEBERkaQw3BAREZGkMNwQERGRpDDcEBERkaQw3BAREZGkMNwQERGRpDDcEBERkaQY6rsAIiKiylJnyg59l/AcDDH+SLS+i9DJlbk99bp+HrkhIiIiSWG4ISIiIklhuCEiIiJJYbghIiIiSWG4ISIiIklhuCEiIiJJYbghIiIiSWG4ISIiIklhuCEiIiJJYbghIiIiSWG4ISIiIklhuCEiIiJJYbghIiIiSWG4ISIiIknRe7hZtGgR3N3dYWJiAi8vLxw+fLjEvikpKRg8eDAaNWoEAwMDTJgwofIKJSIiompBr+Fmw4YNmDBhAqZNm4aEhAT4+vqiR48eSE5OLrZ/bm4u7O3tMW3aNLRs2bKSqyUiIqLqQK/hJiIiAsHBwRg1ahQ8PDwQGRkJV1dXLF68uNj+derUwYIFCzBs2DBYWVlVcrVERERUHegt3OTl5SE+Ph4BAQEa7QEBAYiNjdVTVURERFTdGeprxXfu3EFBQQEcHR012h0dHZGamlpu68nNzUVubq56OiMjAwCgUqmgUqnKbT3VVdEYcCwqFseZiF4mFfFep8sy9RZuishkMo1pIYRW24sIDw/HrFmztNqjo6Nhampabuup7mJiYvRdwkuh+o2z3t8iiKgaioqKKvdlZmdnl7mv3t657OzsIJfLtY7SpKWlaR3NeRFTp05FaGioejojIwOurq4ICAiApaVlua2nulKpVIiJiYG/vz8UCoW+y5Gs6jrO449E67sEIqqGAgMDy32ZRWdeykJv4cbIyAheXl6IiYlB37591e0xMTHo3bt3ua3H2NgYxsbGWu0KhaJafchUNI5H5eA4E9HLoCLe53RZpl6POYeGhmLo0KHw9vaGj48Pli5diuTkZIwePRrA46MuN27cwOrVq9XzJCYmAgCysrJw+/ZtJCYmwsjICE2aNNHHJhAREVEVo9dwM2DAAKSnp2P27NlISUlBs2bNEBUVBTc3NwCPv7Tv6e+88fT0VP8eHx+PdevWwc3NDVeuXKnM0omIiKiK0vvVgiEhIQgJCSn2sZUrV2q1CSEquCIiIiKqzvT+7xeIiIiIyhPDDREREUkKww0RERFJCsMNERERSQrDDREREUkKww0RERFJCsMNERERSQrDDREREUkKww0RERFJCsMNERERSQrDDREREUkKww0RERFJCsMNERERSQrDDREREUkKww0RERFJCsMNERERSQrDDREREUkKww0RERFJCsMNERERSQrDDREREUkKww0RERFJCsMNERERSQrDDREREUkKww0RERFJCsMNERERSQrDDREREUkKww0RERFJCsMNERERSQrDDREREUkKww0RERFJCsMNERERSQrDDREREUkKww0RERFJCsMNERERSQrDDREREUkKww0RERFJiqG+CyB6HnWm7NB3Cc/BEOOPROu7CCIiyeORGyIiIpIUhhsiIiKSFIYbIiIikhSGGyIiIpIUhhsiIiKSFIYbIiIikhSGGyIiIpIUhhsiIiKSFIYbIiIikhSGGyIiIpIUhhsiIiKSFIYbIiIikhSGGyIiIpIUhhsiIiKSFIYbIiIikhSGGyIiIpIUhhsiIiKSFIYbIiIikhSGGyIiIpIUhhsiIiKSFIYbIiIikhSGGyIiIpIUvYebRYsWwd3dHSYmJvDy8sLhw4dL7X/w4EF4eXnBxMQEdevWxffff19JlRIREVF1oNdws2HDBkyYMAHTpk1DQkICfH190aNHDyQnJxfbPykpCYGBgfD19UVCQgI++eQTfPDBB9i0aVMlV05ERERVlV7DTUREBIKDgzFq1Ch4eHggMjISrq6uWLx4cbH9v//+e9SuXRuRkZHw8PDAqFGjMHLkSMyfP7+SKyciIqKqSm/hJi8vD/Hx8QgICNBoDwgIQGxsbLHzHDlyRKt/t27dEBcXB5VKVWG1EhERUfVhqK8V37lzBwUFBXB0dNRod3R0RGpqarHzpKamFts/Pz8fd+7cgbOzs9Y8ubm5yM3NVU8/ePAAAHD37t0KCURe846W+zIrniHGH4nWdxFERCQR6enp5b7MzMxMAIAQ4pl99RZuishkMo1pIYRW27P6F9deJDw8HLNmzdJqd3d317VUIiIiKgO7Lypu2ZmZmbCysiq1j97CjZ2dHeRyudZRmrS0NK2jM0WcnJyK7W9oaAhbW9ti55k6dSpCQ0PV04WFhbh79y5sbW1LDVEvi4yMDLi6uuLatWuwtLTUdzmS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", 159 | "text/plain": [ 160 | "Figure(PyObject
)" 161 | ] 162 | }, 163 | "metadata": {}, 164 | "output_type": "display_data" 165 | } 166 | ], 167 | "source": [ 168 | "plot_num_turns(\n", 169 | " num_turns, \n", 170 | " title_text=\"5-turn solution for hard mode, starting with SCAMP\",\n", 171 | " saved_filename=\"strat_using_solutions_only_hard_mode_optimal.png\"\n", 172 | ")" 173 | ] 174 | }, 175 | { 176 | "cell_type": "code", 177 | "execution_count": 7, 178 | "id": "79b0fc5c", 179 | "metadata": {}, 180 | "outputs": [], 181 | "source": [ 182 | "open(\"hard_mode_strategy.md\", \"w\") do io\n", 183 | " println(io, get_strategy_text([guess], strat, print_prefix = true))\n", 184 | "end" 185 | ] 186 | }, 187 | { 188 | "cell_type": "markdown", 189 | "id": "7473eabf", 190 | "metadata": {}, 191 | "source": [ 192 | "# Hard mode, guess any 5-letter word, `turns_budget` = 4\n", 193 | "\n", 194 | "We revisit the situation where our budget is 4 turns, this time relaxing the constraint that we guess only potential solutions. It takes a bit more time, but we can show that no strategy guarantees a solution in 4 steps - even when we allow guessing all ~13k valid 5-letter words (rather than just the ~2.3k potential solutions)!" 195 | ] 196 | }, 197 | { 198 | "cell_type": "code", 199 | "execution_count": 7, 200 | "id": "7a42c810", 201 | "metadata": {}, 202 | "outputs": [ 203 | { 204 | "name": "stdout", 205 | "output_type": "stream", 206 | "text": [ 207 | " 86.447014 seconds (996.38 M allocations: 80.041 GiB, 18.71% gc time)\n" 208 | ] 209 | } 210 | ], 211 | "source": [ 212 | "cache_word_scores(ALL_WORDS, ALL_WORDS)" 213 | ] 214 | }, 215 | { 216 | "cell_type": "code", 217 | "execution_count": 8, 218 | "id": "28a289f9", 219 | "metadata": {}, 220 | "outputs": [ 221 | { 222 | "name": "stderr", 223 | "output_type": "stream", 224 | "text": [ 225 | "\u001b[32mProgress: 100%|█████████████████████████████████████████| Time: 2:39:58\u001b[39m\n", 226 | "\u001b[34m best_guess: N/A\u001b[39m\n", 227 | "\u001b[34m best_max_num_turns: N/A\u001b[39m\n", 228 | "\u001b[34m best_average_num_turns: N/A\u001b[39m\n", 229 | "\u001b[34m num_skipped: 12972\u001b[39m\n", 230 | "\u001b[34m valid_guesses: Tuple{String, Float64, Float64}[]\u001b[39m\n" 231 | ] 232 | } 233 | ], 234 | "source": [ 235 | "r = get_optimal_strategy_exhaustive(\n", 236 | " ALL_WORD_IDXS, \n", 237 | " SOLUTION_WORD_IDXS, \n", 238 | " hard_mode=true, \n", 239 | " turns_budget=4,\n", 240 | " show_progress=true\n", 241 | ")" 242 | ] 243 | }, 244 | { 245 | "cell_type": "code", 246 | "execution_count": null, 247 | "id": "503363db", 248 | "metadata": {}, 249 | "outputs": [], 250 | "source": [] 251 | }, 252 | { 253 | "cell_type": "markdown", 254 | "id": "73e6f8a5-6da0-4bf4-b37d-0d8e8c348778", 255 | "metadata": {}, 256 | "source": [ 257 | "# Easy mode, guess any 5-letter word, `turns_budget` = 4" 258 | ] 259 | }, 260 | { 261 | "cell_type": "code", 262 | "execution_count": 1, 263 | "id": "7704ec2b-5501-434a-a3dd-659987cc8d1b", 264 | "metadata": {}, 265 | "outputs": [], 266 | "source": [ 267 | "include(\"utils.jl\");\n", 268 | "ProgressMeter.ijulia_behavior(:clear);" 269 | ] 270 | }, 271 | { 272 | "cell_type": "code", 273 | "execution_count": 2, 274 | "id": "c5f50e23-7b3c-4595-bf42-59833061d1d4", 275 | "metadata": {}, 276 | "outputs": [ 277 | { 278 | "name": "stdout", 279 | "output_type": "stream", 280 | "text": [ 281 | " 12.488186 seconds (172.37 M allocations: 14.203 GiB, 8.42% gc time, 0.03% compilation time)\n", 282 | "Updated MAX_NUM_SHARDS to 150.\n" 283 | ] 284 | } 285 | ], 286 | "source": [ 287 | "cache_word_scores(ALL_WORDS, SOLUTION_WORDS)\n", 288 | "optimize_max_num_shards()" 289 | ] 290 | }, 291 | { 292 | "cell_type": "code", 293 | "execution_count": 3, 294 | "id": "caa91989-5112-4173-bbd7-afe3b5414f5d", 295 | "metadata": {}, 296 | "outputs": [ 297 | { 298 | "name": "stderr", 299 | "output_type": "stream", 300 | "text": [ 301 | "\u001b[32mProgress: 100%|█████████████████████████████████████████| Time: 0:26:29\u001b[39m\n", 302 | "\u001b[34m best_guess: N/A\u001b[39m\n", 303 | "\u001b[34m best_max_num_turns: N/A\u001b[39m\n", 304 | "\u001b[34m best_average_num_turns: N/A\u001b[39m\n", 305 | "\u001b[34m num_skipped: 12972\u001b[39m\n", 306 | "\u001b[34m valid_guesses: Tuple{String, Float64, Float64}[]\u001b[39m\n" 307 | ] 308 | }, 309 | { 310 | "name": "stdout", 311 | "output_type": "stream", 312 | "text": [ 313 | "cigar: no solution\n" 314 | ] 315 | } 316 | ], 317 | "source": [ 318 | "initial_guess = 1\n", 319 | "\n", 320 | "r = get_optimal_strategy_exhaustive_helper(\n", 321 | " ALL_WORD_IDXS,\n", 322 | " SOLUTION_WORD_IDXS,\n", 323 | " initial_guess,\n", 324 | " hard_mode = false,\n", 325 | " turns_budget = 4\n", 326 | ")\n", 327 | "if isnothing(r)\n", 328 | " println(ALL_WORDS[initial_guess], \": no solution\")\n", 329 | "else\n", 330 | " best_num_turns, best_strat = r\n", 331 | " print_strategy(initial_guess, best_strat)\n", 332 | "end" 333 | ] 334 | }, 335 | { 336 | "cell_type": "code", 337 | "execution_count": null, 338 | "id": "24971763-27f7-41f1-b7eb-99aebc4b3fa8", 339 | "metadata": {}, 340 | "outputs": [], 341 | "source": [] 342 | } 343 | ], 344 | "metadata": { 345 | "kernelspec": { 346 | "display_name": "Julia 1.7.1", 347 | "language": "julia", 348 | "name": "julia-1.7" 349 | }, 350 | "language_info": { 351 | "file_extension": ".jl", 352 | "mimetype": "application/julia", 353 | "name": "julia", 354 | "version": "1.7.1" 355 | } 356 | }, 357 | "nbformat": 4, 358 | "nbformat_minor": 5 359 | } 360 | --------------------------------------------------------------------------------